fix(thumbs): fold the heid bug-hunt: a cache that cannot be planted, alpha, orientation

The heid bug-hunt panel on c2b1454 (4/4 arms, five seat-executed probes). The
new size rules governed only cache MISSES; the hit path trusted a name and an
mtime, inside a directory any fleet session can write into.

- A cache hit is a REGULAR file (lstat) carrying its source's EXACT mtime (4/4).
  A planted directory at the cache path was returned as the thumbnail, and a
  source replaced by `cp -p` or an archive extract kept an older stamp that
  `>=` served forever. The encoder now stamps the thumbnail with the source's
  mtime, so any change to the source is a miss.
- The cache directories are made component by component and never through a
  link (seat P4). A `.thumbs` planted as a link put the cache outside the
  booth, beyond the sweep. The booth-mtime restore now keys on creating
  `.thumbs` itself.
- The temp file is mkstemp (4/4, seat P5). The old `<out>.<pid>.tmp` was
  predictable, and a link planted there made the encoder overwrite its target
  (600 B became 316,400 B).
- Palette transparency survives (3/4, seat-executed, and INTRODUCED by
  c2b1454). The fits-but-heavy branch newly re-encoded palette PNGs, and
  getbands() of mode P has no A even with tRNS.
- EXIF orientation is honoured for sizing and for the saved image (groa,
  seat-verified). A camera portrait stored sideways was sized and tiled as a
  landscape.
- A 64 MP decode budget (2/4). A header claims any size, and a failure is not
  cached, so every request re-decoded it.
- The cache name carries the whole rule: width, height cap, quality and an
  encoding version (groa). The width alone would have served stale bytes after
  a quality change.

