#[cfg(windows)]
use crate::soft_canonicalize;
#[cfg(windows)]
use std::path::PathBuf;
#[cfg(windows)]
#[test]
fn test_windows_specific_paths() -> std::io::Result<()> {
let unc_style = r"\\?\C:\temp\non\existing\file.txt";
if let Ok(result) = soft_canonicalize(unc_style) {
assert!(result.is_absolute());
assert!(result.to_string_lossy().contains("file.txt"));
}
let drive_path = "C:/non/existing/file.txt";
let result = soft_canonicalize(drive_path)?;
assert!(result.is_absolute());
assert!(result.to_string_lossy().contains("file.txt"));
let mixed_path = r"C:\non/existing\file.txt";
let result = soft_canonicalize(mixed_path)?;
assert!(result.is_absolute());
assert!(result.to_string_lossy().contains("file.txt"));
Ok(())
}
#[cfg(windows)]
#[test]
fn test_windows_unc_nonexistent_absolute_has_expected_prefix() {
let got = soft_canonicalize(r"C:\NonExistent\Path\That\Does\Not\Exist").unwrap();
#[cfg(not(feature = "dunce"))]
{
let expected = PathBuf::from(r"\\?\C:\\NonExistent\Path\That\Does\Not\Exist");
assert_eq!(
got, expected,
"Without dunce feature, must return extended-length UNC format"
);
assert!(
got.to_string_lossy().starts_with(r"\\?\"),
"Expected UNC prefix \\\\?\\, got: {}",
got.display()
);
}
#[cfg(feature = "dunce")]
{
let got_str = got.to_string_lossy();
assert!(
!got_str.starts_with(r"\\?\"),
"With dunce feature, safe paths should be simplified (no \\\\?\\), got: {}",
got_str
);
assert!(got_str.ends_with(r"NonExistent\Path\That\Does\Not\Exist"));
}
}
#[cfg(windows)]
#[test]
fn test_windows_nonexistent_jail_starts_with_consistency() {
let jail_raw = r"C:\NonExistent\Path\That\Does\Not\Exist";
let child_raw = format!("{jail_raw}\\foo.txt");
let jail = soft_canonicalize(jail_raw).expect("canonicalize jail");
let child = soft_canonicalize(child_raw).expect("canonicalize child");
let expected = jail.join("foo.txt");
assert_eq!(child, expected);
}
#[cfg(windows)]
#[test]
fn test_windows_extended_prefix_idempotent_for_nonexistent() {
let verbatim = PathBuf::from(r"\\?\C:\\NonExistent\Path\That\Does\Not\Exist");
let again = soft_canonicalize(&verbatim).expect("canonicalize verbatim");
#[cfg(not(feature = "dunce"))]
{
assert_eq!(
again, verbatim,
"Without dunce, verbatim path canonicalization must be idempotent"
);
}
#[cfg(feature = "dunce")]
{
let again_str = again.to_string_lossy();
let verbatim_str = verbatim.to_string_lossy();
let again_normalized = PathBuf::from(again_str.strip_prefix(r"\\?\").unwrap_or(&again_str));
let verbatim_normalized =
PathBuf::from(verbatim_str.strip_prefix(r"\\?\").unwrap_or(&verbatim_str));
assert_eq!(
again_normalized, verbatim_normalized,
"With dunce, normalized paths must be equivalent"
);
}
}
#[cfg(windows)]
#[test]
fn test_windows_unc_server_share_nonexistent_starts_with() {
let jail_raw = r"\\server\share\nonexistent";
let child_raw = format!("{jail_raw}\\foo.txt");
let jail = soft_canonicalize(jail_raw).expect("canonicalize UNC jail");
let child = soft_canonicalize(child_raw).expect("canonicalize UNC child");
let expected = jail.join("foo.txt");
assert_eq!(child, expected);
}
#[cfg(windows)]
#[test]
fn test_windows_unc_root_canonicalizes_to_verbatim_unc() {
let input = r"\\server\share";
let got = soft_canonicalize(input).expect("canonicalize UNC root");
assert_eq!(got, PathBuf::from(r"\\?\UNC\server\share"));
}
#[cfg(windows)]
#[test]
fn test_windows_unc_root_with_trailing_separator_idempotent() {
let base = PathBuf::from(r"\\server\share");
let variants = [
PathBuf::from(r"\\server\share\\"),
PathBuf::from(r"\\server\share\."),
PathBuf::from(r"\\?\UNC\server\share\\"),
PathBuf::from(r"\\?\UNC\server\share\."),
];
let canonical_base = soft_canonicalize(&base).expect("canonicalize UNC base");
#[cfg(not(feature = "dunce"))]
{
let expected = PathBuf::from(r"\\?\UNC\server\share");
assert_eq!(
canonical_base, expected,
"Base should be \\\\?\\UNC\\server\\share"
);
for v in variants {
let got = soft_canonicalize(&v).expect("canonicalize UNC root variant");
assert_eq!(
got, expected,
"Variant {v:?} should normalize to {expected:?}"
);
}
}
#[cfg(feature = "dunce")]
{
let canonical_base_str = canonical_base.to_string_lossy();
let canonical_base_normalized = PathBuf::from(
canonical_base_str
.strip_prefix(r"\\?\")
.unwrap_or(&canonical_base_str),
);
for v in variants {
let got = soft_canonicalize(&v).expect("canonicalize UNC root variant");
let got_str = got.to_string_lossy();
let got_normalized = PathBuf::from(got_str.strip_prefix(r"\\?\").unwrap_or(&got_str));
assert_eq!(
got_normalized, canonical_base_normalized,
"Variant {v:?} should normalize to same as base"
);
}
}
}
#[cfg(windows)]
#[test]
fn test_windows_unc_very_deep_stress_fast() {
let mut p = PathBuf::from(r"\\?\UNC\server\share");
for i in 0..400u32 {
p.push(format!("dir{i:04}"));
}
p.push("leaf.txt");
let got = soft_canonicalize(&p).expect("canonicalize very deep UNC");
assert_eq!(got, p);
}
#[cfg(windows)]
#[test]
fn test_windows_verbatim_unc_idempotent() {
let input = PathBuf::from(r"\\?\UNC\server\share\path\to\file.txt");
let got = soft_canonicalize(&input).expect("canonicalize verbatim UNC");
assert_eq!(got, input);
}
#[cfg(windows)]
#[test]
fn test_windows_unc_mixed_separators_are_normalized() {
let input = r"\\server\share/mixed\\seps/dir\file.txt";
let got = soft_canonicalize(input).expect("canonicalize UNC with mixed separators");
let expected = PathBuf::from(r"\\?\UNC\server\share\mixed\seps\dir\file.txt");
assert_eq!(got, expected);
}
#[cfg(windows)]
#[test]
fn test_windows_unc_dotdot_does_not_escape_share_root() {
let input = r"\\server\share\folder\..\..\sibling\file.txt";
let got = soft_canonicalize(input).expect("canonicalize UNC with dotdot");
assert_eq!(got, PathBuf::from(r"\\?\UNC\server\share\sibling\file.txt"));
}
#[cfg(windows)]
#[test]
fn test_windows_unc_preserves_shortname_like_component_for_nonexistent() {
let input = r"\\server\share\PROGRA~1\foo.txt";
let got = soft_canonicalize(input).expect("canonicalize UNC with shortname-like component");
assert!(got.ends_with(PathBuf::from(r"PROGRA~1\foo.txt")));
}
#[cfg(windows)]
#[test]
fn test_windows_unc_preserves_trailing_dot_and_space_in_names() {
let input = r"\\server\share\dir. \file. txt"; let got = soft_canonicalize(input).expect("canonicalize UNC with trailing dot/space");
assert!(got.ends_with(PathBuf::from(r"dir. \file. txt")));
}
#[cfg(windows)]
#[test]
fn test_windows_multiple_drive_letters_produce_verbatim_disk_prefix() {
for drive in ['C', 'D', 'E', 'Z'] {
let input = format!(r"{drive}:\nonexistent\child.txt");
let got = soft_canonicalize(&input).expect("canonicalize drive letter path");
let got_str = got.to_string_lossy();
#[cfg(not(feature = "dunce"))]
{
let expected_starts = format!(r"\\?\{drive}:\");
assert!(
got_str.starts_with(&expected_starts),
"Without dunce, expected \\\\?\\{drive}:\\ prefix, got: {got_str}"
);
}
#[cfg(feature = "dunce")]
{
assert!(
!got_str.starts_with(r"\\?\"),
"With dunce, safe paths should not have \\\\?\\ prefix, got: {got_str}"
);
let expected_starts = format!(r"{drive}:\");
assert!(
got_str.starts_with(&expected_starts),
"Expected {drive}:\\ prefix, got: {got_str}"
);
}
assert!(
got.ends_with(PathBuf::from(r"nonexistent\child.txt")),
"Result should end with suffix for drive {drive}: {got:?}"
);
}
}
#[cfg(windows)]
#[test]
fn test_windows_raw_vs_canonicalized_starts_with_is_false() {
let jail_raw = PathBuf::from(r"C:\NonExistent\Path\That\Does\Not\Exist");
let child = soft_canonicalize(jail_raw.join("foo.txt")).expect("canonicalize child");
#[cfg(not(feature = "dunce"))]
{
assert!(
!child.starts_with(&jail_raw),
"Raw jail (C:\\...) must not match canonicalized child (\\\\?\\C:\\...) in starts_with"
);
assert!(child.to_string_lossy().starts_with(r"\\?\"));
}
#[cfg(feature = "dunce")]
{
let jail_canonical = soft_canonicalize(&jail_raw).expect("canonicalize jail");
let child_str = child.to_string_lossy();
let jail_canonical_str = jail_canonical.to_string_lossy();
let child_normalized = PathBuf::from(child_str.strip_prefix(r"\\?\").unwrap_or(&child_str));
let jail_normalized = PathBuf::from(
jail_canonical_str
.strip_prefix(r"\\?\")
.unwrap_or(&jail_canonical_str),
);
assert!(
child_normalized.starts_with(jail_normalized),
"Child should be under canonicalized jail (normalized)"
);
}
}
#[cfg(windows)]
#[test]
fn test_windows_relative_path_becomes_absolute_with_extended_prefix() {
use std::env;
let rel = r".\non\existent\file.txt";
let abs = soft_canonicalize(rel).expect("canonicalize relative");
assert!(abs.is_absolute());
let cwd = soft_canonicalize(env::current_dir().unwrap()).expect("canonicalize cwd");
let expected = cwd.join(r"non\existent\file.txt");
assert_eq!(abs, expected);
}
#[cfg(windows)]
#[test]
fn test_windows_nonexistent_shortname_component_preserved() {
let p = r"C:\NonExistent\PROGRA~1\foo.txt";
let got = soft_canonicalize(p).expect("canonicalize with shortname component");
assert!(
got.ends_with(PathBuf::from(r"PROGRA~1\foo.txt")),
"Expected to preserve shortname component in non-existent path: {got:?}"
);
}
#[cfg(windows)]
#[test]
fn test_windows_false_positive_tilde_names_not_treated_as_short() {
let test_cases = vec![
r"C:\Users\test\hello~world.txt",
r"C:\Projects\backup~file.doc",
r"C:\Config\settings~old.json",
r"C:\Temp\test~project\file.txt",
];
for test_path in test_cases {
let got = soft_canonicalize(test_path).expect("canonicalize regular tilde filename");
assert!(
got.to_string_lossy().contains('~'),
"Tilde should be preserved in regular filename: {got:?}"
);
}
}
#[cfg(windows)]
#[test]
fn test_windows_actual_short_name_detection() {
let short_name_paths = vec![
r"C:\PROGRA~1\MyApp\config.txt",
r"C:\Users\RUNNER~1\Documents\file.txt",
r"C:\Temp\LONGFI~1.TXT",
];
for test_path in short_name_paths {
let got = soft_canonicalize(test_path).expect("canonicalize short name path");
assert!(got.is_absolute(), "Result should be absolute: {got:?}");
}
}
#[cfg(windows)]
#[test]
fn test_windows_device_namespace_lexical_only_pipe() {
let input = r"\\.\PIPE\name\..\other";
let got = soft_canonicalize(input).expect("canonicalize device namespace (PIPE)");
assert_eq!(got, PathBuf::from(r"\\.\PIPE\other"));
}
#[cfg(windows)]
#[test]
fn test_windows_device_namespace_globalroot_lexical() {
let input = r"\\?\GLOBALROOT\Device\HarddiskVolume1\foo\.\bar\..\baz";
let got = soft_canonicalize(input).expect("canonicalize GLOBALROOT path lexically");
assert_eq!(
got,
PathBuf::from(r"\\?\GLOBALROOT\Device\HarddiskVolume1\foo\baz")
);
}
#[cfg(windows)]
#[test]
fn test_windows_device_namespace_colon_is_rejected_as_ads() {
let input = r"\\.\PIPE\name:stream\..\other";
let err = soft_canonicalize(input).expect_err("colon in non-final component must be invalid");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
}
#[cfg(windows)]
#[test]
fn test_windows_device_namespace_idempotent_for_physicaldrive() {
let input = PathBuf::from(r"\\.\PhysicalDrive0");
let got = soft_canonicalize(&input).expect("canonicalize PhysicalDrive0 lexically");
assert_eq!(got, input);
}
#[cfg(windows)]
#[test]
fn test_windows_device_namespace_parent_clamps_at_prefix() {
let input = r"\\.\PIPE\name\..\..\other";
let got = soft_canonicalize(input).expect("canonicalize device namespace with double dotdot");
assert_eq!(got, PathBuf::from(r"\\.\PIPE\other"));
}
#[cfg(windows)]
#[test]
fn test_windows_trailing_spaces_and_dots_preserved_verbatim() {
let p1 = r"C:\NonExistent\trailing\file. ";
let p2 = r"C:\NonExistent\trailing\file..";
let got1 = soft_canonicalize(p1).expect("canonicalize trailing space");
let got2 = soft_canonicalize(p2).expect("canonicalize trailing dots");
let expected1 = PathBuf::from(r"\\?\C:\\NonExistent\trailing\file. ");
let expected2 = PathBuf::from(r"\\?\C:\\NonExistent\trailing\file..");
assert_eq!(got1, expected1);
assert_eq!(got2, expected2);
}
#[cfg(windows)]
#[test]
fn test_windows_symlink_ancestor_nonexisting_tail_matches_std() -> std::io::Result<()> {
use std::fs;
let td = tempfile::tempdir()?;
let base = td.path();
let target = base.join("target_dir");
fs::create_dir(&target)?;
let link = base.join("link_dir");
match std::os::windows::fs::symlink_dir(&target, &link) {
Ok(()) => {
let leaf = "child_nonexist.txt";
let got = soft_canonicalize(link.join(leaf))?;
let expected = std::fs::canonicalize(&target)?.join(leaf);
#[cfg(not(feature = "dunce"))]
{
assert_eq!(got, expected);
}
#[cfg(feature = "dunce")]
{
let got_str = got.to_string_lossy();
let expected_str = expected.to_string_lossy();
assert!(!got_str.starts_with(r"\\?\"), "dunce should simplify path");
assert!(expected_str.starts_with(r"\\?\"), "std returns UNC");
assert_eq!(got_str.as_ref(), expected_str.trim_start_matches(r"\\?\"));
}
}
Err(e) => {
eprintln!("Skipping symlink ancestor test due to symlink privilege error: {e}");
}
}
Ok(())
}