From 1b4df8acc2ae7c0f4ebc6f98baa3570ea747a25c Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Thu, 15 Oct 2026 06:22:02 +0200 Subject: [PATCH] Refuse table names that clash with defined names or R1C1 references Found while testing crates/engine/src/cmd/insert.rs:477 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/formulas.rs | 2 +- crates/engine/src/cmd/insert.rs | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/formulas.rs b/crates/engine/src/cmd/formulas.rs index 40df72df0..eca08a093 100644 --- a/crates/engine/src/cmd/formulas.rs +++ b/crates/engine/src/cmd/formulas.rs @@ -237,7 +237,7 @@ fn valid_name(n: &str) -> bool { /// Looks like an R1C1 reference (`R`, `C`, `RC`, `R2`, `C3`, `R1C1`), which Excel doesn't /// accept as a name. -fn is_r1c1(n: &str) -> bool { +pub(crate) fn is_r1c1(n: &str) -> bool { let digits = |s: &str| s.bytes().take_while(u8::is_ascii_digit).count(); let mut rest = n; if let Some(r) = rest.strip_prefix(['R', 'r']) { diff --git a/crates/engine/src/cmd/insert.rs b/crates/engine/src/cmd/insert.rs index d878e4469..5e84b292b 100644 --- a/crates/engine/src/cmd/insert.rs +++ b/crates/engine/src/cmd/insert.rs @@ -471,6 +471,7 @@ fn table_style(s: &mut Session, p: &Json) -> Result { fn valid_table_name(name: &str) -> bool { name.starts_with(|c: char| c.is_alphabetic() || c == '_' || c == '\\') && CellRef::parse(name).is_none() + && !super::formulas::is_r1c1(name) && name.chars().enumerate().all(|(i, c)| c.is_alphanumeric() || c == '_' || c == '.' || (i == 0 && c == '\\')) } @@ -480,7 +481,8 @@ fn table_rename(s: &mut Session, p: &Json) -> Result { 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)) { + let wb = &s.doc()?.wb; + if wb.table(&name).is_some_and(|x| x != (si, ti)) || wb.names.iter().any(|n| n.name.eq_ignore_ascii_case(&name)) { return Err(EngineError::Other("The name entered already exists. Enter a unique name.".into())); } edit(s, |cx| { @@ -1553,4 +1555,25 @@ mod tests { let rules = s.execute("home.manageRules", json!({})).unwrap(); assert_eq!(rules["rules"].as_array().map(Vec::len), Some(0)); } + + #[test] + fn table_names_refuse_defined_names_and_r1c1() { + let mut s = Session::new(); + s.new_workbook(); + s.execute("range.setValues", json!({"range": "A1:A2", "values": [["h"], ["1"]]})).unwrap(); + s.execute("formulas.defineName", json!({"name": "Rate", "refersTo": "=0.2"})).unwrap(); + s.execute("insert.table", json!({"range": "A1:A2"})).unwrap(); + let e = s.execute("table.rename", json!({"table": "Table1", "name": "Rate"})).unwrap_err(); + assert!(e.to_string().contains("already exists"), "{e}"); + let e = s.execute("table.rename", json!({"table": "Table1", "name": "rate"})).unwrap_err(); + assert!(e.to_string().contains("already exists"), "{e}"); + for n in ["R2C3", "c", "r", "RC", "C5"] { + let e = s.execute("table.rename", json!({"table": "Table1", "name": n})).unwrap_err(); + assert!(e.to_string().contains("not valid"), "{n}: {e}"); + let e = s.execute("insert.table", json!({"range": "C1:C2", "name": n})).unwrap_err(); + assert!(e.to_string().contains("not valid"), "{n}: {e}"); + } + s.execute("table.rename", json!({"table": "Table1", "name": "Table1"})).unwrap(); + s.execute("table.rename", json!({"table": "Table1", "name": "Rates"})).unwrap(); + } }