diff --git a/arshal_any.go b/arshal_any.go index 317d572..9b8d85c 100644 --- a/arshal_any.go +++ b/arshal_any.go @@ -177,7 +177,7 @@ func unmarshalObjectAny(dec *jsontext.Decoder, uo *jsonopts.Struct) (map[string] // Manually check for duplicate names. if _, ok := obj[name]; ok { name := xd.PreviousBuffer() - err := export.NewDuplicateNameError(name, dec.InputOffset()-len64(name)) + err := newDuplicateNameError(dec.StackPointer(), nil, dec.InputOffset()-len64(name)) return obj, err } diff --git a/arshal_default.go b/arshal_default.go index dadc87a..582703d 100644 --- a/arshal_default.go +++ b/arshal_default.go @@ -890,7 +890,7 @@ func makeMapArshaler(t reflect.Type) *arshaler { if !xd.Flags.Get(jsonflags.AllowDuplicateNames) && (!seen.IsValid() || seen.MapIndex(k.Value).IsValid()) { // TODO: Unread the object name. name := xd.PreviousBuffer() - err := export.NewDuplicateNameError(name, dec.InputOffset()-len64(name)) + err := newDuplicateNameError(dec.StackPointer(), nil, dec.InputOffset()-len64(name)) return err } v.Set(v2) @@ -1160,7 +1160,7 @@ func makeStructArshaler(t reflect.Type) *arshaler { } if !xd.Flags.Get(jsonflags.AllowDuplicateNames) && !xd.Namespaces.Last().InsertUnquoted(name) { // TODO: Unread the object name. - err := export.NewDuplicateNameError(val, dec.InputOffset()-len64(val)) + err := newDuplicateNameError(dec.StackPointer(), nil, dec.InputOffset()-len64(val)) return err } @@ -1180,7 +1180,7 @@ func makeStructArshaler(t reflect.Type) *arshaler { } if !xd.Flags.Get(jsonflags.AllowDuplicateNames) && !seenIdxs.insert(uint(f.id)) { // TODO: Unread the object name. - err := export.NewDuplicateNameError(val, dec.InputOffset()-len64(val)) + err := newDuplicateNameError(dec.StackPointer(), nil, dec.InputOffset()-len64(val)) return err } diff --git a/arshal_inlined.go b/arshal_inlined.go index f78690f..3ba4131 100644 --- a/arshal_inlined.go +++ b/arshal_inlined.go @@ -78,7 +78,7 @@ func marshalInlinedFallbackAll(enc *jsontext.Encoder, va addressableValue, mo *j if insertUnquotedName != nil { name := jsonwire.UnquoteMayCopy(val, flags.IsVerbatim()) if !insertUnquotedName(name) { - return export.NewDuplicateNameError(val, 0) + return newDuplicateNameError(enc.StackPointer().Parent(), val, enc.OutputOffset()) } } if err := enc.WriteValue(val); err != nil { @@ -119,7 +119,7 @@ func marshalInlinedFallbackAll(enc *jsontext.Encoder, va addressableValue, mo *j isVerbatim := bytes.IndexByte(b, '\\') < 0 name := jsonwire.UnquoteMayCopy(b, isVerbatim) if !insertUnquotedName(name) { - return export.NewDuplicateNameError(b, 0) + return newDuplicateNameError(enc.StackPointer().Parent(), b, enc.OutputOffset()) } } return enc.WriteValue(b) diff --git a/arshal_test.go b/arshal_test.go index f4d1c1b..ff87327 100644 --- a/arshal_test.go +++ b/arshal_test.go @@ -25,9 +25,22 @@ import ( "github.com/go-json-experiment/json/internal/jsonflags" "github.com/go-json-experiment/json/internal/jsonopts" "github.com/go-json-experiment/json/internal/jsontest" + "github.com/go-json-experiment/json/internal/jsonwire" "github.com/go-json-experiment/json/jsontext" ) +func newNonStringNameError(offset int64, pointer jsontext.Pointer) error { + return &jsontext.SyntacticError{ByteOffset: offset, JSONPointer: pointer, Err: jsontext.ErrNonStringName} +} + +func newInvalidCharacterError(prefix, where string, offset int64, pointer jsontext.Pointer) error { + return &jsontext.SyntacticError{ByteOffset: offset, JSONPointer: pointer, Err: jsonwire.NewInvalidCharacterError(prefix, where)} +} + +func newInvalidUTF8Error(offset int64, pointer jsontext.Pointer) error { + return &jsontext.SyntacticError{ByteOffset: offset, JSONPointer: pointer, Err: jsonwire.ErrInvalidUTF8} +} + type ( jsonObject = map[string]any jsonArray = []any @@ -837,17 +850,17 @@ func TestMarshal(t *testing.T) { name: jsontest.Name("Maps/InvalidKey/Bool"), in: map[bool]string{false: "value"}, want: `{`, - wantErr: export.NewMissingNameError(len64(`{`)), + wantErr: newNonStringNameError(len64(`{`), ""), }, { name: jsontest.Name("Maps/InvalidKey/NamedBool"), in: map[namedBool]string{false: "value"}, want: `{`, - wantErr: export.NewMissingNameError(len64(`{`)), + wantErr: newNonStringNameError(len64(`{`), ""), }, { name: jsontest.Name("Maps/InvalidKey/Array"), in: map[[1]string]string{{"key"}: "value"}, want: `{`, - wantErr: export.NewMissingNameError(len64(`{`)), + wantErr: newNonStringNameError(len64(`{`), ""), }, { name: jsontest.Name("Maps/InvalidKey/Channel"), in: map[chan string]string{make(chan string): "value"}, @@ -868,7 +881,7 @@ func TestMarshal(t *testing.T) { in: map[*int64]string{addr(int64(0)): "0", addr(int64(0)): "0"}, canonicalize: true, want: `{"0":"0"`, - wantErr: export.NewDuplicateNameError([]byte(`"0"`), len64(`{"0":"0",`)), + wantErr: newDuplicateNameError("", []byte(`"0"`), len64(`{"0":"0",`)), }, { name: jsontest.Name("Maps/ValidKey/NamedInt"), in: map[namedInt64]string{math.MinInt64: "MinInt64", 0: "Zero", math.MaxInt64: "MaxInt64"}, @@ -913,7 +926,7 @@ func TestMarshal(t *testing.T) { opts: []Options{jsontext.AllowInvalidUTF8(true)}, in: map[string]string{"\x80": "", "\x81": ""}, want: `{"�":""`, - wantErr: export.NewDuplicateNameError([]byte(`"�"`), len64(`{"�":"",`)), + wantErr: newDuplicateNameError("", []byte(`"�"`), len64(`{"�":"",`)), }, { name: jsontest.Name("Maps/DuplicateName/NoCaseString/AllowDuplicateNames"), opts: []Options{jsontext.AllowDuplicateNames(true)}, @@ -923,7 +936,7 @@ func TestMarshal(t *testing.T) { name: jsontest.Name("Maps/DuplicateName/NoCaseString"), in: map[nocaseString]string{"hello": "", "HELLO": ""}, want: `{"hello":""`, - wantErr: &SemanticError{action: "marshal", JSONKind: '"', GoType: reflect.TypeFor[nocaseString](), Err: export.NewDuplicateNameError([]byte(`"hello"`), len64(`{"hello":"",`))}, + wantErr: &SemanticError{action: "marshal", JSONKind: '"', GoType: reflect.TypeFor[nocaseString](), Err: newDuplicateNameError("", []byte(`"hello"`), len64(`{"hello":"",`))}, }, { name: jsontest.Name("Maps/DuplicateName/NaNs/Deterministic+AllowDuplicateNames"), opts: []Options{ @@ -956,7 +969,7 @@ func TestMarshal(t *testing.T) { }, in: map[string]int{"\xff": 0, "\xfe": 1}, want: `{"�":1`, - wantErr: export.NewDuplicateNameError([]byte(`"�"`), len64(`{"�":1,`)), + wantErr: newDuplicateNameError("", []byte(`"�"`), len64(`{"�":1,`)), }, { name: jsontest.Name("Maps/String/Deterministic+AllowInvalidUTF8+AllowDuplicateNames"), opts: []Options{ @@ -1005,7 +1018,7 @@ func TestMarshal(t *testing.T) { }, in: map[namedString]map[string]int{"X": {"a": 1, "b": 1}}, want: `{"X":{"x":1`, - wantErr: export.NewDuplicateNameError([]byte(`"x"`), len64(`{"X":{"x":1,`)), + wantErr: newDuplicateNameError("/X/x", nil, len64(`{"X":{"x":1,`)), }, { name: jsontest.Name("Maps/String/Deterministic+MarshalFuncs+AllowDuplicateNames"), opts: []Options{ @@ -2451,7 +2464,7 @@ func TestMarshal(t *testing.T) { opts: []Options{jsontext.AllowDuplicateNames(false)}, in: structInlineTextValue{X: jsontext.Value(` { "fizz" : "buzz" , "fizz" : "buzz" } `)}, want: `{"fizz":"buzz"`, - wantErr: export.NewDuplicateNameError([]byte(`"fizz"`), 0), + wantErr: newDuplicateNameError("/fizz", nil, len64(`{"fizz":"buzz"`)), }, { name: jsontest.Name("Structs/InlinedFallback/TextValue/AllowDuplicateNames"), opts: []Options{jsontext.AllowDuplicateNames(true)}, @@ -2462,7 +2475,7 @@ func TestMarshal(t *testing.T) { opts: []Options{jsontext.AllowInvalidUTF8(false)}, in: structInlineTextValue{X: jsontext.Value(`{"` + "\xde\xad\xbe\xef" + `":"value"}`)}, want: `{`, - wantErr: export.NewInvalidUTF8Error(len64(`{"` + "\xde\xad")), + wantErr: newInvalidUTF8Error(len64(`{"`+"\xde\xad"), ""), }, { name: jsontest.Name("Structs/InlinedFallback/TextValue/AllowInvalidUTF8"), opts: []Options{jsontext.AllowInvalidUTF8(true)}, @@ -2482,17 +2495,17 @@ func TestMarshal(t *testing.T) { name: jsontest.Name("Structs/InlinedFallback/TextValue/InvalidObjectName"), in: structInlineTextValue{X: jsontext.Value(` { true : false } `)}, want: `{`, - wantErr: &SemanticError{action: "marshal", GoType: jsontextValueType, Err: export.NewMissingNameError(len64(" { "))}, + wantErr: &SemanticError{action: "marshal", GoType: jsontextValueType, Err: newNonStringNameError(len64(" { "), "")}, }, { name: jsontest.Name("Structs/InlinedFallback/TextValue/InvalidObjectEnd"), in: structInlineTextValue{X: jsontext.Value(` { "name" : false , } `)}, want: `{"name":false`, - wantErr: &SemanticError{action: "marshal", GoType: jsontextValueType, Err: export.NewInvalidCharacterError(",", "before next token", len64(` { "name" : false `))}, + wantErr: &SemanticError{action: "marshal", GoType: jsontextValueType, Err: newInvalidCharacterError(",", "before next token", len64(` { "name" : false `), "")}, }, { name: jsontest.Name("Structs/InlinedFallback/TextValue/InvalidDualObject"), in: structInlineTextValue{X: jsontext.Value(`{}{}`)}, want: `{`, - wantErr: &SemanticError{action: "marshal", GoType: jsontextValueType, Err: export.NewInvalidCharacterError("{", "after top-level value", len64(`{}`))}, + wantErr: &SemanticError{action: "marshal", GoType: jsontextValueType, Err: newInvalidCharacterError("{", "after top-level value", len64(`{}`), "")}, }, { name: jsontest.Name("Structs/InlinedFallback/TextValue/Nested/Nil"), in: structInlinePointerInlineTextValue{}, @@ -2540,7 +2553,7 @@ func TestMarshal(t *testing.T) { opts: []Options{jsontext.AllowInvalidUTF8(false)}, in: structInlineMapStringAny{X: jsonObject{"\xde\xad\xbe\xef": nil}}, want: `{`, - wantErr: export.NewInvalidUTF8Error(0), + wantErr: jsonwire.ErrInvalidUTF8, }, { name: jsontest.Name("Structs/InlinedFallback/MapStringAny/AllowInvalidUTF8"), opts: []Options{jsontext.AllowInvalidUTF8(true)}, @@ -2598,7 +2611,7 @@ func TestMarshal(t *testing.T) { X: map[string]int{"\xff": 0, "\xfe": 1}, }, want: `{"�":1`, - wantErr: export.NewDuplicateNameError([]byte(`"�"`), 0), + wantErr: newDuplicateNameError("", []byte(`"�"`), len64(`{"�":1`)), }, { name: jsontest.Name("Structs/InlinedFallback/MapStringInt/Deterministic+AllowInvalidUTF8+AllowDuplicateNames"), opts: []Options{Deterministic(true), jsontext.AllowInvalidUTF8(true), jsontext.AllowDuplicateNames(true)}, @@ -2665,7 +2678,7 @@ func TestMarshal(t *testing.T) { X: jsontext.Value(`{"dupe":"","dupe":""}`), }, want: `{"dupe":""`, - wantErr: export.NewDuplicateNameError([]byte(`"dupe"`), 0), + wantErr: newDuplicateNameError("", []byte(`"dupe"`), len64(`{"dupe":""`)), }, { name: jsontest.Name("Structs/DuplicateName/NoCaseInlineTextValue/Other/AllowDuplicateNames"), opts: []Options{jsontext.AllowDuplicateNames(true)}, @@ -2685,7 +2698,7 @@ func TestMarshal(t *testing.T) { X: jsontext.Value(`{"Aaa": "", "Aaa": ""}`), }, want: `{"Aaa":""`, - wantErr: export.NewDuplicateNameError([]byte(`"Aaa"`), 0), + wantErr: newDuplicateNameError("", []byte(`"Aaa"`), len64(`{"Aaa":""`)), }, { name: jsontest.Name("Structs/DuplicateName/NoCaseInlineTextValue/ExactConflict/AllowDuplicateNames"), opts: []Options{jsontext.AllowDuplicateNames(true)}, @@ -2699,7 +2712,7 @@ func TestMarshal(t *testing.T) { X: jsontext.Value(`{"Aaa": "", "AaA": "", "aaa": ""}`), }, want: `{"Aaa":"","AaA":""`, - wantErr: export.NewDuplicateNameError([]byte(`"aaa"`), 0), + wantErr: newDuplicateNameError("", []byte(`"aaa"`), len64(`{"Aaa":"","AaA":""`)), }, { name: jsontest.Name("Structs/DuplicateName/NoCaseInlineTextValue/NoCaseConflict/AllowDuplicateNames"), opts: []Options{jsontext.AllowDuplicateNames(true)}, @@ -2723,7 +2736,7 @@ func TestMarshal(t *testing.T) { X: jsontext.Value(`{"AAA": ""}`), }, want: `{"AAA":"x","AaA":"x"`, - wantErr: export.NewDuplicateNameError([]byte(`"AAA"`), 0), + wantErr: newDuplicateNameError("", []byte(`"AAA"`), len64(`{"AAA":"x","AaA":"x"`)), }, { name: jsontest.Name("Structs/DuplicateName/NoCaseInlineTextValue/NoCaseConflictWithField"), in: structNoCaseInlineTextValue{ @@ -2732,7 +2745,7 @@ func TestMarshal(t *testing.T) { X: jsontext.Value(`{"aaa": ""}`), }, want: `{"AAA":"x","AaA":"x"`, - wantErr: export.NewDuplicateNameError([]byte(`"aaa"`), 0), + wantErr: newDuplicateNameError("", []byte(`"aaa"`), len64(`{"AAA":"x","AaA":"x"`)), }, { name: jsontest.Name("Structs/DuplicateName/MatchCaseInsensitiveDelimiter"), in: structNoCaseInlineTextValue{ @@ -2740,7 +2753,7 @@ func TestMarshal(t *testing.T) { X: jsontext.Value(`{"aa_a": ""}`), }, want: `{"AaA":"x"`, - wantErr: export.NewDuplicateNameError([]byte(`"aa_a"`), 0), + wantErr: newDuplicateNameError("", []byte(`"aa_a"`), len64(`{"AaA":"x"`)), }, { name: jsontest.Name("Structs/DuplicateName/MatchCaseSensitiveDelimiter"), opts: []Options{jsonflags.MatchCaseSensitiveDelimiter | 1}, @@ -2765,7 +2778,7 @@ func TestMarshal(t *testing.T) { X: jsontext.Value(`{"aa_b": ""}`), }, want: `{"AA_b":"x"`, - wantErr: export.NewDuplicateNameError([]byte(`"aa_b"`), 0), + wantErr: newDuplicateNameError("", []byte(`"aa_b"`), len64(`{"AA_b":"x"`)), }, { name: jsontest.Name("Structs/DuplicateName/NoCaseInlineMapStringAny/ExactDifferent"), in: structNoCaseInlineMapStringAny{ @@ -2789,7 +2802,7 @@ func TestMarshal(t *testing.T) { X: jsonObject{"AAA": ""}, }, want: `{"AAA":"x","AaA":"x"`, - wantErr: export.NewDuplicateNameError([]byte(`"AAA"`), 0), + wantErr: newDuplicateNameError("", []byte(`"AAA"`), len64(`{"AAA":"x","AaA":"x"`)), }, { name: jsontest.Name("Structs/DuplicateName/NoCaseInlineMapStringAny/NoCaseConflictWithField"), in: structNoCaseInlineMapStringAny{ @@ -2798,7 +2811,7 @@ func TestMarshal(t *testing.T) { X: jsonObject{"aaa": ""}, }, want: `{"AAA":"x","AaA":"x"`, - wantErr: export.NewDuplicateNameError([]byte(`"aaa"`), 0), + wantErr: newDuplicateNameError("", []byte(`"aaa"`), len64(`{"AAA":"x","AaA":"x"`)), }, { name: jsontest.Name("Structs/Invalid/Conflicting"), in: structConflicting{}, @@ -3083,7 +3096,7 @@ func TestMarshal(t *testing.T) { opts: []Options{Deterministic(true), jsontext.AllowInvalidUTF8(true), jsontext.AllowDuplicateNames(false)}, in: struct{ X any }{map[string]any{"\xff": "", "\xfe": ""}}, want: `{"X":{"�":""`, - wantErr: export.NewDuplicateNameError([]byte(`"�"`), len64(`{"X":{"�":"",`)), + wantErr: newDuplicateNameError("/X", []byte(`"�"`), len64(`{"X":{"�":"",`)), }, { name: jsontest.Name("Interfaces/Any/Maps/Deterministic+AllowInvalidUTF8+AllowDuplicateNames"), opts: []Options{Deterministic(true), jsontext.AllowInvalidUTF8(true), jsontext.AllowDuplicateNames(true)}, @@ -3093,13 +3106,13 @@ func TestMarshal(t *testing.T) { name: jsontest.Name("Interfaces/Any/Maps/RejectInvalidUTF8"), in: struct{ X any }{map[string]any{"\xff": "", "\xfe": ""}}, want: `{"X":{`, - wantErr: export.NewInvalidUTF8Error(len64(`{"X":{`)), + wantErr: newInvalidUTF8Error(len64(`{"X":{`), "/X"), }, { name: jsontest.Name("Interfaces/Any/Maps/AllowInvalidUTF8+RejectDuplicateNames"), opts: []Options{jsontext.AllowInvalidUTF8(true)}, in: struct{ X any }{map[string]any{"\xff": "", "\xfe": ""}}, want: `{"X":{"�":""`, - wantErr: export.NewDuplicateNameError([]byte(`"�"`), len64(`{"X":{"�":"",`)), + wantErr: newDuplicateNameError("/X", []byte(`"�"`), len64(`{"X":{"�":"",`)), }, { name: jsontest.Name("Interfaces/Any/Maps/AllowInvalidUTF8+AllowDuplicateNames"), opts: []Options{jsontext.AllowInvalidUTF8(true), jsontext.AllowDuplicateNames(true)}, @@ -3271,7 +3284,7 @@ func TestMarshal(t *testing.T) { in: marshalJSONv1Func(func() ([]byte, error) { return []byte("invalid"), nil }), - wantErr: &SemanticError{action: "marshal", JSONKind: 'i', GoType: marshalJSONv1FuncType, Err: export.NewInvalidCharacterError("i", "at start of value", 0)}, + wantErr: &SemanticError{action: "marshal", JSONKind: 'i', GoType: marshalJSONv1FuncType, Err: newInvalidCharacterError("i", "at start of value", 0, "")}, }, { name: jsontest.Name("Methods/Invalid/JSONv1/SkipFunc"), in: marshalJSONv1Func(func() ([]byte, error) { @@ -3293,7 +3306,7 @@ func TestMarshal(t *testing.T) { }, { name: jsontest.Name("Methods/AppendText/RejectInvalidUTF8"), in: appendTextFunc(func(b []byte) ([]byte, error) { return append(b, "\xde\xad\xbe\xef"...), nil }), - wantErr: &SemanticError{action: "marshal", JSONKind: '"', GoType: appendTextFuncType, Err: export.NewInvalidUTF8Error(0)}, + wantErr: &SemanticError{action: "marshal", JSONKind: '"', GoType: appendTextFuncType, Err: newInvalidUTF8Error(0, "")}, }, { name: jsontest.Name("Methods/AppendText/AllowInvalidUTF8"), opts: []Options{jsontext.AllowInvalidUTF8(true)}, @@ -3310,7 +3323,7 @@ func TestMarshal(t *testing.T) { in: marshalTextFunc(func() ([]byte, error) { return []byte("\xde\xad\xbe\xef"), nil }), - wantErr: &SemanticError{action: "marshal", JSONKind: '"', GoType: marshalTextFuncType, Err: export.NewInvalidUTF8Error(0)}, + wantErr: &SemanticError{action: "marshal", JSONKind: '"', GoType: marshalTextFuncType, Err: newInvalidUTF8Error(0, "")}, }, { name: jsontest.Name("Methods/Text/AllowInvalidUTF8"), opts: []Options{jsontext.AllowInvalidUTF8(true)}, @@ -3332,7 +3345,7 @@ func TestMarshal(t *testing.T) { })): "invalid", }, want: `{`, - wantErr: &SemanticError{action: "marshal", GoType: marshalJSONv2FuncType, Err: export.NewMissingNameError(len64(`{`))}, + wantErr: &SemanticError{action: "marshal", GoType: marshalJSONv2FuncType, Err: newNonStringNameError(len64(`{`), "")}, }, { name: jsontest.Name("Methods/Invalid/MapKey/JSONv1/Syntax"), in: map[any]string{ @@ -3341,7 +3354,7 @@ func TestMarshal(t *testing.T) { })): "invalid", }, want: `{`, - wantErr: &SemanticError{action: "marshal", JSONKind: 'n', GoType: marshalJSONv1FuncType, Err: export.NewMissingNameError(len64(`{`))}, + wantErr: &SemanticError{action: "marshal", JSONKind: 'n', GoType: marshalJSONv1FuncType, Err: newNonStringNameError(len64(`{`), "")}, }, { name: jsontest.Name("Functions/Bool/V1"), opts: []Options{ @@ -3461,7 +3474,7 @@ func TestMarshal(t *testing.T) { })), }, in: true, - wantErr: &SemanticError{action: "marshal", JSONKind: 'i', GoType: boolType, Err: export.NewInvalidCharacterError("i", "at start of value", 0)}, + wantErr: &SemanticError{action: "marshal", JSONKind: 'i', GoType: boolType, Err: newInvalidCharacterError("i", "at start of value", 0, "")}, }, { name: jsontest.Name("Functions/Bool/V2/DirectError"), opts: []Options{ @@ -3559,7 +3572,7 @@ func TestMarshal(t *testing.T) { }, in: map[nocaseString]string{"hello": "world"}, want: `{`, - wantErr: &SemanticError{action: "marshal", JSONKind: 'n', GoType: nocaseStringType, Err: export.NewMissingNameError(len64(`{`))}, + wantErr: &SemanticError{action: "marshal", JSONKind: 'n', GoType: nocaseStringType, Err: newNonStringNameError(len64(`{`), "")}, }, { name: jsontest.Name("Functions/Map/Key/NoCaseString/V2/InvalidKind"), opts: []Options{ @@ -3569,7 +3582,7 @@ func TestMarshal(t *testing.T) { }, in: map[nocaseString]string{"hello": "world"}, want: `{`, - wantErr: &SemanticError{action: "marshal", JSONKind: 'n', GoType: nocaseStringType, Err: export.NewMissingNameError(len64(`{`))}, + wantErr: &SemanticError{action: "marshal", JSONKind: 'n', GoType: nocaseStringType, Err: newNonStringNameError(len64(`{`), "")}, }, { name: jsontest.Name("Functions/Map/Key/String/V1/DuplicateName"), opts: []Options{ @@ -3579,7 +3592,7 @@ func TestMarshal(t *testing.T) { }, in: map[string]string{"name1": "value", "name2": "value"}, want: `{"name":"name"`, - wantErr: &SemanticError{action: "marshal", JSONKind: '"', GoType: stringType, Err: export.NewDuplicateNameError([]byte(`"name"`), len64(`{"name":"name",`))}, + wantErr: &SemanticError{action: "marshal", JSONKind: '"', GoType: stringType, Err: newDuplicateNameError("", []byte(`"name"`), len64(`{"name":"name",`))}, }, { name: jsontest.Name("Functions/Map/Key/NoCaseString/V2"), opts: []Options{ @@ -3618,7 +3631,7 @@ func TestMarshal(t *testing.T) { }, in: map[nocaseString]string{"hello": "world"}, want: `{`, - wantErr: &SemanticError{action: "marshal", GoType: nocaseStringType, Err: export.NewMissingNameError(len64(`{`))}, + wantErr: &SemanticError{action: "marshal", GoType: nocaseStringType, Err: newNonStringNameError(len64(`{`), "")}, }, { name: jsontest.Name("Functions/Map/Key/NoCaseString/V2/InvalidValue"), opts: []Options{ @@ -3628,7 +3641,7 @@ func TestMarshal(t *testing.T) { }, in: map[nocaseString]string{"hello": "world"}, want: `{`, - wantErr: &SemanticError{action: "marshal", GoType: nocaseStringType, Err: export.NewMissingNameError(len64(`{`))}, + wantErr: &SemanticError{action: "marshal", GoType: nocaseStringType, Err: newNonStringNameError(len64(`{`), "")}, }, { name: jsontest.Name("Functions/Map/Value/NoCaseString/V1"), opts: []Options{ @@ -4423,7 +4436,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `falsetrue`, inVal: addr(true), want: addr(false), - wantErr: export.NewInvalidCharacterError("t", "after top-level value", len64(`false`)), + wantErr: newInvalidCharacterError("t", "after top-level value", len64(`false`), ""), }, { name: jsontest.Name("Bools/Null"), inBuf: `null`, @@ -4571,14 +4584,14 @@ func TestUnmarshal(t *testing.T) { inBuf: `"\"foo\" "`, inVal: new(string), want: new(string), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: stringType, Err: export.NewInvalidCharacterError(" ", "after string value", 0)}, + wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: stringType, Err: newInvalidCharacterError(" ", "after string value", 0, "")}, }, { name: jsontest.Name("Strings/StringifiedString/InvalidString"), opts: []Options{jsonflags.StringifyBoolsAndStrings | 1}, inBuf: `""`, inVal: new(string), want: new(string), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: stringType, Err: io.ErrUnexpectedEOF}, + wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: stringType, Err: &jsontext.SyntacticError{Err: io.ErrUnexpectedEOF}}, }, { name: jsontest.Name("Bytes/Null"), inBuf: `null`, @@ -5432,7 +5445,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"0":1,"-0":-1}`, inVal: new(map[int]int), want: addr(map[int]int{0: 1}), - wantErr: export.NewDuplicateNameError([]byte(`"-0"`), len64(`{"0":1,`)), + wantErr: newDuplicateNameError("", []byte(`"-0"`), len64(`{"0":1,`)), }, { name: jsontest.Name("Maps/DuplicateName/Int/AllowDuplicateNames"), opts: []Options{jsontext.AllowDuplicateNames(true)}, @@ -5449,7 +5462,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"1.0":"1.0","1":"1","1e0":"1e0"}`, inVal: new(map[float64]string), want: addr(map[float64]string{1: "1.0"}), - wantErr: export.NewDuplicateNameError([]byte(`"1"`), len64(`{"1.0":"1.0",`)), + wantErr: newDuplicateNameError("", []byte(`"1"`), len64(`{"1.0":"1.0",`)), }, { name: jsontest.Name("Maps/DuplicateName/Float/AllowDuplicateNames"), opts: []Options{jsontext.AllowDuplicateNames(true)}, @@ -5466,7 +5479,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"hello":"hello","HELLO":"HELLO"}`, inVal: new(map[nocaseString]string), want: addr(map[nocaseString]string{"hello": "hello"}), - wantErr: export.NewDuplicateNameError([]byte(`"HELLO"`), len64(`{"hello":"hello",`)), + wantErr: newDuplicateNameError("", []byte(`"HELLO"`), len64(`{"hello":"hello",`)), }, { name: jsontest.Name("Maps/DuplicateName/NoCaseString/AllowDuplicateNames"), opts: []Options{jsontext.AllowDuplicateNames(true)}, @@ -5992,7 +6005,7 @@ func TestUnmarshal(t *testing.T) { opts: []Options{jsonflags.StringifyWithLegacySemantics | 1}, inBuf: `{"String": "string"}`, inVal: new(structStringifiedAll), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: stringType, Err: export.NewInvalidCharacterError("s", "at start of string (expecting '\"')", 0)}, + wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: stringType, Err: newInvalidCharacterError("s", "at start of string (expecting '\"')", 0, "")}, }, { name: jsontest.Name("Structs/Format/Bytes"), inBuf: `{ @@ -6445,7 +6458,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"A":1,"fizz":nil,"B":2}`, inVal: new(structInlineTextValue), want: addr(structInlineTextValue{A: 1, X: jsontext.Value(`{"fizz":`)}), - wantErr: export.NewInvalidCharacterError("i", "within literal null (expecting 'u')", len64(`{"A":1,"fizz":n`)), + wantErr: newInvalidCharacterError("i", "within literal null (expecting 'u')", len64(`{"A":1,"fizz":n`), "/fizz"), }, { name: jsontest.Name("Structs/InlinedFallback/TextValue/CaseSensitive"), inBuf: `{"A":1,"fizz":"buzz","B":2,"a":3}`, @@ -6457,7 +6470,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"A":1,"fizz":"buzz","B":2,"fizz":"buzz"}`, inVal: new(structInlineTextValue), want: addr(structInlineTextValue{A: 1, X: jsontext.Value(`{"fizz":"buzz"}`), B: 2}), - wantErr: export.NewDuplicateNameError([]byte(`"fizz"`), len64(`{"A":1,"fizz":"buzz","B":2,`)), + wantErr: newDuplicateNameError("", []byte(`"fizz"`), len64(`{"A":1,"fizz":"buzz","B":2,`)), }, { name: jsontest.Name("Structs/InlinedFallback/TextValue/AllowDuplicateNames"), opts: []Options{jsontext.AllowDuplicateNames(true)}, @@ -6555,13 +6568,13 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"A":1,"fizz":nil,"B":2}`, inVal: new(structInlineMapStringAny), want: addr(structInlineMapStringAny{A: 1, X: jsonObject{"fizz": nil}}), - wantErr: export.NewInvalidCharacterError("i", "within literal null (expecting 'u')", len64(`{"A":1,"fizz":n`)), + wantErr: newInvalidCharacterError("i", "within literal null (expecting 'u')", len64(`{"A":1,"fizz":n`), "/fizz"), }, { name: jsontest.Name("Structs/InlinedFallback/MapStringAny/MergeInvalidValue/Existing"), inBuf: `{"A":1,"fizz":nil,"B":2}`, inVal: addr(structInlineMapStringAny{A: 1, X: jsonObject{"fizz": true}}), want: addr(structInlineMapStringAny{A: 1, X: jsonObject{"fizz": true}}), - wantErr: export.NewInvalidCharacterError("i", "within literal null (expecting 'u')", len64(`{"A":1,"fizz":n`)), + wantErr: newInvalidCharacterError("i", "within literal null (expecting 'u')", len64(`{"A":1,"fizz":n`), "/fizz"), }, { name: jsontest.Name("Structs/InlinedFallback/MapStringAny/CaseSensitive"), inBuf: `{"A":1,"fizz":"buzz","B":2,"a":3}`, @@ -6573,7 +6586,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"A":1,"fizz":"buzz","B":2,"fizz":"buzz"}`, inVal: new(structInlineMapStringAny), want: addr(structInlineMapStringAny{A: 1, X: jsonObject{"fizz": "buzz"}, B: 2}), - wantErr: export.NewDuplicateNameError([]byte(`"fizz"`), len64(`{"A":1,"fizz":"buzz","B":2,`)), + wantErr: newDuplicateNameError("", []byte(`"fizz"`), len64(`{"A":1,"fizz":"buzz","B":2,`)), }, { name: jsontest.Name("Structs/InlinedFallback/MapStringAny/AllowDuplicateNames"), opts: []Options{jsontext.AllowDuplicateNames(true)}, @@ -6782,7 +6795,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"AaA":"AaA","aaa":"aaa"}`, inVal: new(structNoCase), want: addr(structNoCase{AaA: "AaA"}), - wantErr: export.NewDuplicateNameError([]byte(`"aaa"`), len64(`{"AaA":"AaA",`)), + wantErr: newDuplicateNameError("", []byte(`"aaa"`), len64(`{"AaA":"AaA",`)), }, { name: jsontest.Name("Structs/CaseSensitive"), inBuf: `{"BOOL": true, "STRING": "hello", "BYTES": "AQID", "INT": -64, "UINT": 64, "FLOAT": 3.14159}`, @@ -6798,7 +6811,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"AAA":"AAA","AAA":"AAA"}`, inVal: addr(structNoCaseInlineTextValue{}), want: addr(structNoCaseInlineTextValue{AAA: "AAA"}), - wantErr: export.NewDuplicateNameError([]byte(`"AAA"`), len64(`{"AAA":"AAA",`)), + wantErr: newDuplicateNameError("", []byte(`"AAA"`), len64(`{"AAA":"AAA",`)), }, { name: jsontest.Name("Structs/DuplicateName/NoCase/OverwriteExact"), inBuf: `{"AAA":"after"}`, @@ -6809,13 +6822,13 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"aaa":"aaa","aaA":"aaA"}`, inVal: addr(structNoCaseInlineTextValue{}), want: addr(structNoCaseInlineTextValue{AaA: "aaa"}), - wantErr: export.NewDuplicateNameError([]byte(`"aaA"`), len64(`{"aaa":"aaa",`)), + wantErr: newDuplicateNameError("", []byte(`"aaA"`), len64(`{"aaa":"aaa",`)), }, { name: jsontest.Name("Structs/DuplicateName/NoCase/OverwriteNoCase"), inBuf: `{"aaa":"aaa","aaA":"aaA"}`, inVal: addr(structNoCaseInlineTextValue{}), want: addr(structNoCaseInlineTextValue{AaA: "aaa"}), - wantErr: export.NewDuplicateNameError([]byte(`"aaA"`), len64(`{"aaa":"aaa",`)), + wantErr: newDuplicateNameError("", []byte(`"aaA"`), len64(`{"aaa":"aaa",`)), }, { name: jsontest.Name("Structs/DuplicateName/Inline/Unknown"), inBuf: `{"unknown":""}`, @@ -6836,7 +6849,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"b":"","b":""}`, inVal: addr(structNoCaseInlineTextValue{}), want: addr(structNoCaseInlineTextValue{X: jsontext.Value(`{"b":""}`)}), - wantErr: export.NewDuplicateNameError([]byte(`"b"`), len64(`{"b":"",`)), + wantErr: newDuplicateNameError("", []byte(`"b"`), len64(`{"b":"",`)), }, { name: jsontest.Name("Structs/Invalid/ErrUnexpectedEOF"), inBuf: ``, @@ -6848,7 +6861,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"Pointer":`, inVal: addr(structAll{}), want: addr(structAll{Pointer: new(structAll)}), - wantErr: io.ErrUnexpectedEOF, + wantErr: &jsontext.SyntacticError{ByteOffset: len64(`{"Pointer":`), JSONPointer: "/Pointer", Err: io.ErrUnexpectedEOF}, }, { name: jsontest.Name("Structs/Invalid/Conflicting"), inBuf: `{}`, @@ -7219,7 +7232,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `]`, inVal: new(any), want: new(any), - wantErr: export.NewInvalidCharacterError("]", "at start of value", 0), + wantErr: newInvalidCharacterError("]", "at start of value", 0, ""), }, { // NOTE: The semantics differs from v1, // where existing map entries were not merged into. @@ -7358,7 +7371,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"X":{"fizz":"buzz","fizz":true}}`, inVal: new(struct{ X any }), want: addr(struct{ X any }{map[string]any{"fizz": "buzz"}}), - wantErr: export.NewDuplicateNameError([]byte(`"fizz"`), len64(`{"X":{"fizz":"buzz",`)), + wantErr: newDuplicateNameError("/X", []byte(`"fizz"`), len64(`{"X":{"fizz":"buzz",`)), }, { name: jsontest.Name("Interfaces/Any/Maps/AllowDuplicateNames"), opts: []Options{jsontext.AllowDuplicateNames(true)}, @@ -7815,7 +7828,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"name":"value","name":"value"}`, inVal: addr(map[string]string{}), want: addr(map[string]string{"1-1": "1-2"}), - wantErr: &SemanticError{action: "unmarshal", GoType: reflect.PointerTo(stringType), Err: export.NewDuplicateNameError([]byte(`"name"`), len64(`{"name":"value",`))}, + wantErr: &SemanticError{action: "unmarshal", GoType: reflect.PointerTo(stringType), Err: newDuplicateNameError("", []byte(`"name"`), len64(`{"name":"value",`))}, }, { name: jsontest.Name("Functions/Map/Value/NoCaseString/V1"), opts: []Options{ @@ -8495,7 +8508,7 @@ func TestUnmarshal(t *testing.T) { want: addr(struct { D time.Duration }{1}), - wantErr: export.NewInvalidCharacterError("x", "at start of value", len64(`{"D":`)), + wantErr: newInvalidCharacterError("x", "at start of value", len64(`{"D":`), "/D"), }, { name: jsontest.Name("Duration/Format/Invalid"), inBuf: `{"D":"0s"}`, @@ -8720,7 +8733,7 @@ func TestUnmarshal(t *testing.T) { inVal: new(struct { T time.Time }), - wantErr: export.NewInvalidCharacterError("x", "at start of value", len64(`{"D":`)), + wantErr: newInvalidCharacterError("x", "at start of value", len64(`{"T":`), "/T"), }, { name: jsontest.Name("Time/IgnoreInvalidFormat"), opts: []Options{invalidFormatOption}, diff --git a/errors.go b/errors.go index 396e5fa..35099d9 100644 --- a/errors.go +++ b/errors.go @@ -9,6 +9,7 @@ import ( "strconv" "strings" + "github.com/go-json-experiment/json/internal/jsonwire" "github.com/go-json-experiment/json/jsontext" ) @@ -115,3 +116,15 @@ func (e *SemanticError) Error() string { func (e *SemanticError) Unwrap() error { return e.Err } + +func newDuplicateNameError(ptr jsontext.Pointer, quotedName []byte, offset int64) error { + if quotedName != nil { + name, _ := jsonwire.AppendUnquote(nil, quotedName) + ptr = ptr.AppendToken(string(name)) + } + return &jsontext.SyntacticError{ + ByteOffset: offset, + JSONPointer: ptr, + Err: jsontext.ErrDuplicateName, + } +} diff --git a/internal/jsonwire/wire.go b/internal/jsonwire/wire.go index 6adfa3e..a1add8e 100644 --- a/internal/jsonwire/wire.go +++ b/internal/jsonwire/wire.go @@ -141,16 +141,11 @@ func truncateMaxUTF8[Bytes ~[]byte | ~string](b Bytes) Bytes { return b } -// NewError and ErrInvalidUTF8 are injected by the "jsontext" package, -// so that these error types use the jsontext.SyntacticError type. -var ( - NewError = errors.New - ErrInvalidUTF8 = errors.New("invalid UTF-8 within string") -) +var ErrInvalidUTF8 = errors.New("invalid UTF-8") func NewInvalidCharacterError[Bytes ~[]byte | ~string](prefix Bytes, where string) error { what := QuoteRune(prefix) - return NewError("invalid character " + what + " " + where) + return errors.New("invalid character " + what + " " + where) } func NewInvalidEscapeSequenceError[Bytes ~[]byte | ~string](what Bytes) error { @@ -162,8 +157,8 @@ func NewInvalidEscapeSequenceError[Bytes ~[]byte | ~string](what Bytes) error { return r == '`' || r == utf8.RuneError || unicode.IsSpace(r) || !unicode.IsPrint(r) }) >= 0 if needEscape { - return NewError("invalid " + label + " " + strconv.Quote(string(what)) + " within string") + return errors.New("invalid " + label + " " + strconv.Quote(string(what)) + " within string") } else { - return NewError("invalid " + label + " `" + string(what) + "` within string") + return errors.New("invalid " + label + " `" + string(what) + "` within string") } } diff --git a/jsontext/coder_test.go b/jsontext/coder_test.go index a5cd3c6..8c34721 100644 --- a/jsontext/coder_test.go +++ b/jsontext/coder_test.go @@ -16,10 +16,29 @@ import ( "testing" "github.com/go-json-experiment/json/internal/jsontest" + "github.com/go-json-experiment/json/internal/jsonwire" ) -func len64[Bytes ~[]byte | ~string](in Bytes) int64 { - return int64(len(in)) +func E(err error) *SyntacticError { + return &SyntacticError{Err: err} +} + +func newInvalidCharacterError(prefix, where string) *SyntacticError { + return E(jsonwire.NewInvalidCharacterError(prefix, where)) +} + +func newInvalidEscapeSequenceError(what string) *SyntacticError { + return E(jsonwire.NewInvalidEscapeSequenceError(what)) +} + +func (e *SyntacticError) withPos(prefix string, pointer Pointer) *SyntacticError { + e.ByteOffset = int64(len(prefix)) + e.JSONPointer = pointer + return e +} + +func equalError(x, y error) bool { + return reflect.DeepEqual(x, y) } var ( @@ -591,13 +610,13 @@ func TestCoderMaxDepth(t *testing.T) { var dec Decoder checkReadToken := func(t *testing.T, wantKind Kind, wantErr error) { t.Helper() - if tok, err := dec.ReadToken(); tok.Kind() != wantKind || !reflect.DeepEqual(err, wantErr) { + if tok, err := dec.ReadToken(); tok.Kind() != wantKind || !equalError(err, wantErr) { t.Fatalf("Decoder.ReadToken = (%q, %v), want (%q, %v)", byte(tok.Kind()), err, byte(wantKind), wantErr) } } checkReadValue := func(t *testing.T, wantLen int, wantErr error) { t.Helper() - if val, err := dec.ReadValue(); len(val) != wantLen || !reflect.DeepEqual(err, wantErr) { + if val, err := dec.ReadValue(); len(val) != wantLen || !equalError(err, wantErr) { t.Fatalf("Decoder.ReadValue = (%d, %v), want (%d, %v)", len(val), err, wantLen, wantErr) } } @@ -622,21 +641,26 @@ func TestCoderMaxDepth(t *testing.T) { } }) + wantErr := &SyntacticError{ + ByteOffset: maxNestingDepth, + JSONPointer: Pointer(strings.Repeat("/0", maxNestingDepth)), + Err: errMaxDepth, + } t.Run("ArraysInvalid/SingleValue", func(t *testing.T) { dec.s.reset(maxArrays, nil) - checkReadValue(t, 0, errMaxDepth.withOffset(maxNestingDepth)) + checkReadValue(t, 0, wantErr) }) t.Run("ArraysInvalid/TokenThenValue", func(t *testing.T) { dec.s.reset(maxArrays, nil) checkReadToken(t, '[', nil) - checkReadValue(t, 0, errMaxDepth.withOffset(maxNestingDepth)) + checkReadValue(t, 0, wantErr) }) t.Run("ArraysInvalid/AllTokens", func(t *testing.T) { dec.s.reset(maxArrays, nil) for range maxNestingDepth { checkReadToken(t, '[', nil) } - checkReadToken(t, 0, errMaxDepth.withOffset(maxNestingDepth)) + checkReadValue(t, 0, wantErr) }) t.Run("ObjectsValid/SingleValue", func(t *testing.T) { @@ -662,15 +686,20 @@ func TestCoderMaxDepth(t *testing.T) { } }) + wantErr = &SyntacticError{ + ByteOffset: maxNestingDepth * int64(len(`{"":`)), + JSONPointer: Pointer(strings.Repeat("/", maxNestingDepth)), + Err: errMaxDepth, + } t.Run("ObjectsInvalid/SingleValue", func(t *testing.T) { dec.s.reset(maxObjects, nil) - checkReadValue(t, 0, errMaxDepth.withOffset(maxNestingDepth*len64(`{"":`))) + checkReadValue(t, 0, wantErr) }) t.Run("ObjectsInvalid/TokenThenValue", func(t *testing.T) { dec.s.reset(maxObjects, nil) checkReadToken(t, '{', nil) checkReadToken(t, '"', nil) - checkReadValue(t, 0, errMaxDepth.withOffset(maxNestingDepth*len64(`{"":`))) + checkReadValue(t, 0, wantErr) }) t.Run("ObjectsInvalid/AllTokens", func(t *testing.T) { dec.s.reset(maxObjects, nil) @@ -678,7 +707,7 @@ func TestCoderMaxDepth(t *testing.T) { checkReadToken(t, '{', nil) checkReadToken(t, '"', nil) } - checkReadToken(t, 0, errMaxDepth.withOffset(maxNestingDepth*len64(`{"":`))) + checkReadToken(t, 0, wantErr) }) }) @@ -686,26 +715,31 @@ func TestCoderMaxDepth(t *testing.T) { var enc Encoder checkWriteToken := func(t *testing.T, tok Token, wantErr error) { t.Helper() - if err := enc.WriteToken(tok); !reflect.DeepEqual(err, wantErr) { + if err := enc.WriteToken(tok); !equalError(err, wantErr) { t.Fatalf("Encoder.WriteToken = %v, want %v", err, wantErr) } } checkWriteValue := func(t *testing.T, val Value, wantErr error) { t.Helper() - if err := enc.WriteValue(val); !reflect.DeepEqual(err, wantErr) { + if err := enc.WriteValue(val); !equalError(err, wantErr) { t.Fatalf("Encoder.WriteValue = %v, want %v", err, wantErr) } } + wantErr := &SyntacticError{ + ByteOffset: maxNestingDepth, + JSONPointer: Pointer(strings.Repeat("/0", maxNestingDepth)), + Err: errMaxDepth, + } t.Run("Arrays/SingleValue", func(t *testing.T) { enc.s.reset(enc.s.Buf[:0], nil) - checkWriteValue(t, maxArrays, errMaxDepth.withOffset(maxNestingDepth)) + checkWriteValue(t, maxArrays, wantErr) checkWriteValue(t, trimArray(maxArrays), nil) }) t.Run("Arrays/TokenThenValue", func(t *testing.T) { enc.s.reset(enc.s.Buf[:0], nil) checkWriteToken(t, ArrayStart, nil) - checkWriteValue(t, trimArray(maxArrays), errMaxDepth.withOffset(maxNestingDepth)) + checkWriteValue(t, trimArray(maxArrays), wantErr) checkWriteValue(t, trimArray(trimArray(maxArrays)), nil) checkWriteToken(t, ArrayEnd, nil) }) @@ -714,22 +748,27 @@ func TestCoderMaxDepth(t *testing.T) { for range maxNestingDepth { checkWriteToken(t, ArrayStart, nil) } - checkWriteToken(t, ArrayStart, errMaxDepth.withOffset(maxNestingDepth)) + checkWriteToken(t, ArrayStart, wantErr) for range maxNestingDepth { checkWriteToken(t, ArrayEnd, nil) } }) + wantErr = &SyntacticError{ + ByteOffset: maxNestingDepth * int64(len(`{"":`)), + JSONPointer: Pointer(strings.Repeat("/", maxNestingDepth)), + Err: errMaxDepth, + } t.Run("Objects/SingleValue", func(t *testing.T) { enc.s.reset(enc.s.Buf[:0], nil) - checkWriteValue(t, maxObjects, errMaxDepth.withOffset(maxNestingDepth*len64(`{"":`))) + checkWriteValue(t, maxObjects, wantErr) checkWriteValue(t, trimObject(maxObjects), nil) }) t.Run("Objects/TokenThenValue", func(t *testing.T) { enc.s.reset(enc.s.Buf[:0], nil) checkWriteToken(t, ObjectStart, nil) checkWriteToken(t, String(""), nil) - checkWriteValue(t, trimObject(maxObjects), errMaxDepth.withOffset(maxNestingDepth*len64(`{"":`))) + checkWriteValue(t, trimObject(maxObjects), wantErr) checkWriteValue(t, trimObject(trimObject(maxObjects)), nil) checkWriteToken(t, ObjectEnd, nil) }) @@ -741,7 +780,7 @@ func TestCoderMaxDepth(t *testing.T) { } checkWriteToken(t, ObjectStart, nil) checkWriteToken(t, String(""), nil) - checkWriteToken(t, ObjectStart, errMaxDepth.withOffset(maxNestingDepth*len64(`{"":`))) + checkWriteToken(t, ObjectStart, wantErr) checkWriteToken(t, String(""), nil) for range maxNestingDepth { checkWriteToken(t, ObjectEnd, nil) diff --git a/jsontext/decode.go b/jsontext/decode.go index 2ac2fec..f2538fe 100644 --- a/jsontext/decode.go +++ b/jsontext/decode.go @@ -255,16 +255,7 @@ func (d *decodeBuffer) needMore(pos int) bool { return pos == len(d.buf) } -// injectSyntacticErrorWithPosition wraps a SyntacticError with the position, -// otherwise it returns the error as is. -// It takes a position relative to the start of the start of d.buf. -func (d *decodeBuffer) injectSyntacticErrorWithPosition(err error, pos int) error { - if serr, ok := err.(*SyntacticError); ok { - return serr.withOffset(d.baseOffset + int64(pos)) - } - return err -} - +func (d *decodeBuffer) offsetAt(pos int) int64 { return d.baseOffset + int64(pos) } func (d *decodeBuffer) previousOffsetStart() int64 { return d.baseOffset + int64(d.prevStart) } func (d *decodeBuffer) previousOffsetEnd() int64 { return d.baseOffset + int64(d.prevEnd) } func (d *decodeBuffer) PreviousBuffer() []byte { return d.buf[d.prevStart:d.prevEnd] } @@ -292,7 +283,7 @@ func (d *decoderState) PeekKind() Kind { if err == io.ErrUnexpectedEOF && d.Tokens.Depth() == 1 { err = io.EOF // EOF possibly if no Tokens present after top-level value } - d.peekPos, d.peekErr = -1, err + d.peekPos, d.peekErr = -1, wrapSyntacticError(d, err, pos, 0) return invalidKind } } @@ -305,6 +296,7 @@ func (d *decoderState) PeekKind() Kind { pos += jsonwire.ConsumeWhitespace(d.buf[pos:]) if d.needMore(pos) { if pos, err = d.consumeWhitespace(pos); err != nil { + err = wrapSyntacticError(d, err, pos, 0) d.peekPos, d.peekErr = -1, d.checkDelimBeforeIOError(delim, err) return invalidKind } @@ -344,7 +336,7 @@ func (d *decoderState) checkDelim(delim byte, next Kind) error { pos := d.prevEnd // restore position to right after leading whitespace pos += jsonwire.ConsumeWhitespace(d.buf[pos:]) err := d.Tokens.checkDelim(delim, next) - return d.injectSyntacticErrorWithPosition(err, pos) + return wrapSyntacticError(d, err, pos, 0) } // SkipValue is semantically equivalent to calling [Decoder.ReadValue] and discarding @@ -408,7 +400,7 @@ func (d *decoderState) ReadToken() (Token, error) { if err == io.ErrUnexpectedEOF && d.Tokens.Depth() == 1 { err = io.EOF // EOF possibly if no Tokens present after top-level value } - return Token{}, err + return Token{}, wrapSyntacticError(d, err, pos, 0) } } @@ -420,6 +412,7 @@ func (d *decoderState) ReadToken() (Token, error) { pos += jsonwire.ConsumeWhitespace(d.buf[pos:]) if d.needMore(pos) { if pos, err = d.consumeWhitespace(pos); err != nil { + err = wrapSyntacticError(d, err, pos, 0) return Token{}, d.checkDelimBeforeIOError(delim, err) } } @@ -437,13 +430,13 @@ func (d *decoderState) ReadToken() (Token, error) { if jsonwire.ConsumeNull(d.buf[pos:]) == 0 { pos, err = d.consumeLiteral(pos, "null") if err != nil { - return Token{}, d.injectSyntacticErrorWithPosition(err, pos) + return Token{}, wrapSyntacticError(d, err, pos, +1) } } else { pos += len("null") } if err = d.Tokens.appendLiteral(); err != nil { - return Token{}, d.injectSyntacticErrorWithPosition(err, pos-len("null")) // report position at start of literal + return Token{}, wrapSyntacticError(d, err, pos-len("null"), +1) // report position at start of literal } d.prevStart, d.prevEnd = pos, pos return Null, nil @@ -452,13 +445,13 @@ func (d *decoderState) ReadToken() (Token, error) { if jsonwire.ConsumeFalse(d.buf[pos:]) == 0 { pos, err = d.consumeLiteral(pos, "false") if err != nil { - return Token{}, d.injectSyntacticErrorWithPosition(err, pos) + return Token{}, wrapSyntacticError(d, err, pos, +1) } } else { pos += len("false") } if err = d.Tokens.appendLiteral(); err != nil { - return Token{}, d.injectSyntacticErrorWithPosition(err, pos-len("false")) // report position at start of literal + return Token{}, wrapSyntacticError(d, err, pos-len("false"), +1) // report position at start of literal } d.prevStart, d.prevEnd = pos, pos return False, nil @@ -467,13 +460,13 @@ func (d *decoderState) ReadToken() (Token, error) { if jsonwire.ConsumeTrue(d.buf[pos:]) == 0 { pos, err = d.consumeLiteral(pos, "true") if err != nil { - return Token{}, d.injectSyntacticErrorWithPosition(err, pos) + return Token{}, wrapSyntacticError(d, err, pos, +1) } } else { pos += len("true") } if err = d.Tokens.appendLiteral(); err != nil { - return Token{}, d.injectSyntacticErrorWithPosition(err, pos-len("true")) // report position at start of literal + return Token{}, wrapSyntacticError(d, err, pos-len("true"), +1) // report position at start of literal } d.prevStart, d.prevEnd = pos, pos return True, nil @@ -486,7 +479,7 @@ func (d *decoderState) ReadToken() (Token, error) { newAbsPos := d.baseOffset + int64(pos) n = int(newAbsPos - oldAbsPos) if err != nil { - return Token{}, d.injectSyntacticErrorWithPosition(err, pos) + return Token{}, wrapSyntacticError(d, err, pos, +1) } } else { pos += n @@ -494,17 +487,17 @@ func (d *decoderState) ReadToken() (Token, error) { if d.Tokens.Last.NeedObjectName() { if !d.Flags.Get(jsonflags.AllowDuplicateNames) { if !d.Tokens.Last.isValidNamespace() { - return Token{}, errInvalidNamespace + return Token{}, wrapSyntacticError(d, errInvalidNamespace, pos-n, +1) } if d.Tokens.Last.isActiveNamespace() && !d.Namespaces.Last().insertQuoted(d.buf[pos-n:pos], flags.IsVerbatim()) { - err = newDuplicateNameError(d.buf[pos-n : pos]) - return Token{}, d.injectSyntacticErrorWithPosition(err, pos-n) // report position at start of string + err = wrapWithObjectName(ErrDuplicateName, d.buf[pos-n:pos]) + return Token{}, wrapSyntacticError(d, err, pos-n, +1) // report position at start of string } } d.Names.ReplaceLastQuotedOffset(pos - n) // only replace if insertQuoted succeeds } if err = d.Tokens.appendString(); err != nil { - return Token{}, d.injectSyntacticErrorWithPosition(err, pos-n) // report position at start of string + return Token{}, wrapSyntacticError(d, err, pos-n, +1) // report position at start of string } d.prevStart, d.prevEnd = pos-n, pos return Token{raw: &d.decodeBuffer, num: uint64(d.previousOffsetStart())}, nil @@ -518,20 +511,20 @@ func (d *decoderState) ReadToken() (Token, error) { newAbsPos := d.baseOffset + int64(pos) n = int(newAbsPos - oldAbsPos) if err != nil { - return Token{}, d.injectSyntacticErrorWithPosition(err, pos) + return Token{}, wrapSyntacticError(d, err, pos, +1) } } else { pos += n } if err = d.Tokens.appendNumber(); err != nil { - return Token{}, d.injectSyntacticErrorWithPosition(err, pos-n) // report position at start of number + return Token{}, wrapSyntacticError(d, err, pos-n, +1) // report position at start of number } d.prevStart, d.prevEnd = pos-n, pos return Token{raw: &d.decodeBuffer, num: uint64(d.previousOffsetStart())}, nil case '{': if err = d.Tokens.pushObject(); err != nil { - return Token{}, d.injectSyntacticErrorWithPosition(err, pos) + return Token{}, wrapSyntacticError(d, err, pos, +1) } d.Names.push() if !d.Flags.Get(jsonflags.AllowDuplicateNames) { @@ -543,7 +536,7 @@ func (d *decoderState) ReadToken() (Token, error) { case '}': if err = d.Tokens.popObject(); err != nil { - return Token{}, d.injectSyntacticErrorWithPosition(err, pos) + return Token{}, wrapSyntacticError(d, err, pos, +1) } d.Names.pop() if !d.Flags.Get(jsonflags.AllowDuplicateNames) { @@ -555,7 +548,7 @@ func (d *decoderState) ReadToken() (Token, error) { case '[': if err = d.Tokens.pushArray(); err != nil { - return Token{}, d.injectSyntacticErrorWithPosition(err, pos) + return Token{}, wrapSyntacticError(d, err, pos, +1) } pos += 1 d.prevStart, d.prevEnd = pos, pos @@ -563,15 +556,15 @@ func (d *decoderState) ReadToken() (Token, error) { case ']': if err = d.Tokens.popArray(); err != nil { - return Token{}, d.injectSyntacticErrorWithPosition(err, pos) + return Token{}, wrapSyntacticError(d, err, pos, +1) } pos += 1 d.prevStart, d.prevEnd = pos, pos return ArrayEnd, nil default: - err = newInvalidCharacterError(d.buf[pos:], "at start of token") - return Token{}, d.injectSyntacticErrorWithPosition(err, pos) + err = jsonwire.NewInvalidCharacterError(d.buf[pos:], "at start of token") + return Token{}, wrapSyntacticError(d, err, pos, +1) } } @@ -614,7 +607,7 @@ func (d *decoderState) ReadValue(flags *jsonwire.ValueFlags) (Value, error) { if err == io.ErrUnexpectedEOF && d.Tokens.Depth() == 1 { err = io.EOF // EOF possibly if no Tokens present after top-level value } - return nil, err + return nil, wrapSyntacticError(d, err, pos, 0) } } @@ -626,6 +619,7 @@ func (d *decoderState) ReadValue(flags *jsonwire.ValueFlags) (Value, error) { pos += jsonwire.ConsumeWhitespace(d.buf[pos:]) if d.needMore(pos) { if pos, err = d.consumeWhitespace(pos); err != nil { + err = wrapSyntacticError(d, err, pos, 0) return nil, d.checkDelimBeforeIOError(delim, err) } } @@ -642,7 +636,7 @@ func (d *decoderState) ReadValue(flags *jsonwire.ValueFlags) (Value, error) { newAbsPos := d.baseOffset + int64(pos) n := int(newAbsPos - oldAbsPos) if err != nil { - return nil, d.injectSyntacticErrorWithPosition(err, pos) + return nil, wrapSyntacticError(d, err, pos, +1) } switch next { case 'n', 't', 'f': @@ -655,7 +649,7 @@ func (d *decoderState) ReadValue(flags *jsonwire.ValueFlags) (Value, error) { break } if d.Tokens.Last.isActiveNamespace() && !d.Namespaces.Last().insertQuoted(d.buf[pos-n:pos], flags.IsVerbatim()) { - err = newDuplicateNameError(d.buf[pos-n : pos]) + err = wrapWithObjectName(ErrDuplicateName, d.buf[pos-n:pos]) break } } @@ -680,7 +674,7 @@ func (d *decoderState) ReadValue(flags *jsonwire.ValueFlags) (Value, error) { } } if err != nil { - return nil, d.injectSyntacticErrorWithPosition(err, pos-n) // report position at start of value + return nil, wrapSyntacticError(d, err, pos-n, +1) // report position at start of value } d.prevEnd = pos d.prevStart = pos - n @@ -691,8 +685,8 @@ func (d *decoderState) ReadValue(flags *jsonwire.ValueFlags) (Value, error) { func (d *decoderState) CheckEOF() error { switch pos, err := d.consumeWhitespace(d.prevEnd); err { case nil: - err := newInvalidCharacterError(d.buf[pos:], "after top-level value") - return d.injectSyntacticErrorWithPosition(err, pos) + err := jsonwire.NewInvalidCharacterError(d.buf[pos:], "after top-level value") + return wrapSyntacticError(d, err, pos, 0) case io.ErrUnexpectedEOF: return nil default: @@ -767,14 +761,14 @@ func (d *decoderState) consumeValue(flags *jsonwire.ValueFlags, pos, depth int) case '[': return d.consumeArray(flags, pos, depth) default: - return pos, newInvalidCharacterError(d.buf[pos:], "at start of value") + return pos, jsonwire.NewInvalidCharacterError(d.buf[pos:], "at start of value") } if err == io.ErrUnexpectedEOF { absPos := d.baseOffset + int64(pos) err = d.fetch() // will mutate d.buf and invalidate pos pos = int(absPos - d.baseOffset) if err != nil { - return pos, err + return pos + n, err } continue } @@ -792,7 +786,7 @@ func (d *decoderState) consumeLiteral(pos int, lit string) (newPos int, err erro err = d.fetch() // will mutate d.buf and invalidate pos pos = int(absPos - d.baseOffset) if err != nil { - return pos, err + return pos + n, err } continue } @@ -811,7 +805,7 @@ func (d *decoderState) consumeString(flags *jsonwire.ValueFlags, pos int) (newPo err = d.fetch() // will mutate d.buf and invalidate pos pos = int(absPos - d.baseOffset) if err != nil { - return pos, err + return pos + n, err } continue } @@ -898,19 +892,21 @@ func (d *decoderState) consumeObject(flags *jsonwire.ValueFlags, pos, depth int) } else { pos += n } - if !d.Flags.Get(jsonflags.AllowDuplicateNames) && !names.insertQuoted(d.buf[pos-n:pos], flags2.IsVerbatim()) { - return pos - n, newDuplicateNameError(d.buf[pos-n : pos]) + quotedName := d.buf[pos-n : pos] + if !d.Flags.Get(jsonflags.AllowDuplicateNames) && !names.insertQuoted(quotedName, flags2.IsVerbatim()) { + return pos - n, wrapWithObjectName(ErrDuplicateName, quotedName) } // Handle after name. pos += jsonwire.ConsumeWhitespace(d.buf[pos:]) if d.needMore(pos) { if pos, err = d.consumeWhitespace(pos); err != nil { - return pos, err + return pos, wrapWithObjectName(err, quotedName) } } if d.buf[pos] != ':' { - return pos, newInvalidCharacterError(d.buf[pos:], "after object name (expecting ':')") + err := jsonwire.NewInvalidCharacterError(d.buf[pos:], "after object name (expecting ':')") + return pos, wrapWithObjectName(err, quotedName) } pos++ @@ -918,12 +914,12 @@ func (d *decoderState) consumeObject(flags *jsonwire.ValueFlags, pos, depth int) pos += jsonwire.ConsumeWhitespace(d.buf[pos:]) if d.needMore(pos) { if pos, err = d.consumeWhitespace(pos); err != nil { - return pos, err + return pos, wrapWithObjectName(err, quotedName) } } pos, err = d.consumeValue(flags, pos, depth) if err != nil { - return pos, err + return pos, wrapWithObjectName(err, quotedName) } // Handle after value. @@ -941,7 +937,7 @@ func (d *decoderState) consumeObject(flags *jsonwire.ValueFlags, pos, depth int) pos++ return pos, nil default: - return pos, newInvalidCharacterError(d.buf[pos:], "after object value (expecting ',' or '}')") + return pos, jsonwire.NewInvalidCharacterError(d.buf[pos:], "after object value (expecting ',' or '}')") } } } @@ -969,6 +965,7 @@ func (d *decoderState) consumeArray(flags *jsonwire.ValueFlags, pos, depth int) return pos, nil } + var idx int64 depth++ for { // Handle before value. @@ -980,7 +977,7 @@ func (d *decoderState) consumeArray(flags *jsonwire.ValueFlags, pos, depth int) } pos, err = d.consumeValue(flags, pos, depth) if err != nil { - return pos, err + return pos, wrapWithArrayIndex(err, idx) } // Handle after value. @@ -993,12 +990,13 @@ func (d *decoderState) consumeArray(flags *jsonwire.ValueFlags, pos, depth int) switch d.buf[pos] { case ',': pos++ + idx++ continue case ']': pos++ return pos, nil default: - return pos, newInvalidCharacterError(d.buf[pos:], "after array value (expecting ',' or ']')") + return pos, jsonwire.NewInvalidCharacterError(d.buf[pos:], "after array value (expecting ',' or ']')") } } } @@ -1057,6 +1055,10 @@ func (d *Decoder) StackIndex(i int) (Kind, int64) { // Object names are only present if [AllowDuplicateNames] is false, otherwise // object members are represented using their index within the object. func (d *Decoder) StackPointer() Pointer { - d.s.Names.copyQuotedBuffer(d.s.buf) - return Pointer(d.s.appendStackPointer(nil)) + return Pointer(d.s.AppendStackPointer(nil, -1)) +} + +func (d *decoderState) AppendStackPointer(b []byte, where int) []byte { + d.Names.copyQuotedBuffer(d.buf) + return d.state.appendStackPointer(b, where) } diff --git a/jsontext/decode_test.go b/jsontext/decode_test.go index 70b46c5..b7e7efd 100644 --- a/jsontext/decode_test.go +++ b/jsontext/decode_test.go @@ -19,6 +19,7 @@ import ( "github.com/go-json-experiment/json/internal/jsonflags" "github.com/go-json-experiment/json/internal/jsontest" + "github.com/go-json-experiment/json/internal/jsonwire" ) // equalTokens reports whether to sequences of tokens formats the same way. @@ -190,8 +191,8 @@ var decoderErrorTestdata = []struct { name: jsontest.Name("InvalidStart"), in: ` #`, calls: []decoderMethodCall{ - {'#', zeroToken, newInvalidCharacterError("#", "at start of token").withOffset(len64(" ")), ""}, - {'#', zeroValue, newInvalidCharacterError("#", "at start of value").withOffset(len64(" ")), ""}, + {'#', zeroToken, newInvalidCharacterError("#", "at start of token").withPos(" ", ""), ""}, + {'#', zeroValue, newInvalidCharacterError("#", "at start of value").withPos(" ", ""), ""}, }, }, { name: jsontest.Name("StreamN0"), @@ -224,65 +225,65 @@ var decoderErrorTestdata = []struct { in: ` null , null `, calls: []decoderMethodCall{ {'n', Null, nil, ""}, - {0, zeroToken, newInvalidCharacterError(",", `before next token`).withOffset(len64(` null `)), ""}, - {0, zeroValue, newInvalidCharacterError(",", `before next token`).withOffset(len64(` null `)), ""}, + {0, zeroToken, newInvalidCharacterError(",", `before next token`).withPos(` null `, ""), ""}, + {0, zeroValue, newInvalidCharacterError(",", `before next token`).withPos(` null `, ""), ""}, }, wantOffset: len(` null`), }, { name: jsontest.Name("TruncatedNull"), in: `nul`, calls: []decoderMethodCall{ - {'n', zeroToken, io.ErrUnexpectedEOF, ""}, - {'n', zeroValue, io.ErrUnexpectedEOF, ""}, + {'n', zeroToken, E(io.ErrUnexpectedEOF).withPos(`nul`, ""), ""}, + {'n', zeroValue, E(io.ErrUnexpectedEOF).withPos(`nul`, ""), ""}, }, }, { name: jsontest.Name("InvalidNull"), in: `nulL`, calls: []decoderMethodCall{ - {'n', zeroToken, newInvalidCharacterError("L", `within literal null (expecting 'l')`).withOffset(len64(`nul`)), ""}, - {'n', zeroValue, newInvalidCharacterError("L", `within literal null (expecting 'l')`).withOffset(len64(`nul`)), ""}, + {'n', zeroToken, newInvalidCharacterError("L", `within literal null (expecting 'l')`).withPos(`nul`, ""), ""}, + {'n', zeroValue, newInvalidCharacterError("L", `within literal null (expecting 'l')`).withPos(`nul`, ""), ""}, }, }, { name: jsontest.Name("TruncatedFalse"), in: `fals`, calls: []decoderMethodCall{ - {'f', zeroToken, io.ErrUnexpectedEOF, ""}, - {'f', zeroValue, io.ErrUnexpectedEOF, ""}, + {'f', zeroToken, E(io.ErrUnexpectedEOF).withPos(`fals`, ""), ""}, + {'f', zeroValue, E(io.ErrUnexpectedEOF).withPos(`fals`, ""), ""}, }, }, { name: jsontest.Name("InvalidFalse"), in: `falsE`, calls: []decoderMethodCall{ - {'f', zeroToken, newInvalidCharacterError("E", `within literal false (expecting 'e')`).withOffset(len64(`fals`)), ""}, - {'f', zeroValue, newInvalidCharacterError("E", `within literal false (expecting 'e')`).withOffset(len64(`fals`)), ""}, + {'f', zeroToken, newInvalidCharacterError("E", `within literal false (expecting 'e')`).withPos(`fals`, ""), ""}, + {'f', zeroValue, newInvalidCharacterError("E", `within literal false (expecting 'e')`).withPos(`fals`, ""), ""}, }, }, { name: jsontest.Name("TruncatedTrue"), in: `tru`, calls: []decoderMethodCall{ - {'t', zeroToken, io.ErrUnexpectedEOF, ""}, - {'t', zeroValue, io.ErrUnexpectedEOF, ""}, + {'t', zeroToken, E(io.ErrUnexpectedEOF).withPos(`tru`, ""), ""}, + {'t', zeroValue, E(io.ErrUnexpectedEOF).withPos(`tru`, ""), ""}, }, }, { name: jsontest.Name("InvalidTrue"), in: `truE`, calls: []decoderMethodCall{ - {'t', zeroToken, newInvalidCharacterError("E", `within literal true (expecting 'e')`).withOffset(len64(`tru`)), ""}, - {'t', zeroValue, newInvalidCharacterError("E", `within literal true (expecting 'e')`).withOffset(len64(`tru`)), ""}, + {'t', zeroToken, newInvalidCharacterError("E", `within literal true (expecting 'e')`).withPos(`tru`, ""), ""}, + {'t', zeroValue, newInvalidCharacterError("E", `within literal true (expecting 'e')`).withPos(`tru`, ""), ""}, }, }, { name: jsontest.Name("TruncatedString"), in: `"start`, calls: []decoderMethodCall{ - {'"', zeroToken, io.ErrUnexpectedEOF, ""}, - {'"', zeroValue, io.ErrUnexpectedEOF, ""}, + {'"', zeroToken, E(io.ErrUnexpectedEOF).withPos(`"start`, ""), ""}, + {'"', zeroValue, E(io.ErrUnexpectedEOF).withPos(`"start`, ""), ""}, }, }, { name: jsontest.Name("InvalidString"), in: `"ok` + "\x00", calls: []decoderMethodCall{ - {'"', zeroToken, newInvalidCharacterError("\x00", `within string (expecting non-control character)`).withOffset(len64(`"ok`)), ""}, - {'"', zeroValue, newInvalidCharacterError("\x00", `within string (expecting non-control character)`).withOffset(len64(`"ok`)), ""}, + {'"', zeroToken, newInvalidCharacterError("\x00", `within string (expecting non-control character)`).withPos(`"ok`, ""), ""}, + {'"', zeroValue, newInvalidCharacterError("\x00", `within string (expecting non-control character)`).withPos(`"ok`, ""), ""}, }, }, { name: jsontest.Name("ValidString/AllowInvalidUTF8/Token"), @@ -305,238 +306,238 @@ var decoderErrorTestdata = []struct { opts: []Options{AllowInvalidUTF8(false)}, in: "\"living\xde\xad\xbe\xef\"", calls: []decoderMethodCall{ - {'"', zeroToken, errInvalidUTF8.withOffset(len64("\"living\xde\xad")), ""}, - {'"', zeroValue, errInvalidUTF8.withOffset(len64("\"living\xde\xad")), ""}, + {'"', zeroToken, E(jsonwire.ErrInvalidUTF8).withPos("\"living\xde\xad", ""), ""}, + {'"', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos("\"living\xde\xad", ""), ""}, }, }, { name: jsontest.Name("TruncatedNumber"), in: `0.`, calls: []decoderMethodCall{ - {'0', zeroToken, io.ErrUnexpectedEOF, ""}, - {'0', zeroValue, io.ErrUnexpectedEOF, ""}, + {'0', zeroToken, E(io.ErrUnexpectedEOF), ""}, + {'0', zeroValue, E(io.ErrUnexpectedEOF), ""}, }, }, { name: jsontest.Name("InvalidNumber"), in: `0.e`, calls: []decoderMethodCall{ - {'0', zeroToken, newInvalidCharacterError("e", "within number (expecting digit)").withOffset(len64(`0.`)), ""}, - {'0', zeroValue, newInvalidCharacterError("e", "within number (expecting digit)").withOffset(len64(`0.`)), ""}, + {'0', zeroToken, newInvalidCharacterError("e", "within number (expecting digit)").withPos(`0.`, ""), ""}, + {'0', zeroValue, newInvalidCharacterError("e", "within number (expecting digit)").withPos(`0.`, ""), ""}, }, }, { name: jsontest.Name("TruncatedObject/AfterStart"), in: `{`, calls: []decoderMethodCall{ - {'{', zeroValue, io.ErrUnexpectedEOF, ""}, + {'{', zeroValue, E(io.ErrUnexpectedEOF).withPos("{", ""), ""}, {'{', ObjectStart, nil, ""}, - {0, zeroToken, io.ErrUnexpectedEOF, ""}, - {0, zeroValue, io.ErrUnexpectedEOF, ""}, + {0, zeroToken, E(io.ErrUnexpectedEOF).withPos("{", ""), ""}, + {0, zeroValue, E(io.ErrUnexpectedEOF).withPos("{", ""), ""}, }, wantOffset: len(`{`), }, { name: jsontest.Name("TruncatedObject/AfterName"), in: `{"0"`, calls: []decoderMethodCall{ - {'{', zeroValue, io.ErrUnexpectedEOF, ""}, + {'{', zeroValue, E(io.ErrUnexpectedEOF).withPos(`{"0"`, "/0"), ""}, {'{', ObjectStart, nil, ""}, {'"', String("0"), nil, ""}, - {0, zeroToken, io.ErrUnexpectedEOF, ""}, - {0, zeroValue, io.ErrUnexpectedEOF, ""}, + {0, zeroToken, E(io.ErrUnexpectedEOF).withPos(`{"0"`, "/0"), ""}, + {0, zeroValue, E(io.ErrUnexpectedEOF).withPos(`{"0"`, "/0"), ""}, }, wantOffset: len(`{"0"`), }, { name: jsontest.Name("TruncatedObject/AfterColon"), in: `{"0":`, calls: []decoderMethodCall{ - {'{', zeroValue, io.ErrUnexpectedEOF, ""}, + {'{', zeroValue, E(io.ErrUnexpectedEOF).withPos(`{"0":`, "/0"), ""}, {'{', ObjectStart, nil, ""}, {'"', String("0"), nil, ""}, - {0, zeroToken, io.ErrUnexpectedEOF, ""}, - {0, zeroValue, io.ErrUnexpectedEOF, ""}, + {0, zeroToken, E(io.ErrUnexpectedEOF).withPos(`{"0":`, "/0"), ""}, + {0, zeroValue, E(io.ErrUnexpectedEOF).withPos(`{"0":`, "/0"), ""}, }, wantOffset: len(`{"0"`), }, { name: jsontest.Name("TruncatedObject/AfterValue"), in: `{"0":0`, calls: []decoderMethodCall{ - {'{', zeroValue, io.ErrUnexpectedEOF, ""}, + {'{', zeroValue, E(io.ErrUnexpectedEOF).withPos(`{"0":0`, ""), ""}, {'{', ObjectStart, nil, ""}, {'"', String("0"), nil, ""}, {'0', Uint(0), nil, ""}, - {0, zeroToken, io.ErrUnexpectedEOF, ""}, - {0, zeroValue, io.ErrUnexpectedEOF, ""}, + {0, zeroToken, E(io.ErrUnexpectedEOF).withPos(`{"0":0`, ""), ""}, + {0, zeroValue, E(io.ErrUnexpectedEOF).withPos(`{"0":0`, ""), ""}, }, wantOffset: len(`{"0":0`), }, { name: jsontest.Name("TruncatedObject/AfterComma"), in: `{"0":0,`, calls: []decoderMethodCall{ - {'{', zeroValue, io.ErrUnexpectedEOF, ""}, + {'{', zeroValue, E(io.ErrUnexpectedEOF).withPos(`{"0":0,`, ""), ""}, {'{', ObjectStart, nil, ""}, {'"', String("0"), nil, ""}, {'0', Uint(0), nil, ""}, - {0, zeroToken, io.ErrUnexpectedEOF, ""}, - {0, zeroValue, io.ErrUnexpectedEOF, ""}, + {0, zeroToken, E(io.ErrUnexpectedEOF).withPos(`{"0":0,`, ""), ""}, + {0, zeroValue, E(io.ErrUnexpectedEOF).withPos(`{"0":0,`, ""), ""}, }, wantOffset: len(`{"0":0`), }, { name: jsontest.Name("InvalidObject/MissingColon"), in: ` { "fizz" "buzz" } `, calls: []decoderMethodCall{ - {'{', zeroValue, newInvalidCharacterError("\"", "after object name (expecting ':')").withOffset(len64(` { "fizz" `)), ""}, + {'{', zeroValue, newInvalidCharacterError("\"", "after object name (expecting ':')").withPos(` { "fizz" `, "/fizz"), ""}, {'{', ObjectStart, nil, ""}, {'"', String("fizz"), nil, ""}, - {0, zeroToken, errMissingColon.withOffset(len64(` { "fizz" `)), ""}, - {0, zeroValue, errMissingColon.withOffset(len64(` { "fizz" `)), ""}, + {0, zeroToken, E(errMissingColon).withPos(` { "fizz" `, "/fizz"), ""}, + {0, zeroValue, E(errMissingColon).withPos(` { "fizz" `, "/fizz"), ""}, }, wantOffset: len(` { "fizz"`), }, { name: jsontest.Name("InvalidObject/MissingColon/GotComma"), in: ` { "fizz" , "buzz" } `, calls: []decoderMethodCall{ - {'{', zeroValue, newInvalidCharacterError(",", "after object name (expecting ':')").withOffset(len64(` { "fizz" `)), ""}, + {'{', zeroValue, newInvalidCharacterError(",", "after object name (expecting ':')").withPos(` { "fizz" `, "/fizz"), ""}, {'{', ObjectStart, nil, ""}, {'"', String("fizz"), nil, ""}, - {0, zeroToken, errMissingColon.withOffset(len64(` { "fizz" `)), ""}, - {0, zeroValue, errMissingColon.withOffset(len64(` { "fizz" `)), ""}, + {0, zeroToken, E(errMissingColon).withPos(` { "fizz" `, "/fizz"), ""}, + {0, zeroValue, E(errMissingColon).withPos(` { "fizz" `, "/fizz"), ""}, }, wantOffset: len(` { "fizz"`), }, { name: jsontest.Name("InvalidObject/MissingColon/GotHash"), in: ` { "fizz" # "buzz" } `, calls: []decoderMethodCall{ - {'{', zeroValue, newInvalidCharacterError("#", "after object name (expecting ':')").withOffset(len64(` { "fizz" `)), ""}, + {'{', zeroValue, newInvalidCharacterError("#", "after object name (expecting ':')").withPos(` { "fizz" `, "/fizz"), ""}, {'{', ObjectStart, nil, ""}, {'"', String("fizz"), nil, ""}, - {0, zeroToken, errMissingColon.withOffset(len64(` { "fizz" `)), ""}, - {0, zeroValue, errMissingColon.withOffset(len64(` { "fizz" `)), ""}, + {0, zeroToken, E(errMissingColon).withPos(` { "fizz" `, "/fizz"), ""}, + {0, zeroValue, E(errMissingColon).withPos(` { "fizz" `, "/fizz"), ""}, }, wantOffset: len(` { "fizz"`), }, { name: jsontest.Name("InvalidObject/MissingComma"), in: ` { "fizz" : "buzz" "gazz" } `, calls: []decoderMethodCall{ - {'{', zeroValue, newInvalidCharacterError("\"", "after object value (expecting ',' or '}')").withOffset(len64(` { "fizz" : "buzz" `)), ""}, + {'{', zeroValue, newInvalidCharacterError("\"", "after object value (expecting ',' or '}')").withPos(` { "fizz" : "buzz" `, ""), ""}, {'{', ObjectStart, nil, ""}, {'"', String("fizz"), nil, ""}, {'"', String("buzz"), nil, ""}, - {0, zeroToken, errMissingComma.withOffset(len64(` { "fizz" : "buzz" `)), ""}, - {0, zeroValue, errMissingComma.withOffset(len64(` { "fizz" : "buzz" `)), ""}, + {0, zeroToken, E(errMissingComma).withPos(` { "fizz" : "buzz" `, ""), ""}, + {0, zeroValue, E(errMissingComma).withPos(` { "fizz" : "buzz" `, ""), ""}, }, wantOffset: len(` { "fizz" : "buzz"`), }, { name: jsontest.Name("InvalidObject/MissingComma/GotColon"), in: ` { "fizz" : "buzz" : "gazz" } `, calls: []decoderMethodCall{ - {'{', zeroValue, newInvalidCharacterError(":", "after object value (expecting ',' or '}')").withOffset(len64(` { "fizz" : "buzz" `)), ""}, + {'{', zeroValue, newInvalidCharacterError(":", "after object value (expecting ',' or '}')").withPos(` { "fizz" : "buzz" `, ""), ""}, {'{', ObjectStart, nil, ""}, {'"', String("fizz"), nil, ""}, {'"', String("buzz"), nil, ""}, - {0, zeroToken, errMissingComma.withOffset(len64(` { "fizz" : "buzz" `)), ""}, - {0, zeroValue, errMissingComma.withOffset(len64(` { "fizz" : "buzz" `)), ""}, + {0, zeroToken, E(errMissingComma).withPos(` { "fizz" : "buzz" `, ""), ""}, + {0, zeroValue, E(errMissingComma).withPos(` { "fizz" : "buzz" `, ""), ""}, }, wantOffset: len(` { "fizz" : "buzz"`), }, { name: jsontest.Name("InvalidObject/MissingComma/GotHash"), in: ` { "fizz" : "buzz" # "gazz" } `, calls: []decoderMethodCall{ - {'{', zeroValue, newInvalidCharacterError("#", "after object value (expecting ',' or '}')").withOffset(len64(` { "fizz" : "buzz" `)), ""}, + {'{', zeroValue, newInvalidCharacterError("#", "after object value (expecting ',' or '}')").withPos(` { "fizz" : "buzz" `, ""), ""}, {'{', ObjectStart, nil, ""}, {'"', String("fizz"), nil, ""}, {'"', String("buzz"), nil, ""}, - {0, zeroToken, errMissingComma.withOffset(len64(` { "fizz" : "buzz" `)), ""}, - {0, zeroValue, errMissingComma.withOffset(len64(` { "fizz" : "buzz" `)), ""}, + {0, zeroToken, E(errMissingComma).withPos(` { "fizz" : "buzz" `, ""), ""}, + {0, zeroValue, E(errMissingComma).withPos(` { "fizz" : "buzz" `, ""), ""}, }, wantOffset: len(` { "fizz" : "buzz"`), }, { name: jsontest.Name("InvalidObject/ExtraComma/AfterStart"), in: ` { , } `, calls: []decoderMethodCall{ - {'{', zeroValue, newInvalidCharacterError(",", `at start of string (expecting '"')`).withOffset(len64(` { `)), ""}, + {'{', zeroValue, newInvalidCharacterError(",", `at start of string (expecting '"')`).withPos(` { `, ""), ""}, {'{', ObjectStart, nil, ""}, - {0, zeroToken, newInvalidCharacterError(",", `before next token`).withOffset(len64(` { `)), ""}, - {0, zeroValue, newInvalidCharacterError(",", `before next token`).withOffset(len64(` { `)), ""}, + {0, zeroToken, newInvalidCharacterError(",", `before next token`).withPos(` { `, ""), ""}, + {0, zeroValue, newInvalidCharacterError(",", `before next token`).withPos(` { `, ""), ""}, }, wantOffset: len(` {`), }, { name: jsontest.Name("InvalidObject/ExtraComma/AfterValue"), in: ` { "fizz" : "buzz" , } `, calls: []decoderMethodCall{ - {'{', zeroValue, newInvalidCharacterError("}", `at start of string (expecting '"')`).withOffset(len64(` { "fizz" : "buzz" , `)), ""}, + {'{', zeroValue, newInvalidCharacterError("}", `at start of string (expecting '"')`).withPos(` { "fizz" : "buzz" , `, ""), ""}, {'{', ObjectStart, nil, ""}, {'"', String("fizz"), nil, ""}, {'"', String("buzz"), nil, ""}, - {0, zeroToken, newInvalidCharacterError(",", `before next token`).withOffset(len64(` { "fizz" : "buzz" `)), ""}, - {0, zeroValue, newInvalidCharacterError(",", `before next token`).withOffset(len64(` { "fizz" : "buzz" `)), ""}, + {0, zeroToken, newInvalidCharacterError(",", `before next token`).withPos(` { "fizz" : "buzz" `, ""), ""}, + {0, zeroValue, newInvalidCharacterError(",", `before next token`).withPos(` { "fizz" : "buzz" `, ""), ""}, }, wantOffset: len(` { "fizz" : "buzz"`), }, { name: jsontest.Name("InvalidObject/InvalidName/GotNull"), in: ` { null : null } `, calls: []decoderMethodCall{ - {'{', zeroValue, newInvalidCharacterError("n", "at start of string (expecting '\"')").withOffset(len64(` { `)), ""}, + {'{', zeroValue, newInvalidCharacterError("n", "at start of string (expecting '\"')").withPos(` { `, ""), ""}, {'{', ObjectStart, nil, ""}, - {'n', zeroToken, errMissingName.withOffset(len64(` { `)), ""}, - {'n', zeroValue, errMissingName.withOffset(len64(` { `)), ""}, + {'n', zeroToken, E(ErrNonStringName).withPos(` { `, ""), ""}, + {'n', zeroValue, E(ErrNonStringName).withPos(` { `, ""), ""}, }, wantOffset: len(` {`), }, { name: jsontest.Name("InvalidObject/InvalidName/GotFalse"), in: ` { false : false } `, calls: []decoderMethodCall{ - {'{', zeroValue, newInvalidCharacterError("f", "at start of string (expecting '\"')").withOffset(len64(` { `)), ""}, + {'{', zeroValue, newInvalidCharacterError("f", "at start of string (expecting '\"')").withPos(` { `, ""), ""}, {'{', ObjectStart, nil, ""}, - {'f', zeroToken, errMissingName.withOffset(len64(` { `)), ""}, - {'f', zeroValue, errMissingName.withOffset(len64(` { `)), ""}, + {'f', zeroToken, E(ErrNonStringName).withPos(` { `, ""), ""}, + {'f', zeroValue, E(ErrNonStringName).withPos(` { `, ""), ""}, }, wantOffset: len(` {`), }, { name: jsontest.Name("InvalidObject/InvalidName/GotTrue"), in: ` { true : true } `, calls: []decoderMethodCall{ - {'{', zeroValue, newInvalidCharacterError("t", "at start of string (expecting '\"')").withOffset(len64(` { `)), ""}, + {'{', zeroValue, newInvalidCharacterError("t", "at start of string (expecting '\"')").withPos(` { `, ""), ""}, {'{', ObjectStart, nil, ""}, - {'t', zeroToken, errMissingName.withOffset(len64(` { `)), ""}, - {'t', zeroValue, errMissingName.withOffset(len64(` { `)), ""}, + {'t', zeroToken, E(ErrNonStringName).withPos(` { `, ""), ""}, + {'t', zeroValue, E(ErrNonStringName).withPos(` { `, ""), ""}, }, wantOffset: len(` {`), }, { name: jsontest.Name("InvalidObject/InvalidName/GotNumber"), in: ` { 0 : 0 } `, calls: []decoderMethodCall{ - {'{', zeroValue, newInvalidCharacterError("0", "at start of string (expecting '\"')").withOffset(len64(` { `)), ""}, + {'{', zeroValue, newInvalidCharacterError("0", "at start of string (expecting '\"')").withPos(` { `, ""), ""}, {'{', ObjectStart, nil, ""}, - {'0', zeroToken, errMissingName.withOffset(len64(` { `)), ""}, - {'0', zeroValue, errMissingName.withOffset(len64(` { `)), ""}, + {'0', zeroToken, E(ErrNonStringName).withPos(` { `, ""), ""}, + {'0', zeroValue, E(ErrNonStringName).withPos(` { `, ""), ""}, }, wantOffset: len(` {`), }, { name: jsontest.Name("InvalidObject/InvalidName/GotObject"), in: ` { {} : {} } `, calls: []decoderMethodCall{ - {'{', zeroValue, newInvalidCharacterError("{", "at start of string (expecting '\"')").withOffset(len64(` { `)), ""}, + {'{', zeroValue, newInvalidCharacterError("{", "at start of string (expecting '\"')").withPos(` { `, ""), ""}, {'{', ObjectStart, nil, ""}, - {'{', zeroToken, errMissingName.withOffset(len64(` { `)), ""}, - {'{', zeroValue, errMissingName.withOffset(len64(` { `)), ""}, + {'{', zeroToken, E(ErrNonStringName).withPos(` { `, ""), ""}, + {'{', zeroValue, E(ErrNonStringName).withPos(` { `, ""), ""}, }, wantOffset: len(` {`), }, { name: jsontest.Name("InvalidObject/InvalidName/GotArray"), in: ` { [] : [] } `, calls: []decoderMethodCall{ - {'{', zeroValue, newInvalidCharacterError("[", "at start of string (expecting '\"')").withOffset(len64(` { `)), ""}, + {'{', zeroValue, newInvalidCharacterError("[", "at start of string (expecting '\"')").withPos(` { `, ""), ""}, {'{', ObjectStart, nil, ""}, - {'[', zeroToken, errMissingName.withOffset(len64(` { `)), ""}, - {'[', zeroValue, errMissingName.withOffset(len64(` { `)), ""}, + {'[', zeroToken, E(ErrNonStringName).withPos(` { `, ""), ""}, + {'[', zeroValue, E(ErrNonStringName).withPos(` { `, ""), ""}, }, wantOffset: len(` {`), }, { name: jsontest.Name("InvalidObject/MismatchingDelim"), in: ` { ] `, calls: []decoderMethodCall{ - {'{', zeroValue, newInvalidCharacterError("]", "at start of string (expecting '\"')").withOffset(len64(` { `)), ""}, + {'{', zeroValue, newInvalidCharacterError("]", "at start of string (expecting '\"')").withPos(` { `, ""), ""}, {'{', ObjectStart, nil, ""}, - {']', zeroToken, errMismatchDelim.withOffset(len64(` { `)), ""}, - {']', zeroValue, newInvalidCharacterError("]", "at start of value").withOffset(len64(` { `)), ""}, + {']', zeroToken, E(errMismatchDelim).withPos(` { `, ""), ""}, + {']', zeroValue, newInvalidCharacterError("]", "at start of value").withPos(` { `, ""), ""}, }, wantOffset: len(` {`), }, { @@ -544,7 +545,7 @@ var decoderErrorTestdata = []struct { in: ` { } `, calls: []decoderMethodCall{ {'{', ObjectStart, nil, ""}, - {'}', zeroValue, newInvalidCharacterError("}", "at start of value").withOffset(len64(" { ")), ""}, + {'}', zeroValue, newInvalidCharacterError("}", "at start of value").withPos(" { ", ""), ""}, }, wantOffset: len(` {`), }, { @@ -574,71 +575,71 @@ var decoderErrorTestdata = []struct { wantOffset: len(`{"0":0,"0":0}`), }, { name: jsontest.Name("InvalidObject/DuplicateNames"), - in: `{"0":{},"1":{},"0":{}} `, + in: `{"X":{},"Y":{},"X":{}} `, calls: []decoderMethodCall{ - {'{', zeroValue, newDuplicateNameError(`"0"`).withOffset(len64(`{"0":{},"1":{},`)), ""}, + {'{', zeroValue, E(ErrDuplicateName).withPos(`{"X":{},"Y":{},`, "/X"), ""}, {'{', ObjectStart, nil, ""}, - {'"', String("0"), nil, ""}, + {'"', String("X"), nil, ""}, {'{', ObjectStart, nil, ""}, {'}', ObjectEnd, nil, ""}, - {'"', String("1"), nil, ""}, + {'"', String("Y"), nil, ""}, {'{', ObjectStart, nil, ""}, {'}', ObjectEnd, nil, ""}, - {'"', zeroToken, newDuplicateNameError(`"0"`).withOffset(len64(`{"0":{},"1":{},`)), "/1"}, - {'"', zeroValue, newDuplicateNameError(`"0"`).withOffset(len64(`{"0":{},"1":{},`)), "/1"}, + {'"', zeroToken, E(ErrDuplicateName).withPos(`{"X":{},"Y":{},`, "/X"), "/Y"}, + {'"', zeroValue, E(ErrDuplicateName).withPos(`{"0":{},"Y":{},`, "/X"), "/Y"}, }, wantOffset: len(`{"0":{},"1":{}`), }, { name: jsontest.Name("TruncatedArray/AfterStart"), in: `[`, calls: []decoderMethodCall{ - {'[', zeroValue, io.ErrUnexpectedEOF, ""}, + {'[', zeroValue, E(io.ErrUnexpectedEOF).withPos("[", ""), ""}, {'[', ArrayStart, nil, ""}, - {0, zeroToken, io.ErrUnexpectedEOF, ""}, - {0, zeroValue, io.ErrUnexpectedEOF, ""}, + {0, zeroToken, E(io.ErrUnexpectedEOF).withPos("[", ""), ""}, + {0, zeroValue, E(io.ErrUnexpectedEOF).withPos("[", ""), ""}, }, wantOffset: len(`[`), }, { name: jsontest.Name("TruncatedArray/AfterValue"), in: `[0`, calls: []decoderMethodCall{ - {'[', zeroValue, io.ErrUnexpectedEOF, ""}, + {'[', zeroValue, E(io.ErrUnexpectedEOF).withPos("[0", ""), ""}, {'[', ArrayStart, nil, ""}, {'0', Uint(0), nil, ""}, - {0, zeroToken, io.ErrUnexpectedEOF, ""}, - {0, zeroValue, io.ErrUnexpectedEOF, ""}, + {0, zeroToken, E(io.ErrUnexpectedEOF).withPos("[0", ""), ""}, + {0, zeroValue, E(io.ErrUnexpectedEOF).withPos("[0", ""), ""}, }, wantOffset: len(`[0`), }, { name: jsontest.Name("TruncatedArray/AfterComma"), in: `[0,`, calls: []decoderMethodCall{ - {'[', zeroValue, io.ErrUnexpectedEOF, ""}, + {'[', zeroValue, E(io.ErrUnexpectedEOF).withPos("[0,", ""), ""}, {'[', ArrayStart, nil, ""}, {'0', Uint(0), nil, ""}, - {0, zeroToken, io.ErrUnexpectedEOF, ""}, - {0, zeroValue, io.ErrUnexpectedEOF, ""}, + {0, zeroToken, E(io.ErrUnexpectedEOF).withPos("[0,", ""), ""}, + {0, zeroValue, E(io.ErrUnexpectedEOF).withPos("[0,", ""), ""}, }, wantOffset: len(`[0`), }, { name: jsontest.Name("InvalidArray/MissingComma"), in: ` [ "fizz" "buzz" ] `, calls: []decoderMethodCall{ - {'[', zeroValue, newInvalidCharacterError("\"", "after array value (expecting ',' or ']')").withOffset(len64(` [ "fizz" `)), ""}, + {'[', zeroValue, newInvalidCharacterError("\"", "after array value (expecting ',' or ']')").withPos(` [ "fizz" `, ""), ""}, {'[', ArrayStart, nil, ""}, {'"', String("fizz"), nil, ""}, - {0, zeroToken, errMissingComma.withOffset(len64(` [ "fizz" `)), ""}, - {0, zeroValue, errMissingComma.withOffset(len64(` [ "fizz" `)), ""}, + {0, zeroToken, E(errMissingComma).withPos(` [ "fizz" `, ""), ""}, + {0, zeroValue, E(errMissingComma).withPos(` [ "fizz" `, ""), ""}, }, wantOffset: len(` [ "fizz"`), }, { name: jsontest.Name("InvalidArray/MismatchingDelim"), in: ` [ } `, calls: []decoderMethodCall{ - {'[', zeroValue, newInvalidCharacterError("}", "at start of value").withOffset(len64(` [ `)), ""}, + {'[', zeroValue, newInvalidCharacterError("}", "at start of value").withPos(` [ `, "/0"), ""}, {'[', ArrayStart, nil, ""}, - {'}', zeroToken, errMismatchDelim.withOffset(len64(` { `)), ""}, - {'}', zeroValue, newInvalidCharacterError("}", "at start of value").withOffset(len64(` [ `)), ""}, + {'}', zeroToken, E(errMismatchDelim).withPos(` [ `, "/0"), ""}, + {'}', zeroValue, newInvalidCharacterError("}", "at start of value").withPos(` [ `, "/0"), ""}, }, wantOffset: len(` [`), }, { @@ -646,7 +647,7 @@ var decoderErrorTestdata = []struct { in: ` [ ] `, calls: []decoderMethodCall{ {'[', ArrayStart, nil, ""}, - {']', zeroValue, newInvalidCharacterError("]", "at start of value").withOffset(len64(" [ ")), ""}, + {']', zeroValue, newInvalidCharacterError("]", "at start of value").withPos(" [ ", "/0"), ""}, }, wantOffset: len(` [`), }, { @@ -654,8 +655,8 @@ var decoderErrorTestdata = []struct { in: `"",`, calls: []decoderMethodCall{ {'"', String(""), nil, ""}, - {0, zeroToken, newInvalidCharacterError([]byte(","), "before next token").withOffset(len64(`""`)), ""}, - {0, zeroValue, newInvalidCharacterError([]byte(","), "before next token").withOffset(len64(`""`)), ""}, + {0, zeroToken, newInvalidCharacterError(",", "before next token").withPos(`""`, ""), ""}, + {0, zeroValue, newInvalidCharacterError(",", "before next token").withPos(`""`, ""), ""}, }, wantOffset: len(`""`), }, { @@ -663,8 +664,8 @@ var decoderErrorTestdata = []struct { in: `{:`, calls: []decoderMethodCall{ {'{', ObjectStart, nil, ""}, - {0, zeroToken, newInvalidCharacterError([]byte(":"), "before next token").withOffset(len64(`{`)), ""}, - {0, zeroValue, newInvalidCharacterError([]byte(":"), "before next token").withOffset(len64(`{`)), ""}, + {0, zeroToken, newInvalidCharacterError(":", "before next token").withPos(`{`, ""), ""}, + {0, zeroValue, newInvalidCharacterError(":", "before next token").withPos(`{`, ""), ""}, }, wantOffset: len(`{`), }, { @@ -673,8 +674,8 @@ var decoderErrorTestdata = []struct { calls: []decoderMethodCall{ {'{', ObjectStart, nil, ""}, {'"', String(""), nil, ""}, - {0, zeroToken, errMissingColon.withOffset(len64(`{""`)), ""}, - {0, zeroValue, errMissingColon.withOffset(len64(`{""`)), ""}, + {0, zeroToken, E(errMissingColon).withPos(`{""`, "/"), ""}, + {0, zeroValue, E(errMissingColon).withPos(`{""`, "/"), ""}, }, wantOffset: len(`{""`), }, { @@ -683,8 +684,8 @@ var decoderErrorTestdata = []struct { calls: []decoderMethodCall{ {'{', ObjectStart, nil, ""}, {'"', String(""), nil, ""}, - {0, zeroToken, io.ErrUnexpectedEOF, ""}, - {0, zeroValue, io.ErrUnexpectedEOF, ""}, + {0, zeroToken, E(io.ErrUnexpectedEOF).withPos(`{"":`, "/"), ""}, + {0, zeroValue, E(io.ErrUnexpectedEOF).withPos(`{"":`, "/"), ""}, }, wantOffset: len(`{""`), }, { @@ -694,8 +695,8 @@ var decoderErrorTestdata = []struct { {'{', ObjectStart, nil, ""}, {'"', String(""), nil, ""}, {'"', String(""), nil, ""}, - {0, zeroToken, errMissingComma.withOffset(len64(`{"":""`)), ""}, - {0, zeroValue, errMissingComma.withOffset(len64(`{"":""`)), ""}, + {0, zeroToken, E(errMissingComma).withPos(`{"":""`, ""), ""}, + {0, zeroValue, E(errMissingComma).withPos(`{"":""`, ""), ""}, }, wantOffset: len(`{"":""`), }, { @@ -705,8 +706,8 @@ var decoderErrorTestdata = []struct { {'{', ObjectStart, nil, ""}, {'"', String(""), nil, ""}, {'"', String(""), nil, ""}, - {0, zeroToken, io.ErrUnexpectedEOF, ""}, - {0, zeroValue, io.ErrUnexpectedEOF, ""}, + {0, zeroToken, E(io.ErrUnexpectedEOF).withPos(`{"":"",`, ""), ""}, + {0, zeroValue, E(io.ErrUnexpectedEOF).withPos(`{"":"",`, ""), ""}, }, wantOffset: len(`{"":""`), }, { @@ -714,8 +715,8 @@ var decoderErrorTestdata = []struct { in: `[,`, calls: []decoderMethodCall{ {'[', ArrayStart, nil, ""}, - {0, zeroToken, newInvalidCharacterError([]byte(","), "before next token").withOffset(len64(`[`)), ""}, - {0, zeroValue, newInvalidCharacterError([]byte(","), "before next token").withOffset(len64(`[`)), ""}, + {0, zeroToken, newInvalidCharacterError(",", "before next token").withPos(`[`, ""), ""}, + {0, zeroValue, newInvalidCharacterError(",", "before next token").withPos(`[`, ""), ""}, }, wantOffset: len(`[`), }, { @@ -724,8 +725,8 @@ var decoderErrorTestdata = []struct { calls: []decoderMethodCall{ {'[', ArrayStart, nil, ""}, {'"', String(""), nil, ""}, - {0, zeroToken, errMissingComma.withOffset(len64(`[""`)), ""}, - {0, zeroValue, errMissingComma.withOffset(len64(`[""`)), ""}, + {0, zeroToken, E(errMissingComma).withPos(`[""`, ""), ""}, + {0, zeroValue, E(errMissingComma).withPos(`[""`, ""), ""}, }, wantOffset: len(`[""`), }, { @@ -734,10 +735,247 @@ var decoderErrorTestdata = []struct { calls: []decoderMethodCall{ {'[', ArrayStart, nil, ""}, {'"', String(""), nil, ""}, - {0, zeroToken, io.ErrUnexpectedEOF, ""}, - {0, zeroValue, io.ErrUnexpectedEOF, ""}, + {0, zeroToken, E(io.ErrUnexpectedEOF).withPos(`["",`, ""), ""}, + {0, zeroValue, E(io.ErrUnexpectedEOF).withPos(`["",`, ""), ""}, }, wantOffset: len(`[""`), +}, { + name: jsontest.Name("ErrorPosition"), + in: ` "a` + "\xff" + `0" `, + calls: []decoderMethodCall{ + {'"', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` "a`, ""), ""}, + {'"', zeroToken, E(jsonwire.ErrInvalidUTF8).withPos(` "a`, ""), ""}, + }, +}, { + name: jsontest.Name("ErrorPosition/0"), + in: ` [ "a` + "\xff" + `1" ] `, + calls: []decoderMethodCall{ + {'[', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ "a`, "/0"), ""}, + {'[', ArrayStart, nil, ""}, + {'"', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ "a`, "/0"), ""}, + {'"', zeroToken, E(jsonwire.ErrInvalidUTF8).withPos(` [ "a`, "/0"), ""}, + }, + wantOffset: len(` [`), +}, { + name: jsontest.Name("ErrorPosition/1"), + in: ` [ "a1" , "b` + "\xff" + `1" ] `, + calls: []decoderMethodCall{ + {'[', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ "a1" , "b`, "/1"), ""}, + {'[', ArrayStart, nil, ""}, + {'"', String("a1"), nil, ""}, + {'"', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ "a1" , "b`, "/1"), ""}, + {'"', zeroToken, E(jsonwire.ErrInvalidUTF8).withPos(` [ "a1" , "b`, "/1"), ""}, + }, + wantOffset: len(` [ "a1"`), +}, { + name: jsontest.Name("ErrorPosition/0/0"), + in: ` [ [ "a` + "\xff" + `2" ] ] `, + calls: []decoderMethodCall{ + {'[', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ [ "a`, "/0/0"), ""}, + {'[', ArrayStart, nil, ""}, + {'[', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ [ "a`, "/0/0"), ""}, + {'[', ArrayStart, nil, "/0"}, + {'"', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ [ "a`, "/0/0"), ""}, + {'"', zeroToken, E(jsonwire.ErrInvalidUTF8).withPos(` [ [ "a`, "/0/0"), ""}, + }, + wantOffset: len(` [ [`), +}, { + name: jsontest.Name("ErrorPosition/1/0"), + in: ` [ "a1" , [ "a` + "\xff" + `2" ] ] `, + calls: []decoderMethodCall{ + {'[', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ "a1" , [ "a`, "/1/0"), ""}, + {'[', ArrayStart, nil, ""}, + {'"', String("a1"), nil, "/0"}, + {'[', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ "a1" , [ "a`, "/1/0"), "/0"}, + {'[', ArrayStart, nil, "/1"}, + {'"', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ "a1" , [ "a`, "/1/0"), "/1"}, + {'"', zeroToken, E(jsonwire.ErrInvalidUTF8).withPos(` [ "a1" , [ "a`, "/1/0"), "/1"}, + }, + wantOffset: len(` [ "a1" , [`), +}, { + name: jsontest.Name("ErrorPosition/0/1"), + in: ` [ [ "a2" , "b` + "\xff" + `2" ] ] `, + calls: []decoderMethodCall{ + {'[', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ [ "a2" , "b`, "/0/1"), ""}, + {'[', ArrayStart, nil, ""}, + {'[', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ [ "a2" , "b`, "/0/1"), ""}, + {'[', ArrayStart, nil, "/0"}, + {'"', String("a2"), nil, "/0/0"}, + {'"', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ [ "a2" , "b`, "/0/1"), "/0/0"}, + {'"', zeroToken, E(jsonwire.ErrInvalidUTF8).withPos(` [ [ "a2" , "b`, "/0/1"), "/0/0"}, + }, + wantOffset: len(` [ [ "a2"`), +}, { + name: jsontest.Name("ErrorPosition/1/1"), + in: ` [ "a1" , [ "a2" , "b` + "\xff" + `2" ] ] `, + calls: []decoderMethodCall{ + {'[', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ "a1" , [ "a2" , "b`, "/1/1"), ""}, + {'[', ArrayStart, nil, ""}, + {'"', String("a1"), nil, "/0"}, + {'[', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ "a1" , [ "a2" , "b`, "/1/1"), ""}, + {'[', ArrayStart, nil, "/1"}, + {'"', String("a2"), nil, "/1/0"}, + {'"', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ "a1" , [ "a2" , "b`, "/1/1"), "/1/0"}, + {'"', zeroToken, E(jsonwire.ErrInvalidUTF8).withPos(` [ "a1" , [ "a2" , "b`, "/1/1"), "/1/0"}, + }, + wantOffset: len(` [ "a1" , [ "a2"`), +}, { + name: jsontest.Name("ErrorPosition/a1-"), + in: ` { "a` + "\xff" + `1" : "b1" } `, + calls: []decoderMethodCall{ + {'{', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` { "a`, ""), ""}, + {'{', ObjectStart, nil, ""}, + {'"', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` { "a`, ""), ""}, + {'"', zeroToken, E(jsonwire.ErrInvalidUTF8).withPos(` { "a`, ""), ""}, + }, + wantOffset: len(` {`), +}, { + name: jsontest.Name("ErrorPosition/a1"), + in: ` { "a1" : "b` + "\xff" + `1" } `, + calls: []decoderMethodCall{ + {'{', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : "b`, "/a1"), ""}, + {'{', ObjectStart, nil, ""}, + {'"', String("a1"), nil, "/a1"}, + {'"', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : "b`, "/a1"), ""}, + {'"', zeroToken, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : "b`, "/a1"), ""}, + }, + wantOffset: len(` { "a1"`), +}, { + name: jsontest.Name("ErrorPosition/c1-"), + in: ` { "a1" : "b1" , "c` + "\xff" + `1" : "d1" } `, + calls: []decoderMethodCall{ + {'{', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : "b1" , "c`, ""), ""}, + {'{', ObjectStart, nil, ""}, + {'"', String("a1"), nil, "/a1"}, + {'"', String("b1"), nil, "/a1"}, + {'"', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : "b1" : "c`, ""), "/a1"}, + {'"', zeroToken, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : "b1" : "c`, ""), "/a1"}, + }, + wantOffset: len(` { "a1" : "b1"`), +}, { + name: jsontest.Name("ErrorPosition/c1"), + in: ` { "a1" : "b1" , "c1" : "d` + "\xff" + `1" } `, + calls: []decoderMethodCall{ + {'{', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : "b1" , "c1" : "d`, "/c1"), ""}, + {'{', ObjectStart, nil, ""}, + {'"', String("a1"), nil, "/a1"}, + {'"', String("b1"), nil, "/a1"}, + {'"', String("c1"), nil, "/c1"}, + {'"', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : "b1" : "c1" : "d`, "/c1"), "/c1"}, + {'"', zeroToken, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : "b1" : "c1" : "d`, "/c1"), "/c1"}, + }, + wantOffset: len(` { "a1" : "b1" , "c1"`), +}, { + name: jsontest.Name("ErrorPosition/a1/a2-"), + in: ` { "a1" : { "a` + "\xff" + `2" : "b2" } } `, + calls: []decoderMethodCall{ + {'{', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : { "a`, "/a1"), ""}, + {'{', ObjectStart, nil, ""}, + {'"', String("a1"), nil, "/a1"}, + {'{', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : { "a`, "/a1"), ""}, + {'{', ObjectStart, nil, "/a1"}, + {'"', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : { "a`, "/a1"), "/a1"}, + {'"', zeroToken, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : { "a`, "/a1"), "/a1"}, + }, + wantOffset: len(` { "a1" : {`), +}, { + name: jsontest.Name("ErrorPosition/a1/a2"), + in: ` { "a1" : { "a2" : "b` + "\xff" + `2" } } `, + calls: []decoderMethodCall{ + {'{', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : { "a2" : "b`, "/a1/a2"), ""}, + {'{', ObjectStart, nil, ""}, + {'"', String("a1"), nil, "/a1"}, + {'{', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : { "a2" : "b`, "/a1/a2"), ""}, + {'{', ObjectStart, nil, "/a1"}, + {'"', String("a2"), nil, "/a1/a2"}, + {'"', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : { "a2" : "b`, "/a1/a2"), "/a1/a2"}, + {'"', zeroToken, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : { "a2" : "b`, "/a1/a2"), "/a1/a2"}, + }, + wantOffset: len(` { "a1" : { "a2"`), +}, { + name: jsontest.Name("ErrorPosition/a1/c2-"), + in: ` { "a1" : { "a2" : "b2" , "c` + "\xff" + `2" : "d2" } } `, + calls: []decoderMethodCall{ + {'{', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : { "a2" : "b2" , "c`, "/a1"), ""}, + {'{', ObjectStart, nil, ""}, + {'"', String("a1"), nil, "/a1"}, + {'{', ObjectStart, nil, "/a1"}, + {'"', String("a2"), nil, "/a1/a2"}, + {'"', String("b2"), nil, "/a1/a2"}, + {'"', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : { "a2" : "b2" , "c`, "/a1"), "/a1/a2"}, + {'"', zeroToken, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : { "a2" : "b2" , "c`, "/a1"), "/a1/a2"}, + }, + wantOffset: len(` { "a1" : { "a2" : "b2"`), +}, { + name: jsontest.Name("ErrorPosition/a1/c2"), + in: ` { "a1" : { "a2" : "b2" , "c2" : "d` + "\xff" + `2" } } `, + calls: []decoderMethodCall{ + {'{', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : { "a2" : "b2" , "c2" : "d`, "/a1/c2"), ""}, + {'{', ObjectStart, nil, ""}, + {'"', String("a1"), nil, "/a1"}, + {'{', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : { "a2" : "b2" , "c2" : "d`, "/a1/c2"), ""}, + {'{', ObjectStart, nil, ""}, + {'"', String("a2"), nil, "/a1/a2"}, + {'"', String("b2"), nil, "/a1/a2"}, + {'"', String("c2"), nil, "/a1/c2"}, + {'"', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : { "a2" : "b2" , "c2" : "d`, "/a1/c2"), "/a1/c2"}, + {'"', zeroToken, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : { "a2" : "b2" , "c2" : "d`, "/a1/c2"), "/a1/c2"}, + }, + wantOffset: len(` { "a1" : { "a2" : "b2" , "c2"`), +}, { + name: jsontest.Name("ErrorPosition/1/a2"), + in: ` [ "a1" , { "a2" : "b` + "\xff" + `2" } ] `, + calls: []decoderMethodCall{ + {'[', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ "a1" , { "a2" : "b`, "/1/a2"), ""}, + {'[', ArrayStart, nil, ""}, + {'"', String("a1"), nil, "/0"}, + {'{', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ "a1" , { "a2" : "b`, "/1/a2"), ""}, + {'{', ObjectStart, nil, "/1"}, + {'"', String("a2"), nil, "/1/a2"}, + {'"', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ "a1" , { "a2" : "b`, "/1/a2"), "/1/a2"}, + {'"', zeroToken, E(jsonwire.ErrInvalidUTF8).withPos(` [ "a1" , { "a2" : "b`, "/1/a2"), "/1/a2"}, + }, + wantOffset: len(` [ "a1" , { "a2"`), +}, { + name: jsontest.Name("ErrorPosition/c1/1"), + in: ` { "a1" : "b1" , "c1" : [ "a2" , "b` + "\xff" + `2" ] } `, + calls: []decoderMethodCall{ + {'{', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : "b1" , "c1" : [ "a2" , "b`, "/c1/1"), ""}, + {'{', ObjectStart, nil, ""}, + {'"', String("a1"), nil, "/a1"}, + {'"', String("b1"), nil, "/a1"}, + {'"', String("c1"), nil, "/c1"}, + {'[', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : "b1" , "c1" : [ "a2" , "b`, "/c1/1"), ""}, + {'[', ArrayStart, nil, "/c1"}, + {'"', String("a2"), nil, "/c1/0"}, + {'"', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : "b1" , "c1" : [ "a2" , "b`, "/c1/1"), "/c1/0"}, + {'"', zeroToken, E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : "b1" , "c1" : [ "a2" , "b`, "/c1/1"), "/c1/0"}, + }, + wantOffset: len(` { "a1" : "b1" , "c1" : [ "a2"`), +}, { + name: jsontest.Name("ErrorPosition/0/a1/1/c3/1"), + in: ` [ { "a1" : [ "a2" , { "a3" : "b3" , "c3" : [ "a4" , "b` + "\xff" + `4" ] } ] } ] `, + calls: []decoderMethodCall{ + {'[', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ { "a1" : [ "a2" , { "a3" : "b3" , "c3" : [ "a4" , "b`, "/0/a1/1/c3/1"), ""}, + {'[', ArrayStart, nil, ""}, + {'{', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ { "a1" : [ "a2" , { "a3" : "b3" , "c3" : [ "a4" , "b`, "/0/a1/1/c3/1"), ""}, + {'{', ObjectStart, nil, "/0"}, + {'"', String("a1"), nil, "/0/a1"}, + {'[', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ { "a1" : [ "a2" , { "a3" : "b3" , "c3" : [ "a4" , "b`, "/0/a1/1/c3/1"), ""}, + {'[', ArrayStart, nil, ""}, + {'"', String("a2"), nil, "/0/a1/0"}, + {'{', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ { "a1" : [ "a2" , { "a3" : "b3" , "c3" : [ "a4" , "b`, "/0/a1/1/c3/1"), ""}, + {'{', ObjectStart, nil, "/0/a1/1"}, + {'"', String("a3"), nil, "/0/a1/1/a3"}, + {'"', String("b3"), nil, "/0/a1/1/a3"}, + {'"', String("c3"), nil, "/0/a1/1/c3"}, + {'[', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ { "a1" : [ "a2" , { "a3" : "b3" , "c3" : [ "a4" , "b`, "/0/a1/1/c3/1"), ""}, + {'[', ArrayStart, nil, "/0/a1/1/c3"}, + {'"', String("a4"), nil, "/0/a1/1/c3/0"}, + {'"', zeroValue, E(jsonwire.ErrInvalidUTF8).withPos(` [ { "a1" : [ "a2" , { "a3" : "b3" , "c3" : [ "a4" , "b`, "/0/a1/1/c3/1"), "/0/a1/1/c3/0"}, + {'"', zeroToken, E(jsonwire.ErrInvalidUTF8).withPos(` [ { "a1" : [ "a2" , { "a3" : "b3" , "c3" : [ "a4" , "b`, "/0/a1/1/c3/1"), "/0/a1/1/c3/0"}, + }, + wantOffset: len(` [ { "a1" : [ "a2" , { "a3" : "b3" , "c3" : [ "a4"`), }} // TestDecoderErrors test that Decoder errors occur when we expect and @@ -773,7 +1011,7 @@ func testDecoderErrors(t *testing.T, where jsontest.CasePos, opts []Options, in t.Fatalf("%s: %d: Decoder.ReadValue = %s, want %s", where, i, gotOut, wantOut) } } - if !reflect.DeepEqual(gotErr, call.wantErr) { + if !equalError(gotErr, call.wantErr) { t.Fatalf("%s: %d: error mismatch:\ngot %v\nwant %v", where, i, gotErr, call.wantErr) } if call.wantPointer != "" { @@ -806,7 +1044,7 @@ func TestBufferDecoder(t *testing.T) { bb.WriteByte(' ') // not allowed to write to the buffer while reading } want := &ioError{action: "read", err: errBufferWriteAfterNext} - if !reflect.DeepEqual(err, want) { + if !equalError(err, want) { t.Fatalf("error mismatch: got %v, want %v", err, want) } } @@ -950,8 +1188,8 @@ func TestPeekableDecoder(t *testing.T) { PeekKind{0}, WriteString{"] "}, - ReadValue{0, io.ErrUnexpectedEOF}, // previous error from PeekKind is cached once - ReadValue{0, newInvalidCharacterError("]", "at start of value").withOffset(2)}, + ReadValue{0, E(io.ErrUnexpectedEOF).withPos("[ ", "")}, // previous error from PeekKind is cached once + ReadValue{0, newInvalidCharacterError("]", "at start of value").withPos("[ ", "/0")}, ReadToken{']', nil}, WriteString{"[ "}, @@ -966,7 +1204,7 @@ func TestPeekableDecoder(t *testing.T) { PeekKind{0}, WriteString{"fal"}, PeekKind{'f'}, - ReadValue{0, io.ErrUnexpectedEOF}, + ReadValue{0, E(io.ErrUnexpectedEOF).withPos("[ ] [ null , fal", "/1")}, WriteString{"se "}, ReadValue{'f', nil}, @@ -974,7 +1212,7 @@ func TestPeekableDecoder(t *testing.T) { WriteString{" , "}, PeekKind{0}, WriteString{` "" `}, - ReadValue{0, io.ErrUnexpectedEOF}, // previous error from PeekKind is cached once + ReadValue{0, E(io.ErrUnexpectedEOF).withPos("[ ] [ null , false , ", "")}, // previous error from PeekKind is cached once ReadValue{'"', nil}, WriteString{" , 0"}, @@ -1001,13 +1239,13 @@ func TestPeekableDecoder(t *testing.T) { case ReadToken: gotTok, gotErr := d.ReadToken() gotKind := gotTok.Kind() - if gotKind != op.wantKind || !reflect.DeepEqual(gotErr, op.wantErr) { + if gotKind != op.wantKind || !equalError(gotErr, op.wantErr) { t.Fatalf("%d: Decoder.ReadToken() = (%v, %v), want (%v, %v)", i, gotKind, gotErr, op.wantKind, op.wantErr) } case ReadValue: gotVal, gotErr := d.ReadValue() gotKind := gotVal.Kind() - if gotKind != op.wantKind || !reflect.DeepEqual(gotErr, op.wantErr) { + if gotKind != op.wantKind || !equalError(gotErr, op.wantErr) { t.Fatalf("%d: Decoder.ReadValue() = (%v, %v), want (%v, %v)", i, gotKind, gotErr, op.wantKind, op.wantErr) } case WriteString: diff --git a/jsontext/encode.go b/jsontext/encode.go index 9c26a3c..aa341ee 100644 --- a/jsontext/encode.go +++ b/jsontext/encode.go @@ -209,17 +209,7 @@ func (e *encoderState) Flush() error { return nil } - -// injectSyntacticErrorWithPosition wraps a SyntacticError with the position, -// otherwise it returns the error as is. -// It takes a position relative to the start of the start of e.buf. -func (e *encodeBuffer) injectSyntacticErrorWithPosition(err error, pos int) error { - if serr, ok := err.(*SyntacticError); ok { - return serr.withOffset(e.baseOffset + int64(pos)) - } - return err -} - +func (d *encodeBuffer) offsetAt(pos int) int64 { return d.baseOffset + int64(pos) } func (e *encodeBuffer) previousOffsetEnd() int64 { return e.baseOffset + int64(len(e.Buf)) } func (e *encodeBuffer) unflushedBuffer() []byte { return e.Buf } @@ -374,7 +364,7 @@ func (e *encoderState) WriteToken(t Token) error { break } if e.Tokens.Last.isActiveNamespace() && !e.Namespaces.Last().insertQuoted(b[pos:], false) { - err = newDuplicateNameError(b[pos:]) + err = wrapWithObjectName(ErrDuplicateName, b[pos:]) break } } @@ -411,10 +401,10 @@ func (e *encoderState) WriteToken(t Token) error { b = append(b, ']') err = e.Tokens.popArray() default: - err = &SyntacticError{str: "invalid json.Token"} + err = errInvalidToken } if err != nil { - return e.injectSyntacticErrorWithPosition(err, pos) + return wrapSyntacticError(e, err, pos, +1) } // Finish off the buffer and store it back into e. @@ -464,7 +454,7 @@ func (e *encoderState) AppendRaw(k Kind, safeASCII bool, appendFn func([]byte) ( b, err = jsonwire.AppendQuote(b[:pos], string(b2), &e.Flags) e.unusedCache = b2[:0] if err != nil { - return e.injectSyntacticErrorWithPosition(err, pos) + return wrapSyntacticError(e, err, pos, +1) } } @@ -472,24 +462,24 @@ func (e *encoderState) AppendRaw(k Kind, safeASCII bool, appendFn func([]byte) ( if e.Tokens.Last.NeedObjectName() { if !e.Flags.Get(jsonflags.AllowDuplicateNames) { if !e.Tokens.Last.isValidNamespace() { - return errInvalidNamespace + return wrapSyntacticError(e, err, pos, +1) } if e.Tokens.Last.isActiveNamespace() && !e.Namespaces.Last().insertQuoted(b[pos:], isVerbatim) { - err := newDuplicateNameError(b[pos:]) - return e.injectSyntacticErrorWithPosition(err, pos) + err = wrapWithObjectName(ErrDuplicateName, b[pos:]) + return wrapSyntacticError(e, err, pos, +1) } } e.Names.ReplaceLastQuotedOffset(pos) // only replace if insertQuoted succeeds } if err := e.Tokens.appendString(); err != nil { - return e.injectSyntacticErrorWithPosition(err, pos) + return wrapSyntacticError(e, err, pos, +1) } case '0': if b, err = appendFn(b); err != nil { return err } if err := e.Tokens.appendNumber(); err != nil { - return e.injectSyntacticErrorWithPosition(err, pos) + return wrapSyntacticError(e, err, pos, +1) } default: panic("BUG: invalid kind") @@ -536,13 +526,13 @@ func (e *encoderState) WriteValue(v Value) error { n += jsonwire.ConsumeWhitespace(v[n:]) b, m, err := e.reformatValue(b, v[n:], e.Tokens.Depth()) if err != nil { - return e.injectSyntacticErrorWithPosition(err, pos+n+m) + return wrapSyntacticError(e, err, pos+n+m, +1) } n += m n += jsonwire.ConsumeWhitespace(v[n:]) if len(v) > n { - err = newInvalidCharacterError(v[n:], "after top-level value") - return e.injectSyntacticErrorWithPosition(err, pos+n) + err = jsonwire.NewInvalidCharacterError(v[n:], "after top-level value") + return wrapSyntacticError(e, err, pos+n, 0) } // Append the kind to the state machine. @@ -557,7 +547,7 @@ func (e *encoderState) WriteValue(v Value) error { break } if e.Tokens.Last.isActiveNamespace() && !e.Namespaces.Last().insertQuoted(b[pos:], false) { - err = newDuplicateNameError(b[pos:]) + err = wrapWithObjectName(ErrDuplicateName, b[pos:]) break } } @@ -582,7 +572,7 @@ func (e *encoderState) WriteValue(v Value) error { } } if err != nil { - return e.injectSyntacticErrorWithPosition(err, pos) + return wrapSyntacticError(e, err, pos, +1) } // Finish off the buffer and store it back into e. @@ -668,7 +658,7 @@ func (e *encoderState) reformatValue(dst []byte, src Value, depth int) ([]byte, case '[': return e.reformatArray(dst, src, depth) default: - return dst, 0, newInvalidCharacterError(src, "at start of value") + return dst, 0, jsonwire.NewInvalidCharacterError(src, "at start of value") } } @@ -727,17 +717,18 @@ func (e *encoderState) reformatObject(dst []byte, src Value, depth int) ([]byte, } quotedName := src[n : n+m] if !e.Flags.Get(jsonflags.AllowDuplicateNames) && !names.insertQuoted(quotedName, isVerbatim) { - return dst, n, newDuplicateNameError(quotedName) + return dst, n, wrapWithObjectName(ErrDuplicateName, quotedName) } n += m // Append colon. n += jsonwire.ConsumeWhitespace(src[n:]) if uint(len(src)) <= uint(n) { - return dst, n, io.ErrUnexpectedEOF + return dst, n, wrapWithObjectName(io.ErrUnexpectedEOF, quotedName) } if src[n] != ':' { - return dst, n, newInvalidCharacterError(src[n:], "after object name (expecting ':')") + err = jsonwire.NewInvalidCharacterError(src[n:], "after object name (expecting ':')") + return dst, n, wrapWithObjectName(err, quotedName) } dst = append(dst, ':') n += len(":") @@ -748,11 +739,11 @@ func (e *encoderState) reformatObject(dst []byte, src Value, depth int) ([]byte, // Append object value. n += jsonwire.ConsumeWhitespace(src[n:]) if uint(len(src)) <= uint(n) { - return dst, n, io.ErrUnexpectedEOF + return dst, n, wrapWithObjectName(io.ErrUnexpectedEOF, quotedName) } dst, m, err = e.reformatValue(dst, src[n:], depth) if err != nil { - return dst, n + m, err + return dst, n + m, wrapWithObjectName(err, quotedName) } n += m @@ -777,7 +768,7 @@ func (e *encoderState) reformatObject(dst []byte, src Value, depth int) ([]byte, n += len("}") return dst, n, nil default: - return dst, n, newInvalidCharacterError(src[n:], "after object value (expecting ',' or '}')") + return dst, n, jsonwire.NewInvalidCharacterError(src[n:], "after object value (expecting ',' or '}')") } } } @@ -806,6 +797,7 @@ func (e *encoderState) reformatArray(dst []byte, src Value, depth int) ([]byte, return dst, n, nil } + var idx int64 var err error depth++ for { @@ -822,7 +814,7 @@ func (e *encoderState) reformatArray(dst []byte, src Value, depth int) ([]byte, var m int dst, m, err = e.reformatValue(dst, src[n:], depth) if err != nil { - return dst, n + m, err + return dst, n + m, wrapWithArrayIndex(err, idx) } n += m @@ -838,6 +830,7 @@ func (e *encoderState) reformatArray(dst []byte, src Value, depth int) ([]byte, dst = append(dst, ' ') } n += len(",") + idx++ continue case ']': if e.Flags.Get(jsonflags.Multiline) { @@ -847,7 +840,7 @@ func (e *encoderState) reformatArray(dst []byte, src Value, depth int) ([]byte, n += len("]") return dst, n, nil default: - return dst, n, newInvalidCharacterError(src[n:], "after array value (expecting ',' or ']')") + return dst, n, jsonwire.NewInvalidCharacterError(src[n:], "after array value (expecting ',' or ']')") } } } @@ -925,6 +918,10 @@ func (e *Encoder) StackIndex(i int) (Kind, int64) { // Object names are only present if [AllowDuplicateNames] is false, otherwise // object members are represented using their index within the object. func (e *Encoder) StackPointer() Pointer { - e.s.Names.copyQuotedBuffer(e.s.Buf) - return Pointer(e.s.appendStackPointer(nil)) + return Pointer(e.s.AppendStackPointer(nil, -1)) +} + +func (e *encoderState) AppendStackPointer(b []byte, where int) []byte { + e.Names.copyQuotedBuffer(e.Buf) + return e.state.appendStackPointer(b, where) } diff --git a/jsontext/encode_test.go b/jsontext/encode_test.go index 72d55f5..372a1f3 100644 --- a/jsontext/encode_test.go +++ b/jsontext/encode_test.go @@ -9,7 +9,6 @@ import ( "errors" "io" "path" - "reflect" "slices" "testing" @@ -148,7 +147,7 @@ var encoderErrorTestdata = []struct { }{{ name: jsontest.Name("InvalidToken"), calls: []encoderMethodCall{ - {zeroToken, &SyntacticError{str: "invalid json.Token"}, ""}, + {zeroToken, E(errInvalidToken), ""}, }, }, { name: jsontest.Name("InvalidValue"), @@ -158,52 +157,52 @@ var encoderErrorTestdata = []struct { }, { name: jsontest.Name("InvalidValue/DoubleZero"), calls: []encoderMethodCall{ - {Value(`00`), newInvalidCharacterError("0", "after top-level value").withOffset(len64(`0`)), ""}, + {Value(`00`), newInvalidCharacterError("0", "after top-level value").withPos(`0`, ""), ""}, }, }, { name: jsontest.Name("TruncatedValue"), calls: []encoderMethodCall{ - {zeroValue, io.ErrUnexpectedEOF, ""}, + {zeroValue, E(io.ErrUnexpectedEOF).withPos("", ""), ""}, }, }, { name: jsontest.Name("TruncatedNull"), calls: []encoderMethodCall{ - {Value(`nul`), io.ErrUnexpectedEOF, ""}, + {Value(`nul`), E(io.ErrUnexpectedEOF).withPos("nul", ""), ""}, }, }, { name: jsontest.Name("InvalidNull"), calls: []encoderMethodCall{ - {Value(`nulL`), newInvalidCharacterError("L", "within literal null (expecting 'l')").withOffset(len64(`nul`)), ""}, + {Value(`nulL`), newInvalidCharacterError("L", "within literal null (expecting 'l')").withPos(`nul`, ""), ""}, }, }, { name: jsontest.Name("TruncatedFalse"), calls: []encoderMethodCall{ - {Value(`fals`), io.ErrUnexpectedEOF, ""}, + {Value(`fals`), E(io.ErrUnexpectedEOF).withPos("fals", ""), ""}, }, }, { name: jsontest.Name("InvalidFalse"), calls: []encoderMethodCall{ - {Value(`falsE`), newInvalidCharacterError("E", "within literal false (expecting 'e')").withOffset(len64(`fals`)), ""}, + {Value(`falsE`), newInvalidCharacterError("E", "within literal false (expecting 'e')").withPos(`fals`, ""), ""}, }, }, { name: jsontest.Name("TruncatedTrue"), calls: []encoderMethodCall{ - {Value(`tru`), io.ErrUnexpectedEOF, ""}, + {Value(`tru`), E(io.ErrUnexpectedEOF).withPos(`tru`, ""), ""}, }, }, { name: jsontest.Name("InvalidTrue"), calls: []encoderMethodCall{ - {Value(`truE`), newInvalidCharacterError("E", "within literal true (expecting 'e')").withOffset(len64(`tru`)), ""}, + {Value(`truE`), newInvalidCharacterError("E", "within literal true (expecting 'e')").withPos(`tru`, ""), ""}, }, }, { name: jsontest.Name("TruncatedString"), calls: []encoderMethodCall{ - {Value(`"star`), io.ErrUnexpectedEOF, ""}, + {Value(`"star`), E(io.ErrUnexpectedEOF).withPos(`"star`, ""), ""}, }, }, { name: jsontest.Name("InvalidString"), calls: []encoderMethodCall{ - {Value(`"ok` + "\x00"), newInvalidCharacterError("\x00", `within string (expecting non-control character)`).withOffset(len64(`"ok`)), ""}, + {Value(`"ok` + "\x00"), newInvalidCharacterError("\x00", `within string (expecting non-control character)`).withPos(`"ok`, ""), ""}, }, }, { name: jsontest.Name("ValidString/AllowInvalidUTF8/Token"), @@ -223,100 +222,106 @@ var encoderErrorTestdata = []struct { name: jsontest.Name("InvalidString/RejectInvalidUTF8"), opts: []Options{AllowInvalidUTF8(false)}, calls: []encoderMethodCall{ - {String("living\xde\xad\xbe\xef"), errInvalidUTF8, ""}, - {Value("\"living\xde\xad\xbe\xef\""), errInvalidUTF8.withOffset(len64("\"living\xde\xad")), ""}, + {String("living\xde\xad\xbe\xef"), E(jsonwire.ErrInvalidUTF8), ""}, + {Value("\"living\xde\xad\xbe\xef\""), E(jsonwire.ErrInvalidUTF8).withPos("\"living\xde\xad", ""), ""}, + {ObjectStart, nil, ""}, + {String("name"), nil, ""}, + {ArrayStart, nil, ""}, + {String("living\xde\xad\xbe\xef"), E(jsonwire.ErrInvalidUTF8).withPos(`{"name":[`, "/name/0"), ""}, + {Value("\"living\xde\xad\xbe\xef\""), E(jsonwire.ErrInvalidUTF8).withPos("{\"name\":[\"living\xde\xad", "/name/0"), ""}, }, + wantOut: `{"name":[`, }, { name: jsontest.Name("TruncatedNumber"), calls: []encoderMethodCall{ - {Value(`0.`), io.ErrUnexpectedEOF, ""}, + {Value(`0.`), E(io.ErrUnexpectedEOF).withPos("0", ""), ""}, }, }, { name: jsontest.Name("InvalidNumber"), calls: []encoderMethodCall{ - {Value(`0.e`), newInvalidCharacterError("e", "within number (expecting digit)").withOffset(len64(`0.`)), ""}, + {Value(`0.e`), newInvalidCharacterError("e", "within number (expecting digit)").withPos(`0.`, ""), ""}, }, }, { name: jsontest.Name("TruncatedObject/AfterStart"), calls: []encoderMethodCall{ - {Value(`{`), io.ErrUnexpectedEOF, ""}, + {Value(`{`), E(io.ErrUnexpectedEOF).withPos("{", ""), ""}, }, }, { name: jsontest.Name("TruncatedObject/AfterName"), calls: []encoderMethodCall{ - {Value(`{"0"`), io.ErrUnexpectedEOF, ""}, + {Value(`{"X"`), E(io.ErrUnexpectedEOF).withPos(`{"X"`, "/X"), ""}, }, }, { name: jsontest.Name("TruncatedObject/AfterColon"), calls: []encoderMethodCall{ - {Value(`{"0":`), io.ErrUnexpectedEOF, ""}, + {Value(`{"X":`), E(io.ErrUnexpectedEOF).withPos(`{"X":`, "/X"), ""}, }, }, { name: jsontest.Name("TruncatedObject/AfterValue"), calls: []encoderMethodCall{ - {Value(`{"0":0`), io.ErrUnexpectedEOF, ""}, + {Value(`{"0":0`), E(io.ErrUnexpectedEOF).withPos(`{"0":0`, ""), ""}, }, }, { name: jsontest.Name("TruncatedObject/AfterComma"), calls: []encoderMethodCall{ - {Value(`{"0":0,`), io.ErrUnexpectedEOF, ""}, + {Value(`{"0":0,`), E(io.ErrUnexpectedEOF).withPos(`{"0":0,`, ""), ""}, }, }, { name: jsontest.Name("InvalidObject/MissingColon"), calls: []encoderMethodCall{ - {Value(` { "fizz" "buzz" } `), newInvalidCharacterError("\"", "after object name (expecting ':')").withOffset(len64(` { "fizz" `)), ""}, - {Value(` { "fizz" , "buzz" } `), newInvalidCharacterError(",", "after object name (expecting ':')").withOffset(len64(` { "fizz" `)), ""}, + {Value(` { "fizz" "buzz" } `), newInvalidCharacterError("\"", "after object name (expecting ':')").withPos(` { "fizz" `, "/fizz"), ""}, + {Value(` { "fizz" , "buzz" } `), newInvalidCharacterError(",", "after object name (expecting ':')").withPos(` { "fizz" `, "/fizz"), ""}, }, }, { name: jsontest.Name("InvalidObject/MissingComma"), calls: []encoderMethodCall{ - {Value(` { "fizz" : "buzz" "gazz" } `), newInvalidCharacterError("\"", "after object value (expecting ',' or '}')").withOffset(len64(` { "fizz" : "buzz" `)), ""}, - {Value(` { "fizz" : "buzz" : "gazz" } `), newInvalidCharacterError(":", "after object value (expecting ',' or '}')").withOffset(len64(` { "fizz" : "buzz" `)), ""}, + {Value(` { "fizz" : "buzz" "gazz" } `), newInvalidCharacterError("\"", "after object value (expecting ',' or '}')").withPos(` { "fizz" : "buzz" `, ""), ""}, + {Value(` { "fizz" : "buzz" : "gazz" } `), newInvalidCharacterError(":", "after object value (expecting ',' or '}')").withPos(` { "fizz" : "buzz" `, ""), ""}, }, }, { name: jsontest.Name("InvalidObject/ExtraComma"), calls: []encoderMethodCall{ - {Value(` { , } `), newInvalidCharacterError(",", `at start of string (expecting '"')`).withOffset(len64(` { `)), ""}, - {Value(` { "fizz" : "buzz" , } `), newInvalidCharacterError("}", `at start of string (expecting '"')`).withOffset(len64(` { "fizz" : "buzz" , `)), ""}, + {Value(` { , } `), newInvalidCharacterError(",", `at start of string (expecting '"')`).withPos(` { `, ""), ""}, + {Value(` { "fizz" : "buzz" , } `), newInvalidCharacterError("}", `at start of string (expecting '"')`).withPos(` { "fizz" : "buzz" , `, ""), ""}, }, }, { name: jsontest.Name("InvalidObject/InvalidName"), calls: []encoderMethodCall{ - {Value(`{ null }`), newInvalidCharacterError("n", `at start of string (expecting '"')`).withOffset(len64(`{ `)), ""}, - {Value(`{ false }`), newInvalidCharacterError("f", `at start of string (expecting '"')`).withOffset(len64(`{ `)), ""}, - {Value(`{ true }`), newInvalidCharacterError("t", `at start of string (expecting '"')`).withOffset(len64(`{ `)), ""}, - {Value(`{ 0 }`), newInvalidCharacterError("0", `at start of string (expecting '"')`).withOffset(len64(`{ `)), ""}, - {Value(`{ {} }`), newInvalidCharacterError("{", `at start of string (expecting '"')`).withOffset(len64(`{ `)), ""}, - {Value(`{ [] }`), newInvalidCharacterError("[", `at start of string (expecting '"')`).withOffset(len64(`{ `)), ""}, + {Value(`{ null }`), newInvalidCharacterError("n", `at start of string (expecting '"')`).withPos(`{ `, ""), ""}, + {Value(`{ false }`), newInvalidCharacterError("f", `at start of string (expecting '"')`).withPos(`{ `, ""), ""}, + {Value(`{ true }`), newInvalidCharacterError("t", `at start of string (expecting '"')`).withPos(`{ `, ""), ""}, + {Value(`{ 0 }`), newInvalidCharacterError("0", `at start of string (expecting '"')`).withPos(`{ `, ""), ""}, + {Value(`{ {} }`), newInvalidCharacterError("{", `at start of string (expecting '"')`).withPos(`{ `, ""), ""}, + {Value(`{ [] }`), newInvalidCharacterError("[", `at start of string (expecting '"')`).withPos(`{ `, ""), ""}, {ObjectStart, nil, ""}, - {Null, errMissingName.withOffset(len64(`{`)), ""}, - {Value(`null`), errMissingName.withOffset(len64(`{`)), ""}, - {False, errMissingName.withOffset(len64(`{`)), ""}, - {Value(`false`), errMissingName.withOffset(len64(`{`)), ""}, - {True, errMissingName.withOffset(len64(`{`)), ""}, - {Value(`true`), errMissingName.withOffset(len64(`{`)), ""}, - {Uint(0), errMissingName.withOffset(len64(`{`)), ""}, - {Value(`0`), errMissingName.withOffset(len64(`{`)), ""}, - {ObjectStart, errMissingName.withOffset(len64(`{`)), ""}, - {Value(`{}`), errMissingName.withOffset(len64(`{`)), ""}, - {ArrayStart, errMissingName.withOffset(len64(`{`)), ""}, - {Value(`[]`), errMissingName.withOffset(len64(`{`)), ""}, + {Null, E(ErrNonStringName).withPos(`{`, ""), ""}, + {Value(`null`), E(ErrNonStringName).withPos(`{`, ""), ""}, + {False, E(ErrNonStringName).withPos(`{`, ""), ""}, + {Value(`false`), E(ErrNonStringName).withPos(`{`, ""), ""}, + {True, E(ErrNonStringName).withPos(`{`, ""), ""}, + {Value(`true`), E(ErrNonStringName).withPos(`{`, ""), ""}, + {Uint(0), E(ErrNonStringName).withPos(`{`, ""), ""}, + {Value(`0`), E(ErrNonStringName).withPos(`{`, ""), ""}, + {ObjectStart, E(ErrNonStringName).withPos(`{`, ""), ""}, + {Value(`{}`), E(ErrNonStringName).withPos(`{`, ""), ""}, + {ArrayStart, E(ErrNonStringName).withPos(`{`, ""), ""}, + {Value(`[]`), E(ErrNonStringName).withPos(`{`, ""), ""}, {ObjectEnd, nil, ""}, }, wantOut: "{}\n", }, { name: jsontest.Name("InvalidObject/InvalidValue"), calls: []encoderMethodCall{ - {Value(`{ "0": x }`), newInvalidCharacterError("x", `at start of value`).withOffset(len64(`{ "0": `)), ""}, + {Value(`{ "0": x }`), newInvalidCharacterError("x", `at start of value`).withPos(`{ "0": `, "/0"), ""}, }, }, { name: jsontest.Name("InvalidObject/MismatchingDelim"), calls: []encoderMethodCall{ - {Value(` { ] `), newInvalidCharacterError("]", `at start of string (expecting '"')`).withOffset(len64(` { `)), ""}, - {Value(` { "0":0 ] `), newInvalidCharacterError("]", `after object value (expecting ',' or '}')`).withOffset(len64(` { "0":0 `)), ""}, + {Value(` { ] `), newInvalidCharacterError("]", `at start of string (expecting '"')`).withPos(` { `, ""), ""}, + {Value(` { "0":0 ] `), newInvalidCharacterError("]", `after object value (expecting ',' or '}')`).withPos(` { "0":0 `, ""), ""}, {ObjectStart, nil, ""}, - {ArrayEnd, errMismatchDelim.withOffset(len64(`{`)), ""}, - {Value(`]`), newInvalidCharacterError("]", "at start of value").withOffset(len64(`{`)), ""}, + {ArrayEnd, E(errMismatchDelim).withPos(`{`, ""), ""}, + {Value(`]`), newInvalidCharacterError("]", "at start of value").withPos(`{`, ""), ""}, {ObjectEnd, nil, ""}, }, wantOut: "{}\n", @@ -349,49 +354,49 @@ var encoderErrorTestdata = []struct { name: jsontest.Name("InvalidObject/DuplicateNames"), calls: []encoderMethodCall{ {ObjectStart, nil, ""}, - {String("0"), nil, ""}, + {String("X"), nil, ""}, {ObjectStart, nil, ""}, {ObjectEnd, nil, ""}, - {String("0"), newDuplicateNameError(`"0"`).withOffset(len64(`{"0":{},`)), "/0"}, - {Value(`"0"`), newDuplicateNameError(`"0"`).withOffset(len64(`{"0":{},`)), "/0"}, - {String("1"), nil, ""}, + {String("X"), E(ErrDuplicateName).withPos(`{"X":{},`, "/X"), "/X"}, + {Value(`"X"`), E(ErrDuplicateName).withPos(`{"X":{},`, "/X"), "/X"}, + {String("Y"), nil, ""}, {ObjectStart, nil, ""}, {ObjectEnd, nil, ""}, - {String("0"), newDuplicateNameError(`"0"`).withOffset(len64(`{"0":{},"1":{},`)), "/1"}, - {Value(`"0"`), newDuplicateNameError(`"0"`).withOffset(len64(`{"0":{},"1":{},`)), "/1"}, - {String("1"), newDuplicateNameError(`"1"`).withOffset(len64(`{"0":{},"1":{},`)), "/1"}, - {Value(`"1"`), newDuplicateNameError(`"1"`).withOffset(len64(`{"0":{},"1":{},`)), "/1"}, + {String("X"), E(ErrDuplicateName).withPos(`{"X":{},"Y":{},`, "/X"), "/Y"}, + {Value(`"X"`), E(ErrDuplicateName).withPos(`{"X":{},"Y":{},`, "/X"), "/Y"}, + {String("Y"), E(ErrDuplicateName).withPos(`{"X":{},"Y":{},`, "/Y"), "/Y"}, + {Value(`"Y"`), E(ErrDuplicateName).withPos(`{"X":{},"Y":{},`, "/Y"), "/Y"}, {ObjectEnd, nil, ""}, - {Value(` { "0" : 0 , "1" : 1 , "0" : 0 } `), newDuplicateNameError(`"0"`).withOffset(len64(`{"0":{},"1":{}}` + "\n" + ` { "0" : 0 , "1" : 1 , `)), ""}, + {Value(` { "X" : 0 , "Y" : 1 , "X" : 0 } `), E(ErrDuplicateName).withPos(`{"X":{},"Y":{}}`+"\n"+` { "X" : 0 , "Y" : 1 , `, "/X"), ""}, }, - wantOut: `{"0":{},"1":{}}` + "\n", + wantOut: `{"X":{},"Y":{}}` + "\n", }, { name: jsontest.Name("TruncatedArray/AfterStart"), calls: []encoderMethodCall{ - {Value(`[`), io.ErrUnexpectedEOF, ""}, + {Value(`[`), E(io.ErrUnexpectedEOF).withPos(`[`, ""), ""}, }, }, { name: jsontest.Name("TruncatedArray/AfterValue"), calls: []encoderMethodCall{ - {Value(`[0`), io.ErrUnexpectedEOF, ""}, + {Value(`[0`), E(io.ErrUnexpectedEOF).withPos(`[0`, ""), ""}, }, }, { name: jsontest.Name("TruncatedArray/AfterComma"), calls: []encoderMethodCall{ - {Value(`[0,`), io.ErrUnexpectedEOF, ""}, + {Value(`[0,`), E(io.ErrUnexpectedEOF).withPos(`[0,`, ""), ""}, }, }, { name: jsontest.Name("TruncatedArray/MissingComma"), calls: []encoderMethodCall{ - {Value(` [ "fizz" "buzz" ] `), newInvalidCharacterError("\"", "after array value (expecting ',' or ']')").withOffset(len64(` [ "fizz" `)), ""}, + {Value(` [ "fizz" "buzz" ] `), newInvalidCharacterError("\"", "after array value (expecting ',' or ']')").withPos(` [ "fizz" `, ""), ""}, }, }, { name: jsontest.Name("InvalidArray/MismatchingDelim"), calls: []encoderMethodCall{ - {Value(` [ } `), newInvalidCharacterError("}", `at start of value`).withOffset(len64(` [ `)), ""}, + {Value(` [ } `), newInvalidCharacterError("}", `at start of value`).withPos(` [ `, "/0"), ""}, {ArrayStart, nil, ""}, - {ObjectEnd, errMismatchDelim.withOffset(len64(`[`)), ""}, - {Value(`}`), newInvalidCharacterError("}", "at start of value").withOffset(len64(`[`)), ""}, + {ObjectEnd, E(errMismatchDelim).withPos(`[`, "/0"), ""}, + {Value(`}`), newInvalidCharacterError("}", "at start of value").withPos(`[`, "/0"), ""}, {ArrayEnd, nil, ""}, }, wantOut: "[]\n", @@ -425,6 +430,225 @@ var encoderErrorTestdata = []struct { opts: []Options{SpaceAfterComma(false), Multiline(true)}, calls: []encoderMethodCall{{Value(`["fizz","buzz"]`), nil, ""}}, wantOut: "[\n\t\"fizz\",\n\t\"buzz\"\n]\n", +}, { + name: jsontest.Name("ErrorPosition"), + calls: []encoderMethodCall{ + {Value(` "a` + "\xff" + `0" `), E(jsonwire.ErrInvalidUTF8).withPos(` "a`, ""), ""}, + {String(`a` + "\xff" + `0`), E(jsonwire.ErrInvalidUTF8).withPos(``, ""), ""}, + }, +}, { + name: jsontest.Name("ErrorPosition/0"), + calls: []encoderMethodCall{ + {Value(` [ "a` + "\xff" + `1" ] `), E(jsonwire.ErrInvalidUTF8).withPos(` [ "a`, "/0"), ""}, + {ArrayStart, nil, ""}, + {Value(` "a` + "\xff" + `1" `), E(jsonwire.ErrInvalidUTF8).withPos(`[ "a`, "/0"), ""}, + {String(`a` + "\xff" + `1`), E(jsonwire.ErrInvalidUTF8).withPos(`[`, "/0"), ""}, + }, + wantOut: `[`, +}, { + name: jsontest.Name("ErrorPosition/1"), + calls: []encoderMethodCall{ + {Value(` [ "a1" , "b` + "\xff" + `1" ] `), E(jsonwire.ErrInvalidUTF8).withPos(` [ "a1" , "b`, "/1"), ""}, + {ArrayStart, nil, ""}, + {String("a1"), nil, ""}, + {Value(` "b` + "\xff" + `1" `), E(jsonwire.ErrInvalidUTF8).withPos(`["a1", "b`, "/1"), ""}, + {String(`b` + "\xff" + `1`), E(jsonwire.ErrInvalidUTF8).withPos(`["a1",`, "/1"), ""}, + }, + wantOut: `["a1"`, +}, { + name: jsontest.Name("ErrorPosition/0/0"), + calls: []encoderMethodCall{ + {Value(` [ [ "a` + "\xff" + `2" ] ] `), E(jsonwire.ErrInvalidUTF8).withPos(` [ [ "a`, "/0/0"), ""}, + {ArrayStart, nil, ""}, + {Value(` [ "a` + "\xff" + `2" ] `), E(jsonwire.ErrInvalidUTF8).withPos(`[ [ "a`, "/0/0"), ""}, + {ArrayStart, nil, "/0"}, + {Value(` "a` + "\xff" + `2" `), E(jsonwire.ErrInvalidUTF8).withPos(`[[ "a`, "/0/0"), "/0"}, + {String(`a` + "\xff" + `2`), E(jsonwire.ErrInvalidUTF8).withPos(`[[`, "/0/0"), "/0"}, + }, + wantOut: `[[`, +}, { + name: jsontest.Name("ErrorPosition/1/0"), + calls: []encoderMethodCall{ + {Value(` [ "a1" , [ "a` + "\xff" + `2" ] ] `), E(jsonwire.ErrInvalidUTF8).withPos(` [ "a1" , [ "a`, "/1/0"), ""}, + {ArrayStart, nil, ""}, + {String("a1"), nil, "/0"}, + {Value(` [ "a` + "\xff" + `2" ] `), E(jsonwire.ErrInvalidUTF8).withPos(`["a1", [ "a`, "/1/0"), ""}, + {ArrayStart, nil, "/1"}, + {Value(` "a` + "\xff" + `2" `), E(jsonwire.ErrInvalidUTF8).withPos(`["a1",[ "a`, "/1/0"), "/1"}, + {String(`a` + "\xff" + `2`), E(jsonwire.ErrInvalidUTF8).withPos(`["a1",[`, "/1/0"), "/1"}, + }, + wantOut: `["a1",[`, +}, { + name: jsontest.Name("ErrorPosition/0/1"), + calls: []encoderMethodCall{ + {Value(` [ [ "a2" , "b` + "\xff" + `2" ] ] `), E(jsonwire.ErrInvalidUTF8).withPos(` [ [ "a2" , "b`, "/0/1"), ""}, + {ArrayStart, nil, ""}, + {Value(` [ "a2" , "b` + "\xff" + `2" ] `), E(jsonwire.ErrInvalidUTF8).withPos(`[ [ "a2" , "b`, "/0/1"), ""}, + {ArrayStart, nil, "/0"}, + {String("a2"), nil, "/0/0"}, + {Value(` "b` + "\xff" + `2" `), E(jsonwire.ErrInvalidUTF8).withPos(`[["a2", "b`, "/0/1"), "/0/0"}, + {String(`b` + "\xff" + `2`), E(jsonwire.ErrInvalidUTF8).withPos(`[["a2",`, "/0/1"), "/0/0"}, + }, + wantOut: `[["a2"`, +}, { + name: jsontest.Name("ErrorPosition/1/1"), + calls: []encoderMethodCall{ + {Value(` [ "a1" , [ "a2" , "b` + "\xff" + `2" ] ] `), E(jsonwire.ErrInvalidUTF8).withPos(` [ "a1" , [ "a2" , "b`, "/1/1"), ""}, + {ArrayStart, nil, ""}, + {String("a1"), nil, "/0"}, + {Value(` [ "a2" , "b` + "\xff" + `2" ] `), E(jsonwire.ErrInvalidUTF8).withPos(`["a1", [ "a2" , "b`, "/1/1"), ""}, + {ArrayStart, nil, "/1"}, + {String("a2"), nil, "/1/0"}, + {Value(` "b` + "\xff" + `2" `), E(jsonwire.ErrInvalidUTF8).withPos(`["a1",["a2", "b`, "/1/1"), "/1/0"}, + {String(`b` + "\xff" + `2`), E(jsonwire.ErrInvalidUTF8).withPos(`["a1",["a2",`, "/1/1"), "/1/0"}, + }, + wantOut: `["a1",["a2"`, +}, { + name: jsontest.Name("ErrorPosition/a1-"), + calls: []encoderMethodCall{ + {Value(` { "a` + "\xff" + `1" : "b1" } `), E(jsonwire.ErrInvalidUTF8).withPos(` { "a`, ""), ""}, + {ObjectStart, nil, ""}, + {Value(` "a` + "\xff" + `1" `), E(jsonwire.ErrInvalidUTF8).withPos(`{ "a`, ""), ""}, + {String(`a` + "\xff" + `1`), E(jsonwire.ErrInvalidUTF8).withPos(`{`, ""), ""}, + }, + wantOut: `{`, +}, { + name: jsontest.Name("ErrorPosition/a1"), + calls: []encoderMethodCall{ + {Value(` { "a1" : "b` + "\xff" + `1" } `), E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : "b`, "/a1"), ""}, + {ObjectStart, nil, ""}, + {String("a1"), nil, "/a1"}, + {Value(` "b` + "\xff" + `1" `), E(jsonwire.ErrInvalidUTF8).withPos(`{"a1": "b`, "/a1"), ""}, + {String(`b` + "\xff" + `1`), E(jsonwire.ErrInvalidUTF8).withPos(`{"a1":`, "/a1"), ""}, + }, + wantOut: `{"a1"`, +}, { + name: jsontest.Name("ErrorPosition/c1-"), + calls: []encoderMethodCall{ + {Value(` { "a1" : "b1" , "c` + "\xff" + `1" : "d1" } `), E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : "b1" , "c`, ""), ""}, + {ObjectStart, nil, ""}, + {String("a1"), nil, "/a1"}, + {String("b1"), nil, "/a1"}, + {Value(` "c` + "\xff" + `1" `), E(jsonwire.ErrInvalidUTF8).withPos(`{"a1":"b1": "c`, ""), "/a1"}, + {String(`c` + "\xff" + `1`), E(jsonwire.ErrInvalidUTF8).withPos(`{"a1":"b1":`, ""), "/a1"}, + }, + wantOut: `{"a1":"b1"`, +}, { + name: jsontest.Name("ErrorPosition/c1"), + calls: []encoderMethodCall{ + {Value(` { "a1" : "b1" , "c1" : "d` + "\xff" + `1" } `), E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : "b1" , "c1" : "d`, "/c1"), ""}, + {ObjectStart, nil, ""}, + {String("a1"), nil, "/a1"}, + {String("b1"), nil, "/a1"}, + {String("c1"), nil, "/c1"}, + {Value(` "d` + "\xff" + `1" `), E(jsonwire.ErrInvalidUTF8).withPos(`{"a1":"b1":"c1": "d`, "/c1"), "/c1"}, + {String(`d` + "\xff" + `1`), E(jsonwire.ErrInvalidUTF8).withPos(`{"a1":"b1":"c1":`, "/c1"), "/c1"}, + }, + wantOut: `{"a1":"b1","c1"`, +}, { + name: jsontest.Name("ErrorPosition/a1/a2-"), + calls: []encoderMethodCall{ + {Value(` { "a1" : { "a` + "\xff" + `2" : "b2" } } `), E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : { "a`, "/a1"), ""}, + {ObjectStart, nil, ""}, + {String("a1"), nil, "/a1"}, + {Value(` { "a` + "\xff" + `2" : "b2" } `), E(jsonwire.ErrInvalidUTF8).withPos(`{"a1": { "a`, "/a1"), ""}, + {ObjectStart, nil, "/a1"}, + {Value(` "a` + "\xff" + `2" `), E(jsonwire.ErrInvalidUTF8).withPos(`{"a1":{ "a`, "/a1"), "/a1"}, + {String(`a` + "\xff" + `2`), E(jsonwire.ErrInvalidUTF8).withPos(`{"a1":{`, "/a1"), "/a1"}, + }, + wantOut: `{"a1":{`, +}, { + name: jsontest.Name("ErrorPosition/a1/a2"), + calls: []encoderMethodCall{ + {Value(` { "a1" : { "a2" : "b` + "\xff" + `2" } } `), E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : { "a2" : "b`, "/a1/a2"), ""}, + {ObjectStart, nil, ""}, + {String("a1"), nil, "/a1"}, + {Value(` { "a2" : "b` + "\xff" + `2" } `), E(jsonwire.ErrInvalidUTF8).withPos(`{"a1": { "a2" : "b`, "/a1/a2"), ""}, + {ObjectStart, nil, "/a1"}, + {String("a2"), nil, "/a1/a2"}, + {Value(` "b` + "\xff" + `2" `), E(jsonwire.ErrInvalidUTF8).withPos(`{"a1":{"a2": "b`, "/a1/a2"), "/a1/a2"}, + {String(`b` + "\xff" + `2`), E(jsonwire.ErrInvalidUTF8).withPos(`{"a1":{"a2":`, "/a1/a2"), "/a1/a2"}, + }, + wantOut: `{"a1":{"a2"`, +}, { + name: jsontest.Name("ErrorPosition/a1/c2-"), + calls: []encoderMethodCall{ + {Value(` { "a1" : { "a2" : "b2" , "c` + "\xff" + `2" : "d2" } } `), E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : { "a2" : "b2" , "c`, "/a1"), ""}, + {ObjectStart, nil, ""}, + {String("a1"), nil, "/a1"}, + {ObjectStart, nil, "/a1"}, + {String("a2"), nil, "/a1/a2"}, + {String("b2"), nil, "/a1/a2"}, + {Value(` "c` + "\xff" + `2" `), E(jsonwire.ErrInvalidUTF8).withPos(`{"a1":{"a2":"b2", "c`, "/a1"), "/a1/a2"}, + {String(`c` + "\xff" + `2`), E(jsonwire.ErrInvalidUTF8).withPos(`{"a1":{"a2":"b2",`, "/a1"), "/a1/a2"}, + }, + wantOut: `{"a1":{"a2":"b2"`, +}, { + name: jsontest.Name("ErrorPosition/a1/c2"), + calls: []encoderMethodCall{ + {Value(` { "a1" : { "a2" : "b2" , "c2" : "d` + "\xff" + `2" } } `), E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : { "a2" : "b2" , "c2" : "d`, "/a1/c2"), ""}, + {ObjectStart, nil, ""}, + {String("a1"), nil, "/a1"}, + {Value(` { "a2" : "b2" , "c2" : "d` + "\xff" + `2" } `), E(jsonwire.ErrInvalidUTF8).withPos(`{"a1": { "a2" : "b2" , "c2" : "d`, "/a1/c2"), ""}, + {ObjectStart, nil, ""}, + {String("a2"), nil, "/a1/a2"}, + {String("b2"), nil, "/a1/a2"}, + {String("c2"), nil, "/a1/c2"}, + {Value(` "d` + "\xff" + `2" `), E(jsonwire.ErrInvalidUTF8).withPos(`{"a1":{"a2":"b2","c2": "d`, "/a1/c2"), "/a1/c2"}, + {String(`d` + "\xff" + `2`), E(jsonwire.ErrInvalidUTF8).withPos(`{"a1":{"a2":"b2","c2":`, "/a1/c2"), "/a1/c2"}, + }, + wantOut: `{"a1":{"a2":"b2","c2"`, +}, { + name: jsontest.Name("ErrorPosition/1/a2"), + calls: []encoderMethodCall{ + {Value(` [ "a1" , { "a2" : "b` + "\xff" + `2" } ] `), E(jsonwire.ErrInvalidUTF8).withPos(` [ "a1" , { "a2" : "b`, "/1/a2"), ""}, + {ArrayStart, nil, ""}, + {String("a1"), nil, "/0"}, + {Value(` { "a2" : "b` + "\xff" + `2" } `), E(jsonwire.ErrInvalidUTF8).withPos(`["a1", { "a2" : "b`, "/1/a2"), ""}, + {ObjectStart, nil, "/1"}, + {String("a2"), nil, "/1/a2"}, + {Value(` "b` + "\xff" + `2" `), E(jsonwire.ErrInvalidUTF8).withPos(`["a1",{"a2": "b`, "/1/a2"), "/1/a2"}, + {String(`b` + "\xff" + `2`), E(jsonwire.ErrInvalidUTF8).withPos(`["a1",{"a2":`, "/1/a2"), "/1/a2"}, + }, + wantOut: `["a1",{"a2"`, +}, { + name: jsontest.Name("ErrorPosition/c1/1"), + calls: []encoderMethodCall{ + {Value(` { "a1" : "b1" , "c1" : [ "a2" , "b` + "\xff" + `2" ] } `), E(jsonwire.ErrInvalidUTF8).withPos(` { "a1" : "b1" , "c1" : [ "a2" , "b`, "/c1/1"), ""}, + {ObjectStart, nil, ""}, + {String("a1"), nil, "/a1"}, + {String("b1"), nil, "/a1"}, + {String("c1"), nil, "/c1"}, + {Value(` [ "a2" , "b` + "\xff" + `2" ] `), E(jsonwire.ErrInvalidUTF8).withPos(`{"a1":"b1","c1": [ "a2" , "b`, "/c1/1"), ""}, + {ArrayStart, nil, "/c1"}, + {String("a2"), nil, "/c1/0"}, + {Value(` "b` + "\xff" + `2" `), E(jsonwire.ErrInvalidUTF8).withPos(`{"a1":"b1","c1":["a2", "b`, "/c1/1"), "/c1/0"}, + {String(`b` + "\xff" + `2`), E(jsonwire.ErrInvalidUTF8).withPos(`{"a1":"b1","c1":["a2",`, "/c1/1"), "/c1/0"}, + }, + wantOut: `{"a1":"b1","c1":["a2"`, +}, { + name: jsontest.Name("ErrorPosition/0/a1/1/c3/1"), + calls: []encoderMethodCall{ + {Value(` [ { "a1" : [ "a2" , { "a3" : "b3" , "c3" : [ "a4" , "b` + "\xff" + `4" ] } ] } ] `), E(jsonwire.ErrInvalidUTF8).withPos(` [ { "a1" : [ "a2" , { "a3" : "b3" , "c3" : [ "a4" , "b`, "/0/a1/1/c3/1"), ""}, + {ArrayStart, nil, ""}, + {Value(` { "a1" : [ "a2" , { "a3" : "b3" , "c3" : [ "a4" , "b` + "\xff" + `4" ] } ] } `), E(jsonwire.ErrInvalidUTF8).withPos(`[ { "a1" : [ "a2" , { "a3" : "b3" , "c3" : [ "a4" , "b`, "/0/a1/1/c3/1"), ""}, + {ObjectStart, nil, "/0"}, + {String("a1"), nil, "/0/a1"}, + {Value(` [ "a2" , { "a3" : "b3" , "c3" : [ "a4" , "b` + "\xff" + `4" ] } ] `), E(jsonwire.ErrInvalidUTF8).withPos(`[{"a1": [ "a2" , { "a3" : "b3" , "c3" : [ "a4" , "b`, "/0/a1/1/c3/1"), ""}, + {ArrayStart, nil, ""}, + {String("a2"), nil, "/0/a1/0"}, + {Value(` { "a3" : "b3" , "c3" : [ "a4" , "b` + "\xff" + `4" ] } `), E(jsonwire.ErrInvalidUTF8).withPos(`[{"a1":["a2", { "a3" : "b3" , "c3" : [ "a4" , "b`, "/0/a1/1/c3/1"), ""}, + {ObjectStart, nil, "/0/a1/1"}, + {String("a3"), nil, "/0/a1/1/a3"}, + {String("b3"), nil, "/0/a1/1/a3"}, + {String("c3"), nil, "/0/a1/1/c3"}, + {Value(` [ "a4" , "b` + "\xff" + `4" ] `), E(jsonwire.ErrInvalidUTF8).withPos(`[{"a1":["a2",{"a3":"b3","c3": [ "a4" , "b`, "/0/a1/1/c3/1"), ""}, + {ArrayStart, nil, "/0/a1/1/c3"}, + {String("a4"), nil, "/0/a1/1/c3/0"}, + {Value(` "b` + "\xff" + `4" `), E(jsonwire.ErrInvalidUTF8).withPos(`[{"a1":["a2",{"a3":"b3","c3":["a4", "b`, "/0/a1/1/c3/1"), "/0/a1/1/c3/0"}, + {String(`b` + "\xff" + `4`), E(jsonwire.ErrInvalidUTF8).withPos(`[{"a1":["a2",{"a3":"b3","c3":["a4",`, "/0/a1/1/c3/1"), "/0/a1/1/c3/0"}, + }, + wantOut: `[{"a1":["a2",{"a3":"b3","c3":["a4"`, }} // TestEncoderErrors test that Encoder errors occur when we expect and @@ -447,7 +671,7 @@ func testEncoderErrors(t *testing.T, where jsontest.CasePos, opts []Options, cal case Value: gotErr = enc.WriteValue(tokVal) } - if !reflect.DeepEqual(gotErr, call.wantErr) { + if !equalError(gotErr, call.wantErr) { t.Fatalf("%s: %d: error mismatch:\ngot %v\nwant %v", where, i, gotErr, call.wantErr) } if call.wantPointer != "" { diff --git a/jsontext/errors.go b/jsontext/errors.go index 2a5d078..6ad397f 100644 --- a/jsontext/errors.go +++ b/jsontext/errors.go @@ -5,6 +5,10 @@ package jsontext import ( + "bytes" + "io" + "strconv" + "github.com/go-json-experiment/json/internal/jsonwire" ) @@ -32,29 +36,138 @@ type SyntacticError struct { // ByteOffset indicates that an error occurred after this byte offset. ByteOffset int64 - str string + // JSONPointer indicates that an error occurred within this JSON value + // as indicated using the JSON Pointer notation (see RFC 6901). + JSONPointer Pointer + + // Err is the underlying error. + Err error +} + +// wrapSyntacticError wraps an error and annotates it with a precise location +// using the provided [encoderState] or [decoderState]. +// If err is an [ioError] or [io.EOF], then it is not wrapped. +// +// It takes a relative offset pos that can be resolved into +// an absolute offset using state.offsetAt. +// +// It takes a where that specify how the JSON pointer is derived. +// If where is 0 and the next token is an object value, +// then it the pointer implicitly upgraded to point at the next token. +// If the underlying error is a [pointerSuffixError], +// then the suffix is appended to the derived pointer. +func wrapSyntacticError(state interface { + offsetAt(pos int) int64 + needObjectValue() bool + AppendStackPointer(b []byte, where int) []byte +}, err error, pos, where int) error { + if _, ok := err.(*ioError); err == io.EOF || ok { + return err + } + offset := state.offsetAt(pos) + if where == 0 && state.needObjectValue() { + where = +1 + } + ptr := state.AppendStackPointer(nil, where) + if serr, ok := err.(*pointerSuffixError); ok { + ptr = serr.appendPointer(ptr) + err = serr.error + } + return &SyntacticError{ByteOffset: offset, JSONPointer: Pointer(ptr), Err: err} } func (e *SyntacticError) Error() string { - return errorPrefix + e.str + pointer := e.JSONPointer + offset := e.ByteOffset + b := []byte(errorPrefix) + if e.Err != nil { + b = append(b, e.Err.Error()...) + if e.Err == ErrDuplicateName { + b = strconv.AppendQuote(append(b, ' '), pointer.LastToken()) + pointer = pointer.Parent() + offset = 0 // not useful to print offset for duplicate names + } + } else { + b = append(b, "syntactic error"...) + } + if pointer != "" { + // TODO: Truncate excessively long pointers. + b = strconv.AppendQuote(append(b, " within "...), string(pointer)) + } + if offset > 0 { + b = strconv.AppendInt(append(b, " after offset "...), offset, 10) + } + return string(b) +} + +func (e *SyntacticError) Unwrap() error { + return e.Err } -func (e *SyntacticError) withOffset(pos int64) error { - return &SyntacticError{ByteOffset: pos, str: e.str} + +// pointerSuffixError represents a JSON pointer suffix to be appended +// to [SyntacticError.JSONPointer]. It is an internal error type +// used within this package and does not appear in the public API. +// +// This type is primarily used to annotate errors in Encoder.WriteValue +// and Decoder.ReadValue with precise positions. +// At the time WriteValue or ReadValue is called, a JSON pointer to the +// upcoming value can be constructed using the Encoder/Decoder state. +// However, tracking pointers within values during normal operation +// would incur a performance penalty in the error-free case. +// +// To provide precise error locations without this overhead, +// the error is wrapped with object names or array indices +// as the call stack is popped when an error occurs. +// Since this happens in reverse order, pointerSuffixError holds +// the pointer in reverse and is only later reversed when appending to +// the pointer prefix. +// +// For example, if the encoder is at "/alpha/bravo/charlie" +// and an error occurs in WriteValue at "/xray/yankee/zulu", then +// the final pointer should be "/alpha/bravo/charlie/xray/yankee/zulu". +// +// As pointerSuffixError is populated during the error return path, +// it first contains "/zulu", then "/zulu/yankee", +// and finally "/zulu/yankee/xray". +// These tokens are reversed and concatenated to "/alpha/bravo/charlie" +// to form the full pointer. +type pointerSuffixError struct { + error + + // reversePointer is a JSON pointer, but with each token in reverse order. + reversePointer []byte } -func newDuplicateNameError[Bytes ~[]byte | ~string](quoted Bytes) *SyntacticError { - return &SyntacticError{str: "duplicate name " + string(quoted) + " in object"} +// wrapWithObjectName wraps err with a JSON object name access, +// which must be a valid quoted JSON string. +func wrapWithObjectName(err error, quotedName []byte) error { + serr, _ := err.(*pointerSuffixError) + if serr == nil { + serr = &pointerSuffixError{error: err} + } + name := jsonwire.UnquoteMayCopy(quotedName, false) + serr.reversePointer = appendEscapePointerName(append(serr.reversePointer, '/'), name) + return serr } -func newInvalidCharacterError[Bytes ~[]byte | ~string](prefix Bytes, where string) *SyntacticError { - what := jsonwire.QuoteRune(prefix) - return &SyntacticError{str: "invalid character " + what + " " + where} +// wrapWithArrayIndex wraps err with a JSON array index access. +func wrapWithArrayIndex(err error, index int64) error { + serr, _ := err.(*pointerSuffixError) + if serr == nil { + serr = &pointerSuffixError{error: err} + } + serr.reversePointer = strconv.AppendUint(append(serr.reversePointer, '/'), uint64(index), 10) + return serr } -// TODO: Error types between "json", "jsontext", and "jsonwire" is a mess. -// Clean this up. -func init() { - // Inject behavior in "jsonwire" so that it can produce SyntacticError types. - jsonwire.NewError = func(s string) error { return &SyntacticError{str: s} } - jsonwire.ErrInvalidUTF8 = &SyntacticError{str: jsonwire.ErrInvalidUTF8.Error()} +// appendPointer appends the path encoded in e to the end of pointer. +func (e *pointerSuffixError) appendPointer(pointer []byte) []byte { + // Copy each token in reversePointer to the end of pointer in reverse order. + // Double reversal means that the appended suffix is now in forward order. + bi, bo := e.reversePointer, pointer + for len(bi) > 0 { + i := bytes.LastIndexByte(bi, '/') + bi, bo = bi[:i], append(bo, bi[i:]...) + } + return bo } diff --git a/jsontext/export.go b/jsontext/export.go index 06b3335..e47a80b 100644 --- a/jsontext/export.go +++ b/jsontext/export.go @@ -68,16 +68,3 @@ func (export) GetStreamingDecoder(r io.Reader, o ...Options) *Decoder { func (export) PutStreamingDecoder(d *Decoder) { putStreamingDecoder(d) } - -func (export) NewDuplicateNameError(quoted []byte, pos int64) error { - return newDuplicateNameError(quoted).withOffset(pos) -} -func (export) NewInvalidCharacterError(prefix, where string, pos int64) error { - return newInvalidCharacterError(prefix, where).withOffset(pos) -} -func (export) NewMissingNameError(pos int64) error { - return errMissingName.withOffset(pos) -} -func (export) NewInvalidUTF8Error(pos int64) error { - return errInvalidUTF8.withOffset(pos) -} diff --git a/jsontext/fuzz_test.go b/jsontext/fuzz_test.go index cfe6fca..9b17a2e 100644 --- a/jsontext/fuzz_test.go +++ b/jsontext/fuzz_test.go @@ -9,7 +9,7 @@ import ( "errors" "io" "math/rand" - "reflect" + "slices" "testing" "github.com/go-json-experiment/json/internal/jsontest" @@ -118,7 +118,7 @@ func FuzzResumableDecoder(f *testing.F) { decWant := NewDecoder(bytes.NewReader(b)) gotTok, gotErr := decGot.ReadToken() wantTok, wantErr := decWant.ReadToken() - if gotTok.String() != wantTok.String() || !reflect.DeepEqual(gotErr, wantErr) { + if gotTok.String() != wantTok.String() || !equalError(gotErr, wantErr) { t.Errorf("Decoder.ReadToken = (%v, %v), want (%v, %v)", gotTok, gotErr, wantTok, wantErr) } }) @@ -127,7 +127,7 @@ func FuzzResumableDecoder(f *testing.F) { decWant := NewDecoder(bytes.NewReader(b)) gotVal, gotErr := decGot.ReadValue() wantVal, wantErr := decWant.ReadValue() - if !reflect.DeepEqual(gotVal, wantVal) || !reflect.DeepEqual(gotErr, wantErr) { + if !slices.Equal(gotVal, wantVal) || !equalError(gotErr, wantErr) { t.Errorf("Decoder.ReadValue = (%s, %v), want (%s, %v)", gotVal, gotErr, wantVal, wantErr) } }) diff --git a/jsontext/quote.go b/jsontext/quote.go index 27f846d..d65839d 100644 --- a/jsontext/quote.go +++ b/jsontext/quote.go @@ -9,15 +9,17 @@ import ( "github.com/go-json-experiment/json/internal/jsonwire" ) -var errInvalidUTF8 = &SyntacticError{str: "invalid UTF-8 within string"} - // AppendQuote appends a double-quoted JSON string literal representing src // to dst and returns the extended buffer. // It uses the minimal string representation per RFC 8785, section 3.2.2.2. // Invalid UTF-8 bytes are replaced with the Unicode replacement character // and an error is returned at the end indicating the presence of invalid UTF-8. func AppendQuote[Bytes ~[]byte | ~string](dst []byte, src Bytes) ([]byte, error) { - return jsonwire.AppendQuote(dst, src, &jsonflags.Flags{}) + dst, err := jsonwire.AppendQuote(dst, src, &jsonflags.Flags{}) + if err != nil { + err = &SyntacticError{Err: err} + } + return dst, err } // AppendUnquote appends the decoded interpretation of src as a @@ -27,5 +29,9 @@ func AppendQuote[Bytes ~[]byte | ~string](dst []byte, src Bytes) ([]byte, error) // and an error is returned at the end indicating the presence of invalid UTF-8. // Any trailing bytes after the JSON string literal results in an error. func AppendUnquote[Bytes ~[]byte | ~string](dst []byte, src Bytes) ([]byte, error) { - return jsonwire.AppendUnquote(dst, src) + dst, err := jsonwire.AppendUnquote(dst, src) + if err != nil { + err = &SyntacticError{Err: err} + } + return dst, err } diff --git a/jsontext/state.go b/jsontext/state.go index e2f4d47..6d509d2 100644 --- a/jsontext/state.go +++ b/jsontext/state.go @@ -5,6 +5,7 @@ package jsontext import ( + "errors" "iter" "math" "strconv" @@ -14,14 +15,36 @@ import ( ) var ( - errMissingName = &SyntacticError{str: "missing string for object name"} - errMissingColon = &SyntacticError{str: "missing character ':' after object name"} - errMissingValue = &SyntacticError{str: "missing value after object name"} - errMissingComma = &SyntacticError{str: "missing character ',' after object or array value"} - errMismatchDelim = &SyntacticError{str: "mismatching structural token for object or array"} - errMaxDepth = &SyntacticError{str: "exceeded max depth"} - - errInvalidNamespace = &SyntacticError{str: "object namespace is in an invalid state"} + // ErrDuplicateName indicates that a JSON token could not be + // encoded or decoded because it results in a duplicate JSON object name. + // This error is wrapped within a [SyntacticError] when produced. + // + // The name of a duplicate JSON object member can be extracted as: + // + // err := ... + // var serr jsontext.SyntacticError + // if errors.As(err, &serr) && serr.Err == jsontext.ErrDuplicateName { + // ptr := serr.JSONPointer // JSON pointer to duplicate name + // name := ptr.LastToken() // duplicate name itself + // ... + // } + // + // This error is only returned if [AllowDuplicateNames] is false. + ErrDuplicateName = errors.New("duplicate object name") + + // ErrNonStringName indicates that a JSON token could not be + // encoded or decoded because it is not a string, + // as required for JSON object names according to RFC 8259, section 4. + // This error is wrapped within a [SyntacticError] when produced. + ErrNonStringName = errors.New("object name must be a string") + + errMissingColon = errors.New("missing character ':' after object name") + errMissingValue = errors.New("missing value after object name") + errMissingComma = errors.New("missing character ',' after object or array value") + errMismatchDelim = errors.New("mismatching structural token for object or array") + errMaxDepth = errors.New("exceeded max depth") + + errInvalidNamespace = errors.New("object namespace is in an invalid state") ) // Per RFC 8259, section 9, implementations may enforce a maximum depth. @@ -44,6 +67,12 @@ type state struct { Namespaces objectNamespaceStack } +// needObjectValue reports whether the next token should be an object value. +// This method is used by [wrapSyntacticError]. +func (s *state) needObjectValue() bool { + return s.Tokens.Last.needObjectValue() +} + func (s *state) reset() { s.Tokens.reset() s.Names.reset() @@ -58,6 +87,24 @@ func (s *state) reset() { // they both point to the exact same value. type Pointer string +// Parent strips off the last token and returns the remaining pointer. +// The parent of an empty p is an empty string. +func (p Pointer) Parent() Pointer { + return p[:max(strings.LastIndexByte(string(p), '/'), 0)] +} + +// LastToken returns the last token in the pointer. +// The last token of an empty p is an empty string. +func (p Pointer) LastToken() string { + last := p[max(strings.LastIndexByte(string(p), '/'), 0):] + return unescapePointerToken(strings.TrimPrefix(string(last), "/")) +} + +// AppendToken appends a token to the end of p and returns the full pointer. +func (p Pointer) AppendToken(tok string) Pointer { + return p + "/" + Pointer(appendEscapePointerName(nil, []byte(string([]rune(tok))))) +} + // Tokens returns an iterator over the reference tokens in the JSON pointer, // starting from the first token until the last token (unless stopped early). // @@ -71,47 +118,67 @@ func (p Pointer) Tokens() iter.Seq[string] { for len(p) > 0 { p = Pointer(strings.TrimPrefix(string(p), "/")) i := min(uint(strings.IndexByte(string(p), '/')), uint(len(p))) - token := string(p)[:i] - p = p[i:] - if strings.Contains(token, "~") { - // Per RFC 6901, section 3, unescape '~' and '/' characters. - token = strings.ReplaceAll(token, "~1", "/") - token = strings.ReplaceAll(token, "~0", "~") - } - if !yield(token) { + if !yield(unescapePointerToken(string(p)[:i])) { return } + p = p[i:] } } } +func unescapePointerToken(token string) string { + if strings.Contains(token, "~") { + // Per RFC 6901, section 3, unescape '~' and '/' characters. + token = strings.ReplaceAll(token, "~1", "/") + token = strings.ReplaceAll(token, "~0", "~") + } + return token +} + // appendStackPointer appends a JSON Pointer (RFC 6901) to the current value. // +// If where is -1, then it points to the previously processed token. +// If where is 0, then it points to the parent JSON object or array. +// If where is +1, then it points to the next expected token, +// assuming that it continues the current JSON object or array. +// As a special case, if the next token is a JSON object name, +// then it points to the parent JSON object. +// // Invariant: Must call s.names.copyQuotedBuffer beforehand. -func (s state) appendStackPointer(b []byte) []byte { +func (s state) appendStackPointer(b []byte, where int) []byte { var objectDepth int for i := 1; i < s.Tokens.Depth(); i++ { e := s.Tokens.index(i) - if e.Length() == 0 { - break // empty object or array + arrayDelta := -1 // by default point to previous array element + if isLast := i == s.Tokens.Depth()-1; isLast { + switch { + case where < 0 && e.Length() == 0 || where == 0 || where > 0 && e.NeedObjectName(): + return b + case where > 0 && e.isArray(): + arrayDelta = 0 // point to next array element + } } - b = append(b, '/') switch { case e.isObject(): - for _, c := range s.Names.getUnquoted(objectDepth) { - // Per RFC 6901, section 3, escape '~' and '/' characters. - switch c { - case '~': - b = append(b, "~0"...) - case '/': - b = append(b, "~1"...) - default: - b = append(b, c) - } - } + b = appendEscapePointerName(append(b, '/'), s.Names.getUnquoted(objectDepth)) objectDepth++ case e.isArray(): - b = strconv.AppendUint(b, uint64(e.Length()-1), 10) + b = strconv.AppendUint(append(b, '/'), uint64(e.Length()+int64(arrayDelta)), 10) + } + } + return b +} + +func appendEscapePointerName(b, name []byte) []byte { + for _, c := range name { + // Per RFC 6901, section 3, escape '~' and '/' characters. + switch c { + case '~': + b = append(b, "~0"...) + case '/': + b = append(b, "~1"...) + default: + b = append(b, c) } } return b @@ -170,7 +237,7 @@ func (m stateMachine) DepthLength() (int, int64) { func (m *stateMachine) appendLiteral() error { switch { case m.Last.NeedObjectName(): - return errMissingName + return ErrNonStringName case !m.Last.isValidNamespace(): return errInvalidNamespace default: @@ -202,7 +269,7 @@ func (m *stateMachine) appendNumber() error { func (m *stateMachine) pushObject() error { switch { case m.Last.NeedObjectName(): - return errMissingName + return ErrNonStringName case !m.Last.isValidNamespace(): return errInvalidNamespace case len(m.Stack) == maxNestingDepth: @@ -237,7 +304,7 @@ func (m *stateMachine) popObject() error { func (m *stateMachine) pushArray() error { switch { case m.Last.NeedObjectName(): - return errMissingName + return ErrNonStringName case !m.Last.isValidNamespace(): return errInvalidNamespace case len(m.Stack) == maxNestingDepth: @@ -322,7 +389,7 @@ func (m stateMachine) checkDelim(delim byte, next Kind) error { case ',': return errMissingComma default: - return newInvalidCharacterError([]byte{delim}, "before next token") + return jsonwire.NewInvalidCharacterError([]byte{delim}, "before next token") } } diff --git a/jsontext/state_test.go b/jsontext/state_test.go index 349a124..f9ec121 100644 --- a/jsontext/state_test.go +++ b/jsontext/state_test.go @@ -6,29 +6,40 @@ package jsontext import ( "fmt" - "reflect" "slices" "strings" "testing" ) -func TestPointerTokens(t *testing.T) { +func TestPointer(t *testing.T) { tests := []struct { - in Pointer - want []string + in Pointer + wantParent Pointer + wantLast string + wantTokens []string }{ - {in: "", want: nil}, - {in: "a", want: []string{"a"}}, - {in: "~", want: []string{"~"}}, - {in: "/a", want: []string{"a"}}, - {in: "/foo/bar", want: []string{"foo", "bar"}}, - {in: "///", want: []string{"", "", ""}}, - {in: "/~0~1", want: []string{"~/"}}, + {"", "", "", nil}, + {"a", "", "a", []string{"a"}}, + {"~", "", "~", []string{"~"}}, + {"/a", "", "a", []string{"a"}}, + {"/foo/bar", "/foo", "bar", []string{"foo", "bar"}}, + {"///", "//", "", []string{"", "", ""}}, + {"/~0~1", "", "~/", []string{"~/"}}, } for _, tt := range tests { - got := slices.Collect(tt.in.Tokens()) - if !slices.Equal(got, tt.want) { - t.Errorf("Pointer(%q).Tokens = %q, want %q", tt.in, got, tt.want) + if got := tt.in.Parent(); got != tt.wantParent { + t.Errorf("Pointer(%q).Parent = %q, want %q", tt.in, got, tt.wantParent) + } + if got := tt.in.LastToken(); got != tt.wantLast { + t.Errorf("Pointer(%q).Last = %q, want %q", tt.in, got, tt.wantLast) + } + if strings.HasPrefix(string(tt.in), "/") { + if got := tt.in.Parent().AppendToken(tt.in.LastToken()); got != tt.in { + t.Errorf("Pointer(%q).Parent().AppendToken(LastToken()) = %q, want %q", tt.in, got, tt.in) + } + } + if got := slices.Collect(tt.in.Tokens()); !slices.Equal(got, tt.wantTokens) { + t.Errorf("Pointer(%q).Tokens = %q, want %q", tt.in, got, tt.wantTokens) } } } @@ -130,12 +141,12 @@ func TestStateMachine(t *testing.T) { appendTokens(`{`), // Appending any kind other than string for object name is an error. - appendToken{'n', errMissingName}, - appendToken{'f', errMissingName}, - appendToken{'t', errMissingName}, - appendToken{'0', errMissingName}, - appendToken{'{', errMissingName}, - appendToken{'[', errMissingName}, + appendToken{'n', ErrNonStringName}, + appendToken{'f', ErrNonStringName}, + appendToken{'t', ErrNonStringName}, + appendToken{'0', ErrNonStringName}, + appendToken{'{', ErrNonStringName}, + appendToken{'[', ErrNonStringName}, appendTokens(`"`), // Appending '}' without first appending any value is an error. @@ -189,7 +200,7 @@ func TestStateMachine(t *testing.T) { } case appendToken: got := state.append(op.kind) - if !reflect.DeepEqual(got, op.want) { + if !equalError(got, op.want) { t.Fatalf("%s: append('%c') = %v, want %v", sequence, op.kind, got, op.want) } if got == nil { diff --git a/jsontext/token.go b/jsontext/token.go index 689093d..69e909e 100644 --- a/jsontext/token.go +++ b/jsontext/token.go @@ -6,6 +6,7 @@ package jsontext import ( "bytes" + "errors" "math" "strconv" @@ -24,6 +25,8 @@ const ( invalidTokenPanic = "invalid json.Token; it has been voided by a subsequent json.Decoder call" ) +var errInvalidToken = errors.New("invalid jsontext.Token") + // Token represents a lexical JSON token, which may be one of the following: // - a JSON literal (i.e., null, true, or false) // - a JSON string (e.g., "hello, world!") diff --git a/jsontext/value_test.go b/jsontext/value_test.go index 87223e7..a1348fe 100644 --- a/jsontext/value_test.go +++ b/jsontext/value_test.go @@ -6,11 +6,11 @@ package jsontext import ( "io" - "reflect" "strings" "testing" "github.com/go-json-experiment/json/internal/jsontest" + "github.com/go-json-experiment/json/internal/jsonwire" ) type valueTestdataEntry struct { @@ -107,13 +107,13 @@ var valueTestdata = append(func() (out []valueTestdataEntry) { in: ` "living` + "\xde\xad\xbe\xef" + `\ufffd�" `, wantValid: false, // uses RFC 7493 as the definition; which validates UTF-8 wantCompacted: `"living` + "\xde\xad\xbe\xef" + `\ufffd�"`, - wantCanonicalizeErr: errInvalidUTF8.withOffset(len64(` "living` + "\xde\xad")), + wantCanonicalizeErr: E(jsonwire.ErrInvalidUTF8).withPos(` "living`+"\xde\xad", ""), }, { name: jsontest.Name("InvalidUTF8/SurrogateHalf"), in: `"\ud800"`, wantValid: false, // uses RFC 7493 as the definition; which validates UTF-8 wantCompacted: `"\ud800"`, - wantCanonicalizeErr: &SyntacticError{str: "invalid surrogate pair `\\ud800\"` within string", ByteOffset: len64(`"`)}, + wantCanonicalizeErr: newInvalidEscapeSequenceError(`\ud800"`).withPos(`"`, ""), }, { name: jsontest.Name("UppercaseEscaped"), in: `"\u000B"`, @@ -130,15 +130,15 @@ var valueTestdata = append(func() (out []valueTestdataEntry) { "1": 1, "0": 0 }`, - wantCanonicalizeErr: newDuplicateNameError(`"0"`).withOffset(len64(` { "0" : 0 , "1" : 1 , `)), + wantCanonicalizeErr: E(ErrDuplicateName).withPos(` { "0" : 0 , "1" : 1 , `, "/0"), }, { name: jsontest.Name("Whitespace"), in: " \n\r\t", wantValid: false, wantCompacted: " \n\r\t", - wantCompactErr: io.ErrUnexpectedEOF, - wantIndentErr: io.ErrUnexpectedEOF, - wantCanonicalizeErr: io.ErrUnexpectedEOF, + wantCompactErr: E(io.ErrUnexpectedEOF).withPos(" \n\r\t", ""), + wantIndentErr: E(io.ErrUnexpectedEOF).withPos(" \n\r\t", ""), + wantCanonicalizeErr: E(io.ErrUnexpectedEOF).withPos(" \n\r\t", ""), }}...) func TestValueMethods(t *testing.T) { @@ -171,7 +171,7 @@ func TestValueMethods(t *testing.T) { if string(gotCompacted) != td.wantCompacted { t.Errorf("%s: Value.Compact = %s, want %s", td.name.Where, gotCompacted, td.wantCompacted) } - if !reflect.DeepEqual(gotCompactErr, td.wantCompactErr) { + if !equalError(gotCompactErr, td.wantCompactErr) { t.Errorf("%s: Value.Compact error mismatch:\ngot %v\nwant %v", td.name.Where, gotCompactErr, td.wantCompactErr) } @@ -180,7 +180,7 @@ func TestValueMethods(t *testing.T) { if string(gotIndented) != td.wantIndented { t.Errorf("%s: Value.Indent = %s, want %s", td.name.Where, gotIndented, td.wantIndented) } - if !reflect.DeepEqual(gotIndentErr, td.wantIndentErr) { + if !equalError(gotIndentErr, td.wantIndentErr) { t.Errorf("%s: Value.Indent error mismatch:\ngot %v\nwant %v", td.name.Where, gotIndentErr, td.wantIndentErr) } @@ -189,7 +189,7 @@ func TestValueMethods(t *testing.T) { if string(gotCanonicalized) != td.wantCanonicalized { t.Errorf("%s: Value.Canonicalize = %s, want %s", td.name.Where, gotCanonicalized, td.wantCanonicalized) } - if !reflect.DeepEqual(gotCanonicalizeErr, td.wantCanonicalizeErr) { + if !equalError(gotCanonicalizeErr, td.wantCanonicalizeErr) { t.Errorf("%s: Value.Canonicalize error mismatch:\ngot %v\nwant %v", td.name.Where, gotCanonicalizeErr, td.wantCanonicalizeErr) } })