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
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]>
691 lines
28 KiB
Python
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))
|