Skip to content

Commit

Permalink
Add JSONPointer to SyntacticError (#66)
Browse files Browse the repository at this point in the history
Make SyntacticError precise about the precise location 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 since
  truncation of JSON text is technically a syntactic error.
* Avoid any dependency injection between jsontext and jsonwire.
* Add convenience methods to jsontext.Pointer.
  • Loading branch information
dsnet authored Dec 9, 2024
1 parent bc21dd9 commit cef7d84
Show file tree
Hide file tree
Showing 19 changed files with 1,196 additions and 488 deletions.
2 changes: 1 addition & 1 deletion arshal_any.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
6 changes: 3 additions & 3 deletions arshal_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}

Expand All @@ -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
}

Expand Down
4 changes: 2 additions & 2 deletions arshal_inlined.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
135 changes: 74 additions & 61 deletions arshal_test.go

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strconv"
"strings"

"github.com/go-json-experiment/json/internal/jsonwire"
"github.com/go-json-experiment/json/jsontext"
)

Expand Down Expand Up @@ -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,
}
}
13 changes: 4 additions & 9 deletions internal/jsonwire/wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,16 +141,11 @@ func truncateMaxUTF8[Bytes ~[]byte | ~string](b Bytes) Bytes {
return b
}

// NewError and ErrInvalidUTF8 are injected by the "jsontext" package,
// so that these error types use the jsontext.SyntacticError type.
var (
NewError = errors.New
ErrInvalidUTF8 = errors.New("invalid UTF-8 within string")
)
var ErrInvalidUTF8 = errors.New("invalid UTF-8")

func NewInvalidCharacterError[Bytes ~[]byte | ~string](prefix Bytes, where string) error {
what := QuoteRune(prefix)
return NewError("invalid character " + what + " " + where)
return errors.New("invalid character " + what + " " + where)
}

func NewInvalidEscapeSequenceError[Bytes ~[]byte | ~string](what Bytes) error {
Expand All @@ -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")
}
}
75 changes: 57 additions & 18 deletions jsontext/coder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,29 @@ import (
"testing"

"github.com/go-json-experiment/json/internal/jsontest"
"github.com/go-json-experiment/json/internal/jsonwire"
)

func len64[Bytes ~[]byte | ~string](in Bytes) int64 {
return int64(len(in))
func E(err error) *SyntacticError {
return &SyntacticError{Err: err}
}

func newInvalidCharacterError(prefix, where string) *SyntacticError {
return E(jsonwire.NewInvalidCharacterError(prefix, where))
}

func newInvalidEscapeSequenceError(what string) *SyntacticError {
return E(jsonwire.NewInvalidEscapeSequenceError(what))
}

func (e *SyntacticError) withPos(prefix string, pointer Pointer) *SyntacticError {
e.ByteOffset = int64(len(prefix))
e.JSONPointer = pointer
return e
}

func equalError(x, y error) bool {
return reflect.DeepEqual(x, y)
}

var (
Expand Down Expand Up @@ -591,13 +610,13 @@ func TestCoderMaxDepth(t *testing.T) {
var dec Decoder
checkReadToken := func(t *testing.T, wantKind Kind, wantErr error) {
t.Helper()
if tok, err := dec.ReadToken(); tok.Kind() != wantKind || !reflect.DeepEqual(err, wantErr) {
if tok, err := dec.ReadToken(); tok.Kind() != wantKind || !equalError(err, wantErr) {
t.Fatalf("Decoder.ReadToken = (%q, %v), want (%q, %v)", byte(tok.Kind()), err, byte(wantKind), wantErr)
}
}
checkReadValue := func(t *testing.T, wantLen int, wantErr error) {
t.Helper()
if val, err := dec.ReadValue(); len(val) != wantLen || !reflect.DeepEqual(err, wantErr) {
if val, err := dec.ReadValue(); len(val) != wantLen || !equalError(err, wantErr) {
t.Fatalf("Decoder.ReadValue = (%d, %v), want (%d, %v)", len(val), err, wantLen, wantErr)
}
}
Expand All @@ -622,21 +641,26 @@ func TestCoderMaxDepth(t *testing.T) {
}
})

wantErr := &SyntacticError{
ByteOffset: maxNestingDepth,
JSONPointer: Pointer(strings.Repeat("/0", maxNestingDepth)),
Err: errMaxDepth,
}
t.Run("ArraysInvalid/SingleValue", func(t *testing.T) {
dec.s.reset(maxArrays, nil)
checkReadValue(t, 0, errMaxDepth.withOffset(maxNestingDepth))
checkReadValue(t, 0, wantErr)
})
t.Run("ArraysInvalid/TokenThenValue", func(t *testing.T) {
dec.s.reset(maxArrays, nil)
checkReadToken(t, '[', nil)
checkReadValue(t, 0, errMaxDepth.withOffset(maxNestingDepth))
checkReadValue(t, 0, wantErr)
})
t.Run("ArraysInvalid/AllTokens", func(t *testing.T) {
dec.s.reset(maxArrays, nil)
for range maxNestingDepth {
checkReadToken(t, '[', nil)
}
checkReadToken(t, 0, errMaxDepth.withOffset(maxNestingDepth))
checkReadValue(t, 0, wantErr)
})

t.Run("ObjectsValid/SingleValue", func(t *testing.T) {
Expand All @@ -662,50 +686,60 @@ 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)
for range maxNestingDepth {
checkReadToken(t, '{', nil)
checkReadToken(t, '"', nil)
}
checkReadToken(t, 0, errMaxDepth.withOffset(maxNestingDepth*len64(`{"":`)))
checkReadToken(t, 0, wantErr)
})
})

