Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add fuzz testing for parser and handle escaped colons #10

Merged
merged 3 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions parser/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package parser_test

import (
"strings"
"testing"

"github.com/laojianzi/kql-go/parser"
"github.com/stretchr/testify/assert"
)

func FuzzParser(f *testing.F) {
// Add initial corpus
seeds := []string{
"field:value",
"field: value",
"field : value",
`field: "value"`,
"field: *",
"field: value*",
"field: *value",
"field: *value*",
"field > 10",
"field >= 10",
"field < 10",
"field <= 10",
"field: true",
"field: false",
"field: null",
"field1: value1 AND field2: value2",
"field1: value1 OR field2: value2",
"NOT field: value",
"(field: value)",
"(field1: value1) AND (field2: value2)",
`field: "value with spaces"`,
`field: "value with \"escaped\" quotes"`,
`field: "value with \n newline"`,
"field1: value1 AND field2: value2 OR field3: value3",
"field1: (value1 OR value2) AND field2: value3",
}

for _, seed := range seeds {
f.Add(seed)
}

f.Fuzz(func(t *testing.T, query string) {
if strings.TrimSpace(query) == "" {
return
}

// TODO: Current fuzzing implementation has limitations in input/output validation.
// This test only covers basic safety checks:
// 1. No panics during parsing
// 2. String() output can be re-parsed
// 3. String() output remains stable
//
// Contributions welcome for better validation approaches :)
stmt, err := parser.New(query).Stmt()
if err != nil || stmt == nil {
return
}

stmt2, err := parser.New(stmt.String()).Stmt()
assert.NoError(t, err)
assert.Equal(t, stmt.String(), stmt2.String())
})
}
13 changes: 10 additions & 3 deletions parser/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,22 @@

// shouldBreak checks if token collection should stop
func (l *defaultLexer) shouldBreak(i int, isString, withEscape bool, endChar rune) bool {
if isString && !withEscape && l.peek(i) == endChar {
ch := l.peek(i)
if isString && !withEscape && ch == endChar {
return true
}

if !isString && !withEscape {
if unicode.IsSpace(l.peek(i)) || l.peek(i) == ')' {
if unicode.IsSpace(ch) || ch == ')' || ch == ':' {
return true
}
}

// not \:
if !isString && withEscape && ch == ':' && (!l.peekOk(i-1) || l.peek(i-1) != '\\') {
return true
}

return false
}

Expand All @@ -121,8 +127,9 @@
buf.WriteRune(l.peek(start))

for j := start; l.peekOk(j + 1); j++ {
currentRune := l.peek(j)
nextRune := l.peek(j + 1)
if unicode.IsSpace(nextRune) || nextRune == ')' {
if currentRune != '\\' && (unicode.IsSpace(nextRune) || nextRune == ')' || nextRune == ':') {

Check failure on line 132 in parser/lexer.go

View workflow job for this annotation

GitHub Actions / Lint/Build/Test (1.16.x, ubuntu-latest)

only one cuddle assignment allowed before if statement (wsl)
break
}

Expand Down
2 changes: 1 addition & 1 deletion parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func (p *defaultParser) parseBinary() (ast.Expr, error) {
}

op := p.lexer.Token.Kind
if !op.IsOperator() {
if !op.IsOperator() || !p.lexer.lastTokenKind.IsField() {
return ast.NewBinaryExpr(pos, "", 0, expr, hasNot), nil
}

Expand Down
42 changes: 42 additions & 0 deletions parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -740,3 +740,45 @@ func TestParser_ComplexQueries(t *testing.T) {
})
}
}

func TestParserFuzzBugs(t *testing.T) {
tests := []struct {
query string
want ast.Expr
wantErr error
}{
{
"0 :0",
nil,
errors.New("line 0:2 expected keyword OR|AND|NOT, but got \":\"\n0 :0\n ^\n"),
},
{
"\\AND :0",
ast.NewBinaryExpr(0, "\\AND", token.TokenKindOperatorEql, ast.NewLiteral(
6, 7, token.TokenKindInt, "0", nil,
), false),
nil,
},
{
"\\AND: 0",
ast.NewBinaryExpr(0, "\\AND", token.TokenKindOperatorEql, ast.NewLiteral(
6, 7, token.TokenKindInt, "0", nil,
), false),
nil,
},
}

for _, tt := range tests {
t.Run(tt.query, func(t *testing.T) {
stmt, err := parser.New(tt.query).Stmt()

if tt.wantErr == nil {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tt.wantErr.Error())
}

assert.EqualValues(t, tt.want, stmt)
})
}
}
2 changes: 2 additions & 0 deletions parser/testdata/fuzz/FuzzParser/45eecbed51a417f5
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
string("\\AND :0")
2 changes: 2 additions & 0 deletions parser/testdata/fuzz/FuzzParser/74ba8e9a0ce951ee
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
string("0 :0")
Loading