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, Mutex, MutexExt, 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 decoded_streams: DecodedStreams::default(),
265 })));
266
267 let decryptor = {
272 match input {
273 XRefInput::TrailerDictData(trailer_dict_data) => {
274 let mut r = Reader::new(trailer_dict_data);
275
276 let trailer_dict = r
277 .read_with_context::<Dict<'_>>(&ReaderContext::new(&xref, false))
278 .ok_or(XRefError::Unknown)?;
279
280 get_decryptor(&trailer_dict, password)?
281 }
282 XRefInput::RootRef(_) => Decryptor::None,
283 }
284 };
285
286 match &mut xref.0 {
287 Inner::Dummy => unreachable!(),
288 Inner::Some(r) => {
289 let mutable = Arc::make_mut(r);
290 mutable.decryptor = Arc::new(decryptor.clone());
291 }
292 }
293
294 let (trailer_data, has_ocgs, metadata) = match input {
295 XRefInput::TrailerDictData(trailer_dict_data) => {
296 let mut r = Reader::new(trailer_dict_data);
297
298 let trailer_dict = r
299 .read_with_context::<Dict<'_>>(&ReaderContext::new(&xref, false))
300 .ok_or(XRefError::Unknown)?;
301
302 let root_ref = trailer_dict.get_ref(ROOT).ok_or(XRefError::Unknown)?;
303 let root = trailer_dict
304 .get::<Dict<'_>>(ROOT)
305 .ok_or(XRefError::Unknown)?;
306 let metadata = trailer_dict
307 .get::<Dict<'_>>(INFO)
308 .map(|d| parse_metadata(&d))
309 .unwrap_or_default();
310 let pages_ref = root.get_ref(PAGES).ok_or(XRefError::Unknown)?;
311 let has_ocgs = root.get::<Dict<'_>>(OCPROPERTIES).is_some();
312 let version = root
313 .get::<Name>(VERSION)
314 .and_then(|v| PdfVersion::from_bytes(v.deref()));
315
316 let td = TrailerData {
317 pages_ref: pages_ref.into(),
318 root_ref: root_ref.into(),
319 version,
320 };
321
322 (td, has_ocgs, metadata)
323 }
324 XRefInput::RootRef(root_ref) => {
325 let root = xref.get::<Dict<'_>>(root_ref).ok_or(XRefError::Unknown)?;
326 let pages_ref = root.get_ref(PAGES).ok_or(XRefError::Unknown)?;
327
328 let td = TrailerData {
329 pages_ref: pages_ref.into(),
330 root_ref,
331 version: None,
332 };
333
334 (td, false, Metadata::default())
335 }
336 };
337
338 match &mut xref.0 {
339 Inner::Dummy => unreachable!(),
340 Inner::Some(r) => {
341 let mutable = Arc::make_mut(r);
342 mutable.trailer_data = trailer_data;
343 mutable.decryptor = Arc::new(decryptor);
344 mutable.has_ocgs = has_ocgs;
345 mutable.metadata = Arc::new(metadata);
346 }
347 }
348
349 Ok(xref)
350 }
351
352 pub(crate) fn dummy() -> &'static Self {
353 &DUMMY_XREF
354 }
355
356 pub(crate) fn load_limits(&self) -> PdfLoadLimits {
357 match &self.0 {
358 Inner::Dummy => PdfLoadLimits::default(),
359 Inner::Some(r) => r.load_limits,
360 }
361 }
362
363 pub(crate) fn len(&self) -> usize {
364 match &self.0 {
365 Inner::Dummy => 0,
366 Inner::Some(r) => r.map.get().xref_map.len(),
367 }
368 }
369
370 pub(crate) fn trailer_data(&self) -> &TrailerData {
371 match &self.0 {
372 Inner::Dummy => unreachable!(),
373 Inner::Some(r) => &r.trailer_data,
374 }
375 }
376
377 #[cfg(test)]
380 pub(crate) fn object_stream_offsets_cache_len(&self) -> usize {
381 match &self.0 {
382 Inner::Dummy => 0,
383 Inner::Some(r) => r.data.object_stream_offsets_cache_len(),
384 }
385 }
386
387 pub(crate) fn metadata(&self) -> &Metadata {
388 match &self.0 {
389 Inner::Dummy => unreachable!(),
390 Inner::Some(r) => &r.metadata,
391 }
392 }
393
394 pub fn root_id(&self) -> ObjectIdentifier {
396 self.trailer_data().root_ref
397 }
398
399 pub fn has_optional_content_groups(&self) -> bool {
401 match &self.0 {
402 Inner::Dummy => false,
403 Inner::Some(r) => r.has_ocgs,
404 }
405 }
406
407 pub(crate) fn objects(&self) -> impl IntoIterator<Item = Object<'_>> + '_ {
408 match &self.0 {
409 Inner::Dummy => unimplemented!(),
410 Inner::Some(r) => {
411 let locked = r.map.get();
412 let mut elements = locked
413 .xref_map
414 .iter()
415 .map(|(id, e)| {
416 let offset = match e {
417 EntryType::Normal(o) => (*o, 0),
418 EntryType::ObjStream(id, index) => {
419 if let Some(EntryType::Normal(offset)) =
420 locked.xref_map.get(&ObjectIdentifier::new(*id as i32, 0))
421 {
422 (*offset, *index)
423 } else {
424 (usize::MAX, 0)
425 }
426 }
427 };
428
429 (*id, offset)
430 })
431 .collect::<Vec<_>>();
432
433 elements.sort_by_key(|e1| e1.1);
436
437 let mut iter = elements.into_iter();
438
439 iter::from_fn(move || {
440 for next in iter.by_ref() {
441 if let Some(obj) = self.get_with(next.0, &ReaderContext::new(self, false)) {
442 return Some(obj);
443 } else {
444 continue;
446 }
447 }
448
449 None
450 })
451 }
452 }
453 }
454
455 pub(crate) fn repair(&self) {
456 let Inner::Some(r) = &self.0 else {
457 unreachable!();
458 };
459
460 let mut locked = r.map.put();
461 if locked.repaired {
462 return;
463 }
464
465 let (xref_map, _) = fallback_xref_map(r.data.get(), &r.password);
466 locked.xref_map = xref_map;
467 locked.repaired = true;
468 }
469
470 #[inline]
471 pub(crate) fn needs_decryption(&self, ctx: &ReaderContext<'_>) -> bool {
472 match &self.0 {
473 Inner::Dummy => false,
474 Inner::Some(r) => {
475 if matches!(r.decryptor.as_ref(), Decryptor::None) {
476 false
477 } else {
478 !ctx.in_content_stream() && !ctx.in_object_stream()
479 }
480 }
481 }
482 }
483
484 #[inline]
485 pub(crate) fn decoded_stream<E>(
489 &self,
490 id: ObjectIdentifier,
491 decode: impl FnOnce() -> Result<Vec<u8>, E>,
492 ) -> Result<Arc<Vec<u8>>, E> {
493 let Inner::Some(repr) = &self.0 else {
494 return decode().map(Arc::new);
495 };
496 let cache = &repr.decoded_streams.0;
497 {
498 let mut g = cache.get();
499 if let Some(hit) = g.map.get(&id).cloned() {
500 g.stats.hits += 1;
501 return Ok(hit);
502 }
503 }
504 let decoded = Arc::new(decode()?);
505 let mut g = cache.get();
506 g.stats.decodes += 1;
507 if g.stats.bytes + decoded.len() <= DECODED_STREAMS_BYTE_BUDGET
508 && g.map.insert(id, decoded.clone()).is_none()
509 {
510 g.stats.entries += 1;
511 g.stats.bytes += decoded.len();
512 }
513 Ok(decoded)
514 }
515
516 pub fn shared_decode_stats(&self) -> SharedDecodeStats {
518 match &self.0 {
519 Inner::Dummy => SharedDecodeStats::default(),
520 Inner::Some(repr) => repr.decoded_streams.0.get().stats,
521 }
522 }
523
524 pub(crate) fn decrypt(
525 &self,
526 id: ObjectIdentifier,
527 data: &[u8],
528 target: DecryptionTarget,
529 ) -> Option<Vec<u8>> {
530 match &self.0 {
531 Inner::Dummy => Some(data.to_vec()),
532 Inner::Some(r) => r.decryptor.decrypt(id, data, target),
533 }
534 }
535
536 #[allow(private_bounds)]
538 pub fn get<'a, T>(&'a self, id: ObjectIdentifier) -> Option<T>
539 where
540 T: ObjectLike<'a>,
541 {
542 let ctx = ReaderContext::new(self, false);
543 self.get_with(id, &ctx)
544 }
545
546 #[allow(private_bounds)]
548 pub(crate) fn get_with<'a, T>(
549 &'a self,
550 id: ObjectIdentifier,
551 ctx: &ReaderContext<'a>,
552 ) -> Option<T>
553 where
554 T: ObjectLike<'a>,
555 {
556 let Inner::Some(repr) = &self.0 else {
557 return None;
558 };
559
560 let (entry, was_repaired) = {
562 let locked = repr.map.get();
563 (*locked.xref_map.get(&id)?, locked.repaired)
566 };
567
568 let mut r = Reader::new(repr.data.get().as_ref());
569
570 let mut ctx = ctx.clone();
571 ctx.set_obj_number(id);
572 ctx.set_in_content_stream(false);
573
574 match entry {
575 EntryType::Normal(offset) => {
576 ctx.set_in_object_stream(false);
577 r.jump(offset);
578
579 if let Some(object) = r.read_with_context::<IndirectObject<T>>(&ctx) {
580 if object.id() == &id {
581 return Some(object.get());
582 }
583 } else {
584 if r.skip_not_in_content_stream::<IndirectObject<Object<'_>>>()
587 .is_some()
588 {
589 return None;
590 }
591 };
592
593 if was_repaired {
595 error!(
596 "attempt was made at repairing xref, but object {id:?} still couldn't be read"
597 );
598
599 None
600 } else {
601 warn!("broken xref, attempting to repair");
602
603 self.repair();
604
605 self.get_with::<T>(id, &ctx)
607 }
608 }
609 EntryType::ObjStream(obj_stram_gen_num, index) => {
610 let obj_stream_id = ObjectIdentifier::new(obj_stram_gen_num as i32, 0);
612
613 if obj_stream_id == id || ctx.parent_chain_contains(&id) {
614 warn!("cycle detected in object stream");
615
616 return None;
617 }
618
619 ctx.parent_chain_push(id);
620
621 let stream = self.get_with::<Stream<'_>>(obj_stream_id, &ctx)?;
622 let data = repr.data.get_with(obj_stream_id, &ctx)?;
623 let offsets = repr
628 .data
629 .get_object_stream_offsets_or_init(obj_stream_id, || {
630 parse_object_stream_offsets(&stream, data)
631 })?;
632 let object_stream = ObjectStream::from_cached_offsets(data, &ctx, offsets);
633 object_stream.get(index)
634 }
635 }
636 }
637}
638
639#[derive(Debug, Copy, Clone)]
642pub(crate) enum XRefInput<'a> {
643 TrailerDictData(&'a [u8]),
646 RootRef(ObjectIdentifier),
654}
655
656pub(crate) fn find_last_xref_pos(data: &[u8]) -> Option<usize> {
657 let mut finder = Reader::new(data);
658 let mut pos = finder.len().checked_sub(1)?;
659 finder.jump(pos);
660
661 let needle = b"startxref";
662
663 loop {
664 if finder.forward_tag(needle).is_some() {
665 finder.skip_white_spaces_and_comments();
666
667 let offset = finder.read_without_context::<i32>()?.try_into().ok()?;
668
669 return Some(offset);
670 }
671
672 pos = pos.checked_sub(1)?;
673 finder.jump(pos);
674 }
675}
676
677#[derive(Debug, PartialEq, Eq, Clone, Copy)]
679enum EntryType {
680 Normal(usize),
682 ObjStream(u32, u32),
686}
687
688type XrefMap = FxHashMap<ObjectIdentifier, EntryType>;
689
690#[derive(Debug)]
692struct MapRepr {
693 xref_map: XrefMap,
694 repaired: bool,
695}
696
697#[derive(Debug, Copy, Clone)]
698pub(crate) struct TrailerData {
699 pub(crate) pages_ref: ObjectIdentifier,
700 pub(crate) root_ref: ObjectIdentifier,
701 pub(crate) version: Option<PdfVersion>,
702}
703
704impl TrailerData {
705 pub(crate) fn dummy() -> Self {
706 Self {
707 pages_ref: ObjectIdentifier::new(0, 0),
708 root_ref: ObjectIdentifier::new(0, 0),
709 version: None,
710 }
711 }
712}
713
714#[derive(Debug, Clone)]
715struct SomeRepr {
716 data: Arc<Data>,
717 map: Arc<RwLock<MapRepr>>,
718 metadata: Arc<Metadata>,
719 decryptor: Arc<Decryptor>,
720 has_ocgs: bool,
721 password: Vec<u8>,
722 trailer_data: TrailerData,
723 load_limits: PdfLoadLimits,
724 decoded_streams: DecodedStreams,
726}
727
728const DECODED_STREAMS_BYTE_BUDGET: usize = 64 * 1024 * 1024;
731
732#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
734pub struct SharedDecodeStats {
735 pub hits: usize,
737 pub decodes: usize,
739 pub entries: usize,
741 pub bytes: usize,
743}
744
745#[derive(Debug, Default)]
746struct DecodedStreamsInner {
747 map: FxHashMap<ObjectIdentifier, Arc<Vec<u8>>>,
748 stats: SharedDecodeStats,
749}
750
751#[derive(Debug, Default, Clone)]
757struct DecodedStreams(Arc<Mutex<DecodedStreamsInner>>);
758
759#[derive(Debug, Clone)]
760enum Inner {
761 Dummy,
763 Some(Arc<SomeRepr>),
765}
766
767#[derive(Debug)]
768struct XRefEntry {
769 offset: usize,
770 gen_number: i32,
771 used: bool,
772}
773
774impl XRefEntry {
775 pub(crate) fn read(data: &[u8]) -> Option<Self> {
776 #[inline(always)]
777 fn parse_u32(data: &[u8]) -> Option<u32> {
778 let mut accum = 0_u32;
779
780 for byte in data {
781 accum = accum.checked_mul(10)?;
782
783 match *byte {
784 b'0'..=b'9' => accum = accum.checked_add((*byte - b'0') as u32)?,
785 _ => return None,
786 }
787 }
788
789 Some(accum)
790 }
791
792 let offset = parse_u32(&data[0..10])? as usize;
793 let gen_number = i32::try_from(parse_u32(&data[11..16])?).ok()?;
794
795 let used = data[17] == b'n';
796
797 Some(Self {
798 offset,
799 gen_number,
800 used,
801 })
802 }
803}
804
805const MAX_XREF_CHAIN_DEPTH: usize = 64;
808
809fn populate_xref_impl<'a>(data: &'a [u8], pos: usize, xref_map: &mut XrefMap) -> Option<&'a [u8]> {
810 populate_xref_depth(data, pos, xref_map, 0)
811}
812
813fn populate_xref_depth<'a>(
814 data: &'a [u8],
815 pos: usize,
816 xref_map: &mut XrefMap,
817 depth: usize,
818) -> Option<&'a [u8]> {
819 if depth > MAX_XREF_CHAIN_DEPTH {
820 log::warn!("Xref chain depth exceeds {MAX_XREF_CHAIN_DEPTH}, stopping traversal");
821 return None;
822 }
823 let mut reader = Reader::new(data);
824 reader.jump(pos);
825 reader.skip_white_spaces_and_comments();
827
828 let mut r2 = reader.clone();
829 if reader
830 .clone()
831 .read_without_context::<ObjectIdentifier>()
832 .is_some()
833 {
834 populate_from_xref_stream(data, &mut r2, xref_map, depth)
835 } else {
836 populate_from_xref_table(data, &mut r2, xref_map, depth)
837 }
838}
839
840pub(super) struct SubsectionHeader {
841 pub(super) start: u32,
842 pub(super) num_entries: u32,
843}
844
845impl Readable<'_> for SubsectionHeader {
846 fn read(r: &mut Reader<'_>, _: &ReaderContext<'_>) -> Option<Self> {
847 r.skip_white_spaces();
848 let start = r.read_without_context::<u32>()?;
849 r.skip_white_spaces();
850 let num_entries = r.read_without_context::<u32>()?;
851 r.skip_white_spaces();
852
853 Some(Self { start, num_entries })
854 }
855}
856
857fn populate_from_xref_table<'a>(
859 data: &'a [u8],
860 reader: &mut Reader<'a>,
861 insert_map: &mut XrefMap,
862 depth: usize,
863) -> Option<&'a [u8]> {
864 let trailer = {
865 let mut reader = reader.clone();
866 read_xref_table_trailer(&mut reader, &ReaderContext::dummy())?
867 };
868
869 reader.skip_white_spaces();
870 reader.forward_tag(b"xref")?;
871 reader.skip_white_spaces();
872
873 let mut max_obj = 0;
874
875 if let Some(prev) = trailer.get::<i32>(PREV) {
876 populate_xref_depth(data, prev as usize, insert_map, depth + 1)?;
878 }
879
880 if let Some(xref_stm) = trailer.get::<i32>(XREF_STM) {
883 populate_xref_depth(data, xref_stm as usize, insert_map, depth + 1)?;
884 }
885
886 while let Some(header) = reader.read_without_context::<SubsectionHeader>() {
887 reader.skip_white_spaces();
888
889 let start = header.start;
890 let end = start.checked_add(header.num_entries)?;
893
894 for obj_number in start..end {
895 max_obj = max(max_obj, obj_number);
896 let bytes = reader.read_bytes(XREF_ENTRY_LEN)?;
897 let entry = XRefEntry::read(bytes)?;
898
899 if entry.used {
902 insert_map.insert(
903 ObjectIdentifier::new(obj_number as i32, entry.gen_number),
904 EntryType::Normal(entry.offset),
905 );
906 }
907 }
908 }
909
910 Some(trailer.data())
911}
912
913fn populate_from_xref_stream<'a>(
914 data: &'a [u8],
915 reader: &mut Reader<'a>,
916 insert_map: &mut XrefMap,
917 depth: usize,
918) -> Option<&'a [u8]> {
919 let stream = reader
920 .read_with_context::<IndirectObject<Stream<'_>>>(&ReaderContext::dummy())?
921 .get();
922
923 if let Some(prev) = stream.dict().get::<i32>(PREV) {
924 let _ = populate_xref_depth(data, prev as usize, insert_map, depth + 1)?;
926 }
927
928 let size = stream.dict().get::<u32>(SIZE)?;
929
930 let [f1_len, f2_len, f3_len] = stream.dict().get::<[u8; 3]>(W)?;
931
932 if f2_len > size_of::<u64>() as u8 {
933 error!("xref offset length is larger than the allowed limit");
934
935 return None;
936 }
937
938 if f1_len != 1 {
940 warn!("first field in xref stream was longer than 1");
941 }
942
943 let xref_data = stream.decoded().ok()?;
944 let mut xref_reader = Reader::new(xref_data.as_ref());
945
946 if let Some(arr) = stream.dict().get::<Array<'_>>(INDEX) {
947 let iter = arr.iter::<(u32, u32)>();
948
949 for (start, num_elements) in iter {
950 xref_stream_subsection(
951 &mut xref_reader,
952 start,
953 num_elements,
954 f1_len,
955 f2_len,
956 f3_len,
957 insert_map,
958 )?;
959 }
960 } else {
961 xref_stream_subsection(
962 &mut xref_reader,
963 0,
964 size,
965 f1_len,
966 f2_len,
967 f3_len,
968 insert_map,
969 )?;
970 }
971
972 Some(stream.dict().data())
973}
974
975fn xref_stream_num(data: &[u8]) -> Option<u32> {
976 Some(match data.len() {
977 0 => return None,
978 1 => u8::from_be(data[0]) as u32,
979 2 => u16::from_be_bytes(data[0..2].try_into().ok()?) as u32,
980 3 => u32::from_be_bytes([0, data[0], data[1], data[2]]),
981 4 => u32::from_be_bytes(data[0..4].try_into().ok()?),
982 8 => {
983 if let Ok(num) = u32::try_from(u64::from_be_bytes(data[0..8].try_into().ok()?)) {
984 return Some(num);
985 } else {
986 warn!("xref stream number is too large");
987
988 return None;
989 }
990 }
991 n => {
992 warn!("invalid xref stream number {n}");
993
994 return None;
995 }
996 })
997}
998
999fn xref_stream_subsection<'a>(
1000 xref_reader: &mut Reader<'a>,
1001 start: u32,
1002 num_elements: u32,
1003 f1_len: u8,
1004 f2_len: u8,
1005 f3_len: u8,
1006 insert_map: &mut XrefMap,
1007) -> Option<()> {
1008 for i in 0..num_elements {
1009 let f_type = if f1_len == 0 {
1010 1
1011 } else {
1012 xref_reader.read_bytes(1)?[0]
1014 };
1015
1016 let obj_number = start + i;
1017
1018 match f_type {
1019 0 => {
1021 xref_reader.skip_bytes(f2_len as usize + f3_len as usize)?;
1022 }
1023 1 => {
1024 let offset = if f2_len > 0 {
1025 let data = xref_reader.read_bytes(f2_len as usize)?;
1026 xref_stream_num(data)?
1027 } else {
1028 0
1029 };
1030
1031 let gen_number = if f3_len > 0 {
1032 let data = xref_reader.read_bytes(f3_len as usize)?;
1033 xref_stream_num(data)?
1034 } else {
1035 0
1036 };
1037
1038 insert_map.insert(
1039 ObjectIdentifier::new(obj_number as i32, gen_number as i32),
1040 EntryType::Normal(offset as usize),
1041 );
1042 }
1043 2 => {
1044 let obj_stream_number = {
1045 let data = xref_reader.read_bytes(f2_len as usize)?;
1046 xref_stream_num(data)?
1047 };
1048 let gen_number = 0;
1049 let index = if f3_len > 0 {
1050 let data = xref_reader.read_bytes(f3_len as usize)?;
1051 xref_stream_num(data)?
1052 } else {
1053 0
1054 };
1055
1056 insert_map.insert(
1057 ObjectIdentifier::new(obj_number as i32, gen_number),
1058 EntryType::ObjStream(obj_stream_number, index),
1059 );
1060 }
1061 _ => {
1062 warn!("xref has unknown field type {f_type}");
1063
1064 return None;
1065 }
1066 }
1067 }
1068
1069 Some(())
1070}
1071
1072fn read_xref_table_trailer<'a>(
1073 reader: &mut Reader<'a>,
1074 ctx: &ReaderContext<'a>,
1075) -> Option<Dict<'a>> {
1076 reader.skip_white_spaces();
1077 reader.forward_tag(b"xref")?;
1078 reader.skip_white_spaces();
1079
1080 while let Some(header) = reader.read_without_context::<SubsectionHeader>() {
1081 let len = XREF_ENTRY_LEN.checked_mul(header.num_entries as usize)?;
1082 reader.jump(reader.offset().checked_add(len)?);
1083 }
1084
1085 reader.skip_white_spaces();
1086 reader.forward_tag(b"trailer")?;
1087 reader.skip_white_spaces();
1088
1089 reader.read_with_context::<Dict<'_>>(ctx)
1090}
1091
1092fn get_decryptor(trailer_dict: &Dict<'_>, password: &[u8]) -> Result<Decryptor, XRefError> {
1093 if let Some(encryption_dict) = trailer_dict.get::<Dict<'_>>(ENCRYPT) {
1094 let id = if let Some(id) = trailer_dict
1095 .get::<Array<'_>>(ID)
1096 .and_then(|a| a.flex_iter().next::<object::String>())
1097 {
1098 id.to_vec()
1099 } else {
1100 vec![]
1102 };
1103
1104 get(&encryption_dict, &id, password).map_err(XRefError::Encryption)
1105 } else {
1106 Ok(Decryptor::None)
1107 }
1108}
1109
1110fn parse_object_stream_offsets(
1119 inner: &Stream<'_>,
1120 data: &[u8],
1121) -> Option<crate::data::ObjectStreamOffsets> {
1122 let num_objects = inner.dict().get::<usize>(N)?;
1123 let first_offset = inner.dict().get::<usize>(FIRST)?;
1124
1125 let mut r = Reader::new(data);
1126 let mut offsets = Vec::with_capacity(num_objects);
1127
1128 for _ in 0..num_objects {
1129 r.skip_white_spaces_and_comments();
1130 let obj_num = r.read_without_context::<u32>()?;
1132 r.skip_white_spaces_and_comments();
1133 let relative_offset = r.read_without_context::<usize>()?;
1134 offsets.push((obj_num, first_offset + relative_offset));
1135 }
1136
1137 Some(offsets)
1138}
1139
1140struct ObjectStream<'a> {
1149 data: &'a [u8],
1150 ctx: ReaderContext<'a>,
1151 offsets: Arc<crate::data::ObjectStreamOffsets>,
1152}
1153
1154impl<'a> ObjectStream<'a> {
1155 fn new(inner: Stream<'_>, data: &'a [u8], ctx: &ReaderContext<'a>) -> Option<Self> {
1159 let offsets = Arc::new(parse_object_stream_offsets(&inner, data)?);
1160
1161 let mut ctx = ctx.clone();
1162 ctx.set_in_object_stream(true);
1163
1164 Some(Self { data, ctx, offsets })
1165 }
1166
1167 fn from_cached_offsets(
1171 data: &'a [u8],
1172 ctx: &ReaderContext<'a>,
1173 offsets: Arc<crate::data::ObjectStreamOffsets>,
1174 ) -> Self {
1175 let mut ctx = ctx.clone();
1176 ctx.set_in_object_stream(true);
1177
1178 Self { data, ctx, offsets }
1179 }
1180
1181 fn get<T>(&self, index: u32) -> Option<T>
1182 where
1183 T: ObjectLike<'a>,
1184 {
1185 let offset = self.offsets.get(index as usize)?.1;
1186 let mut r = Reader::new(self.data);
1187 r.jump(offset);
1188 r.skip_white_spaces_and_comments();
1189
1190 r.read_with_context::<T>(&self.ctx)
1191 }
1192}
1193
1194fn parse_metadata(info_dict: &Dict<'_>) -> Metadata {
1195 Metadata {
1196 creation_date: info_dict
1197 .get::<object::String>(CREATION_DATE)
1198 .and_then(|c| DateTime::from_bytes(&c)),
1199 modification_date: info_dict
1200 .get::<object::String>(MOD_DATE)
1201 .and_then(|c| DateTime::from_bytes(&c)),
1202 title: info_dict.get::<object::String>(TITLE).map(|t| t.to_vec()),
1203 author: info_dict.get::<object::String>(AUTHOR).map(|t| t.to_vec()),
1204 subject: info_dict.get::<object::String>(SUBJECT).map(|t| t.to_vec()),
1205 keywords: info_dict
1206 .get::<object::String>(KEYWORDS)
1207 .map(|t| t.to_vec()),
1208 creator: info_dict.get::<object::String>(CREATOR).map(|t| t.to_vec()),
1209 producer: info_dict
1210 .get::<object::String>(PRODUCER)
1211 .map(|t| t.to_vec()),
1212 }
1213}
1214
1215#[cfg(test)]
1216mod qf2b_objectstream_cache_tests {
1217 use crate::pdf::Pdf;
1221 use crate::xref::parse_object_stream_offsets;
1222
1223 fn objstm_fixture() -> Vec<u8> {
1236 fn deflate(data: &[u8]) -> Vec<u8> {
1237 use std::io::Write;
1238 let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
1239 e.write_all(data).expect("in-memory write");
1240 e.finish().expect("in-memory finish")
1241 }
1242
1243 let catalog = b"<< /Type /Catalog /Pages 3 0 R >>";
1244 let pages = b"<< /Type /Pages /Kids [] /Count 0 >>";
1245 let pairs = format!("2 0 3 {}", catalog.len() + 1);
1246 let first = pairs.len() + 1;
1247 let mut payload = pairs.into_bytes();
1248 payload.push(b'\n');
1249 payload.extend_from_slice(catalog);
1250 payload.push(b' ');
1251 payload.extend_from_slice(pages);
1252 let body = deflate(&payload);
1253
1254 let mut out = b"%PDF-1.5\n".to_vec();
1255 let objstm_at = out.len();
1256 out.extend_from_slice(
1257 format!(
1258 "1 0 obj\n<< /Type /ObjStm /N 2 /First {first} /Length {} \
1259 /Filter /FlateDecode >>\nstream\n",
1260 body.len()
1261 )
1262 .as_bytes(),
1263 );
1264 out.extend_from_slice(&body);
1265 out.extend_from_slice(b"\nendstream\nendobj\n");
1266
1267 let xref_at = out.len();
1268 let entry = |t: u8, a: usize, b: u8| {
1269 let mut v = vec![t];
1270 v.extend_from_slice(&(a as u16).to_be_bytes());
1271 v.push(b);
1272 v
1273 };
1274 let mut rows = Vec::new();
1275 rows.extend(entry(0, 0, 255));
1276 rows.extend(entry(1, objstm_at, 0));
1277 rows.extend(entry(2, 1, 0)); rows.extend(entry(2, 1, 1)); rows.extend(entry(1, xref_at, 0));
1280 let xbody = deflate(&rows);
1281 out.extend_from_slice(
1282 format!(
1283 "4 0 obj\n<< /Type /XRef /Size 5 /W [1 2 1] /Root 2 0 R /Length {} \
1284 /Filter /FlateDecode >>\nstream\n",
1285 xbody.len()
1286 )
1287 .as_bytes(),
1288 );
1289 out.extend_from_slice(&xbody);
1290 out.extend_from_slice(b"\nendstream\nendobj\n");
1291 out.extend_from_slice(format!("startxref\n{xref_at}\n%%EOF\n").as_bytes());
1292 out
1293 }
1294
1295 fn load_fixture() -> Pdf {
1299 Pdf::new(objstm_fixture()).expect("the generated /ObjStm fixture must parse")
1300 }
1301
1302 #[test]
1303 fn qf2b_objstm_cache_populates_and_is_stable_on_repeat() {
1304 let pdf = load_fixture();
1305
1306 let xref = pdf.xref();
1307
1308 let after_construction = xref.object_stream_offsets_cache_len();
1313 assert!(
1314 after_construction >= 1,
1315 "fixture is a PDF 1.5+ doc with /ObjStm; at least one offsets table should already be cached after construction; got {after_construction}"
1316 );
1317
1318 let _: Option<crate::object::Dict<'_>> = xref.get(xref.root_id());
1321 assert_eq!(
1322 xref.object_stream_offsets_cache_len(),
1323 after_construction,
1324 "repeated resolution of the same indirect object must reuse the cached offsets table"
1325 );
1326
1327 for raw in 1..=20i32 {
1331 let id = crate::object::ObjectIdentifier::new(raw, 0);
1332 let _: Option<crate::object::Dict<'_>> = xref.get(id);
1333 }
1334 let after_scan = xref.object_stream_offsets_cache_len();
1335
1336 for raw in 1..=20i32 {
1338 let id = crate::object::ObjectIdentifier::new(raw, 0);
1339 let _: Option<crate::object::Dict<'_>> = xref.get(id);
1340 }
1341 assert_eq!(
1342 xref.object_stream_offsets_cache_len(),
1343 after_scan,
1344 "repeated full scans must be cache-stable (no re-parse)"
1345 );
1346 }
1347
1348 #[test]
1349 fn qf2b_two_pdfs_have_independent_caches() {
1350 let pdf_a = load_fixture();
1351 let pdf_b = load_fixture();
1352
1353 let base_a = pdf_a.xref().object_stream_offsets_cache_len();
1356 let base_b = pdf_b.xref().object_stream_offsets_cache_len();
1357 assert_eq!(base_a, base_b);
1358
1359 for raw in 1..=50i32 {
1362 let id = crate::object::ObjectIdentifier::new(raw, 0);
1363 let _: Option<crate::object::Dict<'_>> = pdf_a.xref().get(id);
1364 }
1365 let warm_a = pdf_a.xref().object_stream_offsets_cache_len();
1366
1367 assert_eq!(
1369 pdf_b.xref().object_stream_offsets_cache_len(),
1370 base_b,
1371 "pdf_b cache must be independent of pdf_a's warming (base_a={base_a}, warm_a={warm_a}, base_b={base_b})"
1372 );
1373 }
1374
1375 #[test]
1376 fn qf2b_parse_helper_returns_none_on_truncated_header() {
1377 use crate::object::Stream;
1381 use crate::reader::{Reader, ReaderContext, ReaderExt};
1382 use crate::xref::DUMMY_XREF;
1383
1384 let raw: &[u8] = b"1 0 obj <</N 3 /First 6 /Length 4>>\nstream\n1 0 \nendstream\nendobj\n";
1387 let mut r = Reader::new(raw);
1388 let ctx = ReaderContext::new(&DUMMY_XREF, false);
1389 let stream: Stream<'_> = r
1390 .read_with_context::<crate::object::indirect::IndirectObject<Stream<'_>>>(&ctx)
1391 .expect("synthetic stream should parse")
1392 .get();
1393
1394 let body: &[u8] = b"1 0 ";
1397 assert!(
1398 parse_object_stream_offsets(&stream, body).is_none(),
1399 "truncated headers must not produce a partial offsets table"
1400 );
1401 }
1402
1403 #[test]
1412 #[ignore = "perf measurement; run with `cargo test --release -- --ignored qf2b_bench`"]
1413 fn qf2b_bench_offsets_parse_vs_cached() {
1414 use std::time::Instant;
1415
1416 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1425 .join("..")
1426 .join("..")
1427 .join("corpus")
1428 .join("f3800.pdf");
1429 let Ok(bytes) = std::fs::read(&path) else {
1430 eprintln!(
1431 "[qf2b_bench] fixture {} unavailable; skipping",
1432 path.display()
1433 );
1434 return;
1435 };
1436 let pdf = Pdf::new(bytes).expect("load f3800.pdf");
1437 let xref = pdf.xref();
1438
1439 let max_id = (xref.len() as i32).min(3000);
1442 for n in 1..=max_id {
1443 let id = crate::object::ObjectIdentifier::new(n, 0);
1444 let _: Option<crate::object::Object<'_>> = xref.get(id);
1445 }
1446 let cached_objstms = xref.object_stream_offsets_cache_len();
1447 assert!(
1448 cached_objstms >= 5,
1449 "fixture must trigger several /ObjStms (got {cached_objstms})"
1450 );
1451
1452 let mut objstm_ids: Vec<crate::object::ObjectIdentifier> = Vec::new();
1456 for n in 1..=max_id {
1457 let id = crate::object::ObjectIdentifier::new(n, 0);
1458 if let Some(stream) = xref.get::<crate::object::Stream<'_>>(id)
1460 && stream
1461 .dict()
1462 .get::<crate::object::Name>(crate::object::dict::keys::TYPE)
1463 .as_deref()
1464 == Some(b"ObjStm")
1465 {
1466 objstm_ids.push(id);
1467 }
1468 if objstm_ids.len() >= cached_objstms {
1469 break;
1470 }
1471 }
1472 let containers = objstm_ids.len();
1473 assert!(containers > 0);
1474
1475 const REPEATS: u32 = 200;
1478 let mut sink_parse = 0usize;
1479 let t_parse = Instant::now();
1480 for _ in 0..REPEATS {
1481 for id in &objstm_ids {
1482 let stream = xref
1483 .get::<crate::object::Stream<'_>>(*id)
1484 .expect("stream resolves");
1485 let Ok(decoded) = stream.decoded() else {
1486 continue;
1487 };
1488 if let Some(offs) = parse_object_stream_offsets(&stream, &decoded) {
1489 sink_parse = sink_parse.wrapping_add(offs.len());
1490 }
1491 }
1492 }
1493 let parse_elapsed = t_parse.elapsed();
1494
1495 let inner = match &xref.0 {
1498 crate::xref::Inner::Some(r) => r.clone(),
1499 _ => unreachable!(),
1500 };
1501 let mut sink_cache = 0usize;
1502 let t_cache = Instant::now();
1503 for _ in 0..REPEATS {
1504 for id in &objstm_ids {
1505 let offs = inner
1506 .data
1507 .get_object_stream_offsets_or_init(*id, || {
1508 let stream = xref
1509 .get::<crate::object::Stream<'_>>(*id)
1510 .expect("stream resolves");
1511 let decoded = stream.decoded().ok()?;
1512 parse_object_stream_offsets(&stream, &decoded)
1513 })
1514 .expect("cached entry must exist after warm-up");
1515 sink_cache = sink_cache.wrapping_add(offs.len());
1516 }
1517 }
1518 let cache_elapsed = t_cache.elapsed();
1519
1520 assert_eq!(
1521 sink_parse, sink_cache,
1522 "parsed and cached results must agree on offset-count totals"
1523 );
1524
1525 let speedup = parse_elapsed.as_secs_f64() / cache_elapsed.as_secs_f64().max(1e-9);
1526 let reduction = (1.0 - cache_elapsed.as_secs_f64() / parse_elapsed.as_secs_f64()) * 100.0;
1527
1528 eprintln!("[qf2b_bench] fixture: f3800.pdf");
1529 eprintln!("[qf2b_bench] /ObjStm containers measured: {containers}");
1530 eprintln!("[qf2b_bench] iterations per container: {REPEATS}");
1531 eprintln!("[qf2b_bench] direct re-parse total: {parse_elapsed:?}");
1532 eprintln!("[qf2b_bench] cached lookup total: {cache_elapsed:?}");
1533 eprintln!("[qf2b_bench] speedup: {speedup:.1}x");
1534 eprintln!("[qf2b_bench] parse-time reduction: {reduction:.1}%");
1535
1536 assert!(
1540 reduction >= 10.0,
1541 "QF2-B acceptance: ≥ 10 % parse-time reduction required; got {reduction:.2} %"
1542 );
1543 }
1544}
1545
1546#[cfg(test)]
1552mod upstream_hardening_tests {
1553 use super::*;
1554 use crate::object::dict::keys::V;
1555 use crate::pdf::Pdf;
1556
1557 #[test]
1558 fn xref_table_trailer_rejects_overflowing_entry_skip() {
1559 let data = b"xref\n0 999999999999999999999\ntrailer\n<<>>";
1560 let mut reader = Reader::new(data);
1561 assert!(read_xref_table_trailer(&mut reader, &ReaderContext::dummy()).is_none());
1562 }
1563
1564 #[test]
1565 fn xref_table_trailer_still_reads_a_sane_header() {
1566 let data = b"xref\n0 1\n0000000000 65535 f \ntrailer\n<< /Size 1 >>";
1567 let mut reader = Reader::new(data);
1568 assert!(read_xref_table_trailer(&mut reader, &ReaderContext::dummy()).is_some());
1569 }
1570
1571 fn objstm_cycle_fixture() -> Vec<u8> {
1577 use alloc::format;
1578
1579 let obj2 = b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>";
1580 let obj3 = b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] \
1581 /Resources << /ColorSpace << /CS0 5 0 R >> >> /Contents 4 0 R >>";
1582 let obj5 = b"[/Indexed 5 0 R 0 <00>]";
1583 let off3 = obj2.len() + 1;
1585 let off5 = off3 + obj3.len() + 1;
1586 let header = format!("2 0 3 {off3} 5 {off5}\n");
1587 let mut payload = header.into_bytes();
1588 payload.extend_from_slice(obj2);
1589 payload.push(b'\n');
1590 payload.extend_from_slice(obj3);
1591 payload.push(b'\n');
1592 payload.extend_from_slice(obj5);
1593 payload.push(b'\n');
1594
1595 let content = b"0 0 10 10 re f";
1596 let catalog = b"<< /Type /Catalog /Pages 2 0 R >>";
1597
1598 let mut out = b"%PDF-1.7\n".to_vec();
1599 let off1 = out.len();
1600 out.extend_from_slice(b"1 0 obj\n");
1601 out.extend_from_slice(catalog);
1602 out.extend_from_slice(b"\nendobj\n");
1603 let off4 = out.len();
1604 out.extend_from_slice(
1605 format!("4 0 obj\n<< /Length {} >>\nstream\n", content.len()).as_bytes(),
1606 );
1607 out.extend_from_slice(content);
1608 out.extend_from_slice(b"\nendstream\nendobj\n");
1609 let off6 = out.len();
1610 out.extend_from_slice(
1611 format!(
1612 "6 0 obj\n<< /Type /ObjStm /N 3 /First {} /Length {} >>\nstream\n",
1613 payload.len() - (obj2.len() + 1 + obj3.len() + 1 + obj5.len() + 1),
1614 payload.len()
1615 )
1616 .as_bytes(),
1617 );
1618 out.extend_from_slice(&payload);
1619 out.extend_from_slice(b"\nendstream\nendobj\n");
1620
1621 let off7 = out.len();
1622 let entry = |t: u8, a: usize, b: u8| {
1623 let mut v = vec![t];
1624 v.extend_from_slice(&(a as u16).to_be_bytes());
1625 v.push(b);
1626 v
1627 };
1628 let mut rows = Vec::new();
1629 rows.extend(entry(0, 0, 255));
1630 rows.extend(entry(1, off1, 0));
1631 rows.extend(entry(2, 6, 0));
1632 rows.extend(entry(2, 6, 1));
1633 rows.extend(entry(1, off4, 0));
1634 rows.extend(entry(2, 6, 2));
1635 rows.extend(entry(1, off6, 0));
1636 rows.extend(entry(1, off7, 0));
1637 out.extend_from_slice(
1638 format!(
1639 "7 0 obj\n<< /Type /XRef /Size 8 /W [1 2 1] /Root 1 0 R /Length {} >>\nstream\n",
1640 rows.len()
1641 )
1642 .as_bytes(),
1643 );
1644 out.extend_from_slice(&rows);
1645 out.extend_from_slice(b"\nendstream\nendobj\n");
1646 out.extend_from_slice(format!("startxref\n{off7}\n%%EOF\n").as_bytes());
1647 out
1648 }
1649
1650 #[test]
1658 fn objstm_cycle_through_second_stream_resolves_to_none() {
1659 let pdf = Pdf::new(objstm_cycle_fixture()).expect("the cyclic fixture must parse");
1660 let xref = pdf.xref();
1661 let array: Array<'_> = xref
1662 .get(ObjectIdentifier::new(5, 0))
1663 .expect("the cyclic array itself must resolve");
1664 let mut items = array.flex_iter();
1665 assert!(items.next::<Name>().is_some(), "element 0 is /Indexed");
1666 assert!(
1667 items.next::<Array<'_>>().is_none(),
1668 "the self-referential base space (5 0 R) must give up, not resolve again"
1669 );
1670 }
1671
1672 fn broken_xref_fixture() -> Vec<u8> {
1678 use alloc::format;
1679
1680 let mut out = b"%PDF-1.7\n".to_vec();
1681 let mut offsets = Vec::new();
1682 let bodies: Vec<Vec<u8>> = (1..=10)
1684 .map(|n| {
1685 if n == 1 {
1686 b"<< /Type /Catalog /Pages 2 0 R >>".to_vec()
1687 } else if n == 2 {
1688 b"<< /Type /Pages /Kids [] /Count 0 >>".to_vec()
1689 } else {
1690 format!("<< /V {} >>", 100 + n).into_bytes()
1691 }
1692 })
1693 .collect();
1694 for (i, body) in bodies.iter().enumerate() {
1695 offsets.push(out.len());
1696 out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
1697 out.extend_from_slice(body);
1698 out.extend_from_slice(b"\nendobj\n");
1699 }
1700
1701 let xref_at = out.len();
1702 out.extend_from_slice(b"xref\n0 11\n0000000000 65535 f \n");
1703 for (i, offset) in offsets.iter().enumerate() {
1704 let recorded = if i < 2 { *offset } else { 1 };
1706 out.extend_from_slice(format!("{recorded:010} 00000 n \n").as_bytes());
1707 }
1708 out.extend_from_slice(
1709 format!("trailer\n<< /Size 11 /Root 1 0 R >>\nstartxref\n{xref_at}\n%%EOF\n")
1710 .as_bytes(),
1711 );
1712 out
1713 }
1714
1715 #[test]
1719 fn xref_repair_is_idempotent() {
1720 let pdf = Pdf::new(objstm_cycle_fixture()).expect("the fixture must parse");
1721 pdf.xref().repair();
1722 pdf.xref().repair();
1723 }
1724
1725 #[test]
1731 fn concurrent_xref_repair_resolves_after_repair() {
1732 use std::sync::Barrier;
1733
1734 for _ in 0..16 {
1735 let pdf = Pdf::new(broken_xref_fixture()).expect("the broken fixture must load");
1736 let barrier = Barrier::new(8);
1737
1738 std::thread::scope(|scope| {
1739 for thread in 0..8 {
1740 let pdf = &pdf;
1741 let barrier = &barrier;
1742 scope.spawn(move || {
1743 barrier.wait();
1744
1745 for offset in 0..8 {
1747 let number = 3 + (thread + offset) % 8;
1748 let value = pdf
1749 .xref()
1750 .get::<Dict<'_>>(ObjectIdentifier::new(number, 0))
1751 .and_then(|dict| dict.get::<i32>(V));
1752 assert_eq!(value, Some(100 + number));
1753 }
1754 });
1755 }
1756 });
1757 }
1758 }
1759
1760 fn concurrent_objstm_fixture() -> Vec<u8> {
1765 use alloc::format;
1766 use std::io::Write;
1767
1768 fn deflate(data: &[u8]) -> Vec<u8> {
1769 let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
1770 e.write_all(data).expect("in-memory write");
1771 e.finish().expect("in-memory finish")
1772 }
1773
1774 fn pack(first_id: i32, count: usize) -> (Vec<u8>, usize) {
1777 use alloc::format;
1778 use alloc::string::String;
1779 use alloc::vec::Vec;
1780 use core::fmt::Write;
1781
1782 let bodies: Vec<Vec<u8>> = (0..count)
1783 .map(|i| format!("<< /V {} >>", 100 + first_id + i as i32).into_bytes())
1784 .collect();
1785 let mut header = String::new();
1786 let mut at = 0;
1787 for (i, body) in bodies.iter().enumerate() {
1788 write!(header, "{} {at} ", first_id + i as i32).expect("in-memory write");
1789 at += body.len() + 1;
1790 }
1791 let first = header.len();
1792 let mut payload = header.into_bytes();
1793 for body in &bodies {
1794 payload.extend_from_slice(body);
1795 payload.push(b'\n');
1796 }
1797 (payload, first)
1798 }
1799
1800 let catalog = b"<< /Type /Catalog /Pages 2 0 R >>";
1801 let pages = b"<< /Type /Pages /Kids [] /Count 0 >>";
1802 let (payload_a, first_a) = pack(6, 12);
1803 let (payload_b, first_b) = pack(18, 12);
1804
1805 let mut out = b"%PDF-1.5\n".to_vec();
1806 let off1 = out.len();
1807 out.extend_from_slice(b"1 0 obj\n");
1808 out.extend_from_slice(catalog);
1809 out.extend_from_slice(b"\nendobj\n");
1810 let off2 = out.len();
1811 out.extend_from_slice(b"2 0 obj\n");
1812 out.extend_from_slice(pages);
1813 out.extend_from_slice(b"\nendobj\n");
1814 let n_a = deflate(&payload_a);
1815 let off3 = out.len();
1816 out.extend_from_slice(
1817 format!(
1818 "3 0 obj\n<< /Type /ObjStm /N 12 /First {first_a} /Length {} /Filter /FlateDecode >>\nstream\n",
1819 n_a.len()
1820 )
1821 .as_bytes(),
1822 );
1823 out.extend_from_slice(&n_a);
1824 out.extend_from_slice(b"\nendstream\nendobj\n");
1825 let n_b = deflate(&payload_b);
1826 let off4 = out.len();
1827 out.extend_from_slice(
1828 format!(
1829 "4 0 obj\n<< /Type /ObjStm /N 12 /First {first_b} /Length {} /Filter /FlateDecode >>\nstream\n",
1830 n_b.len()
1831 )
1832 .as_bytes(),
1833 );
1834 out.extend_from_slice(&n_b);
1835 out.extend_from_slice(b"\nendstream\nendobj\n");
1836
1837 let off30 = out.len();
1838 let entry = |t: u8, a: usize, b: u8| {
1839 let mut v = vec![t];
1840 v.extend_from_slice(&(a as u16).to_be_bytes());
1841 v.push(b);
1842 v
1843 };
1844 let mut rows = Vec::new();
1845 for n in 0..31 {
1847 match n {
1848 0 | 5 => rows.extend(entry(0, 0, 255)),
1849 1 => rows.extend(entry(1, off1, 0)),
1850 2 => rows.extend(entry(1, off2, 0)),
1851 3 => rows.extend(entry(1, off3, 0)),
1852 4 => rows.extend(entry(1, off4, 0)),
1853 6..=17 => rows.extend(entry(2, 3, (n - 6) as u8)),
1854 18..=29 => rows.extend(entry(2, 4, (n - 18) as u8)),
1855 30 => rows.extend(entry(1, off30, 0)),
1856 _ => unreachable!(),
1857 }
1858 }
1859 out.extend_from_slice(
1860 format!(
1861 "30 0 obj\n<< /Type /XRef /Size 31 /W [1 2 1] /Root 1 0 R /Length {} >>\nstream\n",
1862 rows.len()
1863 )
1864 .as_bytes(),
1865 );
1866 out.extend_from_slice(&rows);
1867 out.extend_from_slice(b"\nendstream\nendobj\n");
1868 out.extend_from_slice(format!("startxref\n{off30}\n%%EOF\n").as_bytes());
1869 out
1870 }
1871
1872 #[test]
1877 fn concurrent_objstm_resolution_publishes_atomically() {
1878 use std::sync::Barrier;
1879
1880 for _ in 0..16 {
1881 let pdf = Pdf::new(concurrent_objstm_fixture()).expect("the fixture must load");
1882 let barrier = Barrier::new(8);
1883
1884 std::thread::scope(|scope| {
1885 for thread in 0..8 {
1886 let pdf = &pdf;
1887 let barrier = &barrier;
1888 scope.spawn(move || {
1889 barrier.wait();
1890
1891 for offset in 0..24 {
1893 let number = 6 + (thread * 12 + offset) % 24;
1894 let value = pdf
1895 .xref()
1896 .get::<Dict<'_>>(ObjectIdentifier::new(number, 0))
1897 .and_then(|dict| dict.get::<i32>(V));
1898 assert_eq!(value, Some(100 + number));
1899 }
1900 });
1901 }
1902 });
1903 }
1904 }
1905}