use crate::generated::types::TargetClass;
use super::super::facts::FactSet;
use super::super::types::EventEnv;
use super::catalogue::{Catalogue, Origin, PatternKind};
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 fn glob_match(pattern: &str, value: &str) -> bool {
let value: Vec<char> = value.chars().collect();
let full: Vec<char> = pattern.chars().collect();
if match_from(&full, 0, &value, 0) {
return true;
}
match pattern.strip_prefix("**/") {
Some(rest) => {
let rest: Vec<char> = rest.chars().collect();
match_from(&rest, 0, &value, 0)
}
None => false,
}
}
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<'a> {
pub home: String,
pub cwd: String,
pub path_dirs: Vec<String>,
pub additional_dirs: Vec<String>,
pub catalogue: Option<&'a Catalogue>,
}
impl<'a> PathEnv<'a> {
pub fn from_event(env: Option<&EventEnv>) -> PathEnv<'a> {
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(),
catalogue: None,
}
}
pub fn with_catalogue(mut self, catalogue: Option<&'a Catalogue>) -> PathEnv<'a> {
self.catalogue = catalogue;
self
}
}
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 candidates(path: &str, env: &PathEnv) -> Vec<String> {
let mut out = vec![path.to_string()];
if !env.home.is_empty() {
if let Some(rest) = path.strip_prefix(env.home.as_str()) {
out.push(format!("~{rest}"));
}
if let Some(expanded) = expand_home(path, &env.home) {
out.push(expanded);
}
}
out
}
fn pattern_matches(pattern: &str, candidates: &[String], env: &PathEnv) -> bool {
let expanded = match pattern.strip_prefix('~') {
Some(rest) if !env.home.is_empty() => format!("{}{rest}", env.home),
_ => pattern.to_string(),
};
candidates
.iter()
.any(|candidate| glob_match(pattern, candidate) || glob_match(&expanded, candidate))
}
fn match_any(path: &str, patterns: &[&str], env: &PathEnv) -> bool {
let forms = candidates(path, env);
patterns
.iter()
.any(|pattern| pattern_matches(pattern, &forms, env))
}
fn match_any_owned<I: IntoIterator<Item = String>>(path: &str, patterns: I, env: &PathEnv) -> bool {
let forms = candidates(path, env);
patterns
.into_iter()
.any(|pattern| pattern_matches(&pattern, &forms, env))
}
#[derive(Debug, Clone, PartialEq)]
pub struct PathClass {
pub class: TargetClass,
pub origins: Vec<Origin>,
}
fn rank(class: &str) -> u8 {
match class {
"secret_material" => 1,
"agent_config" => 2,
"system_path" => 3,
"classified_source" => 4,
"data_store" => 5,
"workspace_file" => 6,
_ => 7,
}
}
fn add_class(out: &mut Vec<PathClass>, class: &str, origin: Option<&Origin>) {
let existing = out.iter_mut().find(|c| c.class.0 == class);
let entry = match existing {
Some(entry) => entry,
None => {
out.push(PathClass {
class: TargetClass(class.to_string()),
origins: Vec::new(),
});
out.last_mut().expect("just pushed")
}
};
if let Some(origin) = origin {
if !entry.origins.contains(origin) {
entry.origins.push(origin.clone());
}
}
}
pub fn resolve_all(path: &str, env: &PathEnv, facts: &FactSet) -> Vec<PathClass> {
if path.is_empty() {
return Vec::new();
}
let absolute = expand_home(path, &env.home).unwrap_or_else(|| path.to_string());
let forms = candidates(path, env);
let mut out: Vec<PathClass> = Vec::new();
if let Some(catalogue) = env.catalogue {
let mut removed: Vec<&str> = Vec::new();
for target in catalogue
.targets
.iter()
.filter(|t| t.kind == PatternKind::Path)
{
if !pattern_matches(&target.pattern, &forms, env) {
continue;
}
if target.narrow {
removed.push(&target.target_class);
} else {
add_class(&mut out, &target.target_class, Some(&target.origin));
}
}
out.retain(|c| !removed.contains(&c.class.0.as_str()));
}
if match_any(path, SYSTEM_PATH_GLOBS, env)
|| same_dir(&absolute, &env.home)
|| (!env.cwd.is_empty() && !same_dir(&absolute, &env.cwd) && under(&env.cwd, &absolute))
|| env.path_dirs.iter().any(|dir| same_dir(&absolute, dir))
{
add_class(&mut out, "system_path", None);
}
if match_any_owned(path, fact_members(facts, "classified_sources"), env) {
add_class(&mut out, "classified_source", None);
}
if fact_members(facts, "data_store_roots")
.iter()
.any(|root| under(&absolute, root))
{
add_class(&mut out, "data_store", None);
}
if out.is_empty()
&& ((!path.starts_with('/') && !path.starts_with('~') && !path.contains("://"))
|| under(&absolute, &env.cwd)
|| env.additional_dirs.iter().any(|dir| under(&absolute, dir)))
{
add_class(&mut out, "workspace_file", None);
}
out.sort_by_key(|c| rank(&c.class.0));
out
}
pub fn resolve_host(host: &str, env: &PathEnv) -> Vec<PathClass> {
let mut out: Vec<PathClass> = Vec::new();
let Some(catalogue) = env.catalogue else {
return out;
};
let mut removed: Vec<&str> = Vec::new();
for target in catalogue
.targets
.iter()
.filter(|t| t.kind == PatternKind::Host)
{
if !super::catalogue::host_matches(&target.pattern, host) {
continue;
}
if target.narrow {
removed.push(&target.target_class);
} else {
add_class(&mut out, &target.target_class, Some(&target.origin));
}
}
out.retain(|c| !removed.contains(&c.class.0.as_str()));
out
}
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> {
resolve_all(path, env, facts)
.into_iter()
.next()
.map(|c| c.class)
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::catalogue::fixtures::{seed_r1, seed_r1_plus};
fn seeded() -> PathEnv<'static> {
PathEnv::default().with_catalogue(Some(seed_r1()))
}
fn env() -> PathEnv<'static> {
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()],
catalogue: Some(seed_r1()),
}
}
fn class_of(path: &str, env: &PathEnv) -> Option<String> {
resolve_with(path, env, &FactSet::default()).map(|c| c.0)
}
fn classes_of(path: &str, env: &PathEnv) -> Vec<String> {
resolve_all(path, env, &FactSet::default())
.into_iter()
.map(|c| c.class.0)
.collect()
}
#[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 = seeded();
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 = seeded();
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", &seeded()),
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"
);
}
#[test]
fn a_path_matching_several_patterns_carries_every_class() {
let catalogue = seed_r1_plus(serde_json::json!({"targets": [
{"id": "n1", "origin": "org", "pattern_kind": "path", "key": "**/.npmrc",
"target_class": "secret_material"}
]}));
let env = PathEnv::default().with_catalogue(Some(catalogue));
assert_eq!(
classes_of(".npmrc", &env),
vec!["secret_material", "agent_config"]
);
assert_eq!(
classes_of("/home/dev/p/.npmrc", &env),
vec!["secret_material", "agent_config"]
);
let all = resolve_all(".npmrc", &env, &FactSet::default());
assert_eq!(all[0].origins, vec![Origin::Entry("n1".to_string())]);
}
#[test]
fn a_code_row_and_a_catalogue_row_both_add_their_class() {
let catalogue = seed_r1_plus(serde_json::json!({"targets": [
{"id": "acme", "origin": "org", "pattern_kind": "path", "key": "/opt/acme/**",
"target_class": "secret_material"}
]}));
let env = PathEnv::default().with_catalogue(Some(catalogue));
assert_eq!(
classes_of("/opt/acme/secrets.yaml", &env),
vec!["secret_material", "system_path"]
);
}
#[test]
fn a_leading_double_star_slash_is_optional() {
assert!(glob_match("**/.npmrc", ".npmrc"));
assert!(glob_match("**/.npmrc", "/home/dev/p/.npmrc"));
assert!(glob_match("**/.ssh/**", ".ssh/id_rsa"));
assert!(!glob_match("**/.npmrc", "x.npmrc"));
assert!(
!glob_match(".npmrc", "p/.npmrc"),
"only a leading `**/` is optional"
);
}
#[test]
fn a_narrow_target_removes_its_class_in_every_scope() {
let catalogue = seed_r1_plus(serde_json::json!({"targets": [
{"id": "fix", "origin": "org", "narrow": true, "pattern_kind": "path",
"key": "**/*token*", "target_class": "secret_material"}
]}));
let env = PathEnv::default().with_catalogue(Some(catalogue));
assert_eq!(
classes_of("docs/token-bucket.md", &env),
vec!["workspace_file"]
);
}
#[test]
fn with_no_catalogue_no_pattern_row_fires() {
let env = PathEnv::default();
assert_eq!(classes_of("config/.env", &env), vec!["workspace_file"]);
}
}