"""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\s+) | (?P\() | (?P\)) | (?P"(?:\\.|[^"\\])*") | (?P[^"\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"))