Skip to main content

pdfrum_parser/xref/
mod.rs

1//! Cross-reference information: where every object in the file lives
2//! (ISO 32000-1 §7.5.4 to §7.5.8).
3//!
4//! # One map, many sources
5//!
6//! A file can describe its objects four ways — a classic table, a
7//! cross-reference stream, both at once in a hybrid file, or a chain of
8//! `/Prev`-linked sections recording successive edits — and a damaged file
9//! can describe them none of those ways, in which case the whole file is
10//! scanned for object headers instead. All four paths and the recovery scan
11//! produce the same [`Xref`], so nothing above this module needs to know
12//! which one ran.
13//!
14//! # Newest wins, and the merge is asymmetric
15//!
16//! Sections are read newest-first and merged so that an entry already present
17//! is never overwritten by an older section's version of it. The trailer
18//! merge inverts that for exactly two keys: `/Prev` and `/XRefStm` keep the
19//! *older* section's values, because those are the pointers the walk is
20//! following and taking the newer ones would make it revisit sections it has
21//! already read. That asymmetry is the whole reason the walk terminates.
22
23mod chain;
24mod classic;
25mod rebuild;
26mod stream;
27
28use pdfrum_common::{Diagnostics, Limits};
29use pdfrum_object::{ByteSpan, Dict, Name, Object, names};
30
31pub(crate) use chain::XrefShape;
32pub(crate) use rebuild::rebuild;
33
34/// Where one object lives.
35///
36/// The three variants are the three things a cross-reference entry can say
37/// (ISO 32000-1 tables 18 and 18): the object is at a byte offset, it is
38/// packed inside an object stream, or it is not there at all.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum Entry {
41    /// The object's `N G obj` header starts at this byte offset.
42    Offset(u64),
43    /// The object is the `index`-th member of an object stream.
44    InObjStream {
45        /// The object stream holding it.
46        stream: pdfrum_object::ObjRef,
47        /// Its position in that stream's member table.
48        index: u32,
49    },
50    /// The slot is free: the object was deleted, or never existed.
51    Free,
52}
53
54/// One row of the table: where the object is, plus the bookkeeping the
55/// reader needs to merge sections correctly.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub(crate) struct XEntry {
58    /// Where the object lives.
59    pub kind: Entry,
60    /// The generation the table claims. Merges compare it: an entry naming a
61    /// newer generation is never replaced by one naming an older.
62    pub generation: u16,
63    /// Whether some entry has named this object as its object stream. Only
64    /// an object flagged here may be *used* as one, which is what stops a
65    /// forged `/Type /ObjStm` from being decoded as a container.
66    pub objstm_flag: bool,
67}
68
69impl XEntry {
70    /// A free entry of the given generation.
71    fn free(generation: u16) -> Self {
72        Self {
73            kind: Entry::Free,
74            generation,
75            objstm_flag: false,
76        }
77    }
78}
79
80impl Default for XEntry {
81    fn default() -> Self {
82        Self::free(0)
83    }
84}
85
86/// The merged cross-reference table: which object number holds which entry.
87///
88/// # Why a slot vector and not a map
89///
90/// Object numbers are dense. A trailer `/Size` is a claim that the file's
91/// objects are numbered `0..size`, and real files honor it: the table is a
92/// near-complete range, not a sparse scattering. That makes the natural
93/// representation an array indexed by object number, where reading or
94/// writing a slot is an offset computation rather than a tree descent.
95///
96/// Callgrind put the per-entry tree cost at 28% of `Document::from_bytes` and
97/// [`Xref::merge_up`] — which re-inserted every key of every section while
98/// walking `/Prev` — at a further 24%: **52% of open spent in the container
99/// alone**, for a load that writes each object's entry once per section
100/// naming it.
101///
102/// What the vector removes is the per-entry cost. What it keeps is every
103/// rule about which entry wins, unchanged and applied in the same order.
104#[derive(Debug, Clone, Default)]
105struct EntryTable {
106    /// One slot per object number, `None` where the table says nothing;
107    /// `slots[n]` is object `n`. Growth is bounded by
108    /// [`Limits::max_object_number`], which every writer checks before
109    /// reaching this type.
110    slots: Vec<Option<XEntry>>,
111    /// How many slots are occupied, kept incrementally: `len` is read per
112    /// document and counting it otherwise means scanning the vector.
113    occupied: usize,
114    /// One past the largest occupied object number, kept incrementally for
115    /// the same reason `occupied` is. `last` is read on **every object
116    /// fetch**, because `Store::get` calls `Xref::is_valid_object_number`
117    /// before it looks anything up (`crate::store`), and deriving it with a
118    /// backwards scan makes that fetch O(number of slots) — quadratic over a
119    /// document whose objects are each fetched once, on a buffer larger than
120    /// L2 for a table of any size (9950 objects at 32 bytes a slot is
121    /// 318 KiB).
122    ///
123    /// Measured on `forms_widgets_407`, the corpus's largest table, this was
124    /// **not** what made that file's open slow — the scan does not show up in
125    /// a cachegrind A/B — so this field is a bound made unreachable rather
126    /// than a regression repaired. It stays because the bound is real and the
127    /// field costs one `usize`.
128    ///
129    /// Only `put` raises this, and only `truncate` and `clear` lower it.
130    end: usize,
131}
132
133impl EntryTable {
134    /// The entry for `num`, if the table describes it.
135    fn lookup(&self, num: u32) -> Option<&XEntry> {
136        self.slots.get(num as usize)?.as_ref()
137    }
138
139    /// Write `entry` into `num`'s slot, growing the vector to reach it.
140    ///
141    /// The caller has already decided this entry wins, and the bounds check
142    /// against [`Limits::max_object_number`] is likewise the caller's — only
143    /// it knows whether an out-of-range number is a refusal or a skip.
144    fn put(&mut self, num: u32, entry: XEntry) {
145        let idx = num as usize;
146        if idx >= self.slots.len() {
147            self.slots.resize(idx + 1, None);
148        }
149        // The resize above guarantees the slot; `get_mut` rather than an
150        // index so the lint that forbids panicking indexing stays on.
151        if let Some(slot) = self.slots.get_mut(idx) {
152            if slot.is_none() {
153                self.occupied += 1;
154            }
155            *slot = Some(entry);
156            self.end = self.end.max(idx + 1);
157        }
158    }
159
160    /// The entry for `num`, materialized as free when the slot is empty.
161    fn or_default(&mut self, num: u32) -> &mut XEntry {
162        if self.lookup(num).is_none() {
163            self.put(num, XEntry::default());
164        }
165        self.slots
166            .get_mut(num as usize)
167            .and_then(Option::as_mut)
168            .unwrap_or_else(|| unreachable!("the slot was just materialized"))
169    }
170
171    /// How many objects the table describes.
172    fn len(&self) -> usize {
173        self.occupied
174    }
175
176    /// Every occupied slot, in ascending object-number order.
177    fn iter(&self) -> impl Iterator<Item = (u32, &XEntry)> + '_ {
178        // Every index is an object number that was written through `put`,
179        // so it came from a `u32` and converts back; a slot past `u32::MAX`
180        // cannot exist and is skipped rather than truncated into a wrong one.
181        self.slots.iter().enumerate().filter_map(|(i, slot)| {
182            let num = u32::try_from(i).ok()?;
183            slot.as_ref().map(|e| (num, e))
184        })
185    }
186
187    /// The largest object number the table describes.
188    ///
189    /// Read per object fetch, so it is a field read and not a scan; see
190    /// `end` for what that costs when it is not.
191    fn last(&self) -> u32 {
192        u32::try_from(self.end.saturating_sub(1)).unwrap_or(u32::MAX)
193    }
194
195    /// Drop every slot at or past `size`.
196    fn truncate(&mut self, size: u32) {
197        let keep = size as usize;
198        if let Some(dropped) = self.slots.get(keep..) {
199            self.occupied -= dropped.iter().flatten().count();
200            self.slots.truncate(keep);
201            // The scan a fetch no longer pays for, paid once here instead:
202            // truncation is the one operation that can lower the end, and it
203            // runs per document rather than per object.
204            self.end = self
205                .slots
206                .iter()
207                .rposition(Option::is_some)
208                .map_or(0, |i| i + 1);
209        }
210    }
211
212    /// Forget every entry.
213    fn clear(&mut self) {
214        self.slots.clear();
215        self.occupied = 0;
216        self.end = 0;
217    }
218}
219
220/// The trailer dictionary plus which object it came out of.
221///
222/// The object number matters to incremental saving: a trailer written as a
223/// plain `trailer` keyword has no object number, while one that is a
224/// cross-reference stream's dictionary belongs to that stream's object.
225#[derive(Debug, Clone, Default, PartialEq)]
226pub struct Trailer {
227    /// The trailer's keys.
228    pub dict: Dict,
229    /// The object number the trailer came from; zero when it was written as
230    /// a bare `trailer` dictionary.
231    pub object_number: u32,
232}
233
234/// Where every object in a document lives.
235///
236/// Ordered by object number, because reading it that way is what both the
237/// page walk and the writer want, and because the largest object number is a
238/// value the reader consults constantly. That order is an array index rather
239/// than a tree; the private `EntryTable` this wraps carries the why.
240#[derive(Debug, Clone, Default)]
241pub struct Xref {
242    entries: EntryTable,
243    /// The cross-reference sections the load followed, oldest first: each
244    /// one an incremental update's table or stream, at its byte offset.
245    sections: Vec<Section>,
246}
247
248/// One cross-reference section of the file, as the `/Prev` chain found it.
249#[derive(Debug, Clone, Copy, PartialEq, Eq)]
250pub struct Section {
251    /// Where the section starts.
252    pub offset: u64,
253    /// A cross-reference stream rather than a classic table.
254    pub is_stream: bool,
255}
256
257impl Xref {
258    /// The sections the load followed, oldest first — one per revision of
259    /// an incrementally updated file. Empty when the table was rebuilt by
260    /// scanning, since no chain was followed then.
261    #[must_use]
262    pub fn sections(&self) -> &[Section] {
263        &self.sections
264    }
265
266    /// Record the chain the load followed.
267    pub(crate) fn set_sections(&mut self, sections: Vec<Section>) {
268        self.sections = sections;
269    }
270
271    /// An empty table.
272    #[must_use]
273    pub fn new() -> Self {
274        Self::default()
275    }
276
277    /// Where object `num` lives, if the table says anything about it.
278    #[must_use]
279    pub fn entry(&self, num: u32) -> Option<Entry> {
280        self.entries.lookup(num).map(|e| e.kind)
281    }
282
283    /// The generation the table records for `num`.
284    #[must_use]
285    pub fn generation(&self, num: u32) -> u16 {
286        self.entries.lookup(num).map_or(0, |e| e.generation)
287    }
288
289    /// Whether some entry named `num` as the object stream it lives in.
290    #[must_use]
291    pub fn is_object_stream(&self, num: u32) -> bool {
292        self.entries.lookup(num).is_some_and(|e| e.objstm_flag)
293    }
294
295    /// How many objects the table describes.
296    #[must_use]
297    pub fn len(&self) -> usize {
298        self.entries.len()
299    }
300
301    /// Whether the table describes nothing.
302    #[must_use]
303    pub fn is_empty(&self) -> bool {
304        self.entries.len() == 0
305    }
306
307    /// Every object number the table describes, in ascending order.
308    pub fn object_numbers(&self) -> impl Iterator<Item = u32> + '_ {
309        self.entries.iter().map(|(num, _)| num)
310    }
311
312    /// Every entry, in ascending object-number order.
313    pub(crate) fn iter(&self) -> impl Iterator<Item = (u32, XEntry)> + '_ {
314        self.entries.iter().map(|(num, &e)| (num, e))
315    }
316
317    /// The largest object number the table describes.
318    #[must_use]
319    pub fn last_object_number(&self) -> u32 {
320        self.entries.last()
321    }
322
323    /// Whether `num` is a number this table could possibly describe.
324    ///
325    /// Object numbers past the table's largest are unfetchable even when the
326    /// bytes for them sit in the file — the table is the only index the
327    /// reader has, and PDFium refuses to look past its end.
328    #[must_use]
329    pub fn is_valid_object_number(&self, num: u32) -> bool {
330        num <= self.last_object_number()
331    }
332
333    /// Record an object at a byte offset.
334    ///
335    /// Ignored when an entry already present names a *newer* generation, so
336    /// a later-read older section cannot undo a newer one's edit. The
337    /// object-stream flag is sticky: once something has named this object as
338    /// a container, overwriting the entry does not clear that.
339    ///
340    /// The returned flag says only whether the object *number* was usable —
341    /// not whether the entry changed. Declining to overwrite a newer
342    /// generation is a success: the table already knows something better
343    /// about that object.
344    pub(crate) fn add_normal(
345        &mut self,
346        num: u32,
347        generation: u16,
348        is_objstm: bool,
349        pos: u64,
350        limits: &Limits,
351    ) -> bool {
352        if num > limits.max_object_number {
353            return false;
354        }
355        let flag = match self.entries.lookup(num) {
356            Some(existing) if existing.generation > generation => return true,
357            Some(existing) => existing.objstm_flag || is_objstm,
358            None => is_objstm,
359        };
360        self.entries.put(
361            num,
362            XEntry {
363                kind: Entry::Offset(pos),
364                generation,
365                objstm_flag: flag,
366            },
367        );
368        true
369    }
370
371    /// Record an object as living inside an object stream.
372    ///
373    /// Ignored when the existing entry names a non-zero generation or is
374    /// itself a known container: a compressed object always has generation
375    /// zero, so an entry claiming otherwise is describing something else.
376    /// The archive's own entry is created if absent and flagged either way —
377    /// that flag is what later authorizes decoding it as a container.
378    ///
379    /// As with [`Xref::add_normal`], the returned flag reports the object
380    /// numbers' usability rather than whether anything was written.
381    pub(crate) fn add_compressed(
382        &mut self,
383        num: u32,
384        archive: u32,
385        index: u32,
386        limits: &Limits,
387    ) -> bool {
388        if num > limits.max_object_number || archive > limits.max_object_number {
389            return false;
390        }
391        if let Some(existing) = self.entries.lookup(num)
392            && (existing.generation > 0 || existing.objstm_flag)
393        {
394            return true;
395        }
396        self.entries.put(
397            num,
398            XEntry {
399                kind: Entry::InObjStream {
400                    stream: pdfrum_object::ObjRef::new(archive, 0),
401                    index,
402                },
403                generation: 0,
404                objstm_flag: false,
405            },
406        );
407        self.entries.or_default(archive).objstm_flag = true;
408        true
409    }
410
411    /// Mark an object free, unconditionally.
412    pub(crate) fn set_free(&mut self, num: u32, generation: u16) {
413        self.entries.put(num, XEntry::free(generation));
414    }
415
416    /// Resize the table to describe exactly `size` objects.
417    ///
418    /// Two things happen, both observable. Entries for object numbers at or
419    /// past `size` are erased — a trailer `/Size` is a claim about what the
420    /// file contains and the reader honors it. And the last slot is
421    /// materialized as free if nothing else claimed it, which is why a
422    /// document's table often ends in an entry no section ever wrote.
423    pub(crate) fn set_size(&mut self, size: u32) {
424        if size == 0 {
425            self.entries.clear();
426            return;
427        }
428        self.entries.truncate(size);
429        self.entries.or_default(size - 1);
430    }
431
432    /// Apply `top` onto `self`, with `top`'s entries winning conflicts.
433    ///
434    /// The one thing carried the other way: when both sides have an offset
435    /// entry for the same object, `self`'s object-stream flag survives onto
436    /// the winner, because knowing an object is a container is information
437    /// neither section can invalidate.
438    pub(crate) fn merge_up(&mut self, top: &Self) {
439        for (num, &entry) in top.entries.iter() {
440            let merged = match (self.entries.lookup(num), entry.kind) {
441                (Some(current), Entry::Offset(_))
442                    if matches!(current.kind, Entry::Offset(_)) && current.objstm_flag =>
443                {
444                    XEntry {
445                        objstm_flag: true,
446                        ..entry
447                    }
448                }
449                _ => entry,
450            };
451            self.entries.put(num, merged);
452        }
453    }
454}
455
456/// Merge `winner`'s keys onto `base`, leaving the result in `base`.
457///
458/// Every key of `winner` overwrites `base`'s — except `/Prev` and `/XRefStm`,
459/// which stay as **`base`** has them. Those two say where to look next, and
460/// replacing them would send the walk through sections it has already read.
461///
462/// The two roles are not "older" and "newer", and getting them backwards is
463/// the easiest mistake in this file. During the `/Prev` walk the trailer
464/// accumulated so far is the *newer* one and wins, while the section just
465/// read is the *older* one supplying the next pointer — so `base` is the
466/// freshly-read older trailer and `winner` is the accumulation. During the
467/// recovery scan the roles invert: what has been scanned so far is `base` and
468/// each newly-found trailer wins. Callers say which they mean by argument
469/// order; see [`merge_into_walk`].
470pub(crate) fn merge_trailers(base: &mut Trailer, winner: &Trailer) {
471    if base.dict.is_empty() && base.object_number == 0 {
472        *base = winner.clone();
473        return;
474    }
475    let kept: Vec<(Name, Option<Object>)> = [names::XREF_STM, names::PREV]
476        .into_iter()
477        .map(|key| (key.clone(), base.dict.raw(key).cloned()))
478        .collect();
479
480    for (key, value) in winner.dict.iter() {
481        base.dict.push(key.clone(), value.clone());
482    }
483    for (key, value) in kept {
484        match value {
485            Some(v) => base.dict.push(key, v),
486            // `base` had no such pointer, so the winner's must not survive.
487            None => remove_key(&mut base.dict, &key),
488        }
489    }
490}
491
492/// Fold a section's trailer into the walk's accumulation.
493///
494/// The walk reads newest-first, so `section` is *older* than everything
495/// `accumulated` holds: its keys lose, and its `/Prev` and `/XRefStm` — the
496/// pointers that say where to go next — are the ones kept. That inversion is
497/// why this wrapper exists rather than callers arranging the arguments
498/// themselves.
499pub(crate) fn merge_into_walk(accumulated: &mut Trailer, section: &Trailer) {
500    let mut merged = section.clone();
501    merge_trailers(&mut merged, accumulated);
502    // The accumulated trailer keeps its own object number, since the walk's
503    // identity is the newest section's.
504    if !accumulated.dict.is_empty() || accumulated.object_number != 0 {
505        merged.object_number = accumulated.object_number;
506    }
507    *accumulated = merged;
508}
509
510/// Drop every entry with this key.
511fn remove_key(dict: &mut Dict, key: &Name) {
512    let kept: Vec<(Name, Object)> = dict
513        .iter()
514        .filter(|(k, _)| k != key)
515        .map(|(k, v)| (k.clone(), v.clone()))
516        .collect();
517    *dict = Dict::from_pairs(kept);
518}
519
520/// Read a document's cross-reference information, rebuilding it if needed.
521///
522/// `file` is the document from its `%PDF` header onwards, so every offset
523/// this returns indexes straight into it. Failure means neither the
524/// structured paths nor the full-file scan found anything usable.
525///
526/// # Errors
527///
528/// [`Error::XrefBroken`](crate::Error::XrefBroken) when no section could be
529/// read and the rebuild found no objects or no trailer.
530///
531/// ```
532/// use pdfrum_common::{Diagnostics, Limits};
533/// use pdfrum_parser::{Entry, read_xref};
534///
535/// let file = b"%PDF-1.7\n\
536///              1 0 obj << /Type /Catalog >> endobj\n\
537///              trailer << /Root 1 0 R >>\n\
538///              startxref\n0\n%%EOF\n";
539/// let mut diags = Diagnostics::default();
540/// let (xref, trailer) = read_xref(file, &Limits::default(), &mut diags)?;
541/// // No usable startxref, so the file was scanned for object headers.
542/// assert!(matches!(xref.entry(1), Some(Entry::Offset(_))));
543/// assert!(trailer.raw(pdfrum_object::names::ROOT).is_some());
544/// # Ok::<(), pdfrum_parser::Error>(())
545/// ```
546pub fn read_xref(
547    file: &[u8],
548    limits: &Limits,
549    diags: &mut Diagnostics,
550) -> Result<(Xref, Dict), crate::Error> {
551    // This entry point takes a plain slice, so it is the one place that has
552    // to pay for the copy. Every reader inside the crate arrives through
553    // `read_xref_full` with the span it already holds.
554    let (xref, trailer, _) = read_xref_full(&ByteSpan::from(file.to_vec()), limits, diags)?;
555    Ok((xref, trailer.dict))
556}
557
558/// Read cross-reference information, reporting the shape it turned out to
559/// have — see [`XrefShape`].
560pub(crate) fn read_xref_full(
561    file: &ByteSpan,
562    limits: &Limits,
563    diags: &mut Diagnostics,
564) -> Result<(Xref, Trailer, XrefShape), crate::Error> {
565    chain::load(file, limits, diags)
566}
567
568#[cfg(test)]
569mod tests {
570    use super::{Entry, Trailer, Xref, merge_trailers};
571    use pdfrum_common::Limits;
572    use pdfrum_object::{Dict, Object, names};
573
574    fn limits() -> Limits {
575        Limits::default()
576    }
577
578    #[test]
579    fn a_newer_generation_is_not_overwritten() {
580        let mut x = Xref::new();
581        assert!(x.add_normal(4, 3, false, 100, &limits()));
582        // An older generation loses — and that is not a failure: the number
583        // was fine, the table simply already knew something newer.
584        assert!(x.add_normal(4, 1, false, 200, &limits()));
585        assert_eq!(x.entry(4), Some(Entry::Offset(100)));
586        // The same or a newer one wins.
587        assert!(x.add_normal(4, 3, false, 300, &limits()));
588        assert_eq!(x.entry(4), Some(Entry::Offset(300)));
589    }
590
591    #[test]
592    fn the_object_stream_flag_is_sticky() {
593        let mut x = Xref::new();
594        x.add_normal(7, 0, true, 10, &limits());
595        x.add_normal(7, 0, false, 20, &limits());
596        assert!(x.is_object_stream(7));
597    }
598
599    #[test]
600    fn compressed_entries_flag_their_archive() {
601        let mut x = Xref::new();
602        assert!(x.add_compressed(5, 7, 2, &limits()));
603        assert!(x.is_object_stream(7));
604        assert_eq!(
605            x.entry(5),
606            Some(Entry::InObjStream {
607                stream: pdfrum_object::ObjRef::new(7, 0),
608                index: 2,
609            })
610        );
611    }
612
613    #[test]
614    fn a_compressed_entry_loses_to_a_newer_generation() {
615        let mut x = Xref::new();
616        x.add_normal(5, 2, false, 100, &limits());
617        assert!(x.add_compressed(5, 7, 0, &limits()));
618        // Accepted as a number, declined as an entry.
619        assert_eq!(x.entry(5), Some(Entry::Offset(100)));
620    }
621
622    #[test]
623    fn object_numbers_past_the_cap_are_refused() {
624        let mut x = Xref::new();
625        assert!(x.add_normal(limits().max_object_number, 0, false, 1, &limits()));
626        assert!(!x.add_normal(limits().max_object_number + 1, 0, false, 1, &limits()));
627    }
628
629    /// The cached end tracks the same value a backwards scan would find.
630    ///
631    /// `last` is a field read rather than a scan because `Store::get`
632    /// consults it per fetch; this pins the two against each other across
633    /// every operation that can move it — growing, overwriting, truncating
634    /// down and back up, and clearing.
635    #[test]
636    fn the_cached_end_matches_a_scan_after_every_operation() {
637        fn scanned(x: &Xref) -> u32 {
638            u32::try_from(
639                x.entries
640                    .slots
641                    .iter()
642                    .rposition(Option::is_some)
643                    .unwrap_or(0),
644            )
645            .unwrap_or(0)
646        }
647
648        let mut x = Xref::new();
649        assert_eq!(x.last_object_number(), scanned(&x));
650        x.add_normal(9, 0, false, 90, &limits());
651        assert_eq!(x.last_object_number(), 9);
652        assert_eq!(x.last_object_number(), scanned(&x));
653        // Writing below the end does not move it.
654        x.add_normal(3, 0, false, 30, &limits());
655        assert_eq!(x.last_object_number(), 9);
656        assert_eq!(x.last_object_number(), scanned(&x));
657        // Overwriting the end does not move it either.
658        x.add_normal(9, 0, false, 91, &limits());
659        assert_eq!(x.last_object_number(), scanned(&x));
660        // Truncating lowers it, to the largest survivor.
661        x.set_size(5);
662        assert_eq!(x.last_object_number(), scanned(&x));
663        // And growing again raises it.
664        x.add_normal(20, 0, false, 200, &limits());
665        assert_eq!(x.last_object_number(), 20);
666        assert_eq!(x.last_object_number(), scanned(&x));
667        x.set_size(0);
668        assert_eq!(x.last_object_number(), 0);
669        assert_eq!(x.last_object_number(), scanned(&x));
670    }
671
672    #[test]
673    fn resizing_truncates_and_materializes_the_last_slot() {
674        let mut x = Xref::new();
675        x.add_normal(1, 0, false, 10, &limits());
676        x.add_normal(9, 0, false, 90, &limits());
677        x.set_size(5);
678        assert_eq!(x.entry(9), None);
679        assert_eq!(x.entry(1), Some(Entry::Offset(10)));
680        // A phantom last entry appears, free.
681        assert_eq!(x.entry(4), Some(Entry::Free));
682        x.set_size(0);
683        assert!(x.is_empty());
684    }
685
686    #[test]
687    fn merging_lets_the_top_win_but_keeps_the_container_flag() {
688        let mut current = Xref::new();
689        current.add_normal(1, 0, true, 10, &limits());
690        current.add_normal(2, 0, false, 20, &limits());
691        let mut top = Xref::new();
692        top.add_normal(1, 0, false, 111, &limits());
693        top.add_normal(3, 0, false, 30, &limits());
694
695        current.merge_up(&top);
696        assert_eq!(current.entry(1), Some(Entry::Offset(111)));
697        assert!(current.is_object_stream(1));
698        assert_eq!(current.entry(2), Some(Entry::Offset(20)));
699        assert_eq!(current.entry(3), Some(Entry::Offset(30)));
700    }
701
702    #[test]
703    fn trailer_merge_keeps_the_older_walk_pointers() {
704        let mut older = Trailer {
705            dict: Dict::from_pairs([
706                (names::PREV.clone(), Object::Int(100)),
707                (names::SIZE.clone(), Object::Int(5)),
708            ]),
709            object_number: 0,
710        };
711        let newer = Trailer {
712            dict: Dict::from_pairs([
713                (names::PREV.clone(), Object::Int(999)),
714                (names::SIZE.clone(), Object::Int(9)),
715                (names::ROOT.clone(), Object::Int(1)),
716            ]),
717            object_number: 7,
718        };
719        merge_trailers(&mut older, &newer);
720        // The newer /Size and /Root win...
721        assert_eq!(older.dict.direct_int(names::SIZE), Some(9));
722        assert!(older.dict.raw(names::ROOT).is_some());
723        // ...but /Prev stays the older section's, or the walk would loop.
724        assert_eq!(older.dict.direct_int(names::PREV), Some(100));
725        // The object number stays the accumulating trailer's.
726        assert_eq!(older.object_number, 0);
727    }
728
729    #[test]
730    fn a_pointer_the_older_trailer_lacked_does_not_survive() {
731        let mut older = Trailer {
732            dict: Dict::from_pairs([(names::SIZE.clone(), Object::Int(5))]),
733            object_number: 3,
734        };
735        let newer = Trailer {
736            dict: Dict::from_pairs([(names::PREV.clone(), Object::Int(999))]),
737            object_number: 0,
738        };
739        merge_trailers(&mut older, &newer);
740        assert_eq!(older.dict.raw(names::PREV), None);
741    }
742
743    #[test]
744    fn merging_into_an_empty_trailer_just_takes_it() {
745        let mut older = Trailer::default();
746        let newer = Trailer {
747            dict: Dict::from_pairs([(names::PREV.clone(), Object::Int(42))]),
748            object_number: 7,
749        };
750        merge_trailers(&mut older, &newer);
751        assert_eq!(older.dict.direct_int(names::PREV), Some(42));
752        assert_eq!(older.object_number, 7);
753    }
754}