• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stacks-network / stacks-core / 23490236853

24 Mar 2026 12:48PM UTC coverage: 85.693% (+0.008%) from 85.685%
23490236853

push

github

web-flow
Merge pull request #6927 from federico-stacks/test/multi-btc-core-rpc

ci: multi Bitcoin Core RPC testing

186538 of 217682 relevant lines covered (85.69%)

17310026.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.17
/stackslib/src/util_lib/strings.rs
1
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
2
// Copyright (C) 2020-2026 Stacks Open Internet Foundation
3
//
4
// This program is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// This program is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

17
use std::borrow::Borrow;
18
use std::fmt;
19
use std::io::{Read, Write};
20
use std::ops::{Deref, DerefMut};
21

22
use clarity::vm::errors::ClarityTypeError;
23
use clarity::vm::representations::{
24
    ClarityName, ContractName, MAX_STRING_LEN as CLARITY_MAX_STRING_LENGTH,
25
};
26
use lazy_static::lazy_static;
27
use regex::Regex;
28
use stacks_common::codec::{
29
    read_next, write_next, Error as codec_error, StacksMessageCodec, MAX_MESSAGE_LEN,
30
};
31
use stacks_common::util::retry::BoundReader;
32
use url;
33

34
lazy_static! {
35
    static ref URL_STRING_REGEX: Regex =
36
        Regex::new(r#"^[a-zA-Z0-9._~:/?#\[\]@!$&'()*+,;%=-]*$"#).unwrap();
37
}
38

39
guarded_string!(
40
    UrlString,
41
    URL_STRING_REGEX,
42
    CLARITY_MAX_STRING_LENGTH,
43
    ClarityTypeError,
44
    ClarityTypeError::InvalidUrlString
45
);
46

47
/// printable-ASCII-only string, but encodable.
48
/// Note that it cannot be longer than ARRAY_MAX_LEN (4.1 billion bytes)
49
#[derive(Clone, PartialEq, Serialize, Deserialize)]
50
pub struct StacksString(Vec<u8>);
51

52
pub struct VecDisplay<'a, T: fmt::Display>(pub &'a [T]);
53

54
impl<T: fmt::Display> fmt::Display for VecDisplay<'_, T> {
55
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
59,598✔
56
        write!(f, "[")?;
59,598✔
57
        for (ix, val) in self.0.iter().enumerate() {
300,810✔
58
            if ix == 0 {
299,154✔
59
                write!(f, "{}", val)?;
44,633✔
60
            } else {
61
                write!(f, ", {}", val)?;
254,521✔
62
            }
63
        }
64
        write!(f, "]")
59,598✔
65
    }
59,598✔
66
}
67

68
impl fmt::Display for StacksString {
69
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
70
        f.write_str(String::from_utf8_lossy(self).into_owned().as_str())
×
71
    }
×
72
}
73

74
impl fmt::Debug for StacksString {
75
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46,187✔
76
        f.write_str(String::from_utf8_lossy(self).into_owned().as_str())
46,187✔
77
    }
46,187✔
78
}
79

80
impl Deref for StacksString {
81
    type Target = Vec<u8>;
82
    fn deref(&self) -> &Vec<u8> {
46,188✔
83
        &self.0
46,188✔
84
    }
46,188✔
85
}
86

87
impl DerefMut for StacksString {
88
    fn deref_mut(&mut self) -> &mut Vec<u8> {
×
89
        &mut self.0
×
90
    }
×
91
}
92

