204 lines
5.1 KiB
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
|
|
}
|