// Copyright (C) 2023 Luke Shumaker // // SPDX-License-Identifier: GPL-2.0-or-later package json_test import ( "bytes" "reflect" "strings" "testing" "github.com/stretchr/testify/assert" // When adding new testcases, comment this out and import // "encoding/json", to validate your testcase. "git.lukeshu.com/go/lowmemjson/compat/json" ) func TestCompatHTMLEscape(t *testing.T) { t.Parallel() type testcase struct { In string Out string } testcases := map[string]testcase{ "invalid": {In: `x`, Out: `x`}, "hex-lower": {In: `"\uabcd"`, Out: `"\uabcd"`}, "hex-upper": {In: `"\uABCD"`, Out: `"\uABCD"`}, "hex-mixed": {In: `"\uAbCd"`, Out: `"\uAbCd"`}, } for tcName, tc := range testcases { tc := tc t.Run(tcName, func(t *testing.T) { t.Parallel() t.Logf("in=%q", tc.In) var dst bytes.Buffer json.HTMLEscape(&dst, []byte(tc.In)) assert.Equal(t, tc.Out, dst.String()) }) } } func TestCompatValid(t *testing.T) { t.Parallel() type testcase struct { In string Exp bool } testcases := map[string]testcase{ "empty": {In: ``, Exp: false}, "num": {In: `1`, Exp: true}, "trunc": {In: `{`, Exp: false}, "object": {In: `{}`, Exp: true}, "non-utf8": {In: "\"\x85\xcd\"", Exp: false}, // https://github.com/golang/go/issues/58517 "hex-lower": {In: `"\uabcd"`, Exp: true}, "hex-upper": {In: `"\uABCD"`, Exp: true}, "hex-mixed": {In: `"\uAbCd"`, Exp: true}, } for tcName, tc := range testcases { tc := tc t.Run(tcName, func(t *testing.T) { t.Parallel() t.Logf("in=%q", tc.In) act := json.Valid([]byte(tc.In)) assert.Equal(t, tc.Exp, act) }) } } func TestCompatCompact(t *testing.T) { t.Parallel() type testcase struct { In string Out string Err string } testcases := map[string]testcase{ "empty": {In: ``, Out: ``, Err: `unexpected end of JSON input`}, "trunc": {In: `{`, Out: ``, Err: `unexpected end of JSON input`}, "object": {In: `{}`, Out: `{}`}, "non-utf8": {In: "\"\x85\xcd\"", Out: "\"\x85\xcd\""}, "float": {In: `1.200e003`, Out: `1.200e003`}, "hex-lower": {In: `"\uabcd"`, Out: `"\uabcd"`}, "hex-upper": {In: `"\uABCD"`, Out: `"\uABCD"`}, "hex-mixed": {In: `"\uAbCd"`, Out: `"\uAbCd"`}, "invalid-utf8": {In: "\x85", Err: `invalid character '\x85' looking for beginning of value`}, } for tcName, tc := range testcases { tc := tc t.Run(tcName, func(t *testing.T) { t.Parallel() t.Logf("in=%q", tc.In) var out bytes.Buffer err := json.Compact(&out, []byte(tc.In)) assert.Equal(t, tc.Out, out.String()) if tc.Err == "" { assert.NoError(t, err) } else { assert.EqualError(t, err, tc.Err) } }) } } func TestCompatIndent(t *testing.T) { t.Parallel() type testcase struct { In string Out string Err string } testcases := map[string]testcase{ "empty": {In: ``, Out: ``, Err: `unexpected end of JSON input`}, "trunc": {In: `{`, Out: ``, Err: `unexpected end of JSON input`}, "object": {In: `{}`, Out: `{}`}, "non-utf8": {In: "\"\x85\xcd\"", Out: "\"\x85\xcd\""}, "float": {In: `1.200e003`, Out: `1.200e003`}, "tailws0": {In: `0`, Out: `0`}, "tailws1": {In: `0 `, Out: `0 `}, "tailws2": {In: `0 `, Out: `0 `}, "tailws3": {In: "0\n", Out: "0\n"}, "headws1": {In: ` 0`, Out: `0`}, "objws1": {In: `{"a" : 1}`, Out: "{\n>.\"a\": 1\n>}"}, "objws2": {In: "{\"a\"\n:\n1}", Out: "{\n>.\"a\": 1\n>}"}, "hex-lower": {In: `"\uabcd"`, Out: `"\uabcd"`}, "hex-upper": {In: `"\uABCD"`, Out: `"\uABCD"`}, "hex-mixed": {In: `"\uAbCd"`, Out: `"\uAbCd"`}, "invalid-utf8": {In: "\x85", Err: `invalid character '\x85' looking for beginning of value`}, } for tcName, tc := range testcases { tc := tc t.Run(tcName, func(t *testing.T) { t.Parallel() t.Logf("in=%q", tc.In) var out bytes.Buffer err := json.Indent(&out, []byte(tc.In), ">", ".") assert.Equal(t, tc.Out, out.String()) if tc.Err == "" { assert.NoError(t, err) } else { assert.EqualError(t, err, tc.Err) } }) } } func TestCompatMarshal(t *testing.T) { t.Parallel() type testcase struct { In any Out string Err string } testcases := map[string]testcase{ "non-utf8": {In: "\x85\xcd", Out: "\"\\ufffd\\ufffd\""}, "urc": {In: "\ufffd", Out: "\"\ufffd\""}, "float": {In: 1.2e3, Out: `1200`}, "obj": {In: map[string]any{"": 1, " ": 2}, Out: `{"":1," ":2}`}, "byte-ary": {In: struct{ Label [5]byte }{}, Out: `{"Label":[0,0,0,0,0]}`}, } for tcName, tc := range testcases { tc := tc t.Run(tcName, func(t *testing.T) { t.Parallel() out, err := json.Marshal(tc.In) assert.Equal(t, tc.Out, string(out)) if tc.Err == "" { assert.NoError(t, err) } else { assert.EqualError(t, err, tc.Err) } }) } } func TestCompatUnmarshal(t *testing.T) { t.Parallel() type testcase struct { In string InPtr any ExpOut any ExpErr string } testcases := map[string]testcase{ "empty-obj": {In: `{}`, ExpOut: map[string]any{}}, "partial-obj": {In: `{"foo":"bar",`, ExpOut: nil, ExpErr: `unexpected end of JSON input`}, "existing-obj": {In: `{"baz":"quz"}`, InPtr: &map[string]string{"foo": "bar"}, ExpOut: map[string]string{"foo": "bar", "baz": "quz"}}, "existing-obj-partial": {In: `{"baz":"quz"`, InPtr: &map[string]string{"foo": "bar"}, ExpOut: map[string]string{"foo": "bar"}, ExpErr: "unexpected end of JSON input"}, "empty-ary": {In: `[]`, ExpOut: []any{}}, "two-objs": {In: `{} {}`, ExpOut: nil, ExpErr: `invalid character '{' after top-level value`}, "two-numbers1": {In: `00`, ExpOut: nil, ExpErr: `invalid character '0' after top-level value`}, "two-numbers2": {In: `1 2`, ExpOut: nil, ExpErr: `invalid character '2' after top-level value`}, "invalid-utf8": {In: "\x85", ExpErr: `invalid character '\x85' looking for beginning of value`}, "byte-ary": {In: `{"Label":[1,0,0,0,0]}`, InPtr: new(struct{ Label [5]byte }), ExpOut: struct{ Label [5]byte }{Label: [5]byte{1, 0, 0, 0, 0}}}, // 2e308 is slightly more than math.MaxFloat64 (~1.79e308) "obj-overflow": {In: `{"foo":"bar", "baz":2e308, "qux": "orb"}`, ExpOut: map[string]any{"foo": "bar", "baz": nil, "qux": "orb"}, ExpErr: `json: cannot unmarshal number 2e308 into Go value of type float64`}, "ary-overflow": {In: `["foo",2e308,"bar",3e308]`, ExpOut: []any{"foo", nil, "bar", nil}, ExpErr: `json: cannot unmarshal number 2e308 into Go value of type float64`}, "existing-overflow": {In: `2e308`, InPtr: func() any { x := 4; return &x }(), ExpOut: 4, ExpErr: `json: cannot unmarshal number 2e308 into Go value of type int`}, // syntax error messages "syntax-01": {In: `{}x`, ExpErr: `invalid character 'x' after top-level value`}, "syntax-02": {In: `x`, ExpErr: `invalid character 'x' looking for beginning of value`}, "syntax-03": {In: `{x`, ExpErr: `invalid character 'x' looking for beginning of object key string`}, "syntax-18": {In: `{"":0,}`, ExpErr: `invalid character '}' looking for beginning of object key string`}, "syntax-04": {In: `{""x`, ExpErr: `invalid character 'x' after object key`}, "syntax-05": {In: `{"":0x`, ExpErr: `invalid character 'x' after object key:value pair`}, "syntax-06": {In: `[0x`, ExpErr: `invalid character 'x' after array element`}, "syntax-07": {In: "\"\x01\"", ExpErr: `invalid character '\x01' in string literal`}, "syntax-08": {In: `"\x`, ExpErr: `invalid character 'x' in string escape code`}, "syntax-09": {In: `"\ux`, ExpErr: `invalid character 'x' in \u hexadecimal character escape`}, "syntax-10": {In: `"\u0x`, ExpErr: `invalid character 'x' in \u hexadecimal character escape`}, "syntax-11": {In: `"\u00x`, ExpErr: `invalid character 'x' in \u hexadecimal character escape`}, "syntax-12": {In: `"\u000x`, ExpErr: `invalid character 'x' in \u hexadecimal character escape`}, "syntax-13": {In: `-x`, ExpErr: `invalid character 'x' in numeric literal`}, "syntax-14": {In: `0.x`, ExpErr: `invalid character 'x' after decimal point in numeric literal`}, "syntax-15": {In: `1ex`, ExpErr: `invalid character 'x' in exponent of numeric literal`}, "syntax-16": {In: `1e+x`, ExpErr: `invalid character 'x' in exponent of numeric literal`}, "syntax-17": {In: `fx`, ExpErr: `invalid character 'x' in literal false (expecting 'a')`}, } for tcName, tc := range testcases { tc := tc t.Run(tcName, func(t *testing.T) { t.Parallel() ptr := tc.InPtr if ptr == nil { var out any ptr = &out } err := json.Unmarshal([]byte(tc.In), ptr) assert.Equal(t, tc.ExpOut, reflect.ValueOf(ptr).Elem().Interface()) if tc.ExpErr == "" { assert.NoError(t, err) } else { assert.EqualError(t, err, tc.ExpErr) } }) } } func TestCompatDecode(t *testing.T) { t.Parallel() type testcase struct { In string InPtr any ExpOut any ExpErr string } testcases := map[string]testcase{ "empty-obj": {In: `{}`, ExpOut: map[string]any{}}, "partial-obj": {In: `{"foo":"bar",`, ExpOut: nil, ExpErr: `unexpected EOF`}, "existing-obj": {In: `{"baz":"quz"}`, InPtr: &map[string]string{"foo": "bar"}, ExpOut: map[string]string{"foo": "bar", "baz": "quz"}}, "existing-obj-partial": {In: `{"baz":"quz"`, InPtr: &map[string]string{"foo": "bar"}, ExpOut: map[string]string{"foo": "bar"}, ExpErr: "unexpected EOF"}, "empty-ary": {In: `[]`, ExpOut: []any{}}, "two-objs": {In: `{} {}`, ExpOut: map[string]any{}}, "two-numbers1": {In: `00`, ExpOut: float64(0)}, "two-numbers2": {In: `1 2`, ExpOut: float64(1)}, "invalid-utf8": {In: "\x85", ExpErr: `invalid character '\x85' looking for beginning of value`}, // 2e308 is slightly more than math.MaxFloat64 (~1.79e308) "obj-overflow": {In: `{"foo":"bar", "baz":2e308, "qux": "orb"}`, ExpOut: map[string]any{"foo": "bar", "baz": nil, "qux": "orb"}, ExpErr: `json: cannot unmarshal number 2e308 into Go value of type float64`}, "ary-overflow": {In: `["foo",2e308,"bar",3e308]`, ExpOut: []any{"foo", nil, "bar", nil}, ExpErr: `json: cannot unmarshal number 2e308 into Go value of type float64`}, "existing-overflow": {In: `2e308`, InPtr: func() any { x := 4; return &x }(), ExpOut: 4, ExpErr: `json: cannot unmarshal number 2e308 into Go value of type int`}, } for tcName, tc := range testcases { tc := tc t.Run(tcName, func(t *testing.T) { t.Parallel() ptr := tc.InPtr if ptr == nil { var out any ptr = &out } err := json.NewDecoder(strings.NewReader(tc.In)).Decode(ptr) assert.Equal(t, tc.ExpOut, reflect.ValueOf(ptr).Elem().Interface()) if tc.ExpErr == "" { assert.NoError(t, err) } else { assert.EqualError(t, err, tc.ExpErr) } }) } }