use lightcraft_tiff::image::{Layout, chunk_bytes}; use lightcraft_tiff::writer::{rational, srational}; use lightcraft_tiff::{ByteOrder, FieldType, IfdBuilder, ImageData, ParseOptions, Tiff, TiffError, TiffWriter, Value, parse_ifd_at, tags}; use proptest::prelude::*; const ORDERS: [ByteOrder; 2] = [ByteOrder::Little, ByteOrder::Big]; fn all_values() -> Vec<(u16, Value)> { vec![ (1000, Value::Byte(vec![1, 2, 3, 255])), (1001, Value::Ascii("hello tiff".into())), (1002, Value::Short(vec![1, 65535, 3])), (1003, Value::Long(vec![7])), (1004, Value::Rational(vec![(1, 3), (22, 7)])), (1005, Value::SByte(vec![-1, 0, 127, -128])), (1006, Value::Undefined(vec![9; 17])), (1007, Value::SShort(vec![-32768, 32767])), (1008, Value::SLong(vec![-5, i32::MAX])), (1009, Value::SRational(vec![(-1, 3), (5, -2)])), (1010, Value::Float(vec![1.5, -0.25, f32::MAX])), (1011, Value::Double(vec![std::f64::consts::PI])), (1012, Value::Ifd(vec![0])), (1013, Value::Short(vec![42])), (1014, Value::Ascii(String::new())), ] } fn big_values() -> Vec<(u16, Value)> { vec![(1020, Value::Long8(vec![1 << 40, 3])), (1021, Value::SLong8(vec![-(1 << 50)])), (1022, Value::Ifd8(vec![0]))] } #[test] fn all_field_types_roundtrip_every_variant() { for order in ORDERS { for big in [false, true] { let mut ifd = IfdBuilder::new(); for (t, v) in all_values() { ifd.set(t, v); } if big { for (t, v) in big_values() { ifd.set(t, v); } } let bytes = TiffWriter::new(order, big).write(&[ifd]).unwrap(); let t = Tiff::parse(&bytes).unwrap(); assert_eq!(t.order, order); assert_eq!(t.bigtiff, big); assert_eq!(Tiff::sniff(&bytes), Some((order, big))); let ifd0 = &t.ifds[0]; for (tag, v) in all_values().into_iter().chain(if big { big_values() } else { vec![] }) { assert_eq!(ifd0.value(tag), Some(&v), "tag {tag} order {order:?} big {big}"); } } } } #[test] fn accessors() { let mut ifd = IfdBuilder::new(); ifd.set(1, Value::Rational(vec![(28, 10), (0, 0)])); ifd.set(2, Value::Short(vec![5, 6])); ifd.set(3, Value::SLong(vec![-3])); ifd.set(4, Value::Ascii("Canon ".into())); ifd.set(5, Value::Byte(b"\0junk".to_vec())); let bytes = TiffWriter::default().write(&[ifd]).unwrap(); let t = Tiff::parse(&bytes).unwrap(); let i = &t.ifds[0]; assert_eq!(i.f64(1), Some(2.8)); assert_eq!(i.f64s(1).unwrap(), vec![2.8, 0.0]); assert_eq!(i.u32(2), Some(5)); assert_eq!(i.u64s(2).unwrap(), vec![5, 6]); assert_eq!(i.u32(3), None); assert_eq!(i.i64(3), Some(-3)); assert_eq!(i.string(4).as_deref(), Some("Canon")); assert_eq!(i.string(5).as_deref(), Some("")); assert_eq!(i.get(4).unwrap().field_type(), FieldType::Ascii); assert!(i.u32(999).is_none()); assert_eq!(t.find(2).map(|e| e.tag), Some(2)); } #[test] fn chain_subifds_and_pointer_ifds() { for order in ORDERS { for big in [false, true] { let mut interop = IfdBuilder::new(); interop.set(1, Value::Ascii("R98".into())); let mut exif = IfdBuilder::new(); exif.set(tags::F_NUMBER, Value::Rational(vec![(56, 10)])); exif.set_child(tags::INTEROP_IFD, interop); let mut gps = IfdBuilder::new(); gps.set(tags::GPS_LATITUDE_REF, Value::Ascii("N".into())); let mut ifd0 = IfdBuilder::new(); ifd0.set(tags::MAKE, Value::Ascii("Maker".into())); ifd0.set_child(tags::EXIF_IFD, exif); ifd0.set_child(tags::GPS_IFD, gps); let mut raw = IfdBuilder::new(); raw.set(tags::IMAGE_WIDTH, Value::Long(vec![4])); let mut nested = IfdBuilder::new(); nested.set(tags::IMAGE_WIDTH, Value::Long(vec![2])); raw.add_sub_ifd(nested); ifd0.add_sub_ifd(raw); ifd0.add_sub_ifd(IfdBuilder::new().with(tags::IMAGE_WIDTH, Value::Short(vec![3]))); let ifd1 = IfdBuilder::new().with(tags::IMAGE_WIDTH, Value::Short(vec![160])); let ifd2 = IfdBuilder::new().with(tags::IMAGE_WIDTH, Value::Short(vec![80])); let bytes = TiffWriter::new(order, big).write(&[ifd0, ifd1, ifd2]).unwrap(); let t = Tiff::parse(&bytes).unwrap(); assert_eq!(t.ifds.len(), 3); assert_eq!(t.ifds[1].u32(tags::IMAGE_WIDTH), Some(160)); assert_eq!(t.ifds[2].u32(tags::IMAGE_WIDTH), Some(80)); let i0 = &t.ifds[0]; assert_eq!(i0.sub_ifds.len(), 2); assert_eq!(i0.sub_ifds[0].u32(tags::IMAGE_WIDTH), Some(4)); assert_eq!(i0.sub_ifds[0].sub_ifds[0].u32(tags::IMAGE_WIDTH), Some(2)); assert_eq!(i0.sub_ifds[1].u32(tags::IMAGE_WIDTH), Some(3)); let exif = t.exif().unwrap(); assert_eq!(exif.f64(tags::F_NUMBER), Some(5.6)); assert_eq!(exif.interop.as_ref().unwrap().string(1).as_deref(), Some("R98")); assert_eq!(t.gps().unwrap().string(tags::GPS_LATITUDE_REF).as_deref(), Some("N")); // IFD0, sub0, sub0.sub, sub1, exif, interop, gps, ifd1, ifd2 assert_eq!(t.all_ifds().len(), 9); } } } fn gradient(w: usize, h: usize) -> Vec { (0..w * h).map(|i| (i * 7 % 251) as u8).collect() } #[test] fn strips_layout_and_bytes() { let (w, h) = (10usize, 7usize); let px = gradient(w, h); let rps = 3; let strips: Vec> = px.chunks(w * rps).map(|c| c.to_vec()).collect(); let mut ifd = IfdBuilder::new(); ifd.set(tags::IMAGE_WIDTH, Value::Long(vec![w as u32])); ifd.set(tags::IMAGE_LENGTH, Value::Long(vec![h as u32])); ifd.set(tags::BITS_PER_SAMPLE, Value::Short(vec![8])); ifd.set(tags::COMPRESSION, Value::Short(vec![1])); ifd.set_image(ImageData::Strips { rows_per_strip: rps as u32, strips }); for order in ORDERS { for big in [false, true] { let bytes = TiffWriter::new(order, big).write(std::slice::from_ref(&ifd)).unwrap(); let t = Tiff::parse(&bytes).unwrap(); let info = t.ifds[0].image().unwrap(); assert_eq!(info.layout, Layout::Strips { rows_per_strip: 3 }); assert_eq!(info.grid(), (1, 3)); let chunks = info.chunks(bytes.len() as u64); assert_eq!(chunks.len(), 3); assert_eq!(chunks[2].height, 1); let mut out = Vec::new(); for c in &chunks { out.extend_from_slice(chunk_bytes(&bytes, c).unwrap()); } assert_eq!(out, px); } } } #[test] fn tiles_layout_and_bytes() { let (w, h, tw, th) = (20usize, 13usize, 16usize, 8usize); let px = gradient(w, h); let (ta, td) = (w.div_ceil(tw), h.div_ceil(th)); let mut tiles = Vec::new(); for ty in 0..td { for tx in 0..ta { let mut t = vec![0u8; tw * th]; for y in 0..th { for x in 0..tw { let (sx, sy) = (tx * tw + x, ty * th + y); if sx < w && sy < h { t[y * tw + x] = px[sy * w + sx]; } } } tiles.push(t); } } let mut ifd = IfdBuilder::new(); ifd.set(tags::IMAGE_WIDTH, Value::Short(vec![w as u16])); ifd.set(tags::IMAGE_LENGTH, Value::Short(vec![h as u16])); ifd.set_image(ImageData::Tiles { tile_width: tw as u32, tile_height: th as u32, tiles }); let bytes = TiffWriter::new(ByteOrder::Big, false).write(&[ifd]).unwrap(); let t = Tiff::parse(&bytes).unwrap(); let info = t.ifds[0].image().unwrap(); assert_eq!(info.grid(), (2, 2)); let chunks = info.chunks(bytes.len() as u64); assert_eq!(chunks.len(), 4); let mut out = vec![0u8; w * h]; for c in &chunks { let b = chunk_bytes(&bytes, c).unwrap(); for y in 0..c.height as usize { for x in 0..c.width as usize { let (sx, sy) = (c.x as usize + x, c.y as usize + y); if sx < w && sy < h { out[sy * w + sx] = b[y * tw + x]; } } } } assert_eq!(out, px); } #[test] fn missing_byte_counts_are_inferred() { let mut ifd = IfdBuilder::new(); ifd.set(tags::IMAGE_WIDTH, Value::Long(vec![4])); ifd.set(tags::IMAGE_LENGTH, Value::Long(vec![2])); ifd.set_image(ImageData::Strips { rows_per_strip: 1, strips: vec![vec![1; 4], vec![2; 4]] }); let bytes = TiffWriter::default().write(&[ifd]).unwrap(); let t = Tiff::parse(&bytes).unwrap(); let mut info = t.ifds[0].image().unwrap(); info.byte_counts.clear(); let c = info.chunks(bytes.len() as u64); assert_eq!(c[0].len, 4); assert_eq!(&chunk_bytes(&bytes, &c[0]).unwrap()[..4], &[1; 4]); assert!(c[1].len >= 4); } #[test] fn bad_headers() { assert_eq!(Tiff::parse(b""), Err(TiffError::NotTiff)); assert_eq!(Tiff::parse(b"XX*\0\0\0\0\0"), Err(TiffError::NotTiff)); assert_eq!(Tiff::parse(b"II\x2b\0\x04\0\0\0"), Err(TiffError::NotTiff)); assert!(Tiff::parse(b"II*\0").is_err()); assert!(Tiff::parse(b"II*\0\x08\0\0\0").is_err()); assert!(Tiff::parse(b"II*\0\x08\0\0\0\0\0").is_err()); assert!(Tiff::sniff(b"MM\0*\0\0\0\x08").is_some()); } #[test] fn ifd_loops_are_detected() { // IFD0 at 8 whose next pointer points back to itself, and whose SubIFDs point to itself too. let mut b = b"II*\0\x08\0\0\0".to_vec(); b.extend_from_slice(&1u16.to_le_bytes()); b.extend_from_slice(&330u16.to_le_bytes()); b.extend_from_slice(&4u16.to_le_bytes()); b.extend_from_slice(&1u32.to_le_bytes()); b.extend_from_slice(&8u32.to_le_bytes()); b.extend_from_slice(&8u32.to_le_bytes()); let t = Tiff::parse(&b).unwrap(); assert_eq!(t.ifds.len(), 1); assert!(t.ifds[0].sub_ifds.is_empty()); } #[test] fn out_of_range_entries_are_skipped() { let mut ifd = IfdBuilder::new(); ifd.set(1, Value::Long(vec![1, 2, 3])); ifd.set(2, Value::Short(vec![5])); let mut bytes = TiffWriter::default().write(&[ifd]).unwrap(); let t = Tiff::parse(&bytes).unwrap(); let e = t.ifds[0].get(1).unwrap().clone(); // point tag 1's value offset past EOF let ifd_off = t.ifds[0].offset as usize; let field = ifd_off + 2 + 8; assert_eq!(u32::from_le_bytes(bytes[field..field + 4].try_into().unwrap()) as u64, e.offset); bytes[field..field + 4].copy_from_slice(&0xffff_fff0u32.to_le_bytes()); let t = Tiff::parse(&bytes).unwrap(); assert!(t.ifds[0].get(1).is_none()); assert_eq!(t.ifds[0].u32(2), Some(5)); } #[test] fn value_budget_limits_allocation() { // 50 entries all pointing at the same 1000-byte blob. let mut ifd = IfdBuilder::new(); ifd.set(1, Value::Undefined(vec![7; 1000])); let bytes = TiffWriter::default().write(&[ifd]).unwrap(); let t = Tiff::parse(&bytes).unwrap(); let blob = t.ifds[0].get(1).unwrap().offset as u32; let mut b = b"II*\0".to_vec(); b.extend_from_slice(&(bytes.len() as u32).to_le_bytes()); let mut file = bytes.clone(); file[..8].copy_from_slice(&b); file.extend_from_slice(&50u16.to_le_bytes()); for i in 0..50u16 { file.extend_from_slice(&i.to_le_bytes()); file.extend_from_slice(&7u16.to_le_bytes()); file.extend_from_slice(&1000u32.to_le_bytes()); file.extend_from_slice(&blob.to_le_bytes()); } file.extend_from_slice(&0u32.to_le_bytes()); let opts = ParseOptions { value_budget: Some(10_000), ..Default::default() }; let t = Tiff::parse_with(&file, &opts).unwrap(); assert_eq!(t.ifds[0].entries.len(), 10); let t = Tiff::parse(&file).unwrap(); assert_eq!(t.ifds[0].entries.len(), 50); } #[test] fn parse_ifd_with_base() { // an IFD blob embedded at 100 whose offsets are relative to 100 let mut blob = Vec::new(); let o = ByteOrder::Big; o.put_u16(&mut blob, 1); o.put_u16(&mut blob, 5); o.put_u16(&mut blob, 4); o.put_u32(&mut blob, 3); o.put_u32(&mut blob, 18); o.put_u32(&mut blob, 0); for v in [10u32, 20, 30] { o.put_u32(&mut blob, v); } let mut data = vec![0u8; 100]; data.extend(blob); let (ifd, next) = parse_ifd_at(&data, 100, o, 100, false, &ParseOptions::default()).unwrap(); assert_eq!(next, 0); assert_eq!(ifd.u64s(5).unwrap(), vec![10, 20, 30]); assert_eq!(ifd.get(5).unwrap().offset, 118); } #[test] fn rationals() { assert_eq!(rational(2.8), (28, 10)); assert_eq!(rational(0.004), (4, 1000)); assert_eq!(rational(-1.0), (0, 1)); assert_eq!(srational(-0.333), (-333, 1000)); assert_eq!(srational(7.0), (7, 1)); let (n, d) = rational(1.0 / 3.0); assert!((n as f64 / d as f64 - 1.0 / 3.0).abs() < 1e-6); } fn sample_file() -> Vec { let mut exif = IfdBuilder::new(); exif.set(tags::F_NUMBER, Value::Rational(vec![(4, 1)])); exif.set(tags::MAKER_NOTE, Value::Undefined(b"Nikon\0\x02\x10\0\0II*\0\x08\0\0\0\x01\0\x01\0\x03\0\x01\0\0\0\x05\0\0\0\0\0\0\0".to_vec())); let mut ifd0 = IfdBuilder::new(); ifd0.set(tags::IMAGE_WIDTH, Value::Long(vec![8])); ifd0.set(tags::IMAGE_LENGTH, Value::Long(vec![4])); ifd0.set(tags::MAKE, Value::Ascii("NIKON".into())); ifd0.set_child(tags::EXIF_IFD, exif); ifd0.set_image(ImageData::Strips { rows_per_strip: 2, strips: vec![vec![1; 16], vec![2; 16]] }); ifd0.add_sub_ifd(IfdBuilder::new().with(tags::IMAGE_WIDTH, Value::Long(vec![2]))); TiffWriter::default().write(&[ifd0, IfdBuilder::new().with(1, Value::Short(vec![1]))]).unwrap() } fn exercise(t: &Tiff, data: &[u8]) { for ifd in t.all_ifds() { if let Ok(info) = ifd.image() { for c in info.chunks(data.len() as u64).iter().take(64) { let _ = chunk_bytes(data, c); } } for e in &ifd.entries { let _ = e.value.to_f64_vec(); let _ = e.value.as_str(); } } if let Some(e) = t.exif().and_then(|x| x.get(tags::MAKER_NOTE)) { let _ = lightcraft_tiff::makernote::parse_makernote(data, e.offset, e.count() as u64, t.order, "NIKON"); } } proptest! { #![proptest_config(ProptestConfig { cases: 2000, .. ProptestConfig::default() })] #[test] fn random_bytes_never_panic(mut data in proptest::collection::vec(any::(), 0..512), hdr in 0usize..4) { let heads: [&[u8]; 4] = [b"II*\0", b"MM\0*", b"II+\0\x08\0\0\0", b"MM\0+\0\x08\0\0"]; let h = heads[hdr]; if data.len() >= h.len() { data[..h.len()].copy_from_slice(h); } if let Ok(t) = Tiff::parse(&data) { exercise(&t, &data); } } #[test] fn mutated_files_never_panic(flips in proptest::collection::vec((any::(), any::()), 1..16), cut in any::()) { let mut data = sample_file(); for (i, v) in flips { let n = data.len(); data[i % n] = v; } let keep = if cut % 4 == 0 { cut % data.len() } else { data.len() }; let data = &data[..keep]; if let Ok(t) = Tiff::parse(data) { exercise(&t, data); } } #[test] fn random_short_arrays_roundtrip(v in proptest::collection::vec(any::(), 1..300), big in any::(), le in any::()) { let order = if le { ByteOrder::Little } else { ByteOrder::Big }; let ifd = IfdBuilder::new().with(500, Value::Short(v.clone())).with(501, Value::Double(v.iter().map(|&x| x as f64 / 3.0).collect())); let bytes = TiffWriter::new(order, big).write(&[ifd]).unwrap(); let t = Tiff::parse(&bytes).unwrap(); prop_assert_eq!(t.ifds[0].value(500), Some(&Value::Short(v.clone()))); prop_assert_eq!(t.ifds[0].f64s(501).unwrap().len(), v.len()); } } /// A BigTIFF whose IFD says it has an enormous number of entries, with one readable, computed where the next-IFD /// offset would be from the advertised count: an add overflow, a panic in builds with overflow checks (issue /// #1516). The readable entry is kept and there is no next IFD; complete chains still link. #[test] fn bigtiff_with_an_enormous_entry_count_does_not_panic() { for order in ORDERS { for count in [u64::MAX, 1 << 62, 2] { // little-endian bytes, reversed for big-endian files let e = |le: &[u8]| -> Vec { if order == ByteOrder::Little { le.to_vec() } else { le.iter().rev().copied().collect() } }; let mut f = if order == ByteOrder::Little { b"II".to_vec() } else { b"MM".to_vec() }; for part in [e(&43u16.to_le_bytes()), e(&8u16.to_le_bytes()), e(&0u16.to_le_bytes()), e(&16u64.to_le_bytes()), e(&count.to_le_bytes())] { f.extend(part); } // one entry: ImageWidth, LONG, 1 value (inline in the 8-byte field) for part in [e(&tags::IMAGE_WIDTH.to_le_bytes()), e(&4u16.to_le_bytes()), e(&1u64.to_le_bytes()), e(&640u32.to_le_bytes())] { f.extend(part); } f.extend([0; 4]); let (ifd, next) = parse_ifd_at(&f, 16, order, 0, true, &ParseOptions::default()).unwrap(); assert_eq!(ifd.value(tags::IMAGE_WIDTH), Some(&Value::Long(vec![640])), "{order:?} {count}"); assert_eq!(next, 0, "{order:?} {count}"); let t = Tiff::parse(&f).unwrap(); assert_eq!(t.ifds.len(), 1); } // a complete chain of two IFDs still links let mut a = IfdBuilder::new(); a.set(1000, Value::Short(vec![1])); let mut b = IfdBuilder::new(); b.set(1000, Value::Short(vec![2])); let t = Tiff::parse(&TiffWriter::new(order, true).write(&[a, b]).unwrap()).unwrap(); assert_eq!(t.ifds.len(), 2, "{order:?}"); } }