Skip to main content

pistonite_cu/str/
path.rs

1use std::borrow::Cow;
2use std::path::{Path, PathBuf};
3
4use cu::Context as _;
5
6/// # File System Paths and Strings
7/// Rust works with [`String`](std::string)s, which are UTF-8 encoded bytes.
8/// However, not all operating systems work with UTF-8. That's why Rust has
9/// [`OsString`](std::ffi::OsString), which has platform-specific implementations.
10/// And `PathBuf`s are wrappers for `OsString`.
11///
12/// However, often when writing platform-independent code, we want to stay
13/// in the UTF-8 realm, but conversion can be painful because you must handle
14/// the error when the `OsString` is not valid UTF-8.
15///
16/// ```rust
17/// # use pistonite_cu as cu;
18/// use std::ffi::{OsString, OsStr};
19///
20/// use cu::pre::*;
21///
22/// fn take_os_string(s: &OsStr) -> cu::Result<()> {
23///     match s.to_str() {
24///         Some(s) => {
25///             cu::info!("valid utf-8: {s}");
26///             Ok(())
27///         }
28///         None => {
29///             cu::bail!("not valid utf-8!");
30///         }
31///     }
32/// }
33/// ```
34///
35/// `cu` provides extension traits that integrates with `cu::Result`,
36/// so you can have the error handling by simply propagate with `?`.
37///
38/// There are 4 traits, all will be included into scope with `use cu::pre::*;`.
39/// The path extensions also have utilities for working with file system specifically,
40/// (such as normalizing it), which is why they require the `fs` feature.
41///
42/// - [`OsStrExtension`](trait@crate::str::OsStrExtension)
43/// - [`OsStrExtensionOwned`](trait@crate::str::OsStrExtensionOwned)
44/// - `PathExtension` - requires `fs` feature
45pub trait PathExtension {
46    /// Get file name. Error if the file name is not UTF-8 or other error occurs
47    fn file_name_str(&self) -> cu::Result<&str>;
48
49    /// Check that the path exists, or fail with an error
50    fn ensure_exists(&self) -> cu::Result<()>;
51
52    /// Return the simplified path if the path has a Windows UNC prefix
53    ///
54    /// The behavior is the same cross-platform.
55    fn simplified(&self) -> &Path;
56
57    /// Get absolute path for a path.
58    ///
59    /// This is different from `canonicalize()` since that fails for paths that don't
60    /// exist. Here, we do a fallback that normalizes the path manually if `canonicalize()`
61    /// fails:
62    /// - If the path is absolute, then empty, `.`, and `..` path segments are normalized and removed,
63    ///   and error if it tries to get the parent of root at some point.
64    /// - If the path is relative, then the path is normalized the same way as above,
65    ///   and then appended to the absolute path of the current working directory.
66    ///
67    /// On Windows only, it returns the most compatible form using the `dunce` crate instead of
68    /// UNC, and the drive letter is also normalized to upper case
69    fn normalize(&self) -> cu::Result<PathBuf>;
70
71    /// Like `normalize`, but with the additional guarantee that the path exists
72    fn normalize_exists(&self) -> cu::Result<PathBuf> {
73        let x = self.normalize()?;
74        x.ensure_exists()?;
75        Ok(x)
76    }
77
78    /// Like `normalize`, but with the additional guarantee that:
79    /// - The file name of the output will be the same as the input. This is because
80    ///   the executable can be a multicall binary that behaves differently
81    ///   depending on the executable name.
82    /// - The path exists and is not a directory
83    fn normalize_executable(&self) -> cu::Result<PathBuf>;
84
85    /// Get the parent path as an absolute path
86    ///
87    /// Path navigation is very complex and that's why we are paying a little performance
88    /// cost and always returning `PathBuf`, and always converting the path to absolute.
89    ///
90    /// For path manipulation (i.e. as a OsStr), instead of navigation, use std `parent()`
91    /// instead
92    #[inline(always)]
93    fn parent_abs(&self) -> cu::Result<PathBuf> {
94        self.parent_abs_times(1)
95    }
96
97    /// Effecitvely chaining `parent_abs` `x` times
98    fn parent_abs_times(&self, x: usize) -> cu::Result<PathBuf>;
99
100    /// Try converting self to a relative path from current working directory.
101    ///
102    /// Return the path itself unmodified if conversion failed
103    fn try_to_rel(&self) -> Cow<'_, Path> {
104        self.try_to_rel_from(".")
105    }
106
107    /// Try converting self to a relative path from a base path
108    fn try_to_rel_from(&self, path: impl AsRef<Path>) -> Cow<'_, Path>;
109
110    /// Start building a child process with the path as the executable
111    ///
112    /// See [Spawn Commands](crate::CommandBuilder)
113    #[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        // to_str is ok on all platforms, because Rust internally
123        // represent OsStrings on Windows as WTF-8
124        // see https://doc.rust-lang.org/src/std/sys_common/wtf8.rs.html
125        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        // canonicalize will resolve symlinks, which will destroy
167        // the file name, so we cannot use that
168        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        // this is correct since out is normalized
200        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
219/// Normalize a path that failed to canonicalize
220fn 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("\\"); // ok since this only occurs on windows
248            out.into()
249        }
250    };
251    out.extend(components);
252    // simplify windows UNC if needed
253    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}