The reader used miniz_oxide, which accepts a dynamic block whose lone distance code is longer than one bit. zlib's inflate, and so pack_verify.py, refuses that block. A golden resealed with such a still verified in wai-pack and was refused by pack_verify. Both readers now refuse incomplete code sets exactly as zlib does: the only incomplete literal/length or distance code allowed is one whose longest code is one bit. The reader now uses its own inflater (png::inflate_zlib), written from RFC 1950/1951 to zlib's inflate rules: - the header's check bits, method, window and preset-dictionary flag; - the stored-block complement; - the symbol counts; - a complete code-length code; - repeats with a length before them and within the code lengths; - a code for the end of the block; - over-subscribed and incomplete codes; - a code with no symbol, or a distance past the bytes written, refused where it is read; - the Adler-32, with nothing after it. miniz_oxide is kept only as a dev-dependency: the writer's round-trip test reads its stills back with an inflater independent of the reader's. packaging §5.7 now states these rules instead of "any PNG reader", and says that a still the verifier does not read is prefix-digest. stills/ gains the same dynamic block twice: a lone one-bit distance code (read) and a lone two-bit one (refused, still-incomplete-dist). png_diff.py drives wai-pack's reader (a new stage-json op, read-png) against pack_verify.py's. The inputs are: - zlib's own streams at every level, strategy and window; - the packager's own stills; - dynamic blocks built on each code-set rule (22 kinds, each checked against the verdict the rules give it); - mutations of all of these. wai-pack.yml runs it in debug and release. Co-Authored-By: Claude Opus 5.5 <[email protected]>
535 lines
19 KiB
Python
535 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
"""A still's reader, differentially: wai-pack's PNG reader against pack_verify.py's, which
|
|
reads the zlib stream with Python's zlib module (extensions/packaging.md §5.7).
|
|
|
|
The inputs, seeded:
|
|
|
|
- streams the zlib library writes, at every level and strategy and at windows from
|
|
512 bytes to 32 KiB, and the reference packager's own stills;
|
|
- dynamic blocks built here, each code set chosen to sit on one of RFC 1951's and zlib's
|
|
rules: complete codes; no distance code at all; a lone distance code of one bit (the
|
|
one incomplete set zlib takes), used and misused; a lone distance code of 2 to 15 bits;
|
|
over-subscribed and incomplete literal/length, distance and code-length codes; a
|
|
repeat with no length before it, or past the code lengths; no code for the end of the
|
|
block; too many length or distance symbols; a distance past the bytes written; stored
|
|
blocks with a wrong complement; fixed blocks with the symbols they cannot use; and
|
|
several blocks of mixed kinds;
|
|
- and every one of those altered: bits flipped, cut short, bytes appended, the header's
|
|
bytes and the Adler-32 changed, chunks dropped, repeated, moved or added, the IHDR's
|
|
fields and a chunk's length changed.
|
|
|
|
Each one's verdict and, read, its samples' SHA-256 must be the same from both readers,
|
|
and the constructions whose verdict the rules decide must have it.
|
|
|
|
python3 png_diff.py <wai-pack binary> [seed] [count]
|
|
|
|
Exit status 0 only if every input agrees.
|
|
"""
|
|
import hashlib
|
|
import heapq
|
|
import json
|
|
import os
|
|
import random
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
import zlib
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, HERE)
|
|
import pack_verify as PV # noqa: E402
|
|
|
|
SIG = b"\x89PNG\r\n\x1a\n"
|
|
ORDER = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]
|
|
|
|
|
|
def chunk(kind, data):
|
|
return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data))
|
|
|
|
|
|
def png_of(w, h, z, extra=()):
|
|
ihdr = struct.pack(">II", w, h) + bytes([8, 2, 0, 0, 0])
|
|
return SIG + chunk(b"IHDR", ihdr) + b"".join(chunk(k, d) for k, d in extra) + chunk(b"IDAT", z) + chunk(b"IEND", b"")
|
|
|
|
|
|
def picture(rng):
|
|
"""A picture's filtered rows: random, flat or striped samples, each row with a filter."""
|
|
w, h = rng.randint(1, 40), rng.randint(1, 12)
|
|
kind = rng.randrange(3)
|
|
raw = bytearray()
|
|
for y in range(h):
|
|
raw.append(rng.randrange(5))
|
|
for x in range(3 * w):
|
|
raw.append(rng.randrange(256) if kind == 0 else (7 if kind == 1 else (x // 3 * 11 + y) % 256))
|
|
return w, h, bytes(raw)
|
|
|
|
|
|
class Bits:
|
|
def __init__(self):
|
|
self.out, self.acc, self.n = bytearray(), 0, 0
|
|
|
|
def put(self, v, n):
|
|
for i in range(n):
|
|
self.acc |= ((v >> i) & 1) << self.n
|
|
self.n += 1
|
|
if self.n == 8:
|
|
self.out.append(self.acc)
|
|
self.acc, self.n = 0, 0
|
|
|
|
def code(self, c, n):
|
|
for i in reversed(range(n)):
|
|
self.put((c >> i) & 1, 1)
|
|
|
|
def align(self):
|
|
if self.n:
|
|
self.out.append(self.acc)
|
|
self.acc, self.n = 0, 0
|
|
|
|
|
|
def canonical(lengths):
|
|
"""RFC 1951 §3.2.2's codes of `lengths` (an over-subscribed set gives codes anyway)."""
|
|
count = [0] * 16
|
|
for x in lengths:
|
|
count[x] += 1
|
|
count[0] = 0
|
|
nxt, code = [0] * 16, 0
|
|
for b in range(1, 16):
|
|
code = (code + count[b - 1]) << 1
|
|
nxt[b] = code
|
|
out = []
|
|
for x in lengths:
|
|
out.append(nxt[x] if x else 0)
|
|
if x:
|
|
nxt[x] += 1
|
|
return out
|
|
|
|
|
|
def huffman(freq, n, limit):
|
|
"""Code lengths for symbols 0..n-1 from their counts (a symbol of count 0 gets none):
|
|
Huffman's, or equal lengths where Huffman's pass `limit`. Complete for two or more
|
|
symbols; one symbol gets one bit."""
|
|
used = [s for s in range(n) if freq.get(s)]
|
|
lengths = [0] * n
|
|
if len(used) == 1:
|
|
lengths[used[0]] = 1
|
|
return lengths
|
|
heap = [(freq[s], i, [s]) for i, s in enumerate(used)]
|
|
heapq.heapify(heap)
|
|
k = len(heap)
|
|
while len(heap) > 1:
|
|
a, b = heapq.heappop(heap), heapq.heappop(heap)
|
|
for s in a[2] + b[2]:
|
|
lengths[s] += 1
|
|
heapq.heappush(heap, (a[0] + b[0], k, a[2] + b[2]))
|
|
k += 1
|
|
if max(lengths) > limit:
|
|
# Equal lengths, then the shortest codes split off until the set is complete.
|
|
bits = max(1, (len(used) - 1).bit_length())
|
|
lengths = [bits if s in used else 0 for s in range(n)]
|
|
spare = (1 << bits) - len(used)
|
|
for s in used:
|
|
if spare == 0:
|
|
break
|
|
lengths[s] -= 1
|
|
spare -= 1
|
|
return lengths
|
|
|
|
|
|
LEN_BASE, LEN_EXTRA = PV.LEN_BASE, PV.LEN_EXTRA
|
|
DIST_BASE, DIST_EXTRA = PV.DIST_BASE, PV.DIST_EXTRA
|
|
|
|
|
|
def tokens(raw, dists, rng):
|
|
"""`raw` as literals and matches, each match at a distance of a symbol in `dists`."""
|
|
out, i = [], 0
|
|
while i < len(raw):
|
|
best = None
|
|
if dists and rng.random() < 0.8:
|
|
for s in dists:
|
|
for d in {DIST_BASE[s], DIST_BASE[s] + (1 << DIST_EXTRA[s]) - 1}:
|
|
if d <= i:
|
|
m = 0
|
|
while m < 258 and i + m < len(raw) and raw[i + m - d] == raw[i + m]:
|
|
m += 1
|
|
if m >= 3 and (best is None or m > best[0]):
|
|
best = (m, d, s)
|
|
if best:
|
|
out.append(("m", best[0], best[1], best[2]))
|
|
i += best[0]
|
|
else:
|
|
out.append(("l", raw[i]))
|
|
i += 1
|
|
return out
|
|
|
|
|
|
def length_symbol(m):
|
|
li = max(k for k, x in enumerate(LEN_BASE) if x <= m)
|
|
return 257 + li, m - LEN_BASE[li], LEN_EXTRA[li]
|
|
|
|
|
|
def write_tokens(b, toks, lit, lc, dist, dc, bad_dist_bit=False):
|
|
misused = False
|
|
for t in toks:
|
|
if t[0] == "l":
|
|
b.code(lc[t[1]], lit[t[1]])
|
|
continue
|
|
_, m, d, s = t
|
|
sym, extra, nbits = length_symbol(m)
|
|
b.code(lc[sym], lit[sym])
|
|
b.put(extra, nbits)
|
|
if bad_dist_bit and not misused:
|
|
b.put(1, 1) # the unassigned half of a lone one-bit code
|
|
misused = True
|
|
continue
|
|
b.code(dc[s], dist[s])
|
|
b.put(d - DIST_BASE[s], DIST_EXTRA[s])
|
|
b.code(lc[256], lit[256])
|
|
|
|
|
|
def over(lengths):
|
|
"""A complete set made over-subscribed: a code for a symbol that had none, or, with
|
|
every symbol coded, its longest code a bit shorter."""
|
|
free = [x for x in range(len(lengths)) if not lengths[x]]
|
|
if free:
|
|
lengths[free[0]] = 1
|
|
else:
|
|
s = max(range(len(lengths)), key=lambda x: lengths[x])
|
|
lengths[s] -= 1
|
|
|
|
|
|
def dynamic_block(b, toks, last, variant, rng):
|
|
"""One dynamic block of `toks` with the code sets `variant` asks for."""
|
|
freq = {256: 1}
|
|
dfreq = {}
|
|
for t in toks:
|
|
s = t[1] if t[0] == "l" else length_symbol(t[1])[0]
|
|
freq[s] = freq.get(s, 0) + 1
|
|
if t[0] == "m":
|
|
dfreq[t[3]] = dfreq.get(t[3], 0) + 1
|
|
if len(freq) == 1:
|
|
freq[0] = 1
|
|
nlen = max(257, max(freq) + 1)
|
|
lit = huffman(freq, nlen, 15)
|
|
if variant == "dist-none":
|
|
ndist, dist = 1, [0]
|
|
elif dfreq:
|
|
ndist = max(dfreq) + 1
|
|
dist = huffman(dfreq, ndist, 15)
|
|
else:
|
|
ndist, dist = 1, [0]
|
|
used_d = [s for s in range(ndist) if dist[s]]
|
|
if variant.startswith("dist-lone-") and len(used_d) == 1:
|
|
dist[used_d[0]] = 1 if variant in ("dist-lone-1", "dist-lone-1-misused") else rng.randint(2, 15)
|
|
elif variant == "dist-over":
|
|
dist = dist + [1, 1, 1]
|
|
ndist = len(dist)
|
|
elif variant == "dist-incomplete" and used_d:
|
|
# One code a bit longer: the set falls short of complete, its longest past one bit.
|
|
s = min(used_d, key=lambda x: dist[x])
|
|
dist[s] += 1
|
|
elif variant == "lit-incomplete":
|
|
s = min((x for x in range(nlen) if lit[x]), key=lambda x: lit[x])
|
|
lit[s] += 1
|
|
elif variant == "lit-over":
|
|
# A code for a symbol that had none, on a set that was complete.
|
|
over(lit)
|
|
elif variant == "no-eob":
|
|
lit[256] = 0
|
|
ndist = min(ndist, 30)
|
|
dist = dist[:ndist]
|
|
lc, dc = canonical(lit), canonical(dist)
|
|
seq = lit + dist
|
|
# The code lengths, run-length coded at random (§3.2.7).
|
|
syms, i = [], 0
|
|
if variant == "rep16-first":
|
|
syms.append((16, 0, 2))
|
|
while i < len(seq):
|
|
x = seq[i]
|
|
run = 1
|
|
while i + run < len(seq) and seq[i + run] == x:
|
|
run += 1
|
|
if x == 0 and run >= 11 and rng.random() < 0.7:
|
|
n = min(run, 138)
|
|
syms.append((18, n - 11, 7))
|
|
i += n
|
|
elif x == 0 and run >= 3 and rng.random() < 0.7:
|
|
n = min(run, 10)
|
|
syms.append((17, n - 3, 3))
|
|
i += n
|
|
elif i > 0 and seq[i - 1] == x and run >= 3 and rng.random() < 0.7:
|
|
n = min(run, 6)
|
|
syms.append((16, n - 3, 2))
|
|
i += n
|
|
else:
|
|
syms.append((x, 0, 0))
|
|
i += 1
|
|
if variant == "rep-past":
|
|
syms.append((17, 7, 3))
|
|
cfreq = {}
|
|
for s, _, _ in syms:
|
|
cfreq[s] = cfreq.get(s, 0) + 1
|
|
if len(cfreq) == 1:
|
|
cfreq[0 if 0 not in cfreq else 1] = 1
|
|
cl = huffman(cfreq, 19, 7)
|
|
if variant == "cl-incomplete":
|
|
s = min((x for x in range(19) if 0 < cl[x] < 7), key=lambda x: cl[x])
|
|
cl[s] += 1
|
|
elif variant == "cl-over":
|
|
over(cl)
|
|
cc = canonical(cl)
|
|
ncode = max(4, max(k for k, s in enumerate(ORDER) if cl[s]) + 1)
|
|
b.put(last, 1)
|
|
b.put(2, 2)
|
|
hlit = nlen - 257 if variant != "hlit-big" else rng.choice([30, 31])
|
|
hdist = ndist - 1 if variant != "hdist-big" else rng.choice([30, 31])
|
|
b.put(hlit, 5)
|
|
b.put(hdist, 5)
|
|
b.put(ncode - 4, 4)
|
|
for s in ORDER[:ncode]:
|
|
b.put(cl[s], 3)
|
|
for s, extra, nbits in syms:
|
|
b.code(cc[s], cl[s])
|
|
b.put(extra, nbits)
|
|
write_tokens(b, toks, lit, lc, dist, dc, bad_dist_bit=variant == "dist-lone-1-misused")
|
|
|
|
|
|
def fixed_block(b, toks, last, variant):
|
|
lit = [8] * 144 + [9] * 112 + [7] * 24 + [8] * 8
|
|
lc = canonical(lit)
|
|
dist, dc = [5] * 32, canonical([5] * 32)
|
|
b.put(last, 1)
|
|
b.put(1, 2)
|
|
if variant == "fixed-lit-286":
|
|
b.code(lc[286], 8)
|
|
elif variant == "fixed-dist-30" and any(t[0] == "m" for t in toks):
|
|
k = next(i for i, t in enumerate(toks) if t[0] == "m")
|
|
toks = toks[:k] + [("m", toks[k][1], toks[k][2], 30)] + toks[k + 1:]
|
|
dist_base = DIST_BASE + [0, 0]
|
|
for t in toks:
|
|
if t[0] == "l":
|
|
b.code(lc[t[1]], 8 if t[1] < 144 else 9)
|
|
else:
|
|
sym, extra, nbits = length_symbol(t[1])
|
|
b.code(lc[sym], lit[sym])
|
|
b.put(extra, nbits)
|
|
b.code(dc[t[3]], 5)
|
|
if t[3] < 30:
|
|
b.put(t[2] - dist_base[t[3]], DIST_EXTRA[t[3]])
|
|
b.code(lc[256], 7)
|
|
return
|
|
write_tokens(b, toks, lit, lc, dist, dc)
|
|
|
|
|
|
def stored_block(b, data, last, variant):
|
|
b.put(last, 1)
|
|
b.put(0, 2)
|
|
b.align()
|
|
n = len(data)
|
|
nn = (~n) & 0xFFFF if variant != "stored-complement" else n
|
|
b.out += struct.pack("<HH", n, nn) + data
|
|
|
|
|
|
# Each construction, and the verdict RFC 1951 and zlib's inflate give it (None: the
|
|
# readers must only agree).
|
|
VARIANTS = {
|
|
"complete": "ok", "dist-none": "ok", "dist-lone-1": "ok", "dist-lone-1-misused": "refused",
|
|
"dist-lone-L": "refused", "dist-over": "refused", "dist-incomplete": "refused",
|
|
"lit-incomplete": "refused", "lit-over": "refused", "cl-incomplete": "refused", "cl-over": "refused",
|
|
"rep16-first": "refused", "rep-past": "refused", "no-eob": "refused", "hlit-big": "refused",
|
|
"hdist-big": "refused", "far": "refused", "stored": "ok", "stored-complement": "refused",
|
|
"fixed-lit-286": "refused", "fixed-dist-30": "refused", "mixed": "ok",
|
|
}
|
|
|
|
|
|
def construct(rng, variant):
|
|
w, h, raw = picture(rng)
|
|
b = Bits()
|
|
if variant in ("stored", "stored-complement"):
|
|
stored_block(b, raw, 1, variant)
|
|
elif variant in ("fixed-lit-286", "fixed-dist-30"):
|
|
toks = tokens(raw, list(range(4)), rng)
|
|
if variant == "fixed-dist-30" and not any(t[0] == "m" for t in toks):
|
|
return None
|
|
fixed_block(b, toks, 1, variant)
|
|
elif variant == "mixed":
|
|
cut = sorted(rng.sample(range(len(raw) + 1), 2)) if len(raw) >= 2 else [0, len(raw)]
|
|
parts = [raw[:cut[0]], raw[cut[0]:cut[1]], raw[cut[1]:]]
|
|
for k, part in enumerate(parts):
|
|
last = int(k == 2)
|
|
kind = rng.randrange(3)
|
|
if kind == 0:
|
|
stored_block(b, part, last, "stored")
|
|
else:
|
|
# Matches only within this part, so a distance never reaches into another.
|
|
toks = tokens(part, list(range(4)), rng)
|
|
if kind == 1:
|
|
fixed_block(b, toks, last, "")
|
|
else:
|
|
dynamic_block(b, toks, last, "complete", rng)
|
|
else:
|
|
dists = []
|
|
if variant not in ("dist-none",):
|
|
dists = [rng.randrange(4)] if variant.startswith("dist-lone-") else rng.sample(range(8), rng.randint(1, 4))
|
|
toks = tokens(raw, dists, rng)
|
|
if variant.startswith("dist-lone-") or variant in ("dist-over", "dist-incomplete"):
|
|
if not any(t[0] == "m" for t in toks):
|
|
return None
|
|
if variant == "far":
|
|
toks = [("m", 3, 1, 0)] + toks
|
|
dynamic_block(b, toks, 1, "dist-lone-L" if variant == "dist-lone-L" else variant, rng)
|
|
b.align()
|
|
z = b"\x78\x01" + bytes(b.out) + struct.pack(">I", zlib.adler32(raw))
|
|
return png_of(w, h, z)
|
|
|
|
|
|
def zlib_streams(rng):
|
|
out = []
|
|
for _ in range(40):
|
|
w, h, raw = picture(rng)
|
|
co = zlib.compressobj(rng.choice([0, 1, 6, 9]), zlib.DEFLATED, rng.choice([9, 10, 12, 15]), 9,
|
|
rng.choice([zlib.Z_DEFAULT_STRATEGY, zlib.Z_FIXED, zlib.Z_HUFFMAN_ONLY, zlib.Z_RLE]))
|
|
out.append(png_of(w, h, co.compress(raw) + co.flush()))
|
|
rgb = bytes(rng.randrange(256) if rng.random() < 0.5 else 9 for _ in range(3 * w * h))
|
|
out.append(PV.png_still(rgb, w, h))
|
|
return out
|
|
|
|
|
|
def chunks_of(b):
|
|
at, out = 8, []
|
|
while at + 12 <= len(b):
|
|
n = struct.unpack_from(">I", b, at)[0]
|
|
if at + 12 + n > len(b):
|
|
break
|
|
out.append((b[at + 4:at + 8], b[at + 8:at + 8 + n]))
|
|
at += 12 + n
|
|
return out
|
|
|
|
|
|
def build(chs):
|
|
return SIG + b"".join(chunk(k, d) for k, d in chs)
|
|
|
|
|
|
def mutate(b, rng):
|
|
if len(b) < 2:
|
|
return b + bytes([rng.randrange(256)])
|
|
chs = chunks_of(b)
|
|
op = rng.randrange(12)
|
|
if op == 0 or not chs:
|
|
b = bytearray(b)
|
|
for _ in range(rng.randint(1, 3)):
|
|
i = rng.randrange(len(b))
|
|
b[i] ^= 1 << rng.randrange(8)
|
|
return bytes(b)
|
|
if op == 1:
|
|
return b[:rng.randrange(len(b))]
|
|
if op == 2:
|
|
i, j = rng.randrange(len(chs)), rng.randrange(len(chs))
|
|
chs[i], chs[j] = chs[j], chs[i]
|
|
return build(chs)
|
|
if op == 3:
|
|
chs.insert(rng.randrange(len(chs) + 1), chs[rng.randrange(len(chs))])
|
|
return build(chs)
|
|
if op == 4:
|
|
del chs[rng.randrange(len(chs))]
|
|
return build(chs)
|
|
if op == 5:
|
|
kind = rng.choice([b"tEXt", b"PLTE", b"IDAT", b"IEND", b"IHDR", b"abCD", b"ABCD", b"gAMA"])
|
|
data = bytes(rng.randrange(256) for _ in range(rng.choice([0, 1, 3, 6, 13, 768, 771])))
|
|
chs.insert(rng.randrange(len(chs) + 1), (kind, data))
|
|
return build(chs)
|
|
idats = [i for i, (k, _) in enumerate(chs) if k == b"IDAT"]
|
|
if op in (6, 7, 8, 9) and idats:
|
|
z = bytearray(b"".join(chs[i][1] for i in idats))
|
|
if op == 6 and z:
|
|
for _ in range(rng.randint(1, 4)):
|
|
i = rng.randrange(len(z))
|
|
z[i] ^= 1 << rng.randrange(8)
|
|
elif op == 7:
|
|
z = z[:rng.randrange(len(z) + 1)]
|
|
elif op == 8:
|
|
z += bytes(rng.randrange(256) for _ in range(rng.randint(1, 6)))
|
|
elif op == 9 and len(z) >= 2:
|
|
z[rng.randrange(2)] = rng.randrange(256)
|
|
if rng.random() < 0.7:
|
|
flg = z[1] & 0xE0
|
|
z[1] = flg | (31 - (z[0] * 256 + flg) % 31) % 31
|
|
z = bytes(z)
|
|
cut = rng.randrange(len(z) + 1)
|
|
rest = [c for i, c in enumerate(chs) if i not in idats]
|
|
pos = idats[0]
|
|
return build(rest[:pos] + [(b"IDAT", z[:cut]), (b"IDAT", z[cut:])] + rest[pos:])
|
|
if op == 10:
|
|
for i, (k, d) in enumerate(chs):
|
|
if k == b"IHDR" and len(d) == 13:
|
|
d = bytearray(d)
|
|
d[rng.randrange(13)] = rng.choice([0, 1, 2, 3, 6, 8, 16, 255])
|
|
chs[i] = (k, bytes(d))
|
|
return build(chs)
|
|
return b + bytes(rng.randrange(256) for _ in range(rng.randint(1, 12)))
|
|
|
|
|
|
def py_verdict(b):
|
|
try:
|
|
w, h, px = PV.png_frame(b)
|
|
return {"verdict": "ok", "w": w, "h": h, "rgb_sha256": hashlib.sha256(px).hexdigest()}
|
|
except ValueError:
|
|
return {"verdict": "refused"}
|
|
|
|
|
|
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 2000
|
|
rng = random.Random(seed)
|
|
cases = [] # (bytes, variant or None)
|
|
for f in sorted(os.listdir(os.path.join(HERE, "stills"))):
|
|
if f.endswith(".png"):
|
|
cases.append((open(os.path.join(HERE, "stills", f), "rb").read(), None))
|
|
cases += [(b, None) for b in zlib_streams(rng)]
|
|
names = sorted(VARIANTS)
|
|
while len(cases) < count // 2:
|
|
v = names[len(cases) % len(names)]
|
|
b = construct(rng, v)
|
|
if b is not None:
|
|
cases.append((b, v))
|
|
base = [b for b, _ in cases]
|
|
while len(cases) < count:
|
|
b = rng.choice(base)
|
|
for _ in range(rng.choice([1, 1, 2, 3])):
|
|
b = mutate(b, rng)
|
|
cases.append((b, None))
|
|
lines = "".join(json.dumps({"op": "read-png", "hex": b.hex()}) + "\n" for b, _ in cases)
|
|
out = subprocess.run([binary, "stage-json"], input=lines.encode(), capture_output=True, check=True).stdout.decode().splitlines()
|
|
if len(out) != len(cases):
|
|
print(f"FAIL wai-pack answered {len(out)} of {len(cases)} lines")
|
|
return 1
|
|
fails, seen = 0, {"ok": 0, "refused": 0}
|
|
by_variant = {}
|
|
for (b, v), got in zip(cases, out):
|
|
rust, mine = json.loads(got), py_verdict(b)
|
|
seen[mine["verdict"]] += 1
|
|
if v is not None:
|
|
by_variant[v] = by_variant.get(v, 0) + 1
|
|
why = None
|
|
if rust != mine:
|
|
why = f"wai-pack {json.dumps(rust)}, pack_verify.py {json.dumps(mine)}"
|
|
elif v is not None and VARIANTS[v] != mine["verdict"]:
|
|
why = f"the construction {v} should be {VARIANTS[v]}, both readers say {mine['verdict']}"
|
|
if why:
|
|
fails += 1
|
|
if fails <= 5:
|
|
print(f"FAIL {v or 'input'} {b[:64].hex()}…: {why}")
|
|
missing = [v for v in names if v not in by_variant]
|
|
if missing:
|
|
fails += 1
|
|
print(f"FAIL constructions never built: {missing}")
|
|
print(f"png_diff seed {seed}: {len(cases) - fails}/{len(cases)} agree ({seen['ok']} read, {seen['refused']} refused; "
|
|
f"{sum(by_variant.values())} constructions of {len(by_variant)} kinds)")
|
|
return 0 if fails == 0 else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv))
|