//! The data model (`datasets` packet, XFA 3.3 part 1, "Data Binding"): reading the values a //! form was saved with, and writing the values of its fields back, so Adobe's viewers (which //! lay the form out from the XFA packets) show what was filled in here. //! //! Binding is the default "normal" one: a field's data node is named after the field and nested //! under nodes named after its named ancestor subforms, repeated instances as repeated sibling //! elements. Areas are named in SOM expressions but are not data scopes, so their contents bind //! under the area's parent ([`data_path`]). Explicit `bind ref` expressions are not followed. use std::collections::HashMap; use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream}; use crate::XfaError; use crate::model::{Field, Node, SomIndices, Template, Ui}; use crate::packets::{Encoding, encode}; const XFA_DATA_NS: &str = "http://www.xfa.org/schema/xfa-data/1.0/"; /// Most data nodes read. const MAX_NODES: u32 = 1_000_000; const MAX_DEPTH: usize = 64; /// One element of the data document. #[derive(Clone, Debug, Default, PartialEq)] pub struct DataNode { pub name: String, pub text: String, pub children: Vec, } /// A path into the data: `(name, index among same-named siblings)` per level. pub type DataPath = Vec<(String, usize)>; impl DataNode { /// The node at `path` below this one. pub fn get(&self, path: &[(String, usize)]) -> Option<&DataNode> { let mut node = self; for (name, idx) in path { node = node.children.iter().filter(|c| &c.name == name).nth(*idx)?; } Some(node) } /// How many children of the node at `parent` are named `name`. pub fn count(&self, parent: &[(String, usize)], name: &str) -> usize { self.get(parent).map_or(0, |n| n.children.iter().filter(|c| c.name == name).count()) } pub fn text_at(&self, path: &[(String, usize)]) -> Option<&str> { self.get(path).map(|n| n.text.as_str()) } /// The value at `path`: its text, or, for a container whose children are all `value` /// elements (one way a multi-select list's picks are stored), those on separate lines. pub fn value_at(&self, path: &[(String, usize)]) -> Option { let n = self.get(path)?; if n.children.is_empty() || !n.children.iter().all(|c| c.name == "value") { return Some(n.text.clone()); } Some(n.children.iter().map(|c| c.text.trim()).filter(|t| !t.is_empty()).take(1000).collect::>().join("\n")) } } fn to_node(n: roxmltree::Node, depth: usize) -> DataNode { let name = n.tag_name().name().to_string(); let children: Vec = if depth < MAX_DEPTH { n.children().filter(|c| c.is_element()).map(|c| to_node(c, depth + 1)).collect() } else { Vec::new() }; // The text of a leaf is its value; a container's own text is whitespace. let text = if children.is_empty() { n.children().filter_map(|c| c.text()).collect::() } else { String::new() }; DataNode { name, text, children } } /// The `xfa:data` element of the datasets packet in `xdp` (or a bare datasets document), as a /// tree whose root is that element. pub fn parse_datasets(xdp: &str) -> Option { if crate::xml_nesting_depth(xdp) > crate::MAX_XML_DEPTH { return None; } let opts = roxmltree::ParsingOptions { allow_dtd: false, nodes_limit: MAX_NODES }; let doc = roxmltree::Document::parse_with_options(xdp, opts).ok()?; let datasets = doc.descendants().find(|n| n.is_element() && n.tag_name().name() == "datasets")?; let data = datasets.children().find(|n| n.is_element() && n.tag_name().name() == "data")?; Some(to_node(data, 0)) } /// The data path a SOM expression names: `form[0].#subform[0].name[1]` → `[(form, 0), (name, 1)]`. /// Unnamed containers (`#subform`) have no data node. pub fn som_to_path(som: &str) -> DataPath { som.split('.') .filter_map(|seg| { let seg = seg.trim(); if seg.is_empty() || seg.starts_with('#') || seg.starts_with('$') { return None; } let (name, idx) = match seg.split_once('[') { Some((n, rest)) => (n, rest.trim_end_matches(']').parse::().unwrap_or(0)), None => (seg, 0), }; Some((name.to_string(), idx.min(100_000))) }) .collect() } /// The data path the object at `som` binds to: [`som_to_path`] without the areas on the way. /// An area is a named container in the form, so it is part of SOM expressions, but only /// subforms, exclusion groups and fields are data scopes: what an area holds binds under the /// area's parent, as Adobe's viewers write it. The template tells which segments are areas; /// segments it doesn't have (page-set objects, or a SOM from another template) are kept. pub fn data_path(tpl: &Template, som: &str) -> DataPath { let mut out = DataPath::new(); // The template children the next segment is looked up in; `None` until the root matched, // and again once a segment is not a container the template has. let mut level: Option<&[Node]> = None; for (i, (name, index)) in som_to_path(som).into_iter().enumerate() { let found = match level { None if i == 0 && tpl.root.common.name.as_deref() == Some(name.as_str()) => Some(Container::Children(&tpl.root.children)), None => None, Some(children) => match named(children, &name, 0) { Some(Node::Subform(sf)) => Some(Container::Children(&sf.children)), Some(Node::Area(a)) => Some(Container::Area(&a.children)), _ => None, }, }; match found { Some(Container::Area(children)) => level = Some(children), Some(Container::Children(children)) => { level = Some(children); out.push((name, index)); } None => { level = None; out.push((name, index)); } } } out } enum Container<'a> { Children(&'a [Node]), /// Contents bind under the area's parent. Area(&'a [Node]), } /// The child of `nodes` named `name`, looking through unnamed subforms and areas (which have no /// SOM segment of their own). fn named<'a>(nodes: &'a [Node], name: &str, depth: usize) -> Option<&'a Node> { if depth > MAX_DEPTH { return None; } nodes.iter().find(|n| n.common().name.as_deref() == Some(name)).or_else(|| { nodes.iter().find_map(|n| match n { Node::Subform(sf) if sf.common.name.is_none() => named(&sf.children, name, depth + 1), Node::Area(a) if a.common.name.is_none() => named(&a.children, name, depth + 1), _ => None, }) }) } /// The date letters (`yyyy-mm-dd`, `yyyymmdd`) of an XFA date picture clause such as /// `date{YYYY-MM-DD}`, or `None` when it has none this reads (empty, or with symbols beyond /// years, months, days and separators). pub(crate) fn date_letters(picture: &str) -> Option { let inner = picture .find('{') .and_then(|a| { let rest = picture.get(a + 1..)?; rest.find('}').and_then(|b| rest.get(..b)) }) .unwrap_or(picture) .trim(); if inner.is_empty() || inner.chars().any(|c| !"YMDEJ-/., :".contains(c)) { return None; } Some(inner.replace('E', "d").to_ascii_lowercase()) } /// The date letters of `f`'s data picture, when it holds a whole date. A data picture without a /// year, a month or a day can't hold the date the field shows, so such a field's data keeps /// ISO dates. pub(crate) fn data_date_letters(f: &Field) -> Option { let letters = f.data_picture.as_deref().and_then(date_letters)?; let tokens = date_tokens(&letters); let has = |any: &[&str]| tokens.iter().any(|t| any.contains(&t.as_str())); (has(&["yyyy", "yy"]) && has(&["mm", "m"]) && has(&["dd", "d"])).then_some(letters) } /// A date as the data holds it, written in `data_pattern` (the field's data picture), read as /// an ISO date. A value that doesn't match, or a field without a data picture, is taken as it /// is: the data holds ISO dates by default. pub(crate) fn data_to_iso(value: &str, data_pattern: Option<&str>) -> String { data_pattern.and_then(|p| pattern_to_iso(value, p)).unwrap_or_else(|| value.to_string()) } /// An ISO date as the data should hold it: in `data_pattern` when the field has one and the /// date reads back from it unchanged (a two-digit year holds only 1950 to 2049), else as ISO. pub(crate) fn iso_to_data(iso: String, data_pattern: Option<&str>) -> String { match data_pattern { Some(p) => { let written = iso_to_pattern(&iso, p); if pattern_to_iso(&written, p).as_deref() == Some(iso.as_str()) { written } else { iso } } None => iso, } } /// The child of `nodes` the SOM segment `name[index]` names, numbered as the form numbers its /// objects ([`SomIndices`]): same-named siblings count up, and the instances of a repeating /// subform take the numbers from its own on. Unnamed subforms and areas are looked through, /// as [`named`] does. fn named_at<'a>(nodes: &'a [Node], name: &str, index: usize, depth: usize) -> Option<&'a Node> { if depth > MAX_DEPTH { return None; } let sibs = SomIndices::of(nodes); let mut repeating = None; for (i, n) in nodes.iter().enumerate() { if n.common().name.as_deref() != Some(name) { continue; } let sib = sibs.at(i); if sib == index { return Some(n); } if sib < index && matches!(n, Node::Subform(sf) if sf.occur.max != Some(1)) { repeating = Some(n); } } repeating.or_else(|| { nodes.iter().find_map(|n| match n { Node::Subform(sf) if sf.common.name.is_none() => named_at(&sf.children, name, index, depth + 1), Node::Area(a) if a.common.name.is_none() => named_at(&a.children, name, index, depth + 1), _ => None, }) }) } /// The template field the object at a SOM path is an instance of, each segment's number counted /// as the form counts it ([`named_at`]). `None` when the template has no such field, or it is /// a button of an exclusion group (whose buttons take their value from the group). pub(crate) fn template_field<'a>(tpl: &'a Template, som: &str) -> Option<&'a Field> { let path = som_to_path(som); let ((root, _), rest) = path.split_first()?; if tpl.root.common.name.as_deref() != Some(root.as_str()) { return None; } let ((last, last_index), inner) = rest.split_last()?; let mut children: &[Node] = &tpl.root.children; for (name, index) in inner { children = match named_at(children, name, *index, 0)? { Node::Subform(sf) => &sf.children, Node::Area(a) => &a.children, _ => return None, }; } match named_at(children, last, *last_index, 0)? { Node::Field(f) => Some(f), _ => None, } } /// The date letters of the data picture of the field `som` names, when it has one. fn data_date_pattern(tpl: Option<&Template>, som: &str) -> Option { tpl.and_then(|t| template_field(t, som)).and_then(data_date_letters) } /// What a field holds, for writing the datasets. #[derive(Clone, Debug, PartialEq)] pub enum FieldData { Text(String), /// Checked or not. Check(bool), /// The selected button's on value, if any. Radio(Option), /// Nothing to write (buttons, signatures). None, } /// One field of the AcroForm, as the engine sees it. #[derive(Clone, Debug, PartialEq)] pub struct FieldDatum { /// The terminal field dictionary (where `/PCSom` and `/PCItems` live). pub obj: ObjRef, /// The fully qualified field name (a static form's SOM path). pub name: String, pub data: FieldData, } fn text_of(doc: &Document, d: &Dict, key: &[u8]) -> Option { d.get(key).map(|o| doc.resolve(o)).and_then(|o| o.as_string().map(|s| s.to_text())) } /// The SOM path of a field: its `/PCSom` when PdfCraft generated it, else its name. fn som_of(doc: &Document, f: &FieldDatum) -> String { doc.get(f.obj).as_dict().and_then(|d| text_of(doc, d, crate::pdf::SOM_KEY)).unwrap_or_else(|| f.name.clone()) } /// The datasets text for a radio group. The AcroForm value is an appearance-state name /// (`New York` is stored as `New_York`); `/PCOn` on the selected widget is the original item. fn radio_dataset_value(doc: &Document, f: &FieldDatum, sel: &Option) -> String { let Some(selected) = sel.as_deref().filter(|s| !s.is_empty() && *s != "Off") else { return String::new(); }; let Some(group) = doc.get(f.obj).as_dict().cloned() else { return selected.to_string() }; let kids = group.get(b"Kids").map(|k| doc.resolve(k)).and_then(|k| k.as_array().cloned()).unwrap_or_else(|| vec![Object::Ref(f.obj)]); for kid in kids { let Some(widget) = doc.resolve(&kid).as_dict().cloned() else { continue }; let original = text_of(doc, &widget, b"PCOn"); let state = widget .get(b"AP") .map(|ap| doc.resolve(ap)) .and_then(|ap| ap.as_dict().and_then(|a| a.get(b"N").cloned())) .and_then(|n| doc.resolve(&n).as_dict().cloned()) .and_then(|n| n.iter().map(|(k, _)| String::from_utf8_lossy(k).into_owned()).find(|k| k != "Off")); if state.as_deref() == Some(selected) || original.as_deref() == Some(selected) { return original.unwrap_or_else(|| selected.to_string()); } } selected.to_string() } /// The on and off item values of a check box (`/PCItems`), or the widget's on state. fn items_of(doc: &Document, f: &FieldDatum) -> (String, String) { let d = doc.get(f.obj); let items: Vec = d .as_dict() .and_then(|d| d.get(b"PCItems").map(|o| doc.resolve(o))) .and_then(|a| a.as_array().map(|a| a.iter().filter_map(|x| x.as_string().map(|s| s.to_text())).collect())) .unwrap_or_default(); let on = items.first().cloned().unwrap_or_else(|| "1".into()); let off = items.get(1).cloned().unwrap_or_default(); (on, off) } /// The Acrobat date pattern a field's format action names (`AFDate_FormatEx("yyyy-mm-dd")`). fn date_pattern_of(doc: &Document, f: &FieldDatum) -> Option { let d = doc.get(f.obj); let aa = doc.resolve(d.as_dict()?.get(b"AA")?); let fa = doc.resolve(aa.as_dict()?.get(b"F")?); let js = text_of(doc, fa.as_dict()?, b"JS")?; let i = js.find("AFDate_FormatEx(")?; let rest = &js[i + "AFDate_FormatEx(".len()..]; let q = rest.find('"')?; let body = &rest[q + 1..]; Some(body[..body.find('"')?].to_string()) } const MONTHS: [&str; 12] = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; const WEEKDAYS: [&str; 7] = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; /// The weekday (0 = Sunday) of a proleptic Gregorian date. fn weekday(y: u32, m: u32, d: u32) -> usize { const T: [i64; 12] = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4]; let y = i64::from(y) - i64::from(m < 3); let t = T.get((m as usize).wrapping_sub(1)).copied().unwrap_or(0); ((y + y / 4 - y / 100 + y / 400 + t + i64::from(d)).rem_euclid(7)) as usize } /// The index and length of the name in `names` that `text` starts with (ignoring case), /// cut to `abbr` letters when that is set. fn name_at(text: &str, names: &[&str], abbr: bool) -> Option<(usize, usize)> { names.iter().enumerate().find_map(|(i, n)| { let n = if abbr { n.get(..3)? } else { n }; text.get(..n.len()).filter(|t| t.eq_ignore_ascii_case(n)).map(|_| (i, n.len())) }) } /// Whether `token` is a run of the date letters (`y`, `m`, `d`) rather than a literal. fn is_date_run(token: &str) -> bool { token.starts_with(['y', 'm', 'd']) } /// The tokens of a date pattern: runs of `y`, `m` and `d` (`yyyy`, `yy`, `mmmm`, `mmm`, `mm`, /// `m`, `dddd`, `ddd`, `dd`, `d` are understood), or a literal character. pub(crate) fn date_tokens(pattern: &str) -> Vec { let mut out = Vec::new(); let chars: Vec = pattern.chars().collect(); let mut i = 0; while i < chars.len() { let c = chars[i].to_ascii_lowercase(); if "ymd".contains(c) { let mut j = i; while j < chars.len() && chars[j].to_ascii_lowercase() == c { j += 1; } out.push(std::iter::repeat_n(c, j - i).collect()); i = j; } else { out.push(chars[i].to_string()); i += 1; } } out } /// `2001-02-03` shown in `pattern` (`mm/dd/yyyy` → `02/03/2001`, `mmm d, yyyy` → /// `Feb 3, 2001`). Patterns with a token this doesn't know and non-dates come back as they are. pub fn iso_to_pattern(iso: &str, pattern: &str) -> String { let parts: Vec<&str> = iso.trim().splitn(3, '-').collect(); let [y, m, d] = parts[..] else { return iso.to_string() }; let (Ok(y), Ok(m), Ok(d)) = (y.parse::(), m.parse::(), d.parse::()) else { return iso.to_string() }; let mut out = String::new(); for t in date_tokens(pattern) { match t.as_str() { "yyyy" => out.push_str(&format!("{y:04}")), "yy" => out.push_str(&format!("{:02}", y % 100)), "mm" => out.push_str(&format!("{m:02}")), "m" => out.push_str(&m.to_string()), "dd" => out.push_str(&format!("{d:02}")), "d" => out.push_str(&d.to_string()), "mmm" | "mmmm" => { let Some(name) = m.checked_sub(1).and_then(|i| MONTHS.get(i as usize)) else { return iso.to_string() }; match t.len() { 3 => out.push_str(name.get(..3).unwrap_or(name)), _ => out.push_str(name), } } "ddd" | "dddd" => { let name = WEEKDAYS.get(weekday(y, m, d)).copied().unwrap_or_default(); match t.len() { 3 => out.push_str(name.get(..3).unwrap_or(name)), _ => out.push_str(name), } } other if is_date_run(other) => return iso.to_string(), other => out.push_str(other), } } out } /// A date typed as `pattern` back to `yyyy-mm-dd`. `None` when it doesn't match. pub fn pattern_to_iso(text: &str, pattern: &str) -> Option { let text = text.trim(); let (mut y, mut m, mut d) = (None, None, None); let mut day_name = None; let mut rest = text; let tokens = date_tokens(pattern); for (i, t) in tokens.iter().enumerate() { match t.as_str() { "yyyy" | "yy" | "mm" | "m" | "dd" | "d" => { // Digits up to the next literal (or a fixed width). let fixed = matches!(t.as_str(), "yyyy" | "yy" | "mm" | "dd"); let width = if fixed { t.len() } else { rest.chars().take_while(char::is_ascii_digit).count().min(if t == "d" || t == "m" { 2 } else { 4 }) }; let digits: String = rest.chars().take(width).collect(); if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) { return None; } let v: u32 = digits.parse().ok()?; rest = &rest[digits.len()..]; match t.as_str() { "yyyy" => y = Some(v), "yy" => y = Some(if v < 50 { 2000 + v } else { 1900 + v }), "mm" | "m" => m = Some(v), _ => d = Some(v), } } "mmm" | "mmmm" => { let (i, len) = name_at(rest, &MONTHS, t.len() == 3)?; m = Some(i as u32 + 1); rest = rest.get(len..)?; } "ddd" | "dddd" => { let (i, len) = name_at(rest, &WEEKDAYS, t.len() == 3)?; day_name = Some(i); rest = rest.get(len..)?; } run if is_date_run(run) => return None, lit => { rest = rest.strip_prefix(lit)?; } } let _ = i; } if !rest.is_empty() { return None; } let (y, m, d) = (y?, m?, d?); if day_name.is_some_and(|w| w != weekday(y, m, d)) { return None; } (1..=12).contains(&m).then(|| format!("{y:04}-{m:02}-{d:02}")).filter(|_| (1..=31).contains(&d)) } fn xml_name(s: &str) -> String { let mut out: String = s.chars().map(|c| if c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' { c } else { '_' }).collect(); if out.is_empty() || !(out.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')) { out.insert(0, '_'); } out } /// `s` as XML character data. Characters XML 1.0 cannot hold are left out; the flag says /// whether any were. fn escape(s: &str) -> (String, bool) { let mut out = String::with_capacity(s.len()); let mut dropped = false; for c in s.chars() { match c { '&' => out.push_str("&"), '<' => out.push_str("<"), '>' => out.push_str(">"), '"' => out.push_str("""), // Parsers turn a raw carriage return into a line feed; a reference keeps it. '\r' => out.push_str(" "), '\u{FFFE}' | '\u{FFFF}' => dropped = true, c if c.is_control() && c != '\n' && c != '\t' => dropped = true, c => out.push(c), } } (out, dropped) } /// Most data nodes one write may create (instances skipped by a SOM index count too). const MAX_NEW_NODES: usize = 20_000; /// Highest same-name sibling index a write follows. const MAX_INDEX: usize = 10_000; /// Most bytes of names the nodes one write creates hold together: every sibling an index /// skips over gets its own copy of the name. const MAX_NEW_NAME_BYTES: usize = 4 << 20; /// What one write may still create: nodes and bytes of their names. struct NewNodes { nodes: usize, name_bytes: usize, } impl NewNodes { fn new() -> NewNodes { NewNodes { nodes: MAX_NEW_NODES, name_bytes: MAX_NEW_NAME_BYTES } } } /// A data tree being built for writing. #[derive(Default)] struct Build { name: String, text: String, children: Vec, /// Positions in `children` by name, in order. by_name: HashMap>, } impl Build { /// The node at `path` below this one, created as needed. `None` when the path is deeper /// than [`MAX_DEPTH`], an index is above [`MAX_INDEX`], or creating it would spend more /// new nodes or name bytes than `budget` holds. fn at(&mut self, path: &[(String, usize)], budget: &mut NewNodes) -> Option<&mut Build> { if path.len() > MAX_DEPTH { return None; } let mut node = self; for (name, idx) in path { if *idx > MAX_INDEX { return None; } let name = xml_name(name); let have = node.by_name.get(&name).map_or(0, Vec::len); if *idx >= have { let need = idx - have + 1; let bytes = need.saturating_mul(name.len()); if need > budget.nodes || bytes > budget.name_bytes { return None; } budget.nodes -= need; budget.name_bytes -= bytes; let start = node.children.len(); node.children.extend((0..need).map(|_| Build { name: name.clone(), ..Default::default() })); node.by_name.entry(name.clone()).or_default().extend(start..start + need); } let pos = *node.by_name.get(&name)?.get(*idx)?; node = node.children.get_mut(pos)?; } Some(node) } /// The tree as XML. Depth is bounded by [`Build::at`]. fn write(&self, out: &mut String) { if self.children.is_empty() { out.push_str(&format!("<{}>{}", self.name, escape(&self.text).0, self.name)); return; } out.push_str(&format!("<{}>", self.name)); self.write_children(out); out.push_str(&format!("", self.name)); } fn write_children(&self, out: &mut String) { for c in &self.children { c.write(out); } } } /// A value to write: where (the field's SOM path, and the data path it binds to) and what. struct Want { som: String, path: DataPath, text: String, /// Create the node even for an empty value (an instance of a repeating subform). ensure: bool, } /// `tpl`, or the document's own template when none is given (parsed once). fn with_template(doc: &Document, tpl: Option<&Template>, f: impl FnOnce(Option<&Template>) -> T) -> T { match tpl { Some(t) => f(Some(t)), None => { let parsed = crate::script::template_of(doc).ok(); f(parsed.as_ref()) } } } /// [`data_path`] when the template is known, else [`som_to_path`]. fn path_of(tpl: Option<&Template>, som: &str) -> DataPath { match tpl { Some(t) => data_path(t, som), None => som_to_path(som), } } /// The values the datasets should hold for `fields`, by data path: dates in each field's data /// picture (or ISO), check and radio states, choice values as saved. fn wanted(doc: &Document, fields: &[FieldDatum], tpl: Option<&Template>) -> Vec { let mut out = Vec::new(); for f in fields { let som = som_of(doc, f); let path = path_of(tpl, &som); if path.is_empty() { continue; } let text = match &f.data { FieldData::Text(t) => match date_pattern_of(doc, f) { Some(p) => match pattern_to_iso(t, &p) { // The date goes into the data in the field's data picture, or as an ISO date. Some(iso) => iso_to_data(iso, data_date_pattern(tpl, &som).as_deref()), None => t.clone(), }, None => t.clone(), }, FieldData::Check(on) => { let (on_item, off_item) = items_of(doc, f); if *on { on_item } else { off_item } } FieldData::Radio(sel) => radio_dataset_value(doc, f, sel), FieldData::None => continue, }; out.push(Want { som, path, text, ensure: false }); } out } /// What a rewrite of the datasets packet does. #[derive(Clone, Copy)] enum Op<'a> { /// Put these values in (adding nodes as needed). Values(&'a [Want]), /// Take the node at this path out. Remove(&'a DataPath), /// Make these nodes the whole content of the `xfa:data` element (an import). ReplaceData(&'a [ImportNode]), /// Make the leaf at this path a picture: base64 text with its content type. Picture(&'a DataPath, &'a str, &'a str), } /// Note that a value could not be written (once per kind of reason, naming a few fields). fn skipped(warnings: &mut Vec, reason: &str, som: &str) { const MAX_WARNINGS: usize = 20; if let Some(w) = warnings.iter_mut().find(|w| w.starts_with(reason)) { if w.matches(", ").count() < 4 { w.push_str(&format!(", {som}")); } else if !w.ends_with('…') { w.push_str(", …"); } } else if warnings.len() < MAX_WARNINGS { warnings.push(format!("{reason}: {som}")); } } const TOO_MANY: &str = "XFA data not written for fields whose data path is too deep or repeats too often"; const STRUCTURED: &str = "XFA data not written for fields whose data node holds structured content"; const CONFLICT: &str = "XFA data not written for fields bound to the same data node as another field"; const CONTROL: &str = "Characters XML can't hold were left out of the XFA data of"; /// Fresh data for `wants`: the children of an `xfa:data` element. fn build_children(wants: &[Want], warnings: &mut Vec) -> String { let mut root = Build::default(); let mut budget = NewNodes::new(); for w in wants { match root.at(&w.path, &mut budget) { Some(node) => node.text = w.text.clone(), None => skipped(warnings, TOO_MANY, &w.som), } if escape(&w.text).1 { skipped(warnings, CONTROL, &w.som); } } let mut out = String::new(); root.write_children(&mut out); out } /// The `xfa:data` element for `fields`, built afresh. pub fn build_data(doc: &Document, fields: &[FieldDatum]) -> String { let wants = with_template(doc, None, |t| wanted(doc, fields, t)); format!("{}", build_children(&wants, &mut Vec::new())) } fn fresh_datasets(data_children: &str) -> String { format!("{data_children}") } /// Where an element's start tag ends (the index of its `>`), skipping quoted attribute values. fn start_tag_end(src: &str, range: &std::ops::Range) -> Option { let bytes = src.as_bytes(); let mut quote = None; for i in range.clone() { let b = *bytes.get(i)?; match (quote, b) { (Some(q), _) if b == q => quote = None, (Some(_), _) => {} (None, b'"' | b'\'') => quote = Some(b), (None, b'>') => return Some(i), _ => {} } } None } /// An element's qualified name as written. fn qname<'s>(src: &'s str, range: &std::ops::Range) -> Option<&'s str> { let rest = src.get(range.start.checked_add(1)?..range.end)?; let end = rest.find(|c: char| c.is_whitespace() || c == '/' || c == '>')?; rest.get(..end).filter(|n| !n.is_empty()) } /// One change to the packet text: replace `start..end` with `text`. struct Edit { start: usize, end: usize, text: String, } /// The edit that puts `inner` (markup) at the end of `node`'s content. fn append_edit(src: &str, node: roxmltree::Node, inner: &str) -> Option { let range = node.range(); let tag_end = start_tag_end(src, &range)?; if tag_end.checked_add(1)? == range.end && src.get(..tag_end)?.ends_with('/') { // `` becomes `inner`. let name = qname(src, &range)?; return Some(Edit { start: tag_end.checked_sub(1)?, end: range.end, text: format!(">{inner}") }); } let close = range.start + src.get(range.clone())?.rfind(" Option { let range = node.range(); let tag_end = start_tag_end(src, &range)?; if tag_end.checked_add(1)? == range.end && src.get(..tag_end)?.ends_with('/') { let name = qname(src, &range)?; return Some(Edit { start: tag_end.checked_sub(1)?, end: range.end, text: format!(">{text}") }); } let close = range.start + src.get(range.clone())?.rfind(" tag_end).then(|| Edit { start: tag_end + 1, end: close, text: text.to_string() }) } /// What merging the values into a packet's text came to. enum Merged { /// The data already holds every value. Unchanged, Changed(String), /// The text could not be merged into (the reason is in the warnings). Failed, } /// Where a datasets packet sits: one packet of an array (`Packet`), or inside the whole XDP. #[derive(Clone, Copy, PartialEq)] enum Within { Packet, Xdp, } const WRAP_OPEN: &str = ""; const WRAP_CLOSE: &str = ""; /// Write `wants` into the data of the datasets packet in `text`, changing only what has to /// change: values go into the data nodes they bind to, missing nodes are added, and everything /// else (data no field binds to, other namespaces, attributes, comments, layout) stays as /// written. /// The prefix bound to the XFA data namespace where `n` is (`None`: unbound or the default /// namespace, which attributes can't use). fn xfa_prefix(n: roxmltree::Node) -> Option { n.namespaces().find(|ns| ns.uri() == XFA_DATA_NS).and_then(|ns| ns.name()).filter(|p| !p.is_empty()).map(str::to_string) } /// Data read from a file, held as a tree so it can be written into any packet with the /// prefix that packet uses, never as the file's own markup. #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct ImportNode { pub name: String, /// The element's namespace (`None`: none). pub ns: Option, /// `(namespace, name, value)`. pub attrs: Vec<(Option, String, String)>, pub text: String, pub children: Vec, } /// Deepest data an import keeps. const MAX_IMPORT_DEPTH: usize = MAX_DEPTH; /// Most bytes of names, namespaces, attributes and text an import copies out of its file: a /// namespace declared once is copied into every element and attribute that inherits it. pub(crate) const MAX_IMPORT_BYTES: usize = 4 * MAX_IMPORT; /// What an import may still copy: nodes and bytes. struct ImportBudget { nodes: usize, bytes: usize, } impl ImportBudget { fn new() -> ImportBudget { ImportBudget { nodes: MAX_NODES as usize, bytes: MAX_IMPORT_BYTES } } fn take_bytes(&mut self, n: usize) -> Result<(), XfaError> { self.bytes = self .bytes .checked_sub(n) .ok_or_else(|| XfaError::TooLarge(format!("the data file expands to more than {} MB", MAX_IMPORT_BYTES >> 20)))?; Ok(()) } } /// `n` as an [`ImportNode`], counting nodes and copied bytes against `budget`; `TooLarge` past /// the depth or either budget. fn import_node(n: roxmltree::Node, depth: usize, budget: &mut ImportBudget) -> Result { if depth > MAX_IMPORT_DEPTH { return Err(XfaError::TooLarge(format!("the data file nests more than {MAX_IMPORT_DEPTH} levels deep"))); } budget.nodes = budget.nodes.checked_sub(1).ok_or_else(|| XfaError::TooLarge("the data file holds too many nodes".into()))?; let name = n.tag_name().name(); let ns = n.tag_name().namespace(); budget.take_bytes(name.len().saturating_add(ns.map_or(0, str::len)))?; let mut attrs = Vec::new(); for a in n.attributes().filter(|a| a.name() != "xmlns" && a.namespace() != Some("http://www.w3.org/2000/xmlns/")) { budget.take_bytes(a.namespace().map_or(0, str::len).saturating_add(a.name().len()).saturating_add(a.value().len()))?; attrs.push((a.namespace().map(str::to_string), a.name().to_string(), a.value().to_string())); } let mut children = Vec::new(); let mut text = String::new(); for c in n.children() { if c.is_element() { children.push(import_node(c, depth + 1, budget)?); } else if let Some(t) = c.text() { budget.take_bytes(t.len())?; text.push_str(t); } } // A group's own text is layout whitespace. if !children.is_empty() && text.trim().is_empty() { text.clear(); } Ok(ImportNode { name: name.to_string(), ns: ns.map(str::to_string), attrs, text, children }) } /// `nodes` as markup for a packet whose XFA data namespace prefix is `xfa` (declared where /// they go); other namespaces are declared on the elements and attributes that use them. fn serialize_nodes(nodes: &[ImportNode], xfa: &str) -> String { let mut out = String::new(); for n in nodes { serialize_node(n, xfa, &mut out, 0); } out } fn serialize_node(n: &ImportNode, xfa: &str, out: &mut String, depth: usize) { if depth > MAX_IMPORT_DEPTH { return; } let mut decls: Vec<(String, String)> = Vec::new(); let mut qualify = |ns: &Option, name: &str, attr: bool| -> String { match ns.as_deref() { None => xml_name(name), Some(uri) if uri == XFA_DATA_NS => format!("{xfa}:{}", xml_name(name)), Some("http://www.w3.org/XML/1998/namespace") => format!("xml:{}", xml_name(name)), Some(uri) => { let p = match decls.iter().position(|(_, u)| u == uri) { Some(i) => format!("n{i}"), None => { decls.push((format!("n{}", decls.len()), uri.to_string())); format!("n{}", decls.len() - 1) } }; let _ = attr; format!("{p}:{}", xml_name(name)) } } }; let tag = qualify(&n.ns, &n.name, false); let attrs: Vec = n .attrs .iter() .take(256) .map(|(ns, name, value)| { let q = qualify(ns, name, true); format!(" {q}=\"{}\"", escape(value).0) }) .collect(); out.push('<'); out.push_str(&tag); for (p, uri) in &decls { out.push_str(&format!(" xmlns:{p}=\"{}\"", escape(uri).0)); } for a in &attrs { out.push_str(a); } if n.text.is_empty() && n.children.is_empty() { out.push_str("/>"); return; } out.push('>'); out.push_str(&escape(&n.text).0); for c in &n.children { serialize_node(c, xfa, out, depth + 1); } out.push_str(&format!("")); } fn merge(text: &str, within: Within, op: Op, warnings: &mut Vec) -> Merged { let wants: &[Want] = match op { Op::Values(w) => w, Op::Remove(_) | Op::ReplaceData(_) | Op::Picture(..) => &[], }; // Fresh data, where none exists yet (declared with the `xfa` prefix). let fresh_children = |warnings: &mut Vec| match op { Op::ReplaceData(nodes) => serialize_nodes(nodes, "xfa"), _ => build_children(wants, warnings), }; if crate::xml_nesting_depth(text) > crate::MAX_XML_DEPTH { warnings.push("The XFA data nests XML elements deeper than the reader accepts; the field values were not written to it".into()); return Merged::Failed; } let opts = || roxmltree::ParsingOptions { allow_dtd: false, nodes_limit: MAX_NODES }; // A packet of an array may use the prefixes the XDP envelope declares: read it inside // an element declaring the usual ones. let wrapped; let (src, shift, parsed) = match roxmltree::Document::parse_with_options(text, opts()) { Ok(d) => (text, 0, d), Err(first) => { wrapped = format!("{WRAP_OPEN}{text}{WRAP_CLOSE}"); match (within, roxmltree::Document::parse_with_options(&wrapped, opts())) { (Within::Packet, Ok(d)) => (wrapped.as_str(), WRAP_OPEN.len(), d), _ => { warnings.push(format!("The XFA data could not be read ({first}); the field values were not written to it")); return Merged::Failed; } } } }; let is = |n: &roxmltree::Node, name: &str| n.is_element() && n.tag_name().name() == name; let datasets = parsed .descendants() .find(|n| is(n, "datasets") && n.tag_name().namespace() == Some(XFA_DATA_NS)) .or_else(|| parsed.descendants().find(|n| is(n, "datasets"))); let mut edits = Vec::new(); let mut data = None; match datasets { None if matches!(op, Op::Remove(_) | Op::Picture(..)) => return Merged::Unchanged, None if within == Within::Xdp => { let root = parsed.root_element(); let children = fresh_children(warnings); match append_edit(src, root, &fresh_datasets(&children)) { Some(e) => edits.push(e), None => { warnings.push("The XFA packets could not be extended with data; the field values were not written".into()); return Merged::Failed; } } } None => { warnings.push("The XFA datasets packet holds no datasets element; the field values were not written to it".into()); return Merged::Failed; } Some(ds) => match ds.children().find(|n| is(n, "data")) { Some(d) => data = Some(d), None if matches!(op, Op::Remove(_) | Op::Picture(..)) => return Merged::Unchanged, None => { let prefix = qname(src, &ds.range()).and_then(|q| q.split_once(':')).map(|(p, _)| format!("{p}:")).unwrap_or_default(); let children = fresh_children(warnings); let element = format!("<{prefix}data>{}", children); match append_edit(src, ds, &element) { Some(e) => edits.push(e), None => { warnings.push("The XFA datasets could not be extended with data; the field values were not written".into()); return Merged::Failed; } } } }, } if let Some(data) = data { match op { Op::Values(w) => merge_values(src, data, w, &mut edits, warnings), Op::ReplaceData(nodes) => { // Everything between the start and end tags; `` opens up. Written // with the prefix the packet binds to the data namespace. let prefix = xfa_prefix(data); let markup = serialize_nodes(nodes, prefix.as_deref().unwrap_or("xfa")); let range = data.range(); match (start_tag_end(src, &range), qname(src, &range)) { (Some(tag_end), Some(name)) if tag_end + 1 == range.end && src.get(..tag_end).is_some_and(|s| s.ends_with('/')) => { edits.push(Edit { start: range.start, end: range.end, text: format!("<{name}>{markup}") }); } (Some(tag_end), _) => { let close = src.get(range.clone()).and_then(|s| s.rfind(" edits.push(Edit { start: tag_end + 1, end: close, text: markup }), _ => { warnings.push("The XFA data element could not be rewritten; the data was not imported".into()); return Merged::Failed; } } } _ => { warnings.push("The XFA data element could not be rewritten; the data was not imported".into()); return Merged::Failed; } } } Op::Picture(path, content_type, base64) => { let mut node = Some(data); for (name, idx) in path.iter() { let want = xml_name(name); node = node.and_then(|n| n.children().filter(|c| c.is_element() && c.tag_name().name() == want).nth(*idx)); } let Some(n) = node else { return Merged::Unchanged }; if n.children().any(|c| c.is_element()) { // A data group, not a leaf: a picture would wipe what's under it. skipped(warnings, STRUCTURED, &path.iter().map(|(n, i)| format!("{n}[{i}]")).collect::>().join(".")); return Merged::Failed; } let name = qname(src, &n.range()).unwrap_or("value").to_string(); let (ct, _) = escape(content_type); let attr = match xfa_prefix(n) { Some(p) => format!("{p}:contentType=\"{ct}\""), None => format!("xmlns:xfa=\"{XFA_DATA_NS}\" xfa:contentType=\"{ct}\""), }; edits.push(Edit { start: n.range().start, end: n.range().end, text: format!("<{name} {attr}>{base64}") }); } Op::Remove(path) => { // The element at `path`, with the whitespace before it. let mut node = Some(data); for (name, idx) in path { node = node.and_then(|n| n.children().filter(|c| c.is_element() && c.tag_name().name() == name).nth(*idx)); } if let Some(n) = node { let r = n.range(); let start = src.get(..r.start).map_or(r.start, |before| r.start - before.len() + before.trim_end().len()); edits.push(Edit { start, end: r.end, text: String::new() }); } } } } if edits.is_empty() { return Merged::Unchanged; } // Apply from the end, so earlier positions stay valid; overlapping edits (two fields bound // to one node) keep the first. edits.sort_by_key(|e| (e.start, e.end)); let mut kept: Vec = Vec::with_capacity(edits.len()); for e in edits { if kept.last().is_some_and(|k| e.start < k.end) { if kept.last().is_some_and(|k| k.text != e.text) && !warnings.iter().any(|w| w == CONFLICT) { warnings.push(CONFLICT.into()); } continue; } kept.push(e); } let mut out = src.to_string(); for e in kept.iter().rev() { if out.get(e.start..e.end).is_none() { warnings.push("The XFA data could not be updated; the field values were not written to it".into()); return Merged::Failed; } out.replace_range(e.start..e.end, &e.text); } if shift > 0 { match out.get(shift..out.len().saturating_sub(WRAP_CLOSE.len())) { Some(inner) => out = inner.to_string(), None => return Merged::Failed, } } Merged::Changed(out) } /// The edits that put `wants` into the `data` element. fn merge_values(src: &str, data: roxmltree::Node, wants: &[Want], edits: &mut Vec, warnings: &mut Vec) { // Element children by local name, per element visited (built once each). let mut index: HashMap>> = HashMap::new(); let tree = data.document(); // Nodes to add, per existing element they go into. let mut added: Vec<(roxmltree::NodeId, Build)> = Vec::new(); let mut budget = NewNodes::new(); for w in wants { let (escaped, dropped) = escape(&w.text); let mut node = data; let mut depth = 0; let mut have = 0; for (name, idx) in &w.path { let kids = index.entry(node.id()).or_insert_with(|| { let mut by: HashMap> = HashMap::new(); for c in node.children().filter(|c| c.is_element()) { by.entry(c.tag_name().name().to_string()).or_default().push(c.id()); } by }); let same = kids.get(name.as_str()); match same.and_then(|v| v.get(*idx)).and_then(|id| tree.get_node(*id)) { Some(next) => { node = next; depth += 1; } None => { have = same.map_or(0, Vec::len); break; } } } if depth == w.path.len() { // The data node exists: give it the value, unless it already holds it. A node // only wanted to exist (a row instance) is left as it is. if w.ensure && w.text.is_empty() { continue; } let current: String = node.children().filter(|c| c.is_text()).filter_map(|c| c.text()).collect(); if node.children().any(|c| c.is_element()) { let all: String = node.descendants().filter(|c| c.is_text()).filter_map(|c| c.text()).collect(); // Ignore existing XML indentation, but not whitespace in the accepted value. // A skipped mismatch must warn: relayout would otherwise use stale data. if all.trim() != w.text { skipped(warnings, STRUCTURED, &w.som); } continue; } if current == w.text { continue; } match replace_edit(src, node, &escaped) { Some(e) => edits.push(e), None => skipped(warnings, STRUCTURED, &w.som), } } else { // Missing: add the rest of the path below the deepest node that exists. An empty // value needs no node, unless the node itself is wanted (a new instance). if w.text.is_empty() && !w.ensure { continue; } let mut rest: DataPath = w.path.get(depth..).unwrap_or_default().to_vec(); if let Some(first) = rest.first_mut() { first.1 = first.1.saturating_sub(have); } let pos = match added.iter().position(|(id, _)| *id == node.id()) { Some(p) => p, None => { added.push((node.id(), Build::default())); added.len() - 1 } }; let built = added.get_mut(pos).and_then(|(_, b)| b.at(&rest, &mut budget)); match built { Some(b) => b.text = w.text.clone(), None => { skipped(warnings, TOO_MANY, &w.som); continue; } } } if dropped { skipped(warnings, CONTROL, &w.som); } } for (id, build) in added { let Some(node) = tree.get_node(id) else { continue }; let mut inner = String::new(); build.write_children(&mut inner); match append_edit(src, node, &inner) { Some(e) => edits.push(e), None => warnings.push("Some XFA data nodes could not be added; their field values were not written".into()), } } } /// What [`write_datasets`] did. #[derive(Clone, Debug, Default, PartialEq)] pub struct DatasetsWrite { /// The datasets packet changed (a new revision of it was added to the document). pub written: bool, /// Values that could not be written, and why. pub warnings: Vec, /// The datasets stream written, when one was. pub stream: Option, } /// A packet stream's text and encoding; `None` (with a warning) when it can't be read exactly. fn packet_text(doc: &Document, o: &Object, warnings: &mut Vec) -> Option<(String, Encoding)> { let s = doc.resolve(o); let Object::Stream(s) = &*s else { warnings.push("The XFA data packet is not a stream; the field values were not written to it".into()); return None; }; let bytes = match s.decoded_within(64 << 20) { Ok(b) => b, Err(e) => { warnings.push(format!("The XFA data could not be decoded ({e}); the field values were not written to it")); return None; } }; let decoded = crate::packets::decode(&bytes); if decoded.is_none() { warnings.push("The XFA data is neither UTF-8 nor UTF-16; the field values were not written to it, so it stays as it was".into()); } decoded } /// Write the fields' values into the datasets packet, keeping all the data no field binds to. /// Nothing is written when the document has no XFA entry or the data already holds the values; /// values that can't be written are reported, never silently dropped. pub fn write_datasets(doc: &mut Document, fields: &[FieldDatum]) -> Result { write_datasets_reusing(doc, fields, None) } /// [`write_datasets`], replacing the stream `reuse` in place when the packet is that stream /// (written earlier in the same edit) rather than adding another. pub fn write_datasets_reusing(doc: &mut Document, fields: &[FieldDatum], reuse: Option) -> Result { write_datasets_with(doc, None, fields, reuse) } /// [`write_datasets_reusing`] with the form's parsed template when the caller has it (it says /// which SOM segments are areas, see [`data_path`]); `None` parses the document's. pub fn write_datasets_with( doc: &mut Document, tpl: Option<&Template>, fields: &[FieldDatum], reuse: Option, ) -> Result { let wants = with_template(doc, tpl, |t| wanted(doc, fields, t)); rewrite(doc, &[Op::Values(&wants)], reuse) } /// One change to the data, for [`write_data_ops`]. #[derive(Clone, Debug, PartialEq)] pub enum DataOp { /// Put `text` at the node `path`, adding the node (and the ones above it) as needed. Value { path: DataPath, text: String }, /// Make sure the node at `parent` has at least `count` children named `name` (a script /// added rows: the layout repeats the subform once per data instance). Instances { parent: DataPath, name: String, count: usize }, /// Take the node at `path` out (a script removed a row). Remove(DataPath), } impl DataNode { /// The node at `path`, made (with empty siblings before it) where missing; `None` past /// the index cap. fn ensure(&mut self, path: &[(String, usize)]) -> Option<&mut DataNode> { let mut node = self; for (name, idx) in path { if *idx >= MAX_INDEX { return None; } let have = node.children.iter().filter(|c| &c.name == name).count(); for _ in have..=*idx { node.children.push(DataNode { name: name.clone(), ..Default::default() }); } node = node.children.iter_mut().filter(|c| &c.name == name).nth(*idx)?; } Some(node) } /// Apply `op` to this tree (the root is the `xfa:data` element), as [`write_data_ops`] /// will to the packet, so scripts that run before it is written see the change. pub fn apply(&mut self, op: &DataOp) { match op { DataOp::Value { path, text } => { if let Some(n) = self.ensure(path) && n.children.is_empty() { n.text = text.clone(); } } DataOp::Instances { parent, name, count } => { if let Some(&last) = count.checked_sub(1).as_ref() { let mut path = parent.clone(); path.push((name.clone(), last.min(MAX_INDEX.saturating_sub(1)))); let _ = self.ensure(&path); } } DataOp::Remove(path) => { let Some(((name, idx), parent)) = path.split_last() else { return }; let mut node = self; for (n, i) in parent { let Some(next) = node.children.iter_mut().filter(|c| &c.name == n).nth(*i) else { return }; node = next; } if let Some(at) = node.children.iter().enumerate().filter(|(_, c)| &c.name == name).nth(*idx).map(|(at, _)| at) { node.children.remove(at); } } } } } fn path_som(path: &DataPath) -> String { path.iter().map(|(n, i)| format!("{n}[{i}]")).collect::>().join(".") } /// Make sure the data has at least `count` instances of `name` under `parent` (a script added /// rows: the layout repeats the subform once per data instance). pub fn add_data_instances(doc: &mut Document, parent: &DataPath, name: &str, count: usize) -> Result { write_data_ops(doc, &[DataOp::Instances { parent: parent.clone(), name: name.to_string(), count }], None) } /// Put `text` at the data node `path` (a field with no widget, a hidden one or a draw, that a /// script gave a value), adding the node as needed. pub fn write_data_value(doc: &mut Document, path: &DataPath, text: &str) -> Result { write_data_ops(doc, &[DataOp::Value { path: path.clone(), text: text.to_string() }], None) } /// Remove the data node at `path` (a script removed a row). pub fn remove_data_instance(doc: &mut Document, path: &DataPath) -> Result { write_data_ops(doc, &[DataOp::Remove(path.clone())], None) } /// Largest picture written into the data (its bytes, before base64): with the 64 MB cap on /// all packets together, several pictures still fit. const MAX_DATA_IMAGE: usize = 8 << 20; /// Longest data file imported. const MAX_IMPORT: usize = 16 << 20; /// Put a picture into the data node at `path` as Designer saves one a user chose: base64, with /// its content type (`image/png`, …) as `xfa:contentType` (in the packet's own prefix). The /// node is made when missing; a data group is refused rather than wiped. `reuse`: the stream /// this edit already wrote. pub fn write_data_image( doc: &mut Document, path: &DataPath, content_type: &str, bytes: &[u8], reuse: Option, ) -> Result { use base64::Engine; if path.is_empty() { return Err(XfaError::Malformed("a picture needs a data path".into())); } if bytes.len() > MAX_DATA_IMAGE { return Err(XfaError::TooLarge(format!("the picture is larger than {} MB", MAX_DATA_IMAGE >> 20))); } let ct: String = content_type.chars().filter(|c| c.is_ascii_alphanumeric() || matches!(c, '/' | '-' | '.' | '+')).take(64).collect(); let b64 = base64::engine::general_purpose::STANDARD.encode(bytes); let ensure = [Want { som: String::new(), path: path.clone(), text: String::new(), ensure: true }]; rewrite(doc, &[Op::Values(&ensure), Op::Picture(path, &ct, &b64)], reuse) } /// What an import of a data file did. #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct DataImport { /// Element nodes the new data holds. pub nodes: usize, pub warnings: Vec, /// The datasets stream written. pub stream: Option, } /// The data a file holds, as the children of `xfa:data`: an XDP or datasets document (Export /// Data's output, Designer's), a bare data element, or a data document whose root is the /// form's root data node (named `expect_root`, when the caller knows it). Anything else is /// refused: a form's data is never replaced by what is not its data. pub fn parse_data_file(text: &str, expect_root: Option<&str>) -> Result, XfaError> { if text.len() > MAX_IMPORT { return Err(XfaError::TooLarge(format!("the data file is larger than {} MB", MAX_IMPORT >> 20))); } let text = text.trim_start_matches('\u{feff}'); if crate::xml_nesting_depth(text) > MAX_IMPORT_DEPTH { return Err(XfaError::TooLarge(format!("the data file nests more than {MAX_IMPORT_DEPTH} levels deep"))); } let opts = roxmltree::ParsingOptions { allow_dtd: false, nodes_limit: MAX_NODES }; let parsed = roxmltree::Document::parse_with_options(text, opts).map_err(|e| XfaError::Malformed(format!("the data file is not XML: {e}")))?; let root = parsed.root_element(); let local = root.tag_name().name(); let in_xfa = |n: &roxmltree::Node| n.tag_name().namespace().is_none_or(|ns| ns == XFA_DATA_NS); let is = |n: &roxmltree::Node, name: &str| n.is_element() && n.tag_name().name() == name && in_xfa(n); let data = match local { "xdp" => { let ds = root.children().find(|n| is(n, "datasets")).ok_or_else(|| XfaError::Malformed("the XDP holds no datasets".into()))?; ds.children().find(|n| is(n, "data")).ok_or_else(|| XfaError::Malformed("the datasets hold no data element".into()))? } "datasets" if in_xfa(&root) => { root.children().find(|n| is(n, "data")).ok_or_else(|| XfaError::Malformed("the datasets hold no data element".into()))? } "data" if in_xfa(&root) => root, _ => { if root.tag_name().namespace().is_some() { return Err(XfaError::Malformed(format!("<{local}> is not XFA form data"))); } if let Some(want) = expect_root && local != want { return Err(XfaError::Malformed(format!("the data file's root is <{local}>; this form's data root is <{want}>"))); } let node = import_node(root, 0, &mut ImportBudget::new())?; return Ok(vec![node]); } }; let mut budget = ImportBudget::new(); let mut out = Vec::new(); for c in data.children().filter(|c| c.is_element()) { out.push(import_node(c, 1, &mut budget)?); } Ok(out) } /// Replace the form's data with what the file `text` holds (see [`parse_data_file`]), keeping /// the rest of the datasets (data descriptions, say). `reuse`: the stream this edit already /// wrote. pub fn import_data(doc: &mut Document, text: &str, expect_root: Option<&str>, reuse: Option) -> Result { let nodes = parse_data_file(text, expect_root)?; fn count(n: &[ImportNode]) -> usize { n.iter().map(|c| 1 + count(&c.children)).sum() } let r = rewrite(doc, &[Op::ReplaceData(&nodes)], reuse)?; if !r.written { return Err(XfaError::Malformed(r.warnings.first().cloned().unwrap_or_else(|| "the data could not be written".into()))); } Ok(DataImport { nodes: count(&nodes), warnings: r.warnings, stream: r.stream }) } /// The datasets packet's text: from the XFA array's `datasets` item, or the whole XDP. fn datasets_packet(doc: &Document) -> Result, XfaError> { let Some(root) = doc.root() else { return Ok(None) }; let catalog = doc.get(root); let Some(acro) = catalog.as_dict().and_then(|c| c.get(b"AcroForm")).map(|a| doc.resolve(a)) else { return Ok(None) }; let Some(xfa) = acro.as_dict().and_then(|a| a.get(b"XFA")).map(|x| doc.resolve(x)) else { return Ok(None) }; let text_of = |o: &Object| -> Result, XfaError> { match &*doc.resolve(o) { Object::Stream(s) => Ok(crate::packets::decode(&s.decoded_within(64 << 20)?).map(|(t, _)| t)), _ => Ok(None), } }; match &*xfa { Object::Array(items) => { let at = (0..items.len()) .step_by(2) .find(|&i| items.get(i).and_then(|n| n.as_string()).is_some_and(|s| s.to_text() == "datasets") && i + 1 < items.len()); match at.and_then(|i| items.get(i + 1)) { Some(o) => text_of(o), None => Ok(None), } } Object::Stream(_) => text_of(&xfa), _ => Ok(None), } } /// The data to export, as Acrobat's Export Data writes it: `xdp` false gives an XML document /// holding the data alone (the form's root data node at the root, or the data element when /// there are several); true gives an XDP holding the whole datasets and a reference to the /// PDF, named `pdf_name`. A document whose packets can't be read is an error, not empty data. pub fn export_data(doc: &Document, xdp: bool, pdf_name: &str) -> Result { if crate::read_packets(doc)?.is_none() { return Err(XfaError::NotXfa); } let packet = datasets_packet(doc)?; let (datasets, data) = match &packet { Some(text) => match datasets_markup(text) { Ok(Some(m)) => m, Ok(None) => (fresh_datasets(""), String::new()), Err(()) => return Err(XfaError::Malformed("the XFA data could not be read".into())), }, None => (fresh_datasets(""), String::new()), }; let mut out = String::from("\n"); if xdp { let (name, _) = escape(pdf_name); out.push_str("\n"); out.push_str(&datasets); out.push_str(&format!("\n\n\n")); } else { out.push_str(if data.is_empty() { &datasets } else { &data }); out.push('\n'); } Ok(out) } /// The `xfa:datasets` element of `text` as written, and the data alone (its one child, or the /// data element), each with the namespaces it uses declared on it. `Ok(None)`: no datasets /// element; `Err`: the text can't be read. fn datasets_markup(text: &str) -> Result, ()> { let opts = roxmltree::ParsingOptions { allow_dtd: false, nodes_limit: MAX_NODES }; if crate::xml_nesting_depth(text) > crate::MAX_XML_DEPTH { return Err(()); } let wrapped = format!("{WRAP_OPEN}{text}{WRAP_CLOSE}"); let (src, parsed) = match roxmltree::Document::parse_with_options(text, opts) { Ok(d) => (text, d), Err(_) => (wrapped.as_str(), roxmltree::Document::parse_with_options(&wrapped, opts).map_err(|_| ())?), }; let Some(ds) = parsed.descendants().find(|n| n.is_element() && n.tag_name().name() == "datasets") else { return Ok(None) }; let data = ds.children().find(|n| n.is_element() && n.tag_name().name() == "data"); let data_alone = match data { Some(d) => { let kids: Vec = d.children().filter(|c| c.is_element()).collect(); match kids.as_slice() { [one] => element_markup(src, *one).ok_or(())?, _ => element_markup(src, d).ok_or(())?, } } None => String::new(), }; Ok(Some((element_markup(src, ds).ok_or(())?, data_alone))) } /// `n` as written in `src`, with the namespace declarations in scope that its start tag lacks /// added to it, so it stands alone. fn element_markup(src: &str, n: roxmltree::Node) -> Option { let range = n.range(); let text = src.get(range.clone())?; let tag_end = start_tag_end(src, &range)?; let start_tag = src.get(range.start..tag_end)?; let mut decls = String::new(); for ns in n.namespaces() { let attr = match ns.name() { Some(p) => format!("xmlns:{p}="), None => "xmlns=".to_string(), }; if !start_tag.contains(&attr) { decls.push_str(&format!(" {attr}\"{}\"", escape(ns.uri()).0)); } } if decls.is_empty() { return Some(text.to_string()); } let name_end = qname(src, &range)?.len() + 1; Some(format!("{}{decls}{}", text.get(..name_end)?, text.get(name_end..)?)) } /// Most instance nodes one [`write_data_ops`] queues over all its passes (a removal starts a new /// pass, which asks for the instances again). const MAX_ENSURED: usize = 200_000; // Instance nodes write_data_ops has queued on this thread, for tests that bound its work. #[cfg(test)] thread_local! { pub(crate) static ENSURED: std::cell::Cell = const { std::cell::Cell::new(0) }; } /// Apply `ops` to the datasets packet, in order, as one rewrite of it: everything one script /// event changed becomes a single new datasets stream. When the packet is already the stream /// `reuse` (written earlier for the same event), that stream is replaced in place instead of /// a new one being added. The stream written is in the report. pub fn write_data_ops(doc: &mut Document, ops: &[DataOp], reuse: Option) -> Result { // Runs of values (and instances) merge in one pass; a value set twice keeps the last. let mut steps: Vec> = Vec::new(); let mut removes: Vec> = Vec::new(); let mut run: Vec = Vec::new(); // Instances queued in this run, by parent and name: asking for more only queues the rest. let mut queued: HashMap<(&DataPath, &str), usize> = HashMap::new(); let (mut ensured, mut capped) = (0usize, false); let flush = |run: &mut Vec, steps: &mut Vec>, removes: &mut Vec>| { if !run.is_empty() { let mut seen = std::collections::HashSet::new(); let mut kept: Vec = std::mem::take(run).into_iter().rev().filter(|w| seen.insert((w.ensure, w.path.clone()))).collect(); kept.reverse(); steps.push(kept); removes.push(None); } }; for op in ops { match op { DataOp::Value { path, text } if !path.is_empty() => { // An empty value clears an existing node and needs no new one. run.push(Want { som: path_som(path), path: path.clone(), text: text.clone(), ensure: false }); } DataOp::Instances { parent, name, count } => { let done = queued.entry((parent, name.as_str())).or_insert(0); while *done < (*count).min(MAX_INDEX) { if ensured >= MAX_ENSURED { capped = true; break; } ensured += 1; #[cfg(test)] ENSURED.with(|n| n.set(n.get() + 1)); let k = *done; *done += 1; let mut path = parent.clone(); path.push((name.clone(), k)); run.push(Want { som: format!("{name}[{k}]"), path, text: String::new(), ensure: true }); } } DataOp::Remove(path) if !path.is_empty() => { flush(&mut run, &mut steps, &mut removes); queued.clear(); steps.push(Vec::new()); removes.push(Some(path.clone())); } _ => {} } } flush(&mut run, &mut steps, &mut removes); let plan: Vec = steps .iter() .zip(&removes) .map(|(w, r)| match r { Some(p) => Op::Remove(p), None => Op::Values(w), }) .collect(); let mut written = rewrite(doc, &plan, reuse)?; if capped { written.warnings.push(format!("XFA data instances not written past {MAX_ENSURED}: the rows were added and removed too often")); } Ok(written) } /// Put `bytes` in the document as a datasets stream: in place of `old` when that is the stream /// `reuse` or already an unsaved overlay stream of `doc`, else as a new object. fn put_stream(doc: &mut Document, old: Option<&Object>, reuse: Option, bytes: &[u8]) -> ObjRef { let stream = Object::Stream(Stream::flate(Dict::new(), bytes)); match old.and_then(Object::as_ref) { Some(r) if reuse == Some(r) || doc.is_edited(r.num) => { doc.set(r, stream); r } _ => doc.add(stream), } } /// Merge `ops` into `text` in order. `None`: nothing changed (or nothing could be merged). fn merge_all(text: &str, within: Within, ops: &[Op], warnings: &mut Vec) -> Option { let mut current: Option = None; for op in ops { match merge(current.as_deref().unwrap_or(text), within, *op, warnings) { Merged::Changed(t) => current = Some(t), Merged::Unchanged => {} // Later steps still apply to what merged so far; the reason is in the warnings. Merged::Failed => {} } } current } /// Longest packet text written (the packets together are read up to 64 MB). const MAX_PACKET_WRITE: usize = 48 << 20; /// Is `text` a packet the reader will take back (well-formed, within size)? A rewrite that /// isn't is not written: better the old data than an unreadable packet. fn readable(text: &str, within: Within, warnings: &mut Vec) -> bool { if text.len() > MAX_PACKET_WRITE { warnings.push(format!("The XFA data would be larger than {} MB; it was not written", MAX_PACKET_WRITE >> 20)); return false; } let opts = roxmltree::ParsingOptions { allow_dtd: false, nodes_limit: MAX_NODES }; let ok = roxmltree::Document::parse_with_options(text, opts).is_ok() || (within == Within::Packet && roxmltree::Document::parse_with_options(&format!("{WRAP_OPEN}{text}{WRAP_CLOSE}"), opts).is_ok()); if !ok { warnings.push("The XFA data would not be well-formed XML after this change; it was not written".into()); } ok } fn rewrite(doc: &mut Document, ops: &[Op], reuse: Option) -> Result { let mut report = DatasetsWrite::default(); if ops.is_empty() { return Ok(report); } let Some(root) = doc.root() else { return Ok(report) }; let catalog = doc.get(root); let Some(acro_obj) = catalog.as_dict().and_then(|c| c.get(b"AcroForm")).cloned() else { return Ok(report) }; let acro_ref = acro_obj.as_ref(); let Some(mut acro) = doc.resolve(&acro_obj).as_dict().cloned() else { return Ok(report) }; let Some(xfa) = acro.get(b"XFA").cloned() else { return Ok(report) }; let warnings = &mut report.warnings; match &*doc.resolve(&xfa) { Object::Array(items) => { let mut items = items.clone(); let at = (0..items.len()) .step_by(2) .find(|&i| items.get(i).and_then(|n| n.as_string()).is_some_and(|s| s.to_text() == "datasets") && i + 1 < items.len()); match at { Some(i) => { let Some(existing) = items.get(i + 1).cloned() else { return Ok(report) }; let Some((text, encoding)) = packet_text(doc, &existing, warnings) else { return Ok(report) }; let Some(t) = merge_all(&text, Within::Packet, ops, warnings) else { return Ok(report) }; if !readable(&t, Within::Packet, warnings) { return Ok(report); } let r = put_stream(doc, Some(&existing), reuse, &encode(&t, encoding)); report.stream = Some(r); if let Some(slot) = items.get_mut(i + 1) { *slot = Object::Ref(r); } } None => { // No datasets packet yet: made from the first values, then the rest merged // in; before the postamble, or last. let Some(first) = ops.iter().position(|op| matches!(op, Op::Values(_))) else { return Ok(report) }; let Some(Op::Values(wants)) = ops.get(first) else { return Ok(report) }; let fresh = fresh_datasets(&build_children(wants, warnings)); let text = merge_all(&fresh, Within::Packet, ops.get(first + 1..).unwrap_or_default(), warnings).unwrap_or(fresh); if !readable(&text, Within::Packet, warnings) { return Ok(report); } let r = doc.add(Object::Stream(Stream::flate(Dict::new(), text.as_bytes()))); report.stream = Some(r); let pos = items.iter().position(|n| n.as_string().is_some_and(|s| s.to_text() == "postamble")).unwrap_or(items.len()); items.insert(pos, Object::Ref(r)); items.insert(pos, Object::String(PdfString::text("datasets"))); } } acro.set(b"XFA".to_vec(), Object::Array(items)); } Object::Stream(_) => { let Some((text, encoding)) = packet_text(doc, &xfa, warnings) else { return Ok(report) }; let Some(new) = merge_all(&text, Within::Xdp, ops, warnings) else { return Ok(report) }; if !readable(&new, Within::Xdp, warnings) { return Ok(report); } let r = put_stream(doc, Some(&xfa), reuse, &encode(&new, encoding)); report.stream = Some(r); acro.set(b"XFA".to_vec(), Object::Ref(r)); } _ => return Ok(report), } match acro_ref { Some(ar) => doc.set(ar, Object::Dict(acro)), None => doc.update_dict(root, |c| c.set(b"AcroForm".to_vec(), Object::Dict(acro)))?, } report.written = true; Ok(report) } /// The values the datasets hold for `fields`: `(field name, what it should hold)`, only for /// fields the data mentions. pub fn read_values(doc: &Document, fields: &[FieldDatum]) -> Vec<(String, FieldData)> { read_values_with(doc, None, fields) } /// [`read_values`] with the form's parsed template when the caller has it; `None` parses the /// document's. pub fn read_values_with(doc: &Document, tpl: Option<&Template>, fields: &[FieldDatum]) -> Vec<(String, FieldData)> { with_template(doc, tpl, |t| values_at(doc, t, fields)) } /// The values the document's datasets hold for `fields`, read in each field's data picture and /// shown in its own pattern; only fields the data mentions. fn values_at(doc: &Document, tpl: Option<&Template>, fields: &[FieldDatum]) -> Vec<(String, FieldData)> { let Ok(Some(p)) = crate::read_packets(doc) else { return Vec::new() }; let Some(data) = parse_datasets(&p.xdp) else { return Vec::new() }; let mut out = Vec::new(); for f in fields { let som = som_of(doc, f); let path = path_of(tpl, &som); let Some(text) = data.value_at(&path) else { continue }; let text = text.as_str(); let value = match &f.data { FieldData::Text(_) => FieldData::Text(match date_pattern_of(doc, f) { Some(pat) => iso_to_pattern(&data_to_iso(text, data_date_pattern(tpl, &som).as_deref()), &pat), None => text.to_string(), }), FieldData::Check(_) => { let (on, _) = items_of(doc, f); FieldData::Check(is_on(text, &on)) } FieldData::Radio(_) => FieldData::Radio((!text.trim().is_empty()).then(|| text.trim().to_string())), FieldData::None => continue, }; out.push((f.name.clone(), value)); } out } /// Does a data value mean "checked" for a button whose on value is `on`? pub fn is_on(text: &str, on: &str) -> bool { let t = text.trim(); if t == on { return true; } !matches!(t.to_ascii_lowercase().as_str(), "" | "0" | "off" | "false" | "no") && on.trim().is_empty() } /// The datasets stream's object reference in `doc` (the `datasets` item in the `/XFA` array, or /// the single `/XFA` stream), when `doc` has one. fn datasets_stream_ref(doc: &Document) -> Option { let root = doc.root()?; let catalog = doc.get(root); let acro = catalog.as_dict()?.get(b"AcroForm").map(|a| doc.resolve(a))?; let xfa = acro.as_dict()?.get(b"XFA")?; match &*doc.resolve(xfa) { Object::Array(items) => { let at = (0..items.len()) .step_by(2) .find(|&i| items.get(i).and_then(|n| n.as_string()).is_some_and(|s| s.to_text() == "datasets") && i + 1 < items.len())?; items.get(at + 1)?.as_ref() } Object::Stream(_) => xfa.as_ref(), _ => None, } } /// The SOM path and default value of every AcroForm text field with the Password flag (bit 14, /// `1 << 13`) in `doc`, for static XFA forms whose password fields set the flag in the AcroForm /// dictionary. fn acro_password_fields(doc: &Document) -> Vec<(String, String)> { const PASSWORD_FLAG: i64 = 1 << 13; let Some(root) = doc.root() else { return Vec::new() }; let catalog = doc.get(root); let Some(acro) = catalog.as_dict().and_then(|c| c.get(b"AcroForm")).map(|a| doc.resolve(a)) else { return Vec::new() }; let Some(top) = acro.as_dict().and_then(|a| a.get(b"Fields")).map(|f| doc.resolve(f)).and_then(|f| f.as_array().cloned()) else { return Vec::new(); }; struct AcroWalk<'a> { doc: &'a Document, seen: std::collections::HashSet, out: Vec<(String, String)>, } impl AcroWalk<'_> { fn walk( &mut self, items: &[Object], parent_name: &str, inherited_ft: Option<&[u8]>, inherited_ff: i64, inherited_dv: Option<&str>, depth: usize, ) { if depth > 32 { return; } for item in items { let Some(r) = item.as_ref() else { continue }; if !self.seen.insert(r) { continue; } let obj = self.doc.get(r); let Some(d) = obj.as_dict() else { continue }; let ft = d.name(b"FT").or(inherited_ft); let ff = d.int(b"Ff").unwrap_or(inherited_ff); let dv_own = text_of(self.doc, d, b"DV"); let dv = dv_own.as_deref().or(inherited_dv); let t = text_of(self.doc, d, b"T"); let full_name = match (&t, parent_name.is_empty()) { (Some(seg), true) => seg.clone(), (Some(seg), false) => format!("{parent_name}.{seg}"), (None, _) => parent_name.to_string(), }; let kids = d.get(b"Kids").map(|k| self.doc.resolve(k)).and_then(|k| k.as_array().cloned()).unwrap_or_default(); let has_named_kid = kids.iter().filter_map(Object::as_ref).any(|kr| self.doc.get(kr).as_dict().is_some_and(|kd| kd.contains(b"T"))); if !has_named_kid && ft == Some(b"Tx") && (ff & PASSWORD_FLAG) != 0 { let som = text_of(self.doc, d, crate::pdf::SOM_KEY).unwrap_or_else(|| full_name.clone()); if !som.is_empty() { self.out.push((som, dv.unwrap_or_default().to_string())); } } if !kids.is_empty() { self.walk(&kids, &full_name, ft, ff, dv, depth + 1); } } } } let mut w = AcroWalk { doc, seen: std::collections::HashSet::new(), out: Vec::new() }; w.walk(&top, "", None, 0, None, 0); w.out } /// The `Want` entries needed to restore password fields in `doc`'s datasets packet to their /// defaults (or empty when they have no default). fn password_dataset_wants(doc: &Document) -> Vec { if datasets_stream_ref(doc).is_none() { return Vec::new(); } let Ok(Some(packet)) = datasets_packet(doc) else { return Vec::new() }; let Some(data) = parse_datasets(&packet) else { return Vec::new() }; with_template(doc, None, |tpl| { let mut candidates: Vec<(String, DataPath, String)> = Vec::new(); let mut seen = std::collections::HashSet::new(); if let Some(t) = tpl { struct Walk<'a> { tpl: &'a Template, data: &'a DataNode, nodes: usize, seen: &'a mut std::collections::HashSet, out: &'a mut Vec<(String, DataPath, String)>, } impl Walk<'_> { fn child_som(parent: &str, name: Option<&str>, index: usize) -> String { match name { Some(n) if !parent.is_empty() => format!("{parent}.{n}[{index}]"), Some(n) => format!("{n}[{index}]"), None => parent.to_string(), } } fn walk(&mut self, children: &[Node], parent_som: &str, depth: usize) { if depth > MAX_DEPTH || self.nodes >= crate::script::MAX_FORM_NODES { return; } let sibs = SomIndices::of(children); for (i, n) in children.iter().enumerate() { if self.nodes >= crate::script::MAX_FORM_NODES { break; } self.nodes += 1; let sib = sibs.at(i); match n { Node::Subform(sf) => { let from_data = match &sf.common.name { Some(name) if sf.occur.max != Some(1) => self.data.count(&data_path(self.tpl, parent_som), name), _ => 0, }; let instances = crate::layout::instance_count(&sf.occur, from_data).max(from_data); for k in 0..instances { if k > 0 { if self.nodes >= crate::script::MAX_FORM_NODES { break; } self.nodes += 1; } let som = Self::child_som(parent_som, sf.common.name.as_deref(), sib + k); self.walk(&sf.children, &som, depth + 1); } } Node::Area(a) => { let som = Self::child_som(parent_som, a.common.name.as_deref(), sib); self.walk(&a.children, &som, depth + 1); } Node::Field(f) if f.ui == Ui::PasswordEdit => { let som = Self::child_som(parent_som, f.common.name.as_deref().or(Some("field")), sib); let path = data_path(self.tpl, &som); if !path.is_empty() && self.seen.insert(path.clone()) { let default = f.value.plain().map(|v| f.saved_value(v.trim())).unwrap_or_default(); self.out.push((som, path, default)); } } _ => {} } } } } let root = Node::Subform(Box::new(t.root.clone())); let mut w = Walk { tpl: t, data: &data, nodes: 0, seen: &mut seen, out: &mut candidates }; w.walk(std::slice::from_ref(&root), "", 0); if let Some(ps) = &t.root.page_set { for (i, area) in ps.areas.iter().enumerate() { let som = area.name.clone().map(|n| format!("{n}[0]")).unwrap_or_else(|| format!("#pageArea[{i}]")); w.walk(&area.items, &som, 1); } } } for (som, default) in acro_password_fields(doc) { let path = path_of(tpl, &som); if !path.is_empty() && seen.insert(path.clone()) { candidates.push((som, path, default)); } } candidates .into_iter() .filter_map(|(som, path, default)| { let cur = data.value_at(&path)?; (!cur.is_empty() && cur != default).then_some(Want { som, path, text: default, ensure: false }) }) .collect() }) } /// Whether any XFA password field (`` or an AcroForm text field with the Password /// flag) holds a value in `doc`'s datasets packet beyond its default (#3944). pub fn has_password_values(doc: &Document) -> bool { !password_dataset_wants(doc).is_empty() } /// Clear typed password values from `doc`'s XFA datasets packet (restoring a template default /// where the field has one, else emptying the element), replacing the datasets stream in place /// (#3944). Returns how many password fields were cleared in the datasets. pub fn clear_password_values(doc: &mut Document) -> Result { let wants = password_dataset_wants(doc); if wants.is_empty() { return Ok(0); } let count = wants.len(); let reuse = datasets_stream_ref(doc); let report = rewrite(doc, &[Op::Values(&wants)], reuse)?; if !report.written || !password_dataset_wants(doc).is_empty() { let reason = report.warnings.first().map_or("the XFA datasets password values could not be cleared", String::as_str); return Err(XfaError::Malformed(reason.to_string())); } Ok(count) }