1use crate::bytes::Reader;
44use crate::detect::Format;
45use crate::error::{MalformedDetail, ResourceLimit, Result, StryptError};
46use crate::formats::{MetadataHandler, ParseLimits, StripOptions, Stripped, xmp};
47use crate::report::{
48 Finding, InspectOptions, MetadataKind, MetadataReport, MetadataValue, Note, Retained,
49 RetentionReason, StripReport,
50};
51
52#[derive(Debug, Clone, Copy, Default)]
54pub struct GifHandler;
55
56impl MetadataHandler for GifHandler {
57 fn name(&self) -> &'static str {
58 Format::Gif.id()
59 }
60
61 fn format(&self) -> Format {
62 Format::Gif
63 }
64
65 fn inspect(&self, input: &[u8], options: &InspectOptions) -> Result<MetadataReport> {
66 let processed = process(input, options, &ParseLimits::default())?;
71 Ok(MetadataReport {
72 format: Format::Gif,
73 findings: processed.findings,
74 notes: processed.notes,
75 })
76 }
77
78 fn strip(&self, input: &[u8], options: &StripOptions) -> Result<Stripped> {
79 let processed = process(input, &options.inspect, &options.limits)?;
80 Ok(Stripped {
81 report: StripReport {
82 format: Format::Gif,
83 removed: processed.findings,
84 retained: processed.retained,
85 notes: processed.notes,
86 input_bytes: as_u64(input.len()),
87 output_bytes: as_u64(processed.output.len()),
88 },
89 bytes: processed.output,
90 })
91 }
92}
93
94const SIGNATURES: [&[u8; 6]; 2] = [b"GIF87a", b"GIF89a"];
98
99const TRAILER: u8 = 0x3B;
101const EXTENSION_INTRODUCER: u8 = 0x21;
103const IMAGE_SEPARATOR: u8 = 0x2C;
105
106const LABEL_PLAIN_TEXT: u8 = 0x01;
108const LABEL_GRAPHIC_CONTROL: u8 = 0xF9;
109const LABEL_COMMENT: u8 = 0xFE;
110const LABEL_APPLICATION: u8 = 0xFF;
111
112const APPLICATION_IDENTIFIER_LEN: usize = 11;
114
115const LOOP_EXTENSIONS: [&[u8; APPLICATION_IDENTIFIER_LEN]; 2] = [b"NETSCAPE2.0", b"ANIMEXTS1.0"];
124
125const XMP_IDENTIFIER: &[u8] = b"XMP DataXMP";
132
133const APPLICATION_KINDS: &[(&[u8], MetadataKind)] = &[
139 (b"ICCRGBG1012", MetadataKind::ColourProfile),
140 (b"MGK8BIM0000", MetadataKind::SoftwareFingerprint),
141 (b"MGKIPTC0000", MetadataKind::PersonalIdentity),
142 (b"ImageMagick", MetadataKind::SoftwareFingerprint),
143 (b"Adobe Gif", MetadataKind::SoftwareFingerprint),
144];
145
146struct Block<'a> {
148 kind: BlockKind,
149 body: &'a [u8],
152 head: &'a [u8],
155 raw: &'a [u8],
158}
159
160#[derive(Clone, Copy, PartialEq, Eq)]
162enum BlockKind {
163 Extension(u8),
165 Image,
167}
168
169struct Processed {
171 findings: Vec<Finding>,
172 retained: Vec<Retained>,
173 notes: Vec<Note>,
174 output: Vec<u8>,
175}
176
177fn walk<'a>(input: &'a [u8], limits: &ParseLimits) -> Result<(&'a [u8], Vec<Block<'a>>, &'a [u8])> {
184 let mut r = Reader::new(input);
185 let signature = r
186 .take(6)
187 .ok_or_else(|| malformed(MalformedDetail::Truncated, Some(0)))?;
188 if !SIGNATURES.iter().any(|candidate| *candidate == signature) {
189 return Err(malformed(MalformedDetail::MissingMarker, Some(0)));
190 }
191
192 let descriptor = r
195 .take(7)
196 .ok_or_else(|| malformed(MalformedDetail::Truncated, as_offset(6)))?;
197 let packed = descriptor.get(4).copied().unwrap_or_default();
200 if packed & 0x80 != 0 {
201 let entries = colour_table_bytes(packed);
202 r.skip(entries)
203 .ok_or_else(|| malformed(MalformedDetail::LengthOutOfRange, as_offset(13)))?;
204 }
205 let prefix = input.get(0..r.position()).unwrap_or_default();
206
207 let mut blocks: Vec<Block<'a>> = Vec::new();
208 let mut budget = limits.max_items;
209
210 loop {
211 let start = r.position();
212 let introducer = r
213 .u8()
214 .ok_or_else(|| malformed(MalformedDetail::Truncated, as_offset(start)))?;
215 if introducer == TRAILER {
216 break;
217 }
218 spend(&mut budget)?;
219
220 let (kind, head, body) = match introducer {
221 EXTENSION_INTRODUCER => {
222 let label = r
223 .u8()
224 .ok_or_else(|| malformed(MalformedDetail::Truncated, as_offset(start)))?;
225 let (head, body) = sub_blocks(&mut r, input, &mut budget, start)?;
226 (BlockKind::Extension(label), head, body)
227 }
228 IMAGE_SEPARATOR => {
229 let descriptor = r
231 .take(9)
232 .ok_or_else(|| malformed(MalformedDetail::Truncated, as_offset(start)))?;
233 let packed = descriptor.get(8).copied().unwrap_or_default();
234 if packed & 0x80 != 0 {
235 r.skip(colour_table_bytes(packed)).ok_or_else(|| {
237 malformed(MalformedDetail::LengthOutOfRange, as_offset(start))
238 })?;
239 }
240 r.skip(1)
243 .ok_or_else(|| malformed(MalformedDetail::Truncated, as_offset(start)))?;
244 let _ = sub_blocks(&mut r, input, &mut budget, start)?;
245 (BlockKind::Image, &[][..], &[][..])
246 }
247 _ => {
248 return Err(malformed(
252 MalformedDetail::UnexpectedMarker,
253 as_offset(start),
254 ));
255 }
256 };
257
258 blocks.push(Block {
259 kind,
260 body,
261 head,
262 raw: input.get(start..r.position()).unwrap_or_default(),
263 });
264 }
265
266 Ok((prefix, blocks, r.take_rest()))
267}
268
269fn colour_table_bytes(packed: u8) -> usize {
273 let n = u32::from(packed & 0b0000_0111);
274 3usize.saturating_mul(1usize << n.saturating_add(1))
275}
276
277fn sub_blocks<'a>(
283 r: &mut Reader<'a>,
284 input: &'a [u8],
285 budget: &mut u32,
286 block_start: usize,
287) -> Result<(&'a [u8], &'a [u8])> {
288 let span_start = r.position();
289 let mut head: &[u8] = &[];
290 loop {
291 spend(budget)?;
292 let at = r.position();
293 let length = r
294 .u8()
295 .ok_or_else(|| malformed(MalformedDetail::Truncated, as_offset(block_start)))?;
296 if length == 0 {
297 let span = input.get(span_start..at).unwrap_or_default();
298 return Ok((head, span));
299 }
300 let data = r
301 .take(usize::from(length))
302 .ok_or_else(|| malformed(MalformedDetail::LengthOutOfRange, as_offset(at)))?;
303 if head.is_empty() {
304 head = data;
305 }
306 }
307}
308
309fn spend(budget: &mut u32) -> Result<()> {
311 if *budget == 0 {
312 return Err(StryptError::LimitExceeded {
313 format: Format::Gif,
314 limit: ResourceLimit::ItemCount,
315 });
316 }
317 *budget = budget.saturating_sub(1);
318 Ok(())
319}
320
321enum Outcome {
323 Keep,
325 Drop,
327}
328
329struct Decision {
331 outcome: Outcome,
332 findings: Vec<Finding>,
333 retained: Vec<Retained>,
337}
338
339impl Decision {
340 const fn keep() -> Self {
341 Self {
342 outcome: Outcome::Keep,
343 findings: Vec::new(),
344 retained: Vec::new(),
345 }
346 }
347
348 fn kept_on_purpose(location: &'static str, reason: RetentionReason) -> Self {
350 Self {
351 outcome: Outcome::Keep,
352 findings: Vec::new(),
353 retained: vec![Retained {
354 location: location.to_owned(),
355 reason,
356 }],
357 }
358 }
359
360 fn drop_with(findings: Vec<Finding>) -> Self {
361 Self {
362 outcome: Outcome::Drop,
363 findings,
364 retained: Vec::new(),
365 }
366 }
367
368 const fn drop_silently() -> Self {
372 Self {
373 outcome: Outcome::Drop,
374 findings: Vec::new(),
375 retained: Vec::new(),
376 }
377 }
378}
379
380fn process(input: &[u8], options: &InspectOptions, limits: &ParseLimits) -> Result<Processed> {
382 let (prefix, blocks, trailing) = walk(input, limits)?;
383 let mut out = Processed {
384 findings: Vec::new(),
385 retained: Vec::new(),
386 notes: Vec::new(),
387 output: Vec::with_capacity(input.len()),
388 };
389 out.output.extend_from_slice(prefix);
390
391 for (index, block) in blocks.iter().enumerate() {
392 let next = blocks.get(index.saturating_add(1));
393 let decision = decide(block, next, options);
394 out.retained.extend(decision.retained);
395 match decision.outcome {
396 Outcome::Keep => out.output.extend_from_slice(block.raw),
397 Outcome::Drop => out.findings.extend(decision.findings),
398 }
399 }
400 out.output.push(TRAILER);
401
402 if !trailing.is_empty() {
403 let kind = if SIGNATURES
406 .iter()
407 .any(|signature| trailing.starts_with(signature.as_slice()))
408 {
409 MetadataKind::Thumbnail
410 } else {
411 MetadataKind::Other
412 };
413 out.findings.push(Finding::new(
414 kind,
415 "trailing data after the trailer",
416 as_u64(trailing.len()),
417 ));
418 }
419
420 Ok(out)
421}
422
423fn decide(block: &Block<'_>, next: Option<&Block<'_>>, options: &InspectOptions) -> Decision {
426 let size = as_u64(block.body.len());
427 match block.kind {
428 BlockKind::Image => Decision::keep(),
429 BlockKind::Extension(LABEL_GRAPHIC_CONTROL) => {
430 if matches!(
433 next.map(|b| b.kind),
434 Some(BlockKind::Extension(LABEL_PLAIN_TEXT))
435 ) {
436 return Decision::drop_silently();
437 }
438 Decision::keep()
439 }
440 BlockKind::Extension(LABEL_COMMENT) => Decision::drop_with(vec![
441 Finding::new(MetadataKind::Comment, "Comment Extension", size)
442 .with_field("Comment")
443 .with_value(options, || MetadataValue::Text(xmp::name_of(block.body))),
444 ]),
445 BlockKind::Extension(LABEL_PLAIN_TEXT) => Decision::drop_with(vec![
446 Finding::new(MetadataKind::Comment, "Plain Text Extension", size)
447 .with_field("PlainText"),
448 ]),
449 BlockKind::Extension(LABEL_APPLICATION) => application(block, size, options),
450 BlockKind::Extension(label) => {
451 Decision::drop_with(vec![Finding::new(
455 MetadataKind::Other,
456 format!("Extension 0x{label:02X}"),
457 size,
458 )])
459 }
460 }
461}
462
463fn application(block: &Block<'_>, size: u64, options: &InspectOptions) -> Decision {
465 let identifier = block
466 .head
467 .get(0..APPLICATION_IDENTIFIER_LEN)
468 .unwrap_or(block.head);
469
470 if LOOP_EXTENSIONS
471 .iter()
472 .any(|candidate| candidate.as_slice() == identifier)
473 {
474 return Decision::kept_on_purpose(
477 "Application Extension (loop count)",
478 RetentionReason::RemovalWouldAlterPayload,
479 );
480 }
481
482 if identifier == XMP_IDENTIFIER {
483 return Decision::drop_with(xmp::scan(
484 block.body,
485 "Application Extension (XMP)",
486 options,
487 ));
488 }
489
490 let kind = APPLICATION_KINDS
491 .iter()
492 .find(|(candidate, _)| *candidate == identifier)
493 .map_or(MetadataKind::Other, |(_, kind)| *kind);
494 Decision::drop_with(vec![
495 Finding::new(kind, "Application Extension", size).with_field(xmp::name_of(identifier)),
496 ])
497}
498
499fn malformed(detail: MalformedDetail, offset: Option<u64>) -> StryptError {
501 StryptError::Malformed {
502 format: Format::Gif,
503 offset,
504 detail,
505 }
506}
507
508fn as_offset(position: usize) -> Option<u64> {
510 u64::try_from(position).ok()
511}
512
513fn as_u64(value: usize) -> u64 {
515 u64::try_from(value).unwrap_or(u64::MAX)
516}
517
518#[cfg(test)]
519mod tests {
520 #![allow(
523 clippy::unwrap_used,
524 clippy::expect_used,
525 clippy::indexing_slicing,
526 clippy::arithmetic_side_effects
527 )]
528
529 use super::*;
530
531 fn chain(payload: &[u8]) -> Vec<u8> {
533 let mut out = Vec::new();
534 for part in payload.chunks(255) {
535 out.push(u8::try_from(part.len()).unwrap());
536 out.extend_from_slice(part);
537 }
538 out.push(0);
539 out
540 }
541
542 fn extension(label: u8, payload: &[u8]) -> Vec<u8> {
543 let mut out = vec![EXTENSION_INTRODUCER, label];
544 out.extend_from_slice(&chain(payload));
545 out
546 }
547
548 fn application_extension(identifier: &[u8], data: &[u8]) -> Vec<u8> {
550 let mut out = vec![EXTENSION_INTRODUCER, LABEL_APPLICATION, 11];
551 out.extend_from_slice(identifier);
552 out.extend_from_slice(&chain(data));
553 out
554 }
555
556 fn image() -> Vec<u8> {
558 let mut out = vec![IMAGE_SEPARATOR];
559 out.extend_from_slice(&[0, 0, 0, 0, 1, 0, 1, 0, 0]);
560 out.push(2);
561 out.extend_from_slice(&chain(b"SYNTHETIC-PIXELS"));
562 out
563 }
564
565 fn graphic_control() -> Vec<u8> {
567 extension(LABEL_GRAPHIC_CONTROL, &[0x04, 0x0A, 0x00, 0x00])
568 }
569
570 fn gif(blocks: &[Vec<u8>]) -> Vec<u8> {
573 let mut out = b"GIF89a".to_vec();
574 out.extend_from_slice(&[1, 0, 1, 0, 0x00, 0, 0]);
575 for block in blocks {
576 out.extend_from_slice(block);
577 }
578 out.push(TRAILER);
579 out
580 }
581
582 fn strip_ok(data: &[u8]) -> Stripped {
583 GifHandler
584 .strip(data, &StripOptions::default())
585 .expect("strip failed")
586 }
587
588 fn findings(data: &[u8]) -> Vec<Finding> {
589 GifHandler
590 .inspect(data, &InspectOptions::names_only())
591 .expect("inspect failed")
592 .findings
593 }
594
595 fn contains(haystack: &[u8], needle: &[u8]) -> bool {
596 haystack.windows(needle.len()).any(|w| w == needle)
597 }
598
599 #[test]
600 fn the_picture_is_never_touched() {
601 let input = gif(&[extension(LABEL_COMMENT, b"SYNTHETIC-COMMENT-0001"), image()]);
602 let output = strip_ok(&input).bytes;
603 assert!(
604 contains(&output, b"SYNTHETIC-PIXELS"),
605 "the compressed image data did not survive byte for byte"
606 );
607 }
608
609 #[test]
610 fn a_clean_file_strips_to_a_byte_identical_copy() {
611 let input = gif(&[image()]);
615 let stripped = strip_ok(&input);
616 assert!(stripped.report.removed.is_empty());
617 assert_eq!(stripped.bytes, input);
618 }
619
620 #[test]
621 fn a_global_colour_table_is_carried_across() {
622 let mut input = b"GIF89a".to_vec();
625 input.extend_from_slice(&[1, 0, 1, 0, 0x80, 0, 0]); input.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
627 input.extend_from_slice(&extension(LABEL_COMMENT, b"SYNTHETIC-COMMENT-0002"));
628 input.extend_from_slice(&image());
629 input.push(TRAILER);
630
631 let output = strip_ok(&input).bytes;
632 assert!(contains(&output, &[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]));
633 assert!(!contains(&output, b"SYNTHETIC-COMMENT-0002"));
634 }
635
636 #[test]
637 fn a_comment_is_reported_and_removed_and_its_text_withheld_by_default() {
638 let input = gif(&[extension(LABEL_COMMENT, b"SYNTHETIC-COMMENT-0003"), image()]);
639 let found = findings(&input);
640 assert_eq!(found[0].kind, MetadataKind::Comment);
641 assert_eq!(found[0].field.as_deref(), Some("Comment"));
642 assert_eq!(
643 found[0].value, None,
644 "a default inspection withholds values"
645 );
646
647 let with_values = GifHandler
648 .inspect(&input, &InspectOptions::with_values())
649 .unwrap();
650 assert_eq!(
651 with_values.findings[0].value,
652 Some(MetadataValue::Text("SYNTHETIC-COMMENT-0003".to_owned()))
653 );
654 assert!(!contains(
655 &strip_ok(&input).bytes,
656 b"SYNTHETIC-COMMENT-0003"
657 ));
658 }
659
660 #[test]
661 fn the_loop_extension_survives_and_says_so() {
662 let input = gif(&[
666 application_extension(b"NETSCAPE2.0", &[0x01, 0x00, 0x00]),
667 image(),
668 ]);
669 let stripped = strip_ok(&input);
670 assert!(contains(&stripped.bytes, b"NETSCAPE2.0"));
671 assert!(stripped.report.removed.is_empty());
672 assert_eq!(
673 stripped.report.retained[0].location,
674 "Application Extension (loop count)"
675 );
676 }
677
678 #[test]
679 fn the_older_loop_spelling_survives_too() {
680 let input = gif(&[
681 application_extension(b"ANIMEXTS1.0", &[0x01, 0x00, 0x00]),
682 image(),
683 ]);
684 assert!(contains(&strip_ok(&input).bytes, b"ANIMEXTS1.0"));
685 }
686
687 #[test]
688 fn an_unknown_application_extension_does_not_survive_by_being_unknown() {
689 let input = gif(&[
692 application_extension(b"VENDORX1.0\0", b"SYNTHETIC-VENDOR-0004"),
693 image(),
694 ]);
695 let found = findings(&input);
696 assert_eq!(found[0].kind, MetadataKind::Other);
697 assert_eq!(found[0].location, "Application Extension");
698 assert!(!contains(&strip_ok(&input).bytes, b"SYNTHETIC-VENDOR-0004"));
699 }
700
701 #[test]
702 fn an_imagemagick_iptc_block_is_ranked_as_naming_a_person() {
703 let input = gif(&[
705 application_extension(b"MGKIPTC0000", b"\x1c\x02\x50SYNTHETIC-BYLINE-0005"),
706 image(),
707 ]);
708 assert_eq!(findings(&input)[0].kind, MetadataKind::PersonalIdentity);
709 assert!(!contains(&strip_ok(&input).bytes, b"SYNTHETIC-BYLINE-0005"));
710 }
711
712 #[test]
713 fn an_xmp_packet_is_itemised_by_property() {
714 let mut packet = b"<x:xmpmeta><dc:creator>SYNTHETIC-XMP-0006</dc:creator>".to_vec();
718 packet.extend_from_slice(b"<xmp:CreatorTool>SYNTHETIC-TOOL</xmp:CreatorTool></x:xmpmeta>");
719 let input = gif(&[application_extension(b"XMP DataXMP", &packet), image()]);
720
721 let fields: Vec<String> = findings(&input)
722 .into_iter()
723 .filter_map(|f| f.field)
724 .collect();
725 assert!(fields.iter().any(|f| f == "dc:creator"));
726 assert!(fields.iter().any(|f| f == "xmp:CreatorTool"));
727 assert!(!contains(&strip_ok(&input).bytes, b"SYNTHETIC-XMP-0006"));
728 }
729
730 #[test]
731 fn a_graphic_control_block_stays_with_its_image_and_goes_with_its_plain_text() {
732 let kept = gif(&[graphic_control(), image()]);
735 assert_eq!(strip_ok(&kept).bytes, kept);
736
737 let mut plain_text = vec![EXTENSION_INTRODUCER, LABEL_PLAIN_TEXT, 12];
738 plain_text.extend_from_slice(&[0; 12]);
739 plain_text.extend_from_slice(&chain(b"SYNTHETIC-PLAINTEXT-0007"));
740 let input = gif(&[graphic_control(), plain_text, image()]);
741
742 let stripped = strip_ok(&input);
743 assert!(!contains(&stripped.bytes, b"SYNTHETIC-PLAINTEXT-0007"));
744 assert_eq!(
745 stripped.bytes,
746 gif(&[image()]),
747 "the orphaned graphic control block was left behind"
748 );
749 assert_eq!(stripped.report.removed.len(), 1);
750 assert_eq!(stripped.report.removed[0].location, "Plain Text Extension");
751 }
752
753 #[test]
754 fn an_extension_under_an_undefined_label_is_removed() {
755 let input = gif(&[extension(0x42, b"SYNTHETIC-UNKNOWN-0008"), image()]);
756 let found = findings(&input);
757 assert_eq!(found[0].location, "Extension 0x42");
758 assert!(!contains(
759 &strip_ok(&input).bytes,
760 b"SYNTHETIC-UNKNOWN-0008"
761 ));
762 }
763
764 #[test]
765 fn data_hidden_after_the_trailer_is_removed() {
766 let mut input = gif(&[image()]);
767 input.extend_from_slice(b"SYNTHETIC-APPENDED-0009");
768 let stripped = strip_ok(&input);
769 assert!(!contains(&stripped.bytes, b"SYNTHETIC-APPENDED-0009"));
770 assert_eq!(
771 stripped.report.removed[0].location,
772 "trailing data after the trailer"
773 );
774 }
775
776 #[test]
777 fn a_second_image_after_the_trailer_is_reported_as_a_thumbnail() {
778 let mut input = gif(&[image()]);
779 input.extend_from_slice(&gif(&[image()]));
780 assert_eq!(
781 strip_ok(&input).report.removed[0].kind,
782 MetadataKind::Thumbnail
783 );
784 }
785
786 #[test]
787 fn stripping_twice_changes_nothing() {
788 let input = gif(&[
789 extension(LABEL_COMMENT, b"SYNTHETIC-COMMENT-0010"),
790 application_extension(b"NETSCAPE2.0", &[0x01, 0x00, 0x00]),
791 graphic_control(),
792 image(),
793 ]);
794 let once = strip_ok(&input).bytes;
795 let twice = strip_ok(&once).bytes;
796 assert_eq!(once, twice, "strip is not idempotent");
797 }
798
799 #[test]
800 fn a_file_without_a_trailer_is_refused() {
801 let input = gif(&[image()]);
804 let truncated = &input[0..input.len() - 1];
805 assert!(matches!(
806 GifHandler.strip(truncated, &StripOptions::default()),
807 Err(StryptError::Malformed {
808 detail: MalformedDetail::Truncated,
809 ..
810 })
811 ));
812 }
813
814 #[test]
815 fn a_block_introducer_the_format_does_not_define_is_refused() {
816 let input = gif(&[vec![0x99, 0x00]]);
817 assert!(matches!(
818 GifHandler.inspect(&input, &InspectOptions::names_only()),
819 Err(StryptError::Malformed {
820 detail: MalformedDetail::UnexpectedMarker,
821 ..
822 })
823 ));
824 }
825
826 #[test]
827 fn a_sub_block_length_running_past_the_end_of_the_file_is_refused() {
828 let mut input = gif(&[extension(LABEL_COMMENT, b"short"), image()]);
829 let at = 13 + 2;
831 input[at] = 0xFF;
832 assert!(matches!(
833 GifHandler.inspect(&input, &InspectOptions::names_only()),
834 Err(StryptError::Malformed {
835 detail: MalformedDetail::LengthOutOfRange,
836 ..
837 })
838 ));
839 }
840
841 #[test]
842 fn a_header_that_is_not_a_gif_signature_is_refused() {
843 assert!(matches!(
844 GifHandler.inspect(
845 b"GIF88a\x01\x00\x01\x00\x00\x00\x00\x3B",
846 &InspectOptions::names_only()
847 ),
848 Err(StryptError::Malformed {
849 detail: MalformedDetail::MissingMarker,
850 ..
851 })
852 ));
853 }
854
855 #[test]
856 fn a_block_count_beyond_the_limit_is_refused() {
857 let mut blocks: Vec<Vec<u8>> = (0..64).map(|_| extension(LABEL_COMMENT, b"x")).collect();
858 blocks.push(image());
859 let input = gif(&blocks);
860 let options = StripOptions {
861 limits: ParseLimits {
862 max_items: 8,
863 ..ParseLimits::default()
864 },
865 ..StripOptions::default()
866 };
867 assert!(matches!(
868 GifHandler.strip(&input, &options),
869 Err(StryptError::LimitExceeded { .. })
870 ));
871 }
872
873 #[test]
874 fn truncation_at_every_length_is_refused_or_survived_but_never_panics() {
875 let input = gif(&[
876 extension(LABEL_COMMENT, b"SYNTHETIC-COMMENT-0011"),
877 application_extension(b"XMP DataXMP", b"<x:xmpmeta><dc:creator>x</dc:creator>"),
878 graphic_control(),
879 image(),
880 ]);
881 for n in 0..=input.len() {
882 let prefix = &input[0..n];
883 let _ = GifHandler.inspect(prefix, &InspectOptions::names_only());
884 let _ = GifHandler.strip(prefix, &StripOptions::default());
885 }
886 }
887}