use std::collections::BTreeMap;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct ObjectOffsets(BTreeMap<u32, u64>);
impl ObjectOffsets {
#[must_use]
pub(crate) fn new() -> Self {
Self(BTreeMap::new())
}
pub(crate) fn set(&mut self, num: u32, offset: u64) {
self.0.insert(num, offset);
}
pub(crate) fn erase(&mut self, num: u32) {
self.0.remove(&num);
}
#[must_use]
pub(crate) fn get(&self, num: u32) -> Option<u64> {
self.0.get(&num).copied()
}
#[must_use]
pub(crate) fn contains(&self, num: u32) -> bool {
self.0.contains_key(&num)
}
#[must_use]
pub(crate) fn last(&self) -> u32 {
self.0.keys().next_back().copied().unwrap_or(0)
}
}
fn entry_line(out: &mut Vec<u8>, offset: u64) {
out.extend_from_slice(format!("{offset:010} 00000 n\r\n").as_bytes());
}
const FREE_HEAD: &[u8] = b"0000000000 65535 f\r\n";
pub(crate) fn classic_full(out: &mut Vec<u8>, offsets: &ObjectOffsets, last: u32) {
out.extend_from_slice(b"xref\r\n");
if !offsets.contains(1) {
out.extend_from_slice(b"0 1\r\n");
out.extend_from_slice(FREE_HEAD);
}
let mut i: u32 = 1;
while i <= last {
while i <= last && !offsets.contains(i) {
i = i.saturating_add(1);
}
if i > last {
break;
}
let start = i;
let mut end = i;
while end <= last && offsets.contains(end) {
end = end.saturating_add(1);
}
if start == 1 {
out.extend_from_slice(format!("0 {end}\r\n").as_bytes());
out.extend_from_slice(FREE_HEAD);
} else {
out.extend_from_slice(format!("{start} {}\r\n", end - start).as_bytes());
}
for n in start..end {
entry_line(out, offsets.get(n).unwrap_or(0));
}
i = end;
}
}
pub(crate) fn classic_delta(out: &mut Vec<u8>, offsets: &ObjectOffsets, written: &[u32]) {
out.extend_from_slice(b"xref\r\n");
let mut i = 0usize;
while i < written.len() {
let Some(&start) = written.get(i) else {
break;
};
let mut j = i.saturating_add(1);
while written
.get(j)
.zip(written.get(j.saturating_sub(1)))
.is_some_and(|(next, prev)| *next == prev.saturating_add(1))
{
j = j.saturating_add(1);
}
let run = j - i;
if start == 1 {
out.extend_from_slice(format!("0 {}\r\n", run + 1).as_bytes());
out.extend_from_slice(FREE_HEAD);
} else {
out.extend_from_slice(format!("{start} {run}\r\n").as_bytes());
}
for n in written.get(i..j).unwrap_or_default() {
entry_line(out, offsets.get(*n).unwrap_or(0));
}
i = j;
}
}
pub(crate) fn stream_record(out: &mut Vec<u8>, offset: u64) {
let truncated = u32::try_from(offset).unwrap_or(u32::MAX);
out.extend_from_slice(&truncated.to_be_bytes());
out.push(0);
}
#[cfg(test)]
mod tests {
use super::{ObjectOffsets, classic_delta, classic_full, stream_record};
fn offsets(entries: &[(u32, u64)]) -> ObjectOffsets {
let mut o = ObjectOffsets::new();
for (num, at) in entries {
o.set(*num, *at);
}
o
}
fn text(f: impl FnOnce(&mut Vec<u8>)) -> String {
let mut out = Vec::new();
f(&mut out);
String::from_utf8_lossy(&out).into_owned()
}
#[test]
fn one_run_from_object_one_folds_the_free_head_in() {
let o = offsets(&[(1, 9), (2, 100), (3, 250)]);
assert_eq!(
text(|out| classic_full(out, &o, 3)),
"xref\r\n\
0 4\r\n\
0000000000 65535 f\r\n\
0000000009 00000 n\r\n\
0000000100 00000 n\r\n\
0000000250 00000 n\r\n"
);
}
#[test]
fn the_free_head_generation_is_sixty_five_five_three_five() {
let o = offsets(&[(1, 9)]);
let table = text(|out| classic_full(out, &o, 1));
assert!(table.contains("0000000000 65535 f\r\n"));
assert!(!table.contains("65536"));
}
#[test]
fn a_missing_object_one_gets_a_standalone_free_subsection() {
let o = offsets(&[(2, 100), (3, 250)]);
assert_eq!(
text(|out| classic_full(out, &o, 3)),
"xref\r\n\
0 1\r\n\
0000000000 65535 f\r\n\
2 2\r\n\
0000000100 00000 n\r\n\
0000000250 00000 n\r\n"
);
}
#[test]
fn a_gap_starts_a_second_subsection() {
let o = offsets(&[(1, 9), (2, 100), (7, 700), (8, 800)]);
assert_eq!(
text(|out| classic_full(out, &o, 8)),
"xref\r\n\
0 3\r\n\
0000000000 65535 f\r\n\
0000000009 00000 n\r\n\
0000000100 00000 n\r\n\
7 2\r\n\
0000000700 00000 n\r\n\
0000000800 00000 n\r\n"
);
}
#[test]
fn nothing_past_last_is_written() {
let o = offsets(&[(1, 9), (2, 100), (50, 5000)]);
let table = text(|out| classic_full(out, &o, 2));
assert!(!table.contains("0000005000"));
}
#[test]
fn a_delta_table_emits_one_subsection_per_run() {
let o = offsets(&[(2, 200), (3, 300), (4, 400), (9, 900), (10, 1000)]);
assert_eq!(
text(|out| classic_delta(out, &o, &[2, 3, 4, 9, 10])),
"xref\r\n\
2 3\r\n\
0000000200 00000 n\r\n\
0000000300 00000 n\r\n\
0000000400 00000 n\r\n\
9 2\r\n\
0000000900 00000 n\r\n\
0000001000 00000 n\r\n"
);
}
#[test]
fn a_delta_run_starting_at_one_still_carries_the_free_head() {
let o = offsets(&[(1, 10), (2, 20)]);
assert_eq!(
text(|out| classic_delta(out, &o, &[1, 2])),
"xref\r\n\
0 3\r\n\
0000000000 65535 f\r\n\
0000000010 00000 n\r\n\
0000000020 00000 n\r\n"
);
}
#[test]
fn an_empty_delta_writes_only_the_keyword() {
let o = ObjectOffsets::new();
assert_eq!(text(|out| classic_delta(out, &o, &[])), "xref\r\n");
}
#[test]
fn a_stream_record_is_a_big_endian_offset_and_a_zero() {
let mut out = Vec::new();
stream_record(&mut out, 0x0001_0203);
assert_eq!(out, vec![0x00, 0x01, 0x02, 0x03, 0x00]);
}
#[test]
fn offsets_erase_removes_the_promise_entirely() {
let mut o = offsets(&[(1, 9), (2, 100)]);
o.erase(2);
assert!(!o.contains(2));
assert_eq!(o.last(), 1);
assert_eq!(
text(|out| classic_full(out, &o, 2)),
"xref\r\n0 2\r\n0000000000 65535 f\r\n0000000009 00000 n\r\n"
);
}
#[test]
fn a_huge_offset_saturates_rather_than_wrapping() {
let mut out = Vec::new();
stream_record(&mut out, u64::from(u32::MAX) + 1);
assert_eq!(out, vec![0xFF, 0xFF, 0xFF, 0xFF, 0x00]);
}
}