1use crate::ext::anyhow::{anyhow, Context, Result};
2use camino::{Utf8Path, Utf8PathBuf};
3
4pub trait PathExt {
5 fn relative_to(&self, to: impl AsRef<Utf8Path>) -> Option<Utf8PathBuf>;
7
8 fn rebase(&self, src_root: &Utf8Path, dest_root: &Utf8Path) -> Result<Utf8PathBuf>;
10
11 fn unbase(&self, base: &Utf8Path) -> Result<Utf8PathBuf>;
13}
14
15pub trait PathBufExt: PathExt {
16 fn without_last(self) -> Self;
18
19 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 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 if added.starts_with(&path) {
164 *added = path;
165 return vec;
166 }
167 if path.starts_with(added) {
169 return vec;
170 }
171 }
172 vec.push(path);
173 vec
174 })
175}
176
177pub 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
210pub 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}