Files
magdevandClaude Fable 5 4737a5bae8 Add boxsim CLI: pure Python codec, netlist extraction, KiCad project generation
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>
2026-09-29 11:10:43 +02:00

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