open-standards/wai/live-conformance/injection_verify.py
Transaction Science 1eabee8f24 wai: register wai.live.injection; SPEC §5, CHANGELOG, the crate's README; CI
The registry row and its export; the verifiers' mutation self-tests; a step for both
verifiers in wai-conformance, and joule-code-conformance's path filters for the
corpora and texts its tests read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 23:02:04 -04:00

647 lines
26 KiB
Python

#!/usr/bin/env python3
"""Second implementation of WAI's cue injection (extensions/cue-injection.md), written from
that text, from ANSI/SCTE 104 2023 and ANSI/SCTE 35-1 2023r2, and from live-events and
live-ads where it cites them. It shares no code with the reference (joule-moq-ingest).
For the corpus live-conformance/injection/vectors.json it:
- translates every message (§4 to §6) and requires the processing instant, the warning, and
each request's opID, splice instant, section, cues and withdrawals the corpus states; a
translation that names a standard section requires that section byte for byte;
- requires every refusal's result code, extension and reason (§4.1, §5, §9);
- runs the injector's rules (§7.3) over every scenario, requires each message's result,
what was dropped and withdrawn, and the chain: each event verifies under the principal
(WAI's strict Ed25519), links to the one before, and is the record and cue §7.2 makes;
then verifies the chain as §8 does and derives its breaks and chapters by live-ads §4.3;
- verifies every negative chain and requires the event and reason it is refused at.
python3 -I injection_verify.py [--self-test] <live-conformance/injection>
With --self-test it then requires every mutated copy of the corpus (a section byte, a cue's
instant or duration, a refusal's reason, an event dropped or its signature changed, a break's
end, a count, where a negative chain is refused) to fail.
Exit status 0 only if every case agrees. Python 3.8 or later; standard library only.
"""
import hashlib
import json
import os
import struct
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
EVENT_DOMAIN = b"wai:live-event\x01"
CUEI = b"CUEI"
EPOCH_1980 = 315_964_800
MIN_PREROLL = 4000
# §4.3: the requests this revision implements.
NORMAL = {0x0101: "splice", 0x0102: "splice_null", 0x0104: "time_signal"}
SUPPLEMENTAL = {0x0108, 0x0109, 0x010A, 0x010B, 0x010F, 0x0110}
UNIMPLEMENTED = {0x0100, 0x0103, 0x0105, 0x0106, 0x0107, 0x010C, 0x010D, 0x010E, 0x0111, 0x0112, 0x0113, 0x0300, 0x0301}
# §6.2.
BREAK_STARTS = {0x22, 0x30, 0x32, 0x34, 0x36}
BREAK_ENDS = {0x23, 0x31, 0x33, 0x35, 0x37}
CHAPTER_START = 0x20
SUB_SEGMENT_TYPES = {0x30, 0x32, 0x34, 0x36, 0x38, 0x3A, 0x44, 0x46}
class Refused(Exception):
def __init__(self, result, reason, ext=0xFFFF):
super().__init__(reason)
self.result, self.reason, self.ext = result, reason, ext
def crc32_mpeg2(b):
"""ISO/IEC 13818-1 Annex A: polynomial 0x04C11DB7, preset all ones, no reflection."""
c = 0xFFFFFFFF
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 pts(s_us):
"""§5.2: the room clock at 90 kHz, modulo 2^33."""
return (s_us * 9 // 100) % (1 << 33)
def splice_time(p):
"""SCTE 35-1 Table 14: time_specified_flag 1, six reserved bits (1), pts_time."""
return bytes([0xFE | (p >> 32)]) + (p & 0xFFFFFFFF).to_bytes(4, "big")
def break_duration(auto, ticks):
"""SCTE 35-1 Table 15."""
return bytes([(0x80 if auto else 0) | 0x7E | (ticks >> 32)]) + (ticks & 0xFFFFFFFF).to_bytes(4, "big")
def section(tier, cmd_type, cmd, descriptors):
"""§5.1 and SCTE 35-1 Table 5."""
loop = b"".join(descriptors)
length = 1 + 5 + 1 + 3 + 1 + len(cmd) + 2 + len(loop) + 4
if length > 4093:
raise Refused(115, "section_too_long")
o = bytearray([0xFC, 0x30 | (length >> 8), length & 0xFF, 0])
o += bytes(5) # encrypted_packet, encryption_algorithm, pts_adjustment
o.append(0xFF) # cw_index
o += bytes([(tier >> 4) & 0xFF, ((tier & 0xF) << 4) | (len(cmd) >> 8), len(cmd) & 0xFF, cmd_type])
o += cmd + len(loop).to_bytes(2, "big") + loop
o += crc32_mpeg2(bytes(o)).to_bytes(4, "big")
return bytes(o)
def descriptor(tag, body):
return bytes([tag, len(body)]) + body
class Reader:
def __init__(self, b):
self.b, self.p = b, 0
def take(self, n):
if self.p + n > len(self.b):
raise Refused(114, "message_size")
s = self.b[self.p:self.p + n]
self.p += n
return s
def u(self, n):
return int.from_bytes(self.take(n), "big")
def left(self):
return len(self.b) - self.p
def flag(x):
if x not in (0, 1):
raise Refused(115, "field_range")
return x == 1
def segmentation(d, prm):
"""§5.6, SCTE 104 Table 9-29 to SCTE 35-1 Table 20."""
r = Reader(d)
eid = r.u(4)
cancel = flag(r.u(1))
duration = r.u(2)
upid_type = r.u(1)
upid = r.take(r.u(1))
type_id, seg_num, seg_exp = r.u(1), r.u(1), r.u(1)
rest = r.left()
ext, flags, sub = 0, None, None
if rest == 0:
pass
elif rest == 1:
ext = r.u(1)
elif rest in (6, 9):
ext = r.u(1)
flags = [r.u(1) for _ in range(5)]
if rest == 9:
sub = [r.u(1) for _ in range(3)]
else:
raise Refused(114, "message_size")
body = CUEI + eid.to_bytes(4, "big")
if cancel:
return {"event_id": eid, "cancel": True, "type_id": type_id, "ticks": None, "descriptor": descriptor(0x02, body + b"\xff")}
body += b"\x7f"
if flags is None:
dnr, web, nrb, arch, dev = True, False, False, False, 0
else:
if flags[4] > 3:
raise Refused(115, "field_range")
dnr, web, nrb, arch = (flag(x) for x in flags[:4])
dev = flags[4]
ticks = None
if duration > 0:
ticks = duration * 90000 + (ext * 90000 * prm["fps_den"]) // prm["fps_num"]
b = 0x80 | (0x40 if ticks is not None else 0) | (0x20 if dnr else 0)
b |= 0x1F if dnr else (web << 4) | (nrb << 3) | (arch << 2) | dev
body += bytes([b])
if ticks is not None:
body += ticks.to_bytes(5, "big")
body += bytes([upid_type, len(upid)]) + upid + bytes([type_id, seg_num, seg_exp])
if sub is not None and flag(sub[0]):
if type_id not in SUB_SEGMENT_TYPES:
raise Refused(115, "field_range")
body += bytes(sub[1:])
if len(body) > 254:
raise Refused(115, "descriptor_too_long")
return {"event_id": eid, "cancel": False, "type_id": type_id, "ticks": ticks, "descriptor": descriptor(0x02, body)}
def supplements(sups, prm):
descs, tier, segs = [], None, []
for opid, d in sups:
if opid == 0x010A:
if not d or len(d) != 1 + 4 * d[0]:
raise Refused(114, "message_size")
for i in range(d[0]):
descs.append(descriptor(0x00, CUEI + d[1 + 4 * i:5 + 4 * i]))
elif opid == 0x0109:
if len(d) < 2 or len(d) != 2 + d[1]:
raise Refused(114, "message_size")
if d[1] > 7:
raise Refused(115, "field_range")
descs.append(descriptor(0x01, CUEI + bytes([d[0], (d[1] << 5) | 0x1F]) + d[2:]))
elif opid == 0x0110:
if len(d) != 12:
raise Refused(114, "message_size")
descs.append(descriptor(0x03, CUEI + d))
elif opid == 0x0108:
if not d:
raise Refused(114, "message_size")
q = 1
for _ in range(d[0]):
if q + 2 > len(d) or q + 2 + d[q + 1] > len(d):
raise Refused(114, "message_size")
img = d[q:q + 2 + d[q + 1]]
if img[0] <= 0x04 and img[2:6] == CUEI:
raise Refused(115, "descriptor_image_standard")
descs.append(img)
q += len(img)
if q != len(d):
raise Refused(114, "message_size")
elif opid == 0x010F:
if len(d) != 2:
raise Refused(114, "message_size")
t = int.from_bytes(d, "big")
if t >> 12 or tier is not None:
raise Refused(115, "field_range")
tier = t
elif opid == 0x010B:
s = segmentation(d, prm)
descs.append(s["descriptor"])
segs.append(s)
return descs, (0xFFF if tier is None else tier), segs
def cue_payload(c):
o = bytes([c["kind"]]) + struct.pack("<II", c["cue_id"], c["duration_ms"])
if c["section"] is None:
return o + b"\x00"
return o + b"\x01" + struct.pack("<H", len(c["section"])) + c["section"]
def request(name, data, sups, P, prm):
descs, tier, segs = supplements(sups, prm)
warning = None
if name == "splice":
if len(data) not in (14, 15):
raise Refused(114, "message_size")
ty = data[0]
eid = int.from_bytes(data[1:5], "big")
upid = data[5:7]
preroll = int.from_bytes(data[7:9], "big")
tenths = int.from_bytes(data[9:11], "big")
avail_num, avails_exp, auto = data[11], data[12], data[13] != 0
if ty == 5:
return {"opid": 0x0101, "splice_us": P, "section": None, "cues": [], "withdraws": [["splice", eid]]}, None
if ty < 1 or ty > 4:
raise Refused(121, "splice_type")
start = ty <= 2
delay = preroll if ty in (1, 3) else 0
if ty == 1 and 0 < delay < MIN_PREROLL:
warning = 122
S = P + delay * 1000
cmd = bytearray(eid.to_bytes(4, "big") + b"\x7f")
has_dur = start and tenths > 0
cmd.append((0x80 if start else 0) | 0x40 | (0x20 if has_dur else 0) | (0x10 if delay == 0 else 0) | 0x08 | 0x07)
if delay > 0:
cmd += splice_time(pts(S))
if has_dur:
cmd += break_duration(auto, tenths * 9000)
cmd += upid + bytes([avail_num, avails_exp])
sec = section(tier, 0x05, bytes(cmd), descs)
dur = tenths * 100 if start else 0
cues = [{"t": S, "kind": 1 if start else 2, "cue_id": eid, "duration_ms": dur, "derived": False, "section": sec}]
withdraws = []
if start and auto and tenths > 0:
cues.append({"t": S + tenths * 100_000, "kind": 2, "cue_id": eid, "duration_ms": dur, "derived": True, "section": None})
if not start:
withdraws.append(["auto_return", eid])
return {"opid": 0x0101, "splice_us": S, "section": sec, "cues": cues, "withdraws": withdraws}, warning
if name == "time_signal":
if len(data) != 2:
raise Refused(114, "message_size")
S = P + int.from_bytes(data, "big") * 1000
sec = section(tier, 0x06, splice_time(pts(S)), descs)
ins, outs, chapters, withdraws = [], [], [], []
for s in segs:
if s["cancel"]:
withdraws.append(["segmentation", s["event_id"]])
continue
dur = (s["ticks"] // 90) if s["ticks"] is not None else 0
c = {"t": S, "cue_id": s["event_id"], "derived": False, "section": sec}
if s["type_id"] in BREAK_ENDS:
ins.append(dict(c, kind=2, duration_ms=0))
elif s["type_id"] in BREAK_STARTS:
outs.append(dict(c, kind=1, duration_ms=dur))
elif s["type_id"] == CHAPTER_START:
chapters.append(dict(c, kind=3, duration_ms=dur))
return {"opid": 0x0104, "splice_us": S, "section": sec, "cues": ins + outs + chapters, "withdraws": withdraws}, None
if data:
raise Refused(114, "message_size")
return {"opid": 0x0102, "splice_us": P, "section": section(tier, 0x00, b"", descs), "cues": [], "withdraws": []}, None
def translate(msg, received, prm):
"""§4 to §6. Returns the translation, or raises Refused."""
r = Reader(msg)
if r.u(2) != 0xFFFF:
raise Refused(115, "not_multiple")
if r.u(2) != len(msg):
raise Refused(114, "message_size")
pv, as_index, number, dpi, scte_pv = r.u(1), r.u(1), r.u(1), r.u(2), r.u(1)
if pv != 0 or scte_pv != 0:
raise Refused(127, "version_unsupported")
tt = r.u(1)
if tt == 0:
P = received
elif tt == 1:
secs, us = r.u(4), r.u(2) << 8
unix_us = (secs + EPOCH_1980 - prm["leap_s"]) * 1_000_000 + us
if unix_us < prm["t0_us"]:
raise Refused(115, "timestamp_before_room")
P = max(unix_us - prm["t0_us"], received)
else:
raise Refused(123, "time_type_unsupported")
n = r.u(1)
if n == 0:
raise Refused(115, "no_operation")
ops = []
for _ in range(n):
opid = r.u(2)
ops.append((opid, r.take(r.u(2))))
if r.left():
raise Refused(114, "message_size")
groups = []
for opid, d in ops:
if opid in NORMAL:
groups.append([NORMAL[opid], d, []])
elif opid in SUPPLEMENTAL:
if not groups:
raise Refused(115, "supplemental_first")
groups[-1][2].append((opid, d))
elif opid in UNIMPLEMENTED:
raise Refused(124, "operation_unsupported", opid)
else:
raise Refused(125, "opid_unknown", opid)
reqs, warning = [], None
for name, d, sups in groups:
q, w = request(name, d, sups, P, prm)
warning = warning or w
reqs.append(q)
if sum(len(q["cues"]) for q in reqs) > 255:
raise Refused(115, "too_many_cues")
return {"number": number, "as_index": as_index, "dpi": dpi, "processed_us": P, "warning": warning, "requests": reqs}
def cues_of(tr):
return [c for q in tr["requests"] for c in q["cues"]]
def injection_record(received, prm, index, msg):
"""§7.1."""
return b"WIJ1\x01" + struct.pack("<QBIIBH", received, prm["leap_s"], prm["fps_num"], prm["fps_den"], index, len(msg)) + msg
# ── §7.3: the injector's rules ──────────────────────────────────────────────────
class Injector:
def __init__(self, prm):
self.prm = prm
self.held = [] # (cue, message, received, index, number, from_time_signal)
self.open = None
self.last_end = 0
self.outstanding = {}
self.counts = {}
self.signed = [] # (t, kind, payload)
def count(self, k, n=1):
self.counts[k] = self.counts.get(k, 0) + n
def accept(self, msg, received):
tr = translate(msg, received, self.prm)
if tr["number"] in self.outstanding:
self.count("repeated_message")
return 100
result = tr["warning"] or 100
for q in tr["requests"]:
for kind, eid in q["withdraws"]:
before = len(self.held)
if kind == "splice":
self.held = [h for h in self.held if not (h[0]["cue_id"] == eid and not h[5])]
elif kind == "segmentation":
self.held = [h for h in self.held if not (h[0]["cue_id"] == eid and h[5])]
else:
self.held = [h for h in self.held if not (h[0]["cue_id"] == eid and h[0]["derived"])]
gone = before - len(self.held)
if gone == 0 and kind != "auto_return":
self.count("cancel_too_late")
result = 121
if gone:
self.count("withdrawn", gone)
i = 0
for q in tr["requests"]:
for c in q["cues"]:
self.held.append((c, msg, received, i, tr["number"], q["opid"] == 0x0104))
i += 1
if i:
self.outstanding[tr["number"]] = True
return result
def due(self, now):
due = sorted([h for h in self.held if h[0]["t"] <= now], key=lambda h: (h[0]["t"], h[2], h[3]))
self.held = [h for h in self.held if h[0]["t"] > now]
for c, msg, received, index, number, _ in due:
k = c["kind"]
if k == 1:
if self.open is not None or c["t"] < self.last_end:
self.count("break_overlap")
continue
self.open = (c["cue_id"], c["t"])
elif k == 2:
if self.open is None or self.open[0] != c["cue_id"]:
self.count("cue_in_unmatched")
continue
if (c["t"] - self.open[1]) // 1000 == 0:
self.count("break_zero_duration")
continue
self.last_end = c["t"]
self.open = None
self.signed.append((c["t"], 0x01, injection_record(received, self.prm, index, msg)))
self.signed.append((c["t"], 0x02, cue_payload(c)))
alive = {h[4] for h in self.held}
for m in list(self.outstanding):
if m not in alive:
del self.outstanding[m]
# ── Events (live-events §3) and §8 ───────────────────────────────────────────────
def read_event(e):
"""An event without an anchor: (room, principal, seq, prev, t, kind, payload, body, sig)."""
if len(e) < 121 + 64 or e[:5] != b"WEV1\x01" or e[117] != 0:
return None
n = struct.unpack_from("<H", e, 119)[0]
if len(e) != 121 + n + 64:
return None
return {"room": e[5:37], "principal": e[37:69], "seq": struct.unpack_from("<Q", e, 69)[0], "prev": e[77:109],
"t": struct.unpack_from("<Q", e, 109)[0], "kind": e[118], "payload": e[121:121 + n], "body": e[:121 + n], "sig": e[121 + n:]}
def verify_chain(events, principal, prm):
"""§8 over an injector's chain: None, or (index, reason)."""
prev = bytes(32)
evs = []
for i, raw in enumerate(events):
e = read_event(raw)
if e is None or e["principal"] != principal:
return (i, "malformed")
if ED.verify(principal, EVENT_DOMAIN + e["body"], e["sig"]) is not None:
return (i, "bad_signature")
if e["seq"] != i or e["prev"] != prev:
return (i, "chain_broken")
prev = hashlib.sha256(e["body"]).digest()
evs.append(e)
i = 0
while i < len(evs):
e = evs[i]
if e["kind"] == 0x01 and e["payload"][:4] == b"WIJ1":
p = e["payload"]
if len(p) < 25 or p[4] != 1:
return (i, "injection_malformed")
received, leap, fn, fd, index, mlen = struct.unpack_from("<QBIIBH", p, 5)
if fn == 0 or fd == 0 or len(p) != 25 + mlen:
return (i, "injection_malformed")
try:
tr = translate(p[25:], received, {"t0_us": prm["t0_us"], "leap_s": leap, "fps_num": fn, "fps_den": fd})
except Refused:
return (i, "injection_untranslatable")
if i + 1 >= len(evs) or evs[i + 1]["kind"] != 0x02:
return (i, "injection_unanswered")
cs = cues_of(tr)
nxt = evs[i + 1]
if index >= len(cs) or nxt["payload"] != cue_payload(cs[index]) or nxt["t"] != cs[index]["t"] or e["t"] != cs[index]["t"]:
return (i + 1, "injection_mismatch")
i += 2
continue
if e["kind"] == 0x02:
return (i, "injection_missing")
i += 1
return None
def breaks_of(events):
"""live-ads §4.3 over one chain's cues: (breaks, chapters)."""
open_, breaks, chapters = None, [], []
for raw in events:
e = read_event(raw)
if e["kind"] != 0x02:
continue
ty, cid, dur = e["payload"][0], struct.unpack_from("<I", e["payload"], 1)[0], struct.unpack_from("<I", e["payload"], 5)[0]
if ty == 1:
assert open_ is None, "an out while a break is open"
open_ = (cid, e["t"])
elif ty == 2:
assert open_ is not None and open_[0] == cid, "an in that closes nothing"
d = (e["t"] - open_[1]) // 1000
assert d > 0 and (dur == 0 or dur == d), "cue_duration_mismatch"
breaks.append({"cue_id": cid, "out_t": open_[1], "in_t": e["t"], "duration_ms": d})
open_ = None
elif ty == 3:
chapters.append({"cue_id": cid, "t": e["t"], "duration_ms": dur})
return {"breaks": breaks, "chapters": chapters}
# ── The corpus ───────────────────────────────────────────────────────────────────
def hx(b):
return None if b is None else b.hex()
def check(v):
"""Every check of the corpus `v`: (failures, checks)."""
prm = v["params"]
principal = bytes.fromhex(v["principal"])
fails, n = [], 0
def expect(ok, what):
nonlocal n
n += 1
if not ok:
fails.append(what)
for t in v["translations"]:
try:
tr = translate(bytes.fromhex(t["message"]), t["received_us"], prm)
except Refused as e:
expect(False, f"{t['name']}: refused {e.reason}")
continue
expect(tr["processed_us"] == t["processed_us"] and tr["warning"] == t["warning"], f"{t['name']}: processing")
expect(len(tr["requests"]) == len(t["requests"]), f"{t['name']}: requests")
for mine, want in zip(tr["requests"], t["requests"]):
expect(mine["opid"] == want["opid"] and mine["splice_us"] == want["splice_us"], f"{t['name']}: request")
expect(hx(mine["section"]) == want["section"], f"{t['name']}: section")
if mine["section"] is not None:
expect(crc32_mpeg2(mine["section"]) == 0, f"{t['name']}: CRC over the section")
expect(mine["withdraws"] == want["withdraws"], f"{t['name']}: withdrawals")
cs = [{"t": c["t"], "kind": c["kind"], "cue_id": c["cue_id"], "duration_ms": c["duration_ms"], "derived": c["derived"], "section": hx(c["section"]), "payload": cue_payload(c).hex()} for c in mine["cues"]]
expect(cs == want["cues"], f"{t['name']}: cues")
if "standard_section" in t:
expect(hx(tr["requests"][0]["section"]) == t["standard_section"], f"{t['name']}: the standard's sample, byte for byte")
for r in v["refusals"]:
try:
translate(bytes.fromhex(r["message"]), r["received_us"], prm)
expect(False, f"{r['name']}: accepted")
except Refused as e:
expect((e.result, e.ext, e.reason) == (r["result"], r["ext"], r["reason"]), f"{r['name']}: {e.result} {e.ext} {e.reason}")
for s in v["scenarios"]:
inj = Injector(prm)
try:
for m in s["messages"]:
inj.due(m["received_us"])
res = inj.accept(bytes.fromhex(m["message"]), m["received_us"])
expect(res == m["result"], f"{s['name']}: a message's result {res}")
inj.due(s["until_us"])
except Refused as e:
expect(False, f"{s['name']}: a message refused {e.reason}")
continue
expect(inj.counts == s["counts"], f"{s['name']}: counts {inj.counts}")
evs = [bytes.fromhex(e) for e in s["events"]]
expect(len(evs) == len(inj.signed), f"{s['name']}: {len(inj.signed)} events, the corpus {len(evs)}")
for raw, (t, kind, payload) in zip(evs, inj.signed):
e = read_event(raw)
expect(e is not None and (e["t"], e["kind"], e["payload"]) == (t, kind, payload) and e["room"] == bytes.fromhex(v["room"]), f"{s['name']}: an event")
expect(verify_chain(evs, principal, prm) is None, f"{s['name']}: the chain verifies")
try:
b = breaks_of(evs)
except (AssertionError, TypeError):
b = None
expect(b == s["breaks"], f"{s['name']}: breaks {b}")
for c in v["negative_chains"]:
got = verify_chain([bytes.fromhex(e) for e in c["events"]], principal, prm)
expect(got == (c["refused_at"], c["reason"]), f"{c['name']}: {got}")
return fails, n
def flip(h, i):
b = bytearray(bytes.fromhex(h))
b[i % len(b)] ^= 0x01
return b.hex()
def mutations(v):
"""Copies of the corpus, each with one stated value changed: each must fail."""
import copy
for i, t in enumerate(v["translations"]):
for q, r in enumerate(t["requests"]):
if r["section"]:
m = copy.deepcopy(v)
m["translations"][i]["requests"][q]["section"] = flip(r["section"], 7 + q)
yield f"translation {t['name']}: a section byte", m
for c, cue in enumerate(r["cues"]):
m = copy.deepcopy(v)
m["translations"][i]["requests"][q]["cues"][c]["t"] += 1
yield f"translation {t['name']}: a cue's instant", m
m = copy.deepcopy(v)
m["translations"][i]["requests"][q]["cues"][c]["duration_ms"] += 1
yield f"translation {t['name']}: a cue's duration", m
if "standard_section" in t:
m = copy.deepcopy(v)
m["translations"][i]["standard_section"] = flip(t["standard_section"], -1)
yield "the standard's sample's CRC", m
for i, r in enumerate(v["refusals"]):
m = copy.deepcopy(v)
m["refusals"][i]["reason"] = "message_size" if r["reason"] != "message_size" else "field_range"
yield f"refusal {r['name']}: its reason", m
for i, s in enumerate(v["scenarios"]):
if s["events"]:
m = copy.deepcopy(v)
m["scenarios"][i]["events"] = s["events"][:-2]
yield f"scenario {s['name']}: its last cue dropped", m
m = copy.deepcopy(v)
m["scenarios"][i]["events"][-1] = flip(s["events"][-1], -1)
yield f"scenario {s['name']}: a signature", m
if s["breaks"]["breaks"]:
m = copy.deepcopy(v)
m["scenarios"][i]["breaks"]["breaks"][0]["in_t"] += 1000
yield f"scenario {s['name']}: a break's end", m
m = copy.deepcopy(v)
m["scenarios"][i]["counts"]["withdrawn"] = 99
yield f"scenario {s['name']}: a count", m
for i, c in enumerate(v["negative_chains"]):
m = copy.deepcopy(v)
m["negative_chains"][i]["refused_at"] += 1
yield f"negative {c['name']}: where it is refused", m
def main(args):
self_test = "--self-test" in args
args = [a for a in args if a != "--self-test"]
d = args[0] if args else os.path.join(HERE, "injection")
v = json.load(open(os.path.join(d, "vectors.json")))
fails, n = check(v)
for f in fails:
print("FAIL", f)
print(f"injection: {n - len(fails)} of {n} checks agree "
f"({len(v['translations'])} translations, {len(v['refusals'])} refusals, {len(v['scenarios'])} scenarios, {len(v['negative_chains'])} negative chains)")
if fails:
return 1
if self_test:
survived = [what for what, m in mutations(v) if not check(m)[0]]
total = sum(1 for _ in mutations(v))
for w in survived:
print("SURVIVED", w)
print(f"injection self-test: {total - len(survived)} of {total} mutated corpora refused")
return 1 if survived else 0
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))