diff --git a/crates/js/src/regexp_vm.rs b/crates/js/src/regexp_vm.rs index c0c557ea7..139469422 100644 --- a/crates/js/src/regexp_vm.rs +++ b/crates/js/src/regexp_vm.rs @@ -5,7 +5,8 @@ //! pattern with a loop and a stack of open groups, and the matcher keeps its choice points and the //! values it must restore on its own stack, so neither recurses however deeply the groups nest. //! Quantifiers count their iterations in registers instead of repeating their atom's code. Every -//! instruction run, every capture slot cleared and every choice point taken back costs fuel, and +//! instruction run, every capture slot cleared, every choice point taken back and every 16 units a +//! backreference compares cost fuel, and //! the compiler's work, the matcher's state and its stack are held against the heap limit, so a //! pattern that backtracks without end stops at a limit. @@ -1294,12 +1295,19 @@ impl<'r> Matcher<'r> { true } else { let len = e - s; - let hit = pos + len <= input.len() - && (0..len).all(|k| match (input.get(s + k), input.get(pos + k)) { + let mut hit = pos + len <= input.len(); + // The units are compared 16 at a time, and each 16, or fewer at the end, + // count as a step taken before they are compared. + let mut k = 0; + while hit && k < len { + let end = len.min(k + 16); + self.step(interp)?; + hit = (k..end).all(|i| match (input.get(s + i), input.get(pos + i)) { (Some(&a), Some(&b)) => re.canon(a) == re.canon(b), _ => false, }); - self.steps += len as u64 / 16; + k = end; + } if hit { pos += len; } diff --git a/crates/js/src/text_tests.rs b/crates/js/src/text_tests.rs index 809adb5f8..70e3cd6eb 100644 --- a/crates/js/src/text_tests.rs +++ b/crates/js/src/text_tests.rs @@ -447,6 +447,33 @@ fn the_matcher_state_counts_against_the_heap() { assert_eq!(interp.heap.bytes, live); } +#[test] +fn a_backreference_takes_fuel_for_the_units_it_compares() { + let mut realm = Realm::with_options(Options::default(), Limits::default()); + let interp = realm.interp(); + let mut fuel_for = |pattern: &str, input: &str, want: bool| { + let input: Vec = input.encode_utf16().collect(); + let Ok(re) = compile_within(pattern, usize::MAX, u64::MAX).0 else { panic!("the pattern compiles") }; + let Ok(mut m) = crate::regexp_vm::Matcher::new(&re, interp) else { panic!("the state fits") }; + interp.fuel = 1 << 40; + assert!(matches!(m.match_at(interp, &input, 0), Ok(hit) if hit == want), "{pattern}"); + (1u64 << 40) - interp.fuel + }; + // The same 20,000 iterations, each running a one-unit backreference instead of a character: + // each comparison takes a step besides the instruction's own. + let a = "a".repeat(20_001); + let (by_char, by_backref) = (fuel_for("(a)(?:a)*$", &a, true), fuel_for("(a)(?:\\1)*$", &a, true)); + assert!(by_backref >= by_char + 20_000, "{by_backref} < {by_char} + 20000"); + // A long comparison takes a step for each 16 units it compares. + let long = fuel_for("^(a{10000})\\1", &a, true); + let plain = fuel_for("^(a{10000})", &a, true); + assert!(long >= plain + 625, "{long} < {plain} + 625"); + // One that fails on its first unit takes a step for those 16 units only. + let ab = format!("{}{}", "a".repeat(10_000), "b".repeat(10_000)); + let (by_backref, by_char) = (fuel_for("^(a{10000})\\1", &ab, false), fuel_for("^(a{10000})c", &ab, false)); + assert!(by_backref <= by_char + 2, "{by_backref} > {by_char} + 2"); +} + #[test] fn clearing_matcher_state_takes_fuel() { let limits = Limits { fuel: 500_000, ..Limits::default() };