The new max_ms check bounded nothing, because a policy could set max_ms up to
2^53 − 1. A still's piece could then state 2^32 − 1 frames. Rust hashed once
per stated frame (about 29 minutes), and Python built a list of n references
(about 34 GB).
- **§3.4 item 5 and §4.4.** max_ms is at most 600000, and at most 2^24 units on
every component's timebase. Both are policy-piece-bounds, in both readers.
§4.4 gives the reason: a verifier takes a piece whole, so the longest piece
bounds its work. Ten minutes is far past any piece a delivery cuts, and 2^24
units bounds the work on any timebase, which may have up to 2^32 − 1 units a
second.
- **§11.3, before any frame is decoded.** Each piece's floor object, where its
framing reads, must hold one unit per frame of its piece
(floor-object-mismatch). Both verifiers make this check first.
- **Held stills.** Python holds a still's frame and its count (Held) and hashes
the digest a block at a time. Rust's held_digest also feeds a block at a time.
- **Corpus.**
- refusals: max_ms 600001; and the default max_ms on a timebase of 2^32 − 1
units a second.
- policy/max-piece.toml: accepted at both bounds.
- staged-mutations max-ms-past-cap (policy-piece-bounds).
- staged-mutations still-at-cap: the still given the longest piece the policy
allows, its manifest naming the frames, refused by the floor count in both
verifiers.
- **policy_diff.** Its random policies now reach both bounds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
234 lines
10 KiB
Python
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, 4294967295, 16777216]))), val(str(biased([1, 1001, 0, 600])))
|
|
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, 600000, 600001, 349525]),
|
|
("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))
|