1use crate::error::{FormatError, Result};
2use crate::input::{BinaryTagInput, EmbeddedBinaryTag, EmbeddedPicture};
3use crate::probe::Extent;
4use crate::size;
5use std::collections::HashSet;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct WavBounds {
10 pub audio_offset: u64,
11 pub audio_length: u64,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct WavScan {
18 pub fmt: Vec<u8>,
19 pub fact: Option<Vec<u8>>,
20}
21
22fn riff_wave_start(buf: &[u8]) -> Result<(usize, u64)> {
26 if buf.len() < 12 || &buf[0..4] != b"RIFF" || &buf[8..12] != b"WAVE" {
27 return Err(FormatError::NotWav);
28 }
29 let riff_size = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
30 Ok((12, 8 + u64::from(riff_size)))
31}
32
33fn walk_chunks(buf: &[u8]) -> Vec<([u8; 4], usize, u64)> {
39 let mut out = Vec::new();
40 let Ok((mut pos, form_end)) = riff_wave_start(buf) else {
41 return out;
42 };
43 let ceiling = crate::convert::usize_from(form_end.min(buf.len() as u64));
47 while pos + 8 <= ceiling {
48 let mut id = [0u8; 4];
49 id.copy_from_slice(&buf[pos..pos + 4]);
50 let size = u64::from(u32::from_le_bytes([
51 buf[pos + 4],
52 buf[pos + 5],
53 buf[pos + 6],
54 buf[pos + 7],
55 ]));
56 let payload_offset = pos + 8;
57 out.push((id, payload_offset, size));
58 let advance = 8u64 + size + (size & 1); match (pos as u64).checked_add(advance) {
60 Some(next) if next <= ceiling as u64 => pos = crate::convert::usize_from(next),
61 _ => break,
62 }
63 }
64 out
65}
66
67fn chunk_slice(buf: &[u8], offset: usize, len: u64) -> Option<&[u8]> {
69 let end = offset.checked_add(crate::convert::usize_from(len))?;
70 buf.get(offset..end)
71}
72
73pub fn locate_audio(buf: &[u8]) -> Result<WavBounds> {
77 let (_, form_end) = riff_wave_start(buf)?;
78 if form_end > buf.len() as u64 {
79 return Err(FormatError::Malformed);
80 }
81 let chunks = walk_chunks(buf);
82 let has_fmt = chunks.iter().any(|(id, _, _)| id == b"fmt ");
83 let data = chunks.iter().find(|(id, _, _)| id == b"data");
84 match (has_fmt, data) {
85 (true, Some(&(_, off, len))) => {
86 let data_end = (off as u64).saturating_add(len);
91 if data_end > form_end {
92 return Err(FormatError::Malformed);
93 }
94 Ok(WavBounds {
95 audio_offset: off as u64,
96 audio_length: len,
97 })
98 }
99 _ => Err(FormatError::NotWav),
100 }
101}
102
103pub fn locate_audio_bounded(prefix: &[u8], file_len: u64) -> Result<Extent<WavBounds>> {
108 if (prefix.len() as u64) < file_len {
109 return Ok(Extent::NeedMore { up_to: file_len });
110 }
111 Ok(Extent::Complete(locate_audio(prefix)?))
112}
113
114pub fn locate_audio_at_ceiling(prefix: &[u8], file_len: u64) -> Result<WavBounds> {
122 let (_, form_end) = riff_wave_start(prefix)?;
123 if form_end > file_len {
124 return Err(FormatError::Malformed);
125 }
126 let chunks = walk_chunks(prefix);
127 let has_fmt = chunks.iter().any(|(id, _, _)| id == b"fmt ");
128 let data = chunks.iter().find(|(id, _, _)| id == b"data");
129 match (has_fmt, data) {
130 (true, Some(&(_, off, len))) => {
131 let data_end = (off as u64).saturating_add(len);
136 if data_end > form_end {
137 return Err(FormatError::Malformed);
138 }
139 Ok(WavBounds {
140 audio_offset: off as u64,
141 audio_length: len,
142 })
143 }
144 _ => Err(FormatError::NotWav),
145 }
146}
147
148pub fn read_structure(front: &[u8]) -> Result<WavScan> {
152 riff_wave_start(front)?;
153 let chunks = walk_chunks(front);
154
155 let &(_, fmt_off, fmt_len) = chunks
156 .iter()
157 .find(|(id, _, _)| id == b"fmt ")
158 .ok_or(FormatError::NotWav)?;
159 let fmt = chunk_slice(front, fmt_off, fmt_len)
160 .ok_or(FormatError::Malformed)?
161 .to_vec();
162
163 let fact = match chunks.iter().find(|(id, _, _)| id == b"fact") {
164 Some(&(_, off, len)) => Some(
165 chunk_slice(front, off, len)
166 .ok_or(FormatError::Malformed)?
167 .to_vec(),
168 ),
169 None => None,
170 };
171
172 Ok(WavScan { fmt, fact })
173}
174
175use crate::input::{ArtInput, TagInput};
176use crate::layout::{RegionLayout, Segment};
177
178fn info_fourcc(key: &str) -> Option<&'static [u8; 4]> {
182 Some(match key {
183 "title" => b"INAM",
184 "artist" => b"IART",
185 "album" => b"IPRD",
186 "date" => b"ICRD",
187 "genre" => b"IGNR",
188 "comment" => b"ICMT",
189 "tracknumber" => b"ITRK",
190 _ => return None,
191 })
192}
193
194fn build_info_payload(tags: &[TagInput]) -> Result<Option<Vec<u8>>> {
198 let mut entries: Vec<(&'static [u8; 4], &str)> = Vec::new();
199 let mut used: Vec<&str> = Vec::new();
200 for t in tags {
201 if used.contains(&t.key.as_str()) {
202 continue;
203 }
204 if let Some(cc) = info_fourcc(&t.key) {
205 used.push(t.key.as_str());
206 entries.push((cc, t.value.as_str()));
207 }
208 }
209 if entries.is_empty() {
210 return Ok(None);
211 }
212 let mut payload = Vec::new();
213 payload.extend_from_slice(b"INFO");
214 for (cc, value) in entries {
215 let mut v = value.as_bytes().to_vec();
216 v.push(0x00); append_chunk(&mut payload, cc, &v)?;
218 }
219 Ok(Some(payload))
220}
221
222fn chunk_header(id: &[u8; 4], len: u32) -> [u8; 8] {
224 let mut h = [0u8; 8];
225 h[..4].copy_from_slice(id);
226 h[4..].copy_from_slice(&len.to_le_bytes());
227 h
228}
229
230fn append_chunk(out: &mut Vec<u8>, id: &[u8; 4], payload: &[u8]) -> Result<()> {
232 let len = u32::try_from(payload.len()).map_err(|_| FormatError::TooLarge)?;
233 out.extend_from_slice(&chunk_header(id, len));
234 out.extend_from_slice(payload);
235 if payload.len() % 2 == 1 {
236 out.push(0x00);
237 }
238 Ok(())
239}
240
241fn push_inline_chunk(segments: &mut Vec<Segment>, id: &[u8; 4], payload: &[u8]) -> Result<()> {
243 let mut chunk = Vec::with_capacity(8 + payload.len() + 1);
244 append_chunk(&mut chunk, id, payload)?;
245 segments.push(Segment::Inline(chunk));
246 Ok(())
247}
248
249pub fn synthesize_layout(
255 scan: &WavScan,
256 audio_offset: u64,
257 audio_length: u64,
258 tags: &[TagInput],
259 binary_tags: &[BinaryTagInput],
260 arts: &[ArtInput],
261) -> Result<RegionLayout> {
262 let audio_length_u32 = u32::try_from(audio_length).map_err(|_| FormatError::TooLarge)?; let mut segments: Vec<Segment> = Vec::new();
265
266 push_inline_chunk(&mut segments, b"fmt ", &scan.fmt)?;
267 if let Some(fact) = &scan.fact {
268 push_inline_chunk(&mut segments, b"fact", fact)?;
269 }
270 if let Some(info) = build_info_payload(tags)? {
271 push_inline_chunk(&mut segments, b"LIST", &info)?;
272 }
273
274 let (tag_segments, tag_len) = crate::mp3::build_id3v2_segments(tags, binary_tags, arts)?;
278 let tag_len_u32 = u32::try_from(tag_len).map_err(|_| FormatError::TooLarge)?;
279 segments.push(Segment::Inline(chunk_header(b"id3 ", tag_len_u32).to_vec()));
280 segments.extend(tag_segments);
281 if tag_len % 2 == 1 {
282 segments.push(Segment::Inline(vec![0x00]));
283 }
284
285 segments.push(Segment::Inline(
287 chunk_header(b"data", audio_length_u32).to_vec(),
288 ));
289 segments.push(Segment::BackingAudio {
290 offset: audio_offset,
291 len: audio_length,
292 });
293 if audio_length % 2 == 1 {
294 segments.push(Segment::Inline(vec![0x00]));
295 }
296
297 let body_len: u64 = size::checked_sum(segments.iter().map(Segment::len))?;
299 let riff_size =
300 u32::try_from(size::checked_add(body_len, 4)?).map_err(|_| FormatError::TooLarge)?;
301 let mut header = Vec::with_capacity(12);
302 header.extend_from_slice(b"RIFF");
303 header.extend_from_slice(&riff_size.to_le_bytes());
304 header.extend_from_slice(b"WAVE");
305 segments.insert(0, Segment::Inline(header));
306
307 Ok(RegionLayout::validated(segments)?)
308}
309
310fn info_to_key(id: &[u8; 4]) -> Option<&'static str> {
313 Some(match id {
314 b"INAM" => "title",
315 b"IART" => "artist",
316 b"IPRD" => "album",
317 b"ICRD" => "date",
318 b"IGNR" => "genre",
319 b"ICMT" => "comment",
320 b"ITRK" => "tracknumber",
321 _ => return None,
322 })
323}
324
325fn find_id3_chunk<'a>(buf: &'a [u8], chunks: &[([u8; 4], usize, u64)]) -> Option<&'a [u8]> {
327 let &(_, off, len) = chunks
328 .iter()
329 .find(|(id, _, _)| id == b"id3 " || id == b"ID3 ")?;
330 chunk_slice(buf, off, len)
331}
332
333fn read_info_tags(body: &[u8]) -> Vec<(String, String)> {
336 let mut out = Vec::new();
337 let mut pos = 0usize;
338 while pos + 8 <= body.len() {
339 let mut id = [0u8; 4];
340 id.copy_from_slice(&body[pos..pos + 4]);
341 let size = u32::from_le_bytes([body[pos + 4], body[pos + 5], body[pos + 6], body[pos + 7]])
342 as usize;
343 let val_start = pos + 8;
344 let val_end = val_start.saturating_add(size).min(body.len());
345 if let Some(key) = info_to_key(&id) {
346 let raw = String::from_utf8_lossy(&body[val_start..val_end]);
347 let value = raw.trim_end_matches('\0').to_string();
348 if !value.is_empty() {
349 out.push((key.to_string(), value));
350 }
351 }
352 pos = val_start + size + (size & 1);
353 }
354 out
355}
356
357pub fn read_tags(buf: &[u8]) -> Vec<(String, String)> {
361 let chunks = walk_chunks(buf);
362
363 let from_id3 = find_id3_chunk(buf, &chunks)
364 .map(crate::mp3::read_tags)
365 .unwrap_or_default();
366
367 let from_info = chunks
368 .iter()
369 .find(|(id, _, _)| id == b"LIST")
370 .and_then(|&(_, off, len)| chunk_slice(buf, off, len))
371 .filter(|slice| slice.len() >= 4 && &slice[0..4] == b"INFO")
372 .map(|slice| read_info_tags(&slice[4..]))
373 .unwrap_or_default();
374
375 let id3_keys: HashSet<&str> = from_id3.iter().map(|(k, _)| k.as_str()).collect();
376 let mut out = from_id3.clone();
377 for (k, v) in from_info {
378 if !id3_keys.contains(k.as_str()) {
379 out.push((k, v));
380 }
381 }
382 out
383}
384
385pub fn read_binary_tags(data: &[u8]) -> (Vec<EmbeddedBinaryTag>, Vec<(String, String)>) {
389 let chunks = walk_chunks(data);
390 match find_id3_chunk(data, &chunks) {
391 Some(id3_bytes) => crate::mp3::read_binary_tags(id3_bytes),
392 None => (Vec::new(), Vec::new()),
393 }
394}
395
396pub fn read_pictures(buf: &[u8]) -> Vec<EmbeddedPicture> {
399 let chunks = walk_chunks(buf);
400 find_id3_chunk(buf, &chunks)
401 .map(crate::mp3::read_pictures)
402 .unwrap_or_default()
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
418 fn wav_oom_crash_artifact_is_safe() {
419 const CRASH: &[u8] = &[
421 0x52, 0x49, 0x46, 0x46, 0x32, 0x00, 0x00, 0x00, 0x57, 0x41, 0x56, 0x45, 0x49, 0x44,
422 0x33, 0x20, 0x38, 0x00, 0x00, 0x00, 0x52, 0x49, 0x46, 0x46, 0x32, 0x00, 0x00, 0x00,
423 0x57, 0x41, 0x56, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
424 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x44, 0x33, 0x20, 0x15, 0x00, 0x00, 0x00,
425 0x49, 0x44, 0x33, 0x03, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x54, 0x44, 0x41, 0x03,
426 0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
427 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
428 0x00, 0x00, 0x00, 0x01, 0x00, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
429 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
430 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
431 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
432 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
433 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
434 ];
435 assert!(
436 read_tags(CRASH).is_empty(),
437 "read_tags must not OOM on WAV crash artifact"
438 );
439 assert!(
440 read_pictures(CRASH).is_empty(),
441 "read_pictures must not OOM on WAV crash artifact"
442 );
443 }
444
445 #[test]
446 fn riff_wave_start_accepts_exactly_twelve_bytes() {
447 let buf = b"RIFF\0\0\0\0WAVE".to_vec();
450 assert_eq!(buf.len(), 12);
451 assert_eq!(riff_wave_start(&buf), Ok((12, 8)));
452 }
453
454 #[test]
455 fn riff_wave_start_rejects_eleven_byte_riff_without_panic() {
456 let buf = b"RIFF\0\0\0\0WAV".to_vec();
461 assert_eq!(buf.len(), 11);
462 assert_eq!(riff_wave_start(&buf), Err(FormatError::NotWav));
463 }
464
465 fn wav(chunks: &[(&[u8; 4], Vec<u8>)]) -> Vec<u8> {
468 let mut body = Vec::new();
469 for (id, payload) in chunks {
470 body.extend_from_slice(*id);
471 body.extend_from_slice(&u32::try_from(payload.len()).unwrap().to_le_bytes());
472 body.extend_from_slice(payload);
473 if payload.len() % 2 == 1 {
474 body.push(0x00);
475 }
476 }
477 let mut out = b"RIFF".to_vec();
478 out.extend_from_slice(&u32::try_from(body.len() + 4).unwrap().to_le_bytes());
479 out.extend_from_slice(b"WAVE");
480 out.extend_from_slice(&body);
481 out
482 }
483
484 #[test]
485 fn walk_chunks_advances_past_each_payload() {
486 let buf = wav(&[(b"AAAA", vec![0x11; 3]), (b"data", vec![0xBB; 8])]);
490 let ids: Vec<[u8; 4]> = walk_chunks(&buf).iter().map(|(id, _, _)| *id).collect();
491 assert_eq!(ids, vec![*b"AAAA", *b"data"]);
492 }
493
494 fn fmt_pcm() -> Vec<u8> {
496 let mut f = Vec::new();
497 f.extend_from_slice(&1u16.to_le_bytes());
498 f.extend_from_slice(&1u16.to_le_bytes());
499 f.extend_from_slice(&44_100u32.to_le_bytes());
500 f.extend_from_slice(&88_200u32.to_le_bytes());
501 f.extend_from_slice(&2u16.to_le_bytes());
502 f.extend_from_slice(&16u16.to_le_bytes());
503 f
504 }
505
506 #[test]
507 fn locate_requires_fmt_chunk() {
508 let buf = wav(&[(b"data", vec![0x11; 8])]);
512 assert_eq!(locate_audio(&buf), Err(FormatError::NotWav));
513 }
514
515 #[test]
516 fn locate_accepts_data_with_trailing_chunk() {
517 let buf = wav(&[
521 (b"fmt ", fmt_pcm()),
522 (b"data", vec![0x11; 8]),
523 (b"junk", vec![0x00; 4]),
524 ]);
525 let bounds = locate_audio(&buf).unwrap();
526 assert_eq!(bounds.audio_length, 8);
527 }
528
529 #[test]
530 fn info_fourcc_emits_each_mapped_key() {
531 let cases: [(&str, &[u8; 4]); 6] = [
534 ("artist", b"IART"),
535 ("album", b"IPRD"),
536 ("date", b"ICRD"),
537 ("genre", b"IGNR"),
538 ("comment", b"ICMT"),
539 ("tracknumber", b"ITRK"),
540 ];
541 for (key, cc) in cases {
542 let payload = build_info_payload(&[TagInput::new(key, "X")])
543 .unwrap()
544 .unwrap_or_else(|| panic!("INFO payload for {key}"));
545 assert!(
546 payload.windows(4).any(|w| w == &cc[..]),
547 "key {key} must emit FourCC {:?}",
548 std::str::from_utf8(cc).unwrap()
549 );
550 }
551 }
552
553 #[test]
554 fn build_info_payload_word_aligns_values() {
555 let even = build_info_payload(&[TagInput::new("title", "a")])
560 .unwrap()
561 .unwrap();
562 assert_eq!(even.len(), 14);
564
565 let odd = build_info_payload(&[TagInput::new("title", "ab")])
566 .unwrap()
567 .unwrap();
568 assert_eq!(odd.len(), 16);
570 }
571
572 #[test]
573 fn push_inline_chunk_word_aligns_payload() {
574 let mut segs = Vec::new();
577 push_inline_chunk(&mut segs, b"test", &[0xAA, 0xBB]).unwrap();
578 assert_eq!(segs.len(), 1);
579 assert_eq!(segs[0].len(), 10); let mut segs2 = Vec::new();
583 push_inline_chunk(&mut segs2, b"test", &[0xAA, 0xBB, 0xCC]).unwrap();
584 assert_eq!(segs2[0].len(), 12); }
586
587 fn info_payload(pairs: &[(&[u8; 4], &str)]) -> Vec<u8> {
589 let mut p = b"INFO".to_vec();
590 for (cc, val) in pairs {
591 let mut v = val.as_bytes().to_vec();
592 v.push(0x00);
593 p.extend_from_slice(*cc);
594 p.extend_from_slice(&u32::try_from(v.len()).unwrap().to_le_bytes());
595 p.extend_from_slice(&v);
596 if v.len() % 2 == 1 {
597 p.push(0x00);
598 }
599 }
600 p
601 }
602
603 #[test]
604 fn info_to_key_decodes_each_mapped_fourcc() {
605 let cases: [(&[u8; 4], &str, &str); 4] = [
607 (b"IPRD", "album", "Anthology"),
608 (b"ICRD", "date", "1999"),
609 (b"ICMT", "comment", "Nice"),
610 (b"ITRK", "tracknumber", "3"),
611 ];
612 for (cc, key, val) in cases {
613 let buf = wav(&[
614 (b"fmt ", fmt_pcm()),
615 (b"LIST", info_payload(&[(cc, val)])),
616 (b"data", vec![0x00; 4]),
617 ]);
618 let tags = read_tags(&buf);
619 assert!(
620 tags.contains(&(key.to_string(), val.to_string())),
621 "FourCC {:?} must decode to {key}",
622 std::str::from_utf8(cc).unwrap()
623 );
624 }
625 }
626
627 #[test]
628 fn read_tags_rejects_short_list_without_panic() {
629 let buf = wav(&[
634 (b"fmt ", fmt_pcm()),
635 (b"LIST", vec![0x49, 0x4E]), (b"data", vec![0x00; 4]),
637 ]);
638 assert!(read_tags(&buf).is_empty());
639 }
640
641 fn inline_offset_of(layout: &RegionLayout, fourcc: &[u8; 4]) -> u64 {
644 let mut off = 0u64;
645 for s in layout.segments() {
646 if let Segment::Inline(b) = s
647 && b.len() >= 4
648 && &b[0..4] == fourcc
649 {
650 return off;
651 }
652 off += s.len();
653 }
654 panic!("no inline chunk starting with {fourcc:?}");
655 }
656
657 #[test]
658 fn synthesize_word_aligns_embedded_id3_chunk() {
659 let mut tags = Vec::new();
668 let mut tag_len = 0u64;
669 for n in 1..64 {
670 let cand = vec![TagInput::new("albumartist", &"x".repeat(n))];
671 let (_, tl) = crate::mp3::build_id3v2_segments(&cand, &[], &[]).unwrap();
672 if tl % 2 == 1 {
673 tags = cand;
674 tag_len = tl;
675 break;
676 }
677 }
678 assert_eq!(tag_len % 2, 1, "expected to find an odd-length id3 tag");
679
680 let scan = WavScan {
681 fmt: fmt_pcm(),
682 fact: None,
683 };
684 let layout = synthesize_layout(&scan, 0, 8, &tags, &[], &[]).unwrap();
685 assert_eq!(
686 inline_offset_of(&layout, b"data") % 2,
687 0,
688 "the data chunk must be word-aligned"
689 );
690 }
691
692 #[test]
693 fn synthesize_rejects_riff_size_overflow() {
694 let scan = WavScan {
702 fmt: fmt_pcm(),
703 fact: None,
704 };
705 let res = synthesize_layout(&scan, 0, u64::from(u32::MAX), &[], &[], &[]);
706 assert_eq!(res, Err(FormatError::TooLarge));
707 }
708
709 fn wav_file(audio: &[u8]) -> Vec<u8> {
711 let mut body = Vec::new();
712 body.extend_from_slice(b"fmt ");
713 body.extend_from_slice(&16u32.to_le_bytes());
714 body.extend(std::iter::repeat_n(0u8, 16));
715 body.extend_from_slice(b"data");
716 body.extend_from_slice(&u32::try_from(audio.len()).unwrap().to_le_bytes());
717 body.extend_from_slice(audio);
718 let mut v = b"RIFF".to_vec();
719 v.extend_from_slice(&u32::try_from(4 + body.len()).unwrap().to_le_bytes());
720 v.extend_from_slice(b"WAVE");
721 v.extend_from_slice(&body);
722 v
723 }
724
725 #[test]
726 fn locate_audio_bounded_complete_when_prefix_is_whole_file() {
727 let full = wav_file(b"AUDIOAUDIO");
728 let file_len = full.len() as u64;
729 match locate_audio_bounded(&full, file_len).unwrap() {
730 Extent::Complete(b) => assert_eq!(b.audio_length, 10),
731 other @ Extent::NeedMore { .. } => panic!("expected Complete, got {other:?}"),
732 }
733 }
734
735 #[test]
736 fn locate_audio_bounded_needmore_when_prefix_short() {
737 let full = wav_file(b"AUDIOAUDIO");
738 let file_len = full.len() as u64;
739 let prefix = &full[..full.len() - 4];
740 match locate_audio_bounded(prefix, file_len).unwrap() {
741 Extent::NeedMore { up_to } => assert_eq!(up_to, file_len),
742 other @ Extent::Complete(_) => panic!("expected NeedMore, got {other:?}"),
743 }
744 }
745
746 fn wav_front(data_len: u64) -> Vec<u8> {
751 let mut body = Vec::new();
752 body.extend_from_slice(b"fmt ");
753 body.extend_from_slice(&16u32.to_le_bytes());
754 body.extend(std::iter::repeat_n(0u8, 16));
755 body.extend_from_slice(b"data");
756 body.extend_from_slice(&u32::try_from(data_len).unwrap().to_le_bytes());
757 let mut v = b"RIFF".to_vec();
758 let riff_size = 36u32 + u32::try_from(data_len).unwrap(); v.extend_from_slice(&riff_size.to_le_bytes());
760 v.extend_from_slice(b"WAVE");
761 v.extend_from_slice(&body);
762 v
763 }
764
765 #[test]
766 fn locate_audio_at_ceiling_trusts_data_header_without_payload() {
767 let data_len = 200u64;
768 let front = wav_front(data_len);
769 let audio_offset = front.len() as u64; let file_len = audio_offset + data_len;
771 let b = locate_audio_at_ceiling(&front, file_len).unwrap();
772 assert_eq!(b.audio_offset, audio_offset);
773 assert_eq!(b.audio_length, data_len);
774 }
775
776 #[test]
777 fn locate_audio_at_ceiling_accepts_data_shorter_than_file() {
778 let data_len = 200u64;
780 let front = wav_front(data_len);
781 let audio_offset = front.len() as u64;
782 let file_len = audio_offset + data_len + 64;
783 let b = locate_audio_at_ceiling(&front, file_len).unwrap();
784 assert_eq!(b.audio_offset, audio_offset);
785 assert_eq!(b.audio_length, data_len);
786 }
787
788 #[test]
789 fn locate_audio_at_ceiling_rejects_data_running_past_file() {
790 let front = wav_front(1_000);
792 let audio_offset = front.len() as u64;
793 let file_len = audio_offset + 10;
794 assert_eq!(
795 locate_audio_at_ceiling(&front, file_len),
796 Err(FormatError::Malformed)
797 );
798 }
799
800 #[test]
801 fn locate_audio_at_ceiling_requires_fmt_chunk() {
802 let mut body = Vec::new();
804 body.extend_from_slice(b"data");
805 body.extend_from_slice(&200u32.to_le_bytes());
806 let mut front = b"RIFF".to_vec();
807 front.extend_from_slice(&0u32.to_le_bytes());
808 front.extend_from_slice(b"WAVE");
809 front.extend_from_slice(&body);
810 let file_len = front.len() as u64 + 200;
811 assert_eq!(
812 locate_audio_at_ceiling(&front, file_len),
813 Err(FormatError::NotWav)
814 );
815 }
816
817 #[test]
818 fn locate_audio_rejects_form_end_before_data() {
819 let mut buf = wav(&[(b"fmt ", fmt_pcm()), (b"data", vec![0x11; 8])]);
823 buf[4..8].copy_from_slice(&40u32.to_le_bytes()); assert_eq!(locate_audio(&buf), Err(FormatError::Malformed));
825 }
826
827 #[test]
828 fn locate_audio_rejects_form_end_past_file() {
829 let mut buf = wav(&[(b"fmt ", fmt_pcm()), (b"data", vec![0x11; 8])]);
830 let huge = u32::try_from(buf.len()).unwrap() + 100;
831 buf[4..8].copy_from_slice(&huge.to_le_bytes()); assert_eq!(locate_audio(&buf), Err(FormatError::Malformed));
833 }
834
835 #[test]
836 fn locate_audio_accepts_valid_form_with_odd_chunk_and_trailing_metadata() {
837 let buf = wav(&[
840 (b"fmt ", fmt_pcm()),
841 (b"data", vec![0x22; 7]), (b"LIST", vec![0x33; 4]),
843 ]);
844 let b = locate_audio(&buf).unwrap();
845 assert_eq!(b.audio_length, 7);
846 }
847
848 #[test]
849 fn locate_audio_at_ceiling_rejects_form_end_before_data() {
850 let mut buf = wav(&[(b"fmt ", fmt_pcm()), (b"data", vec![0x11; 8])]);
854 let file_len = buf.len() as u64;
855 buf[4..8].copy_from_slice(&40u32.to_le_bytes()); assert_eq!(
857 locate_audio_at_ceiling(&buf, file_len),
858 Err(FormatError::Malformed)
859 );
860 }
861
862 #[test]
863 fn locate_audio_rejects_fmt_outside_declared_form() {
864 let mut buf = wav(&[(b"data", vec![0x11; 8]), (b"fmt ", fmt_pcm())]);
869 buf[4..8].copy_from_slice(&20u32.to_le_bytes()); assert_eq!(locate_audio(&buf), Err(FormatError::NotWav));
871 }
872
873 #[test]
874 fn locate_audio_at_ceiling_rejects_fmt_outside_declared_form() {
875 let mut buf = wav(&[(b"data", vec![0x11; 8]), (b"fmt ", fmt_pcm())]);
876 let file_len = buf.len() as u64;
877 buf[4..8].copy_from_slice(&20u32.to_le_bytes()); assert_eq!(
879 locate_audio_at_ceiling(&buf, file_len),
880 Err(FormatError::NotWav)
881 );
882 }
883
884 #[test]
885 fn wav_read_binary_tags_extracts_id3_chunk_frames() {
886 use id3::frame::{Content, Unknown};
887 use id3::{Frame, Tag, TagLike, Version};
888 let mut tag = Tag::new();
889 tag.add_frame(Frame::with_content(
890 "PRIV",
891 Content::Unknown(Unknown {
892 data: vec![5, 6, 7],
893 version: Version::Id3v24,
894 }),
895 ));
896 let mut id3 = Vec::new();
897 id3::Encoder::new()
898 .version(Version::Id3v24)
899 .encode(&tag, &mut id3)
900 .unwrap();
901 let wav = wav(&[(b"id3 ", id3)]);
902
903 let (opaque, _promoted) = super::read_binary_tags(&wav);
904 let priv_tag = opaque
905 .iter()
906 .find(|e| e.key == "PRIV")
907 .expect("PRIV preserved");
908 assert_eq!(priv_tag.payload, vec![5, 6, 7]);
909 }
910
911 }