open-standards/wai/pack-conformance/policy_diff.py
Transaction Science e864482332 wai-pack: a policy is read from its bytes: UTF-8, no byte-order mark, nesting capped at 32
Fix round, item 2. Policy::parse_bytes reads a policy as bytes; the CLI and
the corpus use it (no lossy decode). Before the value kinds, and before
anything else, these are policy-toml in both implementations:

- bytes that are not UTF-8;
- a byte-order mark (the house rule: UTF-8 with no byte-order mark);
- not TOML 1.0, an integer outside TOML's 64-bit range among it
  (pack_verify.py's reader takes any size, so it now checks the range);
- arrays and tables nested past 32, the document's table at depth 1: a
  pass of its own before the value-kind walk, so a nesting fault outranks
  a float found earlier in key order, as the Rust parser's own nesting
  limit already made it. pack_verify.py also reads a RecursionError from
  its TOML reader as policy-toml.

packaging §3.1 and §3.2 state it. Corpus: a byte-order mark, bytes that are
not UTF-8, an integer at 2^63, nesting to depth 33 and 80. policy_diff.py
(from the review's byte-level cases and seeded policies) compares every
verdict and digest of wai-pack policy-verdict with pack_verify.py's; CI
runs it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 09:09:57 -04:00

234 lines
10 KiB
Python

#!/usr/bin/env python3
"""The policy reader, differentially: wai-pack against pack_verify.py.
Two sets of policies, each file's verdict (`ok`, or the reason a packager refuses it)
and, when the policy itself parses, its digest, from `wai-pack policy-verdict`, must
equal pack_verify.py's:
- byte-level cases: a byte-order mark, bytes that are not UTF-8, integers at and past
2^53 and 2^63, nesting at and past the cap in arrays, inline tables and dotted keys,
control characters, line endings, NUL, escapes, ids at the byte limit, one id named
twice across kinds;
- seeded random policies with random faults in every table (from a fixed seed).
python3 policy_diff.py <wai-pack binary> [seed] [count]
Exit status 0 only if every verdict and digest agrees. Python 3.11 or later (tomllib).
"""
import os
import random
import subprocess
import sys
import tempfile
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import pack_verify as PV # noqa: E402
MIN = '''schema = "wai.pack.policy/1"
asset_id = "a"
lead = "v"
[[component]]
id = "v"
media = "video"
timebase = { num = 25, den = 1 }
[[ladder]]
id = "b0"
component = "v"
capability = "wai.video.int_motion"
role = "base"
'''
def byte_cases():
c = {}
def case(name, b):
c[name] = b.encode() if isinstance(b, str) else b
case("min", MIN)
case("bom", b"\xef\xbb\xbf" + MIN.encode())
case("int-2^53", MIN + "zzz = 9007199254740992\n")
case("int-2^63", MIN + "zzz = 9223372036854775808\n")
case("int-2^64", MIN + "zzz = 18446744073709551616\n")
case("int-i64max", MIN + "zzz = 9223372036854775807\n")
case("int-i64min", MIN + "zzz = -9223372036854775808\n")
case("hex-int", MIN + "[segmentation]\ntarget_ms = 0x7d0\n")
case("nan", MIN + "zzz = nan\n")
case("neg-zero", MIN + "[segmentation]\ncut_window = -0\n")
case("plus", MIN + "[segmentation]\ncut_window = +8\n")
case("non-utf8", MIN.encode() + b"# \xff\xfe comment\n")
case("non-utf8-str", MIN.replace('asset_id = "a"', 'asset_id = "a\xff"').encode("latin-1"))
case("surrogate-utf8", MIN.encode() + b"# \xed\xa0\x80\n")
for d in (31, 32, 33, 60, 79, 80, 200, 3000):
case(f"deep-array-{d}", "zzz = " + "[" * d + "]" * d + "\n" + MIN)
for d in (31, 32, 200):
case(f"deep-inline-{d}", "zzz = " + "{a=" * d + "1" + "}" * d + "\n" + MIN)
for d in (31, 32, 2000):
case(f"deep-dotted-{d}", "zzz." + ".".join(["a"] * d) + " = 1\n" + MIN)
case("deep-after-float", "aaa = 1.5\nzzz = " + "[" * 40 + "]" * 40 + "\n" + MIN)
case("dup-key", MIN + "asset_id = \"b\"\n")
case("ctrl-del", MIN.replace('asset_id = "a"', 'asset_id = "a\\u007f\\u001f\\b\\f"'))
case("u2028", MIN.replace('asset_id = "a"', 'asset_id = "a\\u2028\\u2029"'))
case("astral", MIN.replace('asset_id = "a"', 'asset_id = "\\U0001F600"'))
case("id-255-bytes", MIN.replace('asset_id = "a"', 'asset_id = "' + "é" * 127 + 'a"'))
case("id-256-bytes", MIN.replace('asset_id = "a"', 'asset_id = "' + "é" * 128 + '"'))
case("crlf", MIN.replace("\n", "\r\n"))
case("lone-cr", MIN.replace("\n", "\r"))
case("same-id-across-kinds", MIN.replace('id = "b0"', 'id = "v"'))
case("tab-in-literal", MIN.replace('asset_id = "a"', "asset_id = 'a\tb'"))
case("float-exp", MIN + "zzz = 1e3\n")
case("local-time", MIN + "zzz = 07:32:00\n")
case("int-refine-base", MIN.replace("wai.video.int_motion", "wai.video.int_refine"))
case("empty", b"")
case("nul", MIN.encode() + b"\x00")
case("u0000-escape", MIN.replace('asset_id = "a"', 'asset_id = "a\\u0000"'))
case("key-unicode", MIN + '"é" = 1\n')
case("ladder-empty", MIN.split("[[ladder]]")[0] + "ladder = []\n")
case("mixed-array", MIN + 'zzz = [1, "a"]\n')
case("params-wrong-type", MIN + "params = 5\n")
case("cut-window-2", MIN + "[segmentation]\ncut_window = 2\n")
return c
CAPS = ["wai.video.int_motion", "wai.video.int_refine", "wai.video.av1", "wai.audio.flac", "wai.image.png",
"wai.audio.int_refine", "wai.audio.int_layered", "wai.splat.sog", "wai.video.avc", "wai.video.package",
"wai.meta.hdr10plus", "wai.nope"]
PARAMS = ['{}', '{ block = 8 }', '{ block = 5 }', '{ search = 33 }', '{ q = 6 }', '{ rate = 8000 }',
'{ q_candidates = [] }', '{ q_candidates = [16, 12] }', '{ source = "encode" }', '{ bogus = 1 }',
'{ to_rate = 48000, q = 64 }']
KINDS = ['"static"', '"bogus"', '']
def random_policy(rng):
def biased(seq):
seq = list(seq)
return seq[0] if (rng.random() < 0.85 and seq) else rng.choice(seq)
def val(kind):
if rng.random() < 0.01:
return biased(['1.5', '-3', '9007199254740992', '"x"', 'true', '[]', '{}'])
return kind
q = '"'
L = []
if rng.random() > 0.03:
L.append("schema = " + val(q + "wai.pack.policy/1" + q))
if rng.random() > 0.03:
L.append("asset_id = " + val(q + biased(["a", "", "x" * 256]) + q))
if rng.random() < 0.5:
L.append("royalty_free_only = " + val(biased(["true", "false"])))
comps = [rng.choice(["v", "a", "v2", "v"]) for _ in range(biased([1, 2, 0, 3]))]
lead = rng.choice(comps if rng.random() < 0.95 else comps + ["zz"]) if comps else "v"
L.append(f'lead = "{lead}"')
if rng.random() < 0.05:
L.append("zz_unknown = 1")
body = []
for c in comps:
body += ["[[component]]", f'id = "{c}"', "media = " + val(q + biased(["video", "audio", "bogus"]) + q)]
if rng.random() > 0.05:
num, den = val(str(biased([25, 0, 48000]))), val(str(biased([1, 1001, 0])))
body.append("timebase = { num = " + num + ", den = " + den + " }")
if rng.random() < 0.6:
body.append("[segmentation]")
for k, choices in [("target_ms", [2000, 500, 0]), ("min_ms", [1000, 3000, 0]), ("max_ms", [4000, 100]),
("cut_abs_permille", [300, 0, 1001]), ("cut_window", [8, 0, 2, 3, 65]),
("lookahead_frames", [6, 65]), ("closed", ["true", "false"])]:
if rng.random() < 0.4:
body.append(f"{k} = " + val(str(biased(choices))))
rungs = []
for i in range(biased([2, 1, 3, 4, 0])):
rid = biased([f"r{i}", "r0", "b", "v"])
role = rng.choice(["base", "refinement", "base", "floor", "bogus"])
rungs.append(rid)
comp = rng.choice(comps if rng.random() < 0.95 else comps + ["zz"]) if comps else "v"
cap = rng.choice(CAPS[:5] if rng.random() < 0.8 else CAPS)
body += ["[[ladder]]", f'id = "{rid}"', f'component = "{comp}"', f'capability = "{cap}"', f'role = "{role}"']
if role == "refinement" or rng.random() < 0.1:
body.append(f'refines = "{rng.choice(rungs + ["nope"])}"')
if rng.random() < 0.5:
body.append("params = " + biased(PARAMS))
if rng.random() < 0.3:
body += ["[live]", "layer_lag_ms = " + biased(["[0]", "[0, 200]", "[0, 200, 400]", "[1]", "[0, 100, 50]"])]
for i in range(biased([0, 0, 1, 2])):
body += ["[[class]]", f'id = "{biased(["c" + str(i), "c0"])}"']
if rng.random() < 0.5:
body.append(f"max_layer = {biased([0, 1, 2, 3])}")
if rng.random() < 0.3:
body.append(f"max_kbps = {biased([1, 0, 900])}")
for i in range(biased([0, 0, 1, 2])):
body += ["[[moment]]", f'id = "m{i}"', f"from_ms = {biased([0, 100])}", f"to_ms = {biased([0, 100, 200])}"]
if rng.random() < 0.5:
body.append('tags = ["event"]')
for i in range(biased([0, 0, 1, 2, 5])):
body += ["[[rule]]", f'id = "{biased(["u" + str(i), "u0"])}"']
if rng.random() < 0.5:
body.append("when = { piece_kind = [" + biased(KINDS) + "] }")
t = []
if rng.random() < 0.4:
pick = rng.sample(rungs + ["nope"], k=min(2, len(rungs) + 1))
t.append("ladder = [" + ", ".join(q + x + q for x in pick) + "]")
if rng.random() < 0.4:
t.append(f"max_layer = {biased([0, 1, 2])}")
if rng.random() < 0.4:
t.append(f"deadline_ms = {biased([1, 100, 200, 300, 400, 800])}")
if t:
body.append("then = { " + ", ".join(t) + " }")
if rng.random() < 0.3:
body += ["[defaults]", f"deadline_ms = {biased([1500, 1, 0, 150])}"]
if rng.random() < 0.2:
body += ["[classify]", f"synthetic_bins = {biased([12, 65])}"]
return ("\n".join(L + body) + "\n").encode()
def python_verdict(b):
try:
P = PV.parse_policy_bytes(b)
except PV.Refused as e:
return e.code, "-"
try:
PV.admit(b)
v = "ok"
except PV.Refused as e:
v = e.code
return v, PV.digest(P)
def main(argv):
if len(argv) < 2:
print(__doc__)
return 2
binary = argv[1]
seed = int(argv[2]) if len(argv) > 2 else 1
count = int(argv[3]) if len(argv) > 3 else 3000
rng = random.Random(seed)
cases = list(byte_cases().items()) + [(f"random-{i}", random_policy(rng)) for i in range(count)]
bad = 0
with tempfile.TemporaryDirectory() as d:
paths = {}
for name, b in cases:
p = os.path.join(d, name + ".toml")
with open(p, "wb") as f:
f.write(b)
paths[p] = (name, b)
rust = {}
files = list(paths)
for k in range(0, len(files), 500):
r = subprocess.run([binary, "policy-verdict"] + files[k:k + 500], capture_output=True)
if r.returncode != 0:
print(r.stderr.decode(errors="replace")[-2000:])
return 1
for line in r.stdout.decode().splitlines():
p, v, dg = line.split("\t")
rust[p] = (v, dg)
for p, (name, b) in paths.items():
py = python_verdict(b)
if rust.get(p) != py:
bad += 1
if bad <= 10:
print(f"DIFF {name}: wai-pack {rust.get(p)}, pack_verify.py {py}")
print(f"policy_diff: seed {seed}, {len(cases)} policies, {bad} differences")
return 0 if bad == 0 else 1
if __name__ == "__main__":
sys.exit(main(sys.argv))