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>
| M | internal/ | 33 | ++++++++++++++++++++++++++++++ |
| M | internal/ | 24 | ++++++++++++++++++++++ |
| M | internal/ | 10 | +++++++++ |
| M | internal/ | 4 | +++- |
| M | internal/ | 13 | ++++++++++++ |
@@ -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@@ -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@@ -6,6 +6,8 @@ import (
66 "strings"77 "testing"88 "unicode"9+10+ "github.com/FelineStateMachine/012/internal/formula"911 )1012 1113 func at(s string) Addr {@@ -131,6 +133,7 @@ func TestParseErrors(t *testing.T) {
131133 {`="abc`, "Missing closing quote"},132134 {"=ROUND()", "Wrong number of arguments to ROUND(value, [places])"},133135 {"=PI(1)", "Wrong number of arguments to PI()"},136+ {"=" + strings.Repeat("(", 2000) + "1" + strings.Repeat(")", 2000), "Formula is nested too deeply (more than 1024 levels)"},134137 }135138 for _, tt := range tests {136139 var pe *ParseError@@ -235,6 +238,13 @@ func FuzzParse(f *testing.F) {
235238 for _, s := range []string{"=A1*2", "=SUM(A1:B3)", "=IF(AND(A1>2,B1),1,NA())", `="a"&"b"`, "=1.5E-3^2", "$1,200", "12%", "@SUM(A1..B3)", "=$A$1+A$2*$B3", "=SUM($A1:B$3)", "=#REF!+1", "=A$$1"} {236239 f.Add(s)237240 }241+ // Around the nesting cap: parentheses, calls and prefix operators.242+ for _, n := range []int{formula.MaxDepth - 2, formula.MaxDepth + 1, 4 * formula.MaxDepth} {243+ f.Add("=" + strings.Repeat("(", n) + "1" + strings.Repeat(")", n))244+ f.Add("=" + strings.Repeat("ABS(", n) + "A1" + strings.Repeat(")", n))245+ f.Add("=" + strings.Repeat("-+", n/2) + "1")246+ f.Add("=" + strings.Repeat("(", n))247+ }238248 f.Fuzz(func(t *testing.T, in string) {239249 s := New()240250 printable := strings.ContainsFunc(in, func(r rune) bool { return !unicode.IsControl(r) })@@ -118,6 +118,8 @@ func BenchmarkHistoryWide(b *testing.B) {
118118 }119119 }120120 121+// BenchmarkNested enters deeply nested formulas: within the parser's cap122+// (formula.MaxDepth, 1024) they're parsed and evaluated, past it refused.121123 func BenchmarkNested(b *testing.B) {122124 for _, depth := range []int{100, 1000, 10000} {123125 for name, f := range map[string]func(int) string{"paren": stress.Nested, "if": stress.NestedIF} {@@ -125,7 +127,7 @@ func BenchmarkNested(b *testing.B) {
125127 s := sheet.New()126128 src := f(depth)127129 for b.Loop() {128- if err := s.Set(sheet.Addr{Col: 1}, src); err != nil {130+ if err := s.Set(sheet.Addr{Col: 1}, src); (err != nil) != (depth > 1000) {129131 b.Fatal(err)130132 }131133 }@@ -13,6 +13,19 @@ func init() {
1313 named["end"] = tea.Key{Code: tea.KeyEnd}1414 }1515 16+// A formula nested past the parser's cap stays in EDIT, saying why on17+// the context line.18+func TestTooDeepFormulaExplained(t *testing.T) {19+ m := newModel()20+ press(t, m, "="+strings.Repeat("(", 1100)+"1"+strings.Repeat(")", 1100), "<enter>")21+ if m.mode != modeEdit || m.sheet.Cell(addr("A1")) != nil {22+ t.Fatalf("mode %v, A1 %+v", m.mode, m.sheet.Cell(addr("A1")))23+ }24+ if got := line(m, contextLine); !strings.Contains(got, "Formula is nested too deeply (more than 1024 levels)") {25+ t.Errorf("context line %q", got)26+ }27+}28+1629 func TestUndoRedoKeys(t *testing.T) {1730 t.Chdir(t.TempDir())1831 m := newModel()