qemu and build: review fixes across the rig driver, boot script, and fetch helpers
Bounded waits and validated arguments in run.sh and ui-drive.sh, a seeded settings directory and root-only staged rootfs permissions with their own tests, qmp.py and imgtools.py hardening, the fetch scripts checking what they download, and ASCII typography throughout. Each fix carries its test under qemu/tests or tests/. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N3G6m9Aw5RyVY4ZowtKzEj
This commit is contained in:
+48
-58
@@ -35,11 +35,10 @@ import json
|
||||
|
||||
from PIL import Image, ImageFilter
|
||||
|
||||
# occupancy thresholds for structural(): a downsampled cell counts as
|
||||
# "occupied" if it is meaningfully brighter than black, or sits on an edge.
|
||||
# Both are 0..255 greyscale/edge-magnitude averages over the cell.
|
||||
# structural(): a cell is occupied when its grey is this far from the crop's
|
||||
# median (its background) or its FIND_EDGES energy exceeds the edge threshold.
|
||||
# occupancy thresholds for structural(): a cell is occupied when its grey is
|
||||
# this far from the crop's median (its background), or its FIND_EDGES energy
|
||||
# exceeds the edge threshold. Both are 0..255 greyscale/edge-magnitude
|
||||
# averages over the cell.
|
||||
# Validated on real captures: a switch knob left/right differs in 240/256
|
||||
# cells, a dark card's icon and text stand out from its (7,13,29) ground.
|
||||
DEVIATION_THRESHOLD = 28
|
||||
@@ -53,64 +52,31 @@ EDGE_THRESHOLD = 24
|
||||
def load_ppm(path):
|
||||
"""Read a binary PPM (P6), return (w, h, rgb_bytes).
|
||||
|
||||
QEMU's screendump writes a comment-free header ("P6\\nW H\\n255\\n"
|
||||
then raw bytes) but the token reader here handles the general P6
|
||||
grammar -- whitespace runs and '#' comments -- since that costs
|
||||
nothing extra and means any other producer of P6 files just works.
|
||||
Delegates to Pillow's own P6 decoder (Image.open, format auto-detected
|
||||
from the magic) instead of re-parsing the PPM grammar by hand: skipping
|
||||
whitespace runs and '#' comments, and normalising any maxval to 8-bit,
|
||||
are Pillow's problem here, not ours. convert("RGB") guarantees the
|
||||
tightly-packed 3-bytes-per-pixel layout every caller in this file
|
||||
assumes regardless of the source channel depth.
|
||||
"""
|
||||
with open(path, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
def skip_ws_comments(p):
|
||||
while p < len(data):
|
||||
c = data[p]
|
||||
if c in b" \t\r\n":
|
||||
p += 1
|
||||
elif c == ord("#"):
|
||||
while p < len(data) and data[p] != ord("\n"):
|
||||
p += 1
|
||||
else:
|
||||
break
|
||||
return p
|
||||
|
||||
def read_token(p):
|
||||
p = skip_ws_comments(p)
|
||||
start = p
|
||||
while p < len(data) and data[p] not in b" \t\r\n":
|
||||
p += 1
|
||||
return data[start:p], p
|
||||
|
||||
pos = 0
|
||||
magic, pos = read_token(pos)
|
||||
if magic != b"P6":
|
||||
raise ValueError(f"{path}: not a P6 PPM (magic={magic!r})")
|
||||
w_tok, pos = read_token(pos)
|
||||
h_tok, pos = read_token(pos)
|
||||
maxval_tok, pos = read_token(pos)
|
||||
w, h, maxval = int(w_tok), int(h_tok), int(maxval_tok)
|
||||
if maxval != 255:
|
||||
raise ValueError(f"{path}: unsupported PPM maxval {maxval} (only 255 handled)")
|
||||
# exactly one whitespace byte separates the header from the binary data
|
||||
pos += 1
|
||||
need = w * h * 3
|
||||
pixels = data[pos:pos + need]
|
||||
if len(pixels) != need:
|
||||
raise ValueError(f"{path}: truncated PPM, want {need} bytes got {len(pixels)}")
|
||||
return w, h, pixels
|
||||
with Image.open(path) as im:
|
||||
im = im.convert("RGB")
|
||||
return im.width, im.height, im.tobytes()
|
||||
|
||||
|
||||
def crop(img, x, y, w, h):
|
||||
"""Crop (W,H,rgb) to the x,y,w,h box, return a new (w,h,rgb) image."""
|
||||
"""Crop (W,H,rgb) to the x,y,w,h box, return a new (w,h,rgb) image.
|
||||
|
||||
The bounds check stays explicit and fails loudly (see main()'s own
|
||||
comment on that) rather than delegating to PIL's crop, which silently
|
||||
zero-pads a box that runs outside the source image instead of raising --
|
||||
exactly the kind of stale/mistyped region box this check exists to catch.
|
||||
"""
|
||||
width, height, data = img
|
||||
if x < 0 or y < 0 or w <= 0 or h <= 0 or x + w > width or y + h > height:
|
||||
raise ValueError(f"crop box {x},{y},{w}x{h} outside image {width}x{height}")
|
||||
row_bytes = w * 3
|
||||
out = bytearray(h * row_bytes)
|
||||
for row in range(h):
|
||||
src_off = ((y + row) * width + x) * 3
|
||||
dst_off = row * row_bytes
|
||||
out[dst_off:dst_off + row_bytes] = data[src_off:src_off + row_bytes]
|
||||
return w, h, bytes(out)
|
||||
cropped = _to_pil(img).crop((x, y, x + w, y + h))
|
||||
return w, h, cropped.tobytes()
|
||||
|
||||
|
||||
def _to_pil(img):
|
||||
@@ -188,7 +154,7 @@ def phash(img):
|
||||
|
||||
def hamming(a, b):
|
||||
"""Bit-differences between two phash ints."""
|
||||
return bin(a ^ b).count("1")
|
||||
return (a ^ b).bit_count()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -241,7 +207,7 @@ def _median_grey(grey):
|
||||
|
||||
def _structural_diff(a, b):
|
||||
"""Count of differing bits between two 32-byte occupancy masks."""
|
||||
return sum(bin(x ^ y).count("1") for x, y in zip(a, b))
|
||||
return (int.from_bytes(a, "big") ^ int.from_bytes(b, "big")).bit_count()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -405,6 +371,10 @@ def cmd_bench(argv):
|
||||
print(f"bench: {cw}x{ch} region, n={n} iterations")
|
||||
for label, fn in (("phash", lambda: phash(region)),
|
||||
("structural", lambda: structural(region))):
|
||||
for _ in range(5):
|
||||
fn() # warm up: first call can carry one-off costs (e.g. module-
|
||||
# level caches settling) that don't belong in the steady-state
|
||||
# population below
|
||||
samples = timeit.repeat(fn, repeat=n, number=1)
|
||||
lo, p50, p95, hi = percentiles(samples)
|
||||
print(f" {label:<10} min={lo * 1000:7.3f}ms p50={p50 * 1000:7.3f}ms "
|
||||
@@ -493,6 +463,26 @@ def selftest():
|
||||
check("crop keeps the drawn pixel in place",
|
||||
cropped[2][px_off:px_off + 3] == bytes((220, 20, 20)))
|
||||
|
||||
# a header QEMU never writes but the P6 grammar allows: a '#'
|
||||
# comment line and a non-255 maxval. Pillow's decoder (load_ppm no
|
||||
# longer hand-parses the header) must still read it correctly.
|
||||
odd_path = os.path.join(tmp, "odd-header.ppm")
|
||||
with open(odd_path, "wb") as f:
|
||||
f.write(b"P6\n# generated for a selftest, not by QEMU\n2 2\n100\n")
|
||||
f.write(bytes((100, 0, 0, 0, 100, 0, 0, 0, 100, 100, 100, 100)))
|
||||
ow, oh, odata = load_ppm(odd_path)
|
||||
check("load_ppm reads width/height past a comment line", (ow, oh) == (2, 2))
|
||||
check("load_ppm scales a non-255 maxval channel to 8-bit",
|
||||
odata[0:3] == bytes((255, 0, 0)))
|
||||
|
||||
# crop box outside the image must still fail loudly, not silently
|
||||
# zero-pad the way PIL's own Image.crop does
|
||||
try:
|
||||
crop((bw, bh, bdata), bw - 4, bh - 4, 32, 32)
|
||||
check("crop rejects a box past the image edge", False)
|
||||
except ValueError:
|
||||
check("crop rejects a box past the image edge", True)
|
||||
|
||||
# _dct2d_32's _DCT_KEEP truncation must land on the exact same
|
||||
# coefficients an untruncated 32x32 DCT would produce
|
||||
small = _to_pil((bw, bh, bdata)).convert("L").resize((32, 32), Image.LANCZOS)
|
||||
|
||||
Reference in New Issue
Block a user