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

stacks-network / stacks-core / 26250451051-1

21 May 2026 08:11PM UTC coverage: 85.585% (-0.1%) from 85.712%
26250451051-1

Pull #7215

github

ec9d4c
web-flow
Merge 9487bf852 into af1280aac
Pull Request #7215: Chore: fix flake in non_blocking_minority_configured_to_favour_...

188844 of 220651 relevant lines covered (85.58%)

18975267.44 hits per line

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

86.1
/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;
21

22
use clarity::vm::errors::ClarityTypeError;
23
use clarity::vm::representations::MAX_STRING_LEN as CLARITY_MAX_STRING_LENGTH;
24
use lazy_static::lazy_static;
25
use regex::Regex;
26
pub use stacks_codec::strings::StacksString;
27
use stacks_common::codec::{read_next, write_next, Error as codec_error, StacksMessageCodec};
28
use url;
29

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

35
guarded_string!(
36
    UrlString,
37
    URL_STRING_REGEX,
38
    CLARITY_MAX_STRING_LENGTH,
39
    ClarityTypeError,
40
    ClarityTypeError::InvalidUrlString
41
);
42

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

45
impl<T: fmt::Display> fmt::Display for VecDisplay<'_, T> {
46
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
61,658✔
47
        write!(f, "[")?;
61,658✔
48
        for (ix, val) in self.0.iter().enumerate() {
315,776✔
49
            if ix == 0 {
314,255✔
50
                write!(f, "{}", val)?;
46,678✔
51
            } else {
52
                write!(f, ", {}", val)?;
267,577✔
53
            }
54
        }
55
        write!(f, "]")
61,658✔
56
    }
61,658✔
57
}
58

59
impl StacksMessageCodec for UrlString {
60
    fn consensus_serialize<W: Write>(&self, fd: &mut W) -> Result<(), codec_error> {
1,476,235✔
61
        // UrlString can't be longer than vm::representations::MAX_STRING_LEN, which itself is
62
        // a u8, so we should be good here.
63
        if self.as_bytes().len() > CLARITY_MAX_STRING_LENGTH as usize {
1,476,235✔
64
            return Err(codec_error::SerializeError(
×
65
                "Failed to serialize URL string: too long".to_string(),
×
66
            ));
×
67
        }
1,476,235✔
68

69
        // must be a valid block URL, or empty string
70
        if !self.as_bytes().is_empty() {
1,476,235✔
71
            let _ = self.parse_to_block_url()?;
1,476,235✔
72
        }
×
73

74
        write_next(fd, &(self.as_bytes().len() as u8))?;
1,476,235✔
75
        fd.write_all(self.as_bytes())
1,476,235✔
76
            .map_err(codec_error::WriteError)?;
1,476,235✔
77
        Ok(())
1,476,235✔
78
    }
1,476,235✔
79

80
    fn consensus_deserialize<R: Read>(fd: &mut R) -> Result<UrlString, codec_error> {
731,459✔
81
        let len_byte: u8 = read_next(fd)?;
731,459✔
82
        if len_byte > CLARITY_MAX_STRING_LENGTH {
731,459✔
83
            return Err(codec_error::DeserializeError(
×
84
                "Failed to deserialize URL string: too long".to_string(),
×
85
            ));
×
86
        }
731,459✔
87
        let mut bytes = vec![0u8; len_byte as usize];
731,459✔
88
        fd.read_exact(&mut bytes).map_err(codec_error::ReadError)?;
731,459✔
89

90
        // must encode a valid string
91
        let s = String::from_utf8(bytes).map_err(|_e| {
731,457✔
92
            codec_error::DeserializeError(
×
93
                "Failed to parse URL string: could not construct from utf8".to_string(),
×
94
            )
×
95
        })?;
×
96

97
        // must decode to a URL
98
        let url = UrlString::try_from(s).map_err(|e| {
731,457✔
99
            codec_error::DeserializeError(format!("Failed to parse URL string: {:?}", e))
×
100
        })?;
×
101

102
        // must be a valid block URL, or empty string
103
        if !url.is_empty() {
731,457✔
104
            let _ = url.parse_to_block_url()?;
731,457✔
105
        }
×
106
        Ok(url)
731,457✔
107
    }
731,459✔
108
}
109

