use crate::PathBoundary;
#[cfg(feature = "virtual-path")]
use crate::VirtualRoot;
#[test]
fn test_strict_read_dir_iterates_files() {
let temp = tempfile::tempdir().unwrap();
let test_dir: PathBoundary = PathBoundary::try_new(temp.path()).unwrap();
let dir = test_dir.strict_join("docs").unwrap();
dir.create_dir_all().unwrap();
test_dir
.strict_join("docs/readme.md")
.unwrap()
.write("# Readme")
.unwrap();
test_dir
.strict_join("docs/guide.md")
.unwrap()
.write("# Guide")
.unwrap();
test_dir
.strict_join("docs/api.md")
.unwrap()
.write("# API")
.unwrap();
let entries: Vec<_> = dir
.strict_read_dir()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(entries.len(), 3);
for entry in &entries {
assert!(entry.is_file());
}
}
#[test]
fn test_strict_read_dir_mixed_files_and_dirs() {
let temp = tempfile::tempdir().unwrap();
let test_dir: PathBoundary = PathBoundary::try_new(temp.path()).unwrap();
let root = test_dir.strict_join("project").unwrap();
root.create_dir_all().unwrap();
test_dir
.strict_join("project/file.txt")
.unwrap()
.write("content")
.unwrap();
test_dir
.strict_join("project/subdir")
.unwrap()
.create_dir_all()
.unwrap();
let entries: Vec<_> = root
.strict_read_dir()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(entries.len(), 2);
let files: Vec<_> = entries.iter().filter(|e| e.is_file()).collect();
let dirs: Vec<_> = entries.iter().filter(|e| e.is_dir()).collect();
assert_eq!(files.len(), 1);
assert_eq!(dirs.len(), 1);
}
#[test]
fn test_strict_read_dir_empty_directory() {
let temp = tempfile::tempdir().unwrap();
let test_dir: PathBoundary = PathBoundary::try_new(temp.path()).unwrap();
let empty = test_dir.strict_join("empty").unwrap();
empty.create_dir_all().unwrap();
let entries: Vec<_> = empty
.strict_read_dir()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert!(entries.is_empty());
}
#[test]
fn test_strict_read_dir_on_file_errors() {
let temp = tempfile::tempdir().unwrap();
let test_dir: PathBoundary = PathBoundary::try_new(temp.path()).unwrap();
let file = test_dir.strict_join("not_a_dir.txt").unwrap();
file.write("content").unwrap();
file.strict_read_dir().unwrap_err();
}
#[test]
#[cfg(feature = "virtual-path")]
fn test_virtual_read_dir_iterates_files() {
let temp = tempfile::tempdir().unwrap();
let vroot: VirtualRoot = VirtualRoot::try_new(temp.path()).unwrap();
let dir = vroot.virtual_join("uploads").unwrap();
dir.create_dir_all().unwrap();
vroot
.virtual_join("uploads/photo.jpg")
.unwrap()
.write(b"JPG")
.unwrap();
vroot
.virtual_join("uploads/doc.pdf")
.unwrap()
.write(b"PDF")
.unwrap();
let entries: Vec<_> = dir
.virtual_read_dir()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(entries.len(), 2);
for entry in &entries {
assert!(entry.is_file());
let display = entry.virtualpath_display().to_string();
assert!(display.starts_with("/uploads/"));
}
}
#[test]
#[cfg(feature = "virtual-path")]
fn test_virtual_read_dir_preserves_virtual_paths() {
let temp = tempfile::tempdir().unwrap();
let vroot: VirtualRoot = VirtualRoot::try_new(temp.path()).unwrap();
let dir = vroot.virtual_join("nested/deep").unwrap();
dir.create_dir_all().unwrap();
vroot
.virtual_join("nested/deep/file.txt")
.unwrap()
.write("test")
.unwrap();
let entries: Vec<_> = dir
.virtual_read_dir()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(
entry.virtualpath_display().to_string(),
"/nested/deep/file.txt"
);
}
#[test]
#[cfg(unix)]
fn test_strict_join_catches_escaping_symlinks() {
let temp = tempfile::tempdir().unwrap();
let boundary_dir = temp.path().join("boundary");
let outside_dir = temp.path().join("outside");
std::fs::create_dir_all(&boundary_dir).unwrap();
std::fs::create_dir_all(&outside_dir).unwrap();
let test_dir: PathBoundary = PathBoundary::try_new(&boundary_dir).unwrap();
let outside_file = outside_dir.join("secret.txt");
std::fs::write(&outside_file, "secret data").unwrap();
let link_path = boundary_dir.join("escape_link");
std::os::unix::fs::symlink(&outside_file, &link_path).unwrap();
let result = test_dir.strict_join("escape_link");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(
err,
crate::StrictPathError::PathEscapesBoundary { .. }
));
}
#[test]
#[cfg(all(unix, feature = "virtual-path"))]
fn test_virtual_join_clamps_escaping_symlink_target() {
let temp = tempfile::tempdir().unwrap();
let vroot_dir = temp.path().join("vroot");
let outside_dir = temp.path().join("outside");
std::fs::create_dir_all(&vroot_dir).unwrap();
std::fs::create_dir_all(&outside_dir).unwrap();
let vroot: VirtualRoot = VirtualRoot::try_new(&vroot_dir).unwrap();
let outside_file = outside_dir.join("external.txt");
std::fs::write(&outside_file, "external").unwrap();
let link_path = vroot_dir.join("escape_link");
std::os::unix::fs::symlink(&outside_file, &link_path).unwrap();
let result = vroot.virtual_join("escape_link");
assert!(
result.is_ok(),
"VirtualPath should clamp escaping symlinks, not error: {:?}",
result
);
let vpath = result.unwrap();
let canonical_vroot = std::fs::canonicalize(&vroot_dir).unwrap();
let system_path = vpath.interop_path();
assert!(
AsRef::<std::path::Path>::as_ref(system_path).starts_with(&canonical_vroot),
"Clamped path must be within vroot. Got: {:?}, VRoot: {:?}",
system_path,
canonical_vroot
);
}
#[test]
fn test_strict_path_set_permissions() {
let temp = tempfile::tempdir().unwrap();
let test_dir: PathBoundary = PathBoundary::try_new(temp.path()).unwrap();
let file = test_dir.strict_join("test.txt").unwrap();
file.write("content").unwrap();
let mut perms = file.metadata().unwrap().permissions();
perms.set_readonly(true);
file.set_permissions(perms).unwrap();
assert!(file.metadata().unwrap().permissions().readonly());
}
#[test]
fn test_strict_path_try_exists() {
let temp = tempfile::tempdir().unwrap();
let test_dir: PathBoundary = PathBoundary::try_new(temp.path()).unwrap();
let existing = test_dir.strict_join("exists.txt").unwrap();
existing.write("content").unwrap();
assert!(existing.try_exists().unwrap());
let missing = test_dir.strict_join("missing.txt").unwrap();
assert!(!missing.try_exists().unwrap());
let dir = test_dir.strict_join("subdir").unwrap();
dir.create_dir_all().unwrap();
assert!(dir.try_exists().unwrap());
}
#[test]
fn test_strict_path_touch_creates_file() {
let temp = tempfile::tempdir().unwrap();
let test_dir: PathBoundary = PathBoundary::try_new(temp.path()).unwrap();
let file = test_dir.strict_join("new_file.txt").unwrap();
assert!(!file.exists());
file.touch().unwrap();
assert!(file.exists());
assert!(file.is_file());
assert_eq!(file.read_to_string().unwrap(), "");
}
#[test]
fn test_strict_path_touch_updates_existing() {
let temp = tempfile::tempdir().unwrap();
let test_dir: PathBoundary = PathBoundary::try_new(temp.path()).unwrap();
let file = test_dir.strict_join("existing.txt").unwrap();
file.write("original content").unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
file.touch().unwrap();
assert_eq!(file.read_to_string().unwrap(), "original content")
}
#[test]
#[cfg(feature = "virtual-path")]
fn test_virtual_path_set_permissions() {
let temp = tempfile::tempdir().unwrap();
let vroot: VirtualRoot = VirtualRoot::try_new(temp.path()).unwrap();
let file = vroot.virtual_join("/test.txt").unwrap();
file.write("content").unwrap();
let mut perms = file.metadata().unwrap().permissions();
perms.set_readonly(true);
file.set_permissions(perms).unwrap();
assert!(file.metadata().unwrap().permissions().readonly());
}
#[test]
#[cfg(feature = "virtual-path")]
fn test_virtual_path_try_exists() {
let temp = tempfile::tempdir().unwrap();
let vroot: VirtualRoot = VirtualRoot::try_new(temp.path()).unwrap();
let existing = vroot.virtual_join("/exists.txt").unwrap();
existing.write("content").unwrap();
assert!(existing.try_exists().unwrap());
let missing = vroot.virtual_join("/missing.txt").unwrap();
assert!(!missing.try_exists().unwrap());
}
#[test]
#[cfg(feature = "virtual-path")]
fn test_virtual_path_touch() {
let temp = tempfile::tempdir().unwrap();
let vroot: VirtualRoot = VirtualRoot::try_new(temp.path()).unwrap();
let file = vroot.virtual_join("/touched.txt").unwrap();
assert!(!file.exists());
file.touch().unwrap();
assert!(file.exists());
assert_eq!(file.read_to_string().unwrap(), "");
}