From 9db68c9d5264c62ddfb39579b6f010bf709a3e02 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sat, 17 Oct 2026 15:52:59 +0200 Subject: [PATCH] fix: refuse a yaml mapping that repeats a key --- scripts/safeparse.py | 26 +++++++++++++++++++++++++- tests/test_audit_regressions.py | 13 +++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/scripts/safeparse.py b/scripts/safeparse.py index 53d60a38..21b927e9 100644 --- a/scripts/safeparse.py +++ b/scripts/safeparse.py @@ -60,7 +60,31 @@ def _pick_yaml_loader(): """ if yaml is None: return None - return getattr(yaml, "CSafeLoader", None) or yaml.SafeLoader + base = getattr(yaml, "CSafeLoader", None) or yaml.SafeLoader + + class _UniqueKeyLoader(base): + """Refuses a mapping that names a key twice. + + PyYAML keeps the last value without a word: the registry once + carried two target_source keys for Recalbox and read only one. Only + a mapping that came out shorter than its source is re-read. No data + here uses merge keys (<<), which would count as repeats too. + """ + + def construct_mapping(self, node, deep=False): + mapping = super().construct_mapping(node, deep=deep) + if len(mapping) < len(node.value): + seen: set = set() + for key_node, _value in node.value: + key = self.construct_object(key_node, deep=deep) + if key in seen: + raise yaml.constructor.ConstructorError( + None, None, f"duplicate key {key!r}", key_node.start_mark + ) + seen.add(key) + return mapping + + return _UniqueKeyLoader _YAML_LOADER = _pick_yaml_loader() diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index 7d8ac7ab..b43aeb1a 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -2846,5 +2846,18 @@ class ADataDirectoryIsWalkedOnce(unittest.TestCase): self.assertIn(first[0], expected) self.assertEqual(second[0], str(cache / "b" / "other.bin")) + +class ADuplicateYamlKeyIsRefused(unittest.TestCase): + """PyYAML kept the last of two equal keys: the registry carried two + target_source keys for Recalbox and nothing noticed.""" + + def test_the_loader_names_the_repeated_key(self): + from safeparse import yaml_load # noqa: PLC0415 + + with self.assertRaises(yaml.constructor.ConstructorError) as caught: + yaml_load("platform: x\ntarget_source: a\ntarget_source: b\n") + self.assertIn("duplicate key 'target_source'", str(caught.exception)) + self.assertEqual(yaml_load("a: &x {b: 1}\nc: *x\n"), {"a": {"b": 1}, "c": {"b": 1}}) + if __name__ == "__main__": unittest.main()