Skip to main content

regex_automata/util/prefilter/
aho_corasick.rs

1use crate::util::{
2    prefilter::PrefilterI,
3    search::{MatchKind, Span},
4};
5
6#[derive(Clone, Debug)]
7pub(crate) struct AhoCorasick {
8    #[cfg(not(feature = "perf-literal-multisubstring"))]
9    _unused: (),
10    #[cfg(feature = "perf-literal-multisubstring")]
11    ac: aho_corasick::AhoCorasick,
12}
13
14impl AhoCorasick {
15    pub(crate) fn new<B: AsRef<[u8]>>(
16        kind: MatchKind,
17        needles: &[B],
18    ) -> Option<AhoCorasick> {
19        #[cfg(not(feature = "perf-literal-multisubstring"))]
20        {
21            None
22        }
23        #[cfg(feature = "perf-literal-multisubstring")]
24        {
25            // We used to use `aho_corasick::MatchKind::Standard` here when
26            // `kind` was `MatchKind::All`, but this is not correct. The
27            // "standard" Aho-Corasick match semantics are to report a match
28            // immediately as soon as it is seen, but `All` isn't like that.
29            // In particular, with "standard" semantics, given the needles
30            // "abc" and "b" and the haystack "abc," it would report a match
31            // at offset 1 before a match at offset 0. This is never what we
32            // want in the context of the regex engine, regardless of whether
33            // we have leftmost-first or 'all' semantics. Namely, we always
34            // want the leftmost match.
35            let ac_match_kind = match kind {
36                MatchKind::LeftmostFirst | MatchKind::All => {
37                    aho_corasick::MatchKind::LeftmostFirst
38                }
39            };
40            // This is kind of just an arbitrary number, but basically, if we
41            // have a small enough set of literals, then we try to use the VERY
42            // memory hungry DFA. Otherwise, we wimp out and use an NFA. The
43            // upshot is that the NFA is quite lean and decently fast. Faster
44            // than a naive Aho-Corasick NFA anyway.
45            let ac_kind = if needles.len() <= 500 {
46                aho_corasick::AhoCorasickKind::DFA
47            } else {
48                aho_corasick::AhoCorasickKind::ContiguousNFA
49            };
50            let result = aho_corasick::AhoCorasick::builder()
51                .kind(Some(ac_kind))
52                .match_kind(ac_match_kind)
53                .start_kind(aho_corasick::StartKind::Both)
54                // We try to handle all of the prefilter cases in the super
55                // module, and only use Aho-Corasick for the actual automaton.
56                // The aho-corasick crate does have some extra prefilters,
57                // namely, looking for rare bytes to feed to memchr{,2,3}
58                // instead of just the first byte. If we end up wanting
59                // those---and they are somewhat tricky to implement---then
60                // we could port them to this crate.
61                //
62                // The main reason for doing things this way is so we have a
63                // complete and easy to understand picture of which prefilters
64                // are available and how they work. Otherwise it seems too
65                // easy to get into a situation where we have a prefilter
66                // layered on top of prefilter, and that might have unintended
67                // consequences.
68                .prefilter(false)
69                .build(needles);
70            let ac = match result {
71                Ok(ac) => ac,
72                Err(_err) => {
73                    debug!("aho-corasick prefilter failed to build: {_err}");
74                    return None;
75                }
76            };
77            Some(AhoCorasick { ac })
78        }
79    }
80}
81
82impl PrefilterI for AhoCorasick {
83    fn name(&self) -> &'static str {
84        "aho-corasick"
85    }
86
87    fn find(&self, haystack: &[u8], span: Span) -> Option<Span> {
88        #[cfg(not(feature = "perf-literal-multisubstring"))]
89        {
90            unreachable!()
91        }
92        #[cfg(feature = "perf-literal-multisubstring")]
93        {
94            let input =
95                aho_corasick::Input::new(haystack).span(span.start..span.end);
96            self.ac
97                .find(input)
98                .map(|m| Span { start: m.start(), end: m.end() })
99        }
100    }
101
102    fn prefix(&self, haystack: &[u8], span: Span) -> Option<Span> {
103        #[cfg(not(feature = "perf-literal-multisubstring"))]
104        {
105            unreachable!()
106        }
107        #[cfg(feature = "perf-literal-multisubstring")]
108        {
109            let input = aho_corasick::Input::new(haystack)
110                .anchored(aho_corasick::Anchored::Yes)
111                .span(span.start..span.end);
112            self.ac
113                .find(input)
114                .map(|m| Span { start: m.start(), end: m.end() })
115        }
116    }
117
118    fn memory_usage(&self) -> usize {
119        #[cfg(not(feature = "perf-literal-multisubstring"))]
120        {
121            unreachable!()
122        }
123        #[cfg(feature = "perf-literal-multisubstring")]
124        {
125            self.ac.memory_usage()
126        }
127    }
128
129    fn is_fast(&self) -> bool {
130        #[cfg(not(feature = "perf-literal-multisubstring"))]
131        {
132            unreachable!()
133        }
134        #[cfg(feature = "perf-literal-multisubstring")]
135        {
136            // Aho-Corasick is never considered "fast" because it's never
137            // going to be even close to an order of magnitude faster than the
138            // regex engine itself (assuming a DFA is used). In fact, it is
139            // usually slower. The magic of Aho-Corasick is that it can search
140            // a *large* number of literals with a relatively small amount of
141            // memory. The regex engines are far more wasteful.
142            //
143            // Aho-Corasick may be "fast" when the regex engine corresponds
144            // to, say, the PikeVM. That happens when the lazy DFA couldn't be
145            // built or used for some reason. But in these cases, the regex
146            // itself is likely quite big and we're probably hosed no matter
147            // what we do. (In this case, the best bet is for the caller to
148            // increase some of the memory limits on the hybrid cache capacity
149            // and hope that's enough.)
150            false
151        }
152    }
153}