From 7cd0a386aa4af37aaca234ad5b15a45e70164627 Mon Sep 17 00:00:00 2001 From: Mouhand-Kaddo Date: Mon, 19 Oct 2026 17:29:40 +0400 Subject: [PATCH] M12.AR17: skip .xls dialog sheets instead of reading them as worksheets --- crates/xlsx/src/tests/xls_tests.rs | 14 ++++++++++++++ crates/xlsx/src/xls/mod.rs | 4 ++++ docs/file-format-parity.md | 3 ++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/xlsx/src/tests/xls_tests.rs b/crates/xlsx/src/tests/xls_tests.rs index 91e63c18f..cf99a9447 100644 --- a/crates/xlsx/src/tests/xls_tests.rs +++ b/crates/xlsx/src/tests/xls_tests.rs @@ -454,6 +454,20 @@ fn hostile_xls_input_never_panics_or_hangs() { } } +#[test] +fn dialog_sheets_are_skipped_not_read_as_worksheets() { + let boundsheet = |pos: u32, name: &str| rec(0x0085, &cat(&[&pos.to_le_bytes(), &[0, 0, name.len() as u8, 0], name.as_bytes()])); + let globals = |positions: [u32; 2]| cat(&[&bof(0x0005), &boundsheet(positions[0], "Dialog1"), &boundsheet(positions[1], "Sheet1"), &eof()]); + let dialog = cat(&[&bof(0x0010), &rec(0x0081, &[0x10, 0x00]), &rec(0x0203, &cat(&[&cell(0, 0, 0), &1.0f64.to_le_bytes()])), &eof()]); + let worksheet = cat(&[&bof(0x0010), &rec(0x0081, &[0x01, 0x04]), &rec(0x0203, &cat(&[&cell(0, 0, 0), &7.0f64.to_le_bytes()])), &eof()]); + let first = globals([0; 2]).len() as u32; + let stream = cat(&[&globals([first, first + dialog.len() as u32]), &dialog, &worksheet]); + let (wb, report) = read_xls(&compound("Workbook", &stream)).unwrap(); + assert_eq!(wb.sheets.iter().map(|s| s.name.as_str()).collect::>(), ["Sheet1"]); + assert_eq!(v(&wb, 0, "A1"), Value::Number(7.0)); + assert!(report.warnings.iter().any(|w| w.contains("Dialog sheets")), "{:?}", report.warnings); +} + #[test] fn sheets_share_one_record_budget() { // Many BoundSheet records point into one long substream (at successive nested BOFs), so each diff --git a/crates/xlsx/src/xls/mod.rs b/crates/xlsx/src/xls/mod.rs index dde8641e5..7a5e6e4b3 100644 --- a/crates/xlsx/src/xls/mod.rs +++ b/crates/xlsx/src/xls/mod.rs @@ -40,6 +40,9 @@ const MAX_RECORDS: usize = 40_000_000; const BOF: u16 = 0x0809; const EOF: u16 = 0x000A; const BIFF8: u16 = 0x0600; +/// WsBool (MS-XLS 2.4.351): its first byte's `fDialog` bit marks a dialog sheet, whose BOF says worksheet. +const WSBOOL: u16 = 0x0081; +const WSBOOL_DIALOG: u8 = 0x10; pub(crate) fn invalid(msg: impl Into) -> IoError { IoError::Format(format!("XLS: {}", msg.into())) @@ -524,6 +527,7 @@ fn worksheet( continue; } _ if depth > 1 => continue, + WSBOOL if rec.parts.first().and_then(|p| p.first()).is_some_and(|b| b & WSBOOL_DIALOG != 0) => return Ok(None), _ => {} } sheet_record(&rec, &mut st, budget, report)?; diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 158149fb3..ab60b59a9 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File-format parity -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-14 · **Change:** trivial (formulas over Excel's 8,192 characters, and `%` or call chains past the nesting limit, are parse errors instead of stack overflows when a file is opened; previously trivial: XLSX chart sheets are kept on save (#727)) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-19 · **Change:** minor (.xls dialog sheets are skipped instead of read as worksheets) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 Every format Excel opens or saves, with GridCraft's read and write support, fidelity and tests. Part of [target-app-parity.md](target-app-parity.md). **File formats: ~50% ready, 80–120 h** @@ -85,6 +85,7 @@ refresh. Counted under features (Get & Transform, ~10% ready), not here. | Date | Change | Summary | |---|---|---| +| 2026-10-19 | minor | .xls import skips dialog sheets (WsBool `fDialog`), which share the worksheet BOF type and were read as empty worksheets | | 2026-10-14 | trivial | Formula parser: text longer than Excel's 8,192 characters, and postfix chains (`1%%%…`, `f(1)(1)…`) past the 256-level nesting limit, are parse errors (the cell keeps its formula text and cached value) instead of trees deep enough to overflow the stack when a hostile file is opened, saved or dropped | | 2026-10-12 | trivial | XLSX chart sheets are kept on save (#727) | | 2026-10-12 | trivial | CSV and TSV export is capped at 512 MiB and refuses a larger used range with an error (#528) |