Merge remote-tracking branch 'upstream/main' into pdf-reader-sync

# Conflicts:
#	vendor/hayro-syntax/SOURCE.json
This commit is contained in:
aiongg committed 2026-10-24 12:11:27 +00:00
commit fcbf08de14
5 files changed
+96 -16

No files matched your search

+6
View File
@@ -175,6 +175,12 @@ comma. The unit tests of all these items come with them; the CCITT, Type 3 and c
raster tests are adapted in `tests/upstream_raster.rs` and the text contracts in
`tests/upstream_text.rs`. (P8)'s PdfCraft test needs a Japanese substitute font and is not
ported.
- A CCITT image with `/Rows 0` (unknown height) decodes at most the rows that fit the stream's
output limit (counted in 1-bit rows) and 2^28 pixels; a known height is clamped the same way.
Before, it decoded to the end of the data, and a bit of 2D code can repeat a whole row.
- A JPEG 2000 image over 2^28 pixels, or over its output limit (at least 64 MiB), by the
codestream's own dimensions is refused before decoding, and its buffer is reserved fallibly:
the image dictionary's smaller size no longer stands in for the size the decoder allocates.
Paint fixes from the [PdfCraft revision 0b670caf4a090e9d83a7f41a84b4cb2144a2d33e ledger](https://github.com/storytold/pdfcraft/blob/0b670caf4a090e9d83a7f41a84b4cb2144a2d33e/vendor/README.md),
each marked `PdfCraft patch` in the source: hayro-interpret (46) with its hayro counterpart
+4 -4
View File
@@ -146,7 +146,7 @@
"path": "src/filter/ccitt.rs",
"source_sha256": "45d7eb89934d28cff97a00c7b264010bccfd14f3d511e1307ad50a9fef3b53c8",
"source_git_blob": "685d680d2fbadce608a9bd3ca925c2ccc6c5d641",
"sha256": "b0fef2271417c3009a1de569c9573b05dd464ff76c1a5f7220f634bb5feb44e2"
"sha256": "67d17a339639eb0fef4cc6c745b579c6ba9ae3f2f67fd3bacb1bf1389aa5ffab"
},
{
"path": "src/filter/dct.rs",
@@ -162,9 +162,9 @@
},
{
"path": "src/filter/jpx.rs",
"source_sha256": "f0a2b01c692c086d30a26562d7548f9e837af166be737d2c543de7c886a45f82",
"source_sha256": "c9f068629c9cbe79af6793c3c125df4631ca1455691cb1335b44ff43cb1b4056",
"source_git_blob": "74efd8b31e2b2935ff2bbceda2c1ffd36ea9c20d",
"sha256": "5364837caa8ed2abc33a1977b5c91f6895be17c899ae0cc6fe1ebc0723a62448"
"sha256": "dc3742454cfde9a994ce48984e8fcb7b18851efb36ce2d62e31629efea063a1b"
},
{
"path": "src/filter/lzw_flate.rs",
@@ -176,7 +176,7 @@
"path": "src/filter/mod.rs",
"source_sha256": "0af7145ab16b15b7a60c4b57efecb9bd5fd0cfdc9bb5f9388abb695386e63e49",
"source_git_blob": "06bda29487d62c25dc39c1ee109091cb17e3867f",
"sha256": "e33e74ee55dcc850db9eb64811c56b4ffe1932908ab9684dfba07f4277b1eea9"
"sha256": "e43d7d309a4993f4a1f698208997122431b4a8b0db838bb07d4358ccbc00f02f"
},
{
"path": "src/filter/run_length.rs",
+13 -1
View File
@@ -8,10 +8,12 @@ use alloc::vec::Vec;
use core::iter;
use hayro_ccitt::{DecodeSettings, Decoder, DecoderContext, EncodingMode};
/// PdfCraft patch: `limit` is the most bytes the filter may produce (see `decode_limit`).
pub(crate) fn decode(
data: &[u8],
params: &Dict<'_>,
image_params: &ImageDecodeParams,
limit: usize,
) -> Option<FilterResult<'static>> {
let k = params.get::<i32>(K).unwrap_or(0);
@@ -52,6 +54,16 @@ pub(crate) fn decode(
if !super::ccitt_size_ok(settings.columns, settings.rows) {
return None;
}
// PdfCraft patch: the decoder stops after `rows` rows; with `/Rows 0` (unknown) it ran to the
// end of the data, and a bit of 2D code can repeat a whole row, so a small stream produced
// gigabytes. Decode at most the rows that fit the output limit and the pixel cap. The limit
// counts the stream's own 1-bit rows (`decode_limit`), whatever depth the rows are widened to.
let mut settings = settings;
let fit = super::ccitt_max_rows(settings.columns, settings.columns.div_ceil(8), limit);
settings.rows = if settings.rows == 0 { fit } else { settings.rows.min(fit) };
if settings.rows == 0 {
return None;
}
// Whenever possible (if we don't have an indexed color space), we convert
// the data as 8-bit instead of 1-bit, so that it can be easier converted
@@ -181,7 +193,7 @@ mod tests {
#[test]
fn columns_past_u32_max_are_refused_not_wrapped() {
let params = ImageDecodeParams { width: 8, height: 4, ..Default::default() };
let decoded = |dict: &[u8]| decode(&ALL_BLACK_8X4, &Dict::from_bytes(dict).unwrap(), &params);
let decoded = |dict: &[u8]| decode(&ALL_BLACK_8X4, &Dict::from_bytes(dict).unwrap(), &params, 1 << 20);
assert!(decoded(b"<< /K -1 /Columns 8 >>").is_some());
// 2^32 + 8 would wrap to 8 columns.
assert!(decoded(b"<< /K -1 /Columns 4294967304 >>").is_none());
+45 -7
View File
@@ -17,7 +17,15 @@ impl ImageColorSpace {
}
}
pub(crate) fn decode(data: &[u8], params: &ImageDecodeParams) -> Option<FilterResult<'static>> {
/// PdfCraft patch: the most pixels a JPEG 2000 image may decode to, as for JBIG2.
const MAX_PIXELS: u64 = 1 << 28;
/// PdfCraft patch: the least output `limit` a JPEG 2000 image is held to: the limit comes from the
/// image dictionary's size, which a decoder may round differently from the codestream's.
const MIN_LIMIT: usize = 64 << 20;
/// PdfCraft patch: `limit` is the most bytes the filter may produce (see `decode_limit`).
pub(crate) fn decode(data: &[u8], params: &ImageDecodeParams, limit: usize) -> Option<FilterResult<'static>> {
use crate::object::stream::ImageColorSpace;
let settings = DecodeSettings {
@@ -74,7 +82,23 @@ pub(crate) fn decode(data: &[u8], params: &ImageDecodeParams) -> Option<FilterRe
},
};
let has_alpha = image.has_alpha();
let bitmap = image.decode().ok()?;
// PdfCraft patch: the decoder sizes its output from the codestream's own dimensions, not the
// image dictionary's, and allocates it infallibly. Refuse more than `MAX_PIXELS` pixels or
// more than `limit` bytes before decoding, and reserve the buffer fallibly.
if u64::from(width) * u64::from(height) > MAX_PIXELS {
return None;
}
let channels = usize::from(cs.num_components()) + usize::from(has_alpha);
let len = (width as usize).checked_mul(height as usize)?.checked_mul(channels)?;
let output_limit = limit.max(MIN_LIMIT);
if len > output_limit {
return None;
}
let mut bitmap = Vec::new();
bitmap.try_reserve_exact(len).ok()?;
bitmap.resize(len, 0);
let mut context = hayro_jpeg2000::DecoderContext::default();
image.decode_into(&mut bitmap, &mut context).ok()?;
let (mut data, mut alpha) = if !has_alpha {
(bitmap, None)
@@ -98,6 +122,18 @@ pub(crate) fn decode(data: &[u8], params: &ImageDecodeParams) -> Option<FilterRe
// The decoded image is always 8-bit, so if necessary we have to rescale
// ourselves.
if bpc != 8 {
// PdfCraft patch: the rescaled buffer can be larger than the 8-bit bitmap (up to 32 bits
// per component), so bound it, with alpha, by the same output limit.
let bits_per_row = (width as usize)
.checked_mul(usize::from(cs.num_components()))?
.checked_mul(usize::from(bpc))?;
let scaled_len = bits_per_row
.div_ceil(8)
.checked_mul(height as usize)?
.checked_mul(if alpha.is_some() { 2 } else { 1 })?;
if scaled_len > output_limit {
return None;
}
data = scale(&data, bpc, cs.num_components(), width, height)?;
alpha = alpha.and_then(|alpha| scale(&alpha, bpc, cs.num_components(), width, height));
}
@@ -223,7 +259,9 @@ fn scale(
.checked_mul(num_components as usize)?
.checked_mul(bit_per_component as usize)?;
let input_len = bits_per_row.div_ceil(8).checked_mul(height as usize)?;
let mut input = vec![0; input_len];
let mut input = Vec::new();
input.try_reserve_exact(input_len).ok()?;
input.resize(input_len, 0);
let mut writer = BitWriter::new(&mut input, bit_per_component)?;
let components_per_row = (num_components as usize).checked_mul(width as usize)?;
@@ -417,7 +455,7 @@ mod tests {
height: 2,
..ImageDecodeParams::default()
};
let result = decode(PALETTE_JP2, &params).unwrap();
let result = decode(PALETTE_JP2, &params, usize::MAX).unwrap();
let image = result.image_data.unwrap();
assert!(matches!(image.color_space, Some(ImageColorSpace::Rgb)));
let red = [255, 0, 0];
@@ -437,7 +475,7 @@ mod tests {
height: 2,
..ImageDecodeParams::default()
};
let result = decode(PALETTE_JP2, &params).unwrap();
let result = decode(PALETTE_JP2, &params, usize::MAX).unwrap();
assert_eq!(&*result.data, &[0, 1, 0, 1, 1, 0, 1, 0][..]);
}
@@ -479,7 +517,7 @@ mod tests {
..ImageDecodeParams::default()
};
let bpc = |data: &[u8]| {
decode(data, &params)
decode(data, &params, usize::MAX)
.and_then(|r| r.image_data)
.map(|d| d.bits_per_component)
};
@@ -501,7 +539,7 @@ mod tests {
bpc: Some(1),
..params.clone()
};
let palette_bpc = decode(&one_bit_jp2(&palette), &one_bit)
let palette_bpc = decode(&one_bit_jp2(&palette), &one_bit, usize::MAX)
.and_then(|r| r.image_data)
.map(|d| d.bits_per_component);
assert_eq!(palette_bpc, Some(8), "/BitsPerComponent 1 with a palette");
+28 -4
View File
@@ -110,11 +110,11 @@ impl Filter {
#[cfg(feature = "images")]
Self::DctDecode => dct::decode(data, params, image_params).ok_or(DecodeFailure::ImageDecode),
#[cfg(feature = "images")]
Self::CcittFaxDecode => ccitt::decode(data, params, image_params).ok_or(DecodeFailure::ImageDecode),
Self::CcittFaxDecode => ccitt::decode(data, params, image_params, limit).ok_or(DecodeFailure::ImageDecode),
#[cfg(feature = "images")]
Self::Jbig2Decode => jbig2::decode(data, params, image_params).ok_or(DecodeFailure::ImageDecode),
#[cfg(feature = "images")]
Self::JpxDecode => jpx::decode(data, image_params).ok_or(DecodeFailure::ImageDecode),
Self::JpxDecode => jpx::decode(data, image_params, limit).ok_or(DecodeFailure::ImageDecode),
#[cfg(not(feature = "images"))]
Self::DctDecode | Self::CcittFaxDecode | Self::Jbig2Decode | Self::JpxDecode => {
warn!("image decoding is not supported (enable the `images` feature)");
@@ -168,6 +168,30 @@ pub(crate) fn decode_limit(params: &ImageDecodeParams, last: bool, limits: crate
/// real fax lines are a few thousand pixels wide.
pub fn ccitt_size_ok(columns: u32, rows: u32) -> bool {
const MAX_COLUMNS: u32 = 1 << 20;
const MAX_PIXELS: u64 = 1 << 28;
columns > 0 && columns <= MAX_COLUMNS && u64::from(columns) * u64::from(rows.max(1)) <= MAX_PIXELS
columns > 0 && columns <= MAX_COLUMNS && u64::from(columns) * u64::from(rows.max(1)) <= MAX_CCITT_PIXELS
}
/// PdfCraft patch: the most pixels a CCITT image may decode to.
const MAX_CCITT_PIXELS: u64 = 1 << 28;
/// PdfCraft patch: the most rows of `columns` pixels, `row_bytes` bytes each once decoded, that fit
/// both `limit` bytes and the pixel cap; at least one when one row fits.
pub(crate) fn ccitt_max_rows(columns: u32, row_bytes: u32, limit: usize) -> u32 {
let by_pixels = MAX_CCITT_PIXELS / u64::from(columns.max(1));
let by_bytes = u64::try_from(limit).unwrap_or(u64::MAX) / u64::from(row_bytes.max(1));
u32::try_from(by_pixels.min(by_bytes)).unwrap_or(u32::MAX)
}
#[cfg(test)]
mod ccitt_tests {
use super::ccitt_max_rows;
/// PdfCraft patch: an image of unknown height decodes at most the rows that fit both the output
/// limit (in 1-bit rows) and the 2^28-pixel cap.
#[test]
fn unknown_ccitt_rows_fit_the_limit_and_the_pixel_cap() {
assert_eq!(ccitt_max_rows(1728, 216, 216 * 100), 100);
assert_eq!(ccitt_max_rows(1 << 20, 1 << 17, usize::MAX), 1 << 8);
assert_eq!(ccitt_max_rows(1728, 216, 100), 0);
}
}