diff --git a/crates/xfdf/src/lib.rs b/crates/xfdf/src/lib.rs index 35bc78154..30512f04e 100644 --- a/crates/xfdf/src/lib.rs +++ b/crates/xfdf/src/lib.rs @@ -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 `` 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 => { diff --git a/crates/xfdf/src/tests.rs b/crates/xfdf/src/tests.rs index c33c115e7..8e6f753a8 100644 --- a/crates/xfdf/src/tests.rs +++ b/crates/xfdf/src/tests.rs @@ -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 `` 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::>().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