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, 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
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            decoded_streams: DecodedStreams::default(),
265        })));
266
267        // We read the trailer twice, once to determine the encryption used and then a second
268        // time to resolve the catalog dictionary, etc. This allows us to support catalog dictionaries
269        // that are stored in an encrypted object stream.
270
271        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    /// Number of cached parsed object-stream offset tables. QF2-B test
378    /// hook; not part of the public API.
379    #[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    /// Return the object ID of the root dictionary.
395    pub fn root_id(&self) -> ObjectIdentifier {
396        self.trailer_data().root_ref
397    }
398
399    /// Whether the PDF has optional content groups.
400    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                // Try to yield in the order the objects appeared in the
434                // PDF.
435                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                            // Skip invalid objects.
445                            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    /// The decoded form of the stream `id`, decoded at most once per document
486    /// load. `decode` runs only on a miss, outside the lock; a failure is
487    /// returned and not remembered.
488    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    /// Counters of the decoded-stream cache of this document load.
517    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    /// Return the object with the given identifier.
537    #[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    /// Return the object with the given identifier.
547    #[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        // The flag must describe the map this entry came from.
561        let (entry, was_repaired) = {
562            let locked = repr.map.get();
563            // An indirect reference to an undefined object shall not be considered an error by a PDF processor; it
564            // shall be treated as a reference to the null object.
565            (*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                    // There is a valid object at the offset, it's just not of the type the caller
585                    // expected, which is fine.
586                    if r.skip_not_in_content_stream::<IndirectObject<Object<'_>>>()
587                        .is_some()
588                    {
589                        return None;
590                    }
591                };
592
593                // The xref table is broken, try to repair if not already repaired.
594                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                    // Now try reading again.
606                    self.get_with::<T>(id, &ctx)
607                }
608            }
609            EntryType::ObjStream(obj_stram_gen_num, index) => {
610                // Generation number is implicitly 0.
611                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                // QF2-B: re-use a cached `(obj_num, offset)` index table if
624                // we've already parsed this `/ObjStm` once for this
625                // document. The cache lives on the per-document `Data` and
626                // is dropped together with it.
627                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/// An input that is passed to the xref constructor so that we can fully resolve
640/// the PDF.
641#[derive(Debug, Copy, Clone)]
642pub(crate) enum XRefInput<'a> {
643    /// This option is going to be uesd in 99.999% of the case. It contains the
644    /// raw data of the trailer dictionary which is then going to be processed.
645    TrailerDictData(&'a [u8]),
646    /// In case the trailer dictionary could not be read (for example because
647    /// it is cut-off), we just pass the object ID of the root dictionary
648    /// in case we have found one, and try our best to build the PDF just
649    /// with the information we have there.
650    ///
651    /// Note that this won't work if the document is encrypted, as we
652    /// can't access the crypto dictionary.
653    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/// A type of xref entry.
678#[derive(Debug, PartialEq, Eq, Clone, Copy)]
679enum EntryType {
680    /// An indirect object that is at a specific offset in the original data.
681    Normal(usize),
682    /// An indirect object that is part of an object stream. First number indicates the object
683    /// number of the _object stream_ (the generation number is always 0), the second number indicates
684    /// the index in the object stream.
685    ObjStream(u32, u32),
686}
687
688type XrefMap = FxHashMap<ObjectIdentifier, EntryType>;
689
690/// Representation of a proper xref table.
691#[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 shared by everything reading this document load.
725    decoded_streams: DecodedStreams,
726}
727
728/// Upper bound on the bytes of decoded streams kept per document load. A
729/// stream that would exceed it is decoded and returned but not kept.
730const DECODED_STREAMS_BYTE_BUDGET: usize = 64 * 1024 * 1024;
731
732/// Counters of the per-document decoded-stream cache.
733#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
734pub struct SharedDecodeStats {
735    /// Lookups answered without decoding.
736    pub hits: usize,
737    /// Streams actually decoded on a lookup.
738    pub decodes: usize,
739    /// Streams currently kept.
740    pub entries: usize,
741    /// Total size in bytes of the streams currently kept.
742    pub bytes: usize,
743}
744
745#[derive(Debug, Default)]
746struct DecodedStreamsInner {
747    map: FxHashMap<ObjectIdentifier, Arc<Vec<u8>>>,
748    stats: SharedDecodeStats,
749}
750
751/// Cache of decoded streams keyed by object number and generation.
752///
753/// It lives as long as the document load: the xref is the document's, so
754/// nothing outlives it and nothing needs invalidating. Cloning the xref shares
755/// the cache, which is right because the clone reads the same bytes.
756#[derive(Debug, Default, Clone)]
757struct DecodedStreams(Arc<Mutex<DecodedStreamsInner>>);
758
759#[derive(Debug, Clone)]
760enum Inner {
761    /// A dummy xref table that doesn't have any entries.
762    Dummy,
763    /// A proper xref table.
764    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
805/// Maximum depth for following xref Prev/XRefStm chains to prevent stack
806/// overflow on circular or deeply chained xref tables.
807const 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    // In case the position points to before the object number of a xref stream.
826    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
857/// Populate the xref table, and return the trailer dict.
858fn 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        // First insert the entries from any previous xref tables.
877        populate_xref_depth(data, prev as usize, insert_map, depth + 1)?;
878    }
879
880    // In hybrid files, entries in `XRefStm` should have higher priority, therefore we insert them
881    // after looking at `PREV`.
882    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        // Both come from the file. Ported from hayro upstream
891        // (LaurenzV/hayro#1194).
892        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            // Specification says we should ignore any object number > SIZE, but probably
900            // not important?
901            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        // First insert the entries from any previous xref tables.
925        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    // Do such files exist?
939    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            // We assume a length of 1.
1013            xref_reader.read_bytes(1)?[0]
1014        };
1015
1016        let obj_number = start + i;
1017
1018        match f_type {
1019            // We don't care about free objects.
1020            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            // Assume an empty ID entry.
1101            vec![]
1102        };
1103
1104        get(&encryption_dict, &id, password).map_err(XRefError::Encryption)
1105    } else {
1106        Ok(Decryptor::None)
1107    }
1108}
1109
1110/// Parse the `(obj_num, absolute_byte_offset)` index table that lives at the
1111/// start of a compressed `/ObjStm`.
1112///
1113/// Returns `None` if the stream dict is missing `/N` / `/First`, or if the
1114/// header is truncated. The returned table is the same value that an
1115/// `ObjectStream` would have populated internally before this was split out
1116/// (QF2-B). Splitting it allows the result to be cached per-document; see
1117/// [`crate::data::Data::get_object_stream_offsets_or_init`].
1118fn 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        // Skip object number
1131        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
1140/// Holds a borrowed view onto the decoded bytes of an `/ObjStm` plus a
1141/// (possibly cached) parsed offset table.
1142///
1143/// QF2-B: `offsets` is now an `Arc<...>` so the same allocation can be
1144/// returned from the per-document cache on subsequent lookups, eliminating
1145/// the linear re-parse hot loop reported in
1146/// `QF1_A_FLAMEGRAPH_REPORT.md` (449× per main thread on the
1147/// `scan_for_xfa` fallback path).
1148struct ObjectStream<'a> {
1149    data: &'a [u8],
1150    ctx: ReaderContext<'a>,
1151    offsets: Arc<crate::data::ObjectStreamOffsets>,
1152}
1153
1154impl<'a> ObjectStream<'a> {
1155    /// Build a fresh `ObjectStream` by parsing the index table inline (no
1156    /// caching). Used by the xref-repair / trailer-fallback paths that
1157    /// don't have access to a `Data` cache.
1158    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    /// Build an `ObjectStream` that reuses an already-parsed offsets table
1168    /// (typically retrieved from the per-document cache). Cheap — does no
1169    /// header scan.
1170    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    //! QF2-B — end-to-end coverage for the per-document ObjectStream
1218    //! offsets cache, using a real `/ObjStm`-containing fixture.
1219
1220    use crate::pdf::Pdf;
1221    use crate::xref::parse_object_stream_offsets;
1222
1223    /// A PDF 1.5 document carrying an `/ObjStm`, built here rather than read
1224    /// from the corpus.
1225    ///
1226    /// This used to name a corpus file and `return` when it was missing — so in
1227    /// a tree without the goldens the test passed while asserting nothing, and
1228    /// the corpus name travelled into published source. Building the fixture
1229    /// fixes both: its provenance is this function, and it exists everywhere
1230    /// the test runs.
1231    ///
1232    /// Two objects (catalog and pages) packed into an object stream, addressed
1233    /// by an xref stream — the smallest shape that makes the offsets cache do
1234    /// its work.
1235    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)); // object 2 lives in stream 1, index 0
1278        rows.extend(entry(2, 1, 1)); // object 3 lives in stream 1, index 1
1279        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    /// The fixture is built, so failing to parse it is a fault in this crate,
1296    /// not a missing file. It used to be an `Option` that the tests turned into
1297    /// an early `return`, which is how they passed while asserting nothing.
1298    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        // `Pdf::new` resolves the trailer and catalog during construction;
1309        // for PDF 1.5+ files those typically live in an `/ObjStm` so the
1310        // cache is already non-empty at this point. That is itself
1311        // evidence that the cache is active.
1312        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        // Resolve the catalog explicitly — must not grow the cache because
1319        // the /ObjStm carrying the catalog is already a hit.
1320        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        // Resolve a number of additional indirect objects. Each new
1328        // `/ObjStm` we touch may add one entry, but re-touching anything
1329        // already seen must not.
1330        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        // Idempotency: a second sweep must not grow the cache further.
1337        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        // Sanity: both start with the same construction-time count for
1354        // the same fixture (same shape).
1355        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        // Touch many ids in pdf_a to (likely) populate additional /ObjStm
1360        // cache entries.
1361        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        // pdf_b must NOT have grown — caches are per-document.
1368        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        // Synthetic: dict says N=3 but data has only one (num, offset)
1378        // pair. The helper must return None, and the caller must not
1379        // cache a `None`.
1380        use crate::object::Stream;
1381        use crate::reader::{Reader, ReaderContext, ReaderExt};
1382        use crate::xref::DUMMY_XREF;
1383
1384        // Build a minimal indirect stream object with /N 3 /First 6:
1385        // "1 0 obj <</N 3/First 6/Length 4>>stream\n1 0\nendstream\nendobj\n"
1386        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        // The stream body has only "1 0 " — three entries cannot be
1395        // recovered, so the helper must return `None`.
1396        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    /// QF2-B perf harness. Not a correctness test — it prints microbench
1404    /// numbers and only runs when explicitly requested with
1405    /// `--ignored qf2b_bench`. The harness compares **direct re-parse**
1406    /// of /ObjStm offsets tables (what the pre-QF2-B `ObjectStream::new`
1407    /// did on every `xref.get` of an /ObjStm-stored object) versus the
1408    /// QF2-B cached lookup. This isolates the parse cost from the
1409    /// downstream object-decoding cost, which dominates `xref.get` and
1410    /// would otherwise hide the cache win.
1411    #[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        // 161 /ObjStm headers, 575 KB. Walking and decoding 30+ ObjStms
1417        // here is enough to measure the parse delta cleanly.
1418        // Built from the manifest directory rather than written out as one
1419        // string. This file ships in the published crate and the repository
1420        // tree it points at does not, so a literal path into it is a leaked
1421        // internal path -- which is what the prepublish audit refuses, and it
1422        // stopped the 1.0.0 publish at crate 8 of 32 (#358). It also makes the
1423        // benchmark independent of the working directory it is started from.
1424        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        // Warm cache via a single full sweep so we know which /ObjStms
1440        // exist.
1441        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        // Collect the object-stream container ids by looking them up in
1453        // the xref entries. We then iterate the cache to compare timing
1454        // for parse-from-scratch vs cache-hit.
1455        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            // Only ObjStm container ids resolve as Stream + /Type ObjStm.
1459            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        // Direct re-parse loop — mirrors pre-QF2-B behaviour: parse the
1476        // offsets table from scratch every time, no cache.
1477        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        // Cache-hit loop — mirrors QF2-B behaviour: retrieve the same
1496        // parsed table from the per-document cache.
1497        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        // Acceptance gate: QF2-B target is ≥ 10 % reduction on parse path.
1537        // The microbench should show much more than that, since the cache
1538        // hit is O(1) hashmap fetch + Arc clone vs O(N) memchr+nom parse.
1539        assert!(
1540            reduction >= 10.0,
1541            "QF2-B acceptance: ≥ 10 % parse-time reduction required; got {reduction:.2} %"
1542        );
1543    }
1544}
1545
1546/// Regression tests for fixes ported from hayro upstream (LaurenzV/hayro#1194).
1547///
1548/// Both subsection-header fields come from the file. `start + num_entries` and
1549/// `XREF_ENTRY_LEN * num_entries` overflowed on a header that declares more
1550/// entries than the address space holds.
1551#[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    /// A document whose `/ObjStm` holds an `/Indexed` colour space that names
1572    /// itself as its base (`5 0 R` inside object 5), built here rather than
1573    /// read from the corpus. Mirrors upstream's `object_stream_cycle.pdf`
1574    /// (LaurenzV/hayro#1389) with an uncompressed xref stream, so no
1575    /// test-only deflater is needed.
1576    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        // Offsets count from the first object body, past the `/First` header.
1584        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    /// Ported from LaurenzV/hayro ced00dd0 (#1389): a cycle that runs through
1651    /// a second object stream must resolve to `None`, not recurse. The old
1652    /// code only caught an object stream containing itself
1653    /// (`obj_stream_id == id`); here object 5 lives in stream 6 and refers
1654    /// back to 5, so the base colour space resolved again on every visit.
1655    /// Without the fix this fails on the `is_none` (the self-reference
1656    /// keeps resolving); through a renderer it overflows the stack.
1657    #[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    /// A document whose xref table points objects 3..=10 at byte 1 (inside
1673    /// the `%PDF` header, so no object parses there), while the catalog and
1674    /// page tree stay valid. Mirrors upstream's
1675    /// `concurrent_xref_repair.pdf`: resolving any of 3..=10 must trigger a
1676    /// repair and then read the rebuilt entry.
1677    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        // Object 1: catalog; object 2: page tree. Both keep valid offsets.
1683        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            // Objects 3..=10 carry a bogus offset; 1 and 2 stay valid.
1705            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    /// Ported from LaurenzV/hayro d6fa1180: repairing twice must be a no-op,
1716    /// not a panic. The old code asserted the table was not already
1717    /// repaired, so any second repair — concurrent or sequential — aborted.
1718    #[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    /// Ported from LaurenzV/hayro d6fa1180 + 41d11844: threads that read
1726    /// stale entries while another thread repairs must block and retry on
1727    /// the rebuilt table, not panic on the lock or give up with `None`.
1728    /// Without the fix this panics (`try_put` contention / `assert!`) or
1729    /// resolves `None` for objects the repaired table holds.
1730    #[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                        // Objects 3..=10 have incorrect xref offsets.
1746                        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    /// Two compressed object streams holding `<< /V .. >>` dicts 6..=29,
1761    /// addressed by an xref stream — the smallest shape that races the
1762    /// decoded-bytes publish in `Data::get_with`. Mirrors upstream's
1763    /// `concurrent_object_streams.pdf`.
1764    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        /// Pack `count` dicts into one object-stream payload; returns the
1775        /// payload and the `/First` header length.
1776        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        // Size 31: objects 0..=30. 5 and 30's predecessors stay free.
1846        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    /// Ported from LaurenzV/hayro 0829d581: the decoded bytes of an object
1873    /// stream must be published atomically. The old code inserted the cache
1874    /// index before decoding, so a thread resolving the same stream
1875    /// concurrently read an empty slot and resolved `None`.
1876    #[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                        // Objects 6..=29 are split between two compressed streams.
1892                        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}