use std::path::{Path, PathBuf};
pub fn normalize_windows_path(path: &Path) -> PathBuf {
let raw = path.to_string_lossy().replace('/', "\\");
let mut normalized = non_verbatim_path_text(&raw).unwrap_or(raw);
if normalized.as_bytes().get(1) == Some(&b':') {
let drive = normalized.as_bytes()[0];
if drive.is_ascii_lowercase() {
normalized.replace_range(0..1, &(drive as char).to_ascii_uppercase().to_string());
}
}
PathBuf::from(normalized)
}
pub fn non_verbatim_path_text(path: &str) -> Option<String> {
for prefix in [r"\\?\UNC\", r"\\??\UNC\", r"\??\UNC\"] {
if let Some(tail) = strip_ascii_prefix(path, prefix) {
if is_safe_unc_tail(tail) {
return Some(format!(r"\\{tail}"));
}
return None;
}
}
for prefix in [r"\\?\", r"\\??\", r"\??\"] {
if let Some(tail) = strip_ascii_prefix(path, prefix) {
let bytes = tail.as_bytes();
if bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& matches!(bytes[2], b'\\' | b'/')
&& !has_dot_component(tail)
{
return Some(tail.to_string());
}
return None;
}
}
None
}
fn is_safe_unc_tail(tail: &str) -> bool {
let mut components = tail.split(['\\', '/']);
components.next().is_some_and(|server| !server.is_empty())
&& components.next().is_some_and(|share| !share.is_empty())
&& !has_dot_component(tail)
}
fn has_dot_component(path: &str) -> bool {
path.split(['\\', '/'])
.any(|component| matches!(component, "." | ".."))
}
fn strip_ascii_prefix<'a>(value: &'a str, prefix: &str) -> Option<&'a str> {
let head = value.get(..prefix.len())?;
if head.eq_ignore_ascii_case(prefix) {
value.get(prefix.len()..)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::{non_verbatim_path_text, normalize_windows_path};
use std::path::{Path, PathBuf};
#[test]
fn converts_only_valid_dos_and_unc_verbatim_paths() {
assert_eq!(
non_verbatim_path_text(r"\\?\C:\cache\server.cmd"),
Some(r"C:\cache\server.cmd".to_string())
);
assert_eq!(
non_verbatim_path_text(r"\\?\unc\host\share\server.cmd"),
Some(r"\\host\share\server.cmd".to_string())
);
assert_eq!(
normalize_windows_path(Path::new(r"\\??\d:\repo")),
PathBuf::from(r"D:\repo")
);
}
#[test]
fn preserves_unsupported_or_malformed_verbatim_namespaces() {
for path in [
r"\\?\Volume{1234}\server.cmd",
r"\\?\UNC\host",
r"\\?\UNC\\host\share",
r"\\??\UNC\\host\share",
r"\\?\UNC\host\share\..\file",
r"\\?\C:\repo\..\other",
r"\\?\C:relative",
r"\\?\relative",
r"C:\ordinary\path",
] {
assert_eq!(non_verbatim_path_text(path), None, "{path}");
}
}
}