93
impl StacksMessageCodec for StacksString {
94
    fn consensus_serialize<W: Write>(&self, fd: &mut W) -> Result<(), codec_error> {
1,542,475✔
95
        write_next(fd, &self.0)
1,542,475✔
96
    }
1,542,475✔
97

98
    fn consensus_deserialize<R: Read>(fd: &mut R) -> Result<StacksString, codec_error> {
389,661✔
99
        let bytes: Vec<u8> = {
387,885✔
100
            let mut bound_read = BoundReader::from_reader(fd, MAX_MESSAGE_LEN as u64);
389,661✔
101
            read_next(&mut bound_read)
389,661✔
102
        }?;
1,776✔
103

104
        // must encode a valid string
105
        let s = String::from_utf8(bytes.clone()).map_err(|_e| {
387,885✔
106
            warn!("Invalid StacksString -- could not build from utf8");
×
107
            codec_error::DeserializeError(
×
108
                "Invalid Stacks string: could not build from utf8".to_string(),
×
109
            )
×
110
        })?;
×
111

112
        if !StacksString::is_valid_string(&s) {
387,885✔
113
            // non-printable ASCII or not ASCII
114
            warn!("Invalid StacksString -- non-printable ASCII or non-ASCII");
×
115
            return Err(codec_error::DeserializeError(
×
116
                "Invalid Stacks string: non-printable or non-ASCII string".to_string(),
×
117
            ));
×
118
        }
387,885✔
119

120
        Ok(StacksString(bytes))
387,885✔
121
    }
389,661✔
122
}
123

124
impl StacksMessageCodec for UrlString {
125
    fn consensus_serialize<W: Write>(&self, fd: &mut W) -> Result<(), codec_error> {
1,473,603✔
126
        // UrlString can't be longer than vm::representations::MAX_STRING_LEN, which itself is
127
        // a u8, so we should be good here.
128
        if self.as_bytes().len() > CLARITY_MAX_STRING_LENGTH as usize {
1,473,603✔
129
            return Err(codec_error::SerializeError(
×
130
                "Failed to serialize URL string: too long".to_string(),
×
131
            ));
×
132
        }
1,473,603✔
133

134
        // must be a valid block URL, or empty string
135
        if !self.as_bytes().is_empty() {
1,473,603✔
136
            let _ = self.parse_to_block_url()?;
1,473,603✔
137
        }
×
138

139
        write_next(fd, &(self.as_bytes().len() as u8))?;
1,473,603✔
140
        fd.write_all(self.as_bytes())
1,473,603✔
141
            .map_err(codec_error::WriteError)?;
1,473,603✔
142
        Ok(())
1,473,603✔
143
    }
1,473,603✔
144

145
    fn consensus_deserialize<R: Read>(fd: &mut R) -> Result<UrlString, codec_error> {
729,156✔
146
        let len_byte: u8 = read_next(fd)?;
729,156✔
147
        if len_byte > CLARITY_MAX_STRING_LENGTH {
729,156✔
148
            return Err(codec_error::DeserializeError(
×
149
                "Failed to deserialize URL string: too long".to_string(),
×
150
            ));
×
151
        }
729,156✔
152
        let mut bytes = vec![0u8; len_byte as usize];
729,156✔
153
        fd.read_exact(&mut bytes).map_err(codec_error::ReadError)?;
729,156✔
154

155
        // must encode a valid string
156
        let s = String::from_utf8(bytes).map_err(|_e| {
729,154✔
157
            codec_error::DeserializeError(
×
158
                "Failed to parse URL string: could not construct from utf8".to_string(),
×
159
            )
×
160
        })?;
×
161

162
        // must decode to a URL
163
        let url = UrlString::try_from(s).map_err(|e| {
729,154✔
164
            codec_error::DeserializeError(format!("Failed to parse URL string: {:?}", e))
×
165
        })?;
×
166

167
        // must be a valid block URL, or empty string
168
        if !url.is_empty() {
729,154✔
169
            let _ = url.parse_to_block_url()?;
729,154✔
170
        }
×
171
        Ok(url)
729,154✔
172
    }
729,156✔
173
}
174

175
impl From<ClarityName> for StacksString {
176
    fn from(clarity_name: ClarityName) -> StacksString {
252,619✔
177
        // .unwrap() is safe since StacksString is less strict
178
        StacksString::from_str(&clarity_name).unwrap()
252,619✔
179
    }
252,619✔
180
}
181

