Skip to main content

pdfrum_parser/
doc.rs

1//! Opening a document: the header, the load ladder, and the page tree.
2//!
3//! # The ladder
4//!
5//! [`load`] is a sequence of attempts, each one falling back to a cruder
6//! repair. Find the header; read the cross-reference information, rebuilding
7//! it from a full-file scan if the structured paths fail; set up decryption;
8//! then check that the trailer names a catalog and that the catalog names at
9//! least one page. If that last check fails the reader *throws away the
10//! table it just built* and rebuilds anyway, because a table that yields no
11//! pages is more likely stale than the file is empty.
12//!
13//! That retry is why the ladder is written as a ladder rather than a straight
14//! line: the same steps run twice with different inputs, and which rung a
15//! file lands on decides whether it opens at all.
16//!
17//! # `/Root` must be a reference
18//!
19//! A trailer whose `/Root` is a dictionary written inline is treated as
20//! having no catalog, even though the dictionary is right there. It reads as
21//! damage, and damage triggers the rebuild — which on real files finds a
22//! better catalog. Honoring the inline dictionary would skip that.
23//!
24//! # Counting pages without walking them
25//!
26//! A `/Pages` node's `/Count` is believed whenever it is positive and below
27//! the cap, without checking it against the tree. Files whose counts are
28//! wrong therefore report the wrong number — and lookups past the real end
29//! fail individually, which is exactly what a reader that trusted the walk
30//! instead would not reproduce.
31
32use std::sync::{Arc, Mutex};
33
34use pdfrum_common::{
35    DiagKind, Diagnostics, LimitExceeded, Limits, Operation, PageIndex, PdfVersion, Severity,
36};
37use pdfrum_crypt::{Permissions, SecurityHandler};
38use pdfrum_object::{ByteSpan, Dict, NoResolve, ObjRef, Object, Resolve, names};
39
40use crate::error::Error;
41use crate::store::ObjectStore;
42use crate::xref::{Trailer, Xref, XrefShape};
43
44/// How many bytes a `%PDF-1.7\n` header occupies, and the least a file can be.
45const HEADER_SIZE: usize = 9;
46
47/// Why a document could not be opened.
48#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
49#[non_exhaustive]
50pub enum LoadError {
51    /// No `%PDF` header within the first kilobyte, or a file too short to
52    /// hold one.
53    #[error("not a PDF file")]
54    NotPdf,
55
56    /// The document is encrypted and the password given does not open it.
57    /// Distinct from the others because callers ask again rather than give
58    /// up.
59    #[error("wrong password")]
60    WrongPassword,
61
62    /// The document uses a security handler this reader does not implement.
63    #[error("unsupported encryption: {0}")]
64    UnsupportedEncryption(String),
65
66    /// The file is damaged past what recovery could repair: no usable
67    /// cross-reference information, or no catalog with pages in it.
68    #[error("damaged beyond recovery: {0}")]
69    Broken(String),
70
71    /// The caller's `Limits::deadline` had passed when the open began, or
72    /// passed during the cross-reference rebuild scan.
73    #[error(transparent)]
74    Limit(LimitExceeded),
75}
76
77/// How to open a document.
78#[derive(Debug, Clone, Default)]
79pub struct LoadOptions {
80    /// The password to try, as raw bytes. Not capped in length.
81    pub password: Option<Vec<u8>>,
82    /// Caps to enforce while reading.
83    pub limits: Limits,
84}
85
86/// One page's dictionary, with the attributes it inherits already resolved.
87#[derive(Debug, Clone, PartialEq)]
88pub struct PageDict {
89    /// The page's own dictionary.
90    pub dict: Dict,
91    /// The reference it was reached through, when it had one. A page written
92    /// inline in its parent's `/Kids` has none.
93    pub reference: Option<ObjRef>,
94}
95
96impl PageDict {
97    /// An attribute of this page, looked up through its `/Parent` chain
98    /// (ISO 32000-1 §7.7.3.4).
99    ///
100    /// `/Resources`, `/MediaBox`, `/CropBox` and `/Rotate` are inheritable:
101    /// a page that does not state one takes its parent's, or its
102    /// grandparent's. The walk stops at the first node that states the key
103    /// *directly* — a value that is itself a reference is resolved, but a
104    /// `/Parent` that is not a dictionary ends the chain.
105    #[must_use]
106    pub fn inherited(&self, key: &pdfrum_object::Name, r: &impl Resolve) -> Option<Object> {
107        let mut node = self.dict.clone();
108        let mut seen: Vec<Dict> = Vec::new();
109        for _ in 0..64 {
110            if let Some(value) = node.get(key, r) {
111                return Some(value.get().clone());
112            }
113            // A cycle in the parent chain would otherwise spin forever.
114            if seen.contains(&node) {
115                return None;
116            }
117            seen.push(node.clone());
118            node = node.dict(names::PARENT, r)?;
119        }
120        None
121    }
122}
123
124/// An opened document.
125///
126/// Holds the file, everything the reader learned about where its objects are,
127/// and the lazy store that turns references into objects. `Send + Sync`, so
128/// pages can be rendered in parallel.
129#[derive(Debug)]
130pub struct Document {
131    /// The file from its header onwards; every offset indexes into this.
132    bytes: ByteSpan,
133    /// The trailer, merged across every section that contributed one.
134    trailer: Trailer,
135    /// The object store.
136    store: Arc<ObjectStore>,
137    /// The version the header declared, or `None` when it declared none.
138    version: Option<PdfVersion>,
139    /// Where the header was found in the original file.
140    header_offset: u64,
141    /// The shape of the cross-reference the load used, for the writer.
142    xref_shape: XrefShape,
143    /// The `/Encrypt` dictionary as the file wrote it, and whether the
144    /// trailer held it directly rather than by reference.
145    encrypt: Option<(Dict, bool)>,
146    /// How many pages the catalog says there are.
147    page_count: u32,
148    /// Page dictionaries found so far, by index.
149    pages: Mutex<PageCache>,
150    /// Everything repaired while opening the file.
151    pub diags: Diagnostics,
152}
153
154/// The page lookup's memory. The cache, not the index.
155#[derive(Debug, Default)]
156struct PageCache {
157    /// One slot per page, filled as pages are found.
158    slots: Vec<Option<PageDict>>,
159    /// Whether the tree turned out to be deeper than the cap, which stops
160    /// every later lookup as well.
161    poisoned: bool,
162}
163
164/// Open a document.
165///
166/// `bytes` is the whole file. Every repair the reader performed is in
167/// [`Document::diags`] afterwards, and a document that opened with a rebuilt
168/// table reports so through [`Document::xref_was_rebuilt`].
169///
170/// # Errors
171///
172/// [`LoadError::NotPdf`] for a file with no header, [`LoadError::WrongPassword`]
173/// and [`LoadError::UnsupportedEncryption`] for encryption the password or
174/// the reader cannot handle, and [`LoadError::Broken`] for damage recovery
175/// could not repair.
176///
177/// ```
178/// use std::sync::Arc;
179/// use pdfrum_parser::{LoadError, LoadOptions, load};
180///
181/// let not_a_pdf: Arc<[u8]> = Arc::from(&b"just some bytes"[..]);
182/// assert_eq!(load(not_a_pdf, &LoadOptions::default()).err(), Some(LoadError::NotPdf));
183/// ```
184pub fn load(bytes: impl Into<ByteSpan>, opts: &LoadOptions) -> Result<Document, LoadError> {
185    opts.limits
186        .check_deadline(Operation::Open)
187        .map_err(LoadError::Limit)?;
188    let mut diags = Diagnostics::default();
189    let bytes: ByteSpan = bytes.into();
190    let header_offset = find_header(&bytes, &opts.limits).ok_or(LoadError::NotPdf)?;
191    if bytes.len() < header_offset.saturating_add(HEADER_SIZE) {
192        return Err(LoadError::NotPdf);
193    }
194    if header_offset > 0 {
195        diags.record(
196            Severity::Recovered,
197            DiagKind::HeaderOffset,
198            Some(header_offset as u64),
199        );
200    }
201
202    // Everything before the header is invisible: offsets in the file are
203    // relative to it, so the reader works on the slice from there on.
204    //
205    // A window, not a copy: junk before `%PDF` costs a refcount bump like
206    // any other offset. Every stream is then a window into this one.
207    let body = bytes
208        .subspan(header_offset..bytes.len())
209        .unwrap_or_else(|_| ByteSpan::empty());
210    let version = read_version(&body);
211
212    let (xref, mut trailer, mut xref_shape) =
213        crate::xref::read_xref_full(&body, &opts.limits, &mut diags).map_err(|e| match e {
214            crate::Error::Limit(limit) => LoadError::Limit(limit),
215            other => LoadError::Broken(other.to_string()),
216        })?;
217
218    // First attempt: the catalog has to be reachable *and* have pages in it.
219    // Shared from here on: the stores below only read the table, and a slot
220    // vector is expensive to copy — see `ObjectStore`'s `xref` field. The
221    // rebuild path below re-shares after it mutates.
222    let mut shared_xref = Arc::new(xref);
223    let mut security = build_security(&body, &shared_xref, &trailer.dict, opts, &mut diags)?;
224    let mut store = build_store(&body, &shared_xref, opts, &trailer.dict, security);
225    let mut page_count = catalog_page_count(&store, &trailer.dict, &opts.limits);
226
227    if page_count.is_none() {
228        if xref_shape.rebuilt {
229            return Err(LoadError::Broken("no document catalog".into()));
230        }
231        // The table is the suspect, not the file: scan it and try again.
232        diags.record(Severity::Recovered, DiagKind::RootRecovered, None);
233        let mut fresh = Xref::new();
234        let mut fresh_trailer = Trailer::default();
235        let rebuilt = crate::xref::rebuild(
236            &body,
237            &mut fresh,
238            &mut fresh_trailer,
239            &opts.limits,
240            &mut diags,
241            &NoResolve,
242        )
243        .map_err(LoadError::Limit)?;
244        if !rebuilt {
245            return Err(LoadError::Broken("no document catalog".into()));
246        }
247        // The recovery path, so the copy this makes is not the common case:
248        // the first attempt's store still holds a reference, and the merged
249        // table has to be a fresh value for the second attempt's stores to
250        // share in turn.
251        let mut merged = (*shared_xref).clone();
252        merged.merge_up(&fresh);
253        crate::xref::merge_trailers(&mut trailer, &fresh_trailer);
254        xref_shape = XrefShape::rebuilt();
255
256        shared_xref = Arc::new(merged);
257        security = build_security(&body, &shared_xref, &trailer.dict, opts, &mut diags)?;
258        store = build_store(&body, &shared_xref, opts, &trailer.dict, security);
259        // Second attempt asks only for a catalog. A rebuilt document whose
260        // catalog is reachable but describes no pages still opens — it is
261        // then a document of zero pages, which is a thing a file can be.
262        if catalog(&store, &trailer.dict).is_none() {
263            return Err(LoadError::Broken("no document catalog".into()));
264        }
265        page_count = catalog_page_count(&store, &trailer.dict, &opts.limits);
266    }
267
268    let page_count = page_count.unwrap_or(0);
269    // Read last, from the trailer as it finally stands, so a rebuild that
270    // replaced the trailer is reflected.
271    let encrypt = encrypt_dict_located(&body, &shared_xref, &trailer.dict, &opts.limits);
272
273    diags.extend(&store.drain_diags());
274
275    Ok(Document {
276        bytes: body,
277        trailer,
278        store,
279        version,
280        header_offset: header_offset as u64,
281        xref_shape,
282        encrypt,
283        page_count,
284        pages: Mutex::new(PageCache {
285            slots: vec![None; usize::try_from(page_count).unwrap_or(0)],
286            poisoned: false,
287        }),
288        diags,
289    })
290}
291
292/// Find `%PDF` within the first `limits.header_scan` bytes.
293fn find_header(bytes: &[u8], limits: &Limits) -> Option<usize> {
294    let window = usize::try_from(limits.header_scan).unwrap_or(usize::MAX);
295    let last = bytes.len().checked_sub(4)?.min(window);
296    (0..=last).find(|&i| bytes.get(i..i + 4) == Some(b"%PDF"))
297}
298
299/// Read the version digits out of `%PDF-M.N`.
300///
301/// Never validated: a header claiming version 9.9 opens like any other, and a
302/// non-digit contributes nothing.
303///
304/// This is the one place in the workspace that knows the `major × 10 + minor`
305/// packing the old `Document::version() -> u8` published: the digits are read,
306/// packed, and immediately unpacked into a [`PdfVersion`]. The round trip is
307/// kept rather than removed because `0` — the answer when neither digit is
308/// readable — is what distinguishes "no version declared" from version 0.0,
309/// and collapsing the two would change which documents the writer gives its
310/// 1.7 fallback to.
311fn read_version(body: &[u8]) -> Option<PdfVersion> {
312    let digit = |i: usize| -> u8 {
313        body.get(i)
314            .filter(|b| b.is_ascii_digit())
315            .map_or(0, |b| b - b'0')
316    };
317    match digit(5).saturating_mul(10).saturating_add(digit(7)) {
318        0 => None,
319        packed => Some(PdfVersion::new(packed / 10, packed % 10)),
320    }
321}
322
323/// Build the security handler the trailer's `/Encrypt` calls for.
324fn build_security(
325    body: &ByteSpan,
326    xref: &Arc<Xref>,
327    trailer: &Dict,
328    opts: &LoadOptions,
329    diags: &mut Diagnostics,
330) -> Result<SecurityHandler, LoadError> {
331    let Some(encrypt) = encrypt_dict(body, xref, trailer, &opts.limits) else {
332        return Ok(SecurityHandler::Identity);
333    };
334    // The handler name is type-checked before being resolved, so a `/Filter`
335    // written as a string is not the standard handler however it spells it.
336    if encrypt.name(names::FILTER) != Some(names::STANDARD) {
337        return Err(LoadError::UnsupportedEncryption(
338            encrypt
339                .name(names::FILTER)
340                .map_or_else(|| "unnamed".to_owned(), |n| n.as_text().into_owned()),
341        ));
342    }
343
344    let file_id = trailer
345        .array(names::ID, &NoResolve)
346        .and_then(|a| a.string_at(0).map(|s| s.bytes.to_vec()))
347        .unwrap_or_default();
348    let password = opts.password.clone().unwrap_or_default();
349
350    match SecurityHandler::from_encrypt_dict(&encrypt, &file_id, &password, &NoResolve) {
351        Ok(handler) => {
352            if handler.password_encoding() != pdfrum_crypt::PasswordEncoding::AsGiven {
353                diags.record(Severity::Recovered, DiagKind::PasswordReencoded, None);
354            }
355            Ok(handler)
356        }
357        Err(pdfrum_crypt::Error::WrongPassword) => Err(LoadError::WrongPassword),
358        Err(pdfrum_crypt::Error::UnsupportedHandler(name)) => Err(
359            LoadError::UnsupportedEncryption(String::from_utf8_lossy(&name).into_owned()),
360        ),
361        Err(e) => Err(LoadError::UnsupportedEncryption(e.to_string())),
362    }
363}
364
365/// The `/Encrypt` dictionary, written inline or reached through one
366/// reference.
367///
368/// Most files write it indirectly, so the reference has to be chased — but
369/// the real store does not exist yet, and could not read this dictionary if
370/// it did, since it would try to decrypt it with the key this dictionary
371/// defines. So the lookup goes through a throwaway store that decrypts
372/// nothing. That is not a shortcut: the encryption dictionary is the one
373/// object in a document that is always plaintext.
374fn encrypt_dict(
375    body: &ByteSpan,
376    xref: &Arc<Xref>,
377    trailer: &Dict,
378    limits: &Limits,
379) -> Option<Dict> {
380    encrypt_dict_located(body, xref, trailer, limits).map(|(d, _)| d)
381}
382
383/// The same lookup, additionally reporting whether the trailer held the
384/// dictionary **directly** rather than by reference.
385///
386/// A writer needs that second fact: an inline encryption dictionary has no
387/// object number of its own, so it must be promoted to a fresh indirect
388/// object before the trailer's `/Encrypt` can name it (ISO 32000-1 §7.6.1
389/// requires `/Encrypt` be indirect).
390fn encrypt_dict_located(
391    body: &ByteSpan,
392    xref: &Arc<Xref>,
393    trailer: &Dict,
394    limits: &Limits,
395) -> Option<(Dict, bool)> {
396    match trailer.raw(names::ENCRYPT)? {
397        Object::Dict(d) => Some((d.clone(), true)),
398        Object::Ref(r) => {
399            let plain = ObjectStore::new(
400                body.clone(),
401                Arc::clone(xref),
402                limits.clone(),
403                SecurityHandler::Identity,
404            );
405            Some((plain.get(r.num).ok()?.as_dict().cloned()?, false))
406        }
407        _ => None,
408    }
409}
410
411/// Record the metadata object as exempt from decryption when the document
412/// says its metadata is not encrypted.
413fn exempt_metadata(store: &mut ObjectStore, trailer: &Dict) {
414    if store.security().encrypt_metadata() {
415        return;
416    }
417    let Some(root) = trailer.reference(names::ROOT) else {
418        return;
419    };
420    let Ok(catalog) = store.get(root.num) else {
421        return;
422    };
423    if let Some(metadata) = catalog.as_dict().and_then(|d| d.reference(names::METADATA)) {
424        store.exempt_from_decryption(metadata.num);
425    }
426}
427
428/// Build a store over the table, with the metadata exemption applied.
429fn build_store(
430    body: &ByteSpan,
431    xref: &Arc<Xref>,
432    opts: &LoadOptions,
433    trailer: &Dict,
434    security: SecurityHandler,
435) -> Arc<ObjectStore> {
436    let mut store = ObjectStore::new(
437        body.clone(),
438        Arc::clone(xref),
439        opts.limits.clone(),
440        security,
441    );
442    exempt_metadata(&mut store, trailer);
443    Arc::new(store)
444}
445
446/// The document catalog, if the trailer names one reachably.
447///
448/// A `/Root` written as anything but a reference does not name a catalog,
449/// even when it is a perfectly good dictionary written inline: that reads as
450/// damage, and damage is what triggers the rebuild that finds a better one.
451fn catalog(store: &ObjectStore, trailer: &Dict) -> Option<Dict> {
452    let root = trailer.reference(names::ROOT)?;
453    store.get(root.num).ok()?.as_dict().cloned()
454}
455
456/// How many pages the catalog claims, or `None` when there is no usable
457/// catalog or it describes no pages at all.
458fn catalog_page_count(store: &ObjectStore, trailer: &Dict, limits: &Limits) -> Option<u32> {
459    let catalog = catalog(store, trailer)?;
460    let count = page_count_of(store, &catalog, limits);
461    (count > 0).then_some(count)
462}
463
464/// The number of pages under a catalog.
465///
466/// A catalog without `/Pages` has none. A `/Pages` node without `/Kids` is
467/// itself the single page — that is not a repair, it is what a file with one
468/// page and no tree means.
469fn page_count_of(store: &ObjectStore, catalog: &Dict, limits: &Limits) -> u32 {
470    let Some(pages) = catalog.dict(names::PAGES, store) else {
471        return 0;
472    };
473    if pages.raw(names::KIDS).is_none() {
474        return 1;
475    }
476    // The root counts as an ancestor from the start, so a kid pointing back
477    // at it is a loop rather than a subtree.
478    let mut ancestors = vec![pages.clone()];
479    count_subtree(store, &pages, limits, &mut ancestors).unwrap_or(0)
480}
481
482/// Count the leaves under a node, or `None` when the tree claims more pages
483/// than a document may have.
484///
485/// `/Count` is believed whenever it is positive and under the cap, without
486/// checking it against the tree — so a file that lies about its length
487/// reports the lie, and the individual lookups past its real end are what
488/// fail.
489///
490/// The `None` propagates all the way out rather than being absorbed as a
491/// zero: a subtree that overflows makes the *whole* document uncountable,
492/// which is why this returns an `Option` instead of saturating.
493///
494/// `ancestors` holds the nodes currently being descended through, and is the
495/// cycle guard — the only one, since there is no depth cap here. Note what it
496/// is *not*: a record of every node already seen. A node listed twice among
497/// one parent's `/Kids` is counted twice, because the second listing is a
498/// sibling rather than a loop, and a file whose tree shares subtrees that way
499/// really does have that many pages.
500fn count_subtree(
501    store: &ObjectStore,
502    node: &Dict,
503    limits: &Limits,
504    ancestors: &mut Vec<Dict>,
505) -> Option<u32> {
506    if let Some(count) = node.int(names::COUNT, store)
507        && count > 0
508        && count < i64::from(limits.max_page_count)
509        && let Ok(count) = u32::try_from(count)
510    {
511        return Some(count);
512    }
513
514    let Some(kids) = node.array(names::KIDS, store) else {
515        return Some(0);
516    };
517    let mut total: u32 = 0;
518    for kid in kids.iter() {
519        let Some(kid) = kid.resolve(store).ok().and_then(|k| k.as_dict().cloned()) else {
520            continue;
521        };
522        // Only a kid that is already an ancestor would loop.
523        if ancestors.contains(&kid) {
524            continue;
525        }
526        total = total.saturating_add(match node_kind(&kid) {
527            NodeKind::Branch => {
528                ancestors.push(kid.clone());
529                let under = count_subtree(store, &kid, limits, ancestors);
530                ancestors.pop();
531                under?
532            }
533            NodeKind::Leaf => 1,
534        });
535        if total >= limits.max_page_count {
536            return None;
537        }
538    }
539    Some(total)
540}
541
542/// What a page-tree node is.
543enum NodeKind {
544    /// An interior node whose `/Kids` hold more nodes.
545    Branch,
546    /// A page.
547    Leaf,
548}
549
550/// Classify a node, guessing when `/Type` does not say.
551///
552/// A node with `/Kids` is a branch and one without is a page, whatever its
553/// `/Type` claims — files write the wrong type often enough that the
554/// structure is the more reliable witness.
555fn node_kind(node: &Dict) -> NodeKind {
556    match node.name(names::TYPE) {
557        Some(t) if t == names::PAGES => NodeKind::Branch,
558        Some(t) if t == names::PAGE => NodeKind::Leaf,
559        _ => {
560            if node.contains_key(names::KIDS) {
561                NodeKind::Branch
562            } else {
563                NodeKind::Leaf
564            }
565        }
566    }
567}
568
569impl Document {
570    /// How many pages the document has.
571    ///
572    /// A `u32`, deliberately, and not a
573    /// [`PageIndex`](pdfrum_common::PageIndex): a count answers "how many"
574    /// and an index answers "which one", and the last valid index of a
575    /// three-page document is 2, not 3. Giving them one type would let each be
576    /// passed where the other is meant, which is what the newtype exists to
577    /// stop.
578    #[must_use]
579    pub fn page_count(&self) -> u32 {
580        self.page_count
581    }
582
583    /// The page at `index`, counting from zero.
584    ///
585    /// The tree is walked in order and the pages found along the way are
586    /// remembered, so reading a document front to back costs one traversal.
587    /// A kid that will not load as a dictionary still **consumes its slot**:
588    /// a missing page leaves a hole rather than shifting every page after it.
589    ///
590    /// Takes `impl Into<PageIndex>`, so `doc.page(0)` reads as it always has.
591    ///
592    /// # Errors
593    ///
594    /// [`Error::NoPage`] for an index past the count, or one the walk could
595    /// not reach.
596    pub fn page(&self, index: impl Into<PageIndex>) -> Result<PageDict, Error> {
597        let index = index.into();
598        let slot = usize::try_from(index.get()).unwrap_or(usize::MAX);
599        if index.get() >= self.page_count {
600            return Err(Error::NoPage(index));
601        }
602        let Ok(mut pages) = self.pages.lock() else {
603            return Err(Error::NoPage(index));
604        };
605        if let Some(Some(found)) = pages.slots.get(slot) {
606            return Ok(found.clone());
607        }
608        if pages.poisoned {
609            return Err(Error::NoPage(index));
610        }
611
612        self.walk_pages(&mut pages);
613        pages
614            .slots
615            .get(slot)
616            .and_then(Clone::clone)
617            .ok_or(Error::NoPage(index))
618    }
619
620    /// Walk the whole tree once, filling every slot it can reach.
621    fn walk_pages(&self, pages: &mut PageCache) {
622        let Some(root) = self.trailer.dict.reference(names::ROOT) else {
623            return;
624        };
625        let Ok(catalog) = self.store.get(root.num) else {
626            return;
627        };
628        let Some(node) = catalog
629            .as_dict()
630            .and_then(|d| d.dict(names::PAGES, &*self.store))
631        else {
632            return;
633        };
634
635        let mut next: usize = 0;
636        let mut ancestors = Vec::new();
637        let objref = catalog.as_dict().and_then(|d| d.reference(names::PAGES));
638        self.visit(&node, objref, pages, &mut next, 0, &mut ancestors);
639    }
640
641    /// Depth-first, in order, filling slots as leaves are reached.
642    ///
643    /// `ancestors` is the cycle guard: the nodes on the path from the root to
644    /// here. A node that reappears as a *sibling* is a second page, not a
645    /// loop, so only an ancestor stops the descent.
646    fn visit(
647        &self,
648        node: &Dict,
649        reference: Option<ObjRef>,
650        pages: &mut PageCache,
651        next: &mut usize,
652        depth: u32,
653        ancestors: &mut Vec<Dict>,
654    ) {
655        if *next >= pages.slots.len() {
656            return;
657        }
658        // A node without `/Kids` is where the walk stops, whatever it claims
659        // to be — but a node that claims `/Type /Pages` and has no children
660        // is describing a subtree that is not there, so no page comes of it.
661        // (`page_count` still counts such a root as one page; the lookup is
662        // what fails.)
663        if node.raw(names::KIDS).is_none() {
664            if matches!(node_kind(node), NodeKind::Branch) {
665                return;
666            }
667            if let Some(slot) = pages.slots.get_mut(*next) {
668                *slot = Some(PageDict {
669                    dict: node.clone(),
670                    reference,
671                });
672            }
673            *next += 1;
674            return;
675        }
676
677        // Only a node with children can be too deep, so the cap is checked
678        // after the leaf case rather than on the way in. Exceeding it stops
679        // every later lookup too, not just this one.
680        if depth >= self.store.limits().max_page_tree_depth {
681            // The oracle records the same fact in the same shape:
682            // `reached_max_page_level_ = true` at `cpdf_document.cpp:281-283`,
683            // after which its own later lookups fail too. `poisoned` is that
684            // flag; the diagnostic is how a caller finds out *why* the pages
685            // stopped resolving.
686            self.store
687                .note(Severity::Suspicious, DiagKind::PageTreeDepthExceeded, None);
688            pages.poisoned = true;
689            return;
690        }
691
692        let Some(kids) = node.array(names::KIDS, &*self.store) else {
693            return;
694        };
695        ancestors.push(node.clone());
696        for kid in kids.iter() {
697            let kid_ref = kid.as_ref_id();
698            let loaded = kid
699                .resolve(&*self.store)
700                .ok()
701                .and_then(|k| k.as_dict().cloned());
702            let Some(loaded) = loaded else {
703                // A kid that will not load still costs a slot, so a missing
704                // page leaves a hole rather than shifting every page after it.
705                *next += 1;
706                continue;
707            };
708            // Only a kid that is already an ancestor would loop; the same
709            // node appearing twice as a sibling is two pages. PDFium skips a
710            // kid it has already visited for the same reason
711            // (`cpdf_document.cpp:87-88`), as part of the same pass that
712            // rewrites a wrong `/Count` (`:111`) and guesses a missing `/Type`
713            // (`:60`) — all of it "fix the in-memory representation for page
714            // tree nodes that violate the spec".
715            if ancestors.contains(&loaded) {
716                self.store
717                    .note(Severity::Recovered, DiagKind::PageTreeRepaired, None);
718                continue;
719            }
720            self.visit(&loaded, kid_ref, pages, next, depth + 1, ancestors);
721            if *next >= pages.slots.len() {
722                break;
723            }
724        }
725        ancestors.pop();
726    }
727
728    /// The trailer dictionary, merged across every section.
729    #[must_use]
730    pub fn trailer(&self) -> &Dict {
731        &self.trailer.dict
732    }
733
734    /// The object number the trailer came from; zero for a bare `trailer`
735    /// dictionary.
736    #[must_use]
737    pub fn trailer_object_number(&self) -> u32 {
738        self.trailer.object_number
739    }
740
741    /// The document catalog.
742    ///
743    /// # Errors
744    ///
745    /// [`Error::NoCatalog`] when the trailer names none.
746    pub fn catalog(&self) -> Result<Dict, Error> {
747        let root = self
748            .trailer
749            .dict
750            .reference(names::ROOT)
751            .ok_or(Error::NoCatalog)?;
752        self.store
753            .get(root.num)
754            .ok()
755            .and_then(|c| c.as_dict().cloned())
756            .ok_or(Error::NoCatalog)
757    }
758
759    /// The version the header declared: [`PdfVersion::PDF_1_7`] for
760    /// `%PDF-1.7`.
761    ///
762    /// Never validated — a header claiming 9.9 opens like any other and
763    /// reports 9.9. `None` means the header carried no readable digits at
764    /// all, which a file with no `%PDF` line and one with `%PDF-x.y` both
765    /// produce; the writer's fallback for that case is 1.7.
766    #[must_use]
767    pub fn version(&self) -> Option<PdfVersion> {
768        self.version
769    }
770
771    /// Where the `%PDF` header sat in the original file. Non-zero means
772    /// everything before it was ignored.
773    #[must_use]
774    pub fn header_offset(&self) -> u64 {
775        self.header_offset
776    }
777
778    /// What the object store has repaired since the file opened, as a running
779    /// total — read it *after* the work, not at load.
780    ///
781    /// [`Document::diags`] is the load-time snapshot and never changes. This
782    /// one grows, because the store is lazy: a wrong `/Length` or a bad table
783    /// offset is only discovered when a caller first reaches that object. It
784    /// clones rather than draining, so asking twice between two fetches gives
785    /// the same answer twice.
786    ///
787    /// ```
788    /// # use std::sync::Arc;
789    /// # use pdfrum_parser::{LoadOptions, load};
790    /// let bytes: Arc<[u8]> = Arc::from(&include_bytes!("../tests/files/minimal.pdf")[..]);
791    /// let doc = load(bytes, &LoadOptions::default())?;
792    /// // A clean file repairs nothing, before or after its pages are read.
793    /// let _ = doc.page(0);
794    /// assert!(doc.lazy_diagnostics().is_empty());
795    /// # Ok::<(), pdfrum_parser::LoadError>(())
796    /// ```
797    #[must_use]
798    pub fn lazy_diagnostics(&self) -> Diagnostics {
799        self.store.peek_diags()
800    }
801
802    /// Whether the cross-reference table came from the recovery scan rather
803    /// than the file's own sections. An incremental save is unsafe when it
804    /// did.
805    #[must_use]
806    pub fn xref_was_rebuilt(&self) -> bool {
807        self.xref_shape.rebuilt
808    }
809
810    /// Byte offset of the newest cross-reference section the load chained
811    /// from, or **0** when the table was rebuilt by scanning.
812    ///
813    /// This is what an incremental update writes as its `/Prev`, so the zero
814    /// carries meaning rather than being an absence: a rebuilt document has
815    /// no previous section worth naming, and the writer answers by emitting a
816    /// full table after the original bytes instead of a delta.
817    ///
818    /// ```
819    /// # use std::sync::Arc;
820    /// # use pdfrum_parser::{LoadOptions, load};
821    /// let bytes: Arc<[u8]> = Arc::from(&include_bytes!("../tests/files/minimal.pdf")[..]);
822    /// let doc = load(bytes, &LoadOptions::default())?;
823    /// assert!(doc.last_xref_offset() > 0);
824    /// assert!(!doc.xref_was_rebuilt());
825    /// # Ok::<(), pdfrum_parser::LoadError>(())
826    /// ```
827    #[must_use]
828    pub fn last_xref_offset(&self) -> u64 {
829        self.xref_shape.last_offset
830    }
831
832    /// Whether the document's **main** cross-reference — the newest section,
833    /// the one `startxref` names — was a stream rather than a classic table.
834    ///
835    /// Not "the chain contained a stream somewhere": a hybrid file whose
836    /// newest section is a classic table answers `false`. The writer reads it
837    /// to decide whether an incremental update appends a classic delta table
838    /// or folds the cross-reference into a stream object, and matching the
839    /// original keeps a reader that only understands one of the two working.
840    #[must_use]
841    pub fn main_xref_is_stream(&self) -> bool {
842        self.xref_shape.main_is_stream
843    }
844
845    /// The `/Encrypt` dictionary as the file wrote it, and whether the
846    /// trailer held it **directly** rather than by reference.
847    ///
848    /// Returned raw and undecrypted, because the encryption dictionary is the
849    /// one object in a document that is always plaintext. `Some` here does
850    /// not imply the document opened encrypted — a file can declare a handler
851    /// this reader answered with [`SecurityHandler::Identity`].
852    #[must_use]
853    pub fn encrypt_dict(&self) -> Option<(&Dict, bool)> {
854        self.encrypt.as_ref().map(|(d, inline)| (d, *inline))
855    }
856
857    /// What the document permits, for the password that opened it.
858    ///
859    /// The owner's own unrestricted view is
860    /// [`Document::owner_permissions`]. An unencrypted document permits
861    /// everything.
862    #[must_use]
863    pub fn permissions(&self) -> Permissions {
864        self.store.security().permissions()
865    }
866
867    /// What the document permits under the owner's view.
868    ///
869    /// Every permission, for a document the owner password opened; otherwise
870    /// the same answer as [`Document::permissions`].
871    #[must_use]
872    pub fn owner_permissions(&self) -> Permissions {
873        self.store.security().owner_permissions()
874    }
875
876    /// Whether the document is encrypted.
877    #[must_use]
878    pub fn is_encrypted(&self) -> bool {
879        !matches!(self.store.security(), SecurityHandler::Identity)
880    }
881
882    /// The security handler the password opened this document with.
883    ///
884    /// [`SecurityHandler::Identity`] for an unencrypted document, and for one
885    /// whose crypt filter is `/Identity`.
886    ///
887    /// The writer needs this to save an encrypted document *as encrypted*: it
888    /// re-enciphers every string and stream under the same handler, so the
889    /// result opens with the same password. It carries the file key, so it
890    /// is deliberately not `Clone`-friendly to hold onto — borrow it for the
891    /// length of a save and let it go.
892    #[must_use]
893    pub fn security_handler(&self) -> &SecurityHandler {
894        self.store.security()
895    }
896
897    /// The file, from its header onwards.
898    #[must_use]
899    pub fn bytes(&self) -> &[u8] {
900        &self.bytes
901    }
902
903    /// The object store, for fetching references.
904    #[must_use]
905    pub fn store(&self) -> &Arc<ObjectStore> {
906        &self.store
907    }
908
909    /// Where every object lives.
910    #[must_use]
911    pub fn xref(&self) -> &Xref {
912        self.store.xref()
913    }
914}
915
916impl Resolve for Document {
917    fn fetch(&self, r: ObjRef) -> Result<Arc<Object>, pdfrum_object::Error> {
918        self.store.fetch(r)
919    }
920}
921
922/// The content of `dict`'s page, decoded and joined, with the end offset of
923/// each `/Contents` element — read through `r`.
924pub fn content_segments(
925    dict: &PageDict,
926    r: &impl Resolve,
927    limits: &Limits,
928    diags: &mut Diagnostics,
929) -> (Vec<u8>, Vec<usize>) {
930    let Some(contents) = dict.dict.get(&pdfrum_object::Name::from("Contents"), r) else {
931        return (Vec::new(), Vec::new());
932    };
933    let mut out = Vec::new();
934    let mut ends = Vec::new();
935    let mut push = |object: &pdfrum_object::Object, out: &mut Vec<u8>, ends: &mut Vec<usize>| {
936        if let Some(stream) = object.as_stream() {
937            let decoded = pdfrum_filters::decode_chain(stream, 0, r, limits, diags);
938            out.extend_from_slice(&decoded.data);
939            // The separating space belongs to the element before it: it is
940            // what terminates a stream ending mid-token.
941            out.push(b' ');
942        }
943        ends.push(out.len());
944    };
945    let Some(direct) = contents.as_direct() else {
946        return (out, ends);
947    };
948    match direct {
949        pdfrum_object::Object::Stream(_) => push(direct, &mut out, &mut ends),
950        pdfrum_object::Object::Array(array) => {
951            for element in array.iter() {
952                if let Ok(resolved) = element.resolve(r) {
953                    push(resolved.get(), &mut out, &mut ends);
954                } else {
955                    // A dangling element still occupies an index, so the
956                    // ones after it keep their numbers.
957                    ends.push(out.len());
958                }
959            }
960        }
961        _ => {}
962    }
963    (out, ends)
964}
965
966/// The byte after the `%%EOF` that closes the revision whose `startxref`
967/// names `offset`.
968pub fn revision_end(bytes: &[u8], offset: u64) -> Option<usize> {
969    let needle = b"startxref";
970    let mut at = 0;
971    while let Some(found) = find(bytes.get(at..)?, needle) {
972        let start = at + found + needle.len();
973        let rest = bytes.get(start..)?;
974        let digits: String = rest
975            .iter()
976            .skip_while(|b| b.is_ascii_whitespace())
977            .take_while(|b| b.is_ascii_digit())
978            .map(|&b| char::from(b))
979            .collect();
980        if digits.parse::<u64>().ok() == Some(offset) {
981            let eof = find(rest, b"%%EOF")?;
982            let mut end = start + eof + b"%%EOF".len();
983            while bytes.get(end).is_some_and(|b| *b == b'\r' || *b == b'\n') {
984                end += 1;
985            }
986            return Some(end);
987        }
988        at = start;
989    }
990    None
991}
992
993fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
994    haystack.windows(needle.len()).position(|w| w == needle)
995}
996
997#[cfg(test)]
998mod tests {
999    use super::{LoadError, LoadOptions, find_header, load, read_version};
1000    use pdfrum_common::{DiagKind, Limits, PdfVersion};
1001    use pdfrum_crypt::Permissions;
1002    use pdfrum_object::{Name, names};
1003    use std::sync::Arc;
1004
1005    fn open(bytes: &[u8]) -> Result<super::Document, LoadError> {
1006        load(Arc::from(bytes), &LoadOptions::default())
1007    }
1008
1009    /// A document with `count` pages under one `/Pages` node.
1010    fn build(count: usize) -> Vec<u8> {
1011        let mut out = Vec::new();
1012        out.extend_from_slice(b"%PDF-1.7\n");
1013        let mut offsets = vec![0usize];
1014
1015        offsets.push(out.len());
1016        out.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
1017
1018        offsets.push(out.len());
1019        let kids: Vec<String> = (0..count).map(|i| format!("{} 0 R", i + 3)).collect();
1020        out.extend_from_slice(
1021            format!(
1022                "2 0 obj\n<< /Type /Pages /Count {count} /Kids [{}] /MediaBox [0 0 612 792] >>\nendobj\n",
1023                kids.join(" ")
1024            )
1025            .as_bytes(),
1026        );
1027
1028        for i in 0..count {
1029            offsets.push(out.len());
1030            out.extend_from_slice(
1031                format!(
1032                    "{} 0 obj\n<< /Type /Page /Parent 2 0 R /PageNumber {i} >>\nendobj\n",
1033                    i + 3
1034                )
1035                .as_bytes(),
1036            );
1037        }
1038
1039        let xref_at = out.len();
1040        out.extend_from_slice(format!("xref\n0 {}\n", offsets.len()).as_bytes());
1041        out.extend_from_slice(b"0000000000 65535 f \n");
1042        for offset in offsets.iter().skip(1) {
1043            out.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
1044        }
1045        out.extend_from_slice(
1046            format!(
1047                "trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref_at}\n%%EOF\n",
1048                offsets.len()
1049            )
1050            .as_bytes(),
1051        );
1052        out
1053    }
1054
1055    #[test]
1056    fn finds_a_header_at_the_start_or_after_junk() {
1057        assert_eq!(find_header(b"%PDF-1.7\n", &Limits::default()), Some(0));
1058        assert_eq!(find_header(b"junk%PDF-1.7\n", &Limits::default()), Some(4));
1059        assert_eq!(find_header(b"no header", &Limits::default()), None);
1060    }
1061
1062    #[test]
1063    fn version_digits_are_read_not_validated() {
1064        assert_eq!(read_version(b"%PDF-1.7\n"), Some(PdfVersion::PDF_1_7));
1065        assert_eq!(read_version(b"%PDF-2.0\n"), Some(PdfVersion::PDF_2_0));
1066        assert_eq!(read_version(b"%PDF-x.y\n"), None);
1067        // The private packing round-trips every digit pair the header can
1068        // spell, and only `0.0` — which is unreachable, since a `0` packed
1069        // value is reported as "no version" — is not a `Some`.
1070        for major in 0..=9u8 {
1071            for minor in 0..=9u8 {
1072                let header = format!("%PDF-{major}.{minor}\n");
1073                let expected = (major, minor) != (0, 0);
1074                assert_eq!(
1075                    read_version(header.as_bytes()),
1076                    expected.then(|| PdfVersion::new(major, minor)),
1077                    "header {header:?}"
1078                );
1079            }
1080        }
1081    }
1082
1083    #[test]
1084    fn a_file_without_a_header_is_not_a_pdf() {
1085        assert_eq!(open(b"just some bytes").err(), Some(LoadError::NotPdf));
1086        // A header at the very end with no room for a document.
1087        assert_eq!(open(b"%PDF").err(), Some(LoadError::NotPdf));
1088    }
1089
1090    #[test]
1091    fn opens_a_document_and_counts_its_pages() {
1092        let doc = open(&build(3)).expect("document");
1093        assert_eq!(doc.page_count(), 3);
1094        assert_eq!(doc.version(), Some(PdfVersion::PDF_1_7));
1095        assert!(!doc.xref_was_rebuilt());
1096        assert!(!doc.is_encrypted());
1097        assert_eq!(doc.permissions(), Permissions::ALL);
1098    }
1099
1100    #[test]
1101    fn reads_pages_in_order() {
1102        let doc = open(&build(5)).expect("document");
1103        let number = Name::from("PageNumber");
1104        for i in 0..5u32 {
1105            let page = doc.page(i).expect("page");
1106            assert_eq!(page.dict.direct_int(&number), Some(i64::from(i)));
1107        }
1108        assert!(doc.page(5).is_err());
1109    }
1110
1111    #[test]
1112    fn reads_pages_in_reverse_and_out_of_order() {
1113        let doc = open(&build(5)).expect("document");
1114        let number = Name::from("PageNumber");
1115        for i in (0..5u32).rev() {
1116            assert_eq!(
1117                doc.page(i).expect("page").dict.direct_int(&number),
1118                Some(i64::from(i))
1119            );
1120        }
1121        // An out-of-range lookup must not poison the ones after it.
1122        assert!(doc.page(99).is_err());
1123        assert_eq!(doc.page(3).expect("page").dict.direct_int(&number), Some(3));
1124    }
1125
1126    #[test]
1127    fn a_count_larger_than_the_tree_reports_the_lie() {
1128        let text = String::from_utf8_lossy(&build(3)).replace("/Count 3", "/Count 9");
1129        let doc = open(text.as_bytes()).expect("document");
1130        // The claimed count is what the document reports...
1131        assert_eq!(doc.page_count(), 9);
1132        // ...and the pages that exist still resolve.
1133        assert!(doc.page(0).is_ok());
1134        assert!(doc.page(2).is_ok());
1135        // The ones past the real tree do not.
1136        assert!(doc.page(3).is_err());
1137        assert!(doc.page(8).is_err());
1138        // And the real ones still work afterwards.
1139        assert!(doc.page(2).is_ok());
1140    }
1141
1142    #[test]
1143    fn a_kids_less_pages_node_counts_as_a_page_it_cannot_produce() {
1144        // Counting and looking up disagree here, and both are right. The
1145        // count treats a `/Pages` node with no `/Kids` as the document's one
1146        // page, but the walk refuses to hand back a node that calls itself a
1147        // branch — so the document reports one page and has none.
1148        let file = b"%PDF-1.7\n\
1149                     1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n\
1150                     2 0 obj\n<< /Type /Pages /Count 3 >>\nendobj\n\
1151                     trailer\n<< /Root 1 0 R >>\nstartxref\n0\n%%EOF\n";
1152        let doc = open(file).expect("document");
1153        assert_eq!(doc.page_count(), 1);
1154        assert!(doc.page(0).is_err());
1155        assert!(doc.page(1).is_err());
1156    }
1157
1158    #[test]
1159    fn a_kids_less_node_that_does_not_claim_to_be_a_branch_is_a_page() {
1160        // The same shape without the `/Type /Pages` claim: the node has no
1161        // children, so it is the page itself and the lookup succeeds.
1162        let file = b"%PDF-1.7\n\
1163                     1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n\
1164                     2 0 obj\n<< /MediaBox [0 0 10 10] >>\nendobj\n\
1165                     trailer\n<< /Root 1 0 R >>\nstartxref\n0\n%%EOF\n";
1166        let doc = open(file).expect("document");
1167        assert_eq!(doc.page_count(), 1);
1168        assert!(doc.page(0).is_ok());
1169    }
1170
1171    #[test]
1172    fn a_catalog_without_pages_will_not_open() {
1173        let file = b"%PDF-1.7\n\
1174                     1 0 obj\n<< /Type /Catalog >>\nendobj\n\
1175                     trailer\n<< /Root 1 0 R >>\nstartxref\n0\n%%EOF\n";
1176        assert!(matches!(open(file), Err(LoadError::Broken(_))));
1177    }
1178
1179    #[test]
1180    fn a_root_written_inline_does_not_name_a_catalog() {
1181        let file = b"%PDF-1.7\n\
1182                     1 0 obj\n<< /Type /Page >>\nendobj\n\
1183                     trailer\n<< /Root << /Type /Catalog /Pages 2 0 R >> >>\n\
1184                     startxref\n0\n%%EOF\n";
1185        assert!(matches!(open(file), Err(LoadError::Broken(_))));
1186    }
1187
1188    #[test]
1189    fn a_broken_start_xref_still_opens_the_document() {
1190        let text = String::from_utf8_lossy(&build(2)).into_owned();
1191        let broken = text
1192            .replace("%%EOF", "")
1193            .replace("startxref\n", "startxref\n1\n");
1194        let doc = open(broken.as_bytes()).expect("document");
1195        assert!(doc.xref_was_rebuilt());
1196        assert_eq!(doc.page_count(), 2);
1197        assert!(doc.diags.contains(&DiagKind::XrefRebuilt));
1198    }
1199
1200    #[test]
1201    fn a_header_after_junk_shifts_every_offset() {
1202        let mut file = vec![b'x'; 100];
1203        file.extend_from_slice(&build(2));
1204        let doc = open(&file).expect("document");
1205        assert_eq!(doc.header_offset(), 100);
1206        assert_eq!(doc.page_count(), 2);
1207        assert!(doc.diags.contains(&DiagKind::HeaderOffset));
1208    }
1209
1210    #[test]
1211    fn inheritable_attributes_come_from_the_parent() {
1212        let doc = open(&build(2)).expect("document");
1213        let page = doc.page(0).expect("page");
1214        // The page states no /MediaBox; its /Pages parent does.
1215        let inherited = page
1216            .inherited(names::MEDIA_BOX, &doc)
1217            .expect("inherited media box");
1218        let array = inherited.as_array().expect("array");
1219        assert_eq!(array.number_at(2), Some(612.0));
1220        assert_eq!(array.number_at(3), Some(792.0));
1221        // A key nobody states is absent.
1222        assert!(page.inherited(names::ROTATE, &doc).is_none());
1223    }
1224
1225    #[test]
1226    fn a_pages_reference_is_reported() {
1227        let doc = open(&build(1)).expect("document");
1228        assert_eq!(doc.page(0).expect("page").reference.map(|r| r.num), Some(3));
1229    }
1230
1231    #[test]
1232    fn documents_are_send_and_sync() {
1233        fn assert_both<T: Send + Sync>() {}
1234        assert_both::<super::Document>();
1235    }
1236
1237    #[test]
1238    fn never_panics_on_arbitrary_bytes() {
1239        let seeds: &[&[u8]] = &[
1240            b"",
1241            b"%PDF",
1242            b"%PDF-1.7",
1243            b"%PDF-1.7\nstartxref\n0\n%%EOF",
1244            b"%PDF-1.7\ntrailer<</Root 1 0 R>>",
1245            b"%PDF-1.7\n1 0 obj<</Length 1 0 R>>stream\n",
1246            b"%PDF-1.7\n\x00\xff\x80\x0b",
1247        ];
1248        for seed in seeds {
1249            let _ = open(seed);
1250        }
1251    }
1252}