cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

Cap formula nesting at 1024 levels in the parser

Dami committed 00faa1f
A formula nested deeper (parentheses, calls, prefix operators) fails to
parse with "Formula is nested too deeply (more than 1024 levels)" at the
level past the cap, shown on the context line like any parse error, so
nothing that walks a formula recurses without bound. Fuzz seeds around
the cap.
commit
00faa1f90ae8dc54d621122e4bcdefa6cfb49356
parent
cedd61e40a14226726a7ae431746bb6e427af0c6
author
Dami <dami.etoile@gmail.com>
5 files changed, +83 -1
Minternal/formula/formula_test.go33++++++++++++++++++++++++++++++
Minternal/formula/parse.go24++++++++++++++++++++++
Minternal/sheet/sheet_test.go10+++++++++
Minternal/sheet/stress_test.go4+++-
Minternal/ui/edit_test.go13++++++++++++
M internal/formula/formula_test.go +33 -0
@@ -2,6 +2,7 @@ package formula
22 33 import (44 "reflect"5+ "strings"56 "testing"67 )78
@@ -249,6 +250,10 @@ func FuzzPrint(f *testing.F) {
249250 for _, s := range []string{250251 "=A1*2", "=$A$1+A$2-$B3", "=SUM($A1:B$3)", "=-2^-2%", "=1-(2-3)", "=#NOT#A1=1#OR#B1",251252 "=2^(3^2)", `=IF(A1>2,"x""y",#REF!)`, "=1.5E-3^2", "=@SUM(A1..B3)", "=(1+2)%*-A1",253+ // At and past the nesting cap.254+ "=" + strings.Repeat("(", MaxDepth-1) + "1" + strings.Repeat(")", MaxDepth-1),255+ "=" + strings.Repeat("-(", MaxDepth/2) + "1" + strings.Repeat(")", MaxDepth/2),256+ "=" + strings.Repeat("(", MaxDepth+1) + "1" + strings.Repeat(")", MaxDepth+1),252257 } {253258 f.Add(s)254259 }
@@ -268,6 +273,34 @@ func FuzzPrint(f *testing.F) {
268273 })269274 }270275 276+// Nesting is capped at MaxDepth levels, with the error at the level277+// past it; anything within the cap parses.278+func TestParseDepth(t *testing.T) {279+ nested := func(n int) string {280+ return "=" + strings.Repeat("(", n) + "1" + strings.Repeat(")", n)281+ }282+ if _, err := Parse(nested(MaxDepth-1), testFuncs); err != nil {283+ t.Errorf("%d parentheses: %v", MaxDepth-1, err)284+ }285+ for _, in := range []string{286+ nested(MaxDepth),287+ nested(100_000),288+ "=" + strings.Repeat("-", 5000) + "1",289+ "=" + strings.Repeat("SUM(", 2000) + "1" + strings.Repeat(")", 2000),290+ "=" + strings.Repeat("(", 5000),291+ } {292+ _, err := Parse(in, testFuncs)293+ pe, ok := err.(*ParseError)294+ if !ok || !strings.HasPrefix(pe.Msg, "Formula is nested too deeply") {295+ t.Errorf("Parse(%.20q...) error = %v", in, err)296+ continue297+ }298+ if pe.Pos < MaxDepth || pe.Pos > len(in) {299+ t.Errorf("Parse(%.20q...) error at %d", in, pe.Pos)300+ }301+ }302+}303+271304 func TestParseErrors(t *testing.T) {272305 tests := []struct {273306 in string
M internal/formula/parse.go +24 -0
@@ -23,11 +23,21 @@ const (
2323 percentPower = 802424 )2525 26+// MaxDepth is how deeply a formula may nest: parentheses, function calls27+// and prefix operators each open a level (an operator of higher28+// precedence may add one or two). Excel allows 64 nested functions; this29+// is far more than a formula written by hand needs, and it bounds the30+// recursion of everything that walks a formula (the parser, printer,31+// evaluator and reference rewriting), so a pathological file fails to32+// parse rather than exhausting the stack.33+const MaxDepth = 102434+2635 type parser struct {2736 src string // the formula without its "=", for names' original spelling2837 toks []token2938 pos int3039 funcs Funcs40+ depth int // of expr calls, see MaxDepth3141 }3242 3343 // Parse parses a formula, finding the functions it calls with funcs. src
@@ -73,7 +83,21 @@ func (p *parser) isOp(op string) bool {
7383 return t.kind == tokOp && t.text == op7484 }7585 86+// expr parses an expression whose operators bind tighter than minPower.87+// Every level of nesting passes through here, so here it's counted.7688 func (p *parser) expr(minPower int) (Node, error) {89+ if p.depth >= MaxDepth {90+ return nil, &ParseError{p.peek().pos, fmt.Sprintf("Formula is nested too deeply (more than %d levels)", MaxDepth)}91+ }92+ p.depth++93+ n, err := p.climb(minPower)94+ p.depth--95+ return n, err96+}97+98+// climb parses by precedence climbing: a prefix, then infix and postfix99+// operators binding tighter than minPower.100+func (p *parser) climb(minPower int) (Node, error) {77101 left, err := p.prefix()78102 if err != nil {79103 return nil, err
M internal/sheet/sheet_test.go +10 -0
M internal/sheet/stress_test.go +3 -1
M internal/ui/edit_test.go +13 -0