diff --git a/src/encoding/json/jsontext/decode.go b/src/encoding/json/jsontext/decode.go index f505de44684..511832f2ae0 100644 --- a/src/encoding/json/jsontext/decode.go +++ b/src/encoding/json/jsontext/decode.go @@ -792,6 +792,12 @@ func (d *decoderState) CheckNextValue(last bool) error { return nil } +// AtEOF reports whether the decoder is at EOF. +func (d *decoderState) AtEOF() bool { + _, err := d.consumeWhitespace(d.prevEnd) + return err == io.ErrUnexpectedEOF +} + // CheckEOF verifies that the input has no more data. func (d *decoderState) CheckEOF() error { return d.checkEOF(d.prevEnd) diff --git a/src/encoding/json/v2/arshal.go b/src/encoding/json/v2/arshal.go index 6b4bcb0c74c..573d26567f3 100644 --- a/src/encoding/json/v2/arshal.go +++ b/src/encoding/json/v2/arshal.go @@ -440,8 +440,9 @@ func UnmarshalRead(in io.Reader, out any, opts ...Options) (err error) { // Unlike [Unmarshal] and [UnmarshalRead], decode options are ignored because // they must have already been specified on the provided [jsontext.Decoder]. // -// The input may be a stream of one or more JSON values, +// The input may be a stream of zero or more JSON values, // where this only unmarshals the next JSON value in the stream. +// If there are no more top-level JSON values, it reports [io.EOF]. // The output must be a non-nil pointer. // See [Unmarshal] for details about the conversion of JSON into a Go value. func UnmarshalDecode(in *jsontext.Decoder, out any, opts ...Options) (err error) { diff --git a/src/encoding/json/v2/arshal_funcs.go b/src/encoding/json/v2/arshal_funcs.go index 673caf3c376..28916af948d 100644 --- a/src/encoding/json/v2/arshal_funcs.go +++ b/src/encoding/json/v2/arshal_funcs.go @@ -9,6 +9,7 @@ package json import ( "errors" "fmt" + "io" "reflect" "sync" @@ -306,6 +307,9 @@ func UnmarshalFromFunc[T any](fn func(*jsontext.Decoder, T) error) *Unmarshalers fnc: func(dec *jsontext.Decoder, va addressableValue, uo *jsonopts.Struct) error { xd := export.Decoder(dec) prevDepth, prevLength := xd.Tokens.DepthLength() + if prevDepth == 1 && xd.AtEOF() { + return io.EOF // check EOF early to avoid fn reporting an EOF + } xd.Flags.Set(jsonflags.WithinArshalCall | 1) v, _ := reflect.TypeAssert[T](va.castTo(t)) err := fn(dec, v) diff --git a/src/encoding/json/v2/arshal_methods.go b/src/encoding/json/v2/arshal_methods.go index 2decd144dbe..1621eadc080 100644 --- a/src/encoding/json/v2/arshal_methods.go +++ b/src/encoding/json/v2/arshal_methods.go @@ -9,6 +9,7 @@ package json import ( "encoding" "errors" + "io" "reflect" "encoding/json/internal" @@ -302,6 +303,9 @@ func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { } xd := export.Decoder(dec) prevDepth, prevLength := xd.Tokens.DepthLength() + if prevDepth == 1 && xd.AtEOF() { + return io.EOF // check EOF early to avoid fn reporting an EOF + } xd.Flags.Set(jsonflags.WithinArshalCall | 1) unmarshaler, _ := reflect.TypeAssert[UnmarshalerFrom](va.Addr()) err := unmarshaler.UnmarshalJSONFrom(dec) diff --git a/src/encoding/json/v2/arshal_test.go b/src/encoding/json/v2/arshal_test.go index 75093345a3b..dc15c5a5f53 100644 --- a/src/encoding/json/v2/arshal_test.go +++ b/src/encoding/json/v2/arshal_test.go @@ -7833,7 +7833,8 @@ func TestUnmarshal(t *testing.T) { })), wantErr: EU(errSomeError).withType(0, T[unmarshalJSONv2Func]()), }, { - name: jsontest.Name("Methods/Invalid/JSONv2/TooFew"), + name: jsontest.Name("Methods/Invalid/JSONv2/TooFew"), + inBuf: `{}`, inVal: addr(unmarshalJSONv2Func(func(*jsontext.Decoder) error { return nil // do nothing })), @@ -9234,6 +9235,43 @@ func TestUnmarshalReuse(t *testing.T) { }) } +type unmarshalerEOF struct{} + +func (unmarshalerEOF) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return io.EOF // should be wrapped and converted by Unmarshal to io.ErrUnexpectedEOF +} + +// TestUnmarshalEOF verifies that io.EOF is only ever returned by +// UnmarshalDecode for a top-level value. +func TestUnmarshalEOF(t *testing.T) { + opts := WithUnmarshalers(UnmarshalFromFunc(func(dec *jsontext.Decoder, _ *struct{}) error { + return io.EOF // should be wrapped and converted by Unmarshal to io.ErrUnexpectedEOF + })) + + for _, in := range []string{"", "[", "[null", "[null]"} { + for _, newOut := range []func() any{ + func() any { return new(unmarshalerEOF) }, + func() any { return new([]unmarshalerEOF) }, + func() any { return new(struct{}) }, + func() any { return new([]struct{}) }, + } { + wantErr := io.ErrUnexpectedEOF + if gotErr := Unmarshal([]byte(in), newOut(), opts); !errors.Is(gotErr, wantErr) { + t.Errorf("Unmarshal = %v, want %v", gotErr, wantErr) + } + if gotErr := UnmarshalRead(strings.NewReader(in), newOut(), opts); !errors.Is(gotErr, wantErr) { + t.Errorf("Unmarshal = %v, want %v", gotErr, wantErr) + } + switch gotErr := UnmarshalDecode(jsontext.NewDecoder(strings.NewReader(in)), newOut(), opts); { + case in != "" && !errors.Is(gotErr, wantErr): + t.Errorf("Unmarshal = %v, want %v", gotErr, wantErr) + case in == "" && gotErr != io.EOF: + t.Errorf("Unmarshal = %v, want %v", gotErr, io.EOF) + } + } + } +} + type ReaderFunc func([]byte) (int, error) func (f ReaderFunc) Read(b []byte) (int, error) { return f(b) } diff --git a/src/encoding/json/v2/errors.go b/src/encoding/json/v2/errors.go index 0f50d608c9e..4421f8b821c 100644 --- a/src/encoding/json/v2/errors.go +++ b/src/encoding/json/v2/errors.go @@ -10,6 +10,7 @@ import ( "cmp" "errors" "fmt" + "io" "reflect" "strconv" "strings" @@ -118,7 +119,7 @@ func newInvalidFormatError(c coder, t reflect.Type) error { // newMarshalErrorBefore wraps err in a SemanticError assuming that e // is positioned right before the next token or value, which causes an error. func newMarshalErrorBefore(e *jsontext.Encoder, t reflect.Type, err error) error { - return &SemanticError{action: "marshal", GoType: t, Err: err, + return &SemanticError{action: "marshal", GoType: t, Err: toUnexpectedEOF(err), ByteOffset: e.OutputOffset() + int64(export.Encoder(e).CountNextDelimWhitespace()), JSONPointer: jsontext.Pointer(export.Encoder(e).AppendStackPointer(nil, +1))} } @@ -134,7 +135,7 @@ func newUnmarshalErrorBefore(d *jsontext.Decoder, t reflect.Type, err error) err if export.Decoder(d).Flags.Get(jsonflags.ReportErrorsWithLegacySemantics) { k = d.PeekKind() } - return &SemanticError{action: "unmarshal", GoType: t, Err: err, + return &SemanticError{action: "unmarshal", GoType: t, Err: toUnexpectedEOF(err), ByteOffset: d.InputOffset() + int64(export.Decoder(d).CountNextDelimWhitespace()), JSONPointer: jsontext.Pointer(export.Decoder(d).AppendStackPointer(nil, +1)), JSONKind: k} @@ -157,7 +158,7 @@ func newUnmarshalErrorBeforeWithSkipping(d *jsontext.Decoder, t reflect.Type, er // is positioned right after the previous token or value, which caused an error. func newUnmarshalErrorAfter(d *jsontext.Decoder, t reflect.Type, err error) error { tokOrVal := export.Decoder(d).PreviousTokenOrValue() - return &SemanticError{action: "unmarshal", GoType: t, Err: err, + return &SemanticError{action: "unmarshal", GoType: t, Err: toUnexpectedEOF(err), ByteOffset: d.InputOffset() - int64(len(tokOrVal)), JSONPointer: jsontext.Pointer(export.Decoder(d).AppendStackPointer(nil, -1)), JSONKind: jsontext.Value(tokOrVal).Kind()} @@ -206,6 +207,7 @@ func newSemanticErrorWithPosition(c coder, t reflect.Type, prevDepth int, prevLe if serr == nil { serr = &SemanticError{Err: err} } + serr.Err = toUnexpectedEOF(serr.Err) var currDepth int var currLength int64 var coderState interface{ AppendStackPointer([]byte, int) []byte } @@ -432,3 +434,11 @@ func newDuplicateNameError(ptr jsontext.Pointer, quotedName []byte, offset int64 Err: jsontext.ErrDuplicateName, } } + +// toUnexpectedEOF converts [io.EOF] to [io.ErrUnexpectedEOF]. +func toUnexpectedEOF(err error) error { + if err == io.EOF { + return io.ErrUnexpectedEOF + } + return err +}