1use std::path::{Component, Path, PathBuf};
4
5pub 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
33pub 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}