1use crate::hash::{FastMap, FastSet};
6use std::cell::{OnceCell, RefCell};
7use std::path::Path;
8use std::rc::Rc;
9
10use crate::crypt::Decryptor;
11use crate::elements::Span;
12use crate::error::{Error, Result};
13use crate::filters;
14use crate::geom::Rect;
15use crate::object::{decode_text_string, Dict, ObjRef, Object, Stream};
16use crate::objstm;
17use crate::parser::{Parser, Resolve};
18use crate::xref::{load_xref, Xref, XrefEntry};
19
20const US_LETTER: Rect = Rect::new(0.0, 0.0, 612.0, 792.0);
22
23const MAX_RESOLVE_DEPTH: usize = 32;
25
26const MAX_TREE_DEPTH: usize = 256;
28
29pub struct Document {
31 data: Vec<u8>,
32 version: (u8, u8),
33 xref: Xref,
34 cache: RefCell<FastMap<(u32, u16), Rc<Object>>>,
36 loading: RefCell<FastSet<u32>>,
39 objstms: RefCell<FastMap<u32, Rc<objstm::ObjStm>>>,
43 decryptor: Option<Decryptor>,
47 pages: OnceCell<Vec<PageRec>>,
51}
52
53struct PageRec {
55 obj_ref: Option<ObjRef>,
56 media_box: Rect,
57 crop_box: Rect,
58 rotate: i32,
59 resources: Dict,
60 dict: Dict,
61}
62
63#[derive(Clone, Default)]
65struct Inherited {
66 resources: Option<Dict>,
67 media_box: Option<Rect>,
68 crop_box: Option<Rect>,
69 rotate: Option<i32>,
70}
71
72fn parse_version(data: &[u8]) -> (u8, u8) {
75 try_parse_version(data).unwrap_or((1, 4))
76}
77
78fn try_parse_version(data: &[u8]) -> Option<(u8, u8)> {
79 let window = &data[..data.len().min(1024)];
80 let pos = memchr::memmem::find(window, b"%PDF-")?;
81 let rest = &window[pos + 5..];
82 let (major, used) = read_version_component(rest)?;
83 if rest.get(used) != Some(&b'.') {
84 return None;
85 }
86 let (minor, _) = read_version_component(&rest[used + 1..])?;
87 Some((major, minor))
88}
89
90fn read_version_component(bytes: &[u8]) -> Option<(u8, usize)> {
93 let end = bytes
94 .iter()
95 .position(|b| !b.is_ascii_digit())
96 .unwrap_or(bytes.len());
97 if end == 0 || end > 3 {
98 return None;
99 }
100 let value = std::str::from_utf8(&bytes[..end]).ok()?.parse().ok()?;
101 Some((value, end))
102}
103
104fn normalize_rotation(deg: i32) -> i32 {
107 let r = deg.rem_euclid(360);
108 if r % 90 == 0 {
109 r
110 } else {
111 0
112 }
113}
114
115impl Document {
116 pub fn load(data: Vec<u8>) -> Result<Document> {
126 let version = parse_version(&data);
127 let xref = load_xref(&data)?;
128 let mut doc = Document {
129 data,
130 version,
131 xref,
132 cache: RefCell::new(FastMap::default()),
133 loading: RefCell::new(FastSet::default()),
134 objstms: RefCell::new(FastMap::default()),
135 decryptor: None,
136 pages: OnceCell::new(),
137 };
138 if doc
139 .xref
140 .trailer
141 .get("Encrypt")
142 .is_some_and(|o| !o.is_null())
143 {
144 doc.setup_decryption()?;
145 }
146 Ok(doc)
147 }
148
149 fn setup_decryption(&mut self) -> Result<()> {
156 let enc_obj = self
157 .xref
158 .trailer
159 .get("Encrypt")
160 .cloned()
161 .unwrap_or(Object::Null);
162 let enc = self.resolve(&enc_obj)?;
163 let enc_dict = enc.as_dict().ok_or(Error::Encrypted)?;
164 let id0: Vec<u8> = self
165 .xref
166 .trailer
167 .get("ID")
168 .and_then(Object::as_array)
169 .and_then(<[Object]>::first)
170 .and_then(Object::as_str_bytes)
171 .unwrap_or(&[])
172 .to_vec();
173 match Decryptor::from_standard(enc_dict, &id0) {
174 Some(dec) => {
175 self.decryptor = Some(dec);
176 self.cache.borrow_mut().clear();
180 Ok(())
181 }
182 None => Err(Error::Encrypted),
183 }
184 }
185
186 pub fn open(path: impl AsRef<Path>) -> Result<Document> {
188 Document::load(std::fs::read(path)?)
189 }
190
191 pub fn version(&self) -> (u8, u8) {
193 self.version
194 }
195
196 pub fn bytes(&self) -> &[u8] {
198 &self.data
199 }
200
201 pub fn xref(&self) -> &Xref {
203 &self.xref
204 }
205
206 pub fn get(&self, r: ObjRef) -> Result<Object> {
210 if let Some(cached) = self.cache.borrow().get(&(r.num, r.gen)) {
211 return Ok((**cached).clone());
212 }
213 if !self.loading.borrow_mut().insert(r.num) {
214 return Err(Error::CircularReference(r.num));
215 }
216 let result = self.load_object(r);
217 self.loading.borrow_mut().remove(&r.num);
218 let object = result?;
219 self.cache
220 .borrow_mut()
221 .insert((r.num, r.gen), Rc::new(object.clone()));
222 Ok(object)
223 }
224
225 fn load_object(&self, r: ObjRef) -> Result<Object> {
228 match self.xref.get(r.num) {
229 None | Some(XrefEntry::Free) => Err(Error::ObjectNotFound(r.num, r.gen)),
230 Some(XrefEntry::InFile { offset, .. }) => {
231 let offset = usize::try_from(offset)
232 .ok()
233 .filter(|&o| o < self.data.len())
234 .ok_or(Error::ObjectNotFound(r.num, r.gen))?;
235 self.object_at_spanned(offset).map(|parsed| parsed.1)
236 }
237 Some(XrefEntry::InStream { stream_num, index }) => {
238 self.load_from_object_stream(stream_num, index)
239 }
240 }
241 }
242
243 pub(crate) fn object_at_spanned(&self, offset: usize) -> Result<(ObjRef, Object, Span)> {
246 let mut parser = Parser::at(&self.data, offset);
247 let (r, mut object) = parser.parse_indirect(self)?;
248 if let Some(dec) = &self.decryptor {
252 dec.decrypt_object(&mut object, r.num, r.gen);
253 }
254 Ok((r, object, Span::new(offset as u64, parser.pos() as u64)))
255 }
256
257 pub(crate) fn objstm_handle(&self, stream_num: u32) -> Result<Rc<objstm::ObjStm>> {
260 if let Some(stm) = self.objstms.borrow().get(&stream_num) {
261 return Ok(Rc::clone(stm));
262 }
263 let container = self.get(ObjRef {
264 num: stream_num,
265 gen: 0,
266 })?;
267 let stream = container.as_stream().ok_or_else(|| Error::TypeMismatch {
268 expected: "stream",
269 found: type_name(&container),
270 })?;
271 let n = self
272 .resolve(stream.dict.get("N").unwrap_or(&Object::Null))?
273 .as_int()
274 .and_then(|v| usize::try_from(v).ok())
275 .ok_or(Error::MissingKey("N"))?;
276 let first = self
277 .resolve(stream.dict.get("First").unwrap_or(&Object::Null))?
278 .as_int()
279 .and_then(|v| usize::try_from(v).ok())
280 .ok_or(Error::MissingKey("First"))?;
281 let decoded = self.stream_data(stream)?;
282 let stm = Rc::new(objstm::ObjStm::parse(decoded, n, first)?);
283 self.objstms
284 .borrow_mut()
285 .insert(stream_num, Rc::clone(&stm));
286 Ok(stm)
287 }
288
289 fn load_from_object_stream(&self, stream_num: u32, index: u32) -> Result<Object> {
291 self.objstm_handle(stream_num)?.object(index)
292 }
293
294 pub fn resolve(&self, o: &Object) -> Result<Object> {
298 let mut current = o.clone();
299 let mut last_num = 0;
300 for _ in 0..MAX_RESOLVE_DEPTH {
301 match current {
302 Object::Ref(r) => {
303 last_num = r.num;
304 current = match self.get(r) {
305 Ok(object) => object,
306 Err(Error::CircularReference(n)) => {
307 return Err(Error::CircularReference(n))
308 }
309 Err(_) => return Ok(Object::Null),
310 };
311 }
312 other => return Ok(other),
313 }
314 }
315 Err(Error::CircularReference(last_num))
316 }
317
318 pub fn stream_data(&self, s: &Stream) -> Result<Vec<u8>> {
321 filters::decode_stream(s, self)
322 }
323
324 fn pages(&self) -> &[PageRec] {
326 self.pages.get_or_init(|| self.flatten_pages())
327 }
328
329 pub fn page_count(&self) -> usize {
338 if let Some(pages) = self.pages.get() {
339 return pages.len();
340 }
341 if let Some(count) = self.declared_page_count() {
342 return count;
343 }
344 self.pages().len()
345 }
346
347 fn declared_page_count(&self) -> Option<usize> {
353 let root = self.xref.trailer.get("Root")?;
354 let catalog = self.resolve(root).ok()?;
355 let pages = self.resolve(catalog.as_dict()?.get("Pages")?).ok()?;
356 let count = usize::try_from(self.int_value(pages.as_dict()?, "Count")?).ok()?;
357 (count <= self.data.len()).then_some(count)
360 }
361
362 pub fn page(&self, index: usize) -> Result<Page> {
364 let pages = self.pages();
365 let rec = pages
366 .get(index)
367 .ok_or(Error::PageNotFound(index, pages.len()))?;
368 Ok(Page {
369 index,
370 media_box: rec.media_box,
371 crop_box: rec.crop_box,
372 rotate: rec.rotate,
373 resources: rec.resources.clone(),
374 dict: rec.dict.clone(),
375 obj_ref: rec.obj_ref,
376 })
377 }
378
379 pub fn metadata(&self) -> Metadata {
382 let mut meta = Metadata::default();
383 let Some(info) = self.xref.trailer.get("Info") else {
384 return meta;
385 };
386 let Ok(info) = self.resolve(info) else {
387 return meta;
388 };
389 let Some(dict) = info.as_dict() else {
390 return meta;
391 };
392 meta.title = self.meta_string(dict, "Title");
393 meta.author = self.meta_string(dict, "Author");
394 meta.subject = self.meta_string(dict, "Subject");
395 meta.keywords = self.meta_string(dict, "Keywords");
396 meta.creator = self.meta_string(dict, "Creator");
397 meta.producer = self.meta_string(dict, "Producer");
398 meta.creation_date = self.meta_string(dict, "CreationDate");
399 meta.mod_date = self.meta_string(dict, "ModDate");
400 meta
401 }
402
403 fn meta_string(&self, dict: &Dict, key: &str) -> Option<String> {
405 let value = self.resolve(dict.get(key)?).ok()?;
406 Some(decode_text_string(value.as_str_bytes()?))
407 }
408
409 fn flatten_pages(&self) -> Vec<PageRec> {
414 let mut pages = Vec::new();
415 let Some(root) = self.xref.trailer.get("Root") else {
416 return pages;
417 };
418 let Ok(catalog) = self.resolve(root) else {
419 return pages;
420 };
421 let Some(tree_root) = catalog.as_dict().and_then(|d| d.get("Pages")) else {
422 return pages;
423 };
424 let mut visited: FastSet<ObjRef> = FastSet::default();
425 let mut stack: Vec<(Object, Inherited, usize)> =
426 vec![(tree_root.clone(), Inherited::default(), 0)];
427 while let Some((node, mut inherited, depth)) = stack.pop() {
428 if depth > MAX_TREE_DEPTH {
429 continue;
430 }
431 let node_ref = if let Object::Ref(r) = node {
432 Some(r)
433 } else {
434 None
435 };
436 if let Some(r) = node_ref {
437 if !visited.insert(r) {
438 continue; }
440 }
441 let Ok(resolved) = self.resolve(&node) else {
442 continue;
443 };
444 let Some(dict) = resolved.as_dict() else {
445 continue;
446 };
447 if let Some(res) = self.dict_value(dict, "Resources") {
448 inherited.resources = Some(res);
449 }
450 if let Some(mb) = self.rect_value(dict, "MediaBox") {
451 inherited.media_box = Some(mb);
452 }
453 if let Some(cb) = self.rect_value(dict, "CropBox") {
454 inherited.crop_box = Some(cb);
455 }
456 if let Some(rot) = self.int_value(dict, "Rotate") {
457 inherited.rotate = Some(rot);
458 }
459 let is_page = dict.get_name("Type").is_some_and(|n| n.0 == "Page");
460 let kids = if is_page {
461 None
462 } else {
463 self.array_value(dict, "Kids")
464 };
465 match kids {
466 Some(kids) => {
467 for kid in kids.iter().rev() {
469 stack.push((kid.clone(), inherited.clone(), depth + 1));
470 }
471 }
472 None => pages.push(make_page_rec(node_ref, dict.clone(), &inherited)),
473 }
474 }
475 pages
476 }
477
478 fn dict_value(&self, dict: &Dict, key: &str) -> Option<Dict> {
480 self.resolve(dict.get(key)?).ok()?.as_dict().cloned()
481 }
482
483 fn array_value(&self, dict: &Dict, key: &str) -> Option<Vec<Object>> {
485 match self.resolve(dict.get(key)?).ok()? {
486 Object::Array(items) => Some(items),
487 _ => None,
488 }
489 }
490
491 fn int_value(&self, dict: &Dict, key: &str) -> Option<i32> {
493 let v = self.resolve(dict.get(key)?).ok()?.as_f64()?;
494 if v.is_finite() {
495 Some(v as i32)
496 } else {
497 None
498 }
499 }
500
501 fn rect_value(&self, dict: &Dict, key: &str) -> Option<Rect> {
504 let items = self.array_value(dict, key)?;
505 if items.len() != 4 {
506 return None;
507 }
508 let mut coords = [0.0f32; 4];
509 for (slot, item) in coords.iter_mut().zip(&items) {
510 let n = self.resolve(item).ok()?.as_f64()?;
511 if !n.is_finite() {
512 return None;
513 }
514 *slot = n as f32;
515 }
516 Some(Rect::new(coords[0], coords[1], coords[2], coords[3]).normalize())
517 }
518}
519
520fn make_page_rec(obj_ref: Option<ObjRef>, dict: Dict, inherited: &Inherited) -> PageRec {
523 let media_box = inherited
524 .media_box
525 .filter(|r| r.width() > 0.0 && r.height() > 0.0)
526 .unwrap_or(US_LETTER);
527 let crop_box = inherited
528 .crop_box
529 .and_then(|c| c.intersect(media_box))
530 .filter(|r| r.width() > 0.0 && r.height() > 0.0)
531 .unwrap_or(media_box);
532 PageRec {
533 obj_ref,
534 media_box,
535 crop_box,
536 rotate: normalize_rotation(inherited.rotate.unwrap_or(0)),
537 resources: inherited.resources.clone().unwrap_or_default(),
538 dict,
539 }
540}
541
542fn type_name(o: &Object) -> &'static str {
544 match o {
545 Object::Null => "null",
546 Object::Bool(_) => "boolean",
547 Object::Int(_) => "integer",
548 Object::Real(_) => "real",
549 Object::String(_) => "string",
550 Object::Name(_) => "name",
551 Object::Array(_) => "array",
552 Object::Dict(_) => "dictionary",
553 Object::Stream(_) => "stream",
554 Object::Ref(_) => "reference",
555 }
556}
557
558impl Resolve for Document {
559 fn resolve_ref(&self, r: ObjRef) -> Option<Object> {
560 self.get(r).ok()
561 }
562}
563
564#[derive(Debug, Clone, Default, PartialEq, Eq)]
567pub struct Metadata {
568 pub title: Option<String>,
569 pub author: Option<String>,
570 pub subject: Option<String>,
571 pub keywords: Option<String>,
572 pub creator: Option<String>,
573 pub producer: Option<String>,
574 pub creation_date: Option<String>,
575 pub mod_date: Option<String>,
576}
577
578pub struct Page {
584 pub index: usize,
586 pub media_box: Rect,
587 pub crop_box: Rect,
588 pub rotate: i32,
589 pub resources: Dict,
591 dict: Dict,
592 obj_ref: Option<ObjRef>,
593}
594
595impl Page {
596 pub fn object_ref(&self) -> Option<ObjRef> {
600 self.obj_ref
601 }
602
603 pub fn content(&self, doc: &Document) -> Result<Vec<u8>> {
607 let Some(contents) = self.dict.get("Contents") else {
608 return Ok(Vec::new());
609 };
610 match doc.resolve(contents)? {
611 Object::Stream(ref s) => doc.stream_data(s),
612 Object::Array(items) => {
613 let mut out = Vec::new();
614 let mut first = true;
615 for item in &items {
616 let part = doc.resolve(item)?;
617 let Some(stream) = part.as_stream() else {
618 continue; };
620 if !first {
621 out.push(b'\n');
622 }
623 out.extend_from_slice(&doc.stream_data(stream)?);
624 first = false;
625 }
626 Ok(out)
627 }
628 _ => Ok(Vec::new()),
629 }
630 }
631
632 pub fn size(&self) -> (f32, f32) {
634 let (w, h) = (self.crop_box.width(), self.crop_box.height());
635 if self.rotate == 90 || self.rotate == 270 {
636 (h, w)
637 } else {
638 (w, h)
639 }
640 }
641
642 pub fn dict(&self) -> &Dict {
644 &self.dict
645 }
646}
647
648#[cfg(test)]
649mod tests {
650 use super::*;
651 use crate::parser::{NoResolve, Parser};
652 use crate::xref::XrefEntry;
653 use pdfboss_testkit::{multi_page_doc, objstm_doc, objstm_payload, simple_doc, PdfBuilder};
654
655 fn replace_once(data: &[u8], from: &[u8], to: &[u8]) -> Vec<u8> {
658 let pos = memchr::memmem::find(data, from).expect("pattern present in fixture");
659 let mut out = Vec::with_capacity(data.len() - from.len() + to.len());
660 out.extend_from_slice(&data[..pos]);
661 out.extend_from_slice(to);
662 out.extend_from_slice(&data[pos + from.len()..]);
663 out
664 }
665
666 fn contains(haystack: &[u8], needle: &[u8]) -> bool {
667 memchr::memmem::find(haystack, needle).is_some()
668 }
669
670 const FONT: &str = "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>";
671
672 #[test]
673 fn loads_simple_doc() {
674 let doc = Document::load(simple_doc("Greetings, cosmos!")).unwrap();
675 assert_eq!(doc.version(), (1, 7));
676 assert_eq!(doc.page_count(), 1);
677 let page = doc.page(0).unwrap();
678 assert_eq!(page.index, 0);
679 assert_eq!(page.media_box, Rect::new(0.0, 0.0, 612.0, 792.0));
680 assert_eq!(page.crop_box, page.media_box);
681 assert_eq!(page.rotate, 0);
682 assert_eq!(page.size(), (612.0, 792.0));
683 assert!(page.resources.get("Font").is_some());
684 let content = page.content(&doc).unwrap();
685 assert!(contains(&content, b"Greetings, cosmos!"));
686 }
687
688 #[test]
689 fn multi_page_ordering() {
690 let doc = Document::load(multi_page_doc(&["alpha", "beta", "gamma"])).unwrap();
691 assert_eq!(doc.page_count(), 3);
692 for (i, text) in ["alpha", "beta", "gamma"].iter().enumerate() {
693 let content = doc.page(i).unwrap().content(&doc).unwrap();
694 assert!(
695 contains(&content, text.as_bytes()),
696 "page {i} should show {text}"
697 );
698 }
699 }
700
701 #[test]
702 fn page_index_out_of_bounds() {
703 let doc = Document::load(simple_doc("x")).unwrap();
704 assert!(matches!(doc.page(5), Err(Error::PageNotFound(5, 1))));
705 }
706
707 #[test]
708 fn open_reads_from_disk() {
709 let dir = std::env::temp_dir();
710 let path = dir.join(format!("pdfboss-doc-test-{}.pdf", std::process::id()));
711 std::fs::write(&path, simple_doc("from disk")).unwrap();
712 let doc = Document::open(&path).unwrap();
713 std::fs::remove_file(&path).ok();
714 assert_eq!(doc.page_count(), 1);
715 let content = doc.page(0).unwrap().content(&doc).unwrap();
716 assert!(contains(&content, b"from disk"));
717 assert!(matches!(
718 Document::open(dir.join("pdfboss-doc-test-missing.pdf")),
719 Err(Error::Io(_))
720 ));
721 }
722
723 #[test]
724 fn encrypt_in_trailer_is_rejected() {
725 let data = replace_once(
726 &simple_doc("secret"),
727 b"trailer\n<< /Size",
728 b"trailer\n<< /Encrypt 9 0 R /Size",
729 );
730 assert!(matches!(Document::load(data), Err(Error::Encrypted)));
731 }
732
733 #[test]
734 fn metadata_utf16be_round_trip() {
735 let mut b = PdfBuilder::new();
736 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
737 b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
738 b.object(6, "<< /Title <FEFF00480151> /Author (plain author) >>");
740 let data = replace_once(&b.build(1), b"<< /Size", b"<< /Info 6 0 R /Size");
741 let doc = Document::load(data).unwrap();
742 let meta = doc.metadata();
743 assert_eq!(meta.title.as_deref(), Some("H\u{151}"));
744 assert_eq!(meta.author.as_deref(), Some("plain author"));
745 assert_eq!(meta.subject, None);
746 assert_eq!(meta.keywords, None);
747 assert_eq!(meta.creation_date, None);
748 }
749
750 #[test]
751 fn metadata_without_info_is_all_none() {
752 let doc = Document::load(simple_doc("x")).unwrap();
753 assert_eq!(doc.metadata(), Metadata::default());
754 }
755
756 #[test]
757 fn missing_object_resolves_to_null() {
758 let doc = Document::load(simple_doc("x")).unwrap();
759 let missing = Object::Ref(ObjRef { num: 99, gen: 0 });
760 assert_eq!(doc.resolve(&missing).unwrap(), Object::Null);
761 assert!(matches!(
762 doc.get(ObjRef { num: 99, gen: 0 }),
763 Err(Error::ObjectNotFound(99, 0))
764 ));
765 }
766
767 #[test]
768 fn self_reference_is_circular() {
769 let mut b = PdfBuilder::new();
770 b.object(1, "<< /Type /Catalog >>");
771 b.object(6, "6 0 R");
772 let doc = Document::load(b.build(1)).unwrap();
773 let loops = Object::Ref(ObjRef { num: 6, gen: 0 });
774 assert!(matches!(
775 doc.resolve(&loops),
776 Err(Error::CircularReference(6))
777 ));
778 }
779
780 #[test]
781 fn generation_mismatch_is_tolerated() {
782 let doc = Document::load(simple_doc("x")).unwrap();
783 let catalog = doc.get(ObjRef { num: 1, gen: 7 }).unwrap();
784 let dict = catalog.as_dict().unwrap();
785 assert_eq!(dict.get_name("Type").map(|n| n.0.as_str()), Some("Catalog"));
786 }
787
788 #[test]
789 fn objects_in_object_streams_are_fetched() {
790 let mut b = PdfBuilder::new();
791 let (dict, payload) =
792 objstm_payload(&[(1, "<< /Type /Catalog /Pages 2 0 R >>"), (5, FONT)]);
793 b.stream(6, &dict, &payload);
794 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
795 b.object(
796 3,
797 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
798 /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
799 );
800 b.stream(4, "", b"BT /F1 12 Tf (compressed hello) Tj ET");
801 let doc = Document::load(b.build_xref_stream(1)).unwrap();
802 assert_eq!(doc.page_count(), 1);
803 let page = doc.page(0).unwrap();
804 assert!(contains(&page.content(&doc).unwrap(), b"compressed hello"));
805 let font = doc.get(ObjRef { num: 5, gen: 0 }).unwrap();
806 assert_eq!(
807 font.as_dict()
808 .and_then(|d| d.get_name("BaseFont"))
809 .map(|n| n.0.as_str()),
810 Some("Helvetica")
811 );
812 }
813
814 #[test]
815 fn contents_array_is_joined_with_newlines() {
816 let mut b = PdfBuilder::new();
817 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
818 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
819 b.object(
820 3,
821 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
822 /Contents [4 0 R null 5 0 R] >>",
823 );
824 b.stream(4, "", b"q");
825 b.stream(5, "", b"Q");
826 let doc = Document::load(b.build(1)).unwrap();
827 let content = doc.page(0).unwrap().content(&doc).unwrap();
828 assert_eq!(content, b"q\nQ", "streams joined by \\n, null skipped");
829 }
830
831 #[test]
832 fn inheritance_from_pages_node_and_rotate_swap() {
833 let mut b = PdfBuilder::new();
834 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
835 b.object(
836 2,
837 "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 \
838 /Resources << /Font << /F1 5 0 R >> >> /MediaBox [0 0 400 600] >>",
839 );
840 b.object(3, "<< /Type /Page /Parent 2 0 R >>");
841 b.object(4, "<< /Type /Page /Parent 2 0 R /Rotate 270 >>");
842 b.object(5, FONT);
843 let doc = Document::load(b.build(1)).unwrap();
844 assert_eq!(doc.page_count(), 2);
845
846 let first = doc.page(0).unwrap();
847 assert_eq!(first.media_box, Rect::new(0.0, 0.0, 400.0, 600.0));
848 assert_eq!(first.crop_box, first.media_box);
849 assert!(first.resources.get("Font").is_some(), "inherited resources");
850 assert_eq!(first.rotate, 0);
851 assert!(
852 first.content(&doc).unwrap().is_empty(),
853 "no /Contents means empty content"
854 );
855 assert_eq!(first.size(), (400.0, 600.0));
856
857 let second = doc.page(1).unwrap();
858 assert_eq!(second.rotate, 270);
859 assert_eq!(second.size(), (600.0, 400.0), "rotate 270 swaps w/h");
860 }
861
862 #[test]
863 fn crop_box_intersected_and_rotate_normalized() {
864 let mut b = PdfBuilder::new();
865 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
866 b.object(2, "<< /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 >>");
867 b.object(
868 3,
869 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] \
870 /CropBox [100 100 400 400] /Rotate 450 >>",
871 );
872 b.object(4, "<< /Type /Page /Parent 2 0 R /Rotate -90 >>");
873 b.object(
874 5,
875 "<< /Type /Page /Parent 2 0 R /Rotate 45 /MediaBox [0 0 0 0] >>",
876 );
877 let doc = Document::load(b.build(1)).unwrap();
878
879 let clipped = doc.page(0).unwrap();
880 assert_eq!(clipped.crop_box, Rect::new(100.0, 100.0, 200.0, 200.0));
881 assert_eq!(clipped.rotate, 90, "450 normalizes to 90");
882 assert_eq!(clipped.size(), (100.0, 100.0));
883
884 assert_eq!(doc.page(1).unwrap().rotate, 270, "-90 normalizes to 270");
885 let odd = doc.page(2).unwrap();
886 assert_eq!(odd.rotate, 0, "non-multiple of 90 falls back to 0");
887 assert_eq!(odd.media_box, US_LETTER, "degenerate media box defaults");
888 }
889
890 #[test]
891 fn kids_cycle_truncates_without_hanging() {
892 let mut b = PdfBuilder::new();
893 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
894 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
896 b.object(3, "<< /Type /Pages /Kids [4 0 R 2 0 R] /Count 1 >>");
897 b.object(
898 4,
899 "<< /Type /Page /Parent 3 0 R /MediaBox [0 0 100 100] /Contents 5 0 R >>",
900 );
901 b.stream(5, "", b"0 0 50 50 re f");
902 let doc = Document::load(b.build(1)).unwrap();
903 assert_eq!(doc.page_count(), 1, "cycle back-edge yields no extra pages");
904 assert!(contains(
905 &doc.page(0).unwrap().content(&doc).unwrap(),
906 b"re f"
907 ));
908 }
909
910 #[test]
911 fn tree_depth_is_capped() {
912 let mut b = PdfBuilder::new();
913 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
914 let last = 302u32;
916 for num in 2..last {
917 b.object(
918 num,
919 &format!("<< /Type /Pages /Kids [{} 0 R] /Count 1 >>", num + 1),
920 );
921 }
922 b.object(last, "<< /Type /Page >>");
923 let doc = Document::load(b.build(1)).unwrap();
924 assert_eq!(doc.page_count(), 1, "declared /Count is reported cheaply");
929 assert!(
930 matches!(doc.page(0), Err(Error::PageNotFound(0, 0))),
931 "leaf beyond the depth cap cannot be materialized"
932 );
933 }
934
935 #[test]
936 fn page_count_reports_declared_count_cheaply() {
937 let mut b = PdfBuilder::new();
941 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
942 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 5 >>");
943 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
944 let doc = Document::load(b.build(1)).unwrap();
945 assert_eq!(doc.page_count(), 5, "declared /Count reported verbatim");
946 assert!(doc.page(0).is_ok(), "the one real page materializes");
947 assert!(
948 matches!(doc.page(1), Err(Error::PageNotFound(1, 1))),
949 "access past the real pages fails with the true length"
950 );
951 }
952
953 #[test]
954 fn page_count_falls_back_to_walk_when_count_absent() {
955 let mut b = PdfBuilder::new();
956 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
957 b.object(2, "<< /Type /Pages /Kids [3 0 R] >>"); b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
959 let doc = Document::load(b.build(1)).unwrap();
960 assert_eq!(
961 doc.page_count(),
962 1,
963 "missing /Count is recovered by walking"
964 );
965 }
966
967 #[test]
968 fn page_count_ignores_corrupt_oversized_count() {
969 let mut b = PdfBuilder::new();
972 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
973 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 999999999 >>");
974 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
975 let doc = Document::load(b.build(1)).unwrap();
976 assert_eq!(doc.page_count(), 1, "implausible /Count is rejected");
977 }
978
979 #[test]
980 fn version_scan_and_default() {
981 let mut b = PdfBuilder::new().version(2, 0);
982 b.object(1, "<< /Type /Catalog >>");
983 assert_eq!(Document::load(b.build(1)).unwrap().version(), (2, 0));
984 let data = replace_once(&simple_doc("v"), b"%PDF-", b"%QQQ-");
986 assert_eq!(Document::load(data).unwrap().version(), (1, 4));
987 }
988
989 #[test]
990 fn deeply_nested_root_object_does_not_overflow_the_stack() {
991 let mut data = b"%PDF-1.7\n1 0 obj\n".to_vec();
996 data.extend(std::iter::repeat_n(b'[', 50_000));
997 data.extend(std::iter::repeat_n(b']', 50_000));
998 data.extend_from_slice(b"\nendobj\ntrailer\n<</Root 1 0 R>>\n%%EOF\n");
999 let outcome = std::thread::Builder::new()
1000 .stack_size(1024 * 1024)
1001 .spawn(move || Document::load(data).map(|doc| doc.page_count()))
1002 .expect("spawn test thread")
1003 .join()
1004 .expect("Document::load must not overflow the stack");
1005 assert!(matches!(outcome, Ok(0) | Err(_)));
1008 }
1009
1010 #[test]
1011 fn bytes_and_xref_accessors() {
1012 let data = simple_doc("accessors");
1013 let doc = Document::load(data.clone()).unwrap();
1014 assert_eq!(doc.bytes(), &data[..]);
1015 assert!(!doc.xref().is_empty());
1016 assert!(doc.xref().trailer.get("Root").is_some());
1017 }
1018
1019 #[test]
1020 fn object_at_spanned_reparses_identically() {
1021 let data = simple_doc("spanned");
1022 let doc = Document::load(data).unwrap();
1023 for (num, entry) in doc.xref().iter() {
1024 let XrefEntry::InFile { offset, gen } = entry else {
1025 continue;
1026 };
1027 let (r, object, span) = doc.object_at_spanned(offset as usize).unwrap();
1028 assert_eq!(r.num, num);
1029 assert_eq!(r.gen, gen);
1030 assert_eq!(span.start, offset);
1031 assert!(span.end as usize <= doc.bytes().len());
1032 let slice = &doc.bytes()[span.start as usize..span.end as usize];
1034 let (r2, object2) = Parser::new(slice).parse_indirect(&NoResolve).unwrap();
1035 assert_eq!(r2, r);
1036 assert_eq!(object2, object);
1037 }
1038 }
1039
1040 #[test]
1041 fn page_object_ref_points_at_a_page_dict() {
1042 let doc = Document::load(multi_page_doc(&["one", "two"])).unwrap();
1043 for index in 0..doc.page_count() {
1044 let page = doc.page(index).unwrap();
1045 let r = page.object_ref().expect("builder pages are indirect");
1046 let resolved = doc.get(r).unwrap();
1047 assert_eq!(
1048 resolved
1049 .as_dict()
1050 .unwrap()
1051 .get_name("Type")
1052 .map(|n| n.0.as_str()),
1053 Some("Page")
1054 );
1055 }
1056 }
1057
1058 const RC4_FIXTURE_KEY_LEN: usize = 16; const RC4_FIXTURE_P: i32 = -44;
1068 const RC4_FIXTURE_ID0: &[u8] = b"0123456789abcdef";
1069 const RC4_FIXTURE_PAD: [u8; 32] = [
1070 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01,
1071 0x08, 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53,
1072 0x69, 0x7A,
1073 ];
1074
1075 #[rustfmt::skip]
1076 const RC4_FIXTURE_MD5_S: [u32; 64] = [
1077 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
1078 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
1079 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
1080 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
1081 ];
1082 #[rustfmt::skip]
1083 const RC4_FIXTURE_MD5_K: [u32; 64] = [
1084 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
1085 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
1086 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
1087 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
1088 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
1089 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
1090 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
1091 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,
1092 ];
1093
1094 fn rc4_fixture_md5(input: &[u8]) -> [u8; 16] {
1095 let (mut a0, mut b0, mut c0, mut d0) = (
1096 0x6745_2301u32,
1097 0xefcd_ab89u32,
1098 0x98ba_dcfeu32,
1099 0x1032_5476u32,
1100 );
1101 let mut msg = input.to_vec();
1102 let bitlen = (input.len() as u64).wrapping_mul(8);
1103 msg.push(0x80);
1104 while msg.len() % 64 != 56 {
1105 msg.push(0);
1106 }
1107 msg.extend_from_slice(&bitlen.to_le_bytes());
1108 for chunk in msg.chunks_exact(64) {
1109 let mut m = [0u32; 16];
1110 for (word, bytes) in m.iter_mut().zip(chunk.chunks_exact(4)) {
1111 *word = u32::from_le_bytes(bytes.try_into().unwrap());
1112 }
1113 let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0);
1114 for i in 0..64 {
1115 let (f, g) = match i {
1116 0..=15 => ((b & c) | (!b & d), i),
1117 16..=31 => ((d & b) | (!d & c), (5 * i + 1) % 16),
1118 32..=47 => (b ^ c ^ d, (3 * i + 5) % 16),
1119 _ => (c ^ (b | !d), (7 * i) % 16),
1120 };
1121 let f = f
1122 .wrapping_add(a)
1123 .wrapping_add(RC4_FIXTURE_MD5_K[i])
1124 .wrapping_add(m[g]);
1125 a = d;
1126 d = c;
1127 c = b;
1128 b = b.wrapping_add(f.rotate_left(RC4_FIXTURE_MD5_S[i]));
1129 }
1130 a0 = a0.wrapping_add(a);
1131 b0 = b0.wrapping_add(b);
1132 c0 = c0.wrapping_add(c);
1133 d0 = d0.wrapping_add(d);
1134 }
1135 let mut out = [0u8; 16];
1136 out[0..4].copy_from_slice(&a0.to_le_bytes());
1137 out[4..8].copy_from_slice(&b0.to_le_bytes());
1138 out[8..12].copy_from_slice(&c0.to_le_bytes());
1139 out[12..16].copy_from_slice(&d0.to_le_bytes());
1140 out
1141 }
1142
1143 fn rc4_fixture_rc4(key: &[u8], data: &[u8]) -> Vec<u8> {
1144 let mut s: [u8; 256] = core::array::from_fn(|i| i as u8);
1145 let mut j = 0u8;
1146 for i in 0..256 {
1147 j = j.wrapping_add(s[i]).wrapping_add(key[i % key.len()]);
1148 s.swap(i, j as usize);
1149 }
1150 let mut out = Vec::with_capacity(data.len());
1151 let (mut i, mut j) = (0u8, 0u8);
1152 for &byte in data {
1153 i = i.wrapping_add(1);
1154 j = j.wrapping_add(s[i as usize]);
1155 s.swap(i as usize, j as usize);
1156 let k = s[s[i as usize].wrapping_add(s[j as usize]) as usize];
1157 out.push(byte ^ k);
1158 }
1159 out
1160 }
1161
1162 fn rc4_fixture_owner_entry() -> Vec<u8> {
1164 let mut d = rc4_fixture_md5(&RC4_FIXTURE_PAD);
1165 for _ in 0..50 {
1166 d = rc4_fixture_md5(&d[..RC4_FIXTURE_KEY_LEN]);
1167 }
1168 let rc4key = d[..RC4_FIXTURE_KEY_LEN].to_vec();
1169 let mut o = rc4_fixture_rc4(&rc4key, &RC4_FIXTURE_PAD);
1170 for i in 1u8..=19 {
1171 let k: Vec<u8> = rc4key.iter().map(|b| b ^ i).collect();
1172 o = rc4_fixture_rc4(&k, &o);
1173 }
1174 o
1175 }
1176
1177 fn rc4_fixture_file_key(o: &[u8]) -> Vec<u8> {
1179 let mut input = Vec::new();
1180 input.extend_from_slice(&RC4_FIXTURE_PAD);
1181 input.extend_from_slice(o);
1182 input.extend_from_slice(&(RC4_FIXTURE_P as u32).to_le_bytes());
1183 input.extend_from_slice(RC4_FIXTURE_ID0);
1184 let mut d = rc4_fixture_md5(&input);
1185 for _ in 0..50 {
1186 d = rc4_fixture_md5(&d[..RC4_FIXTURE_KEY_LEN]);
1187 }
1188 d[..RC4_FIXTURE_KEY_LEN].to_vec()
1189 }
1190
1191 fn rc4_fixture_user_entry(key: &[u8]) -> Vec<u8> {
1193 let mut input = Vec::new();
1194 input.extend_from_slice(&RC4_FIXTURE_PAD);
1195 input.extend_from_slice(RC4_FIXTURE_ID0);
1196 let mut x = rc4_fixture_md5(&input).to_vec();
1197 x = rc4_fixture_rc4(key, &x);
1198 for i in 1u8..=19 {
1199 let k: Vec<u8> = key.iter().map(|b| b ^ i).collect();
1200 x = rc4_fixture_rc4(&k, &x);
1201 }
1202 x.resize(32, 0); x
1204 }
1205
1206 fn rc4_fixture_obj_key(key: &[u8], num: u32, gen: u16) -> Vec<u8> {
1207 let mut input = key.to_vec();
1208 input.extend_from_slice(&num.to_le_bytes()[..3]);
1209 input.extend_from_slice(&gen.to_le_bytes()[..2]);
1210 rc4_fixture_md5(&input)[..(key.len() + 5).min(16)].to_vec()
1211 }
1212
1213 fn rc4_fixture_hexstr(b: &[u8]) -> String {
1214 let mut s = String::from("<");
1215 for x in b {
1216 s.push_str(&format!("{x:02x}"));
1217 }
1218 s.push('>');
1219 s
1220 }
1221
1222 fn rc4_encrypted_fixture() -> Vec<u8> {
1226 let o = rc4_fixture_owner_entry();
1227 let key = rc4_fixture_file_key(&o);
1228 let u = rc4_fixture_user_entry(&key);
1229 let msg = rc4_fixture_rc4(&rc4_fixture_obj_key(&key, 3, 0), b"Top secret message");
1230
1231 let mut b = PdfBuilder::new().version(1, 4);
1232 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1233 b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
1234 b.object(3, &format!("<< /Msg {} >>", rc4_fixture_hexstr(&msg)));
1235 b.object(
1236 9,
1237 &format!(
1238 "<< /Filter /Standard /V 2 /R 3 /Length 128 /P {} /O {} /U {} >>",
1239 RC4_FIXTURE_P,
1240 rc4_fixture_hexstr(&o),
1241 rc4_fixture_hexstr(&u)
1242 ),
1243 );
1244 let trailer = format!(
1245 "/Encrypt 9 0 R /ID [{}{}]",
1246 rc4_fixture_hexstr(RC4_FIXTURE_ID0),
1247 rc4_fixture_hexstr(RC4_FIXTURE_ID0)
1248 );
1249 b.trailer_extra(&trailer).build(1)
1250 }
1251
1252 #[test]
1253 fn encrypted_generation_mismatch_still_decrypts() {
1254 let doc = Document::load(rc4_encrypted_fixture()).expect("empty password opens the file");
1264 let obj3 = doc.get(ObjRef { num: 3, gen: 7 }).unwrap();
1265 let msg = obj3
1266 .as_dict()
1267 .unwrap()
1268 .get("Msg")
1269 .unwrap()
1270 .as_str_bytes()
1271 .unwrap();
1272 assert_eq!(
1273 msg, b"Top secret message",
1274 "decrypted using the file's real gen (0), not the mismatched request (7)"
1275 );
1276 }
1277
1278 #[test]
1279 fn objstm_doc_fixture_loads_and_resolves_members() {
1280 let data = objstm_doc(&[(7, "<< /Marker (inside) >>")]);
1281 let doc = Document::load(data).unwrap();
1282 assert_eq!(doc.page_count(), 1);
1283 let member = doc.get(ObjRef { num: 7, gen: 0 }).unwrap();
1284 let text = member.as_dict().unwrap().get("Marker").unwrap();
1285 assert_eq!(text.as_str_bytes(), Some(&b"inside"[..]));
1286 assert!(matches!(
1288 doc.xref().get(7),
1289 Some(XrefEntry::InStream { stream_num: 4, .. })
1290 ));
1291 }
1292}