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 }