From b42ca767772c796820b02322ba0f51d978725677 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 4 Oct 2026 12:00:26 -0400 Subject: [PATCH] Never crash: raw and tiff decoders free of panicking calls - ljpeg: a scan naming an undefined Huffman table is an error, not an expect. - ARW2/RW2 block reads use get() + try_from instead of slice + expect. - Vendor fallback CFAs use Cfa::bayer_static (literal names, tested) instead of Cfa::bayer(..).expect("static"). - dngwrite matches (data, compression) instead of a let-else unreachable!. - lightcraft-raw and lightcraft-tiff now #![deny] unwrap/expect/panic/ unimplemented/todo/unreachable; clippy.toml (same as the steering PR) keeps them allowed in tests. - tests/vendor_robust.rs: synthetic NEF, ARW, PEF, SRW, ORF, RW2, CR2 and RAF files, truncated at every length and randomly mutated, must decode to an error, never panic (no new crash found). Co-Authored-By: Claude Opus 5.5 --- clippy.toml | 4 + crates/raw/src/dngwrite.rs | 9 +- crates/raw/src/lib.rs | 9 ++ crates/raw/src/ljpeg.rs | 6 +- crates/raw/src/vendor/arw.rs | 5 +- crates/raw/src/vendor/cr2.rs | 2 +- crates/raw/src/vendor/nef.rs | 2 +- crates/raw/src/vendor/orf.rs | 2 +- crates/raw/src/vendor/pef.rs | 2 +- crates/raw/src/vendor/raf.rs | 2 +- crates/raw/src/vendor/rw2.rs | 6 +- crates/raw/tests/vendor_robust.rs | 220 ++++++++++++++++++++++++++++++ crates/tiff/src/lib.rs | 1 + 13 files changed, 254 insertions(+), 16 deletions(-) create mode 100644 clippy.toml create mode 100644 crates/raw/tests/vendor_robust.rs diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 000000000..4b6c79f55 --- /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/raw/src/dngwrite.rs b/crates/raw/src/dngwrite.rs index 10bb8ded5..ac8ce7990 100644 --- a/crates/raw/src/dngwrite.rs +++ b/crates/raw/src/dngwrite.rs @@ -185,9 +185,8 @@ pub fn write_dng(raw: &RawImage, opts: &DngWriteOptions) -> Result> { } } - match &raw.data { - RawData::F32(v) if matches!(opts.compression, DngCompression::Deflate { .. }) => { - let DngCompression::Deflate { tile, half } = opts.compression else { unreachable!() }; + match (&raw.data, opts.compression) { + (RawData::F32(v), DngCompression::Deflate { tile, half }) => { let tile = (tile.max(16) & !15) as usize; let bp = if half { 2 } else { 4 }; ifd.set(t::BITS_PER_SAMPLE, Value::Short(vec![(bp * 8) as u16; cpp])); @@ -222,7 +221,7 @@ pub fn write_dng(raw: &RawImage, opts: &DngWriteOptions) -> Result> { .collect(); ifd.set_image(ImageData::Tiles { tile_width: tile as u32, tile_height: tile as u32, tiles }); } - RawData::F32(v) => { + (RawData::F32(v), _) => { ifd.set(t::BITS_PER_SAMPLE, Value::Short(vec![32; cpp])); ifd.set(t::SAMPLE_FORMAT, Value::Short(vec![3; cpp])); ifd.set(t::COMPRESSION, Value::Short(vec![compression::NONE])); @@ -234,7 +233,7 @@ pub fn write_dng(raw: &RawImage, opts: &DngWriteOptions) -> Result> { } ifd.set_image(ImageData::Strips { rows_per_strip: h as u32, strips: vec![bytes] }); } - RawData::U16(v) => { + (RawData::U16(v), _) => { let maxv = v.iter().copied().max().unwrap_or(0); let bits = (16 - maxv.leading_zeros()).max(raw.bits.min(16)).max(2) as u8; ifd.set( diff --git a/crates/raw/src/lib.rs b/crates/raw/src/lib.rs index be7e0fdc3..554bc1f01 100644 --- a/crates/raw/src/lib.rs +++ b/crates/raw/src/lib.rs @@ -18,6 +18,7 @@ //! (public specifications, tag-name documentation, black-box analysis of CC0 samples) and gaps. Non-DNG files carry no //! colour matrix: [`color`] falls back to a documented neutral model. The decoders never panic on malformed input. #![forbid(unsafe_code)] +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] mod binned; pub mod color; @@ -266,6 +267,11 @@ impl Cfa { .collect::>()?; (p.len() == 4).then_some(Cfa { width: 2, height: 2, pattern: p }) } + /// A 2×2 Bayer layout named in code (`"RGGB"`, `"BGGR"`, `"GRBG"`, `"GBRG"`). Only for literal + /// names (tested below); anything else falls back to RGGB instead of failing. + pub(crate) fn bayer_static(s: &'static str) -> Cfa { + Cfa::bayer(s).unwrap_or(Cfa { width: 2, height: 2, pattern: vec![0, 1, 1, 2] }) + } /// The Fujifilm X-Trans 6×6 layout (as commonly documented), anchored at (0, 0). pub fn xtrans() -> Cfa { let rows = ["GGRGGB", "GGBGGR", "BRGRBG", "GGBGGR", "GGRGGB", "RBGBRG"]; @@ -575,6 +581,9 @@ mod tests { assert_eq!(c.shifted(1, 0).name(), "GRBG"); assert_eq!(c.shifted(1, 1).name(), "BGGR"); assert!(Cfa::bayer("RGGX").is_none()); + for name in ["RGGB", "BGGR", "GRBG", "GBRG"] { + assert_eq!(Cfa::bayer_static(name).name(), name); + } let x = Cfa::xtrans(); assert!(!x.is_bayer()); let greens = x.pattern.iter().filter(|&&p| p == 1).count(); diff --git a/crates/raw/src/ljpeg.rs b/crates/raw/src/ljpeg.rs index 000a23640..cb2d04675 100644 --- a/crates/raw/src/ljpeg.rs +++ b/crates/raw/src/ljpeg.rs @@ -359,7 +359,11 @@ pub fn decode(d: &[u8], max_samples: usize) -> Result { if (entropy.len() as u64 + 64) * 8 < total as u64 { return Err(err("entropy data too short for frame")); } - let tables: Vec<&Huffman> = h.table_for.iter().map(|&t| h.tables[t].as_ref().expect("checked")).collect(); + let tables: Vec<&Huffman> = h + .table_for + .iter() + .map(|&t| h.tables.get(t).and_then(Option::as_ref).ok_or_else(|| err("scan uses an undefined Huffman table"))) + .collect::>()?; let mut out = vec![0u16; total]; let mut br = BitReader::new(entropy); let init = 1i32 << (h.precision - h.pt - 1); diff --git a/crates/raw/src/vendor/arw.rs b/crates/raw/src/vendor/arw.rs index 2cd509abe..a9eb2c7d2 100644 --- a/crates/raw/src/vendor/arw.rs +++ b/crates/raw/src/vendor/arw.rs @@ -60,7 +60,8 @@ pub(crate) fn decode_row(row: &[u8], out: &mut [u16]) { while x0 + 32 <= w && x0 + 32 <= row.len() { for half in 0..2 { let off = x0 + half * 16; - let block = u128::from_le_bytes(row[off..off + 16].try_into().expect("16 bytes")); + let Some(bytes) = row.get(off..off + 16).and_then(|b| <[u8; 16]>::try_from(b).ok()) else { return }; + let block = u128::from_le_bytes(bytes); let max = (block & 0x7ff) as u16; let min = ((block >> 11) & 0x7ff) as u16; let imax = ((block >> 22) & 0xf) as usize; @@ -217,7 +218,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result { let cfa = match (raw.u64s(t::CFA_REPEAT_PATTERN_DIM).as_deref(), raw.bytes(t::CFA_PATTERN_EP)) { (Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => Cfa { width: 2, height: 2, pattern: p.to_vec() }, - _ => Cfa::bayer("RGGB").expect("static"), + _ => Cfa::bayer_static("RGGB"), }; let default_black = if out_bits >= 14 { 512.0 } else { 128.0 }; let black = match raw.f64s(BLACK_LEVEL).as_deref() { diff --git a/crates/raw/src/vendor/cr2.rs b/crates/raw/src/vendor/cr2.rs index 8629f75cf..fa15456a0 100644 --- a/crates/raw/src/vendor/cr2.rs +++ b/crates/raw/src/vendor/cr2.rs @@ -117,7 +117,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result { } } let wb = mn.as_ref().and_then(|m| m.ifd.u64s(COLOR_BALANCE)).and_then(|v| wb_from_color_balance(&v)); - let cfa = Cfa::bayer("RGGB").expect("static"); + let cfa = Cfa::bayer_static("RGGB"); let black = if active.x >= 8 { black_from_columns(&data, width, 2..active.x - 2, active.y..active.y + active.height, active) } else { diff --git a/crates/raw/src/vendor/nef.rs b/crates/raw/src/vendor/nef.rs index 11700bd1b..fb4b644a8 100644 --- a/crates/raw/src/vendor/nef.rs +++ b/crates/raw/src/vendor/nef.rs @@ -84,7 +84,7 @@ pub(crate) fn decode(bytes: &[u8]) -> Result { .map(|v| [v[0] as f32, 1.0, v[1] as f32]); let cfa = match (raw.u64s(t::CFA_REPEAT_PATTERN_DIM).as_deref(), raw.bytes(t::CFA_PATTERN_EP)) { (Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => Cfa { width: 2, height: 2, pattern: p.to_vec() }, - _ => Cfa::bayer("RGGB").expect("static"), + _ => Cfa::bayer_static("RGGB"), }; let white = white_from_data(samples, bits); let active_w = trailing_masked_columns(samples, w, h, white); diff --git a/crates/raw/src/vendor/orf.rs b/crates/raw/src/vendor/orf.rs index 2e2eebb38..52f597d2c 100644 --- a/crates/raw/src/vendor/orf.rs +++ b/crates/raw/src/vendor/orf.rs @@ -79,7 +79,7 @@ pub(crate) fn cfa_from_data(d: &[u16], w: usize, a: Rect) -> Cfa { anti += (p(1, 0) - p(0, 1)).unsigned_abs(); } } - Cfa::bayer(if main < anti { "GRBG" } else { "RGGB" }).expect("static") + Cfa::bayer_static(if main < anti { "GRBG" } else { "RGGB" }) } pub(crate) fn decode(bytes: &[u8]) -> Result { diff --git a/crates/raw/src/vendor/pef.rs b/crates/raw/src/vendor/pef.rs index a6fb8c579..78cfa87a7 100644 --- a/crates/raw/src/vendor/pef.rs +++ b/crates/raw/src/vendor/pef.rs @@ -232,7 +232,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result { }; let cfa = match (ifd0.u64s(t::CFA_REPEAT_PATTERN_DIM).as_deref(), ifd0.bytes(t::CFA_PATTERN_EP)) { (Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => Cfa { width: 2, height: 2, pattern: p.to_vec() }, - _ => Cfa::bayer("BGGR").expect("static"), + _ => Cfa::bayer_static("BGGR"), }; let black = match mn.as_ref().and_then(|m| m.ifd.f64s(BLACK_POINT)).as_deref() { Some(v @ [_, _, _, _]) => { diff --git a/crates/raw/src/vendor/raf.rs b/crates/raw/src/vendor/raf.rs index e8ee126e7..05b09f499 100644 --- a/crates/raw/src/vendor/raf.rs +++ b/crates/raw/src/vendor/raf.rs @@ -91,7 +91,7 @@ fn raw_ifd(raw: &[u8]) -> Result { fn cfa(h: &Header) -> Cfa { match record(h, XTRANS_LAYOUT) { Some(l) if l.len() == 36 && l.iter().all(|&c| c <= 2) => Cfa { width: 6, height: 6, pattern: l.iter().rev().copied().collect() }, - _ => Cfa::bayer("RGGB").expect("static"), + _ => Cfa::bayer_static("RGGB"), } } diff --git a/crates/raw/src/vendor/rw2.rs b/crates/raw/src/vendor/rw2.rs index d996d5b21..efd515f2d 100644 --- a/crates/raw/src/vendor/rw2.rs +++ b/crates/raw/src/vendor/rw2.rs @@ -74,11 +74,11 @@ pub(crate) fn unpack(stream: &[u8], bits: u32, out: &mut [u16]) -> Result<()> { let first_block = ci * 1024; for (bi, px) in o.chunks_mut(per).enumerate() { let at = (first_block + bi) * 16; - let Some(b) = stream.get(at..at + 16) else { + let Some(b) = stream.get(at..at + 16).and_then(|b| <[u8; 16]>::try_from(b).ok()) else { px.fill(0); continue; }; - let word = u128::from_le_bytes(b.try_into().expect("16 bytes")); + let word = u128::from_le_bytes(b); for (i, p) in px.iter_mut().enumerate() { *p = ((word >> (start + i as u32 * bits)) & mask) as u16; } @@ -94,7 +94,7 @@ fn cfa(code: u16) -> Cfa { 4 => "BGGR", _ => "RGGB", }; - Cfa::bayer(s).expect("static") + Cfa::bayer_static(s) } pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result { diff --git a/crates/raw/tests/vendor_robust.rs b/crates/raw/tests/vendor_robust.rs new file mode 100644 index 000000000..2db97c64f --- /dev/null +++ b/crates/raw/tests/vendor_robust.rs @@ -0,0 +1,220 @@ +//! Malformed vendor raw files (NEF, ARW, PEF, ORF, RW2, CR2, RAF) must decode to an error, never panic. + +use lightcraft_tiff::tags as t; +use lightcraft_tiff::{ByteOrder, IfdBuilder, ImageData, TiffWriter, Value}; +use proptest::prelude::*; + +const W: u32 = 32; +const H: u32 = 8; + +/// A CFA TIFF the way most vendors lay it out: IFD0 (make) + a SubIFD holding the raw strip. +fn cfa_tiff(make: &str, compression: u16, bits: u16, order: ByteOrder) -> Vec { + let px = (W * H) as usize; + // Sony's ARW2 (32767) packs a sample per byte + let len = if compression == 32767 { px } else { px * 2 }; + let strip: Vec = (0..len).map(|i| (i * 37 % 251) as u8).collect(); + let mut raw = IfdBuilder::new(); + raw.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![0])); + 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![bits])); + raw.set(t::COMPRESSION, Value::Short(vec![compression])); + raw.set(t::PHOTOMETRIC, Value::Short(vec![t::photometric::CFA])); + raw.set(t::CFA_REPEAT_PATTERN_DIM, Value::Short(vec![2, 2])); + raw.set(t::CFA_PATTERN_EP, Value::Byte(vec![0, 1, 1, 2])); + raw.set_image(ImageData::Strips { rows_per_strip: H, strips: vec![strip] }); + let mut ifd0 = IfdBuilder::new(); + ifd0.set(t::MAKE, Value::Ascii(make.into())); + ifd0.set(t::MODEL, Value::Ascii("TEST".into())); + ifd0.add_sub_ifd(raw); + TiffWriter::new(order, false).write(&[ifd0]).unwrap() +} + +/// An Olympus ORF (TIFF with an `IIRO` magic) or Panasonic RW2 (`IIU\0`), raw image in IFD0. +fn magic_tiff(magic: &[u8; 4], make: &str) -> Vec { + let px = (W * H) as usize; + let mut ifd = IfdBuilder::new(); + ifd.set(t::IMAGE_WIDTH, Value::Long(vec![W])); + ifd.set(t::IMAGE_LENGTH, Value::Long(vec![H])); + ifd.set(t::BITS_PER_SAMPLE, Value::Short(vec![16])); + ifd.set(t::COMPRESSION, Value::Short(vec![1])); + ifd.set(t::PHOTOMETRIC, Value::Short(vec![1])); + ifd.set(t::MAKE, Value::Ascii(make.into())); + ifd.set_image(ImageData::Strips { rows_per_strip: H, strips: vec![(0..px * 2).map(|i| (i * 13) as u8).collect()] }); + let mut b = TiffWriter::new(ByteOrder::Little, false).write(&[ifd]).unwrap(); + b[..4].copy_from_slice(magic); + b +} + +/// A Panasonic RW2: sensor size, bit depth and raw format in IFD0, 12-bit packed blocks. +fn rw2() -> Vec { + let mut ifd = IfdBuilder::new(); + ifd.set(0x0002, Value::Short(vec![W as u16])); + ifd.set(0x0003, Value::Short(vec![H as u16])); + ifd.set(0x0004, Value::Short(vec![1])); + ifd.set(0x0005, Value::Short(vec![2])); + ifd.set(0x0006, Value::Short(vec![H as u16 - 1])); + ifd.set(0x0007, Value::Short(vec![W as u16 - 2])); + ifd.set(0x000a, Value::Short(vec![12])); + ifd.set(0x002d, Value::Short(vec![5])); + ifd.set(0x001c, Value::Short(vec![64])); + ifd.set(0x001d, Value::Short(vec![64])); + ifd.set(0x001e, Value::Short(vec![64])); + ifd.set(t::MAKE, Value::Ascii("Panasonic".into())); + let need = (W * H).div_ceil(10) as usize * 16; + ifd.set_image(ImageData::Strips { rows_per_strip: H, strips: vec![(0..need).map(|i| (i * 29) as u8).collect()] }); + let mut b = TiffWriter::new(ByteOrder::Little, false).write(&[ifd]).unwrap(); + b[..4].copy_from_slice(b"IIU\0"); + b +} + +/// A Pentax PEF: the raw image in IFD0, wide enough for the dark-border trim. +fn pef(compression: u16) -> Vec { + let (w, h) = (264u32, 4u32); + let mut ifd = IfdBuilder::new(); + ifd.set(t::MAKE, Value::Ascii("PENTAX".into())); + ifd.set(t::IMAGE_WIDTH, Value::Long(vec![w])); + ifd.set(t::IMAGE_LENGTH, Value::Long(vec![h])); + ifd.set(t::BITS_PER_SAMPLE, Value::Short(vec![16])); + ifd.set(t::COMPRESSION, Value::Short(vec![compression])); + ifd.set(t::PHOTOMETRIC, Value::Short(vec![t::photometric::CFA])); + let strip: Vec = (0..(w * h * 2) as usize).map(|i| if i % 528 < 8 { 0 } else { (i * 7 % 200) as u8 }).collect(); + ifd.set_image(ImageData::Strips { rows_per_strip: h, strips: vec![strip] }); + TiffWriter::new(ByteOrder::Big, false).write(&[ifd]).unwrap() +} + +/// A Canon CR2: three placeholder IFDs and IFD3 with a 2-component lossless JPEG frame. +fn cr2() -> Vec { + let img: Vec = (0..(W * H) as usize).map(|i| 1024 + (i * 7919 % 12000) as u16).collect(); + let enc = lightcraft_raw::ljpeg::encode(&img, W as usize / 2, H as usize, 2, 14, 1, 0); + let mut ifd0 = IfdBuilder::new(); + ifd0.set(t::MAKE, Value::Ascii("Canon".into())); + let mut ifd3 = IfdBuilder::new(); + ifd3.set(t::COMPRESSION, Value::Short(vec![6])); + ifd3.set(0xc640, Value::Short(vec![1, 16, 16])); + ifd3.set_image(ImageData::Strips { rows_per_strip: H, strips: vec![enc] }); + let blank = || IfdBuilder::new().with(1, Value::Short(vec![0])); + TiffWriter::new(ByteOrder::Little, false).write(&[ifd0, blank(), blank(), ifd3]).unwrap() +} + +/// A Fujifilm RAF: header, records, raw block (LE TIFF whose IFD0 points to a 0xf000 raw IFD). +fn raf_file(w: u32, h: u32, bits: u32, strip: Vec, layout: Option<[u8; 36]>, jpeg: &[u8]) -> Vec { + let mut block = b"II*\0\x08\0\0\0".to_vec(); + let sub_off: u32 = 8 + 2 + 12 + 4; + block.extend_from_slice(&1u16.to_le_bytes()); + block.extend_from_slice(&[0x00, 0xf0, 13, 0, 1, 0, 0, 0]); + block.extend_from_slice(&sub_off.to_le_bytes()); + block.extend_from_slice(&0u32.to_le_bytes()); + let n = 7u32; + let wb_off = sub_off + 2 + 12 * n + 4; + let strip_off = wb_off + 12; + let entries: [(u16, u32, u32); 7] = [ + (0xf001, 1, w), + (0xf002, 1, h), + (0xf003, 1, bits), + (0xf007, 1, strip_off), + (0xf008, 1, strip.len() as u32), + (0xf00a, 1, 64), + (0xf00e, 3, wb_off), + ]; + block.extend_from_slice(&(n as u16).to_le_bytes()); + for (tag, count, v) in entries { + block.extend_from_slice(&tag.to_le_bytes()); + block.extend_from_slice(&4u16.to_le_bytes()); + block.extend_from_slice(&count.to_le_bytes()); + block.extend_from_slice(&v.to_le_bytes()); + } + block.extend_from_slice(&0u32.to_le_bytes()); + for v in [300u32, 600, 450] { + block.extend_from_slice(&v.to_le_bytes()); + } + block.extend_from_slice(&strip); + let mut recs: Vec = Vec::new(); + let mut nrec = 0u32; + let mut add = |tag: u16, d: &[u8]| { + recs.extend_from_slice(&tag.to_be_bytes()); + recs.extend_from_slice(&(d.len() as u16).to_be_bytes()); + recs.extend_from_slice(d); + nrec += 1; + }; + add(0x0100, &[(h >> 8) as u8, h as u8, (w >> 8) as u8, w as u8]); + add(0x0110, &[0, 2, 0, 4]); + add(0x0111, &[0, (h - 2) as u8, 0, (w - 4) as u8]); + if let Some(l) = layout { + add(0x0131, &l); + } + let mut dir = nrec.to_be_bytes().to_vec(); + dir.extend_from_slice(&recs); + let mut out = b"FUJIFILMCCD-RAW 0201FF000000TEST".to_vec(); + out.resize(108, 0); + let jpeg_off = 160u32; + let dir_off = jpeg_off + jpeg.len() as u32; + let raw_off = dir_off + dir.len() as u32; + for (i, v) in [jpeg_off, jpeg.len() as u32, dir_off, dir.len() as u32, raw_off, block.len() as u32].iter().enumerate() { + out[84 + i * 4..88 + i * 4].copy_from_slice(&v.to_be_bytes()); + } + out.resize(160, 0); + out.extend_from_slice(jpeg); + out.extend_from_slice(&dir); + out.extend_from_slice(&block); + out +} + +fn raf() -> Vec { + let strip: Vec = (0..(W * H * 2) as usize).map(|i| (i * 11) as u8).collect(); + let layout: [u8; 36] = std::array::from_fn(|i| (i % 3) as u8); + raf_file(W, H, 16, strip, Some(layout), b"\xff\xd8\xff\xd9") +} + +fn samples() -> Vec> { + vec![ + cfa_tiff("NIKON CORPORATION", 1, 12, ByteOrder::Big), + cfa_tiff("NIKON CORPORATION", 34713, 14, ByteOrder::Big), + cfa_tiff("SONY", 32767, 12, ByteOrder::Little), + cfa_tiff("SONY", 1, 14, ByteOrder::Little), + pef(1), + pef(65535), + cfa_tiff("SAMSUNG", 1, 12, ByteOrder::Little), + cfa_tiff("ACME", 1, 16, ByteOrder::Little), + magic_tiff(b"IIRO", "OLYMPUS IMAGING CORP."), + rw2(), + cr2(), + raf(), + ] +} + +fn exercise(bytes: &[u8]) { + let _ = lightcraft_raw::probe(bytes); + let _ = lightcraft_raw::probe_info(bytes); + let _ = lightcraft_raw::embedded_preview(bytes); + if let Ok(img) = lightcraft_raw::decode(bytes) + && img.width * img.height <= 1 << 16 + { + let _ = img.develop(lightcraft_raw::Method::Ahd); + } +} + +#[test] +fn vendor_samples_truncated_at_every_length() { + for s in samples() { + exercise(&s); + for n in (0..s.len()).step_by(3) { + exercise(&s[..n]); + } + } +} + +proptest! { + #![proptest_config(ProptestConfig { cases: 2000, .. ProptestConfig::default() })] + + #[test] + fn mutated_vendor_files_never_panic(kind in 0usize..12, flips in proptest::collection::vec((any::(), any::()), 1..16), cut in any::()) { + let mut data = samples().swap_remove(kind); + let n = data.len(); + for (i, v) in flips { + data[i % n] = v; + } + let keep = if cut % 3 == 0 { cut % n } else { n }; + exercise(&data[..keep]); + } +} diff --git a/crates/tiff/src/lib.rs b/crates/tiff/src/lib.rs index f7637fb7b..bafa3eb5e 100644 --- a/crates/tiff/src/lib.rs +++ b/crates/tiff/src/lib.rs @@ -17,6 +17,7 @@ //! Sources: TIFF 6.0 specification (Adobe, 1992), TIFF Technical Note / supplements, the BigTIFF //! design notes (AWare Systems), Adobe DNG specification 1.7 (tag numbers), CIPA DC-008 (Exif 2.32). #![forbid(unsafe_code)] +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] pub mod image; pub mod makernote;