1use crate::convert::usize_from;
2use crate::error::{FormatError, Result};
3use crate::input::{
4 ArtInput, BinaryTagInput, EmbeddedBinaryTag, EmbeddedPicture, PictureType, TagInput,
5};
6use crate::layout::{RegionLayout, Segment};
7use crate::probe::Extent;
8use crate::size;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Mp3Bounds {
16 pub audio_offset: u64,
17 pub audio_length: u64,
18}
19
20fn synchsafe_decode(b: &[u8]) -> u32 {
21 u32::from(b[0] & 0x7F) << 21
22 | u32::from(b[1] & 0x7F) << 14
23 | u32::from(b[2] & 0x7F) << 7
24 | u32::from(b[3] & 0x7F)
25}
26
27fn id3v2_header_len(data: &[u8]) -> Result<Option<usize>> {
28 if data.len() < 10 || &data[0..3] != b"ID3" {
29 return Ok(None);
30 }
31 if !matches!(data[3], 2..=4) {
32 return Err(FormatError::Malformed);
33 }
34 if data[6..10].iter().any(|&b| b & 0x80 != 0) {
37 return Err(FormatError::Malformed);
38 }
39 Ok(Some(10 + synchsafe_decode(&data[6..10]) as usize))
40}
41
42pub fn locate_audio(data: &[u8]) -> Result<Mp3Bounds> {
47 let len = data.len();
48
49 let mut audio_offset = 0usize;
50 if let Some(base) = id3v2_header_len(data)? {
51 let flags = data[5];
52 let mut tag_len = base;
53 if flags & 0x10 != 0 {
54 tag_len += 10; }
56 if tag_len > len {
57 return Err(FormatError::Malformed);
58 }
59 audio_offset = tag_len;
60 }
61
62 let mut audio_end = len;
63 if audio_end >= audio_offset + 128 && &data[audio_end - 128..audio_end - 125] == b"TAG" {
64 audio_end -= 128; }
66
67 if audio_offset + 1 >= len
69 || data[audio_offset] != 0xFF
70 || (data[audio_offset + 1] & 0xE0) != 0xE0
71 {
72 return Err(FormatError::NotMp3);
73 }
74
75 Ok(Mp3Bounds {
76 audio_offset: audio_offset as u64,
77 audio_length: (audio_end - audio_offset) as u64,
78 })
79}
80
81pub fn locate_audio_bounded(
88 prefix: &[u8],
89 file_len: u64,
90 tail: Option<&[u8; 128]>,
91) -> Result<Extent<Mp3Bounds>> {
92 let mut audio_offset = 0usize;
93 if prefix.len() < 10 && file_len >= 10 {
94 return Ok(Extent::NeedMore { up_to: 10 });
96 }
97 if let Some(base) = id3v2_header_len(prefix)? {
98 let flags = prefix[5];
99 let mut tag_len = base;
100 if flags & 0x10 != 0 {
101 tag_len += 10; }
103 if tag_len as u64 > file_len {
104 return Err(FormatError::Malformed);
105 }
106 audio_offset = tag_len;
107 }
108
109 if audio_offset as u64 + 2 > file_len {
115 return Err(FormatError::NotMp3);
116 }
117
118 if audio_offset + 2 > prefix.len() {
120 return Ok(Extent::NeedMore {
121 up_to: (audio_offset + 2) as u64,
122 });
123 }
124
125 if prefix[audio_offset] != 0xFF || (prefix[audio_offset + 1] & 0xE0) != 0xE0 {
126 return Err(FormatError::NotMp3);
127 }
128
129 let mut audio_end = file_len;
130 if let Some(tail) = tail
131 && file_len >= audio_offset as u64 + 128
132 && &tail[0..3] == b"TAG"
133 {
134 audio_end -= 128;
135 }
136
137 Ok(Extent::Complete(Mp3Bounds {
138 audio_offset: audio_offset as u64,
139 audio_length: audio_end - audio_offset as u64,
140 }))
141}
142
143const ENC_UTF8: u8 = 0x03;
144
145fn syncsafe(n: u32) -> [u8; 4] {
146 [
147 ((n >> 21) & 0x7F) as u8,
148 ((n >> 14) & 0x7F) as u8,
149 ((n >> 7) & 0x7F) as u8,
150 (n & 0x7F) as u8,
151 ]
152}
153
154const SYNCHSAFE_MAX: u32 = 0x0FFF_FFFF;
156
157fn push_frame_header(out: &mut Vec<u8>, id: &[u8; 4], data_len: usize) -> Result<()> {
158 let data_len_u32 = u32::try_from(data_len)
161 .ok()
162 .filter(|&v| v <= SYNCHSAFE_MAX)
163 .ok_or(FormatError::TooLarge)?;
164 out.extend_from_slice(id);
165 out.extend_from_slice(&syncsafe(data_len_u32));
166 out.extend_from_slice(&[0x00, 0x00]); Ok(())
168}
169
170fn text_frame_data(values: &[String]) -> Vec<u8> {
171 let mut d = vec![ENC_UTF8];
172 d.extend_from_slice(values.join("\0").as_bytes());
173 d
174}
175
176fn txxx_frame_data(desc: &str, value: &str) -> Vec<u8> {
177 let mut d = vec![ENC_UTF8];
178 d.extend_from_slice(desc.as_bytes());
179 d.push(0x00);
180 d.extend_from_slice(value.as_bytes());
181 d
182}
183
184fn comm_like_frame_data(value: &str) -> Vec<u8> {
188 let mut d = vec![ENC_UTF8];
189 d.extend_from_slice(b"XXX"); d.push(0x00); d.extend_from_slice(value.as_bytes());
192 d
193}
194
195fn is_id3_text_frame_id(key: &str) -> bool {
198 key.len() == 4
199 && key != "TXXX"
200 && key.starts_with('T')
201 && key
202 .bytes()
203 .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit())
204}
205
206fn apic_framing(art: &ArtInput) -> Vec<u8> {
209 let mut d = vec![ENC_UTF8];
210 d.extend_from_slice(art.mime.as_bytes());
211 d.push(0x00);
212 #[expect(
213 clippy::cast_possible_truncation,
214 reason = "ID3 APIC type is one byte; valid picture types are 0..=20"
215 )]
216 d.push(art.picture_type.get() as u8);
217 d.extend_from_slice(art.description.as_bytes());
218 d.push(0x00);
219 d
220}
221
222fn popm_frame_data(rating: u8, playcount: u64) -> Vec<u8> {
228 let mut d = Vec::new();
229 d.push(0x00); d.push(rating);
231 if playcount > 0 {
232 let c = u32::try_from(playcount).unwrap_or(u32::MAX);
233 d.extend_from_slice(&c.to_be_bytes());
234 }
235 d
236}
237
238fn ufid_frame_data(owner: &str, identifier: &[u8]) -> Vec<u8> {
240 let mut d = Vec::new();
241 d.extend_from_slice(owner.as_bytes());
242 d.push(0x00);
243 d.extend_from_slice(identifier);
244 d
245}
246
247fn is_promoted_key(key: &str) -> bool {
250 matches!(key, "rating" | "playcount" | "musicbrainz_trackid")
251}
252
253pub fn build_id3v2_segments(
259 tags: &[TagInput],
260 binary_tags: &[BinaryTagInput],
261 arts: &[ArtInput],
262) -> Result<(Vec<Segment>, u64)> {
263 let mut popm_rating: Option<u8> = None;
268 let mut popm_playcount: u64 = 0;
269 let mut mbid: Option<String> = None;
270 for t in tags {
271 match t.key.as_str() {
272 "rating" if popm_rating.is_none() => popm_rating = t.value.parse().ok(),
273 "playcount" => popm_playcount = t.value.parse().unwrap_or(popm_playcount),
274 "musicbrainz_trackid" if mbid.is_none() => mbid = Some(t.value.clone()),
275 _ => {}
276 }
277 }
278
279 let mut groups: Vec<(String, Vec<String>)> = Vec::new();
283 for t in tags {
284 if is_promoted_key(&t.key) {
285 continue;
286 }
287 match groups.last_mut() {
288 Some(g) if g.0 == t.key => g.1.push(t.value.clone()),
289 _ => groups.push((t.key.clone(), vec![t.value.clone()])),
290 }
291 }
292
293 let mut segments: Vec<Segment> = Vec::new();
294 let mut buf: Vec<u8> = Vec::new();
295 let mut frames_len: u64 = 0;
296
297 for (key, values) in &groups {
298 match crate::tagmap::key_to_id3(key) {
299 Some(crate::tagmap::Id3Slot::Text(id)) => {
300 let data = text_frame_data(values);
301 push_frame_header(&mut buf, id, data.len())?;
302 buf.extend_from_slice(&data);
303 frames_len = size::checked_add(frames_len, 10 + data.len() as u64)?;
304 }
305 Some(crate::tagmap::Id3Slot::Txxx(desc)) => {
306 for value in values {
307 let data = txxx_frame_data(desc, value);
308 push_frame_header(&mut buf, b"TXXX", data.len())?;
309 buf.extend_from_slice(&data);
310 frames_len = size::checked_add(frames_len, 10 + data.len() as u64)?;
311 }
312 }
313 Some(crate::tagmap::Id3Slot::Comment) => {
314 for value in values {
315 let data = comm_like_frame_data(value);
316 push_frame_header(&mut buf, b"COMM", data.len())?;
317 buf.extend_from_slice(&data);
318 frames_len = size::checked_add(frames_len, 10 + data.len() as u64)?;
319 }
320 }
321 Some(crate::tagmap::Id3Slot::Lyrics) => {
322 for value in values {
323 let data = comm_like_frame_data(value);
324 push_frame_header(&mut buf, b"USLT", data.len())?;
325 buf.extend_from_slice(&data);
326 frames_len = size::checked_add(frames_len, 10 + data.len() as u64)?;
327 }
328 }
329 None if is_id3_text_frame_id(key) => {
330 let id: [u8; 4] = key.as_bytes().try_into().unwrap();
332 let data = text_frame_data(values);
333 push_frame_header(&mut buf, &id, data.len())?;
334 buf.extend_from_slice(&data);
335 frames_len = size::checked_add(frames_len, 10 + data.len() as u64)?;
336 }
337 None => {
338 for value in values {
339 let data = txxx_frame_data(key, value);
340 push_frame_header(&mut buf, b"TXXX", data.len())?;
341 buf.extend_from_slice(&data);
342 frames_len = size::checked_add(frames_len, 10 + data.len() as u64)?;
343 }
344 }
345 }
346 }
347
348 if let Some(rating) = popm_rating {
350 let data = popm_frame_data(rating, popm_playcount);
351 push_frame_header(&mut buf, b"POPM", data.len())?;
352 buf.extend_from_slice(&data);
353 frames_len = size::checked_add(frames_len, 10 + data.len() as u64)?;
354 }
355 if let Some(id) = &mbid {
356 let data = ufid_frame_data(MUSICBRAINZ_UFID_OWNER, id.as_bytes());
357 push_frame_header(&mut buf, b"UFID", data.len())?;
358 buf.extend_from_slice(&data);
359 frames_len = size::checked_add(frames_len, 10 + data.len() as u64)?;
360 }
361
362 for bt in binary_tags {
364 let Ok(id): std::result::Result<[u8; 4], _> = bt.key.as_bytes().try_into() else {
366 continue;
367 };
368 push_frame_header(&mut buf, &id, usize_from(bt.len.get()))?;
369 segments.push(Segment::Inline(std::mem::take(&mut buf)));
370 segments.push(Segment::BinaryTag {
371 payload_id: bt.payload_id,
372 len: bt.len,
373 });
374 frames_len = size::checked_add(frames_len, size::checked_add(10, bt.len.get())?)?;
375 }
376
377 for art in arts {
378 let framing = apic_framing(art);
379 let data_len = size::checked_add(framing.len() as u64, art.data_len.get())?;
380 push_frame_header(&mut buf, b"APIC", usize_from(data_len))?;
381 buf.extend_from_slice(&framing);
382 segments.push(Segment::Inline(std::mem::take(&mut buf)));
383 segments.push(Segment::ArtImage {
384 art_id: art.art_id,
385 len: art.data_len,
386 });
387 frames_len = size::checked_add(frames_len, size::checked_add(10, data_len)?)?;
388 }
389
390 if !buf.is_empty() {
391 segments.push(Segment::Inline(std::mem::take(&mut buf)));
392 }
393
394 let mut header = Vec::with_capacity(10);
396 header.extend_from_slice(b"ID3");
397 header.extend_from_slice(&[0x04, 0x00]); header.push(0x00); let frames_len_ss = u32::try_from(frames_len)
404 .ok()
405 .filter(|&v| v <= SYNCHSAFE_MAX)
406 .ok_or(FormatError::TooLarge)?;
407 header.extend_from_slice(&syncsafe(frames_len_ss));
408 segments.insert(0, Segment::Inline(header));
409
410 Ok((segments, size::checked_add(10, frames_len)?))
411}
412
413pub fn synthesize_layout(
417 audio_offset: u64,
418 audio_length: u64,
419 tags: &[TagInput],
420 binary_tags: &[BinaryTagInput],
421 arts: &[ArtInput],
422) -> Result<RegionLayout> {
423 let (mut segments, _tag_len) = build_id3v2_segments(tags, binary_tags, arts)?;
424 segments.push(Segment::BackingAudio {
425 offset: audio_offset,
426 len: audio_length,
427 });
428 Ok(RegionLayout::validated(segments)?)
429}
430
431fn id3v2_alloc_safe(data: &[u8]) -> bool {
441 let Ok(Some(tag_end)) = id3v2_header_len(data) else {
449 return false;
451 };
452 let flags = data[5];
453 if flags & 0xC0 != 0 {
456 return false;
457 }
458 if tag_end > data.len() {
459 return false;
460 }
461 let major = data[3];
462 let header_len = if major == 2 { 6 } else { 10 };
463 let scan_end = data.len();
469 let mut pos = 10usize;
470 while pos + header_len <= scan_end {
471 if data[pos] == 0 {
473 break;
474 }
475 if major != 2 && (&data[pos..pos + 4] == b"CHAP" || &data[pos..pos + 4] == b"CTOC") {
480 return false;
481 }
482 let size = if major == 2 {
483 u32::from_be_bytes([0, data[pos + 3], data[pos + 4], data[pos + 5]]) as usize
484 } else if major == 3 {
485 if data[pos + 8] != 0 || data[pos + 9] != 0 {
492 return false;
493 }
494 u32::from_be_bytes([data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]])
495 as usize
496 } else {
497 if data[pos + 4] | data[pos + 5] | data[pos + 6] | data[pos + 7] >= 0x80 {
501 return false;
502 }
503 if data[pos + 8] != 0 || data[pos + 9] != 0 {
504 return false;
505 }
506 synchsafe_decode(&data[pos + 4..pos + 8]) as usize
507 };
508 let data_start = pos + header_len;
509 if data_start > tag_end || size > tag_end - data_start {
514 return false;
515 }
516 pos = data_start + size;
517 if pos >= tag_end {
520 break;
521 }
522 }
523 true
524}
525
526pub fn read_pictures(data: &[u8]) -> Vec<EmbeddedPicture> {
529 if !id3v2_alloc_safe(data) {
530 return Vec::new();
531 }
532 let Ok(tag) = id3::Tag::read_from2(std::io::Cursor::new(data)) else {
533 return Vec::new();
534 };
535 tag.pictures()
536 .map(|p| EmbeddedPicture {
537 mime: p.mime_type.clone(),
538 picture_type: PictureType::new(u8::from(p.picture_type).into())
542 .unwrap_or(PictureType::ZERO),
543 description: p.description.clone(),
544 width: 0,
545 height: 0,
546 data: p.data.clone(),
547 })
548 .collect()
549}
550
551pub fn read_tags(data: &[u8]) -> Vec<(String, String)> {
559 if !id3v2_alloc_safe(data) {
560 return Vec::new();
561 }
562 let Ok(tag) = id3::Tag::read_from2(std::io::Cursor::new(data)) else {
563 return Vec::new();
564 };
565 let mut out = Vec::new();
566 for frame in tag.frames() {
567 let content = frame.content();
568 if let Some(et) = content.extended_text() {
569 let key = crate::tagmap::id3_txxx_to_key(&et.description)
570 .map_or_else(|| et.description.clone(), str::to_string);
571 out.push((key, et.value.clone()));
572 } else if let Some(c) = content.comment() {
573 out.push(("comment".to_string(), c.text.clone()));
574 } else if let Some(l) = content.lyrics() {
575 out.push(("lyrics".to_string(), l.text.clone()));
576 } else if let Some(text) = content.text() {
577 let id = frame.id();
578 let key =
579 crate::tagmap::id3_text_to_key(id).map_or_else(|| id.to_string(), str::to_string);
580 for value in text.split('\0').filter(|v| !v.is_empty()) {
581 out.push((key.clone(), value.to_string()));
582 }
583 }
584 }
585 out
586}
587
588pub(crate) const MUSICBRAINZ_UFID_OWNER: &str = "http://musicbrainz.org";
589
590pub fn read_binary_tags(data: &[u8]) -> (Vec<EmbeddedBinaryTag>, Vec<(String, String)>) {
602 let mut opaque = Vec::new();
603 let mut promoted = Vec::new();
604 if !id3v2_alloc_safe(data) || data[3] < 3 {
605 return (opaque, promoted);
606 }
607 let tag_end = 10 + synchsafe_decode(&data[6..10]) as usize;
608 let mut pos = 10usize;
609 while pos + 10 <= tag_end {
610 if data[pos] == 0 {
611 break;
612 }
613 let id = &data[pos..pos + 4];
614 let size = if data[3] == 3 {
615 u32::from_be_bytes([data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]])
616 as usize
617 } else {
618 synchsafe_decode(&data[pos + 4..pos + 8]) as usize
619 };
620 let body_start = pos + 10;
621 if body_start + size > tag_end {
622 break;
623 }
624 classify_binary_frame(
625 id,
626 &data[body_start..body_start + size],
627 &mut opaque,
628 &mut promoted,
629 );
630 pos = body_start + size;
631 }
632 (opaque, promoted)
633}
634
635fn classify_binary_frame(
637 id: &[u8],
638 body: &[u8],
639 opaque: &mut Vec<EmbeddedBinaryTag>,
640 promoted: &mut Vec<(String, String)>,
641) {
642 if id[0] == b'T' || id == b"COMM" || id == b"USLT" || id == b"APIC" {
644 return;
645 }
646 match id {
647 b"POPM" => {
648 if let Some(nul) = body.iter().position(|&b| b == 0)
650 && let Some((&rating, counter)) = body[nul + 1..].split_first()
651 {
652 promoted.push(("rating".to_string(), rating.to_string()));
653 let c = counter
654 .iter()
655 .take(8)
656 .fold(0u64, |a, &b| (a << 8) | u64::from(b));
657 if c > 0 {
658 promoted.push(("playcount".to_string(), c.to_string()));
659 }
660 }
661 }
662 b"UFID" => {
663 match body.iter().position(|&b| b == 0) {
665 Some(nul) if &body[..nul] == MUSICBRAINZ_UFID_OWNER.as_bytes() => {
666 promoted.push((
667 "musicbrainz_trackid".to_string(),
668 String::from_utf8_lossy(&body[nul + 1..]).into_owned(),
669 ));
670 }
671 _ => opaque.push(EmbeddedBinaryTag {
672 key: "UFID".to_string(),
673 payload: body.to_vec(),
674 }),
675 }
676 }
677 _ => {
678 if id.iter().all(u8::is_ascii_graphic) {
680 opaque.push(EmbeddedBinaryTag {
681 key: String::from_utf8_lossy(id).into_owned(),
682 payload: body.to_vec(),
683 });
684 }
685 }
686 }
687}
688
689#[cfg(test)]
690mod tests {
691 use super::*;
692 use crate::input::{BlobLen, PictureType};
693
694 #[test]
697 fn id3v2_guard_rejects_oversized_v23_frame() {
698 let mut bytes: Vec<u8> = Vec::new();
703 bytes.extend_from_slice(b"ID3");
704 bytes.push(0x03); bytes.push(0x00); bytes.push(0x00); bytes.extend_from_slice(&[0x00, 0x00, 0x00, 0x0A]);
709 bytes.extend_from_slice(b"TIT2");
711 bytes.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]);
712 bytes.extend_from_slice(&[0x00, 0x00]); assert!(
715 !id3v2_alloc_safe(&bytes),
716 "guard should reject frame claiming more bytes than the tag holds"
717 );
718 assert!(
720 read_tags(&bytes).is_empty(),
721 "read_tags must return empty for unsafe tag"
722 );
723 }
724
725 #[test]
729 fn id3v2_guard_rejects_non_id3_prefixed() {
730 assert!(
732 !id3v2_alloc_safe(b"RIFF....just not an id3 tag...."),
733 "guard must reject buffer not starting with ID3"
734 );
735 assert!(
736 read_tags(b"RIFF....just not an id3 tag....").is_empty(),
737 "read_tags must return empty for non-ID3-prefixed buffer"
738 );
739
740 const RIFF_BODY: &[u8] = &[
745 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,
748 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,
752 0x00, ];
754 assert!(
755 !id3v2_alloc_safe(RIFF_BODY),
756 "guard must reject RIFF-prefixed buffer (WAV crash vector)"
757 );
758 assert!(
759 read_tags(RIFF_BODY).is_empty(),
760 "read_tags must return empty for RIFF-prefixed buffer"
761 );
762 }
763
764 #[test]
767 fn id3v2_guard_allows_valid_tag() {
768 use id3::{Tag, TagLike, Version};
769
770 let mut tag = Tag::new();
771 tag.set_text("TIT2", "Hello");
772 tag.set_text("TPE1", "Artist");
773 let mut buf = Vec::new();
774 tag.write_to(&mut buf, Version::Id3v24).unwrap();
775
776 assert!(
777 id3v2_alloc_safe(&buf),
778 "guard should allow a well-formed tag written by the id3 crate"
779 );
780 let tags = read_tags(&buf);
781 assert!(
782 tags.contains(&("title".to_string(), "Hello".to_string())),
783 "missing title in {tags:?}"
784 );
785 assert!(
786 tags.contains(&("artist".to_string(), "Artist".to_string())),
787 "missing artist in {tags:?}"
788 );
789 }
790
791 #[test]
794 fn read_tags_handles_oom_crash_input_safely() {
795 const CRASH1: &[u8] = &[
799 0x49, 0x44, 0x33, 0x03, 0xf0, 0x00, 0x00, 0xf9, 0x2d, 0x49, 0x50, 0x4c, 0x53, 0x00, 0xf9, 0x3d, 0x02, 0x00, 0x2d, 0x01, 0x00, 0x00, 0x03, 0x00, 0x49, 0x07, 0x10, 0xff, 0x07, 0xfe,
806 ];
807 const CRASH2: &[u8] = &[
813 0x49, 0x44, 0x33, 0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0a, 0x27, 0x2f, 0x00, 0xff, 0xee, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x2f,
819 ];
820 for (i, crash) in [CRASH1, CRASH2].iter().enumerate() {
821 assert!(
822 read_tags(crash).is_empty(),
823 "read_tags must be safe on crash artifact {i}"
824 );
825 }
826 }
827
828 #[test]
829 fn read_tags_captures_txxx_comm_uslt_and_unmapped_text() {
830 use id3::frame::{Comment, ExtendedText, Lyrics};
831 use id3::{Tag, TagLike, Version}; let mut tag = Tag::new();
834 tag.set_text("TIT2", "Song");
835 tag.set_text("TBPM", "120"); tag.add_frame(ExtendedText {
837 description: "MOOD".into(),
838 value: "happy".into(),
839 });
840 tag.add_frame(ExtendedText {
841 description: "REPLAYGAIN_TRACK_GAIN".into(),
842 value: "-6.5 dB".into(),
843 });
844 tag.add_frame(Comment {
845 lang: "eng".into(),
846 description: String::new(),
847 text: "nice".into(),
848 });
849 tag.add_frame(Lyrics {
850 lang: "eng".into(),
851 description: String::new(),
852 text: "la la".into(),
853 });
854
855 let mut buf = Vec::new();
856 tag.write_to(&mut buf, Version::Id3v24).unwrap();
857
858 let tags = read_tags(&buf);
859 assert!(tags.contains(&("title".to_string(), "Song".to_string())));
860 assert!(tags.contains(&("TBPM".to_string(), "120".to_string())));
861 assert!(tags.contains(&("MOOD".to_string(), "happy".to_string())));
862 assert!(tags.contains(&("replaygain_track_gain".to_string(), "-6.5 dB".to_string())));
863 assert!(tags.contains(&("comment".to_string(), "nice".to_string())));
864 assert!(tags.contains(&("lyrics".to_string(), "la la".to_string())));
865 }
866
867 #[test]
868 fn synthesize_round_trips_arbitrary_id3_tags() {
869 let tags = vec![
870 TagInput::new("title", "Song"),
871 TagInput::new("TBPM", "120"), TagInput::new("MyRating", "5"), TagInput::new("comment", "nice"), TagInput::new("lyrics", "la la"), TagInput::new("replaygain_track_gain", "-3.21 dB"), ];
877 let (segments, _len) = build_id3v2_segments(&tags, &[], &[]).unwrap();
878 let mut buf = Vec::new();
879 for seg in &segments {
880 if let Segment::Inline(bytes) = seg {
881 buf.extend_from_slice(bytes);
882 }
883 }
884 let read = read_tags(&buf);
885 for expected in [
886 ("title", "Song"),
887 ("TBPM", "120"),
888 ("MyRating", "5"),
889 ("comment", "nice"),
890 ("lyrics", "la la"),
891 ("replaygain_track_gain", "-3.21 dB"),
892 ] {
893 assert!(
894 read.contains(&(expected.0.to_string(), expected.1.to_string())),
895 "missing {expected:?} in {read:?}"
896 );
897 }
898 }
899
900 #[test]
901 fn synchsafe_decode_assembles_7bit_groups() {
902 assert_eq!(synchsafe_decode(&[0x01, 0x02, 0x03, 0x04]), 0x0020_8184);
904 assert_eq!(synchsafe_decode(&[0xFF, 0xFF, 0xFF, 0xFF]), 0x0FFF_FFFF);
906 assert_eq!(synchsafe_decode(&[0x7F, 0x00, 0x00, 0x00]), 0x0FE0_0000);
908 assert_eq!(synchsafe_decode(&[0x00, 0x7F, 0x00, 0x00]), 0x001F_C000);
910 }
911
912 #[test]
913 fn syncsafe_encodes_and_round_trips() {
914 assert_eq!(syncsafe(0x0FE0_0000), [0x7F, 0x00, 0x00, 0x00]);
916 assert_eq!(syncsafe(0x001F_C000), [0x00, 0x7F, 0x00, 0x00]);
917 for n in [0u32, 1, 127, 128, 0x0123_4567, 0x0FFF_FFFF] {
919 assert_eq!(synchsafe_decode(&syncsafe(n)), n);
920 }
921 }
922
923 #[test]
924 fn locate_audio_no_id3_starts_at_zero() {
925 let data = [0xFF, 0xFB, 0x90, 0x00, 0, 0, 0, 0, 0, 0];
929 let b = locate_audio(&data).unwrap();
930 assert_eq!(b.audio_offset, 0);
931 assert_eq!(b.audio_length, 10);
932 }
933
934 #[test]
935 fn locate_audio_skips_id3v2_then_finds_sync() {
936 let mut data = Vec::new();
938 data.extend_from_slice(b"ID3");
939 data.extend_from_slice(&[0x04, 0x00, 0x00]); data.extend_from_slice(&[0x00, 0x00, 0x00, 0x04]); data.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD]); data.extend_from_slice(&[0xFF, 0xFB, 0x90, 0x00]); let b = locate_audio(&data).unwrap();
944 assert_eq!(b.audio_offset, 14);
945 assert_eq!(b.audio_length, 4);
946 }
947
948 #[test]
949 fn locate_audio_honors_footer_flag() {
950 let mut data = Vec::new();
954 data.extend_from_slice(b"ID3");
955 data.extend_from_slice(&[0x04, 0x00, 0x10]); data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); data.extend_from_slice(&[0u8; 10]); data.extend_from_slice(&[0xFF, 0xFB, 0x90, 0x00]); let b = locate_audio(&data).unwrap();
960 assert_eq!(b.audio_offset, 20);
961 }
962
963 #[test]
964 fn locate_audio_requires_frame_sync() {
965 let data = [0xFF, 0x00, 0x00, 0x00, 0, 0, 0, 0, 0, 0];
970 assert_eq!(locate_audio(&data), Err(FormatError::NotMp3));
971 assert_eq!(locate_audio(&[0xFF]), Err(FormatError::NotMp3));
974 }
975
976 #[test]
977 fn push_frame_header_size_boundary_is_inclusive() {
978 let mut out = Vec::new();
981 assert!(push_frame_header(&mut out, b"TIT2", 0x0FFF_FFFF).is_ok());
982 let mut over = Vec::new();
983 assert_eq!(
984 push_frame_header(&mut over, b"TIT2", 0x1000_0000),
985 Err(FormatError::TooLarge)
986 );
987 }
988
989 #[test]
990 fn is_id3_text_frame_id_classifies_text_frames() {
991 assert!(is_id3_text_frame_id("TPE1")); assert!(is_id3_text_frame_id("TIT2"));
993 assert!(!is_id3_text_frame_id("TXXX")); assert!(!is_id3_text_frame_id("COMM")); assert!(!is_id3_text_frame_id("TPE")); assert!(!is_id3_text_frame_id("Txx1")); }
998
999 #[test]
1000 fn build_id3v2_segments_emits_standard_text_frame_as_itself() {
1001 let tags = vec![TagInput::new("TPE1", "Band")];
1005 let (segments, _len) = build_id3v2_segments(&tags, &[], &[]).unwrap();
1006 let mut buf = Vec::new();
1007 for seg in &segments {
1008 if let Segment::Inline(b) = seg {
1009 buf.extend_from_slice(b);
1010 }
1011 }
1012 assert!(
1014 buf.windows(4).any(|w| w == b"TPE1"),
1015 "TPE1 frame not emitted: routed elsewhere"
1016 );
1017 let read = read_tags(&buf);
1019 assert!(
1020 read.contains(&("artist".to_string(), "Band".to_string())),
1021 "got {read:?}"
1022 );
1023 }
1024
1025 #[test]
1026 fn build_id3v2_segments_rejects_oversized_total_tag() {
1027 let mk = |data_len: u64| ArtInput {
1031 art_id: 1,
1032 mime: "image/png".to_string(),
1033 description: String::new(),
1034 picture_type: PictureType::new(3).unwrap(),
1035 width: 0,
1036 height: 0,
1037 data_len: BlobLen::new(data_len).unwrap(),
1038 };
1039 assert_eq!(
1040 build_id3v2_segments(&[], &[], &[mk(0x1000_0000)]).err(),
1041 Some(FormatError::TooLarge)
1042 );
1043 assert!(build_id3v2_segments(&[], &[], &[mk(16)]).is_ok());
1044 let (_, total_at_one) = build_id3v2_segments(&[], &[], &[mk(1)]).unwrap();
1049 let overhead = total_at_one - 10 - 1; let boundary_data_len = 0x0FFF_FFFF - overhead;
1051 assert!(
1052 build_id3v2_segments(&[], &[], &[mk(boundary_data_len)]).is_ok(),
1053 "exact boundary (frames_len == 0x0FFF_FFFF) should be accepted"
1054 );
1055 assert_eq!(
1056 build_id3v2_segments(&[], &[], &[mk(boundary_data_len + 1)]).err(),
1057 Some(FormatError::TooLarge),
1058 "one byte past boundary must be rejected"
1059 );
1060 }
1061
1062 #[test]
1063 fn build_id3v2_segments_emits_art_segment_with_correct_id_and_len() {
1064 let mk = |art_id: i64, data_len: u64| ArtInput {
1067 art_id,
1068 mime: "image/png".to_string(),
1069 description: String::new(),
1070 picture_type: PictureType::new(3).unwrap(),
1071 width: 0,
1072 height: 0,
1073 data_len: BlobLen::new(data_len).unwrap(),
1074 };
1075 let (segments, _len) = build_id3v2_segments(&[], &[], &[mk(2, 16)]).unwrap();
1076 let art_segs: Vec<_> = segments
1077 .iter()
1078 .filter_map(|s| match s {
1079 Segment::ArtImage { art_id, len } => Some((*art_id, len.get())),
1080 _ => None,
1081 })
1082 .collect();
1083 assert_eq!(
1084 art_segs,
1085 vec![(2_i64, 16_u64)],
1086 "only the non-empty art should be emitted"
1087 );
1088 }
1089
1090 fn ss(n: u32) -> [u8; 4] {
1093 [
1094 ((n >> 21) & 0x7F) as u8,
1095 ((n >> 14) & 0x7F) as u8,
1096 ((n >> 7) & 0x7F) as u8,
1097 (n & 0x7F) as u8,
1098 ]
1099 }
1100
1101 fn id3v2(major: u8, flags: u8, body: u32, frames: &[u8]) -> Vec<u8> {
1104 let mut v = Vec::new();
1105 v.extend_from_slice(b"ID3");
1106 v.push(major);
1107 v.push(0x00);
1108 v.push(flags);
1109 v.extend_from_slice(&ss(body));
1110 v.extend_from_slice(frames);
1111 v
1112 }
1113
1114 #[test]
1115 fn alloc_safe_accepts_minimal_valid_header() {
1116 let tag = id3v2(0x04, 0x00, 0, &[]);
1119 assert_eq!(tag.len(), 10);
1120 assert!(id3v2_alloc_safe(&tag));
1121 }
1122
1123 #[test]
1124 fn alloc_safe_rejects_short_and_non_id3() {
1125 assert!(!id3v2_alloc_safe(b"ID3xx"));
1129 assert!(!id3v2_alloc_safe(b"XXX\x04\x00\x00\x00\x00\x00\x00"));
1131 }
1132
1133 #[test]
1134 fn alloc_safe_rejects_bad_version_and_header_flags() {
1135 assert!(!id3v2_alloc_safe(&id3v2(0x05, 0x00, 0, &[])));
1137 assert!(!id3v2_alloc_safe(&id3v2(0x01, 0x00, 0, &[])));
1138 assert!(!id3v2_alloc_safe(&id3v2(0x04, 0x40, 0, &[])));
1140 assert!(!id3v2_alloc_safe(&id3v2(0x04, 0x80, 0, &[])));
1141 }
1142
1143 #[test]
1144 fn alloc_safe_rejects_high_bit_in_body_size() {
1145 let tag = vec![b'I', b'D', b'3', 0x04, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00];
1149 assert!(!id3v2_alloc_safe(&tag));
1150 let tag1 = vec![b'I', b'D', b'3', 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80];
1152 assert!(!id3v2_alloc_safe(&tag1));
1153 }
1154
1155 #[test]
1156 fn alloc_safe_rejects_high_bit_in_v24_frame_size() {
1157 let mut frame = b"TIT2".to_vec();
1163 frame.extend_from_slice(&[0x80, 0x80, 0x00, 0x00]); frame.extend_from_slice(&[0x00, 0x00]); let tag = id3v2(0x04, 0x00, 10, &frame);
1166 assert!(!id3v2_alloc_safe(&tag));
1167 }
1168
1169 fn v23_frame(id: &[u8; 4], size: u32, payload: &[u8]) -> Vec<u8> {
1172 let mut v = id.to_vec();
1173 v.extend_from_slice(&size.to_be_bytes());
1174 v.extend_from_slice(&[0x00, 0x00]);
1175 v.extend_from_slice(payload);
1176 v
1177 }
1178
1179 #[test]
1180 fn alloc_safe_v22_24bit_size_decode() {
1181 let mut f_mid = b"TT2".to_vec();
1186 f_mid.extend_from_slice(&[0x00, 0x01, 0x00]); assert!(!id3v2_alloc_safe(&id3v2(0x02, 0x00, 6, &f_mid))); let mut f_hi = b"TT2".to_vec();
1190 f_hi.extend_from_slice(&[0x01, 0x00, 0x00]);
1191 assert!(!id3v2_alloc_safe(&id3v2(0x02, 0x00, 6, &f_hi)));
1192 let mut f_lo = b"TT2".to_vec();
1196 f_lo.extend_from_slice(&[0x00, 0x00, 0x10]); assert!(!id3v2_alloc_safe(&id3v2(0x02, 0x00, 6, &f_lo)));
1198 let mut f_ok = b"TT2".to_vec();
1200 f_ok.extend_from_slice(&[0x00, 0x00, 0x04]);
1201 f_ok.extend_from_slice(&[1, 2, 3, 4]);
1202 assert!(id3v2_alloc_safe(&id3v2(0x02, 0x00, 10, &f_ok)));
1203 }
1204
1205 #[test]
1206 fn alloc_safe_rejects_nonzero_frame_flags() {
1207 let mut f3 = b"TIT2".to_vec();
1209 f3.extend_from_slice(&4u32.to_be_bytes()); f3.extend_from_slice(&[0x00, 0x01]); f3.extend_from_slice(&[1, 2, 3, 4]);
1212 assert!(!id3v2_alloc_safe(&id3v2(0x03, 0x00, 14, &f3)));
1213
1214 let mut f4 = b"TIT2".to_vec();
1217 f4.extend_from_slice(&ss(4)); f4.extend_from_slice(&[0x00, 0x01]); f4.extend_from_slice(&[1, 2, 3, 4]);
1220 assert!(!id3v2_alloc_safe(&id3v2(0x04, 0x00, 14, &f4)));
1221 }
1222
1223 #[test]
1224 fn alloc_safe_rejects_chap_and_ctoc() {
1225 let chap = v23_frame(b"CHAP", 4, &[1, 2, 3, 4]);
1227 assert!(!id3v2_alloc_safe(&id3v2(0x03, 0x00, 14, &chap)));
1228 let ctoc = v23_frame(b"CTOC", 4, &[1, 2, 3, 4]);
1229 assert!(!id3v2_alloc_safe(&id3v2(0x03, 0x00, 14, &ctoc)));
1230 }
1231
1232 #[test]
1233 fn alloc_safe_frame_size_bounds() {
1234 let ok = v23_frame(b"TIT2", 4, &[1, 2, 3, 4]);
1239 assert!(id3v2_alloc_safe(&id3v2(0x03, 0x00, 14, &ok)));
1240 let over = v23_frame(b"TIT2", 5, &[1, 2, 3, 4]);
1245 assert!(!id3v2_alloc_safe(&id3v2(0x03, 0x00, 14, &over)));
1246 }
1247
1248 #[test]
1249 fn alloc_safe_data_start_equal_to_tag_end_is_ok() {
1250 let zero = v23_frame(b"TIT2", 0, &[]);
1253 assert!(id3v2_alloc_safe(&id3v2(0x03, 0x00, 10, &zero)));
1254 }
1255
1256 #[test]
1257 fn alloc_safe_rejects_bad_second_frame_in_body() {
1258 let mut frames = v23_frame(b"TIT2", 2, &[0xAA, 0xBB]); frames.extend_from_slice(&v23_frame(b"TPE1", 100, &[1, 2, 3, 4])); assert!(!id3v2_alloc_safe(&id3v2(0x03, 0x00, 26, &frames)));
1266 }
1267
1268 #[test]
1269 fn alloc_safe_stops_at_tag_body_end() {
1270 let mut frames = v23_frame(b"TIT2", 0, &[]); frames.extend_from_slice(&v23_frame(b"TPE1", 100, &[1, 2, 3, 4])); assert!(id3v2_alloc_safe(&id3v2(0x03, 0x00, 10, &frames)));
1277 }
1278
1279 #[test]
1280 fn alloc_safe_walks_two_frames_and_stops_at_padding() {
1281 let mut frames = v23_frame(b"TIT2", 2, &[0xAA, 0xBB]);
1288 frames.extend_from_slice(&v23_frame(b"TPE1", 2, &[0xCC, 0xDD]));
1289 frames.extend_from_slice(&[0u8; 10]); assert!(id3v2_alloc_safe(&id3v2(0x03, 0x00, 25, &frames)));
1291 }
1292
1293 #[test]
1294 fn alloc_safe_rejects_frame_size_exceeding_tag_end() {
1295 let huge = v23_frame(b"TIT2", 100, &[1, 2, 3, 4]);
1298 assert!(!id3v2_alloc_safe(&id3v2(0x03, 0x00, 14, &huge)));
1299 }
1300
1301 fn mp3_with_id3v2(body_len: usize, audio: &[u8]) -> (Vec<u8>, u64) {
1304 let mut v = b"ID3\x04\x00\x00".to_vec(); v.extend_from_slice(&syncsafe(u32::try_from(body_len).unwrap()));
1306 v.extend(std::iter::repeat_n(0u8, body_len)); let audio_offset = v.len() as u64;
1308 v.extend_from_slice(&[0xFF, 0xFB]); v.extend_from_slice(audio);
1310 (v, audio_offset)
1311 }
1312
1313 #[test]
1314 fn locate_audio_bounded_complete_with_no_id3v1() {
1315 let (full, audio_offset) = mp3_with_id3v2(8, b"frames");
1316 let prefix = &full[..usize_from(audio_offset) + 2]; let file_len = full.len() as u64;
1318 match locate_audio_bounded(prefix, file_len, None).unwrap() {
1319 Extent::Complete(b) => {
1320 assert_eq!(b.audio_offset, audio_offset);
1321 assert_eq!(b.audio_length, file_len - audio_offset);
1322 }
1323 other @ Extent::NeedMore { .. } => panic!("expected Complete, got {other:?}"),
1324 }
1325 }
1326
1327 #[test]
1328 fn locate_audio_bounded_needmore_when_tag_exceeds_prefix() {
1329 let (full, _audio_offset) = mp3_with_id3v2(4096, b"frames");
1330 let prefix = &full[..32]; let file_len = full.len() as u64;
1332 match locate_audio_bounded(prefix, file_len, None).unwrap() {
1333 Extent::NeedMore { up_to } => assert_eq!(up_to, 10 + 4096 + 2),
1334 other @ Extent::Complete(_) => panic!("expected NeedMore, got {other:?}"),
1335 }
1336 }
1337
1338 #[test]
1339 fn locate_audio_bounded_strips_id3v1_tail() {
1340 let (mut full, audio_offset) = mp3_with_id3v2(8, b"frames");
1341 let body_end = full.len();
1342 full.extend_from_slice(b"TAG"); full.extend(std::iter::repeat_n(0u8, 125)); let file_len = full.len() as u64;
1345 let tail: [u8; 128] = full[full.len() - 128..].try_into().unwrap();
1346 let prefix = &full[..usize_from(audio_offset) + 2];
1347 match locate_audio_bounded(prefix, file_len, Some(&tail)).unwrap() {
1348 Extent::Complete(b) => {
1349 assert_eq!(b.audio_offset, audio_offset);
1350 assert_eq!(b.audio_length, body_end as u64 - audio_offset);
1351 }
1352 other @ Extent::NeedMore { .. } => panic!("expected Complete, got {other:?}"),
1353 }
1354 }
1355
1356 #[test]
1357 fn locate_audio_bounded_rejects_audio_start_past_eof() {
1358 let mut full = b"ID3\x04\x00\x00".to_vec();
1362 full.extend_from_slice(&syncsafe(8));
1363 full.extend(std::iter::repeat_n(0u8, 8)); let file_len = full.len() as u64; match locate_audio_bounded(&full, file_len, None) {
1366 Err(FormatError::NotMp3) => {}
1367 other => panic!("expected Err(NotMp3), got {other:?}"),
1368 }
1369 }
1370
1371 #[test]
1378 fn locate_audio_bounded_plain_mp3_no_id3_starts_at_zero() {
1379 let data = [0xFF, 0xFB, 0x90, 0x00, 1, 2, 3, 4, 5, 6, 7, 8];
1381 let file_len = data.len() as u64;
1382 match locate_audio_bounded(&data, file_len, None).unwrap() {
1383 Extent::Complete(b) => {
1384 assert_eq!(b.audio_offset, 0);
1385 assert_eq!(b.audio_length, file_len);
1386 }
1387 other @ Extent::NeedMore { .. } => {
1388 panic!("expected Complete at offset 0, got {other:?}")
1389 }
1390 }
1391 }
1392
1393 #[test]
1402 fn locate_audio_bounded_short_non_id3_with_small_file() {
1403 let data = [0xFF, 0xFB, 0x90, 0x00, 0x00];
1405 let file_len = data.len() as u64; match locate_audio_bounded(&data, file_len, None).unwrap() {
1407 Extent::Complete(b) => {
1408 assert_eq!(b.audio_offset, 0);
1409 assert_eq!(b.audio_length, 5);
1410 }
1411 other @ Extent::NeedMore { .. } => panic!("expected Complete, got {other:?}"),
1412 }
1413 }
1414
1415 #[test]
1421 fn locate_audio_bounded_footer_flag_adds_ten() {
1422 let body = 6usize;
1423 let mut full = b"ID3\x04\x00".to_vec();
1424 full.push(0x10); full.extend_from_slice(&syncsafe(u32::try_from(body).unwrap()));
1426 full.extend(std::iter::repeat_n(0u8, body)); full.extend(std::iter::repeat_n(0u8, 10)); let expected_offset = full.len() as u64; full.extend_from_slice(&[0xFF, 0xFB]); full.extend_from_slice(b"audio");
1431 let file_len = full.len() as u64;
1432 match locate_audio_bounded(&full, file_len, None).unwrap() {
1433 Extent::Complete(b) => {
1434 assert_eq!(b.audio_offset, 26);
1435 assert_eq!(b.audio_offset, expected_offset);
1436 assert_eq!(b.audio_length, file_len - 26);
1437 }
1438 other @ Extent::NeedMore { .. } => {
1439 panic!("expected Complete at offset 26, got {other:?}")
1440 }
1441 }
1442 }
1443
1444 #[test]
1451 fn locate_audio_bounded_tag_len_equals_file_len_is_notmp3_not_malformed() {
1452 let body = 8usize;
1453 let mut full = b"ID3\x04\x00\x00".to_vec();
1454 full.extend_from_slice(&syncsafe(u32::try_from(body).unwrap()));
1455 full.extend(std::iter::repeat_n(0u8, body)); let file_len = full.len() as u64; match locate_audio_bounded(&full, file_len, None) {
1458 Err(FormatError::NotMp3) => {}
1459 other => panic!("expected Err(NotMp3) for tag_len==file_len, got {other:?}"),
1460 }
1461 }
1462
1463 #[test]
1468 fn locate_audio_bounded_tag_len_exceeds_file_len_is_malformed() {
1469 let mut full = b"ID3\x04\x00\x00".to_vec();
1471 full.extend_from_slice(&syncsafe(100));
1472 full.extend_from_slice(&[0xFF, 0xFB]); let file_len = full.len() as u64; match locate_audio_bounded(&full, file_len, None) {
1475 Err(FormatError::Malformed) => {}
1476 other => panic!("expected Err(Malformed), got {other:?}"),
1477 }
1478 }
1479
1480 #[test]
1485 fn locate_audio_bounded_short_prefix_large_file_needs_header() {
1486 let prefix = [0x00, 0x00, 0x00, 0x00, 0x00]; let file_len = 64u64; match locate_audio_bounded(&prefix, file_len, None).unwrap() {
1489 Extent::NeedMore { up_to } => assert_eq!(up_to, 10),
1490 other @ Extent::Complete(_) => panic!("expected NeedMore{{up_to:10}}, got {other:?}"),
1491 }
1492 }
1493
1494 #[test]
1501 fn locate_audio_bounded_prefix_len_exactly_ten_proceeds() {
1502 let prefix = [0xFF, 0xFB, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
1504 let file_len = 64u64; match locate_audio_bounded(&prefix, file_len, None).unwrap() {
1506 Extent::Complete(b) => {
1507 assert_eq!(b.audio_offset, 0);
1508 assert_eq!(b.audio_length, file_len);
1509 }
1510 other @ Extent::NeedMore { .. } => {
1511 panic!("expected Complete (10<10 false), got {other:?}")
1512 }
1513 }
1514 }
1515
1516 #[test]
1524 fn locate_audio_bounded_short_prefix_small_file_proceeds() {
1525 let data = [0xFF, 0xFB, 0x90, 0x00, 0x00]; let file_len = 8u64;
1529 match locate_audio_bounded(&data, file_len, None).unwrap() {
1530 Extent::Complete(b) => {
1531 assert_eq!(b.audio_offset, 0);
1532 assert_eq!(b.audio_length, 8);
1533 }
1534 other @ Extent::NeedMore { .. } => {
1535 panic!("expected Complete (file_len<10), got {other:?}")
1536 }
1537 }
1538 }
1539
1540 #[test]
1547 fn locate_audio_bounded_sync_one_byte_past_eof_is_notmp3() {
1548 let body = 4usize;
1549 let mut full = b"ID3\x04\x00\x00".to_vec();
1550 full.extend_from_slice(&syncsafe(u32::try_from(body).unwrap()));
1551 full.extend(std::iter::repeat_n(0u8, body)); let audio_offset = full.len() as u64; full.push(0xFF); let file_len = audio_offset + 1; match locate_audio_bounded(&full, file_len, None) {
1557 Err(FormatError::NotMp3) => {}
1558 other => panic!("expected Err(NotMp3) (sync past EOF), got {other:?}"),
1559 }
1560 }
1561
1562 #[test]
1566 fn locate_audio_bounded_sync_fits_in_file_proceeds() {
1567 let (full, audio_offset) = mp3_with_id3v2(4, b"frames");
1568 let file_len = full.len() as u64; match locate_audio_bounded(&full, file_len, None).unwrap() {
1570 Extent::Complete(b) => assert_eq!(b.audio_offset, audio_offset),
1571 other @ Extent::NeedMore { .. } => panic!("expected Complete, got {other:?}"),
1572 }
1573 }
1574
1575 #[test]
1576 fn locate_audio_bounded_sync_exactly_at_eof_proceeds() {
1577 let body = 4usize;
1582 let mut full = b"ID3\x04\x00\x00".to_vec();
1583 full.extend_from_slice(&syncsafe(u32::try_from(body).unwrap()));
1584 full.extend(std::iter::repeat_n(0u8, body)); let audio_offset = full.len() as u64; full.push(0xFF); full.push(0xFB);
1588 let file_len = full.len() as u64; match locate_audio_bounded(&full, file_len, None).unwrap() {
1591 Extent::Complete(b) => {
1592 assert_eq!(b.audio_offset, audio_offset);
1593 assert_eq!(b.audio_length, 2);
1594 }
1595 other @ Extent::NeedMore { .. } => {
1596 panic!("expected Complete (exact fit), got {other:?}")
1597 }
1598 }
1599 }
1600
1601 #[test]
1611 fn locate_audio_bounded_rejects_bad_second_sync_byte() {
1612 let data = [
1614 0xFF, 0x00, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1615 ];
1616 let file_len = data.len() as u64;
1617 match locate_audio_bounded(&data, file_len, None) {
1618 Err(FormatError::NotMp3) => {}
1619 other => panic!("expected Err(NotMp3) (bad sync byte 1), got {other:?}"),
1620 }
1621 }
1622
1623 #[test]
1630 fn locate_audio_bounded_rejects_bad_second_sync_byte_after_id3() {
1631 let body = 4usize;
1632 let mut full = b"ID3\x04\x00\x00".to_vec();
1633 full.extend_from_slice(&syncsafe(u32::try_from(body).unwrap()));
1634 full.extend(std::iter::repeat_n(0u8, body)); full.extend_from_slice(&[0xFF, 0x00]); full.extend_from_slice(b"tail");
1637 let file_len = full.len() as u64;
1638 match locate_audio_bounded(&full, file_len, None) {
1639 Err(FormatError::NotMp3) => {}
1640 other => panic!("expected Err(NotMp3) (bad sync at 15), got {other:?}"),
1641 }
1642 }
1643
1644 #[test]
1651 fn locate_audio_bounded_needmore_for_sync_past_prefix() {
1652 let body = 4usize;
1653 let mut full = b"ID3\x04\x00\x00".to_vec();
1654 full.extend_from_slice(&syncsafe(u32::try_from(body).unwrap()));
1655 full.extend(std::iter::repeat_n(0u8, body)); full.extend_from_slice(&[0xFF, 0xFB]); full.extend_from_slice(b"more audio bytes here");
1658 let file_len = full.len() as u64; let prefix = &full[..15]; match locate_audio_bounded(prefix, file_len, None).unwrap() {
1661 Extent::NeedMore { up_to } => assert_eq!(up_to, 16), other @ Extent::Complete(_) => panic!("expected NeedMore{{up_to:16}}, got {other:?}"),
1663 }
1664 }
1665
1666 #[test]
1672 fn locate_audio_bounded_trims_id3v1_when_tag_and_room() {
1673 let (mut full, audio_offset) = mp3_with_id3v2(8, b"frames");
1674 let body_end = full.len();
1675 full.extend_from_slice(b"TAG");
1676 full.extend(std::iter::repeat_n(0u8, 125)); let file_len = full.len() as u64;
1678 assert!(file_len >= audio_offset + 128); let tail: [u8; 128] = full[full.len() - 128..].try_into().unwrap();
1680 let prefix = &full[..usize_from(audio_offset) + 2];
1681 match locate_audio_bounded(prefix, file_len, Some(&tail)).unwrap() {
1682 Extent::Complete(b) => {
1683 assert_eq!(b.audio_offset, audio_offset);
1684 assert_eq!(b.audio_length, file_len - audio_offset - 128);
1686 assert_eq!(b.audio_length, body_end as u64 - audio_offset);
1687 }
1688 other @ Extent::NeedMore { .. } => panic!("expected Complete (trimmed), got {other:?}"),
1689 }
1690 }
1691
1692 #[test]
1698 fn locate_audio_bounded_no_trim_when_tail_not_tag() {
1699 let (mut full, audio_offset) = mp3_with_id3v2(8, b"frames");
1700 full.extend(std::iter::repeat_n(0u8, 200));
1702 let file_len = full.len() as u64;
1703 assert!(file_len >= audio_offset + 128); let tail: [u8; 128] = full[full.len() - 128..].try_into().unwrap();
1705 assert_ne!(&tail[0..3], b"TAG"); let prefix = &full[..usize_from(audio_offset) + 2];
1707 match locate_audio_bounded(prefix, file_len, Some(&tail)).unwrap() {
1708 Extent::Complete(b) => {
1709 assert_eq!(b.audio_offset, audio_offset);
1710 assert_eq!(b.audio_length, file_len - audio_offset);
1712 }
1713 other @ Extent::NeedMore { .. } => panic!("expected Complete (no trim), got {other:?}"),
1714 }
1715 }
1716
1717 #[test]
1723 fn locate_audio_bounded_no_trim_when_no_room_even_with_tag_tail() {
1724 let (mut full, audio_offset) = mp3_with_id3v2(8, b"frames");
1725 full.extend_from_slice(b"TAGxx"); let file_len = full.len() as u64;
1728 assert!(file_len < audio_offset + 128); let mut tail = [0u8; 128];
1732 tail[0..3].copy_from_slice(b"TAG");
1733 let prefix = &full[..usize_from(audio_offset) + 2];
1734 match locate_audio_bounded(prefix, file_len, Some(&tail)).unwrap() {
1735 Extent::Complete(b) => {
1736 assert_eq!(b.audio_offset, audio_offset);
1737 assert_eq!(b.audio_length, file_len - audio_offset); }
1739 other @ Extent::NeedMore { .. } => {
1740 panic!("expected Complete (no room, no trim), got {other:?}")
1741 }
1742 }
1743 }
1744
1745 fn build_v24_tag(frames: &[(&[u8; 4], &[u8])]) -> Vec<u8> {
1751 let total_body: usize = frames.iter().map(|(_, b)| 10 + b.len()).sum();
1752 let mut out = Vec::new();
1753 out.extend_from_slice(b"ID3");
1754 out.extend_from_slice(&[0x04, 0x00, 0x00]); out.extend_from_slice(&ss(u32::try_from(total_body).unwrap()));
1756 for (id, body) in frames {
1757 out.extend_from_slice(*id);
1758 out.extend_from_slice(&ss(u32::try_from(body.len()).unwrap()));
1759 out.extend_from_slice(&[0x00, 0x00]); out.extend_from_slice(body);
1761 }
1762 out
1763 }
1764
1765 fn build_v23_tag(frames: &[(&[u8; 4], &[u8])]) -> Vec<u8> {
1769 let total_body: usize = frames.iter().map(|(_, b)| 10 + b.len()).sum();
1770 let mut out = Vec::new();
1771 out.extend_from_slice(b"ID3");
1772 out.extend_from_slice(&[0x03, 0x00, 0x00]); out.extend_from_slice(&ss(u32::try_from(total_body).unwrap())); for (id, body) in frames {
1775 out.extend_from_slice(*id);
1776 out.extend_from_slice(&(u32::try_from(body.len()).unwrap()).to_be_bytes()); out.extend_from_slice(&[0x00, 0x00]); out.extend_from_slice(body);
1779 }
1780 out
1781 }
1782
1783 #[test]
1790 fn read_binary_tags_v23_plain_u32_frame_size() {
1791 let filler = vec![0xAAu8; 8];
1799 let body: Vec<u8> = (0..200u32)
1800 .map(|i| u8::try_from(i % 250 + 1).unwrap())
1801 .collect();
1802 let tag = build_v23_tag(&[(b"GEOB", &filler), (b"PRIV", &body)]);
1803 let (opaque, _promoted) = super::read_binary_tags(&tag);
1804 let geob = opaque
1805 .iter()
1806 .find(|e| e.key == "GEOB")
1807 .expect("v2.3 GEOB preserved");
1808 assert_eq!(
1809 geob.payload, filler,
1810 "v2.3 first frame must survive byte-exact"
1811 );
1812 let priv_frame = opaque
1813 .iter()
1814 .find(|e| e.key == "PRIV")
1815 .expect("v2.3 PRIV preserved");
1816 assert_eq!(
1817 priv_frame.payload, body,
1818 "v2.3 plain-u32 frame body must survive byte-exact"
1819 );
1820 }
1821
1822 #[test]
1823 fn read_binary_tags_skips_unsafe_tag() {
1824 let mut tag = build_v24_tag(&[(b"PRIV", &[1, 2, 3])]);
1829 tag[5] = 0x80; let (opaque, promoted) = super::read_binary_tags(&tag);
1831 assert!(
1832 opaque.is_empty() && promoted.is_empty(),
1833 "an alloc-unsafe tag must yield no binary frames"
1834 );
1835 }
1836
1837 #[test]
1838 fn read_binary_tags_skips_text_comm_uslt_apic() {
1839 let tag = build_v24_tag(&[
1842 (b"TIT2", &[0x00, b'x']),
1843 (b"COMM", &[0x00]),
1844 (b"USLT", &[0x00]),
1845 (b"APIC", &[0x00]),
1846 (b"PRIV", &[9, 9, 9]),
1847 ]);
1848 let (opaque, _promoted) = super::read_binary_tags(&tag);
1849 let keys: Vec<&str> = opaque.iter().map(|e| e.key.as_str()).collect();
1850 assert_eq!(
1851 keys,
1852 vec!["PRIV"],
1853 "only PRIV is opaque; T***/COMM/USLT/APIC are handled elsewhere: {keys:?}"
1854 );
1855 }
1856
1857 #[test]
1858 fn read_binary_tags_decodes_popm_counter_big_endian_and_zero() {
1859 let tag = build_v24_tag(&[(b"POPM", &[0x00, 200, 0x01, 0x02])]);
1862 let (_opaque, promoted) = super::read_binary_tags(&tag);
1863 assert!(
1864 promoted.contains(&("rating".to_string(), "200".to_string())),
1865 "rating: {promoted:?}"
1866 );
1867 assert!(
1868 promoted.contains(&("playcount".to_string(), "258".to_string())),
1869 "counter must decode big-endian: {promoted:?}"
1870 );
1871
1872 let tag0 = build_v24_tag(&[(b"POPM", &[0x00, 128, 0x00])]);
1874 let (_o0, promoted0) = super::read_binary_tags(&tag0);
1875 assert!(
1876 promoted0.contains(&("rating".to_string(), "128".to_string())),
1877 "rating: {promoted0:?}"
1878 );
1879 assert!(
1880 !promoted0.iter().any(|(k, _)| k == "playcount"),
1881 "a zero POPM counter must not promote playcount: {promoted0:?}"
1882 );
1883 }
1884
1885 #[test]
1886 fn popm_frame_data_emits_counter_only_when_positive() {
1887 assert_eq!(
1889 super::popm_frame_data(200, 0),
1890 vec![0x00, 200],
1891 "playcount 0 must omit the counter"
1892 );
1893 assert_eq!(
1895 super::popm_frame_data(200, 5),
1896 vec![0x00, 200, 0x00, 0x00, 0x00, 0x05],
1897 "playcount > 0 must append a 4-byte counter"
1898 );
1899 }
1900
1901 #[test]
1902 fn build_id3v2_segments_accounts_playcount_and_opaque_len() {
1903 use crate::{BinaryTagInput, TagInput};
1904
1905 let tags = vec![
1908 TagInput::new("rating", "100"),
1909 TagInput::new("playcount", "42"),
1910 ];
1911 let (segments, _len) = build_id3v2_segments(&tags, &[], &[]).unwrap();
1912 let inline: Vec<u8> = segments
1913 .iter()
1914 .flat_map(|s| match s {
1915 Segment::Inline(b) => b.clone(),
1916 _ => Vec::new(),
1917 })
1918 .collect();
1919 let (_opaque, promoted) = super::read_binary_tags(&inline);
1920 assert!(
1921 promoted.contains(&("playcount".to_string(), "42".to_string())),
1922 "playcount must rebuild into the POPM counter: {promoted:?}"
1923 );
1924
1925 let bin = vec![BinaryTagInput {
1928 key: "PRIV".into(),
1929 payload_id: 1,
1930 len: BlobLen::new(7).unwrap(),
1931 }];
1932 let (_segs, total) = build_id3v2_segments(&[], &bin, &[]).unwrap();
1933 assert_eq!(total, 10 + 10 + 7, "opaque binary frame length accounting");
1934 }
1935
1936 #[test]
1937 fn read_binary_tags_promotes_popm_and_mbid_and_passes_through_priv() {
1938 use id3::frame::{Content, Popularimeter, UniqueFileIdentifier, Unknown};
1939 use id3::{Encoder, Frame, Tag, TagLike, Version};
1940
1941 let mut tag = Tag::new();
1942 tag.add_frame(Popularimeter {
1943 user: "a@b.c".into(),
1944 rating: 200,
1945 counter: 7,
1946 });
1947 tag.add_frame(UniqueFileIdentifier {
1948 owner_identifier: "http://musicbrainz.org".into(),
1949 identifier: b"mbid-123".to_vec(),
1950 });
1951 tag.add_frame(UniqueFileIdentifier {
1952 owner_identifier: "http://other.example".into(),
1953 identifier: b"other".to_vec(),
1954 });
1955 tag.add_frame(Frame::with_content(
1956 "PRIV",
1957 Content::Unknown(Unknown {
1958 data: vec![9, 8, 7],
1959 version: Version::Id3v24,
1960 }),
1961 ));
1962 let mut buf = Vec::new();
1963 Encoder::new()
1964 .version(Version::Id3v24)
1965 .encode(&tag, &mut buf)
1966 .unwrap();
1967
1968 let (opaque, promoted) = super::read_binary_tags(&buf);
1969 assert!(promoted.contains(&("rating".to_string(), "200".to_string())));
1970 assert!(promoted.contains(&("playcount".to_string(), "7".to_string())));
1971 assert!(promoted.contains(&("musicbrainz_trackid".to_string(), "mbid-123".to_string())));
1972 let keys: Vec<&str> = opaque.iter().map(|e| e.key.as_str()).collect();
1973 assert!(keys.contains(&"PRIV"));
1974 assert_eq!(keys.iter().filter(|k| **k == "UFID").count(), 1);
1976 assert_eq!(
1977 opaque.iter().find(|e| e.key == "PRIV").unwrap().payload,
1978 vec![9, 8, 7]
1979 );
1980 }
1981
1982 #[test]
1983 fn read_binary_tags_preserves_geob_body_byte_exact() {
1984 let geob_body: Vec<u8> = {
1989 let mut b = vec![0x00]; b.extend_from_slice(b"application/octet-stream\0"); b.extend_from_slice(b"Serato Overview\0"); b.extend_from_slice(b"\0"); b.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]); b
1995 };
1996 let tag = build_v24_tag(&[(b"GEOB", &geob_body)]);
1997
1998 let (opaque, _promoted) = super::read_binary_tags(&tag);
1999 let geob = opaque
2000 .iter()
2001 .find(|e| e.key == "GEOB")
2002 .expect("GEOB preserved");
2003 assert_eq!(
2004 geob.payload, geob_body,
2005 "GEOB body must survive byte-identical"
2006 );
2007 }
2008
2009 #[test]
2010 fn build_id3v2_segments_rebuilds_popm_ufid_and_streams_opaque() {
2011 use crate::BinaryTagInput;
2012 let tags = vec![
2013 TagInput::new("artist", "A"),
2014 TagInput::new("rating", "200"),
2015 TagInput::new("playcount", "7"),
2016 TagInput::new("musicbrainz_trackid", "mbid-123"),
2017 ];
2018 let bin = vec![BinaryTagInput {
2019 key: "PRIV".into(),
2020 payload_id: 42,
2021 len: BlobLen::new(3).unwrap(),
2022 }];
2023 let (segments, _len) = super::build_id3v2_segments(&tags, &bin, &[]).unwrap();
2024
2025 assert!(
2026 segments.iter().any(|s| matches!(
2027 s,
2028 Segment::BinaryTag {
2029 payload_id: 42,
2030 len,
2031 ..
2032 } if len.get() == 3
2033 )),
2034 "opaque PRIV must stream as Segment::BinaryTag"
2035 );
2036
2037 let inline: Vec<u8> = segments
2038 .iter()
2039 .flat_map(|s| match s {
2040 Segment::Inline(b) => b.clone(),
2041 _ => Vec::new(),
2042 })
2043 .collect();
2044 assert!(find_sub(&inline, b"POPM"), "POPM not rebuilt");
2045 assert!(find_sub(&inline, b"UFID"), "UFID not rebuilt");
2046 assert!(
2047 find_sub(&inline, b"http://musicbrainz.org"),
2048 "UFID owner missing"
2049 );
2050 assert!(!find_sub(&inline, b"rating"), "promoted key leaked as TXXX");
2051 assert!(
2052 !find_sub(&inline, b"musicbrainz_trackid"),
2053 "promoted key leaked as TXXX"
2054 );
2055 }
2056
2057 #[test]
2058 fn build_id3v2_segments_first_promoted_scalar_wins() {
2059 let tags = vec![
2063 TagInput::new("rating", "10"),
2064 TagInput::new("rating", "20"),
2065 TagInput::new("musicbrainz_trackid", "mbid-first"),
2066 TagInput::new("musicbrainz_trackid", "mbid-second"),
2067 ];
2068 let (segments, _len) = build_id3v2_segments(&tags, &[], &[]).unwrap();
2069 let inline: Vec<u8> = segments
2070 .iter()
2071 .flat_map(|s| match s {
2072 Segment::Inline(b) => b.clone(),
2073 _ => Vec::new(),
2074 })
2075 .collect();
2076
2077 assert!(find_sub(&inline, b"mbid-first"), "first mbid must win");
2079 assert!(
2080 !find_sub(&inline, b"mbid-second"),
2081 "later mbid must be dropped"
2082 );
2083
2084 let (_opaque, promoted) = super::read_binary_tags(&inline);
2086 assert!(
2087 promoted.contains(&("rating".to_string(), "10".to_string())),
2088 "first rating must win: {promoted:?}"
2089 );
2090 assert!(
2091 !promoted.iter().any(|(k, v)| k == "rating" && v == "20"),
2092 "later rating must be dropped: {promoted:?}"
2093 );
2094 }
2095
2096 #[test]
2097 fn build_id3v2_segments_checked_art_len_rejects_overflow() {
2098 let mk = |data_len: u64| ArtInput {
2101 art_id: 1,
2102 mime: "image/png".to_string(),
2103 description: String::new(),
2104 picture_type: PictureType::new(3).unwrap(),
2105 width: 0,
2106 height: 0,
2107 data_len: BlobLen::new(data_len).unwrap(),
2108 };
2109 assert_eq!(
2110 build_id3v2_segments(&[], &[], &[mk(u64::MAX)]).err(),
2111 Some(FormatError::TooLarge)
2112 );
2113 }
2114
2115 fn find_sub(hay: &[u8], needle: &[u8]) -> bool {
2116 hay.windows(needle.len()).any(|w| w == needle)
2117 }
2118
2119 fn assert_mp3_bounded_matches_full(data: &[u8]) {
2124 let len = data.len() as u64;
2125 let tail: Option<&[u8; 128]> = if data.len() >= 128 {
2126 data[data.len() - 128..].try_into().ok()
2127 } else {
2128 None
2129 };
2130 match (locate_audio(data), locate_audio_bounded(data, len, tail)) {
2131 (Ok(full), Ok(Extent::Complete(bounded))) => assert_eq!(full, bounded),
2132 (Err(_), Err(_)) => {}
2133 (full, bounded) => {
2134 panic!("mp3 bounded/full divergence: full={full:?} bounded={bounded:?}")
2135 }
2136 }
2137 }
2138
2139 #[test]
2140 fn locate_audio_rejects_high_bit_size_byte() {
2141 let mut data = Vec::new();
2144 data.extend_from_slice(b"ID3");
2145 data.extend_from_slice(&[0x04, 0x00, 0x00]); data.extend_from_slice(&[0x00, 0x00, 0x00, 0x80]); data.extend_from_slice(&[0xFF, 0xFB, 0x90, 0x00]); assert_eq!(locate_audio(&data), Err(FormatError::Malformed));
2149 }
2150
2151 #[test]
2152 fn locate_audio_rejects_unsupported_major_version() {
2153 let mut data = Vec::new();
2154 data.extend_from_slice(b"ID3");
2155 data.extend_from_slice(&[0x05, 0x00, 0x00]); data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
2157 data.extend_from_slice(&[0xFF, 0xFB, 0x90, 0x00]);
2158 assert_eq!(locate_audio(&data), Err(FormatError::Malformed));
2159 }
2160
2161 #[test]
2162 fn locate_audio_bounded_rejects_high_bit_size_byte() {
2163 let mut data = Vec::new();
2164 data.extend_from_slice(b"ID3");
2165 data.extend_from_slice(&[0x04, 0x00, 0x00]);
2166 data.extend_from_slice(&[0x00, 0x00, 0x00, 0x80]);
2167 data.extend_from_slice(&[0xFF, 0xFB, 0x90, 0x00]);
2168 let file_len = data.len() as u64;
2169 assert_eq!(
2170 locate_audio_bounded(&data, file_len, None),
2171 Err(FormatError::Malformed)
2172 );
2173 }
2174
2175 #[test]
2176 fn mp3_bounded_matches_full_on_whole_buffer() {
2177 assert_mp3_bounded_matches_full(&crate::fuzz_check::fixtures::mp3());
2179 assert_mp3_bounded_matches_full(&crate::fuzz_check::fixtures::mp3_with_binary_frame());
2181
2182 let mut with_trailer = crate::fuzz_check::fixtures::mp3();
2185 with_trailer.resize(200, 0x00);
2186 with_trailer.extend_from_slice(b"TAG");
2187 with_trailer.resize(with_trailer.len() + 125, 0x00); assert_mp3_bounded_matches_full(&with_trailer);
2189 }
2190}