Fix 26 bugs, edge cases, and consistency issues from fifth audit pass
Critical: undo toast now trashes only batch output files (not entire dir), JPEG scanline write errors propagated, selective metadata write result returned. High: zero-dimension guards in ResizeConfig/fit_within, negative aspect ratio rejection, FM integration toggle infinite recursion guard, saturating counter arithmetic in executor. Medium: PNG compression level passed to oxipng, pct mode updates job_config, external file loading updates step indicator, CLI undo removes history entries, watch config write failures reported, fast-copy path reads image dimensions for rename templates, discovery excludes unprocessable formats (heic/svg/ico/jxl), CLI warns on invalid algorithm/overwrite values, resolve_collision trailing dot fix, generation guards on all preview threads to cancel stale results, default DPI aligned to 0, watermark text width uses char count not byte length. Low: binary path escaped in Nautilus extension, file dialog filter aligned with discovery, reset_wizard clears preset_mode and output_dir.
This commit is contained in:
@@ -122,6 +122,9 @@ fn days_to_ymd(total_days: u64) -> (u64, u64, u64) {
|
||||
let mut days = total_days;
|
||||
let mut year = 1970u64;
|
||||
loop {
|
||||
if year > 9999 {
|
||||
break;
|
||||
}
|
||||
let days_in_year = if is_leap(year) { 366 } else { 365 };
|
||||
if days < days_in_year {
|
||||
break;
|
||||
@@ -149,6 +152,46 @@ fn is_leap(year: u64) -> bool {
|
||||
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
|
||||
}
|
||||
|
||||
/// Apply space replacement on a filename
|
||||
/// mode: 0=none, 1=underscore, 2=hyphen, 3=dot, 4=camelcase, 5=remove
|
||||
pub fn apply_space_replacement(name: &str, mode: u32) -> String {
|
||||
match mode {
|
||||
1 => name.replace(' ', "_"),
|
||||
2 => name.replace(' ', "-"),
|
||||
3 => name.replace(' ', "."),
|
||||
4 => {
|
||||
let mut result = String::with_capacity(name.len());
|
||||
let mut capitalize_next = false;
|
||||
for ch in name.chars() {
|
||||
if ch == ' ' {
|
||||
capitalize_next = true;
|
||||
} else if capitalize_next {
|
||||
result.extend(ch.to_uppercase());
|
||||
capitalize_next = false;
|
||||
} else {
|
||||
result.push(ch);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
5 => name.replace(' ', ""),
|
||||
_ => name.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply special character filtering on a filename
|
||||
/// mode: 0=keep all, 1=filesystem-safe, 2=web-safe, 3=hyphens+underscores, 4=hyphens only, 5=alphanumeric only
|
||||
pub fn apply_special_chars(name: &str, mode: u32) -> String {
|
||||
match mode {
|
||||
1 => name.chars().filter(|c| !matches!(c, '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|')).collect(),
|
||||
2 => name.chars().filter(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.')).collect(),
|
||||
3 => name.chars().filter(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_')).collect(),
|
||||
4 => name.chars().filter(|c| c.is_ascii_alphanumeric() || *c == '-').collect(),
|
||||
5 => name.chars().filter(|c| c.is_ascii_alphanumeric()).collect(),
|
||||
_ => name.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply case conversion to a filename (without extension)
|
||||
/// case_mode: 0=none, 1=lowercase, 2=uppercase, 3=title case
|
||||
pub fn apply_case_conversion(name: &str, case_mode: u32) -> String {
|
||||
@@ -156,20 +199,25 @@ pub fn apply_case_conversion(name: &str, case_mode: u32) -> String {
|
||||
1 => name.to_lowercase(),
|
||||
2 => name.to_uppercase(),
|
||||
3 => {
|
||||
// Title case: capitalize first letter of each word (split on _ - space)
|
||||
name.split(|c: char| c == '_' || c == '-' || c == ' ')
|
||||
.map(|word| {
|
||||
let mut chars = word.chars();
|
||||
match chars.next() {
|
||||
Some(first) => {
|
||||
let upper: String = first.to_uppercase().collect();
|
||||
upper + &chars.as_str().to_lowercase()
|
||||
}
|
||||
None => String::new(),
|
||||
// Title case: capitalize first letter of each word, preserve original separators
|
||||
let mut result = String::with_capacity(name.len());
|
||||
let mut capitalize_next = true;
|
||||
for c in name.chars() {
|
||||
if c == '_' || c == '-' || c == ' ' {
|
||||
result.push(c);
|
||||
capitalize_next = true;
|
||||
} else if capitalize_next {
|
||||
for uc in c.to_uppercase() {
|
||||
result.push(uc);
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("_")
|
||||
capitalize_next = false;
|
||||
} else {
|
||||
for lc in c.to_lowercase() {
|
||||
result.push(lc);
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
_ => name.to_string(),
|
||||
}
|
||||
@@ -180,7 +228,10 @@ pub fn apply_regex_replace(name: &str, find: &str, replace: &str) -> String {
|
||||
if find.is_empty() {
|
||||
return name.to_string();
|
||||
}
|
||||
match regex::Regex::new(find) {
|
||||
match regex::RegexBuilder::new(find)
|
||||
.size_limit(1 << 16)
|
||||
.build()
|
||||
{
|
||||
Ok(re) => re.replace_all(name, replace).into_owned(),
|
||||
Err(_) => name.to_string(),
|
||||
}
|
||||
@@ -213,5 +264,9 @@ pub fn resolve_collision(path: &Path) -> PathBuf {
|
||||
}
|
||||
|
||||
// Fallback - should never happen with 1000 attempts
|
||||
parent.join(format!("{}_{}.{}", stem, "overflow", ext))
|
||||
if ext.is_empty() {
|
||||
parent.join(format!("{}_overflow", stem))
|
||||
} else {
|
||||
parent.join(format!("{}_overflow.{}", stem, ext))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user