From fc13807f9173f57bf4dbe1f96deaff562bf02634 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 4 Oct 2026 12:34:53 -0400 Subject: [PATCH] Never crash: color, geom, raster, develop, scenes, preview, pipeline - color: RgbSpace::to_xyz/from_xyz and bradford() no longer expect a matrix inverse. Degenerate (collinear) primaries, which readers reject before building a space from a file, leave the primaries unscaled / give the identity; the constant Bradford and OKLab matrices fall back to the identity, which can't happen (round trips are tested). - pipeline geometry: sample() matches (mode, warp) instead of expect("warp"); a warp plan without a warp (sample_plan never makes one) samples through the equivalent affine. - color, geom, raster, develop, scenes, preview and pipeline now #![deny] unwrap/expect/panic/unimplemented/todo/unreachable; clippy.toml as in the steering PR. Co-Authored-By: Claude Opus 5.5 --- clippy.toml | 4 ++++ crates/color/src/lib.rs | 11 ++++++++--- crates/color/src/perceptual.rs | 5 +++-- crates/develop/src/lib.rs | 1 + crates/geom/src/lib.rs | 1 + crates/pipeline/src/geometry.rs | 29 ++++++++++++++++------------- crates/pipeline/src/lib.rs | 1 + crates/preview/src/lib.rs | 1 + crates/raster/src/lib.rs | 1 + crates/scenes/src/lib.rs | 1 + 10 files changed, 37 insertions(+), 18 deletions(-) create mode 100644 clippy.toml diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..4b6c79f5 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,4 @@ +allow-unwrap-in-tests = true +allow-expect-in-tests = true +allow-panic-in-tests = true +allow-indexing-slicing-in-tests = true diff --git a/crates/color/src/lib.rs b/crates/color/src/lib.rs index da9ea700..7d137d56 100644 --- a/crates/color/src/lib.rs +++ b/crates/color/src/lib.rs @@ -9,6 +9,7 @@ //! //! The pipeline's working space is **linear Rec.2020, D65** ([`WORKING`]). #![forbid(unsafe_code)] +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] pub mod cct; pub mod perceptual; @@ -125,14 +126,17 @@ pub const WORKING: RgbSpace = REC2020; impl RgbSpace { /// Matrix taking linear RGB in this space to XYZ (white Y = 1) relative to its own white. + /// Degenerate (collinear) primaries, which readers reject before building a space from a + /// file, leave the primaries unscaled instead of panicking. pub fn to_xyz(&self) -> Mat3 { let p = [self.r.to_xyz(), self.g.to_xyz(), self.b.to_xyz()]; let m = Mat3([[p[0][0], p[1][0], p[2][0]], [p[0][1], p[1][1], p[2][1]], [p[0][2], p[1][2], p[2][2]]]); - let s = m.inverse().expect("primaries are independent").apply(self.white.to_xyz()); + let s = m.inverse().map_or([1.0; 3], |inv| inv.apply(self.white.to_xyz())); m.mul(&Mat3::diag(s[0], s[1], s[2])) } + /// Inverse of [`to_xyz`](Self::to_xyz); the identity for degenerate primaries. pub fn from_xyz(&self) -> Mat3 { - self.to_xyz().inverse().expect("invertible") + self.to_xyz().inverse().unwrap_or(Mat3::IDENTITY) } /// Luminance weights (the Y row of `to_xyz`). pub fn luma(&self) -> [f64; 3] { @@ -154,7 +158,8 @@ pub fn bradford(src: Xy, dst: Xy) -> Mat3 { } let s = BRADFORD.apply(src.to_xyz()); let d = BRADFORD.apply(dst.to_xyz()); - let inv = BRADFORD.inverse().expect("invertible"); + // a constant, invertible matrix: the fallback (no adaptation) can't happen + let Some(inv) = BRADFORD.inverse() else { return Mat3::IDENTITY }; inv.mul(&Mat3::diag(d[0] / s[0], d[1] / s[1], d[2] / s[2])).mul(&BRADFORD) } diff --git a/crates/color/src/perceptual.rs b/crates/color/src/perceptual.rs index aa3ffbfa..78824321 100644 --- a/crates/color/src/perceptual.rs +++ b/crates/color/src/perceptual.rs @@ -28,9 +28,10 @@ fn mats() -> &'static OkMats { let to_lms = XYZ_TO_LMS.mul(&REC2020.to_xyz()); OkMats { to_lms: to_lms.to_f32(), - from_lms: to_lms.inverse().expect("invertible").to_f32(), + // constant, invertible matrices (round trips tested below): the identity fallback can't happen + from_lms: to_lms.inverse().unwrap_or(Mat3::IDENTITY).to_f32(), to_lab: LMS_TO_LAB.to_f32(), - from_lab: LMS_TO_LAB.inverse().expect("invertible").to_f32(), + from_lab: LMS_TO_LAB.inverse().unwrap_or(Mat3::IDENTITY).to_f32(), } }) } diff --git a/crates/develop/src/lib.rs b/crates/develop/src/lib.rs index f0ffffd2..72660afd 100644 --- a/crates/develop/src/lib.rs +++ b/crates/develop/src/lib.rs @@ -4,6 +4,7 @@ //! The pipeline evaluates it; the catalog stores it; presets, copy/paste, sync, versions and history //! are operations on it. Numeric sliders are addressed by id through [`controls`]. #![forbid(unsafe_code)] +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] pub mod controls; pub mod presets; diff --git a/crates/geom/src/lib.rs b/crates/geom/src/lib.rs index 530d089d..509285aa 100644 --- a/crates/geom/src/lib.rs +++ b/crates/geom/src/lib.rs @@ -3,6 +3,7 @@ //! Conventions: image coordinates are y-down. *Normalized* coordinates map the full (uncropped, //! oriented) image to `0..1` on both axes, so tools and settings are resolution independent. #![forbid(unsafe_code)] +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] mod crop; mod homography; diff --git a/crates/pipeline/src/geometry.rs b/crates/pipeline/src/geometry.rs index e2f3fcc1..56b158e2 100644 --- a/crates/pipeline/src/geometry.rs +++ b/crates/pipeline/src/geometry.rs @@ -211,19 +211,22 @@ impl Frame { Some((nw, nh)) => std::borrow::Cow::Owned(resize(o, nw, nh, Filter::Mitchell)), None => std::borrow::Cow::Borrowed(o), }; - match plan.mode { - SampleMode::Copy => base.into_owned(), - SampleMode::Warp(o2t) => sample_warped(&base, plan.sx, plan.sy, self.warp.as_ref().expect("warp"), o2t, w, h), - SampleMode::Affine(xf) => { - let mut out = Rgb32f::new(w, h); - par_rows(&mut out.data, w, |y, row| { - for (x, px) in row.iter_mut().enumerate() { - let p = xf.apply(Point::new(x as f64 + 0.5, y as f64 + 0.5)); - *px = base.sample_bilinear(p.x as f32, p.y as f32); - } - }); - out - } + let affine = |xf: Affine| { + let mut out = Rgb32f::new(w, h); + par_rows(&mut out.data, w, |y, row| { + for (x, px) in row.iter_mut().enumerate() { + let p = xf.apply(Point::new(x as f64 + 0.5, y as f64 + 0.5)); + *px = base.sample_bilinear(p.x as f32, p.y as f32); + } + }); + out + }; + match (plan.mode, self.warp.as_ref()) { + (SampleMode::Copy, _) => base.into_owned(), + (SampleMode::Warp(o2t), Some(warp)) => sample_warped(&base, plan.sx, plan.sy, warp, o2t, w, h), + // `sample_plan` plans a warp only when there is one; without it the same mapping is affine + (SampleMode::Warp(o2t), None) => affine(Affine::scale(plan.sx, plan.sy) * o2t), + (SampleMode::Affine(xf), _) => affine(xf), } } diff --git a/crates/pipeline/src/lib.rs b/crates/pipeline/src/lib.rs index fb361c3c..875aca11 100644 --- a/crates/pipeline/src/lib.rs +++ b/crates/pipeline/src/lib.rs @@ -21,6 +21,7 @@ //! [`render_cached`] each stage's output is reused while its inputs are unchanged ([`StageCache`]): //! dragging a tone, colour or exposure slider re-runs only the per-pixel stage. #![forbid(unsafe_code)] +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] pub mod auto; pub mod colorops; diff --git a/crates/preview/src/lib.rs b/crates/preview/src/lib.rs index e36f1b41..9518983c 100644 --- a/crates/preview/src/lib.rs +++ b/crates/preview/src/lib.rs @@ -10,6 +10,7 @@ //! //! No UI dependencies (L3): the egui frontend, the CLI and the MCP server share it. #![forbid(unsafe_code)] +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] pub mod disk; pub mod hash; diff --git a/crates/raster/src/lib.rs b/crates/raster/src/lib.rs index 50593f29..a4c7a9c2 100644 --- a/crates/raster/src/lib.rs +++ b/crates/raster/src/lib.rs @@ -6,6 +6,7 @@ //! - Blur ([`blur`]): separable Gaussian via repeated box filters (O(1) per pixel in the radius). //! - [`Histogram`] of display-encoded RGB + luminance. #![forbid(unsafe_code)] +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] pub mod blur; pub mod histogram; diff --git a/crates/scenes/src/lib.rs b/crates/scenes/src/lib.rs index 69d80eb7..0b7ad016 100644 --- a/crates/scenes/src/lib.rs +++ b/crates/scenes/src/lib.rs @@ -5,6 +5,7 @@ //! develop pipeline's highlight recovery, tone mapping and colour tools have something real to do. //! Every scene is deterministic for its seed. No external assets: zero licensing or privacy risk. #![forbid(unsafe_code)] +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] mod noise; mod paint;