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:
2026-03-07 19:47:23 +02:00
parent 270a7db60d
commit b432cc7431
44 changed files with 5748 additions and 2221 deletions

View File

@@ -76,6 +76,14 @@ pub fn regenerate_all() -> Result<()> {
Ok(())
}
/// Sanitize a string for safe use in shell Exec= lines and XML command elements.
/// Removes or replaces characters that could cause shell injection.
fn shell_safe(s: &str) -> String {
s.chars()
.filter(|c| c.is_alphanumeric() || matches!(c, ' ' | '-' | '_' | '.' | '(' | ')' | ','))
.collect()
}
fn pixstrip_bin() -> String {
// Try to find the pixstrip binary path
if let Ok(exe) = std::env::current_exe() {
@@ -116,7 +124,7 @@ fn get_preset_names() -> Vec<String> {
fn nautilus_extension_dir() -> PathBuf {
let data = std::env::var("XDG_DATA_HOME")
.unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap_or_else(|_| "~".into());
let home = std::env::var("HOME").unwrap_or_else(|_| dirs::home_dir().map(|h| h.to_string_lossy().into_owned()).unwrap_or_else(|| "/tmp".into()));
format!("{}/.local/share", home)
});
PathBuf::from(data).join("nautilus-python").join("extensions")
@@ -143,11 +151,12 @@ fn install_nautilus() -> Result<()> {
\x20 item.connect('activate', self._on_preset, '{}', files)\n\
\x20 submenu.append_item(item)\n\n",
name.replace(' ', "_"),
name,
name,
name.replace('\'', "\\'"),
name.replace('\'', "\\'"),
));
}
let escaped_bin = bin.replace('\\', "\\\\").replace('\'', "\\'");
let script = format!(
r#"import subprocess
from gi.repository import Nautilus, GObject
@@ -202,7 +211,7 @@ class PixstripExtension(GObject.GObject, Nautilus.MenuProvider):
subprocess.Popen(['{bin}', '--preset', preset_name, '--files'] + paths)
"#,
preset_items = preset_items,
bin = bin,
bin = escaped_bin,
);
std::fs::write(nautilus_extension_path(), script)?;
@@ -222,7 +231,7 @@ fn uninstall_nautilus() -> Result<()> {
fn nemo_action_dir() -> PathBuf {
let data = std::env::var("XDG_DATA_HOME")
.unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap_or_else(|_| "~".into());
let home = std::env::var("HOME").unwrap_or_else(|_| dirs::home_dir().map(|h| h.to_string_lossy().into_owned()).unwrap_or_else(|| "/tmp".into()));
format!("{}/.local/share", home)
});
PathBuf::from(data).join("nemo").join("actions")
@@ -261,12 +270,13 @@ fn install_nemo() -> Result<()> {
"[Nemo Action]\n\
Name=Pixstrip: {name}\n\
Comment=Process with {name} preset\n\
Exec={bin} --preset \"{name}\" --files %F\n\
Exec={bin} --preset \"{safe_label}\" --files %F\n\
Icon-Name=applications-graphics-symbolic\n\
Selection=Any\n\
Extensions=jpg;jpeg;png;webp;gif;tiff;tif;avif;bmp;\n\
Mimetypes=image/*;\n",
name = name,
safe_label = shell_safe(name),
bin = bin,
);
std::fs::write(action_path, action)?;
@@ -300,7 +310,7 @@ fn uninstall_nemo() -> Result<()> {
fn thunar_action_dir() -> PathBuf {
let config = std::env::var("XDG_CONFIG_HOME")
.unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap_or_else(|_| "~".into());
let home = std::env::var("HOME").unwrap_or_else(|_| dirs::home_dir().map(|h| h.to_string_lossy().into_owned()).unwrap_or_else(|| "/tmp".into()));
format!("{}/.config", home)
});
PathBuf::from(config).join("Thunar")
@@ -337,14 +347,15 @@ fn install_thunar() -> Result<()> {
actions.push_str(&format!(
" <action>\n\
\x20 <icon>applications-graphics-symbolic</icon>\n\
\x20 <name>Pixstrip: {name}</name>\n\
\x20 <command>{bin} --preset \"{name}\" --files %F</command>\n\
\x20 <description>Process with {name} preset</description>\n\
\x20 <name>Pixstrip: {xml_name}</name>\n\
\x20 <command>{bin} --preset \"{safe_label}\" --files %F</command>\n\
\x20 <description>Process with {xml_name} preset</description>\n\
\x20 <patterns>*.jpg;*.jpeg;*.png;*.webp;*.gif;*.tiff;*.tif;*.avif;*.bmp</patterns>\n\
\x20 <image-files/>\n\
\x20 <directories/>\n\
</action>\n",
name = name,
xml_name = name.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;").replace('"', "&quot;"),
safe_label = shell_safe(name),
bin = bin,
));
}
@@ -367,7 +378,7 @@ fn uninstall_thunar() -> Result<()> {
fn dolphin_service_dir() -> PathBuf {
let data = std::env::var("XDG_DATA_HOME")
.unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap_or_else(|_| "~".into());
let home = std::env::var("HOME").unwrap_or_else(|_| dirs::home_dir().map(|h| h.to_string_lossy().into_owned()).unwrap_or_else(|| "/tmp".into()));
format!("{}/.local/share", home)
});
PathBuf::from(data).join("kio").join("servicemenus")
@@ -410,9 +421,10 @@ fn install_dolphin() -> Result<()> {
"[Desktop Action Preset{i}]\n\
Name={name}\n\
Icon=applications-graphics-symbolic\n\
Exec={bin} --preset \"{name}\" --files %F\n\n",
Exec={bin} --preset \"{safe_label}\" --files %F\n\n",
i = i,
name = name,
safe_label = shell_safe(name),
bin = bin,
));
}