Skip to main content

pdfboss_core/
objstm.rs

1//! Object streams (ISO 32000 §7.5.7): compressed objects stored inside a
2//! `/Type/ObjStm` stream.
3
4use crate::error::{Error, Result};
5use crate::lexer::{Lexer, Token};
6use crate::object::Object;
7use crate::parser::{NoResolve, Parser};
8
9/// A decoded object stream with its header parsed once.
10///
11/// The header is `2*n` integers — pairs of object number and byte offset —
12/// followed by the object bodies; offsets are relative to `first`, where the
13/// first body begins. Parsing the header up front turns per-object extraction
14/// from an O(n) rescan (O(n^2) across a whole stream) into a direct lookup.
15pub struct ObjStm {
16    data: Vec<u8>,
17    first: usize,
18    /// Body offset (relative to `first`) for each of the `n` entries, in order.
19    offsets: Vec<usize>,
20}
21
22impl ObjStm {
23    /// Decodes the header of an object stream, keeping the decompressed bytes
24    /// for later per-object parsing.
25    pub fn parse(data: Vec<u8>, n: usize, first: usize) -> Result<ObjStm> {
26        let offsets = {
27            let mut lexer = Lexer::new(&data);
28            let mut offsets = Vec::with_capacity(n);
29            for _ in 0..n {
30                expect_int(&mut lexer)?; // object number (position implies index)
31                offsets.push(expect_int(&mut lexer)?);
32            }
33            offsets
34        };
35        Ok(ObjStm {
36            data,
37            first,
38            offsets,
39        })
40    }
41
42    /// Parses the object at `index` from the already-decoded bytes.
43    pub fn object(&self, index: u32) -> Result<Object> {
44        self.object_spanned(index).map(|pair| pair.0)
45    }
46
47    /// Parses the object at `index`, also reporting its byte range within
48    /// the decoded stream data.
49    pub fn object_spanned(&self, index: u32) -> Result<(Object, (usize, usize))> {
50        let offset = *self.offsets.get(index as usize).ok_or_else(|| {
51            Error::Other(format!(
52                "object stream index {index} out of range (N = {})",
53                self.offsets.len()
54            ))
55        })?;
56        let pos = self
57            .first
58            .checked_add(offset)
59            .filter(|&p| p <= self.data.len())
60            .ok_or_else(|| {
61                Error::Other(format!(
62                    "object stream offset {offset} lies outside the stream"
63                ))
64            })?;
65        let mut parser = Parser::at(&self.data, pos);
66        let object = parser.parse_object(&NoResolve)?;
67        Ok((object, (pos, parser.pos())))
68    }
69}
70
71/// Extracts the object at `index` from decoded object-stream data in one shot
72/// (parses the header, then the object). Callers that read many objects from
73/// the same stream should build an [`ObjStm`] once and reuse it.
74pub fn extract(stream_data: &[u8], n: usize, first: usize, index: u32) -> Result<Object> {
75    if index as usize >= n {
76        return Err(Error::Other(format!(
77            "object stream index {index} out of range (N = {n})"
78        )));
79    }
80    ObjStm::parse(stream_data.to_vec(), n, first)?.object(index)
81}
82
83/// Reads one non-negative integer from the object-stream header.
84fn expect_int(lexer: &mut Lexer) -> Result<usize> {
85    match lexer.next_token()? {
86        Token::Int(v) if v >= 0 => Ok(v as usize),
87        _ => Err(Error::Syntax {
88            offset: lexer.pos(),
89            msg: "malformed object stream header".to_string(),
90        }),
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    /// Builds decoded object-stream bytes for `(num, body)` pairs; returns
99    /// `(data, n, first)`.
100    fn build_stream(objects: &[(u32, &str)]) -> (Vec<u8>, usize, usize) {
101        let mut header = String::new();
102        let mut bodies = String::new();
103        for (num, body) in objects {
104            header.push_str(&format!("{} {} ", num, bodies.len()));
105            bodies.push_str(body);
106            bodies.push(' ');
107        }
108        let first = header.len();
109        header.push_str(&bodies);
110        (header.into_bytes(), objects.len(), first)
111    }
112
113    #[test]
114    fn extracts_both_objects() {
115        let (data, n, first) = build_stream(&[(11, "<< /A 1 >>"), (12, "(hi)")]);
116        let obj0 = extract(&data, n, first, 0).unwrap();
117        assert_eq!(obj0.as_dict().unwrap().get_int("A"), Some(1));
118        let obj1 = extract(&data, n, first, 1).unwrap();
119        assert_eq!(obj1, Object::String(b"hi".to_vec()));
120    }
121
122    #[test]
123    fn extracts_from_testkit_payload() {
124        let (_, payload) = pdfboss_testkit::objstm_payload(&[(3, "[1 2 3]"), (9, "/Name")]);
125        // Header of 2*n integers; /First is where the first body starts.
126        let n = 2;
127        let first = payload.iter().position(|&b| b == b'\n').unwrap() + 1;
128        let array = extract(&payload, n, first, 0).unwrap();
129        assert_eq!(array.as_array().unwrap().len(), 3);
130        let name = extract(&payload, n, first, 1).unwrap();
131        assert_eq!(name.as_name().map(|nm| nm.0.as_str()), Some("Name"));
132    }
133
134    #[test]
135    fn index_out_of_range_errors() {
136        let (data, n, first) = build_stream(&[(1, "true"), (2, "null")]);
137        assert!(extract(&data, n, first, 2).is_err());
138        assert!(extract(&data, n, first, u32::MAX).is_err());
139    }
140
141    #[test]
142    fn malformed_header_errors() {
143        assert!(extract(b"/NotAnInt 0 true", 1, 12, 0).is_err());
144        assert!(extract(b"5 -3 true", 1, 5, 0).is_err());
145    }
146
147    #[test]
148    fn offset_beyond_data_errors() {
149        assert!(extract(b"1 999\ntrue", 1, 6, 0).is_err());
150    }
151
152    #[test]
153    fn object_spanned_reports_reparseable_range() {
154        let (data, n, first) = build_stream(&[(11, "<< /A 1 >>"), (12, "(hi)")]);
155        let stm = ObjStm::parse(data.clone(), n, first).unwrap();
156        for index in 0..2u32 {
157            let (object, (start, end)) = stm.object_spanned(index).unwrap();
158            assert!(start >= first && end <= data.len() && start < end);
159            let reparsed = Parser::at(&data, start).parse_object(&NoResolve).unwrap();
160            assert_eq!(reparsed, object);
161            assert_eq!(stm.object(index).unwrap(), object);
162        }
163    }
164}