use lopdf::Document;
#[derive(Debug, Clone)]
pub struct EncryptionInfo {
pub is_encrypted: bool,
pub version: Option<i64>,
pub key_length: Option<i64>,
pub filter: Option<String>,
pub permissions: Option<i64>,
}
impl EncryptionInfo {
pub fn can_print(&self) -> bool {
self.permissions.is_none_or(|p| p & 0x4 != 0)
}
pub fn can_copy(&self) -> bool {
self.permissions.is_none_or(|p| p & 0x10 != 0)
}
pub fn can_modify(&self) -> bool {
self.permissions.is_none_or(|p| p & 0x8 != 0)
}
}
pub fn detect_encryption(doc: &Document) -> EncryptionInfo {
let trailer = &doc.trailer;
let encrypt_dict = trailer.get(b"Encrypt").ok().and_then(|obj| match obj {
lopdf::Object::Dictionary(d) => Some(d.clone()),
lopdf::Object::Reference(id) => doc
.get_object(*id)
.ok()
.and_then(|o| o.as_dict().ok().cloned()),
_ => None,
});
let Some(dict) = encrypt_dict else {
return EncryptionInfo {
is_encrypted: false,
version: None,
key_length: None,
filter: None,
permissions: None,
};
};
let version = dict.get(b"V").ok().and_then(|o| {
if let lopdf::Object::Integer(i) = o {
Some(*i)
} else {
None
}
});
let key_length = dict.get(b"Length").ok().and_then(|o| {
if let lopdf::Object::Integer(i) = o {
Some(*i)
} else {
None
}
});
let filter = dict.get(b"Filter").ok().and_then(|o| match o {
lopdf::Object::Name(n) => String::from_utf8(n.clone()).ok(),
_ => None,
});
let permissions = dict.get(b"P").ok().and_then(|o| {
if let lopdf::Object::Integer(i) = o {
Some(*i)
} else {
None
}
});
EncryptionInfo {
is_encrypted: true,
version,
key_length,
filter,
permissions,
}
}
pub fn load_with_password(
data: &[u8],
password: Option<&str>,
) -> Result<Document, crate::EdgePdfError> {
match Document::load_mem(data) {
Ok(doc) => {
let info = detect_encryption(&doc);
if info.is_encrypted && password.is_none() {
log::warn!("Document is encrypted but no password was provided");
}
Ok(doc)
}
Err(e) => {
if password.is_some() {
Err(crate::EdgePdfError::LoadError(format!(
"Failed to load encrypted PDF (password may be incorrect): {}",
e
)))
} else {
Err(crate::EdgePdfError::LoadError(format!(
"Failed to load PDF (may be encrypted — try providing a password): {}",
e
)))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unencrypted_document() {
let doc = Document::new();
let info = detect_encryption(&doc);
assert!(!info.is_encrypted);
assert!(info.version.is_none());
assert!(info.can_print());
assert!(info.can_copy());
assert!(info.can_modify());
}
#[test]
fn test_permissions_parsing() {
let info = EncryptionInfo {
is_encrypted: true,
version: Some(2),
key_length: Some(128),
filter: Some("Standard".to_string()),
permissions: Some(-1), };
assert!(info.can_print());
assert!(info.can_copy());
assert!(info.can_modify());
}
#[test]
fn test_restricted_permissions() {
let info = EncryptionInfo {
is_encrypted: true,
version: Some(2),
key_length: Some(128),
filter: Some("Standard".to_string()),
permissions: Some(0),
};
assert!(!info.can_print());
assert!(!info.can_copy());
assert!(!info.can_modify());
}
#[test]
fn test_load_empty_pdf_bytes() {
let mut doc = Document::new();
let mut buf = Vec::new();
doc.save_to(&mut buf).unwrap();
let result = load_with_password(&buf, None);
assert!(result.is_ok());
}
}