diff --git a/crates/engine/src/io.rs b/crates/engine/src/io.rs index 6dd96b0424..95cb4eba74 100644 --- a/crates/engine/src/io.rs +++ b/crates/engine/src/io.rs @@ -350,7 +350,13 @@ fn read_plain(name: &str, bytes: &[u8], csv_as_text: bool) -> Result<(Workbook, let opts = gridcraft_xlsx::CsvOptions { delimiter: 0, parse_values: !csv_as_text, ..Default::default() }; let (mut wb, report) = gridcraft_xlsx::read_csv_with_report(bytes, &opts).map_err(|e| EngineError::Other(e.to_string()))?; rename_first_sheet(&mut wb, name); - Ok((wb, report.warnings)) + let mut warnings = report.warnings; + // Text under an Excel package name is a truncated, unsynced or misnamed file far more + // often than a CSV; it still opens as text, but says so (#3975). + if kind == Some(FileKind::Xlsx) { + warnings.insert(0, format!("'{name}' isn't a valid .xlsx package (it is not a ZIP file), so it was read as plain text.")); + } + Ok((wb, warnings)) } } } diff --git a/crates/engine/src/io/tests.rs b/crates/engine/src/io/tests.rs index ad8ca40f25..13343d3fcc 100644 --- a/crates/engine/src/io/tests.rs +++ b/crates/engine/src/io/tests.rs @@ -881,3 +881,14 @@ fn the_merge_index_holds_a_bounded_number_of_cells() { sh.merges = vec![RangeRef::new(CellRef::new(0, 0), CellRef::new(1_000_000, 0))]; assert!(merge_index(&sh, rect).is_none()); } + +#[test] +fn text_named_xlsx_opens_with_a_warning_but_real_csv_does_not() { + // #3975: a ".xlsx" that isn't a ZIP package was read as a one-cell text file without a word. + let (wb, warnings) = open_bytes("broken.xlsx", b"kein zip\n").unwrap(); + assert_eq!(wb.sheets.len(), 1); + assert!(warnings.iter().any(|w| w.contains("broken.xlsx") && w.contains("isn't a valid .xlsx package")), "{warnings:?}"); + // Text under an honest name opens as before, with no warning. + let (_, warnings) = open_bytes("book.csv", b"a,b\n1,2\n").unwrap(); + assert!(warnings.is_empty(), "{warnings:?}"); +}