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