Skip to main content

squeeze/
scanner.rs

1use crate::classify::{self, BLOCK, Backend, CAT_NONE, CATEGORY, Lanes, Rules};
2use crate::search::{self, NibbleSet};
3use crate::{Anchor, ByteSet, Finder, Memo, RunCache, RunHint, RunRule, Runs};
4use std::fmt;
5use std::mem::MaybeUninit;
6use std::ops::Range;
7
8const CL_DIGIT: u16 = 1 << 0;
9const CL_HEX_ALPHA: u16 = 1 << 1;
10const CL_ALPHA_OTHER: u16 = 1 << 2;
11const CL_AT: u16 = 1 << 3;
12const CL_DOLLAR: u16 = 1 << 4;
13const CL_HASH: u16 = 1 << 5;
14const CL_OPEN_BRACE: u16 = 1 << 6;
15const CL_OPEN_BRACKET: u16 = 1 << 7;
16const CL_COLON: u16 = 1 << 8;
17const CL_DOT: u16 = 1 << 9;
18const CL_SLASH: u16 = 1 << 10;
19const CL_TILDE: u16 = 1 << 11;
20const CL_PLUS: u16 = 1 << 12;
21const CL_OPEN_PAREN: u16 = 1 << 13;
22const CL_DASH: u16 = 1 << 14;
23
24const CL_ALL_USED: u16 = (1 << 15) - 1;
25const CL_HEX: u16 = CL_DIGIT | CL_HEX_ALPHA;
26
27const fn build_byte_class_table() -> [u16; 256] {
28    let mut table = [0u16; 256];
29    let mut i = 0u16;
30    while i < 256 {
31        let b = i as u8;
32        let mut c = 0u16;
33        if b >= b'0' && b <= b'9' {
34            c |= CL_DIGIT;
35        }
36        if (b >= b'a' && b <= b'f') || (b >= b'A' && b <= b'F') {
37            c |= CL_HEX_ALPHA;
38        }
39        if (b >= b'g' && b <= b'z') || (b >= b'G' && b <= b'Z') {
40            c |= CL_ALPHA_OTHER;
41        }
42        if b == b'@' {
43            c |= CL_AT;
44        }
45        if b == b'$' {
46            c |= CL_DOLLAR;
47        }
48        if b == b'#' {
49            c |= CL_HASH;
50        }
51        if b == b'{' {
52            c |= CL_OPEN_BRACE;
53        }
54        if b == b'[' {
55            c |= CL_OPEN_BRACKET;
56        }
57        if b == b':' {
58            c |= CL_COLON;
59        }
60        if b == b'.' {
61            c |= CL_DOT;
62        }
63        if b == b'/' {
64            c |= CL_SLASH;
65        }
66        if b == b'~' {
67            c |= CL_TILDE;
68        }
69        if b == b'+' {
70            c |= CL_PLUS;
71        }
72        if b == b'(' {
73            c |= CL_OPEN_PAREN;
74        }
75        if b == b'-' {
76            c |= CL_DASH;
77        }
78        table[i as usize] = c;
79        i += 1;
80    }
81    table
82}
83
84static BYTE_CLASSES: [u16; 256] = build_byte_class_table();
85
86/// Coarse byte classes used to index the context gate tables. Bytes that
87/// finders single out in their boundary rules get their own class so the
88/// gates stay exact for them; everything else is grouped.
89const CTX_CLASSES: usize = 18;
90/// Class used when there is no byte (start or end of the line).
91const CTX_NONE: usize = CTX_CLASSES - 1;
92
93const fn ctx_class(b: u8) -> usize {
94    match b {
95        b'0'..=b'9' => 0,
96        b'a'..=b'f' | b'A'..=b'F' => 1,
97        b'g' | b'G' => 2,
98        b's' | b'S' => 3,
99        b'y' | b'Y' => 4,
100        b'h'..=b'z' | b'H'..=b'Z' => 5,
101        b'.' => 6,
102        b':' => 7,
103        b'-' => 8,
104        b'_' => 9,
105        b'/' => 10,
106        b'@' => 11,
107        b' ' | b'\t' | b'\n' | b'\r' | 0x0B | 0x0C => 12,
108        0x80..=0xBF => 13,
109        0xC0..=0xFF => 14,
110        b'(' | b'[' | b'{' | b'<' | b'"' | b'\'' | b'`' | b'=' | b'>' => 15,
111        _ => 16,
112    }
113}
114
115const fn build_ctx_class_table() -> [u8; 256] {
116    let mut table = [0u8; 256];
117    let mut i = 0;
118    while i < 256 {
119        table[i] = ctx_class(i as u8) as u8;
120        i += 1;
121    }
122    table
123}
124
125static CTX_CLASS: [u8; 256] = build_ctx_class_table();
126
127/// Vector categories covering a context class (see `classify`).
128fn ctx_class_category(class: usize) -> u8 {
129    match class {
130        0 => classify::CAT_DIGIT,
131        1 => classify::CAT_HEX_ALPHA,
132        2 => classify::CAT_ALPHA_GO,
133        3 | 4 => classify::CAT_ALPHA_PZ,
134        5 => classify::CAT_OTHER_ALPHA,
135        6 => classify::CAT_DOT,
136        7 => classify::CAT_COLON,
137        13 | 14 => classify::CAT_HIGH,
138        _ => classify::CAT_OTHER,
139    }
140}
141
142#[inline]
143fn prescan(input: &[u8]) -> u16 {
144    let mut classes = 0u16;
145    let (chunks, remainder) = input.as_chunks::<8>();
146    for chunk in chunks {
147        classes |= BYTE_CLASSES[chunk[0] as usize]
148            | BYTE_CLASSES[chunk[1] as usize]
149            | BYTE_CLASSES[chunk[2] as usize]
150            | BYTE_CLASSES[chunk[3] as usize]
151            | BYTE_CLASSES[chunk[4] as usize]
152            | BYTE_CLASSES[chunk[5] as usize]
153            | BYTE_CLASSES[chunk[6] as usize]
154            | BYTE_CLASSES[chunk[7] as usize];
155        if classes == CL_ALL_USED {
156            return classes;
157        }
158    }
159    for &b in remainder {
160        classes |= BYTE_CLASSES[b as usize];
161    }
162    classes
163}
164
165fn required_classes(id: &str) -> &'static [(u16, bool)] {
166    match id {
167        "cidr" => &[(CL_DIGIT, true), (CL_SLASH, true)],
168        "codetag" => &[(CL_COLON, true)],
169        "color" => &[(CL_HASH | CL_ALPHA_OTHER | CL_HEX_ALPHA, false)],
170        "datetime" => &[(CL_DIGIT, true)],
171        "domain" => &[(CL_DOT, true), (CL_ALPHA_OTHER | CL_HEX_ALPHA, false)],
172        "email" => &[(CL_AT, true)],
173        "emoji" => &[],
174        "env" => &[(CL_DOLLAR, true)],
175        "handle" => &[(CL_AT, true)],
176        "hash" => &[(CL_HEX, false)],
177        "ip" => &[(CL_DIGIT | CL_HEX_ALPHA | CL_COLON | CL_OPEN_BRACKET, false)],
178        "json" => &[(CL_OPEN_BRACE | CL_OPEN_BRACKET, false)],
179        "jwt" => &[(CL_HEX_ALPHA, false)],
180        "mac" => &[(CL_HEX, false), (CL_COLON | CL_DASH | CL_DOT, false)],
181        "mirror" => &[],
182        "modeline" => &[(CL_COLON, true)],
183        "path" => &[(CL_SLASH | CL_DOT | CL_TILDE, false)],
184        "phone" => &[(CL_DIGIT | CL_PLUS | CL_OPEN_PAREN, false)],
185        "semver" => &[(CL_DIGIT, true), (CL_DOT, true)],
186        "uri" => &[(CL_COLON, true)],
187        "uuid" => &[(CL_HEX, false), (CL_DASH, true)],
188        _ => &[],
189    }
190}
191
192#[inline]
193fn can_skip_with_mask(cl: u16, required: &[(u16, bool)]) -> bool {
194    for &(mask, all_required) in required {
195        if all_required {
196            if cl & mask != mask {
197                return true;
198            }
199        } else if (cl & mask) == 0 {
200            return true;
201        }
202    }
203    false
204}
205
206const MAX_FINDERS: usize = 32;
207
208#[derive(Clone, Debug, PartialEq, Eq)]
209pub enum ScannerError {
210    TooManyFinders { len: usize, max: usize },
211}
212
213impl fmt::Display for ScannerError {
214    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215        match self {
216            ScannerError::TooManyFinders { len, max } => {
217                write!(f, "too many finders: got {}, max is {}", len, max)
218            }
219        }
220    }
221}
222
223#[derive(Clone, Debug, PartialEq, Eq)]
224pub struct Match {
225    pub finder_index: usize,
226    pub range: Range<usize>,
227}
228
229/// Per-finder operation counters, see [`ScanStats`].
230#[derive(Clone, Debug, Default, PartialEq, Eq)]
231pub struct FinderStats {
232    /// Lines on which the finder ran (was not disabled by the prescan).
233    pub lines_run: u64,
234    /// Calls to [`Finder::find`] (scan mode).
235    pub find_calls: u64,
236    /// Calls to [`Finder::try_at`] (dispatch mode).
237    pub try_at_calls: u64,
238    /// Calls to [`Finder::try_trigger_at`] (trigger mode).
239    pub try_trigger_calls: u64,
240    /// Calls of any kind that returned `Some`.
241    pub hits: u64,
242    /// Matches kept after the per-finder overlap check.
243    pub matches: u64,
244}
245
246impl FinderStats {
247    /// Total finder invocations of any mode.
248    pub fn calls(&self) -> u64 {
249        self.find_calls + self.try_at_calls + self.try_trigger_calls
250    }
251
252    fn add(&mut self, other: &FinderStats) {
253        self.lines_run += other.lines_run;
254        self.find_calls += other.find_calls;
255        self.try_at_calls += other.try_at_calls;
256        self.try_trigger_calls += other.try_trigger_calls;
257        self.hits += other.hits;
258        self.matches += other.matches;
259    }
260}
261
262/// Operation counters collected by [`Scanner::scan_line_stats`].
263///
264/// The counters describe how much work the scanner and its finders did, so
265/// that performance can be reasoned about independently of wall-clock time:
266/// a finder invocation is the unit of work the scanner dispatches, and the
267/// ratio of invocations to hits shows how well candidate filtering works.
268#[derive(Clone, Debug, Default, PartialEq, Eq)]
269pub struct ScanStats {
270    /// Lines scanned (including empty lines).
271    pub lines: u64,
272    /// Bytes scanned.
273    pub bytes: u64,
274    /// Lines on which the prescan disabled every finder.
275    pub lines_skipped: u64,
276    /// Finder×line pairs disabled by the prescan.
277    pub finder_lines_skipped: u64,
278    /// Positions visited by the dispatch and trigger loops (one per byte and
279    /// per loop that ran).
280    pub positions: u64,
281    /// Positions the vector stage passed on to the exact gates
282    /// ([`Strategy::Vector`] only).
283    pub coarse_positions: u64,
284    /// Positions where at least one candidate finder was invoked.
285    pub candidate_positions: u64,
286    /// Finder invocations avoided by run rules.
287    pub run_gated: u64,
288    /// Coarse positions the exact gates rejected, by the byte at the
289    /// position (diagnostics for the vector stage's precision).
290    pub coarse_rejected: Vec<u64>,
291    /// Matches returned to the caller.
292    pub matches: u64,
293    /// Lines whose matches had to be sorted.
294    pub sorts: u64,
295    /// One entry per finder, in scanner order.
296    pub finders: Vec<FinderStats>,
297}
298
299impl ScanStats {
300    /// Creates counters sized for `scanner`.
301    pub fn for_scanner(scanner: &Scanner) -> Self {
302        ScanStats {
303            finders: vec![FinderStats::default(); scanner.finders.len()],
304            ..Default::default()
305        }
306    }
307
308    /// Adds `other` into `self`; both must come from scanners with the same
309    /// finder list.
310    pub fn merge(&mut self, other: &ScanStats) {
311        self.lines += other.lines;
312        self.bytes += other.bytes;
313        self.lines_skipped += other.lines_skipped;
314        self.finder_lines_skipped += other.finder_lines_skipped;
315        self.positions += other.positions;
316        self.coarse_positions += other.coarse_positions;
317        self.candidate_positions += other.candidate_positions;
318        self.run_gated += other.run_gated;
319        if self.coarse_rejected.len() < other.coarse_rejected.len() {
320            self.coarse_rejected.resize(other.coarse_rejected.len(), 0);
321        }
322        for (i, n) in other.coarse_rejected.iter().enumerate() {
323            self.coarse_rejected[i] += n;
324        }
325        self.matches += other.matches;
326        self.sorts += other.sorts;
327        if self.finders.len() < other.finders.len() {
328            self.finders
329                .resize(other.finders.len(), FinderStats::default());
330        }
331        for (a, b) in self.finders.iter_mut().zip(&other.finders) {
332            a.add(b);
333        }
334    }
335
336    /// Total finder invocations of any mode.
337    pub fn calls(&self) -> u64 {
338        self.finders.iter().map(FinderStats::calls).sum()
339    }
340
341    /// Total finder invocations that returned `Some`.
342    pub fn hits(&self) -> u64 {
343        self.finders.iter().map(|f| f.hits).sum()
344    }
345}
346
347/// Receives scanner events. `()` ignores them at zero cost; [`ScanStats`]
348/// counts them.
349trait Probe {
350    #[inline(always)]
351    fn line(&mut self, _bytes: usize) {}
352    #[inline(always)]
353    fn line_skipped(&mut self) {}
354    #[inline(always)]
355    fn finders_skipped(&mut self, _count: u32) {}
356    #[inline(always)]
357    fn finder_run(&mut self, _finder: usize) {}
358    #[inline(always)]
359    fn positions(&mut self, _count: usize) {}
360    #[inline(always)]
361    fn coarse_position(&mut self) {}
362    #[inline(always)]
363    fn candidate_position(&mut self) {}
364    #[inline(always)]
365    fn run_gated(&mut self) {}
366    #[inline(always)]
367    fn run_gated_n(&mut self, _n: u32) {}
368    #[inline(always)]
369    fn coarse_rejected(&mut self, _cur: u8) {}
370    #[inline(always)]
371    fn find_call(&mut self, _finder: usize, _hit: bool) {}
372    #[inline(always)]
373    fn try_at_call(&mut self, _finder: usize, _hit: bool) {}
374    #[inline(always)]
375    fn try_trigger_call(&mut self, _finder: usize, _hit: bool) {}
376    #[inline(always)]
377    fn matched(&mut self, _finder: usize) {}
378    #[inline(always)]
379    fn sorted(&mut self) {}
380}
381
382impl Probe for () {}
383
384impl Probe for ScanStats {
385    fn line(&mut self, bytes: usize) {
386        self.lines += 1;
387        self.bytes += bytes as u64;
388    }
389    fn line_skipped(&mut self) {
390        self.lines_skipped += 1;
391    }
392    fn finders_skipped(&mut self, count: u32) {
393        self.finder_lines_skipped += u64::from(count);
394    }
395    fn finder_run(&mut self, finder: usize) {
396        self.finders[finder].lines_run += 1;
397    }
398    fn positions(&mut self, count: usize) {
399        self.positions += count as u64;
400    }
401    fn coarse_position(&mut self) {
402        self.coarse_positions += 1;
403    }
404    fn candidate_position(&mut self) {
405        self.candidate_positions += 1;
406    }
407    fn run_gated(&mut self) {
408        self.run_gated += 1;
409    }
410    fn run_gated_n(&mut self, n: u32) {
411        self.run_gated += u64::from(n);
412    }
413    fn coarse_rejected(&mut self, cur: u8) {
414        if self.coarse_rejected.len() < 256 {
415            self.coarse_rejected.resize(256, 0);
416        }
417        self.coarse_rejected[cur as usize] += 1;
418    }
419    fn find_call(&mut self, finder: usize, hit: bool) {
420        let f = &mut self.finders[finder];
421        f.find_calls += 1;
422        f.hits += u64::from(hit);
423    }
424    fn try_at_call(&mut self, finder: usize, hit: bool) {
425        let f = &mut self.finders[finder];
426        f.try_at_calls += 1;
427        f.hits += u64::from(hit);
428    }
429    fn try_trigger_call(&mut self, finder: usize, hit: bool) {
430        let f = &mut self.finders[finder];
431        f.try_trigger_calls += 1;
432        f.hits += u64::from(hit);
433    }
434    fn matched(&mut self, finder: usize) {
435        self.finders[finder].matches += 1;
436        self.matches += 1;
437    }
438    fn sorted(&mut self) {
439        self.sorts += 1;
440    }
441}
442
443/// How the scanner walks a line. Strategies produce identical matches; they
444/// exist so that alternatives can be measured against each other in the
445/// same process (see the `scanner` bench).
446#[derive(Clone, Copy, Debug, PartialEq, Eq)]
447#[non_exhaustive]
448pub enum Strategy {
449    /// Byte-by-byte dispatch on the current byte only, dispatch and trigger
450    /// finders in separate passes. The original implementation.
451    Legacy,
452    /// Single pass with context gates: a finder is only invoked when the
453    /// previous and next bytes allow a match to start.
454    Gated,
455    /// Context gates evaluated sixteen bytes at a time with SIMD (or with
456    /// `memchr` when the finders start at three bytes or fewer), then the
457    /// exact gates on the surviving positions (default).
458    Vector,
459}
460
461impl Strategy {
462    /// All strategies, in evaluation order.
463    pub const ALL: &[Strategy] = &[Strategy::Legacy, Strategy::Gated, Strategy::Vector];
464
465    /// Short identifier, e.g. for command-line selection.
466    pub fn name(self) -> &'static str {
467        match self {
468            Strategy::Legacy => "legacy",
469            Strategy::Gated => "gated",
470            Strategy::Vector => "vector",
471        }
472    }
473
474    /// Parses [`name`](Strategy::name).
475    pub fn parse(name: &str) -> Option<Strategy> {
476        Strategy::ALL.iter().copied().find(|s| s.name() == name)
477    }
478}
479
480/// Per-line state of the dispatch/trigger pass: where each finder's last
481/// match ended (matches stay disjoint per finder), each finder's memo, and
482/// the run cache. Only the slots of the scanner's finders are initialised:
483/// zeroing all `MAX_FINDERS` slots cost more than scanning a short line.
484struct LineState {
485    finder_pos: [MaybeUninit<usize>; MAX_FINDERS],
486    memos: [MaybeUninit<Memo>; MAX_FINDERS],
487    /// Anchor plan: positions below this were already tried for the finder.
488    tried: [MaybeUninit<usize>; MAX_FINDERS],
489    runs: RunCache,
490    /// Number of initialised slots.
491    len: usize,
492}
493
494impl LineState {
495    #[inline(always)]
496    fn new(len: usize) -> LineState {
497        let mut state = LineState {
498            finder_pos: [MaybeUninit::uninit(); MAX_FINDERS],
499            memos: [MaybeUninit::uninit(); MAX_FINDERS],
500            tried: [MaybeUninit::uninit(); MAX_FINDERS],
501            runs: RunCache::default(),
502            len,
503        };
504        for i in 0..len {
505            state.finder_pos[i].write(0);
506            state.memos[i].write(Memo::default());
507            state.tried[i].write(0);
508        }
509        state
510    }
511
512    #[inline(always)]
513    fn tried(&self, i: usize) -> usize {
514        debug_assert!(i < self.len);
515        // SAFETY: as in `pos`.
516        unsafe { self.tried[i].assume_init() }
517    }
518
519    #[inline(always)]
520    fn set_tried(&mut self, i: usize, pos: usize) {
521        debug_assert!(i < self.len);
522        self.tried[i].write(pos);
523    }
524
525    #[inline(always)]
526    fn pos(&self, i: usize) -> usize {
527        debug_assert!(i < self.len);
528        // SAFETY: `i` is a finder index, and `new` initialised every slot
529        // below `len`, the scanner's finder count.
530        unsafe { self.finder_pos[i].assume_init() }
531    }
532
533    #[inline(always)]
534    fn set_pos(&mut self, i: usize, pos: usize) {
535        debug_assert!(i < self.len);
536        self.finder_pos[i].write(pos);
537    }
538
539    #[inline(always)]
540    fn memo(&mut self, i: usize) -> &mut Memo {
541        debug_assert!(i < self.len);
542        // SAFETY: as in `pos`.
543        unsafe { self.memos[i].assume_init_mut() }
544    }
545}
546
547/// Receives candidate positions from a pass's search.
548trait Sink {
549    fn candidate(&mut self, scanner: &Scanner, pass: &Pass, pos: usize, hint: Option<&RunHint<'_>>);
550    fn stopped(&self) -> bool {
551        false
552    }
553}
554
555/// Per-line scanning: candidates are probed in place.
556struct LineSink<'a, P: Probe> {
557    input: &'a [u8],
558    active_ctx: u32,
559    state: LineState,
560    matches: &'a mut Vec<Match>,
561    probe: &'a mut P,
562}
563
564impl<P: Probe> Sink for LineSink<'_, P> {
565    #[inline(always)]
566    fn candidate(
567        &mut self,
568        scanner: &Scanner,
569        pass: &Pass,
570        pos: usize,
571        hint: Option<&RunHint<'_>>,
572    ) {
573        self.probe.coarse_position();
574        scanner.probe_position(
575            pass,
576            self.input,
577            pos,
578            self.active_ctx,
579            &mut self.state,
580            self.matches,
581            self.probe,
582            hint,
583        );
584    }
585}
586
587/// Where a whole-buffer walk sends the matches of a line.
588trait LineOutput {
589    /// Receives one line's matches, relative to `start`, in candidate
590    /// order; returns whether the walk must stop.
591    fn line(&mut self, start: usize, end: usize, matches: &mut Vec<Match>) -> bool;
592}
593
594/// Emits each line as soon as the walk leaves it.
595struct Stream<'a, F: FnMut(usize, usize, &[Match]) -> bool>(&'a mut F);
596
597impl<F: FnMut(usize, usize, &[Match]) -> bool> LineOutput for Stream<'_, F> {
598    #[inline(always)]
599    fn line(&mut self, start: usize, end: usize, matches: &mut Vec<Match>) -> bool {
600        sort_matches(matches);
601        (self.0)(start, end, matches)
602    }
603}
604
605/// Keeps every match with absolute positions, for merging with other passes.
606struct Collect<'a>(&'a mut Vec<Match>);
607
608impl LineOutput for Collect<'_> {
609    #[inline(always)]
610    fn line(&mut self, start: usize, _end: usize, matches: &mut Vec<Match>) -> bool {
611        for m in matches.drain(..) {
612            self.0.push(Match {
613                finder_index: m.finder_index,
614                range: m.range.start + start..m.range.end + start,
615            });
616        }
617        false
618    }
619}
620
621/// Whole-buffer walk of a pass whose finders are bound to lines: the line
622/// around each candidate is resolved lazily and its matches go to `out`
623/// once the walk leaves it.
624struct BufferSink<'a, O: LineOutput> {
625    data: &'a [u8],
626    line_start: usize,
627    /// End of the current line without its terminator and trailing `\r`s.
628    line_end: usize,
629    /// Start of the next line (past the terminator).
630    line_next: usize,
631    active_ctx: u32,
632    state: LineState,
633    matches: Vec<Match>,
634    out: O,
635    stopped: bool,
636}
637
638impl<'a, O: LineOutput> BufferSink<'a, O> {
639    fn new(data: &'a [u8], scanner: &Scanner, out: O) -> Self {
640        BufferSink {
641            data,
642            line_start: 0,
643            line_end: 0,
644            line_next: 0,
645            active_ctx: 0,
646            state: LineState::new(scanner.finders.len()),
647            matches: Vec::new(),
648            out,
649            stopped: false,
650        }
651    }
652
653    #[inline(always)]
654    fn flush(&mut self) {
655        if self.matches.is_empty() || self.stopped {
656            return;
657        }
658        self.stopped = self
659            .out
660            .line(self.line_start, self.line_end, &mut self.matches);
661        self.matches.clear();
662    }
663
664    /// Makes the line holding `pos` current.
665    #[inline(always)]
666    fn enter_line(&mut self, scanner: &Scanner, pass: &Pass, pos: usize) {
667        let data = self.data;
668        self.line_start = memchr::memrchr(b'\n', &data[..pos]).map_or(0, |nl| nl + 1);
669        let nl = memchr::memchr(b'\n', &data[pos..]).map_or(data.len(), |i| pos + i);
670        self.line_next = (nl + 1).min(data.len());
671        let mut end = nl;
672        while end > self.line_start && data[end - 1] == b'\r' {
673            end -= 1;
674        }
675        self.line_end = end;
676        self.state = LineState::new(scanner.finders.len());
677        let line = &data[self.line_start..self.line_end];
678        self.active_ctx = scanner.buffer_active(pass, line);
679    }
680}
681
682impl<O: LineOutput> Sink for BufferSink<'_, O> {
683    #[inline(always)]
684    fn candidate(
685        &mut self,
686        scanner: &Scanner,
687        pass: &Pass,
688        pos: usize,
689        hint: Option<&RunHint<'_>>,
690    ) {
691        if pos >= self.line_next {
692            self.flush();
693            if self.stopped {
694                return;
695            }
696            self.enter_line(scanner, pass, pos);
697        }
698        if pos >= self.line_end || self.active_ctx == 0 {
699            return;
700        }
701        let line = &self.data[self.line_start..self.line_end];
702        scanner.probe_position(
703            pass,
704            line,
705            pos - self.line_start,
706            self.active_ctx,
707            &mut self.state,
708            &mut self.matches,
709            &mut (),
710            hint,
711        );
712    }
713
714    #[inline(always)]
715    fn stopped(&self) -> bool {
716        self.stopped
717    }
718}
719
720/// Whole-buffer walk of a pass whose finders are line-agnostic: candidates
721/// are probed with absolute positions and no line is resolved until the
722/// matches are grouped for emission.
723struct WholeSink<'a> {
724    data: &'a [u8],
725    state: LineState,
726    matches: &'a mut Vec<Match>,
727}
728
729impl Sink for WholeSink<'_> {
730    #[inline(always)]
731    fn candidate(
732        &mut self,
733        scanner: &Scanner,
734        pass: &Pass,
735        pos: usize,
736        hint: Option<&RunHint<'_>>,
737    ) {
738        let active_ctx = scanner.dispatch_mask | scanner.trigger_mask;
739        scanner.probe_position(
740            pass,
741            self.data,
742            pos,
743            active_ctx,
744            &mut self.state,
745            self.matches,
746            &mut (),
747            hint,
748        );
749    }
750}
751
752/// Orders matches as `scan_line` reports them: by start, then finder.
753#[inline]
754fn sort_matches(matches: &mut [Match]) {
755    let sorted = matches
756        .windows(2)
757        .all(|w| (w[0].range.start, w[0].finder_index) <= (w[1].range.start, w[1].finder_index));
758    if !sorted {
759        // Lists are nearly sorted (a trigger match may start before the
760        // previous one); the stable sort exploits the existing runs.
761        matches.sort_by(|a, b| {
762            a.range
763                .start
764                .cmp(&b.range.start)
765                .then(a.finder_index.cmp(&b.finder_index))
766        });
767    }
768}
769
770/// Merges per-pass match lists, each sorted, into `scan_line` order.
771fn merge_matches(mut lists: Vec<Vec<Match>>) -> Vec<Match> {
772    lists.retain(|list| !list.is_empty());
773    if lists.len() <= 1 {
774        return lists.pop().unwrap_or_default();
775    }
776    let total = lists.iter().map(Vec::len).sum();
777    let mut out = Vec::with_capacity(total);
778    let mut heads = vec![0usize; lists.len()];
779    loop {
780        let mut best: Option<usize> = None;
781        for (k, list) in lists.iter().enumerate() {
782            if heads[k] >= list.len() {
783                continue;
784            }
785            let m = &list[heads[k]];
786            let better = match best {
787                None => true,
788                Some(b) => {
789                    let head = &lists[b][heads[b]];
790                    (m.range.start, m.finder_index) < (head.range.start, head.finder_index)
791                }
792            };
793            if better {
794                best = Some(k);
795            }
796        }
797        let Some(b) = best else { break };
798        out.push(lists[b][heads[b]].clone());
799        heads[b] += 1;
800    }
801    out
802}
803
804/// Emits absolute matches, sorted in `scan_line` order, grouped per line
805/// with positions relative to the line. Returns whether `emit` stopped.
806fn emit_grouped(
807    data: &[u8],
808    matches: Vec<Match>,
809    emit: &mut impl FnMut(usize, usize, &[Match]) -> bool,
810) -> bool {
811    let mut line_start = 0;
812    let mut line_end = 0;
813    let mut line_next = 0;
814    let mut group: Vec<Match> = Vec::new();
815    for m in matches {
816        if m.range.start >= line_next || group.is_empty() {
817            if !group.is_empty() && emit(line_start, line_end, &group) {
818                return true;
819            }
820            group.clear();
821            line_start = memchr::memrchr(b'\n', &data[..m.range.start]).map_or(0, |nl| nl + 1);
822            let nl = memchr::memchr(b'\n', &data[m.range.start..])
823                .map_or(data.len(), |i| m.range.start + i);
824            line_next = (nl + 1).min(data.len());
825            line_end = nl;
826            while line_end > line_start && data[line_end - 1] == b'\r' {
827                line_end -= 1;
828            }
829        }
830        group.push(Match {
831            finder_index: m.finder_index,
832            range: m.range.start - line_start..m.range.end - line_start,
833        });
834    }
835    if !group.is_empty() && emit(line_start, line_end, &group) {
836        return true;
837    }
838    false
839}
840
841/// How a pass finds its candidate positions.
842#[derive(Clone, Copy, Debug, PartialEq, Eq)]
843enum Search {
844    One(u8),
845    Two(u8, u8),
846    Three(u8, u8, u8),
847    /// The pass's `literals`, ASCII case ignored.
848    Prefixes,
849    Blocks,
850}
851
852/// Frequency rank (see `search::rank`) from which searching a finder's
853/// literal prefixes beats searching its start or anchor bytes: every
854/// letter ranks above, `J` below.
855const PREFIX_RANK: u8 = 160;
856
857/// One search over the input serving a subset of the finders: `memchr` for
858/// up to three bytes (trigger bytes, start bytes or anchor bytes), a few
859/// literals starting every candidate, or the block classifier. Passes have
860/// disjoint finder sets.
861struct Pass {
862    search: Search,
863    /// Finders served by this pass.
864    finders: u32,
865    /// Dispatch finders of this pass reached by walking back from an anchor
866    /// byte rather than probed at their start bytes.
867    anchored: u32,
868    /// Every finder of the pass is line-agnostic, so a buffer can be probed
869    /// with absolute positions and lines resolved only around matches.
870    whole: bool,
871    /// Coarse rules of a `Search::Blocks` pass.
872    rules: Option<Rules>,
873    /// Bytes that may precede a searched byte where a finder of a `memchr`
874    /// pass can start or have its first anchor byte (a superset; `\n`
875    /// always, as a line start has no previous byte), or `None` when every
876    /// byte may.
877    prev: Option<NibbleSet>,
878    /// Anchor checks of a `memchr` pass the search tests on its masks: a
879    /// searched byte whose every finder is an anchored one with a check
880    /// over searched bytes only needs one of those checks to pass.
881    checks: search::Checks,
882    /// Literals of a `Search::Prefixes` pass.
883    literals: Option<search::Literals>,
884    /// Every dispatch finder of a `Search::Blocks` pass that can start at a
885    /// hex digit needs a hex run of at least this many bytes (0 when one
886    /// needs less): the block stage drops shorter runs before any per-lane
887    /// work.
888    min_hex_run: usize,
889}
890
891impl Pass {
892    /// The search, for diagnostics: `blocks`, `start bytes(:@)` or
893    /// `anchors(-)`.
894    fn describe(&self) -> String {
895        let show = |list: &[u8]| -> String {
896            list.iter()
897                .map(|&b| {
898                    if b.is_ascii_graphic() {
899                        (b as char).to_string()
900                    } else {
901                        format!("\\x{b:02x}")
902                    }
903                })
904                .collect()
905        };
906        let what = if self.anchored != 0 {
907            "anchors"
908        } else {
909            "start bytes"
910        };
911        match self.search {
912            Search::One(a) => format!("{what}({})", show(&[a])),
913            Search::Two(a, b) => format!("{what}({})", show(&[a, b])),
914            Search::Three(a, b, c) => format!("{what}({})", show(&[a, b, c])),
915            Search::Prefixes => "prefixes".to_string(),
916            Search::Blocks => "blocks".to_string(),
917        }
918    }
919}
920
921/// Most `memchr` passes a scanner runs; the finders that do not fit go to
922/// the block pass.
923const MAX_MEMCHR_PASSES: usize = 3;
924
925/// Longest word run measured for [`RunClass::Word`](crate::RunClass::Word)
926/// rules; longer runs report as capped.
927const WORD_CAP: usize = 16;
928
929pub struct Scanner {
930    finders: Vec<Box<dyn Finder>>,
931    strategy: Strategy,
932    backend: classify::Kind,
933    /// The searches run over each line or buffer, in order.
934    passes: Vec<Pass>,
935    anchors: Vec<Option<Anchor>>,
936    /// Each finder's start bytes (`could_start_at`).
937    starts: Vec<ByteSet>,
938    /// Start bytes per dispatch finder (`Strategy::Legacy`).
939    dispatch: [u32; 256],
940    /// Trigger bytes per trigger finder (`Strategy::Legacy`).
941    trigger: [u32; 256],
942    /// Candidates by (class of previous byte, current byte); row `CTX_NONE`
943    /// is the start of the line. Covers dispatch and trigger finders.
944    gate_prev: Box<[[u32; 256]; CTX_CLASSES]>,
945    /// Candidates by (current byte, class of next byte); column `CTX_NONE`
946    /// is the end of the line.
947    gate_next: Box<[[u32; CTX_CLASSES]; 256]>,
948    dispatch_mask: u32,
949    trigger_mask: u32,
950    /// Trigger finders with a tabulated context gate.
951    context_mask: u32,
952    /// Per finder, a bitset over the two previous bytes
953    /// (`prev2 << 8 | prev1`) that may precede a trigger, and the classes
954    /// of the next byte that exempt a trigger from that condition.
955    contexts: Vec<Option<(Box<[u64; 1024]>, u32)>>,
956    scan_mask: u32,
957    /// Finders with digit or hex run rules.
958    run_mask: u32,
959    run_rules: Vec<Vec<RunRule>>,
960    /// Finders with a digit or hex rule applicable at a start byte.
961    rule_cur: Box<[u32; 256]>,
962    /// Finders with a digit-class (resp. hex-class) rule accepting the byte
963    /// after the run; index 256 stands for the end of the input. A finder
964    /// absent from both entries for a candidate's runs cannot accept it,
965    /// so the rules are only evaluated for the others.
966    after_digit: Box<[u32; 257]>,
967    after_hex: Box<[u32; 257]>,
968    /// Finders with a digit-class (resp. hex-class) rule accepting a run
969    /// of the indexed length (up to `RUN_CAP`).
970    len_digit: Box<[u32; 130]>,
971    len_hex: Box<[u32; 130]>,
972    /// Finders with digit-class (resp. hex-class) rules: a capped run may
973    /// satisfy any of them.
974    digit_ruled: u32,
975    hex_ruled: u32,
976    /// Longest run any rule needs to distinguish, plus one.
977    run_cap: usize,
978    /// Finders with a [`RunClass::Word`] rule: the word run is measured
979    /// only for their candidates.
980    word_mask: u32,
981    /// Longest word run any rule needs to distinguish, plus one.
982    word_cap: usize,
983    skip_requirements: Vec<&'static [(u16, bool)]>,
984}
985
986impl Scanner {
987    pub fn new(finders: Vec<Box<dyn Finder>>) -> Self {
988        Self::try_new(finders).expect("too many finders")
989    }
990
991    pub fn try_new(finders: Vec<Box<dyn Finder>>) -> Result<Self, ScannerError> {
992        if finders.len() > MAX_FINDERS {
993            return Err(ScannerError::TooManyFinders {
994                len: finders.len(),
995                max: MAX_FINDERS,
996            });
997        }
998
999        let mut dispatch = [0u32; 256];
1000        let mut trigger = [0u32; 256];
1001        let mut dispatch_mask = 0u32;
1002        let mut trigger_mask = 0u32;
1003        let mut scan_mask = 0u32;
1004
1005        let skip_requirements: Vec<&'static [(u16, bool)]> =
1006            finders.iter().map(|f| required_classes(f.id())).collect();
1007        let run_rules: Vec<Vec<RunRule>> = finders
1008            .iter()
1009            .map(|f| {
1010                if f.dispatchable() || f.triggerable() {
1011                    f.run_rules()
1012                } else {
1013                    Vec::new()
1014                }
1015            })
1016            .collect();
1017        // A finder's rules are either all on the word run or all on the
1018        // digit/hex runs, so each kind is measured only when a candidate
1019        // finder needs it.
1020        for (i, rules) in run_rules.iter().enumerate() {
1021            let words = rules
1022                .iter()
1023                .filter(|r| r.class == crate::RunClass::Word)
1024                .count();
1025            assert!(
1026                words == 0 || words == rules.len(),
1027                "finder {} mixes word rules with digit or hex rules",
1028                finders[i].id()
1029            );
1030        }
1031        let run_mask = run_rules
1032            .iter()
1033            .enumerate()
1034            .filter(|(_, rules)| rules.iter().any(|r| r.class != crate::RunClass::Word))
1035            .fold(0u32, |mask, (i, _)| mask | (1u32 << i));
1036        let mut rule_cur = Box::new([0u32; 256]);
1037        let mut after_digit = Box::new([0u32; 257]);
1038        let mut after_hex = Box::new([0u32; 257]);
1039        let mut len_digit = Box::new([0u32; 130]);
1040        let mut len_hex = Box::new([0u32; 130]);
1041        let mut digit_ruled = 0u32;
1042        let mut hex_ruled = 0u32;
1043        for (i, rules) in run_rules.iter().enumerate() {
1044            let bit = 1u32 << i;
1045            for rule in rules {
1046                let (after, lens, ruled) = match rule.class {
1047                    crate::RunClass::Digit => (&mut after_digit, &mut len_digit, &mut digit_ruled),
1048                    crate::RunClass::Hex => (&mut after_hex, &mut len_hex, &mut hex_ruled),
1049                    crate::RunClass::Word => continue,
1050                };
1051                *ruled |= bit;
1052                for len in rule.min..=rule.max.min(crate::RUN_CAP) {
1053                    if rule.lengths == 0 || len >= 64 || rule.lengths & (1u64 << len) != 0 {
1054                        lens[len as usize] |= bit;
1055                    }
1056                }
1057                for b in 0..=255u8 {
1058                    if rule.cur.contains(b) {
1059                        rule_cur[b as usize] |= bit;
1060                    }
1061                    if rule.after.contains(b) {
1062                        after[b as usize] |= bit;
1063                    }
1064                }
1065                if rule.after_end {
1066                    after[256] |= bit;
1067                }
1068            }
1069        }
1070        // A run longer than every rule's `max` is rejected whatever follows
1071        // it, so measuring stops one byte past the longest `max`.
1072        let run_cap = run_rules
1073            .iter()
1074            .flatten()
1075            .filter(|rule| rule.class != crate::RunClass::Word)
1076            .map(|rule| rule.max as usize + 1)
1077            .max()
1078            .unwrap_or(1)
1079            .min(crate::RUN_CAP as usize);
1080        let word_mask = run_rules
1081            .iter()
1082            .enumerate()
1083            .filter(|(_, rules)| rules.iter().any(|r| r.class == crate::RunClass::Word))
1084            .fold(0u32, |mask, (i, _)| mask | (1u32 << i));
1085        // A word run is measured up to 16 bytes: enough to check the length
1086        // and the following byte for every mnemonic-sized rule, while a
1087        // longer run reports as capped and is accepted whatever the rule's
1088        // maximum (a rule's minimum never exceeds the cap).
1089        let word_cap = run_rules
1090            .iter()
1091            .flatten()
1092            .filter(|rule| rule.class == crate::RunClass::Word)
1093            .map(|rule| (rule.max as usize + 1).min(WORD_CAP).max(rule.min as usize))
1094            .max()
1095            .unwrap_or(1);
1096
1097        let mut gate_prev = Box::new([[0u32; 256]; CTX_CLASSES]);
1098        let mut gate_next = Box::new([[0u32; CTX_CLASSES]; 256]);
1099
1100        for (i, finder) in finders.iter().enumerate() {
1101            let bit = 1u32 << i;
1102            if finder.dispatchable() {
1103                dispatch_mask |= bit;
1104                for cur in 0..=255u8 {
1105                    if !finder.could_start_at(cur) {
1106                        continue;
1107                    }
1108                    dispatch[cur as usize] |= bit;
1109                    // A class is allowed as soon as one of its bytes is: the
1110                    // gate only skips contexts every byte of the class rules
1111                    // out, so it can never lose a match.
1112                    gate_prev[CTX_NONE][cur as usize] |= bit;
1113                    gate_next[cur as usize][CTX_NONE] |= bit;
1114                    for other in 0..=255u8 {
1115                        if finder.could_start_after(other, cur) {
1116                            gate_prev[ctx_class(other)][cur as usize] |= bit;
1117                        }
1118                        if finder.could_continue_with(cur, other) {
1119                            gate_next[cur as usize][ctx_class(other)] |= bit;
1120                        }
1121                    }
1122                }
1123            } else if finder.triggerable() {
1124                trigger_mask |= bit;
1125                for cur in 0..=255u8 {
1126                    if !finder.could_trigger_at(cur) {
1127                        continue;
1128                    }
1129                    trigger[cur as usize] |= bit;
1130                    gate_prev[CTX_NONE][cur as usize] |= bit;
1131                    gate_next[cur as usize][CTX_NONE] |= bit;
1132                    for other in 0..=255u8 {
1133                        if finder.could_start_after(other, cur) {
1134                            gate_prev[ctx_class(other)][cur as usize] |= bit;
1135                        }
1136                        if finder.could_continue_with(cur, other) {
1137                            gate_next[cur as usize][ctx_class(other)] |= bit;
1138                        }
1139                    }
1140                }
1141            } else {
1142                scan_mask |= bit;
1143            }
1144        }
1145
1146        let mut context_mask = 0u32;
1147        let contexts: Vec<Option<(Box<[u64; 1024]>, u32)>> = finders
1148            .iter()
1149            .enumerate()
1150            .map(|(i, f)| {
1151                if !f.triggerable() || !f.has_trigger_context() {
1152                    return None;
1153                }
1154                context_mask |= 1u32 << i;
1155                let mut pairs = Box::new([0u64; 1024]);
1156                for prev2 in 0..=255u8 {
1157                    for prev1 in 0..=255u8 {
1158                        if f.trigger_context(prev2, prev1) {
1159                            let index = usize::from(prev2) << 8 | usize::from(prev1);
1160                            pairs[index >> 6] |= 1 << (index & 63);
1161                        }
1162                    }
1163                }
1164                // A class is exempt when any of its bytes is; the end of the
1165                // input is class CTX_NONE.
1166                let mut exempt = 0u32;
1167                for b in 0..=255u8 {
1168                    if f.trigger_context_exempt(Some(b)) {
1169                        exempt |= 1 << ctx_class(b);
1170                    }
1171                }
1172                if f.trigger_context_exempt(None) {
1173                    exempt |= 1 << CTX_NONE;
1174                }
1175                Some((pairs, exempt))
1176            })
1177            .collect();
1178        let anchors: Vec<Option<Anchor>> = finders
1179            .iter()
1180            .map(|f| if f.dispatchable() { f.anchor() } else { None })
1181            .collect();
1182        for (i, anchor) in anchors.iter().enumerate() {
1183            // An exact offset relies on the walk stopping at anchor bytes:
1184            // a position before a probed anchor can then never start a
1185            // match whose first anchor lies beyond it.
1186            if let Some(anchor) = anchor
1187                && anchor.back.is_some()
1188            {
1189                assert!(
1190                    (0..=255u8).all(|b| !(anchor.bytes.contains(b) && anchor.walk.contains(b))),
1191                    "finder {} anchors on a walk byte with an exact offset",
1192                    finders[i].id()
1193                );
1194            }
1195        }
1196        let starts: Vec<ByteSet> = finders
1197            .iter()
1198            .map(|f| ByteSet::from_fn(|b| f.dispatchable() && f.could_start_at(b)))
1199            .collect();
1200        let ctx_mask = dispatch_mask | trigger_mask;
1201
1202        // Coarse rules of a block pass: for each finder and start byte, the
1203        // previous/next byte classes its gates accept, mapped to the vector
1204        // categories. Categories are coarser than classes and unions only
1205        // widen, so the block stage always yields a superset of the exact
1206        // gates.
1207        let build_rules = |mask: u32| -> Rules {
1208            let mut items = Vec::new();
1209            for i in 0..finders.len() {
1210                let bit = 1u32 << i;
1211                if bit & mask == 0 {
1212                    continue;
1213                }
1214                for b in 0..=255u8 {
1215                    if gate_prev[CTX_NONE][b as usize] & bit == 0 {
1216                        continue;
1217                    }
1218                    let mut prev = 0u8;
1219                    let mut next = 0u8;
1220                    for class in 0..CTX_NONE {
1221                        if gate_prev[class][b as usize] & bit != 0 {
1222                            prev |= ctx_class_category(class);
1223                        }
1224                        if gate_next[b as usize][class] & bit != 0 {
1225                            next |= ctx_class_category(class);
1226                        }
1227                    }
1228                    items.push((b, prev, next));
1229                }
1230            }
1231            Rules::build(items)
1232        };
1233        // Shortest hex run any hex-starting dispatch finder of `mask` can
1234        // match: only meaningful when each such finder has hex-class rules
1235        // covering all its hex start bytes, so that a shorter run rules
1236        // every finder out. 0 when the filter cannot apply.
1237        let min_hex_run = |mask: u32| -> usize {
1238            let mut min_hex_run = usize::MAX;
1239            for (i, finder) in finders.iter().enumerate() {
1240                if mask & (1u32 << i) == 0 || !finder.dispatchable() {
1241                    continue;
1242                }
1243                let hex_starts: Vec<u8> = (0..=255u8)
1244                    .filter(|&b| b.is_ascii_hexdigit() && finder.could_start_at(b))
1245                    .collect();
1246                if hex_starts.is_empty() {
1247                    continue;
1248                }
1249                let mut finder_min = usize::MAX;
1250                for &b in &hex_starts {
1251                    let mut byte_min = usize::MAX;
1252                    let mut covered = false;
1253                    for rule in &run_rules[i] {
1254                        if !rule.cur.contains(b) {
1255                            continue;
1256                        }
1257                        covered = true;
1258                        match rule.class {
1259                            crate::RunClass::Hex => byte_min = byte_min.min(rule.min as usize),
1260                            // A digit or word rule accepts runs the hex run
1261                            // does not bound.
1262                            crate::RunClass::Digit | crate::RunClass::Word => byte_min = 1,
1263                        }
1264                    }
1265                    if !covered {
1266                        byte_min = 1;
1267                    }
1268                    finder_min = finder_min.min(byte_min);
1269                }
1270                min_hex_run = min_hex_run.min(finder_min);
1271            }
1272            if min_hex_run == usize::MAX || min_hex_run < 2 {
1273                0
1274            } else {
1275                min_hex_run
1276            }
1277        };
1278
1279        // Passes. A finder is searched with `memchr` when a few bytes locate
1280        // every match: its trigger bytes, its start bytes when they number
1281        // three or fewer, or its anchor bytes. The rest needs the block
1282        // classifier, which tests extra start bytes for free, so when a
1283        // block pass runs anyway the `memchr` finders join it unless they
1284        // would weaken its hex-run filter. `memchr` finders sharing bytes
1285        // are grouped three bytes per pass, at most MAX_MEMCHR_PASSES
1286        // passes; the overflow goes to the block pass.
1287        let bytes_of =
1288            |set: &dyn Fn(u8) -> bool| -> Vec<u8> { (0..=255u8).filter(|&b| set(b)).collect() };
1289        let few = |bytes: &[u8]| (1..=3).contains(&bytes.len());
1290        let search_bytes = |i: usize| -> Option<(Vec<u8>, bool)> {
1291            let finder = &finders[i];
1292            let bit = 1u32 << i;
1293            if trigger_mask & bit != 0 {
1294                let bytes = bytes_of(&|b| finder.could_trigger_at(b));
1295                return few(&bytes).then_some((bytes, false));
1296            }
1297            if dispatch_mask & bit == 0 {
1298                return None;
1299            }
1300            // Start bytes or anchor bytes, whichever are rarer (`J`
1301            // rather than `e` for a JWT).
1302            let bytes = bytes_of(&|b| starts[i].contains(b));
1303            let anchored = anchors[i]
1304                .map(|anchor| bytes_of(&|b| anchor.bytes.contains(b)))
1305                .filter(|bytes| few(bytes));
1306            match anchored {
1307                Some(anchor) if !few(&bytes) || search::rank(&anchor) < search::rank(&bytes) => {
1308                    Some((anchor, true))
1309                }
1310                _ => few(&bytes).then_some((bytes, false)),
1311            }
1312        };
1313        let mut cheap: Vec<(usize, Vec<u8>, bool)> = Vec::new();
1314        let mut block_mask = 0u32;
1315        // Finders the block classifier would serve, found by a literal
1316        // search instead when they name their candidates' prefixes.
1317        let mut prefix_mask = 0u32;
1318        let mut prefixes: Vec<Vec<u8>> = Vec::new();
1319        for (i, finder) in finders.iter().enumerate() {
1320            let bit = 1u32 << i;
1321            if ctx_mask & bit == 0 {
1322                continue;
1323            }
1324            let bytes = search_bytes(i);
1325            // Prefixes beat the block classifier, and bytes as frequent as
1326            // letters (`t` for `todo`).
1327            let own = match &bytes {
1328                Some((bytes, _)) if search::rank(bytes) < PREFIX_RANK => Vec::new(),
1329                _ if dispatch_mask & bit != 0 => finder.prefixes(),
1330                _ => Vec::new(),
1331            };
1332            let mut joined = prefixes.clone();
1333            joined.extend(own.iter().cloned());
1334            if !own.is_empty() && search::Literals::new(&joined).is_some() {
1335                prefix_mask |= bit;
1336                prefixes = joined;
1337                continue;
1338            }
1339            match bytes {
1340                Some((bytes, anchored)) => cheap.push((i, bytes, anchored)),
1341                None => block_mask |= bit,
1342            }
1343        }
1344        // A block pass running anyway classifies their start bytes for free.
1345        let literals = if block_mask != 0 {
1346            block_mask |= prefix_mask;
1347            prefix_mask = 0;
1348            None
1349        } else {
1350            search::Literals::new(&prefixes)
1351        };
1352        if block_mask != 0 {
1353            let base = min_hex_run(block_mask);
1354            let mut joined = block_mask;
1355            cheap.retain(|&(i, _, _)| {
1356                let bit = 1u32 << i;
1357                let weakens = base >= 2 && min_hex_run(joined | bit) < 2;
1358                if !weakens {
1359                    joined |= bit;
1360                }
1361                weakens
1362            });
1363            block_mask = joined;
1364        }
1365        cheap.sort_by_key(|(_, bytes, _)| bytes.len());
1366        let mut groups: Vec<(Vec<u8>, u32, u32)> = Vec::new();
1367        for (i, bytes, anchored) in cheap {
1368            let bit = 1u32 << i;
1369            let anchored_bit = if anchored { bit } else { 0 };
1370            let fits = groups.iter_mut().find(|group| {
1371                let extra = bytes.iter().filter(|b| !group.0.contains(b)).count();
1372                group.0.len() + extra <= 3
1373            });
1374            if let Some(group) = fits {
1375                for &b in &bytes {
1376                    if !group.0.contains(&b) {
1377                        group.0.push(b);
1378                    }
1379                }
1380                group.1 |= bit;
1381                group.2 |= anchored_bit;
1382            } else if groups.len() < MAX_MEMCHR_PASSES {
1383                groups.push((bytes, bit, anchored_bit));
1384            } else {
1385                block_mask |= bit;
1386            }
1387        }
1388        let whole_for = |mask: u32| {
1389            (0..finders.len())
1390                .filter(|&i| mask & (1u32 << i) != 0)
1391                .all(|i| finders[i].line_agnostic())
1392        };
1393        // Bytes before a searched byte where a finder may start: the
1394        // finder's own gate, and the pairs of its context table when no
1395        // next byte exempts a trigger from it. An anchor whose matches
1396        // start a fixed number of bytes before their first anchor byte has
1397        // a walk byte right before that one.
1398        let prev_filter = |bytes: &[u8], mask: u32, anchored: u32| -> Option<NibbleSet> {
1399            let mut members = [false; 256];
1400            members[usize::from(b'\n')] = true;
1401            for (i, f) in finders.iter().enumerate() {
1402                if mask & (1u32 << i) == 0 {
1403                    continue;
1404                }
1405                if anchored & (1u32 << i) != 0 {
1406                    let anchor = anchors[i]
1407                        .as_ref()
1408                        .expect("an anchored finder has an anchor");
1409                    if !anchor.back.is_some_and(|back| back >= 1) {
1410                        return None;
1411                    }
1412                    for prev in 0..=255u8 {
1413                        if anchor.walk.contains(prev) {
1414                            members[usize::from(prev)] = true;
1415                        }
1416                    }
1417                    continue;
1418                }
1419                let context = match &contexts[i] {
1420                    Some((pairs, 0)) => {
1421                        let mut allowed = [false; 256];
1422                        for (word, &bits) in pairs.iter().enumerate() {
1423                            for bit in 0..64 {
1424                                if bits & (1 << bit) != 0 {
1425                                    allowed[(word * 64 + bit) & 255] = true;
1426                                }
1427                            }
1428                        }
1429                        Some(allowed)
1430                    }
1431                    _ => None,
1432                };
1433                for &cur in bytes {
1434                    let starts = if f.dispatchable() {
1435                        f.could_start_at(cur)
1436                    } else {
1437                        f.triggerable() && f.could_trigger_at(cur)
1438                    };
1439                    if !starts {
1440                        continue;
1441                    }
1442                    for prev in 0..=255u8 {
1443                        if f.could_start_after(prev, cur)
1444                            && context.is_none_or(|allowed| allowed[usize::from(prev)])
1445                        {
1446                            members[usize::from(prev)] = true;
1447                        }
1448                    }
1449                }
1450            }
1451            NibbleSet::new(&members)
1452        };
1453        let search_checks = |bytes: &[u8], mask: u32, anchored: u32| -> search::Checks {
1454            let mut checks = search::Checks::default();
1455            // The searched bytes of a set, or `None` when it holds others.
1456            let indices = |set: &crate::ByteSet| -> Option<u8> {
1457                if (0..=255u8).any(|b| set.contains(b) && !bytes.contains(&b)) {
1458                    return None;
1459                }
1460                Some(
1461                    (0..bytes.len())
1462                        .filter(|&j| set.contains(bytes[j]))
1463                        .fold(0u8, |m, j| m | 1 << j),
1464                )
1465            };
1466            for (k, &b) in bytes.iter().enumerate() {
1467                let (mut used, mut conditional) = (false, true);
1468                for (i, f) in finders.iter().enumerate() {
1469                    let bit = 1u32 << i;
1470                    if mask & bit == 0 {
1471                        continue;
1472                    }
1473                    if anchored & bit == 0 {
1474                        let starts = if f.dispatchable() {
1475                            f.could_start_at(b)
1476                        } else {
1477                            f.triggerable() && f.could_trigger_at(b)
1478                        };
1479                        if starts {
1480                            used = true;
1481                            conditional = false;
1482                        }
1483                        continue;
1484                    }
1485                    let anchor = anchors[i]
1486                        .as_ref()
1487                        .expect("an anchored finder has an anchor");
1488                    if !anchor.bytes.contains(b) {
1489                        continue;
1490                    }
1491                    used = true;
1492                    let Some((check, bytes_mask)) = anchor
1493                        .checks
1494                        .iter()
1495                        .flatten()
1496                        .find(|check| check.anchors.contains(b))
1497                        .and_then(|check| Some((check, indices(&check.bytes)?)))
1498                        .filter(|(check, _)| check.offsets().iter().all(|&o| o < 64))
1499                    else {
1500                        conditional = false;
1501                        continue;
1502                    };
1503                    let entry = search::Check {
1504                        hits: 1 << k,
1505                        bytes: bytes_mask,
1506                        offsets: check.offsets,
1507                        count: check.offset_count,
1508                        all: check.all,
1509                    };
1510                    let same = |e: &&mut search::Check| {
1511                        (e.bytes, e.offsets, e.count, e.all)
1512                            == (entry.bytes, entry.offsets, entry.count, entry.all)
1513                    };
1514                    if let Some(existing) = checks.list.iter_mut().flatten().find(same) {
1515                        existing.hits |= 1 << k;
1516                    } else if let Some(slot) = checks.list.iter_mut().find(|e| e.is_none()) {
1517                        *slot = Some(entry);
1518                    } else {
1519                        conditional = false;
1520                    }
1521                }
1522                if used && conditional {
1523                    checks.conditional |= 1 << k;
1524                }
1525            }
1526            checks
1527        };
1528        let mut passes = Vec::new();
1529        for (mut bytes, mask, anchored) in groups {
1530            bytes.sort_unstable();
1531            let prev = prev_filter(&bytes, mask, anchored);
1532            let checks = search_checks(&bytes, mask, anchored);
1533            let search = match bytes.as_slice() {
1534                [a] => Search::One(*a),
1535                [a, b] => Search::Two(*a, *b),
1536                [a, b, c] => Search::Three(*a, *b, *c),
1537                _ => unreachable!("a memchr pass searches at most three bytes"),
1538            };
1539            passes.push(Pass {
1540                search,
1541                finders: mask,
1542                anchored,
1543                whole: whole_for(mask),
1544                rules: None,
1545                prev,
1546                checks,
1547                literals: None,
1548                min_hex_run: 0,
1549            });
1550        }
1551        if prefix_mask != 0 {
1552            passes.push(Pass {
1553                search: Search::Prefixes,
1554                finders: prefix_mask,
1555                anchored: 0,
1556                whole: whole_for(prefix_mask),
1557                rules: None,
1558                prev: None,
1559                checks: search::Checks::default(),
1560                literals,
1561                min_hex_run: 0,
1562            });
1563        }
1564        if block_mask != 0 {
1565            passes.push(Pass {
1566                search: Search::Blocks,
1567                finders: block_mask,
1568                anchored: 0,
1569                whole: whole_for(block_mask),
1570                rules: Some(build_rules(block_mask)),
1571                prev: None,
1572                checks: search::Checks::default(),
1573                literals: None,
1574                min_hex_run: min_hex_run(block_mask),
1575            });
1576        }
1577
1578        Ok(Scanner {
1579            finders,
1580            strategy: Strategy::Vector,
1581            backend: classify::detect(),
1582            passes,
1583            anchors,
1584            starts,
1585            dispatch,
1586            trigger,
1587            gate_prev,
1588            gate_next,
1589            dispatch_mask,
1590            trigger_mask,
1591            context_mask,
1592            contexts,
1593            scan_mask,
1594            run_mask,
1595            run_rules,
1596            rule_cur,
1597            after_digit,
1598            after_hex,
1599            len_digit,
1600            len_hex,
1601            digit_ruled,
1602            hex_ruled,
1603            run_cap,
1604            word_mask,
1605            word_cap,
1606            skip_requirements,
1607        })
1608    }
1609
1610    pub fn finders(&self) -> &[Box<dyn Finder>] {
1611        &self.finders
1612    }
1613
1614    /// The line-walking strategy in use.
1615    pub fn strategy(&self) -> Strategy {
1616        self.strategy
1617    }
1618
1619    /// Selects the line-walking strategy; results do not depend on it.
1620    pub fn set_strategy(&mut self, strategy: Strategy) {
1621        self.strategy = strategy;
1622    }
1623
1624    /// Name of the block classifier used by [`Strategy::Vector`]
1625    /// (`"neon"`, `"ssse3"` or `"scalar"`), for diagnostics.
1626    pub fn backend(&self) -> &'static str {
1627        self.backend.name()
1628    }
1629
1630    /// How [`Strategy::Vector`] searches candidates, for diagnostics: the
1631    /// passes in order, such as `anchors(-)`, `start bytes(:@)` or
1632    /// `blocks`, joined with ` + `.
1633    pub fn plan(&self) -> String {
1634        if self.passes.is_empty() {
1635            return "none".to_string();
1636        }
1637        self.passes
1638            .iter()
1639            .map(Pass::describe)
1640            .collect::<Vec<_>>()
1641            .join(" + ")
1642    }
1643
1644    /// Whether every pass searches with `memchr`, so the prescan has
1645    /// nothing left to skip.
1646    #[inline]
1647    fn all_memchr(&self) -> bool {
1648        !self.passes.is_empty() && self.passes.iter().all(|p| p.search != Search::Blocks)
1649    }
1650
1651    /// Forces the portable block classifier (for tests and benchmarks).
1652    #[doc(hidden)]
1653    pub fn use_scalar_backend(&mut self) {
1654        self.backend = classify::Kind::Scalar;
1655    }
1656
1657    #[inline]
1658    fn compute_active(&self, line_classes: u16) -> u32 {
1659        let mut active = 0u32;
1660        for (i, req) in self.skip_requirements.iter().enumerate() {
1661            if !can_skip_with_mask(line_classes, req) {
1662                active |= 1u32 << i;
1663            }
1664        }
1665        active
1666    }
1667
1668    pub fn scan_line(&self, line: &str) -> Vec<Match> {
1669        let mut matches = Vec::new();
1670        self.scan_line_into(line, &mut matches);
1671        matches
1672    }
1673
1674    /// Start of the first line at or after `from` (itself a line start) in
1675    /// `text` that may contain a match, or `None` when no later line can.
1676    ///
1677    /// When the finders start at three bytes or fewer, those bytes are
1678    /// searched with `memchr` across the whole buffer, so lines that cannot
1679    /// match are never visited individually; otherwise every line is a
1680    /// candidate and `from` is returned. Lines end at `\n`.
1681    pub fn skip_to_candidate_line(&self, text: &[u8], from: usize) -> Option<usize> {
1682        let from = from.min(text.len());
1683        // Only a lone memchr pass can prove that a line holds nothing.
1684        let search = match self.passes.as_slice() {
1685            [pass] => pass.search,
1686            _ => return Some(from),
1687        };
1688        let hay = &text[from..];
1689        let hit = match search {
1690            Search::One(a) => memchr::memchr(a, hay),
1691            Search::Two(a, b) => memchr::memchr2(a, b, hay),
1692            Search::Three(a, b, c) => memchr::memchr3(a, b, c, hay),
1693            Search::Prefixes | Search::Blocks => return Some(from),
1694        }?;
1695        let hit = from + hit;
1696        Some(
1697            memchr::memrchr(b'\n', &text[from..hit])
1698                .map(|nl| from + nl + 1)
1699                .unwrap_or(from),
1700        )
1701    }
1702
1703    pub fn scan_line_into(&self, line: &str, matches: &mut Vec<Match>) {
1704        self.scan_line_probe(line, matches, &mut ());
1705    }
1706
1707    /// Like [`scan_line_into`](Self::scan_line_into), additionally counting
1708    /// the work performed into `stats`.
1709    pub fn scan_line_stats(&self, line: &str, matches: &mut Vec<Match>, stats: &mut ScanStats) {
1710        if stats.finders.len() < self.finders.len() {
1711            stats
1712                .finders
1713                .resize(self.finders.len(), FinderStats::default());
1714        }
1715        self.scan_line_probe(line, matches, stats);
1716    }
1717
1718    /// Active finders of `pass` for a line reached from the buffer walk:
1719    /// the prescan pays off for a block pass with several finders; a
1720    /// `memchr` pass or a single gated finder skips it.
1721    #[inline]
1722    fn buffer_active(&self, pass: &Pass, line: &[u8]) -> u32 {
1723        let ctx = pass.finders;
1724        let single_gated = self.finders.len() == 1 && ctx != 0;
1725        if single_gated || pass.search != Search::Blocks || line.is_empty() {
1726            return ctx;
1727        }
1728        self.compute_active(prescan(line)) & ctx
1729    }
1730
1731    /// Prescan and per-finder activation shared by every entry point.
1732    /// Returns the active finder mask, or `None` when nothing can match.
1733    #[inline]
1734    fn activate<P: Probe>(&self, input: &[u8], probe: &mut P) -> Option<u32> {
1735        probe.line(input.len());
1736        if input.is_empty() {
1737            probe.line_skipped();
1738            return None;
1739        }
1740        let all = if self.finders.is_empty() {
1741            0
1742        } else {
1743            u32::MAX >> (32 - self.finders.len())
1744        };
1745        // A scanner of `memchr` passes walks the line for a few bytes,
1746        // which already skips everything the prescan could disable,
1747        // and a single gated finder is filtered at least as well by its own
1748        // gates and run rules; the prescan would only add a pass over every
1749        // byte.
1750        let single_gated = self.finders.len() == 1 && (self.dispatch_mask | self.trigger_mask) != 0;
1751        if single_gated || (self.all_memchr() && self.strategy == Strategy::Vector) {
1752            if all == 0 {
1753                probe.line_skipped();
1754                return None;
1755            }
1756            let mut bits = all;
1757            while bits != 0 {
1758                probe.finder_run(bits.trailing_zeros() as usize);
1759                bits &= bits - 1;
1760            }
1761            return Some(all);
1762        }
1763        let line_classes = prescan(input);
1764        let active = self.compute_active(line_classes);
1765        probe.finders_skipped((all & !active).count_ones());
1766        if active == 0 {
1767            probe.line_skipped();
1768            return None;
1769        }
1770        let mut bits = active;
1771        while bits != 0 {
1772            probe.finder_run(bits.trailing_zeros() as usize);
1773            bits &= bits - 1;
1774        }
1775        Some(active)
1776    }
1777
1778    fn scan_line_probe<P: Probe>(&self, line: &str, matches: &mut Vec<Match>, probe: &mut P) {
1779        matches.clear();
1780
1781        let input = line.as_bytes();
1782        let Some(active) = self.activate(input, probe) else {
1783            return;
1784        };
1785
1786        let active_scan = active & self.scan_mask;
1787        if active_scan != 0 {
1788            let mut bits = active_scan;
1789            while bits != 0 {
1790                let i = bits.trailing_zeros() as usize;
1791                bits &= bits - 1;
1792                let finder = &self.finders[i];
1793                let mut idx = 0;
1794                while idx < line.len() {
1795                    let found = finder.find(&line[idx..]);
1796                    probe.find_call(i, found.is_some());
1797                    if let Some(range) = found {
1798                        probe.matched(i);
1799                        matches.push(Match {
1800                            finder_index: i,
1801                            range: (idx + range.start)..(idx + range.end),
1802                        });
1803                        idx += range.end;
1804                    } else {
1805                        break;
1806                    }
1807                }
1808            }
1809        }
1810
1811        match self.strategy {
1812            Strategy::Legacy => self.walk_legacy(input, active, matches, probe),
1813            Strategy::Gated => self.walk_gated(input, active, matches, probe),
1814            Strategy::Vector => self.walk_vector(input, active, matches, probe),
1815        }
1816
1817        // Scan-mode matches come first in position order, then the merged
1818        // dispatch/trigger pass in candidate order; trigger matches may start
1819        // before their trigger byte, so check before paying for a sort.
1820        let sorted = matches.windows(2).all(|w| {
1821            (w[0].range.start, w[0].finder_index) <= (w[1].range.start, w[1].finder_index)
1822        });
1823        if !sorted {
1824            probe.sorted();
1825            sort_matches(matches);
1826        }
1827    }
1828
1829    /// `Strategy::Legacy`: the original two-pass byte loop.
1830    fn walk_legacy<P: Probe>(
1831        &self,
1832        input: &[u8],
1833        active: u32,
1834        matches: &mut Vec<Match>,
1835        probe: &mut P,
1836    ) {
1837        let active_dispatch = active & self.dispatch_mask;
1838        if active_dispatch != 0 {
1839            let mut finder_pos = [0usize; MAX_FINDERS];
1840            probe.positions(input.len());
1841
1842            for pos in 0..input.len() {
1843                let mut candidates = self.dispatch[input[pos] as usize] & active_dispatch;
1844                if candidates == 0 {
1845                    continue;
1846                }
1847                probe.candidate_position();
1848                while candidates != 0 {
1849                    let i = candidates.trailing_zeros() as usize;
1850                    candidates &= candidates - 1;
1851
1852                    if pos < finder_pos[i] {
1853                        continue;
1854                    }
1855                    let found = self.finders[i].try_at(input, pos);
1856                    probe.try_at_call(i, found.is_some());
1857                    if let Some(range) = found {
1858                        if range.start < finder_pos[i] {
1859                            continue;
1860                        }
1861                        probe.matched(i);
1862                        matches.push(Match {
1863                            finder_index: i,
1864                            range: range.clone(),
1865                        });
1866                        finder_pos[i] = range.end;
1867                    }
1868                }
1869            }
1870        }
1871
1872        let active_trigger = active & self.trigger_mask;
1873        if active_trigger != 0 {
1874            let mut finder_pos = [0usize; MAX_FINDERS];
1875            probe.positions(input.len());
1876
1877            for pos in 0..input.len() {
1878                let mut candidates = self.trigger[input[pos] as usize] & active_trigger;
1879                if candidates == 0 {
1880                    continue;
1881                }
1882                probe.candidate_position();
1883                while candidates != 0 {
1884                    let i = candidates.trailing_zeros() as usize;
1885                    candidates &= candidates - 1;
1886
1887                    if pos < finder_pos[i] {
1888                        continue;
1889                    }
1890                    let found = self.finders[i].try_trigger_at(input, pos);
1891                    probe.try_trigger_call(i, found.is_some());
1892                    if let Some(range) = found {
1893                        if range.start < finder_pos[i] {
1894                            continue;
1895                        }
1896                        finder_pos[i] = range.end;
1897                        probe.matched(i);
1898                        matches.push(Match {
1899                            finder_index: i,
1900                            range,
1901                        });
1902                    }
1903                }
1904            }
1905        }
1906    }
1907
1908    /// Runs every finder of `pass` the exact gates allow at `pos`, keeping
1909    /// matches disjoint per finder through `finder_pos`.
1910    #[inline(always)]
1911    #[allow(clippy::too_many_arguments)]
1912    fn probe_position<P: Probe>(
1913        &self,
1914        pass: &Pass,
1915        input: &[u8],
1916        pos: usize,
1917        active_ctx: u32,
1918        state: &mut LineState,
1919        matches: &mut Vec<Match>,
1920        probe: &mut P,
1921        hint: Option<&RunHint<'_>>,
1922    ) {
1923        let active = active_ctx & pass.finders;
1924        if pass.anchored != 0 {
1925            self.probe_anchor(input, pos, active & pass.anchored, state, matches, probe);
1926        }
1927        let plain = active & !pass.anchored;
1928        if plain == 0 {
1929            return;
1930        }
1931        let cur = input[pos];
1932        let next_class = if pos + 1 < input.len() {
1933            CTX_CLASS[input[pos + 1] as usize] as usize
1934        } else {
1935            CTX_NONE
1936        };
1937        let mut plain = plain;
1938        let contextual = plain & self.context_mask & self.trigger[cur as usize];
1939        if contextual != 0 {
1940            // Trigger finders with a context table: the two previous bytes
1941            // and the class of the next one reject most positions (every
1942            // timestamp colon for the URI finder), so they go first.
1943            let prev1 = if pos > 0 { input[pos - 1] } else { b' ' };
1944            let prev2 = if pos > 1 { input[pos - 2] } else { b' ' };
1945            let index = usize::from(prev2) << 8 | usize::from(prev1);
1946            let mut bits = contextual;
1947            while bits != 0 {
1948                let i = bits.trailing_zeros() as usize;
1949                bits &= bits - 1;
1950                let (pairs, exempt) = self.contexts[i]
1951                    .as_ref()
1952                    .expect("a context finder has a table");
1953                if exempt & (1 << next_class) == 0 && pairs[index >> 6] & (1 << (index & 63)) == 0 {
1954                    plain &= !(1u32 << i);
1955                }
1956            }
1957            if plain == 0 {
1958                probe.coarse_rejected(cur);
1959                return;
1960            }
1961        }
1962        let prev_class = if pos > 0 {
1963            CTX_CLASS[input[pos - 1] as usize] as usize
1964        } else {
1965            CTX_NONE
1966        };
1967        let mut candidates = self.gate_prev[prev_class][cur as usize] & plain;
1968        if candidates == 0 {
1969            probe.coarse_rejected(cur);
1970            return;
1971        }
1972        candidates &= self.gate_next[cur as usize][next_class];
1973        if candidates == 0 {
1974            probe.coarse_rejected(cur);
1975            return;
1976        }
1977        probe.candidate_position();
1978        self.invoke(input, pos, candidates, state, matches, probe, hint);
1979    }
1980
1981    /// Anchored dispatch finders in `mask`: `pos` may hold one of their
1982    /// anchor bytes. Each walks back over its `walk` bytes to where a match
1983    /// could start and tries the dispatch positions in order, exactly as
1984    /// the dispatch walk would have, without ever retrying a position
1985    /// (`tried`) so a run with many anchors stays linear.
1986    #[inline]
1987    fn probe_anchor<P: Probe>(
1988        &self,
1989        input: &[u8],
1990        pos: usize,
1991        mask: u32,
1992        state: &mut LineState,
1993        matches: &mut Vec<Match>,
1994        probe: &mut P,
1995    ) {
1996        let cur = input[pos];
1997        let mut bits = mask;
1998        while bits != 0 {
1999            let i = bits.trailing_zeros() as usize;
2000            bits &= bits - 1;
2001            // By reference: the anchor with its check is over a hundred
2002            // bytes, copied on every anchor byte of the input otherwise.
2003            let Some(anchor) = &self.anchors[i] else {
2004                continue;
2005            };
2006            if !anchor.bytes.contains(cur) || pos < state.pos(i) {
2007                continue;
2008            }
2009            // A cheap confirmation before any walk: the first anchor byte
2010            // of a match has a known byte at a fixed offset.
2011            let mut confirmed = false;
2012            if let Some(check) = anchor
2013                .checks
2014                .iter()
2015                .flatten()
2016                .find(|check| check.anchors.contains(cur))
2017            {
2018                if !check.passes(input, pos) {
2019                    continue;
2020                }
2021                confirmed = true;
2022            }
2023            let tried = state.tried(i);
2024            if let Some(back) = anchor.back {
2025                // The match can only start `back` bytes before the anchor.
2026                let mut next_tried = pos + 1;
2027                if let Some(p) = pos.checked_sub(back as usize)
2028                    && p >= tried
2029                    && self.starts[i].contains(input[p])
2030                {
2031                    // A confirmed anchor at its exact offset is almost always
2032                    // a match: the finder validates it in one go, without the
2033                    // gates and rules it would pass anyway.
2034                    let matched = if confirmed {
2035                        self.call_finder_at(i, input, p, state, matches, probe)
2036                    } else {
2037                        self.try_finder_at(i, input, p, state, matches, probe)
2038                    };
2039                    if matched {
2040                        next_tried = next_tried.max(state.pos(i));
2041                    }
2042                }
2043                state.set_tried(i, next_tried);
2044                continue;
2045            }
2046            let mut start = pos;
2047            while start > tried && anchor.walk.contains(input[start - 1]) {
2048                start -= 1;
2049            }
2050            let mut p = start.max(tried);
2051            let mut next_tried = pos + 1;
2052            while p <= pos {
2053                if self.starts[i].contains(input[p])
2054                    && self.try_finder_at(i, input, p, state, matches, probe)
2055                {
2056                    next_tried = next_tried.max(state.pos(i));
2057                    break;
2058                }
2059                p += 1;
2060            }
2061            state.set_tried(i, next_tried);
2062        }
2063    }
2064
2065    /// Calls finder `i` at `pos` without any gate or rule (they are only
2066    /// necessary conditions, so skipping them changes nothing but the
2067    /// cost). Returns whether a match was recorded.
2068    #[inline]
2069    fn call_finder_at<P: Probe>(
2070        &self,
2071        i: usize,
2072        input: &[u8],
2073        pos: usize,
2074        state: &mut LineState,
2075        matches: &mut Vec<Match>,
2076        probe: &mut P,
2077    ) -> bool {
2078        probe.candidate_position();
2079        let found = self.finders[i].try_at_memo(input, pos, state.memo(i));
2080        probe.try_at_call(i, found.is_some());
2081        match found {
2082            Some(range) if range.start >= state.pos(i) => {
2083                state.set_pos(i, range.end);
2084                probe.matched(i);
2085                matches.push(Match {
2086                    finder_index: i,
2087                    range,
2088                });
2089                true
2090            }
2091            _ => false,
2092        }
2093    }
2094
2095    /// Dispatch attempt of finder `i` at `pos` with the exact gates and run
2096    /// rules applied first. Returns whether a match was recorded.
2097    #[inline]
2098    fn try_finder_at<P: Probe>(
2099        &self,
2100        i: usize,
2101        input: &[u8],
2102        pos: usize,
2103        state: &mut LineState,
2104        matches: &mut Vec<Match>,
2105        probe: &mut P,
2106    ) -> bool {
2107        let bit = 1u32 << i;
2108        let cur = input[pos];
2109        let prev_class = if pos > 0 {
2110            CTX_CLASS[input[pos - 1] as usize] as usize
2111        } else {
2112            CTX_NONE
2113        };
2114        let next_class = if pos + 1 < input.len() {
2115            CTX_CLASS[input[pos + 1] as usize] as usize
2116        } else {
2117            CTX_NONE
2118        };
2119        if self.gate_prev[prev_class][cur as usize] & self.gate_next[cur as usize][next_class] & bit
2120            == 0
2121        {
2122            return false;
2123        }
2124        probe.candidate_position();
2125        if self.word_mask & bit != 0 {
2126            let runs = Runs::only_word(Runs::word_at(input, pos, self.word_cap));
2127            if !RunRule::allow(&self.run_rules[i], cur, &runs, input, pos) {
2128                probe.run_gated();
2129                return false;
2130            }
2131        }
2132        if self.run_mask & self.rule_cur[cur as usize] & bit != 0 {
2133            let runs = Runs::at_cached(input, pos, &mut state.runs, self.run_cap);
2134            if self.may_accept(&runs) & bit == 0
2135                || !RunRule::allow(&self.run_rules[i], cur, &runs, input, pos)
2136            {
2137                probe.run_gated();
2138                return false;
2139            }
2140        }
2141        let found = self.finders[i].try_at_memo(input, pos, state.memo(i));
2142        probe.try_at_call(i, found.is_some());
2143        match found {
2144            Some(range) if range.start >= state.pos(i) => {
2145                state.set_pos(i, range.end);
2146                probe.matched(i);
2147                matches.push(Match {
2148                    finder_index: i,
2149                    range,
2150                });
2151                true
2152            }
2153            _ => false,
2154        }
2155    }
2156
2157    /// Finders at least one of whose digit or hex rules may accept `runs`,
2158    /// judged by the byte after each run alone.
2159    #[inline(always)]
2160    fn may_accept(&self, runs: &Runs) -> u32 {
2161        let index = |run: &crate::Run| run.after.map_or(256, |b| b as usize);
2162        let mut may = (self.after_digit[index(&runs.digit)]
2163            & self.len_digit[runs.digit.len as usize])
2164            | (self.after_hex[index(&runs.hex)] & self.len_hex[runs.hex.len as usize]);
2165        if runs.digit.capped {
2166            may |= self.digit_ruled & self.len_digit[runs.digit.len as usize];
2167        }
2168        if runs.hex.capped {
2169            may |= self.hex_ruled & self.len_hex[runs.hex.len as usize];
2170        }
2171        may
2172    }
2173
2174    /// Drops the candidates whose word rules reject the word run at `pos`.
2175    /// Kept out of line: word rules are rare and the run measurement would
2176    /// bloat the candidate loop.
2177    #[inline(never)]
2178    fn word_gate<P: Probe>(
2179        &self,
2180        input: &[u8],
2181        pos: usize,
2182        mut candidates: u32,
2183        probe: &mut P,
2184    ) -> u32 {
2185        let runs = Runs::only_word(Runs::word_at(input, pos, self.word_cap));
2186        let mut gated = candidates & self.word_mask;
2187        while gated != 0 {
2188            let i = gated.trailing_zeros() as usize;
2189            gated &= gated - 1;
2190            if !RunRule::allow(&self.run_rules[i], input[pos], &runs, input, pos) {
2191                candidates &= !(1u32 << i);
2192                probe.run_gated();
2193            }
2194        }
2195        candidates
2196    }
2197
2198    /// Invokes the finders in `candidates` at `pos`.
2199    #[inline(always)]
2200    #[allow(clippy::too_many_arguments)]
2201    fn invoke<P: Probe>(
2202        &self,
2203        input: &[u8],
2204        pos: usize,
2205        mut candidates: u32,
2206        state: &mut LineState,
2207        matches: &mut Vec<Match>,
2208        probe: &mut P,
2209        hint: Option<&RunHint<'_>>,
2210    ) {
2211        if candidates & self.word_mask != 0 {
2212            candidates = self.word_gate(input, pos, candidates, probe);
2213        }
2214        let ruled = candidates & self.run_mask & self.rule_cur[input[pos] as usize];
2215        if ruled != 0 {
2216            let runs = match hint {
2217                Some(hint) => Runs::at_hinted(input, pos, hint, &mut state.runs, self.run_cap),
2218                None => Runs::at_cached(input, pos, &mut state.runs, self.run_cap),
2219            };
2220            // Finders none of whose rules can accept the byte after the run
2221            // are dropped without visiting their rules.
2222            let may = self.may_accept(&runs);
2223            let rejected = ruled & !may;
2224            candidates &= !rejected;
2225            probe.run_gated_n(rejected.count_ones());
2226            let mut gated = ruled & may;
2227            while gated != 0 {
2228                let i = gated.trailing_zeros() as usize;
2229                gated &= gated - 1;
2230                if !RunRule::allow(&self.run_rules[i], input[pos], &runs, input, pos) {
2231                    candidates &= !(1u32 << i);
2232                    probe.run_gated();
2233                }
2234            }
2235        }
2236        while candidates != 0 {
2237            let i = candidates.trailing_zeros() as usize;
2238            candidates &= candidates - 1;
2239
2240            if pos < state.pos(i) {
2241                continue;
2242            }
2243            let found = if self.dispatch_mask & (1u32 << i) != 0 {
2244                let found = self.finders[i].try_at_memo(input, pos, state.memo(i));
2245                probe.try_at_call(i, found.is_some());
2246                found
2247            } else {
2248                let found = self.finders[i].try_trigger_at_memo(input, pos, state.memo(i));
2249                probe.try_trigger_call(i, found.is_some());
2250                found
2251            };
2252            if let Some(range) = found {
2253                // Trigger matches can extend backward past the previous
2254                // match for this finder (e.g. an email local part walked
2255                // back across it); matches must stay disjoint per finder.
2256                if range.start < state.pos(i) {
2257                    continue;
2258                }
2259                state.set_pos(i, range.end);
2260                probe.matched(i);
2261                matches.push(Match {
2262                    finder_index: i,
2263                    range,
2264                });
2265            }
2266        }
2267    }
2268
2269    /// `Strategy::Gated`: one pass, context-gated candidates.
2270    fn walk_gated<P: Probe>(
2271        &self,
2272        input: &[u8],
2273        active: u32,
2274        matches: &mut Vec<Match>,
2275        probe: &mut P,
2276    ) {
2277        let active_ctx = active & (self.dispatch_mask | self.trigger_mask);
2278        if active_ctx == 0 {
2279            return;
2280        }
2281        let mut state = LineState::new(self.finders.len());
2282        probe.positions(input.len());
2283        let len = input.len();
2284        let mut prev_class = CTX_NONE;
2285
2286        for pos in 0..len {
2287            let cur = input[pos];
2288            let mut candidates = self.gate_prev[prev_class][cur as usize] & active_ctx;
2289            prev_class = CTX_CLASS[cur as usize] as usize;
2290            if candidates == 0 {
2291                continue;
2292            }
2293            let next_class = if pos + 1 < len {
2294                CTX_CLASS[input[pos + 1] as usize] as usize
2295            } else {
2296                CTX_NONE
2297            };
2298            candidates &= self.gate_next[cur as usize][next_class];
2299            if candidates == 0 {
2300                continue;
2301            }
2302            probe.candidate_position();
2303            self.invoke(input, pos, candidates, &mut state, matches, probe, None);
2304        }
2305    }
2306
2307    /// `Strategy::Vector`: every pass over the line, then the exact gates.
2308    fn walk_vector<P: Probe>(
2309        &self,
2310        input: &[u8],
2311        active: u32,
2312        matches: &mut Vec<Match>,
2313        probe: &mut P,
2314    ) {
2315        let active_ctx = active & (self.dispatch_mask | self.trigger_mask);
2316        if active_ctx == 0 {
2317            return;
2318        }
2319        probe.positions(input.len());
2320        let mut sink = LineSink {
2321            input,
2322            active_ctx,
2323            state: LineState::new(self.finders.len()),
2324            matches,
2325            probe,
2326        };
2327        for pass in &self.passes {
2328            if active_ctx & pass.finders != 0 {
2329                self.walk_pass(pass, input, &mut sink);
2330            }
2331        }
2332    }
2333
2334    /// Runs the search of `pass` over `input` and hands every candidate to
2335    /// `sink`.
2336    #[inline(always)]
2337    fn walk_pass<S: Sink>(&self, pass: &Pass, input: &[u8], sink: &mut S) {
2338        match pass.search {
2339            Search::One(_) | Search::Two(..) | Search::Three(..) => {
2340                // One call site, so the probe inlines once.
2341                let (bytes, count) = match pass.search {
2342                    Search::One(a) => ([a, a, a], 1),
2343                    Search::Two(a, b) => ([a, b, b], 2),
2344                    Search::Three(a, b, c) => ([a, b, c], 3),
2345                    Search::Prefixes | Search::Blocks => unreachable!("handled below"),
2346                };
2347                self.search(pass, &bytes[..count], input, sink);
2348            }
2349            Search::Prefixes => {
2350                let literals = pass.literals.as_ref().expect("a prefix pass has literals");
2351                let engine = search::Engine::of(self.backend);
2352                search::search_literals(engine, literals, input, |pos| {
2353                    sink.candidate(self, pass, pos, None);
2354                    sink.stopped()
2355                });
2356            }
2357            Search::Blocks => {
2358                if input.len() < BLOCK {
2359                    // Shorter than a block: the table walk has no setup.
2360                    let mut prev_class = CTX_NONE;
2361                    for (pos, &cur) in input.iter().enumerate() {
2362                        let candidates = self.gate_prev[prev_class][cur as usize] & pass.finders;
2363                        prev_class = CTX_CLASS[cur as usize] as usize;
2364                        if candidates != 0 {
2365                            sink.candidate(self, pass, pos, None);
2366                            if sink.stopped() {
2367                                return;
2368                            }
2369                        }
2370                    }
2371                    return;
2372                }
2373                match self.backend {
2374                    classify::Kind::Scalar => {
2375                        self.walk_blocks::<classify::Scalar, S>(pass, input, sink)
2376                    }
2377                    #[cfg(target_arch = "aarch64")]
2378                    classify::Kind::Neon => {
2379                        self.walk_blocks::<classify::neon::Neon, S>(pass, input, sink)
2380                    }
2381                    #[cfg(target_arch = "x86_64")]
2382                    classify::Kind::Ssse3 => {
2383                        // SAFETY: `Kind::Ssse3` is only selected after the CPU check.
2384                        unsafe { self.walk_blocks_ssse3(pass, input, sink) }
2385                    }
2386                }
2387            }
2388        }
2389    }
2390
2391    #[inline(always)]
2392    fn search<S: Sink>(&self, pass: &Pass, bytes: &[u8], input: &[u8], sink: &mut S) {
2393        let engine = search::Engine::of(self.backend);
2394        search::search(
2395            engine,
2396            bytes,
2397            pass.prev.as_ref(),
2398            &pass.checks,
2399            input,
2400            |pos| {
2401                sink.candidate(self, pass, pos, None);
2402                sink.stopped()
2403            },
2404        );
2405    }
2406
2407    #[cfg(target_arch = "x86_64")]
2408    #[target_feature(enable = "ssse3")]
2409    unsafe fn walk_blocks_ssse3<S: Sink>(&self, pass: &Pass, input: &[u8], sink: &mut S) {
2410        self.walk_blocks::<classify::ssse3::Ssse3, S>(pass, input, sink)
2411    }
2412
2413    #[inline(always)]
2414    fn walk_blocks<B: Backend, S: Sink>(&self, pass: &Pass, input: &[u8], sink: &mut S) {
2415        const GROUP: usize = 4;
2416        let rules = pass.rules.as_ref().expect("a block pass has rules");
2417        let tables = B::tables(rules);
2418        let len = input.len();
2419        debug_assert!(len >= BLOCK);
2420        let block_at =
2421            |at: usize| -> &[u8; BLOCK] { input[at..at + BLOCK].try_into().expect("a full block") };
2422        let cat_at =
2423            |at: usize| -> u8 { input.get(at).map_or(CAT_NONE, |&b| CATEGORY[b as usize]) };
2424
2425        let mut base = 0;
2426        // Groups of four independent blocks: one emptiness test skips 64
2427        // bytes, and the four chains overlap in the pipeline.
2428        while base + GROUP * BLOCK <= len {
2429            let prev = if base == 0 {
2430                CAT_NONE
2431            } else {
2432                cat_at(base - 1)
2433            };
2434            let l0 = B::block(&tables, rules, block_at(base), prev, cat_at(base + BLOCK));
2435            let l1 = B::block(
2436                &tables,
2437                rules,
2438                block_at(base + BLOCK),
2439                cat_at(base + BLOCK - 1),
2440                cat_at(base + 2 * BLOCK),
2441            );
2442            let l2 = B::block(
2443                &tables,
2444                rules,
2445                block_at(base + 2 * BLOCK),
2446                cat_at(base + 2 * BLOCK - 1),
2447                cat_at(base + 3 * BLOCK),
2448            );
2449            let l3 = B::block(
2450                &tables,
2451                rules,
2452                block_at(base + 3 * BLOCK),
2453                cat_at(base + 3 * BLOCK - 1),
2454                cat_at(base + 4 * BLOCK),
2455            );
2456            let mut lanes = [l0, l1, l2, l3];
2457            if pass.min_hex_run >= 2 {
2458                classify::drop_short_hex_runs::<B>(&mut lanes, pass.min_hex_run);
2459            }
2460            let any = B::or(
2461                B::or(lanes[0].cand, lanes[1].cand),
2462                B::or(lanes[2].cand, lanes[3].cand),
2463            );
2464            if !B::is_zero(any) {
2465                self.probe_group::<B, S>(pass, base, &lanes, 0, sink);
2466                if sink.stopped() {
2467                    return;
2468                }
2469            }
2470            base += GROUP * BLOCK;
2471        }
2472        // Remaining full blocks, one at a time.
2473        while base + BLOCK <= len {
2474            let prev = if base == 0 {
2475                CAT_NONE
2476            } else {
2477                cat_at(base - 1)
2478            };
2479            let mut lanes = [B::block(
2480                &tables,
2481                rules,
2482                block_at(base),
2483                prev,
2484                cat_at(base + BLOCK),
2485            )];
2486            if pass.min_hex_run >= 2 {
2487                classify::drop_short_hex_runs::<B>(&mut lanes, pass.min_hex_run);
2488            }
2489            if !B::is_zero(lanes[0].cand) {
2490                self.probe_group::<B, S>(pass, base, &lanes, 0, sink);
2491                if sink.stopped() {
2492                    return;
2493                }
2494            }
2495            base += BLOCK;
2496        }
2497        // Tail: re-classify the last full block, which overlaps the bytes
2498        // already walked, and keep only the lanes past `base`. No copy.
2499        if base < len {
2500            let at = len - BLOCK;
2501            let mut lanes = [B::block(
2502                &tables,
2503                rules,
2504                block_at(at),
2505                cat_at(at.wrapping_sub(1)),
2506                CAT_NONE,
2507            )];
2508            if pass.min_hex_run >= 2 {
2509                classify::drop_short_hex_runs::<B>(&mut lanes, pass.min_hex_run);
2510            }
2511            if !B::is_zero(lanes[0].cand) {
2512                self.probe_group::<B, S>(pass, at, &lanes, base - at, sink);
2513            }
2514        }
2515    }
2516
2517    /// Probes every candidate lane of the blocks in `lanes` (contiguous
2518    /// from `base`), ignoring the first `skip` lanes.
2519    #[inline(always)]
2520    fn probe_group<B: Backend, S: Sink>(
2521        &self,
2522        pass: &Pass,
2523        base: usize,
2524        lanes: &[Lanes<B::Vec>],
2525        skip: usize,
2526        sink: &mut S,
2527    ) {
2528        let mut hex = [0u64; 4];
2529        let mut digit = [0u64; 4];
2530        let mut cand = [0u64; 4];
2531        let count = lanes.len().min(4);
2532        for (i, l) in lanes.iter().take(count).enumerate() {
2533            cand[i] = B::mask(l.cand);
2534            hex[i] = B::mask(l.hex);
2535            digit[i] = B::mask(l.digit);
2536        }
2537        cand[0] &= !B::lanes(skip);
2538        for i in 0..count {
2539            let mut mask = cand[i];
2540            while mask != 0 {
2541                let lane = B::lane(mask);
2542                mask = B::clear(mask, lane);
2543                let hint = RunHint {
2544                    hex: &hex[i..count],
2545                    digit: &digit[i..count],
2546                    lane,
2547                    stride: B::STRIDE,
2548                };
2549                sink.candidate(self, pass, base + i * BLOCK + lane, Some(&hint));
2550                if sink.stopped() {
2551                    return;
2552                }
2553            }
2554        }
2555    }
2556
2557    /// Scans every line of `text` in one pass over the buffer and calls
2558    /// `emit(line_start, line_end, matches)` for each line with at least one
2559    /// match, in order; `line_end` excludes the terminator and trailing
2560    /// `\r`s, and the match ranges are relative to `line_start`, exactly as
2561    /// [`scan_line`](Self::scan_line) would report them for that line.
2562    /// Returns `true` when `emit` asked to stop.
2563    ///
2564    /// With the vector strategy and no scan-mode finder the candidate search
2565    /// runs over the whole buffer and only lines holding a candidate are
2566    /// looked at, which is much cheaper than a call per line.
2567    pub fn scan_buffer(
2568        &self,
2569        text: &str,
2570        mut emit: impl FnMut(usize, usize, &[Match]) -> bool,
2571    ) -> bool {
2572        let data = text.as_bytes();
2573        if self.strategy != Strategy::Vector || self.scan_mask != 0 || self.passes.is_empty() {
2574            return self.scan_lines(text, &mut emit);
2575        }
2576        if let [pass] = self.passes.as_slice() {
2577            if pass.whole {
2578                let mut matches = Vec::new();
2579                let mut sink = WholeSink {
2580                    data,
2581                    state: LineState::new(self.finders.len()),
2582                    matches: &mut matches,
2583                };
2584                self.walk_pass(pass, data, &mut sink);
2585                sort_matches(&mut matches);
2586                return emit_grouped(data, matches, &mut emit);
2587            }
2588            let mut sink = BufferSink::new(data, self, Stream(&mut emit));
2589            self.walk_pass(pass, data, &mut sink);
2590            sink.flush();
2591            return sink.stopped;
2592        }
2593        // Several passes: absolute matches per pass, merged into `scan_line`
2594        // order and grouped per line.
2595        let mut lists = Vec::with_capacity(self.passes.len());
2596        for pass in &self.passes {
2597            let mut matches = Vec::new();
2598            if pass.whole {
2599                let mut sink = WholeSink {
2600                    data,
2601                    state: LineState::new(self.finders.len()),
2602                    matches: &mut matches,
2603                };
2604                self.walk_pass(pass, data, &mut sink);
2605            } else {
2606                let mut sink = BufferSink::new(data, self, Collect(&mut matches));
2607                self.walk_pass(pass, data, &mut sink);
2608                sink.flush();
2609            }
2610            sort_matches(&mut matches);
2611            lists.push(matches);
2612        }
2613        emit_grouped(data, merge_matches(lists), &mut emit)
2614    }
2615
2616    /// Scans `text` like [`scan_buffer`](Self::scan_buffer) but hands every
2617    /// match to `emit(finder_index, range)` with a range into `text`, in the
2618    /// same order (by start, then finder), without resolving the lines
2619    /// around them; a caller that prints values alone skips that work.
2620    /// Returns `true` when `emit` asked to stop.
2621    pub fn scan_buffer_matches(
2622        &self,
2623        text: &str,
2624        mut emit: impl FnMut(usize, Range<usize>) -> bool,
2625    ) -> bool {
2626        let data = text.as_bytes();
2627        if self.strategy != Strategy::Vector || self.scan_mask != 0 || self.passes.is_empty() {
2628            return self.scan_lines(text, &mut |start, _end, matches: &[Match]| {
2629                matches
2630                    .iter()
2631                    .any(|m| emit(m.finder_index, m.range.start + start..m.range.end + start))
2632            });
2633        }
2634        let mut lists = Vec::with_capacity(self.passes.len());
2635        for pass in &self.passes {
2636            let mut matches = Vec::new();
2637            if pass.whole {
2638                let mut sink = WholeSink {
2639                    data,
2640                    state: LineState::new(self.finders.len()),
2641                    matches: &mut matches,
2642                };
2643                self.walk_pass(pass, data, &mut sink);
2644            } else {
2645                let mut sink = BufferSink::new(data, self, Collect(&mut matches));
2646                self.walk_pass(pass, data, &mut sink);
2647                sink.flush();
2648            }
2649            sort_matches(&mut matches);
2650            lists.push(matches);
2651        }
2652        merge_matches(lists)
2653            .into_iter()
2654            .any(|m| emit(m.finder_index, m.range))
2655    }
2656
2657    /// Line-by-line scanning of `text`, for every strategy.
2658    fn scan_lines(
2659        &self,
2660        text: &str,
2661        emit: &mut impl FnMut(usize, usize, &[Match]) -> bool,
2662    ) -> bool {
2663        let data = text.as_bytes();
2664        let mut matches = Vec::new();
2665        let mut pos = 0;
2666        while pos < data.len() {
2667            let nl = memchr::memchr(b'\n', &data[pos..]).map_or(data.len(), |i| pos + i);
2668            let mut end = nl;
2669            while end > pos && data[end - 1] == b'\r' {
2670                end -= 1;
2671            }
2672            self.scan_line_into(&text[pos..end], &mut matches);
2673            if !matches.is_empty() && emit(pos, end, &matches) {
2674                return true;
2675            }
2676            pos = nl + 1;
2677        }
2678        false
2679    }
2680
2681    /// Returns the same match as `scan_line(line).into_iter().next()`: the
2682    /// earliest-starting match, ties broken by the lowest finder index.
2683    pub fn scan_line_first(&self, line: &str) -> Option<Match> {
2684        let input = line.as_bytes();
2685        let active = self.activate(input, &mut ())?;
2686
2687        let mut best: Option<Match> = None;
2688        // Matches scan_line's sort order: (range.start, finder_index) ascending.
2689        let beats = |best: &Option<Match>, start: usize, index: usize| match best {
2690            None => true,
2691            Some(b) => (start, index) < (b.range.start, b.finder_index),
2692        };
2693
2694        let active_scan = active & self.scan_mask;
2695        if active_scan != 0 {
2696            let mut bits = active_scan;
2697            while bits != 0 {
2698                let i = bits.trailing_zeros() as usize;
2699                bits &= bits - 1;
2700                if let Some(range) = self.finders[i].find(line)
2701                    && beats(&best, range.start, i)
2702                {
2703                    best = Some(Match {
2704                        finder_index: i,
2705                        range,
2706                    });
2707                }
2708            }
2709        }
2710
2711        let active_ctx = active & (self.dispatch_mask | self.trigger_mask);
2712        if active_ctx != 0 {
2713            let mut state = LineState::new(self.finders.len());
2714            let len = input.len();
2715            let mut prev_class = CTX_NONE;
2716            let mut allowed = active_ctx;
2717
2718            for pos in 0..len {
2719                // Dispatch matches start at `pos`, so once `pos` passes the best
2720                // start no dispatch candidate can win (ties at equal start still
2721                // can). Trigger matches can start before their trigger byte, so
2722                // those finders must keep visiting every position.
2723                if allowed & self.dispatch_mask != 0
2724                    && let Some(b) = &best
2725                    && pos > b.range.start
2726                {
2727                    allowed &= self.trigger_mask;
2728                    if allowed == 0 {
2729                        break;
2730                    }
2731                }
2732                let cur = input[pos];
2733                let mut candidates = self.gate_prev[prev_class][cur as usize] & allowed;
2734                prev_class = CTX_CLASS[cur as usize] as usize;
2735                if candidates == 0 {
2736                    continue;
2737                }
2738                let next_class = if pos + 1 < len {
2739                    CTX_CLASS[input[pos + 1] as usize] as usize
2740                } else {
2741                    CTX_NONE
2742                };
2743                candidates &= self.gate_next[cur as usize][next_class];
2744                if candidates & self.word_mask != 0 {
2745                    let runs = Runs::only_word(Runs::word_at(input, pos, self.word_cap));
2746                    let mut gated = candidates & self.word_mask;
2747                    while gated != 0 {
2748                        let i = gated.trailing_zeros() as usize;
2749                        gated &= gated - 1;
2750                        if !RunRule::allow(&self.run_rules[i], cur, &runs, input, pos) {
2751                            candidates &= !(1u32 << i);
2752                        }
2753                    }
2754                }
2755                let ruled = candidates & self.run_mask & self.rule_cur[cur as usize];
2756                if ruled != 0 {
2757                    let runs = Runs::at_cached(input, pos, &mut state.runs, self.run_cap);
2758                    let may = self.may_accept(&runs);
2759                    candidates &= !(ruled & !may);
2760                    let mut gated = ruled & may;
2761                    while gated != 0 {
2762                        let i = gated.trailing_zeros() as usize;
2763                        gated &= gated - 1;
2764                        if !RunRule::allow(&self.run_rules[i], cur, &runs, input, pos) {
2765                            candidates &= !(1u32 << i);
2766                        }
2767                    }
2768                }
2769                while candidates != 0 {
2770                    let i = candidates.trailing_zeros() as usize;
2771                    candidates &= candidates - 1;
2772
2773                    if pos < state.pos(i) {
2774                        continue;
2775                    }
2776                    let found = if self.dispatch_mask & (1u32 << i) != 0 {
2777                        self.finders[i].try_at_memo(input, pos, state.memo(i))
2778                    } else {
2779                        self.finders[i].try_trigger_at_memo(input, pos, state.memo(i))
2780                    };
2781                    if let Some(range) = found {
2782                        if range.start < state.pos(i) {
2783                            continue;
2784                        }
2785                        state.set_pos(i, range.end);
2786                        if beats(&best, range.start, i) {
2787                            best = Some(Match {
2788                                finder_index: i,
2789                                range,
2790                            });
2791                        }
2792                    }
2793                }
2794            }
2795        }
2796
2797        best
2798    }
2799}
2800
2801#[cfg(test)]
2802mod tests {
2803    use super::*;
2804
2805    #[test]
2806    fn prescan_empty_input() {
2807        assert_eq!(prescan(b""), 0);
2808    }
2809
2810    #[test]
2811    fn prescan_detects_digit() {
2812        assert_ne!(prescan(b"abc123") & CL_DIGIT, 0);
2813    }
2814
2815    #[test]
2816    fn prescan_detects_at() {
2817        assert_ne!(prescan(b"user@host") & CL_AT, 0);
2818    }
2819
2820    #[test]
2821    fn prescan_detects_dollar() {
2822        assert_ne!(prescan(b"$HOME") & CL_DOLLAR, 0);
2823    }
2824
2825    #[test]
2826    fn prescan_no_false_positives() {
2827        let cl = prescan(b"hello world");
2828        assert_eq!(cl & CL_AT, 0);
2829        assert_eq!(cl & CL_DOLLAR, 0);
2830        assert_eq!(cl & CL_DIGIT, 0);
2831    }
2832
2833    #[test]
2834    fn can_skip_email_without_at() {
2835        let cl = prescan(b"hello world");
2836        assert!(can_skip_with_mask(cl, required_classes("email")));
2837    }
2838
2839    #[test]
2840    fn cannot_skip_email_with_at() {
2841        let cl = prescan(b"user@example.com");
2842        assert!(!can_skip_with_mask(cl, required_classes("email")));
2843    }
2844
2845    #[test]
2846    fn can_skip_env_without_dollar() {
2847        let cl = prescan(b"no vars here");
2848        assert!(can_skip_with_mask(cl, required_classes("env")));
2849    }
2850
2851    #[test]
2852    fn can_skip_json_without_brackets() {
2853        let cl = prescan(b"no json here");
2854        assert!(can_skip_with_mask(cl, required_classes("json")));
2855    }
2856
2857    #[test]
2858    fn can_skip_uri_without_colon() {
2859        let cl = prescan(b"no uris here");
2860        assert!(can_skip_with_mask(cl, required_classes("uri")));
2861    }
2862
2863    #[test]
2864    fn can_skip_uuid_without_dash() {
2865        let cl = prescan(b"abcdef1234567890");
2866        assert!(can_skip_with_mask(cl, required_classes("uuid")));
2867    }
2868
2869    #[test]
2870    fn cannot_skip_mirror() {
2871        let cl = prescan(b"anything");
2872        assert!(!can_skip_with_mask(cl, required_classes("mirror")));
2873    }
2874
2875    #[test]
2876    fn can_skip_hash_without_hex() {
2877        let cl = prescan(b"no hx zzz");
2878        assert!(can_skip_with_mask(cl, required_classes("hash")));
2879    }
2880
2881    #[test]
2882    fn can_skip_mac_without_separator() {
2883        let cl = prescan(b"aabbccddeeff");
2884        assert!(can_skip_with_mask(cl, required_classes("mac")));
2885    }
2886
2887    #[test]
2888    fn can_skip_semver_without_dot() {
2889        let cl = prescan(b"version 100");
2890        assert!(can_skip_with_mask(cl, required_classes("semver")));
2891    }
2892
2893    #[test]
2894    fn can_skip_cidr_without_slash() {
2895        let cl = prescan(b"192.168.1.0");
2896        assert!(can_skip_with_mask(cl, required_classes("cidr")));
2897    }
2898
2899    #[test]
2900    fn can_skip_path_without_prefix() {
2901        let cl = prescan(b"no paths here");
2902        assert!(can_skip_with_mask(cl, required_classes("path")));
2903    }
2904
2905    #[test]
2906    fn skip_to_candidate_line_jumps_over_lines_without_start_bytes() {
2907        let scanner = Scanner::new(vec![Box::new(crate::email::Email::default())]);
2908        let text = b"no mail\nstill none\nhi a@b.co there\ntail";
2909        assert_eq!(scanner.skip_to_candidate_line(text, 0), Some(19));
2910        assert_eq!(scanner.skip_to_candidate_line(text, 19), Some(19));
2911        assert_eq!(scanner.skip_to_candidate_line(text, 34), None);
2912        assert_eq!(scanner.skip_to_candidate_line(text, 100), None);
2913        let scanner = Scanner::new(vec![Box::new(crate::hash::Hash::default())]);
2914        assert_eq!(scanner.skip_to_candidate_line(text, 8), Some(8));
2915    }
2916
2917    #[test]
2918    fn sparse_scanner_matches_prescanned_results() {
2919        let text = "mail a@b.co, url http://x.y, none";
2920        let finders = || -> Vec<Box<dyn Finder>> {
2921            vec![
2922                Box::new(crate::email::Email::default()),
2923                Box::new(crate::uri::URI::default()),
2924            ]
2925        };
2926        let mut gated = Scanner::new(finders());
2927        gated.set_strategy(Strategy::Gated);
2928        let vector = Scanner::new(finders());
2929        assert_eq!(gated.scan_line(text), vector.scan_line(text));
2930        assert_eq!(gated.scan_line("nothing"), vector.scan_line("nothing"));
2931    }
2932
2933    #[test]
2934    fn scanner_empty_finders() {
2935        let scanner = Scanner::new(Vec::new());
2936        assert!(scanner.scan_line("hello").is_empty());
2937    }
2938
2939    #[test]
2940    fn scanner_try_new_rejects_too_many_finders() {
2941        let finders: Vec<Box<dyn Finder>> = (0..=MAX_FINDERS)
2942            .map(|_| Box::new(crate::mirror::Mirror::default()) as Box<dyn Finder>)
2943            .collect();
2944        assert!(matches!(
2945            Scanner::try_new(finders),
2946            Err(ScannerError::TooManyFinders {
2947                len,
2948                max,
2949            })
2950            if len == MAX_FINDERS + 1 && max == MAX_FINDERS
2951        ));
2952    }
2953
2954    #[test]
2955    fn scanner_empty_line() {
2956        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::mirror::Mirror::default())];
2957        let scanner = Scanner::new(finders);
2958        assert!(scanner.scan_line("").is_empty());
2959    }
2960
2961    #[test]
2962    fn scanner_mirror_matches_everything() {
2963        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::mirror::Mirror::default())];
2964        let scanner = Scanner::new(finders);
2965        let matches = scanner.scan_line("hello world");
2966        assert_eq!(matches.len(), 1);
2967        assert_eq!(matches[0].range, 0..11);
2968    }
2969
2970    #[test]
2971    fn scanner_prescan_skips_impossible_finders() {
2972        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::email::Email::default())];
2973        let scanner = Scanner::new(finders);
2974        let matches = scanner.scan_line("no at sign here");
2975        assert!(matches.is_empty());
2976    }
2977
2978    #[test]
2979    fn scanner_dispatch_finds_hash() {
2980        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::hash::Hash::default())];
2981        let scanner = Scanner::new(finders);
2982        let input = "md5: 5d41402abc4b2a76b9719d911017c592";
2983        let matches = scanner.scan_line(input);
2984        assert_eq!(matches.len(), 1);
2985        assert_eq!(
2986            &input[matches[0].range.clone()],
2987            "5d41402abc4b2a76b9719d911017c592"
2988        );
2989    }
2990
2991    #[test]
2992    fn scanner_dispatch_finds_multiple_hashes() {
2993        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::hash::Hash::default())];
2994        let scanner = Scanner::new(finders);
2995        let input = "5d41402abc4b2a76b9719d911017c592 and 2aae6c35c94fcfb415dbe95f408b9ce91ee846ed";
2996        let matches = scanner.scan_line(input);
2997        assert_eq!(matches.len(), 2);
2998        assert_eq!(
2999            &input[matches[0].range.clone()],
3000            "5d41402abc4b2a76b9719d911017c592"
3001        );
3002        assert_eq!(
3003            &input[matches[1].range.clone()],
3004            "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"
3005        );
3006    }
3007
3008    #[test]
3009    fn scanner_mixed_dispatch_and_scan() {
3010        let finders: Vec<Box<dyn Finder>> = vec![
3011            Box::new(crate::hash::Hash::default()),
3012            Box::new(crate::email::Email::default()),
3013        ];
3014        let scanner = Scanner::new(finders);
3015        let input = "user@example.com 5d41402abc4b2a76b9719d911017c592";
3016        let matches = scanner.scan_line(input);
3017        assert_eq!(matches.len(), 2);
3018        assert_eq!(&input[matches[0].range.clone()], "user@example.com");
3019        assert_eq!(
3020            &input[matches[1].range.clone()],
3021            "5d41402abc4b2a76b9719d911017c592"
3022        );
3023    }
3024
3025    #[test]
3026    fn scanner_position_ordered_output() {
3027        let finders: Vec<Box<dyn Finder>> = vec![
3028            Box::new(crate::env::Env::default()),
3029            Box::new(crate::hash::Hash::default()),
3030        ];
3031        let scanner = Scanner::new(finders);
3032        let input = "5d41402abc4b2a76b9719d911017c592 $HOME";
3033        let matches = scanner.scan_line(input);
3034        assert_eq!(matches.len(), 2);
3035        assert!(matches[0].range.start < matches[1].range.start);
3036    }
3037
3038    #[test]
3039    fn scanner_first_returns_earliest() {
3040        let finders: Vec<Box<dyn Finder>> = vec![
3041            Box::new(crate::env::Env::default()),
3042            Box::new(crate::hash::Hash::default()),
3043        ];
3044        let scanner = Scanner::new(finders);
3045        let input = "$HOME then 5d41402abc4b2a76b9719d911017c592";
3046        let m = scanner.scan_line_first(input).unwrap();
3047        assert_eq!(&input[m.range], "$HOME");
3048    }
3049
3050    #[test]
3051    fn scanner_dispatch_finds_env() {
3052        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::env::Env::default())];
3053        let scanner = Scanner::new(finders);
3054        let input = "use $HOME and ${PATH}";
3055        let matches = scanner.scan_line(input);
3056        assert_eq!(matches.len(), 2);
3057        assert_eq!(&input[matches[0].range.clone()], "$HOME");
3058        assert_eq!(&input[matches[1].range.clone()], "${PATH}");
3059    }
3060
3061    #[test]
3062    fn scanner_dispatch_finds_json() {
3063        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::json::Json::default())];
3064        let scanner = Scanner::new(finders);
3065        let input = r#"data: {"key": "value"} end"#;
3066        let matches = scanner.scan_line(input);
3067        assert_eq!(matches.len(), 1);
3068        assert_eq!(&input[matches[0].range.clone()], r#"{"key": "value"}"#);
3069    }
3070
3071    #[test]
3072    fn scanner_dispatch_finds_uuid() {
3073        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::uuid::Uuid::default())];
3074        let scanner = Scanner::new(finders);
3075        let input = "id: 550e8400-e29b-41d4-a716-446655440000 end";
3076        let matches = scanner.scan_line(input);
3077        assert_eq!(matches.len(), 1);
3078        assert_eq!(
3079            &input[matches[0].range.clone()],
3080            "550e8400-e29b-41d4-a716-446655440000"
3081        );
3082    }
3083
3084    #[test]
3085    fn scanner_dispatch_finds_color() {
3086        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::color::Color::default())];
3087        let scanner = Scanner::new(finders);
3088        let input = "color: #ff00aa and rgb(0, 255, 0)";
3089        let matches = scanner.scan_line(input);
3090        assert_eq!(matches.len(), 2);
3091        assert_eq!(&input[matches[0].range.clone()], "#ff00aa");
3092        assert_eq!(&input[matches[1].range.clone()], "rgb(0, 255, 0)");
3093    }
3094
3095    #[test]
3096    fn scanner_dispatch_finds_ip() {
3097        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::ip::Ip::default())];
3098        let scanner = Scanner::new(finders);
3099        let input = "connect to 192.168.1.1 now";
3100        let matches = scanner.scan_line(input);
3101        assert_eq!(matches.len(), 1);
3102        assert_eq!(&input[matches[0].range.clone()], "192.168.1.1");
3103    }
3104
3105    #[test]
3106    fn scanner_dispatch_finds_datetime() {
3107        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::datetime::Datetime::default())];
3108        let scanner = Scanner::new(finders);
3109        let input = "at 2024-01-15T10:30:00Z end";
3110        let matches = scanner.scan_line(input);
3111        assert_eq!(matches.len(), 1);
3112        assert_eq!(&input[matches[0].range.clone()], "2024-01-15T10:30:00Z");
3113    }
3114
3115    #[test]
3116    fn scanner_many_finders() {
3117        let finders: Vec<Box<dyn Finder>> = vec![
3118            Box::new(crate::hash::Hash::default()),
3119            Box::new(crate::email::Email::default()),
3120            Box::new(crate::env::Env::default()),
3121            Box::new(crate::json::Json::default()),
3122            Box::new(crate::ip::Ip::default()),
3123        ];
3124        let scanner = Scanner::new(finders);
3125        let input =
3126            r#"192.168.1.1 user@example.com $HOME {"key": "val"} 5d41402abc4b2a76b9719d911017c592"#;
3127        let matches = scanner.scan_line(input);
3128        assert_eq!(matches.len(), 5);
3129        assert_eq!(&input[matches[0].range.clone()], "192.168.1.1");
3130        assert_eq!(&input[matches[1].range.clone()], "user@example.com");
3131        assert_eq!(&input[matches[2].range.clone()], "$HOME");
3132        assert_eq!(&input[matches[3].range.clone()], r#"{"key": "val"}"#);
3133        assert_eq!(
3134            &input[matches[4].range.clone()],
3135            "5d41402abc4b2a76b9719d911017c592"
3136        );
3137    }
3138
3139    // --- Edge cases: single byte inputs ---
3140
3141    #[test]
3142    fn scanner_single_byte_inputs() {
3143        let finders: Vec<Box<dyn Finder>> = vec![
3144            Box::new(crate::hash::Hash::default()),
3145            Box::new(crate::email::Email::default()),
3146            Box::new(crate::env::Env::default()),
3147            Box::new(crate::ip::Ip::default()),
3148            Box::new(crate::json::Json::default()),
3149            Box::new(crate::color::Color::default()),
3150            Box::new(crate::uuid::Uuid::default()),
3151        ];
3152        let scanner = Scanner::new(finders);
3153        for b in 0..=127u8 {
3154            let s = String::from(b as char);
3155            let _ = scanner.scan_line(&s);
3156        }
3157    }
3158
3159    #[test]
3160    fn scanner_two_byte_combinations() {
3161        let finders: Vec<Box<dyn Finder>> = vec![
3162            Box::new(crate::env::Env::default()),
3163            Box::new(crate::json::Json::default()),
3164            Box::new(crate::color::Color::default()),
3165        ];
3166        let scanner = Scanner::new(finders);
3167        let interesting = b"${}[]#rgb()0aA@:/.~+-\"\\";
3168        for &a in interesting {
3169            for &b in interesting {
3170                let s = String::from_utf8(vec![a, b]).unwrap();
3171                let _ = scanner.scan_line(&s);
3172            }
3173        }
3174    }
3175
3176    // --- Edge cases: repeated delimiters ---
3177
3178    #[test]
3179    fn scanner_repeated_dollars() {
3180        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::env::Env::default())];
3181        let scanner = Scanner::new(finders);
3182        assert!(scanner.scan_line("$$$").is_empty());
3183        assert!(scanner.scan_line("$$$$").is_empty());
3184    }
3185
3186    #[test]
3187    fn scanner_repeated_hashes() {
3188        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::color::Color::default())];
3189        let scanner = Scanner::new(finders);
3190        assert!(scanner.scan_line("###").is_empty());
3191    }
3192
3193    #[test]
3194    fn scanner_repeated_brackets() {
3195        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::json::Json::default())];
3196        let scanner = Scanner::new(finders);
3197        let matches = scanner.scan_line("{}{}{}");
3198        assert_eq!(matches.len(), 3);
3199    }
3200
3201    #[test]
3202    fn scanner_repeated_dots() {
3203        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::ip::Ip::default())];
3204        let scanner = Scanner::new(finders);
3205        assert!(scanner.scan_line("....").is_empty());
3206    }
3207
3208    // --- Edge cases: only whitespace ---
3209
3210    #[test]
3211    fn scanner_whitespace() {
3212        let finders: Vec<Box<dyn Finder>> = vec![
3213            Box::new(crate::hash::Hash::default()),
3214            Box::new(crate::email::Email::default()),
3215        ];
3216        let scanner = Scanner::new(finders);
3217        assert!(scanner.scan_line("   \t\n  ").is_empty());
3218    }
3219
3220    // --- Edge cases: very long lines ---
3221
3222    #[test]
3223    fn scanner_long_hex_run() {
3224        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::hash::Hash::default())];
3225        let scanner = Scanner::new(finders);
3226        let long_hex = "a".repeat(10000);
3227        assert!(scanner.scan_line(&long_hex).is_empty());
3228    }
3229
3230    #[test]
3231    fn scanner_many_matches_in_line() {
3232        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::env::Env::default())];
3233        let scanner = Scanner::new(finders);
3234        let input = (0..100)
3235            .map(|i| format!("$VAR{}", i))
3236            .collect::<Vec<_>>()
3237            .join(" ");
3238        let matches = scanner.scan_line(&input);
3239        assert_eq!(matches.len(), 100);
3240    }
3241
3242    // --- Edge cases: adjacent matches ---
3243
3244    #[test]
3245    fn scanner_adjacent_env_vars() {
3246        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::env::Env::default())];
3247        let scanner = Scanner::new(finders);
3248        let input = "$A$B$C";
3249        let matches = scanner.scan_line(input);
3250        let texts: Vec<&str> = matches.iter().map(|m| &input[m.range.clone()]).collect();
3251        assert_eq!(texts, vec!["$A", "$B", "$C"]);
3252    }
3253
3254    #[test]
3255    fn scanner_adjacent_json_objects() {
3256        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::json::Json::default())];
3257        let scanner = Scanner::new(finders);
3258        let input = r#"{"a":1}{"b":2}[3]"#;
3259        let matches = scanner.scan_line(input);
3260        let texts: Vec<&str> = matches.iter().map(|m| &input[m.range.clone()]).collect();
3261        assert_eq!(texts, vec![r#"{"a":1}"#, r#"{"b":2}"#, "[3]"]);
3262    }
3263
3264    // --- Edge cases: prescan correctness ---
3265
3266    #[test]
3267    fn prescan_all_byte_classes() {
3268        let input = b"09afAF gZ@$#{}[]:./~+(- ";
3269        let cl = prescan(input);
3270        assert_ne!(cl & CL_DIGIT, 0);
3271        assert_ne!(cl & CL_HEX_ALPHA, 0);
3272        assert_ne!(cl & CL_ALPHA_OTHER, 0);
3273        assert_ne!(cl & CL_AT, 0);
3274        assert_ne!(cl & CL_DOLLAR, 0);
3275        assert_ne!(cl & CL_HASH, 0);
3276        assert_ne!(cl & CL_OPEN_BRACE, 0);
3277        assert_ne!(cl & CL_OPEN_BRACKET, 0);
3278        assert_ne!(cl & CL_COLON, 0);
3279        assert_ne!(cl & CL_DOT, 0);
3280        assert_ne!(cl & CL_SLASH, 0);
3281        assert_ne!(cl & CL_TILDE, 0);
3282        assert_ne!(cl & CL_PLUS, 0);
3283        assert_ne!(cl & CL_OPEN_PAREN, 0);
3284        assert_ne!(cl & CL_DASH, 0);
3285    }
3286
3287    #[test]
3288    fn prescan_high_bytes_have_no_class() {
3289        for b in 128..=255u8 {
3290            assert_eq!(
3291                BYTE_CLASSES[b as usize], 0,
3292                "byte {} should have no class",
3293                b
3294            );
3295        }
3296    }
3297
3298    // --- Edge cases: dispatch table ---
3299
3300    #[test]
3301    fn dispatch_table_env_only_dollar() {
3302        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::env::Env::default())];
3303        let scanner = Scanner::new(finders);
3304        for b in 0..=255u8 {
3305            if b == b'$' {
3306                assert_ne!(scanner.gate_prev[CTX_NONE][b as usize], 0);
3307            } else {
3308                assert_eq!(scanner.gate_prev[CTX_NONE][b as usize], 0);
3309            }
3310        }
3311    }
3312
3313    #[test]
3314    fn dispatch_table_color_selective() {
3315        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::color::Color::default())];
3316        let scanner = Scanner::new(finders);
3317        let start = &scanner.gate_prev[CTX_NONE];
3318        assert_ne!(start[b'#' as usize], 0);
3319        assert_ne!(start[b'r' as usize], 0);
3320        assert_ne!(start[b'R' as usize], 0);
3321        assert_ne!(start[b'h' as usize], 0);
3322        assert_ne!(start[b'H' as usize], 0);
3323        assert_eq!(start[b'x' as usize], 0);
3324        assert_eq!(start[b' ' as usize], 0);
3325        // `#` has no previous-byte rule; `r` must follow a non-alphanumeric.
3326        assert_ne!(scanner.gate_prev[ctx_class(b'x')][b'#' as usize], 0);
3327        assert_eq!(scanner.gate_prev[ctx_class(b'x')][b'r' as usize], 0);
3328        assert_ne!(scanner.gate_prev[ctx_class(b' ')][b'r' as usize], 0);
3329        // `r` must be followed by `g`, `#` by a hex digit.
3330        assert_ne!(scanner.gate_next[b'r' as usize][ctx_class(b'g')], 0);
3331        assert_eq!(scanner.gate_next[b'r' as usize][ctx_class(b'x')], 0);
3332        assert_eq!(scanner.gate_next[b'#' as usize][ctx_class(b'x')], 0);
3333    }
3334
3335    #[test]
3336    fn scanner_scan_line_first_with_no_matches() {
3337        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::email::Email::default())];
3338        let scanner = Scanner::new(finders);
3339        assert!(scanner.scan_line_first("no at sign").is_none());
3340    }
3341
3342    #[test]
3343    fn scanner_scan_line_first_prescan_skip() {
3344        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::email::Email::default())];
3345        let scanner = Scanner::new(finders);
3346        assert!(scanner.scan_line_first("no matches possible").is_none());
3347    }
3348
3349    // --- Regression: prescan early exit with all classes ---
3350
3351    #[test]
3352    fn prescan_early_exit_fires_with_all_classes() {
3353        let input = b"09afAF gZ@$#{}[]:./~+(- ";
3354        let cl = prescan(input);
3355        assert_eq!(cl, CL_ALL_USED);
3356    }
3357
3358    #[test]
3359    fn prescan_early_exit_on_long_input() {
3360        let mut buf = Vec::new();
3361        buf.extend_from_slice(b"09afAF gZ@$#{}[]:./~+(- ");
3362        buf.extend_from_slice(&[b'x'; 10000]);
3363        let cl = prescan(&buf);
3364        assert_eq!(cl, CL_ALL_USED);
3365    }
3366
3367    // --- Regression: can_skip_with_mask all_required distinction ---
3368
3369    #[test]
3370    fn can_skip_all_required_single_bit_present() {
3371        let cl = CL_DIGIT | CL_SLASH;
3372        assert!(!can_skip_with_mask(
3373            cl,
3374            &[(CL_DIGIT, true), (CL_SLASH, true)]
3375        ));
3376    }
3377
3378    #[test]
3379    fn can_skip_all_required_missing_one() {
3380        let cl = CL_DIGIT;
3381        assert!(can_skip_with_mask(
3382            cl,
3383            &[(CL_DIGIT, true), (CL_SLASH, true)]
3384        ));
3385    }
3386
3387    #[test]
3388    fn can_skip_any_required_at_least_one() {
3389        let cl = CL_HASH;
3390        assert!(!can_skip_with_mask(
3391            cl,
3392            &[(CL_HASH | CL_ALPHA_OTHER, false)]
3393        ));
3394    }
3395
3396    #[test]
3397    fn can_skip_any_required_none_present() {
3398        let cl = CL_DIGIT;
3399        assert!(can_skip_with_mask(cl, &[(CL_HASH | CL_ALPHA_OTHER, false)]));
3400    }
3401
3402    #[test]
3403    fn can_skip_cidr_with_digit_but_no_slash() {
3404        let cl = CL_DIGIT | CL_DOT;
3405        assert!(can_skip_with_mask(cl, required_classes("cidr")));
3406    }
3407
3408    #[test]
3409    fn can_skip_semver_with_digit_but_no_dot() {
3410        let cl = CL_DIGIT;
3411        assert!(can_skip_with_mask(cl, required_classes("semver")));
3412    }
3413
3414    #[test]
3415    fn cannot_skip_semver_with_digit_and_dot() {
3416        let cl = CL_DIGIT | CL_DOT;
3417        assert!(!can_skip_with_mask(cl, required_classes("semver")));
3418    }
3419
3420    // --- Regression: scan_line_into reuses buffer ---
3421
3422    #[test]
3423    fn scan_line_into_reuses_buffer() {
3424        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::env::Env::default())];
3425        let scanner = Scanner::new(finders);
3426        let mut buf = Vec::new();
3427
3428        scanner.scan_line_into("$HOME here", &mut buf);
3429        assert_eq!(buf.len(), 1);
3430        assert_eq!(buf[0].range, 0..5);
3431
3432        scanner.scan_line_into("$PATH and $USER", &mut buf);
3433        assert_eq!(buf.len(), 2);
3434
3435        scanner.scan_line_into("no vars", &mut buf);
3436        assert!(buf.is_empty());
3437    }
3438
3439    #[test]
3440    fn scan_line_into_matches_scan_line() {
3441        let finders: Vec<Box<dyn Finder>> = vec![
3442            Box::new(crate::hash::Hash::default()),
3443            Box::new(crate::email::Email::default()),
3444            Box::new(crate::env::Env::default()),
3445        ];
3446        let scanner = Scanner::new(finders);
3447        let input = "user@example.com $HOME 5d41402abc4b2a76b9719d911017c592";
3448
3449        let from_scan_line = scanner.scan_line(input);
3450        let mut buf = Vec::new();
3451        scanner.scan_line_into(input, &mut buf);
3452
3453        assert_eq!(from_scan_line.len(), buf.len());
3454        for (a, b) in from_scan_line.iter().zip(buf.iter()) {
3455            assert_eq!(a.range, b.range);
3456            assert_eq!(a.finder_index, b.finder_index);
3457        }
3458    }
3459
3460    // --- Regression: trigger matches must stay disjoint per finder ---
3461
3462    #[test]
3463    fn trigger_matches_never_overlap_per_finder() {
3464        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::email::Email::default())];
3465        let scanner = Scanner::new(finders);
3466        for input in ["a@b.co@d.ef", "user@example.com@evil.org"] {
3467            let matches = scanner.scan_line(input);
3468            for w in matches.windows(2) {
3469                assert!(
3470                    w[1].range.start >= w[0].range.end,
3471                    "overlapping matches on {input:?}: {:?} vs {:?}",
3472                    w[0].range,
3473                    w[1].range
3474                );
3475            }
3476        }
3477    }
3478
3479    #[test]
3480    fn trigger_overlap_suppressed_keeps_first_match() {
3481        let finders: Vec<Box<dyn Finder>> = vec![Box::new(crate::email::Email::default())];
3482        let scanner = Scanner::new(finders);
3483        let input = "user@example.com@evil.org";
3484        let matches = scanner.scan_line(input);
3485        assert_eq!(matches.len(), 1);
3486        assert_eq!(&input[matches[0].range.clone()], "user@example.com");
3487    }
3488
3489    // --- Regression: scan_line_first must agree with scan_line()[0] ---
3490
3491    #[test]
3492    fn scan_line_first_ties_broken_by_finder_index() {
3493        // hash (dispatch mode, index 0) and mirror (scan mode, index 1) both
3494        // match at position 0; scan_line sorts ties by finder index, and
3495        // scan_line_first must agree instead of favoring scan-mode finders.
3496        let input = "5d41402abc4b2a76b9719d911017c592 tail";
3497        let finders: Vec<Box<dyn Finder>> = vec![
3498            Box::new(crate::hash::Hash::default()),
3499            Box::new(crate::mirror::Mirror::default()),
3500        ];
3501        let scanner = Scanner::new(finders);
3502        let all = scanner.scan_line(input);
3503        let first = scanner.scan_line_first(input).unwrap();
3504        assert_eq!(first.finder_index, all[0].finder_index);
3505        assert_eq!(first.range, all[0].range);
3506        assert_eq!(first.finder_index, 0);
3507    }
3508
3509    #[test]
3510    fn scan_line_first_agrees_with_scan_line_across_orders() {
3511        let inputs = [
3512            "2024-01-15.example.com",
3513            "x 2024-01-15.example.com",
3514            "$HOME 5d41402abc4b2a76b9719d911017c592",
3515            "5d41402abc4b2a76b9719d911017c592 $HOME",
3516            "user@example.com $HOME",
3517        ];
3518        let build_finders = |reversed: bool| -> Vec<Box<dyn Finder>> {
3519            let mut v: Vec<Box<dyn Finder>> = vec![
3520                Box::new(crate::datetime::Datetime::default()),
3521                Box::new(crate::hash::Hash::default()),
3522                Box::new(crate::env::Env::default()),
3523                Box::new(crate::email::Email::default()),
3524                Box::new(crate::mirror::Mirror::default()),
3525            ];
3526            if reversed {
3527                v.reverse();
3528            }
3529            v
3530        };
3531        for reversed in [false, true] {
3532            let scanner = Scanner::new(build_finders(reversed));
3533            for input in inputs {
3534                let all = scanner.scan_line(input);
3535                let first = scanner.scan_line_first(input);
3536                match (all.first(), first) {
3537                    (None, None) => {}
3538                    (Some(a), Some(f)) => {
3539                        assert_eq!(a.range, f.range, "range mismatch on {input:?}");
3540                        assert_eq!(
3541                            a.finder_index, f.finder_index,
3542                            "finder mismatch on {input:?}"
3543                        );
3544                    }
3545                    (a, f) => panic!("presence mismatch on {input:?}: {a:?} vs {f:?}"),
3546                }
3547            }
3548        }
3549    }
3550
3551    // --- Regression: all finders combined ---
3552
3553    #[test]
3554    fn scanner_all_finders_no_panic() {
3555        let mut codetag = crate::codetag::Codetag::default();
3556        codetag.build_mnemonics_regex().unwrap();
3557        let finders: Vec<Box<dyn Finder>> = vec![
3558            Box::new(crate::cidr::Cidr::default()),
3559            Box::new(codetag),
3560            Box::new(crate::color::Color::default()),
3561            Box::new(crate::datetime::Datetime::default()),
3562            Box::new(crate::email::Email::default()),
3563            Box::new(crate::emoji::Emoji::default()),
3564            Box::new(crate::env::Env::default()),
3565            Box::new(crate::hash::Hash::default()),
3566            Box::new(crate::ip::Ip::default()),
3567            Box::new(crate::json::Json::default()),
3568            Box::new(crate::jwt::Jwt::default()),
3569            Box::new(crate::mac::Mac::default()),
3570            Box::new(crate::path::Path::default()),
3571            Box::new(crate::phone::Phone::default()),
3572            Box::new(crate::semver::Semver::default()),
3573            Box::new(crate::uri::URI::default()),
3574            Box::new(crate::uuid::Uuid::default()),
3575        ];
3576        let scanner = Scanner::new(finders);
3577
3578        for input in [
3579            "",
3580            " ",
3581            "hello world",
3582            "user@example.com",
3583            "http://example.com",
3584            "$HOME /etc/hosts 192.168.1.0/24",
3585            "550e8400-e29b-41d4-a716-446655440000",
3586            r#"{"key": "value"} TODO: fix this"#,
3587            "v1.2.3 #ff00aa 2024-01-15T10:30:00Z",
3588            "00:1A:2B:3C:4D:5E +14155551234",
3589            "😀🎉 hello",
3590        ] {
3591            let _ = scanner.scan_line(input);
3592            let _ = scanner.scan_line_first(input);
3593        }
3594    }
3595}