use crate::anchored_canonicalize;
use std::fs;
use tempfile::TempDir;
#[cfg(unix)]
#[test]
fn anchored_strips_root_and_clamps_dotdot() -> std::io::Result<()> {
let td = TempDir::new()?;
let anchor = td.path().join("home").join("myuser");
fs::create_dir_all(&anchor)?;
let abs_anchor = fs::canonicalize(&anchor)?;
let out1 = anchored_canonicalize(&abs_anchor, "/etc/passwd")?;
let out2 = anchored_canonicalize(&abs_anchor, "../../../../../etc/passwd")?;
assert_eq!(out1, abs_anchor.join("etc").join("passwd"));
assert_eq!(out2, abs_anchor.join("etc").join("passwd"));
Ok(())
}
#[cfg(unix)]
#[test]
fn absolute_symlink_inside_anchor_keeps_within_anchor() -> std::io::Result<()> {
let td = TempDir::new()?;
let anchor = td.path().join("home").join("myuser");
let docs = anchor.join("docs");
let project = anchor.join("project");
fs::create_dir_all(docs.join("dir1/dir2/dir3"))?;
fs::create_dir_all(&project)?;
fs::create_dir_all(&project)?;
fs::create_dir_all(&anchor)?; let abs_anchor = fs::canonicalize(&anchor)?;
let target = abs_anchor.join("docs/dir1/dir2/dir3/project");
let link = abs_anchor.join("project/docs");
fs::create_dir_all(link.parent().unwrap())?;
std::os::unix::fs::symlink(target, &link)?;
let out = anchored_canonicalize(&abs_anchor, "project/docs/../../etc/passwd")?;
assert_eq!(out, abs_anchor.join("docs/dir1/dir2/etc/passwd"));
Ok(())
}
#[cfg(unix)]
#[test]
fn absolute_symlink_is_clamped_to_anchor() -> std::io::Result<()> {
let td = TempDir::new()?;
let anchor = td.path().join("home").join("myuser");
fs::create_dir_all(&anchor)?;
let abs_anchor = fs::canonicalize(&anchor)?;
let outside = td.path().join("etc/outside/anchor/path");
fs::create_dir_all(&outside)?;
let abs_outside = fs::canonicalize(&outside)?;
let link = abs_anchor.join("escape_anchor_dir");
std::os::unix::fs::symlink(&abs_outside, link)?;
let out = anchored_canonicalize(&abs_anchor, "escape_anchor_dir")?;
assert!(
out.starts_with(&abs_anchor),
"Absolute symlink should be clamped to anchor. Got: {:?}, Anchor: {:?}",
out,
abs_anchor
);
let outside_stripped = abs_outside.strip_prefix("/").unwrap();
let expected = abs_anchor.join(outside_stripped);
assert_eq!(out, expected);
Ok(())
}
#[cfg(unix)]
#[test]
fn relative_symlink_resolves_under_anchor_and_clamps() -> std::io::Result<()> {
let td = TempDir::new()?;
let anchor = td.path().join("home").join("myuser");
fs::create_dir_all(&anchor)?;
let abs_anchor = fs::canonicalize(&anchor)?;
let docs = abs_anchor.join("docs/dir1/dir2/dir3/project");
fs::create_dir_all(docs)?;
let project = abs_anchor.join("project");
fs::create_dir_all(&project)?;
let link_parent = project;
let link = link_parent.join("docs");
std::os::unix::fs::symlink("../docs/dir1/dir2/dir3/project", link)?;
let out = anchored_canonicalize(&abs_anchor, "project/docs/../../etc/passwd")?;
assert_eq!(out, abs_anchor.join("docs/dir1/dir2/etc/passwd"));
Ok(())
}
#[cfg(unix)]
#[test]
fn hop_limit_and_cycles_are_detected() -> std::io::Result<()> {
use std::os::unix::fs::symlink;
let td = TempDir::new()?;
let anchor = td.path().join("x");
fs::create_dir_all(&anchor)?;
let abs_anchor = fs::canonicalize(&anchor)?;
let a = abs_anchor.join("a");
let b = abs_anchor.join("b");
symlink("b", a)?; symlink("a", b)?;
let err = anchored_canonicalize(&abs_anchor, "a").unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
Ok(())
}
#[cfg(unix)]
#[test]
fn anchor_can_be_non_existing() -> std::io::Result<()> {
let missing = std::path::Path::new("/definitely/does/not/exist/soft_canon_anchor_test");
let out = anchored_canonicalize(missing, "foo")?;
assert!(out.is_absolute());
assert!(out.ends_with("foo"));
Ok(())
}
#[cfg(windows)]
#[test]
fn anchored_basic_clamp_windows() -> std::io::Result<()> {
let td = TempDir::new()?;
let anchor = td.path().join("a").join("b");
fs::create_dir_all(&anchor)?;
let abs_anchor = crate::soft_canonicalize(&anchor)?;
let out1 = anchored_canonicalize(&abs_anchor, r"/etc/passwd")?;
let out2 = anchored_canonicalize(&abs_anchor, r"..\\..\\..\\etc\\passwd")?;
let expected =
std::path::PathBuf::from(format!(r"{}\etc\passwd", abs_anchor.to_string_lossy()));
assert_eq!(out1, expected);
assert_eq!(out2, expected);
Ok(())
}
#[cfg(windows)]
#[test]
fn ads_validation_applies() -> std::io::Result<()> {
let td = TempDir::new()?;
let anchor = td.path().join("x");
fs::create_dir_all(&anchor)?;
let abs_anchor = crate::soft_canonicalize(&anchor)?;
let err = anchored_canonicalize(&abs_anchor, r"bad:part\tail").unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
let ok = anchored_canonicalize(&abs_anchor, r"file.txt:stream");
assert!(ok.is_ok());
Ok(())
}
#[cfg(unix)]
#[test]
fn anchored_canonicalize_enforces_virtual_boundary() -> std::io::Result<()> {
use std::fs;
let td = TempDir::new()?;
let anchor = td.path().join("secure_root");
fs::create_dir_all(&anchor)?;
let abs_anchor = fs::canonicalize(&anchor)?;
let outside = td.path().join("external");
fs::create_dir_all(&outside)?;
let abs_outside = fs::canonicalize(&outside)?;
let escape_link = abs_anchor.join("escape_link");
std::os::unix::fs::symlink(&abs_outside, escape_link)?;
let result = anchored_canonicalize(&abs_anchor, "escape_link")?;
assert!(
result.starts_with(&abs_anchor),
"Result should be clamped to anchor. Got: {:?}, Anchor: {:?}",
result,
abs_anchor
);
let outside_stripped = abs_outside.strip_prefix("/").unwrap();
let expected = abs_anchor.join(outside_stripped);
assert_eq!(result, expected);
let safe = anchored_canonicalize(&abs_anchor, "safe/path")?;
assert!(safe.starts_with(&abs_anchor));
assert_eq!(safe, abs_anchor.join("safe/path"));
Ok(())
}