Files
Abdessamad Derraz 3b8f2d75d5 perf: read yaml through the c loader
Loading the emulator profiles is the most expensive step of every
command here, and all forty call sites used the pure-Python scanner
while libyaml sat unused in the same wheel. One shared yaml_load picks
the C loader when pyyaml ships it: the 375 profiles parse in 0.18s
instead of 1.39s, and verify --platform retroarch drops from 2.17s to
0.73s. The loader class is the same restricted one safe_load uses.

es_bios.xml was parsed straight from the network while install.py
already refused a document declaring entities; both now share one
guard. Scrapers reach it through a single path bootstrap in the
package rather than two ad-hoc ones.
2026-08-11 00:55:16 +02:00

92 lines
2.5 KiB
Python

"""Parser for Logiqx XML DAT format.
Parses files like No-Intro daily packs and TOSEC releases which use:
<datafile>
<header><name>...</name><version>...</version></header>
<game name="[BIOS] Game Boy Advance (World)">
<description>...</description>
<rom name="..." size="16384" crc="..." md5="..." sha1="..."/>
</game>
</datafile>
"""
from __future__ import annotations
import xml.etree.ElementTree as ET
from dataclasses import dataclass, field
from common import parse_untrusted_xml
@dataclass
class LogiqxRom:
"""A ROM entry from a Logiqx DAT file."""
game: str
description: str
name: str
size: int
crc32: str
md5: str
sha1: str
@dataclass
class LogiqxDat:
"""A parsed Logiqx DAT file."""
name: str = ""
version: str = ""
roms: list[LogiqxRom] = field(default_factory=list)
def parse_logiqx(content: str | bytes) -> LogiqxDat:
"""Parse Logiqx XML DAT content.
Handles both <game> and <machine> entries, and games with
multiple <rom> children. Content declaring XML entities is
rejected: DAT files never define them, and expanding entities
from untrusted packs opens entity-expansion attacks.
"""
root = parse_untrusted_xml(content, "DAT files")
dat = LogiqxDat()
header = root.find("header")
if header is not None:
dat.name = header.findtext("name", "").strip()
dat.version = header.findtext("version", "").strip()
for game in list(root.iter("game")) + list(root.iter("machine")):
game_name = game.get("name", "")
description = game.findtext("description", "").strip()
for rom in game.iter("rom"):
name = rom.get("name", "")
if not name:
continue
try:
size = int(rom.get("size", "0"))
except ValueError:
size = 0
dat.roms.append(
LogiqxRom(
game=game_name,
description=description or game_name,
name=name,
size=size,
crc32=rom.get("crc", "").lower(),
md5=rom.get("md5", "").lower(),
sha1=rom.get("sha1", "").lower(),
)
)
return dat
def validate_logiqx_format(content: str | bytes) -> bool:
"""Validate that content parses as a Logiqx DAT with rom entries."""
try:
dat = parse_logiqx(content)
except ET.ParseError:
return False
return bool(dat.roms)