use anyhow::{Context as _, Result, anyhow}; use std::{ borrow::{Borrow, Cow}, fmt, ops::Deref, path::{Path, PathBuf}, sync::Arc, }; use crate::{ PathStyle, abs_path::{AbsPath, AbsPathBuf}, is_absolute, }; /// A file system path that is guaranteed to be relative and normalized. /// /// This type can be used to represent paths in a uniform way, regardless of /// whether they refer to Windows or POSIX file systems, and regardless of /// the host platform. /// /// Internally, paths are stored in POSIX ('/'-delimited) format, but they can /// be displayed in either POSIX or Windows format. /// /// Relative paths are also guaranteed to be valid unicode. #[repr(transparent)] #[derive(PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize))] pub struct RelPath(str); /// An owned representation of a file system path that is guaranteed to be /// relative and normalized. /// /// This type is to [`RelPath`] as [`std::path::PathBuf`] is to [`std::path::Path`] #[derive(PartialEq, Eq, Clone, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize))] pub struct RelPathBuf(String); impl RelPath { /// Creates an empty [`RelPath`]. pub fn empty() -> &'static Self { Self::from_str("") } /// Creates an empty [`RelPath`]. pub fn empty_arc() -> Arc { static EMPTY: std::sync::OnceLock> = std::sync::OnceLock::new(); EMPTY.get_or_init(|| Arc::from(Self::empty())).clone() } /// Converts a path with a given style into a [`RelPath`]. /// /// Returns an error if the path is absolute, or is not valid unicode. /// /// This method will normalize the path by removing `.` components, /// processing `..` components, and removing trailing separators. It does /// not allocate unless it's necessary to reformat the path. #[track_caller] pub fn new<'a>(path: &'a Path, path_style: PathStyle) -> Result> { let mut path = path.to_str().context("non utf-8 path")?; let (prefixes, suffixes): (&[_], &[_]) = match path_style { PathStyle::Unix => (&["./"], &['/']), PathStyle::Windows => (&["./", ".\\"], &['/', '\\']), }; while prefixes.iter().any(|prefix| path.starts_with(prefix)) { path = &path[prefixes[0].len()..]; } while let Some(prefix) = path.strip_suffix(suffixes) && !prefix.is_empty() { path = prefix; } if is_absolute(&path, path_style) { return Err(anyhow!("absolute path not allowed: {path:?}")); } let mut string = Cow::Borrowed(path); if path_style == PathStyle::Windows && path.contains('\\') { string = Cow::Owned(string.as_ref().replace('\\', "/")) } let mut result = match string { Cow::Borrowed(string) => Cow::Borrowed(Self::from_str(string)), Cow::Owned(string) => Cow::Owned(RelPathBuf(string)), }; if result .components() .any(|component| component == "" || component == "." || component == "..") { let mut normalized = RelPathBuf::new(); for component in result.components() { match component { "" => {} "." => {} ".." => { if !normalized.pop() { return Err(anyhow!("path is not relative: {result:?}")); } } other => normalized.push(RelPath::from_str(other)), } } result = Cow::Owned(normalized) } Ok(result) } #[track_caller] pub fn new_test<'a>(path: &'a str) -> Cow<'a, Self> { Self::new(Path::new(path), PathStyle::Unix).unwrap() } /// Converts a path that is already normalized and uses '/' separators /// into a [`RelPath`] . /// /// Returns an error if the path is not already in the correct format. #[track_caller] pub fn from_unix_str + ?Sized>(path: &S) -> anyhow::Result<&Self> { let path = path.as_ref(); match Self::new(path, PathStyle::Unix)? { Cow::Borrowed(path) => Ok(path), Cow::Owned(_) => Err(anyhow!("invalid relative path {path:?}")), } } fn from_str(s: &str) -> &Self { // Safety: `RelPath` is a transparent wrapper around `str`. unsafe { &*(s as *const str as *const Self) } } pub fn is_empty(&self) -> bool { self.0.is_empty() } pub fn components(&self) -> RelPathComponents<'_> { RelPathComponents(&self.0) } pub fn ancestors(&self) -> RelPathAncestors<'_> { RelPathAncestors { full: &self.0, front: self.0.len(), back: 0, done: false, } } pub fn file_name(&self) -> Option<&str> { self.components().next_back() } pub fn file_stem(&self) -> Option<&str> { Some(self.as_std_path().file_stem()?.to_str().unwrap()) } pub fn extension(&self) -> Option<&str> { Some(self.as_std_path().extension()?.to_str().unwrap()) } pub fn parent(&self) -> Option<&Self> { let mut components = self.components(); components.next_back()?; Some(components.rest()) } pub fn starts_with(&self, other: &Self) -> bool { self.strip_prefix(other).is_ok() } /// Returns true if this path is a strict descendant of `ancestor`. /// /// Unlike `starts_with`, this returns false when `self == ancestor` /// and false when `ancestor` is empty (since every path trivially /// starts with the empty prefix). pub fn is_descendant_of(&self, ancestor: &Self) -> bool { if ancestor.is_empty() || self == ancestor { return false; } self.starts_with(ancestor) } pub fn ends_with(&self, other: &Self) -> bool { if other.is_empty() { return true; } if let Some(suffix) = self.0.strip_suffix(&other.0) { if suffix.ends_with('/') { return true; } else if suffix.is_empty() { return true; } } false } pub fn strip_prefix<'a>(&'a self, other: &Self) -> Result<&'a Self, StripPrefixError> { if other.is_empty() { return Ok(self); } if let Some(suffix) = self.0.strip_prefix(&other.0) { if let Some(suffix) = suffix.strip_prefix('/') { return Ok(Self::from_str(suffix)); } else if suffix.is_empty() { return Ok(Self::empty()); } } Err(StripPrefixError) } pub fn len(&self) -> usize { if self.0.is_empty() { 0 } else { self.0.matches('/').count() + 1 } } pub fn last_n_components(&self, count: usize) -> Option<&Self> { let len = self.len(); if len >= count { let mut components = self.components(); for _ in 0..(len - count) { components.next()?; } Some(components.rest()) } else { None } } pub fn join(&self, other: &Self) -> RelPathBuf { if self.0.is_empty() { other.to_rel_path_buf() } else if other.0.is_empty() { self.to_rel_path_buf() } else { RelPathBuf(format!("{}/{}", &self.0, &other.0)) } } pub fn to_rel_path_buf(&self) -> RelPathBuf { RelPathBuf(self.0.to_string()) } pub fn into_arc(&self) -> Arc { Arc::from(self) } /// Convert the path into a string with the given path style. /// /// Whenever a path is presented to the user, it should be converted to /// a string via this method. pub fn display(&self, style: PathStyle) -> Cow<'_, str> { match style { PathStyle::Unix => Cow::Borrowed(&self.0), PathStyle::Windows if self.0.contains('/') => Cow::Owned(self.0.replace('/', "\\")), PathStyle::Windows => Cow::Borrowed(&self.0), } } /// Get the internal unix-style representation of the path. /// /// This should not be shown to the user. pub fn as_unix_str(&self) -> &str { &self.0 } /// Interprets the path as a [`std::path::Path`], suitable for file system calls. /// /// This is guaranteed to be a valid path regardless of the host platform, because /// the `/` is accepted as a path separator on windows. /// /// This should not be shown to the user. pub fn as_std_path(&self) -> &Path { Path::new(&self.0) } /// Resolves this relative path against an absolute base path. pub fn absolutize(&self, base: impl AsRef) -> AbsPathBuf { base.as_ref().join(self.as_unix_str()) } } #[derive(Debug)] pub struct StripPrefixError; impl ToOwned for RelPath { type Owned = RelPathBuf; fn to_owned(&self) -> Self::Owned { self.to_rel_path_buf() } } impl Borrow for RelPathBuf { fn borrow(&self) -> &RelPath { self.as_rel_path() } } impl PartialOrd for RelPath { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for RelPath { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.components().cmp(other.components()) } } impl fmt::Debug for RelPath { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(&self.0, f) } } impl fmt::Debug for RelPathBuf { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(&self.0, f) } } impl RelPathBuf { pub fn new() -> Self { Self(String::new()) } pub fn pop(&mut self) -> bool { if let Some(ix) = self.0.rfind('/') { self.0.truncate(ix); true } else if !self.is_empty() { self.0.clear(); true } else { false } } pub fn push(&mut self, path: &RelPath) { if path.is_empty() { return; } if !self.is_empty() { self.0.push('/'); } self.0.push_str(&path.0); } pub fn push_component(&mut self, component: &str) -> Result<()> { anyhow::ensure!( !component.is_empty() && !component.contains('/') && component != "." && component != "..", "invalid relative path component: {component:?}" ); if !self.is_empty() { self.0.push('/'); } self.0.push_str(component); Ok(()) } pub fn as_rel_path(&self) -> &RelPath { RelPath::from_str(self.0.as_str()) } pub fn set_extension(&mut self, extension: &str) -> bool { if let Some(filename) = self.file_name() { let mut filename = PathBuf::from(filename); filename.set_extension(extension); self.pop(); self.push(RelPath::from_str(filename.to_str().unwrap())); true } else { false } } } impl PartialOrd for RelPathBuf { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for RelPathBuf { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.as_rel_path().cmp(other.as_rel_path()) } } impl From for Arc { fn from(value: RelPathBuf) -> Self { Arc::from(value.as_rel_path()) } } impl AsRef for RelPathBuf { fn as_ref(&self) -> &RelPath { self.as_rel_path() } } impl AsRef for RelPathBuf { fn as_ref(&self) -> &Path { self.as_std_path() } } impl AsRef for RelPath { fn as_ref(&self) -> &RelPath { self } } impl AsRef for RelPath { fn as_ref(&self) -> &Path { self.as_std_path() } } impl Deref for RelPathBuf { type Target = RelPath; fn deref(&self) -> &Self::Target { self.as_ref() } } impl<'a> From<&'a RelPath> for Cow<'a, RelPath> { fn from(value: &'a RelPath) -> Self { Self::Borrowed(value) } } impl From<&RelPath> for Arc { fn from(rel_path: &RelPath) -> Self { let bytes: Arc = Arc::from(&rel_path.0); // SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`. unsafe { Arc::from_raw(Arc::into_raw(bytes) as *const RelPath) } } } impl<'a> TryFrom<&'a str> for &'a RelPath { type Error = anyhow::Error; fn try_from(s: &'a str) -> Result { RelPath::from_unix_str(s) } } impl TryFrom<&str> for RelPathBuf { type Error = anyhow::Error; fn try_from(s: &str) -> Result { RelPath::new(Path::new(s), PathStyle::Unix).map(|cow| cow.into_owned()) } } #[cfg(any(test, feature = "test-support"))] #[track_caller] pub fn rel_path(path: &str) -> &RelPath { RelPath::from_unix_str(path).unwrap() } #[cfg(any(test, feature = "test-support"))] #[track_caller] pub fn rel_path_buf(path: &str) -> RelPathBuf { rel_path(path).to_owned() } impl PartialEq for RelPath { fn eq(&self, other: &str) -> bool { self.0 == *other } } impl PartialEq for RelPathBuf { fn eq(&self, other: &RelPath) -> bool { self.as_rel_path() == other } } impl PartialEq for RelPath { fn eq(&self, other: &RelPathBuf) -> bool { other.as_rel_path() == self } } impl fmt::Display for RelPath { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.0) } } impl fmt::Display for RelPathBuf { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.0) } } #[derive(Default)] pub struct RelPathComponents<'a>(&'a str); pub struct RelPathAncestors<'a> { full: &'a str, front: usize, back: usize, done: bool, } const SEPARATOR: char = '/'; impl<'a> RelPathComponents<'a> { pub fn rest(&self) -> &'a RelPath { RelPath::from_str(self.0) } } impl<'a> Iterator for RelPathComponents<'a> { type Item = &'a str; fn next(&mut self) -> Option { if let Some(sep_ix) = self.0.find(SEPARATOR) { let (head, tail) = self.0.split_at(sep_ix); self.0 = &tail[1..]; Some(head) } else if self.0.is_empty() { None } else { let result = self.0; self.0 = ""; Some(result) } } } impl<'a> Iterator for RelPathAncestors<'a> { type Item = &'a RelPath; fn next(&mut self) -> Option { if self.done { return None; } let result = &self.full[..self.front]; if self.front == self.back { self.done = true; } else { self.front = result.rfind(SEPARATOR).unwrap_or_default(); } Some(RelPath::from_str(result)) } } impl<'a> DoubleEndedIterator for RelPathAncestors<'a> { fn next_back(&mut self) -> Option { if self.done { return None; } let result = &self.full[..self.back]; if self.front == self.back { self.done = true; } else { let search_start = if self.back == 0 { 0 } else { self.back + 1 }; self.back = match self.full[search_start..].find(SEPARATOR) { Some(sep_ix) => search_start + sep_ix, None => self.full.len(), }; } Some(RelPath::from_str(result)) } } impl<'a> DoubleEndedIterator for RelPathComponents<'a> { fn next_back(&mut self) -> Option { if let Some(sep_ix) = self.0.rfind(SEPARATOR) { let (head, tail) = self.0.split_at(sep_ix); self.0 = head; Some(&tail[1..]) } else if self.0.is_empty() { None } else { let result = self.0; self.0 = ""; Some(result) } } } impl<'de> serde::Deserialize<'de> for RelPathBuf { fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { let path = String::deserialize(deserializer)?; let rel_path = RelPath::new(Path::new(&path), PathStyle::local()).map_err(serde::de::Error::custom)?; Ok(rel_path.into_owned()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_rel_path_new() { assert!(RelPath::new(Path::new("/"), PathStyle::local()).is_err()); assert!(RelPath::new(Path::new("//"), PathStyle::local()).is_err()); assert!(RelPath::new(Path::new("/foo/"), PathStyle::local()).is_err()); let path = RelPath::new("foo/".as_ref(), PathStyle::local()).unwrap(); assert_eq!(path, rel_path("foo").into()); assert!(matches!(path, Cow::Borrowed(_))); let path = RelPath::new("foo\\".as_ref(), PathStyle::Windows).unwrap(); assert_eq!(path, rel_path("foo").into()); assert!(matches!(path, Cow::Borrowed(_))); assert_eq!( RelPath::new("foo/bar/../baz/./quux/".as_ref(), PathStyle::local()) .unwrap() .as_ref(), rel_path("foo/baz/quux") ); let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Unix).unwrap(); assert_eq!(path.as_ref(), rel_path("foo/bar")); assert!(matches!(path, Cow::Borrowed(_))); let path = RelPath::new(".\\foo".as_ref(), PathStyle::Windows).unwrap(); assert_eq!(path, rel_path("foo").into()); assert!(matches!(path, Cow::Borrowed(_))); let path = RelPath::new("./.\\./foo/\\/".as_ref(), PathStyle::Windows).unwrap(); assert_eq!(path, rel_path("foo").into()); assert!(matches!(path, Cow::Borrowed(_))); let path = RelPath::new("foo/./bar".as_ref(), PathStyle::Unix).unwrap(); assert_eq!(path.as_ref(), rel_path("foo/bar")); assert!(matches!(path, Cow::Owned(_))); let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Windows).unwrap(); assert_eq!(path.as_ref(), rel_path("foo/bar")); assert!(matches!(path, Cow::Borrowed(_))); let path = RelPath::new(".\\foo\\bar".as_ref(), PathStyle::Windows).unwrap(); assert_eq!(path.as_ref(), rel_path("foo/bar")); assert!(matches!(path, Cow::Owned(_))); } #[test] fn test_rel_path_components() { let path = rel_path("foo/bar/baz"); assert_eq!( path.components().collect::>(), vec!["foo", "bar", "baz"] ); assert_eq!( path.components().rev().collect::>(), vec!["baz", "bar", "foo"] ); let path = rel_path(""); let mut components = path.components(); assert_eq!(components.next(), None); } #[test] fn test_rel_path_ancestors() { let path = rel_path("foo/bar/baz"); let mut ancestors = path.ancestors(); assert_eq!(ancestors.next(), Some(rel_path("foo/bar/baz"))); assert_eq!(ancestors.next(), Some(rel_path("foo/bar"))); assert_eq!(ancestors.next(), Some(rel_path("foo"))); assert_eq!(ancestors.next(), Some(rel_path(""))); assert_eq!(ancestors.next(), None); let path = rel_path("foo"); let mut ancestors = path.ancestors(); assert_eq!(ancestors.next(), Some(rel_path("foo"))); assert_eq!(ancestors.next(), Some(RelPath::empty())); assert_eq!(ancestors.next(), None); let path = RelPath::empty(); let mut ancestors = path.ancestors(); assert_eq!(ancestors.next(), Some(RelPath::empty())); assert_eq!(ancestors.next(), None); let path = rel_path("foo/bar/baz"); let mut ancestors = path.ancestors(); assert_eq!(ancestors.next_back(), Some(rel_path(""))); assert_eq!(ancestors.next_back(), Some(rel_path("foo"))); assert_eq!(ancestors.next_back(), Some(rel_path("foo/bar"))); assert_eq!(ancestors.next_back(), Some(rel_path("foo/bar/baz"))); assert_eq!(ancestors.next_back(), None); let path = rel_path("foo/bar/baz"); let mut ancestors = path.ancestors(); assert_eq!(ancestors.next(), Some(rel_path("foo/bar/baz"))); assert_eq!(ancestors.next_back(), Some(rel_path(""))); assert_eq!(ancestors.next(), Some(rel_path("foo/bar"))); assert_eq!(ancestors.next_back(), Some(rel_path("foo"))); assert_eq!(ancestors.next(), None); assert_eq!(ancestors.next_back(), None); let path = rel_path("foo"); let mut ancestors = path.ancestors(); assert_eq!(ancestors.next_back(), Some(RelPath::empty())); assert_eq!(ancestors.next_back(), Some(rel_path("foo"))); assert_eq!(ancestors.next_back(), None); let path = RelPath::empty(); let mut ancestors = path.ancestors(); assert_eq!(ancestors.next_back(), Some(RelPath::empty())); assert_eq!(ancestors.next_back(), None); let path = rel_path("über/x"); let mut ancestors = path.ancestors(); assert_eq!(ancestors.next_back(), Some(RelPath::empty())); assert_eq!(ancestors.next_back(), Some(rel_path("über"))); assert_eq!(ancestors.next_back(), Some(rel_path("über/x"))); assert_eq!(ancestors.next_back(), None); } #[test] fn test_rel_path_parent() { assert_eq!(rel_path("foo/bar/baz").parent(), Some(rel_path("foo/bar"))); assert_eq!(rel_path("foo").parent(), Some(RelPath::empty())); assert_eq!(rel_path("").parent(), None); } #[test] fn test_rel_path_partial_ord_is_compatible_with_std() { let test_cases = ["a/b/c", "relative/path/with/dot.", "relative/path/with.dot"]; for (i, lhs) in test_cases.iter().enumerate() { for rhs in &test_cases[i + 1..] { assert_eq!( Path::new(lhs).cmp(Path::new(rhs)), RelPath::from_unix_str(lhs) .unwrap() .cmp(RelPath::from_unix_str(rhs).unwrap()), "ordering mismatch for {:?} vs {:?}", lhs, rhs, ); } } } #[test] fn test_strip_prefix() { let parent = rel_path(""); let child = rel_path(".foo"); assert!(child.starts_with(parent)); assert_eq!(child.strip_prefix(parent).unwrap(), child); } #[test] fn test_ends_with() { assert!(rel_path("foo/bar").ends_with(rel_path("bar"))); assert!(rel_path("foo/bar").ends_with(rel_path("foo/bar"))); assert!(rel_path("foo/bar").ends_with(RelPath::empty())); assert!(RelPath::empty().ends_with(RelPath::empty())); assert!(!rel_path("foobar").ends_with(rel_path("bar"))); } #[test] fn test_rel_path_constructors_absolute_path() { assert!(RelPath::new(Path::new("/a/b"), PathStyle::Windows).is_err()); assert!(RelPath::new(Path::new("\\a\\b"), PathStyle::Windows).is_err()); assert!(RelPath::new(Path::new("/a/b"), PathStyle::Unix).is_err()); assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Windows).is_err()); assert!(RelPath::new(Path::new("C:\\a\\b"), PathStyle::Windows).is_err()); assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Unix).is_ok()); } #[test] fn test_pop() { let mut path = rel_path_buf("a/b"); path.pop(); assert_eq!(path.as_rel_path().as_unix_str(), "a"); path.pop(); assert_eq!(path.as_rel_path().as_unix_str(), ""); path.pop(); assert_eq!(path.as_rel_path().as_unix_str(), ""); } #[test] fn test_len() { assert_eq!(RelPath::empty().len(), 0); assert_eq!(rel_path("a").len(), 1); assert_eq!(rel_path("a/b").len(), 2); assert_eq!(rel_path("a/b/c").len(), 3); } #[test] fn test_set_extension() { let mut path = rel_path_buf("a/b/c.txt"); assert!(path.set_extension("rs")); assert_eq!(path.as_rel_path().as_unix_str(), "a/b/c.rs"); let mut single = rel_path_buf("file.txt"); assert!(single.set_extension("md")); assert_eq!(single.as_rel_path().as_unix_str(), "file.md"); let mut no_ext = rel_path_buf("a/b/c"); assert!(no_ext.set_extension("rs")); assert_eq!(no_ext.as_rel_path().as_unix_str(), "a/b/c.rs"); } }