lolly/gjson/encode.go
xfy c37364b309 style: format code and modernize loop syntax
- Align struct fields and constants in gjson/config.go
- Add missing newline at EOF in gjson/decode.go
- Remove trailing blank line in gjson/encode.go
- Remove extra blank line in internal/lua/coroutine.go
- Use modern for range syntax in internal/lua/pool.go

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 17:34:59 +08:00

270 lines
6.6 KiB
Go

package gjson
import (
"fmt"
"sort"
"strconv"
"strings"
json "github.com/goccy/go-json"
glua "github.com/yuin/gopher-lua"
)
// encodeLuaValue converts a Lua value to a JSON string.
func encodeLuaValue(L *glua.LState, value glua.LValue, config *Config, depth int) (string, error) {
switch value.Type() {
case glua.LTNil:
return "null", nil
case glua.LTBool:
if value == glua.LTrue {
return "true", nil
}
return "false", nil
case glua.LTNumber:
num := float64(value.(glua.LNumber)) //nolint:errcheck // switch case guarantees type
return formatNumber(num, config.encodeNumberPrecision), nil
case glua.LTString:
str := string(value.(glua.LString)) //nolint:errcheck // switch case guarantees type
// Use go-json for proper string escaping
result, err := json.Marshal(str)
if err != nil {
return "", fmt.Errorf("failed to encode string: %w", err)
}
return string(result), nil
case glua.LTTable:
return encodeTable(L, value.(*glua.LTable), config, depth) //nolint:errcheck // switch case guarantees type
case glua.LTUserData:
if isNull(value) {
return "null", nil
}
return "", fmt.Errorf("cannot encode userdata (not gjson.null)")
case glua.LTFunction:
return "", fmt.Errorf("cannot encode function")
case glua.LTThread:
return "", fmt.Errorf("cannot encode thread")
case glua.LTChannel:
return "", fmt.Errorf("cannot encode channel")
default:
return "", fmt.Errorf("cannot encode unknown type: %s", value.Type())
}
}
// encodeTable converts a Lua table to JSON (array or object).
func encodeTable(L *glua.LState, tbl *glua.LTable, config *Config, depth int) (string, error) {
// Check depth limit
if depth >= config.encodeMaxDepth {
return "", fmt.Errorf("maximum nesting depth %d exceeded", config.encodeMaxDepth)
}
// Determine if table is an array or object
isArray, maxIndex, _ := checkArrayType(tbl, config)
if isArray {
return encodeArray(L, tbl, maxIndex, config, depth)
}
return encodeObject(L, tbl, config, depth)
}
// checkArrayType determines if a table should be encoded as an array.
// Returns: (isArray, maxIndex, count)
func checkArrayType(tbl *glua.LTable, config *Config) (bool, int, int) {
maxIndex := 0
count := 0
hasStringKey := false
tbl.ForEach(func(key, _ glua.LValue) {
switch k := key.(type) {
case glua.LNumber:
// Protect against integer overflow - only accept positive integers within int range
floatIdx := float64(k)
if floatIdx < 1 || floatIdx > float64(int(^uint(0)>>1)) {
hasStringKey = true // Treat out-of-range as object key
return
}
idx := int(k)
if idx > maxIndex {
maxIndex = idx
}
count++
case glua.LString:
hasStringKey = true
}
})
// If there are string keys, it's an object
if hasStringKey {
return false, maxIndex, count
}
// Empty table is encoded as empty object (lua-cjson behavior)
if count == 0 {
return false, 0, 0
}
// Check for sparse array
// Sparse condition: ratio > 0 && maxIndex > safe && maxIndex > count * ratio
if config.encodeSparseArray.ratio > 0 &&
maxIndex > config.encodeSparseArray.safe &&
maxIndex > count*config.encodeSparseArray.ratio {
// Sparse array detected
if !config.encodeSparseArray.convert {
// Would return error, but we return false to indicate object encoding
return false, maxIndex, count
}
// Convert to object
return false, maxIndex, count
}
// Check if keys are sequential starting from 1
// Use MaxN() for quick check
maxN := tbl.MaxN()
if maxN == count && maxN == maxIndex {
return true, maxIndex, count
}
// Non-sequential keys -> object
return false, maxIndex, count
}
// encodeArray encodes a Lua table as a JSON array.
func encodeArray(L *glua.LState, tbl *glua.LTable, maxIndex int, config *Config, depth int) (string, error) {
elements := make([]string, 0, maxIndex)
for i := 1; i <= maxIndex; i++ {
val := tbl.RawGetInt(i)
if val == glua.LNil {
// Missing element in sparse array - encode as null
elements = append(elements, "null")
continue
}
encoded, err := encodeLuaValue(L, val, config, depth+1)
if err != nil {
return "", err
}
elements = append(elements, encoded)
}
return "[" + strings.Join(elements, ",") + "]", nil
}
// encodeObject encodes a Lua table as a JSON object.
func encodeObject(L *glua.LState, tbl *glua.LTable, config *Config, depth int) (string, error) {
// 快速路径:不需要排序时直接编码
if !config.encodeSortKeys {
elements := make([]string, 0)
var encodeErr error
tbl.ForEach(func(key, value glua.LValue) {
if encodeErr != nil {
return
}
// Encode key
var keyStr string
switch k := key.(type) {
case glua.LString:
encoded, _ := json.Marshal(string(k))
keyStr = string(encoded)
case glua.LNumber:
keyStr = formatNumber(float64(k), config.encodeNumberPrecision)
keyStr = "\"" + keyStr + "\""
default:
return
}
// Encode value
valStr, err := encodeLuaValue(L, value, config, depth+1)
if err != nil {
encodeErr = err
return
}
elements = append(elements, keyStr+":"+valStr)
})
if encodeErr != nil {
return "", encodeErr
}
return "{" + strings.Join(elements, ",") + "}", nil
}
// 排序路径:收集所有键值对后排序
type kv struct {
key string
value glua.LValue
}
pairs := make([]kv, 0)
var encodeErr error
tbl.ForEach(func(key, value glua.LValue) {
if encodeErr != nil {
return
}
// Encode key
var keyStr string
switch k := key.(type) {
case glua.LString:
encoded, _ := json.Marshal(string(k))
keyStr = string(encoded)
case glua.LNumber:
keyStr = formatNumber(float64(k), config.encodeNumberPrecision)
keyStr = "\"" + keyStr + "\""
default:
return
}
pairs = append(pairs, kv{key: keyStr, value: value})
})
if encodeErr != nil {
return "", encodeErr
}
// 按键排序(保证输出顺序稳定)
sort.Slice(pairs, func(i, j int) bool {
return pairs[i].key < pairs[j].key
})
// 编码值
elements := make([]string, 0, len(pairs))
for _, p := range pairs {
valStr, err := encodeLuaValue(L, p.value, config, depth+1)
if err != nil {
return "", err
}
elements = append(elements, p.key+":"+valStr)
}
return "{" + strings.Join(elements, ",") + "}", nil
}
// formatNumber formats a number with the specified precision.
func formatNumber(n float64, precision int) string {
if precision <= 0 {
precision = 14
}
if precision > 14 {
precision = 14
}
// Check if it's an integer
if n == float64(int64(n)) && n >= -9007199254740992 && n <= 9007199254740992 {
return strconv.FormatInt(int64(n), 10)
}
// Use 'g' format for floating point
return strconv.FormatFloat(n, 'g', precision, 64)
}