fix(xfdf): text data from a one-field form keeps its commas (#1321) (#1371)

The tab-delimited export of a single field has no tab, so import read it
as CSV and kept only the text before the first comma. CSV quotes a value
that holds a comma, so a single name over several unquoted cells is now
read as that tab-delimited text. Exported files are unchanged, and CSV
(quoted, multi-line, one or more columns) reads as before.
This commit is contained in:
Marco Arnulfo authored and GitHub committed 2026-10-12 06:24:27 -04:00
1 parent d2d1b16b21
commit ec5d9da36c
2 files changed
+62 -4

No files matched your search

+11 -4
View File
@@ -1093,9 +1093,7 @@ fn import_table(doc: &mut Document, text: &str) -> Result<Report, DataError> {
})
.collect()
} else if t.lines().next().is_some_and(|l| l.contains('\t')) {
let mut lines = t.lines();
let (Some(head), Some(row)) = (lines.next(), lines.next()) else { return Err(DataError::UnknownFormat) };
head.split('\t').map(str::to_string).zip(row.split('\t')).map(|(k, v)| (k, vec![v.to_string()])).collect()
tab_delimited(t)?
} else {
// CSV with quotes: a quoted value may hold commas, doubled quotes and line breaks.
let mut records: Vec<Vec<String>> = Vec::new();
@@ -1124,12 +1122,21 @@ fn import_table(doc: &mut Document, text: &str) -> Result<Report, DataError> {
// Blank lines (such as the one a trailing line break leaves) are not records.
let mut records = records.into_iter().filter(|r| !matches!(r.as_slice(), [only] if only.is_empty()));
let (Some(head), Some(row)) = (records.next(), records.next()) else { return Err(DataError::UnknownFormat) };
head.into_iter().zip(row).map(|(k, v)| (k, vec![v])).collect()
// Text exported from a one-field form has no tab to show for it (#1321). CSV quotes a
// value with a comma, so one name over several unquoted cells is that text, not CSV.
if head.len() == 1 && row.len() > 1 { tab_delimited(t)? } else { head.into_iter().zip(row).map(|(k, v)| (k, vec![v])).collect() }
};
apply_values(doc, &values, &mut report);
Ok(report)
}
/// Tab-delimited text: a row of names, a row of values.
fn tab_delimited(t: &str) -> Result<Vec<(String, Vec<String>)>, DataError> {
let mut lines = t.lines();
let (Some(head), Some(row)) = (lines.next(), lines.next()) else { return Err(DataError::UnknownFormat) };
Ok(head.split('\t').map(str::to_string).zip(row.split('\t')).map(|(k, v)| (k, vec![v.to_string()])).collect())
}
/// Import comments and/or field values from XFDF, FDF, XML, CSV or tab-delimited text
/// (detected from the content).
pub fn import(doc: &mut Document, bytes: &[u8]) -> Result<Report, DataError> {
+51
View File
@@ -328,6 +328,57 @@ fn csv_values_may_span_lines() {
assert!(import(&mut blank(), b"Name,Agree\r\n\"Ada, Yes\r\n").is_ok());
}
/// One page with one text field, `Name`.
fn one_field() -> Document {
let mut doc = Document::new_empty();
let pages = doc.get(doc.root().unwrap()).as_dict().unwrap().reference(b"Pages").unwrap();
let mut p = Dict::new();
p.set(b"Type".to_vec(), Object::name("Page"));
p.set(b"Parent".to_vec(), Object::Ref(pages));
p.set(b"MediaBox".to_vec(), Object::Array(vec![0.into(), 0.into(), 600.into(), 800.into()]));
let page = doc.add(p);
doc.update_dict(pages, |d| {
d.set(b"Count".to_vec(), Object::Int(1));
d.set(b"Kids".to_vec(), Object::Array(vec![Object::Ref(page)]));
})
.unwrap();
add_field(&mut doc, 0, [50.0, 700.0, 250.0, 720.0], &NewField::Text { multiline: false }, Some("Name")).unwrap();
Document::open(Arc::new(write_full(&doc, &SaveOptions::default()).unwrap())).unwrap()
}
/// #1321: the text export of a one-field form has no tab to tell it from CSV, so a comma in the
/// value used to split it and only the text before the comma was imported.
#[test]
fn one_field_text_data_keeps_its_commas() {
for value in ["Hello, world", "Say \"hi\", twice"] {
let mut src = one_field();
set_value(&mut src, "Name", &FieldValue::Text(value.into())).unwrap();
let txt = export_data(&src, Format::Txt);
assert_eq!(txt, format!("Name\n{value}\n"));
let mut dst = one_field();
let r = import(&mut dst, txt.as_bytes()).unwrap();
assert_eq!(r.fields, 1, "{r:?}");
assert!(r.rejected.is_empty(), "{r:?}");
assert_eq!(values(&dst)[0].1, [value]);
}
}
/// A one-column CSV is still CSV: quotes come off, and a quoted value keeps its commas and line
/// breaks.
#[test]
fn one_column_csv_still_reads_its_quotes() {
let mut src = one_field();
set_value(&mut src, "Name", &FieldValue::Text("Hello, world".into())).unwrap();
let csv = export_data(&src, Format::Csv);
assert_eq!(csv, "Name\n\"Hello, world\"\n");
let mut dst = one_field();
assert_eq!(import(&mut dst, csv.as_bytes()).unwrap().fields, 1);
assert_eq!(values(&dst)[0].1, ["Hello, world"]);
let mut dst = one_field();
assert_eq!(import(&mut dst, b"Name\r\n\"Ada \"\"the\"\", \r\nCountess\"\r\n").unwrap().fields, 1);
assert!(values(&dst)[0].1[0].starts_with("Ada \"the\", ") && values(&dst)[0].1[0].ends_with("Countess"), "{:?}", values(&dst));
}
#[test]
fn multi_select_lists_round_trip_through_xml_csv_and_text() {
let src = filled();