1use crate::bytes::read_u32_le;
2use crate::error::{FormatError, Result};
3use crate::input::{BinaryTagInput, EmbeddedBinaryTag, EmbeddedPicture};
4use crate::probe::Extent;
5use crate::size;
6use std::collections::HashSet;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct WavBounds {
11 pub audio_offset: u64,
12 pub audio_length: u64,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct WavScan {
19 pub fmt: Vec<u8>,
20 pub fact: Option<Vec<u8>>,
21}
22
23fn riff_wave_start(buf: &[u8]) -> Result<(usize, u64)> {
27 if buf.len() < 12 || &buf[0..4] != b"RIFF" || &buf[8..12] != b"WAVE" {
28 return Err(FormatError::NotWav);
29 }
30 let riff_size =
31 read_u32_le(buf, 4).expect("RIFF size field within the validated 12-byte header");
32 Ok((12, 8 + u64::from(riff_size)))
33}
34
35fn walk_chunks(buf: &[u8]) -> Vec<([u8; 4], usize, u64)> {
41 let mut out = Vec::new();
42 let Ok((mut pos, form_end)) = riff_wave_start(buf) else {
43 return out;
44 };
45 let ceiling = crate::convert::usize_from(form_end.min(buf.len() as u64));
49 while pos + 8 <= ceiling {
50 let mut id = [0u8; 4];
51 id.copy_from_slice(&buf[pos..pos + 4]);
52 let size = u64::from(
53 read_u32_le(buf, pos + 4).expect("chunk size field within the loop-guarded bounds"),
54 );
55 let payload_offset = pos + 8;
56 out.push((id, payload_offset, size));
57 let advance = 8u64 + size + (size & 1); match (pos as u64).checked_add(advance) {
59 Some(next) if next <= ceiling as u64 => pos = crate::convert::usize_from(next),
60 _ => break,
61 }
62 }
63 out
64}
65
66fn chunk_slice(buf: &[u8], offset: usize, len: u64) -> Option<&[u8]> {
68 let end = offset.checked_add(crate::convert::usize_from(len))?;
69 buf.get(offset..end)
70}
71
72pub fn locate_audio(buf: &[u8]) -> Result<WavBounds> {
76 let (_, form_end) = riff_wave_start(buf)?;
77 if form_end > buf.len() as u64 {
78 return Err(FormatError::Malformed);
79 }
80 let chunks = walk_chunks(buf);
81 let has_fmt = chunks.iter().any(|(id, _, _)| id == b"fmt ");
82 let data = chunks.iter().find(|(id, _, _)| id == b"data");
83 match (has_fmt, data) {
84 (true, Some(&(_, off, len))) => {
85 let data_end = (off as u64).saturating_add(len);
90 if data_end > form_end {
91 return Err(FormatError::Malformed);
92 }
93 Ok(WavBounds {
94 audio_offset: off as u64,
95 audio_length: len,
96 })
97 }
98 _ => Err(FormatError::NotWav),
99 }
100}
101
102pub fn locate_audio_bounded(prefix: &[u8], file_len: u64) -> Result<Extent<WavBounds>> {
107 if (prefix.len() as u64) < file_len {
108 return Ok(Extent::NeedMore { up_to: file_len });
109 }
110 Ok(Extent::Complete(locate_audio(prefix)?))
111}
112
113pub fn locate_audio_at_ceiling(prefix: &[u8], file_len: u64) -> Result<WavBounds> {
121 let (_, form_end) = riff_wave_start(prefix)?;
122 if form_end > file_len {
123 return Err(FormatError::Malformed);
124 }
125 let chunks = walk_chunks(prefix);
126 let has_fmt = chunks.iter().any(|(id, _, _)| id == b"fmt ");
127 let data = chunks.iter().find(|(id, _, _)| id == b"data");
128 match (has_fmt, data) {
129 (true, Some(&(_, off, len))) => {
130 let data_end = (off as u64).saturating_add(len);
135 if data_end > form_end {
136 return Err(FormatError::Malformed);
137 }
138 Ok(WavBounds {
139 audio_offset: off as u64,
140 audio_length: len,
141 })
142 }
143 _ => Err(FormatError::NotWav),
144 }
145}
146
147pub fn read_structure(front: &[u8]) -> Result<WavScan> {
151 riff_wave_start(front)?;
152 let chunks = walk_chunks(front);
153
154 let &(_, fmt_off, fmt_len) = chunks
155 .iter()
156 .find(|(id, _, _)| id == b"fmt ")
157 .ok_or(FormatError::NotWav)?;
158 let fmt = chunk_slice(front, fmt_off, fmt_len)
159 .ok_or(FormatError::Malformed)?
160 .to_vec();
161
162 let fact = match chunks.iter().find(|(id, _, _)| id == b"fact") {
163 Some(&(_, off, len)) => Some(
164 chunk_slice(front, off, len)
165 .ok_or(FormatError::Malformed)?
166 .to_vec(),
167 ),
168 None => None,
169 };
170
171 Ok(WavScan { fmt, fact })
172}
173
174use crate::input::{ArtInput, TagInput};
175use crate::layout::{RegionLayout, Segment};
176
177fn info_fourcc(key: &str) -> Option<&'static [u8; 4]> {
181 Some(match key {
182 "title" => b"INAM",
183 "artist" => b"IART",
184 "album" => b"IPRD",
185 "date" => b"ICRD",
186 "genre" => b"IGNR",
187 "comment" => b"ICMT",
188 "tracknumber" => b"ITRK",
189 _ => return None,
190 })
191}
192
193fn build_info_payload(tags: &[TagInput]) -> Result<Option<Vec<u8>>> {
197 let mut entries: Vec<(&'static [u8; 4], &str)> = Vec::new();
198 let mut used: Vec<&str> = Vec::new();
199 for t in tags {
200 if used.contains(&t.key.as_str()) {
201 continue;
202 }
203 if let Some(cc) = info_fourcc(&t.key) {
204 used.push(t.key.as_str());
205 entries.push((cc, t.value.as_str()));
206 }
207 }
208 if entries.is_empty() {
209 return Ok(None);
210 }
211 let mut payload = Vec::new();
212 payload.extend_from_slice(b"INFO");
213 for (cc, value) in entries {
214 let mut v = value.as_bytes().to_vec();
215 v.push(0x00); append_chunk(&mut payload, cc, &v)?;
217 }
218 Ok(Some(payload))
219}
220
221fn chunk_header(id: &[u8; 4], len: u32) -> [u8; 8] {
223 let mut h = [0u8; 8];
224 h[..4].copy_from_slice(id);
225 h[4..].copy_from_slice(&len.to_le_bytes());
226 h
227}
228
229fn append_chunk(out: &mut Vec<u8>, id: &[u8; 4], payload: &[u8]) -> Result<()> {
231 let len = u32::try_from(payload.len()).map_err(|_| FormatError::TooLarge)?;
232 out.extend_from_slice(&chunk_header(id, len));
233 out.extend_from_slice(payload);
234 if payload.len() % 2 == 1 {
235 out.push(0x00);
236 }
237 Ok(())
238}
239
240fn push_inline_chunk(segments: &mut Vec<Segment>, id: &[u8; 4], payload: &[u8]) -> Result<()> {
242 let mut chunk = Vec::with_capacity(8 + payload.len() + 1);
243 append_chunk(&mut chunk, id, payload)?;
244 segments.push(Segment::Inline(chunk));
245 Ok(())
246}
247
248pub fn synthesize_layout(
254 scan: &WavScan,
255 audio_offset: u64,
256 audio_length: u64,
257 tags: &[TagInput],
258 binary_tags: &[BinaryTagInput],
259 arts: &[ArtInput],
260) -> Result<RegionLayout> {
261 let audio_length_u32 = u32::try_from(audio_length).map_err(|_| FormatError::TooLarge)?; let mut segments: Vec<Segment> = Vec::new();
264
265 push_inline_chunk(&mut segments, b"fmt ", &scan.fmt)?;
266 if let Some(fact) = &scan.fact {
267 push_inline_chunk(&mut segments, b"fact", fact)?;
268 }
269 if let Some(info) = build_info_payload(tags)? {
270 push_inline_chunk(&mut segments, b"LIST", &info)?;
271 }
272
273 let (tag_segments, tag_len) = crate::mp3::build_id3v2_segments(tags, binary_tags, arts)?;
277 let tag_len_u32 = u32::try_from(tag_len).map_err(|_| FormatError::TooLarge)?;
278 segments.push(Segment::Inline(chunk_header(b"id3 ", tag_len_u32).to_vec()));
279 segments.extend(tag_segments);
280 if tag_len % 2 == 1 {
281 segments.push(Segment::Inline(vec![0x00]));
282 }
283
284 segments.push(Segment::Inline(
286 chunk_header(b"data", audio_length_u32).to_vec(),
287 ));
288 segments.push(Segment::BackingAudio {
289 offset: audio_offset,
290 len: audio_length,
291 });
292 if audio_length % 2 == 1 {
293 segments.push(Segment::Inline(vec![0x00]));
294 }
295
296 let body_len: u64 = size::checked_sum(segments.iter().map(Segment::len))?;
298 let riff_size =
299 u32::try_from(size::checked_add(body_len, 4)?).map_err(|_| FormatError::TooLarge)?;
300 let mut header = Vec::with_capacity(12);
301 header.extend_from_slice(b"RIFF");
302 header.extend_from_slice(&riff_size.to_le_bytes());
303 header.extend_from_slice(b"WAVE");
304 segments.insert(0, Segment::Inline(header));
305
306 Ok(RegionLayout::validated(segments)?)
307}
308
309fn info_to_key(id: &[u8; 4]) -> Option<&'static str> {
312 Some(match id {
313 b"INAM" => "title",
314 b"IART" => "artist",
315 b"IPRD" => "album",
316 b"ICRD" => "date",
317 b"IGNR" => "genre",
318 b"ICMT" => "comment",
319 b"ITRK" => "tracknumber",
320 _ => return None,
321 })
322}
323
324fn find_id3_chunk<'a>(buf: &'a [u8], chunks: &[([u8; 4], usize, u64)]) -> Option<&'a [u8]> {
326 let &(_, off, len) = chunks
327 .iter()
328 .find(|(id, _, _)| id == b"id3 " || id == b"ID3 ")?;
329 chunk_slice(buf, off, len)
330}
331
332fn read_info_tags(body: &[u8]) -> Vec<(String, String)> {
335 let mut out = Vec::new();
336 let mut pos = 0usize;
337 while pos + 8 <= body.len() {
338 let mut id = [0u8; 4];
339 id.copy_from_slice(&body[pos..pos + 4]);
340 let size = read_u32_le(body, pos + 4)
341 .expect("subchunk size field within the loop-guarded bounds")
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 }