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 rotate: i32,
83 resources: Dict,
84 dict: Dict,
85}
86
87#[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
96fn 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
114fn 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
128fn 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 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 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 self.cache.borrow_mut().clear();
204 Ok(())
205 }
206 None => Err(Error::Encrypted),
207 }
208 }
209
210 pub fn open(path: impl AsRef<Path>) -> Result<Document> {
212 Document::load(std::fs::read(path)?)
213 }
214
215 pub fn version(&self) -> (u8, u8) {
217 self.version
218 }
219
220 pub fn bytes(&self) -> &[u8] {
222 &self.data
223 }
224
225 pub fn xref(&self) -> &Xref {
227 &self.xref
228 }
229
230 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 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 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 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 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 fn load_from_object_stream(&self, stream_num: u32, index: u32) -> Result<Object> {
315 self.objstm_handle(stream_num)?.object(index)
316 }
317
318 pub fn resolve(&self, o: &Object) -> Result<Object> {
329 crate::source::resolve_sync_with(self, o)
330 }
331
332 pub fn stream_data(&self, s: &Stream) -> Result<Vec<u8>> {
335 filters::decode_stream(s, self)
336 }
337
338 fn pages(&self) -> &[PageRec] {
340 self.pages.get_or_init(|| Arc::new(self.flatten_pages()))
341 }
342
343 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 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 pub fn fork(&self) -> Document {
386 Document::from_seed(self.seed())
387 }
388
389 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 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 (count <= self.data.len()).then_some(count)
420 }
421
422 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 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 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 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; }
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 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 fn dict_value(&self, dict: &Dict, key: &str) -> Option<Dict> {
540 self.resolve(dict.get(key)?).ok()?.as_dict().cloned()
541 }
542
543 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 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 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
580pub 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 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 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
654fn 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
678fn 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
700impl 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#[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#[derive(Clone)]
745pub struct Page {
746 pub index: usize,
748 pub media_box: Rect,
749 pub crop_box: Rect,
750 pub rotate: i32,
751 pub resources: Dict,
753 dict: Dict,
754 obj_ref: Option<ObjRef>,
755}
756
757impl Page {
758 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 pub const US_LETTER: Rect = Rect::new(0.0, 0.0, 612.0, 792.0);
796
797 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 pub fn object_ref(&self) -> Option<ObjRef> {
841 self.obj_ref
842 }
843
844 pub fn content(&self, doc: &Document) -> Result<Vec<u8>> {
853 block_on(page_content_with(Immediate(doc), self))
854 }
855
856 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 pub fn dict(&self) -> &Dict {
876 &self.dict
877 }
878}
879
880pub 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) => src.stream_data(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; };
926 if !first {
927 out.push(b'\n');
928 }
929 out.extend_from_slice(&src.stream_data(stream).await?);
930 first = false;
931 }
932 Ok(out)
933 }
934 _ => Ok(Vec::new()),
935 }
936}
937
938#[cfg(test)]
939mod tests {
940 use super::*;
941 use crate::object::Name;
942 use crate::parser::{NoResolve, Parser};
943 use crate::xref::XrefEntry;
944 use pdfboss_testkit::{multi_page_doc, objstm_doc, objstm_payload, simple_doc, PdfBuilder};
945
946 #[test]
950 fn async_page_content_matches_the_sync_accessor() {
951 let doc = Document::load(multi_page_doc(&["one", "two", "three"])).expect("load");
952 let mut saw_content = false;
953 for index in 0..3 {
954 let page = doc.page(index).expect("page");
955 let direct = page.content(&doc).expect("sync content");
956 let awaited =
957 block_on(page_content_with(Immediate(&doc), &page)).expect("async content");
958 assert_eq!(direct, awaited, "page {index}");
959 saw_content |= !direct.is_empty();
960 }
961 assert!(
963 saw_content,
964 "fixture must give at least one page real content"
965 );
966 }
967
968 #[test]
973 fn a_shared_helper_is_reachable_from_a_caller_that_owns_its_source() {
974 let doc = Document::load(simple_doc("Hello")).expect("load");
975 let page = doc.page(0).expect("page");
976
977 let owner = Immediate(&doc);
978 let through_reference = block_on(page_content_with(&owner, &page)).expect("async content");
979
980 assert_eq!(through_reference, page.content(&doc).expect("sync content"));
981 assert!(!through_reference.is_empty());
982 }
983
984 #[test]
988 fn the_seed_is_shareable_across_threads() {
989 fn assert_send_sync<T: Send + Sync>() {}
990 assert_send_sync::<DocumentSeed>();
991 }
992
993 #[test]
997 fn a_fork_reads_exactly_what_its_parent_reads() {
998 let doc = Document::load(pdfboss_testkit::encrypted_rc4_doc("forked secret")).unwrap();
999 let fork = doc.fork();
1000 assert_eq!(fork.page_count(), doc.page_count());
1001 let (a, b) = (doc.page(0).unwrap(), fork.page(0).unwrap());
1002 assert_eq!(a.media_box, b.media_box);
1003 assert_eq!(
1004 a.content(&doc).expect("parent content"),
1005 b.content(&fork).expect("fork content"),
1006 "decrypted content agrees"
1007 );
1008 }
1009
1010 #[test]
1014 fn map_pages_preserves_page_order() {
1015 let doc = Document::load(multi_page_doc(&["one", "two", "three"])).unwrap();
1016 let contents = map_pages(&doc, |doc, page| page.content(doc));
1017 assert_eq!(contents.len(), 3);
1018 let texts: Vec<String> = contents
1019 .into_iter()
1020 .map(|c| String::from_utf8_lossy(&c.unwrap()).into_owned())
1021 .collect();
1022 assert!(texts[0].contains("(one)"), "{}", texts[0]);
1023 assert!(texts[1].contains("(two)"), "{}", texts[1]);
1024 assert!(texts[2].contains("(three)"), "{}", texts[2]);
1025 }
1026
1027 fn replace_once(data: &[u8], from: &[u8], to: &[u8]) -> Vec<u8> {
1030 let pos = memchr::memmem::find(data, from).expect("pattern present in fixture");
1031 let mut out = Vec::with_capacity(data.len() - from.len() + to.len());
1032 out.extend_from_slice(&data[..pos]);
1033 out.extend_from_slice(to);
1034 out.extend_from_slice(&data[pos + from.len()..]);
1035 out
1036 }
1037
1038 fn contains(haystack: &[u8], needle: &[u8]) -> bool {
1039 memchr::memmem::find(haystack, needle).is_some()
1040 }
1041
1042 const FONT: &str = "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>";
1043
1044 #[test]
1045 fn loads_simple_doc() {
1046 let doc = Document::load(simple_doc("Greetings, cosmos!")).unwrap();
1047 assert_eq!(doc.version(), (1, 7));
1048 assert_eq!(doc.page_count(), 1);
1049 let page = doc.page(0).unwrap();
1050 assert_eq!(page.index, 0);
1051 assert_eq!(page.media_box, Rect::new(0.0, 0.0, 612.0, 792.0));
1052 assert_eq!(page.crop_box, page.media_box);
1053 assert_eq!(page.rotate, 0);
1054 assert_eq!(page.size(), (612.0, 792.0));
1055 assert!(page.resources.get("Font").is_some());
1056 let content = page.content(&doc).unwrap();
1057 assert!(contains(&content, b"Greetings, cosmos!"));
1058 }
1059
1060 #[test]
1061 fn multi_page_ordering() {
1062 let doc = Document::load(multi_page_doc(&["alpha", "beta", "gamma"])).unwrap();
1063 assert_eq!(doc.page_count(), 3);
1064 for (i, text) in ["alpha", "beta", "gamma"].iter().enumerate() {
1065 let content = doc.page(i).unwrap().content(&doc).unwrap();
1066 assert!(
1067 contains(&content, text.as_bytes()),
1068 "page {i} should show {text}"
1069 );
1070 }
1071 }
1072
1073 #[test]
1074 fn page_index_out_of_bounds() {
1075 let doc = Document::load(simple_doc("x")).unwrap();
1076 assert!(matches!(doc.page(5), Err(Error::PageNotFound(5, 1))));
1077 }
1078
1079 #[test]
1080 fn open_reads_from_disk() {
1081 let dir = std::env::temp_dir();
1082 let path = dir.join(format!("pdfboss-doc-test-{}.pdf", std::process::id()));
1083 std::fs::write(&path, simple_doc("from disk")).unwrap();
1084 let doc = Document::open(&path).unwrap();
1085 std::fs::remove_file(&path).ok();
1086 assert_eq!(doc.page_count(), 1);
1087 let content = doc.page(0).unwrap().content(&doc).unwrap();
1088 assert!(contains(&content, b"from disk"));
1089 assert!(matches!(
1090 Document::open(dir.join("pdfboss-doc-test-missing.pdf")),
1091 Err(Error::Io(_))
1092 ));
1093 }
1094
1095 #[test]
1096 fn encrypt_in_trailer_is_rejected() {
1097 let data = replace_once(
1098 &simple_doc("secret"),
1099 b"trailer\n<< /Size",
1100 b"trailer\n<< /Encrypt 9 0 R /Size",
1101 );
1102 assert!(matches!(Document::load(data), Err(Error::Encrypted)));
1103 }
1104
1105 #[test]
1106 fn metadata_utf16be_round_trip() {
1107 let mut b = PdfBuilder::new();
1108 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1109 b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
1110 b.object(6, "<< /Title <FEFF00480151> /Author (plain author) >>");
1112 let data = replace_once(&b.build(1), b"<< /Size", b"<< /Info 6 0 R /Size");
1113 let doc = Document::load(data).unwrap();
1114 let meta = doc.metadata();
1115 assert_eq!(meta.title.as_deref(), Some("H\u{151}"));
1116 assert_eq!(meta.author.as_deref(), Some("plain author"));
1117 assert_eq!(meta.subject, None);
1118 assert_eq!(meta.keywords, None);
1119 assert_eq!(meta.creation_date, None);
1120 }
1121
1122 #[test]
1123 fn metadata_without_info_is_all_none() {
1124 let doc = Document::load(simple_doc("x")).unwrap();
1125 assert_eq!(doc.metadata(), Metadata::default());
1126 }
1127
1128 #[test]
1129 fn missing_object_resolves_to_null() {
1130 let doc = Document::load(simple_doc("x")).unwrap();
1131 let missing = Object::Ref(ObjRef { num: 99, gen: 0 });
1132 assert_eq!(doc.resolve(&missing).unwrap(), Object::Null);
1133 assert!(matches!(
1134 doc.get(ObjRef { num: 99, gen: 0 }),
1135 Err(Error::ObjectNotFound(99, 0))
1136 ));
1137 }
1138
1139 #[test]
1140 fn self_reference_is_circular() {
1141 let mut b = PdfBuilder::new();
1142 b.object(1, "<< /Type /Catalog >>");
1143 b.object(6, "6 0 R");
1144 let doc = Document::load(b.build(1)).unwrap();
1145 let loops = Object::Ref(ObjRef { num: 6, gen: 0 });
1146 assert!(matches!(
1147 doc.resolve(&loops),
1148 Err(Error::CircularReference(6))
1149 ));
1150 }
1151
1152 #[test]
1153 fn generation_mismatch_is_tolerated() {
1154 let doc = Document::load(simple_doc("x")).unwrap();
1155 let catalog = doc.get(ObjRef { num: 1, gen: 7 }).unwrap();
1156 let dict = catalog.as_dict().unwrap();
1157 assert_eq!(dict.get_name("Type").map(|n| n.0.as_str()), Some("Catalog"));
1158 }
1159
1160 #[test]
1161 fn objects_in_object_streams_are_fetched() {
1162 let mut b = PdfBuilder::new();
1163 let (dict, payload) =
1164 objstm_payload(&[(1, "<< /Type /Catalog /Pages 2 0 R >>"), (5, FONT)]);
1165 b.stream(6, &dict, &payload);
1166 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1167 b.object(
1168 3,
1169 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1170 /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
1171 );
1172 b.stream(4, "", b"BT /F1 12 Tf (compressed hello) Tj ET");
1173 let doc = Document::load(b.build_xref_stream(1)).unwrap();
1174 assert_eq!(doc.page_count(), 1);
1175 let page = doc.page(0).unwrap();
1176 assert!(contains(&page.content(&doc).unwrap(), b"compressed hello"));
1177 let font = doc.get(ObjRef { num: 5, gen: 0 }).unwrap();
1178 assert_eq!(
1179 font.as_dict()
1180 .and_then(|d| d.get_name("BaseFont"))
1181 .map(|n| n.0.as_str()),
1182 Some("Helvetica")
1183 );
1184 }
1185
1186 #[test]
1187 fn contents_array_is_joined_with_newlines() {
1188 let mut b = PdfBuilder::new();
1189 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1190 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1191 b.object(
1192 3,
1193 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1194 /Contents [4 0 R null 5 0 R] >>",
1195 );
1196 b.stream(4, "", b"q");
1197 b.stream(5, "", b"Q");
1198 let doc = Document::load(b.build(1)).unwrap();
1199 let content = doc.page(0).unwrap().content(&doc).unwrap();
1200 assert_eq!(content, b"q\nQ", "streams joined by \\n, null skipped");
1201 }
1202
1203 #[test]
1204 fn inheritance_from_pages_node_and_rotate_swap() {
1205 let mut b = PdfBuilder::new();
1206 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1207 b.object(
1208 2,
1209 "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 \
1210 /Resources << /Font << /F1 5 0 R >> >> /MediaBox [0 0 400 600] >>",
1211 );
1212 b.object(3, "<< /Type /Page /Parent 2 0 R >>");
1213 b.object(4, "<< /Type /Page /Parent 2 0 R /Rotate 270 >>");
1214 b.object(5, FONT);
1215 let doc = Document::load(b.build(1)).unwrap();
1216 assert_eq!(doc.page_count(), 2);
1217
1218 let first = doc.page(0).unwrap();
1219 assert_eq!(first.media_box, Rect::new(0.0, 0.0, 400.0, 600.0));
1220 assert_eq!(first.crop_box, first.media_box);
1221 assert!(first.resources.get("Font").is_some(), "inherited resources");
1222 assert_eq!(first.rotate, 0);
1223 assert!(
1224 first.content(&doc).unwrap().is_empty(),
1225 "no /Contents means empty content"
1226 );
1227 assert_eq!(first.size(), (400.0, 600.0));
1228
1229 let second = doc.page(1).unwrap();
1230 assert_eq!(second.rotate, 270);
1231 assert_eq!(second.size(), (600.0, 400.0), "rotate 270 swaps w/h");
1232 }
1233
1234 #[test]
1235 fn crop_box_intersected_and_rotate_normalized() {
1236 let mut b = PdfBuilder::new();
1237 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1238 b.object(2, "<< /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 >>");
1239 b.object(
1240 3,
1241 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] \
1242 /CropBox [100 100 400 400] /Rotate 450 >>",
1243 );
1244 b.object(4, "<< /Type /Page /Parent 2 0 R /Rotate -90 >>");
1245 b.object(
1246 5,
1247 "<< /Type /Page /Parent 2 0 R /Rotate 45 /MediaBox [0 0 0 0] >>",
1248 );
1249 let doc = Document::load(b.build(1)).unwrap();
1250
1251 let clipped = doc.page(0).unwrap();
1252 assert_eq!(clipped.crop_box, Rect::new(100.0, 100.0, 200.0, 200.0));
1253 assert_eq!(clipped.rotate, 90, "450 normalizes to 90");
1254 assert_eq!(clipped.size(), (100.0, 100.0));
1255
1256 assert_eq!(doc.page(1).unwrap().rotate, 270, "-90 normalizes to 270");
1257 let odd = doc.page(2).unwrap();
1258 assert_eq!(odd.rotate, 0, "non-multiple of 90 falls back to 0");
1259 assert_eq!(
1260 odd.media_box,
1261 Page::US_LETTER,
1262 "degenerate media box defaults"
1263 );
1264 }
1265
1266 #[test]
1267 fn kids_cycle_truncates_without_hanging() {
1268 let mut b = PdfBuilder::new();
1269 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1270 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1272 b.object(3, "<< /Type /Pages /Kids [4 0 R 2 0 R] /Count 1 >>");
1273 b.object(
1274 4,
1275 "<< /Type /Page /Parent 3 0 R /MediaBox [0 0 100 100] /Contents 5 0 R >>",
1276 );
1277 b.stream(5, "", b"0 0 50 50 re f");
1278 let doc = Document::load(b.build(1)).unwrap();
1279 assert_eq!(doc.page_count(), 1, "cycle back-edge yields no extra pages");
1280 assert!(contains(
1281 &doc.page(0).unwrap().content(&doc).unwrap(),
1282 b"re f"
1283 ));
1284 }
1285
1286 #[test]
1287 fn tree_depth_is_capped() {
1288 let mut b = PdfBuilder::new();
1289 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1290 let last = 302u32;
1292 for num in 2..last {
1293 b.object(
1294 num,
1295 &format!("<< /Type /Pages /Kids [{} 0 R] /Count 1 >>", num + 1),
1296 );
1297 }
1298 b.object(last, "<< /Type /Page >>");
1299 let doc = Document::load(b.build(1)).unwrap();
1300 assert_eq!(doc.page_count(), 1, "declared /Count is reported cheaply");
1305 assert!(
1306 matches!(doc.page(0), Err(Error::PageNotFound(0, 0))),
1307 "leaf beyond the depth cap cannot be materialized"
1308 );
1309 }
1310
1311 #[test]
1312 fn page_count_reports_declared_count_cheaply() {
1313 let mut b = PdfBuilder::new();
1317 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1318 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 5 >>");
1319 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
1320 let doc = Document::load(b.build(1)).unwrap();
1321 assert_eq!(doc.page_count(), 5, "declared /Count reported verbatim");
1322 assert!(doc.page(0).is_ok(), "the one real page materializes");
1323 assert!(
1324 matches!(doc.page(1), Err(Error::PageNotFound(1, 1))),
1325 "access past the real pages fails with the true length"
1326 );
1327 }
1328
1329 #[test]
1330 fn page_count_falls_back_to_walk_when_count_absent() {
1331 let mut b = PdfBuilder::new();
1332 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1333 b.object(2, "<< /Type /Pages /Kids [3 0 R] >>"); b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
1335 let doc = Document::load(b.build(1)).unwrap();
1336 assert_eq!(
1337 doc.page_count(),
1338 1,
1339 "missing /Count is recovered by walking"
1340 );
1341 }
1342
1343 #[test]
1344 fn page_count_ignores_corrupt_oversized_count() {
1345 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] /Count 999999999 >>");
1350 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
1351 let doc = Document::load(b.build(1)).unwrap();
1352 assert_eq!(doc.page_count(), 1, "implausible /Count is rejected");
1353 }
1354
1355 #[test]
1356 fn version_scan_and_default() {
1357 let mut b = PdfBuilder::new().version(2, 0);
1358 b.object(1, "<< /Type /Catalog >>");
1359 assert_eq!(Document::load(b.build(1)).unwrap().version(), (2, 0));
1360 let data = replace_once(&simple_doc("v"), b"%PDF-", b"%QQQ-");
1362 assert_eq!(Document::load(data).unwrap().version(), (1, 4));
1363 }
1364
1365 #[test]
1366 fn deeply_nested_root_object_does_not_overflow_the_stack() {
1367 let mut data = b"%PDF-1.7\n1 0 obj\n".to_vec();
1372 data.extend(std::iter::repeat_n(b'[', 50_000));
1373 data.extend(std::iter::repeat_n(b']', 50_000));
1374 data.extend_from_slice(b"\nendobj\ntrailer\n<</Root 1 0 R>>\n%%EOF\n");
1375 let outcome = std::thread::Builder::new()
1376 .stack_size(1024 * 1024)
1377 .spawn(move || Document::load(data).map(|doc| doc.page_count()))
1378 .expect("spawn test thread")
1379 .join()
1380 .expect("Document::load must not overflow the stack");
1381 assert!(matches!(outcome, Ok(0) | Err(_)));
1384 }
1385
1386 #[test]
1387 fn bytes_and_xref_accessors() {
1388 let data = simple_doc("accessors");
1389 let doc = Document::load(data.clone()).unwrap();
1390 assert_eq!(doc.bytes(), &data[..]);
1391 assert!(!doc.xref().is_empty());
1392 assert!(doc.xref().trailer.get("Root").is_some());
1393 }
1394
1395 #[test]
1396 fn object_at_spanned_reparses_identically() {
1397 let data = simple_doc("spanned");
1398 let doc = Document::load(data).unwrap();
1399 for (num, entry) in doc.xref().iter() {
1400 let XrefEntry::InFile { offset, gen } = entry else {
1401 continue;
1402 };
1403 let (r, object, span) = doc.object_at_spanned(offset as usize).unwrap();
1404 assert_eq!(r.num, num);
1405 assert_eq!(r.gen, gen);
1406 assert_eq!(span.start, offset);
1407 assert!(span.end as usize <= doc.bytes().len());
1408 let slice = &doc.bytes()[span.start as usize..span.end as usize];
1410 let (r2, object2) = Parser::new(slice).parse_indirect(&NoResolve).unwrap();
1411 assert_eq!(r2, r);
1412 assert_eq!(object2, object);
1413 }
1414 }
1415
1416 #[test]
1417 fn page_object_ref_points_at_a_page_dict() {
1418 let doc = Document::load(multi_page_doc(&["one", "two"])).unwrap();
1419 for index in 0..doc.page_count() {
1420 let page = doc.page(index).unwrap();
1421 let r = page.object_ref().expect("builder pages are indirect");
1422 let resolved = doc.get(r).unwrap();
1423 assert_eq!(
1424 resolved
1425 .as_dict()
1426 .unwrap()
1427 .get_name("Type")
1428 .map(|n| n.0.as_str()),
1429 Some("Page")
1430 );
1431 }
1432 }
1433
1434 const RC4_FIXTURE_KEY_LEN: usize = 16; const RC4_FIXTURE_P: i32 = -44;
1444 const RC4_FIXTURE_ID0: &[u8] = b"0123456789abcdef";
1445 const RC4_FIXTURE_PAD: [u8; 32] = [
1446 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01,
1447 0x08, 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53,
1448 0x69, 0x7A,
1449 ];
1450
1451 #[rustfmt::skip]
1452 const RC4_FIXTURE_MD5_S: [u32; 64] = [
1453 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
1454 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
1455 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
1456 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
1457 ];
1458 #[rustfmt::skip]
1459 const RC4_FIXTURE_MD5_K: [u32; 64] = [
1460 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
1461 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
1462 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
1463 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
1464 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
1465 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
1466 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
1467 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,
1468 ];
1469
1470 fn rc4_fixture_md5(input: &[u8]) -> [u8; 16] {
1471 let (mut a0, mut b0, mut c0, mut d0) = (
1472 0x6745_2301u32,
1473 0xefcd_ab89u32,
1474 0x98ba_dcfeu32,
1475 0x1032_5476u32,
1476 );
1477 let mut msg = input.to_vec();
1478 let bitlen = (input.len() as u64).wrapping_mul(8);
1479 msg.push(0x80);
1480 while msg.len() % 64 != 56 {
1481 msg.push(0);
1482 }
1483 msg.extend_from_slice(&bitlen.to_le_bytes());
1484 for chunk in msg.chunks_exact(64) {
1485 let mut m = [0u32; 16];
1486 for (word, bytes) in m.iter_mut().zip(chunk.chunks_exact(4)) {
1487 *word = u32::from_le_bytes(bytes.try_into().unwrap());
1488 }
1489 let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0);
1490 for i in 0..64 {
1491 let (f, g) = match i {
1492 0..=15 => ((b & c) | (!b & d), i),
1493 16..=31 => ((d & b) | (!d & c), (5 * i + 1) % 16),
1494 32..=47 => (b ^ c ^ d, (3 * i + 5) % 16),
1495 _ => (c ^ (b | !d), (7 * i) % 16),
1496 };
1497 let f = f
1498 .wrapping_add(a)
1499 .wrapping_add(RC4_FIXTURE_MD5_K[i])
1500 .wrapping_add(m[g]);
1501 a = d;
1502 d = c;
1503 c = b;
1504 b = b.wrapping_add(f.rotate_left(RC4_FIXTURE_MD5_S[i]));
1505 }
1506 a0 = a0.wrapping_add(a);
1507 b0 = b0.wrapping_add(b);
1508 c0 = c0.wrapping_add(c);
1509 d0 = d0.wrapping_add(d);
1510 }
1511 let mut out = [0u8; 16];
1512 out[0..4].copy_from_slice(&a0.to_le_bytes());
1513 out[4..8].copy_from_slice(&b0.to_le_bytes());
1514 out[8..12].copy_from_slice(&c0.to_le_bytes());
1515 out[12..16].copy_from_slice(&d0.to_le_bytes());
1516 out
1517 }
1518
1519 fn rc4_fixture_rc4(key: &[u8], data: &[u8]) -> Vec<u8> {
1520 let mut s: [u8; 256] = core::array::from_fn(|i| i as u8);
1521 let mut j = 0u8;
1522 for i in 0..256 {
1523 j = j.wrapping_add(s[i]).wrapping_add(key[i % key.len()]);
1524 s.swap(i, j as usize);
1525 }
1526 let mut out = Vec::with_capacity(data.len());
1527 let (mut i, mut j) = (0u8, 0u8);
1528 for &byte in data {
1529 i = i.wrapping_add(1);
1530 j = j.wrapping_add(s[i as usize]);
1531 s.swap(i as usize, j as usize);
1532 let k = s[s[i as usize].wrapping_add(s[j as usize]) as usize];
1533 out.push(byte ^ k);
1534 }
1535 out
1536 }
1537
1538 fn rc4_fixture_owner_entry() -> Vec<u8> {
1540 let mut d = rc4_fixture_md5(&RC4_FIXTURE_PAD);
1541 for _ in 0..50 {
1542 d = rc4_fixture_md5(&d[..RC4_FIXTURE_KEY_LEN]);
1543 }
1544 let rc4key = d[..RC4_FIXTURE_KEY_LEN].to_vec();
1545 let mut o = rc4_fixture_rc4(&rc4key, &RC4_FIXTURE_PAD);
1546 for i in 1u8..=19 {
1547 let k: Vec<u8> = rc4key.iter().map(|b| b ^ i).collect();
1548 o = rc4_fixture_rc4(&k, &o);
1549 }
1550 o
1551 }
1552
1553 fn rc4_fixture_file_key(o: &[u8]) -> Vec<u8> {
1555 let mut input = Vec::new();
1556 input.extend_from_slice(&RC4_FIXTURE_PAD);
1557 input.extend_from_slice(o);
1558 input.extend_from_slice(&(RC4_FIXTURE_P as u32).to_le_bytes());
1559 input.extend_from_slice(RC4_FIXTURE_ID0);
1560 let mut d = rc4_fixture_md5(&input);
1561 for _ in 0..50 {
1562 d = rc4_fixture_md5(&d[..RC4_FIXTURE_KEY_LEN]);
1563 }
1564 d[..RC4_FIXTURE_KEY_LEN].to_vec()
1565 }
1566
1567 fn rc4_fixture_user_entry(key: &[u8]) -> Vec<u8> {
1569 let mut input = Vec::new();
1570 input.extend_from_slice(&RC4_FIXTURE_PAD);
1571 input.extend_from_slice(RC4_FIXTURE_ID0);
1572 let mut x = rc4_fixture_md5(&input).to_vec();
1573 x = rc4_fixture_rc4(key, &x);
1574 for i in 1u8..=19 {
1575 let k: Vec<u8> = key.iter().map(|b| b ^ i).collect();
1576 x = rc4_fixture_rc4(&k, &x);
1577 }
1578 x.resize(32, 0); x
1580 }
1581
1582 fn rc4_fixture_obj_key(key: &[u8], num: u32, gen: u16) -> Vec<u8> {
1583 let mut input = key.to_vec();
1584 input.extend_from_slice(&num.to_le_bytes()[..3]);
1585 input.extend_from_slice(&gen.to_le_bytes()[..2]);
1586 rc4_fixture_md5(&input)[..(key.len() + 5).min(16)].to_vec()
1587 }
1588
1589 fn rc4_fixture_hexstr(b: &[u8]) -> String {
1590 let mut s = String::from("<");
1591 for x in b {
1592 s.push_str(&format!("{x:02x}"));
1593 }
1594 s.push('>');
1595 s
1596 }
1597
1598 fn rc4_encrypted_fixture() -> Vec<u8> {
1602 let o = rc4_fixture_owner_entry();
1603 let key = rc4_fixture_file_key(&o);
1604 let u = rc4_fixture_user_entry(&key);
1605 let msg = rc4_fixture_rc4(&rc4_fixture_obj_key(&key, 3, 0), b"Top secret message");
1606
1607 let mut b = PdfBuilder::new().version(1, 4);
1608 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1609 b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
1610 b.object(3, &format!("<< /Msg {} >>", rc4_fixture_hexstr(&msg)));
1611 b.object(
1612 9,
1613 &format!(
1614 "<< /Filter /Standard /V 2 /R 3 /Length 128 /P {} /O {} /U {} >>",
1615 RC4_FIXTURE_P,
1616 rc4_fixture_hexstr(&o),
1617 rc4_fixture_hexstr(&u)
1618 ),
1619 );
1620 let trailer = format!(
1621 "/Encrypt 9 0 R /ID [{}{}]",
1622 rc4_fixture_hexstr(RC4_FIXTURE_ID0),
1623 rc4_fixture_hexstr(RC4_FIXTURE_ID0)
1624 );
1625 b.trailer_extra(&trailer).build(1)
1626 }
1627
1628 #[test]
1629 fn encrypted_generation_mismatch_still_decrypts() {
1630 let doc = Document::load(rc4_encrypted_fixture()).expect("empty password opens the file");
1640 let obj3 = doc.get(ObjRef { num: 3, gen: 7 }).unwrap();
1641 let msg = obj3
1642 .as_dict()
1643 .unwrap()
1644 .get("Msg")
1645 .unwrap()
1646 .as_str_bytes()
1647 .unwrap();
1648 assert_eq!(
1649 msg, b"Top secret message",
1650 "decrypted using the file's real gen (0), not the mismatched request (7)"
1651 );
1652 }
1653
1654 #[test]
1655 fn objstm_doc_fixture_loads_and_resolves_members() {
1656 let data = objstm_doc(&[(7, "<< /Marker (inside) >>")]);
1657 let doc = Document::load(data).unwrap();
1658 assert_eq!(doc.page_count(), 1);
1659 let member = doc.get(ObjRef { num: 7, gen: 0 }).unwrap();
1660 let text = member.as_dict().unwrap().get("Marker").unwrap();
1661 assert_eq!(text.as_str_bytes(), Some(&b"inside"[..]));
1662 assert!(matches!(
1664 doc.xref().get(7),
1665 Some(XrefEntry::InStream { stream_num: 4, .. })
1666 ));
1667 }
1668
1669 #[test]
1672 fn page_from_parts_exposes_its_accessors() {
1673 let mut dict = Dict::default();
1674 dict.insert(Name("Type".into()), Object::Name(Name("Page".into())));
1675 let media = Rect::new(0.0, 0.0, 200.0, 400.0);
1676 let obj_ref = ObjRef { num: 7, gen: 0 };
1677
1678 let page = Page::from_parts(
1679 3,
1680 media,
1681 media,
1682 90,
1683 Dict::default(),
1684 dict.clone(),
1685 Some(obj_ref),
1686 );
1687
1688 assert_eq!(page.index, 3);
1689 assert_eq!(page.object_ref(), Some(obj_ref));
1690 assert_eq!(page.dict(), &dict);
1691 assert_eq!(page.size(), (400.0, 200.0));
1693 }
1694
1695 #[test]
1699 fn page_from_parts_normalizes_rotation() {
1700 let media = Rect::new(0.0, 0.0, 200.0, 400.0);
1701 let build = |rotate: i32| {
1702 Page::from_parts(
1703 0,
1704 media,
1705 media,
1706 rotate,
1707 Dict::default(),
1708 Dict::default(),
1709 None,
1710 )
1711 };
1712
1713 for (given, canonical) in [(-90, 270), (450, 90), (540, 180), (720, 0), (-360, 0)] {
1714 let page = build(given);
1715 assert_eq!(
1716 page.rotate, canonical,
1717 "from_parts must normalize /Rotate {given} to {canonical}"
1718 );
1719 assert_eq!(
1720 page.size(),
1721 build(canonical).size(),
1722 "/Rotate {given} must report the same size as {canonical}"
1723 );
1724 }
1725 }
1726
1727 #[test]
1732 fn page_size_handles_unnormalized_rotation() {
1733 let media = Rect::new(0.0, 0.0, 200.0, 400.0);
1734 let mut page = Page::from_parts(0, media, media, 0, Dict::default(), Dict::default(), None);
1735
1736 for rotate in [90, 270, -90, -270, 450, 630] {
1737 page.rotate = rotate;
1738 assert_eq!(
1739 page.size(),
1740 (400.0, 200.0),
1741 "/Rotate {rotate} is a quarter turn and must swap the size"
1742 );
1743 }
1744 for rotate in [0, 180, -180, 360, 540, 720] {
1745 page.rotate = rotate;
1746 assert_eq!(
1747 page.size(),
1748 (200.0, 400.0),
1749 "/Rotate {rotate} is a half turn and must leave the size alone"
1750 );
1751 }
1752 }
1753}