use super::RevocationMaterial;
use crate::document::PdfDocument;
use crate::error::{Error, Result};
use crate::object::{Object, ObjectRef};
use crate::redaction::serialize::serialize_object;
use std::collections::HashMap;
fn scan_last_startxref(pdf: &[u8]) -> Option<u64> {
let tail = &pdf[pdf.len().saturating_sub(2048)..];
let s = String::from_utf8_lossy(tail);
let idx = s.rfind("startxref")?;
s[idx + "startxref".len()..]
.split_whitespace()
.next()?
.parse()
.ok()
}
fn xref_in_use(offset: usize, gen: u16) -> String {
format!("{offset:010} {gen:05} n \r\n")
}
pub fn append_dss(
pdf: &[u8],
doc: &PdfDocument,
material: &RevocationMaterial,
vri: &[String],
) -> Result<Vec<u8>> {
let trailer = doc.trailer();
let tdict = trailer
.as_dict()
.ok_or_else(|| Error::InvalidPdf("DSS: trailer is not a dictionary".into()))?;
let root_ref = tdict
.get("Root")
.and_then(|o| o.as_reference())
.ok_or_else(|| Error::InvalidPdf("DSS: trailer has no /Root reference".into()))?;
let mut next_id = tdict
.get("Size")
.and_then(|o| o.as_integer())
.ok_or_else(|| Error::InvalidPdf("DSS: trailer has no /Size".into()))?
as u32;
let prev_startxref = scan_last_startxref(pdf)
.ok_or_else(|| Error::InvalidPdf("DSS: cannot find existing startxref".into()))?;
let old_catalog = doc.load_object(root_ref)?;
let mut catalog = old_catalog
.as_dict()
.ok_or_else(|| Error::InvalidPdf("DSS: Catalog is not a dictionary".into()))?
.clone();
let mut alloc = || {
let id = next_id;
next_id += 1;
id
};
let mut streams: Vec<(u32, Vec<u8>)> = Vec::new();
let id_array =
|items: &[Vec<u8>], streams: &mut Vec<(u32, Vec<u8>)>, a: &mut dyn FnMut() -> u32| {
let mut refs = Vec::with_capacity(items.len());
for it in items {
let id = a();
streams.push((id, it.clone()));
refs.push(Object::Reference(ObjectRef { id, gen: 0 }));
}
Object::Array(refs)
};
let certs = id_array(&material.certificates, &mut streams, &mut alloc);
let crls = id_array(&material.crls, &mut streams, &mut alloc);
let ocsps = id_array(&material.ocsp_responses, &mut streams, &mut alloc);
let mut vri_dict: HashMap<String, Object> = HashMap::new();
vri_dict.insert("Type".into(), Object::Name("VRI".into()));
for key in vri {
let mut entry: HashMap<String, Object> = HashMap::new();
entry.insert("Type".into(), Object::Name("VRI".into()));
if let Object::Array(a) = &certs {
if !a.is_empty() {
entry.insert("Cert".into(), certs.clone());
}
}
if let Object::Array(a) = &crls {
if !a.is_empty() {
entry.insert("CRL".into(), crls.clone());
}
}
if let Object::Array(a) = &ocsps {
if !a.is_empty() {
entry.insert("OCSP".into(), ocsps.clone());
}
}
vri_dict.insert(key.clone(), Object::Dictionary(entry));
}
let dss_id = alloc();
let mut dss: HashMap<String, Object> = HashMap::new();
dss.insert("Type".into(), Object::Name("DSS".into()));
if let Object::Array(a) = &certs {
if !a.is_empty() {
dss.insert("Certs".into(), certs);
}
}
if let Object::Array(a) = &crls {
if !a.is_empty() {
dss.insert("CRLs".into(), crls);
}
}
if let Object::Array(a) = &ocsps {
if !a.is_empty() {
dss.insert("OCSPs".into(), ocsps);
}
}
if vri_dict.len() > 1 {
dss.insert("VRI".into(), Object::Dictionary(vri_dict));
}
catalog.insert("DSS".into(), Object::Reference(ObjectRef { id: dss_id, gen: 0 }));
let mut out = Vec::with_capacity(pdf.len() + 2048);
out.extend_from_slice(pdf);
if !out.ends_with(b"\n") {
out.push(b'\n');
}
let mut written: Vec<(u32, u16, usize)> = Vec::new();
for (id, body) in &streams {
let off = out.len();
out.extend_from_slice(
format!("{id} 0 obj\n<< /Length {} >>\nstream\n", body.len()).as_bytes(),
);
out.extend_from_slice(body);
out.extend_from_slice(b"\nendstream\nendobj\n");
written.push((*id, 0, off));
}
let off = out.len();
out.extend_from_slice(format!("{dss_id} 0 obj\n").as_bytes());
serialize_object(&mut out, &Object::Dictionary(dss));
out.extend_from_slice(b"\nendobj\n");
written.push((dss_id, 0, off));
let off = out.len();
out.extend_from_slice(format!("{} {} obj\n", root_ref.id, root_ref.gen).as_bytes());
serialize_object(&mut out, &Object::Dictionary(catalog));
out.extend_from_slice(b"\nendobj\n");
written.push((root_ref.id, root_ref.gen, off));
written.sort_by_key(|(id, _, _)| *id);
let xref_offset = out.len();
out.extend_from_slice(b"xref\n");
let mut i = 0;
while i < written.len() {
let start = written[i].0;
let mut j = i;
while j + 1 < written.len() && written[j + 1].0 == written[j].0 + 1 {
j += 1;
}
out.extend_from_slice(format!("{} {}\n", start, j - i + 1).as_bytes());
for &(_, gen, offset) in &written[i..=j] {
out.extend_from_slice(xref_in_use(offset, gen).as_bytes());
}
i = j + 1;
}
out.extend_from_slice(
format!(
"trailer\n<< /Size {} /Prev {} /Root {} {} R >>\n",
next_id, prev_startxref, root_ref.id, root_ref.gen
)
.as_bytes(),
);
out.extend_from_slice(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn minimal_pdf() -> Vec<u8> {
let mut p = Vec::new();
p.extend_from_slice(b"%PDF-1.7\n");
let mut offs = [0usize; 4];
offs[1] = p.len();
p.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
offs[2] = p.len();
p.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n");
offs[3] = p.len();
p.extend_from_slice(
b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n",
);
let xref = p.len();
p.extend_from_slice(b"xref\n0 4\n0000000000 65535 f \n");
for o in offs.iter().skip(1) {
p.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
}
p.extend_from_slice(
format!("trailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n").as_bytes(),
);
p
}
#[test]
fn i2_pre_is_strict_byte_prefix_of_post() {
let pre = minimal_pdf();
let doc = PdfDocument::from_bytes(pre.clone()).expect("parse minimal pdf");
let material = RevocationMaterial {
certificates: vec![b"\x30\x03\x02\x01\x2A".to_vec()],
..RevocationMaterial::default()
};
let post = append_dss(&pre, &doc, &material, &["ABCD".to_string()]).expect("append dss");
assert!(post.len() > pre.len());
assert_eq!(&post[..pre.len()], &pre[..], "pre must be a byte prefix");
}
#[test]
fn i3_reparse_resolves_new_catalog_with_dss() {
let pre = minimal_pdf();
let doc = PdfDocument::from_bytes(pre.clone()).unwrap();
let material = RevocationMaterial {
certificates: vec![b"CERTA".to_vec(), b"CERTB".to_vec()],
ocsp_responses: vec![b"OCSP1".to_vec()],
..RevocationMaterial::default()
};
let post = append_dss(&pre, &doc, &material, &["DEADBEEF".to_string()]).unwrap();
let doc2 = PdfDocument::from_bytes(post).expect("re-parse post-DSS");
let dss = super::super::read_dss(&doc2)
.expect("read_dss ok")
.expect("DSS present after append");
assert_eq!(dss.certificates, vec![b"CERTA".to_vec(), b"CERTB".to_vec()]);
assert_eq!(dss.ocsp_responses, vec![b"OCSP1".to_vec()]);
assert!(dss.crls.is_empty());
assert_eq!(
dss.vri_for("DEADBEEF").map(|e| e.certificates.len()),
Some(2),
"VRI entry present and references the shared cert streams"
);
}
#[test]
fn empty_material_still_appends_a_valid_dss() {
let pre = minimal_pdf();
let doc = PdfDocument::from_bytes(pre.clone()).unwrap();
let post = append_dss(&pre, &doc, &RevocationMaterial::default(), &[]).unwrap();
assert_eq!(&post[..pre.len()], &pre[..]);
let doc2 = PdfDocument::from_bytes(post).unwrap();
assert!(super::super::read_dss(&doc2).is_ok());
}
}