diff --git a/crates/cos/src/object.rs b/crates/cos/src/object.rs index f2c9760c12..48a23c4af2 100644 --- a/crates/cos/src/object.rs +++ b/crates/cos/src/object.rs @@ -453,6 +453,12 @@ impl Stream { self.decode_chain(max, decode).map(|(v, _)| v).map_err(|(e, _)| CosError::Filter(e.to_string())) } + /// [`Stream::decoded_strict`] for a caller with less than [`MAX_DECODED`] left to spend: + /// decoding past `max` bytes (in any filter stage) is an error. + pub fn decoded_strict_within(&self, max: usize) -> Result, CosError> { + self.decoded_strict_within_counted(max).map(|(v, _)| v).map_err(|(e, _)| CosError::Filter(e.to_string())) + } + /// [`Stream::decoded_strict`] bounded by `max` and counted like /// [`Stream::decoded_within_counted`]. pub(crate) fn decoded_strict_within_counted(&self, max: usize) -> Result<(Vec, usize), (pdfcraft_filters::FilterError, usize)> { @@ -746,6 +752,13 @@ mod tests { } } + #[test] + fn strict_decoding_is_bounded_by_the_callers_remaining_allowance() { + let stream = Stream::flate(Dict::new(), &vec![0u8; 1 << 20]); + assert!(matches!(stream.decoded_strict_within(1000), Err(CosError::Filter(_)))); + assert_eq!(stream.decoded_strict_within(1 << 20).map(|v| v.len()).ok(), Some(1 << 20)); + } + #[test] fn empty_filter_arrays_refuse_decoding_past_a_caller_limit() { let mut dict = Dict::new(); diff --git a/crates/optimize/src/unused.rs b/crates/optimize/src/unused.rs index b8c8a62b23..002d50263f 100644 --- a/crates/optimize/src/unused.rs +++ b/crates/optimize/src/unused.rs @@ -168,7 +168,9 @@ fn plain_metadata(doc: &Document) -> bool { .is_some_and(|e| matches!(e.as_dict().and_then(|e| e.get(b"EncryptMetadata")), Some(Object::Bool(false)))) } -fn data(s: &Stream, num: u32, g: &Graph, encrypted: bool, plain_metadata: bool) -> Data { +/// `left` is what the caller may still decode: a stream that would decode past it (in any filter +/// stage) is `Unknown`, so no stage is allowed to grow beyond the shared budget first. +fn data(s: &Stream, num: u32, g: &Graph, encrypted: bool, plain_metadata: bool, left: usize) -> Data { let d = &s.dict; // Streams the security handler leaves as stored, which a viewer may still decrypt. let ty = d.name(b"Type"); @@ -219,7 +221,7 @@ fn data(s: &Stream, num: u32, g: &Graph, encrypted: bool, plain_metadata: bool) if !parms_line_up { return Data::Unknown; } - s.decoded_strict().map_or(Data::Unknown, Data::Decoded) + s.decoded_strict_within(left).map_or(Data::Unknown, Data::Decoded) } /// Drop the image and form XObjects that `pages` list in their own resources but no stream @@ -254,7 +256,7 @@ pub(crate) fn remove_unused(doc: &mut Document, pages: &[ObjRef], progress: &mut } let obj = doc.get(ObjRef::new(*num, doc.generation(*num))); let Object::Stream(s) = &*obj else { continue }; - match data(s, *num, &g, encrypted, plain_metadata) { + match data(s, *num, &g, encrypted, plain_metadata, DECODE_BUDGET.saturating_sub(decoded)) { Data::Picture => {} Data::Decoded(bytes) => { decoded = decoded.saturating_add(bytes.len());