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