Skip to content

Commit 26d3515

Browse files
committed
v2.1.4
* Fixed a bug with UTF-8 special characters in JSON keys * Made fuzz tests easier to understand/debug in case of failures Signed-off-by: Jean Rouge <[email protected]>
1 parent 1a1f8a3 commit 26d3515

13 files changed

+214
-106
lines changed

.circleci/circle_build.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
set -ex
44

5+
# might as well run a little longer
6+
export FUZZ_TIME=25s
7+
58
# there are too many golangci plugins that don't work for 1.19 just yet, so just skip linting for it
69
if [[ "$GO_VER" == 1.18.* ]]; then
710
make
811
else
9-
make test fuzz
12+
make test_with_fuzz
1013
fi

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
[comment]: # (Changes since last release go here)
44

5+
# 2.1.4 - Dec 12th 2022
6+
7+
* Fixed a bug with UTF-8 special characters in JSON keys
8+
59
# 2.1.3 - Dec 11th 2022
610

711
* Added support for JSON marshalling/unmarshalling of wrapper of primitive types

Makefile

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
.DEFAULT_GOAL := all
22

33
.PHONY: all
4-
all: test lint fuzz
4+
all: test_with_fuzz lint
55

66
# the TEST_FLAGS env var can be set to eg run only specific tests
7+
TEST_COMMAND = go test -v -count=1 -race -cover $(TEST_FLAGS)
8+
79
.PHONY: test
810
test:
9-
go test -v -count=1 -race -cover $(TEST_FLAGS)
11+
$(TEST_COMMAND)
1012

1113
.PHONY: bench
1214
bench:
1315
go test -bench=.
1416

17+
FUZZ_TIME ?= 10s
18+
19+
.PHONY: test_with_fuzz
20+
test_with_fuzz:
21+
$(TEST_COMMAND) -fuzz=. -fuzztime=$(FUZZ_TIME)
22+
1523
.PHONY: fuzz
16-
fuzz:
17-
go test -fuzz=. -fuzztime=10s ./...
24+
fuzz: test_with_fuzz
1825

1926
.PHONY: lint
2027
lint:

fuzz_test.go

Lines changed: 0 additions & 77 deletions
This file was deleted.

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ require (
66
github.com/bahlo/generic-list-go v0.2.0
77
github.com/buger/jsonparser v1.1.1
88
github.com/mailru/easyjson v0.7.7
9-
github.com/stretchr/testify v1.6.1
9+
github.com/stretchr/testify v1.8.1
1010
)
1111

1212
require (
13-
github.com/davecgh/go-spew v1.1.0 // indirect
13+
github.com/davecgh/go-spew v1.1.1 // indirect
1414
github.com/pmezard/go-difflib v1.0.0 // indirect
15-
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
15+
gopkg.in/yaml.v3 v3.0.1 // indirect
1616
)

go.sum

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn
22
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
33
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
44
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
5-
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
65
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
7+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
78
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
89
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
910
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
1011
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
1112
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
1213
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
13-
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
14-
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
14+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
15+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
16+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
17+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
18+
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
19+
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
1520
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
1621
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
17-
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
1822
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
23+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
24+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

json.go

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"fmt"
88
"reflect"
9+
"unicode/utf8"
910

