Repository: bytedance/sonic (go)

Issue:
Inconsistent Handling of Invalid JSON Compared to Standard Library
### Description

When using the Sonic library to unmarshal an invalid JSON string, the behavior differs from the standard Go `encoding/json` library. The standard library correctly returns an error, while Sonic does not report any error for the same input.

### Test Code

The following Go code demonstrates the issue:

```go
func TestSonic(t *testing.T) {
	str := "123abc"
	var msg json.RawMessage

	err := json.Unmarshal([]byte(str), &msg)
	fmt.Printf("standard json: %v\n", err)

	err = sonic.ConfigStd.UnmarshalFromString(str, &msg)
	fmt.Printf("sonic err: %v\n", err)
}
```

### Output

```
standard json: invalid character 'a' after top-level value
sonic err: <nil>
```

### Actual Behavior

- **Standard Library:** Returns an error (`invalid character 'a' after top-level value`)
- **Sonic:** Returns `<nil>` (no error)

### Environment

- Go version: go1.19.13
- Sonic version: v1.13.2

Pull request description:
fix(compat): should return error when unmarshaling json with trailing chars
#### What type of PR is this?
<!--
Add one of the following kinds:

build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
docs: Documentation only changes
feat: A new feature
optimize: A new optimization
fix: A bug fix
perf: A code change that improves performance
refactor: A code change that neither fixes a bug nor adds a feature
style: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc)
test: Adding missing tests or correcting existing tests
chore: Changes to the build process or auxiliary tools and libraries such as documentation generation
-->
fix

