1use std::path::{Path, PathBuf};
11
12pub fn canonicalize(path: &Path) -> std::io::Result<PathBuf> {
14 let canonical = path.canonicalize()?;
15 if !cfg!(windows) {
16 return Ok(canonical);
17 }
18 Ok(match canonical.to_str().and_then(simplified) {
19 Some(plain) => PathBuf::from(plain),
20 None => canonical,
21 })
22}
23
24fn simplified(path: &str) -> Option<String> {
30 let (plain, names) = if let Some(rest) = path.strip_prefix(r"\\?\UNC\") {
31 (format!(r"\\{rest}"), rest)
32 } else {
33 let rest = path.strip_prefix(r"\\?\")?;
34 let bytes = rest.as_bytes();
35 let drive = bytes.len() >= 3 && bytes[0].is_ascii_alphabetic() && &bytes[1..3] == b":\\";
36 if !drive {
37 return None;
38 }
39 (rest.to_string(), &rest[3..])
40 };
41 const MAX_PATH: usize = 260;
42 if plain.len() >= MAX_PATH {
43 return None;
44 }
45 names
46 .split('\\')
47 .filter(|name| !name.is_empty())
48 .all(plain_name_means_the_same)
49 .then_some(plain)
50}
51
52fn plain_name_means_the_same(name: &str) -> bool {
53 const DEVICES: [&str; 22] = [
54 "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8",
55 "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
56 ];
57 let stem = name.split('.').next().unwrap_or(name).trim_end();
58 !name.ends_with(['.', ' '])
59 && !name.contains('/')
60 && !DEVICES.iter().any(|d| d.eq_ignore_ascii_case(stem))
61}
62
63#[cfg(test)]
64mod tests {
65 use super::simplified;
66
67 #[test]
68 fn a_drive_path_loses_its_prefix() {
69 assert_eq!(
70 simplified(r"\\?\C:\Users\me\data\a.csv").as_deref(),
71 Some(r"C:\Users\me\data\a.csv")
72 );
73 assert_eq!(simplified(r"\\?\D:\").as_deref(), Some(r"D:\"));
74 }
75
76 #[test]
77 fn a_share_becomes_a_unc_path() {
78 assert_eq!(
79 simplified(r"\\?\UNC\nas\data\sales.parquet").as_deref(),
80 Some(r"\\nas\data\sales.parquet")
81 );
82 }
83
84 #[test]
85 fn a_path_only_the_verbatim_form_reaches_keeps_it() {
86 for kept in [
87 r"\\?\C:\data\trailing.",
88 r"\\?\C:\data\trailing ",
89 r"\\?\C:\data\CON",
90 r"\\?\C:\data\nul.txt",
91 r"\\?\C:\data\a/b",
92 r"\\?\Volume{0b1c}\data",
93 r"C:\already\plain",
94 ] {
95 assert_eq!(simplified(kept), None, "{kept}");
96 }
97 let long = format!(r"\\?\C:\{}", "a".repeat(300));
98 assert_eq!(simplified(&long), None);
99 }
100
101 #[test]
102 fn a_name_that_only_starts_like_a_device_is_plain() {
103 assert_eq!(
104 simplified(r"\\?\C:\data\console.csv").as_deref(),
105 Some(r"C:\data\console.csv")
106 );
107 }
108}