use crate::SOURCE_FORMAT_PDF;
use crate::container::{Descriptor, UNIVERSE};
use crate::dra::{Op, Program};
use crate::encode::candidates::{Candidate, CandidateKind};
use crate::error::Result;
use crate::integrity::sha256;
use crate::limits::Limits;
use super::physical::{PdfPhysical, scan};
pub fn detect(input: &[u8], limits: Limits) -> bool {
match scan(input, limits) {
Ok(physical) => is_validated_pdf(&physical),
Err(_) => false,
}
}
fn is_validated_pdf(physical: &PdfPhysical) -> bool {
physical.header.is_some() && !physical.objects.is_empty() && !physical.eofs.is_empty()
}
pub fn propose_pdf(input: &[u8], limits: Limits) -> Result<Option<Candidate>> {
let physical = match scan(input, limits) {
Ok(p) => p,
Err(_) => return Ok(None),
};
if !is_validated_pdf(&physical) {
return Ok(None);
}
if physical.spans.len() as u64 > limits.max_graph_ops as u64 {
return Ok(None);
}
let ops: Vec<Op> = physical
.spans
.iter()
.map(|span| {
let start = span.start as usize;
let end = start + span.len as usize;
Op::Inline {
bytes: input[start..end].to_vec(),
}
})
.collect();
let format_basis = format!(
"pdf;spans={};objects={};revisions={}",
physical.spans.len(),
physical.objects.len(),
physical.revisions.len()
);
let descriptor = Descriptor {
universe: UNIVERSE.to_string(),
source_format: SOURCE_FORMAT_PDF,
format_basis,
models: vec![],
channels: vec![],
objects: vec![],
program: Program::new(ops),
source_sha256: sha256(input),
source_len: input.len() as u64,
};
Ok(Some(Candidate {
kind: CandidateKind::PdfPhysical,
descriptor,
}))
}
#[cfg(test)]
mod tests {
use super::*;
fn canonical_pdf() -> Vec<u8> {
let mut s = String::new();
s.push_str("%PDF-1.7\n");
s.push_str("1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
s.push_str("2 0 obj\n<< /Length 6 >>\nstream\nhello\nendstream\nendobj\n");
s.push_str("3 0 obj\n<< /Length 7 >>\nstream\nworld\nendstream\nendobj\n");
s.push_str("4 0 obj\n<< /Length 4 >>\nstream\nxyz\nendstream\nendobj\n");
s.push_str("xref\n0 5\n0000000000 65535 f \n0000000010 00000 n \n");
s.push_str("trailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n321\n%%EOF");
s.into_bytes()
}
#[test]
fn detect_accepts_canonical_pdf() {
assert!(detect(&canonical_pdf(), Limits::DEFAULT));
}
#[test]
fn detect_rejects_non_pdf_and_truncated() {
assert!(!detect(b"this is definitely not a PDF", Limits::DEFAULT));
assert!(!detect(b"", Limits::DEFAULT));
assert!(!detect(b"%PDF-1.7\n", Limits::DEFAULT));
assert!(!detect(b"1 0 obj\n<< >>\nendobj\n%%EOF", Limits::DEFAULT));
}
#[test]
fn propose_declines_non_pdf() {
assert!(
propose_pdf(b"not a pdf", Limits::DEFAULT)
.unwrap()
.is_none()
);
assert!(
propose_pdf(b"%PDF-1.7\n", Limits::DEFAULT)
.unwrap()
.is_none()
);
}
#[test]
fn propose_returns_pdf_candidate() {
let pdf = canonical_pdf();
let cand = propose_pdf(&pdf, Limits::DEFAULT).unwrap().unwrap();
assert_eq!(cand.kind, CandidateKind::PdfPhysical);
assert_eq!(cand.descriptor.source_format, SOURCE_FORMAT_PDF);
assert!(cand.descriptor.objects.is_empty());
assert!(cand.descriptor.models.is_empty());
assert!(cand.descriptor.channels.is_empty());
assert_eq!(cand.descriptor.source_len, pdf.len() as u64);
assert_eq!(cand.descriptor.source_sha256, sha256(&pdf));
}
#[test]
fn pdf_candidate_round_trips_exactly() {
let pdf = canonical_pdf();
let cand = propose_pdf(&pdf, Limits::DEFAULT).unwrap().unwrap();
let (bytes, cost) = cand.descriptor.serialize().unwrap();
assert_eq!(cost.total(), bytes.len() as u64);
let (out, parsed) = crate::materialize::decode_to_bytes(&bytes, Limits::DEFAULT).unwrap();
assert_eq!(out.len(), pdf.len());
assert_eq!(out, pdf, "PDF candidate must materialize byte-for-byte");
assert_eq!(sha256(&out), sha256(&pdf));
assert_eq!(parsed.descriptor.source_sha256, sha256(&pdf));
assert_eq!(parsed.descriptor.source_format, SOURCE_FORMAT_PDF);
}
#[test]
fn court_prefers_raw_for_small_pdf() {
let pdf = canonical_pdf();
let (bytes, report) = crate::encode::encode(&pdf, Limits::DEFAULT).unwrap();
let (out, _) = crate::materialize::decode_to_bytes(&bytes, Limits::DEFAULT).unwrap();
assert_eq!(out, pdf);
assert_eq!(report.kind, CandidateKind::Raw);
}
#[test]
fn propose_declines_when_span_count_exceeds_graph_ops() {
let pdf = canonical_pdf();
let limits = Limits {
max_graph_ops: 1,
..Limits::DEFAULT
};
assert!(
propose_pdf(&pdf, limits).unwrap().is_none(),
"a multi-span PDF cannot fit in a one-op graph"
);
}
}