#### Check the PR title.
<!--
The description of the title will be attached in Release Notes, 
so please describe it from user-oriented, what this PR does / why we need it.
Please check your PR title with the below requirements:
-->
- [x] This PR title match the format: \<type\>(optional scope): \<description\>
- [x] The description of this PR title is user-oriented and clear enough for others to understand.
- [x] Attach the PR updating the user documentation if the current PR requires user awareness at the usage level. [User docs repo](https://github.com/cloudwego/cloudwego.github.io)


#### (Optional) Translate the PR title into Chinese.


#### (Optional) More detailed description for this PR(en: English/zh: Chinese).
<!--
Provide more detailed info for review(e.g., it's recommended to provide perf data if this is a perf type PR).
-->
en: 
zh(optional): 

#### (Optional) Which issue(s) this PR fixes:
<!--
Automatically closes linked issue when PR is merged.
Eg: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
Fixes #797 

#### (optional) The PR that updates user documentation:
<!--
If the current PR requires user awareness at the usage level, please submit a PR to update user docs. [User docs repo](https://github.com/cloudwego/cloudwego.github.io)
-->

Patch:
diff --git a/compat.go b/compat.go
index b694d7c..ec99649 100644
--- a/compat.go
+++ b/compat.go
@@ -87,7 +87,17 @@ func (cfg frozenConfig) UnmarshalFromString(buf string, val interface{}) error {
     if cfg.DisallowUnknownFields {
         dec.DisallowUnknownFields()
     }
-    return dec.Decode(val)
+    err := dec.Decode(val)
+    if err != nil {
+        return err
+    }
+
+    // check the trailing chars
+    offset := dec.InputOffset()
+    if t, err := dec.Token(); !(t == nil && err == io.EOF) {
+        return &json.SyntaxError{ Offset: offset}
+    }
+    return nil
 }
 
 // Unmarshal is implemented by sonic
diff --git a/internal/decoder/api/stream.go b/internal/decoder/api/stream.go
index 8a8102d..ecf1204 100644
--- a/internal/decoder/api/stream.go
+++ b/internal/decoder/api/stream.go
@@ -76,11 +76,12 @@ func (self *StreamDecoder) Decode(val interface{}) (err error) {
         if y := native.SkipOneFast(&src, &x); y < 0 {
             if self.readMore()  {
                 goto try_skip
-            } else {
-                err = SyntaxError{e, self.s, types.ParsingError(-s), ""}
-                self.setErr(err)
-                return
+            }                
+            if self.err == nil {
+                self.err = SyntaxError{e, self.s, types.ParsingError(-s), ""}
+                self.setErr(self.err)
             }
+            return self.err
         } else {
             s = y + s
             e = x + s


Test patch:
diff --git a/compat_test.go b/compat_test.go
index 0c8b0be..5ed084c 100644
--- a/compat_test.go
+++ b/compat_test.go
@@ -17,13 +17,13 @@
 package sonic
 
 import (
-    `bytes`
-    `encoding/json`
-    `reflect`
-    `testing`
+	"bytes"
+	"encoding/json"
+	"reflect"
+	"testing"
 
-    `github.com/bytedance/sonic/option`
-    `github.com/stretchr/testify/require`
+	"github.com/bytedance/sonic/option"
+	"github.com/stretchr/testify/require"
 )
 
 func TestCompatUnmarshalStd(t *testing.T) {
@@ -186,3 +186,21 @@ func TestGet(t *testing.T) {
     }
 }
 
+func TestUnmarshalWithTrailingChars(t *testing.T) {
+    for i, str := range([]string {
+        "123",
+        "123 ",
+        "{} [] ",
+        "{} [ ",
+        "[],",
+        "[]null",
+        "false null",
+    }) {
+        var msg1, msg2 json.RawMessage
+        err1 := json.Unmarshal([]byte(str), &msg1)
+        err2 := ConfigStd.UnmarshalFromString(str, &msg2)
+        require.Equal(t, err1 == nil, err2 == nil, i)
+        // sonic will not clear the unmarshaled value here, but encoding/json will
+        // require.Equal(t, msg1, msg2)
+    }
+}
diff --git a/internal/decoder/api/stream_test.go b/internal/decoder/api/stream_test.go
index 222d4cf..451b923 100644
--- a/internal/decoder/api/stream_test.go
+++ b/internal/decoder/api/stream_test.go
@@ -17,16 +17,16 @@
 package api
 
 import (
-    `bytes`
-    `encoding/json`
-    `io`
-    `io/ioutil`
-    `strings`
-    `testing`
-
-    `github.com/bytedance/sonic/option`
-    `github.com/stretchr/testify/assert`
-    `github.com/stretchr/testify/require`
+	"bytes"
+	"encoding/json"
+	"io"
+	"io/ioutil"
+	"strings"
+	"testing"
+
+	"github.com/bytedance/sonic/option"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 var (
@@ -71,6 +71,47 @@ func TestStreamError(t *testing.T) {
     }
 }
 
+func TestReaderError(t *testing.T) {
+    err := &SyntaxError{}
+    t.Run("middle", func(t *testing.T) {
+        er := &ErrReader{err : err, src: `{"a":1`+strings.Repeat(" ", int(option.DefaultDecoderBufferSize))}
+        dc := NewStreamDecoder(er)
+        var v interface{}
+        e2 := dc.Decode(&v)
+        if e2 != err {
+            t.Fail()
+        }
+    })
+    t.Run("after", func(t *testing.T) {
+        er := &ErrReader{err : err, src: `{"a":1}`+strings.Repeat(" ", int(option.DefaultDecoderBufferSize))}
+        dc := NewStreamDecoder(er)
+        var v interface{}
+        e2 := dc.Decode(&v)
+        if e2 != nil {
+            t.Fail()
+        }
+        e3 := dc.Decode(&v)
+        if e3 != err {
+            t.Fail()
+        }
+    })
+}
+
+type ErrReader struct {
+    src string
+    read int
+    err error
+}
+
+func (er *ErrReader) Read(p []byte) (int, error) {
+    if er.read >= len(er.src) {
+        return 0, er.err
+    }
+    n := copy(p, []byte(er.src[er.read:]))
+    er.read += n
+    return n, nil
+}
+
 func TestDecodeEmpty(t *testing.T) {
     var str = ``
     var r1 = strings.NewReader(str)