Merge pull request #53 from storytold/pr/never-crash-raw-tiff

Never crash: raw and tiff decoders
This commit is contained in:
Brandon Thomas authored and GitHub committed 2026-10-04 14:22:47 -04:00
commit 2e2f7ee5ac
12 files changed
+250 -16

No files matched your search

+4 -5
View File
@@ -185,9 +185,8 @@ pub fn write_dng(raw: &RawImage, opts: &DngWriteOptions) -> Result<Vec<u8>> {
}
}
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<Vec<u8>> {
.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<Vec<u8>> {
}
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(
+9
View File
@@ -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::<Option<_>>()?;
(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();
+5 -1
View File
@@ -359,7 +359,11 @@ pub fn decode(d: &[u8], max_samples: usize) -> Result<Frame, RawError> {
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::<Result<_, RawError>>()?;
let mut out = vec![0u16; total];
let mut br = BitReader::new(entropy);
let init = 1i32 << (h.precision - h.pt - 1);
+3 -2
View File
@@ -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<RawImage> {
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() {
+1 -1
View File
@@ -117,7 +117,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
}
}
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 {
+1 -1
View File
@@ -84,7 +84,7 @@ pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
.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);
+1 -1
View File
@@ -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<RawImage> {
+1 -1
View File
@@ -232,7 +232,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
};
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 @ [_, _, _, _]) => {
+1 -1
View File
@@ -91,7 +91,7 @@ fn raw_ifd(raw: &[u8]) -> Result<Ifd> {
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"),
}
}
+3 -3
View File
@@ -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<RawImage> {
+220
View File
@@ -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<u8> {
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<u8> = (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<u8> {
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<u8> {
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<u8> {
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<u8> = (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<u8> {
let img: Vec<u16> = (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<u8>, layout: Option<[u8; 36]>, jpeg: &[u8]) -> Vec<u8> {
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<u8> = 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<u8> {
let strip: Vec<u8> = (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<u8>> {
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::<usize>(), any::<u8>()), 1..16), cut in any::<usize>()) {
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]);
}
}
+1
View File
@@ -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;