use crate::document::Document;
use crate::objects::{Dictionary, Object};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Edition {
OpenSource,
}
impl Edition {
pub fn as_str(&self) -> &'static str {
match self {
Edition::OpenSource => "OpenSource",
}
}
}
pub struct PdfSignature {
#[allow(dead_code)]
version: String,
edition: Edition,
build_hash: String,
features_fingerprint: u16,
}
impl PdfSignature {
pub fn new(document: &Document, edition: Edition) -> Self {
Self {
version: env!("CARGO_PKG_VERSION").to_string(),
edition,
build_hash: Self::generate_build_hash(edition),
features_fingerprint: Self::compute_features(document),
}
}
fn generate_build_hash(edition: Edition) -> String {
let mut hasher = Sha256::new();
hasher.update(env!("CARGO_PKG_VERSION").as_bytes());
hasher.update(edition.as_str().as_bytes());
let build_time = option_env!("BUILD_TIMESTAMP").unwrap_or("2024-10-05");
hasher.update(build_time.as_bytes());
hasher.update(b"oxidize-pdf-signature-v1");
let hash = hasher.finalize();
format!("oxpdf-{}", hex_encode(&hash[..8]))
}
fn compute_features(document: &Document) -> u16 {
let mut features = 0u16;
if document.encryption.is_some() {
features |= 0x0001;
}
if !document.semantic_entities.is_empty() {
features |= 0x0002;
}
if document.outline.is_some() {
features |= 0x0004;
}
if document.acro_form.is_some() {
features |= 0x0008;
}
if document.named_destinations.is_some() {
features |= 0x0010;
}
if document.page_labels.is_some() {
features |= 0x0020;
}
if document.open_action.is_some() {
features |= 0x0040;
}
if document.viewer_preferences.is_some() {
features |= 0x0080;
}
if !document.custom_fonts.is_empty() {
features |= 0x0100;
}
if document.compress {
features |= 0x0200;
}
if document.use_xref_streams {
features |= 0x0400;
}
features
}
pub fn write_to_info_dict(&self, info_dict: &mut Dictionary) {
info_dict.set("oxidize-pdf-build", Object::String(self.build_hash.clone()));
info_dict.set(
"oxidize-pdf-features",
Object::String(format!("{:04x}", self.features_fingerprint)),
);
info_dict.set(
"oxidize-pdf-edition",
Object::String(self.edition.as_str().to_string()),
);
}
#[allow(dead_code)]
pub fn build_hash(&self) -> &str {
&self.build_hash
}
#[allow(dead_code)]
pub fn features(&self) -> u16 {
self.features_fingerprint
}
}
fn hex_encode(bytes: &[u8]) -> String {
bytes
.iter()
.map(|b| format!("{:02x}", b))
.collect::<String>()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Document;
#[test]
fn test_edition_as_str() {
assert_eq!(Edition::OpenSource.as_str(), "OpenSource");
}
#[test]
fn test_build_hash_format() {
let hash = PdfSignature::generate_build_hash(Edition::OpenSource);
assert!(hash.starts_with("oxpdf-"));
assert_eq!(hash.len(), 22); }
#[test]
fn test_compute_features_empty() {
let doc = Document::new();
let features = PdfSignature::compute_features(&doc);
assert!(features & 0x0200 != 0, "Compression should be enabled");
}
#[test]
fn test_compute_features_with_encryption() {
let mut doc = Document::new();
let permissions = crate::encryption::Permissions::default();
let encryption = crate::document::DocumentEncryption::new(
"user_password",
"owner_password",
permissions,
crate::document::EncryptionStrength::Rc4_128bit,
);
doc.set_encryption(encryption);
let features = PdfSignature::compute_features(&doc);
assert!(features & 0x0001 != 0, "Encryption bit should be set");
}
#[test]
fn test_pdf_signature_creation() {
let doc = Document::new();
let signature = PdfSignature::new(&doc, Edition::OpenSource);
assert_eq!(signature.version, env!("CARGO_PKG_VERSION"));
assert_eq!(signature.edition, Edition::OpenSource);
assert!(signature.build_hash.starts_with("oxpdf-"));
assert!(signature.features_fingerprint > 0); }
#[test]
fn test_write_to_info_dict() {
let doc = Document::new();
let signature = PdfSignature::new(&doc, Edition::OpenSource);
let mut dict = Dictionary::new();
signature.write_to_info_dict(&mut dict);
assert!(dict.get("oxidize-pdf-build").is_some());
assert!(dict.get("oxidize-pdf-features").is_some());
}
#[test]
fn test_features_fingerprint_format() {
let doc = Document::new();
let signature = PdfSignature::new(&doc, Edition::OpenSource);
let mut dict = Dictionary::new();
signature.write_to_info_dict(&mut dict);
let features = dict.get("oxidize-pdf-features").unwrap();
if let Object::String(features_str) = features {
assert_eq!(features_str.len(), 4);
assert!(
features_str.chars().all(|c| c.is_ascii_hexdigit()),
"Features should be hex encoded"
);
} else {
panic!("Features should be a string");
}
}
}