Skip to content

Commit c11954e

Browse files
Add Fuzz test
Co-authored-by: Koen Bollen <[email protected]>
1 parent a373afd commit c11954e

File tree

9 files changed

+134
-5
lines changed

9 files changed

+134
-5
lines changed

filter/converter.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,16 @@ func (c *Converter) convertFilter(filter map[string]any, paramIndex int) (string
6464

6565
switch key {
6666
case "$or", "$and":
67-
orConditions, ok := anyToSliceMapAny(value)
67+
opConditions, ok := anyToSliceMapAny(value)
6868
if !ok {
6969
return "", nil, fmt.Errorf("invalid value for $or operator (must be array of objects): %v", value)
7070
}
71+
if len(opConditions) == 0 {
72+
return "", nil, fmt.Errorf("empty arrays not allowed")
73+
}
7174

7275
inner := []string{}
73-
for _, orCondition := range orConditions {
76+
for _, orCondition := range opConditions {
7477
innerConditions, innerValues, err := c.convertFilter(orCondition, paramIndex)
7578
if err != nil {
7679
return "", nil, err
@@ -89,8 +92,16 @@ func (c *Converter) convertFilter(filter map[string]any, paramIndex int) (string
8992
conditions = append(conditions, strings.Join(inner, " "+op+" "))
9093
}
9194
default:
95+
if !isValidPostgresIdentifier(key) {
96+
return "", nil, fmt.Errorf("invalid column name: %s", key)
97+
}
98+
9299
switch v := value.(type) {
93100
case map[string]any:
101+
if len(v) == 0 {
102+
return "", nil, fmt.Errorf("empty objects not allowed")
103+
}
104+
94105
inner := []string{}
95106
operators := []string{}
96107
for operator := range v {
@@ -104,7 +115,8 @@ func (c *Converter) convertFilter(filter map[string]any, paramIndex int) (string
104115
case "$and":
105116
return "", nil, fmt.Errorf("$and as scalar operator not supported")
106117
case "$in":
107-
inner = append(inner, fmt.Sprintf("(%s = ANY(?))", c.columnName(key)))
118+
paramIndex++
119+
inner = append(inner, fmt.Sprintf("(%s = ANY($%d))", c.columnName(key), paramIndex))
108120
if !isScalarSlice(v[operator]) {
109121
return "", nil, fmt.Errorf("invalid value for $in operator (must array of primatives): %v", v[operator])
110122
}

filter/converter_test.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ func TestConverter_Convert(t *testing.T) {
7777
"in-array operator simple",
7878
nil,
7979
`{"status": {"$in": ["NEW", "OPEN"]}}`,
80-
`("status" = ANY(?))`,
80+
`("status" = ANY($1))`,
8181
[]any{[]any{"NEW", "OPEN"}},
8282
nil,
8383
},
@@ -101,7 +101,7 @@ func TestConverter_Convert(t *testing.T) {
101101
"in-array operator with null value",
102102
nil,
103103
`{"status": {"$in": ["guest", null]}}`,
104-
`("status" = ANY(?))`,
104+
`("status" = ANY($1))`,
105105
[]any{[]any{"guest", nil}},
106106
nil,
107107
},
@@ -169,6 +169,30 @@ func TestConverter_Convert(t *testing.T) {
169169
[]any{"John", "Jane"},
170170
nil,
171171
},
172+
{
173+
"don't allow empty objects",
174+
nil,
175+
`{"name": {}}`,
176+
``,
177+
nil,
178+
fmt.Errorf("empty objects not allowed"),
179+
},
180+
{
181+
"don't allow empty arrays",
182+
nil,
183+
`{"$or": []}`,
184+
``,
185+
nil,
186+
fmt.Errorf("empty arrays not allowed"),
187+
},
188+
{
189+
"do allow empty $in arrays",
190+
nil,
191+
`{"status": {"$in": []}}`,
192+
`("status" = ANY($1))`,
193+
[]any{[]any{}},
194+
nil,
195+
},
172196
}
173197
for _, tt := range tests {
174198
t.Run(tt.name, func(t *testing.T) {

filter/fuzz_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package filter_test
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
pg_query "github.com/pganalyze/pg_query_go/v5"
8+
"github.com/poki/mongodb-filter-to-postgres/filter"
9+
)
10+
11+
func FuzzConverter(f *testing.F) {
12+
tcs := []string{
13+
`{"name": "John"}`,
14+
`{"age": 30, "name": "John"}`,
15+
`{"players": {"$gt": 0}}`,
16+
`{"age": {"$gte": 18}, "name": "John"}`,
17+
`{"created_at": {"$gte": "2020-01-01T00:00:00Z"}, "name": "John", "role": "admin"}`,
18+
`{"b": 1, "c": 2, "a": 3}`,
19+
`{"status": {"$in": ["NEW", "OPEN"]}}`,
20+
`{"status": {"$in": [{"hacker": 1}, "OPEN"]}}`,
21+
`{"status": {"$in": "text"}}`,
22+
`{"status": {"$in": ["guest", null]}}`,
23+
`{"$or": [{"name": "John"}, {"name": "Doe"}]}`,
24+
`{"$or": [{"org": "poki", "admin": true}, {"age": {"$gte": 18}}]}`,
25+
`{"$or": [{"$or": [{"name": "John"}, {"name": "Doe"}]}, {"name": "Jane"}]}`,
26+
`{"foo": { "$or": [ "bar", "baz" ] }}`,
27+
`{"$and": [{"name": "John"}, {"version": 3}]}`,
28+
`{"$and": [{"name": "John", "version": 3}]}`,
29+
`{"name": {"$regex": "John"}}`,
30+
`{"$or": [{"name": {"$regex": "John"}}, {"name": {"$regex": "Jane"}}]}`,
31+
`{"name": {}}`,
32+
`{"$or": []}`,
33+
`{"status": {"$in": []}}`,
34+
}
35+
for _, tc := range tcs {
36+
f.Add(tc)
37+
}
38+
39+
f.Fuzz(func(t *testing.T, in string) {
40+
c := filter.NewConverter()
41+
where, _, err := c.Convert([]byte(in))
42+
if err == nil && where != "" {
43+
j, err := pg_query.ParseToJSON("SELECT * FROM test WHERE 1 AND " + where)
44+
if err != nil {
45+
t.Fatalf("%q %q %v", in, where, err)
46+
}
47+
48+
if strings.Contains(j, "CommentStmt") {
49+
t.Fatal(where, "CommentStmt found")
50+
}
51+
}
52+
})
53+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
go test fuzz v1
2+
string("{\"A00\":{}, \"A\":\"\"}")
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
go test fuzz v1
2+
string("{}")
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
go test fuzz v1
2+
string("{\"\":\"0\"}")

filter/util.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,23 @@ func anyToSliceMapAny(v any) ([]map[string]any, bool) {
4444
return nil, false
4545
}
4646
}
47+
48+
func isValidPostgresIdentifier(s string) bool {
49+
if len(s) == 0 {
50+
return false
51+
}
52+
53+
// The first character needs to be a letter or _
54+
if !(s[0] >= 'a' && s[0] <= 'z') && !(s[0] >= 'A' && s[0] <= 'Z') && s[0] != '_' {
55+
return false
56+
}
57+
58+
for _, r := range s {
59+
if (r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || r == '_' {
60+
continue
61+
}
62+
return false
63+
}
64+
65+
return true
66+
}

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
module github.com/poki/mongodb-filter-to-postgres
22

33
go 1.21.0
4+
5+
require github.com/pganalyze/pg_query_go/v5 v5.1.0
6+
7+
require google.golang.org/protobuf v1.31.0 // indirect

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
2+
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
3+
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
4+
github.com/pganalyze/pg_query_go/v5 v5.1.0 h1:MlxQqHZnvA3cbRQYyIrjxEjzo560P6MyTgtlaf3pmXg=
5+
github.com/pganalyze/pg_query_go/v5 v5.1.0/go.mod h1:FsglvxidZsVN+Ltw3Ai6nTgPVcK2BPukH3jCDEqc1Ug=
6+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
7+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
8+
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
9+
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
10+
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=

0 commit comments

Comments
 (0)