open-standards/wai/live-conformance/audience_decision_verify.py
Transaction Science 41510d35ae
Some checks failed
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 12s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 22s
wai-audit collector and player integration / does this change touch it (push) Successful in 19s
wai-fleet service / does this change touch it (push) Successful in 21s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 23s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 33s
wai-sink-swift native sink package / does this change touch it (push) Successful in 23s
WAI Studio / does this change touch it (push) Successful in 18s
wai-sink-android native sink library / does this change touch it (push) Successful in 47s
wai-quantum for JavaScript / does this change touch it (push) Successful in 1m0s
wai-app and Create / wai-app · does this change touch it (push) Successful in 1m2s
wai-key-proxy / does this change touch it (push) Successful in 1m2s
WAI operation catalogue / does this change touch it (push) Successful in 1m3s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 1m9s
wai-ledger service / does this change touch it (push) Successful in 1m2s
wai-quantum studio and desktop / does this change touch it (push) Successful in 1m2s
wai-fleet service / service · macos (push) Successful in 55s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 51s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 1m25s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 1m35s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 1m48s
wai-key-proxy / proxy · linux (push) Successful in 1m41s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 2m0s
wai-ledger service / service · macos (push) Successful in 56s
wai-fleet service / service · linux (push) Successful in 2m32s
wai-ledger service / service · linux (push) Successful in 2m13s
wai-audit collector and player integration / collector · linux (push) Successful in 2m57s
wai-quantum for JavaScript / javascript · linux (push) Successful in 4m8s
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
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Has been cancelled
wai-app and Create / wai-app · Create, browser end-to-end (macos) (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 studio and desktop / studio · browser (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 breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 5m13s
wai-quantum studio and desktop / desktop app · 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-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Has been cancelled
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Has been cancelled
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Has been cancelled
wai-quantum studio and desktop / desktop app · windows (push) Has been cancelled
WAI Studio / Studio in Chromium and Firefox · macos (push) Has been cancelled
WAI operation catalogue / the catalogue and every door · linux (push) Has been cancelled
WAI Studio / Studio in WebKit · linux (push) Has been cancelled
wai-quantum studio and desktop / desktop app · linux (push) Has been cancelled
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Has been cancelled
wai: audience decisions and territories — blackouts decided from a location aggregated in the session, and where a publisher may be shown
extensions/audience-decision.md registers two route-record documents, each signed in its
canonical form under its own domain.

`wai.video.audience` (`wai-audience-decision`): a decider's signed decision of whether the
sessions in one region may be shown a programme over a span, under the blackouts in force. Each
blackout is named by the id a session class carries (http-delivery Appendix H `blackouts`) and
lists its regions in rung-price §2's region codes, with the programme shown in its place. A
session aggregates its own location before it leaves the session: to the one listed code that
holds it, to outside, or to unresolved (no location, or a country of which only subdivisions are
listed); the aggregation decides exactly what the location would. A decision whose evidence holds
anything else (coordinates, a postal code, a session's identifier) or a region finer than the
aggregation gives is refused `audience_location_not_aggregated`; one whose region the blackouts
neither list nor hold, `audience_region_not_in_set`. The blackouts applied (every one, unresolved),
the verdict (show, withhold, or alternate when all that apply name one programme) and that
programme are recomputed; a session class of the programme carries the applied set as its
`blackouts` and the region's country as its `geo`, and a verifier given both states the class's
hash.

`wai.delivery.territory` (`wai-territory`): a publisher's signed statement of where its content may
be shown (everywhere, only in some regions, everywhere but some). Its latest statement stands
alone; a lapsed or contradicted latest revives none. The territory (within, outside,
undetermined, or unstated with its reason) is reported beside a server key's placement, which
cache-authority §4.4 computes from its own inputs and no statement changes; a statement that names
servers, nodes, origins, roles or may_sign is refused `territory_scopes_serving`. cache-authority
§4.3 and §4.4 are unchanged; §4.4 gains a paragraph saying so.

The reference is the `audience_decision` module of wai-rs (feature `audience_decision`); the
corpus is live-conformance/audience/ (13 aggregations, 67 decisions, 17 classes, 39 statements,
32 territories, 10 placements), and audience_decision_verify.py is its second implementation:
178 cases agree, 21 of 21 mutated copies fail and 34 of 34 weakened verifiers are caught.
Twelve faults planted in the reference each fail its tests: the evidence's extra-member check and
its finer-region check, rule 6 (a region not the blackouts'), the serving-member check and
`may_sign` among its members, a placement refused beside an outside territory, a decision's and a
statement's signature verified without the domain, over the text as written and under the other
record's domain, an unresolved session read as outside, and a lapsed statement letting an earlier
one stand.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 19:55:54 -05:00

691 lines
28 KiB
Python

#!/usr/bin/env python3
"""Independent verifier of the audience decision corpus (extensions/audience-decision.md).
Written from the extension's text. It shares no code with wai-rs, which generates the corpus, and
never runs it. It takes from the second implementations of the texts this extension builds on what
they define: documents, their canonical form, digest and tokens from route-record's
(`route_record_verify.py`, route-record §2); region codes, their country and *holds* from rung
price's (`rung_price_verify.py`, rung-price §2); the placement of a server key from cache
authority's (`cache_authority_verify.py`, cache-authority §4.4); Ed25519 verification as SPEC §7
*Signatures* states it from the routine every Python verifier of WAI's corpora shares
(`../claim-conformance/_ed25519.py`); and BLAKE3 from `../claim-conformance/_blake3.py`.
Everything this extension adds is written here:
- §4.3: a session's location aggregated against the listed codes of blackouts;
- §4.4 to §4.6: a decision's members and rules, its evidence, what follows from its blackouts and
region, its digest and its signature;
- §4.7: a decision beside a session class, and the class's hash (http-delivery Appendix H);
- §5.2 and §5.3: a territory statement's members and rules, the members that would name who
serves, its digest and its signature;
- §5.4 and §5.5: the statement that stands at an instant, and a region against it;
- §5.6: the territory beside a placement.
For every case it requires the outcome the corpus states, member for member; that every reason of
§7's table appears in the corpus and that the corpus states no other; that a decision aggregated
from a location applies exactly the blackouts that hold the location; that no placement changes
when its statements are removed; and that no outcome depends on the order of a document's members,
nor on the order of the statements given or on a statement given twice. It checks what no vector
carries: the bound of a text. 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 -I audience_decision_verify.py <live-conformance/audience>
Exit status 0 only if every case verifies, every mutated copy fails and every weakened verifier is
caught. Python 3.8 or later; the `cryptography` package, which the cache authority verifier's
readers import.
"""
import copy
import hashlib
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
for d in ("claim-conformance", "jwp-conformance", "signer-conformance"):
sys.path.insert(0, os.path.join(HERE, "..", d))
import _ed25519 as ED # noqa: E402
import _blake3 as B3 # noqa: E402
import route_record_verify as RR # noqa: E402
import rung_price_verify as RP # noqa: E402
import cache_authority_verify as CAV # noqa: E402
EXTENSION = os.path.join(HERE, "..", "extensions", "audience-decision.md")
DECISION_KIND = "wai-audience-decision" # §4.1
TERRITORY_KIND = "wai-territory" # §5.1
DECISION_DOMAIN = b"wai:audience-decision\x01" # §4.4
TERRITORY_DOMAIN = b"wai:territory\x01" # §5.2
CLASS_DOMAIN = b"wai:video-manifest-class\x01" # http-delivery Appendix H
MAX = (1 << 53) - 1
MAX_TEXT = 1 << 20 # §4.5, §5.3: 1,048,576 bytes
MAX_BLACKOUTS = 256 # §4.1
MAX_REGIONS = 8192 # §4.2, §5.1
DECISION = ["kind", "version", "decider", "programme", "start", "end", "blackouts", "evidence", "applied", "verdict",
"alternate", "sig"] # §4.1
BLACKOUT = ["id", "regions", "alternate"] # §4.2
EVIDENCE = ["region", "resolved"] # §4.3
CLASS = ["content_id", "entitlement", "geo", "blackouts", "ad_pod_id", "window"] # §4.7 step 2
TERRITORY = ["kind", "version", "publisher", "issued", "expires", "show", "regions", "sig"] # §5.1
SERVING = ["servers", "nodes", "origins", "roles", "may_sign"] # §5.3
VERDICTS = ["show", "withhold", "alternate"] # §4.1
SHOWS = ["everywhere", "only", "except"] # §5.1
MALFORMED = "audience_decision_malformed"
NOT_AGGREGATED = "audience_location_not_aggregated"
NOT_IN_SET = "audience_region_not_in_set"
INCONSISTENT = "audience_decision_inconsistent"
SIGNATURE = "audience_decision_signature_invalid"
CLASS_MALFORMED = "audience_class_malformed"
CLASS_MISMATCH = "audience_class_mismatch"
T_MALFORMED = "territory_malformed"
T_SERVING = "territory_scopes_serving"
T_SIGNATURE = "territory_signature_invalid"
T_NOT_STATED = "territory_not_stated"
T_CONTRADICTED = "territory_contradicted"
T_LAPSED = "territory_lapsed"
# A weakened run drops one of these checks (see `WEAKENINGS`); None checks everything.
DROPPED = None
def drops(name):
return DROPPED == name
def section(heading):
text = open(EXTENSION, encoding="utf-8").read()
at = text.index("\n" + heading)
end = text.find("\n## ", at + 1)
return text[at:end if end >= 0 else len(text)]
def reasons_table():
"""§7's codes, in order."""
return [r.split("`")[1] for r in section("## 7. Reasons").splitlines() if r.startswith("| `")]
class Refused(Exception):
def __init__(self, reason):
super().__init__(reason)
self.reason = reason
def need(ok, reason, check=None):
"""Refuse with `reason` unless `ok`, or unless `check` is the check a weakened run drops."""
if not ok and not (check is not None and drops(check)):
raise Refused(reason)
# ------------------------------------------------------------------ forms
def is_token(v):
return RR.is_token(v)
def is_int(v):
return RR.is_int(v)
def region_code(v):
return RP.region_code(v)
def holds(codes, r):
return RP.holds(codes, r)
def ascending(xs):
return all(xs[i].encode() < xs[i + 1].encode() for i in range(len(xs) - 1))
def regions_ok(v, lo, hi):
"""§4.2 and §5.1: `lo` to `hi` region codes, strictly ascending by bytes, with no subdivision code
whose country is also listed."""
if not (isinstance(v, list) and lo <= len(v) <= hi and all(region_code(x) for x in v)):
return False
if not ascending(v) and not drops("regions_ascending"):
return False
if drops("one_encoding"):
return True
return not any(len(c) > 2 and RP.country(c) in v for c in v)
def tokens_ok(v, hi):
return isinstance(v, list) and len(v) <= hi and all(is_token(x) for x in v) and (ascending(v) or drops("applied_ascending"))
def null_or_token(v):
return v is None or is_token(v)
def read_text(text):
"""A document of at most 1,048,576 bytes, or None."""
if not isinstance(text, str) or len(text.encode("utf-8")) > MAX_TEXT:
return None
return RR.read_document(text)
_VERIFIED = {}
def signed(key, domain, v, sig):
"""§4.4, §5.2: an Ed25519 signature by `key` over `domain` and the canonical form of `v` without
`sig`, verified strictly."""
if drops("signature"):
return True
body = {k: x for k, x in v.items() if k != "sig"}
if drops("signature_as_written"):
form = json.dumps(body, separators=(",", ":"), ensure_ascii=False)
else:
form = RR.canonical(body)
msg = (b"" if drops("signature_domain") else domain) + form.encode("utf-8")
k = (key, hashlib.sha256(msg).digest(), sig)
if k not in _VERIFIED:
try:
_VERIFIED[k] = ED.verify(bytes.fromhex(key), msg, bytes.fromhex(sig)) is None
except ValueError:
_VERIFIED[k] = False
return _VERIFIED[k]
def digest(v):
return RR.digest({k: x for k, x in v.items() if k != "sig"})
# ------------------------------------------------------------------ §4.3 and §4.6
def listed(blackouts):
"""§2 *Listed codes*: every code any blackout lists, distinct, in byte order."""
return sorted({c for b in blackouts for c in b["regions"]}, key=lambda c: c.encode())
def aggregate(blackouts, location):
"""§4.3: the evidence a session's location aggregates to."""
codes = listed(blackouts)
if location is None:
return {"region": None, "resolved": False}
if location in codes:
return {"region": location, "resolved": True}
if len(location) > 2 and RP.country(location) in codes and not drops("aggregate_country"):
return {"region": RP.country(location), "resolved": True}
if len(location) == 2 and any(len(c) > 2 and RP.country(c) == location for c in codes) and not drops("aggregate_unresolved"):
return {"region": None, "resolved": False}
return {"region": None, "resolved": True}
def follows(blackouts, region, resolved):
"""§4.6: (applied, verdict, alternate)."""
if not resolved and not drops("unresolved_applies_all"):
apply = list(blackouts)
elif region is None:
apply = []
else:
apply = [b for b in blackouts if holds(b["regions"], region)]
ids = [b["id"] for b in apply]
if not apply:
return ids, "show", None
alts = {b["alternate"] for b in apply}
if len(alts) == 1 and None not in alts:
return ids, "alternate", apply[0]["alternate"]
if drops("alternates_agree") and apply[0]["alternate"] is not None:
return ids, "alternate", apply[0]["alternate"]
return ids, "withhold", None
# ------------------------------------------------------------------ §4.5
def blackout_ok(b):
return (RR.exactly(b, BLACKOUT) and is_token(b["id"]) and regions_ok(b["regions"], 1, MAX_REGIONS)
and null_or_token(b["alternate"]))
def decision_form(v):
"""Rules 1 to 4."""
if not RR.exactly(v, DECISION):
return False
if v["kind"] != DECISION_KIND or not RR.same(v["version"], 1):
return False
if not (RR.is_hex(v["decider"], 64) and is_token(v["programme"]) and is_int(v["start"]) and is_int(v["end"])
and RR.is_hex(v["sig"], 128)):
return False
if v["start"] >= v["end"] and not drops("start_below_end"):
return False
bs = v["blackouts"]
if not (isinstance(bs, list) and len(bs) <= MAX_BLACKOUTS and all(blackout_ok(b) for b in bs)):
return False
if not ascending([b["id"] for b in bs]) and not drops("blackouts_ascending"):
return False
e = v["evidence"]
if not (isinstance(e, dict) and "region" in e and "resolved" in e):
return False
if not (e["region"] is None or region_code(e["region"])) or not isinstance(e["resolved"], bool):
return False
if e["resolved"] is False and e["region"] is not None and not drops("unresolved_no_region"):
return False
if not tokens_ok(v["applied"], MAX_BLACKOUTS):
return False
if v["verdict"] not in VERDICTS or not null_or_token(v["alternate"]):
return False
if (v["alternate"] is not None) != (v["verdict"] == "alternate") and not drops("alternate_with_verdict"):
return False
return True
def read_decision(text):
"""§4.5: the decision's value, or Refused."""
v = read_text(text)
need(v is not None and decision_form(v), MALFORMED)
e, codes = v["evidence"], listed(v["blackouts"])
# Rule 5.
need(set(e) == set(EVIDENCE), NOT_AGGREGATED, "evidence_members")
r = e["region"]
if r is not None:
need(r in codes or not holds(codes, r), NOT_AGGREGATED, "finer_region")
# Rule 6.
need(r in codes, NOT_IN_SET, "in_set")
# Rule 7.
applied, verdict, alternate = follows(v["blackouts"], r, e["resolved"])
need(RR.same([applied, verdict, alternate], [v["applied"], v["verdict"], v["alternate"]]), INCONSISTENT, "follows")
need(signed(v["decider"], DECISION_DOMAIN, v, v["sig"]), SIGNATURE)
return v
def decision_outcome(text):
try:
v = read_decision(text)
except Refused as r:
return {"outcome": "refuse", "reason": r.reason}
return {"outcome": "read", "digest": digest(v), "verdict": v["verdict"], "applied": v["applied"],
"alternate": v["alternate"]}
# ------------------------------------------------------------------ §4.7
def lp(s):
b = s.encode("utf-8")
return len(b).to_bytes(4, "big") + b
def class_hash(c):
"""http-delivery Appendix H's session_class_hash."""
bs = sorted(set(c["blackouts"]), key=lambda s: s.encode())
data = CLASS_DOMAIN + lp(c["content_id"]) + lp(c["entitlement"]) + lp(c["geo"]) + lp(c["ad_pod_id"])
data += len(bs).to_bytes(4, "big") + b"".join(lp(s) for s in bs) + c["window"].to_bytes(8, "big")
return B3.blake3(data).hex()
def class_outcome(case):
try:
v = read_decision(case["decision"])
except Refused as r:
return {"outcome": "refuse", "reason": r.reason}
c = case["class"]
ok = (RR.exactly(c, CLASS) and all(isinstance(c[m], str) for m in ("content_id", "entitlement", "geo", "ad_pod_id"))
and isinstance(c["blackouts"], list) and all(isinstance(b, str) for b in c["blackouts"]) and is_int(c["window"]))
if not ok and not drops("class_form"):
return {"outcome": "refuse", "reason": CLASS_MALFORMED}
mismatch = lambda m: {"outcome": "refuse", "reason": CLASS_MISMATCH, "member": m} # noqa: E731
if c["content_id"] != v["programme"] and not drops("class_content"):
return mismatch("content_id")
distinct = list(c["blackouts"]) if drops("class_distinct") else sorted(set(c["blackouts"]), key=lambda s: s.encode())
if distinct != v["applied"] and not drops("class_blackouts"):
return mismatch("blackouts")
r, resolved = v["evidence"]["region"], v["evidence"]["resolved"]
if r is not None:
geo_ok = c["geo"] == RP.country(r)
elif resolved:
geo_ok = c["geo"] not in listed(v["blackouts"]) or drops("class_geo_outside")
else:
geo_ok = True
if not geo_ok and not drops("class_geo"):
return mismatch("geo")
return {"outcome": "consistent", "session_class_hash": class_hash(c)}
# ------------------------------------------------------------------ §5.3
def territory_form(v):
"""Rules 1 to 3."""
if not RR.exactly(v, TERRITORY):
return False
if v["kind"] != TERRITORY_KIND or not RR.same(v["version"], 1):
return False
if not (RR.is_hex(v["publisher"], 64) and is_int(v["issued"]) and is_int(v["expires"]) and RR.is_hex(v["sig"], 128)):
return False
if v["issued"] >= v["expires"] and not drops("issued_below_expires"):
return False
if v["show"] not in SHOWS or not regions_ok(v["regions"], 0, MAX_REGIONS):
return False
if (len(v["regions"]) == 0) != (v["show"] == "everywhere") and not drops("everywhere_empty"):
return False
return True
def read_territory(text):
"""§5.3: the statement's value, or Refused."""
v = read_text(text)
need(v is not None, T_MALFORMED)
need(not (isinstance(v, dict) and any(m in v for m in SERVING)), T_SERVING, "serving")
need(territory_form(v), T_MALFORMED)
need(signed(v["publisher"], TERRITORY_DOMAIN, v, v["sig"]), T_SIGNATURE)
return v
def statement_outcome(text):
try:
v = read_territory(text)
except Refused as r:
return {"outcome": "refuse", "reason": r.reason}
return {"outcome": "read", "digest": digest(v), "publisher": v["publisher"], "show": v["show"]}
# ------------------------------------------------------------------ §5.4 and §5.5
def standing(texts, publisher, at_us):
"""§5.4: (the standing statement, None) or (None, the reason none stands)."""
mine = []
for t in texts:
try:
v = read_territory(t)
except Refused:
continue
if (v["publisher"] == publisher or drops("publisher")) and (v["issued"] <= at_us or drops("issued_by_then")):
mine.append(v)
if not mine:
return None, T_NOT_STATED
latest = max(v["issued"] for v in mine)
at = [v for v in mine if v["issued"] == latest]
if len({digest(v) for v in at}) > 1 and not drops("contradicted"):
return None, T_CONTRADICTED
v = at[0]
if v["expires"] <= at_us and not drops("lapsed"):
return None, T_LAPSED
return v, None
def territory_word(v, region):
"""§5.5."""
if v["show"] == "everywhere":
return "within"
if region is None:
return "undetermined"
rs = v["regions"]
country_of_listed = len(region) == 2 and any(len(c) > 2 and RP.country(c) == region for c in rs)
if holds(rs, region):
return "outside" if v["show"] == "except" else "within"
if country_of_listed and not drops("undetermined"):
return "undetermined"
return "within" if v["show"] == "except" or drops("except") else "outside"
def territory_label(texts, publisher, region, at_us):
v, reason = standing(texts, publisher, at_us)
if v is None:
return f"unstated reason={reason}"
return f"{territory_word(v, region)} statement={digest(v)}"
def territory_outcome(case):
return {"territory": territory_label(case["texts"], case["publisher"], case["region"], case["at_us"])}
# ------------------------------------------------------------------ §5.6
def beside(case, texts):
placement = CAV.place(case["key"], case["at_ms"], case["heads"])
label = territory_label(texts, case["publisher"], case["region"], case["at_ms"] * 1000)
if drops("placement_alone") and label.startswith("outside ") and placement.startswith("confirmed "):
placement = "refused reason=outside_territory"
return placement, label
def placement_outcome(case):
placement, label = beside(case, case["texts"])
return {"placement": placement, "territory": label}
def aggregation_outcome(case):
return aggregate(case["blackouts"], case["location"])
RUNS = (("aggregations", aggregation_outcome), ("decisions", lambda c: decision_outcome(c["text"])),
("classes", class_outcome), ("statements", lambda c: statement_outcome(c["text"])),
("territories", territory_outcome), ("placements", placement_outcome))
KINDS = tuple(k for k, _ in RUNS)
# ------------------------------------------------------------------ the corpus
def reordered(text):
"""`text`'s document with its members in reverse order, or None when it is not a document object."""
v = read_text(text)
if not isinstance(v, dict):
return None
return json.dumps(dict(reversed(list(v.items()))), indent=1, ensure_ascii=False)
def invariances(doc, fail):
# A decision aggregated from a location applies exactly the blackouts that hold the location.
for c in doc["aggregations"]:
e = aggregate(c["blackouts"], c["location"])
if e["resolved"] and c["location"] is not None:
direct = [b["id"] for b in c["blackouts"] if holds(b["regions"], c["location"])]
if follows(c["blackouts"], e["region"], True)[0] != direct:
fail(f"aggregations/{c['name']}: the aggregation applies other blackouts than the location does")
# The order of a document's members.
for kind, run in (("decisions", decision_outcome), ("statements", statement_outcome)):
for c in doc[kind]:
t = reordered(c["text"])
if t is not None and len(t.encode()) <= MAX_TEXT and not RR.same(run(t), c["outcome"]):
fail(f"{kind}/{c['name']}: its members in another order give another outcome")
# The order of the statements given, and a statement given twice.
for c in doc["territories"]:
for texts in (list(reversed(c["texts"])), c["texts"] + c["texts"]):
if territory_label(texts, c["publisher"], c["region"], c["at_us"]) != c["outcome"]["territory"]:
fail(f"territories/{c['name']}: the statements in another order, or twice, give another territory")
# §5.6: the placement is the same with no statement at all.
for c in doc["placements"]:
if beside(c, [])[0] != beside(c, c["texts"])[0]:
fail(f"placements/{c['name']}: its placement changes with its statements")
def strings(v, name, out):
if isinstance(v, dict):
for k, x in v.items():
if k == name and isinstance(x, str):
out.append(x)
strings(x, name, out)
elif isinstance(v, list):
for x in v:
strings(x, name, out)
def check(doc, quiet=False, full=True):
fails = []
def fail(msg):
fails.append(msg)
if not quiet:
print("FAIL", msg)
for kind, run in RUNS:
cases = doc.get(kind) or []
if not cases:
fail(f"no {kind} cases")
for c in cases:
try:
got = run(c)
except (KeyError, TypeError, ValueError, AttributeError) as e:
fail(f"{kind}/{c.get('name')}: {e!r}")
continue
if not RR.same(got, c["outcome"]):
fail(f"{kind}/{c['name']}: stated {json.dumps(c['outcome'])} got {json.dumps(got)}")
table = reasons_table()
outcomes = [c["outcome"] for k in KINDS for c in doc.get(k) or []]
reasons, labels = [], []
strings(outcomes, "reason", reasons)
strings(outcomes, "territory", labels)
stated = set(reasons) | {s.split("reason=", 1)[1] for s in labels if "reason=" in s}
for r in table:
if r not in stated:
fail(f"no case gives {r}")
for r in sorted(stated - set(table)):
fail(f"a case gives {r}, which §7 does not list")
for word in ("within", "outside", "undetermined", "unstated"):
if not any(s.startswith(word + " ") for s in labels):
fail(f"no territory is {word}")
if full:
try:
invariances(doc, fail)
except (KeyError, TypeError, ValueError, AttributeError) as e:
fail(f"invariances: {e!r}")
return fails
WEAKENINGS = (
"regions_ascending", "one_encoding", "applied_ascending", "start_below_end", "blackouts_ascending",
"unresolved_no_region", "alternate_with_verdict", "evidence_members", "finer_region", "in_set", "follows",
"unresolved_applies_all", "alternates_agree", "aggregate_country", "aggregate_unresolved", "signature",
"signature_domain", "signature_as_written", "class_form", "class_content", "class_distinct", "class_blackouts",
"class_geo", "class_geo_outside", "issued_below_expires", "everywhere_empty", "serving", "publisher",
"issued_by_then", "contradicted", "lapsed", "undetermined", "except", "placement_alone",
)
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_aggregate_location(d):
case(d, "aggregations", "subdivision_of_a_listed_country")["location"] = "XB-1"
def m_aggregate_outcome(d):
case(d, "aggregations", "country_of_listed_subdivisions")["outcome"]["resolved"] = True
def m_verdict(d):
case(d, "decisions", "withhold_unresolved")["outcome"]["verdict"] = "alternate"
def m_applied(d):
case(d, "decisions", "alternate_in_a_listed_subdivision")["outcome"]["applied"] = ["partner"]
def m_digest(d):
case(d, "decisions", "show_outside")["outcome"]["digest"] = "00" * 32
def m_sig(d):
c = case(d, "decisions", "show_outside")
v = json.loads(c["text"])
v["sig"] = ("01" if v["sig"][:2] != "01" else "02") + v["sig"][2:]
c["text"] = RR.canonical(v)
def m_aggregated_reason(d):
case(d, "decisions", "evidence_coordinates")["outcome"]["reason"] = NOT_IN_SET
def m_in_set_reason(d):
case(d, "decisions", "region_the_blackouts_do_not_hold")["outcome"]["reason"] = INCONSISTENT
def m_class_geo(d):
case(d, "classes", "consistent_alternate")["class"]["geo"] = "XA"
def m_class_hash(d):
case(d, "classes", "consistent_outside")["outcome"]["session_class_hash"] = "00" * 32
def m_class_member(d):
case(d, "classes", "geo_not_the_regions_country")["outcome"]["member"] = "blackouts"
def m_statement_reason(d):
case(d, "statements", "names_servers")["outcome"]["reason"] = T_MALFORMED
def m_statement_show(d):
case(d, "statements", "except")["outcome"]["show"] = "only"
def m_territory_at(d):
case(d, "territories", "at_issued")["at_us"] -= 1
def m_territory_region(d):
case(d, "territories", "only_sibling_subdivision")["region"] = "XB-1"
def m_territory_word(d):
c = case(d, "territories", "except_country_of_a_listed_subdivision")
c["outcome"]["territory"] = c["outcome"]["territory"].replace("undetermined", "within", 1)
def m_placement_key(d):
case(d, "placements", "confirmed_outside")["key"] = case(d, "placements", "revoked_within")["key"]
def m_placement_outcome(d):
case(d, "placements", "confirmed_outside")["outcome"]["placement"] = "refused reason=outside_territory"
def m_reason_gone(d):
d["territories"] = [c for c in d["territories"] if "territory_contradicted" not in c["outcome"]["territory"]]
def m_reason_unknown(d):
case(d, "statements", "names_roles")["outcome"]["reason"] = "territory_roles"
def m_no_kind(d):
d["classes"] = []
return [m_aggregate_location, m_aggregate_outcome, m_verdict, m_applied, m_digest, m_sig, m_aggregated_reason,
m_in_set_reason, m_class_geo, m_class_hash, m_class_member, m_statement_reason, m_statement_show,
m_territory_at, m_territory_region, m_territory_word, m_placement_key, m_placement_outcome, m_reason_gone,
m_reason_unknown, m_no_kind]
def direct(doc):
"""§4.5's and §5.3's bound of 1,048,576 bytes: a text of that many bytes, whitespace added, reads
as itself, and one of a byte more is refused."""
fails = []
for kind, name, run, refusal in (("decisions", "alternate_in_a_listed_subdivision", decision_outcome, MALFORMED),
("statements", "only", statement_outcome, T_MALFORMED)):
c = next(x for x in doc[kind] if x["name"] == name)
pad = MAX_TEXT - len(c["text"].encode("utf-8"))
if not RR.same(run(c["text"] + " " * pad), c["outcome"]):
fails.append(f"{kind}/{name} of 1,048,576 bytes, whitespace added, did not read as itself")
if run(c["text"] + " " * (pad + 1)) != {"outcome": "refuse", "reason": refusal}:
fails.append(f"{kind}/{name} of 1,048,577 bytes was not refused {refusal}")
return fails
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}")
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)")
b = direct(doc)
print(f"what no vector carries: {len(b)} failure(s)")
fails += b
caught = 0
ms = mutations(doc)
for m in ms:
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(ms)} mutated copies fail")
weak = 0
for w in WEAKENINGS:
DROPPED = w
try:
if check(doc, quiet=True, full=False):
weak += 1
else:
fails.append(f"a verifier without its {w} check passes the corpus")
finally:
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))