mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-28 12:08:10 -06:00
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:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user