1use std::cmp::Ordering;
6use std::ffi::OsStr;
7use std::hash::{Hash, Hasher};
8use std::ops::Deref;
9use std::path::{Path, PathBuf};
10
11use serde::{Deserialize, Deserializer, Serialize, Serializer};
12
13#[derive(Debug, Clone)]
18pub struct NormalizedPath {
19 path: PathBuf,
21 case_key: Option<String>,
23}
24
25impl PartialEq for NormalizedPath {
26 fn eq(&self, other: &Self) -> bool {
27 normalize_for_key(&self.path) == normalize_for_key(&other.path)
28 }
29}
30
31impl PartialEq<PathBuf> for NormalizedPath {
32 fn eq(&self, other: &PathBuf) -> bool {
33 self == &Self::new(other)
34 }
35}
36
37impl PartialEq<NormalizedPath> for PathBuf {
38 fn eq(&self, other: &NormalizedPath) -> bool {
39 other == self
40 }
41}
42
43impl PartialEq<Path> for NormalizedPath {
44 fn eq(&self, other: &Path) -> bool {
45 self == &Self::new(other)
46 }
47}
48
49impl PartialEq<&Path> for NormalizedPath {
50 fn eq(&self, other: &&Path) -> bool {
51 self == *other
52 }
53}
54
55impl PartialEq<NormalizedPath> for Path {
56 fn eq(&self, other: &NormalizedPath) -> bool {
57 other == self
58 }
59}
60
61impl PartialEq<&NormalizedPath> for Path {
62 fn eq(&self, other: &&NormalizedPath) -> bool {
63 *other == self
64 }
65}
66
67impl PartialEq<&PathBuf> for NormalizedPath {
68 fn eq(&self, other: &&PathBuf) -> bool {
69 self == *other
70 }
71}
72
73impl PartialEq<&NormalizedPath> for PathBuf {
74 fn eq(&self, other: &&NormalizedPath) -> bool {
75 *other == self
76 }
77}
78
79impl Eq for NormalizedPath {}
80
81impl Hash for NormalizedPath {
82 fn hash<H: Hasher>(&self, state: &mut H) {
83 normalize_for_key(&self.path).hash(state);
84 }
85}
86
87impl PartialOrd for NormalizedPath {
88 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
89 Some(self.cmp(other))
90 }
91}
92
93impl Ord for NormalizedPath {
94 fn cmp(&self, other: &Self) -> Ordering {
95 normalize_for_key(&self.path).cmp(&normalize_for_key(&other.path))
96 }
97}
98
99impl NormalizedPath {
100 pub fn new(path: impl AsRef<Path>) -> Self {
104 let path = normalize(path.as_ref());
105 let case_key = if cfg!(windows) || cfg!(target_os = "macos") {
106 Some(normalize_for_key(&path))
107 } else {
108 None
109 };
110 Self { path, case_key }
111 }
112
113 #[must_use]
115 pub fn as_path(&self) -> &Path {
116 &self.path
117 }
118
119 #[must_use]
121 pub fn case_key(&self) -> Option<&str> {
122 self.case_key.as_deref()
123 }
124
125 #[must_use]
127 pub fn into_path_buf(self) -> PathBuf {
128 self.path
129 }
130
131 #[must_use]
133 pub fn join(&self, path: impl AsRef<Path>) -> Self {
134 Self::new(self.path.join(path))
135 }
136}
137
138impl AsRef<Path> for NormalizedPath {
139 fn as_ref(&self) -> &Path {
140 self.as_path()
141 }
142}
143
144impl AsRef<OsStr> for NormalizedPath {
145 fn as_ref(&self) -> &OsStr {
146 self.as_path().as_os_str()
147 }
148}
149
150impl Deref for NormalizedPath {
151 type Target = Path;
152
153 fn deref(&self) -> &Self::Target {
154 self.as_path()
155 }
156}
157
158impl From<PathBuf> for NormalizedPath {
159 fn from(path: PathBuf) -> Self {
160 Self::new(path)
161 }
162}
163
164impl From<&Path> for NormalizedPath {
165 fn from(path: &Path) -> Self {
166 Self::new(path)
167 }
168}
169
170impl From<String> for NormalizedPath {
171 fn from(path: String) -> Self {
172 Self::new(path)
173 }
174}
175
176impl From<&str> for NormalizedPath {
177 fn from(path: &str) -> Self {
178 Self::new(path)
179 }
180}
181
182impl From<&String> for NormalizedPath {
183 fn from(path: &String) -> Self {
184 Self::new(path)
185 }
186}
187
188impl Serialize for NormalizedPath {
189 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
190 where
191 S: Serializer,
192 {
193 self.path.serialize(serializer)
194 }
195}
196
197impl<'de> Deserialize<'de> for NormalizedPath {
198 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
199 where
200 D: Deserializer<'de>,
201 {
202 PathBuf::deserialize(deserializer).map(Self::new)
203 }
204}
205
206#[must_use]
212pub fn normalize(path: &Path) -> PathBuf {
213 use std::path::Component;
214
215 let mut components = Vec::new();
216 for component in path.components() {
217 match component {
218 Component::CurDir => {}
219 Component::ParentDir => {
220 if let Some(Component::Normal(_)) = components.last() {
221 components.pop();
222 } else {
223 components.push(component);
224 }
225 }
226 _ => components.push(component),
227 }
228 }
229 components.iter().collect()
230}
231
232#[must_use]
238pub fn normalize_for_key(path: &Path) -> String {
239 let normalized = normalize(path);
240
241 #[cfg(windows)]
242 {
243 let mut s = normalized.to_string_lossy().replace('\\', "/");
244 if let Some(stripped) = s.strip_prefix("//?/") {
245 s = stripped.to_string();
246 }
247 s.make_ascii_lowercase();
248 s
249 }
250
251 #[cfg(target_os = "macos")]
252 {
253 normalized.to_string_lossy().to_lowercase()
254 }
255
256 #[cfg(not(any(windows, target_os = "macos")))]
257 {
258 normalized.to_string_lossy().into_owned()
259 }
260}
261
262#[must_use]
268pub fn stable_path_id(path: &Path) -> String {
269 const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
270 const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
271
272 let key = normalize_for_key(path);
273 let mut hash = FNV_OFFSET;
274 for byte in key.as_bytes() {
275 hash ^= u64::from(*byte);
276 hash = hash.wrapping_mul(FNV_PRIME);
277 }
278 format!("{hash:016x}")
279}
280
281#[must_use]
289pub fn normalize_msys_path(path: &str) -> String {
290 #[cfg(windows)]
291 {
292 let bytes = path.as_bytes();
293 if bytes.len() >= 2
295 && bytes[0] == b'/'
296 && bytes[1].is_ascii_alphabetic()
297 && (bytes.len() == 2 || bytes[2] == b'/')
298 {
299 let drive = (bytes[1] as char).to_ascii_uppercase();
300 let rest = if bytes.len() > 2 { &path[2..] } else { "" };
301 return format!("{drive}:{rest}").replace('/', "\\");
302 }
303 path.to_string()
304 }
305 #[cfg(not(windows))]
306 {
307 path.to_string()
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn normalize_removes_dot() {
317 let p = normalize(Path::new("a/./b/c"));
318 assert_eq!(p, PathBuf::from("a/b/c"));
319 }
320
321 #[test]
322 fn normalize_resolves_dotdot() {
323 let p = normalize(Path::new("a/b/../c"));
324 assert_eq!(p, PathBuf::from("a/c"));
325 }
326
327 #[cfg(windows)]
328 #[test]
329 fn normalize_for_key_windows_equivalent_spellings_match() {
330 let a = normalize_for_key(Path::new(r"\\?\C:\Work\src\..\src\main.cpp"));
331 let b = normalize_for_key(Path::new("c:/work/src/main.cpp"));
332 assert_eq!(a, b);
333 }
334
335 #[test]
336 fn msys_path_drive_letter() {
337 let result = normalize_msys_path("/c/Users/foo/bar");
338 #[cfg(windows)]
339 assert_eq!(result, r"C:\Users\foo\bar");
340 #[cfg(not(windows))]
341 assert_eq!(result, "/c/Users/foo/bar");
342 }
343
344 #[test]
345 fn msys_path_uppercase_drive() {
346 let result = normalize_msys_path("/D/project/build");
347 #[cfg(windows)]
348 assert_eq!(result, r"D:\project\build");
349 #[cfg(not(windows))]
350 assert_eq!(result, "/D/project/build");
351 }
352
353 #[test]
354 fn msys_path_bare_drive() {
355 let result = normalize_msys_path("/c");
356 #[cfg(windows)]
357 assert_eq!(result, "C:");
358 #[cfg(not(windows))]
359 assert_eq!(result, "/c");
360 }
361
362 #[test]
363 fn native_windows_path_unchanged() {
364 let result = normalize_msys_path(r"C:\Users\foo\bar");
365 assert_eq!(result, r"C:\Users\foo\bar");
366 }
367
368 #[test]
369 fn relative_path_unchanged() {
370 let result = normalize_msys_path("relative/path");
371 assert_eq!(result, "relative/path");
372 }
373
374 #[test]
375 fn empty_path_unchanged() {
376 let result = normalize_msys_path("");
377 assert_eq!(result, "");
378 }
379
380 #[test]
381 fn unix_absolute_path_not_drive() {
382 let result = normalize_msys_path("/usr/bin/gcc");
384 assert_eq!(result, "/usr/bin/gcc");
385 }
386
387 #[test]
388 fn stable_path_id_is_compact_and_deterministic() {
389 let path = Path::new("a/./b/../cache");
390 assert_eq!(stable_path_id(path), stable_path_id(path));
391 assert_eq!(stable_path_id(path).len(), 16);
392 }
393}