Skip to main content

musefs_format/ogg/
mod.rs

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/// The codec carried inside an Ogg logical bitstream that we synthesize.
18#[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
39/// For OggFLAC, packet 0 is `0x7F "FLAC" major minor count(2, BE) "fLaC" STREAMINFO`.
40/// The 16-bit big-endian count is the number of metadata-block packets that follow
41/// packet 0.
42fn 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/// The parsed Ogg header region: codec, serial, the reassembled header packets,
50/// the number of header pages, and where audio begins.
51#[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
60/// Reject multiplexed/chained Ogg: within the header region every page must share
61/// the first page's serial and only the first page may carry BOS.
62fn 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
79/// Parse the header region from the front of a logical bitstream. `data` may be the
80/// whole file or just `[0, audio_offset)`; either way parsing stops once all header
81/// packets are reassembled.
82pub fn read_header(data: &[u8]) -> Result<OggHeader> {
83    let first_page = page::parse_page(data, 0)?;
84    let serial = first_page.serial;
85
86    // Reassemble the first packet to detect the codec and (for OggFLAC) the count.
87    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
113/// Strip a codec's comment-packet prefix, returning the VorbisComment body slice.
114fn comment_body(codec: Codec, packet: &[u8]) -> Result<&[u8]> {
115    let prefix = match codec {
116        Codec::Opus => 8,    // "OpusTags"
117        Codec::Vorbis => 7,  // 0x03 "vorbis"
118        Codec::OggFlac => 4, // FLAC metadata block header (type + 24-bit length)
119    };
120    if packet.len() < prefix {
121        return Err(FormatError::Malformed);
122    }
123    Ok(&packet[prefix..])
124}
125
126/// The index of the comment packet within the reassembled header packets.
127fn comment_packet_index(header: &OggHeader) -> usize {
128    match header.codec {
129        Codec::Opus | Codec::Vorbis => 1,
130        // OggFLAC: packet 0 is the mapping header; the VORBIS_COMMENT block is
131        // whichever following packet has block type 4.
132        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
142/// Read existing `(FIELD, value)` tags from a complete file. Empty if none.
143pub 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()); // no comment packet present
148    }
149    let body = comment_body(header.codec, &header.packets[idx])?;
150    let mut tags = crate::vorbiscomment::parse(body)?;
151    // Cover art rides in the comment as a base64 METADATA_BLOCK_PICTURE entry, but
152    // it has its own channel (read_pictures). Excluding it keeps read_tags
153    // text-only and prevents the art being stored — and re-synthesized — twice.
154    tags.retain(|(field, _)| !field.eq_ignore_ascii_case("METADATA_BLOCK_PICTURE"));
155    Ok(tags)
156}
157
158use crate::input::EmbeddedPicture;
159
160/// Extract embedded pictures from a complete file for scan-time ingestion.
161///
162/// Opus/Vorbis carry art as a base64 `METADATA_BLOCK_PICTURE` comment whose decoded
163/// bytes are a FLAC PICTURE block body; OggFLAC carries native PICTURE block
164/// packets (block type 6). Plan 1 only *reads* art (to seed the DB); synthesis does
165/// not yet re-embed it.
166pub 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                    // Strip the 4-byte FLAC metadata block header.
190                    out.push(crate::flac::parse_picture_block(&pkt[4..])?);
191                }
192            }
193        }
194    }
195    Ok(out)
196}
197
198/// Audio bounds + codec from a complete file, for the scanner.
199#[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
218/// The header region parsed from the front of the file (`[0, audio_offset)`), for
219/// synthesis. Identical to `read_header` but named to mirror `flac::read_metadata`.
220pub fn read_metadata(front: &[u8]) -> Result<OggHeader> {
221    read_header(front)
222}
223
224/// Bounded twin of [`read_metadata`]. OGG header packets (and all OGG embedded
225/// art) are front-anchored, so a prefix covering the header region is sufficient.
226/// `read_header` does not expose an exact byte need, so on a short/truncated
227/// prefix this geometrically grows the window (doubling, capped at `file_len`):
228/// header regions are tiny, so the first 1 MiB window almost always completes,
229/// and the cap guarantees the worst case equals reading the whole file.
230pub 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        // `read_header` cannot distinguish a truncated front from genuine
234        // corruption, so we widen optimistically; a real error resurfaces via the
235        // `Err(e)` arm once `prefix` reaches `file_len` (and the caller's retry
236        // limit + full-read fallback bound the cost).
237        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
277/// Build the FLAC PICTURE block *body prefix* (everything before the image data:
278/// type, mime, description, dimensions, depth, colors, data-length) for `art`,
279/// padding the description with spaces so the prefix length is a multiple of 3.
280/// This makes `base64(prefix ++ image) == base64(prefix) ++ base64(image)`, so the
281/// image's base64 is an independent substring that can be served incrementally.
282/// The declared data-length field is the true image length (`art.data_len`).
283fn picture_prefix(art: &crate::input::ArtInput) -> Result<Vec<u8>> {
284    // Unpadded prefix length = 4(type)+4(mimelen)+mime +4(desclen)+desc
285    //   +4(w)+4(h)+4(depth)+4(colors)+4(datalen) = 32 + mime + desc.
286    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()); // depth
307    out.extend_from_slice(&0u32.to_be_bytes()); // colors
308    out.extend_from_slice(
309        &u32::try_from(art.data_len.get())
310            .map_err(|_| FormatError::TooLarge)?
311            .to_be_bytes(),
312    ); // image data length
313    Ok(out)
314}
315
316use crate::ogg::page::PayloadChunk;
317use base64::Engine;
318
319/// One image to embed: its metadata. Bytes are read from an `ArtSource` only to
320/// compute page CRCs at synthesis time; they are never retained in the layout.
321#[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
332/// Build the regenerated header packets as chunk lists, embedding `arts`.
333/// Opus/Vorbis: art goes into the comment packet as `METADATA_BLOCK_PICTURE`
334/// comments (last). OggFLAC: each art is a native PICTURE block packet.
335fn 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            // VorbisComment value length is a 32-bit field; guard against overflow
343            // for absurdly large images (cover art is far below this). The full
344            // value includes the key, base64 of the picture prefix, and base64 of
345            // the image; any one of these alone may fit in u32 but the sum may not.
346            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
378/// Build a VorbisComment-style comment packet (Opus `OpusTags` / Vorbis
379/// `0x03vorbis`) as chunks: a leading `Bytes` chunk (magic + vendor + count + text
380/// comments + each art comment's framing and base64(prefix)), an `Art` chunk per
381/// image (base64 of the image), and — for Vorbis — a trailing framing-bit `Bytes`
382/// chunk.
383fn 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)?; // vendor + count(text) + text comments
390    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
434/// OggFLAC header packets with art: the text comment packet (no art) plus one
435/// native PICTURE block packet per image. The last metadata-block packet carries
436/// the last-block flag, and packet 0's 16-bit following-packet count is recomputed.
437fn 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    /// An empty VorbisComment body (vendor + zero comments), for fixtures.
514    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        // Append one audio page so audio_offset lands before EOF.
548        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        // Build an OpusTags packet with one real comment via the shared builder.
563        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        // A METADATA_BLOCK_PICTURE comment whose value is a base64 FLAC picture
579        // block carrying a 1-byte image, plus one ordinary text tag.
580        let mut block = Vec::new();
581        block.extend_from_slice(&3u32.to_be_bytes()); // picture type: front cover
582        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()); // description length
585        block.extend_from_slice(&1u32.to_be_bytes()); // width
586        block.extend_from_slice(&1u32.to_be_bytes()); // height
587        block.extend_from_slice(&8u32.to_be_bytes()); // depth
588        block.extend_from_slice(&0u32.to_be_bytes()); // colors used
589        block.extend_from_slice(&1u32.to_be_bytes()); // image length
590        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        // read_tags returns only the text tag — the picture comment is excluded...
606        let tags = read_tags(&data).unwrap();
607        assert_eq!(tags, vec![("title".to_string(), "Sun".to_string())]);
608        // ...while read_pictures still finds the embedded art.
609        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        // Collect all Inline header bytes (one per page) until OggAudio.
636        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        // seq_delta = synthesized_page_count - original_header_pages. When the
661        // original header spanned a different number of pages than the regenerated
662        // one, the delta is non-zero — pins the subtraction at the OggAudio segment.
663        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        // Synthesis re-lays the Opus header into a known page count; record it.
671        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        // Pretend the ORIGINAL header spanned three extra pages, so the served audio
688        // pages must be renumbered downward by exactly three.
689        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        // Minimal-but-shaped Vorbis ID header (30 bytes from 0x01"vorbis").
718        let mut id = b"\x01vorbis".to_vec();
719        id.extend_from_slice(&0u32.to_le_bytes()); // version
720        id.push(2); // channels
721        id.extend_from_slice(&44100u32.to_le_bytes()); // sample rate
722        id.extend_from_slice(&0u32.to_le_bytes()); // bitrate max
723        id.extend_from_slice(&128_000u32.to_le_bytes()); // nominal
724        id.extend_from_slice(&0u32.to_le_bytes()); // min
725        id.push(0xB8); // blocksizes
726        id.push(0x01); // framing bit
727        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        // The original setup packet (3rd header packet) must be carried through.
745        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); // setup preserved byte-for-byte
768        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        // A minimal FLAC PICTURE block body: type=3, mime="image/png", empty desc,
777        // 1x1, depth 0, colors 0, data="PNG".
778        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()); // desc len
784        pic.extend_from_slice(&1u32.to_be_bytes()); // width
785        pic.extend_from_slice(&1u32.to_be_bytes()); // height
786        pic.extend_from_slice(&0u32.to_be_bytes()); // depth
787        pic.extend_from_slice(&0u32.to_be_bytes()); // colors
788        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()); // one comment
801        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        // STREAMINFO block (type 0): 4-byte header + 34-byte body (zeros are fine
820        // for our framing test).
821        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        // Mapping header packet: 0x7F "FLAC" v1.0 count "fLaC" STREAMINFO.
826        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()); // count: SEEKTABLE + VORBIS_COMMENT
831        mapping.extend_from_slice(b"fLaC");
832        mapping.extend_from_slice(&streaminfo);
833
834        // A SEEKTABLE block (type 3, structural — must be preserved).
835        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        // An existing VORBIS_COMMENT (type 4, last) to be replaced.
840        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        // Two BOS pages with DIFFERENT serials at the start => multiplexed; must reject.
852        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        // A second logical stream's BOS page (different serial).
855        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        // Some audio after, so audio_offset (if it were accepted) is past these pages.
864        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        // packet 0 mapping count == number of following blocks (SEEKTABLE + VC == 2)
901        assert_eq!(u16::from_be_bytes([h.packets[0][7], h.packets[0][8]]), 2);
902        // SEEKTABLE preserved
903        assert!(h.packets.iter().skip(1).any(|p| (p[0] & 0x7F) == 3));
904        // exactly one VORBIS_COMMENT, with the new tag, flagged last
905        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        // Materialize the header region from the layout, expanding OggArtSlice by
949        // re-deriving its bytes from `image` (mirrors what read_at does).
950        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, // header region ends here
972                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    // Materialize the header region of a synthesized layout into bytes, expanding
985    // each OggArtSlice from `images` (art_id -> raw image), mirroring read_at.
986    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); // setup preserved
1060        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        // A >64 KiB raw PICTURE block forces the art run across multiple pages,
1097        // exercising the non-base64 streaming-CRC path at page boundaries (the
1098        // base64 path is covered by the lacer test; this pins the raw path).
1099        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        // The art run must split across pages: more than one raw OggArtSlice.
1119        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        // data_len and prefix individually fit in u32, but the full value
1201        // (key + b64(prefix) + b64(data)) exceeds u32::MAX.
1202        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        // Pin the exact `value_len > u32::MAX` boundary: with mime "image/png" and
1229        // an empty description, key(23) + b64(prefix=42)=56 + b64(data_len) lands on
1230        // u32::MAX EXACTLY when data_len == 3_221_225_412. A correct `>` admits it
1231        // (the downstream u32 length field still fits); `>=`/`==` or a `*`-mutated
1232        // sum would wrongly reject it. The declared 3 GiB image is never
1233        // materialized (image bytes are empty), so the build is cheap.
1234        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        // data_len near u64::MAX makes b64_len(data_len) overflow u64; the builder
1261        // must fail closed with TooLarge at the checked b64 length, not panic
1262        // (debug) inside the pre-flight value_len computation.
1263        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        // The Ogg-FLAC art path builds a METADATA_BLOCK_PICTURE body as
1288        // prefix + raw image. A hostile data_len near u64::MAX must fail closed
1289        // with TooLarge, not panic (debug) / wrap (release). picture_prefix's u32
1290        // length field already rejects it; the checked add keeps the body-length
1291        // site self-defending regardless of that ordering.
1292        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(), // 9 -> base = 32+9+0 = 41 -> pad 1
1319            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        // datalen is the last 4 bytes (big-endian) and equals the true image length.
1328        let dl = u32::from_be_bytes(p[p.len() - 4..].try_into().unwrap());
1329        assert_eq!(dl, 12345);
1330        // Reusing the existing FLAC picture parser proves the framing is valid:
1331        // parse_picture_block expects the body (prefix + image); append dummy image.
1332        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        // Too-short and non-matching inputs must error (kills the :25 && -> || and
1345        // the length-guard mutations).
1346        assert!(detect_codec(b"OpusHea").is_err()); // 7 bytes, len guard
1347        assert!(detect_codec(b"XXXXXXXX").is_err()); // right length, wrong magic
1348        assert!(detect_codec(b"\x01vorbi").is_err()); // 6 bytes
1349    }
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        // packet shorter than the prefix errors (kills :113 < -> ==/<=).
1363        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        // 0x7F"FLAC" major minor count(BE) ... ; count bytes at [7],[8].
1370        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()); // 7 bytes (<9)
1373    }
1374
1375    #[test]
1376    fn oggflac_comment_block_size_boundary_is_inclusive() {
1377        // The regenerated OggFLAC VORBIS_COMMENT block shares FLAC's 24-bit
1378        // block length. Derive the non-value overhead from production, then
1379        // size the value so the body lands exactly on the limit; one more byte
1380        // errors. The `>` accepts the inclusive limit; the `>=` mutant rejects.
1381        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        // one byte over must still error, pinning the high side of the boundary.
1397        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        // body_len = picture_prefix(meta).len() + data_len; the guard shares
1408        // FLAC's 24-bit block limit. data_len is only a count (image bytes are
1409        // streamed), so the exact boundary is cheap to pin. The `>` accepts the
1410        // inclusive limit — which also pins the `+` assembly, since a product
1411        // of the two terms overshoots it — while the `>=` mutant rejects it.
1412        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        // one byte over must still error, pinning the high side of the boundary.
1433        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        // Opus/Vorbis: always packet index 1 (kills :121 -> 1 only if a non-1 case
1444        // exists; assert OggFLAC search to pin the skip(1)+find logic at :130).
1445        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        // OggFLAC: packet 0 mapping, packet 1 type 1 (non-comment), packet 2 type 4.
1455        let oggflac = OggHeader {
1456            codec: Codec::OggFlac,
1457            serial: 1,
1458            packets: vec![vec![0x7F], vec![0x01], vec![0x84]], // 0x84 & 0x7F == 4
1459            header_pages: 1,
1460            audio_offset: 0,
1461        };
1462        assert_eq!(comment_packet_index(&oggflac), 2);
1463        // No type-4 block -> 0 (kills the bitmask / == mutations at :130).
1464        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        // opus_headers() is header pages only: audio_offset == data.len(). The
1477        // original `>` yields Ok (audio_length 0); the :196 `==`/`>=` mutants reject.
1478        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(), // 9
1490            description: "x".into(),  // 1 -> base = 42, 42 % 3 == 0 -> pad 0
1491            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        // Declared description length lives at offset 8 + mime.len() (after
1499        // type[4] + mimelen[4] + mime). pad = declared - desc.len() must be 0..=2.
1500        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        // Inline Opus fixture, mirroring synthesize_opus_emits_valid_header_and_audio_segment.
1522        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        // One Ogg page's payload is at most 255*255 = 65025 bytes; raw windows are
1556        // <= that. The 500 KB image is never read in a single call.
1557        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    /// Pins the fixture's contract: a parseable VorbisComment with zero
1568    /// comments. Consumers (musefs-core's ogg tests) splice it into OpusTags
1569    /// packets, but only format-local tests can kill mutations of it — the
1570    /// mutation gate runs each crate's own suite.
1571    #[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    /// A minimal Opus stream: OpusHead + OpusTags header packets, then a trailing
1585    /// audio page. Returns (full, audio_offset). Mirrors the proven fixture in
1586    /// `musefs-core/src/scan.rs::ogg_probe_tests::probe_detects_opus_and_seeds_tags`.
1587    /// `build_header_pub(serial, &[&[u8]])` laces *all* header packets across
1588    /// pages (BOS set once) and returns `(Vec<u8>, u32)`; `lace_packet_pub` takes
1589    /// `(serial, seq_start, bos, granule, packet)` and returns `(Vec<u8>, u32)`.
1590    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)]; // exactly the header region
1607        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]; // mid first page
1618        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        // A short garbage buffer that IS the whole file: prefix.len() == file_len and
1627        // read_header errors. The guard `(prefix.len() as u64) < file_len` is FALSE,
1628        // so the function must fall to the `Err(e)` arm and return Err — never grow.
1629        let bad: &[u8] = b"not an ogg stream at all"; // capture pattern != "OggS"
1630        // Confirm the premise: read_header genuinely errors on this buffer.
1631        assert!(read_header(bad).is_err());
1632        let len = bad.len() as u64;
1633        // kills ogg L226 guard `< file_len` -> `true`: under `true` this returns
1634        // NeedMore; correct is Err.
1635        // kills ogg L226 `<` -> `<=`: `len <= len` is true -> NeedMore; correct is Err.
1636        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        // L = 100_000 bytes of garbage (read_header errors): L > 64*1024 so `.max`
1645        // does not mask, and file_len = 10_000_000 > L*2 so `.min` does not clamp.
1646        // Correct up_to = L*2 = 200_000. `+`->100_002, `/`->50_000 all differ.
1647        let buf = vec![0u8; 100_000]; // all zeros: capture pattern != "OggS" -> errors
1648        // Confirm the premise: read_header genuinely errors on this buffer.
1649        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        // The `*` at L227 col 74 is the `64 * 1024` FLOOR in `.max(64 * 1024)`,
1660        // not the doubling. To exercise it the floor must bind: a tiny prefix whose
1661        // doubled length (200) is below 64 KiB, with file_len well above 64 KiB so
1662        // `.min(file_len)` doesn't clamp. Correct floor = 65_536.
1663        let buf = vec![0u8; 100]; // garbage: read_header errors
1664        assert!(read_header(&buf).is_err());
1665        let file_len = 10_000_000u64;
1666        // kills ogg L227 `64 * 1024` -> `64 + 1024` (=1088) and `64 / 1024` (=0):
1667        // only `*` yields the 65_536 floor when the doubled length is smaller.
1668        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        // Pins the TRUE side of the guard: a truncated valid-prefix where
1677        // prefix.len() < file_len must return NeedMore (kills `<`->`<=` from the
1678        // other direction by requiring growth here while requiring Err when equal).
1679        let (full, _audio_offset) = opus_stream();
1680        let file_len = full.len() as u64;
1681        let prefix = &full[..10]; // far short of the header region
1682        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}