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}