t.Run("Encoder", func(t *testing.T) {
var enc Encoder
checkWriteToken := func(t *testing.T, tok Token, wantErr error) {
t.Helper()
if err := enc.WriteToken(tok); !reflect.DeepEqual(err, wantErr) {
if err := enc.WriteToken(tok); !equalError(err, wantErr) {
t.Fatalf("Encoder.WriteToken = %v, want %v", err, wantErr)
}
}
checkWriteValue := func(t *testing.T, val Value, wantErr error) {
t.Helper()
if err := enc.WriteValue(val); !reflect.DeepEqual(err, wantErr) {
if err := enc.WriteValue(val); !equalError(err, wantErr) {
t.Fatalf("Encoder.WriteValue = %v, want %v", err, wantErr)
}
}

wantErr := &SyntacticError{
ByteOffset: maxNestingDepth,
JSONPointer: Pointer(strings.Repeat("/0", maxNestingDepth)),
Err: errMaxDepth,
}
t.Run("Arrays/SingleValue", func(t *testing.T) {
enc.s.reset(enc.s.Buf[:0], nil)
checkWriteValue(t, maxArrays, errMaxDepth.withOffset(maxNestingDepth))
checkWriteValue(t, maxArrays, wantErr)
checkWriteValue(t, trimArray(maxArrays), nil)
})
t.Run("Arrays/TokenThenValue", func(t *testing.T) {
enc.s.reset(enc.s.Buf[:0], nil)
checkWriteToken(t, ArrayStart, nil)
checkWriteValue(t, trimArray(maxArrays), errMaxDepth.withOffset(maxNestingDepth))
checkWriteValue(t, trimArray(maxArrays), wantErr)
checkWriteValue(t, trimArray(trimArray(maxArrays)), nil)
checkWriteToken(t, ArrayEnd, nil)
})
Expand All @@ -714,22 +748,27 @@ func TestCoderMaxDepth(t *testing.T) {
for range maxNestingDepth {
checkWriteToken(t, ArrayStart, nil)
}
checkWriteToken(t, ArrayStart, errMaxDepth.withOffset(maxNestingDepth))
checkWriteToken(t, ArrayStart, wantErr)
for range maxNestingDepth {
checkWriteToken(t, ArrayEnd, nil)
}
})

wantErr = &SyntacticError{
ByteOffset: maxNestingDepth * int64(len(`{"":`)),
JSONPointer: Pointer(strings.Repeat("/", maxNestingDepth)),
Err: errMaxDepth,
}
t.Run("Objects/SingleValue", func(t *testing.T) {
enc.s.reset(enc.s.Buf[:0], nil)
checkWriteValue(t, maxObjects, errMaxDepth.withOffset(maxNestingDepth*len64(`{"":`)))
checkWriteValue(t, maxObjects, wantErr)
checkWriteValue(t, trimObject(maxObjects), nil)
})
t.Run("Objects/TokenThenValue", func(t *testing.T) {
enc.s.reset(enc.s.Buf[:0], nil)
checkWriteToken(t, ObjectStart, nil)
checkWriteToken(t, String(""), nil)
checkWriteValue(t, trimObject(maxObjects), errMaxDepth.withOffset(maxNestingDepth*len64(`{"":`)))
checkWriteValue(t, trimObject(maxObjects), wantErr)
checkWriteValue(t, trimObject(trimObject(maxObjects)), nil)
checkWriteToken(t, ObjectEnd, nil)
})
Expand All @@ -741,7 +780,7 @@ func TestCoderMaxDepth(t *testing.T) {
}
checkWriteToken(t, ObjectStart, nil)
checkWriteToken(t, String(""), nil)
checkWriteToken(t, ObjectStart, errMaxDepth.withOffset(maxNestingDepth*len64(`{"":`)))
checkWriteToken(t, ObjectStart, wantErr)
checkWriteToken(t, String(""), nil)
for range maxNestingDepth {
checkWriteToken(t, ObjectEnd, nil)
Expand Down
Loading

0 comments on commit cef7d84

Please sign in to comment.