feat: migrate to golang

This commit is contained in:
2026-07-19 20:01:38 +08:00
parent 302d4e6bb5
commit a2630df9e0
69 changed files with 4750 additions and 3369 deletions

View File

@ -0,0 +1,82 @@
// Package yamlutil wraps gopkg.in/yaml.v3 unmarshaling to tolerate duplicate
// mapping keys.
//
// yaml.v3 treats a duplicate key in the same mapping as a hard error
// ("mapping key ... already defined at line ..."), whereas PyYAML (used by
// the original Python implementation) silently accepts it, with the last
// occurrence's value winning. Real-world mihomo/clash configs — including,
// ironically, this project's own bundled default-core-config.yaml before it
// was fixed — are frequently hand-edited/merged from multiple sources and do
// contain duplicate keys. Unmarshal here supports both: well-formed YAML
// decodes exactly as gopkg.in/yaml.v3 would, and YAML with duplicate keys
// decodes using last-value-wins instead of erroring.
package yamlutil
import (
"fmt"
"gopkg.in/yaml.v3"
)
// Unmarshal decodes YAML data into out, tolerating duplicate mapping keys
// (last one wins), unlike yaml.Unmarshal.
func Unmarshal(data []byte, out interface{}) error {
var doc yaml.Node
if err := yaml.Unmarshal(data, &doc); err != nil {
return err
}
if len(doc.Content) == 0 {
return nil
}
root := doc.Content[0]
dedupeMappingKeys(root)
return root.Decode(out)
}
// dedupeMappingKeys walks the node tree, collapsing duplicate keys within
// each mapping so that only the last occurrence of each key survives (with
// its value), matching PyYAML/CPython dict-construction semantics. Recurses
// into sequences and mapping values so nested duplicates are handled too.
func dedupeMappingKeys(n *yaml.Node) {
if n == nil {
return
}
switch n.Kind {
case yaml.DocumentNode, yaml.SequenceNode:
for _, c := range n.Content {
dedupeMappingKeys(c)
}
case yaml.MappingNode:
type pair struct {
key *yaml.Node
value *yaml.Node
}
seen := make(map[string]int, len(n.Content)/2)
pairs := make([]pair, 0, len(n.Content)/2)
for i := 0; i+1 < len(n.Content); i += 2 {
key := n.Content[i]
value := n.Content[i+1]
// Matches yaml.v3's own duplicate-key comparison (Kind+Value).
id := fmt.Sprintf("%d:%s", key.Kind, key.Value)
if idx, ok := seen[id]; ok {
pairs[idx].value = value
} else {
seen[id] = len(pairs)
pairs = append(pairs, pair{key: key, value: value})
}
}
content := make([]*yaml.Node, 0, len(pairs)*2)
for _, p := range pairs {
dedupeMappingKeys(p.value)
content = append(content, p.key, p.value)
}
n.Content = content
}
}

View File

@ -0,0 +1,103 @@
package yamlutil
import (
"testing"
"gopkg.in/yaml.v3"
)
func TestUnmarshal_NoDuplicates(t *testing.T) {
data := []byte("a: 1\nb: 2\n")
var out map[string]interface{}
if err := Unmarshal(data, &out); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
}
if out["a"] != 1 || out["b"] != 2 {
t.Fatalf("unexpected result: %#v", out)
}
}
func TestUnmarshal_DuplicateKeyLastWins(t *testing.T) {
// Mirrors the real bug: a duplicate top-level key that gopkg.in/yaml.v3
// rejects outright, but PyYAML (and this package) tolerate by letting
// the later occurrence win.
data := []byte(`external-controller-cors:
allow-origins:
- "*"
allow-private-network: true
allow-origins:
- tauri://localhost
- https://yacd.metacubex.one
`)
// Confirm plain yaml.Unmarshal actually fails on this input, i.e. that
// the test is exercising the real bug.
var control map[string]interface{}
if err := yaml.Unmarshal(data, &control); err == nil {
t.Fatalf("expected plain yaml.Unmarshal to fail on duplicate keys, it didn't")
}
var out map[string]interface{}
if err := Unmarshal(data, &out); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
}
cors, ok := out["external-controller-cors"].(map[string]interface{})
if !ok {
t.Fatalf("expected external-controller-cors map, got %#v", out["external-controller-cors"])
}
origins, ok := cors["allow-origins"].([]interface{})
if !ok || len(origins) != 2 {
t.Fatalf("expected last allow-origins (2 entries) to win, got %#v", cors["allow-origins"])
}
if origins[0] != "tauri://localhost" {
t.Fatalf("expected last-occurrence value to win, got %#v", origins)
}
if cors["allow-private-network"] != true {
t.Fatalf("expected sibling key to survive dedupe, got %#v", cors)
}
}
func TestUnmarshal_NestedDuplicates(t *testing.T) {
data := []byte(`outer:
inner:
x: 1
x: 2
inner:
y: 3
`)
var out map[string]interface{}
if err := Unmarshal(data, &out); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
}
outer := out["outer"].(map[string]interface{})
inner := outer["inner"].(map[string]interface{})
if _, hasX := inner["x"]; hasX {
t.Fatalf("expected last 'inner' occurrence to fully replace the first, got %#v", inner)
}
if inner["y"] != 3 {
t.Fatalf("expected inner.y == 3, got %#v", inner)
}
}
func TestUnmarshal_Empty(t *testing.T) {
var out map[string]interface{}
if err := Unmarshal([]byte(""), &out); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
}
if out != nil {
t.Fatalf("expected nil map for empty input, got %#v", out)
}
}
func TestUnmarshal_Sequence(t *testing.T) {
data := []byte("- a: 1\n a: 2\n- b: 3\n")
var out []map[string]interface{}
if err := Unmarshal(data, &out); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
}
if len(out) != 2 || out[0]["a"] != 2 || out[1]["b"] != 3 {
t.Fatalf("unexpected result: %#v", out)
}
}