1011
"github.com/buger/jsonparser"
1112
"github.com/mailru/easyjson/jwriter"
@@ -116,30 +117,33 @@ func (om *OrderedMap[K, V]) UnmarshalJSON(data []byte) error {
116117
var key K
117118
var value V
118119

119-
switch tkp := any(&key).(type) {
120+
switch typedKey := any(&key).(type) {
120121
case *string:
121-
*tkp = string(keyData)
122-
case encoding.TextUnmarshaler:
123-
if err := tkp.UnmarshalText(keyData); err != nil {
122+
s, err := decodeUTF8(keyData)
123+
if err != nil {
124124
return err
125125
}
126-
case *encoding.TextUnmarshaler:
127-
// This is to preserve compatibility with original implementation
128-
// that handled none pointer receivers, but I (xiegeo) believes this is unused.
129-
if err := (*tkp).UnmarshalText(keyData); err != nil {
126+
*typedKey = s
127+
case encoding.TextUnmarshaler:
128+
if err := typedKey.UnmarshalText(keyData); err != nil {
130129
return err
131130
}
132131
case *int, *int8, *int16, *int32, *int64, *uint, *uint8, *uint16, *uint32, *uint64:
133-
if err := json.Unmarshal(keyData, tkp); err != nil {
132+
if err := json.Unmarshal(keyData, typedKey); err != nil {
134133
return err
135134
}
136135
default:
137136
// this switch takes care of wrapper types around primitive types, such as
138137
// type myType string
139138
switch reflect.TypeOf(key).Kind() {
140139
case reflect.String:
141-
convertedkeyData := reflect.ValueOf(keyData).Convert(reflect.TypeOf(key))
142-
reflect.ValueOf(&key).Elem().Set(convertedkeyData)
140+
s, err := decodeUTF8(keyData)
141+
if err != nil {
142+
return err
143+
}
144+
145+
convertedKeyData := reflect.ValueOf(s).Convert(reflect.TypeOf(key))
146+
reflect.ValueOf(&key).Elem().Set(convertedKeyData)
143147
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
144148
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
145149
if err := json.Unmarshal(keyData, &key); err != nil {
@@ -158,3 +162,21 @@ func (om *OrderedMap[K, V]) UnmarshalJSON(data []byte) error {
158162
return nil
159163
})
160164
}
165+
166+
func decodeUTF8(input []byte) (string, error) {
167+
remaining, offset := input, 0
168+
runes := make([]rune, 0, len(remaining))
169+
170+
for len(remaining) > 0 {
171+
r, size := utf8.DecodeRune(remaining)
172+
if r == utf8.RuneError && size <= 1 {
173+
return "", fmt.Errorf("not a valid UTF-8 string (at position %d): %s", offset, string(input))
174+
}
175+
176+
runes = append(runes, r)
177+
remaining = remaining[size:]
178+
offset += size
179+
}
180+
181+
return string(runes), nil
182+
}

json_fuzz_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package orderedmap
2+
3+
// Adapted from https://github.com/dvyukov/go-fuzz-corpus/blob/c42c1b2/json/json.go
4+
5+
import (
6+
"encoding/json"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
"testing"
10+
)
11+
12+
func FuzzRoundTrip(f *testing.F) {
13+
f.Fuzz(func(t *testing.T, data []byte) {
14+
for _, testCase := range []struct {
15+
name string
16+
constructor func() any
17+
// should be a function that asserts that 2 objects of the type returned by constructor are equal
18+
equalityAssertion func(*testing.T, any, any) bool
19+
}{
20+
{
21+
name: "with a string -> string map",
22+
constructor: func() any { return &OrderedMap[string, string]{} },
23+
equalityAssertion: assertOrderedMapsEqual[string, string],
24+
},
25+
{
26+
name: "with a string -> int map",
27+
constructor: func() any { return &OrderedMap[string, int]{} },
28+
equalityAssertion: assertOrderedMapsEqual[string, int],
29+
},
30+
{
31+
name: "with a string -> any map",
32+
constructor: func() any { return &OrderedMap[string, any]{} },
33+
equalityAssertion: assertOrderedMapsEqual[string, any],
34+
},
35+
{
36+
name: "with a struct with map fields",
37+
constructor: func() any { return new(testFuzzStruct) },
38+
equalityAssertion: assertTestFuzzStructEqual,
39+
},
40+
} {
41+
t.Run(testCase.name, func(t *testing.T) {
42+
v1 := testCase.constructor()
43+
if json.Unmarshal(data, v1) != nil {
44+
return
45+
}
46+
47+
jsonData, err := json.Marshal(v1)
48+
require.NoError(t, err)
49+
50+
v2 := testCase.constructor()
51+
require.NoError(t, json.Unmarshal(jsonData, v2))
52+
53+
if !assert.True(t, testCase.equalityAssertion(t, v1, v2), "failed with input data %q", string(data)) {
54+
// look at that what the standard lib does with regular map, to help with debugging
55+
56+
var m1 map[string]any
57+
require.NoError(t, json.Unmarshal(data, &m1))
58+
59+
mapJsonData, err := json.Marshal(m1)
60+
require.NoError(t, err)
61+
62+
var m2 map[string]any
63+
require.NoError(t, json.Unmarshal(mapJsonData, &m2))
64+
65+
t.Logf("initial data = %s", string(data))
66+
t.Logf("unmarshalled map = %v", m1)
67+
t.Logf("re-marshalled from map = %s", string(mapJsonData))
68+
t.Logf("re-marshalled from test obj = %s", string(jsonData))
69+
t.Logf("re-unmarshalled map = %s", m2)
70+
}
71+
})
72+
}
73+
})
74+
}
75+
76+
// only works for fairly basic maps, that's why it's just in this file
77+
func assertOrderedMapsEqual[K comparable, V any](t *testing.T, v1, v2 any) bool {
78+
om1, ok1 := v1.(*OrderedMap[K, V])
79+
om2, ok2 := v2.(*OrderedMap[K, V])
80+
81+
if !assert.True(t, ok1, "v1 not an orderedmap") ||
82+
!assert.True(t, ok2, "v2 not an orderedmap") {
83+
return false
84+
}
85+
86+
success := assert.Equal(t, om1.Len(), om2.Len(), "om1 and om2 have different lengths: %d vs %d", om1.Len(), om2.Len())
87+
88+
for i, pair1, pair2 := 0, om1.Oldest(), om2.Oldest(); pair1 != nil && pair2 != nil; i, pair1, pair2 = i+1, pair1.Next(), pair2.Next() {
89+
success = assert.Equal(t, pair1.Key, pair2.Key, "different keys at position %d: %v vs %v", i, pair1.Key, pair2.Key) && success
90+
success = assert.Equal(t, pair1.Value, pair2.Value, "different values at position %d: %v vs %v", i, pair1.Value, pair2.Value) && success
91+
}
92+
93+
return success
94+
}
95+
96+
type testFuzzStruct struct {
97+
M1 *OrderedMap[int, any]
98+
M2 *OrderedMap[int, string]
99+
M3 *OrderedMap[string, string]
100+
}
101+
102+
func assertTestFuzzStructEqual(t *testing.T, v1, v2 any) bool {
103+
s1, ok := v1.(*testFuzzStruct)
104+
s2, ok := v2.(*testFuzzStruct)
105+
106+
if !assert.True(t, ok, "v1 not an testFuzzStruct") ||
107+
!assert.True(t, ok, "v2 not an testFuzzStruct") {
108+
return false
109+
}
110+
111+
success := assertOrderedMapsEqual[int, any](t, s1.M1, s2.M1)
112+
success = assertOrderedMapsEqual[int, string](t, s1.M2, s2.M2) && success
113+
success = assertOrderedMapsEqual[string, string](t, s1.M3, s2.M3) && success
114+
115+
return success
116+
}

0 commit comments

Comments
 (0)