1mod art_source;
2mod b64;
3mod crc;
4mod page;
5
6pub use art_source::{ArtSource, MapArtSource};
7
8pub use b64::{B64Window, b64_len, b64_len_checked, b64_window, encode_b64_slice};
9pub use page::{
10 PageHeader, parse_page, patch_page_header, patch_page_header_algebraic, verify_page_crc,
11};
12
13use crate::error::{FormatError, Result};
14use crate::probe::Extent;
15use crate::size;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Codec {
20 Opus,
21 Vorbis,
22 OggFlac,
23}
24
25const METADATA_BLOCK_PICTURE_KEY: &[u8] = b"METADATA_BLOCK_PICTURE=";
26
27fn detect_codec(first_packet: &[u8]) -> Result<Codec> {
28 if first_packet.len() >= 8 && &first_packet[0..8] == b"OpusHead" {
29 Ok(Codec::Opus)
30 } else if first_packet.len() >= 7 && &first_packet[0..7] == b"\x01vorbis" {
31 Ok(Codec::Vorbis)
32 } else if first_packet.len() >= 5 && &first_packet[0..5] == b"\x7FFLAC" {
33 Ok(Codec::OggFlac)
34 } else {
35 Err(FormatError::Malformed)
36 }
37}
38
39fn oggflac_following_packets(first_packet: &[u8]) -> Result<usize> {
43 if first_packet.len() < 9 {
44 return Err(FormatError::Malformed);
45 }
46 Ok(u16::from_be_bytes([first_packet[7], first_packet[8]]) as usize)
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct OggHeader {
53 pub codec: Codec,
54 pub serial: u32,
55 pub packets: Vec<Vec<u8>>,
56 pub header_pages: u32,
57 pub audio_offset: u64,
58}
59
60fn validate_single_bitstream(data: &[u8], audio_offset: u64, serial: u32) -> Result<()> {
63 let mut pos = 0usize;
64 let mut first = true;
65 while (pos as u64) < audio_offset {
66 let h = crate::ogg::page::parse_page(data, pos)?;
67 if h.serial != serial {
68 return Err(FormatError::Malformed);
69 }
70 if !first && (h.header_type & crate::ogg::page::FLAG_BOS) != 0 {
71 return Err(FormatError::Malformed);
72 }
73 first = false;
74 pos += h.total_len();
75 }
76 Ok(())
77}
78
79pub fn read_header(data: &[u8]) -> Result<OggHeader> {
83 let first_page = page::parse_page(data, 0)?;
84 let serial = first_page.serial;
85
86 let first = page::read_packets(data, 1)?;
88 let first_pkt = first.first().ok_or(FormatError::Malformed)?;
89 let codec = detect_codec(&first_pkt.data)?;
90
91 let want = match codec {
92 Codec::Opus => 2,
93 Codec::Vorbis => 3,
94 Codec::OggFlac => 1 + oggflac_following_packets(&first_pkt.data)?,
95 };
96
97 let pkts = page::read_packets(data, want)?;
98 if pkts.len() != want {
99 return Err(FormatError::Malformed);
100 }
101 let last = pkts.last().unwrap();
102 let audio_offset = last.end_offset as u64;
103 validate_single_bitstream(data, audio_offset, serial)?;
104 Ok(OggHeader {
105 codec,
106 serial,
107 packets: pkts.iter().map(|p| p.data.clone()).collect(),
108 header_pages: last.pages_through_end,
109 audio_offset,
110 })
111}
112
113fn comment_body(codec: Codec, packet: &[u8]) -> Result<&[u8]> {
115 let prefix = match codec {
116 Codec::Opus => 8, Codec::Vorbis => 7, Codec::OggFlac => 4, };
120 if packet.len() < prefix {
121 return Err(FormatError::Malformed);
122 }
123 Ok(&packet[prefix..])
124}
125
126fn comment_packet_index(header: &OggHeader) -> usize {
128 match header.codec {
129 Codec::Opus | Codec::Vorbis => 1,
130 Codec::OggFlac => header
133 .packets
134 .iter()
135 .enumerate()
136 .skip(1)
137 .find(|(_, p)| !p.is_empty() && (p[0] & 0x7F) == 4)
138 .map_or(0, |(i, _)| i),
139 }
140}
141
142pub fn read_tags(data: &[u8]) -> Result<Vec<(String, String)>> {
144 let header = read_header(data)?;
145 let idx = comment_packet_index(&header);
146 if idx == 0 {
147 return Ok(Vec::new()); }
149 let body = comment_body(header.codec, &header.packets[idx])?;
150 let mut tags = crate::vorbiscomment::parse(body)?;
151 tags.retain(|(field, _)| !field.eq_ignore_ascii_case("METADATA_BLOCK_PICTURE"));
155 Ok(tags)
156}
157
158use crate::input::EmbeddedPicture;
159
160pub fn read_pictures(data: &[u8]) -> Result<Vec<EmbeddedPicture>> {
167 use base64::Engine;
168 let header = read_header(data)?;
169 let mut out = Vec::new();
170 match header.codec {
171 Codec::Opus | Codec::Vorbis => {
172 let idx = comment_packet_index(&header);
173 if idx == 0 {
174 return Ok(out);
175 }
176 let body = comment_body(header.codec, &header.packets[idx])?;
177 for (field, value) in crate::vorbiscomment::parse(body)? {
178 if field.eq_ignore_ascii_case("METADATA_BLOCK_PICTURE") {
179 let raw = base64::engine::general_purpose::STANDARD
180 .decode(value.as_bytes())
181 .map_err(|_| FormatError::Malformed)?;
182 out.push(crate::flac::parse_picture_block(&raw)?);
183 }
184 }
185 }
186 Codec::OggFlac => {
187 for pkt in header.packets.iter().skip(1) {
188 if pkt.len() >= 4 && (pkt[0] & 0x7F) == 6 {
191 out.push(crate::flac::parse_picture_block(&pkt[4..])?);
193 }
194 }
195 }
196 }
197 Ok(out)
198}
199
200#[derive(Debug, Clone, PartialEq, Eq)]
202pub struct OggScan {
203 pub codec: Codec,
204 pub audio_offset: u64,
205 pub audio_length: u64,
206}
207
208pub fn locate_audio(data: &[u8]) -> Result<OggScan> {
209 let header = read_header(data)?;
210 if header.audio_offset > data.len() as u64 {
211 return Err(FormatError::Malformed);
212 }
213 Ok(OggScan {
214 codec: header.codec,
215 audio_offset: header.audio_offset,
216 audio_length: data.len() as u64 - header.audio_offset,
217 })
218}
219
220pub fn read_metadata(front: &[u8]) -> Result<OggHeader> {
223 read_header(front)
224}
225
226pub fn read_metadata_bounded(prefix: &[u8], file_len: u64) -> Result<Extent<OggHeader>> {
233 match read_header(prefix) {
234 Ok(header) => Ok(Extent::Complete(header)),
235 Err(_) if (prefix.len() as u64) < file_len => {
240 let grown = ((prefix.len() as u64).saturating_mul(2)).max(64 * 1024);
241 Ok(Extent::NeedMore {
242 up_to: grown.min(file_len),
243 })
244 }
245 Err(e) => Err(e),
246 }
247}
248
249use crate::input::TagInput;
250use crate::layout::{RegionLayout, Segment};
251
252pub fn synthesize_layout(
253 header: &OggHeader,
254 audio_offset: u64,
255 audio_length: u64,
256 tags: &[TagInput],
257 arts: &[OggArt],
258 src: &dyn ArtSource,
259) -> Result<RegionLayout> {
260 let arts: Vec<OggArt> = arts.to_vec();
261 let packet_chunks = build_packets_with_art(header, tags, &arts)?;
262 let mut segments: Vec<Segment> = Vec::new();
263 let mut seq = 0u32;
264 for (i, chunks) in packet_chunks.iter().enumerate() {
265 let (segs, used) =
266 crate::ogg::page::lace_chunks_to_segments(header.serial, seq, i == 0, chunks, src)?;
267 segments.extend(segs);
268 seq += used;
269 }
270 let seq_delta = i64::from(seq) - i64::from(header.header_pages);
271 segments.push(Segment::OggAudio {
272 offset: audio_offset,
273 len: audio_length,
274 seq_delta,
275 });
276 Ok(RegionLayout::validated(segments)?)
277}
278
279fn picture_prefix(art: &crate::input::ArtInput) -> Result<Vec<u8>> {
289 let base = 32 + art.mime.len() + art.description.len();
292 let pad = (3 - base % 3) % 3;
293 let description = format!("{}{}", art.description, " ".repeat(pad));
294 crate::flac::picture_body_framing(art, &description)
295}
296
297use crate::ogg::page::PayloadChunk;
298use base64::Engine;
299
300#[derive(Clone, Copy)]
303pub struct OggArt<'a> {
304 pub meta: &'a crate::input::ArtInput,
305}
306
307fn b64_encode(bytes: &[u8]) -> Vec<u8> {
308 base64::engine::general_purpose::STANDARD
309 .encode(bytes)
310 .into_bytes()
311}
312
313fn build_packets_with_art(
317 header: &OggHeader,
318 tags: &[TagInput],
319 arts: &[OggArt],
320) -> Result<Vec<Vec<PayloadChunk>>> {
321 match header.codec {
322 Codec::Opus | Codec::Vorbis => {
323 for a in arts {
328 let prefix = picture_prefix(a.meta)?;
329 let b64_prefix_len =
330 b64_len_checked(prefix.len() as u64).ok_or(FormatError::TooLarge)?;
331 let b64_image_len =
332 b64_len_checked(a.meta.data_len.get()).ok_or(FormatError::TooLarge)?;
333 let value_len = size::checked_sum([
334 METADATA_BLOCK_PICTURE_KEY.len() as u64,
335 b64_prefix_len,
336 b64_image_len,
337 ])?;
338 if value_len > u64::from(u32::MAX) {
339 return Err(FormatError::TooLarge);
340 }
341 }
342 if header.codec == Codec::Opus {
343 Ok(vec![
344 vec![PayloadChunk::Bytes(header.packets[0].clone())],
345 comment_packet_chunks(b"OpusTags", tags, arts, false)?,
346 ])
347 } else {
348 Ok(vec![
349 vec![PayloadChunk::Bytes(header.packets[0].clone())],
350 comment_packet_chunks(b"\x03vorbis", tags, arts, true)?,
351 vec![PayloadChunk::Bytes(header.packets[2].clone())],
352 ])
353 }
354 }
355 Codec::OggFlac => oggflac_packets_with_art(header, tags, arts),
356 }
357}
358
359fn comment_packet_chunks(
365 magic: &[u8],
366 tags: &[TagInput],
367 arts: &[OggArt],
368 framing_bit: bool,
369) -> Result<Vec<PayloadChunk>> {
370 let text_body = crate::vorbiscomment::build(tags)?; let vendor_len = u32::from_le_bytes(text_body[0..4].try_into().unwrap()) as usize;
372 let count_pos = 4 + vendor_len;
373 let text_count = u32::from_le_bytes(text_body[count_pos..count_pos + 4].try_into().unwrap());
374 let mut leading = text_body.clone();
375 let new_count = text_count + u32::try_from(arts.len()).map_err(|_| FormatError::TooLarge)?;
376 leading[count_pos..count_pos + 4].copy_from_slice(&new_count.to_le_bytes());
377
378 let mut chunks: Vec<PayloadChunk> = Vec::new();
379 let mut head = magic.to_vec();
380 head.extend_from_slice(&leading);
381
382 for art in arts {
383 let prefix = picture_prefix(art.meta)?;
384 let b64_prefix = b64_encode(&prefix);
385 let b64_image_len =
386 b64_len_checked(art.meta.data_len.get()).ok_or(FormatError::TooLarge)?;
387 let value_len = size::checked_sum([
388 METADATA_BLOCK_PICTURE_KEY.len() as u64,
389 b64_prefix.len() as u64,
390 b64_image_len,
391 ])?;
392 head.extend_from_slice(
393 &u32::try_from(value_len)
394 .map_err(|_| FormatError::TooLarge)?
395 .to_le_bytes(),
396 );
397 head.extend_from_slice(METADATA_BLOCK_PICTURE_KEY);
398 head.extend_from_slice(&b64_prefix);
399 chunks.push(PayloadChunk::Bytes(std::mem::take(&mut head)));
400 chunks.push(PayloadChunk::Art {
401 art_id: art.meta.art_id,
402 base64: true,
403 art_total: art.meta.data_len.get(),
404 });
405 }
406 if framing_bit {
407 head.push(0x01);
408 }
409 if !head.is_empty() {
410 chunks.push(PayloadChunk::Bytes(head));
411 }
412 Ok(chunks)
413}
414
415fn oggflac_packets_with_art(
419 header: &OggHeader,
420 tags: &[TagInput],
421 arts: &[OggArt],
422) -> Result<Vec<Vec<PayloadChunk>>> {
423 if header.packets.is_empty() {
424 return Err(FormatError::Malformed);
425 }
426 let mut structural: Vec<Vec<u8>> = Vec::new();
427 for pkt in header.packets.iter().skip(1) {
428 if !pkt.is_empty() && matches!(pkt[0] & 0x7F, 2 | 3 | 5) {
429 structural.push(pkt.clone());
430 }
431 }
432
433 let vc = crate::vorbiscomment::build(tags)?;
434 if vc.len() as u64 > crate::flac::MAX_BLOCK_BODY {
435 return Err(FormatError::TooLarge);
436 }
437 let mut comment = Vec::new();
438 crate::flac::push_block_header(&mut comment, 4, vc.len(), false)?;
439 comment.extend_from_slice(&vc);
440
441 let following_count = structural.len() + 1 + arts.len();
442 let count = u16::try_from(following_count).map_err(|_| FormatError::TooLarge)?;
443
444 let mut block_packets: Vec<Vec<PayloadChunk>> = Vec::new();
445 for s in &structural {
446 block_packets.push(vec![PayloadChunk::Bytes(s.clone())]);
447 }
448 block_packets.push(vec![PayloadChunk::Bytes(comment)]);
449 for art in arts {
450 let prefix = picture_prefix(art.meta)?;
451 let body_len = size::checked_add(prefix.len() as u64, art.meta.data_len.get())?;
452 if body_len > crate::flac::MAX_BLOCK_BODY {
453 return Err(FormatError::TooLarge);
454 }
455 let mut blk = Vec::new();
456 crate::flac::push_block_header(&mut blk, 6, crate::convert::usize_from(body_len), false)?;
457 blk.extend_from_slice(&prefix);
458 block_packets.push(vec![
459 PayloadChunk::Bytes(blk),
460 PayloadChunk::Art {
461 art_id: art.meta.art_id,
462 base64: false,
463 art_total: art.meta.data_len.get(),
464 },
465 ]);
466 }
467
468 let n = block_packets.len();
469 for (i, bp) in block_packets.iter_mut().enumerate() {
470 if let Some(PayloadChunk::Bytes(b)) = bp.first_mut() {
471 if i + 1 == n {
472 b[0] |= 0x80;
473 } else {
474 b[0] &= 0x7F;
475 }
476 }
477 }
478
479 let mut mapping = header.packets[0].clone();
480 if mapping.len() < 9 {
481 return Err(FormatError::Malformed);
482 }
483 mapping[7..9].copy_from_slice(&count.to_be_bytes());
484
485 let mut out = vec![vec![PayloadChunk::Bytes(mapping)]];
486 out.extend(block_packets);
487 Ok(out)
488}
489
490#[doc(hidden)]
491pub mod page_test_support {
492 pub use crate::ogg::page::{build_header as build_header_pub, lace_packet as lace_packet_pub};
493
494 pub fn vorbis_body_empty() -> Vec<u8> {
496 crate::vorbiscomment::build(&[]).unwrap()
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503 use crate::ogg::page::{build_header, lace_packet};
504
505 fn opus_headers() -> Vec<u8> {
506 let head = b"OpusHead\x01\x02\x38\x01\x80\xbb\x00\x00\x00\x00\x00".to_vec();
507 let tags = b"OpusTags\x06\x00\x00\x00musefs\x00\x00\x00\x00".to_vec();
508 let (bytes, _) = build_header(0x1234, &[&head, &tags]);
509 bytes
510 }
511
512 #[test]
513 fn locate_audio_reports_bounds() {
514 let mut data = opus_headers();
515 let header_len = data.len();
516 let (audio, _) = crate::ogg::page::lace_packet(0x1234, 2, false, 960, &[0u8; 120]);
517 data.extend_from_slice(&audio);
518
519 let scan = locate_audio(&data).unwrap();
520 assert_eq!(scan.codec, Codec::Opus);
521 assert_eq!(scan.audio_offset, header_len as u64);
522 assert_eq!(scan.audio_length, (data.len() - header_len) as u64);
523 }
524
525 #[test]
526 fn reads_opus_header() {
527 let mut data = opus_headers();
528 let (audio, _) = lace_packet(0x1234, 2, false, 960, &[0u8; 100]);
530 let header_len = data.len();
531 data.extend_from_slice(&audio);
532
533 let h = read_header(&data).unwrap();
534 assert_eq!(h.codec, Codec::Opus);
535 assert_eq!(h.serial, 0x1234);
536 assert_eq!(h.packets.len(), 2);
537 assert_eq!(h.audio_offset, header_len as u64);
538 assert_eq!(h.header_pages, 2);
539 }
540
541 #[test]
542 fn oggflac_following_packets_accepts_minimal_9_byte_packet() {
543 let mut pkt = [0u8; 9];
547 pkt[7] = 0x00;
548 pkt[8] = 0x03; assert_eq!(oggflac_following_packets(&pkt).unwrap(), 3);
550 }
551
552 #[test]
553 fn comment_body_accepts_packet_with_empty_body() {
554 assert!(comment_body(Codec::Opus, b"OpusTags").unwrap().is_empty());
558 }
559
560 #[test]
561 fn read_tags_opus() {
562 let body =
564 crate::vorbiscomment::build(&[crate::input::TagInput::new("title", "Sun")]).unwrap();
565 let mut tags_pkt = b"OpusTags".to_vec();
566 tags_pkt.extend_from_slice(&body);
567 let head = b"OpusHead\x01\x02\x38\x01\x80\xbb\x00\x00\x00\x00\x00".to_vec();
568 let (mut data, _) = crate::ogg::page::build_header(7, &[&head, &tags_pkt]);
569 let (audio, _) = crate::ogg::page::lace_packet(7, 2, false, 960, &[0u8; 50]);
570 data.extend_from_slice(&audio);
571
572 let tags = read_tags(&data).unwrap();
573 assert_eq!(tags, vec![("title".to_string(), "Sun".to_string())]);
574 }
575
576 #[test]
577 fn read_tags_excludes_metadata_block_picture() {
578 let mut block = Vec::new();
581 block.extend_from_slice(&3u32.to_be_bytes()); block.extend_from_slice(&9u32.to_be_bytes());
583 block.extend_from_slice(b"image/png");
584 block.extend_from_slice(&0u32.to_be_bytes()); block.extend_from_slice(&1u32.to_be_bytes()); block.extend_from_slice(&1u32.to_be_bytes()); block.extend_from_slice(&8u32.to_be_bytes()); block.extend_from_slice(&0u32.to_be_bytes()); block.extend_from_slice(&1u32.to_be_bytes()); block.push(0xAB);
591 let pic_value = base64::engine::general_purpose::STANDARD.encode(&block);
592
593 let body = crate::vorbiscomment::build(&[
594 crate::input::TagInput::new("title", "Sun"),
595 crate::input::TagInput::new("METADATA_BLOCK_PICTURE", &pic_value),
596 ])
597 .unwrap();
598 let mut tags_pkt = b"OpusTags".to_vec();
599 tags_pkt.extend_from_slice(&body);
600 let head = b"OpusHead\x01\x02\x38\x01\x80\xbb\x00\x00\x00\x00\x00".to_vec();
601 let (mut data, _) = crate::ogg::page::build_header(7, &[&head, &tags_pkt]);
602 let (audio, _) = crate::ogg::page::lace_packet(7, 2, false, 960, &[0u8; 50]);
603 data.extend_from_slice(&audio);
604
605 let tags = read_tags(&data).unwrap();
607 assert_eq!(tags, vec![("title".to_string(), "Sun".to_string())]);
608 let pics = read_pictures(&data).unwrap();
610 assert_eq!(pics.len(), 1);
611 assert_eq!(pics[0].data, vec![0xAB]);
612 }
613
614 #[test]
615 fn synthesize_opus_emits_valid_header_and_audio_segment() {
616 let mut data = opus_headers();
617 let scan = locate_audio({
618 let (audio, _) = crate::ogg::page::lace_packet(0x1234, 2, false, 960, &[0u8; 80]);
619 data.extend_from_slice(&audio);
620 &data
621 })
622 .unwrap();
623 let header = read_metadata(&data[..crate::convert::usize_from(scan.audio_offset)]).unwrap();
624
625 let layout = synthesize_layout(
626 &header,
627 scan.audio_offset,
628 scan.audio_length,
629 &[TagInput::new("album", "Geogaddi")],
630 &[],
631 &MapArtSource::default(),
632 )
633 .unwrap();
634
635 let mut header_bytes: Vec<u8> = Vec::new();
637 let mut audio_seg = None;
638 for seg in layout.segments() {
639 match seg {
640 Segment::Inline(b) => header_bytes.extend_from_slice(b),
641 Segment::OggAudio { offset, len, .. } => {
642 audio_seg = Some((*offset, *len));
643 break;
644 }
645 other => panic!("unexpected segment {other:?}"),
646 }
647 }
648 let h = read_header(&header_bytes).unwrap();
649 assert_eq!(h.codec, Codec::Opus);
650 let body = comment_body(Codec::Opus, &h.packets[1]).unwrap();
651 let tags = crate::vorbiscomment::parse(body).unwrap();
652 assert_eq!(tags, vec![("album".to_string(), "Geogaddi".to_string())]);
653 let (offset, len) = audio_seg.expect("expected OggAudio segment");
654 assert_eq!(offset, scan.audio_offset);
655 assert_eq!(len, scan.audio_length);
656 }
657
658 #[test]
659 fn synthesize_emits_nonzero_seq_delta_when_header_page_count_changes() {
660 let mut data = opus_headers();
664 let (audio, _) = crate::ogg::page::lace_packet(0x1234, 2, false, 960, &[0u8; 80]);
665 data.extend_from_slice(&audio);
666 let scan = locate_audio(&data).unwrap();
667 let mut header =
668 read_metadata(&data[..crate::convert::usize_from(scan.audio_offset)]).unwrap();
669
670 let baseline = synthesize_layout(
672 &header,
673 scan.audio_offset,
674 scan.audio_length,
675 &[],
676 &[],
677 &MapArtSource::default(),
678 )
679 .unwrap();
680 let synth_pages = baseline
681 .segments()
682 .iter()
683 .filter(|s| matches!(s, Segment::Inline(_)))
684 .count();
685 assert!(synth_pages >= 1);
686
687 let original_pages = u32::try_from(synth_pages).unwrap() + 3;
690 header.header_pages = original_pages;
691 let layout = synthesize_layout(
692 &header,
693 scan.audio_offset,
694 scan.audio_length,
695 &[],
696 &[],
697 &MapArtSource::default(),
698 )
699 .unwrap();
700 let delta = layout
701 .segments()
702 .iter()
703 .find_map(|s| match s {
704 Segment::OggAudio { seq_delta, .. } => Some(*seq_delta),
705 _ => None,
706 })
707 .expect("expected an OggAudio segment");
708 assert_eq!(
709 delta,
710 i64::try_from(synth_pages).unwrap() - i64::from(original_pages),
711 "seq_delta must be synthesized pages minus original header pages"
712 );
713 assert_eq!(delta, -3);
714 }
715
716 fn vorbis_headers_with(setup: &[u8]) -> Vec<u8> {
717 let mut id = b"\x01vorbis".to_vec();
719 id.extend_from_slice(&0u32.to_le_bytes()); id.push(2); id.extend_from_slice(&44100u32.to_le_bytes()); id.extend_from_slice(&0u32.to_le_bytes()); id.extend_from_slice(&128_000u32.to_le_bytes()); id.extend_from_slice(&0u32.to_le_bytes()); id.push(0xB8); id.push(0x01); let mut comment = b"\x03vorbis".to_vec();
728 comment.extend_from_slice(&crate::vorbiscomment::build(&[]).unwrap());
729 comment.push(0x01);
730 let (bytes, _) = crate::ogg::page::build_header(55, &[&id, &comment, setup]);
731 bytes
732 }
733
734 #[test]
735 fn synthesize_vorbis_preserves_setup_and_rewrites_comment() {
736 let setup = b"\x05vorbis-SETUP-CODEBOOKS-PLACEHOLDER".to_vec();
737 let mut data = vorbis_headers_with(&setup);
738 let (audio, _) = crate::ogg::page::lace_packet(55, 99, false, 1024, &[0u8; 64]);
739 data.extend_from_slice(&audio);
740
741 let scan = locate_audio(&data).unwrap();
742 assert_eq!(scan.codec, Codec::Vorbis);
743 let header = read_metadata(&data[..crate::convert::usize_from(scan.audio_offset)]).unwrap();
744 assert_eq!(header.packets[2], setup);
746
747 let layout = synthesize_layout(
748 &header,
749 scan.audio_offset,
750 scan.audio_length,
751 &[TagInput::new("artist", "Autechre")],
752 &[],
753 &MapArtSource::default(),
754 )
755 .unwrap();
756
757 let mut header_bytes: Vec<u8> = Vec::new();
758 for seg in layout.segments() {
759 match seg {
760 Segment::Inline(b) => header_bytes.extend_from_slice(b),
761 Segment::OggAudio { .. } => break,
762 other => panic!("unexpected segment {other:?}"),
763 }
764 }
765 let h = read_header(&header_bytes).unwrap();
766 assert_eq!(h.codec, Codec::Vorbis);
767 assert_eq!(h.packets[2], setup); let body = comment_body(Codec::Vorbis, &h.packets[1]).unwrap();
769 let tags = crate::vorbiscomment::parse(body).unwrap();
770 assert_eq!(tags, vec![("artist".to_string(), "Autechre".to_string())]);
771 }
772
773 #[test]
774 fn read_pictures_opus_decodes_metadata_block_picture() {
775 use base64::Engine;
776 let mut pic = Vec::new();
779 pic.extend_from_slice(&3u32.to_be_bytes());
780 let mime = b"image/png";
781 pic.extend_from_slice(&u32::try_from(mime.len()).unwrap().to_be_bytes());
782 pic.extend_from_slice(mime);
783 pic.extend_from_slice(&0u32.to_be_bytes()); pic.extend_from_slice(&1u32.to_be_bytes()); pic.extend_from_slice(&1u32.to_be_bytes()); pic.extend_from_slice(&0u32.to_be_bytes()); pic.extend_from_slice(&0u32.to_be_bytes()); let img = b"PNG";
789 pic.extend_from_slice(&u32::try_from(img.len()).unwrap().to_be_bytes());
790 pic.extend_from_slice(img);
791 let b64 = base64::engine::general_purpose::STANDARD.encode(&pic);
792
793 let mut body = Vec::new();
794 body.extend_from_slice(
795 &u32::try_from(crate::vorbiscomment::VENDOR.len())
796 .unwrap()
797 .to_le_bytes(),
798 );
799 body.extend_from_slice(crate::vorbiscomment::VENDOR.as_bytes());
800 body.extend_from_slice(&1u32.to_le_bytes()); let comment = format!("METADATA_BLOCK_PICTURE={b64}");
802 body.extend_from_slice(&u32::try_from(comment.len()).unwrap().to_le_bytes());
803 body.extend_from_slice(comment.as_bytes());
804
805 let mut tags_pkt = b"OpusTags".to_vec();
806 tags_pkt.extend_from_slice(&body);
807 let head = b"OpusHead\x01\x02\x38\x01\x80\xbb\x00\x00\x00\x00\x00".to_vec();
808 let (mut data, _) = crate::ogg::page::build_header(7, &[&head, &tags_pkt]);
809 let (audio, _) = crate::ogg::page::lace_packet(7, 2, false, 960, &[0u8; 50]);
810 data.extend_from_slice(&audio);
811
812 let pics = read_pictures(&data).unwrap();
813 assert_eq!(pics.len(), 1);
814 assert_eq!(pics[0].mime, "image/png");
815 assert_eq!(pics[0].data, b"PNG");
816 }
817
818 #[test]
819 fn read_pictures_oggflac_short_picture_packet_does_not_panic() {
820 let mut mapping = vec![0x7F];
825 mapping.extend_from_slice(b"FLAC");
826 mapping.push(1);
827 mapping.push(0);
828 mapping.extend_from_slice(&1u16.to_be_bytes()); mapping.extend_from_slice(b"fLaC");
830 let mut streaminfo = Vec::new();
831 crate::flac::push_block_header(&mut streaminfo, 0, 34, false).unwrap();
832 streaminfo.extend(std::iter::repeat_n(0u8, 34));
833 mapping.extend_from_slice(&streaminfo);
834
835 let short_picture = vec![0x06u8];
837
838 let (data, _) = crate::ogg::page::build_header(77, &[&mapping, &short_picture]);
839
840 assert_eq!(read_header(&data).unwrap().codec, Codec::OggFlac);
842 assert!(read_pictures(&data).unwrap().is_empty());
843 }
844
845 fn oggflac_headers() -> Vec<u8> {
846 let mut streaminfo = Vec::new();
849 crate::flac::push_block_header(&mut streaminfo, 0, 34, false).unwrap();
850 streaminfo.extend(std::iter::repeat_n(0u8, 34));
851
852 let mut mapping = vec![0x7F];
854 mapping.extend_from_slice(b"FLAC");
855 mapping.push(1);
856 mapping.push(0);
857 mapping.extend_from_slice(&2u16.to_be_bytes()); mapping.extend_from_slice(b"fLaC");
859 mapping.extend_from_slice(&streaminfo);
860
861 let mut seektable = Vec::new();
863 crate::flac::push_block_header(&mut seektable, 3, 18, false).unwrap();
864 seektable.extend(std::iter::repeat_n(0xEEu8, 18));
865
866 let mut old_vc = Vec::new();
868 let body = crate::vorbiscomment::build(&[crate::input::TagInput::new("x", "old")]).unwrap();
869 crate::flac::push_block_header(&mut old_vc, 4, body.len(), true).unwrap();
870 old_vc.extend_from_slice(&body);
871
872 let (bytes, _) = crate::ogg::page::build_header(77, &[&mapping, &seektable, &old_vc]);
873 bytes
874 }
875
876 #[test]
877 fn rejects_multiplexed_second_bitstream() {
878 let head = b"OpusHead\x01\x02\x38\x01\x80\xbb\x00\x00\x00\x00\x00".to_vec();
880 let (mut data, _) = crate::ogg::page::lace_packet(0x1111, 0, true, 0, &head);
881 let (other, _) = crate::ogg::page::lace_packet(
883 0x2222,
884 0,
885 true,
886 0,
887 b"OpusHead\x01\x02\x38\x01\x80\xbb\x00\x00\x00\x00\x00".as_ref(),
888 );
889 data.extend_from_slice(&other);
890 let (audio, _) = crate::ogg::page::lace_packet(0x1111, 1, false, 960, &[0u8; 50]);
892 data.extend_from_slice(&audio);
893 assert!(read_header(&data).is_err());
894 assert!(locate_audio(&data).is_err());
895 }
896
897 #[test]
898 fn synthesize_oggflac_keeps_seektable_replaces_comment_and_count() {
899 let mut data = oggflac_headers();
900 let (audio, _) = crate::ogg::page::lace_packet(77, 3, false, 4096, &[0u8; 64]);
901 data.extend_from_slice(&audio);
902
903 let scan = locate_audio(&data).unwrap();
904 assert_eq!(scan.codec, Codec::OggFlac);
905 let header = read_metadata(&data[..crate::convert::usize_from(scan.audio_offset)]).unwrap();
906
907 let layout = synthesize_layout(
908 &header,
909 scan.audio_offset,
910 scan.audio_length,
911 &[TagInput::new("title", "Kaini Industries")],
912 &[],
913 &MapArtSource::default(),
914 )
915 .unwrap();
916
917 let mut header_bytes: Vec<u8> = Vec::new();
918 for seg in layout.segments() {
919 match seg {
920 Segment::Inline(b) => header_bytes.extend_from_slice(b),
921 Segment::OggAudio { .. } => break,
922 other => panic!("unexpected segment {other:?}"),
923 }
924 }
925 let h = read_header(&header_bytes).unwrap();
926 assert_eq!(h.codec, Codec::OggFlac);
927 assert_eq!(u16::from_be_bytes([h.packets[0][7], h.packets[0][8]]), 2);
929 assert!(h.packets.iter().skip(1).any(|p| (p[0] & 0x7F) == 3));
931 let vc = h
933 .packets
934 .iter()
935 .skip(1)
936 .find(|p| (p[0] & 0x7F) == 4)
937 .unwrap();
938 assert_eq!(vc[0] & 0x80, 0x80);
939 let tags = crate::vorbiscomment::parse(&vc[4..]).unwrap();
940 assert_eq!(
941 tags,
942 vec![("title".to_string(), "Kaini Industries".to_string())]
943 );
944 }
945
946 #[test]
947 fn synthesize_opus_embeds_art_that_round_trips() {
948 let mut data = opus_headers();
949 let (audio, _) = crate::ogg::page::lace_packet(0x1234, 2, false, 960, &[0u8; 80]);
950 data.extend_from_slice(&audio);
951 let scan = locate_audio(&data).unwrap();
952 let header = read_metadata(&data[..crate::convert::usize_from(scan.audio_offset)]).unwrap();
953
954 let image: Vec<u8> = (0..5000u32).map(|i| (i % 251) as u8).collect();
955 let meta = crate::input::ArtInput {
956 art_id: 7,
957 mime: "image/jpeg".to_string(),
958 description: String::new(),
959 picture_type: crate::input::PictureType::new(3).unwrap(),
960 width: 64,
961 height: 64,
962 data_len: crate::input::BlobLen::new(image.len() as u64).unwrap(),
963 };
964 let src = MapArtSource::new([(meta.art_id, image.clone())]);
965 let layout = synthesize_layout(
966 &header,
967 scan.audio_offset,
968 scan.audio_length,
969 &[TagInput::new("title", "Cover")],
970 &[OggArt { meta: &meta }],
971 &src,
972 )
973 .unwrap();
974
975 let mut bytes = Vec::new();
978 for s in layout.segments() {
979 match s {
980 Segment::Inline(b) => bytes.extend_from_slice(b),
981 Segment::OggArtSlice {
982 offset,
983 len,
984 base64,
985 art_total,
986 ..
987 } => {
988 assert!(*base64);
989 let w = b64_window(*offset, len.get(), *art_total);
990 let raw = &image[crate::convert::usize_from(w.in_start)
991 ..crate::convert::usize_from(w.in_start + w.in_len)];
992 bytes.extend_from_slice(
993 &encode_b64_slice(raw, w.skip, crate::convert::usize_from(len.get()))
994 .expect("window lies within the encoded output"),
995 );
996 }
997 Segment::OggAudio { .. } => break, other => panic!("unexpected {other:?}"),
999 }
1000 }
1001
1002 let pics = read_pictures(&bytes).unwrap();
1003 assert_eq!(pics.len(), 1);
1004 assert_eq!(pics[0].mime, "image/jpeg");
1005 assert_eq!(pics[0].data, image);
1006 let h = read_header(&bytes).unwrap();
1007 assert_eq!(h.codec, Codec::Opus);
1008 }
1009
1010 fn materialize_header(layout: &RegionLayout, images: &[(i64, &[u8])]) -> Vec<u8> {
1013 let mut bytes = Vec::new();
1014 for s in layout.segments() {
1015 match s {
1016 Segment::Inline(b) => bytes.extend_from_slice(b),
1017 Segment::OggArtSlice {
1018 art_id,
1019 offset,
1020 len,
1021 base64,
1022 art_total,
1023 } => {
1024 let img = images.iter().find(|(id, _)| id == art_id).expect("image").1;
1025 if *base64 {
1026 let w = b64_window(*offset, len.get(), *art_total);
1027 let raw = &img[crate::convert::usize_from(w.in_start)
1028 ..crate::convert::usize_from(w.in_start + w.in_len)];
1029 bytes.extend_from_slice(
1030 &encode_b64_slice(raw, w.skip, crate::convert::usize_from(len.get()))
1031 .expect("window lies within the encoded output"),
1032 );
1033 } else {
1034 bytes.extend_from_slice(
1035 &img[crate::convert::usize_from(*offset)
1036 ..crate::convert::usize_from(*offset + len.get())],
1037 );
1038 }
1039 }
1040 Segment::OggAudio { .. } => break,
1041 other => panic!("unexpected {other:?}"),
1042 }
1043 }
1044 bytes
1045 }
1046
1047 fn art_input(art_id: i64, mime: &str, len: usize) -> crate::input::ArtInput {
1048 crate::input::ArtInput {
1049 art_id,
1050 mime: mime.to_string(),
1051 description: String::new(),
1052 picture_type: crate::input::PictureType::new(3).unwrap(),
1053 width: 10,
1054 height: 10,
1055 data_len: crate::input::BlobLen::new(len as u64).unwrap(),
1056 }
1057 }
1058
1059 #[test]
1060 fn synthesize_vorbis_embeds_art_that_round_trips() {
1061 let setup = b"\x05vorbis-SETUP".to_vec();
1062 let mut data = vorbis_headers_with(&setup);
1063 let (audio, _) = crate::ogg::page::lace_packet(55, 99, false, 1024, &[0u8; 64]);
1064 data.extend_from_slice(&audio);
1065 let scan = locate_audio(&data).unwrap();
1066 let header = read_metadata(&data[..crate::convert::usize_from(scan.audio_offset)]).unwrap();
1067
1068 let image: Vec<u8> = (0..4000u32).map(|i| (i % 251) as u8).collect();
1069 let meta = art_input(11, "image/png", image.len());
1070 let src = MapArtSource::new([(meta.art_id, image.clone())]);
1071 let layout = synthesize_layout(
1072 &header,
1073 scan.audio_offset,
1074 scan.audio_length,
1075 &[TagInput::new("artist", "X")],
1076 &[OggArt { meta: &meta }],
1077 &src,
1078 )
1079 .unwrap();
1080
1081 let bytes = materialize_header(&layout, &[(11, &image)]);
1082 let h = read_header(&bytes).unwrap();
1083 assert_eq!(h.codec, Codec::Vorbis);
1084 assert_eq!(h.packets[2], setup); let pics = read_pictures(&bytes).unwrap();
1086 assert_eq!(pics.len(), 1);
1087 assert_eq!(pics[0].data, image);
1088 }
1089
1090 #[test]
1091 fn synthesize_oggflac_embeds_art_that_round_trips() {
1092 let mut data = oggflac_headers();
1093 let (audio, _) = crate::ogg::page::lace_packet(77, 3, false, 4096, &[0u8; 64]);
1094 data.extend_from_slice(&audio);
1095 let scan = locate_audio(&data).unwrap();
1096 let header = read_metadata(&data[..crate::convert::usize_from(scan.audio_offset)]).unwrap();
1097
1098 let image: Vec<u8> = (0..4000u32).map(|i| (i % 251) as u8).collect();
1099 let meta = art_input(22, "image/png", image.len());
1100 let src = MapArtSource::new([(meta.art_id, image.clone())]);
1101 let layout = synthesize_layout(
1102 &header,
1103 scan.audio_offset,
1104 scan.audio_length,
1105 &[TagInput::new("title", "Y")],
1106 &[OggArt { meta: &meta }],
1107 &src,
1108 )
1109 .unwrap();
1110
1111 let bytes = materialize_header(&layout, &[(22, &image)]);
1112 let h = read_header(&bytes).unwrap();
1113 assert_eq!(h.codec, Codec::OggFlac);
1114 let pics = read_pictures(&bytes).unwrap();
1115 assert_eq!(pics.len(), 1);
1116 assert_eq!(pics[0].data, image);
1117 }
1118
1119 #[test]
1120 fn synthesize_oggflac_embeds_large_art_spanning_pages_round_trips() {
1121 let mut data = oggflac_headers();
1125 let (audio, _) = crate::ogg::page::lace_packet(77, 3, false, 4096, &[0u8; 64]);
1126 data.extend_from_slice(&audio);
1127 let scan = locate_audio(&data).unwrap();
1128 let header = read_metadata(&data[..crate::convert::usize_from(scan.audio_offset)]).unwrap();
1129
1130 let image: Vec<u8> = (0..200_000u32).map(|i| (i % 251) as u8).collect();
1131 let meta = art_input(31, "image/png", image.len());
1132 let src = MapArtSource::new([(meta.art_id, image.clone())]);
1133 let layout = synthesize_layout(
1134 &header,
1135 scan.audio_offset,
1136 scan.audio_length,
1137 &[TagInput::new("title", "Big")],
1138 &[OggArt { meta: &meta }],
1139 &src,
1140 )
1141 .unwrap();
1142
1143 let art_slices = layout
1145 .segments()
1146 .iter()
1147 .filter(|s| matches!(s, Segment::OggArtSlice { base64: false, .. }))
1148 .count();
1149 assert!(
1150 art_slices >= 2,
1151 "expected the raw art to span multiple pages, got {art_slices} slice(s)"
1152 );
1153
1154 let bytes = materialize_header(&layout, &[(31, &image)]);
1155 let h = read_header(&bytes).unwrap();
1156 assert_eq!(h.codec, Codec::OggFlac);
1157 let pics = read_pictures(&bytes).unwrap();
1158 assert_eq!(pics.len(), 1);
1159 assert_eq!(
1160 pics[0].data, image,
1161 "large art must round-trip byte-for-byte"
1162 );
1163 }
1164
1165 #[test]
1166 fn synthesize_opus_embeds_multiple_images() {
1167 let mut data = opus_headers();
1168 let (audio, _) = crate::ogg::page::lace_packet(0x1234, 2, false, 960, &[0u8; 64]);
1169 data.extend_from_slice(&audio);
1170 let scan = locate_audio(&data).unwrap();
1171 let header = read_metadata(&data[..crate::convert::usize_from(scan.audio_offset)]).unwrap();
1172
1173 let img_a: Vec<u8> = (0..3000u32).map(|i| (i % 251) as u8).collect();
1174 let img_b: Vec<u8> = (0..1500u32).map(|i| ((i * 3) % 251) as u8).collect();
1175 let meta_a = art_input(1, "image/png", img_a.len());
1176 let meta_b = art_input(2, "image/jpeg", img_b.len());
1177 let src = MapArtSource::new([
1178 (meta_a.art_id, img_a.clone()),
1179 (meta_b.art_id, img_b.clone()),
1180 ]);
1181 let layout = synthesize_layout(
1182 &header,
1183 scan.audio_offset,
1184 scan.audio_length,
1185 &[TagInput::new("title", "Multi")],
1186 &[OggArt { meta: &meta_a }, OggArt { meta: &meta_b }],
1187 &src,
1188 )
1189 .unwrap();
1190
1191 let bytes = materialize_header(&layout, &[(1, &img_a), (2, &img_b)]);
1192 let h = read_header(&bytes).unwrap();
1193 assert_eq!(h.codec, Codec::Opus);
1194 let pics = read_pictures(&bytes).unwrap();
1195 assert_eq!(pics.len(), 2);
1196 assert_eq!(pics[0].data, img_a);
1197 assert_eq!(pics[1].data, img_b);
1198 }
1199
1200 #[test]
1201 fn oversized_full_art_value_rejected_by_build_packets() {
1202 let meta = crate::input::ArtInput {
1203 art_id: 0,
1204 mime: "image/jpeg".to_string(),
1205 description: String::new(),
1206 data_len: crate::input::BlobLen::new(u64::from(u32::MAX)).unwrap(),
1207 picture_type: crate::input::PictureType::new(3).unwrap(),
1208 width: 0,
1209 height: 0,
1210 };
1211 let art = OggArt { meta: &meta };
1212 let header = OggHeader {
1213 codec: Codec::Vorbis,
1214 serial: 0,
1215 packets: vec![vec![], vec![], vec![]],
1216 header_pages: 1,
1217 audio_offset: 0,
1218 };
1219 let result = build_packets_with_art(&header, &[], &[art]);
1220 assert!(result.is_err(), "expected Err for oversized art");
1221 }
1222
1223 #[test]
1224 fn sum_overflow_art_value_rejected_by_build_packets() {
1225 let meta = crate::input::ArtInput {
1228 art_id: 0,
1229 mime: "image/png".to_string(),
1230 description: "x".repeat(256),
1231 data_len: crate::input::BlobLen::new(3_221_225_470).unwrap(),
1232 picture_type: crate::input::PictureType::new(3).unwrap(),
1233 width: 0,
1234 height: 0,
1235 };
1236 let art = OggArt { meta: &meta };
1237 let header = OggHeader {
1238 codec: Codec::Vorbis,
1239 serial: 0,
1240 packets: vec![vec![], vec![], vec![]],
1241 header_pages: 1,
1242 audio_offset: 0,
1243 };
1244 let result = build_packets_with_art(&header, &[], &[art]);
1245 assert!(
1246 result.is_err(),
1247 "expected Err when key + b64(prefix) + b64(data) overflows u32"
1248 );
1249 }
1250
1251 #[test]
1252 fn art_value_at_u32_max_boundary_is_accepted_by_build_packets() {
1253 let meta = crate::input::ArtInput {
1260 art_id: 0,
1261 mime: "image/png".to_string(),
1262 description: String::new(),
1263 data_len: crate::input::BlobLen::new(3_221_225_412).unwrap(),
1264 picture_type: crate::input::PictureType::new(3).unwrap(),
1265 width: 0,
1266 height: 0,
1267 };
1268 let art = OggArt { meta: &meta };
1269 let header = OggHeader {
1270 codec: Codec::Vorbis,
1271 serial: 0,
1272 packets: vec![vec![], vec![], vec![]],
1273 header_pages: 1,
1274 audio_offset: 0,
1275 };
1276 let accepted = build_packets_with_art(&header, &[], &[art]).is_ok();
1277 assert!(
1278 accepted,
1279 "value_len exactly u32::MAX must be accepted by build_packets_with_art"
1280 );
1281 }
1282
1283 #[test]
1284 fn near_u64_max_art_value_rejected_by_build_packets() {
1285 let meta = crate::input::ArtInput {
1289 art_id: 0,
1290 mime: "image/jpeg".to_string(),
1291 description: String::new(),
1292 data_len: crate::input::BlobLen::new(u64::MAX).unwrap(),
1293 picture_type: crate::input::PictureType::new(3).unwrap(),
1294 width: 0,
1295 height: 0,
1296 };
1297 let art = OggArt { meta: &meta };
1298 let header = OggHeader {
1299 codec: Codec::Vorbis,
1300 serial: 0,
1301 packets: vec![vec![], vec![], vec![]],
1302 header_pages: 1,
1303 audio_offset: 0,
1304 };
1305 let result = build_packets_with_art(&header, &[], &[art]);
1306 let is_too_large = matches!(&result, Err(FormatError::TooLarge));
1307 assert!(is_too_large, "expected Err(TooLarge) for near-u64::MAX art");
1308 }
1309
1310 #[test]
1311 fn near_u64_max_art_value_rejected_by_oggflac_build_packets() {
1312 let meta = crate::input::ArtInput {
1318 art_id: 0,
1319 mime: "image/jpeg".to_string(),
1320 description: String::new(),
1321 data_len: crate::input::BlobLen::new(u64::MAX).unwrap(),
1322 picture_type: crate::input::PictureType::new(3).unwrap(),
1323 width: 0,
1324 height: 0,
1325 };
1326 let art = OggArt { meta: &meta };
1327 let header = OggHeader {
1328 codec: Codec::OggFlac,
1329 serial: 0,
1330 packets: vec![vec![0x7F]],
1331 header_pages: 1,
1332 audio_offset: 0,
1333 };
1334 let result = build_packets_with_art(&header, &[], &[art]);
1335 let is_too_large = matches!(&result, Err(FormatError::TooLarge));
1336 assert!(is_too_large, "expected Err(TooLarge) for near-u64::MAX art");
1337 }
1338
1339 #[test]
1340 fn picture_prefix_is_3_aligned_and_declares_image_len() {
1341 let art = crate::input::ArtInput {
1342 art_id: 1,
1343 mime: "image/png".to_string(), description: String::new(),
1345 picture_type: crate::input::PictureType::new(3).unwrap(),
1346 width: 1,
1347 height: 1,
1348 data_len: crate::input::BlobLen::new(12345).unwrap(),
1349 };
1350 let p = picture_prefix(&art).unwrap();
1351 assert_eq!(p.len() % 3, 0);
1352 let dl = u32::from_be_bytes(p[p.len() - 4..].try_into().unwrap());
1354 assert_eq!(dl, 12345);
1355 let mut body = p.clone();
1358 body.extend(std::iter::repeat_n(0u8, 12345));
1359 let pic = crate::flac::parse_picture_block(&body).unwrap();
1360 assert_eq!(pic.mime, "image/png");
1361 assert_eq!(pic.picture_type.get(), 3);
1362 }
1363
1364 #[test]
1365 fn detect_codec_matches_each_magic_and_rejects_others() {
1366 assert_eq!(detect_codec(b"OpusHead........").unwrap(), Codec::Opus);
1367 assert_eq!(detect_codec(b"\x01vorbis...").unwrap(), Codec::Vorbis);
1368 assert_eq!(detect_codec(b"\x7FFLAC...").unwrap(), Codec::OggFlac);
1369 assert!(detect_codec(b"OpusHea").is_err()); assert!(detect_codec(b"XXXXXXXX").is_err()); assert!(detect_codec(b"\x01vorbi").is_err()); }
1375
1376 #[test]
1377 fn comment_body_strips_each_codec_prefix_and_guards_length() {
1378 assert_eq!(comment_body(Codec::Opus, b"OpusTagsBODY").unwrap(), b"BODY");
1379 assert_eq!(
1380 comment_body(Codec::Vorbis, b"\x03vorbisBODY").unwrap(),
1381 b"BODY"
1382 );
1383 assert_eq!(
1384 comment_body(Codec::OggFlac, b"\x04\x00\x00\x00BODY").unwrap(),
1385 b"BODY"
1386 );
1387 assert!(comment_body(Codec::Opus, b"OpusTa").is_err());
1389 assert!(comment_body(Codec::OggFlac, b"\x04\x00\x00").is_err());
1390 }
1391
1392 #[test]
1393 fn oggflac_following_packets_reads_be_count_and_guards_length() {
1394 let pkt = b"\x7FFLAC\x01\x00\x00\x05rest";
1396 assert_eq!(oggflac_following_packets(pkt).unwrap(), 5);
1397 assert!(oggflac_following_packets(b"\x7FFLAC\x01\x00").is_err()); }
1399
1400 #[test]
1401 fn oggflac_comment_block_size_boundary_is_inclusive() {
1402 let header = OggHeader {
1407 codec: Codec::OggFlac,
1408 serial: 1,
1409 packets: vec![vec![0x7F; 9]],
1410 header_pages: 1,
1411 audio_offset: 0,
1412 };
1413 let overhead = crate::vorbiscomment::build(&[crate::input::TagInput::new("title", "")])
1414 .unwrap()
1415 .len() as u64;
1416 let at_limit = "x".repeat(crate::convert::usize_from(
1417 crate::flac::MAX_BLOCK_BODY - overhead,
1418 ));
1419 let tags = [crate::input::TagInput::new("title", at_limit.as_str())];
1420 assert!(oggflac_packets_with_art(&header, &tags, &[]).is_ok());
1421 let over = format!("{at_limit}x");
1423 let tags = [crate::input::TagInput::new("title", over.as_str())];
1424 assert!(matches!(
1425 oggflac_packets_with_art(&header, &tags, &[]),
1426 Err(FormatError::TooLarge)
1427 ));
1428 }
1429
1430 #[test]
1431 fn oggflac_picture_block_size_boundary_is_inclusive() {
1432 let header = OggHeader {
1438 codec: Codec::OggFlac,
1439 serial: 1,
1440 packets: vec![vec![0x7F; 9]],
1441 header_pages: 1,
1442 audio_offset: 0,
1443 };
1444 let mk = |data_len: u64| crate::input::ArtInput {
1445 art_id: 1,
1446 mime: "image/png".to_string(),
1447 description: String::new(),
1448 picture_type: crate::input::PictureType::new(3).unwrap(),
1449 width: 0,
1450 height: 0,
1451 data_len: crate::input::BlobLen::new(data_len).unwrap(),
1452 };
1453 let framing_len = picture_prefix(&mk(1)).unwrap().len() as u64;
1454 let at_limit = mk(crate::flac::MAX_BLOCK_BODY - framing_len);
1455 let arts = [OggArt { meta: &at_limit }];
1456 assert!(oggflac_packets_with_art(&header, &[], &arts).is_ok());
1457 let over = mk(crate::flac::MAX_BLOCK_BODY - framing_len + 1);
1459 let arts = [OggArt { meta: &over }];
1460 assert!(matches!(
1461 oggflac_packets_with_art(&header, &[], &arts),
1462 Err(FormatError::TooLarge)
1463 ));
1464 }
1465
1466 #[test]
1467 fn comment_packet_index_locates_the_comment_block() {
1468 let opus = OggHeader {
1471 codec: Codec::Opus,
1472 serial: 1,
1473 packets: vec![vec![], vec![]],
1474 header_pages: 1,
1475 audio_offset: 0,
1476 };
1477 assert_eq!(comment_packet_index(&opus), 1);
1478
1479 let oggflac = OggHeader {
1481 codec: Codec::OggFlac,
1482 serial: 1,
1483 packets: vec![vec![0x7F], vec![0x01], vec![0x84]], header_pages: 1,
1485 audio_offset: 0,
1486 };
1487 assert_eq!(comment_packet_index(&oggflac), 2);
1488 let none = OggHeader {
1490 codec: Codec::OggFlac,
1491 serial: 1,
1492 packets: vec![vec![0x7F], vec![0x01], vec![0x05]],
1493 header_pages: 1,
1494 audio_offset: 0,
1495 };
1496 assert_eq!(comment_packet_index(&none), 0);
1497 }
1498
1499 #[test]
1500 fn locate_audio_accepts_empty_audio_region() {
1501 let file = opus_headers();
1504 let scan = locate_audio(&file).unwrap();
1505 assert_eq!(scan.codec, Codec::Opus);
1506 assert_eq!(scan.audio_offset, file.len() as u64);
1507 assert_eq!(scan.audio_length, 0);
1508 }
1509
1510 #[test]
1511 fn picture_prefix_declared_desc_len_pins_padding() {
1512 let art = crate::input::ArtInput {
1513 art_id: 1,
1514 mime: "image/png".into(), description: "x".into(), picture_type: crate::input::PictureType::new(3).unwrap(),
1517 width: 1,
1518 height: 1,
1519 data_len: crate::input::BlobLen::new(100).unwrap(),
1520 };
1521 let prefix = picture_prefix(&art).unwrap();
1522 assert_eq!(prefix.len() % 3, 0);
1523 let off = 8 + art.mime.len();
1526 let declared = u32::from_be_bytes(prefix[off..off + 4].try_into().unwrap());
1527 let pad = declared - u32::try_from(art.description.len()).unwrap();
1528 assert!(pad <= 2, "pad must be 0..=2, got {pad}");
1529 assert_eq!(pad, 0, "base % 3 == 0 implies pad 0");
1530 }
1531
1532 #[test]
1533 fn synthesis_reads_art_in_page_bounded_windows() {
1534 use std::cell::Cell;
1535 struct Counting<'a> {
1536 inner: MapArtSource,
1537 max: &'a Cell<usize>,
1538 }
1539 impl ArtSource for Counting<'_> {
1540 fn read_window(&self, art_id: i64, offset: u64, buf: &mut [u8]) -> crate::Result<()> {
1541 self.max.set(self.max.get().max(buf.len()));
1542 self.inner.read_window(art_id, offset, buf)
1543 }
1544 }
1545
1546 let mut data = opus_headers();
1548 let scan = locate_audio({
1549 let (audio, _) = crate::ogg::page::lace_packet(0x1234, 2, false, 960, &[0u8; 80]);
1550 data.extend_from_slice(&audio);
1551 &data
1552 })
1553 .unwrap();
1554 let header = read_metadata(&data[..crate::convert::usize_from(scan.audio_offset)]).unwrap();
1555
1556 let image: Vec<u8> = (0..500_000u32).map(|i| (i % 251) as u8).collect();
1557 let meta = crate::input::ArtInput {
1558 art_id: 7,
1559 mime: "image/jpeg".to_string(),
1560 description: String::new(),
1561 picture_type: crate::input::PictureType::new(3).unwrap(),
1562 width: 0,
1563 height: 0,
1564 data_len: crate::input::BlobLen::new(image.len() as u64).unwrap(),
1565 };
1566 let max = Cell::new(0usize);
1567 let src = Counting {
1568 inner: MapArtSource::new([(7i64, image.clone())]),
1569 max: &max,
1570 };
1571 synthesize_layout(
1572 &header,
1573 scan.audio_offset,
1574 scan.audio_length,
1575 &[],
1576 &[OggArt { meta: &meta }],
1577 &src,
1578 )
1579 .unwrap();
1580 assert!(
1583 max.get() > 0 && max.get() <= 65_025,
1584 "max single read was {}",
1585 max.get()
1586 );
1587 }
1588}
1589
1590#[cfg(test)]
1591mod page_test_support_tests {
1592 #[test]
1597 fn vorbis_body_empty_is_a_parseable_empty_comment() {
1598 let body = super::page_test_support::vorbis_body_empty();
1599 let parsed = crate::vorbiscomment::parse(&body).unwrap();
1600 assert!(parsed.is_empty());
1601 }
1602}
1603
1604#[cfg(test)]
1605mod bounded_tests {
1606 use super::*;
1607 use crate::ogg::page_test_support::{build_header_pub, lace_packet_pub, vorbis_body_empty};
1608
1609 fn opus_stream() -> (Vec<u8>, u64) {
1616 let head = b"OpusHead\x01\x02\x38\x01\x80\xbb\x00\x00\x00\x00\x00".to_vec();
1617 let mut tags = b"OpusTags".to_vec();
1618 tags.extend_from_slice(&vorbis_body_empty());
1619 let serial = 0x1234;
1620 let (mut v, _) = build_header_pub(serial, &[&head, &tags]);
1621 let audio_offset = v.len() as u64;
1622 let (audio, _) = lace_packet_pub(serial, 2, false, 960, &[0u8; 100]);
1623 v.extend_from_slice(&audio);
1624 (v, audio_offset)
1625 }
1626
1627 #[test]
1628 fn read_metadata_bounded_complete_when_prefix_covers_header() {
1629 let (full, audio_offset) = opus_stream();
1630 let file_len = full.len() as u64;
1631 let prefix = &full[..crate::convert::usize_from(audio_offset)]; match read_metadata_bounded(prefix, file_len).unwrap() {
1633 Extent::Complete(h) => assert_eq!(h.audio_offset, audio_offset),
1634 other @ Extent::NeedMore { .. } => panic!("expected Complete, got {other:?}"),
1635 }
1636 }
1637
1638 #[test]
1639 fn read_metadata_bounded_needmore_when_header_truncated() {
1640 let (full, _audio_offset) = opus_stream();
1641 let file_len = full.len() as u64;
1642 let prefix = &full[..20]; match read_metadata_bounded(prefix, file_len).unwrap() {
1644 Extent::NeedMore { up_to } => assert!(up_to > 20 && up_to <= file_len),
1645 other @ Extent::Complete(_) => panic!("expected NeedMore, got {other:?}"),
1646 }
1647 }
1648
1649 #[test]
1650 fn read_metadata_bounded_errors_when_whole_file_is_unparseable() {
1651 let bad: &[u8] = b"not an ogg stream at all"; assert!(read_header(bad).is_err());
1657 let len = bad.len() as u64;
1658 match read_metadata_bounded(bad, len) {
1662 Err(_) => {}
1663 Ok(other) => panic!("expected Err when whole file unparseable, got {other:?}"),
1664 }
1665 }
1666
1667 #[test]
1668 fn read_metadata_bounded_doubles_window_exactly() {
1669 let buf = vec![0u8; 100_000]; assert!(read_header(&buf).is_err());
1675 let file_len = 10_000_000u64;
1676 match read_metadata_bounded(&buf, file_len).unwrap() {
1677 Extent::NeedMore { up_to } => assert_eq!(up_to, 200_000),
1678 other @ Extent::Complete(_) => panic!("expected NeedMore, got {other:?}"),
1679 }
1680 }
1681
1682 #[test]
1683 fn read_metadata_bounded_floor_is_64kib_for_small_prefix() {
1684 let buf = vec![0u8; 100]; assert!(read_header(&buf).is_err());
1690 let file_len = 10_000_000u64;
1691 match read_metadata_bounded(&buf, file_len).unwrap() {
1694 Extent::NeedMore { up_to } => assert_eq!(up_to, 65_536),
1695 other @ Extent::Complete(_) => panic!("expected NeedMore, got {other:?}"),
1696 }
1697 }
1698
1699 #[test]
1700 fn read_metadata_bounded_grows_when_truncated_prefix_shorter_than_file() {
1701 let (full, _audio_offset) = opus_stream();
1705 let file_len = full.len() as u64;
1706 let prefix = &full[..10]; assert!(read_header(prefix).is_err());
1708 match read_metadata_bounded(prefix, file_len).unwrap() {
1709 Extent::NeedMore { up_to } => assert!(up_to > prefix.len() as u64),
1710 other @ Extent::Complete(_) => panic!("expected NeedMore, got {other:?}"),
1711 }
1712 }
1713}