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

@@ -44,6 +44,7 @@ impl OutputEncoder {
ImageFormat::Avif => self.encode_avif(img, quality.unwrap_or(80)),
ImageFormat::Gif => self.encode_fallback(img, image::ImageFormat::Gif),
ImageFormat::Tiff => self.encode_fallback(img, image::ImageFormat::Tiff),
ImageFormat::Bmp => self.encode_fallback(img, image::ImageFormat::Bmp),
}
}
@@ -68,7 +69,7 @@ impl OutputEncoder {
ImageFormat::WebP => preset.webp_quality() as u8,
ImageFormat::Avif => preset.avif_quality() as u8,
ImageFormat::Png => preset.png_level(),
_ => preset.jpeg_quality(),
ImageFormat::Gif | ImageFormat::Tiff | ImageFormat::Bmp => preset.jpeg_quality(),
}
}
@@ -202,6 +203,11 @@ impl OutputEncoder {
/// The pHYs chunk must appear before the first IDAT chunk.
/// DPI is converted to pixels per meter (1 inch = 0.0254 meters).
fn insert_png_phys_chunk(png_data: &[u8], dpi: u32) -> Vec<u8> {
// Not a valid PNG (too short for signature) - return as-is
if png_data.len() < 8 {
return png_data.to_vec();
}
// PNG pixels per meter = DPI / 0.0254
let ppm = (dpi as f64 / 0.0254).round() as u32;
@@ -241,9 +247,12 @@ fn insert_png_phys_chunk(png_data: &[u8], dpi: u32) -> Vec<u8> {
png_data[pos], png_data[pos + 1], png_data[pos + 2], png_data[pos + 3],
]) as usize;
let chunk_type = &png_data[pos + 4..pos + 8];
let total_chunk_size = 4 + 4 + chunk_len + 4; // len + type + data + crc
// Use checked arithmetic to prevent overflow on malformed PNGs
let Some(total_chunk_size) = chunk_len.checked_add(12) else {
break; // chunk_len so large it overflows - malformed PNG
};
if pos + total_chunk_size > png_data.len() {
if pos.checked_add(total_chunk_size).map_or(true, |end| end > png_data.len()) {
break;
}
@@ -263,6 +272,11 @@ fn insert_png_phys_chunk(png_data: &[u8], dpi: u32) -> Vec<u8> {
pos += total_chunk_size;
}
// Copy any remaining bytes (e.g. trailing data after a truncated chunk)
if pos < png_data.len() {
result.extend_from_slice(&png_data[pos..]);
}
result
}