use crate::PathBoundary;
#[cfg(feature = "virtual-path")]
use crate::VirtualRoot;
#[test]
fn f1_strictpath_with_extension_rejects_separator_without_panic() {
let td = tempfile::tempdir().unwrap();
let boundary: PathBoundary = PathBoundary::try_new(td.path()).unwrap();
let f = boundary.strict_join("report.txt").unwrap();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
f.strictpath_with_extension("/../../../etc/passwd")
}));
assert!(
result.is_ok(),
"strictpath_with_extension must not panic on attacker-controlled \
extensions"
);
assert!(
result.unwrap().is_err(),
"separator-bearing extension must produce Err"
);
}
#[test]
fn f1_strictpath_with_extension_rejects_backslash_on_windows() {
let td = tempfile::tempdir().unwrap();
let boundary: PathBoundary = PathBoundary::try_new(td.path()).unwrap();
let f = boundary.strict_join("report.txt").unwrap();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
f.strictpath_with_extension(r"a\b")
}))
.expect("must not panic");
#[cfg(windows)]
assert!(result.is_err(), "backslash must be rejected on Windows");
#[cfg(not(windows))]
let _ = result; }
#[test]
fn f1_strictpath_with_extension_rejects_nul_byte() {
let td = tempfile::tempdir().unwrap();
let boundary: PathBoundary = PathBoundary::try_new(td.path()).unwrap();
let f = boundary.strict_join("report.txt").unwrap();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
f.strictpath_with_extension("x\0y")
}))
.expect("must not panic");
assert!(result.is_err(), "NUL byte in extension must produce Err");
}
#[cfg(feature = "virtual-path")]
#[test]
fn f1_virtualpath_with_extension_rejects_separator_without_panic() {
let td = tempfile::tempdir().unwrap();
let vroot: VirtualRoot = VirtualRoot::try_new(td.path()).unwrap();
let f = vroot.virtual_join("report.txt").unwrap();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
f.virtualpath_with_extension("/../../../etc/passwd")
}));
assert!(
result.is_ok(),
"virtualpath_with_extension must not panic on attacker-controlled \
extensions"
);
assert!(
result.unwrap().is_err(),
"separator-bearing extension must produce Err"
);
}
#[cfg(feature = "virtual-path")]
#[test]
fn f2_virtualpath_display_scrubs_carriage_return() {
let td = tempfile::tempdir().unwrap();
let vroot: VirtualRoot = VirtualRoot::try_new(td.path()).unwrap();
let v = vroot.virtual_join("foo\rDELETE_ME").unwrap();
let display = v.virtualpath_display().to_string();
assert!(
!display.contains('\r'),
"virtualpath_display leaks CR into user-facing output; got {:?}",
display
);
}
#[cfg(feature = "virtual-path")]
#[test]
fn f2_virtualpath_display_scrubs_ansi_escape() {
let td = tempfile::tempdir().unwrap();
let vroot: VirtualRoot = VirtualRoot::try_new(td.path()).unwrap();
let v = vroot.virtual_join("foo\x1b[2Jbar").unwrap();
let display = v.virtualpath_display().to_string();
assert!(
!display.contains('\x1b'),
"virtualpath_display leaks ESC into user-facing output; got bytes {:x?}",
display.as_bytes()
);
}
#[cfg(feature = "virtual-path")]
#[test]
fn f2_virtualpath_display_scrubs_all_c0_controls() {
let td = tempfile::tempdir().unwrap();
let vroot: VirtualRoot = VirtualRoot::try_new(td.path()).unwrap();
let payload = "a\x01b\x02c\x07d\x08e\x0bf\x0cg\x1ch";
let v = vroot.virtual_join(payload).unwrap();
let display = v.virtualpath_display().to_string();
for ch in display.chars() {
assert!(
ch == '/' || ch == '_' || (ch as u32) >= 0x20,
"virtualpath_display leaked a C0 control: {:?} (in {:?})",
ch,
display
);
}
}
#[cfg(feature = "virtual-path")]
#[test]
fn f2_virtualpath_display_scrubs_del() {
let td = tempfile::tempdir().unwrap();
let vroot: VirtualRoot = VirtualRoot::try_new(td.path()).unwrap();
let v = vroot.virtual_join("foo\x7fbar").unwrap();
let display = v.virtualpath_display().to_string();
assert!(
!display.contains('\x7f'),
"virtualpath_display leaks DEL (0x7f); got bytes {:x?}",
display.as_bytes()
);
}
#[cfg(feature = "virtual-path")]
#[test]
fn f2_virtualpath_display_preserves_newline_scrub() {
let td = tempfile::tempdir().unwrap();
let vroot: VirtualRoot = VirtualRoot::try_new(td.path()).unwrap();
let v = vroot.virtual_join("foo\nbar").unwrap();
let display = v.virtualpath_display().to_string();
assert!(!display.contains('\n'), "newline must remain scrubbed");
}
#[test]
fn f3_strictpath_parent_returns_none_at_boundary_root() {
let td = tempfile::tempdir().unwrap();
let boundary: PathBoundary = PathBoundary::try_new(td.path()).unwrap();
let root = boundary.into_strictpath().unwrap();
let parent = root.strictpath_parent();
assert!(
matches!(parent, Ok(None)),
"strictpath_parent must be Ok(None) at the boundary root; got {:?}",
parent
.as_ref()
.map(|o| o.as_ref().map(|p| p.strictpath_display().to_string()))
);
}
#[test]
fn f3_strictpath_parent_unchanged_for_non_root_paths() {
let td = tempfile::tempdir().unwrap();
let boundary: PathBoundary = PathBoundary::try_new(td.path()).unwrap();
boundary.strict_join("logs").unwrap().create_dir().unwrap();
let child = boundary.strict_join("logs/app.log").unwrap();
let parent = child
.strictpath_parent()
.unwrap()
.expect("non-root path must have a Some(parent)");
assert!(
parent.strictpath_display().to_string().ends_with("logs"),
"parent of logs/app.log should end with 'logs'"
);
}
#[test]
fn f3_create_parent_dir_at_boundary_root_is_noop() {
let td = tempfile::tempdir().unwrap();
let boundary: PathBoundary = PathBoundary::try_new(td.path()).unwrap();
let root = boundary.into_strictpath().unwrap();
root.create_parent_dir().unwrap();
root.create_parent_dir_all().unwrap();
}
#[test]
fn f3_strict_rename_at_boundary_root_does_not_emit_bogus_escape_error() {
let td = tempfile::tempdir().unwrap();
let boundary: PathBoundary = PathBoundary::try_new(td.path()).unwrap();
boundary.strict_join("src.txt").unwrap().write("x").unwrap();
boundary
.strict_join("src.txt")
.unwrap()
.strict_rename("renamed.txt")
.unwrap();
let root_as_sp = boundary.clone().into_strictpath().unwrap();
if let Err(e) = root_as_sp.strict_rename("anything") {
let msg = e.to_string();
assert!(
!msg.contains("escapes boundary"),
"relative rename on boundary root must not report a bogus escape; got {msg}"
);
}
}
#[test]
fn f4_pathboundary_from_str_does_not_create_directory() {
let td = tempfile::tempdir().unwrap();
let phantom = td.path().join("never-should-be-created-by-parse");
assert!(!phantom.exists(), "precondition: directory does not exist");
let parsed: std::result::Result<PathBoundary, _> = phantom.to_string_lossy().parse();
assert!(
parsed.is_err(),
"FromStr for PathBoundary must reject non-existent paths (not silently \
create them)"
);
assert!(
!phantom.exists(),
"FromStr must not materialize a directory — this would be a filesystem \
side effect from parsing untrusted input"
);
}
#[cfg(feature = "virtual-path")]
#[test]
fn f4_virtualroot_from_str_does_not_create_directory() {
let td = tempfile::tempdir().unwrap();
let phantom = td.path().join("never-should-be-created-by-parse-vroot");
assert!(!phantom.exists(), "precondition: directory does not exist");
let parsed: std::result::Result<VirtualRoot, _> = phantom.to_string_lossy().parse();
assert!(
parsed.is_err(),
"FromStr for VirtualRoot must reject non-existent paths"
);
assert!(
!phantom.exists(),
"FromStr must not materialize the sandbox — an attacker-controlled \
string could otherwise pick any writable directory as the sandbox"
);
}
#[test]
fn f4_pathboundary_from_str_accepts_existing_directory() {
let td = tempfile::tempdir().unwrap();
let parsed: PathBoundary = td
.path()
.to_string_lossy()
.parse()
.expect("existing directory should parse");
assert!(parsed.exists());
}
#[cfg(all(windows, feature = "junctions"))]
#[test]
fn f5_strict_junction_works_on_verbatim_prefixed_boundary() {
let td = tempfile::tempdir().unwrap();
let boundary: PathBoundary = PathBoundary::try_new(td.path()).unwrap();
let target = boundary.strict_join("target_dir").unwrap();
target.create_dir().unwrap();
let link_result = target.strict_junction("link_dir");
if let Err(e) = link_result {
let code = e.raw_os_error().unwrap_or(0);
assert_ne!(
code, 123,
"ERROR_INVALID_NAME from junction crate indicates the \\\\?\\ \
prefix was not stripped before creation"
);
}
}