use codespan_reporting::diagnostic::{Diagnostic, Label}; use color_eyre::eyre::{bail, eyre}; use regex_automata::{MatchKind, meta, util::syntax}; use toml::Spanned; use crate::config::{Action, Match, Rule}; fn build_regex(re: &str) -> Result { meta::Builder::new() .configure( meta::Config::new() .nfa_size_limit(Some(10 * (1 << 20))) .hybrid_cache_capacity(2 * (1 << 20)) .match_kind(MatchKind::LeftmostFirst) .utf8_empty(true), ) .syntax(syntax::Config::new().utf8(true)) .build(re) } struct CompiledMatch { title: Option, app_id: Option, } impl CompiledMatch { fn new( match_rule: &Match, diagnostics: &mut Diagnostic<()>, ) -> Result { let app_id = match_rule .app_id .as_ref() .map(|x| build_regex(x.get_ref()).map_err(|err| (err, x.span()))) .transpose(); let title = match_rule .title .as_ref() .map(|x| build_regex(x.get_ref()).map_err(|err| (err, x.span()))) .transpose(); let (app_id, title) = match (app_id, title) { (Ok(None), Ok(None)) => bail!("You must specify at least one of `title` or `app-id`"), (Ok(app_id), Ok(title)) => (app_id, title), (app_id, title) => { for res in [app_id, title] { if let Err((err, span)) = res { if let Some(err) = err.syntax_error() { let (message, (start, end)) = match err { regex_syntax::Error::Parse(err) => { let span = err.span(); ( err.kind().to_string(), (span.start.offset + 1, span.end.offset + 1), ) } regex_syntax::Error::Translate(err) => { let span = err.span(); ( err.kind().to_string(), (span.start.offset + 1, span.end.offset + 1), ) } _ => (err.to_string(), (0, span.end - span.start)), }; *diagnostics = diagnostics.clone().with_label( Label::primary((), (span.start + start)..(span.start + end)) .with_message(message), ); } else if let Some(limit) = err.size_limit() { *diagnostics = diagnostics.clone().with_label( Label::primary((), span).with_message(format!( "Compiled regex exceeds size limit of {limit} bytes." )), ); } else { *diagnostics = diagnostics .clone() .with_label(Label::primary((), span).with_message(err.to_string())); } } } bail!("Failed to compile regex"); } }; Ok(Self { title, app_id }) } } struct CompiledFilter { matches: Box<[CompiledMatch]>, excludes: Box<[CompiledMatch]>, } impl CompiledFilter { fn new( matches: &Spanned>, excludes: Option<&[Match]>, diagnostics: &mut Diagnostic<()>, ) -> color_eyre::Result { let matches_empty = if matches.get_ref().is_empty() { *diagnostics = diagnostics.clone().with_label( Label::primary((), matches.span()) .with_message("matches array should contain at least one element"), ); Err(eyre!("matches array is empty")) } else { Ok(()) }; let compiled_matches = matches .get_ref() .iter() .map(|m| CompiledMatch::new(m, diagnostics)) .collect::>() .into_iter() .collect::, _>>(); let compiled_excludes = excludes .unwrap_or(&[]) .iter() .map(|e| CompiledMatch::new(e, diagnostics)) .collect::>() .into_iter() .collect::, _>>(); match (compiled_matches, compiled_excludes, matches_empty) { (Err(err), _, _) | (_, Err(err), _) | (_, _, Err(err)) => Err(err), (Ok(matches), Ok(excludes), Ok(_)) => Ok(Self { matches, excludes }), } } } struct CompiledRule { filter: CompiledFilter, actions: Box<[Action]>, } impl CompiledRule { fn new(rule: &Rule, diagnostics: &mut Diagnostic<()>) -> color_eyre::Result { let filter = CompiledFilter::new( &rule.matches, rule.excludes.as_ref().map(|x| x.as_ref()), diagnostics, )?; Ok(Self { filter, actions: rule.actions.clone().unwrap_or_default(), }) } } pub struct CompiledRules(Box<[CompiledRule]>); impl CompiledRules { pub fn new(rules: &[Rule], diagnostics: &mut Diagnostic<()>) -> color_eyre::Result { let compiled_rules = rules .iter() .map(|r| CompiledRule::new(r, diagnostics)) .collect::>() .into_iter() .collect::, _>>()?; Ok(Self(compiled_rules)) } }