#!/usr/bin/env python3 """Second implementation of extensions/opportunity-report.md (wai.delivery.opportunity). Written from the extension's text and sharing no code with wai-rs: it reads every opportunity report (WOR1), statement check (WCK1), SCTE 35 section (§5) and reported statement (§9) itself; verifies contracts, runs and signed revenue-share statements with revenue_share_verify.py, and rooms (their events, grants, policy and the schedule of breaks their cues make) with live_ads_verify.py; and recomputes every break, attribution, line, citation and flag. python3 -I opportunity_report_verify.py python3 -I opportunity_report_verify.py --document FILE With a corpus directory it gives every vector of vectors.json its outcome and compares it with the one the corpus states, requires every reason of §13 and every code it reuses to refuse a vector, requires every mutated copy of the corpus to fail, and drops each of its own checks in turn and requires the corpus to catch the weakened verifier. With --document it prints the outcome of one report's or check's verification document, status 0 only when it is accepted. Standard library only. """ import copy import hashlib import json import os import re import sys HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance")) import revenue_share_verify as RSV # noqa: E402 import live_ads_verify as LAV # noqa: E402 import live_rooms_verify as LRV # noqa: E402 import live_events_verify as LEV # noqa: E402 import ad_presentation_verify as APV # noqa: E402 EXTENSION = os.path.join(HERE, "..", "extensions", "opportunity-report.md") REPORT_MAGIC, CHECK_MAGIC = b"WOR1", b"WCK1" # §7.1, §10.1 REPORT_DOMAIN = b"wai:opportunity-report\x01" # §7.4 CHECK_DOMAIN = b"wai:statement-check\x01" # §10.5 CITES_DOMAIN = b"wai:opportunity-cites\x01" # §7.3 CUED, SCHEDULED = 1, 2 # §4.3 CLASS_NAMES = {CUED: "cued", SCHEDULED: "scheduled"} RUN_KIND, CUE_KIND, POLICY_KIND = 10, 12, 13 # §7.3 PERIOD, DAY, PROGRAMME = 1, 2, 3 # §10.2 SCOPE_NAMES = {PERIOD: "period", DAY: "day", PROGRAMME: "programme"} FLAGS = ["unproved", "above", "below", "impressions_above_signed", "impressions_below_signed", "revenue_above_signed", "revenue_below_signed"] # §10.3 FORMS = {"json": 1, "csv": 2} FORM_NAMES = {1: "json", 2: "csv"} BASES = {"gross": 1, "share": 2} BASIS_NAMES = {1: "gross", 2: "share"} WHOLE = 10 ** 6 U64 = 2 ** 64 - 1 DAY_US = 86_400_000_000 ZERO = bytes(32) ROOM_CARRIER = 1 # revenue-share §3.1 OPENING = (0x22, 0x30, 0x32, 0x34, 0x36) # §5.2, cue-injection §6.2 PLACEMENT = (0x34, 0x36) SUB_SEGMENTED = (0x30, 0x32, 0x34, 0x36, 0x38, 0x3A, 0x44, 0x46) # §5.1 step 7 REUSED = ["truncated", "malformed", "unknown_version", "bad_key", "bad_signature", "contract_mismatch", "outside_contract", "delivery_gap", "break_overlap", "outside_window", "asset_unmapped", "line_unsourced", "citation_mismatch"] SKIP = set() Refuse = RSV.Refuse # Pure steps, held once per input: a room's live-ads verification, a run's, a signed statement's. _ROOMS, _RUNS, _STATEMENTS = {}, {}, {} def memo(cache, value, f): k = json.dumps(value, sort_keys=True) if k not in cache: cache[k] = f() return cache[k] def sha(*parts): h = hashlib.sha256() for p in parts: h.update(p) return h.digest() def ed_verify(key, msg, sig): return RSV.ed_verify(key, msg, sig) def lowhex(v): """A byte field of a document: lowercase hexadecimal, or None.""" if not isinstance(v, str) or len(v) % 2 or any(c not in "0123456789abcdef" for c in v): return None return bytes.fromhex(v) # ---------------------------------------------------------------- §5 the section a cue carries class Bad(Exception): pass def crc32(b): """ISO/IEC 13818-1 Annex A: 0x04C11DB7, preset all ones, no reflection, no final inversion.""" 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 class Bits: """Big-endian bit fields of b, up to the byte `end`; running out is Bad.""" def __init__(self, b, start, end): self.b, self.at, self.end = b, start * 8, end def take(self, n): if self.at + n > self.end * 8: raise Bad() v = 0 for _ in range(n): v = (v << 1) | ((self.b[self.at // 8] >> (7 - self.at % 8)) & 1) self.at += 1 return v def skip(self, nbytes): if self.at // 8 + nbytes > self.end: raise Bad() self.at += 8 * nbytes def splice_time(r): if r.take(1): r.take(6) r.take(33) else: r.take(7) def segmentation(s, start, end): """SCTE 35-1 Table 20 after its identifier, filling [start, end) exactly.""" r = Bits(s, start, end) d = {"event_id": r.take(32), "cancelled": r.take(1) == 1, "duration": None, "type": None, "sub": None} r.take(7) if d["cancelled"]: if r.at // 8 != end: raise Bad() return d program, flagged = r.take(1), r.take(1) r.take(6) # delivery_not_restricted_flag and the restriction bits if not program: r.skip(6 * r.take(8)) if flagged: d["duration"] = r.take(40) r.take(8) # segmentation_upid_type r.skip(r.take(8)) d["type"] = r.take(8) r.take(8) # segment_num r.take(8) # segments_expected left = end - r.at // 8 if left == 2 and d["type"] in SUB_SEGMENTED: d["sub"] = (r.take(8), r.take(8)) elif left != 0: raise Bad() return d def nothing(command): return {"command": command, "insert": None, "id": None, "duration": None, "slots": None} def read_section(s): """§5.1, for what §5.2 says a section states; Bad is cue_section_invalid.""" n = len(s) if n < 20: raise Bad() crc_at = n - 4 if crc32(s[:crc_at]) != int.from_bytes(s[crc_at:], "big") and "section-crc" not in SKIP: raise Bad() if s[3] != 0: raise Bad() if s[4] >> 7: return nothing(None) cmd_len = ((s[11] & 0x0F) << 8) | s[12] cmd = s[13] if cmd_len == 0xFFF and cmd not in (0x00, 0x05, 0x06): return nothing(cmd) end = crc_at if cmd_len == 0xFFF else 14 + cmd_len if end > crc_at: raise Bad() r = Bits(s, 14, end) st = nothing(cmd) if cmd == 0x05: # splice_insert() st["id"] = r.take(32) cancel = r.take(1) == 1 r.take(7) out = False if not cancel: out, program, flagged, immediate = r.take(1) == 1, r.take(1), r.take(1), r.take(1) r.take(4) if program and not immediate: splice_time(r) if not program: for _ in range(r.take(8)): r.take(8) if not immediate: splice_time(r) if flagged: r.take(7) st["duration"] = r.take(33) r.take(16) r.take(8) r.take(8) st["insert"] = (cancel, out) elif cmd == 0x06: # time_signal() splice_time(r) elif cmd != 0x00: r.at = end * 8 cmd_end = r.at // 8 if cmd_len != 0xFFF and cmd_end != end: raise Bad() if cmd_end + 2 > crc_at: raise Bad() p = cmd_end + 2 if p + int.from_bytes(s[cmd_end:p], "big") != crc_at: raise Bad() segs = [] while p < crc_at: if p + 2 > crc_at: raise Bad() tag, ln = s[p], s[p + 1] if p + 2 + ln > crc_at: raise Bad() if tag == 0x02 and ln >= 4 and s[p + 2:p + 6] == b"CUEI": segs.append(segmentation(s, p + 6, p + 2 + ln)) p += 2 + ln live = [d for d in segs if not d["cancelled"]] if cmd == 0x06: opening = next((d for d in live if d["type"] in OPENING), None) if opening is not None: st["id"], st["duration"] = opening["event_id"], opening["duration"] ppo = next((d for d in live if d["type"] in PLACEMENT), None) if ppo is not None and ppo["sub"] is not None and ppo["sub"][1] > 0: st["slots"] = ppo["sub"][1] return st def agrees(st, cue): """§5.3: the section and its opening cue's summary describe one splice.""" if st["insert"] is not None and (st["insert"][0] or not st["insert"][1]): return False if st["id"] is not None and st["id"] != cue["cue_id"]: return False return st["duration"] is None or st["duration"] // 90 == cue["duration_ms"] def command_name(c): return {None: "encrypted", 0x05: "splice_insert", 0x06: "time_signal", 0x00: "splice_null"}.get(c, "other") # ---------------------------------------------------------------- §3 the evidence def the_contract(doc): """§8.2 steps 1 and 2.""" c = RSV.contract_at(RSV.unhex(doc["contract"]), ["contract"]) for k, h in enumerate(doc["contracts"]): o = RSV.contract_at(RSV.unhex(h), ["contracts", k]) if o["digest"] != c["digest"] and RSV.overlaps(o, c) and "contract-overlap" not in SKIP: raise Refuse("contract_overlap", ["contracts", k]) return c def the_runs(c, runs): """§8.2 step 4, as revenue-share §8.2 step 4.""" out = [] for k, o in enumerate(runs): try: r = RSV.read_run(o) except RSV.NoRead: raise Refuse("malformed", ["run", k]) if "run-verify" not in SKIP and not memo(_RUNS, o, lambda: RSV.run_verifies(r)): raise Refuse("run_invalid", ["run", k]) if "run-foreign" not in SKIP and (r["channel_id"].encode() != c["channel"] or r["signer_pubkey"] != c["channel_signer"]): raise Refuse("run_foreign", ["run", k]) if "run-overlap" not in SKIP and any(r["window_start_ms"] < x["window_end_ms"] and x["window_start_ms"] < r["window_end_ms"] for x in out): raise Refuse("run_overlap", ["run", k]) out.append(r) return out def room_reads(v): """§8.1: a room's object.""" if not isinstance(v, dict) or set(v) != {"descriptor", "grants", "key_log", "chains", "policy"}: return False if lowhex(v["descriptor"]) is None or lowhex(v["policy"]) is None: return False if not isinstance(v["grants"], list) or any(lowhex(g) is None for g in v["grants"]): return False if v["key_log"] is not None and not isinstance(v["key_log"], str): return False return isinstance(v["chains"], list) and all(isinstance(c, list) and all(lowhex(e) is not None for e in c) for c in v["chains"]) def room_outcome(v): """live-ads §9.2 steps 1 to 3 over a room with no receipts: live_ads_verify.py's outcome.""" holder = {"creatives": [], "issuers": []} APV.held(holder) lv = dict(v, pt0_ms=0, receipts=[], pods=[], claims=[], creatives=[], issuers=[], name="room") return LAV.ads(lv, judge=lambda vv, d, p: APV.Judge(holder, vv, p))[0] def the_rooms(c, rooms): """§8.2 step 5: each verified as live-ads §9.2 steps 1 to 3 verify one, with no receipts.""" out = [] for k, v in enumerate(rooms): at = ["room", k] if not room_reads(v): raise Refuse("malformed", at) got = memo(_ROOMS, v, lambda: room_outcome(v)) if got["outcome"] == "gap": raise Refuse("delivery_gap", at) if got["outcome"] != "accept": raise Refuse(got["reason"], at + got["at"]) room_id = bytes.fromhex(got["room"]) if (ROOM_CARRIER, room_id) not in c["carriers"] and "room-foreign" not in SKIP: raise Refuse("room_foreign", at) if any(x["id"] == room_id for x in out) and "room-repeated" not in SKIP: raise Refuse("room_repeated", at) policy = LAV.read_policy(bytes.fromhex(v["policy"])) events = {} for chain in v["chains"]: for h in chain: ev = LEV.read_record(bytes.fromhex(h)) events[ev["digest"]] = ev out.append({"id": room_id, "t0": LRV.read_descriptor(bytes.fromhex(v["descriptor"]))["t0"], "slots": policy["slots"], "policy": sha(policy["body"]), "breaks": got["breaks"], "events": events}) return out # ---------------------------------------------------------------- §4, §6 breaks def share_of(c, party, split): return next((p for k, p in RSV.parties(c, split) if k == party), None) def attribute(c, run, rid, e, at): """§6: an entry's asset by the rule, and its split; the between split for none.""" asset = RSV.asset_of(run["schedule"], e, c["rule"]) split = c["between"] if asset is not None: split = RSV.split_of(c, asset.encode()) if split is None: if "unmapped" not in SKIP: raise Refuse("asset_unmapped", at) split = c["between"] return rid, e, asset, split def place(runs, t): """§6: the run whose window holds t, and its entry whose span does.""" for k, r in enumerate(runs): if r["window_start_ms"] * 1000 <= t < r["window_end_ms"] * 1000: for e, x in enumerate(r["schedule"]): if x["start_ms"] * 1000 <= t < (x["start_ms"] + x["duration_ms"]) * 1000: return k, e return None return None def the_breaks(c, start, end, runs, ids, rooms): """§8.2 steps 6 and 7.""" out, starts = [], [] for k, room in enumerate(rooms): mine = [] for i, b in enumerate(room["breaks"]): at = ["room", k, "break", i] s = room["t0"] + b["t"] if s > U64: continue starts.append(s) if not (start <= s < end) and "period" not in SKIP: continue cue = LEV.read_payload(0x02, room["events"][bytes.fromhex(b["digest"])]["payload"], 65535) st = None if cue["form"] == 1: try: st = read_section(cue["scte"]) except Bad: if "section-read" not in SKIP: raise Refuse("cue_section_invalid", at) if st is not None and not agrees(st, cue) and "section-agree" not in SKIP: raise Refuse("cue_section_mismatch", at) duration = b["duration_ms"] if b["duration_ms"] is not None else b["avail_ms"] if "break-overlap" not in SKIP and any(o["start"] < s + duration * 1000 and s < o["start"] + o["duration_ms"] * 1000 for o in out): raise Refuse("break_overlap", at) where = place(runs, s) if where is None: if "window" in SKIP: continue raise Refuse("outside_window", at) rid, e, asset, split = attribute(c, runs[where[0]], ids[where[0]], where[1], at) if st is not None and st["slots"] is not None and "slots-section" not in SKIP: slots, source, pol = st["slots"], "section", None else: slots, source, pol = room["slots"], "policy", room["policy"] cited = {(CUE_KIND, bytes.fromhex(b["digest"])), (RUN_KIND, rid)} if b["closed_by"] is not None: cited.add((CUE_KIND, bytes.fromhex(b["closed_by"]))) if pol is not None: cited.add((POLICY_KIND, pol)) mine.append({"class": CUED, "room": k, "index": i, "taken": None, "cue": b["digest"], "start": s, "duration_ms": duration, "open": b["type"] == "out" and b["closed_by"] is None, "slots": slots, "slots_from": source, "section": None if st is None else {"command": command_name(st["command"]), "id": st["id"], "duration": st["duration"], "slots": st["slots"]}, "run": rid, "entry": e, "asset": asset, "split": split, "cited": cited}) out += mine seen = set() for k in sorted(range(len(runs)), key=lambda j: runs[j]["window_start_ms"]): r = runs[k] for e in r["aired"]: x = r["schedule"][e] if x["kind"] != "ad": continue key = (x["asset_id"], x["start_ms"], x["duration_ms"]) if key in seen and "dedupe" not in SKIP: continue seen.add(key) s, z = x["start_ms"] * 1000, (x["start_ms"] + x["duration_ms"]) * 1000 if any(s <= t < z for t in starts) and "cued-test" not in SKIP: continue if not (start <= s < end) and "period" not in SKIP: continue rid, e2, asset, split = attribute(c, r, ids[k], e, ["run", k, "entry", e]) out.append({"class": SCHEDULED, "room": None, "index": None, "taken": {"run": k, "entry": e}, "cue": None, "start": s, "duration_ms": x["duration_ms"], "open": False, "slots": None, "slots_from": None, "section": None, "run": rid, "entry": e2, "asset": asset, "split": split, "cited": {(RUN_KIND, rid)}}) return out # ---------------------------------------------------------------- §7 the report def cites_digest(cited): return sha(CITES_DOMAIN, *[bytes([k]) + i for k, i in sorted(cited)]) def read_report(b): """§7.5, or Refuse at `report`.""" at = ["report"] bad = RSV.head(b, REPORT_MAGIC) if bad: raise Refuse(bad, at) r = RSV.R(b, 5) try: rep = {"issuer": r.take(32), "contract": r.take(32), "party": r.take(32), "start": r.u(8), "end": r.u(8)} rep["rooms"] = [r.take(32) for _ in range(r.u(2))] rep["runs"] = [r.take(32) for _ in range(r.u(2))] rep["lines"] = [] for _ in range(r.u(2)): rep["lines"].append({"split": r.u(2), "class": r.u(1), "breaks": r.u(8), "break_ms": r.u(8), "slots": r.u(8), "unslotted": r.u(8), "ppm": r.u(4), "cited": r.u(4), "cites": r.take(32)}) rep["totals"] = (r.u(8), r.u(8), r.u(8)) rep["sig"] = r.take(64) except ValueError: raise Refuse("truncated", at) if r.p != len(b): raise Refuse("malformed", at) keys = [(x["split"], x["class"]) for x in rep["lines"]] rules = (rep["start"] < rep["end"] and rep["issuer"] != ZERO and rep["party"] != ZERO and RSV.ascending(rep["rooms"]) and RSV.ascending(rep["runs"]) and RSV.ascending(keys) and all(x["class"] in (1, 2) and x["breaks"] > 0 and x["cited"] > 0 and x["unslotted"] <= x["breaks"] and 1 <= x["ppm"] <= WHOLE for x in rep["lines"])) if not rules and "report-rules" not in SKIP: raise Refuse("opportunity_report_invalid", at) rep["body"] = b[:-64] rep["digest"] = sha(rep["body"]) return rep def groups_of(breaks): g = {} for b in breaks: x = g.setdefault((b["split"], b["class"]), {"breaks": 0, "break_ms": 0, "slots": 0, "unslotted": 0, "cited": set()}) x["breaks"] += 1 x["break_ms"] += b["duration_ms"] if b["slots"] is None: x["unslotted"] += 1 else: x["slots"] += b["slots"] x["cited"] |= b["cited"] return g def lines_of(c, party, breaks): """§7.2, §7.3: the party's lines and totals, or None for a figure past its field.""" out = [] for (split, cls), g in sorted(groups_of(breaks).items()): ppm = share_of(c, party, split) if ppm is None: continue out.append({"split": split, "class": cls, "breaks": g["breaks"], "break_ms": g["break_ms"], "slots": g["slots"], "unslotted": g["unslotted"], "ppm": ppm, "cited": len(g["cited"]), "cites": cites_digest(g["cited"]), "cited_records": sorted(g["cited"])}) totals = (sum(x["breaks"] for x in out), sum(x["break_ms"] for x in out), sum(x["slots"] for x in out)) if any(x[f] > U64 for x in out for f in ("break_ms", "slots")) or any(t > U64 for t in totals): return None, None return out, totals LINE_FIELDS = ("split", "class", "breaks", "break_ms", "slots", "unslotted", "ppm", "cited", "cites") def verify_report(doc): """§8.2: (outcome, held) where held is what a check reads of an accepted report.""" try: if not isinstance(doc, dict) or set(doc) != {"contract", "contracts", "report", "runs", "rooms"}: raise Refuse("malformed", []) if not all(isinstance(doc[k], list) for k in ("contracts", "runs", "rooms")): raise Refuse("malformed", []) if lowhex(doc["contract"]) is None or lowhex(doc["report"]) is None or any(lowhex(h) is None for h in doc["contracts"]): raise Refuse("malformed", []) c = the_contract(doc) # steps 1, 2 rep = read_report(bytes.fromhex(doc["report"])) # step 3 if "binding" not in SKIP and (rep["contract"] != c["digest"] or (rep["party"] != c["operator"] and rep["party"] not in c["owners"])): raise Refuse("contract_mismatch", ["report"]) if "contract-period" not in SKIP and not (c["start"] <= rep["start"] and rep["end"] <= c["end"]): raise Refuse("outside_contract", ["report"]) if "report-signature" not in SKIP: bad = ed_verify(rep["issuer"], REPORT_DOMAIN + rep["body"], rep["sig"]) if bad: raise Refuse(bad, ["report"]) runs = the_runs(c, doc["runs"]) # step 4 ids = [RSV.run_identity(r) for r in runs] rooms = the_rooms(c, doc["rooms"]) # step 5 breaks = the_breaks(c, rep["start"], rep["end"], runs, ids, rooms) # steps 6, 7 groups = groups_of(breaks) # step 8 for x in rep["lines"]: g = groups.get((x["split"], x["class"])) holds = x["split"] < len(c["splits"]) and share_of(c, rep["party"], x["split"]) is not None if "unsourced" not in SKIP and (g is None or not holds or x["breaks"] > g["breaks"]): raise Refuse("line_unsourced", ["report"]) lines, totals = lines_of(c, rep["party"], breaks) if lines is None: raise Refuse("opportunity_report_mismatch", ["report"]) for x in rep["lines"]: mine = [y for y in lines if (y["split"], y["class"]) == (x["split"], x["class"])] if mine and "citations" not in SKIP and (x["cited"] != mine[0]["cited"] or x["cites"] != mine[0]["cites"]): raise Refuse("citation_mismatch", ["report"]) same = (rep["rooms"] == sorted(r["id"] for r in rooms) and rep["runs"] == sorted(ids) and [{k: x[k] for k in LINE_FIELDS} for x in rep["lines"]] == [{k: y[k] for k in LINE_FIELDS} for y in lines] and rep["totals"] == totals) if not same and "mismatch" not in SKIP: raise Refuse("opportunity_report_mismatch", ["report"]) except Refuse as e: return {"outcome": "refuse", "reason": e.reason, "at": e.at}, None counted = [b for b in breaks if share_of(c, rep["party"], b["split"]) is not None] return accepted_report(c, rep, breaks, lines), {"contract": c, "report": rep, "counted": counted} def accepted_report(c, rep, breaks, lines): """§8.3.""" return { "outcome": "accept", "report": rep["digest"].hex(), "contract": rep["contract"].hex(), "issuer": rep["issuer"].hex(), "party": rep["party"].hex(), "role": "operator" if rep["party"] == c["operator"] else "owner", "start": rep["start"], "end": rep["end"], "rooms": [d.hex() for d in rep["rooms"]], "runs": [d.hex() for d in rep["runs"]], "breaks": [{"class": CLASS_NAMES[b["class"]], "room": b["room"], "index": b["index"], "taken": b["taken"], "cue": b["cue"], "start": b["start"], "duration_ms": b["duration_ms"], "open": b["open"], "slots": b["slots"], "slots_from": b["slots_from"], "section": b["section"], "run": b["run"].hex(), "entry": b["entry"], "asset": b["asset"], "split": b["split"], "cites": [[k, d.hex()] for k, d in sorted(b["cited"])]} for b in breaks], "lines": [{"split": y["split"], "class": CLASS_NAMES[y["class"]], "breaks": y["breaks"], "break_ms": y["break_ms"], "slots": y["slots"], "unslotted": y["unslotted"], "ppm": y["ppm"], "cites": [[k, d.hex()] for k, d in y["cited_records"]]} for y in lines], "totals": {"breaks": rep["totals"][0], "break_ms": rep["totals"][1], "slots": rep["totals"][2]}, } # ---------------------------------------------------------------- §9 the reported statement DATE = re.compile(r"(\d{4})-(\d{2})-(\d{2})") def day_start(s): """§2: the start of a day `YYYY-MM-DD` from 1970 to 9999, in microseconds, or None.""" m = DATE.fullmatch(s) if isinstance(s, str) and s.isascii() else None if m is None: return None y, mo, d = (int(x) for x in m.groups()) leap = (y % 4 == 0 and y % 100 != 0) or y % 400 == 0 days = [31, 29 if leap else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] if not (1970 <= y <= 9999 and 1 <= mo <= 12 and 1 <= d <= days[mo - 1]): return None ordinal = sum(366 if (x % 4 == 0 and x % 100 != 0) or x % 400 == 0 else 365 for x in range(1970, y)) + sum(days[:mo - 1]) + d - 1 return ordinal * DAY_US def an_int(v): return isinstance(v, int) and not isinstance(v, bool) and 0 <= v <= U64 def utf8(s): try: return s.encode("utf-8") except UnicodeEncodeError: return None def from_json(text): try: o = json.loads(text) except ValueError: return None need = {"channel", "start", "end", "currency", "basis", "impressions", "revenue"} if not isinstance(o, dict) or not need <= set(o) or not set(o) <= need | {"days", "programmes"}: return None if not all(isinstance(o[k], str) for k in ("channel", "currency", "basis")) or not all(an_int(o[k]) for k in ("start", "end", "impressions", "revenue")): return None if o["basis"] not in BASES: return None r = {"channel": utf8(o["channel"]), "start": o["start"], "end": o["end"], "currency": utf8(o["currency"]), "basis": BASES[o["basis"]], "impressions": o["impressions"], "revenue": o["revenue"], "days": [], "programmes": []} for name, key in (("days", "day"), ("programmes", "asset")): items = o.get(name, []) if not isinstance(items, list): return None for x in items: if not isinstance(x, dict) or set(x) != {key, "impressions", "revenue"} or not isinstance(x[key], str): return None if not an_int(x["impressions"]) or not an_int(x["revenue"]): return None if name == "days": start = day_start(x["day"]) if start is None: return None r["days"].append({"day": x["day"], "start": start, "impressions": x["impressions"], "revenue": x["revenue"]}) else: r["programmes"].append({"asset": utf8(x["asset"]), "impressions": x["impressions"], "revenue": x["revenue"]}) if r["channel"] is None or r["currency"] is None or any(p["asset"] is None for p in r["programmes"]): return None return r def csv_records(text): """RFC 4180 records, each ended by CRLF or LF, the last line break optional; None when not.""" b, n, p, out = text, len(text), 0, [] if n == 0: return None while True: rec = [] while True: f = [] if p < n and b[p] == '"': p += 1 while True: if p >= n: return None if b[p] == '"': if p + 1 < n and b[p + 1] == '"': f.append('"') p += 2 else: p += 1 break else: f.append(b[p]) p += 1 else: while p < n and b[p] not in ",\r\n": if b[p] == '"': return None f.append(b[p]) p += 1 rec.append("".join(f)) if p < n and b[p] == ",": p += 1 continue break out.append(rec) if p == n: return out if b[p] == "\r" and p + 1 < n and b[p + 1] == "\n": p += 2 elif b[p] == "\n": p += 1 else: return None if p == n: return out def csv_int(s): if not re.fullmatch(r"0|[1-9][0-9]*", s or "", re.ASCII): return None v = int(s) return v if v <= U64 else None def from_csv(text): recs = csv_records(text) if recs is None or any(len(r) != 4 for r in recs) or not recs or recs[0] != ["scope", "key", "impressions", "revenue"]: return None rows = recs[1:] meta = {} for name in ("channel", "start", "end", "currency", "basis"): if not rows or rows[0][0] != name or rows[0][2] != "" or rows[0][3] != "": return None meta[name] = rows[0][1] rows = rows[1:] if not rows or rows[0][0] != "period" or rows[0][1] != "": return None period, rows = rows[0], rows[1:] start, end, imps, rev = csv_int(meta["start"]), csv_int(meta["end"]), csv_int(period[2]), csv_int(period[3]) if None in (start, end, imps, rev) or meta["basis"] not in BASES: return None r = {"channel": meta["channel"].encode(), "start": start, "end": end, "currency": meta["currency"].encode(), "basis": BASES[meta["basis"]], "impressions": imps, "revenue": rev, "days": [], "programmes": []} in_programmes = False for row in rows: i, v = csv_int(row[2]), csv_int(row[3]) if i is None or v is None: return None if row[0] == "day" and not in_programmes: ds = day_start(row[1]) if ds is None: return None r["days"].append({"day": row[1], "start": ds, "impressions": i, "revenue": v}) elif row[0] == "programme": in_programmes = True r["programmes"].append({"asset": row[1].encode(), "impressions": i, "revenue": v}) else: return None return r def read_reported(form, text): """§9: the reported statement of a form, held to §9.3's rules, or None (reported_invalid).""" r = from_json(text) if form == 1 else from_csv(text) if r is None: return None rules = (1 <= len(r["channel"]) <= 1024 and len(r["currency"]) == 3 and all(0x41 <= x <= 0x5A for x in r["currency"]) and r["start"] < r["end"] and all(r["start"] <= d["start"] and d["start"] + DAY_US <= r["end"] for d in r["days"]) and all(a["start"] < b["start"] for a, b in zip(r["days"], r["days"][1:])) and all(1 <= len(p["asset"]) <= 1024 for p in r["programmes"]) and all(a["asset"] < b["asset"] for a, b in zip(r["programmes"], r["programmes"][1:]))) if not rules and "reported-rules" not in SKIP: return None return r # ---------------------------------------------------------------- §10 the check def read_check(b): """§10.6, or Refuse at `check`.""" at = ["check"] bad = RSV.head(b, CHECK_MAGIC) if bad: raise Refuse(bad, at) r = RSV.R(b, 5) try: c = {"issuer": r.take(32), "report": r.take(32), "reported": r.take(32), "form": r.u(1), "statement": r.take(32), "floor": r.u(4), "ceiling": r.u(4), "lines": []} for _ in range(r.u(2)): x = {"scope": r.u(1), "key": r.text()} for f in ("breaks", "break_ms", "slots", "unslotted", "impressions", "revenue", "fill_ppm", "revenue_per_slot"): x[f] = r.u(8) signed = r.u(1) if signed not in (0, 1): raise Refuse("statement_check_invalid", at) x["signed"] = (r.u(8), r.u(8)) if signed else None x["flags"] = r.u(1) c["lines"].append(x) c["sig"] = r.take(64) except ValueError: raise Refuse("truncated", at) if r.p != len(b): raise Refuse("malformed", at) def key_ok(x): if x["scope"] == PERIOD: return x["key"] == b"" try: k = x["key"].decode("utf-8") except UnicodeDecodeError: return False if x["scope"] == DAY: return day_start(k) is not None return x["scope"] == PROGRAMME and 1 <= len(x["key"]) <= 1024 ls = c["lines"] rules = (c["issuer"] != ZERO and c["form"] in (1, 2) and c["floor"] <= WHOLE and c["ceiling"] >= WHOLE and bool(ls) and ls[0]["scope"] == PERIOD and sum(1 for x in ls if x["scope"] == PERIOD) == 1 and RSV.ascending([(x["scope"], x["key"]) for x in ls]) and all(key_ok(x) and (x["signed"] is None or x["scope"] == PERIOD) and x["flags"] < 0x80 for x in ls)) if not rules and "check-rules" not in SKIP: raise Refuse("statement_check_invalid", at) c["body"] = b[:-64] c["digest"] = sha(c["body"]) return c def ratio(a, m, b): return 0 if b == 0 else min(a * m // b, U64) def tally(bs): return (len(bs), min(sum(b["duration_ms"] for b in bs), U64), min(sum(b["slots"] for b in bs if b["slots"] is not None), U64), sum(1 for b in bs if b["slots"] is None)) def check_lines(counted, reported, signed, floor, ceiling): """§10.2 to §10.4.""" def line(scope, key, t, imps, rev, sig): return {"scope": scope, "key": key, "breaks": t[0], "break_ms": t[1], "slots": t[2], "unslotted": t[3], "impressions": imps, "revenue": rev, "fill_ppm": ratio(imps, WHOLE, t[2]), "revenue_per_slot": ratio(rev, 1, t[2]), "signed": sig, "flags": 0} out = [line(PERIOD, b"", tally(counted), reported["impressions"], reported["revenue"], signed)] for d in reported["days"]: out.append(line(DAY, d["day"].encode(), tally([b for b in counted if d["start"] <= b["start"] < d["start"] + DAY_US]), d["impressions"], d["revenue"], None)) for p in reported["programmes"]: out.append(line(PROGRAMME, p["asset"], tally([b for b in counted if b["asset"] is not None and b["asset"].encode() == p["asset"]]), p["impressions"], p["revenue"], None)) rate, period_slots = out[0]["fill_ppm"], out[0]["slots"] for x in out: f = 0 if x["impressions"] > 0 and x["breaks"] == 0 and "flag-unproved" not in SKIP: f |= 1 if x["scope"] != PERIOD and x["slots"] > 0 and period_slots > 0 and "flag-bounds" not in SKIP: if x["fill_ppm"] * WHOLE > rate * ceiling: f |= 2 if x["fill_ppm"] * WHOLE < rate * floor: f |= 4 if x["signed"] is not None and "flag-signed" not in SKIP: si, sr = x["signed"] f |= (8 if x["impressions"] > si else 0) | (16 if x["impressions"] < si else 0) f |= (32 if x["revenue"] > sr else 0) | (64 if x["revenue"] < sr else 0) x["flags"] = f return out CHECK_LINE_FIELDS = ("scope", "key", "breaks", "break_ms", "slots", "unslotted", "impressions", "revenue", "fill_ppm", "revenue_per_slot", "signed", "flags") def verify_check(doc): """§11.2.""" try: if not isinstance(doc, dict) or set(doc) != {"report", "reported", "statement", "check"}: raise Refuse("malformed", []) rp = doc["reported"] if not isinstance(rp, dict) or set(rp) != {"form", "text"} or rp["form"] not in FORMS or not isinstance(rp["text"], str): raise Refuse("malformed", []) cb = lowhex(doc["check"]) if cb is None or not (doc["statement"] is None or isinstance(doc["statement"], dict)): raise Refuse("malformed", []) got, held = verify_report(doc["report"]) # step 1 if got["outcome"] != "accept": raise Refuse(got["reason"], ["report"] + got["at"]) c, rep = held["contract"], held["report"] form = FORMS[rp["form"]] reported = read_reported(form, rp["text"]) # step 2 if reported is None: raise Refuse("reported_invalid", ["reported"]) if "reported-foreign" not in SKIP and (reported["channel"] != c["channel"] or reported["currency"] != c["currency"] or (reported["start"], reported["end"]) != (rep["start"], rep["end"])): raise Refuse("reported_foreign", ["reported"]) statement = None if doc["statement"] is not None: # step 3 sg = memo(_STATEMENTS, doc["statement"], lambda: RSV.verify(doc["statement"])) if sg["outcome"] != "accept": raise Refuse(sg["reason"], ["statement"] + sg["at"]) statement = RSV.read_statement(bytes.fromhex(doc["statement"]["statement"])) if "statement-foreign" not in SKIP and (statement["contract"] != rep["contract"] or statement["party"] != rep["party"] or (statement["start"], statement["end"]) != (rep["start"], rep["end"])): raise Refuse("statement_foreign", ["statement"]) ch = read_check(cb) # step 4 if "check-signature" not in SKIP: bad = ed_verify(ch["issuer"], CHECK_DOMAIN + ch["body"], ch["sig"]) if bad: raise Refuse(bad, ["check"]) signed = None if statement is not None: imps = min(sum(x["impressions"] for x in statement["lines"]), U64) rev = min(sum(x["revenue"] for x in statement["lines"]), U64) if reported["basis"] == 1 else statement["total"] signed = (imps, rev) lines = check_lines(held["counted"], reported, signed, ch["floor"], ch["ceiling"]) same = (ch["report"] == rep["digest"] and ch["reported"] == sha(rp["text"].encode()) and ch["form"] == form and ch["statement"] == (ZERO if statement is None else statement["digest"]) and [{k: x[k] for k in CHECK_LINE_FIELDS} for x in ch["lines"]] == [{k: y[k] for k in CHECK_LINE_FIELDS} for y in lines]) if not same and "check-mismatch" not in SKIP: raise Refuse("statement_check_mismatch", ["check"]) except Refuse as e: return {"outcome": "refuse", "reason": e.reason, "at": e.at} return { "outcome": "accept", "check": ch["digest"].hex(), "issuer": ch["issuer"].hex(), "report": ch["report"].hex(), "party": rep["party"].hex(), "start": rep["start"], "end": rep["end"], "reported": ch["reported"].hex(), "form": FORM_NAMES[ch["form"]], "basis": BASIS_NAMES[reported["basis"]], "statement": None if statement is None else statement["digest"].hex(), "floor_ppm": ch["floor"], "ceiling_ppm": ch["ceiling"], "lines": [{"scope": SCOPE_NAMES[x["scope"]], "key": x["key"].decode("utf-8"), "breaks": x["breaks"], "break_ms": x["break_ms"], "slots": x["slots"], "unslotted": x["unslotted"], "impressions": x["impressions"], "revenue": x["revenue"], "fill_ppm": x["fill_ppm"], "revenue_per_slot": x["revenue_per_slot"], "signed": None if x["signed"] is None else {"impressions": x["signed"][0], "revenue": x["signed"][1]}, "flags": [n for i, n in enumerate(FLAGS) if x["flags"] & (1 << i)]} for x in ch["lines"]], } def verify(doc): """A report's or a check's document: a check's holds `reported`.""" if isinstance(doc, dict) and "reported" in doc: return verify_check(doc) return verify_report(doc)[0] # ---------------------------------------------------------------- the corpus def document(doc, v): """A vector as its verification document, its parts resolved by name.""" def report_doc(p): return {"contract": doc["contracts"][p["contract"]], "contracts": [doc["contracts"][n] for n in p["contracts"]], "report": p["report"], "runs": None if p["runs"] is None else [doc["runs"][n] for n in p["runs"]], "rooms": [doc["rooms"][n] for n in p["rooms"]]} if v["kind"] == "report": return report_doc(v["report"]) s = v["statement"] sd = None if s is None else {"contract": doc["contracts"][s["contract"]], "contracts": [], "statement": s["statement"], "runs": [doc["runs"][n] for n in s["runs"]], "ledgers": [doc["ledgers"][n] for n in s["ledgers"]]} return {"report": report_doc(v["report"]), "reported": doc["reported"][v["reported"]], "statement": sd, "check": v["check"]} def check(doc, quiet=False): """The failures: every vector whose stated outcome is not the one this verifier reaches.""" fails = [] for v in doc.get("vectors", []): try: got = verify(document(doc, v)) except Exception as e: # a crash on a mutated copy is a failure to verify it got = {"outcome": "crash", "error": "%s: %s" % (type(e).__name__, e)} ok = got == v.get("outcome") if not quiet: what = got["outcome"] + (" %s at %s" % (got["reason"], got["at"]) if got["outcome"] == "refuse" else "") print(("PASS" if ok else "FAIL") + " %s: %s" % (v.get("name"), what)) if not ok: fails.append(v.get("name")) return fails def reasons_listed(): """§13's table, in order.""" text = open(EXTENSION, encoding="utf-8").read() at = text.index("\n## 13. Reasons") end = text.find("\n## ", at + 1) return [m.group(1) for m in re.finditer(r"^\| `([a-z_]+)` \|", text[at:end], re.M)] def flip_hex(h, at): b = bytearray(bytes.fromhex(h)) b[at % len(b)] ^= 0x01 return b.hex() def mutations(doc): """Copies of the corpus, one defect each, that a verifier must reject.""" out = [] names = [v["name"] for v in doc["vectors"]] def mutate(label, f): m = copy.deepcopy(doc) f(m) out.append((label, m)) for k, v in enumerate(doc["vectors"]): if v["outcome"]["outcome"] != "accept": continue if v["kind"] == "report": for at in (6, 40, 100, 140, -90, -70, -1): mutate("%s: report byte %d flipped" % (v["name"], at), lambda m, k=k, at=at: m["vectors"][k]["report"].update( report=flip_hex(m["vectors"][k]["report"]["report"], at))) mutate("%s: its stated totals raised" % v["name"], lambda m, k=k: m["vectors"][k]["outcome"]["totals"].update( breaks=m["vectors"][k]["outcome"]["totals"]["breaks"] + 1)) if v["outcome"]["breaks"]: mutate("%s: its first break moved a second" % v["name"], lambda m, k=k: m["vectors"][k]["outcome"]["breaks"][0].update( start=m["vectors"][k]["outcome"]["breaks"][0]["start"] + 1_000_000)) mutate("%s: its first break's slots changed" % v["name"], lambda m, k=k: m["vectors"][k]["outcome"]["breaks"][0].update( slots=7)) if v["report"]["rooms"]: mutate("%s: its last room dropped" % v["name"], lambda m, k=k: m["vectors"][k]["report"]["rooms"].pop()) else: for at in (40, 104, 150, -80, -66, -1): mutate("%s: check byte %d flipped" % (v["name"], at), lambda m, k=k, at=at: m["vectors"][k].update( check=flip_hex(m["vectors"][k]["check"], at))) mutate("%s: a line's flags changed in its outcome" % v["name"], lambda m, k=k: m["vectors"][k]["outcome"]["lines"][-1].update( flags=["above"])) if v["statement"] is not None: mutate("%s: its signed statement left out" % v["name"], lambda m, k=k: m["vectors"][k].update(statement=None)) for name, at in (("main", -1), ("main", 40), ("two_rooms", 60)): mutate("the contract %s: byte %d flipped" % (name, at), lambda m, name=name, at=at: m["contracts"].update( {name: flip_hex(doc["contracts"][name], at)})) mutate("the run of the first hour: its signature flipped", lambda m: m["runs"]["hour_1"].update(sig=flip_hex(doc["runs"]["hour_1"]["sig"], 3))) mutate("the run of the second hour: its first programme renamed", lambda m: m["runs"]["hour_2"]["schedule"][0].update(asset_id="a/ep-1")) mutate("the main room: an event of its producer's flipped", lambda m: m["rooms"]["main"]["chains"][1].__setitem__(5, flip_hex(doc["rooms"]["main"]["chains"][1][5], 130))) mutate("the main room: its policy flipped", lambda m: m["rooms"]["main"].update(policy=flip_hex(doc["rooms"]["main"]["policy"], 70))) mutate("owner B's reported statement: one impression more", lambda m: m["reported"]["b_programmes"].update(text=doc["reported"]["b_programmes"]["text"].replace('"impressions":14', '"impressions":15'))) mutate("owner A's CSV: its line breaks made LF", lambda m: m["reported"]["a_days"].update(text=doc["reported"]["a_days"]["text"].replace("\r\n", "\n"))) for k, v in enumerate(doc["vectors"]): if v["outcome"]["outcome"] == "refuse": mutate("%s: its stated reason changed" % v["name"], lambda m, k=k, v=v: m["vectors"][k]["outcome"].update( reason="malformed" if v["outcome"]["reason"] != "malformed" else "truncated")) break assert len(names) == len(set(names)), "vector names are unique" return out CHECKS = ["contract-overlap", "report-rules", "binding", "contract-period", "report-signature", "run-verify", "run-foreign", "run-overlap", "room-foreign", "room-repeated", "period", "section-crc", "section-read", "section-agree", "break-overlap", "window", "unmapped", "slots-section", "dedupe", "cued-test", "unsourced", "citations", "mismatch", "reported-rules", "reported-foreign", "statement-foreign", "check-rules", "check-signature", "check-mismatch", "flag-unproved", "flag-bounds", "flag-signed"] def self_tests(): """The standard's sample (SCTE 35-1 §14.1, as cue-injection §5.7 holds it) reads as §5 reads it.""" sample = bytes.fromhex("fc3034000000000000fffff00506fe72bd0050001e021c435545494800008e7fcf0001a599b00808000000002ca0a18a3402009ac9d17e") st = read_section(sample) fails = [] if (st["command"], st["id"], st["duration"], st["slots"]) != (0x06, 0x4800008E, 307 * 90_000, None): fails.append("the standard's sample does not state its opportunity") damaged = bytearray(sample) damaged[-1] ^= 1 try: read_section(bytes(damaged)) fails.append("a section with a damaged CRC reads") except Bad: pass print("%s the standard's SCTE 35 sample reads, and a damaged copy does not" % ("PASS" if not fails else "FAIL")) return fails def main(corpus_dir): doc = json.load(open(os.path.join(corpus_dir, "vectors.json"), encoding="utf-8")) fails = self_tests() fails += check(doc) files = sorted(os.listdir(corpus_dir)) if files != ["README.md", "vectors.json"]: fails.append("the corpus holds other files: %s" % files) seen = {v["outcome"].get("reason") for v in doc["vectors"]} listed = reasons_listed() missing = [r for r in listed + REUSED if r not in seen] if missing: fails.append("no vector is refused %s" % missing) print("%s every reason of §13 and every reused code refuses a vector (%d listed)" % ("PASS" if not missing else "FAIL", len(listed))) # And no refusal is unaccounted for: each is §13's, a code §13 reuses, or a code a text it # delegates to (revenue-share, live-ads) raises there. delegated = set(RSV.reasons_listed()) | set(RSV.REUSED) | set(LAV.REASONS) unexplained = sorted(r for r in seen if r is not None and r not in listed and r not in REUSED and r not in delegated) if unexplained: fails.append("refusals §13 does not account for: %s" % unexplained) print("%s every refusal of the corpus is a reason of §13, a code it reuses, or one a text it delegates to raises" % ( "PASS" if not unexplained else "FAIL")) muts = mutations(doc) weak = [name for name, m in muts if not check(m, quiet=True)] print("%s every mutated copy of the corpus fails (%d copies)" % ("PASS" if not weak else "FAIL", len(muts))) fails += ["the mutated copy %r verifies" % w for w in weak] undetected = [] for c in CHECKS: SKIP.add(c) try: if not check(doc, quiet=True): undetected.append(c) finally: SKIP.discard(c) print("%s the corpus catches the verifier with each of its checks dropped (%d checks)" % ("PASS" if not undetected else "FAIL", len(CHECKS))) fails += ["dropping the check %r goes unnoticed" % c for c in undetected] for f in fails: print("FAIL", f) return 1 if fails else 0 def check_document(path): doc = json.load(open(path, encoding="utf-8")) got = verify(doc) print(json.dumps(got, indent=1)) return 0 if got["outcome"] == "accept" else 1 if __name__ == "__main__": if len(sys.argv) == 3 and sys.argv[1] == "--document": sys.exit(check_document(sys.argv[2])) if len(sys.argv) != 2: print("usage: opportunity_report_verify.py | --document FILE", file=sys.stderr) sys.exit(2) sys.exit(main(sys.argv[1]))