fix(engine): preset imports number duplicate names in linear time

This commit is contained in:
Dylan M. Taylor
2026-11-03 22:17:45 +07:00
committed by bflatastic
parent 706c8f850e
commit 891e90bfee
4 changed files with 54 additions and 4 deletions
+20
View File
@@ -611,6 +611,26 @@ pub(crate) fn unique_name(base: &str, taken: impl Fn(&str) -> bool) -> String {
(2..).map(|i| format!("{base} {i}")).find(|n| !taken(n)).unwrap_or_else(|| base.to_string())
}
/// [`unique_name`] for names given one after another (an import): `next` keeps, per base (in
/// any case), the number after the last one tried, so a run of one name doesn't count up from 2
/// again for each copy. Names only become taken meanwhile, so the result is the same.
pub(crate) fn unique_name_from(base: &str, next: &mut std::collections::HashMap<String, usize>, taken: impl Fn(&str) -> bool) -> String {
if !taken(base) {
return base.to_string();
}
let i = next.entry(base.to_lowercase()).or_insert(2);
loop {
let n = format!("{base} {i}");
*i += 1;
if !taken(&n) {
return n;
}
}
}
/// The most presets one presets file imports: each is looked up among the saved ones.
pub(crate) const MAX_IMPORTED_PRESETS: usize = 1000;
pub(crate) use edit::{settle_interaction, typing_in_progress};
pub(crate) use object::bounds_rewrite;
pub(crate) use vectorcraft_doc::range::parse_range;
+6 -2
View File
@@ -8,7 +8,7 @@ use serde_json::{Value, json};
use vectorcraft_pdf::{PdfPreset, builtin_preset, builtin_presets};
use super::fileio::pdf::{find_preset, resolve};
use super::{CommandSpec, Result, Session, always, bad, bool_or, cmd, str_param, unique_name};
use super::{CommandSpec, Result, Session, always, bad, bool_or, cmd, str_param};
/// The `format` of a PDF presets file, also its extension.
pub const PRESET_FORMAT: &str = "vcpdfpresets";
@@ -196,8 +196,12 @@ fn import(s: &mut Session, p: &Value) -> Result<Value> {
for q in &file.presets {
q.settings.check_values().map_err(|e| bad(C, format!("`{}`: {e}", q.name)))?;
}
if file.presets.len() > super::MAX_IMPORTED_PRESETS {
return Err(bad(C, format!("a PDF presets file with over {} presets", super::MAX_IMPORTED_PRESETS)));
}
let replace = bool_or(p, "replace", false);
let mut imported = vec![];
let mut next = std::collections::HashMap::new();
for mut preset in file.presets {
preset.settings.clear_passwords();
let base = Some(preset.name.trim()).filter(|n| !n.is_empty()).unwrap_or("PDF Preset").to_string();
@@ -211,7 +215,7 @@ fn import(s: &mut Session, p: &Value) -> Result<Value> {
}
}
None => {
preset.name = unique_name(&base, |n| s.pdf_preset_taken(n, None));
preset.name = super::unique_name_from(&base, &mut next, |n| s.pdf_preset_taken(n, None));
imported.push(preset.name.clone());
s.store_pdf_preset(None, &preset);
s.prefs.pdf_presets.push(preset);
+6 -2
View File
@@ -8,7 +8,7 @@ use vectorcraft_pdf::PrintSettings;
use super::fileio::pdf::changed;
use super::print::settings_over;
use super::{CommandSpec, EngineError, Result, Session, always, bad, bool_or, cmd, str_param, unique_name};
use super::{CommandSpec, EngineError, Result, Session, always, bad, bool_or, cmd, str_param};
/// The built-in preset's name.
pub const DEFAULT_PRESET: &str = "[Default]";
@@ -221,8 +221,12 @@ fn import(s: &mut Session, p: &Value) -> Result<Value> {
for q in &file.presets {
q.settings.check().map_err(|e| bad(C, format!("`{}`: {e}", q.name)))?;
}
if file.presets.len() > super::MAX_IMPORTED_PRESETS {
return Err(bad(C, format!("a print presets file with over {} presets", super::MAX_IMPORTED_PRESETS)));
}
let replace = bool_or(p, "replace", false);
let mut imported = vec![];
let mut next = std::collections::HashMap::new();
for mut preset in file.presets {
// [Default] comes in as a saved copy ("Default 2"…): the built-in one stays.
let name = preset.name.trim().trim_start_matches('[').trim_end_matches(']').trim();
@@ -236,7 +240,7 @@ fn import(s: &mut Session, p: &Value) -> Result<Value> {
}
}
None => {
preset.name = unique_name(&base, |n| s.print_preset_taken(n, None));
preset.name = super::unique_name_from(&base, &mut next, |n| s.print_preset_taken(n, None));
imported.push(preset.name.clone());
s.prefs.print_presets.push(preset);
}
+22
View File
@@ -66,6 +66,28 @@ fn unique_name_appends_the_first_free_number() {
assert_eq!(unique_name("A 2", |n| taken.contains(&n)), "A 2 2");
}
#[test]
fn unique_name_from_counts_on_for_a_run_of_one_name() {
let mut taken = vec!["A".to_string(), "a 3".to_string()];
let mut next = std::collections::HashMap::new();
for want in ["A 2", "A 4", "A 5"] {
let is_taken = |n: &str| taken.iter().any(|t| t.eq_ignore_ascii_case(n));
let n = crate::cmd::unique_name_from("A", &mut next, is_taken);
assert_eq!(n, want);
taken.push(n);
}
assert_eq!(crate::cmd::unique_name_from("B", &mut next, |_| false), "B");
}
#[test]
fn a_presets_file_with_too_many_presets_is_refused() {
let mut s = Session::new();
let presets: Vec<_> = (0..=crate::cmd::MAX_IMPORTED_PRESETS).map(|_| serde_json::json!({"name": "P"})).collect();
let file = serde_json::json!({"format": crate::cmd::printpresets::PRESET_FORMAT, "presets": presets}).to_string();
let e = s.execute("print.presets.import", &serde_json::json!({"data": file})).unwrap_err().to_string();
assert!(e.contains("over"), "{e}");
}
#[test]
fn leaf_targets_expand_groups_and_stop_at_compound_paths() {
let mut s = session();