Files
quic-go/qlogwriter/jsontext/encoder.go
2025-10-11 07:08:06 +02:00

325 lines
7.1 KiB
Go

// Package jsontext provides a fast JSON encoder providing only the necessary features
// for qlog encoding. No efforts are made to add any features beyond qlog's requirements.
//
// The API aims to be compatible with the standard library's encoding/json/jsontext package.
package jsontext
import (
"fmt"
"io"
"strconv"
"unsafe"
)
type kind uint8
const (
kindString kind = iota
kindInt
kindUint
kindFloat
kindBool
kindNull
kindObjectStart
kindObjectEnd
kindArrayStart
kindArrayEnd
)
// Token represents a JSON token.
type Token struct {
kind kind
str string
i64 int64
u64 uint64
f64 float64
b bool
}
// String creates a string token.
func String(s string) Token {
return Token{kind: kindString, str: s}
}
// Int creates an int token.
func Int(i int64) Token {
return Token{kind: kindInt, i64: i}
}
// Uint creates a uint token.
func Uint(u uint64) Token {
return Token{kind: kindUint, u64: u}
}
// Float creates a float token.
func Float(f float64) Token {
return Token{kind: kindFloat, f64: f}
}
// Bool creates a bool token.
func Bool(b bool) Token {
return Token{kind: kindBool, b: b}
}
// Null is a null token.
var Null Token = Token{kind: kindNull}
// BeginObject is the begin object token.
var BeginObject Token = Token{kind: kindObjectStart}
// EndObject is the end object token.
var EndObject Token = Token{kind: kindObjectEnd}
// BeginArray is the begin array token.
var BeginArray Token = Token{kind: kindArrayStart}
// EndArray is the end array token.
var EndArray Token = Token{kind: kindArrayEnd}
// True is a true token.
var True Token = Bool(true)
// False is a false token.
var False Token = Bool(false)
var hexDigits = [16]byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}
var (
commaByte = []byte(",")
quoteByte = []byte(`"`)
colonByte = []byte(":")
trueByte = []byte("true")
falseByte = []byte("false")
nullByte = []byte("null")
openObjectByte = []byte("{")
closeObjectByte = []byte("}")
openArrayByte = []byte("[")
closeArrayByte = []byte("]")
newlineByte = []byte("\n")
escapeQuote = []byte(`\"`)
escapeBackslash = []byte(`\\`)
escapeBackspace = []byte(`\b`)
escapeFormfeed = []byte(`\f`)
escapeNewline = []byte(`\n`)
escapeCarriage = []byte(`\r`)
escapeTab = []byte(`\t`)
escapeUnicode = []byte(`\u00`)
)
type context struct {
isObject bool
needsComma bool
expectKey bool
}
// Encoder encodes JSON to an io.Writer.
type Encoder struct {
w io.Writer
buf [64]byte // scratch buffer for number formatting
stack []context
}
// NewEncoder creates a new Encoder.
func NewEncoder(w io.Writer) *Encoder {
stack := make([]context, 0, 8)
stack = append(stack, context{isObject: false, needsComma: false, expectKey: false})
return &Encoder{
w: w,
stack: stack,
}
}
// WriteToken writes a token to the encoder.
func (e *Encoder) WriteToken(t Token) error {
if len(e.stack) == 0 {
return fmt.Errorf("empty stack")
}
curr := &e.stack[len(e.stack)-1]
isClosing := t.kind == kindObjectEnd || t.kind == kindArrayEnd
if !isClosing && curr.needsComma {
if _, err := e.w.Write(commaByte); err != nil {
return err
}
curr.needsComma = false
}
var err error
switch t.kind {
case kindString:
data := stringToBytes(t.str)
needsEscape := false
for _, b := range data {
if b == '"' || b == '\\' || b < 0x20 {
needsEscape = true
break
}
}
if !needsEscape {
if _, err = e.w.Write(quoteByte); err != nil {
return err
}
if _, err = e.w.Write(data); err != nil {
return err
}
if _, err = e.w.Write(quoteByte); err != nil {
return err
}
} else {
if _, err = e.w.Write(quoteByte); err != nil {
return err
}
for i := 0; i < len(t.str); i++ {
c := t.str[i]
switch c {
case '"':
if _, err = e.w.Write(escapeQuote); err != nil {
return err
}
case '\\':
if _, err = e.w.Write(escapeBackslash); err != nil {
return err
}
case '\b':
if _, err = e.w.Write(escapeBackspace); err != nil {
return err
}
case '\f':
if _, err = e.w.Write(escapeFormfeed); err != nil {
return err
}
case '\n':
if _, err = e.w.Write(escapeNewline); err != nil {
return err
}
case '\r':
if _, err = e.w.Write(escapeCarriage); err != nil {
return err
}
case '\t':
if _, err = e.w.Write(escapeTab); err != nil {
return err
}
default:
if c < 0x20 {
if _, err = e.w.Write(escapeUnicode); err != nil {
return err
}
if _, err = e.w.Write([]byte{hexDigits[c>>4], hexDigits[c&0xf]}); err != nil {
return err
}
} else {
if _, err = e.w.Write([]byte{c}); err != nil {
return err
}
}
}
}
if _, err = e.w.Write(quoteByte); err != nil {
return err
}
}
if curr.isObject {
if curr.expectKey {
// key
if _, err = e.w.Write(colonByte); err != nil {
return err
}
curr.expectKey = false
return nil // do not call afterValue for keys
} else {
// value
e.afterValue()
}
} else {
e.afterValue()
}
case kindInt:
b := strconv.AppendInt(e.buf[:0], t.i64, 10)
if _, err = e.w.Write(b); err != nil {
return err
}
e.afterValue()
case kindUint:
b := strconv.AppendUint(e.buf[:0], t.u64, 10)
if _, err = e.w.Write(b); err != nil {
return err
}
e.afterValue()
case kindFloat:
b := strconv.AppendFloat(e.buf[:0], t.f64, 'g', -1, 64)
if _, err = e.w.Write(b); err != nil {
return err
}
e.afterValue()
case kindBool:
if t.b {
if _, err = e.w.Write(trueByte); err != nil {
return err
}
} else {
if _, err = e.w.Write(falseByte); err != nil {
return err
}
}
e.afterValue()
case kindNull:
if _, err = e.w.Write(nullByte); err != nil {
return err
}
e.afterValue()
case kindObjectStart:
if _, err = e.w.Write(openObjectByte); err != nil {
return err
}
e.stack = append(e.stack, context{isObject: true, needsComma: false, expectKey: true})
return nil
case kindObjectEnd:
if _, err = e.w.Write(closeObjectByte); err != nil {
return err
}
e.stack = e.stack[:len(e.stack)-1]
e.afterValue()
if len(e.stack) == 1 {
if _, err = e.w.Write(newlineByte); err != nil {
return err
}
}
return nil
case kindArrayStart:
if _, err = e.w.Write(openArrayByte); err != nil {
return err
}
e.stack = append(e.stack, context{isObject: false, needsComma: false, expectKey: false})
return nil
case kindArrayEnd:
if _, err = e.w.Write(closeArrayByte); err != nil {
return err
}
e.stack = e.stack[:len(e.stack)-1]
e.afterValue()
if len(e.stack) == 1 {
if _, err = e.w.Write(newlineByte); err != nil {
return err
}
}
return nil
default:
return fmt.Errorf("unknown token kind")
}
return err
}
// afterValue updates the state after encoding a value
func (e *Encoder) afterValue() {
if len(e.stack) > 1 {
curr := &e.stack[len(e.stack)-1]
curr.needsComma = true
if curr.isObject {
curr.expectKey = true
}
}
}
func stringToBytes(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s))
}