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