// Copyright 2019 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package json_test import ( "strings" "testing" "unicode/utf8" "google.golang.org/protobuf/internal/encoding/json" ) type R struct { // T is expected Type returned from calling Decoder.Read. T json.Type // E is expected error substring from calling Decoder.Read if set. E string // V is expected value from calling // Value.{Bool()|Float()|Int()|Uint()|String()} depending on type. V interface{} // VE is expected error substring from calling // Value.{Bool()|Float()|Int()|Uint()|String()} depending on type if set. VE string } func TestDecoder(t *testing.T) { const space = " \n\r\t" tests := []struct { input string // want is a list of expected values returned from calling // Decoder.Read. An item makes the test code invoke // Decoder.Read and compare against R.T and R.E. For Bool, // Number and String tokens, it invokes the corresponding getter method // and compares the returned value against R.V or R.VE if it returned an // error. want []R }{ { input: ``, want: []R{{T: json.EOF}}, }, { input: space, want: []R{{T: json.EOF}}, }, { // Calling Read after EOF will keep returning EOF for // succeeding Read calls. input: space, want: []R{ {T: json.EOF}, {T: json.EOF}, {T: json.EOF}, }, }, // JSON literals. { input: space + `null` + space, want: []R{ {T: json.Null}, {T: json.EOF}, }, }, { input: space + `true` + space, want: []R{ {T: json.Bool, V: true}, {T: json.EOF}, }, }, { input: space + `false` + space, want: []R{ {T: json.Bool, V: false}, {T: json.EOF}, }, }, { // Error returned will produce the same error again. input: space + `foo` + space, want: []R{ {E: `invalid value foo`}, {E: `invalid value foo`}, }, }, // JSON strings. { input: space + `""` + space, want: []R{ {T: json.String, V: ""}, {T: json.EOF}, }, }, { input: space + `"hello"` + space, want: []R{ {T: json.String, V: "hello"}, {T: json.EOF}, }, }, { input: `"hello`, want: []R{{E: `unexpected EOF`}}, }, { input: "\"\x00\"", want: []R{{E: `invalid character '\x00' in string`}}, }, { input: "\"\u0031\u0032\"", want: []R{ {T: json.String, V: "12"}, {T: json.EOF}, }, }, { // Invalid UTF-8 error is returned in ReadString instead of Read. input: "\"\xff\"", want: []R{{E: `syntax error (line 1:1): invalid UTF-8 in string`}}, }, { input: `"` + string(utf8.RuneError) + `"`, want: []R{ {T: json.String, V: string(utf8.RuneError)}, {T: json.EOF}, }, }, { input: `"\uFFFD"`, want: []R{ {T: json.String, V: string(utf8.RuneError)}, {T: json.EOF}, }, }, { input: `"\x"`, want: []R{{E: `invalid escape code "\\x" in string`}}, }, { input: `"\uXXXX"`, want: []R{{E: `invalid escape code "\\uXXXX" in string`}}, }, { input: `"\uDEAD"`, // unmatched surrogate pair want: []R{{E: `unexpected EOF`}}, }, { input: `"\uDEAD\uBEEF"`, // invalid surrogate half want: []R{{E: `invalid escape code "\\uBEEF" in string`}}, }, { input: `"\uD800\udead"`, // valid surrogate pair want: []R{ {T: json.String, V: `𐊭`}, {T: json.EOF}, }, }, { input: `"\u0000\"\\\/\b\f\n\r\t"`, want: []R{ {T: json.String, V: "\u0000\"\\/\b\f\n\r\t"}, {T: json.EOF}, }, }, // Invalid JSON numbers. { input: `-`, want: []R{{E: `invalid number -`}}, }, { input: `+0`, want: []R{{E: `invalid value +0`}}, }, { input: `-+`, want: []R{{E: `invalid number -+`}}, }, { input: `0.`, want: []R{{E: `invalid number 0.`}}, }, { input: `.1`, want: []R{{E: `invalid value .1`}}, }, { input: `1.0.1`, want: []R{{E: `invalid number 1.0.1`}}, }, { input: `1..1`, want: []R{{E: `invalid number 1..1`}}, }, { input: `-1-2`, want: []R{{E: `invalid number -1-2`}}, }, { input: `01`, want: []R{{E: `invalid number 01`}}, }, { input: `1e`, want: []R{{E: `invalid number 1e`}}, }, { input: `1e1.2`, want: []R{{E: `invalid number 1e1.2`}}, }, { input: `1Ee`, want: []R{{E: `invalid number 1Ee`}}, }, { input: `1.e1`, want: []R{{E: `invalid number 1.e1`}}, }, { input: `1.e+`, want: []R{{E: `invalid number 1.e+`}}, }, { input: `1e+-2`, want: []R{{E: `invalid number 1e+-2`}}, }, { input: `1e--2`, want: []R{{E: `invalid number 1e--2`}}, }, { input: `1.0true`, want: []R{{E: `invalid number 1.0true`}}, }, // JSON numbers as floating point. { input: space + `0.0` + space, want: []R{ {T: json.Number, V: float32(0)}, {T: json.EOF}, }, }, { input: space + `0` + space, want: []R{ {T: json.Number, V: float32(0)}, {T: json.EOF}, }, }, { input: space + `-0` + space, want: []R{ {T: json.Number, V: float32(0)}, {T: json.EOF}, }, }, { input: `-1.02`, want: []R{ {T: json.Number, V: float32(-1.02)}, {T: json.EOF}, }, }, { input: `1.020000`, want: []R{ {T: json.Number, V: float32(1.02)}, {T: json.EOF}, }, }, { input: `-1.0e0`, want: []R{ {T: json.Number, V: float32(-1)}, {T: json.EOF}, }, }, { input: `1.0e-000`, want: []R{ {T: json.Number, V: float32(1)}, {T: json.EOF}, }, }, { input: `1e+00`, want: []R{ {T: json.Number, V: float32(1)}, {T: json.EOF}, }, }, { input: `1.02e3`, want: []R{ {T: json.Number, V: float32(1.02e3)}, {T: json.EOF}, }, }, { input: `-1.02E03`, want: []R{ {T: json.Number, V: float32(-1.02e3)}, {T: json.EOF}, }, }, { input: `1.0200e+3`, want: []R{ {T: json.Number, V: float32(1.02e3)}, {T: json.EOF}, }, }, { input: `-1.0200E+03`, want: []R{ {T: json.Number, V: float32(-1.02e3)}, {T: json.EOF}, }, }, { input: `1.0200e-3`, want: []R{ {T: json.Number, V: float32(1.02e-3)}, {T: json.EOF}, }, }, { input: `-1.0200E-03`, want: []R{ {T: json.Number, V: float32(-1.02e-3)}, {T: json.EOF}, }, }, { // Exceeds max float32 limit, but should be ok for float64. input: `3.4e39`, want: []R{ {T: json.Number, V: float64(3.4e39)}, {T: json.EOF}, }, }, { // Exceeds max float32 limit. input: `3.4e39`, want: []R{ {T: json.Number, V: float32(0), VE: `value out of range`}, {T: json.EOF}, }, }, { // Less than negative max float32 limit. input: `-3.4e39`, want: []R{ {T: json.Number, V: float32(0), VE: `value out of range`}, {T: json.EOF}, }, }, { // Exceeds max float64 limit. input: `1.79e+309`, want: []R{ {T: json.Number, V: float64(0), VE: `value out of range`}, {T: json.EOF}, }, }, { // Less than negative max float64 limit. input: `-1.79e+309`, want: []R{ {T: json.Number, V: float64(0), VE: `value out of range`}, {T: json.EOF}, }, }, // JSON numbers as signed integers. { input: space + `0` + space, want: []R{ {T: json.Number, V: int32(0)}, {T: json.EOF}, }, }, { input: space + `-0` + space, want: []R{ {T: json.Number, V: int32(0)}, {T: json.EOF}, }, }, { // Fractional part equals 0 is ok. input: `1.00000`, want: []R{ {T: json.Number, V: int32(1)}, {T: json.EOF}, }, }, { // Fractional part not equals 0 returns error. input: `1.0000000001`, want: []R{ {T: json.Number, V: int32(0), VE: `cannot convert 1.0000000001 to integer`}, {T: json.EOF}, }, }, { input: `0e0`, want: []R{ {T: json.Number, V: int32(0)}, {T: json.EOF}, }, }, { input: `0.0E0`, want: []R{ {T: json.Number, V: int32(0)}, {T: json.EOF}, }, }, { input: `0.0E10`, want: []R{ {T: json.Number, V: int32(0)}, {T: json.EOF}, }, }, { input: `-1`, want: []R{ {T: json.Number, V: int32(-1)}, {T: json.EOF}, }, }, { input: `1.0e+0`, want: []R{ {T: json.Number, V: int32(1)}, {T: json.EOF}, }, }, { input: `-1E-0`, want: []R{ {T: json.Number, V: int32(-1)}, {T: json.EOF}, }, }, { input: `1E1`, want: []R{ {T: json.Number, V: int32(10)}, {T: json.EOF}, }, }, { input: `-100.00e-02`, want: []R{ {T: json.Number, V: int32(-1)}, {T: json.EOF}, }, }, { input: `0.1200E+02`, want: []R{ {T: json.Number, V: int64(12)}, {T: json.EOF}, }, }, { input: `0.012e2`, want: []R{ {T: json.Number, V: int32(0), VE: `cannot convert 0.012e2 to integer`}, {T: json.EOF}, }, }, { input: `12e-2`, want: []R{ {T: json.Number, V: int32(0), VE: `cannot convert 12e-2 to integer`}, {T: json.EOF}, }, }, { // Exceeds math.MaxInt32. input: `2147483648`, want: []R{ {T: json.Number, V: int32(0), VE: `value out of range`}, {T: json.EOF}, }, }, { // Exceeds math.MinInt32. input: `-2147483649`, want: []R{ {T: json.Number, V: int32(0), VE: `value out of range`}, {T: json.EOF}, }, }, { // Exceeds math.MaxInt32, but ok for int64. input: `2147483648`, want: []R{ {T: json.Number, V: int64(2147483648)}, {T: json.EOF}, }, }, { // Exceeds math.MinInt32, but ok for int64. input: `-2147483649`, want: []R{ {T: json.Number, V: int64(-2147483649)}, {T: json.EOF}, }, }, { // Exceeds math.MaxInt64. input: `9223372036854775808`, want: []R{ {T: json.Number, V: int64(0), VE: `value out of range`}, {T: json.EOF}, }, }, { // Exceeds math.MinInt64. input: `-9223372036854775809`, want: []R{ {T: json.Number, V: int64(0), VE: `value out of range`}, {T: json.EOF}, }, }, // JSON numbers as unsigned integers. { input: space + `0` + space, want: []R{ {T: json.Number, V: uint32(0)}, {T: json.EOF}, }, }, { input: space + `-0` + space, want: []R{ {T: json.Number, V: uint32(0)}, {T: json.EOF}, }, }, { input: `-1`, want: []R{ {T: json.Number, V: uint32(0), VE: `invalid syntax`}, {T: json.EOF}, }, }, { // Exceeds math.MaxUint32. input: `4294967296`, want: []R{ {T: json.Number, V: uint32(0), VE: `value out of range`}, {T: json.EOF}, }, }, { // Exceeds math.MaxUint64. input: `18446744073709551616`, want: []R{ {T: json.Number, V: uint64(0), VE: `value out of range`}, {T: json.EOF}, }, }, // JSON sequence of values. { input: `true null`, want: []R{ {T: json.Bool, V: true}, {E: `unexpected value null`}, }, }, { input: "null false", want: []R{ {T: json.Null}, {E: `unexpected value false`}, }, }, { input: `true,false`, want: []R{ {T: json.Bool, V: true}, {E: `unexpected character ,`}, }, }, { input: `47"hello"`, want: []R{ {T: json.Number, V: int32(47)}, {E: `unexpected value "hello"`}, }, }, { input: `47 "hello"`, want: []R{ {T: json.Number, V: int32(47)}, {E: `unexpected value "hello"`}, }, }, { input: `true 42`, want: []R{ {T: json.Bool, V: true}, {E: `unexpected value 42`}, }, }, // JSON arrays. { input: space + `[]` + space, want: []R{ {T: json.StartArray}, {T: json.EndArray}, {T: json.EOF}, }, }, { input: space + `[` + space + `]` + space, want: []R{ {T: json.StartArray}, {T: json.EndArray}, {T: json.EOF}, }, }, { input: space + `[` + space, want: []R{ {T: json.StartArray}, {E: `unexpected EOF`}, }, }, { input: space + `]` + space, want: []R{{E: `unexpected character ]`}}, }, { input: `[null,true,false, 1e1, "hello" ]`, want: []R{ {T: json.StartArray}, {T: json.Null}, {T: json.Bool, V: true}, {T: json.Bool, V: false}, {T: json.Number, V: int32(10)}, {T: json.String, V: "hello"}, {T: json.EndArray}, {T: json.EOF}, }, }, { input: `[` + space + `true` + space + `,` + space + `"hello"` + space + `]`, want: []R{ {T: json.StartArray}, {T: json.Bool, V: true}, {T: json.String, V: "hello"}, {T: json.EndArray}, {T: json.EOF}, }, }, { input: `[` + space + `true` + space + `,` + space + `]`, want: []R{ {T: json.StartArray}, {T: json.Bool, V: true}, {E: `unexpected character ]`}, }, }, { input: `[` + space + `false` + space + `]`, want: []R{ {T: json.StartArray}, {T: json.Bool, V: false}, {T: json.EndArray}, {T: json.EOF}, }, }, { input: `[` + space + `1` + space + `0` + space + `]`, want: []R{ {T: json.StartArray}, {T: json.Number, V: int64(1)}, {E: `unexpected value 0`}, }, }, { input: `[null`, want: []R{ {T: json.StartArray}, {T: json.Null}, {E: `unexpected EOF`}, }, }, { input: `[foo]`, want: []R{ {T: json.StartArray}, {E: `invalid value foo`}, }, }, { input: `[{}, "hello", [true, false], null]`, want: []R{ {T: json.StartArray}, {T: json.StartObject}, {T: json.EndObject}, {T: json.String, V: "hello"}, {T: json.StartArray}, {T: json.Bool, V: true}, {T: json.Bool, V: false}, {T: json.EndArray}, {T: json.Null}, {T: json.EndArray}, {T: json.EOF}, }, }, { input: `[{ ]`, want: []R{ {T: json.StartArray}, {T: json.StartObject}, {E: `unexpected character ]`}, }, }, { input: `[[ ]`, want: []R{ {T: json.StartArray}, {T: json.StartArray}, {T: json.EndArray}, {E: `unexpected EOF`}, }, }, { input: `[,]`, want: []R{ {T: json.StartArray}, {E: `unexpected character ,`}, }, }, { input: `[true "hello"]`, want: []R{ {T: json.StartArray}, {T: json.Bool, V: true}, {E: `unexpected value "hello"`}, }, }, { input: `[] null`, want: []R{ {T: json.StartArray}, {T: json.EndArray}, {E: `unexpected value null`}, }, }, { input: `true []`, want: []R{ {T: json.Bool, V: true}, {E: `unexpected character [`}, }, }, // JSON objects. { input: space + `{}` + space, want: []R{ {T: json.StartObject}, {T: json.EndObject}, {T: json.EOF}, }, }, { input: space + `{` + space + `}` + space, want: []R{ {T: json.StartObject}, {T: json.EndObject}, {T: json.EOF}, }, }, { input: space + `{` + space, want: []R{ {T: json.StartObject}, {E: `unexpected EOF`}, }, }, { input: space + `}` + space, want: []R{{E: `unexpected character }`}}, }, { input: `{` + space + `null` + space + `}`, want: []R{ {T: json.StartObject}, {E: `unexpected value null`}, }, }, { input: `{[]}`, want: []R{ {T: json.StartObject}, {E: `unexpected character [`}, }, }, { input: `{,}`, want: []R{ {T: json.StartObject}, {E: `unexpected character ,`}, }, }, { input: `{"345678"}`, want: []R{ {T: json.StartObject}, {E: `unexpected character }, missing ":" after object name`}, }, }, { input: `{` + space + `"hello"` + space + `:` + space + `"world"` + space + `}`, want: []R{ {T: json.StartObject}, {T: json.Name, V: "hello"}, {T: json.String, V: "world"}, {T: json.EndObject}, {T: json.EOF}, }, }, { input: `{"hello" "world"}`, want: []R{ {T: json.StartObject}, {E: `unexpected character ", missing ":" after object name`}, }, }, { input: `{"hello":`, want: []R{ {T: json.StartObject}, {T: json.Name, V: "hello"}, {E: `unexpected EOF`}, }, }, { input: `{"hello":"world"`, want: []R{ {T: json.StartObject}, {T: json.Name, V: "hello"}, {T: json.String, V: "world"}, {E: `unexpected EOF`}, }, }, { input: `{"hello":"world",`, want: []R{ {T: json.StartObject}, {T: json.Name, V: "hello"}, {T: json.String, V: "world"}, {E: `unexpected EOF`}, }, }, { input: `{"34":"89",}`, want: []R{ {T: json.StartObject}, {T: json.Name, V: "34"}, {T: json.String, V: "89"}, {E: `syntax error (line 1:12): unexpected character }`}, }, }, { input: `{ "number": 123e2, "bool" : false, "object": {"string": "world"}, "null" : null, "array" : [1.01, "hello", true], "string": "hello" }`, want: []R{ {T: json.StartObject}, {T: json.Name, V: "number"}, {T: json.Number, V: int32(12300)}, {T: json.Name, V: "bool"}, {T: json.Bool, V: false}, {T: json.Name, V: "object"}, {T: json.StartObject}, {T: json.Name, V: "string"}, {T: json.String, V: "world"}, {T: json.EndObject}, {T: json.Name, V: "null"}, {T: json.Null}, {T: json.Name, V: "array"}, {T: json.StartArray}, {T: json.Number, V: float32(1.01)}, {T: json.String, V: "hello"}, {T: json.Bool, V: true}, {T: json.EndArray}, {T: json.Name, V: "string"}, {T: json.String, V: "hello"}, {T: json.EndObject}, {T: json.EOF}, }, }, { input: `[ {"object": {"number": 47}}, ["list"], null ]`, want: []R{ {T: json.StartArray}, {T: json.StartObject}, {T: json.Name, V: "object"}, {T: json.StartObject}, {T: json.Name, V: "number"}, {T: json.Number, V: uint32(47)}, {T: json.EndObject}, {T: json.EndObject}, {T: json.StartArray}, {T: json.String, V: "list"}, {T: json.EndArray}, {T: json.Null}, {T: json.EndArray}, {T: json.EOF}, }, }, // Tests for line and column info. { input: `12345678 x`, want: []R{ {T: json.Number, V: int64(12345678)}, {E: `syntax error (line 1:10): invalid value x`}, }, }, { input: "\ntrue\n x", want: []R{ {T: json.Bool, V: true}, {E: `syntax error (line 3:4): invalid value x`}, }, }, { input: `"💩"x`, want: []R{ {T: json.String, V: "💩"}, {E: `syntax error (line 1:4): invalid value x`}, }, }, { input: "\n\n[\"🔥🔥🔥\"x", want: []R{ {T: json.StartArray}, {T: json.String, V: "🔥🔥🔥"}, {E: `syntax error (line 3:7): invalid value x`}, }, }, { // Multi-rune emojis. input: `["👍🏻👍🏿"x`, want: []R{ {T: json.StartArray}, {T: json.String, V: "👍🏻👍🏿"}, {E: `syntax error (line 1:8): invalid value x`}, }, }, { input: `{ "45678":-1 }`, want: []R{ {T: json.StartObject}, {T: json.Name, V: "45678"}, {T: json.Number, V: uint64(1), VE: "error (line 2:11)"}, }, }, } for _, tc := range tests { tc := tc t.Run("", func(t *testing.T) { dec := json.NewDecoder([]byte(tc.input)) for i, want := range tc.want { typ := dec.Peek() if typ != want.T { t.Errorf("input: %v\nPeek() got %v want %v", tc.input, typ, want.T) } value, err := dec.Read() if err != nil { if want.E == "" { t.Errorf("input: %v\nRead() got unexpected error: %v", tc.input, err) } else if !strings.Contains(err.Error(), want.E) { t.Errorf("input: %v\nRead() got %q, want %q", tc.input, err, want.E) } } else { if want.E != "" { t.Errorf("input: %v\nRead() got nil error, want %q", tc.input, want.E) } } token := value.Type() if token != want.T { t.Errorf("input: %v\nRead() got %v, want %v", tc.input, token, want.T) break } checkValue(t, value, i, want) } }) } } func checkValue(t *testing.T, value json.Value, wantIdx int, want R) { var got interface{} var err error switch value.Type() { case json.Bool: got, err = value.Bool() case json.String: got = value.String() case json.Name: got, err = value.Name() case json.Number: switch want.V.(type) { case float32: got, err = value.Float(32) got = float32(got.(float64)) case float64: got, err = value.Float(64) case int32: got, err = value.Int(32) got = int32(got.(int64)) case int64: got, err = value.Int(64) case uint32: got, err = value.Uint(32) got = uint32(got.(uint64)) case uint64: got, err = value.Uint(64) } default: return } if err != nil { if want.VE == "" { t.Errorf("want%d: %v got unexpected error: %v", wantIdx, value, err) } else if !strings.Contains(err.Error(), want.VE) { t.Errorf("want#%d: %v got %q, want %q", wantIdx, value, err, want.VE) } return } else { if want.VE != "" { t.Errorf("want#%d: %v got nil error, want %q", wantIdx, value, want.VE) return } } if got != want.V { t.Errorf("want#%d: %v got %v, want %v", wantIdx, value, got, want.V) } } func TestClone(t *testing.T) { input := `{"outer":{"str":"hello", "number": 123}}` dec := json.NewDecoder([]byte(input)) // Clone at the start should produce the same reads as the original. clone := dec.Clone() compareDecoders(t, dec, clone) // Advance to inner object, clone and compare again. dec.Read() // Read StartObject. dec.Read() // Read Name. clone = dec.Clone() compareDecoders(t, dec, clone) } func compareDecoders(t *testing.T, d1 *json.Decoder, d2 *json.Decoder) { for { v1, err1 := d1.Read() v2, err2 := d2.Read() if v1.Type() != v2.Type() { t.Errorf("cloned decoder: got Type %v, want %v", v2.Type(), v1.Type()) } if v1.Raw() != v2.Raw() { t.Errorf("cloned decoder: got Raw %v, want %v", v2.Raw(), v1.Raw()) } if err1 != err2 { t.Errorf("cloned decoder: got error %v, want %v", err2, err1) } if v1.Type() == json.EOF { break } } }