182
impl From<ContractName> for StacksString {
183
    fn from(contract_name: ContractName) -> StacksString {
×
184
        // .unwrap() is safe since StacksString is less strict
185
        StacksString::from_str(&contract_name).unwrap()
×
186
    }
×
187
}
188

189
impl StacksString {
190
    /// Is the given string a valid Clarity string?
191
    pub fn is_valid_string(s: &String) -> bool {
888,479✔
192
        s.is_ascii() && StacksString::is_printable(s)
888,479✔
193
    }
888,479✔
194

195
    pub fn is_printable(s: &String) -> bool {
888,479✔
196
        if !s.is_ascii() {
888,479✔
197
            return false;
×
198
        }
888,479✔
199
        // all characters must be ASCII "printable" characters, excluding "delete".
200
        // This is 0x20 through 0x7e, inclusive, as well as '\t' and '\n'
201
        // TODO: DRY up with vm::representations
202
        for c in s.as_bytes().iter() {
2,147,483,647✔
203
            if (*c < 0x20 && *c != b'\t' && *c != b'\n') || *c > 0x7e {
2,147,483,647✔
204
                return false;
2✔
205
            }
2,147,483,647✔
206
        }
207
        true
888,477✔
208
    }
888,479✔
209

210
    pub fn is_clarity_variable(&self) -> bool {
252,619✔
211
        ClarityName::try_from(self.to_string()).is_ok()
252,619✔
212
    }
252,619✔
213

214
    pub fn from_string(s: &String) -> Option<StacksString> {
4,500✔
215
        if !StacksString::is_valid_string(s) {
4,500✔
216
            return None;
×
217
        }
4,500✔
218
        Some(StacksString(s.as_bytes().to_vec()))
4,500✔
219
    }
4,500✔
220

221
    pub fn from_str(s: &str) -> Option<StacksString> {
496,094✔
222
        if !StacksString::is_valid_string(&String::from(s)) {
496,094✔
223
            return None;
2✔
224
        }
496,092✔
225
        Some(StacksString(s.as_bytes().to_vec()))
496,092✔
226
    }
496,094✔
227

228
    pub fn to_string(&self) -> String {
512,333✔
229
        // guaranteed to always succeed because the string is ASCII
230
        String::from_utf8(self.0.clone()).unwrap()
512,333✔
231
    }
512,333✔
232
}
233

