Skip to repository content824 lines · 25.0 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:30:19.599Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
rel_path.rs
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