110
impl UrlString {
111
    /// Determine that the UrlString parses to something that can be used to fetch blocks via HTTP(S).
112
    /// A block URL must be an HTTP(S) URL without a query or fragment, and without a login.
113
    pub fn parse_to_block_url(&self) -> Result<url::Url, codec_error> {
2,261,758✔
114
        // even though this code uses from_utf8_unchecked() internally, we've already verified that
115
        // the bytes in this string are all ASCII.
116
        let url = url::Url::parse(&self.to_string())
2,261,758✔
117
            .map_err(|e| codec_error::DeserializeError(format!("Invalid URL: {:?}", &e)))?;
2,261,758✔
118

119
        if url.scheme() != "http" && url.scheme() != "https" {
2,261,756✔
120
            return Err(codec_error::DeserializeError(format!(
1✔
121
                "Invalid URL: invalid scheme '{}'",
1✔
122
                url.scheme()
1✔
123
            )));
1✔
124
        }
2,261,755✔
125

126
        if !url.username().is_empty() || url.password().is_some() {
2,261,755✔
127
            return Err(codec_error::DeserializeError(
2✔
128
                "Invalid URL: must not contain a username/password".to_string(),
2✔
129
            ));
2✔
130
        }
2,261,753✔
131

132
        if url.host_str().is_none() {
2,261,753✔
133
            return Err(codec_error::DeserializeError(
×
134
                "Invalid URL: no host string".to_string(),
×
135
            ));
×
136
        }
2,261,753✔
137

138
        if url.query().is_some() {
2,261,753✔
139
            return Err(codec_error::DeserializeError(
1✔
140
                "Invalid URL: query strings not supported for block URLs".to_string(),
1✔
141
            ));
1✔
142
        }
2,261,752✔
143

144
        if url.fragment().is_some() {
2,261,752✔
145
            return Err(codec_error::DeserializeError(
1✔
146
                "Invalid URL: fragments are not supported for block URLs".to_string(),
1✔
147
            ));
1✔
148
        }
2,261,751✔
149

150
        Ok(url)
2,261,751✔
151
    }
2,261,758✔
152

153
    /// Is this URL routable?
154
    /// i.e. is the host _not_ 0.0.0.0 or ::?
155
    pub fn has_routable_host(&self) -> bool {
731,970✔
156
        let url = match url::Url::parse(&self.to_string()) {
731,970✔
157
            Ok(x) => x,
731,970✔
158
            Err(_) => {
159
                // should be unreachable
160
                return false;
×
161
            }
162
        };
163
        match url.host_str() {
731,970✔
164
            Some(host_str) => {
731,970✔
165
                if host_str == "0.0.0.0" || host_str == "[::]" || host_str == "::" {
731,970✔
166
                    return false;
×
167
                } else {
168
                    return true;
731,970✔
169
                }
170
            }
171
            None => {
172
                return false;
×
173
            }
174
        }
175
    }
731,970✔
176

177
    /// Get the port. Returns 0 for unknown
178
    pub fn get_port(&self) -> Option<u16> {
×
179
        let url = match url::Url::parse(&self.to_string()) {
×
180
            Ok(x) => x,
×
181
            Err(_) => {
182
                // unknown, but should be unreachable anyway
183
                return None;
×
184
            }
185
        };
186
        url.port_or_known_default()
×
187
    }
×
188
}
189

