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