CI.1: preserve parser guards and make macOS checks portable

Keep Rust 1.95 match/boolean guards equivalent, fix the glow on_exit call,
and retain physical-file collision checks across case aliases. Exercise
APFS invalid-name handling and ordinary symlink checks without requiring
unrepresentable filenames; locate the Unix test child through PATH.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
grlessa
2026-10-11 15:04:13 -03:00
co-authored by Codex
parent 76b5377868
commit 3fd5d82967
15 changed files with 57 additions and 50 deletions
+1 -1
View File
@@ -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);
+1 -2
View File
@@ -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;
+6 -8
View File
@@ -75,14 +75,12 @@ pub(crate) fn parse_markers(b: &[u8]) -> Option<Markers> {
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() {
+1 -1
View File
@@ -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);
+4 -4
View File
@@ -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<PhotoId>) {
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)),
_ => {}
@@ -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());
@@ -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)
@@ -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();
+15 -5
View File
@@ -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
+2 -4
View File
@@ -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)?;
+2 -4
View File
@@ -112,10 +112,8 @@ pub fn parse_gpx(s: &str) -> Result<Tracklog, GpxError> {
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();
+5 -7
View File
@@ -252,13 +252,11 @@ fn parse_tree(s: &str) -> Result<Node, XmpError> {
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) => {
+1 -1
View File
@@ -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;
}
+3 -2
View File
@@ -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:?}");
}
+3 -4
View File
@@ -191,11 +191,10 @@ pub fn check(crates: &[Crate]) -> Vec<Violation> {
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)) {