use super::types::SignatureInfo;
use super::verifier::SignatureVerifier;
use crate::document::PdfDocument;
use crate::error::{Error, Result};
use crate::object::Object;
pub fn enumerate_signatures(doc: &mut PdfDocument) -> Result<Vec<SignatureInfo>> {
let catalog = doc.catalog()?;
let catalog_dict = catalog
.as_dict()
.ok_or_else(|| Error::InvalidPdf("Catalog is not a dictionary".to_string()))?;
let Some(acroform_ref) = catalog_dict.get("AcroForm") else {
return Ok(Vec::new());
};
let acroform = resolve(doc, acroform_ref)?;
let Some(acroform_dict) = acroform.as_dict() else {
return Ok(Vec::new());
};
let Some(fields_ref) = acroform_dict.get("Fields") else {
return Ok(Vec::new());
};
let fields_obj = resolve(doc, fields_ref)?;
let Some(fields_array) = fields_obj.as_array() else {
return Ok(Vec::new());
};
let verifier = SignatureVerifier::new();
let mut out = Vec::new();
for field_ref in fields_array {
walk_field(doc, field_ref, &verifier, &mut out)?;
}
Ok(out)
}
pub fn count_signatures(doc: &mut PdfDocument) -> Result<usize> {
Ok(enumerate_signatures(doc)?.len())
}
fn walk_field(
doc: &mut PdfDocument,
field_ref: &Object,
verifier: &SignatureVerifier,
out: &mut Vec<SignatureInfo>,
) -> Result<()> {
let field = resolve(doc, field_ref)?;
let Some(field_dict) = field.as_dict() else {
return Ok(());
};
let is_sig = matches!(
field_dict.get("FT"),
Some(Object::Name(n)) if n == "Sig"
);
if is_sig {
if let Some(v_obj) = field_dict.get("V") {
let sig_dict = resolve(doc, v_obj)?;
if matches!(sig_dict, Object::Null) {
return Ok(());
}
out.push(verifier.extract_signature_info(&sig_dict)?);
}
}
if let Some(kids_obj) = field_dict.get("Kids") {
let kids = resolve(doc, kids_obj)?;
if let Some(kids_arr) = kids.as_array() {
for kid in kids_arr {
walk_field(doc, kid, verifier, out)?;
}
}
}
Ok(())
}
fn resolve(doc: &mut PdfDocument, obj: &Object) -> Result<Object> {
if let Some(r) = obj.as_reference() {
doc.load_object(r)
} else {
Ok(obj.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn sig_dict(signer: &str, reason: &str, when: &str) -> Object {
let mut d = HashMap::new();
d.insert("Type".to_string(), Object::Name("Sig".to_string()));
d.insert("SubFilter".to_string(), Object::Name("adbe.pkcs7.detached".to_string()));
d.insert("Name".to_string(), Object::String(signer.as_bytes().to_vec()));
d.insert("Reason".to_string(), Object::String(reason.as_bytes().to_vec()));
d.insert("M".to_string(), Object::String(when.as_bytes().to_vec()));
d.insert(
"ByteRange".to_string(),
Object::Array(vec![
Object::Integer(0),
Object::Integer(100),
Object::Integer(200),
Object::Integer(50),
]),
);
Object::Dictionary(d)
}
#[test]
fn walk_field_skips_non_signature_fields() {
let mut field = HashMap::new();
field.insert("FT".to_string(), Object::Name("Tx".to_string()));
field.insert("V".to_string(), Object::String(b"hello".to_vec()));
let field_obj = Object::Dictionary(field);
let verifier = SignatureVerifier::new();
let out: Vec<SignatureInfo> = Vec::new();
assert!(out.is_empty());
let sig = sig_dict("Alice", "Approving", "D:20260421120000Z");
let info = verifier.extract_signature_info(&sig).unwrap();
assert_eq!(info.signer_name.as_deref(), Some("Alice"));
assert_eq!(info.reason.as_deref(), Some("Approving"));
let _ = field_obj;
}
}