use crate::error::FormeError;
use crate::model::CertificationConfig;
use der::Encode;
use pkcs8::{DecodePrivateKey, EncodePublicKey};
use rsa::pkcs1v15::SigningKey;
use rsa::RsaPrivateKey;
use sha2::{Digest, Sha256};
use signature::{SignatureEncoding, SignerMut};
use x509_cert::Certificate;
const SIG_PLACEHOLDER_HEX_LEN: usize = 8192;
struct PdfScanResult {
startxref_offset: usize,
size: usize,
root_obj: usize,
first_page_obj: usize,
}
pub fn certify_pdf(pdf_bytes: &[u8], config: &CertificationConfig) -> Result<Vec<u8>, FormeError> {
let cert = parse_pem_certificate(&config.certificate_pem)?;
let private_key = parse_pem_private_key(&config.private_key_pem)?;
let cert_pub_key_der = cert
.tbs_certificate
.subject_public_key_info
.to_der()
.map_err(|e| FormeError::RenderError(format!("Failed to encode cert public key: {e}")))?;
let key_pub_der = rsa::RsaPublicKey::from(&private_key)
.to_public_key_der()
.map_err(|e| FormeError::RenderError(format!("Failed to encode public key: {e}")))?;
if cert_pub_key_der != key_pub_der.as_bytes() {
return Err(FormeError::RenderError(
"Certificate and private key do not match".to_string(),
));
}
let scan = scan_pdf_metadata(pdf_bytes)?;
let cert_der = cert
.to_der()
.map_err(|e| FormeError::RenderError(format!("Failed to DER-encode certificate: {e}")))?;
let (mut output, placeholder_offset) =
build_incremental_update(pdf_bytes, &scan, config, &cert_der)?;
let before_sig_hex = placeholder_offset; let after_sig_hex = placeholder_offset + 1 + SIG_PLACEHOLDER_HEX_LEN + 1; let total_len = output.len();
update_byte_range(&mut output, before_sig_hex, after_sig_hex, total_len)?;
let mut signed_data = Vec::with_capacity(before_sig_hex + (total_len - after_sig_hex));
signed_data.extend_from_slice(&output[0..before_sig_hex]);
signed_data.extend_from_slice(&output[after_sig_hex..total_len]);
let mut signing_key = SigningKey::<Sha256>::new(private_key);
let sig_result: rsa::pkcs1v15::Signature = signing_key.sign(&signed_data);
let sig_bytes = sig_result.to_bytes();
let hash = Sha256::digest(&signed_data);
let pkcs7_der = build_pkcs7_signed_data(&cert_der, &sig_bytes, &hash)?;
let hex_sig = hex_encode(&pkcs7_der);
if hex_sig.len() > SIG_PLACEHOLDER_HEX_LEN {
return Err(FormeError::RenderError(format!(
"PKCS#7 signature ({} hex chars) exceeds placeholder size ({})",
hex_sig.len(),
SIG_PLACEHOLDER_HEX_LEN
)));
}
let sig_start = placeholder_offset + 1; for (i, b) in hex_sig.bytes().enumerate() {
output[sig_start + i] = b;
}
Ok(output)
}
fn parse_pem_certificate(pem: &str) -> Result<Certificate, FormeError> {
use der::DecodePem;
Certificate::from_pem(pem)
.map_err(|e| FormeError::RenderError(format!("Failed to parse PEM certificate: {e}")))
}
fn parse_pem_private_key(pem: &str) -> Result<RsaPrivateKey, FormeError> {
use rsa::pkcs1::DecodeRsaPrivateKey;
match RsaPrivateKey::from_pkcs8_pem(pem) {
Ok(key) => Ok(key),
Err(pkcs8_err) => {
if pem.contains("BEGIN RSA PRIVATE KEY") {
return RsaPrivateKey::from_pkcs1_pem(pem).map_err(|e| {
FormeError::RenderError(format!(
"Failed to parse PKCS#1 (RSA) private key: {e}"
))
});
}
let msg = pkcs8_err.to_string();
if msg.contains("algorithm") || msg.contains("OID") {
return Err(FormeError::RenderError(
"Only RSA private keys are supported for PDF signing. \
ECDSA, Ed25519, and other key types are not supported."
.to_string(),
));
}
Err(FormeError::RenderError(format!(
"Failed to parse PEM private key: {pkcs8_err}"
)))
}
}
}
fn scan_pdf_metadata(pdf: &[u8]) -> Result<PdfScanResult, FormeError> {
let startxref_pos = rfind_bytes(pdf, b"startxref")
.ok_or_else(|| FormeError::RenderError("No startxref found in PDF".to_string()))?;
let after_startxref = &pdf[startxref_pos + 9..];
let startxref_offset: usize = parse_number_from_bytes(after_startxref)
.ok_or_else(|| FormeError::RenderError("Cannot parse startxref value".to_string()))?;
let trailer_pos = rfind_bytes(pdf, b"trailer")
.ok_or_else(|| FormeError::RenderError("No trailer found in PDF".to_string()))?;
let trailer_section = &pdf[trailer_pos..startxref_pos];
let size = find_value_in_bytes(trailer_section, b"/Size")
.ok_or_else(|| FormeError::RenderError("No /Size found in trailer".to_string()))?;
let root_obj = find_ref_in_bytes(trailer_section, b"/Root")
.ok_or_else(|| FormeError::RenderError("No /Root found in trailer".to_string()))?;
let text = String::from_utf8_lossy(pdf);
let first_page_obj = find_first_page_obj(&text)
.ok_or_else(|| FormeError::RenderError("No /Type /Page found in PDF".to_string()))?;
Ok(PdfScanResult {
startxref_offset,
size,
root_obj,
first_page_obj,
})
}
fn rfind_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.len() > haystack.len() {
return None;
}
for i in (0..=haystack.len() - needle.len()).rev() {
if haystack[i..i + needle.len()] == *needle {
return Some(i);
}
}
None
}
fn parse_number_from_bytes(bytes: &[u8]) -> Option<usize> {
let start = bytes.iter().position(|&b| b.is_ascii_digit())?;
let end = bytes[start..]
.iter()
.position(|b| !b.is_ascii_digit())
.map(|p| start + p)
.unwrap_or(bytes.len());
std::str::from_utf8(&bytes[start..end]).ok()?.parse().ok()
}
fn find_value_in_bytes(section: &[u8], key: &[u8]) -> Option<usize> {
let pos = find_bytes(section, key)?;
parse_number_from_bytes(§ion[pos + key.len()..])
}
fn find_ref_in_bytes(section: &[u8], key: &[u8]) -> Option<usize> {
let pos = find_bytes(section, key)?;
parse_number_from_bytes(§ion[pos + key.len()..])
}
fn find_first_page_obj(text: &str) -> Option<usize> {
let mut search_from = 0;
while let Some(pos) = text[search_from..].find("/Type /Page") {
let abs_pos = search_from + pos;
let after = &text[abs_pos + 11..];
if after.starts_with('s') || after.starts_with('S') {
search_from = abs_pos + 11;
continue;
}
let before = &text[..abs_pos];
if let Some(obj_pos) = before.rfind(" 0 obj") {
let line_start = before[..obj_pos].rfind('\n').map(|p| p + 1).unwrap_or(0);
let obj_num_str = text[line_start..obj_pos].trim();
if let Ok(obj_num) = obj_num_str.parse::<usize>() {
return Some(obj_num);
}
}
search_from = abs_pos + 11;
}
None
}
fn build_incremental_update(
original: &[u8],
scan: &PdfScanResult,
config: &CertificationConfig,
cert_der: &[u8],
) -> Result<(Vec<u8>, usize), FormeError> {
let mut buf = Vec::from(original);
if !buf.ends_with(b"\n") {
buf.push(b'\n');
}
let next_id = scan.size;
let sig_dict_id = next_id;
let sig_field_id = next_id + 1;
let new_catalog_id = next_id + 2;
let ap_xobj_id = if config.visible {
Some(next_id + 3)
} else {
None
};
let new_size = next_id + if config.visible { 4 } else { 3 };
let mut xref_entries: Vec<(usize, usize)> = Vec::new();
xref_entries.push((sig_dict_id, buf.len()));
let date_str = format_pdf_date();
let byte_range_placeholder = "/ByteRange [0 0000000000 0000000000 0000000000]";
let mut sig_dict = format!(
"{sig_dict_id} 0 obj\n<<\n/Type /Sig\n/Filter /Adobe.PPKLite\n/SubFilter /adbe.pkcs7.detached\n{byte_range_placeholder}\n/M ({date_str})\n"
);
if let Some(ref reason) = config.reason {
sig_dict.push_str(&format!("/Reason ({})\n", escape_pdf_string(reason)));
}
if let Some(ref location) = config.location {
sig_dict.push_str(&format!("/Location ({})\n", escape_pdf_string(location)));
}
if let Some(ref contact) = config.contact {
sig_dict.push_str(&format!("/ContactInfo ({})\n", escape_pdf_string(contact)));
}
let cert_hex = hex_encode(cert_der);
sig_dict.push_str(&format!("/Cert <{cert_hex}>\n"));
sig_dict.push_str("/Contents <");
buf.extend_from_slice(sig_dict.as_bytes());
let placeholder_offset = buf.len() - 1;
buf.extend(std::iter::repeat_n(b'0', SIG_PLACEHOLDER_HEX_LEN));
buf.extend_from_slice(b">\n>>\nendobj\n");
if let Some(ap_id) = ap_xobj_id {
xref_entries.push((ap_id, buf.len()));
let w = config.width.unwrap_or(200.0);
let h = config.height.unwrap_or(50.0);
let signer_name =
extract_cn_from_cert_der(cert_der).unwrap_or_else(|| "Unknown".to_string());
let date_display = format_display_date();
let mut content = String::new();
let font_size = 9.0_f64;
let line_height = font_size + 3.0;
let margin = 4.0_f64;
let mut y_pos = h - margin - font_size;
content.push_str(&format!(
"BT /Helv {font_size:.1} Tf {margin:.2} {y_pos:.2} Td (Digitally signed by) Tj ET\n"
));
y_pos -= line_height;
content.push_str(&format!(
"BT /Helv {font_size:.1} Tf {margin:.2} {y_pos:.2} Td ({}) Tj ET\n",
escape_pdf_string(&signer_name)
));
y_pos -= line_height;
content.push_str(&format!(
"BT /Helv {font_size:.1} Tf {margin:.2} {y_pos:.2} Td (Date: {date_display}) Tj ET\n"
));
y_pos -= line_height;
if let Some(ref reason) = config.reason {
content.push_str(&format!(
"BT /Helv {font_size:.1} Tf {margin:.2} {y_pos:.2} Td (Reason: {}) Tj ET\n",
escape_pdf_string(reason)
));
let _ = y_pos; }
let _ = y_pos;
let content_bytes = content.as_bytes();
let ap_obj = format!(
"{ap_id} 0 obj\n<<\n/Type /XObject\n/Subtype /Form\n/BBox [0 0 {w:.2} {h:.2}]\n/Resources << /Font << /Helv << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >> >>\n/Length {}\n>>\nstream\n",
content_bytes.len()
);
buf.extend_from_slice(ap_obj.as_bytes());
buf.extend_from_slice(content_bytes);
buf.extend_from_slice(b"\nendstream\nendobj\n");
}
xref_entries.push((sig_field_id, buf.len()));
let sig_name = next_signature_name(original);
let rect = if config.visible {
let x = config.x.unwrap_or(0.0);
let y = config.y.unwrap_or(0.0);
let w = config.width.unwrap_or(200.0);
let h = config.height.unwrap_or(50.0);
format!("[{x:.2} {y:.2} {:.2} {:.2}]", x + w, y + h)
} else {
"[0 0 0 0]".to_string()
};
let ap_entry = if let Some(ap_id) = ap_xobj_id {
format!("/AP << /N {ap_id} 0 R >>\n")
} else {
String::new()
};
let sig_field = format!(
"{sig_field_id} 0 obj\n<<\n/Type /Annot\n/Subtype /Widget\n/FT /Sig\n/T ({sig_name})\n/V {sig_dict_id} 0 R\n/Rect {rect}\n/P {page_ref} 0 R\n/F 132\n{ap_entry}>>\nendobj\n",
page_ref = scan.first_page_obj
);
buf.extend_from_slice(sig_field.as_bytes());
xref_entries.push((new_catalog_id, buf.len()));
let original_lossy = String::from_utf8_lossy(original);
let original_text: &str = &original_lossy;
let pages_ref = find_catalog_pages_ref(original_text, scan.root_obj).unwrap_or(2);
let existing_fields = find_existing_acroform_fields(original, scan.root_obj);
let all_fields = if existing_fields.is_empty() {
format!("{sig_field_id} 0 R")
} else {
let mut fields = existing_fields.join(" ");
fields.push(' ');
fields.push_str(&format!("{sig_field_id} 0 R"));
fields
};
let acroform_meta = find_existing_acroform_metadata(original, scan.root_obj);
let mut acroform_entries = format!("/Fields [{all_fields}] /SigFlags 3");
if acroform_meta.need_appearances {
acroform_entries.push_str(" /NeedAppearances true");
}
if let Some(ref da) = acroform_meta.da {
acroform_entries.push_str(&format!(" /DA ({})", escape_pdf_string(da)));
}
if config.visible {
acroform_entries.push_str(
" /DR << /Font << /Helv << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >> >>",
);
}
let mut catalog = format!(
"{new_catalog_id} 0 obj\n<<\n/Type /Catalog\n/Pages {pages_ref} 0 R\n/AcroForm << {acroform_entries} >>\n"
);
if let Some(lang) = find_catalog_string(original_text, scan.root_obj, "/Lang") {
catalog.push_str(&format!("/Lang ({lang})\n"));
}
if catalog_has_key(original_text, scan.root_obj, "/MarkInfo") {
catalog.push_str("/MarkInfo << /Marked true >>\n");
}
if let Some(struct_ref) = find_catalog_ref(original_text, scan.root_obj, "/StructTreeRoot") {
catalog.push_str(&format!("/StructTreeRoot {struct_ref} 0 R\n"));
}
if let Some(meta_ref) = find_catalog_ref(original_text, scan.root_obj, "/Metadata") {
catalog.push_str(&format!("/Metadata {meta_ref} 0 R\n"));
}
if let Some(names_ref) = find_catalog_ref(original_text, scan.root_obj, "/Names") {
catalog.push_str(&format!("/Names {names_ref} 0 R\n"));
}
if let Some(vp_ref) = find_catalog_ref(original_text, scan.root_obj, "/ViewerPreferences") {
catalog.push_str(&format!("/ViewerPreferences {vp_ref} 0 R\n"));
}
if let Some(oi_content) =
find_catalog_array_content(original_text, scan.root_obj, "/OutputIntents")
{
catalog.push_str(&format!("/OutputIntents {oi_content}\n"));
}
catalog.push_str(">>\nendobj\n");
buf.extend_from_slice(catalog.as_bytes());
let xref_offset = buf.len();
buf.extend_from_slice(b"xref\n");
let mut sorted_entries = xref_entries.clone();
sorted_entries.sort_by_key(|(id, _)| *id);
let mut i = 0;
while i < sorted_entries.len() {
let start_id = sorted_entries[i].0;
let mut count = 1;
while i + count < sorted_entries.len() && sorted_entries[i + count].0 == start_id + count {
count += 1;
}
buf.extend_from_slice(format!("{start_id} {count}\n").as_bytes());
for j in 0..count {
let offset = sorted_entries[i + j].1;
buf.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
}
i += count;
}
buf.extend_from_slice(
format!(
"trailer\n<<\n/Size {new_size}\n/Root {new_catalog_id} 0 R\n/Prev {prev}\n>>\nstartxref\n{xref_offset}\n%%EOF\n",
prev = scan.startxref_offset
)
.as_bytes(),
);
Ok((buf, placeholder_offset))
}
fn update_byte_range(
buf: &mut [u8],
before_sig: usize,
after_sig: usize,
total_len: usize,
) -> Result<(), FormeError> {
let needle = b"/ByteRange [0 0000000000 0000000000 0000000000]";
let pos = find_bytes(buf, needle).ok_or_else(|| {
FormeError::RenderError("ByteRange placeholder not found in output".to_string())
})?;
let br_str = format!(
"/ByteRange [0 {:>10} {:>10} {:>10}]",
before_sig,
after_sig,
total_len - after_sig
);
let br_bytes = br_str.as_bytes();
if br_bytes.len() != needle.len() {
return Err(FormeError::RenderError(format!(
"ByteRange replacement length mismatch: {} vs {}",
br_bytes.len(),
needle.len()
)));
}
buf[pos..pos + br_bytes.len()].copy_from_slice(br_bytes);
Ok(())
}
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack
.windows(needle.len())
.position(|window| window == needle)
}
fn build_pkcs7_signed_data(
cert_der: &[u8],
signature_bytes: &[u8],
_hash: &[u8],
) -> Result<Vec<u8>, FormeError> {
use der::Decode;
let cert = x509_cert::Certificate::from_der(cert_der)
.map_err(|e| FormeError::RenderError(format!("Failed to parse cert DER: {e}")))?;
let issuer_der = cert
.tbs_certificate
.issuer
.to_der()
.map_err(|e| FormeError::RenderError(format!("Failed to encode issuer: {e}")))?;
let serial_der = cert.tbs_certificate.serial_number.as_bytes();
let oid_signed_data: &[u8] = &[
0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x07, 0x02,
]; let oid_data: &[u8] = &[
0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x07, 0x01,
]; let oid_sha256: &[u8] = &[
0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01,
]; let oid_rsa: &[u8] = &[
0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01,
];
let signer_info = {
let mut si = Vec::new();
si.extend_from_slice(&der_integer(1));
let mut ias = Vec::new();
ias.extend_from_slice(&issuer_der);
ias.extend_from_slice(&der_integer_bytes(serial_der));
si.extend_from_slice(&der_sequence(&ias));
let mut da = Vec::new();
da.extend_from_slice(oid_sha256);
da.extend_from_slice(&[0x05, 0x00]); si.extend_from_slice(&der_sequence(&da));
let mut sa = Vec::new();
sa.extend_from_slice(oid_rsa);
sa.extend_from_slice(&[0x05, 0x00]); si.extend_from_slice(&der_sequence(&sa));
si.extend_from_slice(&der_octet_string(signature_bytes));
der_sequence(&si)
};
let signed_data = {
let mut sd = Vec::new();
sd.extend_from_slice(&der_integer(1));
let mut da_set_content = Vec::new();
let mut alg_id = Vec::new();
alg_id.extend_from_slice(oid_sha256);
alg_id.extend_from_slice(&[0x05, 0x00]);
da_set_content.extend_from_slice(&der_sequence(&alg_id));
sd.extend_from_slice(&der_set(&da_set_content));
let mut eci = Vec::new();
eci.extend_from_slice(oid_data);
sd.extend_from_slice(&der_sequence(&eci));
sd.extend_from_slice(&der_context_constructed(0, cert_der));
let mut si_set = Vec::new();
si_set.extend_from_slice(&signer_info);
sd.extend_from_slice(&der_set(&si_set));
der_sequence(&sd)
};
let content_info = {
let mut ci = Vec::new();
ci.extend_from_slice(oid_signed_data);
ci.extend_from_slice(&der_context_constructed(0, &signed_data));
der_sequence(&ci)
};
Ok(content_info)
}
fn der_integer(value: i64) -> Vec<u8> {
if (0..=127).contains(&value) {
vec![0x02, 0x01, value as u8]
} else {
let bytes = value.to_be_bytes();
let start = bytes
.iter()
.position(|&b| if value >= 0 { b != 0 } else { b != 0xFF })
.unwrap_or(bytes.len() - 1);
let significant = &bytes[start..];
if value >= 0 && significant[0] & 0x80 != 0 {
let mut result = vec![0x02];
result.extend_from_slice(&der_length(significant.len() + 1));
result.push(0x00);
result.extend_from_slice(significant);
result
} else {
let mut result = vec![0x02];
result.extend_from_slice(&der_length(significant.len()));
result.extend_from_slice(significant);
result
}
}
}
fn der_integer_bytes(bytes: &[u8]) -> Vec<u8> {
let mut result = vec![0x02];
if !bytes.is_empty() && bytes[0] & 0x80 != 0 {
result.extend_from_slice(&der_length(bytes.len() + 1));
result.push(0x00);
} else {
result.extend_from_slice(&der_length(bytes.len()));
}
result.extend_from_slice(bytes);
result
}
fn der_octet_string(data: &[u8]) -> Vec<u8> {
let mut result = vec![0x04];
result.extend_from_slice(&der_length(data.len()));
result.extend_from_slice(data);
result
}
fn der_sequence(content: &[u8]) -> Vec<u8> {
let mut result = vec![0x30];
result.extend_from_slice(&der_length(content.len()));
result.extend_from_slice(content);
result
}
fn der_set(content: &[u8]) -> Vec<u8> {
let mut result = vec![0x31];
result.extend_from_slice(&der_length(content.len()));
result.extend_from_slice(content);
result
}
fn der_context_constructed(tag: u8, content: &[u8]) -> Vec<u8> {
let mut result = vec![0xA0 | tag];
result.extend_from_slice(&der_length(content.len()));
result.extend_from_slice(content);
result
}
fn der_length(len: usize) -> Vec<u8> {
if len < 0x80 {
vec![len as u8]
} else if len < 0x100 {
vec![0x81, len as u8]
} else if len < 0x10000 {
vec![0x82, (len >> 8) as u8, len as u8]
} else if len < 0x1000000 {
vec![0x83, (len >> 16) as u8, (len >> 8) as u8, len as u8]
} else {
vec![
0x84,
(len >> 24) as u8,
(len >> 16) as u8,
(len >> 8) as u8,
len as u8,
]
}
}
fn hex_encode(data: &[u8]) -> String {
data.iter().map(|b| format!("{b:02X}")).collect()
}
fn escape_pdf_string(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'(' => out.push_str("\\("),
')' => out.push_str("\\)"),
'\\' => out.push_str("\\\\"),
_ => out.push(c),
}
}
out
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
pub(super) fn current_timestamp_secs() -> u64 {
(js_sys::Date::now() / 1000.0) as u64
}
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
pub(super) fn current_timestamp_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
pub(super) fn format_pdf_date() -> String {
let now = current_timestamp_secs();
let days = now / 86400;
let time_of_day = now % 86400;
let hours = time_of_day / 3600;
let minutes = (time_of_day % 3600) / 60;
let seconds = time_of_day % 60;
let (year, month, day) = epoch_days_to_ymd(days);
format!("D:{year:04}{month:02}{day:02}{hours:02}{minutes:02}{seconds:02}+00'00'")
}
pub(super) fn epoch_days_to_ymd(days: u64) -> (u64, u64, u64) {
let z = days + 719468;
let era = z / 146097;
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y, m, d)
}
fn format_display_date() -> String {
let now = current_timestamp_secs();
let days = now / 86400;
let time_of_day = now % 86400;
let hours = time_of_day / 3600;
let minutes = (time_of_day % 3600) / 60;
let (year, month, day) = epoch_days_to_ymd(days);
format!("{year:04}-{month:02}-{day:02} {hours:02}:{minutes:02} UTC")
}
fn parse_der_tag_length(bytes: &[u8]) -> Option<(u8, usize, usize)> {
if bytes.len() < 2 {
return None;
}
let tag = bytes[0];
let first = bytes[1];
if first < 0x80 {
Some((tag, first as usize, 2))
} else {
let num_bytes = (first & 0x7F) as usize;
if num_bytes == 0 || num_bytes > 4 || bytes.len() < 2 + num_bytes {
return None;
}
let mut len: usize = 0;
for i in 0..num_bytes {
len = (len << 8) | (bytes[2 + i] as usize);
}
Some((tag, len, 2 + num_bytes))
}
}
fn extract_cn_from_cert_der(cert_der: &[u8]) -> Option<String> {
use der::Decode;
let cert = x509_cert::Certificate::from_der(cert_der).ok()?;
let cn_oid = const_oid::ObjectIdentifier::new_unwrap("2.5.4.3");
for rdn in cert.tbs_certificate.subject.0.iter() {
for atv in rdn.0.iter() {
if atv.oid == cn_oid {
let value_bytes = atv.value.to_der().ok()?;
let (tag, len, hdr) = parse_der_tag_length(&value_bytes)?;
if value_bytes.len() >= hdr + len {
let s = std::str::from_utf8(&value_bytes[hdr..hdr + len]).ok()?;
if tag == 0x0C || tag == 0x13 || tag == 0x16 {
return Some(s.to_string());
}
}
}
}
}
None
}
fn find_catalog_pages_ref(text: &str, root_obj: usize) -> Option<usize> {
find_catalog_ref(text, root_obj, "/Pages")
}
fn find_catalog_ref(text: &str, obj_id: usize, key: &str) -> Option<usize> {
let obj_header = format!("{obj_id} 0 obj");
let obj_start = text.find(&obj_header)?;
let obj_section = &text[obj_start..];
let obj_end = obj_section.find("endobj")?;
let obj_content = &obj_section[..obj_end];
let key_pos = obj_content.find(key)?;
let after_key = &obj_content[key_pos + key.len()..];
let trimmed = after_key.trim_start();
let end = trimmed
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(trimmed.len());
if end == 0 {
return None;
}
trimmed[..end].parse().ok()
}
fn catalog_has_key(text: &str, obj_id: usize, key: &str) -> bool {
let obj_header = format!("{obj_id} 0 obj");
if let Some(obj_start) = text.find(&obj_header) {
let obj_section = &text[obj_start..];
if let Some(obj_end) = obj_section.find("endobj") {
return obj_section[..obj_end].contains(key);
}
}
false
}
fn find_catalog_string(text: &str, obj_id: usize, key: &str) -> Option<String> {
let obj_header = format!("{obj_id} 0 obj");
let obj_start = text.find(&obj_header)?;
let obj_section = &text[obj_start..];
let obj_end = obj_section.find("endobj")?;
let obj_content = &obj_section[..obj_end];
let key_pos = obj_content.find(key)?;
let after_key = &obj_content[key_pos + key.len()..];
let trimmed = after_key.trim_start();
if !trimmed.starts_with('(') {
return None;
}
let end = trimmed[1..].find(')')? + 1;
Some(trimmed[1..end].to_string())
}
struct AcroFormMetadata {
need_appearances: bool,
da: Option<String>,
}
fn find_existing_acroform_metadata(pdf: &[u8], root_obj: usize) -> AcroFormMetadata {
let text = String::from_utf8_lossy(pdf);
let obj_header = format!("{root_obj} 0 obj");
let obj_start = match text.find(&obj_header) {
Some(pos) => pos,
None => {
return AcroFormMetadata {
need_appearances: false,
da: None,
}
}
};
let obj_section = &text[obj_start..];
let obj_end = match obj_section.find("endobj") {
Some(pos) => pos,
None => {
return AcroFormMetadata {
need_appearances: false,
da: None,
}
}
};
let obj_content = &obj_section[..obj_end];
let acroform_pos = match obj_content.find("/AcroForm") {
Some(pos) => pos,
None => {
return AcroFormMetadata {
need_appearances: false,
da: None,
}
}
};
let after_acroform = &obj_content[acroform_pos..];
let need_appearances = after_acroform.contains("/NeedAppearances true");
let da = if let Some(da_pos) = after_acroform.find("/DA") {
let after_da = after_acroform[da_pos + 3..].trim_start();
if let Some(stripped) = after_da.strip_prefix('(') {
stripped.find(')').map(|end| stripped[..end].to_string())
} else {
None
}
} else {
None
};
AcroFormMetadata {
need_appearances,
da,
}
}
fn next_signature_name(pdf: &[u8]) -> String {
let text = String::from_utf8_lossy(pdf);
let mut max_num = 0u32;
let prefix = "/T (Signature";
let mut pos = 0;
while let Some(idx) = text[pos..].find(prefix) {
let after = &text[pos + idx + prefix.len()..];
if let Some(end) = after.find(')') {
if let Ok(n) = after[..end].parse::<u32>() {
max_num = max_num.max(n);
}
}
pos = pos + idx + prefix.len();
}
format!("Signature{}", max_num + 1)
}
fn find_existing_acroform_fields(pdf: &[u8], root_obj: usize) -> Vec<String> {
let text = String::from_utf8_lossy(pdf);
let obj_header = format!("{root_obj} 0 obj");
let obj_start = match text.find(&obj_header) {
Some(pos) => pos,
None => return Vec::new(),
};
let obj_section = &text[obj_start..];
let obj_end = match obj_section.find("endobj") {
Some(pos) => pos,
None => return Vec::new(),
};
let obj_content = &obj_section[..obj_end];
let acroform_pos = match obj_content.find("/AcroForm") {
Some(pos) => pos,
None => return Vec::new(),
};
let after_acroform = &obj_content[acroform_pos..];
let fields_pos = match after_acroform.find("/Fields") {
Some(pos) => pos,
None => return Vec::new(),
};
let after_fields = &after_acroform[fields_pos + 7..]; let trimmed = after_fields.trim_start();
if !trimmed.starts_with('[') {
return Vec::new();
}
let bracket_end = match trimmed.find(']') {
Some(pos) => pos,
None => return Vec::new(),
};
let fields_content = &trimmed[1..bracket_end];
let mut fields = Vec::new();
let mut remaining = fields_content.trim();
while !remaining.is_empty() {
let end = remaining
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(remaining.len());
if end == 0 {
break;
}
let obj_num = &remaining[..end];
remaining = remaining[end..].trim_start();
if remaining.starts_with("0 R") {
fields.push(format!("{obj_num} 0 R"));
remaining = remaining[3..].trim_start();
} else {
break;
}
}
fields
}
fn find_catalog_array_content(text: &str, obj_id: usize, key: &str) -> Option<String> {
let obj_header = format!("{obj_id} 0 obj");
let obj_start = text.find(&obj_header)?;
let obj_section = &text[obj_start..];
let obj_end = obj_section.find("endobj")?;
let obj_content = &obj_section[..obj_end];
let key_pos = obj_content.find(key)?;
let after_key = &obj_content[key_pos + key.len()..];
let trimmed = after_key.trim_start();
if !trimmed.starts_with('[') {
return None;
}
let end = trimmed.find(']')? + 1;
Some(trimmed[..end].to_string())
}