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

@@ -199,12 +199,21 @@ fn is_image_file(path: &std::path::Path) -> bool {
}
fn add_images_from_dir(dir: &std::path::Path, files: &mut Vec<PathBuf>) {
let existing: std::collections::HashSet<PathBuf> = files.iter().cloned().collect();
add_images_from_dir_inner(dir, files, &existing);
}
fn add_images_from_dir_inner(
dir: &std::path::Path,
files: &mut Vec<PathBuf>,
existing: &std::collections::HashSet<PathBuf>,
) {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
add_images_from_dir(&path, files);
} else if is_image_file(&path) && !files.contains(&path) {
add_images_from_dir_inner(&path, files, existing);
} else if is_image_file(&path) && !existing.contains(&path) && !files.contains(&path) {
files.push(path);
}
}
@@ -212,10 +221,11 @@ fn add_images_from_dir(dir: &std::path::Path, files: &mut Vec<PathBuf>) {
}
fn add_images_flat(dir: &std::path::Path, files: &mut Vec<PathBuf>) {
let existing: std::collections::HashSet<PathBuf> = files.iter().cloned().collect();
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() && is_image_file(&path) && !files.contains(&path) {
if path.is_file() && is_image_file(&path) && !existing.contains(&path) {
files.push(path);
}
}
@@ -548,20 +558,32 @@ fn build_loaded_state(state: &AppState) -> gtk::Box {
let select_all_button = gtk::Button::builder()
.icon_name("edit-select-all-symbolic")
.tooltip_text("Select all images (Ctrl+A)")
.sensitive(false)
.build();
select_all_button.add_css_class("flat");
select_all_button.update_property(&[
gtk::accessible::Property::Label("Select all images for processing"),
]);
let deselect_all_button = gtk::Button::builder()
.icon_name("edit-clear-symbolic")
.tooltip_text("Deselect all images (Ctrl+Shift+A)")
.sensitive(false)
.build();
deselect_all_button.add_css_class("flat");
deselect_all_button.update_property(&[
gtk::accessible::Property::Label("Deselect all images from processing"),
]);
let clear_button = gtk::Button::builder()
.icon_name("edit-clear-all-symbolic")
.tooltip_text("Remove all images")
.sensitive(false)
.build();
clear_button.add_css_class("flat");
clear_button.update_property(&[
gtk::accessible::Property::Label("Remove all images from list"),
]);
// Build the grid view model
let store = gtk::gio::ListStore::new::<ImageItem>();
@@ -835,6 +857,19 @@ fn build_loaded_state(state: &AppState) -> gtk::Box {
toolbar.append(&add_button);
toolbar.append(&clear_button);
// Enable/disable toolbar buttons based on whether the store has items
{
let sa = select_all_button.clone();
let da = deselect_all_button.clone();
let cl = clear_button.clone();
store.connect_items_changed(move |store, _, _, _| {
let has_items = store.n_items() > 0;
sa.set_sensitive(has_items);
da.set_sensitive(has_items);
cl.set_sensitive(has_items);
});
}
toolbar.update_property(&[
gtk::accessible::Property::Label("Image toolbar with count, selection, and add controls"),
]);