mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-12 07:32:11 +00:00
fix(formula): bound the wrappers stacked on one path of a parsed formula (#3813)
This commit is contained in:
@@ -69,11 +69,35 @@ fn parse_top(src: &str, locale: crate::FormulaLocale, allow_union: bool) -> Resu
|
||||
let mut p = Parser { toks, i: 0, depth: 0, input: locale == crate::FormulaLocale::Es };
|
||||
let e = p.expr(0, allow_union)?;
|
||||
match p.peek() {
|
||||
Tok::Eof if wrapper_height(&e) > MAX_DEPTH => Err(ParseError { msg: "formula is nested too deeply".into(), pos: 0 }),
|
||||
Tok::Eof => Ok(e),
|
||||
t => Err(p.err(format!("unexpected {}", describe(t)))),
|
||||
}
|
||||
}
|
||||
|
||||
/// The most unary operators, parentheses and invocations on one path of `e`. The parser bounds
|
||||
/// each level's postfix wrappers by its own depth, so levels inside levels can still stack them
|
||||
/// far past [`MAX_DEPTH`]; code that maps over the tree recurses once per wrapper.
|
||||
fn wrapper_height(e: &Expr) -> usize {
|
||||
let mut most = 0;
|
||||
let mut pending = vec![(e, 0usize)];
|
||||
while let Some((e, h)) = pending.pop() {
|
||||
most = most.max(h);
|
||||
match e {
|
||||
Expr::Unary(_, a) | Expr::Paren(a) => pending.push((a, h + 1)),
|
||||
Expr::Invoke(callee, args) => {
|
||||
pending.push((callee, h + 1));
|
||||
pending.extend(args.iter().map(|a| (a, h + 1)));
|
||||
}
|
||||
Expr::Binary(_, a, b) => pending.extend([(&**a, h), (&**b, h)]),
|
||||
Expr::Call(_, args) => pending.extend(args.iter().map(|a| (a, h))),
|
||||
Expr::Array(rows) => pending.extend(rows.iter().flatten().map(|a| (a, h))),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
most
|
||||
}
|
||||
|
||||
fn describe(t: &Tok) -> String {
|
||||
match t {
|
||||
Tok::RParen => "`)`".into(),
|
||||
|
||||
@@ -576,3 +576,19 @@ fn column_starting_with_at_round_trips() {
|
||||
assert_eq!(parse(&text).ok(), Some(Expr::Struct(s)), "printed as {text}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrappers_stacked_across_levels_are_bounded_as_a_whole() {
|
||||
// Each level adds 40 percents inside the parser's per-level bound, so no level is too deep alone.
|
||||
let stacked = |levels: usize| {
|
||||
let mut f = "1".to_string();
|
||||
for _ in 0..levels {
|
||||
f = format!("({f}){}", "%".repeat(40));
|
||||
}
|
||||
f
|
||||
};
|
||||
assert!(parse(&stacked(5)).is_ok());
|
||||
assert!(parse(&stacked(7)).is_err(), "7 x 41 wrappers on one path");
|
||||
assert!(parse(&format!("{}1{}", "(".repeat(200), ")".repeat(200))).is_ok());
|
||||
assert!(parse(&format!("={}", vec!["A1%"; 100].join("+"))).is_ok(), "siblings are not stacked");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user