Skip to main content

auri/
path.rs

1use std::{
2    ffi::OsStr,
3    path::{Path, PathBuf},
4};
5
6// ------------------------------------------------------------------
7// Trait for conversion to paths; Into<Box<Path>> does not allow &str.
8
9pub trait IntoBoxPath {
10    fn into_box_path(self) -> Box<Path>;
11}
12
13impl IntoBoxPath for &str {
14    fn into_box_path(self) -> Box<Path> {
15        PathBuf::from(self).into()
16    }
17}
18impl IntoBoxPath for String {
19    fn into_box_path(self) -> Box<Path> {
20        PathBuf::from(self).into()
21    }
22}
23impl IntoBoxPath for PathBuf {
24    fn into_box_path(self) -> Box<Path> {
25        self.into()
26    }
27}
28impl IntoBoxPath for &PathBuf {
29    fn into_box_path(self) -> Box<Path> {
30        // self.clone().into() works but might this avoid a copy?:
31        (**self).into()
32    }
33}
34impl IntoBoxPath for &Path {
35    fn into_box_path(self) -> Box<Path> {
36        self.into()
37    }
38}
39impl IntoBoxPath for Box<Path> {
40    fn into_box_path(self) -> Box<Path> {
41        self
42    }
43}
44impl IntoBoxPath for &Box<Path> {
45    fn into_box_path(self) -> Box<Path> {
46        self.clone()
47    }
48}
49// ------------------------------------------------------------------
50
51// A path operation that doesn't actually work on Path, bummer. Only
52// for strings.
53
54pub fn _base_and_suffix<T: AsRef<[u8]> + ?Sized>(s: &T) -> Option<(&[u8], &str)> {
55    let bs: &[u8] = s.as_ref();
56    let len = bs.len();
57    for (i, c) in bs.iter().rev().enumerate() {
58        match c {
59            b'/' => return None,
60            b'.' => {
61                return Some((
62                    &bs[..(len - i - 1)],
63                    std::str::from_utf8(&bs[(len - i)..]).unwrap(),
64                ))
65            }
66            _ => {
67                if !c.is_ascii_alphanumeric() {
68                    return None;
69                }
70            }
71        }
72    }
73    None
74}
75
76pub fn base_and_suffix(s: &str) -> Option<(&str, &str)> {
77    let (base, suffix) = _base_and_suffix(s)?;
78    Some((std::str::from_utf8(base).unwrap(), suffix))
79}
80
81pub fn base(s: &str) -> Option<&str> {
82    let (base, _suffix) = _base_and_suffix(s)?;
83    Some(std::str::from_utf8(base).unwrap())
84}
85
86// Can't do the same for Path/PathBuf, no way to go via bytes. See
87// path_replace_extension below
88
89/// Find suffix and if present, return as a &str. Only allows \w
90/// characters in suffix.
91pub fn suffix<T: AsRef<[u8]> + ?Sized>(s: &T) -> Option<&str> {
92    _base_and_suffix(s).and_then(|(_, suffix)| Some(suffix))
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    macro_rules! t {
100        ($e:expr, $r:expr) => {
101            assert_eq!(suffix($e), $r);
102        };
103    }
104
105    #[test]
106    fn t_suffix() {
107        t!("foo", None);
108        t!("foo.md", Some("md"));
109        t!("foo.", Some("")); // hmm
110        t!("foo. md", None);
111        t!("foo.md/bar", None);
112        t!("foo.md/", None);
113        t!("foo.mäd", None);
114    }
115    #[test]
116    fn t_base_and_suffix() {
117        assert_eq!(base_and_suffix("foo"), None);
118        assert_eq!(base_and_suffix("bar.md"), Some(("bar", "md")));
119        assert_eq!(base_and_suffix("foo.md/bar"), None);
120        assert_eq!(base_and_suffix("foo.md/bar.md"), Some(("foo.md/bar", "md")));
121    }
122}
123
124// ------------------------------------------------------------------
125
126// XX lib; haven't I done (something like) this already? -- and then
127// this one doesn't work, need the below.
128// fn path_append<P: AsRef<Path> + AsRef<OsStr>>(base: &P, rel: &P) -> PathBuf {
129//     let mut p = PathBuf::from(base);
130//     p.push(rel);
131//     p
132// }
133pub fn path_append<P: AsRef<Path>>(base: &Path, rel: &P) -> PathBuf {
134    let mut p = PathBuf::from(base);
135    p.push(rel);
136    p
137}
138
139/// Careful, this drops any empty segments, regardless whether at the
140/// beginning, end or in the middle. This is useful for search
141/// (iterating into a trie), but can't be used as sole information for
142/// path operations (e.g. adding paths).
143pub fn path_segments<'s>(s: &'s str) -> impl Iterator<Item = &'s str> {
144    s.split('/').filter(|s| !s.is_empty())
145}
146
147/// Return the path with the extension (filename suffix) replaced. If
148/// it doesn't have an extension, or the extension is different from
149/// `orig_extension`, None is returned.
150pub fn path_replace_extension<P: AsRef<Path>>(
151    p: &P,
152    orig_extension: &str,
153    new_extension: &str,
154) -> Option<PathBuf> {
155    let p: &Path = p.as_ref();
156    let ext = p.extension()?;
157    if ext == orig_extension {
158        Some(p.with_extension(new_extension))
159    } else {
160        None
161    }
162}
163
164// Allocation-less(?) way to compare the extension for Path values
165pub fn extension_eq<P: AsRef<Path> + ?Sized, E: AsRef<OsStr> + ?Sized>(path: &P, ext: &E) -> bool {
166    // OsStr::try_from( ) interesting, vs. AsRef which never fails?
167    // XX Does AsRef allocate?
168    let p: &Path = path.as_ref();
169    let ext: &OsStr = ext.as_ref();
170    p.extension() == Some(ext)
171}
172
173#[cfg(test)]
174mod tests_extension_eq {
175    use super::*;
176
177    macro_rules! t {
178        ($e:expr, $r:expr) => {
179            let r: Option<&str> = $r;
180            if let Some(r) = r {
181                assert!(extension_eq($e, r));
182            } else {
183                assert!(!extension_eq($e, "")); // XX oh well.
184            }
185        };
186    }
187
188    // copy-paste from above
189    #[test]
190    fn t_suffix() {
191        t!("foo", None);
192        t!("foo.md", Some("md"));
193        t!("foo.", Some("")); // hmm
194        t!("foo. md", None);
195        t!("foo.md/bar", None);
196        t!("foo.md/", None);
197        t!("foo.mäd", None);
198    }
199}