Skip to main content

fancy_regex/
regexset.rs

1// Copyright 2026 The Fancy Regex Authors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19// THE SOFTWARE.
20
21//! RegexSet API for matching multiple patterns against the same input.
22//!
23//! This module provides [`RegexSet`], which allows efficient matching of multiple
24//! regular expression patterns against the same input text. This is particularly
25//! useful for applications like syntax highlighting, where many patterns need to
26//! be matched against each line of text.
27//!
28//! # Examples
29//!
30//! Basic usage:
31//!
32//! ```rust
33//! use fancy_regex::{RegexInput, RegexSet};
34//!
35//! # fn main() -> Result<(), fancy_regex::Error> {
36//! let set = RegexSet::new(&[
37//!     r"\d+",              // Pattern 0: numbers
38//!     r"\w+",              // Pattern 1: words
39//!     r"\.\d+",            // Pattern 2: decimal fractions - only matches later in "$29.99"
40//!     r"(?<=\$)\d+\.\d+",  // Pattern 3: prices (with lookbehind)
41//! ])?;
42//!
43//! let text = "$29.99";
44//!
45//! let mut matches = set
46//!     .find_input(RegexInput::new(text))?
47//!     .expect("expected at least one match");
48//!
49//! let first = matches.next().expect("expected first match")?;
50//! assert_eq!(first.pattern(), 0);
51//! assert_eq!(first.as_str(), "29");
52//! assert_eq!(first.start(), 1);
53//!
54//! let second = matches.next().expect("expected second match")?;
55//! assert_eq!(second.pattern(), 1);
56//! assert_eq!(second.as_str(), "29");
57//!
58//! // Pattern 2 (\.\d+) matches ".99" but only at position 3, not at the
59//! // earliest match position (1), so it is not returned here.
60//!
61//! let third = matches.next().expect("expected third match")?;
62//! assert_eq!(third.pattern(), 3);
63//! assert_eq!(third.as_str(), "29.99");
64//! assert!(matches.next().is_none());
65//! # Ok(())
66//! # }
67//! ```
68//!
69//! # Differences from `regex::RegexSet`
70//!
71//! [`regex::RegexSet`](https://docs.rs/regex/latest/regex/struct.RegexSet.html) and
72//! `fancy_regex::RegexSet` solve similar "multiple patterns" problems, but their APIs
73//! are intentionally different:
74//!
75//! - `regex::RegexSet` reports which patterns match somewhere in the haystack.
76//! - `fancy_regex::RegexSet::find_input` finds the **earliest match position** and then
77//!   yields concrete matches at that position in pattern index order.
78//! - `regex::RegexSet` does not produce captures or match spans. `fancy_regex::RegexSet`
79//!   yields [`RegexSetMatch`] values with captures and offsets.
80//! - Each yielded item is `Result<RegexSetMatch, Error>` because fancy features
81//!   (look-around, backreferences, etc.) can fail at runtime, for example due to a
82//!   backtrack limit.
83//!
84//! # Performance
85//!
86//! The `RegexSet` uses a hybrid approach to achieve good performance:
87//!
88//! A multi-pattern DFA is built for parallel evaluation, to provide very fast
89//! candidate position matching with linear time complexity.
90//!
91//! At the earliest candidate position:
92//! - any **hard patterns** (those with backreferences, lookaround, etc.) which could
93//!   match there are evaluated individually using a backtracking VM in anchored mode.
94//!   These may have exponential time complexity in pathological cases.
95//! - any **easy patterns** (those without backreferences, lookaround, etc.) which do
96//!   match are also individually run through the underlying regex crate, to resolve
97//!   capture groups etc. which may have been skipped in multi-DFA mode.
98//!
99//! For best performance, try to design patterns that can be fully delegated to the
100//! DFA when possible.
101//!
102//! # Priority and Non-Overlapping Matches
103//!
104//! `find_input` returns only matches at the earliest start offset. This is deliberate:
105//! the caller can inspect all matches at that position and decide which one has priority.
106//! This allows for most flexibility, without having to cater for various scenarios in
107//! the RegexSet itself. The `Input` struct makes it easy for the caller to advance
108//! position in case of an empty match winning, and the `RegexInput` struct sits on top
109//! of that to make it possible to specify rules like whether `\G` should match or not.
110
111use alloc::boxed::Box;
112use alloc::string::ToString;
113use alloc::sync::Arc;
114use alloc::vec::Vec;
115
116use crate::CompileOptions;
117use crate::Input;
118use crate::RegexInput;
119use crate::RegexOptionsBuilder;
120use regex_automata::hybrid::dfa;
121use regex_automata::meta::Builder as RaBuilder;
122use regex_automata::meta::Config as RaConfig;
123use regex_automata::meta::Regex as RaRegex;
124use regex_automata::nfa::thompson;
125use regex_automata::nfa::thompson::WhichCaptures;
126use regex_automata::util::pool::Pool;
127use regex_automata::util::syntax::Config as SyntaxConfig;
128use regex_automata::Anchored;
129use regex_automata::Input as RaInput;
130use regex_automata::MatchErrorKind;
131use regex_automata::MatchKind;
132use regex_automata::PatternID;
133use regex_automata::PatternSet;
134
135use crate::vm::OPTION_NOT_CONTINUED_FROM_PREVIOUS_MATCH;
136use crate::CompileError;
137use crate::Error;
138use crate::RegexOptions;
139use crate::{BytesMode, Captures, Regex, Result};
140
141type DfaCachePoolFactory = alloc::boxed::Box<
142    dyn Fn() -> dfa::Cache + Send + Sync + core::panic::UnwindSafe + core::panic::RefUnwindSafe,
143>;
144
145const BYTES_PER_MIB: usize = 1 << 20;
146const DEFAULT_META_NFA_SIZE_LIMIT: usize = 64 * BYTES_PER_MIB;
147const DEFAULT_META_HYBRID_CACHE_CAPACITY: usize = 64 * BYTES_PER_MIB;
148const DEFAULT_OVERLAPPING_DFA_CACHE_CAPACITY: usize = 64 * BYTES_PER_MIB;
149
150#[derive(Clone, Debug)]
151/// RegexSet API for matching multiple patterns against the same input.
152pub struct RegexSet {
153    regexes: Vec<Arc<Regex>>,
154    earliest_match_finder: RaRegex,
155    overlapping_dfa: Arc<dfa::DFA>,
156    overlapping_cache_pool: Arc<Pool<dfa::Cache, DfaCachePoolFactory>>,
157}
158
159#[derive(Clone, Debug)]
160/// Configuration for a RegexSet
161pub struct RegexSetOptions {
162    syntaxc: SyntaxConfig,
163    delegate_size_limit: Option<usize>,
164    delegate_dfa_size_limit: Option<usize>,
165    meta_nfa_size_limit: Option<usize>,
166    meta_hybrid_cache_capacity: usize,
167    overlapping_dfa_cache_capacity: usize,
168    overlapping_dfa_skip_cache_capacity_check: bool,
169    bytes_mode: BytesMode,
170}
171
172impl Default for RegexSetOptions {
173    fn default() -> Self {
174        let default_options = RegexOptions::default();
175        RegexSetOptions {
176            syntaxc: default_options.syntaxc,
177            delegate_size_limit: default_options.delegate_size_limit,
178            delegate_dfa_size_limit: default_options.delegate_dfa_size_limit,
179            meta_nfa_size_limit: Some(DEFAULT_META_NFA_SIZE_LIMIT),
180            meta_hybrid_cache_capacity: DEFAULT_META_HYBRID_CACHE_CAPACITY,
181            overlapping_dfa_cache_capacity: DEFAULT_OVERLAPPING_DFA_CACHE_CAPACITY,
182            overlapping_dfa_skip_cache_capacity_check: true,
183            bytes_mode: default_options.bytes_mode,
184        }
185    }
186}
187
188impl RegexSetOptions {
189    /// Create a new RegexSet options value with default settings.
190    pub fn new() -> Self {
191        Self::default()
192    }
193
194    /// Set the approximate size limit of each delegated sub-regex.
195    ///
196    /// This option is forwarded from the wrapped `regex` crate. Note that depending on the used
197    /// regex features there may be multiple delegated sub-regexes fed to the `regex` crate. As
198    /// such the actual limit is closer to `<number of delegated regexes> * delegate_size_limit`.
199    pub fn delegate_size_limit(mut self, limit: usize) -> Self {
200        self.delegate_size_limit = Some(limit);
201        self
202    }
203
204    /// Set the approximate size of the cache used by delegated DFAs.
205    ///
206    /// This option is forwarded from the wrapped `regex` crate. Note that depending on the used
207    /// regex features there may be multiple delegated sub-regexes fed to the `regex` crate. As
208    /// such the actual limit is closer to `<number of delegated regexes> *
209    /// delegate_dfa_size_limit`.
210    pub fn delegate_dfa_size_limit(mut self, limit: usize) -> Self {
211        self.delegate_dfa_size_limit = Some(limit);
212        self
213    }
214
215    /// Set the approximate NFA size limit for the regex-automata meta regex used to find the
216    /// earliest match position.
217    ///
218    /// Passing `None` disables the limit. The default is 64 MiB.
219    pub fn meta_nfa_size_limit(mut self, limit: Option<usize>) -> Self {
220        self.meta_nfa_size_limit = limit;
221        self
222    }
223
224    /// Set the cache capacity, in bytes, for the lazy DFA used by the regex-automata meta regex
225    /// that finds the earliest match position.
226    ///
227    /// The default is 64 MiB.
228    pub fn meta_hybrid_cache_capacity(mut self, limit: usize) -> Self {
229        self.meta_hybrid_cache_capacity = limit;
230        self
231    }
232
233    /// Set the cache capacity, in bytes, for the lazy DFA used to find overlapping matches at a
234    /// candidate match position.
235    ///
236    /// The default is 64 MiB.
237    pub fn overlapping_dfa_cache_capacity(mut self, limit: usize) -> Self {
238        self.overlapping_dfa_cache_capacity = limit;
239        self
240    }
241
242    /// Configure whether the overlapping DFA builder should skip its minimum cache capacity check.
243    ///
244    /// Enabling this can allow large DFAs to build when the default cache capacity check would
245    /// reject them, but it may allocate more memory at build time. The default is `true`.
246    pub fn overlapping_dfa_skip_cache_capacity_check(mut self, yes: bool) -> Self {
247        self.overlapping_dfa_skip_cache_capacity_check = yes;
248        self
249    }
250}
251
252impl RegexSet {
253    /// Create a new RegexSet from an iterator of patterns using default options.
254    ///
255    /// All patterns will use the same default configuration:
256    /// - Case sensitive
257    /// - Multi-line mode disabled
258    /// - Dot does not match newline
259    /// - Unicode mode enabled
260    ///
261    /// # Errors
262    ///
263    /// Returns an error if any pattern fails to compile.
264    pub fn new<I, S>(patterns: I) -> Result<Self>
265    where
266        I: IntoIterator<Item = S>,
267        S: AsRef<str>,
268    {
269        let builder = RegexOptionsBuilder::new();
270        Self::new_with_options(patterns, &builder)
271    }
272
273    /// Create a new RegexSet from an iterator of patterns using specified options.
274    ///
275    /// # Errors
276    ///
277    /// Returns an error if any pattern fails to compile.
278    pub fn new_with_options<I, S>(
279        patterns: I,
280        options_builder: &RegexOptionsBuilder,
281    ) -> Result<Self>
282    where
283        I: IntoIterator<Item = S>,
284        S: AsRef<str>,
285    {
286        // Set members are only ever searched anchored at candidate positions
287        // (see `match_pattern_at_input_position`), where the engine never
288        // consults a prefilter — skip spending build time and memory on one.
289        let mut member_options = options_builder.options.clone();
290        member_options.delegate_prefilter = false;
291        let regexes = patterns
292            .into_iter()
293            .map(|pattern| {
294                Regex::new_options(pattern.as_ref().to_string(), &member_options).map(Arc::new)
295            })
296            .collect::<Result<Vec<_>>>()?;
297
298        let config = RegexSetOptions {
299            syntaxc: options_builder.options.syntaxc,
300            delegate_size_limit: options_builder.options.delegate_size_limit,
301            delegate_dfa_size_limit: options_builder.options.delegate_dfa_size_limit,
302            meta_nfa_size_limit: Some(DEFAULT_META_NFA_SIZE_LIMIT),
303            meta_hybrid_cache_capacity: DEFAULT_META_HYBRID_CACHE_CAPACITY,
304            overlapping_dfa_cache_capacity: DEFAULT_OVERLAPPING_DFA_CACHE_CAPACITY,
305            overlapping_dfa_skip_cache_capacity_check: true,
306            bytes_mode: options_builder.options.bytes_mode,
307        };
308        Self::from_regexes(regexes, config)
309    }
310
311    /// Create a new RegexSet from pre-built `Arc<Regex>` instances.
312    ///
313    /// # Examples
314    ///
315    /// ```rust
316    /// use fancy_regex::{Regex, RegexBuilder, RegexInput, RegexSet, RegexSetOptions};
317    /// use std::sync::Arc;
318    ///
319    /// # fn main() -> Result<(), fancy_regex::Error> {
320    /// // Create regexes with different options
321    /// let re1 = Arc::new(RegexBuilder::new(r"hello")
322    ///     .case_insensitive(true)
323    ///     .build()?);
324    /// let re2 = Arc::new(Regex::new(r"\d+")?);
325    /// let re3 = Arc::new(Regex::new(r"(?<=\w)end")?); // lookbehind - fancy pattern
326    ///
327    /// // Combine them into a RegexSet
328    /// let set = RegexSet::from_regexes([re1, re2, re3], Default::default())?;
329    ///
330    /// let text = "HELLO";
331    /// let mut matches = set
332    ///     .find_input(RegexInput::new(text))?
333    ///     .expect("expected at least one match");
334    ///
335    /// let m = matches.next().expect("expected first match")?;
336    /// assert_eq!(m.pattern(), 0);
337    /// assert_eq!(m.as_str(), "HELLO");
338    /// assert!(matches.next().is_none());
339    /// # Ok(())
340    /// # }
341    /// ```
342    ///
343    /// # Errors
344    ///
345    /// Returns an error if the multi-pattern DFA construction fails.
346    pub fn from_regexes<I>(regexes: I, config: RegexSetOptions) -> Result<Self>
347    where
348        I: IntoIterator<Item = Arc<Regex>>,
349    {
350        let regexes_vec: Vec<Arc<Regex>> = regexes.into_iter().collect();
351
352        let mut patterns = Vec::with_capacity(regexes_vec.len());
353
354        for regex in &regexes_vec {
355            patterns.push(regex.seek_pattern());
356        }
357
358        let compile_options = CompileOptions {
359            bytes_mode: config.bytes_mode,
360            unicode: config.syntaxc.get_unicode() && !matches!(config.bytes_mode, BytesMode::Ascii),
361            delegate_size_limit: config.delegate_size_limit,
362            delegate_dfa_size_limit: config.delegate_dfa_size_limit,
363            ..CompileOptions::default()
364        };
365
366        let utf8 = matches!(compile_options.bytes_mode, BytesMode::Unicode);
367
368        // Parse each pattern once and share the resulting Hirs between the two
369        // set-wide engines (the "earliest match" meta regex and the overlapping
370        // DFA), instead of letting each engine re-parse every pattern string.
371        let syntax_config = SyntaxConfig::new()
372            .utf8(utf8)
373            .unicode(compile_options.unicode);
374        let hirs = regex_automata::util::syntax::parse_many_with(&patterns, &syntax_config)
375            .map_err(|e| {
376                Error::CompileError(Box::new(CompileError::UnexpectedGeneralError(
377                    alloc::format!("failed to parse regex set pattern: {}", e),
378                )))
379            })?;
380
381        // The multi-pattern "earliest match" engine is searched unanchored. The
382        // syntax options are already encoded in the Hirs.
383        let mut earliest_builder = RaBuilder::new();
384        earliest_builder.configure(
385            RaConfig::new()
386                .match_kind(MatchKind::LeftmostFirst)
387                .nfa_size_limit(config.meta_nfa_size_limit)
388                .hybrid_cache_capacity(config.meta_hybrid_cache_capacity),
389        );
390        let earliest_match_finder = earliest_builder
391            .build_many_from_hir(&hirs)
392            .map_err(CompileError::InnerError)
393            .map_err(|e| Error::CompileError(Box::new(e)))?;
394
395        let format_patterns = || {
396            patterns
397                .iter()
398                .enumerate()
399                .map(|(i, p)| alloc::format!("[{}]: {}", i, p))
400                .collect::<Vec<_>>()
401                .join("\n---\n")
402        };
403
404        // Build the overlapping DFA from a Thompson NFA compiled from the same
405        // Hirs. This replicates what `dfa::Builder::build_many` does internally
406        // (it forces `WhichCaptures::None` and calls `build_from_nfa`), minus
407        // the extra pattern parse.
408        let mut thompson_config = thompson::Config::new().which_captures(WhichCaptures::None);
409        if let Some(limit) = compile_options.delegate_size_limit {
410            thompson_config = thompson_config.nfa_size_limit(Some(limit));
411        }
412        let nfa = thompson::Compiler::new()
413            .configure(thompson_config)
414            .build_many_from_hir(&hirs)
415            .map_err(|e| {
416                Error::CompileError(Box::new(CompileError::DfaBuildError(
417                    format_patterns(),
418                    e.to_string(),
419                )))
420            })?;
421
422        let mut overlapping_dfa_builder = dfa::DFA::builder();
423        let mut overlapping_config = dfa::Config::new()
424            .match_kind(MatchKind::All)
425            .unicode_word_boundary(compile_options.unicode)
426            .cache_capacity(config.overlapping_dfa_cache_capacity);
427        if config.overlapping_dfa_skip_cache_capacity_check {
428            overlapping_config = overlapping_config.skip_cache_capacity_check(true);
429        }
430        overlapping_dfa_builder.configure(overlapping_config);
431        let overlapping_dfa =
432            Arc::new(overlapping_dfa_builder.build_from_nfa(nfa).map_err(|e| {
433                Error::CompileError(Box::new(CompileError::DfaBuildError(
434                    format_patterns(),
435                    e.to_string(),
436                )))
437            })?);
438        let create: DfaCachePoolFactory = alloc::boxed::Box::new({
439            let dfa = Arc::clone(&overlapping_dfa);
440            move || dfa.create_cache()
441        });
442        let overlapping_cache_pool = Arc::new(Pool::new(create));
443
444        Ok(Self {
445            regexes: regexes_vec,
446            earliest_match_finder,
447            overlapping_dfa,
448            overlapping_cache_pool,
449        })
450    }
451
452    /// Returns the number of patterns in the set.
453    pub fn len(&self) -> usize {
454        self.regexes.len()
455    }
456
457    /// Returns true if the set contains no patterns.
458    pub fn is_empty(&self) -> bool {
459        self.len() == 0
460    }
461
462    /// Returns an iterator over matches at the earliest match position - if any.
463    /// Iterator yields matches in pattern index order
464    pub fn find_input<'r, 't, S: Input + ?Sized>(
465        &'r self,
466        input: RegexInput<'t, S>,
467    ) -> Result<Option<RegexSetMatchesAt<'r, 't, S>>> {
468        if input.is_done() {
469            return Ok(None);
470        }
471
472        let haystack = input.haystack();
473        let match_range = input.get_range();
474        let mut search_start = input.effective_start();
475        let mut seen_pattern_indices = PatternSet::new(self.regexes.len());
476
477        while search_start <= match_range.end {
478            let ra_input = RaInput::new(haystack.as_bytes())
479                .range(search_start..match_range.end)
480                .anchored(if input.is_anchored() {
481                    Anchored::Yes
482                } else {
483                    Anchored::No
484                });
485            let Some(candidate) = self.earliest_match_finder.search(&ra_input) else {
486                return Ok(None);
487            };
488            let match_start = candidate.start();
489            let overlapping_input = RaInput::new(haystack.as_bytes())
490                .anchored(Anchored::Yes)
491                .range(match_start..match_range.end);
492            seen_pattern_indices.clear();
493            {
494                let mut cache_guard = self.overlapping_cache_pool.get();
495                if let Err(e) = self.overlapping_dfa.try_which_overlapping_matches(
496                    &mut cache_guard,
497                    &overlapping_input,
498                    &mut seen_pattern_indices,
499                ) {
500                    match e.kind() {
501                        MatchErrorKind::Quit { .. } | MatchErrorKind::GaveUp { .. } => {
502                            // The DFA gave up (e.g. it encountered non-ASCII bytes while
503                            // unicode word boundaries are enabled, which adds quit bytes
504                            // for multi-byte UTF-8 sequences). Fall back to trying every
505                            // pattern at this position so correctness is preserved.
506                            seen_pattern_indices.clear();
507                            for i in 0..self.regexes.len() {
508                                seen_pattern_indices.insert(PatternID::must(i));
509                            }
510                        }
511                        _ => panic!("unexpected overlapping DFA error: {:?}", e),
512                    }
513                }
514            } // release cache_guard back to pool before doing per-pattern matching
515              // Verify candidates straight off the PatternSet (ascending pattern
516              // order). Only when a match is found are the *remaining* indices
517              // collected, for lazy verification while the caller iterates — so
518              // a failed candidate position allocates nothing.
519            let mut candidate_pattern_indices = seen_pattern_indices
520                .iter()
521                .map(|pattern| pattern.as_usize());
522            let mut first_match = None;
523            for pattern_index in &mut candidate_pattern_indices {
524                if let Some(candidate_match) =
525                    self.match_pattern_at_input_position(pattern_index, &input, match_start)?
526                {
527                    first_match = Some(candidate_match);
528                    break;
529                }
530            }
531
532            if let Some(first_match) = first_match {
533                let pending_pattern_indices = candidate_pattern_indices.collect::<Vec<_>>();
534                return Ok(Some(RegexSetMatchesAt {
535                    regex_set: self,
536                    input,
537                    haystack,
538                    match_start,
539                    first_match: Some(first_match),
540                    pending_pattern_indices: pending_pattern_indices.into_iter(),
541                }));
542            }
543
544            search_start = haystack.advance_position(match_start);
545        }
546
547        Ok(None)
548    }
549
550    fn match_pattern_at_input_position<'t, S: Input + ?Sized>(
551        &self,
552        pattern_index: usize,
553        input: &RegexInput<'t, S>,
554        match_start: usize,
555    ) -> Result<Option<RegexSetMatch<'t, S>>> {
556        let candidate_input = input.clone().from_pos(match_start).anchored(true);
557        let regex = &self.regexes[pattern_index];
558        let mut option_flags = 0;
559        if input.start() < match_start {
560            option_flags |= OPTION_NOT_CONTINUED_FROM_PREVIOUS_MATCH;
561        }
562        if regex.captures_len() == 1 {
563            return Ok(regex.find_input_raw(&candidate_input, option_flags)?.map(
564                |(start, end)| RegexSetMatch {
565                    pattern_index,
566                    captures: regex.captures_for_span(input.haystack(), start, end),
567                },
568            ));
569        }
570        Ok(regex
571            .captures_input_with_option_flags(&candidate_input, option_flags)?
572            .map(|captures| RegexSetMatch {
573                pattern_index,
574                captures,
575            }))
576    }
577}
578
579/// A match from a RegexSet, including the pattern index and capture groups.
580///
581/// This type represents a single match found by a [`RegexSet`]. It provides
582/// information about which pattern matched, the location of the match, and
583/// access to any capture groups.
584///
585/// # Examples
586///
587/// ```rust
588/// use fancy_regex::{RegexInput, RegexSet};
589///
590/// # fn main() -> Result<(), fancy_regex::Error> {
591/// let set = RegexSet::new(&[r"\w+"])?;
592/// let mut matches = set
593///     .find_input(RegexInput::new("abc"))?
594///     .expect("expected at least one match");
595///
596/// let m = matches.next().expect("expected first match")?;
597/// assert_eq!(m.pattern(), 0);
598/// assert_eq!(m.get().as_str(), "abc");
599/// assert_eq!(m.start(), 0);
600/// assert_eq!(m.end(), 3);
601/// assert_eq!(m.captures().len(), 1);
602/// # Ok(())
603/// # }
604/// ```
605#[derive(Debug)]
606pub struct RegexSetMatch<'t, S: Input + ?Sized> {
607    pattern_index: usize,
608    captures: Captures<'t, S>,
609}
610
611impl<'t, S: Input + ?Sized> RegexSetMatch<'t, S> {
612    /// Returns the pattern index that matched.
613    pub fn pattern(&self) -> usize {
614        self.pattern_index
615    }
616
617    /// Returns the full set of capture groups for this match.
618    pub fn captures(&self) -> &Captures<'t, S> {
619        &self.captures
620    }
621
622    /// Returns the full match.
623    pub fn get(&self) -> S::Match<'t> {
624        self.captures
625            .get(0)
626            .expect("`RegexSetMatch` must always contain the overall match")
627    }
628
629    /// Returns the start offset of the full match.
630    pub fn start(&self) -> usize {
631        self.captures
632            .get_span(0)
633            .expect("`RegexSetMatch` must always contain the overall match")
634            .0
635    }
636
637    /// Returns the end offset of the full match.
638    pub fn end(&self) -> usize {
639        self.captures
640            .get_span(0)
641            .expect("`RegexSetMatch` must always contain the overall match")
642            .1
643    }
644}
645
646impl<'t> RegexSetMatch<'t, str> {
647    /// Returns the matched text.
648    pub fn as_str(&self) -> &'t str {
649        self.captures
650            .get(0)
651            .expect("`RegexSetMatch` must always contain the overall match")
652            .as_str()
653    }
654}
655
656#[derive(Debug)]
657pub struct RegexSetMatchesAt<'r, 't, S: Input + ?Sized> {
658    regex_set: &'r RegexSet,
659    input: RegexInput<'t, S>,
660    haystack: &'t S,
661    match_start: usize,
662    first_match: Option<RegexSetMatch<'t, S>>,
663    pending_pattern_indices: alloc::vec::IntoIter<usize>,
664}
665
666impl<'r, 't, S: Input + ?Sized> RegexSetMatchesAt<'r, 't, S> {
667    /// Returns the originating regex set.
668    pub fn regex_set(&self) -> &'r RegexSet {
669        self.regex_set
670    }
671
672    /// Returns the searched haystack.
673    pub fn haystack(&self) -> &'t S {
674        self.haystack
675    }
676
677    /// Returns the earliest start position shared by these matches.
678    pub fn start(&self) -> usize {
679        self.match_start
680    }
681}
682
683impl<'r, 't, S: Input + ?Sized> Iterator for RegexSetMatchesAt<'r, 't, S> {
684    type Item = Result<RegexSetMatch<'t, S>>;
685
686    fn next(&mut self) -> Option<Self::Item> {
687        if let Some(first_match) = self.first_match.take() {
688            return Some(Ok(first_match));
689        }
690
691        for pattern_index in self.pending_pattern_indices.by_ref() {
692            match self.regex_set.match_pattern_at_input_position(
693                pattern_index,
694                &self.input,
695                self.match_start,
696            ) {
697                Ok(Some(regex_set_match)) => return Some(Ok(regex_set_match)),
698                Ok(None) => continue,
699                Err(err) => return Some(Err(err)),
700            }
701        }
702
703        None
704    }
705}
706
707#[cfg(test)]
708mod tests {
709    use super::{
710        RegexSet, RegexSetOptions, DEFAULT_META_HYBRID_CACHE_CAPACITY, DEFAULT_META_NFA_SIZE_LIMIT,
711        DEFAULT_OVERLAPPING_DFA_CACHE_CAPACITY,
712    };
713    use crate::{Error, RegexInput, RegexOptionsBuilder, RuntimeError};
714
715    #[test]
716    fn regex_set_options_defaults_to_larger_regex_automata_limits() {
717        let options = RegexSetOptions::default();
718
719        assert_eq!(
720            options.meta_nfa_size_limit,
721            Some(DEFAULT_META_NFA_SIZE_LIMIT)
722        );
723        assert_eq!(
724            options.meta_hybrid_cache_capacity,
725            DEFAULT_META_HYBRID_CACHE_CAPACITY
726        );
727        assert_eq!(
728            options.overlapping_dfa_cache_capacity,
729            DEFAULT_OVERLAPPING_DFA_CACHE_CAPACITY
730        );
731        assert!(options.overlapping_dfa_skip_cache_capacity_check);
732    }
733
734    #[test]
735    fn regex_set_options_setters_update_regex_automata_limits() {
736        let options = RegexSetOptions::new()
737            .delegate_size_limit(11)
738            .delegate_dfa_size_limit(22)
739            .meta_nfa_size_limit(None)
740            .meta_hybrid_cache_capacity(33)
741            .overlapping_dfa_cache_capacity(44)
742            .overlapping_dfa_skip_cache_capacity_check(false);
743
744        assert_eq!(options.delegate_size_limit, Some(11));
745        assert_eq!(options.delegate_dfa_size_limit, Some(22));
746        assert_eq!(options.meta_nfa_size_limit, None);
747        assert_eq!(options.meta_hybrid_cache_capacity, 33);
748        assert_eq!(options.overlapping_dfa_cache_capacity, 44);
749        assert!(!options.overlapping_dfa_skip_cache_capacity_check);
750    }
751
752    #[test]
753    fn find_input_returns_all_matches_at_earliest_position_in_pattern_order() {
754        let set = RegexSet::new(&[r"\d+", r"\w+", r"(?<=\$)\d+\.\d+"]).unwrap();
755        let mut matches = set.find_input(RegexInput::new("$29.99")).unwrap().unwrap();
756
757        let first = matches.next().unwrap().unwrap();
758        assert_eq!(0, first.pattern());
759        assert_eq!(1, first.start());
760        assert_eq!(3, first.end());
761        assert_eq!("29", first.as_str());
762
763        let second = matches.next().unwrap().unwrap();
764        assert_eq!(1, second.pattern());
765        assert_eq!(1, second.start());
766        assert_eq!(3, second.end());
767        assert_eq!("29", second.as_str());
768
769        let third = matches.next().unwrap().unwrap();
770        assert_eq!(2, third.pattern());
771        assert_eq!(1, third.start());
772        assert_eq!(6, third.end());
773        assert_eq!("29.99", third.as_str());
774
775        assert!(matches.next().is_none());
776    }
777
778    #[test]
779    fn find_input_skips_false_positive_candidate_positions() {
780        let set = RegexSet::new(&[r"(?<=foo)bar"]).unwrap();
781        let mut matches = set
782            .find_input(RegexInput::new("barfoobar"))
783            .unwrap()
784            .unwrap();
785
786        let only = matches.next().unwrap().unwrap();
787        assert_eq!(0, only.pattern());
788        assert_eq!(6, only.start());
789        assert_eq!(9, only.end());
790        assert_eq!("bar", only.as_str());
791        assert!(matches.next().is_none());
792    }
793
794    #[test]
795    fn find_input_returns_none_when_input_is_done() {
796        let set = RegexSet::new(&[r"."]).unwrap();
797
798        assert!(set
799            .find_input(RegexInput::new("a").from_pos(2))
800            .unwrap()
801            .is_none());
802    }
803
804    #[test]
805    fn find_input_returns_none_when_input_is_anchored_and_match_not_at_start_position() {
806        let set = RegexSet::new(&[r"b"]).unwrap();
807
808        assert!(set
809            .find_input(RegexInput::new("ab").from_pos(0).anchored(true))
810            .unwrap()
811            .is_none());
812    }
813
814    #[test]
815    fn find_input_returns_match_when_input_is_anchored_and_match_at_start_position() {
816        let set = RegexSet::new(&[r"b"]).unwrap();
817
818        let mut matches = set
819            .find_input(RegexInput::new("ab").from_pos(1).anchored(true))
820            .unwrap()
821            .unwrap();
822
823        let only = matches.next().unwrap().unwrap();
824        assert_eq!(0, only.pattern());
825        assert_eq!(1, only.start());
826        assert_eq!(2, only.end());
827        assert_eq!("b", only.as_str());
828        assert!(matches.next().is_none());
829    }
830
831    #[test]
832    fn find_input_defers_later_pattern_evaluation_until_iteration() {
833        let mut options_builder = RegexOptionsBuilder::new();
834        options_builder.backtrack_limit(0);
835        let set = RegexSet::new_with_options(&[r"a", r"(?:(a|aa)+)\1"], &options_builder).unwrap();
836
837        let mut matches = set.find_input(RegexInput::new("aa")).unwrap().unwrap();
838
839        let first = matches.next().unwrap().unwrap();
840        assert_eq!(0, first.pattern());
841        assert_eq!(0, first.start());
842        assert_eq!(1, first.end());
843
844        let second = matches.next().unwrap();
845        assert!(matches!(
846            second,
847            Err(Error::RuntimeError(RuntimeError::BacktrackLimitExceeded))
848        ));
849    }
850
851    #[test]
852    fn find_input_picks_earliest_start_position_before_iterating_pattern_order() {
853        let mut options_builder = RegexOptionsBuilder::new();
854        options_builder.multi_line(true);
855        let set = RegexSet::new_with_options(
856            &[
857                r"//.*$",
858                r#""(?:[^"\\]|\\.)*""#,
859                r"\b(fn|let|mut|if|else)\b",
860                r"\b[0-9]+\b",
861                r"[a-zA-Z_][a-zA-Z0-9_]*",
862            ],
863            &options_builder,
864        )
865        .unwrap();
866
867        let mut matches = set
868            .find_input(RegexInput::new(
869                "let x = 42; // a comment\nlet s = \"hello world\";",
870            ))
871            .unwrap()
872            .unwrap();
873
874        let first = matches.next().unwrap().unwrap();
875        assert_eq!(2, first.pattern());
876        assert_eq!(0, first.start());
877        assert_eq!(3, first.end());
878        assert_eq!("let", first.as_str());
879    }
880
881    #[test]
882    fn find_input_yields_each_pattern_at_match_start_once() {
883        let set = RegexSet::new(&[r"a+", r"a"]).unwrap();
884        let mut matches = set.find_input(RegexInput::new("aaa")).unwrap().unwrap();
885
886        let first = matches.next().unwrap().unwrap();
887        assert_eq!(0, first.pattern());
888        assert_eq!(0, first.start());
889        assert_eq!(3, first.end());
890        assert_eq!("aaa", first.as_str());
891
892        let second = matches.next().unwrap().unwrap();
893        assert_eq!(1, second.pattern());
894        assert_eq!(0, second.start());
895        assert_eq!(1, second.end());
896        assert_eq!("a", second.as_str());
897
898        assert!(matches.next().is_none());
899    }
900
901    #[test]
902    fn test_no_captures_returns_group_0() {
903        let set = RegexSet::new(&[r"\w+"]).unwrap();
904        let mut matches = set.find_input(RegexInput::new("abc")).unwrap().unwrap();
905
906        let only = matches.next().unwrap().unwrap();
907        assert_eq!(0, only.pattern());
908        assert_eq!(1, only.captures().len());
909        assert_eq!("abc", only.as_str());
910    }
911
912    #[test]
913    fn word_boundary_matches_correctly_with_unicode_text() {
914        // \bbar\b uses a unicode word boundary; the DFA may encounter quit bytes
915        // for multi-byte UTF-8 characters such as 'é' (0xC3 0xA9). The RegexSet
916        // must not panic and must still return the correct match.
917        let set = RegexSet::new([r"foo", r"\bbar\b"]).unwrap();
918        let mut matches = set
919            .find_input(RegexInput::new("fooé bar"))
920            .unwrap()
921            .unwrap();
922
923        // "foo" is the earliest match (position 0)
924        let first = matches.next().unwrap().unwrap();
925        assert_eq!(0, first.pattern());
926        assert_eq!("foo", first.as_str());
927        assert!(matches.next().is_none());
928
929        // "bar" is matched later in the string (after the unicode character)
930        let mut matches2 = set
931            .find_input(RegexInput::new("fooé bar").from_pos(first.end()))
932            .unwrap()
933            .unwrap();
934        let bar_match = matches2.next().unwrap().unwrap();
935        assert_eq!(1, bar_match.pattern());
936        assert_eq!("bar", bar_match.as_str());
937        assert!(matches2.next().is_none());
938    }
939
940    #[test]
941    fn find_input_continue_from_prev_match_inside_negative_lookbehind() {
942        let options = &mut RegexOptionsBuilder::new();
943        let options = options.allow_input_assertion_overrides(true);
944        let set = RegexSet::new_with_options([r"(?<!\G)b"], &options).unwrap();
945        let mut matches = set
946            .find_input(RegexInput::new("ab").continue_from_previous_match_end(false))
947            .unwrap()
948            .unwrap();
949
950        let first = matches.next().unwrap().unwrap();
951        assert_eq!(0, first.pattern());
952        assert_eq!("b", first.as_str());
953        assert!(matches.next().is_none());
954    }
955
956    #[test]
957    fn continue_from_prev_match_works_as_expected_when_match_is_not_at_search_start() {
958        use crate::Arc;
959        use crate::RegexBuilder;
960
961        let (pat, hay) = (r"\Gx", "yx");
962
963        // `\G` holds only at the search start (index 0); 'x' is at 1 -> no match.
964        let re = RegexBuilder::new(pat).build().unwrap();
965        let single = re
966            .find_input(RegexInput::new(hay).from_pos(0))
967            .unwrap()
968            .map(|m| (m.start(), m.end()));
969
970        // Same regex, same input, via a RegexSet:
971        let set = RegexSet::from_regexes([Arc::new(re)], Default::default()).unwrap();
972        let via_set = set
973            .find_input(RegexInput::new(hay).from_pos(0))
974            .unwrap()
975            .and_then(|mut it| it.next())
976            .map(|m| {
977                let m = m.unwrap();
978                (m.start(), m.end())
979            });
980
981        assert_eq!(
982            single, via_set,
983            "RegexSet should behave the same as a standalone regex"
984        );
985        assert_eq!(single, None);
986    }
987
988    #[test]
989    fn continue_from_prev_match_works_as_expected_when_match_is_at_search_start() {
990        use crate::Arc;
991        use crate::RegexBuilder;
992
993        let (pat, hay) = (r"\Gx", "yx");
994
995        let re = RegexBuilder::new(pat).build().unwrap();
996        let single = re
997            .find_input(RegexInput::new(hay).from_pos(1))
998            .unwrap()
999            .map(|m| (m.start(), m.end()));
1000
1001        // Same regex, same input, via a RegexSet:
1002        let set = RegexSet::from_regexes([Arc::new(re)], Default::default()).unwrap();
1003        let via_set = set
1004            .find_input(RegexInput::new(hay).from_pos(1))
1005            .unwrap()
1006            .and_then(|mut it| it.next())
1007            .map(|m| {
1008                let m = m.unwrap();
1009                (m.start(), m.end())
1010            });
1011
1012        assert_eq!(
1013            single, via_set,
1014            "RegexSet should behave the same as a standalone regex"
1015        );
1016        assert_eq!(single, Some((1, 2)));
1017    }
1018}