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

stacks-network / stacks-core / 25903914664-1

15 May 2026 06:28AM UTC coverage: 47.122% (-38.8%) from 85.959%
25903914664-1

Pull #7199

github

94e391
web-flow
Merge 109f2828c into 1c7b8e6ac
Pull Request #7199: Feat: L1 and L2 early unlocks, updating signer

103343 of 219309 relevant lines covered (47.12%)

12880462.62 hits per line

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

39.15
/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 {
40,297✔
56
        write!(f, "[")?;
40,297✔
57
        for (ix, val) in self.0.iter().enumerate() {
209,332✔
58
            if ix == 0 {
207,838✔
59
                write!(f, "{}", val)?;
29,389✔
60
            } else {
61
                write!(f, ", {}", val)?;
178,449✔
62
            }
63
        }
64
        write!(f, "]")
40,297✔
65
    }
40,297✔
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 {
43,758✔
76
        f.write_str(String::from_utf8_lossy(self).into_owned().as_str())
43,758✔
77
    }
43,758✔
78
}
79

80
impl Deref for StacksString {
81
    type Target = Vec<u8>;
82
    fn deref(&self) -> &Vec<u8> {
43,758✔
83
        &self.0
43,758✔
84
    }
43,758✔
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,265,250✔
95
        write_next(fd, &self.0)
1,265,250✔
96
    }
1,265,250✔
97

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

104
        // must encode a valid string
105
        let s = String::from_utf8(bytes.clone()).map_err(|_e| {
287,469✔
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) {
287,469✔
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
        }
287,469✔
119

120
        Ok(StacksString(bytes))
287,469✔
121
    }
287,469✔
122
}
123

124
impl StacksMessageCodec for UrlString {
125
    fn consensus_serialize<W: Write>(&self, fd: &mut W) -> Result<(), codec_error> {
1,522,233✔
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,522,233✔
129
            return Err(codec_error::SerializeError(
×
130
                "Failed to serialize URL string: too long".to_string(),
×
131
            ));
×
132
        }
1,522,233✔
133

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

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

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

155
        // must encode a valid string
156
        let s = String::from_utf8(bytes).map_err(|_e| {
753,127✔
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| {
753,127✔
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() {
753,127✔
169
            let _ = url.parse_to_block_url()?;
753,127✔
170
        }
×
171
        Ok(url)
753,127✔
172
    }
753,127✔
173
}
174

175
impl From<ClarityName> for StacksString {
176
    fn from(clarity_name: ClarityName) -> StacksString {
212,766✔
177
        // .unwrap() is safe since StacksString is less strict
178
        StacksString::from_str(&clarity_name).unwrap()
212,766✔
179
    }
212,766✔
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 {
705,891✔
192
        s.is_ascii() && StacksString::is_printable(s)
705,891✔
193
    }
705,891✔
194

195
    pub fn is_printable(s: &String) -> bool {
705,891✔
196
        if !s.is_ascii() {
705,891✔
197
            return false;
×
198
        }
705,891✔
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;
×
205
            }
2,147,483,647✔
206
        }
207
        true
705,891✔
208
    }
705,891✔
209

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

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

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

228
    pub fn to_string(&self) -> String {
424,005✔
229
        // guaranteed to always succeed because the string is ASCII
230
        String::from_utf8(self.0.clone()).unwrap()
424,005✔
231
    }
424,005✔
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,334,284✔
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,334,284✔
241
            .map_err(|e| codec_error::DeserializeError(format!("Invalid URL: {:?}", &e)))?;
2,334,284✔
242

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

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

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

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

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

274
        Ok(url)
2,334,284✔
275
    }
2,334,284✔
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 {
753,727✔
280
        let url = match url::Url::parse(&self.to_string()) {
753,727✔
281
            Ok(x) => x,
753,727✔
282
            Err(_) => {
283
                // should be unreachable
284
                return false;
×
285
            }
286
        };
287
        match url.host_str() {
753,727✔
288
            Some(host_str) => {
753,727✔
289
                if host_str == "0.0.0.0" || host_str == "[::]" || host_str == "::" {
753,727✔
290
                    return false;
×
291
                } else {
292
                    return true;
753,727✔
293
                }
294
            }
295
            None => {
296
                return false;
×
297
            }
298
        }
299
    }
753,727✔
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() {
×
323
        let s = "hello-world";
×
324
        let stacks_str = StacksString::from_str(s).unwrap();
×
325
        let clarity_str = ClarityName::try_from(s).unwrap();
×
326
        let contract_str = ContractName::try_from(s).unwrap();
×
327

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

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

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

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

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

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

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

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

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

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

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

374
    #[test]
375
    fn test_url_parse() {
×
376
        assert!(UrlString::try_from("asdfjkl;")
×
377
            .unwrap()
×
378
            .parse_to_block_url()
×
379
            .unwrap_err()
×
380
            .to_string()
×
381
            .find("Invalid URL")
×
382
            .is_some());
×
383
        assert!(UrlString::try_from("http://")
×
384
            .unwrap()
×
385
            .parse_to_block_url()
×
386
            .unwrap_err()
×
387
            .to_string()
×
388
            .find("Invalid URL")
×
389
            .is_some());
×
390
        assert!(UrlString::try_from("ftp://ftp.google.com")
×
391
            .unwrap()
×
392
            .parse_to_block_url()
×
393
            .unwrap_err()
×
394
            .to_string()
×
395
            .find("invalid scheme")
×
396
            .is_some());
×
397
        assert!(UrlString::try_from("http://jude@google.com")
×
398
            .unwrap()
×
399
            .parse_to_block_url()
×
400
            .unwrap_err()
×
401
            .to_string()
×
402
            .find("must not contain a username/password")
×
403
            .is_some());
×
404
        assert!(UrlString::try_from("http://jude:pw@google.com")
×
405
            .unwrap()
×
406
            .parse_to_block_url()
×
407
            .unwrap_err()
×
408
            .to_string()
×
409
            .find("must not contain a username/password")
×
410
            .is_some());
×
411
        assert!(UrlString::try_from("http://www.google.com/foo/bar?baz=goo")
×
412
            .unwrap()
×
413
            .parse_to_block_url()
×
414
            .unwrap_err()
×
415
            .to_string()
×
416
            .find("query strings not supported")
×
417
            .is_some());
×
418
        assert!(UrlString::try_from("http://www.google.com/foo/bar#baz")
×
419
            .unwrap()
×
420
            .parse_to_block_url()
×
421
            .unwrap_err()
×
422
            .to_string()
×
423
            .find("fragments are not supported")
×
424
            .is_some());
×
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")
×
428
            .unwrap()
×
429
            .parse_to_block_url()
×
430
            .unwrap();
×
431
        assert_eq!(url.host_str(), Some("127.0.0.1"));
×
432
        assert_eq!(url.port(), Some(1234));
×
433
        assert_eq!(url.path(), "/v2/info");
×
434
        assert_eq!(url.scheme(), "http");
×
435
    }
×
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