Skip to main content

squeeze/
lib.rs

1//! # Squeeze
2//!
3//! A library for extracting rich information from any text.
4//!
5//! This crate provides finders for extracting structured data from text:
6//! - [`uri::URI`] - Extract URIs/URLs/URNs as defined by [RFC 3986](https://tools.ietf.org/html/rfc3986/)
7//! - [`cidr::Cidr`] - Extract CIDR notation (IPv4/IPv6 network ranges)
8//! - [`codetag::Codetag`] - Extract codetags (TODO, FIXME, etc.) as defined by [PEP 350](https://www.python.org/dev/peps/pep-0350/)
9//! - [`color::Color`] - Extract colors (hex, rgb, hsl)
10//! - [`datetime::Datetime`] - Extract ISO 8601 datetimes
11//! - [`domain::Domain`] - Extract DNS-style domain names
12//! - [`email::Email`] - Extract email addresses
13//! - [`emoji::Emoji`] - Extract emojis and emoji sequences
14//! - [`env::Env`] - Extract environment variable references
15//! - [`handle::Handle`] - Extract social `@handle` mentions
16//! - [`hash::Hash`] - Extract hashes (MD5, SHA-1, SHA-256, SHA-512)
17//! - [`ip::Ip`] - Extract IP addresses (IPv4, IPv6)
18//! - [`json::Json`] - Extract JSON objects and arrays
19//! - [`jwt::Jwt`] - Extract JSON Web Tokens
20//! - [`mac::Mac`] - Extract MAC addresses
21//! - [`modeline::Modeline`] - Extract vim/vi/ex modelines
22//! - [`path::Path`] - Extract file paths (absolute, relative, and home-relative)
23//! - [`phone::Phone`] - Extract phone numbers
24//! - [`semver::Semver`] - Extract semantic versions
25//! - [`uuid::Uuid`] - Extract UUIDs
26//! - [`mirror::Mirror`] - A passthrough finder that returns the entire input
27//!
28//! The [`scanner::Scanner`] runs any set of finders over a line, or over a
29//! whole buffer of lines with [`scanner::Scanner::scan_buffer`], in one
30//! pass: a prescan disables finders whose bytes are absent, SIMD
31//! classification and per-finder context gates keep finder calls to the
32//! positions where a match can start, and declarative run rules gate
33//! several finders at once from a single measurement of the digit and hex
34//! runs. See `docs/performance.md` in the repository for the architecture,
35//! the contracts finders follow, and how to measure.
36//!
37//! ## Example
38//!
39//! ```
40//! use squeeze::{uri::URI, Finder};
41//!
42//! let finder = URI::default();
43//! let text = "Visit https://example.com for more info";
44//!
45//! if let Some(range) = finder.find(text) {
46//!     println!("Found: {}", &text[range]);
47//! }
48//! ```
49
50pub mod cidr;
51pub(crate) mod classify;
52pub mod codetag;
53pub mod color;
54pub mod datetime;
55pub mod domain;
56pub mod email;
57pub mod emoji;
58pub mod env;
59pub mod handle;
60pub mod hash;
61pub(crate) mod iana;
62pub mod ip;
63pub(crate) mod ipv6;
64pub mod json;
65pub mod jwt;
66pub mod mac;
67pub mod mirror;
68pub mod modeline;
69pub mod path;
70pub mod phone;
71pub(crate) mod registry;
72pub mod scanner;
73pub(crate) mod search;
74pub mod semver;
75pub mod uri;
76pub mod uuid;
77pub(crate) mod word;
78
79use std::ops::Range;
80
81/// A set of bytes, used by [`RunRule`].
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub struct ByteSet([u64; 4]);
84
85impl ByteSet {
86    pub const EMPTY: ByteSet = ByteSet([0; 4]);
87    pub const ALL: ByteSet = ByteSet([u64::MAX; 4]);
88
89    pub const fn from_bytes(bytes: &[u8]) -> ByteSet {
90        let mut set = ByteSet::EMPTY;
91        let mut i = 0;
92        while i < bytes.len() {
93            set = set.with(bytes[i]);
94            i += 1;
95        }
96        set
97    }
98
99    /// Every byte for which `f` holds.
100    pub fn from_fn(f: impl Fn(u8) -> bool) -> ByteSet {
101        let mut set = ByteSet::EMPTY;
102        for b in 0..=255u8 {
103            if f(b) {
104                set = set.with(b);
105            }
106        }
107        set
108    }
109
110    pub const fn with(mut self, byte: u8) -> ByteSet {
111        self.0[(byte >> 6) as usize] |= 1u64 << (byte & 63);
112        self
113    }
114
115    #[inline]
116    pub const fn contains(&self, byte: u8) -> bool {
117        self.0[(byte >> 6) as usize] & (1u64 << (byte & 63)) != 0
118    }
119}
120
121/// The byte class a [`RunRule`] measures.
122#[derive(Clone, Copy, Debug, PartialEq, Eq)]
123#[repr(u8)]
124pub enum RunClass {
125    /// ASCII digits.
126    Digit,
127    /// ASCII hexadecimal digits.
128    Hex,
129    /// ASCII letters, digits and `_`: the bytes of a `\b`-delimited word.
130    Word,
131}
132
133impl RunClass {
134    /// Whether `b` belongs to the class.
135    #[inline(always)]
136    pub fn contains(self, b: u8) -> bool {
137        match self {
138            RunClass::Digit => b.is_ascii_digit(),
139            RunClass::Hex => b.is_ascii_hexdigit(),
140            RunClass::Word => b.is_ascii_alphanumeric() || b == b'_',
141        }
142    }
143}
144
145/// Longest run length the scanner distinguishes; longer runs are reported
146/// as this value with [`Run::capped`] set.
147pub const RUN_CAP: u8 = 129;
148
149/// A maximal run of one [`RunClass`] starting at a candidate position.
150#[derive(Clone, Copy, Debug, PartialEq, Eq)]
151pub struct Run {
152    /// Length of the run, at most [`RUN_CAP`].
153    pub len: u8,
154    /// Whether the run reached [`RUN_CAP`] and may continue.
155    pub capped: bool,
156    /// The byte after the run, `None` at the end of the input.
157    pub after: Option<u8>,
158}
159
160/// The digit, hex and word runs at a candidate position, measured once by
161/// the scanner and shared by every finder's [`RunRule`]s. The word run is
162/// only measured when a candidate finder has a [`RunClass::Word`] rule.
163#[derive(Clone, Copy, Debug, PartialEq, Eq)]
164pub struct Runs {
165    pub digit: Run,
166    pub hex: Run,
167    pub word: Run,
168}
169
170/// The last hex run measured on a line, so candidates inside the same run
171/// (digit-run starts inside a hash, say) do not measure it again.
172#[derive(Clone, Copy, Debug, Default)]
173pub(crate) struct RunCache {
174    start: usize,
175    end: usize,
176    /// The run may continue past `end` (the measurement hit the cap).
177    open: bool,
178}
179
180/// Hex and digit lane masks the vector stage already computed for the
181/// bytes around a candidate: `hex[0]`/`digit[0]` cover the block holding
182/// the candidate at `lane`, following entries the following blocks.
183pub(crate) struct RunHint<'a> {
184    pub(crate) hex: &'a [u64],
185    pub(crate) digit: &'a [u64],
186    pub(crate) lane: usize,
187    pub(crate) stride: u32,
188}
189
190impl RunHint<'_> {
191    /// Consecutive set lanes from `lane` across `masks`, or `None` when the
192    /// run reaches the end of the last mask (it may continue).
193    #[inline(always)]
194    pub(crate) fn run(&self, masks: &[u64]) -> Option<usize> {
195        let lane_bits = |m: u64, lane: usize| {
196            let shifted = m >> (lane as u32 * self.stride);
197            (((!shifted).trailing_zeros() / self.stride) as usize).min(BLOCK_LANES - lane)
198        };
199        let mut lane = self.lane;
200        let mut total = 0;
201        for &m in masks {
202            let run = lane_bits(m, lane);
203            total += run;
204            if run < BLOCK_LANES - lane {
205                return Some(total);
206            }
207            lane = 0;
208        }
209        None
210    }
211}
212
213/// Lanes per vector block (see `classify::BLOCK`).
214const BLOCK_LANES: usize = 16;
215
216impl Runs {
217    /// Measures the runs starting at `pos`.
218    pub fn at(input: &[u8], pos: usize) -> Runs {
219        let mut runs = Self::at_cached(input, pos, &mut RunCache::default(), RUN_CAP as usize);
220        runs.measure_word(input, pos, RUN_CAP as usize);
221        runs
222    }
223
224    /// Measures the word run starting at `pos`, reporting runs of `cap`
225    /// bytes or more as capped.
226    #[inline(always)]
227    pub(crate) fn measure_word(&mut self, input: &[u8], pos: usize, cap: usize) {
228        self.word = Self::word_at(input, pos, cap);
229    }
230
231    /// The word run starting at `pos`, capped at `cap` bytes.
232    #[inline(always)]
233    pub(crate) fn word_at(input: &[u8], pos: usize, cap: usize) -> Run {
234        let limit = input.len().min(pos + cap);
235        let mut end = pos;
236        while end < limit && RunClass::Word.contains(input[end]) {
237            end += 1;
238        }
239        Run {
240            len: (end - pos) as u8,
241            capped: end - pos >= cap,
242            after: input.get(end).copied(),
243        }
244    }
245
246    /// Runs holding only a word run; the digit and hex runs count as
247    /// unmeasured.
248    #[inline(always)]
249    pub(crate) fn only_word(word: Run) -> Runs {
250        Runs {
251            digit: UNMEASURED,
252            hex: UNMEASURED,
253            word,
254        }
255    }
256
257    /// Like [`at_cached`](Self::at_cached), taking the run lengths from the
258    /// vector stage's lane masks when they cover the whole run.
259    #[inline(always)]
260    pub(crate) fn at_hinted(
261        input: &[u8],
262        pos: usize,
263        hint: &RunHint<'_>,
264        cache: &mut RunCache,
265        cap: usize,
266    ) -> Runs {
267        if cache.start <= pos && pos < cache.end && (!cache.open || cache.end - pos > cap) {
268            return Self::at_cached(input, pos, cache, cap);
269        }
270        let hex_len = match hint.run(hint.hex) {
271            Some(len) => len.min(cap),
272            None => {
273                // The run leaves the masks: continue byte by byte.
274                let limit = input.len().min(pos + cap);
275                let mut hex = pos + hint.hex.len() * BLOCK_LANES - hint.lane;
276                while hex < limit && input[hex].is_ascii_hexdigit() {
277                    hex += 1;
278                }
279                hex.min(limit) - pos
280            }
281        };
282        let hex_end = pos + hex_len;
283        *cache = RunCache {
284            start: pos,
285            end: hex_end,
286            open: hex_len >= cap && hex_end < input.len(),
287        };
288        let digit_len = match hint.run(hint.digit) {
289            Some(len) => len.min(hex_len),
290            None => {
291                let mut digit = pos + hint.digit.len() * BLOCK_LANES - hint.lane;
292                while digit < hex_end && input[digit].is_ascii_digit() {
293                    digit += 1;
294                }
295                digit.min(hex_end) - pos
296            }
297        };
298        let run = |len: usize| Run {
299            len: len as u8,
300            capped: len >= cap,
301            after: input.get(pos + len).copied(),
302        };
303        Runs {
304            digit: run(digit_len),
305            hex: run(hex_len),
306            word: UNMEASURED,
307        }
308    }
309
310    /// Measures the runs starting at `pos`, reporting runs of `cap` bytes or
311    /// more as capped, and reusing `cache` when `pos` lies inside the hex
312    /// run it remembers.
313    #[inline(always)]
314    pub(crate) fn at_cached(input: &[u8], pos: usize, cache: &mut RunCache, cap: usize) -> Runs {
315        let hex_end =
316            if cache.start <= pos && pos < cache.end && (!cache.open || cache.end - pos > cap) {
317                cache.end.min(pos + cap)
318            } else {
319                let limit = input.len().min(pos + cap);
320                let mut hex = pos;
321                while hex < limit && input[hex].is_ascii_hexdigit() {
322                    hex += 1;
323                }
324                *cache = RunCache {
325                    start: pos,
326                    end: hex,
327                    open: hex == limit && hex < input.len(),
328                };
329                hex
330            };
331        let mut digit = pos;
332        while digit < hex_end && input[digit].is_ascii_digit() {
333            digit += 1;
334        }
335        let run = |stop: usize| Run {
336            len: (stop - pos) as u8,
337            capped: stop - pos >= cap,
338            after: input.get(stop).copied(),
339        };
340        Runs {
341            digit: run(digit),
342            hex: run(hex_end),
343            word: UNMEASURED,
344        }
345    }
346
347    #[inline(always)]
348    fn get(&self, class: RunClass) -> &Run {
349        // Indexed rather than matched: this sits in the rule loop.
350        [&self.digit, &self.hex, &self.word][class as usize]
351    }
352}
353
354/// A run the scanner did not measure: it counts as capped, which accepts
355/// every rule on it, so an unmeasured word run can never lose a match.
356const UNMEASURED: Run = Run {
357    len: RUN_CAP,
358    capped: true,
359    after: None,
360};
361
362/// A run required right after the byte that ends the previous run of a
363/// [`RunRule`]: `class` bytes numbering `min..=max`, followed by a byte in
364/// `after` (or the end of the input when `after_end` is set).
365#[derive(Clone, Copy, Debug, PartialEq, Eq)]
366pub struct Then {
367    pub class: RunClass,
368    pub min: u8,
369    pub max: u8,
370    pub after: ByteSet,
371    pub after_end: bool,
372}
373
374/// Most follow-up runs a rule can chain, see [`RunRule::then`].
375pub const MAX_THEN: usize = 2;
376
377/// A declarative rule on the run starting at a candidate position, see
378/// [`Finder::run_rules`].
379///
380/// A rule applies when the start byte is in `cur`; it accepts when the
381/// `class` run has a length in `min..=max` (and in `lengths` when set),
382/// the byte after it is in `after` (or the input ends there and
383/// `after_end` is set), and every [`Then`] step is met in turn right after
384/// that byte (an IPv4 address starts with `1.2.` whatever follows, a MAC
385/// address with `aa:bb:cc:`). A capped run always satisfies the rest of the
386/// rule, since its true end is unknown.
387#[derive(Clone, Copy, Debug, PartialEq, Eq)]
388pub struct RunRule {
389    pub cur: ByteSet,
390    pub class: RunClass,
391    pub min: u8,
392    pub max: u8,
393    /// Bit `n` set: a run of `n` bytes is accepted; zero means every
394    /// length in `min..=max`. Lengths of 64 and more are governed by
395    /// `min..=max` alone.
396    pub lengths: u64,
397    pub after: ByteSet,
398    pub after_end: bool,
399    pub then: [Option<Then>; MAX_THEN],
400}
401
402impl RunRule {
403    /// A rule for `class` runs of `min..=max` bytes starting at any byte of
404    /// the class and followed by any byte, including the end of the input.
405    pub fn new(class: RunClass, min: u8, max: u8) -> RunRule {
406        let cur = ByteSet::from_fn(|b| class.contains(b));
407        RunRule {
408            cur,
409            class,
410            min,
411            max,
412            lengths: 0,
413            after: ByteSet::ALL,
414            after_end: true,
415            then: [None; MAX_THEN],
416        }
417    }
418
419    /// Restricts the rule's own run to the lengths whose bit is set in
420    /// `lengths` (bit `n` for `n` bytes), within `min..=max`.
421    pub fn lengths(mut self, lengths: u64) -> RunRule {
422        self.lengths = lengths;
423        self
424    }
425
426    /// Requires the byte after the last run described so far (the rule's
427    /// own run, or its latest [`then`](Self::then) step) to be one of
428    /// `bytes`; the end of the input no longer qualifies unless
429    /// [`or_end`](Self::or_end) follows.
430    pub fn followed_by(self, bytes: &[u8]) -> RunRule {
431        self.followed_by_set(ByteSet::from_bytes(bytes))
432    }
433
434    /// Like [`followed_by`](Self::followed_by) with a byte set.
435    pub fn followed_by_set(mut self, set: ByteSet) -> RunRule {
436        match self.then.iter_mut().rev().find_map(Option::as_mut) {
437            Some(step) => {
438                step.after = set;
439                step.after_end = false;
440            }
441            None => {
442                self.after = set;
443                self.after_end = false;
444            }
445        }
446        self
447    }
448
449    /// Also accepts the end of the input after the last run described so
450    /// far.
451    pub fn or_end(mut self) -> RunRule {
452        match self.then.iter_mut().rev().find_map(Option::as_mut) {
453            Some(step) => step.after_end = true,
454            None => self.after_end = true,
455        }
456        self
457    }
458
459    /// Chains a run of `class` bytes numbering `min..=max` that must start
460    /// right after the byte ending the previous run; it may be followed by
461    /// anything until [`followed_by`](Self::followed_by) restricts it. Up
462    /// to [`MAX_THEN`] steps can be chained. As for the first run, a `max`
463    /// of [`RUN_CAP`] means unbounded.
464    pub fn then(mut self, class: RunClass, min: u8, max: u8) -> RunRule {
465        let slot = self
466            .then
467            .iter_mut()
468            .find(|slot| slot.is_none())
469            .expect("a rule chains at most MAX_THEN steps");
470        *slot = Some(Then {
471            class,
472            min,
473            max,
474            after: ByteSet::ALL,
475            after_end: true,
476        });
477        self
478    }
479
480    #[inline]
481    fn applies(&self, cur: u8) -> bool {
482        self.cur.contains(cur)
483    }
484
485    #[inline]
486    fn accepts(&self, runs: &Runs, input: &[u8], pos: usize) -> bool {
487        let run = runs.get(self.class);
488        if run.len < self.min || run.len > self.max {
489            return false;
490        }
491        if run.capped {
492            return true;
493        }
494        if self.lengths != 0 && run.len < 64 && self.lengths & (1u64 << run.len) == 0 {
495            return false;
496        }
497        match run.after {
498            Some(b) if self.after.contains(b) => {}
499            Some(_) => return false,
500            None => return self.after_end,
501        }
502        let mut at = pos + run.len as usize + 1;
503        for step in self.then.iter().flatten() {
504            let limit = input.len().min(at + step.max as usize + 1);
505            let mut end = at;
506            while end < limit && step.class.contains(input[end]) {
507                end += 1;
508            }
509            let len = end - at;
510            if len > step.max as usize && step.max >= RUN_CAP {
511                // Longer than the cap: unbounded, as a capped first run.
512                return true;
513            }
514            if len < step.min as usize || len > step.max as usize {
515                return false;
516            }
517            match input.get(end) {
518                Some(&b) if step.after.contains(b) => {}
519                Some(_) => return false,
520                None => return step.after_end,
521            }
522            at = end + 1;
523        }
524        true
525    }
526
527    /// Evaluates a finder's rules for the candidate at `pos` of `input`,
528    /// whose runs are `runs`: a match may start when no rule applies to
529    /// `cur`, or when at least one applicable rule accepts.
530    #[inline(always)]
531    pub fn allow(rules: &[RunRule], cur: u8, runs: &Runs, input: &[u8], pos: usize) -> bool {
532        let mut applicable = false;
533        for rule in rules {
534            if rule.applies(cur) {
535                if rule.accepts(runs, input, pos) {
536                    return true;
537                }
538                applicable = true;
539            }
540        }
541        !applicable
542    }
543}
544
545/// Per-line, per-finder scratch handed to
546/// [`Finder::try_at_memo`]/[`Finder::try_trigger_at_memo`].
547///
548/// A finder may record what it learned about the current line (typically
549/// the extent of a run that cannot match) so that later attempts on the same
550/// line stay cheap. It is a cache only: results must not depend on it. The
551/// scanner resets it to `Memo::default()` (an empty range) for every line.
552#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
553pub struct Memo {
554    pub start: usize,
555    pub end: usize,
556    /// Free slot for one more position the finder wants to remember.
557    pub aux: usize,
558}
559
560impl Memo {
561    /// Whether `pos` lies in the remembered range.
562    #[inline]
563    pub fn covers(&self, pos: usize) -> bool {
564        self.start <= pos && pos < self.end
565    }
566}
567
568/// Bytes that every match of a dispatch finder contains, and the bytes that
569/// may lie between a match's start and its first anchor byte, see
570/// [`Finder::anchor`].
571#[derive(Clone, Copy, Debug, PartialEq, Eq)]
572pub struct Anchor {
573    pub bytes: ByteSet,
574    pub walk: ByteSet,
575    /// Bytes at fixed offsets that confirm the first anchor byte of a
576    /// match, one check per group of anchor bytes, see
577    /// [`confirm`](Self::confirm).
578    pub checks: [Option<AnchorCheck>; MAX_CHECKS],
579    /// Exact number of walk bytes between a match's start and its first
580    /// anchor byte, see [`back`](Self::back).
581    pub back: Option<u8>,
582}
583
584/// Most offsets an [`AnchorCheck`] can list.
585pub const MAX_CHECK_OFFSETS: usize = 5;
586
587/// Most checks an [`Anchor`] can carry (one per group of anchor bytes).
588pub const MAX_CHECKS: usize = 2;
589
590/// When the anchor byte at `pos` is one of `anchors`, the byte at one of
591/// the `offsets` after `pos` (or at every offset, with `all`) must be one
592/// of `bytes` (and exist) for `pos` to be the first anchor byte of a
593/// match: an IPv4 address's first `.` has another two to four bytes on, a
594/// UUID's first `-` others 5, 10 and 15 bytes on, a MAC address's first
595/// `:` others 3, 6, 9 and 12 bytes on.
596#[derive(Clone, Copy, Debug, PartialEq, Eq)]
597pub struct AnchorCheck {
598    pub anchors: ByteSet,
599    pub offsets: [u8; MAX_CHECK_OFFSETS],
600    pub offset_count: u8,
601    pub bytes: ByteSet,
602    /// Every offset must hold one of `bytes`, not just one of them.
603    pub all: bool,
604}
605
606impl AnchorCheck {
607    /// The offsets to test.
608    pub fn offsets(&self) -> &[u8] {
609        &self.offsets[..self.offset_count as usize]
610    }
611
612    /// Whether the anchor byte at `pos` of `input` passes the check.
613    #[inline]
614    pub fn passes(&self, input: &[u8], pos: usize) -> bool {
615        let seen = |&offset: &u8| {
616            input
617                .get(pos + offset as usize)
618                .is_some_and(|&b| self.bytes.contains(b))
619        };
620        if self.all {
621            self.offsets().iter().all(seen)
622        } else {
623            self.offsets().iter().any(seen)
624        }
625    }
626}
627
628impl Anchor {
629    /// An anchor on `bytes` reached by walking back over `walk` bytes.
630    pub fn new(bytes: ByteSet, walk: ByteSet) -> Anchor {
631        Anchor {
632            bytes,
633            walk,
634            checks: [None; MAX_CHECKS],
635            back: None,
636        }
637    }
638
639    /// States that a match starts exactly `back` bytes before its first
640    /// anchor byte (a UUID's first `-` is eight bytes in), so the scanner
641    /// tries that one position instead of every walk position. Combined
642    /// with [`confirm`](Self::confirm), a confirmed anchor goes straight to
643    /// the finder, skipping the gates and run rules it would pass anyway.
644    pub fn back(mut self, back: u8) -> Anchor {
645        self.back = Some(back);
646        self
647    }
648
649    /// Requires, when the anchor byte is one of `anchors`, the byte at one
650    /// of the `offsets` (at most [`MAX_CHECK_OFFSETS`]) after it to be one
651    /// of `bytes`; anchor bytes outside every check's `anchors` are not
652    /// checked, and at most [`MAX_CHECKS`] checks can be added. Only the
653    /// first anchor byte of a match has to pass.
654    pub fn confirm(self, anchors: &[u8], offsets: &[u8], bytes: &[u8]) -> Anchor {
655        self.check(anchors, offsets, bytes, false)
656    }
657
658    /// Like [`confirm`](Self::confirm), but the byte at every one of the
659    /// `offsets` must be one of `bytes`: the separators of a UUID or a MAC
660    /// address, which dates and timestamps do not repeat that far.
661    pub fn confirm_all(self, anchors: &[u8], offsets: &[u8], bytes: &[u8]) -> Anchor {
662        self.check(anchors, offsets, bytes, true)
663    }
664
665    fn check(mut self, anchors: &[u8], offsets: &[u8], bytes: &[u8], all: bool) -> Anchor {
666        assert!(
667            !offsets.is_empty() && offsets.len() <= MAX_CHECK_OFFSETS,
668            "an anchor check lists one to {MAX_CHECK_OFFSETS} offsets"
669        );
670        let mut list = [0u8; MAX_CHECK_OFFSETS];
671        list[..offsets.len()].copy_from_slice(offsets);
672        let slot = self
673            .checks
674            .iter_mut()
675            .find(|slot| slot.is_none())
676            .expect("an anchor carries at most MAX_CHECKS checks");
677        *slot = Some(AnchorCheck {
678            anchors: ByteSet::from_bytes(anchors),
679            offsets: list,
680            offset_count: offsets.len() as u8,
681            bytes: ByteSet::from_bytes(bytes),
682            all,
683        });
684        self
685    }
686}
687
688/// A trait for finding patterns in text.
689///
690/// All finders implement this trait. A finder implementation should be stateless;
691/// it's up to the caller to call it repeatedly until no more results can be extracted.
692///
693/// # Contract
694///
695/// - Returned ranges are non-empty (`start < end`) and lie on UTF-8 character
696///   boundaries.
697/// - [`try_at`](Finder::try_at)/[`try_trigger_at`](Finder::try_trigger_at)
698///   receive positions into the whole line and return absolute ranges.
699/// - Matches produced through the [`scanner::Scanner`] are disjoint per
700///   finder and position-sorted.
701///
702/// Note that iterating with `find` over advancing sub-slices (as below) erases
703/// the left context at each slice boundary, so finders that reject matches
704/// based on what precedes them (e.g. a handle glued to a word) can accept a
705/// match at position 0 of a sub-slice that the [`scanner::Scanner`] — which
706/// always sees the whole line — would reject. The Scanner behavior is
707/// authoritative; prefer it over hand-rolled `find` loops.
708///
709/// # Example
710///
711/// ```
712/// use squeeze::{uri::URI, Finder};
713///
714/// let finder = URI::default();
715/// let text = "Check https://foo.com and https://bar.com";
716///
717/// let mut results = Vec::new();
718/// let mut idx = 0;
719///
720/// while idx < text.len() {
721///     if let Some(range) = finder.find(&text[idx..]) {
722///         results.push(&text[idx + range.start..idx + range.end]);
723///         idx += range.end;
724///     } else {
725///         break;
726///     }
727/// }
728///
729/// assert_eq!(results, vec!["https://foo.com", "https://bar.com"]);
730/// ```
731pub trait Finder: Send + Sync {
732    /// Returns a unique identifier for this finder.
733    fn id(&self) -> &'static str;
734
735    /// Finds the first match in the given string.
736    ///
737    /// Returns `Some(range)` containing the byte range of the match, or `None` if no match is found.
738    /// The range is relative to the input string slice.
739    fn find(&self, s: &str) -> Option<Range<usize>>;
740
741    /// Whether this finder supports dispatch-mode scanning via [`try_at`](Finder::try_at).
742    fn dispatchable(&self) -> bool {
743        false
744    }
745
746    /// Whether the given byte could be the first byte of a match.
747    /// Only meaningful when [`dispatchable`](Finder::dispatchable) returns true.
748    fn could_start_at(&self, _byte: u8) -> bool {
749        true
750    }
751
752    /// Try to find a match starting exactly at `pos` in the full input.
753    /// Only called when [`dispatchable`](Finder::dispatchable) returns true.
754    fn try_at(&self, _input: &[u8], _pos: usize) -> Option<Range<usize>> {
755        None
756    }
757
758    /// Whether a match could be attempted at byte `cur` when the byte
759    /// immediately before it is `prev`: a dispatch-mode start, or a
760    /// trigger-mode trigger byte.
761    ///
762    /// Only meaningful when [`dispatchable`](Finder::dispatchable) or
763    /// [`triggerable`](Finder::triggerable) returns true. Must agree with
764    /// [`try_at`](Finder::try_at) (or [`try_trigger_at`](Finder::try_trigger_at)):
765    /// whenever this returns `false`, the attempt must return `None` in that
766    /// context. The [`scanner::Scanner`] folds the answers into lookup tables
767    /// so positions that cannot start a match never reach the finder.
768    fn could_start_after(&self, _prev: u8, _cur: u8) -> bool {
769        true
770    }
771
772    /// Whether an attempt at byte `cur` could succeed when the byte
773    /// immediately after it is `next`. Same contract as
774    /// [`could_start_after`](Finder::could_start_after).
775    fn could_continue_with(&self, _cur: u8, _next: u8) -> bool {
776        true
777    }
778
779    /// [`try_at`](Finder::try_at) with a per-line [`Memo`]; the scanner
780    /// calls this variant. Finders whose attempts can rescan the same bytes
781    /// override it to remember what already failed. Must return exactly what
782    /// `try_at` returns.
783    fn try_at_memo(&self, input: &[u8], pos: usize, memo: &mut Memo) -> Option<Range<usize>> {
784        let _ = memo;
785        self.try_at(input, pos)
786    }
787
788    /// [`try_trigger_at`](Finder::try_trigger_at) with a per-line
789    /// [`Memo`], same contract as [`try_at_memo`](Finder::try_at_memo).
790    fn try_trigger_at_memo(
791        &self,
792        input: &[u8],
793        pos: usize,
794        memo: &mut Memo,
795    ) -> Option<Range<usize>> {
796        let _ = memo;
797        self.try_trigger_at(input, pos)
798    }
799
800    /// Whether `try_at`/`try_trigger_at` give the same answer on a whole
801    /// buffer as on the line alone: `\n` and `\r` never belong to a match,
802    /// and every walk treats them exactly like the end of the input (as a
803    /// boundary), so a match never depends on what surrounds its line. A
804    /// finder that matches "up to the end of the line" (codetag), accepts
805    /// line terminators as whitespace (JSON, phone) or otherwise looks at
806    /// the line as a whole must keep the default. Line-agnostic finders are
807    /// run over whole buffers with absolute positions, which lets the
808    /// scanner skip resolving the line of every candidate; property tests
809    /// compare both modes. Defaults to `false`.
810    fn line_agnostic(&self) -> bool {
811        false
812    }
813
814    /// Bytes every match of this dispatch finder contains (`bytes`), and the
815    /// bytes that can separate a match start from the first of them
816    /// (`walk`). When every finder of a scanner declares an anchor (or is a
817    /// trigger finder) and the anchor bytes number three or fewer, the
818    /// scanner searches for those bytes with `memchr`, walks back over
819    /// `walk` and tries the dispatch positions in order, so lines without
820    /// an anchor byte cost nothing.
821    ///
822    /// Contract: for every range `try_at` returns, some byte of the range is
823    /// in `bytes`, and every byte from the range start up to the first such
824    /// byte is in `walk`.
825    fn anchor(&self) -> Option<Anchor> {
826        None
827    }
828
829    /// Byte strings, ASCII case ignored, one of which starts the input at
830    /// every position where [`try_at`](Finder::try_at) can match (`todo`
831    /// for `--todo` codetags). A finder that would otherwise need the block
832    /// classifier is then found by searching for them. Empty when unknown.
833    ///
834    /// Contract: `try_at(input, pos)` returns `None` unless `input[pos..]`
835    /// starts with one of the prefixes, ASCII case ignored.
836    fn prefixes(&self) -> Vec<Vec<u8>> {
837        Vec::new()
838    }
839
840    /// Rules on the digit or hex run starting at a candidate position,
841    /// evaluated with [`RunRule::allow`] before [`try_at`](Finder::try_at)
842    /// is called. Same contract as
843    /// [`could_start_after`](Finder::could_start_after): when the rules
844    /// reject a position, `try_at` must return `None` there. The scanner
845    /// measures the runs once per position for all finders.
846    fn run_rules(&self) -> Vec<RunRule> {
847        Vec::new()
848    }
849
850    /// Whether this finder supports trigger-mode scanning.
851    ///
852    /// Trigger-mode is for finders whose cheapest reliable signal is inside the
853    /// match rather than at the first byte, such as the `@` in an email address
854    /// or `:` in a URI.
855    fn triggerable(&self) -> bool {
856        false
857    }
858
859    /// Whether the given byte could trigger a match for this finder.
860    /// Only meaningful when [`triggerable`](Finder::triggerable) returns true.
861    fn could_trigger_at(&self, _byte: u8) -> bool {
862        false
863    }
864
865    /// Whether the trigger context gate constrains anything: the scanner
866    /// then tabulates [`trigger_context`](Finder::trigger_context) over the
867    /// 65536 pairs of previous bytes and
868    /// [`trigger_context_exempt`](Finder::trigger_context_exempt) over the
869    /// next bytes, once, and consults them before calling the finder.
870    /// Defaults to `false`.
871    fn has_trigger_context(&self) -> bool {
872        false
873    }
874
875    /// Whether a match may be triggered at a byte preceded by `prev2` and
876    /// `prev1`, unless the byte after the trigger is exempt
877    /// ([`trigger_context_exempt`](Finder::trigger_context_exempt)): a
878    /// necessary condition for [`try_trigger_at`](Finder::try_trigger_at)
879    /// to match at that position. The scanner passes a space for a missing
880    /// previous byte (the trigger sits in the first two bytes of the
881    /// input). Defaults to `true`.
882    fn trigger_context(&self, _prev2: u8, _prev1: u8) -> bool {
883        true
884    }
885
886    /// Whether a trigger followed by `next` (`None` at the end of the
887    /// input) escapes the previous-bytes condition: the URI finder needs a
888    /// registered scheme before a colon unless `//` follows. Defaults to
889    /// `false`.
890    fn trigger_context_exempt(&self, _next: Option<u8>) -> bool {
891        false
892    }
893
894    /// Try to find a match triggered by `pos` in the full input.
895    /// Only called when [`triggerable`](Finder::triggerable) returns true.
896    fn try_trigger_at(&self, _input: &[u8], _pos: usize) -> Option<Range<usize>> {
897        None
898    }
899}