ICO: gate AND-mask handling on the embedded BMP's real bit depth, not ICONDIRENTRY#3057
Conversation
…RENTRY
The decision to ignore the AND mask for true-color icons was keyed on the
ICONDIRENTRY bits-per-pixel field, which is unreliable: for ICO it may be 0
("unspecified", then guessed from bColorCount), and for CUR that field holds
the hotspot Y coordinate. The authoritative value is the embedded BMP's real
biBitCount.
As a result, a genuine 32bpp icon whose ICONDIRENTRY bpp resolves to <32 took
the low-bit-depth branch and applied the AND mask to a true-color image,
zeroing its native alpha.
Add a pub(crate) `BmpDecoder::source_bit_count()` accessor exposing the parsed
biBitCount and gate the mask-ignore decision (and the sibling lenient
truncated-mask acceptance) on it instead of `selected_entry.bits_per_pixel`.
Adds a regression test: the existing 32bpp conflicting-AND-mask fixture with its
ICONDIRENTRY wBitCount rewritten to "unspecified" (0) must still preserve its
native alpha.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thank you, but... In general, before searching for bugs and making fixes, it's a good idea to look at existing issues and PRs. 😅 See: #3047. Funnily enough, the two fixes are near identical. The main differences are that the AI liked to write (probably needlessly) verbose comments and that the added tests are different. And the main difference for the added tests are that this PR uses an icon entry bit count of 0 while #3047 uses 8. (Note that I picked 8 to try and trick the decoder. A decoder might trust plausible values (1, 4, 8, 16, 24, 32) even when it shouldn't.) So we are unfortunately now in the situation where I can't meaningfully review this PR. I'd basically review my own code and any changes I want to request would only change your PR into my PR. (Because I like my own PR better, of course :) ) So.... @197g, which one do you want to pick? This PR and #3047 are basically the same. Really the one thing that needs changing here is the bit count in the test (any plausible value <32 will do).
Very good disclaimer 👍 |
|
You're right, this overlaps #3047 and I should have checked the open PRs first. Closing it. Thanks for the catch. |
Follow-up to #3056. After that merged, @RunDevelopment kindly invited more ICO edge cases ("if you found more edge cases that aren't handled properly, feel free to let us know, either by opening an issue or providing a fix"), so here is one.
The bug
In
src/codecs/ico/decoder.rs, the decision to ignore theANDmask for true-color icons is keyed on theICONDIRENTRYbits-per-pixel field (selected_entry.bits_per_pixel):That field is unreliable:
wBitCountmay be0("unspecified"), in which case the decoder guesses a bit depth frombColorCount(seeread_entry). A genuine 32bpp icon authored withwBitCount = 0therefore resolves to a low guessed depth.The authoritative bit depth is the embedded BMP's real
biBitCount, which is already parsed intoBmpDecoder's privatebit_countbut had no accessor.Consequence: a true-32bpp icon whose
ICONDIRENTRYbpp resolves to<32wrongly takes the low-bit-depth branch and applies theANDmask to a true-color image, zeroing its native alpha.The fix
pub(crate) fn source_bit_count(&self) -> u16accessor onBmpDecoderexposing the parsedbiBitCount(mirrors the existing#[cfg(feature = "ico")]reader()accessor).source_bit_countinstead ofselected_entry.bits_per_pixel.Test
Added
bmp_32bpp_unspecified_entry_bpp_and_mask_ignored. It takes the existingbmp-32bpp-conflicting-and-mask.icofixture (a genuine 32bpp icon whoseANDmask, if applied, would zero alpha), rewrites itsICONDIRENTRYwBitCountto0("unspecified" — still a valid 32bpp icon), and asserts the native alpha (128) is preserved. This test fails onmain(alpha comes back0) and passes with the fix.Verified locally:
cargo test --no-default-features --features ico— all ICO tests pass, including the existingbmp_32bpp_and_mask_ignoredandtruncated_mask_32bpp_lenient(both fixtures have realbiBitCount == 32, so behavior is unchanged for them).cargo test --no-default-features --features icoBMP tests — all pass.clippyandrustfmt --checkclean on both edited files;--features bmp(withoutico) still builds (accessor iscfg-gated).Disclaimer: this change was prepared with AI assistance; I reviewed the diff, re-verified the bug, and ran the tests myself before opening this.