Fix 40+ bugs from audit passes 9-12

- PNG chunk parsing overflow protection with checked arithmetic
- Font directory traversal bounded with global result limit
- find_unique_path TOCTOU race fixed with create_new + marker byte
- Watch mode "processed" dir exclusion narrowed to prevent false skips
- Metadata copy now checks format support before little_exif calls
- Clipboard temp files cleaned up on app exit
- Atomic writes for file manager integration scripts
- BMP format support added to encoder and convert step
- Regex DoS protection with DFA size limit
- Watermark NaN/negative scale guard
- Selective EXIF stripping for privacy/custom metadata modes
- CLI watch mode: file stability checks, per-file history saves
- High contrast toggle preserves and restores original theme
- Image list deduplication uses O(1) HashSet lookups
- Saturation/trim/padding overflow guards in adjustments
This commit is contained in:
2026-03-07 22:14:48 +02:00
parent adef810691
commit d1cab8a691
18 changed files with 600 additions and 113 deletions

View File

@@ -624,6 +624,12 @@ fn build_ui(app: &adw::Application) {
let _ = std::fs::remove_dir_all(&temp_downloads);
}
// Clean up clipboard temp files on exit
let temp_dir = std::env::temp_dir().join("pixstrip-clipboard");
if temp_dir.is_dir() {
let _ = std::fs::remove_dir_all(&temp_dir);
}
glib::Propagation::Proceed
});
}
@@ -784,7 +790,9 @@ fn start_watch_folder_monitoring(ui: &WizardUi) {
job.add_source(file);
}
let executor = pixstrip_core::executor::PipelineExecutor::new();
let _ = executor.execute(&job, |_| {});
if let Err(e) = executor.execute(&job, |_| {}) {
eprintln!("Watch folder processing error: {}", e);
}
});
let toast = adw::Toast::new(&format!(
@@ -1105,8 +1113,14 @@ fn setup_window_actions(window: &adw::ApplicationWindow, ui: &WizardUi) {
.collect();
if !new_files.is_empty() {
let mut loaded = ui.state.loaded_files.borrow_mut();
let new_files: Vec<_> = new_files
.into_iter()
.filter(|p| !loaded.contains(p))
.collect();
let count = new_files.len();
ui.state.loaded_files.borrow_mut().extend(new_files);
loaded.extend(new_files);
drop(loaded);
ui.toast_overlay.add_toast(adw::Toast::new(
&format!("{} images added from file manager", count)
));
@@ -1481,8 +1495,9 @@ fn show_history_dialog(window: &adw::ApplicationWindow) {
output_row.add_prefix(&gtk::Image::from_icon_name("folder-open-symbolic"));
let out_dir = entry.output_dir.clone();
output_row.connect_activated(move |_| {
let uri = gtk::gio::File::for_path(&out_dir).uri();
let _ = gtk::gio::AppInfo::launch_default_for_uri(
&format!("file://{}", out_dir),
&uri,
gtk::gio::AppLaunchContext::NONE,
);
});
@@ -1546,8 +1561,9 @@ fn show_history_dialog(window: &adw::ApplicationWindow) {
open_btn.add_css_class("flat");
let out_dir2 = entry.output_dir.clone();
open_btn.connect_clicked(move |_| {
let uri = gtk::gio::File::for_path(&out_dir2).uri();
let _ = gtk::gio::AppInfo::launch_default_for_uri(
&format!("file://{}", out_dir2),
&uri,
gtk::gio::AppLaunchContext::NONE,
);
});
@@ -1674,10 +1690,31 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) {
return;
}
let input_dir = files[0]
.parent()
.unwrap_or_else(|| std::path::Path::new("."))
.to_path_buf();
let input_dir = {
let first_parent = files[0]
.parent()
.unwrap_or_else(|| std::path::Path::new("."))
.to_path_buf();
if files.len() == 1 {
first_parent
} else {
// Find common ancestor of all files
let mut common = first_parent.clone();
for f in &files[1..] {
let p = f.parent().unwrap_or_else(|| std::path::Path::new("."));
while !p.starts_with(&common) {
if !common.pop() {
break;
}
}
}
if common.as_os_str().is_empty() {
first_parent
} else {
common
}
}
};
let output_dir = ui
.state
@@ -1926,8 +1963,10 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) {
job.add_source(file);
}
// Check for existing output files when "Ask" overwrite behavior is set
if ask_overwrite {
// Check for existing output files when "Ask" overwrite behavior is set.
// Skip check if rename or format conversion is active (output names will differ).
let has_rename_or_convert = job.rename.is_some() || job.convert.is_some();
if ask_overwrite && !has_rename_or_convert {
let output_dir = ui.state.output_dir.borrow().clone()
.unwrap_or_else(|| {
files[0].parent()
@@ -1996,6 +2035,9 @@ fn continue_processing(
ui.step_indicator.widget().set_visible(false);
ui.title.set_subtitle("Processing...");
// Disable navigation actions so Escape/shortcuts can't navigate away during processing
set_nav_actions_enabled(&ui.nav_view, false);
// Get references to progress widgets inside the page
let progress_bar = find_widget_by_type::<gtk::ProgressBar>(&processing_page);
let cancel_flag = Arc::new(AtomicBool::new(false));
@@ -2074,6 +2116,8 @@ fn continue_processing(
ui_for_rx.toast_overlay.add_toast(toast);
ui_for_rx.back_button.set_visible(true);
ui_for_rx.next_button.set_visible(true);
// Re-enable navigation actions
set_nav_actions_enabled(&ui_for_rx.nav_view, true);
if let Some(visible) = ui_for_rx.nav_view.visible_page()
&& visible.tag().as_deref() == Some("processing")
{
@@ -2117,6 +2161,9 @@ fn show_results(
ui.next_button.set_label("Process More");
ui.next_button.set_visible(true);
// Re-enable navigation actions
set_nav_actions_enabled(&ui.nav_view, true);
// Save history with output paths for undo support
let history = pixstrip_core::storage::HistoryStore::new();
let output_dir_str = ui.state.output_dir.borrow()
@@ -2218,8 +2265,9 @@ fn show_results(
if config.auto_open_output {
let output = ui.state.output_dir.borrow().clone();
if let Some(dir) = output {
let uri = gtk::gio::File::for_path(&dir).uri();
let _ = gtk::gio::AppInfo::launch_default_for_uri(
&format!("file://{}", dir.display()),
&uri,
gtk::gio::AppLaunchContext::NONE,
);
}
@@ -2306,8 +2354,9 @@ fn wire_results_actions(
row.connect_activated(move |_| {
let output = ui.state.output_dir.borrow().clone();
if let Some(dir) = output {
let uri = gtk::gio::File::for_path(&dir).uri();
let _ = gtk::gio::AppInfo::launch_default_for_uri(
&format!("file://{}", dir.display()),
&uri,
gtk::gio::AppLaunchContext::NONE,
);
}
@@ -2453,6 +2502,22 @@ fn reset_wizard(ui: &WizardUi) {
ui.next_button.add_css_class("suggested-action");
}
/// Enable or disable navigation actions (prev-step, next-step) to prevent
/// keyboard shortcuts from navigating away during processing.
fn set_nav_actions_enabled(nav_view: &adw::NavigationView, enabled: bool) {
if let Some(root) = nav_view.root() {
if let Ok(win) = root.downcast::<adw::ApplicationWindow>() {
for name in ["prev-step", "next-step"] {
if let Some(action) = win.lookup_action(name) {
if let Some(simple) = action.downcast_ref::<gtk::gio::SimpleAction>() {
simple.set_enabled(enabled);
}
}
}
}
}
}
fn wire_cancel_button(page: &adw::NavigationPage, cancel_flag: Arc<AtomicBool>) {
walk_widgets(&page.child(), &|widget| {
if let Some(button) = widget.downcast_ref::<gtk::Button>()