1use std::collections::HashSet;
51use std::fs::File;
52use std::io::{BufReader, Read, Seek, SeekFrom};
53use std::path::Path;
54
55pub fn extract_largest_preview(path: &Path) -> Result<Option<Vec<u8>>, String> {
63 let file = File::open(path).map_err(|e| format!("open: {e}"))?;
64 let file_len = file.metadata().map_err(|e| format!("stat: {e}"))?.len();
65 let mut reader = TiffReader::new(BufReader::new(file), file_len)?;
66 let candidates = reader.find_candidates()?;
67 let Some(best) = candidates.into_iter().max_by_key(|c| c.length) else {
68 return Ok(None);
69 };
70 let bytes = reader
71 .read_at(best.offset, best.length)
72 .map_err(|e| format!("read preview bytes: {e}"))?;
73 Ok(Some(bytes))
74}
75
76#[derive(Debug, Clone, Copy)]
79struct PreviewCandidate {
80 offset: u64,
81 length: u64,
82}
83
84struct TiffReader {
85 file: BufReader<File>,
86 file_len: u64,
87 little_endian: bool,
88}
89
90const TAG_COMPRESSION: u16 = 0x0103;
92const TAG_STRIP_OFFSETS: u16 = 0x0111;
93const TAG_STRIP_BYTE_COUNTS: u16 = 0x0117;
94const TAG_JPEG_INTERCHANGE_FORMAT: u16 = 0x0201;
95const TAG_JPEG_INTERCHANGE_FORMAT_LENGTH: u16 = 0x0202;
96const TAG_SUB_IFDS: u16 = 0x014A;
97const TAG_EXIF_IFD: u16 = 0x8769;
98const TAG_GPS_IFD: u16 = 0x8825;
99const TAG_INTEROP_IFD: u16 = 0xA005;
100
101fn is_jpeg_compression(code: u32) -> bool {
105 code == 6 || code == 7
106}
107
108const MAX_IFDS: usize = 64;
112
113const MAX_PREVIEW_BYTES: u64 = 128 * 1024 * 1024;
117
118impl TiffReader {
119 fn new(mut file: BufReader<File>, file_len: u64) -> Result<Self, String> {
120 let mut header = [0u8; 8];
121 file.seek(SeekFrom::Start(0))
122 .map_err(|e| format!("seek header: {e}"))?;
123 file.read_exact(&mut header)
124 .map_err(|e| format!("read header: {e}"))?;
125 let little_endian = match &header[0..2] {
126 b"II" => true,
127 b"MM" => false,
128 _ => return Err("not a TIFF (bad byte-order mark)".into()),
129 };
130 let magic = read_u16(&header[2..4], little_endian);
134 if magic == 0 {
135 return Err("not a TIFF (zero magic)".into());
136 }
137 Ok(Self {
138 file,
139 file_len,
140 little_endian,
141 })
142 }
143
144 fn find_candidates(&mut self) -> Result<Vec<PreviewCandidate>, String> {
145 let mut header = [0u8; 8];
147 self.file
148 .seek(SeekFrom::Start(0))
149 .map_err(|e| format!("seek: {e}"))?;
150 self.file
151 .read_exact(&mut header)
152 .map_err(|e| format!("read: {e}"))?;
153 let ifd0 = read_u32(&header[4..8], self.little_endian) as u64;
154
155 let mut out: Vec<PreviewCandidate> = Vec::new();
156 let mut visited: HashSet<u64> = HashSet::new();
157 let mut count = 0usize;
158 self.walk(ifd0, &mut visited, &mut out, &mut count)?;
159 Ok(out)
160 }
161
162 fn walk(
163 &mut self,
164 ifd_offset: u64,
165 visited: &mut HashSet<u64>,
166 out: &mut Vec<PreviewCandidate>,
167 count: &mut usize,
168 ) -> Result<(), String> {
169 if ifd_offset == 0 || ifd_offset >= self.file_len {
170 return Ok(());
171 }
172 if !visited.insert(ifd_offset) {
173 return Ok(());
174 }
175 *count += 1;
176 if *count > MAX_IFDS {
177 return Ok(());
178 }
179
180 let entry_count = self.read_u16(ifd_offset)? as u64;
182 if entry_count > 4096 {
184 return Ok(());
185 }
186 let entries_start = ifd_offset + 2;
187 let next_ifd_pos = entries_start + entry_count * 12;
188 if next_ifd_pos + 4 > self.file_len {
189 return Ok(());
190 }
191
192 struct Entry {
196 tag: u16,
197 type_id: u16,
198 count: u32,
199 value: u32,
200 }
201 let mut entries: Vec<Entry> = Vec::with_capacity(entry_count as usize);
202 let mut buf = [0u8; 12];
203 for i in 0..entry_count {
204 self.file
205 .seek(SeekFrom::Start(entries_start + i * 12))
206 .map_err(|e| format!("seek entry: {e}"))?;
207 self.file
208 .read_exact(&mut buf)
209 .map_err(|e| format!("read entry: {e}"))?;
210 entries.push(Entry {
211 tag: read_u16(&buf[0..2], self.little_endian),
212 type_id: read_u16(&buf[2..4], self.little_endian),
213 count: read_u32(&buf[4..8], self.little_endian),
214 value: read_u32(&buf[8..12], self.little_endian),
215 });
216 }
217
218 let mut compression: Option<u32> = None;
223 let mut strip_offset: Option<u64> = None;
224 let mut strip_byte_count: Option<u64> = None;
225 let mut jpeg_offset: Option<u64> = None;
226 let mut jpeg_length: Option<u64> = None;
227 let mut sub_ifd_offsets: Vec<u64> = Vec::new();
228 let mut exif_sub_ifd: Option<u64> = None;
229
230 for e in &entries {
231 match e.tag {
232 TAG_COMPRESSION => {
233 compression = self.read_long_value(e.type_id, e.count, e.value).ok();
234 }
235 TAG_STRIP_OFFSETS if e.count == 1 => {
240 strip_offset = self
241 .read_long_value(e.type_id, e.count, e.value)
242 .ok()
243 .map(|v| v as u64);
244 }
245 TAG_STRIP_BYTE_COUNTS if e.count == 1 => {
246 strip_byte_count = self
247 .read_long_value(e.type_id, e.count, e.value)
248 .ok()
249 .map(|v| v as u64);
250 }
251 TAG_JPEG_INTERCHANGE_FORMAT => {
252 jpeg_offset = self
253 .read_long_value(e.type_id, e.count, e.value)
254 .ok()
255 .map(|v| v as u64);
256 }
257 TAG_JPEG_INTERCHANGE_FORMAT_LENGTH => {
258 jpeg_length = self
259 .read_long_value(e.type_id, e.count, e.value)
260 .ok()
261 .map(|v| v as u64);
262 }
263 TAG_SUB_IFDS => {
264 sub_ifd_offsets = self.read_long_array(e.count, e.value)?;
268 }
269 TAG_EXIF_IFD => {
270 exif_sub_ifd = Some(e.value as u64);
271 }
272 TAG_GPS_IFD | TAG_INTEROP_IFD => {
273 self.walk(e.value as u64, visited, out, count)?;
278 }
279 _ => {}
280 }
281 }
282
283 if let (Some(off), Some(len)) = (jpeg_offset, jpeg_length) {
285 if let Some(c) = self.bounded(off, len) {
286 out.push(c);
287 }
288 }
289 if let (Some(off), Some(len), Some(comp)) = (strip_offset, strip_byte_count, compression) {
291 if is_jpeg_compression(comp) {
292 if let Some(c) = self.bounded(off, len) {
293 out.push(c);
294 }
295 }
296 }
297
298 for sub in sub_ifd_offsets {
302 self.walk(sub, visited, out, count)?;
303 }
304 if let Some(off) = exif_sub_ifd {
305 self.walk(off, visited, out, count)?;
306 }
307
308 let next_ifd = self.read_u32(next_ifd_pos)? as u64;
312 self.walk(next_ifd, visited, out, count)?;
313
314 Ok(())
315 }
316
317 fn bounded(&self, offset: u64, length: u64) -> Option<PreviewCandidate> {
320 if length == 0 || offset == 0 {
321 return None;
322 }
323 if length > MAX_PREVIEW_BYTES {
324 return None;
325 }
326 let end = offset.checked_add(length)?;
327 if end > self.file_len {
328 return None;
329 }
330 Some(PreviewCandidate { offset, length })
331 }
332
333 fn read_u16(&mut self, offset: u64) -> Result<u16, String> {
334 let mut buf = [0u8; 2];
335 self.file
336 .seek(SeekFrom::Start(offset))
337 .map_err(|e| format!("seek u16: {e}"))?;
338 self.file
339 .read_exact(&mut buf)
340 .map_err(|e| format!("read u16: {e}"))?;
341 Ok(read_u16(&buf, self.little_endian))
342 }
343
344 fn read_u32(&mut self, offset: u64) -> Result<u32, String> {
345 let mut buf = [0u8; 4];
346 self.file
347 .seek(SeekFrom::Start(offset))
348 .map_err(|e| format!("seek u32: {e}"))?;
349 self.file
350 .read_exact(&mut buf)
351 .map_err(|e| format!("read u32: {e}"))?;
352 Ok(read_u32(&buf, self.little_endian))
353 }
354
355 fn read_long_value(&mut self, type_id: u16, count: u32, value: u32) -> Result<u32, String> {
360 if count != 1 {
362 return Err(format!("expected count=1, got {count}"));
363 }
364 match type_id {
365 3 => Ok(value & 0xFFFF), 4 => Ok(value), _ => Err(format!("unsupported type {type_id}")),
368 }
369 }
370
371 fn read_long_array(&mut self, count: u32, value: u32) -> Result<Vec<u64>, String> {
373 if count == 0 {
374 return Ok(Vec::new());
375 }
376 if count == 1 {
377 return Ok(vec![value as u64]);
378 }
379 let bytes_needed = (count as u64) * 4;
381 if (value as u64) + bytes_needed > self.file_len {
382 return Ok(Vec::new()); }
384 let mut buf = vec![0u8; bytes_needed as usize];
385 self.file
386 .seek(SeekFrom::Start(value as u64))
387 .map_err(|e| format!("seek long-array: {e}"))?;
388 self.file
389 .read_exact(&mut buf)
390 .map_err(|e| format!("read long-array: {e}"))?;
391 let mut out = Vec::with_capacity(count as usize);
392 for i in 0..count as usize {
393 let off = read_u32(&buf[i * 4..i * 4 + 4], self.little_endian);
394 out.push(off as u64);
395 }
396 Ok(out)
397 }
398
399 fn read_at(&mut self, offset: u64, length: u64) -> std::io::Result<Vec<u8>> {
400 self.file.seek(SeekFrom::Start(offset))?;
401 let mut buf = vec![0u8; length as usize];
402 self.file.read_exact(&mut buf)?;
403 Ok(buf)
404 }
405}
406
407fn read_u16(b: &[u8], little_endian: bool) -> u16 {
408 if little_endian {
409 u16::from_le_bytes([b[0], b[1]])
410 } else {
411 u16::from_be_bytes([b[0], b[1]])
412 }
413}
414
415fn read_u32(b: &[u8], little_endian: bool) -> u32 {
416 if little_endian {
417 u32::from_le_bytes([b[0], b[1], b[2], b[3]])
418 } else {
419 u32::from_be_bytes([b[0], b[1], b[2], b[3]])
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426 use std::io::Write;
427
428 fn make_tiff(blob_lengths: &[u32]) -> Vec<u8> {
435 let n = blob_lengths.len() as u64;
448 let mut bytes: Vec<u8> = Vec::new();
449
450 bytes.extend_from_slice(b"II");
452 bytes.extend_from_slice(&42u16.to_le_bytes());
453 bytes.extend_from_slice(&8u32.to_le_bytes()); let ifd_size: u64 = 30;
458 let ifds_total: u64 = ifd_size * n;
459 let blobs_start: u64 = 8 + ifds_total;
460
461 let mut blob_offsets: Vec<u64> = Vec::with_capacity(n as usize);
463 let mut cursor = blobs_start;
464 for &len in blob_lengths {
465 blob_offsets.push(cursor);
466 cursor += len as u64;
467 }
468
469 for (i, (&len, &off)) in blob_lengths.iter().zip(&blob_offsets).enumerate() {
471 bytes.extend_from_slice(&2u16.to_le_bytes());
473 bytes.extend_from_slice(&TAG_JPEG_INTERCHANGE_FORMAT.to_le_bytes());
475 bytes.extend_from_slice(&4u16.to_le_bytes());
476 bytes.extend_from_slice(&1u32.to_le_bytes());
477 bytes.extend_from_slice(&(off as u32).to_le_bytes());
478 bytes.extend_from_slice(&TAG_JPEG_INTERCHANGE_FORMAT_LENGTH.to_le_bytes());
480 bytes.extend_from_slice(&4u16.to_le_bytes());
481 bytes.extend_from_slice(&1u32.to_le_bytes());
482 bytes.extend_from_slice(&len.to_le_bytes());
483 let next: u32 = if i + 1 < blob_lengths.len() {
485 (8 + ifd_size * (i as u64 + 1)) as u32
486 } else {
487 0
488 };
489 bytes.extend_from_slice(&next.to_le_bytes());
490 }
491
492 for (i, &len) in blob_lengths.iter().enumerate() {
494 let filler = (i as u8).wrapping_add(0xA0);
497 for _ in 0..len {
498 bytes.push(filler);
499 }
500 }
501 bytes
502 }
503
504 fn write_tiff(bytes: &[u8]) -> tempfile::NamedTempFile {
505 let mut f = tempfile::NamedTempFile::new().unwrap();
506 f.as_file_mut().write_all(bytes).unwrap();
507 f.as_file_mut().sync_all().unwrap();
508 f
509 }
510
511 #[test]
512 fn single_preview_is_returned() {
513 let blob_len: u32 = 64;
514 let tiff = make_tiff(&[blob_len]);
515 let f = write_tiff(&tiff);
516 let bytes = extract_largest_preview(f.path()).unwrap().unwrap();
517 assert_eq!(bytes.len(), blob_len as usize);
518 assert!(bytes.iter().all(|&b| b == 0xA0));
520 }
521
522 #[test]
523 fn largest_of_multiple_wins() {
524 let tiff = make_tiff(&[64, 256, 32]);
526 let f = write_tiff(&tiff);
527 let bytes = extract_largest_preview(f.path()).unwrap().unwrap();
528 assert_eq!(bytes.len(), 256);
529 assert!(bytes.iter().all(|&b| b == 0xA1));
531 }
532
533 #[test]
534 fn no_preview_returns_none() {
535 let mut bytes: Vec<u8> = Vec::new();
537 bytes.extend_from_slice(b"II");
538 bytes.extend_from_slice(&42u16.to_le_bytes());
539 bytes.extend_from_slice(&8u32.to_le_bytes());
540 bytes.extend_from_slice(&0u16.to_le_bytes());
542 bytes.extend_from_slice(&0u32.to_le_bytes());
543 let f = write_tiff(&bytes);
544 assert!(extract_largest_preview(f.path()).unwrap().is_none());
545 }
546
547 #[test]
548 fn bad_header_errors() {
549 let mut f = tempfile::NamedTempFile::new().unwrap();
550 f.as_file_mut().write_all(b"NOTATIFF").unwrap();
551 f.as_file_mut().sync_all().unwrap();
552 assert!(extract_largest_preview(f.path()).is_err());
553 }
554
555 #[test]
556 fn out_of_bounds_offset_is_skipped() {
557 let mut bytes: Vec<u8> = Vec::new();
560 bytes.extend_from_slice(b"II");
561 bytes.extend_from_slice(&42u16.to_le_bytes());
562 bytes.extend_from_slice(&8u32.to_le_bytes());
563 bytes.extend_from_slice(&2u16.to_le_bytes());
565 bytes.extend_from_slice(&TAG_JPEG_INTERCHANGE_FORMAT.to_le_bytes());
566 bytes.extend_from_slice(&4u16.to_le_bytes());
567 bytes.extend_from_slice(&1u32.to_le_bytes());
568 bytes.extend_from_slice(&10_000_000u32.to_le_bytes());
569 bytes.extend_from_slice(&TAG_JPEG_INTERCHANGE_FORMAT_LENGTH.to_le_bytes());
570 bytes.extend_from_slice(&4u16.to_le_bytes());
571 bytes.extend_from_slice(&1u32.to_le_bytes());
572 bytes.extend_from_slice(&1024u32.to_le_bytes());
573 bytes.extend_from_slice(&0u32.to_le_bytes()); let f = write_tiff(&bytes);
575 assert!(extract_largest_preview(f.path()).unwrap().is_none());
576 }
577
578 #[test]
579 fn oversized_preview_length_is_skipped_without_allocation() {
580 let mut bytes: Vec<u8> = Vec::new();
581 bytes.extend_from_slice(b"II");
582 bytes.extend_from_slice(&42u16.to_le_bytes());
583 bytes.extend_from_slice(&8u32.to_le_bytes());
584 bytes.extend_from_slice(&2u16.to_le_bytes());
585 bytes.extend_from_slice(&TAG_JPEG_INTERCHANGE_FORMAT.to_le_bytes());
586 bytes.extend_from_slice(&4u16.to_le_bytes());
587 bytes.extend_from_slice(&1u32.to_le_bytes());
588 bytes.extend_from_slice(&64u32.to_le_bytes());
589 bytes.extend_from_slice(&TAG_JPEG_INTERCHANGE_FORMAT_LENGTH.to_le_bytes());
590 bytes.extend_from_slice(&4u16.to_le_bytes());
591 bytes.extend_from_slice(&1u32.to_le_bytes());
592 bytes.extend_from_slice(&((MAX_PREVIEW_BYTES + 1) as u32).to_le_bytes());
593 bytes.extend_from_slice(&0u32.to_le_bytes());
594
595 let mut f = write_tiff(&bytes);
596 f.as_file_mut().set_len(64 + MAX_PREVIEW_BYTES + 1).unwrap();
597
598 assert!(extract_largest_preview(f.path()).unwrap().is_none());
599 }
600}