use crate::PathBoundary;
#[cfg(unix)]
#[test]
fn strict_symlink_outside_parent_traversal_is_rejected() {
use std::fs;
use std::os::unix::fs as unix_fs;
let td = tempfile::tempdir().unwrap();
let base = td.path();
let boundary_dir = base.join("boundary");
let outside_dir = base.join("outside");
fs::create_dir_all(&boundary_dir).unwrap();
fs::create_dir_all(&outside_dir).unwrap();
fs::write(boundary_dir.join("secret"), "decoy-in-boundary").unwrap();
fs::write(outside_dir.join("secret"), "real-outside-file").unwrap();
unix_fs::symlink(&outside_dir, boundary_dir.join("link")).unwrap();
let restriction: PathBoundary = PathBoundary::try_new(&boundary_dir).unwrap();
let result = restriction.strict_join("link/../secret");
assert!(
result.is_err(),
"strict_join must reject symlink/../sibling when the symlink points outside. \
Got: {:?}",
result.unwrap().strictpath_display()
);
}
#[cfg(unix)]
#[test]
fn strict_relative_symlink_outside_parent_traversal_is_rejected() {
use std::fs;
use std::os::unix::fs as unix_fs;
let td = tempfile::tempdir().unwrap();
let base = td.path();
let boundary_dir = base.join("boundary");
let outside_dir = base.join("outside");
fs::create_dir_all(&boundary_dir).unwrap();
fs::create_dir_all(&outside_dir).unwrap();
fs::write(boundary_dir.join("secret"), "decoy").unwrap();
unix_fs::symlink("../outside", boundary_dir.join("link")).unwrap();
let restriction: PathBoundary = PathBoundary::try_new(&boundary_dir).unwrap();
let result = restriction.strict_join("link/../secret");
assert!(
result.is_err(),
"strict_join must reject relative symlink/../sibling escape. Got: {:?}",
result.unwrap().strictpath_display()
);
}
#[cfg(unix)]
#[test]
fn strict_symlink_inside_parent_traversal_reaches_correct_sibling() {
use std::fs;
use std::os::unix::fs as unix_fs;
let td = tempfile::tempdir().unwrap();
let boundary_dir = td.path();
let nested = boundary_dir.join("nested");
let nested_dir = nested.join("dir");
fs::create_dir_all(&nested_dir).unwrap();
fs::write(nested.join("a"), "correct-nested-a").unwrap();
fs::write(boundary_dir.join("a"), "wrong-root-decoy").unwrap();
unix_fs::symlink("nested/dir", boundary_dir.join("link")).unwrap();
let restriction: PathBoundary = PathBoundary::try_new(boundary_dir).unwrap();
let sp = restriction
.strict_join("link/../a")
.expect("link/../a should resolve inside boundary to nested/a");
let content = sp.read_to_string().unwrap();
assert_eq!(
content,
"correct-nested-a",
"strict_join(\"link/../a\") must reach nested/a, not the root decoy. \
Resolved to: {}",
sp.strictpath_display()
);
}
#[cfg(unix)]
#[test]
fn strict_symlink_inside_parent_traversal_nonexistent_suffix() {
use std::fs;
use std::os::unix::fs as unix_fs;
let td = tempfile::tempdir().unwrap();
let boundary_dir = td.path();
let nested_dir = boundary_dir.join("nested").join("dir");
fs::create_dir_all(&nested_dir).unwrap();
unix_fs::symlink("nested/dir", boundary_dir.join("link")).unwrap();
let restriction: PathBoundary = PathBoundary::try_new(boundary_dir).unwrap();
let sp = restriction
.strict_join("link/../nonexistent")
.expect("link/../nonexistent should resolve inside boundary");
let display = sp.strictpath_display().to_string();
assert!(
display.contains("nested"),
"Resolved path should be under nested/, got: {display}"
);
}
#[cfg(all(unix, feature = "virtual-path"))]
#[test]
fn virtual_symlink_outside_parent_traversal_is_clamped() {
use crate::VirtualRoot;
use std::fs;
use std::os::unix::fs as unix_fs;
let td = tempfile::tempdir().unwrap();
let base = td.path();
let boundary_dir = base.join("boundary");
let outside_dir = base.join("outside");
fs::create_dir_all(&boundary_dir).unwrap();
fs::create_dir_all(&outside_dir).unwrap();
unix_fs::symlink(&outside_dir, boundary_dir.join("link")).unwrap();
let vroot: VirtualRoot = VirtualRoot::try_new(&boundary_dir).unwrap();
let vp = vroot
.virtual_join("link/../secret")
.expect("VirtualPath should clamp, not error");
let canonical_boundary = fs::canonicalize(&boundary_dir).unwrap();
let system_path = vp.interop_path();
assert!(
AsRef::<std::path::Path>::as_ref(system_path).starts_with(&canonical_boundary),
"Virtual clamped path must stay inside boundary. Got: {system_path:?}"
);
}
#[cfg(all(unix, feature = "virtual-path"))]
#[test]
fn virtual_symlink_inside_parent_traversal_reaches_correct_sibling() {
use crate::VirtualRoot;
use std::fs;
use std::os::unix::fs as unix_fs;
let td = tempfile::tempdir().unwrap();
let boundary_dir = td.path();
let nested = boundary_dir.join("nested");
let nested_dir = nested.join("dir");
fs::create_dir_all(&nested_dir).unwrap();
fs::write(nested.join("a"), "correct-nested-a").unwrap();
fs::write(boundary_dir.join("a"), "wrong-root-decoy").unwrap();
unix_fs::symlink("nested/dir", boundary_dir.join("link")).unwrap();
let vroot: VirtualRoot = VirtualRoot::try_new(boundary_dir).unwrap();
let vp = vroot
.virtual_join("link/../a")
.expect("link/../a with internal symlink should succeed");
let content = vp.read_to_string().unwrap();
assert_eq!(
content,
"correct-nested-a",
"virtual_join(\"link/../a\") must reach nested/a. Got display: {}",
vp.virtualpath_display()
);
let display = vp.virtualpath_display().to_string();
assert_eq!(display, "/nested/a", "Expected /nested/a, got {display}");
}
#[cfg(windows)]
#[test]
fn strict_junction_outside_parent_traversal_stays_inside_on_windows() {
use std::fs;
let td = tempfile::tempdir().unwrap();
let base = td.path();
let boundary_dir = base.join("boundary");
let outside_dir = base.join("outside");
fs::create_dir_all(&boundary_dir).unwrap();
fs::create_dir_all(&outside_dir).unwrap();
fs::write(boundary_dir.join("secret"), "inside-boundary").unwrap();
let jlink = boundary_dir.join("jlink");
let outside_canonical = fs::canonicalize(&outside_dir).unwrap();
if junction::create(&outside_canonical, &jlink).is_err() {
return;
}
let restriction: PathBoundary = PathBoundary::try_new(&boundary_dir).unwrap();
let result = restriction.strict_join("jlink/../secret");
match result {
Ok(sp) => {
let content = sp.read_to_string().unwrap();
assert_eq!(
content, "inside-boundary",
"Windows lexical resolution should reach boundary/secret"
);
}
Err(_) => {
}
}
}
#[cfg(windows)]
#[test]
fn strict_junction_inside_parent_traversal_lexical_on_windows() {
use std::fs;
let td = tempfile::tempdir().unwrap();
let boundary_dir = td.path();
let nested = boundary_dir.join("nested");
let nested_dir = nested.join("dir");
fs::create_dir_all(&nested_dir).unwrap();
fs::write(nested.join("a"), "correct-nested-a").unwrap();
fs::write(boundary_dir.join("a"), "root-level-a").unwrap();
let nested_dir_canonical = fs::canonicalize(&nested_dir).unwrap();
let jlink = boundary_dir.join("jlink");
if junction::create(&nested_dir_canonical, &jlink).is_err() {
return;
}
let restriction: PathBoundary = PathBoundary::try_new(boundary_dir).unwrap();
let sp = restriction
.strict_join("jlink/../a")
.expect("jlink/../a should resolve inside boundary");
let content = sp.read_to_string().unwrap();
assert_eq!(
content, "root-level-a",
"Windows resolves junction/../a lexically to boundary/a"
);
}
#[cfg(unix)]
#[test]
fn strict_chained_symlinks_parent_traversal() {
use std::fs;
use std::os::unix::fs as unix_fs;
let td = tempfile::tempdir().unwrap();
let boundary_dir = td.path();
let sub1 = boundary_dir.join("sub1");
let sub2 = sub1.join("sub2");
fs::create_dir_all(&sub2).unwrap();
fs::write(sub1.join("a"), "correct-sub1-a").unwrap();
fs::write(boundary_dir.join("a"), "wrong-root-decoy").unwrap();
unix_fs::symlink("sub1", boundary_dir.join("link1")).unwrap();
unix_fs::symlink("sub2", sub1.join("link2")).unwrap();
let restriction: PathBoundary = PathBoundary::try_new(boundary_dir).unwrap();
let sp = restriction
.strict_join("link1/link2/../a")
.expect("chained symlink traversal should resolve inside boundary");
let content = sp.read_to_string().unwrap();
assert_eq!(
content,
"correct-sub1-a",
"Chained symlink/.. must follow links, not collapse lexically. \
Resolved to: {}",
sp.strictpath_display()
);
}
#[cfg(unix)]
#[test]
fn strict_symlink_to_dot_parent_traversal_escape() {
use std::fs;
use std::os::unix::fs as unix_fs;
let td = tempfile::tempdir().unwrap();
let base = td.path();
let boundary_dir = base.join("boundary");
fs::create_dir_all(&boundary_dir).unwrap();
unix_fs::symlink(".", boundary_dir.join("link")).unwrap();
let restriction: PathBoundary = PathBoundary::try_new(&boundary_dir).unwrap();
let result = restriction.strict_join("link/../../escape");
assert!(
result.is_err(),
"link(→.)/../../escape must be rejected as boundary escape"
);
let safe = restriction.strict_join("link/..");
if let Ok(sp) = safe {
let canonical_boundary = fs::canonicalize(&boundary_dir).unwrap();
assert_eq!(
std::path::PathBuf::from(sp.interop_path()),
canonical_boundary,
"link/.. should resolve to boundary root"
);
}
}
#[cfg(unix)]
#[test]
fn strict_symlink_deep_parent_traversal_escape() {
use std::fs;
use std::os::unix::fs as unix_fs;
let td = tempfile::tempdir().unwrap();
let base = td.path();
let boundary_dir = base.join("boundary");
let nested = boundary_dir.join("a").join("b").join("c");
fs::create_dir_all(&nested).unwrap();
unix_fs::symlink("a/b/c", boundary_dir.join("link")).unwrap();
let restriction: PathBoundary = PathBoundary::try_new(&boundary_dir).unwrap();
let inside = restriction.strict_join("link/../../../within");
if let Ok(sp) = &inside {
let display = sp.strictpath_display().to_string();
assert!(
!display.contains(".."),
"Resolved path must not contain raw ..: {display}"
);
}
let escaped = restriction.strict_join("link/../../../../escape");
assert!(
escaped.is_err(),
"link/../../../../escape must be rejected when link points 3 levels deep"
);
}