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

stacks-network / stacks-core / 23524375303

25 Mar 2026 04:05AM UTC coverage: 70.787% (-15.1%) from 85.84%
23524375303

Pull #7036

github

web-flow
Merge a2f94584c into 91ed8c178
Pull Request #7036: Feat/improved const callable

28 of 29 new or added lines in 5 files covered. (96.55%)

32970 existing lines in 242 files now uncovered.

154677 of 218510 relevant lines covered (70.79%)

14759359.98 hits per line

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

47.29
/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 {
48,539✔
56
        write!(f, "[")?;
48,539✔
57
        for (ix, val) in self.0.iter().enumerate() {
246,128✔
58
            if ix == 0 {
244,679✔
59
                write!(f, "{}", val)?;
35,860✔
60
            } else {
61
                write!(f, ", {}", val)?;
208,819✔
62
            }
63
        }
64
        write!(f, "]")
48,539✔
65
    }
48,539✔
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 {
42,782✔
76
        f.write_str(String::from_utf8_lossy(self).into_owned().as_str())
42,782✔
77
    }
42,782✔
78
}
79

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

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

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

120
        Ok(StacksString(bytes))
304,751✔
121
    }
305,269✔
122
}
123

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

274
        Ok(url)
2,273,902✔
275
    }
2,273,902✔
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 {
735,881✔
280
        let url = match url::Url::parse(&self.to_string()) {
735,881✔
281
            Ok(x) => x,
735,881✔
282
            Err(_) => {
283
                // should be unreachable
284
                return false;
×
285
            }
286
        };
287
        match url.host_str() {
735,881✔
288
            Some(host_str) => {
735,881✔
289
                if host_str == "0.0.0.0" || host_str == "[::]" || host_str == "::" {
735,881✔
290
                    return false;
×
291
                } else {
292
                    return true;
735,881✔
293
                }
294
            }
295
            None => {
296
                return false;
×
297
            }
298
        }
299
    }
735,881✔
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]
UNCOV
351
    fn tx_stacks_string_invalid() {
×
UNCOV
352
        let s = "hello\rworld";
×
UNCOV
353
        assert!(StacksString::from_str(s).is_none());
×
354

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

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

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

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

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

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

426
        // don't need to cover the happy path too much, since the rust-url package already tests it.
UNCOV
427
        let url = UrlString::try_from("http://127.0.0.1:1234/v2/info")
×
UNCOV
428
            .unwrap()
×
UNCOV
429
            .parse_to_block_url()
×
UNCOV
430
            .unwrap();
×
UNCOV
431
        assert_eq!(url.host_str(), Some("127.0.0.1"));
×
UNCOV
432
        assert_eq!(url.port(), Some(1234));
×
UNCOV
433
        assert_eq!(url.path(), "/v2/info");
×
UNCOV
434
        assert_eq!(url.scheme(), "http");
×
UNCOV
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