Files
AC6_recomp/tools/ac6_mode1_codec.py
T

83 lines
2.7 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import struct
import zlib
from functools import lru_cache
# Number of distinct pads. The keygen masks the index with 0xFF, so there are
# exactly 256 possible pads; entry indices beyond 255 reuse them mod 256.
_PAD_COUNT = 256
# We need pi fractional words up to 2*(255)+2 = 512, plus slack.
_PI_WORDS_NEEDED = 2 * (_PAD_COUNT - 1) + 3
@lru_cache(maxsize=1)
def _pi_fractional_words(nwords: int) -> tuple[int, ...]:
"""First `nwords` base-2^32 words of frac(pi), most-significant word first.
Pure-integer Machin formula: pi = 16*arctan(1/5) - 4*arctan(1/239).
"""
bits = 32 * nwords + 64
def arctan_inv(x: int) -> int:
total = 0
term = (1 << bits) // x
x2 = x * x
k = 0
sign = 1
while term // (2 * k + 1):
total += sign * (term // (2 * k + 1))
term //= x2
k += 1
sign = -sign
return total
pi_scaled = 16 * arctan_inv(5) - 4 * arctan_inv(239)
frac = pi_scaled - (3 << bits) # drop the integer part "3"
return tuple(
(frac >> (bits - 32 * (i + 1))) & 0xFFFFFFFF for i in range(nwords)
)
@lru_cache(maxsize=_PAD_COUNT)
def pad_for_index(index: int) -> bytes:
"""Return the 8-byte descramble pad for a DATA.TBL entry index."""
words = _pi_fractional_words(_PI_WORDS_NEEDED)
w0 = 2 * (index & 0xFF) + 1
return struct.pack(">II", words[w0], words[w0 + 1])
def descramble(data: bytes, index: int) -> bytes:
"""Undo the XOR scrambling for a compressed entry at the given table index."""
pad = pad_for_index(index)
return bytes(b ^ pad[i & 7] for i, b in enumerate(data))
def decompress_entry(data: bytes, index: int, expected_size: int | None = None) -> bytes:
"""Descramble + raw-inflate a compressed PAC entry.
Args:
data: the on-disk (scrambled, compressed) entry bytes from DATA00/01.PAC.
index: the entry's DATA.TBL row index (drives the descramble pad).
expected_size: optional decompressed size from DATA.TBL for validation.
Returns the decompressed payload (typically an FHM container).
Raises zlib.error on a decode failure or ValueError on a size mismatch.
"""
raw = descramble(data, index)
out = zlib.decompress(raw, wbits=-15)
if expected_size is not None and len(out) != expected_size:
raise ValueError(
f"decompressed size mismatch: got {len(out)}, expected {expected_size}"
)
return out
if __name__ == "__main__":
# Smoke test: print the first few pads so the table can be eyeballed.
for i in range(4):
print(f"pad[{i}] = {pad_for_index(i).hex()}")