feat: migrate to golang
This commit is contained in:
82
internal/yamlutil/yamlutil.go
Normal file
82
internal/yamlutil/yamlutil.go
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user