fix(thumbs): fold the heid bug-hunt: a cache that cannot be planted, alpha, orientation
The heid bug-hunt panel onc2b1454(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 byc2b1454). 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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user