From 5809ed4e51a6fb7feba7e453dc66a90429016040 Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 26 Oct 2026 18:16:09 +0000 Subject: [PATCH] refactor(lightcraft-raw): move preview.rs tests to crates/raw/src/preview/tests.rs --- crates/raw/src/preview.rs | 349 +------------------------------- crates/raw/src/preview/tests.rs | 346 +++++++++++++++++++++++++++++++ 2 files changed, 347 insertions(+), 348 deletions(-) create mode 100644 crates/raw/src/preview/tests.rs diff --git a/crates/raw/src/preview.rs b/crates/raw/src/preview.rs index 4bcf6963e..245b24f75 100644 --- a/crates/raw/src/preview.rs +++ b/crates/raw/src/preview.rs @@ -435,351 +435,4 @@ fn trim_eoi(s: &[u8]) -> &[u8] { } #[cfg(test)] -mod tests { - use super::*; - use lightcraft_tiff::{IfdBuilder, ImageData, TiffWriter, Value}; - - fn fake_jpeg(n: usize) -> Vec { - let mut j = vec![0xff, 0xd8, 0xff, 0xc0, 0x00, 0x0b, 8, 0, 1, 0, 1, 1, 1, 0x11, 0]; - j.extend(std::iter::repeat_n(0x55u8, n)); - j.extend_from_slice(&[0xff, 0xd9]); - j - } - - /// Preview extraction only serves single-chunk JPEG-compressed IFDs and asks the `single_chunk` first: - /// a TIFF whose strip array declares 100 000 one-row strips and no StripByteCounts must not build their - /// descriptor vector, and of course finds no preview in it. - #[test] - fn a_multi_strip_jpeg_ifd_costs_no_descriptor_vector() { - let mut ifd = IfdBuilder::new(); - ifd.set(t::COMPRESSION, Value::Short(vec![7])); - ifd.set(t::IMAGE_WIDTH, Value::Long(vec![16])); - ifd.set(t::IMAGE_LENGTH, Value::Long(vec![100_000])); - ifd.set(t::ROWS_PER_STRIP, Value::Short(vec![1])); - ifd.set(t::STRIP_OFFSETS, Value::Byte((0..100_000).map(|i| (i % 255 + 1) as u8).collect())); - let file = TiffWriter::default().write(&[ifd]).unwrap(); - let (tx, rx) = std::sync::mpsc::channel(); - std::thread::spawn(move || tx.send(embedded_preview(&file))); - assert_eq!(rx.recv_timeout(std::time::Duration::from_secs(2)), Ok(None)); - } - - /// CR3 candidates claim overlapping extents: each block's first bytes double as a 40-byte jump marker so - /// a walk jogs past the next header and reaches the end of the file, making every one of the 9000 - /// candidates walk its whole suffix before the marker-walk budget — quadratic without it. Nothing here - /// is a JPEG (no frame header), so the answer is `None` either way. - #[test] - fn cr3_preview_boxes_stop_walking_when_the_budget_is_spent() { - // one 256-byte block per candidate, all marker-aligned so walks chain through later blocks: - // 0..4, size (itself a jump marker of 40 bytes); 4..8, "PRVW"; 20..24, the claimed length - // (to the file end); 24..28, an SOI plus a marker jumping to the fill at 40; 64..256, a fill of - // 4-byte markers (`ff 01 00 02`). - let blocks = 9000usize; - let total = blocks * 256; - let mut f = Vec::with_capacity(total); - for k in 0..blocks { - let base = k * 256; - f.extend_from_slice(&[0xff, 0x01, 0x00, 0x26, b'P', b'R', b'V', b'W', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); - f.extend_from_slice(&u32::try_from(total - base - 24).unwrap().to_be_bytes()); - f.extend_from_slice(&[0xff, 0xd8, 0x00, 0x24, 0xff, 0x01, 0x00, 0x26, 0, 0, 0, 0, 0, 0, 0, 0]); - f.resize(base + 64, 0); // 40..64 filler: walks jump over it, so its bytes are free - for _ in 0..48 { - f.extend_from_slice(&[0xff, 0x01, 0x00, 0x02]); - } - } - assert_eq!(f.len(), total); - let (tx, rx) = std::sync::mpsc::channel(); - std::thread::spawn(move || tx.send(cr3_preview_boxes(&f).map(|s| s.len()))); - assert_eq!(rx.recv_timeout(std::time::Duration::from_secs(2)), Ok(None), "no PRVW candidate is a JPEG"); - } - - /// A minimal RAF: the fixed header (JPEG offset/length at 84/88), the JPEG, then a raw tail the - /// caller must not have to read. `jpeg_len` is written as declared, so a test can claim more than - /// the JPEG holds the way a padded container does. - fn raf_fixture(jpeg: &[u8], gap: usize, preamble: usize, declared_len: Option) -> Vec { - let head = 160 + preamble; - let jpeg_off = head; - let mut f = vec![0u8; head]; - f[..16].copy_from_slice(b"FUJIFILMCCD-RAW "); - f[84..88].copy_from_slice(&(jpeg_off as u32).to_be_bytes()); - f[88..92].copy_from_slice(&(declared_len.unwrap_or(jpeg.len()) as u32).to_be_bytes()); - // records directory and raw block are absent (offset 0), which `header` reads as "none" - f.extend_from_slice(jpeg); - f.extend(std::iter::repeat_n(0x33u8, gap)); - f - } - - /// A RAF's preview pointer sits at a fixed offset, so a header-sized prefix locates it and the - /// caller reads just that range instead of the whole file — the whole point on a network share. - #[test] - fn bounded_preview_locates_a_rafs_jpeg_from_the_header_alone() { - let jpeg = fake_jpeg(5000); - let file = raf_fixture(&jpeg, 40_000, 0, None); - // A prefix that covers the pointer's range: header + JPEG, and nothing of the tail. - let prefix_len = 8192.min(file.len()); - let prefix = &file[..prefix_len]; - - let range = bounded_preview(prefix).expect("a RAF's preview is found from its header"); - assert_eq!(range.offset, 160); - assert_eq!(range.window_len, jpeg.len() as u64); - assert!(range.offset + range.window_len <= prefix_len as u64, "the prefix covers the whole range: {range:?} of {prefix_len}"); - assert!((range.offset + range.window_len) < file.len() as u64, "the range stops short of the tail: {range:?} of {}", file.len()); - - // What the caller reads, and what it holds afterwards: identical to the whole-file answer. - let window = prefix.get(range.offset as usize..(range.offset + range.window_len) as usize).expect("the range lies inside the prefix"); - let bounded = embedded_preview_from_range(window, range).expect("the range holds the preview"); - assert_eq!(Some(bounded.clone()), embedded_preview(&file)); - assert!(bounded.ends_with(&[0xff, 0xd9]), "the preview ends at its EOI marker"); - - // A prefix that stops inside the declared extent is clamped by the container reader. The - // caller then reads a short window; what comes back is the whole JPEG it holds (the EOI - // marker is inside), never bytes past the window. - let short = &file[..2000]; - let clipped = bounded_preview(short).expect("a pointer before the cut is still found"); - assert!(clipped.window_len < jpeg.len() as u64, "clamped: {clipped:?}"); - let window = short.get(clipped.offset as usize..).expect("inside"); - let preview = embedded_preview_from_range(window, clipped).expect("the bytes are a JPEG start"); - // No end-of-image marker inside the window: the bytes come back as they are, exactly as the - // whole-file path returns an unclosed JPEG. The decoder refuses it later; nothing is invented. - assert_eq!(preview, window, "an unclosed JPEG is handed on unchanged, never padded"); - assert!(preview.len() <= window.len(), "never past the window"); - } - - /// The preview pointer may sit past the prefix (a long maker-note record before it), and a - /// container may declare padding after the JPEG: both stay correct, and the padding is dropped. - #[test] - fn bounded_preview_handles_a_late_pointer_and_padding() { - let jpeg = fake_jpeg(300); - let late = raf_fixture(&jpeg, 0, 9000, None); - let range = bounded_preview(&late).expect("the whole file is always a valid prefix"); - assert_eq!(range.offset, (160 + 9000) as u64); - let padded = raf_fixture(&jpeg, 500, 0, Some(jpeg.len() + 500)); - let range = bounded_preview(&padded).expect("a padded extent is still located"); - assert_eq!(range.window_len, (jpeg.len() + 500) as u64); - let window = padded.get(range.offset as usize..(range.offset + range.window_len) as usize).expect("the window is inside the file"); - let preview = embedded_preview_from_range(window, range).expect("the padding is dropped"); - assert_eq!(preview, embedded_preview(&padded).expect("the whole-file answer agrees")); - assert!(preview.ends_with(&[0xff, 0xd9])); - } - - /// Without a usable header or pointer the caller must read the file whole: the answer is `None`, - /// never a wrong range. - #[test] - fn bounded_preview_declines_what_the_header_cannot_answer() { - let jpeg = fake_jpeg(64); - let file = raf_fixture(&jpeg, 0, 0, None); - assert_eq!(bounded_preview(&jpeg), None, "not a raw at all"); - assert_eq!(bounded_preview(b""), None); - if let Some(short) = file.get(..64) { - assert_eq!(bounded_preview(short), None, "a header cut short has not shown the pointer yet"); - } - // a pointer that claims more than the file holds: `header` clamps the slice to the buffer, - // so the range never runs past the end (no out-of-bounds read can follow from it) - let mut lying = raf_fixture(&jpeg, 4096, 0, None); - lying[88..92].copy_from_slice(&(u32::MAX / 2).to_be_bytes()); - let clamped = bounded_preview(&lying).expect("a pointer inside the file is still usable"); - assert!(clamped.offset + clamped.window_len <= lying.len() as u64, "clamped to the file: {clamped:?}"); - // a range the window cannot satisfy yields nothing - assert_eq!(embedded_preview_from_range(&jpeg, PreviewRange { offset: 0, window_len: u64::MAX }), None); - // a TIFF-based raw: its pointer tag lives in an IFD this prefix may cut short - let mut tiff = b"II*\0\x08\0\0\0".to_vec(); - tiff.extend_from_slice(&[0u8; 64]); - assert_eq!(bounded_preview(&tiff), None); - } - - /// A range that does not hold a JPEG yields nothing, rather than a truncated image handed on. - #[test] - fn a_range_that_does_not_hold_a_jpeg_yields_nothing() { - let jpeg = fake_jpeg(200); - let range = PreviewRange { offset: 0, window_len: jpeg.len() as u64 }; - assert_eq!(embedded_preview_from_range(&jpeg, range).as_deref(), Some(&jpeg[..])); - assert_eq!(embedded_preview_from_range(&jpeg[..10], range), None, "window shorter than the range claims"); - let not_jpeg = vec![0u8; jpeg.len()]; - assert_eq!(embedded_preview_from_range(¬_jpeg, range), None, "not a JPEG"); - assert_eq!(embedded_preview_from_range(&[], PreviewRange { offset: 0, window_len: 0 }), None, "empty window"); - } - - #[test] - fn picks_largest_dct_jpeg() { - let small = fake_jpeg(10); - let big = fake_jpeg(500); - let lossless = crate::ljpeg::encode(&[1u16; 64], 8, 8, 1, 12, 1, 0); - let mut ifd0 = IfdBuilder::new(); - ifd0.set(t::COMPRESSION, Value::Short(vec![6])); - ifd0.set(t::IMAGE_WIDTH, Value::Long(vec![1])); - ifd0.set(t::IMAGE_LENGTH, Value::Long(vec![1])); - ifd0.set_image(ImageData::Strips { rows_per_strip: 1, strips: vec![small.clone()] }); - let mut raw = IfdBuilder::new(); - raw.set(t::COMPRESSION, Value::Short(vec![7])); - raw.set(t::IMAGE_WIDTH, Value::Long(vec![8])); - raw.set(t::IMAGE_LENGTH, Value::Long(vec![8])); - raw.set_image(ImageData::Strips { rows_per_strip: 8, strips: vec![lossless] }); - ifd0.add_sub_ifd(raw); - let mut sub = IfdBuilder::new(); - sub.set(t::COMPRESSION, Value::Short(vec![7])); - sub.set(t::IMAGE_WIDTH, Value::Long(vec![2])); - sub.set(t::IMAGE_LENGTH, Value::Long(vec![2])); - sub.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![1])); - sub.set_image(ImageData::Strips { rows_per_strip: 2, strips: vec![big.clone()] }); - ifd0.add_sub_ifd(sub); - let bytes = TiffWriter::default().write(&[ifd0]).unwrap(); - assert_eq!(embedded_preview(&bytes).unwrap(), big); - assert!(embedded_preview(b"nope").is_none()); - // CR3: a PRVW box after the ftyp - let mut cr3 = b"\0\0\0\x18ftypcrx \0\0\0\x01crx isom".to_vec(); - let j = fake_jpeg(300); - cr3.extend_from_slice(&((24 + j.len()) as u32).to_be_bytes()); - cr3.extend_from_slice(b"PRVW\0\0\0\0\0\x01\x06\x54\x04\x38\0\x01"); - cr3.extend_from_slice(&(j.len() as u32).to_be_bytes()); - cr3.extend_from_slice(&j); - assert_eq!(embedded_preview(&cr3).unwrap(), j); - for n in 0..cr3.len() { - let _ = embedded_preview(&cr3[..n]); - } - } - - /// A JPEG like [`fake_jpeg`] whose frame header says `w` x `h` with `nc` components. - fn sized_jpeg(w: u16, h: u16, nc: u8, n: usize) -> Vec { - let mut j = vec![0xff, 0xd8, 0xff, 0xc0, 0x00, 8 + 3 * nc, 8]; - j.extend_from_slice(&h.to_be_bytes()); - j.extend_from_slice(&w.to_be_bytes()); - j.push(nc); - for c in 0..nc { - j.extend_from_slice(&[c + 1, 0x11, 0]); - } - j.extend(std::iter::repeat_n(0x55u8, n)); - j.extend_from_slice(&[0xff, 0xd9]); - j - } - - /// A TIFF raw holding `jpegs` as JPEG-compressed strips: the first in IFD0, the others in sub-IFDs. - fn tiff_with_jpegs(jpegs: &[&[u8]]) -> Vec { - let ifd = |j: &[u8]| { - let mut ifd = IfdBuilder::new(); - ifd.set(t::COMPRESSION, Value::Short(vec![6])); - ifd.set(t::IMAGE_WIDTH, Value::Long(vec![1])); - ifd.set(t::IMAGE_LENGTH, Value::Long(vec![1])); - ifd.set_image(ImageData::Strips { rows_per_strip: 1, strips: vec![j.to_vec()] }); - ifd - }; - let mut ifd0 = ifd(jpegs[0]); - for j in &jpegs[1..] { - ifd0.add_sub_ifd(ifd(j)); - } - TiffWriter::default().write(&[ifd0]).unwrap() - } - - /// A stand-in decodes the smallest JPEG that covers its box and shows the same picture as the largest: not a - /// thumbnail of another shape (a letterboxed one) or a grey mask, and the largest when no smaller one is big enough. - #[test] - fn a_stand_in_takes_the_smallest_covering_jpeg_of_the_same_shape() { - let (big, small, boxed) = (sized_jpeg(9984, 6656, 3, 500), sized_jpeg(1616, 1080, 3, 50), sized_jpeg(160, 120, 3, 10)); - // a ProRAW DNG keeps grey semantic masks of the picture's shape as JPEGs too - let mask = sized_jpeg(768, 512, 1, 20); - let file = tiff_with_jpegs(&[&small, &big, &boxed, &mask]); - assert_eq!(embedded_preview(&file).unwrap(), big, "unchanged: the largest"); - assert_eq!(embedded_preview_covering(&file, 1600).unwrap(), small, "1616 px covers a 1600 px box"); - assert_eq!(embedded_preview_covering(&file, 1616).unwrap(), small); - assert_eq!(embedded_preview_covering(&file, 1617).unwrap(), big, "too small: the largest"); - assert_eq!(embedded_preview_covering(&file, u32::MAX).unwrap(), big); - assert_eq!(embedded_preview_covering(&file, 100).unwrap(), small, "the 4:3 thumbnail and the grey mask are not this picture"); - // a small copy of the same shape (within 1 %) is taken - let thumb = sized_jpeg(160, 107, 3, 10); - let file = tiff_with_jpegs(&[&small, &big, &thumb]); - assert_eq!(embedded_preview_covering(&file, 100).unwrap(), thumb); - assert_eq!(embedded_preview_covering(&tiff_with_jpegs(&[&big]), 100).unwrap(), big, "one preview: that one"); - for n in (0..file.len()).step_by(7) { - let _ = embedded_preview_covering(&file[..n], 100); - } - } - - /// CR3 with a full-size JPEG track: that JPEG wins over the smaller `PRVW` box. - #[test] - fn cr3_prefers_the_full_size_jpeg_track() { - let bx = |kind: &[u8; 4], body: &[u8]| -> Vec { [&((body.len() + 8) as u32).to_be_bytes()[..], kind, body].concat() }; - let full = |kind: &[u8; 4], body: &[u8]| bx(kind, &[&[0u8; 4][..], body].concat()); - let (small, big) = (fake_jpeg(300), fake_jpeg(3000)); - let mut file = bx(b"ftyp", b"crx \0\0\0\x01crx isom"); - let mut prvw = b"\0\0\0\0\0\x01\x06\x54\x04\x38\0\x01".to_vec(); - prvw.extend_from_slice(&(small.len() as u32).to_be_bytes()); - prvw.extend_from_slice(&small); - file.extend(bx(b"PRVW", &prvw)); - // CRAW sample entry: 82 bytes, then a JPEG child box; one sample at `at` - let mut craw = vec![0u8; 82]; - craw.extend(bx(b"JPEG", &[0; 4])); - let at = 2048u64; - let stsd = full(b"stsd", &[&1u32.to_be_bytes()[..], &bx(b"CRAW", &craw)].concat()); - let stsz = full(b"stsz", &[0u32.to_be_bytes(), 1u32.to_be_bytes(), (big.len() as u32).to_be_bytes()].concat()); - let co64 = full(b"co64", &[&1u32.to_be_bytes()[..], &at.to_be_bytes()].concat()); - let trak = bx(b"trak", &bx(b"mdia", &bx(b"minf", &bx(b"stbl", &[stsd, stsz, co64].concat())))); - file.extend(bx(b"moov", &trak)); - file.resize(at as usize, 0); - file.extend_from_slice(&big); - assert_eq!(embedded_preview(&file).unwrap(), big); - // a track pointing past the end falls back to PRVW - file.truncate(at as usize + 100); - assert_eq!(embedded_preview(&file).unwrap(), small); - } - - /// A structurally complete DCT JPEG (no real image data): `nc` components, `n` entropy bytes. - fn scan_jpeg(nc: u8, n: usize) -> Vec { - let mut j = vec![0xff, 0xd8, 0xff, 0xdb, 0x00, 0x43, 0x00]; - j.extend(std::iter::repeat_n(8u8, 64)); - j.extend_from_slice(&[0xff, 0xc0, 0x00, 8 + 3 * nc, 8, 0, 16, 0, 24, nc]); - for c in 0..nc { - j.extend_from_slice(&[c + 1, 0x11, 0]); - } - j.extend_from_slice(&[0xff, 0xda, 0x00, 6 + 2 * nc, nc]); - for c in 0..nc { - j.extend_from_slice(&[c + 1, 0]); - } - j.extend_from_slice(&[0, 63, 0]); - j.extend(std::iter::repeat_n(0x55u8, n)); - j.extend_from_slice(&[0xff, 0x00, 0x12, 0xff, 0xd0, 0x34, 0xff, 0xd9]); - j - } - - /// A crafted container full of JPEG-like starts (each walk running on through the ones after it) - /// is scanned in linear time: the walks share a budget (without it this 40 MB file takes about a minute). - #[test] - fn a_scan_through_many_fake_jpegs_stays_linear() { - let mut unit = vec![0u8, 0xd8, 0xff, 0xdb, 0x00, 0x02, 0xff, 0xda, 0x00, 0x02]; - unit.extend(std::iter::repeat_n(0x55u8, 190)); - let mut f = b"\0MRM\0\x01\0\0".to_vec(); - for _ in 0..200_000 { - f.extend_from_slice(&unit); - } - let t = std::time::Instant::now(); - assert_eq!(scan_for_jpeg(&f), None); - assert!(t.elapsed() < std::time::Duration::from_secs(10), "{:?}", t.elapsed()); - } - - /// Containers whose structure we do not walk give up their largest colour JPEG, found by scanning. - #[test] - fn scanned_containers_give_the_largest_colour_jpeg() { - let (small, big, mosaic) = (scan_jpeg(3, 100), scan_jpeg(3, 900), scan_jpeg(1, 5000)); - let heads: [&[u8]; 3] = [b"II\x1a\0\0\0HEAPCCDR", b"\0MRM\0\x01\0\0", b"FOVb\x02\0\x02\0"]; - for head in heads { - let mut f = head.to_vec(); - for part in [&small, &mosaic, &big] { - f.extend_from_slice(&[0xff, 0xd8, 0xff, 0x00, 0x00]); // a stray SOI-like run: not a JPEG - f.extend_from_slice(part); - f.extend_from_slice(&[0u8; 7]); - } - // MRW decodes; its preview is still found by the scan - assert!(crate::probe(&f).is_some_and(|p| !p.is_supported() || p == crate::RawFormat::Mrw), "{head:?}"); - assert_eq!(embedded_preview(&f).as_deref(), Some(&big[..]), "{head:?}"); - for n in (0..f.len()).step_by(11) { - let _ = embedded_preview(&f[..n]); - } - } - // a preview whose first byte was overwritten (Minolta and Epson files store 00, 02 or ee there) is restored - let mut f = b"\0MRM\0\x01\0\0".to_vec(); - f.push(0x02); - f.extend_from_slice(&big[1..]); - assert_eq!(embedded_preview(&f).as_deref(), Some(&big[..])); - // only a one-component JPEG, or none: nothing - let mut f = b"FOVb\x02\0\x02\0".to_vec(); - f.extend_from_slice(&mosaic); - assert_eq!(embedded_preview(&f), None); - } -} +mod tests; diff --git a/crates/raw/src/preview/tests.rs b/crates/raw/src/preview/tests.rs new file mode 100644 index 000000000..286ad1d77 --- /dev/null +++ b/crates/raw/src/preview/tests.rs @@ -0,0 +1,346 @@ +use super::*; +use lightcraft_tiff::{IfdBuilder, ImageData, TiffWriter, Value}; + +fn fake_jpeg(n: usize) -> Vec { + let mut j = vec![0xff, 0xd8, 0xff, 0xc0, 0x00, 0x0b, 8, 0, 1, 0, 1, 1, 1, 0x11, 0]; + j.extend(std::iter::repeat_n(0x55u8, n)); + j.extend_from_slice(&[0xff, 0xd9]); + j +} + +/// Preview extraction only serves single-chunk JPEG-compressed IFDs and asks the `single_chunk` first: +/// a TIFF whose strip array declares 100 000 one-row strips and no StripByteCounts must not build their +/// descriptor vector, and of course finds no preview in it. +#[test] +fn a_multi_strip_jpeg_ifd_costs_no_descriptor_vector() { + let mut ifd = IfdBuilder::new(); + ifd.set(t::COMPRESSION, Value::Short(vec![7])); + ifd.set(t::IMAGE_WIDTH, Value::Long(vec![16])); + ifd.set(t::IMAGE_LENGTH, Value::Long(vec![100_000])); + ifd.set(t::ROWS_PER_STRIP, Value::Short(vec![1])); + ifd.set(t::STRIP_OFFSETS, Value::Byte((0..100_000).map(|i| (i % 255 + 1) as u8).collect())); + let file = TiffWriter::default().write(&[ifd]).unwrap(); + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || tx.send(embedded_preview(&file))); + assert_eq!(rx.recv_timeout(std::time::Duration::from_secs(2)), Ok(None)); +} + +/// CR3 candidates claim overlapping extents: each block's first bytes double as a 40-byte jump marker so +/// a walk jogs past the next header and reaches the end of the file, making every one of the 9000 +/// candidates walk its whole suffix before the marker-walk budget — quadratic without it. Nothing here +/// is a JPEG (no frame header), so the answer is `None` either way. +#[test] +fn cr3_preview_boxes_stop_walking_when_the_budget_is_spent() { + // one 256-byte block per candidate, all marker-aligned so walks chain through later blocks: + // 0..4, size (itself a jump marker of 40 bytes); 4..8, "PRVW"; 20..24, the claimed length + // (to the file end); 24..28, an SOI plus a marker jumping to the fill at 40; 64..256, a fill of + // 4-byte markers (`ff 01 00 02`). + let blocks = 9000usize; + let total = blocks * 256; + let mut f = Vec::with_capacity(total); + for k in 0..blocks { + let base = k * 256; + f.extend_from_slice(&[0xff, 0x01, 0x00, 0x26, b'P', b'R', b'V', b'W', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + f.extend_from_slice(&u32::try_from(total - base - 24).unwrap().to_be_bytes()); + f.extend_from_slice(&[0xff, 0xd8, 0x00, 0x24, 0xff, 0x01, 0x00, 0x26, 0, 0, 0, 0, 0, 0, 0, 0]); + f.resize(base + 64, 0); // 40..64 filler: walks jump over it, so its bytes are free + for _ in 0..48 { + f.extend_from_slice(&[0xff, 0x01, 0x00, 0x02]); + } + } + assert_eq!(f.len(), total); + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || tx.send(cr3_preview_boxes(&f).map(|s| s.len()))); + assert_eq!(rx.recv_timeout(std::time::Duration::from_secs(2)), Ok(None), "no PRVW candidate is a JPEG"); +} + +/// A minimal RAF: the fixed header (JPEG offset/length at 84/88), the JPEG, then a raw tail the +/// caller must not have to read. `jpeg_len` is written as declared, so a test can claim more than +/// the JPEG holds the way a padded container does. +fn raf_fixture(jpeg: &[u8], gap: usize, preamble: usize, declared_len: Option) -> Vec { + let head = 160 + preamble; + let jpeg_off = head; + let mut f = vec![0u8; head]; + f[..16].copy_from_slice(b"FUJIFILMCCD-RAW "); + f[84..88].copy_from_slice(&(jpeg_off as u32).to_be_bytes()); + f[88..92].copy_from_slice(&(declared_len.unwrap_or(jpeg.len()) as u32).to_be_bytes()); + // records directory and raw block are absent (offset 0), which `header` reads as "none" + f.extend_from_slice(jpeg); + f.extend(std::iter::repeat_n(0x33u8, gap)); + f +} + +/// A RAF's preview pointer sits at a fixed offset, so a header-sized prefix locates it and the +/// caller reads just that range instead of the whole file — the whole point on a network share. +#[test] +fn bounded_preview_locates_a_rafs_jpeg_from_the_header_alone() { + let jpeg = fake_jpeg(5000); + let file = raf_fixture(&jpeg, 40_000, 0, None); + // A prefix that covers the pointer's range: header + JPEG, and nothing of the tail. + let prefix_len = 8192.min(file.len()); + let prefix = &file[..prefix_len]; + + let range = bounded_preview(prefix).expect("a RAF's preview is found from its header"); + assert_eq!(range.offset, 160); + assert_eq!(range.window_len, jpeg.len() as u64); + assert!(range.offset + range.window_len <= prefix_len as u64, "the prefix covers the whole range: {range:?} of {prefix_len}"); + assert!((range.offset + range.window_len) < file.len() as u64, "the range stops short of the tail: {range:?} of {}", file.len()); + + // What the caller reads, and what it holds afterwards: identical to the whole-file answer. + let window = prefix.get(range.offset as usize..(range.offset + range.window_len) as usize).expect("the range lies inside the prefix"); + let bounded = embedded_preview_from_range(window, range).expect("the range holds the preview"); + assert_eq!(Some(bounded.clone()), embedded_preview(&file)); + assert!(bounded.ends_with(&[0xff, 0xd9]), "the preview ends at its EOI marker"); + + // A prefix that stops inside the declared extent is clamped by the container reader. The + // caller then reads a short window; what comes back is the whole JPEG it holds (the EOI + // marker is inside), never bytes past the window. + let short = &file[..2000]; + let clipped = bounded_preview(short).expect("a pointer before the cut is still found"); + assert!(clipped.window_len < jpeg.len() as u64, "clamped: {clipped:?}"); + let window = short.get(clipped.offset as usize..).expect("inside"); + let preview = embedded_preview_from_range(window, clipped).expect("the bytes are a JPEG start"); + // No end-of-image marker inside the window: the bytes come back as they are, exactly as the + // whole-file path returns an unclosed JPEG. The decoder refuses it later; nothing is invented. + assert_eq!(preview, window, "an unclosed JPEG is handed on unchanged, never padded"); + assert!(preview.len() <= window.len(), "never past the window"); +} + +/// The preview pointer may sit past the prefix (a long maker-note record before it), and a +/// container may declare padding after the JPEG: both stay correct, and the padding is dropped. +#[test] +fn bounded_preview_handles_a_late_pointer_and_padding() { + let jpeg = fake_jpeg(300); + let late = raf_fixture(&jpeg, 0, 9000, None); + let range = bounded_preview(&late).expect("the whole file is always a valid prefix"); + assert_eq!(range.offset, (160 + 9000) as u64); + let padded = raf_fixture(&jpeg, 500, 0, Some(jpeg.len() + 500)); + let range = bounded_preview(&padded).expect("a padded extent is still located"); + assert_eq!(range.window_len, (jpeg.len() + 500) as u64); + let window = padded.get(range.offset as usize..(range.offset + range.window_len) as usize).expect("the window is inside the file"); + let preview = embedded_preview_from_range(window, range).expect("the padding is dropped"); + assert_eq!(preview, embedded_preview(&padded).expect("the whole-file answer agrees")); + assert!(preview.ends_with(&[0xff, 0xd9])); +} + +/// Without a usable header or pointer the caller must read the file whole: the answer is `None`, +/// never a wrong range. +#[test] +fn bounded_preview_declines_what_the_header_cannot_answer() { + let jpeg = fake_jpeg(64); + let file = raf_fixture(&jpeg, 0, 0, None); + assert_eq!(bounded_preview(&jpeg), None, "not a raw at all"); + assert_eq!(bounded_preview(b""), None); + if let Some(short) = file.get(..64) { + assert_eq!(bounded_preview(short), None, "a header cut short has not shown the pointer yet"); + } + // a pointer that claims more than the file holds: `header` clamps the slice to the buffer, + // so the range never runs past the end (no out-of-bounds read can follow from it) + let mut lying = raf_fixture(&jpeg, 4096, 0, None); + lying[88..92].copy_from_slice(&(u32::MAX / 2).to_be_bytes()); + let clamped = bounded_preview(&lying).expect("a pointer inside the file is still usable"); + assert!(clamped.offset + clamped.window_len <= lying.len() as u64, "clamped to the file: {clamped:?}"); + // a range the window cannot satisfy yields nothing + assert_eq!(embedded_preview_from_range(&jpeg, PreviewRange { offset: 0, window_len: u64::MAX }), None); + // a TIFF-based raw: its pointer tag lives in an IFD this prefix may cut short + let mut tiff = b"II*\0\x08\0\0\0".to_vec(); + tiff.extend_from_slice(&[0u8; 64]); + assert_eq!(bounded_preview(&tiff), None); +} + +/// A range that does not hold a JPEG yields nothing, rather than a truncated image handed on. +#[test] +fn a_range_that_does_not_hold_a_jpeg_yields_nothing() { + let jpeg = fake_jpeg(200); + let range = PreviewRange { offset: 0, window_len: jpeg.len() as u64 }; + assert_eq!(embedded_preview_from_range(&jpeg, range).as_deref(), Some(&jpeg[..])); + assert_eq!(embedded_preview_from_range(&jpeg[..10], range), None, "window shorter than the range claims"); + let not_jpeg = vec![0u8; jpeg.len()]; + assert_eq!(embedded_preview_from_range(¬_jpeg, range), None, "not a JPEG"); + assert_eq!(embedded_preview_from_range(&[], PreviewRange { offset: 0, window_len: 0 }), None, "empty window"); +} + +#[test] +fn picks_largest_dct_jpeg() { + let small = fake_jpeg(10); + let big = fake_jpeg(500); + let lossless = crate::ljpeg::encode(&[1u16; 64], 8, 8, 1, 12, 1, 0); + let mut ifd0 = IfdBuilder::new(); + ifd0.set(t::COMPRESSION, Value::Short(vec![6])); + ifd0.set(t::IMAGE_WIDTH, Value::Long(vec![1])); + ifd0.set(t::IMAGE_LENGTH, Value::Long(vec![1])); + ifd0.set_image(ImageData::Strips { rows_per_strip: 1, strips: vec![small.clone()] }); + let mut raw = IfdBuilder::new(); + raw.set(t::COMPRESSION, Value::Short(vec![7])); + raw.set(t::IMAGE_WIDTH, Value::Long(vec![8])); + raw.set(t::IMAGE_LENGTH, Value::Long(vec![8])); + raw.set_image(ImageData::Strips { rows_per_strip: 8, strips: vec![lossless] }); + ifd0.add_sub_ifd(raw); + let mut sub = IfdBuilder::new(); + sub.set(t::COMPRESSION, Value::Short(vec![7])); + sub.set(t::IMAGE_WIDTH, Value::Long(vec![2])); + sub.set(t::IMAGE_LENGTH, Value::Long(vec![2])); + sub.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![1])); + sub.set_image(ImageData::Strips { rows_per_strip: 2, strips: vec![big.clone()] }); + ifd0.add_sub_ifd(sub); + let bytes = TiffWriter::default().write(&[ifd0]).unwrap(); + assert_eq!(embedded_preview(&bytes).unwrap(), big); + assert!(embedded_preview(b"nope").is_none()); + // CR3: a PRVW box after the ftyp + let mut cr3 = b"\0\0\0\x18ftypcrx \0\0\0\x01crx isom".to_vec(); + let j = fake_jpeg(300); + cr3.extend_from_slice(&((24 + j.len()) as u32).to_be_bytes()); + cr3.extend_from_slice(b"PRVW\0\0\0\0\0\x01\x06\x54\x04\x38\0\x01"); + cr3.extend_from_slice(&(j.len() as u32).to_be_bytes()); + cr3.extend_from_slice(&j); + assert_eq!(embedded_preview(&cr3).unwrap(), j); + for n in 0..cr3.len() { + let _ = embedded_preview(&cr3[..n]); + } +} + +/// A JPEG like [`fake_jpeg`] whose frame header says `w` x `h` with `nc` components. +fn sized_jpeg(w: u16, h: u16, nc: u8, n: usize) -> Vec { + let mut j = vec![0xff, 0xd8, 0xff, 0xc0, 0x00, 8 + 3 * nc, 8]; + j.extend_from_slice(&h.to_be_bytes()); + j.extend_from_slice(&w.to_be_bytes()); + j.push(nc); + for c in 0..nc { + j.extend_from_slice(&[c + 1, 0x11, 0]); + } + j.extend(std::iter::repeat_n(0x55u8, n)); + j.extend_from_slice(&[0xff, 0xd9]); + j +} + +/// A TIFF raw holding `jpegs` as JPEG-compressed strips: the first in IFD0, the others in sub-IFDs. +fn tiff_with_jpegs(jpegs: &[&[u8]]) -> Vec { + let ifd = |j: &[u8]| { + let mut ifd = IfdBuilder::new(); + ifd.set(t::COMPRESSION, Value::Short(vec![6])); + ifd.set(t::IMAGE_WIDTH, Value::Long(vec![1])); + ifd.set(t::IMAGE_LENGTH, Value::Long(vec![1])); + ifd.set_image(ImageData::Strips { rows_per_strip: 1, strips: vec![j.to_vec()] }); + ifd + }; + let mut ifd0 = ifd(jpegs[0]); + for j in &jpegs[1..] { + ifd0.add_sub_ifd(ifd(j)); + } + TiffWriter::default().write(&[ifd0]).unwrap() +} + +/// A stand-in decodes the smallest JPEG that covers its box and shows the same picture as the largest: not a +/// thumbnail of another shape (a letterboxed one) or a grey mask, and the largest when no smaller one is big enough. +#[test] +fn a_stand_in_takes_the_smallest_covering_jpeg_of_the_same_shape() { + let (big, small, boxed) = (sized_jpeg(9984, 6656, 3, 500), sized_jpeg(1616, 1080, 3, 50), sized_jpeg(160, 120, 3, 10)); + // a ProRAW DNG keeps grey semantic masks of the picture's shape as JPEGs too + let mask = sized_jpeg(768, 512, 1, 20); + let file = tiff_with_jpegs(&[&small, &big, &boxed, &mask]); + assert_eq!(embedded_preview(&file).unwrap(), big, "unchanged: the largest"); + assert_eq!(embedded_preview_covering(&file, 1600).unwrap(), small, "1616 px covers a 1600 px box"); + assert_eq!(embedded_preview_covering(&file, 1616).unwrap(), small); + assert_eq!(embedded_preview_covering(&file, 1617).unwrap(), big, "too small: the largest"); + assert_eq!(embedded_preview_covering(&file, u32::MAX).unwrap(), big); + assert_eq!(embedded_preview_covering(&file, 100).unwrap(), small, "the 4:3 thumbnail and the grey mask are not this picture"); + // a small copy of the same shape (within 1 %) is taken + let thumb = sized_jpeg(160, 107, 3, 10); + let file = tiff_with_jpegs(&[&small, &big, &thumb]); + assert_eq!(embedded_preview_covering(&file, 100).unwrap(), thumb); + assert_eq!(embedded_preview_covering(&tiff_with_jpegs(&[&big]), 100).unwrap(), big, "one preview: that one"); + for n in (0..file.len()).step_by(7) { + let _ = embedded_preview_covering(&file[..n], 100); + } +} + +/// CR3 with a full-size JPEG track: that JPEG wins over the smaller `PRVW` box. +#[test] +fn cr3_prefers_the_full_size_jpeg_track() { + let bx = |kind: &[u8; 4], body: &[u8]| -> Vec { [&((body.len() + 8) as u32).to_be_bytes()[..], kind, body].concat() }; + let full = |kind: &[u8; 4], body: &[u8]| bx(kind, &[&[0u8; 4][..], body].concat()); + let (small, big) = (fake_jpeg(300), fake_jpeg(3000)); + let mut file = bx(b"ftyp", b"crx \0\0\0\x01crx isom"); + let mut prvw = b"\0\0\0\0\0\x01\x06\x54\x04\x38\0\x01".to_vec(); + prvw.extend_from_slice(&(small.len() as u32).to_be_bytes()); + prvw.extend_from_slice(&small); + file.extend(bx(b"PRVW", &prvw)); + // CRAW sample entry: 82 bytes, then a JPEG child box; one sample at `at` + let mut craw = vec![0u8; 82]; + craw.extend(bx(b"JPEG", &[0; 4])); + let at = 2048u64; + let stsd = full(b"stsd", &[&1u32.to_be_bytes()[..], &bx(b"CRAW", &craw)].concat()); + let stsz = full(b"stsz", &[0u32.to_be_bytes(), 1u32.to_be_bytes(), (big.len() as u32).to_be_bytes()].concat()); + let co64 = full(b"co64", &[&1u32.to_be_bytes()[..], &at.to_be_bytes()].concat()); + let trak = bx(b"trak", &bx(b"mdia", &bx(b"minf", &bx(b"stbl", &[stsd, stsz, co64].concat())))); + file.extend(bx(b"moov", &trak)); + file.resize(at as usize, 0); + file.extend_from_slice(&big); + assert_eq!(embedded_preview(&file).unwrap(), big); + // a track pointing past the end falls back to PRVW + file.truncate(at as usize + 100); + assert_eq!(embedded_preview(&file).unwrap(), small); +} + +/// A structurally complete DCT JPEG (no real image data): `nc` components, `n` entropy bytes. +fn scan_jpeg(nc: u8, n: usize) -> Vec { + let mut j = vec![0xff, 0xd8, 0xff, 0xdb, 0x00, 0x43, 0x00]; + j.extend(std::iter::repeat_n(8u8, 64)); + j.extend_from_slice(&[0xff, 0xc0, 0x00, 8 + 3 * nc, 8, 0, 16, 0, 24, nc]); + for c in 0..nc { + j.extend_from_slice(&[c + 1, 0x11, 0]); + } + j.extend_from_slice(&[0xff, 0xda, 0x00, 6 + 2 * nc, nc]); + for c in 0..nc { + j.extend_from_slice(&[c + 1, 0]); + } + j.extend_from_slice(&[0, 63, 0]); + j.extend(std::iter::repeat_n(0x55u8, n)); + j.extend_from_slice(&[0xff, 0x00, 0x12, 0xff, 0xd0, 0x34, 0xff, 0xd9]); + j +} + +/// A crafted container full of JPEG-like starts (each walk running on through the ones after it) +/// is scanned in linear time: the walks share a budget (without it this 40 MB file takes about a minute). +#[test] +fn a_scan_through_many_fake_jpegs_stays_linear() { + let mut unit = vec![0u8, 0xd8, 0xff, 0xdb, 0x00, 0x02, 0xff, 0xda, 0x00, 0x02]; + unit.extend(std::iter::repeat_n(0x55u8, 190)); + let mut f = b"\0MRM\0\x01\0\0".to_vec(); + for _ in 0..200_000 { + f.extend_from_slice(&unit); + } + let t = std::time::Instant::now(); + assert_eq!(scan_for_jpeg(&f), None); + assert!(t.elapsed() < std::time::Duration::from_secs(10), "{:?}", t.elapsed()); +} + +/// Containers whose structure we do not walk give up their largest colour JPEG, found by scanning. +#[test] +fn scanned_containers_give_the_largest_colour_jpeg() { + let (small, big, mosaic) = (scan_jpeg(3, 100), scan_jpeg(3, 900), scan_jpeg(1, 5000)); + let heads: [&[u8]; 3] = [b"II\x1a\0\0\0HEAPCCDR", b"\0MRM\0\x01\0\0", b"FOVb\x02\0\x02\0"]; + for head in heads { + let mut f = head.to_vec(); + for part in [&small, &mosaic, &big] { + f.extend_from_slice(&[0xff, 0xd8, 0xff, 0x00, 0x00]); // a stray SOI-like run: not a JPEG + f.extend_from_slice(part); + f.extend_from_slice(&[0u8; 7]); + } + // MRW decodes; its preview is still found by the scan + assert!(crate::probe(&f).is_some_and(|p| !p.is_supported() || p == crate::RawFormat::Mrw), "{head:?}"); + assert_eq!(embedded_preview(&f).as_deref(), Some(&big[..]), "{head:?}"); + for n in (0..f.len()).step_by(11) { + let _ = embedded_preview(&f[..n]); + } + } + // a preview whose first byte was overwritten (Minolta and Epson files store 00, 02 or ee there) is restored + let mut f = b"\0MRM\0\x01\0\0".to_vec(); + f.push(0x02); + f.extend_from_slice(&big[1..]); + assert_eq!(embedded_preview(&f).as_deref(), Some(&big[..])); + // only a one-component JPEG, or none: nothing + let mut f = b"FOVb\x02\0\x02\0".to_vec(); + f.extend_from_slice(&mosaic); + assert_eq!(embedded_preview(&f), None); +}