diff --git a/crates/xfdf/src/lib.rs b/crates/xfdf/src/lib.rs index 80c37b5e8..1eac1f96c 100644 --- a/crates/xfdf/src/lib.rs +++ b/crates/xfdf/src/lib.rs @@ -1287,6 +1287,10 @@ pub fn data_values(bytes: &[u8]) -> Result)>, 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 { let mut columns: Vec<&str> = Vec::new(); for f in files { @@ -1296,7 +1300,19 @@ pub fn merge_csv(files: &[Vec<(String, Vec)>]) -> 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::().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::>().join(","); out.push('\n'); for f in files { diff --git a/crates/xfdf/src/tests.rs b/crates/xfdf/src/tests.rs index 7951bef4d..3e6dc58a5 100644 --- a/crates/xfdf/src/tests.rs +++ b/crates/xfdf/src/tests.rs @@ -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"); +}