use crate::bytes::read_u32_le;
use crate::error::{FormatError, Result};
use crate::input::{BinaryTagInput, EmbeddedBinaryTag, EmbeddedPicture};
use crate::probe::Extent;
use crate::size;
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WavBounds {
pub audio_offset: u64,
pub audio_length: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WavScan {
pub fmt: Vec<u8>,
pub fact: Option<Vec<u8>>,
}
fn riff_wave_start(buf: &[u8]) -> Result<(usize, u64)> {
if buf.len() < 12 || &buf[0..4] != b"RIFF" || &buf[8..12] != b"WAVE" {
return Err(FormatError::NotWav);
}
let riff_size =
read_u32_le(buf, 4).expect("RIFF size field within the validated 12-byte header");
Ok((12, 8 + u64::from(riff_size)))
}
fn walk_chunks(buf: &[u8]) -> Vec<([u8; 4], usize, u64)> {
let mut out = Vec::new();
let Ok((mut pos, form_end)) = riff_wave_start(buf) else {
return out;
};
let ceiling = crate::convert::usize_from(form_end.min(buf.len() as u64));
while pos + 8 <= ceiling {
let mut id = [0u8; 4];
id.copy_from_slice(&buf[pos..pos + 4]);
let size = u64::from(
read_u32_le(buf, pos + 4).expect("chunk size field within the loop-guarded bounds"),
);
let payload_offset = pos + 8;
out.push((id, payload_offset, size));
let advance = 8u64 + size + (size & 1); match (pos as u64).checked_add(advance) {
Some(next) if next <= ceiling as u64 => pos = crate::convert::usize_from(next),
_ => break,
}
}
out
}
fn chunk_slice(buf: &[u8], offset: usize, len: u64) -> Option<&[u8]> {
let end = offset.checked_add(crate::convert::usize_from(len))?;
buf.get(offset..end)
}
pub fn locate_audio(buf: &[u8]) -> Result<WavBounds> {
let (_, form_end) = riff_wave_start(buf)?;
if form_end > buf.len() as u64 {
return Err(FormatError::Malformed);
}
let chunks = walk_chunks(buf);
let has_fmt = chunks.iter().any(|(id, _, _)| id == b"fmt ");
let data = chunks.iter().find(|(id, _, _)| id == b"data");
match (has_fmt, data) {
(true, Some(&(_, off, len))) => {
let data_end = (off as u64).saturating_add(len);
if data_end > form_end {
return Err(FormatError::Malformed);
}
Ok(WavBounds {
audio_offset: off as u64,
audio_length: len,
})
}
_ => Err(FormatError::NotWav),
}
}
pub fn locate_audio_bounded(prefix: &[u8], file_len: u64) -> Result<Extent<WavBounds>> {
if (prefix.len() as u64) < file_len {
return Ok(Extent::NeedMore { up_to: file_len });
}
Ok(Extent::Complete(locate_audio(prefix)?))
}
pub fn locate_audio_at_ceiling(prefix: &[u8], file_len: u64) -> Result<WavBounds> {
let (_, form_end) = riff_wave_start(prefix)?;
if form_end > file_len {
return Err(FormatError::Malformed);
}
let chunks = walk_chunks(prefix);
let has_fmt = chunks.iter().any(|(id, _, _)| id == b"fmt ");
let data = chunks.iter().find(|(id, _, _)| id == b"data");
match (has_fmt, data) {
(true, Some(&(_, off, len))) => {
let data_end = (off as u64).saturating_add(len);
if data_end > form_end {
return Err(FormatError::Malformed);
}
Ok(WavBounds {
audio_offset: off as u64,
audio_length: len,
})
}
_ => Err(FormatError::NotWav),
}
}
pub fn read_structure(front: &[u8]) -> Result<WavScan> {
riff_wave_start(front)?;
let chunks = walk_chunks(front);
let &(_, fmt_off, fmt_len) = chunks
.iter()
.find(|(id, _, _)| id == b"fmt ")
.ok_or(FormatError::NotWav)?;
let fmt = chunk_slice(front, fmt_off, fmt_len)
.ok_or(FormatError::Malformed)?
.to_vec();
let fact = match chunks.iter().find(|(id, _, _)| id == b"fact") {
Some(&(_, off, len)) => Some(
chunk_slice(front, off, len)
.ok_or(FormatError::Malformed)?
.to_vec(),
),
None => None,
};
Ok(WavScan { fmt, fact })
}
use crate::input::{ArtInput, TagInput};
use crate::layout::{RegionLayout, Segment};
fn info_fourcc(key: &str) -> Option<&'static [u8; 4]> {
Some(match key {
"title" => b"INAM",
"artist" => b"IART",
"album" => b"IPRD",
"date" => b"ICRD",
"genre" => b"IGNR",
"comment" => b"ICMT",
"tracknumber" => b"ITRK",
_ => return None,
})
}
fn build_info_payload(tags: &[TagInput]) -> Result<Option<Vec<u8>>> {
let mut entries: Vec<(&'static [u8; 4], &str)> = Vec::new();
let mut used: Vec<&str> = Vec::new();
for t in tags {
if used.contains(&t.key.as_str()) {
continue;
}
if let Some(cc) = info_fourcc(&t.key) {
used.push(t.key.as_str());
entries.push((cc, t.value.as_str()));
}
}
if entries.is_empty() {
return Ok(None);
}
let mut payload = Vec::new();
payload.extend_from_slice(b"INFO");
for (cc, value) in entries {
let mut v = value.as_bytes().to_vec();
v.push(0x00); append_chunk(&mut payload, cc, &v)?;
}
Ok(Some(payload))
}
fn chunk_header(id: &[u8; 4], len: u32) -> [u8; 8] {
let mut h = [0u8; 8];
h[..4].copy_from_slice(id);
h[4..].copy_from_slice(&len.to_le_bytes());
h
}
fn append_chunk(out: &mut Vec<u8>, id: &[u8; 4], payload: &[u8]) -> Result<()> {
let len = u32::try_from(payload.len()).map_err(|_| FormatError::TooLarge)?;
out.extend_from_slice(&chunk_header(id, len));
out.extend_from_slice(payload);
if payload.len() % 2 == 1 {
out.push(0x00);
}
Ok(())
}
fn push_inline_chunk(segments: &mut Vec<Segment>, id: &[u8; 4], payload: &[u8]) -> Result<()> {
let mut chunk = Vec::with_capacity(8 + payload.len() + 1);
append_chunk(&mut chunk, id, payload)?;
segments.push(Segment::Inline(chunk));
Ok(())
}
pub fn synthesize_layout(
scan: &WavScan,
audio_offset: u64,
audio_length: u64,
tags: &[TagInput],
binary_tags: &[BinaryTagInput],
arts: &[ArtInput],
) -> Result<RegionLayout> {
let audio_length_u32 = u32::try_from(audio_length).map_err(|_| FormatError::TooLarge)?;
let mut segments: Vec<Segment> = Vec::new();
push_inline_chunk(&mut segments, b"fmt ", &scan.fmt)?;
if let Some(fact) = &scan.fact {
push_inline_chunk(&mut segments, b"fact", fact)?;
}
if let Some(info) = build_info_payload(tags)? {
push_inline_chunk(&mut segments, b"LIST", &info)?;
}
let (tag_segments, tag_len) = crate::mp3::build_id3v2_segments(tags, binary_tags, arts)?;
let tag_len_u32 = u32::try_from(tag_len).map_err(|_| FormatError::TooLarge)?;
segments.push(Segment::Inline(chunk_header(b"id3 ", tag_len_u32).to_vec()));
segments.extend(tag_segments);
if tag_len % 2 == 1 {
segments.push(Segment::Inline(vec![0x00]));
}
segments.push(Segment::Inline(
chunk_header(b"data", audio_length_u32).to_vec(),
));
segments.push(Segment::BackingAudio {
offset: audio_offset,
len: audio_length,
});
if audio_length % 2 == 1 {
segments.push(Segment::Inline(vec![0x00]));
}
let body_len: u64 = size::checked_sum(segments.iter().map(Segment::len))?;
let riff_size =
u32::try_from(size::checked_add(body_len, 4)?).map_err(|_| FormatError::TooLarge)?;
let mut header = Vec::with_capacity(12);
header.extend_from_slice(b"RIFF");
header.extend_from_slice(&riff_size.to_le_bytes());
header.extend_from_slice(b"WAVE");
segments.insert(0, Segment::Inline(header));
Ok(RegionLayout::validated(segments)?)
}
fn info_to_key(id: &[u8; 4]) -> Option<&'static str> {
Some(match id {
b"INAM" => "title",
b"IART" => "artist",
b"IPRD" => "album",
b"ICRD" => "date",
b"IGNR" => "genre",
b"ICMT" => "comment",
b"ITRK" => "tracknumber",
_ => return None,
})
}
fn find_id3_chunk<'a>(buf: &'a [u8], chunks: &[([u8; 4], usize, u64)]) -> Option<&'a [u8]> {
let &(_, off, len) = chunks
.iter()
.find(|(id, _, _)| id == b"id3 " || id == b"ID3 ")?;
chunk_slice(buf, off, len)
}
fn read_info_tags(body: &[u8]) -> Vec<(String, String)> {
let mut out = Vec::new();
let mut pos = 0usize;
while pos + 8 <= body.len() {
let mut id = [0u8; 4];
id.copy_from_slice(&body[pos..pos + 4]);
let size = read_u32_le(body, pos + 4)
.expect("subchunk size field within the loop-guarded bounds")
as usize;
let val_start = pos + 8;
let val_end = val_start.saturating_add(size).min(body.len());
if let Some(key) = info_to_key(&id) {
let raw = String::from_utf8_lossy(&body[val_start..val_end]);
let value = raw.trim_end_matches('\0').to_string();
if !value.is_empty() {
out.push((key.to_string(), value));
}
}
pos = val_start + size + (size & 1);
}
out
}
pub fn read_tags(buf: &[u8]) -> Vec<(String, String)> {
let chunks = walk_chunks(buf);
let from_id3 = find_id3_chunk(buf, &chunks)
.map(crate::mp3::read_tags)
.unwrap_or_default();
let from_info = chunks
.iter()
.find(|(id, _, _)| id == b"LIST")
.and_then(|&(_, off, len)| chunk_slice(buf, off, len))
.filter(|slice| slice.len() >= 4 && &slice[0..4] == b"INFO")
.map(|slice| read_info_tags(&slice[4..]))
.unwrap_or_default();
let id3_keys: HashSet<&str> = from_id3.iter().map(|(k, _)| k.as_str()).collect();
let mut out = from_id3.clone();
for (k, v) in from_info {
if !id3_keys.contains(k.as_str()) {
out.push((k, v));
}
}
out
}
pub fn read_binary_tags(data: &[u8]) -> (Vec<EmbeddedBinaryTag>, Vec<(String, String)>) {
let chunks = walk_chunks(data);
match find_id3_chunk(data, &chunks) {
Some(id3_bytes) => crate::mp3::read_binary_tags(id3_bytes),
None => (Vec::new(), Vec::new()),
}
}
pub fn read_pictures(buf: &[u8]) -> Vec<EmbeddedPicture> {
let chunks = walk_chunks(buf);
find_id3_chunk(buf, &chunks)
.map(crate::mp3::read_pictures)
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wav_oom_crash_artifact_is_safe() {
const CRASH: &[u8] = &[
0x52, 0x49, 0x46, 0x46, 0x32, 0x00, 0x00, 0x00, 0x57, 0x41, 0x56, 0x45, 0x49, 0x44,
0x33, 0x20, 0x38, 0x00, 0x00, 0x00, 0x52, 0x49, 0x46, 0x46, 0x32, 0x00, 0x00, 0x00,
0x57, 0x41, 0x56, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x44, 0x33, 0x20, 0x15, 0x00, 0x00, 0x00,
0x49, 0x44, 0x33, 0x03, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x54, 0x44, 0x41, 0x03,
0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0x00, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
assert!(
read_tags(CRASH).is_empty(),
"read_tags must not OOM on WAV crash artifact"
);
assert!(
read_pictures(CRASH).is_empty(),
"read_pictures must not OOM on WAV crash artifact"
);
}
#[test]
fn riff_wave_start_accepts_exactly_twelve_bytes() {
let buf = b"RIFF\0\0\0\0WAVE".to_vec();
assert_eq!(buf.len(), 12);
assert_eq!(riff_wave_start(&buf), Ok((12, 8)));
}
#[test]
fn riff_wave_start_rejects_eleven_byte_riff_without_panic() {
let buf = b"RIFF\0\0\0\0WAV".to_vec();
assert_eq!(buf.len(), 11);
assert_eq!(riff_wave_start(&buf), Err(FormatError::NotWav));
}
fn wav(chunks: &[(&[u8; 4], Vec<u8>)]) -> Vec<u8> {
let mut body = Vec::new();
for (id, payload) in chunks {
body.extend_from_slice(*id);
body.extend_from_slice(&u32::try_from(payload.len()).unwrap().to_le_bytes());
body.extend_from_slice(payload);
if payload.len() % 2 == 1 {
body.push(0x00);
}
}
let mut out = b"RIFF".to_vec();
out.extend_from_slice(&u32::try_from(body.len() + 4).unwrap().to_le_bytes());
out.extend_from_slice(b"WAVE");
out.extend_from_slice(&body);
out
}
#[test]
fn walk_chunks_advances_past_each_payload() {
let buf = wav(&[(b"AAAA", vec![0x11; 3]), (b"data", vec![0xBB; 8])]);
let ids: Vec<[u8; 4]> = walk_chunks(&buf).iter().map(|(id, _, _)| *id).collect();
assert_eq!(ids, vec![*b"AAAA", *b"data"]);
}
fn fmt_pcm() -> Vec<u8> {
let mut f = Vec::new();
f.extend_from_slice(&1u16.to_le_bytes());
f.extend_from_slice(&1u16.to_le_bytes());
f.extend_from_slice(&44_100u32.to_le_bytes());
f.extend_from_slice(&88_200u32.to_le_bytes());
f.extend_from_slice(&2u16.to_le_bytes());
f.extend_from_slice(&16u16.to_le_bytes());
f
}
#[test]
fn locate_requires_fmt_chunk() {
let buf = wav(&[(b"data", vec![0x11; 8])]);
assert_eq!(locate_audio(&buf), Err(FormatError::NotWav));
}
#[test]
fn locate_accepts_data_with_trailing_chunk() {
let buf = wav(&[
(b"fmt ", fmt_pcm()),
(b"data", vec![0x11; 8]),
(b"junk", vec![0x00; 4]),
]);
let bounds = locate_audio(&buf).unwrap();
assert_eq!(bounds.audio_length, 8);
}
#[test]
fn info_fourcc_emits_each_mapped_key() {
let cases: [(&str, &[u8; 4]); 6] = [
("artist", b"IART"),
("album", b"IPRD"),
("date", b"ICRD"),
("genre", b"IGNR"),
("comment", b"ICMT"),
("tracknumber", b"ITRK"),
];
for (key, cc) in cases {
let payload = build_info_payload(&[TagInput::new(key, "X")])
.unwrap()
.unwrap_or_else(|| panic!("INFO payload for {key}"));
assert!(
payload.windows(4).any(|w| w == &cc[..]),
"key {key} must emit FourCC {:?}",
std::str::from_utf8(cc).unwrap()
);
}
}
#[test]
fn build_info_payload_word_aligns_values() {
let even = build_info_payload(&[TagInput::new("title", "a")])
.unwrap()
.unwrap();
assert_eq!(even.len(), 14);
let odd = build_info_payload(&[TagInput::new("title", "ab")])
.unwrap()
.unwrap();
assert_eq!(odd.len(), 16);
}
#[test]
fn push_inline_chunk_word_aligns_payload() {
let mut segs = Vec::new();
push_inline_chunk(&mut segs, b"test", &[0xAA, 0xBB]).unwrap();
assert_eq!(segs.len(), 1);
assert_eq!(segs[0].len(), 10);
let mut segs2 = Vec::new();
push_inline_chunk(&mut segs2, b"test", &[0xAA, 0xBB, 0xCC]).unwrap();
assert_eq!(segs2[0].len(), 12); }
fn info_payload(pairs: &[(&[u8; 4], &str)]) -> Vec<u8> {
let mut p = b"INFO".to_vec();
for (cc, val) in pairs {
let mut v = val.as_bytes().to_vec();
v.push(0x00);
p.extend_from_slice(*cc);
p.extend_from_slice(&u32::try_from(v.len()).unwrap().to_le_bytes());
p.extend_from_slice(&v);
if v.len() % 2 == 1 {
p.push(0x00);
}
}
p
}
#[test]
fn info_to_key_decodes_each_mapped_fourcc() {
let cases: [(&[u8; 4], &str, &str); 4] = [
(b"IPRD", "album", "Anthology"),
(b"ICRD", "date", "1999"),
(b"ICMT", "comment", "Nice"),
(b"ITRK", "tracknumber", "3"),
];
for (cc, key, val) in cases {
let buf = wav(&[
(b"fmt ", fmt_pcm()),
(b"LIST", info_payload(&[(cc, val)])),
(b"data", vec![0x00; 4]),
]);
let tags = read_tags(&buf);
assert!(
tags.contains(&(key.to_string(), val.to_string())),
"FourCC {:?} must decode to {key}",
std::str::from_utf8(cc).unwrap()
);
}
}
#[test]
fn read_tags_rejects_short_list_without_panic() {
let buf = wav(&[
(b"fmt ", fmt_pcm()),
(b"LIST", vec![0x49, 0x4E]), (b"data", vec![0x00; 4]),
]);
assert!(read_tags(&buf).is_empty());
}
fn inline_offset_of(layout: &RegionLayout, fourcc: &[u8; 4]) -> u64 {
let mut off = 0u64;
for s in layout.segments() {
if let Segment::Inline(b) = s
&& b.len() >= 4
&& &b[0..4] == fourcc
{
return off;
}
off += s.len();
}
panic!("no inline chunk starting with {fourcc:?}");
}
#[test]
fn synthesize_word_aligns_embedded_id3_chunk() {
let mut tags = Vec::new();
let mut tag_len = 0u64;
for n in 1..64 {
let cand = vec![TagInput::new("albumartist", &"x".repeat(n))];
let (_, tl) = crate::mp3::build_id3v2_segments(&cand, &[], &[]).unwrap();
if tl % 2 == 1 {
tags = cand;
tag_len = tl;
break;
}
}
assert_eq!(tag_len % 2, 1, "expected to find an odd-length id3 tag");
let scan = WavScan {
fmt: fmt_pcm(),
fact: None,
};
let layout = synthesize_layout(&scan, 0, 8, &tags, &[], &[]).unwrap();
assert_eq!(
inline_offset_of(&layout, b"data") % 2,
0,
"the data chunk must be word-aligned"
);
}
#[test]
fn synthesize_rejects_riff_size_overflow() {
let scan = WavScan {
fmt: fmt_pcm(),
fact: None,
};
let res = synthesize_layout(&scan, 0, u64::from(u32::MAX), &[], &[], &[]);
assert_eq!(res, Err(FormatError::TooLarge));
}
fn wav_file(audio: &[u8]) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(b"fmt ");
body.extend_from_slice(&16u32.to_le_bytes());
body.extend(std::iter::repeat_n(0u8, 16));
body.extend_from_slice(b"data");
body.extend_from_slice(&u32::try_from(audio.len()).unwrap().to_le_bytes());
body.extend_from_slice(audio);
let mut v = b"RIFF".to_vec();
v.extend_from_slice(&u32::try_from(4 + body.len()).unwrap().to_le_bytes());
v.extend_from_slice(b"WAVE");
v.extend_from_slice(&body);
v
}
#[test]
fn locate_audio_bounded_complete_when_prefix_is_whole_file() {
let full = wav_file(b"AUDIOAUDIO");
let file_len = full.len() as u64;
match locate_audio_bounded(&full, file_len).unwrap() {
Extent::Complete(b) => assert_eq!(b.audio_length, 10),
other @ Extent::NeedMore { .. } => panic!("expected Complete, got {other:?}"),
}
}
#[test]
fn locate_audio_bounded_needmore_when_prefix_short() {
let full = wav_file(b"AUDIOAUDIO");
let file_len = full.len() as u64;
let prefix = &full[..full.len() - 4];
match locate_audio_bounded(prefix, file_len).unwrap() {
Extent::NeedMore { up_to } => assert_eq!(up_to, file_len),
other @ Extent::Complete(_) => panic!("expected NeedMore, got {other:?}"),
}
}
fn wav_front(data_len: u64) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(b"fmt ");
body.extend_from_slice(&16u32.to_le_bytes());
body.extend(std::iter::repeat_n(0u8, 16));
body.extend_from_slice(b"data");
body.extend_from_slice(&u32::try_from(data_len).unwrap().to_le_bytes());
let mut v = b"RIFF".to_vec();
let riff_size = 36u32 + u32::try_from(data_len).unwrap(); v.extend_from_slice(&riff_size.to_le_bytes());
v.extend_from_slice(b"WAVE");
v.extend_from_slice(&body);
v
}
#[test]
fn locate_audio_at_ceiling_trusts_data_header_without_payload() {
let data_len = 200u64;
let front = wav_front(data_len);
let audio_offset = front.len() as u64; let file_len = audio_offset + data_len;
let b = locate_audio_at_ceiling(&front, file_len).unwrap();
assert_eq!(b.audio_offset, audio_offset);
assert_eq!(b.audio_length, data_len);
}
#[test]
fn locate_audio_at_ceiling_accepts_data_shorter_than_file() {
let data_len = 200u64;
let front = wav_front(data_len);
let audio_offset = front.len() as u64;
let file_len = audio_offset + data_len + 64;
let b = locate_audio_at_ceiling(&front, file_len).unwrap();
assert_eq!(b.audio_offset, audio_offset);
assert_eq!(b.audio_length, data_len);
}
#[test]
fn locate_audio_at_ceiling_rejects_data_running_past_file() {
let front = wav_front(1_000);
let audio_offset = front.len() as u64;
let file_len = audio_offset + 10;
assert_eq!(
locate_audio_at_ceiling(&front, file_len),
Err(FormatError::Malformed)
);
}
#[test]
fn locate_audio_at_ceiling_requires_fmt_chunk() {
let mut body = Vec::new();
body.extend_from_slice(b"data");
body.extend_from_slice(&200u32.to_le_bytes());
let mut front = b"RIFF".to_vec();
front.extend_from_slice(&0u32.to_le_bytes());
front.extend_from_slice(b"WAVE");
front.extend_from_slice(&body);
let file_len = front.len() as u64 + 200;
assert_eq!(
locate_audio_at_ceiling(&front, file_len),
Err(FormatError::NotWav)
);
}
#[test]
fn locate_audio_rejects_form_end_before_data() {
let mut buf = wav(&[(b"fmt ", fmt_pcm()), (b"data", vec![0x11; 8])]);
buf[4..8].copy_from_slice(&40u32.to_le_bytes()); assert_eq!(locate_audio(&buf), Err(FormatError::Malformed));
}
#[test]
fn locate_audio_rejects_form_end_past_file() {
let mut buf = wav(&[(b"fmt ", fmt_pcm()), (b"data", vec![0x11; 8])]);
let huge = u32::try_from(buf.len()).unwrap() + 100;
buf[4..8].copy_from_slice(&huge.to_le_bytes()); assert_eq!(locate_audio(&buf), Err(FormatError::Malformed));
}
#[test]
fn locate_audio_accepts_valid_form_with_odd_chunk_and_trailing_metadata() {
let buf = wav(&[
(b"fmt ", fmt_pcm()),
(b"data", vec![0x22; 7]), (b"LIST", vec![0x33; 4]),
]);
let b = locate_audio(&buf).unwrap();
assert_eq!(b.audio_length, 7);
}
#[test]
fn locate_audio_at_ceiling_rejects_form_end_before_data() {
let mut buf = wav(&[(b"fmt ", fmt_pcm()), (b"data", vec![0x11; 8])]);
let file_len = buf.len() as u64;
buf[4..8].copy_from_slice(&40u32.to_le_bytes()); assert_eq!(
locate_audio_at_ceiling(&buf, file_len),
Err(FormatError::Malformed)
);
}
#[test]
fn locate_audio_rejects_fmt_outside_declared_form() {
let mut buf = wav(&[(b"data", vec![0x11; 8]), (b"fmt ", fmt_pcm())]);
buf[4..8].copy_from_slice(&20u32.to_le_bytes()); assert_eq!(locate_audio(&buf), Err(FormatError::NotWav));
}
#[test]
fn locate_audio_at_ceiling_rejects_fmt_outside_declared_form() {
let mut buf = wav(&[(b"data", vec![0x11; 8]), (b"fmt ", fmt_pcm())]);
let file_len = buf.len() as u64;
buf[4..8].copy_from_slice(&20u32.to_le_bytes()); assert_eq!(
locate_audio_at_ceiling(&buf, file_len),
Err(FormatError::NotWav)
);
}
#[test]
fn wav_read_binary_tags_extracts_id3_chunk_frames() {
use id3::frame::{Content, Unknown};
use id3::{Frame, Tag, TagLike, Version};
let mut tag = Tag::new();
tag.add_frame(Frame::with_content(
"PRIV",
Content::Unknown(Unknown {
data: vec![5, 6, 7],
version: Version::Id3v24,
}),
));
let mut id3 = Vec::new();
id3::Encoder::new()
.version(Version::Id3v24)
.encode(&tag, &mut id3)
.unwrap();
let wav = wav(&[(b"id3 ", id3)]);
let (opaque, _promoted) = super::read_binary_tags(&wav);
let priv_tag = opaque
.iter()
.find(|e| e.key == "PRIV")
.expect("PRIV preserved");
assert_eq!(priv_tag.payload, vec![5, 6, 7]);
}
}