1use crate::error::PathError;
6use crate::internal::components::{is_drive_relative_path, reserved_base_name};
7use std::ffi::OsStr;
8use std::path::{Component, Path, PathBuf, Prefix};
9
10pub fn is_drive_relative(path: &Path) -> bool {
18 is_drive_relative_path(path)
19}
20
21pub fn is_unc(path: &Path) -> bool {
27 match path.components().next() {
28 Some(Component::Prefix(prefix)) => {
29 matches!(prefix.kind(), Prefix::UNC(_, _) | Prefix::VerbatimUNC(_, _))
30 }
31 _ => false,
32 }
33}
34
35pub fn is_verbatim(path: &Path) -> bool {
41 match path.components().next() {
42 Some(Component::Prefix(prefix)) => matches!(
43 prefix.kind(),
44 Prefix::Verbatim(_) | Prefix::VerbatimUNC(_, _) | Prefix::VerbatimDisk(_)
45 ),
46 _ => false,
47 }
48}
49
50pub fn is_device_namespace(path: &Path) -> bool {
56 match path.components().next() {
57 Some(Component::Prefix(prefix)) => matches!(prefix.kind(), Prefix::DeviceNS(_)),
58 _ => false,
59 }
60}
61
62pub fn is_reserved_windows_name(name: &OsStr) -> bool {
70 reserved_base_name(name).is_some()
71}
72
73pub fn path_contains_reserved_name(path: &Path) -> bool {
79 path.components().any(|c| match c {
80 Component::Normal(name) => is_reserved_windows_name(name),
81 _ => false,
82 })
83}
84
85pub fn translate_wsl_path(input: &str) -> Result<Option<PathBuf>, PathError> {
98 let trimmed = input.trim();
99 if !trimmed.starts_with("/mnt/") {
100 return Ok(None);
101 }
102
103 let rest = &trimmed[5..]; if rest.is_empty() {
105 return Err(PathError::invalid(
106 "WSL path '/mnt/' is incomplete; expected /mnt/<drive>/...",
107 ));
108 }
109
110 let mut parts = rest.splitn(2, '/');
111 let drive = parts.next().unwrap_or("");
112 let tail = parts.next().unwrap_or("");
113
114 if drive.is_empty() {
115 return Err(PathError::invalid("WSL path is missing a drive letter"));
116 }
117
118 let mut chars = drive.chars();
120 let Some(letter) = chars.next() else {
121 return Err(PathError::invalid("WSL path is missing a drive letter"));
122 };
123 if chars.next().is_some() || !letter.is_ascii_alphabetic() {
124 return Ok(None);
126 }
127
128 let mut out = PathBuf::new();
131 let mut drive_root = String::with_capacity(3);
132 drive_root.push(letter.to_ascii_uppercase());
133 drive_root.push(':');
134 drive_root.push('\\');
135 out.push(drive_root);
136
137 if !tail.is_empty() {
138 for segment in tail.split('/') {
139 if segment.is_empty() || segment == "." {
140 continue;
141 }
142 if segment == ".." {
143 if !out.pop() {
144 return Err(PathError::invalid(
145 "WSL path escapes the drive root via '..'",
146 ));
147 }
148 if out.as_os_str().is_empty() {
150 return Err(PathError::invalid(
151 "WSL path escapes the drive root via '..'",
152 ));
153 }
154 continue;
155 }
156 out.push(segment);
157 }
158 }
159
160 Ok(Some(out))
161}
162
163pub fn simplify_for_display(path: &Path) -> PathBuf {
171 dunce::simplified(path).to_path_buf()
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use std::path::Path;
178
179 #[test]
180 fn reserved_names() {
181 assert!(is_reserved_windows_name(OsStr::new("CON")));
182 assert!(is_reserved_windows_name(OsStr::new("nul.txt")));
183 assert!(is_reserved_windows_name(OsStr::new("COM1.log")));
184 assert!(!is_reserved_windows_name(OsStr::new("console")));
185 assert!(!is_reserved_windows_name(OsStr::new("file.txt")));
186 }
187
188 #[test]
189 fn wsl_translation() {
190 let p = translate_wsl_path("/mnt/c/Users/floris").unwrap().unwrap();
191 assert_eq!(p, PathBuf::from(r"C:\Users\floris"));
192
193 let p = translate_wsl_path("/mnt/d/repo").unwrap().unwrap();
194 assert_eq!(p, PathBuf::from(r"D:\repo"));
195
196 assert!(translate_wsl_path("/mnt/data/foo").unwrap().is_none());
197 assert!(translate_wsl_path("/mnt/cc/foo").unwrap().is_none());
198 assert!(translate_wsl_path("/mnt/1/foo").unwrap().is_none());
199 assert!(translate_wsl_path("/mnt/~/foo").unwrap().is_none());
200 assert!(translate_wsl_path("/home/user").unwrap().is_none());
201 assert!(translate_wsl_path("/mnt/").is_err());
202 }
203
204 #[cfg(windows)]
205 #[test]
206 fn windows_prefix_detection() {
207 assert!(is_drive_relative(Path::new(r"C:foo")));
208 assert!(is_drive_relative(Path::new(r"C:")));
209 assert!(!is_drive_relative(Path::new(r"C:\foo")));
210 assert!(!is_drive_relative(Path::new(r"C:/foo")));
211 assert!(is_unc(Path::new(r"\\server\share")));
212 assert!(is_verbatim(Path::new(r"\\?\C:\repo")));
213 assert!(is_device_namespace(Path::new(r"\\.\PIPE\name")));
214 }
215}