Skip to content

Commit 42b159b

Browse files
authored
🧹 chore: Handle nil map targets in Binder (#3839)
1 parent bc74824 commit 42b159b

File tree

4 files changed

+67
-16
lines changed

4 files changed

+67
-16
lines changed

binder/binder.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
var (
1010
ErrSuitableContentNotFound = errors.New("binder: suitable content not found to parse body")
1111
ErrMapNotConvertible = errors.New("binder: map is not convertible to map[string]string or map[string][]string")
12+
ErrMapNilDestination = errors.New("binder: map destination is nil and cannot be initialized")
1213
)
1314

1415
var HeaderBinderPool = sync.Pool{

binder/binder_test.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,19 @@ func Test_GetFieldCache_Panic(t *testing.T) {
7272
func Test_parseToMap_defaultCase(t *testing.T) {
7373
t.Parallel()
7474
m := map[string]int{}
75-
err := parseToMap(m, map[string][]string{"a": {"1"}})
75+
err := parseToMap(reflect.ValueOf(m), map[string][]string{"a": {"1"}})
7676
require.NoError(t, err)
7777
require.Empty(t, m)
7878

7979
m2 := map[string]string{}
80-
err = parseToMap(m2, map[string][]string{"empty": {}})
80+
err = parseToMap(reflect.ValueOf(m2), map[string][]string{"empty": {}})
8181
require.NoError(t, err)
8282
require.Empty(t, m2["empty"])
83+
84+
var zeroStringMap map[string]string
85+
err = parseToMap(reflect.ValueOf(&zeroStringMap).Elem(), map[string][]string{"name": {"john"}})
86+
require.NoError(t, err)
87+
require.Equal(t, "john", zeroStringMap["name"])
8388
}
8489

8590
func Test_parse_function_maps(t *testing.T) {
@@ -94,6 +99,16 @@ func Test_parse_function_maps(t *testing.T) {
9499
err = parse("query", &m2, map[string][]string{"a": {"b"}})
95100
require.NoError(t, err)
96101
require.Equal(t, "b", m2["a"])
102+
103+
var zeroStringMap map[string]string
104+
err = parse("query", &zeroStringMap, map[string][]string{"foo": {"bar", "baz"}})
105+
require.NoError(t, err)
106+
require.Equal(t, "baz", zeroStringMap["foo"])
107+
108+
var zeroSliceMap map[string][]string
109+
err = parse("query", &zeroSliceMap, map[string][]string{"foo": {"bar", "baz"}})
110+
require.NoError(t, err)
111+
require.Equal(t, []string{"bar", "baz"}, zeroSliceMap["foo"])
97112
}
98113

99114
func Test_SetParserDecoder_UnknownKeys(t *testing.T) {

binder/mapping.go

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func parse(aliasTag string, out any, data map[string][]string, files ...map[stri
8181

8282
// Parse into the map
8383
if ptrVal.Kind() == reflect.Map && ptrVal.Type().Key().Kind() == reflect.String {
84-
return parseToMap(ptrVal.Interface(), data)
84+
return parseToMap(ptrVal, data)
8585
}
8686

8787
// Parse into the struct
@@ -106,19 +106,36 @@ func parseToStruct(aliasTag string, out any, data map[string][]string, files ...
106106

107107
// Parse data into the map
108108
// thanks to https://github.com/gin-gonic/gin/blob/master/binding/binding.go
109-
func parseToMap(ptr any, data map[string][]string) error {
110-
elem := reflect.TypeOf(ptr).Elem()
109+
func parseToMap(target reflect.Value, data map[string][]string) error {
110+
if !target.IsValid() {
111+
return errors.New("binder: invalid destination value")
112+
}
113+
114+
if target.Kind() == reflect.Interface && !target.IsNil() {
115+
target = target.Elem()
116+
}
117+
118+
if target.Kind() != reflect.Map || target.Type().Key().Kind() != reflect.String {
119+
return nil // nothing to do for non-map destinations
120+
}
121+
122+
if target.IsNil() {
123+
if !target.CanSet() {
124+
return ErrMapNilDestination
125+
}
126+
target.Set(reflect.MakeMap(target.Type()))
127+
}
111128

112-
switch elem.Kind() {
129+
switch target.Type().Elem().Kind() {
113130
case reflect.Slice:
114-
newMap, ok := ptr.(map[string][]string)
131+
newMap, ok := target.Interface().(map[string][]string)
115132
if !ok {
116133
return ErrMapNotConvertible
117134
}
118135

119136
maps.Copy(newMap, data)
120-
case reflect.String, reflect.Interface:
121-
newMap, ok := ptr.(map[string]string)
137+
case reflect.String:
138+
newMap, ok := target.Interface().(map[string]string)
122139
if !ok {
123140
return ErrMapNotConvertible
124141
}
@@ -131,6 +148,10 @@ func parseToMap(ptr any, data map[string][]string) error {
131148
newMap[k] = v[len(v)-1]
132149
}
133150
default:
151+
// Interface element maps (e.g. map[string]any) are left untouched because
152+
// the binder cannot safely infer element conversions without mutating
153+
// caller-provided values. These destinations therefore see a successful
154+
// no-op parse.
134155
return nil // it's not necessary to check all types
135156
}
136157

binder/mapping_test.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ func Test_parseToMap(t *testing.T) {
133133

134134
// Test map[string]string
135135
m := make(map[string]string)
136-
err := parseToMap(m, inputMap)
136+
err := parseToMap(reflect.ValueOf(m), inputMap)
137137
require.NoError(t, err)
138138

139139
require.Equal(t, "value2", m["key1"])
@@ -142,7 +142,7 @@ func Test_parseToMap(t *testing.T) {
142142

143143
// Test map[string][]string
144144
m2 := make(map[string][]string)
145-
err = parseToMap(m2, inputMap)
145+
err = parseToMap(reflect.ValueOf(m2), inputMap)
146146
require.NoError(t, err)
147147

148148
require.Len(t, m2["key1"], 2)
@@ -153,8 +153,22 @@ func Test_parseToMap(t *testing.T) {
153153

154154
// Test map[string]any
155155
m3 := make(map[string]any)
156-
err = parseToMap(m3, inputMap)
157-
require.ErrorIs(t, err, ErrMapNotConvertible)
156+
err = parseToMap(reflect.ValueOf(m3), inputMap)
157+
require.NoError(t, err)
158+
require.Empty(t, m3)
159+
160+
var zeroStringMap map[string]string
161+
err = parseToMap(reflect.ValueOf(&zeroStringMap).Elem(), inputMap)
162+
require.NoError(t, err)
163+
require.Equal(t, "value2", zeroStringMap["key1"])
164+
165+
var zeroSliceMap map[string][]string
166+
err = parseToMap(reflect.ValueOf(&zeroSliceMap).Elem(), inputMap)
167+
require.NoError(t, err)
168+
require.Len(t, zeroSliceMap["key1"], 2)
169+
170+
err = parseToMap(reflect.ValueOf(map[string]string(nil)), inputMap)
171+
require.ErrorIs(t, err, ErrMapNilDestination)
158172
}
159173

160174
func Test_FilterFlags(t *testing.T) {
@@ -384,16 +398,16 @@ func Test_parseToMap_Extended(t *testing.T) {
384398
}
385399

386400
m := make(map[string]string)
387-
err := parseToMap(m, data)
401+
err := parseToMap(reflect.ValueOf(m), data)
388402
require.NoError(t, err)
389403
require.Empty(t, m["empty"])
390404

391405
m2 := make(map[string][]int)
392-
err = parseToMap(m2, data)
406+
err = parseToMap(reflect.ValueOf(m2), data)
393407
require.ErrorIs(t, err, ErrMapNotConvertible)
394408

395409
m3 := make(map[string]int)
396-
err = parseToMap(m3, data)
410+
err = parseToMap(reflect.ValueOf(m3), data)
397411
require.NoError(t, err)
398412
}
399413

0 commit comments

Comments
 (0)