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

@@ -782,7 +782,7 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage {
.child(&scrolled)
.build();
// On page map: refresh preview and show/hide per-format rows
// On page map: refresh thumbnail strip, preview, and show/hide per-format rows
{
let up = update_preview.clone();
let jc = state.job_config.clone();
@@ -793,7 +793,71 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage {
let wer = webp_effort_row;
let ar = avif_row;
let asr = avif_speed_row;
let lf = state.loaded_files.clone();
let tb = thumb_box.clone();
let ts = thumb_scrolled.clone();
let pidx = preview_index.clone();
let up2 = update_preview.clone();
page.connect_map(move |_| {
// Rebuild thumbnail strip from current file list
while let Some(child) = tb.first_child() {
tb.remove(&child);
}
let files = lf.borrow();
let max_thumbs = files.len().min(10);
for i in 0..max_thumbs {
let pic = gtk::Picture::builder()
.content_fit(gtk::ContentFit::Cover)
.width_request(50)
.height_request(50)
.build();
pic.set_filename(Some(&files[i]));
let frame = gtk::Frame::builder()
.child(&pic)
.build();
if i == *pidx.borrow() {
frame.add_css_class("accent");
}
let pidx_c = pidx.clone();
let up_c = up2.clone();
let tb_c = tb.clone();
let current_idx = i;
let btn = gtk::Button::builder()
.child(&frame)
.has_frame(false)
.tooltip_text(files[i].file_name().and_then(|n| n.to_str()).unwrap_or("image"))
.build();
btn.connect_clicked(move |_| {
*pidx_c.borrow_mut() = current_idx;
up_c(true);
let mut c = tb_c.first_child();
let mut j = 0usize;
while let Some(w) = c {
if let Some(b) = w.downcast_ref::<gtk::Button>() {
if let Some(f) = b.child().and_then(|c| c.downcast::<gtk::Frame>().ok()) {
if j == current_idx {
f.add_css_class("accent");
} else {
f.remove_css_class("accent");
}
}
}
c = w.next_sibling();
j += 1;
}
});
tb.append(&btn);
}
ts.set_visible(max_thumbs > 1);
// Clamp preview index if files were removed
{
let mut idx = pidx.borrow_mut();
if *idx >= files.len() && !files.is_empty() {
*idx = 0;
}
}
drop(files);
up(true);
let cfg = jc.borrow();
@@ -814,7 +878,7 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage {
Some(ImageFormat::Png) => has_png = true,
Some(ImageFormat::WebP) => has_webp = true,
Some(ImageFormat::Avif) => has_avif = true,
Some(ImageFormat::Gif) | Some(ImageFormat::Tiff) => {}
Some(ImageFormat::Gif) | Some(ImageFormat::Tiff) | Some(ImageFormat::Bmp) => {}
}
for (_, &choice_idx) in &cfg.format_mappings {