129 lines
3.6 KiB
Python
129 lines
3.6 KiB
Python
"""Parser for clrmamepro (.dat) files.
|
|
|
|
Handles the clrmamepro text DAT format used by Redump and No-Intro::
|
|
|
|
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"))
|