1use 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
22const MAX_TREE_DEPTH: usize = 256;
24
25pub struct Document {
27 data: Arc<Vec<u8>>,
28 version: (u8, u8),
29 xref: Arc<Xref>,
30 cache: RefCell<FastMap<(u32, u16), Rc<Object>>>,
32 loading: RefCell<FastSet<u32>>,
35 objstms: RefCell<FastMap<u32, Rc<objstm::ObjStm>>>,
39 decryptor: Option<Decryptor>,
43 pages: OnceCell<Arc<Vec<PageRec>>>,
48}
49
50pub 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
77struct PageRec {
79 obj_ref: Option<ObjRef>,
80 media_box: Rect,
81 crop_box: Rect,
82 bleed_box: Rect,
83 trim_box: Rect,
84 art_box: Rect,
85 rotate: i32,
86 resources: Dict,
87 dict: Dict,
88}
89
90#[derive(Clone, Default)]
92struct Inherited {
93 resources: Option<Dict>,
94 media_box: Option<Rect>,
95 crop_box: Option<Rect>,
96 rotate: Option<i32>,
97}
98
99fn parse_version(data: &[u8]) -> (u8, u8) {
102 try_parse_version(data).unwrap_or((1, 4))
103}
104
105fn try_parse_version(data: &[u8]) -> Option<(u8, u8)> {
106 let window = &data[..data.len().min(1024)];
107 let pos = memchr::memmem::find(window, b"%PDF-")?;
108 let rest = &window[pos + 5..];
109 let (major, used) = read_version_component(rest)?;
110 if rest.get(used) != Some(&b'.') {
111 return None;
112 }
113 let (minor, _) = read_version_component(&rest[used + 1..])?;
114 Some((major, minor))
115}
116
117fn read_version_component(bytes: &[u8]) -> Option<(u8, usize)> {
120 let end = bytes
121 .iter()
122 .position(|b| !b.is_ascii_digit())
123 .unwrap_or(bytes.len());
124 if end == 0 || end > 3 {
125 return None;
126 }
127 let value = std::str::from_utf8(&bytes[..end]).ok()?.parse().ok()?;
128 Some((value, end))
129}
130
131fn normalize_rotation(deg: i32) -> i32 {
134 let r = deg.rem_euclid(360);
135 if r % 90 == 0 {
136 r
137 } else {
138 0
139 }
140}
141
142impl Document {
143 pub fn load(data: Vec<u8>) -> Result<Document> {
153 Document::load_with_password(data, "")
154 }
155
156 pub fn load_with_password(data: Vec<u8>, password: &str) -> Result<Document> {
164 let version = parse_version(&data);
165 let xref = load_xref(&data)?;
166 let mut doc = Document {
167 data: Arc::new(data),
168 version,
169 xref: Arc::new(xref),
170 cache: RefCell::new(FastMap::default()),
171 loading: RefCell::new(FastSet::default()),
172 objstms: RefCell::new(FastMap::default()),
173 decryptor: None,
174 pages: OnceCell::new(),
175 };
176 if doc
177 .xref
178 .trailer
179 .get("Encrypt")
180 .is_some_and(|o| !o.is_null())
181 {
182 doc.setup_decryption(password)?;
183 }
184 Ok(doc)
185 }
186
187 pub fn open_with_password(path: impl AsRef<Path>, password: &str) -> Result<Document> {
190 Document::load_with_password(std::fs::read(path)?, password)
191 }
192
193 fn setup_decryption(&mut self, password: &str) -> Result<()> {
201 let enc_obj = self
202 .xref
203 .trailer
204 .get("Encrypt")
205 .cloned()
206 .unwrap_or(Object::Null);
207 let enc = self.resolve(&enc_obj)?;
208 let enc_dict = enc.as_dict().ok_or(Error::Encrypted)?;
209 let id0: Vec<u8> = self
210 .xref
211 .trailer
212 .get("ID")
213 .and_then(Object::as_array)
214 .and_then(<[Object]>::first)
215 .and_then(Object::as_str_bytes)
216 .unwrap_or(&[])
217 .to_vec();
218 match Decryptor::from_standard_with_password_str(enc_dict, &id0, password) {
219 Some(dec) => {
220 self.decryptor = Some(dec);
221 self.cache.borrow_mut().clear();
225 Ok(())
226 }
227 None => Err(Error::Encrypted),
228 }
229 }
230
231 pub fn open(path: impl AsRef<Path>) -> Result<Document> {
233 Document::load(std::fs::read(path)?)
234 }
235
236 pub fn version(&self) -> (u8, u8) {
238 self.version
239 }
240
241 pub fn bytes(&self) -> &[u8] {
243 &self.data
244 }
245
246 pub fn xref(&self) -> &Xref {
248 &self.xref
249 }
250
251 pub fn get(&self, r: ObjRef) -> Result<Object> {
255 if let Some(cached) = self.cache.borrow().get(&(r.num, r.gen)) {
256 return Ok((**cached).clone());
257 }
258 if !self.loading.borrow_mut().insert(r.num) {
259 return Err(Error::CircularReference(r.num));
260 }
261 let result = self.load_object(r);
262 self.loading.borrow_mut().remove(&r.num);
263 let object = result?;
264 self.cache
265 .borrow_mut()
266 .insert((r.num, r.gen), Rc::new(object.clone()));
267 Ok(object)
268 }
269
270 fn load_object(&self, r: ObjRef) -> Result<Object> {
273 match self.xref.get(r.num) {
274 None | Some(XrefEntry::Free) => Err(Error::ObjectNotFound(r.num, r.gen)),
275 Some(XrefEntry::InFile { offset, .. }) => {
276 let offset = usize::try_from(offset)
277 .ok()
278 .filter(|&o| o < self.data.len())
279 .ok_or(Error::ObjectNotFound(r.num, r.gen))?;
280 self.object_at_spanned(offset).map(|parsed| parsed.1)
281 }
282 Some(XrefEntry::InStream { stream_num, index }) => {
283 self.load_from_object_stream(stream_num, index)
284 }
285 }
286 }
287
288 pub(crate) fn object_at_spanned(&self, offset: usize) -> Result<(ObjRef, Object, Span)> {
291 let mut parser = Parser::at(&self.data, offset);
292 let (r, mut object) = parser.parse_indirect(self)?;
293 if let Some(dec) = &self.decryptor {
297 dec.decrypt_object(&mut object, r.num, r.gen);
298 }
299 Ok((r, object, Span::new(offset as u64, parser.pos() as u64)))
300 }
301
302 pub(crate) fn objstm_handle(&self, stream_num: u32) -> Result<Rc<objstm::ObjStm>> {
305 if let Some(stm) = self.objstms.borrow().get(&stream_num) {
306 return Ok(Rc::clone(stm));
307 }
308 let container = self.get(ObjRef {
309 num: stream_num,
310 gen: 0,
311 })?;
312 let stream = container.as_stream().ok_or_else(|| Error::TypeMismatch {
313 expected: "stream",
314 found: type_name(&container),
315 })?;
316 let n = self
317 .resolve(stream.dict.get("N").unwrap_or(&Object::Null))?
318 .as_int()
319 .and_then(|v| usize::try_from(v).ok())
320 .ok_or(Error::MissingKey("N"))?;
321 let first = self
322 .resolve(stream.dict.get("First").unwrap_or(&Object::Null))?
323 .as_int()
324 .and_then(|v| usize::try_from(v).ok())
325 .ok_or(Error::MissingKey("First"))?;
326 let decoded = self.stream_data(stream)?;
327 let stm = Rc::new(objstm::ObjStm::parse(decoded, n, first)?);
328 self.objstms
329 .borrow_mut()
330 .insert(stream_num, Rc::clone(&stm));
331 Ok(stm)
332 }
333
334 fn load_from_object_stream(&self, stream_num: u32, index: u32) -> Result<Object> {
336 self.objstm_handle(stream_num)?.object(index)
337 }
338
339 pub fn resolve(&self, o: &Object) -> Result<Object> {
350 crate::source::resolve_sync_with(self, o)
351 }
352
353 pub fn stream_data(&self, s: &Stream) -> Result<Vec<u8>> {
356 filters::decode_stream(s, self)
357 }
358
359 fn pages(&self) -> &[PageRec] {
361 self.pages.get_or_init(|| Arc::new(self.flatten_pages()))
362 }
363
364 pub fn seed(&self) -> DocumentSeed {
368 self.pages();
369 DocumentSeed {
370 data: Arc::clone(&self.data),
371 version: self.version,
372 xref: Arc::clone(&self.xref),
373 decryptor: self.decryptor.clone(),
374 pages: Arc::clone(self.pages.get().expect("pages() was just forced")),
375 }
376 }
377
378 pub fn from_seed(seed: DocumentSeed) -> Document {
392 Document {
393 data: seed.data,
394 version: seed.version,
395 xref: seed.xref,
396 cache: RefCell::new(FastMap::default()),
397 loading: RefCell::new(FastSet::default()),
398 objstms: RefCell::new(FastMap::default()),
399 decryptor: seed.decryptor,
400 pages: OnceCell::from(seed.pages),
401 }
402 }
403
404 pub fn fork(&self) -> Document {
407 Document::from_seed(self.seed())
408 }
409
410 pub fn page_count(&self) -> usize {
419 if let Some(pages) = self.pages.get() {
420 return pages.len();
421 }
422 if let Some(count) = self.declared_page_count() {
423 return count;
424 }
425 self.pages().len()
426 }
427
428 fn declared_page_count(&self) -> Option<usize> {
434 let root = self.xref.trailer.get("Root")?;
435 let catalog = self.resolve(root).ok()?;
436 let pages = self.resolve(catalog.as_dict()?.get("Pages")?).ok()?;
437 let count = usize::try_from(self.int_value(pages.as_dict()?, "Count")?).ok()?;
438 (count <= self.data.len()).then_some(count)
441 }
442
443 pub fn page(&self, index: usize) -> Result<Page> {
445 let pages = self.pages();
446 let rec = pages
447 .get(index)
448 .ok_or(Error::PageNotFound(index, pages.len()))?;
449 Ok(Page {
450 index,
451 media_box: rec.media_box,
452 crop_box: rec.crop_box,
453 bleed_box: rec.bleed_box,
454 trim_box: rec.trim_box,
455 art_box: rec.art_box,
456 rotate: rec.rotate,
457 resources: rec.resources.clone(),
458 dict: rec.dict.clone(),
459 obj_ref: rec.obj_ref,
460 })
461 }
462
463 pub fn metadata(&self) -> Metadata {
466 let mut meta = Metadata::default();
467 let Some(info) = self.xref.trailer.get("Info") else {
468 return meta;
469 };
470 let Ok(info) = self.resolve(info) else {
471 return meta;
472 };
473 let Some(dict) = info.as_dict() else {
474 return meta;
475 };
476 meta.title = self.meta_string(dict, "Title");
477 meta.author = self.meta_string(dict, "Author");
478 meta.subject = self.meta_string(dict, "Subject");
479 meta.keywords = self.meta_string(dict, "Keywords");
480 meta.creator = self.meta_string(dict, "Creator");
481 meta.producer = self.meta_string(dict, "Producer");
482 meta.creation_date = self.meta_string(dict, "CreationDate");
483 meta.mod_date = self.meta_string(dict, "ModDate");
484 meta
485 }
486
487 pub fn oc_state(&self) -> Option<crate::oc::OcState> {
492 block_on(crate::oc::OcState::load_with(
493 &Immediate(self),
494 &self.xref.trailer,
495 ))
496 }
497
498 fn meta_string(&self, dict: &Dict, key: &str) -> Option<String> {
500 let value = self.resolve(dict.get(key)?).ok()?;
501 Some(decode_text_string(value.as_str_bytes()?))
502 }
503
504 fn flatten_pages(&self) -> Vec<PageRec> {
509 let mut pages = Vec::new();
510 let Some(root) = self.xref.trailer.get("Root") else {
511 return pages;
512 };
513 let Ok(catalog) = self.resolve(root) else {
514 return pages;
515 };
516 let Some(tree_root) = catalog.as_dict().and_then(|d| d.get("Pages")) else {
517 return pages;
518 };
519 let mut visited: FastSet<ObjRef> = FastSet::default();
520 let mut stack: Vec<(Object, Inherited, usize)> =
521 vec![(tree_root.clone(), Inherited::default(), 0)];
522 while let Some((node, mut inherited, depth)) = stack.pop() {
523 if depth > MAX_TREE_DEPTH {
524 continue;
525 }
526 let node_ref = if let Object::Ref(r) = node {
527 Some(r)
528 } else {
529 None
530 };
531 if let Some(r) = node_ref {
532 if !visited.insert(r) {
533 continue; }
535 }
536 let Ok(resolved) = self.resolve(&node) else {
537 continue;
538 };
539 let Some(dict) = resolved.as_dict() else {
540 continue;
541 };
542 if let Some(res) = self.dict_value(dict, "Resources") {
543 inherited.resources = Some(res);
544 }
545 if let Some(mb) = self.rect_value(dict, "MediaBox") {
546 inherited.media_box = Some(mb);
547 }
548 if let Some(cb) = self.rect_value(dict, "CropBox") {
549 inherited.crop_box = Some(cb);
550 }
551 if let Some(rot) = self.int_value(dict, "Rotate") {
552 inherited.rotate = Some(rot);
553 }
554 let is_page = dict.get_name("Type").is_some_and(|n| n.0 == "Page");
555 let kids = if is_page {
556 None
557 } else {
558 self.array_value(dict, "Kids")
559 };
560 match kids {
561 Some(kids) => {
562 for kid in kids.iter().rev() {
564 stack.push((kid.clone(), inherited.clone(), depth + 1));
565 }
566 }
567 None => {
568 let bleed = self.rect_value(dict, "BleedBox");
572 let trim = self.rect_value(dict, "TrimBox");
573 let art = self.rect_value(dict, "ArtBox");
574 pages.push(make_page_rec(
575 node_ref,
576 dict.clone(),
577 &inherited,
578 bleed,
579 trim,
580 art,
581 ));
582 }
583 }
584 }
585 pages
586 }
587
588 fn dict_value(&self, dict: &Dict, key: &str) -> Option<Dict> {
590 self.resolve(dict.get(key)?).ok()?.as_dict().cloned()
591 }
592
593 fn array_value(&self, dict: &Dict, key: &str) -> Option<Vec<Object>> {
595 match self.resolve(dict.get(key)?).ok()? {
596 Object::Array(items) => Some(items),
597 _ => None,
598 }
599 }
600
601 fn int_value(&self, dict: &Dict, key: &str) -> Option<i32> {
603 let v = self.resolve(dict.get(key)?).ok()?.as_f64()?;
604 if v.is_finite() {
605 Some(v as i32)
606 } else {
607 None
608 }
609 }
610
611 fn rect_value(&self, dict: &Dict, key: &str) -> Option<Rect> {
614 let items = self.array_value(dict, key)?;
615 if items.len() != 4 {
616 return None;
617 }
618 let mut coords = [0.0f32; 4];
619 for (slot, item) in coords.iter_mut().zip(&items) {
620 let n = self.resolve(item).ok()?.as_f64()?;
621 if !n.is_finite() {
622 return None;
623 }
624 *slot = n as f32;
625 }
626 Some(Rect::new(coords[0], coords[1], coords[2], coords[3]).normalize())
627 }
628}
629
630pub fn map_pages<T, F>(doc: &Document, work: F) -> Vec<Result<T>>
649where
650 T: Send + Sync,
651 F: Fn(&Document, &Page) -> Result<T> + Send + Sync,
652{
653 let count = doc.pages().len();
654 let workers = std::thread::available_parallelism()
655 .map(std::num::NonZeroUsize::get)
656 .unwrap_or(1)
657 .min(count);
658 if workers <= 1 {
659 return (0..count)
660 .map(|i| doc.page(i).and_then(|page| work(doc, &page)))
661 .collect();
662 }
663
664 let seed = doc.seed();
665 let next = std::sync::atomic::AtomicUsize::new(0);
666 let slots: Vec<std::sync::OnceLock<Result<T>>> =
667 (0..count).map(|_| std::sync::OnceLock::new()).collect();
668 std::thread::scope(|scope| {
669 for _ in 0..workers {
670 let seed = seed.clone();
673 let next = &next;
674 let slots = &slots;
675 let work = &work;
676 scope.spawn(move || {
677 let worker = Document::from_seed(seed);
678 loop {
679 let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
680 if i >= count {
681 break;
682 }
683 let outcome = worker.page(i).and_then(|page| work(&worker, &page));
684 if slots[i].set(outcome).is_err() {
689 unreachable!("page index {i} was dispatched twice");
690 }
691 }
692 });
693 }
694 });
695 slots
696 .into_iter()
697 .map(|slot| {
698 slot.into_inner()
699 .expect("every page index below the count was dispatched")
700 })
701 .collect()
702}
703
704fn make_page_rec(
709 obj_ref: Option<ObjRef>,
710 dict: Dict,
711 inherited: &Inherited,
712 bleed_box: Option<Rect>,
713 trim_box: Option<Rect>,
714 art_box: Option<Rect>,
715) -> PageRec {
716 let page = Page::from_tree_attrs(
717 0,
718 inherited.resources.clone(),
719 inherited.media_box,
720 inherited.crop_box,
721 bleed_box,
722 trim_box,
723 art_box,
724 inherited.rotate,
725 dict,
726 obj_ref,
727 );
728 PageRec {
729 obj_ref: page.obj_ref,
730 media_box: page.media_box,
731 crop_box: page.crop_box,
732 bleed_box: page.bleed_box,
733 trim_box: page.trim_box,
734 art_box: page.art_box,
735 rotate: page.rotate,
736 resources: page.resources,
737 dict: page.dict,
738 }
739}
740
741fn type_name(o: &Object) -> &'static str {
743 match o {
744 Object::Null => "null",
745 Object::Bool(_) => "boolean",
746 Object::Int(_) => "integer",
747 Object::Real(_) => "real",
748 Object::String(_) => "string",
749 Object::Name(_) => "name",
750 Object::Array(_) => "array",
751 Object::Dict(_) => "dictionary",
752 Object::Stream(_) => "stream",
753 Object::Ref(_) => "reference",
754 }
755}
756
757impl Resolve for Document {
758 fn resolve_ref(&self, r: ObjRef) -> Option<Object> {
759 self.get(r).ok()
760 }
761}
762
763impl crate::source::ObjectSource for Document {
771 fn get(&self, r: ObjRef) -> Result<Object> {
772 Document::get(self, r)
773 }
774
775 fn stream_data(&self, s: &Stream) -> Result<Vec<u8>> {
776 Document::stream_data(self, s)
777 }
778
779 fn resolve(&self, o: &Object) -> Result<Object> {
780 Document::resolve(self, o)
781 }
782}
783
784#[derive(Debug, Clone, Default, PartialEq, Eq)]
787pub struct Metadata {
788 pub title: Option<String>,
789 pub author: Option<String>,
790 pub subject: Option<String>,
791 pub keywords: Option<String>,
792 pub creator: Option<String>,
793 pub producer: Option<String>,
794 pub creation_date: Option<String>,
795 pub mod_date: Option<String>,
796}
797
798#[derive(Clone)]
813pub struct Page {
814 pub index: usize,
816 pub media_box: Rect,
817 pub crop_box: Rect,
818 pub bleed_box: Rect,
819 pub trim_box: Rect,
820 pub art_box: Rect,
821 pub rotate: i32,
822 pub resources: Dict,
824 dict: Dict,
825 obj_ref: Option<ObjRef>,
826}
827
828impl Page {
829 pub fn from_parts(
847 index: usize,
848 media_box: Rect,
849 crop_box: Rect,
850 rotate: i32,
851 resources: Dict,
852 dict: Dict,
853 obj_ref: Option<ObjRef>,
854 ) -> Page {
855 Page {
856 index,
857 media_box,
858 crop_box,
859 bleed_box: crop_box,
860 trim_box: crop_box,
861 art_box: crop_box,
862 rotate: normalize_rotation(rotate),
863 resources,
864 dict,
865 obj_ref,
866 }
867 }
868
869 pub const US_LETTER: Rect = Rect::new(0.0, 0.0, 612.0, 792.0);
874
875 #[allow(clippy::too_many_arguments)]
890 pub fn from_tree_attrs(
891 index: usize,
892 resources: Option<Dict>,
893 media_box: Option<Rect>,
894 crop_box: Option<Rect>,
895 bleed_box: Option<Rect>,
896 trim_box: Option<Rect>,
897 art_box: Option<Rect>,
898 rotate: Option<i32>,
899 dict: Dict,
900 obj_ref: Option<ObjRef>,
901 ) -> Page {
902 let media_box = media_box
903 .filter(|r| r.width() > 0.0 && r.height() > 0.0)
904 .unwrap_or(Page::US_LETTER);
905 let crop_box = crop_box
906 .and_then(|c| c.intersect(media_box))
907 .filter(|r| r.width() > 0.0 && r.height() > 0.0)
908 .unwrap_or(media_box);
909 let clip = |declared: Option<Rect>| {
910 declared
911 .and_then(|b| b.intersect(media_box))
912 .filter(|r| r.width() > 0.0 && r.height() > 0.0)
913 .unwrap_or(crop_box)
914 };
915 Page {
916 index,
917 media_box,
918 crop_box,
919 bleed_box: clip(bleed_box),
920 trim_box: clip(trim_box),
921 art_box: clip(art_box),
922 rotate: normalize_rotation(rotate.unwrap_or(0)),
923 resources: resources.unwrap_or_default(),
924 dict,
925 obj_ref,
926 }
927 }
928
929 pub fn object_ref(&self) -> Option<ObjRef> {
933 self.obj_ref
934 }
935
936 pub fn content(&self, doc: &Document) -> Result<Vec<u8>> {
945 block_on(page_content_with(Immediate(doc), self))
946 }
947
948 pub fn size(&self) -> (f32, f32) {
957 let (w, h) = (self.crop_box.width(), self.crop_box.height());
958 let turn = self.rotate.rem_euclid(360);
959 if turn == 90 || turn == 270 {
960 (h, w)
961 } else {
962 (w, h)
963 }
964 }
965
966 pub fn dict(&self) -> &Dict {
968 &self.dict
969 }
970}
971
972pub async fn page_content_with<S: AsyncObjectSource>(src: S, page: &Page) -> Result<Vec<u8>> {
1005 let Some(contents) = page.dict.get("Contents") else {
1006 return Ok(Vec::new());
1007 };
1008 match src.resolve(contents).await? {
1009 Object::Stream(ref s) => content_stream_data_with(&src, s).await,
1010 Object::Array(items) => {
1011 let mut out = Vec::new();
1012 let mut first = true;
1013 for item in &items {
1014 let part = src.resolve(item).await?;
1015 let Some(stream) = part.as_stream() else {
1016 continue; };
1018 if !first {
1019 out.push(b'\n');
1020 }
1021 out.extend_from_slice(&content_stream_data_with(&src, stream).await?);
1022 first = false;
1023 }
1024 Ok(out)
1025 }
1026 _ => Ok(Vec::new()),
1027 }
1028}
1029
1030pub async fn decoded_stream_data_with<S: AsyncObjectSource>(src: S, s: &Stream) -> Result<Vec<u8>> {
1063 if let Some(name) = filters::trailing_filter_checked_with(&src, &s.dict).await? {
1064 if filters::is_image_codec(&name.0) {
1065 return Err(Error::UnsupportedFilter(name.0));
1066 }
1067 }
1068 src.stream_data(s).await
1069}
1070
1071pub async fn content_stream_data_with<S: AsyncObjectSource>(src: S, s: &Stream) -> Result<Vec<u8>> {
1081 decoded_stream_data_with(src, s).await
1082}
1083
1084#[cfg(test)]
1085mod tests {
1086 use super::*;
1087 use crate::object::Name;
1088 use crate::parser::{NoResolve, Parser};
1089 use crate::xref::XrefEntry;
1090 use pdfboss_testkit::{multi_page_doc, objstm_doc, objstm_payload, simple_doc, PdfBuilder};
1091
1092 #[test]
1096 fn async_page_content_matches_the_sync_accessor() {
1097 let doc = Document::load(multi_page_doc(&["one", "two", "three"])).expect("load");
1098 let mut saw_content = false;
1099 for index in 0..3 {
1100 let page = doc.page(index).expect("page");
1101 let direct = page.content(&doc).expect("sync content");
1102 let awaited =
1103 block_on(page_content_with(Immediate(&doc), &page)).expect("async content");
1104 assert_eq!(direct, awaited, "page {index}");
1105 saw_content |= !direct.is_empty();
1106 }
1107 assert!(
1109 saw_content,
1110 "fixture must give at least one page real content"
1111 );
1112 }
1113
1114 #[test]
1122 fn image_codec_page_contents_are_refused_not_returned() {
1123 for codec in ["JPXDecode", "DCTDecode"] {
1124 let mut b = PdfBuilder::new();
1125 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1126 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1127 b.object(
1128 3,
1129 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents 4 0 R >>",
1130 );
1131 b.stream(
1132 4,
1133 &format!("/Filter /{codec}"),
1134 b"1 0 0 rg 0 0 100 100 re f",
1135 );
1136 let doc = Document::load(b.build(1)).expect("load");
1137 let page = doc.page(0).expect("page");
1138 match page.content(&doc) {
1139 Err(Error::UnsupportedFilter(n)) => assert_eq!(n, codec),
1140 other => panic!("{codec} content must be refused, got {other:?}"),
1141 }
1142 }
1143 }
1144
1145 #[test]
1152 fn an_unreadable_filter_refuses_the_content_not_returns_it_raw() {
1153 let mut b = PdfBuilder::new();
1154 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1155 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1156 b.object(
1157 3,
1158 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents 4 0 R >>",
1159 );
1160 b.stream(4, "/Filter 6 0 R", b"1 0 0 rg 0 0 100 100 re f");
1161 b.object(6, "7 0 R");
1162 b.object(7, "6 0 R");
1163 let doc = Document::load(b.build(1)).expect("load");
1164 let page = doc.page(0).expect("page");
1165 assert!(
1166 page.content(&doc).is_err(),
1167 "an unreadable /Filter must refuse, not pass stored bytes"
1168 );
1169 }
1170
1171 #[test]
1176 fn a_shared_helper_is_reachable_from_a_caller_that_owns_its_source() {
1177 let doc = Document::load(simple_doc("Hello")).expect("load");
1178 let page = doc.page(0).expect("page");
1179
1180 let owner = Immediate(&doc);
1181 let through_reference = block_on(page_content_with(&owner, &page)).expect("async content");
1182
1183 assert_eq!(through_reference, page.content(&doc).expect("sync content"));
1184 assert!(!through_reference.is_empty());
1185 }
1186
1187 #[test]
1191 fn the_seed_is_shareable_across_threads() {
1192 fn assert_send_sync<T: Send + Sync>() {}
1193 assert_send_sync::<DocumentSeed>();
1194 }
1195
1196 #[test]
1200 fn a_fork_reads_exactly_what_its_parent_reads() {
1201 let doc = Document::load(pdfboss_testkit::encrypted_rc4_doc("forked secret")).unwrap();
1202 let fork = doc.fork();
1203 assert_eq!(fork.page_count(), doc.page_count());
1204 let (a, b) = (doc.page(0).unwrap(), fork.page(0).unwrap());
1205 assert_eq!(a.media_box, b.media_box);
1206 assert_eq!(
1207 a.content(&doc).expect("parent content"),
1208 b.content(&fork).expect("fork content"),
1209 "decrypted content agrees"
1210 );
1211 }
1212
1213 #[test]
1217 fn map_pages_preserves_page_order() {
1218 let doc = Document::load(multi_page_doc(&["one", "two", "three"])).unwrap();
1219 let contents = map_pages(&doc, |doc, page| page.content(doc));
1220 assert_eq!(contents.len(), 3);
1221 let texts: Vec<String> = contents
1222 .into_iter()
1223 .map(|c| String::from_utf8_lossy(&c.unwrap()).into_owned())
1224 .collect();
1225 assert!(texts[0].contains("(one)"), "{}", texts[0]);
1226 assert!(texts[1].contains("(two)"), "{}", texts[1]);
1227 assert!(texts[2].contains("(three)"), "{}", texts[2]);
1228 }
1229
1230 fn replace_once(data: &[u8], from: &[u8], to: &[u8]) -> Vec<u8> {
1233 let pos = memchr::memmem::find(data, from).expect("pattern present in fixture");
1234 let mut out = Vec::with_capacity(data.len() - from.len() + to.len());
1235 out.extend_from_slice(&data[..pos]);
1236 out.extend_from_slice(to);
1237 out.extend_from_slice(&data[pos + from.len()..]);
1238 out
1239 }
1240
1241 fn contains(haystack: &[u8], needle: &[u8]) -> bool {
1242 memchr::memmem::find(haystack, needle).is_some()
1243 }
1244
1245 const FONT: &str = "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>";
1246
1247 #[test]
1248 fn loads_simple_doc() {
1249 let doc = Document::load(simple_doc("Greetings, cosmos!")).unwrap();
1250 assert_eq!(doc.version(), (1, 7));
1251 assert_eq!(doc.page_count(), 1);
1252 let page = doc.page(0).unwrap();
1253 assert_eq!(page.index, 0);
1254 assert_eq!(page.media_box, Rect::new(0.0, 0.0, 612.0, 792.0));
1255 assert_eq!(page.crop_box, page.media_box);
1256 assert_eq!(page.rotate, 0);
1257 assert_eq!(page.size(), (612.0, 792.0));
1258 assert!(page.resources.get("Font").is_some());
1259 let content = page.content(&doc).unwrap();
1260 assert!(contains(&content, b"Greetings, cosmos!"));
1261 }
1262
1263 #[test]
1264 fn multi_page_ordering() {
1265 let doc = Document::load(multi_page_doc(&["alpha", "beta", "gamma"])).unwrap();
1266 assert_eq!(doc.page_count(), 3);
1267 for (i, text) in ["alpha", "beta", "gamma"].iter().enumerate() {
1268 let content = doc.page(i).unwrap().content(&doc).unwrap();
1269 assert!(
1270 contains(&content, text.as_bytes()),
1271 "page {i} should show {text}"
1272 );
1273 }
1274 }
1275
1276 #[test]
1277 fn page_index_out_of_bounds() {
1278 let doc = Document::load(simple_doc("x")).unwrap();
1279 assert!(matches!(doc.page(5), Err(Error::PageNotFound(5, 1))));
1280 }
1281
1282 #[test]
1283 fn open_reads_from_disk() {
1284 let dir = std::env::temp_dir();
1285 let path = dir.join(format!("pdfboss-doc-test-{}.pdf", std::process::id()));
1286 std::fs::write(&path, simple_doc("from disk")).unwrap();
1287 let doc = Document::open(&path).unwrap();
1288 std::fs::remove_file(&path).ok();
1289 assert_eq!(doc.page_count(), 1);
1290 let content = doc.page(0).unwrap().content(&doc).unwrap();
1291 assert!(contains(&content, b"from disk"));
1292 assert!(matches!(
1293 Document::open(dir.join("pdfboss-doc-test-missing.pdf")),
1294 Err(Error::Io(_))
1295 ));
1296 }
1297
1298 #[test]
1299 fn encrypt_in_trailer_is_rejected() {
1300 let data = replace_once(
1301 &simple_doc("secret"),
1302 b"trailer\n<< /Size",
1303 b"trailer\n<< /Encrypt 9 0 R /Size",
1304 );
1305 assert!(matches!(Document::load(data), Err(Error::Encrypted)));
1306 }
1307
1308 #[test]
1309 fn metadata_utf16be_round_trip() {
1310 let mut b = PdfBuilder::new();
1311 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1312 b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
1313 b.object(6, "<< /Title <FEFF00480151> /Author (plain author) >>");
1315 let data = replace_once(&b.build(1), b"<< /Size", b"<< /Info 6 0 R /Size");
1316 let doc = Document::load(data).unwrap();
1317 let meta = doc.metadata();
1318 assert_eq!(meta.title.as_deref(), Some("H\u{151}"));
1319 assert_eq!(meta.author.as_deref(), Some("plain author"));
1320 assert_eq!(meta.subject, None);
1321 assert_eq!(meta.keywords, None);
1322 assert_eq!(meta.creation_date, None);
1323 }
1324
1325 #[test]
1326 fn metadata_without_info_is_all_none() {
1327 let doc = Document::load(simple_doc("x")).unwrap();
1328 assert_eq!(doc.metadata(), Metadata::default());
1329 }
1330
1331 #[test]
1332 fn missing_object_resolves_to_null() {
1333 let doc = Document::load(simple_doc("x")).unwrap();
1334 let missing = Object::Ref(ObjRef { num: 99, gen: 0 });
1335 assert_eq!(doc.resolve(&missing).unwrap(), Object::Null);
1336 assert!(matches!(
1337 doc.get(ObjRef { num: 99, gen: 0 }),
1338 Err(Error::ObjectNotFound(99, 0))
1339 ));
1340 }
1341
1342 #[test]
1343 fn self_reference_is_circular() {
1344 let mut b = PdfBuilder::new();
1345 b.object(1, "<< /Type /Catalog >>");
1346 b.object(6, "6 0 R");
1347 let doc = Document::load(b.build(1)).unwrap();
1348 let loops = Object::Ref(ObjRef { num: 6, gen: 0 });
1349 assert!(matches!(
1350 doc.resolve(&loops),
1351 Err(Error::CircularReference(6))
1352 ));
1353 }
1354
1355 #[test]
1356 fn generation_mismatch_is_tolerated() {
1357 let doc = Document::load(simple_doc("x")).unwrap();
1358 let catalog = doc.get(ObjRef { num: 1, gen: 7 }).unwrap();
1359 let dict = catalog.as_dict().unwrap();
1360 assert_eq!(dict.get_name("Type").map(|n| n.0.as_str()), Some("Catalog"));
1361 }
1362
1363 #[test]
1364 fn objects_in_object_streams_are_fetched() {
1365 let mut b = PdfBuilder::new();
1366 let (dict, payload) =
1367 objstm_payload(&[(1, "<< /Type /Catalog /Pages 2 0 R >>"), (5, FONT)]);
1368 b.stream(6, &dict, &payload);
1369 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1370 b.object(
1371 3,
1372 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1373 /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
1374 );
1375 b.stream(4, "", b"BT /F1 12 Tf (compressed hello) Tj ET");
1376 let doc = Document::load(b.build_xref_stream(1)).unwrap();
1377 assert_eq!(doc.page_count(), 1);
1378 let page = doc.page(0).unwrap();
1379 assert!(contains(&page.content(&doc).unwrap(), b"compressed hello"));
1380 let font = doc.get(ObjRef { num: 5, gen: 0 }).unwrap();
1381 assert_eq!(
1382 font.as_dict()
1383 .and_then(|d| d.get_name("BaseFont"))
1384 .map(|n| n.0.as_str()),
1385 Some("Helvetica")
1386 );
1387 }
1388
1389 #[test]
1390 fn contents_array_is_joined_with_newlines() {
1391 let mut b = PdfBuilder::new();
1392 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1393 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1394 b.object(
1395 3,
1396 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1397 /Contents [4 0 R null 5 0 R] >>",
1398 );
1399 b.stream(4, "", b"q");
1400 b.stream(5, "", b"Q");
1401 let doc = Document::load(b.build(1)).unwrap();
1402 let content = doc.page(0).unwrap().content(&doc).unwrap();
1403 assert_eq!(content, b"q\nQ", "streams joined by \\n, null skipped");
1404 }
1405
1406 #[test]
1407 fn inheritance_from_pages_node_and_rotate_swap() {
1408 let mut b = PdfBuilder::new();
1409 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1410 b.object(
1411 2,
1412 "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 \
1413 /Resources << /Font << /F1 5 0 R >> >> /MediaBox [0 0 400 600] >>",
1414 );
1415 b.object(3, "<< /Type /Page /Parent 2 0 R >>");
1416 b.object(4, "<< /Type /Page /Parent 2 0 R /Rotate 270 >>");
1417 b.object(5, FONT);
1418 let doc = Document::load(b.build(1)).unwrap();
1419 assert_eq!(doc.page_count(), 2);
1420
1421 let first = doc.page(0).unwrap();
1422 assert_eq!(first.media_box, Rect::new(0.0, 0.0, 400.0, 600.0));
1423 assert_eq!(first.crop_box, first.media_box);
1424 assert!(first.resources.get("Font").is_some(), "inherited resources");
1425 assert_eq!(first.rotate, 0);
1426 assert!(
1427 first.content(&doc).unwrap().is_empty(),
1428 "no /Contents means empty content"
1429 );
1430 assert_eq!(first.size(), (400.0, 600.0));
1431
1432 let second = doc.page(1).unwrap();
1433 assert_eq!(second.rotate, 270);
1434 assert_eq!(second.size(), (600.0, 400.0), "rotate 270 swaps w/h");
1435 }
1436
1437 #[test]
1438 fn crop_box_intersected_and_rotate_normalized() {
1439 let mut b = PdfBuilder::new();
1440 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1441 b.object(2, "<< /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 >>");
1442 b.object(
1443 3,
1444 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] \
1445 /CropBox [100 100 400 400] /Rotate 450 >>",
1446 );
1447 b.object(4, "<< /Type /Page /Parent 2 0 R /Rotate -90 >>");
1448 b.object(
1449 5,
1450 "<< /Type /Page /Parent 2 0 R /Rotate 45 /MediaBox [0 0 0 0] >>",
1451 );
1452 let doc = Document::load(b.build(1)).unwrap();
1453
1454 let clipped = doc.page(0).unwrap();
1455 assert_eq!(clipped.crop_box, Rect::new(100.0, 100.0, 200.0, 200.0));
1456 assert_eq!(clipped.rotate, 90, "450 normalizes to 90");
1457 assert_eq!(clipped.size(), (100.0, 100.0));
1458
1459 assert_eq!(doc.page(1).unwrap().rotate, 270, "-90 normalizes to 270");
1460 let odd = doc.page(2).unwrap();
1461 assert_eq!(odd.rotate, 0, "non-multiple of 90 falls back to 0");
1462 assert_eq!(
1463 odd.media_box,
1464 Page::US_LETTER,
1465 "degenerate media box defaults"
1466 );
1467 }
1468
1469 #[test]
1470 fn page_boxes_declared_and_defaulted() {
1471 let mut b = PdfBuilder::new();
1472 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1473 b.object(2, "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>");
1474 b.object(
1475 3,
1476 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 600 800] \
1477 /CropBox [50 50 550 750] /BleedBox [40 40 560 760] \
1478 /TrimBox [5 0 R 6 0 R 7 0 R 8 0 R] >>",
1479 );
1480 b.object(4, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 600 800] >>");
1481 b.object(5, "60");
1482 b.object(6, "70");
1483 b.object(7, "540");
1484 b.object(8, "730");
1485 let doc = Document::load(b.build(1)).unwrap();
1486
1487 let page = doc.page(0).unwrap();
1488 assert_eq!(page.bleed_box, Rect::new(40.0, 40.0, 560.0, 760.0));
1489 assert_eq!(
1490 page.trim_box,
1491 Rect::new(60.0, 70.0, 540.0, 730.0),
1492 "indirect array elements resolve"
1493 );
1494 assert_eq!(
1495 page.art_box, page.crop_box,
1496 "undeclared box is the crop box"
1497 );
1498
1499 let bare = doc.page(1).unwrap();
1500 assert_eq!(bare.bleed_box, bare.crop_box);
1501 assert_eq!(bare.trim_box, bare.crop_box);
1502 assert_eq!(bare.art_box, bare.crop_box);
1503 }
1504
1505 #[test]
1506 fn page_boxes_clip_to_media_and_fall_back() {
1507 let mut b = PdfBuilder::new();
1508 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1509 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1510 b.object(
1511 3,
1512 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] \
1513 /CropBox [10 10 190 190] /TrimBox [100 100 400 400] \
1514 /BleedBox [0 0 0 0] /ArtBox [300 300 400 400] >>",
1515 );
1516 let doc = Document::load(b.build(1)).unwrap();
1517
1518 let page = doc.page(0).unwrap();
1519 assert_eq!(
1520 page.trim_box,
1521 Rect::new(100.0, 100.0, 200.0, 200.0),
1522 "a trim box past the media box clips to it"
1523 );
1524 assert_eq!(
1525 page.bleed_box, page.crop_box,
1526 "a degenerate bleed box falls back to the crop box"
1527 );
1528 assert_eq!(
1529 page.art_box, page.crop_box,
1530 "an art box disjoint from the media box falls back to the crop box"
1531 );
1532 }
1533
1534 #[test]
1538 fn page_boxes_are_not_inherited() {
1539 let mut b = PdfBuilder::new();
1540 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1541 b.object(
1542 2,
1543 "<< /Type /Pages /Kids [3 0 R] /Count 1 \
1544 /TrimBox [10 10 90 90] /BleedBox [5 5 95 95] /ArtBox [20 20 80 80] >>",
1545 );
1546 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
1547 let doc = Document::load(b.build(1)).unwrap();
1548
1549 let page = doc.page(0).unwrap();
1550 assert_eq!(page.trim_box, page.crop_box);
1551 assert_eq!(page.bleed_box, page.crop_box);
1552 assert_eq!(page.art_box, page.crop_box);
1553 }
1554
1555 #[test]
1556 fn kids_cycle_truncates_without_hanging() {
1557 let mut b = PdfBuilder::new();
1558 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1559 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1561 b.object(3, "<< /Type /Pages /Kids [4 0 R 2 0 R] /Count 1 >>");
1562 b.object(
1563 4,
1564 "<< /Type /Page /Parent 3 0 R /MediaBox [0 0 100 100] /Contents 5 0 R >>",
1565 );
1566 b.stream(5, "", b"0 0 50 50 re f");
1567 let doc = Document::load(b.build(1)).unwrap();
1568 assert_eq!(doc.page_count(), 1, "cycle back-edge yields no extra pages");
1569 assert!(contains(
1570 &doc.page(0).unwrap().content(&doc).unwrap(),
1571 b"re f"
1572 ));
1573 }
1574
1575 #[test]
1576 fn tree_depth_is_capped() {
1577 let mut b = PdfBuilder::new();
1578 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1579 let last = 302u32;
1581 for num in 2..last {
1582 b.object(
1583 num,
1584 &format!("<< /Type /Pages /Kids [{} 0 R] /Count 1 >>", num + 1),
1585 );
1586 }
1587 b.object(last, "<< /Type /Page >>");
1588 let doc = Document::load(b.build(1)).unwrap();
1589 assert_eq!(doc.page_count(), 1, "declared /Count is reported cheaply");
1594 assert!(
1595 matches!(doc.page(0), Err(Error::PageNotFound(0, 0))),
1596 "leaf beyond the depth cap cannot be materialized"
1597 );
1598 }
1599
1600 #[test]
1601 fn page_count_reports_declared_count_cheaply() {
1602 let mut b = PdfBuilder::new();
1606 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1607 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 5 >>");
1608 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
1609 let doc = Document::load(b.build(1)).unwrap();
1610 assert_eq!(doc.page_count(), 5, "declared /Count reported verbatim");
1611 assert!(doc.page(0).is_ok(), "the one real page materializes");
1612 assert!(
1613 matches!(doc.page(1), Err(Error::PageNotFound(1, 1))),
1614 "access past the real pages fails with the true length"
1615 );
1616 }
1617
1618 #[test]
1619 fn page_count_falls_back_to_walk_when_count_absent() {
1620 let mut b = PdfBuilder::new();
1621 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1622 b.object(2, "<< /Type /Pages /Kids [3 0 R] >>"); b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
1624 let doc = Document::load(b.build(1)).unwrap();
1625 assert_eq!(
1626 doc.page_count(),
1627 1,
1628 "missing /Count is recovered by walking"
1629 );
1630 }
1631
1632 #[test]
1633 fn page_count_ignores_corrupt_oversized_count() {
1634 let mut b = PdfBuilder::new();
1637 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1638 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 999999999 >>");
1639 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
1640 let doc = Document::load(b.build(1)).unwrap();
1641 assert_eq!(doc.page_count(), 1, "implausible /Count is rejected");
1642 }
1643
1644 #[test]
1645 fn version_scan_and_default() {
1646 let mut b = PdfBuilder::new().version(2, 0);
1647 b.object(1, "<< /Type /Catalog >>");
1648 assert_eq!(Document::load(b.build(1)).unwrap().version(), (2, 0));
1649 let data = replace_once(&simple_doc("v"), b"%PDF-", b"%QQQ-");
1651 assert_eq!(Document::load(data).unwrap().version(), (1, 4));
1652 }
1653
1654 #[test]
1655 fn deeply_nested_root_object_does_not_overflow_the_stack() {
1656 let mut data = b"%PDF-1.7\n1 0 obj\n".to_vec();
1661 data.extend(std::iter::repeat_n(b'[', 50_000));
1662 data.extend(std::iter::repeat_n(b']', 50_000));
1663 data.extend_from_slice(b"\nendobj\ntrailer\n<</Root 1 0 R>>\n%%EOF\n");
1664 let outcome = std::thread::Builder::new()
1665 .stack_size(1024 * 1024)
1666 .spawn(move || Document::load(data).map(|doc| doc.page_count()))
1667 .expect("spawn test thread")
1668 .join()
1669 .expect("Document::load must not overflow the stack");
1670 assert!(matches!(outcome, Ok(0) | Err(_)));
1673 }
1674
1675 #[test]
1676 fn bytes_and_xref_accessors() {
1677 let data = simple_doc("accessors");
1678 let doc = Document::load(data.clone()).unwrap();
1679 assert_eq!(doc.bytes(), &data[..]);
1680 assert!(!doc.xref().is_empty());
1681 assert!(doc.xref().trailer.get("Root").is_some());
1682 }
1683
1684 #[test]
1685 fn object_at_spanned_reparses_identically() {
1686 let data = simple_doc("spanned");
1687 let doc = Document::load(data).unwrap();
1688 for (num, entry) in doc.xref().iter() {
1689 let XrefEntry::InFile { offset, gen } = entry else {
1690 continue;
1691 };
1692 let (r, object, span) = doc.object_at_spanned(offset as usize).unwrap();
1693 assert_eq!(r.num, num);
1694 assert_eq!(r.gen, gen);
1695 assert_eq!(span.start, offset);
1696 assert!(span.end as usize <= doc.bytes().len());
1697 let slice = &doc.bytes()[span.start as usize..span.end as usize];
1699 let (r2, object2) = Parser::new(slice).parse_indirect(&NoResolve).unwrap();
1700 assert_eq!(r2, r);
1701 assert_eq!(object2, object);
1702 }
1703 }
1704
1705 #[test]
1706 fn page_object_ref_points_at_a_page_dict() {
1707 let doc = Document::load(multi_page_doc(&["one", "two"])).unwrap();
1708 for index in 0..doc.page_count() {
1709 let page = doc.page(index).unwrap();
1710 let r = page.object_ref().expect("builder pages are indirect");
1711 let resolved = doc.get(r).unwrap();
1712 assert_eq!(
1713 resolved
1714 .as_dict()
1715 .unwrap()
1716 .get_name("Type")
1717 .map(|n| n.0.as_str()),
1718 Some("Page")
1719 );
1720 }
1721 }
1722
1723 const RC4_FIXTURE_KEY_LEN: usize = 16; const RC4_FIXTURE_P: i32 = -44;
1733 const RC4_FIXTURE_ID0: &[u8] = b"0123456789abcdef";
1734 const RC4_FIXTURE_PAD: [u8; 32] = [
1735 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01,
1736 0x08, 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53,
1737 0x69, 0x7A,
1738 ];
1739
1740 #[rustfmt::skip]
1741 const RC4_FIXTURE_MD5_S: [u32; 64] = [
1742 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
1743 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
1744 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
1745 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
1746 ];
1747 #[rustfmt::skip]
1748 const RC4_FIXTURE_MD5_K: [u32; 64] = [
1749 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
1750 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
1751 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
1752 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
1753 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
1754 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
1755 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
1756 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,
1757 ];
1758
1759 fn rc4_fixture_md5(input: &[u8]) -> [u8; 16] {
1760 let (mut a0, mut b0, mut c0, mut d0) = (
1761 0x6745_2301u32,
1762 0xefcd_ab89u32,
1763 0x98ba_dcfeu32,
1764 0x1032_5476u32,
1765 );
1766 let mut msg = input.to_vec();
1767 let bitlen = (input.len() as u64).wrapping_mul(8);
1768 msg.push(0x80);
1769 while msg.len() % 64 != 56 {
1770 msg.push(0);
1771 }
1772 msg.extend_from_slice(&bitlen.to_le_bytes());
1773 for chunk in msg.as_chunks::<64>().0 {
1774 let mut m = [0u32; 16];
1775 for (word, bytes) in m.iter_mut().zip(chunk.as_chunks::<4>().0) {
1776 *word = u32::from_le_bytes(*bytes);
1777 }
1778 let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0);
1779 for i in 0..64 {
1780 let (f, g) = match i {
1781 0..=15 => ((b & c) | (!b & d), i),
1782 16..=31 => ((d & b) | (!d & c), (5 * i + 1) % 16),
1783 32..=47 => (b ^ c ^ d, (3 * i + 5) % 16),
1784 _ => (c ^ (b | !d), (7 * i) % 16),
1785 };
1786 let f = f
1787 .wrapping_add(a)
1788 .wrapping_add(RC4_FIXTURE_MD5_K[i])
1789 .wrapping_add(m[g]);
1790 a = d;
1791 d = c;
1792 c = b;
1793 b = b.wrapping_add(f.rotate_left(RC4_FIXTURE_MD5_S[i]));
1794 }
1795 a0 = a0.wrapping_add(a);
1796 b0 = b0.wrapping_add(b);
1797 c0 = c0.wrapping_add(c);
1798 d0 = d0.wrapping_add(d);
1799 }
1800 let mut out = [0u8; 16];
1801 out[0..4].copy_from_slice(&a0.to_le_bytes());
1802 out[4..8].copy_from_slice(&b0.to_le_bytes());
1803 out[8..12].copy_from_slice(&c0.to_le_bytes());
1804 out[12..16].copy_from_slice(&d0.to_le_bytes());
1805 out
1806 }
1807
1808 fn rc4_fixture_rc4(key: &[u8], data: &[u8]) -> Vec<u8> {
1809 let mut s: [u8; 256] = core::array::from_fn(|i| i as u8);
1810 let mut j = 0u8;
1811 for i in 0..256 {
1812 j = j.wrapping_add(s[i]).wrapping_add(key[i % key.len()]);
1813 s.swap(i, j as usize);
1814 }
1815 let mut out = Vec::with_capacity(data.len());
1816 let (mut i, mut j) = (0u8, 0u8);
1817 for &byte in data {
1818 i = i.wrapping_add(1);
1819 j = j.wrapping_add(s[i as usize]);
1820 s.swap(i as usize, j as usize);
1821 let k = s[s[i as usize].wrapping_add(s[j as usize]) as usize];
1822 out.push(byte ^ k);
1823 }
1824 out
1825 }
1826
1827 fn rc4_fixture_owner_entry() -> Vec<u8> {
1829 let mut d = rc4_fixture_md5(&RC4_FIXTURE_PAD);
1830 for _ in 0..50 {
1831 d = rc4_fixture_md5(&d[..RC4_FIXTURE_KEY_LEN]);
1832 }
1833 let rc4key = d[..RC4_FIXTURE_KEY_LEN].to_vec();
1834 let mut o = rc4_fixture_rc4(&rc4key, &RC4_FIXTURE_PAD);
1835 for i in 1u8..=19 {
1836 let k: Vec<u8> = rc4key.iter().map(|b| b ^ i).collect();
1837 o = rc4_fixture_rc4(&k, &o);
1838 }
1839 o
1840 }
1841
1842 fn rc4_fixture_file_key(o: &[u8]) -> Vec<u8> {
1844 let mut input = Vec::new();
1845 input.extend_from_slice(&RC4_FIXTURE_PAD);
1846 input.extend_from_slice(o);
1847 input.extend_from_slice(&(RC4_FIXTURE_P as u32).to_le_bytes());
1848 input.extend_from_slice(RC4_FIXTURE_ID0);
1849 let mut d = rc4_fixture_md5(&input);
1850 for _ in 0..50 {
1851 d = rc4_fixture_md5(&d[..RC4_FIXTURE_KEY_LEN]);
1852 }
1853 d[..RC4_FIXTURE_KEY_LEN].to_vec()
1854 }
1855
1856 fn rc4_fixture_user_entry(key: &[u8]) -> Vec<u8> {
1858 let mut input = Vec::new();
1859 input.extend_from_slice(&RC4_FIXTURE_PAD);
1860 input.extend_from_slice(RC4_FIXTURE_ID0);
1861 let mut x = rc4_fixture_md5(&input).to_vec();
1862 x = rc4_fixture_rc4(key, &x);
1863 for i in 1u8..=19 {
1864 let k: Vec<u8> = key.iter().map(|b| b ^ i).collect();
1865 x = rc4_fixture_rc4(&k, &x);
1866 }
1867 x.resize(32, 0); x
1869 }
1870
1871 fn rc4_fixture_obj_key(key: &[u8], num: u32, gen: u16) -> Vec<u8> {
1872 let mut input = key.to_vec();
1873 input.extend_from_slice(&num.to_le_bytes()[..3]);
1874 input.extend_from_slice(&gen.to_le_bytes()[..2]);
1875 rc4_fixture_md5(&input)[..(key.len() + 5).min(16)].to_vec()
1876 }
1877
1878 fn rc4_fixture_hexstr(b: &[u8]) -> String {
1879 let mut s = String::from("<");
1880 for x in b {
1881 s.push_str(&format!("{x:02x}"));
1882 }
1883 s.push('>');
1884 s
1885 }
1886
1887 fn rc4_encrypted_fixture() -> Vec<u8> {
1891 let o = rc4_fixture_owner_entry();
1892 let key = rc4_fixture_file_key(&o);
1893 let u = rc4_fixture_user_entry(&key);
1894 let msg = rc4_fixture_rc4(&rc4_fixture_obj_key(&key, 3, 0), b"Top secret message");
1895
1896 let mut b = PdfBuilder::new().version(1, 4);
1897 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1898 b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
1899 b.object(3, &format!("<< /Msg {} >>", rc4_fixture_hexstr(&msg)));
1900 b.object(
1901 9,
1902 &format!(
1903 "<< /Filter /Standard /V 2 /R 3 /Length 128 /P {} /O {} /U {} >>",
1904 RC4_FIXTURE_P,
1905 rc4_fixture_hexstr(&o),
1906 rc4_fixture_hexstr(&u)
1907 ),
1908 );
1909 let trailer = format!(
1910 "/Encrypt 9 0 R /ID [{}{}]",
1911 rc4_fixture_hexstr(RC4_FIXTURE_ID0),
1912 rc4_fixture_hexstr(RC4_FIXTURE_ID0)
1913 );
1914 b.trailer_extra(&trailer).build(1)
1915 }
1916
1917 #[test]
1918 fn encrypted_generation_mismatch_still_decrypts() {
1919 let doc = Document::load(rc4_encrypted_fixture()).expect("empty password opens the file");
1929 let obj3 = doc.get(ObjRef { num: 3, gen: 7 }).unwrap();
1930 let msg = obj3
1931 .as_dict()
1932 .unwrap()
1933 .get("Msg")
1934 .unwrap()
1935 .as_str_bytes()
1936 .unwrap();
1937 assert_eq!(
1938 msg, b"Top secret message",
1939 "decrypted using the file's real gen (0), not the mismatched request (7)"
1940 );
1941 }
1942
1943 #[test]
1944 fn objstm_doc_fixture_loads_and_resolves_members() {
1945 let data = objstm_doc(&[(7, "<< /Marker (inside) >>")]);
1946 let doc = Document::load(data).unwrap();
1947 assert_eq!(doc.page_count(), 1);
1948 let member = doc.get(ObjRef { num: 7, gen: 0 }).unwrap();
1949 let text = member.as_dict().unwrap().get("Marker").unwrap();
1950 assert_eq!(text.as_str_bytes(), Some(&b"inside"[..]));
1951 assert!(matches!(
1953 doc.xref().get(7),
1954 Some(XrefEntry::InStream { stream_num: 4, .. })
1955 ));
1956 }
1957
1958 #[test]
1961 fn page_from_parts_exposes_its_accessors() {
1962 let mut dict = Dict::default();
1963 dict.insert(Name("Type".into()), Object::Name(Name("Page".into())));
1964 let media = Rect::new(0.0, 0.0, 200.0, 400.0);
1965 let obj_ref = ObjRef { num: 7, gen: 0 };
1966
1967 let page = Page::from_parts(
1968 3,
1969 media,
1970 media,
1971 90,
1972 Dict::default(),
1973 dict.clone(),
1974 Some(obj_ref),
1975 );
1976
1977 assert_eq!(page.index, 3);
1978 assert_eq!(page.object_ref(), Some(obj_ref));
1979 assert_eq!(page.dict(), &dict);
1980 assert_eq!(page.size(), (400.0, 200.0));
1982 assert_eq!(page.bleed_box, page.crop_box);
1984 assert_eq!(page.trim_box, page.crop_box);
1985 assert_eq!(page.art_box, page.crop_box);
1986 }
1987
1988 #[test]
1992 fn page_from_parts_normalizes_rotation() {
1993 let media = Rect::new(0.0, 0.0, 200.0, 400.0);
1994 let build = |rotate: i32| {
1995 Page::from_parts(
1996 0,
1997 media,
1998 media,
1999 rotate,
2000 Dict::default(),
2001 Dict::default(),
2002 None,
2003 )
2004 };
2005
2006 for (given, canonical) in [(-90, 270), (450, 90), (540, 180), (720, 0), (-360, 0)] {
2007 let page = build(given);
2008 assert_eq!(
2009 page.rotate, canonical,
2010 "from_parts must normalize /Rotate {given} to {canonical}"
2011 );
2012 assert_eq!(
2013 page.size(),
2014 build(canonical).size(),
2015 "/Rotate {given} must report the same size as {canonical}"
2016 );
2017 }
2018 }
2019
2020 #[test]
2025 fn page_size_handles_unnormalized_rotation() {
2026 let media = Rect::new(0.0, 0.0, 200.0, 400.0);
2027 let mut page = Page::from_parts(0, media, media, 0, Dict::default(), Dict::default(), None);
2028
2029 for rotate in [90, 270, -90, -270, 450, 630] {
2030 page.rotate = rotate;
2031 assert_eq!(
2032 page.size(),
2033 (400.0, 200.0),
2034 "/Rotate {rotate} is a quarter turn and must swap the size"
2035 );
2036 }
2037 for rotate in [0, 180, -180, 360, 540, 720] {
2038 page.rotate = rotate;
2039 assert_eq!(
2040 page.size(),
2041 (200.0, 400.0),
2042 "/Rotate {rotate} is a half turn and must leave the size alone"
2043 );
2044 }
2045 }
2046}