Skip to main content

miden_debug_types/
lib.rs

1#![no_std]
2
3extern crate alloc;
4
5#[cfg(any(feature = "std", test))]
6extern crate std;
7
8mod location;
9mod selection;
10mod source_file;
11mod source_manager;
12mod span;
13
14#[cfg(feature = "arbitrary")]
15use alloc::vec;
16use alloc::{format, string::String, sync::Arc};
17
18use miden_crypto::utils::{
19    ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
20};
21#[cfg(feature = "arbitrary")]
22use proptest::prelude::*;
23#[cfg(feature = "serde")]
24use serde::{Deserialize, Serialize};
25#[cfg(feature = "serde")]
26pub use serde_spanned;
27
28#[cfg(feature = "std")]
29pub use self::source_manager::SourceManagerExt;
30pub use self::{
31    location::{FileLineCol, Location},
32    selection::{Position, Selection},
33    source_file::{
34        ByteIndex, ByteOffset, ColumnIndex, ColumnNumber, LineIndex, LineNumber, SourceContent,
35        SourceContentUpdateError, SourceFile, SourceFileRef, SourceLanguage,
36    },
37    source_manager::{
38        DefaultSourceManager, SourceId, SourceManager, SourceManagerError, SourceManagerSync,
39    },
40    span::{SourceSpan, Span, Spanned},
41};
42
43// URI
44// ================================================================================================
45
46/// A [URI reference](https://datatracker.ietf.org/doc/html/rfc3986#section-4.1) that specifies
47/// the location of a source file, whether on disk, on the network, or elsewhere.
48#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
49#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
50#[cfg_attr(
51    all(feature = "arbitrary", test),
52    miden_test_serde_macros::serde_test(binary_serde(true))
53)]
54pub struct Uri(Arc<str>);
55
56impl Uri {
57    pub fn new(uri: impl AsRef<str>) -> Self {
58        uri.as_ref().into()
59    }
60
61    #[inline]
62    pub fn as_str(&self) -> &str {
63        self.0.as_ref()
64    }
65
66    #[inline]
67    pub fn as_bytes(&self) -> &[u8] {
68        self.0.as_bytes()
69    }
70
71    /// Returns the scheme portion of this URI, if present.
72    pub fn scheme(&self) -> Option<&str> {
73        match self.0.split_once("://") {
74            Some((prefix, _))
75                if prefix.contains(|c: char| {
76                    !c.is_ascii_alphanumeric() && !matches!(c, '+' | '-' | '.')
77                }) =>
78            {
79                None
80            },
81            Some((prefix, _)) => Some(prefix),
82            None => None,
83        }
84    }
85
86    /// Returns the authority portion of this URI, if present.
87    pub fn authority(&self) -> Option<&str> {
88        let rest = self.hierarchical_part();
89        let authority_and_path = rest.strip_prefix("//")?;
90        match authority_and_path.split_once(['/', '?', '#']) {
91            Some((authority, _)) => Some(authority),
92            None => Some(authority_and_path),
93        }
94    }
95
96    /// Returns the path portion of this URI.
97    pub fn path(&self) -> &str {
98        let rest = self.hierarchical_part();
99        let path = match rest.strip_prefix("//") {
100            Some(authority_and_path) => match authority_and_path.find('/') {
101                Some(pos) => &authority_and_path[pos..],
102                None => return "",
103            },
104            None => rest,
105        };
106        strip_query_and_fragment(path)
107    }
108
109    /// Convert this URI to a [std::path::PathBuf], if it represents a file path
110    #[cfg(feature = "std")]
111    pub fn to_path(&self) -> Option<std::path::PathBuf> {
112        if has_windows_drive_prefix(self.as_str()) {
113            return Some(std::path::PathBuf::from(self.as_str()));
114        }
115
116        match self.scheme() {
117            None if has_restricted_scheme_prefix_without_slashes(self.as_str()) => None,
118            None if self.authority().is_none() => Some(std::path::PathBuf::from(self.as_str())),
119            None => None,
120            Some(scheme)
121                if scheme.eq_ignore_ascii_case("file")
122                    && (matches!(self.authority(), None | Some(""))
123                        || self.authority().is_some_and(|authority| {
124                            authority.eq_ignore_ascii_case("localhost")
125                        })) =>
126            {
127                Some(std::path::PathBuf::from(local_file_uri_path(self.path())))
128            },
129            Some(_) => None,
130        }
131    }
132
133    fn hierarchical_part(&self) -> &str {
134        match self.scheme() {
135            Some(scheme) => &self.0[scheme.len() + 1..],
136            None => self.0.as_ref(),
137        }
138    }
139}
140
141#[cfg(feature = "std")]
142fn has_windows_drive_prefix(path: &str) -> bool {
143    let bytes = path.as_bytes();
144    bytes.len() >= 3
145        && bytes[0].is_ascii_alphabetic()
146        && bytes[1] == b':'
147        && matches!(bytes[2], b'/' | b'\\')
148}
149
150#[cfg(feature = "std")]
151fn has_restricted_scheme_prefix_without_slashes(uri: &str) -> bool {
152    matches!(
153        uri.split_once(':'),
154        Some((prefix, rest))
155            if !rest.starts_with("//")
156                && !prefix.is_empty()
157                && prefix
158                    .chars()
159                    .all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
160    )
161}
162
163#[cfg(feature = "std")]
164fn local_file_uri_path(path: &str) -> &str {
165    match path.strip_prefix('/') {
166        Some(path_without_leading_slash)
167            if has_windows_drive_prefix(path_without_leading_slash) =>
168        {
169            path_without_leading_slash
170        },
171        _ => path,
172    }
173}
174
175fn strip_query_and_fragment(path: &str) -> &str {
176    match path.split_once(['?', '#']) {
177        Some((path, _)) => path,
178        None => path,
179    }
180}
181
182impl core::fmt::Display for Uri {
183    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
184        core::fmt::Display::fmt(&self.0, f)
185    }
186}
187
188impl AsRef<str> for Uri {
189    fn as_ref(&self) -> &str {
190        self.0.as_ref()
191    }
192}
193
194impl From<&str> for Uri {
195    #[inline]
196    fn from(value: &str) -> Self {
197        use alloc::string::ToString;
198
199        value.to_string().into()
200    }
201}
202
203impl From<Uri> for Arc<str> {
204    fn from(value: Uri) -> Self {
205        value.0
206    }
207}
208
209impl From<Arc<str>> for Uri {
210    #[inline]
211    fn from(uri: Arc<str>) -> Self {
212        Self(uri)
213    }
214}
215
216impl From<alloc::boxed::Box<str>> for Uri {
217    #[inline]
218    fn from(uri: alloc::boxed::Box<str>) -> Self {
219        Self(uri.into())
220    }
221}
222
223impl From<String> for Uri {
224    #[inline]
225    fn from(uri: String) -> Self {
226        Self(uri.into_boxed_str().into())
227    }
228}
229
230#[cfg(feature = "std")]
231impl<'a> From<&'a std::path::Path> for Uri {
232    fn from(path: &'a std::path::Path) -> Self {
233        use alloc::string::ToString;
234
235        Self::from(path.display().to_string())
236    }
237}
238
239#[cfg(feature = "std")]
240impl From<std::path::PathBuf> for Uri {
241    fn from(path: std::path::PathBuf) -> Self {
242        use alloc::string::ToString;
243
244        Self::from(path.display().to_string())
245    }
246}
247
248#[cfg(feature = "std")]
249impl From<Arc<std::path::Path>> for Uri {
250    fn from(path: Arc<std::path::Path>) -> Self {
251        use alloc::string::ToString;
252
253        Self::from(path.display().to_string())
254    }
255}
256
257impl Serializable for Uri {
258    fn write_into<W: ByteWriter>(&self, target: &mut W) {
259        self.as_str().write_into(target);
260    }
261}
262
263impl Deserializable for Uri {
264    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
265        read_bounded_string(source).map(Self::from)
266    }
267}
268
269fn read_bounded_string<R: ByteReader>(source: &mut R) -> Result<String, DeserializationError> {
270    let len = source.read_usize()?;
271    validate_len(source, "uri bytes", len)?;
272    let bytes = source.read_slice(len)?;
273    core::str::from_utf8(bytes)
274        .map(String::from)
275        .map_err(|err| DeserializationError::InvalidValue(format!("{err}")))
276}
277
278fn validate_len<R: ByteReader>(
279    source: &R,
280    label: &str,
281    len: usize,
282) -> Result<(), DeserializationError> {
283    let max_len = source.max_alloc(1);
284    if len > max_len {
285        return Err(DeserializationError::InvalidValue(format!(
286            "{label} count {len} exceeds budget {max_len}"
287        )));
288    }
289
290    source.check_eor(len).map_err(|err| match err {
291        DeserializationError::UnexpectedEOF => DeserializationError::InvalidValue(format!(
292            "{label} count {len} exceeds remaining input"
293        )),
294        err => err,
295    })
296}
297
298impl core::str::FromStr for Uri {
299    type Err = ();
300
301    fn from_str(s: &str) -> Result<Self, Self::Err> {
302        Ok(Self::from(s))
303    }
304}
305
306#[cfg(feature = "arbitrary")]
307impl Arbitrary for Uri {
308    type Parameters = ();
309    type Strategy = BoxedStrategy<Self>;
310
311    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
312        use alloc::string::String;
313
314        proptest::collection::vec(
315            proptest::prop_oneof![
316                proptest::char::range('a', 'z'),
317                proptest::char::range('A', 'Z'),
318                proptest::char::range('0', '9'),
319                Just('/'),
320                Just(':'),
321                Just('.'),
322                Just('-'),
323                Just('_'),
324                Just('#'),
325                Just('?'),
326                Just('@'),
327            ],
328            1..48,
329        )
330        .prop_map(|chars| Self::from(chars.into_iter().collect::<String>()))
331        .boxed()
332    }
333}
334
335// TESTS
336// ================================================================================================
337
338#[cfg(test)]
339mod tests {
340    use miden_crypto::utils::{Deserializable, DeserializationError, SliceReader};
341
342    use super::*;
343
344    #[test]
345    fn uri_rejects_oversized_length_prefix() {
346        let bytes = [0x08, 0x2a, 0xfe, 0xfe, 0x01];
347        let mut reader = SliceReader::new(&bytes);
348        let err = Uri::read_from(&mut reader).unwrap_err();
349        let DeserializationError::InvalidValue(message) = err else {
350            panic!("expected InvalidValue error");
351        };
352        assert!(message.contains("uri bytes count"));
353        assert!(message.contains("exceeds remaining input"));
354    }
355
356    #[test]
357    fn uri_scheme_extraction() {
358        let relative_file = Uri::new("foo.masm");
359        let relative_file_path = Uri::new("./foo.masm");
360        let relative_file_path_with_colon = Uri::new("file:foo.masm");
361        let absolute_file_path = Uri::new("file:///tmp/foo.masm");
362        let http_simple_uri = Uri::new("http://www.example.com");
363        let http_simple_uri_with_userinfo = Uri::new("http://foo:bar@www.example.com");
364        let http_simple_uri_with_userinfo_and_port = Uri::new("http://foo:bar@www.example.com:443");
365        let http_simple_uri_with_userinfo_and_path =
366            Uri::new("http://foo:bar@www.example.com/api/v1");
367        let http_simple_uri_with_userinfo_and_query =
368            Uri::new("http://foo:bar@www.example.com?param=1");
369        let http_simple_uri_with_userinfo_and_fragment =
370            Uri::new("http://foo:bar@www.example.com#about");
371        let http_simple_uri_with_userinfo_and_path_and_query =
372            Uri::new("http://foo:bar@www.example.com/api/v1/user?id=1");
373        let http_simple_uri_with_userinfo_and_path_and_query_and_fragment =
374            Uri::new("http://foo:bar@www.example.com/api/v1/user?id=1#redirect=/home");
375
376        assert_eq!(relative_file.scheme(), None);
377        assert_eq!(relative_file_path.scheme(), None);
378        assert_eq!(relative_file_path_with_colon.scheme(), None);
379        assert_eq!(absolute_file_path.scheme(), Some("file"));
380        assert_eq!(http_simple_uri.scheme(), Some("http"));
381        assert_eq!(http_simple_uri_with_userinfo.scheme(), Some("http"));
382        assert_eq!(http_simple_uri_with_userinfo_and_port.scheme(), Some("http"));
383        assert_eq!(http_simple_uri_with_userinfo_and_path.scheme(), Some("http"));
384        assert_eq!(http_simple_uri_with_userinfo_and_query.scheme(), Some("http"));
385        assert_eq!(http_simple_uri_with_userinfo_and_fragment.scheme(), Some("http"));
386        assert_eq!(http_simple_uri_with_userinfo_and_path_and_query.scheme(), Some("http"));
387        assert_eq!(
388            http_simple_uri_with_userinfo_and_path_and_query_and_fragment.scheme(),
389            Some("http")
390        );
391    }
392
393    #[test]
394    fn uri_authority_extraction() {
395        let relative_file = Uri::new("foo.masm");
396        let relative_file_path = Uri::new("./foo.masm");
397        let relative_file_path_with_empty_segment = Uri::new("foo//bar/baz");
398        let relative_file_path_with_colon = Uri::new("file:foo.masm");
399        let absolute_file_path = Uri::new("file:///tmp/foo.masm");
400        let network_path_reference = Uri::new("//www.example.com/api/v1");
401        let http_simple_uri = Uri::new("http://www.example.com");
402        let http_simple_uri_with_userinfo = Uri::new("http://foo:bar@www.example.com");
403        let http_simple_uri_with_userinfo_and_port = Uri::new("http://foo:bar@www.example.com:443");
404        let http_simple_uri_with_userinfo_and_path =
405            Uri::new("http://foo:bar@www.example.com/api/v1");
406        let http_simple_uri_with_userinfo_and_query =
407            Uri::new("http://foo:bar@www.example.com?param=1");
408        let http_simple_uri_with_userinfo_and_fragment =
409            Uri::new("http://foo:bar@www.example.com#about");
410        let http_simple_uri_with_userinfo_and_path_and_query =
411            Uri::new("http://foo:bar@www.example.com/api/v1/user?id=1");
412        let http_simple_uri_with_userinfo_and_path_and_query_and_fragment =
413            Uri::new("http://foo:bar@www.example.com/api/v1/user?id=1#redirect=/home");
414
415        assert_eq!(relative_file.authority(), None);
416        assert_eq!(relative_file_path.authority(), None);
417        assert_eq!(relative_file_path_with_empty_segment.authority(), None);
418        assert_eq!(relative_file_path_with_colon.authority(), None);
419        assert_eq!(absolute_file_path.authority(), Some(""));
420        assert_eq!(network_path_reference.authority(), Some("www.example.com"));
421        assert_eq!(http_simple_uri.authority(), Some("www.example.com"));
422        assert_eq!(http_simple_uri_with_userinfo.authority(), Some("foo:bar@www.example.com"));
423        assert_eq!(
424            http_simple_uri_with_userinfo_and_port.authority(),
425            Some("foo:bar@www.example.com:443")
426        );
427        assert_eq!(
428            http_simple_uri_with_userinfo_and_path.authority(),
429            Some("foo:bar@www.example.com")
430        );
431        assert_eq!(
432            http_simple_uri_with_userinfo_and_query.authority(),
433            Some("foo:bar@www.example.com")
434        );
435        assert_eq!(
436            http_simple_uri_with_userinfo_and_fragment.authority(),
437            Some("foo:bar@www.example.com")
438        );
439        assert_eq!(
440            http_simple_uri_with_userinfo_and_path_and_query.authority(),
441            Some("foo:bar@www.example.com")
442        );
443        assert_eq!(
444            http_simple_uri_with_userinfo_and_path_and_query_and_fragment.authority(),
445            Some("foo:bar@www.example.com")
446        );
447    }
448
449    #[test]
450    fn uri_path_extraction() {
451        let relative_file = Uri::new("foo.masm");
452        let relative_file_path = Uri::new("./foo.masm");
453        let relative_file_path_with_empty_segment = Uri::new("foo//bar/baz");
454        let relative_file_path_with_colon = Uri::new("file:foo.masm");
455        let absolute_file_path = Uri::new("file:///tmp/foo.masm");
456        let network_path_reference = Uri::new("//www.example.com/api/v1");
457        let http_simple_uri = Uri::new("http://www.example.com");
458        let http_simple_uri_with_userinfo = Uri::new("http://foo:bar@www.example.com");
459        let http_simple_uri_with_userinfo_and_port = Uri::new("http://foo:bar@www.example.com:443");
460        let http_simple_uri_with_userinfo_and_path =
461            Uri::new("http://foo:bar@www.example.com/api/v1");
462        let http_simple_uri_with_userinfo_and_query =
463            Uri::new("http://foo:bar@www.example.com?param=1");
464        let http_simple_uri_with_userinfo_and_fragment =
465            Uri::new("http://foo:bar@www.example.com#about");
466        let http_simple_uri_with_userinfo_and_path_and_query =
467            Uri::new("http://foo:bar@www.example.com/api/v1/user?id=1");
468        let http_simple_uri_with_userinfo_and_path_and_query_and_fragment =
469            Uri::new("http://foo:bar@www.example.com/api/v1/user?id=1#redirect=/home");
470
471        assert_eq!(relative_file.path(), "foo.masm");
472        assert_eq!(relative_file_path.path(), "./foo.masm");
473        assert_eq!(relative_file_path_with_empty_segment.path(), "foo//bar/baz");
474        assert_eq!(relative_file_path_with_colon.path(), "file:foo.masm");
475        assert_eq!(absolute_file_path.path(), "/tmp/foo.masm");
476        assert_eq!(network_path_reference.path(), "/api/v1");
477        assert_eq!(http_simple_uri.path(), "");
478        assert_eq!(http_simple_uri_with_userinfo.path(), "");
479        assert_eq!(http_simple_uri_with_userinfo_and_port.path(), "");
480        assert_eq!(http_simple_uri_with_userinfo_and_path.path(), "/api/v1");
481        assert_eq!(http_simple_uri_with_userinfo_and_query.path(), "");
482        assert_eq!(http_simple_uri_with_userinfo_and_fragment.path(), "");
483        assert_eq!(http_simple_uri_with_userinfo_and_path_and_query.path(), "/api/v1/user");
484        assert_eq!(
485            http_simple_uri_with_userinfo_and_path_and_query_and_fragment.path(),
486            "/api/v1/user"
487        );
488    }
489
490    #[cfg(feature = "std")]
491    #[test]
492    fn uri_file_paths_convert_to_paths() {
493        assert_eq!(Uri::new("foo.masm").to_path(), Some(std::path::PathBuf::from("foo.masm")));
494        assert_eq!(
495            Uri::new("foo#bar?.masm").to_path(),
496            Some(std::path::PathBuf::from("foo#bar?.masm"))
497        );
498        assert_eq!(
499            Uri::new("C:\\tmp\\foo.masm").to_path(),
500            Some(std::path::PathBuf::from("C:\\tmp\\foo.masm"))
501        );
502        assert_eq!(
503            Uri::new("C:/tmp/foo.masm").to_path(),
504            Some(std::path::PathBuf::from("C:/tmp/foo.masm"))
505        );
506        assert_eq!(Uri::new("file:foo.masm").to_path(), None);
507        assert_eq!(
508            Uri::new("file:///tmp/foo.masm").to_path(),
509            Some(std::path::PathBuf::from("/tmp/foo.masm"))
510        );
511        assert_eq!(
512            Uri::new("FILE:///tmp/foo.masm").to_path(),
513            Some(std::path::PathBuf::from("/tmp/foo.masm"))
514        );
515        assert_eq!(
516            Uri::new("File:///tmp/foo.masm").to_path(),
517            Some(std::path::PathBuf::from("/tmp/foo.masm"))
518        );
519        assert_eq!(
520            Uri::new("file:///C:/tmp/foo.masm").to_path(),
521            Some(std::path::PathBuf::from("C:/tmp/foo.masm"))
522        );
523        assert_eq!(
524            Uri::new("file://localhost/tmp/foo.masm").to_path(),
525            Some(std::path::PathBuf::from("/tmp/foo.masm"))
526        );
527        assert_eq!(
528            Uri::new("file://LOCALHOST/tmp/foo.masm").to_path(),
529            Some(std::path::PathBuf::from("/tmp/foo.masm"))
530        );
531        assert_eq!(Uri::new("//www.example.com/api/v1").to_path(), None);
532        assert_eq!(Uri::new("file://www.example.com/tmp/foo.masm").to_path(), None);
533        assert_eq!(Uri::new("memory:foo.masm").to_path(), None);
534    }
535}