fix(xfdf): a field name full of periods no longer overflows the stack on export

The XFDF export nested one <field> element per period-separated part of a field's full name. A /T can hold periods of its own, so a name can have far more parts than the field tree has levels. Past 64 levels the import refuses the file the export wrote, and around 100,000 parts the recursive write overflows the main thread's stack and aborts the process.

The export now nests at most as many levels as the import reads back. The parts past the last level stay in the deepest element's name, and the import joins them back into the same full name.
This commit is contained in:
Unhumanje
2026-10-21 11:17:56 +02:00
parent 4164951453
commit d335ff375d
2 changed files with 46 additions and 1 deletions
+7 -1
View File
@@ -415,7 +415,13 @@ fn xfdf_fields(doc: &Document, out: &mut String) {
continue;
}
let mut node = &mut root;
for part in f.name.split('.') {
// One `<field>` level per part of the name, but no more levels than the import reads
// back. A `/T` can hold periods of its own, so a name can split into far more parts than
// the field tree has levels: nesting every part wrote a file the import refuses and, for
// a long enough name, recursed in `write_node` until the stack overflowed. The parts past
// the last level stay together in the deepest element's name, and the import joins them
// back with periods into the same full name.
for part in f.name.splitn(MAX_DEPTH, '.') {
let i = match node.kids.iter().position(|(k, _)| k == part) {
Some(i) => i,
None => {
+39
View File
@@ -1104,6 +1104,45 @@ fn wide_or_commented_xml_is_not_mistaken_for_deep() {
assert!(check_form_xml_depth(&nested(MAX_XML_DEPTH + 1)).is_err());
}
/// [`one_field`] with its field's `/T` set to `name`, periods and all, holding `value`.
fn one_field_named(name: &str, value: &str) -> Document {
let mut doc = one_field();
let r = fields(&doc).into_iter().find(|f| f.name == "Name").unwrap().obj;
doc.update_dict(r, |d| {
d.set(b"T".to_vec(), Object::String(PdfString::text(name)));
d.set(b"V".to_vec(), Object::String(PdfString::text(value)));
})
.unwrap();
doc
}
/// A `/T` can hold periods of its own, so a field name can split into more parts than the field
/// tree has levels. The XFDF export nested one `<field>` per part, and the import refuses more
/// than 64 levels, so the form's own export could not be read back.
#[test]
fn field_names_with_more_parts_than_the_import_reads_still_round_trip() {
for parts in [1, 2, 63, 64, 65, 66, 100, 1_000] {
let name = (0..parts).map(|i| format!("p{i}")).collect::<Vec<_>>().join(".");
let doc = one_field_named(&name, "v");
assert_eq!(values(&doc), vec![(name.clone(), vec!["v".to_string()])], "{parts} parts");
let xfdf = export_xfdf(&doc, false, true, "form.pdf");
assert_eq!(data_values(xfdf.as_bytes()), Ok(vec![(name.clone(), vec!["v".to_string()])]), "{parts} parts");
let mut back = one_field_named(&name, "old");
let report = import(&mut back, xfdf.as_bytes()).unwrap();
assert_eq!((report.fields, values(&back)), (1, vec![(name.clone(), vec!["v".to_string()])]), "{parts} parts");
}
}
/// A name of a hundred thousand parts nested as deep, and writing it out recursed once per level
/// until the stack overflowed, which aborts the process.
#[test]
fn a_field_name_of_many_parts_exports_without_overflowing_the_stack() {
let name = vec!["p"; 100_000].join(".");
let want = vec![(name.clone(), vec!["v".to_string()])];
let read = on_small_stack(move || data_values(export_xfdf(&one_field_named(&name, "v"), false, true, "form.pdf").as_bytes()));
assert_eq!(read, Ok(want));
}
#[test]
fn duplicate_xfdf_annotation_flags_do_not_turn_on_unrelated_bits() {
// Flags are a set of bit masks, not additive integers. A repeated "print" flag