Replaces the bpj2ini.py prototype with a packaged tool. The A2F codec is now reimplemented in Python, so neither Boxsim.exe nor a CPU emulator is needed to decode a container; the emulator survives as a development-only oracle that the codec is differentially tested against. Verified: - 134 containers decode byte-identically to Boxsim's own decoder, 270 pre-2.0 files rejected by both implementations (tools/difftest.py --self-test) - Default.BPJ: 22 components, 14 nets, no dangling terminal - 22 of 22 parts resolve to library symbols; one series inferred from a sibling, two DC resistance deviations reported - ERC clean and the netlist KiCad extracts from the generated sheet is an identical partition to the one derived from the Boxsim grid - container build byte-identical to the host build - 32 tests passing, ruff clean Decode time went 69 s -> 14.5 s (binding emulator hooks to trap addresses) -> 1.6 s (pure Python). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
56 lines
1.7 KiB
Python
56 lines
1.7 KiB
Python
"""Boxsim file scrambler: the XOR layer of the MyIni2CK container.
|
|
|
|
Reverse engineered from Boxsim 2.1 (Init at VA 0xa12be4, next byte at 0xa12c9c).
|
|
A table-shuffled combination of three linear congruential generators, seeded
|
|
with the same constants for every file Boxsim has ever written.
|
|
"""
|
|
|
|
M32 = 1 << 32
|
|
|
|
SEED1 = 123
|
|
SEED2 = 456
|
|
# Compared against a global at VA 0xb10588, which the image ships as 0xffffffff
|
|
# and no code path ever writes.
|
|
THRESHOLD = 0xFFFFFFFF
|
|
|
|
|
|
class KryptPRNG:
|
|
"""Keystream generator for the XOR layer."""
|
|
|
|
__slots__ = ("s1", "s2", "s3", "table", "thr")
|
|
|
|
def __init__(self, s1: int = SEED1, s2: int = SEED2, threshold: int = THRESHOLD):
|
|
self.s1, self.s2, self.thr = s1, s2, threshold
|
|
self.s3 = (s1 % 0xFFFE) * (s2 % 0x7F23)
|
|
self.table = [0] * 256
|
|
for i in range(256):
|
|
self._step()
|
|
self.table[i] = (self.s1 + self.s2) % M32
|
|
|
|
def _step(self) -> None:
|
|
self.s1 = (self.s1 * 0x10B95 + 1) % M32
|
|
self.s2 = (self.s2 * 0x10BA9 + 1) % M32
|
|
if self.s2 >= self.thr:
|
|
self.s2 = 0
|
|
self.s3 = (self.s3 * 0x1155D + 1) % M32
|
|
|
|
def next_byte(self) -> int:
|
|
self._step()
|
|
idx = self.s3 & 0xFF
|
|
out = self.table[idx]
|
|
self.table[idx] = (self.s1 + self.s2) % M32
|
|
return out & 0xFF
|
|
|
|
def keystream(self, n: int) -> bytes:
|
|
return bytes([self.next_byte() for _ in range(n)])
|
|
|
|
|
|
def unscramble(body: bytes) -> bytes:
|
|
"""XOR-decrypt the container body (everything after the 8-byte magic)."""
|
|
ks = KryptPRNG().keystream(len(body))
|
|
return bytes(c ^ k for c, k in zip(body, ks, strict=True))
|
|
|
|
|
|
# The layer is symmetric, but keep both names so call sites read correctly.
|
|
scramble = unscramble
|