engine: rebuild cell-store bands and counts when importing a JSON workbook

This commit is contained in:
midasmatterhorn
2026-10-15 00:09:25 +03:00
parent d9d09f0baa
commit 34a4ed3ead
3 changed files with 62 additions and 1 deletions
+8 -1
View File
@@ -150,7 +150,14 @@ fn open_plain(name: &str, bytes: &[u8]) -> Result<(Workbook, Vec<String>)> {
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() };
+32
View File
@@ -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();
+22
View File
@@ -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<Cell> {
let key = c.row / BAND;
let band = self.bands.get_mut(&key)?;