From 9bbb2636617d324ba348cc45bf7bf4b47a04a4db Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Mon, 12 Oct 2026 08:30:28 +0200 Subject: [PATCH] HSTACK checks its padded output size before building it (#536) --- crates/functions/src/dynamic.rs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/crates/functions/src/dynamic.rs b/crates/functions/src/dynamic.rs index 3a66ca8..86f5afc 100644 --- a/crates/functions/src/dynamic.rs +++ b/crates/functions/src/dynamic.rs @@ -30,6 +30,11 @@ fn rows_of(a: &Array) -> Vec> { (0..a.rows).map(|r| (0..a.cols).map(|c| a.get(r, c).cloned().unwrap_or(Value::Empty)).collect()).collect() } +/// `#NUM!` when an `h` x `w` output would exceed the function cell budget. +fn check_output_size(h: usize, w: usize) -> R<()> { + if h.saturating_mul(w) > MAX_CELLS { Err(CellError::Num) } else { Ok(()) } +} + /// Builds an array from rows (all rows padded to the widest with `pad`). Empty → `#CALC!`. fn from_rows(rows: Vec>, pad: &Value) -> R { let h = rows.len(); @@ -37,9 +42,7 @@ fn from_rows(rows: Vec>, pad: &Value) -> R { if h == 0 || w == 0 { return Err(CellError::Calc); } - if h.saturating_mul(w) > MAX_CELLS { - return Err(CellError::Num); - } + check_output_size(h, w)?; let mut data = Vec::with_capacity(h * w); for mut r in rows { r.resize(w, pad.clone()); @@ -321,16 +324,20 @@ fn vstack(args: &[Arg], _c: &mut dyn Ctx) -> R { fn hstack(args: &[Arg], _c: &mut dyn Ctx) -> R { let mut arrays = Vec::new(); let mut h = 0usize; + let mut w = 0usize; let mut total = 0usize; for i in 0..args.len() { let a = arr_arg(args, i)?; h = h.max(a.rows); + w = w.saturating_add(a.cols); total = total.saturating_add(a.data.len()); if total > MAX_CELLS { return Err(CellError::Num); } arrays.push(a); } + // The padded rectangle can be far larger than the inputs; refuse it before building any rows. + check_output_size(h, w)?; let mut rows = vec![Vec::new(); h]; for a in &arrays { for (r, row) in rows.iter_mut().enumerate() { @@ -1132,6 +1139,17 @@ mod tests { is_err(ev("TOROW", vec![av(arr(vec![vec![Value::Empty]])), n(1.0)]), CellError::Calc); } + #[test] + fn output_size_checked_before_padding() { + assert_eq!(super::check_output_size(1_000_001, 10), Err(CellError::Num)); + assert_eq!(super::check_output_size(usize::MAX, 2), Err(CellError::Num)); + assert_eq!(super::check_output_size(1_000_000, 10), Ok(())); + // A 1M-row column beside a 9-wide row: ~1M input cells, but the padded result would be 10,000,010. + let tall = Value::from(gridcraft_core::Array::column(vec![nv(1.0); 1_000_001])); + let wide = row(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]); + is_err(ev("HSTACK", vec![av(tall), av(wide)]), CellError::Num); + } + #[test] fn wrapping() { let r = ev("WRAPROWS", vec![av(row(&[1.0, 2.0, 3.0, 4.0, 5.0])), n(2.0)]);