diff --git a/pkg/parse/fuzz_test.go b/pkg/parse/fuzz_test.go new file mode 100644 index 00000000..bd78ca15 --- /dev/null +++ b/pkg/parse/fuzz_test.go @@ -0,0 +1,17 @@ +//go:build go1.18 +// +build go1.18 + +package parse + +import ( + "testing" +) + +func FuzzParse(f *testing.F) { + f.Add("echo") + f.Add("put $x") + f.Add("put foo bar | each {|x| echo $x }") + f.Fuzz(func(t *testing.T, code string) { + Parse(Source{Name: "fuzz", Code: code}, Config{}) + }) +} diff --git a/pkg/parse/parse.go b/pkg/parse/parse.go index d3a0b439..8770693c 100644 --- a/pkg/parse/parse.go +++ b/pkg/parse/parse.go @@ -14,6 +14,7 @@ import ( "bytes" "fmt" "io" + "strings" "unicode" "src.elv.sh/pkg/diag" @@ -156,12 +157,29 @@ type Form struct { func (fn *Form) parse(ps *parser) { parseSpaces(fn, ps) - for fn.tryAssignment(ps) { + for startsCompound(ps.peek(), CmdExpr) { + initial := ps.save() + parsedCmd := ps.parse(&Compound{ExprCtx: CmdExpr}) + + if s := SourceText(parsedCmd.n); strings.ContainsRune(s, '=') { + postCmd := ps.save() + ps.restore(initial) + parsedAssignment := ps.parse(&Assignment{}) + if len(ps.errors.Entries) == len(initial.errors.Entries) { + parsedAssignment.addTo(&fn.Assignments, fn) + parseSpaces(fn, ps) + continue + } else { + ps.restore(postCmd) + } + } + + parsedCmd.addAs(&fn.Head, fn) parseSpaces(fn, ps) + break } - // Parse head. - if !startsCompound(ps.peek(), CmdExpr) { + if fn.Head == nil { if len(fn.Assignments) > 0 { // Assignment-only form. return @@ -169,8 +187,6 @@ func (fn *Form) parse(ps *parser) { // Bad form. ps.error(fmt.Errorf("bad rune at form head: %q", ps.peek())) } - ps.parse(&Compound{ExprCtx: CmdExpr}).addAs(&fn.Head, fn) - parseSpaces(fn, ps) for { r := ps.peek() @@ -202,27 +218,6 @@ func (fn *Form) parse(ps *parser) { } } -// tryAssignment tries to parse an assignment. If succeeded, it adds the parsed -// assignment to fn.Assignments and returns true. Otherwise it rewinds the -// parser and returns false. -func (fn *Form) tryAssignment(ps *parser) bool { - if !startsIndexing(ps.peek(), LHSExpr) { - return false - } - - pos := ps.pos - errorEntries := ps.errors.Entries - parsedAssignment := ps.parse(&Assignment{}) - // If errors were added, revert - if len(ps.errors.Entries) > len(errorEntries) { - ps.errors.Entries = errorEntries - ps.pos = pos - return false - } - parsedAssignment.addTo(&fn.Assignments, fn) - return true -} - func startsForm(r rune) bool { return IsInlineWhitespace(r) || startsCompound(r, CmdExpr) } diff --git a/pkg/parse/parser.go b/pkg/parse/parser.go index 0452fa10..6bbe582d 100644 --- a/pkg/parse/parser.go +++ b/pkg/parse/parser.go @@ -33,6 +33,20 @@ func (ps *parser) parse(n Node) parsed { return parsed{n} } +type parserState struct { + pos int + overEOF int + errors Error +} + +func (ps *parser) save() parserState { + return parserState{ps.pos, ps.overEOF, ps.errors} +} + +func (ps *parser) restore(s parserState) { + ps.pos, ps.overEOF, ps.errors = s.pos, s.overEOF, s.errors +} + var nodeType = reflect.TypeOf((*Node)(nil)).Elem() type parsed struct { diff --git a/pkg/parse/testdata/fuzz/FuzzParse/20ffbed79ec93b4b3f5872e6a329fee708da4b901c1b9c9ac10912ea7e862c9c b/pkg/parse/testdata/fuzz/FuzzParse/20ffbed79ec93b4b3f5872e6a329fee708da4b901c1b9c9ac10912ea7e862c9c new file mode 100644 index 00000000..7994bf20 --- /dev/null +++ b/pkg/parse/testdata/fuzz/FuzzParse/20ffbed79ec93b4b3f5872e6a329fee708da4b901c1b9c9ac10912ea7e862c9c @@ -0,0 +1,2 @@ +go test fuzz v1 +string("(((((((((((((((((((((((((((((((($'")