Files
maenekoandClaude Sonnet 5.5 18106115f7
ci / check (push) Successful in 6m6s
Start the Go rewrite: jsjson, awg-ctrl port, differential bench
- internal/jsjson: JSON.stringify-compatible encoder and order-preserving decoder,
  tested against fixtures produced by Node itself
- internal/ctrl: awg-ctrl in Go (params, vpn:// keys, users, peers, sync, 3.1
  upgrade, CAKE shaping, metrics, monitor job, Express-compatible HTTP layer)
- internal/iauth, internal/logx: internal Ed25519 tokens and logging
- tools/diffbench: Docker bench with stubbed awg/ip/tc/nft that runs one scenario
  against the TS and Go implementations for comparison

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 15:27:05 +03:00

394 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package jsjson повторяет JSON.stringify из JavaScript байт в байт.
//
// Зачем: выход Node-версии зашит в форматы, которые проверяют клиенты и хэши —
// распакованное содержимое vpn://, rev конфига sen:// (sha256 от строки),
// подписанные тела ответов /sub/v1, /api/profile. encoding/json отличается
// (экранирует < > & и U+2028/2029, иначе пишет числа, добавляет перевод
// строки, сортирует ключи map), поэтому здесь свой сериализатор.
//
// Порядок ключей — порядок объявления: Object хранит пары как есть, у структур
// берутся поля по порядку (тег json задаёт имя, "-" — пропуск, omitempty —
// пропуск нулевого значения, как undefined в JS).
package jsjson
import (
"bytes"
"encoding/json"
"fmt"
"math"
"reflect"
"strconv"
"strings"
"unicode/utf8"
)
// KV — пара ключ/значение упорядоченного объекта.
type KV struct {
Key string
Val any
}
// Object — JS-объект с сохранённым порядком ключей.
type Object []KV
// Get возвращает значение по ключу.
func (o Object) Get(key string) (any, bool) {
for _, kv := range o {
if kv.Key == key {
return kv.Val, true
}
}
return nil, false
}
// Raw — уже готовый JSON, вставляется как есть (без проверки).
type Raw []byte
// Marshal — аналог JSON.stringify(v).
func Marshal(v any) ([]byte, error) {
var b bytes.Buffer
if err := encode(&b, reflect.ValueOf(v), "", ""); err != nil {
return nil, err
}
return b.Bytes(), nil
}
// MarshalIndent — аналог JSON.stringify(v, null, indent) (у нас indent = " ").
func MarshalIndent(v any, indent string) ([]byte, error) {
var b bytes.Buffer
if err := encode(&b, reflect.ValueOf(v), indent, ""); err != nil {
return nil, err
}
return b.Bytes(), nil
}
// MustMarshal — для значений, которые заведомо сериализуемы.
func MustMarshal(v any) []byte {
b, err := Marshal(v)
if err != nil {
panic(err)
}
return b
}
var (
objectType = reflect.TypeOf(Object(nil))
rawType = reflect.TypeOf(Raw(nil))
jsonRaw = reflect.TypeOf(json.RawMessage(nil))
jsonNumber = reflect.TypeOf(json.Number(""))
)
func encode(b *bytes.Buffer, v reflect.Value, indent, cur string) error {
if !v.IsValid() {
b.WriteString("null")
return nil
}
switch v.Type() {
case objectType:
return encodeObject(b, v.Interface().(Object), indent, cur)
case rawType:
b.Write(v.Bytes())
return nil
case jsonRaw:
raw := v.Bytes()
if len(raw) == 0 {
b.WriteString("null")
return nil
}
// JSON.stringify(JSON.parse(x)): нормализуем, не доверяя форме исходника.
val, err := Decode(raw)
if err != nil {
return err
}
return encode(b, reflect.ValueOf(val), indent, cur)
case jsonNumber:
f, err := strconv.ParseFloat(v.String(), 64)
if err != nil {
return err
}
b.WriteString(Number(f))
return nil
}
switch v.Kind() {
case reflect.Interface, reflect.Pointer:
if v.IsNil() {
b.WriteString("null")
return nil
}
return encode(b, v.Elem(), indent, cur)
case reflect.Bool:
if v.Bool() {
b.WriteString("true")
} else {
b.WriteString("false")
}
case reflect.String:
writeString(b, v.String())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
b.WriteString(Number(float64(v.Int())))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
b.WriteString(Number(float64(v.Uint())))
case reflect.Float32, reflect.Float64:
b.WriteString(Number(v.Float()))
case reflect.Slice:
if v.IsNil() {
// nil-срез — это пустой массив, а не null: в JS неоткуда взяться null вместо []
b.WriteString("[]")
return nil
}
fallthrough
case reflect.Array:
return encodeArray(b, v, indent, cur)
case reflect.Map:
// В JS порядок ключей — порядок вставки; у Go-map его нет. Разрешаем только
// map[string]..., сортируя ключи, — вызывающий отвечает за то, что это совпадает.
if v.Type().Key().Kind() != reflect.String {
return fmt.Errorf("jsjson: map key must be string, got %s", v.Type().Key())
}
keys := v.MapKeys()
ks := make([]string, len(keys))
for i, k := range keys {
ks[i] = k.String()
}
sortStrings(ks)
obj := make(Object, 0, len(ks))
for _, k := range ks {
obj = append(obj, KV{k, v.MapIndex(reflect.ValueOf(k).Convert(v.Type().Key())).Interface()})
}
return encodeObject(b, obj, indent, cur)
case reflect.Struct:
return encodeObject(b, structObject(v), indent, cur)
default:
return fmt.Errorf("jsjson: unsupported kind %s", v.Kind())
}
return nil
}
func encodeArray(b *bytes.Buffer, v reflect.Value, indent, cur string) error {
n := v.Len()
if n == 0 {
b.WriteString("[]")
return nil
}
b.WriteByte('[')
inner := cur + indent
for i := 0; i < n; i++ {
if i > 0 {
b.WriteByte(',')
}
if indent != "" {
b.WriteByte('\n')
b.WriteString(inner)
}
// undefined/функция в массиве — null; у нас таких нет, кроме nil
if err := encode(b, v.Index(i), indent, inner); err != nil {
return err
}
}
if indent != "" {
b.WriteByte('\n')
b.WriteString(cur)
}
b.WriteByte(']')
return nil
}
// Omit — значение, которое JSON.stringify пропускает (undefined): ключ не выводится.
type omit struct{}
// Undefined — маркер «ключа нет», как undefined в JS-объекте.
var Undefined = omit{}
func encodeObject(b *bytes.Buffer, o Object, indent, cur string) error {
first := true
inner := cur + indent
b.WriteByte('{')
for _, kv := range o {
if _, skip := kv.Val.(omit); skip {
continue
}
if !first {
b.WriteByte(',')
}
first = false
if indent != "" {
b.WriteByte('\n')
b.WriteString(inner)
}
writeString(b, kv.Key)
b.WriteByte(':')
if indent != "" {
b.WriteByte(' ')
}
if err := encode(b, reflect.ValueOf(kv.Val), indent, inner); err != nil {
return err
}
}
if !first && indent != "" {
b.WriteByte('\n')
b.WriteString(cur)
}
b.WriteByte('}')
return nil
}
func structObject(v reflect.Value) Object {
t := v.Type()
obj := make(Object, 0, t.NumField())
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if !f.IsExported() {
continue
}
name := f.Name
omitEmpty := false
if tag, ok := f.Tag.Lookup("json"); ok {
if tag == "-" {
continue
}
parts := strings.Split(tag, ",")
if parts[0] != "" {
name = parts[0]
}
for _, p := range parts[1:] {
if p == "omitempty" {
omitEmpty = true
}
}
}
fv := v.Field(i)
if omitEmpty && isEmpty(fv) {
continue
}
obj = append(obj, KV{name, fv.Interface()})
}
return obj
}
func isEmpty(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Pointer:
return v.IsNil()
}
return false
}
// writeString экранирует строку как JSON.stringify (ES2019): " \ и управляющие
// символы; \b \f \n \r \t коротко, прочие < 0x20 — \u00xx строчными. Не трогает
// / < > & и U+2028/2029. Неверный UTF-8 заменяется на U+FFFD (в JS его не бывает).
func writeString(b *bytes.Buffer, s string) {
b.WriteByte('"')
for i := 0; i < len(s); {
c := s[i]
if c < utf8.RuneSelf {
switch c {
case '"':
b.WriteString(`\"`)
case '\\':
b.WriteString(`\\`)
case '\b':
b.WriteString(`\b`)
case '\f':
b.WriteString(`\f`)
case '\n':
b.WriteString(`\n`)
case '\r':
b.WriteString(`\r`)
case '\t':
b.WriteString(`\t`)
default:
if c < 0x20 {
fmt.Fprintf(b, `\u%04x`, c)
} else {
b.WriteByte(c)
}
}
i++
continue
}
r, size := utf8.DecodeRuneInString(s[i:])
if r == utf8.RuneError && size == 1 {
b.WriteString("�")
} else {
b.WriteString(s[i : i+size])
}
i += size
}
b.WriteByte('"')
}
// Number форматирует число как String(x) в JS (Number::toString), а NaN/±Infinity —
// как null, что делает JSON.stringify.
func Number(x float64) string {
if math.IsNaN(x) || math.IsInf(x, 0) {
return "null"
}
if x == 0 {
return "0" // и -0
}
neg := x < 0
if neg {
x = -x
}
// Кратчайшие цифры, однозначно восстанавливающие x: d.ddddde±n
e := strconv.FormatFloat(x, 'e', -1, 64)
mant, expStr, _ := strings.Cut(e, "e")
digits := strings.Replace(mant, ".", "", 1)
exp, _ := strconv.Atoi(expStr)
k := len(digits) // число значащих цифр
n := exp + 1 // позиция десятичной точки (ECMA-262 Number::toString)
var out string
switch {
case k <= n && n <= 21:
out = digits + strings.Repeat("0", n-k)
case 0 < n && n <= 21:
out = digits[:n] + "." + digits[n:]
case -6 < n && n <= 0:
out = "0." + strings.Repeat("0", -n) + digits
default:
sign := "+"
if n-1 < 0 {
sign = "-"
}
ee := n - 1
if ee < 0 {
ee = -ee
}
if k == 1 {
out = digits + "e" + sign + strconv.Itoa(ee)
} else {
out = digits[:1] + "." + digits[1:] + "e" + sign + strconv.Itoa(ee)
}
}
if neg {
return "-" + out
}
return out
}
func sortStrings(s []string) {
// вставками: ключей мало, а сортировка строк в JS-порядке — по UTF-16, что для
// наших ASCII-ключей совпадает с побайтовой
for i := 1; i < len(s); i++ {
for j := i; j > 0 && s[j] < s[j-1]; j-- {
s[j], s[j-1] = s[j-1], s[j]
}
}
}
// Get2 — Get без признака наличия (nil, если ключа нет); для цепочек разбора.
func (o Object) Get2(key string) any {
v, _ := o.Get(key)
return v
}