review: keep the mask of an over-chunked JPEG and size the stripped buffer from the kept bytes

This commit is contained in:
Dylan M. Taylor committed 2026-11-08 11:34:13 -05:00
1 parent 2ffbf23fb2
commit db690aaded
3 files changed
+28 -7

No files matched your search

+9
View File
@@ -329,6 +329,15 @@ fn jpeg_samples(data: &[u8], space: &Space, w: usize, h: usize) -> Option<(Vec<u
Space::Cmyk => return cmyk_jpeg_samples(data, w, h).map(|px| (px, Space::Cmyk)),
_ => {}
}
// Past the decoder's metadata bound, drop the Extended XMP chunks rather than the mask:
// decoded as it stands, an over-chunked JPEG is written as is, without its /SMask.
let stripped;
let data = if crate::upright_image::decodable_metadata(data) {
data
} else {
stripped = crate::upright_image::without_extended_xmp(data);
&stripped[..]
};
let img = jpeg_of_size(data, w, h)?;
Some(match space {
Space::Gray => (img.into_luma8().into_raw(), Space::Gray),
+11 -5
View File
@@ -845,10 +845,11 @@ fn damaged_masks_and_sizes_never_panic() {
}
#[test]
fn a_jpeg_with_thousands_of_extended_xmp_chunks_is_exported_without_decoding() {
// Export decodes a JPEG with a soft mask, or a JPEG soft mask, to make a PNG with alpha: a JPEG
// whose Extended XMP chunks would take the decoder quadratic time is written as it is instead,
// and such a mask is left out, without reaching the decoder.
fn a_jpeg_with_thousands_of_extended_xmp_chunks_keeps_its_mask() {
// Export decodes a JPEG with a soft mask, or a JPEG soft mask, to make a PNG with alpha. A JPEG
// whose Extended XMP chunks would take the decoder quadratic time is decoded without them,
// which is linear in what is left, so the mask survives; a JPEG mask past the same bound is
// still left out, without reaching the decoder.
let flood = crate::upright_image::tests::with_extended_xmp_chunks(&real_jpeg(), 5_000);
let mut doc = from_images(&[("flat.png".into(), png_bytes(false))]).unwrap();
with_image(&mut doc, |doc| {
@@ -860,7 +861,12 @@ fn a_jpeg_with_thousands_of_extended_xmp_chunks_is_exported_without_decoding() {
});
crate::extract::JPEGS_DECODED.with(|n| n.set(0));
let out = extract_images(&doc, &[0], 0);
assert_eq!((out.images[0].extension, &out.images[0].data), ("jpg", &flood));
assert_eq!(out.images[0].extension, "png");
assert_eq!(crate::extract::JPEGS_DECODED.with(std::cell::Cell::get), 1);
// The chunks themselves are gone, so the decoder never saw them.
assert!(!out.images[0].data.windows(5).any(|w| w == b"http:"));
crate::extract::JPEGS_DECODED.with(|n| n.set(0));
let mut doc = from_images(&[("flat.png".into(), png_bytes(false))]).unwrap();
with_image(&mut doc, |doc| {
+8 -2
View File
@@ -82,7 +82,11 @@ fn extended_xmp_chunks(bytes: &[u8]) -> usize {
/// JPEG `bytes` without their Extended XMP chunks, which nothing here reads: what is left decodes
/// in time linear in its size however many chunks the file carried.
pub(crate) fn without_extended_xmp(bytes: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(bytes.len());
// Only the bytes that are kept: a CMYK JPEG can be mostly Extended XMP, and reserving the
// whole file for the little that remains aborts the export under a tight address-space limit.
let mut removed = 0usize;
each_extended_xmp(bytes, |r| removed = removed.saturating_add(r.len()));
let mut out = Vec::with_capacity(bytes.len().saturating_sub(removed));
let mut at = 0;
each_extended_xmp(bytes, |r| {
out.extend_from_slice(bytes.get(at..r.start).unwrap_or_default());
@@ -114,7 +118,9 @@ fn each_extended_xmp(bytes: &[u8], mut f: impl FnMut(std::ops::Range<usize>)) {
let Some(&[hi, lo]) = bytes.get(i.saturating_add(2)..i.saturating_add(4)) else { break };
let len = usize::from(u16::from_be_bytes([hi, lo]));
let end = i.saturating_add(2).saturating_add(len.max(2));
if marker == 0xE1 && bytes.get(i.saturating_add(4)..).is_some_and(|seg| seg.starts_with(PREFIX)) {
// The prefix must start the APP1 segment's own payload: a zero-length header
// followed by prefix bytes is not a chunk.
if marker == 0xE1 && bytes.get(i.saturating_add(4)..end.min(bytes.len())).is_some_and(|seg| seg.starts_with(PREFIX)) {
f(i..end.min(bytes.len()));
}
i = end;