fix(regular_expression): allow oversized decimal escape for Annex B (#26070)

Follow up on #26055.

Saturate oversized decimal escapes instead of failing during number parsing.
Preserve Annex B fallback in non-Unicode mode while rejecting invalid backreferences in `u|v` modes.
This commit is contained in:
leaysgur committed 2026-08-25 01:45:12 +00:00
1 parent fc175b09d3
commit 9b516587ab
2 files changed
+26 -11

No files matched your search

@@ -151,7 +151,6 @@ mod test {
("x{9007199254740992}", ""),
("x{9007199254740991,9007199254740992}", ""),
("x{99999999999999999999999999999999999999999999999999}", ""),
(r"\99999999999999999999999999999999999999999999999999", ""),
(r"\u{FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF}", "u"),
("(?=a", ""),
("(?<!a", ""),
@@ -292,7 +291,14 @@ mod test {
#[test]
fn oversized_decimal_backreferences() {
let allocator = Allocator::default();
let patterns = [r"()\4294967295", r"()\4294967296", r"()\4294967297"];
let patterns = [
r"()\4294967295", // u32::MAX
r"()\4294967296", // u32::MAX + 1, previously wrapped to 0
r"()\4294967297", // u32::MAX + 2, previously wrapped to 1
r"()\18446744073709551615", // u64::MAX
r"()\18446744073709551616", // u64::MAX + 1
r"()\999999999999999999999999999999999999", // Arbitrarily large
];
for pattern_text in patterns {
for flags_text in ["u", "v"] {
@@ -470,7 +470,7 @@ impl<'a> PatternParser<'a> {
let checkpoint = self.reader.checkpoint();
// DecimalEscape: \1 means indexed reference
if let Some(index) = self.consume_decimal_escape()? {
if let Some(index) = self.consume_decimal_escape() {
if self.state.unicode_mode {
// [SS:EE] AtomEscape :: DecimalEscape
// It is a Syntax Error if the CapturingGroupNumber of DecimalEscape is strictly greater than CountLeftCapturingParensWithin(the Pattern containing AtomEscape).
@@ -1778,19 +1778,28 @@ impl<'a> PatternParser<'a> {
// DecimalEscape ::
// NonZeroDigit DecimalDigits[~Sep][opt] [lookahead ∉ DecimalDigit]
// ```
fn consume_decimal_escape(&mut self) -> Result<Option<u64>> {
fn consume_decimal_escape(&mut self) -> Option<u64> {
let checkpoint = self.reader.checkpoint();
if let Some(index) = self.consume_decimal_digits()? {
// \0 is CharacterEscape, not DecimalEscape
if index != 0 {
return Ok(Some(index));
}
let mut index = 0_u64;
while let Some(cp) = self.reader.peek().filter(|&cp| character::is_decimal_digit(cp)) {
// `- '0' as u32`: convert code point to digit
#[expect(clippy::cast_lossless)]
let digit = (cp - '0' as u32) as u64;
self.reader.rewind(checkpoint);
// The exact value is irrelevant once it exceeds the capture count.
// Saturate instead of erroring so Annex B can rewind and reinterpret the entire escape.
index = index.saturating_mul(10).saturating_add(digit);
self.reader.advance();
}
Ok(None)
// \0 is CharacterEscape, not DecimalEscape
if index != 0 {
return Some(index);
}
self.reader.rewind(checkpoint);
None
}
// ```