1use crate::container::riff::{self, Chunk, FourCc, WalkError, name_of};
35use crate::detect::Format;
36use crate::error::{MalformedDetail, Result, StryptError, UnsupportedKind};
37use crate::formats::{MetadataHandler, ParseLimits, StripOptions, Stripped, xmp};
38use crate::report::{
39 Finding, InspectOptions, MetadataKind, MetadataReport, MetadataValue, Note, Retained,
40 RetentionReason, StripReport,
41};
42
43#[derive(Debug, Clone, Copy, Default)]
45pub struct WavHandler;
46
47impl MetadataHandler for WavHandler {
48 fn name(&self) -> &'static str {
49 Format::Wav.id()
50 }
51
52 fn format(&self) -> Format {
53 Format::Wav
54 }
55
56 fn inspect(&self, input: &[u8], options: &InspectOptions) -> Result<MetadataReport> {
57 let processed = process(input, options, &ParseLimits::default())?;
60 Ok(MetadataReport {
61 format: Format::Wav,
62 findings: processed.findings,
63 notes: processed.notes,
64 })
65 }
66
67 fn strip(&self, input: &[u8], options: &StripOptions) -> Result<Stripped> {
68 let processed = process(input, &options.inspect, &options.limits)?;
69 Ok(Stripped {
70 report: StripReport {
71 format: Format::Wav,
72 removed: processed.findings,
73 retained: processed.retained,
74 notes: processed.notes,
75 input_bytes: as_u64(input.len()),
76 output_bytes: as_u64(processed.output.len()),
77 },
78 bytes: processed.output,
79 })
80 }
81}
82
83const WAVE: FourCc = *b"WAVE";
85
86const FMT_MINIMUM: usize = 16;
88
89const KEPT: [&FourCc; 4] = [b"fmt ", b"data", b"fact", b"cue "];
95
96const ZEROED: [&FourCc; 3] = [b"JUNK", b"PAD ", b"FLLR"];
102
103const LIST_INFO: FourCc = *b"INFO";
105const LIST_ADTL: FourCc = *b"adtl";
106const LIST_WAVL: FourCc = *b"wavl";
108
109const INFO_TAGS: &[(&[u8; 4], &str, MetadataKind)] = &[
111 (b"IART", "Artist", MetadataKind::PersonalIdentity),
112 (b"IENG", "Engineer", MetadataKind::PersonalIdentity),
113 (b"ITCH", "Technician", MetadataKind::PersonalIdentity),
114 (b"ICMS", "Commissioned", MetadataKind::PersonalIdentity),
115 (b"ICOP", "Copyright", MetadataKind::PersonalIdentity),
116 (b"IARL", "ArchivalLocation", MetadataKind::Location),
117 (b"ICRD", "CreationDate", MetadataKind::Timestamp),
118 (b"IDIT", "DigitisationTime", MetadataKind::Timestamp),
119 (b"ISFT", "Software", MetadataKind::SoftwareFingerprint),
120 (b"ITOC", "TableOfContents", MetadataKind::DocumentIdentifier),
121 (b"ISRC", "Source", MetadataKind::DocumentIdentifier),
122 (b"ICMT", "Comment", MetadataKind::Comment),
123 (b"INAM", "Title", MetadataKind::Comment),
124 (b"ISBJ", "Subject", MetadataKind::Comment),
125 (b"IKEY", "Keywords", MetadataKind::Comment),
126 (b"IPRD", "Product", MetadataKind::Comment),
127 (b"IGNR", "Genre", MetadataKind::Comment),
128 (b"IMED", "Medium", MetadataKind::Comment),
129];
130
131const BEXT_FIELDS: &[(usize, usize, &str, MetadataKind)] = &[
136 (0, 256, "Description", MetadataKind::Comment),
137 (256, 32, "Originator", MetadataKind::PersonalIdentity),
138 (288, 32, "OriginatorReference", MetadataKind::DeviceIdentity),
139 (320, 10, "OriginationDate", MetadataKind::Timestamp),
140 (330, 8, "OriginationTime", MetadataKind::Timestamp),
141 (348, 64, "UMID", MetadataKind::DocumentIdentifier),
143 (
145 602,
146 usize::MAX,
147 "CodingHistory",
148 MetadataKind::EditingHistory,
149 ),
150];
151
152const CART_FIELDS: &[(usize, usize, &str, MetadataKind)] = &[
154 (4, 64, "Title", MetadataKind::Comment),
155 (68, 64, "Artist", MetadataKind::PersonalIdentity),
156 (132, 64, "CutID", MetadataKind::DocumentIdentifier),
157 (196, 64, "ClientID", MetadataKind::PersonalIdentity),
158 (388, 10, "StartDate", MetadataKind::Timestamp),
159 (420, 64, "ProducerAppID", MetadataKind::SoftwareFingerprint),
160 (
161 484,
162 64,
163 "ProducerAppVersion",
164 MetadataKind::SoftwareFingerprint,
165 ),
166 (1024, 1024, "URL", MetadataKind::Location),
167];
168
169struct Processed {
171 findings: Vec<Finding>,
172 retained: Vec<Retained>,
173 notes: Vec<Note>,
174 output: Vec<u8>,
175}
176
177enum Outcome {
179 Keep,
181 Replace(Vec<u8>),
183 Drop,
185}
186
187struct Decision {
189 outcome: Outcome,
190 findings: Vec<Finding>,
191 retained: Vec<Retained>,
192 notes: Vec<Note>,
193}
194
195impl Decision {
196 const fn keep() -> Self {
197 Self {
198 outcome: Outcome::Keep,
199 findings: Vec::new(),
200 retained: Vec::new(),
201 notes: Vec::new(),
202 }
203 }
204
205 fn drop_with(findings: Vec<Finding>) -> Self {
206 Self {
207 outcome: Outcome::Drop,
208 findings,
209 retained: Vec::new(),
210 notes: Vec::new(),
211 }
212 }
213
214 fn drop_one(kind: MetadataKind, location: impl Into<String>, bytes: u64) -> Self {
215 Self::drop_with(vec![Finding::new(kind, location, bytes)])
216 }
217}
218
219fn process(input: &[u8], options: &InspectOptions, limits: &ParseLimits) -> Result<Processed> {
221 let mut budget = limits.max_items;
222 let (chunks, trailing) = riff::read(input, WAVE, &mut budget).map_err(convert)?;
223 validate_shape(&chunks)?;
224
225 let mut findings = Vec::new();
226 let mut retained = Vec::new();
227 let mut notes = Vec::new();
228
229 let mut body: Vec<u8> = Vec::with_capacity(input.len());
230 for chunk in &chunks {
231 let decision = decide(chunk, options, limits);
232 notes.extend(decision.notes);
233 retained.extend(decision.retained);
234 findings.extend(decision.findings);
235 match decision.outcome {
236 Outcome::Keep => body.extend_from_slice(chunk.raw),
237 Outcome::Replace(bytes) => body.extend_from_slice(&bytes),
238 Outcome::Drop => {}
239 }
240 }
241
242 if !trailing.is_empty() {
243 findings.push(Finding::new(
246 MetadataKind::Other,
247 "trailing data after the RIFF chunk",
248 as_u64(trailing.len()),
249 ));
250 }
251
252 notes.push(Note::OutOfScopeContent {
255 location: "audio samples, which are copied without being decoded".to_owned(),
256 });
257
258 let output = riff::write(WAVE, &body).map_err(|d| malformed(d, None))?;
261
262 Ok(Processed {
263 findings,
264 retained,
265 notes,
266 output,
267 })
268}
269
270fn convert(error: WalkError) -> StryptError {
272 match error {
273 WalkError::Malformed { detail, offset } => malformed(detail, as_offset(offset)),
274 WalkError::Limit(limit) => StryptError::LimitExceeded {
275 format: Format::Wav,
276 limit,
277 },
278 }
279}
280
281fn validate_shape(chunks: &[Chunk<'_>]) -> Result<()> {
287 for chunk in chunks {
288 if &chunk.kind == b"LIST" && matches!(riff::list_form(chunk.data), Some((LIST_WAVL, _))) {
289 return Err(StryptError::UnsupportedFormat {
290 format: UnsupportedKind::WaveList,
291 });
292 }
293 if &chunk.kind == b"fmt " && chunk.data.len() < FMT_MINIMUM {
296 return Err(malformed(
297 MalformedDetail::LengthOutOfRange,
298 as_offset(chunk.offset),
299 ));
300 }
301 }
302
303 for required in [b"fmt ", b"data"] {
304 if !chunks.iter().any(|c| &c.kind == required) {
305 return Err(malformed(
306 MalformedDetail::MissingMarker,
307 as_offset(riff::HEADER_BYTES),
308 ));
309 }
310 }
311 Ok(())
312}
313
314fn decide(chunk: &Chunk<'_>, options: &InspectOptions, limits: &ParseLimits) -> Decision {
316 let size = as_u64(chunk.data.len());
317 let kind = &chunk.kind;
318
319 if KEPT.contains(&kind) {
320 return Decision::keep();
321 }
322 if ZEROED.contains(&kind) {
323 return zeroed(chunk);
324 }
325
326 match kind {
327 b"LIST" => list(chunk, options, limits),
328 b"bext" => Decision::drop_with(fields(
331 chunk.data,
332 BEXT_FIELDS,
333 "bext",
334 size,
335 MetadataKind::Other,
336 options,
337 )),
338 b"cart" => Decision::drop_with(fields(
340 chunk.data,
341 CART_FIELDS,
342 "cart",
343 size,
344 MetadataKind::Other,
345 options,
346 )),
347 b"_PMX" => {
349 let found = xmp::scan(chunk.data, "_PMX", options);
350 if found.is_empty() {
351 Decision::drop_one(MetadataKind::Other, "_PMX", size)
352 } else {
353 Decision::drop_with(found)
354 }
355 }
356 b"iXML" => Decision::drop_one(MetadataKind::DeviceIdentity, "iXML", size),
358 b"id3 " | b"ID3 " => Decision::drop_one(MetadataKind::Other, "id3", size),
361 b"DISP" => Decision::drop_one(MetadataKind::Comment, "DISP", size),
363 b"CSET" => Decision::drop_one(MetadataKind::Other, "CSET", size),
365 b"smpl" => Decision {
368 outcome: Outcome::Drop,
369 findings: vec![Finding::new(MetadataKind::DeviceIdentity, "smpl", size)],
370 retained: Vec::new(),
371 notes: vec![Note::CapabilityRemoved {
372 location: "smpl".to_owned(),
373 capability: "be looped by a sampler at the points it recorded".to_owned(),
374 }],
375 },
376 b"inst" => Decision::drop_one(MetadataKind::Other, "inst", size),
377 b"plst" => Decision::drop_one(MetadataKind::Other, "plst", size),
378 _ => Decision::drop_one(MetadataKind::Other, name_of(kind), size),
383 }
384}
385
386fn zeroed(chunk: &Chunk<'_>) -> Decision {
391 if chunk.data.iter().all(|b| *b == 0) {
392 return Decision::keep();
395 }
396 let mut out = Vec::with_capacity(chunk.raw.len());
397 if riff::write_chunk(&mut out, chunk.kind, &vec![0u8; chunk.data.len()]).is_err() {
398 return Decision::keep();
400 }
401 Decision {
402 outcome: Outcome::Replace(out),
403 findings: vec![Finding::new(
404 MetadataKind::Other,
405 name_of(&chunk.kind),
406 as_u64(chunk.data.len()),
407 )],
408 retained: vec![Retained {
409 location: name_of(&chunk.kind),
410 reason: RetentionReason::StructurallyRequired,
411 }],
412 notes: Vec::new(),
413 }
414}
415
416fn list(chunk: &Chunk<'_>, options: &InspectOptions, limits: &ParseLimits) -> Decision {
418 let size = as_u64(chunk.data.len());
419 let Some((form, rest)) = riff::list_form(chunk.data) else {
420 return Decision::drop_one(MetadataKind::Other, "LIST", size);
421 };
422 if form != LIST_INFO && form != LIST_ADTL {
424 return Decision::drop_one(
425 MetadataKind::Other,
426 format!("LIST {}", name_of(&form)),
427 size,
428 );
429 }
430
431 let mut budget = limits.max_items;
432 let Ok(items) = riff::chunks(rest, 0, &mut budget) else {
433 return Decision::drop_one(
436 MetadataKind::Other,
437 format!("LIST {}", name_of(&form)),
438 size,
439 );
440 };
441
442 let mut findings = Vec::new();
443 for item in &items {
444 let bytes = as_u64(item.data.len());
445 let (field, kind) = if form == LIST_INFO {
446 INFO_TAGS
447 .iter()
448 .find(|(tag, _, _)| *tag == &item.kind)
449 .map_or_else(
450 || (name_of(&item.kind), MetadataKind::Other),
451 |(_, name, kind)| ((*name).to_owned(), *kind),
452 )
453 } else {
454 (name_of(&item.kind), MetadataKind::Comment)
457 };
458 findings.push(
459 Finding::new(kind, format!("LIST {}", name_of(&form)), bytes)
460 .with_field(field)
461 .with_value(options, || MetadataValue::Text(text_of(item.data))),
462 );
463 }
464 if findings.is_empty() {
465 findings.push(Finding::new(
466 MetadataKind::Other,
467 format!("LIST {}", name_of(&form)),
468 size,
469 ));
470 }
471 Decision::drop_with(findings)
472}
473
474fn fields(
479 data: &[u8],
480 table: &[(usize, usize, &str, MetadataKind)],
481 location: &str,
482 size: u64,
483 fallback: MetadataKind,
484 options: &InspectOptions,
485) -> Vec<Finding> {
486 let mut out = Vec::new();
487 for (at, len, name, kind) in table {
488 let Some(tail) = data.get(*at..) else {
489 continue;
490 };
491 let raw = tail.get(..*len).unwrap_or(tail);
492 let value = text_of(raw);
493 if value.is_empty() {
494 continue;
495 }
496 out.push(
497 Finding::new(*kind, location.to_owned(), as_u64(raw.len()))
498 .with_field((*name).to_owned())
499 .with_value(options, || MetadataValue::Text(value.clone())),
500 );
501 }
502 if out.is_empty() {
503 out.push(Finding::new(fallback, location.to_owned(), size));
504 }
505 out
506}
507
508fn text_of(raw: &[u8]) -> String {
514 let end = raw.iter().position(|b| *b == 0).unwrap_or(raw.len());
515 let head = raw.get(..end).unwrap_or_default();
516 xmp::name_of(head).trim().to_owned()
517}
518
519fn malformed(detail: MalformedDetail, offset: Option<u64>) -> StryptError {
521 StryptError::Malformed {
522 format: Format::Wav,
523 offset,
524 detail,
525 }
526}
527
528fn as_offset(position: usize) -> Option<u64> {
530 u64::try_from(position).ok()
531}
532
533fn as_u64(value: usize) -> u64 {
535 u64::try_from(value).unwrap_or(u64::MAX)
536}
537
538#[cfg(test)]
539mod tests {
540 #![allow(
543 clippy::unwrap_used,
544 clippy::expect_used,
545 clippy::indexing_slicing,
546 clippy::arithmetic_side_effects
547 )]
548
549 use super::*;
550
551 fn chunk(kind: FourCc, payload: &[u8]) -> Vec<u8> {
552 let mut out = Vec::new();
553 riff::write_chunk(&mut out, kind, payload).unwrap();
554 out
555 }
556
557 fn fmt() -> Vec<u8> {
559 let mut p = Vec::new();
560 p.extend_from_slice(&1u16.to_le_bytes()); p.extend_from_slice(&1u16.to_le_bytes()); p.extend_from_slice(&8000u32.to_le_bytes());
563 p.extend_from_slice(&16000u32.to_le_bytes());
564 p.extend_from_slice(&2u16.to_le_bytes());
565 p.extend_from_slice(&16u16.to_le_bytes());
566 chunk(*b"fmt ", &p)
567 }
568
569 fn data() -> Vec<u8> {
570 chunk(*b"data", b"SYNTHETIC-SAMPLES-0001\0\0")
571 }
572
573 fn wav(parts: &[Vec<u8>]) -> Vec<u8> {
574 riff::write(WAVE, &parts.concat()).unwrap()
575 }
576
577 fn strip_ok(input: &[u8]) -> Stripped {
578 WavHandler
579 .strip(input, &StripOptions::default())
580 .expect("strip failed")
581 }
582
583 fn findings(input: &[u8]) -> Vec<Finding> {
584 WavHandler
585 .inspect(input, &InspectOptions::names_only())
586 .expect("inspect failed")
587 .findings
588 }
589
590 fn contains(haystack: &[u8], needle: &[u8]) -> bool {
591 haystack.windows(needle.len()).any(|w| w == needle)
592 }
593
594 fn info(tags: &[(FourCc, &[u8])]) -> Vec<u8> {
596 let mut body = LIST_INFO.to_vec();
597 for (tag, value) in tags {
598 let mut v = value.to_vec();
599 v.push(0);
600 body.extend_from_slice(&chunk(*tag, &v));
601 }
602 chunk(*b"LIST", &body)
603 }
604
605 #[test]
606 fn a_clean_file_comes_back_byte_identical() {
607 let input = wav(&[fmt(), data()]);
608 let stripped = strip_ok(&input);
609 assert!(stripped.report.removed.is_empty());
610 assert_eq!(stripped.bytes, input, "a clean WAV was rewritten");
611 }
612
613 #[test]
614 fn the_audio_crosses_byte_for_byte() {
615 let input = wav(&[fmt(), info(&[(*b"IART", b"SYNTHETIC-ARTIST-0002")]), data()]);
616 let output = strip_ok(&input).bytes;
617 assert!(contains(&output, b"SYNTHETIC-SAMPLES-0001"));
618 assert!(!contains(&output, b"SYNTHETIC-ARTIST-0002"));
619 }
620
621 #[test]
622 fn an_info_list_is_itemised_by_tag() {
623 let input = wav(&[
624 fmt(),
625 info(&[
626 (*b"IART", b"SYNTHETIC-ARTIST-0002"),
627 (*b"ISFT", b"SYNTHETIC-RECORDER-0003"),
628 (*b"ICRD", b"2026-09-01"),
629 ]),
630 data(),
631 ]);
632 let found = findings(&input);
633 let fields: Vec<&str> = found.iter().filter_map(|f| f.field.as_deref()).collect();
634 assert!(fields.contains(&"Artist"), "{fields:?}");
635 assert!(fields.contains(&"Software"), "{fields:?}");
636 assert!(fields.contains(&"CreationDate"), "{fields:?}");
637 let kinds: Vec<MetadataKind> = found.iter().map(|f| f.kind).collect();
638 assert!(kinds.contains(&MetadataKind::PersonalIdentity));
639 assert!(kinds.contains(&MetadataKind::SoftwareFingerprint));
640 assert!(kinds.contains(&MetadataKind::Timestamp));
641 }
642
643 #[test]
644 fn an_unrecognised_info_tag_is_still_removed_and_reported() {
645 let input = wav(&[
646 fmt(),
647 info(&[(*b"IZZZ", b"SYNTHETIC-PRIVATE-0004")]),
648 data(),
649 ]);
650 let stripped = strip_ok(&input);
651 assert!(!contains(&stripped.bytes, b"SYNTHETIC-PRIVATE-0004"));
652 assert_eq!(stripped.report.removed[0].field.as_deref(), Some("IZZZ"));
653 }
654
655 #[test]
656 fn a_broadcast_extension_is_itemised_down_to_its_coding_history() {
657 let mut p = vec![0u8; 602];
658 p[0..9].copy_from_slice(b"SYNTHETIC");
659 p[256..265].copy_from_slice(b"ORIG-0005");
660 p[288..297].copy_from_slice(b"REF--0006");
661 p[320..330].copy_from_slice(b"2026-09-01");
662 p[348..357].copy_from_slice(b"UMID-0007");
663 p.extend_from_slice(b"A=PCM,F=8000,W=16,M=mono,T=SYNTHETIC-DECK-0008");
664 let input = wav(&[fmt(), chunk(*b"bext", &p), data()]);
665
666 let found = findings(&input);
667 let fields: Vec<&str> = found.iter().filter_map(|f| f.field.as_deref()).collect();
668 for want in [
669 "Description",
670 "Originator",
671 "OriginatorReference",
672 "OriginationDate",
673 "UMID",
674 "CodingHistory",
675 ] {
676 assert!(fields.contains(&want), "{want} missing from {fields:?}");
677 }
678
679 let stripped = strip_ok(&input);
680 assert!(!contains(&stripped.bytes, b"SYNTHETIC-DECK-0008"));
681 assert!(!contains(&stripped.bytes, b"UMID-0007"));
682 }
683
684 #[test]
685 fn an_empty_broadcast_extension_still_yields_one_finding() {
686 let input = wav(&[fmt(), chunk(*b"bext", &[0u8; 602]), data()]);
688 let found = findings(&input);
689 assert_eq!(found.len(), 1);
690 assert_eq!(found[0].location, "bext");
691 }
692
693 #[test]
694 fn an_id3_tag_is_dropped_whole_without_being_parsed() {
695 let mut tag = b"ID3\x04\x00\x00\x00\x00\x00\x20".to_vec();
696 tag.extend_from_slice(b"TPE1SYNTHETIC-ID3-ARTIST-0009");
697 let input = wav(&[fmt(), data(), chunk(*b"id3 ", &tag)]);
698 let stripped = strip_ok(&input);
699 assert!(!contains(&stripped.bytes, b"SYNTHETIC-ID3-ARTIST-0009"));
700 assert_eq!(stripped.report.removed[0].location, "id3");
701 }
702
703 #[test]
704 fn an_xmp_packet_is_itemised_by_property() {
705 let input = wav(&[
706 fmt(),
707 data(),
708 chunk(
709 *b"_PMX",
710 br#"<x:xmpmeta xmpMM:DocumentID="uuid:1" xmp:CreatorTool="SYNTHETIC"/>"#,
711 ),
712 ]);
713 let fields: Vec<String> = findings(&input)
714 .iter()
715 .filter_map(|f| f.field.clone())
716 .collect();
717 assert!(fields.iter().any(|f| f == "xmpMM:DocumentID"), "{fields:?}");
718 }
719
720 #[test]
721 fn a_sampler_chunk_says_what_the_file_can_no_longer_do() {
722 let input = wav(&[fmt(), data(), chunk(*b"smpl", &[0u8; 36])]);
723 let stripped = strip_ok(&input);
724 assert!(matches!(
725 stripped.report.notes.first(),
726 Some(Note::CapabilityRemoved { location, .. }) if location == "smpl"
727 ));
728 }
729
730 #[test]
731 fn padding_is_zeroed_at_its_length_rather_than_dropped() {
732 let input = wav(&[fmt(), chunk(*b"JUNK", b"SYNTHETIC-LEFTOVER-0010!"), data()]);
733 let stripped = strip_ok(&input);
734 assert!(!contains(&stripped.bytes, b"SYNTHETIC-LEFTOVER-0010"));
735 assert_eq!(
736 stripped.bytes.len(),
737 input.len(),
738 "zeroing changed the file's length"
739 );
740 assert_eq!(
741 stripped.report.retained[0].reason,
742 RetentionReason::StructurallyRequired
743 );
744 }
745
746 #[test]
747 fn padding_that_is_already_zero_is_copied_rather_than_rebuilt() {
748 let input = wav(&[fmt(), chunk(*b"JUNK", &[0u8; 32]), data()]);
749 let stripped = strip_ok(&input);
750 assert!(stripped.report.removed.is_empty());
751 assert_eq!(stripped.bytes, input);
752 }
753
754 #[test]
755 fn a_cue_chunk_survives_because_removing_metadata_cannot_move_its_offsets() {
756 let mut cue = 1u32.to_le_bytes().to_vec();
757 cue.extend_from_slice(&[0u8; 24]);
758 let input = wav(&[fmt(), data(), chunk(*b"cue ", &cue)]);
759 assert_eq!(strip_ok(&input).bytes, input);
760 }
761
762 #[test]
763 fn an_unknown_chunk_is_removed_rather_than_preserved() {
764 let input = wav(&[fmt(), data(), chunk(*b"PrVw", b"SYNTHETIC-PRIVATE-0011")]);
765 let stripped = strip_ok(&input);
766 assert!(!contains(&stripped.bytes, b"SYNTHETIC-PRIVATE-0011"));
767 assert_eq!(stripped.report.removed[0].location, "PrVw");
768 }
769
770 #[test]
771 fn data_after_the_riff_chunk_is_removed() {
772 let mut input = wav(&[fmt(), data()]);
773 input.extend_from_slice(b"SYNTHETIC-APPENDED-0012");
774 let stripped = strip_ok(&input);
775 assert!(!contains(&stripped.bytes, b"SYNTHETIC-APPENDED-0012"));
776 assert_eq!(
777 stripped.report.removed[0].location,
778 "trailing data after the RIFF chunk"
779 );
780 }
781
782 #[test]
783 fn a_file_with_no_format_or_no_audio_is_refused_rather_than_emptied() {
784 for parts in [
787 vec![fmt()],
788 vec![data()],
789 vec![chunk(*b"bext", &[0u8; 602])],
790 ] {
791 assert!(
792 matches!(
793 WavHandler.strip(&wav(&parts), &StripOptions::default()),
794 Err(StryptError::Malformed {
795 detail: MalformedDetail::MissingMarker,
796 ..
797 })
798 ),
799 "a WAV missing fmt or data was accepted"
800 );
801 }
802 }
803
804 #[test]
805 fn a_wave_list_is_refused_by_name_rather_than_edited() {
806 let mut body = LIST_WAVL.to_vec();
808 body.extend_from_slice(&chunk(*b"data", b"AB"));
809 let input = wav(&[fmt(), chunk(*b"LIST", &body), data()]);
810 assert!(matches!(
811 WavHandler.inspect(&input, &InspectOptions::names_only()),
812 Err(StryptError::UnsupportedFormat {
813 format: UnsupportedKind::WaveList,
814 })
815 ));
816 }
817
818 #[test]
819 fn a_format_chunk_shorter_than_the_pcm_form_is_refused() {
820 let input = wav(&[chunk(*b"fmt ", &[0u8; 8]), data()]);
821 assert!(matches!(
822 WavHandler.inspect(&input, &InspectOptions::names_only()),
823 Err(StryptError::Malformed {
824 detail: MalformedDetail::LengthOutOfRange,
825 ..
826 })
827 ));
828 }
829
830 #[test]
831 fn a_riff_size_beyond_the_end_of_the_file_is_refused_rather_than_clamped() {
832 let mut input = wav(&[fmt(), data()]);
833 input[4..8].copy_from_slice(&0x00FF_FFFFu32.to_le_bytes());
834 assert!(matches!(
835 WavHandler.inspect(&input, &InspectOptions::names_only()),
836 Err(StryptError::Malformed {
837 detail: MalformedDetail::LengthOutOfRange,
838 ..
839 })
840 ));
841 }
842
843 #[test]
844 fn a_chunk_count_beyond_the_limit_is_refused() {
845 let mut parts = vec![fmt(), data()];
846 parts.extend((0..64).map(|_| chunk(*b"PrVw", b"xx")));
847 let options = StripOptions {
848 limits: ParseLimits {
849 max_items: 8,
850 ..ParseLimits::default()
851 },
852 ..StripOptions::default()
853 };
854 assert!(matches!(
855 WavHandler.strip(&wav(&parts), &options),
856 Err(StryptError::LimitExceeded { .. })
857 ));
858 }
859
860 #[test]
861 fn values_are_withheld_from_a_default_inspection() {
862 let input = wav(&[fmt(), info(&[(*b"IART", b"SYNTHETIC-ARTIST-0002")]), data()]);
863 assert_eq!(findings(&input)[0].value, None);
864 let with_values = WavHandler
865 .inspect(&input, &InspectOptions::with_values())
866 .unwrap();
867 assert_eq!(
868 with_values.findings[0].value,
869 Some(MetadataValue::Text("SYNTHETIC-ARTIST-0002".to_owned()))
870 );
871 }
872
873 #[test]
874 fn the_uninspected_audio_is_declared_on_every_file() {
875 let stripped = strip_ok(&wav(&[fmt(), data()]));
876 assert!(
877 stripped
878 .report
879 .notes
880 .iter()
881 .any(|n| matches!(n, Note::OutOfScopeContent { location } if location.starts_with("audio samples")))
882 );
883 }
884
885 #[test]
886 fn stripping_twice_changes_nothing() {
887 let input = wav(&[
888 fmt(),
889 info(&[(*b"IART", b"SYNTHETIC-ARTIST-0002")]),
890 chunk(*b"JUNK", b"SYNTHETIC-LEFTOVER-0010!"),
891 data(),
892 chunk(*b"bext", &[0u8; 602]),
893 ]);
894 let once = strip_ok(&input).bytes;
895 let twice = strip_ok(&once).bytes;
896 assert_eq!(once, twice, "strip is not idempotent");
897 assert!(findings(&once).is_empty(), "a strip left something behind");
898 }
899
900 #[test]
901 fn an_odd_length_chunk_keeps_its_padding_byte() {
902 let input = wav(&[fmt(), chunk(*b"data", b"ODD")]);
903 let stripped = strip_ok(&input);
904 assert_eq!(stripped.bytes, input);
905 assert_eq!(stripped.bytes.len() % 2, 0);
906 }
907
908 #[test]
909 fn truncation_at_every_length_is_refused_or_survived_but_never_panics() {
910 let input = wav(&[
911 fmt(),
912 info(&[(*b"IART", b"SYNTHETIC-ARTIST-0002")]),
913 chunk(*b"bext", &[7u8; 700]),
914 chunk(*b"JUNK", b"x"),
915 data(),
916 chunk(*b"smpl", &[0u8; 36]),
917 ]);
918 for n in 0..=input.len() {
919 let prefix = &input[0..n];
920 let _ = WavHandler.inspect(prefix, &InspectOptions::names_only());
921 let _ = WavHandler.strip(prefix, &StripOptions::default());
922 }
923 }
924}