From 2b74366c64cdfb504d6c52689d54b0343253cf01 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Mon, 2 Nov 2026 10:53:08 +0800 Subject: [PATCH] M6: the engine checks that the XFA data layer and forms spell state names alike pdfcraft-xfa keeps its own copy of pdfcraft_forms::name_text (it can't depend on forms), and a radio group's value only finds its button while the two agree. xfa_and_forms_spell_names_alike compares them, and checks that forms' name_bytes reads the copy's spelling back, for UTF-8, '#', Shift-JIS and stray bytes. With the copy writing lowercase hex it fails at the Shift-JIS case. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv --- crates/engine/src/tests.rs | 11 +++++++++++ crates/xfa/src/data.rs | 4 +--- crates/xfa/src/data/values.rs | 3 ++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 0759685ff6..111696b731 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -6146,6 +6146,17 @@ fn a_static_xfa_radio_group_with_numeric_items_keeps_the_picked_button() { } } +/// The XFA data layer keeps its own copy of the forms crate's name spelling (it can't depend on +/// forms), and a radio group's value only finds its button while the two agree: UTF-8 as +/// itself, `#` and bytes that aren't UTF-8 as `#XX`. +#[test] +fn xfa_and_forms_spell_names_alike() { + for bytes in [&b"Yes"[..], b"", b"a b", b"A#1", "はい".as_bytes(), b"\x82\xcd\x82\xa2", b"\xff#\x00", b"\x7f\x20~"] { + assert_eq!(pdfcraft_xfa::data::name_text(bytes), pdfcraft_forms::name_text(bytes), "{bytes:?}"); + assert_eq!(pdfcraft_forms::name_bytes(&pdfcraft_xfa::data::name_text(bytes)), bytes, "{bytes:?}"); + } +} + /// Taking the XFA out of a radio group whose buttons carry `/PCOn` as well as `/Opt` (the item /// saving writes ahead of the `/Opt` entry) notes no lost data when the data holds that item: /// one equal to the on state, and one equal to neither the state nor the `/Opt` entry. diff --git a/crates/xfa/src/data.rs b/crates/xfa/src/data.rs index 1455586383..ba17c65ca6 100644 --- a/crates/xfa/src/data.rs +++ b/crates/xfa/src/data.rs @@ -36,9 +36,7 @@ pub use import::{DataImport, ImportNode, export_data, import_data, parse_data_fi pub use passwords::{clear_password_values, has_password_values}; pub(crate) use path::template_field; pub use path::{data_path, parse_datasets, som_to_path}; -#[cfg(test)] -pub(crate) use values::name_text; -pub use values::{FieldData, FieldDatum, is_on, radio_dataset_value, radio_state_for_item, read_values, read_values_with}; +pub use values::{FieldData, FieldDatum, is_on, name_text, radio_dataset_value, radio_state_for_item, read_values, read_values_with}; #[cfg(test)] pub(crate) use write::ENSURED; pub use write::{ diff --git a/crates/xfa/src/data/values.rs b/crates/xfa/src/data/values.rs index 294b407540..3fb8b3ca97 100644 --- a/crates/xfa/src/data/values.rs +++ b/crates/xfa/src/data/values.rs @@ -76,7 +76,8 @@ fn radio_widgets(doc: &Document, f: &FieldDatum) -> Vec<(Option, Option< /// written `#XX`, as in PDF name syntax. This is `pdfcraft_forms::name_text`, which this crate /// can't depend on; the value saving gets is read through it, and the state opening returns is /// written back through its inverse, `pdfcraft_forms::name_bytes`. -pub(crate) fn name_text(bytes: &[u8]) -> String { +/// The engine checks that the two spell names alike (`xfa_and_forms_spell_names_alike`). +pub fn name_text(bytes: &[u8]) -> String { let escape = |b: u8| format!("#{b:02X}"); match std::str::from_utf8(bytes) { Ok(s) => s.replace('#', "#23"),