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:
@@ -224,6 +224,18 @@ pub fn apply_case_conversion(name: &str, case_mode: u32) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pre-compile a regex for batch use. Returns None (with message) if invalid.
|
||||
pub fn compile_rename_regex(find: &str) -> Option<regex::Regex> {
|
||||
if find.is_empty() {
|
||||
return None;
|
||||
}
|
||||
regex::RegexBuilder::new(find)
|
||||
.size_limit(1 << 16)
|
||||
.dfa_size_limit(1 << 16)
|
||||
.build()
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Apply regex find-and-replace on a filename
|
||||
pub fn apply_regex_replace(name: &str, find: &str, replace: &str) -> String {
|
||||
if find.is_empty() {
|
||||
@@ -231,6 +243,7 @@ pub fn apply_regex_replace(name: &str, find: &str, replace: &str) -> String {
|
||||
}
|
||||
match regex::RegexBuilder::new(find)
|
||||
.size_limit(1 << 16)
|
||||
.dfa_size_limit(1 << 16)
|
||||
.build()
|
||||
{
|
||||
Ok(re) => re.replace_all(name, replace).into_owned(),
|
||||
@@ -238,9 +251,17 @@ pub fn apply_regex_replace(name: &str, find: &str, replace: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a pre-compiled regex find-and-replace on a filename
|
||||
pub fn apply_regex_replace_compiled(name: &str, re: ®ex::Regex, replace: &str) -> String {
|
||||
re.replace_all(name, replace).into_owned()
|
||||
}
|
||||
|
||||
pub fn resolve_collision(path: &Path) -> PathBuf {
|
||||
if !path.exists() {
|
||||
return path.to_path_buf();
|
||||
// Use create_new (O_CREAT|O_EXCL) for atomic reservation, preventing TOCTOU races
|
||||
match std::fs::OpenOptions::new().write(true).create_new(true).open(path) {
|
||||
Ok(_) => return path.to_path_buf(),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
|
||||
Err(_) => return path.to_path_buf(), // other errors (e.g. permission) - let caller handle
|
||||
}
|
||||
|
||||
let parent = path.parent().unwrap_or(Path::new("."));
|
||||
@@ -259,15 +280,21 @@ pub fn resolve_collision(path: &Path) -> PathBuf {
|
||||
} else {
|
||||
parent.join(format!("{}_{}.{}", stem, i, ext))
|
||||
};
|
||||
if !candidate.exists() {
|
||||
return candidate;
|
||||
match std::fs::OpenOptions::new().write(true).create_new(true).open(&candidate) {
|
||||
Ok(_) => return candidate,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback - should never happen with 1000 attempts
|
||||
// Fallback with timestamp for uniqueness
|
||||
let ts = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis())
|
||||
.unwrap_or(0);
|
||||
if ext.is_empty() {
|
||||
parent.join(format!("{}_overflow", stem))
|
||||
parent.join(format!("{}_{}", stem, ts))
|
||||
} else {
|
||||
parent.join(format!("{}_overflow.{}", stem, ext))
|
||||
parent.join(format!("{}_{}.{}", stem, ts, ext))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user