use std::path::{Path, PathBuf};
const RESERVED_EXACT: &[&str] = &["CON", "PRN", "AUX", "NUL"];
const RESERVED_PREFIX: &[&str] = &[
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3",
"LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
];
pub fn is_reserved_name(basename: &str) -> bool {
let stem = basename.split('.').next().unwrap_or(basename);
let upper = stem.to_ascii_uppercase();
RESERVED_EXACT.iter().any(|r| upper == *r) || RESERVED_PREFIX.iter().any(|r| upper == *r)
}
pub fn sanitize_reserved_name(name: &str, strict: bool) -> Result<String, String> {
if !is_reserved_name(name) {
return Ok(name.to_string());
}
if strict {
return Err(format!("reserved filename: {}", name));
}
match name.find('.') {
Some(dot) => {
let (stem, rest) = name.split_at(dot);
Ok(format!("{}_{}", stem, rest))
}
None => Ok(format!("{}_", name)),
}
}
pub fn sanitize_relative_path(rel: &str, strict: bool) -> Result<String, String> {
if rel.is_empty() {
return Ok(String::new());
}
let (body, trailing_slash) = if let Some(stripped) = rel.strip_suffix('/') {
(stripped, true)
} else {
(rel, false)
};
let mut parts: Vec<String> = Vec::new();
for part in body.split('/') {
parts.push(sanitize_reserved_name(part, strict)?);
}
let mut out = parts.join("/");
if trailing_slash {
out.push('/');
}
Ok(out)
}
#[cfg(windows)]
pub fn long_path_prefix(path: &Path) -> PathBuf {
let s = path.to_string_lossy();
if s.len() > 255 && !s.starts_with(r"\\?\") {
PathBuf::from(format!(r"\\?\{}", s))
} else {
path.to_path_buf()
}
}
#[cfg(not(windows))]
pub fn long_path_prefix(path: &Path) -> PathBuf {
path.to_path_buf()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_reserved_name() {
assert!(is_reserved_name("CON"));
assert!(is_reserved_name("con"));
assert!(is_reserved_name("Con"));
assert!(is_reserved_name("CON.txt"));
assert!(is_reserved_name("NUL"));
assert!(is_reserved_name("NUL.EXE"));
assert!(!is_reserved_name("CONFIG"));
assert!(!is_reserved_name("CONFIG.txt"));
assert!(is_reserved_name("COM1"));
assert!(!is_reserved_name("COM10"));
assert!(is_reserved_name("LPT9"));
assert!(!is_reserved_name("LPT10"));
}
#[test]
fn test_sanitize_reserved_name_non_strict() {
assert_eq!(
sanitize_reserved_name("safe.txt", false).expect("safe"),
"safe.txt"
);
assert_eq!(sanitize_reserved_name("CON", false).expect("CON"), "CON_");
assert_eq!(
sanitize_reserved_name("CON.txt", false).expect("CON.txt"),
"CON_.txt"
);
}
#[test]
fn test_sanitize_reserved_name_strict() {
assert!(sanitize_reserved_name("safe.txt", true).is_ok());
assert!(sanitize_reserved_name("CON", true).is_err());
assert!(sanitize_reserved_name("CON.txt", true).is_err());
}
#[test]
fn test_sanitize_relative_path_preserves_dirs() {
assert_eq!(
sanitize_relative_path("dir1/CON.txt", false).expect("path"),
"dir1/CON_.txt"
);
assert_eq!(
sanitize_relative_path("dir/sub/NUL", false).expect("path"),
"dir/sub/NUL_"
);
assert_eq!(
sanitize_relative_path("safe/path/file.txt", false).expect("path"),
"safe/path/file.txt"
);
assert_eq!(
sanitize_relative_path("some/dir/", false).expect("path"),
"some/dir/"
);
}
#[cfg(not(windows))]
#[test]
fn test_long_path_prefix_noop_on_non_windows() {
let tmp = std::env::temp_dir();
let p = tmp.join("foo");
let expected = tmp.join("foo");
assert_eq!(long_path_prefix(&p), expected);
}
}