190
#[cfg(test)]
191
mod test {
192
    use clarity::vm::representations::{ClarityName, ContractName, CONTRACT_MAX_NAME_LENGTH};
193

194
    use super::*;
195
    use crate::net::codec::test::check_codec_and_corruption;
196

197
    #[test]
198
    fn tx_stacks_strings_codec() {
1✔
199
        let s = "hello-world";
1✔
200
        let stacks_str = StacksString::from_str(s).unwrap();
1✔
201
        let clarity_str = ClarityName::try_from(s).unwrap();
1✔
202
        let contract_str = ContractName::try_from(s).unwrap();
1✔
203

204
        assert_eq!(stacks_str[..], s.as_bytes().to_vec()[..]);
1✔
205
        let s2 = stacks_str.to_string();
1✔
206
        assert_eq!(s2, s.to_string());
1✔
207

208
        // stacks strings have a 4-byte length prefix
209
        let mut b = vec![];
1✔
210
        stacks_str.consensus_serialize(&mut b).unwrap();
1✔
211
        let mut bytes = vec![0x00, 0x00, 0x00, s.len() as u8];
1✔
212
        bytes.extend_from_slice(s.as_bytes());
1✔
213

214
        check_codec_and_corruption::<StacksString>(&stacks_str, &bytes);
1✔
215

216
        // clarity names and contract names have a 1-byte length prefix
217
        let mut clarity_bytes = vec![s.len() as u8];
1✔
218
        clarity_bytes.extend_from_slice(clarity_str.as_bytes());
1✔
219
        check_codec_and_corruption::<ClarityName>(&clarity_str, &clarity_bytes);
1✔
220

221
        let mut contract_bytes = vec![s.len() as u8];
1✔
222
        contract_bytes.extend_from_slice(contract_str.as_bytes());
1✔
223
        check_codec_and_corruption::<ContractName>(&contract_str, &contract_bytes);
1✔
224
    }
1✔
225

226
    #[test]
227
    fn tx_stacks_string_invalid() {
1✔
228
        let s = "hello\rworld";
1✔
229
        assert!(StacksString::from_str(s).is_none());
1✔
230

231
        let s = "hello\x01world";
1✔
232
        assert!(StacksString::from_str(s).is_none());
1✔
233
    }
1✔
234

235
    #[test]
236
    fn test_contract_name_invalid() {
1✔
237
        let s = [0u8];
1✔
238
        assert!(ContractName::consensus_deserialize(&mut &s[..]).is_err());
1✔
239

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

243
        let s_body = [0x6fu8; CONTRACT_MAX_NAME_LENGTH + 1];
1✔
244
        let mut s_payload = vec![s_body.len() as u8];
1✔
245
        s_payload.extend_from_slice(&s_body);
1✔
246

247
        assert!(ContractName::consensus_deserialize(&mut &s_payload[..]).is_err());
1✔
248
    }
1✔
249

250
    #[test]
251
    fn test_url_parse() {
1✔
252
        assert!(UrlString::try_from("asdfjkl;")
1✔
253
            .unwrap()
1✔
254
            .parse_to_block_url()
1✔
255
            .unwrap_err()
1✔
256
            .to_string()
1✔
257
            .find("Invalid URL")
1✔
258
            .is_some());
1✔
259
        assert!(UrlString::try_from("http://")
1✔
260
            .unwrap()
1✔
261
            .parse_to_block_url()
1✔
262
            .unwrap_err()
1✔
263
            .to_string()
1✔
264
            .find("Invalid URL")
1✔
265
            .is_some());
1✔
266
        assert!(UrlString::try_from("ftp://ftp.google.com")
1✔
267
            .unwrap()
1✔
268
            .parse_to_block_url()
1✔
269
            .unwrap_err()
1✔
270
            .to_string()
1✔
271
            .find("invalid scheme")
1✔
272
            .is_some());
1✔
273
        assert!(UrlString::try_from("http://jude@google.com")
1✔
274
            .unwrap()
1✔
275
            .parse_to_block_url()
1✔
276
            .unwrap_err()
1✔
277
            .to_string()
1✔
278
            .find("must not contain a username/password")
1✔
279
            .is_some());
1✔
280
        assert!(UrlString::try_from("http://jude:pw@google.com")
1✔
281
            .unwrap()
1✔
282
            .parse_to_block_url()
1✔
283
            .unwrap_err()
1✔
284
            .to_string()
1✔
285
            .find("must not contain a username/password")
1✔
286
            .is_some());
1✔
287
        assert!(UrlString::try_from("http://www.google.com/foo/bar?baz=goo")
1✔
288
            .unwrap()
1✔
289
            .parse_to_block_url()
1✔
290
            .unwrap_err()
1✔
291
            .to_string()
1✔
292
            .find("query strings not supported")
1✔
293
            .is_some());
1✔
294
        assert!(UrlString::try_from("http://www.google.com/foo/bar#baz")
1✔
295
            .unwrap()
1✔
296
            .parse_to_block_url()
1✔
297
            .unwrap_err()
1✔
298
            .to_string()
1✔
299
            .find("fragments are not supported")
1✔
300
            .is_some());
1✔
301

302
        // don't need to cover the happy path too much, since the rust-url package already tests it.
303
        let url = UrlString::try_from("http://127.0.0.1:1234/v2/info")
1✔
304
            .unwrap()
1✔
305
            .parse_to_block_url()
1✔
306
            .unwrap();
1✔
307
        assert_eq!(url.host_str(), Some("127.0.0.1"));
1✔
308
        assert_eq!(url.port(), Some(1234));
1✔
309
        assert_eq!(url.path(), "/v2/info");
1✔
310
        assert_eq!(url.scheme(), "http");
1✔
311
    }
1✔
312
}
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