mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-26 02:58:32 -06:00
fix(measure): a failed content decode uses up the snap byte budget
Snap geometry charged decoded bytes only on success, so a /Contents array repeating one stream that inflates past the 16 MiB limit ran that inflation for every entry without spending any budget. A tolerant decode fails only on an unsupported filter or after producing the remaining budget, so a failure now spends the rest of the budget and later parts are skipped as truncated.
This commit is contained in:
@@ -214,14 +214,14 @@ impl Walker<'_> {
|
||||
self.geometry.truncated = true;
|
||||
break;
|
||||
}
|
||||
// An undecodable part is skipped; the rest of the page still snaps.
|
||||
// An undecodable part is skipped; the parts before it still snap.
|
||||
match s.decoded_within(remaining) {
|
||||
Ok(data) => {
|
||||
joined.extend(data);
|
||||
// A space, as the renderer joins pieces: a trailing comment runs on.
|
||||
joined.push(b' ');
|
||||
}
|
||||
Err(_) => self.geometry.unreadable = self.geometry.unreadable.saturating_add(1),
|
||||
Err(_) => self.decode_failed(remaining),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,11 +236,17 @@ impl Walker<'_> {
|
||||
match stream.decoded_within(remaining) {
|
||||
Ok(data) => self.ops(&data, resources, initial, depth),
|
||||
Err(_) => {
|
||||
self.geometry.unreadable = self.geometry.unreadable.saturating_add(1);
|
||||
self.decode_failed(remaining);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
/// A tolerant decode fails on an unsupported filter or once it has produced `remaining` bytes,
|
||||
/// so a failure uses up the byte budget: repeating a failing stream cannot repeat that work.
|
||||
fn decode_failed(&mut self, remaining: usize) {
|
||||
self.geometry.unreadable = self.geometry.unreadable.saturating_add(1);
|
||||
self.bytes = self.bytes.saturating_add(remaining);
|
||||
}
|
||||
fn ops(&mut self, data: &[u8], resources: &Dict, initial: Matrix, depth: usize) -> Result<()> {
|
||||
self.bytes = self.bytes.saturating_add(data.len());
|
||||
self.streams = self.streams.saturating_add(1);
|
||||
|
||||
@@ -423,3 +423,16 @@ fn listing_skips_repeated_refs_and_caps_what_it_keeps() {
|
||||
assert!(listing.measurements.iter().map(retained).sum::<usize>() <= MAX_LISTED_BYTES);
|
||||
assert!(!listing.measurements.is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn a_stream_over_the_byte_budget_uses_it_up_once() {
|
||||
// 50 references to one Flate stream that inflates past the 16 MiB budget decode once.
|
||||
let big = pdfcraft_cos::Stream::flate(Dict::new(), &vec![b' '; 17 << 20]);
|
||||
let hex: String = big.raw.iter().map(|b| format!("{b:02x}")).collect();
|
||||
let bomb = String::from_utf8(pdfcraft_testkit::stream_obj("/Filter [/ASCIIHexDecode /FlateDecode]", hex)).unwrap();
|
||||
let contents = format!("[4 0 R{}]", " 5 0 R".repeat(50));
|
||||
let d = fixture_with(&contents, "0 0 m 10 0 l S", "", &[bomb.as_str()]);
|
||||
let g = snap::geometry(&d, 0).unwrap();
|
||||
assert_eq!(g.unreadable, 1);
|
||||
assert!(g.truncated);
|
||||
assert_eq!(g.segments.len(), 1, "the parts before it still snap");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user