Skip to main content

real_regex/
lib.rs

1//! Linear-time, ReDoS-safe regular expressions with **bounded lookarounds** — a drop-in-shaped Rust binding
2//! to the REAL C++ engine (via its C ABI). The API mirrors the [`regex`](https://docs.rs/regex) crate; every
3//! pattern that compiles matches in time linear in the input, with no backtracking and so no catastrophic
4//! blow-up. The engine is **strict by design** — a construct it cannot run linearly (a backreference, an
5//! unbounded lookaround) is rejected at [`Regex::new`], never silently made non-linear.
6//!
7//! ```
8//! use real_regex::Regex;
9//! let re = Regex::new(r"(?P<y>\d{4})-(?P<m>\d{2})").unwrap();
10//! let caps = re.captures("2026-07").unwrap();
11//! assert_eq!(&caps["y"], "2026");
12//! assert_eq!(caps.get(2).unwrap().as_str(), "07");
13//! let re: Regex = r"\d+".parse().unwrap();
14//! assert_eq!(format!("{re}"), r"\d+");
15//! ```
16use std::collections::HashMap;
17use std::marker::PhantomData;
18use std::ops::Index;
19use std::os::raw::c_char;
20use std::sync::Arc;
21
22/// The crate's version (CalVer, shared with the C++ engine and the Python wheel).
23pub const VERSION: &str = env!("CARGO_PKG_VERSION");
24
25// Opaque C handles.
26enum RealRegex {}
27enum RealIter {}
28enum RealRegexSet {}
29
30extern "C" {
31    fn real_compile(pattern: *const c_char, len: usize, flags: u32,
32                    errbuf: *mut c_char, errbuf_len: usize, code: *mut i32) -> *mut RealRegex;
33    fn real_group_count(re: *const RealRegex) -> usize;
34    fn real_group_name(re: *const RealRegex, group: usize, buf: *mut c_char, buflen: usize) -> usize;
35    fn real_free(re: *mut RealRegex);
36    fn real_find_iter(re: *const RealRegex, text: *const c_char, len: usize) -> *mut RealIter;
37    fn real_find_iter_at(re: *const RealRegex, text: *const c_char, len: usize, start: usize) -> *mut RealIter;
38    fn real_iter_next(iter: *mut RealIter, spans: *mut usize) -> i32;
39    fn real_iter_free(iter: *mut RealIter);
40    fn real_count_matches(re: *const RealRegex, text: *const c_char, len: usize) -> usize;
41    fn real_set_compile(patterns: *const *const c_char, lens: *const usize, n: usize, flags: u32,
42                        errbuf: *mut c_char, errbuf_len: usize, code: *mut i32) -> *mut RealRegexSet;
43    fn real_set_size(set: *const RealRegexSet) -> usize;
44    fn real_set_free(set: *mut RealRegexSet);
45    fn real_set_is_match(set: *const RealRegexSet, text: *const c_char, len: usize) -> i32;
46    fn real_set_matches(set: *const RealRegexSet, text: *const c_char, len: usize, out: *mut u8) -> i32;
47}
48
49const DIVERGENCES_URL: &str = "https://github.com/RECHE23/real-regex/blob/main/docs/COMPATIBILITY.md";
50const REAL_ERR_UNSUPPORTED: i32 = 2; // must match REAL_ERR_UNSUPPORTED in real_capi.h
51// real::flags::dollar_endonly — `$` (no multiline) matches only at the very end, never before a final `\n`.
52// The crate compiles every pattern with it, so `$` carries rust's `\z` semantics instead of Python re's.
53const DOLLAR_ENDONLY: u32 = 128;
54
55/// Why a pattern failed to compile.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum Error {
58    /// A syntax error in the pattern, with the engine's message and (when known) the byte position.
59    Syntax { msg: String, pos: Option<usize> },
60    /// A construct REAL does not support linearly (`\p{…}`, a backreference, an unbounded lookaround, …).
61    /// `hint` points at the divergences page and, when there is one, the way out — the `fallback`
62    /// feature. It names the absence just as plainly: a [`RegexSet`] never delegates, and the regex
63    /// crate is linear too, so it refuses a backreference exactly as REAL does. The hint sells a
64    /// remedy only where one exists.
65    Unsupported { construct: String, hint: String },
66}
67
68impl Error {
69    /// Whether this is an unsupported-construct error (rather than a syntax error).
70    pub fn is_unsupported(&self) -> bool {
71        matches!(self, Error::Unsupported { .. })
72    }
73
74    // Build an Error from the engine's message and its structured code (REAL_ERR_*). The classification comes
75    // from the code the C ABI reports — never from matching on the message text, so a reworded engine message
76    // cannot silently change whether a pattern is treated as unsupported.
77    fn from_engine(raw: &str, code: i32, rescue: Rescue) -> Error {
78        let body = raw.strip_prefix("regex_error").unwrap_or(raw).trim_start();
79        let (pos, msg) = match body.strip_prefix("at ").and_then(|r| r.split_once(':')) {
80            Some((n, rest)) => (n.trim().parse::<usize>().ok(), rest.trim().to_string()),
81            None => (None, body.trim_start_matches(':').trim().to_string()),
82        };
83        if code == REAL_ERR_UNSUPPORTED {
84            unsupported_construct(&msg, rescue)
85        } else {
86            Error::Syntax { msg, pos }
87        }
88    }
89}
90
91impl std::fmt::Display for Error {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        match self {
94            Error::Syntax { msg, pos: Some(p) } => write!(f, "syntax error at {p}: {msg}"),
95            Error::Syntax { msg, pos: None } => write!(f, "syntax error: {msg}"),
96            Error::Unsupported { construct, hint } => write!(f, "{construct} ({hint})"),
97        }
98    }
99}
100
101impl std::error::Error for Error {}
102
103// Group metadata, shared cheaply (Arc) by the Regex and every Captures it produces — this is what lets
104// Captures carry a single lifetime, like the regex crate.
105struct GroupInfo {
106    names: Vec<Option<String>>,       // by group index (None = unnamed)
107    by_name: HashMap<String, usize>,  // name -> group index
108}
109
110// Inline capacity of a Captures, in SLOTS (two per group, group 0 included) — 8 slots = 4 groups.
111// Covers the overwhelming majority of real patterns; beyond it a Captures spills to the heap once.
112const CAPS_INLINE_SLOTS: usize = 8;
113
114// Capture slots for one match, flat and inline: [start0, end0, start1, end1, …], usize::MAX marking a
115// group that did not participate — the same representation the C ABI fills and CaptureLocations holds,
116// so building one is a straight copy with no per-group Option mapping.
117//
118// Why inline: Captures must OWN its slots (it outlives the iterator step that produced it), and the
119// previous Vec<Option<(usize, usize)>> meant one malloc + free per match. On a groupless pattern that
120// was ~19–27 ns/match of pure allocator traffic to carry a single span — measured as the whole of the
121// crate's captures_iter-vs-find_iter gap, and the reason `regex`'s captures_iter costs what its
122// find_iter costs while ours cost 1.6–2.6× more. Spilling keeps the many-group case correct rather
123// than capping it.
124#[derive(Clone, Debug)]
125enum SlotStore {
126    Inline { len: u8, slots: [usize; CAPS_INLINE_SLOTS] },
127    Spilled(Box<[usize]>),
128}
129
130impl SlotStore {
131    // Take a flat slot run (len = 2 * ngroups) by value-copy, inline when it fits.
132    fn from_flat(src: &[usize]) -> SlotStore {
133        // Group 0 alone -- a groupless pattern -- is the dominant shape, and taking it with two plain
134        // stores rather than `copy_from_slice` is the whole point: a runtime length compiles to a memcpy
135        // CALL, which costs more than the stores it replaces at one or two slots. That is the same reason
136        // the C ABI reads slots pairwise instead of with one memcpy, and it was measured here too:
137        // ablating this call closed the entire remaining captures_iter-vs-find_iter gap (114 of the 171 us
138        // on `\b\w+\b` over a 64 KiB corpus, ~9.4 ns a match), where the object's size, Drop glue and Arc
139        // traffic together accounted for the other 57.
140        if src.len() == 2 {
141            let mut slots = [usize::MAX; CAPS_INLINE_SLOTS];
142            slots[0] = src[0];
143            slots[1] = src[1];
144            return SlotStore::Inline { len: 2, slots };
145        }
146        if src.len() <= CAPS_INLINE_SLOTS {
147            let mut slots = [usize::MAX; CAPS_INLINE_SLOTS];
148            slots[..src.len()].copy_from_slice(src);
149            SlotStore::Inline { len: src.len() as u8, slots }
150        } else {
151            SlotStore::Spilled(src.to_vec().into_boxed_slice())
152        }
153    }
154
155    fn as_slice(&self) -> &[usize] {
156        match self {
157            SlotStore::Inline { len, slots } => &slots[..*len as usize],
158            SlotStore::Spilled(b) => b,
159        }
160    }
161
162    // Byte offsets of group `i`, or None when it did not participate (or `i` is out of range).
163    // checked_mul, not `2 * i`: `i` is caller-supplied (Captures::get / Index take any usize), and a
164    // plain multiply panics on overflow in a debug build for i > usize::MAX / 2. The slot indexing is
165    // this type's own doing -- the Vec<Option<_>> this replaced was indexed by group, so it could not
166    // overflow -- so the bound has to be re-established here. `lo + 1` cannot overflow: lo came back
167    // from a successful get() on a slice, so lo < len <= isize::MAX.
168    fn group(&self, i: usize) -> Option<(usize, usize)> {
169        let s = self.as_slice();
170        let lo = i.checked_mul(2)?;
171        let a = *s.get(lo)?;
172        let b = *s.get(lo + 1)?;
173        if a == usize::MAX {
174            None
175        } else {
176            Some((a, b))
177        }
178    }
179
180    // Slot count / 2 — the number of groups, group 0 included.
181    fn ngroups(&self) -> usize {
182        self.as_slice().len() / 2
183    }
184}
185
186/// Whether the `fallback` feature could actually rescue the pattern that was just refused.
187///
188/// Two halves must hold before the hint is worth printing, the same pair the Python binding
189/// settled on: the call site has a fallback at all, and the delegate can run the pattern. The
190/// second half is where Rust differs from Python — `re` backtracks and takes anything, the regex
191/// crate is linear and refuses a backreference exactly as REAL does (pinned in tests/fallback.rs).
192// Without the feature the regex crate is not linked, so `rescue_for` can only ever answer
193// `Unknown` and the two decided variants are unconstructible — a fact of that build, not dead code.
194#[cfg_attr(not(feature = "fallback"), allow(dead_code))]
195enum Rescue {
196    /// The delegate accepts it — say so, and name the switch.
197    Delegable,
198    /// The delegate refuses it too. Offering the feature here sends the reader down a dead end.
199    Refused,
200    /// No oracle: the feature is off, so the regex crate is not linked and cannot be asked.
201    Unknown,
202    /// This call site has no fallback whatever the construct — `RegexSet` never delegates.
203    NoFallbackHere,
204}
205
206/// Ask the delegate itself rather than classifying the construct by hand. A hand-written list of
207/// "constructs the regex crate refuses" would be a second model to keep true; the crate is the
208/// authority on its own grammar.
209#[cfg(feature = "fallback")]
210fn rescue_for(pattern: &[u8]) -> Rescue {
211    match std::str::from_utf8(pattern) {
212        Ok(p) if regex::Regex::new(p).is_ok() => Rescue::Delegable,
213        Ok(_) => Rescue::Refused,
214        Err(_) => Rescue::Unknown,
215    }
216}
217
218#[cfg(not(feature = "fallback"))]
219fn rescue_for(_pattern: &[u8]) -> Rescue {
220    Rescue::Unknown
221}
222
223// The standard unsupported-construct error, hint included (shared by the engine path and the pre-scan below).
224fn unsupported_construct(construct: &str, rescue: Rescue) -> Error {
225    let remedy = match rescue {
226        Rescue::Delegable => "the `fallback` feature plus `RegexBuilder::fallback(true)` delegates this \
227                              pattern to the regex crate (forfeiting the linear-time guarantee for it)"
228            .to_string(),
229        Rescue::Refused => "the `fallback` feature does not help here: the regex crate is linear too and \
230                            refuses this pattern as well"
231            .to_string(),
232        Rescue::Unknown => "the `fallback` feature delegates some such patterns to the regex crate, but not \
233                            a non-regular one (a backreference, a conditional) — the regex crate, linear \
234                            itself, refuses those too"
235            .to_string(),
236        Rescue::NoFallbackHere => "a RegexSet never delegates: compile the pattern on its own with the \
237                                   `fallback` feature if you need it"
238            .to_string(),
239    };
240    Error::Unsupported {
241        construct: construct.to_string(),
242        hint: format!("unsupported by REAL — see {DIVERGENCES_URL} ; {remedy}"),
243    }
244}
245
246// Rust's regex crate parses nested character classes (`[a[b]]` = union) and the class set operators `&&`,
247// `--`, `~~`; Python `re` — REAL's model — treats `[` as a literal inside a class, so `[a[b]]` parses to two
248// different classes (and two match sets). Rather than implement rust's class algebra, the crate declines such
249// patterns up front with a hint (the `fallback` feature then delegates them, and `regex` does support them).
250// Returns the offending construct, or None. Escapes (`\[`, `\-`, `\\`) are respected. `\p{…}` is a separate
251// arc; here we only spot the class-set syntax.
252fn nested_class_syntax(pattern: &[u8]) -> Option<&'static str> {
253    let mut i = 0;
254    let mut in_class = false;
255    let mut class_pos = 0usize; // members seen in the current class (0 = just after `[` / `[^`)
256    while i < pattern.len() {
257        let b = pattern[i];
258        if b == b'\\' {
259            i += 2; // skip the escaped byte — an escaped `[` is a literal, never a nested class
260            if in_class {
261                class_pos += 1;
262            }
263            continue;
264        }
265        if !in_class {
266            if b == b'[' {
267                in_class = true;
268                class_pos = 0;
269                if pattern.get(i + 1) == Some(&b'^') {
270                    i += 1; // negation; the first real member is still class_pos 0
271                }
272            }
273        } else if b == b']' {
274            if class_pos == 0 {
275                class_pos += 1; // a `]` right after `[` is a literal member, not the close
276            } else {
277                in_class = false;
278            }
279        } else if b == b'[' {
280            return Some("nested character class");
281        } else if matches!(b, b'&' | b'-' | b'~') && pattern.get(i + 1) == Some(&b) {
282            return Some("character-class set operation");
283        } else {
284            class_pos += 1;
285        }
286        i += 1;
287    }
288    None
289}
290
291// Compile a pattern (as raw bytes) and precompute its group names. Shared by the str and bytes APIs.
292fn compile_handle(pattern: &[u8], flags: u32) -> Result<(*mut RealRegex, usize, Arc<GroupInfo>), Error> {
293    if let Some(construct) = nested_class_syntax(pattern) {
294        return Err(unsupported_construct(construct, rescue_for(pattern))); // rust-only class syntax REAL would parse differently
295    }
296    let mut err = [0u8; 256];
297    let mut code: i32 = 0;
298    let handle = unsafe {
299        real_compile(pattern.as_ptr() as *const c_char, pattern.len(), flags | DOLLAR_ENDONLY,
300                     err.as_mut_ptr() as *mut c_char, err.len(), &mut code)
301    };
302    if handle.is_null() {
303        let end = err.iter().position(|&b| b == 0).unwrap_or(err.len());
304        return Err(Error::from_engine(&String::from_utf8_lossy(&err[..end]), code, rescue_for(pattern)));
305    }
306    let ngroups = unsafe { real_group_count(handle) };
307    let mut names = Vec::with_capacity(ngroups);
308    let mut by_name = HashMap::new();
309    // Two-call protocol (same shape as Go SubexpNames): length query with null buf, then
310    // exact-sized fill — no fixed buffer, no 127-byte truncation / name-map alias collapse.
311    for g in 0..ngroups {
312        let len = unsafe { real_group_name(handle, g, std::ptr::null_mut(), 0) };
313        if len == 0 {
314            names.push(None);
315        } else {
316            let mut buf = vec![0u8; len + 1];
317            unsafe {
318                real_group_name(handle, g, buf.as_mut_ptr() as *mut c_char, buf.len());
319            }
320            let name = String::from_utf8_lossy(&buf[..len]).into_owned();
321            by_name.insert(name.clone(), g);
322            names.push(Some(name));
323        }
324    }
325    Ok((handle, ngroups, Arc::new(GroupInfo { names, by_name })))
326}
327
328/// Which engine backs a compiled pattern — observable via [`Regex::engine`].
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
330pub enum Engine {
331    /// REAL's linear-time, ReDoS-safe engine.
332    Real,
333    /// The regex crate (only when the `fallback` feature delegated this pattern) — not ReDoS-safe.
334    Fallback,
335}
336
337/// A compiled pattern.
338pub struct Regex {
339    handle: *mut RealRegex, // null when a fallback backend is in use
340    ngroups: usize,         // capture slots per match, including group 0
341    pattern: String,
342    groups: Arc<GroupInfo>,
343    #[cfg(feature = "fallback")]
344    fallback: Option<regex::Regex>, // Some when delegated to the regex crate
345}
346
347// The handle is an owned heap object with no interior mutability observable from Rust; sharing a &Regex
348// across threads (read-only matching) is sound.
349unsafe impl Send for Regex {}
350unsafe impl Sync for Regex {}
351
352impl Regex {
353    /// Compile `pattern`. Returns the engine's error message if the pattern is invalid or cannot be run
354    /// linearly (the strict policy).
355    pub fn new(pattern: &str) -> Result<Regex, Error> {
356        Regex::with_flags(pattern, 0)
357    }
358
359    /// Compile with a `real::flags` bitmask (icase=1, multiline=2, dotall=4, bytes=8, verbose=16, ecma=32,
360    /// ascii=64). Prefer [`RegexBuilder`] for readable options.
361    pub fn with_flags(pattern: &str, flags: u32) -> Result<Regex, Error> {
362        let (handle, ngroups, groups) = compile_handle(pattern.as_bytes(), flags)?;
363        Ok(Regex {
364            handle,
365            ngroups,
366            pattern: pattern.to_string(),
367            groups,
368            #[cfg(feature = "fallback")]
369            fallback: None,
370        })
371    }
372
373    /// Which engine backs this pattern — [`Engine::Real`] (linear, ReDoS-safe) or [`Engine::Fallback`] (the
374    /// regex crate, when the `fallback` feature delegated it). Always `Real` unless the feature is used.
375    pub fn engine(&self) -> Engine {
376        #[cfg(feature = "fallback")]
377        if self.fallback.is_some() {
378            return Engine::Fallback;
379        }
380        Engine::Real
381    }
382
383    // Delegate a pattern REAL cannot run linearly to the regex crate (only reachable via the `fallback`
384    // feature + RegexBuilder::fallback(true)). The wrapper keeps our own types over regex's results.
385    #[cfg(feature = "fallback")]
386    fn build_fallback(pattern: &str, flags: u32) -> Result<Regex, Error> {
387        let fb = regex::RegexBuilder::new(pattern)
388            .case_insensitive(flags & FLAG_ICASE != 0)
389            .multi_line(flags & FLAG_MULTILINE != 0)
390            .dot_matches_new_line(flags & FLAG_DOTALL != 0)
391            .ignore_whitespace(flags & FLAG_VERBOSE != 0)
392            .unicode(flags & FLAG_ASCII == 0)
393            .build()
394            .map_err(|e| Error::Syntax { msg: e.to_string(), pos: None })?;
395        let ngroups = fb.captures_len();
396        let mut names = Vec::with_capacity(ngroups);
397        let mut by_name = HashMap::new();
398        for (i, n) in fb.capture_names().enumerate() {
399            match n {
400                Some(name) => {
401                    by_name.insert(name.to_string(), i);
402                    names.push(Some(name.to_string()));
403                }
404                None => names.push(None),
405            }
406        }
407        Ok(Regex {
408            handle: std::ptr::null_mut(),
409            ngroups,
410            pattern: pattern.to_string(),
411            groups: Arc::new(GroupInfo { names, by_name }),
412            fallback: Some(fb),
413        })
414    }
415
416    /// The original pattern string.
417    pub fn as_str(&self) -> &str {
418        &self.pattern
419    }
420
421    /// The number of capture slots, **including** the implicit whole-match group 0 (so always >= 1) —
422    /// the regex crate's convention.
423    pub fn captures_len(&self) -> usize {
424        self.ngroups
425    }
426
427    /// The name of each capture group (group 0 first), `None` for the unnamed ones.
428    pub fn capture_names(&self) -> impl Iterator<Item = Option<&str>> {
429        self.groups.names.iter().map(|o| o.as_deref())
430    }
431
432    fn raw<'r, 't>(&'r self, text: &'t str, start: Option<usize>) -> SpanCursor<'r, 't> {
433        #[cfg(feature = "fallback")]
434        if let Some(fb) = &self.fallback {
435            return SpanCursor::Fallback {
436                it: fb.captures_iter(text),
437                ngroups: self.ngroups,
438                min_start: start.unwrap_or(0),
439                cur: Vec::new(),
440            };
441        }
442        let iter = unsafe {
443            match start {
444                None => real_find_iter(self.handle, text.as_ptr() as *const c_char, text.len()),
445                Some(s) => real_find_iter_at(self.handle, text.as_ptr() as *const c_char, text.len(), s),
446            }
447        };
448        // A null cursor means the engine failed to construct the iterator (never dereference it).
449        assert!(!iter.is_null(), "real-regex: engine iteration failed");
450        SpanCursor::Real(RawSpans { iter, handle: self.handle, text: text.as_bytes(), ngroups: self.ngroups, buf: vec![0usize; 2 * self.ngroups], last_end: None, drive_pos: None, utf8: true, _re: PhantomData })
451    }
452
453    fn caps_from<'t>(&self, text: &'t str, cur: &SpanCursor<'_, '_>) -> Captures<'t> {
454        Captures { text, slots: cur.slot_store(), groups: Arc::clone(&self.groups) }
455    }
456
457    /// Whether the pattern matches anywhere in `text`.
458    pub fn is_match(&self, text: &str) -> bool {
459        self.raw(text, None).advance().is_some()
460    }
461
462    /// Like [`is_match`](Regex::is_match), searching from byte offset `start`.
463    pub fn is_match_at(&self, text: &str, start: usize) -> bool {
464        self.raw(text, Some(start)).advance().is_some()
465    }
466
467    /// The leftmost match's whole-match span, or `None`.
468    pub fn find<'t>(&self, text: &'t str) -> Option<Match<'t>> {
469        self.raw(text, None).advance().map(|(a, b)| Match { text, start: a, end: b })
470    }
471
472    /// Like [`find`](Regex::find), searching from byte offset `start`.
473    pub fn find_at<'t>(&self, text: &'t str, start: usize) -> Option<Match<'t>> {
474        self.raw(text, Some(start)).advance().map(|(a, b)| Match { text, start: a, end: b })
475    }
476
477    /// Iterate the non-overlapping whole-match spans in `text`.
478    pub fn find_iter<'r, 't>(&'r self, text: &'t str) -> Matches<'r, 't> {
479        Matches { raw: self.raw(text, None), text }
480    }
481
482    /// The capture groups of the leftmost match, or `None`.
483    pub fn captures<'t>(&self, text: &'t str) -> Option<Captures<'t>> {
484        {
485            let mut c = self.raw(text, None);
486            c.advance().map(|_| self.caps_from(text, &c))
487        }
488    }
489
490    /// Like [`captures`](Regex::captures), searching from byte offset `start`.
491    pub fn captures_at<'t>(&self, text: &'t str, start: usize) -> Option<Captures<'t>> {
492        {
493            let mut c = self.raw(text, Some(start));
494            c.advance().map(|_| self.caps_from(text, &c))
495        }
496    }
497
498    /// A reusable capture-slot buffer for this pattern — drop-in for
499    /// [`regex::Regex::capture_locations`]. Pair with [`captures_read`](Regex::captures_read)
500    /// to extract groups without allocating a [`Captures`] per match.
501    pub fn capture_locations(&self) -> CaptureLocations {
502        CaptureLocations {
503            slots: vec![0; 2 * self.ngroups],
504            ngroups: self.ngroups,
505        }
506    }
507
508    /// Fill `locs` with the leftmost match's group spans (no per-match allocation). Returns the
509    /// whole-match [`Match`] span, or `None`. Mirrors `regex::Regex::captures_read`.
510    pub fn captures_read<'t>(
511        &self,
512        locs: &mut CaptureLocations,
513        text: &'t str,
514    ) -> Option<Match<'t>> {
515        self.captures_read_at(locs, text, 0)
516    }
517
518    /// Like [`captures_read`](Regex::captures_read), searching from byte offset `start`.
519    pub fn captures_read_at<'t>(
520        &self,
521        locs: &mut CaptureLocations,
522        text: &'t str,
523        start: usize,
524    ) -> Option<Match<'t>> {
525        locs.ensure(self.ngroups);
526        let mut c = self.raw(text, if start == 0 { None } else { Some(start) });
527        let (a, b) = c.advance()?;
528        c.copy_slots_into(locs);
529        Some(Match {
530            text,
531            start: a,
532            end: b,
533        })
534    }
535
536    /// Iterate non-overlapping matches without allocating a [`Captures`] per match.
537    /// Yields the whole-match [`Match`]; after each step, read groups with
538    /// [`CaptureLocationMatches::get`] (or copy into a [`CaptureLocations`] via
539    /// [`CaptureLocationMatches::read_captures`]). Prefer this over
540    /// [`captures_iter`](Regex::captures_iter) in capture-dense hot loops.
541    pub fn captures_read_iter<'r, 't>(
542        &'r self,
543        text: &'t str,
544    ) -> CaptureLocationMatches<'r, 't> {
545        CaptureLocationMatches {
546            raw: self.raw(text, None),
547            text,
548            ngroups: self.ngroups,
549        }
550    }
551
552    /// Iterate the capture groups of each non-overlapping match in `text`.
553    pub fn captures_iter<'r, 't>(&'r self, text: &'t str) -> CaptureMatches<'r, 't> {
554        CaptureMatches { raw: self.raw(text, None), re: self, text }
555    }
556
557    /// The end offset of the leftmost match (a match exists iff this is `Some`). **Divergence:** like the
558    /// regex crate, REAL is leftmost-**first**, but this returns the leftmost match's *greedy* end, whereas
559    /// the regex crate returns the earliest position at which a match completes (e.g. `a+` on `"aaa"`: REAL
560    /// 3, regex 1). A true earliest-completion mode is a parked follow-up (a `first-accept` stop in the
561    /// forward pass). Use this as an `is_match` that also reports where the leftmost match ends.
562    pub fn shortest_match(&self, text: &str) -> Option<usize> {
563        #[cfg(feature = "fallback")]
564        if let Some(fb) = &self.fallback {
565            return fb.shortest_match(text); // the regex backend gives true earliest-completion
566        }
567        self.raw(text, None).advance().map(|(_, e)| e)
568    }
569
570    /// Count non-overlapping matches without materialising match objects (matching-only).
571    ///
572    /// Prefer this over counting [`find_iter`](Regex::find_iter) when only the count matters, and for
573    /// trailing-lookahead class+ patterns where the fast path lives here (not on find_iter). Parity:
574    /// `re.count_matches(t) == re.find_iter(t).count()`.
575    pub fn count_matches(&self, text: &str) -> usize {
576        #[cfg(feature = "fallback")]
577        if let Some(fb) = &self.fallback {
578            return fb.find_iter(text).count();
579        }
580        let n = unsafe {
581            real_count_matches(self.handle, text.as_ptr() as *const c_char, text.len())
582        };
583        assert_ne!(n, usize::MAX, "real-regex: count_matches failed");
584        n
585    }
586}
587
588/// A multi-pattern set: which patterns match the subject at least once (which-matched).
589///
590/// Mirrors the [`regex`](https://docs.rs/regex) crate's `RegexSet`. Bitset order is the
591/// construction order of the patterns. Captures are not reported — re-run the individual
592/// pattern if groups are needed. Stage-1 is N independent walks with per-pattern early-exit
593/// (not a fused single-pass automaton).
594pub struct RegexSet {
595    handle: *mut RealRegexSet,
596    patterns: Vec<String>,
597}
598
599unsafe impl Send for RegexSet {}
600unsafe impl Sync for RegexSet {}
601
602impl RegexSet {
603    /// Compile every pattern; fails if any pattern is invalid (no silent skip).
604    pub fn new<I, S>(patterns: I) -> Result<RegexSet, Error>
605    where
606        I: IntoIterator<Item = S>,
607        S: AsRef<str>,
608    {
609        RegexSet::with_flags(patterns, 0)
610    }
611
612    /// Compile with a `real::flags` bitmask (same bits as [`Regex::with_flags`]).
613    pub fn with_flags<I, S>(patterns: I, flags: u32) -> Result<RegexSet, Error>
614    where
615        I: IntoIterator<Item = S>,
616        S: AsRef<str>,
617    {
618        let owned: Vec<String> = patterns.into_iter().map(|s| s.as_ref().to_string()).collect();
619        let mut ptrs: Vec<*const c_char> = Vec::with_capacity(owned.len());
620        let mut lens: Vec<usize> = Vec::with_capacity(owned.len());
621        for p in &owned {
622            ptrs.push(p.as_ptr() as *const c_char);
623            lens.push(p.len());
624        }
625        let mut err = [0i8; 512];
626        let mut code: i32 = 0;
627        let handle = unsafe {
628            real_set_compile(
629                ptrs.as_ptr(),
630                lens.as_ptr(),
631                owned.len(),
632                flags | DOLLAR_ENDONLY,
633                err.as_mut_ptr(),
634                err.len(),
635                &mut code,
636            )
637        };
638        if handle.is_null() {
639            let raw = unsafe { std::ffi::CStr::from_ptr(err.as_ptr()) }
640                .to_string_lossy()
641                .into_owned();
642            return Err(Error::from_engine(&raw, code, Rescue::NoFallbackHere));
643        }
644        Ok(RegexSet {
645            handle,
646            patterns: owned,
647        })
648    }
649
650    /// Number of patterns in the set.
651    pub fn len(&self) -> usize {
652        unsafe { real_set_size(self.handle) }
653    }
654
655    /// Whether the set has no patterns.
656    pub fn is_empty(&self) -> bool {
657        self.len() == 0
658    }
659
660    /// The original pattern strings (construction order).
661    pub fn patterns(&self) -> &[String] {
662        &self.patterns
663    }
664
665    /// True if **any** pattern matches `text` (stops at the first hit).
666    pub fn is_match(&self, text: &str) -> bool {
667        let r = unsafe {
668            real_set_is_match(self.handle, text.as_ptr() as *const c_char, text.len())
669        };
670        r == 1
671    }
672
673    /// Which patterns match at least once: bitset of length [`len`](RegexSet::len),
674    /// construction order. Index `i` is true iff pattern `i` matched.
675    pub fn matches(&self, text: &str) -> Vec<bool> {
676        let n = self.len();
677        let mut out = vec![0u8; n];
678        let r = unsafe {
679            real_set_matches(
680                self.handle,
681                text.as_ptr() as *const c_char,
682                text.len(),
683                out.as_mut_ptr(),
684            )
685        };
686        assert_eq!(r, 0, "real-regex: regex_set matches failed");
687        out.into_iter().map(|b| b != 0).collect()
688    }
689
690    /// Indices of matching patterns (ascending, construction order).
691    pub fn matched_ids(&self, text: &str) -> Vec<usize> {
692        self.matches(text)
693            .into_iter()
694            .enumerate()
695            .filter_map(|(i, hit)| hit.then_some(i))
696            .collect()
697    }
698}
699
700impl Drop for RegexSet {
701    fn drop(&mut self) {
702        if !self.handle.is_null() {
703            unsafe { real_set_free(self.handle) }
704        }
705    }
706}
707
708impl Drop for Regex {
709    fn drop(&mut self) {
710        if !self.handle.is_null() {
711            unsafe { real_free(self.handle) } // null when a fallback backend is in use
712        }
713    }
714}
715
716impl std::fmt::Debug for Regex {
717    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
718        write!(f, "Regex({:?})", self.pattern)
719    }
720}
721
722impl std::fmt::Display for Regex {
723    /// The original pattern — the same affordance as the regex crate.
724    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
725        f.write_str(self.as_str())
726    }
727}
728
729impl std::str::FromStr for Regex {
730    type Err = Error;
731
732    fn from_str(s: &str) -> Result<Regex, Error> {
733        Regex::new(s)
734    }
735}
736
737// The low-level cursor: yields one match's full span vector at a time.
738struct RawSpans<'r, 't> {
739    iter: *mut RealIter,           // fast-mode iterator (re's stream); abandoned once we switch to driving
740    handle: *const RealRegex,      // for drive mode: re-search from a position with real_find_iter_at
741    text: &'t [u8],                // the haystack (drive-mode search pointer + codepoint stepping)
742    ngroups: usize,
743    buf: Vec<usize>,               // reused span buffer (2*ngroups), refilled per match — never reallocated
744    last_end: Option<usize>,       // end of the last YIELDED match — for the empty-adjacent rule
745    drive_pos: Option<usize>,      // None = fast mode; Some(p) = driving the search from position p
746    utf8: bool,                    // step by one codepoint (str) vs one byte (bytes) past an empty match
747    _re: PhantomData<&'r ()>,      // ties the borrowed handle to the Regex's lifetime
748}
749
750impl RawSpans<'_, '_> {
751    // Advance to the next match, reproducing rust's iteration exactly (regex-automata's util::iter::Searcher).
752    // rust DRIVES the search by position: it finds the leftmost match from `input.start`, sets the next start
753    // to that match's end, and on an empty match adjacent to the previous end it steps the start forward by
754    // one codepoint and re-searches. A filter over REAL's re-ordered stream cannot reproduce this — rust
755    // visits positions the re-stream never does (`(?:|ab)*` on "abab": rust yields empties at 1 and 3, which
756    // re, advancing by its own wider matches, skips). So we drive too, via real_find_iter_at.
757    //
758    // But driving allocates an iterator per step, which would undo the span-0 fast path. Since re and rust
759    // diverge ONLY at empty matches (a non-empty leftmost match is identical for both, and both advance to its
760    // end), we stay on the cheap re-iterator until the FIRST empty match, then switch to driving from rust's
761    // current position. Patterns that never match empty (the throughput-critical ones) never switch.
762    fn advance(&mut self) -> Option<(usize, usize)> {
763        if self.drive_pos.is_some() {
764            return self.drive_advance();
765        }
766        loop {
767            let got = unsafe { real_iter_next(self.iter, self.buf.as_mut_ptr()) };
768            match got {
769                0 => return None,
770                // -1 is an internal engine error (or a null cursor). A linear search never "fails to match" —
771                // the rust contract is compile -> Result, then matching is infallible — so we surface it.
772                -1 => panic!("real-regex: engine iteration failed"),
773                _ => {
774                    let (s0, e0) = (self.buf[0], self.buf[1]); // group 0 always participates
775                    if s0 == e0 {
776                        // First empty match: re and rust's advancement diverge here. Switch to driving the
777                        // search by position, resuming from rust's current start (the last yielded end).
778                        self.drive_pos = Some(self.last_end.unwrap_or(0));
779                        return self.drive_advance();
780                    }
781                    self.last_end = Some(e0);
782                    return Some((s0, e0));
783                }
784            }
785        }
786    }
787
788    // The leftmost match at or after `pos` (unanchored), filling `buf` with its groups. Each call spins up a
789    // one-shot iterator — only reached in drive mode, i.e. for empty-capable patterns.
790    fn search_at(&mut self, pos: usize) -> Option<(usize, usize)> {
791        if pos > self.text.len() {
792            return None;
793        }
794        let it = unsafe {
795            real_find_iter_at(self.handle, self.text.as_ptr() as *const c_char, self.text.len(), pos)
796        };
797        assert!(!it.is_null(), "real-regex: engine iteration failed");
798        let got = unsafe { real_iter_next(it, self.buf.as_mut_ptr()) };
799        unsafe { real_iter_free(it) };
800        match got {
801            0 => None,
802            -1 => panic!("real-regex: engine iteration failed"),
803            _ => Some((self.buf[0], self.buf[1])),
804        }
805    }
806
807    // Bytes to step past position `pos` when skipping an empty match — one codepoint in str mode (so the next
808    // search stays on a char boundary, as rust's UTF-8 Input does), one byte in bytes mode.
809    fn step_len(&self, pos: usize) -> usize {
810        if !self.utf8 || pos >= self.text.len() {
811            return 1;
812        }
813        match self.text[pos] {
814            b if b < 0x80 => 1,
815            b if b < 0xE0 => 2,
816            b if b < 0xF0 => 3,
817            _ => 4,
818        }
819    }
820
821    // One step of rust's position-driven iteration: find from drive_pos; if that match is empty and adjacent
822    // to the previous yielded end, step forward one codepoint and re-search once (handle_overlapping_empty_
823    // match); then yield it and set the next start to its end.
824    fn drive_advance(&mut self) -> Option<(usize, usize)> {
825        let pos = self.drive_pos.expect("drive_advance in fast mode");
826        let mut m = self.search_at(pos)?;
827        if m.0 == m.1 && Some(m.1) == self.last_end {
828            let next = m.1 + self.step_len(m.1);
829            m = self.search_at(next)?;
830        }
831        self.last_end = Some(m.1);
832        self.drive_pos = Some(m.1);
833        Some(m)
834    }
835
836}
837
838impl Drop for RawSpans<'_, '_> {
839    fn drop(&mut self) {
840        unsafe { real_iter_free(self.iter) }
841    }
842}
843
844// Unifies the two backends behind one span stream: REAL's cursor (with the empty-match filter) or, under the
845// fallback feature, the regex crate's capture iterator (already rust-correct, converted to span vectors).
846enum SpanCursor<'r, 't> {
847    Real(RawSpans<'r, 't>),
848    #[cfg(feature = "fallback")]
849    Fallback {
850        it: regex::CaptureMatches<'r, 't>,
851        ngroups: usize,
852        min_start: usize,
853        cur: Vec<Option<(usize, usize)>>, // current match's groups, reused across advances
854    },
855}
856
857impl SpanCursor<'_, '_> {
858    // Advance to the next match; return its whole-match span (group 0). The group slots are then available
859    // via slot_store() / write_slots() — for the Real backend straight out of the reused flat buffer, so
860    // find_iter / is_match / split touch no group storage at all; only captures_iter builds a Captures.
861    fn advance(&mut self) -> Option<(usize, usize)> {
862        match self {
863            SpanCursor::Real(r) => r.advance(),
864            #[cfg(feature = "fallback")]
865            SpanCursor::Fallback { it, ngroups, min_start, cur } => loop {
866                let caps = it.next()?;
867                let m0 = caps.get(0).unwrap();
868                if m0.start() < *min_start {
869                    continue; // for the *_at variants: skip matches before the requested start
870                }
871                cur.clear();
872                cur.extend((0..*ngroups).map(|g| caps.get(g).map(|m| (m.start(), m.end()))));
873                return Some((m0.start(), m0.end()));
874            },
875        }
876    }
877
878    // Number of capture slots this cursor reports per match (2 per group, group 0 included).
879    fn nslots(&self) -> usize {
880        match self {
881            SpanCursor::Real(r) => 2 * r.ngroups,
882            #[cfg(feature = "fallback")]
883            SpanCursor::Fallback { ngroups, .. } => 2 * *ngroups,
884        }
885    }
886
887    // Write the current match's slots (after advance() returned Some) flat into `out`, whose length is
888    // nslots(). The Real backend's buffer is already in this representation; the fallback's Option
889    // vector is mapped onto it. The one place either shape is converted.
890    fn write_slots(&self, out: &mut [usize]) {
891        match self {
892            SpanCursor::Real(r) => out.copy_from_slice(&r.buf),
893            #[cfg(feature = "fallback")]
894            SpanCursor::Fallback { cur, .. } => {
895                for (g, s) in cur.iter().enumerate() {
896                    let (a, b) = s.unwrap_or((usize::MAX, usize::MAX));
897                    out[2 * g] = a;
898                    out[(2 * g) + 1] = b;
899                }
900            }
901        }
902    }
903
904    // The current match's slots as an owned, inline-when-it-fits store — what a Captures carries.
905    fn slot_store(&self) -> SlotStore {
906        match self {
907            // Fast path: the engine's buffer is already flat, so this is one copy and no conversion.
908            SpanCursor::Real(r) => SlotStore::from_flat(&r.buf),
909            #[cfg(feature = "fallback")]
910            SpanCursor::Fallback { .. } => {
911                let n = self.nslots();
912                if n <= CAPS_INLINE_SLOTS {
913                    let mut slots = [usize::MAX; CAPS_INLINE_SLOTS];
914                    self.write_slots(&mut slots[..n]);
915                    SlotStore::Inline { len: n as u8, slots }
916                } else {
917                    let mut v = vec![usize::MAX; n];
918                    self.write_slots(&mut v);
919                    SlotStore::Spilled(v.into_boxed_slice())
920                }
921            }
922        }
923    }
924
925    // Copy the current match's flat slots into a reusable CaptureLocations (no alloc).
926    fn copy_slots_into(&self, locs: &mut CaptureLocations) {
927        let ngroups = self.nslots() / 2;
928        locs.ensure(ngroups);
929        self.write_slots(&mut locs.slots);
930    }
931}
932
933/// Reusable capture-slot buffer — drop-in for [`regex::CaptureLocations`].
934///
935/// Obtain via [`Regex::capture_locations`], refill with [`Regex::captures_read`] (or
936/// [`captures_read_at`](Regex::captures_read_at)). Spans are read with [`get`](CaptureLocations::get).
937/// The buffer is not tied to a text lifetime, so it can be reused across many subjects without
938/// allocating a [`Captures`] (or a group vector) per match.
939#[derive(Clone, Debug)]
940pub struct CaptureLocations {
941    slots: Vec<usize>, // flat [start0, end0, …]; usize::MAX marks an unset group
942    ngroups: usize,
943}
944
945impl CaptureLocations {
946    /// Number of capture slots, including group 0.
947    pub fn len(&self) -> usize {
948        self.ngroups
949    }
950
951    /// Whether there are no capture slots (never true for a live `Regex`).
952    pub fn is_empty(&self) -> bool {
953        self.ngroups == 0
954    }
955
956    /// Byte offsets `(start, end)` of group `i`, or `None` if the group did not participate
957    /// (or `i` is out of range).
958    pub fn get(&self, i: usize) -> Option<(usize, usize)> {
959        if i >= self.ngroups {
960            return None;
961        }
962        let a = self.slots[2 * i];
963        let b = self.slots[2 * i + 1];
964        if a == usize::MAX {
965            None
966        } else {
967            Some((a, b))
968        }
969    }
970
971    fn ensure(&mut self, ngroups: usize) {
972        if self.ngroups != ngroups || self.slots.len() != 2 * ngroups {
973            self.slots.resize(2 * ngroups, 0);
974            self.ngroups = ngroups;
975        }
976    }
977}
978
979/// A single match — one span into the subject (the whole match, or one capture group).
980#[derive(Clone, Copy, Debug, PartialEq, Eq)]
981pub struct Match<'t> {
982    text: &'t str,
983    start: usize,
984    end: usize,
985}
986
987impl<'t> Match<'t> {
988    /// The start byte offset.
989    pub fn start(&self) -> usize {
990        self.start
991    }
992
993    /// The end byte offset (exclusive).
994    pub fn end(&self) -> usize {
995        self.end
996    }
997
998    /// The byte range `start..end`.
999    pub fn range(&self) -> std::ops::Range<usize> {
1000        self.start..self.end
1001    }
1002
1003    /// The matched slice.
1004    pub fn as_str(&self) -> &'t str {
1005        &self.text[self.start..self.end]
1006    }
1007
1008    /// Whether the match is empty.
1009    pub fn is_empty(&self) -> bool {
1010        self.start == self.end
1011    }
1012
1013    /// The length of the match in bytes.
1014    pub fn len(&self) -> usize {
1015        self.end - self.start
1016    }
1017}
1018
1019/// The capture groups of a single match. Group 0 is the whole match.
1020pub struct Captures<'t> {
1021    text: &'t str,
1022    slots: SlotStore,
1023    groups: Arc<GroupInfo>,
1024}
1025
1026impl<'t> Captures<'t> {
1027    /// Capture group `i` (0 = the whole match), or `None` if it did not participate.
1028    pub fn get(&self, i: usize) -> Option<Match<'t>> {
1029        self.slots.group(i).map(|(s, e)| Match { text: self.text, start: s, end: e })
1030    }
1031
1032    /// The named capture group `name`, or `None` if it is absent or did not participate.
1033    pub fn name(&self, name: &str) -> Option<Match<'t>> {
1034        self.groups.by_name.get(name).and_then(|&i| self.get(i))
1035    }
1036
1037    /// The number of capture slots, including group 0.
1038    pub fn len(&self) -> usize {
1039        self.slots.ngroups()
1040    }
1041
1042    /// Whether there are no capture slots (never true for a real match — group 0 always exists).
1043    pub fn is_empty(&self) -> bool {
1044        self.slots.ngroups() == 0
1045    }
1046
1047    /// Iterate every group in order (`None` for a group that did not participate).
1048    pub fn iter(&self) -> impl Iterator<Item = Option<Match<'t>>> + '_ {
1049        (0..self.len()).map(move |i| self.get(i))
1050    }
1051}
1052
1053// Panicking index access, mirroring regex: caps[0] / caps["name"] return the matched &str.
1054impl Index<usize> for Captures<'_> {
1055    type Output = str;
1056    fn index(&self, i: usize) -> &str {
1057        self.get(i).map(|m| m.as_str()).unwrap_or_else(|| panic!("no group at index {i}"))
1058    }
1059}
1060
1061impl Index<&str> for Captures<'_> {
1062    type Output = str;
1063    fn index(&self, name: &str) -> &str {
1064        self.name(name).map(|m| m.as_str()).unwrap_or_else(|| panic!("no group named {name:?}"))
1065    }
1066}
1067
1068/// Iterator over whole-match spans, from [`Regex::find_iter`].
1069pub struct Matches<'r, 't> {
1070    raw: SpanCursor<'r, 't>,
1071    text: &'t str,
1072}
1073
1074impl<'t> Iterator for Matches<'_, 't> {
1075    type Item = Match<'t>;
1076    fn next(&mut self) -> Option<Match<'t>> {
1077        self.raw.advance().map(|(a, b)| Match { text: self.text, start: a, end: b })
1078    }
1079}
1080
1081/// Iterator over capture groups, from [`Regex::captures_iter`].
1082pub struct CaptureMatches<'r, 't> {
1083    raw: SpanCursor<'r, 't>,
1084    re: &'r Regex,
1085    text: &'t str,
1086}
1087
1088/// Iterator over matches with reusable group slots — from [`Regex::captures_read_iter`].
1089///
1090/// After each [`next`](Iterator::next) that returns `Some`, the current match's groups are
1091/// available via [`get`](CaptureLocationMatches::get) without allocating a [`Captures`].
1092pub struct CaptureLocationMatches<'r, 't> {
1093    raw: SpanCursor<'r, 't>,
1094    text: &'t str,
1095    ngroups: usize,
1096}
1097
1098impl CaptureLocationMatches<'_, '_> {
1099    /// Number of capture slots (including group 0).
1100    pub fn len(&self) -> usize {
1101        self.ngroups
1102    }
1103
1104    /// Whether there are no capture slots.
1105    pub fn is_empty(&self) -> bool {
1106        self.ngroups == 0
1107    }
1108
1109    /// Group `i` of the **current** match (after `next` returned `Some`), or `None` if unset /
1110    /// out of range.
1111    pub fn get(&self, i: usize) -> Option<(usize, usize)> {
1112        if i >= self.ngroups {
1113            return None;
1114        }
1115        match &self.raw {
1116            SpanCursor::Real(r) => {
1117                let a = r.buf[2 * i];
1118                let b = r.buf[2 * i + 1];
1119                if a == usize::MAX {
1120                    None
1121                } else {
1122                    Some((a, b))
1123                }
1124            }
1125            #[cfg(feature = "fallback")]
1126            SpanCursor::Fallback { cur, .. } => cur.get(i).copied().flatten(),
1127        }
1128    }
1129
1130    /// Copy the current match's spans into `locs` (reusable across subjects / steps).
1131    pub fn read_captures(&self, locs: &mut CaptureLocations) {
1132        self.raw.copy_slots_into(locs);
1133    }
1134}
1135
1136impl<'t> Iterator for CaptureLocationMatches<'_, 't> {
1137    type Item = Match<'t>;
1138    fn next(&mut self) -> Option<Match<'t>> {
1139        let (a, b) = self.raw.advance()?;
1140        Some(Match {
1141            text: self.text,
1142            start: a,
1143            end: b,
1144        })
1145    }
1146}
1147
1148impl<'t> Iterator for CaptureMatches<'_, 't> {
1149    type Item = Captures<'t>;
1150    fn next(&mut self) -> Option<Captures<'t>> {
1151        self.raw.advance().map(|_| self.re.caps_from(self.text, &self.raw))
1152    }
1153}
1154
1155// ── Flags (real::flags bits) ────────────────────────────────────────────────────────────────────────────
1156const FLAG_ICASE: u32 = 1;
1157const FLAG_MULTILINE: u32 = 2;
1158const FLAG_DOTALL: u32 = 4;
1159const FLAG_VERBOSE: u32 = 16;
1160const FLAG_ASCII: u32 = 64;
1161
1162/// A builder for a [`Regex`] with readable options — the mirror of `regex::RegexBuilder`.
1163pub struct RegexBuilder {
1164    pattern: String,
1165    flags: u32,
1166    #[cfg(feature = "fallback")]
1167    fallback: bool,
1168}
1169
1170impl RegexBuilder {
1171    /// Start building from `pattern`.
1172    pub fn new(pattern: &str) -> RegexBuilder {
1173        RegexBuilder {
1174            pattern: pattern.to_string(),
1175            flags: 0,
1176            #[cfg(feature = "fallback")]
1177            fallback: false,
1178        }
1179    }
1180
1181    fn set(&mut self, bit: u32, yes: bool) -> &mut RegexBuilder {
1182        if yes { self.flags |= bit } else { self.flags &= !bit }
1183        self
1184    }
1185
1186    /// Delegate this pattern to the regex crate if REAL cannot run it linearly (requires the `fallback`
1187    /// feature). Off by default — the crate stays strict. A delegated pattern reports
1188    /// [`Engine::Fallback`](crate::Engine) and forfeits the linear-time guarantee.
1189    #[cfg(feature = "fallback")]
1190    pub fn fallback(&mut self, yes: bool) -> &mut RegexBuilder {
1191        self.fallback = yes;
1192        self
1193    }
1194
1195    /// Case-insensitive matching (ASCII; REAL's icase). Maps to `(?i)`.
1196    pub fn case_insensitive(&mut self, yes: bool) -> &mut RegexBuilder {
1197        self.set(FLAG_ICASE, yes)
1198    }
1199
1200    /// `^`/`$` match at line boundaries. Maps to `(?m)`.
1201    pub fn multi_line(&mut self, yes: bool) -> &mut RegexBuilder {
1202        self.set(FLAG_MULTILINE, yes)
1203    }
1204
1205    /// `.` matches newlines. Maps to `(?s)`.
1206    pub fn dot_matches_new_line(&mut self, yes: bool) -> &mut RegexBuilder {
1207        self.set(FLAG_DOTALL, yes)
1208    }
1209
1210    /// Verbose mode — insignificant whitespace and `#` comments. Maps to `(?x)`.
1211    pub fn ignore_whitespace(&mut self, yes: bool) -> &mut RegexBuilder {
1212        self.set(FLAG_VERBOSE, yes)
1213    }
1214
1215    /// Unicode mode. `true` (the default) keeps REAL's Unicode str semantics; `false` restricts `\w \d \s \b`
1216    /// and case folding to ASCII (REAL's `ascii` flag), mirroring `regex`'s `unicode(false)`.
1217    pub fn unicode(&mut self, yes: bool) -> &mut RegexBuilder {
1218        self.set(FLAG_ASCII, !yes)
1219    }
1220
1221    /// Accepted for API compatibility with `regex`; REAL enforces its own fixed complexity caps, so this is a
1222    /// no-op (there is no per-pattern memory budget to set).
1223    pub fn size_limit(&mut self, _bytes: usize) -> &mut RegexBuilder {
1224        self
1225    }
1226
1227    /// Compile the configured pattern.
1228    pub fn build(&self) -> Result<Regex, Error> {
1229        match Regex::with_flags(&self.pattern, self.flags) {
1230            Ok(re) => Ok(re),
1231            Err(e) => {
1232                #[cfg(feature = "fallback")]
1233                if self.fallback && e.is_unsupported() {
1234                    return Regex::build_fallback(&self.pattern, self.flags);
1235                }
1236                Err(e)
1237            }
1238        }
1239    }
1240}
1241
1242// ── Replace ─────────────────────────────────────────────────────────────────────────────────────────────
1243use std::borrow::Cow;
1244
1245/// A replacement value for [`Regex::replace`] and friends — a `&str`/`String` template (with `$0`, `$1`,
1246/// `$name`, `${name}` expansion and `$$` for a literal `$`), a [`NoExpand`] literal, or a closure
1247/// `FnMut(&Captures) -> impl AsRef<str>`.
1248pub trait Replacer {
1249    /// Append the replacement for `caps` to `dst`.
1250    fn replace_append(&mut self, caps: &Captures, dst: &mut String);
1251}
1252
1253/// A literal replacement, with no `$` expansion (mirrors `regex::NoExpand`).
1254pub struct NoExpand<'a>(pub &'a str);
1255
1256impl Replacer for NoExpand<'_> {
1257    fn replace_append(&mut self, _caps: &Captures, dst: &mut String) {
1258        dst.push_str(self.0);
1259    }
1260}
1261
1262impl Replacer for &str {
1263    fn replace_append(&mut self, caps: &Captures, dst: &mut String) {
1264        expand(caps, self, dst);
1265    }
1266}
1267
1268impl Replacer for String {
1269    fn replace_append(&mut self, caps: &Captures, dst: &mut String) {
1270        expand(caps, self, dst);
1271    }
1272}
1273
1274impl<F, T> Replacer for F
1275where
1276    F: FnMut(&Captures) -> T,
1277    T: AsRef<str>,
1278{
1279    fn replace_append(&mut self, caps: &Captures, dst: &mut String) {
1280        dst.push_str((*self)(caps).as_ref());
1281    }
1282}
1283
1284// Expand a `$`-template against caps. $$ -> $, $N / ${N} -> group N, $name / ${name} -> named group; an
1285// unknown group expands to nothing, as regex does. A `$` with no valid name following stays literal.
1286fn expand(caps: &Captures, template: &str, dst: &mut String) {
1287    let mut rest = template;
1288    while let Some(i) = rest.find('$') {
1289        dst.push_str(&rest[..i]);
1290        rest = &rest[i + 1..];
1291        if let Some(stripped) = rest.strip_prefix('$') {
1292            dst.push('$');
1293            rest = stripped;
1294            continue;
1295        }
1296        let (name, after) = if let Some(braced) = rest.strip_prefix('{') {
1297            match braced.find('}') {
1298                Some(j) => (&braced[..j], &braced[j + 1..]),
1299                None => {
1300                    dst.push('$');
1301                    ("", rest)
1302                }
1303            }
1304        } else {
1305            let end = rest.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_')).unwrap_or(rest.len());
1306            (&rest[..end], &rest[end..])
1307        };
1308        rest = after;
1309        if name.is_empty() {
1310            dst.push('$');
1311            continue;
1312        }
1313        let m = match name.parse::<usize>() {
1314            Ok(n) => caps.get(n),
1315            Err(_) => caps.name(name),
1316        };
1317        if let Some(m) = m {
1318            dst.push_str(m.as_str());
1319        }
1320    }
1321    dst.push_str(rest);
1322}
1323
1324impl Regex {
1325    /// Replace the leftmost match in `text` with `rep`. If there is no match, `text` is returned unchanged
1326    /// (borrowed).
1327    pub fn replace<'t, R: Replacer>(&self, text: &'t str, rep: R) -> Cow<'t, str> {
1328        self.replacen(text, 1, rep)
1329    }
1330
1331    /// Replace every non-overlapping match in `text` with `rep`.
1332    pub fn replace_all<'t, R: Replacer>(&self, text: &'t str, rep: R) -> Cow<'t, str> {
1333        self.replacen(text, 0, rep)
1334    }
1335
1336    /// Replace at most `limit` matches (`0` means all).
1337    pub fn replacen<'t, R: Replacer>(&self, text: &'t str, limit: usize, mut rep: R) -> Cow<'t, str> {
1338        let mut out: Option<String> = None;
1339        let mut last = 0;
1340        for (i, caps) in self.captures_iter(text).enumerate() {
1341            if limit != 0 && i >= limit {
1342                break;
1343            }
1344            let m = caps.get(0).unwrap();
1345            let dst = out.get_or_insert_with(|| String::with_capacity(text.len()));
1346            dst.push_str(&text[last..m.start()]);
1347            rep.replace_append(&caps, dst);
1348            last = m.end();
1349        }
1350        match out {
1351            Some(mut dst) => {
1352                dst.push_str(&text[last..]);
1353                Cow::Owned(dst)
1354            }
1355            None => Cow::Borrowed(text),
1356        }
1357    }
1358
1359    /// Iterate the substrings of `text` delimited by matches (leading/trailing empties included), mirroring
1360    /// `regex::Regex::split`.
1361    pub fn split<'r, 't>(&'r self, text: &'t str) -> Split<'r, 't> {
1362        Split { text, it: self.find_iter(text), last: 0, done: false }
1363    }
1364
1365    /// Like [`split`](Regex::split), but yielding at most `limit` substrings (the last is the unsplit
1366    /// remainder). `limit == 0` yields nothing.
1367    pub fn splitn<'r, 't>(&'r self, text: &'t str, limit: usize) -> SplitN<'r, 't> {
1368        SplitN { inner: self.split(text), limit, n: 0 }
1369    }
1370}
1371
1372/// Iterator of the pieces between matches, from [`Regex::split`].
1373pub struct Split<'r, 't> {
1374    text: &'t str,
1375    it: Matches<'r, 't>,
1376    last: usize,
1377    done: bool,
1378}
1379
1380impl<'t> Iterator for Split<'_, 't> {
1381    type Item = &'t str;
1382    fn next(&mut self) -> Option<&'t str> {
1383        if self.done {
1384            return None;
1385        }
1386        match self.it.next() {
1387            Some(m) => {
1388                let piece = &self.text[self.last..m.start()];
1389                self.last = m.end();
1390                Some(piece)
1391            }
1392            None => {
1393                self.done = true;
1394                Some(&self.text[self.last..])
1395            }
1396        }
1397    }
1398}
1399
1400/// Iterator of at most `limit` pieces, from [`Regex::splitn`].
1401pub struct SplitN<'r, 't> {
1402    inner: Split<'r, 't>,
1403    limit: usize,
1404    n: usize,
1405}
1406
1407impl<'t> Iterator for SplitN<'_, 't> {
1408    type Item = &'t str;
1409    fn next(&mut self) -> Option<&'t str> {
1410        if self.n >= self.limit {
1411            return None;
1412        }
1413        self.n += 1;
1414        if self.n == self.limit {
1415            // Last allowed piece: the unsplit remainder from the current cursor to the end.
1416            if self.inner.done {
1417                return None;
1418            }
1419            self.inner.done = true;
1420            return Some(&self.inner.text[self.inner.last..]);
1421        }
1422        self.inner.next()
1423    }
1424}
1425
1426/// Byte-oriented regular expressions — the mirror of [`regex::bytes`], matching over `&[u8]` (which need not
1427/// be valid UTF-8). Patterns compile in REAL's raw-byte mode (`\w \d \s \b` are ASCII); every other method
1428/// mirrors the top-level string API. Group 0 is the whole match; spans are byte offsets.
1429pub mod bytes {
1430    use super::{
1431        compile_handle, real_find_iter, real_find_iter_at, real_free, CaptureLocations, Error,
1432        GroupInfo, RawSpans, RealRegex, SlotStore, FLAG_ASCII, FLAG_DOTALL, FLAG_ICASE,
1433        FLAG_MULTILINE, FLAG_VERBOSE,
1434    };
1435    use std::borrow::Cow;
1436    use std::marker::PhantomData;
1437    use std::ops::Index;
1438    use std::os::raw::c_char;
1439    use std::sync::Arc;
1440
1441    const FLAG_BYTES: u32 = 8;
1442
1443    /// A compiled byte pattern.
1444    pub struct Regex {
1445        handle: *mut RealRegex,
1446        ngroups: usize,
1447        pattern: Vec<u8>,
1448        groups: Arc<GroupInfo>,
1449    }
1450
1451    unsafe impl Send for Regex {}
1452    unsafe impl Sync for Regex {}
1453
1454    impl Regex {
1455        /// Compile `pattern` (given as text) in byte mode.
1456        pub fn new(pattern: &str) -> Result<Regex, Error> {
1457            Regex::with_flags(pattern.as_bytes(), 0)
1458        }
1459
1460        /// Compile a raw-byte pattern with extra `real::flags` (byte mode is always on).
1461        pub fn with_flags(pattern: &[u8], flags: u32) -> Result<Regex, Error> {
1462            let (handle, ngroups, groups) = compile_handle(pattern, flags | FLAG_BYTES)?;
1463            Ok(Regex { handle, ngroups, pattern: pattern.to_vec(), groups })
1464        }
1465
1466        /// The pattern bytes.
1467        pub fn as_bytes(&self) -> &[u8] {
1468            &self.pattern
1469        }
1470
1471        /// The number of capture slots, including group 0.
1472        pub fn captures_len(&self) -> usize {
1473            self.ngroups
1474        }
1475
1476        /// The name of each capture group (group 0 first), `None` for the unnamed ones.
1477        pub fn capture_names(&self) -> impl Iterator<Item = Option<&str>> {
1478            self.groups.names.iter().map(|o| o.as_deref())
1479        }
1480
1481        fn raw<'r, 't>(&'r self, text: &'t [u8], start: Option<usize>) -> RawSpans<'r, 't> {
1482            let iter = unsafe {
1483                match start {
1484                    None => real_find_iter(self.handle, text.as_ptr() as *const c_char, text.len()),
1485                    Some(s) => real_find_iter_at(self.handle, text.as_ptr() as *const c_char, text.len(), s),
1486                }
1487            };
1488            // A null cursor means the engine failed to construct the iterator (never dereference it).
1489            assert!(!iter.is_null(), "real-regex: engine iteration failed");
1490            RawSpans { iter, handle: self.handle, text, ngroups: self.ngroups, buf: vec![0usize; 2 * self.ngroups], last_end: None, drive_pos: None, utf8: false, _re: PhantomData }
1491        }
1492
1493        fn caps_from<'t>(&self, text: &'t [u8], raw: &RawSpans<'_, '_>) -> Captures<'t> {
1494            // RawSpans::buf is already the flat [start0, end0, …] representation, so this is one copy.
1495            Captures { text, slots: SlotStore::from_flat(&raw.buf), groups: Arc::clone(&self.groups) }
1496        }
1497
1498        /// Whether the pattern matches anywhere in `text`.
1499        pub fn is_match(&self, text: &[u8]) -> bool {
1500            self.raw(text, None).advance().is_some()
1501        }
1502
1503        /// The leftmost whole match, or `None`.
1504        pub fn find<'t>(&self, text: &'t [u8]) -> Option<Match<'t>> {
1505            self.raw(text, None).advance().map(|(a, b)| Match { text, start: a, end: b })
1506        }
1507
1508        /// Like [`find`](Regex::find), searching from byte offset `start`.
1509        pub fn find_at<'t>(&self, text: &'t [u8], start: usize) -> Option<Match<'t>> {
1510            self.raw(text, Some(start)).advance().map(|(a, b)| Match { text, start: a, end: b })
1511        }
1512
1513        /// Iterate whole matches.
1514        pub fn find_iter<'r, 't>(&'r self, text: &'t [u8]) -> Matches<'r, 't> {
1515            Matches { raw: self.raw(text, None), text }
1516        }
1517
1518        /// Whether the pattern matches at or after byte offset `start`.
1519        pub fn is_match_at(&self, text: &[u8], start: usize) -> bool {
1520            self.raw(text, Some(start)).advance().is_some()
1521        }
1522
1523        /// The capture groups of the leftmost match, or `None`.
1524        pub fn captures<'t>(&self, text: &'t [u8]) -> Option<Captures<'t>> {
1525            {
1526                let mut c = self.raw(text, None);
1527                c.advance().map(|_| self.caps_from(text, &c))
1528            }
1529        }
1530
1531        /// Like [`captures`](Regex::captures), searching from byte offset `start`.
1532        pub fn captures_at<'t>(&self, text: &'t [u8], start: usize) -> Option<Captures<'t>> {
1533            {
1534                let mut c = self.raw(text, Some(start));
1535                c.advance().map(|_| self.caps_from(text, &c))
1536            }
1537        }
1538
1539        /// Reusable capture-slot buffer — see [`crate::Regex::capture_locations`].
1540        pub fn capture_locations(&self) -> CaptureLocations {
1541            CaptureLocations {
1542                slots: vec![0; 2 * self.ngroups],
1543                ngroups: self.ngroups,
1544            }
1545        }
1546
1547        /// Fill `locs` with the leftmost match's groups (no per-match allocation).
1548        pub fn captures_read<'t>(
1549            &self,
1550            locs: &mut CaptureLocations,
1551            text: &'t [u8],
1552        ) -> Option<Match<'t>> {
1553            self.captures_read_at(locs, text, 0)
1554        }
1555
1556        /// Like [`captures_read`](Regex::captures_read), searching from byte offset `start`.
1557        pub fn captures_read_at<'t>(
1558            &self,
1559            locs: &mut CaptureLocations,
1560            text: &'t [u8],
1561            start: usize,
1562        ) -> Option<Match<'t>> {
1563            locs.ensure(self.ngroups);
1564            let mut c = self.raw(text, if start == 0 { None } else { Some(start) });
1565            let (a, b) = c.advance()?;
1566            locs.slots.copy_from_slice(&c.buf);
1567            Some(Match {
1568                text,
1569                start: a,
1570                end: b,
1571            })
1572        }
1573
1574        /// Iterate matches without a per-match `Captures` — see [`crate::Regex::captures_read_iter`].
1575        pub fn captures_read_iter<'r, 't>(
1576            &'r self,
1577            text: &'t [u8],
1578        ) -> CaptureLocationMatches<'r, 't> {
1579            CaptureLocationMatches {
1580                raw: self.raw(text, None),
1581                text,
1582                ngroups: self.ngroups,
1583            }
1584        }
1585
1586        /// Iterate the capture groups of each match.
1587        pub fn captures_iter<'r, 't>(&'r self, text: &'t [u8]) -> CaptureMatches<'r, 't> {
1588            CaptureMatches { raw: self.raw(text, None), re: self, text }
1589        }
1590
1591        /// The end offset of the leftmost match. Same divergence as the string API's
1592        /// [`shortest_match`](crate::Regex::shortest_match) — the leftmost match's greedy end.
1593        pub fn shortest_match(&self, text: &[u8]) -> Option<usize> {
1594            self.raw(text, None).advance().map(|(_, e)| e)
1595        }
1596
1597        /// Replace the leftmost match with `rep` (a `&[u8]`/`Vec<u8>` template with `$`-expansion, or a
1598        /// closure `FnMut(&Captures) -> impl AsRef<[u8]>`).
1599        pub fn replace<'t, R: Replacer>(&self, text: &'t [u8], rep: R) -> Cow<'t, [u8]> {
1600            self.replacen(text, 1, rep)
1601        }
1602
1603        /// Replace every non-overlapping match with `rep`.
1604        pub fn replace_all<'t, R: Replacer>(&self, text: &'t [u8], rep: R) -> Cow<'t, [u8]> {
1605            self.replacen(text, 0, rep)
1606        }
1607
1608        /// Replace at most `limit` matches (`0` = all).
1609        pub fn replacen<'t, R: Replacer>(&self, text: &'t [u8], limit: usize, mut rep: R) -> Cow<'t, [u8]> {
1610            let mut out: Option<Vec<u8>> = None;
1611            let mut last = 0;
1612            for (i, caps) in self.captures_iter(text).enumerate() {
1613                if limit != 0 && i >= limit {
1614                    break;
1615                }
1616                let m = caps.get(0).unwrap();
1617                let dst = out.get_or_insert_with(|| Vec::with_capacity(text.len()));
1618                dst.extend_from_slice(&text[last..m.start()]);
1619                rep.replace_append(&caps, dst);
1620                last = m.end();
1621            }
1622            match out {
1623                Some(mut dst) => {
1624                    dst.extend_from_slice(&text[last..]);
1625                    Cow::Owned(dst)
1626                }
1627                None => Cow::Borrowed(text),
1628            }
1629        }
1630
1631        /// Iterate the pieces of `text` delimited by matches.
1632        pub fn split<'r, 't>(&'r self, text: &'t [u8]) -> Split<'r, 't> {
1633            Split { text, it: self.find_iter(text), last: 0, done: false }
1634        }
1635
1636        /// Like [`split`](Regex::split), but yielding at most `limit` pieces (the last is the unsplit
1637        /// remainder). `limit == 0` yields nothing.
1638        pub fn splitn<'r, 't>(&'r self, text: &'t [u8], limit: usize) -> SplitN<'r, 't> {
1639            SplitN { inner: self.split(text), limit, n: 0 }
1640        }
1641    }
1642
1643    impl std::fmt::Display for Regex {
1644        /// The original pattern — the same affordance as the regex crate.
1645        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1646            match std::str::from_utf8(&self.pattern) {
1647                Ok(s) => f.write_str(s),
1648                Err(_) => f.write_str(&String::from_utf8_lossy(&self.pattern)),
1649            }
1650        }
1651    }
1652
1653    impl std::str::FromStr for Regex {
1654        type Err = Error;
1655
1656        fn from_str(s: &str) -> Result<Regex, Error> {
1657            Regex::new(s)
1658        }
1659    }
1660
1661    impl Drop for Regex {
1662        fn drop(&mut self) {
1663            unsafe { real_free(self.handle) }
1664        }
1665    }
1666
1667    /// A single byte-span match.
1668    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1669    pub struct Match<'t> {
1670        text: &'t [u8],
1671        start: usize,
1672        end: usize,
1673    }
1674
1675    impl<'t> Match<'t> {
1676        /// The start byte offset.
1677        pub fn start(&self) -> usize { self.start }
1678        /// The end byte offset.
1679        pub fn end(&self) -> usize { self.end }
1680        /// The matched bytes.
1681        pub fn as_bytes(&self) -> &'t [u8] { &self.text[self.start..self.end] }
1682        /// The byte range.
1683        pub fn range(&self) -> std::ops::Range<usize> { self.start..self.end }
1684        /// Whether the match is empty.
1685        pub fn is_empty(&self) -> bool { self.start == self.end }
1686        /// The match length in bytes.
1687        pub fn len(&self) -> usize { self.end - self.start }
1688    }
1689
1690    /// The capture groups of one byte match.
1691    pub struct Captures<'t> {
1692        text: &'t [u8],
1693        slots: SlotStore,
1694        groups: Arc<GroupInfo>,
1695    }
1696
1697    impl<'t> Captures<'t> {
1698        /// Capture group `i` (0 = whole match).
1699        pub fn get(&self, i: usize) -> Option<Match<'t>> {
1700            self.slots.group(i).map(|(s, e)| Match { text: self.text, start: s, end: e })
1701        }
1702        /// The named capture group `name`.
1703        pub fn name(&self, name: &str) -> Option<Match<'t>> {
1704            self.groups.by_name.get(name).and_then(|&i| self.get(i))
1705        }
1706        /// The number of capture slots (incl. group 0).
1707        pub fn len(&self) -> usize { self.slots.ngroups() }
1708        /// Whether there are no slots (never for a real match).
1709        pub fn is_empty(&self) -> bool { self.slots.ngroups() == 0 }
1710    }
1711
1712    impl Index<usize> for Captures<'_> {
1713        type Output = [u8];
1714        fn index(&self, i: usize) -> &[u8] {
1715            self.get(i).map(|m| m.as_bytes()).unwrap_or_else(|| panic!("no group at index {i}"))
1716        }
1717    }
1718
1719    impl Index<&str> for Captures<'_> {
1720        type Output = [u8];
1721        fn index(&self, name: &str) -> &[u8] {
1722            self.name(name).map(|m| m.as_bytes()).unwrap_or_else(|| panic!("no group named {name:?}"))
1723        }
1724    }
1725
1726    /// Iterator over whole matches.
1727    pub struct Matches<'r, 't> {
1728        raw: RawSpans<'r, 't>,
1729        text: &'t [u8],
1730    }
1731
1732    impl<'t> Iterator for Matches<'_, 't> {
1733        type Item = Match<'t>;
1734        fn next(&mut self) -> Option<Match<'t>> {
1735            self.raw.advance().map(|(a, b)| Match { text: self.text, start: a, end: b })
1736        }
1737    }
1738
1739    /// Iterator over capture groups.
1740    pub struct CaptureMatches<'r, 't> {
1741        raw: RawSpans<'r, 't>,
1742        re: &'r Regex,
1743        text: &'t [u8],
1744    }
1745
1746    /// Iterator over matches with reusable group slots — from [`Regex::captures_read_iter`].
1747    pub struct CaptureLocationMatches<'r, 't> {
1748        raw: RawSpans<'r, 't>,
1749        text: &'t [u8],
1750        ngroups: usize,
1751    }
1752
1753    impl CaptureLocationMatches<'_, '_> {
1754        /// Number of capture slots (including group 0).
1755        pub fn len(&self) -> usize {
1756            self.ngroups
1757        }
1758
1759        /// Group `i` of the current match, or `None` if unset / out of range.
1760        pub fn get(&self, i: usize) -> Option<(usize, usize)> {
1761            if i >= self.ngroups {
1762                return None;
1763            }
1764            let a = self.raw.buf[2 * i];
1765            let b = self.raw.buf[2 * i + 1];
1766            if a == usize::MAX {
1767                None
1768            } else {
1769                Some((a, b))
1770            }
1771        }
1772
1773        /// Copy the current match into `locs`.
1774        pub fn read_captures(&self, locs: &mut CaptureLocations) {
1775            locs.ensure(self.ngroups);
1776            locs.slots.copy_from_slice(&self.raw.buf);
1777        }
1778    }
1779
1780    impl<'t> Iterator for CaptureLocationMatches<'_, 't> {
1781        type Item = Match<'t>;
1782        fn next(&mut self) -> Option<Match<'t>> {
1783            let (a, b) = self.raw.advance()?;
1784            Some(Match {
1785                text: self.text,
1786                start: a,
1787                end: b,
1788            })
1789        }
1790    }
1791
1792    impl<'t> Iterator for CaptureMatches<'_, 't> {
1793        type Item = Captures<'t>;
1794        fn next(&mut self) -> Option<Captures<'t>> {
1795            self.raw.advance().map(|_| self.re.caps_from(self.text, &self.raw))
1796        }
1797    }
1798
1799    /// Iterator of the pieces between matches.
1800    pub struct Split<'r, 't> {
1801        text: &'t [u8],
1802        it: Matches<'r, 't>,
1803        last: usize,
1804        done: bool,
1805    }
1806
1807    impl<'t> Iterator for Split<'_, 't> {
1808        type Item = &'t [u8];
1809        fn next(&mut self) -> Option<&'t [u8]> {
1810            if self.done {
1811                return None;
1812            }
1813            match self.it.next() {
1814                Some(m) => {
1815                    let piece = &self.text[self.last..m.start()];
1816                    self.last = m.end();
1817                    Some(piece)
1818                }
1819                None => {
1820                    self.done = true;
1821                    Some(&self.text[self.last..])
1822                }
1823            }
1824        }
1825    }
1826
1827    /// Iterator of at most `limit` pieces, from [`Regex::splitn`].
1828    pub struct SplitN<'r, 't> {
1829        inner: Split<'r, 't>,
1830        limit: usize,
1831        n: usize,
1832    }
1833
1834    impl<'t> Iterator for SplitN<'_, 't> {
1835        type Item = &'t [u8];
1836        fn next(&mut self) -> Option<&'t [u8]> {
1837            if self.n >= self.limit {
1838                return None;
1839            }
1840            self.n += 1;
1841            if self.n == self.limit {
1842                if self.inner.done {
1843                    return None;
1844                }
1845                self.inner.done = true;
1846                return Some(&self.inner.text[self.inner.last..]);
1847            }
1848            self.inner.next()
1849        }
1850    }
1851
1852    /// A byte replacement — a `&[u8]`/`Vec<u8>` template (with `$0`/`$1`/`$name`/`${name}`/`$$`), a
1853    /// [`NoExpand`] literal, or a closure `FnMut(&Captures) -> impl AsRef<[u8]>`.
1854    pub trait Replacer {
1855        /// Append the replacement for `caps` to `dst`.
1856        fn replace_append(&mut self, caps: &Captures, dst: &mut Vec<u8>);
1857    }
1858
1859    /// A literal byte replacement, no `$` expansion.
1860    pub struct NoExpand<'a>(pub &'a [u8]);
1861
1862    impl Replacer for NoExpand<'_> {
1863        fn replace_append(&mut self, _caps: &Captures, dst: &mut Vec<u8>) {
1864            dst.extend_from_slice(self.0);
1865        }
1866    }
1867
1868    impl Replacer for &[u8] {
1869        fn replace_append(&mut self, caps: &Captures, dst: &mut Vec<u8>) {
1870            expand_bytes(caps, self, dst);
1871        }
1872    }
1873
1874    impl<F, T> Replacer for F
1875    where
1876        F: FnMut(&Captures) -> T,
1877        T: AsRef<[u8]>,
1878    {
1879        fn replace_append(&mut self, caps: &Captures, dst: &mut Vec<u8>) {
1880            dst.extend_from_slice((*self)(caps).as_ref());
1881        }
1882    }
1883
1884    // Byte-template expansion: $$ -> $, $N / ${N} -> group N, $name / ${name} -> named group; an unknown
1885    // group expands to nothing, a lone `$` stays literal — the same rules as the str expander.
1886    fn expand_bytes(caps: &Captures, template: &[u8], dst: &mut Vec<u8>) {
1887        let mut i = 0;
1888        while i < template.len() {
1889            let b = template[i];
1890            if b != b'$' {
1891                dst.push(b);
1892                i += 1;
1893                continue;
1894            }
1895            i += 1; // consume '$'
1896            if i < template.len() && template[i] == b'$' {
1897                dst.push(b'$');
1898                i += 1;
1899                continue;
1900            }
1901            let (name, next) = if i < template.len() && template[i] == b'{' {
1902                match template[i + 1..].iter().position(|&c| c == b'}') {
1903                    Some(j) => (&template[i + 1..i + 1 + j], i + 1 + j + 1),
1904                    None => {
1905                        dst.push(b'$');
1906                        continue;
1907                    }
1908                }
1909            } else {
1910                let mut j = i;
1911                while j < template.len() && (template[j].is_ascii_alphanumeric() || template[j] == b'_') {
1912                    j += 1;
1913                }
1914                (&template[i..j], j)
1915            };
1916            i = next;
1917            if name.is_empty() {
1918                dst.push(b'$');
1919                continue;
1920            }
1921            let name_str = std::str::from_utf8(name).unwrap_or("");
1922            let m = match name_str.parse::<usize>() {
1923                Ok(n) => caps.get(n),
1924                Err(_) => caps.name(name_str),
1925            };
1926            if let Some(m) = m {
1927                dst.extend_from_slice(m.as_bytes());
1928            }
1929        }
1930    }
1931
1932    /// A builder for a byte [`Regex`] — the mirror of `regex::bytes::RegexBuilder`.
1933    pub struct RegexBuilder {
1934        pattern: Vec<u8>,
1935        flags: u32,
1936    }
1937
1938    impl RegexBuilder {
1939        /// Start building from `pattern`.
1940        pub fn new(pattern: &str) -> RegexBuilder {
1941            RegexBuilder { pattern: pattern.as_bytes().to_vec(), flags: 0 }
1942        }
1943        fn set(&mut self, bit: u32, yes: bool) -> &mut RegexBuilder {
1944            if yes { self.flags |= bit } else { self.flags &= !bit }
1945            self
1946        }
1947        /// Case-insensitive matching (ASCII).
1948        pub fn case_insensitive(&mut self, yes: bool) -> &mut RegexBuilder { self.set(FLAG_ICASE, yes) }
1949        /// `^`/`$` match at line boundaries.
1950        pub fn multi_line(&mut self, yes: bool) -> &mut RegexBuilder { self.set(FLAG_MULTILINE, yes) }
1951        /// `.` matches newlines.
1952        pub fn dot_matches_new_line(&mut self, yes: bool) -> &mut RegexBuilder { self.set(FLAG_DOTALL, yes) }
1953        /// Verbose mode.
1954        pub fn ignore_whitespace(&mut self, yes: bool) -> &mut RegexBuilder { self.set(FLAG_VERBOSE, yes) }
1955        /// Unicode mode (`false` restricts `\w \d \s` to ASCII).
1956        pub fn unicode(&mut self, yes: bool) -> &mut RegexBuilder { self.set(FLAG_ASCII, !yes) }
1957        /// Accepted for API compatibility; a no-op (REAL has fixed complexity caps).
1958        pub fn size_limit(&mut self, _bytes: usize) -> &mut RegexBuilder { self }
1959        /// Compile.
1960        pub fn build(&self) -> Result<Regex, Error> {
1961            Regex::with_flags(&self.pattern, self.flags)
1962        }
1963    }
1964}