open-standards/wai/pack-conformance/cmaf_diff.py
Transaction Science 4e0c3fd8c9 cmaf_diff.py: the timescale case alters mdhd's timescale (#426 review S5)
#426 review S5. The "timescale altered" branch wrote at mdhd's type offset + 20,
which in version 0 is the duration, not the timescale (+16; +24 in version 1).
Neither reader reads the duration, so the branch always gave ok/ok and a timescale
of 0 was reached only by a stray bit flip. The offset now follows the box's version;
on video-cmaf-01's init, +16 reads 1,200,000 and +20 the duration, 0. The elst
offsets were right.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 21:01:58 -04:00

152 lines
6.3 KiB
Python

#!/usr/bin/env python3
"""The supplied CMAF reader differentially (extensions/packaging.md §7.7): wai-pack's
(`wai-pack cmaf-verdict`) against pack_verify.py's, which shares no code with it, on
seeded alterations of real files. The files are the CMAF golden's floor (a media muxer's
initialization and media segments, packs/video-cmaf-01/floor/) and §7.6's writer's (the
ladder golden's package, packs/video-ladder-01/package/floor/). Each case alters the
initialization segment or one media segment: a byte flipped in its first boxes, a 32-bit
field overwritten at a box boundary, the file cut short, a box's size changed, a box
inserted at the top level or in a moof, the init's timescale or edit list altered, or a
segment's moof and mdat repeated. Every verdict (ok, floor-cmaf-malformed or
floor-cmaf-timing), and for an accepted segment its samples (offset, length, duration,
flags, decode time), must agree, and no reader may panic.
python3 cmaf_diff.py <wai-pack binary> <seed> <cases>
"""
import hashlib
import os
import random
import subprocess
import sys
import tempfile
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import pack_verify as PV # noqa: E402
def py_init(b):
try:
return "ok", PV.cmaf_init(b)
except PV.CmafFault as x:
return x.code, None
except Exception as x: # a reader must refuse, never fail otherwise
return f"CRASH {type(x).__name__}", None
def py_segment(b, init):
try:
s = PV.cmaf_segment(b, init)
except PV.CmafFault as x:
return (x.code, 0, "-")
except Exception as x:
return (f"CRASH {type(x).__name__}", 0, "-")
text = "".join(f"{a},{n},{d},{f},{t};" for a, n, d, f, t in s)
return ("ok", len(s), hashlib.sha256(text.encode()).hexdigest())
def boxes(b, start, end):
try:
return PV.bmff_boxes(b, start, end)
except PV.CmafFault:
return []
def alter(rng, b):
b = bytearray(b)
top = boxes(b, 0, len(b))
k = rng.randrange(8)
if k == 6:
# The times: an init's timescale or its edit list's media time, at their offsets
# from the box type. `mdhd`'s timescale follows its version and flags and its two
# times, 4 bytes each in version 0 and 8 in version 1.
for kind, off in ((b"mdhd", None), (b"elst", 20), (b"elst", 16)):
at = bytes(b).find(kind)
if at >= 0 and off is None:
off = 16 if at + 4 < len(b) and b[at + 4] == 0 else 24
if at >= 0 and rng.random() < 0.5 and at + off + 4 <= len(b):
b[at + off:at + off + 4] = rng.choice([0, 1, 2, rng.randrange(1 << 32)]).to_bytes(4, "big")
return bytes(b)
k = 0
if k == 7:
# A media segment's moof and mdat repeated: the second's decode time is the first's.
pairs = [i for i, x in enumerate(top) if x[0] == b"moof" and i + 1 < len(top) and top[i + 1][0] == b"mdat"]
if pairs:
i = rng.choice(pairs)
b += b[top[i][1]:top[i + 1][3]]
return bytes(b)
k = 0
if k == 0 and b:
b[rng.randrange(min(len(b), 400))] ^= 1 << rng.randrange(8)
elif k == 1 and top:
x = rng.choice(top)
inner = boxes(b, x[2], x[3]) if x[0] in (b"moov", b"moof", b"trak", b"traf", b"mdia", b"minf", b"stbl", b"mvex") else []
y = rng.choice(inner) if inner and rng.random() < 0.7 else x
at = y[1] + rng.choice([8, 12, 16, 20, 24])
if at + 4 <= len(b):
b[at:at + 4] = rng.choice([0, 1, 7, 0xFFFFFFFF, rng.randrange(1 << 32)]).to_bytes(4, "big")
elif k == 2 and len(b) > 8:
del b[rng.randrange(8, len(b)):]
elif k == 3 and top:
x = rng.choice(top)
b[x[1]:x[1] + 4] = max(0, (x[3] - x[1]) + rng.choice([-9, -1, 1, 4, 8, -(x[3] - x[1])])).to_bytes(4, "big")
elif k == 4 and top:
kind = rng.choice([b"free", b"skip", b"uuid", b"styp", b"mdat", b"moof"])
box = (8 + 4).to_bytes(4, "big") + kind + bytes(4)
at = rng.choice([x[1] for x in top] + [len(b)])
b[at:at] = box
elif top:
moofs = [x for x in top if x[0] == b"moof"]
if moofs:
x = rng.choice(moofs)
inner = boxes(b, x[2], x[3])
at = rng.choice([y[1] for y in inner] + [x[3]])
kind = rng.choice([b"sbgp", b"free", b"tfdt", b"trun"])
box = (8 + 8).to_bytes(4, "big") + kind + bytes(8)
b[at:at] = box
b[x[1]:x[1] + 4] = (x[3] - x[1] + len(box)).to_bytes(4, "big")
return bytes(b)
def main(argv):
if len(argv) != 4:
print(__doc__)
return 2
wai_pack, seed, n = argv[1], int(argv[2]), int(argv[3])
rng = random.Random(seed)
root = os.path.join(HERE, "packs")
sets = []
for d in (os.path.join(root, "video-cmaf-01", "floor", "hi"), os.path.join(root, "video-cmaf-01", "floor", "lo"),
os.path.join(root, "video-ladder-01", "package", "floor", "hi")):
names = sorted(os.listdir(d))
sets.append((PV.load(os.path.join(d, "init.mp4")), [PV.load(os.path.join(d, x)) for x in names if x.endswith(".m4s")]))
bad, seen = 0, {}
with tempfile.TemporaryDirectory() as tmp:
for c in range(n):
init, segs = rng.choice(sets)
seg = rng.choice(segs)
if rng.random() < 0.25:
init = alter(rng, init)
else:
seg = alter(rng, seg)
ip, sp = os.path.join(tmp, "init.mp4"), os.path.join(tmp, "seg.m4s")
open(ip, "wb").write(init)
open(sp, "wb").write(seg)
out = subprocess.run([wai_pack, "cmaf-verdict", ip, sp], capture_output=True, text=True).stdout.splitlines()
iv, pi = py_init(init)
want = [f"init\t{iv}"]
if iv == "ok":
v = py_segment(seg, pi)
want.append(f"{sp}\t{v[0]}\t{v[1]}\t{v[2]}")
verdict = want[-1].split("\t")[1]
seen[verdict] = seen.get(verdict, 0) + 1
if out != want or any("PANIC" in x or "CRASH" in x for x in out + want):
bad += 1
if bad <= 10:
print(f"case {c}: wai-pack {out}, pack_verify {want}")
print(f"cmaf_diff: {n - bad}/{n} cases agree; verdicts {dict(sorted(seen.items()))}")
return 0 if bad == 0 else 1
if __name__ == "__main__":
sys.exit(main(sys.argv))