use ggen_utils::path_validator::PathValidator;
use std::path::Path;
use tempfile::tempdir;
#[test]
fn test_basic_path_traversal() {
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path());
let attacks = vec![
"../../../etc/passwd",
"../../etc/passwd",
"../etc/passwd",
"subdir/../../etc/passwd",
"./../../etc/passwd",
];
for attack in attacks {
let result = validator.validate(attack);
assert!(result.is_err(), "Should block path traversal: {}", attack);
assert!(
result.unwrap_err().to_string().contains("traversal"),
"Error should mention path traversal"
);
}
}
#[test]
fn test_encoded_path_traversal() {
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path());
let attacks = vec![
"..%2F..%2F..%2Fetc%2Fpasswd", "..\\..\\..\\etc\\passwd", ];
for attack in attacks {
let result = validator.validate(attack);
if let Ok(safe_path) = result {
assert!(
safe_path.absolute().starts_with(workspace.path()),
"Path should be within workspace"
);
}
}
}
#[test]
fn test_double_encoded_traversal() {
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path());
let attacks = vec![
"%252e%252e%252f%252e%252e%252fetc%252fpasswd", ];
for attack in attacks {
let result = validator.validate(attack);
if let Ok(safe_path) = result {
assert!(safe_path.absolute().starts_with(workspace.path()));
}
}
}
#[test]
fn test_null_byte_injection() {
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path());
let attacks = vec![
"file.txt\0.evil",
"safe\0../../etc/passwd",
"\0",
"dir/\0/file.txt",
];
for attack in attacks {
let result = validator.validate(attack);
assert!(result.is_err(), "Should block null byte: {}", attack);
assert!(
result.unwrap_err().to_string().contains("null byte"),
"Error should mention null byte"
);
}
}
#[test]
fn test_absolute_paths_blocked_by_default() {
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path());
let absolute_paths = vec!["/etc/passwd", "/tmp/evil", "/var/log/secrets"];
for path in absolute_paths {
let result = validator.validate(path);
assert!(result.is_err(), "Should block absolute path: {}", path);
assert!(
result.unwrap_err().to_string().contains("Absolute"),
"Error should mention absolute path"
);
}
}
#[test]
fn test_absolute_paths_within_workspace_allowed() {
let workspace = tempdir().expect("Failed to create temp dir");
let test_file = workspace.path().join("test.txt");
std::fs::write(&test_file, "content").expect("Failed to create test file");
let validator = PathValidator::new(workspace.path()).with_absolute_paths(true);
let result = validator.validate(&test_file);
assert!(
result.is_ok(),
"Should allow absolute path within workspace"
);
}
#[test]
fn test_absolute_paths_outside_workspace_blocked() {
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path()).with_absolute_paths(true);
let result = validator.validate("/etc/passwd");
assert!(
result.is_err(),
"Should block absolute path outside workspace"
);
assert!(
result.unwrap_err().to_string().contains("workspace"),
"Error should mention workspace escape"
);
}
#[test]
#[cfg(unix)]
fn test_symlink_pointing_outside_workspace_blocked() {
use std::os::unix::fs::symlink;
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path());
let link_path = workspace.path().join("evil_link");
symlink("/etc/passwd", &link_path).expect("Failed to create symlink");
let result = validator.validate("evil_link");
assert!(result.is_err(), "Should block symlink escape");
}
#[test]
#[cfg(unix)]
fn test_symlink_within_workspace_allowed() {
use std::os::unix::fs::symlink;
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path());
let target = workspace.path().join("target.txt");
std::fs::write(&target, "content").expect("Failed to create target");
let link_path = workspace.path().join("link.txt");
symlink(&target, &link_path).expect("Failed to create symlink");
let result = validator.validate("link.txt");
assert!(result.is_ok(), "Should allow symlink within workspace");
}
#[test]
#[cfg(unix)]
fn test_symlink_chain_attack() {
use std::os::unix::fs::symlink;
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path());
let link1 = workspace.path().join("link1");
let link2 = workspace.path().join("link2");
symlink("/etc/passwd", &link2).expect("Failed to create link2");
symlink(&link2, &link1).expect("Failed to create link1");
let result = validator.validate("link1");
assert!(result.is_err(), "Should block symlink chain escape");
}
#[test]
fn test_extension_whitelist_enforced() {
let workspace = tempdir().expect("Failed to create temp dir");
let validator =
PathValidator::new(workspace.path()).with_allowed_extensions(vec!["tmpl", "tera", "ttl"]);
let allowed = vec!["template.tmpl", "example.tera", "ontology.ttl"];
for path in allowed {
let result = validator.validate(path);
assert!(result.is_ok(), "Should allow extension: {}", path);
}
let blocked = vec!["script.sh", "binary.exe", "config.yaml"];
for path in blocked {
let result = validator.validate(path);
assert!(result.is_err(), "Should block extension: {}", path);
assert!(
result.unwrap_err().to_string().contains("extension"),
"Error should mention extension"
);
}
}
#[test]
fn test_extension_case_sensitivity() {
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path()).with_allowed_extensions(vec!["tera"]);
let result = validator.validate("template.TERA");
assert!(
result.is_err(),
"Extension validation should be case-sensitive"
);
}
#[test]
fn test_double_extension_handling() {
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path()).with_allowed_extensions(vec!["tera"]);
let result = validator.validate("archive.tar.tera");
assert!(result.is_ok(), "Should check only the last extension");
}
#[test]
fn test_depth_limit_enforced() {
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path()).with_max_depth(3);
let shallow = "a/b/c.txt";
assert!(
validator.validate(shallow).is_ok(),
"Should allow path within depth limit"
);
let deep = "a/b/c/d/e.txt";
let result = validator.validate(deep);
assert!(result.is_err(), "Should block path exceeding depth");
assert!(
result.unwrap_err().to_string().contains("depth"),
"Error should mention depth"
);
}
#[test]
fn test_unicode_path_allowed() {
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path());
let unicode_paths = vec![
"文件.txt", "файл.txt", "ファイル.txt", "αρχείο.txt", "📁/file.txt", ];
for path in unicode_paths {
let result = validator.validate(path);
assert!(result.is_ok(), "Should allow Unicode path: {}", path);
}
}
#[test]
fn test_mixed_unicode_and_ascii() {
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path());
let mixed = "templates/例え_example_文件.tera";
let result = validator.validate(mixed);
assert!(result.is_ok(), "Should allow mixed Unicode/ASCII");
}
#[test]
fn test_empty_path_blocked() {
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path());
let result = validator.validate("");
assert!(result.is_err(), "Should block empty path");
assert!(
result.unwrap_err().to_string().contains("empty"),
"Error should mention empty path"
);
}
#[test]
fn test_current_directory_reference() {
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path());
let paths = vec!["./file.txt", "./dir/./file.txt", "././file.txt"];
for path in paths {
let result = validator.validate(path);
assert!(
result.is_ok(),
"Should allow current dir reference: {}",
path
);
}
}
#[test]
fn test_trailing_slashes() {
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path());
let paths = vec!["dir/", "dir/file.txt/"];
for path in paths {
let result = validator.validate(path);
let _ = result;
}
}
#[test]
fn test_very_long_path() {
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path());
let long_component = "a".repeat(255); let long_path = format!("{}/file.txt", long_component);
let result = validator.validate(&long_path);
let _ = result;
}
#[test]
fn test_special_characters_in_filename() {
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path());
let special_paths = vec![
"file-name.txt",
"file_name.txt",
"file.name.txt",
"file (1).txt",
"file@2024.txt",
];
for path in special_paths {
let result = validator.validate(path);
assert!(result.is_ok(), "Should allow special char path: {}", path);
}
}
#[test]
fn test_batch_validation_all_valid() {
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path());
let paths = vec!["file1.txt", "file2.txt", "dir/file3.txt"];
let result = validator.validate_batch(&paths);
assert!(result.is_ok());
let safe_paths = result.expect("All should validate");
assert_eq!(safe_paths.len(), 3);
}
#[test]
fn test_batch_validation_with_invalid() {
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path());
let paths = vec!["file1.txt", "../../../etc/passwd", "file3.txt"];
let result = validator.validate_batch(&paths);
assert!(result.is_err());
}
#[test]
fn test_safe_path_accessors() {
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path());
let safe_path = validator
.validate("templates/example.tera")
.expect("Should validate");
assert_eq!(safe_path.extension(), Some("tera"));
assert_eq!(safe_path.file_name(), Some("example.tera"));
assert_eq!(safe_path.as_path(), Path::new("templates/example.tera"));
assert!(safe_path.absolute().is_absolute());
}
#[test]
fn test_safe_path_as_ref() {
let workspace = tempdir().expect("Failed to create temp dir");
let validator = PathValidator::new(workspace.path());
let safe_path = validator.validate("file.txt").expect("Should validate");
fn take_path_ref<P: AsRef<Path>>(p: P) -> bool {
p.as_ref().to_str().is_some()
}
assert!(take_path_ref(&safe_path));
}