M6: a document script that throws no longer stops the form's other scripts (#3940)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mtlcl410
2026-10-23 22:25:57 +08:00
co-authored by Claude Opus 5.5
parent 75207dc8ed
commit e76ebb9ea3
6 changed files with 76 additions and 5 deletions
+7
View File
@@ -31,6 +31,13 @@ impl JsOutput {
self.console.extend(o.console.iter().cloned());
self.requests.extend(o.requests.iter().cloned());
self.errors.extend(o.error.iter().cloned());
// The document scripts run before every field script, so a failing one would repeat
// once per calculation; it is reported once.
for e in &o.doc_errors {
if !self.errors.contains(e) {
self.errors.push(e.clone());
}
}
}
pub(crate) fn append(&mut self, other: JsOutput) {
+27 -1
View File
@@ -4015,6 +4015,11 @@ fn recognize_text_reads_a_picture_on_a_page_that_has_text() {
/// A form whose scripts are custom JavaScript: total = price × qty (calculate, through a
/// document-level function), shown with a custom format; qty is validated; a button script.
fn scripted_form() -> Vec<u8> {
scripted_form_with("function times\\(a, b\\) { console.println('times'); return a * b; }")
}
/// [`scripted_form`] with `helpers` (PDF string syntax) as its document-level script.
fn scripted_form_with(helpers: &str) -> Vec<u8> {
let text = |name: &str, y: u32, aa: &str| {
format!("<< /Type /Annot /Subtype /Widget /FT /Tx /T ({name}) /Rect [20 {y} 180 {}] /P 3 0 R /F 4 {aa} >>", y + 20)
};
@@ -4028,7 +4033,7 @@ fn scripted_form() -> Vec<u8> {
"<< /Type /Annot /Subtype /Widget /FT /Btn /Ff 65536 /T (go) /Rect [20 150 80 170] /P 3 0 R /F 4 >>".to_string(),
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".to_string(),
"<< >>".to_string(),
"<< /S /JavaScript /JS (function times\\(a, b\\) { console.println('times'); return a * b; }) >>".to_string(),
format!("<< /S /JavaScript /JS ({helpers}) >>"),
];
let mut out = b"%PDF-1.7\n".to_vec();
let mut offsets = Vec::new();
@@ -4090,6 +4095,27 @@ fn scripts_in_an_xfa_form_see_the_xfa_plugin_installed() {
assert_eq!((o.error, o.result.as_deref()), (None, Some("undefined number 3.5")));
}
/// #3940: a document-level script that defines the helper and then calls a method PdfCraft lacks
/// (an Acrobat form copying a button icon at open) left every calculated field blank. The
/// helper stays defined, the form calculates, and the error is reported once.
#[test]
fn a_document_script_that_throws_still_lets_the_form_calculate() {
let helpers =
"function times\\(a, b\\) { return a * b; } this.getField\\('go'\\).buttonSetIcon\\(this.getField\\('go'\\).buttonGetIcon\\(\\)\\);";
let mut s = Session::new().with_clock(|| 1_700_000_000);
let id = s.open("order.pdf", None, Arc::new(scripted_form_with(helpers)), None).unwrap();
let _ = s.take_js_output(id);
s.apply(id, Edit::SetFieldValue { name: "qty".into(), value: FieldValue::Text("4".into()) }).unwrap();
let total = s.get(id).unwrap().form.iter().find(|f| f.name == "total").unwrap().value.clone();
assert_eq!(total, ["10"], "calculated through the helper the failing script defined");
let out = s.take_js_output(id);
assert_eq!(out.errors.len(), 1, "reported once, not once per script: {out:?}");
assert!(out.errors[0].starts_with("document script 1: TypeError"), "{out:?}");
// The console runs too, and says which document script failed.
let o = s.run_javascript(id, "times(2, 3)", None).unwrap();
assert_eq!((o.error, o.result.as_deref(), o.doc_errors.len()), (None, Some("6"), 1));
}
#[test]
fn edit_history_does_not_replay_field_script_side_effects() {
let mut s = Session::new().with_clock(|| 1_700_000_000);