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#[repr(transparent)]
 13#[derive(PartialEq, Eq, Hash, Serialize)]
 14pub struct RelPath(str);
 15
 16#[derive(Clone, Serialize, Deserialize)]
 17pub struct RelPathBuf(String);
 18
 19impl RelPath {
 20    pub fn empty() -> &'static Self {
 21        unsafe { Self::new_unchecked("") }
 22    }
 23
 24    #[track_caller]
 25    pub fn new<'a>(path: &'a Path, path_style: PathStyle) -> Result<Cow<'a, Self>> {
 26        let mut path = path.to_str().context("non utf-8 path")?;
 27
 28        let (prefixes, suffixes): (&[_], &[_]) = match path_style {
 29            PathStyle::Posix => (&["./"], &['/']),
 30            PathStyle::Windows => (&["./", ".\\"], &['/', '\\']),
 31        };
 32
 33        while prefixes.iter().any(|prefix| path.starts_with(prefix)) {
 34            path = &path[prefixes[0].len()..];
 35        }
 36        while let Some(prefix) = path.strip_suffix(suffixes)
 37            && !prefix.is_empty()
 38        {
 39            path = prefix;
 40        }
 41
 42        if is_absolute(&path, path_style) {
 43            return Err(anyhow!("absolute path not allowed: {path:?}"));
 44        }
 45
 46        let mut string = Cow::Borrowed(path);
 47        if path_style == PathStyle::Windows && path.contains('\\') {
 48            string = Cow::Owned(string.as_ref().replace('\\', "/"))
 49        }
 50
 51        let mut result = match string {
 52            Cow::Borrowed(string) => Cow::Borrowed(unsafe { Self::new_unchecked(string) }),
 53            Cow::Owned(string) => Cow::Owned(RelPathBuf(string)),
 54        };
 55
 56        if result
 57            .components()
 58            .any(|component| component == "" || component == "." || component == "..")
 59        {
 60            let mut normalized = RelPathBuf::new();
 61            for component in result.components() {
 62                match component {
 63                    "" => {}
 64                    "." => {}
 65                    ".." => {
 66                        if !normalized.pop() {
 67                            return Err(anyhow!("path is not relative: {result:?}"));
 68                        }
 69                    }
 70                    other => normalized.push(&RelPath::unix(other)?),
 71                }
 72            }
 73            result = Cow::Owned(normalized)
 74        }
 75
 76        Ok(result)
 77    }
 78
 79    #[track_caller]
 80    pub fn unix<S: AsRef<Path> + ?Sized>(path: &S) -> anyhow::Result<&Self> {
 81        let path = path.as_ref();
 82        match Self::new(path, PathStyle::Posix)? {
 83            Cow::Borrowed(path) => Ok(path),
 84            Cow::Owned(_) => Err(anyhow!("invalid relative path {path:?}")),
 85        }
 86    }
 87
 88    unsafe fn new_unchecked(s: &str) -> &Self {
 89        unsafe { &*(s as *const str as *const Self) }
 90    }
 91
 92    pub fn is_empty(&self) -> bool {
 93        self.0.is_empty()
 94    }
 95
 96    pub fn components(&self) -> RelPathComponents<'_> {
 97        RelPathComponents(&self.0)
 98    }
 99
