diff --git a/.circleci/config.yml b/.circleci/config.yml index 7a01b6f..4cf1ee7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -30,3 +30,4 @@ jobs: - run: curl https://raw.githubusercontent.com/satori/go.uuid/b061729afc07e77a8aa4fad0a2fd840958f1942a/uuid.go > /go/src/github.com/satori/go.uuid/uuid.go - run: go test -v ./... - run: go run tools/wstest.go `find features -name '*.feature'` + - run: go build -tags=gofuzz ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bfa02df --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.DS_Store + +# ignore generated corpus +fuzz/corpus/* +!fuzz/corpus/corpus_* +fuzz/crashers/* +fuzz/suppressions/* + +# ignore generated fuzz bin +worksheets-fuzz.zip + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c86e0c1 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +fuzz: clean + go-fuzz-build github.com/helloeave/worksheets/fuzz + go-fuzz -bin=./worksheets-fuzz.zip -workdir=fuzz/ + +clean: + rm -f worksheets-fuzz.zip + find fuzz/corpus/ -type f -not -name 'corpus_[0-9]*' -exec rm {} \; + rm -Rf fuzz/crashers/ + rm -Rf fuzz/suppressions/ diff --git a/fuzz/corpus/corpus_000 b/fuzz/corpus/corpus_000 new file mode 100644 index 0000000..8786692 --- /dev/null +++ b/fuzz/corpus/corpus_000 @@ -0,0 +1 @@ +worksheet simplest {} \ No newline at end of file diff --git a/fuzz/corpus/corpus_001 b/fuzz/corpus/corpus_001 new file mode 100644 index 0000000..5ebcf72 --- /dev/null +++ b/fuzz/corpus/corpus_001 @@ -0,0 +1,10 @@ +worksheet all_types { + 1:text text + 2:bool bool + 3:num_0 number[0] + 4:num_2 number[2] + 5:undefined undefined + 6:ws all_types + 7:slice_t []text + 8:slice_ws []all_types +} \ No newline at end of file diff --git a/fuzz/corpus/corpus_002 b/fuzz/corpus/corpus_002 new file mode 100644 index 0000000..44a4812 --- /dev/null +++ b/fuzz/corpus/corpus_002 @@ -0,0 +1,5 @@ +worksheet a_b_c_d { + 789 : foo computed_by { + external + } +} \ No newline at end of file diff --git a/fuzz/fuzz.go b/fuzz/fuzz.go new file mode 100644 index 0000000..5ea176a --- /dev/null +++ b/fuzz/fuzz.go @@ -0,0 +1,29 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build gofuzz + +package worksheets + +import ( + "strings" + + "github.com/helloeave/worksheets" +) + +func Fuzz(data []byte) int { + _, err := worksheets.NewDefinitions(strings.NewReader(string(data))) + if err != nil { + return 1 + } + return 0 +} diff --git a/parser.go b/parser.go index 5a474be..af60ea0 100644 --- a/parser.go +++ b/parser.go @@ -158,10 +158,13 @@ func (p *parser) parseField() (*Field, error) { if err != nil { return nil, err } - index, err := strconv.Atoi(sIndex) - if err != nil { - // unexpected since sIndex should conform to pIndex - panic(err) + index := maxFieldIndex + 1 + if len(sIndex) <= len(strconv.Itoa(maxFieldIndex)) { + index, err = strconv.Atoi(sIndex) + if err != nil { + // unexpected since sIndex should conform to pIndex + panic(err) + } } _, err = p.nextAndCheck(pColon) @@ -519,17 +522,12 @@ func (p *parser) parseRound() (*tRound, error) { } p.next() - sIndex, err := p.nextAndCheck(pIndex) + scale, err := p.parseScale() if err != nil { return nil, err } - index, err := strconv.Atoi(sIndex) - if err != nil { - // unexpected since sIndex should conform to pIndex - panic(err) - } - return &tRound{RoundingMode(mode), index}, nil + return &tRound{RoundingMode(mode), scale}, nil } func (p *parser) parseType() (Type, error) { @@ -563,15 +561,10 @@ func (p *parser) parseType() (Type, error) { if err != nil { return nil, err } - sScale, err := p.nextAndCheck(pIndex) + scale, err := p.parseScale() if err != nil { return nil, err } - scale, err := strconv.Atoi(sScale) - if err != nil { - // unexpected since sIndex should conform to pIndex - panic(err) - } _, err = p.nextAndCheck(pRbracket) if err != nil { return nil, err @@ -600,6 +593,27 @@ func (p *parser) parseType() (Type, error) { } } +const maxScale = 32 + +func (p *parser) parseScale() (int, error) { + sScale, err := p.nextAndCheck(pIndex) + if err != nil { + return -1, err + } + var scale int = maxScale + 1 + if len(sScale) <= len(strconv.Itoa(maxScale)) { + scale, err = strconv.Atoi(sScale) + if err != nil { + // unexpected since sScale should conform to pIndex + panic(err) + } + } + if scale > maxScale { + return -1, fmt.Errorf("scale cannot be greater than 32") + } + return scale, nil +} + func (p *parser) parseLiteral() (Value, error) { var err error var negNumber bool diff --git a/parser_test.go b/parser_test.go index 86aa614..050cf57 100644 --- a/parser_test.go +++ b/parser_test.go @@ -241,6 +241,9 @@ func (s *Zuite) TestParser_parseExpressionErrors() { `1_234._67`: `number fraction cannot start with underscore`, `1_234.+7`: `number cannot terminate with dot`, + `5 round down 33`: `scale cannot be greater than 32`, + `5 round down 9999999999999999999999999999999999999999999999999`: `scale cannot be greater than 32`, + // will need to revisit when we implement mod operator `4%0`: `number must terminate with percent if present`, `-1%_000`: `number must terminate with percent if present`, @@ -304,12 +307,14 @@ func (s *Zuite) TestParser_parseLiteral() { func (s *Zuite) TestParser_parseType() { cases := map[string]Type{ - `undefined`: &UndefinedType{}, - `text`: &TextType{}, - `bool`: &BoolType{}, - `number[5]`: &NumberType{5}, - `[]bool`: &SliceType{&BoolType{}}, - `foobar`: &Definition{name: "foobar"}, + `undefined`: &UndefinedType{}, + `text`: &TextType{}, + `bool`: &BoolType{}, + `number[5]`: &NumberType{5}, + `number[32]`: &NumberType{32}, + `[]bool`: &SliceType{&BoolType{}}, + `[][]number[9]`: &SliceType{&SliceType{&NumberType{9}}}, + `foobar`: &Definition{name: "foobar"}, } for input, expected := range cases { p := newParser(strings.NewReader(input)) @@ -319,6 +324,19 @@ func (s *Zuite) TestParser_parseType() { } } +func (s *Zuite) TestParser_parseTypeErrors() { + cases := map[string]string{ + `number[-7]`: `expected index, found -`, + `number[33]`: `scale cannot be greater than 32`, + `number[9999999999999999999999999999999999999999999999999]`: `scale cannot be greater than 32`, + } + for input, expected := range cases { + p := newParser(strings.NewReader(input)) + _, err := p.parseType() + assert.EqualError(s.T(), err, expected, input) + } +} + func (s *Zuite) TestTokenPatterns() { cases := []struct { pattern *tokenPattern diff --git a/tree.go b/tree.go index 6aab160..c136a45 100644 --- a/tree.go +++ b/tree.go @@ -14,8 +14,11 @@ package worksheets import ( "fmt" + "math" ) +const maxFieldIndex = math.MaxUint16 + type Definition struct { name string fieldsByName map[string]*Field @@ -32,6 +35,8 @@ func (def *Definition) addField(field *Field) error { if _, ok := def.fieldsByIndex[field.index]; ok { return fmt.Errorf("%s.%s: index %d cannot be reused", def.name, field.name, field.index) + } else if field.index > maxFieldIndex { + return fmt.Errorf("%s.%s: index cannot be greater than %d", def.name, field.name, maxFieldIndex) } def.fieldsByIndex[field.index] = field diff --git a/worksheets_test.go b/worksheets_test.go index 22f8226..72ada8a 100644 --- a/worksheets_test.go +++ b/worksheets_test.go @@ -49,6 +49,18 @@ func (s *Zuite) TestExample() { require.False(s.T(), isSet) } +func (s *Zuite) TestNewDefinitionsGood() { + cases := []string{ + `worksheet simple { + 65535:index_at_max bool + }`, + } + for _, ex := range cases { + _, err := NewDefinitions(strings.NewReader(ex)) + assert.NoError(s.T(), err, ex) + } +} + func (s *Zuite) TestNewDefinitionsErrors() { cases := map[string]string{ // crap input @@ -59,6 +71,14 @@ func (s *Zuite) TestNewDefinitionsErrors() { `work sheet`: `expecting worksheet`, // worksheet semantics + `worksheet simple { + 65536:index_too_large bool + }`: `simple.index_too_large: index cannot be greater than 65535`, + + `worksheet simple { + 9999999999999999999999999999999999999999999999999:index_too_large bool + }`: `simple.index_too_large: index cannot be greater than 65535`, + `worksheet simple { 0:no_can_do_with_zero bool }`: `simple.no_can_do_with_zero: index cannot be zero`,