diff --git a/README.md b/README.md index 9957ac2d..c477b7c3 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,13 @@ go tool cover -func coverage.out | grep total gotestsum --watch -- -coverprofile=coverage.out ``` +### test-fuzz + +```sh +./parser/v2/fuzz.sh +./parser/v2/goexpression/fuzz.sh +``` + ### benchmark Run benchmarks. diff --git a/parser/v2/elementparser.go b/parser/v2/elementparser.go index ec4ff3ff..b9ecf4b4 100644 --- a/parser/v2/elementparser.go +++ b/parser/v2/elementparser.go @@ -98,13 +98,41 @@ var ( }) ) +type attributeValueParser struct { + EqualsAndQuote parse.Parser[string] + Suffix parse.Parser[string] + UseSingleQuote bool +} + +func (avp attributeValueParser) Parse(pi *parse.Input) (value string, ok bool, err error) { + start := pi.Index() + if _, ok, err = avp.EqualsAndQuote.Parse(pi); err != nil || !ok { + return + } + if value, ok, err = parse.StringUntil(avp.Suffix).Parse(pi); err != nil || !ok { + pi.Seek(start) + return + } + if _, ok, err = avp.Suffix.Parse(pi); err != nil || !ok { + pi.Seek(start) + return + } + return value, true, nil +} + // Constant attribute. var ( - attributeConstantValueParser = parse.StringUntil(parse.Rune('"')) - attributeConstantValueSingleQuoteParser = parse.StringUntil(parse.Rune('\'')) - // A valid unquoted attribute value in HTML is any string of text that is not an empty string and that doesn’t contain spaces, tabs, line feeds, form feeds, carriage returns, ", ', `, =, <, or >. - attributeConstantValueUnquotedParser = parse.StringUntil(parse.Or(parse.RuneIn(" \t\n\r\"'`=<>/"), parse.EOF[string]())) - constantAttributeParser = parse.Func(func(pi *parse.Input) (attr ConstantAttribute, ok bool, err error) { + attributeValueParsers = []attributeValueParser{ + // Double quoted. + {EqualsAndQuote: parse.String(`="`), Suffix: parse.String(`"`), UseSingleQuote: false}, + // Single quoted. + {EqualsAndQuote: parse.String(`='`), Suffix: parse.String(`'`), UseSingleQuote: true}, + // Unquoted. + // A valid unquoted attribute value in HTML is any string of text that is not an empty string, + // and that doesn’t contain spaces, tabs, line feeds, form feeds, carriage returns, ", ', `, =, <, or >. + {EqualsAndQuote: parse.String("="), Suffix: parse.Any(parse.RuneIn(" \t\n\r\"'`=<>/"), parse.EOF[string]()), UseSingleQuote: false}, + } + constantAttributeParser = parse.Func(func(pi *parse.Input) (attr ConstantAttribute, ok bool, err error) { start := pi.Index() // Optional whitespace leader. @@ -119,63 +147,30 @@ var ( } attr.NameRange = NewRange(pi.PositionAt(pi.Index()-len(attr.Name)), pi.Position()) - // =" - var index int - attributeEquals := []parse.Parser[string]{ - parse.String(`="`), - parse.String(`='`), - parse.String(`=`), - } - valueParsers := []parse.Parser[string]{ - attributeConstantValueParser, - attributeConstantValueSingleQuoteParser, - attributeConstantValueUnquotedParser, - } - attributeClosers := []parse.Parser[string]{ - parse.String(`"`), - parse.String(`'`), - parse.Func(func(pi *parse.Input) (n string, ok bool, err error) { - return "", true, nil - }), - } - singleQuoteSetting := []bool{ - false, - true, - false, - } - var matched bool - for index = 0; index < len(attributeEquals); index++ { - if _, ok, err = attributeEquals[index].Parse(pi); err != nil || ok { - matched = true + for _, p := range attributeValueParsers { + attr.Value, ok, err = p.Parse(pi) + if err != nil { + pos := pi.Position() + if pErr, isParseError := err.(parse.ParseError); isParseError { + pos = pErr.Pos + } + return attr, false, parse.Error(fmt.Sprintf("%s: %v", attr.Name, err), pos) + } + if ok { + attr.SingleQuote = p.UseSingleQuote break } } - if err != nil || !matched { - pi.Seek(start) - return - } - - attr.SingleQuote = singleQuoteSetting[index] - valueParser := valueParsers[index] - closeParser := attributeClosers[index] - // Attribute value. - if attr.Value, ok, err = valueParser.Parse(pi); err != nil || !ok { + if !ok { pi.Seek(start) - return + return attr, false, nil } attr.Value = html.UnescapeString(attr.Value) - // Only use single quotes if actually required, due to double quote in the value (prefer double quotes). - if attr.SingleQuote && !strings.Contains(attr.Value, "\"") { - attr.SingleQuote = false - } - // " - closing quote. - if _, ok, err = closeParser.Parse(pi); err != nil || !ok { - err = parse.Error(fmt.Sprintf("missing closing quote on attribute %q", attr.Name), pi.Position()) - return - } + // Only use single quotes if actually required, due to double quote in the value (prefer double quotes). + attr.SingleQuote = attr.SingleQuote && strings.Contains(attr.Value, "\"") return attr, true, nil }) diff --git a/parser/v2/elementparser_test.go b/parser/v2/elementparser_test.go index 47d78dc4..19050c18 100644 --- a/parser/v2/elementparser_test.go +++ b/parser/v2/elementparser_test.go @@ -1786,3 +1786,21 @@ func TestBigElement(t *testing.T) { t.Errorf("unexpected failure to parse") } } + +func FuzzElement(f *testing.F) { + seeds := []string{ + `
`, + ``, + ``, + `
{ "test" }
`, + `
Test`, + } + + for _, tc := range seeds { + f.Add(tc) + } + + f.Fuzz(func(t *testing.T, input string) { + _, _, _ = element.Parse(parse.NewInput(input)) + }) +} diff --git a/parser/v2/fuzz.sh b/parser/v2/fuzz.sh new file mode 100755 index 00000000..cee8f72d --- /dev/null +++ b/parser/v2/fuzz.sh @@ -0,0 +1,2 @@ +echo Element +go test -fuzz=FuzzElement -fuzztime=120s