Skip to main content

ferroni/
api.rs

1//! The idiomatic Rust API. [`Regex`] compiles and searches a pattern,
2//! [`RegexBuilder`] sets its options and syntax, and [`Match`], [`Captures`] and
3//! [`FindIter`] report what a search found. It wraps the C-port functions
4//! [`onig_new`](crate::regcomp::onig_new) and [`onig_search`].
5
6use std::cell::RefCell;
7use std::fmt;
8use std::ops::{Index, Range};
9use std::panic::AssertUnwindSafe;
10use std::str::FromStr;
11use std::sync::Arc;
12#[cfg(feature = "dfa-prefilter")]
13use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
14use std::time::Duration;
15
16#[cfg(feature = "dfa-prefilter")]
17use crate::dfa_prefilter::SingleSeek;
18use crate::encodings::utf8::ONIG_ENCODING_UTF8;
19use crate::error::RegexError;
20use crate::oniguruma::*;
21use crate::regcomp::{AcceptSeek, ScannerSeek, onig_new_for_single_seek};
22use crate::regexec::{
23    MatchArg, SearchLimits, cache_msa, onig_name_to_backref_number, onig_search,
24    onig_search_bounds, onig_search_captures, pure_literal_first, release_oversized_stack,
25    search_in_range, search_in_range_inner, take_cached_msa,
26};
27use crate::regint::RegexType;
28use crate::regparse_types::NameTable;
29use crate::regsyntax::{
30    OnigSyntaxASIS, OnigSyntaxEmacs, OnigSyntaxGnuRegex, OnigSyntaxGrep, OnigSyntaxJava,
31    OnigSyntaxOniguruma, OnigSyntaxPerl, OnigSyntaxPerl_NG, OnigSyntaxPosixBasic,
32    OnigSyntaxPosixExtended, OnigSyntaxPython, OnigSyntaxRuby,
33};
34
35thread_local! {
36    /// One reusable capture region per thread. Taking the value out keeps
37    /// nested and re-entrant searches independent; returning the larger region
38    /// retains the most useful allocation when result values overlap.
39    static CACHED_REGION: RefCell<Option<OnigRegion>> = const { RefCell::new(None) };
40}
41
42fn take_cached_region() -> OnigRegion {
43    CACHED_REGION
44        .try_with(|cached| cached.borrow_mut().take())
45        .ok()
46        .flatten()
47        .unwrap_or_default()
48}
49
50fn cache_region(region: OnigRegion) {
51    // Drop can run after this thread-local's destructor during thread
52    // teardown. In that case, simply let the uncached region be freed.
53    let _ = CACHED_REGION.try_with(|cached| {
54        let mut cached = cached.borrow_mut();
55        let region_capacity = region.beg.capacity() + region.end.capacity();
56        let should_replace = match cached.as_ref() {
57            Some(current) => region_capacity > current.beg.capacity() + current.end.capacity(),
58            None => true,
59        };
60        if should_replace {
61            *cached = Some(region);
62        }
63    });
64}
65
66/// Panics unless `start` is a char boundary of `text` at or before its end:
67/// the start offsets that `find_at` and its relatives accept for `&str`.
68#[track_caller]
69fn check_str_start(text: &str, start: usize) {
70    check_bytes_start(text.as_bytes(), start);
71    assert!(
72        text.is_char_boundary(start),
73        "start {start} is not a char boundary of the text"
74    );
75}
76
77/// Panics unless `start` is at or before the end of `text`.
78#[track_caller]
79fn check_bytes_start(text: &[u8], start: usize) {
80    assert!(
81        start <= text.len(),
82        "start {start} is past the end of the text ({} bytes)",
83        text.len()
84    );
85}
86
87fn timeout_to_millis(timeout: Duration) -> u64 {
88    if timeout.is_zero() {
89        return 0;
90    }
91
92    let millis = timeout.as_millis();
93    let millis = millis + u128::from(!timeout.subsec_nanos().is_multiple_of(1_000_000));
94    millis.clamp(1, u64::MAX as u128) as u64
95}
96
97/// Limits for a single search.
98///
99/// Every limit left unset keeps its process-wide setting (for example from
100/// [`onig_set_time_limit`](crate::regexec::onig_set_time_limit)). A limit set
101/// to zero turns that limit off for this search, as in Oniguruma.
102///
103/// Pass the options to the `*_with` search methods of [`Regex`], such as
104/// [`Regex::find_with`]. Unlike the plain methods, they report a search that
105/// stopped at a limit as an error instead of as "no match".
106///
107/// # Examples
108///
109/// ```
110/// use std::time::Duration;
111/// use ferroni::prelude::*;
112///
113/// let re = Regex::new(r"(a+)+b").unwrap();
114/// let options = SearchOptions::new().timeout(Duration::from_millis(50));
115///
116/// assert_eq!(re.find_with("aab", options).unwrap().unwrap().as_str(), "aab");
117///
118/// let hostile = "a".repeat(40);
119/// assert!(re.find_with(&hostile, options).is_err());
120/// ```
121#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
122pub struct SearchOptions {
123    timeout: Option<Duration>,
124    retry_limit_in_match: Option<u64>,
125    retry_limit_in_search: Option<u64>,
126    match_stack_limit: Option<u32>,
127}
128
129impl SearchOptions {
130    /// Options that keep every process-wide limit.
131    pub const fn new() -> Self {
132        SearchOptions {
133            timeout: None,
134            retry_limit_in_match: None,
135            retry_limit_in_search: None,
136            match_stack_limit: None,
137        }
138    }
139
140    /// Stop the search with [`RegexError::TimeLimitOver`] once `timeout` has
141    /// passed. `Duration::ZERO` removes the time limit for this search.
142    ///
143    /// The engine reads the clock every 512 backtracks, so the search can run
144    /// slightly past the deadline. Durations are rounded up to whole
145    /// milliseconds.
146    ///
147    /// `wasm32-unknown-unknown` has no clock: there a search with a time
148    /// limit, from this option or from
149    /// [`onig_set_time_limit`](crate::regexec::onig_set_time_limit), panics
150    /// when it starts. Bound the work with
151    /// [`retry_limit_in_search`](Self::retry_limit_in_search) on that target
152    /// instead. A search without a time limit never reads the clock.
153    pub const fn timeout(mut self, timeout: Duration) -> Self {
154        self.timeout = Some(timeout);
155        self
156    }
157
158    /// Stop the search with [`RegexError::RetryLimitInMatchOver`] after `limit`
159    /// backtracks at one start position. Zero removes the limit.
160    pub const fn retry_limit_in_match(mut self, limit: u64) -> Self {
161        self.retry_limit_in_match = Some(limit);
162        self
163    }
164
165    /// Stop the search with [`RegexError::RetryLimitInSearchOver`] after `limit`
166    /// backtracks in total. Zero removes the limit.
167    pub const fn retry_limit_in_search(mut self, limit: u64) -> Self {
168        self.retry_limit_in_search = Some(limit);
169        self
170    }
171
172    /// Stop the search with [`RegexError::MatchStackLimitOver`] once the
173    /// backtracking stack needs more than `limit` entries. As in C Oniguruma,
174    /// a limit below 320 lets the stack hold 321 entries. Zero removes the
175    /// limit.
176    pub const fn match_stack_limit(mut self, limit: u32) -> Self {
177        self.match_stack_limit = Some(limit);
178        self
179    }
180
181    /// The limits a search with these options applies, or `None` when every
182    /// limit is process-wide. `None` runs the same path as the standard methods.
183    #[inline]
184    fn limits(self) -> Option<SearchLimits> {
185        if self == Self::new() {
186            return None;
187        }
188        let process_wide = SearchLimits::process_wide();
189        Some(SearchLimits {
190            match_stack_limit: self
191                .match_stack_limit
192                .unwrap_or(process_wide.match_stack_limit),
193            retry_limit_in_match: self
194                .retry_limit_in_match
195                .unwrap_or(process_wide.retry_limit_in_match),
196            retry_limit_in_search: self
197                .retry_limit_in_search
198                .unwrap_or(process_wide.retry_limit_in_search),
199            time_limit: self
200                .timeout
201                .map_or(process_wide.time_limit, timeout_to_millis),
202            progress_callout: process_wide.progress_callout,
203            retraction_callout: process_wide.retraction_callout,
204            callout_user_data: process_wide.callout_user_data,
205        })
206    }
207}
208
209/// The immutable state behind a [`Regex`]: the compiled program, the pattern
210/// it was compiled from, and the single-pattern seek automaton. Every clone of
211/// a `Regex`, and every [`Captures`] it returns, shares one value through an
212/// `Arc`.
213struct Compiled {
214    /// Wrapped in `AssertUnwindSafe`; the notes on [`Regex`] explain why the
215    /// assertion holds.
216    program: AssertUnwindSafe<RegexType>,
217    pattern: Box<[u8]>,
218    /// Rust-only (ADR-008): the seek automaton that standard searches consult,
219    /// when the builder's `dfa` option is on, built on the first search that
220    /// consults it. Wrapped like `program`: the lazy DFA's cache pool is not
221    /// `RefUnwindSafe`.
222    #[cfg(feature = "dfa-prefilter")]
223    seek: AssertUnwindSafe<LazySeek>,
224    /// Rust-only (ADR-008): decides whether the capture and find methods consult the
225    /// seek, from the rejections they measure. Shared by the clones with the
226    /// seek, as is the seek's cache pool.
227    #[cfg(feature = "dfa-prefilter")]
228    seek_gate: SeekGate,
229}
230
231/// Rust-only (ADR-008): a regex's seek automaton, built from its encoded seek
232/// and accept seek by the first search that consults it. Building decodes the
233/// seeks and compiles their NFAs and lazy DFAs, which costs a large part of
234/// a compilation, while the iterators and the `*_with` methods never consult
235/// the seek and many regexes are never searched at all. Every clone shares
236/// the one automaton.
237#[cfg(feature = "dfa-prefilter")]
238struct LazySeek {
239    /// The seek and the accept seek, until the automaton is built.
240    codes: std::sync::Mutex<Option<(crate::dfa_prefilter::Seek, AcceptSeek)>>,
241    /// The automaton, or `None` where it does not build (`SingleSeek::build`).
242    automaton: std::sync::OnceLock<Option<Box<SingleSeek>>>,
243}
244
245#[cfg(feature = "dfa-prefilter")]
246impl LazySeek {
247    /// A seek to build from `codes`, or none.
248    fn new(codes: Option<(crate::dfa_prefilter::Seek, AcceptSeek)>) -> LazySeek {
249        let automaton = std::sync::OnceLock::new();
250        if codes.is_none() {
251            let _ = automaton.set(None);
252        }
253        LazySeek {
254            codes: std::sync::Mutex::new(codes),
255            automaton,
256        }
257    }
258
259    /// The automaton, built on the first call; a concurrent first call waits
260    /// for it.
261    #[inline]
262    fn get(&self) -> Option<&SingleSeek> {
263        self.automaton
264            .get_or_init(|| {
265                let codes = self
266                    .codes
267                    .lock()
268                    .unwrap_or_else(std::sync::PoisonError::into_inner)
269                    .take();
270                codes
271                    .and_then(|(seek, accept)| SingleSeek::build(&seek, accept.as_ref()))
272                    .map(Box::new)
273            })
274            .as_deref()
275    }
276
277    /// Whether the regex may have an automaton: none was built yet, or one
278    /// was. Does not build it.
279    #[inline]
280    fn may_exist(&self) -> bool {
281        self.automaton.get().is_none_or(Option::is_some)
282    }
283
284    /// Whether the automaton was built. For the tests.
285    #[cfg(test)]
286    fn built(&self) -> bool {
287        self.automaton.get().is_some_and(Option::is_some)
288    }
289}
290
291/// While the [`SeekGate`] is closed, one capture or find search in this many consults
292/// the seek anyway, so that the rejection rate is still measured.
293#[cfg(feature = "dfa-prefilter")]
294const SEEK_GATE_PROBE_INTERVAL: u32 = 32;
295
296#[cfg(feature = "dfa-prefilter")]
297thread_local! {
298    /// Per-thread state of [`probe_closed_gate`]: a xorshift32 generator with
299    /// a fixed nonzero seed, so a thread's probe sequence is reproducible.
300    static GATE_PROBE: std::cell::Cell<u32> = const { std::cell::Cell::new(0x9E37_79B9) };
301}
302
303/// Whether a search through a closed [`SeekGate`] consults the seek: one in
304/// [`SEEK_GATE_PROBE_INTERVAL`] on average.
305///
306/// The gate is shared by every thread that searches with the regex. Counting
307/// the passed searches in one shared atomic made every search write it, which
308/// serialized those threads. A thread-local counter would avoid that but would
309/// alias with regexes a thread alternates between (two regexes taking turns
310/// would always land the probe on the same one); independent draws from a
311/// per-thread generator do neither.
312#[cfg(feature = "dfa-prefilter")]
313fn probe_closed_gate() -> bool {
314    GATE_PROBE.with(|state| {
315        let mut x = state.get();
316        x ^= x << 13;
317        x ^= x >> 17;
318        x ^= x << 5;
319        state.set(x);
320        // The high bits decide: xorshift's low bits are its weakest.
321        (u64::from(x) * u64::from(SEEK_GATE_PROBE_INTERVAL)) >> 32 == 0
322    })
323}
324
325/// Estimated instructions of one consultation: a seek search of the text from
326/// the start, about 250 to 410 on the production patterns that consult it.
327#[cfg(feature = "dfa-prefilter")]
328const SEEK_CONSULT_COST_IR: u32 = 300;
329
330/// Estimated instructions one rejection saves: the matcher's attempts on a
331/// text the seek rejects. Estimated from the production cases that reject:
332/// about 800 for `jinja_float_token` and about 5,000 for
333/// `fluent_bit_nginx`, so this is the low end.
334#[cfg(feature = "dfa-prefilter")]
335const SEEK_REJECT_SAVING_IR: u32 = 800;
336
337/// The rejection rate, in permille, at which a closed [`SeekGate`] opens: the
338/// break-even of a consultation, `SEEK_CONSULT_COST_IR / SEEK_REJECT_SAVING_IR`.
339#[cfg(feature = "dfa-prefilter")]
340const SEEK_GATE_OPEN_PERMILLE: u32 = SEEK_CONSULT_COST_IR * 1000 / SEEK_REJECT_SAVING_IR;
341
342/// The rate, in permille, below which an open [`SeekGate`] closes: two thirds
343/// of the break-even. The gap keeps a run of accepted searches from closing
344/// the gate on a pattern that rejects often.
345#[cfg(feature = "dfa-prefilter")]
346const SEEK_GATE_CLOSE_PERMILLE: u32 = SEEK_GATE_OPEN_PERMILLE * 2 / 3;
347
348/// The adaptive gate of the seek's consultations by the standard capture
349/// methods (ADR-008). A consultation costs about [`SEEK_CONSULT_COST_IR`]
350/// instructions and saves [`SEEK_REJECT_SAVING_IR`] for each rejection, so it
351/// pays at a rejection rate of about [`SEEK_GATE_OPEN_PERMILLE`] per mille.
352///
353/// The gate keeps an exponentially weighted rate of the consultations' answers
354/// (each new answer weighs 1/8, a memory of about eight searches). It starts
355/// open, so the first searches consult the seek and the rate settles within
356/// a few of them. An open gate closes below [`SEEK_GATE_CLOSE_PERMILLE`]; a
357/// closed one consults one search in [`SEEK_GATE_PROBE_INTERVAL`], so the rate
358/// keeps being measured, and opens again at [`SEEK_GATE_OPEN_PERMILLE`].
359///
360/// The gate changes only which search runs the seek: a rejection is the
361/// matcher's answer under every limit, so no result depends on it. Its state
362/// is relaxed atomics, shared by the clones of a `Regex` as the seek's cache
363/// pool is. Two threads may lose an update to the rate, which costs time at
364/// most.
365#[cfg(feature = "dfa-prefilter")]
366struct SeekGate {
367    /// The rejection rate of the consultations, in permille.
368    rate: AtomicU32,
369    /// Whether every search consults the seek.
370    open: AtomicBool,
371    /// Test-only override: 0 is the adaptive gate, 1 consults on every search,
372    /// 2 on none.
373    #[cfg(test)]
374    forced: std::sync::atomic::AtomicU8,
375}
376
377#[cfg(feature = "dfa-prefilter")]
378impl SeekGate {
379    const fn new() -> Self {
380        SeekGate {
381            rate: AtomicU32::new(400),
382            open: AtomicBool::new(true),
383            #[cfg(test)]
384            forced: std::sync::atomic::AtomicU8::new(0),
385        }
386    }
387
388    /// Whether this search consults the seek.
389    #[inline]
390    fn consults(&self) -> bool {
391        #[cfg(test)]
392        match self.forced.load(Ordering::Relaxed) {
393            1 => return true,
394            2 => return false,
395            _ => {}
396        }
397        self.open.load(Ordering::Relaxed) || probe_closed_gate()
398    }
399
400    /// Records the answer of one consultation and sets the state for the next
401    /// search.
402    fn record(&self, rejected: bool) {
403        let sample = if rejected { 1000 } else { 0 };
404        let previous = self.rate.load(Ordering::Relaxed);
405        let rate = (previous * 7 + sample) / 8;
406        // Stores only on a change: a steady rejection rate reaches a fixed
407        // point (0 or 993), and threads that keep rewriting the same value
408        // still take the cache line from each other.
409        if rate != previous {
410            self.rate.store(rate, Ordering::Relaxed);
411        }
412        let open = self.open.load(Ordering::Relaxed);
413        let threshold = if open {
414            SEEK_GATE_CLOSE_PERMILLE
415        } else {
416            SEEK_GATE_OPEN_PERMILLE
417        };
418        if (rate >= threshold) != open {
419            self.open.store(!open, Ordering::Relaxed);
420        }
421    }
422
423    /// Whether the gate is open and the rejection rate, in permille. For the
424    /// tests.
425    #[cfg(test)]
426    fn state(&self) -> (bool, u32) {
427        (
428            self.open.load(Ordering::Relaxed),
429            self.rate.load(Ordering::Relaxed),
430        )
431    }
432}
433
434/// Whether a builder and `Regex::new` build the seek automaton by default:
435/// with the `dfa-prefilter` feature, yes (ADR-008).
436const DFA_DEFAULT: bool = cfg!(feature = "dfa-prefilter");
437
438/// Whether `start` is a character boundary of valid UTF-8 `text` (or the
439/// end of it). The seek reads characters from there.
440#[cfg(feature = "dfa-prefilter")]
441fn is_char_boundary(text: &[u8], start: usize) -> bool {
442    text.get(start).is_none_or(|&byte| (byte as i8) >= -0x40)
443}
444
445/// A compiled regular expression.
446///
447/// Cloning a `Regex` is cheap: clones share one compiled program through an
448/// `Arc`, so a clone costs one atomic increment and no recompilation.
449///
450/// `Regex` is [`UnwindSafe`](std::panic::UnwindSafe) and
451/// [`RefUnwindSafe`](std::panic::RefUnwindSafe), so it can be used inside
452/// [`std::panic::catch_unwind`] without a wrapper. This holds because the
453/// compiled program is never mutated after compilation: searches only read
454/// it, apart from search helpers built on first use behind a `OnceLock`, which
455/// is unwind-safe itself. Their scratch state lives in thread-local caches and
456/// in each search's own region. The one field the auto traits reject is the
457/// `&'static dyn Encoding` inside `RegexType`, since a trait object does not
458/// promise `RefUnwindSafe`. Encodings are stateless statics, so the assertion
459/// is sound, and it needs no `unsafe`.
460///
461/// # Examples
462///
463/// ```
464/// use ferroni::api::Regex;
465///
466/// let re = Regex::new(r"\d+").unwrap();
467/// assert!(re.is_match("hello 42"));
468///
469/// let m = re.find("hello 42").unwrap();
470/// assert_eq!(m.as_str(), "42");
471/// assert_eq!(m.start(), 6);
472/// assert_eq!(m.end(), 8);
473/// ```
474///
475/// A `Regex` parses from a string and displays as its pattern:
476///
477/// ```
478/// use ferroni::api::Regex;
479///
480/// let re: Regex = r"\d+".parse().unwrap();
481/// assert_eq!(re.to_string(), r"\d+");
482/// assert_eq!(re.clone().as_str(), Some(r"\d+"));
483/// ```
484#[derive(Clone)]
485pub struct Regex {
486    inner: Arc<Compiled>,
487}
488
489impl Regex {
490    /// Compile a pattern using default options (Oniguruma syntax, UTF-8, no flags).
491    pub fn new(pattern: &str) -> Result<Regex, RegexError> {
492        Self::new_bytes(pattern.as_bytes())
493    }
494
495    /// Compile a pattern from raw bytes using default options.
496    ///
497    /// The pattern is kept as given, so [`Regex::as_bytes`] returns exactly
498    /// these bytes. Oniguruma accepts some byte sequences that are not valid
499    /// UTF-8 (encoded surrogates, for example), and [`Regex::as_str`] returns
500    /// `None` for those.
501    pub fn new_bytes(pattern: &[u8]) -> Result<Regex, RegexError> {
502        let (program, seek, accept) = onig_new_for_single_seek(
503            pattern,
504            ONIG_OPTION_NONE,
505            &ONIG_ENCODING_UTF8,
506            &OnigSyntaxOniguruma,
507            false,
508            DFA_DEFAULT,
509        )?;
510        Ok(Self::from_parts(program, pattern.into(), seek, accept))
511    }
512
513    /// Wraps a compiled program, its seek and its accept seek. The seek's
514    /// automaton is built by the first search that consults it (ADR-008,
515    /// `LazySeek`), once for every clone.
516    #[cfg_attr(not(feature = "dfa-prefilter"), allow(unused_variables))]
517    fn from_parts(
518        program: RegexType,
519        pattern: Box<[u8]>,
520        seek: ScannerSeek,
521        accept: AcceptSeek,
522    ) -> Regex {
523        // A pure literal is found by a literal search, so it has no automaton.
524        // The automaton is boxed: inline, it grew every compiled regex, and
525        // the compile and search costs of the others with it.
526        #[cfg(feature = "dfa-prefilter")]
527        let seek = AssertUnwindSafe(LazySeek::new(if program.pure_literal {
528            None
529        } else {
530            seek.map(|seek| (seek, accept))
531        }));
532        Regex {
533            inner: Arc::new(Compiled {
534                program: AssertUnwindSafe(program),
535                pattern,
536                #[cfg(feature = "dfa-prefilter")]
537                seek,
538                #[cfg(feature = "dfa-prefilter")]
539                seek_gate: SeekGate::new(),
540            }),
541        }
542    }
543
544    /// The compiled program, shared by every clone of this regex.
545    fn program(&self) -> &RegexType {
546        &self.inner.program
547    }
548
549    /// The answer of the single-pattern seek (ADR-008) to a standard search
550    /// from `start`, when it decides one, and `None` for the matcher.
551    ///
552    /// `Some(false)` is "no match", given when no seek match starts in
553    /// `text[start..]`: the matcher can only find a match the seek describes,
554    /// so the answer is the matcher's under every limit, which can only turn
555    /// a match into "no match".
556    ///
557    /// `Some(true)` is "match", given with `accept` only when the seek is
558    /// exact (`SeekCode::exact`) or the accept seek, an under-approximation
559    /// of the pattern, matches. A match of either is a match the matcher
560    /// finds when no limit stops it. Under ADR-017 that is the answer
561    /// wherever the matcher would stop at a process-wide limit: C then stops
562    /// with an error, and Ferroni answers the result C gives without the
563    /// limit (rule 2), and it never reports an error where C completes
564    /// (rule 3). With `accept`, the accept seek also answers "no match" where
565    /// it is exact on the text (`SingleSeek::accept_answer`), which is the
566    /// matcher's answer under every limit. Every other case goes to the
567    /// matcher. `find` does not accept, since it needs the match's bounds.
568    ///
569    /// `utf8` says whether `text` is known to be valid UTF-8; otherwise it is
570    /// checked. A seek reads characters, so the answer is only the matcher's
571    /// for valid UTF-8 and a `start` on a character boundary. Searches under
572    /// the caller's own limits (`*_with`) do not come here, so their limit
573    /// errors are the matcher's.
574    #[cfg(feature = "dfa-prefilter")]
575    #[inline]
576    fn seek_answer(&self, text: &[u8], start: usize, mut utf8: bool, accept: bool) -> Option<bool> {
577        let seek = self.inner.seek.get()?;
578        if accept && seek.has_accept() {
579            if !(utf8 || std::str::from_utf8(text).is_ok()) || !is_char_boundary(text, start) {
580                return None;
581            }
582            utf8 = true;
583            if let Some(answer) = seek.accept_answer(text, start) {
584                #[cfg(feature = "prefilter-self-check")]
585                self.check_accept_answer(text, start, answer);
586                return Some(answer);
587            }
588        }
589        let found = seek.search(text, start)?;
590        if found && !(accept && seek.exact()) {
591            return None;
592        }
593        if !(utf8 || std::str::from_utf8(text).is_ok()) || !is_char_boundary(text, start) {
594            return None;
595        }
596        Some(found)
597    }
598
599    /// Validation (feature `prefilter-self-check`): an answer of the accept
600    /// seek is the matcher's wherever the matcher completes under the
601    /// process-wide limits.
602    #[cfg(feature = "prefilter-self-check")]
603    fn check_accept_answer(&self, text: &[u8], start: usize, answer: bool) {
604        let (result, _) = onig_search(
605            self.program(),
606            text,
607            text.len(),
608            start,
609            text.len(),
610            None,
611            ONIG_OPTION_NONE,
612        );
613        if result >= 0 || result == ONIG_MISMATCH {
614            assert_eq!(
615                result >= 0,
616                answer,
617                "the accept seek of {:?} answers {answer} on {:?} from {start}",
618                String::from_utf8_lossy(self.as_bytes()),
619                String::from_utf8_lossy(text),
620            );
621        }
622    }
623
624    #[cfg(not(feature = "dfa-prefilter"))]
625    #[inline]
626    fn seek_answer(&self, text: &[u8], start: usize, utf8: bool, accept: bool) -> Option<bool> {
627        let _ = (text, start, utf8, accept);
628        None
629    }
630
631    /// Whether the seek rejects a capture or find search from `start`: the answer of
632    /// [`Regex::seek_answer`] is "no match", and the adaptive gate
633    /// ([`SeekGate`]) lets this search consult the seek. The rejection is the
634    /// matcher's answer under every limit, so the capture and find methods return `None`
635    /// for it without running the matcher.
636    #[cfg(feature = "dfa-prefilter")]
637    #[inline]
638    fn seek_rejects_search(&self, text: &[u8], start: usize, utf8: bool) -> bool {
639        if !self.inner.seek.may_exist() || !self.inner.seek_gate.consults() {
640            return false;
641        }
642        let rejected = self.seek_answer(text, start, utf8, false) == Some(false);
643        self.inner.seek_gate.record(rejected);
644        rejected
645    }
646
647    #[cfg(not(feature = "dfa-prefilter"))]
648    #[inline(always)]
649    fn seek_rejects_search(&self, text: &[u8], start: usize, utf8: bool) -> bool {
650        let _ = (text, start, utf8);
651        false
652    }
653
654    /// Whether the regex has a seek automaton. For the tests.
655    #[cfg(all(test, feature = "dfa-prefilter"))]
656    pub(crate) fn seek_present(&self) -> bool {
657        self.inner.seek.get().is_some()
658    }
659
660    /// Whether the seek's automaton was built. For the tests.
661    #[cfg(all(test, feature = "dfa-prefilter"))]
662    pub(crate) fn seek_built(&self) -> bool {
663        self.inner.seek.built()
664    }
665
666    /// Whether the seek automaton is exact. For the tests.
667    #[cfg(all(test, feature = "dfa-prefilter"))]
668    pub(crate) fn seek_exact(&self) -> Option<bool> {
669        self.inner.seek.get().map(SingleSeek::exact)
670    }
671
672    /// The seek's answer to `is_match` on `text` from 0, accepting where it
673    /// may, as `is_match` asks. For the tests.
674    #[cfg(all(test, feature = "dfa-prefilter"))]
675    pub(crate) fn seek_decides_is_match(&self, text: &str) -> Option<bool> {
676        self.seek_answer(text.as_bytes(), 0, true, true)
677    }
678
679    /// Whether the regex has an accept seek. For the tests.
680    #[cfg(all(test, feature = "dfa-prefilter"))]
681    pub(crate) fn accept_seek_present(&self) -> bool {
682        self.inner.seek.get().is_some_and(SingleSeek::has_accept)
683    }
684
685    /// The accept seek's own answer on `text` from `start`
686    /// (`SingleSeek::accept_answer`). For the tests.
687    #[cfg(all(test, feature = "dfa-prefilter"))]
688    pub(crate) fn accept_answer(&self, text: &str, start: usize) -> Option<bool> {
689        self.inner.seek.get()?.accept_answer(text.as_bytes(), start)
690    }
691
692    /// Heap memory of the seek's automata and this thread's caches. For the
693    /// tests.
694    #[cfg(all(test, feature = "dfa-prefilter"))]
695    pub(crate) fn seek_memory_usage(&self) -> usize {
696        self.inner.seek.get().map_or(0, SingleSeek::memory_usage)
697    }
698
699    /// The adaptive gate of the capture methods: whether it is open and its
700    /// rejection rate in permille. For the tests.
701    #[cfg(all(test, feature = "dfa-prefilter"))]
702    pub(crate) fn seek_gate_state(&self) -> (bool, u32) {
703        self.inner.seek_gate.state()
704    }
705
706    /// Forces the gate on (`Some(true)`), off (`Some(false)`) or back to the
707    /// adaptive rule (`None`). For the tests.
708    #[cfg(all(test, feature = "dfa-prefilter"))]
709    pub(crate) fn seek_gate_force(&self, mode: Option<bool>) {
710        let value = match mode {
711            None => 0,
712            Some(true) => 1,
713            Some(false) => 2,
714        };
715        self.inner
716            .seek_gate
717            .forced
718            .store(value, std::sync::atomic::Ordering::Relaxed);
719    }
720
721    /// The pattern this regex was compiled from, or `None` if the pattern is
722    /// not valid UTF-8 (only possible through [`Regex::new_bytes`]).
723    ///
724    /// Use [`Regex::as_bytes`] to read the pattern losslessly.
725    ///
726    /// ```
727    /// use ferroni::api::Regex;
728    ///
729    /// assert_eq!(Regex::new(r"a+b").unwrap().as_str(), Some("a+b"));
730    /// assert_eq!(Regex::new_bytes(b"a+b").unwrap().as_str(), Some("a+b"));
731    /// ```
732    pub fn as_str(&self) -> Option<&str> {
733        std::str::from_utf8(&self.inner.pattern).ok()
734    }
735
736    /// The pattern's bytes, exactly as passed to [`Regex::new`] or
737    /// [`Regex::new_bytes`].
738    ///
739    /// ```
740    /// use ferroni::api::Regex;
741    ///
742    /// assert_eq!(Regex::new("é").unwrap().as_bytes(), "é".as_bytes());
743    /// ```
744    pub fn as_bytes(&self) -> &[u8] {
745        &self.inner.pattern
746    }
747
748    /// Iterate over the names of the capture groups, in group order.
749    ///
750    /// The iterator yields one item per group, starting with group 0, so item
751    /// `i` names group `i`, the index that [`Captures::get`] takes. Group 0
752    /// and unnamed groups yield `None`. A name shared by several groups
753    /// appears at each of them.
754    ///
755    /// Oniguruma does not capture plain `(...)` groups in a pattern that has
756    /// named groups, so such groups are absent from the iterator as well.
757    ///
758    /// ```
759    /// use ferroni::api::Regex;
760    ///
761    /// let re = Regex::new(r"(?<year>\d{4})-(?<month>\d{2})").unwrap();
762    /// let names: Vec<_> = re.capture_names().collect();
763    /// assert_eq!(names, [None, Some("year"), Some("month")]);
764    /// ```
765    pub fn capture_names(&self) -> CaptureNames<'_> {
766        CaptureNames {
767            table: self.program().name_table.as_ref(),
768            next_group: 0,
769            end_group: self.captures_len() as i32 + 1,
770        }
771    }
772
773    /// Create a [`RegexBuilder`] for fine-grained control over compilation.
774    pub fn builder(pattern: &str) -> RegexBuilder {
775        RegexBuilder::new(pattern)
776    }
777
778    /// Return the first match in `text`, or `None` if no match.
779    ///
780    /// A search that stops at a process-wide limit (time, retry or stack) is
781    /// reported as no match. Use [`Regex::find_with`] with [`SearchOptions`] to tell
782    /// the two apart.
783    pub fn find<'t>(&self, text: &'t str) -> Option<Match<'t>> {
784        self.find_from(Subject::Str(text), 0)
785    }
786
787    /// Return the first match in `text` (as bytes), or `None` if no match.
788    pub fn find_bytes<'t>(&self, text: &'t [u8]) -> Option<Match<'t>> {
789        self.find_from(Subject::Bytes(text), 0)
790    }
791
792    /// Return the first match in `text` at or after byte offset `start`, or
793    /// `None` if no match.
794    ///
795    /// The search sees all of `text`, not only the part from `start`. A
796    /// look-behind, `\b` or `^` at `start` looks at the bytes before it, and
797    /// `\G` matches at `start`. Slicing the text instead (`&text[start..]`)
798    /// hides those bytes, which can change the result. The returned offsets
799    /// are relative to `text`.
800    ///
801    /// # Panics
802    ///
803    /// Panics if `start` is greater than `text.len()` or is not on a char
804    /// boundary of `text`.
805    ///
806    /// ```
807    /// use ferroni::api::Regex;
808    ///
809    /// let re = Regex::new(r"(?<=a)b").unwrap();
810    /// // The look-behind sees the `a` before offset 1.
811    /// assert_eq!(re.find_at("ab", 1).unwrap().range(), 1..2);
812    /// // A slice is a new text, and its `a` is out of view.
813    /// assert!(re.find(&"ab"[1..]).is_none());
814    /// ```
815    ///
816    /// `\G` matches at `start` itself:
817    ///
818    /// ```
819    /// use ferroni::api::Regex;
820    ///
821    /// let re = Regex::new(r"\Gb").unwrap();
822    /// assert_eq!(re.find_at("ab", 1).unwrap().range(), 1..2);
823    /// assert!(re.find_at("ab", 0).is_none());
824    /// ```
825    #[inline]
826    pub fn find_at<'t>(&self, text: &'t str, start: usize) -> Option<Match<'t>> {
827        check_str_start(text, start);
828        self.find_from(Subject::Str(text), start)
829    }
830
831    /// Return the first match in `text` (as bytes) at or after byte offset
832    /// `start`, or `None` if no match.
833    ///
834    /// Behaves as [`Regex::find_at`] does, except that `text` is not required
835    /// to be valid UTF-8 and `start` may fall inside a character.
836    ///
837    /// # Panics
838    ///
839    /// Panics if `start` is greater than `text.len()`.
840    pub fn find_bytes_at<'t>(&self, text: &'t [u8], start: usize) -> Option<Match<'t>> {
841        check_bytes_start(text, start);
842        self.find_from(Subject::Bytes(text), start)
843    }
844
845    /// The search behind the `find` family: the first match from `start`,
846    /// with all of `text` visible.
847    #[inline]
848    fn find_from<'t>(&self, subject: Subject<'t>, start: usize) -> Option<Match<'t>> {
849        // The adaptive gate decides, as for the capture methods: a seek that
850        // keeps finding matches only repeats the matcher's scan up to the
851        // match, so a search consults it only while it rejects often enough.
852        if self.seek_rejects_search(subject.bytes(), start, subject.is_str()) {
853            return None;
854        }
855        // A standard search reports a limit it reached as no match.
856        self.find_from_limited(subject, start, None).ok().flatten()
857    }
858
859    /// The search behind the `find` family under `limits` (`None`: the
860    /// process-wide limits). Returns the code of a limit the search reached.
861    #[inline]
862    fn find_from_limited<'t>(
863        &self,
864        subject: Subject<'t>,
865        start: usize,
866        limits: Option<SearchLimits>,
867    ) -> Result<Option<Match<'t>>, i32> {
868        let text = subject.bytes();
869        // Only the bounds are returned: where they follow from the attempt
870        // position and the match length, no region is needed (see
871        // `find_iter_bytes`).
872        if !self.program().keep_moves_match_start
873            && !self.program().options.contains(ONIG_OPTION_FIND_LONGEST)
874        {
875            // Rust-only (ADR-008): a pure literal pattern skips the search state.
876            // It runs no attempt that could fail, so no limit can be reached.
877            if let Some(found) = pure_literal_first(
878                self.program(),
879                text,
880                text.len(),
881                start,
882                text.len(),
883                text.len(),
884                self.program().options,
885            ) {
886                return Ok(found.map(|(match_start, match_end)| Match {
887                    subject,
888                    start: match_start,
889                    end: match_end,
890                }));
891            }
892            let mut msa = take_cached_msa(self.program(), ONIG_OPTION_NONE, None, start, limits);
893            let found = onig_search_bounds(self.program(), text, start, &mut msa);
894            cache_msa(msa);
895            let Some((match_start, match_end)) = found? else {
896                return Ok(None);
897            };
898            return Ok((match_end <= text.len()).then_some(Match {
899                subject,
900                start: match_start,
901                end: match_end,
902            }));
903        }
904        let len = text.len();
905        let (result, region) = search_in_range(
906            self.program(),
907            text,
908            len,
909            start,
910            len,
911            len,
912            Some(take_cached_region()),
913            ONIG_OPTION_NONE,
914            limits,
915        );
916        let Some(region) = region else {
917            return Ok(None);
918        };
919        if result < 0 {
920            cache_region(region);
921            return if result == ONIG_MISMATCH {
922                Ok(None)
923            } else {
924                Err(result)
925            };
926        }
927        if region.num_regs < 1 {
928            cache_region(region);
929            return Ok(None);
930        }
931        let m = Match::from_region(subject, region.beg[0], region.end[0]);
932        cache_region(region);
933        Ok(m)
934    }
935
936    /// Return the first match in `text` under per-search `options`.
937    ///
938    /// # Errors
939    ///
940    /// Returns the [`RegexError`] of a limit the search reached, such as
941    /// [`RegexError::TimeLimitOver`], instead of treating it as "no match".
942    #[inline]
943    pub fn find_with<'t>(
944        &self,
945        text: &'t str,
946        options: SearchOptions,
947    ) -> Result<Option<Match<'t>>, RegexError> {
948        self.find_from_with(Subject::Str(text), 0, options)
949    }
950
951    /// Return the first match in `text` (as bytes) under per-search `options`.
952    ///
953    /// # Errors
954    ///
955    /// Returns the [`RegexError`] of a limit the search reached.
956    #[inline]
957    pub fn find_bytes_with<'t>(
958        &self,
959        text: &'t [u8],
960        options: SearchOptions,
961    ) -> Result<Option<Match<'t>>, RegexError> {
962        self.find_from_with(Subject::Bytes(text), 0, options)
963    }
964
965    /// Return the first match in `text` at or after byte offset `start` under
966    /// per-search `options`.
967    ///
968    /// Searches as [`Regex::find_at`] does.
969    ///
970    /// # Errors
971    ///
972    /// Returns the [`RegexError`] of a limit the search reached.
973    ///
974    /// # Panics
975    ///
976    /// Panics if `start` is greater than `text.len()` or is not on a char
977    /// boundary of `text`.
978    #[inline]
979    pub fn find_at_with<'t>(
980        &self,
981        text: &'t str,
982        start: usize,
983        options: SearchOptions,
984    ) -> Result<Option<Match<'t>>, RegexError> {
985        check_str_start(text, start);
986        self.find_from_with(Subject::Str(text), start, options)
987    }
988
989    /// Return the first match in `text` (as bytes) at or after byte offset
990    /// `start` under per-search `options`.
991    ///
992    /// # Errors
993    ///
994    /// Returns the [`RegexError`] of a limit the search reached.
995    ///
996    /// # Panics
997    ///
998    /// Panics if `start` is greater than `text.len()`.
999    #[inline]
1000    pub fn find_bytes_at_with<'t>(
1001        &self,
1002        text: &'t [u8],
1003        start: usize,
1004        options: SearchOptions,
1005    ) -> Result<Option<Match<'t>>, RegexError> {
1006        check_bytes_start(text, start);
1007        self.find_from_with(Subject::Bytes(text), start, options)
1008    }
1009
1010    fn find_from_with<'t>(
1011        &self,
1012        subject: Subject<'t>,
1013        start: usize,
1014        options: SearchOptions,
1015    ) -> Result<Option<Match<'t>>, RegexError> {
1016        self.find_from_limited(subject, start, options.limits())
1017            .map_err(RegexError::from)
1018    }
1019
1020    /// Check whether `text` matches the pattern anywhere.
1021    ///
1022    /// A search that stops at a process-wide limit (time, retry or stack) is
1023    /// reported as no match. Use [`Regex::is_match_with`] with [`SearchOptions`] to tell
1024    /// the two apart. The single-pattern seek ([`RegexBuilder::dfa`]) can
1025    /// report a match that the matcher reaches only without the limit.
1026    pub fn is_match(&self, text: &str) -> bool {
1027        self.is_match_from(text.as_bytes(), 0, true)
1028    }
1029
1030    /// Check whether `text` (as bytes) matches the pattern anywhere.
1031    pub fn is_match_bytes(&self, text: &[u8]) -> bool {
1032        self.is_match_from(text, 0, false)
1033    }
1034
1035    /// Check whether `text` has a match at or after byte offset `start`.
1036    ///
1037    /// Searches as [`Regex::find_at`] does: look-behind, `\b` and `^` see the
1038    /// bytes before `start`, and `\G` matches at `start`.
1039    ///
1040    /// # Panics
1041    ///
1042    /// Panics if `start` is greater than `text.len()` or is not on a char
1043    /// boundary of `text`.
1044    pub fn is_match_at(&self, text: &str, start: usize) -> bool {
1045        check_str_start(text, start);
1046        self.is_match_from(text.as_bytes(), start, true)
1047    }
1048
1049    /// Check whether `text` (as bytes) has a match at or after byte offset
1050    /// `start`.
1051    ///
1052    /// # Panics
1053    ///
1054    /// Panics if `start` is greater than `text.len()`.
1055    pub fn is_match_bytes_at(&self, text: &[u8], start: usize) -> bool {
1056        check_bytes_start(text, start);
1057        self.is_match_from(text, start, false)
1058    }
1059
1060    #[inline]
1061    fn is_match_from(&self, text: &[u8], start: usize, utf8: bool) -> bool {
1062        if let Some(answer) = self.seek_answer(text, start, utf8, true) {
1063            return answer;
1064        }
1065        let (result, _) = onig_search(
1066            self.program(),
1067            text,
1068            text.len(),
1069            start,
1070            text.len(),
1071            None,
1072            ONIG_OPTION_NONE,
1073        );
1074        result >= 0
1075    }
1076
1077    /// Check whether `text` matches under per-search `options`.
1078    ///
1079    /// # Errors
1080    ///
1081    /// Returns the [`RegexError`] of a limit the search reached.
1082    #[inline]
1083    pub fn is_match_with(&self, text: &str, options: SearchOptions) -> Result<bool, RegexError> {
1084        self.is_match_bytes_with(text.as_bytes(), options)
1085    }
1086
1087    /// Check whether `text` (as bytes) matches under per-search `options`.
1088    ///
1089    /// # Errors
1090    ///
1091    /// Returns the [`RegexError`] of a limit the search reached.
1092    #[inline]
1093    pub fn is_match_bytes_with(
1094        &self,
1095        text: &[u8],
1096        options: SearchOptions,
1097    ) -> Result<bool, RegexError> {
1098        self.is_match_from_with(text, 0, options)
1099    }
1100
1101    /// Check whether `text` has a match at or after byte offset `start` under
1102    /// per-search `options`.
1103    ///
1104    /// # Errors
1105    ///
1106    /// Returns the [`RegexError`] of a limit the search reached.
1107    ///
1108    /// # Panics
1109    ///
1110    /// Panics if `start` is greater than `text.len()` or is not on a char
1111    /// boundary of `text`.
1112    #[inline]
1113    pub fn is_match_at_with(
1114        &self,
1115        text: &str,
1116        start: usize,
1117        options: SearchOptions,
1118    ) -> Result<bool, RegexError> {
1119        check_str_start(text, start);
1120        self.is_match_from_with(text.as_bytes(), start, options)
1121    }
1122
1123    /// Check whether `text` (as bytes) has a match at or after byte offset
1124    /// `start` under per-search `options`.
1125    ///
1126    /// # Errors
1127    ///
1128    /// Returns the [`RegexError`] of a limit the search reached.
1129    ///
1130    /// # Panics
1131    ///
1132    /// Panics if `start` is greater than `text.len()`.
1133    #[inline]
1134    pub fn is_match_bytes_at_with(
1135        &self,
1136        text: &[u8],
1137        start: usize,
1138        options: SearchOptions,
1139    ) -> Result<bool, RegexError> {
1140        check_bytes_start(text, start);
1141        self.is_match_from_with(text, start, options)
1142    }
1143
1144    fn is_match_from_with(
1145        &self,
1146        text: &[u8],
1147        start: usize,
1148        options: SearchOptions,
1149    ) -> Result<bool, RegexError> {
1150        let len = text.len();
1151        let (result, _) = search_in_range(
1152            self.program(),
1153            text,
1154            len,
1155            start,
1156            len,
1157            len,
1158            None,
1159            ONIG_OPTION_NONE,
1160            options.limits(),
1161        );
1162        if result < 0 && result != ONIG_MISMATCH {
1163            return Err(RegexError::from(result));
1164        }
1165        Ok(result >= 0)
1166    }
1167
1168    /// Return the first match with all capture groups, or `None`.
1169    ///
1170    /// A search that stops at a process-wide limit (time, retry or stack) is
1171    /// reported as no match. Use [`Regex::captures_with`] with [`SearchOptions`] to tell
1172    /// the two apart.
1173    pub fn captures<'t>(&self, text: &'t str) -> Option<Captures<'t>> {
1174        self.captures_from(Subject::Str(text), 0)
1175    }
1176
1177    /// Return the first match with all capture groups (bytes), or `None`.
1178    pub fn captures_bytes<'t>(&self, text: &'t [u8]) -> Option<Captures<'t>> {
1179        self.captures_from(Subject::Bytes(text), 0)
1180    }
1181
1182    /// Return the first match at or after byte offset `start`, with all
1183    /// capture groups, or `None`.
1184    ///
1185    /// Searches as [`Regex::find_at`] does. Group 0 is the match, and the
1186    /// offsets of every group are relative to `text`.
1187    ///
1188    /// # Panics
1189    ///
1190    /// Panics if `start` is greater than `text.len()` or is not on a char
1191    /// boundary of `text`.
1192    ///
1193    /// ```
1194    /// use ferroni::api::Regex;
1195    ///
1196    /// let re = Regex::new(r"(?<=a)(b)(c)").unwrap();
1197    /// let caps = re.captures_at("abc", 1).unwrap();
1198    /// assert_eq!(&caps[0], "bc");
1199    /// assert_eq!(&caps[1], "b");
1200    /// assert_eq!(&caps[2], "c");
1201    /// ```
1202    pub fn captures_at<'t>(&self, text: &'t str, start: usize) -> Option<Captures<'t>> {
1203        check_str_start(text, start);
1204        self.captures_from(Subject::Str(text), start)
1205    }
1206
1207    /// Return the first match at or after byte offset `start` (as bytes), with
1208    /// all capture groups, or `None`.
1209    ///
1210    /// # Panics
1211    ///
1212    /// Panics if `start` is greater than `text.len()`.
1213    pub fn captures_bytes_at<'t>(&self, text: &'t [u8], start: usize) -> Option<Captures<'t>> {
1214        check_bytes_start(text, start);
1215        self.captures_from(Subject::Bytes(text), start)
1216    }
1217
1218    /// The search behind the capture methods that report no limit: the seek
1219    /// answers a rejection first (see [`Regex::seek_rejects_search`]).
1220    #[inline]
1221    fn captures_from<'t>(&self, subject: Subject<'t>, start: usize) -> Option<Captures<'t>> {
1222        let text = subject.bytes();
1223        if self.seek_rejects_search(text, start, subject.is_str()) {
1224            return None;
1225        }
1226        let (result, region) =
1227            onig_search_captures(self.program(), text, start, take_cached_region());
1228        let region = region?;
1229        if result < 0 {
1230            cache_region(region);
1231            return None;
1232        }
1233        Some(Captures {
1234            text: subject,
1235            region,
1236            compiled: name_lookup(&self.inner),
1237        })
1238    }
1239
1240    /// Return the first match with all capture groups under per-search
1241    /// `options`.
1242    ///
1243    /// # Errors
1244    ///
1245    /// Returns the [`RegexError`] of a limit the search reached.
1246    #[inline]
1247    pub fn captures_with<'t>(
1248        &self,
1249        text: &'t str,
1250        options: SearchOptions,
1251    ) -> Result<Option<Captures<'t>>, RegexError> {
1252        self.captures_from_with(Subject::Str(text), 0, options)
1253    }
1254
1255    /// Return the first match with all capture groups (bytes) under
1256    /// per-search `options`.
1257    ///
1258    /// # Errors
1259    ///
1260    /// Returns the [`RegexError`] of a limit the search reached.
1261    #[inline]
1262    pub fn captures_bytes_with<'t>(
1263        &self,
1264        text: &'t [u8],
1265        options: SearchOptions,
1266    ) -> Result<Option<Captures<'t>>, RegexError> {
1267        self.captures_from_with(Subject::Bytes(text), 0, options)
1268    }
1269
1270    /// Return the first match at or after byte offset `start`, with all
1271    /// capture groups, under per-search `options`.
1272    ///
1273    /// # Errors
1274    ///
1275    /// Returns the [`RegexError`] of a limit the search reached.
1276    ///
1277    /// # Panics
1278    ///
1279    /// Panics if `start` is greater than `text.len()` or is not on a char
1280    /// boundary of `text`.
1281    #[inline]
1282    pub fn captures_at_with<'t>(
1283        &self,
1284        text: &'t str,
1285        start: usize,
1286        options: SearchOptions,
1287    ) -> Result<Option<Captures<'t>>, RegexError> {
1288        check_str_start(text, start);
1289        self.captures_from_with(Subject::Str(text), start, options)
1290    }
1291
1292    /// Return the first match at or after byte offset `start` (as bytes), with
1293    /// all capture groups, under per-search `options`.
1294    ///
1295    /// # Errors
1296    ///
1297    /// Returns the [`RegexError`] of a limit the search reached.
1298    ///
1299    /// # Panics
1300    ///
1301    /// Panics if `start` is greater than `text.len()`.
1302    #[inline]
1303    pub fn captures_bytes_at_with<'t>(
1304        &self,
1305        text: &'t [u8],
1306        start: usize,
1307        options: SearchOptions,
1308    ) -> Result<Option<Captures<'t>>, RegexError> {
1309        check_bytes_start(text, start);
1310        self.captures_from_with(Subject::Bytes(text), start, options)
1311    }
1312
1313    fn captures_from_with<'t>(
1314        &self,
1315        subject: Subject<'t>,
1316        start: usize,
1317        options: SearchOptions,
1318    ) -> Result<Option<Captures<'t>>, RegexError> {
1319        let text = subject.bytes();
1320        let len = text.len();
1321        let (result, region) = search_in_range(
1322            self.program(),
1323            text,
1324            len,
1325            start,
1326            len,
1327            len,
1328            Some(take_cached_region()),
1329            ONIG_OPTION_NONE,
1330            options.limits(),
1331        );
1332        let Some(region) = region else {
1333            return Ok(None);
1334        };
1335        if result < 0 {
1336            cache_region(region);
1337            return if result == ONIG_MISMATCH {
1338                Ok(None)
1339            } else {
1340                Err(RegexError::from(result))
1341            };
1342        }
1343        Ok(Some(Captures {
1344            text: subject,
1345            region,
1346            compiled: name_lookup(&self.inner),
1347        }))
1348    }
1349
1350    /// Iterate over all non-overlapping matches in `text`.
1351    ///
1352    /// Unlike the `regex` crate, this also reports an empty match right after
1353    /// a non-empty one; see [the guide](https://ferroni.dev/guide/coming-from-regex).
1354    ///
1355    /// Iteration ends early when a search stops at a process-wide limit. Use
1356    /// [`Regex::find_iter_with`] with [`SearchOptions`] to see that error.
1357    pub fn find_iter<'r, 't>(&'r self, text: &'t str) -> FindIter<'r, 't> {
1358        self.find_iter_limited(Subject::Str(text), None)
1359    }
1360
1361    /// Iterate over all non-overlapping matches in `text` (as bytes).
1362    pub fn find_iter_bytes<'r, 't>(&'r self, text: &'t [u8]) -> FindIter<'r, 't> {
1363        self.find_iter_limited(Subject::Bytes(text), None)
1364    }
1365
1366    /// The iterator behind the `find_iter` family under `limits` (`None`: the
1367    /// process-wide limits).
1368    fn find_iter_limited<'r, 't>(
1369        &'r self,
1370        subject: Subject<'t>,
1371        limits: Option<SearchLimits>,
1372    ) -> FindIter<'r, 't> {
1373        // An iterator yields match bounds only. Where they follow from the
1374        // attempt position and the match length, it searches without a
1375        // region, reusing one MatchArg for all its searches.
1376        let bounds_only = !self.program().keep_moves_match_start
1377            && !self.program().options.contains(ONIG_OPTION_FIND_LONGEST);
1378        FindIter {
1379            regex: self,
1380            program: self.program(),
1381            text: subject.bytes(),
1382            subject,
1383            cursor: MatchCursor::default(),
1384            region: if bounds_only {
1385                OnigRegion::new()
1386            } else {
1387                take_cached_region()
1388            },
1389            msa: bounds_only
1390                .then(|| take_cached_msa(self.program(), ONIG_OPTION_NONE, None, 0, limits)),
1391            limits,
1392        }
1393    }
1394
1395    /// Iterate over all non-overlapping matches in `text` under per-search
1396    /// `options`.
1397    ///
1398    /// The options apply to each search the iterator runs. A search that
1399    /// reaches a limit yields its error once and ends the iteration.
1400    #[inline]
1401    pub fn find_iter_with<'r, 't>(
1402        &'r self,
1403        text: &'t str,
1404        options: SearchOptions,
1405    ) -> TryFindIter<'r, 't> {
1406        TryFindIter {
1407            iter: self.find_iter_limited(Subject::Str(text), options.limits()),
1408            finished: false,
1409        }
1410    }
1411
1412    /// Iterate over all non-overlapping matches in `text` (as bytes) under
1413    /// per-search `options`.
1414    #[inline]
1415    pub fn find_iter_bytes_with<'r, 't>(
1416        &'r self,
1417        text: &'t [u8],
1418        options: SearchOptions,
1419    ) -> TryFindIter<'r, 't> {
1420        TryFindIter {
1421            iter: self.find_iter_limited(Subject::Bytes(text), options.limits()),
1422            finished: false,
1423        }
1424    }
1425
1426    /// Iterate over the capture groups of all non-overlapping matches in
1427    /// `text`.
1428    ///
1429    /// The iterator yields the matches of [`Regex::find_iter`], in the same
1430    /// order and with the same empty-match handling, each as a [`Captures`].
1431    /// Iteration ends early when a search stops at a process-wide limit. Use
1432    /// [`Regex::captures_iter_with`] with [`SearchOptions`] to see that error.
1433    ///
1434    /// ```
1435    /// use ferroni::api::Regex;
1436    ///
1437    /// let re = Regex::new(r"(?<year>\d{4})-(?<month>\d{2})").unwrap();
1438    /// let years: Vec<&str> = re
1439    ///     .captures_iter("2025-01 and 2026-10")
1440    ///     .map(|caps| caps.name("year").unwrap().as_str())
1441    ///     .collect();
1442    /// assert_eq!(years, ["2025", "2026"]);
1443    /// ```
1444    pub fn captures_iter<'r, 't>(&'r self, text: &'t str) -> CaptureMatches<'r, 't> {
1445        CaptureMatches::new(self, Subject::Str(text), None)
1446    }
1447
1448    /// Iterate over the capture groups of all non-overlapping matches in
1449    /// `text` (as bytes).
1450    pub fn captures_iter_bytes<'r, 't>(&'r self, text: &'t [u8]) -> CaptureMatches<'r, 't> {
1451        CaptureMatches::new(self, Subject::Bytes(text), None)
1452    }
1453
1454    /// Iterate over the capture groups of all non-overlapping matches in
1455    /// `text` under per-search `options`.
1456    ///
1457    /// The options apply to each search the iterator runs. A search that
1458    /// reaches a limit yields its error once and ends the iteration.
1459    #[inline]
1460    pub fn captures_iter_with<'r, 't>(
1461        &'r self,
1462        text: &'t str,
1463        options: SearchOptions,
1464    ) -> TryCaptureMatches<'r, 't> {
1465        TryCaptureMatches {
1466            iter: CaptureMatches::new(self, Subject::Str(text), options.limits()),
1467            finished: false,
1468        }
1469    }
1470
1471    /// Iterate over the capture groups of all non-overlapping matches in
1472    /// `text` (as bytes) under per-search `options`.
1473    #[inline]
1474    pub fn captures_iter_bytes_with<'r, 't>(
1475        &'r self,
1476        text: &'t [u8],
1477        options: SearchOptions,
1478    ) -> TryCaptureMatches<'r, 't> {
1479        TryCaptureMatches {
1480            iter: CaptureMatches::new(self, Subject::Bytes(text), options.limits()),
1481            finished: false,
1482        }
1483    }
1484
1485    /// Return the number of capture groups in the pattern (excluding group 0).
1486    ///
1487    /// Unlike the `regex` crate, group 0 is not counted; see
1488    /// [the guide](https://ferroni.dev/guide/coming-from-regex).
1489    pub fn captures_len(&self) -> usize {
1490        self.program().num_mem as usize
1491    }
1492
1493    /// Findings of the compile-time check for patterns that can backtrack
1494    /// catastrophically, such as `(a+)+` or `(a|aa)*`.
1495    ///
1496    /// The list is empty for patterns the check does not flag. It is a
1497    /// heuristic: it can miss risky patterns and flag harmless ones, and it
1498    /// never changes how the pattern matches. Use
1499    /// [`RegexBuilder::reject_backtracking_risks`] to turn findings into a
1500    /// compile error.
1501    ///
1502    /// ```
1503    /// use ferroni::api::Regex;
1504    ///
1505    /// assert!(Regex::new(r"(a+)+$").unwrap().backtracking_warnings().len() == 1);
1506    /// assert!(Regex::new(r"(a+b)+$").unwrap().backtracking_warnings().is_empty());
1507    /// ```
1508    pub fn backtracking_warnings(&self) -> &[crate::backtrack_lint::BacktrackWarning] {
1509        &self.program().backtrack_warnings
1510    }
1511
1512    /// Applied and refused decimal-loop candidates from the opt-in rewrite
1513    /// pass, in AST traversal order. Empty when optimization is disabled or
1514    /// no supported shape is recognized. This is not a safety certification.
1515    pub fn backtracking_rewrites(&self) -> &[crate::backtrack_rewrite::BacktrackingRewrite] {
1516        &self.program().backtrack_rewrites
1517    }
1518
1519    /// Access the underlying `RegexType` for advanced / C-style usage.
1520    pub fn as_raw(&self) -> &RegexType {
1521        self.program()
1522    }
1523}
1524
1525/// Shows the pattern. Bytes that are not valid UTF-8 appear as U+FFFD.
1526impl fmt::Debug for Regex {
1527    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1528        f.debug_struct("Regex")
1529            .field("pattern", &String::from_utf8_lossy(&self.inner.pattern))
1530            .finish_non_exhaustive()
1531    }
1532}
1533
1534/// Writes the pattern. Bytes that are not valid UTF-8 appear as U+FFFD; use
1535/// [`Regex::as_bytes`] for the exact bytes.
1536impl fmt::Display for Regex {
1537    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1538        f.write_str(&String::from_utf8_lossy(&self.inner.pattern))
1539    }
1540}
1541
1542/// Equivalent to [`Regex::new`].
1543///
1544/// ```
1545/// use ferroni::api::Regex;
1546///
1547/// let re: Regex = "a+".parse().unwrap();
1548/// assert!(re.is_match("aaa"));
1549/// assert!("(".parse::<Regex>().is_err());
1550/// ```
1551impl FromStr for Regex {
1552    type Err = RegexError;
1553
1554    fn from_str(pattern: &str) -> Result<Self, Self::Err> {
1555        Regex::new(pattern)
1556    }
1557}
1558
1559// === RegexBuilder ===
1560
1561/// Builder for compiling a [`Regex`] with custom options.
1562///
1563/// # Examples
1564///
1565/// ```
1566/// use ferroni::api::Regex;
1567///
1568/// let re = Regex::builder(r"hello world")
1569///     .case_insensitive(true)
1570///     .build()
1571///     .unwrap();
1572/// assert!(re.is_match("Hello World"));
1573/// ```
1574pub struct RegexBuilder {
1575    pattern: Vec<u8>,
1576    options: OnigOptionType,
1577    syntax: &'static OnigSyntaxType,
1578    reject_backtracking_risks: bool,
1579    optimize_backtracking: bool,
1580    dfa: bool,
1581}
1582
1583impl RegexBuilder {
1584    /// Create a new builder for the given pattern.
1585    pub fn new(pattern: &str) -> Self {
1586        Self::new_bytes(pattern.as_bytes())
1587    }
1588
1589    /// Create a new builder for a pattern given as raw bytes.
1590    ///
1591    /// The bytes are compiled exactly as [`Regex::new_bytes`] compiles them.
1592    ///
1593    /// ```
1594    /// use ferroni::api::RegexBuilder;
1595    ///
1596    /// let re = RegexBuilder::new_bytes(b"hello")
1597    ///     .case_insensitive(true)
1598    ///     .build()
1599    ///     .unwrap();
1600    /// assert!(re.is_match("HELLO"));
1601    /// ```
1602    pub fn new_bytes(pattern: &[u8]) -> Self {
1603        RegexBuilder {
1604            pattern: pattern.to_vec(),
1605            options: ONIG_OPTION_NONE,
1606            syntax: &OnigSyntaxOniguruma,
1607            reject_backtracking_risks: false,
1608            optimize_backtracking: false,
1609            dfa: DFA_DEFAULT,
1610        }
1611    }
1612
1613    /// Enable or disable case-insensitive matching.
1614    pub fn case_insensitive(mut self, yes: bool) -> Self {
1615        if yes {
1616            self.options |= ONIG_OPTION_IGNORECASE;
1617        } else {
1618            self.options &= !ONIG_OPTION_IGNORECASE;
1619        }
1620        self
1621    }
1622
1623    /// Enable or disable multiline mode (`.` matches `\n`).
1624    pub fn dot_matches_newline(mut self, yes: bool) -> Self {
1625        if yes {
1626            self.options |= ONIG_OPTION_MULTILINE;
1627        } else {
1628            self.options &= !ONIG_OPTION_MULTILINE;
1629        }
1630        self
1631    }
1632
1633    /// Enable or disable `^`/`$` matching at every line boundary.
1634    ///
1635    /// With the default Oniguruma syntax, this behavior is enabled by default.
1636    /// When disabled, anchors match only at input boundaries. Calling this
1637    /// method overrides the selected syntax's default anchor behavior.
1638    pub fn multi_line_anchors(mut self, yes: bool) -> Self {
1639        if yes {
1640            self.options |= ONIG_OPTION_NEGATE_SINGLELINE;
1641            self.options &= !ONIG_OPTION_SINGLELINE;
1642        } else {
1643            self.options &= !ONIG_OPTION_NEGATE_SINGLELINE;
1644            self.options |= ONIG_OPTION_SINGLELINE;
1645        }
1646        self
1647    }
1648
1649    /// Enable or disable extended mode (whitespace and `#` comments ignored).
1650    pub fn extended(mut self, yes: bool) -> Self {
1651        if yes {
1652            self.options |= ONIG_OPTION_EXTEND;
1653        } else {
1654            self.options &= !ONIG_OPTION_EXTEND;
1655        }
1656        self
1657    }
1658
1659    /// Set a raw option flag. See `ONIG_OPTION_*` constants.
1660    ///
1661    /// Use [`clear_option`](Self::clear_option) to unset a flag again.
1662    pub fn option(mut self, flag: OnigOptionType) -> Self {
1663        self.options |= flag;
1664        self
1665    }
1666
1667    /// Clear a raw option flag set earlier on this builder.
1668    ///
1669    /// The counterpart of [`option`](Self::option). Clearing a flag the
1670    /// builder does not hold has no effect.
1671    ///
1672    /// ```
1673    /// use ferroni::api::Regex;
1674    /// use ferroni::oniguruma::ONIG_OPTION_IGNORECASE;
1675    ///
1676    /// let re = Regex::builder("hello")
1677    ///     .option(ONIG_OPTION_IGNORECASE)
1678    ///     .clear_option(ONIG_OPTION_IGNORECASE)
1679    ///     .build()
1680    ///     .unwrap();
1681    /// assert!(!re.is_match("HELLO"));
1682    /// ```
1683    pub fn clear_option(mut self, flag: OnigOptionType) -> Self {
1684        self.options &= !flag;
1685        self
1686    }
1687
1688    /// Capture unnamed groups even when the pattern also has named groups.
1689    ///
1690    /// `true` sets `ONIG_OPTION_CAPTURE_GROUP`, so named and unnamed groups
1691    /// are all captured. It also clears `ONIG_OPTION_DONT_CAPTURE_GROUP` set
1692    /// through [`option`](Self::option), since the two flags conflict.
1693    ///
1694    /// `false` clears `ONIG_OPTION_CAPTURE_GROUP` and leaves the choice to the
1695    /// syntax, which is the default. The Oniguruma, Ruby and Perl-NG syntaxes
1696    /// capture only the named groups of a pattern that has any; the other
1697    /// syntaxes capture unnamed groups as well.
1698    ///
1699    /// ```
1700    /// use ferroni::api::Regex;
1701    ///
1702    /// let pattern = r"(?<word>\w+) (\d+)";
1703    /// let named_only = Regex::builder(pattern).build().unwrap();
1704    /// assert_eq!(named_only.captures_len(), 1);
1705    ///
1706    /// let all = Regex::builder(pattern).capture_group(true).build().unwrap();
1707    /// assert_eq!(all.captures_len(), 2);
1708    /// ```
1709    pub fn capture_group(mut self, yes: bool) -> Self {
1710        if yes {
1711            self.options |= ONIG_OPTION_CAPTURE_GROUP;
1712            self.options &= !ONIG_OPTION_DONT_CAPTURE_GROUP;
1713        } else {
1714            self.options &= !ONIG_OPTION_CAPTURE_GROUP;
1715        }
1716        self
1717    }
1718
1719    /// Select the syntax definition to use (default: Oniguruma).
1720    ///
1721    /// Pass one of the `OnigSyntax*` statics from [`crate::regsyntax`]. The
1722    /// typed alternative [`syntax_mode`](Self::syntax_mode) selects the same
1723    /// definitions by name.
1724    pub fn syntax(mut self, syntax: &'static OnigSyntaxType) -> Self {
1725        self.syntax = syntax;
1726        self
1727    }
1728
1729    /// Select one of the built-in [`Syntax`] definitions (default: Oniguruma).
1730    ///
1731    /// This is the typed counterpart of [`syntax`](Self::syntax), which takes
1732    /// the `OnigSyntax*` statics. Both set the same definition, and the last
1733    /// call wins.
1734    ///
1735    /// ```
1736    /// use ferroni::api::{Regex, Syntax};
1737    ///
1738    /// // Plain text: metacharacters match only themselves.
1739    /// let re = Regex::builder("a.b").syntax_mode(Syntax::Asis).build().unwrap();
1740    /// assert!(re.is_match("a.b"));
1741    /// assert!(!re.is_match("axb"));
1742    /// ```
1743    pub fn syntax_mode(mut self, syntax: Syntax) -> Self {
1744        self.syntax = syntax.as_onig_syntax();
1745        self
1746    }
1747
1748    /// Fail to compile a pattern that the backtracking check flags.
1749    ///
1750    /// By default such a pattern compiles and the findings are available
1751    /// from [`Regex::backtracking_warnings`]. With this set, `build` returns
1752    /// `ONIGERR_VERY_INEFFICIENT_PATTERN` instead. The check is a heuristic
1753    /// and may flag harmless patterns; leave this off to keep them.
1754    ///
1755    /// ```
1756    /// use ferroni::api::Regex;
1757    ///
1758    /// assert!(Regex::builder(r"(a+)+$").reject_backtracking_risks(true).build().is_err());
1759    /// assert!(Regex::builder(r"(a+)+$").build().is_ok());
1760    /// ```
1761    pub fn reject_backtracking_risks(mut self, yes: bool) -> Self {
1762        self.reject_backtracking_risks = yes;
1763        self
1764    }
1765
1766    /// Enable conservative AST rewrites for backtracking-prone patterns.
1767    ///
1768    /// Default: off. The decimal rule recognizes greedy `(?:[0-9]+_?)+`
1769    /// immediately before a mandatory literal dot. Primitive digit runs use
1770    /// possessive bytecode; captured primitives use an outer atomic group.
1771    /// A greedy `(?:[0-9]+_?)*[0-9]+` before a word boundary uses a
1772    /// deterministic prefix when captures wrap only the repeated body. Other
1773    /// capture placements keep the complete pair atomic and its give-back
1774    /// choices. Both rules retain all groups and successful results. Unsupported
1775    /// constructs stay unchanged. Retry, stack, and time-limit outcomes may
1776    /// differ because the optimized matcher performs less work. This does
1777    /// not guarantee linear-time unanchored searches.
1778    ///
1779    /// See [`Regex::backtracking_rewrites`] for applied and refused candidates.
1780    /// Original lint warnings remain available, and risk rejection still
1781    /// rejects the original pattern even when a rewrite would apply.
1782    pub fn optimize_backtracking(mut self, yes: bool) -> Self {
1783        self.optimize_backtracking = yes;
1784        self
1785    }
1786
1787    /// Enable or disable the single-pattern seek automaton for standard
1788    /// searches (ADR-008).
1789    ///
1790    /// The default is on when the `dfa-prefilter` feature is enabled, which
1791    /// it is by default, and off otherwise. With `true`, [`RegexBuilder::build`]
1792    /// derives the seek from the compiled tree and builds its lazy DFA in the
1793    /// same compilation, so the cost is paid once, and the clones and searches
1794    /// of the returned [`Regex`] share the automaton. A standard `is_match`
1795    /// asks the automaton first. The automaton answers "no match" whenever
1796    /// it can, and answers "match" for an exact seek's match, which is the
1797    /// matcher's match with no limit. A pattern whose classes have
1798    /// multi-character case folds (`(?i)[a-zß]`) and whose seek is not exact
1799    /// may get a second automaton, an accept seek, whose matches are matches
1800    /// of the pattern: `is_match` answers "match" from it, and "no match"
1801    /// on a text that holds none of the strings the folds add (`ss`, `ff`,
1802    /// or any non-ASCII character). Where a process-wide limit would stop
1803    /// the matcher, `is_match` can therefore report the match that the
1804    /// matcher finds without the limit (ADR-017). In every other case the
1805    /// matcher answers, so the results, including the limit errors of
1806    /// [`Regex::find_with`] and the other `*_with` methods, are the same with
1807    /// the option on or off.
1808    ///
1809    /// The cost is compile time and memory. The seek is derived in the same
1810    /// compilation as the program, so `build` does not compile the pattern
1811    /// twice, but it does build the automaton: the compile time of a pattern
1812    /// with the option on is higher than with it off by the time of that
1813    /// build. The automaton's NFA is at most 16 MiB, and the seek is absent
1814    /// for a pattern too large for it. Each thread that searches with the
1815    /// regex keeps a lazy DFA cache of at most 1 MiB for the seek, and one
1816    /// more for an accept seek.
1817    ///
1818    /// Use `dfa(false)` for patterns that are compiled once and searched
1819    /// rarely, or in a memory-constrained process; the results do not change
1820    /// except as the paragraph above describes.
1821    ///
1822    /// ```
1823    /// use ferroni::api::Regex;
1824    ///
1825    /// // The automaton is built with the regex (the default with the feature).
1826    /// let with = Regex::builder(r"\d{4}-\d{2}").dfa(true).build().unwrap();
1827    /// // Without it, the matcher answers every search.
1828    /// let without = Regex::builder(r"\d{4}-\d{2}").dfa(false).build().unwrap();
1829    /// assert_eq!(with.find("on 2026-10").unwrap().as_str(), "2026-10");
1830    /// assert_eq!(
1831    ///     with.find("on 2026-10").map(|m| m.range()),
1832    ///     without.find("on 2026-10").map(|m| m.range()),
1833    /// );
1834    /// ```
1835    pub fn dfa(mut self, yes: bool) -> Self {
1836        self.dfa = yes;
1837        self
1838    }
1839
1840    /// Compile the pattern into a [`Regex`].
1841    pub fn build(self) -> Result<Regex, RegexError> {
1842        let (inner, seek, accept) = onig_new_for_single_seek(
1843            &self.pattern,
1844            self.options,
1845            &ONIG_ENCODING_UTF8,
1846            self.syntax,
1847            self.optimize_backtracking,
1848            self.dfa,
1849        )?;
1850        if self.reject_backtracking_risks && !inner.backtrack_warnings.is_empty() {
1851            return Err(ONIGERR_VERY_INEFFICIENT_PATTERN.into());
1852        }
1853        Ok(Regex::from_parts(
1854            inner,
1855            self.pattern.into_boxed_slice(),
1856            seek,
1857            accept,
1858        ))
1859    }
1860}
1861
1862/// Shows the pattern, the options and the settings. `syntax` is `None` when
1863/// the syntax came from [`RegexBuilder::syntax`] and is not a built-in one.
1864impl fmt::Debug for RegexBuilder {
1865    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1866        // Kept in step with the variants of `Syntax`.
1867        const BUILT_IN: [Syntax; 12] = [
1868            Syntax::Oniguruma,
1869            Syntax::Asis,
1870            Syntax::PosixBasic,
1871            Syntax::PosixExtended,
1872            Syntax::Emacs,
1873            Syntax::Grep,
1874            Syntax::GnuRegex,
1875            Syntax::Java,
1876            Syntax::Perl,
1877            Syntax::PerlNg,
1878            Syntax::Ruby,
1879            Syntax::Python,
1880        ];
1881        let syntax = BUILT_IN
1882            .into_iter()
1883            .find(|syntax| std::ptr::eq(syntax.as_onig_syntax(), self.syntax));
1884        f.debug_struct("RegexBuilder")
1885            .field("pattern", &String::from_utf8_lossy(&self.pattern))
1886            .field("options", &self.options)
1887            .field("syntax", &syntax)
1888            .field("reject_backtracking_risks", &self.reject_backtracking_risks)
1889            .field("optimize_backtracking", &self.optimize_backtracking)
1890            .field("dfa", &self.dfa)
1891            .finish()
1892    }
1893}
1894
1895impl Regex {
1896    /// Create a [`RegexBuilder`] for a pattern given as raw bytes.
1897    ///
1898    /// The byte form of [`Regex::builder`]; see [`RegexBuilder::new_bytes`].
1899    ///
1900    /// ```
1901    /// use ferroni::api::Regex;
1902    ///
1903    /// let re = Regex::builder_bytes(br"\d+").build().unwrap();
1904    /// assert_eq!(re.find("abc 42").unwrap().as_str(), "42");
1905    /// ```
1906    pub fn builder_bytes(pattern: &[u8]) -> RegexBuilder {
1907        RegexBuilder::new_bytes(pattern)
1908    }
1909}
1910
1911/// A built-in regex syntax definition.
1912///
1913/// A typed alternative to the `OnigSyntax*` statics in [`crate::regsyntax`],
1914/// accepted by [`RegexBuilder::syntax_mode`] and used by the scanner as
1915/// [`ScannerSyntax`](crate::scanner::ScannerSyntax). The variants match the
1916/// scanner's syntax choices, which follow vscode-oniguruma's `Syntax` enum.
1917///
1918/// The default is [`Syntax::Oniguruma`]. The enum is `#[non_exhaustive]`, so a
1919/// new syntax can be added without a breaking change; a `match` outside this
1920/// crate needs a wildcard arm.
1921///
1922/// ```
1923/// use ferroni::api::{Regex, Syntax};
1924///
1925/// assert_eq!(Syntax::default(), Syntax::Oniguruma);
1926/// let re = Regex::builder(r"\w+").syntax_mode(Syntax::Ruby).build().unwrap();
1927/// assert!(re.is_match("ok"));
1928/// ```
1929#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1930#[non_exhaustive]
1931pub enum Syntax {
1932    /// Oniguruma syntax (default).
1933    #[default]
1934    Oniguruma,
1935    /// Plain text, no metacharacters.
1936    Asis,
1937    /// POSIX Basic Regular Expressions.
1938    PosixBasic,
1939    /// POSIX Extended Regular Expressions.
1940    PosixExtended,
1941    /// Emacs regex syntax.
1942    Emacs,
1943    /// grep syntax.
1944    Grep,
1945    /// GNU regex syntax.
1946    GnuRegex,
1947    /// Java regex syntax.
1948    Java,
1949    /// Perl regex syntax.
1950    Perl,
1951    /// Perl-NG regex syntax.
1952    PerlNg,
1953    /// Ruby regex syntax.
1954    Ruby,
1955    /// Python regex syntax.
1956    Python,
1957}
1958
1959impl Syntax {
1960    pub(crate) fn as_onig_syntax(&self) -> &'static OnigSyntaxType {
1961        match self {
1962            Self::Oniguruma => &OnigSyntaxOniguruma,
1963            Self::Asis => &OnigSyntaxASIS,
1964            Self::PosixBasic => &OnigSyntaxPosixBasic,
1965            Self::PosixExtended => &OnigSyntaxPosixExtended,
1966            Self::Emacs => &OnigSyntaxEmacs,
1967            Self::Grep => &OnigSyntaxGrep,
1968            Self::GnuRegex => &OnigSyntaxGnuRegex,
1969            Self::Java => &OnigSyntaxJava,
1970            Self::Perl => &OnigSyntaxPerl,
1971            Self::PerlNg => &OnigSyntaxPerl_NG,
1972            Self::Ruby => &OnigSyntaxRuby,
1973            Self::Python => &OnigSyntaxPython,
1974        }
1975    }
1976}
1977
1978// === Subject ===
1979
1980/// The text of a search as the caller passed it. A `&str` keeps its type, so
1981/// that [`Match::as_str`] slices it at the match's bounds, two character
1982/// boundary checks, instead of validating the matched bytes as UTF-8 again on
1983/// every call.
1984#[derive(Clone, Copy)]
1985enum Subject<'t> {
1986    Str(&'t str),
1987    Bytes(&'t [u8]),
1988}
1989
1990impl<'t> Subject<'t> {
1991    #[inline(always)]
1992    fn bytes(self) -> &'t [u8] {
1993        match self {
1994            Subject::Str(text) => text.as_bytes(),
1995            Subject::Bytes(text) => text,
1996        }
1997    }
1998
1999    /// Whether the text is known to be valid UTF-8.
2000    #[inline(always)]
2001    fn is_str(self) -> bool {
2002        matches!(self, Subject::Str(_))
2003    }
2004}
2005
2006// === Match ===
2007
2008/// A single match result referencing the original text.
2009#[derive(Clone, Copy)]
2010pub struct Match<'t> {
2011    subject: Subject<'t>,
2012    start: usize,
2013    end: usize,
2014}
2015
2016impl fmt::Debug for Match<'_> {
2017    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2018        // The layout a derived `Debug` printed before `Subject`.
2019        f.debug_struct("Match")
2020            .field("text", &self.subject.bytes())
2021            .field("start", &self.start)
2022            .field("end", &self.end)
2023            .finish()
2024    }
2025}
2026
2027impl<'t> Match<'t> {
2028    /// Build a match from one region entry, or `None` if the entry is not a
2029    /// range of `text`: unset (negative), past the end, or with start > end.
2030    /// Oniguruma can report start > end for a capture whose group started
2031    /// again but failed before closing; every `Match` upholds
2032    /// `start <= end <= text.len()`, so its accessors never panic on slicing.
2033    fn from_region(subject: Subject<'t>, beg: i32, end: i32) -> Option<Self> {
2034        let start = usize::try_from(beg).ok()?;
2035        let end = usize::try_from(end).ok()?;
2036        (start <= end && end <= subject.bytes().len()).then_some(Match {
2037            subject,
2038            start,
2039            end,
2040        })
2041    }
2042
2043    /// Byte offset of the start of the match.
2044    pub fn start(&self) -> usize {
2045        self.start
2046    }
2047
2048    /// Byte offset of the end of the match (exclusive).
2049    pub fn end(&self) -> usize {
2050        self.end
2051    }
2052
2053    /// Byte range of the match.
2054    pub fn range(&self) -> Range<usize> {
2055        self.start..self.end
2056    }
2057
2058    /// The matched text as a byte slice.
2059    pub fn as_bytes(&self) -> &'t [u8] {
2060        &self.subject.bytes()[self.start..self.end]
2061    }
2062
2063    /// The matched text as a `&str`.
2064    ///
2065    /// Use [`Match::as_bytes`] when the match may be invalid UTF-8; see
2066    /// [the guide](https://ferroni.dev/guide/coming-from-regex).
2067    ///
2068    /// A match of a `&str` search costs two character-boundary checks; a match
2069    /// of a `_bytes` search validates its bytes.
2070    ///
2071    /// # Panics
2072    ///
2073    /// Panics if the matched bytes are not valid UTF-8.
2074    pub fn as_str(&self) -> &'t str {
2075        match self.subject {
2076            // Valid UTF-8 already: the slice is valid exactly when both bounds
2077            // are character boundaries, which `get` checks.
2078            Subject::Str(text) => text.get(self.start..self.end),
2079            Subject::Bytes(text) => std::str::from_utf8(&text[self.start..self.end]).ok(),
2080        }
2081        .expect("match is not valid UTF-8")
2082    }
2083
2084    /// Returns the length of the match in bytes.
2085    pub fn len(&self) -> usize {
2086        self.end - self.start
2087    }
2088
2089    /// Returns `true` if the match is empty (zero-length).
2090    pub fn is_empty(&self) -> bool {
2091        self.start == self.end
2092    }
2093}
2094
2095// === Captures ===
2096
2097/// All capture groups from a single match.
2098///
2099/// Group 0 is the entire match. Groups 1..N correspond to `(...)` in the pattern.
2100///
2101/// A `Captures` borrows only the haystack, not the [`Regex`] that produced
2102/// it, so it can be returned from a function that received the regex by
2103/// reference. It keeps a shared handle to the compiled program for name
2104/// lookups, so creating one costs an atomic increment and no allocation.
2105///
2106/// # Examples
2107///
2108/// ```
2109/// use ferroni::api::Regex;
2110///
2111/// fn year<'h>(re: &Regex, text: &'h str) -> Option<&'h str> {
2112///     re.captures(text)?.name("year").map(|m| m.as_str())
2113/// }
2114///
2115/// let re = Regex::new(r"(?<year>\d{4})-\d{2}").unwrap();
2116/// assert_eq!(year(&re, "published 2026-10"), Some("2026"));
2117/// ```
2118pub struct Captures<'t> {
2119    text: Subject<'t>,
2120    region: OnigRegion,
2121    /// The compiled regex, kept only for [`Captures::name`] and so only when
2122    /// the pattern has named groups. Cloning the `Arc` for every match writes
2123    /// its shared reference count, which serializes threads that search with
2124    /// the same regex; see [`name_lookup`].
2125    compiled: Option<Arc<Compiled>>,
2126}
2127
2128/// The compiled regex a [`Captures`] keeps for name lookups: a clone of
2129/// `inner` when the pattern has named groups, `None` otherwise.
2130///
2131/// Every `Arc` clone and drop is an atomic write to one reference count. When
2132/// threads search with the same regex, each match moved that cache line
2133/// between their cores, and `captures_iter` stopped scaling at about four
2134/// threads. Patterns without named groups now leave the count alone; with
2135/// named groups the clone remains, because [`Captures::name`] needs the
2136/// compiled name table and `Captures` does not borrow the regex.
2137fn name_lookup(inner: &Arc<Compiled>) -> Option<Arc<Compiled>> {
2138    (crate::regexec::onig_number_of_names(&inner.program) > 0).then(|| Arc::clone(inner))
2139}
2140
2141impl<'t> Captures<'t> {
2142    /// Get capture group `i`, or `None` if the group did not participate.
2143    ///
2144    /// Group 0 is the entire match.
2145    ///
2146    /// Like Oniguruma, the engine can leave a capture with its start after
2147    /// its end: a group that matched once, then started again and failed
2148    /// before closing, keeps the new start and the old end (for example
2149    /// group 1 of `((?=(a|ab))a?){2}` against `"a"` is `1..0`). Such a
2150    /// capture is not a range of the text and is reported as not
2151    /// participating (`None`), here and in [`Captures::iter`] and
2152    /// [`Captures::name`]. The raw values stay available through the
2153    /// low-level [`OnigRegion`].
2154    pub fn get(&self, i: usize) -> Option<Match<'t>> {
2155        if i >= self.region.num_regs as usize {
2156            return None;
2157        }
2158        Match::from_region(self.text, self.region.beg[i], self.region.end[i])
2159    }
2160
2161    /// Get the last capture group with the given name that participated, or `None`.
2162    pub fn name(&self, name: &str) -> Option<Match<'t>> {
2163        // Without named groups there is no compiled regex to ask, and no name
2164        // it could resolve.
2165        let compiled = self.compiled.as_deref()?;
2166        let num =
2167            onig_name_to_backref_number(&compiled.program, name.as_bytes(), Some(&self.region))
2168                .ok()?;
2169        self.get(num as usize)
2170    }
2171
2172    /// Number of capture groups (including group 0).
2173    pub fn len(&self) -> usize {
2174        self.region.num_regs as usize
2175    }
2176
2177    /// Returns `true` if there are no capture groups (should never happen for a valid match).
2178    pub fn is_empty(&self) -> bool {
2179        self.region.num_regs == 0
2180    }
2181
2182    /// Iterate over all capture groups.
2183    pub fn iter(&self) -> CapturesIter<'_, 't> {
2184        CapturesIter {
2185            captures: self,
2186            index: 0,
2187        }
2188    }
2189}
2190
2191/// Returns the text of group `i`.
2192///
2193/// # Panics
2194///
2195/// Panics if the pattern has no group `i`, if group `i` did not participate
2196/// in the match, or if its bytes are not valid UTF-8 (captures from the
2197/// `_bytes` methods). [`Captures::get`] reports the first two cases as `None`.
2198///
2199/// ```
2200/// use ferroni::api::Regex;
2201///
2202/// let re = Regex::new(r"(?<year>\d{4})-(?<month>\d{2})").unwrap();
2203/// let caps = re.captures("2026-10").unwrap();
2204/// assert_eq!(&caps[1], "2026");
2205/// assert_eq!(&caps[2], "10");
2206/// ```
2207impl Index<usize> for Captures<'_> {
2208    type Output = str;
2209
2210    fn index(&self, i: usize) -> &str {
2211        match self.get(i) {
2212            Some(m) => m.as_str(),
2213            None => panic!("no group at index '{i}'"),
2214        }
2215    }
2216}
2217
2218/// Returns the text of the last participating group named `name`, as
2219/// [`Captures::name`] does.
2220///
2221/// # Panics
2222///
2223/// Panics if no group has that name, if no group with that name participated
2224/// in the match, or if its bytes are not valid UTF-8 (captures from the
2225/// `_bytes` methods). [`Captures::name`] reports these cases as `None`.
2226///
2227/// ```
2228/// use ferroni::api::Regex;
2229///
2230/// let re = Regex::new(r"(?<year>\d{4})-(?<month>\d{2})").unwrap();
2231/// let caps = re.captures("2026-10").unwrap();
2232/// assert_eq!(&caps["year"], "2026");
2233/// ```
2234impl<'n> Index<&'n str> for Captures<'_> {
2235    type Output = str;
2236
2237    fn index(&self, name: &'n str) -> &str {
2238        match self.name(name) {
2239            Some(m) => m.as_str(),
2240            None => panic!("no group named '{name}'"),
2241        }
2242    }
2243}
2244
2245impl fmt::Debug for Captures<'_> {
2246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2247        let mut list = f.debug_list();
2248        for i in 0..self.len() {
2249            list.entry(&self.get(i));
2250        }
2251        list.finish()
2252    }
2253}
2254
2255impl Drop for Captures<'_> {
2256    fn drop(&mut self) {
2257        cache_region(std::mem::take(&mut self.region));
2258    }
2259}
2260
2261// === CapturesIter ===
2262
2263/// Iterator over capture groups in a [`Captures`].
2264pub struct CapturesIter<'c, 't> {
2265    captures: &'c Captures<'t>,
2266    index: usize,
2267}
2268
2269impl<'c, 't> Iterator for CapturesIter<'c, 't> {
2270    type Item = Option<Match<'t>>;
2271
2272    fn next(&mut self) -> Option<Self::Item> {
2273        if self.index >= self.captures.len() {
2274            return None;
2275        }
2276        let m = self.captures.get(self.index);
2277        self.index += 1;
2278        Some(m)
2279    }
2280
2281    fn size_hint(&self) -> (usize, Option<usize>) {
2282        let remaining = self.captures.len() - self.index;
2283        (remaining, Some(remaining))
2284    }
2285}
2286
2287impl ExactSizeIterator for CapturesIter<'_, '_> {}
2288
2289impl fmt::Debug for CapturesIter<'_, '_> {
2290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2291        f.debug_struct("CapturesIter").finish_non_exhaustive()
2292    }
2293}
2294
2295// === CaptureNames ===
2296
2297/// Iterator over the names of the capture groups, from
2298/// [`Regex::capture_names`].
2299pub struct CaptureNames<'r> {
2300    table: Option<&'r NameTable>,
2301    next_group: i32,
2302    end_group: i32,
2303}
2304
2305impl<'r> CaptureNames<'r> {
2306    /// The name of `group`, or `None` if it has none. A name that is not
2307    /// valid UTF-8 is reported as absent, because `&str` cannot hold it.
2308    fn name_of(&self, group: i32) -> Option<&'r str> {
2309        let table = self.table?;
2310        let entry = table
2311            .entries
2312            .values()
2313            .find(|entry| entry.back_refs.contains(&group))?;
2314        std::str::from_utf8(&entry.name).ok()
2315    }
2316}
2317
2318impl<'r> Iterator for CaptureNames<'r> {
2319    type Item = Option<&'r str>;
2320
2321    fn next(&mut self) -> Option<Self::Item> {
2322        if self.next_group >= self.end_group {
2323            return None;
2324        }
2325        let group = self.next_group;
2326        self.next_group += 1;
2327        Some(self.name_of(group))
2328    }
2329
2330    fn size_hint(&self) -> (usize, Option<usize>) {
2331        let remaining = (self.end_group - self.next_group) as usize;
2332        (remaining, Some(remaining))
2333    }
2334}
2335
2336impl ExactSizeIterator for CaptureNames<'_> {}
2337
2338impl std::iter::FusedIterator for CaptureNames<'_> {}
2339
2340impl fmt::Debug for CaptureNames<'_> {
2341    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2342        f.debug_struct("CaptureNames").finish_non_exhaustive()
2343    }
2344}
2345
2346// === MatchCursor ===
2347
2348/// Where a match iterator searches next, and whether its previous match was
2349/// empty. Shared by every iterator that yields matches, so they all apply the
2350/// same empty-match rule. A `last_end` past the end of the text ends the
2351/// iteration.
2352#[derive(Clone, Copy, Debug, Default)]
2353struct MatchCursor {
2354    last_end: usize,
2355    last_was_empty: bool,
2356}
2357
2358/// What an iterator does with a match its search has found.
2359#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2360enum Verdict {
2361    /// Yield the match.
2362    Yield,
2363    /// Search again from the cursor, which moved one character on.
2364    Retry,
2365    /// End the iteration.
2366    Finish,
2367}
2368
2369impl MatchCursor {
2370    /// Apply the empty-match rule to the match `start..end` that a search from
2371    /// the cursor found. An empty match directly after another empty match is
2372    /// skipped: the cursor moves one character on, and the search runs again.
2373    /// That keeps the iteration moving without yielding the same empty match
2374    /// twice.
2375    ///
2376    /// Where the program allows it (`skip_repeated_empty_search`), the step
2377    /// past an empty match is taken with the match: the search from its end
2378    /// would only find that match again, so the iterator does not run it
2379    /// (Rust-only, ADR-008). The rule is the same either way: the next
2380    /// search starts one character on, after an empty match.
2381    #[inline]
2382    fn accept(&mut self, program: &RegexType, text: &[u8], start: usize, end: usize) -> Verdict {
2383        if start == end {
2384            if self.last_was_empty {
2385                if self.last_end >= text.len() {
2386                    return Verdict::Finish;
2387                }
2388                // Skip one character to avoid infinite loop on empty match
2389                self.last_end += program.enc.mbc_enc_len(&text[self.last_end..]);
2390                self.last_was_empty = false;
2391                return Verdict::Retry;
2392            }
2393            self.last_was_empty = true;
2394        } else {
2395            self.last_was_empty = false;
2396        }
2397
2398        self.last_end = end;
2399        if start == end && program.skip_repeated_empty_search {
2400            if end >= text.len() {
2401                // Nothing follows: the search from the end would find this
2402                // empty match again and finish the iteration.
2403                self.last_end = text.len() + 1;
2404            } else {
2405                self.last_end += program.enc.mbc_enc_len(&text[end..]);
2406                self.last_was_empty = false;
2407            }
2408        }
2409        Verdict::Yield
2410    }
2411}
2412
2413// === FindIter ===
2414
2415/// Iterator over all non-overlapping matches in a text.
2416pub struct FindIter<'r, 't> {
2417    regex: &'r Regex,
2418    /// `regex`'s program, read once so that each search does not deref it.
2419    program: &'r RegexType,
2420    text: &'t [u8],
2421    /// `text` as the caller passed it, for the matches.
2422    subject: Subject<'t>,
2423    cursor: MatchCursor,
2424    region: OnigRegion,
2425    /// Set when the searches need no region (`onig_search_bounds`).
2426    msa: Option<Box<MatchArg>>,
2427    /// The limits of each search; `None` keeps the process-wide ones.
2428    limits: Option<SearchLimits>,
2429}
2430
2431impl<'r, 't> FindIter<'r, 't> {
2432    /// The next match, or the code of a limit that a search reached.
2433    fn try_next(&mut self) -> Result<Option<Match<'t>>, i32> {
2434        loop {
2435            if self.cursor.last_end > self.text.len() {
2436                return Ok(None);
2437            }
2438            let program = self.program;
2439            let m = if let Some(msa) = self.msa.as_deref_mut() {
2440                debug_assert!(msa.region.is_none(), "bounds-only searches keep no region");
2441                msa.reset_for_iteration(program, self.cursor.last_end, self.limits);
2442                let Some((start, end)) =
2443                    onig_search_bounds(program, self.text, self.cursor.last_end, msa)?
2444                else {
2445                    return Ok(None);
2446                };
2447                if end > self.text.len() {
2448                    return Ok(None);
2449                }
2450                Match {
2451                    subject: self.subject,
2452                    start,
2453                    end,
2454                }
2455            } else {
2456                let len = self.text.len();
2457                let (result, region) = search_in_range(
2458                    program,
2459                    self.text,
2460                    len,
2461                    self.cursor.last_end,
2462                    len,
2463                    len,
2464                    Some(std::mem::take(&mut self.region)),
2465                    ONIG_OPTION_NONE,
2466                    self.limits,
2467                );
2468                let Some(region) = region else {
2469                    return Ok(None);
2470                };
2471                self.region = region;
2472
2473                if result < 0 {
2474                    return if result == ONIG_MISMATCH {
2475                        Ok(None)
2476                    } else {
2477                        Err(result)
2478                    };
2479                }
2480                if self.region.num_regs < 1 {
2481                    return Ok(None);
2482                }
2483
2484                let Some(m) =
2485                    Match::from_region(self.subject, self.region.beg[0], self.region.end[0])
2486                else {
2487                    return Ok(None);
2488                };
2489                m
2490            };
2491
2492            match self.cursor.accept(program, self.text, m.start, m.end) {
2493                Verdict::Yield => return Ok(Some(m)),
2494                Verdict::Retry => continue,
2495                Verdict::Finish => return Ok(None),
2496            }
2497        }
2498    }
2499}
2500
2501impl<'r, 't> Iterator for FindIter<'r, 't> {
2502    type Item = Match<'t>;
2503
2504    fn next(&mut self) -> Option<Match<'t>> {
2505        // A standard iteration reports a limit as the end of the matches.
2506        self.try_next().ok().flatten()
2507    }
2508}
2509
2510impl Drop for FindIter<'_, '_> {
2511    fn drop(&mut self) {
2512        match self.msa.take() {
2513            Some(msa) => cache_msa(msa),
2514            None => cache_region(std::mem::take(&mut self.region)),
2515        }
2516    }
2517}
2518
2519impl fmt::Debug for FindIter<'_, '_> {
2520    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2521        f.debug_struct("FindIter").finish_non_exhaustive()
2522    }
2523}
2524
2525/// Iterator over matches that applies [`SearchOptions`] to each search.
2526///
2527/// Created by [`Regex::find_iter_with`]. A search that reaches a limit is
2528/// yielded once as `Err` and ends the iterator.
2529pub struct TryFindIter<'r, 't> {
2530    iter: FindIter<'r, 't>,
2531    finished: bool,
2532}
2533
2534impl<'r, 't> Iterator for TryFindIter<'r, 't> {
2535    type Item = Result<Match<'t>, RegexError>;
2536
2537    fn next(&mut self) -> Option<Self::Item> {
2538        if self.finished {
2539            return None;
2540        }
2541        match self.iter.try_next() {
2542            Ok(Some(m)) => Some(Ok(m)),
2543            Ok(None) => {
2544                self.finished = true;
2545                None
2546            }
2547            Err(code) => {
2548                self.finished = true;
2549                Some(Err(RegexError::from(code)))
2550            }
2551        }
2552    }
2553}
2554
2555impl std::iter::FusedIterator for TryFindIter<'_, '_> {}
2556
2557impl fmt::Debug for TryFindIter<'_, '_> {
2558    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2559        f.debug_struct("TryFindIter").finish_non_exhaustive()
2560    }
2561}
2562
2563// === CaptureMatches ===
2564
2565/// Iterator over the capture groups of all non-overlapping matches in a text.
2566///
2567/// Created by [`Regex::captures_iter`] and [`Regex::captures_iter_bytes`]. It
2568/// yields the matches of [`FindIter`], in the same order, each as a
2569/// [`Captures`].
2570pub struct CaptureMatches<'r, 't> {
2571    regex: &'r Regex,
2572    /// `regex`'s program, read once so that each search does not deref it.
2573    program: &'r RegexType,
2574    text: &'t [u8],
2575    /// `text` as the caller passed it, for the captures.
2576    subject: Subject<'t>,
2577    cursor: MatchCursor,
2578    /// Rust-only (ADR-008): the search state of every search of this
2579    /// iterator, kept from one search to the next. Between searches it holds
2580    /// the region of the next search, which a yielded [`Captures`] takes. It
2581    /// returns to the thread cache when the iterator is dropped.
2582    msa: Option<Box<MatchArg>>,
2583    /// The limits of each search; `None` keeps the process-wide ones.
2584    limits: Option<SearchLimits>,
2585}
2586
2587impl<'r, 't> CaptureMatches<'r, 't> {
2588    fn new(regex: &'r Regex, subject: Subject<'t>, limits: Option<SearchLimits>) -> Self {
2589        let program = regex.program();
2590        CaptureMatches {
2591            regex,
2592            program,
2593            text: subject.bytes(),
2594            subject,
2595            cursor: MatchCursor::default(),
2596            msa: Some(take_cached_msa(program, ONIG_OPTION_NONE, None, 0, limits)),
2597            limits,
2598        }
2599    }
2600
2601    /// The region for the next search. A yielded [`Captures`] takes this
2602    /// iterator's region, and returns it to the thread cache when it is
2603    /// dropped, so the search takes the cached region then, instead of
2604    /// allocating a new one (Rust-only, ADR-008).
2605    fn next_region(&mut self) -> OnigRegion {
2606        let region = self
2607            .msa
2608            .as_deref_mut()
2609            .and_then(|msa| msa.region.take())
2610            .unwrap_or_default();
2611        if region.beg.capacity() == 0 {
2612            take_cached_region()
2613        } else {
2614            region
2615        }
2616    }
2617
2618    /// The next match with its captures, or the code of a limit that a search
2619    /// reached.
2620    fn try_next(&mut self) -> Result<Option<Captures<'t>>, i32> {
2621        loop {
2622            if self.cursor.last_end > self.text.len() {
2623                return Ok(None);
2624            }
2625
2626            let program = self.program;
2627            let len = self.text.len();
2628            let start = self.cursor.last_end;
2629            let region = self.next_region();
2630            let Some(msa) = self.msa.as_deref_mut() else {
2631                return Ok(None);
2632            };
2633            // The same search as `search_in_range` runs with a cached
2634            // `MatchArg`, which this iterator keeps instead (Rust-only, ADR-008).
2635            msa.reset_for_iteration(program, start, self.limits);
2636            msa.region = Some(region);
2637            let result = search_in_range_inner(program, self.text, len, start, len, len, msa);
2638            let region = msa.region.take();
2639            release_oversized_stack(msa);
2640            let Some(region) = region else {
2641                return Ok(None);
2642            };
2643            if result < 0 {
2644                msa.region = Some(region);
2645                return if result == ONIG_MISMATCH {
2646                    Ok(None)
2647                } else {
2648                    Err(result)
2649                };
2650            }
2651            if region.num_regs < 1 {
2652                msa.region = Some(region);
2653                return Ok(None);
2654            }
2655            let Some(m) = Match::from_region(self.subject, region.beg[0], region.end[0]) else {
2656                msa.region = Some(region);
2657                return Ok(None);
2658            };
2659
2660            match self.cursor.accept(program, self.text, m.start, m.end) {
2661                Verdict::Yield => {
2662                    return Ok(Some(Captures {
2663                        text: self.subject,
2664                        region,
2665                        compiled: name_lookup(&self.regex.inner),
2666                    }));
2667                }
2668                Verdict::Retry => msa.region = Some(region),
2669                Verdict::Finish => {
2670                    msa.region = Some(region);
2671                    return Ok(None);
2672                }
2673            }
2674        }
2675    }
2676}
2677
2678impl<'r, 't> Iterator for CaptureMatches<'r, 't> {
2679    type Item = Captures<'t>;
2680
2681    fn next(&mut self) -> Option<Captures<'t>> {
2682        // A standard iteration reports a limit as the end of the matches.
2683        self.try_next().ok().flatten()
2684    }
2685}
2686
2687impl Drop for CaptureMatches<'_, '_> {
2688    fn drop(&mut self) {
2689        if let Some(mut msa) = self.msa.take() {
2690            cache_region(msa.region.take().unwrap_or_default());
2691            cache_msa(msa);
2692        }
2693    }
2694}
2695
2696impl fmt::Debug for CaptureMatches<'_, '_> {
2697    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2698        f.debug_struct("CaptureMatches").finish_non_exhaustive()
2699    }
2700}
2701
2702/// Iterator over the capture groups of matches that applies [`SearchOptions`]
2703/// to each search.
2704///
2705/// Created by [`Regex::captures_iter_with`]. A search that reaches a limit is
2706/// yielded once as `Err` and ends the iterator.
2707pub struct TryCaptureMatches<'r, 't> {
2708    iter: CaptureMatches<'r, 't>,
2709    finished: bool,
2710}
2711
2712impl<'r, 't> Iterator for TryCaptureMatches<'r, 't> {
2713    type Item = Result<Captures<'t>, RegexError>;
2714
2715    fn next(&mut self) -> Option<Self::Item> {
2716        if self.finished {
2717            return None;
2718        }
2719        match self.iter.try_next() {
2720            Ok(Some(captures)) => Some(Ok(captures)),
2721            Ok(None) => {
2722                self.finished = true;
2723                None
2724            }
2725            Err(code) => {
2726                self.finished = true;
2727                Some(Err(RegexError::from(code)))
2728            }
2729        }
2730    }
2731}
2732
2733impl std::iter::FusedIterator for TryCaptureMatches<'_, '_> {}
2734
2735impl fmt::Debug for TryCaptureMatches<'_, '_> {
2736    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2737        f.debug_struct("TryCaptureMatches").finish_non_exhaustive()
2738    }
2739}
2740
2741#[cfg(test)]
2742mod tests {
2743    use super::*;
2744
2745    fn assert_send_sync<T: Send + Sync>() {}
2746
2747    fn clear_cached_region() {
2748        CACHED_REGION.with(|cached| *cached.borrow_mut() = None);
2749    }
2750
2751    fn cached_region_buffer() -> Option<(*const i32, usize)> {
2752        CACHED_REGION.with(|cached| {
2753            cached
2754                .borrow()
2755                .as_ref()
2756                .map(|region| (region.beg.as_ptr(), region.beg.capacity()))
2757        })
2758    }
2759
2760    /// The seek automaton is built with the regex, once per compilation and
2761    /// shared by its clones, for a pattern with a seek that can reject; a
2762    /// pure literal and a seek that matches everywhere have none. The option
2763    /// turns it off.
2764    #[cfg(feature = "dfa-prefilter")]
2765    #[test]
2766    fn seek_is_built_with_the_regex() {
2767        let _limits = crate::regexec::shared_limits();
2768        let re = Regex::new(r"^[0-9A-F]{8}$").unwrap();
2769        assert!(re.seek_present());
2770        assert_eq!(re.seek_exact(), Some(true));
2771        assert!(re.clone().seek_present());
2772        assert!(!re.is_match("zzzz"));
2773
2774        // Case folding that reaches non-ASCII characters (the Kelvin sign for
2775        // `k`) is listed exactly, so the seek stays exact.
2776        let folded = RegexBuilder::new("^k+$")
2777            .case_insensitive(true)
2778            .build()
2779            .unwrap();
2780        assert_eq!(folded.seek_exact(), Some(true));
2781        assert!(!folded.is_match("xxx"));
2782        assert!(folded.is_match("\u{212A}k"));
2783
2784        // A pure literal is answered by its own search, and a seek that
2785        // matches everywhere rejects nothing.
2786        for pattern in ["GET", "a*"] {
2787            let re = Regex::new(pattern).unwrap();
2788            assert!(!re.seek_present(), "{pattern}");
2789            assert!(re.is_match("GET a"), "{pattern}");
2790        }
2791
2792        let off = RegexBuilder::new(r"^[0-9A-F]{8}$")
2793            .dfa(false)
2794            .build()
2795            .unwrap();
2796        assert!(!off.seek_present());
2797        assert!(off.is_match("0123ABCD"));
2798    }
2799
2800    /// An exact seek answers "match" for unbounded repetitions, which have no
2801    /// static retry bound: the seek decides, so the matcher is not run, and
2802    /// its answer is the matcher's without limits. A rejection holds as well.
2803    #[cfg(feature = "dfa-prefilter")]
2804    #[test]
2805    fn exact_seek_answers_match_for_an_unbounded_repetition() {
2806        let _limits = crate::regexec::shared_limits();
2807        let pattern = r"^[a-z@.]+@[a-z.]+\.com$";
2808        let re = Regex::new(pattern).unwrap();
2809        let matcher = RegexBuilder::new(pattern).dfa(false).build().unwrap();
2810        assert_eq!(re.seek_exact(), Some(true));
2811        assert_eq!(re.seek_decides_is_match("john@doe.com"), Some(true));
2812        assert_eq!(re.seek_decides_is_match("john@doe.org"), Some(false));
2813        assert!(re.is_match("john@doe.com"));
2814        assert!(matcher.is_match("john@doe.com"));
2815        assert!(!re.is_match("john@doe.org"));
2816    }
2817
2818    /// The repeats `tune_next` makes possessive keep the language of the
2819    /// repeat, so the seek of `^[a-z]+@[a-z]+\.com$` is exact and answers
2820    /// "match" for it. A user-written atomic group next to them is not: its
2821    /// seek stays inexact and answers "match" only from the matcher.
2822    #[cfg(feature = "dfa-prefilter")]
2823    #[test]
2824    fn automatic_possessive_repeats_keep_the_seek_exact() {
2825        let _limits = crate::regexec::shared_limits();
2826        let re = Regex::new(r"^[a-z]+@[a-z]+\.com$").unwrap();
2827        assert_eq!(re.seek_exact(), Some(true));
2828        assert_eq!(re.seek_decides_is_match("john@doe.com"), Some(true));
2829        assert_eq!(re.seek_decides_is_match("john@doe.org"), Some(false));
2830        assert!(re.is_match("john@doe.com"));
2831        assert!(!re.is_match("john@doe.org"));
2832
2833        let atomic = Regex::new(r"^[a-z]+@(?>[a-z]+)\.com$").unwrap();
2834        assert_eq!(atomic.seek_exact(), Some(false));
2835        assert_eq!(atomic.seek_decides_is_match("john@doe.com"), None);
2836        assert_eq!(atomic.seek_decides_is_match("john@doe.org"), Some(false));
2837        assert!(atomic.is_match("john@doe.com"));
2838    }
2839
2840    /// Where a tiny process-wide retry limit stops the matcher, an exact seek
2841    /// still answers "match", and the answer is the matcher's without the
2842    /// limit (ADR-017, rule 2). The limit is restored when the guard drops.
2843    #[cfg(feature = "dfa-prefilter")]
2844    #[test]
2845    fn exact_seek_answers_where_a_tiny_retry_limit_stops_the_matcher() {
2846        let _limits = crate::regexec::exclusive_limits();
2847        let re = Regex::new(r"^[a-z]+ab\z").unwrap();
2848        let text = "xaaab";
2849        assert_eq!(re.seek_exact(), Some(true));
2850        crate::regexec::onig_set_retry_limit_in_match(1);
2851        // The premise: under the limit the matcher stops on this subject.
2852        assert!(
2853            re.is_match_bytes_at_with(text.as_bytes(), 0, SearchOptions::new())
2854                .is_err()
2855        );
2856        assert_eq!(re.seek_decides_is_match(text), Some(true));
2857        assert!(re.is_match(text));
2858        // The matcher without the limit finds the same match.
2859        crate::regexec::onig_set_retry_limit_in_match(0);
2860        assert!(
2861            re.is_match_bytes_at_with(text.as_bytes(), 0, SearchOptions::new())
2862                .unwrap()
2863        );
2864        // A subject without the match stays a rejection under the limit.
2865        crate::regexec::onig_set_retry_limit_in_match(1);
2866        assert!(!re.is_match("xaaac"));
2867    }
2868
2869    /// The catalog's validators have exact seeks, so a standard `is_match`
2870    /// answers their matches from the seek. The django local part has
2871    /// unbounded repeats, and the IPv6 validator a zone suffix with no bound.
2872    #[cfg(feature = "dfa-prefilter")]
2873    #[test]
2874    fn catalog_validators_have_exact_seeks() {
2875        let _limits = crate::regexec::shared_limits();
2876        let cases: &[(&str, &str, bool, &str)] = &[
2877            (
2878                "uuid_shape",
2879                "^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$",
2880                true,
2881                "0123ABCD-0123-0123-0123-0123456789AB",
2882            ),
2883            (
2884                "uuid_v4",
2885                "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$",
2886                true,
2887                "0123ABCD-0123-4123-8123-0123456789AB",
2888            ),
2889            (
2890                "ipv4_validation",
2891                "^((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$",
2892                false,
2893                "192.168.0.1",
2894            ),
2895            (
2896                "ipv6_validation",
2897                "^((?:(?:[0-9a-fA-F]{1,4}):){7}(?:(?:[0-9a-fA-F]{1,4})|:)|(?:(?:[0-9a-fA-F]{1,4}):){6}(?:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|:(?:[0-9a-fA-F]{1,4})|:)|(?:(?:[0-9a-fA-F]{1,4}):){5}(?::((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,2}|:)|(?:(?:[0-9a-fA-F]{1,4}):){4}(?:(:(?:[0-9a-fA-F]{1,4})){0,1}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,3}|:)|(?:(?:[0-9a-fA-F]{1,4}):){3}(?:(:(?:[0-9a-fA-F]{1,4})){0,2}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,4}|:)|(?:(?:[0-9a-fA-F]{1,4}):){2}(?:(:(?:[0-9a-fA-F]{1,4})){0,3}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,5}|:)|(?:(?:[0-9a-fA-F]{1,4}):){1}(?:(:(?:[0-9a-fA-F]{1,4})){0,4}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,6}|:)|(?::((?::(?:[0-9a-fA-F]{1,4})){0,5}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(?::(?:[0-9a-fA-F]{1,4})){1,7}|:)))(%[0-9a-zA-Z.]{1,})?$",
2898                false,
2899                "fe80::1",
2900            ),
2901            (
2902                "django_email_local_part",
2903                "(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*\\z|^\"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*\"\\z)",
2904                true,
2905                "john.doe",
2906            ),
2907        ];
2908        for &(name, pattern, ignore_case, matching) in cases {
2909            let re = RegexBuilder::new(pattern)
2910                .case_insensitive(ignore_case)
2911                .multi_line_anchors(false)
2912                .build()
2913                .unwrap();
2914            assert_eq!(re.seek_exact(), Some(true), "{name}");
2915            assert_eq!(re.seek_decides_is_match(matching), Some(true), "{name}");
2916        }
2917    }
2918
2919    /// Text the seek reads as UTF-8 and the matcher may read otherwise is
2920    /// left to the matcher: an invalid byte or a start inside a character.
2921    #[cfg(feature = "dfa-prefilter")]
2922    #[test]
2923    fn seek_path_leaves_invalid_utf8_and_inner_starts_to_the_matcher() {
2924        let _limits = crate::regexec::shared_limits();
2925        let re = Regex::new(r"[^a]").unwrap();
2926        let text = "é".as_bytes();
2927        for start in 0..=text.len() {
2928            let expected = re
2929                .is_match_bytes_at_with(text, start, SearchOptions::new())
2930                .unwrap();
2931            assert_eq!(re.is_match_bytes_at(text, start), expected, "start {start}");
2932        }
2933        let invalid = [0xFF_u8];
2934        let expected = re
2935            .is_match_bytes_at_with(&invalid, 0, SearchOptions::new())
2936            .unwrap();
2937        assert_eq!(re.is_match_bytes(&invalid), expected);
2938        assert_eq!(re.find_bytes(&invalid).is_some(), expected);
2939    }
2940
2941    /// The bounds of every group of a capture result, `None` for no match.
2942    fn capture_spans(found: Option<Captures<'_>>) -> Option<Vec<Option<Range<usize>>>> {
2943        found.map(|caps| {
2944            (0..caps.len())
2945                .map(|group| caps.get(group).map(|m| m.range()))
2946                .collect()
2947        })
2948    }
2949
2950    /// The capture methods with the seek gate forced on, forced off and left
2951    /// adaptive give the matcher's answer (`captures_*_with`, which never
2952    /// reaches the seek), at every character boundary and for every method.
2953    #[cfg(feature = "dfa-prefilter")]
2954    #[test]
2955    fn capture_gate_never_changes_a_capture_result() {
2956        let _limits = crate::regexec::shared_limits();
2957        let patterns = [
2958            r#"\A(?:('[^'\\]*(?:\\.[^'\\]*)*')|("[^"\\]*(?:\\.[^"\\]*)*"))"#,
2959            r"(\d+)-(\d+)",
2960            r"x(?=y)(.)",
2961            r"\Gab(c)?",
2962            r"^(\w+)\s*=\s*(\S*)$",
2963            r"(?<=é)a(b)?",
2964            r"\A(a|b)\1",
2965            r"(?i)\bfoo\b(.)",
2966        ];
2967        let texts = [
2968            "",
2969            "x",
2970            "ab",
2971            "abc",
2972            "'it''s'",
2973            "\"q\\\"\"",
2974            "12-34 x",
2975            "xy",
2976            "é",
2977            "éab",
2978            "Foo foo",
2979            "a=b",
2980            "bb",
2981            "a\u{1F600}b",
2982        ];
2983        for pattern in patterns {
2984            let re = Regex::new(pattern).unwrap();
2985            for text in texts {
2986                for start in (0..=text.len()).filter(|&at| text.is_char_boundary(at)) {
2987                    let expected = re
2988                        .captures_at_with(text, start, SearchOptions::new())
2989                        .unwrap();
2990                    let expected = capture_spans(expected);
2991                    let expected_find = re
2992                        .find_at_with(text, start, SearchOptions::new())
2993                        .unwrap()
2994                        .map(|found| found.range());
2995                    for mode in [Some(true), Some(false), None] {
2996                        re.seek_gate_force(mode);
2997                        assert_eq!(
2998                            capture_spans(re.captures_at(text, start)),
2999                            expected,
3000                            "{pattern} on {text:?} from {start}, gate {mode:?}"
3001                        );
3002                        assert_eq!(
3003                            re.find_at(text, start).map(|found| found.range()),
3004                            expected_find,
3005                            "find: {pattern} on {text:?} from {start}, gate {mode:?}"
3006                        );
3007                    }
3008                    re.seek_gate_force(None);
3009                }
3010                // The other entry points of the capture family.
3011                let expected = capture_spans(re.captures_with(text, SearchOptions::new()).unwrap());
3012                let expected_bytes = capture_spans(re.captures_bytes(text.as_bytes()));
3013                assert_eq!(expected_bytes, expected, "{pattern} on {text:?}");
3014                assert_eq!(
3015                    capture_spans(re.captures(text)),
3016                    expected,
3017                    "{pattern} on {text:?}"
3018                );
3019            }
3020        }
3021    }
3022
3023    /// The capture methods of a pattern that matches only at its start (`\A`,
3024    /// `\G`, `^`) give the matcher's answer of `captures_*_with`, at every
3025    /// character boundary, through the direct path of `onig_search_captures`.
3026    /// The patterns share the thread's cached region, one with groups and one
3027    /// without, so a region reused from another pattern cannot leak into a result.
3028    #[test]
3029    fn anchored_captures_match_the_general_path_through_the_api() {
3030        let _limits = crate::regexec::shared_limits();
3031        let patterns = [
3032            r"\A(\d+)-(\w+)?",
3033            r"\A",
3034            r"\Gab(c)?",
3035            r"^(\w+)\s*=\s*(\S*)$",
3036            r"\A(?:(a)|b)",
3037            r"(\d+)",
3038            r"\Ab",
3039        ];
3040        let texts = ["", "12-ab", "a=b", "ab", "abc", "bb", "é", "x\nab", "abab"];
3041        for text in texts {
3042            for pattern in patterns {
3043                let re = Regex::new(pattern).unwrap();
3044                for start in (0..=text.len()).filter(|&at| text.is_char_boundary(at)) {
3045                    let expected = capture_spans(
3046                        re.captures_at_with(text, start, SearchOptions::new())
3047                            .unwrap(),
3048                    );
3049                    assert_eq!(
3050                        capture_spans(re.captures_at(text, start)),
3051                        expected,
3052                        "{pattern} on {text:?} from {start}"
3053                    );
3054                    assert_eq!(
3055                        capture_spans(re.captures_bytes_at(text.as_bytes(), start)),
3056                        expected,
3057                        "{pattern} on {text:?} from {start} (bytes)"
3058                    );
3059                }
3060                let expected = capture_spans(re.captures_with(text, SearchOptions::new()).unwrap());
3061                assert_eq!(
3062                    capture_spans(re.captures(text)),
3063                    expected,
3064                    "{pattern} on {text:?}"
3065                );
3066            }
3067        }
3068    }
3069
3070    /// The adaptive gate changes state with the rejections it measures: a
3071    /// pattern the seek rejects keeps the gate open, one whose seek finds a
3072    /// match closes it, and the state of a clone is the original's.
3073    #[cfg(feature = "dfa-prefilter")]
3074    #[test]
3075    fn capture_gate_follows_the_rejection_rate() {
3076        let _limits = crate::regexec::shared_limits();
3077        let rejecting = Regex::new(r"\Aabc(d)").unwrap();
3078        for _ in 0..32 {
3079            assert!(rejecting.captures("xyz").is_none());
3080        }
3081        let (open, rate) = rejecting.seek_gate_state();
3082        assert!(open, "rate {rate}");
3083        assert!(rate >= SEEK_GATE_OPEN_PERMILLE, "rate {rate}");
3084
3085        // A pure literal has no seek, so the accepting pattern is not one.
3086        let accepting = Regex::new(r"x\d").unwrap();
3087        for _ in 0..32 {
3088            assert!(accepting.captures("x1").is_some());
3089        }
3090        let (open, rate) = accepting.seek_gate_state();
3091        assert!(!open, "rate {rate}");
3092        assert!(rate < SEEK_GATE_CLOSE_PERMILLE, "rate {rate}");
3093        // A closed gate still measures: its probes run the seek, and rejections
3094        // among them reopen it.
3095        for _ in 0..SEEK_GATE_PROBE_INTERVAL * 16 {
3096            assert!(accepting.captures("y").is_none());
3097        }
3098        let (open, rate) = accepting.seek_gate_state();
3099        assert!(open, "rate {rate}");
3100
3101        let original = Regex::new(r"\Aabc").unwrap();
3102        let clone = original.clone();
3103        let before = original.seek_gate_state();
3104        assert!(clone.captures("xyz").is_none());
3105        assert_ne!(original.seek_gate_state(), before);
3106        assert_eq!(original.seek_gate_state(), clone.seek_gate_state());
3107    }
3108
3109    /// Clones of a regex share its gate and seek: captures on several threads
3110    /// give the matcher's answers, and nothing panics or deadlocks.
3111    #[cfg(feature = "dfa-prefilter")]
3112    #[test]
3113    fn capture_gate_is_shared_and_thread_safe() {
3114        let _limits = crate::regexec::shared_limits();
3115        let re = Regex::new(r"\Aabc(d)?").unwrap();
3116        std::thread::scope(|scope| {
3117            for thread in 0..4 {
3118                let re = re.clone();
3119                scope.spawn(move || {
3120                    for round in 0..500 {
3121                        let (text, matches) = if (round + thread) % 3 == 0 {
3122                            ("abcd", true)
3123                        } else {
3124                            ("xyz", false)
3125                        };
3126                        let found = re.captures(text);
3127                        assert_eq!(found.is_some(), matches, "{text}");
3128                        let expected = re
3129                            .captures_with(text, SearchOptions::new())
3130                            .unwrap()
3131                            .is_some();
3132                        assert_eq!(found.is_some(), expected, "{text}");
3133                    }
3134                });
3135            }
3136        });
3137        assert_eq!(re.seek_gate_state(), re.clone().seek_gate_state());
3138    }
3139
3140    /// The seek's automaton is built by the first search that consults it,
3141    /// not by the builder: iterating and the `*_with` methods never build
3142    /// it, the first standard search does, and a clone shares it.
3143    #[cfg(feature = "dfa-prefilter")]
3144    #[test]
3145    fn seek_automaton_is_built_by_the_first_search_that_consults_it() {
3146        let _limits = crate::regexec::shared_limits();
3147        let pattern = r"\b[a-z]+ing\b";
3148        let text = "nothing here but sing and ring";
3149        let re = Regex::new(pattern).unwrap();
3150        assert!(!re.seek_built());
3151        assert_eq!(re.find_iter(text).count(), 3);
3152        assert!(
3153            re.captures_with(text, SearchOptions::new())
3154                .unwrap()
3155                .is_some()
3156        );
3157        assert!(!re.seek_built());
3158        let clone = re.clone();
3159        assert!(re.is_match(text));
3160        assert!(re.seek_built() && clone.seek_built());
3161
3162        // A pure literal has no automaton to build, and one that does not
3163        // build stays unbuilt.
3164        let literal = Regex::new("sing").unwrap();
3165        assert!(literal.is_match(text));
3166        assert!(!literal.seek_built() && !literal.seek_present());
3167        let unseekable = RegexBuilder::new(pattern).dfa(false).build().unwrap();
3168        assert!(unseekable.is_match(text));
3169        assert!(!unseekable.seek_built());
3170    }
3171
3172    /// Threads that race to a regex's first search get the one automaton and
3173    /// the matcher's answers.
3174    #[cfg(feature = "dfa-prefilter")]
3175    #[test]
3176    fn seek_automaton_is_built_once_for_racing_threads() {
3177        let _limits = crate::regexec::shared_limits();
3178        let re = Regex::new(r"\b[a-z]+ing\b").unwrap();
3179        std::thread::scope(|scope| {
3180            for _ in 0..4 {
3181                let re = re.clone();
3182                scope.spawn(move || {
3183                    assert!(re.is_match("nothing here but sing and ring"));
3184                    assert!(!re.is_match("no match here"));
3185                });
3186            }
3187        });
3188        assert!(re.seek_built());
3189    }
3190
3191    /// A match of a `&str` search slices the text in `as_str` instead of
3192    /// validating the bytes; every route must give what validation gives, and
3193    /// `Debug` keeps the layout it had before matches knew their subject.
3194    #[test]
3195    fn str_matches_slice_what_validation_returns() {
3196        let _limits = crate::regexec::shared_limits();
3197        let re = Regex::new(r"(\w+)(?:é|ü)?").unwrap();
3198        let text = "Grüße café x 12 naïve";
3199        let validated = |m: Match<'_>| std::str::from_utf8(m.as_bytes()).unwrap().to_owned();
3200        for m in re.find_iter(text) {
3201            assert_eq!(m.as_str(), validated(m));
3202        }
3203        for caps in re.captures_iter(text) {
3204            for m in caps.iter().flatten() {
3205                assert_eq!(m.as_str(), validated(m));
3206            }
3207        }
3208        let m = re.find_at(text, 7).unwrap();
3209        assert_eq!(m.as_str(), validated(m));
3210        let caps = re
3211            .captures_at_with(text, 7, SearchOptions::new())
3212            .unwrap()
3213            .unwrap();
3214        assert_eq!(&caps[1], validated(caps.get(1).unwrap()));
3215        let bytes = re.find_bytes(text.as_bytes()).unwrap();
3216        assert_eq!(bytes.as_str(), "Grüße");
3217        assert_eq!(
3218            format!("{:?}", Regex::new("ab").unwrap().find("xab").unwrap()),
3219            "Match { text: [120, 97, 98], start: 1, end: 3 }"
3220        );
3221    }
3222
3223    /// Captures keep the compiled regex only to resolve names. A pattern
3224    /// without named groups must not touch the shared reference count per
3225    /// match: that write serialized threads iterating with the same regex.
3226    #[test]
3227    fn captures_hold_the_regex_only_for_named_groups() {
3228        let _limits = crate::regexec::shared_limits();
3229        let unnamed = Regex::new(r"<(\w+)>").unwrap();
3230        let held: Vec<Captures<'_>> = unnamed.captures_iter("<a><b><c>").collect();
3231        assert_eq!(held.len(), 3);
3232        assert_eq!(Arc::strong_count(&unnamed.inner), 1);
3233        let single = unnamed.captures("<d>").unwrap();
3234        assert_eq!(Arc::strong_count(&unnamed.inner), 1);
3235        assert!(single.name("tag").is_none());
3236        assert_eq!(&single[1], "d");
3237
3238        let named = Regex::new(r"<(?<tag>\w+)>").unwrap();
3239        let held: Vec<Captures<'_>> = named.captures_iter("<a><b>").collect();
3240        assert_eq!(Arc::strong_count(&named.inner), 3);
3241        let tags: Vec<&str> = held
3242            .iter()
3243            .map(|caps| caps.name("tag").unwrap().as_str())
3244            .collect();
3245        assert_eq!(tags, ["a", "b"]);
3246        drop(held);
3247        assert_eq!(Arc::strong_count(&named.inner), 1);
3248    }
3249
3250    #[test]
3251    fn compiled_regex_types_are_send_and_sync() {
3252        assert_send_sync::<Regex>();
3253        assert_send_sync::<crate::regset::OnigRegSet>();
3254        assert_send_sync::<crate::scanner::Scanner>();
3255    }
3256
3257    #[test]
3258    fn public_types_implement_debug() {
3259        use crate::scanner::{OnigString, Scanner, ScannerConfig, ScannerPatternCache};
3260        let _limits = crate::regexec::shared_limits();
3261
3262        let re = Regex::new(r"(a)(?<n>b)").unwrap();
3263        let text = "ab ab";
3264        let captures = re.captures(text).unwrap();
3265        let config = ScannerConfig::default();
3266        let mut cache = ScannerPatternCache::new();
3267        let _top =
3268            Scanner::with_pattern_cache(&[r"\bfn\b", r#""[^"]*""#], &config, &mut cache).unwrap();
3269        let _call =
3270            Scanner::with_pattern_cache(&[r"\)", r#""[^"]*""#], &config, &mut cache).unwrap();
3271        let scanner = Scanner::new(&[r"\d+", "[a-z]+"]).unwrap();
3272        let custom_syntax: &'static OnigSyntaxType =
3273            Box::leak(Box::new(crate::regsyntax::OnigSyntaxRuby.clone()));
3274
3275        let shown: [(String, &str); 15] = [
3276            (
3277                format!("{:?}", Regex::builder("a+").syntax_mode(Syntax::Ruby)),
3278                "pattern: \"a+\", options:",
3279            ),
3280            (
3281                format!("{:?}", Regex::builder("a+").syntax(custom_syntax)),
3282                "syntax: None",
3283            ),
3284            (format!("{:?}", captures.iter()), "CapturesIter { .. }"),
3285            (format!("{:?}", re.capture_names()), "CaptureNames { .. }"),
3286            (format!("{:?}", re.find_iter(text)), "FindIter { .. }"),
3287            (
3288                format!("{:?}", re.find_iter_with(text, SearchOptions::new())),
3289                "TryFindIter { .. }",
3290            ),
3291            (
3292                format!("{:?}", re.captures_iter(text)),
3293                "CaptureMatches { .. }",
3294            ),
3295            (
3296                format!("{:?}", re.captures_iter_with(text, SearchOptions::new())),
3297                "TryCaptureMatches { .. }",
3298            ),
3299            (format!("{:?}", re.split(text)), "Split { .. }"),
3300            (format!("{:?}", re.splitn(text, 2)), "SplitN { .. }"),
3301            (
3302                format!("{:?}", re.split_bytes(text.as_bytes())),
3303                "SplitBytes { .. }",
3304            ),
3305            (
3306                format!("{:?}", re.splitn_bytes(text.as_bytes(), 2)),
3307                "SplitNBytes { .. }",
3308            ),
3309            (format!("{scanner:?}"), "Scanner { patterns: 2,"),
3310            (format!("{cache:?}"), "ScannerPatternCache { len: 3, .. }"),
3311            (
3312                format!("{:?}", OnigString::new("a💻b")),
3313                "OnigString { content: \"a💻b\", .. }",
3314            ),
3315        ];
3316        for (shown, expected) in &shown {
3317            assert!(
3318                shown.contains(*expected),
3319                "{shown} does not contain {expected}"
3320            );
3321        }
3322    }
3323
3324    #[test]
3325    fn find_reuses_the_thread_local_region_buffer() {
3326        let _limits = crate::regexec::shared_limits();
3327        clear_cached_region();
3328        // `\K` keeps `find` on the region path.
3329        let re = Regex::new(r"(a)(b)\K(c)").unwrap();
3330
3331        assert_eq!(re.find("abc").unwrap().as_str(), "c");
3332        let first = cached_region_buffer().expect("find should return its region to the cache");
3333        assert!(first.1 >= 4);
3334
3335        assert_eq!(re.find("abc").unwrap().as_str(), "c");
3336        let second = cached_region_buffer().expect("find should preserve the cached region");
3337        assert_eq!(second, first);
3338    }
3339
3340    #[test]
3341    fn captures_returns_its_region_buffer_when_dropped() {
3342        let _limits = crate::regexec::shared_limits();
3343        clear_cached_region();
3344        let re = Regex::new(r"(a)(b)(c)").unwrap();
3345        let captures = re.captures("abc").unwrap();
3346        let buffer = captures.region.beg.as_ptr();
3347
3348        assert!(cached_region_buffer().is_none());
3349        drop(captures);
3350
3351        let cached = cached_region_buffer().expect("drop should return the captures region");
3352        assert_eq!(cached.0, buffer);
3353        assert!(cached.1 >= 4);
3354    }
3355
3356    #[test]
3357    fn result_drop_during_thread_local_teardown_does_not_panic() {
3358        let _limits = crate::regexec::shared_limits();
3359        thread_local! {
3360            static LATE_RESULT: RefCell<Option<Captures<'static>>> = const { RefCell::new(None) };
3361        }
3362
3363        std::thread::spawn(|| {
3364            // Initialize the result holder before CACHED_REGION so it is
3365            // destroyed afterwards and drops Captures with the cache gone.
3366            LATE_RESULT.with(|result| assert!(result.borrow().is_none()));
3367            let regex = Box::leak(Box::new(Regex::new(r"(a)").unwrap()));
3368            let captures = regex.captures("a").unwrap();
3369            LATE_RESULT.with(|result| *result.borrow_mut() = Some(captures));
3370        })
3371        .join()
3372        .expect("result drop during thread teardown must not panic");
3373    }
3374
3375    #[test]
3376    fn find_iter_reuses_one_region_for_every_step() {
3377        let _limits = crate::regexec::shared_limits();
3378        clear_cached_region();
3379        // `\K` moves the match start away from the attempt, so only the
3380        // region carries the match.
3381        let re = Regex::new(r"(\w)\K\w+").unwrap();
3382        let mut matches = re.find_iter("one two");
3383        assert!(matches.msa.is_none());
3384
3385        assert_eq!(matches.next().unwrap().as_str(), "ne");
3386        let first = matches.region.beg.as_ptr();
3387        assert_eq!(matches.next().unwrap().as_str(), "wo");
3388        assert_eq!(matches.region.beg.as_ptr(), first);
3389        drop(matches);
3390
3391        let cached = cached_region_buffer().expect("iterator drop should return its region");
3392        assert_eq!(cached.0, first);
3393    }
3394
3395    #[test]
3396    fn find_iter_reuses_one_match_arg_without_a_region() {
3397        let _limits = crate::regexec::shared_limits();
3398        let re = Regex::new(r"(\w+)").unwrap();
3399        let mut matches = re.find_iter("one two");
3400        let msa: *const MatchArg = &**matches.msa.as_ref().expect("bounds-only iterator");
3401        assert_eq!(matches.next().unwrap().as_str(), "one");
3402        assert_eq!(matches.next().unwrap().as_str(), "two");
3403        assert!(matches.next().is_none());
3404        assert!(std::ptr::eq(&**matches.msa.as_ref().unwrap(), msa));
3405        assert!(matches.region.beg.is_empty(), "no region was requested");
3406    }
3407
3408    /// The bounds-only iterator yields what the region-based one yields.
3409    #[test]
3410    fn find_iter_bounds_match_the_region_path() {
3411        let _limits = crate::regexec::shared_limits();
3412        let patterns = [
3413            r"\d+",
3414            r"(\w+)",
3415            r"(a)|b",
3416            r"\b(\w+)\s+\1\b",
3417            r"(?<=@)\w+",
3418            r"\w+(?=\()",
3419            r"x*",
3420            r"",
3421            r"^",
3422            r"$",
3423            r"(?m)^\s*$",
3424            r"alpha|beta|gamma|delta",
3425            r"(alpha|beta|gamma|delta)",
3426            r"(?i)(?:error|warn|fatal|panic)",
3427            r"\b(?:if|else|for|while)\b",
3428            r"(?>a+)b|a",
3429            r"(a|ab)(c|bcd)(d*)",
3430            r"[\w.+-]+@\w+\.\w+",
3431            r"\p{L}+",
3432            r"é|e",
3433            r".*",
3434            r"(?<n>\d)(?<m>\d)?",
3435        ];
3436        let texts = [
3437            "",
3438            "a",
3439            "one two two three",
3440            "x@y.z foo(1) bar( a@b.cd",
3441            "alpha beta gammadelta ERROR warn Panic",
3442            "if else\nfor  \n\nwhile",
3443            "aab abcd abcbcdd",
3444            "Grüße café éte 12 345",
3445            "xxaxxb",
3446        ];
3447        for pattern in patterns {
3448            let re = Regex::new(pattern).unwrap();
3449            for text in texts {
3450                let bounds = re.find_iter(text);
3451                assert!(bounds.msa.is_some(), "{pattern}");
3452                let region_path = FindIter {
3453                    regex: &re,
3454                    program: re.program(),
3455                    text: text.as_bytes(),
3456                    subject: Subject::Str(text),
3457                    cursor: MatchCursor::default(),
3458                    region: OnigRegion::new(),
3459                    msa: None,
3460                    limits: None,
3461                };
3462                let got: Vec<_> = bounds.map(|m| m.range()).collect();
3463                let expected: Vec<_> = region_path.map(|m| m.range()).collect();
3464                assert_eq!(got, expected, "{pattern:?} {text:?}");
3465                assert_eq!(
3466                    re.find(text).map(|m| m.range()),
3467                    expected.first().cloned(),
3468                    "{pattern:?} {text:?}"
3469                );
3470            }
3471        }
3472        // With `\K` the region carries the match start.
3473        let keep = Regex::new(r"(\w)\K\w+").unwrap();
3474        assert_eq!(keep.find("one two").map(|m| m.range()), Some(1..3));
3475    }
3476
3477    /// Iteration over an unbounded map optimizer, where the next search starts
3478    /// on a candidate byte or before one. The expected ranges are the
3479    /// leftmost-first matches, whatever path finds them.
3480    #[test]
3481    fn find_iter_map_candidates_at_and_after_the_start() {
3482        let _limits = crate::regexec::shared_limits();
3483        // The start is a candidate (`a`) after each match: the search starts there.
3484        let re = Regex::new(r"a+|b").unwrap();
3485        let ranges: Vec<_> = re.find_iter("aabxba").map(|m| m.range()).collect();
3486        assert_eq!(ranges, vec![0..2, 2..3, 4..5, 5..6]);
3487        // The first byte is no candidate: the search scans on to the `a`.
3488        let ranges: Vec<_> = re.find_iter("xa").map(|m| m.range()).collect();
3489        assert_eq!(ranges, vec![1..2]);
3490        // Multibyte characters hold no candidate byte of `a+|b`: the match is
3491        // the `a` after them.
3492        let ranges: Vec<_> = re.find_iter("éaé").map(|m| m.range()).collect();
3493        assert_eq!(ranges, vec![2..3]);
3494        let re = Regex::new(r"é+|a").unwrap();
3495        let ranges: Vec<_> = re.find_iter("éaé").map(|m| m.range()).collect();
3496        assert_eq!(ranges, vec![0..2, 2..3, 3..5]);
3497        // A start on a character boundary, where the match starts at once.
3498        assert_eq!(re.find_at("éaé", 2).map(|m| m.range()), Some(2..3));
3499        // No candidate from the start: no match.
3500        assert!(re.find_at("éaé", 5).is_none());
3501        // Empty matches still step one character at a time.
3502        let re = Regex::new(r"x*|a").unwrap();
3503        let ranges: Vec<_> = re.find_iter("ab").map(|m| m.range()).collect();
3504        assert_eq!(ranges, vec![0..0, 1..1, 2..2]);
3505        // Look-behind at the boundary of the previous match.
3506        let re = Regex::new(r"(?<=a)b|c+").unwrap();
3507        let ranges: Vec<_> = re.find_iter("abcc b").map(|m| m.range()).collect();
3508        assert_eq!(ranges, vec![1..2, 2..4]);
3509        // \G anchors each search at its own start.
3510        let re = Regex::new(r"\Ga|b").unwrap();
3511        let ranges: Vec<_> = re.find_iter("aba").map(|m| m.range()).collect();
3512        assert_eq!(ranges, vec![0..1, 1..2, 2..3]);
3513    }
3514
3515    #[test]
3516    fn regex_new_and_find() {
3517        let _limits = crate::regexec::shared_limits();
3518        let re = Regex::new(r"\d+").unwrap();
3519        let m = re.find("hello 42 world").unwrap();
3520        assert_eq!(m.as_str(), "42");
3521        assert_eq!(m.start(), 6);
3522        assert_eq!(m.end(), 8);
3523        assert_eq!(m.range(), 6..8);
3524        assert_eq!(m.len(), 2);
3525        assert!(!m.is_empty());
3526    }
3527
3528    #[test]
3529    fn regex_no_match() {
3530        let _limits = crate::regexec::shared_limits();
3531        let re = Regex::new(r"\d+").unwrap();
3532        assert!(re.find("no digits here").is_none());
3533    }
3534
3535    #[test]
3536    fn regex_is_match() {
3537        let _limits = crate::regexec::shared_limits();
3538        let re = Regex::new(r"hello").unwrap();
3539        assert!(re.is_match("say hello"));
3540        assert!(!re.is_match("say goodbye"));
3541    }
3542
3543    #[test]
3544    fn search_options_methods_return_matches_and_captures() {
3545        let _limits = crate::regexec::shared_limits();
3546        let re = Regex::new(r"(\w+)").unwrap();
3547        let options = SearchOptions::new().timeout(Duration::from_secs(1));
3548
3549        assert_eq!(
3550            re.find_with("hello 42", options).unwrap().unwrap().as_str(),
3551            "hello"
3552        );
3553        assert_eq!(
3554            re.find_bytes_with(b"hello 42", options)
3555                .unwrap()
3556                .unwrap()
3557                .as_str(),
3558            "hello"
3559        );
3560        assert!(re.is_match_with("hello", options).unwrap());
3561        assert!(!re.is_match_bytes_with(b"!!!", options).unwrap());
3562        assert_eq!(
3563            re.captures_with("hello 42", options)
3564                .unwrap()
3565                .unwrap()
3566                .get(1)
3567                .unwrap()
3568                .as_str(),
3569            "hello"
3570        );
3571        assert!(re.captures_bytes_with(b"!!!", options).unwrap().is_none());
3572    }
3573
3574    #[test]
3575    fn search_options_surface_time_limit_errors() {
3576        let _limits = crate::regexec::shared_limits();
3577        let re = Regex::new(r"(a+)+b").unwrap();
3578        let text = "a".repeat(40);
3579        let options = SearchOptions::new()
3580            .timeout(Duration::from_millis(1))
3581            .retry_limit_in_match(0)
3582            .retry_limit_in_search(0);
3583
3584        assert!(matches!(
3585            re.find_with(&text, options),
3586            Err(RegexError::TimeLimitOver)
3587        ));
3588        assert!(matches!(
3589            re.is_match_with(&text, options),
3590            Err(RegexError::TimeLimitOver)
3591        ));
3592        assert!(matches!(
3593            re.captures_with(&text, options),
3594            Err(RegexError::TimeLimitOver)
3595        ));
3596    }
3597
3598    #[test]
3599    fn search_options_timeout_spans_every_start_position() {
3600        let _limits = crate::regexec::shared_limits();
3601        // Each start position backtracks only ~400 times; the limit must still
3602        // fire for the search as a whole.
3603        let re = Regex::new(r"a{1,400}?(?=b)").unwrap();
3604        let text = "a".repeat(500_000);
3605        let options = SearchOptions::new().timeout(Duration::from_millis(10));
3606
3607        let started = std::time::Instant::now();
3608        assert!(matches!(
3609            re.find_with(&text, options),
3610            Err(RegexError::TimeLimitOver)
3611        ));
3612        assert!(started.elapsed() < Duration::from_secs(1));
3613    }
3614
3615    /// Rust-only (ADR-008): the skip gates of guard chains keep the retry
3616    /// limit's boundary where the one-by-one walk puts it. With the gates off,
3617    /// the smallest per-match limit that finds the match is found, and the
3618    /// gated program gives the same result at that limit and one below it.
3619    #[test]
3620    fn skip_gates_keep_the_retry_limit_boundary() {
3621        let _limits = crate::regexec::shared_limits();
3622        let compile = |pattern: &str, gated: bool| {
3623            crate::regcomp::SKIP_GATES_DISABLED.with(|disabled| disabled.set(!gated));
3624            let re = Regex::new(pattern);
3625            crate::regcomp::SKIP_GATES_DISABLED.with(|disabled| disabled.set(false));
3626            re.unwrap()
3627        };
3628        let text = format!("{}ss!", "ß".repeat(4));
3629        let plain = compile(r"(?i)^[[:word:]]{0,40}s!", false);
3630        let gated = compile(r"(?i)^[[:word:]]{0,40}s!", true);
3631        let at = |re: &Regex, limit: u64| {
3632            re.find_with(&text, SearchOptions::new().retry_limit_in_match(limit))
3633                .map(|m| m.map(|m| (m.start(), m.end())))
3634        };
3635        let boundary = (1..=100_000u64)
3636            .find(|&limit| matches!(at(&plain, limit), Ok(Some(_))))
3637            .expect("the match fits in the default limit");
3638        assert!(boundary > 1, "the search backtracks");
3639        for limit in [boundary - 1, boundary] {
3640            assert_eq!(at(&gated, limit), at(&plain, limit), "limit {limit}");
3641        }
3642        assert!(matches!(
3643            at(&plain, boundary - 1),
3644            Err(RegexError::RetryLimitInMatchOver)
3645        ));
3646    }
3647
3648    #[test]
3649    fn search_options_surface_retry_limit_errors() {
3650        let _limits = crate::regexec::shared_limits();
3651        let re = Regex::new(r"(a+)+b").unwrap();
3652        let text = "a".repeat(30);
3653
3654        assert!(matches!(
3655            re.find_with(&text, SearchOptions::new().retry_limit_in_match(1_000)),
3656            Err(RegexError::RetryLimitInMatchOver)
3657        ));
3658        assert!(matches!(
3659            re.find_with(
3660                &text,
3661                SearchOptions::new()
3662                    .retry_limit_in_match(0)
3663                    .retry_limit_in_search(1_000)
3664            ),
3665            Err(RegexError::RetryLimitInSearchOver)
3666        ));
3667    }
3668
3669    /// A search under per-search limits leaves none behind: the standard
3670    /// methods, which reuse the same thread-cached MatchArg, keep the
3671    /// process-wide limits.
3672    #[test]
3673    fn search_options_limits_do_not_leak_into_standard_searches() {
3674        let _limits = crate::regexec::shared_limits();
3675        let re = Regex::new(r"a*?b").unwrap();
3676        let text = format!("{}b", "a".repeat(50));
3677        let tight = SearchOptions::new().retry_limit_in_match(3);
3678
3679        assert!(matches!(
3680            re.find_with(&text, tight),
3681            Err(RegexError::RetryLimitInMatchOver)
3682        ));
3683        assert!(matches!(
3684            re.is_match_with(&text, tight),
3685            Err(RegexError::RetryLimitInMatchOver)
3686        ));
3687        assert!(matches!(
3688            re.captures_with(&text, tight),
3689            Err(RegexError::RetryLimitInMatchOver)
3690        ));
3691        assert!(matches!(
3692            re.find_iter_with(&text, tight).next(),
3693            Some(Err(RegexError::RetryLimitInMatchOver))
3694        ));
3695        assert!(matches!(
3696            re.captures_iter_with(&text, tight).next(),
3697            Some(Err(RegexError::RetryLimitInMatchOver))
3698        ));
3699
3700        assert_eq!(re.find(&text).map(|m| m.range()), Some(0..51));
3701        assert!(re.is_match(&text));
3702        assert!(re.captures(&text).is_some());
3703        assert_eq!(re.find_iter(&text).count(), 1);
3704        assert_eq!(
3705            re.find_with(&text, SearchOptions::new())
3706                .unwrap()
3707                .map(|m| m.range()),
3708            Some(0..51)
3709        );
3710    }
3711
3712    /// The bounds-only path (`find`, `find_iter`) and the region path
3713    /// (`captures`) report the same limit errors for the same options, so
3714    /// `find_with` may use the bounds-only path.
3715    #[test]
3716    fn bounds_and_region_paths_report_the_same_limit_errors() {
3717        let _limits = crate::regexec::shared_limits();
3718        let patterns = [
3719            r"a*?b",
3720            r"(a+)+b",
3721            r"(?:a|aa)*c",
3722            r"\w+\s+\w+x",
3723            r"(\w+)@(\w+)\.com",
3724            r"x*y|a+z",
3725            r"(?>a+)b|a",
3726        ];
3727        // Short subjects: the catastrophic patterns run into their process-wide
3728        // limits, and 16 bytes keep that cheap.
3729        let texts = [
3730            format!("{}b", "a".repeat(16)),
3731            format!("{}c", "a".repeat(15)),
3732            "a".repeat(16),
3733            "one two three four five six".to_owned(),
3734            "mail user@example.com and more".to_owned(),
3735            format!("{}z", "a".repeat(20)),
3736            // Deep enough for a stack limit: any limit below 320 holds 321
3737            // entries.
3738            format!("{}b", "a".repeat(400)),
3739        ];
3740        let limits = [
3741            SearchOptions::new().retry_limit_in_match(1),
3742            SearchOptions::new().retry_limit_in_match(3),
3743            SearchOptions::new().retry_limit_in_match(20),
3744            SearchOptions::new().retry_limit_in_search(5),
3745            SearchOptions::new().retry_limit_in_search(50),
3746            SearchOptions::new().match_stack_limit(4),
3747            SearchOptions::new().match_stack_limit(40),
3748            SearchOptions::new().match_stack_limit(320),
3749        ];
3750        for pattern in patterns {
3751            let re = Regex::new(pattern).unwrap();
3752            for text in &texts {
3753                for options in limits {
3754                    let bounds = re.find_with(text, options).map(|m| m.map(|m| m.range()));
3755                    let region = re
3756                        .captures_with(text, options)
3757                        .map(|c| c.map(|c| c.get(0).unwrap().range()));
3758                    assert_eq!(bounds, region, "{pattern:?} {text:?} {options:?}");
3759                    let is_match = re.is_match_with(text, options);
3760                    assert_eq!(
3761                        is_match,
3762                        bounds.as_ref().map(Option::is_some).map_err(Clone::clone),
3763                        "{pattern:?} {text:?} {options:?}"
3764                    );
3765                }
3766            }
3767        }
3768    }
3769
3770    #[test]
3771    fn search_options_leave_unset_limits_process_wide() {
3772        let _limits = crate::regexec::exclusive_limits();
3773        let old_retry = crate::regexec::onig_get_retry_limit_in_match();
3774        crate::regexec::onig_set_retry_limit_in_match(1_000);
3775        let re = Regex::new(r"(a+)+b").unwrap();
3776        let text = "a".repeat(30);
3777
3778        let unset = re.find_with(&text, SearchOptions::new());
3779        let disabled = re.find_with(
3780            &text,
3781            SearchOptions::new()
3782                .retry_limit_in_match(0)
3783                .retry_limit_in_search(0)
3784                .timeout(Duration::from_millis(50)),
3785        );
3786        crate::regexec::onig_set_retry_limit_in_match(old_retry);
3787
3788        assert!(matches!(unset, Err(RegexError::RetryLimitInMatchOver)));
3789        assert!(matches!(disabled, Err(RegexError::TimeLimitOver)));
3790    }
3791
3792    #[test]
3793    fn search_options_iterator_yields_matches_and_then_finishes() {
3794        let _limits = crate::regexec::shared_limits();
3795        let re = Regex::new(r"\w+").unwrap();
3796        let options = SearchOptions::new().timeout(Duration::from_secs(1));
3797        let mut matches = re.find_iter_with("one two", options);
3798
3799        assert_eq!(matches.next().unwrap().unwrap().as_str(), "one");
3800        assert_eq!(matches.next().unwrap().unwrap().as_str(), "two");
3801        assert!(matches.next().is_none());
3802        assert!(matches.next().is_none());
3803        let bytes: Vec<_> = re
3804            .find_iter_bytes_with(b"a b", options)
3805            .map(|m| m.unwrap().as_bytes().to_vec())
3806            .collect();
3807        assert_eq!(bytes, vec![b"a".to_vec(), b"b".to_vec()]);
3808    }
3809
3810    #[test]
3811    fn search_options_iterator_surfaces_an_error_once() {
3812        let _limits = crate::regexec::shared_limits();
3813        let re = Regex::new(r"(a+)+b").unwrap();
3814        let text = "a".repeat(40);
3815        let options = SearchOptions::new()
3816            .timeout(Duration::from_millis(1))
3817            .retry_limit_in_match(0)
3818            .retry_limit_in_search(0);
3819        let mut matches = re.find_iter_with(&text, options);
3820
3821        assert!(matches!(
3822            matches.next().unwrap(),
3823            Err(RegexError::TimeLimitOver)
3824        ));
3825        assert!(matches.next().is_none());
3826    }
3827
3828    #[test]
3829    fn search_options_default_keeps_every_limit_unset() {
3830        assert_eq!(SearchOptions::default(), SearchOptions::new());
3831        assert_send_sync::<SearchOptions>();
3832        assert_send_sync::<TryFindIter<'static, 'static>>();
3833    }
3834
3835    #[test]
3836    fn timeout_is_rounded_up_to_milliseconds() {
3837        assert_eq!(timeout_to_millis(Duration::ZERO), 0);
3838        assert_eq!(timeout_to_millis(Duration::from_nanos(1)), 1);
3839        assert_eq!(timeout_to_millis(Duration::from_millis(1)), 1);
3840        assert_eq!(timeout_to_millis(Duration::from_nanos(1_000_001)), 2);
3841        assert_eq!(timeout_to_millis(Duration::MAX), u64::MAX);
3842    }
3843
3844    #[test]
3845    fn regex_captures() {
3846        let _limits = crate::regexec::shared_limits();
3847        let re = Regex::new(r"(\d{4})-(\d{2})-(\d{2})").unwrap();
3848        let caps = re.captures("date: 2026-02-14").unwrap();
3849        assert_eq!(caps.get(0).unwrap().as_str(), "2026-02-14");
3850        assert_eq!(caps.get(1).unwrap().as_str(), "2026");
3851        assert_eq!(caps.get(2).unwrap().as_str(), "02");
3852        assert_eq!(caps.get(3).unwrap().as_str(), "14");
3853        assert!(caps.get(4).is_none());
3854        assert_eq!(caps.len(), 4);
3855    }
3856
3857    #[test]
3858    fn regex_captures_len() {
3859        let re = Regex::new(r"(a)(b)(c)").unwrap();
3860        assert_eq!(re.captures_len(), 3);
3861    }
3862
3863    #[test]
3864    fn regex_find_iter() {
3865        let _limits = crate::regexec::shared_limits();
3866        let re = Regex::new(r"\d+").unwrap();
3867        let matches: Vec<&str> = re.find_iter("1 + 22 = 333").map(|m| m.as_str()).collect();
3868        assert_eq!(matches, vec!["1", "22", "333"]);
3869    }
3870
3871    #[test]
3872    fn regex_builder_case_insensitive() {
3873        let _limits = crate::regexec::shared_limits();
3874        let re = Regex::builder(r"hello")
3875            .case_insensitive(true)
3876            .build()
3877            .unwrap();
3878        assert!(re.is_match("HELLO"));
3879        assert!(re.is_match("Hello"));
3880    }
3881
3882    #[test]
3883    fn regex_invalid_pattern() {
3884        let err = Regex::new(r"(unclosed").unwrap_err();
3885        assert!(matches!(err, RegexError::Syntax { .. }));
3886    }
3887
3888    #[test]
3889    fn match_as_bytes() {
3890        let _limits = crate::regexec::shared_limits();
3891        let re = Regex::new(r"world").unwrap();
3892        let m = re.find("hello world").unwrap();
3893        assert_eq!(m.as_bytes(), b"world");
3894    }
3895
3896    #[test]
3897    fn captures_iter() {
3898        let _limits = crate::regexec::shared_limits();
3899        let re = Regex::new(r"(a)(b)?").unwrap();
3900        let caps = re.captures("a").unwrap();
3901        let items: Vec<_> = caps.iter().collect();
3902        // group 0 = "a", group 1 = "a", group 2 = None (didn't participate)
3903        assert_eq!(items.len(), 3);
3904        assert!(items[0].is_some());
3905        assert!(items[1].is_some());
3906        assert!(items[2].is_none());
3907    }
3908
3909    #[test]
3910    fn named_captures() {
3911        let _limits = crate::regexec::shared_limits();
3912        let re = Regex::new(r"(?<year>\d{4})-(?<month>\d{2})").unwrap();
3913        let caps = re.captures("2026-02").unwrap();
3914        assert_eq!(caps.name("year").unwrap().as_str(), "2026");
3915        assert_eq!(caps.name("month").unwrap().as_str(), "02");
3916        assert!(caps.name("day").is_none());
3917    }
3918
3919    #[test]
3920    fn empty_match_find_iter() {
3921        let _limits = crate::regexec::shared_limits();
3922        let re = Regex::new(r"").unwrap();
3923        let matches: Vec<_> = re.find_iter("ab").collect();
3924        // Should yield empty matches at positions 0, 1, 2
3925        assert_eq!(matches.len(), 3);
3926        assert_eq!(matches[0].start(), 0);
3927        assert_eq!(matches[1].start(), 1);
3928        assert_eq!(matches[2].start(), 2);
3929    }
3930
3931    /// The iteration as the search loop ran it before an empty match stepped
3932    /// past its own search: every search starts at the end of the previous
3933    /// match, and an empty match right after an empty match is skipped by a
3934    /// search one character on. `search(at)` returns the match found from
3935    /// `at` with the item that the iterator yields for it.
3936    fn repeated_search_iteration<T>(
3937        re: &Regex,
3938        text: &[u8],
3939        search: impl Fn(usize) -> Option<(usize, usize, T)>,
3940    ) -> Vec<T> {
3941        let mut found = Vec::new();
3942        let mut last_end = 0;
3943        let mut last_was_empty = false;
3944        while last_end <= text.len() {
3945            let Some((start, end, item)) = search(last_end) else {
3946                break;
3947            };
3948            if start == end {
3949                if last_was_empty {
3950                    if last_end >= text.len() {
3951                        break;
3952                    }
3953                    last_end += re.program().enc.mbc_enc_len(&text[last_end..]);
3954                    last_was_empty = false;
3955                    continue;
3956                }
3957                last_was_empty = true;
3958            } else {
3959                last_was_empty = false;
3960            }
3961            last_end = end;
3962            found.push(item);
3963        }
3964        found
3965    }
3966
3967    /// The spans of the groups of a match, `None` for a group that did not
3968    /// participate.
3969    type Groups = Vec<Option<(usize, usize)>>;
3970
3971    fn group_spans(caps: &Captures<'_>) -> Groups {
3972        caps.iter()
3973            .map(|group| group.map(|m| (m.start(), m.end())))
3974            .collect()
3975    }
3976
3977    /// Patterns with empty matches, and with `\G` and `\K`, which keep the
3978    /// searches of the iteration.
3979    const EMPTY_MATCH_PATTERNS: &[&str] = &[
3980        "",
3981        "a*",
3982        "a*?",
3983        "x*",
3984        r"\b",
3985        r"\B",
3986        "(?=x)",
3987        "(?!x)",
3988        "|",
3989        "a|",
3990        "|a",
3991        "(a|)",
3992        "(a)?(b*)",
3993        r"(\w*)",
3994        "(?<=a)",
3995        "^",
3996        "$",
3997        r"\b\w*",
3998        "(?:)",
3999        r"\d*",
4000        "é*",
4001        "(?i)k*",
4002        r"a*?$",
4003        r"(?=\w)|\s",
4004        r"\b|é",
4005        // \G reads the search start: the step is not taken. The search from
4006        // the end of the empty match at 1 matches "b" there, which the
4007        // iterator must yield.
4008        r"(?<=a)(?!\G)|b",
4009        r"\Ga*",
4010        r"\G",
4011        r"\Gx*",
4012        // \K moves the match start: the step is not taken.
4013        r"a\K",
4014        r"\K",
4015        r"x*\K",
4016        r"(a)\K|b*",
4017    ];
4018
4019    const EMPTY_MATCH_TEXTS: &[&str] = &[
4020        "",
4021        "a",
4022        "ab",
4023        "aXa",
4024        "xéx日本",
4025        "a\nb\n",
4026        "ééé",
4027        "\u{1F600}x",
4028        "bbb",
4029        "x  x",
4030    ];
4031
4032    #[test]
4033    fn empty_match_step_only_where_the_program_allows_it() {
4034        let _limits = crate::regexec::shared_limits();
4035        for pattern in ["", "a*", r"\b", "(?=x)", "|", r"(\w*)", "(a)?(b*)"] {
4036            let re = Regex::new(pattern).unwrap();
4037            assert!(
4038                re.program().skip_repeated_empty_search,
4039                "{pattern:?} has no \\G, \\K or callout"
4040            );
4041        }
4042        for pattern in [r"\Ga*", r"\G", r"a\K", r"\K", r"(a)\K|b*"] {
4043            let re = Regex::new(pattern).unwrap();
4044            assert!(
4045                !re.program().skip_repeated_empty_search,
4046                "{pattern:?} reads the search start or keeps the match start"
4047            );
4048        }
4049    }
4050
4051    /// The iterators yield what the repeated search yields, for empty matches
4052    /// over ASCII, multibyte and invalid text, with and without `\G` and `\K`.
4053    #[test]
4054    fn empty_match_step_yields_what_the_repeated_search_yields() {
4055        let _limits = crate::regexec::shared_limits();
4056        for pattern in EMPTY_MATCH_PATTERNS {
4057            let re = Regex::new(pattern).unwrap();
4058            for text in EMPTY_MATCH_TEXTS {
4059                let bytes = text.as_bytes();
4060                let expected = repeated_search_iteration(&re, bytes, |at| {
4061                    re.find_bytes_at(bytes, at)
4062                        .map(|m| (m.start(), m.end(), (m.start(), m.end())))
4063                });
4064                let found: Vec<_> = re.find_iter(text).map(|m| (m.start(), m.end())).collect();
4065                assert_eq!(found, expected, "find_iter: {pattern:?} on {text:?}");
4066                let found: Vec<_> = re
4067                    .find_iter_bytes(bytes)
4068                    .map(|m| (m.start(), m.end()))
4069                    .collect();
4070                assert_eq!(found, expected, "find_iter_bytes: {pattern:?} on {text:?}");
4071
4072                let expected = repeated_search_iteration(&re, bytes, |at| {
4073                    re.captures_bytes_at(bytes, at).map(|caps| {
4074                        let spans = group_spans(&caps);
4075                        let (start, end) = spans[0].expect("whole match");
4076                        (start, end, spans)
4077                    })
4078                });
4079                let found: Vec<_> = re.captures_iter(text).map(|c| group_spans(&c)).collect();
4080                assert_eq!(found, expected, "captures_iter: {pattern:?} on {text:?}");
4081            }
4082        }
4083    }
4084
4085    /// Invalid UTF-8 is searched by the bytes API, one byte character at a time
4086    /// where the encoding cannot read a character.
4087    #[test]
4088    fn empty_match_step_on_invalid_utf8() {
4089        let _limits = crate::regexec::shared_limits();
4090        let text: &[u8] = b"a\xffb\xe6\x97";
4091        for pattern in ["", "a*", r"\b", "x*", r"(\w*)"] {
4092            let re = Regex::new(pattern).unwrap();
4093            let expected = repeated_search_iteration(&re, text, |at| {
4094                re.find_bytes_at(text, at)
4095                    .map(|m| (m.start(), m.end(), (m.start(), m.end())))
4096            });
4097            let found: Vec<_> = re
4098                .find_iter_bytes(text)
4099                .map(|m| (m.start(), m.end()))
4100                .collect();
4101            assert_eq!(found, expected, "{pattern:?} on invalid UTF-8");
4102        }
4103    }
4104
4105    /// An empty match costs one search, not two: the search from its end is
4106    /// not run again.
4107    #[test]
4108    fn empty_match_step_takes_one_search_per_match() {
4109        let _limits = crate::regexec::shared_limits();
4110        let re = Regex::new("x*").unwrap();
4111        let before = crate::regexec::VM_ATTEMPTS.with(std::cell::Cell::get);
4112        let matches = re.find_iter("abcd").count();
4113        let attempts = crate::regexec::VM_ATTEMPTS.with(std::cell::Cell::get) - before;
4114        assert_eq!(matches, 5);
4115        assert_eq!(attempts, 5);
4116    }
4117
4118    /// The captures of every match, including the matches found by a search
4119    /// whose first attempt fails, and by a map search that starts at a later
4120    /// candidate.
4121    #[test]
4122    fn captures_iter_records_the_captures_of_every_match() {
4123        let _limits = crate::regexec::shared_limits();
4124        let re = Regex::new(r"#(\w+)").unwrap();
4125        let found: Vec<_> = re
4126            .captures_iter("x #ab #c")
4127            .map(|c| group_spans(&c))
4128            .collect();
4129        assert_eq!(
4130            found,
4131            vec![
4132                vec![Some((2, 5)), Some((3, 5))],
4133                vec![Some((6, 8)), Some((7, 8))],
4134            ]
4135        );
4136
4137        let re = Regex::new(r"(a)|(b)").unwrap();
4138        let found: Vec<_> = re.captures_iter("cba").map(|c| group_spans(&c)).collect();
4139        assert_eq!(
4140            found,
4141            vec![
4142                vec![Some((1, 2)), None, Some((1, 2))],
4143                vec![Some((2, 3)), Some((2, 3)), None],
4144            ]
4145        );
4146
4147        let re = Regex::new(r"(\d)(\d)?").unwrap();
4148        let found: Vec<_> = re
4149            .captures_iter("a1 22b")
4150            .map(|c| group_spans(&c))
4151            .collect();
4152        assert_eq!(
4153            found,
4154            vec![
4155                vec![Some((1, 2)), Some((1, 2)), None],
4156                vec![Some((3, 5)), Some((3, 4)), Some((4, 5))],
4157            ]
4158        );
4159
4160        // The first attempt fails and a later one matches, untracked: the
4161        // captures are recorded by a second pass.
4162        let cases: [(&str, &str, Vec<Groups>); 4] = [
4163            (
4164                r"(a)b|(c)",
4165                "axc ac",
4166                vec![
4167                    vec![Some((2, 3)), None, Some((2, 3))],
4168                    vec![Some((5, 6)), None, Some((5, 6))],
4169                ],
4170            ),
4171            (
4172                r"(?<=z)(a)|(b)",
4173                "zaxb",
4174                vec![
4175                    vec![Some((1, 2)), Some((1, 2)), None],
4176                    vec![Some((3, 4)), None, Some((3, 4))],
4177                ],
4178            ),
4179            (
4180                r"\w(\d)|(x)",
4181                "1x2",
4182                vec![vec![Some((1, 3)), Some((2, 3)), None]],
4183            ),
4184            (
4185                r"(a)(b)?c|(d)e",
4186                "ab de",
4187                vec![vec![Some((3, 5)), None, None, Some((3, 4))]],
4188            ),
4189        ];
4190        for (pattern, text, expected) in cases {
4191            let re = Regex::new(pattern).unwrap();
4192            let found: Vec<_> = re.captures_iter(text).map(|c| group_spans(&c)).collect();
4193            assert_eq!(found, expected, "{pattern:?} on {text:?}");
4194        }
4195    }
4196
4197    /// An iterator reads the process-wide limits again for every search, so
4198    /// a limit set between two matches applies to the next search, as it does
4199    /// for a plain search (Rust-only, ADR-008: one `MatchArg` serves the
4200    /// iterator's searches).
4201    #[test]
4202    fn find_iter_reads_the_process_wide_limits_for_each_search() {
4203        let _limits = crate::regexec::exclusive_limits();
4204        crate::regexec::onig_set_retry_limit_in_search(0);
4205        let re = Regex::new(r"x|a+c").unwrap();
4206        let mut matches = re.find_iter("x aabaac");
4207        assert_eq!(matches.next().unwrap().range(), 0..1);
4208
4209        crate::regexec::onig_set_retry_limit_in_search(1);
4210        // The search from offset 1 backtracks in `a+c` before it reaches
4211        // `aac`, which it would find without the limit.
4212        assert!(matches.next().is_none());
4213    }
4214
4215    /// The captures iterator reads the process-wide limits for each search,
4216    /// as `find_iter` does.
4217    #[test]
4218    fn captures_iter_reads_the_process_wide_limits_for_each_search() {
4219        let _limits = crate::regexec::exclusive_limits();
4220        crate::regexec::onig_set_retry_limit_in_search(0);
4221        let re = Regex::new(r"(x)|(a+c)").unwrap();
4222        let mut matches = re.captures_iter("x aabaac");
4223        assert_eq!(matches.next().unwrap().get(0).unwrap().range(), 0..1);
4224
4225        crate::regexec::onig_set_retry_limit_in_search(1);
4226        assert!(matches.next().is_none());
4227    }
4228
4229    /// Each yielded `Captures` returns its region to the thread cache when it
4230    /// is dropped, and the next search takes it from there.
4231    #[test]
4232    fn captures_iter_reuses_the_region_of_a_dropped_match() {
4233        let _limits = crate::regexec::shared_limits();
4234        clear_cached_region();
4235        let re = Regex::new(r"(\w)(\w?)").unwrap();
4236        let mut matches = re.captures_iter("ab cd ef");
4237        let first = matches.next().unwrap().region.beg.as_ptr();
4238        let second = matches.next().unwrap().region.beg.as_ptr();
4239        assert_eq!(second, first);
4240        let third = matches.next().unwrap().region.beg.as_ptr();
4241        assert_eq!(third, first);
4242    }
4243}