mirror of
https://github.com/gin-gonic/gin.git
synced 2026-09-04 14:49:27 +08:00
Merge ed773a4d385e0429757a0ebbe8aa8900ce10b44b into dcaa4296d111981ffb31ac3eba90bb63e1eb5ab9
This commit is contained in:
commit
ea8e375072
@ -10,6 +10,7 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
"unicode/utf16"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin/codec/json"
|
"github.com/gin-gonic/gin/codec/json"
|
||||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||||
@ -160,11 +161,20 @@ func (r AsciiJSON) Render(w http.ResponseWriter) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
escapeBuf := make([]byte, 0, 6) // Preallocate 6 bytes for Unicode escape sequences
|
escapeBuf := make([]byte, 0, 13) // Preallocate for worst case: two \uXXXX surrogate pair escapes
|
||||||
|
|
||||||
for _, r := range bytesconv.BytesToString(ret) {
|
for _, r := range bytesconv.BytesToString(ret) {
|
||||||
if r > unicode.MaxASCII {
|
if r > unicode.MaxASCII {
|
||||||
escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x", r) // Reuse escapeBuf
|
if r > 0xFFFF {
|
||||||
|
// Non-BMP character (above U+FFFF): encode as a UTF-16 surrogate pair.
|
||||||
|
// A JSON \u escape is exactly 4 hex digits, so code points requiring more
|
||||||
|
// than 4 digits must be split into a high/low surrogate pair per RFC 8259 §7.
|
||||||
|
high, low := utf16.EncodeRune(r)
|
||||||
|
escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x\\u%04x", high, low)
|
||||||
|
} else {
|
||||||
|
// BMP character (U+0080–U+FFFF): a single \uXXXX escape suffices.
|
||||||
|
escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x", r)
|
||||||
|
}
|
||||||
buffer.Write(escapeBuf)
|
buffer.Write(escapeBuf)
|
||||||
} else {
|
} else {
|
||||||
buffer.WriteByte(byte(r))
|
buffer.WriteByte(byte(r))
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
package render
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
"html/template"
|
"html/template"
|
||||||
@ -261,6 +262,36 @@ func TestRenderAsciiJSON(t *testing.T) {
|
|||||||
assert.Equal(t, "3.1415926", w2.Body.String())
|
assert.Equal(t, "3.1415926", w2.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRenderAsciiJSONNonBMP(t *testing.T) {
|
||||||
|
// Non-BMP characters (code points above U+FFFF) must be encoded as UTF-16
|
||||||
|
// surrogate pairs in JSON \u escapes (RFC 8259 §7). Previously, AsciiJSON
|
||||||
|
// emitted a single \uXXXXX escape with 5+ hex digits, which is syntactically
|
||||||
|
// valid JSON but silently decodes to the wrong character.
|
||||||
|
inputs := []string{
|
||||||
|
"\U0001F600", // U+1F600 GRINNING FACE
|
||||||
|
"\U0001F602", // U+1F602 FACE WITH TEARS OF JOY
|
||||||
|
"\U0001F680", // U+1F680 ROCKET
|
||||||
|
"\U00020000", // U+20000 CJK Extension B
|
||||||
|
}
|
||||||
|
for _, input := range inputs {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
err := (AsciiJSON{input}).Render(w)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
body := w.Body.String()
|
||||||
|
|
||||||
|
// Output must be pure ASCII.
|
||||||
|
for i, b := range []byte(body) {
|
||||||
|
assert.Less(t, b, byte(128), "non-ASCII byte at index %d in rendered output for input %q", i, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round-trip: json.Unmarshal must recover the original string.
|
||||||
|
var decoded string
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(body), &decoded), "input %q", input)
|
||||||
|
assert.Equal(t, input, decoded, "round-trip failed for %q", input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRenderAsciiJSONFail(t *testing.T) {
|
func TestRenderAsciiJSONFail(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
data := make(chan int)
|
data := make(chan int)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user