glory_cli/ext/
path.rs

1use crate::ext::anyhow::{anyhow, Context, Result};
2use camino::{Utf8Path, Utf8PathBuf};
3
4pub trait PathExt {
5    /// converts this absolute path to relative if the start matches
6    fn relative_to(&self, to: impl AsRef<Utf8Path>) -> Option<Utf8PathBuf>;
7
8    /// removes the src_root from the path and adds the dest_root
9    fn rebase(&self, src_root: &Utf8Path, dest_root: &Utf8Path) -> Result<Utf8PathBuf>;
10
11    /// removes base from path (making sure they match)
12    fn unbase(&self, base: &Utf8Path) -> Result<Utf8PathBuf>;
13}
14
15pub trait PathBufExt: PathExt {
16    /// drops the last path component
17    fn without_last(self) -> Self;
18
19    /// returns a platform independent string suitable for testing
20    fn test_string(&self) -> String;
21
22    fn starts_with_any(&self, of: &[Utf8PathBuf]) -> bool;
23
24    fn is_ext_any(&self, of: &[&str]) -> bool;
25
26    fn resolve_home_dir(self) -> Result<Utf8PathBuf>;
27
28    /// cleaning the unc (illegible \\?\) start of windows paths. See dunce crate.
29    fn clean_windows_path(&mut self);
30
31    #[cfg(test)]
32    fn ls_ascii(&self, indent: usize) -> Result<String>;
33}
34
35impl PathExt for Utf8Path {
36    fn relative_to(&self, to: impl AsRef<Utf8Path>) -> Option<Utf8PathBuf> {
37        self.to_path_buf().relative_to(to)
38    }
39
40    fn rebase(&self, src_root: &Utf8Path, dest_root: &Utf8Path) -> Result<Utf8PathBuf> {
41        self.to_path_buf().rebase(src_root, dest_root)
42    }
43
44    fn unbase(&self, base: &Utf8Path) -> Result<Utf8PathBuf> {
45        let path = self
46            .strip_prefix(base)
47            .map(|p| p.to_path_buf())
48            .map_err(|_| anyhow!("Could not remove base {base:?} from {self:?}"))?;
49        if path == "" {
50            Ok(Utf8PathBuf::from("."))
51        } else {
52            Ok(path)
53        }
54    }
55}
56
57impl PathBufExt for Utf8PathBuf {
58    fn without_last(mut self) -> Utf8PathBuf {
59        self.pop();
60        self
61    }
62
63    fn test_string(&self) -> String {
64        let s = self.to_string().replace('\\', "/");
65        if s.ends_with(".exe") {
66            s[..s.len() - 4].to_string()
67        } else {
68            s
69        }
70    }
71
72    fn starts_with_any(&self, of: &[Utf8PathBuf]) -> bool {
73        of.iter().any(|p| self.starts_with(p))
74    }
75
76    fn is_ext_any(&self, of: &[&str]) -> bool {
77        let Some(ext) = self.extension() else { return false };
78        of.contains(&ext)
79    }
80
81    fn resolve_home_dir(self) -> Result<Utf8PathBuf> {
82        if self.starts_with("~") {
83            let home = std::env::var("HOME").context("Could not resolve $HOME")?;
84            let home = Utf8PathBuf::from(home);
85            Ok(home.join(self.strip_prefix("~").unwrap()))
86        } else {
87            Ok(self)
88        }
89    }
90
91    fn clean_windows_path(&mut self) {
92        if cfg!(windows) {
93            let cleaned = dunce::simplified(self.as_ref());
94            *self = Utf8PathBuf::from_path_buf(cleaned.to_path_buf()).unwrap();
95        }
96    }
97
98    #[cfg(test)]
99    fn ls_ascii(&self, indent: usize) -> Result<String> {
100        let mut entries = self.read_dir_utf8()?;
101        let mut out = Vec::new();
102
103        out.push(format!("{}{}:", "  ".repeat(indent), self.file_name().unwrap_or_default()));
104
105        let indent = indent + 1;
106        let mut files = Vec::new();
107        let mut dirs = Vec::new();
108
109        while let Some(Ok(entry)) = entries.next() {
110            let path = entry.path().to_path_buf();
111
112            if entry.file_type()?.is_dir() {
113                dirs.push(path);
114            } else {
115                files.push(path);
116            }
117        }
118
119        dirs.sort();
120        files.sort();
121
122        for file in files {
123            out.push(format!("{}{}", "  ".repeat(indent), file.file_name().unwrap_or_default()));
124        }
125
126        for path in dirs {
127            out.push(path.ls_ascii(indent)?);
128        }
129        Ok(out.join("\n"))
130    }
131}
132
133impl PathExt for Utf8PathBuf {
134    fn relative_to(&self, to: impl AsRef<Utf8Path>) -> Option<Utf8PathBuf> {
135        let root = to.as_ref();
136        if self.is_absolute() && self.starts_with(root) {
137            let len = root.components().count();
138            Some(self.components().skip(len).collect())
139        } else {
140            None
141        }
142    }
143    fn rebase(&self, src_root: &Utf8Path, dest_root: &Utf8Path) -> Result<Utf8PathBuf>
144    where
145        Self: Sized,
146    {
147        let unbased = self
148            .unbase(src_root)
149            .dot()
150            .context(format!("Rebase {self} from {src_root} to {dest_root}"))?;
151        Ok(dest_root.join(unbased))
152    }
153
154    fn unbase(&self, base: &Utf8Path) -> Result<Utf8PathBuf> {
155        self.as_path().unbase(base)
156    }
157}
158
159pub fn remove_nested(paths: impl Iterator<Item = Utf8PathBuf>) -> Vec<Utf8PathBuf> {
160    paths.fold(vec![], |mut vec, path| {
161        for added in vec.iter_mut() {
162            // path is a parent folder of added
163            if added.starts_with(&path) {
164                *added = path;
165                return vec;
166            }
167            // path is a sub folder of added
168            if path.starts_with(added) {
169                return vec;
170            }
171        }
172        vec.push(path);
173        vec
174    })
175}
176
177/// Extension Safe &str Append
178///
179/// # Arguments
180///
181/// * `path` - Current path to file
182/// * `suffix` - &str to be appened before extension
183///
184/// # Example
185///
186/// ```
187/// use camino::Utf8PathBuf;
188/// use glory_cli::ext::append_str_to_filename;
189///
190/// let path: Utf8PathBuf = "foo.bar".into();
191/// assert_eq!(append_str_to_filename(&path, "_bazz").unwrap().as_str(), "foo_bazz.bar");
192/// let path: Utf8PathBuf = "a".into();
193/// assert_eq!(append_str_to_filename(&path, "b").unwrap().as_str(), "ab");
194/// ```
195pub fn append_str_to_filename(path: &Utf8PathBuf, suffix: &str) -> Result<Utf8PathBuf> {
196    match path.file_stem() {
197        Some(stem) => {
198            let new_filename: Utf8PathBuf = match path.extension() {
199                Some(extension) => format!("{stem}{suffix}.{extension}").into(),
200                None => format!("{stem}{suffix}").into(),
201            };
202            let mut full_path: Utf8PathBuf = path.parent().unwrap_or("".into()).into();
203            full_path.push(new_filename);
204            Ok(full_path)
205        }
206        None => Err(anyhow!("no file present in provided path {path:?}")),
207    }
208}
209
210/// Returns path to pdb and verifies it exists, returns None when file does not exist
211pub fn determine_pdb_filename(path: &Utf8PathBuf) -> Option<Utf8PathBuf> {
212    match path.file_stem() {
213        Some(stem) => {
214            let new_filename: Utf8PathBuf = format!("{stem}.pdb").into();
215            let mut full_path: Utf8PathBuf = path.parent().unwrap_or("".into()).into();
216            full_path.push(new_filename);
217            if full_path.exists() {
218                Some(full_path)
219            } else {
220                None
221            }
222        }
223        None => None,
224    }
225}