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