""" rbxl_parser_v0.py — голый парсер Roblox Binary Level (.rbxl) v0. Эта первая итерация — только структура файла: - проверка signature ( RbxlFile: """Парсит .rbxl байты в RbxlFile. Бросает RbxlParseError на ошибке.""" if not blob.startswith(RBXL_SIGNATURE): raise RbxlParseError( f"signature mismatch: got {blob[:14].hex()}, " f"expected {RBXL_SIGNATURE.hex()}" ) stream = io.BytesIO(blob[len(RBXL_SIGNATURE):]) # Header: version (uint16), class_count (uint32), instance_count (uint32), reserved (8 bytes) version, class_count, instance_count = struct.unpack(' 0 else uncompressed_len) if compressed_len == 0: # Не сжато payload = payload_compressed else: # LZ4 raw block decompress try: payload = lz4.block.decompress(payload_compressed, uncompressed_size=uncompressed_len) except Exception as e: raise RbxlParseError( f"LZ4 decompress failed for chunk {name!r}: {e}" ) chunks.append(RbxlChunk(name, compressed_len, uncompressed_len, payload)) if name.startswith('END'): break return RbxlFile(header=header, chunks=chunks) def summarize(rbxl: RbxlFile) -> str: """Печатает компактный отчёт о структуре файла для smoke-test.""" lines = [] lines.append(f"=== rbxl file ===") lines.append(f" version: {rbxl.header.version}") lines.append(f" classes: {rbxl.header.class_count}") lines.append(f" instances: {rbxl.header.instance_count}") lines.append(f" chunks: {len(rbxl.chunks)}") lines.append("") lines.append(f"=== chunks ===") for c in rbxl.chunks: ratio = (c.compressed_len / c.uncompressed_len * 100) if c.uncompressed_len > 0 else 0 lines.append( f" {c.name:6s} compressed={c.compressed_len:>8d} uncompressed={c.uncompressed_len:>10d} " f"({ratio:5.1f}%) first 16 bytes: {c.payload[:16].hex()}" ) return "\n".join(lines) if __name__ == '__main__': import sys path = sys.argv[1] if len(sys.argv) > 1 else '/opt/rbxl-importer/reference_files/easy_obby.rbxl' with open(path, 'rb') as f: blob = f.read() print(f"file size: {len(blob)} bytes") rbxl = parse(blob) print(summarize(rbxl))