diff --git a/.gitignore b/.gitignore index 1e06dc7..b3efc34 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ *.zip boxsim_dl/ +stage/ +.venv/ +*.egg-info/ brute .claude/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..edd7c1d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- `boxsim` command line tool with `decode`, `extract`, `netlist` and `build` subcommands. +- Pure Python implementation of the A2F codec, so decoding no longer needs `Boxsim.exe` or a CPU emulator. Verified byte-identical to Boxsim's own decoder across all 134 containers in the distribution. +- Project model covering `#General`, `#Weiche`, `#Chassis`, `#Einbau` and `#Schallwand`, with the crossover grid decoded to an electrical netlist. +- Symbol resolver driven by Visaton's `Visatonteile.txt` catalogue, matching values numerically against the KiCad library. Infers an omitted series from a sibling part, reports DC resistance deviations, and refuses to substitute an unstocked value. +- KiCad 10 schematic generation that preserves the Boxsim grid layout, including wires, junctions, flattened derived symbols and deterministic UUIDs. +- Verification gate comparing the netlist KiCad extracts from the generated sheet against the netlist derived from the Boxsim grid, plus ERC. +- Docker image bundling the tool, the Visaton library and the parts catalogue, with a build-time check that the bundled catalogue is complete. +- Development-only oracle that runs Boxsim's own `ExpandA2F` under Unicorn, and `tools/difftest.py` to differentially test the codec against it. + +### Changed + +- Emulator hooks are bound to their trap addresses instead of every instruction, cutting a decode from 69 s to 14.5 s with identical output. The pure Python codec then brought it to about 1.6 s. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..58a0400 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +# boxsim: decode Boxsim projects and generate KiCad projects from them. +# +# The image deliberately contains no KiCad and no Boxsim.exe: +# - KiCad 10 is a hard dependency of the *verification* gate only, and stays +# on the host. decode/extract/netlist/build all work without it. +# - The A2F codec is pure Python, so Boxsim's proprietary binary is not needed. +# +# Build via tools/build-image.sh, which stages the Visaton library and the +# Boxsim parts catalogue into the build context. + +FROM python:3.13-slim AS base + +LABEL org.opencontainers.image.title="boxsim" \ + org.opencontainers.image.description="Boxsim project decoder and KiCad project generator" \ + org.opencontainers.image.source="https://src.bundespruefstelle.ch/boxdev/re-boxsim" \ + org.opencontainers.image.licenses="MIT" + +WORKDIR /opt/boxsim + +COPY pyproject.toml README.md ./ +COPY boxsim ./boxsim + +# Staged by tools/build-image.sh: the upgraded Visaton symbol library and the +# Visaton parts catalogue shipped with Boxsim. +COPY stage/Visaton.kicad_sym ./boxsim/data/Visaton.kicad_sym +COPY stage/Visatonteile.txt ./boxsim/data/Visatonteile.txt +COPY stage/footprints ./boxsim/data/footprints + +COPY tools/selfcheck.py ./tools/selfcheck.py + +RUN pip install --no-cache-dir . + +ENV BOXSIM_CATALOG=/opt/boxsim/boxsim/data/Visatonteile.txt \ + BOXSIM_KICAD_LIB=/opt/boxsim/boxsim/data/Visaton.kicad_sym + +# Refuse to produce an image whose bundled library does not cover the catalogue. +RUN python tools/selfcheck.py + +WORKDIR /work +ENTRYPOINT ["boxsim"] +CMD ["--help"] diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..47bb8db --- /dev/null +++ b/PLAN.md @@ -0,0 +1,59 @@ +# Plan + +## Current state + +v0.1.0 is delivered. The `boxsim` package decodes Boxsim containers in pure Python and generates KiCad 10 projects from their crossovers; see `README.md` for the format and the tool, and `CHANGELOG.md` for what changed. + +Environment as of 2026-09-29: KiCad 10.0.6 installed natively, Python 3.13.15, Visaton library at `/home/magdev/workspaces/other/kicad-visaton-lib` (remote `pidock.local:3022`), upgraded to the KiCad 10 format as part of this work. + +Reference project for all work: `boxsim_dl/BoxSim 2.1 DE (Stand 12.2025)/Projekte/Default.BPJ`. + +## v0.1.0 - `boxsim` CLI and KiCad project generation + +Delivered 2026-09-29. All seven tickets landed; what follows records the outcome, including where it differed from the design. + +### What shipped + +- `boxsim` with `decode`, `extract`, `netlist` and `build`, installable and packaged as a `python:3.13-slim` image bundling the Visaton library and catalogue. +- A pure Python A2F codec. `Boxsim.exe` and Unicorn are gone from the shipped tool and the image. +- Grid to netlist extraction, catalogue-driven symbol resolution, KiCad 10 schematic generation preserving the Boxsim layout, and a netlist-equality verification gate. + +### Measured results + +| Gate | Result | +| ---- | ------ | +| Codec vs Boxsim's own decoder | 134 containers byte-identical, 270 pre-2.0 files rejected by both | +| Decode time for `Default.BPJ` | 69 s to 14.5 s (hook binding) to 1.6 s (pure Python) | +| `Default.BPJ` netlist | 22 components, 14 nets, no dangling terminal | +| Symbol resolution | 22 of 22, one series inferred, two DC resistance deviations reported | +| ERC | clean, 0 errors | +| KiCad netlist vs Boxsim netlist | identical partition, 14 nets | +| Container vs host build | `.kicad_sch` and `.kicad_pro` byte-identical | +| Tests / lint | 32 passing, ruff clean | + +### Where it differed from the design + +- **The library needed changing after all.** The plan said no library change was required. That was wrong: the KiCad 6 era syntax (`(id N)` properties, bare `hide` tokens) cannot be embedded into a current schematic's `lib_symbols` cache. `kicad-visaton-lib` was upgraded with `kicad-cli sym upgrade` and `fp upgrade`, and the tool now refuses a legacy library with an error naming the fix. A pre-existing broken footprint reference on the `MKT 22μF` symbol (`Visaton:MKT-Kondensator`, which never existed) was repointed at `MKT 22mF`; all 48 footprint references now resolve. +- **Derived symbols had to be flattened.** 19 of the 98 symbols use `(extends ...)`, and KiCad never writes `extends` into a schematic cache. Pin geometry follows the chain to the base symbol, and the base's graphics are inlined under the derived name. +- **The decoder needed a padding allowance.** Boxsim reads past the end of its staging buffer for the final symbol's 16 bit lookup window. Five files failed until the reader tolerated a bounded number of zero padding bytes; the bound keeps genuine truncation loud. +- **Rotation could not be assumed.** A symbol's origin is not its pin midpoint (`Amplifier` has both pins at px=3.81), and a symbol's pin axis does not follow its slot. Rotation is chosen per symbol from its own pin axis, the origin is derived so the transformed pin midpoint lands on the slot midpoint, and the four transform matrices were measured against KiCad's demo schematics rather than guessed. +- **The image self-check was decorative at first.** `cross_check` only proves the catalogue is a subset of the library, so a truncated catalogue passed it and would have shipped an image rejecting valid parts. `catalog.check_shape` now asserts the catalogue's full shape, and the failure was reproduced deliberately before the fix was accepted. +- **Grid pitch is 25.4 mm, not 20 mm.** The widest symbol spans 22.86 mm between pins, so 20 mm would overlap neighbouring cells. `Default.BPJ` therefore lands on A3 rather than A4. + +### Ticket outcomes + +- **BOX-0** emulator bound to trap addresses, moved to `boxsim/_oracle.py`, reachable as `boxsim decode --oracle`. +- **BOX-1** `boxsim/a2f.py`, differentially tested by `tools/difftest.py`, self-test proves the comparison detects a corrupt digest. +- **BOX-2** `boxsim/model.py`, slot partition covered by tests, netlist cross-checked against crossover physics (600 Hz and 5.7 kHz corners). +- **BOX-3** `boxsim/catalog.py` and `boxsim/symbols.py`. +- **BOX-4** `boxsim/schematic.py`. +- **BOX-5** `boxsim/cli.py` and `boxsim/project.py`. +- **BOX-6** `boxsim/verify.py`, with `test_gate_has_teeth` mis-wiring a component to prove the gate fails when it should. +- **BOX-7** `Dockerfile`, `tools/build-image.sh`, `tools/selfcheck.py`. + +## Backlog + +- Multi-channel projects (`nAmps` up to 6), probably one hierarchical sheet per channel. +- Active filter sections (`#AktFilter`, `#Aktfilter`), which Boxsim models as DSP biquads with no passive equivalent. +- Per-driver symbols instead of the generic `Speaker`, if the library grows them. The 400 `.BCH` files provide the model names and TSP data. +- Round trip: patch component values in a `.bpj` from an edited schematic. Needs the A2F compressor, of which only the decompressor is currently exercised. diff --git a/README.md b/README.md index d46f85d..970f38e 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,160 @@ -# Boxsim .bpj / .bch file format (reverse engineered) +# boxsim -Boxsim (Visaton, Delphi, codec and container "MyIniFile" by UG 2004) stores projects (`.bpj`) and driver files (`.bch`) in a three-layer container. Reverse engineered 2026-09-28 from Boxsim 2.1 DE (`Boxsim.exe`, no prior public documentation existed). +Read Boxsim loudspeaker projects and turn their crossovers into KiCad projects. -## Container layout +[Boxsim](https://www.visaton.de/) is Visaton's free loudspeaker simulation program. Its project files (`.bpj`) and driver files (`.bch`) use an undocumented container, reverse engineered here. `boxsim` decodes them and generates a KiCad 10 project whose schematic mirrors the crossover you drew in Boxsim, with every part bound to a symbol and footprint from [kicad-visaton-lib](https://src.bundespruefstelle.ch/boxdev/kicad-visaton-lib). + +## Install + +```bash +pip install . +``` + +Nothing is required at runtime beyond Python 3.11+. KiCad 10 is a hard dependency of the verification gate (`boxsim build --verify`) only, and lives on the host. + +Point the tool at the Visaton library and the Boxsim parts catalogue: + +```bash +export BOXSIM_KICAD_LIB=/path/to/kicad-visaton-lib +export BOXSIM_CATALOG="/path/to/BoxSim 2.1 DE/Weichenbauteile/Visatonteile.txt" +``` + +The symbol library must be in the current KiCad format. Convert an older one with `kicad-cli sym upgrade --force symbols/Visaton.kicad_sym`. + +## Use + +```bash +boxsim decode project.bpj -o project.ini # raw INI payload +boxsim extract project.bpj -o project.json # structured model, symbols resolved +boxsim netlist project.bpj # crossover netlist +boxsim build project.bpj out/ --verify # a KiCad 10 project +``` + +`build` writes `.kicad_sch`, `.kicad_pro` and project-local `sym-lib-table` / `fp-lib-table`. No board is generated: use KiCad's *Update PCB from Schematic*, which works because every Visaton symbol carries a `Footprint` property. + +### Docker + +```bash +./tools/build-image.sh +docker run --rm -v "$PWD:/work" boxsim:0.1.0 build project.bpj out/ +``` + +The image bundles the tool, the Visaton library and the catalogue. It contains no KiCad and no `Boxsim.exe`, so `decode`, `extract`, `netlist` and `build` work inside it while `--verify` does not. Run the gate on the host. + +## The file format | Layer | Content | | ----- | ------- | | 1 | 8-byte plaintext magic `MyIni2CK` | -| 2 | Everything after the magic is XOR-encrypted with a position-based PRNG keystream, seeds (123, 456), identical for every file | -| 3 | The decrypted body is a custom LZ + canonical-Huffman stream ("A2F" codec, `CompressA2F`/`ExpandA2F` at VA `0xa1013c`/`0xa11880`) | -| 4 | The decompressed payload is plain INI-style text: `key=value` lines, CRLF, `#Section` headers, German decimal commas, arrays as `;`-separated values | +| 2 | XOR against a keystream from a table-shuffled three-LCG generator, seeded (123, 456) for every file ever written | +| 3 | A custom LZ plus canonical-Huffman codec ("A2F") | +| 4 | INI-style text: `#Section` headers, `keyvalue`, CRLF, German decimal commas, `;`-separated arrays | -## Layer 2: the Krypt PRNG +Files written by Boxsim 1.2 use an older container without the magic and are rejected. -Table-shuffled combination of three LCGs (found at VA `0xa12be4` init, `0xa12c9c` next byte). All arithmetic mod 2^32: +### The Krypt keystream + +Three linear congruential generators feeding a 256-entry shuffle table (Boxsim 2.1: init at VA `0xa12be4`, next byte at `0xa12c9c`). All arithmetic mod 2^32: ```text init(s1=123, s2=456): s3 = (s1 mod 0xFFFE) * (s2 mod 0x7F23) - repeat 256 times: step(); table[i] = s1 + s2 + 256 times: step(); table[i] = s1 + s2 -step(): - s1 = s1 * 0x10B95 + 1 - s2 = s2 * 0x10BA9 + 1 (reset to 0 if >= 0xFFFFFFFF) - s3 = s3 * 0x1155D + 1 +step(): s1 = s1*0x10B95 + 1 + s2 = s2*0x10BA9 + 1 (reset to 0 if >= 0xFFFFFFFF) + s3 = s3*0x1155D + 1 -next_byte(): - step() - idx = s3 and 0xFF - out = table[idx]; table[idx] = s1 + s2 - return out mod 256 +next(): step(); i = s3 and 0xFF + out = table[i]; table[i] = s1 + s2 + return out mod 256 ``` -`plaintext[i] = filebyte[8+i] XOR next_byte()`. +### The A2F codec -## Layer 3: the A2F codec +A bit stream read MSB first through a 31-bit accumulator, with symbols from a canonical Huffman code over 356 symbols decoded via a 65536-entry direct lookup table. All code lengths start at 9. -Sliding-window LZ (1 MB window, flush keeps the last 0x3FFF8 bytes) with a 356-symbol canonical Huffman alphabet: 0..255 literals, 0x100 end-of-stream, 0x101 rebuild code-length tables (adaptive), 0x102 special, rest match lengths/distances. Decoding uses a 65536-entry direct lookup table; initial state gives every symbol a 9-bit code. Not re-implemented: `bpj2ini.py` runs the original `ExpandA2F` machine code under Unicorn instead, which is exact by construction. +| Symbol | Meaning | +| ------ | ------- | +| `0x000`-`0x0FF` | literal byte | +| `0x100` | end of stream | +| `0x101` | new code lengths follow, rebuild the tables | +| `0x102` | define N entries of the 64-entry digram dictionary | +| `0x103`-`0x10A` | match, length 3, distance in k+3 bits | +| `0x10B`-`0x114` | match, length 4, distance in k+3 bits | +| `0x115`-`0x123` | match, unary-coded length class, distance in k+3 bits | +| `0x124`-`0x163` | emit the two bytes of digram dictionary entry k | -## Crossover encoding (`#Weiche` section) +Distances take `k+3` raw bits biased by `2**(k+3) - 7`, so the classes tile 1.. with no gaps. Code lengths are coded as two zero bits meaning "same as previous", otherwise a nibble plus one, where the value 5 escapes to a second nibble. -The crossover editor grid serializes as `_Part_*` keys. Only non-empty slots are written: +Boxsim decodes from a staging buffer and reads past the end of the input for the final symbol's lookup window, so the decoder tolerates a bounded number of zero padding bytes. Beyond that a truncated file fails loudly. + +### The crossover grid + +The crossover editor is an 11 x 18 node grid. A slot number encodes position and orientation: + +| Range | Meaning | +| ----- | ------- | +| 1-180 | horizontal, 10 per row, joins node(r,c) to node(r,c+1) | +| 181-367 | vertical, 11 per row, joins node(r,c) to node(r+1,c) | +| 368-375 | free link, endpoints given by `_nod1` and `_nod2` | + +Nodes are numbered row-major and zero based: `node = row * 11 + col`. + +Keys in a `#Weiche` section: | Key | Meaning | | --- | ------- | | `_Part_typ` | 0 empty, 1 wire, 2 resistor, 3 capacitor, 4 inductor, 5 driver, 6 amplifier input | -| `_Part_value` | Component value in SI base units (Ohm, Farad, Henry) | -| `_Part_Rvalue` | Series resistance of an inductor in Ohm | -| `_Part_bem` | Part label, e.g. `MKT-A`, `SP 0,6mm`, `Keramik 10W` | -| `_Part_chassisnr` | Driver index (0-based) for typ 5 | -| `_Part_verpolt` | Polarity flag (-1 / 1) for typ 5 and 6 | -| `_Part368..375_nod1/nod2` | Extra node-to-node connections | +| `_Part_value` | SI units: Ohm, Farad, Henry | +| `_Part_Rvalue` | inductor DC resistance in Ohm | +| `_Part_bem` | Visaton series name, matching `Visatonteile.txt` exactly | +| `_Part_chassisnr` | zero-based driver index | +| `_Part_verpolt` | polarity, -1 meaning inverted | -Other sections: `#General` (nAmps, nChassis), `#AktFilter` (active filters), `#Chassis` (TSP set plus measured frequency/impedance/phase arrays), `#Einbau` (enclosure: Typ 1 closed with `Vb` in m³, etc.), `#Schallwand` (baffle geometry in m), `#GemGeh` (shared enclosure). +Other sections: `#General`, `#AktFilter` and `#Aktfilter` (active filters, note the differing case), `#Chassis` (name plus Thiele-Small parameters and measured response arrays), `#Einbau` (enclosure, `Vb` in m³), `#Schallwand` (baffle geometry in m), `#GemGeh` (shared enclosure). -## Tool +## How the parts are resolved + +`Weichenbauteile/Visatonteile.txt` is the authoritative parts list: 95 parts across 11 series, with inductor DC resistance in the fourth column. Its series strings are exactly the `_bem` values Boxsim writes, so resolution is a lookup rather than a guess. + +Values are matched numerically against the library rather than formatted into symbol names, because the library's own strings are irregular (`MKP 1.0μF` beside `MKP 10μF`, `SP-Spule 1.0mH (0.6)` beside `LR-Spule 10mH`). + +- An empty `_bem` is inferred from a sibling part of the same type and value in the same crossover, else from the widest series for that type. Every inference is logged. +- A `_Rvalue` that differs from the catalogue is reported, never corrected: it is a legitimate edit in Boxsim. +- A value Visaton does not stock is a hard error naming the nearest stocked values. Nothing is substituted silently. + +The library contains one symbol the catalogue does not list, `MKT 22μF`. It is a real Visaton part (radial film capacitor, 250 V) that Boxsim does not offer in its picker, so no project can reference it. + +## Verification + +ERC only proves a schematic is well formed; it would happily pass one wired to the wrong cells. So `boxsim build --verify` compares the netlist KiCad extracts from the generated sheet against the netlist derived independently from the Boxsim grid, and both derivations must agree. + +The codec is held to the same standard. `tools/difftest.py` decodes every container in the Boxsim distribution twice, once with the pure Python codec and once by emulating Boxsim's own `ExpandA2F` under Unicorn, and requires byte-identical output: ```bash -.venv/bin/python bpj2ini.py [output.ini] +.venv/bin/python tools/difftest.py --self-test +# 134 byte-identical, 270 rejected by both as pre-2.0 containers ``` -Needs `unicorn` and `pefile` (see `.venv/`) and `Boxsim.exe` (path via `BOXSIM_EXE` env, default: the bundled download under `boxsim_dl/`). Works for `.bpj` and `.bch` alike. +Both gates are checked against a deliberately broken input so they are known to fail when they should: `--self-test` corrupts a digest, and `test_gate_has_teeth` mis-wires a component. + +The oracle is development-only. It needs `unicorn`, `pefile` and a copy of `Boxsim.exe`: + +```bash +pip install -e '.[oracle,dev]' +export BOXSIM_EXE="/path/to/Boxsim.exe" +``` + +## Tests + +```bash +pytest # fast, no Boxsim.exe needed +ruff check boxsim tools tests +``` + +Tests needing KiCad or the Boxsim distribution skip themselves when those are absent. + +## Licence + +MIT. diff --git a/boxsim/__init__.py b/boxsim/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bpj2ini.py b/boxsim/_oracle.py similarity index 52% rename from bpj2ini.py rename to boxsim/_oracle.py index a3b8ed8..feacd5b 100644 --- a/bpj2ini.py +++ b/boxsim/_oracle.py @@ -1,79 +1,56 @@ -#!/usr/bin/env python3 -"""Decode Boxsim .bpj / .bch (MyIni2CK) files to plain INI text. +"""Development-only reference decoder: runs Boxsim's own machine code. -Pipeline (reverse engineered from Boxsim 2.1, TMyIniFile by UG): - 1. 8-byte magic "MyIni2CK" (plaintext) - 2. body XOR-encrypted with a table-shuffled 3-LCG PRNG, seeds (123, 456) - 3. result is a custom canonical-Huffman + LZ stream ("A2F" codec) - 4. decompressed: tab-separated INI text, CRLF line ends, German decimals +This module emulates `ExpandA2F` (VA 0xa11880) out of an unmodified Boxsim.exe +under Unicorn, with faked Delphi TStream objects. It is exact by construction +and exists solely as the oracle that the pure Python decoder in `boxsim.a2f` +is differentially tested against. -Step 3 is executed by emulating Boxsim's own ExpandA2F function (VA 0xa11880) -with Unicorn, so Boxsim.exe must be available (BOXSIM_EXE env or default path). +It is NOT part of the shipped tool: it needs `unicorn`, `pefile` and an 8 MB +proprietary Windows binary. Point BOXSIM_EXE at a Boxsim.exe to use it. -Usage: bpj2ini.py input.bpj [output.ini] + python -m boxsim.cli decode --oracle file.bpj """ + +from __future__ import annotations + import os import struct -import sys - -import pefile -from unicorn import * -from unicorn.x86_const import * - -EXE = os.environ.get( - "BOXSIM_EXE", - os.path.join(os.path.dirname(os.path.abspath(__file__)), - "boxsim_dl", "BoxSim 2.1 DE (Stand 12.2025)", "Boxsim.exe")) MAGIC = b"MyIni2CK" -ENTRY = 0xA11880 # ExpandA2F(obj, instream, outstream) -ERR_FUNC = 0x97E584 # Boxsim error logger -RAISE_FUNC = 0x80ACC4 # Delphi RaiseExcept -OBJ_BASE = 0x20000000 # decoder object (zero-init, ~11 MB of tables) +ENTRY = 0xA11880 # ExpandA2F(self, instream, outstream) +ERR_FUNC = 0x97E584 # Boxsim's error reporter +RAISE_FUNC = 0x80ACC4 # Delphi RaiseExcept + +OBJ_BASE = 0x20000000 # decoder object: ~11 MB of zero-init tables OBJ_SIZE = 0x01000000 -FAKE_BASE = 0x40000000 # fake TStream objects, vtables, trap stubs +FAKE_BASE = 0x40000000 # fake stream objects, vtables, trap stubs STACK_BASE = 0x60000000 STACK_SIZE = 0x100000 GDT_BASE = 0x50000000 -FS_SEG = 0x50002000 # Delphi SEH chain scratch +FS_SEG = 0x50002000 # Delphi SEH chain scratch space SENTINEL = 0x7F00DEAD IN_OBJ, OUT_OBJ = FAKE_BASE, FAKE_BASE + 0x100 IN_VT, OUT_VT = FAKE_BASE + 0x1000, FAKE_BASE + 0x2000 STUB = FAKE_BASE + 0x3000 S_IN_SIZE, S_IN_READ, S_IN_SEEK, S_OUT_WRITE, S_OUT_SIZE, S_OUT_SEEK = range(6) - -M32 = 1 << 32 +STUB_SPAN = 256 -class KryptPRNG: - """Boxsim file scrambler PRNG (Init at VA 0xa12be4, next at 0xa12c9c).""" - - def __init__(self, s1=123, s2=456, threshold=0xFFFFFFFF): - 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): - 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): - self._step() - idx = self.s3 & 0xFF - out = self.table[idx] - self.table[idx] = (self.s1 + self.s2) % M32 - return out & 0xFF +def default_exe() -> str: + return os.environ.get( + "BOXSIM_EXE", + os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "boxsim_dl", + "BoxSim 2.1 DE (Stand 12.2025)", + "Boxsim.exe", + ), + ) -def _gdt_entry(base, limit, access, gran): +def _gdt_entry(base: int, limit: int, access: int, gran: int) -> bytes: e = limit & 0xFFFF e |= (base & 0xFFFFFF) << 16 e |= (access & 0xFF) << 40 @@ -83,32 +60,62 @@ def _gdt_entry(base, limit, access, gran): return struct.pack(" bytes: + """Decompress a descrambled MyIni2CK stream using Boxsim's own decoder.""" + import pefile + from unicorn import UC_ARCH_X86, UC_HOOK_CODE, UC_MODE_32, Uc + from unicorn.x86_const import ( + UC_X86_REG_CS, + UC_X86_REG_DS, + UC_X86_REG_EAX, + UC_X86_REG_ECX, + UC_X86_REG_EDX, + UC_X86_REG_ES, + UC_X86_REG_ESP, + UC_X86_REG_FS, + UC_X86_REG_GDTR, + UC_X86_REG_GS, + UC_X86_REG_SS, + ) + + exe = exe or default_exe() + if not os.path.exists(exe): + raise FileNotFoundError( + f"oracle needs Boxsim.exe; set BOXSIM_EXE (looked at {exe!r})" + ) + + pe = pefile.PE(exe) base = pe.OPTIONAL_HEADER.ImageBase img = pe.get_memory_mapped_image(ImageBase=base) uc = Uc(UC_ARCH_X86, UC_MODE_32) uc.mem_map(base, (len(img) + 0xFFF) & ~0xFFF) uc.mem_write(base, img) - for addr, size in ((OBJ_BASE, OBJ_SIZE), (FAKE_BASE, 0x10000), - (STACK_BASE, STACK_SIZE), (GDT_BASE, 0x4000)): + for addr, size in ( + (OBJ_BASE, OBJ_SIZE), + (FAKE_BASE, 0x10000), + (STACK_BASE, STACK_SIZE), + (GDT_BASE, 0x4000), + ): uc.mem_map(addr, size) uc.mem_write(IN_OBJ, struct.pack("