diff --git a/crates/xfa/src/fixtures.rs b/crates/xfa/src/fixtures.rs index b075cc811..fb957b3cf 100644 --- a/crates/xfa/src/fixtures.rs +++ b/crates/xfa/src/fixtures.rs @@ -96,6 +96,8 @@ pub fn static_shell(data: &str) -> Vec { assemble(objs) } +/// A PDF file holding `objs` as objects 1, 2, … in order, with a cross-reference table and a +/// trailer whose root is object 1. fn assemble(objs: Vec>) -> Vec { let mut out = b"%PDF-1.7\n".to_vec(); let mut offsets = Vec::new(); diff --git a/crates/xfa/src/script.rs b/crates/xfa/src/script.rs index 1d0dc8a86..f552de778 100644 --- a/crates/xfa/src/script.rs +++ b/crates/xfa/src/script.rs @@ -186,6 +186,9 @@ impl Walker<'_> { true } + /// Record the scripts of the object at `som` as events: each non-empty event script, then its + /// calculate and validate scripts (the validate one with `message`). Once [`MAX_EVENTS`] events + /// are recorded, nothing more is and the form is marked truncated. fn push_events(&mut self, som: &str, scripts: &[Script], calculate: Option<&Script>, validate: Option<&Script>, message: Option<&str>) { if self.events.len() >= MAX_EVENTS { self.truncated = true; @@ -233,6 +236,7 @@ impl Walker<'_> { name.and_then(|_| self.ov.presence.get(som)).cloned().unwrap_or_else(|| presence_name(p).to_string()) } + /// The access of the object at `som`: a script's, or else the template's `a`. fn access(&self, som: &str, a: Access) -> String { self.ov.access.get(som).cloned().unwrap_or_else(|| access_name(a).to_string()) } diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index ab49e26bf..5a7a685a7 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -539,6 +539,9 @@ fn xfa_bytes(doc: &Document) -> Vec { s.decoded_within(1 << 20).unwrap() } +/// Writing field values into the datasets keeps every data node no field binds to as written, +/// changes the bound values in place and adds a missing one beside them. Writing the same values +/// again changes nothing, and a field bound to structured content is reported, not flattened. #[test] fn writing_values_keeps_the_data_no_field_binds_to() { // Regression: the data element was rebuilt from the fields alone, dropping everything else. @@ -579,6 +582,9 @@ fn writing_values_keeps_the_data_no_field_binds_to() { assert!(String::from_utf8(xfa_bytes(&doc)).unwrap().contains("

rich

")); } +/// A UTF-16 packet in either byte order, with a byte order mark, has its values read exactly and +/// is written back as UTF-16 with its mark. A packet in neither UTF-8 nor UTF-16 is left alone, +/// and that is reported. #[test] fn utf16_packets_are_read_and_written_back_as_utf16() { let mut doc = Document::open(Arc::new(static_shell("Zoë 日本ünbound"))).unwrap(); @@ -608,6 +614,9 @@ fn utf16_packets_are_read_and_written_back_as_utf16() { assert_eq!(xfa_bytes(&doc), latin1); } +/// Hundreds of fields with absurd SOM indices finish within seconds, both writing the datasets and +/// building fresh data, create at most the node budget and report what they could not write. A +/// moderate index still works, creating the empty instances before it. #[test] fn huge_som_indices_are_capped_not_built() { // Regression: each index created its missing siblings one count at a time (quadratic), with @@ -647,6 +656,9 @@ fn huge_som_indices_are_capped_not_built() { assert!(d.get(&som_to_path("form1[0].row[2]")).is_some(), "the instances in between exist, empty"); } +/// The live form tree carries each object's SOM path, value, presence and access, one instance of +/// a repeating row without data, and the template's event, calculate and validate scripts (FormCalc +/// when no content type is given). Data adds rows and values, and an override changes presence. #[test] fn the_form_tree_carries_values_presence_instances_and_the_events() { let xml = scripted_template(); @@ -681,6 +693,9 @@ fn the_form_tree_carries_values_presence_instances_and_the_events() { assert_eq!(page.children.iter().find(|c| c.name == "details").unwrap().kind, Some(NodeKind::Subform)); } +/// Presence and access overrides are kept on the AcroForm and applied to the template: `note` has +/// no widget while `details` is hidden and gets one once an override shows it and the form is laid +/// out again. Saving and reopening keeps the overrides and one set of fields; storing none removes the key. #[test] fn overrides_live_in_the_acroform_and_hide_objects_in_the_layout() { let mut doc = Document::open(Arc::new(shell(&scripted_template()))).unwrap(); @@ -739,6 +754,9 @@ fn showing_a_subform_leaves_its_unnamed_hidden_objects_hidden() { assert_eq!(details.children.iter().find(|c| c.kind == Some(NodeKind::Subform)).map(|c| c.presence.as_str()), Some("hidden")); } +/// Adding and removing data instances edits the datasets packet, creating it when missing: asking +/// for no more rows than exist, or removing one that is not there, changes nothing, and removing a +/// row moves the later ones up. The layout follows the data, and an absurd count is capped, not built. #[test] fn data_instances_are_added_and_removed_in_the_packet() { let mut doc = Document::open(Arc::new(shell(&scripted_template()))).unwrap();