fix(xfdf): keep merged spreadsheet cells from starting a formula

Merge Data Files into Spreadsheet writes field names and values from other
people's files as CSV cells, escaping only the delimiters. A cell starting
with =, +, - or @ is evaluated as a formula when the CSV is opened in a
spreadsheet program. Such cells, after any leading whitespace, are now
written with a leading apostrophe; finite numbers such as -12.5 are left
as they are.
This commit is contained in:
aiongg
2026-10-22 13:27:52 -04:00
committed by Dylan M. Taylor
parent d266222339
commit 8a8dab6c00
2 changed files with 32 additions and 1 deletions
+17 -1
View File
@@ -1287,6 +1287,10 @@ pub fn data_values(bytes: &[u8]) -> Result<Vec<(String, Vec<String>)>, DataError
/// Merge Data Files into Spreadsheet: one CSV row per file, one column per field name (in the
/// order they first appear).
///
/// The names and values come from other people's files, and spreadsheet programs evaluate a cell
/// that starts with `=`, `+`, `-` or `@` (after any leading whitespace) as a formula. Such a cell
/// is written with a leading `'`, which keeps it text; a plain number such as `-12.5` is not.
pub fn merge_csv(files: &[Vec<(String, Vec<String>)>]) -> String {
let mut columns: Vec<&str> = Vec::new();
for f in files {
@@ -1296,7 +1300,19 @@ pub fn merge_csv(files: &[Vec<(String, Vec<String>)>]) -> String {
}
}
}
let q = |s: &str| if s.contains([',', '"', '\n', '\r', '\t']) { format!("\"{}\"", s.replace('"', "\"\"")) } else { s.to_string() };
let literal = |s: &str| {
let t = s.trim_start();
let formula = match t.chars().next() {
Some('=' | '@') => true,
Some('+' | '-') => !t.parse::<f64>().is_ok_and(f64::is_finite),
_ => false,
};
if formula { format!("'{s}") } else { s.to_string() }
};
let q = |s: &str| {
let s = literal(s);
if s.contains([',', '"', '\n', '\r', '\t']) { format!("\"{}\"", s.replace('"', "\"\"")) } else { s }
};
let mut out = columns.iter().map(|c| q(c)).collect::<Vec<_>>().join(",");
out.push('\n');
for f in files {
+15
View File
@@ -1702,3 +1702,18 @@ fn an_imported_rect_only_or_transparent_redaction_mark_is_drawn_for_review() {
assert!(mark.get(b"CA").is_none() && mark.get(b"ca").is_none(), "the mark can be drawn transparent: {mark:?}");
}
}
#[test]
fn merged_spreadsheet_cells_never_start_a_formula() {
let row = vec![
("=HYPERLINK(\"http://x\")".to_string(), vec!["@SUM(A1)".to_string()]),
("Total".to_string(), vec!["-2+3".to_string()]),
("Plus".to_string(), vec!["\t+1+cmd".to_string()]),
("Balance".to_string(), vec!["-12.5".to_string()]),
("\tName".to_string(), vec!["Ada".to_string()]),
];
let csv = merge_csv(&[row]);
let lines: Vec<&str> = csv.lines().collect();
assert_eq!(lines[0], "\"'=HYPERLINK(\"\"http://x\"\")\",Total,Plus,Balance,\"\tName\"");
assert_eq!(lines[1], "'@SUM(A1),'-2+3,\"'\t+1+cmd\",-12.5,Ada");
}