mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-17 00:43:29 -05:00
fix: keep each recalbox block and its comments
This commit is contained in:
1 parent
42124618e9
commit
ff243c9978
3 files changed
+100
-19
No files matched your search
@@ -13,7 +13,7 @@ from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from xml.etree.ElementTree import ParseError
|
||||
from xml.etree.ElementTree import Comment, ParseError
|
||||
from xml.sax.saxutils import quoteattr
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
@@ -123,27 +123,32 @@ class Exporter(BaseExporter):
|
||||
]
|
||||
# Recalbox's own file is the base, so an element our transcription
|
||||
# does not carry (added or renamed after a scrape) stays as written,
|
||||
# and its system order with it.
|
||||
theirs: list[tuple[str, str, list[dict[str, str]]]] = []
|
||||
# and its system order with it. A platform can hold several blocks
|
||||
# (Pentagon and Scorpion are both zxspectrum), and an entry the
|
||||
# maintainer commented out stays commented out.
|
||||
theirs: list[tuple[str, str, list[dict[str, str] | str]]] = []
|
||||
original = originals.get(self.native_filename(), "")
|
||||
if original:
|
||||
try:
|
||||
root = parse_untrusted_xml(original, self.native_filename())
|
||||
root = parse_untrusted_xml(original, self.native_filename(), comments=True)
|
||||
except (ParseError, ValueError):
|
||||
root = None
|
||||
if root is not None:
|
||||
theirs = [
|
||||
(s.get("platform", ""), s.get("fullname", ""),
|
||||
[dict(b.attrib) for b in s.iter("bios")])
|
||||
(s.get("platform", ""), s.get("fullname", ""), [
|
||||
f"<!--{child.text}-->" if child.tag is Comment else dict(child.attrib)
|
||||
for child in s
|
||||
if child.tag is Comment or child.tag == "bios"
|
||||
])
|
||||
for s in root.iter("system")
|
||||
]
|
||||
|
||||
ours: dict[str, tuple[NativeSystem, dict[str, str]]] = {}
|
||||
ours: dict[str, tuple[NativeSystem, dict[str, tuple[NativeFile, str]]]] = {}
|
||||
for system in systems.values():
|
||||
writable = [fe for fe in system.files if self.writable(fe)]
|
||||
if writable:
|
||||
ours[system.native_id] = (system, {
|
||||
self._path(fe): self._bios_element(fe, system.native_id)
|
||||
self._path(fe): (fe, self._bios_element(fe, system.native_id))
|
||||
for fe in writable
|
||||
})
|
||||
|
||||
@@ -158,18 +163,37 @@ class Exporter(BaseExporter):
|
||||
lines.extend(body)
|
||||
lines.append(" </system>")
|
||||
|
||||
for platform, fullname, bios in theirs:
|
||||
if platform not in ours:
|
||||
emit(platform, fullname, [element(attrs) for attrs in bios])
|
||||
continue
|
||||
system, elements = ours.pop(platform)
|
||||
body = [elements.pop(attrs.get("path", ""), None) or element(attrs)
|
||||
for attrs in bios]
|
||||
body.extend(elements.values())
|
||||
emit(platform, self.display_name(system), body)
|
||||
# Each block of theirs corrects the entries it holds.
|
||||
blocks: list[tuple[str, str, list[str]]] = []
|
||||
for platform, fullname, children in theirs:
|
||||
elements = ours.get(platform, (None, {}))[1]
|
||||
body: list[str] = []
|
||||
for child in children:
|
||||
if isinstance(child, str):
|
||||
body.append(f" {child}")
|
||||
continue
|
||||
mine = elements.pop(child.get("path", ""), None)
|
||||
body.append(mine[1] if mine else element(child))
|
||||
blocks.append((platform, fullname, body))
|
||||
|
||||
# An addition joins the block named like its file, else the first
|
||||
# block of its platform.
|
||||
first: dict[str, int] = {}
|
||||
named: dict[tuple[str, str], int] = {}
|
||||
for index, (platform, fullname, _) in enumerate(blocks):
|
||||
first.setdefault(platform, index)
|
||||
named.setdefault((platform, fullname), index)
|
||||
for platform in [p for p in ours if p in first]:
|
||||
_, elements = ours.pop(platform)
|
||||
for fe, rendered in elements.values():
|
||||
key = (platform, str(fe.native("native_name", "")))
|
||||
blocks[named.get(key, first[platform])][2].append(rendered)
|
||||
|
||||
for platform, fullname, body in blocks:
|
||||
emit(platform, fullname, body)
|
||||
for platform in sorted(ours):
|
||||
system, elements = ours[platform]
|
||||
emit(platform, self.display_name(system), list(elements.values()))
|
||||
emit(platform, self.display_name(system), [r for _, r in elements.values()])
|
||||
|
||||
lines.append("</biosList>")
|
||||
lines.append("")
|
||||
|
||||
@@ -13,7 +13,9 @@ except ImportError: # optional at import time
|
||||
yaml = None
|
||||
|
||||
|
||||
def parse_untrusted_xml(content: str | bytes, label: str = "XML") -> ET.Element:
|
||||
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
|
||||
@@ -25,10 +27,14 @@ def parse_untrusted_xml(content: str | bytes, label: str = "XML") -> ET.Element:
|
||||
The check targets <!ENTITY rather than <!DOCTYPE because Logiqx DATs
|
||||
legitimately carry a doctype pointing at logiqx.com, and ElementTree does
|
||||
not resolve external entities, so a doctype alone fetches nothing.
|
||||
`comments` keeps comments in the tree, for a file rewritten in place.
|
||||
"""
|
||||
text = content if isinstance(content, str) else content.decode("utf-8", "replace")
|
||||
if "<!ENTITY" in text.upper():
|
||||
raise ValueError(f"XML entity declarations are not allowed in {label}")
|
||||
if comments:
|
||||
parser = ET.XMLParser(target=ET.TreeBuilder(insert_comments=True))
|
||||
return ET.fromstring(content, parser=parser)
|
||||
return ET.fromstring(content)
|
||||
|
||||
def require_yaml():
|
||||
|
||||
Reference in new issue
Block a user