Created
January 8, 2026 04:29
-
-
Save sargunv/c8f83311083a13d04571ce4edc7e3abc to your computer and use it in GitHub Desktop.
Identify an Xbox game using certificate metadata
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Xbox XISO Game Identifier Extractor | |
| Extracts game identification data directly from .xiso.iso files | |
| without needing to extract them first. | |
| Usage: | |
| python xbox_xiso_id.py <xiso_file> | |
| python xbox_xiso_id.py -j <xiso_file> | |
| python xbox_xiso_id.py -j <xiso_file1> <xiso_file2> <xiso_file3> | |
| """ | |
| import argparse | |
| import json | |
| import struct | |
| import sys | |
| from pathlib import Path | |
| from dataclasses import dataclass, asdict | |
| from typing import Optional | |
| import datetime | |
| XISO_MAGIC = b"MICROSOFT*XBOX*MEDIA" | |
| XBE_MAGIC = b"XBEH" | |
| SECTOR_SIZE = 2048 | |
| @dataclass | |
| class XboxGameInfo: | |
| title_id_hex: str | |
| title_id_formatted: str | |
| publisher_code: str | |
| game_number: int | |
| title: str | |
| region: str | |
| region_flags: int | |
| version: int | |
| disc_number: int | |
| build_timestamp: Optional[datetime.datetime] | |
| xbe_path: str | |
| def __str__(self) -> str: | |
| lines = [ | |
| f"Title ID: {self.title_id_formatted} (0x{self.title_id_hex})", | |
| f"Title: {self.title}", | |
| f"Publisher: {self.publisher_code}", | |
| f"Game Number: {self.game_number}", | |
| f"Region: {self.region} (0x{self.region_flags:08X})", | |
| f"Version: {self.version}", | |
| f"Disc: {self.disc_number}", | |
| f"Build Date: {self.build_timestamp}", | |
| f"XBE Path: {self.xbe_path}", | |
| ] | |
| return "\n".join(lines) | |
| def read_u16_le(data: bytes, offset: int) -> int: | |
| return struct.unpack_from("<H", data, offset)[0] | |
| def read_u32_le(data: bytes, offset: int) -> int: | |
| return struct.unpack_from("<I", data, offset)[0] | |
| def decode_region(flags: int) -> str: | |
| regions = [] | |
| if flags & 0x00000001: | |
| regions.append("NA") | |
| if flags & 0x00000002: | |
| regions.append("JP") | |
| if flags & 0x00000004: | |
| regions.append("EU") | |
| if flags & 0x80000000: | |
| regions.append("DEBUG") | |
| return "/".join(regions) if regions else "Unknown" | |
| def decode_title_id(title_id: int) -> tuple[str, str, int]: | |
| """Decode Title ID into publisher code and game number.""" | |
| game_number = title_id & 0xFFFF | |
| publisher_code_int = (title_id >> 16) & 0xFFFF | |
| # Publisher code is 2 ASCII chars (big-endian in the high word) | |
| pub_char1 = chr((publisher_code_int >> 8) & 0xFF) | |
| pub_char2 = chr(publisher_code_int & 0xFF) | |
| publisher_code = f"{pub_char1}{pub_char2}" | |
| formatted = f"{publisher_code}-{game_number:03d}" | |
| return formatted, publisher_code, game_number | |
| def find_xbe_in_directory(f, dir_sector: int, dir_size: int, path: str = "") -> Optional[tuple[int, int, str]]: | |
| """ | |
| Search the XISO directory tree for an .xbe file. | |
| Returns (sector, size, path) of the first .xbe found, preferring default.xbe. | |
| """ | |
| f.seek(dir_sector * SECTOR_SIZE) | |
| dir_data = f.read(dir_size) | |
| xbe_candidates = [] | |
| def parse_entry(offset: int) -> None: | |
| if offset >= len(dir_data) - 14: | |
| return | |
| left_offset = read_u16_le(dir_data, offset) | |
| right_offset = read_u16_le(dir_data, offset + 2) | |
| file_sector = read_u32_le(dir_data, offset + 4) | |
| file_size = read_u32_le(dir_data, offset + 8) | |
| attributes = dir_data[offset + 12] | |
| name_len = dir_data[offset + 13] | |
| if name_len == 0 or offset + 14 + name_len > len(dir_data): | |
| return | |
| name = dir_data[offset + 14 : offset + 14 + name_len].decode("ascii", errors="replace") | |
| full_path = f"{path}/{name}" if path else name | |
| is_directory = (attributes & 0x10) != 0 | |
| if is_directory: | |
| # Recursively search subdirectories | |
| result = find_xbe_in_directory(f, file_sector, file_size, full_path) | |
| if result: | |
| xbe_candidates.append(result) | |
| elif name.lower().endswith(".xbe"): | |
| xbe_candidates.append((file_sector, file_size, full_path)) | |
| # Traverse B-tree | |
| if left_offset != 0 and left_offset != 0xFFFF: | |
| parse_entry(left_offset * 4) | |
| if right_offset != 0 and right_offset != 0xFFFF: | |
| parse_entry(right_offset * 4) | |
| # Start parsing from the root of this directory | |
| if len(dir_data) >= 14: | |
| parse_entry(0) | |
| if not xbe_candidates: | |
| return None | |
| # Prefer default.xbe | |
| for sector, size, filepath in xbe_candidates: | |
| if filepath.lower() == "default.xbe": | |
| return (sector, size, filepath) | |
| # Otherwise return the first one found | |
| return xbe_candidates[0] | |
| def extract_xbe_info(f, xbe_sector: int, xbe_path: str) -> XboxGameInfo: | |
| """Extract game info from an XBE file at the given sector.""" | |
| xbe_offset = xbe_sector * SECTOR_SIZE | |
| # Read XBE header (first 0x200 bytes should be enough for certificate offset) | |
| f.seek(xbe_offset) | |
| header = f.read(0x200) | |
| if header[:4] != XBE_MAGIC: | |
| raise ValueError(f"Invalid XBE magic at sector {xbe_sector}") | |
| # Certificate offset is at 0x118 (virtual address) | |
| # Base address is at 0x104 | |
| base_addr = read_u32_le(header, 0x104) | |
| cert_addr = read_u32_le(header, 0x118) | |
| # Convert virtual address to file offset | |
| cert_file_offset = xbe_offset + (cert_addr - base_addr) | |
| # Read certificate (0x1D0 = 464 bytes is typical size) | |
| f.seek(cert_file_offset) | |
| cert = f.read(0x1D0) | |
| # Parse certificate fields | |
| # cert_size = read_u32_le(cert, 0x00) | |
| timestamp = read_u32_le(cert, 0x04) | |
| title_id = read_u32_le(cert, 0x08) | |
| # Title name is UTF-16LE at offset 0x0C, 40 chars (80 bytes) | |
| title_bytes = cert[0x0C : 0x0C + 80] | |
| title = title_bytes.decode("utf-16-le", errors="replace").rstrip("\x00") | |
| # XBE Certificate structure: | |
| # 0x00: cert size (4), 0x04: timestamp (4), 0x08: title ID (4) | |
| # 0x0C: title name (80), 0x5C: alt title IDs (64), 0x9C: allowed media (4) | |
| # 0xA0: game region (4), 0xA4: game ratings (4), 0xA8: disc number (4), 0xAC: version (4) | |
| region_flags = read_u32_le(cert, 0xA0) | |
| disc_number = read_u32_le(cert, 0xA8) | |
| version = read_u32_le(cert, 0xAC) | |
| # Decode title ID | |
| title_id_formatted, publisher_code, game_number = decode_title_id(title_id) | |
| # Convert timestamp | |
| try: | |
| build_timestamp = datetime.datetime.fromtimestamp(timestamp) | |
| except (ValueError, OSError): | |
| build_timestamp = None | |
| return XboxGameInfo( | |
| title_id_hex=f"{title_id:08X}", | |
| title_id_formatted=title_id_formatted, | |
| publisher_code=publisher_code, | |
| game_number=game_number, | |
| title=title, | |
| region=decode_region(region_flags), | |
| region_flags=region_flags, | |
| version=version, | |
| disc_number=disc_number, | |
| build_timestamp=build_timestamp, | |
| xbe_path=xbe_path, | |
| ) | |
| def extract_game_info(xiso_path: str) -> XboxGameInfo: | |
| """Extract game identification info from an XISO file.""" | |
| with open(xiso_path, "rb") as f: | |
| # XISO header is at offset 0x10000 (sector 32) | |
| f.seek(0x10000) | |
| header = f.read(0x20) | |
| if header[:20] != XISO_MAGIC: | |
| raise ValueError("Not a valid XISO file (missing MICROSOFT*XBOX*MEDIA signature)") | |
| # Root directory info | |
| root_dir_sector = read_u32_le(header, 0x14) | |
| root_dir_size = read_u32_le(header, 0x18) | |
| # Find default.xbe or any .xbe file | |
| xbe_info = find_xbe_in_directory(f, root_dir_sector, root_dir_size) | |
| if not xbe_info: | |
| raise ValueError("No .xbe file found in XISO") | |
| xbe_sector, xbe_size, xbe_path = xbe_info | |
| # Extract info from the XBE | |
| return extract_xbe_info(f, xbe_sector, xbe_path) | |
| def info_to_dict(info: XboxGameInfo, filename: str) -> dict: | |
| """Convert XboxGameInfo to a JSON-serializable dict.""" | |
| return { | |
| "file": filename, | |
| "title_id": info.title_id_hex, | |
| "title_id_formatted": info.title_id_formatted, | |
| "title": info.title, | |
| "publisher": info.publisher_code, | |
| "game_number": info.game_number, | |
| "region": info.region, | |
| "region_flags": info.region_flags, | |
| "version": info.version, | |
| "disc": info.disc_number, | |
| "build_date": info.build_timestamp.isoformat() if info.build_timestamp else None, | |
| "xbe_path": info.xbe_path, | |
| } | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Extract Xbox game identification from XISO files." | |
| ) | |
| parser.add_argument("files", nargs="+", help="XISO file(s) to process") | |
| parser.add_argument("-j", "--json", action="store_true", help="Output as JSON") | |
| args = parser.parse_args() | |
| results = [] | |
| for xiso_path in args.files: | |
| path = Path(xiso_path) | |
| try: | |
| info = extract_game_info(xiso_path) | |
| if args.json: | |
| results.append(info_to_dict(info, path.name)) | |
| else: | |
| print(f"\n{'=' * 60}") | |
| print(f"File: {path.name}") | |
| print('=' * 60) | |
| print(info) | |
| except Exception as e: | |
| if args.json: | |
| results.append({"file": path.name, "error": str(e)}) | |
| else: | |
| print(f"\n{'=' * 60}") | |
| print(f"File: {path.name}") | |
| print('=' * 60) | |
| print(f"Error: {e}") | |
| if args.json: | |
| print(json.dumps(results, indent=2)) | |
| else: | |
| print() | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment