1use crate::attachments::AfRelationship;
25use crate::error::PdfError;
26use crate::objects::{Dict, Object};
27use crate::reader::document::{decode_stream, DocumentReader};
28
29#[derive(Debug, Clone)]
31pub struct PdfAttachment {
32 pub name: String,
35 pub mime_type: Option<String>,
38 pub bytes: Vec<u8>,
40 pub modified: Option<String>,
43 pub af_relationship: Option<AfRelationship>,
52}
53
54pub fn attachments(reader: &mut DocumentReader<'_>) -> Result<Vec<PdfAttachment>, PdfError> {
67 let root_id = reader.xref().root()?;
68 let catalog = reader.resolve(root_id)?;
69 let Object::Dict(catalog_dict) = catalog else {
70 return Err(PdfError::other(format!(
71 "PDF attachments reader: /Root must be a dict (got {catalog:?})"
72 )));
73 };
74
75 let names_obj = catalog_dict
77 .entries()
78 .iter()
79 .find(|(k, _)| k == "Names")
80 .map(|(_, v)| v.clone());
81 let Some(names_obj) = names_obj else {
82 return Ok(Vec::new());
83 };
84 let names_dict = match reader.deref(names_obj)? {
85 Object::Dict(d) => d,
86 _ => return Ok(Vec::new()),
87 };
88
89 let ef_obj = names_dict
91 .entries()
92 .iter()
93 .find(|(k, _)| k == "EmbeddedFiles")
94 .map(|(_, v)| v.clone());
95 let Some(ef_obj) = ef_obj else {
96 return Ok(Vec::new());
97 };
98 let root_node = match reader.deref(ef_obj)? {
99 Object::Dict(d) => d,
100 _ => return Ok(Vec::new()),
101 };
102
103 let mut entries: Vec<(String, Object)> = Vec::new();
105 walk_name_tree(reader, &root_node, &mut entries, 0)?;
106
107 let mut out = Vec::with_capacity(entries.len());
109 for (name, filespec_value) in entries {
110 let filespec_dict = match reader.deref(filespec_value)? {
112 Object::Dict(d) => d,
113 _ => continue, };
115 let resolved_name = decode_filespec_name(&filespec_dict).unwrap_or(name);
119
120 let ef_entry = filespec_dict
122 .entries()
123 .iter()
124 .find(|(k, _)| k == "EF")
125 .map(|(_, v)| v.clone());
126 let Some(ef_entry) = ef_entry else {
127 continue;
128 };
129 let ef_dict = match reader.deref(ef_entry)? {
130 Object::Dict(d) => d,
131 _ => continue,
132 };
133 let stream_ref = ef_dict
135 .entries()
136 .iter()
137 .find(|(k, _)| k == "UF")
138 .or_else(|| ef_dict.entries().iter().find(|(k, _)| k == "F"))
139 .map(|(_, v)| v.clone());
140 let Some(stream_ref) = stream_ref else {
141 continue;
142 };
143 let stream_obj = match reader.deref(stream_ref)? {
144 Object::Stream(s) => s,
145 _ => continue,
146 };
147
148 let mime_type = stream_obj
149 .dict
150 .entries()
151 .iter()
152 .find(|(k, _)| k == "Subtype")
153 .and_then(|(_, v)| match v {
154 Object::Name(s) => Some(s.clone()),
155 _ => None,
156 });
157
158 let modified = read_params_moddate(&stream_obj.dict);
159
160 let af_relationship = read_af_relationship(&filespec_dict);
161
162 let bytes = decode_stream(&stream_obj)?;
163
164 out.push(PdfAttachment {
165 name: resolved_name,
166 mime_type,
167 bytes,
168 modified,
169 af_relationship,
170 });
171 }
172
173 Ok(out)
174}
175
176fn walk_name_tree(
183 reader: &mut DocumentReader<'_>,
184 node: &Dict,
185 out: &mut Vec<(String, Object)>,
186 depth: usize,
187) -> Result<(), PdfError> {
188 if depth > 32 {
189 return Ok(()); }
191 if out.len() > 100_000 {
192 return Ok(()); }
194 if let Some(Object::Array(items)) = node
196 .entries()
197 .iter()
198 .find(|(k, _)| k == "Names")
199 .map(|(_, v)| v)
200 {
201 let mut iter = items.iter();
202 while let (Some(key_obj), Some(val_obj)) = (iter.next(), iter.next()) {
203 let Some(key) = decode_text_obj(key_obj) else {
204 continue;
205 };
206 out.push((key, val_obj.clone()));
207 }
208 return Ok(());
209 }
210 if let Some(kids_obj) = node
212 .entries()
213 .iter()
214 .find(|(k, _)| k == "Kids")
215 .map(|(_, v)| v.clone())
216 {
217 let kids = match reader.deref(kids_obj)? {
218 Object::Array(items) => items,
219 _ => return Ok(()),
220 };
221 for kid in kids {
222 let kid_dict = match reader.deref(kid)? {
223 Object::Dict(d) => d,
224 _ => continue,
225 };
226 walk_name_tree(reader, &kid_dict, out, depth + 1)?;
227 }
228 }
229 Ok(())
230}
231
232fn decode_filespec_name(filespec: &Dict) -> Option<String> {
236 let pick = filespec
237 .entries()
238 .iter()
239 .find(|(k, _)| k == "UF")
240 .or_else(|| filespec.entries().iter().find(|(k, _)| k == "F"));
241 pick.and_then(|(_, v)| decode_text_obj(v))
242}
243
244fn read_af_relationship(filespec: &Dict) -> Option<AfRelationship> {
250 let val = filespec
251 .entries()
252 .iter()
253 .find(|(k, _)| k == "AFRelationship")
254 .map(|(_, v)| v)?;
255 match val {
256 Object::Name(s) => AfRelationship::from_pdf_name(s),
257 _ => None,
258 }
259}
260
261fn read_params_moddate(dict: &Dict) -> Option<String> {
264 let params = dict
265 .entries()
266 .iter()
267 .find(|(k, _)| k == "Params")
268 .map(|(_, v)| v)?;
269 let params_dict = match params {
270 Object::Dict(d) => d,
271 _ => return None,
272 };
273 params_dict
274 .entries()
275 .iter()
276 .find(|(k, _)| k == "ModDate")
277 .and_then(|(_, v)| decode_text_obj(v))
278}
279
280fn decode_text_obj(obj: &Object) -> Option<String> {
284 match obj {
285 Object::LiteralString(b) => Some(String::from_utf8_lossy(b).into_owned()),
286 Object::HexString(b) => {
287 if b.len() >= 2 && b[0] == 0xFE && b[1] == 0xFF {
288 let utf16: Vec<u16> = b[2..]
289 .chunks_exact(2)
290 .map(|c| u16::from_be_bytes([c[0], c[1]]))
291 .collect();
292 Some(String::from_utf16_lossy(&utf16))
293 } else {
294 Some(String::from_utf8_lossy(b).into_owned())
295 }
296 }
297 Object::Name(s) => Some(s.clone()),
298 _ => None,
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn decode_text_obj_literal_string_passes_through() {
308 let s = decode_text_obj(&Object::LiteralString(b"hello".to_vec()));
309 assert_eq!(s.as_deref(), Some("hello"));
310 }
311
312 #[test]
313 fn decode_text_obj_utf16be_hex_decodes() {
314 let s = decode_text_obj(&Object::HexString(vec![0xFE, 0xFF, 0x00, 0x48, 0x00, 0x69]));
316 assert_eq!(s.as_deref(), Some("Hi"));
317 }
318
319 #[test]
320 fn decode_text_obj_hex_without_bom_treated_as_utf8() {
321 let s = decode_text_obj(&Object::HexString(b"hello".to_vec()));
322 assert_eq!(s.as_deref(), Some("hello"));
323 }
324}