Files
unshackle/tests/tracks/test_fragment_decrypted.py
imSp4rky 21d5f1c5e1 refactor(core)!: drop leading underscores from internal definitions
Renames 314 defs and 41 module/class-level constants. Keeps 11 names underscored where the stripped form collides in scope, plus 5.5.0 shims for the three symbols external service repos call.
2026-08-15 22:07:41 -06:00

168 lines
7.2 KiB
Python

"""Self-check for ``assert_fragments_decrypted``: after decryption every moof must have lost
its standard senc. A survivor means the tool silently skipped that fragment (the
tfhd.sample_description_index ceiling bug), which the moov-scoped has_encrypted_sample_entry
structurally cannot see. A leftover PIFF uuid survives a good decrypt too, because content
shipping both boxes keeps it: a decrypter detaches only the atom it consumed.
The guard never reads mdat, so stray byte runs there leave it unmoved.
Run: uv run pytest tests/tracks/test_fragment_decrypted.py
"""
from __future__ import annotations
import struct
from pathlib import Path
import pytest
from unshackle.core.manifests.ism_init import PIFF_SENC_UUID
from unshackle.core.tracks.track import assert_fragments_decrypted
def box(fmt: bytes, payload: bytes = b"") -> bytes:
return struct.pack(">I", 8 + len(payload)) + fmt + payload
def senc(sample_count: int = 4) -> bytes:
return box(b"senc", b"\x00" * 4 + struct.pack(">I", sample_count) + b"\x00" * (8 * sample_count))
def piff_senc(sample_count: int = 4) -> bytes:
body = PIFF_SENC_UUID + b"\x00" * 4 + struct.pack(">I", sample_count) + b"\x00" * (8 * sample_count)
return box(b"uuid", body)
def moof(*children: bytes, sdi: int = 1) -> bytes:
tfhd = box(b"tfhd", b"\x00\x00\x00\x02" + struct.pack(">II", 1, sdi)) # sample-description-index-present
return box(b"moof", box(b"mfhd", b"\x00" * 8) + box(b"traf", tfhd + b"".join(children)))
FTYP = box(b"ftyp", b"isom\x00\x00\x02\x00isomiso2")
MOOV = box(b"moov", box(b"mvex", box(b"trex", b"\x00" * 24)))
MDAT = box(b"mdat", b"\x00" * 64)
# a compressed payload can contain anything, including plausible-looking box runs
STRAY = box(b"mdat", struct.pack(">I", 64) + b"senc" + b"\xbe\xef" * 8 + PIFF_SENC_UUID)
def write(tmp_path: Path, name: str, *boxes: bytes) -> Path:
f = tmp_path / name
f.write_bytes(b"".join(boxes))
return f
def test_all_fragments_decrypted_passes(tmp_path: Path) -> None:
f = write(tmp_path, "clean.mp4", FTYP, MOOV, moof(), MDAT, moof(sdi=2), MDAT)
assert_fragments_decrypted(f)
def test_surviving_senc_raises(tmp_path: Path) -> None:
# the real bug: fragment 1 (index 1) decrypted, the rest (index 2) silently skipped
boxes = [FTYP, MOOV, moof(), MDAT]
for _ in range(3):
boxes += [moof(senc(), sdi=2), MDAT]
f = write(tmp_path, "skipped.mp4", *boxes)
with pytest.raises(ValueError, match=r"3/4 fragment"):
assert_fragments_decrypted(f)
def test_surviving_cenc_senc_raises(tmp_path: Path) -> None:
f = write(tmp_path, "cenc.mp4", FTYP, MOOV, moof(senc()), MDAT)
with pytest.raises(ValueError, match=r"1/1 fragment"):
assert_fragments_decrypted(f)
def test_leftover_piff_uuid_after_senc_removed_does_not_raise(tmp_path: Path) -> None:
# live DASH shape: the traf shipped senc+saiz+saio plus a duplicate PIFF uuid. mp4decrypt
# consumed senc, detached senc/saiz/saio, and left the uuid on a fully decrypted fragment.
boxes = [FTYP, MOOV]
for _ in range(3):
boxes += [moof(piff_senc()), MDAT]
f = write(tmp_path, "piff_leftover.mp4", *boxes)
assert_fragments_decrypted(f)
def test_stray_senc_bytes_in_mdat_ignored(tmp_path: Path) -> None:
f = write(tmp_path, "stray.mp4", FTYP, MOOV, moof(), STRAY, moof(sdi=2), STRAY)
assert_fragments_decrypted(f)
def test_senc_text_without_plausible_size_ignored(tmp_path: Path) -> None:
# "senc" inside a free-box string has no valid box size before it
f = write(tmp_path, "text.mp4", FTYP, MOOV, moof(box(b"free", b"presence sencoded here")), MDAT)
assert_fragments_decrypted(f)
def test_senc_bytes_inside_trun_payload_ignored(tmp_path: Path) -> None:
# a trun sample entry spelling "senc" after a size-looking duration; only a byte scan bites
trun = box(b"trun", b"\x00\x00\x03\x01" + struct.pack(">I", 2) + struct.pack(">I", 512) + b"senc")
f = write(tmp_path, "trun.mp4", FTYP, MOOV, moof(trun), MDAT)
assert_fragments_decrypted(f)
def test_empty_senc_on_clear_fragment_ignored(tmp_path: Path) -> None:
# sample_count 0 marks a fragment with no protected samples, so the guard ignores it
f = write(tmp_path, "empty.mp4", FTYP, MOOV, moof(senc(sample_count=0)), MDAT)
assert_fragments_decrypted(f)
def senc_override(sample_count: int) -> bytes:
# flags&1 => AlgorithmID(3) + IV_size(1) + KID(16) precede sample_count
head = b"\x00\x00\x00\x01" + b"\x00\x00\x08" + b"\x08" + b"\xaa" * 16
return box(b"senc", head + struct.pack(">I", sample_count) + b"\x00" * (8 * sample_count))
def test_override_flag_shifts_sample_count_offset(tmp_path: Path) -> None:
clear = write(tmp_path, "ovr_clear.mp4", FTYP, MOOV, moof(senc_override(0)), MDAT)
assert_fragments_decrypted(clear)
f = write(tmp_path, "ovr.mp4", FTYP, MOOV, moof(senc_override(5)), MDAT)
with pytest.raises(ValueError, match=r"1/1 fragment"):
assert_fragments_decrypted(f)
def test_senc_outside_traf_ignored(tmp_path: Path) -> None:
# a decrypted moof whose only "senc" sits in a sibling box outside any traf
f = write(tmp_path, "sibling.mp4", FTYP, MOOV, box(b"moof", box(b"mfhd", b"\x00" * 8) + senc()), MDAT)
assert_fragments_decrypted(f)
def test_senc_nested_under_non_traf_ignored(tmp_path: Path) -> None:
# senc two levels down under a non-traf container: only traf children count
meta = box(b"meta", senc())
f = write(tmp_path, "nested.mp4", FTYP, MOOV, box(b"moof", box(b"mfhd", b"\x00" * 8) + meta), MDAT)
assert_fragments_decrypted(f)
def test_undersized_largesize_box_does_not_desync_walk(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
# size==1 reads a 16-byte header, so a largesize of 12 clears the 8-byte floor but
# still stalls the cursor onto payload; only a header-aware floor catches it
runt = struct.pack(">I", 1) + b"free" + struct.pack(">Q", 12)
f = write(tmp_path, "runt.mp4", FTYP, MOOV, runt, moof(senc()), MDAT, moof(senc()), MDAT)
with caplog.at_level("WARNING"):
assert_fragments_decrypted(f)
assert "malformed box size" in caplog.text
def test_largesize_and_zero_size_boxes_walked(tmp_path: Path) -> None:
frag = moof(senc())
large = struct.pack(">I", 1) + b"mdat" + struct.pack(">Q", 16 + 64) + b"\x00" * 64
trailing = struct.pack(">I", 0) + b"mdat" + b"\x00" * 64 # size 0 => runs to EOF
f = write(tmp_path, "large.mp4", FTYP, MOOV, frag, large, trailing)
with pytest.raises(ValueError, match=r"1/1 fragment"):
assert_fragments_decrypted(f)
def test_malformed_box_size_warns_and_does_not_raise(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
# failing open is deliberate, but the abort must be logged so it cannot pass as clean
overrun = struct.pack(">I", 1 << 30) + b"free" + b"\x00" * 8
f = write(tmp_path, "overrun.mp4", FTYP, MOOV, overrun, moof(senc()), MDAT)
with caplog.at_level("WARNING"):
assert_fragments_decrypted(f)
assert "malformed box size" in caplog.text
def test_missing_or_bogus_file_is_noop(tmp_path: Path) -> None:
assert_fragments_decrypted(tmp_path / "nope.mp4")
assert_fragments_decrypted(write(tmp_path, "bogus.bin", b"\x00\x01\x02\x03not-an-mp4"))
truncated = write(tmp_path, "cut.mp4", FTYP, MOOV, moof(senc())[:20])
assert_fragments_decrypted(truncated)