Add allowed context matching

This commit is contained in:
2026-05-25 13:04:18 -04:00
parent 0c0d5292a8
commit a7f9ef9e42
+156 -1
View File
@@ -1 +1,156 @@
// Placeholder for upcoming context module.
use crate::domain::AllowedContext;
pub fn domain_allowed(context: &AllowedContext, candidate: &str) -> bool {
let candidate = normalize_domain(candidate);
if candidate.is_empty() {
return false;
}
context.domains.iter().any(|allowed| {
let allowed = normalize_domain(allowed);
!allowed.is_empty()
&& (candidate == allowed
|| candidate
.strip_suffix(&allowed)
.is_some_and(|prefix| prefix.ends_with('.')))
})
}
pub fn window_class_allowed(context: &AllowedContext, candidate: &str) -> bool {
let candidate = normalize_casefolded(candidate);
if candidate.is_empty() {
return false;
}
context
.window_classes
.iter()
.any(|allowed| normalize_casefolded(allowed) == candidate)
}
pub fn window_title_allowed(context: &AllowedContext, candidate: &str) -> bool {
let candidate = normalize_casefolded(candidate);
if candidate.is_empty() {
return false;
}
context.window_title_substrings.iter().any(|allowed| {
let allowed = normalize_casefolded(allowed);
!allowed.is_empty() && candidate.contains(&allowed)
})
}
pub fn command_allowed(context: &AllowedContext, candidate: &str) -> bool {
let candidate = executable_basename(candidate);
if candidate.is_empty() {
return false;
}
context
.commands
.iter()
.any(|allowed| executable_basename(allowed) == candidate)
}
fn normalize_domain(value: &str) -> String {
value.trim().trim_end_matches('.').to_lowercase()
}
fn normalize_casefolded(value: &str) -> String {
value.trim().to_lowercase()
}
fn executable_basename(command: &str) -> String {
let executable = command.split_whitespace().next().unwrap_or_default();
executable
.trim()
.rsplit(['/', '\\'])
.next()
.unwrap_or_default()
.to_lowercase()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::AllowedContext;
#[test]
fn domains_match_exact_or_subdomain() {
let context = AllowedContext {
domains: vec!["github.com".to_string()],
..AllowedContext::default()
};
assert!(domain_allowed(&context, "github.com"));
assert!(domain_allowed(&context, "docs.github.com"));
assert!(!domain_allowed(&context, "evilgithub.com"));
}
#[test]
fn window_classes_match_case_insensitively_after_normalization() {
let context = AllowedContext {
window_classes: vec!["code".to_string()],
..AllowedContext::default()
};
assert!(window_class_allowed(&context, "Code"));
assert!(!window_class_allowed(&context, "firefox"));
}
#[test]
fn titles_match_by_substring() {
let context = AllowedContext {
window_title_substrings: vec!["antidrift".to_string()],
..AllowedContext::default()
};
assert!(window_title_allowed(&context, "Commitment OS - AntiDrift"));
assert!(!window_title_allowed(&context, "random video"));
}
#[test]
fn domains_ignore_whitespace_case_and_trailing_dots() {
let context = AllowedContext {
domains: vec![" GitHub.COM. ".to_string()],
..AllowedContext::default()
};
assert!(domain_allowed(&context, " DOCS.GITHUB.COM. "));
}
#[test]
fn window_class_matching_trims_allowed_and_candidate_values() {
let context = AllowedContext {
window_classes: vec![" code ".to_string()],
..AllowedContext::default()
};
assert!(window_class_allowed(&context, " Code "));
}
#[test]
fn title_matching_trims_allowed_substrings() {
let context = AllowedContext {
window_title_substrings: vec![" antidrift ".to_string()],
..AllowedContext::default()
};
assert!(window_title_allowed(&context, "Commitment OS - AntiDrift"));
}
#[test]
fn commands_match_executable_basename() {
let context = AllowedContext {
commands: vec!["cargo".to_string()],
..AllowedContext::default()
};
assert!(command_allowed(
&context,
"/home/felixm/.cargo/bin/cargo test"
));
assert!(command_allowed(&context, "cargo"));
assert!(!command_allowed(&context, "/usr/bin/git status"));
}
}