Some checks failed
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 7s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 32s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Failing after 1m54s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 4m14s
wai-ledger service / does this change touch it (push) Has been cancelled
wai-ledger service / service · linux (push) Has been cancelled
wai-ledger service / service · macos (push) Has been cancelled
packaging §7.9: a sound track beside the lead whose base is wai.audio.flac may carry one floor rung of wai.audio.opus, supplied in the CMAF form at 48 kHz: an existing ladder's Opus in ISOBMFF (<f>/init.mp4 with its dOps, and its media segments) beside the lead's floors, under the one listing the policy pins. The packer refuses, before it writes anything, an init that is not one Opus track at 48 000 of channel mapping family 0 in the source's channels, a segment whose samples are not sync samples or do not run on from the one before it, a packet that breaks RFC 6716 §3.4 (floor-opus-malformed, new), a sample that does not last its packet, packets that do not last the programme's samples and the pre-skip, and segments other than one per piece and a tail, the packet that carries the programme's last pre-skip samples past its end on the decode timeline. Each piece's floor object is the Ogg Opus stream (RFC 7845) that decodes alone to exactly the piece's samples: 80 ms of pre-roll and the encoder's delay in its OpusHead pre-skip, its last granule position the piece's end, every byte a function of the packets, its derived_from naming the piece's FLAC base. The package carries the ladder's CMAF files byte for byte; wai-pack verify reads them through floor.listing and writes every floor object again. wai-pack gains opus_floor (the init reader, the packet check, the Ogg writer); wai.audio.opus takes source = "supplied" (§3.3), in pack_verify.py and policy_diff.py too. pack-conformance/multi/multi-opus-01: a floor-only lead and a sound track with its Opus floor, floor/a/ written by supplied_opus.py (ffmpeg's libopus at 128 kb/s, its HLS muxer's fragments grouped a piece to a segment); multi_verify.py reads the floor and writes each object again with its own reader and Ogg writer, and finds a flipped object byte, a flipped segment byte and a floor.listing cut short. tests/multi.rs: the objects decode alone to their pieces, the floor out of its CMAF is refused, and what an Opus floor does not take is refused by name. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1055 lines
42 KiB
Python
1055 lines
42 KiB
Python
#!/usr/bin/env python3
|
|
"""Independent reference for the packages of several components (extensions/packaging.md
|
|
§4.5 and §5.8), over pack-conformance/multi/.
|
|
|
|
Written from the extension text. It shares no code with wai-pack, which writes the
|
|
corpus. It takes BLAKE3 from ../claim-conformance/_blake3.py and Ed25519 verification
|
|
from ../claim-conformance/_ed25519.py, and renders vector graphics with
|
|
../tools/wai_vector_check.py, each written from its own specification. From each golden's
|
|
inputs alone (its policy, its committed sources, the stereo WAV of the formula below, and
|
|
the lead's plan file) it recomputes, for every component beside the lead:
|
|
|
|
- its pieces: the lead's starts and end converted to its timebase (§4.5), against its
|
|
stage index's piece records, each flagged aligned to the lead;
|
|
- each base's decoded-output digest (§6.5, §5.8), from the source, never from the
|
|
package's objects: the SHA-256 of the WAV's interleaved s16le samples of the piece; of
|
|
the score's mixdown of the piece (one channel, or two interleaved), by the reference
|
|
synthesis of score.md §3, its voices included, in the Fx floor written here; the clip digest of the source document's frames of the piece at its
|
|
first listed size and the component's rate (graphics-vector §3.1, P7); the SHA-256 of
|
|
the WebVTT segment of the piece (timed-text §5.2), written here; and zero for a feed;
|
|
- each object's bytes where §5.8 fixes them from the source: the FLAC of each audio piece
|
|
and of each score piece's mixdown, by the encoder §5.8 defines, written here; each text
|
|
piece's segment, and its target, the component's language and the piece's span; and
|
|
each object named by its content hash;
|
|
- each feed piece's seal: its final tick, and its signature by the receipt's key.
|
|
|
|
It then plants a fault in a copy of each golden, one per component, and requires the
|
|
recomputation to find it.
|
|
|
|
python3 -I multi_verify.py <pack-conformance dir>
|
|
|
|
Exit status 0 only if every golden holds and every planted fault is found.
|
|
"""
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
import math
|
|
import os
|
|
import struct
|
|
import sys
|
|
import tomllib
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
def _load(name, path):
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
m = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(m)
|
|
return m
|
|
|
|
|
|
B3 = _load("_blake3", os.path.join(HERE, "..", "claim-conformance", "_blake3.py"))
|
|
ED = _load("_ed25519", os.path.join(HERE, "..", "claim-conformance", "_ed25519.py"))
|
|
WVC = _load("wai_vector_check", os.path.join(HERE, "..", "tools", "wai_vector_check.py"))
|
|
|
|
GOLDENS = ["multi-av-01", "multi-titles-01", "multi-feed-01", "multi-sting-01", "multi-opus-01"]
|
|
FLAG_ALIGNED = 8
|
|
|
|
|
|
def sha256(b):
|
|
return hashlib.sha256(b).digest()
|
|
|
|
|
|
def le(b, at, n):
|
|
return int.from_bytes(b[at:at + n], "little")
|
|
|
|
|
|
# ---------------------------------------------------------------- the stage index (§6.2)
|
|
|
|
def read_index(b):
|
|
n_p, n_e = le(b, 16, 4), le(b, 20, 4)
|
|
pieces = [{"start": le(b, 160 + 32 * k, 8), "dur": le(b, 168 + 32 * k, 4), "first": le(b, 172 + 32 * k, 4),
|
|
"n": le(b, 176 + 32 * k, 2), "kind": b[178 + 32 * k], "flags": b[179 + 32 * k]} for k in range(n_p)]
|
|
at = 160 + 32 * n_p
|
|
entries = []
|
|
for k in range(n_e):
|
|
e = b[at + 128 * k:at + 128 * (k + 1)]
|
|
entries.append({"hash": e[0:32], "len": le(e, 32, 8), "role": e[54], "tier": e[56], "digest": e[64:96]})
|
|
return {"media": le(b, 6, 2), "tb": (le(b, 8, 4), le(b, 12, 4)), "pieces": pieces, "entries": entries}
|
|
|
|
|
|
def payload_of(env):
|
|
"""A WAI1 envelope's payload: `WAI1`, u32 manifest length, the manifest, u32 payload
|
|
length, the payload."""
|
|
assert env[:4] == b"WAI1"
|
|
ml = le(env, 4, 4)
|
|
pl = le(env, 8 + ml, 4)
|
|
assert len(env) == 12 + ml + pl
|
|
return env[12 + ml:]
|
|
|
|
|
|
def manifest_of(env):
|
|
"""A WAI1 envelope's manifest, as JSON."""
|
|
return json.loads(env[8:8 + le(env, 4, 4)])
|
|
|
|
|
|
def content_hash(env):
|
|
return B3.blake3(b"moq-jwp:object\x01" + env)
|
|
|
|
|
|
# ---------------------------------------------------------------- §4.5
|
|
|
|
def align(units, lead, tb):
|
|
return units * tb[0] * lead[1] // (tb[1] * lead[0])
|
|
|
|
|
|
def spans(lead_starts, lead_end, lead, tb):
|
|
at = [align(s, lead, tb) for s in lead_starts] + [align(lead_end, lead, tb)]
|
|
return [(at[i], at[i + 1] - at[i]) for i in range(len(lead_starts))]
|
|
|
|
|
|
# ---------------------------------------------------------------- the WAV (multi_fixture)
|
|
|
|
def wav_sample(k, c):
|
|
if c == 0:
|
|
return ((k * 7) % 400) * 80 - 16000
|
|
t = k % 200
|
|
return (t * 300 if t < 100 else (200 - t) * 300) - 15000
|
|
|
|
|
|
def s16le(samples):
|
|
return b"".join(struct.pack("<h", s) for s in samples)
|
|
|
|
|
|
# ---------------------------------------------------------------- FLAC, §5.8
|
|
|
|
class Bits:
|
|
def __init__(self):
|
|
self.out = bytearray()
|
|
self.acc = 0
|
|
self.n = 0
|
|
|
|
def put(self, v, bits):
|
|
for i in range(bits - 1, -1, -1):
|
|
self.acc = (self.acc << 1) | ((v >> i) & 1)
|
|
self.n += 1
|
|
if self.n == 8:
|
|
self.out.append(self.acc)
|
|
self.acc, self.n = 0, 0
|
|
|
|
def unary(self, q):
|
|
for _ in range(q):
|
|
self.put(0, 1)
|
|
self.put(1, 1)
|
|
|
|
def align(self):
|
|
while self.n:
|
|
self.put(0, 1)
|
|
|
|
|
|
def crc8(b):
|
|
c = 0
|
|
for x in b:
|
|
c ^= x
|
|
for _ in range(8):
|
|
c = ((c << 1) ^ 0x07) & 0xFF if c & 0x80 else (c << 1) & 0xFF
|
|
return c
|
|
|
|
|
|
def crc16(b):
|
|
c = 0
|
|
for x in b:
|
|
c ^= x << 8
|
|
for _ in range(8):
|
|
c = ((c << 1) ^ 0x8005) & 0xFFFF if c & 0x8000 else (c << 1) & 0xFFFF
|
|
return c
|
|
|
|
|
|
def coded_number(v):
|
|
if v < 0x80:
|
|
return bytes([v])
|
|
for n, lead, lim in ((2, 0xC0, 0x800), (3, 0xE0, 0x10000), (4, 0xF0, 0x200000), (5, 0xF8, 0x4000000), (6, 0xFC, 1 << 31)):
|
|
if v < lim:
|
|
return bytes([lead | (v >> (6 * (n - 1)))] + [0x80 | ((v >> (6 * i)) & 0x3F) for i in range(n - 2, -1, -1)])
|
|
raise ValueError(v)
|
|
|
|
|
|
def fixed_residual(x, i, order):
|
|
return [x[i], x[i] - x[i - 1], x[i] - 2 * x[i - 1] + x[i - 2], x[i] - 3 * x[i - 1] + 3 * x[i - 2] - x[i - 3],
|
|
x[i] - 4 * x[i - 1] + 6 * x[i - 2] - 4 * x[i - 3] + x[i - 4]][order] if order <= 4 else None
|
|
|
|
|
|
def subframe(w, x):
|
|
n = len(x)
|
|
best, best_bits = None, None
|
|
for order in range(5):
|
|
if order >= n:
|
|
break
|
|
u = [(r << 1) if r >= 0 else ((-r) << 1) - 1 for r in (fixed_residual(x, i, order) for i in range(order, n))]
|
|
p, bits = min(((p, sum(1 + p + (v >> p) for v in u)) for p in range(15)), key=lambda t: (t[1], t[0]))
|
|
total = 8 + 16 * order + 10 + bits
|
|
if best_bits is None or total < best_bits:
|
|
best, best_bits = (order, p, u), total
|
|
if best is None or 8 + 16 * n < best_bits:
|
|
w.put(0x02, 8)
|
|
for s in x:
|
|
w.put(s & 0xFFFF, 16)
|
|
return
|
|
order, p, u = best
|
|
w.put((0b001000 | order) << 1, 8)
|
|
for s in x[:order]:
|
|
w.put(s & 0xFFFF, 16)
|
|
w.put(0, 2)
|
|
w.put(0, 4)
|
|
w.put(p, 4)
|
|
for v in u:
|
|
w.unary(v >> p)
|
|
w.put(v & ((1 << p) - 1), p)
|
|
|
|
|
|
def flac(samples, ch, rate, block=4096):
|
|
frames = len(samples) // ch
|
|
w = Bits()
|
|
w.out += b"fLaC"
|
|
w.put(0x80, 8)
|
|
w.put(34, 24)
|
|
w.put(block, 16)
|
|
w.put(block, 16)
|
|
w.put(0, 24)
|
|
w.put(0, 24)
|
|
w.put(rate, 20)
|
|
w.put(ch - 1, 3)
|
|
w.put(15, 5)
|
|
w.put(frames, 36)
|
|
w.put(0, 128)
|
|
for k, start in enumerate(range(0, frames, block)):
|
|
n = min(block, frames - start)
|
|
at = len(w.out)
|
|
head = bytes([0xFF, 0xF8, 0x70, ((ch - 1) << 4) | 0x08]) + coded_number(k) + (n - 1).to_bytes(2, "big")
|
|
w.out += head + bytes([crc8(head)])
|
|
for c in range(ch):
|
|
subframe(w, [samples[i * ch + c] for i in range(start, start + n)])
|
|
w.align()
|
|
w.out += crc16(bytes(w.out[at:])).to_bytes(2, "big")
|
|
return bytes(w.out)
|
|
|
|
|
|
# ---------------------------------------------------------------- the score, score.md §3
|
|
|
|
I64_MAX, I64_MIN = (1 << 63) - 1, -(1 << 63)
|
|
ONE, HALF, QUARTER = 1 << 32, 1 << 31, 1 << 30
|
|
SAW, SQUARE, TRIANGLE, PULSE25, SAW_BL, SQUARE_BL, PULSE25_BL, SINE, NOISE = range(9)
|
|
SINE_P = 966367642 # 0.225 in Q32.32, rounded (§3.2)
|
|
VOICE = struct.Struct("<qBBBBqqqQIBQqq") # §2.4, 73 bytes
|
|
VOICE_FIELDS = ["pan", "attack_curve", "decay_curve", "release_curve", "lp_poles", "lp_a", "lp_env", "glide_to",
|
|
"glide", "seed", "echo_taps", "echo_delay", "echo_gain", "echo_pan"]
|
|
|
|
|
|
def sat(v):
|
|
return max(I64_MIN, min(I64_MAX, v))
|
|
|
|
|
|
def fmul(a, b):
|
|
return sat((a * b) >> 32)
|
|
|
|
|
|
def fdiv(a, b):
|
|
if b == 0:
|
|
return I64_MAX if a >= 0 else I64_MIN
|
|
return sat((a << 32) // b)
|
|
|
|
|
|
def neg(a):
|
|
return I64_MAX if a == I64_MIN else -a
|
|
|
|
|
|
def fabs(a):
|
|
return neg(a) if a < 0 else a
|
|
|
|
|
|
def fsqrt(a):
|
|
"""The profile's square root: the floor square root of the raw bits times 2^32, and
|
|
0 for a value of 0 or below."""
|
|
return 0 if a <= 0 else math.isqrt(a << 32)
|
|
|
|
|
|
def wrap(p):
|
|
return sat(p - ONE) if p >= ONE else p
|
|
|
|
|
|
def blep(t, dt):
|
|
"""The polynomial correction at phase t of a step at phase 0, at increment dt (§3.2)."""
|
|
if dt <= 0:
|
|
return 0
|
|
if t < dt:
|
|
x = fdiv(t, dt)
|
|
return sat(sat(sat(x + x) - fmul(x, x)) - ONE)
|
|
if t > sat(ONE - dt):
|
|
x = fdiv(sat(t - ONE), dt)
|
|
return sat(sat(sat(fmul(x, x) + x) + x) + ONE)
|
|
return 0
|
|
|
|
|
|
def osc(phase, kind, dt, held):
|
|
if kind == SAW:
|
|
return sat(fmul(2 * ONE, phase) - ONE)
|
|
if kind == SQUARE:
|
|
return ONE if phase < HALF else -ONE
|
|
if kind == TRIANGLE:
|
|
return sat(fmul(4 * ONE, phase) - ONE) if phase < HALF else sat(3 * ONE - fmul(4 * ONE, phase))
|
|
if kind == PULSE25:
|
|
return ONE if phase < QUARTER else -ONE
|
|
if kind == SAW_BL:
|
|
return sat(sat(fmul(2 * ONE, phase) - ONE) - blep(phase, dt))
|
|
if kind == SQUARE_BL:
|
|
naive = ONE if phase < HALF else -ONE
|
|
return sat(sat(naive + blep(phase, dt)) - blep(wrap(sat(phase + HALF)), dt))
|
|
if kind == PULSE25_BL:
|
|
naive = ONE if phase < QUARTER else -ONE
|
|
return sat(sat(naive + blep(phase, dt)) - blep(wrap(sat(sat(phase + HALF) + QUARTER)), dt))
|
|
if kind == SINE:
|
|
u = sat(fmul(2 * ONE, phase) - ONE)
|
|
y = fmul(fmul(4 * ONE, u), sat(ONE - fabs(u)))
|
|
y = sat(fmul(SINE_P, sat(fmul(y, fabs(y)) - y)) + y)
|
|
return neg(y)
|
|
if kind == NOISE:
|
|
return held
|
|
return 0
|
|
|
|
|
|
class Osc:
|
|
"""A note's phase, and its noise generator: s <- 1664525 s + 1013904223 (mod 2^32),
|
|
its top 24 bits held as a value in [-1, 1) until the phase wraps."""
|
|
|
|
def __init__(self, seed):
|
|
self.phase, self.lcg = 0, seed
|
|
self.draw()
|
|
|
|
def draw(self):
|
|
self.lcg = (self.lcg * 1664525 + 1013904223) & 0xFFFFFFFF
|
|
self.held = ((self.lcg >> 8) - (1 << 23)) << 9
|
|
|
|
def advance(self, dt, kind):
|
|
self.phase = sat(self.phase + dt)
|
|
if self.phase >= ONE:
|
|
self.phase = sat(self.phase - ONE)
|
|
if kind == NOISE:
|
|
self.draw()
|
|
|
|
|
|
def adsr(t, n):
|
|
"""The linear ADSR of a score with no voices (§3.1)."""
|
|
if n["attack"] > 0 and t < n["attack"]:
|
|
return fdiv(t << 32, n["attack"] << 32)
|
|
ad = n["attack"] + n["decay"]
|
|
if t < ad:
|
|
return sat(ONE - fdiv(fmul(sat(ONE - n["sustain"]), (t - n["attack"]) << 32), n["decay"] << 32)) if n["decay"] > 0 else n["sustain"]
|
|
if t < n["dur"]:
|
|
return n["sustain"]
|
|
if n["release"] > 0 and t < n["dur"] + n["release"]:
|
|
return fmul(n["sustain"], sat(ONE - fdiv((t - n["dur"]) << 32, n["release"] << 32)))
|
|
return 0
|
|
|
|
|
|
def frac(t, n):
|
|
return (t << 32) // n
|
|
|
|
|
|
def curve(v, k):
|
|
r = v
|
|
for _ in range(1, k):
|
|
r = fmul(r, v)
|
|
return r
|
|
|
|
|
|
def held_level(t, n, v):
|
|
"""A voiced note's attack, decay and sustain (§3.3)."""
|
|
if n["attack"] > 0 and t < n["attack"]:
|
|
return sat(ONE - curve(sat(ONE - frac(t, n["attack"])), v["attack_curve"]))
|
|
td = t - min(n["attack"], t)
|
|
if n["decay"] > 0 and td < n["decay"]:
|
|
return sat(n["sustain"] + fmul(sat(ONE - n["sustain"]), curve(sat(ONE - frac(td, n["decay"])), v["decay_curve"])))
|
|
return n["sustain"]
|
|
|
|
|
|
def voiced_env(t, n, v, off):
|
|
if t < n["dur"]:
|
|
return held_level(t, n, v)
|
|
tr = t - n["dur"]
|
|
if n["release"] > 0 and tr < n["release"]:
|
|
return fmul(off, curve(sat(ONE - frac(tr, n["release"])), v["release_curve"]))
|
|
return 0
|
|
|
|
|
|
def pan_gains(p):
|
|
return fsqrt(sat(ONE - p) >> 1), fsqrt(sat(ONE + p) >> 1)
|
|
|
|
|
|
def put(buf, two, i, x, gains):
|
|
if two:
|
|
buf[2 * i] = sat(buf[2 * i] + fmul(x, gains[0]))
|
|
buf[2 * i + 1] = sat(buf[2 * i + 1] + fmul(x, gains[1]))
|
|
else:
|
|
buf[i] = sat(buf[i] + x)
|
|
|
|
|
|
def render_plain(buf, two, duration, n):
|
|
centre = pan_gains(0)
|
|
o = Osc(0)
|
|
for i in range(n["start"], min(n["start"] + n["dur"] + n["release"], duration)):
|
|
x = fmul(fmul(osc(o.phase, n["waveform"], n["phase_inc"], o.held), adsr(i - n["start"], n)), n["gain"])
|
|
put(buf, two, i, x, centre)
|
|
o.advance(n["phase_inc"], n["waveform"])
|
|
|
|
|
|
def render_voiced(buf, two, duration, n, v):
|
|
gains = pan_gains(v["pan"])
|
|
echo, level = [], ONE
|
|
for j in range(1, min(v["echo_taps"], 8) + 1):
|
|
level = fmul(level, v["echo_gain"])
|
|
echo.append((level, pan_gains(v["echo_pan"] if j % 2 == 1 else neg(v["echo_pan"]))))
|
|
poles = min(v["lp_poles"], 4)
|
|
off = held_level(n["dur"], n, v)
|
|
delta = v["glide_to"] - n["phase_inc"]
|
|
o = Osc(v["seed"])
|
|
y = [0] * 4
|
|
for i in range(n["start"], min(n["start"] + n["dur"] + n["release"], duration)):
|
|
t = i - n["start"]
|
|
dt = n["phase_inc"] if v["glide"] == 0 else sat(n["phase_inc"] + delta * min(t, v["glide"]) // v["glide"])
|
|
env = voiced_env(t, n, v, off)
|
|
x = osc(o.phase, n["waveform"], dt, o.held)
|
|
if poles:
|
|
a = min(ONE, max(0, sat(v["lp_a"] + fmul(v["lp_env"], env))))
|
|
for k in range(poles):
|
|
y[k] = sat(y[k] + fmul(a, sat(x - y[k])))
|
|
x = y[k]
|
|
x = fmul(fmul(x, env), n["gain"])
|
|
put(buf, two, i, x, gains)
|
|
for j, (g, eg) in enumerate(echo):
|
|
at = i + (j + 1) * v["echo_delay"]
|
|
if at >= duration:
|
|
break
|
|
put(buf, two, at, fmul(x, g), eg)
|
|
o.advance(dt, n["waveform"])
|
|
|
|
|
|
def quant(v):
|
|
v = max(-ONE, min(ONE, v))
|
|
num = v * 32767
|
|
return (num + HALF) >> 32 if num >= 0 else -((-num + HALF) >> 32)
|
|
|
|
|
|
def read_score(b):
|
|
"""A WSCR's contract, notes and voices (score §2): the voices None in a score of the
|
|
first revision's two sections."""
|
|
assert b[:4] == b"WSCR"
|
|
n = le(b, 4, 2)
|
|
sec = {}
|
|
base = 6 + 9 * n
|
|
for i in range(n):
|
|
kind, off, ln = b[6 + 9 * i], le(b, 7 + 9 * i, 4), le(b, 11 + 9 * i, 4)
|
|
sec[kind] = b[base + off:base + off + ln]
|
|
contract = json.loads(sec[1])
|
|
assert contract["channels"] in (1, 2)
|
|
nb = sec[2]
|
|
notes = []
|
|
for k in range(le(nb, 0, 4)):
|
|
f = struct.unpack_from("<QQqqBQQqQ", nb, 4 + 65 * k)
|
|
notes.append(dict(zip(["start", "dur", "phase_inc", "gain", "waveform", "attack", "decay", "sustain", "release"], f)))
|
|
voices = None
|
|
if 3 in sec:
|
|
assert len(sec[3]) == VOICE.size * len(notes)
|
|
voices = [dict(zip(VOICE_FIELDS, VOICE.unpack_from(sec[3], VOICE.size * k))) for k in range(len(notes))]
|
|
contract["voices"] = voices
|
|
return contract, notes
|
|
|
|
|
|
def score_contract(b):
|
|
n = le(b, 4, 2)
|
|
for i in range(n):
|
|
if b[6 + 9 * i] == 1:
|
|
off, ln = le(b, 7 + 9 * i, 4), le(b, 11 + 9 * i, 4)
|
|
return b[6 + 9 * n + off:6 + 9 * n + off + ln]
|
|
return None
|
|
|
|
|
|
def canonical_contract(c):
|
|
"""score §2.1's contract as canonical JSON: sorted keys, no whitespace."""
|
|
return ('{"channels":%d,"duration":%d,"numeric":"%s","sample_rate":%d}' % (c["channels"], c["duration"], c["numeric"], c["sample_rate"])).encode()
|
|
|
|
|
|
def write_score(b):
|
|
"""The canonical WSCR of a score's sections (score §2): the magic, the count, the
|
|
section table, then each section padded with zeros to a multiple of 8."""
|
|
n = le(b, 4, 2)
|
|
base = 6 + 9 * n
|
|
secs = [(b[6 + 9 * i], b[base + le(b, 7 + 9 * i, 4):base + le(b, 7 + 9 * i, 4) + le(b, 11 + 9 * i, 4)]) for i in range(n)]
|
|
out, off = bytearray(b"WSCR" + n.to_bytes(2, "little")), 0
|
|
for kind, s in secs:
|
|
out += bytes([kind]) + off.to_bytes(4, "little") + len(s).to_bytes(4, "little")
|
|
off += -(-len(s) // 8) * 8
|
|
for _, s in secs:
|
|
out += s + bytes(-(-len(s) // 8) * 8 - len(s))
|
|
return bytes(out)
|
|
|
|
|
|
def mixdown(contract, notes):
|
|
"""The canonical mixdown (§3): one channel, or two interleaved left, right."""
|
|
two = contract["channels"] == 2
|
|
duration = contract["duration"]
|
|
buf = [0] * (duration * (2 if two else 1))
|
|
for k, n in enumerate(notes):
|
|
if contract["voices"] is None:
|
|
render_plain(buf, two, duration, n)
|
|
else:
|
|
render_voiced(buf, two, duration, n, contract["voices"][k])
|
|
return [quant(v) for v in buf]
|
|
|
|
|
|
# ---------------------------------------------------------------- WebVTT, timed-text §5
|
|
|
|
def ts(ms):
|
|
return "%02d:%02d:%02d.%03d" % (ms // 3600000, ms // 60000 % 60, ms // 1000 % 60, ms % 1000)
|
|
|
|
|
|
def parse_ms(t):
|
|
h, m, s = t.split(":")
|
|
sec, milli = s.split(".")
|
|
return ((int(h) * 60 + int(m)) * 60 + int(sec)) * 1000 + int(milli)
|
|
|
|
|
|
def cue_blocks(doc):
|
|
"""A §5.1 document's cues: (start, end, the cue as written, its trailing line end
|
|
included)."""
|
|
assert doc.startswith("WEBVTT\n\n")
|
|
out = []
|
|
for blk in doc[len("WEBVTT\n\n"):].split("\n\n"):
|
|
blk = blk.rstrip("\n") + "\n"
|
|
a, b = blk.split("\n", 1)[0].split(" ")[0:3:2]
|
|
out.append((parse_ms(a), parse_ms(b), blk))
|
|
return out
|
|
|
|
|
|
def segment(cues, b0, b1, mpegts0=0):
|
|
head = "WEBVTT\nX-TIMESTAMP-MAP=LOCAL:%s,MPEGTS:%d\n\n" % (ts(b0), (mpegts0 + 90 * b0) % (1 << 33))
|
|
return head + "\n".join(c for s, e, c in cues if s < b1 and e > b0)
|
|
|
|
|
|
# ---------------------------------------------------------------- an Opus floor (§7.9)
|
|
|
|
def opus_samples(p):
|
|
"""The samples at 48 kHz an Opus packet lasts (RFC 6716 §3.1), the packet held to §3.4's
|
|
R1 to R7; None for a packet that breaks one."""
|
|
if not p:
|
|
return None
|
|
toc = p[0]
|
|
cfg = toc >> 3
|
|
frame = [480, 960, 1920, 2880][cfg % 4] if cfg < 12 else ([480, 960][cfg % 2] if cfg < 16 else [120, 240, 480, 960][cfg % 4])
|
|
n = len(p)
|
|
|
|
def flen(at, end):
|
|
if at >= end:
|
|
return None
|
|
if p[at] < 252:
|
|
return p[at], 1
|
|
if at + 1 >= end:
|
|
return None
|
|
return p[at + 1] * 4 + p[at], 2
|
|
code = toc & 3
|
|
if code == 0:
|
|
return frame if n - 1 <= 1275 else None
|
|
if code == 1:
|
|
return 2 * frame if (n - 1) % 2 == 0 and (n - 1) // 2 <= 1275 else None
|
|
if code == 2:
|
|
r = flen(1, n)
|
|
if r is None:
|
|
return None
|
|
rest = n - 1 - r[1]
|
|
return 2 * frame if r[0] <= rest and rest - r[0] <= 1275 else None
|
|
if n < 2:
|
|
return None
|
|
vbr, padded, m = p[1] & 0x80, p[1] & 0x40, p[1] & 0x3f
|
|
if m == 0 or m * frame > 5760:
|
|
return None
|
|
at, pad = 2, 0
|
|
if padded:
|
|
while True:
|
|
if at >= n:
|
|
return None
|
|
v = p[at]
|
|
at += 1
|
|
if v == 255:
|
|
pad += 254
|
|
else:
|
|
pad += v
|
|
break
|
|
end = n - pad
|
|
if end < at:
|
|
return None
|
|
if vbr:
|
|
total = 0
|
|
for _ in range(m - 1):
|
|
r = flen(at, end)
|
|
if r is None or r[0] > 1275:
|
|
return None
|
|
at += r[1]
|
|
total += r[0]
|
|
if at > end or total > end - at or end - at - total > 1275:
|
|
return None
|
|
elif (end - at) % m or (end - at) // m > 1275:
|
|
return None
|
|
return m * frame
|
|
|
|
|
|
def ogg_crc(b):
|
|
c = 0
|
|
for x in b:
|
|
c ^= x << 24
|
|
for _ in range(8):
|
|
c = ((c << 1) ^ 0x04C11DB7) & 0xFFFFFFFF if c & 0x80000000 else (c << 1) & 0xFFFFFFFF
|
|
return c
|
|
|
|
|
|
def ogg_page(flags, granule, seq, packet):
|
|
lacing = bytes([255] * (len(packet) // 255) + [len(packet) % 255])
|
|
head = b"OggS" + bytes([0, flags]) + struct.pack("<QII", granule, 0, seq) + bytes(4) + bytes([len(lacing)]) + lacing
|
|
page = bytearray(head + packet)
|
|
page[22:26] = struct.pack("<I", ogg_crc(page))
|
|
return bytes(page)
|
|
|
|
|
|
def ogg_opus(packets, pre_skip, samples, channels, rate, gain):
|
|
"""The Ogg Opus stream (RFC 7845) §7.9 writes of `packets` ((bytes, samples) each)."""
|
|
out = ogg_page(2, 0, 0, b"OpusHead" + struct.pack("<BBHIhB", 1, channels, pre_skip, rate, gain, 0))
|
|
out += ogg_page(0, 0, 1, b"OpusTags" + struct.pack("<I", 8) + b"wai-pack" + struct.pack("<I", 0))
|
|
g = 0
|
|
for i, (b, n) in enumerate(packets):
|
|
g += n
|
|
last = i + 1 == len(packets)
|
|
out += ogg_page(4 if last else 0, pre_skip + samples if last else g, 2 + i, b)
|
|
return out
|
|
|
|
|
|
def boxes_of(b, a, z):
|
|
out = []
|
|
while a < z:
|
|
size, kind = struct.unpack(">I4s", b[a:a + 8])
|
|
h = 8
|
|
if size == 1:
|
|
size, h = struct.unpack(">Q", b[a + 8:a + 16])[0], 16
|
|
if size < h or a + size > z:
|
|
raise ValueError("a box past its parent")
|
|
out.append((kind, a + h, a + size))
|
|
a += size
|
|
return out
|
|
|
|
|
|
def one(bs, kind):
|
|
v = [x for x in bs if x[0] == kind]
|
|
if len(v) != 1:
|
|
raise ValueError("not one %s" % kind)
|
|
return v[0]
|
|
|
|
|
|
def opus_init(b):
|
|
"""A supplied Opus init.mp4 (§7.9): (track, trex defaults, channels, pre-skip, input
|
|
rate, gain), or ValueError."""
|
|
moov = one(boxes_of(b, 0, len(b)), b"moov")
|
|
mb = boxes_of(b, moov[1], moov[2])
|
|
trak, mvex = one(mb, b"trak"), one(mb, b"mvex")
|
|
tb = boxes_of(b, trak[1], trak[2])
|
|
tkhd = one(tb, b"tkhd")
|
|
track = int.from_bytes(b[tkhd[1] + (20 if b[tkhd[1]] == 1 else 12):][:4], "big")
|
|
mdia = one(tb, b"mdia")
|
|
md = boxes_of(b, mdia[1], mdia[2])
|
|
mdhd = one(md, b"mdhd")
|
|
ts = int.from_bytes(b[mdhd[1] + (20 if b[mdhd[1]] == 1 else 12):][:4], "big")
|
|
if ts != 48000:
|
|
raise ValueError("a timescale other than 48000")
|
|
hdlr = one(md, b"hdlr")
|
|
if b[hdlr[1] + 8:hdlr[1] + 12] != b"soun":
|
|
raise ValueError("not a sound track")
|
|
minf = one(md, b"minf")
|
|
stbl = one(boxes_of(b, minf[1], minf[2]), b"stbl")
|
|
stsd = one(boxes_of(b, stbl[1], stbl[2]), b"stsd")
|
|
ents = boxes_of(b, stsd[1] + 8, stsd[2])
|
|
if int.from_bytes(b[stsd[1] + 4:stsd[1] + 8], "big") != 1 or len(ents) != 1 or ents[0][0] != b"Opus":
|
|
raise ValueError("not one Opus sample entry")
|
|
e = ents[0]
|
|
entry_ch = int.from_bytes(b[e[1] + 16:e[1] + 18], "big")
|
|
d = one(boxes_of(b, e[1] + 28, e[2]), b"dOps")
|
|
o = b[d[1]:d[2]]
|
|
if len(o) != 11 or o[0] != 0 or o[10] != 0:
|
|
raise ValueError("not a dOps of version 0 and mapping family 0")
|
|
ch, skip, rate, gain = o[1], int.from_bytes(o[2:4], "big"), int.from_bytes(o[4:8], "big"), int.from_bytes(o[8:10], "big", signed=True)
|
|
if ch not in (1, 2) or ch != entry_ch:
|
|
raise ValueError("channels")
|
|
for x in tb:
|
|
if x[0] == b"edts":
|
|
el = one(boxes_of(b, x[1], x[2]), b"elst")
|
|
v = b[el[1]]
|
|
cnt = int.from_bytes(b[el[1] + 4:el[1] + 8], "big")
|
|
if v == 1:
|
|
mt, r = int.from_bytes(b[el[1] + 16:el[1] + 24], "big", signed=True), int.from_bytes(b[el[1] + 24:el[1] + 28], "big")
|
|
else:
|
|
mt, r = int.from_bytes(b[el[1] + 12:el[1] + 16], "big", signed=True), int.from_bytes(b[el[1] + 16:el[1] + 20], "big")
|
|
if cnt != 1 or mt not in (0, skip) or r != 0x10000:
|
|
raise ValueError("an edit list other than one entry at media time 0 or the pre-skip")
|
|
trex = one(boxes_of(b, mvex[1], mvex[2]), b"trex")
|
|
t = b[trex[1]:trex[2]]
|
|
if int.from_bytes(t[4:8], "big") != track:
|
|
raise ValueError("trex of another track")
|
|
return track, (int.from_bytes(t[12:16], "big"), int.from_bytes(t[16:20], "big"), int.from_bytes(t[20:24], "big")), ch, skip, rate, gain
|
|
|
|
|
|
def opus_segment(b, track, dflt):
|
|
"""A media segment's samples (§7.7's boxes): [(bytes, duration, flags, dts)]."""
|
|
top = boxes_of(b, 0, len(b))
|
|
first = next(i for i, x in enumerate(top) if x[0] == b"moof")
|
|
if any(x[0] not in (b"styp", b"sidx", b"prft", b"emsg", b"free", b"skip") for x in top[:first]):
|
|
raise ValueError("a box before the first moof")
|
|
rest = top[first:]
|
|
if len(rest) % 2 or any(rest[i][0] != b"moof" or rest[i + 1][0] != b"mdat" for i in range(0, len(rest), 2)):
|
|
raise ValueError("other than moof and mdat pairs")
|
|
out, nxt = [], None
|
|
for i in range(0, len(rest), 2):
|
|
moof, mdat = rest[i], rest[i + 1]
|
|
mb = boxes_of(b, moof[1], moof[2])
|
|
traf = one(mb, b"traf")
|
|
tb = boxes_of(b, traf[1], traf[2])
|
|
tfhd = one(tb, b"tfhd")
|
|
tf = int.from_bytes(b[tfhd[1] + 1:tfhd[1] + 4], "big")
|
|
if tf & ~(0x02 | 0x08 | 0x10 | 0x20 | 0x20000) or int.from_bytes(b[tfhd[1] + 4:tfhd[1] + 8], "big") != track:
|
|
raise ValueError("tfhd")
|
|
at = tfhd[1] + 8
|
|
vals = []
|
|
for bit in (0x02, 0x08, 0x10, 0x20):
|
|
if tf & bit:
|
|
vals.append(int.from_bytes(b[at:at + 4], "big"))
|
|
at += 4
|
|
else:
|
|
vals.append(None)
|
|
d_dur = vals[1] if vals[1] is not None else dflt[0]
|
|
d_size = vals[2] if vals[2] is not None else dflt[1]
|
|
d_flags = vals[3] if vals[3] is not None else dflt[2]
|
|
tfdt = one(tb, b"tfdt")
|
|
dts = int.from_bytes(b[tfdt[1] + 4:tfdt[1] + 12], "big") if b[tfdt[1]] == 1 else int.from_bytes(b[tfdt[1] + 4:tfdt[1] + 8], "big")
|
|
if nxt is not None and nxt != dts:
|
|
raise ValueError("a moof not where the one before it ended")
|
|
for trun in [x for x in tb if x[0] == b"trun"]:
|
|
rf = int.from_bytes(b[trun[1] + 1:trun[1] + 4], "big")
|
|
n = int.from_bytes(b[trun[1] + 4:trun[1] + 8], "big")
|
|
data = moof[1] - 8 + int.from_bytes(b[trun[1] + 8:trun[1] + 12], "big", signed=True)
|
|
at = trun[1] + 12
|
|
ff = None
|
|
if rf & 0x04:
|
|
ff = int.from_bytes(b[at:at + 4], "big")
|
|
at += 4
|
|
for k in range(n):
|
|
f = {}
|
|
for bit in (0x100, 0x200, 0x400, 0x800):
|
|
if rf & bit:
|
|
f[bit] = int.from_bytes(b[at:at + 4], "big")
|
|
at += 4
|
|
dur, size = f.get(0x100, d_dur), f.get(0x200, d_size)
|
|
flags = (ff if ff is not None else f.get(0x400, d_flags)) if k == 0 else f.get(0x400, d_flags)
|
|
if f.get(0x800, 0) or size == 0 or data < mdat[1] or data + size > mdat[2]:
|
|
raise ValueError("a sample")
|
|
out.append((b[data:data + size], dur, flags, dts))
|
|
data += size
|
|
dts += dur
|
|
nxt = dts
|
|
return out
|
|
|
|
|
|
def opus_floor(pkg, P, name, starts, total, channels):
|
|
"""The Opus floor a package carries (§7.9): its init's fields and its packets
|
|
[(bytes, samples, dts, segment)], or ValueError naming what does not hold."""
|
|
lb = pkg.get("floor.listing")
|
|
if lb is None or hashlib.sha256(lb).hexdigest() != P["floor"]["supplied_listing"]:
|
|
raise ValueError("floor.listing does not hash to the policy's supplied_listing")
|
|
lines = [ln.split(" ", 1) for ln in lb.decode().splitlines()]
|
|
files = {path: h for h, path in lines}
|
|
init_name = name + "/init.mp4"
|
|
segs = sorted((p for p in files if p.startswith(name + "/") and p != init_name), key=lambda x: x.encode())
|
|
ib = pkg.get("floor/%s/init.mp4" % name)
|
|
if ib is None or hashlib.sha256(ib).hexdigest() != files.get(init_name):
|
|
raise ValueError("floor/%s/init.mp4 is not the supplied init" % name)
|
|
track, dflt, ch, skip, rate, gain = opus_init(ib)
|
|
if ch != channels:
|
|
raise ValueError("channels")
|
|
packets, durs, nxt = [], [], 0
|
|
for si, f in enumerate(segs):
|
|
b = pkg.get("floor/%s/%s.m4s" % (name, files[f]))
|
|
if b is None or hashlib.sha256(b).hexdigest() != files[f] or "/" in f[len(name) + 1:] or not f.endswith(".m4s"):
|
|
raise ValueError("%s is not in the package by its SHA-256" % f)
|
|
smp = opus_segment(b, track, dflt)
|
|
if any(x[2] & 0x10000 for x in smp) or smp[0][3] != nxt:
|
|
raise ValueError("%s: a sample that is not a sync sample, or a gap" % f)
|
|
for data, dur, _, dts in smp:
|
|
n = opus_samples(data)
|
|
if n is None:
|
|
raise ValueError("%s: a malformed packet" % f)
|
|
packets.append((data, n, dts, si))
|
|
durs.append(dur)
|
|
nxt = dts + dur
|
|
if any(d != p[1] for d, p in zip(durs[:-1], packets[:-1])) or not 0 < durs[-1] <= packets[-1][1]:
|
|
raise ValueError("a sample that does not last its packet")
|
|
if nxt < total + skip or nxt - durs[-1] >= total + skip:
|
|
raise ValueError("the packets' length is not the programme's and the pre-skip")
|
|
tail = len(segs) == len(starts) + 1
|
|
if len(segs) not in (len(starts), len(starts) + 1):
|
|
raise ValueError("not a segment per piece")
|
|
for data, n, dts, si in packets:
|
|
want = len(starts) if tail and dts >= total else max(i for i, s0 in enumerate(starts) if s0 <= dts)
|
|
if si != want:
|
|
raise ValueError("a packet in another piece's segment")
|
|
return (ch, skip, rate, gain), packets
|
|
|
|
|
|
def opus_object(floor, s, e):
|
|
"""The floor object §7.9 writes for the piece [s, e)."""
|
|
(ch, skip, rate, gain), packets = floor
|
|
a = max(0, s + skip - 3840)
|
|
first = max(i for i, p in enumerate(packets) if p[2] <= a)
|
|
last = max(i for i, p in enumerate(packets) if p[2] <= e + skip - 1)
|
|
return ogg_opus([(p[0], p[1]) for p in packets[first:last + 1]], s + skip - packets[first][2], e - s, ch, rate, gain)
|
|
|
|
|
|
# ---------------------------------------------------------------- one golden
|
|
|
|
def check_golden(gdir, pkg, fail):
|
|
P = tomllib.load(open(os.path.join(gdir, "policy.toml"), "rb"))
|
|
receipt = json.loads(pkg["package.receipt.json"])
|
|
lead_id = P["lead"]
|
|
comps = {c["id"]: c for c in P["component"]}
|
|
lead_tb = (comps[lead_id]["timebase"]["num"], comps[lead_id]["timebase"]["den"])
|
|
lead_ix = read_index(pkg["index-%s.wsix" % lead_id])
|
|
lead_starts = [int(x) for x in pkg["plan-%s.txt" % lead_id].decode().split()]
|
|
lead_end = lead_ix["pieces"][-1]["start"] + lead_ix["pieces"][-1]["dur"]
|
|
if [p["start"] for p in lead_ix["pieces"]] != lead_starts:
|
|
fail("the lead's plan is not its index's")
|
|
rungs = {}
|
|
for r in P.get("ladder", []):
|
|
rungs.setdefault(r["component"], []).append(r)
|
|
for cid, c in comps.items():
|
|
if cid == lead_id:
|
|
continue
|
|
tb = (c["timebase"]["num"], c["timebase"]["den"])
|
|
ix = read_index(pkg["index-%s.wsix" % cid])
|
|
sp = spans(lead_starts, lead_end, lead_tb, tb)
|
|
if [(p["start"], p["dur"]) for p in ix["pieces"]] != sp or any(p["flags"] != FLAG_ALIGNED for p in ix["pieces"]):
|
|
fail("%s: its pieces are not the lead's, aligned" % cid)
|
|
continue
|
|
if pkg.get("plan-%s.txt" % cid) != "".join("%d\n" % s for s, _ in sp).encode():
|
|
fail("%s: its plan file" % cid)
|
|
objs = []
|
|
for e in ix["entries"]:
|
|
env = pkg.get("objects/%s.wai" % e["hash"].hex())
|
|
if env is None or content_hash(env) != e["hash"] or len(env) != e["len"]:
|
|
fail("%s: an entry's object is not its content hash" % cid)
|
|
objs.append(None)
|
|
else:
|
|
objs.append(env)
|
|
if None in objs:
|
|
continue
|
|
base_cap = next(r["capability"] for r in rungs[cid] if r["role"] == "base")
|
|
src = None if c["source"].endswith(".wav") else open(os.path.join(gdir, "sources", c["source"]), "rb").read()
|
|
if base_cap == "wai.audio.score":
|
|
contract, notes = read_score(src)
|
|
mix = mixdown(contract, notes)
|
|
if base_cap == "wai.text.timed":
|
|
cues = cue_blocks(src.decode())
|
|
if base_cap == "wai.graphics.vector":
|
|
doc = WVC.load(src)
|
|
z = doc.sizes[0]
|
|
# A sound track's Opus floor (§7.9), read from the package's CMAF files through
|
|
# floor.listing, which the policy pins.
|
|
floor_cap = next((r["capability"] for r in rungs[cid] if r["role"] == "floor"), None)
|
|
opus = None
|
|
if floor_cap == "wai.audio.opus":
|
|
try:
|
|
opus = opus_floor(pkg, P, cid, [x for x, _ in sp], sp[-1][0] + sp[-1][1], 2)
|
|
except (ValueError, StopIteration, KeyError, IndexError, struct.error) as x:
|
|
fail("%s: its Opus floor: %s" % (cid, x))
|
|
continue
|
|
listing = P["floor"]["supplied_listing"]
|
|
settings = '{"source":"supplied","supplied_listing":"%s"}' % listing
|
|
settings_digest = B3.blake3(b"wai:render-settings\x01" + settings.encode()).hex()
|
|
for k, (s, n) in enumerate(sp):
|
|
p = ix["pieces"][k]
|
|
es = ix["entries"][p["first"]:p["first"] + p["n"]]
|
|
base = payload_of(objs[p["first"]])
|
|
want_payload, floor_payload = None, None
|
|
if base_cap == "wai.audio.flac":
|
|
pcm = [wav_sample(i, ch) for i in range(s, s + n) for ch in (0, 1)]
|
|
digest = sha256(s16le(pcm))
|
|
want_payload = flac(pcm, 2, tb[0])
|
|
if opus is not None:
|
|
floor_payload = opus_object(opus, s, s + n)
|
|
# Its manifest: the piece's samples, presenting the piece's base.
|
|
fm = manifest_of(objs[p["first"] + 1]) if len(es) > 1 else {}
|
|
df = fm.get("derived_from") or {}
|
|
if fm.get("target") != {"channels": 2, "rate": tb[0], "samples": n} or \
|
|
df != {"motif": es[0]["hash"].hex(), "motif_capability": "wai.audio.flac",
|
|
"renderer": "supplied@" + listing, "settings": settings_digest}:
|
|
fail("%s piece %d: its Opus floor's manifest is not §7.9's" % (cid, k))
|
|
elif base_cap == "wai.audio.score":
|
|
ch = contract["channels"]
|
|
pcm = mix[s * ch:(s + n) * ch]
|
|
digest = sha256(s16le(pcm))
|
|
floor_payload = flac(pcm, ch, tb[0])
|
|
# The piece's own score mixes down to the whole's samples of the piece.
|
|
try:
|
|
pc, pn = read_score(base)
|
|
ok = pc["duration"] == n and mixdown(pc, pn) == pcm and base == write_score(base) and \
|
|
score_contract(base) == canonical_contract(pc)
|
|
except Exception:
|
|
ok = False
|
|
if not ok:
|
|
fail("%s piece %d: its score does not mix down to the source's samples" % (cid, k))
|
|
elif base_cap == "wai.graphics.vector":
|
|
digest = sha256(b"".join(WVC.frame_digest(doc, z.w, z.h, tb[0], tb[1], g) for g in range(s, s + n)))
|
|
# The piece's own document: its window the piece's ticks, its frames the
|
|
# source's (graphics-vector §3.11, P7).
|
|
try:
|
|
pd = WVC.load(base)
|
|
ppf = WVC.ticks_per_frame(pd, tb[0], tb[1])
|
|
ok = (pd.H["t0"], pd.H["dur"]) == (s * ppf, n * ppf) and \
|
|
sha256(b"".join(WVC.frame_digest(pd, z.w, z.h, tb[0], tb[1], g) for g in range(s, s + n))) == digest
|
|
except Exception:
|
|
ok = False
|
|
if not ok:
|
|
fail("%s piece %d: its document is not the source's for the piece" % (cid, k))
|
|
elif base_cap == "wai.text.timed":
|
|
seg = segment(cues, s, s + n).encode()
|
|
digest = sha256(seg)
|
|
want_payload = seg
|
|
# Its manifest's target: the component's language and the piece's span.
|
|
if manifest_of(objs[p["first"]]).get("target") != {"lang": c["lang"], "ms": n}:
|
|
fail("%s piece %d: its target is not the piece's" % (cid, k))
|
|
elif base_cap == "wai.feed.state":
|
|
digest = bytes(32)
|
|
# The seal: its final tick the piece's last, signed by the receipt's key.
|
|
nsec = le(base, 4, 2)
|
|
secs = {}
|
|
for i in range(nsec):
|
|
kind, off, ln = base[6 + 9 * i], le(base, 7 + 9 * i, 4), le(base, 11 + 9 * i, 4)
|
|
secs[kind] = base[6 + 9 * nsec + off:6 + 9 * nsec + off + ln]
|
|
seal = secs.get(5, b"")
|
|
pre = b"wai:world-seal\x01" + B3.blake3(secs[1]) + B3.blake3(secs[2]) + B3.blake3(secs.get(4, b"")) + seal[:40]
|
|
if len(seal) != 104 or le(seal, 0, 8) != s + n - 1 or not ED.verifies(bytes.fromhex(receipt["signer_pubkey"]), pre, seal[40:]):
|
|
fail("%s piece %d: its seal" % (cid, k))
|
|
else:
|
|
fail("%s: a capability this reference does not know: %s" % (cid, base_cap))
|
|
continue
|
|
if es[0]["digest"] != digest:
|
|
fail("%s piece %d: its prefix_digest is not the source's" % (cid, k))
|
|
if want_payload is not None and base != want_payload:
|
|
fail("%s piece %d: its payload is not §5.8's of the source" % (cid, k))
|
|
if floor_payload is not None and (len(es) < 2 or payload_of(objs[p["first"] + 1]) != floor_payload):
|
|
fail("%s piece %d: its floor is not %s" % (cid, k, "§7.9's Ogg Opus of the floor's packets" if opus else "the FLAC of its mixdown"))
|
|
|
|
|
|
def opus_faults(pkg, P):
|
|
"""Copies of `pkg` with one fault each in its Opus floor."""
|
|
out = []
|
|
for c in P["component"]:
|
|
floors = [r for r in P.get("ladder", []) if r["component"] == c["id"] and r["role"] == "floor"]
|
|
if not floors or floors[0]["capability"] != "wai.audio.opus":
|
|
continue
|
|
ixb = bytearray(pkg["index-%s.wsix" % c["id"]])
|
|
ix = read_index(bytes(ixb))
|
|
e1 = ix["entries"][1]
|
|
env = bytearray(pkg["objects/%s.wai" % e1["hash"].hex()])
|
|
env[len(env) - 40] ^= 0x01
|
|
h = content_hash(bytes(env))
|
|
at = 160 + 32 * len(ix["pieces"]) + 128
|
|
ixb[at:at + 32] = h
|
|
m = dict(pkg)
|
|
m["objects/%s.wai" % h.hex()] = bytes(env)
|
|
m["index-%s.wsix" % c["id"]] = bytes(ixb)
|
|
out.append(("a flipped byte in component %s's first Opus floor object" % c["id"], m))
|
|
seg = sorted(k for k in pkg if k.startswith("floor/%s/" % c["id"]) and k.endswith(".m4s"))[0]
|
|
m = dict(pkg)
|
|
b = bytearray(m[seg])
|
|
b[-10] ^= 0x01
|
|
m[seg] = bytes(b)
|
|
out.append(("a flipped byte in component %s's media segment %s" % (c["id"], seg), m))
|
|
m = dict(pkg)
|
|
lb = m["floor.listing"].decode().splitlines()
|
|
m["floor.listing"] = ("\n".join(lb[:-1]) + "\n").encode()
|
|
out.append(("component %s's floor.listing without its last line" % c["id"], m))
|
|
return out
|
|
|
|
|
|
def read_tree(d):
|
|
out = {}
|
|
for root, _, files in os.walk(d):
|
|
for f in files:
|
|
p = os.path.join(root, f)
|
|
out[os.path.relpath(p, d).replace(os.sep, "/")] = open(p, "rb").read()
|
|
return out
|
|
|
|
|
|
def main(argv):
|
|
if len(argv) != 2:
|
|
print(__doc__)
|
|
return 2
|
|
corpus = argv[1]
|
|
bad = 0
|
|
for name in GOLDENS:
|
|
gdir = os.path.join(corpus, "multi", name)
|
|
pkg = read_tree(os.path.join(gdir, "package"))
|
|
errs = []
|
|
check_golden(gdir, pkg, errs.append)
|
|
for e in errs:
|
|
print("FAIL %s: %s" % (name, e))
|
|
bad += len(errs)
|
|
# Planted faults: in each component beside the lead, the first base's object with
|
|
# one payload byte flipped and its entry renamed to it, which every check above
|
|
# that reads the object must find.
|
|
P = tomllib.load(open(os.path.join(gdir, "policy.toml"), "rb"))
|
|
for c in P["component"]:
|
|
if c["id"] == P["lead"]:
|
|
continue
|
|
ixb = bytearray(pkg["index-%s.wsix" % c["id"]])
|
|
ix = read_index(bytes(ixb))
|
|
e0 = ix["entries"][0]
|
|
env = bytearray(pkg["objects/%s.wai" % e0["hash"].hex()])
|
|
env[len(env) - len(payload_of(bytes(env))) // 2 - 1] ^= 0x20
|
|
h = content_hash(bytes(env))
|
|
at = 160 + 32 * len(ix["pieces"])
|
|
ixb[at:at + 32] = h
|
|
m = dict(pkg)
|
|
m["objects/%s.wai" % h.hex()] = bytes(env)
|
|
m["index-%s.wsix" % c["id"]] = bytes(ixb)
|
|
found = []
|
|
check_golden(gdir, m, found.append)
|
|
if not found:
|
|
print("FAIL %s: a flipped payload byte in component %s is not found" % (name, c["id"]))
|
|
bad += 1
|
|
else:
|
|
print("ok %s: a flipped payload byte in component %s: %s" % (name, c["id"], found[0]))
|
|
# An Opus floor's own faults (§7.9): its first floor object with a payload byte
|
|
# flipped, and a media segment with a byte flipped under its old name.
|
|
if os.path.isdir(os.path.join(gdir, "floor")):
|
|
for what, edit in opus_faults(pkg, P):
|
|
found = []
|
|
try:
|
|
check_golden(gdir, edit, found.append)
|
|
except Exception as x: # a refusal the reader raises is a fault found
|
|
found.append(repr(x))
|
|
if not found:
|
|
print("FAIL %s: %s is not found" % (name, what))
|
|
bad += 1
|
|
else:
|
|
print("ok %s: %s: %s" % (name, what, found[0]))
|
|
if not errs:
|
|
print("ok %s: every component's digests and objects recomputed from its sources" % name)
|
|
return 1 if bad else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv))
|