234
impl UrlString {
235
    /// Determine that the UrlString parses to something that can be used to fetch blocks via HTTP(S).
236
    /// A block URL must be an HTTP(S) URL without a query or fragment, and without a login.
237
    pub fn parse_to_block_url(&self) -> Result<url::Url, codec_error> {
2,259,415✔
238
        // even though this code uses from_utf8_unchecked() internally, we've already verified that
239
        // the bytes in this string are all ASCII.
240
        let url = url::Url::parse(&self.to_string())
2,259,415✔
241
            .map_err(|e| codec_error::DeserializeError(format!("Invalid URL: {:?}", &e)))?;
2,259,415✔
242

243
        if url.scheme() != "http" && url.scheme() != "https" {
2,259,413✔
244
            return Err(codec_error::DeserializeError(format!(
1✔
245
                "Invalid URL: invalid scheme '{}'",
1✔
246
                url.scheme()
1✔
247
            )));
1✔
248
        }
2,259,412✔
249

250
        if !url.username().is_empty() || url.password().is_some() {
2,259,412✔
251
            return Err(codec_error::DeserializeError(
2✔
252
                "Invalid URL: must not contain a username/password".to_string(),
2✔
253
            ));
2✔
254
        }
2,259,410✔
255

256
        if url.host_str().is_none() {
2,259,410✔
257
            return Err(codec_error::DeserializeError(
×
258
                "Invalid URL: no host string".to_string(),
×
259
            ));
×
260
        }
2,259,410✔
261

262
        if url.query().is_some() {
2,259,410✔
263
            return Err(codec_error::DeserializeError(
1✔
264
                "Invalid URL: query strings not supported for block URLs".to_string(),
1✔
265
            ));
1✔
266
        }
2,259,409✔
267

268
        if url.fragment().is_some() {
2,259,409✔
269
            return Err(codec_error::DeserializeError(
1✔
270
                "Invalid URL: fragments are not supported for block URLs".to_string(),
1✔
271
            ));
1✔
272
        }
2,259,408✔
273

274
        Ok(url)
2,259,408✔
275
    }
2,259,415✔
276

277
    /// Is this URL routable?
278
    /// i.e. is the host _not_ 0.0.0.0 or ::?
279
    pub fn has_routable_host(&self) -> bool {
729,600✔
280
        let url = match url::Url::parse(&self.to_string()) {
729,600✔
281
            Ok(x) => x,
729,600✔
282
            Err(_) => {
283
                // should be unreachable
284
                return false;
×
285
            }
286
        };
287
        match url.host_str() {
729,600✔
288
            Some(host_str) => {
729,600✔
289
                if host_str == "0.0.0.0" || host_str == "[::]" || host_str == "::" {
729,600✔
290
                    return false;
×
291
                } else {
292
                    return true;
729,600✔
293
                }
294
            }
295
            None => {
296
                return false;
×
297
            }
298
        }
299
    }
729,600✔
300

301
    /// Get the port. Returns 0 for unknown
302
    pub fn get_port(&self) -> Option<u16> {
×
303
        let url = match url::Url::parse(&self.to_string()) {
×
304
            Ok(x) => x,
×
305
            Err(_) => {
306
                // unknown, but should be unreachable anyway
307
                return None;
×
308
            }
309
        };
310
        url.port_or_known_default()
×
311
    }
×
312
}
313

