diff --git a/apps/lightcraft/src/main.rs b/apps/lightcraft/src/main.rs index cc71cc06d..c9b9696e1 100644 --- a/apps/lightcraft/src/main.rs +++ b/apps/lightcraft/src/main.rs @@ -64,7 +64,7 @@ impl eframe::App for App { // Quit from the menu bar: save exactly what closing the window saves (settings and // library, after the unsaved-changes check; otherwise its prompt shows), then end the // process instead of closing the viewport while AppKit is terminating (PR #444). - eframe::App::on_exit(self); + eframe::App::on_exit(self, None); std::process::exit(0); } self.0.logic(ctx); diff --git a/crates/catalog/src/tests_lock.rs b/crates/catalog/src/tests_lock.rs index 1f5dcec24..d464d5c25 100644 --- a/crates/catalog/src/tests_lock.rs +++ b/crates/catalog/src/tests_lock.rs @@ -140,8 +140,7 @@ fn dropping_the_lock_frees_the_library_while_another_thread_spawns_processes() { std::thread::spawn(move || { let mut spawned = 0usize; while !stop.load(Ordering::Relaxed) { - if let Ok(mut c) = - std::process::Command::new("/bin/true").stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null()).spawn() + if let Ok(mut c) = std::process::Command::new("true").stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null()).spawn() { let _ = c.wait(); spawned += 1; diff --git a/crates/codecs/src/jpeg.rs b/crates/codecs/src/jpeg.rs index e40cf7e73..ce2d0480b 100644 --- a/crates/codecs/src/jpeg.rs +++ b/crates/codecs/src/jpeg.rs @@ -75,14 +75,12 @@ pub(crate) fn parse_markers(b: &[u8]) -> Option { let end = (p + 2 + len).min(b.len()); let seg = &b[start..end]; match marker { - 0xC0..=0xCF if marker != 0xC4 && marker != 0xC8 && marker != 0xCC => { - if seg.len() >= 6 && m.components == 0 { - m.precision = seg[0]; - m.sof = marker; - m.height = u16::from_be_bytes([seg[1], seg[2]]) as u32; - m.width = u16::from_be_bytes([seg[3], seg[4]]) as u32; - m.components = seg[5]; - } + 0xC0..=0xCF if marker != 0xC4 && marker != 0xC8 && marker != 0xCC && seg.len() >= 6 && m.components == 0 => { + m.precision = seg[0]; + m.sof = marker; + m.height = u16::from_be_bytes([seg[1], seg[2]]) as u32; + m.width = u16::from_be_bytes([seg[3], seg[4]]) as u32; + m.components = seg[5]; } 0xE1 => { if seg.starts_with(b"Exif\0\0") && m.exif.is_none() { diff --git a/crates/engine/src/media.rs b/crates/engine/src/media.rs index 6f6f0359c..ec3bb3379 100644 --- a/crates/engine/src/media.rs +++ b/crates/engine/src/media.rs @@ -1391,7 +1391,7 @@ impl crate::Session { /// says and what a smart preview renders with. Only for raws edited in Kelvin. fn learn_as_shot_wb(&mut self, id: PhotoId, info: &SourceInfo) { let Some(p) = self.catalog.photo(id) else { return }; - if !(info.raw && !info.relative_wb && p.develops_raw() && !p.relative_wb()) { + if !info.raw || info.relative_wb || !p.develops_raw() || p.relative_wb() { return; } let read = (info.as_shot_temp, info.as_shot_tint); diff --git a/crates/engine/src/sidecar.rs b/crates/engine/src/sidecar.rs index 4f44780ec..45b475d19 100644 --- a/crates/engine/src/sidecar.rs +++ b/crates/engine/src/sidecar.rs @@ -475,10 +475,10 @@ pub fn read_packet(original: &str, kind: MediaKind, naming: SidecarNaming) -> Op /// Photo ids touched by an op (for auto-write). pub(crate) fn op_photos(op: &Op, out: &mut Vec) { match op { - Op::SetRating { id, .. } | Op::SetFlag { id, .. } | Op::SetLabel { id, .. } | Op::SetDevelop { id, .. } | Op::SetMeta { id, .. } => { - if !out.contains(id) { - out.push(*id); - } + Op::SetRating { id, .. } | Op::SetFlag { id, .. } | Op::SetLabel { id, .. } | Op::SetDevelop { id, .. } | Op::SetMeta { id, .. } + if !out.contains(id) => + { + out.push(*id); } Op::Batch { ops } => ops.iter().for_each(|o| op_photos(o, out)), _ => {} diff --git a/crates/engine/src/sidecar/tests_repair_companions.rs b/crates/engine/src/sidecar/tests_repair_companions.rs index cefec46e8..6f4927af5 100644 --- a/crates/engine/src/sidecar/tests_repair_companions.rs +++ b/crates/engine/src/sidecar/tests_repair_companions.rs @@ -16,7 +16,7 @@ impl MoveFs for Faults { RealFs.same_file(a, b) } fn rename_no_replace(&self, a: &Path, b: &Path) -> std::io::Result<()> { - if self.0.iter().any(|p| p == a) { + if self.0.iter().any(|p| p == a || RealFs.same_file(p, a)) { Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "injected partial move")) } else { RealFs.rename_no_replace(a, b) @@ -261,8 +261,9 @@ fn queued_stem_retry_preserves_shared_packet_after_batch_rollback() { assert!(!second_packet.exists()); let pending = queues(&s); assert_eq!(pending.len(), 1); - assert_eq!(pending[0].from, shared); - assert_eq!(pending[0].to, second_packet); + // Case-insensitive filesystems may retain the uppercase alias of the shared packet. + assert!(RealFs.same_file(&pending[0].from, &shared), "repair retains the same physical shared packet"); + assert_eq!(pending[0].to, second_packet.with_extension(pending[0].from.extension().unwrap())); let catalog_before = s.catalog.to_snapshot(); let history_before = (s.undo.len(), s.redo.len(), s.commits()); diff --git a/crates/engine/src/sidecar/tests_repair_provenance.rs b/crates/engine/src/sidecar/tests_repair_provenance.rs index 63ff42002..ea4060aac 100644 --- a/crates/engine/src/sidecar/tests_repair_provenance.rs +++ b/crates/engine/src/sidecar/tests_repair_provenance.rs @@ -16,7 +16,7 @@ impl MoveFs for Faults { RealFs.same_file(a, b) } fn rename_no_replace(&self, a: &Path, b: &Path) -> std::io::Result<()> { - if self.0.iter().any(|p| p == a) { + if self.0.iter().any(|p| p == a || RealFs.same_file(p, a)) { Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "injected provenance partial move")) } else { RealFs.rename_no_replace(a, b) diff --git a/crates/engine/src/sidecar/tests_repair_saves.rs b/crates/engine/src/sidecar/tests_repair_saves.rs index 6f9fa4aa4..82ad454d9 100644 --- a/crates/engine/src/sidecar/tests_repair_saves.rs +++ b/crates/engine/src/sidecar/tests_repair_saves.rs @@ -19,14 +19,14 @@ impl MoveFs for PartialRename { RealFs.same_file(a, b) } fn rename_no_replace(&self, a: &Path, b: &Path) -> std::io::Result<()> { - if a == self.sidecar || a == self.photo { + if a == self.sidecar || a == self.photo || RealFs.same_file(a, &self.sidecar) || RealFs.same_file(a, &self.photo) { Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "transient rename fault")) } else { RealFs.rename_no_replace(a, b) } } fn copy_no_replace(&self, a: &Path, b: &Path) -> std::io::Result<()> { - if a == self.sidecar { + if a == self.sidecar || RealFs.same_file(a, &self.sidecar) { Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "transient copy fault")) } else { RealFs.copy_no_replace(a, b) @@ -151,7 +151,12 @@ fn autowrite_repairs_partial_rename_before_merging_foreign_xmp_across_library_sw assert_eq!(parsed.title.as_deref(), Some("Final title")); assert!(parsed.keywords.as_ref().unwrap().contains(&"café".to_string())); if upper { - assert!(!preferred.exists(), "merge uses the repaired uppercase packet"); + // On a case-insensitive filesystem exists() also finds the uppercase + // packet. Check its directory entry to detect an extra lowercase file. + let lower_entry = std::fs::read_dir(preferred.parent().unwrap()) + .unwrap() + .any(|entry| entry.unwrap().file_name() == preferred.file_name().unwrap()); + assert!(!lower_entry, "merge uses the repaired uppercase packet"); } for (step, title) in [("edit.undo", "Fresh title"), ("edit.redo", "Final title")] { s.execute(step, &json!({})).unwrap(); diff --git a/crates/engine/src/tests_lightroom_folder.rs b/crates/engine/src/tests_lightroom_folder.rs index cdf9905c4..9719c6914 100644 --- a/crates/engine/src/tests_lightroom_folder.rs +++ b/crates/engine/src/tests_lightroom_folder.rs @@ -156,12 +156,22 @@ fn odd_folders_never_fail_the_check() { { use std::os::unix::ffi::OsStrExt; let bad = root.join(std::ffi::OsStr::from_bytes(b"caf\xe9 Previews.lrdata")); - std::fs::create_dir_all(&bad).unwrap(); - std::fs::write(root.join("Cat.lrcat"), b"x").unwrap(); - assert_eq!(lightroom_catalog_at(&bad), Some(LightroomPlace { catalog: Some(root.join("Cat.lrcat")) }), "non-UTF-8 name"); - let lr = root.join(std::ffi::OsStr::from_bytes(b"\xff\xfe")); + let created = std::fs::create_dir_all(&bad); + // APFS rejects non-UTF-8 names with EILSEQ. Test its unavailable-path result, + // and still run the ordinary catalog/symlink checks with a representable directory. + let unrepresentable = cfg!(target_os = "macos") && created.as_ref().is_err_and(|e| e.raw_os_error() == Some(92)); + if unrepresentable { + assert_eq!(lightroom_catalog_at(&bad), Some(LightroomPlace { catalog: None })); + } else { + created.unwrap(); + std::fs::write(root.join("Cat.lrcat"), b"x").unwrap(); + assert_eq!(lightroom_catalog_at(&bad), Some(LightroomPlace { catalog: Some(root.join("Cat.lrcat")) }), "non-UTF-8 name"); + } + let lr = if unrepresentable { root.join("catalog") } else { root.join(std::ffi::OsStr::from_bytes(b"\xff\xfe")) }; std::fs::create_dir_all(&lr).unwrap(); - std::fs::write(lr.join(std::ffi::OsStr::from_bytes(b"\xff.lrcat")), b"x").unwrap(); + if !unrepresentable { + std::fs::write(lr.join(std::ffi::OsStr::from_bytes(b"\xff.lrcat")), b"x").unwrap(); + } std::fs::write(lr.join("b.lrcat"), b"x").unwrap(); assert_eq!(lightroom_catalog_at(&lr), Some(LightroomPlace { catalog: Some(lr.join("b.lrcat")) }), "non-UTF-8 entries skipped"); // a symlink to a catalog folder is one too; a dangling one is not diff --git a/crates/faces/src/net/shape.rs b/crates/faces/src/net/shape.rs index d76271bc7..b8dd6dd40 100644 --- a/crates/faces/src/net/shape.rs +++ b/crates/faces/src/net/shape.rs @@ -186,10 +186,8 @@ pub(super) fn check(n: &Node, g: &Graph) -> Result<()> { floats(n, g, 2)?; } } - "Dropout" => { - if !(0.0..1.0).contains(&float(n, "ratio", 0.5)) { - return shape_err("Dropout ratio is invalid"); - } + "Dropout" if !(0.0..1.0).contains(&float(n, "ratio", 0.5)) => { + return shape_err("Dropout ratio is invalid"); } "MaxPool" => { pair(n, "kernel_shape", 1)?; diff --git a/crates/meta/src/gpx.rs b/crates/meta/src/gpx.rs index a8e8e9071..320f7f97b 100644 --- a/crates/meta/src/gpx.rs +++ b/crates/meta/src/gpx.rs @@ -112,10 +112,8 @@ pub fn parse_gpx(s: &str) -> Result { match name.as_slice() { b"gpx" if path.is_empty() => seen_gpx = true, b"trk" if parent == Some(b"gpx") => log.tracks += 1, - b"trkseg" if parent == Some(b"trk") => { - if log.points.last().is_some_and(|p| p.segment == segment) { - segment += 1; - } + b"trkseg" if parent == Some(b"trk") && log.points.last().is_some_and(|p| p.segment == segment) => { + segment += 1; } b"trkpt" if parent == Some(b"trkseg") => { let mut p = Pending::default(); diff --git a/crates/meta/src/xmp.rs b/crates/meta/src/xmp.rs index 2a4d753dc..924b5fce0 100644 --- a/crates/meta/src/xmp.rs +++ b/crates/meta/src/xmp.rs @@ -252,13 +252,11 @@ fn parse_tree(s: &str) -> Result { stack.push(node); } } - Event::End(_) => { - if stack.len() > 1 { - let n = stack.pop().expect("len > 1"); - scopes.pop(); - if let Some(parent) = stack.last_mut() { - parent.children.push(n); - } + Event::End(_) if stack.len() > 1 => { + let n = stack.pop().expect("len > 1"); + scopes.pop(); + if let Some(parent) = stack.last_mut() { + parent.children.push(n); } } Event::Text(t) => { diff --git a/crates/pipeline/src/local.rs b/crates/pipeline/src/local.rs index ce3994cd8..ed15bd588 100644 --- a/crates/pipeline/src/local.rs +++ b/crates/pipeline/src/local.rs @@ -147,7 +147,7 @@ pub fn legacy_to_kelvin(temp: f64, tint: f64, as_shot: (f64, f64)) -> (f64, f64) /// edited on a rendered photo or a raw without matrices never pass for Kelvin when pasted onto one that has them. pub fn normalize_wb(d: &mut DevelopSettings, info: &SourceInfo) { use lightcraft_develop::{WbMode, WbScale}; - if !(info.raw && !info.relative_wb) { + if !info.raw || info.relative_wb { d.wb.scale = WbScale::Legacy; return; } diff --git a/crates/ui-egui/src/tests_dialog_fit.rs b/crates/ui-egui/src/tests_dialog_fit.rs index 895a1d864..b2b18d79c 100644 --- a/crates/ui-egui/src/tests_dialog_fit.rs +++ b/crates/ui-egui/src/tests_dialog_fit.rs @@ -94,8 +94,9 @@ fn export_dialog_is_unchanged_in_a_tall_window() { assert_fits(&h, "Export at 1600×1000"); let dialog = rect(&h, "dialog:window"); // (846 tall as before #781: the whole dialog, nothing scrolled away; 615 wide since the - // format row gained JPEG XL, #780) - assert!((dialog.height() - 846.0).abs() <= 1.0, "{dialog:?}"); + // format row gained JPEG XL, #780). The macOS-only Apple Photos checkbox adds one row. + let expected_height = 846.0 + if cfg!(target_os = "macos") { 30.0 } else { 0.0 }; + assert!((dialog.height() - expected_height).abs() <= 1.0, "{dialog:?}"); assert!((dialog.width() - 615.2).abs() <= 1.0, "{dialog:?}"); } diff --git a/xtask/src/layers.rs b/xtask/src/layers.rs index 714b47a2f..be4b44f02 100644 --- a/xtask/src/layers.rs +++ b/xtask/src/layers.rs @@ -191,11 +191,10 @@ pub fn check(crates: &[Crate]) -> Vec { match classify(&d.name) { // Unregistered deps are reported on their own entry. None => {} - Some(Class::Testkit) => { - if d.kind != DepKind::Dev { - out.push(Violation::TestkitAsNormalDep { krate: c.name.clone() }); - } + Some(Class::Testkit) if d.kind != DepKind::Dev => { + out.push(Violation::TestkitAsNormalDep { krate: c.name.clone() }); } + Some(Class::Testkit) => {} Some(dc) => { let to = dc.layer().unwrap_or(u8::MAX); if to >= layer && !(to == layer && intra_layer_allowed(&c.name, &d.name)) {