From 010d9bb5daac705d58bf77467f5440887df2b10f Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 9 Oct 2026 20:43:04 -0400 Subject: [PATCH] LR-IMP-FORMATS: saturation-point search refuses NaN, negative or inverted levels and overflowing active areas (underflowed on a negative black level) Co-Authored-By: Claude Opus 5.5 --- crates/raw/src/saturation.rs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/crates/raw/src/saturation.rs b/crates/raw/src/saturation.rs index 25295590..aaa0a6df 100644 --- a/crates/raw/src/saturation.rs +++ b/crates/raw/src/saturation.rs @@ -35,11 +35,14 @@ impl RawImage { let (RawData::U16(d), Some(cfa)) = (&self.data, &self.cfa) else { return none }; let white = self.white_at(0); let black = self.black.mean(); - if self.cpp != 1 || !cfa.valid() || d.len() != self.width * self.height || white <= black || white > 65535.0 { + // a hostile file can state any levels: NaN, negative or inverted ones are left alone + let levels_ok = black.is_finite() && white.is_finite() && black >= 0.0 && white > black && white <= 65535.0; + if self.cpp != 1 || !cfa.valid() || self.width.checked_mul(self.height) != Some(d.len()) || !levels_ok { return none; } let a = self.active_area; - if a.x + a.width > self.width || a.y + a.height > self.height { + let fits = |start: usize, len: usize, max: usize| start.checked_add(len).is_some_and(|end| end <= max); + if !fits(a.x, a.width, self.width) || !fits(a.y, a.height, self.height) { return none; } let lo = (black + 0.5 * (white - black)).ceil().max(0.0) as usize; @@ -64,7 +67,7 @@ impl RawImage { if (n as f64) < (total[c] as f64 * MIN_SHARE).max(100.0) || v as f32 >= white - NEAR_WHITE * (white - black) { continue; } - let below: u64 = (v - NEIGHBOURHOOD..v).map(|u| hist[u][c] as u64).sum(); + let below: u64 = (v.saturating_sub(NEIGHBOURHOOD)..v).map(|u| hist[u][c] as u64).sum(); let from = (v + ((ABOVE_MARGIN * (white - black)) as usize).max(1) + 1).min(hist.len()); let above: u64 = hist[from..].iter().map(|h| h[c] as u64).sum(); if n as f64 >= MIN_SPIKE * (below as f64 / NEIGHBOURHOOD as f64).max(1.0) && above as f64 <= MAX_ABOVE * n as f64 { @@ -146,6 +149,21 @@ mod tests { } } + #[test] + fn hostile_levels_are_left_alone() { + // a negative or NaN black level used to let the search start below NEIGHBOURHOOD and underflow + for black in [-5000.0, f32::NAN] { + let mut r = raw_with(Some(40), 4095.0); + r.black = BlackLevel::uniform(black); + assert_eq!(r.saturation_points(), [None; 3]); + assert_eq!(r.lift_clipped_samples(), 0); + } + // an active area that runs past the image (or overflows when added up) is refused + let mut r = raw_with(Some(3567), 4095.0); + r.active_area = Rect::new(usize::MAX, 0, 2, 64); + assert_eq!(r.saturation_points(), [None; 3]); + } + #[test] fn a_pile_up_below_the_white_level_is_found_and_lifted() { let mut r = raw_with(Some(3567), 4095.0);