diff --git a/query/language_fuzz_test.go b/query/language_fuzz_test.go new file mode 100644 index 0000000..821f88e --- /dev/null +++ b/query/language_fuzz_test.go @@ -0,0 +1,97 @@ +package query + +import ( + "testing" +) + +// FuzzParse exercises the query DSL parser and normalizer with arbitrary input. +// The goal is to ensure Parse never panics regardless of what a caller sends. +// +// Notable panic sites in normalize.go that the fuzzer can reach: +// - Expression.invert(): "This should never happen!" (via NOT-paren paths) +// - EqualExpr.Normalize(): "Called EqualExpr::Normalize on a paren, this is a bug!" +// - EqualExpr.Normalize(): "This should not happen!" +// - EqualExpr.invert(): "This should not happen!" +func FuzzParse(f *testing.F) { + seeds := []string{ + // match-all shortcuts + "$all", + "*", + + // equality + `name = "value"`, + `name != "value"`, + `$key = 0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890`, + `$owner = 0x1234567890abcdef1234567890abcdef12345678`, + `$creator = 0x1234567890abcdef1234567890abcdef12345678`, + `$expiration = 1000`, + `$sequence = 42`, + + // comparisons (numeric and string) + `count > 5`, + `count >= 5`, + `count < 5`, + `count <= 5`, + `name > "bar"`, + `name >= "bar"`, + + // glob + `name ~ "foo*"`, + `name !~ "foo*"`, + `name glob "foo*"`, + `name not glob "foo*"`, + + // inclusion + `name IN ("a" "b" "c")`, + `name NOT IN ("a" "b")`, + `count IN (1 2 3)`, + `count NOT IN (1 2 3)`, + + // boolean combinations + `name = "x" || other = "y"`, + `name = "x" && other = "y"`, + `name = "x" OR other = "y"`, + `name = "x" AND other = "y"`, + `name = "x" or other = "y"`, + `name = "x" and other = "y"`, + + // parenthesised groups + `(name = "x")`, + `(name = "x" && other = "y")`, + `(name = "x" || other = "y") && third = "z"`, + `name = "a" && (b = "c" || d = "e")`, + + // NOT-paren (exercises the invert() path in normalize.go) + `!(name = "value")`, + `not(name = "value")`, + `NOT(name = "value")`, + `!(name = "x" && other = "y")`, + `!(name = "x" || other = "y")`, + `!(!(name = "value"))`, + `!(name ~ "foo*")`, + `!(name IN ("a" "b"))`, + `!(count > 5)`, + + // deeply nested + `((name = "x"))`, + `!((name = "x" || y = "z") && w = "v")`, + `(a = "1" || b = "2") && (c = "3" || d = "4")`, + + // empty / near-empty + "", + } + + for _, s := range seeds { + f.Add(s) + } + + f.Fuzz(func(t *testing.T, input string) { + // Must not panic. Errors are fine — they represent rejected queries. + defer func() { + if r := recover(); r != nil { + t.Errorf("Parse panicked on input %q: %v", input, r) + } + }() + _, _ = Parse(input) + }) +} diff --git a/query/normalize.go b/query/normalize.go index 03afd96..62711a0 100644 --- a/query/normalize.go +++ b/query/normalize.go @@ -309,6 +309,9 @@ func (e *Glob) invert() *Glob { func (e *LessThan) Normalize() *LessThan { switch e.Var { case KeyAttributeKey, OwnerAttributeKey, CreatorAttributeKey: + if e.Value.String == nil { + return e + } val := strings.ToLower(*e.Value.String) return &LessThan{ Var: e.Var, @@ -331,6 +334,9 @@ func (e *LessThan) invert() *GreaterOrEqualThan { func (e *LessOrEqualThan) Normalize() *LessOrEqualThan { switch e.Var { case KeyAttributeKey, OwnerAttributeKey, CreatorAttributeKey: + if e.Value.String == nil { + return e + } val := strings.ToLower(*e.Value.String) return &LessOrEqualThan{ Var: e.Var, @@ -353,6 +359,9 @@ func (e *LessOrEqualThan) invert() *GreaterThan { func (e *GreaterThan) Normalize() *GreaterThan { switch e.Var { case KeyAttributeKey, OwnerAttributeKey, CreatorAttributeKey: + if e.Value.String == nil { + return e + } val := strings.ToLower(*e.Value.String) return &GreaterThan{ Var: e.Var, @@ -375,6 +384,9 @@ func (e *GreaterThan) invert() *LessOrEqualThan { func (e *GreaterOrEqualThan) Normalize() *GreaterOrEqualThan { switch e.Var { case KeyAttributeKey, OwnerAttributeKey, CreatorAttributeKey: + if e.Value.String == nil { + return e + } val := strings.ToLower(*e.Value.String) return &GreaterOrEqualThan{ Var: e.Var, @@ -397,6 +409,9 @@ func (e *GreaterOrEqualThan) invert() *LessThan { func (e *Equality) Normalize() *Equality { switch e.Var { case KeyAttributeKey, OwnerAttributeKey, CreatorAttributeKey: + if e.Value.String == nil { + return e + } val := strings.ToLower(*e.Value.String) return &Equality{ Var: e.Var, diff --git a/query/testdata/fuzz/FuzzParse/key_equals_number b/query/testdata/fuzz/FuzzParse/key_equals_number new file mode 100644 index 0000000..2c81227 --- /dev/null +++ b/query/testdata/fuzz/FuzzParse/key_equals_number @@ -0,0 +1,2 @@ +go test fuzz v1 +string("$key =0")