1use crate::container::riff::{self, Chunk, WalkError, name_of};
54use crate::detect::Format;
55use crate::error::{MalformedDetail, Result, StryptError};
56use crate::formats::{MetadataHandler, ParseLimits, StripOptions, Stripped, exif, xmp};
57use crate::report::{
58 Finding, InspectOptions, MetadataKind, MetadataReport, Note, Retained, StripReport,
59};
60
61#[derive(Debug, Clone, Copy, Default)]
63pub struct WebpHandler;
64
65impl MetadataHandler for WebpHandler {
66 fn name(&self) -> &'static str {
67 Format::Webp.id()
68 }
69
70 fn format(&self) -> Format {
71 Format::Webp
72 }
73
74 fn inspect(&self, input: &[u8], options: &InspectOptions) -> Result<MetadataReport> {
75 let processed = process(input, options, &ParseLimits::default())?;
80 Ok(MetadataReport {
81 format: Format::Webp,
82 findings: processed.findings,
83 notes: processed.notes,
84 })
85 }
86
87 fn strip(&self, input: &[u8], options: &StripOptions) -> Result<Stripped> {
88 let processed = process(input, &options.inspect, &options.limits)?;
89 Ok(Stripped {
90 report: StripReport {
91 format: Format::Webp,
92 removed: processed.findings,
93 retained: processed.retained,
94 notes: processed.notes,
95 input_bytes: as_u64(input.len()),
96 output_bytes: as_u64(processed.output.len()),
97 },
98 bytes: processed.output,
99 })
100 }
101}
102
103const WEBP: riff::FourCc = *b"WEBP";
105
106const VP8X_PAYLOAD_BYTES: u32 = 10;
109
110const ANMF_HEADER_BYTES: usize = 16;
113
114const METADATA_FLAGS: u8 = 0b0010_1100;
121
122const EXIF_INTRODUCER: &[u8] = b"Exif\x00\x00";
128
129const IMAGE_CHUNKS: [&[u8; 4]; 3] = [b"ALPH", b"VP8 ", b"VP8L"];
134
135struct Processed {
137 findings: Vec<Finding>,
138 retained: Vec<Retained>,
139 notes: Vec<Note>,
140 output: Vec<u8>,
141}
142
143fn walk<'a>(input: &'a [u8], limits: &ParseLimits) -> Result<(Vec<Chunk<'a>>, &'a [u8])> {
148 let mut budget = limits.max_items;
149 let (chunks, trailing) = riff::read(input, WEBP, &mut budget).map_err(convert)?;
150 validate_shape(&chunks)?;
151 Ok((chunks, trailing))
152}
153
154fn convert(error: WalkError) -> StryptError {
156 match error {
157 WalkError::Malformed { detail, offset } => malformed(detail, as_offset(offset)),
158 WalkError::Limit(limit) => StryptError::LimitExceeded {
159 format: Format::Webp,
160 limit,
161 },
162 }
163}
164
165fn validate_shape(chunks: &[Chunk<'_>]) -> Result<()> {
172 let body_start = riff::HEADER_BYTES;
175
176 for chunk in chunks {
177 if &chunk.kind == b"VP8X" && as_u64(chunk.data.len()) != u64::from(VP8X_PAYLOAD_BYTES) {
181 return Err(malformed(
182 MalformedDetail::LengthOutOfRange,
183 as_offset(chunk.offset),
184 ));
185 }
186 }
187
188 let opens_correctly =
190 matches!(chunks.first(), Some(c) if matches!(&c.kind, b"VP8X" | b"VP8 " | b"VP8L"));
191 if !opens_correctly {
192 return Err(malformed(
193 MalformedDetail::MissingMarker,
194 as_offset(body_start),
195 ));
196 }
197 let has_picture = chunks
198 .iter()
199 .any(|c| matches!(&c.kind, b"VP8 " | b"VP8L" | b"ANMF"));
200 if !has_picture {
201 return Err(malformed(
202 MalformedDetail::MissingMarker,
203 as_offset(body_start),
204 ));
205 }
206 Ok(())
207}
208
209enum Outcome {
211 Keep,
213 Replace(Vec<u8>),
216 Drop,
218}
219
220struct Decision {
222 outcome: Outcome,
223 findings: Vec<Finding>,
224 retained: Vec<Retained>,
228 notes: Vec<Note>,
229}
230
231impl Decision {
232 const fn keep() -> Self {
233 Self {
234 outcome: Outcome::Keep,
235 findings: Vec::new(),
236 retained: Vec::new(),
237 notes: Vec::new(),
238 }
239 }
240
241 fn drop_with(findings: Vec<Finding>) -> Self {
242 Self {
243 outcome: Outcome::Drop,
244 findings,
245 retained: Vec::new(),
246 notes: Vec::new(),
247 }
248 }
249
250 fn drop_one(kind: MetadataKind, location: impl Into<String>, bytes: u64) -> Self {
251 Self::drop_with(vec![Finding::new(kind, location, bytes)])
252 }
253}
254
255fn process(input: &[u8], options: &InspectOptions, limits: &ParseLimits) -> Result<Processed> {
257 let (chunks, trailing) = walk(input, limits)?;
258
259 let mut findings = Vec::new();
260 let mut retained = Vec::new();
261 let mut notes = Vec::new();
262
263 let mut body: Vec<u8> = Vec::with_capacity(input.len());
266
267 for chunk in &chunks {
268 let decision = decide(chunk, options, limits);
269 notes.extend(decision.notes);
270 retained.extend(decision.retained);
271 findings.extend(decision.findings);
272 match decision.outcome {
273 Outcome::Keep => body.extend_from_slice(chunk.raw),
274 Outcome::Replace(bytes) => body.extend_from_slice(&bytes),
275 Outcome::Drop => {}
276 }
277 }
278
279 if !trailing.is_empty() {
280 let kind = if trailing.starts_with(&riff::RIFF) {
284 MetadataKind::Thumbnail
285 } else {
286 MetadataKind::Other
287 };
288 findings.push(Finding::new(
289 kind,
290 "trailing data after the RIFF chunk",
291 as_u64(trailing.len()),
292 ));
293 }
294
295 let output = riff::write(WEBP, &body).map_err(|d| malformed(d, None))?;
298
299 Ok(Processed {
300 findings,
301 retained,
302 notes,
303 output,
304 })
305}
306
307fn decide(chunk: &Chunk<'_>, options: &InspectOptions, limits: &ParseLimits) -> Decision {
309 let size = as_u64(chunk.data.len());
310 match &chunk.kind {
311 b"VP8X" => extended_header(chunk),
314 b"VP8 " | b"VP8L" | b"ALPH" | b"ANIM" => Decision::keep(),
317 b"ANMF" => animation_frame(chunk, limits),
319 b"ICCP" => Decision::drop_one(MetadataKind::ColourProfile, "ICCP", size),
322 b"EXIF" => exif_chunk(chunk.data, size, options, limits),
323 b"XMP " => Decision::drop_with(xmp::scan(chunk.data, "XMP", options)),
324 _ => {
325 Decision::drop_one(MetadataKind::Other, name_of(&chunk.kind), size)
334 }
335 }
336}
337
338fn extended_header(chunk: &Chunk<'_>) -> Decision {
345 let Some(flags) = chunk.data.first().copied() else {
346 return Decision::keep();
348 };
349 if flags & METADATA_FLAGS == 0 {
350 return Decision::keep();
351 }
352
353 let mut rewritten = Vec::with_capacity(chunk.raw.len());
354 rewritten.extend_from_slice(b"VP8X");
355 rewritten.extend_from_slice(&VP8X_PAYLOAD_BYTES.to_le_bytes());
356 rewritten.push(flags & !METADATA_FLAGS);
357 rewritten.extend_from_slice(chunk.data.get(1..).unwrap_or_default());
360 Decision {
361 outcome: Outcome::Replace(rewritten),
362 findings: Vec::new(),
363 retained: Vec::new(),
364 notes: Vec::new(),
365 }
366}
367
368fn animation_frame(chunk: &Chunk<'_>, limits: &ParseLimits) -> Decision {
384 let Some(header) = chunk.data.get(0..ANMF_HEADER_BYTES) else {
385 return unexamined("ANMF", as_u64(chunk.data.len()));
386 };
387 let Some(rest) = chunk.data.get(ANMF_HEADER_BYTES..) else {
388 return unexamined("ANMF", as_u64(chunk.data.len()));
389 };
390
391 let mut budget = limits.max_items;
392 let Ok(sub_chunks) = riff::chunks(rest, 0, &mut budget) else {
393 return unexamined("ANMF", as_u64(chunk.data.len()));
394 };
395
396 let mut findings = Vec::new();
397 let mut payload = Vec::with_capacity(chunk.data.len());
398 payload.extend_from_slice(header);
399 for sub in &sub_chunks {
400 if IMAGE_CHUNKS.contains(&&sub.kind) {
401 payload.extend_from_slice(sub.raw);
402 } else {
403 findings.push(Finding::new(
404 MetadataKind::Other,
405 format!("ANMF {}", name_of(&sub.kind)),
406 as_u64(sub.data.len()),
407 ));
408 }
409 }
410
411 if findings.is_empty() {
412 return Decision::keep();
415 }
416
417 let mut rewritten = Vec::with_capacity(payload.len().saturating_add(riff::HEADER_BYTES + 1));
418 if riff::write_chunk(&mut rewritten, *b"ANMF", &payload).is_err() {
419 return unexamined("ANMF", as_u64(chunk.data.len()));
421 }
422
423 Decision {
424 outcome: Outcome::Replace(rewritten),
425 findings,
426 retained: Vec::new(),
427 notes: Vec::new(),
428 }
429}
430
431fn exif_chunk(data: &[u8], size: u64, options: &InspectOptions, limits: &ParseLimits) -> Decision {
433 let tiff = if data.starts_with(EXIF_INTRODUCER) {
434 data.get(EXIF_INTRODUCER.len()..).unwrap_or_default()
435 } else {
436 data
437 };
438 let scanned = exif::scan(tiff, "EXIF", options, limits);
439 let findings = if scanned.findings.is_empty() {
440 vec![Finding::new(MetadataKind::Other, "EXIF", size)]
442 } else {
443 scanned.findings
444 };
445 Decision {
446 outcome: Outcome::Drop,
447 findings,
448 retained: Vec::new(),
449 notes: scanned.notes,
450 }
451}
452
453fn unexamined(location: &'static str, bytes: u64) -> Decision {
455 Decision {
456 outcome: Outcome::Keep,
457 findings: Vec::new(),
458 retained: Vec::new(),
459 notes: vec![Note::UnparsedRegion {
460 location: location.to_owned(),
461 bytes,
462 }],
463 }
464}
465
466fn malformed(detail: MalformedDetail, offset: Option<u64>) -> StryptError {
468 StryptError::Malformed {
469 format: Format::Webp,
470 offset,
471 detail,
472 }
473}
474
475fn as_offset(position: usize) -> Option<u64> {
477 u64::try_from(position).ok()
478}
479
480fn as_u64(value: usize) -> u64 {
482 u64::try_from(value).unwrap_or(u64::MAX)
483}
484
485#[cfg(test)]
486mod tests {
487 #![allow(
490 clippy::unwrap_used,
491 clippy::expect_used,
492 clippy::indexing_slicing,
493 clippy::arithmetic_side_effects
494 )]
495
496 use super::*;
497 use crate::report::MetadataValue;
498
499 fn chunk(kind: &[u8], data: &[u8]) -> Vec<u8> {
501 let mut out = kind.to_vec();
502 out.extend_from_slice(&u32::try_from(data.len()).unwrap().to_le_bytes());
503 out.extend_from_slice(data);
504 if data.len() % 2 == 1 {
505 out.push(0);
506 }
507 out
508 }
509
510 fn webp(chunks: &[Vec<u8>]) -> Vec<u8> {
512 riff::write(WEBP, &chunks.concat()).unwrap()
513 }
514
515 fn vp8x(flags: u8) -> Vec<u8> {
517 let mut data = vec![flags, 0, 0, 0];
518 data.extend_from_slice(&15u32.to_le_bytes()[0..3]);
519 data.extend_from_slice(&15u32.to_le_bytes()[0..3]);
520 chunk(b"VP8X", &data)
521 }
522
523 fn bitstream() -> Vec<u8> {
525 chunk(b"VP8L", b"SYNTHETIC-PIXELS")
526 }
527
528 fn strip_ok(data: &[u8]) -> Stripped {
529 WebpHandler
530 .strip(data, &StripOptions::default())
531 .expect("strip failed")
532 }
533
534 fn findings(data: &[u8]) -> Vec<Finding> {
535 WebpHandler
536 .inspect(data, &InspectOptions::names_only())
537 .expect("inspect failed")
538 .findings
539 }
540
541 fn contains(haystack: &[u8], needle: &[u8]) -> bool {
542 haystack.windows(needle.len()).any(|w| w == needle)
543 }
544
545 #[test]
546 fn the_picture_is_never_touched() {
547 let input = webp(&[
548 vp8x(0b0000_1000),
549 bitstream(),
550 chunk(b"EXIF", b"II\x2A\x00\x08\x00\x00\x00\x00\x00"),
551 ]);
552 let output = strip_ok(&input).bytes;
553 assert!(
554 contains(&output, b"SYNTHETIC-PIXELS"),
555 "the image data did not survive byte for byte"
556 );
557 }
558
559 #[test]
560 fn a_simple_file_cannot_carry_metadata_and_comes_back_byte_identical() {
561 for payload in [chunk(b"VP8L", b"SYNTHETIC-PIXELS"), chunk(b"VP8 ", b"ODD")] {
565 let input = webp(&[payload]);
566 let stripped = strip_ok(&input);
567 assert!(stripped.report.removed.is_empty());
568 assert_eq!(
569 stripped.bytes, input,
570 "a simple WebP was not passed through"
571 );
572 }
573 }
574
575 #[test]
576 fn a_clean_extended_file_comes_back_byte_identical_too() {
577 let input = webp(&[vp8x(0b0001_0000), chunk(b"ALPH", b"A"), bitstream()]);
580 let stripped = strip_ok(&input);
581 assert!(stripped.report.removed.is_empty());
582 assert_eq!(stripped.bytes, input);
583 }
584
585 #[test]
586 fn the_metadata_chunks_are_removed_and_the_header_flags_follow() {
587 let mut tiff = b"II\x2A\x00\x08\x00\x00\x00".to_vec();
588 tiff.extend_from_slice(&1u16.to_le_bytes());
589 tiff.extend_from_slice(&0x010Fu16.to_le_bytes()); tiff.extend_from_slice(&2u16.to_le_bytes()); tiff.extend_from_slice(&4u32.to_le_bytes());
592 tiff.extend_from_slice(b"ACME");
593 tiff.extend_from_slice(&0u32.to_le_bytes());
594
595 let input = webp(&[
597 vp8x(0b0011_1100),
598 chunk(b"ICCP", b"SYNTHETIC-PROFILE-0001"),
599 chunk(b"ALPH", b"A"),
600 bitstream(),
601 chunk(b"EXIF", &tiff),
602 chunk(
603 b"XMP ",
604 b"<x:xmpmeta><dc:creator>SYNTHETIC-0002</dc:creator></x:xmpmeta>",
605 ),
606 ]);
607
608 let found = findings(&input);
609 let kinds: Vec<MetadataKind> = found.iter().map(|f| f.kind).collect();
610 assert!(kinds.contains(&MetadataKind::ColourProfile));
611 assert!(kinds.contains(&MetadataKind::DeviceIdentity));
612 assert!(kinds.contains(&MetadataKind::PersonalIdentity));
613
614 let output = strip_ok(&input).bytes;
615 assert!(!contains(&output, b"SYNTHETIC-PROFILE-0001"));
616 assert!(!contains(&output, b"ACME"));
617 assert!(!contains(&output, b"SYNTHETIC-0002"));
618 assert!(findings(&output).is_empty());
619
620 let flags = output[20];
623 assert_eq!(
624 flags, 0b0001_0000,
625 "the VP8X flags still claim metadata that is gone"
626 );
627 }
628
629 #[test]
630 fn flags_that_were_already_lying_are_corrected_even_with_nothing_to_remove() {
631 let input = webp(&[vp8x(0b0000_1100), bitstream()]);
634 let stripped = strip_ok(&input);
635 assert!(stripped.report.removed.is_empty());
636 assert_eq!(stripped.bytes[20], 0);
637 assert_ne!(stripped.bytes, input);
638 }
639
640 #[test]
641 fn an_exif_chunk_written_with_a_jpeg_introducer_is_still_read() {
642 let mut tiff = b"II\x2A\x00\x08\x00\x00\x00".to_vec();
646 tiff.extend_from_slice(&1u16.to_le_bytes());
647 tiff.extend_from_slice(&0x0110u16.to_le_bytes()); tiff.extend_from_slice(&2u16.to_le_bytes());
649 tiff.extend_from_slice(&4u32.to_le_bytes());
650 tiff.extend_from_slice(b"MDL1");
651 tiff.extend_from_slice(&0u32.to_le_bytes());
652
653 let mut payload = EXIF_INTRODUCER.to_vec();
654 payload.extend_from_slice(&tiff);
655 let input = webp(&[vp8x(0b0000_1000), bitstream(), chunk(b"EXIF", &payload)]);
656
657 let found = findings(&input);
658 assert_eq!(found[0].field.as_deref(), Some("Model"));
659 }
660
661 #[test]
662 fn an_unknown_chunk_is_removed_rather_than_preserved() {
663 let input = webp(&[
666 vp8x(0),
667 bitstream(),
668 chunk(b"PRVW", b"SYNTHETIC-PREVIEW-0003"),
669 ]);
670 let stripped = strip_ok(&input);
671 assert!(!contains(&stripped.bytes, b"SYNTHETIC-PREVIEW-0003"));
672 assert_eq!(stripped.report.removed[0].location, "PRVW");
673 }
674
675 #[test]
676 fn an_animation_survives_and_a_chunk_hidden_in_a_frame_does_not() {
677 let mut frame = vec![0u8; ANMF_HEADER_BYTES];
678 frame.extend_from_slice(&chunk(b"VP8L", b"SYNTHETIC-FRAME-PIXELS"));
679 let clean = webp(&[
680 vp8x(0b0000_0010),
681 chunk(b"ANIM", &[0, 0, 0, 0, 0, 0]),
682 chunk(b"ANMF", &frame),
683 ]);
684 assert_eq!(strip_ok(&clean).bytes, clean, "an animation was rewritten");
685
686 let mut hostile = vec![0u8; ANMF_HEADER_BYTES];
687 hostile.extend_from_slice(&chunk(b"VP8L", b"SYNTHETIC-FRAME-PIXELS"));
688 hostile.extend_from_slice(&chunk(b"JUNK", b"SYNTHETIC-IN-FRAME-0004"));
689 let input = webp(&[
690 vp8x(0b0000_0010),
691 chunk(b"ANIM", &[0, 0, 0, 0, 0, 0]),
692 chunk(b"ANMF", &hostile),
693 ]);
694 let stripped = strip_ok(&input);
695 assert!(!contains(&stripped.bytes, b"SYNTHETIC-IN-FRAME-0004"));
696 assert!(
697 contains(&stripped.bytes, b"SYNTHETIC-FRAME-PIXELS"),
698 "the frame's picture did not survive"
699 );
700 assert_eq!(stripped.report.removed[0].location, "ANMF JUNK");
701 }
702
703 #[test]
704 fn a_frame_that_does_not_parse_is_kept_and_declared_unexamined() {
705 let mut frame = vec![0u8; ANMF_HEADER_BYTES];
706 frame.extend_from_slice(b"VP8L\xff\xff\xff\xffPRESERVED-0005");
707 let input = webp(&[
708 vp8x(0b0000_0010),
709 chunk(b"ANIM", &[0, 0, 0, 0, 0, 0]),
710 chunk(b"ANMF", &frame),
711 ]);
712 let stripped = strip_ok(&input);
713 assert!(contains(&stripped.bytes, b"PRESERVED-0005"));
714 assert!(matches!(
715 stripped.report.notes.first(),
716 Some(Note::UnparsedRegion { location, .. }) if location == "ANMF"
717 ));
718 }
719
720 #[test]
721 fn data_after_the_riff_chunk_is_removed() {
722 let mut input = webp(&[vp8x(0), bitstream()]);
723 input.extend_from_slice(b"SYNTHETIC-APPENDED-0006");
724 let stripped = strip_ok(&input);
725 assert!(!contains(&stripped.bytes, b"SYNTHETIC-APPENDED-0006"));
726 assert_eq!(
727 stripped.report.removed[0].location,
728 "trailing data after the RIFF chunk"
729 );
730 }
731
732 #[test]
733 fn a_second_file_after_the_riff_chunk_is_reported_as_a_thumbnail() {
734 let mut input = webp(&[vp8x(0), bitstream()]);
735 input.extend_from_slice(&webp(&[bitstream()]));
736 assert_eq!(
737 strip_ok(&input).report.removed[0].kind,
738 MetadataKind::Thumbnail
739 );
740 }
741
742 #[test]
743 fn an_xmp_packet_is_itemised_by_property() {
744 let input = webp(&[
745 vp8x(0b0000_0100),
746 bitstream(),
747 chunk(
748 b"XMP ",
749 br#"<x:xmpmeta xmpMM:DocumentID="uuid:1" xmp:CreatorTool="SYNTHETIC"/>"#,
750 ),
751 ]);
752 let found = findings(&input);
753 let fields: Vec<&str> = found.iter().filter_map(|f| f.field.as_deref()).collect();
754 assert!(fields.contains(&"xmpMM:DocumentID"), "{fields:?}");
755 assert!(fields.contains(&"xmp:CreatorTool"), "{fields:?}");
756 }
757
758 #[test]
759 fn values_are_withheld_from_a_default_inspection() {
760 let mut tiff = b"II\x2A\x00\x08\x00\x00\x00".to_vec();
761 tiff.extend_from_slice(&1u16.to_le_bytes());
762 tiff.extend_from_slice(&0x010Fu16.to_le_bytes());
763 tiff.extend_from_slice(&2u16.to_le_bytes());
764 tiff.extend_from_slice(&4u32.to_le_bytes());
765 tiff.extend_from_slice(b"ACME");
766 tiff.extend_from_slice(&0u32.to_le_bytes());
767 let input = webp(&[vp8x(0b0000_1000), bitstream(), chunk(b"EXIF", &tiff)]);
768
769 assert_eq!(findings(&input)[0].value, None);
770 let with_values = WebpHandler
771 .inspect(&input, &InspectOptions::with_values())
772 .unwrap();
773 assert_eq!(
774 with_values.findings[0].value,
775 Some(MetadataValue::Text("ACME".to_owned()))
776 );
777 }
778
779 #[test]
780 fn stripping_twice_changes_nothing() {
781 let input = webp(&[
782 vp8x(0b0011_1100),
783 chunk(b"ICCP", b"SYNTHETIC-PROFILE-0001"),
784 bitstream(),
785 chunk(b"XMP ", b"<x:xmpmeta/>"),
786 ]);
787 let once = strip_ok(&input).bytes;
788 let twice = strip_ok(&once).bytes;
789 assert_eq!(once, twice, "strip is not idempotent");
790 }
791
792 #[test]
793 fn a_riff_size_beyond_the_end_of_the_file_is_refused_rather_than_clamped() {
794 let mut input = webp(&[vp8x(0), bitstream()]);
795 input[4..8].copy_from_slice(&0x00FF_FFFFu32.to_le_bytes());
796 assert!(matches!(
797 WebpHandler.inspect(&input, &InspectOptions::names_only()),
798 Err(StryptError::Malformed {
799 detail: MalformedDetail::LengthOutOfRange,
800 ..
801 })
802 ));
803 }
804
805 #[test]
806 fn a_chunk_size_beyond_the_riff_extent_is_refused() {
807 let input = webp(&[vp8x(0), bitstream(), {
808 let mut lying = b"EXIF".to_vec();
809 lying.extend_from_slice(&0x0010_0000u32.to_le_bytes());
810 lying.extend_from_slice(b"II\x2A\x00");
811 lying
812 }]);
813 assert!(matches!(
814 WebpHandler.inspect(&input, &InspectOptions::names_only()),
815 Err(StryptError::Malformed {
816 detail: MalformedDetail::LengthOutOfRange,
817 ..
818 })
819 ));
820 }
821
822 #[test]
823 fn a_file_with_no_picture_chunk_is_refused_rather_than_emptied() {
824 let input = webp(&[
827 vp8x(0b0000_1000),
828 chunk(b"EXIF", b"II\x2A\x00\x08\x00\x00\x00"),
829 ]);
830 assert!(matches!(
831 WebpHandler.strip(&input, &StripOptions::default()),
832 Err(StryptError::Malformed {
833 detail: MalformedDetail::MissingMarker,
834 ..
835 })
836 ));
837 }
838
839 #[test]
840 fn a_file_that_does_not_open_with_a_header_or_bitstream_chunk_is_refused() {
841 let input = webp(&[chunk(b"EXIF", b"II\x2A\x00\x08\x00\x00\x00"), bitstream()]);
842 assert!(matches!(
843 WebpHandler.inspect(&input, &InspectOptions::names_only()),
844 Err(StryptError::Malformed {
845 detail: MalformedDetail::MissingMarker,
846 ..
847 })
848 ));
849 }
850
851 #[test]
852 fn a_vp8x_of_the_wrong_length_is_refused() {
853 let input = webp(&[chunk(b"VP8X", &[0u8; 8]), bitstream()]);
854 assert!(matches!(
855 WebpHandler.inspect(&input, &InspectOptions::names_only()),
856 Err(StryptError::Malformed {
857 detail: MalformedDetail::LengthOutOfRange,
858 ..
859 })
860 ));
861 }
862
863 #[test]
864 fn a_four_character_code_that_is_not_ascii_is_refused() {
865 let input = webp(&[vp8x(0), bitstream(), chunk(b"\x00\x01\x02\x03", b"")]);
866 assert!(matches!(
867 WebpHandler.inspect(&input, &InspectOptions::names_only()),
868 Err(StryptError::Malformed {
869 detail: MalformedDetail::UnexpectedMarker,
870 ..
871 })
872 ));
873 }
874
875 #[test]
876 fn a_file_that_is_not_riff_or_not_webp_is_refused() {
877 assert!(matches!(
878 WebpHandler.inspect(b"RIFX\x04\x00\x00\x00WEBP", &InspectOptions::names_only()),
879 Err(StryptError::Malformed { .. })
880 ));
881 assert!(matches!(
884 WebpHandler.inspect(b"RIFF\x04\x00\x00\x00WAVE", &InspectOptions::names_only()),
885 Err(StryptError::Malformed {
886 detail: MalformedDetail::MissingMarker,
887 ..
888 })
889 ));
890 }
891
892 #[test]
893 fn truncation_at_every_length_is_refused_or_survived_but_never_panics() {
894 let input = webp(&[
895 vp8x(0b0011_1100),
896 chunk(b"ICCP", b"SYNTHETIC-PROFILE-0001"),
897 bitstream(),
898 chunk(b"EXIF", b"II\x2A\x00\x08\x00\x00\x00\x00\x00"),
899 chunk(b"XMP ", b"<x:xmpmeta/>"),
900 ]);
901 for n in 0..=input.len() {
902 let prefix = &input[0..n];
903 let _ = WebpHandler.inspect(prefix, &InspectOptions::names_only());
904 let _ = WebpHandler.strip(prefix, &StripOptions::default());
905 }
906 }
907
908 #[test]
909 fn a_chunk_count_beyond_the_limit_is_refused() {
910 let mut chunks = vec![vp8x(0), bitstream()];
911 chunks.extend((0..64).map(|_| chunk(b"JUNK", b"x")));
912 let input = webp(&chunks);
913 let options = StripOptions {
914 limits: ParseLimits {
915 max_items: 8,
916 ..ParseLimits::default()
917 },
918 ..StripOptions::default()
919 };
920 assert!(matches!(
921 WebpHandler.strip(&input, &options),
922 Err(StryptError::LimitExceeded { .. })
923 ));
924 }
925
926 #[test]
927 fn an_odd_length_chunk_keeps_its_padding_byte() {
928 let input = webp(&[vp8x(0), chunk(b"VP8 ", b"ODD")]);
931 let stripped = strip_ok(&input);
932 assert_eq!(stripped.bytes, input);
933 assert_eq!(stripped.bytes.len() % 2, 0);
934 }
935}