mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-01 14:46:51 -06:00
engine: rebuild cell-store bands and counts when importing a JSON workbook
This commit is contained in:
@@ -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() };
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
Reference in New Issue
Block a user