Files
DeDRM_tools/tests/test_topazextract.py
T
JMR-devandClaude Opus 4.8 ba96df081f Add pytest suite for both plugins
Add a pytest suite covering the pure logic of the DeDRM and Obok plugins.
Because the plugins normally run inside calibre and use intra-package
imports, tests/dedrm_test_utils.py sets up an import shim (plugin dirs on
sys.path, a minimal calibre stub, and a synthetic `dedrm` package) so the
modules can be imported and exercised standalone.

Coverage:
  - alfcrypto: PC1 and Topaz cipher round-trips, PBKDF2 vs hashlib.
  - kgenpids / kindlepid: PID encoding, bit-field extraction, device-PID
    and serial-PID known vectors, CRC32.
  - mobidedrm: PC1 round-trip, trailing-data sizing, bad-key handling.
  - ineptpdf / ineptepub: nunpack and PKCS7 unpad.
  - topazextract: encoded number/string parsing, plus a regression test
    that a malicious header tag ("../../evil") cannot escape the output
    directory (covers the path-traversal fix).
  - obok: unpad, hash-key table, SafeUnbuffered str/bytes handling.
  - utilities / argv_utils: uStrCmp normalisation, unicode_argv.
  - erdr2pml: deXOR/sanitiseFileName, skipped on Python 3.13+ where the
    module's `cgi` import is unavailable.

Run with `poetry install` then `poetry run pytest`. 36 pass, 2 skip on
Python 3.13+.

Note: this surfaced two dead-code modules that are broken on Python 3
(aescbc's pure-Python AES and kgenpids.decode, neither on a live path,
since the real crypto goes through pycryptodome); left as-is here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:08:03 -05:00

61 lines
1.9 KiB
Python

"""Tests for topazextract, including the path-traversal regression test."""
from io import BytesIO
import dedrm_test_utils as U
tz = U.load("topazextract", package="dedrm")
def test_book_read_encoded_number():
assert tz.bookReadEncodedNumber(BytesIO(bytes([0x05]))) == 5
assert tz.bookReadEncodedNumber(BytesIO(bytes([0x81, 0x00]))) == 128
def test_book_read_string():
assert tz.bookReadString(BytesIO(bytes([0x03]) + b"abc")) == b"abc"
def _bare_book(outdir):
"""A TopazBook instance with just the attributes extractFiles() needs."""
book = tz.TopazBook.__new__(tz.TopazBook)
book.outdir = str(outdir)
return book
def test_extractfiles_strips_path_traversal(tmp_path):
"""A malicious Topaz header tag must not write outside the output dir.
Regression test for the arbitrary-file-write fix: the record name is read
verbatim from the (untrusted) book file, so ``../../evil`` must be reduced
to a basename inside ``outdir`` rather than escaping it.
"""
outdir = tmp_path / "out"
outdir.mkdir()
book = _bare_book(outdir)
evil_tag = b"../../evil"
book.bookHeaderRecords = {evil_tag: [[0, 5, 0]]}
# Attacker-controlled payload bytes, no key required.
book.getBookPayloadRecord = lambda name, index: b"PWNED"
book.extractFiles()
# The traversal sequence was stripped: nothing was written above outdir.
assert not (tmp_path / "evil0000.dat").exists()
assert not (tmp_path.parent / "evil0000.dat").exists()
# The (sanitised) write landed safely inside outdir.
assert (outdir / "evil0000.dat").read_bytes() == b"PWNED"
def test_extractfiles_writes_normal_record(tmp_path):
outdir = tmp_path / "out"
outdir.mkdir()
book = _bare_book(outdir)
book.bookHeaderRecords = {b"spam": [[0, 4, 0]]}
book.getBookPayloadRecord = lambda name, index: b"eggs"
book.extractFiles()
assert (outdir / "spam0000.dat").read_bytes() == b"eggs"