Skip to main content

pdfrum_parser/
objstm.rs

1//! Object streams: many small objects packed into one compressed stream
2//! (ISO 32000-1 §7.5.7).
3//!
4//! # Two regions, one stream
5//!
6//! The decoded bytes are a header of `N` pairs — object number and offset —
7//! followed at byte `/First` by the object bodies those offsets index into.
8//! Both regions are read leniently, because the header is written by tools
9//! and tools get it wrong:
10//!
11//! - A pair whose object number is not a number is **dropped**, but its
12//!   offset token is still consumed. So one garbage number does not shift
13//!   every following pair — it removes exactly one entry.
14//! - A pair whose *offset* is not a number reads as offset zero and is kept.
15//! - Offsets are unsigned, so a written `-1` becomes 4294967295 and simply
16//!   never resolves.
17//! - Duplicate object numbers are legal, and the cross-reference entry's
18//!   index is what disambiguates them.
19//!
20//! # Why the header can overrun
21//!
22//! Reading stops after `N` pairs or at the end of the data, whichever comes
23//! first — *not* at `/First`. A stream declaring more objects than it holds
24//! therefore reads pairs out of the body region, which is how an object
25//! stream whose `/N` is too large acquires members nobody wrote.
26
27use pdfrum_common::{DiagKind, Diagnostics, Limits, Severity};
28use pdfrum_object::{Object, Resolve, Stream, names};
29
30use crate::lexer::{Lexer, Token, atoui};
31use crate::syntax::{Context, Strictness, body};
32
33/// One member of an object stream.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct ObjStmEntry {
36    /// The object's number.
37    pub num: u32,
38    /// Where its body starts, relative to `/First`.
39    pub offset: u32,
40}
41
42/// A decoded object stream and its member table.
43///
44/// Built once per container and cached by the store: decoding a stream is
45/// expensive and a document reads most of its objects out of a handful of
46/// them.
47#[derive(Debug, Clone)]
48pub struct ObjStm {
49    /// The decoded bytes: header pairs then bodies.
50    data: Vec<u8>,
51    /// Where the bodies begin.
52    first: usize,
53    /// The members, in the order the header listed them.
54    entries: Vec<ObjStmEntry>,
55}
56
57impl ObjStm {
58    /// Validate a stream as an object stream and read its member table.
59    ///
60    /// `None` when the dictionary does not describe one. The checks are all
61    /// non-resolving and all type-exact: `/Type` must be the *name* `ObjStm`
62    /// (a string spelling it is not), and `/N` and `/First` must be literal
63    /// integers, so a float or a reference disqualifies the stream.
64    pub(crate) fn build<R: Resolve + ?Sized>(
65        stream: &Stream,
66        limits: &Limits,
67        diags: &mut Diagnostics,
68        store: &R,
69    ) -> Option<Self> {
70        let dict = &stream.dict;
71        if dict.name(names::TYPE) != Some(names::OBJ_STM) {
72            return None;
73        }
74        let count = literal_int(dict, names::N)?;
75        if count < 0 || count > i64::from(limits.max_object_number) {
76            return None;
77        }
78        let first = literal_int(dict, names::FIRST)?;
79        if first < 0 {
80            return None;
81        }
82
83        let data = crate::decode::structural_bytes(stream, store, limits, diags)?;
84        let entries = read_header(&data, count.cast_unsigned(), limits, diags);
85        Some(Self {
86            data,
87            first: usize::try_from(first).unwrap_or(usize::MAX),
88            entries,
89        })
90    }
91
92    /// The member table, in header order.
93    #[must_use]
94    pub fn entries(&self) -> &[ObjStmEntry] {
95        &self.entries
96    }
97
98    /// Parse the member at `index`, which must also carry object number
99    /// `num`.
100    ///
101    /// Both have to match: the cross-reference entry names each, and a table
102    /// whose index and object number disagree is describing a stream that has
103    /// since been rewritten.
104    pub(crate) fn member<R: Resolve + ?Sized>(
105        &self,
106        num: u32,
107        index: u32,
108        limits: &Limits,
109        diags: &mut Diagnostics,
110        store: &R,
111        depth: u32,
112    ) -> Option<Object> {
113        let entry = self.entries.get(usize::try_from(index).ok()?)?;
114        if entry.num != num {
115            return None;
116        }
117        let at = self
118            .first
119            .checked_add(usize::try_from(entry.offset).ok()?)?;
120        if at >= self.data.len() {
121            return None;
122        }
123        let mut lx = Lexer::at(&self.data, at);
124        let mut ctx = Context {
125            limits,
126            diags,
127            // No shared file: a member's stream payload, were one somehow
128            // present, would have to be copied out of the decoded buffer.
129            file: None,
130            store: Some(store),
131        };
132        body(&mut lx, &mut ctx, Strictness::Loose, depth).ok()
133    }
134}
135
136/// Read a key that must be a literal integer, without resolving.
137fn literal_int(dict: &pdfrum_object::Dict, key: &pdfrum_object::Name) -> Option<i64> {
138    match dict.raw(key)? {
139        Object::Int(v) => Some(*v),
140        // A real is not an integer here, even when it has no fraction.
141        _ => None,
142    }
143}
144
145/// Read up to `count` object-number/offset pairs.
146fn read_header(
147    data: &[u8],
148    count: u64,
149    limits: &Limits,
150    diags: &mut Diagnostics,
151) -> Vec<ObjStmEntry> {
152    let mut entries = Vec::new();
153    let mut lx = Lexer::new(data);
154    for _ in 0..count {
155        if lx.pos() >= data.len() {
156            break;
157        }
158        let num = direct_num(&mut lx, limits);
159        let offset = direct_num(&mut lx, limits);
160        // Object number zero means nothing, and PDFium drops such a pair
161        // while still having consumed its offset — so the pairs that follow
162        // stay aligned.
163        if num == 0 {
164            diags.record(Severity::Suspicious, DiagKind::ObjStmEntryDropped, None);
165            continue;
166        }
167        entries.push(ObjStmEntry { num, offset });
168    }
169    entries
170}
171
172/// Read one number token, or zero when the next word is not one.
173///
174/// The token is consumed either way, which is what keeps a garbage offset
175/// from shifting the pairs that follow it.
176fn direct_num(lx: &mut Lexer<'_>, limits: &Limits) -> u32 {
177    match lx.next_word(limits) {
178        Token::Number(word) => atoui(word),
179        // A name, a keyword, punctuation, or the end of the data: the token
180        // is spent and the value is nothing.
181        _ => 0,
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::{ObjStm, ObjStmEntry};
188    use pdfrum_common::{Diagnostics, Limits};
189    use pdfrum_object::{ByteSpan, Dict, Name, NoResolve, Object, Stream, names};
190
191    /// The shape every C++ golden uses: a sixteen-byte header then three
192    /// bodies — a dictionary, an array, and a number.
193    const NORMAL: &[u8] = b"10 0 11 14 12 21<</Name /Foo>>[1 2 3]4";
194
195    fn dict(pairs: impl IntoIterator<Item = (&'static str, Object)>) -> Dict {
196        Dict::from_pairs(pairs.into_iter().map(|(k, v)| (Name::from(k), v)))
197    }
198
199    fn objstm_dict(count: i64, first: i64) -> Dict {
200        dict([
201            ("Type", Object::Name(names::OBJ_STM.clone())),
202            ("N", Object::Int(count)),
203            ("First", Object::Int(first)),
204        ])
205    }
206
207    fn build(d: Dict, data: &[u8]) -> Option<ObjStm> {
208        let stream = Stream::new(d, ByteSpan::from(data.to_vec()));
209        ObjStm::build(
210            &stream,
211            &Limits::default(),
212            &mut Diagnostics::default(),
213            &NoResolve,
214        )
215    }
216
217    fn entries(o: &ObjStm) -> Vec<(u32, u32)> {
218        o.entries().iter().map(|e| (e.num, e.offset)).collect()
219    }
220
221    fn member(o: &ObjStm, num: u32, index: u32) -> Option<Object> {
222        o.member(
223            num,
224            index,
225            &Limits::default(),
226            &mut Diagnostics::default(),
227            &NoResolve,
228            0,
229        )
230    }
231
232    #[test]
233    fn a_normal_stream_indexes_its_members() {
234        let o = build(objstm_dict(3, 16), NORMAL).expect("object stream");
235        assert_eq!(entries(&o), vec![(10, 0), (11, 14), (12, 21)]);
236        assert!(member(&o, 10, 0).expect("member").as_dict().is_some());
237        assert!(member(&o, 11, 1).expect("member").as_array().is_some());
238        assert!(member(&o, 12, 2).expect("member").as_number().is_some());
239    }
240
241    #[test]
242    fn both_the_index_and_the_number_must_match() {
243        let o = build(objstm_dict(3, 16), NORMAL).expect("object stream");
244        for (num, index) in [
245            (10, 1),
246            (10, 2),
247            (10, 3),
248            (11, 0),
249            (11, 2),
250            (11, 3),
251            (12, 0),
252            (12, 1),
253            (12, 3),
254        ] {
255            assert!(member(&o, num, index).is_none(), "({num}, {index})");
256        }
257    }
258
259    #[test]
260    fn dictionaries_that_do_not_describe_an_object_stream() {
261        // No /Type at all.
262        assert!(build(Dict::new(), NORMAL).is_none());
263        assert!(
264            build(
265                dict([("N", Object::Int(3)), ("First", Object::Int(5))]),
266                NORMAL
267            )
268            .is_none()
269        );
270        // /Type as a string rather than a name.
271        assert!(
272            build(
273                dict([
274                    (
275                        "Type",
276                        Object::Str(pdfrum_object::PdfString::literal(b"ObjStm"))
277                    ),
278                    ("N", Object::Int(3)),
279                    ("First", Object::Int(5)),
280                ]),
281                NORMAL,
282            )
283            .is_none()
284        );
285        // A /Type that is a name but the wrong one.
286        assert!(
287            build(
288                dict([
289                    ("Type", Object::Name(Name::from("ObjStmmmm"))),
290                    ("N", Object::Int(3)),
291                    ("First", Object::Int(5)),
292                ]),
293                NORMAL,
294            )
295            .is_none()
296        );
297    }
298
299    #[test]
300    fn the_count_must_be_a_literal_nonnegative_integer() {
301        let base = |n: Object| {
302            dict([
303                ("Type", Object::Name(names::OBJ_STM.clone())),
304                ("N", n),
305                ("First", Object::Int(5)),
306            ])
307        };
308        // Missing.
309        assert!(
310            build(
311                dict([
312                    ("Type", Object::Name(names::OBJ_STM.clone())),
313                    ("First", Object::Int(5)),
314                ]),
315                NORMAL,
316            )
317            .is_none()
318        );
319        assert!(build(base(Object::Real(2.2)), NORMAL).is_none());
320        assert!(build(base(Object::Int(-1)), NORMAL).is_none());
321        assert!(build(base(Object::Int(999_999_999)), NORMAL).is_none());
322    }
323
324    #[test]
325    fn the_first_offset_must_be_a_literal_nonnegative_integer() {
326        let base = |f: Object| {
327            dict([
328                ("Type", Object::Name(names::OBJ_STM.clone())),
329                ("N", Object::Int(3)),
330                ("First", f),
331            ])
332        };
333        assert!(
334            build(
335                dict([
336                    ("Type", Object::Name(names::OBJ_STM.clone())),
337                    ("N", Object::Int(3)),
338                ]),
339                NORMAL,
340            )
341            .is_none()
342        );
343        assert!(build(base(Object::Real(5.5)), NORMAL).is_none());
344        assert!(build(base(Object::Int(-5)), NORMAL).is_none());
345    }
346
347    #[test]
348    fn a_first_past_the_data_parses_the_table_but_no_members() {
349        // The data is 38 bytes; /First is 39.
350        let o = build(objstm_dict(3, 39), NORMAL).expect("object stream");
351        assert_eq!(entries(&o), vec![(10, 0), (11, 14), (12, 21)]);
352        assert!(member(&o, 10, 0).is_none());
353        assert!(member(&o, 11, 1).is_none());
354        assert!(member(&o, 12, 2).is_none());
355    }
356
357    #[test]
358    fn a_count_smaller_than_the_table_stops_early() {
359        let o = build(objstm_dict(2, 16), NORMAL).expect("object stream");
360        assert_eq!(entries(&o), vec![(10, 0), (11, 14)]);
361        assert!(member(&o, 10, 0).expect("member").as_dict().is_some());
362        assert!(member(&o, 11, 1).expect("member").as_array().is_some());
363        assert!(member(&o, 12, 2).is_none());
364    }
365
366    #[test]
367    fn a_count_larger_than_the_table_reads_into_the_bodies() {
368        // Nine pairs are asked for; the header holds three, and the reader
369        // then picks `2` and `3` out of the array literal `[1 2 3]`.
370        let o = build(objstm_dict(9, 16), NORMAL).expect("object stream");
371        assert_eq!(entries(&o), vec![(10, 0), (11, 14), (12, 21), (2, 3)]);
372        for index in 0..5 {
373            assert!(member(&o, 2, index).is_none());
374        }
375    }
376
377    #[test]
378    fn a_garbage_object_number_drops_exactly_one_pair() {
379        let data = b"10 0 hi 14 12 21<</Name /Foo>>[1 2 3]4";
380        let o = build(objstm_dict(3, 19), data).expect("object stream");
381        assert_eq!(entries(&o), vec![(10, 0), (12, 21)]);
382    }
383
384    #[test]
385    fn a_garbage_offset_reads_as_zero_and_keeps_the_entry() {
386        let data = b"10 0 11 hi 12 21<</Name /Foo>>[1 2 3]4";
387        let o = build(objstm_dict(3, 16), data).expect("object stream");
388        assert_eq!(entries(&o), vec![(10, 0), (11, 0), (12, 21)]);
389        // Both members now parse the same bytes.
390        assert!(member(&o, 10, 0).expect("member").as_dict().is_some());
391        assert!(member(&o, 11, 1).expect("member").as_dict().is_some());
392    }
393
394    #[test]
395    fn a_negative_offset_wraps_into_an_unreachable_one() {
396        let data = b"10 0 11 -1 12 21<</Name /Foo>>[1 2 3]4";
397        let o = build(objstm_dict(3, 16), data).expect("object stream");
398        assert_eq!(
399            o.entries().get(1),
400            Some(&ObjStmEntry {
401                num: 11,
402                offset: 4_294_967_295,
403            })
404        );
405        assert!(member(&o, 11, 1).is_none());
406    }
407
408    #[test]
409    fn an_offset_past_the_data_yields_no_member() {
410        let data = b"10 0 11 999 12 21<</Name /Foo>>[1 2 3]4";
411        let o = build(objstm_dict(3, 17), data).expect("object stream");
412        assert_eq!(entries(&o), vec![(10, 0), (11, 999), (12, 21)]);
413        assert!(member(&o, 11, 1).is_none());
414    }
415
416    #[test]
417    fn duplicate_object_numbers_are_told_apart_by_index() {
418        let data = b"10 0 10 14 12 21<</Name /Foo>>[1 2 3]4";
419        let o = build(objstm_dict(3, 16), data).expect("object stream");
420        assert_eq!(entries(&o), vec![(10, 0), (10, 14), (12, 21)]);
421        assert!(member(&o, 10, 0).expect("member").as_dict().is_some());
422        assert!(member(&o, 10, 1).expect("member").as_array().is_some());
423        assert!(member(&o, 10, 2).is_none());
424        assert!(member(&o, 10, 3).is_none());
425        assert!(member(&o, 12, 2).expect("member").as_number().is_some());
426    }
427
428    #[test]
429    fn object_numbers_need_not_ascend() {
430        let data = b"11 0 12 14 10 21<</Name /Foo>>[1 2 3]4";
431        let o = build(objstm_dict(3, 16), data).expect("object stream");
432        assert_eq!(entries(&o), vec![(11, 0), (12, 14), (10, 21)]);
433        assert!(member(&o, 10, 2).expect("member").as_number().is_some());
434        assert!(member(&o, 11, 0).expect("member").as_dict().is_some());
435        assert!(member(&o, 12, 1).expect("member").as_array().is_some());
436    }
437
438    #[test]
439    fn offsets_need_not_ascend_either() {
440        let data = b"10 21 11 0 12 14<</Name /Foo>>[1 2 3]4";
441        let o = build(objstm_dict(3, 16), data).expect("object stream");
442        assert_eq!(entries(&o), vec![(10, 21), (11, 0), (12, 14)]);
443        assert!(member(&o, 10, 0).expect("member").as_number().is_some());
444        assert!(member(&o, 11, 1).expect("member").as_dict().is_some());
445        assert!(member(&o, 12, 2).expect("member").as_array().is_some());
446    }
447
448    #[test]
449    fn never_panics_on_arbitrary_bytes() {
450        for data in [
451            &b""[..],
452            b"0 0 0 0",
453            b"999999999999 999999999999",
454            b"\xff\x80\x00",
455            b"1",
456        ] {
457            for (n, first) in [(0, 0), (3, 0), (9, 100), (1, 1)] {
458                let _ = build(objstm_dict(n, first), data);
459            }
460        }
461    }
462}