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    /// Like [`decoded`](Self::decoded), but decoded at most once per document
178    /// load and shared: the cache is keyed by object number and generation.
179    ///
180    /// For data many pages read again unchanged, such as embedded font
181    /// programs. Not for images, whose decode depends on parameters. A stream
182    /// without an object id is decoded each time.
183    pub fn decoded_shared(&self) -> Result<crate::sync::Arc<Vec<u8>>, DecodeFailure> {
184        match self.obj_id() {
185            Some(id) => self
186                .0
187                .dict
188                .ctx()
189                .xref()
190                .decoded_stream(id, || self.decoded()),
191            None => self.decoded().map(crate::sync::Arc::new),
192        }
193    }
194
195    /// Return the decoded data of the stream, and return image metadata
196    /// if available.
197    pub fn decoded_image(
198        &self,
199        image_params: &ImageDecodeParams,
200    ) -> Result<FilterResult, DecodeFailure> {
201        // Carried down to the filters, which have no other way to reach it.
202        let limit_in_force = self.0.dict.ctx().load_limits().image_pixel_limit();
203        let image_params = &ImageDecodeParams {
204            pixel_limit: limit_in_force,
205            ..image_params.clone()
206        };
207
208        if let Some(limit) = limit_in_force
209            && image_params.width > 0
210            && image_params.height > 0
211        {
212            let pixels =
213                u64::from(image_params.width).saturating_mul(u64::from(image_params.height));
214            if pixels > u64::from(limit) {
215                warn!("image pixel count {pixels} exceeds limit {limit}, stopping image decode");
216                return Err(DecodeFailure::ImageDecode);
217            }
218        }
219
220        let data = self.raw_data();
221
222        let mut current: Option<FilterResult> = None;
223
224        for (filter, params) in self.0.filters.iter().zip(self.0.filter_params.iter()) {
225            let new = filter.apply(
226                current.as_ref().map(|c| c.data.as_ref()).unwrap_or(&data),
227                params.clone(),
228                image_params,
229            )?;
230            current = Some(new);
231        }
232
233        let result = current.unwrap_or(FilterResult {
234            data: data.to_vec(),
235            image_data: None,
236        });
237
238        let limit = self.0.stream_byte_limit;
239        if limit != u64::MAX {
240            let observed = result.data.len() as u64;
241            if observed > limit {
242                warn!("decoded stream size {observed} exceeds limit {limit}, stopping decode");
243                return Err(DecodeFailure::StreamTooLarge { observed, limit });
244            }
245        }
246
247        Ok(result)
248    }
249}
250
251impl Debug for Stream<'_> {
252    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
253        write!(f, "Stream (len: {:?})", self.0.data.len())
254    }
255}
256
257impl Skippable for Stream<'_> {
258    fn skip(_: &mut Reader<'_>, _: bool) -> Option<()> {
259        // A stream can never appear in a dict/array, so it should never be skipped.
260        warn!("attempted to skip a stream object");
261
262        None
263    }
264}
265
266impl<'a> Readable<'a> for Stream<'a> {
267    fn read(r: &mut Reader<'a>, ctx: &ReaderContext<'a>) -> Option<Self> {
268        let dict = r.read_with_context::<Dict<'_>>(ctx)?;
269
270        if dict.contains_key(F) {
271            warn!("encountered stream referencing external file, which is unsupported");
272
273            return None;
274        }
275
276        let stream_byte_limit = ctx.load_limits().stream_byte_limit().unwrap_or(u64::MAX);
277        let offset = r.offset();
278        parse_proper(r, &dict, stream_byte_limit)
279            .or_else(|| {
280                warn!("failed to parse stream, trying to parse it manually");
281                crate::leniency::emit(crate::leniency::STREAM_PARSE_FALLBACK);
282                r.jump(offset);
283                parse_fallback(r, &dict, stream_byte_limit)
284            })
285            .error_none("was unable to manually parse the stream")
286    }
287}
288
289#[derive(Debug, Copy, Clone)]
290/// A failure that can occur during decoding a data stream.
291pub enum DecodeFailure {
292    /// An image stream failed to decode.
293    ImageDecode,
294    /// A data stream failed to decode.
295    StreamDecode,
296    /// A failure occurred while decrypting a file.
297    Decryption,
298    /// An unknown failure occurred.
299    Unknown,
300    /// The decoded stream exceeds the configured
301    /// [`PdfLoadLimits::max_stream_bytes`](crate::pdf::PdfLoadLimits) limit.
302    ///
303    /// Unlike the other variants, this is a **hard stop** — callers must not
304    /// silently discard it with `.ok()`. Propagate it as
305    /// `LimitError::StreamTooLarge` / `Error::ResourceLimitExceeded`.
306    StreamTooLarge {
307        /// Decoded byte count that triggered the limit.
308        observed: u64,
309        /// The configured limit.
310        limit: u64,
311    },
312}
313
314/// An image color space.
315#[derive(Debug, Copy, Clone)]
316pub enum ImageColorSpace {
317    /// Grayscale color space.
318    Gray,
319    /// RGB color space.
320    Rgb,
321    /// RGB produced by JPEG YCbCr→RGB decoding.
322    ///
323    /// JPEG images stored with YCbCr encoding (Adobe APP14 transform=1 or
324    /// JFIF default) are converted to sRGB by the JPEG decoder using the
325    /// standard BT.601 matrix. The resulting RGB values are already in sRGB
326    /// colorimetry. Any PDF `/ColorSpace` entry that is not device-RGB (e.g.
327    /// an [`ICCBased`] printer profile) should be ignored for these images;
328    /// the JPEG decoder's own colour model takes precedence, matching MuPDF
329    /// and Acrobat behaviour.
330    RgbFromYCbCr,
331    /// CMYK color space.
332    Cmyk,
333    /// An unknown color space.
334    Unknown(u8),
335}
336
337/// Additional data that is extracted from some image streams.
338pub struct ImageData {
339    /// An optional alpha channel of the image.
340    pub alpha: Option<Vec<u8>>,
341    /// The color space of the image.
342    pub color_space: Option<ImageColorSpace>,
343    /// The bits per component of the image.
344    pub bits_per_component: u8,
345    /// The width of the image.
346    pub width: u32,
347    /// The height of the image.
348    pub height: u32,
349}
350
351/// The result of applying a filter.
352pub struct FilterResult {
353    /// The decoded data.
354    pub data: Vec<u8>,
355    /// Additional data that is extracted from JPX image streams.
356    pub image_data: Option<ImageData>,
357}
358
359impl FilterResult {
360    pub(crate) fn from_data(data: Vec<u8>) -> Self {
361        Self {
362            data,
363            image_data: None,
364        }
365    }
366}
367
368fn parse_proper<'a>(
369    r: &mut Reader<'a>,
370    dict: &Dict<'a>,
371    stream_byte_limit: u64,
372) -> Option<Stream<'a>> {
373    let length = dict.get::<u32>(LENGTH)?;
374
375    r.skip_white_spaces_and_comments();
376    r.forward_tag(b"stream")?;
377    // Skip horizontal whitespace (spaces/tabs) between "stream" keyword and EOL.
378    // Some producers write "stream \r\n" (with a trailing space) which is technically
379    // non-conforming but tolerated by Acrobat and MuPDF.
380    while r.peek_byte().is_some_and(|b| b == b' ' || b == b'\t') {
381        r.forward();
382    }
383    r.forward_tag(b"\n")
384        .or_else(|| r.forward_tag(b"\r\n"))
385        .or_else(|| r.forward_tag(b"\r"))?;
386    let data = r.read_bytes(length as usize)?;
387    r.skip_white_spaces();
388    r.forward_tag(b"endstream")?;
389
390    Some(Stream::new(data, dict.clone(), stream_byte_limit))
391}
392
393fn parse_fallback<'a>(
394    r: &mut Reader<'a>,
395    dict: &Dict<'a>,
396    stream_byte_limit: u64,
397) -> Option<Stream<'a>> {
398    while r.forward_tag(b"stream").is_none() {
399        r.read_byte()?;
400    }
401
402    // Skip any horizontal whitespace between "stream" keyword and EOL (same lenience as
403    // parse_proper — some producers write "stream \r\n").
404    while r.peek_byte().is_some_and(|b| b == b' ' || b == b'\t') {
405        r.forward();
406    }
407    r.forward_tag(b"\n")
408        .or_else(|| r.forward_tag(b"\r\n"))
409        // Technically not allowed, but no reason to not try it.
410        .or_else(|| r.forward_tag(b"\r"))?;
411
412    let data_start = r.tail()?;
413    let start = r.offset();
414
415    loop {
416        if r.peek_byte()?.is_ascii_whitespace() || r.peek_tag(b"endstream").is_some() {
417            let length = r.offset() - start;
418            let data = data_start.get(..length)?;
419
420            r.skip_white_spaces();
421
422            // This was just a whitespace in the data stream but not actually marking the end
423            // of the stream, so continue searching.
424            if r.forward_tag(b"endstream").is_none() {
425                continue;
426            }
427
428            let stream = Stream::new(data, dict.clone(), stream_byte_limit);
429
430            // Seems like we found the end!
431            return Some(stream);
432        } else {
433            r.read_byte()?;
434        }
435    }
436}
437
438impl<'a> TryFrom<Object<'a>> for Stream<'a> {
439    type Error = ();
440
441    fn try_from(value: Object<'a>) -> Result<Self, Self::Error> {
442        match value {
443            Object::Stream(s) => Ok(s),
444            _ => Err(()),
445        }
446    }
447}
448
449impl<'a> ObjectLike<'a> for Stream<'a> {}
450
451#[cfg(test)]
452mod tests {
453    use crate::object::Stream;
454    use crate::pdf::PdfLoadLimits;
455    use crate::reader::Reader;
456    use crate::reader::{ReaderContext, ReaderExt};
457
458    use super::DecodeFailure;
459
460    #[test]
461    fn stream() {
462        let data = b"<< /Length 10 >> stream\nabcdefghij\nendstream";
463        let mut r = Reader::new(data);
464        let stream = r
465            .read_with_context::<Stream<'_>>(&ReaderContext::dummy())
466            .unwrap();
467
468        assert_eq!(stream.0.data, b"abcdefghij");
469    }
470
471    /// `decoded()` on an unfiltered stream succeeds when no byte limit is set.
472    #[test]
473    fn decoded_no_limit() {
474        let data = b"<< /Length 5 >> stream\nhello\nendstream";
475        let mut r = Reader::new(data);
476        let stream = r
477            .read_with_context::<Stream<'_>>(&ReaderContext::dummy())
478            .unwrap();
479
480        let decoded = stream.decoded().unwrap();
481        assert_eq!(decoded, b"hello");
482    }
483
484    /// `decoded()` returns `StreamTooLarge` when the decoded size exceeds the configured limit.
485    #[test]
486    fn decoded_exceeds_byte_limit() {
487        // 10-byte payload, limit of 5 bytes → should fail.
488        let data = b"<< /Length 10 >> stream\nabcdefghij\nendstream";
489        let limits = PdfLoadLimits::new().max_stream_bytes(5);
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        match stream.decoded() {
495            Err(DecodeFailure::StreamTooLarge { observed, limit }) => {
496                assert_eq!(observed, 10);
497                assert_eq!(limit, 5);
498            }
499            other => panic!("expected StreamTooLarge, got {other:?}"),
500        }
501    }
502
503    /// `decoded()` succeeds when the payload exactly equals the byte limit.
504    #[test]
505    fn decoded_at_byte_limit_succeeds() {
506        let data = b"<< /Length 10 >> stream\nabcdefghij\nendstream";
507        let limits = PdfLoadLimits::new().max_stream_bytes(10);
508        let ctx = ReaderContext::dummy_with_limits(limits);
509        let mut r = Reader::new(data);
510        let stream = r.read_with_context::<Stream<'_>>(&ctx).unwrap();
511
512        let decoded = stream.decoded().unwrap();
513        assert_eq!(decoded, b"abcdefghij");
514    }
515
516    /// FlateDecode: a 12-byte compressed payload that expands to 50 bytes must
517    /// be rejected when the configured limit is below the expanded size.
518    ///
519    /// Enforcement happens in decoded_image() after the filter chain: the
520    /// decompressor runs to completion (up to MAX_DECODE_SIZE) and the
521    /// resulting size is compared against the configured limit.
522    #[test]
523    fn flate_decode_exceeds_limit() {
524        // zlib-compressed 50 zero bytes (12-byte compressed form).
525        let compressed: &[u8] = &[
526            0x78, 0x9c, 0x63, 0x60, 0x20, 0x15, 0x00, 0x00, 0x00, 0x32, 0x00, 0x01,
527        ];
528        let length = compressed.len();
529        let mut pdf = format!("<< /Length {length} /Filter /FlateDecode >> stream\n").into_bytes();
530        pdf.extend_from_slice(compressed);
531        pdf.extend_from_slice(b"\nendstream");
532
533        let limits = PdfLoadLimits::new().max_stream_bytes(20); // 50 > 20 → must fail
534        let ctx = ReaderContext::dummy_with_limits(limits);
535        let mut r = Reader::new(&pdf);
536        let stream = r.read_with_context::<Stream<'_>>(&ctx).unwrap();
537
538        match stream.decoded() {
539            Err(DecodeFailure::StreamTooLarge { .. }) => {}
540            other => panic!("expected StreamTooLarge, got {other:?}"),
541        }
542    }
543
544    /// FlateDecode: same stream succeeds when the limit is at or above the
545    /// expanded size (50 bytes).
546    #[test]
547    fn flate_decode_within_limit() {
548        let compressed: &[u8] = &[
549            0x78, 0x9c, 0x63, 0x60, 0x20, 0x15, 0x00, 0x00, 0x00, 0x32, 0x00, 0x01,
550        ];
551        let length = compressed.len();
552        let mut pdf = format!("<< /Length {length} /Filter /FlateDecode >> stream\n").into_bytes();
553        pdf.extend_from_slice(compressed);
554        pdf.extend_from_slice(b"\nendstream");
555
556        let limits = PdfLoadLimits::new().max_stream_bytes(50);
557        let ctx = ReaderContext::dummy_with_limits(limits);
558        let mut r = Reader::new(&pdf);
559        let stream = r.read_with_context::<Stream<'_>>(&ctx).unwrap();
560
561        let decoded = stream.decoded().unwrap();
562        assert_eq!(decoded.len(), 50);
563        assert!(decoded.iter().all(|&b| b == 0));
564    }
565
566    /// FlateDecode: limit exactly one byte below the expanded size must fail.
567    #[test]
568    fn flate_decode_limit_one_below() {
569        let compressed: &[u8] = &[
570            0x78, 0x9c, 0x63, 0x60, 0x20, 0x15, 0x00, 0x00, 0x00, 0x32, 0x00, 0x01,
571        ];
572        let length = compressed.len();
573        let mut pdf = format!("<< /Length {length} /Filter /FlateDecode >> stream\n").into_bytes();
574        pdf.extend_from_slice(compressed);
575        pdf.extend_from_slice(b"\nendstream");
576
577        let limits = PdfLoadLimits::new().max_stream_bytes(49); // 50 > 49 → must fail
578        let ctx = ReaderContext::dummy_with_limits(limits);
579        let mut r = Reader::new(&pdf);
580        let stream = r.read_with_context::<Stream<'_>>(&ctx).unwrap();
581
582        match stream.decoded() {
583            Err(DecodeFailure::StreamTooLarge { .. }) => {}
584            other => panic!("expected StreamTooLarge, got {other:?}"),
585        }
586    }
587
588    /// The wiring, not the check: does the limit reach the filter at all?
589    ///
590    /// The CCITT decoder used to read the limit from its own `/DecodeParms`
591    /// dictionary. When a stream has none that is `Dict::default()`, whose
592    /// context carries no limits -- so the check existed, its unit tests passed,
593    /// and it was unreachable. `Stream::decoded_image` now puts the limit on
594    /// `ImageDecodeParams`, where every filter can see it. (Codex, #1609.)
595    ///
596    /// `/DecodeParms` is present here on purpose. The absent case cannot
597    /// discriminate: `/Columns` then defaults to 1728 and two bytes of data
598    /// never complete a row, so nothing is produced either way. With
599    /// `/Columns 8` the row completes, and the limit either stops it or does not.
600    #[test]
601    fn the_pixel_limit_reaches_the_filter_that_needs_it() {
602        use super::ImageDecodeParams;
603
604        // One row of eight white pixels, Group 3 one-dimensional.
605        let data = b"<< /Length 2 /Filter /CCITTFaxDecode /DecodeParms << /K 0 /Columns 8 /Rows 1 >> >> stream\n\x35\x14\nendstream";
606        let limits = PdfLoadLimits::new().max_image_pixels(4);
607        let mut r = Reader::new(data);
608        let stream = r
609            .read_with_context::<Stream<'_>>(&ReaderContext::dummy_with_limits(limits))
610            .unwrap();
611
612        // Declared 1x1, so the check before decoding is satisfied; the CCITT
613        // parameters ask for 8 pixels against a limit of 4.
614        let params = ImageDecodeParams {
615            width: 1,
616            height: 1,
617            ..Default::default()
618        };
619
620        let produced = stream
621            .decoded_image(&params)
622            .ok()
623            .and_then(|res| res.image_data)
624            .map(|i| u64::from(i.width) * u64::from(i.height));
625        assert!(
626            produced.is_none_or(|pixels| pixels <= 4),
627            "produced {produced:?} pixels under a limit of 4"
628        );
629    }
630}