1use std::fmt;
4
5use bytes::Bytes;
6use structfs_ll_store::LLPath;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum PathError {
12 InvalidComponent {
14 component: String,
15 position: usize,
16 message: String,
17 },
18 InvalidPath { message: String },
20}
21
22impl fmt::Display for PathError {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 PathError::InvalidComponent {
26 component,
27 position,
28 message,
29 } => {
30 write!(
31 f,
32 "invalid path component '{}' at position {}: {}",
33 component, position, message
34 )
35 }
36 PathError::InvalidPath { message } => {
37 write!(f, "invalid path: {}", message)
38 }
39 }
40 }
41}
42
43impl std::error::Error for PathError {}
44
45#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
63pub struct Path(LLPath);
64
65#[inline]
71fn component_str(component: &Bytes) -> &str {
72 unsafe { std::str::from_utf8_unchecked(component) }
75}
76
77fn ll_from_strings(components: Vec<String>) -> LLPath {
80 components
81 .into_iter()
82 .map(|s| Bytes::from(s.into_bytes()))
83 .collect()
84}
85
86impl Path {
87 pub fn parse(s: &str) -> Result<Self, PathError> {
107 if s.is_empty() {
108 return Ok(Path(LLPath::new()));
109 }
110
111 let components: Vec<String> = s
112 .split('/')
113 .filter(|c| !c.is_empty())
114 .map(|c| c.to_string())
115 .collect();
116
117 for (i, component) in components.iter().enumerate() {
119 Self::validate_component(component, i)?;
120 }
121
122 Ok(Path(ll_from_strings(components)))
123 }
124
125 pub fn from_components(components: Vec<String>) -> Self {
132 for (i, component) in components.iter().enumerate() {
133 Self::validate_component(component, i).expect("invalid component");
134 }
135 Path(ll_from_strings(components))
136 }
137
138 #[doc(hidden)]
148 pub fn from_validated_components(components: Vec<String>) -> Self {
149 #[cfg(debug_assertions)]
150 for (i, component) in components.iter().enumerate() {
151 Self::validate_component(component, i).expect("invalid pre-validated component");
152 }
153 Path(ll_from_strings(components))
154 }
155
156 pub fn try_from_components(components: Vec<String>) -> Result<Self, PathError> {
158 for (i, component) in components.iter().enumerate() {
159 Self::validate_component(component, i)?;
160 }
161 Ok(Path(ll_from_strings(components)))
162 }
163
164 pub fn validate_component(component: &str, position: usize) -> Result<(), PathError> {
174 structfs_path_validation::validate_component(component).map_err(|message| {
175 PathError::InvalidComponent {
176 component: component.to_string(),
177 position,
178 message,
179 }
180 })
181 }
182
183 pub fn is_empty(&self) -> bool {
185 self.0.is_empty()
186 }
187
188 pub fn len(&self) -> usize {
190 self.0.len()
191 }
192
193 pub fn iter(&self) -> impl Iterator<Item = &str> {
195 self.0.iter().map(component_str)
196 }
197
198 #[must_use]
200 pub fn join(&self, other: &Path) -> Path {
201 let mut components = self.0.components().to_vec();
202 components.extend(other.0.iter().cloned());
203 Path(LLPath::from_components(components))
204 }
205
206 #[must_use]
208 pub fn child(&self, component: impl Into<PathComponent>) -> Path {
209 let mut components = self.0.components().to_vec();
210 components.push(Bytes::from(component.into().into_string().into_bytes()));
211 Path(LLPath::from_components(components))
212 }
213
214 pub fn push(&mut self, component: impl Into<PathComponent>) {
216 self.0
217 .push(Bytes::from(component.into().into_string().into_bytes()));
218 }
219
220 pub fn has_prefix(&self, prefix: &Path) -> bool {
222 prefix.0.len() <= self.0.len()
223 && prefix.0.components() == &self.0.components()[..prefix.0.len()]
224 }
225
226 #[must_use]
230 pub fn strip_prefix(&self, prefix: &Path) -> Option<Path> {
231 if self.has_prefix(prefix) {
232 Some(Path(LLPath::from_components(
233 self.0.components()[prefix.0.len()..].to_vec(),
234 )))
235 } else {
236 None
237 }
238 }
239
240 pub fn slice(&self, start: usize, end: usize) -> Path {
242 Path(LLPath::from_components(
243 self.0.components()[start..end].to_vec(),
244 ))
245 }
246
247 pub fn as_ll(&self) -> &LLPath {
250 &self.0
251 }
252
253 pub fn into_ll(self) -> LLPath {
256 self.0
257 }
258
259 pub fn validate(ll: LLPath) -> Result<Self, PathError> {
264 for (i, component) in ll.iter().enumerate() {
265 let s = std::str::from_utf8(component.as_ref()).map_err(|_| {
266 PathError::InvalidComponent {
267 component: format!("{:?}", component.as_ref()),
268 position: i,
269 message: "not valid UTF-8".to_string(),
270 }
271 })?;
272 Self::validate_component(s, i)?;
273 }
274 Ok(Path(ll))
275 }
276
277 pub fn from_ll_unchecked(ll: LLPath) -> Self {
284 #[cfg(debug_assertions)]
285 for (i, component) in ll.iter().enumerate() {
286 let s = std::str::from_utf8(component.as_ref())
287 .expect("pre-validated LL component is not UTF-8");
288 Self::validate_component(s, i).expect("invalid pre-validated LL component");
289 }
290 Path(ll)
291 }
292
293 pub fn to_ll_path(&self) -> LLPath {
298 self.0.clone()
299 }
300
301 pub fn try_from_ll_path(ll_path: &[impl AsRef<[u8]>]) -> Result<Self, PathError> {
307 let ll: LLPath = ll_path
308 .iter()
309 .map(|b| Bytes::copy_from_slice(b.as_ref()))
310 .collect();
311 Self::validate(ll)
312 }
313}
314
315impl fmt::Display for Path {
316 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317 let mut first = true;
318 for component in self.iter() {
319 if !first {
320 f.write_str("/")?;
321 }
322 f.write_str(component)?;
323 first = false;
324 }
325 Ok(())
326 }
327}
328
329impl std::ops::Index<usize> for Path {
330 type Output = str;
331
332 fn index(&self, i: usize) -> &Self::Output {
333 component_str(&self.0[i])
334 }
335}
336
337#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
351pub struct PathComponent(String);
352
353impl PathComponent {
354 pub fn try_new(s: impl Into<String>) -> Result<Self, PathError> {
356 let s = s.into();
357 Path::validate_component(&s, 0)?;
358 Ok(Self(s))
359 }
360
361 pub fn encode(s: &str) -> Self {
368 let encoded = namecode::encode(s);
369 debug_assert!(Path::validate_component(&encoded, 0).is_ok());
370 Self(encoded)
371 }
372
373 pub fn decode(&self) -> Result<String, PathError> {
380 match namecode::decode(&self.0) {
381 Ok(decoded) => Ok(decoded),
382 Err(namecode::DecodeError::NotEncoded) => Ok(self.0.clone()),
383 Err(e) => Err(PathError::InvalidComponent {
384 component: self.0.clone(),
385 position: 0,
386 message: format!("malformed namecode encoding: {}", e),
387 }),
388 }
389 }
390
391 pub fn as_str(&self) -> &str {
393 &self.0
394 }
395
396 pub fn validated_str(&self) -> &str {
402 &self.0
403 }
404
405 pub fn into_string(self) -> String {
407 self.0
408 }
409}
410
411impl AsRef<str> for PathComponent {
412 fn as_ref(&self) -> &str {
413 &self.0
414 }
415}
416
417impl fmt::Display for PathComponent {
418 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
419 f.write_str(&self.0)
420 }
421}
422
423impl From<usize> for PathComponent {
425 fn from(i: usize) -> Self {
426 Self(i.to_string())
427 }
428}
429
430impl From<u64> for PathComponent {
431 fn from(i: u64) -> Self {
432 Self(i.to_string())
433 }
434}
435
436impl From<PathComponent> for Path {
437 fn from(c: PathComponent) -> Self {
438 Path(LLPath::from_components(vec![Bytes::from(
439 c.into_string().into_bytes(),
440 )]))
441 }
442}
443
444impl FromIterator<PathComponent> for Path {
445 fn from_iter<I: IntoIterator<Item = PathComponent>>(iter: I) -> Self {
446 Path(
447 iter.into_iter()
448 .map(|c| Bytes::from(c.into_string().into_bytes()))
449 .collect(),
450 )
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457 use crate::path;
458
459 #[test]
460 fn parse_basic_paths() {
461 assert_eq!(Path::parse("").unwrap().len(), 0);
462 assert_eq!(Path::parse("foo").unwrap().len(), 1);
463 assert_eq!(Path::parse("foo/bar").unwrap().len(), 2);
464 assert_eq!(Path::parse("foo/bar/baz").unwrap().len(), 3);
465 }
466
467 #[test]
468 fn normalize_slashes() {
469 assert_eq!(
470 Path::parse("foo/bar/").unwrap(),
471 Path::parse("foo/bar").unwrap()
472 );
473 assert_eq!(
474 Path::parse("foo//bar").unwrap(),
475 Path::parse("foo/bar").unwrap()
476 );
477 assert_eq!(
478 Path::parse("/foo/bar").unwrap(),
479 Path::parse("foo/bar").unwrap()
480 );
481 }
482
483 #[test]
484 fn numeric_components_allowed() {
485 let p = Path::parse("items/0/name").unwrap();
486 assert_eq!(p.len(), 3);
487 assert_eq!(&p[1], "0");
488 }
489
490 #[test]
491 fn unicode_identifiers_allowed() {
492 let p = Path::parse("usuarios/名前").unwrap();
493 assert_eq!(p.len(), 2);
494 }
495
496 #[test]
497 fn invalid_components_rejected() {
498 assert!(Path::parse("foo/bar baz").is_err()); assert!(Path::parse("foo/bar-baz").is_err()); assert!(Path::parse("foo/.hidden").is_err()); assert!(Path::parse("foo/123abc").is_err()); }
503
504 #[test]
505 fn has_prefix_works() {
506 let p = path!("foo/bar/baz");
507 assert!(p.has_prefix(&path!("")));
508 assert!(p.has_prefix(&path!("foo")));
509 assert!(p.has_prefix(&path!("foo/bar")));
510 assert!(p.has_prefix(&path!("foo/bar/baz")));
511 assert!(!p.has_prefix(&path!("bar")));
512 assert!(!p.has_prefix(&path!("foo/bar/baz/qux")));
513 }
514
515 #[test]
516 fn strip_prefix_works() {
517 let p = path!("foo/bar/baz");
518 assert_eq!(p.strip_prefix(&path!("foo")), Some(path!("bar/baz")));
519 assert_eq!(p.strip_prefix(&path!("foo/bar")), Some(path!("baz")));
520 assert_eq!(p.strip_prefix(&path!("other")), None);
521 }
522
523 #[test]
524 fn ll_conversion_roundtrips() {
525 let p = path!("users/123/name");
526 let ll = p.to_ll_path();
527 let p2 = Path::try_from_ll_path(&ll.iter().collect::<Vec<_>>()).unwrap();
528 assert_eq!(p, p2);
529 }
530
531 #[test]
532 fn path_error_display_invalid_component() {
533 let err = PathError::InvalidComponent {
534 component: "bad-name".to_string(),
535 position: 2,
536 message: "test message".to_string(),
537 };
538 let display = format!("{}", err);
539 assert!(display.contains("bad-name"));
540 assert!(display.contains("position 2"));
541 assert!(display.contains("test message"));
542 }
543
544 #[test]
545 fn path_error_display_invalid_path() {
546 let err = PathError::InvalidPath {
547 message: "some reason".to_string(),
548 };
549 let display = format!("{}", err);
550 assert!(display.contains("invalid path"));
551 assert!(display.contains("some reason"));
552 }
553
554 #[test]
555 fn path_error_is_error() {
556 let err: Box<dyn std::error::Error> = Box::new(PathError::InvalidPath {
557 message: "test".to_string(),
558 });
559 let _ = err.to_string();
560 }
561
562 #[test]
563 fn from_components_valid() {
564 let p = Path::from_components(vec!["foo".to_string(), "bar".to_string()]);
565 assert_eq!(p.len(), 2);
566 }
567
568 #[test]
569 #[should_panic(expected = "invalid component")]
570 fn from_components_invalid_panics() {
571 Path::from_components(vec!["foo".to_string(), "bad-name".to_string()]);
572 }
573
574 #[test]
575 fn try_from_components_valid() {
576 let p = Path::try_from_components(vec!["foo".to_string(), "bar".to_string()]).unwrap();
577 assert_eq!(p.len(), 2);
578 }
579
580 #[test]
581 fn try_from_components_invalid() {
582 let result = Path::try_from_components(vec!["foo".to_string(), "bad-name".to_string()]);
583 assert!(result.is_err());
584 }
585
586 #[test]
587 fn validate_empty_component_rejected() {
588 let result = Path::try_from_components(vec!["".to_string()]);
589 assert!(result.is_err());
590 let err = result.unwrap_err();
591 assert!(err.to_string().contains("empty component"));
592 }
593
594 #[test]
595 fn validate_underscore_alone_rejected() {
596 let result = Path::parse("_");
598 assert!(result.is_err());
599 }
600
601 #[test]
602 fn validate_underscore_with_continuation_allowed() {
603 let p = Path::parse("_foo").unwrap();
605 assert_eq!(p.len(), 1);
606 }
607
608 #[test]
609 fn validate_invalid_character_in_middle() {
610 let result = Path::parse("foo$bar");
611 assert!(result.is_err());
612 let err = result.unwrap_err();
613 assert!(err.to_string().contains("invalid character"));
614 }
615
616 #[test]
617 fn index_trait() {
618 let p = path!("foo/bar/baz");
619 assert_eq!(&p[0], "foo");
620 assert_eq!(&p[1], "bar");
621 assert_eq!(&p[2], "baz");
622 }
623
624 #[test]
625 fn slice_method() {
626 let p = path!("a/b/c/d");
627 let sliced = p.slice(1, 3);
628 assert_eq!(sliced.len(), 2);
629 assert_eq!(sliced.to_string(), "b/c");
630 }
631
632 #[test]
633 fn join_method() {
634 let p1 = path!("foo/bar");
635 let p2 = path!("baz/qux");
636 let joined = p1.join(&p2);
637 assert_eq!(joined.to_string(), "foo/bar/baz/qux");
638 }
639
640 #[test]
641 fn join_with_empty() {
642 let p1 = path!("foo");
643 let p2 = path!("");
644 assert_eq!(p1.join(&p2), p1);
645
646 let p3 = path!("");
647 let p4 = path!("bar");
648 assert_eq!(p3.join(&p4), p4);
649 }
650
651 #[test]
652 fn iter_method() {
653 let p = path!("a/b/c");
654 let components: Vec<&str> = p.iter().collect();
655 assert_eq!(components.len(), 3);
656 assert_eq!(components[0], "a");
657 assert_eq!(components[1], "b");
658 assert_eq!(components[2], "c");
659 }
660
661 #[test]
662 fn is_empty() {
663 assert!(path!("").is_empty());
664 assert!(!path!("foo").is_empty());
665 }
666
667 #[test]
668 fn display_impl() {
669 let p = path!("foo/bar/baz");
670 assert_eq!(format!("{}", p), "foo/bar/baz");
671 }
672
673 #[test]
674 fn display_empty() {
675 let p = path!("");
676 assert_eq!(format!("{}", p), "");
677 }
678
679 #[test]
680 fn ll_conversion_invalid_utf8() {
681 let invalid_utf8: Vec<&[u8]> = vec![&[0xff, 0xfe]];
682 let result = Path::try_from_ll_path(&invalid_utf8);
683 assert!(result.is_err());
684 let err = result.unwrap_err();
685 assert!(err.to_string().contains("not valid UTF-8"));
686 }
687
688 #[test]
689 fn path_ord() {
690 let p1 = path!("a/b");
691 let p2 = path!("a/c");
692 let p3 = path!("b/a");
693 assert!(p1 < p2);
694 assert!(p2 < p3);
695 }
696
697 #[test]
698 fn path_hash() {
699 use std::collections::HashSet;
700 let mut set = HashSet::new();
701 set.insert(path!("foo"));
702 set.insert(path!("bar"));
703 set.insert(path!("foo")); assert_eq!(set.len(), 2);
705 }
706
707 #[test]
708 fn macro_component_style() {
709 let p = path!("users", 123, "name");
710 assert_eq!(p.to_string(), "users/123/name");
711 assert_eq!(p, path!("users/123/name"));
712 }
713
714 #[test]
715 fn macro_empty() {
716 let p = path!();
717 assert!(p.is_empty());
718 }
719
720 #[test]
721 fn macro_with_runtime_component() {
722 let name = PathComponent::try_new("alice").unwrap();
723 let p = path!("users", name, "profile");
724 assert_eq!(p.to_string(), "users/alice/profile");
725 }
726
727 #[test]
728 fn macro_mixed_literal_forms() {
729 let p = path!("a/b", "c");
731 assert_eq!(p.to_string(), "a/b/c");
732 }
733
734 #[test]
735 fn path_component_validates() {
736 assert!(PathComponent::try_new("accounts").is_ok());
737 assert!(PathComponent::try_new("42").is_ok());
738 assert!(PathComponent::try_new("café").is_ok());
739 assert!(PathComponent::try_new("_private").is_ok());
740 assert!(PathComponent::try_new("").is_err());
741 assert!(PathComponent::try_new("my-account").is_err());
742 assert!(PathComponent::try_new("my account").is_err());
743 assert!(PathComponent::try_new(".hidden").is_err());
744 assert!(PathComponent::try_new("_").is_err());
745 assert!(PathComponent::try_new("a/b").is_err());
746 }
747
748 #[test]
749 fn path_component_encode_roundtrip() {
750 for original in [
751 "plain",
752 "my-account",
753 "hello world",
754 "slashes/and spaces",
755 "oxide-🦀",
756 "123-456",
757 ] {
758 let component = PathComponent::encode(original);
759 assert!(PathComponent::try_new(component.as_str()).is_ok());
761 assert_eq!(component.decode().unwrap(), original);
762 }
763 }
764
765 #[test]
766 fn path_component_encode_passthrough() {
767 let component = PathComponent::encode("plain");
769 assert_eq!(component.as_str(), "plain");
770 assert_eq!(component.decode().unwrap(), "plain");
771 }
772
773 #[test]
774 fn path_component_from_index() {
775 let c: PathComponent = 7usize.into();
776 assert_eq!(c.as_str(), "7");
777 let c: PathComponent = 7u64.into();
778 assert_eq!(c.as_str(), "7");
779 }
780
781 #[test]
782 fn child_and_push() {
783 let base = path!("users");
784 let p = base.child(PathComponent::try_new("alice").unwrap());
785 assert_eq!(p.to_string(), "users/alice");
786
787 let mut p2 = path!("items");
788 p2.push(3usize);
789 assert_eq!(p2.to_string(), "items/3");
790 }
791
792 #[test]
793 fn path_from_component_iter() {
794 let p: Path = ["a", "b", "c"]
795 .iter()
796 .map(|s| PathComponent::try_new(*s).unwrap())
797 .collect();
798 assert_eq!(p.to_string(), "a/b/c");
799 }
800
801 #[test]
802 fn validate_component_public() {
803 assert!(Path::validate_component("foo", 0).is_ok());
804 let err = Path::validate_component("bad-name", 2).unwrap_err();
805 assert!(err.to_string().contains("position 2"));
806 }
807
808 #[test]
809 fn path_clone() {
810 let p1 = path!("foo/bar");
811 let p2 = p1.clone();
812 assert_eq!(p1, p2);
813 }
814
815 #[test]
816 fn path_debug() {
817 let p = path!("foo/bar");
818 let debug = format!("{:?}", p);
819 assert!(debug.contains("foo"));
820 assert!(debug.contains("bar"));
821 }
822
823 #[test]
824 fn as_ll_and_into_ll_widen_losslessly() {
825 let p = path!("users/123/name");
826 let ll = p.as_ll();
828 assert_eq!(ll.len(), 3);
829 assert_eq!(ll[0].as_ref(), b"users");
830 assert_eq!(ll[2].as_ref(), b"name");
831 assert_eq!(p.clone().into_ll(), ll.clone());
833 }
834
835 #[test]
836 fn validate_narrows_and_rejects() {
837 let ll: LLPath = [Bytes::from_static(b"a"), Bytes::from_static(b"b")]
839 .into_iter()
840 .collect();
841 assert_eq!(Path::validate(ll).unwrap(), path!("a/b"));
842
843 let bad: LLPath = [Bytes::from_static(b"a-b")].into_iter().collect();
845 assert!(Path::validate(bad).is_err());
846
847 let non_utf8: LLPath = [Bytes::from_static(&[0xff, 0xfe])].into_iter().collect();
849 assert!(Path::validate(non_utf8).is_err());
850 }
851
852 #[test]
853 fn widen_then_narrow_roundtrips() {
854 let p = path!("users/名前/0");
855 assert_eq!(Path::validate(p.clone().into_ll()).unwrap(), p);
857 assert_eq!(Path::from_ll_unchecked(p.clone().into_ll()), p);
859 }
860}