diff --git a/contributors/people.toml b/contributors/people.toml index 18c34481..40ae92eb 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -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" diff --git a/crates/engine/src/cmd/utility/calc.rs b/crates/engine/src/cmd/utility/calc.rs index bb65f4f7..437501aa 100644 --- a/crates/engine/src/cmd/utility/calc.rs +++ b/crates/engine/src/cmd/utility/calc.rs @@ -68,6 +68,7 @@ struct Parser<'a> { i: usize, vars: &'a Map, 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 { + 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 { 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) -> Result { 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");