Skip to main content

regexr/
lib.rs

1//! regexr - A high-performance regex engine built from scratch
2//!
3//! This crate provides a regex engine with multiple execution backends:
4//! - PikeVM: Thread-based NFA simulation (supports backreferences, lookaround)
5//! - Shift-Or: Bit-parallel NFA for patterns with ≤64 states
6//! - Lazy DFA: On-demand determinization with caching
7//! - JIT: Native x86-64 code generation (optional, requires `jit` feature)
8//! - SIMD: AVX2-accelerated literal search (optional, requires `simd` feature)
9//!
10//! The [`mod@reference`] module is an executable specification: a simple,
11//! obviously-correct backtracking matcher defining regexr's canonical match
12//! semantics, used as the ground-truth oracle in conformance/differential tests.
13
14#![warn(missing_docs)]
15#![warn(rust_2018_idioms)]
16
17pub mod dfa;
18pub mod engine;
19pub mod error;
20mod hash;
21pub mod hir;
22pub mod literal;
23pub mod nfa;
24pub mod parser;
25pub mod reference;
26pub mod vm;
27
28#[cfg(feature = "jit")]
29pub mod jit;
30
31#[cfg(feature = "simd")]
32pub mod simd;
33
34pub use error::{Error, Result};
35
36use engine::{CompiledRegex, PooledDfa};
37use std::collections::HashMap;
38use std::sync::Arc;
39
40/// Configuration options for regex compilation.
41#[derive(Debug, Clone, Default)]
42pub struct RegexBuilder {
43    pattern: String,
44    /// Whether to enable JIT compilation.
45    jit: bool,
46    /// Whether to enable prefix optimization for large alternations.
47    /// This is critical for tokenizer-style patterns with many literal alternatives.
48    optimize_prefixes: bool,
49    /// Steps a backreference search may take; see [`RegexBuilder::backtrack_limit`].
50    backtrack_limit: u64,
51    /// Elements the pattern may expand to; see [`RegexBuilder::size_limit`].
52    size_limit: u32,
53    /// How deeply groups/classes/flag-scopes may nest; see
54    /// [`RegexBuilder::nest_limit`].
55    nest_limit: u32,
56}
57
58impl RegexBuilder {
59    /// Creates a new RegexBuilder with the given pattern.
60    pub fn new(pattern: &str) -> Self {
61        Self {
62            pattern: pattern.to_string(),
63            jit: false,
64            optimize_prefixes: false,
65            backtrack_limit: vm::backtracking::DEFAULT_BACKTRACK_LIMIT,
66            size_limit: hir::builder::DEFAULT_EXPANDED_SIZE,
67            nest_limit: parser::DEFAULT_NEST_LIMIT,
68        }
69    }
70
71    /// Sets how large a pattern may expand to before it is refused.
72    ///
73    /// Every engine here compiles `{n,m}` by emitting the subexpression `m`
74    /// times, so what a pattern costs to build is its *expanded* size, not its
75    /// text length: `\w{200000,}` is eleven characters and minutes of work.
76    /// Anything compiling a pattern it did not write needs that bounded, so the
77    /// default refuses a pattern past [`hir::builder::DEFAULT_EXPANDED_SIZE`]
78    /// elements — roughly a tenth of a second of compilation.
79    ///
80    /// Raise it when you own the pattern and the cost is acceptable. The error
81    /// reports the size the pattern reached, so it also tells you what to raise
82    /// it to.
83    ///
84    /// The count is taken *after* classes are lowered, which is worth knowing
85    /// for a pattern that looks small: a large Unicode property compiles to a
86    /// single node, but a small multi-byte class becomes a UTF-8 trie of up to
87    /// 64 branches, and a bounded repetition multiplies that.
88    ///
89    /// # Example
90    ///
91    /// ```
92    /// use regexr::RegexBuilder;
93    ///
94    /// assert!(RegexBuilder::new(r"a{50000}").build().is_err());
95    /// assert!(RegexBuilder::new(r"a{50000}")
96    ///     .size_limit(100_000)
97    ///     .build()
98    ///     .is_ok());
99    /// ```
100    pub fn size_limit(mut self, limit: u32) -> Self {
101        self.size_limit = limit;
102        self
103    }
104
105    /// Sets how deeply groups, character classes, and inline flag scopes
106    /// (`(?i)...`) may nest before a pattern is refused.
107    ///
108    /// The parser is mutually recursive with no explicit stack, so nesting
109    /// depth is bounded by the call stack, not by memory the pattern itself
110    /// controls. Left unbounded, a pattern like `"(?:".repeat(50_000) +
111    /// ")".repeat(50_000)` overflows the stack — which in Rust is an
112    /// uncatchable SIGSEGV/abort, not an `Err` a caller can handle. The
113    /// default, [`parser::DEFAULT_NEST_LIMIT`], matches the `regex` crate's
114    /// `nest_limit` and PCRE2's `parens_nest_limit`.
115    ///
116    /// Raise it only for patterns you own and trust; a caller compiling an
117    /// untrusted pattern should keep the default.
118    ///
119    /// # Example
120    ///
121    /// ```
122    /// use regexr::RegexBuilder;
123    ///
124    /// let deep = format!("{}a{}", "(?:".repeat(300), ")".repeat(300));
125    /// assert!(RegexBuilder::new(&deep).build().is_err());
126    /// assert!(RegexBuilder::new(&deep).nest_limit(400).build().is_ok());
127    /// ```
128    pub fn nest_limit(mut self, limit: u32) -> Self {
129        self.nest_limit = limit;
130        self
131    }
132
133    /// Sets how many steps a backreference search may take before giving up.
134    ///
135    /// Every engine in this crate is linear in the input except the backtracking
136    /// one, and only patterns with backreferences reach it. Matching a
137    /// backreference is NP-hard in general — there is no polynomial bound to
138    /// fall back on — so a step budget is what guarantees the search ends.
139    ///
140    /// The default is high enough that ordinary patterns never approach it. A
141    /// search that does exhaust it makes [`Regex::try_find`],
142    /// [`Regex::try_captures`] and [`Regex::try_is_match`] return
143    /// [`error::ErrorKind::MatchLimitExceeded`]; the infallible [`Regex::find`],
144    /// [`Regex::captures`] and [`Regex::is_match`] report it as "no match",
145    /// which is why the fallible forms exist.
146    pub fn backtrack_limit(mut self, limit: u64) -> Self {
147        self.backtrack_limit = limit;
148        self
149    }
150
151    /// Enables or disables JIT compilation.
152    ///
153    /// When enabled, the regex will be compiled to native machine code
154    /// for maximum performance. This is ideal for patterns that will be
155    /// matched many times (e.g., tokenization).
156    ///
157    /// JIT compilation has higher upfront cost but faster matching.
158    /// Only available on x86-64 with the `jit` feature enabled.
159    ///
160    /// # Example
161    ///
162    /// ```
163    /// use regexr::RegexBuilder;
164    ///
165    /// let re = RegexBuilder::new(r"\w+")
166    ///     .jit(true)
167    ///     .build()
168    ///     .unwrap();
169    /// assert!(re.is_match("hello"));
170    /// ```
171    pub fn jit(mut self, enabled: bool) -> Self {
172        self.jit = enabled;
173        self
174    }
175
176    /// Enables or disables prefix optimization for large alternations.
177    ///
178    /// When enabled, large alternations of literals (like `(token1|token2|...|tokenN)`)
179    /// will be optimized by merging common prefixes into a trie structure.
180    /// This reduces the number of active NFA threads from O(vocabulary_size) to O(token_length).
181    ///
182    /// This is critical for tokenizer-style patterns with many literal alternatives.
183    ///
184    /// # Example
185    ///
186    /// ```
187    /// use regexr::RegexBuilder;
188    ///
189    /// // Pattern with many tokens sharing common prefixes
190    /// let re = RegexBuilder::new(r"(the|that|them|they|this)")
191    ///     .optimize_prefixes(true)
192    ///     .build()
193    ///     .unwrap();
194    /// assert!(re.is_match("the"));
195    /// ```
196    pub fn optimize_prefixes(mut self, enabled: bool) -> Self {
197        self.optimize_prefixes = enabled;
198        self
199    }
200
201    /// Builds the regex with the configured options.
202    pub fn build(self) -> Result<Regex> {
203        let ast = parser::parse_with_nest_limit(&self.pattern, self.nest_limit)?;
204        let mut hir_result = hir::translate_with_limit(&ast, self.size_limit)?;
205
206        // Apply prefix optimization if enabled
207        if self.optimize_prefixes {
208            hir_result = hir::optimize_prefixes(hir_result);
209        }
210
211        let named_groups = Arc::new(hir_result.props.named_groups.clone());
212
213        let inner = if self.jit {
214            engine::compile_with_jit(&hir_result)?
215        } else {
216            // Use compile_from_hir for optimal engine selection (ShiftOr, LazyDfa, etc.)
217            engine::compile_from_hir(&hir_result)?
218        };
219
220        Ok(Regex {
221            inner,
222            required_literal: required_literal_finder(&hir_result),
223            pattern: self.pattern,
224            named_groups,
225            backtrack_limit: self.backtrack_limit,
226        })
227    }
228}
229
230fn required_literal_finder(hir: &hir::Hir) -> Option<memchr::memmem::Finder<'static>> {
231    literal::required_literal(hir).map(|l| memchr::memmem::Finder::new(&l).into_owned())
232}
233
234/// Escapes all regex metacharacters in `text` so that the returned pattern
235/// matches `text` literally.
236///
237/// This is regexr's counterpart to `regex::escape` from the `regex` crate:
238/// pass the result to [`Regex::new`] or [`RegexBuilder::new`] when you have a
239/// plain string (e.g. a user-supplied delimiter) that must be matched
240/// character-for-character rather than interpreted as a pattern.
241///
242/// # Which characters are escaped
243///
244/// Escaping covers the characters regexr's parser treats specially at the top
245/// level (outside a character class) - `\ . * + ? | ^ $ ( ) [ ] { }` - plus
246/// `#` and ASCII whitespace, which extended (`x`) mode strips. Every
247/// other character - including `-`, `:`, `<`, `>`, `=`, `!`, `,`, `&`, `~`,
248/// digits, and non-ASCII text - already parses as a literal on its own and is
249/// passed through unchanged.
250///
251/// This differs from `regex::escape` only in leaving `&`, `~` and `-` alone:
252/// those matter inside a character class in engines with class-set operators,
253/// and regexr has none. The result of `escape` is therefore safe as a
254/// standalone pattern, concatenated with other *escaped* text, or spliced into
255/// an extended-mode pattern - but not when spliced directly inside a
256/// hand-written character class.
257///
258/// # Example
259///
260/// ```
261/// use regexr::{escape, Regex};
262///
263/// let delimiter = "a.b|c";
264/// let pattern = escape(delimiter);
265/// let re = Regex::new(&pattern).unwrap();
266/// assert!(re.is_match(delimiter));
267/// assert!(!re.is_match("axb c"));
268/// ```
269pub fn escape(text: &str) -> String {
270    let mut out = String::with_capacity(text.len());
271    for c in text.chars() {
272        match c {
273            // Line-oriented whitespace gets its symbolic escape so the output
274            // stays readable when logged or embedded in source.
275            '\n' => out.push_str(r"\n"),
276            '\r' => out.push_str(r"\r"),
277            '\t' => out.push_str(r"\t"),
278            '\\' | '.' | '*' | '+' | '?' | '|' | '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}'
279            // `#` starts a comment and whitespace separates nothing under
280            // extended mode, so both must survive being spliced into `(?x)`.
281            | '#' => {
282                out.push('\\');
283                out.push(c);
284            }
285            c if c.is_ascii_whitespace() => {
286                out.push('\\');
287                out.push(c);
288            }
289            c => out.push(c),
290        }
291    }
292    out
293}
294
295/// A compiled regular expression.
296#[derive(Debug)]
297pub struct Regex {
298    inner: CompiledRegex,
299    /// A literal every match must contain; absent from the haystack means no
300    /// match exists. Rejection only — never decides which match is reported.
301    required_literal: Option<memchr::memmem::Finder<'static>>,
302    pattern: String,
303    /// Named capture groups: maps name to index.
304    named_groups: Arc<HashMap<String, u32>>,
305    /// Steps a backreference search may take; see [`RegexBuilder::backtrack_limit`].
306    backtrack_limit: u64,
307}
308
309impl Regex {
310    /// Compiles a regular expression pattern.
311    ///
312    /// # Errors
313    /// Returns an error if the pattern is invalid.
314    pub fn new(pattern: &str) -> Result<Regex> {
315        let ast = parser::parse(pattern)?;
316        let hir = hir::translate(&ast)?;
317        let named_groups = Arc::new(hir.props.named_groups.clone());
318        // Use HIR-based compilation to enable Shift-Or and prefilters
319        let inner = engine::compile_from_hir(&hir)?;
320
321        Ok(Regex {
322            inner,
323            required_literal: required_literal_finder(&hir),
324            pattern: pattern.to_string(),
325            named_groups,
326            backtrack_limit: vm::backtracking::DEFAULT_BACKTRACK_LIMIT,
327        })
328    }
329
330    /// Returns the names of all named capture groups.
331    pub fn capture_names(&self) -> impl Iterator<Item = &str> {
332        self.named_groups.keys().map(|s| s.as_str())
333    }
334
335    /// Returns the original pattern string.
336    pub fn as_str(&self) -> &str {
337        &self.pattern
338    }
339
340    /// Returns true if the regex matches anywhere in the text.
341    pub fn is_match(&self, text: &str) -> bool {
342        self.can_match(text) && self.inner.is_match(text.as_bytes())
343    }
344
345    /// Returns the first match in the text.
346    pub fn find<'t>(&self, text: &'t str) -> Option<Match<'t>> {
347        if !self.can_match(text) {
348            return None;
349        }
350        self.inner
351            .find(text.as_bytes())
352            .map(|(start, end)| Match { text, start, end })
353    }
354
355    /// Whether a required literal (if any) is present at all.
356    pub(crate) fn can_match(&self, text: &str) -> bool {
357        // Skipped where the search scans for the same literal itself, so the
358        // haystack is not walked twice to answer one question.
359        if self.inner.scans_for_required_literal() {
360            return true;
361        }
362        match self.required_literal {
363            Some(ref finder) => finder.find(text.as_bytes()).is_some(),
364            None => true,
365        }
366    }
367
368    /// Returns an iterator over all non-overlapping matches.
369    ///
370    /// `.`, character classes, and the Perl shorthand classes (including the
371    /// ASCII-mode negated forms `\W`, `\D`) all match whole codepoints, so
372    /// every span covers complete characters — no match can start or end
373    /// inside one.
374    pub fn find_iter<'a>(&'a self, text: &'a str) -> Matches<'a> {
375        Matches::new(self, text)
376    }
377
378    /// Returns the capture groups for the first match.
379    pub fn captures<'t>(&self, text: &'t str) -> Option<Captures<'t>> {
380        if !self.can_match(text) {
381            return None;
382        }
383        self.inner.captures(text.as_bytes()).map(|slots| Captures {
384            text,
385            slots,
386            named_groups: Arc::clone(&self.named_groups),
387        })
388    }
389
390    /// [`Self::is_match`], reporting an exhausted backtracking budget instead of
391    /// answering "no match".
392    ///
393    /// # Errors
394    /// [`error::ErrorKind::MatchLimitExceeded`] if a backreference search ran
395    /// past [`RegexBuilder::backtrack_limit`]. No other pattern can fail here.
396    pub fn try_is_match(&self, text: &str) -> Result<bool> {
397        Ok(self.try_find(text)?.is_some())
398    }
399
400    /// [`Self::find`], reporting an exhausted backtracking budget instead of
401    /// answering "no match".
402    ///
403    /// # Errors
404    /// [`error::ErrorKind::MatchLimitExceeded`] if a backreference search ran
405    /// past [`RegexBuilder::backtrack_limit`]. No other pattern can fail here.
406    pub fn try_find<'t>(&self, text: &'t str) -> Result<Option<Match<'t>>> {
407        if !self.can_match(text) {
408            return Ok(None);
409        }
410        self.inner
411            .try_find_from(text.as_bytes(), 0, self.backtrack_limit)
412            .map(|found| found.map(|(start, end)| Match { text, start, end }))
413            .map_err(|_| self.match_limit_error())
414    }
415
416    /// [`Self::captures`], reporting an exhausted backtracking budget instead of
417    /// answering "no match".
418    ///
419    /// # Errors
420    /// [`error::ErrorKind::MatchLimitExceeded`] if a backreference search ran
421    /// past [`RegexBuilder::backtrack_limit`]. No other pattern can fail here.
422    pub fn try_captures<'t>(&self, text: &'t str) -> Result<Option<Captures<'t>>> {
423        if !self.can_match(text) {
424            return Ok(None);
425        }
426        self.inner
427            .try_captures_from(text.as_bytes(), 0, self.backtrack_limit)
428            .map(|found| {
429                found.map(|slots| Captures {
430                    text,
431                    slots,
432                    named_groups: Arc::clone(&self.named_groups),
433                })
434            })
435            .map_err(|_| self.match_limit_error())
436    }
437
438    fn match_limit_error(&self) -> Error {
439        Error::new(error::ErrorKind::MatchLimitExceeded, &self.pattern)
440    }
441
442    /// Returns an iterator over all non-overlapping captures.
443    pub fn captures_iter<'r, 't>(&'r self, text: &'t str) -> CapturesIter<'r, 't> {
444        CapturesIter {
445            regex: self,
446            text,
447            // Past the end, so a required literal that is absent yields nothing.
448            last_end: if self.can_match(text) {
449                0
450            } else {
451                text.len() + 1
452            },
453            skip_empty_at: None,
454            dfa: PooledDfa::checkout(&self.inner),
455        }
456    }
457
458    /// Replaces the first match with the replacement string.
459    pub fn replace<'t>(&self, text: &'t str, rep: &str) -> std::borrow::Cow<'t, str> {
460        match self.find(text) {
461            None => std::borrow::Cow::Borrowed(text),
462            Some(m) => {
463                // Assembled as bytes for simplicity; every match span covers
464                // whole codepoints (see `Match::as_str`), so this is always
465                // valid UTF-8.
466                let bytes = text.as_bytes();
467                let mut result = Vec::with_capacity(text.len() + rep.len());
468                result.extend_from_slice(&bytes[..m.start()]);
469                result.extend_from_slice(rep.as_bytes());
470                result.extend_from_slice(&bytes[m.end()..]);
471                std::borrow::Cow::Owned(into_string_lossy(result))
472            }
473        }
474    }
475
476    /// Returns the name of the engine being used (for debugging).
477    pub fn engine_name(&self) -> &'static str {
478        self.inner.engine_name()
479    }
480
481    /// Replaces all matches with the replacement string.
482    pub fn replace_all<'t>(&self, text: &'t str, rep: &str) -> std::borrow::Cow<'t, str> {
483        let bytes = text.as_bytes();
484        let mut last_end = 0;
485        // Assembled as bytes: see `replace`.
486        let mut result = Vec::new();
487        let mut had_match = false;
488
489        for m in self.find_iter(text) {
490            had_match = true;
491            result.extend_from_slice(&bytes[last_end..m.start()]);
492            result.extend_from_slice(rep.as_bytes());
493            last_end = m.end();
494        }
495
496        if !had_match {
497            std::borrow::Cow::Borrowed(text)
498        } else {
499            result.extend_from_slice(&bytes[last_end..]);
500            std::borrow::Cow::Owned(into_string_lossy(result))
501        }
502    }
503}
504
505/// A single match in the text.
506#[derive(Debug, Clone, Copy)]
507pub struct Match<'t> {
508    text: &'t str,
509    start: usize,
510    end: usize,
511}
512
513impl<'t> Match<'t> {
514    /// Returns the start byte offset of the match.
515    pub fn start(&self) -> usize {
516        self.start
517    }
518
519    /// Returns the end byte offset of the match.
520    pub fn end(&self) -> usize {
521        self.end
522    }
523
524    /// Returns the matched text.
525    ///
526    /// Every match-producing construct — `.`, character classes, and the
527    /// Perl shorthand classes including the ASCII-mode negated forms `\W`
528    /// and `\D` — consumes a whole codepoint, so a match span is always a
529    /// valid `&str` slice; this is total, never `""` for a non-empty span.
530    /// (`.get` is still used defensively rather than an indexing panic.)
531    pub fn as_str(&self) -> &'t str {
532        self.text.get(self.start..self.end).unwrap_or("")
533    }
534
535    /// Returns the matched bytes.
536    ///
537    /// Byte-identical to slicing [`Match::as_str`]'s underlying text at
538    /// [`Match::range`]; provided for callers that want raw bytes without
539    /// the `&str` conversion.
540    pub fn as_bytes(&self) -> &'t [u8] {
541        self.text
542            .as_bytes()
543            .get(self.start..self.end)
544            .unwrap_or(&[])
545    }
546
547    /// Returns the byte range of the match.
548    pub fn range(&self) -> std::ops::Range<usize> {
549        self.start..self.end
550    }
551
552    /// Returns the length of the match in bytes.
553    pub fn len(&self) -> usize {
554        self.end - self.start
555    }
556
557    /// Returns true if the match is empty.
558    pub fn is_empty(&self) -> bool {
559        self.start == self.end
560    }
561}
562
563/// Returns the smallest index `>= i` that is a UTF-8 codepoint boundary of
564/// `text`. Indices at or past the end of `text` are returned unchanged, so
565/// callers can use `i + 1` to force forward progress past the last byte.
566///
567/// `std`'s `is_char_boundary` is the sole authority on what a boundary is.
568fn ceil_char_boundary(text: &str, i: usize) -> usize {
569    let mut j = i;
570    while j < text.len() && !text.is_char_boundary(j) {
571        j += 1;
572    }
573    j
574}
575
576/// Converts assembled replacement output into a `String`, substituting U+FFFD
577/// for any invalid UTF-8. Every match span covers whole codepoints (see
578/// `Match::as_str`), and the surrounding bytes are unmodified slices of the
579/// original `&str`, so this never actually falls back to the lossy path
580/// today; it is kept as a defensive guard rather than an unwrap.
581fn into_string_lossy(bytes: Vec<u8>) -> String {
582    match String::from_utf8(bytes) {
583        Ok(s) => s,
584        Err(e) => String::from_utf8_lossy(e.as_bytes()).into_owned(),
585    }
586}
587
588/// An iterator over all non-overlapping matches.
589pub struct Matches<'a> {
590    inner: MatchesInner<'a>,
591    text: &'a str,
592    /// A lazy DFA checked out for the whole iteration rather than per match,
593    /// and returned to the pool when this iterator is dropped — including when
594    /// it is dropped part-way through. `None` unless the generic path is
595    /// taken; the other paths never run the engine.
596    dfa: Option<PooledDfa<'a>>,
597}
598
599impl<'a> std::fmt::Debug for Matches<'a> {
600    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
601        f.debug_struct("Matches")
602            .field("text_len", &self.text.len())
603            .finish_non_exhaustive()
604    }
605}
606
607/// Internal iterator state - either uses TeddyFull fast path or generic find().
608enum MatchesInner<'a> {
609    /// Fast path: use TeddyFull prefilter iterator directly.
610    TeddyFull(literal::FullMatchIter<'a, 'a>),
611    /// Generic path: call find() repeatedly.
612    Generic {
613        regex: &'a Regex,
614        last_end: usize,
615        /// End of the previous match when it was non-empty. An empty match at
616        /// that exact position is the same position reported twice, and every
617        /// other engine drops it.
618        skip_empty_at: Option<usize>,
619    },
620    /// A required literal is absent, so no match exists anywhere.
621    Empty,
622}
623
624impl<'a> Matches<'a> {
625    /// Creates a new matches iterator.
626    fn new(regex: &'a Regex, text: &'a str) -> Self {
627        let inner = if !regex.can_match(text) {
628            MatchesInner::Empty
629        } else if regex.inner.is_full_match_prefilter() {
630            // Fast path: use Teddy iterator directly
631            MatchesInner::TeddyFull(regex.inner.find_full_matches(text.as_bytes()))
632        } else {
633            // Generic path
634            MatchesInner::Generic {
635                regex,
636                last_end: 0,
637                skip_empty_at: None,
638            }
639        };
640        let dfa = matches!(inner, MatchesInner::Generic { .. })
641            .then(|| PooledDfa::checkout(&regex.inner));
642        Matches { inner, text, dfa }
643    }
644}
645
646impl<'a> Iterator for Matches<'a> {
647    type Item = Match<'a>;
648
649    fn next(&mut self) -> Option<Match<'a>> {
650        // Disjoint from the `self.inner` borrow below, so the checked-out
651        // instance stays reachable inside the match arms.
652        let held = &mut self.dfa;
653        match &mut self.inner {
654            MatchesInner::Empty => None,
655            MatchesInner::TeddyFull(iter) => {
656                // Fast path: get match directly from Teddy iterator
657                iter.next().map(|(start, end)| Match {
658                    text: self.text,
659                    start,
660                    end,
661                })
662            }
663            MatchesInner::Generic {
664                regex,
665                last_end,
666                skip_empty_at,
667            } => {
668                loop {
669                    if *last_end > self.text.len() {
670                        return None;
671                    }
672
673                    // The search is resumed at an offset into the *original* text
674                    // rather than run on a slice starting there, so `^`, `\b`/`\B`
675                    // and lookbehind still see the real text to the left of the
676                    // resume point.
677                    let (abs_start, abs_end) = regex.inner.find_from_with(
678                        self.text.as_bytes(),
679                        *last_end,
680                        held.as_mut().and_then(|held| held.get()),
681                    )?;
682
683                    // Every match already ends on a codepoint boundary (the
684                    // engine-wide rule, see `nfa::is_utf8_boundary`), so
685                    // `ceil_char_boundary` is a no-op here in practice; it is
686                    // kept as a defensive snap-forward rather than relying on
687                    // that invariant unchecked. For empty matches, step one
688                    // byte first so the iterator always makes forward progress.
689                    let empty = abs_start == abs_end;
690                    *last_end = if empty {
691                        ceil_char_boundary(self.text, abs_end + 1)
692                    } else {
693                        ceil_char_boundary(self.text, abs_end)
694                    };
695
696                    // An empty match where the previous, non-empty one ended is
697                    // that position reported a second time. `a*` on "aa" is one
698                    // match of "aa", not that plus an empty match at 2.
699                    if empty && *skip_empty_at == Some(abs_start) {
700                        *skip_empty_at = None;
701                        continue;
702                    }
703                    *skip_empty_at = (!empty).then_some(abs_end);
704
705                    return Some(Match {
706                        text: self.text,
707                        start: abs_start,
708                        end: abs_end,
709                    });
710                }
711            }
712        }
713    }
714}
715
716/// An iterator over all non-overlapping captures.
717#[derive(Debug)]
718pub struct CapturesIter<'r, 't> {
719    regex: &'r Regex,
720    text: &'t str,
721    last_end: usize,
722    /// See `MatchesInner::Generic::skip_empty_at`.
723    skip_empty_at: Option<usize>,
724    /// See `Matches::dfa`: one instance for the whole iteration, returned to
725    /// the pool when this iterator is dropped.
726    dfa: PooledDfa<'r>,
727}
728
729impl<'r, 't> Iterator for CapturesIter<'r, 't> {
730    type Item = Captures<'t>;
731
732    fn next(&mut self) -> Option<Captures<'t>> {
733        loop {
734            if self.last_end > self.text.len() {
735                return None;
736            }
737
738            // Resumed at an offset into the original text, for the same reason as
739            // `Matches::next`; the slots come back as absolute offsets.
740            let slots = self.regex.inner.captures_from_with(
741                self.text.as_bytes(),
742                self.last_end,
743                self.dfa.get(),
744            )?;
745            let (start, end) = slots.first().and_then(|s| *s)?;
746
747            // Resume at the next UTF-8 character boundary, ensuring progress on
748            // empty matches by stepping one byte first (see `Matches::next`).
749            let empty = start == end;
750            self.last_end = if empty {
751                ceil_char_boundary(self.text, end + 1)
752            } else {
753                ceil_char_boundary(self.text, end)
754            };
755
756            // Same suppression as `Matches::next`, so the two iterators report
757            // the same match sequence.
758            if empty && self.skip_empty_at == Some(start) {
759                self.skip_empty_at = None;
760                continue;
761            }
762            self.skip_empty_at = (!empty).then_some(end);
763
764            return Some(Captures {
765                text: self.text,
766                slots,
767                named_groups: Arc::clone(&self.regex.named_groups),
768            });
769        }
770    }
771}
772
773/// Captured groups from a regex match.
774#[derive(Debug, Clone)]
775pub struct Captures<'t> {
776    text: &'t str,
777    slots: Vec<Option<(usize, usize)>>,
778    named_groups: Arc<HashMap<String, u32>>,
779}
780
781impl<'t> Captures<'t> {
782    /// Returns the number of capture groups (including group 0 for the full match).
783    pub fn len(&self) -> usize {
784        self.slots.len()
785    }
786
787    /// Returns true if there are no captures.
788    pub fn is_empty(&self) -> bool {
789        self.slots.is_empty()
790    }
791
792    /// Returns the capture group at the given index.
793    pub fn get(&self, i: usize) -> Option<Match<'t>> {
794        self.slots.get(i).and_then(|slot| {
795            slot.map(|(start, end)| Match {
796                text: self.text,
797                start,
798                end,
799            })
800        })
801    }
802
803    /// Returns the capture group with the given name.
804    pub fn name(&self, name: &str) -> Option<Match<'t>> {
805        self.named_groups
806            .get(name)
807            .and_then(|&idx| self.get(idx as usize))
808    }
809}
810
811impl<'t> std::ops::Index<usize> for Captures<'t> {
812    type Output = str;
813
814    fn index(&self, i: usize) -> &str {
815        self.get(i)
816            .map(|m| m.as_str())
817            .unwrap_or_else(|| panic!("no capture group at index {}", i))
818    }
819}
820
821impl<'t> std::ops::Index<&str> for Captures<'t> {
822    type Output = str;
823
824    fn index(&self, name: &str) -> &str {
825        self.name(name)
826            .map(|m| m.as_str())
827            .unwrap_or_else(|| panic!("no capture group named '{}'", name))
828    }
829}
830
831#[cfg(test)]
832mod tests {
833    use super::*;
834
835    /// Sanity check on the escaped output shape: every metacharacter this
836    /// function targets gets a leading backslash, and nothing else does.
837    /// Behavioral round-trip coverage (building a real `Regex` from the
838    /// escaped output and matching against it) lives in `tests/api/`.
839    #[test]
840    fn test_escape_shape() {
841        assert_eq!(escape(r"\.*+?|^$(){}[]"), r"\\\.\*\+\?\|\^\$\(\)\{\}\[\]");
842        assert_eq!(escape("plain"), "plain");
843        assert_eq!(escape(""), "");
844        // Extended mode would otherwise drop these.
845        assert_eq!(escape("plain text"), r"plain\ text");
846        assert_eq!(escape("a#b"), r"a\#b");
847        assert_eq!(escape("a\nb"), r"a\nb");
848    }
849
850    /// `ceil_char_boundary` is the resume-position rule for `Matches` /
851    /// `CapturesIter`: it must never move backwards, must land on a boundary
852    /// inside the text, and must pass indices at/after the end through so an
853    /// empty match at the end terminates the iterators.
854    #[test]
855    fn test_ceil_char_boundary() {
856        let text = "aé世🎉";
857        // Boundaries: 0 (a), 1 (é), 3 (世), 6 (🎉), 10 (end).
858        assert_eq!(ceil_char_boundary(text, 0), 0);
859        assert_eq!(ceil_char_boundary(text, 1), 1);
860        assert_eq!(ceil_char_boundary(text, 2), 3);
861        assert_eq!(ceil_char_boundary(text, 3), 3);
862        assert_eq!(ceil_char_boundary(text, 4), 6);
863        assert_eq!(ceil_char_boundary(text, 5), 6);
864        assert_eq!(ceil_char_boundary(text, 7), 10);
865        assert_eq!(ceil_char_boundary(text, 10), 10);
866        // Past the end: passed through, which is what stops the iterators.
867        assert_eq!(ceil_char_boundary(text, 11), 11);
868
869        // Every ASCII index is already a boundary, so nothing moves.
870        let ascii = "abc";
871        for i in 0..=ascii.len() {
872            assert_eq!(ceil_char_boundary(ascii, i), i);
873        }
874
875        // Result is always a boundary (or past the end) and never regresses.
876        for i in 0..=text.len() {
877            let j = ceil_char_boundary(text, i);
878            assert!(j >= i);
879            assert!(text.is_char_boundary(j));
880        }
881    }
882
883    /// `into_string_lossy` must be an exact round-trip for valid UTF-8 (the
884    /// only case existing replacements produce) and lossy otherwise.
885    #[test]
886    fn test_into_string_lossy() {
887        assert_eq!(into_string_lossy("héllo".as_bytes().to_vec()), "héllo");
888        assert_eq!(into_string_lossy(Vec::new()), "");
889        // Orphaned continuation bytes from a split codepoint.
890        assert_eq!(
891            into_string_lossy(vec![b'-', 0xB8, 0x96]),
892            "-\u{FFFD}\u{FFFD}"
893        );
894    }
895}