From a0308dfa88d5f89bb6527d42874aadcea3f10779 Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Tue, 29 Sep 2026 08:20:41 +0100 Subject: [PATCH] fix(data-link): clamp cell-range column window to the data width Bug: crop_range bounded the row axis with skip/take over the file's real rows but left the column axis unbounded: c0..=c1 collects one String per column, and a range like A1:ZZZZZZ1 parses to roughly 308 million columns, causing a capacity overflow while allocating a row. Reproduce: 1. Data Link -> pick any CSV/xlsx, set Cell Range to A1:ZZZZZZ1 (or any range whose parsed column index is enormous). 2. Before: "capacity overflow" abort while building the preview. 3. After: the window is cropped to the file's widest row; columns past the data yield nothing beyond the data width. Fix: c1 is clamped to widest.saturating_sub(1) before the range is collected, so the span is provably at most the file's own data - allocation is O(file). Rows outside the file still come back empty (in-range blank padding inside the data width is preserved), and c0 > c1 naturally yields an empty window. Regression tests cover the 13-Z capacity-overflow input and a 6-Z range that now crops. --- src/app/annotation_data.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/app/annotation_data.rs b/src/app/annotation_data.rs index b87342b9d..14dba315b 100644 --- a/src/app/annotation_data.rs +++ b/src/app/annotation_data.rs @@ -151,6 +151,12 @@ fn crop_range(rows: Vec>, range: &str) -> Result>, S }; let (r0, r1) = (r0.min(r1), r0.max(r1)); let (c0, c1) = (c0.min(c1), c0.max(c1)); + // The row axis is bounded by the file (skip/take over real rows); the + // column axis was not — `A1:ZZZZZZ1` parses fine and then collected one + // String per column (~308 M slots per row, OOM). Columns past the widest + // row are empty in every row, so clamp the window to the data width. + let widest = rows.iter().map(Vec::len).max().unwrap_or(0); + let c1 = c1.min(widest.saturating_sub(1)); Ok(rows.into_iter() .skip(r0) .take(r1 - r0 + 1) @@ -1451,4 +1457,25 @@ mod tests { ] ); } + + /// `column_index` only checks `checked_mul(26)`, so a range like + /// `A1:ZZZZZZ1` parses fine — and `(c0..=c1)` then collects one `String` + /// per column: ~308 M slots (~7 GB) for 6 Z's, and a `capacity overflow` + /// panic at 13 Z's. Columns past the widest row are empty in *every* + /// row, so the window must be clamped to the data width. + #[test] + fn crop_range_bounds_oversized_column_spans() { + let rows = vec![vec!["x".to_string()]]; + // 13 Z's: pre-fix, size_hint × size_of::() exceeds isize::MAX + // and Vec's capacity check panics before a single cell is read. + assert_eq!( + crop_range(rows.clone(), "A1:ZZZZZZZZZZZZZ1").unwrap(), + vec![vec!["x".to_string()]] + ); + // 6 Z's — the bug-hunt report's repro — stays bounded too. + assert_eq!( + crop_range(rows, "A1:ZZZZZZ1").unwrap(), + vec![vec!["x".to_string()]] + ); + } }