Skip to main content

pdfrum_parser/
store.rs

1//! The lazy object store: the one thing that turns a reference into an
2//! object.
3//!
4//! # Lazy, cached, and cycle-safe
5//!
6//! Objects are parsed the first time something asks for them and kept
7//! forever after. Three guards make that safe on files designed to be
8//! hostile:
9//!
10//! - An object whose fetch is already in progress resolves to nothing rather
11//!   than recursing. This is what makes a self-referential `/Length` — a
12//!   stream whose length lives in an object inside that same stream — end as
13//!   a missing length instead of a stack overflow.
14//! - An object number past the cross-reference table's end is refused, even
15//!   when the bytes for it sit in the file. The table is the only index the
16//!   reader trusts.
17//! - The object header at a cross-reference offset must carry the number the
18//!   table claimed. A mismatch means the table describes a version of the
19//!   file that no longer exists, and the fetch fails rather than returning
20//!   the wrong object.
21//!
22//! # Failures are not cached
23//!
24//! A fetch that fails leaves the slot empty, so a later fetch tries again.
25//! That costs a re-parse on a broken object, and it is deliberate: whether an
26//! object resolves can change as the reader learns more about the file, and a
27//! negative cache would freeze the first answer.
28//!
29//! # Decryption happens here
30//!
31//! An encrypted document's strings and stream payloads are ciphertext until
32//! the object holding them is fetched, and the key depends on the *enclosing
33//! indirect object's* number, which only this layer knows. So the walk that
34//! rewrites them lives here, including the rule that a signature
35//! dictionary's `/Contents` is left alone — a detached signature covers the
36//! raw bytes, and decrypting them would destroy it.
37
38use std::collections::HashMap;
39use std::sync::{Arc, Mutex, OnceLock};
40
41use pdfrum_common::{DiagKind, Diagnostics, Limits, Severity};
42use pdfrum_crypt::{CryptClass, SecurityHandler};
43use pdfrum_object::{
44    Array, ByteSpan, Dict, Name, ObjRef, Object, PdfString, Resolve, Stream, names,
45};
46
47use crate::error::Error;
48use crate::lexer::Lexer;
49use crate::objstm::ObjStm;
50use crate::syntax::{Context, Strictness, indirect};
51use crate::xref::{Entry, Xref};
52
53/// Everything needed to turn a reference into an object.
54///
55/// `Sync` on purpose: rendering pages in parallel means fetching objects in
56/// parallel, so the caches are behind locks and the parsed objects are shared
57/// immutably.
58#[derive(Debug)]
59pub struct ObjectStore {
60    /// The file, from its header onwards.
61    bytes: ByteSpan,
62    /// Where every object lives.
63    ///
64    /// Shared rather than owned. A store is built at least twice per open —
65    /// once through the throwaway plaintext store that reads `/Encrypt`, and
66    /// again for the document itself — and the table is a slot vector, so
67    /// owning it meant a `memcpy` of `slots.len()` entries per construction.
68    /// The store never mutates it, so a reference count is the whole cost.
69    xref: Arc<Xref>,
70    /// Caps.
71    limits: Limits,
72    /// Parsed objects, one slot per object number. The slot is created on
73    /// first request and filled once.
74    /// Keyed with [`pdfrum_common::FxBuildHasher`], not `std`'s `SipHash`: the
75    /// key is an **object number**, a `u32` this crate assigns from the
76    /// cross-reference table, and every resolve of every indirect reference in
77    /// a document goes through this map. A file cannot choose the key — it can
78    /// choose how many there are, which the `Limits` cap governs — so the
79    /// collision resistance `SipHash` is paying for is not resistance to
80    /// anything. See `pdfrum_common::FxBuildHasher`'s docs.
81    cells: Mutex<Cells>,
82    /// Object numbers whose parse is running right now.
83    in_progress: Mutex<Vec<u32>>,
84    /// Decoded object streams, keyed by their own object number.
85    containers: Mutex<Containers>,
86    /// How the document's strings and streams are encrypted.
87    security: SecurityHandler,
88    /// The object holding `/Root/Metadata` when it is exempt from
89    /// decryption, which `/EncryptMetadata false` makes it.
90    metadata_exempt: Option<u32>,
91    /// Repairs recorded during fetches.
92    diags: Mutex<Diagnostics>,
93}
94
95/// The parsed-object slots, keyed by object number.
96///
97/// A named type because the shape is three layers deep and reads badly inline:
98/// an `Arc<OnceLock<_>>` per slot is what lets one thread create the slot and
99/// another wait on the parse without holding the map's lock across it.
100type Cells = HashMap<u32, Arc<OnceLock<Arc<Object>>>, pdfrum_common::FxBuildHasher>;
101
102/// Decoded object streams, keyed by their own object number.
103///
104/// `None` records a container that was tried and could not be decoded, so a
105/// broken object stream is parsed once rather than once per object in it.
106type Containers = HashMap<u32, Option<Arc<ObjStm>>, pdfrum_common::FxBuildHasher>;
107
108impl ObjectStore {
109    /// Build a store over a file and its cross-reference table.
110    pub(crate) fn new(
111        bytes: ByteSpan,
112        xref: Arc<Xref>,
113        limits: Limits,
114        security: SecurityHandler,
115    ) -> Self {
116        Self {
117            bytes,
118            xref,
119            limits,
120            cells: Mutex::new(HashMap::default()),
121            in_progress: Mutex::new(Vec::new()),
122            containers: Mutex::new(HashMap::default()),
123            security,
124            metadata_exempt: None,
125            diags: Mutex::new(Diagnostics::default()),
126        }
127    }
128
129    /// Exempt one object from decryption, for the metadata stream a document
130    /// declared unencrypted.
131    pub(crate) fn exempt_from_decryption(&mut self, num: u32) {
132        self.metadata_exempt = Some(num);
133    }
134
135    /// The cross-reference table.
136    pub(crate) fn xref(&self) -> &Xref {
137        &self.xref
138    }
139
140    /// The caps in force.
141    pub(crate) fn limits(&self) -> &Limits {
142        &self.limits
143    }
144
145    /// How the document is encrypted.
146    pub(crate) fn security(&self) -> &SecurityHandler {
147        &self.security
148    }
149
150    /// Take the repairs recorded so far, leaving the sink empty.
151    pub(crate) fn drain_diags(&self) -> Diagnostics {
152        match self.diags.lock() {
153            Ok(mut guard) => std::mem::take(&mut *guard),
154            Err(_) => Diagnostics::default(),
155        }
156    }
157
158    /// The repairs recorded so far, *without* emptying the sink.
159    ///
160    /// The read behind [`Document::lazy_diagnostics`](crate::Document::lazy_diagnostics):
161    /// a caller asking what the document has needed so far must be able to
162    /// ask twice and get the same answer, which draining would break.
163    pub(crate) fn peek_diags(&self) -> Diagnostics {
164        match self.diags.lock() {
165            Ok(guard) => guard.clone(),
166            Err(_) => Diagnostics::default(),
167        }
168    }
169
170    /// Record a repair.
171    ///
172    /// `&self` rather than `&mut self` because the sink is behind the store's
173    /// own lock: a repair found during a *lazy* read — the page-tree walk
174    /// reaches this way — has no `&mut Diagnostics` to hand, and must still be
175    /// visible to [`Document::lazy_diagnostics`](crate::Document::lazy_diagnostics).
176    pub(crate) fn note(&self, severity: Severity, what: DiagKind, at: Option<u64>) {
177        if let Ok(mut guard) = self.diags.lock() {
178            guard.record(severity, what, at);
179        }
180    }
181
182    /// Run `f` with a diagnostics sink, folding what it recorded back in.
183    fn with_diags<T>(&self, f: impl FnOnce(&mut Diagnostics) -> T) -> T {
184        let mut local = Diagnostics::default();
185        let out = f(&mut local);
186        if let Ok(mut guard) = self.diags.lock() {
187            guard.extend(&local);
188        }
189        out
190    }
191
192    /// Fetch an object, parsing it if this is the first request.
193    ///
194    /// # Errors
195    ///
196    /// [`Error::Unresolved`] when the table has nothing usable for this
197    /// number, and [`Error::Cycle`] when the fetch re-entered one already
198    /// running.
199    pub fn get(&self, num: u32) -> Result<Arc<Object>, Error> {
200        let reference = ObjRef::new(num, self.xref.generation(num));
201        if num == 0 || reference.is_invalid() {
202            return Err(Error::Unresolved(reference));
203        }
204        // Numbers past the table's end name nothing, whatever the file holds.
205        if !self.xref.is_valid_object_number(num) {
206            return Err(Error::Unresolved(reference));
207        }
208
209        let cell = self.cell(num);
210        if let Some(object) = cell.get() {
211            return Ok(Arc::clone(object));
212        }
213
214        let guard = Guard::enter(self, num).ok_or(Error::Cycle(reference))?;
215        // Another thread may have filled the slot while we waited.
216        if let Some(object) = cell.get() {
217            return Ok(Arc::clone(object));
218        }
219
220        // Every fetch already in flight is an object this parse is nested
221        // inside, so the nesting budget continues from there rather than
222        // restarting — an object reachable only through sixty-four layers of
223        // `/Length` references is as unreachable as one nested that deep.
224        let object = self
225            .parse(num, guard.nesting())
226            .ok_or(Error::Unresolved(reference))?;
227        let object = Arc::new(object);
228        // A race here is harmless: both values parse the same bytes.
229        let _ = cell.set(Arc::clone(&object));
230        Ok(cell.get().map_or(object, Arc::clone))
231    }
232
233    /// The slot for an object number, creating it if needed.
234    fn cell(&self, num: u32) -> Arc<OnceLock<Arc<Object>>> {
235        match self.cells.lock() {
236            Ok(mut cells) => Arc::clone(cells.entry(num).or_default()),
237            Err(_) => Arc::new(OnceLock::new()),
238        }
239    }
240
241    /// Parse an object from wherever the table says it lives.
242    ///
243    /// `depth` is the nesting the *triggering* parse had already spent, so a
244    /// fetch reached from inside a deep object continues that budget rather
245    /// than getting a fresh one.
246    fn parse(&self, num: u32, depth: u32) -> Option<Object> {
247        match self.xref.entry(num)? {
248            Entry::Offset(pos) if pos > 0 => self.parse_at(num, pos, depth),
249            // A free slot, and an in-use one at offset zero — which is the
250            // header, where no object can start.
251            Entry::Free | Entry::Offset(_) => None,
252            Entry::InObjStream { stream, index } => {
253                self.parse_member(num, stream.num, index, depth)
254            }
255        }
256    }
257
258    /// Parse the indirect object at a byte offset.
259    ///
260    /// The header's object number is checked against the one asked for: a
261    /// table pointing at the wrong bytes is the failure mode this catches.
262    fn parse_at(&self, num: u32, pos: u64, depth: u32) -> Option<Object> {
263        let pos = usize::try_from(pos).ok()?;
264        if pos >= self.bytes.len() {
265            return None;
266        }
267        let parsed = self.with_diags(|diags| {
268            let mut ctx = Context {
269                limits: &self.limits,
270                diags,
271                file: Some(&self.bytes),
272                store: Some(self),
273            };
274            let mut lx = Lexer::at(&self.bytes, pos);
275            indirect(&mut lx, &mut ctx, Strictness::Loose, depth).ok()
276        })?;
277
278        if parsed.num != num {
279            self.note(
280                Severity::Suspicious,
281                DiagKind::ObjNumMismatch,
282                Some(pos as u64),
283            );
284            return None;
285        }
286        Some(self.decrypted(ObjRef::new(parsed.num, parsed.generation), parsed.object))
287    }
288
289    /// Parse a member of an object stream.
290    fn parse_member(&self, num: u32, archive: u32, index: u32, depth: u32) -> Option<Object> {
291        let container = self.container(archive)?;
292        // Members are already plaintext: the container was decrypted whole.
293        self.with_diags(|diags| container.member(num, index, &self.limits, diags, self, depth))
294    }
295
296    /// The decoded object stream with this number, decoding it once.
297    fn container(&self, archive: u32) -> Option<Arc<ObjStm>> {
298        // Only an object something named as a container may be used as one.
299        if !self.xref.is_object_stream(archive) {
300            return None;
301        }
302        if let Ok(cache) = self.containers.lock()
303            && let Some(hit) = cache.get(&archive)
304        {
305            return hit.clone();
306        }
307
308        let built = self.build_container(archive);
309        if let Ok(mut cache) = self.containers.lock() {
310            cache.insert(archive, built.clone());
311        }
312        built
313    }
314
315    /// Decode and index the object stream with this number.
316    fn build_container(&self, archive: u32) -> Option<Arc<ObjStm>> {
317        let Object::Stream(stream) = &*self.get(archive).ok()? else {
318            return None;
319        };
320        self.with_diags(|diags| ObjStm::build(stream, &self.limits, diags, self).map(Arc::new))
321    }
322
323    /// Rewrite an object's strings and stream payloads as plaintext.
324    ///
325    /// A no-op for an unencrypted document, and for the metadata object a
326    /// document declared exempt.
327    fn decrypted(&self, obj: ObjRef, object: Object) -> Object {
328        if matches!(self.security, SecurityHandler::Identity)
329            || self.metadata_exempt == Some(obj.num)
330        {
331            return object;
332        }
333        let mut deferred = Vec::new();
334        let out = decrypt_node(&self.security, obj, object, &mut deferred, false);
335        // A `/Contents` under a dictionary carrying `/Type` or `/FT` was left
336        // alone because those keys were ciphertext at the time. Now that the
337        // parent is readable, the ones that are not signatures get decrypted
338        // after all.
339        resolve_deferred(&self.security, obj, out, &deferred)
340    }
341}
342
343impl Resolve for ObjectStore {
344    fn fetch(&self, r: ObjRef) -> Result<Arc<Object>, pdfrum_object::Error> {
345        self.get(r.num).map_err(Into::into)
346    }
347}
348
349/// Marks an object number as being parsed, and unmarks it on the way out.
350struct Guard<'s> {
351    store: &'s ObjectStore,
352    num: u32,
353    /// How many fetches were already in flight when this one started.
354    nesting: u32,
355}
356
357impl<'s> Guard<'s> {
358    /// Claim `num`, or `None` when a fetch of it is already running.
359    fn enter(store: &'s ObjectStore, num: u32) -> Option<Self> {
360        let mut running = store.in_progress.lock().ok()?;
361        if running.contains(&num) {
362            return None;
363        }
364        let nesting = u32::try_from(running.len()).unwrap_or(u32::MAX);
365        running.push(num);
366        drop(running);
367        Some(Self {
368            store,
369            num,
370            nesting,
371        })
372    }
373
374    /// How deep the parse this guard protects is nested.
375    fn nesting(&self) -> u32 {
376        self.nesting
377    }
378}
379
380impl Drop for Guard<'_> {
381    fn drop(&mut self) {
382        if let Ok(mut running) = self.store.in_progress.lock() {
383            running.retain(|&n| n != self.num);
384        }
385    }
386}
387
388/// Where a deferred `/Contents` sits, as a path of keys and indices from the
389/// object's root.
390#[derive(Debug, Clone)]
391struct Deferred {
392    /// The dictionary that held it, already decrypted.
393    parent: Dict,
394    /// How to reach the value again.
395    path: Vec<Step>,
396}
397
398/// One step of a path into an object.
399#[derive(Debug, Clone)]
400enum Step {
401    /// Into a dictionary, by key.
402    Key(Name),
403    /// Into an array, by index.
404    Index(usize),
405}
406
407/// Rewrite every string and stream payload as plaintext.
408///
409/// `deferred` collects the `/Contents` values that were skipped; `in_sig`
410/// marks a subtree already known to be one, so nothing inside it is touched.
411fn decrypt_node(
412    handler: &SecurityHandler,
413    obj: ObjRef,
414    object: Object,
415    deferred: &mut Vec<Deferred>,
416    in_sig: bool,
417) -> Object {
418    match object {
419        Object::Str(s) => {
420            if in_sig {
421                Object::Str(s)
422            } else {
423                let plain = handler.decrypt(obj, CryptClass::String, &s.bytes);
424                Object::Str(PdfString::new(plain, syntax_of(&s)))
425            }
426        }
427        Object::Array(a) => Object::Array(
428            a.iter()
429                .map(|v| decrypt_node(handler, obj, v.clone(), deferred, in_sig))
430                .collect(),
431        ),
432        Object::Dict(d) => Object::Dict(decrypt_dict(handler, obj, &d, deferred, in_sig, &[])),
433        Object::Stream(s) => {
434            let dict = decrypt_dict(handler, obj, &s.dict, deferred, in_sig, &[]);
435            // An embedded file stream is its own crypt-filter class
436            // (ISO 32000-1 §7.6.5), and `/EFF` may name a different cipher
437            // from `/StmF`'s. The class is read off `/Type` before the
438            // decrypt, which is safe because a name is never enciphered.
439            // Where `/EFF` is absent or names the stream filter — every file
440            // in the corpus — the two classes are the same call.
441            let class = if s.dict.name(names::TYPE) == Some(names::EMBEDDED_FILE) {
442                CryptClass::Embedded
443            } else {
444                CryptClass::Stream
445            };
446            let plain = handler.decrypt(obj, class, &s.data);
447            Object::Stream(Box::new(Stream::new(
448                dict,
449                pdfrum_object::ByteSpan::from(plain),
450            )))
451        }
452        other => other,
453    }
454}
455
456/// Rewrite a dictionary, deferring the `/Contents` of anything that might be
457/// a signature.
458fn decrypt_dict(
459    handler: &SecurityHandler,
460    obj: ObjRef,
461    dict: &Dict,
462    deferred: &mut Vec<Deferred>,
463    in_sig: bool,
464    path: &[Step],
465) -> Dict {
466    // A dictionary carrying either key *might* be a signature — but both are
467    // still ciphertext, so the question cannot be settled yet.
468    let suspicious = dict.contains_key(names::TYPE) || dict.contains_key(names::FT);
469    let mut out = Dict::new();
470    let mut skipped: Vec<(Name, Object)> = Vec::new();
471
472    for (key, value) in dict.iter() {
473        if suspicious && key == names::CONTENTS && !in_sig {
474            skipped.push((key.clone(), value.clone()));
475            out.push(key.clone(), value.clone());
476            continue;
477        }
478        let mut child = path.to_vec();
479        child.push(Step::Key(key.clone()));
480        out.push(
481            key.clone(),
482            decrypt_value(handler, obj, value.clone(), deferred, in_sig, &child),
483        );
484    }
485
486    for (key, _) in skipped {
487        let mut child = path.to_vec();
488        child.push(Step::Key(key));
489        deferred.push(Deferred {
490            parent: out.clone(),
491            path: child,
492        });
493    }
494    out
495}
496
497/// Rewrite one value, keeping arrays' paths so a deferral inside one can be
498/// found again.
499fn decrypt_value(
500    handler: &SecurityHandler,
501    obj: ObjRef,
502    value: Object,
503    deferred: &mut Vec<Deferred>,
504    in_sig: bool,
505    path: &[Step],
506) -> Object {
507    match value {
508        Object::Dict(d) => Object::Dict(decrypt_dict(handler, obj, &d, deferred, in_sig, path)),
509        Object::Array(a) => {
510            let mut out = Array::new();
511            for (i, v) in a.iter().enumerate() {
512                let mut child = path.to_vec();
513                child.push(Step::Index(i));
514                out.push(decrypt_value(
515                    handler,
516                    obj,
517                    v.clone(),
518                    deferred,
519                    in_sig,
520                    &child,
521                ));
522            }
523            Object::Array(out)
524        }
525        other => decrypt_node(handler, obj, other, deferred, in_sig),
526    }
527}
528
529/// Decrypt the deferred `/Contents` values whose parents turned out not to be
530/// signature dictionaries.
531fn resolve_deferred(
532    handler: &SecurityHandler,
533    obj: ObjRef,
534    object: Object,
535    deferred: &[Deferred],
536) -> Object {
537    let mut out = object;
538    for entry in deferred {
539        if pdfrum_crypt::is_signature_dict(&entry.parent) {
540            // A real signature: its `/Contents` covers the raw bytes and
541            // must stay exactly as written.
542            continue;
543        }
544        out = rewrite_at(handler, obj, out, &entry.path);
545    }
546    out
547}
548
549/// Decrypt the value at `path`, leaving everything else alone.
550fn rewrite_at(handler: &SecurityHandler, obj: ObjRef, object: Object, path: &[Step]) -> Object {
551    let Some((step, rest)) = path.split_first() else {
552        let mut ignored = Vec::new();
553        return decrypt_node(handler, obj, object, &mut ignored, false);
554    };
555    match (object, step) {
556        (Object::Dict(d), Step::Key(key)) => {
557            Object::Dict(Dict::from_pairs(d.iter().map(|(k, v)| {
558                if k == key {
559                    (k.clone(), rewrite_at(handler, obj, v.clone(), rest))
560                } else {
561                    (k.clone(), v.clone())
562                }
563            })))
564        }
565        (Object::Stream(s), Step::Key(key)) => {
566            let dict = Dict::from_pairs(s.dict.iter().map(|(k, v)| {
567                if k == key {
568                    (k.clone(), rewrite_at(handler, obj, v.clone(), rest))
569                } else {
570                    (k.clone(), v.clone())
571                }
572            }));
573            Object::Stream(Box::new(Stream::new(dict, s.data)))
574        }
575        (Object::Array(a), Step::Index(index)) => Object::Array(
576            a.iter()
577                .enumerate()
578                .map(|(i, v)| {
579                    if i == *index {
580                        rewrite_at(handler, obj, v.clone(), rest)
581                    } else {
582                        v.clone()
583                    }
584                })
585                .collect(),
586        ),
587        (other, _) => other,
588    }
589}
590
591/// Keep a string's source spelling across the rewrite, since the writer
592/// round-trips it.
593fn syntax_of(s: &PdfString) -> pdfrum_object::StringSyntax {
594    if s.hex {
595        pdfrum_object::StringSyntax::Hex
596    } else {
597        pdfrum_object::StringSyntax::Literal
598    }
599}
600
601#[cfg(test)]
602mod tests {
603    use super::ObjectStore;
604    use crate::error::Error;
605    use crate::xref::Xref;
606    use pdfrum_common::Limits;
607    use pdfrum_crypt::SecurityHandler;
608    use pdfrum_object::ByteSpan;
609    use pdfrum_object::{ObjRef, Resolve, names};
610    use std::sync::Arc;
611
612    fn store(file: &[u8], build: impl FnOnce(&mut Xref)) -> ObjectStore {
613        let mut xref = Xref::new();
614        build(&mut xref);
615        ObjectStore::new(
616            ByteSpan::from(file.to_vec()),
617            Arc::new(xref),
618            Limits::default(),
619            SecurityHandler::Identity,
620        )
621    }
622
623    #[test]
624    fn fetches_and_caches() {
625        let file = b"%PDF-1.7\n1 0 obj << /Type /Page >> endobj\n";
626        let s = store(file, |x| {
627            x.add_normal(1, 0, false, 9, &Limits::default());
628        });
629        let first = s.get(1).expect("object");
630        assert!(first.as_dict().is_some());
631        // The same Arc comes back.
632        let second = s.get(1).expect("object");
633        assert!(Arc::ptr_eq(&first, &second));
634    }
635
636    #[test]
637    fn nested_fetches_share_one_nesting_budget() {
638        // Each object is a stream whose `/Length` lives in the next object,
639        // so reading object 1 forces a fetch of 2, which forces 3, and so on.
640        // These fetches are genuinely nested — each is suspended while the
641        // next runs — and the budget is spent across the whole chain rather
642        // than reset per object, so the deepest links are unreachable.
643        let count: u32 = 80;
644        let mut file = b"%PDF-1.7\n".to_vec();
645        let mut offsets = Vec::new();
646        for i in 1..=count {
647            offsets.push(file.len());
648            if i == count {
649                file.extend_from_slice(format!("{i} 0 obj 4 endobj\n").as_bytes());
650            } else {
651                file.extend_from_slice(
652                    format!(
653                        "{i} 0 obj << /Length {} 0 R >> stream\nDATA\nendstream endobj\n",
654                        i + 1
655                    )
656                    .as_bytes(),
657                );
658            }
659        }
660        let s = store(&file, |x| {
661            for (i, offset) in offsets.iter().enumerate() {
662                let num = u32::try_from(i).unwrap_or(0) + 1;
663                x.add_normal(num, 0, false, *offset as u64, &Limits::default());
664            }
665        });
666
667        // The outermost object still reads: a `/Length` that cannot be
668        // resolved falls back to scanning for `endstream`, so exhausting the
669        // budget costs accuracy on the innermost links, not the whole read.
670        let first = s.get(1).expect("object 1");
671        assert_eq!(&*first.as_stream().expect("stream").data, b"DATA");
672    }
673
674    #[test]
675    fn object_zero_never_resolves() {
676        let s = store(b"", |x| {
677            x.add_normal(1, 0, false, 0, &Limits::default());
678        });
679        assert!(matches!(s.get(0), Err(Error::Unresolved(_))));
680    }
681
682    #[test]
683    fn numbers_past_the_table_are_unfetchable() {
684        let file = b"%PDF-1.7\n1 0 obj 5 endobj\n9 0 obj 7 endobj\n";
685        let s = store(file, |x| {
686            x.add_normal(1, 0, false, 9, &Limits::default());
687        });
688        // Object 9's bytes are right there, but the table ends at 1.
689        assert!(matches!(s.get(9), Err(Error::Unresolved(_))));
690    }
691
692    #[test]
693    fn a_free_entry_resolves_to_nothing() {
694        let s = store(b"1 0 obj 5 endobj", |x| {
695            x.add_normal(2, 0, false, 0, &Limits::default());
696            x.set_free(1, 1);
697        });
698        assert!(matches!(s.get(1), Err(Error::Unresolved(_))));
699    }
700
701    #[test]
702    fn a_header_naming_another_object_fails_the_fetch() {
703        let file = b"%PDF-1.7\n7 0 obj << >> endobj\n";
704        let s = store(file, |x| {
705            // The table says object 1 lives at the offset where 7 does.
706            x.add_normal(1, 0, false, 9, &Limits::default());
707        });
708        assert!(matches!(s.get(1), Err(Error::Unresolved(_))));
709        assert!(
710            s.drain_diags()
711                .contains(&pdfrum_common::DiagKind::ObjNumMismatch)
712        );
713    }
714
715    #[test]
716    fn a_self_referential_length_ends_as_a_keyword_scan() {
717        // Object 1's /Length points at object 1.
718        let file = b"%PDF-1.7\n1 0 obj << /Length 1 0 R >> stream\nDATA\nendstream endobj\n";
719        let s = store(file, |x| {
720            x.add_normal(1, 0, false, 9, &Limits::default());
721        });
722        let obj = s.get(1).expect("object");
723        assert_eq!(&*obj.as_stream().expect("stream").data, b"DATA");
724    }
725
726    #[test]
727    fn a_failed_fetch_is_retried_rather_than_remembered() {
728        let file = b"%PDF-1.7\nnot an object\n";
729        let s = store(file, |x| {
730            x.add_normal(1, 0, false, 9, &Limits::default());
731        });
732        assert!(s.get(1).is_err());
733        assert!(s.get(1).is_err());
734    }
735
736    #[test]
737    fn resolving_reads_through_the_trait() {
738        let file = b"%PDF-1.7\n1 0 obj << /Count 4 >> endobj\n";
739        let s = store(file, |x| {
740            x.add_normal(1, 0, false, 9, &Limits::default());
741        });
742        let fetched = s.fetch(ObjRef::new(1, 0)).expect("object");
743        assert_eq!(
744            fetched.as_dict().and_then(|d| d.direct_int(names::COUNT)),
745            Some(4)
746        );
747    }
748
749    #[test]
750    fn the_store_is_send_and_sync() {
751        fn assert_both<T: Send + Sync>() {}
752        assert_both::<ObjectStore>();
753    }
754}