Skip to content

Commit

Permalink
Add JSONPointer to SyntacticError
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 committed Dec 9, 2024
1 parent bc21dd9 commit f0592aa
Show file tree
Hide file tree
Showing 18 changed files with 1,172 additions and 469 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 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 {
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")
}
}
63 changes: 49 additions & 14 deletions jsontext/coder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,25 @@ 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
}

var (
Expand Down Expand Up @@ -622,21 +637,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,23 +682,28 @@ 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)
})
})

Expand All @@ -697,15 +722,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)
})
Expand All @@ -714,22 +744,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 +776,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 f0592aa

Please sign in to comment.