use magellan::validation::is_safe_symlink;
use magellan::validation::PathValidationError;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
#[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]
#[cfg(any(unix, windows))]
fn test_symlink_to_file_inside_root() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let target = root.join("target.rs");
fs::write(&target, b"fn target() {}").unwrap();
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_to_file_outside_root() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let outside_dir = TempDir::new().unwrap();
let target = outside_dir.path().join("outside.rs");
fs::write(&target, b"fn outside() {}").unwrap();
let symlink = root.join("link.rs");
create_symlink(&symlink, &target);
let result = is_safe_symlink(&symlink, root);
assert!(result.is_err());
match &result {
Err(PathValidationError::SymlinkEscape(_, _)) => {}
Err(PathValidationError::OutsideRoot(_, _)) => {}
e => panic!("Expected SymlinkEscape or OutsideRoot, got {:?}", e),
}
}
#[test]
#[cfg(any(unix, windows))]
fn test_symlink_to_directory_inside_root() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let target_dir = root.join("target_dir");
fs::create_dir(&target_dir).unwrap();
let target_file = target_dir.join("file.rs");
fs::write(&target_file, b"fn file() {}").unwrap();
let symlink = root.join("link_dir");
create_symlink(&symlink, &target_dir);
let result = is_safe_symlink(&symlink, root);
assert!(result.is_ok() || matches!(result, Err(PathValidationError::CannotCanonicalize(_))));
}
#[test]
#[cfg(any(unix, windows))]
fn test_symlink_to_directory_outside_root() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let outside_dir = TempDir::new().unwrap();
let target = outside_dir.path();
let symlink = root.join("link_dir");
create_symlink(&symlink, target);
let result = is_safe_symlink(&symlink, root);
assert!(result.is_err());
match &result {
Err(PathValidationError::SymlinkEscape(_, _)) => {}
Err(PathValidationError::OutsideRoot(_, _)) => {}
e => panic!("Expected SymlinkEscape or OutsideRoot, got {:?}", e),
}
}
#[test]
#[cfg(any(unix, windows))]
fn test_relative_symlink_inside_root() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let target = root.join("target.rs");
fs::write(&target, b"fn target() {}").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.rs"));
let result = is_safe_symlink(&symlink, root);
assert!(result.is_ok() || matches!(result, Err(PathValidationError::CannotCanonicalize(_))));
}
#[test]
#[cfg(any(unix, windows))]
fn test_relative_symlink_outside_root() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let outside_dir = TempDir::new().unwrap();
let target = outside_dir.path().join("outside.rs");
fs::write(&target, b"fn outside() {}").unwrap();
let link_dir = root.join("links");
fs::create_dir(&link_dir).unwrap();
let symlink = link_dir.join("link.rs");
create_symlink(&symlink, &target);
let result = is_safe_symlink(&symlink, root);
assert!(result.is_err());
}
#[test]
#[cfg(any(unix, windows))]
fn test_symlink_chain_inside_root() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let target = root.join("target.rs");
fs::write(&target, b"fn target() {}").unwrap();
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_ok() || matches!(result, Err(PathValidationError::CannotCanonicalize(_))));
}
#[test]
#[cfg(any(unix, windows))]
fn test_symlink_chain_outside_root() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let outside_dir = TempDir::new().unwrap();
let target = outside_dir.path().join("target.rs");
fs::write(&target, b"fn target() {}").unwrap();
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());
match &result {
Err(PathValidationError::SymlinkEscape(_, _)) => {}
Err(PathValidationError::OutsideRoot(_, _)) => {}
e => panic!("Expected SymlinkEscape or OutsideRoot, got {:?}", e),
}
}
#[test]
#[cfg(any(unix, windows))]
fn test_broken_symlink() {
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]
#[cfg(any(unix, windows))]
fn test_symlink_to_parent_directory() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let parent = root.parent().unwrap();
let symlink = root.join("parent_link");
create_symlink(&symlink, parent);
let result = is_safe_symlink(&symlink, root);
assert!(result.is_err());
}
#[test]
#[cfg(any(unix, windows))]
fn test_symlink_to_sibling_directory() {
let temp_dir1 = TempDir::new().unwrap();
let temp_dir2 = TempDir::new().unwrap();
let symlink = temp_dir1.path().join("sibling_link");
create_symlink(&symlink, temp_dir2.path());
let result = is_safe_symlink(&symlink, temp_dir1.path());
assert!(result.is_err());
}
#[test]
#[cfg(any(unix, windows))]
fn test_symlink_to_nested_inside_root() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let nested = root.join("a").join("b").join("c");
fs::create_dir_all(&nested).unwrap();
let target = nested.join("target.rs");
fs::write(&target, b"fn target() {}").unwrap();
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_from_nested_to_outside() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let nested = root.join("nested");
fs::create_dir(&nested).unwrap();
let outside_dir = TempDir::new().unwrap();
let target = outside_dir.path().join("outside.rs");
fs::write(&target, b"fn outside() {}").unwrap();
let symlink = nested.join("link.rs");
create_symlink(&symlink, &target);
let result = is_safe_symlink(&symlink, root);
assert!(result.is_err());
}
#[test]
#[cfg(all(unix, not(target_os = "macos")))]
fn test_symlink_case_sensitive() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let target = root.join("Target.rs");
fs::write(&target, b"fn target() {}").unwrap();
let symlink = root.join("link.rs");
create_symlink(&symlink, Path::new("Target.rs"));
let result = is_safe_symlink(&symlink, root);
assert!(result.is_ok());
}
#[test]
#[cfg(any(unix, windows))]
fn test_symlink_dotdot_path() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let parent = root.parent().unwrap();
let target = parent.join("parent_file.rs");
fs::write(&target, b"fn parent() {}").unwrap();
let symlink = root.join("link.rs");
create_symlink(&symlink, Path::new("../parent_file.rs"));
let result = is_safe_symlink(&symlink, root);
assert!(result.is_err());
}
#[test]
#[cfg(any(unix, windows))]
fn test_symlink_self_referential() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let symlink = root.join("self.rs");
create_symlink(&symlink, Path::new("self.rs"));
let result = is_safe_symlink(&symlink, root);
match result {
Err(PathValidationError::CannotCanonicalize(_)) => {}
Err(PathValidationError::OutsideRoot(_, _)) => {
}
Ok(_) => {
}
e => panic!("Unexpected result: {:?}", e),
}
}
#[test]
#[cfg(any(unix, windows))]
fn test_symlink_to_symlink_that_escapes() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let outside_dir = TempDir::new().unwrap();
let target = outside_dir.path().join("outside.rs");
fs::write(&target, b"fn outside() {}").unwrap();
let intermediate = root.join("intermediate.rs");
create_symlink(&intermediate, &target);
let link = root.join("link.rs");
create_symlink(&link, &intermediate);
let result1 = is_safe_symlink(&intermediate, root);
assert!(result1.is_err());
let result2 = is_safe_symlink(&link, root);
assert!(result2.is_err());
}