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(®ex.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}