Files
CDJ900NXS-Patcher/test_patcher.py
T
Thiemo TizianiandClaude Opus 5.5 cfa0e85854 Added the CDJ-900NXS firmware patcher.
Takes the official C900NXS.UPD (1.31, checked by SHA-256), lets you choose
mods in a list (arrows, space, enter; numbers where the terminal cannot pass
keys), patches the application with the emulator checkout's
mods/patch_cdj900nxs.py, packs it and writes a new C900NXS.UPD labelled 1.32.

Only the application is replaced: the boot loader and recovery records,
MAIN's S0/S7 records, DRIV and PANL are copied byte for byte and checked again
on the finished file, which is also unpacked the boot loader's way and
compared with the patched application. --stock differs from the official file
in the version label and its CRC only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:05:49 +02:00

96 lines
3.5 KiB
Python

# SPDX-License-Identifier: GPL-2.0-or-later
"""Tests for the patcher: python -m pytest test_patcher.py
Offline: S-records, the boot loader's LZSS against our packer, the version
label guard. With the official update (CDJ900NXS_UPD, or C900NXS.UPD in this
folder): the stock file rebuilt byte for byte, the relabelled stock differing
in three bytes only, and every tampering with the boot area caught."""
import os
import random
import struct
import sys
import pytest
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import upd # noqa: E402
import cdj900nxs_patcher as cli # noqa: E402
STOCK = os.environ.get('CDJ900NXS_UPD') or os.path.join(HERE, 'C900NXS.UPD')
need_stock = pytest.mark.skipif(not os.path.isfile(STOCK), reason='needs the official C900NXS.UPD')
def test_record_round_trip():
rec = upd.Record(2, 0x040020, bytes(range(32)))
line = rec.encode()
assert line.startswith(b'S2') and line.endswith(b'\r\n')
raw = bytes.fromhex(line[2:-2].decode())
assert raw[0] == len(raw) - 1 and sum(raw) & 0xFF == 0xFF
def test_lzss_matches_the_boot_loader():
_, lzss = cli.load_repo(cli.DEFAULT_REPO)
rnd = random.Random(7)
data = bytes(rnd.choice(b' \0ABAB\xff') for _ in range(20000)) + b' ' * 5000 + bytes(4000)
assert upd.unpack_lzss(lzss.pack(data)) == data
def test_region_checksum_and_padding():
reg = upd.pack_region(b'abc')
assert len(reg) % 32 == 0
size = struct.unpack_from('<I', reg)[0]
assert struct.unpack_from('<H', reg, 4 + size)[0] == sum(reg[:4 + size]) & 0xFFFF
def test_relabel_touches_the_label_only():
h = b'CDJ-900NXS MAINVer1.31\x00 0'
assert upd.relabel(h, '1.32') == b'CDJ-900NXS MAINVer1.32\x00 0'
with pytest.raises(upd.UpdError):
upd.relabel(h, '1.3')
@need_stock
def test_stock_rebuilt_byte_for_byte():
raw = open(STOCK, 'rb').read()
stock = upd.Stock(raw)
size = struct.unpack_from('<I', stock.flash, upd.APP_START)[0]
stream = stock.flash[upd.APP_START + 4:upd.APP_START + 4 + size]
assert upd.build(stock, stock.application, stream, None) == raw
@need_stock
def test_relabelled_stock_differs_in_three_bytes():
raw = open(STOCK, 'rb').read()
stock = upd.Stock(raw)
patcher, lzss = cli.load_repo(cli.DEFAULT_REPO)
out, app, checks = cli.build(stock, [], patcher, lzss)
assert len(out) == len(raw) and sum(a != b for a, b in zip(out, raw)) == 3
assert app == stock.application and any('boot loader' in c for c in checks)
@need_stock
def test_other_files_refused():
raw = bytearray(open(STOCK, 'rb').read())
raw[-3] ^= 1
with pytest.raises(upd.UpdError):
upd.Stock(bytes(raw))
@need_stock
def test_boot_area_tampering_caught():
raw = open(STOCK, 'rb').read()
stock = upd.Stock(raw)
size = struct.unpack_from('<I', stock.flash, upd.APP_START)[0]
stream = stock.flash[upd.APP_START + 4:upd.APP_START + 4 + size]
good = upd.build(stock, stock.application, stream)
# rebuild MAIN with one boot loader byte changed, CRCs made right again
sections = upd.parse(good)
main = sections[1]
recs = list(main.records)
i = next(n for n, r in enumerate(recs) if r.kind == 2 and r.address < upd.APP_START)
recs[i] = upd.Record(2, recs[i].address, bytes([recs[i].data[0] ^ 1]) + recs[i].data[1:])
bad = upd.join([sections[0].blob, upd.encode_section(main.header, recs), sections[2].blob])
with pytest.raises(upd.UpdError, match='boot loader'):
upd.verify(bad, stock, stock.application)