1use std::path::{Path, PathBuf};
8
9pub fn normalize_windows_path(path: &Path) -> PathBuf {
15 let raw = path.to_string_lossy().replace('/', "\\");
16 let mut normalized = non_verbatim_path_text(&raw).unwrap_or(raw);
17 if normalized.as_bytes().get(1) == Some(&b':') {
18 let drive = normalized.as_bytes()[0];
19 if drive.is_ascii_lowercase() {
20 normalized.replace_range(0..1, &(drive as char).to_ascii_uppercase().to_string());
21 }
22 }
23 PathBuf::from(normalized)
24}
25
26pub fn non_verbatim_path_text(path: &str) -> Option<String> {
29 for prefix in [r"\\?\UNC\", r"\\??\UNC\", r"\??\UNC\"] {
30 if let Some(tail) = strip_ascii_prefix(path, prefix) {
31 if is_safe_unc_tail(tail) {
32 return Some(format!(r"\\{tail}"));
33 }
34 return None;
35 }
36 }
37
38 for prefix in [r"\\?\", r"\\??\", r"\??\"] {
39 if let Some(tail) = strip_ascii_prefix(path, prefix) {
40 let bytes = tail.as_bytes();
41 if bytes.len() >= 3
42 && bytes[0].is_ascii_alphabetic()
43 && bytes[1] == b':'
44 && matches!(bytes[2], b'\\' | b'/')
45 && !has_dot_component(tail)
46 {
47 return Some(tail.to_string());
48 }
49 return None;
50 }
51 }
52
53 None
54}
55
56fn is_safe_unc_tail(tail: &str) -> bool {
57 let mut components = tail.split(['\\', '/']);
58 components.next().is_some_and(|server| !server.is_empty())
59 && components.next().is_some_and(|share| !share.is_empty())
60 && !has_dot_component(tail)
61}
62
63fn has_dot_component(path: &str) -> bool {
64 path.split(['\\', '/'])
65 .any(|component| matches!(component, "." | ".."))
66}
67
68fn strip_ascii_prefix<'a>(value: &'a str, prefix: &str) -> Option<&'a str> {
69 let head = value.get(..prefix.len())?;
70 if head.eq_ignore_ascii_case(prefix) {
71 value.get(prefix.len()..)
72 } else {
73 None
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::{non_verbatim_path_text, normalize_windows_path};
80 use std::path::{Path, PathBuf};
81
82 #[test]
83 fn converts_only_valid_dos_and_unc_verbatim_paths() {
84 assert_eq!(
85 non_verbatim_path_text(r"\\?\C:\cache\server.cmd"),
86 Some(r"C:\cache\server.cmd".to_string())
87 );
88 assert_eq!(
89 non_verbatim_path_text(r"\\?\unc\host\share\server.cmd"),
90 Some(r"\\host\share\server.cmd".to_string())
91 );
92 assert_eq!(
93 normalize_windows_path(Path::new(r"\\??\d:\repo")),
94 PathBuf::from(r"D:\repo")
95 );
96 }
97
98 #[test]
99 fn preserves_unsupported_or_malformed_verbatim_namespaces() {
100 for path in [
101 r"\\?\Volume{1234}\server.cmd",
102 r"\\?\UNC\host",
103 r"\\?\UNC\\host\share",
104 r"\\??\UNC\\host\share",
105 r"\\?\UNC\host\share\..\file",
106 r"\\?\C:\repo\..\other",
107 r"\\?\C:relative",
108 r"\\?\relative",
109 r"C:\ordinary\path",
110 ] {
111 assert_eq!(non_verbatim_path_text(path), None, "{path}");
112 }
113 }
114}