mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-19 02:09:48 -05:00
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:
2 files changed
+40
-4
No files matched your search
@@ -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"
|
||||
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user