Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:30:19.599Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

rel_path.rs

824 lines · 25.0 KB · rust
1use anyhow::{Context as _, Result, anyhow};
2use std::{
3    borrow::{Borrow, Cow},
4    fmt,
5    ops::Deref,
6    path::{Path, PathBuf},
7    sync::Arc,
8};
9
10use crate::{
11    PathStyle,
12    abs_path::{AbsPath, AbsPathBuf},
13    is_absolute,
14};
15
16/// A file system path that is guaranteed to be relative and normalized.
17///
18/// This type can be used to represent paths in a uniform way, regardless of
19/// whether they refer to Windows or POSIX file systems, and regardless of
20/// the host platform.
21///
22/// Internally, paths are stored in POSIX ('/'-delimited) format, but they can
23/// be displayed in either POSIX or Windows format.
24///
25/// Relative paths are also guaranteed to be valid unicode.
26#[repr(transparent)]
27#[derive(PartialEq, Eq, Hash)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize))]
29pub struct RelPath(str);
30
31/// An owned representation of a file system path that is guaranteed to be
32/// relative and normalized.
33///
34/// This type is to [`RelPath`] as [`std::path::PathBuf`] is to [`std::path::Path`]
35#[derive(PartialEq, Eq, Clone, Hash)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize))]
37pub struct RelPathBuf(String);
38
39impl RelPath {
40    /// Creates an empty [`RelPath`].
41    pub fn empty() -> &'static Self {
42        Self::from_str("")
43    }
44
45    /// Creates an empty [`RelPath`].
46    pub fn empty_arc() -> Arc<Self> {
47        static EMPTY: std::sync::OnceLock<Arc<RelPath>> = std::sync::OnceLock::new();
48        EMPTY.get_or_init(|| Arc::from(Self::empty())).clone()
49    }
50
51    /// Converts a path with a given style into a [`RelPath`].
52    ///
53    /// Returns an error if the path is absolute, or is not valid unicode.
54    ///
55    /// This method will normalize the path by removing `.` components,
56    /// processing `..` components, and removing trailing separators. It does
57    /// not allocate unless it's necessary to reformat the path.
58    #[track_caller]
59    pub fn new<'a>(path: &'a Path, path_style: PathStyle) -> Result<Cow<'a, Self>> {
60        let mut path = path.to_str().context("non utf-8 path")?;
61
62        let (prefixes, suffixes): (&[_], &[_]) = match path_style {
63            PathStyle::Unix => (&["./"], &['/']),
64            PathStyle::Windows => (&["./", ".\\"], &['/', '\\']),
65        };
66
67        while prefixes.iter().any(|prefix| path.starts_with(prefix)) {
68            path = &path[prefixes[0].len()..];
69        }
70        while let Some(prefix) = path.strip_suffix(suffixes)
71            && !prefix.is_empty()
72        {
73            path = prefix;
74        }
75
76        if is_absolute(&path, path_style) {
77            return Err(anyhow!("absolute path not allowed: {path:?}"));
78        }
79
80        let mut string = Cow::Borrowed(path);
81        if path_style == PathStyle::Windows && path.contains('\\') {
82            string = Cow::Owned(string.as_ref().replace('\\', "/"))
83        }
84
85        let mut result = match string {
86            Cow::Borrowed(string) => Cow::Borrowed(Self::from_str(string)),
87            Cow::Owned(string) => Cow::Owned(RelPathBuf(string)),
88        };
89
90        if result
91            .components()
92            .any(|component| component == "" || component == "." || component == "..")
93        {
94            let mut normalized = RelPathBuf::new();
95            for component in result.components() {
96                match component {
97                    "" => {}
98                    "." => {}
99                    ".." => {
100                        if !normalized.pop() {
101                            return Err(anyhow!("path is not relative: {result:?}"));
102                        }
103                    }
104                    other => normalized.push(RelPath::from_str(other)),
105                }
106            }
107            result = Cow::Owned(normalized)
108        }
109
110        Ok(result)
111    }
112
113    #[track_caller]
114    pub fn new_test<'a>(path: &'a str) -> Cow<'a, Self> {
115        Self::new(Path::new(path), PathStyle::Unix).unwrap()
116    }
117
118    /// Converts a path that is already normalized and uses '/' separators
119    /// into a [`RelPath`] .
120    ///
121    /// Returns an error if the path is not already in the correct format.
122    #[track_caller]
123    pub fn from_unix_str<S: AsRef<Path> + ?Sized>(path: &S) -> anyhow::Result<&Self> {
124        let path = path.as_ref();
125        match Self::new(path, PathStyle::Unix)? {
126            Cow::Borrowed(path) => Ok(path),
127            Cow::Owned(_) => Err(anyhow!("invalid relative path {path:?}")),
128        }
129    }
130
131    fn from_str(s: &str) -> &Self {
132        // Safety: `RelPath` is a transparent wrapper around `str`.
133        unsafe { &*(s as *const str as *const Self) }
134    }
135
136    pub fn is_empty(&self) -> bool {
137        self.0.is_empty()
138    }
139
140    pub fn components(&self) -> RelPathComponents<'_> {
141        RelPathComponents(&self.0)
142    }
143
144    pub fn ancestors(&self) -> RelPathAncestors<'_> {
145        RelPathAncestors {
146            full: &self.0,
147            front: self.0.len(),
148            back: 0,
149            done: false,
150        }
151    }
152
153    pub fn file_name(&self) -> Option<&str> {
154        self.components().next_back()
155    }
156
157    pub fn file_stem(&self) -> Option<&str> {
158        Some(self.as_std_path().file_stem()?.to_str().unwrap())
159    }
160
161    pub fn extension(&self) -> Option<&str> {
162        Some(self.as_std_path().extension()?.to_str().unwrap())
163    }
164
165    pub fn parent(&self) -> Option<&Self> {
166        let mut components = self.components();
167        components.next_back()?;
168        Some(components.rest())
169    }
170
171    pub fn starts_with(&self, other: &Self) -> bool {
172        self.strip_prefix(other).is_ok()
173    }
174
175    /// Returns true if this path is a strict descendant of `ancestor`.
176    ///
177    /// Unlike `starts_with`, this returns false when `self == ancestor`
178    /// and false when `ancestor` is empty (since every path trivially
179    /// starts with the empty prefix).
180    pub fn is_descendant_of(&self, ancestor: &Self) -> bool {
181        if ancestor.is_empty() || self == ancestor {
182            return false;
183        }
184        self.starts_with(ancestor)
185    }
186
187    pub fn ends_with(&self, other: &Self) -> bool {
188        if other.is_empty() {
189            return true;
190        }
191        if let Some(suffix) = self.0.strip_suffix(&other.0) {
192            if suffix.ends_with('/') {
193                return true;
194            } else if suffix.is_empty() {
195                return true;
196            }
197        }
198        false
199    }
200
201    pub fn strip_prefix<'a>(&'a self, other: &Self) -> Result<&'a Self, StripPrefixError> {
202        if other.is_empty() {
203            return Ok(self);
204        }
205        if let Some(suffix) = self.0.strip_prefix(&other.0) {
206            if let Some(suffix) = suffix.strip_prefix('/') {
207                return Ok(Self::from_str(suffix));
208            } else if suffix.is_empty() {
209                return Ok(Self::empty());
210            }
211        }
212        Err(StripPrefixError)
213    }
214
215    pub fn len(&self) -> usize {
216        if self.0.is_empty() {
217            0
218        } else {
219            self.0.matches('/').count() + 1
220        }
221    }
222
223    pub fn last_n_components(&self, count: usize) -> Option<&Self> {
224        let len = self.len();
225        if len >= count {
226            let mut components = self.components();
227            for _ in 0..(len - count) {
228                components.next()?;
229            }
230            Some(components.rest())
231        } else {
232            None
233        }
234    }
235
236    pub fn join(&self, other: &Self) -> RelPathBuf {
237        if self.0.is_empty() {
238            other.to_rel_path_buf()
239        } else if other.0.is_empty() {
240            self.to_rel_path_buf()
241        } else {
242            RelPathBuf(format!("{}/{}", &self.0, &other.0))
243        }
244    }
245
246    pub fn to_rel_path_buf(&self) -> RelPathBuf {
247        RelPathBuf(self.0.to_string())
248    }
249
250    pub fn into_arc(&self) -> Arc<Self> {
251        Arc::from(self)
252    }
253
254    /// Convert the path into a string with the given path style.
255    ///
256    /// Whenever a path is presented to the user, it should be converted to
257    /// a string via this method.
258    pub fn display(&self, style: PathStyle) -> Cow<'_, str> {
259        match style {
260            PathStyle::Unix => Cow::Borrowed(&self.0),
261            PathStyle::Windows if self.0.contains('/') => Cow::Owned(self.0.replace('/', "\\")),
262            PathStyle::Windows => Cow::Borrowed(&self.0),
263        }
264    }
265
266    /// Get the internal unix-style representation of the path.
267    ///
268    /// This should not be shown to the user.
269    pub fn as_unix_str(&self) -> &str {
270        &self.0
271    }
272
273    /// Interprets the path as a [`std::path::Path`], suitable for file system calls.
274    ///
275    /// This is guaranteed to be a valid path regardless of the host platform, because
276    /// the `/` is accepted as a path separator on windows.
277    ///
278    /// This should not be shown to the user.
279    pub fn as_std_path(&self) -> &Path {
280        Path::new(&self.0)
281    }
282
283    /// Resolves this relative path against an absolute base path.
284    pub fn absolutize(&self, base: impl AsRef<AbsPath>) -> AbsPathBuf {
285        base.as_ref().join(self.as_unix_str())
286    }
287}
288
289#[derive(Debug)]
290pub struct StripPrefixError;
291
292impl ToOwned for RelPath {
293    type Owned = RelPathBuf;
294
295    fn to_owned(&self) -> Self::Owned {
296        self.to_rel_path_buf()
297    }
298}
299
300impl Borrow<RelPath> for RelPathBuf {
301    fn borrow(&self) -> &RelPath {
302        self.as_rel_path()
303    }
304}
305
306impl PartialOrd for RelPath {
307    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
308        Some(self.cmp(other))
309    }
310}
311
312impl Ord for RelPath {
313    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
314        self.components().cmp(other.components())
315    }
316}
317
318impl fmt::Debug for RelPath {
319    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320        fmt::Debug::fmt(&self.0, f)
321    }
322}
323
324impl fmt::Debug for RelPathBuf {
325    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326        fmt::Debug::fmt(&self.0, f)
327    }
328}
329
330impl RelPathBuf {
331    pub fn new() -> Self {
332        Self(String::new())
333    }
334
335    pub fn pop(&mut self) -> bool {
336        if let Some(ix) = self.0.rfind('/') {
337            self.0.truncate(ix);
338            true
339        } else if !self.is_empty() {
340            self.0.clear();
341            true
342        } else {
343            false
344        }
345    }
346
347    pub fn push(&mut self, path: &RelPath) {
348        if path.is_empty() {
349            return;
350        }
351        if !self.is_empty() {
352            self.0.push('/');
353        }
354        self.0.push_str(&path.0);
355    }
356
357    pub fn push_component(&mut self, component: &str) -> Result<()> {
358        anyhow::ensure!(
359            !component.is_empty()
360                && !component.contains('/')
361                && component != "."
362                && component != "..",
363            "invalid relative path component: {component:?}"
364        );
365
366        if !self.is_empty() {
367            self.0.push('/');
368        }
369        self.0.push_str(component);
370        Ok(())
371    }
372
373    pub fn as_rel_path(&self) -> &RelPath {
374        RelPath::from_str(self.0.as_str())
375    }
376
377    pub fn set_extension(&mut self, extension: &str) -> bool {
378        if let Some(filename) = self.file_name() {
379            let mut filename = PathBuf::from(filename);
380            filename.set_extension(extension);
381            self.pop();
382            self.push(RelPath::from_str(filename.to_str().unwrap()));
383            true
384        } else {
385            false
386        }
387    }
388}
389
390impl PartialOrd for RelPathBuf {
391    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
392        Some(self.cmp(other))
393    }
394}
395
396impl Ord for RelPathBuf {
397    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
398        self.as_rel_path().cmp(other.as_rel_path())
399    }
400}
401
402impl From<RelPathBuf> for Arc<RelPath> {
403    fn from(value: RelPathBuf) -> Self {
404        Arc::from(value.as_rel_path())
405    }
406}
407
408impl AsRef<RelPath> for RelPathBuf {
409    fn as_ref(&self) -> &RelPath {
410        self.as_rel_path()
411    }
412}
413
414impl AsRef<std::path::Path> for RelPathBuf {
415    fn as_ref(&self) -> &Path {
416        self.as_std_path()
417    }
418}
419
420impl AsRef<RelPath> for RelPath {
421    fn as_ref(&self) -> &RelPath {
422        self
423    }
424}
425
426impl AsRef<std::path::Path> for RelPath {
427    fn as_ref(&self) -> &Path {
428        self.as_std_path()
429    }
430}
431
432impl Deref for RelPathBuf {
433    type Target = RelPath;
434
435    fn deref(&self) -> &Self::Target {
436        self.as_ref()
437    }
438}
439
440impl<'a> From<&'a RelPath> for Cow<'a, RelPath> {
441    fn from(value: &'a RelPath) -> Self {
442        Self::Borrowed(value)
443    }
444}
445
446impl From<&RelPath> for Arc<RelPath> {
447    fn from(rel_path: &RelPath) -> Self {
448        let bytes: Arc<str> = Arc::from(&rel_path.0);
449        // SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`.
450        unsafe { Arc::from_raw(Arc::into_raw(bytes) as *const RelPath) }
451    }
452}
453
454impl<'a> TryFrom<&'a str> for &'a RelPath {
455    type Error = anyhow::Error;
456
457    fn try_from(s: &'a str) -> Result<Self> {
458        RelPath::from_unix_str(s)
459    }
460}
461
462impl TryFrom<&str> for RelPathBuf {
463    type Error = anyhow::Error;
464
465    fn try_from(s: &str) -> Result<Self> {
466        RelPath::new(Path::new(s), PathStyle::Unix).map(|cow| cow.into_owned())
467    }
468}
469
470#[cfg(any(test, feature = "test-support"))]
471#[track_caller]
472pub fn rel_path(path: &str) -> &RelPath {
473    RelPath::from_unix_str(path).unwrap()
474}
475
476#[cfg(any(test, feature = "test-support"))]
477#[track_caller]
478pub fn rel_path_buf(path: &str) -> RelPathBuf {
479    rel_path(path).to_owned()
480}
481
482impl PartialEq<str> for RelPath {
483    fn eq(&self, other: &str) -> bool {
484        self.0 == *other
485    }
486}
487
488impl PartialEq<RelPath> for RelPathBuf {
489    fn eq(&self, other: &RelPath) -> bool {
490        self.as_rel_path() == other
491    }
492}
493
494impl PartialEq<RelPathBuf> for RelPath {
495    fn eq(&self, other: &RelPathBuf) -> bool {
496        other.as_rel_path() == self
497    }
498}
499
500impl fmt::Display for RelPath {
501    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
502        f.write_str(&self.0)
503    }
504}
505
506impl fmt::Display for RelPathBuf {
507    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
508        f.write_str(&self.0)
509    }
510}
511
512#[derive(Default)]
513pub struct RelPathComponents<'a>(&'a str);
514
515pub struct RelPathAncestors<'a> {
516    full: &'a str,
517    front: usize,
518    back: usize,
519    done: bool,
520}
521
522const SEPARATOR: char = '/';
523
524impl<'a> RelPathComponents<'a> {
525    pub fn rest(&self) -> &'a RelPath {
526        RelPath::from_str(self.0)
527    }
528}
529
530impl<'a> Iterator for RelPathComponents<'a> {
531    type Item = &'a str;
532
533    fn next(&mut self) -> Option<Self::Item> {
534        if let Some(sep_ix) = self.0.find(SEPARATOR) {
535            let (head, tail) = self.0.split_at(sep_ix);
536            self.0 = &tail[1..];
537            Some(head)
538        } else if self.0.is_empty() {
539            None
540        } else {
541            let result = self.0;
542            self.0 = "";
543            Some(result)
544        }
545    }
546}
547
548impl<'a> Iterator for RelPathAncestors<'a> {
549    type Item = &'a RelPath;
550
551    fn next(&mut self) -> Option<Self::Item> {
552        if self.done {
553            return None;
554        }
555        let result = &self.full[..self.front];
556        if self.front == self.back {
557            self.done = true;
558        } else {
559            self.front = result.rfind(SEPARATOR).unwrap_or_default();
560        }
561        Some(RelPath::from_str(result))
562    }
563}
564
565impl<'a> DoubleEndedIterator for RelPathAncestors<'a> {
566    fn next_back(&mut self) -> Option<Self::Item> {
567        if self.done {
568            return None;
569        }
570        let result = &self.full[..self.back];
571        if self.front == self.back {
572            self.done = true;
573        } else {
574            let search_start = if self.back == 0 { 0 } else { self.back + 1 };
575            self.back = match self.full[search_start..].find(SEPARATOR) {
576                Some(sep_ix) => search_start + sep_ix,
577                None => self.full.len(),
578            };
579        }
580        Some(RelPath::from_str(result))
581    }
582}
583
584impl<'a> DoubleEndedIterator for RelPathComponents<'a> {
585    fn next_back(&mut self) -> Option<Self::Item> {
586        if let Some(sep_ix) = self.0.rfind(SEPARATOR) {
587            let (head, tail) = self.0.split_at(sep_ix);
588            self.0 = head;
589            Some(&tail[1..])
590        } else if self.0.is_empty() {
591            None
592        } else {
593            let result = self.0;
594            self.0 = "";
595            Some(result)
596        }
597    }
598}
599
600#[cfg(feature = "serde")]
601impl<'de> serde::Deserialize<'de> for RelPathBuf {
602    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
603    where
604        D: serde::Deserializer<'de>,
605    {
606        let path = String::deserialize(deserializer)?;
607        let rel_path =
608            RelPath::new(Path::new(&path), PathStyle::local()).map_err(serde::de::Error::custom)?;
609        Ok(rel_path.into_owned())
610    }
611}
612
613#[cfg(test)]
614mod tests {
615    use super::*;
616
617    #[test]
618    fn test_rel_path_new() {
619        assert!(RelPath::new(Path::new("/"), PathStyle::local()).is_err());
620        assert!(RelPath::new(Path::new("//"), PathStyle::local()).is_err());
621        assert!(RelPath::new(Path::new("/foo/"), PathStyle::local()).is_err());
622
623        let path = RelPath::new("foo/".as_ref(), PathStyle::local()).unwrap();
624        assert_eq!(path, rel_path("foo").into());
625        assert!(matches!(path, Cow::Borrowed(_)));
626
627        let path = RelPath::new("foo\\".as_ref(), PathStyle::Windows).unwrap();
628        assert_eq!(path, rel_path("foo").into());
629        assert!(matches!(path, Cow::Borrowed(_)));
630
631        assert_eq!(
632            RelPath::new("foo/bar/../baz/./quux/".as_ref(), PathStyle::local())
633                .unwrap()
634                .as_ref(),
635            rel_path("foo/baz/quux")
636        );
637
638        let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Unix).unwrap();
639        assert_eq!(path.as_ref(), rel_path("foo/bar"));
640        assert!(matches!(path, Cow::Borrowed(_)));
641
642        let path = RelPath::new(".\\foo".as_ref(), PathStyle::Windows).unwrap();
643        assert_eq!(path, rel_path("foo").into());
644        assert!(matches!(path, Cow::Borrowed(_)));
645
646        let path = RelPath::new("./.\\./foo/\\/".as_ref(), PathStyle::Windows).unwrap();
647        assert_eq!(path, rel_path("foo").into());
648        assert!(matches!(path, Cow::Borrowed(_)));
649
650        let path = RelPath::new("foo/./bar".as_ref(), PathStyle::Unix).unwrap();
651        assert_eq!(path.as_ref(), rel_path("foo/bar"));
652        assert!(matches!(path, Cow::Owned(_)));
653
654        let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Windows).unwrap();
655        assert_eq!(path.as_ref(), rel_path("foo/bar"));
656        assert!(matches!(path, Cow::Borrowed(_)));
657
658        let path = RelPath::new(".\\foo\\bar".as_ref(), PathStyle::Windows).unwrap();
659        assert_eq!(path.as_ref(), rel_path("foo/bar"));
660        assert!(matches!(path, Cow::Owned(_)));
661    }
662
663    #[test]
664    fn test_rel_path_components() {
665        let path = rel_path("foo/bar/baz");
666        assert_eq!(
667            path.components().collect::<Vec<_>>(),
668            vec!["foo", "bar", "baz"]
669        );
670        assert_eq!(
671            path.components().rev().collect::<Vec<_>>(),
672            vec!["baz", "bar", "foo"]
673        );
674
675        let path = rel_path("");
676        let mut components = path.components();
677        assert_eq!(components.next(), None);
678    }
679
680    #[test]
681    fn test_rel_path_ancestors() {
682        let path = rel_path("foo/bar/baz");
683        let mut ancestors = path.ancestors();
684        assert_eq!(ancestors.next(), Some(rel_path("foo/bar/baz")));
685        assert_eq!(ancestors.next(), Some(rel_path("foo/bar")));
686        assert_eq!(ancestors.next(), Some(rel_path("foo")));
687        assert_eq!(ancestors.next(), Some(rel_path("")));
688        assert_eq!(ancestors.next(), None);
689
690        let path = rel_path("foo");
691        let mut ancestors = path.ancestors();
692        assert_eq!(ancestors.next(), Some(rel_path("foo")));
693        assert_eq!(ancestors.next(), Some(RelPath::empty()));
694        assert_eq!(ancestors.next(), None);
695
696        let path = RelPath::empty();
697        let mut ancestors = path.ancestors();
698        assert_eq!(ancestors.next(), Some(RelPath::empty()));
699        assert_eq!(ancestors.next(), None);
700
701        let path = rel_path("foo/bar/baz");
702        let mut ancestors = path.ancestors();
703        assert_eq!(ancestors.next_back(), Some(rel_path("")));
704        assert_eq!(ancestors.next_back(), Some(rel_path("foo")));
705        assert_eq!(ancestors.next_back(), Some(rel_path("foo/bar")));
706        assert_eq!(ancestors.next_back(), Some(rel_path("foo/bar/baz")));
707        assert_eq!(ancestors.next_back(), None);
708
709        let path = rel_path("foo/bar/baz");
710        let mut ancestors = path.ancestors();
711        assert_eq!(ancestors.next(), Some(rel_path("foo/bar/baz")));
712        assert_eq!(ancestors.next_back(), Some(rel_path("")));
713        assert_eq!(ancestors.next(), Some(rel_path("foo/bar")));
714        assert_eq!(ancestors.next_back(), Some(rel_path("foo")));
715        assert_eq!(ancestors.next(), None);
716        assert_eq!(ancestors.next_back(), None);
717
718        let path = rel_path("foo");
719        let mut ancestors = path.ancestors();
720        assert_eq!(ancestors.next_back(), Some(RelPath::empty()));
721        assert_eq!(ancestors.next_back(), Some(rel_path("foo")));
722        assert_eq!(ancestors.next_back(), None);
723
724        let path = RelPath::empty();
725        let mut ancestors = path.ancestors();
726        assert_eq!(ancestors.next_back(), Some(RelPath::empty()));
727        assert_eq!(ancestors.next_back(), None);
728
729        let path = rel_path("über/x");
730        let mut ancestors = path.ancestors();
731        assert_eq!(ancestors.next_back(), Some(RelPath::empty()));
732        assert_eq!(ancestors.next_back(), Some(rel_path("über")));
733        assert_eq!(ancestors.next_back(), Some(rel_path("über/x")));
734        assert_eq!(ancestors.next_back(), None);
735    }
736
737    #[test]
738    fn test_rel_path_parent() {
739        assert_eq!(rel_path("foo/bar/baz").parent(), Some(rel_path("foo/bar")));
740        assert_eq!(rel_path("foo").parent(), Some(RelPath::empty()));
741        assert_eq!(rel_path("").parent(), None);
742    }
743
744    #[test]
745    fn test_rel_path_partial_ord_is_compatible_with_std() {
746        let test_cases = ["a/b/c", "relative/path/with/dot.", "relative/path/with.dot"];
747        for (i, lhs) in test_cases.iter().enumerate() {
748            for rhs in &test_cases[i + 1..] {
749                assert_eq!(
750                    Path::new(lhs).cmp(Path::new(rhs)),
751                    RelPath::from_unix_str(lhs)
752                        .unwrap()
753                        .cmp(RelPath::from_unix_str(rhs).unwrap()),
754                    "ordering mismatch for {:?} vs {:?}",
755                    lhs,
756                    rhs,
757                );
758            }
759        }
760    }
761
762    #[test]
763    fn test_strip_prefix() {
764        let parent = rel_path("");
765        let child = rel_path(".foo");
766
767        assert!(child.starts_with(parent));
768        assert_eq!(child.strip_prefix(parent).unwrap(), child);
769    }
770
771    #[test]
772    fn test_ends_with() {
773        assert!(rel_path("foo/bar").ends_with(rel_path("bar")));
774        assert!(rel_path("foo/bar").ends_with(rel_path("foo/bar")));
775        assert!(rel_path("foo/bar").ends_with(RelPath::empty()));
776        assert!(RelPath::empty().ends_with(RelPath::empty()));
777        assert!(!rel_path("foobar").ends_with(rel_path("bar")));
778    }
779
780    #[test]
781    fn test_rel_path_constructors_absolute_path() {
782        assert!(RelPath::new(Path::new("/a/b"), PathStyle::Windows).is_err());
783        assert!(RelPath::new(Path::new("\\a\\b"), PathStyle::Windows).is_err());
784        assert!(RelPath::new(Path::new("/a/b"), PathStyle::Unix).is_err());
785        assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Windows).is_err());
786        assert!(RelPath::new(Path::new("C:\\a\\b"), PathStyle::Windows).is_err());
787        assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Unix).is_ok());
788    }
789
790    #[test]
791    fn test_pop() {
792        let mut path = rel_path_buf("a/b");
793        path.pop();
794        assert_eq!(path.as_rel_path().as_unix_str(), "a");
795        path.pop();
796        assert_eq!(path.as_rel_path().as_unix_str(), "");
797        path.pop();
798        assert_eq!(path.as_rel_path().as_unix_str(), "");
799    }
800
801    #[test]
802    fn test_len() {
803        assert_eq!(RelPath::empty().len(), 0);
804        assert_eq!(rel_path("a").len(), 1);
805        assert_eq!(rel_path("a/b").len(), 2);
806        assert_eq!(rel_path("a/b/c").len(), 3);
807    }
808
809    #[test]
810    fn test_set_extension() {
811        let mut path = rel_path_buf("a/b/c.txt");
812        assert!(path.set_extension("rs"));
813        assert_eq!(path.as_rel_path().as_unix_str(), "a/b/c.rs");
814
815        let mut single = rel_path_buf("file.txt");
816        assert!(single.set_extension("md"));
817        assert_eq!(single.as_rel_path().as_unix_str(), "file.md");
818
819        let mut no_ext = rel_path_buf("a/b/c");
820        assert!(no_ext.set_extension("rs"));
821        assert_eq!(no_ext.as_rel_path().as_unix_str(), "a/b/c.rs");
822    }
823}
824
Served at tenant.openagents/omega Member data and write actions are omitted.