1use percent_encoding::percent_decode;
21use std::fmt::Formatter;
22#[cfg(not(target_arch = "wasm32"))]
23use url::Url;
24
25pub const DELIMITER: &str = "/";
27
28pub const DELIMITER_BYTE: u8 = DELIMITER.as_bytes()[0];
30
31pub const DELIMITER_CHAR: char = DELIMITER_BYTE as char;
33
34mod parts;
35
36pub use parts::{InvalidPart, PathPart, PathParts};
37
38#[derive(Debug, thiserror::Error)]
40#[non_exhaustive]
41pub enum Error {
42 #[error("Path \"{}\" contained empty path segment", path)]
44 EmptySegment {
45 path: String,
47 },
48
49 #[error("Error parsing Path \"{}\": {}", path, source)]
51 BadSegment {
52 path: String,
54 source: InvalidPart,
56 },
57
58 #[error("Failed to canonicalize path \"{}\": {}", path.display(), source)]
60 Canonicalize {
61 path: std::path::PathBuf,
63 source: std::io::Error,
65 },
66
67 #[error("Unable to convert path \"{}\" to URL", path.display())]
69 InvalidPath {
70 path: std::path::PathBuf,
72 },
73
74 #[error("Path \"{}\" contained non-unicode characters: {}", path, source)]
76 NonUnicode {
77 path: String,
79 source: std::str::Utf8Error,
81 },
82
83 #[error("Path {} does not start with prefix {}", path, prefix)]
85 PrefixMismatch {
86 path: String,
88 prefix: String,
90 },
91}
92
93#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Ord, PartialOrd)]
156pub struct Path {
157 raw: String,
159}
160
161impl Path {
162 pub const ROOT: Self = Self { raw: String::new() };
173
174 pub fn parse(path: impl AsRef<str>) -> Result<Self, Error> {
179 let path = path.as_ref();
180
181 let stripped = path.strip_prefix(DELIMITER).unwrap_or(path);
182 if stripped.is_empty() {
183 return Ok(Default::default());
184 }
185
186 let stripped = stripped.strip_suffix(DELIMITER).unwrap_or(stripped);
187
188 for segment in stripped.split(DELIMITER) {
189 if segment.is_empty() {
190 return Err(Error::EmptySegment { path: path.into() });
191 }
192
193 PathPart::parse(segment).map_err(|source| {
194 let path = path.into();
195 Error::BadSegment { source, path }
196 })?;
197 }
198
199 Ok(Self {
200 raw: stripped.to_string(),
201 })
202 }
203
204 #[cfg(not(target_arch = "wasm32"))]
205 pub fn from_filesystem_path(path: impl AsRef<std::path::Path>) -> Result<Self, Error> {
212 let absolute = std::fs::canonicalize(&path).map_err(|source| {
213 let path = path.as_ref().into();
214 Error::Canonicalize { source, path }
215 })?;
216
217 Self::from_absolute_path(absolute)
218 }
219
220 #[cfg(not(target_arch = "wasm32"))]
221 pub fn from_absolute_path(path: impl AsRef<std::path::Path>) -> Result<Self, Error> {
226 Self::from_absolute_path_with_base(path, None)
227 }
228
229 #[cfg(not(target_arch = "wasm32"))]
230 pub(crate) fn from_absolute_path_with_base(
236 path: impl AsRef<std::path::Path>,
237 base: Option<&Url>,
238 ) -> Result<Self, Error> {
239 let url = absolute_path_to_url(path)?;
240 let path = match base {
241 Some(prefix) => {
242 url.path()
243 .strip_prefix(prefix.path())
244 .ok_or_else(|| Error::PrefixMismatch {
245 path: url.path().to_string(),
246 prefix: prefix.to_string(),
247 })?
248 }
249 None => url.path(),
250 };
251
252 Self::from_url_path(path)
254 }
255
256 pub fn from_url_path(path: impl AsRef<str>) -> Result<Self, Error> {
261 let path = path.as_ref();
262 let decoded = percent_decode(path.as_bytes())
263 .decode_utf8()
264 .map_err(|source| {
265 let path = path.into();
266 Error::NonUnicode { source, path }
267 })?;
268
269 Self::parse(decoded)
270 }
271
272 #[doc(alias = "len")]
280 pub fn parts_count(&self) -> usize {
281 self.raw.split_terminator(DELIMITER).count()
282 }
283
284 pub fn is_root(&self) -> bool {
296 self.raw.is_empty()
297 }
298
299 pub fn parts(&self) -> PathParts<'_> {
303 PathParts::new(&self.raw)
304 }
305
306 pub fn parent(&self) -> Option<Self> {
310 if self.raw.is_empty() {
311 return None;
312 }
313
314 let Some((prefix, _filename)) = self.raw.rsplit_once(DELIMITER) else {
315 return Some(Self::ROOT);
316 };
317
318 Some(Self {
319 raw: prefix.to_string(),
320 })
321 }
322
323 pub fn filename(&self) -> Option<&str> {
327 match self.raw.is_empty() {
328 true => None,
329 false => self.raw.rsplit(DELIMITER).next(),
330 }
331 }
332
333 pub fn extension(&self) -> Option<&str> {
335 self.filename()
336 .and_then(|f| f.rsplit_once('.'))
337 .and_then(|(_, extension)| {
338 if extension.is_empty() {
339 None
340 } else {
341 Some(extension)
342 }
343 })
344 }
345
346 pub fn prefix_match(&self, prefix: &Self) -> Option<impl Iterator<Item = PathPart<'_>> + '_> {
350 let mut stripped = self.raw.strip_prefix(&prefix.raw)?;
351 if !stripped.is_empty() && !prefix.raw.is_empty() {
352 stripped = stripped.strip_prefix(DELIMITER)?;
353 }
354 Some(PathParts::new(stripped))
355 }
356
357 pub fn prefix_matches(&self, prefix: &Self) -> bool {
359 self.prefix_match(prefix).is_some()
360 }
361
362 #[deprecated = "use .join() or .clone().join() instead"]
364 pub fn child<'a>(&self, child: impl Into<PathPart<'a>>) -> Self {
365 self.clone().join(child)
366 }
367
368 pub fn join<'a>(self, child: impl Into<PathPart<'a>>) -> Self {
370 let child_cow_str = child.into().raw;
371
372 let raw = if self.raw.is_empty() {
373 child_cow_str.to_string()
374 } else {
375 use std::fmt::Write;
376
377 let mut raw = self.raw;
378 write!(raw, "{DELIMITER}{child_cow_str}").expect("failed to append to string");
379 raw
380 };
381
382 Self { raw }
383 }
384}
385
386impl AsRef<str> for Path {
387 fn as_ref(&self) -> &str {
388 &self.raw
389 }
390}
391
392impl From<&str> for Path {
393 fn from(path: &str) -> Self {
394 Self::from_iter(path.split(DELIMITER))
395 }
396}
397
398impl From<String> for Path {
399 fn from(path: String) -> Self {
400 Self::from_iter(path.split(DELIMITER))
401 }
402}
403
404impl From<Path> for String {
405 fn from(path: Path) -> Self {
406 path.raw
407 }
408}
409
410impl std::fmt::Display for Path {
411 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
412 self.raw.fmt(f)
413 }
414}
415
416impl<'a, I> FromIterator<I> for Path
417where
418 I: Into<PathPart<'a>>,
419{
420 fn from_iter<T: IntoIterator<Item = I>>(iter: T) -> Self {
421 let mut this = Self::ROOT;
422 this.extend(iter);
423 this
424 }
425}
426
427impl<'a> IntoIterator for &'a Path {
429 type Item = PathPart<'a>;
430 type IntoIter = PathParts<'a>;
431
432 fn into_iter(self) -> Self::IntoIter {
433 PathParts::new(&self.raw)
434 }
435}
436
437impl<'a, I: Into<PathPart<'a>>> Extend<I> for Path {
457 fn extend<T: IntoIterator<Item = I>>(&mut self, iter: T) {
458 for s in iter {
459 let s = s.into();
460 if !s.raw.is_empty() {
461 if !self.raw.is_empty() {
462 self.raw.push(DELIMITER_CHAR);
463 }
464 self.raw.push_str(&s.raw);
465 }
466 }
467 }
468}
469
470#[cfg(not(target_arch = "wasm32"))]
471pub(crate) fn absolute_path_to_url(path: impl AsRef<std::path::Path>) -> Result<Url, Error> {
473 Url::from_file_path(&path).map_err(|_| Error::InvalidPath {
474 path: path.as_ref().into(),
475 })
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 #[test]
483 fn delimiter_char_is_forward_slash() {
484 assert_eq!(DELIMITER_CHAR, '/');
485 }
486
487 #[test]
488 fn cloud_prefix_with_trailing_delimiter() {
489 let prefix = Path::from_iter(["test"]);
493 assert_eq!(prefix.as_ref(), "test");
494 }
495
496 #[test]
497 fn push_encodes() {
498 let location = Path::from_iter(["foo/bar", "baz%2Ftest"]);
499 assert_eq!(location.as_ref(), "foo%2Fbar/baz%252Ftest");
500 }
501
502 #[test]
503 fn test_parse() {
504 assert_eq!(Path::parse("/").unwrap().as_ref(), "");
505 assert_eq!(Path::parse("").unwrap().as_ref(), "");
506
507 let err = Path::parse("//").unwrap_err();
508 assert!(matches!(err, Error::EmptySegment { .. }));
509
510 assert_eq!(Path::parse("/foo/bar/").unwrap().as_ref(), "foo/bar");
511 assert_eq!(Path::parse("foo/bar/").unwrap().as_ref(), "foo/bar");
512 assert_eq!(Path::parse("foo/bar").unwrap().as_ref(), "foo/bar");
513
514 let err = Path::parse("foo///bar").unwrap_err();
515 assert!(matches!(err, Error::EmptySegment { .. }));
516 }
517
518 #[test]
519 fn convert_raw_before_partial_eq() {
520 let cloud = Path::from("test_dir/test_file.json");
522 let built = Path::from_iter(["test_dir", "test_file.json"]);
523
524 assert_eq!(built, cloud);
525
526 let cloud = Path::from("test_dir/test_file");
528 let built = Path::from_iter(["test_dir", "test_file"]);
529
530 assert_eq!(built, cloud);
531
532 let cloud = Path::from("test_dir/");
534 let built = Path::from_iter(["test_dir"]);
535 assert_eq!(built, cloud);
536
537 let cloud = Path::from("test_file.json");
539 let built = Path::from_iter(["test_file.json"]);
540 assert_eq!(built, cloud);
541
542 let cloud = Path::from("");
544 let built = Path::from_iter(["", ""]);
545
546 assert_eq!(built, cloud);
547 }
548
549 #[test]
550 fn parts_after_prefix_behavior() {
551 let existing_path = Path::from("apple/bear/cow/dog/egg.json");
552
553 let prefix = Path::from("apple");
555 let expected_parts: Vec<PathPart<'_>> = vec!["bear", "cow", "dog", "egg.json"]
556 .into_iter()
557 .map(Into::into)
558 .collect();
559 let parts: Vec<_> = existing_path.prefix_match(&prefix).unwrap().collect();
560 assert_eq!(parts, expected_parts);
561
562 let prefix = Path::from("apple/bear");
564 let expected_parts: Vec<PathPart<'_>> = vec!["cow", "dog", "egg.json"]
565 .into_iter()
566 .map(Into::into)
567 .collect();
568 let parts: Vec<_> = existing_path.prefix_match(&prefix).unwrap().collect();
569 assert_eq!(parts, expected_parts);
570
571 let prefix = Path::from("cow");
573 assert!(existing_path.prefix_match(&prefix).is_none());
574
575 let prefix = Path::from("ap");
577 assert!(existing_path.prefix_match(&prefix).is_none());
578
579 let existing = Path::from("apple/bear/cow/dog");
581
582 assert_eq!(existing.prefix_match(&existing).unwrap().count(), 0);
583 assert_eq!(Path::default().parts().count(), 0);
584 }
585
586 #[test]
587 fn parts_count() {
588 assert_eq!(Path::ROOT.parts().count(), Path::ROOT.parts_count());
589
590 let path = path("foo/bar/baz");
591 assert_eq!(path.parts_count(), 3);
592 assert_eq!(path.parts_count(), path.parts().count());
593 }
594
595 #[test]
596 fn prefix_matches_raw_content() {
597 assert_eq!(Path::ROOT.parent(), None, "empty path must have no prefix");
598
599 assert_eq!(path("foo").parent().unwrap(), Path::ROOT);
600 assert_eq!(path("foo/bar").parent().unwrap(), path("foo"));
601 assert_eq!(path("foo/bar/baz").parent().unwrap(), path("foo/bar"));
602 }
603
604 #[test]
605 fn prefix_matches() {
606 let haystack = Path::from_iter(["foo/bar", "baz%2Ftest", "something"]);
607 assert!(
609 haystack.prefix_matches(&haystack),
610 "{haystack:?} should have started with {haystack:?}"
611 );
612
613 let needle = haystack.clone().join("longer now");
615 assert!(
616 !haystack.prefix_matches(&needle),
617 "{haystack:?} shouldn't have started with {needle:?}"
618 );
619
620 let needle = Path::from_iter(["foo/bar"]);
622 assert!(
623 haystack.prefix_matches(&needle),
624 "{haystack:?} should have started with {needle:?}"
625 );
626
627 let needle = needle.join("baz%2Ftest");
629 assert!(
630 haystack.prefix_matches(&needle),
631 "{haystack:?} should have started with {needle:?}"
632 );
633
634 let needle = Path::from_iter(["f"]);
636 assert!(
637 !haystack.prefix_matches(&needle),
638 "{haystack:?} should not have started with {needle:?}"
639 );
640
641 let needle = Path::from_iter(["foo/bar", "baz"]);
643 assert!(
644 !haystack.prefix_matches(&needle),
645 "{haystack:?} should not have started with {needle:?}"
646 );
647
648 let needle = Path::from("");
650 assert!(
651 haystack.prefix_matches(&needle),
652 "{haystack:?} should have started with {needle:?}"
653 );
654 }
655
656 #[test]
657 fn prefix_matches_with_file_name() {
658 let haystack = Path::from_iter(["foo/bar", "baz%2Ftest", "something", "foo.segment"]);
659
660 let needle = Path::from_iter(["foo/bar", "baz%2Ftest", "something", "foo"]);
662
663 assert!(
664 !haystack.prefix_matches(&needle),
665 "{haystack:?} should not have started with {needle:?}"
666 );
667
668 let needle = Path::from_iter(["foo/bar", "baz%2Ftest", "something", "e"]);
670
671 assert!(
672 !haystack.prefix_matches(&needle),
673 "{haystack:?} should not have started with {needle:?}"
674 );
675
676 let needle = Path::from_iter(["foo/bar", "baz%2Ftest", "s"]);
679
680 assert!(
681 !haystack.prefix_matches(&needle),
682 "{haystack:?} should not have started with {needle:?}"
683 );
684
685 let needle = Path::from_iter(["foo/bar", "baz%2Ftest", "p"]);
688
689 assert!(
690 !haystack.prefix_matches(&needle),
691 "{haystack:?} should not have started with {needle:?}"
692 );
693 }
694
695 #[test]
696 fn path_containing_spaces() {
697 let a = Path::from_iter(["foo bar", "baz"]);
698 let b = Path::from("foo bar/baz");
699 let c = Path::parse("foo bar/baz").unwrap();
700
701 assert_eq!(a.raw, "foo bar/baz");
702 assert_eq!(a.raw, b.raw);
703 assert_eq!(b.raw, c.raw);
704 }
705
706 #[test]
707 fn from_url_path() {
708 let a = Path::from_url_path("foo%20bar").unwrap();
709 let b = Path::from_url_path("foo/%2E%2E/bar").unwrap_err();
710 let c = Path::from_url_path("foo%2F%252E%252E%2Fbar").unwrap();
711 let d = Path::from_url_path("foo/%252E%252E/bar").unwrap();
712 let e = Path::from_url_path("%48%45%4C%4C%4F").unwrap();
713 let f = Path::from_url_path("foo/%FF/as").unwrap_err();
714
715 assert_eq!(a.raw, "foo bar");
716 assert!(matches!(b, Error::BadSegment { .. }));
717 assert_eq!(c.raw, "foo/%2E%2E/bar");
718 assert_eq!(d.raw, "foo/%2E%2E/bar");
719 assert_eq!(e.raw, "HELLO");
720 assert!(matches!(f, Error::NonUnicode { .. }));
721 }
722
723 #[test]
724 fn filename_from_path() {
725 let a = Path::from("foo/bar");
726 let b = Path::from("foo/bar.baz");
727 let c = Path::from("foo.bar/baz");
728
729 assert_eq!(a.filename(), Some("bar"));
730 assert_eq!(b.filename(), Some("bar.baz"));
731 assert_eq!(c.filename(), Some("baz"));
732 }
733
734 #[test]
735 fn file_extension() {
736 let a = Path::from("foo/bar");
737 let b = Path::from("foo/bar.baz");
738 let c = Path::from("foo.bar/baz");
739 let d = Path::from("foo.bar/baz.qux");
740
741 assert_eq!(a.extension(), None);
742 assert_eq!(b.extension(), Some("baz"));
743 assert_eq!(c.extension(), None);
744 assert_eq!(d.extension(), Some("qux"));
745 }
746
747 #[test]
748 fn root_is_root() {
749 assert!(Path::ROOT.is_root());
750 assert!(Path::ROOT.parts().next().is_none());
751 }
752
753 #[test]
755 fn impl_extend() {
756 let mut p = Path::ROOT;
757
758 p.extend(&Path::ROOT);
759 assert_eq!(p, Path::ROOT);
760
761 p.extend(&path("foo"));
762 assert_eq!(p, path("foo"));
763
764 p.extend(&path("bar/baz"));
765 assert_eq!(p, path("foo/bar/baz"));
766
767 p.extend(&path("a/b/c"));
768 assert_eq!(p, path("foo/bar/baz/a/b/c"));
769 }
770
771 #[test]
773 fn impl_extend_for_one_segment() {
774 let mut p = Path::ROOT;
775
776 p.extend(&path("foo"));
777 assert_eq!(p, path("foo"));
778
779 p.extend(&path("bar"));
780 assert_eq!(p, path("foo/bar"));
781
782 p.extend(&path("baz"));
783 assert_eq!(p, path("foo/bar/baz"));
784 }
785
786 #[test]
787 fn parent() {
788 assert_eq!(Path::ROOT.parent(), None);
789 assert_eq!(path("foo").parent(), Some(Path::ROOT));
790 assert_eq!(path("foo/bar").parent(), Some(path("foo")));
791 assert_eq!(path("foo/bar/baz").parent(), Some(path("foo/bar")));
792 }
793
794 #[track_caller]
796 fn path(raw: &str) -> Path {
797 Path::parse(raw).unwrap()
798 }
799}