From ae93ea5115a007ca3dce137825b110549aa9f8da Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 20:55:20 -0500 Subject: [PATCH] table: refuse to split more than 1024 merged-cell owners at once (#2273) --- crates/doc/src/table/merge.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/doc/src/table/merge.rs b/crates/doc/src/table/merge.rs index 7f3faecfe..1ccdebe0a 100644 --- a/crates/doc/src/table/merge.rs +++ b/crates/doc/src/table/merge.rs @@ -5,6 +5,9 @@ use super::grid::CellRange; use super::story::append_story; use super::{Story, Table}; +/// Most merged-cell owners one `split_cells` call splits. +const MAX_SPLIT_OWNERS: usize = 1024; + impl Table { /// The smallest range containing `r` that doesn't cut through a merged region. pub fn expand_range(&self, mut r: CellRange) -> CellRange { @@ -78,6 +81,11 @@ impl Table { selected.insert(*owner); } } + // Each owner is split by a transformation that rebuilds the grid's ownership: quadratic in the + // owners, so a very large selection is refused (a table this size is split in parts). + if selected.len() > MAX_SPLIT_OWNERS { + return Err(format!("too many cells to split at once (at most {MAX_SPLIT_OWNERS})")); + } let mut targets = Vec::new(); for (r, c) in selected { let cell = self.cell(r, c).ok_or("missing split cell")?; @@ -232,6 +240,18 @@ impl Table { mod split_tests { use super::*; + #[test] + fn splitting_a_huge_selection_is_refused() { + let mut t = Table::new(1, 40, 40, 0, 0, 400.0); + let all = CellRange { r0: 0, c0: 0, r1: 39, c1: 39 }; + let before = (t.nrows(), t.ncols()); + assert!(t.split_cells(all, true).unwrap_err().contains("too many cells")); + assert_eq!((t.nrows(), t.ncols()), before, "nothing was split"); + // A selection within the limit still splits. + let some = CellRange { r0: 0, c0: 0, r1: 1, c1: 1 }; + assert!(t.split_cells(some, true).is_ok()); + } + #[test] fn split_single_and_merged_cells() { let mut t = Table::new(1, 2, 2, 0, 0, 100.0);