From baac184ed87e55642527b72e37c194e8ae32f82d Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Mon, 12 Oct 2026 08:31:22 +0200 Subject: [PATCH] Reject row/column insert counts past the worksheet and clamp delete counts instead of overflowing (#531) --- crates/engine/src/cmd/sheet.rs | 9 ++++- crates/engine/tests/line_count_overflow.rs | 47 ++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 crates/engine/tests/line_count_overflow.rs diff --git a/crates/engine/src/cmd/sheet.rs b/crates/engine/src/cmd/sheet.rs index f05b77d91..48209050a 100644 --- a/crates/engine/src/cmd/sheet.rs +++ b/crates/engine/src/cmd/sheet.rs @@ -249,10 +249,14 @@ fn cancel_copy_mode(s: &mut Session) { fn insert_lines(s: &mut Session, p: &Json, axis: Axis) -> Result { let (at, count) = lines(s, p, axis)?; + let limit = if axis == Axis::Rows { MAX_ROWS } else { MAX_COLS }; + if at.checked_add(count).is_none_or(|end| end > limit) { + let cmd = if axis == Axis::Rows { "home.insertRows" } else { "home.insertColumns" }; + return Err(bad(cmd, "the inserted lines would extend past the end of the worksheet")); + } check_lines_protection(s, p, axis, at, count, true)?; let sheet = target_sheet(s, p)?; // Refuse to push data off the sheet. - let limit = if axis == Axis::Rows { MAX_ROWS } else { MAX_COLS }; if let Some(u) = s.doc()?.wb.sheet(sheet).and_then(|sh| sh.used_range()) { let last = if axis == Axis::Rows { u.end.row } else { u.end.col }; if last >= at && last as u64 + count as u64 >= limit as u64 { @@ -290,6 +294,9 @@ fn insert_lines(s: &mut Session, p: &Json, axis: Axis) -> Result { fn delete_lines(s: &mut Session, p: &Json, axis: Axis) -> Result { let (at, count) = lines(s, p, axis)?; + let limit = if axis == Axis::Rows { MAX_ROWS } else { MAX_COLS }; + // Deleting past the last line only removes up to the end of the worksheet. + let count = count.min(limit.saturating_sub(at)).max(1); check_lines_protection(s, p, axis, at, count, false)?; let sheet = target_sheet(s, p)?; let out = edit(s, |cx| { diff --git a/crates/engine/tests/line_count_overflow.rs b/crates/engine/tests/line_count_overflow.rs new file mode 100644 index 000000000..9ae58d1a9 --- /dev/null +++ b/crates/engine/tests/line_count_overflow.rs @@ -0,0 +1,47 @@ +use gridcraft_engine::{EngineError, Session}; +use serde_json::json; + +fn session() -> Session { + let mut session = Session::new(); + session.new_workbook(); + session +} + +#[test] +fn insert_with_an_out_of_range_count_is_a_normal_error() { + for (id, key, at) in [("home.insertRows", "rows", "3:3"), ("home.insertColumns", "cols", "C:C")] { + let mut session = session(); + let err = session.execute(id, json!({ key: at, "count": u32::MAX })).unwrap_err(); + assert!(matches!(err, EngineError::BadParams { .. }), "{id}: {err:?}"); + } +} + +#[test] +fn insert_after_a_formatted_line_with_an_out_of_range_count_is_a_normal_error() { + // A custom size on the preceding line and no cells: the used-range guard has nothing to check. + let mut session = session(); + session.execute("home.rowHeight", json!({"rows": "2:2", "height": 30})).unwrap(); + let err = session.execute("home.insertRows", json!({"rows": "3:3", "count": u32::MAX})).unwrap_err(); + assert!(matches!(err, EngineError::BadParams { .. }), "{err:?}"); + + let mut session = session(); + session.execute("home.columnWidth", json!({"cols": "B:B", "width": 100})).unwrap(); + let err = session.execute("home.insertColumns", json!({"cols": "C:C", "count": u32::MAX})).unwrap_err(); + assert!(matches!(err, EngineError::BadParams { .. }), "{err:?}"); +} + +#[test] +fn delete_with_an_out_of_range_count_does_not_panic() { + for (id, key, at) in [("home.deleteRows", "rows", "3:3"), ("home.deleteColumns", "cols", "C:C")] { + let mut session = session(); + let res = session.execute(id, json!({ key: at, "count": u32::MAX })); + assert!(!matches!(res, Err(EngineError::Internal(..))), "{id}: {res:?}"); + } +} + +#[test] +fn insert_and_delete_with_a_small_count_still_work() { + let mut session = session(); + session.execute("home.insertRows", json!({"rows": "1:1", "count": 3})).unwrap(); + session.execute("home.deleteRows", json!({"rows": "1:1", "count": 3})).unwrap(); +}