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 decode_frame_size(major_version: u8, raw: &[u8]) -> u32 {
34 if major_version == 3 {
35 u32::from_be_bytes([raw[0], raw[1], raw[2], raw[3]])
36 } else {
37 synchsafe_decode(raw)
38 }
39}
40
41fn id3v2_header_len(data: &[u8]) -> Result<Option<usize>> {
42 if data.len() < 10 || &data[0..3] != b"ID3" {
43 return Ok(None);
44 }
45 if !matches!(data[3], 2..=4) {
46 return Err(FormatError::Malformed);
47 }
48 if data[6..10].iter().any(|&b| b & 0x80 != 0) {
51 return Err(FormatError::Malformed);
52 }
53 Ok(Some(10 + synchsafe_decode(&data[6..10]) as usize))
54}
55
56pub fn locate_audio(data: &[u8]) -> Result<Mp3Bounds> {
61 let len = data.len();
62
63 let mut audio_offset = 0usize;
64 if let Some(base) = id3v2_header_len(data)? {
65 let flags = data[5];
66 let mut tag_len = base;
67 if flags & 0x10 != 0 {
68 tag_len += 10; }
70 if tag_len > len {
71 return Err(FormatError::Malformed);
72 }
73 audio_offset = tag_len;
74 }
75
76 let mut audio_end = len;
77 if audio_end >= audio_offset + 128 && &data[audio_end - 128..audio_end - 125] == b"TAG" {
78 audio_end -= 128; }
80
81 if audio_offset + 1 >= len
83 || data[audio_offset] != 0xFF
84 || (data[audio_offset + 1] & 0xE0) != 0xE0
85 {
86 return Err(FormatError::NotMp3);
87 }
88
89 Ok(Mp3Bounds {
90 audio_offset: audio_offset as u64,
91 audio_length: (audio_end - audio_offset) as u64,
92 })
93}
94
95pub fn locate_audio_bounded(
102 prefix: &[u8],
103 file_len: u64,
104 tail: Option<&[u8; 128]>,
105) -> Result<Extent<Mp3Bounds>> {
106 let mut audio_offset = 0usize;
107 if prefix.len() < 10 && file_len >= 10 {
108 return Ok(Extent::NeedMore { up_to: 10 });
110 }
111 if let Some(base) = id3v2_header_len(prefix)? {
112 let flags = prefix[5];
113 let mut tag_len = base;
114 if flags & 0x10 != 0 {
115 tag_len += 10; }
117 if tag_len as u64 > file_len {
118 return Err(FormatError::Malformed);
119 }
120 audio_offset = tag_len;
121 }
122
123 if audio_offset as u64 + 2 > file_len {
129 return Err(FormatError::NotMp3);
130 }
131
132 if audio_offset + 2 > prefix.len() {
134 return Ok(Extent::NeedMore {
135 up_to: (audio_offset + 2) as u64,
136 });
137 }
138
139 if prefix[audio_offset] != 0xFF || (prefix[audio_offset + 1] & 0xE0) != 0xE0 {
140 return Err(FormatError::NotMp3);
141 }
142
143 let mut audio_end = file_len;
144 if let Some(tail) = tail
145 && file_len >= audio_offset as u64 + 128
146 && &tail[0..3] == b"TAG"
147 {
148 audio_end -= 128;
149 }
150
151 Ok(Extent::Complete(Mp3Bounds {
152 audio_offset: audio_offset as u64,
153 audio_length: audio_end - audio_offset as u64,
154 }))
155}
156
157const ENC_UTF8: u8 = 0x03;
158
159fn syncsafe(n: u32) -> [u8; 4] {
160 [
161 ((n >> 21) & 0x7F) as u8,
162 ((n >> 14) & 0x7F) as u8,
163 ((n >> 7) & 0x7F) as u8,
164 (n & 0x7F) as u8,
165 ]
166}
167
168const SYNCHSAFE_MAX: u32 = 0x0FFF_FFFF;
170
171fn push_frame_header(out: &mut Vec<u8>, id: &[u8; 4], data_len: usize) -> Result<()> {
172 let data_len_u32 = u32::try_from(data_len)
175 .ok()
176 .filter(|&v| v <= SYNCHSAFE_MAX)
177 .ok_or(FormatError::TooLarge)?;
178 out.extend_from_slice(id);
179 out.extend_from_slice(&syncsafe(data_len_u32));
180 out.extend_from_slice(&[0x00, 0x00]); Ok(())
182}
183
184fn text_frame_data(values: &[String]) -> Vec<u8> {
185 let mut d = vec![ENC_UTF8];
186 d.extend_from_slice(values.join("\0").as_bytes());
187 d
188}
189
190fn txxx_frame_data(desc: &str, value: &str) -> Vec<u8> {
191 let mut d = vec![ENC_UTF8];
192 d.extend_from_slice(desc.as_bytes());
193 d.push(0x00);
194 d.extend_from_slice(value.as_bytes());
195 d
196}
197
198fn comm_like_frame_data(lang: &str, description: &str, value: &str) -> Vec<u8> {
204 let l = lang.as_bytes();
205 let mut d = vec![ENC_UTF8];
206 d.extend_from_slice(&[
207 *l.first().unwrap_or(&b'X'),
208 *l.get(1).unwrap_or(&b'X'),
209 *l.get(2).unwrap_or(&b'X'),
210 ]);
211 d.extend_from_slice(description.as_bytes());
212 d.push(0x00); d.extend_from_slice(value.as_bytes());
214 d
215}
216
217fn is_placeholder_lang(lang: &str) -> bool {
220 matches!(lang.to_ascii_lowercase().as_str(), "" | "xxx" | "und")
221}
222
223fn comm_like_key(frame: &str, lang: &str, description: &str, default_key: &str) -> String {
229 if description.is_empty() && is_placeholder_lang(lang) {
230 default_key.to_string()
231 } else {
232 format!("id3:{frame}:{lang}:{description}")
233 }
234}
235
236fn parse_comm_like_key(key: &str) -> Option<(&'static [u8; 4], &str, &str)> {
240 let rest = key.strip_prefix("id3:")?;
241 let (frame, langdesc) = rest.split_once(':')?;
242 let frame_id: &'static [u8; 4] = match frame {
243 "COMM" => b"COMM",
244 "USLT" => b"USLT",
245 _ => return None,
246 };
247 let (lang, desc) = langdesc.split_once(':')?;
248 Some((frame_id, lang, desc))
249}
250
251fn is_id3_text_frame_id(key: &str) -> bool {
254 key.len() == 4
255 && key != "TXXX"
256 && key.starts_with('T')
257 && key
258 .bytes()
259 .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit())
260}
261
262fn reject_embedded_nul(field: &'static str, s: &str) -> Result<()> {
269 if s.as_bytes().contains(&0) {
270 return Err(FormatError::EmbeddedNul { field });
271 }
272 Ok(())
273}
274
275fn apic_framing(art: &ArtInput) -> Vec<u8> {
278 let mut d = vec![ENC_UTF8];
279 d.extend_from_slice(art.mime.as_bytes());
280 d.push(0x00);
281 #[expect(
282 clippy::cast_possible_truncation,
283 reason = "ID3 APIC type is one byte; valid picture types are 0..=20"
284 )]
285 d.push(art.picture_type.get() as u8);
286 d.extend_from_slice(art.description.as_bytes());
287 d.push(0x00);
288 d
289}
290
291fn popm_frame_data(rating: u8, playcount: u64) -> Vec<u8> {
297 let mut d = Vec::new();
298 d.push(0x00); d.push(rating);
300 if playcount > 0 {
301 let c = u32::try_from(playcount).unwrap_or(u32::MAX);
302 d.extend_from_slice(&c.to_be_bytes());
303 }
304 d
305}
306
307fn ufid_frame_data(owner: &str, identifier: &[u8]) -> Vec<u8> {
309 let mut d = Vec::new();
310 d.extend_from_slice(owner.as_bytes());
311 d.push(0x00);
312 d.extend_from_slice(identifier);
313 d
314}
315
316fn is_promoted_key(key: &str) -> bool {
319 matches!(key, "rating" | "playcount" | "musicbrainz_trackid")
320}
321
322pub fn build_id3v2_segments(
328 tags: &[TagInput],
329 binary_tags: &[BinaryTagInput],
330 arts: &[ArtInput],
331) -> Result<(Vec<Segment>, u64)> {
332 for t in tags {
338 reject_embedded_nul("tag key", &t.key)?;
339 reject_embedded_nul("tag value", &t.value)?;
340 }
341 for art in arts {
342 reject_embedded_nul("art mime", &art.mime)?;
343 reject_embedded_nul("art description", &art.description)?;
344 }
345
346 let mut popm_rating: Option<u8> = None;
351 let mut popm_playcount: u64 = 0;
352 let mut mbid: Option<String> = None;
353 for t in tags {
354 match t.key.as_str() {
355 "rating" if popm_rating.is_none() => popm_rating = t.value.parse().ok(),
356 "playcount" => popm_playcount = t.value.parse().unwrap_or(popm_playcount),
357 "musicbrainz_trackid" if mbid.is_none() => mbid = Some(t.value.clone()),
358 _ => {}
359 }
360 }
361
362 let mut groups: Vec<(String, Vec<String>)> = Vec::new();
366 for t in tags {
367 if is_promoted_key(&t.key) {
368 continue;
369 }
370 match groups.last_mut() {
371 Some(g) if g.0 == t.key => g.1.push(t.value.clone()),
372 _ => groups.push((t.key.clone(), vec![t.value.clone()])),
373 }
374 }
375
376 let mut segments: Vec<Segment> = Vec::new();
377 let mut buf: Vec<u8> = Vec::new();
378 let mut frames_len: u64 = 0;
379
380 for (key, values) in &groups {
381 match crate::tagmap::key_to_id3(key) {
382 Some(crate::tagmap::Id3Slot::Text(id)) => {
383 let data = text_frame_data(values);
384 push_frame_header(&mut buf, id, data.len())?;
385 buf.extend_from_slice(&data);
386 frames_len = size::checked_add(frames_len, 10 + data.len() as u64)?;
387 }
388 Some(crate::tagmap::Id3Slot::Txxx(desc)) => {
389 for value in values {
390 let data = txxx_frame_data(desc, value);
391 push_frame_header(&mut buf, b"TXXX", data.len())?;
392 buf.extend_from_slice(&data);
393 frames_len = size::checked_add(frames_len, 10 + data.len() as u64)?;
394 }
395 }
396 Some(crate::tagmap::Id3Slot::Comment) => {
397 for value in values {
398 let data = comm_like_frame_data("XXX", "", value);
399 push_frame_header(&mut buf, b"COMM", data.len())?;
400 buf.extend_from_slice(&data);
401 frames_len = size::checked_add(frames_len, 10 + data.len() as u64)?;
402 }
403 }
404 Some(crate::tagmap::Id3Slot::Lyrics) => {
405 for value in values {
406 let data = comm_like_frame_data("XXX", "", value);
407 push_frame_header(&mut buf, b"USLT", data.len())?;
408 buf.extend_from_slice(&data);
409 frames_len = size::checked_add(frames_len, 10 + data.len() as u64)?;
410 }
411 }
412 None if is_id3_text_frame_id(key) => {
413 let id: [u8; 4] = key.as_bytes().try_into().unwrap();
415 let data = text_frame_data(values);
416 push_frame_header(&mut buf, &id, data.len())?;
417 buf.extend_from_slice(&data);
418 frames_len = size::checked_add(frames_len, 10 + data.len() as u64)?;
419 }
420 None => {
421 if let Some((frame_id, lang, desc)) = parse_comm_like_key(key) {
422 for value in values {
423 let data = comm_like_frame_data(lang, desc, value);
424 push_frame_header(&mut buf, frame_id, data.len())?;
425 buf.extend_from_slice(&data);
426 frames_len = size::checked_add(frames_len, 10 + data.len() as u64)?;
427 }
428 } else {
429 for value in values {
430 let data = txxx_frame_data(key, value);
431 push_frame_header(&mut buf, b"TXXX", data.len())?;
432 buf.extend_from_slice(&data);
433 frames_len = size::checked_add(frames_len, 10 + data.len() as u64)?;
434 }
435 }
436 }
437 }
438 }
439
440 if let Some(rating) = popm_rating {
442 let data = popm_frame_data(rating, popm_playcount);
443 push_frame_header(&mut buf, b"POPM", data.len())?;
444 buf.extend_from_slice(&data);
445 frames_len = size::checked_add(frames_len, 10 + data.len() as u64)?;
446 }
447 if let Some(id) = &mbid {
448 let data = ufid_frame_data(MUSICBRAINZ_UFID_OWNER, id.as_bytes());
449 push_frame_header(&mut buf, b"UFID", data.len())?;
450 buf.extend_from_slice(&data);
451 frames_len = size::checked_add(frames_len, 10 + data.len() as u64)?;
452 }
453
454 for bt in binary_tags {
456 let Ok(id): std::result::Result<[u8; 4], _> = bt.key.as_bytes().try_into() else {
458 continue;
459 };
460 push_frame_header(&mut buf, &id, usize_from(bt.len.get()))?;
461 segments.push(Segment::Inline(std::mem::take(&mut buf)));
462 segments.push(Segment::BinaryTag {
463 payload_id: bt.payload_id,
464 len: bt.len,
465 });
466 frames_len = size::checked_add(frames_len, size::checked_add(10, bt.len.get())?)?;
467 }
468
469 for art in arts {
470 let framing = apic_framing(art);
471 let data_len = size::checked_add(framing.len() as u64, art.data_len.get())?;
472 push_frame_header(&mut buf, b"APIC", usize_from(data_len))?;
473 buf.extend_from_slice(&framing);
474 segments.push(Segment::Inline(std::mem::take(&mut buf)));
475 segments.push(Segment::ArtImage {
476 art_id: art.art_id,
477 len: art.data_len,
478 });
479 frames_len = size::checked_add(frames_len, size::checked_add(10, data_len)?)?;
480 }
481
482 if !buf.is_empty() {
483 segments.push(Segment::Inline(std::mem::take(&mut buf)));
484 }
485
486 let mut header = Vec::with_capacity(10);
488 header.extend_from_slice(b"ID3");
489 header.extend_from_slice(&[0x04, 0x00]); header.push(0x00); let frames_len_ss = u32::try_from(frames_len)
496 .ok()
497 .filter(|&v| v <= SYNCHSAFE_MAX)
498 .ok_or(FormatError::TooLarge)?;
499 header.extend_from_slice(&syncsafe(frames_len_ss));
500 segments.insert(0, Segment::Inline(header));
501
502 Ok((segments, size::checked_add(10, frames_len)?))
503}
504
505pub fn synthesize_layout(
509 audio_offset: u64,
510 audio_length: u64,
511 tags: &[TagInput],
512 binary_tags: &[BinaryTagInput],
513 arts: &[ArtInput],
514) -> Result<RegionLayout> {
515 let (mut segments, _tag_len) = build_id3v2_segments(tags, binary_tags, arts)?;
516 segments.push(Segment::BackingAudio {
517 offset: audio_offset,
518 len: audio_length,
519 });
520 Ok(RegionLayout::validated(segments)?)
521}
522
523fn id3v2_alloc_safe(data: &[u8]) -> bool {
533 let Ok(Some(tag_end)) = id3v2_header_len(data) else {
541 return false;
543 };
544 let flags = data[5];
545 if flags & 0xC0 != 0 {
548 return false;
549 }
550 if tag_end > data.len() {
551 return false;
552 }
553 let major = data[3];
554 let header_len = if major == 2 { 6 } else { 10 };
555 let scan_end = data.len();
561 let mut pos = 10usize;
562 while pos + header_len <= scan_end {
563 if data[pos] == 0 {
565 break;
566 }
567 if major != 2 && (&data[pos..pos + 4] == b"CHAP" || &data[pos..pos + 4] == b"CTOC") {
572 return false;
573 }
574 let size = if major == 2 {
575 u32::from_be_bytes([0, data[pos + 3], data[pos + 4], data[pos + 5]]) as usize
576 } else if major == 3 {
577 if data[pos + 8] != 0 || data[pos + 9] != 0 {
584 return false;
585 }
586 u32::from_be_bytes([data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]])
587 as usize
588 } else {
589 if data[pos + 4] | data[pos + 5] | data[pos + 6] | data[pos + 7] >= 0x80 {
593 return false;
594 }
595 if data[pos + 8] != 0 || data[pos + 9] != 0 {
596 return false;
597 }
598 synchsafe_decode(&data[pos + 4..pos + 8]) as usize
599 };
600 let data_start = pos + header_len;
601 if data_start > tag_end || size > tag_end - data_start {
606 return false;
607 }
608 pos = data_start + size;
609 if pos >= tag_end {
612 break;
613 }
614 }
615 true
616}
617
618pub fn read_pictures(data: &[u8]) -> Vec<EmbeddedPicture> {
621 if !id3v2_alloc_safe(data) {
622 return Vec::new();
623 }
624 let Ok(tag) = id3::Tag::read_from2(std::io::Cursor::new(data)) else {
625 return Vec::new();
626 };
627 tag.pictures()
628 .map(|p| EmbeddedPicture {
629 mime: p.mime_type.clone(),
630 picture_type: PictureType::new(u8::from(p.picture_type).into())
634 .unwrap_or(PictureType::ZERO),
635 description: p.description.clone(),
636 width: 0,
637 height: 0,
638 data: p.data.clone(),
639 })
640 .collect()
641}
642
643pub fn read_tags(data: &[u8]) -> Vec<(String, String)> {
651 if !id3v2_alloc_safe(data) {
652 return Vec::new();
653 }
654 let Ok(tag) = id3::Tag::read_from2(std::io::Cursor::new(data)) else {
655 return Vec::new();
656 };
657 let mut out = Vec::new();
658 for frame in tag.frames() {
659 let content = frame.content();
660 if let Some(et) = content.extended_text() {
661 let key = crate::tagmap::id3_txxx_to_key(&et.description)
662 .map_or_else(|| et.description.clone(), str::to_string);
663 out.push((key, et.value.clone()));
664 } else if let Some(c) = content.comment() {
665 out.push((
666 comm_like_key("COMM", &c.lang, &c.description, "comment"),
667 c.text.clone(),
668 ));
669 } else if let Some(l) = content.lyrics() {
670 out.push((
671 comm_like_key("USLT", &l.lang, &l.description, "lyrics"),
672 l.text.clone(),
673 ));
674 } else if let Some(text) = content.text() {
675 let id = frame.id();
676 let key =
677 crate::tagmap::id3_text_to_key(id).map_or_else(|| id.to_string(), str::to_string);
678 for value in text.split('\0').filter(|v| !v.is_empty()) {
679 out.push((key.clone(), value.to_string()));
680 }
681 }
682 }
683 out
684}
685
686pub(crate) const MUSICBRAINZ_UFID_OWNER: &str = "http://musicbrainz.org";
687
688pub fn read_binary_tags(data: &[u8]) -> (Vec<EmbeddedBinaryTag>, Vec<(String, String)>) {
700 let mut opaque = Vec::new();
701 let mut promoted = Vec::new();
702 if !id3v2_alloc_safe(data) || data[3] < 3 {
703 return (opaque, promoted);
704 }
705 let tag_end = 10 + synchsafe_decode(&data[6..10]) as usize;
706 let mut pos = 10usize;
707 while pos + 10 <= tag_end {
708 if data[pos] == 0 {
709 break;
710 }
711 let id = &data[pos..pos + 4];
712 let size = decode_frame_size(data[3], &data[pos + 4..pos + 8]) as usize;
713 let body_start = pos + 10;
714 if body_start + size > tag_end {
715 break;
716 }
717 classify_binary_frame(
718 id,
719 &data[body_start..body_start + size],
720 &mut opaque,
721 &mut promoted,
722 );
723 pos = body_start + size;
724 }
725 (opaque, promoted)
726}
727
728fn classify_binary_frame(
730 id: &[u8],
731 body: &[u8],
732 opaque: &mut Vec<EmbeddedBinaryTag>,
733 promoted: &mut Vec<(String, String)>,
734) {
735 if id[0] == b'T' || id == b"COMM" || id == b"USLT" || id == b"APIC" {
737 return;
738 }
739 match id {
740 b"POPM" => {
741 if let Some(nul) = body.iter().position(|&b| b == 0)
743 && let Some((&rating, counter)) = body[nul + 1..].split_first()
744 {
745 promoted.push(("rating".to_string(), rating.to_string()));
746 let c = counter
747 .iter()
748 .take(8)
749 .fold(0u64, |a, &b| (a << 8) | u64::from(b));
750 if c > 0 {
751 promoted.push(("playcount".to_string(), c.to_string()));
752 }
753 }
754 }
755 b"UFID" => {
756 match body.iter().position(|&b| b == 0) {
758 Some(nul) if &body[..nul] == MUSICBRAINZ_UFID_OWNER.as_bytes() => {
759 promoted.push((
760 "musicbrainz_trackid".to_string(),
761 String::from_utf8_lossy(&body[nul + 1..]).into_owned(),
762 ));
763 }
764 _ => opaque.push(EmbeddedBinaryTag {
765 key: "UFID".to_string(),
766 payload: body.to_vec(),
767 }),
768 }
769 }
770 _ => {
771 if id.iter().all(u8::is_ascii_graphic) {
773 opaque.push(EmbeddedBinaryTag {
774 key: String::from_utf8_lossy(id).into_owned(),
775 payload: body.to_vec(),
776 });
777 }
778 }
779 }
780}
781
782#[cfg(test)]
783mod tests {
784 use super::*;
785 use crate::input::{BlobLen, PictureType};
786
787 #[test]
790 fn id3v2_guard_rejects_oversized_v23_frame() {
791 let mut bytes: Vec<u8> = Vec::new();
796 bytes.extend_from_slice(b"ID3");
797 bytes.push(0x03); bytes.push(0x00); bytes.push(0x00); bytes.extend_from_slice(&[0x00, 0x00, 0x00, 0x0A]);
802 bytes.extend_from_slice(b"TIT2");
804 bytes.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]);
805 bytes.extend_from_slice(&[0x00, 0x00]); assert!(
808 !id3v2_alloc_safe(&bytes),
809 "guard should reject frame claiming more bytes than the tag holds"
810 );
811 assert!(
813 read_tags(&bytes).is_empty(),
814 "read_tags must return empty for unsafe tag"
815 );
816 }
817
818 fn v24_tag_one_frame(frame_size: [u8; 4]) -> Vec<u8> {
824 let mut t = Vec::new();
825 t.extend_from_slice(b"ID3");
826 t.extend_from_slice(&[4, 0, 0]); t.extend_from_slice(&[0, 0, 0, 10]); t.extend_from_slice(b"TIT2"); t.extend_from_slice(&frame_size); t.extend_from_slice(&[0, 0]); t
832 }
833
834 #[test]
835 fn alloc_safe_rejects_v24_frame_with_nonsynchsafe_size() {
836 for size in [
843 [0x80, 0x00, 0x00, 0x00], [0x00, 0x80, 0x00, 0x00], [0x00, 0x00, 0x80, 0x00], [0x00, 0x80, 0x80, 0x00], [0x00, 0x00, 0x80, 0x80], ] {
849 assert!(
850 !id3v2_alloc_safe(&v24_tag_one_frame(size)),
851 "v2.4 frame size {size:02x?} has a high bit set and must be rejected"
852 );
853 }
854 }
855
856 #[test]
860 fn id3v2_guard_rejects_non_id3_prefixed() {
861 assert!(
863 !id3v2_alloc_safe(b"RIFF....just not an id3 tag...."),
864 "guard must reject buffer not starting with ID3"
865 );
866 assert!(
867 read_tags(b"RIFF....just not an id3 tag....").is_empty(),
868 "read_tags must return empty for non-ID3-prefixed buffer"
869 );
870
871 const RIFF_BODY: &[u8] = &[
876 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,
879 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,
883 0x00, ];
885 assert!(
886 !id3v2_alloc_safe(RIFF_BODY),
887 "guard must reject RIFF-prefixed buffer (WAV crash vector)"
888 );
889 assert!(
890 read_tags(RIFF_BODY).is_empty(),
891 "read_tags must return empty for RIFF-prefixed buffer"
892 );
893 }
894
895 #[test]
898 fn id3v2_guard_allows_valid_tag() {
899 use id3::{Tag, TagLike, Version};
900
901 let mut tag = Tag::new();
902 tag.set_text("TIT2", "Hello");
903 tag.set_text("TPE1", "Artist");
904 let mut buf = Vec::new();
905 tag.write_to(&mut buf, Version::Id3v24).unwrap();
906
907 assert!(
908 id3v2_alloc_safe(&buf),
909 "guard should allow a well-formed tag written by the id3 crate"
910 );
911 let tags = read_tags(&buf);
912 assert!(
913 tags.contains(&("title".to_string(), "Hello".to_string())),
914 "missing title in {tags:?}"
915 );
916 assert!(
917 tags.contains(&("artist".to_string(), "Artist".to_string())),
918 "missing artist in {tags:?}"
919 );
920 }
921
922 #[test]
925 fn read_tags_handles_oom_crash_input_safely() {
926 const CRASH1: &[u8] = &[
930 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,
937 ];
938 const CRASH2: &[u8] = &[
944 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,
950 ];
951 for (i, crash) in [CRASH1, CRASH2].iter().enumerate() {
952 assert!(
953 read_tags(crash).is_empty(),
954 "read_tags must be safe on crash artifact {i}"
955 );
956 }
957 }
958
959 #[test]
960 fn read_tags_captures_txxx_comm_uslt_and_unmapped_text() {
961 use id3::frame::{Comment, ExtendedText, Lyrics};
962 use id3::{Tag, TagLike, Version}; let mut tag = Tag::new();
965 tag.set_text("TIT2", "Song");
966 tag.set_text("TKEY", "120"); tag.add_frame(ExtendedText {
968 description: "MOOD".into(),
969 value: "happy".into(),
970 });
971 tag.add_frame(ExtendedText {
972 description: "REPLAYGAIN_TRACK_GAIN".into(),
973 value: "-6.5 dB".into(),
974 });
975 tag.add_frame(Comment {
976 lang: "XXX".into(),
977 description: String::new(),
978 text: "nice".into(),
979 });
980 tag.add_frame(Lyrics {
981 lang: "XXX".into(),
982 description: String::new(),
983 text: "la la".into(),
984 });
985
986 let mut buf = Vec::new();
987 tag.write_to(&mut buf, Version::Id3v24).unwrap();
988
989 let tags = read_tags(&buf);
990 assert!(tags.contains(&("title".to_string(), "Song".to_string())));
991 assert!(tags.contains(&("TKEY".to_string(), "120".to_string())));
992 assert!(tags.contains(&("MOOD".to_string(), "happy".to_string())));
993 assert!(tags.contains(&("replaygain_track_gain".to_string(), "-6.5 dB".to_string())));
994 assert!(tags.contains(&("comment".to_string(), "nice".to_string())));
995 assert!(tags.contains(&("lyrics".to_string(), "la la".to_string())));
996 }
997
998 #[test]
999 fn synthesize_round_trips_arbitrary_id3_tags() {
1000 let tags = vec![
1001 TagInput::new("title", "Song"),
1002 TagInput::new("TKEY", "120"), TagInput::new("MyRating", "5"), TagInput::new("comment", "nice"), TagInput::new("lyrics", "la la"), TagInput::new("replaygain_track_gain", "-3.21 dB"), ];
1008 let (segments, _len) = build_id3v2_segments(&tags, &[], &[]).unwrap();
1009 let mut buf = Vec::new();
1010 for seg in &segments {
1011 if let Segment::Inline(bytes) = seg {
1012 buf.extend_from_slice(bytes);
1013 }
1014 }
1015 let read = read_tags(&buf);
1016 for expected in [
1017 ("title", "Song"),
1018 ("TKEY", "120"),
1019 ("MyRating", "5"),
1020 ("comment", "nice"),
1021 ("lyrics", "la la"),
1022 ("replaygain_track_gain", "-3.21 dB"),
1023 ] {
1024 assert!(
1025 read.contains(&(expected.0.to_string(), expected.1.to_string())),
1026 "missing {expected:?} in {read:?}"
1027 );
1028 }
1029 }
1030
1031 #[test]
1032 fn read_tags_preserves_comm_uslt_language_and_descriptor() {
1033 use id3::frame::{Comment, Lyrics};
1034 use id3::{Tag, TagLike, Version};
1035
1036 let mut tag = Tag::new();
1037 tag.add_frame(Comment {
1039 lang: "XXX".into(),
1040 description: String::new(),
1041 text: "plain".into(),
1042 });
1043 tag.add_frame(Comment {
1045 lang: "deu".into(),
1046 description: String::new(),
1047 text: "hallo".into(),
1048 });
1049 tag.add_frame(Comment {
1051 lang: "eng".into(),
1052 description: "note".into(),
1053 text: "see liner".into(),
1054 });
1055 tag.add_frame(Lyrics {
1057 lang: "eng".into(),
1058 description: String::new(),
1059 text: "verse".into(),
1060 });
1061 tag.add_frame(Lyrics {
1062 lang: "deu".into(),
1063 description: String::new(),
1064 text: "strophe".into(),
1065 });
1066
1067 let mut buf = Vec::new();
1068 tag.write_to(&mut buf, Version::Id3v24).unwrap();
1069 let tags = read_tags(&buf);
1070
1071 assert!(
1072 tags.contains(&("comment".into(), "plain".into())),
1073 "got {tags:?}"
1074 );
1075 assert!(
1076 tags.contains(&("id3:COMM:deu:".into(), "hallo".into())),
1077 "got {tags:?}"
1078 );
1079 assert!(
1080 tags.contains(&("id3:COMM:eng:note".into(), "see liner".into())),
1081 "got {tags:?}"
1082 );
1083 assert!(
1084 tags.contains(&("id3:USLT:eng:".into(), "verse".into())),
1085 "got {tags:?}"
1086 );
1087 assert!(
1088 tags.contains(&("id3:USLT:deu:".into(), "strophe".into())),
1089 "got {tags:?}"
1090 );
1091 }
1092
1093 #[test]
1094 fn synthesize_round_trips_comm_uslt_language_and_descriptor() {
1095 let tags = vec![
1096 TagInput::new("comment", "plain"),
1097 TagInput::new("id3:COMM:deu:", "hallo"),
1098 TagInput::new("id3:COMM:eng:note", "see liner"),
1099 TagInput::new("id3:USLT:eng:Chorus", "la la"),
1100 ];
1101 let (segments, len) = build_id3v2_segments(&tags, &[], &[]).unwrap();
1102 let mut buf = Vec::new();
1103 for seg in &segments {
1104 if let Segment::Inline(bytes) = seg {
1105 buf.extend_from_slice(bytes);
1106 }
1107 }
1108 assert_eq!(len, buf.len() as u64);
1111 let tag = id3::Tag::read_from2(std::io::Cursor::new(&buf)).unwrap();
1114 assert!(
1115 tag.comments()
1116 .any(|c| c.text == "plain" && c.description.is_empty()),
1117 "plain COMM missing"
1118 );
1119 assert!(
1120 tag.comments()
1121 .any(|c| c.lang == "deu" && c.description.is_empty() && c.text == "hallo"),
1122 "deu COMM missing"
1123 );
1124 assert!(
1125 tag.comments()
1126 .any(|c| c.lang == "eng" && c.description == "note" && c.text == "see liner"),
1127 "descriptor-keyed COMM missing"
1128 );
1129 assert!(
1130 tag.lyrics()
1131 .any(|l| l.lang == "eng" && l.description == "Chorus" && l.text == "la la"),
1132 "USLT missing"
1133 );
1134 }
1135
1136 #[test]
1137 fn synchsafe_decode_assembles_7bit_groups() {
1138 assert_eq!(synchsafe_decode(&[0x01, 0x02, 0x03, 0x04]), 0x0020_8184);
1140 assert_eq!(synchsafe_decode(&[0xFF, 0xFF, 0xFF, 0xFF]), 0x0FFF_FFFF);
1142 assert_eq!(synchsafe_decode(&[0x7F, 0x00, 0x00, 0x00]), 0x0FE0_0000);
1144 assert_eq!(synchsafe_decode(&[0x00, 0x7F, 0x00, 0x00]), 0x001F_C000);
1146 }
1147
1148 #[test]
1149 fn syncsafe_encodes_and_round_trips() {
1150 assert_eq!(syncsafe(0x0FE0_0000), [0x7F, 0x00, 0x00, 0x00]);
1152 assert_eq!(syncsafe(0x001F_C000), [0x00, 0x7F, 0x00, 0x00]);
1153 for n in [0u32, 1, 127, 128, 0x0123_4567, 0x0FFF_FFFF] {
1155 assert_eq!(synchsafe_decode(&syncsafe(n)), n);
1156 }
1157 }
1158
1159 #[test]
1160 fn locate_audio_no_id3_starts_at_zero() {
1161 let data = [0xFF, 0xFB, 0x90, 0x00, 0, 0, 0, 0, 0, 0];
1165 let b = locate_audio(&data).unwrap();
1166 assert_eq!(b.audio_offset, 0);
1167 assert_eq!(b.audio_length, 10);
1168 }
1169
1170 #[test]
1171 fn locate_audio_skips_id3v2_then_finds_sync() {
1172 let mut data = Vec::new();
1174 data.extend_from_slice(b"ID3");
1175 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();
1180 assert_eq!(b.audio_offset, 14);
1181 assert_eq!(b.audio_length, 4);
1182 }
1183
1184 #[test]
1185 fn locate_audio_honors_footer_flag() {
1186 let mut data = Vec::new();
1190 data.extend_from_slice(b"ID3");
1191 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();
1196 assert_eq!(b.audio_offset, 20);
1197 }
1198
1199 #[test]
1200 fn locate_audio_requires_frame_sync() {
1201 let data = [0xFF, 0x00, 0x00, 0x00, 0, 0, 0, 0, 0, 0];
1206 assert_eq!(locate_audio(&data), Err(FormatError::NotMp3));
1207 assert_eq!(locate_audio(&[0xFF]), Err(FormatError::NotMp3));
1210 }
1211
1212 #[test]
1213 fn push_frame_header_size_boundary_is_inclusive() {
1214 let mut out = Vec::new();
1217 assert!(push_frame_header(&mut out, b"TIT2", 0x0FFF_FFFF).is_ok());
1218 let mut over = Vec::new();
1219 assert_eq!(
1220 push_frame_header(&mut over, b"TIT2", 0x1000_0000),
1221 Err(FormatError::TooLarge)
1222 );
1223 }
1224
1225 #[test]
1226 fn is_id3_text_frame_id_classifies_text_frames() {
1227 assert!(is_id3_text_frame_id("TPE1")); assert!(is_id3_text_frame_id("TIT2"));
1229 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")); }
1234
1235 #[test]
1236 fn build_id3v2_segments_emits_standard_text_frame_as_itself() {
1237 let tags = vec![TagInput::new("TPE1", "Band")];
1241 let (segments, _len) = build_id3v2_segments(&tags, &[], &[]).unwrap();
1242 let mut buf = Vec::new();
1243 for seg in &segments {
1244 if let Segment::Inline(b) = seg {
1245 buf.extend_from_slice(b);
1246 }
1247 }
1248 assert!(
1250 buf.windows(4).any(|w| w == b"TPE1"),
1251 "TPE1 frame not emitted: routed elsewhere"
1252 );
1253 let read = read_tags(&buf);
1255 assert!(
1256 read.contains(&("artist".to_string(), "Band".to_string())),
1257 "got {read:?}"
1258 );
1259 }
1260
1261 #[test]
1262 fn build_id3v2_segments_rejects_oversized_total_tag() {
1263 let mk = |data_len: u64| ArtInput {
1267 art_id: 1,
1268 mime: "image/png".to_string(),
1269 description: String::new(),
1270 picture_type: PictureType::new(3).unwrap(),
1271 width: 0,
1272 height: 0,
1273 data_len: BlobLen::new(data_len).unwrap(),
1274 };
1275 assert_eq!(
1276 build_id3v2_segments(&[], &[], &[mk(0x1000_0000)]).err(),
1277 Some(FormatError::TooLarge)
1278 );
1279 assert!(build_id3v2_segments(&[], &[], &[mk(16)]).is_ok());
1280 let (_, total_at_one) = build_id3v2_segments(&[], &[], &[mk(1)]).unwrap();
1285 let overhead = total_at_one - 10 - 1; let boundary_data_len = 0x0FFF_FFFF - overhead;
1287 assert!(
1288 build_id3v2_segments(&[], &[], &[mk(boundary_data_len)]).is_ok(),
1289 "exact boundary (frames_len == 0x0FFF_FFFF) should be accepted"
1290 );
1291 assert_eq!(
1292 build_id3v2_segments(&[], &[], &[mk(boundary_data_len + 1)]).err(),
1293 Some(FormatError::TooLarge),
1294 "one byte past boundary must be rejected"
1295 );
1296 }
1297
1298 #[test]
1299 fn build_id3v2_segments_rejects_embedded_nul() {
1300 let nul_key = build_id3v2_segments(&[TagInput::new("bad\0key", "ok")], &[], &[]);
1303 assert_eq!(
1304 nul_key.err(),
1305 Some(FormatError::EmbeddedNul { field: "tag key" })
1306 );
1307
1308 let nul_value = build_id3v2_segments(&[TagInput::new("TIT2", "a\0b")], &[], &[]);
1309 assert_eq!(
1310 nul_value.err(),
1311 Some(FormatError::EmbeddedNul { field: "tag value" })
1312 );
1313
1314 let art = |mime: &str, desc: &str| ArtInput {
1315 art_id: 1,
1316 mime: mime.to_string(),
1317 description: desc.to_string(),
1318 picture_type: PictureType::new(3).unwrap(),
1319 width: 0,
1320 height: 0,
1321 data_len: BlobLen::new(16).unwrap(),
1322 };
1323 assert_eq!(
1324 build_id3v2_segments(&[], &[], &[art("image/png\0junk", "")]).err(),
1325 Some(FormatError::EmbeddedNul { field: "art mime" })
1326 );
1327 assert_eq!(
1328 build_id3v2_segments(&[], &[], &[art("image/png", "front\0cover")]).err(),
1329 Some(FormatError::EmbeddedNul {
1330 field: "art description"
1331 })
1332 );
1333
1334 assert!(
1336 build_id3v2_segments(
1337 &[TagInput::new("TIT2", "ok")],
1338 &[],
1339 &[art("image/png", "front")]
1340 )
1341 .is_ok()
1342 );
1343 }
1344
1345 #[test]
1346 fn build_id3v2_segments_emits_art_segment_with_correct_id_and_len() {
1347 let mk = |art_id: i64, data_len: u64| ArtInput {
1350 art_id,
1351 mime: "image/png".to_string(),
1352 description: String::new(),
1353 picture_type: PictureType::new(3).unwrap(),
1354 width: 0,
1355 height: 0,
1356 data_len: BlobLen::new(data_len).unwrap(),
1357 };
1358 let (segments, _len) = build_id3v2_segments(&[], &[], &[mk(2, 16)]).unwrap();
1359 let art_segs: Vec<_> = segments
1360 .iter()
1361 .filter_map(|s| match s {
1362 Segment::ArtImage { art_id, len } => Some((*art_id, len.get())),
1363 _ => None,
1364 })
1365 .collect();
1366 assert_eq!(
1367 art_segs,
1368 vec![(2_i64, 16_u64)],
1369 "only the non-empty art should be emitted"
1370 );
1371 }
1372
1373 fn ss(n: u32) -> [u8; 4] {
1376 [
1377 ((n >> 21) & 0x7F) as u8,
1378 ((n >> 14) & 0x7F) as u8,
1379 ((n >> 7) & 0x7F) as u8,
1380 (n & 0x7F) as u8,
1381 ]
1382 }
1383
1384 fn id3v2(major: u8, flags: u8, body: u32, frames: &[u8]) -> Vec<u8> {
1387 let mut v = Vec::new();
1388 v.extend_from_slice(b"ID3");
1389 v.push(major);
1390 v.push(0x00);
1391 v.push(flags);
1392 v.extend_from_slice(&ss(body));
1393 v.extend_from_slice(frames);
1394 v
1395 }
1396
1397 #[test]
1398 fn alloc_safe_accepts_minimal_valid_header() {
1399 let tag = id3v2(0x04, 0x00, 0, &[]);
1402 assert_eq!(tag.len(), 10);
1403 assert!(id3v2_alloc_safe(&tag));
1404 }
1405
1406 #[test]
1407 fn alloc_safe_rejects_short_and_non_id3() {
1408 assert!(!id3v2_alloc_safe(b"ID3xx"));
1412 assert!(!id3v2_alloc_safe(b"XXX\x04\x00\x00\x00\x00\x00\x00"));
1414 }
1415
1416 #[test]
1417 fn alloc_safe_rejects_bad_version_and_header_flags() {
1418 assert!(!id3v2_alloc_safe(&id3v2(0x05, 0x00, 0, &[])));
1420 assert!(!id3v2_alloc_safe(&id3v2(0x01, 0x00, 0, &[])));
1421 assert!(!id3v2_alloc_safe(&id3v2(0x04, 0x40, 0, &[])));
1423 assert!(!id3v2_alloc_safe(&id3v2(0x04, 0x80, 0, &[])));
1424 }
1425
1426 #[test]
1427 fn alloc_safe_rejects_high_bit_in_body_size() {
1428 let tag = vec![b'I', b'D', b'3', 0x04, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00];
1432 assert!(!id3v2_alloc_safe(&tag));
1433 let tag1 = vec![b'I', b'D', b'3', 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80];
1435 assert!(!id3v2_alloc_safe(&tag1));
1436 }
1437
1438 #[test]
1439 fn alloc_safe_rejects_high_bit_in_v24_frame_size() {
1440 let mut frame = b"TIT2".to_vec();
1446 frame.extend_from_slice(&[0x80, 0x80, 0x00, 0x00]); frame.extend_from_slice(&[0x00, 0x00]); let tag = id3v2(0x04, 0x00, 10, &frame);
1449 assert!(!id3v2_alloc_safe(&tag));
1450 }
1451
1452 fn v23_frame(id: &[u8; 4], size: u32, payload: &[u8]) -> Vec<u8> {
1455 let mut v = id.to_vec();
1456 v.extend_from_slice(&size.to_be_bytes());
1457 v.extend_from_slice(&[0x00, 0x00]);
1458 v.extend_from_slice(payload);
1459 v
1460 }
1461
1462 #[test]
1463 fn alloc_safe_v22_24bit_size_decode() {
1464 let mut f_mid = b"TT2".to_vec();
1469 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();
1473 f_hi.extend_from_slice(&[0x01, 0x00, 0x00]);
1474 assert!(!id3v2_alloc_safe(&id3v2(0x02, 0x00, 6, &f_hi)));
1475 let mut f_lo = b"TT2".to_vec();
1479 f_lo.extend_from_slice(&[0x00, 0x00, 0x10]); assert!(!id3v2_alloc_safe(&id3v2(0x02, 0x00, 6, &f_lo)));
1481 let mut f_ok = b"TT2".to_vec();
1483 f_ok.extend_from_slice(&[0x00, 0x00, 0x04]);
1484 f_ok.extend_from_slice(&[1, 2, 3, 4]);
1485 assert!(id3v2_alloc_safe(&id3v2(0x02, 0x00, 10, &f_ok)));
1486 }
1487
1488 #[test]
1489 fn alloc_safe_rejects_nonzero_frame_flags() {
1490 let mut f3 = b"TIT2".to_vec();
1492 f3.extend_from_slice(&4u32.to_be_bytes()); f3.extend_from_slice(&[0x00, 0x01]); f3.extend_from_slice(&[1, 2, 3, 4]);
1495 assert!(!id3v2_alloc_safe(&id3v2(0x03, 0x00, 14, &f3)));
1496
1497 let mut f4 = b"TIT2".to_vec();
1500 f4.extend_from_slice(&ss(4)); f4.extend_from_slice(&[0x00, 0x01]); f4.extend_from_slice(&[1, 2, 3, 4]);
1503 assert!(!id3v2_alloc_safe(&id3v2(0x04, 0x00, 14, &f4)));
1504 }
1505
1506 #[test]
1507 fn alloc_safe_rejects_chap_and_ctoc() {
1508 let chap = v23_frame(b"CHAP", 4, &[1, 2, 3, 4]);
1510 assert!(!id3v2_alloc_safe(&id3v2(0x03, 0x00, 14, &chap)));
1511 let ctoc = v23_frame(b"CTOC", 4, &[1, 2, 3, 4]);
1512 assert!(!id3v2_alloc_safe(&id3v2(0x03, 0x00, 14, &ctoc)));
1513 }
1514
1515 #[test]
1516 fn alloc_safe_frame_size_bounds() {
1517 let ok = v23_frame(b"TIT2", 4, &[1, 2, 3, 4]);
1522 assert!(id3v2_alloc_safe(&id3v2(0x03, 0x00, 14, &ok)));
1523 let over = v23_frame(b"TIT2", 5, &[1, 2, 3, 4]);
1528 assert!(!id3v2_alloc_safe(&id3v2(0x03, 0x00, 14, &over)));
1529 }
1530
1531 #[test]
1532 fn alloc_safe_data_start_equal_to_tag_end_is_ok() {
1533 let zero = v23_frame(b"TIT2", 0, &[]);
1536 assert!(id3v2_alloc_safe(&id3v2(0x03, 0x00, 10, &zero)));
1537 }
1538
1539 #[test]
1540 fn alloc_safe_rejects_bad_second_frame_in_body() {
1541 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)));
1549 }
1550
1551 #[test]
1552 fn alloc_safe_stops_at_tag_body_end() {
1553 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)));
1560 }
1561
1562 #[test]
1563 fn alloc_safe_walks_two_frames_and_stops_at_padding() {
1564 let mut frames = v23_frame(b"TIT2", 2, &[0xAA, 0xBB]);
1571 frames.extend_from_slice(&v23_frame(b"TPE1", 2, &[0xCC, 0xDD]));
1572 frames.extend_from_slice(&[0u8; 10]); assert!(id3v2_alloc_safe(&id3v2(0x03, 0x00, 25, &frames)));
1574 }
1575
1576 #[test]
1577 fn alloc_safe_rejects_frame_size_exceeding_tag_end() {
1578 let huge = v23_frame(b"TIT2", 100, &[1, 2, 3, 4]);
1581 assert!(!id3v2_alloc_safe(&id3v2(0x03, 0x00, 14, &huge)));
1582 }
1583
1584 fn mp3_with_id3v2(body_len: usize, audio: &[u8]) -> (Vec<u8>, u64) {
1587 let mut v = b"ID3\x04\x00\x00".to_vec(); v.extend_from_slice(&syncsafe(u32::try_from(body_len).unwrap()));
1589 v.extend(std::iter::repeat_n(0u8, body_len)); let audio_offset = v.len() as u64;
1591 v.extend_from_slice(&[0xFF, 0xFB]); v.extend_from_slice(audio);
1593 (v, audio_offset)
1594 }
1595
1596 #[test]
1597 fn locate_audio_bounded_complete_with_no_id3v1() {
1598 let (full, audio_offset) = mp3_with_id3v2(8, b"frames");
1599 let prefix = &full[..usize_from(audio_offset) + 2]; let file_len = full.len() as u64;
1601 match locate_audio_bounded(prefix, file_len, None).unwrap() {
1602 Extent::Complete(b) => {
1603 assert_eq!(b.audio_offset, audio_offset);
1604 assert_eq!(b.audio_length, file_len - audio_offset);
1605 }
1606 other @ Extent::NeedMore { .. } => panic!("expected Complete, got {other:?}"),
1607 }
1608 }
1609
1610 #[test]
1611 fn locate_audio_bounded_needmore_when_tag_exceeds_prefix() {
1612 let (full, _audio_offset) = mp3_with_id3v2(4096, b"frames");
1613 let prefix = &full[..32]; let file_len = full.len() as u64;
1615 match locate_audio_bounded(prefix, file_len, None).unwrap() {
1616 Extent::NeedMore { up_to } => assert_eq!(up_to, 10 + 4096 + 2),
1617 other @ Extent::Complete(_) => panic!("expected NeedMore, got {other:?}"),
1618 }
1619 }
1620
1621 #[test]
1622 fn locate_audio_bounded_strips_id3v1_tail() {
1623 let (mut full, audio_offset) = mp3_with_id3v2(8, b"frames");
1624 let body_end = full.len();
1625 full.extend_from_slice(b"TAG"); full.extend(std::iter::repeat_n(0u8, 125)); let file_len = full.len() as u64;
1628 let tail: [u8; 128] = full[full.len() - 128..].try_into().unwrap();
1629 let prefix = &full[..usize_from(audio_offset) + 2];
1630 match locate_audio_bounded(prefix, file_len, Some(&tail)).unwrap() {
1631 Extent::Complete(b) => {
1632 assert_eq!(b.audio_offset, audio_offset);
1633 assert_eq!(b.audio_length, body_end as u64 - audio_offset);
1634 }
1635 other @ Extent::NeedMore { .. } => panic!("expected Complete, got {other:?}"),
1636 }
1637 }
1638
1639 #[test]
1640 fn locate_audio_bounded_rejects_audio_start_past_eof() {
1641 let mut full = b"ID3\x04\x00\x00".to_vec();
1645 full.extend_from_slice(&syncsafe(8));
1646 full.extend(std::iter::repeat_n(0u8, 8)); let file_len = full.len() as u64; match locate_audio_bounded(&full, file_len, None) {
1649 Err(FormatError::NotMp3) => {}
1650 other => panic!("expected Err(NotMp3), got {other:?}"),
1651 }
1652 }
1653
1654 #[test]
1661 fn locate_audio_bounded_plain_mp3_no_id3_starts_at_zero() {
1662 let data = [0xFF, 0xFB, 0x90, 0x00, 1, 2, 3, 4, 5, 6, 7, 8];
1664 let file_len = data.len() as u64;
1665 match locate_audio_bounded(&data, file_len, None).unwrap() {
1666 Extent::Complete(b) => {
1667 assert_eq!(b.audio_offset, 0);
1668 assert_eq!(b.audio_length, file_len);
1669 }
1670 other @ Extent::NeedMore { .. } => {
1671 panic!("expected Complete at offset 0, got {other:?}")
1672 }
1673 }
1674 }
1675
1676 #[test]
1685 fn locate_audio_bounded_short_non_id3_with_small_file() {
1686 let data = [0xFF, 0xFB, 0x90, 0x00, 0x00];
1688 let file_len = data.len() as u64; match locate_audio_bounded(&data, file_len, None).unwrap() {
1690 Extent::Complete(b) => {
1691 assert_eq!(b.audio_offset, 0);
1692 assert_eq!(b.audio_length, 5);
1693 }
1694 other @ Extent::NeedMore { .. } => panic!("expected Complete, got {other:?}"),
1695 }
1696 }
1697
1698 #[test]
1704 fn locate_audio_bounded_footer_flag_adds_ten() {
1705 let body = 6usize;
1706 let mut full = b"ID3\x04\x00".to_vec();
1707 full.push(0x10); full.extend_from_slice(&syncsafe(u32::try_from(body).unwrap()));
1709 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");
1714 let file_len = full.len() as u64;
1715 match locate_audio_bounded(&full, file_len, None).unwrap() {
1716 Extent::Complete(b) => {
1717 assert_eq!(b.audio_offset, 26);
1718 assert_eq!(b.audio_offset, expected_offset);
1719 assert_eq!(b.audio_length, file_len - 26);
1720 }
1721 other @ Extent::NeedMore { .. } => {
1722 panic!("expected Complete at offset 26, got {other:?}")
1723 }
1724 }
1725 }
1726
1727 #[test]
1734 fn locate_audio_bounded_tag_len_equals_file_len_is_notmp3_not_malformed() {
1735 let body = 8usize;
1736 let mut full = b"ID3\x04\x00\x00".to_vec();
1737 full.extend_from_slice(&syncsafe(u32::try_from(body).unwrap()));
1738 full.extend(std::iter::repeat_n(0u8, body)); let file_len = full.len() as u64; match locate_audio_bounded(&full, file_len, None) {
1741 Err(FormatError::NotMp3) => {}
1742 other => panic!("expected Err(NotMp3) for tag_len==file_len, got {other:?}"),
1743 }
1744 }
1745
1746 #[test]
1751 fn locate_audio_bounded_tag_len_exceeds_file_len_is_malformed() {
1752 let mut full = b"ID3\x04\x00\x00".to_vec();
1754 full.extend_from_slice(&syncsafe(100));
1755 full.extend_from_slice(&[0xFF, 0xFB]); let file_len = full.len() as u64; match locate_audio_bounded(&full, file_len, None) {
1758 Err(FormatError::Malformed) => {}
1759 other => panic!("expected Err(Malformed), got {other:?}"),
1760 }
1761 }
1762
1763 #[test]
1768 fn locate_audio_bounded_short_prefix_large_file_needs_header() {
1769 let prefix = [0x00, 0x00, 0x00, 0x00, 0x00]; let file_len = 64u64; match locate_audio_bounded(&prefix, file_len, None).unwrap() {
1772 Extent::NeedMore { up_to } => assert_eq!(up_to, 10),
1773 other @ Extent::Complete(_) => panic!("expected NeedMore{{up_to:10}}, got {other:?}"),
1774 }
1775 }
1776
1777 #[test]
1784 fn locate_audio_bounded_prefix_len_exactly_ten_proceeds() {
1785 let prefix = [0xFF, 0xFB, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
1787 let file_len = 64u64; match locate_audio_bounded(&prefix, file_len, None).unwrap() {
1789 Extent::Complete(b) => {
1790 assert_eq!(b.audio_offset, 0);
1791 assert_eq!(b.audio_length, file_len);
1792 }
1793 other @ Extent::NeedMore { .. } => {
1794 panic!("expected Complete (10<10 false), got {other:?}")
1795 }
1796 }
1797 }
1798
1799 #[test]
1807 fn locate_audio_bounded_short_prefix_small_file_proceeds() {
1808 let data = [0xFF, 0xFB, 0x90, 0x00, 0x00]; let file_len = 8u64;
1812 match locate_audio_bounded(&data, file_len, None).unwrap() {
1813 Extent::Complete(b) => {
1814 assert_eq!(b.audio_offset, 0);
1815 assert_eq!(b.audio_length, 8);
1816 }
1817 other @ Extent::NeedMore { .. } => {
1818 panic!("expected Complete (file_len<10), got {other:?}")
1819 }
1820 }
1821 }
1822
1823 #[test]
1830 fn locate_audio_bounded_sync_one_byte_past_eof_is_notmp3() {
1831 let body = 4usize;
1832 let mut full = b"ID3\x04\x00\x00".to_vec();
1833 full.extend_from_slice(&syncsafe(u32::try_from(body).unwrap()));
1834 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) {
1840 Err(FormatError::NotMp3) => {}
1841 other => panic!("expected Err(NotMp3) (sync past EOF), got {other:?}"),
1842 }
1843 }
1844
1845 #[test]
1849 fn locate_audio_bounded_sync_fits_in_file_proceeds() {
1850 let (full, audio_offset) = mp3_with_id3v2(4, b"frames");
1851 let file_len = full.len() as u64; match locate_audio_bounded(&full, file_len, None).unwrap() {
1853 Extent::Complete(b) => assert_eq!(b.audio_offset, audio_offset),
1854 other @ Extent::NeedMore { .. } => panic!("expected Complete, got {other:?}"),
1855 }
1856 }
1857
1858 #[test]
1859 fn locate_audio_bounded_sync_exactly_at_eof_proceeds() {
1860 let body = 4usize;
1865 let mut full = b"ID3\x04\x00\x00".to_vec();
1866 full.extend_from_slice(&syncsafe(u32::try_from(body).unwrap()));
1867 full.extend(std::iter::repeat_n(0u8, body)); let audio_offset = full.len() as u64; full.push(0xFF); full.push(0xFB);
1871 let file_len = full.len() as u64; match locate_audio_bounded(&full, file_len, None).unwrap() {
1874 Extent::Complete(b) => {
1875 assert_eq!(b.audio_offset, audio_offset);
1876 assert_eq!(b.audio_length, 2);
1877 }
1878 other @ Extent::NeedMore { .. } => {
1879 panic!("expected Complete (exact fit), got {other:?}")
1880 }
1881 }
1882 }
1883
1884 #[test]
1894 fn locate_audio_bounded_rejects_bad_second_sync_byte() {
1895 let data = [
1897 0xFF, 0x00, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1898 ];
1899 let file_len = data.len() as u64;
1900 match locate_audio_bounded(&data, file_len, None) {
1901 Err(FormatError::NotMp3) => {}
1902 other => panic!("expected Err(NotMp3) (bad sync byte 1), got {other:?}"),
1903 }
1904 }
1905
1906 #[test]
1913 fn locate_audio_bounded_rejects_bad_second_sync_byte_after_id3() {
1914 let body = 4usize;
1915 let mut full = b"ID3\x04\x00\x00".to_vec();
1916 full.extend_from_slice(&syncsafe(u32::try_from(body).unwrap()));
1917 full.extend(std::iter::repeat_n(0u8, body)); full.extend_from_slice(&[0xFF, 0x00]); full.extend_from_slice(b"tail");
1920 let file_len = full.len() as u64;
1921 match locate_audio_bounded(&full, file_len, None) {
1922 Err(FormatError::NotMp3) => {}
1923 other => panic!("expected Err(NotMp3) (bad sync at 15), got {other:?}"),
1924 }
1925 }
1926
1927 #[test]
1934 fn locate_audio_bounded_needmore_for_sync_past_prefix() {
1935 let body = 4usize;
1936 let mut full = b"ID3\x04\x00\x00".to_vec();
1937 full.extend_from_slice(&syncsafe(u32::try_from(body).unwrap()));
1938 full.extend(std::iter::repeat_n(0u8, body)); full.extend_from_slice(&[0xFF, 0xFB]); full.extend_from_slice(b"more audio bytes here");
1941 let file_len = full.len() as u64; let prefix = &full[..15]; match locate_audio_bounded(prefix, file_len, None).unwrap() {
1944 Extent::NeedMore { up_to } => assert_eq!(up_to, 16), other @ Extent::Complete(_) => panic!("expected NeedMore{{up_to:16}}, got {other:?}"),
1946 }
1947 }
1948
1949 #[test]
1955 fn locate_audio_bounded_trims_id3v1_when_tag_and_room() {
1956 let (mut full, audio_offset) = mp3_with_id3v2(8, b"frames");
1957 let body_end = full.len();
1958 full.extend_from_slice(b"TAG");
1959 full.extend(std::iter::repeat_n(0u8, 125)); let file_len = full.len() as u64;
1961 assert!(file_len >= audio_offset + 128); let tail: [u8; 128] = full[full.len() - 128..].try_into().unwrap();
1963 let prefix = &full[..usize_from(audio_offset) + 2];
1964 match locate_audio_bounded(prefix, file_len, Some(&tail)).unwrap() {
1965 Extent::Complete(b) => {
1966 assert_eq!(b.audio_offset, audio_offset);
1967 assert_eq!(b.audio_length, file_len - audio_offset - 128);
1969 assert_eq!(b.audio_length, body_end as u64 - audio_offset);
1970 }
1971 other @ Extent::NeedMore { .. } => panic!("expected Complete (trimmed), got {other:?}"),
1972 }
1973 }
1974
1975 #[test]
1981 fn locate_audio_bounded_no_trim_when_tail_not_tag() {
1982 let (mut full, audio_offset) = mp3_with_id3v2(8, b"frames");
1983 full.extend(std::iter::repeat_n(0u8, 200));
1985 let file_len = full.len() as u64;
1986 assert!(file_len >= audio_offset + 128); let tail: [u8; 128] = full[full.len() - 128..].try_into().unwrap();
1988 assert_ne!(&tail[0..3], b"TAG"); let prefix = &full[..usize_from(audio_offset) + 2];
1990 match locate_audio_bounded(prefix, file_len, Some(&tail)).unwrap() {
1991 Extent::Complete(b) => {
1992 assert_eq!(b.audio_offset, audio_offset);
1993 assert_eq!(b.audio_length, file_len - audio_offset);
1995 }
1996 other @ Extent::NeedMore { .. } => panic!("expected Complete (no trim), got {other:?}"),
1997 }
1998 }
1999
2000 #[test]
2006 fn locate_audio_bounded_no_trim_when_no_room_even_with_tag_tail() {
2007 let (mut full, audio_offset) = mp3_with_id3v2(8, b"frames");
2008 full.extend_from_slice(b"TAGxx"); let file_len = full.len() as u64;
2011 assert!(file_len < audio_offset + 128); let mut tail = [0u8; 128];
2015 tail[0..3].copy_from_slice(b"TAG");
2016 let prefix = &full[..usize_from(audio_offset) + 2];
2017 match locate_audio_bounded(prefix, file_len, Some(&tail)).unwrap() {
2018 Extent::Complete(b) => {
2019 assert_eq!(b.audio_offset, audio_offset);
2020 assert_eq!(b.audio_length, file_len - audio_offset); }
2022 other @ Extent::NeedMore { .. } => {
2023 panic!("expected Complete (no room, no trim), got {other:?}")
2024 }
2025 }
2026 }
2027
2028 fn build_v24_tag(frames: &[(&[u8; 4], &[u8])]) -> Vec<u8> {
2034 let total_body: usize = frames.iter().map(|(_, b)| 10 + b.len()).sum();
2035 let mut out = Vec::new();
2036 out.extend_from_slice(b"ID3");
2037 out.extend_from_slice(&[0x04, 0x00, 0x00]); out.extend_from_slice(&ss(u32::try_from(total_body).unwrap()));
2039 for (id, body) in frames {
2040 out.extend_from_slice(*id);
2041 out.extend_from_slice(&ss(u32::try_from(body.len()).unwrap()));
2042 out.extend_from_slice(&[0x00, 0x00]); out.extend_from_slice(body);
2044 }
2045 out
2046 }
2047
2048 fn build_v23_tag(frames: &[(&[u8; 4], &[u8])]) -> Vec<u8> {
2052 let total_body: usize = frames.iter().map(|(_, b)| 10 + b.len()).sum();
2053 let mut out = Vec::new();
2054 out.extend_from_slice(b"ID3");
2055 out.extend_from_slice(&[0x03, 0x00, 0x00]); out.extend_from_slice(&ss(u32::try_from(total_body).unwrap())); for (id, body) in frames {
2058 out.extend_from_slice(*id);
2059 out.extend_from_slice(&(u32::try_from(body.len()).unwrap()).to_be_bytes()); out.extend_from_slice(&[0x00, 0x00]); out.extend_from_slice(body);
2062 }
2063 out
2064 }
2065
2066 #[test]
2073 fn read_binary_tags_v23_plain_u32_frame_size() {
2074 let filler = vec![0xAAu8; 8];
2082 let body: Vec<u8> = (0..200u32)
2083 .map(|i| u8::try_from(i % 250 + 1).unwrap())
2084 .collect();
2085 let tag = build_v23_tag(&[(b"GEOB", &filler), (b"PRIV", &body)]);
2086 let (opaque, _promoted) = super::read_binary_tags(&tag);
2087 let geob = opaque
2088 .iter()
2089 .find(|e| e.key == "GEOB")
2090 .expect("v2.3 GEOB preserved");
2091 assert_eq!(
2092 geob.payload, filler,
2093 "v2.3 first frame must survive byte-exact"
2094 );
2095 let priv_frame = opaque
2096 .iter()
2097 .find(|e| e.key == "PRIV")
2098 .expect("v2.3 PRIV preserved");
2099 assert_eq!(
2100 priv_frame.payload, body,
2101 "v2.3 plain-u32 frame body must survive byte-exact"
2102 );
2103 }
2104
2105 #[test]
2106 fn read_binary_tags_skips_unsafe_tag() {
2107 let mut tag = build_v24_tag(&[(b"PRIV", &[1, 2, 3])]);
2112 tag[5] = 0x80; let (opaque, promoted) = super::read_binary_tags(&tag);
2114 assert!(
2115 opaque.is_empty() && promoted.is_empty(),
2116 "an alloc-unsafe tag must yield no binary frames"
2117 );
2118 }
2119
2120 #[test]
2121 fn read_binary_tags_skips_text_comm_uslt_apic() {
2122 let tag = build_v24_tag(&[
2125 (b"TIT2", &[0x00, b'x']),
2126 (b"COMM", &[0x00]),
2127 (b"USLT", &[0x00]),
2128 (b"APIC", &[0x00]),
2129 (b"PRIV", &[9, 9, 9]),
2130 ]);
2131 let (opaque, _promoted) = super::read_binary_tags(&tag);
2132 let keys: Vec<&str> = opaque.iter().map(|e| e.key.as_str()).collect();
2133 assert_eq!(
2134 keys,
2135 vec!["PRIV"],
2136 "only PRIV is opaque; T***/COMM/USLT/APIC are handled elsewhere: {keys:?}"
2137 );
2138 }
2139
2140 #[test]
2141 fn read_binary_tags_decodes_popm_counter_big_endian_and_zero() {
2142 let tag = build_v24_tag(&[(b"POPM", &[0x00, 200, 0x01, 0x02])]);
2145 let (_opaque, promoted) = super::read_binary_tags(&tag);
2146 assert!(
2147 promoted.contains(&("rating".to_string(), "200".to_string())),
2148 "rating: {promoted:?}"
2149 );
2150 assert!(
2151 promoted.contains(&("playcount".to_string(), "258".to_string())),
2152 "counter must decode big-endian: {promoted:?}"
2153 );
2154
2155 let tag0 = build_v24_tag(&[(b"POPM", &[0x00, 128, 0x00])]);
2157 let (_o0, promoted0) = super::read_binary_tags(&tag0);
2158 assert!(
2159 promoted0.contains(&("rating".to_string(), "128".to_string())),
2160 "rating: {promoted0:?}"
2161 );
2162 assert!(
2163 !promoted0.iter().any(|(k, _)| k == "playcount"),
2164 "a zero POPM counter must not promote playcount: {promoted0:?}"
2165 );
2166 }
2167
2168 #[test]
2169 fn popm_frame_data_emits_counter_only_when_positive() {
2170 assert_eq!(
2172 super::popm_frame_data(200, 0),
2173 vec![0x00, 200],
2174 "playcount 0 must omit the counter"
2175 );
2176 assert_eq!(
2178 super::popm_frame_data(200, 5),
2179 vec![0x00, 200, 0x00, 0x00, 0x00, 0x05],
2180 "playcount > 0 must append a 4-byte counter"
2181 );
2182 }
2183
2184 #[test]
2185 fn build_id3v2_segments_accounts_playcount_and_opaque_len() {
2186 use crate::{BinaryTagInput, TagInput};
2187
2188 let tags = vec![
2191 TagInput::new("rating", "100"),
2192 TagInput::new("playcount", "42"),
2193 ];
2194 let (segments, _len) = build_id3v2_segments(&tags, &[], &[]).unwrap();
2195 let inline: Vec<u8> = segments
2196 .iter()
2197 .flat_map(|s| match s {
2198 Segment::Inline(b) => b.clone(),
2199 _ => Vec::new(),
2200 })
2201 .collect();
2202 let (_opaque, promoted) = super::read_binary_tags(&inline);
2203 assert!(
2204 promoted.contains(&("playcount".to_string(), "42".to_string())),
2205 "playcount must rebuild into the POPM counter: {promoted:?}"
2206 );
2207
2208 let bin = vec![BinaryTagInput {
2211 key: "PRIV".into(),
2212 payload_id: 1,
2213 len: BlobLen::new(7).unwrap(),
2214 }];
2215 let (_segs, total) = build_id3v2_segments(&[], &bin, &[]).unwrap();
2216 assert_eq!(total, 10 + 10 + 7, "opaque binary frame length accounting");
2217 }
2218
2219 #[test]
2220 fn read_binary_tags_promotes_popm_and_mbid_and_passes_through_priv() {
2221 use id3::frame::{Content, Popularimeter, UniqueFileIdentifier, Unknown};
2222 use id3::{Encoder, Frame, Tag, TagLike, Version};
2223
2224 let mut tag = Tag::new();
2225 tag.add_frame(Popularimeter {
2226 user: "a@b.c".into(),
2227 rating: 200,
2228 counter: 7,
2229 });
2230 tag.add_frame(UniqueFileIdentifier {
2231 owner_identifier: "http://musicbrainz.org".into(),
2232 identifier: b"mbid-123".to_vec(),
2233 });
2234 tag.add_frame(UniqueFileIdentifier {
2235 owner_identifier: "http://other.example".into(),
2236 identifier: b"other".to_vec(),
2237 });
2238 tag.add_frame(Frame::with_content(
2239 "PRIV",
2240 Content::Unknown(Unknown {
2241 data: vec![9, 8, 7],
2242 version: Version::Id3v24,
2243 }),
2244 ));
2245 let mut buf = Vec::new();
2246 Encoder::new()
2247 .version(Version::Id3v24)
2248 .encode(&tag, &mut buf)
2249 .unwrap();
2250
2251 let (opaque, promoted) = super::read_binary_tags(&buf);
2252 assert!(promoted.contains(&("rating".to_string(), "200".to_string())));
2253 assert!(promoted.contains(&("playcount".to_string(), "7".to_string())));
2254 assert!(promoted.contains(&("musicbrainz_trackid".to_string(), "mbid-123".to_string())));
2255 let keys: Vec<&str> = opaque.iter().map(|e| e.key.as_str()).collect();
2256 assert!(keys.contains(&"PRIV"));
2257 assert_eq!(keys.iter().filter(|k| **k == "UFID").count(), 1);
2259 assert_eq!(
2260 opaque.iter().find(|e| e.key == "PRIV").unwrap().payload,
2261 vec![9, 8, 7]
2262 );
2263 }
2264
2265 #[test]
2266 fn read_binary_tags_preserves_geob_body_byte_exact() {
2267 let geob_body: Vec<u8> = {
2272 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
2278 };
2279 let tag = build_v24_tag(&[(b"GEOB", &geob_body)]);
2280
2281 let (opaque, _promoted) = super::read_binary_tags(&tag);
2282 let geob = opaque
2283 .iter()
2284 .find(|e| e.key == "GEOB")
2285 .expect("GEOB preserved");
2286 assert_eq!(
2287 geob.payload, geob_body,
2288 "GEOB body must survive byte-identical"
2289 );
2290 }
2291
2292 #[test]
2293 fn build_id3v2_segments_rebuilds_popm_ufid_and_streams_opaque() {
2294 use crate::BinaryTagInput;
2295 let tags = vec![
2296 TagInput::new("artist", "A"),
2297 TagInput::new("rating", "200"),
2298 TagInput::new("playcount", "7"),
2299 TagInput::new("musicbrainz_trackid", "mbid-123"),
2300 ];
2301 let bin = vec![BinaryTagInput {
2302 key: "PRIV".into(),
2303 payload_id: 42,
2304 len: BlobLen::new(3).unwrap(),
2305 }];
2306 let (segments, _len) = super::build_id3v2_segments(&tags, &bin, &[]).unwrap();
2307
2308 assert!(
2309 segments.iter().any(|s| matches!(
2310 s,
2311 Segment::BinaryTag {
2312 payload_id: 42,
2313 len,
2314 ..
2315 } if len.get() == 3
2316 )),
2317 "opaque PRIV must stream as Segment::BinaryTag"
2318 );
2319
2320 let inline: Vec<u8> = segments
2321 .iter()
2322 .flat_map(|s| match s {
2323 Segment::Inline(b) => b.clone(),
2324 _ => Vec::new(),
2325 })
2326 .collect();
2327 assert!(find_sub(&inline, b"POPM"), "POPM not rebuilt");
2328 assert!(find_sub(&inline, b"UFID"), "UFID not rebuilt");
2329 assert!(
2330 find_sub(&inline, b"http://musicbrainz.org"),
2331 "UFID owner missing"
2332 );
2333 assert!(!find_sub(&inline, b"rating"), "promoted key leaked as TXXX");
2334 assert!(
2335 !find_sub(&inline, b"musicbrainz_trackid"),
2336 "promoted key leaked as TXXX"
2337 );
2338 }
2339
2340 #[test]
2341 fn build_id3v2_segments_first_promoted_scalar_wins() {
2342 let tags = vec![
2346 TagInput::new("rating", "10"),
2347 TagInput::new("rating", "20"),
2348 TagInput::new("musicbrainz_trackid", "mbid-first"),
2349 TagInput::new("musicbrainz_trackid", "mbid-second"),
2350 ];
2351 let (segments, _len) = build_id3v2_segments(&tags, &[], &[]).unwrap();
2352 let inline: Vec<u8> = segments
2353 .iter()
2354 .flat_map(|s| match s {
2355 Segment::Inline(b) => b.clone(),
2356 _ => Vec::new(),
2357 })
2358 .collect();
2359
2360 assert!(find_sub(&inline, b"mbid-first"), "first mbid must win");
2362 assert!(
2363 !find_sub(&inline, b"mbid-second"),
2364 "later mbid must be dropped"
2365 );
2366
2367 let (_opaque, promoted) = super::read_binary_tags(&inline);
2369 assert!(
2370 promoted.contains(&("rating".to_string(), "10".to_string())),
2371 "first rating must win: {promoted:?}"
2372 );
2373 assert!(
2374 !promoted.iter().any(|(k, v)| k == "rating" && v == "20"),
2375 "later rating must be dropped: {promoted:?}"
2376 );
2377 }
2378
2379 #[test]
2380 fn build_id3v2_segments_checked_art_len_rejects_overflow() {
2381 let mk = |data_len: u64| ArtInput {
2384 art_id: 1,
2385 mime: "image/png".to_string(),
2386 description: String::new(),
2387 picture_type: PictureType::new(3).unwrap(),
2388 width: 0,
2389 height: 0,
2390 data_len: BlobLen::new(data_len).unwrap(),
2391 };
2392 assert_eq!(
2393 build_id3v2_segments(&[], &[], &[mk(u64::MAX)]).err(),
2394 Some(FormatError::TooLarge)
2395 );
2396 }
2397
2398 fn find_sub(hay: &[u8], needle: &[u8]) -> bool {
2399 hay.windows(needle.len()).any(|w| w == needle)
2400 }
2401
2402 fn assert_mp3_bounded_matches_full(data: &[u8]) {
2407 let len = data.len() as u64;
2408 let tail: Option<&[u8; 128]> = if data.len() >= 128 {
2409 data[data.len() - 128..].try_into().ok()
2410 } else {
2411 None
2412 };
2413 match (locate_audio(data), locate_audio_bounded(data, len, tail)) {
2414 (Ok(full), Ok(Extent::Complete(bounded))) => assert_eq!(full, bounded),
2415 (Err(_), Err(_)) => {}
2416 (full, bounded) => {
2417 panic!("mp3 bounded/full divergence: full={full:?} bounded={bounded:?}")
2418 }
2419 }
2420 }
2421
2422 #[test]
2423 fn locate_audio_rejects_high_bit_size_byte() {
2424 let mut data = Vec::new();
2427 data.extend_from_slice(b"ID3");
2428 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));
2432 }
2433
2434 #[test]
2435 fn locate_audio_rejects_unsupported_major_version() {
2436 let mut data = Vec::new();
2437 data.extend_from_slice(b"ID3");
2438 data.extend_from_slice(&[0x05, 0x00, 0x00]); data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
2440 data.extend_from_slice(&[0xFF, 0xFB, 0x90, 0x00]);
2441 assert_eq!(locate_audio(&data), Err(FormatError::Malformed));
2442 }
2443
2444 #[test]
2445 fn locate_audio_bounded_rejects_high_bit_size_byte() {
2446 let mut data = Vec::new();
2447 data.extend_from_slice(b"ID3");
2448 data.extend_from_slice(&[0x04, 0x00, 0x00]);
2449 data.extend_from_slice(&[0x00, 0x00, 0x00, 0x80]);
2450 data.extend_from_slice(&[0xFF, 0xFB, 0x90, 0x00]);
2451 let file_len = data.len() as u64;
2452 assert_eq!(
2453 locate_audio_bounded(&data, file_len, None),
2454 Err(FormatError::Malformed)
2455 );
2456 }
2457
2458 #[test]
2459 fn mp3_bounded_matches_full_on_whole_buffer() {
2460 assert_mp3_bounded_matches_full(&crate::fuzz_check::fixtures::mp3());
2462 assert_mp3_bounded_matches_full(&crate::fuzz_check::fixtures::mp3_with_binary_frame());
2464
2465 let mut with_trailer = crate::fuzz_check::fixtures::mp3();
2468 with_trailer.resize(200, 0x00);
2469 with_trailer.extend_from_slice(b"TAG");
2470 with_trailer.resize(with_trailer.len() + 125, 0x00); assert_mp3_bounded_matches_full(&with_trailer);
2472 }
2473}