diff --git a/crates/formula/src/parser.rs b/crates/formula/src/parser.rs index 2b6f6d408..f8d73744e 100644 --- a/crates/formula/src/parser.rs +++ b/crates/formula/src/parser.rs @@ -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(), diff --git a/crates/formula/src/tests.rs b/crates/formula/src/tests.rs index 6e3829925..274a66667 100644 --- a/crates/formula/src/tests.rs +++ b/crates/formula/src/tests.rs @@ -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"); +}