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 let mut doc = Document::load_mem(data).map_err(|e| {
110 crate::EdgePdfError::LoadError(format!(
111 "Failed to load PDF (may be encrypted — try providing a password): {e}"
112 ))
113 })?;
114
115 decrypt_document(&mut doc, password)?;
116 Ok(doc)
117}
118
119pub fn decrypt_document(
124 doc: &mut Document,
125 password: Option<&str>,
126) -> Result<(), crate::EdgePdfError> {
127 if !doc.is_encrypted() {
128 return Ok(());
129 }
130
131 let candidates: Vec<&str> = match password {
132 Some(p) => vec![p, ""],
133 None => vec![""],
134 };
135
136 let mut last_err = None;
137 for pw in candidates {
138 match doc.decrypt(pw) {
139 Ok(()) => return Ok(()),
140 Err(e) => last_err = Some(e),
141 }
142 }
143
144 Err(crate::EdgePdfError::LoadError(format!(
145 "Failed to decrypt PDF (password may be incorrect): {}",
146 last_err
147 .map(|e| e.to_string())
148 .unwrap_or_else(|| "unknown error".into())
149 )))
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn test_unencrypted_document() {
158 let doc = Document::new();
159 let info = detect_encryption(&doc);
160 assert!(!info.is_encrypted);
161 assert!(info.version.is_none());
162 assert!(info.can_print());
163 assert!(info.can_copy());
164 assert!(info.can_modify());
165 }
166
167 #[test]
168 fn test_permissions_parsing() {
169 let info = EncryptionInfo {
170 is_encrypted: true,
171 version: Some(2),
172 key_length: Some(128),
173 filter: Some("Standard".to_string()),
174 permissions: Some(-1),
175 };
176 assert!(info.can_print());
177 assert!(info.can_copy());
178 assert!(info.can_modify());
179 }
180
181 #[test]
182 fn test_restricted_permissions() {
183 let info = EncryptionInfo {
184 is_encrypted: true,
185 version: Some(2),
186 key_length: Some(128),
187 filter: Some("Standard".to_string()),
188 permissions: Some(0),
189 };
190 assert!(!info.can_print());
191 assert!(!info.can_copy());
192 assert!(!info.can_modify());
193 }
194
195 #[test]
196 fn test_load_empty_pdf_bytes() {
197 let mut doc = Document::new();
198 let mut buf = Vec::new();
199 doc.save_to(&mut buf).unwrap();
200 let result = load_with_password(&buf, None);
201 assert!(result.is_ok());
202 }
203
204 #[test]
205 fn decrypt_noop_on_plain_doc() {
206 let mut doc = Document::new();
207 assert!(decrypt_document(&mut doc, Some("secret")).is_ok());
208 }
209}