Files
ss/internal/yamlutil/yamlutil.go
2026-07-19 20:01:38 +08:00

83 lines
2.4 KiB
Go

// 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
}
}