mod chain;
mod classic;
mod rebuild;
mod stream;
use pdfrum_common::{Diagnostics, Limits};
use pdfrum_object::{ByteSpan, Dict, Name, Object, names};
pub(crate) use chain::XrefShape;
pub(crate) use rebuild::rebuild;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Entry {
Offset(u64),
InObjStream {
stream: pdfrum_object::ObjRef,
index: u32,
},
Free,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct XEntry {
pub kind: Entry,
pub generation: u16,
pub objstm_flag: bool,
}
impl XEntry {
fn free(generation: u16) -> Self {
Self {
kind: Entry::Free,
generation,
objstm_flag: false,
}
}
}
impl Default for XEntry {
fn default() -> Self {
Self::free(0)
}
}
#[derive(Debug, Clone, Default)]
struct EntryTable {
slots: Vec<Option<XEntry>>,
occupied: usize,
end: usize,
}
impl EntryTable {
fn lookup(&self, num: u32) -> Option<&XEntry> {
self.slots.get(num as usize)?.as_ref()
}
fn put(&mut self, num: u32, entry: XEntry) {
let idx = num as usize;
if idx >= self.slots.len() {
self.slots.resize(idx + 1, None);
}
if let Some(slot) = self.slots.get_mut(idx) {
if slot.is_none() {
self.occupied += 1;
}
*slot = Some(entry);
self.end = self.end.max(idx + 1);
}
}
fn or_default(&mut self, num: u32) -> &mut XEntry {
if self.lookup(num).is_none() {
self.put(num, XEntry::default());
}
self.slots
.get_mut(num as usize)
.and_then(Option::as_mut)
.unwrap_or_else(|| unreachable!("the slot was just materialized"))
}
fn len(&self) -> usize {
self.occupied
}
fn iter(&self) -> impl Iterator<Item = (u32, &XEntry)> + '_ {
self.slots.iter().enumerate().filter_map(|(i, slot)| {
let num = u32::try_from(i).ok()?;
slot.as_ref().map(|e| (num, e))
})
}
fn last(&self) -> u32 {
u32::try_from(self.end.saturating_sub(1)).unwrap_or(u32::MAX)
}
fn truncate(&mut self, size: u32) {
let keep = size as usize;
if let Some(dropped) = self.slots.get(keep..) {
self.occupied -= dropped.iter().flatten().count();
self.slots.truncate(keep);
self.end = self
.slots
.iter()
.rposition(Option::is_some)
.map_or(0, |i| i + 1);
}
}
fn clear(&mut self) {
self.slots.clear();
self.occupied = 0;
self.end = 0;
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Trailer {
pub dict: Dict,
pub object_number: u32,
}
#[derive(Debug, Clone, Default)]
pub struct Xref {
entries: EntryTable,
sections: Vec<Section>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Section {
pub offset: u64,
pub is_stream: bool,
}
impl Xref {
#[must_use]
pub fn sections(&self) -> &[Section] {
&self.sections
}
pub(crate) fn set_sections(&mut self, sections: Vec<Section>) {
self.sections = sections;
}
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn entry(&self, num: u32) -> Option<Entry> {
self.entries.lookup(num).map(|e| e.kind)
}
#[must_use]
pub fn generation(&self, num: u32) -> u16 {
self.entries.lookup(num).map_or(0, |e| e.generation)
}
#[must_use]
pub fn is_object_stream(&self, num: u32) -> bool {
self.entries.lookup(num).is_some_and(|e| e.objstm_flag)
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.len() == 0
}
pub fn object_numbers(&self) -> impl Iterator<Item = u32> + '_ {
self.entries.iter().map(|(num, _)| num)
}
pub(crate) fn iter(&self) -> impl Iterator<Item = (u32, XEntry)> + '_ {
self.entries.iter().map(|(num, &e)| (num, e))
}
#[must_use]
pub fn last_object_number(&self) -> u32 {
self.entries.last()
}
#[must_use]
pub fn is_valid_object_number(&self, num: u32) -> bool {
num <= self.last_object_number()
}
pub(crate) fn add_normal(
&mut self,
num: u32,
generation: u16,
is_objstm: bool,
pos: u64,
limits: &Limits,
) -> bool {
if num > limits.max_object_number {
return false;
}
let flag = match self.entries.lookup(num) {
Some(existing) if existing.generation > generation => return true,
Some(existing) => existing.objstm_flag || is_objstm,
None => is_objstm,
};
self.entries.put(
num,
XEntry {
kind: Entry::Offset(pos),
generation,
objstm_flag: flag,
},
);
true
}
pub(crate) fn add_compressed(
&mut self,
num: u32,
archive: u32,
index: u32,
limits: &Limits,
) -> bool {
if num > limits.max_object_number || archive > limits.max_object_number {
return false;
}
if let Some(existing) = self.entries.lookup(num)
&& (existing.generation > 0 || existing.objstm_flag)
{
return true;
}
self.entries.put(
num,
XEntry {
kind: Entry::InObjStream {
stream: pdfrum_object::ObjRef::new(archive, 0),
index,
},
generation: 0,
objstm_flag: false,
},
);
self.entries.or_default(archive).objstm_flag = true;
true
}
pub(crate) fn set_free(&mut self, num: u32, generation: u16) {
self.entries.put(num, XEntry::free(generation));
}
pub(crate) fn set_size(&mut self, size: u32) {
if size == 0 {
self.entries.clear();
return;
}
self.entries.truncate(size);
self.entries.or_default(size - 1);
}
pub(crate) fn merge_up(&mut self, top: &Self) {
for (num, &entry) in top.entries.iter() {
let merged = match (self.entries.lookup(num), entry.kind) {
(Some(current), Entry::Offset(_))
if matches!(current.kind, Entry::Offset(_)) && current.objstm_flag =>
{
XEntry {
objstm_flag: true,
..entry
}
}
_ => entry,
};
self.entries.put(num, merged);
}
}
}
pub(crate) fn merge_trailers(base: &mut Trailer, winner: &Trailer) {
if base.dict.is_empty() && base.object_number == 0 {
*base = winner.clone();
return;
}
let kept: Vec<(Name, Option<Object>)> = [names::XREF_STM, names::PREV]
.into_iter()
.map(|key| (key.clone(), base.dict.raw(key).cloned()))
.collect();
for (key, value) in winner.dict.iter() {
base.dict.push(key.clone(), value.clone());
}
for (key, value) in kept {
match value {
Some(v) => base.dict.push(key, v),
None => remove_key(&mut base.dict, &key),
}
}
}
pub(crate) fn merge_into_walk(accumulated: &mut Trailer, section: &Trailer) {
let mut merged = section.clone();
merge_trailers(&mut merged, accumulated);
if !accumulated.dict.is_empty() || accumulated.object_number != 0 {
merged.object_number = accumulated.object_number;
}
*accumulated = merged;
}
fn remove_key(dict: &mut Dict, key: &Name) {
let kept: Vec<(Name, Object)> = dict
.iter()
.filter(|(k, _)| k != key)
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
*dict = Dict::from_pairs(kept);
}
pub fn read_xref(
file: &[u8],
limits: &Limits,
diags: &mut Diagnostics,
) -> Result<(Xref, Dict), crate::Error> {
let (xref, trailer, _) = read_xref_full(&ByteSpan::from(file.to_vec()), limits, diags)?;
Ok((xref, trailer.dict))
}
pub(crate) fn read_xref_full(
file: &ByteSpan,
limits: &Limits,
diags: &mut Diagnostics,
) -> Result<(Xref, Trailer, XrefShape), crate::Error> {
chain::load(file, limits, diags)
}
#[cfg(test)]
mod tests {
use super::{Entry, Trailer, Xref, merge_trailers};
use pdfrum_common::Limits;
use pdfrum_object::{Dict, Object, names};
fn limits() -> Limits {
Limits::default()
}
#[test]
fn a_newer_generation_is_not_overwritten() {
let mut x = Xref::new();
assert!(x.add_normal(4, 3, false, 100, &limits()));
assert!(x.add_normal(4, 1, false, 200, &limits()));
assert_eq!(x.entry(4), Some(Entry::Offset(100)));
assert!(x.add_normal(4, 3, false, 300, &limits()));
assert_eq!(x.entry(4), Some(Entry::Offset(300)));
}
#[test]
fn the_object_stream_flag_is_sticky() {
let mut x = Xref::new();
x.add_normal(7, 0, true, 10, &limits());
x.add_normal(7, 0, false, 20, &limits());
assert!(x.is_object_stream(7));
}
#[test]
fn compressed_entries_flag_their_archive() {
let mut x = Xref::new();
assert!(x.add_compressed(5, 7, 2, &limits()));
assert!(x.is_object_stream(7));
assert_eq!(
x.entry(5),
Some(Entry::InObjStream {
stream: pdfrum_object::ObjRef::new(7, 0),
index: 2,
})
);
}
#[test]
fn a_compressed_entry_loses_to_a_newer_generation() {
let mut x = Xref::new();
x.add_normal(5, 2, false, 100, &limits());
assert!(x.add_compressed(5, 7, 0, &limits()));
assert_eq!(x.entry(5), Some(Entry::Offset(100)));
}
#[test]
fn object_numbers_past_the_cap_are_refused() {
let mut x = Xref::new();
assert!(x.add_normal(limits().max_object_number, 0, false, 1, &limits()));
assert!(!x.add_normal(limits().max_object_number + 1, 0, false, 1, &limits()));
}
#[test]
fn the_cached_end_matches_a_scan_after_every_operation() {
fn scanned(x: &Xref) -> u32 {
u32::try_from(
x.entries
.slots
.iter()
.rposition(Option::is_some)
.unwrap_or(0),
)
.unwrap_or(0)
}
let mut x = Xref::new();
assert_eq!(x.last_object_number(), scanned(&x));
x.add_normal(9, 0, false, 90, &limits());
assert_eq!(x.last_object_number(), 9);
assert_eq!(x.last_object_number(), scanned(&x));
x.add_normal(3, 0, false, 30, &limits());
assert_eq!(x.last_object_number(), 9);
assert_eq!(x.last_object_number(), scanned(&x));
x.add_normal(9, 0, false, 91, &limits());
assert_eq!(x.last_object_number(), scanned(&x));
x.set_size(5);
assert_eq!(x.last_object_number(), scanned(&x));
x.add_normal(20, 0, false, 200, &limits());
assert_eq!(x.last_object_number(), 20);
assert_eq!(x.last_object_number(), scanned(&x));
x.set_size(0);
assert_eq!(x.last_object_number(), 0);
assert_eq!(x.last_object_number(), scanned(&x));
}
#[test]
fn resizing_truncates_and_materializes_the_last_slot() {
let mut x = Xref::new();
x.add_normal(1, 0, false, 10, &limits());
x.add_normal(9, 0, false, 90, &limits());
x.set_size(5);
assert_eq!(x.entry(9), None);
assert_eq!(x.entry(1), Some(Entry::Offset(10)));
assert_eq!(x.entry(4), Some(Entry::Free));
x.set_size(0);
assert!(x.is_empty());
}
#[test]
fn merging_lets_the_top_win_but_keeps_the_container_flag() {
let mut current = Xref::new();
current.add_normal(1, 0, true, 10, &limits());
current.add_normal(2, 0, false, 20, &limits());
let mut top = Xref::new();
top.add_normal(1, 0, false, 111, &limits());
top.add_normal(3, 0, false, 30, &limits());
current.merge_up(&top);
assert_eq!(current.entry(1), Some(Entry::Offset(111)));
assert!(current.is_object_stream(1));
assert_eq!(current.entry(2), Some(Entry::Offset(20)));
assert_eq!(current.entry(3), Some(Entry::Offset(30)));
}
#[test]
fn trailer_merge_keeps_the_older_walk_pointers() {
let mut older = Trailer {
dict: Dict::from_pairs([
(names::PREV.clone(), Object::Int(100)),
(names::SIZE.clone(), Object::Int(5)),
]),
object_number: 0,
};
let newer = Trailer {
dict: Dict::from_pairs([
(names::PREV.clone(), Object::Int(999)),
(names::SIZE.clone(), Object::Int(9)),
(names::ROOT.clone(), Object::Int(1)),
]),
object_number: 7,
};
merge_trailers(&mut older, &newer);
assert_eq!(older.dict.direct_int(names::SIZE), Some(9));
assert!(older.dict.raw(names::ROOT).is_some());
assert_eq!(older.dict.direct_int(names::PREV), Some(100));
assert_eq!(older.object_number, 0);
}
#[test]
fn a_pointer_the_older_trailer_lacked_does_not_survive() {
let mut older = Trailer {
dict: Dict::from_pairs([(names::SIZE.clone(), Object::Int(5))]),
object_number: 3,
};
let newer = Trailer {
dict: Dict::from_pairs([(names::PREV.clone(), Object::Int(999))]),
object_number: 0,
};
merge_trailers(&mut older, &newer);
assert_eq!(older.dict.raw(names::PREV), None);
}
#[test]
fn merging_into_an_empty_trailer_just_takes_it() {
let mut older = Trailer::default();
let newer = Trailer {
dict: Dict::from_pairs([(names::PREV.clone(), Object::Int(42))]),
object_number: 7,
};
merge_trailers(&mut older, &newer);
assert_eq!(older.dict.direct_int(names::PREV), Some(42));
assert_eq!(older.object_number, 7);
}
}