use crate::anchored_canonicalize;
use std::fs;
use tempfile::TempDir;
#[cfg(unix)]
#[test]
fn test_relative_symlink_escaping_upwards_is_clamped() {
use std::os::unix::fs::symlink;
let td = TempDir::new().unwrap();
let anchor = td.path().join("vroot");
fs::create_dir_all(&anchor).unwrap();
let abs_anchor = fs::canonicalize(&anchor).unwrap();
let subdir = abs_anchor.join("subdir");
fs::create_dir_all(&subdir).unwrap();
let symlink_path = subdir.join("link");
symlink("../../../etc/passwd", symlink_path).unwrap();
let result = anchored_canonicalize(&abs_anchor, "subdir/link").unwrap();
assert!(
result.starts_with(&abs_anchor),
"Relative symlink should be clamped to anchor. Got: {:?}, Anchor: {:?}",
result,
abs_anchor
);
let expected = abs_anchor.join("etc/passwd");
assert_eq!(
result, expected,
"Relative symlink ../../../etc/passwd from anchor/subdir should resolve to anchor/etc/passwd"
);
}
#[cfg(unix)]
#[test]
fn test_relative_symlink_with_moderate_escape_is_clamped() {
use std::os::unix::fs::symlink;
let td = TempDir::new().unwrap();
let anchor = td.path().join("vroot");
fs::create_dir_all(&anchor).unwrap();
let abs_anchor = fs::canonicalize(&anchor).unwrap();
let deep_dir = abs_anchor.join("a/b/c");
fs::create_dir_all(&deep_dir).unwrap();
let symlink_path = deep_dir.join("link");
symlink("../../../../opt/file", symlink_path).unwrap();
let result = anchored_canonicalize(&abs_anchor, "a/b/c/link").unwrap();
assert!(
result.starts_with(&abs_anchor),
"Relative symlink should be clamped to anchor. Got: {:?}, Anchor: {:?}",
result,
abs_anchor
);
let expected = abs_anchor.join("opt/file");
assert_eq!(
result, expected,
"Relative symlink ../../../../opt/file from anchor/a/b/c should resolve to anchor/opt/file"
);
}
#[cfg(unix)]
#[test]
fn test_relative_symlink_within_anchor_no_escape() {
use std::os::unix::fs::symlink;
let td = TempDir::new().unwrap();
let anchor = td.path().join("vroot");
fs::create_dir_all(&anchor).unwrap();
let abs_anchor = fs::canonicalize(&anchor).unwrap();
let dir1 = abs_anchor.join("dir1");
let dir2 = abs_anchor.join("dir2");
fs::create_dir_all(&dir1).unwrap();
fs::create_dir_all(&dir2).unwrap();
fs::write(dir2.join("file"), b"test data").unwrap();
let symlink_path = dir1.join("link");
symlink("../dir2/file", symlink_path).unwrap();
let result = anchored_canonicalize(&abs_anchor, "dir1/link").unwrap();
let expected = abs_anchor.join("dir2/file");
assert_eq!(
result, expected,
"Relative symlink within anchor bounds should resolve normally"
);
}
#[cfg(windows)]
#[test]
fn test_windows_relative_symlink_escaping_is_clamped() -> std::io::Result<()> {
use std::os::windows::fs::symlink_file;
let td = TempDir::new()?;
let anchor = td.path().join("vroot");
fs::create_dir_all(&anchor)?;
let abs_anchor = fs::canonicalize(&anchor)?;
let subdir = abs_anchor.join("subdir");
fs::create_dir_all(&subdir)?;
let symlink_path = subdir.join("link");
let create_result = symlink_file(r"..\..\..\Windows\System32\cmd.exe", symlink_path);
if let Err(e) = create_result {
if let Some(1314) = e.raw_os_error() {
eprintln!("Skipping test: symlink creation requires elevated privileges on Windows");
return Ok(());
}
return Err(e);
}
let result = anchored_canonicalize(&abs_anchor, r"subdir\link")?;
let expected = abs_anchor.join(r"Windows\System32\cmd.exe");
#[cfg(not(feature = "dunce"))]
{
assert_eq!(
result, expected,
"Relative symlink should be clamped to anchor\\Windows\\System32\\cmd.exe"
);
}
#[cfg(feature = "dunce")]
{
let result_str = result.to_string_lossy();
let expected_str = expected.to_string_lossy();
assert!(
!result_str.starts_with(r"\\?\"),
"dunce should simplify: {:?}",
result_str
);
assert_eq!(
result_str.as_ref(),
expected_str.trim_start_matches(r"\\?\"),
"Relative symlink should be clamped to anchor\\Windows\\System32\\cmd.exe"
);
}
Ok(())
}
#[cfg(windows)]
#[test]
fn test_windows_relative_symlink_within_anchor() -> std::io::Result<()> {
use std::os::windows::fs::symlink_file;
let td = TempDir::new()?;
let anchor = td.path().join("vroot");
fs::create_dir_all(&anchor)?;
let abs_anchor = fs::canonicalize(&anchor)?;
let dir1 = abs_anchor.join("dir1");
let dir2 = abs_anchor.join("dir2");
fs::create_dir_all(&dir1)?;
fs::create_dir_all(&dir2)?;
fs::write(dir2.join("file.txt"), b"test data")?;
let symlink_path = dir1.join("link");
let create_result = symlink_file(r"..\dir2\file.txt", symlink_path);
if let Err(e) = create_result {
if let Some(1314) = e.raw_os_error() {
eprintln!("Skipping test: symlink creation requires elevated privileges on Windows");
return Ok(());
}
return Err(e);
}
let result = anchored_canonicalize(&abs_anchor, r"dir1\link")?;
let expected = abs_anchor.join(r"dir2\file.txt");
#[cfg(not(feature = "dunce"))]
{
assert_eq!(
result, expected,
"Relative symlink within anchor should resolve normally"
);
}
#[cfg(feature = "dunce")]
{
let result_str = result.to_string_lossy();
let expected_str = expected.to_string_lossy();
assert!(
!result_str.starts_with(r"\\?\"),
"dunce should simplify: {:?}",
result_str
);
assert_eq!(
result_str.as_ref(),
expected_str.trim_start_matches(r"\\?\"),
"Relative symlink within anchor should resolve normally"
);
}
Ok(())
}
#[cfg(unix)]
#[test]
fn test_absolute_symlink_to_etc_is_clamped() {
use std::os::unix::fs::symlink;
let td = TempDir::new().unwrap();
let anchor = td.path().join("vroot");
fs::create_dir_all(&anchor).unwrap();
let abs_anchor = fs::canonicalize(&anchor).unwrap();
let symlink_path = abs_anchor.join("link_to_etc");
symlink("/etc/passwd", symlink_path).unwrap();
let result = anchored_canonicalize(&abs_anchor, "link_to_etc").unwrap();
assert!(
result.starts_with(&abs_anchor),
"Absolute symlink should be clamped to anchor. Got: {:?}, Anchor: {:?}",
result,
abs_anchor
);
let expected = abs_anchor.join("etc/passwd");
assert_eq!(
result, expected,
"Absolute symlink /etc/passwd should resolve to anchor/etc/passwd"
);
}
#[cfg(windows)]
#[test]
fn test_windows_absolute_symlink_to_windows_dir_is_clamped() -> std::io::Result<()> {
use std::os::windows::fs::symlink_dir;
let td = TempDir::new()?;
let anchor = td.path().join("vroot");
fs::create_dir_all(&anchor)?;
let abs_anchor = fs::canonicalize(&anchor)?;
let symlink_path = abs_anchor.join("link_to_windows");
let create_result = symlink_dir(r"C:\Windows", symlink_path);
if let Err(e) = create_result {
if let Some(1314) = e.raw_os_error() {
eprintln!("Skipping test: symlink creation requires elevated privileges on Windows");
return Ok(());
}
return Err(e);
}
let result = anchored_canonicalize(&abs_anchor, "link_to_windows")?;
let expected = abs_anchor.join("Windows");
#[cfg(not(feature = "dunce"))]
{
assert_eq!(
result, expected,
"Absolute symlink should be clamped to anchor\\Windows"
);
}
#[cfg(feature = "dunce")]
{
let result_str = result.to_string_lossy();
let expected_str = expected.to_string_lossy();
assert!(
!result_str.starts_with(r"\\?\"),
"dunce should simplify: {:?}",
result_str
);
assert_eq!(
result_str.as_ref(),
expected_str.trim_start_matches(r"\\?\"),
"Absolute symlink should be clamped to anchor\\Windows"
);
}
Ok(())
}