# SPDX-License-Identifier: GPL-2.0-or-later """The CDJ-900NXS update file (C900NXS.UPD, firmware 1.31): read, rebuild with a new MAIN application, and check that nothing else changed. The file: three decimal section lengths, one per line, then the sections DRIV, MAIN and PANL. Each section is a 32-byte header ("CDJ-900NXS MAINVer1.31" ...; its last byte selects the updater's mode, "0" = application only), then Motorola S-records, then a CRC-16/XMODEM of all that, little-endian. MAIN's S-records fill the player's NOR flash from 0: below 0x40000 the boot loader and the recovery loader, from 0x40000 the application as one packed region ([u32 length][LZSS stream][u16 additive checksum of length and stream]). The boot loader checks that sum and unpacks the region to 0x08000000. The updater in application-only mode writes 0x40000-0x7E0000 only. This module never changes the boot loader or recovery records, DRIV or PANL: build_main() copies them byte for byte and verify() checks it again on the finished file. """ import binascii import hashlib import re import struct STOCK_UPD_SHA256 = 'e96bca7d1ac983985992a8e5fa7813e1ebf04bef514e0c6cfd0a8375f667f7f9' STOCK_APP_SHA256 = '1ac3e30cde292865413950fe159663b3958e85939ed0a5d0122f796809a5f8ef' STOCK_APP_SIZE = 16519056 APP_START = 0x40000 # the application's packed region in flash APP_END = 0x7E0000 # what the updater may write, end exclusive RECORD_DATA = 32 # data bytes per new S2 record, as the stock file ADDRESS_BYTES = {0: 2, 1: 2, 2: 3, 3: 4, 5: 2, 6: 3, 7: 4, 8: 3, 9: 2} HEADER = re.compile(rb'CDJ-900NXS (DRIV|MAIN|PANL)Ver(\d\.\d\d)') class UpdError(Exception): pass def sha256(data): return hashlib.sha256(data).hexdigest() def crc(data): return binascii.crc_hqx(data, 0) class Record: __slots__ = ('kind', 'address', 'data') def __init__(self, kind, address, data): self.kind, self.address, self.data = kind, address, bytes(data) def __eq__(self, other): return (self.kind, self.address, self.data) == (other.kind, other.address, other.data) def encode(self): width = ADDRESS_BYTES[self.kind] body = bytes([width + len(self.data) + 1]) + self.address.to_bytes(width, 'big') + self.data body += bytes([~sum(body) & 0xFF]) return b'S%d' % self.kind + body.hex().upper().encode() + b'\r\n' class Section: def __init__(self, blob): self.blob = blob if len(blob) < 36: raise UpdError('a section is truncated') if crc(blob[:-2]) != struct.unpack_from(' size: raise UpdError('%s: a record reaches past 0x%X' % (self.name, size)) image[r.address:r.address + len(r.data)] = r.data return bytes(image) def encode_section(header, records): body = header + b''.join(r.encode() for r in records) return body + struct.pack('= n: break if flags >> bit & 1: b = stream[pos] pos += 1 out.append(b) ring[r] = b r = (r + 1) & 0xFFF else: if pos + 2 > n: raise UpdError('the packed application ends inside a match') lo, hi = stream[pos], stream[pos + 1] pos += 2 at, count = lo | (hi & 0xF0) << 4, (hi & 0x0F) + 3 for k in range(count): b = ring[(at + k) & 0xFFF] out.append(b) ring[r] = b r = (r + 1) & 0xFFF return bytes(out) def region(flash): """(application, packed region bytes) from the flash image.""" size = struct.unpack_from(' APP_END: raise UpdError('the packed application runs past 0x%X' % APP_END) stored = struct.unpack_from(' APP_START for r in low): raise UpdError('a stock record crosses into the application') return low def relabel(header, version): old = HEADER.match(header).group(0) new = old[:-4] + version.encode() if not re.fullmatch(r'\d\.\d\d', version): raise UpdError('the version label must look like 1.32') out = header.replace(old, new, 1) if out[:len(old) - 4] != header[:len(old) - 4] or out[len(old):] != header[len(old):]: raise UpdError('relabelling would change more than the version') return out def build(stock, application, packed_stream, label='1.32'): """The new update: stock DRIV and PANL, MAIN with the stock header (version label only changed), the stock S0, the stock boot loader and recovery records, the new application region, and the stock S7 end record.""" main = stock.sections[1] reg = pack_region(packed_stream) if APP_START + len(reg) > APP_END: raise UpdError('the packed application needs %d bytes, the flash has %d' % (len(reg), APP_END - APP_START)) records = [main.records[0]] + stock.boot_records() records += [Record(2, APP_START + off, reg[off:off + RECORD_DATA]) for off in range(0, len(reg), RECORD_DATA)] records.append(main.records[-1]) blob = encode_section(relabel(main.header, label) if label else main.header, records) return join([stock.sections[0].blob, blob, stock.sections[2].blob]) def verify(raw, stock, application, label='1.32'): """Every check on a finished file; returns the list of what was checked.""" done = [] sections = parse(raw) done.append('section lengths, CRC-16 of every section, every S-record checksum') if sections[0].blob != stock.sections[0].blob or sections[2].blob != stock.sections[2].blob: raise UpdError('DRIV or PANL differs from stock') done.append('DRIV and PANL byte for byte as stock') main, smain = sections[1], stock.sections[1] if main.header != (relabel(smain.header, label) if label else smain.header): raise UpdError('the MAIN header differs beyond its version label') if main.header[31:32] != b'0': raise UpdError('the MAIN header no longer selects application-only mode') done.append('MAIN header as stock but for the label %s; application-only mode kept' % (label or '1.31')) if main.records[0] != smain.records[0] or main.records[-1] != smain.records[-1]: raise UpdError('the S0 header or S7 entry record changed') done.append('S0 header record and S7 entry record as stock') low = [r for r in main.data_records() if r.address < APP_START] if low != stock.boot_records(): raise UpdError('the boot loader or recovery records changed') flash = main.flash() if flash[:APP_START] != stock.flash[:APP_START]: raise UpdError('the boot loader or recovery bytes changed') done.append('boot loader and recovery (flash 0x0-0x3FFFF): every record and byte as stock') if any(r.address < APP_START < r.address + len(r.data) for r in main.data_records()): raise UpdError('a record crosses into the boot area') done.append('no record reaches past 0x%X or into the boot area' % APP_END) app, reg = region(flash) done.append('packed application: additive checksum') if app != application: raise UpdError('the update does not unpack to the patched application') done.append('unpacked with the boot loader\'s LZSS: exactly the patched application (SHA-256 %s)' % sha256(app)[:16]) done.append('flash used by the application: %d of %d bytes' % (len(reg), APP_END - APP_START)) return done