1use crate::crypto::{DecryptionError, DecryptionTarget, Decryptor, get};
4use crate::data::Data;
5use crate::metadata::Metadata;
6use crate::object::Name;
7use crate::object::ObjectIdentifier;
8use crate::object::Stream;
9use crate::object::dict::keys::{
10 AUTHOR, CREATION_DATE, CREATOR, ENCRYPT, FIRST, ID, INDEX, INFO, KEYWORDS, MOD_DATE, N,
11 OCPROPERTIES, PAGES, PREV, PRODUCER, ROOT, SIZE, SUBJECT, TITLE, TYPE, VERSION, W, XREF_STM,
12};
13use crate::object::indirect::IndirectObject;
14use crate::object::{Array, MaybeRef};
15use crate::object::{DateTime, Dict};
16use crate::object::{Object, ObjectLike};
17use crate::pdf::{PdfLoadLimits, PdfVersion};
18use crate::reader::Reader;
19use crate::reader::{Readable, ReaderContext, ReaderExt};
20use crate::sync::{Arc, FxHashMap, RwLock, RwLockExt};
21use crate::{PdfData, object};
22use alloc::vec;
23use alloc::vec::Vec;
24use core::cmp::max;
25use core::iter;
26use core::ops::Deref;
27use log::{error, warn};
28
29pub(crate) const XREF_ENTRY_LEN: usize = 20;
30
31#[derive(Debug, Copy, Clone)]
32pub(crate) enum XRefError {
33 Unknown,
34 Encryption(DecryptionError),
35}
36
37pub(crate) fn root_xref(
39 data: PdfData,
40 password: &[u8],
41 limits: PdfLoadLimits,
42) -> Result<XRef, XRefError> {
43 let mut xref_map = FxHashMap::default();
44 let xref_pos = find_last_xref_pos(data.as_ref()).ok_or(XRefError::Unknown)?;
45 let trailer =
46 populate_xref_impl(data.as_ref(), xref_pos, &mut xref_map).ok_or(XRefError::Unknown)?;
47
48 XRef::new(
49 data.clone(),
50 xref_map,
51 XRefInput::TrailerDictData(trailer),
52 false,
53 password,
54 limits,
55 )
56}
57
58pub(crate) fn fallback(data: PdfData, password: &[u8], limits: PdfLoadLimits) -> Option<XRef> {
60 warn!("xref table was invalid, trying to manually build xref table");
61 let (xref_map, xref_input) = fallback_xref_map(&data, password);
62
63 if let Some(xref_input) = xref_input {
64 warn!("rebuild xref table with {} entries", xref_map.len());
65
66 XRef::new(data.clone(), xref_map, xref_input, true, password, limits).ok()
67 } else {
68 warn!("couldn't find trailer dictionary, failed to rebuild xref table");
69
70 None
71 }
72}
73
74fn fallback_xref_map<'a>(data: &'a PdfData, password: &[u8]) -> (XrefMap, Option<XRefInput<'a>>) {
75 fallback_xref_map_inner(data, ReaderContext::dummy(), true, password)
76}
77
78fn fallback_xref_map_inner<'a>(
79 data: &'a PdfData,
80 mut dummy_ctx: ReaderContext<'a>,
81 recurse: bool,
82 password: &[u8],
83) -> (XrefMap, Option<XRefInput<'a>>) {
84 let mut xref_map = FxHashMap::default();
85 let mut trailer_dicts = vec![];
86 let mut root_ref = None;
87
88 let mut r = Reader::new(data.as_ref());
89
90 let mut last_obj_num = None;
91
92 loop {
93 let cur_pos = r.offset();
94
95 let mut old_r = r.clone();
96
97 if let Some(obj_id) = r.read::<ObjectIdentifier>(&dummy_ctx) {
98 let mut cloned = r.clone();
99 cloned.skip_white_spaces_and_comments();
101 if cloned.skip::<Object<'_>>(false).is_some() {
102 xref_map.insert(obj_id, EntryType::Normal(cur_pos));
103 last_obj_num = Some(obj_id);
104 dummy_ctx.set_obj_number(obj_id);
105 }
106 } else if let Some(dict) = r.read::<Dict<'_>>(&dummy_ctx) {
107 if dict.contains_key(ROOT) {
108 trailer_dicts.push(dict.clone());
109 }
110
111 if dict
112 .get::<Name>(TYPE)
113 .is_some_and(|n| n.as_str() == "Catalog")
114 {
115 root_ref = last_obj_num;
116 }
117
118 if let Some(stream) = old_r.read::<Stream<'_>>(&dummy_ctx)
119 && stream.dict().get::<Name>(TYPE).as_deref() == Some(b"ObjStm")
120 && let Some(data) = stream.decoded().ok()
121 && let Some(last_obj_num) = last_obj_num
122 && let Some(obj_stream) = ObjectStream::new(stream, &data, &dummy_ctx)
123 {
124 for (idx, (obj_num, _)) in obj_stream.offsets.iter().enumerate() {
125 let id = ObjectIdentifier::new(*obj_num as i32, 0);
126 if xref_map
131 .get(&id)
132 .is_none_or(|e| !matches!(e, &EntryType::Normal(_)))
133 {
134 xref_map.insert(
135 id,
136 EntryType::ObjStream(last_obj_num.obj_number as u32, idx as u32),
137 );
138 }
139 }
140 }
141 } else {
142 r.read_byte();
143 }
144
145 if r.at_end() {
146 break;
147 }
148 }
149
150 let mut trailer_dict = None;
152
153 for dict in trailer_dicts {
154 if let Some(root_id) = dict.get_raw::<Dict<'_>>(ROOT) {
155 let check = |dict: &Dict<'_>| -> bool { dict.contains_key(PAGES) };
156
157 match root_id {
158 MaybeRef::Ref(r) => match xref_map.get(&r.into()) {
159 Some(EntryType::Normal(offset)) => {
160 let mut reader = Reader::new(&data.as_ref()[*offset..]);
161
162 if let Some(obj) =
163 reader.read_with_context::<IndirectObject<Dict<'_>>>(&dummy_ctx)
164 && check(&obj.clone().get())
165 {
166 trailer_dict = Some(dict);
167 }
168 }
169 Some(EntryType::ObjStream(obj_num, idx)) => {
170 if let Some(EntryType::Normal(offset)) =
171 xref_map.get(&ObjectIdentifier::new(*obj_num as i32, 0))
172 {
173 let mut reader = Reader::new(&data.as_ref()[*offset..]);
174
175 if let Some(stream) =
176 reader.read_with_context::<IndirectObject<Stream<'_>>>(&dummy_ctx)
177 && let Some(data) = stream.clone().get().decoded().ok()
178 && let Some(object_stream) =
179 ObjectStream::new(stream.get(), &data, &dummy_ctx)
180 && let Some(obj) = object_stream.get::<Dict<'_>>(*idx)
181 && check(&obj)
182 {
183 trailer_dict = Some(dict);
184 }
185 }
186 }
187 _ => {}
188 },
189 MaybeRef::NotRef(d) => {
190 if check(&d) {
191 trailer_dict = Some(dict);
192 }
193 }
194 }
195 }
196 }
197
198 let has_encryption = trailer_dict
199 .as_ref()
200 .is_some_and(|t| t.contains_key(ENCRYPT));
201
202 if has_encryption && recurse {
203 if let Some(Ok(xref)) = trailer_dict.as_ref().map(|d| {
208 XRef::new(
209 data.clone(),
210 xref_map.clone(),
211 XRefInput::TrailerDictData(d.data()),
212 true,
213 password,
214 PdfLoadLimits::default(),
215 )
216 }) {
217 let ctx = ReaderContext::new(&xref, false);
218 let (patched_map, _) = fallback_xref_map_inner(data, ctx, false, password);
219 xref_map = patched_map;
220 }
221 }
222
223 if let Some(trailer_dict_data) = trailer_dict.map(|d| d.data()) {
224 (
225 xref_map,
226 Some(XRefInput::TrailerDictData(trailer_dict_data)),
227 )
228 } else if let Some(root_ref) = root_ref {
229 (xref_map, Some(XRefInput::RootRef(root_ref)))
230 } else {
231 (xref_map, None)
232 }
233}
234
235const DUMMY_XREF: XRef = XRef(Inner::Dummy);
236
237#[derive(Debug, Clone)]
239pub struct XRef(Inner);
240
241impl XRef {
242 fn new(
243 data: PdfData,
244 xref_map: XrefMap,
245 input: XRefInput<'_>,
246 repaired: bool,
247 password: &[u8],
248 load_limits: PdfLoadLimits,
249 ) -> Result<Self, XRefError> {
250 let trailer_data = TrailerData::dummy();
254
255 let mut xref = Self(Inner::Some(Arc::new(SomeRepr {
256 data: Arc::new(Data::new(data)),
257 map: Arc::new(RwLock::new(MapRepr { xref_map, repaired })),
258 decryptor: Arc::new(Decryptor::None),
259 has_ocgs: false,
260 metadata: Arc::new(Metadata::default()),
261 trailer_data,
262 password: password.to_vec(),
263 load_limits,
264 })));
265
266 let decryptor = {
271 match input {
272 XRefInput::TrailerDictData(trailer_dict_data) => {
273 let mut r = Reader::new(trailer_dict_data);
274
275 let trailer_dict = r
276 .read_with_context::<Dict<'_>>(&ReaderContext::new(&xref, false))
277 .ok_or(XRefError::Unknown)?;
278
279 get_decryptor(&trailer_dict, password)?
280 }
281 XRefInput::RootRef(_) => Decryptor::None,
282 }
283 };
284
285 match &mut xref.0 {
286 Inner::Dummy => unreachable!(),
287 Inner::Some(r) => {
288 let mutable = Arc::make_mut(r);
289 mutable.decryptor = Arc::new(decryptor.clone());
290 }
291 }
292
293 let (trailer_data, has_ocgs, metadata) = match input {
294 XRefInput::TrailerDictData(trailer_dict_data) => {
295 let mut r = Reader::new(trailer_dict_data);
296
297 let trailer_dict = r
298 .read_with_context::<Dict<'_>>(&ReaderContext::new(&xref, false))
299 .ok_or(XRefError::Unknown)?;
300
301 let root_ref = trailer_dict.get_ref(ROOT).ok_or(XRefError::Unknown)?;
302 let root = trailer_dict
303 .get::<Dict<'_>>(ROOT)
304 .ok_or(XRefError::Unknown)?;
305 let metadata = trailer_dict
306 .get::<Dict<'_>>(INFO)
307 .map(|d| parse_metadata(&d))
308 .unwrap_or_default();
309 let pages_ref = root.get_ref(PAGES).ok_or(XRefError::Unknown)?;
310 let has_ocgs = root.get::<Dict<'_>>(OCPROPERTIES).is_some();
311 let version = root
312 .get::<Name>(VERSION)
313 .and_then(|v| PdfVersion::from_bytes(v.deref()));
314
315 let td = TrailerData {
316 pages_ref: pages_ref.into(),
317 root_ref: root_ref.into(),
318 version,
319 };
320
321 (td, has_ocgs, metadata)
322 }
323 XRefInput::RootRef(root_ref) => {
324 let root = xref.get::<Dict<'_>>(root_ref).ok_or(XRefError::Unknown)?;
325 let pages_ref = root.get_ref(PAGES).ok_or(XRefError::Unknown)?;
326
327 let td = TrailerData {
328 pages_ref: pages_ref.into(),
329 root_ref,
330 version: None,
331 };
332
333 (td, false, Metadata::default())
334 }
335 };
336
337 match &mut xref.0 {
338 Inner::Dummy => unreachable!(),
339 Inner::Some(r) => {
340 let mutable = Arc::make_mut(r);
341 mutable.trailer_data = trailer_data;
342 mutable.decryptor = Arc::new(decryptor);
343 mutable.has_ocgs = has_ocgs;
344 mutable.metadata = Arc::new(metadata);
345 }
346 }
347
348 Ok(xref)
349 }
350
351 pub(crate) fn dummy() -> &'static Self {
352 &DUMMY_XREF
353 }
354
355 pub(crate) fn load_limits(&self) -> PdfLoadLimits {
356 match &self.0 {
357 Inner::Dummy => PdfLoadLimits::default(),
358 Inner::Some(r) => r.load_limits,
359 }
360 }
361
362 pub(crate) fn len(&self) -> usize {
363 match &self.0 {
364 Inner::Dummy => 0,
365 Inner::Some(r) => r.map.get().xref_map.len(),
366 }
367 }
368
369 pub(crate) fn trailer_data(&self) -> &TrailerData {
370 match &self.0 {
371 Inner::Dummy => unreachable!(),
372 Inner::Some(r) => &r.trailer_data,
373 }
374 }
375
376 #[cfg(test)]
379 pub(crate) fn object_stream_offsets_cache_len(&self) -> usize {
380 match &self.0 {
381 Inner::Dummy => 0,
382 Inner::Some(r) => r.data.object_stream_offsets_cache_len(),
383 }
384 }
385
386 pub(crate) fn metadata(&self) -> &Metadata {
387 match &self.0 {
388 Inner::Dummy => unreachable!(),
389 Inner::Some(r) => &r.metadata,
390 }
391 }
392
393 pub fn root_id(&self) -> ObjectIdentifier {
395 self.trailer_data().root_ref
396 }
397
398 pub fn has_optional_content_groups(&self) -> bool {
400 match &self.0 {
401 Inner::Dummy => false,
402 Inner::Some(r) => r.has_ocgs,
403 }
404 }
405
406 pub(crate) fn objects(&self) -> impl IntoIterator<Item = Object<'_>> + '_ {
407 match &self.0 {
408 Inner::Dummy => unimplemented!(),
409 Inner::Some(r) => {
410 let locked = r.map.get();
411 let mut elements = locked
412 .xref_map
413 .iter()
414 .map(|(id, e)| {
415 let offset = match e {
416 EntryType::Normal(o) => (*o, 0),
417 EntryType::ObjStream(id, index) => {
418 if let Some(EntryType::Normal(offset)) =
419 locked.xref_map.get(&ObjectIdentifier::new(*id as i32, 0))
420 {
421 (*offset, *index)
422 } else {
423 (usize::MAX, 0)
424 }
425 }
426 };
427
428 (*id, offset)
429 })
430 .collect::<Vec<_>>();
431
432 elements.sort_by_key(|e1| e1.1);
435
436 let mut iter = elements.into_iter();
437
438 iter::from_fn(move || {
439 for next in iter.by_ref() {
440 if let Some(obj) = self.get_with(next.0, &ReaderContext::new(self, false)) {
441 return Some(obj);
442 } else {
443 continue;
445 }
446 }
447
448 None
449 })
450 }
451 }
452 }
453
454 pub(crate) fn repair(&self) {
455 let Inner::Some(r) = &self.0 else {
456 unreachable!();
457 };
458
459 let mut locked = r.map.put();
460 if locked.repaired {
461 return;
462 }
463
464 let (xref_map, _) = fallback_xref_map(r.data.get(), &r.password);
465 locked.xref_map = xref_map;
466 locked.repaired = true;
467 }
468
469 #[inline]
470 pub(crate) fn needs_decryption(&self, ctx: &ReaderContext<'_>) -> bool {
471 match &self.0 {
472 Inner::Dummy => false,
473 Inner::Some(r) => {
474 if matches!(r.decryptor.as_ref(), Decryptor::None) {
475 false
476 } else {
477 !ctx.in_content_stream() && !ctx.in_object_stream()
478 }
479 }
480 }
481 }
482
483 #[inline]
484 pub(crate) fn decrypt(
485 &self,
486 id: ObjectIdentifier,
487 data: &[u8],
488 target: DecryptionTarget,
489 ) -> Option<Vec<u8>> {
490 match &self.0 {
491 Inner::Dummy => Some(data.to_vec()),
492 Inner::Some(r) => r.decryptor.decrypt(id, data, target),
493 }
494 }
495
496 #[allow(private_bounds)]
498 pub fn get<'a, T>(&'a self, id: ObjectIdentifier) -> Option<T>
499 where
500 T: ObjectLike<'a>,
501 {
502 let ctx = ReaderContext::new(self, false);
503 self.get_with(id, &ctx)
504 }
505
506 #[allow(private_bounds)]
508 pub(crate) fn get_with<'a, T>(
509 &'a self,
510 id: ObjectIdentifier,
511 ctx: &ReaderContext<'a>,
512 ) -> Option<T>
513 where
514 T: ObjectLike<'a>,
515 {
516 let Inner::Some(repr) = &self.0 else {
517 return None;
518 };
519
520 let (entry, was_repaired) = {
522 let locked = repr.map.get();
523 (*locked.xref_map.get(&id)?, locked.repaired)
526 };
527
528 let mut r = Reader::new(repr.data.get().as_ref());
529
530 let mut ctx = ctx.clone();
531 ctx.set_obj_number(id);
532 ctx.set_in_content_stream(false);
533
534 match entry {
535 EntryType::Normal(offset) => {
536 ctx.set_in_object_stream(false);
537 r.jump(offset);
538
539 if let Some(object) = r.read_with_context::<IndirectObject<T>>(&ctx) {
540 if object.id() == &id {
541 return Some(object.get());
542 }
543 } else {
544 if r.skip_not_in_content_stream::<IndirectObject<Object<'_>>>()
547 .is_some()
548 {
549 return None;
550 }
551 };
552
553 if was_repaired {
555 error!(
556 "attempt was made at repairing xref, but object {id:?} still couldn't be read"
557 );
558
559 None
560 } else {
561 warn!("broken xref, attempting to repair");
562
563 self.repair();
564
565 self.get_with::<T>(id, &ctx)
567 }
568 }
569 EntryType::ObjStream(obj_stram_gen_num, index) => {
570 let obj_stream_id = ObjectIdentifier::new(obj_stram_gen_num as i32, 0);
572
573 if obj_stream_id == id || ctx.parent_chain_contains(&id) {
574 warn!("cycle detected in object stream");
575
576 return None;
577 }
578
579 ctx.parent_chain_push(id);
580
581 let stream = self.get_with::<Stream<'_>>(obj_stream_id, &ctx)?;
582 let data = repr.data.get_with(obj_stream_id, &ctx)?;
583 let offsets = repr
588 .data
589 .get_object_stream_offsets_or_init(obj_stream_id, || {
590 parse_object_stream_offsets(&stream, data)
591 })?;
592 let object_stream = ObjectStream::from_cached_offsets(data, &ctx, offsets);
593 object_stream.get(index)
594 }
595 }
596 }
597}
598
599#[derive(Debug, Copy, Clone)]
602pub(crate) enum XRefInput<'a> {
603 TrailerDictData(&'a [u8]),
606 RootRef(ObjectIdentifier),
614}
615
616pub(crate) fn find_last_xref_pos(data: &[u8]) -> Option<usize> {
617 let mut finder = Reader::new(data);
618 let mut pos = finder.len().checked_sub(1)?;
619 finder.jump(pos);
620
621 let needle = b"startxref";
622
623 loop {
624 if finder.forward_tag(needle).is_some() {
625 finder.skip_white_spaces_and_comments();
626
627 let offset = finder.read_without_context::<i32>()?.try_into().ok()?;
628
629 return Some(offset);
630 }
631
632 pos = pos.checked_sub(1)?;
633 finder.jump(pos);
634 }
635}
636
637#[derive(Debug, PartialEq, Eq, Clone, Copy)]
639enum EntryType {
640 Normal(usize),
642 ObjStream(u32, u32),
646}
647
648type XrefMap = FxHashMap<ObjectIdentifier, EntryType>;
649
650#[derive(Debug)]
652struct MapRepr {
653 xref_map: XrefMap,
654 repaired: bool,
655}
656
657#[derive(Debug, Copy, Clone)]
658pub(crate) struct TrailerData {
659 pub(crate) pages_ref: ObjectIdentifier,
660 pub(crate) root_ref: ObjectIdentifier,
661 pub(crate) version: Option<PdfVersion>,
662}
663
664impl TrailerData {
665 pub(crate) fn dummy() -> Self {
666 Self {
667 pages_ref: ObjectIdentifier::new(0, 0),
668 root_ref: ObjectIdentifier::new(0, 0),
669 version: None,
670 }
671 }
672}
673
674#[derive(Debug, Clone)]
675struct SomeRepr {
676 data: Arc<Data>,
677 map: Arc<RwLock<MapRepr>>,
678 metadata: Arc<Metadata>,
679 decryptor: Arc<Decryptor>,
680 has_ocgs: bool,
681 password: Vec<u8>,
682 trailer_data: TrailerData,
683 load_limits: PdfLoadLimits,
684}
685
686#[derive(Debug, Clone)]
687enum Inner {
688 Dummy,
690 Some(Arc<SomeRepr>),
692}
693
694#[derive(Debug)]
695struct XRefEntry {
696 offset: usize,
697 gen_number: i32,
698 used: bool,
699}
700
701impl XRefEntry {
702 pub(crate) fn read(data: &[u8]) -> Option<Self> {
703 #[inline(always)]
704 fn parse_u32(data: &[u8]) -> Option<u32> {
705 let mut accum = 0_u32;
706
707 for byte in data {
708 accum = accum.checked_mul(10)?;
709
710 match *byte {
711 b'0'..=b'9' => accum = accum.checked_add((*byte - b'0') as u32)?,
712 _ => return None,
713 }
714 }
715
716 Some(accum)
717 }
718
719 let offset = parse_u32(&data[0..10])? as usize;
720 let gen_number = i32::try_from(parse_u32(&data[11..16])?).ok()?;
721
722 let used = data[17] == b'n';
723
724 Some(Self {
725 offset,
726 gen_number,
727 used,
728 })
729 }
730}
731
732const MAX_XREF_CHAIN_DEPTH: usize = 64;
735
736fn populate_xref_impl<'a>(data: &'a [u8], pos: usize, xref_map: &mut XrefMap) -> Option<&'a [u8]> {
737 populate_xref_depth(data, pos, xref_map, 0)
738}
739
740fn populate_xref_depth<'a>(
741 data: &'a [u8],
742 pos: usize,
743 xref_map: &mut XrefMap,
744 depth: usize,
745) -> Option<&'a [u8]> {
746 if depth > MAX_XREF_CHAIN_DEPTH {
747 log::warn!("Xref chain depth exceeds {MAX_XREF_CHAIN_DEPTH}, stopping traversal");
748 return None;
749 }
750 let mut reader = Reader::new(data);
751 reader.jump(pos);
752 reader.skip_white_spaces_and_comments();
754
755 let mut r2 = reader.clone();
756 if reader
757 .clone()
758 .read_without_context::<ObjectIdentifier>()
759 .is_some()
760 {
761 populate_from_xref_stream(data, &mut r2, xref_map, depth)
762 } else {
763 populate_from_xref_table(data, &mut r2, xref_map, depth)
764 }
765}
766
767pub(super) struct SubsectionHeader {
768 pub(super) start: u32,
769 pub(super) num_entries: u32,
770}
771
772impl Readable<'_> for SubsectionHeader {
773 fn read(r: &mut Reader<'_>, _: &ReaderContext<'_>) -> Option<Self> {
774 r.skip_white_spaces();
775 let start = r.read_without_context::<u32>()?;
776 r.skip_white_spaces();
777 let num_entries = r.read_without_context::<u32>()?;
778 r.skip_white_spaces();
779
780 Some(Self { start, num_entries })
781 }
782}
783
784fn populate_from_xref_table<'a>(
786 data: &'a [u8],
787 reader: &mut Reader<'a>,
788 insert_map: &mut XrefMap,
789 depth: usize,
790) -> Option<&'a [u8]> {
791 let trailer = {
792 let mut reader = reader.clone();
793 read_xref_table_trailer(&mut reader, &ReaderContext::dummy())?
794 };
795
796 reader.skip_white_spaces();
797 reader.forward_tag(b"xref")?;
798 reader.skip_white_spaces();
799
800 let mut max_obj = 0;
801
802 if let Some(prev) = trailer.get::<i32>(PREV) {
803 populate_xref_depth(data, prev as usize, insert_map, depth + 1)?;
805 }
806
807 if let Some(xref_stm) = trailer.get::<i32>(XREF_STM) {
810 populate_xref_depth(data, xref_stm as usize, insert_map, depth + 1)?;
811 }
812
813 while let Some(header) = reader.read_without_context::<SubsectionHeader>() {
814 reader.skip_white_spaces();
815
816 let start = header.start;
817 let end = start.checked_add(header.num_entries)?;
820
821 for obj_number in start..end {
822 max_obj = max(max_obj, obj_number);
823 let bytes = reader.read_bytes(XREF_ENTRY_LEN)?;
824 let entry = XRefEntry::read(bytes)?;
825
826 if entry.used {
829 insert_map.insert(
830 ObjectIdentifier::new(obj_number as i32, entry.gen_number),
831 EntryType::Normal(entry.offset),
832 );
833 }
834 }
835 }
836
837 Some(trailer.data())
838}
839
840fn populate_from_xref_stream<'a>(
841 data: &'a [u8],
842 reader: &mut Reader<'a>,
843 insert_map: &mut XrefMap,
844 depth: usize,
845) -> Option<&'a [u8]> {
846 let stream = reader
847 .read_with_context::<IndirectObject<Stream<'_>>>(&ReaderContext::dummy())?
848 .get();
849
850 if let Some(prev) = stream.dict().get::<i32>(PREV) {
851 let _ = populate_xref_depth(data, prev as usize, insert_map, depth + 1)?;
853 }
854
855 let size = stream.dict().get::<u32>(SIZE)?;
856
857 let [f1_len, f2_len, f3_len] = stream.dict().get::<[u8; 3]>(W)?;
858
859 if f2_len > size_of::<u64>() as u8 {
860 error!("xref offset length is larger than the allowed limit");
861
862 return None;
863 }
864
865 if f1_len != 1 {
867 warn!("first field in xref stream was longer than 1");
868 }
869
870 let xref_data = stream.decoded().ok()?;
871 let mut xref_reader = Reader::new(xref_data.as_ref());
872
873 if let Some(arr) = stream.dict().get::<Array<'_>>(INDEX) {
874 let iter = arr.iter::<(u32, u32)>();
875
876 for (start, num_elements) in iter {
877 xref_stream_subsection(
878 &mut xref_reader,
879 start,
880 num_elements,
881 f1_len,
882 f2_len,
883 f3_len,
884 insert_map,
885 )?;
886 }
887 } else {
888 xref_stream_subsection(
889 &mut xref_reader,
890 0,
891 size,
892 f1_len,
893 f2_len,
894 f3_len,
895 insert_map,
896 )?;
897 }
898
899 Some(stream.dict().data())
900}
901
902fn xref_stream_num(data: &[u8]) -> Option<u32> {
903 Some(match data.len() {
904 0 => return None,
905 1 => u8::from_be(data[0]) as u32,
906 2 => u16::from_be_bytes(data[0..2].try_into().ok()?) as u32,
907 3 => u32::from_be_bytes([0, data[0], data[1], data[2]]),
908 4 => u32::from_be_bytes(data[0..4].try_into().ok()?),
909 8 => {
910 if let Ok(num) = u32::try_from(u64::from_be_bytes(data[0..8].try_into().ok()?)) {
911 return Some(num);
912 } else {
913 warn!("xref stream number is too large");
914
915 return None;
916 }
917 }
918 n => {
919 warn!("invalid xref stream number {n}");
920
921 return None;
922 }
923 })
924}
925
926fn xref_stream_subsection<'a>(
927 xref_reader: &mut Reader<'a>,
928 start: u32,
929 num_elements: u32,
930 f1_len: u8,
931 f2_len: u8,
932 f3_len: u8,
933 insert_map: &mut XrefMap,
934) -> Option<()> {
935 for i in 0..num_elements {
936 let f_type = if f1_len == 0 {
937 1
938 } else {
939 xref_reader.read_bytes(1)?[0]
941 };
942
943 let obj_number = start + i;
944
945 match f_type {
946 0 => {
948 xref_reader.skip_bytes(f2_len as usize + f3_len as usize)?;
949 }
950 1 => {
951 let offset = if f2_len > 0 {
952 let data = xref_reader.read_bytes(f2_len as usize)?;
953 xref_stream_num(data)?
954 } else {
955 0
956 };
957
958 let gen_number = if f3_len > 0 {
959 let data = xref_reader.read_bytes(f3_len as usize)?;
960 xref_stream_num(data)?
961 } else {
962 0
963 };
964
965 insert_map.insert(
966 ObjectIdentifier::new(obj_number as i32, gen_number as i32),
967 EntryType::Normal(offset as usize),
968 );
969 }
970 2 => {
971 let obj_stream_number = {
972 let data = xref_reader.read_bytes(f2_len as usize)?;
973 xref_stream_num(data)?
974 };
975 let gen_number = 0;
976 let index = if f3_len > 0 {
977 let data = xref_reader.read_bytes(f3_len as usize)?;
978 xref_stream_num(data)?
979 } else {
980 0
981 };
982
983 insert_map.insert(
984 ObjectIdentifier::new(obj_number as i32, gen_number),
985 EntryType::ObjStream(obj_stream_number, index),
986 );
987 }
988 _ => {
989 warn!("xref has unknown field type {f_type}");
990
991 return None;
992 }
993 }
994 }
995
996 Some(())
997}
998
999fn read_xref_table_trailer<'a>(
1000 reader: &mut Reader<'a>,
1001 ctx: &ReaderContext<'a>,
1002) -> Option<Dict<'a>> {
1003 reader.skip_white_spaces();
1004 reader.forward_tag(b"xref")?;
1005 reader.skip_white_spaces();
1006
1007 while let Some(header) = reader.read_without_context::<SubsectionHeader>() {
1008 let len = XREF_ENTRY_LEN.checked_mul(header.num_entries as usize)?;
1009 reader.jump(reader.offset().checked_add(len)?);
1010 }
1011
1012 reader.skip_white_spaces();
1013 reader.forward_tag(b"trailer")?;
1014 reader.skip_white_spaces();
1015
1016 reader.read_with_context::<Dict<'_>>(ctx)
1017}
1018
1019fn get_decryptor(trailer_dict: &Dict<'_>, password: &[u8]) -> Result<Decryptor, XRefError> {
1020 if let Some(encryption_dict) = trailer_dict.get::<Dict<'_>>(ENCRYPT) {
1021 let id = if let Some(id) = trailer_dict
1022 .get::<Array<'_>>(ID)
1023 .and_then(|a| a.flex_iter().next::<object::String>())
1024 {
1025 id.to_vec()
1026 } else {
1027 vec![]
1029 };
1030
1031 get(&encryption_dict, &id, password).map_err(XRefError::Encryption)
1032 } else {
1033 Ok(Decryptor::None)
1034 }
1035}
1036
1037fn parse_object_stream_offsets(
1046 inner: &Stream<'_>,
1047 data: &[u8],
1048) -> Option<crate::data::ObjectStreamOffsets> {
1049 let num_objects = inner.dict().get::<usize>(N)?;
1050 let first_offset = inner.dict().get::<usize>(FIRST)?;
1051
1052 let mut r = Reader::new(data);
1053 let mut offsets = Vec::with_capacity(num_objects);
1054
1055 for _ in 0..num_objects {
1056 r.skip_white_spaces_and_comments();
1057 let obj_num = r.read_without_context::<u32>()?;
1059 r.skip_white_spaces_and_comments();
1060 let relative_offset = r.read_without_context::<usize>()?;
1061 offsets.push((obj_num, first_offset + relative_offset));
1062 }
1063
1064 Some(offsets)
1065}
1066
1067struct ObjectStream<'a> {
1076 data: &'a [u8],
1077 ctx: ReaderContext<'a>,
1078 offsets: Arc<crate::data::ObjectStreamOffsets>,
1079}
1080
1081impl<'a> ObjectStream<'a> {
1082 fn new(inner: Stream<'_>, data: &'a [u8], ctx: &ReaderContext<'a>) -> Option<Self> {
1086 let offsets = Arc::new(parse_object_stream_offsets(&inner, data)?);
1087
1088 let mut ctx = ctx.clone();
1089 ctx.set_in_object_stream(true);
1090
1091 Some(Self { data, ctx, offsets })
1092 }
1093
1094 fn from_cached_offsets(
1098 data: &'a [u8],
1099 ctx: &ReaderContext<'a>,
1100 offsets: Arc<crate::data::ObjectStreamOffsets>,
1101 ) -> Self {
1102 let mut ctx = ctx.clone();
1103 ctx.set_in_object_stream(true);
1104
1105 Self { data, ctx, offsets }
1106 }
1107
1108 fn get<T>(&self, index: u32) -> Option<T>
1109 where
1110 T: ObjectLike<'a>,
1111 {
1112 let offset = self.offsets.get(index as usize)?.1;
1113 let mut r = Reader::new(self.data);
1114 r.jump(offset);
1115 r.skip_white_spaces_and_comments();
1116
1117 r.read_with_context::<T>(&self.ctx)
1118 }
1119}
1120
1121fn parse_metadata(info_dict: &Dict<'_>) -> Metadata {
1122 Metadata {
1123 creation_date: info_dict
1124 .get::<object::String>(CREATION_DATE)
1125 .and_then(|c| DateTime::from_bytes(&c)),
1126 modification_date: info_dict
1127 .get::<object::String>(MOD_DATE)
1128 .and_then(|c| DateTime::from_bytes(&c)),
1129 title: info_dict.get::<object::String>(TITLE).map(|t| t.to_vec()),
1130 author: info_dict.get::<object::String>(AUTHOR).map(|t| t.to_vec()),
1131 subject: info_dict.get::<object::String>(SUBJECT).map(|t| t.to_vec()),
1132 keywords: info_dict
1133 .get::<object::String>(KEYWORDS)
1134 .map(|t| t.to_vec()),
1135 creator: info_dict.get::<object::String>(CREATOR).map(|t| t.to_vec()),
1136 producer: info_dict
1137 .get::<object::String>(PRODUCER)
1138 .map(|t| t.to_vec()),
1139 }
1140}
1141
1142#[cfg(test)]
1143mod qf2b_objectstream_cache_tests {
1144 use crate::pdf::Pdf;
1148 use crate::xref::parse_object_stream_offsets;
1149
1150 fn objstm_fixture() -> Vec<u8> {
1163 fn deflate(data: &[u8]) -> Vec<u8> {
1164 use std::io::Write;
1165 let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
1166 e.write_all(data).expect("in-memory write");
1167 e.finish().expect("in-memory finish")
1168 }
1169
1170 let catalog = b"<< /Type /Catalog /Pages 3 0 R >>";
1171 let pages = b"<< /Type /Pages /Kids [] /Count 0 >>";
1172 let pairs = format!("2 0 3 {}", catalog.len() + 1);
1173 let first = pairs.len() + 1;
1174 let mut payload = pairs.into_bytes();
1175 payload.push(b'\n');
1176 payload.extend_from_slice(catalog);
1177 payload.push(b' ');
1178 payload.extend_from_slice(pages);
1179 let body = deflate(&payload);
1180
1181 let mut out = b"%PDF-1.5\n".to_vec();
1182 let objstm_at = out.len();
1183 out.extend_from_slice(
1184 format!(
1185 "1 0 obj\n<< /Type /ObjStm /N 2 /First {first} /Length {} \
1186 /Filter /FlateDecode >>\nstream\n",
1187 body.len()
1188 )
1189 .as_bytes(),
1190 );
1191 out.extend_from_slice(&body);
1192 out.extend_from_slice(b"\nendstream\nendobj\n");
1193
1194 let xref_at = out.len();
1195 let entry = |t: u8, a: usize, b: u8| {
1196 let mut v = vec![t];
1197 v.extend_from_slice(&(a as u16).to_be_bytes());
1198 v.push(b);
1199 v
1200 };
1201 let mut rows = Vec::new();
1202 rows.extend(entry(0, 0, 255));
1203 rows.extend(entry(1, objstm_at, 0));
1204 rows.extend(entry(2, 1, 0)); rows.extend(entry(2, 1, 1)); rows.extend(entry(1, xref_at, 0));
1207 let xbody = deflate(&rows);
1208 out.extend_from_slice(
1209 format!(
1210 "4 0 obj\n<< /Type /XRef /Size 5 /W [1 2 1] /Root 2 0 R /Length {} \
1211 /Filter /FlateDecode >>\nstream\n",
1212 xbody.len()
1213 )
1214 .as_bytes(),
1215 );
1216 out.extend_from_slice(&xbody);
1217 out.extend_from_slice(b"\nendstream\nendobj\n");
1218 out.extend_from_slice(format!("startxref\n{xref_at}\n%%EOF\n").as_bytes());
1219 out
1220 }
1221
1222 fn load_fixture() -> Pdf {
1226 Pdf::new(objstm_fixture()).expect("the generated /ObjStm fixture must parse")
1227 }
1228
1229 #[test]
1230 fn qf2b_objstm_cache_populates_and_is_stable_on_repeat() {
1231 let pdf = load_fixture();
1232
1233 let xref = pdf.xref();
1234
1235 let after_construction = xref.object_stream_offsets_cache_len();
1240 assert!(
1241 after_construction >= 1,
1242 "fixture is a PDF 1.5+ doc with /ObjStm; at least one offsets table should already be cached after construction; got {after_construction}"
1243 );
1244
1245 let _: Option<crate::object::Dict<'_>> = xref.get(xref.root_id());
1248 assert_eq!(
1249 xref.object_stream_offsets_cache_len(),
1250 after_construction,
1251 "repeated resolution of the same indirect object must reuse the cached offsets table"
1252 );
1253
1254 for raw in 1..=20i32 {
1258 let id = crate::object::ObjectIdentifier::new(raw, 0);
1259 let _: Option<crate::object::Dict<'_>> = xref.get(id);
1260 }
1261 let after_scan = xref.object_stream_offsets_cache_len();
1262
1263 for raw in 1..=20i32 {
1265 let id = crate::object::ObjectIdentifier::new(raw, 0);
1266 let _: Option<crate::object::Dict<'_>> = xref.get(id);
1267 }
1268 assert_eq!(
1269 xref.object_stream_offsets_cache_len(),
1270 after_scan,
1271 "repeated full scans must be cache-stable (no re-parse)"
1272 );
1273 }
1274
1275 #[test]
1276 fn qf2b_two_pdfs_have_independent_caches() {
1277 let pdf_a = load_fixture();
1278 let pdf_b = load_fixture();
1279
1280 let base_a = pdf_a.xref().object_stream_offsets_cache_len();
1283 let base_b = pdf_b.xref().object_stream_offsets_cache_len();
1284 assert_eq!(base_a, base_b);
1285
1286 for raw in 1..=50i32 {
1289 let id = crate::object::ObjectIdentifier::new(raw, 0);
1290 let _: Option<crate::object::Dict<'_>> = pdf_a.xref().get(id);
1291 }
1292 let warm_a = pdf_a.xref().object_stream_offsets_cache_len();
1293
1294 assert_eq!(
1296 pdf_b.xref().object_stream_offsets_cache_len(),
1297 base_b,
1298 "pdf_b cache must be independent of pdf_a's warming (base_a={base_a}, warm_a={warm_a}, base_b={base_b})"
1299 );
1300 }
1301
1302 #[test]
1303 fn qf2b_parse_helper_returns_none_on_truncated_header() {
1304 use crate::object::Stream;
1308 use crate::reader::{Reader, ReaderContext, ReaderExt};
1309 use crate::xref::DUMMY_XREF;
1310
1311 let raw: &[u8] = b"1 0 obj <</N 3 /First 6 /Length 4>>\nstream\n1 0 \nendstream\nendobj\n";
1314 let mut r = Reader::new(raw);
1315 let ctx = ReaderContext::new(&DUMMY_XREF, false);
1316 let stream: Stream<'_> = r
1317 .read_with_context::<crate::object::indirect::IndirectObject<Stream<'_>>>(&ctx)
1318 .expect("synthetic stream should parse")
1319 .get();
1320
1321 let body: &[u8] = b"1 0 ";
1324 assert!(
1325 parse_object_stream_offsets(&stream, body).is_none(),
1326 "truncated headers must not produce a partial offsets table"
1327 );
1328 }
1329
1330 #[test]
1339 #[ignore = "perf measurement; run with `cargo test --release -- --ignored qf2b_bench`"]
1340 fn qf2b_bench_offsets_parse_vs_cached() {
1341 use std::time::Instant;
1342
1343 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1352 .join("..")
1353 .join("..")
1354 .join("corpus")
1355 .join("f3800.pdf");
1356 let Ok(bytes) = std::fs::read(&path) else {
1357 eprintln!(
1358 "[qf2b_bench] fixture {} unavailable; skipping",
1359 path.display()
1360 );
1361 return;
1362 };
1363 let pdf = Pdf::new(bytes).expect("load f3800.pdf");
1364 let xref = pdf.xref();
1365
1366 let max_id = (xref.len() as i32).min(3000);
1369 for n in 1..=max_id {
1370 let id = crate::object::ObjectIdentifier::new(n, 0);
1371 let _: Option<crate::object::Object<'_>> = xref.get(id);
1372 }
1373 let cached_objstms = xref.object_stream_offsets_cache_len();
1374 assert!(
1375 cached_objstms >= 5,
1376 "fixture must trigger several /ObjStms (got {cached_objstms})"
1377 );
1378
1379 let mut objstm_ids: Vec<crate::object::ObjectIdentifier> = Vec::new();
1383 for n in 1..=max_id {
1384 let id = crate::object::ObjectIdentifier::new(n, 0);
1385 if let Some(stream) = xref.get::<crate::object::Stream<'_>>(id)
1387 && stream
1388 .dict()
1389 .get::<crate::object::Name>(crate::object::dict::keys::TYPE)
1390 .as_deref()
1391 == Some(b"ObjStm")
1392 {
1393 objstm_ids.push(id);
1394 }
1395 if objstm_ids.len() >= cached_objstms {
1396 break;
1397 }
1398 }
1399 let containers = objstm_ids.len();
1400 assert!(containers > 0);
1401
1402 const REPEATS: u32 = 200;
1405 let mut sink_parse = 0usize;
1406 let t_parse = Instant::now();
1407 for _ in 0..REPEATS {
1408 for id in &objstm_ids {
1409 let stream = xref
1410 .get::<crate::object::Stream<'_>>(*id)
1411 .expect("stream resolves");
1412 let Ok(decoded) = stream.decoded() else {
1413 continue;
1414 };
1415 if let Some(offs) = parse_object_stream_offsets(&stream, &decoded) {
1416 sink_parse = sink_parse.wrapping_add(offs.len());
1417 }
1418 }
1419 }
1420 let parse_elapsed = t_parse.elapsed();
1421
1422 let inner = match &xref.0 {
1425 crate::xref::Inner::Some(r) => r.clone(),
1426 _ => unreachable!(),
1427 };
1428 let mut sink_cache = 0usize;
1429 let t_cache = Instant::now();
1430 for _ in 0..REPEATS {
1431 for id in &objstm_ids {
1432 let offs = inner
1433 .data
1434 .get_object_stream_offsets_or_init(*id, || {
1435 let stream = xref
1436 .get::<crate::object::Stream<'_>>(*id)
1437 .expect("stream resolves");
1438 let decoded = stream.decoded().ok()?;
1439 parse_object_stream_offsets(&stream, &decoded)
1440 })
1441 .expect("cached entry must exist after warm-up");
1442 sink_cache = sink_cache.wrapping_add(offs.len());
1443 }
1444 }
1445 let cache_elapsed = t_cache.elapsed();
1446
1447 assert_eq!(
1448 sink_parse, sink_cache,
1449 "parsed and cached results must agree on offset-count totals"
1450 );
1451
1452 let speedup = parse_elapsed.as_secs_f64() / cache_elapsed.as_secs_f64().max(1e-9);
1453 let reduction = (1.0 - cache_elapsed.as_secs_f64() / parse_elapsed.as_secs_f64()) * 100.0;
1454
1455 eprintln!("[qf2b_bench] fixture: f3800.pdf");
1456 eprintln!("[qf2b_bench] /ObjStm containers measured: {containers}");
1457 eprintln!("[qf2b_bench] iterations per container: {REPEATS}");
1458 eprintln!("[qf2b_bench] direct re-parse total: {parse_elapsed:?}");
1459 eprintln!("[qf2b_bench] cached lookup total: {cache_elapsed:?}");
1460 eprintln!("[qf2b_bench] speedup: {speedup:.1}x");
1461 eprintln!("[qf2b_bench] parse-time reduction: {reduction:.1}%");
1462
1463 assert!(
1467 reduction >= 10.0,
1468 "QF2-B acceptance: ≥ 10 % parse-time reduction required; got {reduction:.2} %"
1469 );
1470 }
1471}
1472
1473#[cfg(test)]
1479mod upstream_hardening_tests {
1480 use super::*;
1481 use crate::object::dict::keys::V;
1482 use crate::pdf::Pdf;
1483
1484 #[test]
1485 fn xref_table_trailer_rejects_overflowing_entry_skip() {
1486 let data = b"xref\n0 999999999999999999999\ntrailer\n<<>>";
1487 let mut reader = Reader::new(data);
1488 assert!(read_xref_table_trailer(&mut reader, &ReaderContext::dummy()).is_none());
1489 }
1490
1491 #[test]
1492 fn xref_table_trailer_still_reads_a_sane_header() {
1493 let data = b"xref\n0 1\n0000000000 65535 f \ntrailer\n<< /Size 1 >>";
1494 let mut reader = Reader::new(data);
1495 assert!(read_xref_table_trailer(&mut reader, &ReaderContext::dummy()).is_some());
1496 }
1497
1498 fn objstm_cycle_fixture() -> Vec<u8> {
1504 use alloc::format;
1505
1506 let obj2 = b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>";
1507 let obj3 = b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] \
1508 /Resources << /ColorSpace << /CS0 5 0 R >> >> /Contents 4 0 R >>";
1509 let obj5 = b"[/Indexed 5 0 R 0 <00>]";
1510 let off3 = obj2.len() + 1;
1512 let off5 = off3 + obj3.len() + 1;
1513 let header = format!("2 0 3 {off3} 5 {off5}\n");
1514 let mut payload = header.into_bytes();
1515 payload.extend_from_slice(obj2);
1516 payload.push(b'\n');
1517 payload.extend_from_slice(obj3);
1518 payload.push(b'\n');
1519 payload.extend_from_slice(obj5);
1520 payload.push(b'\n');
1521
1522 let content = b"0 0 10 10 re f";
1523 let catalog = b"<< /Type /Catalog /Pages 2 0 R >>";
1524
1525 let mut out = b"%PDF-1.7\n".to_vec();
1526 let off1 = out.len();
1527 out.extend_from_slice(b"1 0 obj\n");
1528 out.extend_from_slice(catalog);
1529 out.extend_from_slice(b"\nendobj\n");
1530 let off4 = out.len();
1531 out.extend_from_slice(
1532 format!("4 0 obj\n<< /Length {} >>\nstream\n", content.len()).as_bytes(),
1533 );
1534 out.extend_from_slice(content);
1535 out.extend_from_slice(b"\nendstream\nendobj\n");
1536 let off6 = out.len();
1537 out.extend_from_slice(
1538 format!(
1539 "6 0 obj\n<< /Type /ObjStm /N 3 /First {} /Length {} >>\nstream\n",
1540 payload.len() - (obj2.len() + 1 + obj3.len() + 1 + obj5.len() + 1),
1541 payload.len()
1542 )
1543 .as_bytes(),
1544 );
1545 out.extend_from_slice(&payload);
1546 out.extend_from_slice(b"\nendstream\nendobj\n");
1547
1548 let off7 = out.len();
1549 let entry = |t: u8, a: usize, b: u8| {
1550 let mut v = vec![t];
1551 v.extend_from_slice(&(a as u16).to_be_bytes());
1552 v.push(b);
1553 v
1554 };
1555 let mut rows = Vec::new();
1556 rows.extend(entry(0, 0, 255));
1557 rows.extend(entry(1, off1, 0));
1558 rows.extend(entry(2, 6, 0));
1559 rows.extend(entry(2, 6, 1));
1560 rows.extend(entry(1, off4, 0));
1561 rows.extend(entry(2, 6, 2));
1562 rows.extend(entry(1, off6, 0));
1563 rows.extend(entry(1, off7, 0));
1564 out.extend_from_slice(
1565 format!(
1566 "7 0 obj\n<< /Type /XRef /Size 8 /W [1 2 1] /Root 1 0 R /Length {} >>\nstream\n",
1567 rows.len()
1568 )
1569 .as_bytes(),
1570 );
1571 out.extend_from_slice(&rows);
1572 out.extend_from_slice(b"\nendstream\nendobj\n");
1573 out.extend_from_slice(format!("startxref\n{off7}\n%%EOF\n").as_bytes());
1574 out
1575 }
1576
1577 #[test]
1585 fn objstm_cycle_through_second_stream_resolves_to_none() {
1586 let pdf = Pdf::new(objstm_cycle_fixture()).expect("the cyclic fixture must parse");
1587 let xref = pdf.xref();
1588 let array: Array<'_> = xref
1589 .get(ObjectIdentifier::new(5, 0))
1590 .expect("the cyclic array itself must resolve");
1591 let mut items = array.flex_iter();
1592 assert!(items.next::<Name>().is_some(), "element 0 is /Indexed");
1593 assert!(
1594 items.next::<Array<'_>>().is_none(),
1595 "the self-referential base space (5 0 R) must give up, not resolve again"
1596 );
1597 }
1598
1599 fn broken_xref_fixture() -> Vec<u8> {
1605 use alloc::format;
1606
1607 let mut out = b"%PDF-1.7\n".to_vec();
1608 let mut offsets = Vec::new();
1609 let bodies: Vec<Vec<u8>> = (1..=10)
1611 .map(|n| {
1612 if n == 1 {
1613 b"<< /Type /Catalog /Pages 2 0 R >>".to_vec()
1614 } else if n == 2 {
1615 b"<< /Type /Pages /Kids [] /Count 0 >>".to_vec()
1616 } else {
1617 format!("<< /V {} >>", 100 + n).into_bytes()
1618 }
1619 })
1620 .collect();
1621 for (i, body) in bodies.iter().enumerate() {
1622 offsets.push(out.len());
1623 out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
1624 out.extend_from_slice(body);
1625 out.extend_from_slice(b"\nendobj\n");
1626 }
1627
1628 let xref_at = out.len();
1629 out.extend_from_slice(b"xref\n0 11\n0000000000 65535 f \n");
1630 for (i, offset) in offsets.iter().enumerate() {
1631 let recorded = if i < 2 { *offset } else { 1 };
1633 out.extend_from_slice(format!("{recorded:010} 00000 n \n").as_bytes());
1634 }
1635 out.extend_from_slice(
1636 format!("trailer\n<< /Size 11 /Root 1 0 R >>\nstartxref\n{xref_at}\n%%EOF\n")
1637 .as_bytes(),
1638 );
1639 out
1640 }
1641
1642 #[test]
1646 fn xref_repair_is_idempotent() {
1647 let pdf = Pdf::new(objstm_cycle_fixture()).expect("the fixture must parse");
1648 pdf.xref().repair();
1649 pdf.xref().repair();
1650 }
1651
1652 #[test]
1658 fn concurrent_xref_repair_resolves_after_repair() {
1659 use std::sync::Barrier;
1660
1661 for _ in 0..16 {
1662 let pdf = Pdf::new(broken_xref_fixture()).expect("the broken fixture must load");
1663 let barrier = Barrier::new(8);
1664
1665 std::thread::scope(|scope| {
1666 for thread in 0..8 {
1667 let pdf = &pdf;
1668 let barrier = &barrier;
1669 scope.spawn(move || {
1670 barrier.wait();
1671
1672 for offset in 0..8 {
1674 let number = 3 + (thread + offset) % 8;
1675 let value = pdf
1676 .xref()
1677 .get::<Dict<'_>>(ObjectIdentifier::new(number, 0))
1678 .and_then(|dict| dict.get::<i32>(V));
1679 assert_eq!(value, Some(100 + number));
1680 }
1681 });
1682 }
1683 });
1684 }
1685 }
1686
1687 fn concurrent_objstm_fixture() -> Vec<u8> {
1692 use alloc::format;
1693 use std::io::Write;
1694
1695 fn deflate(data: &[u8]) -> Vec<u8> {
1696 let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
1697 e.write_all(data).expect("in-memory write");
1698 e.finish().expect("in-memory finish")
1699 }
1700
1701 fn pack(first_id: i32, count: usize) -> (Vec<u8>, usize) {
1704 use alloc::format;
1705 use alloc::string::String;
1706 use alloc::vec::Vec;
1707 use core::fmt::Write;
1708
1709 let bodies: Vec<Vec<u8>> = (0..count)
1710 .map(|i| format!("<< /V {} >>", 100 + first_id + i as i32).into_bytes())
1711 .collect();
1712 let mut header = String::new();
1713 let mut at = 0;
1714 for (i, body) in bodies.iter().enumerate() {
1715 write!(header, "{} {at} ", first_id + i as i32).expect("in-memory write");
1716 at += body.len() + 1;
1717 }
1718 let first = header.len();
1719 let mut payload = header.into_bytes();
1720 for body in &bodies {
1721 payload.extend_from_slice(body);
1722 payload.push(b'\n');
1723 }
1724 (payload, first)
1725 }
1726
1727 let catalog = b"<< /Type /Catalog /Pages 2 0 R >>";
1728 let pages = b"<< /Type /Pages /Kids [] /Count 0 >>";
1729 let (payload_a, first_a) = pack(6, 12);
1730 let (payload_b, first_b) = pack(18, 12);
1731
1732 let mut out = b"%PDF-1.5\n".to_vec();
1733 let off1 = out.len();
1734 out.extend_from_slice(b"1 0 obj\n");
1735 out.extend_from_slice(catalog);
1736 out.extend_from_slice(b"\nendobj\n");
1737 let off2 = out.len();
1738 out.extend_from_slice(b"2 0 obj\n");
1739 out.extend_from_slice(pages);
1740 out.extend_from_slice(b"\nendobj\n");
1741 let n_a = deflate(&payload_a);
1742 let off3 = out.len();
1743 out.extend_from_slice(
1744 format!(
1745 "3 0 obj\n<< /Type /ObjStm /N 12 /First {first_a} /Length {} /Filter /FlateDecode >>\nstream\n",
1746 n_a.len()
1747 )
1748 .as_bytes(),
1749 );
1750 out.extend_from_slice(&n_a);
1751 out.extend_from_slice(b"\nendstream\nendobj\n");
1752 let n_b = deflate(&payload_b);
1753 let off4 = out.len();
1754 out.extend_from_slice(
1755 format!(
1756 "4 0 obj\n<< /Type /ObjStm /N 12 /First {first_b} /Length {} /Filter /FlateDecode >>\nstream\n",
1757 n_b.len()
1758 )
1759 .as_bytes(),
1760 );
1761 out.extend_from_slice(&n_b);
1762 out.extend_from_slice(b"\nendstream\nendobj\n");
1763
1764 let off30 = out.len();
1765 let entry = |t: u8, a: usize, b: u8| {
1766 let mut v = vec![t];
1767 v.extend_from_slice(&(a as u16).to_be_bytes());
1768 v.push(b);
1769 v
1770 };
1771 let mut rows = Vec::new();
1772 for n in 0..31 {
1774 match n {
1775 0 | 5 => rows.extend(entry(0, 0, 255)),
1776 1 => rows.extend(entry(1, off1, 0)),
1777 2 => rows.extend(entry(1, off2, 0)),
1778 3 => rows.extend(entry(1, off3, 0)),
1779 4 => rows.extend(entry(1, off4, 0)),
1780 6..=17 => rows.extend(entry(2, 3, (n - 6) as u8)),
1781 18..=29 => rows.extend(entry(2, 4, (n - 18) as u8)),
1782 30 => rows.extend(entry(1, off30, 0)),
1783 _ => unreachable!(),
1784 }
1785 }
1786 out.extend_from_slice(
1787 format!(
1788 "30 0 obj\n<< /Type /XRef /Size 31 /W [1 2 1] /Root 1 0 R /Length {} >>\nstream\n",
1789 rows.len()
1790 )
1791 .as_bytes(),
1792 );
1793 out.extend_from_slice(&rows);
1794 out.extend_from_slice(b"\nendstream\nendobj\n");
1795 out.extend_from_slice(format!("startxref\n{off30}\n%%EOF\n").as_bytes());
1796 out
1797 }
1798
1799 #[test]
1804 fn concurrent_objstm_resolution_publishes_atomically() {
1805 use std::sync::Barrier;
1806
1807 for _ in 0..16 {
1808 let pdf = Pdf::new(concurrent_objstm_fixture()).expect("the fixture must load");
1809 let barrier = Barrier::new(8);
1810
1811 std::thread::scope(|scope| {
1812 for thread in 0..8 {
1813 let pdf = &pdf;
1814 let barrier = &barrier;
1815 scope.spawn(move || {
1816 barrier.wait();
1817
1818 for offset in 0..24 {
1820 let number = 6 + (thread * 12 + offset) % 24;
1821 let value = pdf
1822 .xref()
1823 .get::<Dict<'_>>(ObjectIdentifier::new(number, 0))
1824 .and_then(|dict| dict.get::<i32>(V));
1825 assert_eq!(value, Some(100 + number));
1826 }
1827 });
1828 }
1829 });
1830 }
1831 }
1832}