use super::*;
fn make_config(allowed: Vec<&str>, denied: Vec<&str>) -> SafetyConfig {
SafetyConfig {
allowed_paths: allowed.into_iter().map(|s| s.to_string()).collect(),
denied_paths: denied.into_iter().map(|s| s.to_string()).collect(),
protected_branches: vec![],
require_confirmation: vec![],
strict_permissions: false,
permissions: vec![],
}
}
#[cfg(unix)]
#[test]
fn test_normalize_simple_absolute() {
let path = lexical_normalize_path(Path::new("/foo/bar/baz"));
assert_eq!(path, PathBuf::from("/foo/bar/baz"));
}
#[cfg(unix)]
#[test]
fn test_normalize_with_dot() {
let path = lexical_normalize_path(Path::new("/foo/./bar"));
assert_eq!(path, PathBuf::from("/foo/bar"));
}
#[cfg(unix)]
#[test]
fn test_normalize_with_dotdot() {
let path = lexical_normalize_path(Path::new("/foo/bar/../baz"));
assert_eq!(path, PathBuf::from("/foo/baz"));
}
#[cfg(unix)]
#[test]
fn test_normalize_multiple_dotdot() {
let path = lexical_normalize_path(Path::new("/foo/bar/baz/../../qux"));
assert_eq!(path, PathBuf::from("/foo/qux"));
}
#[cfg(unix)]
#[test]
fn test_normalize_dotdot_at_root() {
let path = lexical_normalize_path(Path::new("/foo/../.."));
assert_eq!(path, PathBuf::from(""));
}
#[test]
fn test_normalize_relative() {
let path = lexical_normalize_path(Path::new("foo/./bar/../baz"));
assert_eq!(path, PathBuf::from("foo/baz"));
}
#[test]
fn test_strip_unc_prefix_normal_path() {
assert_eq!(strip_unc_prefix("/foo/bar"), "/foo/bar");
}
#[test]
fn test_strip_unc_prefix_empty() {
assert_eq!(strip_unc_prefix(""), "");
}
#[test]
fn test_allowed_list_empty() {
let config = make_config(vec![], vec![]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
assert!(!validator
.is_path_in_allowed_list("/some/path", "/some/path")
.unwrap());
}
#[test]
fn test_allowed_list_absolute_glob() {
let config = make_config(vec!["/tmp/**"], vec![]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
assert!(validator
.is_path_in_allowed_list("/tmp/foo/bar", "/tmp/foo/bar")
.unwrap());
assert!(!validator
.is_path_in_allowed_list("/etc/passwd", "/etc/passwd")
.unwrap());
}
#[test]
fn test_allowed_list_relative_glob() {
let config = make_config(vec!["./**"], vec![]);
let cwd = std::env::current_dir().unwrap();
let cwd_str = cwd.to_string_lossy();
let validator = PathValidator::new(&config, cwd.clone());
let test_path = format!("{}/src/main.rs", cwd_str);
assert!(validator
.is_path_in_allowed_list(&test_path, "./src/main.rs")
.unwrap());
}
#[test]
fn test_validate_denied_env_file() {
let config = make_config(vec![], vec!["**/.env"]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
let result = validator.validate(".env");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("denied pattern"));
}
#[test]
fn test_validate_denied_ssh() {
let config = make_config(vec![], vec!["**/.ssh/**"]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
let result = validator.validate("/home/user/.ssh/id_rsa");
assert!(result.is_err());
}
#[test]
fn test_validate_allowed_path() {
let config = make_config(vec![], vec![]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd.clone());
let result = validator.validate("src/main.rs");
assert!(result.is_ok());
}
#[test]
fn test_validate_denied_secrets_dir() {
let config = make_config(vec![], vec!["**/secrets/**"]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
let result = validator.validate("config/secrets/api_key.txt");
assert!(result.is_err());
}
#[test]
fn test_validate_path_traversal_detected() {
let config = make_config(vec![], vec![]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
let result = validator.validate("../../../../etc/passwd");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Path traversal") || err_msg.contains("denied"),
"Expected traversal or denied error, got: {}",
err_msg
);
}
#[test]
fn test_validate_not_in_allowed_list() {
let config = make_config(vec!["/allowed/**"], vec![]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
let result = validator.validate("/not-allowed/file.txt");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("not in allowed list") || err.contains("Failed to canonicalize"),
"Expected not-in-allowed-list or canonicalization error, got: {}",
err
);
}
#[test]
fn test_validate_env_local_denied() {
let config = make_config(vec![], vec!["**/.env.local"]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
let result = validator.validate(".env.local");
assert!(result.is_err());
}
#[test]
fn test_validate_null_byte_rejected() {
let config = make_config(vec![], vec![]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
let result = validator.validate("safe_path\0/etc/passwd");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("null bytes"));
}
#[test]
fn test_validate_null_byte_at_end_rejected() {
let config = make_config(vec![], vec![]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
let result = validator.validate("some/file.txt\0");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("null bytes"));
}
#[test]
fn test_no_test_mode_bypass() {
unsafe {
std::env::set_var("SELFWARE_TEST_MODE", "1");
}
let config = make_config(vec![], vec!["**/.env"]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
let result = validator.validate(".env");
assert!(
result.is_err(),
"SELFWARE_TEST_MODE must not bypass path validation"
);
assert!(
result.unwrap_err().to_string().contains("denied pattern"),
"Expected 'denied pattern' error"
);
unsafe {
std::env::remove_var("SELFWARE_TEST_MODE");
}
}
#[test]
fn test_validate_rejects_fullwidth_dot() {
let config = make_config(vec![], vec![]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
let result = validator.validate("src\u{FF0E}./etc/passwd");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Unicode"));
}
#[test]
fn test_validate_rejects_fullwidth_slash() {
let config = make_config(vec![], vec![]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
let result = validator.validate(".\u{FF0F}..\u{FF0F}etc\u{FF0F}passwd");
assert!(result.is_err());
}
#[test]
fn test_validate_rejects_two_dot_leader() {
let config = make_config(vec![], vec![]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
let result = validator.validate("src\u{2025}\u{2025}/etc/passwd");
assert!(result.is_err());
}
#[test]
fn test_validate_rejects_suspicious_mix() {
let config = make_config(vec![], vec![]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
let result = validator.validate("foo./\u{FF0E}/etc/passwd");
assert!(result.is_err());
}
#[test]
fn test_validate_rejects_absolute_system_path() {
let config = make_config(vec![], vec![]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
let result = validator.validate("/etc/passwd");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("outside") || err.contains("traversal") || err.contains("system"),
"Expected outside/traversal/system error, got: {}",
err
);
}
#[test]
fn test_validate_rejects_absolute_root() {
let config = make_config(vec![], vec![]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
let result = validator.validate("/");
assert!(result.is_err());
}
#[test]
fn test_validate_rejects_etc_passwd() {
let config = make_config(vec!["./**"], vec![]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
let result = validator.validate("/etc/passwd");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("system") || err.contains("outside") || err.contains("allowed"),
"Expected security error, got: {}",
err
);
}
#[test]
fn test_validate_rejects_etc_shadow() {
let config = make_config(vec!["./**"], vec![]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
let result = validator.validate("/etc/shadow");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("system") || err.contains("outside") || err.contains("allowed"),
"Expected security error, got: {}",
err
);
}
#[test]
fn test_validate_rejects_proc_access() {
let config = make_config(vec!["./**"], vec![]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
let result = validator.validate("/proc/self/environ");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("system") || err.contains("outside") || err.contains("allowed"),
"Expected security error, got: {}",
err
);
}
#[test]
fn test_validate_rejects_ssh_directory() {
let config = make_config(vec![], vec!["**/.ssh/**"]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
let result = validator.validate("/home/user/.ssh/id_rsa");
assert!(result.is_err());
}
#[test]
fn test_validate_rejects_double_dot_variations() {
let config = make_config(vec![], vec![]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
let result = validator.validate("../../etc/passwd");
assert!(result.is_err(), "Classic path traversal should be blocked");
}
#[test]
fn test_validate_rejects_sibling_prefix_escape() {
let base = tempfile::tempdir().unwrap();
let proj = base.path().join("proj");
let evil = base.path().join("proj-evil");
std::fs::create_dir_all(&proj).unwrap();
std::fs::create_dir_all(&evil).unwrap();
let secret = evil.join("secret.txt");
std::fs::write(&secret, b"x").unwrap();
let config = make_config(vec!["./**"], vec![]);
let validator = PathValidator::new(&config, proj.clone());
let result = validator.validate(secret.to_str().unwrap());
assert!(
result.is_err(),
"sibling-prefix path must be rejected, got: {:?}",
result
);
let child = proj.join("ok.txt");
std::fs::write(&child, b"y").unwrap();
assert!(
validator.validate(child.to_str().unwrap()).is_ok(),
"real child of working dir must be allowed"
);
}
#[cfg(unix)]
#[test]
fn test_allow_list_globstar_pattern_canonicalizes_symlinked_parent() {
let real = tempfile::tempdir().unwrap();
let link_dir = real
.path()
.parent()
.unwrap()
.join(format!("sw-pv-link-{}", std::process::id()));
std::os::unix::fs::symlink(real.path(), &link_dir).unwrap();
let pattern = format!("{}/**", link_dir.display());
let config = make_config(vec![pattern.as_str()], vec![]);
let cwd = std::env::current_dir().unwrap();
let validator = PathValidator::new(&config, cwd);
let canonical_child = real.path().canonicalize().unwrap().join("file.txt");
let allowed = validator
.is_path_in_allowed_list(&canonical_child.to_string_lossy(), "ignored")
.unwrap();
let _ = std::fs::remove_dir_all(&link_dir);
assert!(
allowed,
"<symlink>/** must match a canonical child of the real dir"
);
}