100    pub fn ancestors(&self) -> RelPathAncestors<'_> {
101        RelPathAncestors(Some(&self.0))
102    }
103
104    pub fn file_name(&self) -> Option<&str> {
105        self.components().next_back()
106    }
107
108    pub fn file_stem(&self) -> Option<&str> {
109        Some(self.as_std_path().file_stem()?.to_str().unwrap())
110    }
111
112    pub fn extension(&self) -> Option<&str> {
113        Some(self.as_std_path().extension()?.to_str().unwrap())
114    }
115
116    pub fn parent(&self) -> Option<&Self> {
117        let mut components = self.components();
118        components.next_back()?;
119        Some(components.rest())
120    }
121
122    pub fn starts_with(&self, other: &Self) -> bool {
123        self.strip_prefix(other).is_ok()
124    }
125
126    pub fn ends_with(&self, other: &Self) -> bool {
127        if let Some(suffix) = self.0.strip_suffix(&other.0) {
128            if suffix.ends_with('/') {
129                return true;
130            } else if suffix.is_empty() {
131                return true;
132            }
133        }
134        false
135    }
136
137    pub fn strip_prefix<'a>(&'a self, other: &Self) -> Result<&'a Self> {
138        if other.is_empty() {
139            return Ok(self);
140        }
141        if let Some(suffix) = self.0.strip_prefix(&other.0) {
142            if let Some(suffix) = suffix.strip_prefix('/') {
143                return Ok(unsafe { Self::new_unchecked(suffix) });
144            } else if suffix.is_empty() {
145                return Ok(Self::empty());
146            }
147        }
148        Err(anyhow!("failed to strip prefix: {other:?} from {self:?}"))
149    }
150
151    pub fn len(&self) -> usize {
152        self.0.matches('/').count() + 1
153    }
154
155    pub fn last_n_components(&self, count: usize) -> Option<&Self> {
156        let len = self.len();
157        if len >= count {
158            let mut components = self.components();
159            for _ in 0..(len - count) {
160                components.next()?;
161            }
162            Some(components.rest())
163        } else {
164            None
165        }
166    }
167
168    pub fn join(&self, other: &Self) -> Arc<Self> {
169        let result = if self.0.is_empty() {
170            Cow::Borrowed(&other.0)
171        } else if other.0.is_empty() {
172            Cow::Borrowed(&self.0)
173        } else {
174            Cow::Owned(format!("{}/{}", &self.0, &other.0))
175        };
176        Arc::from(unsafe { Self::new_unchecked(result.as_ref()) })
177    }
178
179    pub fn to_proto(&self) -> String {
180        self.as_unix_str().to_owned()
181    }
182
183    pub fn to_rel_path_buf(&self) -> RelPathBuf {
184        RelPathBuf(self.0.to_string())
185    }
186
187    pub fn from_proto(path: &str) -> Result<Arc<Self>> {
188        Ok(Arc::from(Self::unix(path)?))
189    }
190
191    pub fn as_unix_str(&self) -> &str {
192        &self.0
193    }
194
195    pub fn into_arc(&self) -> Arc<Self> {
196        Arc::from(self)
197    }
198
199    pub fn display(&self, style: PathStyle) -> Cow<'_, str> {
200        match style {
201            PathStyle::Posix => Cow::Borrowed(&self.0),
202            PathStyle::Windows => Cow::Owned(self.0.replace('/', "\\")),
203        }
204    }
205
206    pub fn as_std_path(&self) -> &Path {
207        Path::new(&self.0)
208    }
209}
210
211impl ToOwned for RelPath {
212    type Owned = RelPathBuf;
213
214    fn to_owned(&self) -> Self::Owned {
215        self.to_rel_path_buf()
216    }
217}
218
219impl Borrow<RelPath> for RelPathBuf {
220    fn borrow(&self) -> &RelPath {
221        self.as_rel_path()
222    }
223}
224
225impl PartialOrd for RelPath {
226    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
227        Some(self.cmp(other))
228    }
229}
230
231impl Ord for RelPath {
232    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
233        self.components().cmp(other.components())
234    }
235}
236
237impl fmt::Debug for RelPath {
238    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239        fmt::Debug::fmt(&self.0, f)
240    }
241}
242
243impl fmt::Debug for RelPathBuf {
244    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245        fmt::Debug::fmt(&self.0, f)
246    }
247}
248
249impl RelPathBuf {
250    pub fn new() -> Self {
251        Self(String::new())
252    }
253
254    pub fn pop(&mut self) -> bool {
255        if let Some(ix) = self.0.rfind('/') {
256            self.0.truncate(ix);
257            true
258        } else if !self.is_empty() {
259            self.0.clear();
260            true
261        } else {
262            false
263        }
264    }
265
266    pub fn push(&mut self, path: &RelPath) {
267        if !self.is_empty() {
268            self.0.push('/');
269        }
270        self.0.push_str(&path.0);
271    }
272
273    pub fn as_rel_path(&self) -> &RelPath {
274        unsafe { RelPath::new_unchecked(self.0.as_str()) }
275    }
276
277    pub fn set_extension(&mut self, extension: &str) -> bool {
278        if let Some(filename) = self.file_name() {
279            let mut filename = PathBuf::from(filename);
280            filename.set_extension(extension);
281            self.pop();
282            self.0.push_str(filename.to_str().unwrap());
283            true
284        } else {
285            false
286        }
287    }
288}
289
290impl Into<Arc<RelPath>> for RelPathBuf {
291    fn into(self) -> Arc<RelPath> {
292        Arc::from(self.as_rel_path())
293    }
294}
295
296impl AsRef<RelPath> for RelPathBuf {
297    fn as_ref(&self) -> &RelPath {
298        self.as_rel_path()
299    }
300}
301
302impl Deref for RelPathBuf {
303    type Target = RelPath;
304
305    fn deref(&self) -> &Self::Target {
306        self.as_ref()
307    }
308}
309
310impl<'a> From<&'a RelPath> for Cow<'a, RelPath> {
311    fn from(value: &'a RelPath) -> Self {
312        Self::Borrowed(value)
313    }
314}
315
316impl From<&RelPath> for Arc<RelPath> {
317    fn from(rel_path: &RelPath) -> Self {
318        let bytes: Arc<str> = Arc::from(&rel_path.0);
319        unsafe { Arc::from_raw(Arc::into_raw(bytes) as *const RelPath) }
320    }
321}
322
323#[cfg(any(test, feature = "test-support"))]
324#[track_caller]
325pub fn rel_path(path: &str) -> &RelPath {
326    RelPath::unix(path).unwrap()
327}
328
329impl PartialEq<str> for RelPath {
330    fn eq(&self, other: &str) -> bool {
331        self.0 == *other
332    }
333}
334
335pub struct RelPathComponents<'a>(&'a str);
336
337pub struct RelPathAncestors<'a>(Option<&'a str>);
338
339const SEPARATOR: char = '/';
340
341impl<'a> RelPathComponents<'a> {
342    pub fn rest(&self) -> &'a RelPath {
343        unsafe { RelPath::new_unchecked(self.0) }
344    }
345}
346
347impl<'a> Iterator for RelPathComponents<'a> {
348    type Item = &'a str;
349
350    fn next(&mut self) -> Option<Self::Item> {
351        if let Some(sep_ix) = self.0.find(SEPARATOR) {
352            let (head, tail) = self.0.split_at(sep_ix);
353            self.0 = &tail[1..];
354            Some(head)
355        } else if self.0.is_empty() {
356            None
357        } else {
358            let result = self.0;
359            self.0 = "";
360            Some(result)
361        }
362    }
363}
364
365impl<'a> Iterator for RelPathAncestors<'a> {
366    type Item = &'a RelPath;
367
368    fn next(&mut self) -> Option<Self::Item> {
369        let result = self.0?;
370        if let Some(sep_ix) = result.rfind(SEPARATOR) {
371            self.0 = Some(&result[..sep_ix]);
372        } else if !result.is_empty() {
373            self.0 = Some("");
374        } else {
375            self.0 = None;
376        }
377        Some(unsafe { RelPath::new_unchecked(result) })
378    }
379}
380
381impl<'a> DoubleEndedIterator for RelPathComponents<'a> {
382    fn next_back(&mut self) -> Option<Self::Item> {
383        if let Some(sep_ix) = self.0.rfind(SEPARATOR) {
384            let (head, tail) = self.0.split_at(sep_ix);
385            self.0 = head;
386            Some(&tail[1..])
387        } else if self.0.is_empty() {
388            None
389        } else {
390            let result = self.0;
391            self.0 = "";
392            Some(result)
393        }
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400    use itertools::Itertools;
401    use pretty_assertions::assert_matches;
402
403    #[test]
404    fn test_rel_path_new() {
405        assert!(RelPath::new(Path::new("/"), PathStyle::local()).is_err());
406        assert!(RelPath::new(Path::new("//"), PathStyle::local()).is_err());
407        assert!(RelPath::new(Path::new("/foo/"), PathStyle::local()).is_err());
408
409        let path = RelPath::new("foo/".as_ref(), PathStyle::local()).unwrap();
410        assert_eq!(path, rel_path("foo").into());
411        assert_matches!(path, Cow::Borrowed(_));
412
413        let path = RelPath::new("foo\\".as_ref(), PathStyle::Windows).unwrap();
414        assert_eq!(path, rel_path("foo").into());
415        assert_matches!(path, Cow::Borrowed(_));
416
417        assert_eq!(
418            RelPath::new("foo/bar/../baz/./quux/".as_ref(), PathStyle::local())
419                .unwrap()
420                .as_ref(),
421            rel_path("foo/baz/quux")
422        );
423
424        let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Posix).unwrap();
425        assert_eq!(path.as_ref(), rel_path("foo/bar"));
426        assert_matches!(path, Cow::Borrowed(_));
427
428        let path = RelPath::new(".\\foo".as_ref(), PathStyle::Windows).unwrap();
429        assert_eq!(path, rel_path("foo").into());
430        assert_matches!(path, Cow::Borrowed(_));
431
432        let path = RelPath::new("./.\\./foo/\\/".as_ref(), PathStyle::Windows).unwrap();
433        assert_eq!(path, rel_path("foo").into());
434        assert_matches!(path, Cow::Borrowed(_));
435
436        let path = RelPath::new("foo/./bar".as_ref(), PathStyle::Posix).unwrap();
437        assert_eq!(path.as_ref(), rel_path("foo/bar"));
438        assert_matches!(path, Cow::Owned(_));
439
440        let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Windows).unwrap();
441        assert_eq!(path.as_ref(), rel_path("foo/bar"));
442        assert_matches!(path, Cow::Borrowed(_));
443
444        let path = RelPath::new(".\\foo\\bar".as_ref(), PathStyle::Windows).unwrap();
445        assert_eq!(path.as_ref(), rel_path("foo/bar"));
446        assert_matches!(path, Cow::Owned(_));
447    }
448
449    #[test]
450    fn test_rel_path_components() {
451        let path = rel_path("foo/bar/baz");
452        assert_eq!(
453            path.components().collect::<Vec<_>>(),
454            vec!["foo", "bar", "baz"]
455        );
456        assert_eq!(
457            path.components().rev().collect::<Vec<_>>(),
458            vec!["baz", "bar", "foo"]
459        );
460
461        let path = rel_path("");
462        let mut components = path.components();
463        assert_eq!(components.next(), None);
464    }
465
466    #[test]
467    fn test_rel_path_ancestors() {
468        let path = rel_path("foo/bar/baz");
469        let mut ancestors = path.ancestors();
470        assert_eq!(ancestors.next(), Some(rel_path("foo/bar/baz")));
471        assert_eq!(ancestors.next(), Some(rel_path("foo/bar")));
472        assert_eq!(ancestors.next(), Some(rel_path("foo")));
473        assert_eq!(ancestors.next(), Some(rel_path("")));
474        assert_eq!(ancestors.next(), None);
475
476        let path = rel_path("foo");
477        let mut ancestors = path.ancestors();
478        assert_eq!(ancestors.next(), Some(rel_path("foo")));
479        assert_eq!(ancestors.next(), Some(RelPath::empty()));
480        assert_eq!(ancestors.next(), None);
481
482        let path = RelPath::empty();
483        let mut ancestors = path.ancestors();
484        assert_eq!(ancestors.next(), Some(RelPath::empty()));
485        assert_eq!(ancestors.next(), None);
486    }
487
488    #[test]
489    fn test_rel_path_parent() {
490        assert_eq!(rel_path("foo/bar/baz").parent(), Some(rel_path("foo/bar")));
491        assert_eq!(rel_path("foo").parent(), Some(RelPath::empty()));
492        assert_eq!(rel_path("").parent(), None);
493    }
494
495    #[test]
496    fn test_rel_path_partial_ord_is_compatible_with_std() {
497        let test_cases = ["a/b/c", "relative/path/with/dot.", "relative/path/with.dot"];
498        for [lhs, rhs] in test_cases.iter().array_combinations::<2>() {
499            assert_eq!(
500                Path::new(lhs).cmp(Path::new(rhs)),
501                RelPath::unix(lhs)
502                    .unwrap()
503                    .cmp(&RelPath::unix(rhs).unwrap())
504            );
505        }
506    }
507
508    #[test]
509    fn test_strip_prefix() {
510        let parent = rel_path("");
511        let child = rel_path(".foo");
512
513        assert!(child.starts_with(parent));
514        assert_eq!(child.strip_prefix(parent).unwrap(), child);
515    }
516
517    #[test]
518    fn test_rel_path_constructors_absolute_path() {
519        assert!(RelPath::new(Path::new("/a/b"), PathStyle::Windows).is_err());
520        assert!(RelPath::new(Path::new("\\a\\b"), PathStyle::Windows).is_err());
521        assert!(RelPath::new(Path::new("/a/b"), PathStyle::Posix).is_err());
522        assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Windows).is_err());
523        assert!(RelPath::new(Path::new("C:\\a\\b"), PathStyle::Windows).is_err());
524        assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Posix).is_ok());
525    }
526
527    #[test]
528    fn test_pop() {
529        let mut path = rel_path("a/b").to_rel_path_buf();
530        path.pop();
531        assert_eq!(path.as_rel_path().as_unix_str(), "a");
532        path.pop();
533        assert_eq!(path.as_rel_path().as_unix_str(), "");
534        path.pop();
535        assert_eq!(path.as_rel_path().as_unix_str(), "");
536    }
537}