use glob::Pattern;
use crate::error::PackError;
pub const DEFAULT_CACHE_DIRS: &[&str] = &[
"target",
"node_modules",
".venv",
"venv",
"__pycache__",
".pytest_cache",
".mypy_cache",
".ruff_cache",
".turbo",
".next",
".nuxt",
".parcel-cache",
".gradle",
];
pub const DEFAULT_SECRET_GLOBS: &[&str] = &[
".env",
".env.*",
".netrc",
".npmrc",
".pypirc",
".dockercfg",
".pgpass",
".my.cnf",
".htpasswd",
"credentials",
"credentials.toml",
"secret.toml",
"secrets.toml",
"secret.yaml",
"secrets.yaml",
"secret.yml",
"secrets.yml",
"secret.json",
"secrets.json",
"service-account*.json",
"terraform.tfvars",
"*.auto.tfvars",
"kubeconfig",
"id_rsa",
"id_dsa",
"id_ecdsa",
"id_ed25519",
"*.pem",
"*.key",
"*.p12",
"*.pfx",
"*.jks",
"*.keystore",
"*.p8",
"*.ppk",
"*.asc",
"*.gpg",
];
pub const DEFAULT_KEEP: &[&str] = &[
".env.example",
".env.sample",
".env.template",
".env.dist",
".env.defaults",
];
#[derive(Debug, Clone, Default)]
pub struct RuleOverrides {
pub secret_globs: Vec<String>,
pub cache_dirs: Vec<String>,
pub keep: Vec<String>,
}
impl RuleOverrides {
pub fn is_empty(&self) -> bool {
self.secret_globs.is_empty() && self.cache_dirs.is_empty() && self.keep.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct PackRules {
secret: Vec<Pattern>,
keep: Vec<Pattern>,
cache_dirs: Vec<String>,
pub custom_secret_count: usize,
pub custom_keep_count: usize,
pub custom_cache_count: usize,
}
impl Default for PackRules {
fn default() -> Self {
Self::new(&RuleOverrides::default()).expect("built-in globs must compile")
}
}
impl PackRules {
pub fn new(overrides: &RuleOverrides) -> Result<Self, PackError> {
let mut secret = compile_builtin(DEFAULT_SECRET_GLOBS);
for raw in &overrides.secret_globs {
secret.push(compile_custom(raw)?);
}
let mut keep = compile_builtin(DEFAULT_KEEP);
for raw in &overrides.keep {
keep.push(compile_custom(raw)?);
}
let mut cache_dirs: Vec<String> = DEFAULT_CACHE_DIRS
.iter()
.map(|s| (*s).to_string())
.collect();
cache_dirs.extend(overrides.cache_dirs.iter().cloned());
Ok(Self {
secret,
keep,
cache_dirs,
custom_secret_count: overrides.secret_globs.len(),
custom_keep_count: overrides.keep.len(),
custom_cache_count: overrides.cache_dirs.len(),
})
}
pub fn is_cache_dir(&self, name: &str) -> bool {
if self.is_kept(name) {
return false;
}
self.cache_dirs.iter().any(|d| d == name)
}
pub fn secret_reason(&self, name: &str) -> Option<String> {
if self.is_kept(name) {
return None;
}
let matched = self.secret.iter().find(|p| p.matches(name))?;
Some(format!("secret pattern: {}", matched.as_str()))
}
fn is_kept(&self, name: &str) -> bool {
self.keep.iter().any(|p| p.matches(name))
}
pub fn secret_pattern_count(&self) -> usize {
self.secret.len()
}
pub fn is_customized(&self) -> bool {
self.custom_secret_count + self.custom_keep_count + self.custom_cache_count > 0
}
}
fn compile_builtin(raw: &[&str]) -> Vec<Pattern> {
raw.iter()
.filter_map(|p| Pattern::new(p).ok())
.collect::<Vec<_>>()
}
fn compile_custom(raw: &str) -> Result<Pattern, PackError> {
Pattern::new(raw).map_err(|e| PackError::BadPattern {
pattern: raw.to_string(),
message: e.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_all_builtin_globs_compile() {
for raw in DEFAULT_SECRET_GLOBS.iter().chain(DEFAULT_KEEP.iter()) {
assert!(
Pattern::new(raw).is_ok(),
"built-in glob is malformed: {raw}"
);
}
let rules = PackRules::new(&RuleOverrides::default()).expect("defaults compile");
assert_eq!(rules.secret_pattern_count(), DEFAULT_SECRET_GLOBS.len());
}
#[test]
fn test_builtin_covers_common_secret_names() {
let r = PackRules::default();
for name in [
".env",
".env.production",
"secret.toml",
"secrets.yaml",
"secrets.json",
"credentials.toml",
"terraform.tfvars",
"prod.auto.tfvars",
"service-account-prod.json",
"kubeconfig",
".pgpass",
".pypirc",
"id_ed25519",
"server.pem",
"signing.p8",
"putty.ppk",
"key.asc",
] {
assert!(
r.secret_reason(name).is_some(),
"{name} should be treated as a secret"
);
}
}
#[test]
fn test_builtin_passes_ordinary_files() {
let r = PackRules::default();
for name in [
"main.rs",
"README.md",
".mcp.json",
"Cargo.toml",
"id_rsa.pub",
] {
assert!(r.secret_reason(name).is_none(), "{name} must travel");
}
}
#[test]
fn test_builtin_keep_rescues_templates() {
let r = PackRules::default();
for name in [".env.example", ".env.sample", ".env.template", ".env.dist"] {
assert!(r.secret_reason(name).is_none(), "{name} is a template");
}
assert!(r.secret_reason(".env.local").is_some());
}
#[test]
fn test_builtin_cache_dirs() {
let r = PackRules::default();
assert!(r.is_cache_dir("target"));
assert!(r.is_cache_dir("node_modules"));
assert!(!r.is_cache_dir("src"));
assert!(!r.is_cache_dir("dist"), "dist is source in many projects");
}
#[test]
fn test_custom_secret_glob_adds_without_replacing() {
let r = PackRules::new(&RuleOverrides {
secret_globs: vec!["my-app-keys.json".to_string(), "*.vault".to_string()],
..Default::default()
})
.expect("compile");
assert!(r.secret_reason("my-app-keys.json").is_some());
assert!(r.secret_reason("prod.vault").is_some());
assert!(r.secret_reason(".env").is_some());
assert!(r.secret_reason("secret.toml").is_some());
assert_eq!(r.custom_secret_count, 2);
assert!(r.is_customized());
}
#[test]
fn test_keep_overrides_builtin_secret() {
let r = PackRules::new(&RuleOverrides {
keep: vec![".npmrc".to_string()],
..Default::default()
})
.expect("compile");
assert!(
r.secret_reason(".npmrc").is_none(),
"keep must override the built-in secret rule"
);
assert!(r.secret_reason(".netrc").is_some(), "siblings unaffected");
}
#[test]
fn test_keep_overrides_cache_dir() {
let r = PackRules::new(&RuleOverrides {
keep: vec!["target".to_string()],
..Default::default()
})
.expect("compile");
assert!(!r.is_cache_dir("target"));
}
#[test]
fn test_custom_cache_dir() {
let r = PackRules::new(&RuleOverrides {
cache_dirs: vec!["dist".to_string(), "build".to_string()],
..Default::default()
})
.expect("compile");
assert!(r.is_cache_dir("dist"));
assert!(r.is_cache_dir("build"));
assert!(r.is_cache_dir("target"), "built-ins remain");
assert_eq!(r.custom_cache_count, 2);
}
#[test]
fn test_malformed_custom_glob_is_reported() {
let err = PackRules::new(&RuleOverrides {
secret_globs: vec!["broken[".to_string()],
..Default::default()
})
.expect_err("malformed glob must fail");
match err {
PackError::BadPattern { pattern, .. } => assert_eq!(pattern, "broken["),
other => panic!("expected BadPattern, got {other:?}"),
}
}
#[test]
fn test_reason_names_the_matching_pattern() {
let r = PackRules::new(&RuleOverrides {
secret_globs: vec!["*.vault".to_string()],
..Default::default()
})
.expect("compile");
assert_eq!(
r.secret_reason("prod.vault").as_deref(),
Some("secret pattern: *.vault")
);
}
#[test]
fn test_empty_overrides_are_defaults() {
let o = RuleOverrides::default();
assert!(o.is_empty());
let r = PackRules::new(&o).expect("compile");
assert!(!r.is_customized());
}
}