Some checks failed
WAI operation catalogue / does this change touch it (push) Successful in 1m54s
wai-key-proxy / does this change touch it (push) Successful in 1m55s
wai-audit collector and player integration / does this change touch it (push) Successful in 1m57s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 1m58s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 1m59s
wai-fleet service / does this change touch it (push) Successful in 1m58s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 1m59s
wai-app and Create / wai-app · does this change touch it (push) Successful in 1m59s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 55s
wai-ledger service / does this change touch it (push) Successful in 1m7s
wai-sink-android native sink library / does this change touch it (push) Successful in 1m7s
wai-quantum for JavaScript / does this change touch it (push) Successful in 1m9s
wai-quantum studio and desktop / does this change touch it (push) Successful in 1m8s
WAI Studio / does this change touch it (push) Successful in 1m7s
wai-sink-swift native sink package / does this change touch it (push) Successful in 1m8s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 1m28s
wai-fleet service / service · macos (push) Successful in 42s
wai-ledger service / service · macos (push) Successful in 1m1s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 4m24s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 6m22s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (push) Has been cancelled
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Has been cancelled
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Has been cancelled
wai-audit collector and player integration / collector · linux (push) Has been cancelled
WAI operation catalogue / the catalogue and every door · linux (push) Has been cancelled
wai-fleet service / service · linux (push) Has been cancelled
wai-key-proxy / proxy · linux (push) Has been cancelled
wai-ledger service / service · linux (push) Has been cancelled
wai-quantum for JavaScript / javascript · linux (push) Has been cancelled
wai-quantum energy-landscape jobs / does this change touch it (push) Has been cancelled
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Has been cancelled
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Has been cancelled
wai-quantum energy-landscape jobs / the same bits · macos (push) Has been cancelled
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Has been cancelled
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Has been cancelled
wai-quantum studio and desktop / studio · browser (push) Has been cancelled
wai-quantum studio and desktop / desktop app · linux (push) Has been cancelled
wai-quantum studio and desktop / desktop app · windows (push) Has been cancelled
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Has been cancelled
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Has been cancelled
WAI Studio / Studio in WebKit · linux (push) Has been cancelled
WAI Studio / Studio in Chromium and Firefox · macos (push) Has been cancelled
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Has been cancelled
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Has been cancelled
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 14s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 2m11s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 5m12s
A channel's operator needs to know, per input, whether the picture went black or froze, the sound went silent, captions or cues stopped, or the transport stream broke; and an input switch needs that answer from a monitor it can hold to account. extensions/signal-health.md registers `wai.live.signal_health`: a monitor's signed record (WSH1) of one input over a window with up to six checks: black and freeze over the decoded luma, silence in 10 ms blocks, caption and cue gaps, and the first-priority indicators of ETSI TR 101 290 V1.4.1 (sync lost with its five-and-two hysteresis, bad sync bytes, PAT_error_2, continuity, PMT_error_2, PID_error). Each check states its thresholds, its integer values, the digest of the media it read and its verdict. A verifier holds each verdict to its values and thresholds and each threshold to the profile it expects with no media, and recomputes every value with it; an input switch carries the first input whose latest record by a trusted monitor is fresh and states no fault. The reference is the `signal_health` module of wai-rs; the corpus is live-conformance/signal-health/ (41 records, 26 checks against a profile and their media, 10 switches), its media written from fixed seeds with each case's values declared and held by the generator, and signal_health_verify.py is its second implementation: 77 cases agree, 14 of 14 mutated copies fail and 30 of 30 weakened verifiers are caught. Twelve faults planted in the reference (the verdict rule, the freeze's held frame, the duplicate allowance, the discontinuity indicator, the profile, trust, silence over one channel, the PAT gap, the end of the window, the sync hysteresis, the media digest, freshness) each fail its tests. Co-Authored-By: Claude Opus 5.5 <[email protected]>
693 lines
26 KiB
Python
693 lines
26 KiB
Python
#!/usr/bin/env python3
|
|
"""Independent verifier of the signal-health corpus (extensions/signal-health.md).
|
|
|
|
Written from the extension's text. It shares no code with wai-rs, which generates the corpus, and
|
|
never runs it. It takes Ed25519 verification as SPEC §7 *Signatures* states it from the routine
|
|
every Python verifier of WAI's corpora shares (`../claim-conformance/_ed25519.py`). Everything
|
|
this extension defines is written here: the record's layout and rules (§4, §6.1), its verdicts
|
|
(§6.2), the profile (§6.3), each check's values recomputed from its media (§5, §6.4), the
|
|
transport-stream indicators (§5.6) and the input switch (§7).
|
|
|
|
For every case it requires the outcome the corpus states, field for field; that every reason of
|
|
§8's table appears in the corpus; then it mutates copies of the corpus, one defect each, and
|
|
requires every copy to fail; and it drops each of its own checks in turn and requires the corpus
|
|
to catch the weakened verifier.
|
|
|
|
python3 signal_health_verify.py <live-conformance/signal-health>
|
|
|
|
Exit status 0 only if every case verifies, every mutated copy fails and every weakened verifier is
|
|
caught. Python 3.8 or later and the standard library.
|
|
"""
|
|
|
|
import copy
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance"))
|
|
import _ed25519 as ED # noqa: E402
|
|
|
|
EXTENSION = os.path.join(HERE, "..", "extensions", "signal-health.md")
|
|
MAGIC = b"WSH1"
|
|
DOMAIN = b"wai:signal-health\x01"
|
|
MAX_LEN = 1024
|
|
MAX_INT = (1 << 53) - 1
|
|
NAMES = ["black", "freeze", "silence", "captions", "cues", "transport"]
|
|
NAME_BYTES = set(b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-:")
|
|
# §4.2: each code's thresholds and values, each (name, bytes).
|
|
THRESHOLDS = {
|
|
1: [("luma_max", 1), ("ratio_ppm", 4), ("min_ms", 4)],
|
|
2: [("mad_milli", 4), ("min_ms", 4)],
|
|
3: [("peak_max", 2), ("min_ms", 4)],
|
|
4: [("max_gap_ms", 4)],
|
|
5: [("max_gap_ms", 4)],
|
|
6: [("table_max_gap_ms", 4), ("pid_max_gap_ms", 4)],
|
|
}
|
|
RUNS = [("count", 4), ("matched", 4), ("longest_ms", 8), ("longest_at_ms", 8)]
|
|
SEEN = [("seen", 4), ("last_ms", 8), ("longest_gap_ms", 8)]
|
|
INDICATORS = [("packets", 8), ("sync_loss", 4), ("sync_byte", 4), ("pat", 4), ("cc", 4), ("pmt", 4), ("pid", 4)]
|
|
VALUES = {1: RUNS, 2: RUNS, 3: RUNS, 4: SEEN, 5: SEEN, 6: INDICATORS}
|
|
|
|
# A weakened run drops one of these checks (see `WEAKENINGS`); None checks everything.
|
|
DROPPED = None
|
|
|
|
|
|
def drops(name):
|
|
return DROPPED == name
|
|
|
|
|
|
def reasons_table():
|
|
"""§8's codes, in order."""
|
|
text = open(EXTENSION, encoding="utf-8").read()
|
|
at = text.index("\n## 8. Reasons")
|
|
end = text.find("\n## ", at + 1)
|
|
rows = text[at:end if end >= 0 else len(text)].splitlines()
|
|
return [r.split("`")[1] for r in rows if r.startswith("| `")]
|
|
|
|
|
|
class Refused(Exception):
|
|
def __init__(self, reason):
|
|
super().__init__(reason)
|
|
self.reason = reason
|
|
|
|
|
|
# ------------------------------------------------------------------ §4, §6.1, §6.2: the record
|
|
|
|
def read(b):
|
|
"""The record as a dict, or Refused: the layout and rules of §6.1, the signature, §6.2."""
|
|
bad = Refused("signal_health_malformed")
|
|
if len(b) > MAX_LEN:
|
|
raise bad
|
|
at = 0
|
|
|
|
def take(n):
|
|
nonlocal at
|
|
if at + n > len(b):
|
|
raise bad
|
|
s = b[at:at + n]
|
|
at += n
|
|
return s
|
|
|
|
def num(n):
|
|
return int.from_bytes(take(n), "big")
|
|
|
|
if take(4) != MAGIC or take(1) != b"\x01":
|
|
raise bad
|
|
monitor = take(32)
|
|
n = num(1)
|
|
if not 1 <= n <= 64:
|
|
raise bad
|
|
name = take(n)
|
|
if not drops("name") and any(c not in NAME_BYTES for c in name):
|
|
raise bad
|
|
frm, to = num(8), num(8)
|
|
if (frm >= to and not drops("window")) or to > MAX_INT:
|
|
raise bad
|
|
window = to - frm
|
|
m = num(1)
|
|
if not 1 <= m <= 6:
|
|
raise bad
|
|
checks = []
|
|
for _ in range(m):
|
|
code = num(1)
|
|
if not 1 <= code <= 6:
|
|
raise bad
|
|
if checks and checks[-1]["code"] >= code and not drops("order"):
|
|
raise bad
|
|
c = {"code": code, "media": take(32), "t": {}, "v": {}}
|
|
for k, w in THRESHOLDS[code]:
|
|
c["t"][k] = num(w)
|
|
for k, w in VALUES[code]:
|
|
c["v"][k] = num(w)
|
|
c["fault"] = num(1)
|
|
t, v = c["t"], c["v"]
|
|
if not drops("range"):
|
|
if code == 1 and not (1 <= t["ratio_ppm"] <= 1_000_000):
|
|
raise bad
|
|
if code == 3 and t["peak_max"] > 32_768:
|
|
raise bad
|
|
if any(t.get(k, 1) < 1 for k in ("min_ms", "table_max_gap_ms", "pid_max_gap_ms")):
|
|
raise bad
|
|
if not drops("values"):
|
|
inside = lambda x: frm <= x < to # noqa: E731
|
|
if code <= 3:
|
|
none = v["matched"] == 0 and v["longest_ms"] == 0 and v["longest_at_ms"] == 0
|
|
some = v["matched"] != 0 and v["longest_ms"] != 0 and inside(v["longest_at_ms"])
|
|
if v["matched"] > v["count"] or v["longest_ms"] > window or not (none or some):
|
|
raise bad
|
|
elif code <= 5:
|
|
none = v["seen"] == 0 and v["last_ms"] == 0 and v["longest_gap_ms"] == window
|
|
some = v["seen"] != 0 and inside(v["last_ms"])
|
|
if v["longest_gap_ms"] > window or not (none or some):
|
|
raise bad
|
|
if c["fault"] not in (0, 1):
|
|
raise bad
|
|
checks.append(c)
|
|
signed = b[:at]
|
|
sig = take(64)
|
|
if at != len(b):
|
|
raise bad
|
|
if not drops("signature") and ED.verify(monitor, DOMAIN + signed, sig) is not None:
|
|
raise Refused("signal_health_signature_invalid")
|
|
if not drops("verdict") and any(fails(c) != bool(c["fault"]) for c in checks):
|
|
raise Refused("signal_health_verdict_inconsistent")
|
|
return {"monitor": monitor, "input": name.decode("ascii"), "from": frm, "to": to, "checks": checks,
|
|
"digest": hashlib.sha256(signed).digest()}
|
|
|
|
|
|
def fails(c):
|
|
"""§6.2: the verdict a check's values give under its thresholds."""
|
|
t, v = c["t"], c["v"]
|
|
if c["code"] <= 3:
|
|
return v["longest_ms"] >= t["min_ms"]
|
|
if c["code"] <= 5:
|
|
return t["max_gap_ms"] != 0 and v["longest_gap_ms"] > t["max_gap_ms"]
|
|
return any(v[k] > 0 for k in ("sync_loss", "sync_byte", "pat", "cc", "pmt", "pid"))
|
|
|
|
|
|
def faults(rec):
|
|
return [NAMES[c["code"] - 1] for c in rec["checks"] if c["fault"]]
|
|
|
|
|
|
def conforms(rec, profile):
|
|
"""§6.3: exactly the profile's checks, each with its thresholds."""
|
|
if drops("profile"):
|
|
return True
|
|
want = {NAMES.index(k) + 1: t for k, t in profile.items()}
|
|
return len(rec["checks"]) == len(want) and all(want.get(c["code"]) == c["t"] for c in rec["checks"])
|
|
|
|
|
|
# ------------------------------------------------------------------ §5: the media
|
|
|
|
def sha(*parts):
|
|
return hashlib.sha256(b"".join(parts)).digest()
|
|
|
|
|
|
def be(x, n):
|
|
return x.to_bytes(n, "big")
|
|
|
|
|
|
def longest(runs):
|
|
"""The longest of runs (start, duration), the earliest of equals; (0, 0) for none."""
|
|
best = (0, 0)
|
|
for r in runs:
|
|
if r[1] > best[1]:
|
|
best = r
|
|
return best
|
|
|
|
|
|
def runs_of(flags):
|
|
"""The runs of True in flags: (first index, last index)."""
|
|
out, i = [], 0
|
|
while i < len(flags):
|
|
if flags[i]:
|
|
j = i
|
|
while j + 1 < len(flags) and flags[j + 1]:
|
|
j += 1
|
|
out.append((i, j))
|
|
i = j + 1
|
|
else:
|
|
i += 1
|
|
return out
|
|
|
|
|
|
def frames_values(frames, code, t, frm, to):
|
|
ok = all(f["width"] >= 1 and f["height"] >= 1 and len(f["luma"]) == f["width"] * f["height"]
|
|
and (drops("media_window") or frm <= f["at"] < to)
|
|
and (i == 0 or frames[i - 1]["at"] < f["at"]) for i, f in enumerate(frames))
|
|
if not ok:
|
|
raise Refused("signal_health_media_malformed")
|
|
digest = sha(b"wai:signal-health:video\x01",
|
|
*[be(f["at"], 8) + be(f["width"], 2) + be(f["height"], 2) + f["luma"] for f in frames])
|
|
|
|
def end(i):
|
|
return frames[i + 1]["at"] if i + 1 < len(frames) else to
|
|
|
|
flags = []
|
|
for i, f in enumerate(frames):
|
|
n = len(f["luma"])
|
|
if code == 1:
|
|
if drops("black_ratio"):
|
|
flags.append(all(y <= t["luma_max"] for y in f["luma"]))
|
|
else:
|
|
dark = sum(1 for y in f["luma"] if y <= t["luma_max"])
|
|
flags.append(1_000_000 * dark >= t["ratio_ppm"] * n)
|
|
else:
|
|
if i == 0:
|
|
flags.append(False)
|
|
continue
|
|
p = frames[i - 1]
|
|
same = (p["width"], p["height"]) == (f["width"], f["height"]) or drops("freeze_size")
|
|
sad = sum(abs(a - b) for a, b in zip(f["luma"], p["luma"]))
|
|
flags.append(same and 1000 * sad <= t["mad_milli"] * n)
|
|
runs = []
|
|
for a, z in runs_of(flags):
|
|
start = frames[a - 1]["at"] if code == 2 and not drops("freeze_hold") else frames[a]["at"]
|
|
runs.append((start, end(z) - start))
|
|
at, ms = longest(runs)
|
|
return digest, {"count": len(frames), "matched": sum(flags), "longest_ms": ms, "longest_at_ms": at}
|
|
|
|
|
|
def audio_values(a, t, frm, to):
|
|
ch, rate, s = a["channels"], a["rate"], a["samples"]
|
|
if not (1 <= ch <= 32 and rate % 100 == 0 and 100 <= rate <= 384_000 and len(s) % (2 * ch) == 0):
|
|
raise Refused("signal_health_media_malformed")
|
|
vals = [int.from_bytes(s[i:i + 2], "little", signed=True) for i in range(0, len(s), 2)]
|
|
per = rate // 100 * ch
|
|
blocks = len(vals) // per
|
|
if not drops("media_window") and not (a["at"] >= frm and a["at"] + 10 * blocks <= to):
|
|
raise Refused("signal_health_media_malformed")
|
|
digest = sha(b"wai:signal-health:audio\x01", be(a["at"], 8), be(rate, 4), be(ch, 2), s)
|
|
flags = []
|
|
for b in range(blocks):
|
|
block = vals[b * per:(b + 1) * per]
|
|
if drops("silence_channels"):
|
|
block = block[::ch]
|
|
flags.append(max(abs(x) for x in block) <= t["peak_max"])
|
|
runs = [(a["at"] + 10 * x, 10 * (z - x + 1)) for x, z in runs_of(flags)]
|
|
at, ms = longest(runs)
|
|
return digest, {"count": blocks, "matched": sum(flags), "longest_ms": ms, "longest_at_ms": at}
|
|
|
|
|
|
def instants_values(code, inst, frm, to):
|
|
if not all((drops("media_window") or frm <= x < to) for x in inst) or any(a > b for a, b in zip(inst, inst[1:])):
|
|
raise Refused("signal_health_media_malformed")
|
|
domain = b"wai:signal-health:cues\x01" if code == 5 else b"wai:signal-health:captions\x01"
|
|
digest = sha(domain, be(len(inst), 4), *[be(x, 8) for x in inst])
|
|
edges = list(inst) if drops("gap_edges") and inst else [frm] + list(inst) + [to]
|
|
gaps = [b - a for a, b in zip(edges, edges[1:])]
|
|
return digest, {"seen": len(inst), "last_ms": inst[-1] if inst else 0, "longest_gap_ms": max(gaps) if gaps else 0}
|
|
|
|
|
|
# ------------------------------------------------------------------ §5.5, §5.6: the transport stream
|
|
|
|
def transport_values(ts, t, frm, to):
|
|
b, first, rate = ts["bytes"], ts["first_us"], ts["rate_bps"]
|
|
n = len(b) // 188
|
|
arrive = lambda k: first + k * 188 * 8 * 1_000_000 // rate # noqa: E731
|
|
if not (len(b) % 188 == 0 and n >= 1 and rate >= 1
|
|
and (drops("media_window") or (first >= frm * 1000 and arrive(n - 1) < to * 1000))):
|
|
raise Refused("signal_health_media_malformed")
|
|
digest = sha(b"wai:signal-health:transport\x01", be(first, 8), be(rate, 8), b)
|
|
tg, pg = t["table_max_gap_ms"] * 1000, t["pid_max_gap_ms"] * 1000
|
|
v = {"packets": n, "sync_loss": 0, "sync_byte": 0, "pat": 0, "cc": 0, "pmt": 0, "pid": 0}
|
|
st = {"cc": {}, "dups": {}, "pat": frm * 1000, "pmts": {}, "ref": {}}
|
|
|
|
def late(anchor, a, gap):
|
|
return a - anchor > gap
|
|
|
|
def rerefer(a):
|
|
union = set()
|
|
for _, es in st["pmts"].values():
|
|
union |= es
|
|
for q in [q for q in st["ref"] if q not in union]:
|
|
anchor = st["ref"].pop(q)
|
|
if late(anchor, a, pg):
|
|
v["pid"] += 1
|
|
for q in union:
|
|
st["ref"].setdefault(q, a)
|
|
|
|
def section(p, start):
|
|
s = start + 1 + p[start]
|
|
return s if s < 188 else None
|
|
|
|
def whole(p, s, least):
|
|
if s + 3 > 188:
|
|
return None
|
|
ln = ((p[s + 1] & 0x0F) << 8) | p[s + 2]
|
|
e = s + 3 + ln
|
|
return e if ln >= least and e <= 188 else None
|
|
|
|
def analyse(p, a):
|
|
pid = ((p[1] & 0x1F) << 8) | p[2]
|
|
if pid == 0x1FFF:
|
|
return
|
|
scr, afc, cc, pusi = p[3] >> 6, (p[3] >> 4) & 3, p[3] & 0x0F, p[1] & 0x40
|
|
if afc == 1:
|
|
start, disc = 4, False
|
|
elif afc in (2, 3):
|
|
start, disc = 5 + p[4], p[4] > 0 and p[5] & 0x80 != 0
|
|
else:
|
|
start, disc = 188, False
|
|
payload = afc in (1, 3) and start < 188
|
|
if payload:
|
|
if pid in st["cc"]:
|
|
last = st["cc"][pid]
|
|
if disc and not drops("discontinuity"):
|
|
st["dups"][pid] = 0
|
|
elif cc == last:
|
|
st["dups"][pid] = st["dups"].get(pid, 0) + 1
|
|
if st["dups"][pid] >= 2 or drops("duplicate"):
|
|
v["cc"] += 1
|
|
else:
|
|
if cc != (last + 1) & 0x0F:
|
|
v["cc"] += 1
|
|
st["dups"][pid] = 0
|
|
st["cc"][pid] = cc
|
|
if pid in st["ref"]:
|
|
if late(st["ref"][pid], a, pg) and not drops("pid_gap"):
|
|
v["pid"] += 1
|
|
st["ref"][pid] = a
|
|
if pid == 0:
|
|
if scr and not drops("scrambling"):
|
|
v["pat"] += 1
|
|
s = section(p, start) if pusi and payload else None
|
|
if s is not None:
|
|
if p[s] == 0x00:
|
|
if late(st["pat"], a, tg) and not drops("table_gap"):
|
|
v["pat"] += 1
|
|
st["pat"] = a
|
|
e = whole(p, s, 9)
|
|
if e is not None:
|
|
maps, i = set(), s + 8
|
|
while i + 4 <= e - 4:
|
|
if (p[i] << 8) | p[i + 1]:
|
|
maps.add(((p[i + 2] & 0x1F) << 8) | p[i + 3])
|
|
i += 4
|
|
for m in [m for m in st["pmts"] if m not in maps]:
|
|
anchor, _ = st["pmts"].pop(m)
|
|
if late(anchor, a, tg):
|
|
v["pmt"] += 1
|
|
for m in maps:
|
|
st["pmts"].setdefault(m, (a, set()))
|
|
rerefer(a)
|
|
elif p[s] != 0xFF and not drops("pat_table"):
|
|
v["pat"] += 1
|
|
if pid in st["pmts"]:
|
|
if scr and not drops("scrambling"):
|
|
v["pmt"] += 1
|
|
s = section(p, start) if pusi and payload else None
|
|
if s is not None and p[s] == 0x02:
|
|
anchor, es = st["pmts"][pid]
|
|
if late(anchor, a, tg) and not drops("table_gap"):
|
|
v["pmt"] += 1
|
|
st["pmts"][pid] = (a, es)
|
|
e = whole(p, s, 13)
|
|
if e is not None:
|
|
pil = ((p[s + 10] & 0x0F) << 8) | p[s + 11]
|
|
new, i = set(), s + 12 + pil
|
|
while i + 5 <= e - 4:
|
|
new.add(((p[i + 1] & 0x1F) << 8) | p[i + 2])
|
|
i += 5 + (((p[i + 3] & 0x0F) << 8) | p[i + 4])
|
|
st["pmts"][pid] = (a, new if not drops("es_removed") else es | new)
|
|
rerefer(a)
|
|
|
|
hunting, run, bad, locked = True, 0, 0, False
|
|
for k in range(n):
|
|
p = b[k * 188:(k + 1) * 188]
|
|
good = p[0] == 0x47
|
|
if hunting:
|
|
if not good:
|
|
run = 0
|
|
continue
|
|
run += 1
|
|
if run == 5 or drops("hysteresis"):
|
|
hunting, locked, bad = False, True, 0
|
|
for j in range(k - run + 1, k + 1):
|
|
analyse(b[j * 188:(j + 1) * 188], arrive(j))
|
|
elif good:
|
|
bad = 0
|
|
analyse(p, arrive(k))
|
|
else:
|
|
v["sync_byte"] += 1
|
|
bad += 1
|
|
if bad == 2 or drops("loss_on_two"):
|
|
v["sync_loss"] += 1
|
|
hunting, run = True, 0
|
|
if n and not locked:
|
|
v["sync_loss"] += 1
|
|
if not drops("window_end"):
|
|
e = to * 1000
|
|
v["pat"] += late(st["pat"], e, tg)
|
|
v["pmt"] += sum(1 for anchor, _ in st["pmts"].values() if late(anchor, e, tg))
|
|
v["pid"] += sum(1 for anchor in st["ref"].values() if late(anchor, e, pg))
|
|
return digest, v
|
|
|
|
|
|
def measure(c, media, frm, to):
|
|
"""§5: (digest, values) of the media for check c, None when not held, or Refused."""
|
|
code, t = c["code"], c["t"]
|
|
if code in (1, 2):
|
|
return None if "frames" not in media else frames_values(media["frames"], code, t, frm, to)
|
|
if code == 3:
|
|
return None if "audio" not in media else audio_values(media["audio"], t, frm, to)
|
|
if code in (4, 5):
|
|
k = "captions" if code == 4 else "cues"
|
|
return None if k not in media else instants_values(code, media[k], frm, to)
|
|
return None if "transport" not in media else transport_values(media["transport"], t, frm, to)
|
|
|
|
|
|
def recompute(rec, media):
|
|
"""§6.4: each check's label."""
|
|
labels = []
|
|
for c in rec["checks"]:
|
|
try:
|
|
got = measure(c, media, rec["from"], rec["to"])
|
|
except Refused as r:
|
|
labels.append("reject reason=" + r.reason)
|
|
continue
|
|
if got is None:
|
|
labels.append("not_held")
|
|
elif got[0] != c["media"] and not drops("digest"):
|
|
labels.append("reject reason=signal_health_media_mismatch")
|
|
elif got[1] != c["v"]:
|
|
labels.append("reject reason=signal_health_not_recomputed")
|
|
else:
|
|
labels.append("recomputed")
|
|
return labels
|
|
|
|
|
|
# ------------------------------------------------------------------ §7: the switch
|
|
|
|
def switch(cfg, profile, records, at, current):
|
|
states, read_ = [], {}
|
|
for k, b in enumerate(records):
|
|
try:
|
|
rec = read(b)
|
|
if not conforms(rec, profile):
|
|
raise Refused("signal_health_profile_mismatch")
|
|
except Refused as r:
|
|
states.append("reject reason=" + r.reason)
|
|
continue
|
|
if rec["monitor"].hex() not in cfg["trusted"] and not drops("trusted"):
|
|
states.append("untrusted")
|
|
elif rec["to"] > at and not drops("not_yet"):
|
|
states.append("not_yet")
|
|
else:
|
|
read_[k] = rec
|
|
states.append("stands")
|
|
top = {}
|
|
for rec in read_.values():
|
|
top[rec["input"]] = max(top.get(rec["input"], 0), rec["to"])
|
|
for k, rec in read_.items():
|
|
if rec["to"] < top[rec["input"]] and not drops("supersede"):
|
|
states[k] = "superseded"
|
|
elif not drops("contradiction") and any(o["input"] == rec["input"] and o["to"] == rec["to"]
|
|
and o["digest"] != rec["digest"] for o in read_.values()):
|
|
states[k] = "contradicted"
|
|
inputs, selected = [], None
|
|
for name in cfg["inputs"]:
|
|
mine = [k for k, r in read_.items() if r["input"] == name]
|
|
standing = [k for k in mine if states[k] == "stands"]
|
|
if any(states[k] == "contradicted" for k in mine):
|
|
st = "contradicted"
|
|
elif standing:
|
|
rec = read_[standing[0]]
|
|
if at - rec["to"] > cfg["max_age_ms"] and not drops("freshness"):
|
|
st = "stale"
|
|
elif faults(rec):
|
|
st = "fault:" + "+".join(faults(rec))
|
|
else:
|
|
st = "healthy"
|
|
else:
|
|
st = "none"
|
|
if st == "healthy" and selected is None:
|
|
selected = name
|
|
inputs.append(st)
|
|
return {"records": states, "inputs": inputs, "selected": selected if selected is not None else current}
|
|
|
|
|
|
# ------------------------------------------------------------------ the corpus
|
|
|
|
def media_of(m):
|
|
out = {}
|
|
if "frames" in m:
|
|
out["frames"] = [{"at": f["at_ms"], "width": f["width"], "height": f["height"],
|
|
"luma": bytes.fromhex(f["luma"])} for f in m["frames"]]
|
|
if "audio" in m:
|
|
a = m["audio"]
|
|
out["audio"] = {"at": a["at_ms"], "rate": a["rate"], "channels": a["channels"],
|
|
"samples": bytes.fromhex(a["samples"])}
|
|
for k in ("captions", "cues"):
|
|
if k in m:
|
|
out[k] = list(m[k])
|
|
if "transport" in m:
|
|
t = m["transport"]
|
|
out["transport"] = {"first_us": t["first_us"], "rate_bps": t["rate_bps"], "bytes": bytes.fromhex(t["bytes"])}
|
|
return out
|
|
|
|
|
|
def run(doc, kind, c):
|
|
if kind == "records":
|
|
try:
|
|
rec = read(bytes.fromhex(c["bytes"]))
|
|
except Refused as r:
|
|
return {"outcome": "refuse", "reason": r.reason}
|
|
return {"outcome": "read", "digest": rec["digest"].hex(), "input": rec["input"], "faults": faults(rec)}
|
|
if kind == "checks":
|
|
try:
|
|
rec = read(bytes.fromhex(c["record"]))
|
|
if c["profile"] is not None and not conforms(rec, c["profile"]):
|
|
raise Refused("signal_health_profile_mismatch")
|
|
except Refused as r:
|
|
return {"record": "reject reason=" + r.reason}
|
|
return {"record": "stands", "checks": recompute(rec, media_of(c["media"]))}
|
|
return switch(doc["switch"], doc["profile"], [bytes.fromhex(x) for x in c["records"]], c["at_ms"], c["current"])
|
|
|
|
|
|
KINDS = ("records", "checks", "switches")
|
|
|
|
|
|
def strings(v, out):
|
|
if isinstance(v, str):
|
|
out.append(v)
|
|
elif isinstance(v, list):
|
|
for x in v:
|
|
strings(x, out)
|
|
elif isinstance(v, dict):
|
|
for x in v.values():
|
|
strings(x, out)
|
|
|
|
|
|
def check(doc, quiet=False):
|
|
"""The failures of a corpus document: each case's outcome, and every reason of §8."""
|
|
fails = []
|
|
for kind in KINDS:
|
|
for c in doc[kind]:
|
|
try:
|
|
got = run(doc, kind, c)
|
|
except (Refused, KeyError, TypeError, ValueError, IndexError, ZeroDivisionError) as e:
|
|
got = {"error": repr(e)}
|
|
if got != c["outcome"]:
|
|
fails.append(c["name"])
|
|
if not quiet:
|
|
print(f"FAIL {kind} {c['name']}\n stated {json.dumps(c['outcome'])}\n got {json.dumps(got)}")
|
|
stated = []
|
|
for kind in KINDS:
|
|
for c in doc[kind]:
|
|
strings(c["outcome"], stated)
|
|
for code in reasons_table():
|
|
if not any(s == code or s.endswith("reason=" + code) for s in stated):
|
|
fails.append("no case gives " + code)
|
|
return fails
|
|
|
|
|
|
def mutations(doc):
|
|
"""Copies of the corpus, one defect each, every one of which must fail."""
|
|
def case(d, kind, name):
|
|
return next(c for c in d[kind] if c["name"] == name)
|
|
|
|
def m_bit(d):
|
|
c = case(d, "records", "healthy")
|
|
b = bytearray.fromhex(c["bytes"])
|
|
b[60] ^= 0x01
|
|
c["bytes"] = b.hex()
|
|
|
|
def m_digest(d):
|
|
case(d, "records", "black")["outcome"]["digest"] = "00" * 32
|
|
|
|
def m_faults(d):
|
|
case(d, "records", "silence")["outcome"]["faults"] = []
|
|
|
|
def m_luma(d):
|
|
f = case(d, "checks", "black")["media"]["frames"][6]
|
|
f["luma"] = "ff" + f["luma"][2:]
|
|
|
|
def m_threshold(d):
|
|
case(d, "checks", "healthy")["profile"]["black"]["min_ms"] = 800
|
|
|
|
def m_doc_threshold(d):
|
|
d["profile"]["transport"]["pid_max_gap_ms"] = 999
|
|
|
|
def m_packet(d):
|
|
t = case(d, "checks", "transport_cc")["media"]["transport"]
|
|
b = bytearray.fromhex(t["bytes"])
|
|
b[30 * 188 + 5] ^= 0x80
|
|
t["bytes"] = b.hex()
|
|
|
|
def m_rate(d):
|
|
case(d, "checks", "transport_pid")["media"]["transport"]["rate_bps"] += 1
|
|
|
|
def m_caption(d):
|
|
case(d, "checks", "captions")["media"]["captions"][1] += 1
|
|
|
|
def m_at(d):
|
|
case(d, "switches", "primary_stale")["at_ms"] -= 2_000
|
|
|
|
def m_inputs(d):
|
|
d["switch"]["inputs"].reverse()
|
|
|
|
def m_trusted(d):
|
|
d["switch"]["trusted"].pop()
|
|
|
|
def m_reason_gone(d):
|
|
d["checks"].remove(case(d, "checks", "value_not_given"))
|
|
|
|
def m_media_gone(d):
|
|
del case(d, "checks", "healthy")["media"]["frames"]
|
|
|
|
return [m_bit, m_digest, m_faults, m_luma, m_threshold, m_doc_threshold, m_packet, m_rate, m_caption, m_at,
|
|
m_inputs, m_trusted, m_reason_gone, m_media_gone]
|
|
|
|
|
|
WEAKENINGS = ["name", "window", "order", "range", "values", "signature", "verdict", "profile", "digest", "media_window",
|
|
"black_ratio", "freeze_size", "freeze_hold", "silence_channels", "gap_edges", "hysteresis", "loss_on_two",
|
|
"duplicate", "discontinuity", "scrambling", "table_gap", "pat_table", "pid_gap", "es_removed",
|
|
"window_end", "trusted", "not_yet", "supersede", "contradiction", "freshness"]
|
|
|
|
|
|
def main(argv):
|
|
global DROPPED
|
|
if len(argv) != 2:
|
|
print(__doc__)
|
|
return 2
|
|
doc = json.load(open(os.path.join(argv[1], "vectors.json"), encoding="utf-8"))
|
|
extra = sorted(set(os.listdir(argv[1])) - {"vectors.json", "README.md"})
|
|
fails = check(doc)
|
|
if extra:
|
|
fails.append(f"files the corpus does not hold: {extra}")
|
|
# §6.1's bound, which no vector carries: checked here.
|
|
try:
|
|
read(b"\x00" * (MAX_LEN + 1))
|
|
fails.append("a record past 1024 bytes was read")
|
|
except Refused as r:
|
|
if r.reason != "signal_health_malformed":
|
|
fails.append("a record past the bound was refused " + r.reason)
|
|
print(f"{sum(len(doc[k]) for k in KINDS)} cases ({', '.join(f'{len(doc[k])} {k}' for k in KINDS)}): {len(fails)} failure(s)")
|
|
caught = 0
|
|
for m in mutations(doc):
|
|
d = copy.deepcopy(doc)
|
|
m(d)
|
|
if check(d, quiet=True):
|
|
caught += 1
|
|
else:
|
|
fails.append(f"mutation {m.__name__} was not caught")
|
|
print(f"{caught} of {len(mutations(doc))} mutated copies fail")
|
|
weak = 0
|
|
for w in WEAKENINGS:
|
|
DROPPED = w
|
|
if check(doc, quiet=True):
|
|
weak += 1
|
|
else:
|
|
fails.append(f"a verifier without its {w} check passes the corpus")
|
|
DROPPED = None
|
|
print(f"{weak} of {len(WEAKENINGS)} weakened verifiers are caught")
|
|
for f in fails:
|
|
print("FAIL", f)
|
|
return 1 if fails else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv))
|