Files
NexusGate/agent-go/jsonutil.go

204 lines
5.1 KiB
Go

package main
import (
"bytes"
"encoding/json"
"math"
"regexp"
"strconv"
"strings"
)
// decodeJSON parses JSON the way JS JSON.parse + later property access behaves
// for this codebase: numbers keep their literal form (json.Number) so large
// counters never lose precision through float64.
func decodeJSON(data []byte) (interface{}, error) {
dec := json.NewDecoder(bytes.NewReader(data))
dec.UseNumber()
var v interface{}
if err := dec.Decode(&v); err != nil {
return nil, err
}
return v, nil
}
// marshalJSON renders compact JSON like JSON.stringify: UTF-8 output, no HTML
// escaping (Go's default would escape <, >, & which JS does not).
func marshalJSON(v interface{}) (string, error) {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
if err := enc.Encode(v); err != nil {
return "", err
}
return strings.TrimRight(buf.String(), "\n"), nil
}
// marshalIndentJSON renders like JSON.stringify(v, null, 2).
func marshalIndentJSON(v interface{}) (string, error) {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
if err := enc.Encode(v); err != nil {
return "", err
}
return strings.TrimRight(buf.String(), "\n"), nil
}
func asMap(v interface{}) (map[string]interface{}, bool) {
m, ok := v.(map[string]interface{})
return m, ok
}
func asSlice(v interface{}) []interface{} {
if s, ok := v.([]interface{}); ok {
return s
}
return nil
}
func asString(v interface{}) string {
if s, ok := v.(string); ok {
return s
}
return ""
}
// jsTruthy mirrors JS truthiness for JSON-decoded values.
func jsTruthy(v interface{}) bool {
switch t := v.(type) {
case nil:
return false
case bool:
return t
case string:
return t != ""
case json.Number:
f, err := t.Float64()
return err == nil && f != 0
case float64:
return t != 0
default:
return true // objects and arrays are always truthy in JS
}
}
// jsonNumberToInt converts a decoded JSON number to int (0 for anything else).
func jsonNumberToInt(v interface{}) int {
switch n := v.(type) {
case json.Number:
if i, err := n.Int64(); err == nil {
return int(i)
}
if f, err := n.Float64(); err == nil {
return int(f)
}
case float64:
return int(n)
}
return 0
}
const maxSafeInteger = 9007199254740991 // 2^53 - 1
var intStringRe = regexp.MustCompile(`^-?\d+$`)
// jsNumberString evaluates JS Number(string). Returns (value, ok) where ok is
// false when JS would produce NaN. It supports the forms Number() supports:
// decimal/exponent, hex 0x, binary 0b, octal 0o, surrounding whitespace.
func jsNumberString(s string) (float64, bool) {
t := strings.TrimSpace(s)
if t == "" {
return 0, true // Number("") and Number(" ") are 0 in JS
}
if t == "Infinity" || t == "+Infinity" {
return math.Inf(1), true
}
if t == "-Infinity" {
return math.Inf(-1), true
}
// Hex/binary/octal literals (no sign allowed before the prefix in JS).
if len(t) > 2 && t[0] == '0' {
var base int
switch t[1] {
case 'x', 'X':
base = 16
case 'b', 'B':
base = 2
case 'o', 'O':
base = 8
}
if base != 0 {
n, err := strconv.ParseUint(t[2:], base, 64)
if err != nil {
return 0, false
}
return float64(n), true
}
}
f, err := strconv.ParseFloat(t, 64)
if err != nil {
return 0, false
}
return f, true
}
// jsStatValue validates one stats counter value with the exact semantics of
// parseUsageStats in agent.js. raw is the JSON literal of the `value` field
// (nil RawMessage when the field is absent). Returns (value, reason, valid);
// reason is the Chinese diagnostic fragment JS embeds in its error message.
func jsStatValue(raw json.RawMessage) (int64, string, bool) {
if len(raw) == 0 {
return 0, "", true // missing field: protobuf default zero, JS uses 0
}
text := strings.TrimSpace(string(raw))
if text == "null" {
return 0, "格式 object", false
}
if text == "true" {
return 1, "", true // Number(true) === 1
}
if text == "false" {
return 0, "", true
}
if strings.HasPrefix(text, `"`) {
var s string
if err := json.Unmarshal(raw, &s); err != nil {
return 0, "格式 string", false
}
if s == "" {
return 0, "格式 string", false // JS rejects value === '' explicitly
}
if intStringRe.MatchString(s) {
if strings.HasPrefix(s, "-") {
// "-0" is valid in JS (Number("-0") is -0, not < 0).
n, err := strconv.ParseInt(s, 10, 64)
if err == nil && n == 0 {
return 0, "", true
}
return 0, "负数", false
}
n, err := strconv.ParseUint(s, 10, 64)
if err != nil || n > maxSafeInteger {
return 0, "超出安全整数范围", false
}
return int64(n), "", true
}
f, ok := jsNumberString(s)
if !ok || math.IsNaN(f) || math.IsInf(f, 0) || f != math.Trunc(f) || f < 0 || f > maxSafeInteger {
return 0, "格式 string", false
}
return int64(f), "", true
}
if strings.HasPrefix(text, "{") || strings.HasPrefix(text, "[") {
return 0, "格式 object", false
}
// Bare JSON number.
f, err := strconv.ParseFloat(text, 64)
if err != nil || math.IsNaN(f) || math.IsInf(f, 0) || f != math.Trunc(f) || f < 0 || f > maxSafeInteger {
return 0, "格式 number", false
}
return int64(f), "", true
}