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}