Project: layer name suffixes in one pass; template names bounded and never a parent folder

unique_layer_name tried each numeric suffix against every layer, so
importing many layers of one name was cubic. The suffixes in use are now
gathered once. sanitize kept '.' and '..' (a comp name used as a path
component in an output template could point at the parent folder) and
copied a name of any length into every template token; dot-only names
become underscores and names are cut to 255 characters.
This commit is contained in:
Dylan M. Taylor
2026-11-03 11:32:51 +00:00
parent 7870b5916a
commit 71dca9fe72
3 changed files with 30 additions and 2 deletions
+9 -1
View File
@@ -271,7 +271,15 @@ impl Comp {
return base.to_string();
}
let stem = base.trim_end_matches(|c: char| c.is_ascii_digit()).trim_end();
(2..).map(|i| format!("{stem} {i}")).find(|n| !self.layers.iter().any(|l| &l.name == n)).unwrap_or_default()
// The suffixes in use, gathered in one pass: trying each candidate against every layer
// made importing many layers of one name cubic.
let taken: std::collections::HashSet<u64> = self
.layers
.iter()
.filter_map(|l| l.name.strip_prefix(stem)?.strip_prefix(' '))
.filter_map(|s| s.parse::<u64>().ok().filter(|n| n.to_string() == s))
.collect();
(2..).find(|i| !taken.contains(i)).map(|i| format!("{stem} {i}")).unwrap_or_default()
}
}
+13 -1
View File
@@ -1529,7 +1529,11 @@ pub fn numbered_template(template: &str, n: u32) -> String {
/// Make a name safe as a file name component.
pub fn sanitize(name: &str) -> String {
name.chars().map(|c| if matches!(c, '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|') { '_' } else { c }).collect()
// At most a file name's worth (255 characters): a template repeating a long name token
// must not expand to gigabytes.
let s: String = name.chars().take(255).map(|c| if matches!(c, '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|') { '_' } else { c }).collect();
// `.` and `..` would name the folder itself or its parent when the token is a path component.
if !s.is_empty() && s.chars().all(|c| c == '.') { "_".repeat(s.len()) } else { s }
}
#[cfg(test)]
@@ -1540,6 +1544,14 @@ mod tests {
fn templates_and_sequences() {
let v = TemplateVars { comp_name: "Main/Title", project_name: "P", width: 1920, height: 1080, frame_rate: 29.97, start_frame: 0, end_frame: 9 };
assert_eq!(expand_template(DEFAULT_TEMPLATE, OutputFormat::H264, &v), "Main_Title.mp4");
// A dot-only name is not a parent-folder component; a long one is cut to a file name.
for name in ["..", "."] {
let v = TemplateVars { comp_name: name, ..v };
assert_eq!(expand_template("out/[compName]/x.[fileExtension]", OutputFormat::Gif, &v), format!("out/{}/x.gif", "_".repeat(name.len())));
}
let long = "n".repeat(100_000);
let v2 = TemplateVars { comp_name: &long, ..v };
assert_eq!(expand_template(&"[compName]".repeat(100), OutputFormat::Gif, &v2).len(), 255 * 100);
assert_eq!(expand_template("[compName]_[width]x[height]@[frameRate].[fileExtension]", OutputFormat::Gif, &v), "Main_Title_1920x1080@29.97.gif");
assert_eq!(sequence_path("out/a_[#####].png", 42), "out/a_00042.png");
assert_eq!(sequence_path("a###.tif", 7), "a007.tif");
+8
View File
@@ -119,6 +119,14 @@ fn unique_names_and_cycles() {
let c = p.comp(cid).unwrap();
assert_eq!(c.unique_layer_name("Red Solid 1"), "Red Solid 2");
assert_eq!(c.unique_layer_name("Shape Layer 1"), "Shape Layer 1");
// A suffix written another way (`02`) is not the number taken.
let mut d = c.clone();
for n in ["Red Solid 02", "Red Solid 3"] {
let mut l = d.layers[0].clone();
l.name = n.into();
d.layers.push(l);
}
assert_eq!(d.unique_layer_name("Red Solid 1"), "Red Solid 2");
assert!(p.comp_contains(cid, cid));
}