use magellan::validation::{
has_suspicious_traversal, is_safe_symlink, validate_path_within_root, PathValidationError,
};
use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn create_test_file(dir: &Path, name: &str) -> std::path::PathBuf {
let path = dir.join(name);
fs::write(&path, b"test content").unwrap();
path
}
#[cfg(any(unix, windows))]
fn create_symlink(from: &Path, to: &Path) {
#[cfg(unix)]
std::os::unix::fs::symlink(to, from).unwrap();
#[cfg(windows)]
{
if to.is_dir() {
std::os::windows::fs::symlink_dir(to, from).unwrap();
} else {
std::os::windows::fs::symlink_file(to, from).unwrap();
}
}
}
#[test]
fn test_single_parent_traversal_rejected() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let traversal = root.join("../etc");
let result = validate_path_within_root(&traversal, root);
assert!(result.is_err());
match result {
Err(PathValidationError::SuspiciousTraversal(_)) => {}
Err(PathValidationError::CannotCanonicalize(_)) => {}
e => panic!(
"Expected SuspiciousTraversal or CannotCanonicalize, got {:?}",
e
),
}
}
#[test]
fn test_double_parent_traversal_rejected() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let traversal = root.join("../../etc");
let result = validate_path_within_root(&traversal, root);
match result {
Err(PathValidationError::SuspiciousTraversal(_)) => {}
Err(PathValidationError::OutsideRoot(_, _)) => {}
Err(PathValidationError::CannotCanonicalize(_)) => {}
Ok(_) => panic!("Path should have been rejected"),
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
#[test]
fn test_multiple_parent_traversal_rejected() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let traversal = root.join("../../../etc/passwd");
let result = validate_path_within_root(&traversal, root);
assert!(result.is_err());
match &result {
Err(PathValidationError::SuspiciousTraversal(_)) => {}
e => panic!(
"Expected SuspiciousTraversal error for >=3 parents, got {:?}",
e
),
}
}
#[test]
fn test_legitimate_nested_parent_accepted() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let deep = root.join("a").join("b").join("c");
fs::create_dir_all(&deep).unwrap();
let file = root.join("target").join("file.rs");
fs::create_dir_all(root.join("target")).unwrap();
fs::write(&file, b"fn test() {}").unwrap();
let resolved = deep.join("../../target/file.rs");
let result = validate_path_within_root(&resolved, root);
match result {
Ok(canonical) => {
assert!(canonical.starts_with(root));
}
Err(PathValidationError::CannotCanonicalize(_)) => {
}
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
#[test]
fn test_legitimate_two_parent_deep_path_accepted() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let subdir = root.join("project").join("src").join("module");
fs::create_dir_all(&subdir).unwrap();
let target = root.join("project").join("lib.rs");
fs::write(&target, b"fn lib() {}").unwrap();
let resolved = subdir.join("../../lib.rs");
let result = validate_path_within_root(&resolved, root);
match result {
Ok(_) => {} Err(PathValidationError::CannotCanonicalize(_)) => {} Err(e) => panic!("Unexpected error: {:?}", e),
}
}
#[test]
fn test_forward_slash_paths() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let file = create_test_file(root, "test.rs");
let result = validate_path_within_root(&file, root);
assert!(result.is_ok());
}
#[test]
#[cfg(windows)]
fn test_backslash_paths_windows() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let file_path = root.join("subdir").join("test.rs");
fs::create_dir_all(root.join("subdir")).unwrap();
fs::write(&file_path, b"fn test() {}").unwrap();
let path_str = file_path.to_string_lossy();
assert!(path_str.contains('\\'));
let result = validate_path_within_root(&file_path, root);
assert!(result.is_ok());
}
#[test]
#[cfg(windows)]
fn test_windows_backslash_traversal_rejected() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let mut traversal = root.clone();
traversal.push("..");
traversal.push("..");
traversal.push("..");
traversal.push("windows");
traversal.push("system32");
let result = validate_path_within_root(&traversal, root);
assert!(result.is_err());
}
#[test]
#[cfg(windows)]
fn test_windows_unc_path_rejected() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let unc_path = Path::new(r"\\?\C:\Windows\System32");
let result = validate_path_within_root(unc_path, root);
assert!(result.is_err());
}
#[test]
#[cfg(unix)]
fn test_unix_absolute_path_rejected() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let abs_path = Path::new("/etc/passwd");
let result = validate_path_within_root(abs_path, root);
assert!(result.is_err());
}
#[test]
fn test_case_sensitive_path_validation() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let file = create_test_file(root, "TestFile.rs");
let result = validate_path_within_root(&file, root);
assert!(result.is_ok());
#[cfg(any(target_os = "macos", windows))]
{
let different_case = root.join("testfile.rs");
let _ = validate_path_within_root(&different_case, root);
}
}
#[test]
#[cfg(any(unix, windows))]
fn test_safe_symlink_inside_root_accepted() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let target = create_test_file(root, "target.rs");
let symlink = root.join("link.rs");
create_symlink(&symlink, &target);
let result = is_safe_symlink(&symlink, root);
assert!(result.is_ok() || matches!(result, Err(PathValidationError::CannotCanonicalize(_))));
}
#[test]
#[cfg(any(unix, windows))]
fn test_symlink_outside_root_rejected() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let outside_dir = TempDir::new().unwrap();
let target = create_test_file(outside_dir.path(), "outside.rs");
let symlink = root.join("link.rs");
create_symlink(&symlink, &target);
let result = is_safe_symlink(&symlink, root);
assert!(result.is_err());
match result.unwrap_err() {
PathValidationError::SymlinkEscape(_, _) => {}
PathValidationError::OutsideRoot(_, _) => {}
_ => panic!("Expected SymlinkEscape or OutsideRoot error"),
}
}
#[test]
#[cfg(any(unix, windows))]
fn test_symlink_chain_outside_root_rejected() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let outside_dir = TempDir::new().unwrap();
let target = create_test_file(outside_dir.path(), "target.rs");
let link2 = root.join("link2.rs");
create_symlink(&link2, &target);
let link1 = root.join("link1.rs");
create_symlink(&link1, &link2);
let result = is_safe_symlink(&link1, root);
assert!(result.is_err());
}
#[test]
#[cfg(any(unix, windows))]
fn test_broken_symlink_handled() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let symlink = root.join("broken.rs");
create_symlink(&symlink, Path::new("nonexistent.rs"));
let result = is_safe_symlink(&symlink, root);
assert!(matches!(
result,
Err(PathValidationError::CannotCanonicalize(_))
));
}
#[test]
fn test_mixed_dotdot_pattern_rejected() {
assert!(has_suspicious_traversal("./subdir/../../etc"));
}
#[test]
fn test_mixed_slash_pattern_rejected() {
#[cfg(windows)]
assert!(has_suspicious_traversal(".\\subdir\\..\\..\\etc"));
}
#[test]
fn test_normal_paths_not_flagged() {
assert!(!has_suspicious_traversal("src/main.rs"));
assert!(!has_suspicious_traversal("./src/lib.rs"));
assert!(!has_suspicious_traversal("../../normal/path")); assert!(!has_suspicious_traversal("../parent/deep/nested/path")); }
#[test]
fn test_empty_path_components() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let _ = create_test_file(root, "test.rs");
#[cfg(unix)]
{
let double_slash = Path::new(root.to_str().unwrap()).join("//test.rs");
let result = validate_path_within_root(&double_slash, root);
assert!(result.is_ok() || result.is_err());
}
}
#[test]
fn test_relative_from_root() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let _ = create_test_file(root, "test.rs");
let relative = Path::new("test.rs");
let full = root.join(relative);
let result = validate_path_within_root(&full, root);
assert!(result.is_ok());
}
#[test]
fn test_dot_in_path() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let subdir = root.join("sub.dir");
fs::create_dir(&subdir).unwrap();
let file = subdir.join("test.rs");
fs::write(&file, b"fn test() {}").unwrap();
let result = validate_path_within_root(&file, root);
assert!(result.is_ok());
}
#[test]
fn test_deep_nesting_accepted() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let deep = root.join("a").join("b").join("c").join("d").join("e");
fs::create_dir_all(&deep).unwrap();
let file = deep.join("deep.rs");
fs::write(&file, b"fn deep() {}").unwrap();
let result = validate_path_within_root(&file, root);
assert!(result.is_ok());
assert!(result.unwrap().starts_with(root));
}
#[test]
fn test_nonexistent_file_in_root() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let nonexistent = root.join("nonexistent.rs");
let result = validate_path_within_root(&nonexistent, root);
assert!(matches!(
result,
Err(PathValidationError::CannotCanonicalize(_))
));
}
#[test]
fn test_path_separator_normalization() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let subdir = root.join("src").join("main");
fs::create_dir_all(&subdir).unwrap();
let file = subdir.join("lib.rs");
fs::write(&file, b"fn lib() {}").unwrap();
let result = validate_path_within_root(&file, root);
assert!(result.is_ok());
let canonical = result.unwrap();
assert!(canonical.starts_with(root));
}
#[test]
fn test_traversal_detection_is_platform_agnostic() {
assert!(has_suspicious_traversal("../../../etc/passwd"));
assert!(has_suspicious_traversal("../etc"));
assert!(has_suspicious_traversal("..\\..\\..\\windows\\system32"));
assert!(has_suspicious_traversal("./subdir/../../etc"));
assert!(!has_suspicious_traversal("src/main.rs"));
assert!(!has_suspicious_traversal("./src/lib.rs"));
}
#[test]
fn test_validate_path_rejects_absolute_outside() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
#[cfg(unix)]
let abs_path = Path::new("/etc/passwd");
#[cfg(windows)]
let abs_path = Path::new("C:\\Windows\\System32\\config\\SAM");
let result = validate_path_within_root(abs_path, root);
assert!(result.is_err());
}
#[test]
fn test_symlink_relative_inside_root() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
#[cfg(any(unix, windows))]
{
let target_dir = root.join("target");
fs::create_dir(&target_dir).unwrap();
let target_file = target_dir.join("file.rs");
fs::write(&target_file, b"fn file() {}").unwrap();
let link_dir = root.join("links");
fs::create_dir(&link_dir).unwrap();
let symlink = link_dir.join("link.rs");
create_symlink(&symlink, Path::new("../target/file.rs"));
let result = is_safe_symlink(&symlink, root);
assert!(result.is_ok());
}
}