Skip to content

Commit 2817e3e

Browse files
committed
UPDATE: Adds name validation and exposes it in wasm
We do a trick I am not proud of. Because all of our errors are Strings, we don't have a way to separate a name error from an index error, for instance. What I do in prepend the error with a string that indicates where it comes from.
1 parent 52808ef commit 2817e3e

File tree

7 files changed

+104
-45
lines changed

7 files changed

+104
-45
lines changed

base/src/expressions/utils/mod.rs

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -211,15 +211,19 @@ pub fn parse_reference_a1(r: &str) -> Option<ParsedReference> {
211211
pub fn is_valid_identifier(name: &str) -> bool {
212212
// https://support.microsoft.com/en-us/office/names-in-formulas-fc2935f9-115d-4bef-a370-3aa8bb4c91f1
213213
// https://github.com/MartinTrummer/excel-names/
214-
// NOTE: We are being much more restrictive than Excel.
215-
// In particular we do not support non ascii characters.
216214
let upper = name.to_ascii_uppercase();
217-
let bytes = upper.as_bytes();
218-
let len = bytes.len();
215+
// length of chars
216+
let len = upper.chars().count();
217+
218+
let mut chars = upper.chars();
219+
219220
if len > 255 || len == 0 {
220221
return false;
221222
}
222-
let first = bytes[0] as char;
223+
let first = match chars.next() {
224+
Some(ch) => ch,
225+
None => return false,
226+
};
223227
// The first character of a name must be a letter, an underscore character (_), or a backslash (\).
224228
if !(first.is_ascii_alphabetic() || first == '_' || first == '\\') {
225229
return false;
@@ -237,20 +241,10 @@ pub fn is_valid_identifier(name: &str) -> bool {
237241
if parse_reference_r1c1(name).is_some() {
238242
return false;
239243
}
240-
let mut i = 1;
241-
while i < len {
242-
let ch = bytes[i] as char;
243-
match ch {
244-
'a'..='z' => {}
245-
'A'..='Z' => {}
246-
'0'..='9' => {}
247-
'_' => {}
248-
'.' => {}
249-
_ => {
250-
return false;
251-
}
244+
for ch in chars {
245+
if !(ch.is_alphanumeric() || ch == '_' || ch == '.') {
246+
return false;
252247
}
253-
i += 1;
254248
}
255249

256250
true

base/src/expressions/utils/test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ fn test_names() {
196196
assert!(is_valid_identifier("_."));
197197
assert!(is_valid_identifier("_1"));
198198
assert!(is_valid_identifier("\\."));
199+
assert!(is_valid_identifier("truñe"));
199200

200201
// invalid
201202
assert!(!is_valid_identifier("true"));
@@ -209,7 +210,6 @@ fn test_names() {
209210
assert!(!is_valid_identifier("1true"));
210211

211212
assert!(!is_valid_identifier("test€"));
212-
assert!(!is_valid_identifier("truñe"));
213213
assert!(!is_valid_identifier("tr&ue"));
214214

215215
assert!(!is_valid_identifier("LOG10"));

base/src/model.rs

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2068,29 +2068,57 @@ impl Model {
20682068
scope: Option<u32>,
20692069
formula: &str,
20702070
) -> Result<(), String> {
2071+
let sheet_id = self.is_valid_defined_name(name, scope, formula)?;
2072+
self.workbook.defined_names.push(DefinedName {
2073+
name: name.to_string(),
2074+
formula: formula.to_string(),
2075+
sheet_id,
2076+
});
2077+
self.reset_parsed_structures();
2078+
2079+
Ok(())
2080+
}
2081+
2082+
/// Validates if a defined name can be created
2083+
pub fn is_valid_defined_name(
2084+
&self,
2085+
name: &str,
2086+
scope: Option<u32>,
2087+
formula: &str,
2088+
) -> Result<Option<u32>, String> {
20712089
if !is_valid_identifier(name) {
2072-
return Err("Invalid defined name".to_string());
2073-
};
2090+
return Err("Name: Invalid defined name".to_string());
2091+
}
20742092
let name_upper = name.to_uppercase();
20752093
let defined_names = &self.workbook.defined_names;
20762094
let sheet_id = match scope {
2077-
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
2095+
Some(index) => match self.workbook.worksheet(index) {
2096+
Ok(ws) => Some(ws.sheet_id),
2097+
Err(_) => return Err("Scope: Invalid sheet index".to_string()),
2098+
},
20782099
None => None,
20792100
};
20802101
// if the defined name already exist return error
20812102
for df in defined_names {
20822103
if df.name.to_uppercase() == name_upper && df.sheet_id == sheet_id {
2083-
return Err("Defined name already exists".to_string());
2104+
return Err("Name: Defined name already exists".to_string());
20842105
}
20852106
}
2086-
self.workbook.defined_names.push(DefinedName {
2087-
name: name.to_string(),
2088-
formula: formula.to_string(),
2089-
sheet_id,
2090-
});
2091-
self.reset_parsed_structures();
20922107

2093-
Ok(())
2108+
// Make sure the formula is valid
2109+
match common::ParsedReference::parse_reference_formula(
2110+
None,
2111+
formula,
2112+
&self.locale,
2113+
|name| self.get_sheet_index_by_name(name),
2114+
) {
2115+
Ok(_) => {}
2116+
Err(_) => {
2117+
return Err("Formula: Invalid defined name formula".to_string());
2118+
}
2119+
};
2120+
2121+
Ok(sheet_id)
20942122
}
20952123

20962124
/// Delete defined name of name and scope
@@ -2126,26 +2154,36 @@ impl Model {
21262154
new_formula: &str,
21272155
) -> Result<(), String> {
21282156
if !is_valid_identifier(new_name) {
2129-
return Err("Invalid defined name".to_string());
2157+
return Err("Name: Invalid defined name".to_string());
21302158
};
21312159
let name_upper = name.to_uppercase();
21322160
let new_name_upper = new_name.to_uppercase();
21332161

21342162
if name_upper != new_name_upper || scope != new_scope {
21352163
for key in self.parsed_defined_names.keys() {
21362164
if key.1.to_uppercase() == new_name_upper && key.0 == new_scope {
2137-
return Err("Defined name already exists".to_string());
2165+
return Err("Name: Defined name already exists".to_string());
21382166
}
21392167
}
21402168
}
21412169
let defined_names = &self.workbook.defined_names;
21422170
let sheet_id = match scope {
2143-
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
2171+
Some(index) => Some(
2172+
self.workbook
2173+
.worksheet(index)
2174+
.map_err(|_| "Scope: Invalid sheet index")?
2175+
.sheet_id,
2176+
),
21442177
None => None,
21452178
};
21462179

21472180
let new_sheet_id = match new_scope {
2148-
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
2181+
Some(index) => Some(
2182+
self.workbook
2183+
.worksheet(index)
2184+
.map_err(|_| "Scope: Invalid sheet index")?
2185+
.sheet_id,
2186+
),
21492187
None => None,
21502188
};
21512189

base/src/test/user_model/test_defined_names.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -254,19 +254,19 @@ fn invalid_names() {
254254
// spaces
255255
assert_eq!(
256256
model.new_defined_name("A real", None, "Sheet1!$A$1"),
257-
Err("Invalid defined name".to_string())
257+
Err("Name: Invalid defined name".to_string())
258258
);
259259

260260
// Starts with number
261261
assert_eq!(
262262
model.new_defined_name("2real", None, "Sheet1!$A$1"),
263-
Err("Invalid defined name".to_string())
263+
Err("Name: Invalid defined name".to_string())
264264
);
265265

266266
// Updating also fails
267267
assert_eq!(
268268
model.update_defined_name("MyName", None, "My Name", None, "Sheet1!$A$1"),
269-
Err("Invalid defined name".to_string())
269+
Err("Name: Invalid defined name".to_string())
270270
);
271271
}
272272

@@ -284,13 +284,13 @@ fn already_existing() {
284284
// Can't create a new name with the same name
285285
assert_eq!(
286286
model.new_defined_name("MyName", None, "Sheet1!$A$2"),
287-
Err("Defined name already exists".to_string())
287+
Err("Name: Defined name already exists".to_string())
288288
);
289289

290290
// Can't update one into an existing
291291
assert_eq!(
292292
model.update_defined_name("Another", None, "MyName", None, "Sheet1!$A$1"),
293-
Err("Defined name already exists".to_string())
293+
Err("Name: Defined name already exists".to_string())
294294
);
295295
}
296296

@@ -304,25 +304,25 @@ fn invalid_sheet() {
304304

305305
assert_eq!(
306306
model.new_defined_name("Mything", Some(2), "Sheet1!$A$1"),
307-
Err("Invalid sheet index".to_string())
307+
Err("Scope: Invalid sheet index".to_string())
308308
);
309309

310310
assert_eq!(
311311
model.update_defined_name("MyName", None, "MyName", Some(2), "Sheet1!$A$1"),
312-
Err("Invalid sheet index".to_string())
312+
Err("Scope: Invalid sheet index".to_string())
313313
);
314314

315315
assert_eq!(
316316
model.update_defined_name("MyName", Some(9), "YourName", None, "Sheet1!$A$1"),
317-
Err("Invalid sheet index".to_string())
317+
Err("General: Failed to get old name".to_string())
318318
);
319319
}
320320

321321
#[test]
322322
fn invalid_formula() {
323323
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
324324
model.set_user_input(0, 1, 1, "Hello").unwrap();
325-
model.new_defined_name("MyName", None, "A1").unwrap();
325+
assert!(model.new_defined_name("MyName", None, "A1").is_err());
326326

327327
model.set_user_input(0, 1, 2, "=MyName").unwrap();
328328

base/src/user_model/common.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2001,7 +2001,10 @@ impl UserModel {
20012001
new_scope: Option<u32>,
20022002
new_formula: &str,
20032003
) -> Result<(), String> {
2004-
let old_formula = self.model.get_defined_name_formula(name, scope)?;
2004+
let old_formula = self
2005+
.model
2006+
.get_defined_name_formula(name, scope)
2007+
.map_err(|_| "General: Failed to get old name")?;
20052008
let diff_list = vec![Diff::UpdateDefinedName {
20062009
name: name.to_string(),
20072010
scope,
@@ -2017,6 +2020,16 @@ impl UserModel {
20172020
Ok(())
20182021
}
20192022

2023+
/// validates a new defined name
2024+
pub fn is_valid_defined_name(
2025+
&self,
2026+
name: &str,
2027+
scope: Option<u32>,
2028+
formula: &str,
2029+
) -> Result<Option<u32>, String> {
2030+
self.model.is_valid_defined_name(name, scope, formula)
2031+
}
2032+
20202033
// **** Private methods ****** //
20212034

20222035
pub(crate) fn push_diff_list(&mut self, diff_list: DiffList) {

bindings/wasm/src/lib.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,4 +775,17 @@ impl Model {
775775
.get_first_non_empty_in_row_after_column(sheet, row, column)
776776
.map_err(to_js_error)
777777
}
778+
779+
#[wasm_bindgen(js_name = "isValidDefinedName")]
780+
pub fn is_valid_defined_name(
781+
&self,
782+
name: &str,
783+
scope: Option<u32>,
784+
formula: &str,
785+
) -> Result<(), JsError> {
786+
match self.model.is_valid_defined_name(name, scope, formula) {
787+
Ok(_) => Ok(()),
788+
Err(e) => Err(to_js_error(e.to_string())),
789+
}
790+
}
778791
}

webapp/IronCalc/src/components/RightDrawer/NamedRanges/EditNamedRange.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ function EditNamedRange({
8383
} else {
8484
// Check for duplicates only if format is valid
8585
const scopeIndex = worksheets.findIndex((s) => s.name === scope);
86-
const newScope = scopeIndex >= 0 ? scopeIndex : null;
86+
const newScope = scopeIndex >= 0 ? scopeIndex : undefined;
8787
const existing = definedNameList.find(
8888
(dn) =>
8989
dn.name === trimmed &&
@@ -99,6 +99,7 @@ function EditNamedRange({
9999
}
100100

101101
setNameError(error);
102+
setFormulaError("");
102103
}, [name, scope, definedNameList, editingDefinedName, worksheets]);
103104

104105
const hasAnyError = nameError !== "" || formulaError !== "";

0 commit comments

Comments
 (0)