rel_path.rs

  1use crate::paths::{PathStyle, is_absolute};
  2use anyhow::{Context as _, Result, anyhow};
  3use serde::{Deserialize, Serialize};
  4use std::{
  5    borrow::{Borrow, Cow},
  6    fmt,
  7    ops::Deref,
  8    path::{Path, PathBuf},
  9    sync::Arc,
 10};
 11
 12/// A file system path that is guaranteed to be relative and normalized.
 13///
 14/// This type can be used to represent paths in a uniform way, regardless of
 15/// whether they refer to Windows or POSIX file systems, and regardless of
 16/// the host platform.
 17///
 18/// Internally, paths are stored in POSIX ('/'-delimited) format, but they can
 19/// be displayed in either POSIX or Windows format.
 20///
 21/// Relative paths are also guaranteed to be valid unicode.
 22#[repr(transparent)]
 23#[derive(PartialEq, Eq, Hash, Serialize)]
 24pub struct RelPath(str);
 25
 26/// An owned representation of a file system path that is guaranteed to be
 27/// relative and normalized.
 28///
 29/// This type is to [`RelPath`] as [`std::path::PathBuf`] is to [`std::path::Path`]
 30#[derive(PartialEq, Eq, Clone, Ord, PartialOrd, Serialize)]
 31pub struct RelPathBuf(String);
 32
 33impl RelPath {
 34    /// Creates an empty [`RelPath`].
 35    pub fn empty() -> &'static Self {
 36        Self::new_unchecked("")
 37    }
 38
 39    /// Converts a path with a given style into a [`RelPath`].
 40    ///
 41    /// Returns an error if the path is absolute, or is not valid unicode.
 42    ///
 43    /// This method will normalize the path by removing `.` components,
 44    /// processing `..` components, and removing trailing separators. It does
 45    /// not allocate unless it's necessary to reformat the path.
 46    #[track_caller]
 47    pub fn new<'a>(path: &'a Path, path_style: PathStyle) -> Result<Cow<'a, Self>> {
 48        let mut path = path.to_str().context("non utf-8 path")?;
 49
 50        let (prefixes, suffixes): (&[_], &[_]) = match path_style {
 51            PathStyle::Posix => (&["./"], &['/']),
 52            PathStyle::Windows => (&["./", ".\\"], &['/', '\\']),
 53        };
 54
 55        while prefixes.iter().any(|prefix| path.starts_with(prefix)) {
 56            path = &path[prefixes[0].len()..];
 57        }
 58        while let Some(prefix) = path.strip_suffix(suffixes)
 59            && !prefix.is_empty()
 60        {
 61            path = prefix;
 62        }
 63
 64        if is_absolute(&path, path_style) {
 65            return Err(anyhow!("absolute path not allowed: {path:?}"));
 66        }
 67
 68        let mut string = Cow::Borrowed(path);
 69        if path_style == PathStyle::Windows && path.contains('\\') {
 70            string = Cow::Owned(string.as_ref().replace('\\', "/"))
 71        }
 72
 73        let mut result = match string {
 74            Cow::Borrowed(string) => Cow::Borrowed(Self::new_unchecked(string)),
 75            Cow::Owned(string) => Cow::Owned(RelPathBuf(string)),
 76        };
 77
 78        if result
 79            .components()
 80            .any(|component| component == "" || component == "." || component == "..")
 81        {
 82            let mut normalized = RelPathBuf::new();
 83            for component in result.components() {
 84                match component {
 85                    "" => {}
 86                    "." => {}
 87                    ".." => {
 88                        if !normalized.pop() {
 89                            return Err(anyhow!("path is not relative: {result:?}"));
 90                        }
 91                    }
 92                    other => normalized.push(RelPath::new_unchecked(other)),
 93                }
 94            }
 95            result = Cow::Owned(normalized)
 96        }
 97
 98        Ok(result)
 99    }
