diff --git a/crates/raw/src/vendor/erf.rs b/crates/raw/src/vendor/erf.rs index b9861ef386..7f88dae2af 100644 --- a/crates/raw/src/vendor/erf.rs +++ b/crates/raw/src/vendor/erf.rs @@ -95,8 +95,12 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result { _ => Rect::new(0, 0, w, h), }; // the white balance is measured from the samples, so the header probe reads them too + // the strip length is the tag value, so a short file must fail before the sample buffer is sized from it let chunks = info.chunks(bytes.len() as u64); - let src = chunks.first().and_then(|c| chunk_bytes(bytes, c)).ok_or_else(|| RawError::Corrupt("Erf strip outside file".into()))?; + let src = chunks + .first() + .and_then(|c| chunk_bytes(bytes, c).filter(|s| s.len() as u64 == c.len)) + .ok_or_else(|| RawError::Corrupt("Erf strip outside file".into()))?; let mut d = vec![0u16; n]; unpack(src, &mut d); let white = super::white_from_data(&d, 12); @@ -196,4 +200,35 @@ mod tests { } } } + + /// Rewrite the raw IFD's height, rows per strip and strip length in place (the strip data stays as written). + fn declare(f: &mut [u8], rows: u32, strip_len: u32) { + let tiff = Tiff::parse(f).unwrap(); + let raw = &tiff.ifds[0].sub_ifds[0]; + let spots: Vec<_> = [(t::IMAGE_LENGTH, rows), (t::ROWS_PER_STRIP, rows), (t::STRIP_BYTE_COUNTS, strip_len)] + .into_iter() + .map(|(tag, v)| { + let e = raw.entries.iter().find(|e| e.tag == tag).unwrap(); + (e.offset as usize, matches!(e.value, Value::Short(_)), v) + }) + .collect(); + for (at, short, v) in spots { + if short { + f[at..at + 2].copy_from_slice(&u16::try_from(v).unwrap().to_be_bytes()); + } else { + f[at..at + 4].copy_from_slice(&v.to_be_bytes()); + } + } + } + + #[test] + fn a_strip_longer_than_the_file_is_an_error() { + // #2119: the declared strip length matched the size, so a few bytes of data were decoded as a full frame + let px: Vec = (0..40).map(|i| (i * 13 % 4096) as u16).collect(); + let mut f = sample(20, 2, &px); + declare(&mut f, 2000, 32 * 2000); + assert_eq!(crate::probe(&f), Some(RawFormat::Erf)); + assert!(matches!(crate::probe_info(&f), Err(RawError::Corrupt(_)))); + assert!(matches!(crate::decode(&f), Err(RawError::Corrupt(_)))); + } } diff --git a/crates/raw/src/vendor/mef.rs b/crates/raw/src/vendor/mef.rs index d5c5d83b41..d1c9a7754d 100644 --- a/crates/raw/src/vendor/mef.rs +++ b/crates/raw/src/vendor/mef.rs @@ -87,8 +87,12 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result { None => Rect::new(0, 0, w, h), }; // the white balance is measured from the samples, so the header probe reads them too + // the strip length is the tag value, so a short file must fail before the sample buffer is sized from it let chunks = info.chunks(bytes.len() as u64); - let src = chunks.first().and_then(|c| chunk_bytes(bytes, c)).ok_or_else(|| RawError::Corrupt("Mef strip outside file".into()))?; + let src = chunks + .first() + .and_then(|c| chunk_bytes(bytes, c).filter(|s| s.len() as u64 == c.len)) + .ok_or_else(|| RawError::Corrupt("Mef strip outside file".into()))?; // 12-bit MSB packing takes 3 bytes per 2 samples; a strip that cannot hold the declared // samples is a truncated file, not a picture — unpacking it would run past the real bytes // through every output sample (a short strip asks for the whole allocation and more). @@ -241,4 +245,35 @@ mod tests { } } } + + /// Rewrite the raw IFD's height, rows per strip and strip length in place (the strip data stays as written). + fn declare(f: &mut [u8], rows: u32, strip_len: u32) { + let tiff = Tiff::parse(f).unwrap(); + let raw = &tiff.ifds[0].sub_ifds[0]; + let spots: Vec<_> = [(t::IMAGE_LENGTH, rows), (t::ROWS_PER_STRIP, rows), (t::STRIP_BYTE_COUNTS, strip_len)] + .into_iter() + .map(|(tag, v)| { + let e = raw.entries.iter().find(|e| e.tag == tag).unwrap(); + (e.offset as usize, matches!(e.value, Value::Short(_)), v) + }) + .collect(); + for (at, short, v) in spots { + if short { + f[at..at + 2].copy_from_slice(&u16::try_from(v).unwrap().to_be_bytes()); + } else { + f[at..at + 4].copy_from_slice(&v.to_be_bytes()); + } + } + } + + #[test] + fn a_strip_longer_than_the_file_is_an_error() { + // #2119: the declared strip length matched the size, so a few bytes of data were decoded as a full frame + let px: Vec = (0..40).map(|i| (i * 13 % 4096) as u16).collect(); + let mut f = sample(20, 2, &px); + declare(&mut f, 2000, 30 * 2000); + assert_eq!(crate::probe(&f), Some(RawFormat::Mef)); + assert!(matches!(crate::probe_info(&f), Err(RawError::Corrupt(_)))); + assert!(matches!(crate::decode(&f), Err(RawError::Corrupt(_)))); + } } diff --git a/docs/whats-new.md b/docs/whats-new.md index 0c177ccf85..8bc511dc82 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -282,6 +282,13 @@ another ratio (4 × 5, 2 × 3…) keeps it portrait, as in Lightroom, and a Custom ratio taller than wide gives a portrait crop. +## 2026-10-17 + +### Short Epson and Mamiya raws are refused +- An Epson ERF or Mamiya MEF whose raw strip runs past the end of the file is now an error on import (issue #2119). + Before, the header probe trusted the declared strip length and sized its sample buffer from it, so a few KB of file + could make it allocate gigabytes. + ## 2026-10-16 ### Synchronize Folder shows that it is listing the folder