use super::{DocumentSecurityStore, VriEntry};
use crate::document::PdfDocument;
use crate::error::Result;
use crate::object::{Object, ObjectRef};
type Resolver<'a> = dyn Fn(ObjectRef) -> Option<Object> + 'a;
fn deref(o: &Object, resolve: &Resolver) -> Option<Object> {
match o.as_reference() {
Some(r) => resolve(r),
None => Some(o.clone()),
}
}
fn stream_array(arr_owner: Option<&Object>, resolve: &Resolver) -> Vec<Vec<u8>> {
let mut out = Vec::new();
let Some(arr) = arr_owner.and_then(|o| o.as_array()) else {
return out;
};
for item in arr {
if let Some(obj) = deref(item, resolve) {
if let Ok(bytes) = obj.decode_stream_data() {
if !bytes.is_empty() {
out.push(bytes);
}
}
}
}
out
}
pub fn parse_dss(dss: &Object, resolve: &Resolver) -> DocumentSecurityStore {
let mut store = DocumentSecurityStore::default();
let Some(d) = dss.as_dict() else {
return store;
};
store.certificates = stream_array(d.get("Certs"), resolve);
store.crls = stream_array(d.get("CRLs"), resolve);
store.ocsp_responses = stream_array(d.get("OCSPs"), resolve);
if let Some(vri_obj) = d.get("VRI").and_then(|o| deref(o, resolve)) {
if let Some(vri_dict) = vri_obj.as_dict() {
for (key, val) in vri_dict {
if key == "Type" {
continue;
}
let Some(entry_obj) = deref(val, resolve) else {
continue;
};
let Some(ed) = entry_obj.as_dict() else {
continue;
};
store.vri.push(VriEntry {
signature_digest: key.clone(),
certificates: stream_array(ed.get("Cert"), resolve),
crls: stream_array(ed.get("CRL"), resolve),
ocsp_responses: stream_array(ed.get("OCSP"), resolve),
timestamp: ed
.get("TU")
.and_then(|o| o.as_string())
.map(|b| String::from_utf8_lossy(b).into_owned()),
});
}
}
}
store
}
pub fn read_dss(doc: &PdfDocument) -> Result<Option<DocumentSecurityStore>> {
let catalog = doc.catalog()?;
let Some(cat) = catalog.as_dict() else {
return Ok(None);
};
let Some(dss_ref) = cat.get("DSS") else {
return Ok(None);
};
let resolve = |r: ObjectRef| doc.load_object(r).ok();
let Some(dss) = deref(dss_ref, &resolve) else {
return Ok(None);
};
let store = parse_dss(&dss, &resolve);
if store.is_empty() {
Ok(None)
} else {
Ok(Some(store))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn stream(body: &[u8]) -> Object {
Object::Stream {
dict: HashMap::new(),
data: body.to_vec().into(),
}
}
fn refr(id: u32) -> Object {
Object::Reference(ObjectRef { id, gen: 0 })
}
fn dict(pairs: &[(&str, Object)]) -> Object {
Object::Dictionary(
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect(),
)
}
#[test]
fn parses_doc_level_material_via_indirect_streams() {
let mut objs: HashMap<u32, Object> = HashMap::new();
objs.insert(10, stream(b"\x30\x82CERT"));
objs.insert(11, stream(b"\x30\x82CRL"));
objs.insert(12, stream(b"\x30\x82OCSP"));
let resolve = |r: ObjectRef| objs.get(&r.id).cloned();
let dss = dict(&[
("Type", Object::Name("DSS".into())),
("Certs", Object::Array(vec![refr(10)])),
("CRLs", Object::Array(vec![refr(11)])),
("OCSPs", Object::Array(vec![refr(12)])),
]);
let s = parse_dss(&dss, &resolve);
assert_eq!(s.certificates, vec![b"\x30\x82CERT".to_vec()]);
assert_eq!(s.crls, vec![b"\x30\x82CRL".to_vec()]);
assert_eq!(s.ocsp_responses, vec![b"\x30\x82OCSP".to_vec()]);
assert!(s.vri.is_empty());
assert!(!s.is_empty());
}
#[test]
fn parses_vri_keyed_by_signature_digest() {
let mut objs: HashMap<u32, Object> = HashMap::new();
objs.insert(20, stream(b"VRICERT"));
let resolve = |r: ObjectRef| objs.get(&r.id).cloned();
let vri = dict(&[
("Type", Object::Name("VRI".into())), (
"ABCDEF0123456789",
dict(&[
("Type", Object::Name("VRI".into())),
("Cert", Object::Array(vec![refr(20)])),
("TU", Object::String(b"D:20260516120000Z".to_vec())),
]),
),
]);
let dss = dict(&[("Type", Object::Name("DSS".into())), ("VRI", vri)]);
let s = parse_dss(&dss, &resolve);
assert_eq!(s.vri.len(), 1, "the /Type key must not become a VRI entry");
let e = s.vri_for("ABCDEF0123456789").expect("VRI entry by key");
assert_eq!(e.certificates, vec![b"VRICERT".to_vec()]);
assert_eq!(e.timestamp.as_deref(), Some("D:20260516120000Z"));
}
#[test]
fn dangling_refs_and_non_streams_are_skipped_not_panic() {
let resolve = |_r: ObjectRef| None; let dss = dict(&[
("Certs", Object::Array(vec![refr(99), Object::Integer(5)])),
("OCSPs", Object::Array(vec![refr(98)])),
]);
let s = parse_dss(&dss, &resolve);
assert!(s.is_empty());
}
#[test]
fn non_dict_dss_is_empty() {
let resolve = |_r: ObjectRef| None;
assert!(parse_dss(&Object::Null, &resolve).is_empty());
assert!(parse_dss(&Object::Integer(7), &resolve).is_empty());
}
#[test]
fn direct_inline_streams_also_work() {
let resolve = |_r: ObjectRef| None;
let dss = dict(&[("Certs", Object::Array(vec![stream(b"INLINE")]))]);
let s = parse_dss(&dss, &resolve);
assert_eq!(s.certificates, vec![b"INLINE".to_vec()]);
}
}