Fix 26 bugs, edge cases, and consistency issues from fifth audit pass

Critical: undo toast now trashes only batch output files (not entire dir),
JPEG scanline write errors propagated, selective metadata write result returned.

High: zero-dimension guards in ResizeConfig/fit_within, negative aspect ratio
rejection, FM integration toggle infinite recursion guard, saturating counter
arithmetic in executor.

Medium: PNG compression level passed to oxipng, pct mode updates job_config,
external file loading updates step indicator, CLI undo removes history entries,
watch config write failures reported, fast-copy path reads image dimensions for
rename templates, discovery excludes unprocessable formats (heic/svg/ico/jxl),
CLI warns on invalid algorithm/overwrite values, resolve_collision trailing dot
fix, generation guards on all preview threads to cancel stale results, default
DPI aligned to 0, watermark text width uses char count not byte length.

Low: binary path escaped in Nautilus extension, file dialog filter aligned with
discovery, reset_wizard clears preset_mode and output_dir.
This commit is contained in:
2026-03-07 19:47:23 +02:00
parent 270a7db60d
commit b432cc7431
44 changed files with 5748 additions and 2221 deletions

View File

@@ -91,8 +91,12 @@ fn rename_config_simple_template() {
suffix: String::new(),
counter_start: 1,
counter_padding: 3,
counter_enabled: true,
counter_position: 3,
template: None,
case_mode: 0,
replace_spaces: 0,
special_chars: 0,
regex_find: String::new(),
regex_replace: String::new(),
};
@@ -107,11 +111,101 @@ fn rename_config_with_suffix() {
suffix: "_web".into(),
counter_start: 1,
counter_padding: 2,
counter_enabled: true,
counter_position: 3,
template: None,
case_mode: 0,
replace_spaces: 0,
special_chars: 0,
regex_find: String::new(),
regex_replace: String::new(),
};
let result = config.apply_simple("photo", "webp", 5);
assert_eq!(result, "photo_web_05.webp");
}
#[test]
fn rename_counter_overflow_saturates() {
let config = RenameConfig {
prefix: String::new(),
suffix: String::new(),
counter_start: u32::MAX,
counter_padding: 1,
counter_enabled: true,
counter_position: 3,
template: None,
case_mode: 0,
replace_spaces: 0,
special_chars: 0,
regex_find: String::new(),
regex_replace: String::new(),
};
// Should not panic - saturating arithmetic
let result = config.apply_simple("photo", "jpg", u32::MAX);
assert!(result.contains("photo"));
}
#[test]
fn metadata_config_custom_selective() {
let config = MetadataConfig::Custom {
strip_gps: true,
strip_camera: false,
strip_software: true,
strip_timestamps: false,
strip_copyright: false,
};
assert!(config.should_strip_gps());
assert!(!config.should_strip_camera());
assert!(!config.should_strip_copyright());
}
#[test]
fn metadata_config_custom_all_off() {
let config = MetadataConfig::Custom {
strip_gps: false,
strip_camera: false,
strip_software: false,
strip_timestamps: false,
strip_copyright: false,
};
assert!(!config.should_strip_gps());
assert!(!config.should_strip_camera());
assert!(!config.should_strip_copyright());
}
#[test]
fn metadata_config_custom_all_on() {
let config = MetadataConfig::Custom {
strip_gps: true,
strip_camera: true,
strip_software: true,
strip_timestamps: true,
strip_copyright: true,
};
assert!(config.should_strip_gps());
assert!(config.should_strip_camera());
assert!(config.should_strip_copyright());
}
#[test]
fn watermark_rotation_variants_exist() {
let rotations = [
WatermarkRotation::Degrees45,
WatermarkRotation::DegreesNeg45,
WatermarkRotation::Degrees90,
WatermarkRotation::Custom(30.0),
];
assert_eq!(rotations.len(), 4);
}
#[test]
fn rotation_auto_orient_variant() {
let rotation = Rotation::AutoOrient;
assert!(matches!(rotation, Rotation::AutoOrient));
}
#[test]
fn overwrite_action_default_is_auto_rename() {
let default = OverwriteAction::default();
assert!(matches!(default, OverwriteAction::AutoRename));
}