mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 02:19:56 -06:00
JavaScript: a backreference takes fuel for each 16 units before it compares them
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<u16> = 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() };
|
||||
|
||||
Reference in New Issue
Block a user