Engine: a macro renamed on a name clash keeps its name within the length limit

When a file held two macros with the same name, the second was renamed
"name (2)" without room for the suffix. Two macros with the same
255-character name both opened, but the second name grew to 259
characters, was saved that way, and the next open dropped that macro as
over the limits. unique_name now shortens the name to make room for the
suffix, and load checks the macro again after renaming it.
This commit is contained in:
prxnvt
2026-10-22 02:26:32 -04:00
parent 20d2a3b998
commit 7011395fcf
2 changed files with 47 additions and 8 deletions
+19 -8
View File
@@ -128,12 +128,14 @@ pub fn load(wb: &mut Workbook) -> Vec<String> {
bad += 1;
continue;
}
match m.check_limits().and_then(|()| check_size(&m)) {
Ok(()) => {
let mut m = m;
m.name = unique_name(wb, m.name.trim());
wb.macros.push(m);
}
let mut m = m;
let checked = m.check_limits().and_then(|()| {
m.name = unique_name(wb, m.name.trim());
// The macro as it is kept, renamed, must still pass, or the next save would lose it.
m.check_limits().and_then(|()| check_size(&m))
});
match checked {
Ok(()) => wb.macros.push(m),
Err(_) => bad += 1,
}
}
@@ -150,13 +152,22 @@ pub fn load(wb: &mut Workbook) -> Vec<String> {
warnings
}
/// `name`, or `name (2)`, `name (3)`… if a macro already has it.
/// `name`, or `name (2)`, `name (3)`… if a macro already has it. A long name is shortened to make
/// room for the suffix, so the result stays within [`gridcraft_model::MAX_MACRO_NAME`] characters.
pub fn unique_name(wb: &Workbook, name: &str) -> String {
let name = if name.is_empty() { "Macro" } else { name };
if find(wb, name).is_none() {
return name.to_string();
}
(2..).map(|n| format!("{name} ({n})")).find(|n| find(wb, n).is_none()).unwrap_or_else(|| name.to_string())
(2..)
.map(|n| {
let suffix = format!(" ({n})");
let keep = gridcraft_model::MAX_MACRO_NAME.saturating_sub(suffix.chars().count());
let base: String = name.chars().take(keep).collect();
format!("{}{suffix}", base.trim_end())
})
.find(|n| find(wb, n).is_none())
.unwrap_or_else(|| name.to_string())
}
#[cfg(test)]
+28
View File
@@ -190,6 +190,13 @@ fn names() {
assert_eq!(unique_name(&wb, "macro1"), "macro1 (2)");
assert_eq!(unique_name(&wb, "Other"), "Other");
assert_eq!(unique_name(&wb, ""), "Macro");
// A long name is shortened to make room for the suffix.
let long = "y".repeat(MAX_MACRO_NAME);
wb.macros.push(Macro { name: long.clone(), ..Macro::default() });
let renamed = unique_name(&wb, &long);
assert_eq!(renamed, format!("{} (2)", "y".repeat(MAX_MACRO_NAME - 4)));
wb.macros.push(Macro { name: renamed, ..Macro::default() });
assert_eq!(unique_name(&wb, &long).chars().count(), MAX_MACRO_NAME);
assert_eq!(find(&wb, " MACRO1 "), Some(0));
assert_eq!(find(&wb, "Macro2"), None);
}
@@ -206,3 +213,24 @@ fn json_field_macros_get_the_name_check_too() {
assert_eq!(names, ["Fine"]);
assert!(warnings.len() == 1 && warnings[0].starts_with("1 macro "), "{warnings:?}");
}
#[test]
fn duplicate_names_at_the_length_limit_survive_save_and_reopen() {
// Two macros with the same 255-character name: the second is renamed within the limit, so a
// save and a second open keep both.
let long = "é".repeat(MAX_MACRO_NAME);
for file in ["dup.json", "dup.xlsx"] {
let mut wb = Workbook::new();
wb.macros.push(Macro { name: long.clone(), ..Macro::default() });
wb.macros.push(Macro { name: long.clone(), description: "second".into(), ..Macro::default() });
let (first, warnings) = crate::io::open_bytes(file, &crate::io::save_bytes(&wb, file).unwrap()).unwrap();
assert!(warnings.is_empty(), "{file}: {warnings:?}");
assert_eq!(first.macros.len(), 2, "{file}");
let renamed = &first.macros[1].name;
assert_eq!(renamed.chars().count(), MAX_MACRO_NAME, "{file}");
assert!(renamed.ends_with(" (2)") && renamed.starts_with("éé"), "{file}: {renamed}");
let (again, warnings) = crate::io::open_bytes(file, &crate::io::save_bytes(&first, file).unwrap()).unwrap();
assert!(warnings.is_empty(), "{file}: {warnings:?}");
assert_eq!(again.macros, first.macros, "{file}");
}
}