use super::*;
#[derive(Debug)]
pub struct Matcher {
pub(crate) regex: Option<BytesRegex>,
pub(crate) prefilter: Option<Prefilter>,
pub(crate) synth_verifiable: Vec<memmem::Finder<'static>>,
pub(crate) synth_conservative: Vec<memmem::Finder<'static>>,
}
#[derive(Debug)]
pub(crate) enum Prefilter {
Literal(memmem::Finder<'static>),
CaselessLiteral(BytesRegex),
AnyLiteral(Vec<memmem::Finder<'static>>),
}
impl Prefilter {
pub(crate) fn may_match(&self, haystack: &[u8]) -> bool {
match self {
Prefilter::Literal(finder) => finder.find(haystack).is_some(),
Prefilter::CaselessLiteral(re) => re.is_match(haystack),
Prefilter::AnyLiteral(finders) => finders.iter().any(|f| f.find(haystack).is_some()),
}
}
}
impl Matcher {
pub(crate) fn pure() -> Matcher {
Matcher {
regex: None,
prefilter: None,
synth_verifiable: Vec::new(),
synth_conservative: Vec::new(),
}
}
pub(crate) fn is_pure_filter(&self) -> bool {
self.regex.is_none()
}
#[cfg(test)]
pub(crate) fn is_match(&self, text: &str) -> bool {
match &self.regex {
None => true,
Some(re) => re.is_match(text.as_bytes()),
}
}
pub(crate) fn locate(&self, text: &str) -> Option<Option<(usize, usize)>> {
match &self.regex {
None => Some(None),
Some(re) => re.find(text.as_bytes()).map(|m| Some((m.start(), m.end()))),
}
}
pub(crate) fn line_may_match(&self, line: &[u8]) -> bool {
match &self.prefilter {
Some(pf) => pf.may_match(line) || self.synth_may_match(line),
None => true,
}
}
pub(crate) fn line_prefilter_hits(&self, line: &[u8]) -> bool {
match &self.prefilter {
Some(pf) => pf.may_match(line),
None => true,
}
}
pub(crate) fn has_prefilter(&self) -> bool {
self.prefilter.is_some()
}
#[cfg(test)]
pub(crate) fn file_may_match(&self, bytes: &[u8]) -> bool {
match &self.prefilter {
Some(pf) => pf.may_match(bytes) || self.synth_may_match(bytes),
None => true,
}
}
pub(crate) fn synth_may_match(&self, haystack: &[u8]) -> bool {
self.synth_verifiable
.iter()
.chain(self.synth_conservative.iter())
.any(|f| f.find(haystack).is_some())
}
pub(crate) fn synth_conservative_hits(&self, haystack: &[u8]) -> bool {
self.synth_conservative
.iter()
.any(|f| f.find(haystack).is_some())
}
pub(crate) fn synth_verifiable_hits(&self, haystack: &[u8]) -> bool {
self.synth_verifiable
.iter()
.any(|f| f.find(haystack).is_some())
}
pub(crate) fn synth_texts_match(&self, rec: &Record) -> bool {
let ctx = crate::model::ClassifyCtx::top_level();
if rec
.record_text_sections(&ctx)
.iter()
.any(|sec| self.locate(&sec.text).is_some())
{
return true;
}
if let Some(t) = rec.auq_exchange() {
if self.locate(&t).is_some() {
return true;
}
}
if let Some(t) = rec.automation_label() {
if self.locate(&t).is_some() {
return true;
}
}
if let Some(t) = record_raw_text(rec) {
if self.locate(&t).is_some() {
return true;
}
}
false
}
}
pub fn build_matcher(args: &SearchArgs) -> Result<Matcher> {
if args.pattern.is_empty() {
return Ok(Matcher {
regex: None,
prefilter: None,
synth_verifiable: Vec::new(),
synth_conservative: Vec::new(),
});
}
let has_uppercase = args.pattern.chars().any(|c| c.is_uppercase());
let case_insensitive = args.ignore_case || !has_uppercase;
let regex = BytesRegex::new(&apply_builder(
&args.pattern,
case_insensitive,
args.multiline,
)?)
.with_context(|| format!("invalid regex pattern: {:?}", args.pattern))?;
let prefilter = match required_needles(&args.pattern) {
None => None,
Some(needles) if case_insensitive => {
let alts: Vec<String> = needles.iter().map(|n| regex::escape(n)).collect();
let src = format!("(?i){}", alts.join("|"));
let re = BytesRegex::new(&src)
.with_context(|| format!("invalid caseless prefilter for {:?}", args.pattern))?;
Some(Prefilter::CaselessLiteral(re))
}
Some(needles) if needles.len() == 1 => Some(Prefilter::Literal(
memmem::Finder::new(needles[0].as_bytes()).into_owned(),
)),
Some(needles) => Some(Prefilter::AnyLiteral(
needles
.iter()
.map(|n| memmem::Finder::new(n.as_bytes()).into_owned())
.collect(),
)),
};
let (synth_verifiable, synth_conservative) = if prefilter.is_some() {
synth_marker_finders(args)
} else {
(Vec::new(), Vec::new())
};
Ok(Matcher {
regex: Some(regex),
prefilter,
synth_verifiable,
synth_conservative,
})
}
pub(crate) fn apply_builder(
pattern: &str,
case_insensitive: bool,
multiline: bool,
) -> Result<String> {
let mut flags = String::new();
if case_insensitive {
flags.push('i');
}
if multiline {
flags.push('s');
flags.push('m');
}
regex::bytes::Regex::new(pattern)
.with_context(|| format!("invalid regex pattern: {pattern:?}"))?;
if flags.is_empty() {
Ok(pattern.to_string())
} else {
Ok(format!("(?{flags}){pattern}"))
}
}
pub(crate) fn required_literal(pattern: &str) -> Option<Vec<u8>> {
const META: &[char] = &[
'.', '*', '+', '?', '(', ')', '[', ']', '{', '}', '|', '^', '$', '\\',
];
if pattern.is_empty() || pattern.chars().any(|c| META.contains(&c)) {
return None;
}
if pattern.chars().any(json_escapes_in_string) {
return None;
}
if pattern.chars().any(char::is_whitespace) {
return None;
}
Some(pattern.as_bytes().to_vec())
}
pub(crate) fn required_needles(pattern: &str) -> Option<Vec<String>> {
if let Some(lit) = required_literal(pattern) {
return String::from_utf8(lit).ok().map(|s| vec![s]);
}
if pattern.is_empty() {
return None;
}
let hir = regex_syntax::ParserBuilder::new()
.utf8(false)
.build()
.parse(pattern)
.ok()?;
let needles = hir_needles(&hir)?;
(needles.len() <= MAX_NEEDLES).then_some(needles)
}
const MAX_NEEDLES: usize = 8;
const MIN_NEEDLE_LEN: usize = 3;
fn hir_needles(hir: ®ex_syntax::hir::Hir) -> Option<Vec<String>> {
use regex_syntax::hir::HirKind;
match hir.kind() {
HirKind::Literal(lit) => {
let s = std::str::from_utf8(&lit.0).ok()?;
let run = s
.split(|c: char| c.is_whitespace() || json_escapes_in_string(c))
.max_by_key(|r| r.len())?;
(run.len() >= MIN_NEEDLE_LEN).then(|| vec![run.to_string()])
}
HirKind::Concat(parts) => parts
.iter()
.filter_map(hir_needles)
.max_by(|a, b| set_strength(a).cmp(&set_strength(b))),
HirKind::Alternation(branches) => {
let mut union: Vec<String> = Vec::new();
for b in branches {
for n in hir_needles(b)? {
if !union.contains(&n) {
union.push(n);
}
}
}
(!union.is_empty()).then_some(union)
}
HirKind::Repetition(rep) if rep.min >= 1 => hir_needles(&rep.sub),
HirKind::Capture(g) => hir_needles(&g.sub),
_ => None,
}
}
fn set_strength(set: &[String]) -> (usize, std::cmp::Reverse<usize>) {
(
set.iter().map(String::len).min().unwrap_or(0),
std::cmp::Reverse(set.len()),
)
}
pub(crate) fn synth_marker_finders(
args: &SearchArgs,
) -> (Vec<memmem::Finder<'static>>, Vec<memmem::Finder<'static>>) {
let mut verifiable: Vec<&[u8]> = vec![
b"<task-notification>",
br#""answers""#,
b"User has answered your questions",
b"Your questions have been answered",
b"The user answered:",
b"stopped by the user",
];
if args
.label_filter()
.selected(Class::CompactionBoundary.path())
{
verifiable.push(b"compact_boundary");
}
if args.reaches_gated(Class::MetaTurnDuration) {
verifiable.push(b"turn_duration");
}
if args.reaches_gated(Class::MetaStopHooks) {
verifiable.push(b"stop_hook_summary");
}
if args.reaches_gated(Class::MetaSnapshot) {
verifiable.push(b"file-history-");
}
if args.reaches_gated(Class::MetaSystem) {
verifiable.push(br#""subtype""#);
}
let mut conservative: Vec<&[u8]> = vec![b"To tell you how to proceed"];
if args.resolve_persisted {
conservative.push(b"persistedOutputPath");
conservative.push(b"Full output saved to:");
}
let mk = |ns: Vec<&[u8]>| {
ns.into_iter()
.map(|n| memmem::Finder::new(n).into_owned())
.collect()
};
(mk(verifiable), mk(conservative))
}
pub(crate) fn json_escapes_in_string(c: char) -> bool {
c == '"' || (c as u32) < 0x20 || c == '\u{7f}'
}
pub(crate) struct AddressSet {
pub(crate) lines: BTreeSet<usize>,
pub(crate) uuids: BTreeSet<String>,
}