`level_data` asked `bytes.get(off..off + sz)` and turned a miss into
`Error::OutOfBounds`, which fails the whole decode. Two faults in one line: a
level table may name a span past the file, and `off + sz` may overflow — the
sizes that do this are wrapped values like `0xFFFFD0C4`, which on a 32-bit
`usize` wraps the sum back inside the buffer and hands the caller another
level's bytes.
Now a deeper level that cannot be sliced ends the chain, as a zero size or
offset already does; level 0 still errors, because there is no image without it.
The sum is `checked_add`.
Measured over this install's chain (138,540 BLP headers, read independently of
this decoder): 316 copies declare a level whose span runs past the file. 282 of
those levels sit past `max(log2 w, log2 h)`, so the decoder never reads them.
34 do not — Troll facial hair and `item\TextureComponents\` armour art at
128x32 with a wrapped level 6 — and every one of those 34 is shadowed by an
intact copy in a higher patch, so nothing on this install decodes differently
today. What the fix buys is that the layering is no longer load-bearing: the
shadowing is a property of which patches this realm happens to ship, and a
level that runs past the file now costs the levels below it rather than the
image.
Ported from benilla-bourbon (tripleslash/benilla-bourbon, "a BLP mip past the
file's end ends the chain"), which measured it on patch-A's
`Character\Troll\SCALPUPPERHAIR00_01.blp` — level 6 at the file's end over six
intact levels. That file is in this chain too, where `Patch-P.mpq` shadows it
with an intact 99,476-byte copy. The test is theirs, unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn