mirror of https://github.com/goccy/go-json.git
326 lines
7.4 KiB
Go
326 lines
7.4 KiB
Go
package json
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding"
|
|
"encoding/base64"
|
|
"io"
|
|
"math"
|
|
"reflect"
|
|
"strconv"
|
|
"sync"
|
|
"sync/atomic"
|
|
"unsafe"
|
|
)
|
|
|
|
// An Encoder writes JSON values to an output stream.
|
|
type Encoder struct {
|
|
w io.Writer
|
|
ctx *encodeRuntimeContext
|
|
ptr unsafe.Pointer
|
|
buf []byte
|
|
enabledIndent bool
|
|
enabledHTMLEscape bool
|
|
unorderedMap bool
|
|
prefix []byte
|
|
indentStr []byte
|
|
}
|
|
|
|
type compiledCode struct {
|
|
code *opcode
|
|
}
|
|
|
|
const (
|
|
bufSize = 1024
|
|
)
|
|
|
|
const (
|
|
opCodeEscapedType = iota
|
|
opCodeEscapedIndentType
|
|
opCodeNoEscapeType
|
|
opCodeNoEscapeIndentType
|
|
)
|
|
|
|
type opcodeSet struct {
|
|
code *opcode
|
|
codeLength int
|
|
}
|
|
|
|
func loadOpcodeMap() map[uintptr]*opcodeSet {
|
|
p := atomic.LoadPointer(&cachedOpcode)
|
|
return *(*map[uintptr]*opcodeSet)(unsafe.Pointer(&p))
|
|
}
|
|
|
|
func storeOpcodeSet(typ uintptr, set *opcodeSet, m map[uintptr]*opcodeSet) {
|
|
newOpcodeMap := make(map[uintptr]*opcodeSet, len(m)+1)
|
|
newOpcodeMap[typ] = set
|
|
|
|
for k, v := range m {
|
|
newOpcodeMap[k] = v
|
|
}
|
|
|
|
atomic.StorePointer(&cachedOpcode, *(*unsafe.Pointer)(unsafe.Pointer(&newOpcodeMap)))
|
|
}
|
|
|
|
var (
|
|
encPool sync.Pool
|
|
codePool sync.Pool
|
|
cachedOpcode unsafe.Pointer // map[uintptr]*opcodeSet
|
|
marshalJSONType reflect.Type
|
|
marshalTextType reflect.Type
|
|
)
|
|
|
|
func init() {
|
|
encPool = sync.Pool{
|
|
New: func() interface{} {
|
|
return &Encoder{
|
|
ctx: &encodeRuntimeContext{
|
|
ptrs: make([]uintptr, 128),
|
|
keepRefs: make([]unsafe.Pointer, 0, 8),
|
|
},
|
|
buf: make([]byte, 0, bufSize),
|
|
}
|
|
},
|
|
}
|
|
marshalJSONType = reflect.TypeOf((*Marshaler)(nil)).Elem()
|
|
marshalTextType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
|
|
}
|
|
|
|
// NewEncoder returns a new encoder that writes to w.
|
|
func NewEncoder(w io.Writer) *Encoder {
|
|
enc := encPool.Get().(*Encoder)
|
|
enc.w = w
|
|
enc.reset()
|
|
return enc
|
|
}
|
|
|
|
// Encode writes the JSON encoding of v to the stream, followed by a newline character.
|
|
//
|
|
// See the documentation for Marshal for details about the conversion of Go values to JSON.
|
|
func (e *Encoder) Encode(v interface{}) error {
|
|
return e.EncodeWithOption(v)
|
|
}
|
|
|
|
// EncodeWithOption call Encode with EncodeOption.
|
|
func (e *Encoder) EncodeWithOption(v interface{}, opts ...EncodeOption) error {
|
|
for _, opt := range opts {
|
|
if err := opt(e); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
header := (*interfaceHeader)(unsafe.Pointer(&v))
|
|
buf, err := e.encode(header, v == nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if e.enabledIndent {
|
|
buf = buf[:len(buf)-2]
|
|
} else {
|
|
buf = buf[:len(buf)-1]
|
|
}
|
|
buf = append(buf, '\n')
|
|
if _, err := e.w.Write(buf); err != nil {
|
|
return err
|
|
}
|
|
e.buf = buf[:0]
|
|
return nil
|
|
}
|
|
|
|
// SetEscapeHTML specifies whether problematic HTML characters should be escaped inside JSON quoted strings.
|
|
// The default behavior is to escape &, <, and > to \u0026, \u003c, and \u003e to avoid certain safety problems that can arise when embedding JSON in HTML.
|
|
//
|
|
// In non-HTML settings where the escaping interferes with the readability of the output, SetEscapeHTML(false) disables this behavior.
|
|
func (e *Encoder) SetEscapeHTML(on bool) {
|
|
e.enabledHTMLEscape = on
|
|
}
|
|
|
|
// SetIndent instructs the encoder to format each subsequent encoded value as if indented by the package-level function Indent(dst, src, prefix, indent).
|
|
// Calling SetIndent("", "") disables indentation.
|
|
func (e *Encoder) SetIndent(prefix, indent string) {
|
|
if prefix == "" && indent == "" {
|
|
e.enabledIndent = false
|
|
return
|
|
}
|
|
e.prefix = []byte(prefix)
|
|
e.indentStr = []byte(indent)
|
|
e.enabledIndent = true
|
|
}
|
|
|
|
func (e *Encoder) release() {
|
|
e.w = nil
|
|
encPool.Put(e)
|
|
}
|
|
|
|
func (e *Encoder) reset() {
|
|
e.enabledHTMLEscape = true
|
|
e.enabledIndent = false
|
|
e.unorderedMap = false
|
|
}
|
|
|
|
func (e *Encoder) encodeForMarshal(header *interfaceHeader, isNil bool) ([]byte, error) {
|
|
buf, err := e.encode(header, isNil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
e.buf = buf
|
|
|
|
if e.enabledIndent {
|
|
copied := make([]byte, len(buf)-2)
|
|
copy(copied, buf)
|
|
return copied, nil
|
|
}
|
|
copied := make([]byte, len(buf)-1)
|
|
copy(copied, buf)
|
|
return copied, nil
|
|
}
|
|
|
|
func (e *Encoder) encode(header *interfaceHeader, isNil bool) ([]byte, error) {
|
|
b := e.buf[:0]
|
|
if isNil {
|
|
b = encodeNull(b)
|
|
if e.enabledIndent {
|
|
b = encodeIndentComma(b)
|
|
} else {
|
|
b = encodeComma(b)
|
|
}
|
|
return b, nil
|
|
}
|
|
typ := header.typ
|
|
|
|
typeptr := uintptr(unsafe.Pointer(typ))
|
|
opcodeMap := loadOpcodeMap()
|
|
if codeSet, exists := opcodeMap[typeptr]; exists {
|
|
ctx := e.ctx
|
|
p := uintptr(header.ptr)
|
|
ctx.init(p, codeSet.codeLength)
|
|
|
|
if e.enabledIndent {
|
|
if e.enabledHTMLEscape {
|
|
return e.runEscapedIndent(ctx, b, codeSet.code)
|
|
} else {
|
|
return e.runIndent(ctx, b, codeSet.code)
|
|
}
|
|
}
|
|
if e.enabledHTMLEscape {
|
|
return e.runEscaped(ctx, b, codeSet.code)
|
|
}
|
|
return e.run(ctx, b, codeSet.code)
|
|
}
|
|
|
|
// noescape trick for header.typ ( reflect.*rtype )
|
|
copiedType := *(**rtype)(unsafe.Pointer(&typeptr))
|
|
|
|
code, err := e.compileHead(&encodeCompileContext{
|
|
typ: copiedType,
|
|
root: true,
|
|
structTypeToCompiledCode: map[uintptr]*compiledCode{},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
code = copyOpcode(code)
|
|
codeLength := code.totalLength()
|
|
codeSet := &opcodeSet{
|
|
code: code,
|
|
codeLength: codeLength,
|
|
}
|
|
|
|
storeOpcodeSet(typeptr, codeSet, opcodeMap)
|
|
p := uintptr(header.ptr)
|
|
ctx := e.ctx
|
|
ctx.init(p, codeLength)
|
|
|
|
if e.enabledIndent {
|
|
if e.enabledHTMLEscape {
|
|
return e.runEscapedIndent(ctx, b, codeSet.code)
|
|
} else {
|
|
return e.runIndent(ctx, b, codeSet.code)
|
|
}
|
|
}
|
|
if e.enabledHTMLEscape {
|
|
return e.runEscaped(ctx, b, codeSet.code)
|
|
}
|
|
return e.run(ctx, b, codeSet.code)
|
|
}
|
|
|
|
func encodeFloat32(b []byte, v float32) []byte {
|
|
f64 := float64(v)
|
|
abs := math.Abs(f64)
|
|
fmt := byte('f')
|
|
// Note: Must use float32 comparisons for underlying float32 value to get precise cutoffs right.
|
|
if abs != 0 {
|
|
f32 := float32(abs)
|
|
if f32 < 1e-6 || f32 >= 1e21 {
|
|
fmt = 'e'
|
|
}
|
|
}
|
|
return strconv.AppendFloat(b, f64, fmt, -1, 32)
|
|
}
|
|
|
|
func encodeFloat64(b []byte, v float64) []byte {
|
|
abs := math.Abs(v)
|
|
fmt := byte('f')
|
|
// Note: Must use float32 comparisons for underlying float32 value to get precise cutoffs right.
|
|
if abs != 0 {
|
|
if abs < 1e-6 || abs >= 1e21 {
|
|
fmt = 'e'
|
|
}
|
|
}
|
|
return strconv.AppendFloat(b, v, fmt, -1, 64)
|
|
}
|
|
|
|
func encodeBool(b []byte, v bool) []byte {
|
|
if v {
|
|
return append(b, "true"...)
|
|
}
|
|
return append(b, "false"...)
|
|
}
|
|
|
|
func encodeBytes(dst []byte, src []byte) []byte {
|
|
return append(dst, src...)
|
|
}
|
|
|
|
func encodeNull(b []byte) []byte {
|
|
return append(b, "null"...)
|
|
}
|
|
|
|
func encodeComma(b []byte) []byte {
|
|
return append(b, ',')
|
|
}
|
|
|
|
func encodeIndentComma(b []byte) []byte {
|
|
return append(b, ',', '\n')
|
|
}
|
|
|
|
func appendStructEnd(b []byte) []byte {
|
|
return append(b, '}', ',')
|
|
}
|
|
|
|
func (e *Encoder) appendStructEndIndent(b []byte, indent int) []byte {
|
|
b = append(b, '\n')
|
|
b = append(b, e.prefix...)
|
|
b = append(b, bytes.Repeat(e.indentStr, indent)...)
|
|
return append(b, '}', ',', '\n')
|
|
}
|
|
|
|
func encodeByteSlice(b []byte, src []byte) []byte {
|
|
encodedLen := base64.StdEncoding.EncodedLen(len(src))
|
|
b = append(b, '"')
|
|
pos := len(b)
|
|
remainLen := cap(b[pos:])
|
|
var buf []byte
|
|
if remainLen > encodedLen {
|
|
buf = b[pos : pos+encodedLen]
|
|
} else {
|
|
buf = make([]byte, encodedLen)
|
|
}
|
|
base64.StdEncoding.Encode(buf, src)
|
|
return append(append(b, buf...), '"')
|
|
}
|
|
|
|
func (e *Encoder) encodeIndent(b []byte, indent int) []byte {
|
|
b = append(b, e.prefix...)
|
|
return append(b, bytes.Repeat(e.indentStr, indent)...)
|
|
}
|