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) => content_stream_data_with(&src, s).await,
918 Object::Array(items) => {
919 let mut out = Vec::new();
920 let mut first = true;
921 for item in &items {
922 let part = src.resolve(item).await?;
923 let Some(stream) = part.as_stream() else {
924 continue; };
926 if !first {
927 out.push(b'\n');
928 }
929 out.extend_from_slice(&content_stream_data_with(&src, stream).await?);
930 first = false;
931 }
932 Ok(out)
933 }
934 _ => Ok(Vec::new()),
935 }
936}
937
938pub async fn decoded_stream_data_with<S: AsyncObjectSource>(src: S, s: &Stream) -> Result<Vec<u8>> {
971 if let Some(name) = filters::trailing_filter_checked_with(&src, &s.dict).await? {
972 if filters::is_image_codec(&name.0) {
973 return Err(Error::UnsupportedFilter(name.0));
974 }
975 }
976 src.stream_data(s).await
977}
978
979pub async fn content_stream_data_with<S: AsyncObjectSource>(src: S, s: &Stream) -> Result<Vec<u8>> {
989 decoded_stream_data_with(src, s).await
990}
991
992#[cfg(test)]
993mod tests {
994 use super::*;
995 use crate::object::Name;
996 use crate::parser::{NoResolve, Parser};
997 use crate::xref::XrefEntry;
998 use pdfboss_testkit::{multi_page_doc, objstm_doc, objstm_payload, simple_doc, PdfBuilder};
999
1000 #[test]
1004 fn async_page_content_matches_the_sync_accessor() {
1005 let doc = Document::load(multi_page_doc(&["one", "two", "three"])).expect("load");
1006 let mut saw_content = false;
1007 for index in 0..3 {
1008 let page = doc.page(index).expect("page");
1009 let direct = page.content(&doc).expect("sync content");
1010 let awaited =
1011 block_on(page_content_with(Immediate(&doc), &page)).expect("async content");
1012 assert_eq!(direct, awaited, "page {index}");
1013 saw_content |= !direct.is_empty();
1014 }
1015 assert!(
1017 saw_content,
1018 "fixture must give at least one page real content"
1019 );
1020 }
1021
1022 #[test]
1030 fn image_codec_page_contents_are_refused_not_returned() {
1031 for codec in ["JPXDecode", "DCTDecode"] {
1032 let mut b = PdfBuilder::new();
1033 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1034 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1035 b.object(
1036 3,
1037 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents 4 0 R >>",
1038 );
1039 b.stream(
1040 4,
1041 &format!("/Filter /{codec}"),
1042 b"1 0 0 rg 0 0 100 100 re f",
1043 );
1044 let doc = Document::load(b.build(1)).expect("load");
1045 let page = doc.page(0).expect("page");
1046 match page.content(&doc) {
1047 Err(Error::UnsupportedFilter(n)) => assert_eq!(n, codec),
1048 other => panic!("{codec} content must be refused, got {other:?}"),
1049 }
1050 }
1051 }
1052
1053 #[test]
1060 fn an_unreadable_filter_refuses_the_content_not_returns_it_raw() {
1061 let mut b = PdfBuilder::new();
1062 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1063 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1064 b.object(
1065 3,
1066 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents 4 0 R >>",
1067 );
1068 b.stream(4, "/Filter 6 0 R", b"1 0 0 rg 0 0 100 100 re f");
1069 b.object(6, "7 0 R");
1070 b.object(7, "6 0 R");
1071 let doc = Document::load(b.build(1)).expect("load");
1072 let page = doc.page(0).expect("page");
1073 assert!(
1074 page.content(&doc).is_err(),
1075 "an unreadable /Filter must refuse, not pass stored bytes"
1076 );
1077 }
1078
1079 #[test]
1084 fn a_shared_helper_is_reachable_from_a_caller_that_owns_its_source() {
1085 let doc = Document::load(simple_doc("Hello")).expect("load");
1086 let page = doc.page(0).expect("page");
1087
1088 let owner = Immediate(&doc);
1089 let through_reference = block_on(page_content_with(&owner, &page)).expect("async content");
1090
1091 assert_eq!(through_reference, page.content(&doc).expect("sync content"));
1092 assert!(!through_reference.is_empty());
1093 }
1094
1095 #[test]
1099 fn the_seed_is_shareable_across_threads() {
1100 fn assert_send_sync<T: Send + Sync>() {}
1101 assert_send_sync::<DocumentSeed>();
1102 }
1103
1104 #[test]
1108 fn a_fork_reads_exactly_what_its_parent_reads() {
1109 let doc = Document::load(pdfboss_testkit::encrypted_rc4_doc("forked secret")).unwrap();
1110 let fork = doc.fork();
1111 assert_eq!(fork.page_count(), doc.page_count());
1112 let (a, b) = (doc.page(0).unwrap(), fork.page(0).unwrap());
1113 assert_eq!(a.media_box, b.media_box);
1114 assert_eq!(
1115 a.content(&doc).expect("parent content"),
1116 b.content(&fork).expect("fork content"),
1117 "decrypted content agrees"
1118 );
1119 }
1120
1121 #[test]
1125 fn map_pages_preserves_page_order() {
1126 let doc = Document::load(multi_page_doc(&["one", "two", "three"])).unwrap();
1127 let contents = map_pages(&doc, |doc, page| page.content(doc));
1128 assert_eq!(contents.len(), 3);
1129 let texts: Vec<String> = contents
1130 .into_iter()
1131 .map(|c| String::from_utf8_lossy(&c.unwrap()).into_owned())
1132 .collect();
1133 assert!(texts[0].contains("(one)"), "{}", texts[0]);
1134 assert!(texts[1].contains("(two)"), "{}", texts[1]);
1135 assert!(texts[2].contains("(three)"), "{}", texts[2]);
1136 }
1137
1138 fn replace_once(data: &[u8], from: &[u8], to: &[u8]) -> Vec<u8> {
1141 let pos = memchr::memmem::find(data, from).expect("pattern present in fixture");
1142 let mut out = Vec::with_capacity(data.len() - from.len() + to.len());
1143 out.extend_from_slice(&data[..pos]);
1144 out.extend_from_slice(to);
1145 out.extend_from_slice(&data[pos + from.len()..]);
1146 out
1147 }
1148
1149 fn contains(haystack: &[u8], needle: &[u8]) -> bool {
1150 memchr::memmem::find(haystack, needle).is_some()
1151 }
1152
1153 const FONT: &str = "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>";
1154
1155 #[test]
1156 fn loads_simple_doc() {
1157 let doc = Document::load(simple_doc("Greetings, cosmos!")).unwrap();
1158 assert_eq!(doc.version(), (1, 7));
1159 assert_eq!(doc.page_count(), 1);
1160 let page = doc.page(0).unwrap();
1161 assert_eq!(page.index, 0);
1162 assert_eq!(page.media_box, Rect::new(0.0, 0.0, 612.0, 792.0));
1163 assert_eq!(page.crop_box, page.media_box);
1164 assert_eq!(page.rotate, 0);
1165 assert_eq!(page.size(), (612.0, 792.0));
1166 assert!(page.resources.get("Font").is_some());
1167 let content = page.content(&doc).unwrap();
1168 assert!(contains(&content, b"Greetings, cosmos!"));
1169 }
1170
1171 #[test]
1172 fn multi_page_ordering() {
1173 let doc = Document::load(multi_page_doc(&["alpha", "beta", "gamma"])).unwrap();
1174 assert_eq!(doc.page_count(), 3);
1175 for (i, text) in ["alpha", "beta", "gamma"].iter().enumerate() {
1176 let content = doc.page(i).unwrap().content(&doc).unwrap();
1177 assert!(
1178 contains(&content, text.as_bytes()),
1179 "page {i} should show {text}"
1180 );
1181 }
1182 }
1183
1184 #[test]
1185 fn page_index_out_of_bounds() {
1186 let doc = Document::load(simple_doc("x")).unwrap();
1187 assert!(matches!(doc.page(5), Err(Error::PageNotFound(5, 1))));
1188 }
1189
1190 #[test]
1191 fn open_reads_from_disk() {
1192 let dir = std::env::temp_dir();
1193 let path = dir.join(format!("pdfboss-doc-test-{}.pdf", std::process::id()));
1194 std::fs::write(&path, simple_doc("from disk")).unwrap();
1195 let doc = Document::open(&path).unwrap();
1196 std::fs::remove_file(&path).ok();
1197 assert_eq!(doc.page_count(), 1);
1198 let content = doc.page(0).unwrap().content(&doc).unwrap();
1199 assert!(contains(&content, b"from disk"));
1200 assert!(matches!(
1201 Document::open(dir.join("pdfboss-doc-test-missing.pdf")),
1202 Err(Error::Io(_))
1203 ));
1204 }
1205
1206 #[test]
1207 fn encrypt_in_trailer_is_rejected() {
1208 let data = replace_once(
1209 &simple_doc("secret"),
1210 b"trailer\n<< /Size",
1211 b"trailer\n<< /Encrypt 9 0 R /Size",
1212 );
1213 assert!(matches!(Document::load(data), Err(Error::Encrypted)));
1214 }
1215
1216 #[test]
1217 fn metadata_utf16be_round_trip() {
1218 let mut b = PdfBuilder::new();
1219 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1220 b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
1221 b.object(6, "<< /Title <FEFF00480151> /Author (plain author) >>");
1223 let data = replace_once(&b.build(1), b"<< /Size", b"<< /Info 6 0 R /Size");
1224 let doc = Document::load(data).unwrap();
1225 let meta = doc.metadata();
1226 assert_eq!(meta.title.as_deref(), Some("H\u{151}"));
1227 assert_eq!(meta.author.as_deref(), Some("plain author"));
1228 assert_eq!(meta.subject, None);
1229 assert_eq!(meta.keywords, None);
1230 assert_eq!(meta.creation_date, None);
1231 }
1232
1233 #[test]
1234 fn metadata_without_info_is_all_none() {
1235 let doc = Document::load(simple_doc("x")).unwrap();
1236 assert_eq!(doc.metadata(), Metadata::default());
1237 }
1238
1239 #[test]
1240 fn missing_object_resolves_to_null() {
1241 let doc = Document::load(simple_doc("x")).unwrap();
1242 let missing = Object::Ref(ObjRef { num: 99, gen: 0 });
1243 assert_eq!(doc.resolve(&missing).unwrap(), Object::Null);
1244 assert!(matches!(
1245 doc.get(ObjRef { num: 99, gen: 0 }),
1246 Err(Error::ObjectNotFound(99, 0))
1247 ));
1248 }
1249
1250 #[test]
1251 fn self_reference_is_circular() {
1252 let mut b = PdfBuilder::new();
1253 b.object(1, "<< /Type /Catalog >>");
1254 b.object(6, "6 0 R");
1255 let doc = Document::load(b.build(1)).unwrap();
1256 let loops = Object::Ref(ObjRef { num: 6, gen: 0 });
1257 assert!(matches!(
1258 doc.resolve(&loops),
1259 Err(Error::CircularReference(6))
1260 ));
1261 }
1262
1263 #[test]
1264 fn generation_mismatch_is_tolerated() {
1265 let doc = Document::load(simple_doc("x")).unwrap();
1266 let catalog = doc.get(ObjRef { num: 1, gen: 7 }).unwrap();
1267 let dict = catalog.as_dict().unwrap();
1268 assert_eq!(dict.get_name("Type").map(|n| n.0.as_str()), Some("Catalog"));
1269 }
1270
1271 #[test]
1272 fn objects_in_object_streams_are_fetched() {
1273 let mut b = PdfBuilder::new();
1274 let (dict, payload) =
1275 objstm_payload(&[(1, "<< /Type /Catalog /Pages 2 0 R >>"), (5, FONT)]);
1276 b.stream(6, &dict, &payload);
1277 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1278 b.object(
1279 3,
1280 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1281 /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
1282 );
1283 b.stream(4, "", b"BT /F1 12 Tf (compressed hello) Tj ET");
1284 let doc = Document::load(b.build_xref_stream(1)).unwrap();
1285 assert_eq!(doc.page_count(), 1);
1286 let page = doc.page(0).unwrap();
1287 assert!(contains(&page.content(&doc).unwrap(), b"compressed hello"));
1288 let font = doc.get(ObjRef { num: 5, gen: 0 }).unwrap();
1289 assert_eq!(
1290 font.as_dict()
1291 .and_then(|d| d.get_name("BaseFont"))
1292 .map(|n| n.0.as_str()),
1293 Some("Helvetica")
1294 );
1295 }
1296
1297 #[test]
1298 fn contents_array_is_joined_with_newlines() {
1299 let mut b = PdfBuilder::new();
1300 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1301 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1302 b.object(
1303 3,
1304 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1305 /Contents [4 0 R null 5 0 R] >>",
1306 );
1307 b.stream(4, "", b"q");
1308 b.stream(5, "", b"Q");
1309 let doc = Document::load(b.build(1)).unwrap();
1310 let content = doc.page(0).unwrap().content(&doc).unwrap();
1311 assert_eq!(content, b"q\nQ", "streams joined by \\n, null skipped");
1312 }
1313
1314 #[test]
1315 fn inheritance_from_pages_node_and_rotate_swap() {
1316 let mut b = PdfBuilder::new();
1317 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1318 b.object(
1319 2,
1320 "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 \
1321 /Resources << /Font << /F1 5 0 R >> >> /MediaBox [0 0 400 600] >>",
1322 );
1323 b.object(3, "<< /Type /Page /Parent 2 0 R >>");
1324 b.object(4, "<< /Type /Page /Parent 2 0 R /Rotate 270 >>");
1325 b.object(5, FONT);
1326 let doc = Document::load(b.build(1)).unwrap();
1327 assert_eq!(doc.page_count(), 2);
1328
1329 let first = doc.page(0).unwrap();
1330 assert_eq!(first.media_box, Rect::new(0.0, 0.0, 400.0, 600.0));
1331 assert_eq!(first.crop_box, first.media_box);
1332 assert!(first.resources.get("Font").is_some(), "inherited resources");
1333 assert_eq!(first.rotate, 0);
1334 assert!(
1335 first.content(&doc).unwrap().is_empty(),
1336 "no /Contents means empty content"
1337 );
1338 assert_eq!(first.size(), (400.0, 600.0));
1339
1340 let second = doc.page(1).unwrap();
1341 assert_eq!(second.rotate, 270);
1342 assert_eq!(second.size(), (600.0, 400.0), "rotate 270 swaps w/h");
1343 }
1344
1345 #[test]
1346 fn crop_box_intersected_and_rotate_normalized() {
1347 let mut b = PdfBuilder::new();
1348 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1349 b.object(2, "<< /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 >>");
1350 b.object(
1351 3,
1352 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] \
1353 /CropBox [100 100 400 400] /Rotate 450 >>",
1354 );
1355 b.object(4, "<< /Type /Page /Parent 2 0 R /Rotate -90 >>");
1356 b.object(
1357 5,
1358 "<< /Type /Page /Parent 2 0 R /Rotate 45 /MediaBox [0 0 0 0] >>",
1359 );
1360 let doc = Document::load(b.build(1)).unwrap();
1361
1362 let clipped = doc.page(0).unwrap();
1363 assert_eq!(clipped.crop_box, Rect::new(100.0, 100.0, 200.0, 200.0));
1364 assert_eq!(clipped.rotate, 90, "450 normalizes to 90");
1365 assert_eq!(clipped.size(), (100.0, 100.0));
1366
1367 assert_eq!(doc.page(1).unwrap().rotate, 270, "-90 normalizes to 270");
1368 let odd = doc.page(2).unwrap();
1369 assert_eq!(odd.rotate, 0, "non-multiple of 90 falls back to 0");
1370 assert_eq!(
1371 odd.media_box,
1372 Page::US_LETTER,
1373 "degenerate media box defaults"
1374 );
1375 }
1376
1377 #[test]
1378 fn kids_cycle_truncates_without_hanging() {
1379 let mut b = PdfBuilder::new();
1380 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1381 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1383 b.object(3, "<< /Type /Pages /Kids [4 0 R 2 0 R] /Count 1 >>");
1384 b.object(
1385 4,
1386 "<< /Type /Page /Parent 3 0 R /MediaBox [0 0 100 100] /Contents 5 0 R >>",
1387 );
1388 b.stream(5, "", b"0 0 50 50 re f");
1389 let doc = Document::load(b.build(1)).unwrap();
1390 assert_eq!(doc.page_count(), 1, "cycle back-edge yields no extra pages");
1391 assert!(contains(
1392 &doc.page(0).unwrap().content(&doc).unwrap(),
1393 b"re f"
1394 ));
1395 }
1396
1397 #[test]
1398 fn tree_depth_is_capped() {
1399 let mut b = PdfBuilder::new();
1400 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1401 let last = 302u32;
1403 for num in 2..last {
1404 b.object(
1405 num,
1406 &format!("<< /Type /Pages /Kids [{} 0 R] /Count 1 >>", num + 1),
1407 );
1408 }
1409 b.object(last, "<< /Type /Page >>");
1410 let doc = Document::load(b.build(1)).unwrap();
1411 assert_eq!(doc.page_count(), 1, "declared /Count is reported cheaply");
1416 assert!(
1417 matches!(doc.page(0), Err(Error::PageNotFound(0, 0))),
1418 "leaf beyond the depth cap cannot be materialized"
1419 );
1420 }
1421
1422 #[test]
1423 fn page_count_reports_declared_count_cheaply() {
1424 let mut b = PdfBuilder::new();
1428 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1429 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 5 >>");
1430 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
1431 let doc = Document::load(b.build(1)).unwrap();
1432 assert_eq!(doc.page_count(), 5, "declared /Count reported verbatim");
1433 assert!(doc.page(0).is_ok(), "the one real page materializes");
1434 assert!(
1435 matches!(doc.page(1), Err(Error::PageNotFound(1, 1))),
1436 "access past the real pages fails with the true length"
1437 );
1438 }
1439
1440 #[test]
1441 fn page_count_falls_back_to_walk_when_count_absent() {
1442 let mut b = PdfBuilder::new();
1443 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1444 b.object(2, "<< /Type /Pages /Kids [3 0 R] >>"); b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
1446 let doc = Document::load(b.build(1)).unwrap();
1447 assert_eq!(
1448 doc.page_count(),
1449 1,
1450 "missing /Count is recovered by walking"
1451 );
1452 }
1453
1454 #[test]
1455 fn page_count_ignores_corrupt_oversized_count() {
1456 let mut b = PdfBuilder::new();
1459 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1460 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 999999999 >>");
1461 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
1462 let doc = Document::load(b.build(1)).unwrap();
1463 assert_eq!(doc.page_count(), 1, "implausible /Count is rejected");
1464 }
1465
1466 #[test]
1467 fn version_scan_and_default() {
1468 let mut b = PdfBuilder::new().version(2, 0);
1469 b.object(1, "<< /Type /Catalog >>");
1470 assert_eq!(Document::load(b.build(1)).unwrap().version(), (2, 0));
1471 let data = replace_once(&simple_doc("v"), b"%PDF-", b"%QQQ-");
1473 assert_eq!(Document::load(data).unwrap().version(), (1, 4));
1474 }
1475
1476 #[test]
1477 fn deeply_nested_root_object_does_not_overflow_the_stack() {
1478 let mut data = b"%PDF-1.7\n1 0 obj\n".to_vec();
1483 data.extend(std::iter::repeat_n(b'[', 50_000));
1484 data.extend(std::iter::repeat_n(b']', 50_000));
1485 data.extend_from_slice(b"\nendobj\ntrailer\n<</Root 1 0 R>>\n%%EOF\n");
1486 let outcome = std::thread::Builder::new()
1487 .stack_size(1024 * 1024)
1488 .spawn(move || Document::load(data).map(|doc| doc.page_count()))
1489 .expect("spawn test thread")
1490 .join()
1491 .expect("Document::load must not overflow the stack");
1492 assert!(matches!(outcome, Ok(0) | Err(_)));
1495 }
1496
1497 #[test]
1498 fn bytes_and_xref_accessors() {
1499 let data = simple_doc("accessors");
1500 let doc = Document::load(data.clone()).unwrap();
1501 assert_eq!(doc.bytes(), &data[..]);
1502 assert!(!doc.xref().is_empty());
1503 assert!(doc.xref().trailer.get("Root").is_some());
1504 }
1505
1506 #[test]
1507 fn object_at_spanned_reparses_identically() {
1508 let data = simple_doc("spanned");
1509 let doc = Document::load(data).unwrap();
1510 for (num, entry) in doc.xref().iter() {
1511 let XrefEntry::InFile { offset, gen } = entry else {
1512 continue;
1513 };
1514 let (r, object, span) = doc.object_at_spanned(offset as usize).unwrap();
1515 assert_eq!(r.num, num);
1516 assert_eq!(r.gen, gen);
1517 assert_eq!(span.start, offset);
1518 assert!(span.end as usize <= doc.bytes().len());
1519 let slice = &doc.bytes()[span.start as usize..span.end as usize];
1521 let (r2, object2) = Parser::new(slice).parse_indirect(&NoResolve).unwrap();
1522 assert_eq!(r2, r);
1523 assert_eq!(object2, object);
1524 }
1525 }
1526
1527 #[test]
1528 fn page_object_ref_points_at_a_page_dict() {
1529 let doc = Document::load(multi_page_doc(&["one", "two"])).unwrap();
1530 for index in 0..doc.page_count() {
1531 let page = doc.page(index).unwrap();
1532 let r = page.object_ref().expect("builder pages are indirect");
1533 let resolved = doc.get(r).unwrap();
1534 assert_eq!(
1535 resolved
1536 .as_dict()
1537 .unwrap()
1538 .get_name("Type")
1539 .map(|n| n.0.as_str()),
1540 Some("Page")
1541 );
1542 }
1543 }
1544
1545 const RC4_FIXTURE_KEY_LEN: usize = 16; const RC4_FIXTURE_P: i32 = -44;
1555 const RC4_FIXTURE_ID0: &[u8] = b"0123456789abcdef";
1556 const RC4_FIXTURE_PAD: [u8; 32] = [
1557 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01,
1558 0x08, 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53,
1559 0x69, 0x7A,
1560 ];
1561
1562 #[rustfmt::skip]
1563 const RC4_FIXTURE_MD5_S: [u32; 64] = [
1564 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
1565 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
1566 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
1567 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
1568 ];
1569 #[rustfmt::skip]
1570 const RC4_FIXTURE_MD5_K: [u32; 64] = [
1571 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
1572 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
1573 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
1574 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
1575 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
1576 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
1577 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
1578 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,
1579 ];
1580
1581 fn rc4_fixture_md5(input: &[u8]) -> [u8; 16] {
1582 let (mut a0, mut b0, mut c0, mut d0) = (
1583 0x6745_2301u32,
1584 0xefcd_ab89u32,
1585 0x98ba_dcfeu32,
1586 0x1032_5476u32,
1587 );
1588 let mut msg = input.to_vec();
1589 let bitlen = (input.len() as u64).wrapping_mul(8);
1590 msg.push(0x80);
1591 while msg.len() % 64 != 56 {
1592 msg.push(0);
1593 }
1594 msg.extend_from_slice(&bitlen.to_le_bytes());
1595 for chunk in msg.chunks_exact(64) {
1596 let mut m = [0u32; 16];
1597 for (word, bytes) in m.iter_mut().zip(chunk.chunks_exact(4)) {
1598 *word = u32::from_le_bytes(bytes.try_into().unwrap());
1599 }
1600 let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0);
1601 for i in 0..64 {
1602 let (f, g) = match i {
1603 0..=15 => ((b & c) | (!b & d), i),
1604 16..=31 => ((d & b) | (!d & c), (5 * i + 1) % 16),
1605 32..=47 => (b ^ c ^ d, (3 * i + 5) % 16),
1606 _ => (c ^ (b | !d), (7 * i) % 16),
1607 };
1608 let f = f
1609 .wrapping_add(a)
1610 .wrapping_add(RC4_FIXTURE_MD5_K[i])
1611 .wrapping_add(m[g]);
1612 a = d;
1613 d = c;
1614 c = b;
1615 b = b.wrapping_add(f.rotate_left(RC4_FIXTURE_MD5_S[i]));
1616 }
1617 a0 = a0.wrapping_add(a);
1618 b0 = b0.wrapping_add(b);
1619 c0 = c0.wrapping_add(c);
1620 d0 = d0.wrapping_add(d);
1621 }
1622 let mut out = [0u8; 16];
1623 out[0..4].copy_from_slice(&a0.to_le_bytes());
1624 out[4..8].copy_from_slice(&b0.to_le_bytes());
1625 out[8..12].copy_from_slice(&c0.to_le_bytes());
1626 out[12..16].copy_from_slice(&d0.to_le_bytes());
1627 out
1628 }
1629
1630 fn rc4_fixture_rc4(key: &[u8], data: &[u8]) -> Vec<u8> {
1631 let mut s: [u8; 256] = core::array::from_fn(|i| i as u8);
1632 let mut j = 0u8;
1633 for i in 0..256 {
1634 j = j.wrapping_add(s[i]).wrapping_add(key[i % key.len()]);
1635 s.swap(i, j as usize);
1636 }
1637 let mut out = Vec::with_capacity(data.len());
1638 let (mut i, mut j) = (0u8, 0u8);
1639 for &byte in data {
1640 i = i.wrapping_add(1);
1641 j = j.wrapping_add(s[i as usize]);
1642 s.swap(i as usize, j as usize);
1643 let k = s[s[i as usize].wrapping_add(s[j as usize]) as usize];
1644 out.push(byte ^ k);
1645 }
1646 out
1647 }
1648
1649 fn rc4_fixture_owner_entry() -> Vec<u8> {
1651 let mut d = rc4_fixture_md5(&RC4_FIXTURE_PAD);
1652 for _ in 0..50 {
1653 d = rc4_fixture_md5(&d[..RC4_FIXTURE_KEY_LEN]);
1654 }
1655 let rc4key = d[..RC4_FIXTURE_KEY_LEN].to_vec();
1656 let mut o = rc4_fixture_rc4(&rc4key, &RC4_FIXTURE_PAD);
1657 for i in 1u8..=19 {
1658 let k: Vec<u8> = rc4key.iter().map(|b| b ^ i).collect();
1659 o = rc4_fixture_rc4(&k, &o);
1660 }
1661 o
1662 }
1663
1664 fn rc4_fixture_file_key(o: &[u8]) -> Vec<u8> {
1666 let mut input = Vec::new();
1667 input.extend_from_slice(&RC4_FIXTURE_PAD);
1668 input.extend_from_slice(o);
1669 input.extend_from_slice(&(RC4_FIXTURE_P as u32).to_le_bytes());
1670 input.extend_from_slice(RC4_FIXTURE_ID0);
1671 let mut d = rc4_fixture_md5(&input);
1672 for _ in 0..50 {
1673 d = rc4_fixture_md5(&d[..RC4_FIXTURE_KEY_LEN]);
1674 }
1675 d[..RC4_FIXTURE_KEY_LEN].to_vec()
1676 }
1677
1678 fn rc4_fixture_user_entry(key: &[u8]) -> Vec<u8> {
1680 let mut input = Vec::new();
1681 input.extend_from_slice(&RC4_FIXTURE_PAD);
1682 input.extend_from_slice(RC4_FIXTURE_ID0);
1683 let mut x = rc4_fixture_md5(&input).to_vec();
1684 x = rc4_fixture_rc4(key, &x);
1685 for i in 1u8..=19 {
1686 let k: Vec<u8> = key.iter().map(|b| b ^ i).collect();
1687 x = rc4_fixture_rc4(&k, &x);
1688 }
1689 x.resize(32, 0); x
1691 }
1692
1693 fn rc4_fixture_obj_key(key: &[u8], num: u32, gen: u16) -> Vec<u8> {
1694 let mut input = key.to_vec();
1695 input.extend_from_slice(&num.to_le_bytes()[..3]);
1696 input.extend_from_slice(&gen.to_le_bytes()[..2]);
1697 rc4_fixture_md5(&input)[..(key.len() + 5).min(16)].to_vec()
1698 }
1699
1700 fn rc4_fixture_hexstr(b: &[u8]) -> String {
1701 let mut s = String::from("<");
1702 for x in b {
1703 s.push_str(&format!("{x:02x}"));
1704 }
1705 s.push('>');
1706 s
1707 }
1708
1709 fn rc4_encrypted_fixture() -> Vec<u8> {
1713 let o = rc4_fixture_owner_entry();
1714 let key = rc4_fixture_file_key(&o);
1715 let u = rc4_fixture_user_entry(&key);
1716 let msg = rc4_fixture_rc4(&rc4_fixture_obj_key(&key, 3, 0), b"Top secret message");
1717
1718 let mut b = PdfBuilder::new().version(1, 4);
1719 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1720 b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
1721 b.object(3, &format!("<< /Msg {} >>", rc4_fixture_hexstr(&msg)));
1722 b.object(
1723 9,
1724 &format!(
1725 "<< /Filter /Standard /V 2 /R 3 /Length 128 /P {} /O {} /U {} >>",
1726 RC4_FIXTURE_P,
1727 rc4_fixture_hexstr(&o),
1728 rc4_fixture_hexstr(&u)
1729 ),
1730 );
1731 let trailer = format!(
1732 "/Encrypt 9 0 R /ID [{}{}]",
1733 rc4_fixture_hexstr(RC4_FIXTURE_ID0),
1734 rc4_fixture_hexstr(RC4_FIXTURE_ID0)
1735 );
1736 b.trailer_extra(&trailer).build(1)
1737 }
1738
1739 #[test]
1740 fn encrypted_generation_mismatch_still_decrypts() {
1741 let doc = Document::load(rc4_encrypted_fixture()).expect("empty password opens the file");
1751 let obj3 = doc.get(ObjRef { num: 3, gen: 7 }).unwrap();
1752 let msg = obj3
1753 .as_dict()
1754 .unwrap()
1755 .get("Msg")
1756 .unwrap()
1757 .as_str_bytes()
1758 .unwrap();
1759 assert_eq!(
1760 msg, b"Top secret message",
1761 "decrypted using the file's real gen (0), not the mismatched request (7)"
1762 );
1763 }
1764
1765 #[test]
1766 fn objstm_doc_fixture_loads_and_resolves_members() {
1767 let data = objstm_doc(&[(7, "<< /Marker (inside) >>")]);
1768 let doc = Document::load(data).unwrap();
1769 assert_eq!(doc.page_count(), 1);
1770 let member = doc.get(ObjRef { num: 7, gen: 0 }).unwrap();
1771 let text = member.as_dict().unwrap().get("Marker").unwrap();
1772 assert_eq!(text.as_str_bytes(), Some(&b"inside"[..]));
1773 assert!(matches!(
1775 doc.xref().get(7),
1776 Some(XrefEntry::InStream { stream_num: 4, .. })
1777 ));
1778 }
1779
1780 #[test]
1783 fn page_from_parts_exposes_its_accessors() {
1784 let mut dict = Dict::default();
1785 dict.insert(Name("Type".into()), Object::Name(Name("Page".into())));
1786 let media = Rect::new(0.0, 0.0, 200.0, 400.0);
1787 let obj_ref = ObjRef { num: 7, gen: 0 };
1788
1789 let page = Page::from_parts(
1790 3,
1791 media,
1792 media,
1793 90,
1794 Dict::default(),
1795 dict.clone(),
1796 Some(obj_ref),
1797 );
1798
1799 assert_eq!(page.index, 3);
1800 assert_eq!(page.object_ref(), Some(obj_ref));
1801 assert_eq!(page.dict(), &dict);
1802 assert_eq!(page.size(), (400.0, 200.0));
1804 }
1805
1806 #[test]
1810 fn page_from_parts_normalizes_rotation() {
1811 let media = Rect::new(0.0, 0.0, 200.0, 400.0);
1812 let build = |rotate: i32| {
1813 Page::from_parts(
1814 0,
1815 media,
1816 media,
1817 rotate,
1818 Dict::default(),
1819 Dict::default(),
1820 None,
1821 )
1822 };
1823
1824 for (given, canonical) in [(-90, 270), (450, 90), (540, 180), (720, 0), (-360, 0)] {
1825 let page = build(given);
1826 assert_eq!(
1827 page.rotate, canonical,
1828 "from_parts must normalize /Rotate {given} to {canonical}"
1829 );
1830 assert_eq!(
1831 page.size(),
1832 build(canonical).size(),
1833 "/Rotate {given} must report the same size as {canonical}"
1834 );
1835 }
1836 }
1837
1838 #[test]
1843 fn page_size_handles_unnormalized_rotation() {
1844 let media = Rect::new(0.0, 0.0, 200.0, 400.0);
1845 let mut page = Page::from_parts(0, media, media, 0, Dict::default(), Dict::default(), None);
1846
1847 for rotate in [90, 270, -90, -270, 450, 630] {
1848 page.rotate = rotate;
1849 assert_eq!(
1850 page.size(),
1851 (400.0, 200.0),
1852 "/Rotate {rotate} is a quarter turn and must swap the size"
1853 );
1854 }
1855 for rotate in [0, 180, -180, 360, 540, 720] {
1856 page.rotate = rotate;
1857 assert_eq!(
1858 page.size(),
1859 (200.0, 400.0),
1860 "/Rotate {rotate} is a half turn and must leave the size alone"
1861 );
1862 }
1863 }
1864}