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.is_empty() && (pkt[0] & 0x7F) == 6 {
189 out.push(crate::flac::parse_picture_block(&pkt[4..])?);
191 }
192 }
193 }
194 }
195 Ok(out)
196}
197
198#[derive(Debug, Clone, PartialEq, Eq)]
200pub struct OggScan {
201 pub codec: Codec,
202 pub audio_offset: u64,
203 pub audio_length: u64,
204}
205
206pub fn locate_audio(data: &[u8]) -> Result<OggScan> {
207 let header = read_header(data)?;
208 if header.audio_offset > data.len() as u64 {
209 return Err(FormatError::Malformed);
210 }
211 Ok(OggScan {
212 codec: header.codec,
213 audio_offset: header.audio_offset,
214 audio_length: data.len() as u64 - header.audio_offset,
215 })
216}
217
218pub fn read_metadata(front: &[u8]) -> Result<OggHeader> {
221 read_header(front)
222}
223
224pub fn read_metadata_bounded(prefix: &[u8], file_len: u64) -> Result<Extent<OggHeader>> {
231 match read_header(prefix) {
232 Ok(header) => Ok(Extent::Complete(header)),
233 Err(_) if (prefix.len() as u64) < file_len => {
238 let grown = ((prefix.len() as u64).saturating_mul(2)).max(64 * 1024);
239 Ok(Extent::NeedMore {
240 up_to: grown.min(file_len),
241 })
242 }
243 Err(e) => Err(e),
244 }
245}
246
247use crate::input::TagInput;
248use crate::layout::{RegionLayout, Segment};
249
250pub fn synthesize_layout(
251 header: &OggHeader,
252 audio_offset: u64,
253 audio_length: u64,
254 tags: &[TagInput],
255 arts: &[OggArt],
256 src: &dyn ArtSource,
257) -> Result<RegionLayout> {
258 let arts: Vec<OggArt> = arts.to_vec();
259 let packet_chunks = build_packets_with_art(header, tags, &arts)?;
260 let mut segments: Vec<Segment> = Vec::new();
261 let mut seq = 0u32;
262 for (i, chunks) in packet_chunks.iter().enumerate() {
263 let (segs, used) =
264 crate::ogg::page::lace_chunks_to_segments(header.serial, seq, i == 0, chunks, src)?;
265 segments.extend(segs);
266 seq += used;
267 }
268 let seq_delta = i64::from(seq) - i64::from(header.header_pages);
269 segments.push(Segment::OggAudio {
270 offset: audio_offset,
271 len: audio_length,
272 seq_delta,
273 });
274 Ok(RegionLayout::validated(segments)?)
275}
276
277fn picture_prefix(art: &crate::input::ArtInput) -> Result<Vec<u8>> {
284 let base = 32 + art.mime.len() + art.description.len();
287 let pad = (3 - base % 3) % 3;
288 let description = format!("{}{}", art.description, " ".repeat(pad));
289
290 let mut out = Vec::new();
291 out.extend_from_slice(&art.picture_type.get().to_be_bytes());
292 out.extend_from_slice(
293 &u32::try_from(art.mime.len())
294 .map_err(|_| FormatError::TooLarge)?
295 .to_be_bytes(),
296 );
297 out.extend_from_slice(art.mime.as_bytes());
298 out.extend_from_slice(
299 &u32::try_from(description.len())
300 .map_err(|_| FormatError::TooLarge)?
301 .to_be_bytes(),
302 );
303 out.extend_from_slice(description.as_bytes());
304 out.extend_from_slice(&art.width.to_be_bytes());
305 out.extend_from_slice(&art.height.to_be_bytes());
306 out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(
309 &u32::try_from(art.data_len.get())
310 .map_err(|_| FormatError::TooLarge)?
311 .to_be_bytes(),
312 ); Ok(out)
314}
315
316use crate::ogg::page::PayloadChunk;
317use base64::Engine;
318
319#[derive(Clone, Copy)]
322pub struct OggArt<'a> {
323 pub meta: &'a crate::input::ArtInput,
324}
325
326fn b64_encode(bytes: &[u8]) -> Vec<u8> {
327 base64::engine::general_purpose::STANDARD
328 .encode(bytes)
329 .into_bytes()
330}
331
332fn build_packets_with_art(
336 header: &OggHeader,
337 tags: &[TagInput],
338 arts: &[OggArt],
339) -> Result<Vec<Vec<PayloadChunk>>> {
340 match header.codec {
341 Codec::Opus | Codec::Vorbis => {
342 for a in arts {
347 let prefix = picture_prefix(a.meta)?;
348 let b64_prefix_len =
349 b64_len_checked(prefix.len() as u64).ok_or(FormatError::TooLarge)?;
350 let b64_image_len =
351 b64_len_checked(a.meta.data_len.get()).ok_or(FormatError::TooLarge)?;
352 let value_len = size::checked_sum([
353 METADATA_BLOCK_PICTURE_KEY.len() as u64,
354 b64_prefix_len,
355 b64_image_len,
356 ])?;
357 if value_len > u64::from(u32::MAX) {
358 return Err(FormatError::TooLarge);
359 }
360 }
361 if header.codec == Codec::Opus {
362 Ok(vec![
363 vec![PayloadChunk::Bytes(header.packets[0].clone())],
364 comment_packet_chunks(b"OpusTags", tags, arts, false)?,
365 ])
366 } else {
367 Ok(vec![
368 vec![PayloadChunk::Bytes(header.packets[0].clone())],
369 comment_packet_chunks(b"\x03vorbis", tags, arts, true)?,
370 vec![PayloadChunk::Bytes(header.packets[2].clone())],
371 ])
372 }
373 }
374 Codec::OggFlac => oggflac_packets_with_art(header, tags, arts),
375 }
376}
377
378fn comment_packet_chunks(
384 magic: &[u8],
385 tags: &[TagInput],
386 arts: &[OggArt],
387 framing_bit: bool,
388) -> Result<Vec<PayloadChunk>> {
389 let text_body = crate::vorbiscomment::build(tags)?; let vendor_len = u32::from_le_bytes(text_body[0..4].try_into().unwrap()) as usize;
391 let count_pos = 4 + vendor_len;
392 let text_count = u32::from_le_bytes(text_body[count_pos..count_pos + 4].try_into().unwrap());
393 let mut leading = text_body.clone();
394 let new_count = text_count + u32::try_from(arts.len()).map_err(|_| FormatError::TooLarge)?;
395 leading[count_pos..count_pos + 4].copy_from_slice(&new_count.to_le_bytes());
396
397 let mut chunks: Vec<PayloadChunk> = Vec::new();
398 let mut head = magic.to_vec();
399 head.extend_from_slice(&leading);
400
401 for art in arts {
402 let prefix = picture_prefix(art.meta)?;
403 let b64_prefix = b64_encode(&prefix);
404 let b64_image_len =
405 b64_len_checked(art.meta.data_len.get()).ok_or(FormatError::TooLarge)?;
406 let value_len = size::checked_sum([
407 METADATA_BLOCK_PICTURE_KEY.len() as u64,
408 b64_prefix.len() as u64,
409 b64_image_len,
410 ])?;
411 head.extend_from_slice(
412 &u32::try_from(value_len)
413 .map_err(|_| FormatError::TooLarge)?
414 .to_le_bytes(),
415 );
416 head.extend_from_slice(METADATA_BLOCK_PICTURE_KEY);
417 head.extend_from_slice(&b64_prefix);
418 chunks.push(PayloadChunk::Bytes(std::mem::take(&mut head)));
419 chunks.push(PayloadChunk::Art {
420 art_id: art.meta.art_id,
421 base64: true,
422 art_total: art.meta.data_len.get(),
423 });
424 }
425 if framing_bit {
426 head.push(0x01);
427 }
428 if !head.is_empty() {
429 chunks.push(PayloadChunk::Bytes(head));
430 }
431 Ok(chunks)
432}
433
434fn oggflac_packets_with_art(
438 header: &OggHeader,
439 tags: &[TagInput],
440 arts: &[OggArt],
441) -> Result<Vec<Vec<PayloadChunk>>> {
442 if header.packets.is_empty() {
443 return Err(FormatError::Malformed);
444 }
445 let mut structural: Vec<Vec<u8>> = Vec::new();
446 for pkt in header.packets.iter().skip(1) {
447 if !pkt.is_empty() && matches!(pkt[0] & 0x7F, 2 | 3 | 5) {
448 structural.push(pkt.clone());
449 }
450 }
451
452 let vc = crate::vorbiscomment::build(tags)?;
453 if vc.len() as u64 > crate::flac::MAX_BLOCK_BODY {
454 return Err(FormatError::TooLarge);
455 }
456 let mut comment = Vec::new();
457 crate::flac::push_block_header(&mut comment, 4, vc.len(), false)?;
458 comment.extend_from_slice(&vc);
459
460 let following_count = structural.len() + 1 + arts.len();
461 let count = u16::try_from(following_count).map_err(|_| FormatError::TooLarge)?;
462
463 let mut block_packets: Vec<Vec<PayloadChunk>> = Vec::new();
464 for s in &structural {
465 block_packets.push(vec![PayloadChunk::Bytes(s.clone())]);
466 }
467 block_packets.push(vec![PayloadChunk::Bytes(comment)]);
468 for art in arts {
469 let prefix = picture_prefix(art.meta)?;
470 let body_len = size::checked_add(prefix.len() as u64, art.meta.data_len.get())?;
471 if body_len > crate::flac::MAX_BLOCK_BODY {
472 return Err(FormatError::TooLarge);
473 }
474 let mut blk = Vec::new();
475 crate::flac::push_block_header(&mut blk, 6, crate::convert::usize_from(body_len), false)?;
476 blk.extend_from_slice(&prefix);
477 block_packets.push(vec![
478 PayloadChunk::Bytes(blk),
479 PayloadChunk::Art {
480 art_id: art.meta.art_id,
481 base64: false,
482 art_total: art.meta.data_len.get(),
483 },
484 ]);
485 }
486
487 let n = block_packets.len();
488 for (i, bp) in block_packets.iter_mut().enumerate() {
489 if let Some(PayloadChunk::Bytes(b)) = bp.first_mut() {
490 if i + 1 == n {
491 b[0] |= 0x80;
492 } else {
493 b[0] &= 0x7F;
494 }
495 }
496 }
497
498 let mut mapping = header.packets[0].clone();
499 if mapping.len() < 9 {
500 return Err(FormatError::Malformed);
501 }
502 mapping[7..9].copy_from_slice(&count.to_be_bytes());
503
504 let mut out = vec![vec![PayloadChunk::Bytes(mapping)]];
505 out.extend(block_packets);
506 Ok(out)
507}
508
509#[doc(hidden)]
510pub mod page_test_support {
511 pub use crate::ogg::page::{build_header as build_header_pub, lace_packet as lace_packet_pub};
512
513 pub fn vorbis_body_empty() -> Vec<u8> {
515 crate::vorbiscomment::build(&[]).unwrap()
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522 use crate::ogg::page::{build_header, lace_packet};
523
524 fn opus_headers() -> Vec<u8> {
525 let head = b"OpusHead\x01\x02\x38\x01\x80\xbb\x00\x00\x00\x00\x00".to_vec();
526 let tags = b"OpusTags\x06\x00\x00\x00musefs\x00\x00\x00\x00".to_vec();
527 let (bytes, _) = build_header(0x1234, &[&head, &tags]);
528 bytes
529 }
530
531 #[test]
532 fn locate_audio_reports_bounds() {
533 let mut data = opus_headers();
534 let header_len = data.len();
535 let (audio, _) = crate::ogg::page::lace_packet(0x1234, 2, false, 960, &[0u8; 120]);
536 data.extend_from_slice(&audio);
537
538 let scan = locate_audio(&data).unwrap();
539 assert_eq!(scan.codec, Codec::Opus);
540 assert_eq!(scan.audio_offset, header_len as u64);
541 assert_eq!(scan.audio_length, (data.len() - header_len) as u64);
542 }
543
544 #[test]
545 fn reads_opus_header() {
546 let mut data = opus_headers();
547 let (audio, _) = lace_packet(0x1234, 2, false, 960, &[0u8; 100]);
549 let header_len = data.len();
550 data.extend_from_slice(&audio);
551
552 let h = read_header(&data).unwrap();
553 assert_eq!(h.codec, Codec::Opus);
554 assert_eq!(h.serial, 0x1234);
555 assert_eq!(h.packets.len(), 2);
556 assert_eq!(h.audio_offset, header_len as u64);
557 assert_eq!(h.header_pages, 2);
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 fn oggflac_headers() -> Vec<u8> {
819 let mut streaminfo = Vec::new();
822 crate::flac::push_block_header(&mut streaminfo, 0, 34, false).unwrap();
823 streaminfo.extend(std::iter::repeat_n(0u8, 34));
824
825 let mut mapping = vec![0x7F];
827 mapping.extend_from_slice(b"FLAC");
828 mapping.push(1);
829 mapping.push(0);
830 mapping.extend_from_slice(&2u16.to_be_bytes()); mapping.extend_from_slice(b"fLaC");
832 mapping.extend_from_slice(&streaminfo);
833
834 let mut seektable = Vec::new();
836 crate::flac::push_block_header(&mut seektable, 3, 18, false).unwrap();
837 seektable.extend(std::iter::repeat_n(0xEEu8, 18));
838
839 let mut old_vc = Vec::new();
841 let body = crate::vorbiscomment::build(&[crate::input::TagInput::new("x", "old")]).unwrap();
842 crate::flac::push_block_header(&mut old_vc, 4, body.len(), true).unwrap();
843 old_vc.extend_from_slice(&body);
844
845 let (bytes, _) = crate::ogg::page::build_header(77, &[&mapping, &seektable, &old_vc]);
846 bytes
847 }
848
849 #[test]
850 fn rejects_multiplexed_second_bitstream() {
851 let head = b"OpusHead\x01\x02\x38\x01\x80\xbb\x00\x00\x00\x00\x00".to_vec();
853 let (mut data, _) = crate::ogg::page::lace_packet(0x1111, 0, true, 0, &head);
854 let (other, _) = crate::ogg::page::lace_packet(
856 0x2222,
857 0,
858 true,
859 0,
860 b"OpusHead\x01\x02\x38\x01\x80\xbb\x00\x00\x00\x00\x00".as_ref(),
861 );
862 data.extend_from_slice(&other);
863 let (audio, _) = crate::ogg::page::lace_packet(0x1111, 1, false, 960, &[0u8; 50]);
865 data.extend_from_slice(&audio);
866 assert!(read_header(&data).is_err());
867 assert!(locate_audio(&data).is_err());
868 }
869
870 #[test]
871 fn synthesize_oggflac_keeps_seektable_replaces_comment_and_count() {
872 let mut data = oggflac_headers();
873 let (audio, _) = crate::ogg::page::lace_packet(77, 3, false, 4096, &[0u8; 64]);
874 data.extend_from_slice(&audio);
875
876 let scan = locate_audio(&data).unwrap();
877 assert_eq!(scan.codec, Codec::OggFlac);
878 let header = read_metadata(&data[..crate::convert::usize_from(scan.audio_offset)]).unwrap();
879
880 let layout = synthesize_layout(
881 &header,
882 scan.audio_offset,
883 scan.audio_length,
884 &[TagInput::new("title", "Kaini Industries")],
885 &[],
886 &MapArtSource::default(),
887 )
888 .unwrap();
889
890 let mut header_bytes: Vec<u8> = Vec::new();
891 for seg in layout.segments() {
892 match seg {
893 Segment::Inline(b) => header_bytes.extend_from_slice(b),
894 Segment::OggAudio { .. } => break,
895 other => panic!("unexpected segment {other:?}"),
896 }
897 }
898 let h = read_header(&header_bytes).unwrap();
899 assert_eq!(h.codec, Codec::OggFlac);
900 assert_eq!(u16::from_be_bytes([h.packets[0][7], h.packets[0][8]]), 2);
902 assert!(h.packets.iter().skip(1).any(|p| (p[0] & 0x7F) == 3));
904 let vc = h
906 .packets
907 .iter()
908 .skip(1)
909 .find(|p| (p[0] & 0x7F) == 4)
910 .unwrap();
911 assert_eq!(vc[0] & 0x80, 0x80);
912 let tags = crate::vorbiscomment::parse(&vc[4..]).unwrap();
913 assert_eq!(
914 tags,
915 vec![("title".to_string(), "Kaini Industries".to_string())]
916 );
917 }
918
919 #[test]
920 fn synthesize_opus_embeds_art_that_round_trips() {
921 let mut data = opus_headers();
922 let (audio, _) = crate::ogg::page::lace_packet(0x1234, 2, false, 960, &[0u8; 80]);
923 data.extend_from_slice(&audio);
924 let scan = locate_audio(&data).unwrap();
925 let header = read_metadata(&data[..crate::convert::usize_from(scan.audio_offset)]).unwrap();
926
927 let image: Vec<u8> = (0..5000u32).map(|i| (i % 251) as u8).collect();
928 let meta = crate::input::ArtInput {
929 art_id: 7,
930 mime: "image/jpeg".to_string(),
931 description: String::new(),
932 picture_type: crate::input::PictureType::new(3).unwrap(),
933 width: 64,
934 height: 64,
935 data_len: crate::input::BlobLen::new(image.len() as u64).unwrap(),
936 };
937 let src = MapArtSource::new([(meta.art_id, image.clone())]);
938 let layout = synthesize_layout(
939 &header,
940 scan.audio_offset,
941 scan.audio_length,
942 &[TagInput::new("title", "Cover")],
943 &[OggArt { meta: &meta }],
944 &src,
945 )
946 .unwrap();
947
948 let mut bytes = Vec::new();
951 for s in layout.segments() {
952 match s {
953 Segment::Inline(b) => bytes.extend_from_slice(b),
954 Segment::OggArtSlice {
955 offset,
956 len,
957 base64,
958 art_total,
959 ..
960 } => {
961 assert!(*base64);
962 let w = b64_window(*offset, len.get(), *art_total);
963 let raw = &image[crate::convert::usize_from(w.in_start)
964 ..crate::convert::usize_from(w.in_start + w.in_len)];
965 bytes.extend_from_slice(&encode_b64_slice(
966 raw,
967 w.skip,
968 crate::convert::usize_from(len.get()),
969 ));
970 }
971 Segment::OggAudio { .. } => break, other => panic!("unexpected {other:?}"),
973 }
974 }
975
976 let pics = read_pictures(&bytes).unwrap();
977 assert_eq!(pics.len(), 1);
978 assert_eq!(pics[0].mime, "image/jpeg");
979 assert_eq!(pics[0].data, image);
980 let h = read_header(&bytes).unwrap();
981 assert_eq!(h.codec, Codec::Opus);
982 }
983
984 fn materialize_header(layout: &RegionLayout, images: &[(i64, &[u8])]) -> Vec<u8> {
987 let mut bytes = Vec::new();
988 for s in layout.segments() {
989 match s {
990 Segment::Inline(b) => bytes.extend_from_slice(b),
991 Segment::OggArtSlice {
992 art_id,
993 offset,
994 len,
995 base64,
996 art_total,
997 } => {
998 let img = images.iter().find(|(id, _)| id == art_id).expect("image").1;
999 if *base64 {
1000 let w = b64_window(*offset, len.get(), *art_total);
1001 let raw = &img[crate::convert::usize_from(w.in_start)
1002 ..crate::convert::usize_from(w.in_start + w.in_len)];
1003 bytes.extend_from_slice(&encode_b64_slice(
1004 raw,
1005 w.skip,
1006 crate::convert::usize_from(len.get()),
1007 ));
1008 } else {
1009 bytes.extend_from_slice(
1010 &img[crate::convert::usize_from(*offset)
1011 ..crate::convert::usize_from(*offset + len.get())],
1012 );
1013 }
1014 }
1015 Segment::OggAudio { .. } => break,
1016 other => panic!("unexpected {other:?}"),
1017 }
1018 }
1019 bytes
1020 }
1021
1022 fn art_input(art_id: i64, mime: &str, len: usize) -> crate::input::ArtInput {
1023 crate::input::ArtInput {
1024 art_id,
1025 mime: mime.to_string(),
1026 description: String::new(),
1027 picture_type: crate::input::PictureType::new(3).unwrap(),
1028 width: 10,
1029 height: 10,
1030 data_len: crate::input::BlobLen::new(len as u64).unwrap(),
1031 }
1032 }
1033
1034 #[test]
1035 fn synthesize_vorbis_embeds_art_that_round_trips() {
1036 let setup = b"\x05vorbis-SETUP".to_vec();
1037 let mut data = vorbis_headers_with(&setup);
1038 let (audio, _) = crate::ogg::page::lace_packet(55, 99, false, 1024, &[0u8; 64]);
1039 data.extend_from_slice(&audio);
1040 let scan = locate_audio(&data).unwrap();
1041 let header = read_metadata(&data[..crate::convert::usize_from(scan.audio_offset)]).unwrap();
1042
1043 let image: Vec<u8> = (0..4000u32).map(|i| (i % 251) as u8).collect();
1044 let meta = art_input(11, "image/png", image.len());
1045 let src = MapArtSource::new([(meta.art_id, image.clone())]);
1046 let layout = synthesize_layout(
1047 &header,
1048 scan.audio_offset,
1049 scan.audio_length,
1050 &[TagInput::new("artist", "X")],
1051 &[OggArt { meta: &meta }],
1052 &src,
1053 )
1054 .unwrap();
1055
1056 let bytes = materialize_header(&layout, &[(11, &image)]);
1057 let h = read_header(&bytes).unwrap();
1058 assert_eq!(h.codec, Codec::Vorbis);
1059 assert_eq!(h.packets[2], setup); let pics = read_pictures(&bytes).unwrap();
1061 assert_eq!(pics.len(), 1);
1062 assert_eq!(pics[0].data, image);
1063 }
1064
1065 #[test]
1066 fn synthesize_oggflac_embeds_art_that_round_trips() {
1067 let mut data = oggflac_headers();
1068 let (audio, _) = crate::ogg::page::lace_packet(77, 3, false, 4096, &[0u8; 64]);
1069 data.extend_from_slice(&audio);
1070 let scan = locate_audio(&data).unwrap();
1071 let header = read_metadata(&data[..crate::convert::usize_from(scan.audio_offset)]).unwrap();
1072
1073 let image: Vec<u8> = (0..4000u32).map(|i| (i % 251) as u8).collect();
1074 let meta = art_input(22, "image/png", image.len());
1075 let src = MapArtSource::new([(meta.art_id, image.clone())]);
1076 let layout = synthesize_layout(
1077 &header,
1078 scan.audio_offset,
1079 scan.audio_length,
1080 &[TagInput::new("title", "Y")],
1081 &[OggArt { meta: &meta }],
1082 &src,
1083 )
1084 .unwrap();
1085
1086 let bytes = materialize_header(&layout, &[(22, &image)]);
1087 let h = read_header(&bytes).unwrap();
1088 assert_eq!(h.codec, Codec::OggFlac);
1089 let pics = read_pictures(&bytes).unwrap();
1090 assert_eq!(pics.len(), 1);
1091 assert_eq!(pics[0].data, image);
1092 }
1093
1094 #[test]
1095 fn synthesize_oggflac_embeds_large_art_spanning_pages_round_trips() {
1096 let mut data = oggflac_headers();
1100 let (audio, _) = crate::ogg::page::lace_packet(77, 3, false, 4096, &[0u8; 64]);
1101 data.extend_from_slice(&audio);
1102 let scan = locate_audio(&data).unwrap();
1103 let header = read_metadata(&data[..crate::convert::usize_from(scan.audio_offset)]).unwrap();
1104
1105 let image: Vec<u8> = (0..200_000u32).map(|i| (i % 251) as u8).collect();
1106 let meta = art_input(31, "image/png", image.len());
1107 let src = MapArtSource::new([(meta.art_id, image.clone())]);
1108 let layout = synthesize_layout(
1109 &header,
1110 scan.audio_offset,
1111 scan.audio_length,
1112 &[TagInput::new("title", "Big")],
1113 &[OggArt { meta: &meta }],
1114 &src,
1115 )
1116 .unwrap();
1117
1118 let art_slices = layout
1120 .segments()
1121 .iter()
1122 .filter(|s| matches!(s, Segment::OggArtSlice { base64: false, .. }))
1123 .count();
1124 assert!(
1125 art_slices >= 2,
1126 "expected the raw art to span multiple pages, got {art_slices} slice(s)"
1127 );
1128
1129 let bytes = materialize_header(&layout, &[(31, &image)]);
1130 let h = read_header(&bytes).unwrap();
1131 assert_eq!(h.codec, Codec::OggFlac);
1132 let pics = read_pictures(&bytes).unwrap();
1133 assert_eq!(pics.len(), 1);
1134 assert_eq!(
1135 pics[0].data, image,
1136 "large art must round-trip byte-for-byte"
1137 );
1138 }
1139
1140 #[test]
1141 fn synthesize_opus_embeds_multiple_images() {
1142 let mut data = opus_headers();
1143 let (audio, _) = crate::ogg::page::lace_packet(0x1234, 2, false, 960, &[0u8; 64]);
1144 data.extend_from_slice(&audio);
1145 let scan = locate_audio(&data).unwrap();
1146 let header = read_metadata(&data[..crate::convert::usize_from(scan.audio_offset)]).unwrap();
1147
1148 let img_a: Vec<u8> = (0..3000u32).map(|i| (i % 251) as u8).collect();
1149 let img_b: Vec<u8> = (0..1500u32).map(|i| ((i * 3) % 251) as u8).collect();
1150 let meta_a = art_input(1, "image/png", img_a.len());
1151 let meta_b = art_input(2, "image/jpeg", img_b.len());
1152 let src = MapArtSource::new([
1153 (meta_a.art_id, img_a.clone()),
1154 (meta_b.art_id, img_b.clone()),
1155 ]);
1156 let layout = synthesize_layout(
1157 &header,
1158 scan.audio_offset,
1159 scan.audio_length,
1160 &[TagInput::new("title", "Multi")],
1161 &[OggArt { meta: &meta_a }, OggArt { meta: &meta_b }],
1162 &src,
1163 )
1164 .unwrap();
1165
1166 let bytes = materialize_header(&layout, &[(1, &img_a), (2, &img_b)]);
1167 let h = read_header(&bytes).unwrap();
1168 assert_eq!(h.codec, Codec::Opus);
1169 let pics = read_pictures(&bytes).unwrap();
1170 assert_eq!(pics.len(), 2);
1171 assert_eq!(pics[0].data, img_a);
1172 assert_eq!(pics[1].data, img_b);
1173 }
1174
1175 #[test]
1176 fn oversized_full_art_value_rejected_by_build_packets() {
1177 let meta = crate::input::ArtInput {
1178 art_id: 0,
1179 mime: "image/jpeg".to_string(),
1180 description: String::new(),
1181 data_len: crate::input::BlobLen::new(u64::from(u32::MAX)).unwrap(),
1182 picture_type: crate::input::PictureType::new(3).unwrap(),
1183 width: 0,
1184 height: 0,
1185 };
1186 let art = OggArt { meta: &meta };
1187 let header = OggHeader {
1188 codec: Codec::Vorbis,
1189 serial: 0,
1190 packets: vec![vec![], vec![], vec![]],
1191 header_pages: 1,
1192 audio_offset: 0,
1193 };
1194 let result = build_packets_with_art(&header, &[], &[art]);
1195 assert!(result.is_err(), "expected Err for oversized art");
1196 }
1197
1198 #[test]
1199 fn sum_overflow_art_value_rejected_by_build_packets() {
1200 let meta = crate::input::ArtInput {
1203 art_id: 0,
1204 mime: "image/png".to_string(),
1205 description: "x".repeat(256),
1206 data_len: crate::input::BlobLen::new(3_221_225_470).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!(
1221 result.is_err(),
1222 "expected Err when key + b64(prefix) + b64(data) overflows u32"
1223 );
1224 }
1225
1226 #[test]
1227 fn art_value_at_u32_max_boundary_is_accepted_by_build_packets() {
1228 let meta = crate::input::ArtInput {
1235 art_id: 0,
1236 mime: "image/png".to_string(),
1237 description: String::new(),
1238 data_len: crate::input::BlobLen::new(3_221_225_412).unwrap(),
1239 picture_type: crate::input::PictureType::new(3).unwrap(),
1240 width: 0,
1241 height: 0,
1242 };
1243 let art = OggArt { meta: &meta };
1244 let header = OggHeader {
1245 codec: Codec::Vorbis,
1246 serial: 0,
1247 packets: vec![vec![], vec![], vec![]],
1248 header_pages: 1,
1249 audio_offset: 0,
1250 };
1251 let accepted = build_packets_with_art(&header, &[], &[art]).is_ok();
1252 assert!(
1253 accepted,
1254 "value_len exactly u32::MAX must be accepted by build_packets_with_art"
1255 );
1256 }
1257
1258 #[test]
1259 fn near_u64_max_art_value_rejected_by_build_packets() {
1260 let meta = crate::input::ArtInput {
1264 art_id: 0,
1265 mime: "image/jpeg".to_string(),
1266 description: String::new(),
1267 data_len: crate::input::BlobLen::new(u64::MAX).unwrap(),
1268 picture_type: crate::input::PictureType::new(3).unwrap(),
1269 width: 0,
1270 height: 0,
1271 };
1272 let art = OggArt { meta: &meta };
1273 let header = OggHeader {
1274 codec: Codec::Vorbis,
1275 serial: 0,
1276 packets: vec![vec![], vec![], vec![]],
1277 header_pages: 1,
1278 audio_offset: 0,
1279 };
1280 let result = build_packets_with_art(&header, &[], &[art]);
1281 let is_too_large = matches!(&result, Err(FormatError::TooLarge));
1282 assert!(is_too_large, "expected Err(TooLarge) for near-u64::MAX art");
1283 }
1284
1285 #[test]
1286 fn near_u64_max_art_value_rejected_by_oggflac_build_packets() {
1287 let meta = crate::input::ArtInput {
1293 art_id: 0,
1294 mime: "image/jpeg".to_string(),
1295 description: String::new(),
1296 data_len: crate::input::BlobLen::new(u64::MAX).unwrap(),
1297 picture_type: crate::input::PictureType::new(3).unwrap(),
1298 width: 0,
1299 height: 0,
1300 };
1301 let art = OggArt { meta: &meta };
1302 let header = OggHeader {
1303 codec: Codec::OggFlac,
1304 serial: 0,
1305 packets: vec![vec![0x7F]],
1306 header_pages: 1,
1307 audio_offset: 0,
1308 };
1309 let result = build_packets_with_art(&header, &[], &[art]);
1310 let is_too_large = matches!(&result, Err(FormatError::TooLarge));
1311 assert!(is_too_large, "expected Err(TooLarge) for near-u64::MAX art");
1312 }
1313
1314 #[test]
1315 fn picture_prefix_is_3_aligned_and_declares_image_len() {
1316 let art = crate::input::ArtInput {
1317 art_id: 1,
1318 mime: "image/png".to_string(), description: String::new(),
1320 picture_type: crate::input::PictureType::new(3).unwrap(),
1321 width: 1,
1322 height: 1,
1323 data_len: crate::input::BlobLen::new(12345).unwrap(),
1324 };
1325 let p = picture_prefix(&art).unwrap();
1326 assert_eq!(p.len() % 3, 0);
1327 let dl = u32::from_be_bytes(p[p.len() - 4..].try_into().unwrap());
1329 assert_eq!(dl, 12345);
1330 let mut body = p.clone();
1333 body.extend(std::iter::repeat_n(0u8, 12345));
1334 let pic = crate::flac::parse_picture_block(&body).unwrap();
1335 assert_eq!(pic.mime, "image/png");
1336 assert_eq!(pic.picture_type.get(), 3);
1337 }
1338
1339 #[test]
1340 fn detect_codec_matches_each_magic_and_rejects_others() {
1341 assert_eq!(detect_codec(b"OpusHead........").unwrap(), Codec::Opus);
1342 assert_eq!(detect_codec(b"\x01vorbis...").unwrap(), Codec::Vorbis);
1343 assert_eq!(detect_codec(b"\x7FFLAC...").unwrap(), Codec::OggFlac);
1344 assert!(detect_codec(b"OpusHea").is_err()); assert!(detect_codec(b"XXXXXXXX").is_err()); assert!(detect_codec(b"\x01vorbi").is_err()); }
1350
1351 #[test]
1352 fn comment_body_strips_each_codec_prefix_and_guards_length() {
1353 assert_eq!(comment_body(Codec::Opus, b"OpusTagsBODY").unwrap(), b"BODY");
1354 assert_eq!(
1355 comment_body(Codec::Vorbis, b"\x03vorbisBODY").unwrap(),
1356 b"BODY"
1357 );
1358 assert_eq!(
1359 comment_body(Codec::OggFlac, b"\x04\x00\x00\x00BODY").unwrap(),
1360 b"BODY"
1361 );
1362 assert!(comment_body(Codec::Opus, b"OpusTa").is_err());
1364 assert!(comment_body(Codec::OggFlac, b"\x04\x00\x00").is_err());
1365 }
1366
1367 #[test]
1368 fn oggflac_following_packets_reads_be_count_and_guards_length() {
1369 let pkt = b"\x7FFLAC\x01\x00\x00\x05rest";
1371 assert_eq!(oggflac_following_packets(pkt).unwrap(), 5);
1372 assert!(oggflac_following_packets(b"\x7FFLAC\x01\x00").is_err()); }
1374
1375 #[test]
1376 fn oggflac_comment_block_size_boundary_is_inclusive() {
1377 let header = OggHeader {
1382 codec: Codec::OggFlac,
1383 serial: 1,
1384 packets: vec![vec![0x7F; 9]],
1385 header_pages: 1,
1386 audio_offset: 0,
1387 };
1388 let overhead = crate::vorbiscomment::build(&[crate::input::TagInput::new("title", "")])
1389 .unwrap()
1390 .len() as u64;
1391 let at_limit = "x".repeat(crate::convert::usize_from(
1392 crate::flac::MAX_BLOCK_BODY - overhead,
1393 ));
1394 let tags = [crate::input::TagInput::new("title", at_limit.as_str())];
1395 assert!(oggflac_packets_with_art(&header, &tags, &[]).is_ok());
1396 let over = format!("{at_limit}x");
1398 let tags = [crate::input::TagInput::new("title", over.as_str())];
1399 assert!(matches!(
1400 oggflac_packets_with_art(&header, &tags, &[]),
1401 Err(FormatError::TooLarge)
1402 ));
1403 }
1404
1405 #[test]
1406 fn oggflac_picture_block_size_boundary_is_inclusive() {
1407 let header = OggHeader {
1413 codec: Codec::OggFlac,
1414 serial: 1,
1415 packets: vec![vec![0x7F; 9]],
1416 header_pages: 1,
1417 audio_offset: 0,
1418 };
1419 let mk = |data_len: u64| crate::input::ArtInput {
1420 art_id: 1,
1421 mime: "image/png".to_string(),
1422 description: String::new(),
1423 picture_type: crate::input::PictureType::new(3).unwrap(),
1424 width: 0,
1425 height: 0,
1426 data_len: crate::input::BlobLen::new(data_len).unwrap(),
1427 };
1428 let framing_len = picture_prefix(&mk(1)).unwrap().len() as u64;
1429 let at_limit = mk(crate::flac::MAX_BLOCK_BODY - framing_len);
1430 let arts = [OggArt { meta: &at_limit }];
1431 assert!(oggflac_packets_with_art(&header, &[], &arts).is_ok());
1432 let over = mk(crate::flac::MAX_BLOCK_BODY - framing_len + 1);
1434 let arts = [OggArt { meta: &over }];
1435 assert!(matches!(
1436 oggflac_packets_with_art(&header, &[], &arts),
1437 Err(FormatError::TooLarge)
1438 ));
1439 }
1440
1441 #[test]
1442 fn comment_packet_index_locates_the_comment_block() {
1443 let opus = OggHeader {
1446 codec: Codec::Opus,
1447 serial: 1,
1448 packets: vec![vec![], vec![]],
1449 header_pages: 1,
1450 audio_offset: 0,
1451 };
1452 assert_eq!(comment_packet_index(&opus), 1);
1453
1454 let oggflac = OggHeader {
1456 codec: Codec::OggFlac,
1457 serial: 1,
1458 packets: vec![vec![0x7F], vec![0x01], vec![0x84]], header_pages: 1,
1460 audio_offset: 0,
1461 };
1462 assert_eq!(comment_packet_index(&oggflac), 2);
1463 let none = OggHeader {
1465 codec: Codec::OggFlac,
1466 serial: 1,
1467 packets: vec![vec![0x7F], vec![0x01], vec![0x05]],
1468 header_pages: 1,
1469 audio_offset: 0,
1470 };
1471 assert_eq!(comment_packet_index(&none), 0);
1472 }
1473
1474 #[test]
1475 fn locate_audio_accepts_empty_audio_region() {
1476 let file = opus_headers();
1479 let scan = locate_audio(&file).unwrap();
1480 assert_eq!(scan.codec, Codec::Opus);
1481 assert_eq!(scan.audio_offset, file.len() as u64);
1482 assert_eq!(scan.audio_length, 0);
1483 }
1484
1485 #[test]
1486 fn picture_prefix_declared_desc_len_pins_padding() {
1487 let art = crate::input::ArtInput {
1488 art_id: 1,
1489 mime: "image/png".into(), description: "x".into(), picture_type: crate::input::PictureType::new(3).unwrap(),
1492 width: 1,
1493 height: 1,
1494 data_len: crate::input::BlobLen::new(100).unwrap(),
1495 };
1496 let prefix = picture_prefix(&art).unwrap();
1497 assert_eq!(prefix.len() % 3, 0);
1498 let off = 8 + art.mime.len();
1501 let declared = u32::from_be_bytes(prefix[off..off + 4].try_into().unwrap());
1502 let pad = declared - u32::try_from(art.description.len()).unwrap();
1503 assert!(pad <= 2, "pad must be 0..=2, got {pad}");
1504 assert_eq!(pad, 0, "base % 3 == 0 implies pad 0");
1505 }
1506
1507 #[test]
1508 fn synthesis_reads_art_in_page_bounded_windows() {
1509 use std::cell::Cell;
1510 struct Counting<'a> {
1511 inner: MapArtSource,
1512 max: &'a Cell<usize>,
1513 }
1514 impl ArtSource for Counting<'_> {
1515 fn read_window(&self, art_id: i64, offset: u64, buf: &mut [u8]) -> crate::Result<()> {
1516 self.max.set(self.max.get().max(buf.len()));
1517 self.inner.read_window(art_id, offset, buf)
1518 }
1519 }
1520
1521 let mut data = opus_headers();
1523 let scan = locate_audio({
1524 let (audio, _) = crate::ogg::page::lace_packet(0x1234, 2, false, 960, &[0u8; 80]);
1525 data.extend_from_slice(&audio);
1526 &data
1527 })
1528 .unwrap();
1529 let header = read_metadata(&data[..crate::convert::usize_from(scan.audio_offset)]).unwrap();
1530
1531 let image: Vec<u8> = (0..500_000u32).map(|i| (i % 251) as u8).collect();
1532 let meta = crate::input::ArtInput {
1533 art_id: 7,
1534 mime: "image/jpeg".to_string(),
1535 description: String::new(),
1536 picture_type: crate::input::PictureType::new(3).unwrap(),
1537 width: 0,
1538 height: 0,
1539 data_len: crate::input::BlobLen::new(image.len() as u64).unwrap(),
1540 };
1541 let max = Cell::new(0usize);
1542 let src = Counting {
1543 inner: MapArtSource::new([(7i64, image.clone())]),
1544 max: &max,
1545 };
1546 synthesize_layout(
1547 &header,
1548 scan.audio_offset,
1549 scan.audio_length,
1550 &[],
1551 &[OggArt { meta: &meta }],
1552 &src,
1553 )
1554 .unwrap();
1555 assert!(
1558 max.get() > 0 && max.get() <= 65_025,
1559 "max single read was {}",
1560 max.get()
1561 );
1562 }
1563}
1564
1565#[cfg(test)]
1566mod page_test_support_tests {
1567 #[test]
1572 fn vorbis_body_empty_is_a_parseable_empty_comment() {
1573 let body = super::page_test_support::vorbis_body_empty();
1574 let parsed = crate::vorbiscomment::parse(&body).unwrap();
1575 assert!(parsed.is_empty());
1576 }
1577}
1578
1579#[cfg(test)]
1580mod bounded_tests {
1581 use super::*;
1582 use crate::ogg::page_test_support::{build_header_pub, lace_packet_pub, vorbis_body_empty};
1583
1584 fn opus_stream() -> (Vec<u8>, u64) {
1591 let head = b"OpusHead\x01\x02\x38\x01\x80\xbb\x00\x00\x00\x00\x00".to_vec();
1592 let mut tags = b"OpusTags".to_vec();
1593 tags.extend_from_slice(&vorbis_body_empty());
1594 let serial = 0x1234;
1595 let (mut v, _) = build_header_pub(serial, &[&head, &tags]);
1596 let audio_offset = v.len() as u64;
1597 let (audio, _) = lace_packet_pub(serial, 2, false, 960, &[0u8; 100]);
1598 v.extend_from_slice(&audio);
1599 (v, audio_offset)
1600 }
1601
1602 #[test]
1603 fn read_metadata_bounded_complete_when_prefix_covers_header() {
1604 let (full, audio_offset) = opus_stream();
1605 let file_len = full.len() as u64;
1606 let prefix = &full[..crate::convert::usize_from(audio_offset)]; match read_metadata_bounded(prefix, file_len).unwrap() {
1608 Extent::Complete(h) => assert_eq!(h.audio_offset, audio_offset),
1609 other @ Extent::NeedMore { .. } => panic!("expected Complete, got {other:?}"),
1610 }
1611 }
1612
1613 #[test]
1614 fn read_metadata_bounded_needmore_when_header_truncated() {
1615 let (full, _audio_offset) = opus_stream();
1616 let file_len = full.len() as u64;
1617 let prefix = &full[..20]; match read_metadata_bounded(prefix, file_len).unwrap() {
1619 Extent::NeedMore { up_to } => assert!(up_to > 20 && up_to <= file_len),
1620 other @ Extent::Complete(_) => panic!("expected NeedMore, got {other:?}"),
1621 }
1622 }
1623
1624 #[test]
1625 fn read_metadata_bounded_errors_when_whole_file_is_unparseable() {
1626 let bad: &[u8] = b"not an ogg stream at all"; assert!(read_header(bad).is_err());
1632 let len = bad.len() as u64;
1633 match read_metadata_bounded(bad, len) {
1637 Err(_) => {}
1638 Ok(other) => panic!("expected Err when whole file unparseable, got {other:?}"),
1639 }
1640 }
1641
1642 #[test]
1643 fn read_metadata_bounded_doubles_window_exactly() {
1644 let buf = vec![0u8; 100_000]; assert!(read_header(&buf).is_err());
1650 let file_len = 10_000_000u64;
1651 match read_metadata_bounded(&buf, file_len).unwrap() {
1652 Extent::NeedMore { up_to } => assert_eq!(up_to, 200_000),
1653 other @ Extent::Complete(_) => panic!("expected NeedMore, got {other:?}"),
1654 }
1655 }
1656
1657 #[test]
1658 fn read_metadata_bounded_floor_is_64kib_for_small_prefix() {
1659 let buf = vec![0u8; 100]; assert!(read_header(&buf).is_err());
1665 let file_len = 10_000_000u64;
1666 match read_metadata_bounded(&buf, file_len).unwrap() {
1669 Extent::NeedMore { up_to } => assert_eq!(up_to, 65_536),
1670 other @ Extent::Complete(_) => panic!("expected NeedMore, got {other:?}"),
1671 }
1672 }
1673
1674 #[test]
1675 fn read_metadata_bounded_grows_when_truncated_prefix_shorter_than_file() {
1676 let (full, _audio_offset) = opus_stream();
1680 let file_len = full.len() as u64;
1681 let prefix = &full[..10]; assert!(read_header(prefix).is_err());
1683 match read_metadata_bounded(prefix, file_len).unwrap() {
1684 Extent::NeedMore { up_to } => assert!(up_to > prefix.len() as u64),
1685 other @ Extent::Complete(_) => panic!("expected NeedMore, got {other:?}"),
1686 }
1687 }
1688}