use std::collections::HashMap;
use crate::metadata::{PdfDate, pdf_string_to_rust_pub};
use crate::name_tree::walk_name_tree;
use crate::objects::{PdfDict, PdfObj};
use crate::resolver::Resolver;
#[derive(Debug, Clone)]
pub struct EmbeddedFile {
pub name: String,
pub filename: Option<String>,
pub unicode_filename: Option<String>,
pub description: Option<String>,
pub relationship: Option<AfRelationship>,
pub mime_type: Option<String>,
pub size: Option<u64>,
pub creation_date: Option<PdfDate>,
pub mod_date: Option<PdfDate>,
pub checksum: Option<Vec<u8>>,
pub stream_obj_num: u32,
pub stream_gen_num: u16,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum AfRelationship {
Source,
Data,
Alternative,
Supplement,
EncryptedPayload,
FormData,
Schema,
Unspecified,
Other(String),
}
impl AfRelationship {
fn from_name(name: &[u8]) -> Self {
match name {
b"Source" => AfRelationship::Source,
b"Data" => AfRelationship::Data,
b"Alternative" => AfRelationship::Alternative,
b"Supplement" => AfRelationship::Supplement,
b"EncryptedPayload" => AfRelationship::EncryptedPayload,
b"FormData" => AfRelationship::FormData,
b"Schema" => AfRelationship::Schema,
b"Unspecified" => AfRelationship::Unspecified,
other => AfRelationship::Other(String::from_utf8_lossy(other).into_owned()),
}
}
}
pub fn parse_embedded_files(resolver: &Resolver) -> HashMap<String, EmbeddedFile> {
let mut map = HashMap::new();
let Some(catalog) = catalog_dict(resolver) else {
return map;
};
let Some(names_obj) = catalog.get(b"Names") else {
return map;
};
let Ok(names) = resolver.deref(names_obj) else {
return map;
};
let Some(names_dict) = names.as_dict() else {
return map;
};
let Some(ef_root) = names_dict.get(b"EmbeddedFiles") else {
return map;
};
map = walk_name_tree(resolver, ef_root, parse_filespec);
for (key, value) in map.iter_mut() {
value.name = key.clone();
}
map
}
fn parse_filespec(resolver: &Resolver, obj: &PdfObj) -> Option<EmbeddedFile> {
let resolved = resolver.deref(obj).ok()?;
let dict = resolved.as_dict()?;
let ef_obj = dict.get(b"EF")?;
let ef = resolver.deref(ef_obj).ok()?;
let ef_dict = ef.as_dict()?;
let stream_ref = ef_dict.get_ref(b"UF").or_else(|| ef_dict.get_ref(b"F"))?;
let mut size = None;
let mut creation_date = None;
let mut mod_date = None;
let mut checksum = None;
let mut mime_type = None;
if let Ok(stream) = resolver.resolve(stream_ref.0, stream_ref.1)
&& let Some(stream_dict) = stream.as_dict()
{
if let Some(subtype) = stream_dict.get_name(b"Subtype") {
mime_type = Some(decode_mime_name(subtype));
}
if let Some(params_obj) = stream_dict.get(b"Params")
&& let Ok(params_resolved) = resolver.deref(params_obj)
&& let Some(params) = params_resolved.as_dict()
{
size = params.get_int(b"Size").and_then(|n| u64::try_from(n).ok());
creation_date = params
.get(b"CreationDate")
.and_then(|o| o.as_str())
.and_then(PdfDate::parse);
mod_date = params
.get(b"ModDate")
.and_then(|o| o.as_str())
.and_then(PdfDate::parse);
checksum = params
.get(b"CheckSum")
.and_then(|o| o.as_str())
.map(<[u8]>::to_vec);
}
}
let filename = dict.get(b"F").and_then(pdf_string_to_rust_pub);
let unicode_filename = dict.get(b"UF").and_then(pdf_string_to_rust_pub);
let description = dict.get(b"Desc").and_then(pdf_string_to_rust_pub);
let relationship = dict
.get_name(b"AFRelationship")
.map(AfRelationship::from_name);
Some(EmbeddedFile {
name: String::new(), filename,
unicode_filename,
description,
relationship,
mime_type,
size,
creation_date,
mod_date,
checksum,
stream_obj_num: stream_ref.0,
stream_gen_num: stream_ref.1,
})
}
fn decode_mime_name(name: &[u8]) -> String {
let s = String::from_utf8_lossy(name);
let mut out = String::with_capacity(s.len());
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'#'
&& i + 2 < bytes.len()
&& let Ok(b) =
u8::from_str_radix(std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or(""), 16)
{
out.push(b as char);
i += 3;
continue;
}
out.push(bytes[i] as char);
i += 1;
}
out
}
fn catalog_dict(resolver: &Resolver) -> Option<PdfDict> {
if let Some((num, gen_num)) = resolver.trailer().get_ref(b"Root")
&& let Ok(obj) = resolver.resolve(num, gen_num)
&& let Some(dict) = obj.as_dict()
{
return Some(dict.clone());
}
crate::find_catalog(resolver).and_then(|obj| obj.as_dict().cloned())
}
pub fn decode_embedded_file_stream(
resolver: &Resolver,
obj_num: u32,
gen_num: u16,
) -> Result<Vec<u8>, crate::PdfError> {
resolver.stream_data(obj_num, gen_num)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn af_relationship_from_name() {
assert_eq!(AfRelationship::from_name(b"Source"), AfRelationship::Source);
assert_eq!(AfRelationship::from_name(b"Data"), AfRelationship::Data);
assert_eq!(
AfRelationship::from_name(b"Custom"),
AfRelationship::Other("Custom".to_string())
);
}
#[test]
fn decode_mime_name_with_hex_escape() {
assert_eq!(decode_mime_name(b"text#2Fcsv"), "text/csv");
assert_eq!(decode_mime_name(b"application#2Fpdf"), "application/pdf");
assert_eq!(decode_mime_name(b"plain"), "plain");
}
}