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:
@@ -626,16 +626,19 @@ fn cmd_watch_add(path: &str, preset_name: &str, recursive: bool) {
|
||||
|
||||
watches.push(watch);
|
||||
if let Err(e) = std::fs::create_dir_all(&config_dir) {
|
||||
eprintln!("Warning: failed to create config directory: {}", e);
|
||||
eprintln!("Failed to create config directory: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
match serde_json::to_string_pretty(&watches) {
|
||||
Ok(json) => {
|
||||
if let Err(e) = std::fs::write(&watches_path, json) {
|
||||
eprintln!("Warning: failed to write watch config: {}", e);
|
||||
eprintln!("Failed to write watch config: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Warning: failed to serialize watch config: {}", e);
|
||||
eprintln!("Failed to serialize watch config: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -773,15 +776,35 @@ fn cmd_watch_start() {
|
||||
for event in &rx {
|
||||
match event {
|
||||
pixstrip_core::watcher::WatchEvent::NewImage(path) => {
|
||||
// Skip files inside output directories to prevent infinite processing loop
|
||||
if output_dirs.iter().any(|d| path.starts_with(d)) {
|
||||
// Skip files inside output directories to prevent infinite processing loop.
|
||||
// Check if the file is a direct child of any "processed" subdirectory
|
||||
// under a watched folder.
|
||||
let in_output_dir = output_dirs.iter().any(|d| path.starts_with(d))
|
||||
|| active.iter().any(|w| {
|
||||
// Skip if the file's immediate parent is "processed" under the watch root
|
||||
path.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.is_some_and(|name| name == "processed")
|
||||
&& path.starts_with(&w.path)
|
||||
});
|
||||
if in_output_dir {
|
||||
continue;
|
||||
}
|
||||
|
||||
println!("New image: {}", path.display());
|
||||
|
||||
// Wait briefly for file to be fully written
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
// Wait for file to be fully written (check size stability)
|
||||
{
|
||||
let mut last_size = 0u64;
|
||||
for _ in 0..10 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
let size = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
|
||||
if size > 0 && size == last_size {
|
||||
break;
|
||||
}
|
||||
last_size = size;
|
||||
}
|
||||
}
|
||||
|
||||
// Find which watcher owns this path and use its preset
|
||||
let matched = active.iter()
|
||||
@@ -803,7 +826,23 @@ fn cmd_watch_start() {
|
||||
|
||||
let executor = PipelineExecutor::new();
|
||||
match executor.execute(&job, |_| {}) {
|
||||
Ok(r) => println!(" Processed: {} -> {}", format_bytes(r.total_input_bytes), format_bytes(r.total_output_bytes)),
|
||||
Ok(r) => {
|
||||
println!(" Processed: {} -> {}", format_bytes(r.total_input_bytes), format_bytes(r.total_output_bytes));
|
||||
let history = HistoryStore::new();
|
||||
let _ = history.add(pixstrip_core::storage::HistoryEntry {
|
||||
timestamp: chrono_timestamp(),
|
||||
input_dir: input_dir.to_string_lossy().into(),
|
||||
output_dir: output_dir.to_string_lossy().into(),
|
||||
preset_name: Some(preset_name.to_string()),
|
||||
total: r.total,
|
||||
succeeded: r.succeeded,
|
||||
failed: r.failed,
|
||||
total_input_bytes: r.total_input_bytes,
|
||||
total_output_bytes: r.total_output_bytes,
|
||||
elapsed_ms: r.elapsed_ms,
|
||||
output_files: r.output_files,
|
||||
}, 50, 30);
|
||||
}
|
||||
Err(e) => eprintln!(" Failed: {}", e),
|
||||
}
|
||||
}
|
||||
@@ -882,8 +921,9 @@ fn parse_format(s: &str) -> Option<ImageFormat> {
|
||||
"avif" => Some(ImageFormat::Avif),
|
||||
"gif" => Some(ImageFormat::Gif),
|
||||
"tiff" | "tif" => Some(ImageFormat::Tiff),
|
||||
"bmp" => Some(ImageFormat::Bmp),
|
||||
_ => {
|
||||
eprintln!("Unknown format: '{}'. Supported: jpeg, png, webp, avif, gif, tiff", s);
|
||||
eprintln!("Unknown format: '{}'. Supported: jpeg, png, webp, avif, gif, tiff, bmp", s);
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -980,12 +1020,42 @@ fn format_duration(ms: u64) -> String {
|
||||
}
|
||||
|
||||
fn chrono_timestamp() -> String {
|
||||
// Simple timestamp without chrono dependency
|
||||
// Human-readable timestamp without chrono dependency
|
||||
let now = std::time::SystemTime::now();
|
||||
let duration = now
|
||||
let secs = now
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default();
|
||||
format!("{}", duration.as_secs())
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
// Convert to date/time components
|
||||
let days = secs / 86400;
|
||||
let time_secs = secs % 86400;
|
||||
let hours = time_secs / 3600;
|
||||
let minutes = (time_secs % 3600) / 60;
|
||||
let seconds = time_secs % 60;
|
||||
|
||||
let mut d = days;
|
||||
let mut year = 1970u64;
|
||||
loop {
|
||||
let days_in_year = if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 { 366 } else { 365 };
|
||||
if d < days_in_year { break; }
|
||||
d -= days_in_year;
|
||||
year += 1;
|
||||
}
|
||||
let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
|
||||
let month_days: [u64; 12] = if leap {
|
||||
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||
} else {
|
||||
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||
};
|
||||
let mut month = 1u64;
|
||||
for md in &month_days {
|
||||
if d < *md { break; }
|
||||
d -= md;
|
||||
month += 1;
|
||||
}
|
||||
|
||||
format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}", year, month, d + 1, hours, minutes, seconds)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -999,11 +1069,11 @@ mod tests {
|
||||
assert_eq!(parse_format("PNG"), Some(ImageFormat::Png));
|
||||
assert_eq!(parse_format("webp"), Some(ImageFormat::WebP));
|
||||
assert_eq!(parse_format("avif"), Some(ImageFormat::Avif));
|
||||
assert_eq!(parse_format("bmp"), Some(ImageFormat::Bmp));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_format_invalid() {
|
||||
assert_eq!(parse_format("bmp"), None);
|
||||
assert_eq!(parse_format("xyz"), None);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user