use std::path::Path;
const SECRET_EXTENSIONS: &[&str] = &[
".env",
".pem",
".key",
".p12",
".pfx",
".jks",
".keystore",
".secret",
];
const SECRET_PREFIXES: &[&str] = &["credentials", ".env", "id_rsa", "id_ed25519", "token"];
const SECRET_SUBSTRINGS: &[&str] = &["api_key", "apikey", "_secret.", "_token."];
pub fn validate_path(path: &Path, root: &Path) -> bool {
match (path.canonicalize(), root.canonicalize()) {
(Ok(resolved), Ok(root_resolved)) => resolved.starts_with(&root_resolved),
_ => false,
}
}
pub fn is_symlink_safe(path: &Path, root: &Path) -> bool {
if !path.is_symlink() {
return true;
}
validate_path(path, root)
}
pub fn is_binary(path: &Path) -> bool {
use std::io::Read;
let mut file = match std::fs::File::open(path) {
Ok(f) => f,
Err(_) => return true,
};
let mut buf = [0u8; 8192];
let n = match file.read(&mut buf) {
Ok(n) => n,
Err(_) => return true,
};
buf[..n].contains(&0)
}
pub fn should_exclude(path: &Path, patterns: &[String]) -> bool {
for pattern in patterns {
for component in path.components() {
let name = component.as_os_str().to_string_lossy();
if glob_match(pattern, &name) {
return true;
}
}
}
false
}
pub fn has_secret_extension(path: &Path) -> bool {
let name = path
.file_name()
.map(|n| n.to_string_lossy().to_lowercase())
.unwrap_or_default();
let suffix = path
.extension()
.map(|e| format!(".{}", e.to_string_lossy().to_lowercase()))
.unwrap_or_default();
if SECRET_EXTENSIONS.contains(&suffix.as_str()) {
return true;
}
for prefix in SECRET_PREFIXES {
if name.starts_with(prefix) {
return true;
}
}
for substring in SECRET_SUBSTRINGS {
if name.contains(substring) {
return true;
}
}
false
}
pub fn glob_match(pattern: &str, text: &str) -> bool {
let pc: Vec<char> = pattern.chars().collect();
let tc: Vec<char> = text.chars().collect();
glob_inner(&pc, &tc)
}
fn glob_inner(pattern: &[char], text: &[char]) -> bool {
if pattern.is_empty() {
return text.is_empty();
}
if pattern[0] == '*' {
for i in 0..=text.len() {
if glob_inner(&pattern[1..], &text[i..]) {
return true;
}
}
return false;
}
if text.is_empty() {
return false;
}
if pattern[0] == '?' || pattern[0] == text[0] {
return glob_inner(&pattern[1..], &text[1..]);
}
false
}