From bafcbf6312fa7f670bcda0bb0c716103dc7ceeb6 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Sat, 7 Dec 2024 15:54:14 -0800 Subject: [PATCH] Add JSONPointer to SyntacticError Make SyntacticError precise about the position of the error by also recording the JSON pointer. Other changes: * Make SyntacticError wrap an error. * Export ErrDuplicateName as a means to programmatically identify a duplicate object name error. * Export ErrNonStringName to indicate a syntax error when the expected JSON object name is not a string. * Always wrap io.ErrUnexpectedEOF in SyntacticError. * Avoid any dependency injection between jsontext and jsonwire. * Add convenience methods to jsontext.Pointer. --- arshal_any.go | 2 +- arshal_default.go | 6 +- arshal_inlined.go | 4 +- arshal_test.go | 135 ++++++++++--------- errors.go | 13 ++ internal/jsonwire/wire.go | 13 +- jsontext/coder_test.go | 59 +++++++-- jsontext/decode.go | 112 ++++++++-------- jsontext/decode_test.go | 263 +++++++++++++++++++------------------- jsontext/encode.go | 75 ++++++----- jsontext/encode_test.go | 138 ++++++++++---------- jsontext/errors.go | 148 ++++++++++++++++++--- jsontext/export.go | 13 -- jsontext/quote.go | 14 +- jsontext/state.go | 151 +++++++++++++++------- jsontext/state_test.go | 45 ++++--- jsontext/token.go | 3 + jsontext/value_test.go | 13 +- 18 files changed, 721 insertions(+), 486 deletions(-) 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..c8b4a69 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 within string") 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..dfa876c 100644 --- a/jsontext/coder_test.go +++ b/jsontext/coder_test.go @@ -16,10 +16,21 @@ 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 (e *SyntacticError) withPos(prefix string, pointer Pointer) *SyntacticError { + e.ByteOffset = int64(len(prefix)) + e.JSONPointer = pointer + return e } var ( @@ -622,21 +633,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 +678,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 +699,7 @@ func TestCoderMaxDepth(t *testing.T) { checkReadToken(t, '{', nil) checkReadToken(t, '"', nil) } - checkReadToken(t, 0, errMaxDepth.withOffset(maxNestingDepth*len64(`{"":`))) + checkReadToken(t, 0, wantErr) }) }) @@ -697,15 +718,20 @@ func TestCoderMaxDepth(t *testing.T) { } } + 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 +740,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 +772,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..16f197a 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,7 +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 { - d.peekPos, d.peekErr = -1, d.checkDelimBeforeIOError(delim, err) + d.peekPos, d.peekErr = -1, d.checkDelimBeforeIOError(delim, wrapSyntacticError(d, err, pos, 0)) return invalidKind } } @@ -334,7 +325,7 @@ func (d *decoderState) checkDelimBeforeIOError(delim byte, err error) error { // conservatively assume that is the next kind for validation. const next = Kind('"') if d.Tokens.needDelim(next) != delim { - err = d.checkDelim(delim, next) + return d.checkDelim(delim, next) } return err } @@ -344,7 +335,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 +399,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,7 +411,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 { - return Token{}, d.checkDelimBeforeIOError(delim, err) + return Token{}, d.checkDelimBeforeIOError(delim, wrapSyntacticError(d, err, pos, 0)) } } } @@ -437,13 +428,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 +443,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 +458,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 +477,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 +485,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 +509,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 +534,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 +546,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 +554,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, 0) } } @@ -614,7 +605,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,7 +617,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 { - return nil, d.checkDelimBeforeIOError(delim, err) + return nil, d.checkDelimBeforeIOError(delim, wrapSyntacticError(d, err, pos, 0)) } } } @@ -642,7 +633,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 +646,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 +671,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 +682,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 +758,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 +783,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 +802,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 +889,20 @@ 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 ':')") + return pos, wrapWithObjectName(jsonwire.NewInvalidCharacterError(d.buf[pos:], "after object name (expecting ':')"), quotedName) } pos++ @@ -918,12 +910,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 +933,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 +961,7 @@ func (d *decoderState) consumeArray(flags *jsonwire.ValueFlags, pos, depth int) return pos, nil } + var idx int64 depth++ for { // Handle before value. @@ -980,7 +973,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 +986,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 +1051,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 11b25b4..6cfb254 100644 --- a/jsontext/decode_test.go +++ b/jsontext/decode_test.go @@ -18,6 +18,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. @@ -189,8 +190,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"), @@ -223,65 +224,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"), @@ -304,238 +305,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(` {`), }, { @@ -543,7 +544,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(` {`), }, { @@ -573,71 +574,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(` [`), }, { @@ -645,7 +646,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(` [`), }, { @@ -653,8 +654,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(`""`), }, { @@ -662,8 +663,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(`{`), }, { @@ -672,8 +673,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(`{""`), }, { @@ -682,8 +683,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(`{""`), }, { @@ -693,8 +694,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(`{"":""`), }, { @@ -704,8 +705,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(`{"":""`), }, { @@ -713,8 +714,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(`[`), }, { @@ -723,8 +724,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(`[""`), }, { @@ -733,8 +734,8 @@ 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(`[""`), }} @@ -949,8 +950,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{"[ "}, @@ -965,7 +966,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}, @@ -973,7 +974,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"}, diff --git a/jsontext/encode.go b/jsontext/encode.go index c9c5a5c..66dc735 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") } } @@ -716,7 +706,8 @@ func (e *encoderState) reformatObject(dst []byte, src Value, depth int) ([]byte, return dst, n, io.ErrUnexpectedEOF } m := jsonwire.ConsumeSimpleString(src[n:]) - if m > 0 { + isVerbatim := m > 0 + if isVerbatim { dst = append(dst, src[n:n+m]...) } else { dst, m, err = jsonwire.ReformatString(dst, src[n:], &e.Flags) @@ -724,19 +715,19 @@ func (e *encoderState) reformatObject(dst []byte, src Value, depth int) ([]byte, return dst, n + m, err } } - // TODO: Specify whether the name is verbatim or not. - if !e.Flags.Get(jsonflags.AllowDuplicateNames) && !names.insertQuoted(src[n:n+m], false) { - return dst, n, newDuplicateNameError(src[n : n+m]) + quotedName := src[n : n+m] + if !e.Flags.Get(jsonflags.AllowDuplicateNames) && !names.insertQuoted(quotedName, isVerbatim) { + 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 ':')") + return dst, n, wrapWithObjectName(jsonwire.NewInvalidCharacterError(src[n:], "after object name (expecting ':')"), quotedName) } dst = append(dst, ':') n += len(":") @@ -747,11 +738,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 @@ -776,7 +767,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 '}')") } } } @@ -805,6 +796,7 @@ func (e *encoderState) reformatArray(dst []byte, src Value, depth int) ([]byte, return dst, n, nil } + var idx int64 var err error depth++ for { @@ -821,7 +813,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 @@ -837,6 +829,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) { @@ -846,7 +839,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 ']')") } } } @@ -924,6 +917,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 b7a53c7..05e6bd8 100644 --- a/jsontext/encode_test.go +++ b/jsontext/encode_test.go @@ -147,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"), @@ -157,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"), @@ -222,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", @@ -348,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", diff --git a/jsontext/errors.go b/jsontext/errors.go index 2a5d078..67717bd 100644 --- a/jsontext/errors.go +++ b/jsontext/errors.go @@ -5,6 +5,11 @@ package jsontext import ( + "bytes" + "io" + "strconv" + "strings" + "github.com/go-json-experiment/json/internal/jsonwire" ) @@ -32,29 +37,142 @@ 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 the position. +// It takes a relative position that can be resolved into +// an absolute position using coder.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. +func wrapSyntacticError(coder interface { + OffsetAt(pos int) int64 + AppendStackPointer(b []byte, where int) []byte +}, err error, pos, where int) error { + if where == 0 { + switch c := coder.(type) { + case *encoderState: + if c.Tokens.Last.needObjectValue() { + where = +1 + } + case *decoderState: + if c.Tokens.Last.needObjectValue() { + where = +1 + } + } + } + ptr := coder.AppendStackPointer(nil, where) + if serr, ok := err.(*pointerSuffixError); ok { + ptr = serr.appendPointer(ptr) + err = serr.error + } + if _, ok := err.(*ioError); err == io.EOF || ok { + return err + } + return &SyntacticError{ + ByteOffset: coder.OffsetAt(pos), + JSONPointer: Pointer(ptr), + Err: err, + } } func (e *SyntacticError) Error() string { - return errorPrefix + e.str + var sb strings.Builder + sb.WriteString(errorPrefix) + sb.WriteString("syntactic error") + switch { + case e.JSONPointer != "": + sb.WriteString(" within JSON value at ") + sb.WriteString(strconv.Quote(string(e.JSONPointer))) + case e.ByteOffset > 0: + sb.WriteString(" after byte offset ") + sb.WriteString(strconv.FormatInt(e.ByteOffset, 10)) + } + if e.Err != nil { + sb.WriteString(": ") + sb.WriteString(e.Err.Error()) + } + return sb.String() +} + +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. + bo := pointer + bi := e.reversePointer + for { + i := bytes.LastIndexByte(bi, '/') + if i < 0 { + return bo + } + bo = append(bo, bi[i:]...) + bi = bi[:i] + } } 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/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 aa1b7c9..65738c3 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. @@ -58,6 +81,24 @@ func (s *state) reset() { // they both point to the exact same value. type Pointer string +// Parent strips off the last token from 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,56 +112,74 @@ 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. -// The returned pointer is only accurate if s.names is populated, -// otherwise it uses the numeric index as the object member name. +// 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 + last := i == s.Tokens.Depth()-1 + if last && where == 0 { + return b } - b = append(b, '/') - switch { + switch e := s.Tokens.index(i); { case e.isObject(): - if objectDepth < s.Names.length() { - 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) - } - } - } else { - // Since the names stack is unpopulated, the name is unknown. - // As a best-effort replacement, use the numeric member index. - // While inaccurate, it produces a syntactically valid pointer. - b = strconv.AppendUint(b, uint64((e.Length()-1)/2), 10) + switch { + case last && where > 0 && e.Length()%2 == 0: + return b + case e.Length() == 0: + return b + default: + b = appendEscapePointerName(append(b, '/'), s.Names.getUnquoted(objectDepth)) } objectDepth++ case e.isArray(): - b = strconv.AppendUint(b, uint64(e.Length()-1), 10) + switch { + case last && where > 0: + b = strconv.AppendUint(append(b, '/'), uint64(e.Length()), 10) + case e.Length() == 0: + return b + default: + b = strconv.AppendUint(append(b, '/'), uint64(e.Length()-1), 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 @@ -179,7 +238,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: @@ -211,7 +270,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: @@ -246,7 +305,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: @@ -331,7 +390,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 c771a06..e47fa69 100644 --- a/jsontext/state_test.go +++ b/jsontext/state_test.go @@ -12,23 +12,30 @@ import ( "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 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 +137,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. 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..f609f1f 100644 --- a/jsontext/value_test.go +++ b/jsontext/value_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/go-json-experiment/json/internal/jsontest" + "github.com/go-json-experiment/json/internal/jsonwire" ) type valueTestdataEntry struct { @@ -107,13 +108,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: E(jsonwire.NewInvalidEscapeSequenceError(`\ud800"`)).withPos(`"`, ""), }, { name: jsontest.Name("UppercaseEscaped"), in: `"\u000B"`, @@ -130,15 +131,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) {