Skip to main content

pijul_core/
path.rs

1//! Treating strings as paths. For portability reasons, paths must
2//! internally be treated as strings, and converted to paths only by
3//! the backend, if required (in-memory backends will typically not
4//! need that conversion).
5
6/// Returns the parent of the path, if it exists. This function tries
7/// to replicate the behaviour of `std::path::Path::parent`, but with
8/// `&str` instead of `Path`.
9///
10/// ```ignore
11/// use pijul_core::path::parent;
12/// assert_eq!(parent("/foo/bar"), Some("/foo"));
13/// assert_eq!(parent("foo"), Some(""));
14/// assert_eq!(parent("/"), None);
15/// assert_eq!(parent(""), None);
16/// ```
17pub fn parent(mut path: &str) -> Option<&str> {
18    loop {
19        if path == "/" || path.is_empty() {
20            return None;
21        } else if let Some(i) = path.rfind('/') {
22            let (a, b) = path.split_at(i);
23            if b == "/" {
24                path = a
25            } else {
26                return Some(a);
27            }
28        } else {
29            return Some("");
30        }
31    }
32}
33
34/// Returns the file name of the path. if it exists. This function
35/// tries to replicate the behaviour of `std::path::Path::file_name`,
36/// but with `&str` instead of `Path`.
37///
38/// Like the original, returns `None` if the path terminates in `..`.
39///
40/// ```ignore
41/// use pijul_core::path::file_name;
42/// assert_eq!(file_name("/usr/bin/"), Some("bin"));
43/// assert_eq!(file_name("tmp/foo.txt"), Some("foo.txt"));
44/// assert_eq!(file_name("foo.txt/."), Some("foo.txt"));
45/// assert_eq!(file_name("foo.txt/.//"), Some("foo.txt"));
46/// assert_eq!(file_name("foo.txt/.."), None);
47/// assert_eq!(file_name("/"), None);
48/// ```
49pub fn file_name(mut path: &str) -> Option<&str> {
50    if path == "/" || path.is_empty() {
51        None
52    } else {
53        while let Some(i) = path.rfind('/') {
54            let (_, f) = path.split_at(i + 1);
55            if f == ".." {
56                return None;
57            } else if f.is_empty() || f == "." {
58                path = path.split_at(i).0
59            } else {
60                return Some(f);
61            }
62        }
63        Some(path)
64    }
65}
66
67#[test]
68fn test_file_name() {
69    assert_eq!(file_name("/usr/bin/"), Some("bin"));
70    assert_eq!(file_name("tmp/foo.txt"), Some("foo.txt"));
71    assert_eq!(file_name("foo.txt/."), Some("foo.txt"));
72    assert_eq!(file_name("foo.txt/.//"), Some("foo.txt"));
73    assert_eq!(file_name("foo.txt/.."), None);
74    assert_eq!(file_name("/"), None);
75}
76
77/// Returns an iterator of the non-empty components of a path,
78/// delimited by `/`. Note that `.` and `..` are treated as
79/// components.
80#[cfg(not(windows))]
81pub fn components<'a>(path: &'a str) -> Components<'a> {
82    Components(path.split(&['/'][..]))
83}
84
85#[cfg(windows)]
86pub fn components<'a>(path: &'a str) -> Components<'a> {
87    Components(path.split(&['/', '\\'][..]))
88}
89
90#[derive(Clone)]
91pub struct Components<'a>(std::str::Split<'a, &'static [char]>);
92
93impl<'a> std::fmt::Debug for Components<'a> {
94    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
95        write!(fmt, "Components {{ .. }}")
96    }
97}
98
99impl<'a> Iterator for Components<'a> {
100    type Item = &'a str;
101    fn next(&mut self) -> Option<Self::Item> {
102        loop {
103            if let Some(n) = self.0.next() {
104                if !n.is_empty() {
105                    return Some(n);
106                }
107            } else {
108                return None;
109            }
110        }
111    }
112}
113
114/// Push a path component on an existing path. Only works if `extra`
115/// is a relative path.
116/// ```ignore
117/// use pijul_core::path::push;
118/// let mut s = "a".to_string();
119/// push(&mut s, "b");
120/// assert_eq!(s, "a/b");
121/// push(&mut s, "c");
122/// assert_eq!(s, "a/b/c");
123/// ```
124pub fn push(path: &mut String, extra: &str) {
125    assert!(!extra.starts_with('/')); // Make sure the extra path is relative.
126    if !path.ends_with('/') && !path.is_empty() {
127        path.push('/');
128    }
129    path.push_str(extra)
130}
131
132/// Pop the last component off an existing path.
133/// ```ignore
134/// use pijul_core::path::pop;
135/// let mut s = "a/b/c".to_string();
136/// pop(&mut s);
137/// assert_eq!(s, "a/b");
138/// pop(&mut s);
139/// assert_eq!(s, "a");
140/// pop(&mut s);
141/// assert_eq!(s, "");
142/// ```
143pub fn pop(path: &mut String) {
144    if let Some(i) = path.rfind('/') {
145        path.truncate(i)
146    } else {
147        path.clear()
148    }
149}