Files
psp-preservation-tool/app/dat_parser.py
T
2026-09-06 22:30:13 -04:00

129 lines
3.6 KiB
Python

"""Parser for clrmamepro (.dat) files.
Handles the No-Intro / Redump text DAT format::
clrmamepro (
name "Sony - PlayStation Portable"
version "2021.10.24"
)
game (
name "WipEout Pure (Europe)"
region "Europe"
serial "ULES-00011"
rom ( name "WipEout Pure (Europe).iso" size 123 crc ABCD1234 md5 ... sha1 ... )
)
``parse_dat_text`` returns ``(header, entries)``. Each entry is a dict with
scalar keys (``name``, ``description``, ``region``, ``serial``, ...) and
repeated nested blocks exposed as lists under their block key (``rom``).
"""
import re
from pathlib import Path
_TOKEN_RE = re.compile(
r"""
(?P<ws>\s+)
| (?P<lparen>\()
| (?P<rparen>\))
| (?P<string>"(?:\\.|[^"\\])*")
| (?P<ident>[^"\s()]+)
""",
re.VERBOSE,
)
class _Token:
__slots__ = ("kind", "value")
def __init__(self, kind, value):
self.kind = kind
self.value = value
def _tokenize(text):
tokens = []
for match in _TOKEN_RE.finditer(text):
kind = match.lastgroup
value = match.group()
if kind == "ws":
continue
if kind == "string":
value = value[1:-1].replace('\\"', '"').replace("\\\\", "\\")
kind = "string"
elif kind == "lparen":
kind = "lparen"
value = "("
elif kind == "rparen":
kind = "rparen"
value = ")"
else:
kind = "ident"
tokens.append(_Token(kind, value))
return tokens
class _Parser:
def __init__(self, tokens):
self._tokens = tokens
self._pos = 0
def _peek(self):
return self._tokens[self._pos] if self._pos < len(self._tokens) else None
def _next(self):
token = self._peek()
self._pos += 1
return token
def parse_block(self, name=None):
if name is None:
name_token = self._next()
if name_token is None or name_token.kind != "ident":
raise ValueError(f"expected block name at token {self._pos}")
name = name_token.value
lparen = self._next()
if lparen is None or lparen.kind != "lparen":
raise ValueError(f"expected '(' after '{name}'")
block = {"_type": name}
while True:
token = self._peek()
if token is None:
raise ValueError(f"unexpected EOF inside block '{name}'")
if token.kind == "rparen":
self._next()
break
if token.kind != "ident":
raise ValueError(f"unexpected token '{token.value}' in block '{name}'")
key = self._next().value
nxt = self._peek()
if nxt is not None and nxt.kind == "lparen":
child = self.parse_block(name=key)
block.setdefault(key, []).append(child)
else:
value = self._next()
if value is None:
raise ValueError(f"expected value for '{key}' in block '{name}'")
block[key] = value.value
return block
def parse_dat_text(text):
tokens = _tokenize(text)
parser = _Parser(tokens)
header = None
entries = []
while parser._peek() is not None:
block = parser.parse_block()
kind = block.get("_type")
if kind == "clrmamepro":
header = block
elif kind in ("game", "machine"):
entries.append(block)
# ignore any other top-level blocks
return header, entries
def parse_dat_file(path):
return parse_dat_text(Path(path).read_text(encoding="utf-8-sig"))