use crate::attachments::AfRelationship;
use crate::error::PdfError;
use crate::objects::{Dict, Object};
use crate::reader::document::{decode_stream, DocumentReader};
#[derive(Debug, Clone)]
pub struct PdfAttachment {
pub name: String,
pub mime_type: Option<String>,
pub bytes: Vec<u8>,
pub modified: Option<String>,
pub af_relationship: Option<AfRelationship>,
}
pub fn attachments(reader: &mut DocumentReader<'_>) -> Result<Vec<PdfAttachment>, PdfError> {
let root_id = reader.xref().root()?;
let catalog = reader.resolve(root_id)?;
let Object::Dict(catalog_dict) = catalog else {
return Err(PdfError::other(format!(
"PDF attachments reader: /Root must be a dict (got {catalog:?})"
)));
};
let names_obj = catalog_dict
.entries()
.iter()
.find(|(k, _)| k == "Names")
.map(|(_, v)| v.clone());
let Some(names_obj) = names_obj else {
return Ok(Vec::new());
};
let names_dict = match reader.deref(names_obj)? {
Object::Dict(d) => d,
_ => return Ok(Vec::new()),
};
let ef_obj = names_dict
.entries()
.iter()
.find(|(k, _)| k == "EmbeddedFiles")
.map(|(_, v)| v.clone());
let Some(ef_obj) = ef_obj else {
return Ok(Vec::new());
};
let root_node = match reader.deref(ef_obj)? {
Object::Dict(d) => d,
_ => return Ok(Vec::new()),
};
let mut entries: Vec<(String, Object)> = Vec::new();
walk_name_tree(reader, &root_node, &mut entries, 0)?;
let mut out = Vec::with_capacity(entries.len());
for (name, filespec_value) in entries {
let filespec_dict = match reader.deref(filespec_value)? {
Object::Dict(d) => d,
_ => continue, };
let resolved_name = decode_filespec_name(&filespec_dict).unwrap_or(name);
let ef_entry = filespec_dict
.entries()
.iter()
.find(|(k, _)| k == "EF")
.map(|(_, v)| v.clone());
let Some(ef_entry) = ef_entry else {
continue;
};
let ef_dict = match reader.deref(ef_entry)? {
Object::Dict(d) => d,
_ => continue,
};
let stream_ref = ef_dict
.entries()
.iter()
.find(|(k, _)| k == "UF")
.or_else(|| ef_dict.entries().iter().find(|(k, _)| k == "F"))
.map(|(_, v)| v.clone());
let Some(stream_ref) = stream_ref else {
continue;
};
let stream_obj = match reader.deref(stream_ref)? {
Object::Stream(s) => s,
_ => continue,
};
let mime_type = stream_obj
.dict
.entries()
.iter()
.find(|(k, _)| k == "Subtype")
.and_then(|(_, v)| match v {
Object::Name(s) => Some(s.clone()),
_ => None,
});
let modified = read_params_moddate(&stream_obj.dict);
let af_relationship = read_af_relationship(&filespec_dict);
let bytes = decode_stream(&stream_obj)?;
out.push(PdfAttachment {
name: resolved_name,
mime_type,
bytes,
modified,
af_relationship,
});
}
Ok(out)
}
fn walk_name_tree(
reader: &mut DocumentReader<'_>,
node: &Dict,
out: &mut Vec<(String, Object)>,
depth: usize,
) -> Result<(), PdfError> {
if depth > 32 {
return Ok(()); }
if out.len() > 100_000 {
return Ok(()); }
if let Some(Object::Array(items)) = node
.entries()
.iter()
.find(|(k, _)| k == "Names")
.map(|(_, v)| v)
{
let mut iter = items.iter();
while let (Some(key_obj), Some(val_obj)) = (iter.next(), iter.next()) {
let Some(key) = decode_text_obj(key_obj) else {
continue;
};
out.push((key, val_obj.clone()));
}
return Ok(());
}
if let Some(kids_obj) = node
.entries()
.iter()
.find(|(k, _)| k == "Kids")
.map(|(_, v)| v.clone())
{
let kids = match reader.deref(kids_obj)? {
Object::Array(items) => items,
_ => return Ok(()),
};
for kid in kids {
let kid_dict = match reader.deref(kid)? {
Object::Dict(d) => d,
_ => continue,
};
walk_name_tree(reader, &kid_dict, out, depth + 1)?;
}
}
Ok(())
}
fn decode_filespec_name(filespec: &Dict) -> Option<String> {
let pick = filespec
.entries()
.iter()
.find(|(k, _)| k == "UF")
.or_else(|| filespec.entries().iter().find(|(k, _)| k == "F"));
pick.and_then(|(_, v)| decode_text_obj(v))
}
fn read_af_relationship(filespec: &Dict) -> Option<AfRelationship> {
let val = filespec
.entries()
.iter()
.find(|(k, _)| k == "AFRelationship")
.map(|(_, v)| v)?;
match val {
Object::Name(s) => AfRelationship::from_pdf_name(s),
_ => None,
}
}
fn read_params_moddate(dict: &Dict) -> Option<String> {
let params = dict
.entries()
.iter()
.find(|(k, _)| k == "Params")
.map(|(_, v)| v)?;
let params_dict = match params {
Object::Dict(d) => d,
_ => return None,
};
params_dict
.entries()
.iter()
.find(|(k, _)| k == "ModDate")
.and_then(|(_, v)| decode_text_obj(v))
}
fn decode_text_obj(obj: &Object) -> Option<String> {
match obj {
Object::LiteralString(b) => Some(String::from_utf8_lossy(b).into_owned()),
Object::HexString(b) => {
if b.len() >= 2 && b[0] == 0xFE && b[1] == 0xFF {
let utf16: Vec<u16> = b[2..]
.chunks_exact(2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.collect();
Some(String::from_utf16_lossy(&utf16))
} else {
Some(String::from_utf8_lossy(b).into_owned())
}
}
Object::Name(s) => Some(s.clone()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_text_obj_literal_string_passes_through() {
let s = decode_text_obj(&Object::LiteralString(b"hello".to_vec()));
assert_eq!(s.as_deref(), Some("hello"));
}
#[test]
fn decode_text_obj_utf16be_hex_decodes() {
let s = decode_text_obj(&Object::HexString(vec![0xFE, 0xFF, 0x00, 0x48, 0x00, 0x69]));
assert_eq!(s.as_deref(), Some("Hi"));
}
#[test]
fn decode_text_obj_hex_without_bom_treated_as_utf8() {
let s = decode_text_obj(&Object::HexString(b"hello".to_vec()));
assert_eq!(s.as_deref(), Some("hello"));
}
}