Skip to main content

stet_pdf_reader/
resolver.rs

1// stet-pdf-reader
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Indirect object resolution with lazy caching and stream decompression.
6
7use std::cell::RefCell;
8use std::collections::{HashMap, HashSet};
9
10use crate::error::PdfError;
11use crate::filters;
12use crate::lexer::{Lexer, Token, parse_object};
13use crate::objects::{PdfDict, PdfObj};
14use crate::xref::{XrefEntry, XrefTable};
15
16/// Cached decompressed ObjStm data + parsed header.
17struct ObjStmCache {
18    data: Vec<u8>,
19    first: usize,
20    offsets: Vec<(u32, usize)>,
21}
22
23/// Resolves indirect object references, caching parsed objects.
24pub struct Resolver<'a> {
25    data: &'a [u8],
26    xref: XrefTable,
27    /// Cache of parsed objects, populated on demand.
28    cache: RefCell<HashMap<u32, PdfObj>>,
29    /// Guard against circular references.
30    resolving: RefCell<HashSet<u32>>,
31    /// Encryption state, if the PDF is encrypted.
32    encryption: Option<crate::crypto::EncryptionState>,
33    /// Cache of decompressed stream data by object number.
34    /// Small streams (≤1MB) are cached immediately; larger streams use
35    /// "cache on second sight" to avoid caching one-off large images
36    /// while still caching reused ones (e.g., Type 3 emoji glyphs).
37    stream_cache: RefCell<HashMap<u32, Vec<u8>>>,
38    /// Tracks obj_nums that have been decoded at least once (for large streams).
39    stream_seen: RefCell<HashSet<u32>>,
40    /// Cache of decompressed object streams (ObjStm).
41    /// Avoids re-decompressing the same ObjStm for every object within it.
42    objstm_cache: RefCell<HashMap<u32, ObjStmCache>>,
43    /// Cached scan map: obj_num → file offset of last `N 0 obj` marker.
44    /// Built once on first xref miss, then reused for all subsequent lookups.
45    scan_map: RefCell<Option<HashMap<u32, usize>>>,
46}
47
48impl<'a> Resolver<'a> {
49    /// Create a temporary resolver without encryption (for resolving the
50    /// Encrypt dict before encryption state is known).
51    pub(crate) fn new(data: &'a [u8], xref: &XrefTable) -> Self {
52        Self {
53            data,
54            xref: xref.clone(),
55            cache: RefCell::new(HashMap::new()),
56            resolving: RefCell::new(HashSet::new()),
57            encryption: None,
58            stream_cache: RefCell::new(HashMap::new()),
59            stream_seen: RefCell::new(HashSet::new()),
60            objstm_cache: RefCell::new(HashMap::new()),
61            scan_map: RefCell::new(None),
62        }
63    }
64
65    /// Create a resolver with optional encryption state.
66    pub fn with_encryption(
67        data: &'a [u8],
68        xref: XrefTable,
69        encryption: Option<crate::crypto::EncryptionState>,
70    ) -> Self {
71        Self {
72            data,
73            xref,
74            cache: RefCell::new(HashMap::new()),
75            resolving: RefCell::new(HashSet::new()),
76            encryption,
77            stream_cache: RefCell::new(HashMap::new()),
78            stream_seen: RefCell::new(HashSet::new()),
79            objstm_cache: RefCell::new(HashMap::new()),
80            scan_map: RefCell::new(None),
81        }
82    }
83
84    /// Pre-decompress and parse all object streams (ObjStm) in the PDF.
85    /// This populates the object cache for all objects stored in ObjStms,
86    /// avoiding per-object decompression during page rendering.
87    pub fn preload_object_streams(&self) {
88        let mut stream_nums: HashSet<u32> = HashSet::new();
89        for entry in self.xref.entries() {
90            if let Some(XrefEntry::InStream { stream_obj_num, .. }) = entry {
91                stream_nums.insert(*stream_obj_num);
92            }
93        }
94        for &stm_num in &stream_nums {
95            let _ = self.ensure_objstm_cached(stm_num);
96            // After caching, eagerly parse all objects
97            let cache = self.objstm_cache.borrow();
98            if let Some(cached) = cache.get(&stm_num) {
99                let offsets = cached.offsets.clone();
100                let first = cached.first;
101                drop(cache);
102                for &(num, off) in &offsets {
103                    if !self.cache.borrow().contains_key(&num) {
104                        let abs = first + off;
105                        let stm = self.objstm_cache.borrow();
106                        if let Some(c) = stm.get(&stm_num) {
107                            if abs < c.data.len() {
108                                let mut l = Lexer::new(&c.data[abs..]);
109                                if let Ok(o) = parse_object(&mut l) {
110                                    drop(stm);
111                                    self.cache.borrow_mut().insert(num, o);
112                                }
113                            }
114                        }
115                    }
116                }
117            }
118        }
119    }
120
121    /// Resolve an indirect reference to its parsed object.
122    pub fn resolve(&self, obj_num: u32, _gen_num: u16) -> Result<PdfObj, PdfError> {
123        // Check cache first
124        if let Some(obj) = self.cache.borrow().get(&obj_num) {
125            return Ok(obj.clone());
126        }
127
128        // Circular reference guard
129        if !self.resolving.borrow_mut().insert(obj_num) {
130            return Err(PdfError::CircularReference(obj_num, _gen_num));
131        }
132
133        let result = self.resolve_uncached(obj_num, _gen_num);
134
135        self.resolving.borrow_mut().remove(&obj_num);
136
137        let obj = result?;
138        self.cache.borrow_mut().insert(obj_num, obj.clone());
139        Ok(obj)
140    }
141
142    fn resolve_uncached(&self, obj_num: u32, gen_num: u16) -> Result<PdfObj, PdfError> {
143        let entry = self
144            .xref
145            .get(obj_num)
146            .ok_or(PdfError::ObjectNotFound { obj_num, gen_num })?;
147
148        let obj = match *entry {
149            XrefEntry::InFile { offset, .. } => {
150                // Try the xref offset first; if it's corrupt (e.g. from a
151                // broken incremental update), fall back to scanning the file
152                // for the real "N G obj" header.
153                match self.parse_object_at(offset, Some(obj_num)) {
154                    Ok(obj) => obj,
155                    Err(_) => self.scan_for_object(obj_num)?,
156                }
157            }
158            XrefEntry::InStream {
159                stream_obj_num,
160                index_within,
161            } => {
162                // Objects in object streams are not individually encrypted
163                return self.parse_object_from_stream(stream_obj_num, index_within);
164            }
165            XrefEntry::Free => return Err(PdfError::ObjectNotFound { obj_num, gen_num }),
166        };
167
168        // Decrypt strings in the parsed object (stream data is decrypted separately)
169        Ok(self.decrypt_object(obj, obj_num, gen_num))
170    }
171
172    /// If obj is a Ref, resolve it. Otherwise return as-is.
173    pub fn deref(&self, obj: &PdfObj) -> Result<PdfObj, PdfError> {
174        match obj {
175            PdfObj::Ref(n, g) => self.resolve(*n, *g),
176            other => Ok(other.clone()),
177        }
178    }
179
180    /// Resolve an object and return its decompressed stream data.
181    pub fn stream_data(&self, obj_num: u32, gen_num: u16) -> Result<Vec<u8>, PdfError> {
182        // Check stream cache first
183        if let Some(cached) = self.stream_cache.borrow().get(&obj_num) {
184            return Ok(cached.clone());
185        }
186
187        let obj = self.resolve(obj_num, gen_num)?;
188        match obj {
189            PdfObj::Stream {
190                dict,
191                data_offset,
192                data_len,
193            } => {
194                let raw_slice = &self.data[data_offset..data_offset + data_len];
195                // Decrypt stream data if encrypted
196                let raw = if let Some(ref enc) = self.encryption {
197                    enc.decrypt_stream(raw_slice, obj_num, gen_num)
198                } else {
199                    raw_slice.to_vec()
200                };
201                let (filter_list, parms) = filters::parse_filters(&dict, Some(self))?;
202                let result = if filter_list.is_empty() {
203                    Ok(raw)
204                } else {
205                    let jbig2_globals = self.resolve_jbig2_globals(&filter_list, &parms)?;
206                    filters::decode_stream_bounded(
207                        &raw,
208                        &filter_list,
209                        &parms,
210                        jbig2_globals.as_deref(),
211                        filters::DecodeBudget::for_stream(&dict),
212                    )
213                }?;
214
215                // Cache small streams immediately. For large streams, use
216                // "cache on second sight": first access just marks it as seen,
217                // second access caches it. This avoids caching one-off large
218                // images while caching reused ones (e.g., Type 3 emoji glyphs).
219                if result.len() <= 1_048_576 {
220                    self.stream_cache
221                        .borrow_mut()
222                        .insert(obj_num, result.clone());
223                } else if self.stream_seen.borrow().contains(&obj_num) {
224                    self.stream_cache
225                        .borrow_mut()
226                        .insert(obj_num, result.clone());
227                } else {
228                    self.stream_seen.borrow_mut().insert(obj_num);
229                }
230                Ok(result)
231            }
232            _ => Err(PdfError::Other(format!(
233                "object {obj_num} {gen_num} is not a stream"
234            ))),
235        }
236    }
237
238    /// Get the raw (pre-filter) stream bytes and filter list for an object.
239    /// Used for peeking at JPX metadata before full decode.
240    pub fn raw_stream_and_filters(
241        &self,
242        obj: &PdfObj,
243    ) -> Result<(Vec<u8>, Vec<filters::Filter>), PdfError> {
244        let (obj_num, gen_num) = match obj {
245            PdfObj::Ref(n, g) => (*n, *g),
246            _ => return Err(PdfError::Other("expected Ref".into())),
247        };
248        let resolved = self.resolve(obj_num, gen_num)?;
249        match resolved {
250            PdfObj::Stream {
251                dict,
252                data_offset,
253                data_len,
254            } => {
255                let raw_slice = &self.data[data_offset..data_offset + data_len];
256                let raw = if let Some(ref enc) = self.encryption {
257                    enc.decrypt_stream(raw_slice, obj_num, gen_num)
258                } else {
259                    raw_slice.to_vec()
260                };
261                let (filter_list, _parms) = filters::parse_filters(&dict, Some(self))?;
262                Ok((raw, filter_list))
263            }
264            _ => Err(PdfError::Other("not a stream".into())),
265        }
266    }
267
268    /// Resolve an object and return decompressed stream data, accepting a PdfObj directly.
269    pub fn stream_data_from_obj(&self, obj: &PdfObj) -> Result<Vec<u8>, PdfError> {
270        // If it's a Ref, use stream_data() which handles encryption with obj_num/gen_num.
271        if let PdfObj::Ref(n, g) = obj {
272            return self.stream_data(*n, *g);
273        }
274        let obj = self.deref(obj)?;
275        match obj {
276            PdfObj::Stream {
277                dict,
278                data_offset,
279                data_len,
280            } => {
281                // No encryption for inline stream objects (no obj_num to derive key from)
282                let raw = &self.data[data_offset..data_offset + data_len];
283                let (filter_list, parms) = filters::parse_filters(&dict, Some(self))?;
284                if filter_list.is_empty() {
285                    Ok(raw.to_vec())
286                } else {
287                    let jbig2_globals = self.resolve_jbig2_globals(&filter_list, &parms)?;
288                    filters::decode_stream_bounded(
289                        raw,
290                        &filter_list,
291                        &parms,
292                        jbig2_globals.as_deref(),
293                        filters::DecodeBudget::for_stream(&dict),
294                    )
295                }
296            }
297            _ => Err(PdfError::Other("expected a stream object".into())),
298        }
299    }
300
301    /// Return the raw (undecoded, possibly encrypted) stream bytes for an
302    /// object reference.  Used to parse format-specific headers (e.g. JPEG SOF)
303    /// before full filter decoding.
304    pub fn raw_stream_bytes(&self, obj: &PdfObj) -> Option<&[u8]> {
305        let (obj_num, gen_num) = match obj {
306            PdfObj::Ref(n, g) => (*n, *g),
307            _ => return None,
308        };
309        let resolved = self.resolve(obj_num, gen_num).ok()?;
310        if let PdfObj::Stream {
311            data_offset,
312            data_len,
313            ..
314        } = resolved
315        {
316            Some(&self.data[data_offset..data_offset + data_len])
317        } else {
318            None
319        }
320    }
321
322    /// Access the trailer dictionary.
323    pub fn trailer(&self) -> &PdfDict {
324        &self.xref.trailer
325    }
326
327    /// Access the raw file data.
328    pub fn data(&self) -> &'a [u8] {
329        self.data
330    }
331
332    /// Number of entries in the xref table.
333    pub fn xref_len(&self) -> usize {
334        self.xref.len()
335    }
336
337    /// Decrypt all strings within a parsed object tree.
338    /// Stream data is NOT decrypted here (handled in stream_data()).
339    fn decrypt_object(&self, obj: PdfObj, obj_num: u32, gen_num: u16) -> PdfObj {
340        let enc = match &self.encryption {
341            Some(e) => e,
342            None => return obj,
343        };
344        match obj {
345            PdfObj::Str(s) => PdfObj::Str(enc.decrypt_string(&s, obj_num, gen_num)),
346            PdfObj::Array(arr) => PdfObj::Array(
347                arr.into_iter()
348                    .map(|o| self.decrypt_object(o, obj_num, gen_num))
349                    .collect(),
350            ),
351            PdfObj::Dict(dict) => {
352                let entries: Vec<_> = dict
353                    .into_entries()
354                    .into_iter()
355                    .map(|(k, v)| (k, self.decrypt_object(v, obj_num, gen_num)))
356                    .collect();
357                PdfObj::Dict(PdfDict::from_entries(entries))
358            }
359            PdfObj::Stream {
360                dict,
361                data_offset,
362                data_len,
363            } => {
364                // Decrypt dict entries but NOT stream data (done in stream_data())
365                let entries: Vec<_> = dict
366                    .into_entries()
367                    .into_iter()
368                    .map(|(k, v)| (k, self.decrypt_object(v, obj_num, gen_num)))
369                    .collect();
370                PdfObj::Stream {
371                    dict: PdfDict::from_entries(entries),
372                    data_offset,
373                    data_len,
374                }
375            }
376            other => other,
377        }
378    }
379
380    /// If the filter chain contains JBIG2Decode, resolve the /JBIG2Globals stream
381    /// from the corresponding DecodeParms entry.
382    fn resolve_jbig2_globals(
383        &self,
384        filters: &[filters::Filter],
385        parms: &[Option<PdfDict>],
386    ) -> Result<Option<Vec<u8>>, PdfError> {
387        for (i, f) in filters.iter().enumerate() {
388            if *f == filters::Filter::JBIG2Decode
389                && let Some(Some(dp)) = parms.get(i)
390                && let Some(globals_ref) = dp.get(b"JBIG2Globals")
391            {
392                return self.stream_data_from_obj(globals_ref).map(Some);
393            }
394        }
395        Ok(None)
396    }
397
398    /// Look up an object by scanning the file for `N G obj` markers.
399    /// Builds a scan map on first call (one pass over the file), then
400    /// reuses it for all subsequent lookups — O(file_size) total instead
401    /// of O(file_size × num_misses).
402    fn scan_for_object(&self, obj_num: u32) -> Result<PdfObj, PdfError> {
403        // Build the scan map if we haven't yet
404        {
405            let mut map_ref = self.scan_map.borrow_mut();
406            if map_ref.is_none() {
407                *map_ref = Some(self.build_scan_map());
408            }
409        }
410
411        // Extract the offset and drop the borrow before parse_object_at,
412        // which may recursively call resolve → scan_for_object.
413        let offset = {
414            let map = self.scan_map.borrow();
415            map.as_ref().unwrap().get(&obj_num).copied()
416        };
417        match offset {
418            Some(offset) => self.parse_object_at(offset, None),
419            None => Err(PdfError::ObjectNotFound {
420                obj_num,
421                gen_num: 0,
422            }),
423        }
424    }
425
426    /// Scan the entire file once, recording the last file offset of every
427    /// `N G obj` marker. "Last" because incremental updates append newer
428    /// versions later in the file.
429    fn build_scan_map(&self) -> HashMap<u32, usize> {
430        let mut map = HashMap::new();
431        let data = self.data;
432        let len = data.len();
433        let mut pos = 0;
434
435        while pos < len {
436            // Quick scan for digit characters (start of "N G obj")
437            if !data[pos].is_ascii_digit() {
438                pos += 1;
439                continue;
440            }
441
442            // Check line boundary
443            if pos > 0 && data[pos - 1] != b'\n' && data[pos - 1] != b'\r' && data[pos - 1] != b' '
444            {
445                pos += 1;
446                continue;
447            }
448
449            // Parse "N G obj" pattern
450            let start = pos;
451            // Parse obj_num (digits)
452            while pos < len && data[pos].is_ascii_digit() {
453                pos += 1;
454            }
455            if pos == start || pos >= len || data[pos] != b' ' {
456                continue;
457            }
458            let num_end = pos;
459            pos += 1; // skip space
460
461            // Parse gen_num (digits)
462            let gen_start = pos;
463            while pos < len && data[pos].is_ascii_digit() {
464                pos += 1;
465            }
466            if pos == gen_start || pos >= len || data[pos] != b' ' {
467                continue;
468            }
469            pos += 1; // skip space
470
471            // Check for "obj" keyword followed by whitespace or PDF delimiter
472            if pos + 3 <= len
473                && &data[pos..pos + 3] == b"obj"
474                && (pos + 3 == len
475                    || data[pos + 3].is_ascii_whitespace()
476                    || matches!(data[pos + 3], b'<' | b'[' | b'(' | b'/'))
477            {
478                if let Ok(n) = std::str::from_utf8(&data[start..num_end])
479                    .unwrap_or("")
480                    .parse::<u32>()
481                {
482                    // Last occurrence wins (incremental updates)
483                    map.insert(n, start);
484                }
485                pos += 3;
486            }
487        }
488
489        map
490    }
491
492    /// Parse an indirect object at a file offset.
493    /// Handles xref offsets that are off by a few bytes (common in some PDF generators)
494    /// by scanning backward up to 20 bytes to find the `N G obj` header.
495    /// When `expected_obj_num` is Some, verifies the parsed object number matches;
496    /// returns an error on mismatch so the caller can fall back to scanning.
497    fn parse_object_at(
498        &self,
499        offset: usize,
500        expected_obj_num: Option<u32>,
501    ) -> Result<PdfObj, PdfError> {
502        if offset >= self.data.len() {
503            return Err(PdfError::InvalidObject(offset));
504        }
505
506        // Try the exact offset first (no word-boundary check — some PDFs omit
507        // whitespace between `endobj` and the next object header), then scan
508        // backward with stricter matching if that fails.
509        let actual_offset = self
510            .try_parse_obj_header_relaxed(offset)
511            .or_else(|| {
512                // Scan backward up to 20 bytes for the object header
513                let start = offset.saturating_sub(20);
514                (start..offset)
515                    .rev()
516                    .find_map(|off| self.try_parse_obj_header(off))
517            })
518            .ok_or(PdfError::InvalidObject(offset))?;
519
520        let mut lexer = Lexer::at(self.data, actual_offset);
521
522        // Parse the "N G obj" header and verify the object number
523        let parsed_num = match lexer.next_token()? {
524            Token::Int(n) => n as u32,
525            _ => return Err(PdfError::InvalidObject(offset)),
526        };
527        let _gen_num = lexer.next_token()?; // Int
528        let _obj_kw = lexer.next_token()?; // Keyword("obj")
529
530        if let Some(expected) = expected_obj_num {
531            if parsed_num != expected {
532                return Err(PdfError::InvalidObject(offset));
533            }
534        }
535
536        // Parse the object value
537        let obj = parse_object(&mut lexer)?;
538
539        // Check if this is a stream (dict followed by "stream" keyword)
540        if let PdfObj::Dict(dict) = obj {
541            let saved = lexer.pos();
542            let tok = lexer.next_token()?;
543            if matches!(tok, Token::Keyword(ref kw) if kw == b"stream") {
544                // Stream data starts after "stream" + EOL
545                let mut data_start = lexer.pos();
546                if data_start < self.data.len() && self.data[data_start] == b'\r' {
547                    data_start += 1;
548                }
549                if data_start < self.data.len() && self.data[data_start] == b'\n' {
550                    data_start += 1;
551                }
552
553                // Get length (may be an indirect reference, or missing).
554                // If /Length is present but wrong (doesn't end at endstream),
555                // recover by scanning for endstream.
556                let length = match self.resolve_stream_length(&dict) {
557                    Ok(len) => {
558                        // Validate: endstream should follow at data_start + len
559                        let expected_end = data_start + len;
560                        let valid = self.check_endstream_at(expected_end);
561                        if valid {
562                            len
563                        } else {
564                            // /Length is wrong — recover from endstream
565                            self.recover_stream_length(data_start).unwrap_or(len)
566                        }
567                    }
568                    Err(_) => self.recover_stream_length(data_start)?,
569                };
570                let data_end = std::cmp::min(data_start + length, self.data.len());
571
572                return Ok(PdfObj::Stream {
573                    dict,
574                    data_offset: data_start,
575                    data_len: data_end - data_start,
576                });
577            } else {
578                lexer.set_pos(saved);
579            }
580            // endobj follows — we don't strictly require it
581            Ok(PdfObj::Dict(dict))
582        } else {
583            // endobj follows — we don't strictly require it
584            Ok(obj)
585        }
586    }
587
588    /// Check if `offset` starts with `Int Int Keyword("obj")` at a word boundary.
589    /// Returns `Some(offset)` on success, `None` on failure.
590    /// Check for `N G obj` header without requiring whitespace before the offset.
591    /// Used for the exact xref offset, where some PDFs omit whitespace after `endobj`.
592    fn try_parse_obj_header_relaxed(&self, offset: usize) -> Option<usize> {
593        if offset >= self.data.len() {
594            return None;
595        }
596        let mut lexer = Lexer::at(self.data, offset);
597        if !matches!(lexer.next_token().ok()?, Token::Int(_)) {
598            return None;
599        }
600        if !matches!(lexer.next_token().ok()?, Token::Int(_)) {
601            return None;
602        }
603        match lexer.next_token().ok()? {
604            Token::Keyword(ref kw) if kw == b"obj" => Some(offset),
605            _ => None,
606        }
607    }
608
609    fn try_parse_obj_header(&self, offset: usize) -> Option<usize> {
610        if offset >= self.data.len() {
611            return None;
612        }
613        // Must be at a word boundary (start of file, or preceded by whitespace/newline)
614        if offset > 0 && !matches!(self.data[offset - 1], b' ' | b'\t' | b'\r' | b'\n') {
615            return None;
616        }
617        self.try_parse_obj_header_relaxed(offset)
618    }
619
620    /// Check if `endstream` keyword appears at or near the given offset.
621    fn check_endstream_at(&self, offset: usize) -> bool {
622        // Accept "endstream" or common typo "endsteam" (missing 'r').
623        // The latter appears in some PDFs (e.g. issue18122.pdf) with an empty
624        // pattern stream; without tolerating it, the recovery path would scan
625        // ahead and consume subsequent objects' data as this stream's content.
626        let needles: &[&[u8]] = &[b"endstream", b"endsteam"];
627        // Allow up to 2 bytes of whitespace between stream data and endstream
628        for skip in 0..=2 {
629            let pos = offset + skip;
630            for needle in needles {
631                if pos + needle.len() <= self.data.len()
632                    && &self.data[pos..pos + needle.len()] == *needle
633                {
634                    return true;
635                }
636            }
637        }
638        false
639    }
640
641    /// Recover stream length by scanning for `endstream` keyword.
642    /// Used when /Length is missing from the stream dict.
643    fn recover_stream_length(&self, data_start: usize) -> Result<usize, PdfError> {
644        let needle = b"endstream";
645        let search_end = self.data.len().saturating_sub(needle.len());
646        let mut pos = data_start;
647        while pos <= search_end {
648            if &self.data[pos..pos + needle.len()] == needle {
649                // Strip trailing whitespace before endstream
650                let mut end = pos;
651                while end > data_start && matches!(self.data[end - 1], b' ' | b'\r' | b'\n') {
652                    end -= 1;
653                }
654                return Ok(end - data_start);
655            }
656            pos += 1;
657        }
658        Err(PdfError::StreamMissingLength)
659    }
660
661    /// Resolve the /Length of a stream dict (may be an indirect reference).
662    fn resolve_stream_length(&self, dict: &PdfDict) -> Result<usize, PdfError> {
663        match dict.get(b"Length") {
664            Some(PdfObj::Int(n)) => Ok(*n as usize),
665            Some(PdfObj::Ref(n, g)) => {
666                let len_obj = self.resolve(*n, *g)?;
667                match len_obj {
668                    PdfObj::Int(n) => Ok(n as usize),
669                    _ => Err(PdfError::StreamMissingLength),
670                }
671            }
672            _ => Err(PdfError::StreamMissingLength),
673        }
674    }
675
676    /// Ensure the decompressed ObjStm data + header are cached.
677    /// Returns the index into `objstm_cache` for the given stream object.
678    fn ensure_objstm_cached(&self, stream_obj_num: u32) -> Result<(), PdfError> {
679        if self.objstm_cache.borrow().contains_key(&stream_obj_num) {
680            return Ok(());
681        }
682
683        let stream_obj = self.resolve(stream_obj_num, 0)?;
684        let (dict, data_offset, data_len) = match stream_obj {
685            PdfObj::Stream {
686                dict,
687                data_offset,
688                data_len,
689            } => (dict, data_offset, data_len),
690            _ => {
691                return Err(PdfError::Other(format!(
692                    "object stream {stream_obj_num} is not a stream"
693                )));
694            }
695        };
696
697        let raw_slice = &self.data[data_offset..data_offset + data_len];
698        let raw = if let Some(ref enc) = self.encryption {
699            enc.decrypt_stream(raw_slice, stream_obj_num, 0)
700        } else {
701            raw_slice.to_vec()
702        };
703        let (filter_list, parms) = filters::parse_filters(&dict, Some(self))?;
704        let stream_data = if filter_list.is_empty() {
705            raw
706        } else {
707            filters::decode_stream_bounded(
708                &raw,
709                &filter_list,
710                &parms,
711                None,
712                filters::DecodeBudget::for_stream(&dict),
713            )?
714        };
715
716        let n = dict.get_int(b"N").ok_or(PdfError::MissingKey("N"))? as usize;
717        let first = dict
718            .get_int(b"First")
719            .ok_or(PdfError::MissingKey("First"))? as usize;
720
721        let mut header_lexer = Lexer::new(&stream_data[..first.min(stream_data.len())]);
722        let mut obj_offsets: Vec<(u32, usize)> = Vec::with_capacity(n);
723        for _ in 0..n {
724            let num = match header_lexer.next_token()? {
725                Token::Int(v) => v as u32,
726                _ => break,
727            };
728            let off = match header_lexer.next_token()? {
729                Token::Int(v) => v as usize,
730                _ => break,
731            };
732            obj_offsets.push((num, off));
733        }
734
735        self.objstm_cache.borrow_mut().insert(
736            stream_obj_num,
737            ObjStmCache {
738                data: stream_data,
739                first,
740                offsets: obj_offsets,
741            },
742        );
743        Ok(())
744    }
745
746    /// Parse an object from inside an object stream (ObjStm).
747    fn parse_object_from_stream(
748        &self,
749        stream_obj_num: u32,
750        index_within: u16,
751    ) -> Result<PdfObj, PdfError> {
752        // Ensure decompressed data is cached
753        self.ensure_objstm_cached(stream_obj_num)?;
754
755        let cache = self.objstm_cache.borrow();
756        let cached = cache
757            .get(&stream_obj_num)
758            .ok_or_else(|| PdfError::Other(format!("ObjStm {stream_obj_num} not in cache")))?;
759
760        // Find and parse the target object
761        let idx = index_within as usize;
762        if idx >= cached.offsets.len() {
763            return Err(PdfError::Other(format!(
764                "index {idx} out of range in object stream {stream_obj_num}"
765            )));
766        }
767
768        let (_target_num, target_offset) = cached.offsets[idx];
769        let abs_offset = cached.first + target_offset;
770        if abs_offset >= cached.data.len() {
771            return Err(PdfError::Other(format!(
772                "object offset {abs_offset} out of range in stream {stream_obj_num}"
773            )));
774        }
775
776        let mut obj_lexer = Lexer::new(&cached.data[abs_offset..]);
777        let obj = parse_object(&mut obj_lexer)?;
778
779        // Cache the parsed object
780        let target_num = cached.offsets[idx].0;
781        drop(cache);
782        self.cache.borrow_mut().insert(target_num, obj.clone());
783
784        Ok(obj)
785    }
786}
787
788#[cfg(test)]
789mod tests {
790    use super::*;
791
792    /// Build a minimal valid PDF with one object for testing.
793    fn minimal_pdf() -> Vec<u8> {
794        let mut pdf = Vec::new();
795        pdf.extend(b"%PDF-1.4\n");
796
797        // Object 1: a simple dict
798        let obj1_offset = pdf.len();
799        pdf.extend(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
800
801        // Object 2: pages
802        let obj2_offset = pdf.len();
803        pdf.extend(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
804
805        // Xref
806        let xref_offset = pdf.len();
807        pdf.extend(b"xref\n");
808        pdf.extend(b"0 3\n");
809        pdf.extend(b"0000000000 65535 f\r\n");
810        pdf.extend(format!("{:010} 00000 n\r\n", obj1_offset).as_bytes());
811        pdf.extend(format!("{:010} 00000 n\r\n", obj2_offset).as_bytes());
812        pdf.extend(b"trailer\n");
813        pdf.extend(b"<< /Size 3 /Root 1 0 R >>\n");
814        pdf.extend(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
815
816        pdf
817    }
818
819    #[test]
820    fn resolve_simple_objects() {
821        let data = minimal_pdf();
822        let xref = crate::xref::parse_xref(&data).unwrap();
823        let resolver = Resolver::with_encryption(&data, xref, None);
824
825        let obj1 = resolver.resolve(1, 0).unwrap();
826        let dict = obj1.as_dict().unwrap();
827        assert_eq!(dict.get_name(b"Type"), Some(b"Catalog".as_slice()));
828
829        let obj2 = resolver.resolve(2, 0).unwrap();
830        let dict = obj2.as_dict().unwrap();
831        assert_eq!(dict.get_name(b"Type"), Some(b"Pages".as_slice()));
832    }
833
834    #[test]
835    fn deref_passes_through_non_ref() {
836        let data = minimal_pdf();
837        let xref = crate::xref::parse_xref(&data).unwrap();
838        let resolver = Resolver::with_encryption(&data, xref, None);
839
840        let obj = PdfObj::Int(42);
841        let result = resolver.deref(&obj).unwrap();
842        assert_eq!(result, PdfObj::Int(42));
843    }
844
845    #[test]
846    fn deref_resolves_ref() {
847        let data = minimal_pdf();
848        let xref = crate::xref::parse_xref(&data).unwrap();
849        let resolver = Resolver::with_encryption(&data, xref, None);
850
851        let obj = PdfObj::Ref(1, 0);
852        let result = resolver.deref(&obj).unwrap();
853        assert!(result.as_dict().is_some());
854    }
855
856    #[test]
857    fn object_not_found() {
858        let data = minimal_pdf();
859        let xref = crate::xref::parse_xref(&data).unwrap();
860        let resolver = Resolver::with_encryption(&data, xref, None);
861
862        let result = resolver.resolve(999, 0);
863        assert!(result.is_err());
864    }
865}