use rpdfium_core::{Name, PdfSource};
use rpdfium_parser::{Object, ObjectStore};
use crate::error::{DocError, DocResult};
use crate::name_tree::NameTree;
#[derive(Debug, Clone)]
pub struct JavaScriptAction {
name: String,
script: String,
}
impl JavaScriptAction {
pub fn name(&self) -> &str {
&self.name
}
#[inline]
pub fn javascript_action_get_name(&self) -> &str {
self.name()
}
#[deprecated(
note = "use `javascript_action_get_name()` — matches upstream `FPDFJavaScriptAction_GetName`"
)]
#[inline]
pub fn get_name(&self) -> &str {
self.name()
}
pub fn script(&self) -> &str {
&self.script
}
#[inline]
pub fn javascript_action_get_script(&self) -> &str {
self.script()
}
#[deprecated(
note = "use `javascript_action_get_script()` — matches upstream `FPDFJavaScriptAction_GetScript`"
)]
#[inline]
pub fn get_script(&self) -> &str {
self.script()
}
pub fn close(self) -> crate::error::DocResult<()> {
Err(crate::error::DocError::NotSupported(
"close: JavaScriptAction is RAII-managed; explicit close not supported (ADR-002)"
.into(),
))
}
}
pub fn collect_javascript_actions<S: PdfSource>(
catalog: &Object,
store: &ObjectStore<S>,
) -> DocResult<Vec<JavaScriptAction>> {
let catalog_dict = catalog.as_dict().ok_or(DocError::UnexpectedType)?;
let names_obj = match catalog_dict.get(&Name::names()) {
Some(o) => o,
None => return Ok(Vec::new()),
};
let names_resolved = store
.deep_resolve(names_obj)
.map_err(|e| DocError::Parser(e.to_string()))?;
let names_dict = match names_resolved.as_dict() {
Some(d) => d,
None => return Ok(Vec::new()),
};
let js_obj = match names_dict.get(&Name::java_script()) {
Some(o) => o,
None => return Ok(Vec::new()),
};
let tree = NameTree::<Object>::parse(js_obj, store, |obj| Ok(obj.clone()))?;
let mut actions = Vec::new();
for (name, obj) in tree.entries() {
let resolved = store
.deep_resolve(obj)
.map_err(|e| DocError::Parser(e.to_string()))?;
if let Some(dict) = resolved.as_dict() {
if let Some(script) = extract_js(dict, store) {
actions.push(JavaScriptAction {
name: name.clone(),
script,
});
}
}
}
Ok(actions)
}
fn extract_js<S: PdfSource>(
dict: &std::collections::HashMap<Name, Object>,
store: &ObjectStore<S>,
) -> Option<String> {
let js_obj = dict.get(&Name::js())?;
let resolved = store.deep_resolve(js_obj).ok()?;
if let Some(s) = resolved.as_string() {
return Some(s.to_string_lossy());
}
if resolved.as_stream_dict().is_some() {
if let Ok(data) = store.decode_stream(resolved) {
return Some(String::from_utf8_lossy(&data).into_owned());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use rpdfium_core::PdfString;
use rpdfium_parser::ObjectStore;
use std::collections::HashMap;
fn build_store() -> ObjectStore<Vec<u8>> {
let mut pdf = Vec::new();
pdf.extend_from_slice(b"%PDF-1.4\n");
let o1 = pdf.len();
pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
let o2 = pdf.len();
pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
let xref = pdf.len();
pdf.extend_from_slice(b"xref\n0 3\n");
pdf.extend_from_slice(b"0000000000 65535 f \r\n");
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", o1).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", o2).as_bytes());
pdf.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
pdf.extend_from_slice(format!("startxref\n{}\n%%EOF", xref).as_bytes());
ObjectStore::open(pdf, rpdfium_core::ParsingMode::Lenient).unwrap()
}
fn str_obj(s: &str) -> Object {
Object::String(PdfString::from_bytes(s.as_bytes().to_vec()))
}
#[test]
fn test_empty_catalog_returns_empty() {
let store = build_store();
let catalog = Object::Dictionary(HashMap::new());
let actions = collect_javascript_actions(&catalog, &store).unwrap();
assert!(actions.is_empty());
}
#[test]
fn test_no_names_dict_returns_empty() {
let store = build_store();
let mut catalog_dict = HashMap::new();
catalog_dict.insert(Name::from("Type"), Object::Name(Name::from("Catalog")));
let catalog = Object::Dictionary(catalog_dict);
let actions = collect_javascript_actions(&catalog, &store).unwrap();
assert!(actions.is_empty());
}
#[test]
fn test_single_javascript_action() {
let store = build_store();
let mut action_dict = HashMap::new();
action_dict.insert(Name::s(), Object::Name(Name::java_script()));
action_dict.insert(Name::js(), str_obj("app.alert('hi');"));
let mut js_leaf = HashMap::new();
js_leaf.insert(
Name::names(),
Object::Array(vec![str_obj("myAction"), Object::Dictionary(action_dict)]),
);
let mut names_dict = HashMap::new();
names_dict.insert(Name::java_script(), Object::Dictionary(js_leaf));
let mut catalog_dict = HashMap::new();
catalog_dict.insert(Name::names(), Object::Dictionary(names_dict));
let catalog = Object::Dictionary(catalog_dict);
let actions = collect_javascript_actions(&catalog, &store).unwrap();
assert_eq!(actions.len(), 1);
assert_eq!(actions[0].name(), "myAction");
assert_eq!(actions[0].script(), "app.alert('hi');");
}
#[test]
fn test_multiple_javascript_actions() {
let store = build_store();
let make_action = |script: &str| {
let mut d = HashMap::new();
d.insert(Name::s(), Object::Name(Name::java_script()));
d.insert(Name::js(), str_obj(script));
Object::Dictionary(d)
};
let mut js_leaf = HashMap::new();
js_leaf.insert(
Name::names(),
Object::Array(vec![
str_obj("action1"),
make_action("alert(1);"),
str_obj("action2"),
make_action("alert(2);"),
]),
);
let mut names_dict = HashMap::new();
names_dict.insert(Name::java_script(), Object::Dictionary(js_leaf));
let mut catalog_dict = HashMap::new();
catalog_dict.insert(Name::names(), Object::Dictionary(names_dict));
let catalog = Object::Dictionary(catalog_dict);
let actions = collect_javascript_actions(&catalog, &store).unwrap();
assert_eq!(actions.len(), 2);
assert_eq!(actions[0].script(), "alert(1);");
assert_eq!(actions[1].script(), "alert(2);");
}
#[test]
fn test_accessors_match() {
let action = JavaScriptAction {
name: "test".into(),
script: "1+1;".into(),
};
assert_eq!(action.name(), action.javascript_action_get_name());
assert_eq!(action.script(), action.javascript_action_get_script());
}
#[test]
fn test_close_returns_not_supported() {
let action = JavaScriptAction {
name: "x".into(),
script: "1;".into(),
};
assert!(action.close().is_err());
}
}