use crate::soft_canonicalize;
#[cfg(feature = "anchored")]
use crate::anchored_canonicalize;
#[cfg(windows)]
use std::ffi::OsStr;
#[cfg(windows)]
use std::path::Path;
#[cfg(windows)]
fn windows_char_len(s: &OsStr) -> usize {
use std::os::windows::ffi::OsStrExt;
s.encode_wide().count()
}
#[cfg(windows)]
fn is_valid_filename(file_name: &OsStr) -> bool {
if file_name.len() > 255 && windows_char_len(file_name) > 255 {
return false;
}
let byte_str = if let Some(s) = file_name.to_str() {
s.as_bytes()
} else {
return false;
};
if byte_str.is_empty() {
return false;
}
if byte_str.iter().any(|&c| {
matches!(
c,
0..=31 | b'<' | b'>' | b':' | b'"' | b'/' | b'\\' | b'|' | b'?' | b'*'
)
}) {
return false;
}
if matches!(byte_str.last(), Some(b' ' | b'.')) {
return false;
}
true
}
#[cfg(windows)]
const RESERVED_NAMES: [&str; 22] = [
"AUX", "NUL", "PRN", "CON", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8",
"COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
];
#[cfg(windows)]
fn is_reserved<P: AsRef<OsStr>>(file_name: P) -> bool {
if let Some(name) = Path::new(&file_name)
.file_stem()
.and_then(|s| s.to_str()?.split('.').next())
{
let trimmed = name.trim_end_matches(' ');
return trimmed.len() <= 4
&& RESERVED_NAMES
.into_iter()
.any(|name| trimmed.eq_ignore_ascii_case(name));
}
false
}
#[cfg(windows)]
#[test]
fn test_reserved_names() {
assert!(is_reserved("CON"));
assert!(is_reserved("con"));
assert!(is_reserved("con.con"));
assert!(is_reserved("COM4"));
assert!(is_reserved("COM4.txt"));
assert!(is_reserved("COM4 .txt"));
assert!(is_reserved("con."));
assert!(is_reserved("con ."));
assert!(is_reserved("con "));
assert!(is_reserved("con . "));
assert!(is_reserved("con . .txt"));
assert!(is_reserved("con.....txt"));
assert!(is_reserved("PrN....."));
assert!(is_reserved("nul.tar.gz"));
assert!(!is_reserved(" PrN....."));
assert!(!is_reserved(" CON"));
assert!(!is_reserved("COM0"));
assert!(!is_reserved("COM77"));
assert!(!is_reserved(" CON "));
assert!(!is_reserved(".CON"));
assert!(!is_reserved("@CON"));
assert!(!is_reserved("not.CON"));
assert!(!is_reserved("CON。")); }
#[cfg(windows)]
#[test]
fn test_windows_char_len() {
assert_eq!(1, windows_char_len(OsStr::new("a")));
assert_eq!(1, windows_char_len(OsStr::new("€")));
assert_eq!(1, windows_char_len(OsStr::new("本")));
assert_eq!(2, windows_char_len(OsStr::new("🧐")));
assert_eq!(2, windows_char_len(OsStr::new("®®")));
}
#[cfg(windows)]
#[test]
fn test_filename_validation() {
assert!(!is_valid_filename("..".as_ref()));
assert!(!is_valid_filename(".".as_ref()));
assert!(!is_valid_filename("aaaaaaaaaa:".as_ref()));
assert!(!is_valid_filename("ą:ą".as_ref()));
assert!(!is_valid_filename("".as_ref()));
assert!(!is_valid_filename("a ".as_ref()));
assert!(!is_valid_filename(" a. ".as_ref()));
assert!(!is_valid_filename("a/".as_ref()));
assert!(!is_valid_filename("/a".as_ref()));
assert!(!is_valid_filename("/".as_ref()));
assert!(!is_valid_filename("\\".as_ref()));
assert!(!is_valid_filename("\\a".as_ref()));
assert!(!is_valid_filename("<x>".as_ref()));
assert!(!is_valid_filename("a*".as_ref()));
assert!(!is_valid_filename("?x".as_ref()));
assert!(!is_valid_filename("a\0a".as_ref()));
assert!(!is_valid_filename("\x1f".as_ref()));
assert!(!is_valid_filename("a".repeat(257).as_ref()));
assert!(is_valid_filename("®".repeat(254).as_ref()));
assert!(is_valid_filename("ファイル".as_ref()));
assert!(is_valid_filename("a".as_ref()));
assert!(is_valid_filename("a.aaaaaaaa".as_ref()));
assert!(is_valid_filename("a........a".as_ref()));
assert!(is_valid_filename(" b".as_ref()));
}
#[cfg(windows)]
#[test]
fn test_simplify_unc_paths() {
use tempfile::TempDir;
let tmp = TempDir::new().expect("create tempdir");
let base = tmp.path();
let emoji_dir = base.join("foo").join("😀");
std::fs::create_dir_all(emoji_dir).ok();
let unc_path = format!(r"\\?\{}\foo\😀", base.display());
let result = soft_canonicalize(unc_path).expect("should canonicalize");
let result_str = result.to_str().expect("valid UTF-8");
assert!(
!result_str.starts_with(r"\\?\"),
"Expected simplified path without \\\\?\\, got: {}",
result_str
);
assert!(
result_str.contains('😀'),
"Expected emoji preserved in simplified path: {}",
result_str
);
}
#[cfg(windows)]
#[test]
fn test_simplify_preserve_unc_when_unsafe() {
let server_path = r"\\?\serv\";
let result = soft_canonicalize(server_path).expect("should handle UNC server path");
assert!(
result.to_str().unwrap().starts_with(r"\\?\"),
"Server UNC paths should remain as UNC"
);
let device_path = r"\\.\C:\notdisk";
let result = soft_canonicalize(device_path).expect("should handle device path");
assert!(
result.to_str().unwrap().starts_with(r"\\"),
"Device paths should remain as UNC"
);
let globalroot_path = r"\\?\GLOBALROOT\Device\ImDisk0\path\to\file.txt";
let result = soft_canonicalize(globalroot_path).expect("should handle GLOBALROOT");
assert!(
result.to_str().unwrap().starts_with(r"\\?\"),
"GLOBALROOT paths should remain as UNC"
);
}
#[cfg(windows)]
#[test]
fn test_safe_to_simplify_basic_paths() {
use tempfile::TempDir;
let tmp = TempDir::new().expect("create tempdir");
let foo_bar = tmp.path().join("foo").join("bar");
std::fs::create_dir_all(foo_bar).ok();
let test_cases = vec![
tmp.path().join("foo").join("bar"),
tmp.path().join("😀").join("🎃"),
];
for path in test_cases {
let result = soft_canonicalize(path).expect("should canonicalize");
let result_str = result.to_str().expect("valid UTF-8");
assert!(
!result_str.starts_with(r"\\?\"),
"Expected simplified path without \\\\?\\, got: {}",
result_str
);
}
}
#[cfg(windows)]
#[test]
fn test_unsafe_to_simplify_long_paths() {
use tempfile::TempDir;
let tmp = TempDir::new().expect("create tempdir");
let long = "®".repeat(160);
let short_path = tmp.path().join(&long);
let result = soft_canonicalize(short_path).expect("should canonicalize");
let result_str = result.to_str().expect("valid UTF-8");
assert!(
!result_str.starts_with(r"\\?\"),
"160-char component should be simplifiable"
);
let long_path = tmp.path().join(&long).join(&long);
let result = soft_canonicalize(long_path).expect("should canonicalize");
let result_str = result.to_str().expect("valid UTF-8");
assert!(
result_str.starts_with(r"\\?\"),
"Path > 260 chars should remain UNC, got: {}",
result_str
);
}
#[cfg(windows)]
#[test]
fn test_unsafe_to_simplify_dotdot_paths() {
use tempfile::TempDir;
let tmp = TempDir::new().expect("create tempdir");
let dotdot_path = format!(r"\\?\{}\foo\..\bar", tmp.path().display());
eprintln!("INPUT: {}", dotdot_path);
let result = soft_canonicalize(dotdot_path).expect("should handle UNC dotdot");
let result_str = result.to_str().expect("valid UTF-8");
eprintln!("OUTPUT: {}", result_str);
let expected = tmp.path().join("bar");
let expected_canonical = crate::soft_canonicalize(expected).expect("canonicalize expected");
assert_eq!(
result, expected_canonical,
"Verbatim path with .. should be resolved and then simplified (when safe)"
);
}
#[cfg(windows)]
#[test]
fn test_unsafe_to_simplify_malformed_unc() {
let malformed_cases = vec![
r"\\?\c\foo", r"\\?\c\foo/bar", r"\\?\c:foo", r"\\?\cc:foo", r"\\?\c:foo\bar", ];
for malformed in malformed_cases {
let result = soft_canonicalize(malformed);
if let Ok(path) = result {
let path_str = path.to_str().expect("valid UTF-8");
assert!(
path_str.starts_with(r"\\?\"),
"Malformed UNC path should remain UNC or error: {}",
malformed
);
}
}
}
#[cfg(all(windows, feature = "anchored"))]
#[test]
fn test_anchored_with_dunce_simplification() {
use tempfile::TempDir;
let tmp = TempDir::new().expect("create tempdir");
let base = soft_canonicalize(tmp.path()).expect("canonicalize tempdir");
let result = anchored_canonicalize(base, "foo/bar/file.txt").expect("should canonicalize");
let result_str = result.to_str().expect("valid UTF-8");
assert!(
!result_str.starts_with(r"\\?\"),
"Anchored paths with dunce feature should simplify, got: {}",
result_str
);
assert!(
result_str.contains("foo") && result_str.contains("bar"),
"Path should contain expected components"
);
}
#[cfg(all(windows, feature = "anchored"))]
#[test]
fn test_anchored_with_emoji_and_dunce() {
use tempfile::TempDir;
let tmp = TempDir::new().expect("create tempdir");
let base = soft_canonicalize(tmp.path()).expect("canonicalize tempdir");
let result = anchored_canonicalize(base, "😀/🎃/file.txt").expect("should canonicalize");
let result_str = result.to_str().expect("valid UTF-8");
assert!(
!result_str.starts_with(r"\\?\"),
"Anchored emoji path should simplify"
);
assert!(
result_str.contains('😀') && result_str.contains('🎃'),
"Emoji should be preserved in simplified path"
);
}
#[cfg(all(windows, feature = "anchored"))]
#[test]
fn test_anchored_with_reserved_names_stay_unc() {
use tempfile::TempDir;
let tmp = TempDir::new().expect("create tempdir");
let base = soft_canonicalize(tmp.path()).expect("canonicalize tempdir");
let result = anchored_canonicalize(base, "subdir/CON.txt").expect("should canonicalize");
let result_str = result.to_str().expect("valid UTF-8");
assert!(
result_str.starts_with(r"\\?\"),
"Path with reserved name should remain UNC for safety, got: {}",
result_str
);
}
#[cfg(all(windows, feature = "anchored"))]
#[test]
fn test_anchored_long_path_stays_unc() {
use tempfile::TempDir;
let tmp = TempDir::new().expect("create tempdir");
let base = soft_canonicalize(tmp.path()).expect("canonicalize tempdir");
let long_component = "a".repeat(200);
let long_path = format!(
"{}/{}_{}/file.txt",
long_component, long_component, long_component
);
let result = anchored_canonicalize(base, long_path).expect("should canonicalize");
let result_str = result.to_str().expect("valid UTF-8");
assert!(
result_str.starts_with(r"\\?\"),
"Long anchored path should remain UNC, got length: {}",
result_str.len()
);
}
#[cfg(windows)]
#[test]
fn test_nonexisting_path_simplification() {
use tempfile::TempDir;
let tmp = TempDir::new().expect("create tempdir");
let nonexisting = tmp.path().join("does").join("not").join("exist.txt");
let result = soft_canonicalize(nonexisting).expect("should canonicalize non-existing");
let result_str = result.to_str().expect("valid UTF-8");
assert!(
!result_str.starts_with(r"\\?\"),
"Non-existing path should simplify with dunce feature, got: {}",
result_str
);
assert!(
result_str.contains("does")
&& result_str.contains("not")
&& result_str.contains("exist.txt"),
"Non-existing components should be preserved"
);
}
#[cfg(windows)]
#[test]
fn test_nonexisting_with_reserved_name_stays_unc() {
use tempfile::TempDir;
let tmp = TempDir::new().expect("create tempdir");
let reserved_nonexisting = tmp.path().join("some").join("path").join("CON");
let result = soft_canonicalize(reserved_nonexisting).expect("should canonicalize");
let result_str = result.to_str().expect("valid UTF-8");
assert!(
result_str.starts_with(r"\\?\"),
"Non-existing path with reserved name should remain UNC, got: {}",
result_str
);
}
#[cfg(windows)]
#[test]
fn test_nonexisting_long_path_stays_unc() {
use tempfile::TempDir;
let tmp = TempDir::new().expect("create tempdir");
let long = "a".repeat(100);
let long_nonexisting = tmp
.path()
.join(&long)
.join(&long)
.join(&long)
.join("file.txt");
let result = soft_canonicalize(long_nonexisting).expect("should canonicalize");
let result_str = result.to_str().expect("valid UTF-8");
assert!(
result_str.starts_with(r"\\?\"),
"Long non-existing path should remain UNC, got: {}",
result_str
);
}
#[cfg(unix)]
#[test]
fn test_dunce_noop_on_unix() {
use tempfile::TempDir;
let tmp = TempDir::new().expect("create tempdir");
let path = tmp.path().join("foo").join("bar").join("file.txt");
let result = soft_canonicalize(path).expect("should canonicalize on Unix");
assert!(result.is_absolute());
assert!(result.to_str().unwrap().contains("foo"));
assert!(result.to_str().unwrap().contains("bar"));
}
#[cfg(all(unix, feature = "anchored"))]
#[test]
fn test_anchored_dunce_noop_on_unix() {
use tempfile::TempDir;
let tmp = TempDir::new().expect("create tempdir");
let base = soft_canonicalize(tmp.path()).expect("canonicalize tempdir");
let result = anchored_canonicalize(&base, "foo/bar/file.txt").expect("should canonicalize");
assert!(result.is_absolute());
assert_eq!(result, base.join("foo").join("bar").join("file.txt"));
}