Skip to main content

musefs_format/
flac.rs

1use crate::bytes::read_u32_be;
2use crate::error::{FormatError, Result};
3use crate::probe::Extent;
4use crate::size;
5
6pub(crate) const FLAC_MARKER: &[u8; 4] = b"fLaC";
7
8pub(crate) const BLOCK_STREAMINFO: u8 = 0;
9pub(crate) const BLOCK_APPLICATION: u8 = 2;
10pub(crate) const BLOCK_SEEKTABLE: u8 = 3;
11pub(crate) const BLOCK_VORBIS_COMMENT: u8 = 4;
12pub(crate) const BLOCK_CUESHEET: u8 = 5;
13pub(crate) const BLOCK_PICTURE: u8 = 6;
14
15/// A preserved FLAC metadata block: its type and its body (excluding the 4-byte header).
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct MetadataBlock {
18    pub block_type: u8,
19    pub body: Vec<u8>,
20}
21
22/// Result of scanning a FLAC file: where audio begins/ends and the structural blocks to preserve.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct FlacScan {
25    pub audio_offset: u64,
26    pub audio_length: u64,
27    pub preserved: Vec<MetadataBlock>,
28}
29
30/// The metadata region of a FLAC file: where audio begins and the structural
31/// blocks to carry over. Unlike `FlacScan`, this does not include `audio_length`
32/// (which requires the full file size), so it can be computed from the front alone.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct FlacMeta {
35    pub audio_offset: u64,
36    pub preserved: Vec<MetadataBlock>,
37}
38
39/// Decoded header fields of one FLAC metadata block.
40struct BlockHead {
41    /// 0-based position of this block in the metadata sequence.
42    index: usize,
43    /// The 7-bit block type (`BLOCK_*`).
44    block_type: u8,
45    /// Declared body length in bytes.
46    len: usize,
47}
48
49/// One step of a metadata-block walk produced by [`BlockWalker::next_block`].
50enum BlockStep<'a> {
51    /// A complete block: its decoded header plus an in-bounds view of its body.
52    Ready(BlockHead, &'a [u8]),
53    /// The header decoded but its declared body runs past the data. `up_to` is the
54    /// byte index just past the body — what a bounded reader must widen the window
55    /// to before retrying.
56    Truncated(BlockHead, u64),
57    /// Fewer than the 4 header bytes remain. `up_to` is `pos + 4`.
58    NeedHeader(u64),
59}
60
61/// Walks the metadata blocks of a FLAC stream after the `fLaC` marker, decoding
62/// one block header per [`BlockWalker::next_block`] call. This centralizes the
63/// marker check, header decode, 24-bit length, and body-bounds arithmetic shared
64/// by the four metadata readers; each caller supplies its own short-body policy
65/// (`Malformed` vs `NeedMore`), STREAMINFO validation, and per-block action.
66struct BlockWalker<'a> {
67    data: &'a [u8],
68    pos: usize,
69    index: usize,
70    done: bool,
71}
72
73impl<'a> BlockWalker<'a> {
74    /// Validate the `fLaC` marker and position at the first metadata block.
75    fn new(data: &'a [u8]) -> Result<Self> {
76        if data.len() < 4 || &data[0..4] != FLAC_MARKER {
77            return Err(FormatError::NotFlac);
78        }
79        Ok(Self {
80            data,
81            pos: 4,
82            index: 0,
83            done: false,
84        })
85    }
86
87    /// Byte offset just past the last fully-walked block. Once the walk has
88    /// completed (the `is_last` block was returned as `Ready`), this is the
89    /// audio offset.
90    fn audio_offset(&self) -> u64 {
91        self.pos as u64
92    }
93
94    /// Decode the next block header, or `None` once the last block has been
95    /// returned or a short/truncated read has been reported.
96    fn next_block(&mut self) -> Option<BlockStep<'a>> {
97        if self.done {
98            return None;
99        }
100        if self.pos + 4 > self.data.len() {
101            // Need at least the 4-byte block header.
102            self.done = true;
103            return Some(BlockStep::NeedHeader((self.pos + 4) as u64));
104        }
105        let header = self.data[self.pos];
106        let is_last = (header & 0x80) != 0;
107        let block_type = header & 0x7F;
108        let len = u24_be(
109            self.data[self.pos + 1],
110            self.data[self.pos + 2],
111            self.data[self.pos + 3],
112        );
113        let head = BlockHead {
114            index: self.index,
115            block_type,
116            len,
117        };
118        let body_start = self.pos + 4;
119        let body_end = body_start + len;
120        if body_end > self.data.len() {
121            self.done = true;
122            return Some(BlockStep::Truncated(head, body_end as u64));
123        }
124        self.pos = body_end;
125        self.index += 1;
126        if is_last {
127            self.done = true;
128        }
129        Some(BlockStep::Ready(head, &self.data[body_start..body_end]))
130    }
131}
132
133fn parse_blocks(data: &[u8]) -> Result<FlacMeta> {
134    let mut walker = BlockWalker::new(data)?;
135    let mut preserved = Vec::new();
136    while let Some(step) = walker.next_block() {
137        match step {
138            BlockStep::Ready(head, body) => {
139                check_streaminfo_position(head.index, head.block_type, head.len)?;
140                if matches!(
141                    head.block_type,
142                    BLOCK_STREAMINFO | BLOCK_APPLICATION | BLOCK_SEEKTABLE | BLOCK_CUESHEET
143                ) {
144                    preserved.push(MetadataBlock {
145                        block_type: head.block_type,
146                        body: body.to_vec(),
147                    });
148                }
149            }
150            BlockStep::Truncated(..) | BlockStep::NeedHeader(_) => {
151                return Err(FormatError::Malformed);
152            }
153        }
154    }
155    Ok(FlacMeta {
156        audio_offset: walker.audio_offset(),
157        preserved,
158    })
159}
160
161/// Parse just the FLAC metadata region (the front of the file), recovering the
162/// audio boundary and structural blocks. Use when the audio length is already
163/// known (e.g. stored in a database) and the full file should not be read.
164pub fn read_metadata(data: &[u8]) -> Result<FlacMeta> {
165    parse_blocks(data)
166}
167
168/// Bounded twin of [`read_metadata`]: walk the metadata blocks present in
169/// `prefix` (which may be a front-only window of the file). If a block's declared
170/// body runs past the prefix, return `NeedMore { up_to }` with the exact end of
171/// that block — the caller widens the window and retries. Otherwise `Complete`.
172pub fn read_metadata_bounded(prefix: &[u8]) -> Result<Extent<FlacMeta>> {
173    let mut walker = BlockWalker::new(prefix)?;
174    let mut preserved = Vec::new();
175    while let Some(step) = walker.next_block() {
176        match step {
177            BlockStep::Ready(head, body) => {
178                check_streaminfo_position(head.index, head.block_type, head.len)?;
179                if matches!(
180                    head.block_type,
181                    BLOCK_STREAMINFO | BLOCK_APPLICATION | BLOCK_SEEKTABLE | BLOCK_CUESHEET
182                ) {
183                    preserved.push(MetadataBlock {
184                        block_type: head.block_type,
185                        body: body.to_vec(),
186                    });
187                }
188            }
189            BlockStep::Truncated(head, up_to) => {
190                // Header-only validation: fail closed on a bad STREAMINFO header
191                // (wrong position/length, or a duplicate) before widening the probe
192                // to read a body that can never make the file valid.
193                check_streaminfo_position(head.index, head.block_type, head.len)?;
194                return Ok(Extent::NeedMore { up_to });
195            }
196            BlockStep::NeedHeader(up_to) => {
197                return Ok(Extent::NeedMore { up_to });
198            }
199        }
200    }
201    Ok(Extent::Complete(FlacMeta {
202        audio_offset: walker.audio_offset(),
203        preserved,
204    }))
205}
206
207/// Parse the FLAC metadata section of a complete file, returning the audio
208/// boundary, audio length, and the structural blocks to carry over.
209pub fn locate_audio(data: &[u8]) -> Result<FlacScan> {
210    let meta = parse_blocks(data)?;
211    Ok(FlacScan {
212        audio_offset: meta.audio_offset,
213        audio_length: data.len() as u64 - meta.audio_offset,
214        preserved: meta.preserved,
215    })
216}
217
218use crate::input::{
219    ArtInput, BinaryTagInput, EmbeddedBinaryTag, EmbeddedPicture, PictureType, TagInput,
220};
221use crate::layout::{RegionLayout, Segment};
222
223/// Inclusive maximum body length of a FLAC metadata block (24-bit length field).
224pub const MAX_BLOCK_BODY: u64 = 0x00FF_FFFF;
225
226/// FLAC mandates a single STREAMINFO metadata block, first in the sequence, with
227/// a fixed 34-byte body.
228const STREAMINFO_BODY_LEN: usize = 34;
229
230/// Enforce FLAC's STREAMINFO rule for the metadata block at position `index`:
231/// the first block must be STREAMINFO with a 34-byte body, and STREAMINFO must
232/// not appear anywhere else (so it appears exactly once). Any violation is
233/// `FormatError::Malformed`.
234fn check_streaminfo_position(index: usize, block_type: u8, body_len: usize) -> Result<()> {
235    let is_streaminfo = block_type == BLOCK_STREAMINFO;
236    if index == 0 {
237        if !is_streaminfo || body_len != STREAMINFO_BODY_LEN {
238            return Err(FormatError::Malformed);
239        }
240    } else if is_streaminfo {
241        return Err(FormatError::Malformed);
242    }
243    Ok(())
244}
245
246pub(crate) fn push_block_header(
247    out: &mut Vec<u8>,
248    block_type: u8,
249    body_len: usize,
250    is_last: bool,
251) -> Result<()> {
252    // A FLAC block length is a 24-bit field; refuse anything larger rather
253    // than emit a truncated length.
254    let len = u32::try_from(body_len)
255        .ok()
256        .filter(|&v| u64::from(v) <= MAX_BLOCK_BODY)
257        .ok_or(FormatError::TooLarge)?;
258    let first = (if is_last { 0x80 } else { 0 }) | (block_type & 0x7F);
259    out.push(first);
260    out.extend_from_slice(&len.to_be_bytes()[1..]);
261    Ok(())
262}
263
264/// Map a stored structural-block `kind` string back to its FLAC block type.
265/// Only STREAMINFO/SEEKTABLE live in the structural store; everything else
266/// returns `None` (APPLICATION/CUESHEET are binary tags, not structural).
267pub fn structural_block_type(kind: &str) -> Option<u8> {
268    match kind {
269        "STREAMINFO" => Some(BLOCK_STREAMINFO),
270        "SEEKTABLE" => Some(BLOCK_SEEKTABLE),
271        _ => None,
272    }
273}
274
275/// Split a FLAC file's preserved metadata blocks into the read-only structural
276/// store (STREAMINFO/SEEKTABLE, as `(kind, body)` pairs in file order) and the
277/// editable binary tags (APPLICATION/CUESHEET, as `EmbeddedBinaryTag`s keyed by
278/// block name; `payload` is the full block body, including APPLICATION's 4-byte
279/// app id). Blocks of any other type are ignored (PICTURE/VORBIS_COMMENT are
280/// handled by their own paths and are never in `preserved`).
281pub fn split_preserved(
282    blocks: &[MetadataBlock],
283) -> (Vec<(String, Vec<u8>)>, Vec<EmbeddedBinaryTag>) {
284    let mut structural = Vec::new();
285    let mut binary = Vec::new();
286    for blk in blocks {
287        match blk.block_type {
288            BLOCK_STREAMINFO => structural.push(("STREAMINFO".to_string(), blk.body.clone())),
289            BLOCK_SEEKTABLE => structural.push(("SEEKTABLE".to_string(), blk.body.clone())),
290            BLOCK_APPLICATION => binary.push(EmbeddedBinaryTag {
291                key: "APPLICATION".to_string(),
292                payload: blk.body.clone(),
293            }),
294            BLOCK_CUESHEET => binary.push(EmbeddedBinaryTag {
295                key: "CUESHEET".to_string(),
296                payload: blk.body.clone(),
297            }),
298            _ => {}
299        }
300    }
301    (structural, binary)
302}
303
304/// Serialize a FLAC PICTURE block *body* for `art`: type, mime, description,
305/// dimensions, depth+colors placeholders, and the declared image-data length.
306/// The image bytes themselves are not appended. `description` is taken as a
307/// parameter (rather than `art.description`) so the OGG path can pass a
308/// space-padded variant for incremental base64 — see [`crate::ogg`].
309pub(crate) fn picture_body_framing(art: &ArtInput, description: &str) -> Result<Vec<u8>> {
310    let mut out = Vec::new();
311    out.extend_from_slice(&art.picture_type.get().to_be_bytes());
312    out.extend_from_slice(
313        &u32::try_from(art.mime.len())
314            .map_err(|_| FormatError::TooLarge)?
315            .to_be_bytes(),
316    );
317    out.extend_from_slice(art.mime.as_bytes());
318    out.extend_from_slice(
319        &u32::try_from(description.len())
320            .map_err(|_| FormatError::TooLarge)?
321            .to_be_bytes(),
322    );
323    out.extend_from_slice(description.as_bytes());
324    out.extend_from_slice(&art.width.to_be_bytes());
325    out.extend_from_slice(&art.height.to_be_bytes());
326    out.extend_from_slice(&0u32.to_be_bytes()); // color depth (unknown)
327    out.extend_from_slice(&0u32.to_be_bytes()); // number of colors (non-indexed)
328    out.extend_from_slice(
329        &u32::try_from(art.data_len.get())
330            .map_err(|_| FormatError::TooLarge)?
331            .to_be_bytes(),
332    ); // picture data length
333    Ok(out)
334}
335
336/// Build the ordered segment layout for a synthesized FLAC file:
337/// `fLaC` + structural blocks (sorted by type) + a regenerated VORBIS_COMMENT +
338/// streamed APPLICATION/CUESHEET binary tags + PICTURE blocks (one `ArtImage`
339/// segment each) + the backing audio.  Structural blocks must be only
340/// STREAMINFO/SEEKTABLE; APPLICATION/CUESHEET ride through `binary_tags`.
341pub fn synthesize_layout(
342    structural: &[MetadataBlock],
343    audio_offset: u64,
344    audio_length: u64,
345    tags: &[TagInput],
346    binary_tags: &[BinaryTagInput],
347    arts: &[ArtInput],
348) -> Result<RegionLayout> {
349    let streaminfo: Vec<&MetadataBlock> = structural
350        .iter()
351        .filter(|b| b.block_type == BLOCK_STREAMINFO)
352        .collect();
353    if streaminfo.len() != 1 || streaminfo[0].body.len() != STREAMINFO_BODY_LEN {
354        return Err(FormatError::Malformed);
355    }
356
357    let mut ordered: Vec<&MetadataBlock> = structural.iter().collect();
358    ordered.sort_by_key(|b| b.block_type);
359
360    let valid_binary: Vec<&BinaryTagInput> = binary_tags
361        .iter()
362        .filter(|bt| matches!(bt.key.as_str(), "APPLICATION" | "CUESHEET"))
363        .collect();
364
365    let nonempty_art = arts.len();
366    let num_blocks = ordered.len() + 1 + valid_binary.len() + nonempty_art;
367    let last_index = num_blocks - 1;
368
369    let mut segments: Vec<Segment> = Vec::new();
370    let mut buf: Vec<u8> = Vec::new();
371    buf.extend_from_slice(FLAC_MARKER);
372    let mut idx = 0usize;
373
374    for blk in &ordered {
375        push_block_header(&mut buf, blk.block_type, blk.body.len(), idx == last_index)?;
376        buf.extend_from_slice(&blk.body);
377        idx += 1;
378    }
379
380    let vc = crate::vorbiscomment::build(tags)?;
381    if vc.len() as u64 > MAX_BLOCK_BODY {
382        return Err(FormatError::TooLarge);
383    }
384    push_block_header(&mut buf, BLOCK_VORBIS_COMMENT, vc.len(), idx == last_index)?;
385    buf.extend_from_slice(&vc);
386    idx += 1;
387
388    for bt in valid_binary {
389        let block_type = match bt.key.as_str() {
390            "APPLICATION" => BLOCK_APPLICATION,
391            "CUESHEET" => BLOCK_CUESHEET,
392            _ => continue,
393        };
394        if bt.len.get() > MAX_BLOCK_BODY {
395            return Err(FormatError::TooLarge);
396        }
397        push_block_header(
398            &mut buf,
399            block_type,
400            crate::convert::usize_from(bt.len.get()),
401            idx == last_index,
402        )?;
403        segments.push(Segment::Inline(std::mem::take(&mut buf)));
404        segments.push(Segment::BinaryTag {
405            payload_id: bt.payload_id,
406            len: bt.len,
407        });
408        idx += 1;
409    }
410
411    for art in arts {
412        let framing = picture_body_framing(art, &art.description)?;
413        let body_len = size::checked_add(framing.len() as u64, art.data_len.get())?;
414        if body_len > MAX_BLOCK_BODY {
415            return Err(FormatError::TooLarge);
416        }
417        push_block_header(
418            &mut buf,
419            BLOCK_PICTURE,
420            crate::convert::usize_from(body_len),
421            idx == last_index,
422        )?;
423        buf.extend_from_slice(&framing);
424        segments.push(Segment::Inline(std::mem::take(&mut buf)));
425        segments.push(Segment::ArtImage {
426            art_id: art.art_id,
427            len: art.data_len,
428        });
429        idx += 1;
430    }
431
432    if !buf.is_empty() {
433        segments.push(Segment::Inline(buf));
434    }
435    segments.push(Segment::BackingAudio {
436        offset: audio_offset,
437        len: audio_length,
438    });
439
440    Ok(RegionLayout::validated(segments)?)
441}
442
443/// Read the existing VORBIS_COMMENT block from a complete FLAC file, returning
444/// `(FIELD, value)` pairs in order. Comments without a `=` are skipped. Returns
445/// an empty vec if there is no comment block. Used by the scanner to seed tags.
446pub fn read_vorbis_comments(data: &[u8]) -> Result<Vec<(String, String)>> {
447    let mut walker = BlockWalker::new(data)?;
448    while let Some(step) = walker.next_block() {
449        match step {
450            BlockStep::Ready(head, body) => {
451                if head.block_type == BLOCK_VORBIS_COMMENT {
452                    return crate::vorbiscomment::parse(body);
453                }
454            }
455            BlockStep::Truncated(..) | BlockStep::NeedHeader(_) => {
456                return Err(FormatError::Malformed);
457            }
458        }
459    }
460    Ok(Vec::new())
461}
462
463/// Assemble a 24-bit big-endian block length from its three raw bytes.
464fn u24_be(b0: u8, b1: u8, b2: u8) -> usize {
465    u32::from_be_bytes([0, b0, b1, b2]) as usize
466}
467
468pub(crate) fn parse_picture_block(body: &[u8]) -> Result<EmbeddedPicture> {
469    let mut pos = 0usize;
470    let picture_type = read_u32_be(body, pos)?;
471    pos += 4;
472    let mime_len = read_u32_be(body, pos)? as usize;
473    pos += 4;
474    let mime_end = pos + mime_len;
475    if mime_end > body.len() {
476        return Err(FormatError::Malformed);
477    }
478    let mime = String::from_utf8_lossy(&body[pos..mime_end]).into_owned();
479    pos = mime_end;
480    let desc_len = read_u32_be(body, pos)? as usize;
481    pos += 4;
482    let desc_end = pos + desc_len;
483    if desc_end > body.len() {
484        return Err(FormatError::Malformed);
485    }
486    let description = String::from_utf8_lossy(&body[pos..desc_end]).into_owned();
487    pos = desc_end;
488    let width = read_u32_be(body, pos)?;
489    pos += 4;
490    let height = read_u32_be(body, pos)?;
491    pos += 4;
492    let _depth = read_u32_be(body, pos)?;
493    pos += 4;
494    let _colors = read_u32_be(body, pos)?;
495    pos += 4;
496    let data_len = read_u32_be(body, pos)? as usize;
497    pos += 4;
498    let data_end = pos + data_len;
499    if data_end > body.len() {
500        return Err(FormatError::Malformed);
501    }
502    Ok(EmbeddedPicture {
503        mime,
504        picture_type: PictureType::new(picture_type).unwrap_or(PictureType::ZERO),
505        description,
506        width,
507        height,
508        data: body[pos..data_end].to_vec(),
509    })
510}
511
512/// Extract all PICTURE blocks from a complete FLAC file as embedded pictures, for
513/// scan-time art ingestion. Returns an empty vec if there are none.
514pub fn read_pictures(data: &[u8]) -> Result<Vec<EmbeddedPicture>> {
515    let mut walker = BlockWalker::new(data)?;
516    let mut out = Vec::new();
517    while let Some(step) = walker.next_block() {
518        match step {
519            BlockStep::Ready(head, body) => {
520                if head.block_type == BLOCK_PICTURE {
521                    out.push(parse_picture_block(body)?);
522                }
523            }
524            BlockStep::Truncated(..) | BlockStep::NeedHeader(_) => {
525                return Err(FormatError::Malformed);
526            }
527        }
528    }
529    Ok(out)
530}
531
532#[cfg(test)]
533mod tests {
534    use super::*;
535    use crate::input::{BlobLen, PictureType};
536    use crate::probe::Extent;
537
538    /// Build a minimal FLAC: marker + a single last STREAMINFO (type 0, 34-byte
539    /// body) + `audio` bytes. Returns (full_bytes, audio_offset).
540    fn flac_with_streaminfo(audio: &[u8]) -> (Vec<u8>, u64) {
541        let mut v = b"fLaC".to_vec();
542        push_block_header(&mut v, BLOCK_STREAMINFO, 34, true).unwrap();
543        v.extend(std::iter::repeat_n(0u8, 34));
544        let audio_offset = v.len() as u64;
545        v.extend_from_slice(audio);
546        (v, audio_offset)
547    }
548
549    #[test]
550    fn read_metadata_bounded_complete_when_prefix_covers_blocks() {
551        let (full, audio_offset) = flac_with_streaminfo(b"AUDIOAUDIO");
552        // Prefix that includes all metadata but not all audio.
553        let prefix = &full[..crate::convert::usize_from(audio_offset) + 2];
554        match read_metadata_bounded(prefix).unwrap() {
555            Extent::Complete(meta) => assert_eq!(meta.audio_offset, audio_offset),
556            other @ Extent::NeedMore { .. } => panic!("expected Complete, got {other:?}"),
557        }
558    }
559
560    #[test]
561    fn read_metadata_bounded_needmore_when_block_body_truncated() {
562        let (full, audio_offset) = flac_with_streaminfo(b"AUDIO");
563        // Cut inside the STREAMINFO body (header is 4 bytes after the marker).
564        let prefix = &full[..8];
565        match read_metadata_bounded(prefix).unwrap() {
566            Extent::NeedMore { up_to } => assert_eq!(up_to, audio_offset),
567            other @ Extent::Complete(_) => panic!("expected NeedMore, got {other:?}"),
568        }
569    }
570
571    #[test]
572    fn read_u32_be_assembles_big_endian_and_guards_length() {
573        let data = [0x11u8, 0x22, 0x33, 0x44, 0x55];
574        assert_eq!(read_u32_be(&data, 0).unwrap(), 0x1122_3344);
575        // pins :224 (`+` -> `*`): at pos=1 the second byte is data[2]=0x33, not data[1].
576        // pins :219 (`>` -> `==`/`>=`): pos+4 == len (5) is valid, so this unwrap must
577        // succeed — a mutated bound returns Err here and the unwrap panics.
578        assert_eq!(read_u32_be(&data, 1).unwrap(), 0x2233_4455);
579        assert_eq!(read_u32_be(&data, 2), Err(FormatError::Malformed));
580    }
581
582    #[test]
583    fn push_block_header_emits_24bit_length_big_endian() {
584        // pins :101 (`>>16` -> `<<16`): high byte 0x12 must land in out[1].
585        let mut out = Vec::new();
586        push_block_header(&mut out, BLOCK_PICTURE, 0x12_3456, false).unwrap();
587        assert_eq!(out, vec![BLOCK_PICTURE, 0x12, 0x34, 0x56]);
588        // :99 is equivalent, but exercise the is_last/0x80 path anyway.
589        let mut last = Vec::new();
590        push_block_header(&mut last, BLOCK_VORBIS_COMMENT, 0, true).unwrap();
591        assert_eq!(last, vec![0x80 | BLOCK_VORBIS_COMMENT, 0x00, 0x00, 0x00]);
592    }
593
594    /// One FLAC metadata block: 4-byte header (last-flag, type, 24-bit BE length)
595    /// + body, built independently of production framing so a mutation in
596    ///   `push_block_header` cannot mask a fixture. `len_override` lets a test claim a
597    ///   length different from `body.len()`.
598    fn raw_block(block_type: u8, body: &[u8], last: bool, len_override: Option<usize>) -> Vec<u8> {
599        let n = len_override.unwrap_or(body.len());
600        let mut v = vec![(if last { 0x80 } else { 0 }) | (block_type & 0x7F)];
601        v.extend_from_slice(&u32::try_from(n).unwrap().to_be_bytes()[1..]);
602        v.extend_from_slice(body);
603        v
604    }
605
606    /// `fLaC` + the given blocks (no audio).
607    fn flac_with(blocks: &[Vec<u8>]) -> Vec<u8> {
608        let mut f = b"fLaC".to_vec();
609        for b in blocks {
610            f.extend_from_slice(b);
611        }
612        f
613    }
614
615    /// A structural STREAMINFO block with a valid 34-byte body, for synthesis tests.
616    fn valid_streaminfo() -> MetadataBlock {
617        MetadataBlock {
618            block_type: BLOCK_STREAMINFO,
619            body: vec![0u8; 34],
620        }
621    }
622
623    #[test]
624    fn locate_audio_rejects_missing_streaminfo() {
625        // First (and only) block is VORBIS_COMMENT, no STREAMINFO at all.
626        let file = flac_with(&[raw_block(BLOCK_VORBIS_COMMENT, &[], true, None)]);
627        assert_eq!(locate_audio(&file), Err(FormatError::Malformed));
628    }
629
630    #[test]
631    fn locate_audio_rejects_streaminfo_wrong_body_len() {
632        // STREAMINFO present and first, but body is 10 bytes, not 34.
633        let file = flac_with(&[raw_block(BLOCK_STREAMINFO, &[0u8; 10], true, None)]);
634        assert_eq!(locate_audio(&file), Err(FormatError::Malformed));
635    }
636
637    #[test]
638    fn locate_audio_rejects_duplicate_streaminfo() {
639        let file = flac_with(&[
640            raw_block(BLOCK_STREAMINFO, &[0u8; 34], false, None),
641            raw_block(BLOCK_STREAMINFO, &[0u8; 34], true, None),
642        ]);
643        assert_eq!(locate_audio(&file), Err(FormatError::Malformed));
644    }
645
646    #[test]
647    fn read_metadata_bounded_rejects_duplicate_streaminfo() {
648        let file = flac_with(&[
649            raw_block(BLOCK_STREAMINFO, &[0u8; 34], false, None),
650            raw_block(BLOCK_STREAMINFO, &[0u8; 34], true, None),
651        ]);
652        assert_eq!(read_metadata_bounded(&file), Err(FormatError::Malformed));
653    }
654
655    #[test]
656    fn bounded_fails_closed_on_bad_first_header_before_widening() {
657        // First block header declares STREAMINFO with a non-34 body length, and
658        // the body is absent from the prefix. The header alone is invalid, so we
659        // must reject with Malformed immediately rather than return NeedMore and
660        // drive the prober to widen toward a body that can't make the file valid.
661        let mut prefix = b"fLaC".to_vec();
662        prefix.push(BLOCK_STREAMINFO | 0x80); // last STREAMINFO
663        prefix.extend_from_slice(&[0xFF, 0xFF, 0xFF]); // len = 0xFFFFFF, body absent
664        assert_eq!(read_metadata_bounded(&prefix), Err(FormatError::Malformed));
665    }
666
667    #[test]
668    fn synthesize_layout_rejects_structural_without_streaminfo() {
669        // Hostile DB rows: a SEEKTABLE but no STREAMINFO.
670        let structural = [MetadataBlock {
671            block_type: BLOCK_SEEKTABLE,
672            body: vec![0u8; 4],
673        }];
674        assert_eq!(
675            synthesize_layout(&structural, 0, 0, &[], &[], &[]),
676            Err(FormatError::Malformed)
677        );
678    }
679
680    #[test]
681    fn parse_blocks_rejects_short_and_wrong_marker() {
682        // :37 `< -> ==`: 3-byte input -> original short-circuits NotFlac; the mutant
683        // evaluates &data[0..4] on 3 bytes -> panic. Asserting Err(NotFlac) kills it.
684        assert_eq!(parse_blocks(b"fLa"), Err(FormatError::NotFlac));
685        // :37 `< -> <=`: a 4-byte fLaC-only file. Original proceeds then hits the
686        // loop guard -> Malformed; the `<=` mutant short-circuits to NotFlac.
687        assert_eq!(parse_blocks(b"fLaC"), Err(FormatError::Malformed));
688        assert_eq!(parse_blocks(b"XXXX____"), Err(FormatError::NotFlac));
689    }
690
691    #[test]
692    fn parse_blocks_guards_truncated_block_header() {
693        // 5 bytes: marker + 1 header byte. Original: pos+4=8 > 5 -> Malformed.
694        // :43 `+ -> -` (0 > 5 false) and `> -> ==` (8 == 5 false) both fall through
695        // and panic reading data[5..8].
696        assert_eq!(parse_blocks(b"fLaC\x80"), Err(FormatError::Malformed));
697    }
698
699    #[test]
700    fn parse_blocks_accepts_header_flush_with_end() {
701        // Single last STREAMINFO with a valid 34-byte body, no audio: the final header
702        // still flushes at the buffer end (pos+4 == data.len() at the loop guard), so
703        // this keeps the :43 `> -> >=` mutant coverage while obeying STREAMINFO rules.
704        let file = flac_with(&[raw_block(BLOCK_STREAMINFO, &[0u8; 34], true, None)]);
705        let meta = parse_blocks(&file).unwrap();
706        assert_eq!(meta.audio_offset, 42); // 4 marker + 4 header + 34 body
707    }
708
709    #[test]
710    fn parse_blocks_decodes_24bit_length_high_byte() {
711        // STREAMINFO header claims length 0x010000 (high byte set) over an empty body.
712        // Pins the high byte of the 24-bit length decode: len = 65536 -> body_end >
713        // data.len() -> Malformed; a decode that drops the high byte gets len 0 -> Ok.
714        let file = flac_with(&[raw_block(BLOCK_STREAMINFO, &[], true, Some(0x01_0000))]);
715        assert_eq!(parse_blocks(&file), Err(FormatError::Malformed));
716    }
717
718    #[test]
719    fn parse_blocks_preserves_structural_blocks() {
720        // Positive decode: a normal STREAMINFO (34-byte body) + audio boundary.
721        let si = vec![0xAA; 34];
722        let file = flac_with(&[raw_block(BLOCK_STREAMINFO, &si, true, None)]);
723        let meta = parse_blocks(&file).unwrap();
724        assert_eq!(meta.audio_offset, 4 + 4 + 34);
725        assert_eq!(meta.preserved.len(), 1);
726        assert_eq!(meta.preserved[0].block_type, BLOCK_STREAMINFO);
727        assert_eq!(meta.preserved[0].body, si);
728    }
729
730    /// A VORBIS_COMMENT body: u32-LE vendor length, vendor, u32-LE count, then each
731    /// comment as u32-LE length + bytes.
732    fn vc_body(vendor: &str, comments: &[&str]) -> Vec<u8> {
733        let mut v = Vec::new();
734        v.extend_from_slice(&u32::try_from(vendor.len()).unwrap().to_le_bytes());
735        v.extend_from_slice(vendor.as_bytes());
736        v.extend_from_slice(&u32::try_from(comments.len()).unwrap().to_le_bytes());
737        for c in comments {
738            v.extend_from_slice(&u32::try_from(c.len()).unwrap().to_le_bytes());
739            v.extend_from_slice(c.as_bytes());
740        }
741        v
742    }
743
744    #[test]
745    fn read_vorbis_comments_returns_pairs_and_guards_marker() {
746        // Happy path: VC block is the last block with no audio, so body_end == len.
747        // This also pins :204 (`>` -> `==`/`>=`): the mutant would reject (Malformed)
748        // and the unwrap below would panic.
749        let vc = vc_body("v", &["TITLE=Hi", "ARTIST=Me"]);
750        let file = flac_with(&[raw_block(BLOCK_VORBIS_COMMENT, &vc, true, None)]);
751        let got = read_vorbis_comments(&file).unwrap();
752        assert_eq!(
753            got,
754            vec![
755                ("title".to_string(), "Hi".to_string()),
756                ("artist".to_string(), "Me".to_string()),
757            ]
758        );
759        // :188 `< -> ==` and `|| -> &&`: 3-byte input -> original NotFlac via
760        // short-circuit; both mutants force &data[0..4] -> panic.
761        assert_eq!(read_vorbis_comments(b"fLa"), Err(FormatError::NotFlac));
762        // :188 `< -> <=`: 4-byte fLaC -> original Malformed; mutant NotFlac.
763        assert_eq!(read_vorbis_comments(b"fLaC"), Err(FormatError::Malformed));
764    }
765
766    #[test]
767    fn read_vorbis_comments_guards_block_walk() {
768        // :193 `+ -> -` and `> -> ==`: truncated header -> original Malformed,
769        // mutants fall through and panic.
770        assert_eq!(
771            read_vorbis_comments(b"fLaC\x80"),
772            Err(FormatError::Malformed)
773        );
774        // :193 `> -> >=`: a non-VC last block flush with end -> original returns the
775        // empty vec; the `>=` mutant rejects at the loop guard.
776        let file = flac_with(&[raw_block(BLOCK_STREAMINFO, &[], true, None)]);
777        assert_eq!(read_vorbis_comments(&file).unwrap(), Vec::new());
778    }
779
780    #[test]
781    fn read_vorbis_comments_decodes_24bit_length() {
782        // High length byte set over a short body: len = 0x10000 -> Malformed. Pins
783        // the high byte of the 24-bit length decode (dropping it gets len 0 -> Ok).
784        let hi = flac_with(&[raw_block(BLOCK_STREAMINFO, &[], true, Some(0x01_0000))]);
785        assert_eq!(read_vorbis_comments(&hi), Err(FormatError::Malformed));
786        // Mid length byte set, high byte 0: len = 0x100 -> Malformed. Pins the mid
787        // byte (dropping it gets len 0 -> Ok).
788        let mid = flac_with(&[raw_block(BLOCK_STREAMINFO, &[], true, Some(0x00_0100))]);
789        assert_eq!(read_vorbis_comments(&mid), Err(FormatError::Malformed));
790    }
791
792    /// A FLAC PICTURE block body (big-endian fields), independent of production.
793    fn picture_body(ptype: u32, mime: &str, desc: &str, w: u32, h: u32, data: &[u8]) -> Vec<u8> {
794        let mut v = Vec::new();
795        v.extend_from_slice(&ptype.to_be_bytes());
796        v.extend_from_slice(&u32::try_from(mime.len()).unwrap().to_be_bytes());
797        v.extend_from_slice(mime.as_bytes());
798        v.extend_from_slice(&u32::try_from(desc.len()).unwrap().to_be_bytes());
799        v.extend_from_slice(desc.as_bytes());
800        v.extend_from_slice(&w.to_be_bytes());
801        v.extend_from_slice(&h.to_be_bytes());
802        v.extend_from_slice(&0u32.to_be_bytes()); // depth
803        v.extend_from_slice(&0u32.to_be_bytes()); // colors
804        v.extend_from_slice(&u32::try_from(data.len()).unwrap().to_be_bytes());
805        v.extend_from_slice(data);
806        v
807    }
808
809    #[test]
810    fn parse_picture_block_roundtrips_fields() {
811        let body = picture_body(3, "image/png", "desc", 4, 5, b"PIXELS");
812        let p = parse_picture_block(&body).unwrap();
813        assert_eq!(p.picture_type.get(), 3);
814        assert_eq!(p.mime, "image/png");
815        assert_eq!(p.description, "desc");
816        assert_eq!(p.width, 4);
817        assert_eq!(p.height, 5);
818        assert_eq!(p.data, b"PIXELS");
819    }
820
821    #[test]
822    fn read_picture_clamps_out_of_range_type() {
823        let body = picture_body(99, "png", "", 0, 0, &[0xAB]);
824        let pic = parse_picture_block(&body).unwrap();
825        assert_eq!(pic.picture_type.get(), 0, "out-of-range type clamps to 0");
826    }
827
828    #[test]
829    fn parse_picture_block_guards_field_bounds() {
830        // :237 `> -> ==` (mime bound): claim mime_len far past the end. Original
831        // Malformed; the `==` mutant falls through to slice body[8..8+mime_len] -> panic.
832        let mut bad_mime = 3u32.to_be_bytes().to_vec();
833        bad_mime.extend_from_slice(&16u32.to_be_bytes()); // mime_len = 16
834        bad_mime.extend_from_slice(b"ab"); // only 2 bytes present
835        assert_eq!(parse_picture_block(&bad_mime), Err(FormatError::Malformed));
836
837        // :245 `> -> ==` (desc bound): valid mime, then claim desc_len past the end.
838        let mut bad_desc = 3u32.to_be_bytes().to_vec();
839        bad_desc.extend_from_slice(&3u32.to_be_bytes()); // mime_len = 3
840        bad_desc.extend_from_slice(b"png");
841        bad_desc.extend_from_slice(&16u32.to_be_bytes()); // desc_len = 16
842        bad_desc.extend_from_slice(b"x"); // only 1 byte present
843        assert_eq!(parse_picture_block(&bad_desc), Err(FormatError::Malformed));
844
845        // :261 `> -> <` (data bound): a fully valid picture body with TRAILING bytes.
846        // Original ignores the trailing byte (data_end < len, not >) and returns Ok;
847        // the `<` mutant rejects (data_end < len -> Malformed).
848        let mut trailing = picture_body(3, "png", "", 1, 1, b"DA");
849        trailing.push(0xFF); // one extra trailing byte
850        assert!(parse_picture_block(&trailing).is_ok());
851    }
852
853    #[test]
854    fn read_pictures_extracts_and_guards_marker() {
855        // Happy path: one PICTURE block, last, no audio (body_end == len). Pins :294.
856        let pic = picture_body(3, "image/jpeg", "front", 8, 8, b"IMG");
857        let file = flac_with(&[raw_block(BLOCK_PICTURE, &pic, true, None)]);
858        let pics = read_pictures(&file).unwrap();
859        assert_eq!(pics.len(), 1);
860        assert_eq!(pics[0].mime, "image/jpeg");
861        assert_eq!(pics[0].data, b"IMG");
862        // :277 `< -> ==` and `|| -> &&`: 3-byte input -> panic vs NotFlac.
863        assert_eq!(read_pictures(b"fLa"), Err(FormatError::NotFlac));
864        // :277 `< -> <=`: 4-byte fLaC -> Malformed vs NotFlac.
865        assert_eq!(read_pictures(b"fLaC"), Err(FormatError::Malformed));
866    }
867
868    #[test]
869    fn read_pictures_guards_block_walk_and_length() {
870        // :283 `+ -> -`, `> -> ==`: truncated header.
871        assert_eq!(read_pictures(b"fLaC\x80"), Err(FormatError::Malformed));
872        // :283 `> -> >=`: non-PICTURE last block flush with end -> Ok(empty).
873        let none = flac_with(&[raw_block(BLOCK_STREAMINFO, &[], true, None)]);
874        assert_eq!(read_pictures(&none).unwrap(), Vec::new());
875        // High length byte over short body: pins the 24-bit decode's high byte.
876        let hi = flac_with(&[raw_block(BLOCK_STREAMINFO, &[], true, Some(0x01_0000))]);
877        assert_eq!(read_pictures(&hi), Err(FormatError::Malformed));
878        // Mid length byte (high byte 0): pins the 24-bit decode's mid byte.
879        let mid = flac_with(&[raw_block(BLOCK_STREAMINFO, &[], true, Some(0x00_0100))]);
880        assert_eq!(read_pictures(&mid), Err(FormatError::Malformed));
881    }
882
883    // ---- read_metadata_bounded mutant-kill tests (flac.rs:89-133) ----
884
885    #[test]
886    fn bounded_rejects_short_and_wrong_marker() {
887        // kills flac L90 `<` -> `==`/`<=` and `||` -> `&&`:
888        // a 3-byte prefix is too short. Original short-circuits NotFlac; the `==`
889        // mutant (len==4 is false for len 3) and the `&&` mutant force evaluation
890        // of &prefix[0..4] on 3 bytes -> panic. NotFlac kills both.
891        assert_eq!(read_metadata_bounded(b"fLa"), Err(FormatError::NotFlac));
892        // kills flac L90 `<` -> `<=`: a 4-byte "fLaC"-only prefix is exactly the
893        // marker. Original (len 4 < 4 is false) proceeds; since pos+4=8 > 4 it
894        // returns NeedMore{up_to:8}. The `<=` mutant (4 <= 4 true) wrongly returns
895        // NotFlac. Asserting NOT NotFlac (and the exact NeedMore) kills it.
896        match read_metadata_bounded(b"fLaC").unwrap() {
897            Extent::NeedMore { up_to } => assert_eq!(up_to, 8),
898            other @ Extent::Complete(_) => panic!("expected NeedMore{{up_to:8}}, got {other:?}"),
899        }
900        // kills flac L90 marker check: a non-FLAC 4-byte prefix -> NotFlac.
901        assert_eq!(read_metadata_bounded(b"XXXX"), Err(FormatError::NotFlac));
902    }
903
904    #[test]
905    fn bounded_needmore_up_to_is_pos_plus_4_for_truncated_header() {
906        // Marker + a non-last STREAMINFO (valid 34-byte body) then truncated: after the
907        // first block, pos = 4 + 4 + 34 = 42. The prefix ends exactly there, so the
908        // loop guard `pos + 4 > prefix.len()` fires with pos == 42.
909        let file = flac_with(&[raw_block(BLOCK_STREAMINFO, &[0u8; 34], false, None)]);
910        // file is fLaC(4) + header(4) + body(34) = 42 bytes; pos lands at 42 == prefix.len().
911        assert_eq!(file.len(), 42);
912        match read_metadata_bounded(&file).unwrap() {
913            // kills flac L96 `pos + 4 > prefix.len()` `+` -> `-`: with `pos - 4`
914            // (42-4=38) the comparison 38 > 42 is false, so it would NOT return NeedMore
915            // and instead panic reading prefix[42..]. NeedMore here kills it.
916            // kills flac L99 up_to `(pos + 4)` `+` -> `-`/`*`: pos==42 -> correct
917            // up_to == 46. `pos - 4` gives 38; `pos * 4` gives 168. Exact 46 kills both.
918            Extent::NeedMore { up_to } => assert_eq!(up_to, 46),
919            other @ Extent::Complete(_) => panic!("expected NeedMore{{up_to:46}}, got {other:?}"),
920        }
921    }
922
923    #[test]
924    fn bounded_is_last_flag_continues_past_nonlast_block() {
925        // First NON-last STREAMINFO (valid 34-byte body), then a LAST SEEKTABLE.
926        // audio_offset must span BOTH, proving we walked past the non-last block.
927        let b1 = raw_block(BLOCK_STREAMINFO, &[0u8; 34], false, None); // 4+34 = 38
928        let b2 = raw_block(BLOCK_SEEKTABLE, &[0xBB, 0xBB, 0xBB], true, None); // 4+3 = 7
929        let file = flac_with(&[b1, b2]);
930        let expected_offset = (4 + 38 + 7) as u64; // marker + block1 + block2
931        match read_metadata_bounded(&file).unwrap() {
932            Extent::Complete(meta) => {
933                // kills flac `header & 0x80` `&` -> `|`: that mutant stops after block 1
934                // (audio_offset == 4+38 == 42). Spanning both blocks (49) kills it.
935                assert_eq!(meta.audio_offset, expected_offset);
936                assert_eq!(meta.preserved.len(), 2);
937            }
938            other @ Extent::NeedMore { .. } => panic!("expected Complete, got {other:?}"),
939        }
940    }
941
942    #[test]
943    fn bounded_block_type_mask_preserves_streaminfo() {
944        // A single last STREAMINFO (type 0) with a known 34-byte body.
945        let body = vec![0x5A; 34];
946        let file = flac_with(&[raw_block(BLOCK_STREAMINFO, &body, true, None)]);
947        match read_metadata_bounded(&file).unwrap() {
948            Extent::Complete(meta) => {
949                // kills flac L104 `header & 0x7F` `&` -> `|`/`^`: for a last STREAMINFO
950                // the header byte is 0x80 (is_last set, type 0). Correct block_type =
951                // 0x80 & 0x7F = 0. `0x80 | 0x7F` = 0xFF, `0x80 ^ 0x7F` = 0xFF -> neither
952                // matches the STREAMINFO arm -> preserved stays empty. Asserting the
953                // block IS preserved with block_type 0 kills both.
954                assert_eq!(meta.preserved.len(), 1);
955                assert_eq!(meta.preserved[0].block_type, BLOCK_STREAMINFO);
956                assert_eq!(meta.preserved[0].body, body);
957            }
958            other @ Extent::NeedMore { .. } => panic!("expected Complete, got {other:?}"),
959        }
960    }
961
962    #[test]
963    fn bounded_decodes_24bit_length_exactly() {
964        // STREAMINFO (valid, non-last) then a LAST SEEKTABLE whose declared length =
965        // 0x010203 (bytes 0x01,0x02,0x03), exercising all three length positions.
966        // Body is that many bytes so the block fits and we get a Complete with an
967        // exact audio_offset.
968        let b1 = raw_block(BLOCK_STREAMINFO, &[0u8; 34], false, None); // 4+34 = 38
969        let len = 0x01_0203usize;
970        let body = vec![0u8; len];
971        let b2 = raw_block(BLOCK_SEEKTABLE, &body, true, None); // 4+len
972        let file = flac_with(&[b1, b2]);
973        let expected_offset = (4 + 38 + 4 + len) as u64;
974        match read_metadata_bounded(&file).unwrap() {
975            Extent::Complete(meta) => {
976                // The exact audio_offset pins all three bytes of the 24-bit length
977                // decode: losing the high, mid, or low byte shifts body_end and
978                // yields a wrong audio_offset.
979                assert_eq!(meta.audio_offset, expected_offset);
980            }
981            other @ Extent::NeedMore { .. } => panic!("expected Complete, got {other:?}"),
982        }
983    }
984
985    #[test]
986    fn bounded_length_decodes_high_and_mid_bytes() {
987        // Declare length 0x010100 (high byte 0x01, mid byte 0x01, low 0x00); correct
988        // len = 65792. A decode that collapses the high or mid byte asks for a very
989        // different body. The length-bearing block must NOT be the first one (the
990        // first must be a valid 34-byte STREAMINFO), so exercise it on a SEEKTABLE.
991        // Use NeedMore: body is absent, so the correct parse asks for the full body.
992        let b1 = raw_block(BLOCK_STREAMINFO, &[0u8; 34], false, None); // 4 + 34 = 38
993        let b2 = raw_block(BLOCK_SEEKTABLE, &[], true, Some(0x01_0100)); // declares len, body absent
994        let file = flac_with(&[b1, b2]);
995        match read_metadata_bounded(&file).unwrap() {
996            Extent::NeedMore { up_to } => {
997                // body_start = 4 + 38 + 4 = 46; up_to = body_end = 46 + 0x010100.
998                assert_eq!(up_to, 46 + 0x01_0100);
999            }
1000            other @ Extent::Complete(_) => panic!("expected NeedMore, got {other:?}"),
1001        }
1002    }
1003
1004    #[test]
1005    fn bounded_body_end_equal_to_prefix_is_complete() {
1006        // A STREAMINFO (valid 34-byte body, non-last) then a LAST SEEKTABLE whose
1007        // body ends EXACTLY at the prefix end (no audio). body_end == prefix.len().
1008        let b1 = raw_block(BLOCK_STREAMINFO, &[0u8; 34], false, None); // 4+34 = 38
1009        let b2 = raw_block(BLOCK_SEEKTABLE, &[0xCC; 6], true, None); // 4+6 = 10
1010        let file = flac_with(&[b1, b2]);
1011        let total = file.len() as u64; // 4 + 38 + 10 == 52
1012        match read_metadata_bounded(&file).unwrap() {
1013            // kills flac L110 `body_end > prefix.len()` `>` -> `>=`: with `>=`,
1014            // body_end == prefix.len() is true -> wrongly returns NeedMore. Original
1015            // (`>`) proceeds and, since is_last, returns Complete{audio_offset==len}.
1016            Extent::Complete(meta) => assert_eq!(meta.audio_offset, total),
1017            other @ Extent::NeedMore { .. } => {
1018                panic!("expected Complete (exact fit), got {other:?}")
1019            }
1020        }
1021    }
1022
1023    #[test]
1024    fn bounded_preserves_all_structural_block_types() {
1025        // kills flac L116 (delete the STREAMINFO|APPLICATION|SEEKTABLE|CUESHEET arm):
1026        // a prefix containing each preserved type must yield all four in `preserved`.
1027        // Deleting the arm makes `preserved` empty -> these assertions fail.
1028        let b_si = raw_block(BLOCK_STREAMINFO, &[0x01; 34], false, None);
1029        let b_app = raw_block(BLOCK_APPLICATION, &[0x02, 0x02], false, None);
1030        let b_seek = raw_block(BLOCK_SEEKTABLE, &[0x03, 0x03, 0x03], false, None);
1031        let b_cue = raw_block(BLOCK_CUESHEET, &[0x04], true, None);
1032        let file = flac_with(&[b_si, b_app, b_seek, b_cue]);
1033        match read_metadata_bounded(&file).unwrap() {
1034            Extent::Complete(meta) => {
1035                let types: Vec<u8> = meta.preserved.iter().map(|b| b.block_type).collect();
1036                assert_eq!(
1037                    types,
1038                    vec![
1039                        BLOCK_STREAMINFO,
1040                        BLOCK_APPLICATION,
1041                        BLOCK_SEEKTABLE,
1042                        BLOCK_CUESHEET,
1043                    ]
1044                );
1045                assert_eq!(meta.preserved[0].body, vec![0x01; 34]);
1046                assert_eq!(meta.preserved[1].body, vec![0x02, 0x02]);
1047                assert_eq!(meta.preserved[2].body, vec![0x03, 0x03, 0x03]);
1048                assert_eq!(meta.preserved[3].body, vec![0x04]);
1049            }
1050            other @ Extent::NeedMore { .. } => panic!("expected Complete, got {other:?}"),
1051        }
1052    }
1053
1054    #[test]
1055    fn split_preserved_classifies_structural_and_binary() {
1056        use super::{MetadataBlock, split_preserved, structural_block_type};
1057        // STREAMINFO(0), APPLICATION(2), SEEKTABLE(3), CUESHEET(5) in arbitrary order.
1058        let blocks = vec![
1059            MetadataBlock {
1060                block_type: 0,
1061                body: vec![0xAA],
1062            },
1063            MetadataBlock {
1064                block_type: 2,
1065                body: b"testDATA".to_vec(),
1066            },
1067            MetadataBlock {
1068                block_type: 3,
1069                body: vec![0xBB],
1070            },
1071            MetadataBlock {
1072                block_type: 5,
1073                body: vec![0xCC; 4],
1074            },
1075        ];
1076        let (structural, binary) = split_preserved(&blocks);
1077
1078        assert_eq!(
1079            structural,
1080            vec![
1081                ("STREAMINFO".to_string(), vec![0xAA]),
1082                ("SEEKTABLE".to_string(), vec![0xBB]),
1083            ]
1084        );
1085        assert_eq!(binary.len(), 2);
1086        assert_eq!(binary[0].key, "APPLICATION");
1087        assert_eq!(binary[0].payload, b"testDATA");
1088        assert_eq!(binary[1].key, "CUESHEET");
1089        assert_eq!(binary[1].payload, vec![0xCC; 4]);
1090
1091        assert_eq!(structural_block_type("STREAMINFO"), Some(0));
1092        assert_eq!(structural_block_type("SEEKTABLE"), Some(3));
1093        assert_eq!(structural_block_type("APPLICATION"), None);
1094        assert_eq!(structural_block_type("bogus"), None);
1095    }
1096
1097    #[test]
1098    fn synthesize_layout_picture_block_size_boundary_is_inclusive() {
1099        // body_len = picture_body_framing(art).len() + art.data_len. The guard at
1100        // flac.rs rejects body_len > 0x00FF_FFFF (FLAC's 24-bit block length).
1101        let mk = |data_len: u64| ArtInput {
1102            art_id: 1,
1103            mime: "image/png".to_string(),
1104            description: String::new(),
1105            picture_type: PictureType::new(3).unwrap(),
1106            width: 0,
1107            height: 0,
1108            data_len: BlobLen::new(data_len).unwrap(),
1109        };
1110        // Derive the exact framing length from production rather than hardcoding it
1111        // (it is independent of the data_len *value* — that field is always 4 bytes).
1112        // This keeps the boundary correct regardless of the framing's field count.
1113        let art = mk(1);
1114        let framing_len = picture_body_framing(&art, &art.description).unwrap().len() as u64;
1115        let at_limit = 0x00FF_FFFF - framing_len; // body_len == 0x00FF_FFFF exactly
1116        // original `>` accepts the inclusive boundary; the `>=` mutant rejects it.
1117        // (data_len is only a count; no large allocation occurs.)
1118        assert!(synthesize_layout(&[valid_streaminfo()], 0, 0, &[], &[], &[mk(at_limit)]).is_ok());
1119        // one byte over must still error, pinning the high side of the boundary.
1120        assert_eq!(
1121            synthesize_layout(&[valid_streaminfo()], 0, 0, &[], &[], &[mk(at_limit + 1)]),
1122            Err(FormatError::TooLarge)
1123        );
1124    }
1125
1126    #[test]
1127    fn synthesize_layout_vorbis_comment_block_size_boundary_is_inclusive() {
1128        // The regenerated VORBIS_COMMENT body must also fit FLAC's 24-bit block
1129        // length. Derive the non-value overhead from production, then size the
1130        // value so the body lands exactly on the limit; one more byte errors.
1131        // Mirrors the PICTURE/binary-tag boundary tests: the `>` accepts the
1132        // inclusive limit while the `>=` mutant rejects it.
1133        let overhead = crate::vorbiscomment::build(&[TagInput::new("title", "")])
1134            .unwrap()
1135            .len() as u64;
1136        let at_limit = "x".repeat(crate::convert::usize_from(MAX_BLOCK_BODY - overhead));
1137        let tags = [TagInput::new("title", at_limit.as_str())];
1138        assert!(synthesize_layout(&[valid_streaminfo()], 0, 0, &tags, &[], &[]).is_ok());
1139        // one byte over must still error, pinning the high side of the boundary.
1140        let over = format!("{at_limit}x");
1141        let tags = [TagInput::new("title", over.as_str())];
1142        assert_eq!(
1143            synthesize_layout(&[valid_streaminfo()], 0, 0, &tags, &[], &[]),
1144            Err(FormatError::TooLarge)
1145        );
1146    }
1147
1148    #[test]
1149    fn synthesize_layout_checked_picture_len_rejects_overflow() {
1150        // A hostile art data_len near u64::MAX must fail closed with TooLarge at
1151        // the checked add, not panic (debug) / wrap (release) past the
1152        // MAX_BLOCK_BODY guard.
1153        let mk = |data_len: u64| ArtInput {
1154            art_id: 1,
1155            mime: "image/png".to_string(),
1156            description: String::new(),
1157            picture_type: PictureType::new(3).unwrap(),
1158            width: 0,
1159            height: 0,
1160            data_len: BlobLen::new(data_len).unwrap(),
1161        };
1162        assert_eq!(
1163            synthesize_layout(&[valid_streaminfo()], 0, 0, &[], &[], &[mk(u64::MAX)]),
1164            Err(FormatError::TooLarge)
1165        );
1166    }
1167
1168    #[test]
1169    fn synthesize_layout_binary_tag_block_size_boundary_is_inclusive() {
1170        // The binary-tag guard rejects bt.len > 0x00FF_FFFF (FLAC's 24-bit block
1171        // length). `len` is only a count — no payload is allocated — so the exact
1172        // boundary is cheap to pin. Mirrors the PICTURE boundary test; the `>`
1173        // accepts the inclusive limit while the `>=` mutant rejects it.
1174        let mk = |len: u64| BinaryTagInput {
1175            key: "APPLICATION".to_string(),
1176            payload_id: 1,
1177            len: BlobLen::new(len).unwrap(),
1178        };
1179        // len == 0x00FF_FFFF exactly must succeed.
1180        assert!(
1181            synthesize_layout(&[valid_streaminfo()], 0, 0, &[], &[mk(0x00FF_FFFF)], &[]).is_ok()
1182        );
1183        // one byte over must still error, pinning the high side of the boundary.
1184        assert_eq!(
1185            synthesize_layout(&[valid_streaminfo()], 0, 0, &[], &[mk(0x0100_0000)], &[]),
1186            Err(FormatError::TooLarge)
1187        );
1188    }
1189}