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#[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 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 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 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 #[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#[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}