1use std::borrow::Cow;
2use std::path::{Path, PathBuf};
3
4use cu::Context as _;
5
6pub trait PathExtension {
46 fn file_name_str(&self) -> cu::Result<&str>;
48
49 fn ensure_exists(&self) -> cu::Result<()>;
51
52 fn simplified(&self) -> &Path;
56
57 fn normalize(&self) -> cu::Result<PathBuf>;
70
71 fn normalize_exists(&self) -> cu::Result<PathBuf> {
73 let x = self.normalize()?;
74 x.ensure_exists()?;
75 Ok(x)
76 }
77
78 fn normalize_executable(&self) -> cu::Result<PathBuf>;
84
85 #[inline(always)]
93 fn parent_abs(&self) -> cu::Result<PathBuf> {
94 self.parent_abs_times(1)
95 }
96
97 fn parent_abs_times(&self, x: usize) -> cu::Result<PathBuf>;
99
100 fn try_to_rel(&self) -> Cow<'_, Path> {
104 self.try_to_rel_from(".")
105 }
106
107 fn try_to_rel_from(&self, path: impl AsRef<Path>) -> Cow<'_, Path>;
109
110 #[cfg(feature = "process")]
114 fn command(&self) -> cu::CommandBuilder;
115}
116
117impl PathExtension for Path {
118 fn file_name_str(&self) -> cu::Result<&str> {
119 let file_name = self
120 .file_name()
121 .with_context(|| format!("cannot get file name for path: '{}'", self.display()))?;
122 let Some(file_name) = file_name.to_str() else {
126 crate::bail!("file name is not utf-8: '{}'", self.display());
127 };
128 Ok(file_name)
129 }
130
131 fn simplified(&self) -> &Path {
132 if self.as_os_str().as_encoded_bytes().starts_with(b"\\\\") {
133 dunce::simplified(self)
134 } else {
135 self
136 }
137 }
138
139 fn ensure_exists(&self) -> cu::Result<()> {
140 if !self.exists() {
141 crate::bail!("path '{}' does not exist.", self.display());
142 }
143 Ok(())
144 }
145
146 fn normalize(&self) -> cu::Result<PathBuf> {
147 if let Ok(x) = dunce::canonicalize(self) {
148 return Ok(x);
149 };
150 if self.is_absolute() {
151 return fallback_normalize_absolute(self);
152 }
153
154 let Ok(mut base) = dunce::canonicalize(".") else {
155 crate::bail!(
156 "failed to normalize current directory when normalizing relative path: '{}'",
157 self.display()
158 );
159 };
160
161 base.push(self);
162 fallback_normalize_absolute(&base)
163 }
164
165 fn normalize_executable(&self) -> crate::Result<PathBuf> {
166 let absolute_self = if self.is_absolute() {
169 fallback_normalize_absolute(self)?
170 } else {
171 let Ok(mut base) = dunce::canonicalize(".") else {
172 crate::bail!(
173 "failed to normalize current directory when normalizing relative path: '{}'",
174 self.display()
175 );
176 };
177
178 base.push(self);
179 fallback_normalize_absolute(&base)?
180 };
181 if !absolute_self.exists() {
182 crate::bail!(
183 "failed to normalize executable path '{}': does not exist",
184 absolute_self.display()
185 );
186 }
187 if absolute_self.is_dir() {
188 crate::bail!(
189 "failed to normalize executable path '{}': is a directory",
190 absolute_self.display()
191 )
192 }
193 Ok(absolute_self)
194 }
195
196 fn parent_abs_times(&self, x: usize) -> crate::Result<PathBuf> {
197 let mut out = self.normalize()?;
198
199 for _ in 0..x {
201 if !out.pop() {
202 crate::bail!("trying to get parent of root");
203 }
204 }
205 Ok(out)
206 }
207
208 #[inline(always)]
209 fn try_to_rel_from(&self, path: impl AsRef<Path>) -> Cow<'_, Path> {
210 try_to_rel_from(self, path.as_ref())
211 }
212
213 #[cfg(feature = "process")]
214 fn command(&self) -> crate::CommandBuilder {
215 crate::CommandBuilder::new(self)
216 }
217}
218
219fn fallback_normalize_absolute(path: &Path) -> crate::Result<PathBuf> {
221 let mut prefix = None;
222 let mut components = vec![];
223 for c in path.components() {
224 match c {
225 std::path::Component::Prefix(_prefix) => {
226 prefix = Some(_prefix);
227 }
228 std::path::Component::RootDir => {
229 components.clear();
230 }
231 std::path::Component::CurDir => {}
232 std::path::Component::ParentDir => {
233 if components.pop().is_none() {
234 crate::bail!(
235 "trying to get parent of root when normalizing: {}",
236 path.display()
237 );
238 }
239 }
240 std::path::Component::Normal(os_str) => components.push(os_str),
241 }
242 }
243 let mut out = match prefix {
244 None => PathBuf::from("/"),
245 Some(prefix) => {
246 let mut out = prefix.as_os_str().to_ascii_uppercase();
247 out.push("\\"); out.into()
249 }
250 };
251 out.extend(components);
252 if out.as_os_str().as_encoded_bytes().starts_with(b"\\\\") {
254 Ok(dunce::simplified(&out).to_path_buf())
255 } else {
256 Ok(out)
257 }
258}
259
260fn try_to_rel_from<'a>(self_: &'a Path, path: &Path) -> Cow<'a, Path> {
261 let res = match (self_.is_absolute(), path.is_absolute()) {
262 (true, true) => pathdiff::diff_paths(self_, path),
263 (true, false) => {
264 let Ok(base) = path.normalize() else {
265 return Cow::Borrowed(self_);
266 };
267 pathdiff::diff_paths(self_, base.as_path())
268 }
269 (false, true) => {
270 let Ok(self_) = self_.normalize() else {
271 return Cow::Borrowed(self_);
272 };
273 pathdiff::diff_paths(self_.as_path(), path)
274 }
275 (false, false) => {
276 let Ok(self_abs) = self_.normalize() else {
277 return Cow::Borrowed(self_);
278 };
279 let Ok(base) = path.normalize() else {
280 return Cow::Borrowed(self_);
281 };
282 pathdiff::diff_paths(self_abs.as_path(), base.as_path())
283 }
284 };
285 match res {
286 None => Cow::Borrowed(self_),
287 Some(x) => Cow::Owned(x),
288 }
289}
290
291impl PathExtension for PathBuf {
292 fn file_name_str(&self) -> crate::Result<&str> {
293 self.as_path().file_name_str()
294 }
295 fn simplified(&self) -> &Path {
296 self.as_path().simplified()
297 }
298 fn normalize(&self) -> crate::Result<PathBuf> {
299 self.as_path().normalize()
300 }
301 fn normalize_executable(&self) -> crate::Result<PathBuf> {
302 self.as_path().normalize_executable()
303 }
304 fn ensure_exists(&self) -> crate::Result<()> {
305 self.as_path().ensure_exists()
306 }
307 fn parent_abs_times(&self, x: usize) -> crate::Result<PathBuf> {
308 self.as_path().parent_abs_times(x)
309 }
310 fn try_to_rel_from(&self, path: impl AsRef<Path>) -> Cow<'_, Path> {
311 self.as_path().try_to_rel_from(path)
312 }
313 #[cfg(feature = "process")]
314 fn command(&self) -> crate::CommandBuilder {
315 self.as_path().command()
316 }
317}