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 Document::load_with_password(data, "")
151 }
152
153 pub fn load_with_password(data: Vec<u8>, password: &str) -> Result<Document> {
161 let version = parse_version(&data);
162 let xref = load_xref(&data)?;
163 let mut doc = Document {
164 data: Arc::new(data),
165 version,
166 xref: Arc::new(xref),
167 cache: RefCell::new(FastMap::default()),
168 loading: RefCell::new(FastSet::default()),
169 objstms: RefCell::new(FastMap::default()),
170 decryptor: None,
171 pages: OnceCell::new(),
172 };
173 if doc
174 .xref
175 .trailer
176 .get("Encrypt")
177 .is_some_and(|o| !o.is_null())
178 {
179 doc.setup_decryption(password)?;
180 }
181 Ok(doc)
182 }
183
184 pub fn open_with_password(path: impl AsRef<Path>, password: &str) -> Result<Document> {
187 Document::load_with_password(std::fs::read(path)?, password)
188 }
189
190 fn setup_decryption(&mut self, password: &str) -> Result<()> {
198 let enc_obj = self
199 .xref
200 .trailer
201 .get("Encrypt")
202 .cloned()
203 .unwrap_or(Object::Null);
204 let enc = self.resolve(&enc_obj)?;
205 let enc_dict = enc.as_dict().ok_or(Error::Encrypted)?;
206 let id0: Vec<u8> = self
207 .xref
208 .trailer
209 .get("ID")
210 .and_then(Object::as_array)
211 .and_then(<[Object]>::first)
212 .and_then(Object::as_str_bytes)
213 .unwrap_or(&[])
214 .to_vec();
215 match Decryptor::from_standard_with_password_str(enc_dict, &id0, password) {
216 Some(dec) => {
217 self.decryptor = Some(dec);
218 self.cache.borrow_mut().clear();
222 Ok(())
223 }
224 None => Err(Error::Encrypted),
225 }
226 }
227
228 pub fn open(path: impl AsRef<Path>) -> Result<Document> {
230 Document::load(std::fs::read(path)?)
231 }
232
233 pub fn version(&self) -> (u8, u8) {
235 self.version
236 }
237
238 pub fn bytes(&self) -> &[u8] {
240 &self.data
241 }
242
243 pub fn xref(&self) -> &Xref {
245 &self.xref
246 }
247
248 pub fn get(&self, r: ObjRef) -> Result<Object> {
252 if let Some(cached) = self.cache.borrow().get(&(r.num, r.gen)) {
253 return Ok((**cached).clone());
254 }
255 if !self.loading.borrow_mut().insert(r.num) {
256 return Err(Error::CircularReference(r.num));
257 }
258 let result = self.load_object(r);
259 self.loading.borrow_mut().remove(&r.num);
260 let object = result?;
261 self.cache
262 .borrow_mut()
263 .insert((r.num, r.gen), Rc::new(object.clone()));
264 Ok(object)
265 }
266
267 fn load_object(&self, r: ObjRef) -> Result<Object> {
270 match self.xref.get(r.num) {
271 None | Some(XrefEntry::Free) => Err(Error::ObjectNotFound(r.num, r.gen)),
272 Some(XrefEntry::InFile { offset, .. }) => {
273 let offset = usize::try_from(offset)
274 .ok()
275 .filter(|&o| o < self.data.len())
276 .ok_or(Error::ObjectNotFound(r.num, r.gen))?;
277 self.object_at_spanned(offset).map(|parsed| parsed.1)
278 }
279 Some(XrefEntry::InStream { stream_num, index }) => {
280 self.load_from_object_stream(stream_num, index)
281 }
282 }
283 }
284
285 pub(crate) fn object_at_spanned(&self, offset: usize) -> Result<(ObjRef, Object, Span)> {
288 let mut parser = Parser::at(&self.data, offset);
289 let (r, mut object) = parser.parse_indirect(self)?;
290 if let Some(dec) = &self.decryptor {
294 dec.decrypt_object(&mut object, r.num, r.gen);
295 }
296 Ok((r, object, Span::new(offset as u64, parser.pos() as u64)))
297 }
298
299 pub(crate) fn objstm_handle(&self, stream_num: u32) -> Result<Rc<objstm::ObjStm>> {
302 if let Some(stm) = self.objstms.borrow().get(&stream_num) {
303 return Ok(Rc::clone(stm));
304 }
305 let container = self.get(ObjRef {
306 num: stream_num,
307 gen: 0,
308 })?;
309 let stream = container.as_stream().ok_or_else(|| Error::TypeMismatch {
310 expected: "stream",
311 found: type_name(&container),
312 })?;
313 let n = self
314 .resolve(stream.dict.get("N").unwrap_or(&Object::Null))?
315 .as_int()
316 .and_then(|v| usize::try_from(v).ok())
317 .ok_or(Error::MissingKey("N"))?;
318 let first = self
319 .resolve(stream.dict.get("First").unwrap_or(&Object::Null))?
320 .as_int()
321 .and_then(|v| usize::try_from(v).ok())
322 .ok_or(Error::MissingKey("First"))?;
323 let decoded = self.stream_data(stream)?;
324 let stm = Rc::new(objstm::ObjStm::parse(decoded, n, first)?);
325 self.objstms
326 .borrow_mut()
327 .insert(stream_num, Rc::clone(&stm));
328 Ok(stm)
329 }
330
331 fn load_from_object_stream(&self, stream_num: u32, index: u32) -> Result<Object> {
333 self.objstm_handle(stream_num)?.object(index)
334 }
335
336 pub fn resolve(&self, o: &Object) -> Result<Object> {
347 crate::source::resolve_sync_with(self, o)
348 }
349
350 pub fn stream_data(&self, s: &Stream) -> Result<Vec<u8>> {
353 filters::decode_stream(s, self)
354 }
355
356 fn pages(&self) -> &[PageRec] {
358 self.pages.get_or_init(|| Arc::new(self.flatten_pages()))
359 }
360
361 pub fn seed(&self) -> DocumentSeed {
365 self.pages();
366 DocumentSeed {
367 data: Arc::clone(&self.data),
368 version: self.version,
369 xref: Arc::clone(&self.xref),
370 decryptor: self.decryptor.clone(),
371 pages: Arc::clone(self.pages.get().expect("pages() was just forced")),
372 }
373 }
374
375 pub fn from_seed(seed: DocumentSeed) -> Document {
389 Document {
390 data: seed.data,
391 version: seed.version,
392 xref: seed.xref,
393 cache: RefCell::new(FastMap::default()),
394 loading: RefCell::new(FastSet::default()),
395 objstms: RefCell::new(FastMap::default()),
396 decryptor: seed.decryptor,
397 pages: OnceCell::from(seed.pages),
398 }
399 }
400
401 pub fn fork(&self) -> Document {
404 Document::from_seed(self.seed())
405 }
406
407 pub fn page_count(&self) -> usize {
416 if let Some(pages) = self.pages.get() {
417 return pages.len();
418 }
419 if let Some(count) = self.declared_page_count() {
420 return count;
421 }
422 self.pages().len()
423 }
424
425 fn declared_page_count(&self) -> Option<usize> {
431 let root = self.xref.trailer.get("Root")?;
432 let catalog = self.resolve(root).ok()?;
433 let pages = self.resolve(catalog.as_dict()?.get("Pages")?).ok()?;
434 let count = usize::try_from(self.int_value(pages.as_dict()?, "Count")?).ok()?;
435 (count <= self.data.len()).then_some(count)
438 }
439
440 pub fn page(&self, index: usize) -> Result<Page> {
442 let pages = self.pages();
443 let rec = pages
444 .get(index)
445 .ok_or(Error::PageNotFound(index, pages.len()))?;
446 Ok(Page {
447 index,
448 media_box: rec.media_box,
449 crop_box: rec.crop_box,
450 rotate: rec.rotate,
451 resources: rec.resources.clone(),
452 dict: rec.dict.clone(),
453 obj_ref: rec.obj_ref,
454 })
455 }
456
457 pub fn metadata(&self) -> Metadata {
460 let mut meta = Metadata::default();
461 let Some(info) = self.xref.trailer.get("Info") else {
462 return meta;
463 };
464 let Ok(info) = self.resolve(info) else {
465 return meta;
466 };
467 let Some(dict) = info.as_dict() else {
468 return meta;
469 };
470 meta.title = self.meta_string(dict, "Title");
471 meta.author = self.meta_string(dict, "Author");
472 meta.subject = self.meta_string(dict, "Subject");
473 meta.keywords = self.meta_string(dict, "Keywords");
474 meta.creator = self.meta_string(dict, "Creator");
475 meta.producer = self.meta_string(dict, "Producer");
476 meta.creation_date = self.meta_string(dict, "CreationDate");
477 meta.mod_date = self.meta_string(dict, "ModDate");
478 meta
479 }
480
481 fn meta_string(&self, dict: &Dict, key: &str) -> Option<String> {
483 let value = self.resolve(dict.get(key)?).ok()?;
484 Some(decode_text_string(value.as_str_bytes()?))
485 }
486
487 fn flatten_pages(&self) -> Vec<PageRec> {
492 let mut pages = Vec::new();
493 let Some(root) = self.xref.trailer.get("Root") else {
494 return pages;
495 };
496 let Ok(catalog) = self.resolve(root) else {
497 return pages;
498 };
499 let Some(tree_root) = catalog.as_dict().and_then(|d| d.get("Pages")) else {
500 return pages;
501 };
502 let mut visited: FastSet<ObjRef> = FastSet::default();
503 let mut stack: Vec<(Object, Inherited, usize)> =
504 vec![(tree_root.clone(), Inherited::default(), 0)];
505 while let Some((node, mut inherited, depth)) = stack.pop() {
506 if depth > MAX_TREE_DEPTH {
507 continue;
508 }
509 let node_ref = if let Object::Ref(r) = node {
510 Some(r)
511 } else {
512 None
513 };
514 if let Some(r) = node_ref {
515 if !visited.insert(r) {
516 continue; }
518 }
519 let Ok(resolved) = self.resolve(&node) else {
520 continue;
521 };
522 let Some(dict) = resolved.as_dict() else {
523 continue;
524 };
525 if let Some(res) = self.dict_value(dict, "Resources") {
526 inherited.resources = Some(res);
527 }
528 if let Some(mb) = self.rect_value(dict, "MediaBox") {
529 inherited.media_box = Some(mb);
530 }
531 if let Some(cb) = self.rect_value(dict, "CropBox") {
532 inherited.crop_box = Some(cb);
533 }
534 if let Some(rot) = self.int_value(dict, "Rotate") {
535 inherited.rotate = Some(rot);
536 }
537 let is_page = dict.get_name("Type").is_some_and(|n| n.0 == "Page");
538 let kids = if is_page {
539 None
540 } else {
541 self.array_value(dict, "Kids")
542 };
543 match kids {
544 Some(kids) => {
545 for kid in kids.iter().rev() {
547 stack.push((kid.clone(), inherited.clone(), depth + 1));
548 }
549 }
550 None => pages.push(make_page_rec(node_ref, dict.clone(), &inherited)),
551 }
552 }
553 pages
554 }
555
556 fn dict_value(&self, dict: &Dict, key: &str) -> Option<Dict> {
558 self.resolve(dict.get(key)?).ok()?.as_dict().cloned()
559 }
560
561 fn array_value(&self, dict: &Dict, key: &str) -> Option<Vec<Object>> {
563 match self.resolve(dict.get(key)?).ok()? {
564 Object::Array(items) => Some(items),
565 _ => None,
566 }
567 }
568
569 fn int_value(&self, dict: &Dict, key: &str) -> Option<i32> {
571 let v = self.resolve(dict.get(key)?).ok()?.as_f64()?;
572 if v.is_finite() {
573 Some(v as i32)
574 } else {
575 None
576 }
577 }
578
579 fn rect_value(&self, dict: &Dict, key: &str) -> Option<Rect> {
582 let items = self.array_value(dict, key)?;
583 if items.len() != 4 {
584 return None;
585 }
586 let mut coords = [0.0f32; 4];
587 for (slot, item) in coords.iter_mut().zip(&items) {
588 let n = self.resolve(item).ok()?.as_f64()?;
589 if !n.is_finite() {
590 return None;
591 }
592 *slot = n as f32;
593 }
594 Some(Rect::new(coords[0], coords[1], coords[2], coords[3]).normalize())
595 }
596}
597
598pub fn map_pages<T, F>(doc: &Document, work: F) -> Vec<Result<T>>
617where
618 T: Send + Sync,
619 F: Fn(&Document, &Page) -> Result<T> + Send + Sync,
620{
621 let count = doc.pages().len();
622 let workers = std::thread::available_parallelism()
623 .map(std::num::NonZeroUsize::get)
624 .unwrap_or(1)
625 .min(count);
626 if workers <= 1 {
627 return (0..count)
628 .map(|i| doc.page(i).and_then(|page| work(doc, &page)))
629 .collect();
630 }
631
632 let seed = doc.seed();
633 let next = std::sync::atomic::AtomicUsize::new(0);
634 let slots: Vec<std::sync::OnceLock<Result<T>>> =
635 (0..count).map(|_| std::sync::OnceLock::new()).collect();
636 std::thread::scope(|scope| {
637 for _ in 0..workers {
638 let seed = seed.clone();
641 let next = &next;
642 let slots = &slots;
643 let work = &work;
644 scope.spawn(move || {
645 let worker = Document::from_seed(seed);
646 loop {
647 let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
648 if i >= count {
649 break;
650 }
651 let outcome = worker.page(i).and_then(|page| work(&worker, &page));
652 if slots[i].set(outcome).is_err() {
657 unreachable!("page index {i} was dispatched twice");
658 }
659 }
660 });
661 }
662 });
663 slots
664 .into_iter()
665 .map(|slot| {
666 slot.into_inner()
667 .expect("every page index below the count was dispatched")
668 })
669 .collect()
670}
671
672fn make_page_rec(obj_ref: Option<ObjRef>, dict: Dict, inherited: &Inherited) -> PageRec {
677 let page = Page::from_tree_attrs(
678 0,
679 inherited.resources.clone(),
680 inherited.media_box,
681 inherited.crop_box,
682 inherited.rotate,
683 dict,
684 obj_ref,
685 );
686 PageRec {
687 obj_ref: page.obj_ref,
688 media_box: page.media_box,
689 crop_box: page.crop_box,
690 rotate: page.rotate,
691 resources: page.resources,
692 dict: page.dict,
693 }
694}
695
696fn type_name(o: &Object) -> &'static str {
698 match o {
699 Object::Null => "null",
700 Object::Bool(_) => "boolean",
701 Object::Int(_) => "integer",
702 Object::Real(_) => "real",
703 Object::String(_) => "string",
704 Object::Name(_) => "name",
705 Object::Array(_) => "array",
706 Object::Dict(_) => "dictionary",
707 Object::Stream(_) => "stream",
708 Object::Ref(_) => "reference",
709 }
710}
711
712impl Resolve for Document {
713 fn resolve_ref(&self, r: ObjRef) -> Option<Object> {
714 self.get(r).ok()
715 }
716}
717
718impl crate::source::ObjectSource for Document {
726 fn get(&self, r: ObjRef) -> Result<Object> {
727 Document::get(self, r)
728 }
729
730 fn stream_data(&self, s: &Stream) -> Result<Vec<u8>> {
731 Document::stream_data(self, s)
732 }
733
734 fn resolve(&self, o: &Object) -> Result<Object> {
735 Document::resolve(self, o)
736 }
737}
738
739#[derive(Debug, Clone, Default, PartialEq, Eq)]
742pub struct Metadata {
743 pub title: Option<String>,
744 pub author: Option<String>,
745 pub subject: Option<String>,
746 pub keywords: Option<String>,
747 pub creator: Option<String>,
748 pub producer: Option<String>,
749 pub creation_date: Option<String>,
750 pub mod_date: Option<String>,
751}
752
753#[derive(Clone)]
763pub struct Page {
764 pub index: usize,
766 pub media_box: Rect,
767 pub crop_box: Rect,
768 pub rotate: i32,
769 pub resources: Dict,
771 dict: Dict,
772 obj_ref: Option<ObjRef>,
773}
774
775impl Page {
776 pub fn from_parts(
790 index: usize,
791 media_box: Rect,
792 crop_box: Rect,
793 rotate: i32,
794 resources: Dict,
795 dict: Dict,
796 obj_ref: Option<ObjRef>,
797 ) -> Page {
798 Page {
799 index,
800 media_box,
801 crop_box,
802 rotate: normalize_rotation(rotate),
803 resources,
804 dict,
805 obj_ref,
806 }
807 }
808
809 pub const US_LETTER: Rect = Rect::new(0.0, 0.0, 612.0, 792.0);
814
815 pub fn from_tree_attrs(
829 index: usize,
830 resources: Option<Dict>,
831 media_box: Option<Rect>,
832 crop_box: Option<Rect>,
833 rotate: Option<i32>,
834 dict: Dict,
835 obj_ref: Option<ObjRef>,
836 ) -> Page {
837 let media_box = media_box
838 .filter(|r| r.width() > 0.0 && r.height() > 0.0)
839 .unwrap_or(Page::US_LETTER);
840 let crop_box = crop_box
841 .and_then(|c| c.intersect(media_box))
842 .filter(|r| r.width() > 0.0 && r.height() > 0.0)
843 .unwrap_or(media_box);
844 Page {
845 index,
846 media_box,
847 crop_box,
848 rotate: normalize_rotation(rotate.unwrap_or(0)),
849 resources: resources.unwrap_or_default(),
850 dict,
851 obj_ref,
852 }
853 }
854
855 pub fn object_ref(&self) -> Option<ObjRef> {
859 self.obj_ref
860 }
861
862 pub fn content(&self, doc: &Document) -> Result<Vec<u8>> {
871 block_on(page_content_with(Immediate(doc), self))
872 }
873
874 pub fn size(&self) -> (f32, f32) {
883 let (w, h) = (self.crop_box.width(), self.crop_box.height());
884 let turn = self.rotate.rem_euclid(360);
885 if turn == 90 || turn == 270 {
886 (h, w)
887 } else {
888 (w, h)
889 }
890 }
891
892 pub fn dict(&self) -> &Dict {
894 &self.dict
895 }
896}
897
898pub async fn page_content_with<S: AsyncObjectSource>(src: S, page: &Page) -> Result<Vec<u8>> {
931 let Some(contents) = page.dict.get("Contents") else {
932 return Ok(Vec::new());
933 };
934 match src.resolve(contents).await? {
935 Object::Stream(ref s) => content_stream_data_with(&src, s).await,
936 Object::Array(items) => {
937 let mut out = Vec::new();
938 let mut first = true;
939 for item in &items {
940 let part = src.resolve(item).await?;
941 let Some(stream) = part.as_stream() else {
942 continue; };
944 if !first {
945 out.push(b'\n');
946 }
947 out.extend_from_slice(&content_stream_data_with(&src, stream).await?);
948 first = false;
949 }
950 Ok(out)
951 }
952 _ => Ok(Vec::new()),
953 }
954}
955
956pub async fn decoded_stream_data_with<S: AsyncObjectSource>(src: S, s: &Stream) -> Result<Vec<u8>> {
989 if let Some(name) = filters::trailing_filter_checked_with(&src, &s.dict).await? {
990 if filters::is_image_codec(&name.0) {
991 return Err(Error::UnsupportedFilter(name.0));
992 }
993 }
994 src.stream_data(s).await
995}
996
997pub async fn content_stream_data_with<S: AsyncObjectSource>(src: S, s: &Stream) -> Result<Vec<u8>> {
1007 decoded_stream_data_with(src, s).await
1008}
1009
1010#[cfg(test)]
1011mod tests {
1012 use super::*;
1013 use crate::object::Name;
1014 use crate::parser::{NoResolve, Parser};
1015 use crate::xref::XrefEntry;
1016 use pdfboss_testkit::{multi_page_doc, objstm_doc, objstm_payload, simple_doc, PdfBuilder};
1017
1018 #[test]
1022 fn async_page_content_matches_the_sync_accessor() {
1023 let doc = Document::load(multi_page_doc(&["one", "two", "three"])).expect("load");
1024 let mut saw_content = false;
1025 for index in 0..3 {
1026 let page = doc.page(index).expect("page");
1027 let direct = page.content(&doc).expect("sync content");
1028 let awaited =
1029 block_on(page_content_with(Immediate(&doc), &page)).expect("async content");
1030 assert_eq!(direct, awaited, "page {index}");
1031 saw_content |= !direct.is_empty();
1032 }
1033 assert!(
1035 saw_content,
1036 "fixture must give at least one page real content"
1037 );
1038 }
1039
1040 #[test]
1048 fn image_codec_page_contents_are_refused_not_returned() {
1049 for codec in ["JPXDecode", "DCTDecode"] {
1050 let mut b = PdfBuilder::new();
1051 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1052 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1053 b.object(
1054 3,
1055 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents 4 0 R >>",
1056 );
1057 b.stream(
1058 4,
1059 &format!("/Filter /{codec}"),
1060 b"1 0 0 rg 0 0 100 100 re f",
1061 );
1062 let doc = Document::load(b.build(1)).expect("load");
1063 let page = doc.page(0).expect("page");
1064 match page.content(&doc) {
1065 Err(Error::UnsupportedFilter(n)) => assert_eq!(n, codec),
1066 other => panic!("{codec} content must be refused, got {other:?}"),
1067 }
1068 }
1069 }
1070
1071 #[test]
1078 fn an_unreadable_filter_refuses_the_content_not_returns_it_raw() {
1079 let mut b = PdfBuilder::new();
1080 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1081 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1082 b.object(
1083 3,
1084 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents 4 0 R >>",
1085 );
1086 b.stream(4, "/Filter 6 0 R", b"1 0 0 rg 0 0 100 100 re f");
1087 b.object(6, "7 0 R");
1088 b.object(7, "6 0 R");
1089 let doc = Document::load(b.build(1)).expect("load");
1090 let page = doc.page(0).expect("page");
1091 assert!(
1092 page.content(&doc).is_err(),
1093 "an unreadable /Filter must refuse, not pass stored bytes"
1094 );
1095 }
1096
1097 #[test]
1102 fn a_shared_helper_is_reachable_from_a_caller_that_owns_its_source() {
1103 let doc = Document::load(simple_doc("Hello")).expect("load");
1104 let page = doc.page(0).expect("page");
1105
1106 let owner = Immediate(&doc);
1107 let through_reference = block_on(page_content_with(&owner, &page)).expect("async content");
1108
1109 assert_eq!(through_reference, page.content(&doc).expect("sync content"));
1110 assert!(!through_reference.is_empty());
1111 }
1112
1113 #[test]
1117 fn the_seed_is_shareable_across_threads() {
1118 fn assert_send_sync<T: Send + Sync>() {}
1119 assert_send_sync::<DocumentSeed>();
1120 }
1121
1122 #[test]
1126 fn a_fork_reads_exactly_what_its_parent_reads() {
1127 let doc = Document::load(pdfboss_testkit::encrypted_rc4_doc("forked secret")).unwrap();
1128 let fork = doc.fork();
1129 assert_eq!(fork.page_count(), doc.page_count());
1130 let (a, b) = (doc.page(0).unwrap(), fork.page(0).unwrap());
1131 assert_eq!(a.media_box, b.media_box);
1132 assert_eq!(
1133 a.content(&doc).expect("parent content"),
1134 b.content(&fork).expect("fork content"),
1135 "decrypted content agrees"
1136 );
1137 }
1138
1139 #[test]
1143 fn map_pages_preserves_page_order() {
1144 let doc = Document::load(multi_page_doc(&["one", "two", "three"])).unwrap();
1145 let contents = map_pages(&doc, |doc, page| page.content(doc));
1146 assert_eq!(contents.len(), 3);
1147 let texts: Vec<String> = contents
1148 .into_iter()
1149 .map(|c| String::from_utf8_lossy(&c.unwrap()).into_owned())
1150 .collect();
1151 assert!(texts[0].contains("(one)"), "{}", texts[0]);
1152 assert!(texts[1].contains("(two)"), "{}", texts[1]);
1153 assert!(texts[2].contains("(three)"), "{}", texts[2]);
1154 }
1155
1156 fn replace_once(data: &[u8], from: &[u8], to: &[u8]) -> Vec<u8> {
1159 let pos = memchr::memmem::find(data, from).expect("pattern present in fixture");
1160 let mut out = Vec::with_capacity(data.len() - from.len() + to.len());
1161 out.extend_from_slice(&data[..pos]);
1162 out.extend_from_slice(to);
1163 out.extend_from_slice(&data[pos + from.len()..]);
1164 out
1165 }
1166
1167 fn contains(haystack: &[u8], needle: &[u8]) -> bool {
1168 memchr::memmem::find(haystack, needle).is_some()
1169 }
1170
1171 const FONT: &str = "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>";
1172
1173 #[test]
1174 fn loads_simple_doc() {
1175 let doc = Document::load(simple_doc("Greetings, cosmos!")).unwrap();
1176 assert_eq!(doc.version(), (1, 7));
1177 assert_eq!(doc.page_count(), 1);
1178 let page = doc.page(0).unwrap();
1179 assert_eq!(page.index, 0);
1180 assert_eq!(page.media_box, Rect::new(0.0, 0.0, 612.0, 792.0));
1181 assert_eq!(page.crop_box, page.media_box);
1182 assert_eq!(page.rotate, 0);
1183 assert_eq!(page.size(), (612.0, 792.0));
1184 assert!(page.resources.get("Font").is_some());
1185 let content = page.content(&doc).unwrap();
1186 assert!(contains(&content, b"Greetings, cosmos!"));
1187 }
1188
1189 #[test]
1190 fn multi_page_ordering() {
1191 let doc = Document::load(multi_page_doc(&["alpha", "beta", "gamma"])).unwrap();
1192 assert_eq!(doc.page_count(), 3);
1193 for (i, text) in ["alpha", "beta", "gamma"].iter().enumerate() {
1194 let content = doc.page(i).unwrap().content(&doc).unwrap();
1195 assert!(
1196 contains(&content, text.as_bytes()),
1197 "page {i} should show {text}"
1198 );
1199 }
1200 }
1201
1202 #[test]
1203 fn page_index_out_of_bounds() {
1204 let doc = Document::load(simple_doc("x")).unwrap();
1205 assert!(matches!(doc.page(5), Err(Error::PageNotFound(5, 1))));
1206 }
1207
1208 #[test]
1209 fn open_reads_from_disk() {
1210 let dir = std::env::temp_dir();
1211 let path = dir.join(format!("pdfboss-doc-test-{}.pdf", std::process::id()));
1212 std::fs::write(&path, simple_doc("from disk")).unwrap();
1213 let doc = Document::open(&path).unwrap();
1214 std::fs::remove_file(&path).ok();
1215 assert_eq!(doc.page_count(), 1);
1216 let content = doc.page(0).unwrap().content(&doc).unwrap();
1217 assert!(contains(&content, b"from disk"));
1218 assert!(matches!(
1219 Document::open(dir.join("pdfboss-doc-test-missing.pdf")),
1220 Err(Error::Io(_))
1221 ));
1222 }
1223
1224 #[test]
1225 fn encrypt_in_trailer_is_rejected() {
1226 let data = replace_once(
1227 &simple_doc("secret"),
1228 b"trailer\n<< /Size",
1229 b"trailer\n<< /Encrypt 9 0 R /Size",
1230 );
1231 assert!(matches!(Document::load(data), Err(Error::Encrypted)));
1232 }
1233
1234 #[test]
1235 fn metadata_utf16be_round_trip() {
1236 let mut b = PdfBuilder::new();
1237 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1238 b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
1239 b.object(6, "<< /Title <FEFF00480151> /Author (plain author) >>");
1241 let data = replace_once(&b.build(1), b"<< /Size", b"<< /Info 6 0 R /Size");
1242 let doc = Document::load(data).unwrap();
1243 let meta = doc.metadata();
1244 assert_eq!(meta.title.as_deref(), Some("H\u{151}"));
1245 assert_eq!(meta.author.as_deref(), Some("plain author"));
1246 assert_eq!(meta.subject, None);
1247 assert_eq!(meta.keywords, None);
1248 assert_eq!(meta.creation_date, None);
1249 }
1250
1251 #[test]
1252 fn metadata_without_info_is_all_none() {
1253 let doc = Document::load(simple_doc("x")).unwrap();
1254 assert_eq!(doc.metadata(), Metadata::default());
1255 }
1256
1257 #[test]
1258 fn missing_object_resolves_to_null() {
1259 let doc = Document::load(simple_doc("x")).unwrap();
1260 let missing = Object::Ref(ObjRef { num: 99, gen: 0 });
1261 assert_eq!(doc.resolve(&missing).unwrap(), Object::Null);
1262 assert!(matches!(
1263 doc.get(ObjRef { num: 99, gen: 0 }),
1264 Err(Error::ObjectNotFound(99, 0))
1265 ));
1266 }
1267
1268 #[test]
1269 fn self_reference_is_circular() {
1270 let mut b = PdfBuilder::new();
1271 b.object(1, "<< /Type /Catalog >>");
1272 b.object(6, "6 0 R");
1273 let doc = Document::load(b.build(1)).unwrap();
1274 let loops = Object::Ref(ObjRef { num: 6, gen: 0 });
1275 assert!(matches!(
1276 doc.resolve(&loops),
1277 Err(Error::CircularReference(6))
1278 ));
1279 }
1280
1281 #[test]
1282 fn generation_mismatch_is_tolerated() {
1283 let doc = Document::load(simple_doc("x")).unwrap();
1284 let catalog = doc.get(ObjRef { num: 1, gen: 7 }).unwrap();
1285 let dict = catalog.as_dict().unwrap();
1286 assert_eq!(dict.get_name("Type").map(|n| n.0.as_str()), Some("Catalog"));
1287 }
1288
1289 #[test]
1290 fn objects_in_object_streams_are_fetched() {
1291 let mut b = PdfBuilder::new();
1292 let (dict, payload) =
1293 objstm_payload(&[(1, "<< /Type /Catalog /Pages 2 0 R >>"), (5, FONT)]);
1294 b.stream(6, &dict, &payload);
1295 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1296 b.object(
1297 3,
1298 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1299 /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
1300 );
1301 b.stream(4, "", b"BT /F1 12 Tf (compressed hello) Tj ET");
1302 let doc = Document::load(b.build_xref_stream(1)).unwrap();
1303 assert_eq!(doc.page_count(), 1);
1304 let page = doc.page(0).unwrap();
1305 assert!(contains(&page.content(&doc).unwrap(), b"compressed hello"));
1306 let font = doc.get(ObjRef { num: 5, gen: 0 }).unwrap();
1307 assert_eq!(
1308 font.as_dict()
1309 .and_then(|d| d.get_name("BaseFont"))
1310 .map(|n| n.0.as_str()),
1311 Some("Helvetica")
1312 );
1313 }
1314
1315 #[test]
1316 fn contents_array_is_joined_with_newlines() {
1317 let mut b = PdfBuilder::new();
1318 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1319 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1320 b.object(
1321 3,
1322 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1323 /Contents [4 0 R null 5 0 R] >>",
1324 );
1325 b.stream(4, "", b"q");
1326 b.stream(5, "", b"Q");
1327 let doc = Document::load(b.build(1)).unwrap();
1328 let content = doc.page(0).unwrap().content(&doc).unwrap();
1329 assert_eq!(content, b"q\nQ", "streams joined by \\n, null skipped");
1330 }
1331
1332 #[test]
1333 fn inheritance_from_pages_node_and_rotate_swap() {
1334 let mut b = PdfBuilder::new();
1335 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1336 b.object(
1337 2,
1338 "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 \
1339 /Resources << /Font << /F1 5 0 R >> >> /MediaBox [0 0 400 600] >>",
1340 );
1341 b.object(3, "<< /Type /Page /Parent 2 0 R >>");
1342 b.object(4, "<< /Type /Page /Parent 2 0 R /Rotate 270 >>");
1343 b.object(5, FONT);
1344 let doc = Document::load(b.build(1)).unwrap();
1345 assert_eq!(doc.page_count(), 2);
1346
1347 let first = doc.page(0).unwrap();
1348 assert_eq!(first.media_box, Rect::new(0.0, 0.0, 400.0, 600.0));
1349 assert_eq!(first.crop_box, first.media_box);
1350 assert!(first.resources.get("Font").is_some(), "inherited resources");
1351 assert_eq!(first.rotate, 0);
1352 assert!(
1353 first.content(&doc).unwrap().is_empty(),
1354 "no /Contents means empty content"
1355 );
1356 assert_eq!(first.size(), (400.0, 600.0));
1357
1358 let second = doc.page(1).unwrap();
1359 assert_eq!(second.rotate, 270);
1360 assert_eq!(second.size(), (600.0, 400.0), "rotate 270 swaps w/h");
1361 }
1362
1363 #[test]
1364 fn crop_box_intersected_and_rotate_normalized() {
1365 let mut b = PdfBuilder::new();
1366 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1367 b.object(2, "<< /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 >>");
1368 b.object(
1369 3,
1370 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] \
1371 /CropBox [100 100 400 400] /Rotate 450 >>",
1372 );
1373 b.object(4, "<< /Type /Page /Parent 2 0 R /Rotate -90 >>");
1374 b.object(
1375 5,
1376 "<< /Type /Page /Parent 2 0 R /Rotate 45 /MediaBox [0 0 0 0] >>",
1377 );
1378 let doc = Document::load(b.build(1)).unwrap();
1379
1380 let clipped = doc.page(0).unwrap();
1381 assert_eq!(clipped.crop_box, Rect::new(100.0, 100.0, 200.0, 200.0));
1382 assert_eq!(clipped.rotate, 90, "450 normalizes to 90");
1383 assert_eq!(clipped.size(), (100.0, 100.0));
1384
1385 assert_eq!(doc.page(1).unwrap().rotate, 270, "-90 normalizes to 270");
1386 let odd = doc.page(2).unwrap();
1387 assert_eq!(odd.rotate, 0, "non-multiple of 90 falls back to 0");
1388 assert_eq!(
1389 odd.media_box,
1390 Page::US_LETTER,
1391 "degenerate media box defaults"
1392 );
1393 }
1394
1395 #[test]
1396 fn kids_cycle_truncates_without_hanging() {
1397 let mut b = PdfBuilder::new();
1398 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1399 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1401 b.object(3, "<< /Type /Pages /Kids [4 0 R 2 0 R] /Count 1 >>");
1402 b.object(
1403 4,
1404 "<< /Type /Page /Parent 3 0 R /MediaBox [0 0 100 100] /Contents 5 0 R >>",
1405 );
1406 b.stream(5, "", b"0 0 50 50 re f");
1407 let doc = Document::load(b.build(1)).unwrap();
1408 assert_eq!(doc.page_count(), 1, "cycle back-edge yields no extra pages");
1409 assert!(contains(
1410 &doc.page(0).unwrap().content(&doc).unwrap(),
1411 b"re f"
1412 ));
1413 }
1414
1415 #[test]
1416 fn tree_depth_is_capped() {
1417 let mut b = PdfBuilder::new();
1418 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1419 let last = 302u32;
1421 for num in 2..last {
1422 b.object(
1423 num,
1424 &format!("<< /Type /Pages /Kids [{} 0 R] /Count 1 >>", num + 1),
1425 );
1426 }
1427 b.object(last, "<< /Type /Page >>");
1428 let doc = Document::load(b.build(1)).unwrap();
1429 assert_eq!(doc.page_count(), 1, "declared /Count is reported cheaply");
1434 assert!(
1435 matches!(doc.page(0), Err(Error::PageNotFound(0, 0))),
1436 "leaf beyond the depth cap cannot be materialized"
1437 );
1438 }
1439
1440 #[test]
1441 fn page_count_reports_declared_count_cheaply() {
1442 let mut b = PdfBuilder::new();
1446 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1447 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 5 >>");
1448 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
1449 let doc = Document::load(b.build(1)).unwrap();
1450 assert_eq!(doc.page_count(), 5, "declared /Count reported verbatim");
1451 assert!(doc.page(0).is_ok(), "the one real page materializes");
1452 assert!(
1453 matches!(doc.page(1), Err(Error::PageNotFound(1, 1))),
1454 "access past the real pages fails with the true length"
1455 );
1456 }
1457
1458 #[test]
1459 fn page_count_falls_back_to_walk_when_count_absent() {
1460 let mut b = PdfBuilder::new();
1461 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1462 b.object(2, "<< /Type /Pages /Kids [3 0 R] >>"); b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
1464 let doc = Document::load(b.build(1)).unwrap();
1465 assert_eq!(
1466 doc.page_count(),
1467 1,
1468 "missing /Count is recovered by walking"
1469 );
1470 }
1471
1472 #[test]
1473 fn page_count_ignores_corrupt_oversized_count() {
1474 let mut b = PdfBuilder::new();
1477 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1478 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 999999999 >>");
1479 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
1480 let doc = Document::load(b.build(1)).unwrap();
1481 assert_eq!(doc.page_count(), 1, "implausible /Count is rejected");
1482 }
1483
1484 #[test]
1485 fn version_scan_and_default() {
1486 let mut b = PdfBuilder::new().version(2, 0);
1487 b.object(1, "<< /Type /Catalog >>");
1488 assert_eq!(Document::load(b.build(1)).unwrap().version(), (2, 0));
1489 let data = replace_once(&simple_doc("v"), b"%PDF-", b"%QQQ-");
1491 assert_eq!(Document::load(data).unwrap().version(), (1, 4));
1492 }
1493
1494 #[test]
1495 fn deeply_nested_root_object_does_not_overflow_the_stack() {
1496 let mut data = b"%PDF-1.7\n1 0 obj\n".to_vec();
1501 data.extend(std::iter::repeat_n(b'[', 50_000));
1502 data.extend(std::iter::repeat_n(b']', 50_000));
1503 data.extend_from_slice(b"\nendobj\ntrailer\n<</Root 1 0 R>>\n%%EOF\n");
1504 let outcome = std::thread::Builder::new()
1505 .stack_size(1024 * 1024)
1506 .spawn(move || Document::load(data).map(|doc| doc.page_count()))
1507 .expect("spawn test thread")
1508 .join()
1509 .expect("Document::load must not overflow the stack");
1510 assert!(matches!(outcome, Ok(0) | Err(_)));
1513 }
1514
1515 #[test]
1516 fn bytes_and_xref_accessors() {
1517 let data = simple_doc("accessors");
1518 let doc = Document::load(data.clone()).unwrap();
1519 assert_eq!(doc.bytes(), &data[..]);
1520 assert!(!doc.xref().is_empty());
1521 assert!(doc.xref().trailer.get("Root").is_some());
1522 }
1523
1524 #[test]
1525 fn object_at_spanned_reparses_identically() {
1526 let data = simple_doc("spanned");
1527 let doc = Document::load(data).unwrap();
1528 for (num, entry) in doc.xref().iter() {
1529 let XrefEntry::InFile { offset, gen } = entry else {
1530 continue;
1531 };
1532 let (r, object, span) = doc.object_at_spanned(offset as usize).unwrap();
1533 assert_eq!(r.num, num);
1534 assert_eq!(r.gen, gen);
1535 assert_eq!(span.start, offset);
1536 assert!(span.end as usize <= doc.bytes().len());
1537 let slice = &doc.bytes()[span.start as usize..span.end as usize];
1539 let (r2, object2) = Parser::new(slice).parse_indirect(&NoResolve).unwrap();
1540 assert_eq!(r2, r);
1541 assert_eq!(object2, object);
1542 }
1543 }
1544
1545 #[test]
1546 fn page_object_ref_points_at_a_page_dict() {
1547 let doc = Document::load(multi_page_doc(&["one", "two"])).unwrap();
1548 for index in 0..doc.page_count() {
1549 let page = doc.page(index).unwrap();
1550 let r = page.object_ref().expect("builder pages are indirect");
1551 let resolved = doc.get(r).unwrap();
1552 assert_eq!(
1553 resolved
1554 .as_dict()
1555 .unwrap()
1556 .get_name("Type")
1557 .map(|n| n.0.as_str()),
1558 Some("Page")
1559 );
1560 }
1561 }
1562
1563 const RC4_FIXTURE_KEY_LEN: usize = 16; const RC4_FIXTURE_P: i32 = -44;
1573 const RC4_FIXTURE_ID0: &[u8] = b"0123456789abcdef";
1574 const RC4_FIXTURE_PAD: [u8; 32] = [
1575 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01,
1576 0x08, 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53,
1577 0x69, 0x7A,
1578 ];
1579
1580 #[rustfmt::skip]
1581 const RC4_FIXTURE_MD5_S: [u32; 64] = [
1582 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
1583 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
1584 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
1585 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
1586 ];
1587 #[rustfmt::skip]
1588 const RC4_FIXTURE_MD5_K: [u32; 64] = [
1589 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
1590 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
1591 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
1592 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
1593 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
1594 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
1595 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
1596 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,
1597 ];
1598
1599 fn rc4_fixture_md5(input: &[u8]) -> [u8; 16] {
1600 let (mut a0, mut b0, mut c0, mut d0) = (
1601 0x6745_2301u32,
1602 0xefcd_ab89u32,
1603 0x98ba_dcfeu32,
1604 0x1032_5476u32,
1605 );
1606 let mut msg = input.to_vec();
1607 let bitlen = (input.len() as u64).wrapping_mul(8);
1608 msg.push(0x80);
1609 while msg.len() % 64 != 56 {
1610 msg.push(0);
1611 }
1612 msg.extend_from_slice(&bitlen.to_le_bytes());
1613 for chunk in msg.as_chunks::<64>().0 {
1614 let mut m = [0u32; 16];
1615 for (word, bytes) in m.iter_mut().zip(chunk.as_chunks::<4>().0) {
1616 *word = u32::from_le_bytes(*bytes);
1617 }
1618 let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0);
1619 for i in 0..64 {
1620 let (f, g) = match i {
1621 0..=15 => ((b & c) | (!b & d), i),
1622 16..=31 => ((d & b) | (!d & c), (5 * i + 1) % 16),
1623 32..=47 => (b ^ c ^ d, (3 * i + 5) % 16),
1624 _ => (c ^ (b | !d), (7 * i) % 16),
1625 };
1626 let f = f
1627 .wrapping_add(a)
1628 .wrapping_add(RC4_FIXTURE_MD5_K[i])
1629 .wrapping_add(m[g]);
1630 a = d;
1631 d = c;
1632 c = b;
1633 b = b.wrapping_add(f.rotate_left(RC4_FIXTURE_MD5_S[i]));
1634 }
1635 a0 = a0.wrapping_add(a);
1636 b0 = b0.wrapping_add(b);
1637 c0 = c0.wrapping_add(c);
1638 d0 = d0.wrapping_add(d);
1639 }
1640 let mut out = [0u8; 16];
1641 out[0..4].copy_from_slice(&a0.to_le_bytes());
1642 out[4..8].copy_from_slice(&b0.to_le_bytes());
1643 out[8..12].copy_from_slice(&c0.to_le_bytes());
1644 out[12..16].copy_from_slice(&d0.to_le_bytes());
1645 out
1646 }
1647
1648 fn rc4_fixture_rc4(key: &[u8], data: &[u8]) -> Vec<u8> {
1649 let mut s: [u8; 256] = core::array::from_fn(|i| i as u8);
1650 let mut j = 0u8;
1651 for i in 0..256 {
1652 j = j.wrapping_add(s[i]).wrapping_add(key[i % key.len()]);
1653 s.swap(i, j as usize);
1654 }
1655 let mut out = Vec::with_capacity(data.len());
1656 let (mut i, mut j) = (0u8, 0u8);
1657 for &byte in data {
1658 i = i.wrapping_add(1);
1659 j = j.wrapping_add(s[i as usize]);
1660 s.swap(i as usize, j as usize);
1661 let k = s[s[i as usize].wrapping_add(s[j as usize]) as usize];
1662 out.push(byte ^ k);
1663 }
1664 out
1665 }
1666
1667 fn rc4_fixture_owner_entry() -> Vec<u8> {
1669 let mut d = rc4_fixture_md5(&RC4_FIXTURE_PAD);
1670 for _ in 0..50 {
1671 d = rc4_fixture_md5(&d[..RC4_FIXTURE_KEY_LEN]);
1672 }
1673 let rc4key = d[..RC4_FIXTURE_KEY_LEN].to_vec();
1674 let mut o = rc4_fixture_rc4(&rc4key, &RC4_FIXTURE_PAD);
1675 for i in 1u8..=19 {
1676 let k: Vec<u8> = rc4key.iter().map(|b| b ^ i).collect();
1677 o = rc4_fixture_rc4(&k, &o);
1678 }
1679 o
1680 }
1681
1682 fn rc4_fixture_file_key(o: &[u8]) -> Vec<u8> {
1684 let mut input = Vec::new();
1685 input.extend_from_slice(&RC4_FIXTURE_PAD);
1686 input.extend_from_slice(o);
1687 input.extend_from_slice(&(RC4_FIXTURE_P as u32).to_le_bytes());
1688 input.extend_from_slice(RC4_FIXTURE_ID0);
1689 let mut d = rc4_fixture_md5(&input);
1690 for _ in 0..50 {
1691 d = rc4_fixture_md5(&d[..RC4_FIXTURE_KEY_LEN]);
1692 }
1693 d[..RC4_FIXTURE_KEY_LEN].to_vec()
1694 }
1695
1696 fn rc4_fixture_user_entry(key: &[u8]) -> Vec<u8> {
1698 let mut input = Vec::new();
1699 input.extend_from_slice(&RC4_FIXTURE_PAD);
1700 input.extend_from_slice(RC4_FIXTURE_ID0);
1701 let mut x = rc4_fixture_md5(&input).to_vec();
1702 x = rc4_fixture_rc4(key, &x);
1703 for i in 1u8..=19 {
1704 let k: Vec<u8> = key.iter().map(|b| b ^ i).collect();
1705 x = rc4_fixture_rc4(&k, &x);
1706 }
1707 x.resize(32, 0); x
1709 }
1710
1711 fn rc4_fixture_obj_key(key: &[u8], num: u32, gen: u16) -> Vec<u8> {
1712 let mut input = key.to_vec();
1713 input.extend_from_slice(&num.to_le_bytes()[..3]);
1714 input.extend_from_slice(&gen.to_le_bytes()[..2]);
1715 rc4_fixture_md5(&input)[..(key.len() + 5).min(16)].to_vec()
1716 }
1717
1718 fn rc4_fixture_hexstr(b: &[u8]) -> String {
1719 let mut s = String::from("<");
1720 for x in b {
1721 s.push_str(&format!("{x:02x}"));
1722 }
1723 s.push('>');
1724 s
1725 }
1726
1727 fn rc4_encrypted_fixture() -> Vec<u8> {
1731 let o = rc4_fixture_owner_entry();
1732 let key = rc4_fixture_file_key(&o);
1733 let u = rc4_fixture_user_entry(&key);
1734 let msg = rc4_fixture_rc4(&rc4_fixture_obj_key(&key, 3, 0), b"Top secret message");
1735
1736 let mut b = PdfBuilder::new().version(1, 4);
1737 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1738 b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
1739 b.object(3, &format!("<< /Msg {} >>", rc4_fixture_hexstr(&msg)));
1740 b.object(
1741 9,
1742 &format!(
1743 "<< /Filter /Standard /V 2 /R 3 /Length 128 /P {} /O {} /U {} >>",
1744 RC4_FIXTURE_P,
1745 rc4_fixture_hexstr(&o),
1746 rc4_fixture_hexstr(&u)
1747 ),
1748 );
1749 let trailer = format!(
1750 "/Encrypt 9 0 R /ID [{}{}]",
1751 rc4_fixture_hexstr(RC4_FIXTURE_ID0),
1752 rc4_fixture_hexstr(RC4_FIXTURE_ID0)
1753 );
1754 b.trailer_extra(&trailer).build(1)
1755 }
1756
1757 #[test]
1758 fn encrypted_generation_mismatch_still_decrypts() {
1759 let doc = Document::load(rc4_encrypted_fixture()).expect("empty password opens the file");
1769 let obj3 = doc.get(ObjRef { num: 3, gen: 7 }).unwrap();
1770 let msg = obj3
1771 .as_dict()
1772 .unwrap()
1773 .get("Msg")
1774 .unwrap()
1775 .as_str_bytes()
1776 .unwrap();
1777 assert_eq!(
1778 msg, b"Top secret message",
1779 "decrypted using the file's real gen (0), not the mismatched request (7)"
1780 );
1781 }
1782
1783 #[test]
1784 fn objstm_doc_fixture_loads_and_resolves_members() {
1785 let data = objstm_doc(&[(7, "<< /Marker (inside) >>")]);
1786 let doc = Document::load(data).unwrap();
1787 assert_eq!(doc.page_count(), 1);
1788 let member = doc.get(ObjRef { num: 7, gen: 0 }).unwrap();
1789 let text = member.as_dict().unwrap().get("Marker").unwrap();
1790 assert_eq!(text.as_str_bytes(), Some(&b"inside"[..]));
1791 assert!(matches!(
1793 doc.xref().get(7),
1794 Some(XrefEntry::InStream { stream_num: 4, .. })
1795 ));
1796 }
1797
1798 #[test]
1801 fn page_from_parts_exposes_its_accessors() {
1802 let mut dict = Dict::default();
1803 dict.insert(Name("Type".into()), Object::Name(Name("Page".into())));
1804 let media = Rect::new(0.0, 0.0, 200.0, 400.0);
1805 let obj_ref = ObjRef { num: 7, gen: 0 };
1806
1807 let page = Page::from_parts(
1808 3,
1809 media,
1810 media,
1811 90,
1812 Dict::default(),
1813 dict.clone(),
1814 Some(obj_ref),
1815 );
1816
1817 assert_eq!(page.index, 3);
1818 assert_eq!(page.object_ref(), Some(obj_ref));
1819 assert_eq!(page.dict(), &dict);
1820 assert_eq!(page.size(), (400.0, 200.0));
1822 }
1823
1824 #[test]
1828 fn page_from_parts_normalizes_rotation() {
1829 let media = Rect::new(0.0, 0.0, 200.0, 400.0);
1830 let build = |rotate: i32| {
1831 Page::from_parts(
1832 0,
1833 media,
1834 media,
1835 rotate,
1836 Dict::default(),
1837 Dict::default(),
1838 None,
1839 )
1840 };
1841
1842 for (given, canonical) in [(-90, 270), (450, 90), (540, 180), (720, 0), (-360, 0)] {
1843 let page = build(given);
1844 assert_eq!(
1845 page.rotate, canonical,
1846 "from_parts must normalize /Rotate {given} to {canonical}"
1847 );
1848 assert_eq!(
1849 page.size(),
1850 build(canonical).size(),
1851 "/Rotate {given} must report the same size as {canonical}"
1852 );
1853 }
1854 }
1855
1856 #[test]
1861 fn page_size_handles_unnormalized_rotation() {
1862 let media = Rect::new(0.0, 0.0, 200.0, 400.0);
1863 let mut page = Page::from_parts(0, media, media, 0, Dict::default(), Dict::default(), None);
1864
1865 for rotate in [90, 270, -90, -270, 450, 630] {
1866 page.rotate = rotate;
1867 assert_eq!(
1868 page.size(),
1869 (400.0, 200.0),
1870 "/Rotate {rotate} is a quarter turn and must swap the size"
1871 );
1872 }
1873 for rotate in [0, 180, -180, 360, 540, 720] {
1874 page.rotate = rotate;
1875 assert_eq!(
1876 page.size(),
1877 (200.0, 400.0),
1878 "/Rotate {rotate} is a half turn and must leave the size alone"
1879 );
1880 }
1881 }
1882}