use crate::container::riff::{self, Chunk, FourCc, WalkError, name_of};
use crate::detect::Format;
use crate::error::{MalformedDetail, Result, StryptError, UnsupportedKind};
use crate::formats::{MetadataHandler, ParseLimits, StripOptions, Stripped, xmp};
use crate::report::{
Finding, InspectOptions, MetadataKind, MetadataReport, MetadataValue, Note, Retained,
RetentionReason, StripReport,
};
#[derive(Debug, Clone, Copy, Default)]
pub struct WavHandler;
impl MetadataHandler for WavHandler {
fn name(&self) -> &'static str {
Format::Wav.id()
}
fn format(&self) -> Format {
Format::Wav
}
fn inspect(&self, input: &[u8], options: &InspectOptions) -> Result<MetadataReport> {
let processed = process(input, options, &ParseLimits::default())?;
Ok(MetadataReport {
format: Format::Wav,
findings: processed.findings,
notes: processed.notes,
})
}
fn strip(&self, input: &[u8], options: &StripOptions) -> Result<Stripped> {
let processed = process(input, &options.inspect, &options.limits)?;
Ok(Stripped {
report: StripReport {
format: Format::Wav,
removed: processed.findings,
retained: processed.retained,
notes: processed.notes,
input_bytes: as_u64(input.len()),
output_bytes: as_u64(processed.output.len()),
},
bytes: processed.output,
})
}
}
const WAVE: FourCc = *b"WAVE";
const FMT_MINIMUM: usize = 16;
const KEPT: [&FourCc; 4] = [b"fmt ", b"data", b"fact", b"cue "];
const ZEROED: [&FourCc; 3] = [b"JUNK", b"PAD ", b"FLLR"];
const LIST_INFO: FourCc = *b"INFO";
const LIST_ADTL: FourCc = *b"adtl";
const LIST_WAVL: FourCc = *b"wavl";
const INFO_TAGS: &[(&[u8; 4], &str, MetadataKind)] = &[
(b"IART", "Artist", MetadataKind::PersonalIdentity),
(b"IENG", "Engineer", MetadataKind::PersonalIdentity),
(b"ITCH", "Technician", MetadataKind::PersonalIdentity),
(b"ICMS", "Commissioned", MetadataKind::PersonalIdentity),
(b"ICOP", "Copyright", MetadataKind::PersonalIdentity),
(b"IARL", "ArchivalLocation", MetadataKind::Location),
(b"ICRD", "CreationDate", MetadataKind::Timestamp),
(b"IDIT", "DigitisationTime", MetadataKind::Timestamp),
(b"ISFT", "Software", MetadataKind::SoftwareFingerprint),
(b"ITOC", "TableOfContents", MetadataKind::DocumentIdentifier),
(b"ISRC", "Source", MetadataKind::DocumentIdentifier),
(b"ICMT", "Comment", MetadataKind::Comment),
(b"INAM", "Title", MetadataKind::Comment),
(b"ISBJ", "Subject", MetadataKind::Comment),
(b"IKEY", "Keywords", MetadataKind::Comment),
(b"IPRD", "Product", MetadataKind::Comment),
(b"IGNR", "Genre", MetadataKind::Comment),
(b"IMED", "Medium", MetadataKind::Comment),
];
const BEXT_FIELDS: &[(usize, usize, &str, MetadataKind)] = &[
(0, 256, "Description", MetadataKind::Comment),
(256, 32, "Originator", MetadataKind::PersonalIdentity),
(288, 32, "OriginatorReference", MetadataKind::DeviceIdentity),
(320, 10, "OriginationDate", MetadataKind::Timestamp),
(330, 8, "OriginationTime", MetadataKind::Timestamp),
(348, 64, "UMID", MetadataKind::DocumentIdentifier),
(
602,
usize::MAX,
"CodingHistory",
MetadataKind::EditingHistory,
),
];
const CART_FIELDS: &[(usize, usize, &str, MetadataKind)] = &[
(4, 64, "Title", MetadataKind::Comment),
(68, 64, "Artist", MetadataKind::PersonalIdentity),
(132, 64, "CutID", MetadataKind::DocumentIdentifier),
(196, 64, "ClientID", MetadataKind::PersonalIdentity),
(388, 10, "StartDate", MetadataKind::Timestamp),
(420, 64, "ProducerAppID", MetadataKind::SoftwareFingerprint),
(
484,
64,
"ProducerAppVersion",
MetadataKind::SoftwareFingerprint,
),
(1024, 1024, "URL", MetadataKind::Location),
];
struct Processed {
findings: Vec<Finding>,
retained: Vec<Retained>,
notes: Vec<Note>,
output: Vec<u8>,
}
enum Outcome {
Keep,
Replace(Vec<u8>),
Drop,
}
struct Decision {
outcome: Outcome,
findings: Vec<Finding>,
retained: Vec<Retained>,
notes: Vec<Note>,
}
impl Decision {
const fn keep() -> Self {
Self {
outcome: Outcome::Keep,
findings: Vec::new(),
retained: Vec::new(),
notes: Vec::new(),
}
}
fn drop_with(findings: Vec<Finding>) -> Self {
Self {
outcome: Outcome::Drop,
findings,
retained: Vec::new(),
notes: Vec::new(),
}
}
fn drop_one(kind: MetadataKind, location: impl Into<String>, bytes: u64) -> Self {
Self::drop_with(vec![Finding::new(kind, location, bytes)])
}
}
fn process(input: &[u8], options: &InspectOptions, limits: &ParseLimits) -> Result<Processed> {
let mut budget = limits.max_items;
let (chunks, trailing) = riff::read(input, WAVE, &mut budget).map_err(convert)?;
validate_shape(&chunks)?;
let mut findings = Vec::new();
let mut retained = Vec::new();
let mut notes = Vec::new();
let mut body: Vec<u8> = Vec::with_capacity(input.len());
for chunk in &chunks {
let decision = decide(chunk, options, limits);
notes.extend(decision.notes);
retained.extend(decision.retained);
findings.extend(decision.findings);
match decision.outcome {
Outcome::Keep => body.extend_from_slice(chunk.raw),
Outcome::Replace(bytes) => body.extend_from_slice(&bytes),
Outcome::Drop => {}
}
}
if !trailing.is_empty() {
findings.push(Finding::new(
MetadataKind::Other,
"trailing data after the RIFF chunk",
as_u64(trailing.len()),
));
}
notes.push(Note::OutOfScopeContent {
location: "audio samples, which are copied without being decoded".to_owned(),
});
let output = riff::write(WAVE, &body).map_err(|d| malformed(d, None))?;
Ok(Processed {
findings,
retained,
notes,
output,
})
}
fn convert(error: WalkError) -> StryptError {
match error {
WalkError::Malformed { detail, offset } => malformed(detail, as_offset(offset)),
WalkError::Limit(limit) => StryptError::LimitExceeded {
format: Format::Wav,
limit,
},
}
}
fn validate_shape(chunks: &[Chunk<'_>]) -> Result<()> {
for chunk in chunks {
if &chunk.kind == b"LIST" && matches!(riff::list_form(chunk.data), Some((LIST_WAVL, _))) {
return Err(StryptError::UnsupportedFormat {
format: UnsupportedKind::WaveList,
});
}
if &chunk.kind == b"fmt " && chunk.data.len() < FMT_MINIMUM {
return Err(malformed(
MalformedDetail::LengthOutOfRange,
as_offset(chunk.offset),
));
}
}
for required in [b"fmt ", b"data"] {
if !chunks.iter().any(|c| &c.kind == required) {
return Err(malformed(
MalformedDetail::MissingMarker,
as_offset(riff::HEADER_BYTES),
));
}
}
Ok(())
}
fn decide(chunk: &Chunk<'_>, options: &InspectOptions, limits: &ParseLimits) -> Decision {
let size = as_u64(chunk.data.len());
let kind = &chunk.kind;
if KEPT.contains(&kind) {
return Decision::keep();
}
if ZEROED.contains(&kind) {
return zeroed(chunk);
}
match kind {
b"LIST" => list(chunk, options, limits),
b"bext" => Decision::drop_with(fields(
chunk.data,
BEXT_FIELDS,
"bext",
size,
MetadataKind::Other,
options,
)),
b"cart" => Decision::drop_with(fields(
chunk.data,
CART_FIELDS,
"cart",
size,
MetadataKind::Other,
options,
)),
b"_PMX" => {
let found = xmp::scan(chunk.data, "_PMX", options);
if found.is_empty() {
Decision::drop_one(MetadataKind::Other, "_PMX", size)
} else {
Decision::drop_with(found)
}
}
b"iXML" => Decision::drop_one(MetadataKind::DeviceIdentity, "iXML", size),
b"id3 " | b"ID3 " => Decision::drop_one(MetadataKind::Other, "id3", size),
b"DISP" => Decision::drop_one(MetadataKind::Comment, "DISP", size),
b"CSET" => Decision::drop_one(MetadataKind::Other, "CSET", size),
b"smpl" => Decision {
outcome: Outcome::Drop,
findings: vec![Finding::new(MetadataKind::DeviceIdentity, "smpl", size)],
retained: Vec::new(),
notes: vec![Note::CapabilityRemoved {
location: "smpl".to_owned(),
capability: "be looped by a sampler at the points it recorded".to_owned(),
}],
},
b"inst" => Decision::drop_one(MetadataKind::Other, "inst", size),
b"plst" => Decision::drop_one(MetadataKind::Other, "plst", size),
_ => Decision::drop_one(MetadataKind::Other, name_of(kind), size),
}
}
fn zeroed(chunk: &Chunk<'_>) -> Decision {
if chunk.data.iter().all(|b| *b == 0) {
return Decision::keep();
}
let mut out = Vec::with_capacity(chunk.raw.len());
if riff::write_chunk(&mut out, chunk.kind, &vec![0u8; chunk.data.len()]).is_err() {
return Decision::keep();
}
Decision {
outcome: Outcome::Replace(out),
findings: vec![Finding::new(
MetadataKind::Other,
name_of(&chunk.kind),
as_u64(chunk.data.len()),
)],
retained: vec![Retained {
location: name_of(&chunk.kind),
reason: RetentionReason::StructurallyRequired,
}],
notes: Vec::new(),
}
}
fn list(chunk: &Chunk<'_>, options: &InspectOptions, limits: &ParseLimits) -> Decision {
let size = as_u64(chunk.data.len());
let Some((form, rest)) = riff::list_form(chunk.data) else {
return Decision::drop_one(MetadataKind::Other, "LIST", size);
};
if form != LIST_INFO && form != LIST_ADTL {
return Decision::drop_one(
MetadataKind::Other,
format!("LIST {}", name_of(&form)),
size,
);
}
let mut budget = limits.max_items;
let Ok(items) = riff::chunks(rest, 0, &mut budget) else {
return Decision::drop_one(
MetadataKind::Other,
format!("LIST {}", name_of(&form)),
size,
);
};
let mut findings = Vec::new();
for item in &items {
let bytes = as_u64(item.data.len());
let (field, kind) = if form == LIST_INFO {
INFO_TAGS
.iter()
.find(|(tag, _, _)| *tag == &item.kind)
.map_or_else(
|| (name_of(&item.kind), MetadataKind::Other),
|(_, name, kind)| ((*name).to_owned(), *kind),
)
} else {
(name_of(&item.kind), MetadataKind::Comment)
};
findings.push(
Finding::new(kind, format!("LIST {}", name_of(&form)), bytes)
.with_field(field)
.with_value(options, || MetadataValue::Text(text_of(item.data))),
);
}
if findings.is_empty() {
findings.push(Finding::new(
MetadataKind::Other,
format!("LIST {}", name_of(&form)),
size,
));
}
Decision::drop_with(findings)
}
fn fields(
data: &[u8],
table: &[(usize, usize, &str, MetadataKind)],
location: &str,
size: u64,
fallback: MetadataKind,
options: &InspectOptions,
) -> Vec<Finding> {
let mut out = Vec::new();
for (at, len, name, kind) in table {
let Some(tail) = data.get(*at..) else {
continue;
};
let raw = tail.get(..*len).unwrap_or(tail);
let value = text_of(raw);
if value.is_empty() {
continue;
}
out.push(
Finding::new(*kind, location.to_owned(), as_u64(raw.len()))
.with_field((*name).to_owned())
.with_value(options, || MetadataValue::Text(value.clone())),
);
}
if out.is_empty() {
out.push(Finding::new(fallback, location.to_owned(), size));
}
out
}
fn text_of(raw: &[u8]) -> String {
let end = raw.iter().position(|b| *b == 0).unwrap_or(raw.len());
let head = raw.get(..end).unwrap_or_default();
xmp::name_of(head).trim().to_owned()
}
fn malformed(detail: MalformedDetail, offset: Option<u64>) -> StryptError {
StryptError::Malformed {
format: Format::Wav,
offset,
detail,
}
}
fn as_offset(position: usize) -> Option<u64> {
u64::try_from(position).ok()
}
fn as_u64(value: usize) -> u64 {
u64::try_from(value).unwrap_or(u64::MAX)
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::arithmetic_side_effects
)]
use super::*;
fn chunk(kind: FourCc, payload: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
riff::write_chunk(&mut out, kind, payload).unwrap();
out
}
fn fmt() -> Vec<u8> {
let mut p = Vec::new();
p.extend_from_slice(&1u16.to_le_bytes()); p.extend_from_slice(&1u16.to_le_bytes()); p.extend_from_slice(&8000u32.to_le_bytes());
p.extend_from_slice(&16000u32.to_le_bytes());
p.extend_from_slice(&2u16.to_le_bytes());
p.extend_from_slice(&16u16.to_le_bytes());
chunk(*b"fmt ", &p)
}
fn data() -> Vec<u8> {
chunk(*b"data", b"SYNTHETIC-SAMPLES-0001\0\0")
}
fn wav(parts: &[Vec<u8>]) -> Vec<u8> {
riff::write(WAVE, &parts.concat()).unwrap()
}
fn strip_ok(input: &[u8]) -> Stripped {
WavHandler
.strip(input, &StripOptions::default())
.expect("strip failed")
}
fn findings(input: &[u8]) -> Vec<Finding> {
WavHandler
.inspect(input, &InspectOptions::names_only())
.expect("inspect failed")
.findings
}
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
haystack.windows(needle.len()).any(|w| w == needle)
}
fn info(tags: &[(FourCc, &[u8])]) -> Vec<u8> {
let mut body = LIST_INFO.to_vec();
for (tag, value) in tags {
let mut v = value.to_vec();
v.push(0);
body.extend_from_slice(&chunk(*tag, &v));
}
chunk(*b"LIST", &body)
}
#[test]
fn a_clean_file_comes_back_byte_identical() {
let input = wav(&[fmt(), data()]);
let stripped = strip_ok(&input);
assert!(stripped.report.removed.is_empty());
assert_eq!(stripped.bytes, input, "a clean WAV was rewritten");
}
#[test]
fn the_audio_crosses_byte_for_byte() {
let input = wav(&[fmt(), info(&[(*b"IART", b"SYNTHETIC-ARTIST-0002")]), data()]);
let output = strip_ok(&input).bytes;
assert!(contains(&output, b"SYNTHETIC-SAMPLES-0001"));
assert!(!contains(&output, b"SYNTHETIC-ARTIST-0002"));
}
#[test]
fn an_info_list_is_itemised_by_tag() {
let input = wav(&[
fmt(),
info(&[
(*b"IART", b"SYNTHETIC-ARTIST-0002"),
(*b"ISFT", b"SYNTHETIC-RECORDER-0003"),
(*b"ICRD", b"2026-09-01"),
]),
data(),
]);
let found = findings(&input);
let fields: Vec<&str> = found.iter().filter_map(|f| f.field.as_deref()).collect();
assert!(fields.contains(&"Artist"), "{fields:?}");
assert!(fields.contains(&"Software"), "{fields:?}");
assert!(fields.contains(&"CreationDate"), "{fields:?}");
let kinds: Vec<MetadataKind> = found.iter().map(|f| f.kind).collect();
assert!(kinds.contains(&MetadataKind::PersonalIdentity));
assert!(kinds.contains(&MetadataKind::SoftwareFingerprint));
assert!(kinds.contains(&MetadataKind::Timestamp));
}
#[test]
fn an_unrecognised_info_tag_is_still_removed_and_reported() {
let input = wav(&[
fmt(),
info(&[(*b"IZZZ", b"SYNTHETIC-PRIVATE-0004")]),
data(),
]);
let stripped = strip_ok(&input);
assert!(!contains(&stripped.bytes, b"SYNTHETIC-PRIVATE-0004"));
assert_eq!(stripped.report.removed[0].field.as_deref(), Some("IZZZ"));
}
#[test]
fn a_broadcast_extension_is_itemised_down_to_its_coding_history() {
let mut p = vec![0u8; 602];
p[0..9].copy_from_slice(b"SYNTHETIC");
p[256..265].copy_from_slice(b"ORIG-0005");
p[288..297].copy_from_slice(b"REF--0006");
p[320..330].copy_from_slice(b"2026-09-01");
p[348..357].copy_from_slice(b"UMID-0007");
p.extend_from_slice(b"A=PCM,F=8000,W=16,M=mono,T=SYNTHETIC-DECK-0008");
let input = wav(&[fmt(), chunk(*b"bext", &p), data()]);
let found = findings(&input);
let fields: Vec<&str> = found.iter().filter_map(|f| f.field.as_deref()).collect();
for want in [
"Description",
"Originator",
"OriginatorReference",
"OriginationDate",
"UMID",
"CodingHistory",
] {
assert!(fields.contains(&want), "{want} missing from {fields:?}");
}
let stripped = strip_ok(&input);
assert!(!contains(&stripped.bytes, b"SYNTHETIC-DECK-0008"));
assert!(!contains(&stripped.bytes, b"UMID-0007"));
}
#[test]
fn an_empty_broadcast_extension_still_yields_one_finding() {
let input = wav(&[fmt(), chunk(*b"bext", &[0u8; 602]), data()]);
let found = findings(&input);
assert_eq!(found.len(), 1);
assert_eq!(found[0].location, "bext");
}
#[test]
fn an_id3_tag_is_dropped_whole_without_being_parsed() {
let mut tag = b"ID3\x04\x00\x00\x00\x00\x00\x20".to_vec();
tag.extend_from_slice(b"TPE1SYNTHETIC-ID3-ARTIST-0009");
let input = wav(&[fmt(), data(), chunk(*b"id3 ", &tag)]);
let stripped = strip_ok(&input);
assert!(!contains(&stripped.bytes, b"SYNTHETIC-ID3-ARTIST-0009"));
assert_eq!(stripped.report.removed[0].location, "id3");
}
#[test]
fn an_xmp_packet_is_itemised_by_property() {
let input = wav(&[
fmt(),
data(),
chunk(
*b"_PMX",
br#"<x:xmpmeta xmpMM:DocumentID="uuid:1" xmp:CreatorTool="SYNTHETIC"/>"#,
),
]);
let fields: Vec<String> = findings(&input)
.iter()
.filter_map(|f| f.field.clone())
.collect();
assert!(fields.iter().any(|f| f == "xmpMM:DocumentID"), "{fields:?}");
}
#[test]
fn a_sampler_chunk_says_what_the_file_can_no_longer_do() {
let input = wav(&[fmt(), data(), chunk(*b"smpl", &[0u8; 36])]);
let stripped = strip_ok(&input);
assert!(matches!(
stripped.report.notes.first(),
Some(Note::CapabilityRemoved { location, .. }) if location == "smpl"
));
}
#[test]
fn padding_is_zeroed_at_its_length_rather_than_dropped() {
let input = wav(&[fmt(), chunk(*b"JUNK", b"SYNTHETIC-LEFTOVER-0010!"), data()]);
let stripped = strip_ok(&input);
assert!(!contains(&stripped.bytes, b"SYNTHETIC-LEFTOVER-0010"));
assert_eq!(
stripped.bytes.len(),
input.len(),
"zeroing changed the file's length"
);
assert_eq!(
stripped.report.retained[0].reason,
RetentionReason::StructurallyRequired
);
}
#[test]
fn padding_that_is_already_zero_is_copied_rather_than_rebuilt() {
let input = wav(&[fmt(), chunk(*b"JUNK", &[0u8; 32]), data()]);
let stripped = strip_ok(&input);
assert!(stripped.report.removed.is_empty());
assert_eq!(stripped.bytes, input);
}
#[test]
fn a_cue_chunk_survives_because_removing_metadata_cannot_move_its_offsets() {
let mut cue = 1u32.to_le_bytes().to_vec();
cue.extend_from_slice(&[0u8; 24]);
let input = wav(&[fmt(), data(), chunk(*b"cue ", &cue)]);
assert_eq!(strip_ok(&input).bytes, input);
}
#[test]
fn an_unknown_chunk_is_removed_rather_than_preserved() {
let input = wav(&[fmt(), data(), chunk(*b"PrVw", b"SYNTHETIC-PRIVATE-0011")]);
let stripped = strip_ok(&input);
assert!(!contains(&stripped.bytes, b"SYNTHETIC-PRIVATE-0011"));
assert_eq!(stripped.report.removed[0].location, "PrVw");
}
#[test]
fn data_after_the_riff_chunk_is_removed() {
let mut input = wav(&[fmt(), data()]);
input.extend_from_slice(b"SYNTHETIC-APPENDED-0012");
let stripped = strip_ok(&input);
assert!(!contains(&stripped.bytes, b"SYNTHETIC-APPENDED-0012"));
assert_eq!(
stripped.report.removed[0].location,
"trailing data after the RIFF chunk"
);
}
#[test]
fn a_file_with_no_format_or_no_audio_is_refused_rather_than_emptied() {
for parts in [
vec![fmt()],
vec![data()],
vec![chunk(*b"bext", &[0u8; 602])],
] {
assert!(
matches!(
WavHandler.strip(&wav(&parts), &StripOptions::default()),
Err(StryptError::Malformed {
detail: MalformedDetail::MissingMarker,
..
})
),
"a WAV missing fmt or data was accepted"
);
}
}
#[test]
fn a_wave_list_is_refused_by_name_rather_than_edited() {
let mut body = LIST_WAVL.to_vec();
body.extend_from_slice(&chunk(*b"data", b"AB"));
let input = wav(&[fmt(), chunk(*b"LIST", &body), data()]);
assert!(matches!(
WavHandler.inspect(&input, &InspectOptions::names_only()),
Err(StryptError::UnsupportedFormat {
format: UnsupportedKind::WaveList,
})
));
}
#[test]
fn a_format_chunk_shorter_than_the_pcm_form_is_refused() {
let input = wav(&[chunk(*b"fmt ", &[0u8; 8]), data()]);
assert!(matches!(
WavHandler.inspect(&input, &InspectOptions::names_only()),
Err(StryptError::Malformed {
detail: MalformedDetail::LengthOutOfRange,
..
})
));
}
#[test]
fn a_riff_size_beyond_the_end_of_the_file_is_refused_rather_than_clamped() {
let mut input = wav(&[fmt(), data()]);
input[4..8].copy_from_slice(&0x00FF_FFFFu32.to_le_bytes());
assert!(matches!(
WavHandler.inspect(&input, &InspectOptions::names_only()),
Err(StryptError::Malformed {
detail: MalformedDetail::LengthOutOfRange,
..
})
));
}
#[test]
fn a_chunk_count_beyond_the_limit_is_refused() {
let mut parts = vec![fmt(), data()];
parts.extend((0..64).map(|_| chunk(*b"PrVw", b"xx")));
let options = StripOptions {
limits: ParseLimits {
max_items: 8,
..ParseLimits::default()
},
..StripOptions::default()
};
assert!(matches!(
WavHandler.strip(&wav(&parts), &options),
Err(StryptError::LimitExceeded { .. })
));
}
#[test]
fn values_are_withheld_from_a_default_inspection() {
let input = wav(&[fmt(), info(&[(*b"IART", b"SYNTHETIC-ARTIST-0002")]), data()]);
assert_eq!(findings(&input)[0].value, None);
let with_values = WavHandler
.inspect(&input, &InspectOptions::with_values())
.unwrap();
assert_eq!(
with_values.findings[0].value,
Some(MetadataValue::Text("SYNTHETIC-ARTIST-0002".to_owned()))
);
}
#[test]
fn the_uninspected_audio_is_declared_on_every_file() {
let stripped = strip_ok(&wav(&[fmt(), data()]));
assert!(
stripped
.report
.notes
.iter()
.any(|n| matches!(n, Note::OutOfScopeContent { location } if location.starts_with("audio samples")))
);
}
#[test]
fn stripping_twice_changes_nothing() {
let input = wav(&[
fmt(),
info(&[(*b"IART", b"SYNTHETIC-ARTIST-0002")]),
chunk(*b"JUNK", b"SYNTHETIC-LEFTOVER-0010!"),
data(),
chunk(*b"bext", &[0u8; 602]),
]);
let once = strip_ok(&input).bytes;
let twice = strip_ok(&once).bytes;
assert_eq!(once, twice, "strip is not idempotent");
assert!(findings(&once).is_empty(), "a strip left something behind");
}
#[test]
fn an_odd_length_chunk_keeps_its_padding_byte() {
let input = wav(&[fmt(), chunk(*b"data", b"ODD")]);
let stripped = strip_ok(&input);
assert_eq!(stripped.bytes, input);
assert_eq!(stripped.bytes.len() % 2, 0);
}
#[test]
fn truncation_at_every_length_is_refused_or_survived_but_never_panics() {
let input = wav(&[
fmt(),
info(&[(*b"IART", b"SYNTHETIC-ARTIST-0002")]),
chunk(*b"bext", &[7u8; 700]),
chunk(*b"JUNK", b"x"),
data(),
chunk(*b"smpl", &[0u8; 36]),
]);
for n in 0..=input.len() {
let prefix = &input[0..n];
let _ = WavHandler.inspect(prefix, &InspectOptions::names_only());
let _ = WavHandler.strip(prefix, &StripOptions::default());
}
}
}