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 capacity, in bytes, of the cache used by the lazy DFA of delegated
205    /// regexes.
206    ///
207    /// This corresponds to `hybrid_cache_capacity` in regex-automata, and has the same meaning as
208    /// `RegexBuilder::dfa_size_limit` in the `regex` crate. Note that depending on the used
209    /// regex features there may be multiple delegated sub-regexes. As such the actual limit is
210    /// closer to `<number of delegated regexes> * delegate_dfa_size_limit`.
211    pub fn delegate_dfa_size_limit(mut self, limit: usize) -> Self {
212        self.delegate_dfa_size_limit = Some(limit);
213        self
214    }
215
216    /// Set the approximate NFA size limit for the regex-automata meta regex used to find the
217    /// earliest match position.
218    ///
219    /// Passing `None` disables the limit. The default is 64 MiB.
220    pub fn meta_nfa_size_limit(mut self, limit: Option<usize>) -> Self {
221        self.meta_nfa_size_limit = limit;
222        self
223    }
224
225    /// Set the cache capacity, in bytes, for the lazy DFA used by the regex-automata meta regex
226    /// that finds the earliest match position.
227    ///
228    /// The default is 64 MiB.
229    pub fn meta_hybrid_cache_capacity(mut self, limit: usize) -> Self {
230        self.meta_hybrid_cache_capacity = limit;
231        self
232    }
233
234    /// Set the cache capacity, in bytes, for the lazy DFA used to find overlapping matches at a
235    /// candidate match position.
236    ///
237    /// The default is 64 MiB.
238    pub fn overlapping_dfa_cache_capacity(mut self, limit: usize) -> Self {
239        self.overlapping_dfa_cache_capacity = limit;
240        self
241    }
242
243    /// Configure whether the overlapping DFA builder should skip its minimum cache capacity check.
244    ///
245    /// Enabling this can allow large DFAs to build when the default cache capacity check would
246    /// reject them, but it may allocate more memory at build time. The default is `true`.
247    pub fn overlapping_dfa_skip_cache_capacity_check(mut self, yes: bool) -> Self {
248        self.overlapping_dfa_skip_cache_capacity_check = yes;
249        self
250    }
251}
252
253impl RegexSet {
254    /// Create a new RegexSet from an iterator of patterns using default options.
255    ///
256    /// All patterns will use the same default configuration:
257    /// - Case sensitive
258    /// - Multi-line mode disabled
259    /// - Dot does not match newline
260    /// - Unicode mode enabled
261    ///
262    /// # Errors
263    ///
264    /// Returns an error if any pattern fails to compile.
265    pub fn new<I, S>(patterns: I) -> Result<Self>
266    where
267        I: IntoIterator<Item = S>,
268        S: AsRef<str>,
269    {
270        let builder = RegexOptionsBuilder::new();
271        Self::new_with_options(patterns, &builder)
272    }
273
274    /// Create a new RegexSet from an iterator of patterns using specified options.
275    ///
276    /// # Errors
277    ///
278    /// Returns an error if any pattern fails to compile.
279    pub fn new_with_options<I, S>(
280        patterns: I,
281        options_builder: &RegexOptionsBuilder,
282    ) -> Result<Self>
283    where
284        I: IntoIterator<Item = S>,
285        S: AsRef<str>,
286    {
287        // Set members are only ever searched anchored at candidate positions
288        // (see `match_pattern_at_input_position`), where the engine never
289        // consults a prefilter — skip spending build time and memory on one.
290        let mut member_options = options_builder.options.clone();
291        member_options.delegate_prefilter = false;
292        let regexes = patterns
293            .into_iter()
294            .map(|pattern| {
295                Regex::new_options(pattern.as_ref().to_string(), &member_options).map(Arc::new)
296            })
297            .collect::<Result<Vec<_>>>()?;
298
299        let config = RegexSetOptions {
300            syntaxc: options_builder.options.syntaxc,
301            delegate_size_limit: options_builder.options.delegate_size_limit,
302            delegate_dfa_size_limit: options_builder.options.delegate_dfa_size_limit,
303            meta_nfa_size_limit: Some(DEFAULT_META_NFA_SIZE_LIMIT),
304            meta_hybrid_cache_capacity: DEFAULT_META_HYBRID_CACHE_CAPACITY,
305            overlapping_dfa_cache_capacity: DEFAULT_OVERLAPPING_DFA_CACHE_CAPACITY,
306            overlapping_dfa_skip_cache_capacity_check: true,
307            bytes_mode: options_builder.options.bytes_mode,
308        };
309        Self::from_regexes(regexes, config)
310    }
311
312    /// Create a new RegexSet from pre-built `Arc<Regex>` instances.
313    ///
314    /// # Examples
315    ///
316    /// ```rust
317    /// use fancy_regex::{Regex, RegexBuilder, RegexInput, RegexSet, RegexSetOptions};
318    /// use std::sync::Arc;
319    ///
320    /// # fn main() -> Result<(), fancy_regex::Error> {
321    /// // Create regexes with different options
322    /// let re1 = Arc::new(RegexBuilder::new(r"hello")
323    ///     .case_insensitive(true)
324    ///     .build()?);
325    /// let re2 = Arc::new(Regex::new(r"\d+")?);
326    /// let re3 = Arc::new(Regex::new(r"(?<=\w)end")?); // lookbehind - fancy pattern
327    ///
328    /// // Combine them into a RegexSet
329    /// let set = RegexSet::from_regexes([re1, re2, re3], Default::default())?;
330    ///
331    /// let text = "HELLO";
332    /// let mut matches = set
333    ///     .find_input(RegexInput::new(text))?
334    ///     .expect("expected at least one match");
335    ///
336    /// let m = matches.next().expect("expected first match")?;
337    /// assert_eq!(m.pattern(), 0);
338    /// assert_eq!(m.as_str(), "HELLO");
339    /// assert!(matches.next().is_none());
340    /// # Ok(())
341    /// # }
342    /// ```
343    ///
344    /// # Errors
345    ///
346    /// Returns an error if the multi-pattern DFA construction fails.
347    pub fn from_regexes<I>(regexes: I, config: RegexSetOptions) -> Result<Self>
348    where
349        I: IntoIterator<Item = Arc<Regex>>,
350    {
351        let regexes_vec: Vec<Arc<Regex>> = regexes.into_iter().collect();
352
353        let mut patterns = Vec::with_capacity(regexes_vec.len());
354
355        for regex in &regexes_vec {
356            patterns.push(regex.seek_pattern());
357        }
358
359        let compile_options = CompileOptions {
360            bytes_mode: config.bytes_mode,
361            unicode: config.syntaxc.get_unicode() && !matches!(config.bytes_mode, BytesMode::Ascii),
362            delegate_size_limit: config.delegate_size_limit,
363            delegate_dfa_size_limit: config.delegate_dfa_size_limit,
364            ..CompileOptions::default()
365        };
366
367        let utf8 = matches!(compile_options.bytes_mode, BytesMode::Unicode);
368
369        // Parse each pattern once and share the resulting Hirs between the two
370        // set-wide engines (the "earliest match" meta regex and the overlapping
371        // DFA), instead of letting each engine re-parse every pattern string.
372        let syntax_config = SyntaxConfig::new()
373            .utf8(utf8)
374            .unicode(compile_options.unicode);
375        let hirs = regex_automata::util::syntax::parse_many_with(&patterns, &syntax_config)
376            .map_err(|e| {
377                Error::CompileError(Box::new(CompileError::UnexpectedGeneralError(
378                    alloc::format!("failed to parse regex set pattern: {}", e),
379                )))
380            })?;
381
382        // The multi-pattern "earliest match" engine is searched unanchored. The
383        // syntax options are already encoded in the Hirs.
384        let mut earliest_builder = RaBuilder::new();
385        earliest_builder.configure(
386            RaConfig::new()
387                .match_kind(MatchKind::LeftmostFirst)
388                .nfa_size_limit(config.meta_nfa_size_limit)
389                .hybrid_cache_capacity(config.meta_hybrid_cache_capacity),
390        );
391        let earliest_match_finder = earliest_builder
392            .build_many_from_hir(&hirs)
393            .map_err(CompileError::InnerError)
394            .map_err(|e| Error::CompileError(Box::new(e)))?;
395
396        let format_patterns = || {
397            patterns
398                .iter()
399                .enumerate()
400                .map(|(i, p)| alloc::format!("[{}]: {}", i, p))
401                .collect::<Vec<_>>()
402                .join("\n---\n")
403        };
404
405        // Build the overlapping DFA from a Thompson NFA compiled from the same
406        // Hirs. This replicates what `dfa::Builder::build_many` does internally
407        // (it forces `WhichCaptures::None` and calls `build_from_nfa`), minus
408        // the extra pattern parse.
409        let mut thompson_config = thompson::Config::new().which_captures(WhichCaptures::None);
410        if let Some(limit) = compile_options.delegate_size_limit {
411            thompson_config = thompson_config.nfa_size_limit(Some(limit));
412        }
413        let nfa = thompson::Compiler::new()
414            .configure(thompson_config)
415            .build_many_from_hir(&hirs)
416            .map_err(|e| {
417                Error::CompileError(Box::new(CompileError::DfaBuildError(
418                    format_patterns(),
419                    e.to_string(),
420                )))
421            })?;
422
423        let mut overlapping_dfa_builder = dfa::DFA::builder();
424        let mut overlapping_config = dfa::Config::new()
425            .match_kind(MatchKind::All)
426            .unicode_word_boundary(compile_options.unicode)
427            .cache_capacity(config.overlapping_dfa_cache_capacity);
428        if config.overlapping_dfa_skip_cache_capacity_check {
429            overlapping_config = overlapping_config.skip_cache_capacity_check(true);
430        }
431        overlapping_dfa_builder.configure(overlapping_config);
432        let overlapping_dfa =
433            Arc::new(overlapping_dfa_builder.build_from_nfa(nfa).map_err(|e| {
434                Error::CompileError(Box::new(CompileError::DfaBuildError(
435                    format_patterns(),
436                    e.to_string(),
437                )))
438            })?);
439        let create: DfaCachePoolFactory = alloc::boxed::Box::new({
440            let dfa = Arc::clone(&overlapping_dfa);
441            move || dfa.create_cache()
442        });
443        let overlapping_cache_pool = Arc::new(Pool::new(create));
444
445        Ok(Self {
446            regexes: regexes_vec,
447            earliest_match_finder,
448            overlapping_dfa,
449            overlapping_cache_pool,
450        })
451    }
452
453    /// Returns the number of patterns in the set.
454    pub fn len(&self) -> usize {
455        self.regexes.len()
456    }
457
458    /// Returns true if the set contains no patterns.
459    pub fn is_empty(&self) -> bool {
460        self.len() == 0
461    }
462
463    /// Returns an iterator over matches at the earliest match position - if any.
464    /// Iterator yields matches in pattern index order
465    pub fn find_input<'r, 't, S: Input + ?Sized>(
466        &'r self,
467        input: RegexInput<'t, S>,
468    ) -> Result<Option<RegexSetMatchesAt<'r, 't, S>>> {
469        if input.is_done() {
470            return Ok(None);
471        }
472
473        let haystack = input.haystack();
474        let match_range = input.get_range();
475        let mut search_start = input.effective_start();
476        let mut seen_pattern_indices = PatternSet::new(self.regexes.len());
477
478        while search_start <= match_range.end {
479            let ra_input = RaInput::new(haystack.as_bytes())
480                .range(search_start..match_range.end)
481                .anchored(if input.is_anchored() {
482                    Anchored::Yes
483                } else {
484                    Anchored::No
485                });
486            let Some(candidate) = self.earliest_match_finder.search(&ra_input) else {
487                return Ok(None);
488            };
489            let match_start = candidate.start();
490            let overlapping_input = RaInput::new(haystack.as_bytes())
491                .anchored(Anchored::Yes)
492                .range(match_start..match_range.end);
493            seen_pattern_indices.clear();
494            {
495                let mut cache_guard = self.overlapping_cache_pool.get();
496                if let Err(e) = self.overlapping_dfa.try_which_overlapping_matches(
497                    &mut cache_guard,
498                    &overlapping_input,
499                    &mut seen_pattern_indices,
500                ) {
501                    match e.kind() {
502                        MatchErrorKind::Quit { .. } | MatchErrorKind::GaveUp { .. } => {
503                            // The DFA gave up (e.g. it encountered non-ASCII bytes while
504                            // unicode word boundaries are enabled, which adds quit bytes
505                            // for multi-byte UTF-8 sequences). Fall back to trying every
506                            // pattern at this position so correctness is preserved.
507                            seen_pattern_indices.clear();
508                            for i in 0..self.regexes.len() {
509                                seen_pattern_indices.insert(PatternID::must(i));
510                            }
511                        }
512                        _ => panic!("unexpected overlapping DFA error: {:?}", e),
513                    }
514                }
515            } // release cache_guard back to pool before doing per-pattern matching
516              // Verify candidates straight off the PatternSet (ascending pattern
517              // order). Only when a match is found are the *remaining* indices
518              // collected, for lazy verification while the caller iterates — so
519              // a failed candidate position allocates nothing.
520            let mut candidate_pattern_indices = seen_pattern_indices
521                .iter()
522                .map(|pattern| pattern.as_usize());
523            let mut first_match = None;
524            for pattern_index in &mut candidate_pattern_indices {
525                if let Some(candidate_match) =
526                    self.match_pattern_at_input_position(pattern_index, &input, match_start)?
527                {
528                    first_match = Some(candidate_match);
529                    break;
530                }
531            }
532
533            if let Some(first_match) = first_match {
534                let pending_pattern_indices = candidate_pattern_indices.collect::<Vec<_>>();
535                return Ok(Some(RegexSetMatchesAt {
536                    regex_set: self,
537                    input,
538                    haystack,
539                    match_start,
540                    first_match: Some(first_match),
541                    pending_pattern_indices: pending_pattern_indices.into_iter(),
542                }));
543            }
544
545            search_start = haystack.advance_position(match_start);
546        }
547
548        Ok(None)
549    }
550
551    fn match_pattern_at_input_position<'t, S: Input + ?Sized>(
552        &self,
553        pattern_index: usize,
554        input: &RegexInput<'t, S>,
555        match_start: usize,
556    ) -> Result<Option<RegexSetMatch<'t, S>>> {
557        let candidate_input = input.clone().from_pos(match_start).anchored(true);
558        let regex = &self.regexes[pattern_index];
559        let mut option_flags = 0;
560        if input.start() < match_start {
561            option_flags |= OPTION_NOT_CONTINUED_FROM_PREVIOUS_MATCH;
562        }
563        if regex.captures_len() == 1 {
564            return Ok(regex.find_input_raw(&candidate_input, option_flags)?.map(
565                |(start, end)| RegexSetMatch {
566                    pattern_index,
567                    captures: regex.captures_for_span(input.haystack(), start, end),
568                },
569            ));
570        }
571        Ok(regex
572            .captures_input_with_option_flags(&candidate_input, option_flags)?
573            .map(|captures| RegexSetMatch {
574                pattern_index,
575                captures,
576            }))
577    }
578}
579
580/// A match from a RegexSet, including the pattern index and capture groups.
581///
582/// This type represents a single match found by a [`RegexSet`]. It provides
583/// information about which pattern matched, the location of the match, and
584/// access to any capture groups.
585///
586/// # Examples
587///
588/// ```rust
589/// use fancy_regex::{RegexInput, RegexSet};
590///
591/// # fn main() -> Result<(), fancy_regex::Error> {
592/// let set = RegexSet::new(&[r"\w+"])?;
593/// let mut matches = set
594///     .find_input(RegexInput::new("abc"))?
595///     .expect("expected at least one match");
596///
597/// let m = matches.next().expect("expected first match")?;
598/// assert_eq!(m.pattern(), 0);
599/// assert_eq!(m.get().as_str(), "abc");
600/// assert_eq!(m.start(), 0);
601/// assert_eq!(m.end(), 3);
602/// assert_eq!(m.captures().len(), 1);
603/// # Ok(())
604/// # }
605/// ```
606#[derive(Debug)]
607pub struct RegexSetMatch<'t, S: Input + ?Sized> {
608    pattern_index: usize,
609    captures: Captures<'t, S>,
610}
611
612impl<'t, S: Input + ?Sized> RegexSetMatch<'t, S> {
613    /// Returns the pattern index that matched.
614    pub fn pattern(&self) -> usize {
615        self.pattern_index
616    }
617
618    /// Returns the full set of capture groups for this match.
619    pub fn captures(&self) -> &Captures<'t, S> {
620        &self.captures
621    }
622
623    /// Returns the full match.
624    pub fn get(&self) -> S::Match<'t> {
625        self.captures
626            .get(0)
627            .expect("`RegexSetMatch` must always contain the overall match")
628    }
629
630    /// Returns the start offset of the full match.
631    pub fn start(&self) -> usize {
632        self.captures
633            .get_span(0)
634            .expect("`RegexSetMatch` must always contain the overall match")
635            .0
636    }
637
638    /// Returns the end offset of the full match.
639    pub fn end(&self) -> usize {
640        self.captures
641            .get_span(0)
642            .expect("`RegexSetMatch` must always contain the overall match")
643            .1
644    }
645}
646
647impl<'t> RegexSetMatch<'t, str> {
648    /// Returns the matched text.
649    pub fn as_str(&self) -> &'t str {
650        self.captures
651            .get(0)
652            .expect("`RegexSetMatch` must always contain the overall match")
653            .as_str()
654    }
655}
656
657#[derive(Debug)]
658pub struct RegexSetMatchesAt<'r, 't, S: Input + ?Sized> {
659    regex_set: &'r RegexSet,
660    input: RegexInput<'t, S>,
661    haystack: &'t S,
662    match_start: usize,
663    first_match: Option<RegexSetMatch<'t, S>>,
664    pending_pattern_indices: alloc::vec::IntoIter<usize>,
665}
666
667impl<'r, 't, S: Input + ?Sized> RegexSetMatchesAt<'r, 't, S> {
668    /// Returns the originating regex set.
669    pub fn regex_set(&self) -> &'r RegexSet {
670        self.regex_set
671    }
672
673    /// Returns the searched haystack.
674    pub fn haystack(&self) -> &'t S {
675        self.haystack
676    }
677
678    /// Returns the earliest start position shared by these matches.
679    pub fn start(&self) -> usize {
680        self.match_start
681    }
682}
683
684impl<'r, 't, S: Input + ?Sized> Iterator for RegexSetMatchesAt<'r, 't, S> {
685    type Item = Result<RegexSetMatch<'t, S>>;
686
687    fn next(&mut self) -> Option<Self::Item> {
688        if let Some(first_match) = self.first_match.take() {
689            return Some(Ok(first_match));
690        }
691
692        for pattern_index in self.pending_pattern_indices.by_ref() {
693            match self.regex_set.match_pattern_at_input_position(
694                pattern_index,
695                &self.input,
696                self.match_start,
697            ) {
698                Ok(Some(regex_set_match)) => return Some(Ok(regex_set_match)),
699                Ok(None) => continue,
700                Err(err) => return Some(Err(err)),
701            }
702        }
703
704        None
705    }
706}
707
708#[cfg(test)]
709mod tests {
710    use super::{
711        RegexSet, RegexSetOptions, DEFAULT_META_HYBRID_CACHE_CAPACITY, DEFAULT_META_NFA_SIZE_LIMIT,
712        DEFAULT_OVERLAPPING_DFA_CACHE_CAPACITY,
713    };
714    use crate::{Error, RegexInput, RegexOptionsBuilder, RuntimeError};
715
716    #[test]
717    fn regex_set_options_defaults_to_larger_regex_automata_limits() {
718        let options = RegexSetOptions::default();
719
720        assert_eq!(
721            options.meta_nfa_size_limit,
722            Some(DEFAULT_META_NFA_SIZE_LIMIT)
723        );
724        assert_eq!(
725            options.meta_hybrid_cache_capacity,
726            DEFAULT_META_HYBRID_CACHE_CAPACITY
727        );
728        assert_eq!(
729            options.overlapping_dfa_cache_capacity,
730            DEFAULT_OVERLAPPING_DFA_CACHE_CAPACITY
731        );
732        assert!(options.overlapping_dfa_skip_cache_capacity_check);
733    }
734
735    #[test]
736    fn regex_set_options_setters_update_regex_automata_limits() {
737        let options = RegexSetOptions::new()
738            .delegate_size_limit(11)
739            .delegate_dfa_size_limit(22)
740            .meta_nfa_size_limit(None)
741            .meta_hybrid_cache_capacity(33)
742            .overlapping_dfa_cache_capacity(44)
743            .overlapping_dfa_skip_cache_capacity_check(false);
744
745        assert_eq!(options.delegate_size_limit, Some(11));
746        assert_eq!(options.delegate_dfa_size_limit, Some(22));
747        assert_eq!(options.meta_nfa_size_limit, None);
748        assert_eq!(options.meta_hybrid_cache_capacity, 33);
749        assert_eq!(options.overlapping_dfa_cache_capacity, 44);
750        assert!(!options.overlapping_dfa_skip_cache_capacity_check);
751    }
752
753    #[test]
754    fn find_input_returns_all_matches_at_earliest_position_in_pattern_order() {
755        let set = RegexSet::new(&[r"\d+", r"\w+", r"(?<=\$)\d+\.\d+"]).unwrap();
756        let mut matches = set.find_input(RegexInput::new("$29.99")).unwrap().unwrap();
757
758        let first = matches.next().unwrap().unwrap();
759        assert_eq!(0, first.pattern());
760        assert_eq!(1, first.start());
761        assert_eq!(3, first.end());
762        assert_eq!("29", first.as_str());
763
764        let second = matches.next().unwrap().unwrap();
765        assert_eq!(1, second.pattern());
766        assert_eq!(1, second.start());
767        assert_eq!(3, second.end());
768        assert_eq!("29", second.as_str());
769
770        let third = matches.next().unwrap().unwrap();
771        assert_eq!(2, third.pattern());
772        assert_eq!(1, third.start());
773        assert_eq!(6, third.end());
774        assert_eq!("29.99", third.as_str());
775
776        assert!(matches.next().is_none());
777    }
778
779    #[test]
780    fn find_input_skips_false_positive_candidate_positions() {
781        let set = RegexSet::new(&[r"(?<=foo)bar"]).unwrap();
782        let mut matches = set
783            .find_input(RegexInput::new("barfoobar"))
784            .unwrap()
785            .unwrap();
786
787        let only = matches.next().unwrap().unwrap();
788        assert_eq!(0, only.pattern());
789        assert_eq!(6, only.start());
790        assert_eq!(9, only.end());
791        assert_eq!("bar", only.as_str());
792        assert!(matches.next().is_none());
793    }
794
795    #[test]
796    fn find_input_returns_none_when_input_is_done() {
797        let set = RegexSet::new(&[r"."]).unwrap();
798
799        assert!(set
800            .find_input(RegexInput::new("a").from_pos(2))
801            .unwrap()
802            .is_none());
803    }
804
805    #[test]
806    fn find_input_returns_none_when_input_is_anchored_and_match_not_at_start_position() {
807        let set = RegexSet::new(&[r"b"]).unwrap();
808
809        assert!(set
810            .find_input(RegexInput::new("ab").from_pos(0).anchored(true))
811            .unwrap()
812            .is_none());
813    }
814
815    #[test]
816    fn find_input_returns_match_when_input_is_anchored_and_match_at_start_position() {
817        let set = RegexSet::new(&[r"b"]).unwrap();
818
819        let mut matches = set
820            .find_input(RegexInput::new("ab").from_pos(1).anchored(true))
821            .unwrap()
822            .unwrap();
823
824        let only = matches.next().unwrap().unwrap();
825        assert_eq!(0, only.pattern());
826        assert_eq!(1, only.start());
827        assert_eq!(2, only.end());
828        assert_eq!("b", only.as_str());
829        assert!(matches.next().is_none());
830    }
831
832    #[test]
833    fn find_input_defers_later_pattern_evaluation_until_iteration() {
834        let mut options_builder = RegexOptionsBuilder::new();
835        options_builder.backtrack_limit(0);
836        let set = RegexSet::new_with_options(&[r"a", r"(?:(a|aa)+)\1"], &options_builder).unwrap();
837
838        let mut matches = set.find_input(RegexInput::new("aa")).unwrap().unwrap();
839
840        let first = matches.next().unwrap().unwrap();
841        assert_eq!(0, first.pattern());
842        assert_eq!(0, first.start());
843        assert_eq!(1, first.end());
844
845        let second = matches.next().unwrap();
846        assert!(matches!(
847            second,
848            Err(Error::RuntimeError(RuntimeError::BacktrackLimitExceeded))
849        ));
850    }
851
852    #[test]
853    fn find_input_picks_earliest_start_position_before_iterating_pattern_order() {
854        let mut options_builder = RegexOptionsBuilder::new();
855        options_builder.multi_line(true);
856        let set = RegexSet::new_with_options(
857            &[
858                r"//.*$",
859                r#""(?:[^"\\]|\\.)*""#,
860                r"\b(fn|let|mut|if|else)\b",
861                r"\b[0-9]+\b",
862                r"[a-zA-Z_][a-zA-Z0-9_]*",
863            ],
864            &options_builder,
865        )
866        .unwrap();
867
868        let mut matches = set
869            .find_input(RegexInput::new(
870                "let x = 42; // a comment\nlet s = \"hello world\";",
871            ))
872            .unwrap()
873            .unwrap();
874
875        let first = matches.next().unwrap().unwrap();
876        assert_eq!(2, first.pattern());
877        assert_eq!(0, first.start());
878        assert_eq!(3, first.end());
879        assert_eq!("let", first.as_str());
880    }
881
882    #[test]
883    fn find_input_yields_each_pattern_at_match_start_once() {
884        let set = RegexSet::new(&[r"a+", r"a"]).unwrap();
885        let mut matches = set.find_input(RegexInput::new("aaa")).unwrap().unwrap();
886
887        let first = matches.next().unwrap().unwrap();
888        assert_eq!(0, first.pattern());
889        assert_eq!(0, first.start());
890        assert_eq!(3, first.end());
891        assert_eq!("aaa", first.as_str());
892
893        let second = matches.next().unwrap().unwrap();
894        assert_eq!(1, second.pattern());
895        assert_eq!(0, second.start());
896        assert_eq!(1, second.end());
897        assert_eq!("a", second.as_str());
898
899        assert!(matches.next().is_none());
900    }
901
902    #[test]
903    fn test_no_captures_returns_group_0() {
904        let set = RegexSet::new(&[r"\w+"]).unwrap();
905        let mut matches = set.find_input(RegexInput::new("abc")).unwrap().unwrap();
906
907        let only = matches.next().unwrap().unwrap();
908        assert_eq!(0, only.pattern());
909        assert_eq!(1, only.captures().len());
910        assert_eq!("abc", only.as_str());
911    }
912
913    #[test]
914    fn word_boundary_matches_correctly_with_unicode_text() {
915        // \bbar\b uses a unicode word boundary; the DFA may encounter quit bytes
916        // for multi-byte UTF-8 characters such as 'é' (0xC3 0xA9). The RegexSet
917        // must not panic and must still return the correct match.
918        let set = RegexSet::new([r"foo", r"\bbar\b"]).unwrap();
919        let mut matches = set
920            .find_input(RegexInput::new("fooé bar"))
921            .unwrap()
922            .unwrap();
923
924        // "foo" is the earliest match (position 0)
925        let first = matches.next().unwrap().unwrap();
926        assert_eq!(0, first.pattern());
927        assert_eq!("foo", first.as_str());
928        assert!(matches.next().is_none());
929
930        // "bar" is matched later in the string (after the unicode character)
931        let mut matches2 = set
932            .find_input(RegexInput::new("fooé bar").from_pos(first.end()))
933            .unwrap()
934            .unwrap();
935        let bar_match = matches2.next().unwrap().unwrap();
936        assert_eq!(1, bar_match.pattern());
937        assert_eq!("bar", bar_match.as_str());
938        assert!(matches2.next().is_none());
939    }
940
941    #[test]
942    fn find_input_continue_from_prev_match_inside_negative_lookbehind() {
943        let options = &mut RegexOptionsBuilder::new();
944        let options = options.allow_input_assertion_overrides(true);
945        let set = RegexSet::new_with_options([r"(?<!\G)b"], &options).unwrap();
946        let mut matches = set
947            .find_input(RegexInput::new("ab").continue_from_previous_match_end(false))
948            .unwrap()
949            .unwrap();
950
951        let first = matches.next().unwrap().unwrap();
952        assert_eq!(0, first.pattern());
953        assert_eq!("b", first.as_str());
954        assert!(matches.next().is_none());
955    }
956
957    #[test]
958    fn continue_from_prev_match_works_as_expected_when_match_is_not_at_search_start() {
959        use crate::Arc;
960        use crate::RegexBuilder;
961
962        let (pat, hay) = (r"\Gx", "yx");
963
964        // `\G` holds only at the search start (index 0); 'x' is at 1 -> no match.
965        let re = RegexBuilder::new(pat).build().unwrap();
966        let single = re
967            .find_input(RegexInput::new(hay).from_pos(0))
968            .unwrap()
969            .map(|m| (m.start(), m.end()));
970
971        // Same regex, same input, via a RegexSet:
972        let set = RegexSet::from_regexes([Arc::new(re)], Default::default()).unwrap();
973        let via_set = set
974            .find_input(RegexInput::new(hay).from_pos(0))
975            .unwrap()
976            .and_then(|mut it| it.next())
977            .map(|m| {
978                let m = m.unwrap();
979                (m.start(), m.end())
980            });
981
982        assert_eq!(
983            single, via_set,
984            "RegexSet should behave the same as a standalone regex"
985        );
986        assert_eq!(single, None);
987    }
988
989    #[test]
990    fn continue_from_prev_match_works_as_expected_when_match_is_at_search_start() {
991        use crate::Arc;
992        use crate::RegexBuilder;
993
994        let (pat, hay) = (r"\Gx", "yx");
995
996        let re = RegexBuilder::new(pat).build().unwrap();
997        let single = re
998            .find_input(RegexInput::new(hay).from_pos(1))
999            .unwrap()
1000            .map(|m| (m.start(), m.end()));
1001
1002        // Same regex, same input, via a RegexSet:
1003        let set = RegexSet::from_regexes([Arc::new(re)], Default::default()).unwrap();
1004        let via_set = set
1005            .find_input(RegexInput::new(hay).from_pos(1))
1006            .unwrap()
1007            .and_then(|mut it| it.next())
1008            .map(|m| {
1009                let m = m.unwrap();
1010                (m.start(), m.end())
1011            });
1012
1013        assert_eq!(
1014            single, via_set,
1015            "RegexSet should behave the same as a standalone regex"
1016        );
1017        assert_eq!(single, Some((1, 2)));
1018    }
1019}