Skip to main content

pdf_syntax/object/
stream.rs

1//! Streams.
2
3use crate::crypto::DecryptionTarget;
4use crate::filter::Filter;
5use crate::object;
6use crate::object::Dict;
7use crate::object::Name;
8use crate::object::dict::keys::{DECODE_PARMS, DP, F, FILTER, LENGTH, TYPE};
9use crate::object::{Array, ObjectIdentifier};
10use crate::object::{Object, ObjectLike};
11use crate::reader::Reader;
12use crate::reader::{Readable, ReaderContext, ReaderExt, Skippable};
13use crate::sync::Arc;
14use crate::util::OptionLog;
15use alloc::borrow::Cow;
16use alloc::vec::Vec;
17use core::fmt::{Debug, Formatter};
18use log::warn;
19use smallvec::SmallVec;
20
21#[derive(Clone)]
22struct StreamInner<'a> {
23    dict: Dict<'a>,
24    filters: SmallVec<[Filter; 2]>,
25    filter_params: SmallVec<[Dict<'a>; 2]>,
26    data: &'a [u8],
27    /// Maximum decoded size enforced in `decoded()` / `decoded_image()`.
28    /// Comes from [`PdfLoadLimits::stream_byte_limit`] at parse time.
29    /// `u64::MAX` means no limit (same sentinel as [`PdfLoadLimits::max_stream_bytes`]).
30    stream_byte_limit: u64,
31}
32
33/// A stream of arbitrary data.
34#[derive(Clone)]
35pub struct Stream<'a>(Arc<StreamInner<'a>>);
36
37impl PartialEq for Stream<'_> {
38    fn eq(&self, other: &Self) -> bool {
39        self.0.dict == other.0.dict && self.0.data == other.0.data
40    }
41}
42
43/// Additional parameters for decoding images.
44#[derive(Clone, PartialEq, Default)]
45pub struct ImageDecodeParams {
46    /// Whether the color space of the image is an indexed color space.
47    pub is_indexed: bool,
48    /// The bits per component of the image, if that information is available.
49    pub bpc: Option<u8>,
50    /// The components per channel of the image, if that information is available.
51    pub num_components: Option<u8>,
52    /// A target resolution for the image. Note that this is only a hint so that
53    /// in case it's possible, a version of the image will be extracted that
54    /// is as close as possible to the hinted dimension.
55    pub target_dimension: Option<(u32, u32)>,
56    /// The width of the image as indicated by the image dictionary.
57    pub width: u32,
58    /// The height of the image as indicated by the image dictionary.
59    pub height: u32,
60    /// The image pixel limit in force, if one is configured.
61    ///
62    /// Set by [`Stream::decoded_image`] from the stream's own context, not by
63    /// the caller. A filter cannot read it for itself: the only dictionary it
64    /// receives is `/DecodeParms`, which is `Dict::default()` when the stream
65    /// has none -- and that carries a context with no limits at all. The CCITT
66    /// decoder's own limit check was reading exactly that, so a stream without
67    /// `/DecodeParms` skipped it entirely. (Codex, #1609.)
68    pub pixel_limit: Option<u32>,
69}
70
71impl<'a> Stream<'a> {
72    pub(crate) fn new(data: &'a [u8], dict: Dict<'a>, stream_byte_limit: u64) -> Self {
73        let mut collected_filters = SmallVec::new();
74        let mut collected_params = SmallVec::new();
75
76        if let Some(filter) = dict
77            .get::<Name>(F)
78            .or_else(|| dict.get::<Name>(FILTER))
79            .and_then(Filter::from_name)
80        {
81            let params = dict
82                .get::<Dict<'_>>(DP)
83                .or_else(|| dict.get::<Dict<'_>>(DECODE_PARMS))
84                .unwrap_or_default();
85
86            collected_filters.push(filter);
87            collected_params.push(params);
88        } else if let Some(filters) = dict
89            .get::<Array<'_>>(F)
90            .or_else(|| dict.get::<Array<'_>>(FILTER))
91        {
92            let filters = filters.iter::<Name>().map(Filter::from_name);
93            let mut params = dict
94                .get::<Array<'_>>(DP)
95                .or_else(|| dict.get::<Array<'_>>(DECODE_PARMS))
96                .map(|a| a.iter::<Object<'_>>());
97
98            for filter in filters {
99                let params = params
100                    .as_mut()
101                    .and_then(|p| p.next())
102                    .and_then(|p| p.into_dict())
103                    .unwrap_or_default();
104
105                if let Some(filter) = filter {
106                    collected_filters.push(filter);
107                    collected_params.push(params);
108                }
109            }
110        }
111
112        Self(Arc::new(StreamInner {
113            dict,
114            filters: collected_filters,
115            filter_params: collected_params,
116            data,
117            stream_byte_limit,
118        }))
119    }
120
121    /// Return the raw, decrypted data of the stream.
122    ///
123    /// Stream filters will not be applied.
124    pub fn raw_data(&self) -> Cow<'a, [u8]> {
125        let ctx = self.0.dict.ctx();
126
127        if ctx.xref().needs_decryption(ctx)
128            && self
129                .0
130                .dict
131                .get::<object::String>(TYPE)
132                .map(|t| t.as_ref() != b"XRef")
133                .unwrap_or(true)
134        {
135            // Streams are always indirect objects and therefore always have an obj_id.
136            // If somehow absent (corrupt PDF), fall back to raw data.
137            if let Some(obj_id) = self.0.dict.obj_id() {
138                Cow::Owned(
139                    ctx.xref()
140                        .decrypt(obj_id, self.0.data, DecryptionTarget::Stream)
141                        .unwrap_or_default(),
142                )
143            } else {
144                Cow::Borrowed(self.0.data)
145            }
146        } else {
147            Cow::Borrowed(self.0.data)
148        }
149    }
150
151    /// Return the raw, underlying dictionary of the stream.
152    pub fn dict(&self) -> &Dict<'a> {
153        &self.0.dict
154    }
155
156    /// Return the object identifier of the stream, if available.
157    ///
158    /// Returns `None` if the stream is corrupt and lacks an object ID.
159    pub fn obj_id(&self) -> Option<ObjectIdentifier> {
160        self.0.dict.obj_id()
161    }
162
163    /// Return the filters that are applied to the stream.
164    pub fn filters(&self) -> &[Filter] {
165        &self.0.filters
166    }
167
168    /// Return the decoded data of the stream.
169    ///
170    /// Note that the result of this method will not be cached, so calling it multiple
171    /// times is expensive.
172    pub fn decoded(&self) -> Result<Vec<u8>, DecodeFailure> {
173        self.decoded_image(&ImageDecodeParams::default())
174            .map(|r| r.data)
175    }
176
177    /// Return the decoded data of the stream, and return image metadata
178    /// if available.
179    pub fn decoded_image(
180        &self,
181        image_params: &ImageDecodeParams,
182    ) -> Result<FilterResult, DecodeFailure> {
183        // Carried down to the filters, which have no other way to reach it.
184        let limit_in_force = self.0.dict.ctx().load_limits().image_pixel_limit();
185        let image_params = &ImageDecodeParams {
186            pixel_limit: limit_in_force,
187            ..image_params.clone()
188        };
189
190        if let Some(limit) = limit_in_force
191            && image_params.width > 0
192            && image_params.height > 0
193        {
194            let pixels =
195                u64::from(image_params.width).saturating_mul(u64::from(image_params.height));
196            if pixels > u64::from(limit) {
197                warn!("image pixel count {pixels} exceeds limit {limit}, stopping image decode");
198                return Err(DecodeFailure::ImageDecode);
199            }
200        }
201
202        let data = self.raw_data();
203
204        let mut current: Option<FilterResult> = None;
205
206        for (filter, params) in self.0.filters.iter().zip(self.0.filter_params.iter()) {
207            let new = filter.apply(
208                current.as_ref().map(|c| c.data.as_ref()).unwrap_or(&data),
209                params.clone(),
210                image_params,
211            )?;
212            current = Some(new);
213        }
214
215        let result = current.unwrap_or(FilterResult {
216            data: data.to_vec(),
217            image_data: None,
218        });
219
220        let limit = self.0.stream_byte_limit;
221        if limit != u64::MAX {
222            let observed = result.data.len() as u64;
223            if observed > limit {
224                warn!("decoded stream size {observed} exceeds limit {limit}, stopping decode");
225                return Err(DecodeFailure::StreamTooLarge { observed, limit });
226            }
227        }
228
229        Ok(result)
230    }
231}
232
233impl Debug for Stream<'_> {
234    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
235        write!(f, "Stream (len: {:?})", self.0.data.len())
236    }
237}
238
239impl Skippable for Stream<'_> {
240    fn skip(_: &mut Reader<'_>, _: bool) -> Option<()> {
241        // A stream can never appear in a dict/array, so it should never be skipped.
242        warn!("attempted to skip a stream object");
243
244        None
245    }
246}
247
248impl<'a> Readable<'a> for Stream<'a> {
249    fn read(r: &mut Reader<'a>, ctx: &ReaderContext<'a>) -> Option<Self> {
250        let dict = r.read_with_context::<Dict<'_>>(ctx)?;
251
252        if dict.contains_key(F) {
253            warn!("encountered stream referencing external file, which is unsupported");
254
255            return None;
256        }
257
258        let stream_byte_limit = ctx.load_limits().stream_byte_limit().unwrap_or(u64::MAX);
259        let offset = r.offset();
260        parse_proper(r, &dict, stream_byte_limit)
261            .or_else(|| {
262                warn!("failed to parse stream, trying to parse it manually");
263                crate::leniency::emit(crate::leniency::STREAM_PARSE_FALLBACK);
264                r.jump(offset);
265                parse_fallback(r, &dict, stream_byte_limit)
266            })
267            .error_none("was unable to manually parse the stream")
268    }
269}
270
271#[derive(Debug, Copy, Clone)]
272/// A failure that can occur during decoding a data stream.
273pub enum DecodeFailure {
274    /// An image stream failed to decode.
275    ImageDecode,
276    /// A data stream failed to decode.
277    StreamDecode,
278    /// A failure occurred while decrypting a file.
279    Decryption,
280    /// An unknown failure occurred.
281    Unknown,
282    /// The decoded stream exceeds the configured
283    /// [`PdfLoadLimits::max_stream_bytes`](crate::pdf::PdfLoadLimits) limit.
284    ///
285    /// Unlike the other variants, this is a **hard stop** — callers must not
286    /// silently discard it with `.ok()`. Propagate it as
287    /// `LimitError::StreamTooLarge` / `Error::ResourceLimitExceeded`.
288    StreamTooLarge {
289        /// Decoded byte count that triggered the limit.
290        observed: u64,
291        /// The configured limit.
292        limit: u64,
293    },
294}
295
296/// An image color space.
297#[derive(Debug, Copy, Clone)]
298pub enum ImageColorSpace {
299    /// Grayscale color space.
300    Gray,
301    /// RGB color space.
302    Rgb,
303    /// RGB produced by JPEG YCbCr→RGB decoding.
304    ///
305    /// JPEG images stored with YCbCr encoding (Adobe APP14 transform=1 or
306    /// JFIF default) are converted to sRGB by the JPEG decoder using the
307    /// standard BT.601 matrix. The resulting RGB values are already in sRGB
308    /// colorimetry. Any PDF `/ColorSpace` entry that is not device-RGB (e.g.
309    /// an [`ICCBased`] printer profile) should be ignored for these images;
310    /// the JPEG decoder's own colour model takes precedence, matching MuPDF
311    /// and Acrobat behaviour.
312    RgbFromYCbCr,
313    /// CMYK color space.
314    Cmyk,
315    /// An unknown color space.
316    Unknown(u8),
317}
318
319/// Additional data that is extracted from some image streams.
320pub struct ImageData {
321    /// An optional alpha channel of the image.
322    pub alpha: Option<Vec<u8>>,
323    /// The color space of the image.
324    pub color_space: Option<ImageColorSpace>,
325    /// The bits per component of the image.
326    pub bits_per_component: u8,
327    /// The width of the image.
328    pub width: u32,
329    /// The height of the image.
330    pub height: u32,
331}
332
333/// The result of applying a filter.
334pub struct FilterResult {
335    /// The decoded data.
336    pub data: Vec<u8>,
337    /// Additional data that is extracted from JPX image streams.
338    pub image_data: Option<ImageData>,
339}
340
341impl FilterResult {
342    pub(crate) fn from_data(data: Vec<u8>) -> Self {
343        Self {
344            data,
345            image_data: None,
346        }
347    }
348}
349
350fn parse_proper<'a>(
351    r: &mut Reader<'a>,
352    dict: &Dict<'a>,
353    stream_byte_limit: u64,
354) -> Option<Stream<'a>> {
355    let length = dict.get::<u32>(LENGTH)?;
356
357    r.skip_white_spaces_and_comments();
358    r.forward_tag(b"stream")?;
359    // Skip horizontal whitespace (spaces/tabs) between "stream" keyword and EOL.
360    // Some producers write "stream \r\n" (with a trailing space) which is technically
361    // non-conforming but tolerated by Acrobat and MuPDF.
362    while r.peek_byte().is_some_and(|b| b == b' ' || b == b'\t') {
363        r.forward();
364    }
365    r.forward_tag(b"\n")
366        .or_else(|| r.forward_tag(b"\r\n"))
367        .or_else(|| r.forward_tag(b"\r"))?;
368    let data = r.read_bytes(length as usize)?;
369    r.skip_white_spaces();
370    r.forward_tag(b"endstream")?;
371
372    Some(Stream::new(data, dict.clone(), stream_byte_limit))
373}
374
375fn parse_fallback<'a>(
376    r: &mut Reader<'a>,
377    dict: &Dict<'a>,
378    stream_byte_limit: u64,
379) -> Option<Stream<'a>> {
380    while r.forward_tag(b"stream").is_none() {
381        r.read_byte()?;
382    }
383
384    // Skip any horizontal whitespace between "stream" keyword and EOL (same lenience as
385    // parse_proper — some producers write "stream \r\n").
386    while r.peek_byte().is_some_and(|b| b == b' ' || b == b'\t') {
387        r.forward();
388    }
389    r.forward_tag(b"\n")
390        .or_else(|| r.forward_tag(b"\r\n"))
391        // Technically not allowed, but no reason to not try it.
392        .or_else(|| r.forward_tag(b"\r"))?;
393
394    let data_start = r.tail()?;
395    let start = r.offset();
396
397    loop {
398        if r.peek_byte()?.is_ascii_whitespace() || r.peek_tag(b"endstream").is_some() {
399            let length = r.offset() - start;
400            let data = data_start.get(..length)?;
401
402            r.skip_white_spaces();
403
404            // This was just a whitespace in the data stream but not actually marking the end
405            // of the stream, so continue searching.
406            if r.forward_tag(b"endstream").is_none() {
407                continue;
408            }
409
410            let stream = Stream::new(data, dict.clone(), stream_byte_limit);
411
412            // Seems like we found the end!
413            return Some(stream);
414        } else {
415            r.read_byte()?;
416        }
417    }
418}
419
420impl<'a> TryFrom<Object<'a>> for Stream<'a> {
421    type Error = ();
422
423    fn try_from(value: Object<'a>) -> Result<Self, Self::Error> {
424        match value {
425            Object::Stream(s) => Ok(s),
426            _ => Err(()),
427        }
428    }
429}
430
431impl<'a> ObjectLike<'a> for Stream<'a> {}
432
433#[cfg(test)]
434mod tests {
435    use crate::object::Stream;
436    use crate::pdf::PdfLoadLimits;
437    use crate::reader::Reader;
438    use crate::reader::{ReaderContext, ReaderExt};
439
440    use super::DecodeFailure;
441
442    #[test]
443    fn stream() {
444        let data = b"<< /Length 10 >> stream\nabcdefghij\nendstream";
445        let mut r = Reader::new(data);
446        let stream = r
447            .read_with_context::<Stream<'_>>(&ReaderContext::dummy())
448            .unwrap();
449
450        assert_eq!(stream.0.data, b"abcdefghij");
451    }
452
453    /// `decoded()` on an unfiltered stream succeeds when no byte limit is set.
454    #[test]
455    fn decoded_no_limit() {
456        let data = b"<< /Length 5 >> stream\nhello\nendstream";
457        let mut r = Reader::new(data);
458        let stream = r
459            .read_with_context::<Stream<'_>>(&ReaderContext::dummy())
460            .unwrap();
461
462        let decoded = stream.decoded().unwrap();
463        assert_eq!(decoded, b"hello");
464    }
465
466    /// `decoded()` returns `StreamTooLarge` when the decoded size exceeds the configured limit.
467    #[test]
468    fn decoded_exceeds_byte_limit() {
469        // 10-byte payload, limit of 5 bytes → should fail.
470        let data = b"<< /Length 10 >> stream\nabcdefghij\nendstream";
471        let limits = PdfLoadLimits::new().max_stream_bytes(5);
472        let ctx = ReaderContext::dummy_with_limits(limits);
473        let mut r = Reader::new(data);
474        let stream = r.read_with_context::<Stream<'_>>(&ctx).unwrap();
475
476        match stream.decoded() {
477            Err(DecodeFailure::StreamTooLarge { observed, limit }) => {
478                assert_eq!(observed, 10);
479                assert_eq!(limit, 5);
480            }
481            other => panic!("expected StreamTooLarge, got {other:?}"),
482        }
483    }
484
485    /// `decoded()` succeeds when the payload exactly equals the byte limit.
486    #[test]
487    fn decoded_at_byte_limit_succeeds() {
488        let data = b"<< /Length 10 >> stream\nabcdefghij\nendstream";
489        let limits = PdfLoadLimits::new().max_stream_bytes(10);
490        let ctx = ReaderContext::dummy_with_limits(limits);
491        let mut r = Reader::new(data);
492        let stream = r.read_with_context::<Stream<'_>>(&ctx).unwrap();
493
494        let decoded = stream.decoded().unwrap();
495        assert_eq!(decoded, b"abcdefghij");
496    }
497
498    /// FlateDecode: a 12-byte compressed payload that expands to 50 bytes must
499    /// be rejected when the configured limit is below the expanded size.
500    ///
501    /// Enforcement happens in decoded_image() after the filter chain: the
502    /// decompressor runs to completion (up to MAX_DECODE_SIZE) and the
503    /// resulting size is compared against the configured limit.
504    #[test]
505    fn flate_decode_exceeds_limit() {
506        // zlib-compressed 50 zero bytes (12-byte compressed form).
507        let compressed: &[u8] = &[
508            0x78, 0x9c, 0x63, 0x60, 0x20, 0x15, 0x00, 0x00, 0x00, 0x32, 0x00, 0x01,
509        ];
510        let length = compressed.len();
511        let mut pdf = format!("<< /Length {length} /Filter /FlateDecode >> stream\n").into_bytes();
512        pdf.extend_from_slice(compressed);
513        pdf.extend_from_slice(b"\nendstream");
514
515        let limits = PdfLoadLimits::new().max_stream_bytes(20); // 50 > 20 → must fail
516        let ctx = ReaderContext::dummy_with_limits(limits);
517        let mut r = Reader::new(&pdf);
518        let stream = r.read_with_context::<Stream<'_>>(&ctx).unwrap();
519
520        match stream.decoded() {
521            Err(DecodeFailure::StreamTooLarge { .. }) => {}
522            other => panic!("expected StreamTooLarge, got {other:?}"),
523        }
524    }
525
526    /// FlateDecode: same stream succeeds when the limit is at or above the
527    /// expanded size (50 bytes).
528    #[test]
529    fn flate_decode_within_limit() {
530        let compressed: &[u8] = &[
531            0x78, 0x9c, 0x63, 0x60, 0x20, 0x15, 0x00, 0x00, 0x00, 0x32, 0x00, 0x01,
532        ];
533        let length = compressed.len();
534        let mut pdf = format!("<< /Length {length} /Filter /FlateDecode >> stream\n").into_bytes();
535        pdf.extend_from_slice(compressed);
536        pdf.extend_from_slice(b"\nendstream");
537
538        let limits = PdfLoadLimits::new().max_stream_bytes(50);
539        let ctx = ReaderContext::dummy_with_limits(limits);
540        let mut r = Reader::new(&pdf);
541        let stream = r.read_with_context::<Stream<'_>>(&ctx).unwrap();
542
543        let decoded = stream.decoded().unwrap();
544        assert_eq!(decoded.len(), 50);
545        assert!(decoded.iter().all(|&b| b == 0));
546    }
547
548    /// FlateDecode: limit exactly one byte below the expanded size must fail.
549    #[test]
550    fn flate_decode_limit_one_below() {
551        let compressed: &[u8] = &[
552            0x78, 0x9c, 0x63, 0x60, 0x20, 0x15, 0x00, 0x00, 0x00, 0x32, 0x00, 0x01,
553        ];
554        let length = compressed.len();
555        let mut pdf = format!("<< /Length {length} /Filter /FlateDecode >> stream\n").into_bytes();
556        pdf.extend_from_slice(compressed);
557        pdf.extend_from_slice(b"\nendstream");
558
559        let limits = PdfLoadLimits::new().max_stream_bytes(49); // 50 > 49 → must fail
560        let ctx = ReaderContext::dummy_with_limits(limits);
561        let mut r = Reader::new(&pdf);
562        let stream = r.read_with_context::<Stream<'_>>(&ctx).unwrap();
563
564        match stream.decoded() {
565            Err(DecodeFailure::StreamTooLarge { .. }) => {}
566            other => panic!("expected StreamTooLarge, got {other:?}"),
567        }
568    }
569
570    /// The wiring, not the check: does the limit reach the filter at all?
571    ///
572    /// The CCITT decoder used to read the limit from its own `/DecodeParms`
573    /// dictionary. When a stream has none that is `Dict::default()`, whose
574    /// context carries no limits -- so the check existed, its unit tests passed,
575    /// and it was unreachable. `Stream::decoded_image` now puts the limit on
576    /// `ImageDecodeParams`, where every filter can see it. (Codex, #1609.)
577    ///
578    /// `/DecodeParms` is present here on purpose. The absent case cannot
579    /// discriminate: `/Columns` then defaults to 1728 and two bytes of data
580    /// never complete a row, so nothing is produced either way. With
581    /// `/Columns 8` the row completes, and the limit either stops it or does not.
582    #[test]
583    fn the_pixel_limit_reaches_the_filter_that_needs_it() {
584        use super::ImageDecodeParams;
585
586        // One row of eight white pixels, Group 3 one-dimensional.
587        let data = b"<< /Length 2 /Filter /CCITTFaxDecode /DecodeParms << /K 0 /Columns 8 /Rows 1 >> >> stream\n\x35\x14\nendstream";
588        let limits = PdfLoadLimits::new().max_image_pixels(4);
589        let mut r = Reader::new(data);
590        let stream = r
591            .read_with_context::<Stream<'_>>(&ReaderContext::dummy_with_limits(limits))
592            .unwrap();
593
594        // Declared 1x1, so the check before decoding is satisfied; the CCITT
595        // parameters ask for 8 pixels against a limit of 4.
596        let params = ImageDecodeParams {
597            width: 1,
598            height: 1,
599            ..Default::default()
600        };
601
602        let produced = stream
603            .decoded_image(&params)
604            .ok()
605            .and_then(|res| res.image_data)
606            .map(|i| u64::from(i.width) * u64::from(i.height));
607        assert!(
608            produced.is_none_or(|pixels| pixels <= 4),
609            "produced {produced:?} pixels under a limit of 4"
610        );
611    }
612}