fix(codecs): use exact minimum encoded row length for PCX preflight

This commit is contained in:
Dylan M. Taylor
2026-10-22 19:35:37 -04:00
parent 827d949af9
commit 1bd922f6d1
2 changed files with 34 additions and 3 deletions
+15 -3
View File
@@ -72,11 +72,23 @@ pub(crate) fn decode(b: &[u8], limits: &Limits) -> Result<Image, CodecError> {
return Err(bad("8-bit indexed PCX has no 256-colour palette"));
}
let image_end = if vga { b.len() - 769 } else { b.len() };
// Each RLE packet emits at most 63 bytes and occupies at least two bytes.
// A 63-byte RLE packet takes 2 bytes; a trailing remainder of 1 byte below 0xC0
// can be encoded as a 1-byte literal, while any larger remainder takes at least 2 bytes.
// Because RLE packets may span planes within a scanline (`row_len`), compute the
// minimum encoded bytes across the full scanline (`row_len`) and multiply by `h`.
// Reject inputs that cannot possibly contain all scanlines before allocating
// the decoded image buffer.
let min_packets = stride.div_ceil(63).checked_mul(2).ok_or_else(|| bad("compressed row size overflow"))?;
let min_encoded = min_packets.checked_mul(h as usize).and_then(|n| n.checked_mul(planes)).ok_or_else(|| bad("compressed image size overflow"))?;
let min_packets = (row_len / 63)
.checked_mul(2)
.and_then(|n| {
n.checked_add(match row_len % 63 {
0 => 0,
1 => 1,
_ => 2,
})
})
.ok_or_else(|| bad("compressed row size overflow"))?;
let min_encoded = min_packets.checked_mul(h as usize).ok_or_else(|| bad("compressed image size overflow"))?;
let encoded_len = image_end.saturating_sub(HEADER);
if encoded_len < min_encoded {
return Err(bad("PCX pixel data is too short for the declared image"));
+19
View File
@@ -87,6 +87,25 @@ fn rejects_truncated_and_overshooting_rle_without_panicking() {
let mut b = header(8, 3, 2, 1, 2);
b[66..68].copy_from_slice(&0u16.to_le_bytes());
assert_eq!(detect(&b), None, "invalid stride is not a PCX signature");
// A solid image with stride of the form 63k + 1 (64, 127) encodes as k full 63-byte runs
// plus a 1-byte literal (2k + 1 bytes per plane row), which is valid and must decode.
for width in [64u16, 127] {
let mut solid = header(8, 3, width, 1, width);
for _plane in 0..3 {
let mut rem = usize::from(width);
while rem >= 63 {
solid.extend_from_slice(&[0xff, 0x00]); // 63-byte run of 0x00
rem -= 63;
}
if rem == 1 {
solid.push(0x00); // 1-byte literal 0x00
}
}
let im = decode(&solid).unwrap();
assert_eq!(im.dimensions(), (u32::from(width), 1));
assert!(im.data().iter().all(|&v| v == 0));
}
}
#[test]