|
| 1 | +# PSPTool - Display, extract and manipulate PSP firmware inside UEFI images |
| 2 | +# Copyright (C) 2026 contributors |
| 3 | +# |
| 4 | +# This program is free software: you can redistribute it and/or modify |
| 5 | +# it under the terms of the GNU General Public License as published by |
| 6 | +# the Free Software Foundation, either version 3 of the License, or |
| 7 | +# (at your option) any later version. |
| 8 | + |
| 9 | +"""Synthetic ROM builder for unit tests. |
| 10 | +
|
| 11 | +Constructs an 8 MB ROM-shaped binary that PSPTool.from_file() can parse |
| 12 | +end-to-end: a Firmware Entry Table referencing a single $PSP directory, |
| 13 | +which in turn references one PSP_FW_BOOT_LOADER (type 0x01) HeaderFile. |
| 14 | +Most of the binary is zero padding; only the small windows that PSPTool |
| 15 | +parses are populated. |
| 16 | +
|
| 17 | +The boot loader's `version` field is the only knob tests need: setting |
| 18 | +the major byte (printed as version[1]) chooses which Zen generation |
| 19 | +PSPTool's back-fill assigns. The mapping itself lives in |
| 20 | +psptool.directory.Directory.BOOTLOADER_VERSION_TO_ZEN. |
| 21 | +
|
| 22 | +This is intentionally NOT a faithful PSP image; checksums on file |
| 23 | +contents are zero, no signatures, no AGESA strings. It exercises the |
| 24 | +parsing path that drives zen_generation back-fill and nothing more. |
| 25 | +""" |
| 26 | + |
| 27 | +import struct |
| 28 | + |
| 29 | +from psptool.blob import Blob |
| 30 | +from psptool.utils import fletcher32 |
| 31 | + |
| 32 | +ROM_SIZE = 8 * 1024 * 1024 |
| 33 | +# Smallest FET offset PSPTool will probe — every other constant in this |
| 34 | +# module is derived as a small offset above it, so updating the blob's |
| 35 | +# offset table doesn't desync the builder. |
| 36 | +FET_OFFSET = Blob.POSSIBLE_FET_OFFSETS[0] |
| 37 | +PSP_DIR_OFFSET = FET_OFFSET + 0x1000 |
| 38 | +BL_FILE_OFFSET = FET_OFFSET + 0x2000 |
| 39 | + |
| 40 | +FET_MAGIC = Blob._FIRMWARE_ENTRY_MAGIC |
| 41 | +PSP_DIR_MAGIC = b'$PSP' |
| 42 | + |
| 43 | +DIRECTORY_HEADER_SIZE = 16 |
| 44 | +DIRECTORY_ENTRY_SIZE = 16 |
| 45 | +HEADER_FILE_SIZE = 0x100 # HeaderFile.HEADER_LEN; minimal file body |
| 46 | + |
| 47 | + |
| 48 | +def _build_psp_directory(entry_offset: int, entry_size: int) -> bytes: |
| 49 | + # Header layout: |
| 50 | + # [0:4] magic '$PSP' |
| 51 | + # [4:8] fletcher32(body) |
| 52 | + # [8:12] entry count |
| 53 | + # [12:16] additional_info: bit31=version=1, bits 25:24 = address_mode |
| 54 | + # (1 = flash offset from start of BIOS) |
| 55 | + additional_info = struct.pack('<I', (1 << 31) | (1 << 24)) |
| 56 | + count = struct.pack('<I', 1) |
| 57 | + |
| 58 | + # One DirectoryEntry: type=0x01 PSP_FW_BOOT_LOADER, subprog=0, flags=0, |
| 59 | + # size, offset, rsv0=0 |
| 60 | + entry = struct.pack( |
| 61 | + '<BBHIII', |
| 62 | + 0x01, # type |
| 63 | + 0x00, # subprogram |
| 64 | + 0x0000, # flags |
| 65 | + entry_size, # size |
| 66 | + entry_offset, # offset (raw — directory addr_mode 1 returns this as-is) |
| 67 | + 0x00000000, # rsv0 |
| 68 | + ) |
| 69 | + |
| 70 | + body = count + additional_info + entry # bytes after the checksum field |
| 71 | + checksum = fletcher32(body) |
| 72 | + return PSP_DIR_MAGIC + checksum + body |
| 73 | + |
| 74 | + |
| 75 | +def _build_header_file(bl_major: int) -> bytes: |
| 76 | + # HeaderFile reads version as header[0x63:0x5f:-1], i.e. bytes at |
| 77 | + # 0x63, 0x62, 0x61, 0x60 in printed order. The major byte that drives |
| 78 | + # zen_generation lives at version[1] = header[0x62]. |
| 79 | + header = bytearray(HEADER_FILE_SIZE) |
| 80 | + header[0x60] = 0x00 # version[3] (build, ignored for backfill) |
| 81 | + header[0x61] = 0x00 # version[2] (minor, ignored) |
| 82 | + header[0x62] = bl_major # version[1] (major — drives backfill) |
| 83 | + header[0x63] = 0x00 # version[0] (printed-form leading zero) |
| 84 | + # rom_size (header[0x6c:0x70]) = 0 means "use buffer_size" in HeaderFile |
| 85 | + # All other fields stay zero: not encrypted, not signed, no checksum bits. |
| 86 | + return bytes(header) |
| 87 | + |
| 88 | + |
| 89 | +def build_synthetic_rom(bl_major: int) -> bytes: |
| 90 | + """Return an 8 MB ROM blob whose PSP_FW_BOOT_LOADER version major |
| 91 | + byte is `bl_major`. Pass a value from |
| 92 | + Directory.BOOTLOADER_VERSION_TO_ZEN to drive a specific Zen |
| 93 | + generation through the back-fill path. |
| 94 | + """ |
| 95 | + blob = bytearray(ROM_SIZE) |
| 96 | + |
| 97 | + # FET layout. The FET parser walks 4-byte words from FET_OFFSET until |
| 98 | + # it sees 16 bytes of 0xFF. The first word is the FET magic itself |
| 99 | + # (which the parser tries to interpret as a directory pointer and |
| 100 | + # warns about — benign for tests). The next valid pointer points at |
| 101 | + # our $PSP directory. |
| 102 | + blob[FET_OFFSET - 4:FET_OFFSET] = b'\xff\xff\xff\xff' # required by _find_fets regex |
| 103 | + blob[FET_OFFSET:FET_OFFSET + 4] = FET_MAGIC |
| 104 | + blob[FET_OFFSET + 4:FET_OFFSET + 8] = struct.pack('<I', PSP_DIR_OFFSET) |
| 105 | + blob[FET_OFFSET + 8:FET_OFFSET + 24] = b'\xff' * 16 # FET terminator |
| 106 | + |
| 107 | + psp_dir = _build_psp_directory(BL_FILE_OFFSET, HEADER_FILE_SIZE) |
| 108 | + blob[PSP_DIR_OFFSET:PSP_DIR_OFFSET + len(psp_dir)] = psp_dir |
| 109 | + |
| 110 | + bl_header = _build_header_file(bl_major) |
| 111 | + blob[BL_FILE_OFFSET:BL_FILE_OFFSET + len(bl_header)] = bl_header |
| 112 | + |
| 113 | + return bytes(blob) |
| 114 | + |
| 115 | + |
| 116 | +if __name__ == '__main__': |
| 117 | + # Smoke check: build a Zen 2 ROM and dump its size and the version |
| 118 | + # bytes for sanity. |
| 119 | + data = build_synthetic_rom(0x0C) |
| 120 | + print(f'len={len(data)} fet[0:4]={data[FET_OFFSET:FET_OFFSET+4].hex()} ' |
| 121 | + f'psp_dir[0:4]={data[PSP_DIR_OFFSET:PSP_DIR_OFFSET+4]!r} ' |
| 122 | + f'bl_version[60:64]={data[BL_FILE_OFFSET+0x60:BL_FILE_OFFSET+0x64].hex()}') |
0 commit comments