#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::field_reassign_with_default
)]
mod security;
use exarch_core::ExtractionError;
use exarch_core::SecurityConfig;
use exarch_core::types::DestDir;
use exarch_core::types::SafePath;
use exarch_core::types::SafeSymlink;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
#[test]
fn test_full_safe_path_workflow() {
let temp = TempDir::new().unwrap();
let dest = DestDir::new(temp.path().to_path_buf()).unwrap();
let config = SecurityConfig::default();
let file_path = temp.path().join("test_file.txt");
fs::write(&file_path, "content").unwrap();
let safe = SafePath::validate(&PathBuf::from("test_file.txt"), &dest, &config).unwrap();
let final_path = dest.join(&safe);
assert!(final_path.exists());
assert_eq!(fs::read_to_string(&final_path).unwrap(), "content");
}
#[test]
fn test_dest_dir_join_safe_path() {
let temp = TempDir::new().unwrap();
let dest = DestDir::new(temp.path().to_path_buf()).unwrap();
let config = SecurityConfig::default();
let safe = SafePath::validate(&PathBuf::from("foo/bar.txt"), &dest, &config).unwrap();
let joined = dest.join(&safe);
assert!(joined.starts_with(dest.as_path()));
assert!(joined.ends_with("foo/bar.txt"));
}
#[test]
fn test_nested_directory_creation() {
let temp = TempDir::new().unwrap();
let dest = DestDir::new(temp.path().to_path_buf()).unwrap();
let config = SecurityConfig::default();
let safe = SafePath::validate(&PathBuf::from("a/b/c/d/file.txt"), &dest, &config).unwrap();
let final_path = dest.join(&safe);
if let Some(parent) = final_path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(&final_path, "nested content").unwrap();
assert!(final_path.exists());
}
#[test]
fn test_symlink_workflow() {
let temp = TempDir::new().unwrap();
let dest = DestDir::new(temp.path().to_path_buf()).unwrap();
let mut config = SecurityConfig::default();
config.allowed.symlinks = true;
let target_path = temp.path().join("target.txt");
fs::write(&target_path, "target content").unwrap();
let link = SafePath::validate(&PathBuf::from("link.txt"), &dest, &config).unwrap();
let target = PathBuf::from("target.txt");
let symlink = SafeSymlink::validate(&link, &target, &dest, &config).unwrap();
assert_eq!(symlink.link_path(), PathBuf::from("link.txt").as_path());
assert_eq!(symlink.target_path(), PathBuf::from("target.txt").as_path());
#[cfg(unix)]
{
let link_path = dest.join(&link);
std::os::unix::fs::symlink(&target, &link_path).unwrap();
assert!(link_path.exists());
}
}
#[test]
fn test_path_traversal_blocked() {
let temp = TempDir::new().unwrap();
let dest = DestDir::new(temp.path().to_path_buf()).unwrap();
let config = SecurityConfig::default();
let paths = vec![
"../etc/passwd",
"foo/../../etc/passwd",
"a/b/../../../etc/passwd",
];
for path in paths {
let result = SafePath::validate(&PathBuf::from(path), &dest, &config);
assert!(
matches!(result, Err(ExtractionError::PathTraversal { .. })),
"Path {path} should be rejected"
);
}
}
#[test]
fn test_banned_components_blocked() {
let temp = TempDir::new().unwrap();
let dest = DestDir::new(temp.path().to_path_buf()).unwrap();
let config = SecurityConfig::default();
let paths = vec![".git/config", "user/.ssh/id_rsa", "home/.gnupg/key"];
for path in paths {
let result = SafePath::validate(&PathBuf::from(path), &dest, &config);
assert!(
matches!(result, Err(ExtractionError::SecurityViolation { .. })),
"Path {path} should be rejected"
);
}
}
#[test]
fn test_multiple_files_same_directory() {
let temp = TempDir::new().unwrap();
let dest = DestDir::new(temp.path().to_path_buf()).unwrap();
let config = SecurityConfig::default();
let dir_path = temp.path().join("subdir");
fs::create_dir(&dir_path).unwrap();
let files = vec!["subdir/file1.txt", "subdir/file2.txt", "subdir/file3.txt"];
for file in &files {
let safe = SafePath::validate(&PathBuf::from(file), &dest, &config).unwrap();
let final_path = dest.join(&safe);
fs::write(&final_path, format!("content of {file}")).unwrap();
assert!(final_path.exists());
}
assert_eq!(fs::read_dir(&dir_path).unwrap().count(), 3);
}
#[test]
fn test_relative_symlink_resolution() {
let temp = TempDir::new().unwrap();
let dest = DestDir::new(temp.path().to_path_buf()).unwrap();
let mut config = SecurityConfig::default();
config.allowed.symlinks = true;
let a_dir = temp.path().join("a");
let b_dir = a_dir.join("b");
fs::create_dir_all(&b_dir).unwrap();
let target_path = b_dir.join("target.txt");
fs::write(&target_path, "target").unwrap();
let link = SafePath::validate(&PathBuf::from("a/link.txt"), &dest, &config).unwrap();
let target = PathBuf::from("b/target.txt");
let symlink = SafeSymlink::validate(&link, &target, &dest, &config).unwrap();
assert!(symlink.link_path().starts_with("a"));
assert!(symlink.target_path().starts_with("b"));
}
#[test]
fn test_depth_limit_enforced() {
let temp = TempDir::new().unwrap();
let dest = DestDir::new(temp.path().to_path_buf()).unwrap();
let mut config = SecurityConfig::default();
config.max_path_depth = 5;
let ok_path = "a/b/c/d/e";
let result = SafePath::validate(&PathBuf::from(ok_path), &dest, &config);
assert!(result.is_ok());
let bad_path = "a/b/c/d/e/f";
let result = SafePath::validate(&PathBuf::from(bad_path), &dest, &config);
assert!(matches!(
result,
Err(ExtractionError::SecurityViolation { .. })
));
}