use crate::decision::Decision; use crate::error::Error; use crate::error::Result; use serde::Deserialize; use serde::Serialize; use shlex::try_join; use std::any::Any; use std::fmt::Debug; use std::sync::Arc; /// Matches a single command token, either a fixed string or one of several allowed alternatives. #[derive(Clone, Debug, Eq, PartialEq)] pub enum PatternToken { Single(String), Alts(Vec), } impl PatternToken { fn matches(&self, token: &str) -> bool { match self { Self::Single(expected) => expected == token, Self::Alts(alternatives) => alternatives.iter().any(|alt| alt == token), } } pub fn alternatives(&self) -> &[String] { match self { Self::Single(expected) => std::slice::from_ref(expected), Self::Alts(alternatives) => alternatives, } } } /// Prefix matcher for commands with support for alternative match tokens. /// First token is fixed since we key by the first token in policy. #[derive(Clone, Debug, Eq, PartialEq)] pub struct PrefixPattern { pub first: Arc, pub rest: Arc<[PatternToken]>, } impl PrefixPattern { pub fn matches_prefix(&self, cmd: &[String]) -> Option> { let pattern_length = self.rest.len() + 1; if cmd.len() < pattern_length || cmd[0] != self.first.as_ref() { return None; } for (pattern_token, cmd_token) in self.rest.iter().zip(&cmd[1..pattern_length]) { if !pattern_token.matches(cmd_token) { return None; } } Some(cmd[..pattern_length].to_vec()) } } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum RuleMatch { PrefixRuleMatch { #[serde(rename = "matchedPrefix")] matched_prefix: Vec, decision: Decision, /// Optional rationale for why this rule exists. /// /// This can be supplied for any decision and may be surfaced in different contexts /// (e.g., prompt reasons or rejection messages). #[serde(skip_serializing_if = "Option::is_none")] justification: Option, }, HeuristicsRuleMatch { command: Vec, decision: Decision, }, } impl RuleMatch { pub fn decision(&self) -> Decision { match self { Self::PrefixRuleMatch { decision, .. } => *decision, Self::HeuristicsRuleMatch { decision, .. } => *decision, } } } #[derive(Clone, Debug, Eq, PartialEq)] pub struct PrefixRule { pub pattern: PrefixPattern, pub decision: Decision, pub justification: Option, } pub trait Rule: Any + Debug + Send + Sync { fn program(&self) -> &str; fn matches(&self, cmd: &[String]) -> Option; fn as_any(&self) -> &dyn Any; } pub type RuleRef = Arc; impl Rule for PrefixRule { fn program(&self) -> &str { self.pattern.first.as_ref() } fn matches(&self, cmd: &[String]) -> Option { self.pattern .matches_prefix(cmd) .map(|matched_prefix| RuleMatch::PrefixRuleMatch { matched_prefix, decision: self.decision, justification: self.justification.clone(), }) } fn as_any(&self) -> &dyn Any { self } } /// Count how many rules match each provided example and error if any example is unmatched. pub(crate) fn validate_match_examples(rules: &[RuleRef], matches: &[Vec]) -> Result<()> { let mut unmatched_examples = Vec::new(); for example in matches { if rules.iter().any(|rule| rule.matches(example).is_some()) { continue; } unmatched_examples.push( try_join(example.iter().map(String::as_str)) .unwrap_or_else(|_| "unable to render example".to_string()), ); } if unmatched_examples.is_empty() { Ok(()) } else { Err(Error::ExampleDidNotMatch { rules: rules.iter().map(|rule| format!("{rule:?}")).collect(), examples: unmatched_examples, }) } } /// Ensure that no rule matches any provided negative example. pub(crate) fn validate_not_match_examples( rules: &[RuleRef], not_matches: &[Vec], ) -> Result<()> { for example in not_matches { if let Some(rule) = rules.iter().find(|rule| rule.matches(example).is_some()) { return Err(Error::ExampleDidMatch { rule: format!("{rule:?}"), example: try_join(example.iter().map(String::as_str)) .unwrap_or_else(|_| "unable to render example".to_string()), }); } } Ok(()) }