Fix 30 critical and high severity bugs from audit passes 6-8
Critical fixes: - Prevent path traversal via rename templates (sanitize_filename) - Prevent input == output data loss (paths_are_same check) - Undo now uses actual executor output paths instead of scanning directory - Filter empty paths from output_files (prevents trashing CWD on undo) - Sanitize URL download filenames to prevent path traversal writes High severity fixes: - Fix EXIF orientation 5/7 transforms per spec - Atomic file creation in find_unique_path (TOCTOU race) - Clean up 0-byte placeholder files on encoding failure - Cap canvas padding to 10000px, total dimensions to 65535 - Clamp crop dimensions to minimum 1px - Clamp DPI to 65535 before u16 cast in JPEG encoder - Force pixel path for non-JPEG/TIFF metadata stripping - Fast path now applies regex find/replace on rename stem - Add output_dpi to needs_pixel_processing check - Cap watermark image scale dimensions to 16384 - Cap template counter padding to 10 - Cap URL download size to 100MB - Fix progress bar NaN when total is zero - Fix calculate_eta underflow when current > total - Fix loaded.len()-1 underflow in preview callbacks - Replace ListItem downcast unwrap with if-let - Fix resize preview division by zero on degenerate images - Clamp rename cursor position to prevent overflow panic - Watch mode: skip output dirs to prevent infinite loop - Watch mode: drop tx sender so channel closes on exit - Watch mode: add delay for partially-written files - Watch mode: warn and skip unmatched files instead of wrong preset - Clean temp download directory on app close - Replace action downcast unwrap with checked if-let - Add BatchResult.output_files for accurate undo tracking
This commit is contained in:
@@ -419,37 +419,8 @@ fn cmd_process(args: CmdProcessArgs) {
|
||||
}
|
||||
}
|
||||
|
||||
// Save to history
|
||||
// Save to history - use actual output paths from the executor
|
||||
let history = HistoryStore::new();
|
||||
let output_ext = match job.convert {
|
||||
Some(ConvertConfig::SingleFormat(fmt)) => fmt.extension(),
|
||||
_ => "",
|
||||
};
|
||||
let output_files: Vec<String> = source_files
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, f)| {
|
||||
let stem = f.file_stem().and_then(|s| s.to_str()).unwrap_or("file");
|
||||
let ext = if output_ext.is_empty() {
|
||||
f.extension().and_then(|e| e.to_str()).unwrap_or("jpg")
|
||||
} else {
|
||||
output_ext
|
||||
};
|
||||
let name = if let Some(ref rename) = job.rename {
|
||||
if let Some(ref tmpl) = rename.template {
|
||||
pixstrip_core::operations::rename::apply_template(
|
||||
tmpl, stem, ext, rename.counter_start.saturating_add(i as u32), None,
|
||||
)
|
||||
} else {
|
||||
rename.apply_simple(stem, ext, (i as u32).saturating_add(1))
|
||||
}
|
||||
} else {
|
||||
format!("{}.{}", stem, ext)
|
||||
};
|
||||
output_dir.join(name).to_string_lossy().into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
if let Err(e) = history.add(pixstrip_core::storage::HistoryEntry {
|
||||
timestamp: chrono_timestamp(),
|
||||
input_dir: input_dir.canonicalize().unwrap_or_else(|_| input_dir.to_path_buf()).to_string_lossy().into(),
|
||||
@@ -461,7 +432,7 @@ fn cmd_process(args: CmdProcessArgs) {
|
||||
total_input_bytes: result.total_input_bytes,
|
||||
total_output_bytes: result.total_output_bytes,
|
||||
elapsed_ms: result.elapsed_ms,
|
||||
output_files,
|
||||
output_files: result.output_files,
|
||||
}, 50, 30) {
|
||||
eprintln!("Warning: failed to save history (undo may not work): {}", e);
|
||||
}
|
||||
@@ -777,6 +748,8 @@ fn cmd_watch_start() {
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut watchers = Vec::new();
|
||||
// Collect output directories so we can skip files inside them (prevent infinite loop)
|
||||
let mut output_dirs: Vec<PathBuf> = Vec::new();
|
||||
|
||||
for watch in &active {
|
||||
let watcher = pixstrip_core::watcher::FolderWatcher::new();
|
||||
@@ -784,19 +757,41 @@ fn cmd_watch_start() {
|
||||
eprintln!("Failed to start watching {}: {}", watch.path.display(), e);
|
||||
continue;
|
||||
}
|
||||
output_dirs.push(watch.path.join("processed"));
|
||||
watchers.push((watcher, watch.preset_name.clone()));
|
||||
}
|
||||
|
||||
// Drop the original sender so the channel closes when all watcher threads exit
|
||||
drop(tx);
|
||||
|
||||
if watchers.is_empty() {
|
||||
eprintln!("Failed to start any watchers. Check that watch folders exist and are accessible.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Process incoming files
|
||||
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)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
println!("New image: {}", path.display());
|
||||
|
||||
// Wait briefly for file to be fully written
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
|
||||
// Find which watcher owns this path and use its preset
|
||||
let matched = active.iter()
|
||||
.find(|w| path.starts_with(&w.path))
|
||||
.map(|w| w.preset_name.clone());
|
||||
if let Some(preset_name) = matched.as_deref().or_else(|| watchers.first().map(|(_, n)| n.as_str())) {
|
||||
if matched.is_none() {
|
||||
eprintln!(" Warning: no matching watch folder for {}, skipping", path.display());
|
||||
continue;
|
||||
}
|
||||
if let Some(preset_name) = matched.as_deref() {
|
||||
let Some(preset) = find_preset(preset_name) else {
|
||||
eprintln!(" Preset '{}' not found, skipping", preset_name);
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user