From 34a4ed3eadffd28119b02463819b35d0eed3da26 Mon Sep 17 00:00:00 2001 From: midasmatterhorn <248507632+midasmatterhorn@users.noreply.github.com> Date: Thu, 15 Oct 2026 00:09:25 +0300 Subject: [PATCH] engine: rebuild cell-store bands and counts when importing a JSON workbook --- crates/engine/src/io.rs | 9 ++++++++- crates/engine/src/tests.rs | 32 ++++++++++++++++++++++++++++++++ crates/model/src/store.rs | 22 ++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/io.rs b/crates/engine/src/io.rs index 3e84036cb..f52b15280 100644 --- a/crates/engine/src/io.rs +++ b/crates/engine/src/io.rs @@ -150,7 +150,14 @@ fn open_plain(name: &str, bytes: &[u8]) -> Result<(Workbook, Vec)> { if wb.sheets.is_empty() { wb = Workbook::new(); } - Ok((wb, vec![])) + // The file's band placement and cell count are derived state: rebuild them from the + // cells themselves, dropping what is outside the sheet grid (#1008). + let dropped: usize = wb.sheets.iter_mut().map(|sh| std::sync::Arc::make_mut(sh).cells.rebuild()).sum(); + let mut warnings = Vec::new(); + if dropped > 0 { + warnings.push(format!("{name}: {dropped} cells outside the 1,048,576 × 16,384 grid were dropped")); + } + Ok((wb, warnings)) } Some(FileKind::Tsv) => { let opts = gridcraft_xlsx::CsvOptions { delimiter: b'\t', ..Default::default() }; diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index ae499a189..2ce3418ff 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -2908,6 +2908,38 @@ fn autofill_month_pattern_clamps_to_month_end() { assert_eq!(v(&s, "A6"), ymd(2026, 11, 30)); } +#[test] +fn json_import_rebuilds_cell_store_bands_and_counts() { + // #1008: the JSON reader trusted serialized band keys and the cached length. A file written + // with a cell in the wrong band lost it from lookup, and a forged count underflowed on the + // next removal. + let mut s = s(); + s.execute("cell.set", json!({"cell": "A5", "input": "keep"})).unwrap(); + let wb = &s.doc().unwrap().wb; + let mut v: serde_json::Value = serde_json::to_value(&**wb).unwrap(); + { + let cells = v["sheets"][0]["cells"].as_object_mut().unwrap(); + let bands = cells["bands"].as_object_mut().unwrap(); + let band = bands.remove("0").expect("the cell is in band 0"); + let cell = band["rows"]["4"]["0"].clone(); + // Off-grid rows never belong in a sheet, whatever band holds them. + bands.insert("1".into(), band); + bands.insert("16384".into(), json!({"rows": {"1048576": {"0": cell}}})); + cells.insert("len".into(), json!(0)); + } + let bytes = serde_json::to_vec(&v).unwrap(); + let (back, warnings) = crate::io::open_bytes("book.json", &bytes).unwrap(); + let sh = back.active().unwrap(); + let cell = sh.cell(CellRef::parse("A5").unwrap()); + assert_eq!(cell.map(|c| c.value.clone()), Some(Value::text("keep")), "the cell is found where its row says"); + assert_eq!(sh.cells.len(), 1, "the count is rebuilt from the cells"); + assert!(warnings.iter().any(|w| w.contains("outside the 1,048,576")), "{warnings:?}"); + // Removal stays consistent: the forged count used to underflow. + let mut cells = sh.cells.clone(); + cells.remove(CellRef::parse("A5").unwrap()); + assert_eq!(cells.len(), 0); +} + #[test] fn notes_comments_and_links_survive_a_json_save() { let mut s = s(); diff --git a/crates/model/src/store.rs b/crates/model/src/store.rs index d6a9c4561..51a8e72fd 100644 --- a/crates/model/src/store.rs +++ b/crates/model/src/store.rs @@ -57,6 +57,28 @@ impl CellStore { self.len += 1; } } + /// Puts every cell in the band its row says and recounts the length — serialized band keys + /// and the cached count are derived state, and the ones a file carries are not trusted. Cells + /// outside the sheet grid are dropped; returns how many (#1008). + pub fn rebuild(&mut self) -> usize { + let mut out = CellStore::new(); + let mut dropped = 0; + for (_, band) in std::mem::take(&mut self.bands) { + for (row, cols) in band.rows.iter() { + for (col, cell) in cols.iter() { + let at = CellRef::new(*row, *col); + if at.is_valid() { + out.set(at, cell.clone()); + } else { + dropped += 1; + } + } + } + } + *self = out; + dropped + } + pub fn remove(&mut self, c: CellRef) -> Option { let key = c.row / BAND; let band = self.bands.get_mut(&key)?;