mirror of
https://github.com/gin-gonic/gin.git
synced 2026-09-12 04:46:06 +08:00
fix(render): encode non-BMP runes as UTF-16 surrogate pairs in AsciiJSON
AsciiJSON escaped every non-ASCII rune with fmt.Appendf(buf, "\u%04x", r), but %04x is a minimum width, not a fixed width. Runes outside the Basic Multilingual Plane (e.g. emoji) need 5+ hex digits, so a single malformed \uXXXXX token was written instead of a valid 4-digit JSON \u escape. The output stayed syntactically valid JSON, so this was easy to miss, but a decoder reads the first 4 hex digits as one character and treats the rest as literal text, corrupting the value silently. RFC 8259 §7 requires code points outside the BMP to be encoded as a UTF-16 surrogate pair. Use unicode/utf16.EncodeRune to produce the pair for those runes while keeping the existing single-escape path for BMP characters. Fixes #4688
This commit is contained in:
parent
34dac209ff
commit
6942b48b72
@ -10,6 +10,7 @@ import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"unicode"
|
||||
"unicode/utf16"
|
||||
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||
@ -160,14 +161,22 @@ func (r AsciiJSON) Render(w http.ResponseWriter) error {
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
escapeBuf := make([]byte, 0, 6) // Preallocate 6 bytes for Unicode escape sequences
|
||||
escapeBuf := make([]byte, 0, 12) // Preallocate 12 bytes: worst case is a \uXXXX\uXXXX surrogate pair
|
||||
|
||||
for _, r := range bytesconv.BytesToString(ret) {
|
||||
if r > unicode.MaxASCII {
|
||||
switch {
|
||||
case r <= unicode.MaxASCII:
|
||||
buffer.WriteByte(byte(r))
|
||||
case r <= 0xFFFF:
|
||||
escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x", r) // Reuse escapeBuf
|
||||
buffer.Write(escapeBuf)
|
||||
} else {
|
||||
buffer.WriteByte(byte(r))
|
||||
default:
|
||||
// Code points outside the Basic Multilingual Plane don't fit in a
|
||||
// single 4-hex-digit \u escape; RFC 8259 §7 requires encoding them
|
||||
// as a UTF-16 surrogate pair instead.
|
||||
r1, r2 := utf16.EncodeRune(r)
|
||||
escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x\\u%04x", r1, r2)
|
||||
buffer.Write(escapeBuf)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"html/template"
|
||||
@ -261,6 +262,23 @@ func TestRenderAsciiJSON(t *testing.T) {
|
||||
assert.Equal(t, "3.1415926", w2.Body.String())
|
||||
}
|
||||
|
||||
func TestRenderAsciiJSONNonBMP(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
data := map[string]any{"msg": "😀"}
|
||||
|
||||
err := (AsciiJSON{data}).Render(w)
|
||||
require.NoError(t, err)
|
||||
|
||||
// U+1F600 is outside the Basic Multilingual Plane and must be encoded as
|
||||
// a UTF-16 surrogate pair (RFC 8259 §7), not a single \u escape with 5+
|
||||
// hex digits.
|
||||
assert.Equal(t, "{\"msg\":\"\\ud83d\\ude00\"}", w.Body.String())
|
||||
|
||||
var decoded map[string]string
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &decoded))
|
||||
assert.Equal(t, "😀", decoded["msg"])
|
||||
}
|
||||
|
||||
func TestRenderAsciiJSONFail(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
data := make(chan int)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user