mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 01:26:53 +00:00
fix(create): decode an exported image or mask no further than its samples need
Image export decoded every lossless image and mask with the generic 1 GiB allowance, so a few KB of Flate on an image of one pixel could inflate to a gigabyte before unpack read the first byte. Each stage is now bounded by what the image's size, components and depth need, as Optimize and redaction already do.
This commit is contained in:
@@ -136,8 +136,26 @@ fn filters(s: &Stream) -> Vec<Vec<u8>> {
|
||||
/// through its decoder too, behind other filters as well, a JBIG2 image with its `/JBIG2Globals`.
|
||||
/// A JBIG2 page has to be the image's size, which the caller checked against the export's limit,
|
||||
/// before the page is decoded.
|
||||
fn decoded(doc: &Document, s: &Stream, budget: &mut ImageBudget) -> Result<Vec<u8>, String> {
|
||||
budget.decode(doc, s, MAX_DECODED).map_err(|e| e.to_string())
|
||||
///
|
||||
/// No stage may produce more than [`allowance`] gives for an image of `w` × `h` with `n`
|
||||
/// components of `bpc` bits: what is past the samples is never read, so a few KB of Flate can't
|
||||
/// ask for a gigabyte for an image of one pixel.
|
||||
fn decoded(doc: &Document, s: &Stream, (w, h, n, bpc): (usize, usize, usize, usize), budget: &mut ImageBudget) -> Result<Vec<u8>, String> {
|
||||
let max = allowance(s, w, h, n, bpc).ok_or("the image is too large to export")?;
|
||||
budget.decode(doc, s, max).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// How far image `s` of `w` × `h` with `n` components of `bpc` bits is decoded, as Optimize and
|
||||
/// redaction allow: up to three bytes a sample byte and a PNG predictor's byte a row (ASCIIHex text
|
||||
/// under Flate, its digits spaced), or, for a fax image, the code its decoder reads, at most about a
|
||||
/// byte a pixel and each row's end. More can only be a decompression bomb. Never less than the
|
||||
/// stream's own bytes, which are in memory already, nor more than [`MAX_DECODED`].
|
||||
fn allowance(s: &Stream, w: usize, h: usize, n: usize, bpc: usize) -> Option<usize> {
|
||||
let max = match s.filters().last() {
|
||||
Some((pdfcraft_filters::Filter::CcittFax, _)) => w.checked_mul(h)?.checked_add(h.checked_mul(4)?)?,
|
||||
_ => w.checked_mul(n)?.checked_mul(bpc)?.div_ceil(8).checked_mul(h)?.checked_add(h)?.checked_mul(3)?,
|
||||
};
|
||||
Some(max.checked_add(4096)?.max(s.raw.len()).min(MAX_DECODED))
|
||||
}
|
||||
|
||||
/// The file for one image: its JPEG data as is, or a PNG. An image with transparency (a soft
|
||||
@@ -199,7 +217,6 @@ fn image(doc: &Document, s: &Stream, budget: &mut ImageBudget) -> Result<(&'stat
|
||||
space = decoded;
|
||||
(samples, 8)
|
||||
} else {
|
||||
let data = decoded(doc, s, budget)?;
|
||||
let bpc = if mask { 1 } else { s.dict.int(b"BitsPerComponent").unwrap_or(8) };
|
||||
let bpc = match bpc {
|
||||
1 => 1,
|
||||
@@ -209,6 +226,7 @@ fn image(doc: &Document, s: &Stream, budget: &mut ImageBudget) -> Result<(&'stat
|
||||
16 => 16,
|
||||
other => return Err(format!("{other} bits per component isn't supported")),
|
||||
};
|
||||
let data = decoded(doc, s, (w, h, space.components(), bpc), budget)?;
|
||||
(unpack(&data, w, h, space.components(), bpc).ok_or("the image data is shorter than its size says")?, bpc)
|
||||
};
|
||||
let n = space.components();
|
||||
@@ -567,7 +585,7 @@ fn gray_plane(doc: &Document, m: &Stream, w: usize, h: usize, budget: &mut Image
|
||||
_ => return None,
|
||||
};
|
||||
let max = (1u32 << bpc.min(8)) - 1;
|
||||
unpack(&decoded(doc, m, budget).ok()?, mw, mh, 1, bpc)?.into_iter().map(|v| (v as u32 * 255 / max) as u8).collect()
|
||||
unpack(&decoded(doc, m, (mw, mh, 1, bpc), budget).ok()?, mw, mh, 1, bpc)?.into_iter().map(|v| (v as u32 * 255 / max) as u8).collect()
|
||||
}
|
||||
};
|
||||
if let Some([lo, hi]) = decode(doc, m, 1).and_then(|pairs| pairs.first().copied()) {
|
||||
|
||||
@@ -844,6 +844,32 @@ fn damaged_masks_and_sizes_never_panic() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_data_far_past_its_samples_is_not_decoded() {
|
||||
// A grey image of one pixel whose Flate stream inflates to 64 MiB: only the first byte is a
|
||||
// sample, so the export stops decoding long before the rest and leaves the image out.
|
||||
let bomb = |w: i64, h: i64, len: usize| {
|
||||
let mut doc = from_images(&[("flat.png".into(), png_bytes(false))]).unwrap();
|
||||
with_image(&mut doc, |_| Stream::flate(image_dict(w, h, "DeviceGray", 8), &vec![7u8; len]));
|
||||
doc
|
||||
};
|
||||
let out = extract_images(&bomb(1, 1, 64 << 20), &[0], 0);
|
||||
assert!(out.images.is_empty() && out.skipped.len() == 1, "{out:?}");
|
||||
// Data a little past the samples, as encoders leave, still exports.
|
||||
let (ext, rgba) = exported_rgba(&bomb(4, 2, 4 * 2 + 64));
|
||||
assert_eq!((ext, rgba), ("png", [7, 7, 7, 255].repeat(8)));
|
||||
|
||||
// The same for a soft mask: it is not decoded, and the image exports without it.
|
||||
let mut doc = from_images(&[("flat.png".into(), png_bytes(false))]).unwrap();
|
||||
with_image(&mut doc, |doc| {
|
||||
let m = doc.add(Object::Stream(Stream::flate(image_dict(1, 1, "DeviceGray", 8), &vec![0u8; 64 << 20])));
|
||||
let mut d = image_dict(4, 2, "DeviceRGB", 8);
|
||||
d.set(b"SMask".to_vec(), Object::Ref(m));
|
||||
Stream::from_raw(d, vec![10; 24])
|
||||
});
|
||||
assert_eq!(alphas(&exported_rgba(&doc).1), [255; 8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indexed_image_export_clamps_samples_to_hival() {
|
||||
for palette in [vec![255, 0, 0, 0, 0, 255], vec![255, 0, 0, 0, 0, 255, 0, 255, 0, 255, 255, 255]] {
|
||||
|
||||
Reference in New Issue
Block a user