Files
pdfcraft/crates/xfa/src/data/path.rs
T
Dylan M. Taylor f76dbb9e8c deps: bump tar, zip, roxmltree, rustls-webpki and symphonia to their latest versions (#5381)
- `zip`: 9.1.0 → 9.1.3

Requirement `roxmltree` 0.20 → 0.21: `ParsingOptions` gained `entity_resolver` (initializers take `..Default::default()`, i.e. none, as before) and is no longer `Copy` (two functions build it with a closure, the pattern merge.rs already used).
2026-10-29 22:21:27 -04:00

157 lines
6.6 KiB
Rust

use super::{DataNode, DataPath, Field, MAX_DEPTH, MAX_NODES, Node, SomIndices, Template};
fn to_node(n: roxmltree::Node, depth: usize) -> DataNode {
let name = n.tag_name().name().to_string();
let children: Vec<DataNode> =
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::<String>() } 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<DataNode> {
if crate::xml_nesting_depth(xdp) > crate::MAX_XML_DEPTH {
return None;
}
let opts = roxmltree::ParsingOptions { allow_dtd: false, nodes_limit: MAX_NODES, ..Default::default() };
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::<usize>().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 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,
}
}
pub(super) fn path_som(path: &DataPath) -> String {
path.iter().map(|(n, i)| format!("{n}[{i}]")).collect::<Vec<_>>().join(".")
}