"""Parsers for input the repo does not control. Upstream DATs and scraped documents are data, never instructions, and the parsers here are the boundary that keeps it that way.""" from __future__ import annotations import xml.etree.ElementTree as ET from types import ModuleType from typing import Any, TextIO try: import yaml except ImportError: # optional at import time yaml = None def parse_untrusted_xml( content: str | bytes, label: str = "XML", comments: bool = False ) -> ET.Element: """Parse XML fetched from a third party. ElementTree expands internal entities, so a document that declares them can make the parser build a payload far larger than the bytes downloaded. Nothing this project reads (DAT packs, es_bios.xml, Emulators.xml) ever declares one, so a declaration is grounds to refuse the document rather than something to expand carefully. The check targets ModuleType: """Import and return yaml, exiting if PyYAML is not installed.""" try: import yaml as _yaml return _yaml except ImportError: import sys print("Error: PyYAML required (pip install pyyaml)", file=sys.stderr) sys.exit(1) def _pick_yaml_loader() -> type[yaml.constructor.SafeConstructor] | None: """Prefer the libyaml loader when the wheel ships it. Reading the emulator profiles is the single most expensive step of every command here: 375 files, and the pure-Python scanner accounts for roughly 70% of a verify run. The C loader parses the same documents about eight times faster and pyyaml only exposes it when libyaml was available at build time, so the pure-Python class stays as the fallback. """ if yaml is None: return None 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: yaml.MappingNode, deep: bool = False ) -> dict: 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() def yaml_load(stream: str | TextIO) -> Any: """Parse YAML from a string or file object, safely and quickly.""" if yaml is None: return require_yaml() # exits with the install hint return yaml.load(stream, Loader=_YAML_LOADER)