1use std::borrow::Cow;
18
19pub fn strip_windows_verbatim_prefix(text: &str) -> Cow<'_, str> {
24 if let Some(rest) = text.strip_prefix(r"\\?\UNC\") {
25 Cow::Owned(format!(r"\\{rest}"))
26 } else if let Some(rest) = text.strip_prefix(r"\\?\") {
27 Cow::Borrowed(rest)
28 } else {
29 Cow::Borrowed(text)
30 }
31}
32
33#[cfg(windows)]
45pub(crate) fn wide_maybe_verbatim(path: &std::path::Path) -> Vec<u16> {
46 use std::os::windows::ffi::OsStrExt;
47 use std::path::{Component, Prefix};
48
49 fn nul_terminated(mut wide: Vec<u16>) -> Vec<u16> {
50 wide.push(0);
51 wide
52 }
53
54 let raw: Vec<u16> = path.as_os_str().encode_wide().collect();
55 const BACKSLASH: u16 = b'\\' as u16;
56 const SLASH: u16 = b'/' as u16;
57 const QUESTION: u16 = b'?' as u16;
58 const DOT: u16 = b'.' as u16;
59 if raw.starts_with(&[BACKSLASH, BACKSLASH, QUESTION, BACKSLASH])
61 || raw.starts_with(&[BACKSLASH, BACKSLASH, DOT, BACKSLASH])
62 {
63 return nul_terminated(raw);
64 }
65 if raw.contains(&SLASH) {
68 return nul_terminated(raw);
69 }
70 let mut components = path.components();
71 let is_supported_prefix = matches!(
72 components.next(),
73 Some(Component::Prefix(prefix))
74 if matches!(prefix.kind(), Prefix::Disk(_) | Prefix::UNC(_, _))
75 );
76 if !is_supported_prefix || !matches!(components.next(), Some(Component::RootDir)) {
77 return nul_terminated(raw);
78 }
79 if components.any(|component| !matches!(component, Component::Normal(_))) {
81 return nul_terminated(raw);
82 }
83 let prefix_kind = path
85 .components()
86 .next()
87 .and_then(|component| match component {
88 Component::Prefix(prefix) => Some(prefix.kind()),
89 _ => None,
90 });
91 let prefixed = match prefix_kind {
92 Some(Prefix::Disk(_)) => {
94 let mut out: Vec<u16> = r"\\?\".encode_utf16().collect();
95 out.extend_from_slice(&raw);
96 out
97 }
98 Some(Prefix::UNC(_, _)) => {
100 let mut out: Vec<u16> = r"\\?\UNC".encode_utf16().collect();
101 out.extend_from_slice(&raw[1..]);
102 out
103 }
104 _ => raw,
105 };
106 nul_terminated(prefixed)
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn strips_drive_and_unc_verbatim_prefixes() {
115 assert_eq!(
116 strip_windows_verbatim_prefix(r"\\?\C:\Users\runner\Temp\harn-abc"),
117 r"C:\Users\runner\Temp\harn-abc"
118 );
119 assert_eq!(
120 strip_windows_verbatim_prefix(r"\\?\UNC\server\share\dir"),
121 r"\\server\share\dir"
122 );
123 }
124
125 #[test]
126 fn passes_through_non_verbatim_paths_unchanged() {
127 assert_eq!(
130 strip_windows_verbatim_prefix(r"C:\Temp\harn-abc"),
131 r"C:\Temp\harn-abc"
132 );
133 assert_eq!(
134 strip_windows_verbatim_prefix("/tmp/harn-abc/child"),
135 "/tmp/harn-abc/child"
136 );
137 assert_eq!(
138 strip_windows_verbatim_prefix(r"\\.\PhysicalDrive0"),
139 r"\\.\PhysicalDrive0"
140 );
141 assert_eq!(
142 strip_windows_verbatim_prefix(r"relative\dir"),
143 r"relative\dir"
144 );
145 assert_eq!(strip_windows_verbatim_prefix(r"C:\a\..\b"), r"C:\a\..\b");
146 assert_eq!(
147 strip_windows_verbatim_prefix("C:/forward/slash"),
148 "C:/forward/slash"
149 );
150 }
151
152 #[test]
153 fn strips_prefix_from_paths_longer_than_max_path() {
154 let deep = format!(r"\\?\C:\{}", "segment\\".repeat(40));
155 let stripped = strip_windows_verbatim_prefix(&deep);
156 assert!(stripped.len() > 260, "test path must exceed MAX_PATH");
157 #[expect(clippy::string_slice, reason = "test input is ASCII")]
158 let expected = &deep[r"\\?\".len()..];
159 assert_eq!(stripped, expected);
160 assert!(!stripped.starts_with(r"\\?\"));
161 }
162}