use crate::soft_canonicalize;
use std::fs;
use std::path::Path;
use std::sync::Mutex;
use tempfile::tempdir;
static WORKING_DIR_MUTEX: Mutex<()> = Mutex::new(());
#[test]
fn test_existing_path() -> std::io::Result<()> {
let temp_dir = tempdir()?;
let result = soft_canonicalize(temp_dir.path())?;
let std_result = fs::canonicalize(temp_dir.path())?;
#[cfg(not(feature = "dunce"))]
{
assert_eq!(result, std_result, "Without dunce: must match std EXACTLY");
}
#[cfg(feature = "dunce")]
{
#[cfg(windows)]
{
let result_str = result.to_string_lossy();
let std_str = std_result.to_string_lossy();
assert!(
!result_str.starts_with(r"\\?\"),
"dunce should simplify safe paths"
);
assert!(std_str.starts_with(r"\\?\"), "std returns UNC");
}
#[cfg(not(windows))]
{
assert_eq!(result, std_result);
}
}
Ok(())
}
#[test]
fn test_non_existing_path() -> std::io::Result<()> {
let temp_dir = tempdir()?;
let non_existing = temp_dir
.path()
.join("non_existing_sub_dir/non_existing_file.txt");
let result = soft_canonicalize(non_existing)?;
let base_canonical = fs::canonicalize(temp_dir.path())?;
let expected_tail = base_canonical.join("non_existing_sub_dir/non_existing_file.txt");
#[cfg(not(feature = "dunce"))]
{
assert_eq!(
result, expected_tail,
"Without dunce: must match expected UNC format"
);
}
#[cfg(feature = "dunce")]
{
#[cfg(windows)]
{
let result_str = result.to_string_lossy();
let expected_str = expected_tail.to_string_lossy();
assert!(
!result_str.starts_with(r"\\?\"),
"dunce should simplify non-existing paths"
);
assert!(
expected_str.starts_with(r"\\?\"),
"expected has UNC from std"
);
assert!(result_str.ends_with(r"non_existing_sub_dir\non_existing_file.txt"));
}
#[cfg(not(windows))]
{
assert_eq!(result, expected_tail);
}
}
Ok(())
}
#[test]
fn test_relative_path() -> std::io::Result<()> {
let _lock = WORKING_DIR_MUTEX.lock().unwrap();
let result = soft_canonicalize(Path::new("non/existing/relative/path.txt"))?;
let current_dir = std::env::current_dir()?;
let expected = fs::canonicalize(current_dir)?
.join("non")
.join("existing")
.join("relative")
.join("path.txt");
assert!(result.is_absolute());
assert!(result.ends_with("non/existing/relative/path.txt"));
#[cfg(not(feature = "dunce"))]
{
assert_eq!(result, expected, "Without dunce: exact match");
}
#[cfg(feature = "dunce")]
{
#[cfg(windows)]
{
let result_str = result.to_string_lossy();
let expected_str = expected.to_string_lossy();
assert!(
!result_str.starts_with(r"\\?\"),
"dunce should simplify relative paths"
);
assert!(
expected_str.starts_with(r"\\?\"),
"expected has UNC from std"
);
assert!(result_str.ends_with(r"non\existing\relative\path.txt"));
}
#[cfg(not(windows))]
{
assert_eq!(result, expected);
}
}
Ok(())
}
#[test]
fn test_no_filesystem_modification() -> std::io::Result<()> {
let temp_dir = tempdir()?;
let initial_contents: Vec<_> = fs::read_dir(temp_dir.path())?.collect();
let initial_count = initial_contents.len();
let _result = soft_canonicalize(temp_dir.path().join("non/existing/deep/path.txt"))?;
let final_contents: Vec<_> = fs::read_dir(temp_dir.path())?.collect();
let final_count = final_contents.len();
assert_eq!(
initial_count, final_count,
"soft_canonicalize should not create any files during operation"
);
Ok(())
}
#[test]
fn test_readme_examples() -> std::io::Result<()> {
let _path = soft_canonicalize("some/path/../other/file.txt")?;
fn is_safe_path(user_path: &str, jail: &std::path::Path) -> std::io::Result<bool> {
let canonical_user = soft_canonicalize(user_path)?;
let canonical_jail = soft_canonicalize(jail)?;
Ok(canonical_user.starts_with(canonical_jail))
}
let temp_dir = tempdir()?;
let jail_path = temp_dir.path();
let safe_user_path = format!("{}/safe/file.txt", jail_path.display());
let is_safe = is_safe_path(&safe_user_path, jail_path)?;
assert!(is_safe, "Path inside jail should be safe");
let unsafe_user_path = format!("{}/../escape.txt", jail_path.display());
let is_unsafe = is_safe_path(&unsafe_user_path, jail_path)?;
assert!(
!is_unsafe,
"Path escaping jail should be detected as unsafe"
);
Ok(())
}