Declined: the utime-restore failing on a foreign-owned booth (booths are the
service user's), and regin's two solos (the THUMB_MAX export is not imported
anywhere; the live fixture is function-scoped). thumbs.toml: 14/14 proved.
This commit is contained in:
vh
2026-09-23 23:06:45 -07:00
parent c2b1454358
commit c19d8c9718
4 changed files with 297 additions and 46 deletions
+74 -1
View File
@@ -64,6 +64,79 @@ label = "an unversioned cache name: a thumbnail cut to the old rule is served fo
file = "booth/thumbs.py"
test = "tests/test_thumbs.py::test_a_thumbnail_cut_to_the_old_rule_is_not_served"
old = '''
return booth / THUMB_DIR / f"{rel}.{THUMB_WIDTH}w.webp"'''
return booth / THUMB_DIR / f"{rel}.{rule}.webp"'''
new = '''
return booth / THUMB_DIR / (rel + ".webp")'''
# ---- the heid bug-hunt on this change (4/4 arms), folded ------------------------
[[mutation]]
label = "a cache hit trusts the name and the mtime (a planted directory is served)"
file = "booth/thumbs.py"
test = "tests/test_thumbs.py::test_a_planted_directory_at_the_cache_path_is_not_served"
old = '''
return stat.S_ISREG(o.st_mode) and o.st_mtime_ns == s_stat.st_mtime_ns'''
new = '''
return o.st_mtime_ns >= s_stat.st_mtime_ns'''
[[mutation]]
label = "freshness is 'at least as new' (a cp -p'd older source pins the old thumbnail)"
file = "booth/thumbs.py"
test = "tests/test_thumbs.py::test_a_source_replaced_with_an_older_mtime_is_rebuilt"
old = '''
return stat.S_ISREG(o.st_mode) and o.st_mtime_ns == s_stat.st_mtime_ns'''
new = '''
return stat.S_ISREG(o.st_mode) and o.st_mtime_ns >= s_stat.st_mtime_ns'''
[[mutation]]
label = "the cache dirs are made by following links (a planted .thumbs link escapes the booth)"
file = "booth/thumbs.py"
test = "tests/test_thumbs.py::test_a_symlinked_cache_dir_is_never_written_through"
old = '''
if not _cache_dir(booth, out.parent):
return None'''
new = '''
out.parent.mkdir(parents=True, exist_ok=True)'''
[[mutation]]
label = "a predictable temp name the encoder writes through"
file = "booth/thumbs.py"
test = "tests/test_thumbs.py::test_a_planted_link_at_the_old_temp_name_cannot_redirect_the_write"
old = '''
fd, tmp = tempfile.mkstemp(prefix=".", suffix=".tmp", dir=out.parent)
try:
with os.fdopen(fd, "wb") as fh:
im.save(fh, "WEBP", quality=THUMB_QUALITY, method=4)'''
new = '''
tmp = str(out) + f".{os.getpid()}.tmp"
try:
im.save(tmp, "WEBP", quality=THUMB_QUALITY, method=4)'''
[[mutation]]
label = "RGBA chosen by getbands() alone (palette transparency baked opaque)"
file = "booth/thumbs.py"
test = "tests/test_thumbs.py::test_palette_transparency_survives_the_thumbnail"
old = '''
alpha = "A" in im.getbands() or "transparency" in im.info'''
new = '''
alpha = "A" in im.getbands()'''
[[mutation]]
label = "EXIF orientation ignored (a camera portrait tiled sideways)"
file = "booth/thumbs.py"
test = "tests/test_thumbs.py::test_a_camera_portrait_is_sized_and_saved_upright"
old = '''
orientation = im.getexif().get(0x0112, 1)'''
new = '''
orientation = 1'''
[[mutation]]
label = "no pixel budget: whatever the header claims is decoded"
file = "booth/thumbs.py"
test = "tests/test_thumbs.py::test_an_image_past_the_pixel_budget_is_never_decoded"
old = '''
if w * h > THUMB_MAX_PIXELS:
return None'''
new = '''
if False:
return None'''
+105
View File
@@ -269,3 +269,108 @@ def test_a_thumbnail_cut_to_the_old_rule_is_not_served(tmp_path):
out = ensure_thumb(b, "p.png")
assert out == thumb_path(b, "p.png") and out != legacy
assert PIL.open(out).size == (704, 1408)
# ---- the heid bug-hunt on this change (4/4 arms), folded ------------------------
#
# The cache sits in a directory any fleet session can write into, so every entry
# on the way to it may be planted. The new size rules only governed cache MISSES;
# the hit path trusted a name and an mtime.
def test_a_planted_directory_at_the_cache_path_is_not_served(tmp_path):
"""4/4, seat-executed: a directory at the cache path, with a future mtime,
was returned AS the thumbnail. Defeating change: a cache hit that checks
only the mtime."""
import os
b = tmp_path / "g"
_noise(b / "p.png", 704, 1408)
out = thumb_path(b, "p.png")
out.mkdir(parents=True)
os.utime(out, (2e9, 2e9))
got = ensure_thumb(b, "p.png")
assert got is None or got.is_file()
def test_a_source_replaced_with_an_older_mtime_is_rebuilt(tmp_path):
"""kimi: `cp -p` or an archive extract keeps an OLDER mtime, and a cache
newer than its source was served forever. The cache now carries its
source's exact mtime, so any change is a miss. Defeating change: `>=`."""
import os
b = tmp_path / "g"
_noise(b / "p.png", 704, 1408)
first = ensure_thumb(b, "p.png")
assert PIL.open(first).size == (704, 1408)
_noise(b / "p.png", 1536, 768)
os.utime(b / "p.png", (1e9, 1e9)) # an older stamp than the cache
assert PIL.open(ensure_thumb(b, "p.png")).size == (THUMB_WIDTH, THUMB_WIDTH // 2)
def test_a_symlinked_cache_dir_is_never_written_through(tmp_path):
"""seat P4: `.thumbs` planted as a link to another directory put the cache
outside the booth, beyond the sweep. Defeating change: `mkdir(parents=True)`,
which follows an existing link."""
b = tmp_path / "g"
_noise(b / "p.png", 704, 1408)
elsewhere = tmp_path / "elsewhere"
elsewhere.mkdir()
(b / THUMB_DIR).symlink_to(elsewhere)
assert ensure_thumb(b, "p.png") is None
assert list(elsewhere.iterdir()) == []
def test_a_planted_link_at_the_old_temp_name_cannot_redirect_the_write(tmp_path):
"""groa, seat P5: the temp name was `<out>.<pid>.tmp`, predictable, so a
link planted there made the encoder truncate and overwrite its target
(600 B -> 316,400 B). Defeating change: any predictable temp name."""
import os
b = tmp_path / "g"
_noise(b / "p.png", 704, 1408)
victim = tmp_path / "victim.txt"
victim.write_text("untouched")
out = thumb_path(b, "p.png")
out.parent.mkdir(parents=True)
(out.parent / (out.name + f".{os.getpid()}.tmp")).symlink_to(victim)
ensure_thumb(b, "p.png")
assert victim.read_text() == "untouched"
def test_palette_transparency_survives_the_thumbnail(tmp_path):
"""3/4, seat-executed, and INTRODUCED by this change: the fits-but-heavy
branch newly re-encodes palette PNGs, and `getbands()` of mode P has no A
even with a tRNS chunk, so transparency became opaque. Defeating change:
choosing RGBA by `getbands()` alone."""
import os
b = tmp_path / "g"
b.mkdir()
im = PIL.frombytes("P", (400, 400), os.urandom(400 * 400))
im.putpalette(os.urandom(768))
im.save(b / "p.png", "PNG", transparency=0)
assert (b / "p.png").stat().st_size > THUMB_LIGHT_BYTES
t = PIL.open(ensure_thumb(b, "p.png"))
assert t.mode == "RGBA" and t.getchannel("A").getextrema()[0] == 0
def test_a_camera_portrait_is_sized_and_saved_upright(tmp_path):
"""groa, seat-verified: EXIF orientation was ignored, so a portrait shot
stored sideways was sized as a landscape and tiled sideways. Defeating
change: sizing the raw pixels without `exif_transpose`."""
import os
b = tmp_path / "g"
b.mkdir()
exif = PIL.Exif()
exif[0x0112] = 6 # rotate 90 CW to display
PIL.frombytes("RGB", (1200, 800), os.urandom(1200 * 800 * 3)).save(
b / "cam.jpg", "JPEG", exif=exif, quality=95)
assert PIL.open(ensure_thumb(b, "cam.jpg")).size == (THUMB_WIDTH, 1152)
def test_an_image_past_the_pixel_budget_is_never_decoded(tmp_path, monkeypatch):
"""2/4: the header is free to read and `thumbnail()` then decodes whatever it
claims, on every request, since a failure is not cached. Over the budget,
the original is served instead. Defeating change: no budget check."""
import booth.thumbs as thumbs
b = tmp_path / "g"
_noise(b / "p.png", 704, 1408)
monkeypatch.setattr(thumbs, "THUMB_MAX_PIXELS", 704 * 1408 - 1)
assert ensure_thumb(b, "p.png") is None