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

stacks-network / stacks-core / 26529130192-2

27 May 2026 05:59PM UTC coverage: 85.602% (-0.1%) from 85.712%
26529130192-2

Pull #7219

github

76f7c5
web-flow
Merge 89424be8c into e2361219e
Pull Request #7219: test: Move relevant tests to `stacks-codec` and add some new ones

783 of 791 new or added lines in 3 files covered. (98.99%)

6592 existing lines in 115 files now uncovered.

188950 of 220730 relevant lines covered (85.6%)

18995333.68 hits per line

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

83.85
/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,991✔
47
        write!(f, "[")?;
61,991✔
48
        for (ix, val) in self.0.iter().enumerate() {
318,260✔
49
            if ix == 0 {
316,739✔
50
                write!(f, "{}", val)?;
47,038✔
51
            } else {
52
                write!(f, ", {}", val)?;
269,701✔
53
            }
54
        }
55
        write!(f, "]")
61,991✔
56
    }
61,991✔
57
}
58

59
impl StacksMessageCodec for UrlString {
60
    fn consensus_serialize<W: Write>(&self, fd: &mut W) -> Result<(), codec_error> {
1,536,702✔
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,536,702✔
UNCOV
64
            return Err(codec_error::SerializeError(
×
UNCOV
65
                "Failed to serialize URL string: too long".to_string(),
×
UNCOV
66
            ));
×
67
        }
1,536,702✔
68

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

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

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

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

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

102
        // must be a valid block URL, or empty string
103
        if !url.is_empty() {
759,829✔
104
            let _ = url.parse_to_block_url()?;
759,829✔
UNCOV
105
        }
×
106
        Ok(url)
759,829✔
107
    }
759,831✔
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,353,365✔
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,353,365✔
117
            .map_err(|e| codec_error::DeserializeError(format!("Invalid URL: {:?}", &e)))?;
2,353,365✔
118

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

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

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

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

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

150
        Ok(url)
2,353,358✔
151
    }
2,353,365✔
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 {
761,651✔
156
        let url = match url::Url::parse(&self.to_string()) {
761,651✔
157
            Ok(x) => x,
761,651✔
158
            Err(_) => {
159
                // should be unreachable
160
                return false;
×
161
            }
162
        };
163
        match url.host_str() {
761,651✔
164
            Some(host_str) => {
761,651✔
165
                if host_str == "0.0.0.0" || host_str == "[::]" || host_str == "::" {
761,651✔
UNCOV
166
                    return false;
×
167
                } else {
168
                    return true;
761,651✔
169
                }
170
            }
171
            None => {
UNCOV
172
                return false;
×
173
            }
174
        }
175
    }
761,651✔
176

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

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

194
    use super::*;
195

196
    #[test]
197
    fn test_contract_name_invalid() {
1✔
198
        let s = [0u8];
1✔
199
        assert!(ContractName::consensus_deserialize(&mut &s[..]).is_err());
1✔
200

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

204
        let s_body = [0x6fu8; CONTRACT_MAX_NAME_LENGTH + 1];
1✔
205
        let mut s_payload = vec![s_body.len() as u8];
1✔
206
        s_payload.extend_from_slice(&s_body);
1✔
207

208
        assert!(ContractName::consensus_deserialize(&mut &s_payload[..]).is_err());
1✔
209
    }
1✔
210

211
    #[test]
212
    fn test_url_parse() {
1✔
213
        assert!(UrlString::try_from("asdfjkl;")
1✔
214
            .unwrap()
1✔
215
            .parse_to_block_url()
1✔
216
            .unwrap_err()
1✔
217
            .to_string()
1✔
218
            .find("Invalid URL")
1✔
219
            .is_some());
1✔
220
        assert!(UrlString::try_from("http://")
1✔
221
            .unwrap()
1✔
222
            .parse_to_block_url()
1✔
223
            .unwrap_err()
1✔
224
            .to_string()
1✔
225
            .find("Invalid URL")
1✔
226
            .is_some());
1✔
227
        assert!(UrlString::try_from("ftp://ftp.google.com")
1✔
228
            .unwrap()
1✔
229
            .parse_to_block_url()
1✔
230
            .unwrap_err()
1✔
231
            .to_string()
1✔
232
            .find("invalid scheme")
1✔
233
            .is_some());
1✔
234
        assert!(UrlString::try_from("http://jude@google.com")
1✔
235
            .unwrap()
1✔
236
            .parse_to_block_url()
1✔
237
            .unwrap_err()
1✔
238
            .to_string()
1✔
239
            .find("must not contain a username/password")
1✔
240
            .is_some());
1✔
241
        assert!(UrlString::try_from("http://jude:pw@google.com")
1✔
242
            .unwrap()
1✔
243
            .parse_to_block_url()
1✔
244
            .unwrap_err()
1✔
245
            .to_string()
1✔
246
            .find("must not contain a username/password")
1✔
247
            .is_some());
1✔
248
        assert!(UrlString::try_from("http://www.google.com/foo/bar?baz=goo")
1✔
249
            .unwrap()
1✔
250
            .parse_to_block_url()
1✔
251
            .unwrap_err()
1✔
252
            .to_string()
1✔
253
            .find("query strings not supported")
1✔
254
            .is_some());
1✔
255
        assert!(UrlString::try_from("http://www.google.com/foo/bar#baz")
1✔
256
            .unwrap()
1✔
257
            .parse_to_block_url()
1✔
258
            .unwrap_err()
1✔
259
            .to_string()
1✔
260
            .find("fragments are not supported")
1✔
261
            .is_some());
1✔
262

263
        // don't need to cover the happy path too much, since the rust-url package already tests it.
264
        let url = UrlString::try_from("http://127.0.0.1:1234/v2/info")
1✔
265
            .unwrap()
1✔
266
            .parse_to_block_url()
1✔
267
            .unwrap();
1✔
268
        assert_eq!(url.host_str(), Some("127.0.0.1"));
1✔
269
        assert_eq!(url.port(), Some(1234));
1✔
270
        assert_eq!(url.path(), "/v2/info");
1✔
271
        assert_eq!(url.scheme(), "http");
1✔
272
    }
1✔
273
}
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