rpdfium_doc/
javascript.rs1use rpdfium_core::{Name, PdfSource};
10use rpdfium_parser::{Object, ObjectStore};
11
12use crate::error::{DocError, DocResult};
13use crate::name_tree::NameTree;
14
15#[derive(Debug, Clone)]
20pub struct JavaScriptAction {
21 name: String,
22 script: String,
23}
24
25impl JavaScriptAction {
26 pub fn name(&self) -> &str {
30 &self.name
31 }
32
33 #[inline]
37 pub fn javascript_action_get_name(&self) -> &str {
38 self.name()
39 }
40
41 #[deprecated(
45 note = "use `javascript_action_get_name()` — matches upstream `FPDFJavaScriptAction_GetName`"
46 )]
47 #[inline]
48 pub fn get_name(&self) -> &str {
49 self.name()
50 }
51
52 pub fn script(&self) -> &str {
56 &self.script
57 }
58
59 #[inline]
63 pub fn javascript_action_get_script(&self) -> &str {
64 self.script()
65 }
66
67 #[deprecated(
71 note = "use `javascript_action_get_script()` — matches upstream `FPDFJavaScriptAction_GetScript`"
72 )]
73 #[inline]
74 pub fn get_script(&self) -> &str {
75 self.script()
76 }
77
78 pub fn close(self) -> crate::error::DocResult<()> {
87 Err(crate::error::DocError::NotSupported(
88 "close: JavaScriptAction is RAII-managed; explicit close not supported (ADR-002)"
89 .into(),
90 ))
91 }
92}
93
94pub fn collect_javascript_actions<S: PdfSource>(
106 catalog: &Object,
107 store: &ObjectStore<S>,
108) -> DocResult<Vec<JavaScriptAction>> {
109 let catalog_dict = catalog.as_dict().ok_or(DocError::UnexpectedType)?;
110
111 let names_obj = match catalog_dict.get(&Name::names()) {
112 Some(o) => o,
113 None => return Ok(Vec::new()),
114 };
115
116 let names_resolved = store
117 .deep_resolve(names_obj)
118 .map_err(|e| DocError::Parser(e.to_string()))?;
119
120 let names_dict = match names_resolved.as_dict() {
121 Some(d) => d,
122 None => return Ok(Vec::new()),
123 };
124
125 let js_obj = match names_dict.get(&Name::java_script()) {
126 Some(o) => o,
127 None => return Ok(Vec::new()),
128 };
129
130 let tree = NameTree::<Object>::parse(js_obj, store, |obj| Ok(obj.clone()))?;
131
132 let mut actions = Vec::new();
133 for (name, obj) in tree.entries() {
134 let resolved = store
135 .deep_resolve(obj)
136 .map_err(|e| DocError::Parser(e.to_string()))?;
137 if let Some(dict) = resolved.as_dict() {
138 if let Some(script) = extract_js(dict, store) {
139 actions.push(JavaScriptAction {
140 name: name.clone(),
141 script,
142 });
143 }
144 }
145 }
146
147 Ok(actions)
148}
149
150fn extract_js<S: PdfSource>(
154 dict: &std::collections::HashMap<Name, Object>,
155 store: &ObjectStore<S>,
156) -> Option<String> {
157 let js_obj = dict.get(&Name::js())?;
158 let resolved = store.deep_resolve(js_obj).ok()?;
159 if let Some(s) = resolved.as_string() {
161 return Some(s.to_string_lossy());
162 }
163 if resolved.as_stream_dict().is_some() {
165 if let Ok(data) = store.decode_stream(resolved) {
166 return Some(String::from_utf8_lossy(&data).into_owned());
167 }
168 }
169 None
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use rpdfium_core::PdfString;
176 use rpdfium_parser::ObjectStore;
177 use std::collections::HashMap;
178
179 fn build_store() -> ObjectStore<Vec<u8>> {
180 let mut pdf = Vec::new();
181 pdf.extend_from_slice(b"%PDF-1.4\n");
182 let o1 = pdf.len();
183 pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
184 let o2 = pdf.len();
185 pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
186 let xref = pdf.len();
187 pdf.extend_from_slice(b"xref\n0 3\n");
188 pdf.extend_from_slice(b"0000000000 65535 f \r\n");
189 pdf.extend_from_slice(format!("{:010} 00000 n \r\n", o1).as_bytes());
190 pdf.extend_from_slice(format!("{:010} 00000 n \r\n", o2).as_bytes());
191 pdf.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
192 pdf.extend_from_slice(format!("startxref\n{}\n%%EOF", xref).as_bytes());
193 ObjectStore::open(pdf, rpdfium_core::ParsingMode::Lenient).unwrap()
194 }
195
196 fn str_obj(s: &str) -> Object {
197 Object::String(PdfString::from_bytes(s.as_bytes().to_vec()))
198 }
199
200 #[test]
201 fn test_empty_catalog_returns_empty() {
202 let store = build_store();
203 let catalog = Object::Dictionary(HashMap::new());
204 let actions = collect_javascript_actions(&catalog, &store).unwrap();
205 assert!(actions.is_empty());
206 }
207
208 #[test]
209 fn test_no_names_dict_returns_empty() {
210 let store = build_store();
211 let mut catalog_dict = HashMap::new();
212 catalog_dict.insert(Name::from("Type"), Object::Name(Name::from("Catalog")));
214 let catalog = Object::Dictionary(catalog_dict);
215 let actions = collect_javascript_actions(&catalog, &store).unwrap();
216 assert!(actions.is_empty());
217 }
218
219 #[test]
220 fn test_single_javascript_action() {
221 let store = build_store();
222
223 let mut action_dict = HashMap::new();
225 action_dict.insert(Name::s(), Object::Name(Name::java_script()));
226 action_dict.insert(Name::js(), str_obj("app.alert('hi');"));
227
228 let mut js_leaf = HashMap::new();
230 js_leaf.insert(
231 Name::names(),
232 Object::Array(vec![str_obj("myAction"), Object::Dictionary(action_dict)]),
233 );
234
235 let mut names_dict = HashMap::new();
236 names_dict.insert(Name::java_script(), Object::Dictionary(js_leaf));
237
238 let mut catalog_dict = HashMap::new();
239 catalog_dict.insert(Name::names(), Object::Dictionary(names_dict));
240
241 let catalog = Object::Dictionary(catalog_dict);
242 let actions = collect_javascript_actions(&catalog, &store).unwrap();
243 assert_eq!(actions.len(), 1);
244 assert_eq!(actions[0].name(), "myAction");
245 assert_eq!(actions[0].script(), "app.alert('hi');");
246 }
247
248 #[test]
249 fn test_multiple_javascript_actions() {
250 let store = build_store();
251
252 let make_action = |script: &str| {
253 let mut d = HashMap::new();
254 d.insert(Name::s(), Object::Name(Name::java_script()));
255 d.insert(Name::js(), str_obj(script));
256 Object::Dictionary(d)
257 };
258
259 let mut js_leaf = HashMap::new();
260 js_leaf.insert(
261 Name::names(),
262 Object::Array(vec![
263 str_obj("action1"),
264 make_action("alert(1);"),
265 str_obj("action2"),
266 make_action("alert(2);"),
267 ]),
268 );
269
270 let mut names_dict = HashMap::new();
271 names_dict.insert(Name::java_script(), Object::Dictionary(js_leaf));
272
273 let mut catalog_dict = HashMap::new();
274 catalog_dict.insert(Name::names(), Object::Dictionary(names_dict));
275
276 let catalog = Object::Dictionary(catalog_dict);
277 let actions = collect_javascript_actions(&catalog, &store).unwrap();
278 assert_eq!(actions.len(), 2);
279 assert_eq!(actions[0].script(), "alert(1);");
280 assert_eq!(actions[1].script(), "alert(2);");
281 }
282
283 #[test]
284 fn test_accessors_match() {
285 let action = JavaScriptAction {
286 name: "test".into(),
287 script: "1+1;".into(),
288 };
289 assert_eq!(action.name(), action.javascript_action_get_name());
290 assert_eq!(action.script(), action.javascript_action_get_script());
291 }
292
293 #[test]
294 fn test_close_returns_not_supported() {
295 let action = JavaScriptAction {
296 name: "x".into(),
297 script: "1;".into(),
298 };
299 assert!(action.close().is_err());
300 }
301}