edgeparse_core/pdf/
encryption.rs1use lopdf::Document;
4
5#[derive(Debug, Clone)]
7pub struct EncryptionInfo {
8 pub is_encrypted: bool,
10 pub version: Option<i64>,
12 pub key_length: Option<i64>,
14 pub filter: Option<String>,
16 pub permissions: Option<i64>,
18}
19
20impl EncryptionInfo {
21 pub fn can_print(&self) -> bool {
23 self.permissions.is_none_or(|p| p & 0x4 != 0)
24 }
25
26 pub fn can_copy(&self) -> bool {
28 self.permissions.is_none_or(|p| p & 0x10 != 0)
29 }
30
31 pub fn can_modify(&self) -> bool {
33 self.permissions.is_none_or(|p| p & 0x8 != 0)
34 }
35}
36
37pub fn detect_encryption(doc: &Document) -> EncryptionInfo {
42 let trailer = &doc.trailer;
43
44 let encrypt_dict = trailer.get(b"Encrypt").ok().and_then(|obj| match obj {
45 lopdf::Object::Dictionary(d) => Some(d.clone()),
46 lopdf::Object::Reference(id) => doc
47 .get_object(*id)
48 .ok()
49 .and_then(|o| o.as_dict().ok().cloned()),
50 _ => None,
51 });
52
53 let Some(dict) = encrypt_dict else {
54 return EncryptionInfo {
55 is_encrypted: false,
56 version: None,
57 key_length: None,
58 filter: None,
59 permissions: None,
60 };
61 };
62
63 let version = dict.get(b"V").ok().and_then(|o| {
64 if let lopdf::Object::Integer(i) = o {
65 Some(*i)
66 } else {
67 None
68 }
69 });
70
71 let key_length = dict.get(b"Length").ok().and_then(|o| {
72 if let lopdf::Object::Integer(i) = o {
73 Some(*i)
74 } else {
75 None
76 }
77 });
78
79 let filter = dict.get(b"Filter").ok().and_then(|o| match o {
80 lopdf::Object::Name(n) => String::from_utf8(n.clone()).ok(),
81 _ => None,
82 });
83
84 let permissions = dict.get(b"P").ok().and_then(|o| {
85 if let lopdf::Object::Integer(i) = o {
86 Some(*i)
87 } else {
88 None
89 }
90 });
91
92 EncryptionInfo {
93 is_encrypted: true,
94 version,
95 key_length,
96 filter,
97 permissions,
98 }
99}
100
101pub fn load_with_password(
106 data: &[u8],
107 password: Option<&str>,
108) -> Result<Document, crate::EdgePdfError> {
109 match Document::load_mem(data) {
112 Ok(doc) => {
113 let info = detect_encryption(&doc);
114 if info.is_encrypted && password.is_none() {
115 log::warn!("Document is encrypted but no password was provided");
116 }
117 Ok(doc)
118 }
119 Err(e) => {
120 if password.is_some() {
121 Err(crate::EdgePdfError::LoadError(format!(
122 "Failed to load encrypted PDF (password may be incorrect): {}",
123 e
124 )))
125 } else {
126 Err(crate::EdgePdfError::LoadError(format!(
127 "Failed to load PDF (may be encrypted — try providing a password): {}",
128 e
129 )))
130 }
131 }
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn test_unencrypted_document() {
141 let doc = Document::new();
142 let info = detect_encryption(&doc);
143 assert!(!info.is_encrypted);
144 assert!(info.version.is_none());
145 assert!(info.can_print());
146 assert!(info.can_copy());
147 assert!(info.can_modify());
148 }
149
150 #[test]
151 fn test_permissions_parsing() {
152 let info = EncryptionInfo {
154 is_encrypted: true,
155 version: Some(2),
156 key_length: Some(128),
157 filter: Some("Standard".to_string()),
158 permissions: Some(-1), };
160 assert!(info.can_print());
161 assert!(info.can_copy());
162 assert!(info.can_modify());
163 }
164
165 #[test]
166 fn test_restricted_permissions() {
167 let info = EncryptionInfo {
169 is_encrypted: true,
170 version: Some(2),
171 key_length: Some(128),
172 filter: Some("Standard".to_string()),
173 permissions: Some(0),
174 };
175 assert!(!info.can_print());
176 assert!(!info.can_copy());
177 assert!(!info.can_modify());
178 }
179
180 #[test]
181 fn test_load_empty_pdf_bytes() {
182 let mut doc = Document::new();
184 let mut buf = Vec::new();
185 doc.save_to(&mut buf).unwrap();
186 let result = load_with_password(&buf, None);
187 assert!(result.is_ok());
188 }
189}