100
101    /// Converts a path that is already normalized and uses '/' separators
102    /// into a [`RelPath`] .
103    ///
104    /// Returns an error if the path is not already in the correct format.
105    #[track_caller]
106    pub fn unix<S: AsRef<Path> + ?Sized>(path: &S) -> anyhow::Result<&Self> {
107        let path = path.as_ref();
108        match Self::new(path, PathStyle::Posix)? {
109            Cow::Borrowed(path) => Ok(path),
110            Cow::Owned(_) => Err(anyhow!("invalid relative path {path:?}")),
111        }
112    }
113
114    fn new_unchecked(s: &str) -> &Self {
115        // Safety: `RelPath` is a transparent wrapper around `str`.
116        unsafe { &*(s as *const str as *const Self) }
117    }
118
119    pub fn is_empty(&self) -> bool {
120        self.0.is_empty()
121    }
122
123    pub fn components(&self) -> RelPathComponents<'_> {
124        RelPathComponents(&self.0)
125    }
126
127    pub fn ancestors(&self) -> RelPathAncestors<'_> {
128        RelPathAncestors(Some(&self.0))
129    }
130
131    pub fn file_name(&self) -> Option<&str> {
132        self.components().next_back()
133    }
134
135    pub fn file_stem(&self) -> Option<&str> {
136        Some(self.as_std_path().file_stem()?.to_str().unwrap())
137    }
138
139    pub fn extension(&self) -> Option<&str> {
140        Some(self.as_std_path().extension()?.to_str().unwrap())
141    }
142
143    pub fn parent(&self) -> Option<&Self> {
144        let mut components = self.components();
145        components.next_back()?;
146        Some(components.rest())
147    }
148
149    pub fn starts_with(&self, other: &Self) -> bool {
150        self.strip_prefix(other).is_ok()
151    }
152
153    pub fn ends_with(&self, other: &Self) -> bool {
154        if let Some(suffix) = self.0.strip_suffix(&other.0) {
155            if suffix.ends_with('/') {
156                return true;
157            } else if suffix.is_empty() {
158                return true;
159            }
160        }
161        false
162    }
163
164    pub fn strip_prefix<'a>(&'a self, other: &Self) -> Result<&'a Self, StripPrefixError> {
165        if other.is_empty() {
166            return Ok(self);
167        }
168        if let Some(suffix) = self.0.strip_prefix(&other.0) {
169            if let Some(suffix) = suffix.strip_prefix('/') {
170                return Ok(Self::new_unchecked(suffix));
171            } else if suffix.is_empty() {
172                return Ok(Self::empty());
173            }
174        }
175        Err(StripPrefixError)
176    }
177
178    pub fn len(&self) -> usize {
179        self.0.matches('/').count() + 1
180    }
181
182    pub fn last_n_components(&self, count: usize) -> Option<&Self> {
183        let len = self.len();
184        if len >= count {
185            let mut components = self.components();
186            for _ in 0..(len - count) {
187                components.next()?;
188            }
189            Some(components.rest())
190        } else {
191            None
192        }
193    }
194
195    pub fn join(&self, other: &Self) -> Arc<Self> {
196        let result = if self.0.is_empty() {
197            Cow::Borrowed(&other.0)
198        } else if other.0.is_empty() {
199            Cow::Borrowed(&self.0)
200        } else {
201            Cow::Owned(format!("{}/{}", &self.0, &other.0))
202        };
203        Arc::from(Self::new_unchecked(result.as_ref()))
204    }
205
206    pub fn to_rel_path_buf(&self) -> RelPathBuf {
207        RelPathBuf(self.0.to_string())
208    }
209
210    pub fn into_arc(&self) -> Arc<Self> {
211        Arc::from(self)
212    }
213
214    /// Convert the path into the wire representation.
215    pub fn to_proto(&self) -> String {
216        self.as_unix_str().to_owned()
217    }
218
219    /// Load the path from its wire representation.
220    pub fn from_proto(path: &str) -> Result<Arc<Self>> {
221        Ok(Arc::from(Self::unix(path)?))
222    }
223
224    /// Convert the path into a string with the given path style.
225    ///
226    /// Whenever a path is presented to the user, it should be converted to
227    /// a string via this method.
228    pub fn display(&self, style: PathStyle) -> Cow<'_, str> {
229        match style {
230            PathStyle::Posix => Cow::Borrowed(&self.0),
231            PathStyle::Windows if self.0.contains('/') => Cow::Owned(self.0.replace('/', "\\")),
232            PathStyle::Windows => Cow::Borrowed(&self.0),
233        }
234    }
235
236    /// Get the internal unix-style representation of the path.
237    ///
238    /// This should not be shown to the user.
239    pub fn as_unix_str(&self) -> &str {
240        &self.0
241    }
242
243    /// Interprets the path as a [`std::path::Path`], suitable for file system calls.
244    ///
245    /// This is guaranteed to be a valid path regardless of the host platform, because
246    /// the `/` is accepted as a path separator on windows.
247    ///
248    /// This should not be shown to the user.
249    pub fn as_std_path(&self) -> &Path {
250        Path::new(&self.0)
251    }
252}
253
254#[derive(Debug)]
255pub struct StripPrefixError;
256
257impl ToOwned for RelPath {
258    type Owned = RelPathBuf;
259
260    fn to_owned(&self) -> Self::Owned {
261        self.to_rel_path_buf()
262    }
263}
264
265impl Borrow<RelPath> for RelPathBuf {
266    fn borrow(&self) -> &RelPath {
267        self.as_rel_path()
268    }
269}
270
271impl PartialOrd for RelPath {
272    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
273        Some(self.cmp(other))
274    }
275}
276
277impl Ord for RelPath {
278    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
279        self.components().cmp(other.components())
280    }
281}
282
283impl fmt::Debug for RelPath {
284    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285        fmt::Debug::fmt(&self.0, f)
286    }
287}
288
289impl fmt::Debug for RelPathBuf {
290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291        fmt::Debug::fmt(&self.0, f)
292    }
293}
294
295impl RelPathBuf {
296    pub fn new() -> Self {
297        Self(String::new())
298    }
299
300    pub fn pop(&mut self) -> bool {
301        if let Some(ix) = self.0.rfind('/') {
302            self.0.truncate(ix);
303            true
304        } else if !self.is_empty() {
305            self.0.clear();
306            true
307        } else {
308            false
309        }
310    }
311
312    pub fn push(&mut self, path: &RelPath) {
313        if !self.is_empty() {
314            self.0.push('/');
315        }
316        self.0.push_str(&path.0);
317    }
318
319    pub fn as_rel_path(&self) -> &RelPath {
320        RelPath::new_unchecked(self.0.as_str())
321    }
322
323    pub fn set_extension(&mut self, extension: &str) -> bool {
324        if let Some(filename) = self.file_name() {
325            let mut filename = PathBuf::from(filename);
326            filename.set_extension(extension);
327            self.pop();
328            self.0.push_str(filename.to_str().unwrap());
329            true
330        } else {
331            false
332        }
333    }
334}
335
336impl<'de> Deserialize<'de> for RelPathBuf {
337    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
338    where
339        D: serde::Deserializer<'de>,
340    {
341        let path = String::deserialize(deserializer)?;
342        let rel_path =
343            RelPath::new(Path::new(&path), PathStyle::local()).map_err(serde::de::Error::custom)?;
344        Ok(rel_path.into_owned())
345    }
346}
347
348impl Into<Arc<RelPath>> for RelPathBuf {
349    fn into(self) -> Arc<RelPath> {
350        Arc::from(self.as_rel_path())
351    }
352}
353
354impl AsRef<Path> for RelPathBuf {
355    fn as_ref(&self) -> &Path {
356        self.as_std_path()
357    }
358}
359
360impl AsRef<Path> for RelPath {
361    fn as_ref(&self) -> &Path {
362        self.as_std_path()
363    }
364}
365
366impl AsRef<RelPath> for RelPathBuf {
367    fn as_ref(&self) -> &RelPath {
368        self.as_rel_path()
369    }
370}
371
372impl AsRef<RelPath> for RelPath {
373    fn as_ref(&self) -> &RelPath {
374        self
375    }
376}
377
378impl Deref for RelPathBuf {
379    type Target = RelPath;
380
381    fn deref(&self) -> &Self::Target {
382        self.as_ref()
383    }
384}
385
386impl<'a> From<&'a RelPath> for Cow<'a, RelPath> {
387    fn from(value: &'a RelPath) -> Self {
388        Self::Borrowed(value)
389    }
390}
391
392impl From<&RelPath> for Arc<RelPath> {
393    fn from(rel_path: &RelPath) -> Self {
394        let bytes: Arc<str> = Arc::from(&rel_path.0);
395        unsafe { Arc::from_raw(Arc::into_raw(bytes) as *const RelPath) }
396    }
397}
398
399#[cfg(any(test, feature = "test-support"))]
400#[track_caller]
401pub fn rel_path(path: &str) -> &RelPath {
402    RelPath::unix(path).unwrap()
403}
404
405#[cfg(any(test, feature = "test-support"))]
406#[track_caller]
407pub fn rel_path_buf(path: &str) -> RelPathBuf {
408    RelPath::unix(path).unwrap().to_rel_path_buf()
409}
410
411impl PartialEq<str> for RelPath {
412    fn eq(&self, other: &str) -> bool {
413        self.0 == *other
414    }
415}
416
417pub trait PathExt {
418    fn to_rel_path_buf(&self) -> Result<RelPathBuf>;
419}
420
421impl<T: AsRef<Path> + ?Sized> PathExt for T {
422    fn to_rel_path_buf(&self) -> Result<RelPathBuf> {
423        Ok(RelPath::new(self.as_ref(), PathStyle::local())?.into_owned())
424    }
425}
426
427#[derive(Default)]
428pub struct RelPathComponents<'a>(&'a str);
429
430pub struct RelPathAncestors<'a>(Option<&'a str>);
431
432const SEPARATOR: char = '/';
433
434impl<'a> RelPathComponents<'a> {
435    pub fn rest(&self) -> &'a RelPath {
436        RelPath::new_unchecked(self.0)
437    }
438}
439
440impl<'a> Iterator for RelPathComponents<'a> {
441    type Item = &'a str;
442
443    fn next(&mut self) -> Option<Self::Item> {
444        if let Some(sep_ix) = self.0.find(SEPARATOR) {
445            let (head, tail) = self.0.split_at(sep_ix);
446            self.0 = &tail[1..];
447            Some(head)
448        } else if self.0.is_empty() {
449            None
450        } else {
451            let result = self.0;
452            self.0 = "";
453            Some(result)
454        }
455    }
456}
457
458impl<'a> Iterator for RelPathAncestors<'a> {
459    type Item = &'a RelPath;
460
461    fn next(&mut self) -> Option<Self::Item> {
462        let result = self.0?;
463        if let Some(sep_ix) = result.rfind(SEPARATOR) {
464            self.0 = Some(&result[..sep_ix]);
465        } else if !result.is_empty() {
466            self.0 = Some("");
467        } else {
468            self.0 = None;
469        }
470        Some(RelPath::new_unchecked(result))
471    }
472}
473
474impl<'a> DoubleEndedIterator for RelPathComponents<'a> {
475    fn next_back(&mut self) -> Option<Self::Item> {
476        if let Some(sep_ix) = self.0.rfind(SEPARATOR) {
477            let (head, tail) = self.0.split_at(sep_ix);
478            self.0 = head;
479            Some(&tail[1..])
480        } else if self.0.is_empty() {
481            None
482        } else {
483            let result = self.0;
484            self.0 = "";
485            Some(result)
486        }
487    }
488}
489
490#[cfg(test)]
491mod tests {
492    use super::*;
493    use itertools::Itertools;
494    use pretty_assertions::assert_matches;
495
496    #[test]
497    fn test_rel_path_new() {
498        assert!(RelPath::new(Path::new("/"), PathStyle::local()).is_err());
499        assert!(RelPath::new(Path::new("//"), PathStyle::local()).is_err());
500        assert!(RelPath::new(Path::new("/foo/"), PathStyle::local()).is_err());
501
502        let path = RelPath::new("foo/".as_ref(), PathStyle::local()).unwrap();
503        assert_eq!(path, rel_path("foo").into());
504        assert_matches!(path, Cow::Borrowed(_));
505
506        let path = RelPath::new("foo\\".as_ref(), PathStyle::Windows).unwrap();
507        assert_eq!(path, rel_path("foo").into());
508        assert_matches!(path, Cow::Borrowed(_));
509
510        assert_eq!(
511            RelPath::new("foo/bar/../baz/./quux/".as_ref(), PathStyle::local())
512                .unwrap()
513                .as_ref(),
514            rel_path("foo/baz/quux")
515        );
516
517        let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Posix).unwrap();
518        assert_eq!(path.as_ref(), rel_path("foo/bar"));
519        assert_matches!(path, Cow::Borrowed(_));
520
521        let path = RelPath::new(".\\foo".as_ref(), PathStyle::Windows).unwrap();
522        assert_eq!(path, rel_path("foo").into());
523        assert_matches!(path, Cow::Borrowed(_));
524
525        let path = RelPath::new("./.\\./foo/\\/".as_ref(), PathStyle::Windows).unwrap();
526        assert_eq!(path, rel_path("foo").into());
527        assert_matches!(path, Cow::Borrowed(_));
528
529        let path = RelPath::new("foo/./bar".as_ref(), PathStyle::Posix).unwrap();
530        assert_eq!(path.as_ref(), rel_path("foo/bar"));
531        assert_matches!(path, Cow::Owned(_));
532
533        let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Windows).unwrap();
534        assert_eq!(path.as_ref(), rel_path("foo/bar"));
535        assert_matches!(path, Cow::Borrowed(_));
536
537        let path = RelPath::new(".\\foo\\bar".as_ref(), PathStyle::Windows).unwrap();
538        assert_eq!(path.as_ref(), rel_path("foo/bar"));
539        assert_matches!(path, Cow::Owned(_));
540    }
541
542    #[test]
543    fn test_rel_path_components() {
544        let path = rel_path("foo/bar/baz");
545        assert_eq!(
546            path.components().collect::<Vec<_>>(),
547            vec!["foo", "bar", "baz"]
548        );
549        assert_eq!(
550            path.components().rev().collect::<Vec<_>>(),
551            vec!["baz", "bar", "foo"]
552        );
553
554        let path = rel_path("");
555        let mut components = path.components();
556        assert_eq!(components.next(), None);
557    }
558
559    #[test]
560    fn test_rel_path_ancestors() {
561        let path = rel_path("foo/bar/baz");
562        let mut ancestors = path.ancestors();
563        assert_eq!(ancestors.next(), Some(rel_path("foo/bar/baz")));
564        assert_eq!(ancestors.next(), Some(rel_path("foo/bar")));
565        assert_eq!(ancestors.next(), Some(rel_path("foo")));
566        assert_eq!(ancestors.next(), Some(rel_path("")));
567        assert_eq!(ancestors.next(), None);
568
569        let path = rel_path("foo");
570        let mut ancestors = path.ancestors();
571        assert_eq!(ancestors.next(), Some(rel_path("foo")));
572        assert_eq!(ancestors.next(), Some(RelPath::empty()));
573        assert_eq!(ancestors.next(), None);
574
575        let path = RelPath::empty();
576        let mut ancestors = path.ancestors();
577        assert_eq!(ancestors.next(), Some(RelPath::empty()));
578        assert_eq!(ancestors.next(), None);
579    }
580
581    #[test]
582    fn test_rel_path_parent() {
583        assert_eq!(rel_path("foo/bar/baz").parent(), Some(rel_path("foo/bar")));
584        assert_eq!(rel_path("foo").parent(), Some(RelPath::empty()));
585        assert_eq!(rel_path("").parent(), None);
586    }
587
588    #[test]
589    fn test_rel_path_partial_ord_is_compatible_with_std() {
590        let test_cases = ["a/b/c", "relative/path/with/dot.", "relative/path/with.dot"];
591        for [lhs, rhs] in test_cases.iter().array_combinations::<2>() {
592            assert_eq!(
593                Path::new(lhs).cmp(Path::new(rhs)),
594                RelPath::unix(lhs)
595                    .unwrap()
596                    .cmp(&RelPath::unix(rhs).unwrap())
597            );
598        }
599    }
600
601    #[test]
602    fn test_strip_prefix() {
603        let parent = rel_path("");
604        let child = rel_path(".foo");
605
606        assert!(child.starts_with(parent));
607        assert_eq!(child.strip_prefix(parent).unwrap(), child);
608    }
609
610    #[test]
611    fn test_rel_path_constructors_absolute_path() {
612        assert!(RelPath::new(Path::new("/a/b"), PathStyle::Windows).is_err());
613        assert!(RelPath::new(Path::new("\\a\\b"), PathStyle::Windows).is_err());
614        assert!(RelPath::new(Path::new("/a/b"), PathStyle::Posix).is_err());
615        assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Windows).is_err());
616        assert!(RelPath::new(Path::new("C:\\a\\b"), PathStyle::Windows).is_err());
617        assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Posix).is_ok());
618    }
619
620    #[test]
621    fn test_pop() {
622        let mut path = rel_path("a/b").to_rel_path_buf();
623        path.pop();
624        assert_eq!(path.as_rel_path().as_unix_str(), "a");
625        path.pop();
626        assert_eq!(path.as_rel_path().as_unix_str(), "");
627        path.pop();
628        assert_eq!(path.as_rel_path().as_unix_str(), "");
629    }
630}