314
#[cfg(test)]
315
mod test {
316
    use clarity::vm::representations::CONTRACT_MAX_NAME_LENGTH;
317

318
    use super::*;
319
    use crate::net::codec::test::check_codec_and_corruption;
320

321
    #[test]
322
    fn tx_stacks_strings_codec() {
1✔
323
        let s = "hello-world";
1✔
324
        let stacks_str = StacksString::from_str(s).unwrap();
1✔
325
        let clarity_str = ClarityName::try_from(s).unwrap();
1✔
326
        let contract_str = ContractName::try_from(s).unwrap();
1✔
327

328
        assert_eq!(stacks_str[..], s.as_bytes().to_vec()[..]);
1✔
329
        let s2 = stacks_str.to_string();
1✔
330
        assert_eq!(s2, s.to_string());
1✔
331

332
        // stacks strings have a 4-byte length prefix
333
        let mut b = vec![];
1✔
334
        stacks_str.consensus_serialize(&mut b).unwrap();
1✔
335
        let mut bytes = vec![0x00, 0x00, 0x00, s.len() as u8];
1✔
336
        bytes.extend_from_slice(s.as_bytes());
1✔
337

338
        check_codec_and_corruption::<StacksString>(&stacks_str, &bytes);
1✔
339

340
        // clarity names and contract names have a 1-byte length prefix
341
        let mut clarity_bytes = vec![s.len() as u8];
1✔
342
        clarity_bytes.extend_from_slice(clarity_str.as_bytes());
1✔
343
        check_codec_and_corruption::<ClarityName>(&clarity_str, &clarity_bytes);
1✔
344

345
        let mut contract_bytes = vec![s.len() as u8];
1✔
346
        contract_bytes.extend_from_slice(contract_str.as_bytes());
1✔
347
        check_codec_and_corruption::<ContractName>(&contract_str, &contract_bytes);
1✔
348
    }
1✔
349

350
    #[test]
351
    fn tx_stacks_string_invalid() {
1✔
352
        let s = "hello\rworld";
1✔
353
        assert!(StacksString::from_str(s).is_none());
1✔
354

355
        let s = "hello\x01world";
1✔
356
        assert!(StacksString::from_str(s).is_none());
1✔
357
    }
1✔
358

359
    #[test]
360
    fn test_contract_name_invalid() {
1✔
361
        let s = [0u8];
1✔
362
        assert!(ContractName::consensus_deserialize(&mut &s[..]).is_err());
1✔
363

364
        let s = [5u8, 0x66, 0x6f, 0x6f, 0x6f, 0x6f]; // "foooo"
1✔
365
        assert!(ContractName::consensus_deserialize(&mut &s[..]).is_ok());
1✔
366

367
        let s_body = [0x6fu8; CONTRACT_MAX_NAME_LENGTH + 1];
1✔
368
        let mut s_payload = vec![s_body.len() as u8];
1✔
369
        s_payload.extend_from_slice(&s_body);
1✔
370

371
        assert!(ContractName::consensus_deserialize(&mut &s_payload[..]).is_err());
1✔
372
    }
1✔
373

374
    #[test]
375
    fn test_url_parse() {
1✔
376
        assert!(UrlString::try_from("asdfjkl;")
1✔
377
            .unwrap()
1✔
378
            .parse_to_block_url()
1✔
379
            .unwrap_err()
1✔
380
            .to_string()
1✔
381
            .find("Invalid URL")
1✔
382
            .is_some());
1✔
383
        assert!(UrlString::try_from("http://")
1✔
384
            .unwrap()
1✔
385
            .parse_to_block_url()
1✔
386
            .unwrap_err()
1✔
387
            .to_string()
1✔
388
            .find("Invalid URL")
1✔
389
            .is_some());
1✔
390
        assert!(UrlString::try_from("ftp://ftp.google.com")
1✔
391
            .unwrap()
1✔
392
            .parse_to_block_url()
1✔
393
            .unwrap_err()
1✔
394
            .to_string()
1✔
395
            .find("invalid scheme")
1✔
396
            .is_some());
1✔
397
        assert!(UrlString::try_from("http://jude@google.com")
1✔
398
            .unwrap()
1✔
399
            .parse_to_block_url()
1✔
400
            .unwrap_err()
1✔
401
            .to_string()
1✔
402
            .find("must not contain a username/password")
1✔
403
            .is_some());
1✔
404
        assert!(UrlString::try_from("http://jude:pw@google.com")
1✔
405
            .unwrap()
1✔
406
            .parse_to_block_url()
1✔
407
            .unwrap_err()
1✔
408
            .to_string()
1✔
409
            .find("must not contain a username/password")
1✔
410
            .is_some());
1✔
411
        assert!(UrlString::try_from("http://www.google.com/foo/bar?baz=goo")
1✔
412
            .unwrap()
1✔
413
            .parse_to_block_url()
1✔
414
            .unwrap_err()
1✔
415
            .to_string()
1✔
416
            .find("query strings not supported")
1✔
417
            .is_some());
1✔
418
        assert!(UrlString::try_from("http://www.google.com/foo/bar#baz")
1✔
419
            .unwrap()
1✔
420
            .parse_to_block_url()
1✔
421
            .unwrap_err()
1✔
422
            .to_string()
1✔
423
            .find("fragments are not supported")
1✔
424
            .is_some());
1✔
425

426
        // don't need to cover the happy path too much, since the rust-url package already tests it.
427
        let url = UrlString::try_from("http://127.0.0.1:1234/v2/info")
1✔
428
            .unwrap()
1✔
429
            .parse_to_block_url()
1✔
430
            .unwrap();
1✔
431
        assert_eq!(url.host_str(), Some("127.0.0.1"));
1✔
432
        assert_eq!(url.port(), Some(1234));
1✔
433
        assert_eq!(url.path(), "/v2/info");
1✔
434
        assert_eq!(url.scheme(), "http");
1✔
435
    }
1✔
436
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc