use std::path::{Path, PathBuf};
#[cfg(any(windows, test))]
const PATH_LIMIT: usize = 260;
pub fn canonical(path: &Path) -> std::io::Result<PathBuf> {
let resolved = path.canonicalize()?;
#[cfg(windows)]
{
let simplified = resolved.to_str().and_then(simplify).map(PathBuf::from);
Ok(simplified.unwrap_or(resolved))
}
#[cfg(not(windows))]
{
Ok(resolved)
}
}
#[cfg(any(windows, test))]
fn simplify(path: &str) -> Option<&str> {
let simplified = path.strip_prefix(r"\\?\")?;
if simplified.len() >= PATH_LIMIT {
return None;
}
let (drive, rest) = simplified.split_at_checked(3)?;
let mut spelling = drive.chars();
if !spelling
.next()
.is_some_and(|letter| letter.is_ascii_alphabetic())
|| spelling.next() != Some(':')
|| spelling.next() != Some('\\')
{
return None;
}
if rest.is_empty() {
return Some(simplified);
}
rest.split('\\')
.all(ordinarily_reachable)
.then_some(simplified)
}
#[cfg(any(windows, test))]
fn ordinarily_reachable(component: &str) -> bool {
const RESERVED: [&str; 4] = ["CON", "PRN", "AUX", "NUL"];
const NUMBERED: [&str; 2] = ["COM", "LPT"];
if matches!(component, "" | "." | "..") || component.ends_with('.') || component.ends_with(' ')
{
return false;
}
let stem = component.split('.').next().unwrap_or(component).trim_end();
if RESERVED
.iter()
.any(|reserved| stem.eq_ignore_ascii_case(reserved))
{
return false;
}
!NUMBERED.iter().any(|device| {
stem.len() == device.len() + 1
&& stem
.get(..device.len())
.is_some_and(|prefix| prefix.eq_ignore_ascii_case(device))
&& stem
.as_bytes()
.last()
.is_some_and(|digit| digit.is_ascii_digit() && *digit != b'0')
})
}
#[cfg(test)]
mod tests;