1use crate::bytes::{Reader, u32_to_usize};
42use crate::detect::Format;
43use crate::error::{MalformedDetail, ResourceLimit, Result, StryptError};
44use crate::formats::exif::{Endian, Ifd};
45use crate::formats::{MetadataHandler, ParseLimits, StripOptions, Stripped, exif};
46
47pub(crate) mod tags;
48use crate::report::{Finding, InspectOptions, MetadataKind, MetadataReport, Note, StripReport};
49
50#[derive(Debug, Clone, Copy, Default)]
52#[non_exhaustive]
53pub struct TiffHandler;
54
55impl MetadataHandler for TiffHandler {
56 fn name(&self) -> &'static str {
57 Format::Tiff.id()
58 }
59
60 fn format(&self) -> Format {
61 Format::Tiff
62 }
63
64 fn inspect(&self, input: &[u8], options: &InspectOptions) -> Result<MetadataReport> {
65 let processed = process(input, options, &ParseLimits::default())?;
70 Ok(MetadataReport {
71 format: Format::Tiff,
72 findings: processed.findings,
73 notes: processed.notes,
74 })
75 }
76
77 fn strip(&self, input: &[u8], options: &StripOptions) -> Result<Stripped> {
78 let processed = process(input, &options.inspect, &options.limits)?;
79 Ok(Stripped {
80 report: StripReport {
81 format: Format::Tiff,
82 removed: processed.findings,
83 retained: Vec::new(),
84 notes: processed.notes,
85 input_bytes: as_u64(input.len()),
86 output_bytes: as_u64(processed.output.len()),
87 },
88 bytes: processed.output,
89 })
90 }
91}
92
93struct Processed {
95 output: Vec<u8>,
96 findings: Vec<Finding>,
97 notes: Vec<Note>,
98}
99
100fn process(input: &[u8], options: &InspectOptions, limits: &ParseLimits) -> Result<Processed> {
102 let (endian, first) = header(input)?;
103
104 let mut walk = Walk {
105 input,
106 endian,
107 options,
108 budget: limits.max_items,
109 visited: Vec::new(),
110 findings: Vec::new(),
111 notes: Vec::new(),
112 };
113
114 let mut pages: Vec<Page<'_>> = Vec::new();
119 let mut next = first;
120 while next != 0 {
121 let start =
122 u32_to_usize(next).ok_or_else(|| malformed(MalformedDetail::LengthOutOfRange))?;
123 if walk.visited.contains(&start) {
124 return Err(malformed(MalformedDetail::CyclicReference));
125 }
126 walk.visited.push(start);
127 let (page, following) = walk.directory(start)?;
128 if let Some(page) = page {
129 pages.push(page);
130 }
131 next = following;
132 }
133
134 if pages.is_empty() {
135 return Err(malformed(MalformedDetail::MissingMarker));
139 }
140
141 let output = write(endian, &pages)?;
142 Ok(Processed {
143 output,
144 findings: walk.findings,
145 notes: walk.notes,
146 })
147}
148
149fn header(input: &[u8]) -> Result<(Endian, u32)> {
155 let mut r = Reader::new(input);
156 let endian = match r.take(2) {
157 Some(b"II") => Endian::Little,
158 Some(b"MM") => Endian::Big,
159 _ => return Err(malformed(MalformedDetail::MissingMarker)),
160 };
161 match endian.u16(&mut r) {
164 Some(42) => {}
165 Some(43) => return Err(malformed(MalformedDetail::UnsupportedFeature)),
166 _ => return Err(malformed(MalformedDetail::MissingMarker)),
167 }
168 let first = endian
169 .u32(&mut r)
170 .ok_or_else(|| malformed(MalformedDetail::Truncated))?;
171 Ok((endian, first))
172}
173
174struct Page<'a> {
176 verbatim: Vec<Verbatim<'a>>,
178 data: Vec<&'a [u8]>,
180 tiled: bool,
182}
183
184struct Verbatim<'a> {
186 tag: u16,
187 field_type: u16,
188 count: u32,
189 value: &'a [u8],
190}
191
192struct Walk<'a, 'o> {
196 input: &'a [u8],
197 endian: Endian,
198 options: &'o InspectOptions,
199 budget: u32,
200 visited: Vec<usize>,
201 findings: Vec<Finding>,
202 notes: Vec<Note>,
203}
204
205struct RawEntry<'a> {
207 tag: u16,
208 field_type: u16,
209 count: u32,
210 inline: &'a [u8],
212}
213
214impl<'a> Walk<'a, '_> {
215 fn directory(&mut self, start: usize) -> Result<(Option<Page<'a>>, u32)> {
217 let mut r = Reader::new(self.input);
218 r.seek(start)
219 .ok_or_else(|| malformed(MalformedDetail::LengthOutOfRange))?;
220 let count = self
221 .endian
222 .u16(&mut r)
223 .ok_or_else(|| malformed(MalformedDetail::Truncated))?;
224
225 let mut raw: Vec<RawEntry<'a>> = Vec::new();
226 for _ in 0..count {
227 if self.budget == 0 {
228 return Err(StryptError::LimitExceeded {
229 format: Format::Tiff,
230 limit: ResourceLimit::ItemCount,
231 });
232 }
233 self.budget = self.budget.saturating_sub(1);
234 raw.push(self.raw_entry(&mut r)?);
235 }
236 let following = self
237 .endian
238 .u32(&mut r)
239 .ok_or_else(|| malformed(MalformedDetail::Truncated))?;
240
241 if self.is_reduced_resolution(&raw) {
245 self.findings.push(
246 Finding::new(MetadataKind::Thumbnail, "TIFF reduced-resolution IFD", 0)
247 .with_field("reduced-resolution image"),
248 );
249 return Ok((None, following));
250 }
251
252 let page = self.page(&raw)?;
253 Ok((Some(page), following))
254 }
255
256 fn raw_entry(&mut self, r: &mut Reader<'a>) -> Result<RawEntry<'a>> {
258 let truncated = || malformed(MalformedDetail::Truncated);
259 let tag = self.endian.u16(r).ok_or_else(truncated)?;
260 let field_type = self.endian.u16(r).ok_or_else(truncated)?;
261 let count = self.endian.u32(r).ok_or_else(truncated)?;
262 let inline = r.take(4).ok_or_else(truncated)?;
263 Ok(RawEntry {
264 tag,
265 field_type,
266 count,
267 inline,
268 })
269 }
270
271 fn is_reduced_resolution(&self, raw: &[RawEntry<'a>]) -> bool {
273 raw.iter()
274 .find(|e| e.tag == tags::NEW_SUBFILE_TYPE)
275 .and_then(|e| self.value_of(e))
276 .and_then(|v| integer(self.endian, v, tags::LONG))
277 .is_some_and(|v| v & 1 == 1)
278 }
279
280 fn value_of(&self, entry: &RawEntry<'a>) -> Option<&'a [u8]> {
285 let size = exif::type_size(entry.field_type)?;
286 let length = u64::from(entry.count).checked_mul(u64::from(size))?;
287 let length = usize::try_from(length).ok()?;
288 if length <= 4 {
289 return entry.inline.get(0..length);
290 }
291 let mut w = Reader::new(entry.inline);
292 let at = u32_to_usize(self.endian.u32(&mut w)?)?;
293 let mut r = Reader::new(self.input);
294 r.seek(at)?;
295 r.take(length)
296 }
297
298 fn page(&mut self, raw: &[RawEntry<'a>]) -> Result<Page<'a>> {
300 let tiled = raw.iter().any(|e| e.tag == tags::TILE_OFFSETS);
301
302 let mut verbatim: Vec<Verbatim<'a>> = Vec::new();
303 let mut offsets: Option<Vec<u64>> = None;
304 let mut counts: Option<Vec<u64>> = None;
305
306 for entry in raw {
307 let value = self.value_of(entry);
308
309 let (offsets_tag, counts_tag) = geometry_tags(tiled);
312 if entry.tag == offsets_tag {
313 offsets = Some(self.integers(entry, value)?);
314 continue;
315 }
316 if entry.tag == counts_tag {
317 counts = Some(self.integers(entry, value)?);
318 continue;
319 }
320 if matches!(
324 entry.tag,
325 tags::STRIP_OFFSETS
326 | tags::STRIP_BYTE_COUNTS
327 | tags::TILE_OFFSETS
328 | tags::TILE_BYTE_COUNTS
329 ) {
330 continue;
331 }
332
333 if tags::is_structural(entry.tag) {
334 if verbatim.iter().any(|v| v.tag == entry.tag) {
337 continue;
338 }
339 let value = value.ok_or_else(|| malformed(MalformedDetail::LengthOutOfRange))?;
340 verbatim.push(Verbatim {
341 tag: entry.tag,
342 field_type: entry.field_type,
343 count: entry.count,
344 value,
345 });
346 continue;
347 }
348
349 if let Some(sub) = exif::sub_directory(Ifd::Primary, entry.tag) {
353 self.report_sub_directory(entry, value, sub);
354 continue;
355 }
356 self.report_removed(Ifd::Primary, entry, value);
357 }
358
359 let offsets = offsets.ok_or_else(|| malformed(MalformedDetail::MissingMarker))?;
360 let counts = counts.ok_or_else(|| malformed(MalformedDetail::MissingMarker))?;
361 let data = self.slice_data(&offsets, &counts)?;
362
363 for required in [tags::IMAGE_WIDTH, tags::IMAGE_LENGTH] {
366 if !verbatim.iter().any(|v| v.tag == required) {
367 return Err(malformed(MalformedDetail::MissingMarker));
368 }
369 }
370
371 Ok(Page {
372 verbatim,
373 data,
374 tiled,
375 })
376 }
377
378 fn integers(&self, entry: &RawEntry<'a>, value: Option<&'a [u8]>) -> Result<Vec<u64>> {
380 let value = value.ok_or_else(|| malformed(MalformedDetail::LengthOutOfRange))?;
381 let width = match entry.field_type {
382 tags::SHORT => 2usize,
383 tags::LONG => 4usize,
384 _ => return Err(malformed(MalformedDetail::UnsupportedFeature)),
388 };
389 let mut out = Vec::new();
390 let mut r = Reader::new(value);
391 while r.remaining() >= width {
392 let item = if width == 2 {
393 self.endian.u16(&mut r).map(u64::from)
394 } else {
395 self.endian.u32(&mut r).map(u64::from)
396 };
397 out.push(item.ok_or_else(|| malformed(MalformedDetail::Truncated))?);
398 }
399 Ok(out)
400 }
401
402 fn slice_data(&self, offsets: &[u64], counts: &[u64]) -> Result<Vec<&'a [u8]>> {
404 if offsets.is_empty() || offsets.len() != counts.len() {
405 return Err(malformed(MalformedDetail::BrokenIndex));
406 }
407 let mut data = Vec::new();
408 for (offset, count) in offsets.iter().zip(counts.iter()) {
409 let start = usize::try_from(*offset)
410 .map_err(|_| malformed(MalformedDetail::LengthOutOfRange))?;
411 let len = usize::try_from(*count)
412 .map_err(|_| malformed(MalformedDetail::LengthOutOfRange))?;
413 let mut r = Reader::new(self.input);
414 r.seek(start)
415 .ok_or_else(|| malformed(MalformedDetail::LengthOutOfRange))?;
416 let slice = r
417 .take(len)
418 .ok_or_else(|| malformed(MalformedDetail::LengthOutOfRange))?;
419 data.push(slice);
420 }
421 Ok(data)
422 }
423
424 fn report_sub_directory(&mut self, entry: &RawEntry<'a>, value: Option<&'a [u8]>, sub: Ifd) {
426 let Some(target) = value
427 .or(Some(entry.inline))
428 .and_then(|v| {
429 let mut r = Reader::new(v);
430 self.endian.u32(&mut r)
431 })
432 .and_then(u32_to_usize)
433 else {
434 return;
435 };
436 if self.visited.contains(&target) {
437 return;
438 }
439 self.visited.push(target);
440
441 let mut r = Reader::new(self.input);
442 if r.seek(target).is_none() {
443 return;
444 }
445 let Some(count) = self.endian.u16(&mut r) else {
446 return;
447 };
448 for _ in 0..count {
449 if self.budget == 0 {
450 self.notes.push(Note::UnparsedRegion {
451 location: format!("TIFF {}", label(sub)),
452 bytes: as_u64(r.remaining()),
453 });
454 return;
455 }
456 self.budget = self.budget.saturating_sub(1);
457 let Ok(sub_entry) = self.raw_entry(&mut r) else {
458 return;
459 };
460 let sub_value = self.value_of(&sub_entry);
461 if let Some(deeper) = exif::sub_directory(sub, sub_entry.tag) {
462 self.report_sub_directory(&sub_entry, sub_value, deeper);
463 continue;
464 }
465 self.report_removed(sub, &sub_entry, sub_value);
466 }
467 }
468
469 fn report_removed(&mut self, ifd: Ifd, entry: &RawEntry<'a>, value: Option<&'a [u8]>) {
471 let (name, kind) = exif::describe(ifd, entry.tag);
472 let bytes = value.map_or(0, |v| as_u64(v.len()));
473 let field_type = entry.field_type;
474 self.findings.push(
475 Finding::new(kind, format!("TIFF {}", label(ifd)), bytes)
476 .with_field(name)
477 .with_value(self.options, || exif::render(value, field_type)),
478 );
479 }
480}
481
482const fn label(ifd: Ifd) -> &'static str {
484 match ifd {
485 Ifd::Primary => "IFD0",
486 Ifd::Exif => "Exif IFD",
487 Ifd::Gps => "GPS IFD",
488 Ifd::Interop => "Interop IFD",
489 Ifd::Thumbnail => "IFD1 (thumbnail)",
490 }
491}
492
493const fn geometry_tags(tiled: bool) -> (u16, u16) {
495 if tiled {
496 (tags::TILE_OFFSETS, tags::TILE_BYTE_COUNTS)
497 } else {
498 (tags::STRIP_OFFSETS, tags::STRIP_BYTE_COUNTS)
499 }
500}
501
502fn integer(endian: Endian, value: &[u8], field_type: u16) -> Option<u64> {
504 let mut r = Reader::new(value);
505 match field_type {
506 tags::SHORT => endian.u16(&mut r).map(u64::from),
507 _ => endian.u32(&mut r).map(u64::from),
508 }
509}
510
511struct OutEntry {
517 tag: u16,
518 field_type: u16,
519 count: u32,
520 len: usize,
524 source: Source,
525}
526
527enum Source {
529 Verbatim(usize),
531 DataOffsets,
533 DataCounts,
535}
536
537struct Placed {
539 entries: Vec<OutEntry>,
540 ifd_at: u32,
541 value_at: Vec<u32>,
543 data_at: Vec<u32>,
544}
545
546fn write(endian: Endian, pages: &[Page<'_>]) -> Result<Vec<u8>> {
552 let mut cursor: u64 = 8;
553 let mut placed: Vec<Placed> = Vec::new();
554
555 for page in pages {
556 let entries = out_entries(page);
557 let ifd_at = to_u32(cursor)?;
558 let ifd_size = u64::try_from(entries.len())
561 .ok()
562 .and_then(|n| n.checked_mul(12))
563 .and_then(|n| n.checked_add(6))
564 .ok_or_else(|| malformed(MalformedDetail::LengthOutOfRange))?;
565 cursor = advance(cursor, ifd_size)?;
566
567 let mut value_at = Vec::with_capacity(entries.len());
568 for entry in &entries {
569 if entry.len <= 4 {
570 value_at.push(0);
571 continue;
572 }
573 value_at.push(to_u32(cursor)?);
574 cursor = advance(cursor, pad_even(width_of(entry.len)?))?;
575 }
576
577 let mut data_at = Vec::with_capacity(page.data.len());
578 for blob in &page.data {
579 data_at.push(to_u32(cursor)?);
580 cursor = advance(cursor, pad_even(width_of(blob.len())?))?;
581 }
582
583 placed.push(Placed {
584 entries,
585 ifd_at,
586 value_at,
587 data_at,
588 });
589 }
590
591 let mut out: Vec<u8> = Vec::new();
592 out.extend_from_slice(match endian {
593 Endian::Little => b"II",
594 Endian::Big => b"MM",
595 });
596 put_u16(&mut out, endian, 42);
597 put_u32(
598 &mut out,
599 endian,
600 placed.first().map_or(0, |first| first.ifd_at),
601 );
602
603 for (index, page) in pages.iter().enumerate() {
604 let Some(spot) = placed.get(index) else {
605 return Err(malformed(MalformedDetail::NotRoundTrippable));
606 };
607 let next = placed.get(index.saturating_add(1)).map_or(0, |p| p.ifd_at);
609 write_page(&mut out, endian, page, spot, next)?;
610 }
611 Ok(out)
612}
613
614fn out_entries(page: &Page<'_>) -> Vec<OutEntry> {
616 let mut entries: Vec<OutEntry> = page
617 .verbatim
618 .iter()
619 .enumerate()
620 .map(|(index, v)| OutEntry {
621 tag: v.tag,
622 field_type: v.field_type,
623 count: v.count,
624 len: v.value.len(),
625 source: Source::Verbatim(index),
626 })
627 .collect();
628
629 let (offsets_tag, counts_tag) = geometry_tags(page.tiled);
630 let n = page.data.len();
631 let n32 = u32::try_from(n).unwrap_or(u32::MAX);
635 entries.push(OutEntry {
639 tag: offsets_tag,
640 field_type: tags::LONG,
641 count: n32,
642 len: n.saturating_mul(4),
643 source: Source::DataOffsets,
644 });
645 entries.push(OutEntry {
646 tag: counts_tag,
647 field_type: tags::LONG,
648 count: n32,
649 len: n.saturating_mul(4),
650 source: Source::DataCounts,
651 });
652
653 entries.sort_by_key(|e| e.tag);
655 entries
656}
657
658fn write_page(
660 out: &mut Vec<u8>,
661 endian: Endian,
662 page: &Page<'_>,
663 spot: &Placed,
664 next: u32,
665) -> Result<()> {
666 let entry_count =
667 u16::try_from(spot.entries.len()).map_err(|_| malformed(MalformedDetail::BrokenIndex))?;
668 put_u16(out, endian, entry_count);
669 for (index, entry) in spot.entries.iter().enumerate() {
670 put_u16(out, endian, entry.tag);
671 put_u16(out, endian, entry.field_type);
672 put_u32(out, endian, entry.count);
673 if entry.len <= 4 {
674 let mut inline = value_bytes(endian, page, spot, entry)?;
675 inline.resize(4, 0);
676 out.extend_from_slice(&inline);
677 } else {
678 let at = spot
679 .value_at
680 .get(index)
681 .copied()
682 .ok_or_else(|| malformed(MalformedDetail::NotRoundTrippable))?;
683 put_u32(out, endian, at);
684 }
685 }
686 put_u32(out, endian, next);
687
688 for entry in &spot.entries {
689 if entry.len <= 4 {
690 continue;
691 }
692 let bytes = value_bytes(endian, page, spot, entry)?;
693 out.extend_from_slice(&bytes);
694 if !bytes.len().is_multiple_of(2) {
695 out.push(0);
696 }
697 }
698
699 for blob in &page.data {
700 out.extend_from_slice(blob);
701 if !blob.len().is_multiple_of(2) {
702 out.push(0);
703 }
704 }
705 Ok(())
706}
707
708fn value_bytes(
710 endian: Endian,
711 page: &Page<'_>,
712 spot: &Placed,
713 entry: &OutEntry,
714) -> Result<Vec<u8>> {
715 match entry.source {
716 Source::Verbatim(index) => page
717 .verbatim
718 .get(index)
719 .map(|v| v.value.to_vec())
720 .ok_or_else(|| malformed(MalformedDetail::NotRoundTrippable)),
721 Source::DataOffsets => {
722 let mut bytes = Vec::new();
723 for at in &spot.data_at {
724 put_u32(&mut bytes, endian, *at);
725 }
726 Ok(bytes)
727 }
728 Source::DataCounts => {
729 let mut bytes = Vec::new();
730 for blob in &page.data {
731 put_u32(&mut bytes, endian, to_u32(width_of(blob.len())?)?);
732 }
733 Ok(bytes)
734 }
735 }
736}
737
738const fn pad_even(len: u64) -> u64 {
740 if len.is_multiple_of(2) {
741 len
742 } else {
743 len.saturating_add(1)
744 }
745}
746
747fn advance(cursor: u64, by: u64) -> Result<u64> {
749 cursor
750 .checked_add(by)
751 .filter(|next| u32::try_from(*next).is_ok())
752 .ok_or_else(|| malformed(MalformedDetail::LengthOutOfRange))
753}
754
755fn width_of(len: usize) -> Result<u64> {
757 u64::try_from(len).map_err(|_| malformed(MalformedDetail::LengthOutOfRange))
758}
759
760fn to_u32(value: u64) -> Result<u32> {
762 u32::try_from(value).map_err(|_| malformed(MalformedDetail::LengthOutOfRange))
763}
764
765fn put_u16(out: &mut Vec<u8>, endian: Endian, value: u16) {
766 match endian {
767 Endian::Little => out.extend_from_slice(&value.to_le_bytes()),
768 Endian::Big => out.extend_from_slice(&value.to_be_bytes()),
769 }
770}
771
772fn put_u32(out: &mut Vec<u8>, endian: Endian, value: u32) {
773 match endian {
774 Endian::Little => out.extend_from_slice(&value.to_le_bytes()),
775 Endian::Big => out.extend_from_slice(&value.to_be_bytes()),
776 }
777}
778
779fn malformed(detail: MalformedDetail) -> StryptError {
780 StryptError::Malformed {
781 format: Format::Tiff,
782 offset: None,
783 detail,
784 }
785}
786
787fn as_u64(value: usize) -> u64 {
788 u64::try_from(value).unwrap_or(u64::MAX)
789}
790
791#[cfg(test)]
792mod tests {
793 #![allow(
796 clippy::unwrap_used,
797 clippy::expect_used,
798 clippy::indexing_slicing,
799 clippy::arithmetic_side_effects,
800 clippy::struct_field_names,
801 clippy::too_many_lines
802 )]
803
804 use super::*;
805 use crate::formats::StripOptions;
806
807 struct Tag {
809 tag: u16,
810 field_type: u16,
811 count: u32,
812 value: Vec<u8>,
813 }
814
815 fn short(tag: u16, value: u16) -> Tag {
816 Tag {
817 tag,
818 field_type: tags::SHORT,
819 count: 1,
820 value: value.to_le_bytes().to_vec(),
821 }
822 }
823
824 fn ascii(tag: u16, text: &str) -> Tag {
825 let mut value = text.as_bytes().to_vec();
826 value.push(0);
827 Tag {
828 tag,
829 field_type: 2,
830 count: u32::try_from(value.len()).expect("short string"),
831 value,
832 }
833 }
834
835 fn long(tag: u16, value: u32) -> Tag {
836 Tag {
837 tag,
838 field_type: tags::LONG,
839 count: 1,
840 value: value.to_le_bytes().to_vec(),
841 }
842 }
843
844 fn structural() -> Vec<Tag> {
846 vec![
847 short(tags::IMAGE_WIDTH, 4),
848 short(tags::IMAGE_LENGTH, 1),
849 short(0x0102, 8), short(0x0103, 1), short(0x0106, 1), short(0x0115, 1), short(0x0116, 1), short(0x011C, 1), ]
856 }
857
858 struct Dir {
860 tags: Vec<Tag>,
861 strips: Vec<Vec<u8>>,
862 }
863
864 fn page(tags: Vec<Tag>, strip: &[u8]) -> Dir {
865 Dir {
866 tags,
867 strips: vec![strip.to_vec()],
868 }
869 }
870
871 struct Slot {
873 tag: u16,
874 field_type: u16,
875 count: u32,
876 value: Vec<u8>,
877 geometry: Option<bool>,
880 }
881
882 fn build(dirs: &[Dir]) -> Vec<u8> {
886 let mut per_dir: Vec<Vec<Slot>> = Vec::new();
887 for dir in dirs {
888 let n = u32::try_from(dir.strips.len()).unwrap();
889 let mut entries: Vec<Slot> = dir
890 .tags
891 .iter()
892 .map(|t| Slot {
893 tag: t.tag,
894 field_type: t.field_type,
895 count: t.count,
896 value: t.value.clone(),
897 geometry: None,
898 })
899 .collect();
900 entries.push(Slot {
901 tag: tags::STRIP_OFFSETS,
902 field_type: tags::LONG,
903 count: n,
904 value: vec![0; 4 * dir.strips.len()],
905 geometry: Some(true),
906 });
907 entries.push(Slot {
908 tag: tags::STRIP_BYTE_COUNTS,
909 field_type: tags::LONG,
910 count: n,
911 value: vec![0; 4 * dir.strips.len()],
912 geometry: Some(false),
913 });
914 entries.sort_by_key(|e| e.tag);
915 per_dir.push(entries);
916 }
917
918 let mut cursor = 8usize;
919 let mut starts: Vec<usize> = Vec::new();
920 let mut value_offs: Vec<Vec<usize>> = Vec::new();
921 let mut data_offs: Vec<Vec<usize>> = Vec::new();
922 for (index, entries) in per_dir.iter().enumerate() {
923 starts.push(cursor);
924 cursor += 2 + 12 * entries.len() + 4;
925 let mut offs = Vec::new();
926 for entry in entries {
927 if entry.value.len() > 4 {
928 offs.push(cursor);
929 cursor += entry.value.len() + entry.value.len() % 2;
930 } else {
931 offs.push(0);
932 }
933 }
934 value_offs.push(offs);
935 let mut data = Vec::new();
936 for strip in &dirs[index].strips {
937 data.push(cursor);
938 cursor += strip.len() + strip.len() % 2;
939 }
940 data_offs.push(data);
941 }
942
943 for (index, entries) in per_dir.iter_mut().enumerate() {
944 for entry in entries.iter_mut() {
945 match entry.geometry {
946 Some(true) => {
947 entry.value = data_offs[index]
948 .iter()
949 .flat_map(|at| u32::try_from(*at).unwrap().to_le_bytes())
950 .collect();
951 }
952 Some(false) => {
953 entry.value = dirs[index]
954 .strips
955 .iter()
956 .flat_map(|s| u32::try_from(s.len()).unwrap().to_le_bytes())
957 .collect();
958 }
959 None => {}
960 }
961 }
962 }
963
964 let mut out: Vec<u8> = vec![b'I', b'I', 0x2A, 0x00];
965 out.extend_from_slice(&u32::try_from(starts[0]).unwrap().to_le_bytes());
966 for (index, entries) in per_dir.iter().enumerate() {
967 out.extend_from_slice(&u16::try_from(entries.len()).unwrap().to_le_bytes());
968 for (slot, entry) in entries.iter().enumerate() {
969 out.extend_from_slice(&entry.tag.to_le_bytes());
970 out.extend_from_slice(&entry.field_type.to_le_bytes());
971 out.extend_from_slice(&entry.count.to_le_bytes());
972 if entry.value.len() <= 4 {
973 let mut inline = entry.value.clone();
974 inline.resize(4, 0);
975 out.extend_from_slice(&inline);
976 } else {
977 out.extend_from_slice(
978 &u32::try_from(value_offs[index][slot])
979 .unwrap()
980 .to_le_bytes(),
981 );
982 }
983 }
984 let next = starts
985 .get(index + 1)
986 .map_or(0u32, |s| u32::try_from(*s).unwrap());
987 out.extend_from_slice(&next.to_le_bytes());
988 for entry in entries {
989 if entry.value.len() > 4 {
990 out.extend_from_slice(&entry.value);
991 if entry.value.len() % 2 == 1 {
992 out.push(0);
993 }
994 }
995 }
996 for strip in &dirs[index].strips {
997 out.extend_from_slice(strip);
998 if strip.len() % 2 == 1 {
999 out.push(0);
1000 }
1001 }
1002 }
1003 out
1004 }
1005
1006 fn strip_ok(data: &[u8]) -> Stripped {
1007 TiffHandler
1008 .strip(data, &StripOptions::default())
1009 .expect("strip failed")
1010 }
1011
1012 fn fields(data: &[u8]) -> Vec<String> {
1013 TiffHandler
1014 .inspect(data, &InspectOptions::names_only())
1015 .expect("inspect failed")
1016 .findings
1017 .into_iter()
1018 .filter_map(|f| f.field)
1019 .collect()
1020 }
1021
1022 fn contains(haystack: &[u8], needle: &[u8]) -> bool {
1023 haystack.windows(needle.len()).any(|w| w == needle)
1024 }
1025
1026 #[test]
1027 fn the_picture_is_copied_byte_for_byte() {
1028 let mut t = structural();
1029 t.push(ascii(0x013B, "SYNTHETIC-ARTIST"));
1030 let input = build(&[page(t, b"SYNTHETIC-PIXELS")]);
1031 let output = strip_ok(&input).bytes;
1032 assert!(
1033 contains(&output, b"SYNTHETIC-PIXELS"),
1034 "the image data did not survive byte for byte"
1035 );
1036 }
1037
1038 #[test]
1039 fn identifying_tags_are_reported_and_gone_from_the_output() {
1040 let mut t = structural();
1041 t.push(ascii(0x010F, "SYNTHETIC-MAKE"));
1042 t.push(ascii(0x0110, "SYNTHETIC-MODEL"));
1043 t.push(ascii(0x0131, "SYNTHETIC-SOFTWARE"));
1044 t.push(ascii(0x013B, "SYNTHETIC-ARTIST"));
1045 t.push(ascii(0x0132, "2026:08:25 11:00:00"));
1046 let input = build(&[page(t, b"SYNTHETIC-PIXELS")]);
1047
1048 let named = fields(&input);
1049 for expected in ["Make", "Model", "Software", "Artist", "DateTime"] {
1050 assert!(
1051 named.iter().any(|f| f == expected),
1052 "{expected} not reported"
1053 );
1054 }
1055
1056 let output = strip_ok(&input).bytes;
1057 for secret in [
1058 &b"SYNTHETIC-MAKE"[..],
1059 b"SYNTHETIC-MODEL",
1060 b"SYNTHETIC-SOFTWARE",
1061 b"SYNTHETIC-ARTIST",
1062 b"2026:08:25",
1063 ] {
1064 assert!(
1065 !contains(&output, secret),
1066 "a removed value survived into the output"
1067 );
1068 }
1069 }
1070
1071 #[test]
1072 fn an_unknown_vendor_tag_does_not_survive_by_being_unknown() {
1073 let mut t = structural();
1075 t.push(ascii(0xC5D9, "SYNTHETIC-SERIAL-0001"));
1076 let input = build(&[page(t, b"SYNTHETIC-PIXELS")]);
1077 let output = strip_ok(&input).bytes;
1078 assert!(!contains(&output, b"SYNTHETIC-SERIAL-0001"));
1079 assert!(!fields(&input).is_empty(), "the tag was not reported");
1080 }
1081
1082 #[test]
1083 fn the_output_reads_back_clean() {
1084 let mut t = structural();
1085 t.push(ascii(0x013B, "SYNTHETIC-ARTIST"));
1086 let input = build(&[page(t, b"SYNTHETIC-PIXELS")]);
1087 let output = strip_ok(&input).bytes;
1088 assert!(fields(&output).is_empty(), "metadata survived the rebuild");
1091 }
1092
1093 #[test]
1094 fn stripping_twice_changes_nothing() {
1095 let mut t = structural();
1096 t.push(ascii(0x013B, "SYNTHETIC-ARTIST"));
1097 let input = build(&[page(t, b"SYNTHETIC-PIXELS")]);
1098 let once = strip_ok(&input).bytes;
1099 let twice = strip_ok(&once).bytes;
1100 assert_eq!(once, twice, "strip is not idempotent");
1101 }
1102
1103 #[test]
1104 fn a_reduced_resolution_directory_is_dropped() {
1105 let main = structural();
1108 let mut thumb = structural();
1109 thumb.push(long(tags::NEW_SUBFILE_TYPE, 1));
1110 let input = build(&[
1111 page(main, b"SYNTHETIC-PIXELS"),
1112 page(thumb, b"SYNTHETIC-THUMBNAIL"),
1113 ]);
1114 let output = strip_ok(&input).bytes;
1115 assert!(contains(&output, b"SYNTHETIC-PIXELS"));
1116 assert!(
1117 !contains(&output, b"SYNTHETIC-THUMBNAIL"),
1118 "the reduced-resolution copy survived"
1119 );
1120 }
1121
1122 #[test]
1123 fn the_pages_of_a_multi_page_scan_are_all_kept() {
1124 let input = build(&[
1127 page(structural(), b"SYNTHETIC-PAGE-ONE"),
1128 page(structural(), b"SYNTHETIC-PAGE-TWO"),
1129 page(structural(), b"SYNTHETIC-PAGE-THREE"),
1130 ]);
1131 let output = strip_ok(&input).bytes;
1132 for pixels in [
1133 &b"SYNTHETIC-PAGE-ONE"[..],
1134 b"SYNTHETIC-PAGE-TWO",
1135 b"SYNTHETIC-PAGE-THREE",
1136 ] {
1137 assert!(contains(&output, pixels), "a page was lost");
1138 }
1139 }
1140
1141 #[test]
1142 fn bigtiff_is_refused_rather_than_parsed_as_tiff() {
1143 let mut input = build(&[page(structural(), b"SYNTHETIC-PIXELS")]);
1144 input[2] = 0x2B;
1145 assert!(matches!(
1146 TiffHandler.strip(&input, &StripOptions::default()),
1147 Err(StryptError::Malformed {
1148 detail: MalformedDetail::UnsupportedFeature,
1149 ..
1150 })
1151 ));
1152 }
1153
1154 #[test]
1155 fn a_directory_chain_that_loops_is_refused() {
1156 let mut input = build(&[page(structural(), b"SYNTHETIC-PIXELS")]);
1157 let first = u32::from_le_bytes([input[4], input[5], input[6], input[7]]) as usize;
1159 let count = u16::from_le_bytes([input[first], input[first + 1]]) as usize;
1160 let next_at = first + 2 + 12 * count;
1161 let self_ref = u32::try_from(first).unwrap().to_le_bytes();
1162 input[next_at..next_at + 4].copy_from_slice(&self_ref);
1163 assert!(matches!(
1164 TiffHandler.strip(&input, &StripOptions::default()),
1165 Err(StryptError::Malformed {
1166 detail: MalformedDetail::CyclicReference,
1167 ..
1168 })
1169 ));
1170 }
1171
1172 #[test]
1173 fn a_strip_pointing_outside_the_file_is_refused() {
1174 let mut input = build(&[page(structural(), b"SYNTHETIC-PIXELS")]);
1177 let pixels_at = input
1178 .windows(16)
1179 .position(|w| w == b"SYNTHETIC-PIXELS")
1180 .expect("pixels not found");
1181 let needle = u32::try_from(pixels_at).unwrap().to_le_bytes();
1182 let at = input
1183 .windows(4)
1184 .position(|w| w == needle)
1185 .expect("strip offset not found");
1186 input[at..at + 4].copy_from_slice(&0xFFFF_0000u32.to_le_bytes());
1187 assert!(TiffHandler.strip(&input, &StripOptions::default()).is_err());
1188 }
1189
1190 #[test]
1191 fn a_truncated_file_is_refused_rather_than_completed() {
1192 let input = build(&[page(structural(), b"SYNTHETIC-PIXELS")]);
1193 for cut in [4, 8, 12, 30, input.len() / 2] {
1194 let _ = TiffHandler.strip(&input[..cut], &StripOptions::default());
1195 }
1196 }
1197}