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:
@@ -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(>k::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>()
|
||||
|
||||
Reference in New Issue
Block a user