use vole_document::ErrorClass;
use vole_document::adapter::pdf::propose_pdf_layout;
use vole_document::adapter::pdf::samples::sample_pdfs;
use vole_document::container::{Descriptor, ObjectSource};
use vole_document::dra::Op;
use vole_document::dra::op::PackItem;
use vole_document::encode;
use vole_document::encode::candidates::{Candidate, CandidateKind};
use vole_document::integrity::sha256;
use vole_document::limits::Limits;
use vole_document::materialize;
fn corpus(name: &str) -> Vec<u8> {
sample_pdfs()
.into_iter()
.find(|(n, _)| *n == name)
.unwrap_or_else(|| panic!("sample {name} is missing from the corpus"))
.1
}
fn accepted_samples() -> Vec<(&'static str, Vec<u8>)> {
vec![
("classic.pdf", corpus("classic.pdf")),
("twopage.pdf", twopage_pdf()),
("incremental.pdf", corpus("incremental.pdf")),
("bigtext.pdf", corpus("bigtext.pdf")),
]
}
fn push_obj(b: &mut Vec<u8>, offsets: &mut Vec<u64>, number: u64, body: &[u8]) {
offsets.push(b.len() as u64);
b.extend_from_slice(format!("{number} 0 obj\n").as_bytes());
b.extend_from_slice(body);
b.extend_from_slice(b"\nendobj\n");
}
fn push_stream_obj(b: &mut Vec<u8>, offsets: &mut Vec<u64>, number: u64, data: &[u8]) {
offsets.push(b.len() as u64);
b.extend_from_slice(
format!("{number} 0 obj\n<< /Length {} >>\nstream\n", data.len()).as_bytes(),
);
b.extend_from_slice(data);
b.extend_from_slice(b"\nendstream\nendobj\n");
}
fn twopage_pdf() -> Vec<u8> {
let mut b: Vec<u8> = Vec::new();
let mut offsets: Vec<u64> = Vec::new();
b.extend_from_slice(b"%PDF-1.4\n");
push_obj(
&mut b,
&mut offsets,
1,
b"<< /Type /Catalog /Pages 2 0 R >>",
);
push_obj(
&mut b,
&mut offsets,
2,
b"<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>",
);
push_obj(
&mut b,
&mut offsets,
3,
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 5 0 R >>",
);
push_obj(
&mut b,
&mut offsets,
4,
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 6 0 R >>",
);
push_stream_obj(&mut b, &mut offsets, 5, b"page one\n");
push_stream_obj(&mut b, &mut offsets, 6, b"page two\n");
let xref = b.len() as u64;
b.extend_from_slice(b"xref\n0 7\n");
b.extend_from_slice(b"0000000000 65535 f \n");
for &off in &offsets {
b.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
}
b.extend_from_slice(
format!("trailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n").as_bytes(),
);
b
}
fn classic_with_objects(n: usize) -> Vec<u8> {
let mut b: Vec<u8> = Vec::new();
b.extend_from_slice(b"%PDF-1.4\n");
let mut offsets: Vec<u64> = Vec::with_capacity(n);
for number in 1..=n {
offsets.push(b.len() as u64);
b.extend_from_slice(format!("{number} 0 obj\n<< >>\nendobj\n").as_bytes());
}
let xref = b.len() as u64;
b.extend_from_slice(format!("xref\n0 {}\n", n + 1).as_bytes());
b.extend_from_slice(b"0000000000 65535 f \n");
for &off in &offsets {
b.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
}
b.extend_from_slice(
format!(
"trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n",
n + 1
)
.as_bytes(),
);
b
}
fn classic_with_wrong_offset() -> (Vec<u8>, String) {
let mut b: Vec<u8> = Vec::new();
b.extend_from_slice(b"%PDF-1.4\n");
let off1 = b.len() as u64;
b.extend_from_slice(b"1 0 obj\n<< /Type /Catalog >>\nendobj\n");
let xref = b.len() as u64;
let wrong = format!("{:010}", off1 + 3);
b.extend_from_slice(format!("xref\n0 2\n0000000000 65535 f \n{wrong} 00000 n \n").as_bytes());
b.extend_from_slice(
format!("trailer\n<< /Size 2 /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n").as_bytes(),
);
(b, wrong)
}
fn forced_layout(src: &[u8]) -> Candidate {
propose_pdf_layout(src, Limits::DEFAULT)
.unwrap_or_else(|e| panic!("propose_pdf_layout failed: {e}"))
.unwrap_or_else(|| panic!("layout must be proposed for this input"))
}
fn layout_items(cand: &Candidate) -> &[PackItem] {
cand.descriptor
.program
.ops
.iter()
.find_map(|op| match op {
Op::PackSegments { items, .. } => Some(items.as_slice()),
_ => None,
})
.expect("layout program is one PackSegments op")
}
fn assert_exact(encoded: &[u8], src: &[u8], label: &str) {
let (out, parsed) = materialize::decode_to_bytes(encoded, Limits::DEFAULT)
.unwrap_or_else(|e| panic!("[{label}] decode failed: {e}"));
assert_eq!(out.len(), src.len(), "[{label}] materialized length");
assert_eq!(out, src, "[{label}] materialized bytes");
assert_eq!(sha256(&out), sha256(src), "[{label}] digest");
assert_eq!(out.len() as u64, parsed.descriptor.source_len);
}
fn basis_field(basis: &str, key: &str) -> Option<u64> {
basis.split(';').find_map(|part| {
let (k, v) = part.split_once('=')?;
(k == key).then(|| v.parse().ok()).flatten()
})
}
#[test]
fn forced_layout_is_exact() {
for (name, src) in accepted_samples() {
let cand = forced_layout(&src);
assert_eq!(cand.kind, CandidateKind::PdfLayout, "[{name}] kind");
assert_eq!(
cand.descriptor.objects.len(),
1,
"[{name}] all literal bytes live in one packed data object"
);
assert!(
matches!(
cand.descriptor.program.ops.as_slice(),
[Op::PackSegments { .. }]
),
"[{name}] reconstruction is one packed item table"
);
assert!(cand.descriptor.models.is_empty(), "[{name}] no models");
assert!(cand.descriptor.channels.is_empty(), "[{name}] no channels");
let (encoded, cost) = cand.descriptor.serialize().unwrap();
assert_eq!(
cost.total(),
encoded.len() as u64,
"[{name}] serialize-time cost must sum to the serialized length"
);
assert_exact(&encoded, &src, name);
let parsed = Descriptor::parse(&encoded, Limits::DEFAULT).unwrap();
assert_eq!(
parsed.cost.total(),
encoded.len() as u64,
"[{name}] parsed cost must sum to the serialized length"
);
assert_eq!(parsed.descriptor.source_len, src.len() as u64);
}
}
#[test]
fn layout_predicts_entries() {
let src = corpus("classic.pdf");
let cand = forced_layout(&src);
let items = layout_items(&cand);
let emits = items
.iter()
.filter(|item| matches!(item, PackItem::Emit { .. }))
.count();
let marks = items
.iter()
.filter(|item| matches!(item, PackItem::Mark { .. }))
.count();
assert!(emits >= 1, "classic.pdf must emit at least one offset");
assert!(marks >= 1, "classic.pdf must mark at least one position");
assert!(
basis_field(&cand.descriptor.format_basis, "xref_predicted").unwrap_or(0) >= 1,
"the format basis must record at least one predicted xref entry"
);
}
#[test]
fn layout_bad_offset_falls_back() {
let (src, wrong) = classic_with_wrong_offset();
let cand = forced_layout(&src);
assert_eq!(
basis_field(&cand.descriptor.format_basis, "xref_predicted"),
Some(0),
"a mismatched offset must never be predicted"
);
let data = cand.descriptor.objects[0]
.as_inline()
.expect("the layout object is inline");
assert!(
data.windows(wrong.len()).any(|w| w == wrong.as_bytes()),
"the wrong offset must be stored literally"
);
let (encoded, _) = cand.descriptor.serialize().unwrap();
assert_exact(&encoded, &src, "bad-offset");
}
#[test]
fn layout_declines() {
for name in ["xrefstream.pdf", "notpdf.bin", "malformed.pdf"] {
let src = corpus(name);
assert!(
propose_pdf_layout(&src, Limits::DEFAULT).unwrap().is_none(),
"{name} must decline the layout candidate"
);
}
let too_many = classic_with_objects(256);
assert!(
propose_pdf_layout(&too_many, Limits::DEFAULT)
.unwrap()
.is_none(),
"256 objects exceeds the 255 markable slots"
);
let boundary = classic_with_objects(255);
assert!(
propose_pdf_layout(&boundary, Limits::DEFAULT)
.unwrap()
.is_some(),
"255 objects must still be markable"
);
}
#[test]
fn layout_not_selected_on_corpus() {
let src = corpus("classic.pdf");
assert!(
propose_pdf_layout(&src, Limits::DEFAULT).unwrap().is_some(),
"the layout lane must really be on the table for this loss to be honest"
);
let (encoded, report) = encode::encode(&src, Limits::DEFAULT).unwrap();
assert_exact(&encoded, &src, "classic-court");
assert_ne!(
report.kind,
CandidateKind::PdfLayout,
"PDF_LAYOUT must lose the complete-cost court on classic.pdf"
);
assert_eq!(report.encoded_len, encoded.len() as u64);
assert_eq!(report.cost.total(), encoded.len() as u64);
eprintln!(
"court[classic.pdf]: winner={} source={} encoded={}",
report.kind.name(),
report.source_len,
report.encoded_len
);
}
#[test]
fn layout_deterministic() {
for (name, src) in accepted_samples() {
let a = forced_layout(&src).descriptor.serialize().unwrap().0;
let b = forced_layout(&src).descriptor.serialize().unwrap().0;
assert_eq!(a, b, "{name} layout bytes must be deterministic");
}
}
#[test]
fn layout_hostile() {
let src = corpus("classic.pdf");
let (encoded, _) = forced_layout(&src).descriptor.serialize().unwrap();
assert_exact(&encoded, &src, "hostile-baseline");
let mut corrupted = encoded.clone();
let mid = corrupted.len() / 2;
corrupted[mid] ^= 0xFF;
match materialize::decode_to_bytes(&corrupted, Limits::DEFAULT) {
Ok((out, _)) => assert_ne!(
out, src,
"a corrupted descriptor must not silently reconstruct the source"
),
Err(e) => assert_ne!(
e.class(),
ErrorClass::InternalInvariant,
"hostile input must fail with a typed class, not an internal invariant"
),
}
let mut parsed = Descriptor::parse(&encoded, Limits::DEFAULT).unwrap();
let victim = parsed
.descriptor
.objects
.iter_mut()
.find_map(|obj| match obj {
ObjectSource::Inline(bytes) if !bytes.is_empty() => Some(bytes),
_ => None,
})
.expect("the layout program carries a packed data object");
victim[0] ^= 0xFF;
let (reserialized, _) = parsed.descriptor.serialize().unwrap();
match materialize::decode_to_bytes(&reserialized, Limits::DEFAULT) {
Ok((out, _)) => assert_ne!(
out, src,
"a re-serialized corrupted literal must not silently reconstruct"
),
Err(e) => assert_ne!(
e.class(),
ErrorClass::InternalInvariant,
"a CRC-passing corruption must still fail with a typed class"
),
}
}