Skip to main content

musefs_format/
flac.rs

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