Skip to main content

textmate_lang/
regex.rs

1//! Validation of the Oniguruma regular-expression subset TextMate grammars
2//! may use.
3//!
4//! TextMate grammars are matched by Oniguruma (`vscode-oniguruma` in VS Code,
5//! the native library in TextMate and others). Oniguruma accepts far more than
6//! is portable, and treats some spellings differently from other engines, so
7//! this module accepts a documented subset and refuses everything else with
8//! the byte offset of the first problem. The subset is listed in
9//! `docs/API.md#regex-subset`.
10//!
11//! The checker is one left-to-right pass over the pattern with an explicit
12//! group stack, so its cost is linear in the pattern's length and it never
13//! recurses. Groups and classes nest at most [`MAX_REGEX_DEPTH`] deep.
14
15use alloc::{string::String, vec::Vec};
16use core::fmt;
17
18/// How deep groups and character classes may nest in one pattern.
19///
20/// Oniguruma's own default limit is far higher (4096); this one is low enough
21/// that every engine that loads TextMate grammars handles it, and far above
22/// what a hand-written or generated pattern needs.
23///
24/// # Examples
25///
26/// ```
27/// use textmate_lang::{MAX_REGEX_DEPTH, validate_regex};
28///
29/// let deep = "(".repeat(MAX_REGEX_DEPTH + 1) + &")".repeat(MAX_REGEX_DEPTH + 1);
30/// assert!(validate_regex(&deep).is_err());
31/// ```
32pub const MAX_REGEX_DEPTH: usize = 256;
33
34/// The largest repetition count Oniguruma accepts (`ONIG_MAX_REPEAT_NUM`).
35const MAX_REPEAT: u32 = 100_000;
36
37/// Why a pattern is outside the accepted subset, and where.
38///
39/// `offset` is the byte offset in the pattern where the problem starts; it is
40/// always on a character boundary and never past the end of the pattern.
41///
42/// # Examples
43///
44/// ```
45/// use textmate_lang::{RegexErrorKind, validate_regex};
46///
47/// let err = validate_regex("a(b|c").unwrap_err();
48/// assert_eq!(err.offset, 1);
49/// assert_eq!(err.kind, RegexErrorKind::UnclosedGroup);
50/// assert_eq!(err.to_string(), "at byte 1: unclosed group: `(` has no matching `)`");
51/// ```
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub struct RegexError {
54    /// Byte offset of the problem in the pattern.
55    pub offset: usize,
56    /// What the problem is.
57    pub kind: RegexErrorKind,
58}
59
60/// What is wrong with a pattern. See [`RegexError`].
61///
62/// Every variant names a construct that Oniguruma refuses, that some engine
63/// loading TextMate grammars refuses, or that engines read differently. The
64/// fix for each is in its documentation.
65///
66/// # Examples
67///
68/// ```
69/// use textmate_lang::{RegexErrorKind, validate_regex};
70///
71/// assert_eq!(validate_regex("x{2}?").unwrap_err().kind, RegexErrorKind::AmbiguousQuantifier);
72/// assert_eq!(validate_regex("[z-a]").unwrap_err().kind, RegexErrorKind::InvalidRange);
73/// ```
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75#[non_exhaustive]
76pub enum RegexErrorKind {
77    /// The pattern ends with a lone `\`. Write `\\` for a backslash.
78    TrailingBackslash,
79    /// An escape outside the subset: an unknown letter (`\q`, `\K`, `\R`), an
80    /// octal escape (`\0`, `\012`), `\b` inside a class (a backspace there, a
81    /// word boundary elsewhere), a back-reference inside a class, or a
82    /// backslash before a control or non-ASCII character. Write the character
83    /// itself, or `\xHH` / `\x{H...}` for a code point.
84    UnknownEscape,
85    /// `\x` is not followed by exactly two hex digits or `{` one to eight hex
86    /// digits `}`, or `\u` is not followed by exactly four hex digits.
87    InvalidHexEscape,
88    /// A hex escape names a surrogate or a value above U+10FFFF, or `\xHH`
89    /// names a byte above 7F (Oniguruma reads that as a raw UTF-8 byte). Use
90    /// `\x{HH}` for code points above 7F.
91    InvalidCodePoint,
92    /// `\p` or `\P` is not followed by `{name}` (the one-letter form `\pL` is
93    /// not portable).
94    MalformedProperty,
95    /// `\p{...}` names a property outside the supported list (Unicode
96    /// general categories, a set of scripts and binary properties, and the
97    /// POSIX classes). See `docs/API.md#regex-subset`.
98    UnknownProperty,
99    /// A `(` has no matching `)`.
100    UnclosedGroup,
101    /// A `)` has no matching `(`.
102    UnmatchedParen,
103    /// A group form outside the subset: conditionals `(?(...)`, absent
104    /// operators `(?~...)`, callouts, `(?P<...>)`, `(?'name'...)`, and the
105    /// like.
106    UnknownGroup,
107    /// An inline option other than `i`, `m`, and `x`, or an option group with
108    /// no options (`(?)`, `(?-)`).
109    UnknownOption,
110    /// A `(?#` comment has no closing `)`.
111    UnclosedComment,
112    /// A group or back-reference name is not an ASCII identifier
113    /// (`[A-Za-z_][A-Za-z0-9_]*`).
114    InvalidGroupName,
115    /// Two named groups share a name.
116    DuplicateGroupName,
117    /// The pattern has both named groups and plain capturing groups.
118    /// Oniguruma numbers plain groups only when told to (VS Code does; other
119    /// engines do not), so capture numbers would differ between editors. Use
120    /// `(?:...)` for grouping, or name every group.
121    MixedGroups,
122    /// A numbered back-reference in a pattern with named groups, which
123    /// Oniguruma refuses unless plain groups capture. Use `\k<name>`.
124    NumberedBackrefWithNamedGroups,
125    /// `\k<name>` names no group in the pattern.
126    UndefinedGroupName,
127    /// A numbered back-reference names a group the pattern does not have.
128    /// In an `end` or `while` pattern, numbered back-references name groups of
129    /// the rule's `begin` pattern.
130    BackreferenceOutOfRange {
131        /// The group the back-reference names.
132        group: u32,
133        /// How many capture groups there are to refer to.
134        groups: u32,
135    },
136    /// A `[` has no matching `]`.
137    UnclosedClass,
138    /// A character class, or one side of a `&&` intersection, is empty. `]`
139    /// is never taken literally at the start of a class: write `\]`.
140    EmptyClass,
141    /// A `]` outside a class. Write `\]` for the character.
142    UnescapedBracket,
143    /// A range in a class runs backwards (`z-a`), or a `-` sits where it
144    /// is neither the first or last item nor between two single characters.
145    /// Write `\-` for the character.
146    InvalidRange,
147    /// `[:` inside a class does not start a POSIX bracket from the supported
148    /// list (`[:alpha:]`, `[:^digit:]`, ...). Write `\[:` for the characters.
149    UnknownPosixClass,
150    /// A quantifier with nothing before it to repeat.
151    NothingToRepeat,
152    /// A quantifier directly after another (`a**`, `a{2}{3}`). Oniguruma reads
153    /// these as nested repetition, other engines refuse them; group the inner
154    /// repetition explicitly.
155    RepeatedQuantifier,
156    /// `{n}?`, `{n}+`, or `{n,m}+`. Oniguruma reads `x{n}?` as an optional
157    /// `x{n}` and `x{n,m}+` as a repeated `x{n,m}`, where other engines read
158    /// lazy and possessive repetition. Group explicitly: `(?:x{n})?`.
159    AmbiguousQuantifier,
160    /// A `{` that does not start a valid `{n}`, `{n,}`, or `{n,m}`
161    /// quantifier, a count above 100000, or `{n,m}` with `n > m`. Write `\{`
162    /// for the character; write `{0,m}` for `{,m}`.
163    InvalidInterval,
164    /// A quantifier applied to an anchor or a look-around, which Oniguruma
165    /// refuses or ignores.
166    QuantifiedAssertion,
167    /// A look-behind alternative whose length is not fixed. Each top-level
168    /// alternative of a look-behind must match a fixed number of characters,
169    /// and alternatives of groups inside it must all have the same length.
170    LookbehindNotFixed,
171    /// A capturing group or a back-reference inside a look-behind.
172    CaptureInLookbehind,
173    /// Groups and classes nest more than [`MAX_REGEX_DEPTH`] deep.
174    TooDeep,
175    /// A raw NUL character; engines that read patterns as C strings stop
176    /// there. Write `\x00`.
177    NulCharacter,
178    /// In an `end` or `while` pattern, an escaped backslash followed by a
179    /// digit (`\\1`). VS Code substitutes `begin` captures into these
180    /// patterns by searching the text for a backslash and digits, and would
181    /// treat the second backslash as a back-reference. Write `\x5c1`.
182    BackslashBeforeDigit,
183    /// More capture groups than fit in a `u32`.
184    TooManyGroups,
185}
186
187impl fmt::Display for RegexErrorKind {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        let text = match self {
190            Self::TrailingBackslash => "the pattern ends with a lone `\\`",
191            Self::UnknownEscape => "escape outside the supported subset",
192            Self::InvalidHexEscape => {
193                "`\\x` needs two hex digits or `{1-8 hex digits}`; `\\u` needs four hex digits"
194            }
195            Self::InvalidCodePoint => {
196                "code point is a surrogate, above U+10FFFF, or a `\\xHH` byte above 7F"
197            }
198            Self::MalformedProperty => "`\\p` and `\\P` must be followed by `{name}`",
199            Self::UnknownProperty => "unsupported Unicode property name",
200            Self::UnclosedGroup => "unclosed group: `(` has no matching `)`",
201            Self::UnmatchedParen => "unmatched `)`",
202            Self::UnknownGroup => "group syntax outside the supported subset",
203            Self::UnknownOption => "inline options must be `i`, `m`, or `x`",
204            Self::UnclosedComment => "unclosed `(?#` comment",
205            Self::InvalidGroupName => "group names must be ASCII identifiers",
206            Self::DuplicateGroupName => "two named groups share a name",
207            Self::MixedGroups => {
208                "named and unnamed capturing groups are mixed; use `(?:...)` or name every group"
209            }
210            Self::NumberedBackrefWithNamedGroups => {
211                "numbered back-reference in a pattern with named groups; use `\\k<name>`"
212            }
213            Self::UndefinedGroupName => "back-reference to an undefined group name",
214            Self::BackreferenceOutOfRange { group, groups } => {
215                return write!(
216                    f,
217                    "back-reference to group {group}, but the highest group it can name is {groups}"
218                );
219            }
220            Self::UnclosedClass => "unclosed class: `[` has no matching `]`",
221            Self::EmptyClass => "empty character class (write `\\]` for a literal `]`)",
222            Self::UnescapedBracket => "unbalanced `]`; write `\\]` for the character",
223            Self::InvalidRange => "invalid range in class; write `\\-` for a literal `-`",
224            Self::UnknownPosixClass => "unsupported POSIX bracket expression",
225            Self::NothingToRepeat => "quantifier with nothing to repeat",
226            Self::RepeatedQuantifier => "quantifier follows another quantifier",
227            Self::AmbiguousQuantifier => {
228                "`{n}?`, `{n}+`, and `{n,m}+` mean different things in different engines"
229            }
230            Self::InvalidInterval => "invalid `{n,m}` quantifier; write `\\{` for a literal `{`",
231            Self::QuantifiedAssertion => "quantifier applied to an anchor or look-around",
232            Self::LookbehindNotFixed => "look-behind alternative does not have a fixed length",
233            Self::CaptureInLookbehind => "capture group or back-reference inside a look-behind",
234            Self::TooDeep => "groups and classes nest too deeply",
235            Self::NulCharacter => "raw NUL character; write `\\x00`",
236            Self::BackslashBeforeDigit => {
237                "`\\\\` followed by a digit in an `end`/`while` pattern; write `\\x5c`"
238            }
239            Self::TooManyGroups => "too many capture groups",
240        };
241        f.write_str(text)
242    }
243}
244
245impl fmt::Display for RegexError {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        write!(f, "at byte {}: {}", self.offset, self.kind)
248    }
249}
250
251impl core::error::Error for RegexError {}
252
253/// Checks that `pattern` is in the accepted Oniguruma subset and returns its
254/// number of capture groups.
255///
256/// This is the check every regular expression in a grammar passes before it
257/// is emitted, available on its own for patterns built elsewhere. Numbered
258/// back-references must name a group of the pattern itself. (In an `end` or
259/// `while` pattern of a rule they name groups of the rule's `begin` pattern
260/// instead; [`Grammar`](crate::Grammar) validation handles that case.)
261///
262/// | Parameter | Meaning |
263/// |---|---|
264/// | `pattern` | An Oniguruma regular expression, as it will appear in the grammar. |
265///
266/// # Errors
267///
268/// A [`RegexError`] with the byte offset and kind of the first construct
269/// outside the subset.
270///
271/// # Examples
272///
273/// ```
274/// use textmate_lang::validate_regex;
275///
276/// assert_eq!(validate_regex(r"\b(fn)\s+([a-z_]\w*)"), Ok(2));
277/// assert_eq!(validate_regex(r"(?<=\.)\h+(?!\w)"), Ok(0));
278/// assert_eq!(validate_regex(r#"(?x) "  # a quote, with a comment"#), Ok(0));
279///
280/// let err = validate_regex(r"(a)\2").unwrap_err();
281/// assert_eq!(err.offset, 3);
282/// assert_eq!(err.to_string(), "at byte 3: back-reference to group 2, but the highest group it can name is 1");
283/// ```
284pub fn validate_regex(pattern: &str) -> Result<u32, RegexError> {
285    check(pattern, Backrefs::Own).map(|info| info.groups)
286}
287
288/// Escapes `text` so that it is a regular expression matching exactly
289/// `text`, in every context a grammar uses: plain and extended (`(?x)`) mode,
290/// and `end`/`while` patterns.
291///
292/// ASCII letters, digits, `_`, and non-ASCII characters are copied; every
293/// other printable ASCII character is preceded by `\`; control characters
294/// become `\xHH`. A backslash becomes `\x5c` rather than `\\`, so that no
295/// concatenation of escaped text, with itself or with digits, can put an
296/// escaped backslash before a digit (see
297/// [`RegexErrorKind::BackslashBeforeDigit`]). The result always passes
298/// [`validate_regex`] with zero groups.
299///
300/// | Parameter | Meaning |
301/// |---|---|
302/// | `text` | Literal text, such as a keyword or an operator. |
303///
304/// # Examples
305///
306/// ```
307/// use textmate_lang::{escape_regex, validate_regex};
308///
309/// assert_eq!(escape_regex("a+b"), r"a\+b");
310/// assert_eq!(escape_regex("<=>"), r"\<\=\>");
311/// assert_eq!(escape_regex("tab\there"), r"tab\x09here");
312/// let ops = ["+", "-", "*", "**", "?."];
313/// let alternation: Vec<String> = ops.iter().map(|op| escape_regex(op)).collect();
314/// assert_eq!(validate_regex(&alternation.join("|")), Ok(0));
315/// ```
316#[must_use]
317pub fn escape_regex(text: &str) -> String {
318    let mut out = String::with_capacity(text.len() + text.len() / 2);
319    for c in text.chars() {
320        match c {
321            'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => out.push(c),
322            '\\' => out.push_str("\\x5c"),
323            ' '..='~' => {
324                out.push('\\');
325                out.push(c);
326            }
327            '\0'..='\u{1f}' | '\u{7f}' => {
328                // Both arms are ASCII, so the cast is exact.
329                let byte = c as u8;
330                out.push_str("\\x");
331                out.push(hex_digit(byte >> 4));
332                out.push(hex_digit(byte & 0x0F));
333            }
334            _ => out.push(c),
335        }
336    }
337    out
338}
339
340fn hex_digit(nibble: u8) -> char {
341    char::from(b"0123456789abcdef"[usize::from(nibble & 0x0F)])
342}
343
344/// What numbered back-references refer to.
345#[derive(Clone, Copy, PartialEq, Eq, Debug)]
346pub(crate) enum Backrefs {
347    /// Groups of the pattern itself.
348    Own,
349    /// Groups of the rule's `begin` pattern: this is an `end` or `while`
350    /// pattern, into which VS Code substitutes the captured text.
351    Begin,
352}
353
354/// What a successful check learned about a pattern.
355#[derive(Clone, Copy, Debug)]
356pub(crate) struct Info {
357    /// Number of capture groups.
358    pub(crate) groups: u32,
359    /// In [`Backrefs::Begin`] mode, the largest numbered back-reference and
360    /// its offset; the caller checks it against the `begin` pattern.
361    pub(crate) max_backref: Option<(u32, usize)>,
362}
363
364/// Checks `pattern` against the subset.
365pub(crate) fn check(pattern: &str, backrefs: Backrefs) -> Result<Info, RegexError> {
366    check_in(pattern, backrefs, &mut Buffers::default())
367}
368
369/// Working memory for [`check_in`], reused from one pattern to the next so
370/// that checking a grammar's patterns does not allocate per pattern.
371#[derive(Default)]
372pub(crate) struct Buffers {
373    frames: Vec<Frame>,
374}
375
376/// [`check`] with reused working memory.
377pub(crate) fn check_in(
378    pattern: &str,
379    backrefs: Backrefs,
380    buffers: &mut Buffers,
381) -> Result<Info, RegexError> {
382    let mut parser = Parser::new(pattern, backrefs);
383    parser.frames = core::mem::take(&mut buffers.frames);
384    parser.frames.clear();
385    let result = parser.run();
386    buffers.frames = parser.frames;
387    result
388}
389
390/// A fixed length in characters, or `None` for a variable one.
391type Len = Option<u32>;
392
393#[derive(Clone, Copy, PartialEq, Eq, Debug)]
394enum Kind {
395    Root,
396    Capture,
397    Named,
398    NonCapture,
399    Atomic,
400    Lookahead,
401    Lookbehind,
402    /// The rest of the enclosing group after an isolated `(?imx)`, which
403    /// Oniguruma parses as if it were `(?imx:...)` up to the enclosing `)`.
404    Implicit,
405}
406
407/// What precedes the current position in the current alternative, which is
408/// what a quantifier would apply to.
409#[derive(Clone, Copy, Debug)]
410enum Last {
411    /// Nothing: the start of a group or an alternative.
412    Nothing,
413    /// An atom of this length, not yet quantified.
414    Atom(Len),
415    /// A quantified atom of this length.
416    Quantified(Len),
417    /// An anchor or look-around.
418    Assertion,
419}
420
421#[derive(Clone, Copy, Debug)]
422struct Frame {
423    kind: Kind,
424    /// Offset of the `(` (0 for the root).
425    open: usize,
426    /// Whether extended mode is on inside this group.
427    extended: bool,
428    /// Length of the current alternative, the pending atom excluded.
429    alt: Len,
430    /// Length of the first finished alternative; `None` until there is one.
431    first_alt: Option<Len>,
432    /// Whether every finished alternative had the first one's length.
433    same: bool,
434    last: Last,
435}
436
437impl Frame {
438    fn new(kind: Kind, open: usize, extended: bool) -> Frame {
439        Frame {
440            kind,
441            open,
442            extended,
443            alt: Some(0),
444            first_alt: None,
445            same: true,
446            last: Last::Nothing,
447        }
448    }
449
450    /// Adds the pending atom to the alternative's length.
451    fn flush(&mut self) {
452        let pending = match self.last {
453            Last::Atom(len) | Last::Quantified(len) => len,
454            Last::Nothing | Last::Assertion => Some(0),
455        };
456        self.alt = match (self.alt, pending) {
457            (Some(a), Some(b)) => a.checked_add(b),
458            _ => None,
459        };
460        self.last = Last::Nothing;
461    }
462
463    /// Ends the current alternative; returns its length.
464    fn end_alternative(&mut self) -> Len {
465        self.flush();
466        let len = self.alt;
467        match self.first_alt {
468            None => self.first_alt = Some(len),
469            Some(first) => self.same &= first == len,
470        }
471        self.alt = Some(0);
472        len
473    }
474
475    /// The group's length once all alternatives are finished.
476    fn length(&self) -> Len {
477        match self.first_alt {
478            Some(len) if self.same => len,
479            _ => None,
480        }
481    }
482}
483
484struct Parser<'p> {
485    text: &'p str,
486    bytes: &'p [u8],
487    pos: usize,
488    backrefs: Backrefs,
489    frames: Vec<Frame>,
490    lookbehinds: usize,
491    groups: u32,
492    first_unnamed: Option<usize>,
493    first_named: Option<usize>,
494    /// Named groups and where they start.
495    names: Vec<(&'p str, usize)>,
496    /// `\k<name>` references and where they start.
497    named_refs: Vec<(&'p str, usize)>,
498    /// The largest numbered back-reference and its offset.
499    max_backref: Option<(u32, usize)>,
500    first_backref: Option<usize>,
501}
502
503impl<'p> Parser<'p> {
504    fn new(text: &'p str, backrefs: Backrefs) -> Parser<'p> {
505        Parser {
506            text,
507            bytes: text.as_bytes(),
508            pos: 0,
509            backrefs,
510            frames: Vec::new(),
511            lookbehinds: 0,
512            groups: 0,
513            first_unnamed: None,
514            first_named: None,
515            names: Vec::new(),
516            named_refs: Vec::new(),
517            max_backref: None,
518            first_backref: None,
519        }
520    }
521
522    fn err<T>(offset: usize, kind: RegexErrorKind) -> Result<T, RegexError> {
523        Err(RegexError { offset, kind })
524    }
525
526    fn peek(&self) -> Option<u8> {
527        self.bytes.get(self.pos).copied()
528    }
529
530    fn peek_at(&self, ahead: usize) -> Option<u8> {
531        self.bytes.get(self.pos.saturating_add(ahead)).copied()
532    }
533
534    fn top(&mut self) -> &mut Frame {
535        // The root frame is pushed first and never popped, so the stack is
536        // not empty; the push only guards the index below.
537        if self.frames.is_empty() {
538            self.frames.push(Frame::new(Kind::Root, 0, false));
539        }
540        let last = self.frames.len() - 1;
541        &mut self.frames[last]
542    }
543
544    fn extended(&self) -> bool {
545        self.frames.last().is_some_and(|f| f.extended)
546    }
547
548    fn depth_ok(&self, offset: usize, classes: usize) -> Result<(), RegexError> {
549        // The root frame does not count as nesting.
550        if self.frames.len().saturating_sub(1).saturating_add(classes) > MAX_REGEX_DEPTH {
551            return Self::err(offset, RegexErrorKind::TooDeep);
552        }
553        Ok(())
554    }
555
556    fn run(&mut self) -> Result<Info, RegexError> {
557        self.frames.push(Frame::new(Kind::Root, 0, false));
558        loop {
559            if self.extended() {
560                self.skip_extended();
561            }
562            let Some(b) = self.peek() else { break };
563            let at = self.pos;
564            match b {
565                b'(' => self.open_group()?,
566                b')' => self.close_paren()?,
567                b'|' => {
568                    self.pos += 1;
569                    self.alternative()?;
570                }
571                b'[' => {
572                    self.class()?;
573                    self.atom(Some(1));
574                }
575                b']' => return Self::err(at, RegexErrorKind::UnescapedBracket),
576                b'{' => self.interval()?,
577                b'*' | b'+' | b'?' => {
578                    self.pos += 1;
579                    self.modifier_after_simple();
580                    self.quantify(at, None)?;
581                }
582                b'^' | b'$' => {
583                    self.pos += 1;
584                    self.assertion();
585                }
586                b'.' => {
587                    self.pos += 1;
588                    self.atom(Some(1));
589                }
590                b'\\' => self.escape()?,
591                0 => return Self::err(at, RegexErrorKind::NulCharacter),
592                _ => {
593                    self.pos += char_len(b);
594                    self.atom(Some(1));
595                }
596            }
597        }
598        // Isolated option groups end with the pattern.
599        while self.frames.last().is_some_and(|f| f.kind == Kind::Implicit) {
600            self.close_frame()?;
601        }
602        if let Some(frame) = self.frames.last() {
603            if frame.kind != Kind::Root {
604                return Self::err(frame.open, RegexErrorKind::UnclosedGroup);
605            }
606        }
607        self.finish()
608    }
609
610    /// Whole-pattern checks that need every group seen.
611    fn finish(&mut self) -> Result<Info, RegexError> {
612        if let (Some(named), Some(unnamed)) = (self.first_named, self.first_unnamed) {
613            return Self::err(named.max(unnamed), RegexErrorKind::MixedGroups);
614        }
615        if self.backrefs == Backrefs::Own {
616            if let (Some(_), Some(at)) = (self.first_named, self.first_backref) {
617                return Self::err(at, RegexErrorKind::NumberedBackrefWithNamedGroups);
618            }
619            if let Some((group, at)) = self.max_backref {
620                if group > self.groups {
621                    return Self::err(
622                        at,
623                        RegexErrorKind::BackreferenceOutOfRange {
624                            group,
625                            groups: self.groups,
626                        },
627                    );
628                }
629            }
630        }
631        self.names.sort_unstable();
632        for pair in self.names.windows(2) {
633            if pair[0].0 == pair[1].0 {
634                return Self::err(pair[0].1.max(pair[1].1), RegexErrorKind::DuplicateGroupName);
635            }
636        }
637        for &(name, at) in &self.named_refs {
638            if self
639                .names
640                .binary_search_by(|&(probe, _)| probe.cmp(name))
641                .is_err()
642            {
643                return Self::err(at, RegexErrorKind::UndefinedGroupName);
644            }
645        }
646        Ok(Info {
647            groups: self.groups,
648            max_backref: match self.backrefs {
649                Backrefs::Own => None,
650                Backrefs::Begin => self.max_backref,
651            },
652        })
653    }
654
655    /// Skips whitespace and `#` comments in extended mode.
656    fn skip_extended(&mut self) {
657        while let Some(b) = self.peek() {
658            match b {
659                b' ' | b'\t' | b'\n' | b'\r' | 0x0B | 0x0C => self.pos += 1,
660                b'#' => {
661                    while let Some(c) = self.peek() {
662                        self.pos += 1;
663                        if c == b'\n' {
664                            break;
665                        }
666                    }
667                }
668                _ => break,
669            }
670        }
671    }
672
673    fn atom(&mut self, len: Len) {
674        let top = self.top();
675        top.flush();
676        top.last = Last::Atom(len);
677    }
678
679    fn assertion(&mut self) {
680        let top = self.top();
681        top.flush();
682        top.last = Last::Assertion;
683    }
684
685    /// Consumes a lazy `?` or possessive `+` after `*`, `+`, or `?`.
686    fn modifier_after_simple(&mut self) {
687        if matches!(self.peek(), Some(b'?' | b'+')) {
688            self.pos += 1;
689        }
690    }
691
692    /// Applies a quantifier at `at` to the pending atom. `fixed` is the
693    /// repetition count of a `{n}` or `{n,n}` quantifier.
694    fn quantify(&mut self, at: usize, fixed: Option<u32>) -> Result<(), RegexError> {
695        let top = self.top();
696        let len = match top.last {
697            Last::Nothing => return Self::err(at, RegexErrorKind::NothingToRepeat),
698            Last::Assertion => return Self::err(at, RegexErrorKind::QuantifiedAssertion),
699            Last::Quantified(_) => return Self::err(at, RegexErrorKind::RepeatedQuantifier),
700            Last::Atom(len) => len,
701        };
702        let len = match (len, fixed) {
703            (Some(len), Some(count)) => len.checked_mul(count),
704            _ => None,
705        };
706        top.last = Last::Quantified(len);
707        Ok(())
708    }
709
710    /// `{n}`, `{n,}`, or `{n,m}`.
711    fn interval(&mut self) -> Result<(), RegexError> {
712        let at = self.pos;
713        self.pos += 1;
714        let min = self.number().ok_or(RegexError {
715            offset: at,
716            kind: RegexErrorKind::InvalidInterval,
717        })?;
718        let max = if self.peek() == Some(b',') {
719            self.pos += 1;
720            if self.peek() == Some(b'}') {
721                None
722            } else {
723                Some(self.number().ok_or(RegexError {
724                    offset: at,
725                    kind: RegexErrorKind::InvalidInterval,
726                })?)
727            }
728        } else {
729            Some(min)
730        };
731        let exact = self
732            .bytes
733            .get(at..self.pos)
734            .is_some_and(|s| !s.contains(&b','));
735        if self.peek() != Some(b'}')
736            || min > MAX_REPEAT
737            || max.is_some_and(|m| m > MAX_REPEAT || m < min)
738        {
739            return Self::err(at, RegexErrorKind::InvalidInterval);
740        }
741        self.pos += 1;
742        match self.peek() {
743            Some(b'+') => return Self::err(at, RegexErrorKind::AmbiguousQuantifier),
744            Some(b'?') if exact => return Self::err(at, RegexErrorKind::AmbiguousQuantifier),
745            Some(b'?') => self.pos += 1,
746            _ => {}
747        }
748        let fixed = match max {
749            Some(max) if max == min => Some(min),
750            _ => None,
751        };
752        self.quantify(at, fixed)
753    }
754
755    /// A decimal number of at most six digits (anything longer is above
756    /// [`MAX_REPEAT`] and refused by the caller), or `None` if no digit.
757    fn number(&mut self) -> Option<u32> {
758        let start = self.pos;
759        let mut value: u32 = 0;
760        while let Some(d @ b'0'..=b'9') = self.peek() {
761            value = value.saturating_mul(10).saturating_add(u32::from(d - b'0'));
762            self.pos += 1;
763        }
764        (self.pos > start).then_some(value)
765    }
766
767    /// `|`: each top-level alternative of a look-behind must be fixed.
768    fn alternative(&mut self) -> Result<(), RegexError> {
769        let top = self.top();
770        let (open, kind) = (top.open, top.kind);
771        if top.end_alternative().is_none() && kind == Kind::Lookbehind {
772            return Self::err(open, RegexErrorKind::LookbehindNotFixed);
773        }
774        Ok(())
775    }
776
777    fn open_group(&mut self) -> Result<(), RegexError> {
778        let at = self.pos;
779        self.depth_ok(at, 1)?;
780        let extended = self.extended();
781        self.pos += 1;
782        if self.peek() != Some(b'?') {
783            return self.push_capture(at, Kind::Capture, extended);
784        }
785        self.pos += 1;
786        let kind = match self.peek() {
787            Some(b':') => Kind::NonCapture,
788            Some(b'=' | b'!') => Kind::Lookahead,
789            Some(b'>') => Kind::Atomic,
790            Some(b'<') => match self.peek_at(1) {
791                Some(b'=' | b'!') => {
792                    self.pos += 1;
793                    Kind::Lookbehind
794                }
795                _ => {
796                    self.pos += 1;
797                    let name = self.group_name(at)?;
798                    if self.peek() != Some(b'>') {
799                        return Self::err(at, RegexErrorKind::InvalidGroupName);
800                    }
801                    self.pos += 1;
802                    self.names.push((name, at));
803                    return self.push_capture(at, Kind::Named, extended);
804                }
805            },
806            Some(b'#') => return self.comment(at),
807            Some(b'i' | b'm' | b'x' | b'-') => return self.options(at, extended),
808            _ => return Self::err(at, RegexErrorKind::UnknownGroup),
809        };
810        self.pos += 1;
811        if kind == Kind::Lookbehind {
812            self.lookbehinds += 1;
813        }
814        self.push_frame(at, kind, extended);
815        Ok(())
816    }
817
818    fn push_frame(&mut self, at: usize, kind: Kind, extended: bool) {
819        self.top().flush();
820        self.frames.push(Frame::new(kind, at, extended));
821    }
822
823    fn push_capture(&mut self, at: usize, kind: Kind, extended: bool) -> Result<(), RegexError> {
824        if self.lookbehinds > 0 {
825            return Self::err(at, RegexErrorKind::CaptureInLookbehind);
826        }
827        self.groups = self.groups.checked_add(1).ok_or(RegexError {
828            offset: at,
829            kind: RegexErrorKind::TooManyGroups,
830        })?;
831        let first = if kind == Kind::Named {
832            &mut self.first_named
833        } else {
834            &mut self.first_unnamed
835        };
836        if first.is_none() {
837            *first = Some(at);
838        }
839        self.push_frame(at, kind, extended);
840        Ok(())
841    }
842
843    /// An ASCII identifier, ending at the current position's terminator.
844    fn group_name(&mut self, at: usize) -> Result<&'p str, RegexError> {
845        let start = self.pos;
846        while let Some(b) = self.peek() {
847            let ok =
848                b == b'_' || b.is_ascii_alphabetic() || (self.pos > start && b.is_ascii_digit());
849            if !ok {
850                break;
851            }
852            self.pos += 1;
853        }
854        if self.pos == start {
855            return Self::err(at, RegexErrorKind::InvalidGroupName);
856        }
857        Ok(self.text.get(start..self.pos).unwrap_or(""))
858    }
859
860    /// `(?#...)`: skipped entirely, like Oniguruma's tokenizer does. A
861    /// backslash escapes the next character.
862    fn comment(&mut self, at: usize) -> Result<(), RegexError> {
863        self.pos += 1;
864        while let Some(b) = self.peek() {
865            match b {
866                b'\\' => self.pos += 1 + self.peek_at(1).map_or(0, char_len),
867                b')' => {
868                    self.pos += 1;
869                    return Ok(());
870                }
871                _ => self.pos += char_len(b),
872            }
873        }
874        Self::err(at, RegexErrorKind::UnclosedComment)
875    }
876
877    /// `(?imx-imx)` or `(?imx-imx:...)`.
878    fn options(&mut self, at: usize, extended: bool) -> Result<(), RegexError> {
879        let mut on = true;
880        let mut letters = 0;
881        let mut negated_letters = 0;
882        let mut mode = extended;
883        loop {
884            match self.peek() {
885                Some(b'-') if on => on = false,
886                Some(c @ (b'i' | b'm' | b'x')) => {
887                    if c == b'x' {
888                        mode = on;
889                    }
890                    letters += 1;
891                    if !on {
892                        negated_letters += 1;
893                    }
894                }
895                Some(b')') | Some(b':') => break,
896                _ => return Self::err(at, RegexErrorKind::UnknownOption),
897            }
898            self.pos += 1;
899        }
900        if letters == 0 || (!on && negated_letters == 0) {
901            return Self::err(at, RegexErrorKind::UnknownOption);
902        }
903        let colon = self.peek() == Some(b':');
904        self.pos += 1;
905        if colon {
906            self.push_frame(at, Kind::NonCapture, mode);
907        } else {
908            // The options apply to the rest of the enclosing group, which
909            // Oniguruma parses as a group of its own.
910            self.push_frame(at, Kind::Implicit, mode);
911        }
912        Ok(())
913    }
914
915    fn close_paren(&mut self) -> Result<(), RegexError> {
916        let at = self.pos;
917        while self.frames.last().is_some_and(|f| f.kind == Kind::Implicit) {
918            self.close_frame()?;
919        }
920        if self.frames.last().is_none_or(|f| f.kind == Kind::Root) {
921            return Self::err(at, RegexErrorKind::UnmatchedParen);
922        }
923        self.pos += 1;
924        self.close_frame()
925    }
926
927    /// Pops the innermost group and records it in its parent.
928    fn close_frame(&mut self) -> Result<(), RegexError> {
929        let Some(mut frame) = self.frames.pop() else {
930            return Ok(());
931        };
932        let last_len = frame.end_alternative();
933        if frame.kind == Kind::Lookbehind {
934            if last_len.is_none() {
935                return Self::err(frame.open, RegexErrorKind::LookbehindNotFixed);
936            }
937            self.lookbehinds = self.lookbehinds.saturating_sub(1);
938        }
939        let len = frame.length();
940        let parent = self.top();
941        // A group whose alternatives differ in length is variable, which
942        // makes an enclosing look-behind alternative variable too.
943        parent.last = match frame.kind {
944            Kind::Lookahead | Kind::Lookbehind => Last::Assertion,
945            _ => Last::Atom(len),
946        };
947        Ok(())
948    }
949
950    fn escape(&mut self) -> Result<(), RegexError> {
951        let at = self.pos;
952        let Some(c) = self.peek_at(1) else {
953            return Self::err(at, RegexErrorKind::TrailingBackslash);
954        };
955        match c {
956            b'A' | b'z' | b'Z' | b'G' | b'b' | b'B' => {
957                self.pos += 2;
958                self.assertion();
959            }
960            b'1'..=b'9' => self.backref(at)?,
961            b'k' => self.named_backref(at)?,
962            b'w' | b'W' | b'd' | b'D' | b's' | b'S' | b'h' | b'H' => {
963                self.pos += 2;
964                self.atom(Some(1));
965            }
966            b'p' | b'P' => {
967                self.property(at)?;
968                self.atom(Some(1));
969            }
970            _ => {
971                let _char = self.char_escape(at)?;
972                self.atom(Some(1));
973            }
974        }
975        Ok(())
976    }
977
978    fn backref(&mut self, at: usize) -> Result<(), RegexError> {
979        self.pos += 1;
980        let group = self.number().unwrap_or(0);
981        // `number` saturates at u32::MAX, which no pattern has as many groups.
982        if self.lookbehinds > 0 {
983            return Self::err(at, RegexErrorKind::CaptureInLookbehind);
984        }
985        if self.first_backref.is_none() {
986            self.first_backref = Some(at);
987        }
988        if self.max_backref.is_none_or(|(max, _)| group > max) {
989            self.max_backref = Some((group, at));
990        }
991        self.atom(None);
992        Ok(())
993    }
994
995    fn named_backref(&mut self, at: usize) -> Result<(), RegexError> {
996        self.pos += 2;
997        if self.peek() != Some(b'<') {
998            return Self::err(at, RegexErrorKind::InvalidGroupName);
999        }
1000        self.pos += 1;
1001        let name = self.group_name(at)?;
1002        if self.peek() != Some(b'>') {
1003            return Self::err(at, RegexErrorKind::InvalidGroupName);
1004        }
1005        self.pos += 1;
1006        if self.lookbehinds > 0 {
1007            return Self::err(at, RegexErrorKind::CaptureInLookbehind);
1008        }
1009        self.named_refs.push((name, at));
1010        self.atom(None);
1011        Ok(())
1012    }
1013
1014    /// `\p{Name}`, `\p{^Name}`, `\P{Name}`.
1015    fn property(&mut self, at: usize) -> Result<(), RegexError> {
1016        self.pos += 2;
1017        if self.peek() != Some(b'{') {
1018            return Self::err(at, RegexErrorKind::MalformedProperty);
1019        }
1020        self.pos += 1;
1021        if self.peek() == Some(b'^') {
1022            self.pos += 1;
1023        }
1024        let start = self.pos;
1025        while let Some(b) = self.peek() {
1026            if b == b'}' {
1027                break;
1028            }
1029            if !(b.is_ascii_alphanumeric() || matches!(b, b'_' | b' ' | b'-')) {
1030                return Self::err(at, RegexErrorKind::MalformedProperty);
1031            }
1032            self.pos += 1;
1033        }
1034        if self.peek() != Some(b'}') {
1035            return Self::err(at, RegexErrorKind::MalformedProperty);
1036        }
1037        let name = self.bytes.get(start..self.pos).unwrap_or(&[]);
1038        self.pos += 1;
1039        if known_property(name) {
1040            Ok(())
1041        } else {
1042            Self::err(at, RegexErrorKind::UnknownProperty)
1043        }
1044    }
1045
1046    /// An escape that stands for one character: control escapes, hex
1047    /// escapes, and escaped ASCII punctuation. Returns the character.
1048    fn char_escape(&mut self, at: usize) -> Result<char, RegexError> {
1049        let Some(c) = self.peek_at(1) else {
1050            return Self::err(at, RegexErrorKind::TrailingBackslash);
1051        };
1052        let simple = match c {
1053            b't' => Some('\t'),
1054            b'n' => Some('\n'),
1055            b'r' => Some('\r'),
1056            b'f' => Some('\u{c}'),
1057            b'v' => Some('\u{b}'),
1058            b'a' => Some('\u{7}'),
1059            b'e' => Some('\u{1b}'),
1060            _ => None,
1061        };
1062        if let Some(ch) = simple {
1063            self.pos += 2;
1064            return Ok(ch);
1065        }
1066        match c {
1067            b'x' => self.hex_escape(at),
1068            b'u' => self.unicode_escape(at),
1069            b'0'..=b'9' => Self::err(at, RegexErrorKind::UnknownEscape),
1070            b'\\'
1071                if self.backrefs == Backrefs::Begin
1072                    && self.peek_at(2).is_some_and(|d| d.is_ascii_digit()) =>
1073            {
1074                Self::err(at, RegexErrorKind::BackslashBeforeDigit)
1075            }
1076            0x21..=0x2F | 0x3A..=0x40 | 0x5B..=0x60 | 0x7B..=0x7E | b' ' => {
1077                self.pos += 2;
1078                Ok(char::from(c))
1079            }
1080            _ => Self::err(at, RegexErrorKind::UnknownEscape),
1081        }
1082    }
1083
1084    /// `\xHH` (00-7F) or `\x{H...}`.
1085    fn hex_escape(&mut self, at: usize) -> Result<char, RegexError> {
1086        self.pos += 2;
1087        if self.peek() == Some(b'{') {
1088            self.pos += 1;
1089            let start = self.pos;
1090            let mut value: u32 = 0;
1091            while let Some(d) = self.peek().and_then(hex_value) {
1092                value = value.saturating_mul(16).saturating_add(d);
1093                self.pos += 1;
1094            }
1095            let digits = self.pos - start;
1096            if digits == 0 || digits > 8 || self.peek() != Some(b'}') {
1097                return Self::err(at, RegexErrorKind::InvalidHexEscape);
1098            }
1099            self.pos += 1;
1100            return char::from_u32(value).ok_or(RegexError {
1101                offset: at,
1102                kind: RegexErrorKind::InvalidCodePoint,
1103            });
1104        }
1105        let (Some(hi), Some(lo)) = (
1106            self.peek().and_then(hex_value),
1107            self.peek_at(1).and_then(hex_value),
1108        ) else {
1109            return Self::err(at, RegexErrorKind::InvalidHexEscape);
1110        };
1111        let value = hi * 16 + lo;
1112        if value > 0x7F {
1113            return Self::err(at, RegexErrorKind::InvalidCodePoint);
1114        }
1115        self.pos += 2;
1116        char::from_u32(value).ok_or(RegexError {
1117            offset: at,
1118            kind: RegexErrorKind::InvalidCodePoint,
1119        })
1120    }
1121
1122    /// `\uHHHH`.
1123    fn unicode_escape(&mut self, at: usize) -> Result<char, RegexError> {
1124        self.pos += 2;
1125        let mut value: u32 = 0;
1126        for _ in 0..4 {
1127            let Some(d) = self.peek().and_then(hex_value) else {
1128                return Self::err(at, RegexErrorKind::InvalidHexEscape);
1129            };
1130            value = value * 16 + d;
1131            self.pos += 1;
1132        }
1133        char::from_u32(value).ok_or(RegexError {
1134            offset: at,
1135            kind: RegexErrorKind::InvalidCodePoint,
1136        })
1137    }
1138
1139    /// A character class, from its `[` to its matching `]`.
1140    fn class(&mut self) -> Result<(), RegexError> {
1141        let outer = self.pos;
1142        // Per nested class: items since its start or its last `&&`, and the
1143        // last single character, which may start a range.
1144        let mut stack: Vec<ClassFrame> = Vec::new();
1145        self.open_class(&mut stack)?;
1146        loop {
1147            let at = self.pos;
1148            let Some(b) = self.peek() else {
1149                return Self::err(outer, RegexErrorKind::UnclosedClass);
1150            };
1151            let Some(top) = stack.last_mut() else {
1152                return Ok(());
1153            };
1154            match b {
1155                b']' => {
1156                    if top.items == 0 {
1157                        return Self::err(at, RegexErrorKind::EmptyClass);
1158                    }
1159                    self.pos += 1;
1160                    let _closed = stack.pop();
1161                    match stack.last_mut() {
1162                        None => return Ok(()),
1163                        Some(parent) => parent.set_item(),
1164                    }
1165                }
1166                b'[' if self.peek_at(1) == Some(b':') => {
1167                    self.posix(at)?;
1168                    top.set_item();
1169                }
1170                b'[' => self.open_class(&mut stack)?,
1171                b'&' if self.peek_at(1) == Some(b'&') => {
1172                    if top.items == 0 {
1173                        return Self::err(at, RegexErrorKind::EmptyClass);
1174                    }
1175                    top.items = 0;
1176                    top.endpoint = None;
1177                    self.pos += 2;
1178                }
1179                b'-' => {
1180                    if top.items == 0 || self.peek_at(1) == Some(b']') {
1181                        self.pos += 1;
1182                        top.set_item();
1183                    } else if let Some((lo, lo_at)) = top.endpoint {
1184                        self.pos += 1;
1185                        let hi = self.range_end(at)?;
1186                        if hi < lo {
1187                            return Self::err(lo_at, RegexErrorKind::InvalidRange);
1188                        }
1189                        top.set_item();
1190                    } else {
1191                        return Self::err(at, RegexErrorKind::InvalidRange);
1192                    }
1193                }
1194                b'\\' => {
1195                    let single = self.class_escape(at)?;
1196                    // The borrow of `top` ended at the call; look it up again.
1197                    if let Some(top) = stack.last_mut() {
1198                        top.set_item();
1199                        top.endpoint = single.map(|c| (c, at));
1200                    }
1201                }
1202                0 => return Self::err(at, RegexErrorKind::NulCharacter),
1203                _ => {
1204                    let c = self.next_char();
1205                    top.set_item();
1206                    top.endpoint = Some((c, at));
1207                }
1208            }
1209        }
1210    }
1211
1212    fn open_class(&mut self, stack: &mut Vec<ClassFrame>) -> Result<(), RegexError> {
1213        let at = self.pos;
1214        self.depth_ok(at, stack.len() + 1)?;
1215        self.pos += 1;
1216        if self.peek() == Some(b'^') {
1217            self.pos += 1;
1218        }
1219        stack.push(ClassFrame {
1220            items: 0,
1221            endpoint: None,
1222        });
1223        Ok(())
1224    }
1225
1226    /// The character after `-` in a range: a literal or a one-character
1227    /// escape.
1228    fn range_end(&mut self, dash: usize) -> Result<char, RegexError> {
1229        let at = self.pos;
1230        match self.peek() {
1231            None => Self::err(dash, RegexErrorKind::InvalidRange),
1232            Some(b'[' | b']' | b'-') => Self::err(dash, RegexErrorKind::InvalidRange),
1233            Some(b'&') if self.peek_at(1) == Some(b'&') => {
1234                Self::err(dash, RegexErrorKind::InvalidRange)
1235            }
1236            Some(0) => Self::err(at, RegexErrorKind::NulCharacter),
1237            Some(b'\\') => match self.class_escape(at)? {
1238                Some(c) => Ok(c),
1239                None => Self::err(dash, RegexErrorKind::InvalidRange),
1240            },
1241            Some(_) => Ok(self.next_char()),
1242        }
1243    }
1244
1245    /// An escape inside a class: `Some(c)` for one character, `None` for a
1246    /// set (`\w`, `\p{...}`).
1247    fn class_escape(&mut self, at: usize) -> Result<Option<char>, RegexError> {
1248        match self.peek_at(1) {
1249            None => Self::err(at, RegexErrorKind::TrailingBackslash),
1250            Some(b'w' | b'W' | b'd' | b'D' | b's' | b'S' | b'h' | b'H') => {
1251                self.pos += 2;
1252                Ok(None)
1253            }
1254            Some(b'p' | b'P') => {
1255                self.property(at)?;
1256                Ok(None)
1257            }
1258            Some(b'b' | b'B' | b'A' | b'z' | b'Z' | b'G' | b'k') => {
1259                Self::err(at, RegexErrorKind::UnknownEscape)
1260            }
1261            Some(_) => self.char_escape(at).map(Some),
1262        }
1263    }
1264
1265    /// `[:name:]` or `[:^name:]` inside a class.
1266    fn posix(&mut self, at: usize) -> Result<(), RegexError> {
1267        let rest = self.bytes.get(at + 2..).unwrap_or(&[]);
1268        let rest = rest.strip_prefix(b"^").unwrap_or(rest);
1269        let end = rest
1270            .iter()
1271            .position(|&b| !b.is_ascii_lowercase())
1272            .unwrap_or(rest.len());
1273        let (name, tail) = rest.split_at(end);
1274        if !tail.starts_with(b":]") || !POSIX.contains(&name) {
1275            return Self::err(at, RegexErrorKind::UnknownPosixClass);
1276        }
1277        // `[:` + optional `^` + name + `:]`.
1278        self.pos = at + 2 + usize::from(self.bytes.get(at + 2) == Some(&b'^')) + name.len() + 2;
1279        Ok(())
1280    }
1281
1282    fn next_char(&mut self) -> char {
1283        let c = self
1284            .text
1285            .get(self.pos..)
1286            .and_then(|s| s.chars().next())
1287            .unwrap_or('\0');
1288        self.pos += c.len_utf8();
1289        c
1290    }
1291}
1292
1293#[derive(Clone, Copy)]
1294struct ClassFrame {
1295    items: u32,
1296    endpoint: Option<(char, usize)>,
1297}
1298
1299impl ClassFrame {
1300    fn set_item(&mut self) {
1301        self.items = self.items.saturating_add(1);
1302        self.endpoint = None;
1303    }
1304}
1305
1306/// The length in bytes of the UTF-8 character starting with `first`.
1307fn char_len(first: u8) -> usize {
1308    match first {
1309        0xF0..=0xFF => 4,
1310        0xE0..=0xEF => 3,
1311        0xC0..=0xDF => 2,
1312        _ => 1,
1313    }
1314}
1315
1316fn hex_value(b: u8) -> Option<u32> {
1317    char::from(b).to_digit(16)
1318}
1319
1320/// POSIX bracket names Oniguruma accepts in `[[:name:]]`.
1321const POSIX: &[&[u8]] = &[
1322    b"alnum", b"alpha", b"ascii", b"blank", b"cntrl", b"digit", b"graph", b"lower", b"print",
1323    b"punct", b"space", b"upper", b"xdigit", b"word",
1324];
1325
1326/// Property names accepted in `\p{...}`, normalized: lowercase, with spaces,
1327/// underscores, and hyphens removed, the way Oniguruma compares them.
1328const PROPERTIES: &[&str] = &[
1329    // General categories.
1330    "c",
1331    "cc",
1332    "cf",
1333    "cn",
1334    "co",
1335    "cs",
1336    "l",
1337    "ll",
1338    "lm",
1339    "lo",
1340    "lt",
1341    "lu",
1342    "m",
1343    "mc",
1344    "me",
1345    "mn",
1346    "n",
1347    "nd",
1348    "nl",
1349    "no",
1350    "p",
1351    "pc",
1352    "pd",
1353    "pe",
1354    "pf",
1355    "pi",
1356    "po",
1357    "ps",
1358    "s",
1359    "sc",
1360    "sk",
1361    "sm",
1362    "so",
1363    "z",
1364    "zl",
1365    "zp",
1366    "zs",
1367    // POSIX-like classes.
1368    "alnum",
1369    "alpha",
1370    "ascii",
1371    "blank",
1372    "cntrl",
1373    "digit",
1374    "graph",
1375    "lower",
1376    "print",
1377    "punct",
1378    "space",
1379    "upper",
1380    "xdigit",
1381    "word",
1382    "any",
1383    "assigned",
1384    // Binary properties.
1385    "alphabetic",
1386    "lowercase",
1387    "uppercase",
1388    "whitespace",
1389    "idstart",
1390    "idcontinue",
1391    "xidstart",
1392    "xidcontinue",
1393    // Scripts.
1394    "arabic",
1395    "armenian",
1396    "bengali",
1397    "bopomofo",
1398    "cherokee",
1399    "common",
1400    "cyrillic",
1401    "devanagari",
1402    "ethiopic",
1403    "georgian",
1404    "greek",
1405    "gujarati",
1406    "gurmukhi",
1407    "han",
1408    "hangul",
1409    "hebrew",
1410    "hiragana",
1411    "inherited",
1412    "kannada",
1413    "katakana",
1414    "khmer",
1415    "lao",
1416    "latin",
1417    "malayalam",
1418    "mongolian",
1419    "myanmar",
1420    "oriya",
1421    "sinhala",
1422    "syriac",
1423    "tamil",
1424    "telugu",
1425    "thaana",
1426    "thai",
1427    "tibetan",
1428];
1429
1430/// Whether `name` (as written in `\p{...}`) is a supported property.
1431fn known_property(name: &[u8]) -> bool {
1432    let mut normalized = [0u8; 32];
1433    let mut len = 0;
1434    for &b in name {
1435        if matches!(b, b' ' | b'_' | b'-') {
1436            continue;
1437        }
1438        let Some(slot) = normalized.get_mut(len) else {
1439            return false;
1440        };
1441        *slot = b.to_ascii_lowercase();
1442        len += 1;
1443    }
1444    let normalized = normalized.get(..len).unwrap_or(&[]);
1445    PROPERTIES.iter().any(|p| p.as_bytes() == normalized)
1446}
1447
1448#[cfg(test)]
1449#[allow(clippy::unwrap_used, clippy::panic)]
1450mod tests {
1451    use alloc::string::ToString;
1452
1453    use super::*;
1454
1455    fn kind(pattern: &str) -> RegexErrorKind {
1456        match validate_regex(pattern) {
1457            Err(e) => e.kind,
1458            Ok(groups) => panic!("{pattern:?} was accepted with {groups} groups"),
1459        }
1460    }
1461
1462    #[test]
1463    fn test_accepts_common_grammar_patterns() {
1464        let ok = [
1465            (r"\b(if|else|while)\b", 1),
1466            (r#""(?:[^"\\]|\\.)*""#, 0),
1467            (r"(?<!\w)(\d+)(?:\.(\d+))?", 2),
1468            (r"^\s*(#)\s*(include)\b", 2),
1469            (r"\G(?=\S)", 0),
1470            (r"(?i:select|from)", 0),
1471            (r"(?<name>[a-z]+)\s*=\s*\k<name>", 1),
1472            (r"[\p{L}_][\p{L}\p{N}_]*", 0),
1473            (r"[[:alpha:][:digit:]]", 0),
1474            (r"[a-z&&[^aeiou]]", 0),
1475            (r"\x{1F600}|\u00e9|\x7f", 0),
1476            (r"a{2}b{1,}c{0,3}?", 0),
1477            (r"a*+b++c?+", 0),
1478            (r"(?>a+)b", 0),
1479            (r"(?<=ab|c)d", 0),
1480            (r"(?<=a(?:b|c))d", 0),
1481            (r"(?<=\d{3})x", 0),
1482            ("}", 0),
1483            (r"[-a]|[a-]|[\]\[]", 0),
1484            (r"(?#comment \) here)x", 0),
1485            ("(?x) a + # spaces and a comment ( ) [\n b", 0),
1486            (r"(?x) [ ]", 0),
1487        ];
1488        for (pattern, groups) in ok {
1489            assert_eq!(validate_regex(pattern), Ok(groups), "{pattern}");
1490        }
1491    }
1492
1493    #[test]
1494    fn test_rejects_with_offsets() {
1495        let cases = [
1496            ("ab\\", 2, RegexErrorKind::TrailingBackslash),
1497            (r"a\q", 1, RegexErrorKind::UnknownEscape),
1498            (r"\0", 0, RegexErrorKind::UnknownEscape),
1499            (r"x\xZZ", 1, RegexErrorKind::InvalidHexEscape),
1500            (r"\xff", 0, RegexErrorKind::InvalidCodePoint),
1501            (r"\x{110000}", 0, RegexErrorKind::InvalidCodePoint),
1502            (r"\uD800", 0, RegexErrorKind::InvalidCodePoint),
1503            (r"\pL", 0, RegexErrorKind::MalformedProperty),
1504            (r"\p{Klingon}", 0, RegexErrorKind::UnknownProperty),
1505            ("a(b", 1, RegexErrorKind::UnclosedGroup),
1506            ("a)b", 1, RegexErrorKind::UnmatchedParen),
1507            ("(?(1)a|b)", 0, RegexErrorKind::UnknownGroup),
1508            ("(?iq)", 0, RegexErrorKind::UnknownOption),
1509            ("(?q)", 0, RegexErrorKind::UnknownGroup),
1510            ("(?-)", 0, RegexErrorKind::UnknownOption),
1511            ("a(?#x", 1, RegexErrorKind::UnclosedComment),
1512            ("(?<1a>x)", 0, RegexErrorKind::InvalidGroupName),
1513            ("(?<a>x)(?<a>y)", 7, RegexErrorKind::DuplicateGroupName),
1514            ("(?<a>x)(y)", 7, RegexErrorKind::MixedGroups),
1515            (
1516                r"(?<a>x)\1",
1517                7,
1518                RegexErrorKind::NumberedBackrefWithNamedGroups,
1519            ),
1520            (r"(?<a>x)\k<b>", 7, RegexErrorKind::UndefinedGroupName),
1521            ("[abc", 0, RegexErrorKind::UnclosedClass),
1522            ("[]a]", 1, RegexErrorKind::EmptyClass),
1523            ("[^]", 2, RegexErrorKind::EmptyClass),
1524            ("[a&&]", 4, RegexErrorKind::EmptyClass),
1525            ("a]", 1, RegexErrorKind::UnescapedBracket),
1526            ("[z-a]", 1, RegexErrorKind::InvalidRange),
1527            (r"[a-\w]", 2, RegexErrorKind::InvalidRange),
1528            (r"[\w-a]", 3, RegexErrorKind::InvalidRange),
1529            ("[[:alpah:]]", 1, RegexErrorKind::UnknownPosixClass),
1530            ("*a", 0, RegexErrorKind::NothingToRepeat),
1531            ("(|*)", 2, RegexErrorKind::NothingToRepeat),
1532            ("a**", 2, RegexErrorKind::RepeatedQuantifier),
1533            ("a{2}{3}", 4, RegexErrorKind::RepeatedQuantifier),
1534            ("a{2}?", 1, RegexErrorKind::AmbiguousQuantifier),
1535            ("a{1,2}+", 1, RegexErrorKind::AmbiguousQuantifier),
1536            ("a{", 1, RegexErrorKind::InvalidInterval),
1537            ("a{,2}", 1, RegexErrorKind::InvalidInterval),
1538            ("a{3,2}", 1, RegexErrorKind::InvalidInterval),
1539            ("a{100001}", 1, RegexErrorKind::InvalidInterval),
1540            ("^*", 1, RegexErrorKind::QuantifiedAssertion),
1541            ("(?=a)+", 5, RegexErrorKind::QuantifiedAssertion),
1542            ("(?<=a+)b", 0, RegexErrorKind::LookbehindNotFixed),
1543            ("(?<=a|b*)c", 0, RegexErrorKind::LookbehindNotFixed),
1544            ("(?<=(?:ab|c))d", 0, RegexErrorKind::LookbehindNotFixed),
1545            ("(?<=(a))b", 4, RegexErrorKind::CaptureInLookbehind),
1546            ("a\0", 1, RegexErrorKind::NulCharacter),
1547        ];
1548        for (pattern, offset, expected) in cases {
1549            let err = validate_regex(pattern).unwrap_err();
1550            assert_eq!((err.offset, err.kind), (offset, expected), "{pattern:?}");
1551        }
1552    }
1553
1554    #[test]
1555    fn test_backreferences() {
1556        assert_eq!(validate_regex(r"(a)(b)\2\1"), Ok(2));
1557        assert_eq!(
1558            kind(r"(a)\2"),
1559            RegexErrorKind::BackreferenceOutOfRange {
1560                group: 2,
1561                groups: 1
1562            }
1563        );
1564        // In `end` mode the numbers name `begin` groups; the caller checks.
1565        let info = check(r"\2(x)\10", Backrefs::Begin).unwrap();
1566        assert_eq!(info.groups, 1);
1567        assert_eq!(info.max_backref, Some((10, 5)));
1568        assert_eq!(
1569            check(r"\\1", Backrefs::Begin).unwrap_err().kind,
1570            RegexErrorKind::BackslashBeforeDigit
1571        );
1572        assert!(check(r"\\1", Backrefs::Own).is_ok());
1573        assert_eq!(
1574            check(r"[\\1]", Backrefs::Begin).unwrap_err().kind,
1575            RegexErrorKind::BackslashBeforeDigit
1576        );
1577    }
1578
1579    #[test]
1580    fn test_isolated_option_extends_to_enclosing_group() {
1581        // `(?x)` turns on extended mode up to the enclosing `)` only.
1582        assert_eq!(validate_regex("((?x) a # c\n) b"), Ok(1));
1583        assert_eq!(kind("((?x) a ) #)"), RegexErrorKind::UnmatchedParen);
1584        // `a(?i)b|cd` is `a(?i:b|cd)` in Oniguruma, so in a look-behind the
1585        // alternatives belong to a nested group and must be the same length.
1586        assert_eq!(kind("(?<=a(?i)b|cd)"), RegexErrorKind::LookbehindNotFixed);
1587        assert_eq!(validate_regex("(?<=a(?i)b|c)"), Ok(0));
1588        assert_eq!(kind("(?i)*"), RegexErrorKind::NothingToRepeat);
1589    }
1590
1591    #[test]
1592    fn test_extended_mode_comments_hide_metacharacters() {
1593        assert_eq!(validate_regex("(?x) a # ( [ unbalanced"), Ok(0));
1594        assert_eq!(validate_regex("(?x:a # )\n)"), Ok(0));
1595        assert_eq!(validate_regex(r"(?x) \# \ "), Ok(0));
1596        // Not in extended mode, `#` is an ordinary character.
1597        assert_eq!(kind("a # ("), RegexErrorKind::UnclosedGroup);
1598    }
1599
1600    #[test]
1601    fn test_depth_limit() {
1602        let ok = "(".repeat(MAX_REGEX_DEPTH) + &")".repeat(MAX_REGEX_DEPTH);
1603        assert_eq!(
1604            validate_regex(&ok),
1605            Ok(u32::try_from(MAX_REGEX_DEPTH).unwrap())
1606        );
1607        let deep = "(?:".repeat(MAX_REGEX_DEPTH + 1) + &")".repeat(MAX_REGEX_DEPTH + 1);
1608        assert_eq!(kind(&deep), RegexErrorKind::TooDeep);
1609        let classes = "[".repeat(MAX_REGEX_DEPTH + 1) + "a" + &"]".repeat(MAX_REGEX_DEPTH + 1);
1610        assert_eq!(kind(&classes), RegexErrorKind::TooDeep);
1611    }
1612
1613    #[test]
1614    fn test_escape_regex() {
1615        assert_eq!(escape_regex(""), "");
1616        assert_eq!(escape_regex("abc_09"), "abc_09");
1617        assert_eq!(escape_regex("a.b*c"), r"a\.b\*c");
1618        assert_eq!(escape_regex("\\n"), r"\x5cn");
1619        assert_eq!(escape_regex("\\1"), r"\x5c1");
1620        // Escaped pieces compose: no `\\` + digit at a seam.
1621        let joined = escape_regex("a\\") + &escape_regex("1");
1622        assert!(check(&joined, Backrefs::Begin).is_ok());
1623        assert_eq!(escape_regex("\0\u{7f}"), r"\x00\x7f");
1624        assert_eq!(escape_regex("é →"), "é\\ →");
1625        for text in ["((", "[", "\\", "{1}", "#x", "a b", "\\\\9"] {
1626            assert_eq!(
1627                check(&escape_regex(text), Backrefs::Begin).map(|i| i.groups),
1628                Ok(0)
1629            );
1630        }
1631    }
1632
1633    #[test]
1634    fn test_properties_normalize_like_oniguruma() {
1635        assert!(known_property(b"L"));
1636        assert!(known_property(b"White_Space"));
1637        assert!(known_property(b"xid-start"));
1638        assert!(!known_property(b""));
1639        assert!(!known_property(&[b'a'; 40]));
1640    }
1641
1642    #[test]
1643    fn test_display() {
1644        let e = RegexError {
1645            offset: 4,
1646            kind: RegexErrorKind::UnclosedClass,
1647        };
1648        assert_eq!(
1649            e.to_string(),
1650            "at byte 4: unclosed class: `[` has no matching `]`"
1651        );
1652    }
1653}