Skip to main content

pdfboss_core/
document.rs

1//! The document model: loading, object resolution with caching, the
2//! lazily-flattened page tree with attribute inheritance, and document
3//! metadata.
4
5use crate::hash::{FastMap, FastSet};
6use std::cell::{OnceCell, RefCell};
7use std::path::Path;
8use std::rc::Rc;
9use std::sync::Arc;
10
11use crate::crypt::Decryptor;
12use crate::elements::Span;
13use crate::error::{Error, Result};
14use crate::filters;
15use crate::geom::Rect;
16use crate::object::{decode_text_string, Dict, ObjRef, Object, Stream};
17use crate::objstm;
18use crate::parser::{Parser, Resolve};
19use crate::source::{block_on, AsyncObjectSource, Immediate};
20use crate::xref::{load_xref, Xref, XrefEntry};
21
22/// Page-tree traversal depth cap.
23const MAX_TREE_DEPTH: usize = 256;
24
25/// A loaded PDF document.
26pub struct Document {
27    data: Arc<Vec<u8>>,
28    version: (u8, u8),
29    xref: Arc<Xref>,
30    /// Interior cache of fetched indirect objects.
31    cache: RefCell<FastMap<(u32, u16), Rc<Object>>>,
32    /// Object numbers currently being parsed, guarding re-entrant fetches
33    /// (e.g. a stream whose `/Length` refers back to the stream itself).
34    loading: RefCell<FastSet<u32>>,
35    /// Decoded object streams, keyed by their stream object number, so a
36    /// stream is decompressed and its header parsed at most once even when
37    /// many compressed objects are read from it.
38    objstms: RefCell<FastMap<u32, Rc<objstm::ObjStm>>>,
39    /// Present when the file uses the Standard security handler (RC4 or AES)
40    /// and opens under the empty user password; decrypts strings and stream
41    /// data as objects are loaded from the file.
42    decryptor: Option<Decryptor>,
43    /// The flattened page tree, built lazily on the first page access so that
44    /// merely opening a document (or reading its page count) never parses
45    /// every page dictionary. See [`Document::pages`]. Shared so a
46    /// [`Document::fork`] does not re-walk the tree.
47    pages: OnceCell<Arc<Vec<PageRec>>>,
48}
49
50/// A document's immutable core, detached from its single-threaded caches:
51/// the file bytes, the merged cross-reference table, the decryption key
52/// material, and the flattened page tree. `Send + Sync` (asserted in this
53/// module's tests), which the [`Document`] deliberately is not — this is
54/// the value that crosses a thread boundary, one fresh document
55/// materializing from it on each side. See [`Document::seed`],
56/// [`Document::from_seed`] and [`map_pages`].
57pub struct DocumentSeed {
58    data: Arc<Vec<u8>>,
59    version: (u8, u8),
60    xref: Arc<Xref>,
61    decryptor: Option<Decryptor>,
62    pages: Arc<Vec<PageRec>>,
63}
64
65impl Clone for DocumentSeed {
66    fn clone(&self) -> DocumentSeed {
67        DocumentSeed {
68            data: Arc::clone(&self.data),
69            version: self.version,
70            xref: Arc::clone(&self.xref),
71            decryptor: self.decryptor.clone(),
72            pages: Arc::clone(&self.pages),
73        }
74    }
75}
76
77/// The flattened, inheritance-applied record for one page.
78struct PageRec {
79    obj_ref: Option<ObjRef>,
80    media_box: Rect,
81    crop_box: Rect,
82    rotate: i32,
83    resources: Dict,
84    dict: Dict,
85}
86
87/// Attributes inherited down the page tree (ISO 32000 §7.7.3.4).
88#[derive(Clone, Default)]
89struct Inherited {
90    resources: Option<Dict>,
91    media_box: Option<Rect>,
92    crop_box: Option<Rect>,
93    rotate: Option<i32>,
94}
95
96/// Parses the `%PDF-x.y` header, scanning the first 1 KiB; absent or
97/// malformed headers default to version 1.4.
98fn parse_version(data: &[u8]) -> (u8, u8) {
99    try_parse_version(data).unwrap_or((1, 4))
100}
101
102fn try_parse_version(data: &[u8]) -> Option<(u8, u8)> {
103    let window = &data[..data.len().min(1024)];
104    let pos = memchr::memmem::find(window, b"%PDF-")?;
105    let rest = &window[pos + 5..];
106    let (major, used) = read_version_component(rest)?;
107    if rest.get(used) != Some(&b'.') {
108        return None;
109    }
110    let (minor, _) = read_version_component(&rest[used + 1..])?;
111    Some((major, minor))
112}
113
114/// Reads a run of 1–3 ASCII digits as a `u8`, returning the value and the
115/// number of bytes consumed.
116fn read_version_component(bytes: &[u8]) -> Option<(u8, usize)> {
117    let end = bytes
118        .iter()
119        .position(|b| !b.is_ascii_digit())
120        .unwrap_or(bytes.len());
121    if end == 0 || end > 3 {
122        return None;
123    }
124    let value = std::str::from_utf8(&bytes[..end]).ok()?.parse().ok()?;
125    Some((value, end))
126}
127
128/// Normalizes a `/Rotate` value to one of {0, 90, 180, 270}; values that
129/// are not multiples of 90 fall back to 0 (lenient).
130fn normalize_rotation(deg: i32) -> i32 {
131    let r = deg.rem_euclid(360);
132    if r % 90 == 0 {
133        r
134    } else {
135        0
136    }
137}
138
139impl Document {
140    /// Loads a document from bytes: locates the `%PDF-x.y` header (scanning
141    /// the first 1 KiB, defaulting to 1.4), loads the xref, and sets up
142    /// decryption for files using the Standard security handler (RC4 or AES)
143    /// under the empty user password (password-protected files yield
144    /// [`Error::Encrypted`]).
145    ///
146    /// The page tree is **not** walked here: it is flattened lazily on the
147    /// first page access, so opening a document (or reading `page_count`) does
148    /// not parse every page dictionary.
149    pub fn load(data: Vec<u8>) -> Result<Document> {
150        Document::load_with_password(data, "")
151    }
152
153    /// [`Document::load`] with the password that opens the file, accepted as
154    /// either the user or the owner password. The empty string is the
155    /// transparent empty-user-password case [`Document::load`] always
156    /// handles; a password the file does not accept yields
157    /// [`Error::Encrypted`]. Non-ASCII passwords are tried UTF-8 encoded
158    /// and, for the legacy RC4/AES-128 revisions, Latin-1 encoded as well,
159    /// covering both encodings real files use.
160    pub fn load_with_password(data: Vec<u8>, password: &str) -> Result<Document> {
161        let version = parse_version(&data);
162        let xref = load_xref(&data)?;
163        let mut doc = Document {
164            data: Arc::new(data),
165            version,
166            xref: Arc::new(xref),
167            cache: RefCell::new(FastMap::default()),
168            loading: RefCell::new(FastSet::default()),
169            objstms: RefCell::new(FastMap::default()),
170            decryptor: None,
171            pages: OnceCell::new(),
172        };
173        if doc
174            .xref
175            .trailer
176            .get("Encrypt")
177            .is_some_and(|o| !o.is_null())
178        {
179            doc.setup_decryption(password)?;
180        }
181        Ok(doc)
182    }
183
184    /// Reads the file at `path` and loads it via
185    /// [`Document::load_with_password`].
186    pub fn open_with_password(path: impl AsRef<Path>, password: &str) -> Result<Document> {
187        Document::load_with_password(std::fs::read(path)?, password)
188    }
189
190    /// Configures decryption for an encrypted file. Supports the Standard
191    /// security handler with RC4 (`/V` 1–2), AESV2 (`/V` 4) and AESV3 (`/V` 5)
192    /// with `password` as the user or owner password (the empty string is
193    /// the transparent empty-user-password case); a password the file does
194    /// not accept is reported as [`Error::Encrypted`]. Must run before any
195    /// content object is fetched, and reads `/Encrypt` and `/ID` while
196    /// decryption is still off (those values are stored unencrypted).
197    fn setup_decryption(&mut self, password: &str) -> Result<()> {
198        let enc_obj = self
199            .xref
200            .trailer
201            .get("Encrypt")
202            .cloned()
203            .unwrap_or(Object::Null);
204        let enc = self.resolve(&enc_obj)?;
205        let enc_dict = enc.as_dict().ok_or(Error::Encrypted)?;
206        let id0: Vec<u8> = self
207            .xref
208            .trailer
209            .get("ID")
210            .and_then(Object::as_array)
211            .and_then(<[Object]>::first)
212            .and_then(Object::as_str_bytes)
213            .unwrap_or(&[])
214            .to_vec();
215        match Decryptor::from_standard_with_password_str(enc_dict, &id0, password) {
216            Some(dec) => {
217                self.decryptor = Some(dec);
218                // Objects fetched while resolving /Encrypt were cached without
219                // decryption; drop them so they are re-read through the
220                // decrypting path if referenced again.
221                self.cache.borrow_mut().clear();
222                Ok(())
223            }
224            None => Err(Error::Encrypted),
225        }
226    }
227
228    /// Reads the file at `path` and loads it via [`Document::load`].
229    pub fn open(path: impl AsRef<Path>) -> Result<Document> {
230        Document::load(std::fs::read(path)?)
231    }
232
233    /// The PDF version from the header, e.g. `(1, 7)`.
234    pub fn version(&self) -> (u8, u8) {
235        self.version
236    }
237
238    /// Raw bytes of the loaded file.
239    pub fn bytes(&self) -> &[u8] {
240        &self.data
241    }
242
243    /// The merged cross-reference table and trailer.
244    pub fn xref(&self) -> &Xref {
245        &self.xref
246    }
247
248    /// Fetches an indirect object by reference (xref lookup, object-stream
249    /// indirection, cached). A generation mismatch between the request and
250    /// the file is tolerated (lenient).
251    pub fn get(&self, r: ObjRef) -> Result<Object> {
252        if let Some(cached) = self.cache.borrow().get(&(r.num, r.gen)) {
253            return Ok((**cached).clone());
254        }
255        if !self.loading.borrow_mut().insert(r.num) {
256            return Err(Error::CircularReference(r.num));
257        }
258        let result = self.load_object(r);
259        self.loading.borrow_mut().remove(&r.num);
260        let object = result?;
261        self.cache
262            .borrow_mut()
263            .insert((r.num, r.gen), Rc::new(object.clone()));
264        Ok(object)
265    }
266
267    /// Uncached fetch: parses the object at its file offset or extracts it
268    /// from its containing object stream.
269    fn load_object(&self, r: ObjRef) -> Result<Object> {
270        match self.xref.get(r.num) {
271            None | Some(XrefEntry::Free) => Err(Error::ObjectNotFound(r.num, r.gen)),
272            Some(XrefEntry::InFile { offset, .. }) => {
273                let offset = usize::try_from(offset)
274                    .ok()
275                    .filter(|&o| o < self.data.len())
276                    .ok_or(Error::ObjectNotFound(r.num, r.gen))?;
277                self.object_at_spanned(offset).map(|parsed| parsed.1)
278            }
279            Some(XrefEntry::InStream { stream_num, index }) => {
280                self.load_from_object_stream(stream_num, index)
281            }
282        }
283    }
284
285    /// Parses the indirect object at `offset`, applying decryption, and
286    /// reports the byte range consumed (`N G obj … endobj`).
287    pub(crate) fn object_at_spanned(&self, offset: usize) -> Result<(ObjRef, Object, Span)> {
288        let mut parser = Parser::at(&self.data, offset);
289        let (r, mut object) = parser.parse_indirect(self)?;
290        // Objects stored directly in the file carry encrypted strings and
291        // stream data; decrypt with this object's key. (Objects living in
292        // object streams are decrypted with their container.)
293        if let Some(dec) = &self.decryptor {
294            dec.decrypt_object(&mut object, r.num, r.gen);
295        }
296        Ok((r, object, Span::new(offset as u64, parser.pos() as u64)))
297    }
298
299    /// The decoded, header-parsed object stream `stream_num`, built at most
300    /// once and cached.
301    pub(crate) fn objstm_handle(&self, stream_num: u32) -> Result<Rc<objstm::ObjStm>> {
302        if let Some(stm) = self.objstms.borrow().get(&stream_num) {
303            return Ok(Rc::clone(stm));
304        }
305        let container = self.get(ObjRef {
306            num: stream_num,
307            gen: 0,
308        })?;
309        let stream = container.as_stream().ok_or_else(|| Error::TypeMismatch {
310            expected: "stream",
311            found: type_name(&container),
312        })?;
313        let n = self
314            .resolve(stream.dict.get("N").unwrap_or(&Object::Null))?
315            .as_int()
316            .and_then(|v| usize::try_from(v).ok())
317            .ok_or(Error::MissingKey("N"))?;
318        let first = self
319            .resolve(stream.dict.get("First").unwrap_or(&Object::Null))?
320            .as_int()
321            .and_then(|v| usize::try_from(v).ok())
322            .ok_or(Error::MissingKey("First"))?;
323        let decoded = self.stream_data(stream)?;
324        let stm = Rc::new(objstm::ObjStm::parse(decoded, n, first)?);
325        self.objstms
326            .borrow_mut()
327            .insert(stream_num, Rc::clone(&stm));
328        Ok(stm)
329    }
330
331    /// Extracts a compressed object from the object stream `stream_num`.
332    fn load_from_object_stream(&self, stream_num: u32, index: u32) -> Result<Object> {
333        self.objstm_handle(stream_num)?.object(index)
334    }
335
336    /// Chases reference chains with a depth guard of
337    /// [`MAX_RESOLVE_DEPTH`](crate::source::MAX_RESOLVE_DEPTH) (beyond that:
338    /// [`Error::CircularReference`], naming the last reference followed); a
339    /// reference to a missing or unreadable object resolves to `Null`
340    /// (lenient).
341    ///
342    /// The loop is [`crate::source::resolve_sync_with`], shared with the
343    /// provided [`crate::source::ObjectSource::resolve`], so the two cannot
344    /// drift apart. It reaches `Document::get` through this type's
345    /// `ObjectSource` implementation, which forwards to it unchanged.
346    pub fn resolve(&self, o: &Object) -> Result<Object> {
347        crate::source::resolve_sync_with(self, o)
348    }
349
350    /// Decodes a stream's data through its filter chain, resolving indirect
351    /// filter parameters against this document.
352    pub fn stream_data(&self, s: &Stream) -> Result<Vec<u8>> {
353        filters::decode_stream(s, self)
354    }
355
356    /// The flattened page tree, built once on first access.
357    fn pages(&self) -> &[PageRec] {
358        self.pages.get_or_init(|| Arc::new(self.flatten_pages()))
359    }
360
361    /// The document's shareable core: everything immutable, nothing cached.
362    /// Forces the page tree first, so no document built from the seed
363    /// re-walks it.
364    pub fn seed(&self) -> DocumentSeed {
365        self.pages();
366        DocumentSeed {
367            data: Arc::clone(&self.data),
368            version: self.version,
369            xref: Arc::clone(&self.xref),
370            decryptor: self.decryptor.clone(),
371            pages: Arc::clone(self.pages.get().expect("pages() was just forced")),
372        }
373    }
374
375    /// A handle to the same document for another thread: share a
376    /// [`DocumentSeed`] across the thread boundary — the seed is `Send` and
377    /// `Sync`; the document, whose caches are single-threaded by design, is
378    /// neither — and materialize one of these per thread.
379    ///
380    /// The immutable core is shared — the file bytes, the merged
381    /// cross-reference table, the decryption key material, and the flattened
382    /// page tree — while the interior caches start fresh, private to this
383    /// document. That is the whole design: per-page work is independent and
384    /// lock-free precisely because nothing mutable is shared, so N documents
385    /// on N threads contend on nothing.
386    ///
387    /// [`map_pages`] is the ready-made consumer.
388    pub fn from_seed(seed: DocumentSeed) -> Document {
389        Document {
390            data: seed.data,
391            version: seed.version,
392            xref: seed.xref,
393            cache: RefCell::new(FastMap::default()),
394            loading: RefCell::new(FastSet::default()),
395            objstms: RefCell::new(FastMap::default()),
396            decryptor: seed.decryptor,
397            pages: OnceCell::from(seed.pages),
398        }
399    }
400
401    /// [`Document::seed`] and [`Document::from_seed`] in one step, for a
402    /// fork used on the calling thread.
403    pub fn fork(&self) -> Document {
404        Document::from_seed(self.seed())
405    }
406
407    /// Number of pages.
408    ///
409    /// Reports the page tree's declared `/Count` — the same value mature
410    /// engines return — without walking the tree, so it is cheap on an
411    /// otherwise-untouched document. If `/Count` is absent or implausible the
412    /// tree is flattened and its true (lenient, cycle- and depth-guarded)
413    /// length is returned instead. Once the tree has been flattened for any
414    /// reason, that flattened length is authoritative.
415    pub fn page_count(&self) -> usize {
416        if let Some(pages) = self.pages.get() {
417            return pages.len();
418        }
419        if let Some(count) = self.declared_page_count() {
420            return count;
421        }
422        self.pages().len()
423    }
424
425    /// Reads the page tree root's `/Count` cheaply (Root → `/Pages` →
426    /// `/Count`) without descending into `/Kids`. Returns `None` when the
427    /// entry is missing, non-integer, negative, or larger than the file could
428    /// possibly hold (a corrupt count), so the caller falls back to a real
429    /// walk.
430    fn declared_page_count(&self) -> Option<usize> {
431        let root = self.xref.trailer.get("Root")?;
432        let catalog = self.resolve(root).ok()?;
433        let pages = self.resolve(catalog.as_dict()?.get("Pages")?).ok()?;
434        let count = usize::try_from(self.int_value(pages.as_dict()?, "Count")?).ok()?;
435        // A page occupies at least a handful of bytes on disk, so a count that
436        // exceeds the file length is corrupt: fall back to walking the tree.
437        (count <= self.data.len()).then_some(count)
438    }
439
440    /// The page at 0-based `index`.
441    pub fn page(&self, index: usize) -> Result<Page> {
442        let pages = self.pages();
443        let rec = pages
444            .get(index)
445            .ok_or(Error::PageNotFound(index, pages.len()))?;
446        Ok(Page {
447            index,
448            media_box: rec.media_box,
449            crop_box: rec.crop_box,
450            rotate: rec.rotate,
451            resources: rec.resources.clone(),
452            dict: rec.dict.clone(),
453            obj_ref: rec.obj_ref,
454        })
455    }
456
457    /// Document metadata from the trailer `/Info` dictionary (lenient:
458    /// absent or malformed entries are simply `None`).
459    pub fn metadata(&self) -> Metadata {
460        let mut meta = Metadata::default();
461        let Some(info) = self.xref.trailer.get("Info") else {
462            return meta;
463        };
464        let Ok(info) = self.resolve(info) else {
465            return meta;
466        };
467        let Some(dict) = info.as_dict() else {
468            return meta;
469        };
470        meta.title = self.meta_string(dict, "Title");
471        meta.author = self.meta_string(dict, "Author");
472        meta.subject = self.meta_string(dict, "Subject");
473        meta.keywords = self.meta_string(dict, "Keywords");
474        meta.creator = self.meta_string(dict, "Creator");
475        meta.producer = self.meta_string(dict, "Producer");
476        meta.creation_date = self.meta_string(dict, "CreationDate");
477        meta.mod_date = self.meta_string(dict, "ModDate");
478        meta
479    }
480
481    /// Reads `key` from an info dictionary as a decoded text string.
482    fn meta_string(&self, dict: &Dict, key: &str) -> Option<String> {
483        let value = self.resolve(dict.get(key)?).ok()?;
484        Some(decode_text_string(value.as_str_bytes()?))
485    }
486
487    /// Flattens the page tree by iterative depth-first traversal of `/Kids`
488    /// with a visited-reference cycle guard and a depth cap, applying
489    /// attribute inheritance. Any structural problem simply truncates or
490    /// skips (lenient) — this never fails.
491    fn flatten_pages(&self) -> Vec<PageRec> {
492        let mut pages = Vec::new();
493        let Some(root) = self.xref.trailer.get("Root") else {
494            return pages;
495        };
496        let Ok(catalog) = self.resolve(root) else {
497            return pages;
498        };
499        let Some(tree_root) = catalog.as_dict().and_then(|d| d.get("Pages")) else {
500            return pages;
501        };
502        let mut visited: FastSet<ObjRef> = FastSet::default();
503        let mut stack: Vec<(Object, Inherited, usize)> =
504            vec![(tree_root.clone(), Inherited::default(), 0)];
505        while let Some((node, mut inherited, depth)) = stack.pop() {
506            if depth > MAX_TREE_DEPTH {
507                continue;
508            }
509            let node_ref = if let Object::Ref(r) = node {
510                Some(r)
511            } else {
512                None
513            };
514            if let Some(r) = node_ref {
515                if !visited.insert(r) {
516                    continue; // cycle: this node was already traversed
517                }
518            }
519            let Ok(resolved) = self.resolve(&node) else {
520                continue;
521            };
522            let Some(dict) = resolved.as_dict() else {
523                continue;
524            };
525            if let Some(res) = self.dict_value(dict, "Resources") {
526                inherited.resources = Some(res);
527            }
528            if let Some(mb) = self.rect_value(dict, "MediaBox") {
529                inherited.media_box = Some(mb);
530            }
531            if let Some(cb) = self.rect_value(dict, "CropBox") {
532                inherited.crop_box = Some(cb);
533            }
534            if let Some(rot) = self.int_value(dict, "Rotate") {
535                inherited.rotate = Some(rot);
536            }
537            let is_page = dict.get_name("Type").is_some_and(|n| n.0 == "Page");
538            let kids = if is_page {
539                None
540            } else {
541                self.array_value(dict, "Kids")
542            };
543            match kids {
544                Some(kids) => {
545                    // Reverse push so pop order matches document order.
546                    for kid in kids.iter().rev() {
547                        stack.push((kid.clone(), inherited.clone(), depth + 1));
548                    }
549                }
550                None => pages.push(make_page_rec(node_ref, dict.clone(), &inherited)),
551            }
552        }
553        pages
554    }
555
556    /// Resolves `dict[key]` to a dictionary, if present and well-formed.
557    fn dict_value(&self, dict: &Dict, key: &str) -> Option<Dict> {
558        self.resolve(dict.get(key)?).ok()?.as_dict().cloned()
559    }
560
561    /// Resolves `dict[key]` to an array, if present and well-formed.
562    fn array_value(&self, dict: &Dict, key: &str) -> Option<Vec<Object>> {
563        match self.resolve(dict.get(key)?).ok()? {
564            Object::Array(items) => Some(items),
565            _ => None,
566        }
567    }
568
569    /// Resolves `dict[key]` to an integer (reals truncate, lenient).
570    fn int_value(&self, dict: &Dict, key: &str) -> Option<i32> {
571        let v = self.resolve(dict.get(key)?).ok()?.as_f64()?;
572        if v.is_finite() {
573            Some(v as i32)
574        } else {
575            None
576        }
577    }
578
579    /// Resolves `dict[key]` to a normalized rectangle: a four-number array
580    /// whose elements may themselves be references.
581    fn rect_value(&self, dict: &Dict, key: &str) -> Option<Rect> {
582        let items = self.array_value(dict, key)?;
583        if items.len() != 4 {
584            return None;
585        }
586        let mut coords = [0.0f32; 4];
587        for (slot, item) in coords.iter_mut().zip(&items) {
588            let n = self.resolve(item).ok()?.as_f64()?;
589            if !n.is_finite() {
590                return None;
591            }
592            *slot = n as f32;
593        }
594        Some(Rect::new(coords[0], coords[1], coords[2], coords[3]).normalize())
595    }
596}
597
598/// Applies `work` to every page of `doc` in parallel and returns the
599/// results in page order.
600///
601/// Per-page work over a `Document` — text extraction, rasterization — is
602/// independent and CPU-bound, so this fans it out over
603/// `std::thread::available_parallelism()` threads, each holding its own
604/// [`Document::fork`]: the immutable core is shared, the caches are private,
605/// and the workers contend on nothing.
606///
607/// Workers pull page indexes from a shared counter rather than a static
608/// stride. This is not a style choice: pages vary wildly in cost, and on a
609/// machine with heterogeneous cores a static partition ends with the fast
610/// cores idle while a slow one finishes its stripe — measured as the
611/// difference between 3.9x and 5.9x on twelve cores. A counter keeps every
612/// worker busy until the pages run out.
613///
614/// A single-page document, or a single-core machine, runs inline on the
615/// calling thread with no fork and no thread.
616pub fn map_pages<T, F>(doc: &Document, work: F) -> Vec<Result<T>>
617where
618    T: Send + Sync,
619    F: Fn(&Document, &Page) -> Result<T> + Send + Sync,
620{
621    let count = doc.pages().len();
622    let workers = std::thread::available_parallelism()
623        .map(std::num::NonZeroUsize::get)
624        .unwrap_or(1)
625        .min(count);
626    if workers <= 1 {
627        return (0..count)
628            .map(|i| doc.page(i).and_then(|page| work(doc, &page)))
629            .collect();
630    }
631
632    let seed = doc.seed();
633    let next = std::sync::atomic::AtomicUsize::new(0);
634    let slots: Vec<std::sync::OnceLock<Result<T>>> =
635        (0..count).map(|_| std::sync::OnceLock::new()).collect();
636    std::thread::scope(|scope| {
637        for _ in 0..workers {
638            // The seed crosses the thread boundary; the document — whose
639            // caches are single-threaded by design — materializes inside.
640            let seed = seed.clone();
641            let next = &next;
642            let slots = &slots;
643            let work = &work;
644            scope.spawn(move || {
645                let worker = Document::from_seed(seed);
646                loop {
647                    let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
648                    if i >= count {
649                        break;
650                    }
651                    let outcome = worker.page(i).and_then(|page| work(&worker, &page));
652                    // Each index is handed to exactly one worker, so the
653                    // slot is always empty; a failed set would mean the
654                    // counter duplicated an index, which is worth crashing
655                    // over.
656                    if slots[i].set(outcome).is_err() {
657                        unreachable!("page index {i} was dispatched twice");
658                    }
659                }
660            });
661        }
662    });
663    slots
664        .into_iter()
665        .map(|slot| {
666            slot.into_inner()
667                .expect("every page index below the count was dispatched")
668        })
669        .collect()
670}
671
672/// Builds the final page record from a leaf dictionary and its inherited
673/// attributes. The defaults live in [`Page::from_tree_attrs`] — the one
674/// implementation of page defaulting, shared with the asynchronous API —
675/// and this only reshapes its output into the index-less cache record.
676fn make_page_rec(obj_ref: Option<ObjRef>, dict: Dict, inherited: &Inherited) -> PageRec {
677    let page = Page::from_tree_attrs(
678        0,
679        inherited.resources.clone(),
680        inherited.media_box,
681        inherited.crop_box,
682        inherited.rotate,
683        dict,
684        obj_ref,
685    );
686    PageRec {
687        obj_ref: page.obj_ref,
688        media_box: page.media_box,
689        crop_box: page.crop_box,
690        rotate: page.rotate,
691        resources: page.resources,
692        dict: page.dict,
693    }
694}
695
696/// Human-readable object type name for error messages.
697fn type_name(o: &Object) -> &'static str {
698    match o {
699        Object::Null => "null",
700        Object::Bool(_) => "boolean",
701        Object::Int(_) => "integer",
702        Object::Real(_) => "real",
703        Object::String(_) => "string",
704        Object::Name(_) => "name",
705        Object::Array(_) => "array",
706        Object::Dict(_) => "dictionary",
707        Object::Stream(_) => "stream",
708        Object::Ref(_) => "reference",
709    }
710}
711
712impl Resolve for Document {
713    fn resolve_ref(&self, r: ObjRef) -> Option<Object> {
714        self.get(r).ok()
715    }
716}
717
718/// Forwards to the inherent methods, so reading a document through the trait
719/// is bit-identical to reading it directly. `resolve` is forwarded too, even
720/// though the provided implementation is now the same shared chase
721/// ([`crate::source::resolve_sync_with`]) that the inherent method delegates
722/// to: routing it through the inherent method keeps the two entry points a
723/// single call, so they cannot drift should `Document::resolve` ever grow
724/// document-specific behaviour.
725impl crate::source::ObjectSource for Document {
726    fn get(&self, r: ObjRef) -> Result<Object> {
727        Document::get(self, r)
728    }
729
730    fn stream_data(&self, s: &Stream) -> Result<Vec<u8>> {
731        Document::stream_data(self, s)
732    }
733
734    fn resolve(&self, o: &Object) -> Result<Object> {
735        Document::resolve(self, o)
736    }
737}
738
739/// Document information from the trailer `/Info` dictionary. Only present,
740/// well-formed entries are populated.
741#[derive(Debug, Clone, Default, PartialEq, Eq)]
742pub struct Metadata {
743    pub title: Option<String>,
744    pub author: Option<String>,
745    pub subject: Option<String>,
746    pub keywords: Option<String>,
747    pub creator: Option<String>,
748    pub producer: Option<String>,
749    pub creation_date: Option<String>,
750    pub mod_date: Option<String>,
751}
752
753/// A single page with inherited attributes already applied.
754///
755/// Defaults: `media_box` falls back to US Letter (612x792) when absent or
756/// invalid, `crop_box` falls back to (and is intersected with) `media_box`,
757/// and `rotate` is normalized to one of {0, 90, 180, 270}. Every construction
758/// path normalizes `rotate` — [`Document::page`] and [`Page::from_parts`]
759/// alike — so the invariant holds however a `Page` was built. `rotate` is a
760/// public field, so a caller may still overwrite it afterwards; [`Page::size`]
761/// reads it modulo a full turn and so stays correct if they do.
762#[derive(Clone)]
763pub struct Page {
764    /// 0-based page index.
765    pub index: usize,
766    pub media_box: Rect,
767    pub crop_box: Rect,
768    pub rotate: i32,
769    /// The page's (inherited) `/Resources` dictionary.
770    pub resources: Dict,
771    dict: Dict,
772    obj_ref: Option<ObjRef>,
773}
774
775impl Page {
776    /// Builds a page from already-resolved attributes.
777    ///
778    /// [`Document::page`] resolves page-tree inheritance itself; this is for
779    /// a caller that has done that resolution some other way — notably the
780    /// asynchronous API, which flattens the page tree while reading it — and
781    /// needs the same [`Page`] type back.
782    ///
783    /// `rotate` is normalized here to one of `{0, 90, 180, 270}` exactly as
784    /// [`Document::page`] normalizes it, so a caller may pass a raw `/Rotate`
785    /// straight through: a negative or over-a-full-turn multiple of 90 is
786    /// reduced, and a value that is not a multiple of 90 falls back to 0
787    /// (lenient). The caller does not have to pre-normalize, and the [`Page`]
788    /// invariant holds whichever constructor built it.
789    pub fn from_parts(
790        index: usize,
791        media_box: Rect,
792        crop_box: Rect,
793        rotate: i32,
794        resources: Dict,
795        dict: Dict,
796        obj_ref: Option<ObjRef>,
797    ) -> Page {
798        Page {
799            index,
800            media_box,
801            crop_box,
802            rotate: normalize_rotation(rotate),
803            resources,
804            dict,
805            obj_ref,
806        }
807    }
808
809    /// The default media box for a page that declares none: US Letter
810    /// (612 by 792 points). Public because the default is part of the
811    /// observable contract — a caller comparing two APIs' pages needs to
812    /// know which rectangle "the file said nothing" maps to.
813    pub const US_LETTER: Rect = Rect::new(0.0, 0.0, 612.0, 792.0);
814
815    /// Builds a page from raw, possibly missing page-tree attributes,
816    /// applying the same defaults [`Document::page`] applies (ISO 32000
817    /// §7.7.3.3): a missing or degenerate `/MediaBox` reads as
818    /// [`Page::US_LETTER`], `/CropBox` clips to the media box and falls back
819    /// to it, a missing `/Rotate` reads as 0 and is normalized, and missing
820    /// `/Resources` read as empty.
821    ///
822    /// This is the one implementation of page defaulting. The synchronous
823    /// tree walk routes through it, and a caller that resolved inheritance
824    /// some other way — notably the asynchronous API, which flattens the
825    /// page tree while reading it — gets the identical `Page` back, so the
826    /// two APIs cannot disagree about what an attribute-less page looks
827    /// like.
828    pub fn from_tree_attrs(
829        index: usize,
830        resources: Option<Dict>,
831        media_box: Option<Rect>,
832        crop_box: Option<Rect>,
833        rotate: Option<i32>,
834        dict: Dict,
835        obj_ref: Option<ObjRef>,
836    ) -> Page {
837        let media_box = media_box
838            .filter(|r| r.width() > 0.0 && r.height() > 0.0)
839            .unwrap_or(Page::US_LETTER);
840        let crop_box = crop_box
841            .and_then(|c| c.intersect(media_box))
842            .filter(|r| r.width() > 0.0 && r.height() > 0.0)
843            .unwrap_or(media_box);
844        Page {
845            index,
846            media_box,
847            crop_box,
848            rotate: normalize_rotation(rotate.unwrap_or(0)),
849            resources: resources.unwrap_or_default(),
850            dict,
851            obj_ref,
852        }
853    }
854
855    /// The page's indirect object reference, when the page came from an
856    /// indirect kid in the page tree (pages inlined directly into a `/Kids`
857    /// array have none).
858    pub fn object_ref(&self) -> Option<ObjRef> {
859        self.obj_ref
860    }
861
862    /// The page's decoded content: the `/Contents` stream, or all streams
863    /// of a `/Contents` array decoded and joined with `b"\n"`. A missing
864    /// `/Contents` yields empty content (lenient).
865    ///
866    /// This drives [`page_content_with`] to completion on the calling thread.
867    /// There is one implementation of the algorithm, so this and the
868    /// asynchronous API cannot drift apart in what they consider a page's
869    /// content to be.
870    pub fn content(&self, doc: &Document) -> Result<Vec<u8>> {
871        block_on(page_content_with(Immediate(doc), self))
872    }
873
874    /// Crop-box width and height, swapped when `/Rotate` is a quarter turn.
875    ///
876    /// Both constructors normalize `rotate`, but the field is public and so
877    /// can be overwritten with a raw `/Rotate`; the rotation is therefore read
878    /// modulo a full turn rather than compared against 90 and 270 exactly, so
879    /// that -90 and 450 swap the dimensions just as 270 and 90 do. For an
880    /// already-normalized value this is the same test as before: 0, 90, 180
881    /// and 270 are unchanged by `rem_euclid(360)`.
882    pub fn size(&self) -> (f32, f32) {
883        let (w, h) = (self.crop_box.width(), self.crop_box.height());
884        let turn = self.rotate.rem_euclid(360);
885        if turn == 90 || turn == 270 {
886            (h, w)
887        } else {
888            (w, h)
889        }
890    }
891
892    /// The raw page dictionary.
893    pub fn dict(&self) -> &Dict {
894        &self.dict
895    }
896}
897
898/// A page's decoded content, awaiting each fetch: the `/Contents` stream, or
899/// all streams of a `/Contents` array decoded and joined with `b"\n"`. A
900/// missing `/Contents` yields empty content (lenient).
901///
902/// This is the only implementation of that algorithm. [`Page::content`] is a
903/// [`block_on`] wrapper over it, which is what stops the synchronous and
904/// asynchronous APIs from disagreeing about what a page's content is.
905///
906/// # Choosing this signature
907///
908/// `src` is taken **by value** rather than by reference. A future holding
909/// `&'a S` is `Send` but never `'static`, and the asynchronous consumers —
910/// spawning onto a runtime, or crossing into the Python bindings — need both.
911/// By value costs nothing in practice: an asynchronous document is an `Arc`
912/// handle, and [`Immediate`] over a borrowed document is `Copy`.
913///
914/// There is deliberately **no `Send` or `Sync` bound** on `S`. Auto traits are
915/// inferred per instantiation, so this one function yields a `Send` future over
916/// a genuinely asynchronous source and a non-`Send` future over
917/// `Immediate<&Document>` — which is what is wanted, because [`block_on`]
918/// drives the latter on the calling thread and never sends it anywhere. A
919/// `S: Sync` bound would exclude `Immediate<&Document>` outright; that is also
920/// why this calls `src.resolve` directly rather than
921/// [`crate::source::resolve_with`], which does require `Sync`.
922///
923/// The returned future is `'static` only when the caller owns the [`Page`]
924/// inside its own `async` block — the `&Page` borrow is what otherwise
925/// prevents it.
926///
927/// # Errors
928///
929/// Propagates whatever `src` reports for a fetch or a stream decode.
930pub async fn page_content_with<S: AsyncObjectSource>(src: S, page: &Page) -> Result<Vec<u8>> {
931    let Some(contents) = page.dict.get("Contents") else {
932        return Ok(Vec::new());
933    };
934    match src.resolve(contents).await? {
935        Object::Stream(ref s) => content_stream_data_with(&src, s).await,
936        Object::Array(items) => {
937            let mut out = Vec::new();
938            let mut first = true;
939            for item in &items {
940                let part = src.resolve(item).await?;
941                let Some(stream) = part.as_stream() else {
942                    continue; // non-stream entries are skipped (lenient)
943                };
944                if !first {
945                    out.push(b'\n');
946                }
947                out.extend_from_slice(&content_stream_data_with(&src, stream).await?);
948                first = false;
949            }
950            Ok(out)
951        }
952        _ => Ok(Vec::new()),
953    }
954}
955
956/// Fetches one stream's bytes GUARANTEED DECODED — refusing the streams
957/// whose bytes `decode_stream` leaves encoded for the image layer. This is
958/// the fetch for **every consumer that is not an image decoder**: content
959/// streams, font programs, `/ToUnicode` CMaps, `/CIDToGIDMap` tables —
960/// anything that parses what it fetches.
961///
962/// This is [`AsyncObjectSource::stream_data`] with two refusals in front.
963/// A stream whose trailing `/Filter` entry is an image codec (`DCTDecode`,
964/// `JPXDecode`) fails with [`Error::UnsupportedFilter`] instead of handing
965/// back the passthrough (ISO 32000-1 7.4.9): a raw JPEG or JPEG 2000
966/// codestream is indistinguishable from decoded data to anything that is
967/// not an image decoder, and a parser fed one chews binary garbage into
968/// operators, tables, or mappings with a clean result. And a `/Filter`
969/// that cannot be READ at all — a reference cycle, or a chain deeper than
970/// [`crate::source::MAX_RESOLVE_DEPTH`] — is refused with the resolve
971/// error, because a value that cannot be read might name an image codec,
972/// and `decode_stream` would leniently return the bytes as stored. Either
973/// refusal is the same reportable error as any other filter this library
974/// cannot run.
975///
976/// One leniency is kept, deliberately: a `/Filter` that READS as `null`
977/// (a dangling reference included — ISO 32000-1 7.3.10 makes one
978/// equivalent to `null`) or as an unusable value says "no filter", which
979/// is exactly what `decode_stream` does with it; the stored bytes are the
980/// decoded bytes by that reading, and no codec name can hide in a value
981/// that is fully visible.
982///
983/// Raw [`AsyncObjectSource::stream_data`] remains correct in exactly one
984/// place: the image layer, which reads the trailing filter itself and
985/// decodes the passthrough. A new call site that is not decoding images
986/// belongs here instead. Same calling convention as [`page_content_with`]
987/// (`src` by value; see that function's docs).
988pub async fn decoded_stream_data_with<S: AsyncObjectSource>(src: S, s: &Stream) -> Result<Vec<u8>> {
989    if let Some(name) = filters::trailing_filter_checked_with(&src, &s.dict).await? {
990        if filters::is_image_codec(&name.0) {
991            return Err(Error::UnsupportedFilter(name.0));
992        }
993    }
994    src.stream_data(s).await
995}
996
997/// Fetches and decodes one CONTENT stream — page `/Contents`, a form
998/// XObject, a Type3 CharProc, a pattern cell: anything whose bytes feed a
999/// content parser rather than an image decoder.
1000///
1001/// The content-flavored name for [`decoded_stream_data_with`], which is
1002/// the same refusal for every non-image consumer; see it for why a
1003/// passthrough image codec must never reach a parser. For a content
1004/// stream the refusal is what turns the mislabelled stream into the same
1005/// reported skip as any other filter this library cannot run.
1006pub async fn content_stream_data_with<S: AsyncObjectSource>(src: S, s: &Stream) -> Result<Vec<u8>> {
1007    decoded_stream_data_with(src, s).await
1008}
1009
1010#[cfg(test)]
1011mod tests {
1012    use super::*;
1013    use crate::object::Name;
1014    use crate::parser::{NoResolve, Parser};
1015    use crate::xref::XrefEntry;
1016    use pdfboss_testkit::{multi_page_doc, objstm_doc, objstm_payload, simple_doc, PdfBuilder};
1017
1018    /// The synchronous accessor delegates to the asynchronous one, so the two
1019    /// cannot report different content for the same page. This asserts the
1020    /// equality directly rather than trusting the delegation to stay in place.
1021    #[test]
1022    fn async_page_content_matches_the_sync_accessor() {
1023        let doc = Document::load(multi_page_doc(&["one", "two", "three"])).expect("load");
1024        let mut saw_content = false;
1025        for index in 0..3 {
1026            let page = doc.page(index).expect("page");
1027            let direct = page.content(&doc).expect("sync content");
1028            let awaited =
1029                block_on(page_content_with(Immediate(&doc), &page)).expect("async content");
1030            assert_eq!(direct, awaited, "page {index}");
1031            saw_content |= !direct.is_empty();
1032        }
1033        // Without this the loop above would pass on three empty vectors.
1034        assert!(
1035            saw_content,
1036            "fixture must give at least one page real content"
1037        );
1038    }
1039
1040    /// A page `/Contents` stream whose trailing filter is an image codec is
1041    /// refused, never parsed: `decode_stream` would pass its bytes through
1042    /// STILL ENCODED (ISO 32000-1 7.4.9 reserves that for the image layer),
1043    /// and the stream below is deliberately valid operator syntax to prove
1044    /// the refusal happens on the label, not on the bytes. The error is the
1045    /// same `UnsupportedFilter` any genuinely undecodable filter raises, so
1046    /// every caller reports it identically.
1047    #[test]
1048    fn image_codec_page_contents_are_refused_not_returned() {
1049        for codec in ["JPXDecode", "DCTDecode"] {
1050            let mut b = PdfBuilder::new();
1051            b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1052            b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1053            b.object(
1054                3,
1055                "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents 4 0 R >>",
1056            );
1057            b.stream(
1058                4,
1059                &format!("/Filter /{codec}"),
1060                b"1 0 0 rg 0 0 100 100 re f",
1061            );
1062            let doc = Document::load(b.build(1)).expect("load");
1063            let page = doc.page(0).expect("page");
1064            match page.content(&doc) {
1065                Err(Error::UnsupportedFilter(n)) => assert_eq!(n, codec),
1066                other => panic!("{codec} content must be refused, got {other:?}"),
1067            }
1068        }
1069    }
1070
1071    /// A `/Contents` whose `/Filter` is a reference CYCLE is refused as
1072    /// unreadable, never fetched: `decode_stream` cannot resolve the value
1073    /// either and would leniently return the bytes AS STORED — and a value
1074    /// nobody can read might name an image codec, so the stored bytes may
1075    /// be a passthrough codestream. The bytes below are deliberately valid
1076    /// operator syntax to prove the refusal is about the unreadable label.
1077    #[test]
1078    fn an_unreadable_filter_refuses_the_content_not_returns_it_raw() {
1079        let mut b = PdfBuilder::new();
1080        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1081        b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1082        b.object(
1083            3,
1084            "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents 4 0 R >>",
1085        );
1086        b.stream(4, "/Filter 6 0 R", b"1 0 0 rg 0 0 100 100 re f");
1087        b.object(6, "7 0 R");
1088        b.object(7, "6 0 R");
1089        let doc = Document::load(b.build(1)).expect("load");
1090        let page = doc.page(0).expect("page");
1091        assert!(
1092            page.content(&doc).is_err(),
1093            "an unreadable /Filter must refuse, not pass stored bytes"
1094        );
1095    }
1096
1097    /// The composition every page-reading algorithm actually uses: the caller
1098    /// owns its source so that its own future can be `'static`, and reaches this
1099    /// helper — which owns its source for the same reason — by handing out a
1100    /// reference. That works because `&S` is itself an `AsyncObjectSource`.
1101    #[test]
1102    fn a_shared_helper_is_reachable_from_a_caller_that_owns_its_source() {
1103        let doc = Document::load(simple_doc("Hello")).expect("load");
1104        let page = doc.page(0).expect("page");
1105
1106        let owner = Immediate(&doc);
1107        let through_reference = block_on(page_content_with(&owner, &page)).expect("async content");
1108
1109        assert_eq!(through_reference, page.content(&doc).expect("sync content"));
1110        assert!(!through_reference.is_empty());
1111    }
1112
1113    /// The seed is what crosses a thread boundary, so this is the compile
1114    /// gate for the whole fan-out design. The `Document` itself must stay
1115    /// out of this list: its caches are single-threaded by design.
1116    #[test]
1117    fn the_seed_is_shareable_across_threads() {
1118        fn assert_send_sync<T: Send + Sync>() {}
1119        assert_send_sync::<DocumentSeed>();
1120    }
1121
1122    /// A fork shares the file and the page tree but none of the caches, and
1123    /// reads identically to its parent — including through decryption, whose
1124    /// key material is part of the shared immutable core.
1125    #[test]
1126    fn a_fork_reads_exactly_what_its_parent_reads() {
1127        let doc = Document::load(pdfboss_testkit::encrypted_rc4_doc("forked secret")).unwrap();
1128        let fork = doc.fork();
1129        assert_eq!(fork.page_count(), doc.page_count());
1130        let (a, b) = (doc.page(0).unwrap(), fork.page(0).unwrap());
1131        assert_eq!(a.media_box, b.media_box);
1132        assert_eq!(
1133            a.content(&doc).expect("parent content"),
1134            b.content(&fork).expect("fork content"),
1135            "decrypted content agrees"
1136        );
1137    }
1138
1139    /// The results come back in page order whatever order the workers finish
1140    /// in, and a per-page error occupies its own slot without disturbing the
1141    /// others.
1142    #[test]
1143    fn map_pages_preserves_page_order() {
1144        let doc = Document::load(multi_page_doc(&["one", "two", "three"])).unwrap();
1145        let contents = map_pages(&doc, |doc, page| page.content(doc));
1146        assert_eq!(contents.len(), 3);
1147        let texts: Vec<String> = contents
1148            .into_iter()
1149            .map(|c| String::from_utf8_lossy(&c.unwrap()).into_owned())
1150            .collect();
1151        assert!(texts[0].contains("(one)"), "{}", texts[0]);
1152        assert!(texts[1].contains("(two)"), "{}", texts[1]);
1153        assert!(texts[2].contains("(three)"), "{}", texts[2]);
1154    }
1155
1156    /// Replaces the first occurrence of `from` with `to`. Splicing happens
1157    /// after the xref section, so byte offsets stay valid.
1158    fn replace_once(data: &[u8], from: &[u8], to: &[u8]) -> Vec<u8> {
1159        let pos = memchr::memmem::find(data, from).expect("pattern present in fixture");
1160        let mut out = Vec::with_capacity(data.len() - from.len() + to.len());
1161        out.extend_from_slice(&data[..pos]);
1162        out.extend_from_slice(to);
1163        out.extend_from_slice(&data[pos + from.len()..]);
1164        out
1165    }
1166
1167    fn contains(haystack: &[u8], needle: &[u8]) -> bool {
1168        memchr::memmem::find(haystack, needle).is_some()
1169    }
1170
1171    const FONT: &str = "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>";
1172
1173    #[test]
1174    fn loads_simple_doc() {
1175        let doc = Document::load(simple_doc("Greetings, cosmos!")).unwrap();
1176        assert_eq!(doc.version(), (1, 7));
1177        assert_eq!(doc.page_count(), 1);
1178        let page = doc.page(0).unwrap();
1179        assert_eq!(page.index, 0);
1180        assert_eq!(page.media_box, Rect::new(0.0, 0.0, 612.0, 792.0));
1181        assert_eq!(page.crop_box, page.media_box);
1182        assert_eq!(page.rotate, 0);
1183        assert_eq!(page.size(), (612.0, 792.0));
1184        assert!(page.resources.get("Font").is_some());
1185        let content = page.content(&doc).unwrap();
1186        assert!(contains(&content, b"Greetings, cosmos!"));
1187    }
1188
1189    #[test]
1190    fn multi_page_ordering() {
1191        let doc = Document::load(multi_page_doc(&["alpha", "beta", "gamma"])).unwrap();
1192        assert_eq!(doc.page_count(), 3);
1193        for (i, text) in ["alpha", "beta", "gamma"].iter().enumerate() {
1194            let content = doc.page(i).unwrap().content(&doc).unwrap();
1195            assert!(
1196                contains(&content, text.as_bytes()),
1197                "page {i} should show {text}"
1198            );
1199        }
1200    }
1201
1202    #[test]
1203    fn page_index_out_of_bounds() {
1204        let doc = Document::load(simple_doc("x")).unwrap();
1205        assert!(matches!(doc.page(5), Err(Error::PageNotFound(5, 1))));
1206    }
1207
1208    #[test]
1209    fn open_reads_from_disk() {
1210        let dir = std::env::temp_dir();
1211        let path = dir.join(format!("pdfboss-doc-test-{}.pdf", std::process::id()));
1212        std::fs::write(&path, simple_doc("from disk")).unwrap();
1213        let doc = Document::open(&path).unwrap();
1214        std::fs::remove_file(&path).ok();
1215        assert_eq!(doc.page_count(), 1);
1216        let content = doc.page(0).unwrap().content(&doc).unwrap();
1217        assert!(contains(&content, b"from disk"));
1218        assert!(matches!(
1219            Document::open(dir.join("pdfboss-doc-test-missing.pdf")),
1220            Err(Error::Io(_))
1221        ));
1222    }
1223
1224    #[test]
1225    fn encrypt_in_trailer_is_rejected() {
1226        let data = replace_once(
1227            &simple_doc("secret"),
1228            b"trailer\n<< /Size",
1229            b"trailer\n<< /Encrypt 9 0 R /Size",
1230        );
1231        assert!(matches!(Document::load(data), Err(Error::Encrypted)));
1232    }
1233
1234    #[test]
1235    fn metadata_utf16be_round_trip() {
1236        let mut b = PdfBuilder::new();
1237        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1238        b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
1239        // /Title is UTF-16BE with BOM: "H\u{151}" (H + o with double acute).
1240        b.object(6, "<< /Title <FEFF00480151> /Author (plain author) >>");
1241        let data = replace_once(&b.build(1), b"<< /Size", b"<< /Info 6 0 R /Size");
1242        let doc = Document::load(data).unwrap();
1243        let meta = doc.metadata();
1244        assert_eq!(meta.title.as_deref(), Some("H\u{151}"));
1245        assert_eq!(meta.author.as_deref(), Some("plain author"));
1246        assert_eq!(meta.subject, None);
1247        assert_eq!(meta.keywords, None);
1248        assert_eq!(meta.creation_date, None);
1249    }
1250
1251    #[test]
1252    fn metadata_without_info_is_all_none() {
1253        let doc = Document::load(simple_doc("x")).unwrap();
1254        assert_eq!(doc.metadata(), Metadata::default());
1255    }
1256
1257    #[test]
1258    fn missing_object_resolves_to_null() {
1259        let doc = Document::load(simple_doc("x")).unwrap();
1260        let missing = Object::Ref(ObjRef { num: 99, gen: 0 });
1261        assert_eq!(doc.resolve(&missing).unwrap(), Object::Null);
1262        assert!(matches!(
1263            doc.get(ObjRef { num: 99, gen: 0 }),
1264            Err(Error::ObjectNotFound(99, 0))
1265        ));
1266    }
1267
1268    #[test]
1269    fn self_reference_is_circular() {
1270        let mut b = PdfBuilder::new();
1271        b.object(1, "<< /Type /Catalog >>");
1272        b.object(6, "6 0 R");
1273        let doc = Document::load(b.build(1)).unwrap();
1274        let loops = Object::Ref(ObjRef { num: 6, gen: 0 });
1275        assert!(matches!(
1276            doc.resolve(&loops),
1277            Err(Error::CircularReference(6))
1278        ));
1279    }
1280
1281    #[test]
1282    fn generation_mismatch_is_tolerated() {
1283        let doc = Document::load(simple_doc("x")).unwrap();
1284        let catalog = doc.get(ObjRef { num: 1, gen: 7 }).unwrap();
1285        let dict = catalog.as_dict().unwrap();
1286        assert_eq!(dict.get_name("Type").map(|n| n.0.as_str()), Some("Catalog"));
1287    }
1288
1289    #[test]
1290    fn objects_in_object_streams_are_fetched() {
1291        let mut b = PdfBuilder::new();
1292        let (dict, payload) =
1293            objstm_payload(&[(1, "<< /Type /Catalog /Pages 2 0 R >>"), (5, FONT)]);
1294        b.stream(6, &dict, &payload);
1295        b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1296        b.object(
1297            3,
1298            "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1299             /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
1300        );
1301        b.stream(4, "", b"BT /F1 12 Tf (compressed hello) Tj ET");
1302        let doc = Document::load(b.build_xref_stream(1)).unwrap();
1303        assert_eq!(doc.page_count(), 1);
1304        let page = doc.page(0).unwrap();
1305        assert!(contains(&page.content(&doc).unwrap(), b"compressed hello"));
1306        let font = doc.get(ObjRef { num: 5, gen: 0 }).unwrap();
1307        assert_eq!(
1308            font.as_dict()
1309                .and_then(|d| d.get_name("BaseFont"))
1310                .map(|n| n.0.as_str()),
1311            Some("Helvetica")
1312        );
1313    }
1314
1315    #[test]
1316    fn contents_array_is_joined_with_newlines() {
1317        let mut b = PdfBuilder::new();
1318        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1319        b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1320        b.object(
1321            3,
1322            "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1323             /Contents [4 0 R null 5 0 R] >>",
1324        );
1325        b.stream(4, "", b"q");
1326        b.stream(5, "", b"Q");
1327        let doc = Document::load(b.build(1)).unwrap();
1328        let content = doc.page(0).unwrap().content(&doc).unwrap();
1329        assert_eq!(content, b"q\nQ", "streams joined by \\n, null skipped");
1330    }
1331
1332    #[test]
1333    fn inheritance_from_pages_node_and_rotate_swap() {
1334        let mut b = PdfBuilder::new();
1335        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1336        b.object(
1337            2,
1338            "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 \
1339             /Resources << /Font << /F1 5 0 R >> >> /MediaBox [0 0 400 600] >>",
1340        );
1341        b.object(3, "<< /Type /Page /Parent 2 0 R >>");
1342        b.object(4, "<< /Type /Page /Parent 2 0 R /Rotate 270 >>");
1343        b.object(5, FONT);
1344        let doc = Document::load(b.build(1)).unwrap();
1345        assert_eq!(doc.page_count(), 2);
1346
1347        let first = doc.page(0).unwrap();
1348        assert_eq!(first.media_box, Rect::new(0.0, 0.0, 400.0, 600.0));
1349        assert_eq!(first.crop_box, first.media_box);
1350        assert!(first.resources.get("Font").is_some(), "inherited resources");
1351        assert_eq!(first.rotate, 0);
1352        assert!(
1353            first.content(&doc).unwrap().is_empty(),
1354            "no /Contents means empty content"
1355        );
1356        assert_eq!(first.size(), (400.0, 600.0));
1357
1358        let second = doc.page(1).unwrap();
1359        assert_eq!(second.rotate, 270);
1360        assert_eq!(second.size(), (600.0, 400.0), "rotate 270 swaps w/h");
1361    }
1362
1363    #[test]
1364    fn crop_box_intersected_and_rotate_normalized() {
1365        let mut b = PdfBuilder::new();
1366        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1367        b.object(2, "<< /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 >>");
1368        b.object(
1369            3,
1370            "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] \
1371             /CropBox [100 100 400 400] /Rotate 450 >>",
1372        );
1373        b.object(4, "<< /Type /Page /Parent 2 0 R /Rotate -90 >>");
1374        b.object(
1375            5,
1376            "<< /Type /Page /Parent 2 0 R /Rotate 45 /MediaBox [0 0 0 0] >>",
1377        );
1378        let doc = Document::load(b.build(1)).unwrap();
1379
1380        let clipped = doc.page(0).unwrap();
1381        assert_eq!(clipped.crop_box, Rect::new(100.0, 100.0, 200.0, 200.0));
1382        assert_eq!(clipped.rotate, 90, "450 normalizes to 90");
1383        assert_eq!(clipped.size(), (100.0, 100.0));
1384
1385        assert_eq!(doc.page(1).unwrap().rotate, 270, "-90 normalizes to 270");
1386        let odd = doc.page(2).unwrap();
1387        assert_eq!(odd.rotate, 0, "non-multiple of 90 falls back to 0");
1388        assert_eq!(
1389            odd.media_box,
1390            Page::US_LETTER,
1391            "degenerate media box defaults"
1392        );
1393    }
1394
1395    #[test]
1396    fn kids_cycle_truncates_without_hanging() {
1397        let mut b = PdfBuilder::new();
1398        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1399        // 2 → 3 → {4, back to 2}: the back-edge must be ignored.
1400        b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1401        b.object(3, "<< /Type /Pages /Kids [4 0 R 2 0 R] /Count 1 >>");
1402        b.object(
1403            4,
1404            "<< /Type /Page /Parent 3 0 R /MediaBox [0 0 100 100] /Contents 5 0 R >>",
1405        );
1406        b.stream(5, "", b"0 0 50 50 re f");
1407        let doc = Document::load(b.build(1)).unwrap();
1408        assert_eq!(doc.page_count(), 1, "cycle back-edge yields no extra pages");
1409        assert!(contains(
1410            &doc.page(0).unwrap().content(&doc).unwrap(),
1411            b"re f"
1412        ));
1413    }
1414
1415    #[test]
1416    fn tree_depth_is_capped() {
1417        let mut b = PdfBuilder::new();
1418        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1419        // A unary chain of 300 intermediate nodes, page leaf at the bottom.
1420        let last = 302u32;
1421        for num in 2..last {
1422            b.object(
1423                num,
1424                &format!("<< /Type /Pages /Kids [{} 0 R] /Count 1 >>", num + 1),
1425            );
1426        }
1427        b.object(last, "<< /Type /Page >>");
1428        let doc = Document::load(b.build(1)).unwrap();
1429        // `page_count` reports the tree's declared `/Count` (1) cheaply, as
1430        // mature engines do; the leaf itself lies beyond the traversal depth
1431        // cap, so the flattened tree is empty and the page cannot be
1432        // materialized.
1433        assert_eq!(doc.page_count(), 1, "declared /Count is reported cheaply");
1434        assert!(
1435            matches!(doc.page(0), Err(Error::PageNotFound(0, 0))),
1436            "leaf beyond the depth cap cannot be materialized"
1437        );
1438    }
1439
1440    #[test]
1441    fn page_count_reports_declared_count_cheaply() {
1442        // The tree declares five pages but supplies only one kid. `page_count`
1443        // reports the declared `/Count` (as mature engines do) without walking,
1444        // while page access is bounded by the pages that actually materialize.
1445        let mut b = PdfBuilder::new();
1446        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1447        b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 5 >>");
1448        b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
1449        let doc = Document::load(b.build(1)).unwrap();
1450        assert_eq!(doc.page_count(), 5, "declared /Count reported verbatim");
1451        assert!(doc.page(0).is_ok(), "the one real page materializes");
1452        assert!(
1453            matches!(doc.page(1), Err(Error::PageNotFound(1, 1))),
1454            "access past the real pages fails with the true length"
1455        );
1456    }
1457
1458    #[test]
1459    fn page_count_falls_back_to_walk_when_count_absent() {
1460        let mut b = PdfBuilder::new();
1461        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1462        b.object(2, "<< /Type /Pages /Kids [3 0 R] >>"); // no /Count
1463        b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
1464        let doc = Document::load(b.build(1)).unwrap();
1465        assert_eq!(
1466            doc.page_count(),
1467            1,
1468            "missing /Count is recovered by walking"
1469        );
1470    }
1471
1472    #[test]
1473    fn page_count_ignores_corrupt_oversized_count() {
1474        // A `/Count` larger than the whole file is impossible: fall back to a
1475        // real walk rather than trust it.
1476        let mut b = PdfBuilder::new();
1477        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1478        b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 999999999 >>");
1479        b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
1480        let doc = Document::load(b.build(1)).unwrap();
1481        assert_eq!(doc.page_count(), 1, "implausible /Count is rejected");
1482    }
1483
1484    #[test]
1485    fn version_scan_and_default() {
1486        let mut b = PdfBuilder::new().version(2, 0);
1487        b.object(1, "<< /Type /Catalog >>");
1488        assert_eq!(Document::load(b.build(1)).unwrap().version(), (2, 0));
1489        // Corrupting the header magic (same length) falls back to 1.4.
1490        let data = replace_once(&simple_doc("v"), b"%PDF-", b"%QQQ-");
1491        assert_eq!(Document::load(data).unwrap().version(), (1, 4));
1492    }
1493
1494    #[test]
1495    fn deeply_nested_root_object_does_not_overflow_the_stack() {
1496        // A ~100 KB file whose Root is a 50k-deep array used to drive the
1497        // object parser's recursion into a fatal stack overflow during
1498        // `Document::load`. Run on a small stack so a regression aborts
1499        // loudly rather than depending on the main thread's stack size.
1500        let mut data = b"%PDF-1.7\n1 0 obj\n".to_vec();
1501        data.extend(std::iter::repeat_n(b'[', 50_000));
1502        data.extend(std::iter::repeat_n(b']', 50_000));
1503        data.extend_from_slice(b"\nendobj\ntrailer\n<</Root 1 0 R>>\n%%EOF\n");
1504        let outcome = std::thread::Builder::new()
1505            .stack_size(1024 * 1024)
1506            .spawn(move || Document::load(data).map(|doc| doc.page_count()))
1507            .expect("spawn test thread")
1508            .join()
1509            .expect("Document::load must not overflow the stack");
1510        // The over-nested Root is rejected or ignored (lenient), but the
1511        // process survives and no page is fabricated from it.
1512        assert!(matches!(outcome, Ok(0) | Err(_)));
1513    }
1514
1515    #[test]
1516    fn bytes_and_xref_accessors() {
1517        let data = simple_doc("accessors");
1518        let doc = Document::load(data.clone()).unwrap();
1519        assert_eq!(doc.bytes(), &data[..]);
1520        assert!(!doc.xref().is_empty());
1521        assert!(doc.xref().trailer.get("Root").is_some());
1522    }
1523
1524    #[test]
1525    fn object_at_spanned_reparses_identically() {
1526        let data = simple_doc("spanned");
1527        let doc = Document::load(data).unwrap();
1528        for (num, entry) in doc.xref().iter() {
1529            let XrefEntry::InFile { offset, gen } = entry else {
1530                continue;
1531            };
1532            let (r, object, span) = doc.object_at_spanned(offset as usize).unwrap();
1533            assert_eq!(r.num, num);
1534            assert_eq!(r.gen, gen);
1535            assert_eq!(span.start, offset);
1536            assert!(span.end as usize <= doc.bytes().len());
1537            // The bytes at the span parse back to the same object.
1538            let slice = &doc.bytes()[span.start as usize..span.end as usize];
1539            let (r2, object2) = Parser::new(slice).parse_indirect(&NoResolve).unwrap();
1540            assert_eq!(r2, r);
1541            assert_eq!(object2, object);
1542        }
1543    }
1544
1545    #[test]
1546    fn page_object_ref_points_at_a_page_dict() {
1547        let doc = Document::load(multi_page_doc(&["one", "two"])).unwrap();
1548        for index in 0..doc.page_count() {
1549            let page = doc.page(index).unwrap();
1550            let r = page.object_ref().expect("builder pages are indirect");
1551            let resolved = doc.get(r).unwrap();
1552            assert_eq!(
1553                resolved
1554                    .as_dict()
1555                    .unwrap()
1556                    .get_name("Type")
1557                    .map(|n| n.0.as_str()),
1558                Some("Page")
1559            );
1560        }
1561    }
1562
1563    // --- Minimal Standard-handler (RC4 V2/R3) fixture builder, duplicating
1564    // the key-derivation mechanism `crypt::tests` uses under the empty user
1565    // password (those helpers are private to that module's tests). Needed
1566    // only to pin the decrypt-identity regression test below: RC4's
1567    // per-object key depends on the object's num/gen, so it is the cipher
1568    // that can actually distinguish "decrypt with the parsed header's
1569    // identity" from "decrypt with the caller's requested identity".
1570
1571    const RC4_FIXTURE_KEY_LEN: usize = 16; // 128-bit key
1572    const RC4_FIXTURE_P: i32 = -44;
1573    const RC4_FIXTURE_ID0: &[u8] = b"0123456789abcdef";
1574    const RC4_FIXTURE_PAD: [u8; 32] = [
1575        0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01,
1576        0x08, 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53,
1577        0x69, 0x7A,
1578    ];
1579
1580    #[rustfmt::skip]
1581    const RC4_FIXTURE_MD5_S: [u32; 64] = [
1582        7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
1583        5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
1584        4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
1585        6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
1586    ];
1587    #[rustfmt::skip]
1588    const RC4_FIXTURE_MD5_K: [u32; 64] = [
1589        0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
1590        0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
1591        0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
1592        0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
1593        0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
1594        0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
1595        0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
1596        0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,
1597    ];
1598
1599    fn rc4_fixture_md5(input: &[u8]) -> [u8; 16] {
1600        let (mut a0, mut b0, mut c0, mut d0) = (
1601            0x6745_2301u32,
1602            0xefcd_ab89u32,
1603            0x98ba_dcfeu32,
1604            0x1032_5476u32,
1605        );
1606        let mut msg = input.to_vec();
1607        let bitlen = (input.len() as u64).wrapping_mul(8);
1608        msg.push(0x80);
1609        while msg.len() % 64 != 56 {
1610            msg.push(0);
1611        }
1612        msg.extend_from_slice(&bitlen.to_le_bytes());
1613        for chunk in msg.as_chunks::<64>().0 {
1614            let mut m = [0u32; 16];
1615            for (word, bytes) in m.iter_mut().zip(chunk.as_chunks::<4>().0) {
1616                *word = u32::from_le_bytes(*bytes);
1617            }
1618            let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0);
1619            for i in 0..64 {
1620                let (f, g) = match i {
1621                    0..=15 => ((b & c) | (!b & d), i),
1622                    16..=31 => ((d & b) | (!d & c), (5 * i + 1) % 16),
1623                    32..=47 => (b ^ c ^ d, (3 * i + 5) % 16),
1624                    _ => (c ^ (b | !d), (7 * i) % 16),
1625                };
1626                let f = f
1627                    .wrapping_add(a)
1628                    .wrapping_add(RC4_FIXTURE_MD5_K[i])
1629                    .wrapping_add(m[g]);
1630                a = d;
1631                d = c;
1632                c = b;
1633                b = b.wrapping_add(f.rotate_left(RC4_FIXTURE_MD5_S[i]));
1634            }
1635            a0 = a0.wrapping_add(a);
1636            b0 = b0.wrapping_add(b);
1637            c0 = c0.wrapping_add(c);
1638            d0 = d0.wrapping_add(d);
1639        }
1640        let mut out = [0u8; 16];
1641        out[0..4].copy_from_slice(&a0.to_le_bytes());
1642        out[4..8].copy_from_slice(&b0.to_le_bytes());
1643        out[8..12].copy_from_slice(&c0.to_le_bytes());
1644        out[12..16].copy_from_slice(&d0.to_le_bytes());
1645        out
1646    }
1647
1648    fn rc4_fixture_rc4(key: &[u8], data: &[u8]) -> Vec<u8> {
1649        let mut s: [u8; 256] = core::array::from_fn(|i| i as u8);
1650        let mut j = 0u8;
1651        for i in 0..256 {
1652            j = j.wrapping_add(s[i]).wrapping_add(key[i % key.len()]);
1653            s.swap(i, j as usize);
1654        }
1655        let mut out = Vec::with_capacity(data.len());
1656        let (mut i, mut j) = (0u8, 0u8);
1657        for &byte in data {
1658            i = i.wrapping_add(1);
1659            j = j.wrapping_add(s[i as usize]);
1660            s.swap(i as usize, j as usize);
1661            let k = s[s[i as usize].wrapping_add(s[j as usize]) as usize];
1662            out.push(byte ^ k);
1663        }
1664        out
1665    }
1666
1667    /// `/O` for empty owner and user passwords (Algorithm 3, R3).
1668    fn rc4_fixture_owner_entry() -> Vec<u8> {
1669        let mut d = rc4_fixture_md5(&RC4_FIXTURE_PAD);
1670        for _ in 0..50 {
1671            d = rc4_fixture_md5(&d[..RC4_FIXTURE_KEY_LEN]);
1672        }
1673        let rc4key = d[..RC4_FIXTURE_KEY_LEN].to_vec();
1674        let mut o = rc4_fixture_rc4(&rc4key, &RC4_FIXTURE_PAD);
1675        for i in 1u8..=19 {
1676            let k: Vec<u8> = rc4key.iter().map(|b| b ^ i).collect();
1677            o = rc4_fixture_rc4(&k, &o);
1678        }
1679        o
1680    }
1681
1682    /// File key from `/O` for the empty user password (Algorithm 2, R3).
1683    fn rc4_fixture_file_key(o: &[u8]) -> Vec<u8> {
1684        let mut input = Vec::new();
1685        input.extend_from_slice(&RC4_FIXTURE_PAD);
1686        input.extend_from_slice(o);
1687        input.extend_from_slice(&(RC4_FIXTURE_P as u32).to_le_bytes());
1688        input.extend_from_slice(RC4_FIXTURE_ID0);
1689        let mut d = rc4_fixture_md5(&input);
1690        for _ in 0..50 {
1691            d = rc4_fixture_md5(&d[..RC4_FIXTURE_KEY_LEN]);
1692        }
1693        d[..RC4_FIXTURE_KEY_LEN].to_vec()
1694    }
1695
1696    /// `/U` for the empty user password (Algorithm 5, R3).
1697    fn rc4_fixture_user_entry(key: &[u8]) -> Vec<u8> {
1698        let mut input = Vec::new();
1699        input.extend_from_slice(&RC4_FIXTURE_PAD);
1700        input.extend_from_slice(RC4_FIXTURE_ID0);
1701        let mut x = rc4_fixture_md5(&input).to_vec();
1702        x = rc4_fixture_rc4(key, &x);
1703        for i in 1u8..=19 {
1704            let k: Vec<u8> = key.iter().map(|b| b ^ i).collect();
1705            x = rc4_fixture_rc4(&k, &x);
1706        }
1707        x.resize(32, 0); // trailing padding is arbitrary
1708        x
1709    }
1710
1711    fn rc4_fixture_obj_key(key: &[u8], num: u32, gen: u16) -> Vec<u8> {
1712        let mut input = key.to_vec();
1713        input.extend_from_slice(&num.to_le_bytes()[..3]);
1714        input.extend_from_slice(&gen.to_le_bytes()[..2]);
1715        rc4_fixture_md5(&input)[..(key.len() + 5).min(16)].to_vec()
1716    }
1717
1718    fn rc4_fixture_hexstr(b: &[u8]) -> String {
1719        let mut s = String::from("<");
1720        for x in b {
1721            s.push_str(&format!("{x:02x}"));
1722        }
1723        s.push('>');
1724        s
1725    }
1726
1727    /// Builds a V2/R3 (128-bit RC4) file, encrypted under the empty
1728    /// password, with a single indirect object (`3 0 obj`, i.e. gen 0)
1729    /// holding an encrypted string.
1730    fn rc4_encrypted_fixture() -> Vec<u8> {
1731        let o = rc4_fixture_owner_entry();
1732        let key = rc4_fixture_file_key(&o);
1733        let u = rc4_fixture_user_entry(&key);
1734        let msg = rc4_fixture_rc4(&rc4_fixture_obj_key(&key, 3, 0), b"Top secret message");
1735
1736        let mut b = PdfBuilder::new().version(1, 4);
1737        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1738        b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
1739        b.object(3, &format!("<< /Msg {} >>", rc4_fixture_hexstr(&msg)));
1740        b.object(
1741            9,
1742            &format!(
1743                "<< /Filter /Standard /V 2 /R 3 /Length 128 /P {} /O {} /U {} >>",
1744                RC4_FIXTURE_P,
1745                rc4_fixture_hexstr(&o),
1746                rc4_fixture_hexstr(&u)
1747            ),
1748        );
1749        let trailer = format!(
1750            "/Encrypt 9 0 R /ID [{}{}]",
1751            rc4_fixture_hexstr(RC4_FIXTURE_ID0),
1752            rc4_fixture_hexstr(RC4_FIXTURE_ID0)
1753        );
1754        b.trailer_extra(&trailer).build(1)
1755    }
1756
1757    #[test]
1758    fn encrypted_generation_mismatch_still_decrypts() {
1759        // `object_at_spanned` derives the per-object RC4/AESV2 decrypt key
1760        // from the PARSED "N G obj" header at the object's file offset
1761        // (`r.num`, `r.gen` from `parser.parse_indirect`), never from the
1762        // caller's requested `ObjRef` — mirroring how plain (unencrypted)
1763        // lookups already tolerate a generation mismatch
1764        // (`generation_mismatch_is_tolerated`). Request object 3 (really
1765        // "3 0 obj" in the file) under a deliberately wrong generation: if
1766        // decryption instead used the requested (wrong) gen to derive the
1767        // RC4 object key, the result would be garbage, not the plaintext.
1768        let doc = Document::load(rc4_encrypted_fixture()).expect("empty password opens the file");
1769        let obj3 = doc.get(ObjRef { num: 3, gen: 7 }).unwrap();
1770        let msg = obj3
1771            .as_dict()
1772            .unwrap()
1773            .get("Msg")
1774            .unwrap()
1775            .as_str_bytes()
1776            .unwrap();
1777        assert_eq!(
1778            msg, b"Top secret message",
1779            "decrypted using the file's real gen (0), not the mismatched request (7)"
1780        );
1781    }
1782
1783    #[test]
1784    fn objstm_doc_fixture_loads_and_resolves_members() {
1785        let data = objstm_doc(&[(7, "<< /Marker (inside) >>")]);
1786        let doc = Document::load(data).unwrap();
1787        assert_eq!(doc.page_count(), 1);
1788        let member = doc.get(ObjRef { num: 7, gen: 0 }).unwrap();
1789        let text = member.as_dict().unwrap().get("Marker").unwrap();
1790        assert_eq!(text.as_str_bytes(), Some(&b"inside"[..]));
1791        // The member really is xref'd into the object stream.
1792        assert!(matches!(
1793            doc.xref().get(7),
1794            Some(XrefEntry::InStream { stream_num: 4, .. })
1795        ));
1796    }
1797
1798    /// A page built from parts must expose the same accessors as one that
1799    /// came out of the page tree — this is the constructor aio uses.
1800    #[test]
1801    fn page_from_parts_exposes_its_accessors() {
1802        let mut dict = Dict::default();
1803        dict.insert(Name("Type".into()), Object::Name(Name("Page".into())));
1804        let media = Rect::new(0.0, 0.0, 200.0, 400.0);
1805        let obj_ref = ObjRef { num: 7, gen: 0 };
1806
1807        let page = Page::from_parts(
1808            3,
1809            media,
1810            media,
1811            90,
1812            Dict::default(),
1813            dict.clone(),
1814            Some(obj_ref),
1815        );
1816
1817        assert_eq!(page.index, 3);
1818        assert_eq!(page.object_ref(), Some(obj_ref));
1819        assert_eq!(page.dict(), &dict);
1820        // /Rotate 90 swaps the reported page size.
1821        assert_eq!(page.size(), (400.0, 200.0));
1822    }
1823
1824    /// `from_parts` normalizes `/Rotate` just as the page tree does, so an
1825    /// unnormalized quarter-turn stores — and reports the size of — its
1826    /// canonical equivalent.
1827    #[test]
1828    fn page_from_parts_normalizes_rotation() {
1829        let media = Rect::new(0.0, 0.0, 200.0, 400.0);
1830        let build = |rotate: i32| {
1831            Page::from_parts(
1832                0,
1833                media,
1834                media,
1835                rotate,
1836                Dict::default(),
1837                Dict::default(),
1838                None,
1839            )
1840        };
1841
1842        for (given, canonical) in [(-90, 270), (450, 90), (540, 180), (720, 0), (-360, 0)] {
1843            let page = build(given);
1844            assert_eq!(
1845                page.rotate, canonical,
1846                "from_parts must normalize /Rotate {given} to {canonical}"
1847            );
1848            assert_eq!(
1849                page.size(),
1850                build(canonical).size(),
1851                "/Rotate {given} must report the same size as {canonical}"
1852            );
1853        }
1854    }
1855
1856    /// `rotate` is a public field, so `size()` cannot rely on the constructor
1857    /// having normalized it: any multiple of 90 must be read modulo a full
1858    /// turn. The already-canonical values are listed too, pinning that this
1859    /// is unchanged for them.
1860    #[test]
1861    fn page_size_handles_unnormalized_rotation() {
1862        let media = Rect::new(0.0, 0.0, 200.0, 400.0);
1863        let mut page = Page::from_parts(0, media, media, 0, Dict::default(), Dict::default(), None);
1864
1865        for rotate in [90, 270, -90, -270, 450, 630] {
1866            page.rotate = rotate;
1867            assert_eq!(
1868                page.size(),
1869                (400.0, 200.0),
1870                "/Rotate {rotate} is a quarter turn and must swap the size"
1871            );
1872        }
1873        for rotate in [0, 180, -180, 360, 540, 720] {
1874            page.rotate = rotate;
1875            assert_eq!(
1876                page.size(),
1877                (200.0, 400.0),
1878                "/Rotate {rotate} is a half turn and must leave the size alone"
1879            );
1880        }
1881    }
1882}