use crate::generated::types::TargetClass;
use super::super::facts::FactSet;
use super::super::types::EventEnv;
pub const SECRET_MATERIAL_GLOBS: &[&str] = &[
".env",
".env.*",
"**/.env",
"**/.env.*",
"~/.aws/**",
"**/.aws/**",
"~/.ssh/**",
"**/.ssh/**",
"~/.kube/config",
"**/.kube/config",
"~/.docker/config.json",
"**/.docker/config.json",
"~/.netrc",
"**/.netrc",
"*.pem",
"**/*.pem",
"*.key",
"**/*.key",
"*.p12",
"**/*.p12",
"**/keychain*",
"**/*token*",
"~/.config/gh/hosts.yml",
];
pub const AGENT_CONFIG_GLOBS: &[&str] = &[
".claude/**",
"**/.claude/**",
".mcp.json",
"**/.mcp.json",
".claude.json",
"**/.claude.json",
".git/hooks/**",
"**/.git/hooks/**",
"~/.bashrc",
"~/.zshrc",
"~/.profile",
"**/.bashrc",
"**/.zshrc",
"**/.profile",
".npmrc",
"**/.npmrc",
".pre-commit-config.yaml",
"**/.pre-commit-config.yaml",
"lefthook.yml",
"**/lefthook.yml",
".vscode/**",
"**/.vscode/**",
".idea/**",
"**/.idea/**",
];
pub const SYSTEM_PATH_GLOBS: &[&str] = &[
"/",
"/etc",
"/etc/**",
"/usr",
"/usr/**",
"/bin",
"/bin/**",
"/sbin",
"/sbin/**",
"/lib",
"/lib/**",
"/opt",
"/opt/**",
"/boot",
"/boot/**",
"/proc",
"/proc/**",
"/sys",
"/sys/**",
"/var",
"/dev",
"/dev/**",
"C:\\",
"C:\\Windows\\**",
];
pub const DATA_STORE_GLOBS: &[&str] = &[
"*.db",
"**/*.db",
"*.sqlite",
"**/*.sqlite",
"*.sqlite3",
"**/*.sqlite3",
"*.dump",
"**/*.dump",
"*.sql",
"**/*.sql",
"**/migrations/**",
"/data/**",
"/var/lib/postgresql/**",
"/var/lib/mysql/**",
"/var/lib/mongodb/**",
"s3://**",
"gs://**",
];
pub fn glob_match(pattern: &str, value: &str) -> bool {
let pattern: Vec<char> = pattern.chars().collect();
let value: Vec<char> = value.chars().collect();
match_from(&pattern, 0, &value, 0)
}
fn match_from(pattern: &[char], mut pi: usize, value: &[char], mut vi: usize) -> bool {
while pi < pattern.len() {
if pattern[pi] == '*' {
if pattern.get(pi + 1) == Some(&'*') {
let rest = pi + 2;
if rest == pattern.len() {
return true;
}
return (vi..=value.len()).any(|k| match_from(pattern, rest, value, k));
}
let rest = pi + 1;
let mut k = vi;
loop {
if match_from(pattern, rest, value, k) {
return true;
}
if k >= value.len() || value[k] == '/' {
return false;
}
k += 1;
}
}
let Some(&ch) = value.get(vi) else {
return false;
};
if pattern[pi] == '?' {
if ch == '/' {
return false;
}
} else if pattern[pi] != ch {
return false;
}
pi += 1;
vi += 1;
}
vi == value.len()
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct PathEnv {
pub home: String,
pub cwd: String,
pub path_dirs: Vec<String>,
pub additional_dirs: Vec<String>,
}
impl PathEnv {
pub fn from_event(env: Option<&EventEnv>) -> PathEnv {
let Some(env) = env else {
return PathEnv::default();
};
PathEnv {
home: env.home.clone().unwrap_or_default(),
cwd: env.cwd.clone().unwrap_or_default(),
path_dirs: env.path_dirs.clone(),
additional_dirs: env.additional_dirs.clone(),
}
}
}
pub fn fact_members(facts: &FactSet, fact_id: &str) -> Vec<String> {
super::super::facts::resolve_set(facts, fact_id, facts.now_ms).unwrap_or_default()
}
fn under(path: &str, root: &str) -> bool {
if root.is_empty() {
return false;
}
path == root || path.starts_with(&format!("{}/", root.trim_end_matches('/')))
}
fn same_dir(left: &str, right: &str) -> bool {
!left.is_empty() && left.trim_end_matches('/') == right.trim_end_matches('/')
}
fn expand_home(path: &str, home: &str) -> Option<String> {
if home.is_empty() {
return None;
}
if path == "~" {
return Some(home.to_string());
}
path.strip_prefix("~/")
.map(|rest| format!("{}/{rest}", home.trim_end_matches('/')))
}
fn match_any(path: &str, patterns: &[&str], env: &PathEnv) -> bool {
match_any_owned(path, patterns.iter().map(|p| (*p).to_string()), env)
}
fn match_any_owned<I: IntoIterator<Item = String>>(path: &str, patterns: I, env: &PathEnv) -> bool {
let mut candidates = vec![path.to_string()];
if !env.home.is_empty() {
if let Some(rest) = path.strip_prefix(env.home.as_str()) {
candidates.push(format!("~{rest}"));
}
if let Some(expanded) = expand_home(path, &env.home) {
candidates.push(expanded);
}
}
for pattern in patterns {
let expanded = match pattern.strip_prefix('~') {
Some(rest) if !env.home.is_empty() => format!("{}{rest}", env.home),
_ => pattern.clone(),
};
for candidate in &candidates {
if glob_match(&pattern, candidate) || glob_match(&expanded, candidate) {
return true;
}
}
}
false
}
pub fn resolve(path: &str, env: Option<&EventEnv>, facts: &FactSet) -> Option<TargetClass> {
resolve_with(path, &PathEnv::from_event(env), facts)
}
pub fn resolve_with(path: &str, env: &PathEnv, facts: &FactSet) -> Option<TargetClass> {
if path.is_empty() {
return None;
}
let class = |name: &str| Some(TargetClass(name.to_string()));
let absolute = expand_home(path, &env.home).unwrap_or_else(|| path.to_string());
if match_any(path, SECRET_MATERIAL_GLOBS, env) {
return class("secret_material");
}
if match_any(path, AGENT_CONFIG_GLOBS, env) {
return class("agent_config");
}
if match_any(path, SYSTEM_PATH_GLOBS, env) {
return class("system_path");
}
if same_dir(&absolute, &env.home) {
return class("system_path");
}
if !env.cwd.is_empty() && !same_dir(&absolute, &env.cwd) && under(&env.cwd, &absolute) {
return class("system_path");
}
if env.path_dirs.iter().any(|dir| same_dir(&absolute, dir)) {
return class("system_path");
}
if match_any_owned(path, fact_members(facts, "classified_sources"), env) {
return class("classified_source");
}
if match_any(path, DATA_STORE_GLOBS, env) {
return class("data_store");
}
if fact_members(facts, "data_store_roots")
.iter()
.any(|root| under(&absolute, root))
{
return class("data_store");
}
if !path.starts_with('/') && !path.starts_with('~') && !path.contains("://") {
return class("workspace_file");
}
if under(&absolute, &env.cwd) || env.additional_dirs.iter().any(|dir| under(&absolute, dir)) {
return class("workspace_file");
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn env() -> PathEnv {
PathEnv {
home: "/home/dev".to_string(),
cwd: "/home/dev/proj".to_string(),
path_dirs: vec!["/usr/local/bin".to_string()],
additional_dirs: vec!["/srv/extra".to_string()],
}
}
fn class_of(path: &str, env: &PathEnv) -> Option<String> {
resolve_with(path, env, &FactSet::default()).map(|c| c.0)
}
#[test]
fn double_star_crosses_a_slash_and_single_star_does_not() {
assert!(glob_match("**/*.pem", "certs/nested/server.pem"));
assert!(!glob_match("*.pem", "certs/server.pem"));
assert!(glob_match("*.pem", "server.pem"));
assert!(glob_match("**/migrations/**", "db/migrations/0001.py"));
assert!(!glob_match("/etc/**", "/etcetera/x"));
}
#[test]
fn a_backslash_in_a_windows_seed_is_a_literal() {
assert!(glob_match(
"C:\\Windows\\**",
"C:\\Windows\\System32\\hosts"
));
assert!(!glob_match("C:\\Windows\\**", "C:/Windows/System32"));
}
#[test]
fn the_order_is_the_semantics() {
let env = PathEnv::default();
assert_eq!(
class_of("config/.env", &env).as_deref(),
Some("secret_material")
);
assert_eq!(
class_of(".claude/x.md", &env).as_deref(),
Some("agent_config")
);
assert_eq!(
class_of("/var/lib/postgresql/16/base", &env).as_deref(),
Some("data_store")
);
}
#[test]
fn rows_three_and_six_need_env_and_stay_silent_without_it() {
let none = PathEnv::default();
assert_eq!(class_of("/home/dev", &none), None, "no env, no home root");
assert_eq!(class_of("/srv/extra/notes.md", &none), None);
let env = env();
assert_eq!(class_of("/home/dev", &env).as_deref(), Some("system_path"));
assert_eq!(
class_of("/usr/local/bin", &env).as_deref(),
Some("system_path")
);
assert_eq!(
class_of("/srv/extra/notes.md", &env).as_deref(),
Some("workspace_file")
);
}
#[test]
fn an_unplaceable_absolute_path_is_never_a_guessed_workspace_file() {
assert_eq!(class_of("/srv/blob/opaque.bin", &env()), None);
}
#[test]
fn a_tilde_resolves_against_the_declared_home_and_not_the_host() {
assert_eq!(
class_of("~/proj/notes.md", &env()).as_deref(),
Some("workspace_file")
);
assert_eq!(
class_of("~/tmp/stale", &PathEnv::default()),
None,
"with no declared home there is nothing to expand against"
);
}
#[test]
fn a_customer_root_arrives_as_a_fact_and_not_as_a_constant() {
let facts = FactSet::new(
vec![serde_json::from_value(serde_json::json!({
"fact_id": "data_store_roots",
"kind": "set",
"observed_at": "2025-09-01T15:59:00+00:00",
"max_age_s": 86400,
"value": ["/lake"],
}))
.expect("the fixture fact parses")],
1_756_742_400_000,
);
assert_eq!(
resolve_with("/lake/raw/x.parquet", &PathEnv::default(), &facts).map(|c| c.0),
Some("data_store".to_string())
);
assert_eq!(
resolve_with(
"/lake/raw/x.parquet",
&PathEnv::default(),
&FactSet::default()
),
None,
"without the fact the root is unknown, not empty-and-therefore-fine"
);
}
}