Bound CAL unary minus/plus and ^ chain recursion (#1586)

Found while testing crates/engine/src/cmd/utility/calc.rs:124-140 (Parser::unary recursion, MAX_DEPTH only counted in expr() and '[')

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-19 02:57:52 -04:00
1 parent 8c249aa722
commit c02027abc8
2 files changed
+40 -4

No files matched your search

+4
View File
@@ -67,5 +67,9 @@ real_name = "Antoine Boxho"
display_name = "Antoine Boxho"
sync_github_name = true
[people.Silvertree2010]
display_name = "Netter"
sync_github_name = false
[people.gurparvez]
real_name = "Gurparvez"
+36 -4
View File
@@ -68,6 +68,7 @@ struct Parser<'a> {
i: usize,
vars: &'a Map<String, Value>,
depth: usize,
unary_depth: usize,
}
impl Parser<'_> {
@@ -121,19 +122,31 @@ impl Parser<'_> {
}
Ok(v)
}
/// A unary operand reached by a leading `-`/`+` or after `^`. These recurse without passing
/// through `expr`, so they are bounded by their own counter (`unary_depth`), leaving the
/// parenthesis/bracket limit in `depth` untouched.
fn operand(&mut self) -> Result<Val, String> {
if self.unary_depth >= MAX_DEPTH {
return Err("expression too deeply nested".into());
}
self.unary_depth += 1;
let v = self.unary();
self.unary_depth -= 1;
v
}
fn unary(&mut self) -> Result<Val, String> {
if self.eat(b'-') {
return Ok(match self.unary()? {
return Ok(match self.operand()? {
Val::Num(v) => Val::Num(-v),
Val::Pt(p) => Val::Pt(Vec2::ZERO - p),
});
}
if self.eat(b'+') {
return self.unary();
return self.operand();
}
let b = self.atom()?;
if self.eat(b'^') {
let e = self.unary()?.num("^")?;
let e = self.operand()?.num("^")?;
return Ok(Val::Num(b.num("^")?.powf(e)));
}
Ok(b)
@@ -395,7 +408,7 @@ pub fn eval_val(expr: &str, vars: &Map<String, Value>) -> Result<Val, String> {
if expr.len() > MAX_LEN {
return Err("expression too long".into());
}
let mut p = Parser { s: expr.as_bytes(), i: 0, vars, depth: 0 };
let mut p = Parser { s: expr.as_bytes(), i: 0, vars, depth: 0, unary_depth: 0 };
let v = p.expr()?;
if p.peek().is_some() {
return Err(format!("unexpected `{}`", expr.get(p.i..).unwrap_or("")));
@@ -588,6 +601,25 @@ mod tests {
assert!(ev("max(1,2,3,4,5,6,7,8,9)").is_err());
}
#[test]
fn deep_unary_chain_does_not_overflow_the_stack() {
assert!(ev(&format!("{}1", "-".repeat(600))).is_err());
assert!(ev(&format!("{}1", "+".repeat(600))).is_err());
assert!(ev(&format!("2{}", "^2".repeat(600))).is_err());
// The size from the report: under MAX_LEN, but ~10,000 frames deep without a guard.
assert!(ev(&format!("{}1", "-".repeat(9990))).is_err());
assert!(ev(&format!("2{}", "^2".repeat(4990))).is_err());
assert_eq!(ev("--1"), Ok(Val::Num(1.0)));
assert_eq!(ev("2^3^2"), Ok(Val::Num(512.0)));
}
#[test]
fn parenthesis_nesting_just_under_the_limit_still_parses() {
let n = MAX_DEPTH - 1;
assert_eq!(ev(&format!("{}1{}", "(".repeat(n), ")".repeat(n))), Ok(Val::Num(1.0)));
assert!(ev(&format!("{}1{}", "(".repeat(n + 1), ")".repeat(n + 1))).is_err());
}
#[test]
fn number_text() {
assert_eq!(num_text(0.1 + 0.2), "0.3");