]> Cypherpunks repositories - gostls13.git/commitdiff
encoding/json/v2: fix UnmarshalDecode regression with EOF
authorJoe Tsai <joetsai@digital-static.net>
Thu, 31 Jul 2025 22:01:47 +0000 (15:01 -0700)
committerGopher Robot <gobot@golang.org>
Mon, 11 Aug 2025 19:12:28 +0000 (12:12 -0700)
When EOF is encountered within jsontext.Decoder stream without
starting to parse any token, UnmarshalDecode should report EOF,
rather than converting it into ErrUnexpectedEOF.

This fixes a regression introduced by https://go.dev/cl/689919.

This change only affects code compiled under goexperiment.jsonv2.

Fixes #74835

Change-Id: I7e8e57ab11b462c422c538503ed8c6b91ead53bd
Reviewed-on: https://go-review.googlesource.com/c/go/+/692175
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Sean Liao <sean@liao.dev>
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Jake Bailey <jacob.b.bailey@gmail.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Auto-Submit: Joseph Tsai <joetsai@digital-static.net>

src/encoding/json/v2/arshal.go
src/encoding/json/v2/arshal_test.go

index e2ce778d5ad96c4a641a4765f7072d6942694efa..6b4bcb0c74cf7c9c56099ccc63156c0c66f8f9a6 100644 (file)
@@ -470,7 +470,7 @@ func unmarshalDecode(in *jsontext.Decoder, out any, uo *jsonopts.Struct, last bo
        // was validated before attempting to unmarshal it.
        if uo.Flags.Get(jsonflags.ReportErrorsWithLegacySemantics) {
                if err := export.Decoder(in).CheckNextValue(last); err != nil {
-                       if err == io.EOF {
+                       if err == io.EOF && last {
                                offset := in.InputOffset() + int64(len(in.UnreadBuffer()))
                                return &jsontext.SyntacticError{ByteOffset: offset, Err: io.ErrUnexpectedEOF}
                        }
@@ -487,7 +487,7 @@ func unmarshalDecode(in *jsontext.Decoder, out any, uo *jsonopts.Struct, last bo
                if !uo.Flags.Get(jsonflags.AllowDuplicateNames) {
                        export.Decoder(in).Tokens.InvalidateDisabledNamespaces()
                }
-               if err == io.EOF {
+               if err == io.EOF && last {
                        offset := in.InputOffset() + int64(len(in.UnreadBuffer()))
                        return &jsontext.SyntacticError{ByteOffset: offset, Err: io.ErrUnexpectedEOF}
                }
index f1ee2e2e3a73655ea06a9ed01f40fc80841810ab..764ce690078a874c6c1745a13b66a5af91c08da7 100644 (file)
@@ -9413,6 +9413,51 @@ func TestUnmarshalDecodeOptions(t *testing.T) {
        }
 }
 
+func TestUnmarshalDecodeStream(t *testing.T) {
+       tests := []struct {
+               in   string
+               want []any
+               err  error
+       }{
+               {in: ``, err: io.EOF},
+               {in: `{`, err: &jsontext.SyntacticError{ByteOffset: len64(`{`), Err: io.ErrUnexpectedEOF}},
+               {in: `{"`, err: &jsontext.SyntacticError{ByteOffset: len64(`{"`), Err: io.ErrUnexpectedEOF}},
+               {in: `{"k"`, err: &jsontext.SyntacticError{ByteOffset: len64(`{"k"`), JSONPointer: "/k", Err: io.ErrUnexpectedEOF}},
+               {in: `{"k":`, err: &jsontext.SyntacticError{ByteOffset: len64(`{"k":`), JSONPointer: "/k", Err: io.ErrUnexpectedEOF}},
+               {in: `{"k",`, err: &jsontext.SyntacticError{ByteOffset: len64(`{"k"`), JSONPointer: "/k", Err: jsonwire.NewInvalidCharacterError(",", "after object name (expecting ':')")}},
+               {in: `{"k"}`, err: &jsontext.SyntacticError{ByteOffset: len64(`{"k"`), JSONPointer: "/k", Err: jsonwire.NewInvalidCharacterError("}", "after object name (expecting ':')")}},
+               {in: `[`, err: &jsontext.SyntacticError{ByteOffset: len64(`[`), Err: io.ErrUnexpectedEOF}},
+               {in: `[0`, err: &jsontext.SyntacticError{ByteOffset: len64(`[0`), Err: io.ErrUnexpectedEOF}},
+               {in: ` [0`, err: &jsontext.SyntacticError{ByteOffset: len64(` [0`), Err: io.ErrUnexpectedEOF}},
+               {in: `[0.`, err: &jsontext.SyntacticError{ByteOffset: len64(`[`), JSONPointer: "/0", Err: io.ErrUnexpectedEOF}},
+               {in: `[0. `, err: &jsontext.SyntacticError{ByteOffset: len64(`[0.`), JSONPointer: "/0", Err: jsonwire.NewInvalidCharacterError(" ", "in number (expecting digit)")}},
+               {in: `[0,`, err: &jsontext.SyntacticError{ByteOffset: len64(`[0,`), Err: io.ErrUnexpectedEOF}},
+               {in: `[0:`, err: &jsontext.SyntacticError{ByteOffset: len64(`[0`), Err: jsonwire.NewInvalidCharacterError(":", "after array element (expecting ',' or ']')")}},
+               {in: `n`, err: &jsontext.SyntacticError{ByteOffset: len64(`n`), Err: io.ErrUnexpectedEOF}},
+               {in: `nul`, err: &jsontext.SyntacticError{ByteOffset: len64(`nul`), Err: io.ErrUnexpectedEOF}},
+               {in: `fal `, err: &jsontext.SyntacticError{ByteOffset: len64(`fal`), Err: jsonwire.NewInvalidCharacterError(" ", "in literal false (expecting 's')")}},
+               {in: `false`, want: []any{false}, err: io.EOF},
+               {in: `false0.0[]null`, want: []any{false, 0.0, []any{}, nil}, err: io.EOF},
+       }
+       for _, tt := range tests {
+               d := jsontext.NewDecoder(strings.NewReader(tt.in))
+               var got []any
+               for {
+                       var v any
+                       if err := UnmarshalDecode(d, &v); err != nil {
+                               if !reflect.DeepEqual(err, tt.err) {
+                                       t.Errorf("`%s`: UnmarshalDecode error = %v, want %v", tt.in, err, tt.err)
+                               }
+                               break
+                       }
+                       got = append(got, v)
+               }
+               if !reflect.DeepEqual(got, tt.want) {
+                       t.Errorf("`%s`: UnmarshalDecode = %v, want %v", tt.in, got, tt.want)
+               }
+       }
+}
+
 // BenchmarkUnmarshalDecodeOptions is a minimal decode operation to measure
 // the overhead options setup before the unmarshal operation.
 func BenchmarkUnmarshalDecodeOptions(b *testing.B) {