use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use crate::Severity;
#[derive(Debug, Clone)]
pub struct MatchContext {
pub path: String,
pub language: String,
pub rule_id: String,
pub start: usize,
pub finish: usize,
pub line: usize,
pub col: usize,
pub match_text: String,
pub captures: Vec<(String, String)>,
}
impl MatchContext {
pub fn capture(&self, name: &str) -> Option<&str> {
self.captures
.iter()
.find(|(k, _)| k == name)
.map(|(_, v)| v.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Decision {
Allow,
Violate {
severity: Option<Severity>,
message: String,
},
ViolateDefault,
}
pub trait MatchCallback: Send + Sync {
fn evaluate(&self, ctx: &MatchContext) -> Result<Decision, String>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RawMatch {
pub start: usize,
pub finish: usize,
pub captures: Vec<(String, String)>,
}
impl RawMatch {
pub fn new(start: usize, finish: usize) -> Self {
Self {
start,
finish,
captures: Vec::new(),
}
}
pub fn with_capture(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.captures.push((name.into(), value.into()));
self
}
}
pub trait MatchParser: Send + Sync {
fn find(&self, text: &str, offset: usize) -> Result<Vec<RawMatch>, String>;
}
struct NoopCallback;
impl MatchCallback for NoopCallback {
fn evaluate(&self, _ctx: &MatchContext) -> Result<Decision, String> {
Ok(Decision::Allow)
}
}
#[derive(Default)]
pub struct Callbacks {
callbacks: HashMap<String, Arc<dyn MatchCallback>>,
parsers: HashMap<String, Arc<dyn MatchParser>>,
}
impl Callbacks {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, name: impl Into<String>, callback: Arc<dyn MatchCallback>) {
self.callbacks
.insert(format!("cb:name:{}", name.into()), callback);
}
pub fn register_allow(&mut self, name: impl Into<String>) {
self.register(name, Arc::new(NoopCallback));
}
pub fn register_ref(&mut self, reference: &CallbackRef, callback: Arc<dyn MatchCallback>) {
self.callbacks.insert(key_of(reference, "cb"), callback);
}
pub fn register_parser(&mut self, name: impl Into<String>, parser: Arc<dyn MatchParser>) {
self.parsers
.insert(format!("parser:name:{}", name.into()), parser);
}
pub fn register_parser_ref(&mut self, reference: &CallbackRef, parser: Arc<dyn MatchParser>) {
self.parsers.insert(key_of(reference, "parser"), parser);
}
pub fn resolve(&self, reference: &CallbackRef) -> Option<Arc<dyn MatchCallback>> {
self.callbacks.get(&key_of(reference, "cb")).cloned()
}
pub fn resolve_parser(&self, reference: &CallbackRef) -> Option<Arc<dyn MatchParser>> {
self.parsers.get(&key_of(reference, "parser")).cloned()
}
pub fn is_empty(&self) -> bool {
self.callbacks.is_empty() && self.parsers.is_empty()
}
}
impl fmt::Debug for Callbacks {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Callbacks")
.field("callbacks", &self.callbacks.len())
.field("parsers", &self.parsers.len())
.finish()
}
}
fn key_of(reference: &CallbackRef, kind: &str) -> String {
match reference {
CallbackRef::Name(name) => format!("{kind}:name:{name}"),
CallbackRef::File { path } => format!("{kind}:file:{path}"),
CallbackRef::Inline { source } => format!("{kind}:inline:{source}"),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CallbackRef {
Inline {
source: String,
},
File {
path: String,
},
Name(String),
}
impl CallbackRef {
pub fn parse(s: &str) -> Self {
if s.ends_with(".lua") {
Self::File { path: s.to_string() }
} else if s.chars().any(char::is_whitespace) {
Self::Inline { source: s.to_string() }
} else {
Self::Name(s.to_string())
}
}
pub fn describe(&self) -> String {
match self {
Self::Inline { .. } => "inline callback".to_string(),
Self::File { path } => format!("callback file '{path}'"),
Self::Name(name) => format!("callback '{name}'"),
}
}
}