diff --git a/crates/raw/src/vendor.rs b/crates/raw/src/vendor.rs index b0f65f49e..ab3c8b7e9 100644 --- a/crates/raw/src/vendor.rs +++ b/crates/raw/src/vendor.rs @@ -225,6 +225,24 @@ fn solve4(mut a: [[f64; 4]; 4], mut b: [f64; 4]) -> Option<[f64; 4]> { x.iter().all(|v| v.is_finite()).then_some(x) } +/// Test helper: make the single-strip `StripByteCounts` entry (LONG, count 1) that holds `len` declare `declared` +/// bytes instead, as a file cut short or a hostile header does. +#[cfg(test)] +pub(crate) fn declare_strip_len(file: &mut [u8], len: u32, declared: u32) { + let big = file.starts_with(b"MM"); + let entry = |v: u32| -> Vec { + if big { + [&279u16.to_be_bytes()[..], &4u16.to_be_bytes(), &1u32.to_be_bytes(), &v.to_be_bytes()].concat() + } else { + [&279u16.to_le_bytes()[..], &4u16.to_le_bytes(), &1u32.to_le_bytes(), &v.to_le_bytes()].concat() + } + }; + let (old, new) = (entry(len), entry(declared)); + let at: Vec = file.windows(old.len()).enumerate().filter(|(_, w)| *w == old.as_slice()).map(|(i, _)| i).collect(); + assert_eq!(at.len(), 1, "one StripByteCounts entry of {len} bytes"); + file[at[0]..at[0] + new.len()].copy_from_slice(&new); +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/raw/src/vendor/arw.rs b/crates/raw/src/vendor/arw.rs index 3938fa50d..c40475b1b 100644 --- a/crates/raw/src/vendor/arw.rs +++ b/crates/raw/src/vendor/arw.rs @@ -642,6 +642,9 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result { } 32767 if one_byte_per_sample => { let src = chunk_bytes(bytes, &chunks[0]).ok_or_else(|| RawError::Corrupt("raw strip outside file".into()))?; + if src.len() as u64 != strip_len { + return Err(RawError::Corrupt("raw strip shorter than its rows".into())); + } let curve = code_curve(&raw.u64s(TONE_CURVE).unwrap_or_else(|| vec![8000, 10400, 12900, 14100])); let mut data = vec![0u16; samples as usize]; data.par_chunks_mut(w).enumerate().for_each(|(y, out)| { @@ -659,6 +662,9 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result { } 32767 if packed12 => { let src = chunk_bytes(bytes, &chunks[0]).ok_or_else(|| RawError::Corrupt("raw strip outside file".into()))?; + if src.len() as u64 != strip_len { + return Err(RawError::Corrupt("raw strip shorter than its rows".into())); + } let row_bytes = w / 2 * 3; let mut data = vec![0u16; samples as usize]; data.par_chunks_mut(w).enumerate().for_each(|(y, out)| { @@ -1561,4 +1567,28 @@ mod tests { assert!(is_quad_tiled(&file, &info)); assert_eq!(read_quad_tiles(&file, &info).unwrap(), mosaic); } + + /// #2561: a small file whose one-byte or packed 12-bit strip declares a large frame is refused before the full + /// decode allocates the frame. + #[test] + fn a_strip_cut_short_is_refused_before_allocating() { + use lightcraft_tiff::{IfdBuilder, ImageData, TiffWriter, Value}; + let (w, h) = (3000u32, 2000u32); + for declared in [w * h, w * h / 2 * 3] { + let mut raw = IfdBuilder::new(); + raw.set(t::MAKE, Value::Ascii("SONY".into())); + raw.set(t::MODEL, Value::Ascii("DSLR-A100".into())); + raw.set(t::IMAGE_WIDTH, Value::Long(vec![w])); + raw.set(t::IMAGE_LENGTH, Value::Long(vec![h])); + raw.set(t::BITS_PER_SAMPLE, Value::Short(vec![12])); + raw.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![1])); + raw.set(t::PHOTOMETRIC, Value::Short(vec![photometric::CFA])); + raw.set(t::COMPRESSION, Value::Short(vec![32767])); + raw.set(TONE_CURVE, Value::Short(vec![8000, 10400, 12900, 14100])); + raw.set_image(ImageData::Strips { rows_per_strip: h, strips: vec![vec![0u8; 12]] }); + let mut f = TiffWriter::default().write(&[raw]).unwrap(); + crate::vendor::declare_strip_len(&mut f, 12, declared); + assert!(matches!(decode(&f, Mode::Full), Err(RawError::Corrupt(_))), "{declared} bytes declared"); + } + } } diff --git a/crates/raw/src/vendor/srw.rs b/crates/raw/src/vendor/srw.rs index b22faeec0..17a2a1d70 100644 --- a/crates/raw/src/vendor/srw.rs +++ b/crates/raw/src/vendor/srw.rs @@ -107,6 +107,9 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result { }; let data = if mode == Mode::Full { let src = chunk_bytes(bytes, &chunks[0]).ok_or_else(|| RawError::Corrupt("SRW strip outside file".into()))?; + if src.len() as u64 != strip { + return Err(RawError::Corrupt("SRW strip shorter than its rows".into())); + } let mut d = vec![0u16; n]; match coding { Coding::Words => read_u16s(src, ByteOrder::Little, &mut d), @@ -540,4 +543,16 @@ mod tests { let _ = probe_info(&bytes[..cut]); } } + + /// #2561: a small file whose strip declares a large frame is refused before the full decode allocates the frame. + #[test] + fn a_strip_cut_short_is_refused_before_allocating() { + let (w, h) = (3000u32, 2000u32); + for (compression, declared) in [(32769, w * h * 2), (32770, w * h / 2 * 3)] { + let mut f = srw(Spec { w, h, compression, strip: vec![0u8; 12], pattern: Some([0, 1, 1, 2]), note: None, picture: None }); + crate::vendor::declare_strip_len(&mut f, 12, declared); + assert_eq!(probe(&f), Some(crate::RawFormat::Srw)); + assert!(matches!(decode(&f), Err(RawError::Corrupt(_))), "compression {compression}"); + } + } }