From 8427681d7ceeb638be393cfe31faa8dfd516b6a9 Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Mon, 12 Oct 2026 08:28:30 +0200 Subject: [PATCH] Tables: names with a hyphen or other operator characters are rejected (#576) --- crates/engine/src/cmd/insert.rs | 17 +++++++++++------ crates/engine/src/tests.rs | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/cmd/insert.rs b/crates/engine/src/cmd/insert.rs index 392c5ee..8e9c735 100644 --- a/crates/engine/src/cmd/insert.rs +++ b/crates/engine/src/cmd/insert.rs @@ -240,7 +240,7 @@ fn insert_table(s: &mut Session, p: &Json) -> Result { let name = str_param(p, "name") .map(str::to_string) .unwrap_or_else(|| (1..).map(|n| format!("Table{n}")).find(|n| d.wb.table(n).is_none()).unwrap_or_else(|| "Table".into())); - if name.is_empty() || name.contains(' ') || name.chars().next().is_some_and(|c| c.is_ascii_digit()) || CellRef::parse(&name).is_some() { + if !valid_table_name(&name) { return Err(EngineError::Other("The name that you entered is not valid.".into())); } if d.wb.table(&name).is_some() || d.wb.names.iter().any(|n| n.name.eq_ignore_ascii_case(&name)) { @@ -448,14 +448,19 @@ fn table_style(s: &mut Session, p: &Json) -> Result { }) } +/// Table names follow the Microsoft name rules, as the xlsx writer does: letters, digits, `_` and `.`, +/// starting with a letter, `_` or `\`. Anything else (a hyphen, say) would read as an operator inside a +/// structured reference, and the writer would rename the table on save. +fn valid_table_name(name: &str) -> bool { + name.starts_with(|c: char| c.is_alphabetic() || c == '_' || c == '\\') + && CellRef::parse(name).is_none() + && name.chars().enumerate().all(|(i, c)| c.is_alphanumeric() || c == '_' || c == '.' || (i == 0 && c == '\\')) +} + fn table_rename(s: &mut Session, p: &Json) -> Result { let (si, ti) = find_table(s, p)?; let name = str_param(p, "name").ok_or_else(|| bad("table.rename", "missing `name`"))?.to_string(); - if name.is_empty() - || name.contains(' ') - || name.chars().next().is_some_and(|c| c.is_ascii_digit()) - || gridcraft_core::CellRef::parse(&name).is_some() - { + if !valid_table_name(&name) { return Err(EngineError::Other("The name that you entered is not valid.".into())); } if s.doc()?.wb.table(&name).is_some_and(|x| x != (si, ti)) { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 33fdc11..fa5409e 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -1170,6 +1170,22 @@ fn insert_and_delete_cells_never_shift_part_of_a_table() { assert!(e.to_string().contains("shift cells in a table"), "{e}"); } +#[test] +fn table_names_with_a_hyphen_are_rejected() { + let mut s = Session::new(); + s.execute("range.setValues", json!({"range": "A1", "values": [["Qty"], [7]]})).unwrap(); + let e = s.execute("insert.table", json!({"range": "A1:A2", "header": true, "name": "A-B"})).unwrap_err(); + assert!(e.to_string().contains("not valid"), "{e}"); + assert!(s.doc().unwrap().wb.table("A-B").is_none()); + s.execute("insert.table", json!({"range": "A1:A2", "header": true, "name": "Sales"})).unwrap(); + let e = s.execute("table.rename", json!({"table": "Sales", "name": "A-B"})).unwrap_err(); + assert!(e.to_string().contains("not valid"), "{e}"); + assert!(s.doc().unwrap().wb.table("Sales").is_some()); + assert!(s.execute("table.rename", json!({"table": "Sales", "name": ".AB"})).is_err()); + s.execute("table.rename", json!({"table": "Sales", "name": "A_B"})).unwrap(); + assert!(s.doc().unwrap().wb.table("A_B").is_some()); +} + #[test] fn undo_and_redo_keep_the_watch_window_list() { // Found by the command-sequence fuzzer: the list lives in the workbook but isn't an edit, so