fix(assets): the stream must reach the end of the frame, not just say stop
Chose the CURSOR over a pixel-coverage count because the corpus cannot choose -- 25 of 25 of the R761 delivery's RLE8 BMPs paint every pixel, 25 of 25 reach the end of the frame, 0 of 25 use a delta -- and an independent decoder can: a delta and an end-of-line escape state their skip, so every decoder agrees on the index-0 pixels they pass over, while a pixel count would refuse both constructions the format defines. `_bmp_rle8_rows` now refuses (`asset_samples_invalid`) when the terminator arrives with the cursor short of the last row. Pillow reads 5 of the 8 streams in the table and refuses the same 3, one of them short by a single pixel. Both docstrings the round was sent to correct are rewritten: the test no longer claims every pixel is decoded (it is not -- a stated skip keeps index 0), and `_bmp_rle8_rows` no longer frames the delta argument as read off the corpus, which it never was. R761 rebuilt: bundle `diff -r`-identical to the build before this commit, 50 assets (29 JPEG + 21 PNG), 19 of 19 conversions, SHY 71, u = 0, d = 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
60ad18dba8
commit
561b609a53
2 changed files with 44 additions and 16 deletions
|
|
@ -696,14 +696,32 @@ def _bmp_rle8_rows(data: bytes, head: _BmpHeader, *, name: str) -> bytes:
|
||||||
HERE and nowhere else, and `_bmp_flat_rows` refuses the same shape with the
|
HERE and nowhere else, and `_bmp_flat_rows` refuses the same shape with the
|
||||||
same code.
|
same code.
|
||||||
|
|
||||||
The rule is the terminator rather than `biSizeImage`, which is a claim by
|
The terminator is checked rather than `biSizeImage`, which is a claim by
|
||||||
the same untrusted header, and rather than a coverage count, which would
|
the same untrusted header. Read off the corpus: over the 25 RLE8 BMPs
|
||||||
refuse the delta the format defines. Read off the corpus: over the 19 real
|
the frozen R761 delivery ships (24 distinct; the bundle carries 19 of
|
||||||
RLE8 assets of the frozen R761 delivery, 19 of 19 end at an explicit
|
them, the rest being an unpointed duplicate and four no concept names),
|
||||||
end-of-bitmap, on 19 of 19 it is the stream's LAST two bytes, and on 19 of
|
25 of 25 end at an explicit
|
||||||
19 `biSizeImage` equals the available bytes -- so requiring it costs
|
end-of-bitmap, on 25 of 25 it is the stream's LAST two bytes, and on 25 of
|
||||||
|
25 `biSizeImage` equals the available bytes -- so requiring it costs
|
||||||
nothing measured here, and a whole stream that omits it is refused
|
nothing measured here, and a whole stream that omits it is refused
|
||||||
alongside a cut one because from the reader's side they are the same bytes.
|
alongside a cut one because from the reader's side they are the same bytes.
|
||||||
|
|
||||||
|
AND THE TERMINATOR ALONE IS NOT A COVERAGE PROOF, because a stream may say
|
||||||
|
it is finished anywhere: measured 2026-09-19, one whose FIRST two bytes are
|
||||||
|
the end-of-bitmap escape was carried with every pixel of the frame never
|
||||||
|
decoded. So the cursor must also stand at or past the end of the last row.
|
||||||
|
|
||||||
|
THE LINE IS THE CURSOR AND NOT THE PIXELS, and that is a format argument
|
||||||
|
rather than a corpus one -- the corpus cannot choose between the two, since
|
||||||
|
25 of 25 of those files paint every pixel, 25 of 25 reach the end of the
|
||||||
|
frame and 0 of 25 use a delta. A delta escape and an end-of-line escape
|
||||||
|
STATE their skip, so the pixels they pass over keep index 0 and every
|
||||||
|
decoder produces the same picture; a pixel-coverage count would refuse both
|
||||||
|
constructions the format defines. Pixels the stream never reached have no
|
||||||
|
agreed value at all, which is why an independent decoder refuses the file:
|
||||||
|
measured over eight streams for one frame, Pillow reads the five whose
|
||||||
|
cursor reaches the end and refuses the three whose does not, one of them
|
||||||
|
short by a single pixel (`tests/test_asset_viewable.py`).
|
||||||
"""
|
"""
|
||||||
width, height = head.width, head.height
|
width, height = head.width, head.height
|
||||||
rows = [bytearray(width) for _ in range(height)]
|
rows = [bytearray(width) for _ in range(height)]
|
||||||
|
|
@ -748,6 +766,12 @@ def _bmp_rle8_rows(data: bytes, head: _BmpHeader, *, name: str) -> bytes:
|
||||||
"-- refusing to carry a frame whose remaining pixels were never decoded",
|
"-- refusing to carry a frame whose remaining pixels were never decoded",
|
||||||
code="asset_samples_invalid",
|
code="asset_samples_invalid",
|
||||||
)
|
)
|
||||||
|
if y < height - 1 or (y == height - 1 and x < width):
|
||||||
|
raise ExtractionError(
|
||||||
|
f"the RLE8 stream in {name!r} ends at row {y} column {x} of a {width}x{height} "
|
||||||
|
"frame -- refusing to carry a picture whose last rows the stream never reached",
|
||||||
|
code="asset_samples_invalid",
|
||||||
|
)
|
||||||
if not head.top_down:
|
if not head.top_down:
|
||||||
rows.reverse()
|
rows.reverse()
|
||||||
return b"".join(bytes(row) for row in rows)
|
return b"".join(bytes(row) for row in rows)
|
||||||
|
|
|
||||||
|
|
@ -576,14 +576,18 @@ def test_a_truncated_rle8_stream_is_refused_with_a_published_code() -> None:
|
||||||
a stream cut to 90 % carried with 13 923 pixels wrong, to 50 % with 95 890,
|
a stream cut to 90 % carried with 13 923 pixels wrong, to 50 % with 95 890,
|
||||||
to 10 % with 166 525 -- no code, no row, a partly blank PNG under a name
|
to 10 % with 166 525 -- no code, no row, a partly blank PNG under a name
|
||||||
that says it holds the source's pixels. The uncompressed path already
|
that says it holds the source's pixels. The uncompressed path already
|
||||||
refuses the same shape. NO PIXEL MAY BE GUESSED: either every one of them
|
refuses the same shape. NO PIXEL IS GUESSED THAT THE STREAM DID NOT SKIP
|
||||||
is decoded from the stream, or the picture is refused.
|
ON PURPOSE: a stream must say it is finished, and
|
||||||
|
`test_a_stream_that_stops_before_the_frame_is_refused` adds the other half
|
||||||
|
-- when it says so, the cursor must have reached the end of the frame.
|
||||||
|
Pixels a delta or an end-of-line escape passed over keep index 0, which is
|
||||||
|
the format's own value and what an independent decoder produces.
|
||||||
|
|
||||||
THE RULE IS THE TERMINATOR, and it is read off the corpus rather than
|
THIS GUARD IS THE TERMINATOR, and it is read off the corpus rather than
|
||||||
chosen: over the 19 real RLE8 assets of the frozen R761 delivery, 19 of 19
|
chosen: over the 25 RLE8 BMPs the frozen R761 delivery ships (24 distinct;
|
||||||
end at an explicit end-of-bitmap escape, that escape is the stream's LAST
|
the bundle carries 19), 25 of 25 end at an explicit end-of-bitmap escape, that escape is the stream's LAST
|
||||||
two bytes on 19 of 19, and `biSizeImage` equals the available bytes on
|
two bytes on 25 of 25, and `biSizeImage` equals the available bytes on
|
||||||
19 of 19. So a stream that runs out before its terminator is refused, and
|
25 of 25. So a stream that runs out before its terminator is refused, and
|
||||||
so is one that is whole but never states it is finished -- the two are the
|
so is one that is whole but never states it is finished -- the two are the
|
||||||
same bytes from a reader's side, and the measurement says no real writer
|
same bytes from a reader's side, and the measurement says no real writer
|
||||||
here produces the second.
|
here produces the second.
|
||||||
|
|
@ -675,8 +679,8 @@ def test_a_stream_that_stops_before_the_frame_is_refused() -> None:
|
||||||
not -- including one that is short by a single pixel.
|
not -- including one that is short by a single pixel.
|
||||||
|
|
||||||
A PIXEL coverage count would be a different rule and a wrong one: it
|
A PIXEL coverage count would be a different rule and a wrong one: it
|
||||||
refuses the delta the format defines, and 0 of the 25 real RLE8 sources in
|
refuses the delta the format defines, and 0 of the 25 RLE8 BMPs
|
||||||
the R761 delivery would be affected either way (25 of 25 paint every
|
the R761 delivery ships would be affected either way (25 of 25 paint every
|
||||||
pixel, 25 of 25 reach the end of the frame, 0 of 25 use a delta), so the
|
pixel, 25 of 25 reach the end of the frame, 0 of 25 use a delta), so the
|
||||||
corpus cannot choose between them. The independent decoder can.
|
corpus cannot choose between them. The independent decoder can.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue