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
This commit is contained in:
2026-10-14 13:27:24 +00:00
parent 53d5a08bbd
commit 59e076c1de
28 changed files with 4960 additions and 0 deletions
+86
View File
@@ -0,0 +1,86 @@
package jsjson
import (
"bytes"
"encoding/json"
"errors"
"io"
)
// Decode разбирает JSON с сохранением порядка ключей: объекты — Object, массивы —
// []any, числа — float64, как JSON.parse. Нужен, чтобы JSON.stringify(JSON.parse(x))
// воспроизводился точно (например, сохранённый профиль сервера с ключами awg).
func Decode(data []byte) (any, error) {
d := json.NewDecoder(bytes.NewReader(data))
d.UseNumber()
v, err := decodeValue(d)
if err != nil {
return nil, err
}
if _, err := d.Token(); err != io.EOF {
return nil, errors.New("jsjson: trailing data")
}
return v, nil
}
func decodeValue(d *json.Decoder) (any, error) {
t, err := d.Token()
if err != nil {
return nil, err
}
switch x := t.(type) {
case json.Delim:
switch x {
case '{':
obj := Object{}
for d.More() {
kt, err := d.Token()
if err != nil {
return nil, err
}
key, ok := kt.(string)
if !ok {
return nil, errors.New("jsjson: object key is not a string")
}
val, err := decodeValue(d)
if err != nil {
return nil, err
}
// повторный ключ в JS перезаписывает значение, оставаясь на первом месте
replaced := false
for i := range obj {
if obj[i].Key == key {
obj[i].Val = val
replaced = true
break
}
}
if !replaced {
obj = append(obj, KV{key, val})
}
}
if _, err := d.Token(); err != nil {
return nil, err
}
return obj, nil
case '[':
arr := []any{}
for d.More() {
val, err := decodeValue(d)
if err != nil {
return nil, err
}
arr = append(arr, val)
}
if _, err := d.Token(); err != nil {
return nil, err
}
return arr, nil
}
return nil, errors.New("jsjson: unexpected delimiter")
case json.Number:
return x.Float64()
default:
return x, nil // string, bool, nil
}
}
+393
View File
@@ -0,0 +1,393 @@
// 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
}
+84
View File
@@ -0,0 +1,84 @@
package jsjson
import (
"encoding/json"
"os"
"testing"
)
type fixture struct {
Strings []struct{ In, Out string } `json:"strings"`
Numbers []struct {
In json.RawMessage `json:"in"`
Out string `json:"out"`
Str string `json:"str"`
} `json:"numbers"`
Objects []struct {
In json.RawMessage `json:"in"`
Compact string `json:"compact"`
Indent2 string `json:"indent2"`
} `json:"objects"`
}
// Эталон — вывод JSON.stringify самого Node (tools/golden/jsjson.mjs).
func TestAgainstNode(t *testing.T) {
raw, err := os.ReadFile("../../testdata/golden/jsjson.json")
if err != nil {
t.Fatal(err)
}
var f fixture
if err := json.Unmarshal(raw, &f); err != nil {
t.Fatal(err)
}
for _, c := range f.Strings {
if got := string(MustMarshal(c.In)); got != c.Out {
t.Errorf("string %q: got %s want %s", c.In, got, c.Out)
}
}
for _, c := range f.Numbers {
v, err := Decode(c.In)
if err != nil {
t.Fatal(err)
}
if s, ok := v.(string); ok { // NaN / Infinity переданы строкой
_ = s
continue
}
x := v.(float64)
if got := Number(x); got != c.Str {
t.Errorf("String(%s): got %s want %s", c.In, got, c.Str)
}
if got := string(MustMarshal(x)); got != c.Out {
t.Errorf("JSON.stringify(%s): got %s want %s", c.In, got, c.Out)
}
}
for _, c := range f.Objects {
v, err := Decode(c.In)
if err != nil {
t.Fatal(err)
}
if got := string(MustMarshal(v)); got != c.Compact {
t.Errorf("compact:\n got %s\nwant %s", got, c.Compact)
}
got, _ := MarshalIndent(v, " ")
if string(got) != c.Indent2 {
t.Errorf("indent:\n got %s\nwant %s", got, c.Indent2)
}
}
}
func TestStructOrderAndOmit(t *testing.T) {
type S struct {
B string `json:"b"`
A int `json:"a"`
Opt string `json:"opt,omitempty"`
Skip string `json:"-"`
}
if got := string(MustMarshal(S{B: "x", A: 1})); got != `{"b":"x","a":1}` {
t.Fatal(got)
}
o := Object{{"x", 1}, {"gone", Undefined}, {"y", []string(nil)}}
if got := string(MustMarshal(o)); got != `{"x":1,"y":[]}` {
t.Fatal(got)
}
}