Skip to main content

aube_util/
path.rs

1//! Cross-crate path normalization helpers.
2
3use std::path::{Component, Path, PathBuf};
4
5/// Collapse `.` and resolvable `..` components without touching the
6/// filesystem.
7///
8/// Unlike `canonicalize`, this does not require the path to exist and
9/// does not follow symlinks. Leading `..` components that cannot be
10/// collapsed are preserved.
11pub fn normalize_lexical(path: &Path) -> PathBuf {
12    let mut out = PathBuf::new();
13    for comp in path.components() {
14        match comp {
15            Component::ParentDir => {
16                let prev_is_normal = out
17                    .components()
18                    .next_back()
19                    .is_some_and(|c| matches!(c, Component::Normal(_)));
20                if prev_is_normal {
21                    out.pop();
22                } else {
23                    out.push("..");
24                }
25            }
26            Component::CurDir => {}
27            other => out.push(other.as_os_str()),
28        }
29    }
30    out
31}
32
33/// Strip a Windows `\\?\` verbatim drive prefix from `path` so callers
34/// that interpolate it into a path-concatenating template (`%~dp0\{rel}`
35/// in the bin-shim generator, `starts_with` ownership checks in
36/// `scan_packages` / `remove_package`, `CreateDirectoryW` calls in the
37/// linker) all see the plain `C:\…` form.
38///
39/// `\\?\UNC\server\share\…` identifies a real network share and has no
40/// non-verbatim equivalent — callers that strip it would produce an
41/// unresolvable drive-rooted path, so leave UNC verbatim prefixes
42/// intact.
43///
44/// No-op on non-Windows.
45pub fn strip_verbatim_prefix(path: &Path) -> PathBuf {
46    #[cfg(windows)]
47    {
48        let s = path.to_string_lossy();
49        if let Some(rest) = s.strip_prefix(r"\\?\")
50            && !rest.starts_with("UNC\\")
51        {
52            return PathBuf::from(rest);
53        }
54    }
55    path.to_path_buf()
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn lexical_normalization_collapses_internal_parent_dirs() {
64        assert_eq!(
65            normalize_lexical(Path::new("packages/app/../../vendor")),
66            PathBuf::from("vendor")
67        );
68    }
69
70    #[test]
71    fn lexical_normalization_preserves_leading_parent_dirs() {
72        assert_eq!(
73            normalize_lexical(Path::new("../vendor")),
74            PathBuf::from("../vendor")
75        );
76    }
77
78    #[test]
79    fn lexical_normalization_ignores_current_dirs() {
80        assert_eq!(
81            normalize_lexical(Path::new("./vendor/./pkg")),
82            PathBuf::from("vendor/pkg")
83        );
84    }
85
86    #[cfg(windows)]
87    #[test]
88    fn strips_verbatim_drive_prefix() {
89        let p = Path::new(r"\\?\C:\Users\foo");
90        assert_eq!(strip_verbatim_prefix(p), PathBuf::from(r"C:\Users\foo"));
91    }
92
93    #[cfg(windows)]
94    #[test]
95    fn preserves_verbatim_unc_share() {
96        let p = Path::new(r"\\?\UNC\server\share\foo");
97        assert_eq!(
98            strip_verbatim_prefix(p),
99            PathBuf::from(r"\\?\UNC\server\share\foo")
100        );
101    }
102
103    #[cfg(windows)]
104    #[test]
105    fn leaves_plain_drive_path_unchanged() {
106        let p = Path::new(r"C:\Users\foo");
107        assert_eq!(strip_verbatim_prefix(p), PathBuf::from(r"C:\Users\foo"));
108    }
109
110    #[cfg(not(windows))]
111    #[test]
112    fn is_noop_on_unix() {
113        let p = Path::new("/home/foo");
114        assert_eq!(strip_verbatim_prefix(p), PathBuf::from("/home/foo"));
115    }
116}