Skip to main content

pdf_syntax/
xref.rs

1//! Reading and querying the xref table of a PDF file.
2
3use 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
37/// Parse the "root" xref from the PDF.
38pub(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
58/// Try to manually parse the PDF to build an xref table and trailer dictionary.
59pub(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            // Check that the object following it is actually valid before inserting it.
100            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 we already found an entry for that object number that was not
127                    // inside an object stream. Somewhat arbitrary and maybe
128                    // we can do better, but that seems to work for the current
129                    // set of tests.
130                    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    // Try to choose the right trailer dict by doing basic validation.
151    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        // The problem is that in this case, we have used a dummy reader context which does not have
204        // a decryptor. Therefore, we were unable to decrypt any of the object streams and missed
205        // all objects that are inside of such a stream. Therefore, we need to redo the process
206        // using a `ReaderContext` that does have the ability to decrypt.
207        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/// An xref table.
238#[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        // This is a bit hacky, but the problem is we can't read the resolved trailer dictionary
251        // before we actually created the xref struct. So we first create it using dummy data
252        // and then populate the data.
253        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        // We read the trailer twice, once to determine the encryption used and then a second
267        // time to resolve the catalog dictionary, etc. This allows us to support catalog dictionaries
268        // that are stored in an encrypted object stream.
269
270        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    /// Number of cached parsed object-stream offset tables. QF2-B test
377    /// hook; not part of the public API.
378    #[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    /// Return the object ID of the root dictionary.
394    pub fn root_id(&self) -> ObjectIdentifier {
395        self.trailer_data().root_ref
396    }
397
398    /// Whether the PDF has optional content groups.
399    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                // Try to yield in the order the objects appeared in the
433                // PDF.
434                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                            // Skip invalid objects.
444                            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    /// Return the object with the given identifier.
497    #[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    /// Return the object with the given identifier.
507    #[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        // The flag must describe the map this entry came from.
521        let (entry, was_repaired) = {
522            let locked = repr.map.get();
523            // An indirect reference to an undefined object shall not be considered an error by a PDF processor; it
524            // shall be treated as a reference to the null object.
525            (*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                    // There is a valid object at the offset, it's just not of the type the caller
545                    // expected, which is fine.
546                    if r.skip_not_in_content_stream::<IndirectObject<Object<'_>>>()
547                        .is_some()
548                    {
549                        return None;
550                    }
551                };
552
553                // The xref table is broken, try to repair if not already repaired.
554                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                    // Now try reading again.
566                    self.get_with::<T>(id, &ctx)
567                }
568            }
569            EntryType::ObjStream(obj_stram_gen_num, index) => {
570                // Generation number is implicitly 0.
571                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                // QF2-B: re-use a cached `(obj_num, offset)` index table if
584                // we've already parsed this `/ObjStm` once for this
585                // document. The cache lives on the per-document `Data` and
586                // is dropped together with it.
587                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/// An input that is passed to the xref constructor so that we can fully resolve
600/// the PDF.
601#[derive(Debug, Copy, Clone)]
602pub(crate) enum XRefInput<'a> {
603    /// This option is going to be uesd in 99.999% of the case. It contains the
604    /// raw data of the trailer dictionary which is then going to be processed.
605    TrailerDictData(&'a [u8]),
606    /// In case the trailer dictionary could not be read (for example because
607    /// it is cut-off), we just pass the object ID of the root dictionary
608    /// in case we have found one, and try our best to build the PDF just
609    /// with the information we have there.
610    ///
611    /// Note that this won't work if the document is encrypted, as we
612    /// can't access the crypto dictionary.
613    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/// A type of xref entry.
638#[derive(Debug, PartialEq, Eq, Clone, Copy)]
639enum EntryType {
640    /// An indirect object that is at a specific offset in the original data.
641    Normal(usize),
642    /// An indirect object that is part of an object stream. First number indicates the object
643    /// number of the _object stream_ (the generation number is always 0), the second number indicates
644    /// the index in the object stream.
645    ObjStream(u32, u32),
646}
647
648type XrefMap = FxHashMap<ObjectIdentifier, EntryType>;
649
650/// Representation of a proper xref table.
651#[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    /// A dummy xref table that doesn't have any entries.
689    Dummy,
690    /// A proper xref table.
691    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
732/// Maximum depth for following xref Prev/XRefStm chains to prevent stack
733/// overflow on circular or deeply chained xref tables.
734const 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    // In case the position points to before the object number of a xref stream.
753    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
784/// Populate the xref table, and return the trailer dict.
785fn 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        // First insert the entries from any previous xref tables.
804        populate_xref_depth(data, prev as usize, insert_map, depth + 1)?;
805    }
806
807    // In hybrid files, entries in `XRefStm` should have higher priority, therefore we insert them
808    // after looking at `PREV`.
809    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        // Both come from the file. Ported from hayro upstream
818        // (LaurenzV/hayro#1194).
819        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            // Specification says we should ignore any object number > SIZE, but probably
827            // not important?
828            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        // First insert the entries from any previous xref tables.
852        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    // Do such files exist?
866    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            // We assume a length of 1.
940            xref_reader.read_bytes(1)?[0]
941        };
942
943        let obj_number = start + i;
944
945        match f_type {
946            // We don't care about free objects.
947            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            // Assume an empty ID entry.
1028            vec![]
1029        };
1030
1031        get(&encryption_dict, &id, password).map_err(XRefError::Encryption)
1032    } else {
1033        Ok(Decryptor::None)
1034    }
1035}
1036
1037/// Parse the `(obj_num, absolute_byte_offset)` index table that lives at the
1038/// start of a compressed `/ObjStm`.
1039///
1040/// Returns `None` if the stream dict is missing `/N` / `/First`, or if the
1041/// header is truncated. The returned table is the same value that an
1042/// `ObjectStream` would have populated internally before this was split out
1043/// (QF2-B). Splitting it allows the result to be cached per-document; see
1044/// [`crate::data::Data::get_object_stream_offsets_or_init`].
1045fn 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        // Skip object number
1058        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
1067/// Holds a borrowed view onto the decoded bytes of an `/ObjStm` plus a
1068/// (possibly cached) parsed offset table.
1069///
1070/// QF2-B: `offsets` is now an `Arc<...>` so the same allocation can be
1071/// returned from the per-document cache on subsequent lookups, eliminating
1072/// the linear re-parse hot loop reported in
1073/// `QF1_A_FLAMEGRAPH_REPORT.md` (449× per main thread on the
1074/// `scan_for_xfa` fallback path).
1075struct ObjectStream<'a> {
1076    data: &'a [u8],
1077    ctx: ReaderContext<'a>,
1078    offsets: Arc<crate::data::ObjectStreamOffsets>,
1079}
1080
1081impl<'a> ObjectStream<'a> {
1082    /// Build a fresh `ObjectStream` by parsing the index table inline (no
1083    /// caching). Used by the xref-repair / trailer-fallback paths that
1084    /// don't have access to a `Data` cache.
1085    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    /// Build an `ObjectStream` that reuses an already-parsed offsets table
1095    /// (typically retrieved from the per-document cache). Cheap — does no
1096    /// header scan.
1097    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    //! QF2-B — end-to-end coverage for the per-document ObjectStream
1145    //! offsets cache, using a real `/ObjStm`-containing fixture.
1146
1147    use crate::pdf::Pdf;
1148    use crate::xref::parse_object_stream_offsets;
1149
1150    /// A PDF 1.5 document carrying an `/ObjStm`, built here rather than read
1151    /// from the corpus.
1152    ///
1153    /// This used to name a corpus file and `return` when it was missing — so in
1154    /// a tree without the goldens the test passed while asserting nothing, and
1155    /// the corpus name travelled into published source. Building the fixture
1156    /// fixes both: its provenance is this function, and it exists everywhere
1157    /// the test runs.
1158    ///
1159    /// Two objects (catalog and pages) packed into an object stream, addressed
1160    /// by an xref stream — the smallest shape that makes the offsets cache do
1161    /// its work.
1162    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)); // object 2 lives in stream 1, index 0
1205        rows.extend(entry(2, 1, 1)); // object 3 lives in stream 1, index 1
1206        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    /// The fixture is built, so failing to parse it is a fault in this crate,
1223    /// not a missing file. It used to be an `Option` that the tests turned into
1224    /// an early `return`, which is how they passed while asserting nothing.
1225    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        // `Pdf::new` resolves the trailer and catalog during construction;
1236        // for PDF 1.5+ files those typically live in an `/ObjStm` so the
1237        // cache is already non-empty at this point. That is itself
1238        // evidence that the cache is active.
1239        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        // Resolve the catalog explicitly — must not grow the cache because
1246        // the /ObjStm carrying the catalog is already a hit.
1247        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        // Resolve a number of additional indirect objects. Each new
1255        // `/ObjStm` we touch may add one entry, but re-touching anything
1256        // already seen must not.
1257        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        // Idempotency: a second sweep must not grow the cache further.
1264        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        // Sanity: both start with the same construction-time count for
1281        // the same fixture (same shape).
1282        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        // Touch many ids in pdf_a to (likely) populate additional /ObjStm
1287        // cache entries.
1288        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        // pdf_b must NOT have grown — caches are per-document.
1295        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        // Synthetic: dict says N=3 but data has only one (num, offset)
1305        // pair. The helper must return None, and the caller must not
1306        // cache a `None`.
1307        use crate::object::Stream;
1308        use crate::reader::{Reader, ReaderContext, ReaderExt};
1309        use crate::xref::DUMMY_XREF;
1310
1311        // Build a minimal indirect stream object with /N 3 /First 6:
1312        // "1 0 obj <</N 3/First 6/Length 4>>stream\n1 0\nendstream\nendobj\n"
1313        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        // The stream body has only "1 0 " — three entries cannot be
1322        // recovered, so the helper must return `None`.
1323        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    /// QF2-B perf harness. Not a correctness test — it prints microbench
1331    /// numbers and only runs when explicitly requested with
1332    /// `--ignored qf2b_bench`. The harness compares **direct re-parse**
1333    /// of /ObjStm offsets tables (what the pre-QF2-B `ObjectStream::new`
1334    /// did on every `xref.get` of an /ObjStm-stored object) versus the
1335    /// QF2-B cached lookup. This isolates the parse cost from the
1336    /// downstream object-decoding cost, which dominates `xref.get` and
1337    /// would otherwise hide the cache win.
1338    #[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        // 161 /ObjStm headers, 575 KB. Walking and decoding 30+ ObjStms
1344        // here is enough to measure the parse delta cleanly.
1345        // Built from the manifest directory rather than written out as one
1346        // string. This file ships in the published crate and the repository
1347        // tree it points at does not, so a literal path into it is a leaked
1348        // internal path -- which is what the prepublish audit refuses, and it
1349        // stopped the 1.0.0 publish at crate 8 of 32 (#358). It also makes the
1350        // benchmark independent of the working directory it is started from.
1351        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        // Warm cache via a single full sweep so we know which /ObjStms
1367        // exist.
1368        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        // Collect the object-stream container ids by looking them up in
1380        // the xref entries. We then iterate the cache to compare timing
1381        // for parse-from-scratch vs cache-hit.
1382        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            // Only ObjStm container ids resolve as Stream + /Type ObjStm.
1386            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        // Direct re-parse loop — mirrors pre-QF2-B behaviour: parse the
1403        // offsets table from scratch every time, no cache.
1404        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        // Cache-hit loop — mirrors QF2-B behaviour: retrieve the same
1423        // parsed table from the per-document cache.
1424        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        // Acceptance gate: QF2-B target is ≥ 10 % reduction on parse path.
1464        // The microbench should show much more than that, since the cache
1465        // hit is O(1) hashmap fetch + Arc clone vs O(N) memchr+nom parse.
1466        assert!(
1467            reduction >= 10.0,
1468            "QF2-B acceptance: ≥ 10 % parse-time reduction required; got {reduction:.2} %"
1469        );
1470    }
1471}
1472
1473/// Regression tests for fixes ported from hayro upstream (LaurenzV/hayro#1194).
1474///
1475/// Both subsection-header fields come from the file. `start + num_entries` and
1476/// `XREF_ENTRY_LEN * num_entries` overflowed on a header that declares more
1477/// entries than the address space holds.
1478#[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    /// A document whose `/ObjStm` holds an `/Indexed` colour space that names
1499    /// itself as its base (`5 0 R` inside object 5), built here rather than
1500    /// read from the corpus. Mirrors upstream's `object_stream_cycle.pdf`
1501    /// (LaurenzV/hayro#1389) with an uncompressed xref stream, so no
1502    /// test-only deflater is needed.
1503    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        // Offsets count from the first object body, past the `/First` header.
1511        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    /// Ported from LaurenzV/hayro ced00dd0 (#1389): a cycle that runs through
1578    /// a second object stream must resolve to `None`, not recurse. The old
1579    /// code only caught an object stream containing itself
1580    /// (`obj_stream_id == id`); here object 5 lives in stream 6 and refers
1581    /// back to 5, so the base colour space resolved again on every visit.
1582    /// Without the fix this fails on the `is_none` (the self-reference
1583    /// keeps resolving); through a renderer it overflows the stack.
1584    #[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    /// A document whose xref table points objects 3..=10 at byte 1 (inside
1600    /// the `%PDF` header, so no object parses there), while the catalog and
1601    /// page tree stay valid. Mirrors upstream's
1602    /// `concurrent_xref_repair.pdf`: resolving any of 3..=10 must trigger a
1603    /// repair and then read the rebuilt entry.
1604    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        // Object 1: catalog; object 2: page tree. Both keep valid offsets.
1610        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            // Objects 3..=10 carry a bogus offset; 1 and 2 stay valid.
1632            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    /// Ported from LaurenzV/hayro d6fa1180: repairing twice must be a no-op,
1643    /// not a panic. The old code asserted the table was not already
1644    /// repaired, so any second repair — concurrent or sequential — aborted.
1645    #[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    /// Ported from LaurenzV/hayro d6fa1180 + 41d11844: threads that read
1653    /// stale entries while another thread repairs must block and retry on
1654    /// the rebuilt table, not panic on the lock or give up with `None`.
1655    /// Without the fix this panics (`try_put` contention / `assert!`) or
1656    /// resolves `None` for objects the repaired table holds.
1657    #[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                        // Objects 3..=10 have incorrect xref offsets.
1673                        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    /// Two compressed object streams holding `<< /V .. >>` dicts 6..=29,
1688    /// addressed by an xref stream — the smallest shape that races the
1689    /// decoded-bytes publish in `Data::get_with`. Mirrors upstream's
1690    /// `concurrent_object_streams.pdf`.
1691    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        /// Pack `count` dicts into one object-stream payload; returns the
1702        /// payload and the `/First` header length.
1703        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        // Size 31: objects 0..=30. 5 and 30's predecessors stay free.
1773        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    /// Ported from LaurenzV/hayro 0829d581: the decoded bytes of an object
1800    /// stream must be published atomically. The old code inserted the cache
1801    /// index before decoding, so a thread resolving the same stream
1802    /// concurrently read an empty slot and resolved `None`.
1803    #[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                        // Objects 6..=29 are split between two compressed streams.
1819                        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}