From c20f4c2fcbbc94cebba313d58dc469d44ae00d42 Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Mon, 12 Oct 2026 08:32:35 +0200 Subject: [PATCH] Fail Web Page export on sheets taller than its row limit instead of dropping cells (#514) --- crates/engine/src/io.rs | 33 ++++++++++++++++++++++++++++----- docs/file-format-parity.md | 2 +- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/io.rs b/crates/engine/src/io.rs index e613611..7dc4dff 100644 --- a/crates/engine/src/io.rs +++ b/crates/engine/src/io.rs @@ -167,7 +167,7 @@ pub fn save_bytes(wb: &Workbook, path: &str) -> Result> { FileKind::Csv => Ok(wb.sheet(sheet).map(|sh| csv(wb, sh, wb.csv_delimiter.unwrap_or(b','))).unwrap_or_default()), FileKind::Tsv => Ok(wb.sheet(sheet).map(|sh| csv(wb, sh, b'\t')).unwrap_or_default()), FileKind::Json => serde_json::to_vec_pretty(wb).map_err(|e| EngineError::Other(e.to_string())), - FileKind::Html => Ok(to_html(wb, sheet).into_bytes()), + FileKind::Html => to_html(wb, sheet).map(String::into_bytes), } } @@ -285,9 +285,12 @@ impl std::fmt::Write for ClipboardHtml { } } +/// Most rows Save as Web Page writes. A sheet whose used range is taller fails instead of losing cells. +const HTML_EXPORT_MAX_ROWS: u32 = 100_001; + /// A sheet as a standalone HTML table (Save as Web Page). -pub fn to_html(wb: &Workbook, sheet: usize) -> String { - let Some(sh) = wb.sheet(sheet) else { return String::new() }; +pub fn to_html(wb: &Workbook, sheet: usize) -> Result { + let Some(sh) = wb.sheet(sheet) else { return Ok(String::new()) }; let mut out = String::new(); let _ = write!( out, @@ -295,7 +298,13 @@ pub fn to_html(wb: &Workbook, sheet: usize) -> String { esc(&sh.name) ); if let Some(r) = sh.used_range() { - for row in r.start.row..=r.end.row.min(r.start.row + 100_000) { + if r.height() > HTML_EXPORT_MAX_ROWS { + return Err(EngineError::Other(format!( + "the sheet is too tall for a web page ({} rows, at most {HTML_EXPORT_MAX_ROWS}). Save as .xlsx or CSV instead.", + r.height() + ))); + } + for row in r.start.row..=r.end.row { if sh.is_row_hidden(row) { continue; } @@ -318,7 +327,7 @@ pub fn to_html(wb: &Workbook, sheet: usize) -> String { } } out.push_str(""); - out + Ok(out) } #[cfg(not(target_arch = "wasm32"))] @@ -418,6 +427,20 @@ pub fn image_size(data: &[u8]) -> Option<(u32, u32)> { mod tests { use super::*; + #[test] + fn html_export_fails_instead_of_dropping_cells_past_the_row_limit() { + let mut wb = Workbook::new(); + let sh = wb.sheet_mut(0).unwrap(); + sh.set_value(CellRef::new(0, 0), gridcraft_core::Value::text("near")); + sh.set_value(CellRef::new(149_999, 0), gridcraft_core::Value::text("far")); + assert!(save_bytes(&wb, "book.html").is_err()); + let sh = wb.sheet_mut(0).unwrap(); + sh.remove_cell(CellRef::new(149_999, 0)); + sh.set_value(CellRef::new(100_000, 0), gridcraft_core::Value::text("far")); + let html = String::from_utf8(save_bytes(&wb, "book.html").unwrap()).unwrap(); + assert!(html.contains(">near") && html.contains(">far")); + } + #[test] fn macros_are_saved_only_into_xlsm() { let mut wb = Workbook::new(); diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 8510659..22bc4ec 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -40,7 +40,7 @@ support comes from `crates/engine/src/io.rs` (`FileKind`), `crates/xlsx/src` and | `.txt` tab-delimited, Unicode text | R/W | yes (`.txt`, `.tsv`, `.tab`) | yes (displayed values) | Text Import Wizard options partly via Text to Columns | | `.prn` formatted text (space-delimited) | R/W | no | no | | | `.dif`, `.slk` (SYLK) | R/W | no | no | Small, legacy | -| `.htm` / `.html` web page | R/W (Windows) | yes, via Get Data ▸ From HTML | yes (one sheet as a table) | Formatting not exported | +| `.htm` / `.html` web page | R/W (Windows) | yes, via Get Data ▸ From HTML | yes (one sheet as a table; a used range taller than 100,001 rows fails with an error instead of being cut short) | Formatting not exported | | `.mht` / `.mhtml` single-file web page | R/W (Windows) | no | no | | | `.json` | via Power Query | GridCraft's own workbook JSON (debug) and JSON import | GridCraft JSON | Not Power Query's JSON shaping; notes and hyperlinks break export (#126) | | `.xml` with XML maps | Developer ▸ Import/Export | no | no | |