Skip to main content

musefs_format/
mp4.rs

1//! Hand-rolled MP4/M4A box layer: parse the structure, read iTunes metadata, and
2//! regenerate `moov` (with patched chunk offsets) to synthesize a re-tagged file
3//! whose `mdat` audio payload is served verbatim. Strict: anything outside the
4//! supported shape (single audio track, one `mdat`, non-fragmented) is rejected.
5
6use crate::convert::usize_from;
7use crate::error::{FormatError, Result};
8use crate::input::{
9    ArtInput, BinaryTagInput, EmbeddedBinaryTag, EmbeddedPicture, PictureType, TagInput,
10};
11use crate::layout::{RegionLayout, Segment};
12use crate::size;
13use std::io::{self, Read, Seek, SeekFrom};
14
15const MAX_MP4_METADATA_BYTES: u64 = 256 * 1024 * 1024;
16
17fn be_u32(b: &[u8], pos: usize) -> Result<u32> {
18    let s = b.get(pos..pos + 4).ok_or(FormatError::Malformed)?;
19    Ok(u32::from_be_bytes(s.try_into().unwrap()))
20}
21
22fn be_u64(b: &[u8], pos: usize) -> Result<u64> {
23    let s = b.get(pos..pos + 8).ok_or(FormatError::Malformed)?;
24    Ok(u64::from_be_bytes(s.try_into().unwrap()))
25}
26
27/// A located box header within some buffer. `start` is relative to that buffer.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29struct BoxRef {
30    kind: [u8; 4],
31    start: usize,
32    header_len: usize, // 8, or 16 for 64-bit largesize
33    total_len: usize,  // header + payload
34}
35
36impl BoxRef {
37    fn payload_start(&self) -> usize {
38        self.start + self.header_len
39    }
40    fn end(&self) -> usize {
41        self.start + self.total_len
42    }
43    /// `buf` must be the same buffer `read_box` parsed this header from — offsets
44    /// are relative to it. The debug assertion catches a wrong-buffer call in tests.
45    fn payload<'a>(&self, buf: &'a [u8]) -> &'a [u8] {
46        debug_assert!(
47            self.end() <= buf.len(),
48            "BoxRef::payload called with a buffer it was not parsed from"
49        );
50        &buf[self.payload_start()..self.end()]
51    }
52}
53
54/// A parsed box header (the payload need not be in memory). Public so the core
55/// reader can reason about box bounds while seeking.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub struct BoxHeader {
58    /// The 4-byte box type, e.g. `*b"moov"`.
59    pub kind: [u8; 4],
60    /// 8, or 16 for a 64-bit largesize.
61    pub header_len: u64,
62    /// Total box length: header + payload.
63    pub total_len: u64,
64}
65
66/// Parse a box header from `hdr` (>= 8 bytes; >= 16 if it uses a 64-bit
67/// largesize). `remaining` is the byte count from this box's start to EOF, used
68/// to resolve a `size == 0` ("extends to end") box.
69pub fn box_header(hdr: &[u8], remaining: u64) -> Result<BoxHeader> {
70    let size32 = u64::from(be_u32(hdr, 0)?);
71    let kind: [u8; 4] = hdr
72        .get(4..8)
73        .ok_or(FormatError::Malformed)?
74        .try_into()
75        .unwrap();
76    let (header_len, total_len) = match size32 {
77        1 => (16u64, be_u64(hdr, 8)?),
78        0 => (8u64, remaining),
79        n => (8u64, n),
80    };
81    if total_len < header_len || total_len > remaining {
82        return Err(FormatError::Malformed);
83    }
84    Ok(BoxHeader {
85        kind,
86        header_len,
87        total_len,
88    })
89}
90
91/// Error from the seeking MP4 reader: an IO failure reading the file, or a
92/// structural/format problem. Kept distinct so the core layer can map IO to
93/// `CoreError::Io` (preserving errno) and format to `CoreError::Format`.
94#[derive(Debug, thiserror::Error)]
95pub enum Mp4ScanError {
96    #[error(transparent)]
97    Io(#[from] io::Error),
98    #[error(transparent)]
99    Format(#[from] FormatError),
100    #[error("MP4 {box_kind} box is {size} bytes, exceeds the {cap}-byte metadata cap")]
101    MetadataTooLarge {
102        box_kind: &'static str,
103        size: u64,
104        cap: u64,
105    },
106}
107
108fn read_box(buf: &[u8], pos: usize) -> Result<BoxRef> {
109    let size32 = u64::from(be_u32(buf, pos)?);
110    let kind: [u8; 4] = buf
111        .get(pos + 4..pos + 8)
112        .ok_or(FormatError::Malformed)?
113        .try_into()
114        .unwrap();
115    let (header_len, total) = match size32 {
116        1 => (16usize, be_u64(buf, pos + 8)?),
117        0 => (8usize, (buf.len() - pos) as u64),
118        n => (8usize, n),
119    };
120    let total = usize_from(total);
121    let Some(end) = pos.checked_add(total) else {
122        return Err(FormatError::Malformed);
123    };
124    if total < header_len || end > buf.len() {
125        return Err(FormatError::Malformed);
126    }
127    Ok(BoxRef {
128        kind,
129        start: pos,
130        header_len,
131        total_len: total,
132    })
133}
134
135fn child_boxes(buf: &[u8]) -> Result<Vec<BoxRef>> {
136    let mut out = Vec::new();
137    let mut pos = 0;
138    while pos + 8 <= buf.len() {
139        let b = read_box(buf, pos)?;
140        pos = b.end();
141        out.push(b);
142    }
143    Ok(out)
144}
145
146fn find_box(buf: &[u8], kind: &[u8; 4]) -> Result<Option<BoxRef>> {
147    Ok(child_boxes(buf)?.into_iter().find(|b| &b.kind == kind))
148}
149
150/// Descend a path of box types; return `(payload_start, payload_len)` relative to
151/// `buf` for the box at the end of the path, or None if any step is missing.
152fn find_path(buf: &[u8], path: &[&[u8; 4]]) -> Result<Option<(usize, usize)>> {
153    let mut base = 0usize;
154    let mut last = None;
155    for kind in path {
156        let region = &buf[base..];
157        let Some(b) = find_box(region, kind)? else {
158            return Ok(None);
159        };
160        let ps = base + b.payload_start();
161        last = Some((ps, b.total_len - b.header_len));
162        base = ps;
163    }
164    Ok(last)
165}
166
167/// Audio payload bounds within the backing file (the verbatim `mdat` payload).
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub struct Mp4Bounds {
170    pub audio_offset: u64,
171    pub audio_length: u64,
172}
173
174/// Validate the internal `moov` shape: no fragmentation (`mvex`), exactly one
175/// track, and that track is audio (`soun`). `moov_payload` is the bytes inside
176/// the `moov` box (after its header).
177fn validate_moov(moov_payload: &[u8]) -> Result<()> {
178    if find_box(moov_payload, b"mvex")?.is_some() {
179        return Err(FormatError::NotMp4);
180    }
181    let traks: Vec<_> = child_boxes(moov_payload)?
182        .into_iter()
183        .filter(|b| &b.kind == b"trak")
184        .collect();
185    if traks.len() != 1 {
186        return Err(FormatError::NotMp4);
187    }
188    let trak = traks[0].payload(moov_payload);
189    let (hp, hl) = find_path(trak, &[b"mdia", b"hdlr"])?.ok_or(FormatError::NotMp4)?;
190    if trak[hp..hp + hl].get(8..12) != Some(b"soun") {
191        return Err(FormatError::NotMp4);
192    }
193    Ok(())
194}
195
196/// Validate the supported shape; return the ftyp/moov/mdat boxes (absolute offsets
197/// in `buf`). Rejects fragmented, video, multi-track, and multi-`mdat` files.
198fn locate(buf: &[u8]) -> Result<(BoxRef, BoxRef, BoxRef)> {
199    let top = child_boxes(buf).map_err(|_| FormatError::NotMp4)?;
200    if top.iter().any(|b| &b.kind == b"moof") {
201        return Err(FormatError::NotMp4);
202    }
203    let one = |kind: &[u8; 4]| -> Result<BoxRef> {
204        let mut it = top.iter().filter(|b| &b.kind == kind);
205        let first = it.next().copied().ok_or(FormatError::NotMp4)?;
206        if it.next().is_some() {
207            return Err(FormatError::NotMp4);
208        }
209        Ok(first)
210    };
211    let ftyp = one(b"ftyp")?;
212    let moov = one(b"moov")?;
213    let mdat = one(b"mdat")?;
214
215    validate_moov(moov.payload(buf))?;
216    Ok((ftyp, moov, mdat))
217}
218
219/// Parse the file and return the `mdat` payload bounds, or an error to skip it.
220pub fn locate_audio(buf: &[u8]) -> Result<Mp4Bounds> {
221    let (_ftyp, _moov, mdat) = locate(buf)?;
222    Ok(Mp4Bounds {
223        audio_offset: mdat.payload_start() as u64,
224        audio_length: (mdat.total_len - mdat.header_len) as u64,
225    })
226}
227
228/// Everything `synthesize_layout` needs, read from the backing file once.
229#[derive(Debug, Clone, PartialEq)]
230pub struct Mp4Scan {
231    pub ftyp: Vec<u8>,
232    pub moov: Vec<u8>,
233    pub mdat_header: Vec<u8>,
234    pub mdat_payload_offset: u64,
235    pub mdat_payload_len: u64,
236}
237
238pub fn read_structure(buf: &[u8]) -> Result<Mp4Scan> {
239    let (ftyp, moov, mdat) = locate(buf)?;
240    Ok(Mp4Scan {
241        ftyp: buf[ftyp.start..ftyp.end()].to_vec(),
242        moov: buf[moov.start..moov.end()].to_vec(),
243        mdat_header: buf[mdat.start..mdat.payload_start()].to_vec(),
244        mdat_payload_offset: mdat.payload_start() as u64,
245        mdat_payload_len: (mdat.total_len - mdat.header_len) as u64,
246    })
247}
248
249/// Read the structural boxes (`ftyp`, `moov`, and the `mdat` header) by seeking,
250/// **never** reading the `mdat` payload — for audiobooks that payload is hundreds
251/// of MB and is served from the backing file at read time. Produces an `Mp4Scan`
252/// byte-identical to `read_structure` on the same file, so synthesis is unchanged.
253///
254/// The header walk reads only 8 bytes per top-level box (16 for a 64-bit
255/// largesize), so it skips over the `mdat` payload to reach a trailing `moov`.
256pub fn read_structure_from<R: Read + Seek>(
257    r: &mut R,
258    file_len: u64,
259) -> std::result::Result<Mp4Scan, Mp4ScanError> {
260    fn region<R: Read + Seek>(r: &mut R, off: u64, len: usize) -> io::Result<Vec<u8>> {
261        r.seek(SeekFrom::Start(off))?;
262        let mut buf = vec![0u8; len];
263        r.read_exact(&mut buf)?;
264        Ok(buf)
265    }
266
267    // (start_offset, header) for each box we care about.
268    let mut ftyp: Option<(u64, BoxHeader)> = None;
269    let mut moov: Option<(u64, BoxHeader)> = None;
270    let mut mdat: Option<(u64, BoxHeader)> = None;
271    let mut dup = false;
272
273    let mut pos = 0u64;
274    while pos + 8 <= file_len {
275        // Read exactly the header — 8 bytes, plus 8 more only for a largesize box.
276        // This guarantees we never touch a box's payload (notably mdat's).
277        let first8 = region(r, pos, 8)?;
278        let size32 = u32::from_be_bytes(first8[0..4].try_into().unwrap());
279        // A largesize box needs 8 more header bytes; if the file is truncated
280        // mid-header this read surfaces as Mp4ScanError::Io (UnexpectedEof).
281        let hdr = if size32 == 1 {
282            let mut h = first8;
283            h.extend_from_slice(&region(r, pos + 8, 8)?);
284            h
285        } else {
286            first8
287        };
288        let bh = box_header(&hdr, file_len - pos)?;
289        let total = bh.total_len;
290        match &bh.kind {
291            b"moof" => return Err(FormatError::NotMp4.into()),
292            b"ftyp" => dup |= ftyp.replace((pos, bh)).is_some(),
293            b"moov" => dup |= moov.replace((pos, bh)).is_some(),
294            b"mdat" => dup |= mdat.replace((pos, bh)).is_some(),
295            _ => {}
296        }
297        pos += total;
298    }
299    if dup {
300        return Err(FormatError::NotMp4.into());
301    }
302
303    let (ftyp_s, ftyp_h) = ftyp.ok_or(FormatError::NotMp4)?;
304    let (moov_s, moov_h) = moov.ok_or(FormatError::NotMp4)?;
305    let (mdat_s, mdat_h) = mdat.ok_or(FormatError::NotMp4)?;
306
307    for (box_kind, total_len) in [("ftyp", ftyp_h.total_len), ("moov", moov_h.total_len)] {
308        if total_len > MAX_MP4_METADATA_BYTES {
309            return Err(Mp4ScanError::MetadataTooLarge {
310                box_kind,
311                size: total_len,
312                cap: MAX_MP4_METADATA_BYTES,
313            });
314        }
315    }
316
317    // `try_from` rather than `as usize`: on a 32-bit target an oversized box would
318    // truncate silently; a box larger than `usize` is malformed for our purposes.
319    let ftyp_len = usize::try_from(ftyp_h.total_len).map_err(|_| FormatError::Malformed)?;
320    let moov_len = usize::try_from(moov_h.total_len).map_err(|_| FormatError::Malformed)?;
321    let ftyp_bytes = region(r, ftyp_s, ftyp_len)?;
322    let moov_bytes = region(r, moov_s, moov_len)?;
323    let mdat_header = region(r, mdat_s, usize_from(mdat_h.header_len))?;
324
325    validate_moov(&moov_bytes[usize_from(moov_h.header_len)..])?;
326
327    Ok(Mp4Scan {
328        ftyp: ftyp_bytes,
329        moov: moov_bytes,
330        mdat_header,
331        mdat_payload_offset: mdat_s + mdat_h.header_len,
332        mdat_payload_len: mdat_h.total_len - mdat_h.header_len,
333    })
334}
335
336/// Locate `moov/udta/meta/ilst`; `meta` is a FullBox (4 version/flags bytes before
337/// its children). Returns the ilst payload range absolute within `buf`.
338fn ilst_region(buf: &[u8]) -> Option<(usize, usize)> {
339    let moov = find_box(buf, b"moov").ok()??;
340    let mp = moov.payload(buf);
341    let base = moov.payload_start();
342    let (up, ul) = find_path(mp, &[b"udta"]).ok()??;
343    let udta = &mp[up..up + ul];
344    let meta = find_box(udta, b"meta").ok()??;
345    let meta_children = udta.get(meta.payload_start() + 4..meta.end())?;
346    let il = find_box(meta_children, b"ilst").ok()??;
347    let start = base + up + meta.payload_start() + 4 + il.payload_start();
348    Some((start, il.total_len - il.header_len))
349}
350
351/// Parse a `----` freeform atom payload into `(key, value)`. Folds (mean, name)
352/// to a canonical key via the vocabulary, else keys on the verbatim `name`. Only
353/// the first `data` atom is read (multi-value freeform is rare). None if malformed.
354fn read_freeform(inner: &[u8]) -> Option<(String, String)> {
355    let name_box = find_box(inner, b"name").ok()??;
356    let data_box = find_box(inner, b"data").ok()??;
357    let np = name_box.payload(inner);
358    let dp = data_box.payload(inner);
359    if np.len() < 4 || dp.len() < 8 {
360        return None;
361    }
362    // The `data` box is `[type: u32][locale: u32][value]`; type 1 == UTF-8 text.
363    // Binary-typed freeform values are not text tags, so skip them.
364    let type_code = u32::from_be_bytes([dp[0], dp[1], dp[2], dp[3]]);
365    if type_code != 1 {
366        return None;
367    }
368    // name/mean payloads start with a 4-byte FullBox [version 1][flags 3] prefix.
369    let name = std::str::from_utf8(&np[4..]).ok()?;
370    let value = std::str::from_utf8(&dp[8..]).ok()?;
371    let mean = find_box(inner, b"mean")
372        .ok()
373        .flatten()
374        .map_or("com.apple.iTunes", |m| {
375            let p = m.payload(inner);
376            if p.len() >= 4 {
377                std::str::from_utf8(&p[4..]).unwrap_or("com.apple.iTunes")
378            } else {
379                "com.apple.iTunes"
380            }
381        });
382    let key = crate::tagmap::mp4_freeform_to_key(mean, name)
383        .map_or_else(|| name.to_string(), str::to_string);
384    Some((key, value.to_string()))
385}
386
387/// Lenient: returns empty / skips any malformed atom and never errors — this only
388/// seeds metadata from existing files, so a missing or garbled tag must simply be
389/// absent. Text atoms map via the vocabulary; `trkn`/`disk` yield track/disc
390/// numbers; `----` freeform atoms key on their name (folded when known). Other
391/// atoms are skipped.
392pub fn read_tags(buf: &[u8]) -> Vec<(String, String)> {
393    let Some((start, len)) = ilst_region(buf) else {
394        return Vec::new();
395    };
396    let ilst = &buf[start..start + len];
397    let mut out = Vec::new();
398    for atom in child_boxes(ilst).unwrap_or_default() {
399        let inner = atom.payload(ilst);
400        if &atom.kind == b"----" {
401            if let Some(pair) = read_freeform(inner) {
402                out.push(pair);
403            }
404            continue;
405        }
406        let Ok(Some(data)) = find_box(inner, b"data") else {
407            continue;
408        };
409        let dp = data.payload(inner);
410        if dp.len() < 8 {
411            continue;
412        }
413        let value = &dp[8..]; // skip [type 4][locale 4]
414        if let Some(key) = crate::tagmap::mp4_atom_to_key(&atom.kind) {
415            if let Ok(s) = std::str::from_utf8(value) {
416                out.push((key.to_string(), s.to_string()));
417            }
418        } else if &atom.kind == b"trkn" && value.len() >= 4 {
419            out.push((
420                "tracknumber".into(),
421                u16::from_be_bytes([value[2], value[3]]).to_string(),
422            ));
423        } else if &atom.kind == b"disk" && value.len() >= 4 {
424            out.push((
425                "discnumber".into(),
426                u16::from_be_bytes([value[2], value[3]]).to_string(),
427            ));
428        }
429    }
430    out
431}
432
433/// Lenient: returns empty / skips any malformed atom and never errors — this only
434/// seeds cover art from existing files, so a missing or garbled picture must simply be absent.
435/// Every `data` child of every `covr` atom yields one picture (the iTunes
436/// multiple-artwork convention); non-`data` children are skipped.
437///
438/// `max_art_bytes` caps each image body: a `data` payload whose image bytes
439/// (after the 8-byte `[type][locale]` header) exceed it is skipped before any
440/// copy, so an oversized `covr` in a large `moov` is never materialized.
441pub fn read_pictures(buf: &[u8], max_art_bytes: usize) -> Vec<EmbeddedPicture> {
442    let Some((start, len)) = ilst_region(buf) else {
443        return Vec::new();
444    };
445    let ilst = &buf[start..start + len];
446    let mut out = Vec::new();
447    for atom in child_boxes(ilst).unwrap_or_default() {
448        if &atom.kind != b"covr" {
449            continue;
450        }
451        let inner = atom.payload(ilst);
452        for data in child_boxes(inner).unwrap_or_default() {
453            if &data.kind != b"data" {
454                continue;
455            }
456            let dp = data.payload(inner);
457            if dp.len() < 8 {
458                continue;
459            }
460            if dp.len() - 8 > max_art_bytes {
461                continue;
462            }
463            let mime = match u32::from_be_bytes([dp[0], dp[1], dp[2], dp[3]]) {
464                13 => "image/jpeg",
465                14 => "image/png",
466                _ => continue,
467            };
468            out.push(EmbeddedPicture {
469                mime: mime.to_string(),
470                picture_type: PictureType::new(3).expect("3 is in range"),
471                description: String::new(),
472                width: 0,
473                height: 0,
474                data: dp[8..].to_vec(),
475            });
476        }
477    }
478    out
479}
480
481/// Extract opaque (non-text) MP4 `----` freeform atoms for binary-tag passthrough.
482/// One `EmbeddedBinaryTag` per `----` atom whose first `data` sub-box is
483/// binary-typed (type code != 1): key `----:<mean>:<name>`, payload the `data`
484/// value bytes (after the 8-byte `[type][locale]` header). Text freeform atoms
485/// (type 1) are handled by `read_tags`, so the two paths never double-store.
486/// Lenient: malformed atoms are skipped. Only the first `data` sub-box is read
487/// (multi-value freeform is rare; mirrors `read_freeform`).
488///
489/// `max_binary_tag_bytes` caps each value: a `data` payload whose value bytes
490/// (after the 8-byte `[type][locale]` header) exceed it is skipped before any
491/// copy, so an oversized `----` in a large `moov` is never materialized.
492pub fn read_binary_tags(buf: &[u8], max_binary_tag_bytes: usize) -> Vec<EmbeddedBinaryTag> {
493    let Some((start, len)) = ilst_region(buf) else {
494        return Vec::new();
495    };
496    let ilst = &buf[start..start + len];
497    let mut out = Vec::new();
498    for atom in child_boxes(ilst).unwrap_or_default() {
499        if &atom.kind != b"----" {
500            continue;
501        }
502        let inner = atom.payload(ilst);
503        let Ok(Some(data)) = find_box(inner, b"data") else {
504            continue;
505        };
506        let dp = data.payload(inner);
507        if dp.len() < 8 {
508            continue;
509        }
510        if dp.len() - 8 > max_binary_tag_bytes {
511            continue;
512        }
513        // `data` body is `[type: u32][locale: u32][value]`; type 1 == UTF-8 text,
514        // which is the text path's job. Everything else is opaque binary.
515        let type_code = u32::from_be_bytes([dp[0], dp[1], dp[2], dp[3]]);
516        if type_code == 1 {
517            continue;
518        }
519        // name/mean payloads carry a 4-byte FullBox prefix; default mean to iTunes.
520        let Some(name) = find_box(inner, b"name").ok().flatten().and_then(|n| {
521            let p = n.payload(inner);
522            (p.len() >= 4)
523                .then(|| std::str::from_utf8(&p[4..]).ok())
524                .flatten()
525        }) else {
526            continue;
527        };
528        let mean = find_box(inner, b"mean")
529            .ok()
530            .flatten()
531            .map_or("com.apple.iTunes", |m| {
532                let p = m.payload(inner);
533                if p.len() >= 4 {
534                    std::str::from_utf8(&p[4..]).unwrap_or("com.apple.iTunes")
535                } else {
536                    "com.apple.iTunes"
537                }
538            });
539        out.push(EmbeddedBinaryTag {
540            key: format!("----:{mean}:{name}"),
541            payload: dp[8..].to_vec(),
542        });
543    }
544    out
545}
546
547fn boxed(kind: &[u8; 4], payload: &[u8]) -> Result<Vec<u8>> {
548    let size = u32::try_from(8 + payload.len()).map_err(|_| FormatError::TooLarge)?;
549    let mut v = size.to_be_bytes().to_vec();
550    v.extend_from_slice(kind);
551    v.extend_from_slice(payload);
552    Ok(v)
553}
554
555fn text_atom(kind: &[u8; 4], values: &[&str]) -> Result<Vec<u8>> {
556    let mut inner = Vec::new();
557    for v in values {
558        let mut data = 1u32.to_be_bytes().to_vec(); // type 1 = UTF-8
559        data.extend_from_slice(&0u32.to_be_bytes()); // locale
560        data.extend_from_slice(v.as_bytes());
561        inner.extend(boxed(b"data", &data)?);
562    }
563    boxed(kind, &inner)
564}
565
566fn number_atom(kind: &[u8; 4], n: u16, width: usize) -> Result<Vec<u8>> {
567    debug_assert!(
568        width >= 4,
569        "number_atom width must hold the 4-byte reserved+value prefix"
570    );
571    let mut data = 0u32.to_be_bytes().to_vec(); // type 0 = binary
572    data.extend_from_slice(&0u32.to_be_bytes()); // locale
573    let mut body = vec![0u8, 0];
574    body.extend_from_slice(&n.to_be_bytes());
575    body.resize(width, 0);
576    data.extend_from_slice(&body);
577    boxed(kind, &boxed(b"data", &data)?)
578}
579
580/// Emit a `----` freeform atom: a `mean` and `name` sub-box (each with a 4-byte
581/// FullBox prefix) followed by one UTF-8 `data` sub-box per value. Note that the
582/// scan path (`read_freeform`) only recovers the first value on read-back, so
583/// multi-value freeform tags round-trip only their first value.
584fn freeform_atom(mean: &str, name: &str, values: &[&str]) -> Result<Vec<u8>> {
585    let mut inner = Vec::new();
586    let mut mean_body = 0u32.to_be_bytes().to_vec(); // version/flags
587    mean_body.extend_from_slice(mean.as_bytes());
588    inner.extend(boxed(b"mean", &mean_body)?);
589    let mut name_body = 0u32.to_be_bytes().to_vec();
590    name_body.extend_from_slice(name.as_bytes());
591    inner.extend(boxed(b"name", &name_body)?);
592    for v in values {
593        let mut data = 1u32.to_be_bytes().to_vec(); // type 1 = UTF-8
594        data.extend_from_slice(&0u32.to_be_bytes()); // locale
595        data.extend_from_slice(v.as_bytes());
596        inner.extend(boxed(b"data", &data)?);
597    }
598    boxed(b"----", &inner)
599}
600
601/// Parse a `----:<mean>:<name>` opaque key back into `(mean, name)`. `name` may
602/// contain `:` (only the first separator splits). `None` if not a freeform key.
603fn parse_freeform_key(key: &str) -> Option<(&str, &str)> {
604    key.strip_prefix("----:")?.split_once(':')
605}
606
607/// Inline framing for an opaque binary `----` atom whose `data` value
608/// (`payload_len` bytes) streams next:
609/// `[---- size][----][mean box][name box][data size][data][type 0][locale 0]`.
610/// Mirrors `freeform_atom` but emits type code 0 (binary) and no value bytes — the
611/// value is served from the DB as a `Segment::BinaryTag`.
612fn freeform_binary_prefix(mean: &str, name: &str, payload_len: u64) -> Result<Vec<u8>> {
613    let mut mean_body = 0u32.to_be_bytes().to_vec(); // version/flags
614    mean_body.extend_from_slice(mean.as_bytes());
615    let mean_box = boxed(b"mean", &mean_body)?;
616    let mut name_body = 0u32.to_be_bytes().to_vec();
617    name_body.extend_from_slice(name.as_bytes());
618    let name_box = boxed(b"name", &name_body)?;
619
620    let data_size = size::checked_add(16, payload_len)?; // data header + type + locale + payload
621    let inner_len = size::checked_sum([mean_box.len() as u64, name_box.len() as u64, data_size])?;
622
623    let outer_len = size::checked_add(8, inner_len)?;
624    let mut out = u32::try_from(outer_len)
625        .map_err(|_| FormatError::TooLarge)?
626        .to_be_bytes()
627        .to_vec();
628    out.extend_from_slice(b"----");
629    out.extend_from_slice(&mean_box);
630    out.extend_from_slice(&name_box);
631    out.extend_from_slice(
632        &u32::try_from(data_size)
633            .map_err(|_| FormatError::TooLarge)?
634            .to_be_bytes(),
635    );
636    out.extend_from_slice(b"data");
637    out.extend_from_slice(&0u32.to_be_bytes()); // type 0 = binary/implicit
638    out.extend_from_slice(&0u32.to_be_bytes()); // locale
639    Ok(out)
640}
641
642/// Build the `udta` box as an ordered segment list: `Segment::Inline` for all box
643/// framing, with each opaque `----` value and each cover image streamed from the DB
644/// (`Segment::BinaryTag`/`Segment::ArtImage`) rather than materialized. Returns
645/// `(segments, streamed_total)` where `streamed_total` sums every streamed payload
646/// length (binary `----` values + art). Every enclosing box size
647/// (`----`/`ilst`/`meta`/`udta`) accounts for `streamed_total` at the right nesting
648/// depth, so the streamed bytes splice in correctly at read time.
649fn build_udta(
650    tags: &[TagInput],
651    binary_tags: &[BinaryTagInput],
652    arts: &[ArtInput],
653) -> Result<(Vec<Segment>, u64)> {
654    // Group consecutive same-key text values (the DB returns tags ordered by key).
655    let mut groups: Vec<(&str, Vec<&str>)> = Vec::new();
656    for t in tags {
657        match groups.last_mut() {
658            Some(g) if g.0 == t.key => g.1.push(&t.value),
659            _ => groups.push((&t.key, vec![&t.value])),
660        }
661    }
662
663    // ilst content: text atoms first (materialized), then opaque `----` (streamed),
664    // then cover art (streamed). `ilst_inline` accumulates framing until a streamed
665    // segment forces a flush.
666    let mut ilst_inline: Vec<u8> = Vec::new();
667    for (key, values) in &groups {
668        match crate::tagmap::key_to_mp4(key) {
669            Some(crate::tagmap::Mp4Slot::Text(atom)) => {
670                ilst_inline.extend(text_atom(atom, values)?);
671            }
672            Some(crate::tagmap::Mp4Slot::Number(atom, width)) => {
673                if let Ok(n) = values.first().copied().unwrap_or("").parse::<u16>() {
674                    ilst_inline.extend(number_atom(atom, n, width)?);
675                }
676            }
677            Some(crate::tagmap::Mp4Slot::Freeform(mean, name)) => {
678                ilst_inline.extend(freeform_atom(mean, name, values)?);
679            }
680            None => ilst_inline.extend(freeform_atom("com.apple.iTunes", key, values)?),
681        }
682    }
683
684    let mut ilst_segments: Vec<Segment> = Vec::new();
685    let mut streamed_total: u64 = 0;
686
687    for bt in binary_tags {
688        let Some((mean, name)) = parse_freeform_key(&bt.key) else {
689            // Not a `----:<mean>:<name>` key; skip defensively (no double-store path).
690            continue;
691        };
692        ilst_inline.extend_from_slice(&freeform_binary_prefix(mean, name, bt.len.get())?);
693        ilst_segments.push(Segment::Inline(std::mem::take(&mut ilst_inline)));
694        ilst_segments.push(Segment::BinaryTag {
695            payload_id: bt.payload_id,
696            len: bt.len,
697        });
698        streamed_total = size::checked_add(streamed_total, bt.len.get())?;
699    }
700
701    if !arts.is_empty() {
702        // One covr atom; each art is its own `data` child (the iTunes
703        // convention for multiple artworks).
704        let covr_size: u64 = arts.iter().try_fold(8u64, |acc, a| {
705            size::checked_add(acc, size::checked_add(16, a.data_len.get())?)
706        })?;
707        ilst_inline.extend_from_slice(
708            &u32::try_from(covr_size)
709                .map_err(|_| FormatError::TooLarge)?
710                .to_be_bytes(),
711        );
712        ilst_inline.extend_from_slice(b"covr");
713        for a in arts {
714            let type_code: u32 = if a.mime == "image/png" { 14 } else { 13 };
715            let data_size = size::checked_add(16, a.data_len.get())?; // data header + type + locale + image
716            ilst_inline.extend_from_slice(
717                &u32::try_from(data_size)
718                    .map_err(|_| FormatError::TooLarge)?
719                    .to_be_bytes(),
720            );
721            ilst_inline.extend_from_slice(b"data");
722            ilst_inline.extend_from_slice(&type_code.to_be_bytes());
723            ilst_inline.extend_from_slice(&0u32.to_be_bytes()); // locale; image streams next
724            ilst_segments.push(Segment::Inline(std::mem::take(&mut ilst_inline)));
725            ilst_segments.push(Segment::ArtImage {
726                art_id: a.art_id,
727                len: a.data_len,
728            });
729            streamed_total = size::checked_add(streamed_total, a.data_len.get())?;
730        }
731    } else if !ilst_inline.is_empty() {
732        ilst_segments.push(Segment::Inline(std::mem::take(&mut ilst_inline)));
733    }
734
735    let ilst_inline_len: u64 = ilst_segments
736        .iter()
737        .map(|s| match s {
738            Segment::Inline(b) => b.len() as u64,
739            _ => 0,
740        })
741        .sum();
742
743    let mut hdlr_body = vec![0u8; 8];
744    hdlr_body.extend_from_slice(b"mdir");
745    hdlr_body.extend_from_slice(b"appl");
746    hdlr_body.extend_from_slice(&[0u8; 9]);
747    let hdlr = boxed(b"hdlr", &hdlr_body)?;
748
749    // Box sizes. Each enclosing box adds its 8-byte header to the inline content of
750    // its child and carries `streamed_total` through unchanged (the streamed bytes
751    // live at the deepest level, inside ilst).
752    let ilst_size = size::checked_sum([8, ilst_inline_len, streamed_total])?;
753    let meta_inline_len = 4 + hdlr.len() as u64 + 8 + ilst_inline_len; // [vf][hdlr][ilst hdr][ilst inline]
754    let meta_size = size::checked_sum([8, meta_inline_len, streamed_total])?;
755    let udta_inline_len = 8 + meta_inline_len; // [meta hdr][meta inline]
756    let udta_size = size::checked_sum([8, udta_inline_len, streamed_total])?;
757
758    // MP4 box sizes are 32-bit. udta encloses all inner boxes, so converting it
759    // first bounds them all; refuse oversized metadata at the format boundary
760    // rather than emit a silently-truncated (corrupt) size field.
761    let udta_size = u32::try_from(udta_size).map_err(|_| FormatError::TooLarge)?;
762    let meta_size = u32::try_from(meta_size).map_err(|_| FormatError::TooLarge)?;
763    let ilst_size = u32::try_from(ilst_size).map_err(|_| FormatError::TooLarge)?;
764
765    // Leading framing: everything up to the start of ilst content.
766    let mut header = udta_size.to_be_bytes().to_vec();
767    header.extend_from_slice(b"udta");
768    header.extend_from_slice(&meta_size.to_be_bytes());
769    header.extend_from_slice(b"meta");
770    header.extend_from_slice(&0u32.to_be_bytes()); // meta FullBox version/flags
771    header.extend_from_slice(&hdlr);
772    header.extend_from_slice(&ilst_size.to_be_bytes());
773    header.extend_from_slice(b"ilst");
774
775    // Merge the header into the first ilst inline segment (always Inline when present,
776    // since streamed segments are preceded by their framing).
777    let mut segments: Vec<Segment> = Vec::new();
778    let mut lead = header;
779    for seg in ilst_segments {
780        match seg {
781            Segment::Inline(b) => lead.extend_from_slice(&b),
782            other => {
783                segments.push(Segment::Inline(std::mem::take(&mut lead)));
784                segments.push(other);
785            }
786        }
787    }
788    // `lead` is empty only when the loop's last segment was streamed (it was
789    // `take`n); otherwise it still holds the udta/meta/ilst header (when there are
790    // no streamed segments) or trailing framing. Pushing an empty `lead` would
791    // produce an EmptySegment that fails layout validation, so guard on non-empty.
792    if !lead.is_empty() {
793        segments.push(Segment::Inline(lead));
794    }
795    Ok((segments, streamed_total))
796}
797
798/// Patch every `stco` (4-byte) or `co64` (8-byte) chunk offset in `kept` (moov
799/// children minus udta) by `delta`. Errors if a 32-bit offset would overflow.
800fn patch_chunk_offsets(kept: &mut [u8], delta: i64) -> Result<()> {
801    let (range, entry) = match find_path(kept, &[b"trak", b"mdia", b"minf", b"stbl", b"stco"])? {
802        Some(r) => (r, 4usize),
803        None => match find_path(kept, &[b"trak", b"mdia", b"minf", b"stbl", b"co64"])? {
804            Some(r) => (r, 8usize),
805            None => return Err(FormatError::Malformed),
806        },
807    };
808    let (start, len) = range;
809    let count = be_u32(kept, start + 4)? as usize;
810    for i in 0..count {
811        let pos = start + 8 + i * entry;
812        if pos + entry > start + len {
813            return Err(FormatError::Malformed);
814        }
815        if entry == 4 {
816            let v = i64::from(be_u32(kept, pos)?) + delta;
817            let new_val = u32::try_from(v).map_err(|_| FormatError::TooLarge)?;
818            kept[pos..pos + 4].copy_from_slice(&new_val.to_be_bytes());
819        } else {
820            let v = be_u64(kept, pos)?.cast_signed() + delta;
821            if v < 0 {
822                return Err(FormatError::Malformed);
823            }
824            kept[pos..pos + 8].copy_from_slice(&v.cast_unsigned().to_be_bytes());
825        }
826    }
827    Ok(())
828}
829
830/// Regenerate a re-tagged `moov` and produce the serving layout
831/// `[ftyp][regenerated moov][mdat header][mdat payload]`. The mdat payload is
832/// served verbatim, merely relocated, so every chunk offset shifts by a constant
833/// `delta`. Patching only offset VALUES (never box sizes) means `new_moov_size`
834/// is computable before `delta` — no circular dependency. Cover art (every non-empty art row, in input order) and opaque `----`
835/// binary tags stream from the DB at read time, splicing into the layout.
836pub fn synthesize_layout(
837    scan: &Mp4Scan,
838    tags: &[TagInput],
839    binary_tags: &[BinaryTagInput],
840    arts: &[ArtInput],
841) -> Result<RegionLayout> {
842    let moov_payload_start = read_box(&scan.moov, 0)?.payload_start();
843    let moov_payload = &scan.moov[moov_payload_start..];
844    let mut kept = Vec::new();
845    for b in child_boxes(moov_payload)? {
846        if &b.kind != b"udta" {
847            kept.extend_from_slice(&moov_payload[b.start..b.end()]);
848        }
849    }
850
851    // All art inputs are non-zero-length (the bridge drops zero-length at construction).
852    let arts: Vec<ArtInput> = arts.to_vec();
853    let (udta_segments, _streamed_total) = build_udta(tags, binary_tags, &arts)?;
854    let udta_total: u64 = udta_segments.iter().map(Segment::len).sum();
855
856    let new_moov_size = size::checked_sum([8, kept.len() as u64, udta_total])?;
857    // MP4 box sizes are 32-bit; mirror build_udta's bound. The try_from below
858    // (writing the size field) is the enforcing check.
859    let new_moov_size_u32 = u32::try_from(new_moov_size).map_err(|_| FormatError::TooLarge)?;
860    let new_mdat_payload_pos = size::checked_sum([
861        scan.ftyp.len() as u64,
862        new_moov_size,
863        scan.mdat_header.len() as u64,
864    ])?;
865    let delta = new_mdat_payload_pos.cast_signed() - scan.mdat_payload_offset.cast_signed();
866
867    patch_chunk_offsets(&mut kept, delta)?;
868
869    let mut head = Vec::new();
870    head.extend_from_slice(&scan.ftyp);
871    head.extend_from_slice(&new_moov_size_u32.to_be_bytes());
872    head.extend_from_slice(b"moov");
873    head.extend_from_slice(&kept);
874
875    // Splice the udta segment list after the moov head. `build_udta` guarantees a
876    // non-empty list whose first element is the leading `Inline` framing (it opens
877    // with the udta/meta/ilst header), so fold that into `head`; the rest (streamed
878    // payloads + interleaved inline) follow. Finally append the truncated mdat header
879    // to the last inline run before backing audio.
880    let mut udta_iter = udta_segments.into_iter();
881    let Some(Segment::Inline(first)) = udta_iter.next() else {
882        // build_udta always yields a leading Inline; anything else is a producer bug.
883        return Err(FormatError::ProducerBug(
884            "build_udta did not yield a leading Inline framing segment",
885        ));
886    };
887    head.extend_from_slice(&first);
888    let mut segments: Vec<Segment> = vec![Segment::Inline(head)];
889    segments.extend(udta_iter);
890    match segments.last_mut() {
891        Some(Segment::Inline(b)) => b.extend_from_slice(&scan.mdat_header),
892        _ => segments.push(Segment::Inline(scan.mdat_header.clone())),
893    }
894    segments.push(Segment::BackingAudio {
895        offset: scan.mdat_payload_offset,
896        len: scan.mdat_payload_len,
897    });
898    Ok(RegionLayout::validated(segments)?)
899}
900
901#[cfg(test)]
902mod tests {
903    use super::*;
904    use crate::input::{BlobLen, PictureType};
905
906    /// Build a 32-bit-size box: [size][type][payload].
907    fn bx(kind: &[u8; 4], payload: &[u8]) -> Vec<u8> {
908        let mut v = u32::try_from(8 + payload.len())
909            .unwrap()
910            .to_be_bytes()
911            .to_vec();
912        v.extend_from_slice(kind);
913        v.extend_from_slice(payload);
914        v
915    }
916
917    #[test]
918    fn walks_top_level_boxes() {
919        let mut buf = bx(b"ftyp", b"M4A ");
920        buf.extend(bx(b"free", b"\x00\x00"));
921        let boxes = child_boxes(&buf).unwrap();
922        assert_eq!(boxes.len(), 2);
923        assert_eq!(&boxes[0].kind, b"ftyp");
924        assert_eq!(boxes[0].payload(&buf), b"M4A ");
925        assert_eq!(&boxes[1].kind, b"free");
926    }
927
928    #[test]
929    fn find_box_and_nested_path() {
930        let mut hdlr_payload = vec![0u8; 8];
931        hdlr_payload.extend_from_slice(b"soun");
932        hdlr_payload.extend_from_slice(&[0u8; 12]);
933        let moov = bx(
934            b"moov",
935            &bx(b"trak", &bx(b"mdia", &bx(b"hdlr", &hdlr_payload))),
936        );
937
938        let m = find_box(&moov, b"moov").unwrap().unwrap();
939        let (start, len) = find_path(m.payload(&moov), &[b"trak", b"mdia", b"hdlr"])
940            .unwrap()
941            .unwrap();
942        assert_eq!(&m.payload(&moov)[start..start + len][8..12], b"soun");
943    }
944
945    #[test]
946    fn rejects_truncated_box() {
947        let buf = [0u8, 0, 0, 99, b'm', b'o', b'o', b'v']; // claims 99, only 8 present
948        assert!(child_boxes(&buf).is_err());
949    }
950
951    /// Minimal accepted MP4: ftyp, then (per `moov_first`) moov(one soun trak with
952    /// an stco) and mdat. `mdat_payload` is the verbatim audio.
953    fn mk_mp4(moov_first: bool, mdat_payload: &[u8], stco_entries: &[u32]) -> Vec<u8> {
954        let mut stco = vec![0u8; 4];
955        stco.extend_from_slice(&u32::try_from(stco_entries.len()).unwrap().to_be_bytes());
956        for e in stco_entries {
957            stco.extend_from_slice(&e.to_be_bytes());
958        }
959        let mut hdlr_p = vec![0u8; 8];
960        hdlr_p.extend_from_slice(b"soun");
961        hdlr_p.extend_from_slice(&[0u8; 12]);
962        let minf = bx(b"minf", &bx(b"stbl", &bx(b"stco", &stco)));
963        let mdia = bx(b"mdia", &[bx(b"hdlr", &hdlr_p), minf].concat());
964        let trak = bx(b"trak", &mdia);
965        let moov = bx(b"moov", &[bx(b"mvhd", &[0u8; 8]), trak].concat());
966        let mdat = bx(b"mdat", mdat_payload);
967        let ftyp = bx(b"ftyp", b"M4A isom");
968        if moov_first {
969            [ftyp, moov, mdat].concat()
970        } else {
971            [ftyp, mdat, moov].concat()
972        }
973    }
974
975    #[test]
976    fn locates_audio_moov_first_and_last() {
977        for moov_first in [true, false] {
978            let buf = mk_mp4(moov_first, b"AUDIODATA", &[0]);
979            let b = locate_audio(&buf).unwrap();
980            assert_eq!(b.audio_length, 9);
981            assert_eq!(&buf[usize_from(b.audio_offset)..][..9], b"AUDIODATA");
982        }
983    }
984
985    #[test]
986    fn rejects_fragmented_video_and_multi_mdat() {
987        let base = mk_mp4(true, b"X", &[0]);
988        let mut frag = base.clone();
989        frag.extend(bx(b"moof", b"\x00"));
990        assert!(locate_audio(&frag).is_err());
991
992        let mut two = base.clone();
993        two.extend(bx(b"mdat", b"Y"));
994        assert!(locate_audio(&two).is_err());
995
996        let mut hdlr_p = vec![0u8; 8];
997        hdlr_p.extend_from_slice(b"vide");
998        hdlr_p.extend_from_slice(&[0u8; 12]);
999        let video_moov = bx(b"moov", &bx(b"trak", &bx(b"mdia", &bx(b"hdlr", &hdlr_p))));
1000        let vbuf = [bx(b"ftyp", b"M4A "), video_moov, bx(b"mdat", b"Z")].concat();
1001        assert!(locate_audio(&vbuf).is_err());
1002    }
1003
1004    /// A `soun` trak built the way `mk_mp4` does (hdlr + minf/stbl/stco), for
1005    /// reuse when hand-assembling a moov to exercise a specific reject branch.
1006    fn soun_trak() -> Vec<u8> {
1007        let mut stco = vec![0u8; 4];
1008        stco.extend_from_slice(&1u32.to_be_bytes());
1009        stco.extend_from_slice(&0u32.to_be_bytes());
1010        let mut hdlr_p = vec![0u8; 8];
1011        hdlr_p.extend_from_slice(b"soun");
1012        hdlr_p.extend_from_slice(&[0u8; 12]);
1013        let minf = bx(b"minf", &bx(b"stbl", &bx(b"stco", &stco)));
1014        let mdia = bx(b"mdia", &[bx(b"hdlr", &hdlr_p), minf].concat());
1015        bx(b"trak", &mdia)
1016    }
1017
1018    #[test]
1019    fn rejects_mvex_in_moov() {
1020        // A moov carrying an mvex box (movie-extends header => fragmented) is
1021        // rejected even though it otherwise holds a single valid soun trak.
1022        let moov = bx(
1023            b"moov",
1024            &[bx(b"mvhd", &[0u8; 8]), bx(b"mvex", b"\x00"), soun_trak()].concat(),
1025        );
1026        let buf = [bx(b"ftyp", b"M4A isom"), moov, bx(b"mdat", b"X")].concat();
1027        assert!(locate_audio(&buf).is_err());
1028    }
1029
1030    #[test]
1031    fn rejects_multi_trak() {
1032        // Two trak children in moov is rejected (musefs serves single-track audio).
1033        let moov = bx(
1034            b"moov",
1035            &[bx(b"mvhd", &[0u8; 8]), soun_trak(), soun_trak()].concat(),
1036        );
1037        let buf = [bx(b"ftyp", b"M4A isom"), moov, bx(b"mdat", b"X")].concat();
1038        assert!(locate_audio(&buf).is_err());
1039    }
1040
1041    #[test]
1042    fn reads_structure_parts() {
1043        let buf = mk_mp4(false, b"AUDIODATA", &[0]); // moov last
1044        let s = read_structure(&buf).unwrap();
1045        assert_eq!(&s.ftyp[4..8], b"ftyp");
1046        assert_eq!(&s.moov[4..8], b"moov");
1047        assert_eq!(&s.mdat_header[4..8], b"mdat");
1048        assert_eq!(s.mdat_payload_len, 9);
1049        assert_eq!(&buf[usize_from(s.mdat_payload_offset)..][..9], b"AUDIODATA");
1050    }
1051
1052    fn data_atom(type_code: u32, value: &[u8]) -> Vec<u8> {
1053        let mut p = type_code.to_be_bytes().to_vec();
1054        p.extend_from_slice(&0u32.to_be_bytes()); // locale
1055        p.extend_from_slice(value);
1056        bx(b"data", &p)
1057    }
1058
1059    /// Accepted file with udta/meta/ilst injected (meta is a FullBox).
1060    fn mp4_with_ilst(ilst_atoms: &[u8], moov_first: bool) -> Vec<u8> {
1061        let ilst = bx(b"ilst", ilst_atoms);
1062        let mut hdlr = vec![0u8; 8];
1063        hdlr.extend_from_slice(b"mdir");
1064        hdlr.extend_from_slice(b"appl");
1065        hdlr.extend_from_slice(&[0u8; 9]);
1066        let mut meta = vec![0u8; 4]; // FullBox version/flags
1067        meta.extend(bx(b"hdlr", &hdlr));
1068        meta.extend(ilst);
1069        let udta = bx(b"udta", &bx(b"meta", &meta));
1070
1071        let mut hdlr_p = vec![0u8; 8];
1072        hdlr_p.extend_from_slice(b"soun");
1073        hdlr_p.extend_from_slice(&[0u8; 12]);
1074        let mut stco = vec![0u8; 4];
1075        stco.extend_from_slice(&1u32.to_be_bytes());
1076        stco.extend_from_slice(&0u32.to_be_bytes());
1077        let minf = bx(b"minf", &bx(b"stbl", &bx(b"stco", &stco)));
1078        let trak = bx(
1079            b"trak",
1080            &bx(b"mdia", &[bx(b"hdlr", &hdlr_p), minf].concat()),
1081        );
1082        let moov = bx(b"moov", &[bx(b"mvhd", &[0u8; 8]), trak, udta].concat());
1083        let ftyp = bx(b"ftyp", b"M4A ");
1084        let mdat = bx(b"mdat", b"AUDIO");
1085        if moov_first {
1086            [ftyp, moov, mdat].concat()
1087        } else {
1088            [ftyp, mdat, moov].concat()
1089        }
1090    }
1091
1092    #[test]
1093    fn reads_text_and_track_tags() {
1094        let atoms = [
1095            bx(b"\xa9nam", &data_atom(1, b"Song")),
1096            bx(b"aART", &data_atom(1, b"Band")),
1097            bx(b"trkn", &data_atom(0, &[0, 0, 0, 3, 0, 0, 0, 0])),
1098        ]
1099        .concat();
1100        let buf = mp4_with_ilst(&atoms, true);
1101        let tags = read_tags(&buf);
1102        assert!(tags.contains(&("title".into(), "Song".into())));
1103        assert!(tags.contains(&("albumartist".into(), "Band".into())));
1104        assert!(tags.contains(&("tracknumber".into(), "3".into())));
1105    }
1106
1107    #[test]
1108    fn reads_cover_art() {
1109        let jpeg = [0xff, 0xd8, 0xff, 0xe0, 1, 2, 3];
1110        let buf = mp4_with_ilst(&bx(b"covr", &data_atom(13, &jpeg)), false);
1111        let pics = read_pictures(&buf, usize::MAX);
1112        assert_eq!(pics.len(), 1);
1113        assert_eq!(pics[0].mime, "image/jpeg");
1114        assert_eq!(pics[0].data, jpeg);
1115    }
1116
1117    #[test]
1118    fn read_side_never_panics_on_garbage() {
1119        // Empty buffer.
1120        assert!(read_tags(&[]).is_empty());
1121        assert!(read_pictures(&[], usize::MAX).is_empty());
1122
1123        // Random non-MP4 bytes.
1124        let garbage = b"not an mp4 file at all............";
1125        assert!(read_tags(garbage).is_empty());
1126        assert!(read_pictures(garbage, usize::MAX).is_empty());
1127
1128        // Valid moov but no udta/meta/ilst.
1129        let no_ilst = mk_mp4(true, b"AUDIO", &[0]);
1130        assert!(read_tags(&no_ilst).is_empty());
1131        assert!(read_pictures(&no_ilst, usize::MAX).is_empty());
1132
1133        // A meta FullBox whose payload is shorter than the 4 version/flags bytes it
1134        // needs: exercises the `udta.get(meta.payload_start()+4..meta.end())?` guard.
1135        let truncated_meta = bx(b"udta", &bx(b"meta", &[0u8, 0]));
1136        let moov = bx(b"moov", &[bx(b"mvhd", &[0u8; 8]), truncated_meta].concat());
1137        let ftyp = bx(b"ftyp", b"M4A ");
1138        let mdat = bx(b"mdat", b"AUDIO");
1139        let lying = [ftyp, moov, mdat].concat();
1140        assert!(read_tags(&lying).is_empty());
1141        assert!(read_pictures(&lying, usize::MAX).is_empty());
1142    }
1143
1144    #[test]
1145    fn build_udta_no_art_round_trips() {
1146        let tags = vec![
1147            TagInput::new("title", "Song"),
1148            TagInput::new("tracknumber", "5"),
1149        ];
1150        let (segs, streamed) = build_udta(&tags, &[], &[]).unwrap();
1151        assert_eq!(streamed, 0);
1152        let prefix = materialize_udta(&segs);
1153        let b = read_box(&prefix, 0).unwrap();
1154        assert_eq!(&b.kind, b"udta");
1155        assert_eq!(b.total_len, prefix.len());
1156        // Wrap in a moov and read back through our own reader.
1157        let buf = [
1158            bx(b"ftyp", b"M4A "),
1159            bx(b"moov", &prefix),
1160            bx(b"mdat", b"A"),
1161        ]
1162        .concat();
1163        let tags = read_tags(&buf);
1164        assert!(tags.contains(&("title".into(), "Song".into())));
1165        assert!(tags.contains(&("tracknumber".into(), "5".into())));
1166    }
1167
1168    #[test]
1169    fn build_udta_with_art_reserves_size_without_image() {
1170        let art = ArtInput {
1171            art_id: 1,
1172            mime: "image/png".into(),
1173            description: String::new(),
1174            picture_type: PictureType::new(3).unwrap(),
1175            width: 0,
1176            height: 0,
1177            data_len: BlobLen::new(100).unwrap(),
1178        };
1179        let (segs, streamed) = build_udta(&[TagInput::new("title", "T")], &[], &[art]).unwrap();
1180        assert_eq!(streamed, 100);
1181        // The image streams as the final segment; the udta size field accounts for it.
1182        assert!(matches!(
1183            segs.last(),
1184            Some(Segment::ArtImage { len, .. }) if len.get() == 100
1185        ));
1186        let inline_total: usize = segs
1187            .iter()
1188            .filter_map(|s| match s {
1189                Segment::Inline(b) => Some(b.len()),
1190                _ => None,
1191            })
1192            .sum();
1193        let Segment::Inline(head) = &segs[0] else {
1194            panic!("first udta segment is inline framing");
1195        };
1196        let declared = u32::from_be_bytes(head[0..4].try_into().unwrap()) as usize;
1197        assert_eq!(declared, inline_total + 100);
1198        // The leading inline ends right after the covr/data header (image streams next).
1199        assert!(head.windows(4).any(|w| w == b"covr"));
1200    }
1201
1202    #[test]
1203    fn build_udta_rejects_oversize_art() {
1204        let art = ArtInput {
1205            art_id: 1,
1206            mime: "image/jpeg".into(),
1207            description: String::new(),
1208            picture_type: PictureType::new(3).unwrap(),
1209            width: 0,
1210            height: 0,
1211            data_len: BlobLen::new(u64::from(u32::MAX) + 1).unwrap(),
1212        };
1213        assert!(matches!(
1214            build_udta(&[TagInput::new("title", "T")], &[], &[art]),
1215            Err(FormatError::TooLarge)
1216        ));
1217    }
1218
1219    #[test]
1220    fn build_udta_groups_multi_value_text() {
1221        // Two consecutive same-key text tags must collapse into ONE ilst atom
1222        // carrying REPEATED `data` sub-boxes (iTunes multi-value convention),
1223        // not two separate atoms and not a dropped value.
1224        let tags = vec![
1225            TagInput::new("genre", "Rock"),
1226            TagInput::new("genre", "Metal"),
1227        ];
1228        let (segs, streamed) = build_udta(&tags, &[], &[]).unwrap();
1229        assert_eq!(streamed, 0);
1230        let prefix = materialize_udta(&segs);
1231
1232        // Exactly one `©gen` atom.
1233        let gen_count = prefix.windows(4).filter(|w| *w == b"\xa9gen").count();
1234        assert_eq!(
1235            gen_count, 1,
1236            "expected exactly one genre atom, got {gen_count}"
1237        );
1238
1239        // Locate the `©gen` atom header and parse its children: must be two `data`
1240        // sub-boxes. The 4-byte kind sits at offset +4 of the box, so back up 4.
1241        let kind_at = prefix
1242            .windows(4)
1243            .position(|w| w == b"\xa9gen")
1244            .expect("genre atom present");
1245        let atom = read_box(&prefix, kind_at - 4).unwrap();
1246        assert_eq!(&atom.kind, b"\xa9gen");
1247        let children = child_boxes(atom.payload(&prefix)).unwrap();
1248        let data_count = children.iter().filter(|c| &c.kind == b"data").count();
1249        assert_eq!(
1250            data_count, 2,
1251            "expected two data sub-boxes, got {data_count}"
1252        );
1253
1254        // Both values survive into the bytes.
1255        assert!(prefix.windows(4).any(|w| w == b"Rock"));
1256        assert!(prefix.windows(5).any(|w| w == b"Metal"));
1257    }
1258
1259    #[test]
1260    fn build_udta_empty_tags_is_valid() {
1261        // A real file with no tags must still yield a structurally valid (empty)
1262        // udta, not a malformed box.
1263        let (segs, streamed) = build_udta(&[], &[], &[]).unwrap();
1264        assert_eq!(streamed, 0);
1265        let prefix = materialize_udta(&segs);
1266        let b = read_box(&prefix, 0).unwrap();
1267        assert_eq!(&b.kind, b"udta");
1268        assert_eq!(b.total_len, prefix.len());
1269        // Round-trips as having no tags.
1270        let buf = [
1271            bx(b"ftyp", b"M4A "),
1272            bx(b"moov", &prefix),
1273            bx(b"mdat", b"A"),
1274        ]
1275        .concat();
1276        assert!(read_tags(&buf).is_empty());
1277    }
1278
1279    fn inline_head(layout: &RegionLayout) -> Vec<u8> {
1280        match &layout.segments()[0] {
1281            Segment::Inline(b) => b.clone(),
1282            _ => panic!("expected Inline head"),
1283        }
1284    }
1285    /// Concatenate a udta segment list into a contiguous buffer, substituting `len`
1286    /// zero bytes for each streamed (BinaryTag/ArtImage) segment. Box-size fields
1287    /// already account for these, so the result parses as a complete udta box.
1288    /// Do NOT use for huge reserved art lengths — read the size field off `segs[0]`.
1289    fn materialize_udta(segments: &[Segment]) -> Vec<u8> {
1290        let mut out = Vec::new();
1291        for seg in segments {
1292            match seg {
1293                Segment::Inline(b) => out.extend_from_slice(b),
1294                Segment::BinaryTag { len, .. } | Segment::ArtImage { len, .. } => {
1295                    out.resize(out.len() + usize_from(len.get()), 0);
1296                }
1297                other => panic!("unexpected segment in udta: {other:?}"),
1298            }
1299        }
1300        out
1301    }
1302    /// Locate `moov` by reading complete boxes from the front, stopping before
1303    /// the trailing `mdat` header (whose declared size includes the payload that
1304    /// is *not* present in the synthesized head — it streams as BackingAudio).
1305    fn find_moov_in_head(head: &[u8]) -> BoxRef {
1306        let mut pos = 0;
1307        loop {
1308            let b = read_box(head, pos).unwrap();
1309            if &b.kind == b"moov" {
1310                return b;
1311            }
1312            pos = b.end();
1313        }
1314    }
1315    fn first_stco(head: &[u8]) -> Vec<u32> {
1316        let moov = find_moov_in_head(head);
1317        let mp = moov.payload(head);
1318        let (sp, sl) = find_path(mp, &[b"trak", b"mdia", b"minf", b"stbl", b"stco"])
1319            .unwrap()
1320            .unwrap();
1321        let stco = &mp[sp..sp + sl];
1322        let count = u32::from_be_bytes(stco[4..8].try_into().unwrap()) as usize;
1323        (0..count)
1324            .map(|i| u32::from_be_bytes(stco[8 + i * 4..12 + i * 4].try_into().unwrap()))
1325            .collect()
1326    }
1327
1328    #[test]
1329    fn synthesize_no_art_patches_stco() {
1330        let buf = mk_mp4(true, b"AUDIODATA", &[42, 100]);
1331        let scan = read_structure(&buf).unwrap();
1332        let layout = synthesize_layout(&scan, &[TagInput::new("title", "New")], &[], &[]).unwrap();
1333
1334        match layout.segments().last().unwrap() {
1335            Segment::BackingAudio { offset, len } => {
1336                assert_eq!(*offset, scan.mdat_payload_offset);
1337                assert_eq!(*len, scan.mdat_payload_len);
1338            }
1339            _ => panic!("expected BackingAudio tail"),
1340        }
1341        let head = inline_head(&layout);
1342        // The synthesized head is [ftyp][moov][mdat header]; the mdat payload is
1343        // served verbatim as the BackingAudio tail, so its new position is exactly
1344        // where the head ends.
1345        let new_mdat = head.len() as u64;
1346        let delta = new_mdat - scan.mdat_payload_offset;
1347        assert_eq!(
1348            first_stco(&head),
1349            vec![
1350                42 + u32::try_from(delta).unwrap(),
1351                100 + u32::try_from(delta).unwrap()
1352            ]
1353        );
1354        // The new file head re-parses as a valid moov of the declared size.
1355        let moov = find_moov_in_head(&head);
1356        assert_eq!(moov.end(), head.len() - scan.mdat_header.len());
1357    }
1358
1359    /// Like `mk_mp4` but the soun trak's stbl carries a `co64` (8-byte offsets)
1360    /// box instead of an `stco`. moov-first, since that's all this exercises.
1361    fn mk_mp4_co64(mdat_payload: &[u8], co64_entries: &[u64]) -> Vec<u8> {
1362        let mut co64 = vec![0u8; 4];
1363        co64.extend_from_slice(&u32::try_from(co64_entries.len()).unwrap().to_be_bytes());
1364        for e in co64_entries {
1365            co64.extend_from_slice(&e.to_be_bytes());
1366        }
1367        let mut hdlr_p = vec![0u8; 8];
1368        hdlr_p.extend_from_slice(b"soun");
1369        hdlr_p.extend_from_slice(&[0u8; 12]);
1370        let minf = bx(b"minf", &bx(b"stbl", &bx(b"co64", &co64)));
1371        let mdia = bx(b"mdia", &[bx(b"hdlr", &hdlr_p), minf].concat());
1372        let trak = bx(b"trak", &mdia);
1373        let moov = bx(b"moov", &[bx(b"mvhd", &[0u8; 8]), trak].concat());
1374        let mdat = bx(b"mdat", mdat_payload);
1375        let ftyp = bx(b"ftyp", b"M4A isom");
1376        [ftyp, moov, mdat].concat()
1377    }
1378
1379    fn first_co64(head: &[u8]) -> Vec<u64> {
1380        let moov = find_moov_in_head(head);
1381        let mp = moov.payload(head);
1382        let (sp, sl) = find_path(mp, &[b"trak", b"mdia", b"minf", b"stbl", b"co64"])
1383            .unwrap()
1384            .unwrap();
1385        let co64 = &mp[sp..sp + sl];
1386        let count = u32::from_be_bytes(co64[4..8].try_into().unwrap()) as usize;
1387        (0..count)
1388            .map(|i| u64::from_be_bytes(co64[8 + i * 8..16 + i * 8].try_into().unwrap()))
1389            .collect()
1390    }
1391
1392    #[test]
1393    fn synthesize_patches_co64() {
1394        let buf = mk_mp4_co64(b"AUDIODATA", &[42, 100]);
1395        let scan = read_structure(&buf).unwrap();
1396        let layout = synthesize_layout(&scan, &[TagInput::new("title", "New")], &[], &[]).unwrap();
1397
1398        match layout.segments().last().unwrap() {
1399            Segment::BackingAudio { offset, len } => {
1400                assert_eq!(*offset, scan.mdat_payload_offset);
1401                assert_eq!(*len, scan.mdat_payload_len);
1402            }
1403            _ => panic!("expected BackingAudio tail"),
1404        }
1405        let head = inline_head(&layout);
1406        // mdat payload is served as the BackingAudio tail, so its new position is
1407        // exactly where the head ends; co64 offsets shift by the same delta.
1408        let new_mdat = head.len() as u64;
1409        let delta = new_mdat - scan.mdat_payload_offset;
1410        assert_eq!(first_co64(&head), vec![42 + delta, 100 + delta]);
1411        // The new file head re-parses as a valid moov of the declared size.
1412        let moov = find_moov_in_head(&head);
1413        assert_eq!(moov.end(), head.len() - scan.mdat_header.len());
1414    }
1415
1416    #[test]
1417    fn synthesize_with_art_splits_for_streaming() {
1418        let buf = mk_mp4(false, b"AUDIODATA", &[0]);
1419        let scan = read_structure(&buf).unwrap();
1420        let art = ArtInput {
1421            art_id: 7,
1422            mime: "image/jpeg".into(),
1423            description: String::new(),
1424            picture_type: PictureType::new(3).unwrap(),
1425            width: 0,
1426            height: 0,
1427            data_len: BlobLen::new(50).unwrap(),
1428        };
1429        let layout = synthesize_layout(&scan, &[TagInput::new("title", "T")], &[], &[art]).unwrap();
1430        let segs = layout.segments();
1431        assert!(matches!(segs[1], Segment::ArtImage { art_id: 7, len, .. } if len.get() == 50));
1432        assert!(matches!(segs[2], Segment::Inline(_))); // mdat header
1433        assert!(matches!(segs.last().unwrap(), Segment::BackingAudio { .. }));
1434    }
1435
1436    #[test]
1437    fn synthesize_picks_first_nonempty_art() {
1438        // With multiple non-empty arts, the real art must be served.
1439        let buf = mk_mp4(false, b"AUDIODATA", &[0]);
1440        let scan = read_structure(&buf).unwrap();
1441        let real = ArtInput {
1442            art_id: 9,
1443            mime: "image/png".into(),
1444            description: String::new(),
1445            picture_type: PictureType::new(3).unwrap(),
1446            width: 0,
1447            height: 0,
1448            data_len: BlobLen::new(40).unwrap(),
1449        };
1450        let layout =
1451            synthesize_layout(&scan, &[TagInput::new("title", "T")], &[], &[real]).unwrap();
1452        let segs = layout.segments();
1453        assert!(
1454            segs.iter()
1455                .any(|s| matches!(s, Segment::ArtImage { art_id: 9, len, .. } if len.get() == 40)),
1456            "the first nonempty art must be served"
1457        );
1458    }
1459
1460    #[test]
1461    fn synthesize_handles_zero_length_mdat() {
1462        let buf = mk_mp4(true, b"", &[0]); // empty mdat payload
1463        let scan = read_structure(&buf).unwrap();
1464        assert_eq!(scan.mdat_payload_len, 0);
1465        let layout = synthesize_layout(&scan, &[TagInput::new("title", "Z")], &[], &[]).unwrap();
1466        match layout.segments().last().unwrap() {
1467            Segment::BackingAudio { offset, len } => {
1468                assert_eq!(*offset, scan.mdat_payload_offset);
1469                assert_eq!(*len, 0);
1470            }
1471            _ => panic!("expected BackingAudio tail"),
1472        }
1473    }
1474
1475    #[test]
1476    fn box_header_parses_8_byte_16_byte_and_size0() {
1477        // 8-byte header: size 16, type "moov".
1478        let mut h = 16u32.to_be_bytes().to_vec();
1479        h.extend_from_slice(b"moov");
1480        let bh = box_header(&h, 1000).unwrap();
1481        assert_eq!(&bh.kind, b"moov");
1482        assert_eq!(bh.header_len, 8);
1483        assert_eq!(bh.total_len, 16);
1484
1485        // 64-bit largesize: size32==1, then u64 size = 40.
1486        let mut h = 1u32.to_be_bytes().to_vec();
1487        h.extend_from_slice(b"mdat");
1488        h.extend_from_slice(&40u64.to_be_bytes());
1489        let bh = box_header(&h, 1000).unwrap();
1490        assert_eq!(bh.header_len, 16);
1491        assert_eq!(bh.total_len, 40);
1492
1493        // size32==0 means "extends to EOF" -> total_len == remaining.
1494        let mut h = 0u32.to_be_bytes().to_vec();
1495        h.extend_from_slice(b"mdat");
1496        let bh = box_header(&h, 500).unwrap();
1497        assert_eq!(bh.header_len, 8);
1498        assert_eq!(bh.total_len, 500);
1499    }
1500
1501    #[test]
1502    fn box_header_rejects_impossible_sizes() {
1503        // total_len < header_len.
1504        let mut h = 4u32.to_be_bytes().to_vec();
1505        h.extend_from_slice(b"moov");
1506        assert_eq!(box_header(&h, 1000), Err(FormatError::Malformed));
1507        // total_len > remaining.
1508        let mut h = 2000u32.to_be_bytes().to_vec();
1509        h.extend_from_slice(b"moov");
1510        assert_eq!(box_header(&h, 100), Err(FormatError::Malformed));
1511        // header shorter than 8 bytes.
1512        assert_eq!(box_header(&[0u8; 4], 1000), Err(FormatError::Malformed));
1513    }
1514
1515    #[test]
1516    fn read_structure_from_matches_buffer_path() {
1517        // Both moov-first and moov-last (moov-last is the audiobook spike case).
1518        for moov_first in [true, false] {
1519            let buf = mk_mp4(moov_first, &vec![0xABu8; 4096], &[0]);
1520            let from_buf = read_structure(&buf).unwrap();
1521            let mut cur = std::io::Cursor::new(buf.clone());
1522            let from_stream = read_structure_from(&mut cur, buf.len() as u64).unwrap();
1523            assert_eq!(from_stream, from_buf);
1524        }
1525    }
1526
1527    #[test]
1528    fn read_structure_from_never_reads_mdat_payload() {
1529        // moov LAST: reaching it requires skipping the mdat payload.
1530        let buf = mk_mp4(false, &vec![0xCDu8; 100_000], &[0]);
1531        let scan = read_structure(&buf).unwrap();
1532        let pay_start = scan.mdat_payload_offset;
1533        let pay_end = pay_start + scan.mdat_payload_len;
1534
1535        // A reader that records every byte range it is asked to read.
1536        struct Tracking {
1537            inner: std::io::Cursor<Vec<u8>>,
1538            touched: Vec<(u64, u64)>,
1539        }
1540        impl std::io::Read for Tracking {
1541            fn read(&mut self, b: &mut [u8]) -> std::io::Result<usize> {
1542                let off = self.inner.position();
1543                let n = std::io::Read::read(&mut self.inner, b)?;
1544                self.touched.push((off, off + n as u64));
1545                Ok(n)
1546            }
1547        }
1548        impl std::io::Seek for Tracking {
1549            fn seek(&mut self, p: std::io::SeekFrom) -> std::io::Result<u64> {
1550                self.inner.seek(p)
1551            }
1552        }
1553
1554        let mut tr = Tracking {
1555            inner: std::io::Cursor::new(buf.clone()),
1556            touched: Vec::new(),
1557        };
1558        let from_stream = read_structure_from(&mut tr, buf.len() as u64).unwrap();
1559        assert_eq!(from_stream, scan);
1560        for (s, e) in &tr.touched {
1561            assert!(
1562                *e <= pay_start || *s >= pay_end,
1563                "read [{s},{e}) overlaps mdat payload [{pay_start},{pay_end})"
1564            );
1565        }
1566    }
1567
1568    #[test]
1569    fn read_freeform_extracts_name_and_value() {
1570        // Build a minimal `----` atom: mean + name + data(UTF-8).
1571        let mut mean_body = 0u32.to_be_bytes().to_vec();
1572        mean_body.extend_from_slice(b"com.apple.iTunes");
1573        let mut name_body = 0u32.to_be_bytes().to_vec();
1574        name_body.extend_from_slice(b"MusicBrainz Album Id");
1575        let mut data = 1u32.to_be_bytes().to_vec(); // type 1 = UTF-8
1576        data.extend_from_slice(&0u32.to_be_bytes()); // locale
1577        data.extend_from_slice(b"abc-123");
1578        let mut inner = boxed(b"mean", &mean_body).unwrap();
1579        inner.extend(boxed(b"name", &name_body).unwrap());
1580        inner.extend(boxed(b"data", &data).unwrap());
1581
1582        let (key, value) = read_freeform(&inner).unwrap();
1583        assert_eq!(key, "musicbrainz_albumid"); // folded via vocabulary
1584        assert_eq!(value, "abc-123");
1585    }
1586
1587    #[test]
1588    fn read_freeform_unknown_name_passes_through_verbatim() {
1589        let mut mean_body = 0u32.to_be_bytes().to_vec();
1590        mean_body.extend_from_slice(b"com.apple.iTunes");
1591        let mut name_body = 0u32.to_be_bytes().to_vec();
1592        name_body.extend_from_slice(b"My Custom Field");
1593        let mut data = 1u32.to_be_bytes().to_vec(); // type 1 = UTF-8
1594        data.extend_from_slice(&0u32.to_be_bytes()); // locale
1595        data.extend_from_slice(b"hello");
1596        let mut inner = boxed(b"mean", &mean_body).unwrap();
1597        inner.extend(boxed(b"name", &name_body).unwrap());
1598        inner.extend(boxed(b"data", &data).unwrap());
1599
1600        let (key, value) = read_freeform(&inner).unwrap();
1601        assert_eq!(key, "My Custom Field"); // not in vocabulary -> verbatim name
1602        assert_eq!(value, "hello");
1603    }
1604
1605    #[test]
1606    fn read_freeform_skips_binary_typed_data() {
1607        let mut name_body = 0u32.to_be_bytes().to_vec();
1608        name_body.extend_from_slice(b"My Custom Field");
1609        let mut data = 0u32.to_be_bytes().to_vec(); // type 0 = binary, not text
1610        data.extend_from_slice(&0u32.to_be_bytes()); // locale
1611        data.extend_from_slice(&[0xff, 0x00, 0x01]);
1612        let mut inner = boxed(b"name", &name_body).unwrap();
1613        inner.extend(boxed(b"data", &data).unwrap());
1614
1615        assert!(read_freeform(&inner).is_none()); // binary-typed data is skipped
1616    }
1617
1618    #[test]
1619    fn build_udta_round_trips_freeform_and_vocabulary() {
1620        let tags = vec![
1621            TagInput::new("title", "Song"),
1622            TagInput::new("tracknumber", "3"),
1623            TagInput::new("MyRating", "5"), // user-defined -> ----
1624            TagInput::new("musicbrainz_albumid", "abc-123"), // vocabulary -> ----
1625        ];
1626        let (segs, _streamed) = build_udta(&tags, &[], &[]).unwrap();
1627        let udta = materialize_udta(&segs);
1628        // build_udta returns a full `udta` box; read_tags expects a buffer containing
1629        // moov/udta/meta/ilst, so wrap udta in a minimal moov for the round trip.
1630        let moov = boxed(b"moov", &udta).unwrap();
1631
1632        let tags = read_tags(&moov);
1633        for expected in [
1634            ("title", "Song"),
1635            ("tracknumber", "3"),
1636            ("MyRating", "5"),
1637            ("musicbrainz_albumid", "abc-123"),
1638        ] {
1639            assert!(
1640                tags.contains(&(expected.0.to_string(), expected.1.to_string())),
1641                "missing {expected:?} in {tags:?}"
1642            );
1643        }
1644    }
1645
1646    #[test]
1647    fn read_box_rejects_overflowing_extended_size() {
1648        // The extended-size path (size32 == 1) reads a 64-bit box length from
1649        // untrusted input. Before the checked_add fix, `pos + total` overflowed
1650        // usize in debug (panic) or wrapped silently in release (accepting a
1651        // bogus length). This test feeds size32=1 with a u64::MAX extended size
1652        // and asserts the parser returns an error rather than panicking.
1653        // Bytes: [00 00 00 01] (size32=1) + b"moov" + [FF FF FF FF FF FF FF FF] (u64::MAX)
1654        let mut bytes = 1u32.to_be_bytes().to_vec(); // size32 = 1 → extended-size
1655        bytes.extend_from_slice(b"moov");
1656        bytes.extend_from_slice(&u64::MAX.to_be_bytes()); // huge 64-bit size
1657        assert!(
1658            read_structure(&bytes).is_err(),
1659            "must return an error, not panic"
1660        );
1661    }
1662
1663    #[test]
1664    fn read_structure_from_handles_largesize_mdat() {
1665        // Re-encode a normal fixture's mdat with a 64-bit largesize header (the
1666        // real >4GB audiobook shape) and confirm both readers agree.
1667        fn largesize_mdat(payload: &[u8]) -> Vec<u8> {
1668            let total = 16 + payload.len() as u64;
1669            let mut v = 1u32.to_be_bytes().to_vec(); // size32 == 1
1670            v.extend_from_slice(b"mdat");
1671            v.extend_from_slice(&total.to_be_bytes()); // 64-bit largesize
1672            v.extend_from_slice(payload);
1673            v
1674        }
1675        let normal = mk_mp4(true, &[0xABu8; 64], &[0]); // [ftyp][moov][mdat]
1676        let scan = read_structure(&normal).unwrap();
1677        let payload_start = usize_from(scan.mdat_payload_offset);
1678        let mdat_box_start = payload_start - scan.mdat_header.len(); // normal 8-byte header
1679        let payload = normal[payload_start..].to_vec();
1680        let mut buf = normal[..mdat_box_start].to_vec(); // ftyp + moov
1681        buf.extend(largesize_mdat(&payload));
1682
1683        let from_buf = read_structure(&buf).unwrap();
1684        let mut cur = std::io::Cursor::new(buf.clone());
1685        let from_stream = read_structure_from(&mut cur, buf.len() as u64).unwrap();
1686        assert_eq!(from_stream, from_buf);
1687        assert_eq!(from_stream.mdat_header.len(), 16); // largesize header
1688        assert_eq!(from_stream.mdat_payload_len, payload.len() as u64);
1689    }
1690
1691    #[test]
1692    fn box_header_accepts_empty_payload_box() {
1693        // total_len == header_len (an 8-byte box, no payload) must be accepted.
1694        // `< -> <=` would make the equal case reject.
1695        let mut h = 8u32.to_be_bytes().to_vec();
1696        h.extend_from_slice(b"free");
1697        let bh = box_header(&h, 1000).unwrap();
1698        assert_eq!(bh.header_len, 8);
1699        assert_eq!(bh.total_len, 8);
1700    }
1701
1702    #[test]
1703    fn read_box_size0_extends_to_end_from_offset() {
1704        // A size-0 box ("extends to EOF") at pos > 0: total_len must be
1705        // buf.len() - pos. `- -> +` (buf.len() + pos) and `- -> /` (buf.len() / pos)
1706        // both diverge. The box is placed at pos = 8 with pos + 8 <= buf.len() so the
1707        // be_u32 size read and the kind slice both succeed BEFORE the size-0 branch.
1708        let mut buf = bx(b"free", b""); // 8-byte box at pos 0
1709        buf.extend_from_slice(&0u32.to_be_bytes()); // size32 = 0 at pos 8
1710        buf.extend_from_slice(b"mdat"); // kind at pos 12..16
1711        buf.extend_from_slice(b"AUDIOPAYLOAD"); // 12 payload bytes
1712        assert_eq!(buf.len(), 28);
1713        let b = read_box(&buf, 8).unwrap();
1714        assert_eq!(&b.kind, b"mdat");
1715        assert_eq!(b.total_len, buf.len() - 8); // 20
1716    }
1717
1718    #[test]
1719    fn read_structure_from_rejects_box_overrunning_eof() {
1720        // box_header's `remaining` arg is `file_len - pos`. Inflating the mdat box's
1721        // declared size past the bytes remaining must be rejected. `- -> +` inflates
1722        // `remaining` to `file_len + pos`, wrongly accepting the overrun (returns Ok).
1723        let mut buf = mk_mp4(true, b"AUDIO", &[0]); // [ftyp][moov][mdat], mdat last
1724        let scan = read_structure(&buf).unwrap();
1725        let mdat_start = usize_from(scan.mdat_payload_offset - scan.mdat_header.len() as u64);
1726        let real = u32::from_be_bytes(buf[mdat_start..mdat_start + 4].try_into().unwrap());
1727        buf[mdat_start..mdat_start + 4].copy_from_slice(&(real + 100).to_be_bytes());
1728        let mut cur = std::io::Cursor::new(buf.clone());
1729        assert!(read_structure_from(&mut cur, buf.len() as u64).is_err());
1730    }
1731
1732    #[test]
1733    fn read_structure_from_rejects_moof() {
1734        // A `moof` (fragmented MP4) top-level box must be rejected via the seeking
1735        // path. Deleting the `b"moof"` match arm drops it to `_ => {}` and accepts.
1736        let mut buf = mk_mp4(true, b"AUDIO", &[0]);
1737        buf.extend(bx(b"moof", b"\x00\x00\x00\x00"));
1738        let mut cur = std::io::Cursor::new(buf.clone());
1739        assert!(read_structure_from(&mut cur, buf.len() as u64).is_err());
1740    }
1741
1742    #[test]
1743    fn read_structure_from_rejects_duplicate_top_level_boxes() {
1744        // Each `dup |= X.replace(..).is_some()` accumulates a duplicate. `|= -> &=`
1745        // can never set `dup` (it starts false), so a duplicate box is wrongly
1746        // accepted. One duplicated box per kind isolates each of the three `|=` lines.
1747        let dup = |extra: Vec<u8>| {
1748            let mut buf = mk_mp4(true, b"AUDIO", &[0]);
1749            buf.extend(extra);
1750            let mut cur = std::io::Cursor::new(buf.clone());
1751            read_structure_from(&mut cur, buf.len() as u64).is_err()
1752        };
1753        assert!(dup(bx(b"ftyp", b"M4A isom")), "duplicate ftyp must reject"); // ftyp |= line
1754        // duplicate moov: reuse the moov from a fresh fixture so it is structurally valid.
1755        let extra_moov = {
1756            let other = mk_mp4(true, b"AUDIO", &[0]);
1757            let s = read_structure(&other).unwrap();
1758            s.moov
1759        };
1760        assert!(dup(extra_moov), "duplicate moov must reject"); // moov |= line
1761        assert!(dup(bx(b"mdat", b"Y")), "duplicate mdat must reject"); // mdat |= line
1762    }
1763
1764    #[test]
1765    fn read_freeform_accepts_minimal_name_and_data() {
1766        // name payload == 4 (empty name) and data payload == 8 (empty value) is the
1767        // boundary of `np.len() < 4 || dp.len() < 8`. Both operands at the boundary,
1768        // so flipping EITHER `<` to `==`/`<=` makes that side true -> None.
1769        let name_body = 0u32.to_be_bytes().to_vec(); // exactly 4 bytes
1770        let mut data = 1u32.to_be_bytes().to_vec(); // type 1 = UTF-8
1771        data.extend_from_slice(&0u32.to_be_bytes()); // locale -> dp.len() == 8
1772        let mut inner = boxed(b"name", &name_body).unwrap();
1773        inner.extend(boxed(b"data", &data).unwrap());
1774        let (key, value) = read_freeform(&inner).unwrap();
1775        assert_eq!(key, ""); // empty name, not in vocabulary -> verbatim ""
1776        assert_eq!(value, "");
1777    }
1778
1779    #[test]
1780    fn read_freeform_short_name_returns_none() {
1781        // name payload 3 bytes (< 4) with a valid 8-byte data payload. `|| -> &&`
1782        // makes `true && false == false`, falling through to `&np[4..]` (out of bounds
1783        // -> panic).
1784        let name_body = vec![0u8, 0, 0]; // 3 bytes
1785        let mut data = 1u32.to_be_bytes().to_vec();
1786        data.extend_from_slice(&0u32.to_be_bytes());
1787        let mut inner = boxed(b"name", &name_body).unwrap();
1788        inner.extend(boxed(b"data", &data).unwrap());
1789        assert!(read_freeform(&inner).is_none());
1790    }
1791
1792    #[test]
1793    fn read_freeform_mean_payload_exactly_4_uses_empty_mean() {
1794        // mean payload == 4 (FullBox prefix, empty mean). `p.len() >= 4` must take the
1795        // utf8 branch (mean ""), so the vocabulary does NOT fold the iTunes name.
1796        // `>= -> <` falls to the default "com.apple.iTunes" mean and wrongly folds.
1797        let mean_body = vec![0u8, 0, 0, 0]; // exactly 4 bytes
1798        let mut name_body = 0u32.to_be_bytes().to_vec();
1799        name_body.extend_from_slice(b"MusicBrainz Album Id");
1800        let mut data = 1u32.to_be_bytes().to_vec();
1801        data.extend_from_slice(&0u32.to_be_bytes());
1802        data.extend_from_slice(b"abc-123");
1803        let mut inner = boxed(b"mean", &mean_body).unwrap();
1804        inner.extend(boxed(b"name", &name_body).unwrap());
1805        inner.extend(boxed(b"data", &data).unwrap());
1806        let (key, value) = read_freeform(&inner).unwrap();
1807        assert_eq!(key, "MusicBrainz Album Id"); // empty mean -> not folded
1808        assert_eq!(value, "abc-123");
1809    }
1810
1811    #[test]
1812    fn read_tags_data_payload_exactly_8_is_read() {
1813        // A `data` payload of exactly 8 bytes (type+locale, empty value) is the
1814        // boundary of `dp.len() < 8`. The (empty) value must be read; `< -> ==`/`<= `
1815        // would skip it.
1816        let atoms = bx(b"\xa9nam", &data_atom(1, b"")); // dp.len() == 8
1817        let buf = mp4_with_ilst(&atoms, true);
1818        assert!(read_tags(&buf).contains(&("title".into(), String::new())));
1819    }
1820
1821    #[test]
1822    fn read_tags_disk_exact_4_byte_value_yields_discnumber() {
1823        // disk atom, value exactly 4 bytes: `kind == disk` (== branch) `&&`
1824        // `value.len() >= 4` (>= branch). Kills `== -> !=` (mutant skips a real disk)
1825        // and `>= -> <` (mutant skips the boundary length).
1826        let atoms = bx(b"disk", &data_atom(0, &[0, 0, 0, 2])); // disc 2, value len 4
1827        let buf = mp4_with_ilst(&atoms, true);
1828        assert!(read_tags(&buf).contains(&("discnumber".into(), "2".into())));
1829    }
1830
1831    #[test]
1832    fn read_tags_disk_short_value_is_skipped() {
1833        // disk with a value shorter than 4 bytes: the guard is false. `&& -> ||`
1834        // makes it true and indexes value[2]/value[3] out of bounds (panic).
1835        let atoms = bx(b"disk", &data_atom(0, &[0, 0])); // value len 2
1836        let buf = mp4_with_ilst(&atoms, true);
1837        assert!(!read_tags(&buf).iter().any(|(k, _)| k == "discnumber"));
1838    }
1839
1840    #[test]
1841    fn read_tags_trkn_short_value_is_skipped() {
1842        // trkn with a value shorter than 4 bytes: `kind == trkn && value.len() >= 4`
1843        // is false. `&& -> ||` makes it true and indexes value[2]/value[3] (panic).
1844        let atoms = bx(b"trkn", &data_atom(0, &[0, 0])); // value len 2
1845        let buf = mp4_with_ilst(&atoms, true);
1846        assert!(!read_tags(&buf).iter().any(|(k, _)| k == "tracknumber"));
1847    }
1848
1849    #[test]
1850    fn read_pictures_data_payload_exactly_8_is_read() {
1851        // covr/data payload of exactly 8 bytes (type+locale, empty image) is the
1852        // boundary of `dp.len() < 8`; the (empty) picture must be read.
1853        let buf = mp4_with_ilst(&bx(b"covr", &data_atom(13, b"")), true);
1854        let pics = read_pictures(&buf, usize::MAX);
1855        assert_eq!(pics.len(), 1);
1856        assert_eq!(pics[0].mime, "image/jpeg");
1857        assert!(pics[0].data.is_empty());
1858    }
1859
1860    #[test]
1861    fn read_pictures_recognizes_png() {
1862        // A covr `data` atom with type code 14 is PNG. Deleting the `14 =>` match arm
1863        // drops it to `_ => continue` and yields no picture.
1864        let png = [0x89, b'P', b'N', b'G', 1, 2, 3];
1865        let buf = mp4_with_ilst(&bx(b"covr", &data_atom(14, &png)), false);
1866        let pics = read_pictures(&buf, usize::MAX);
1867        assert_eq!(pics.len(), 1);
1868        assert_eq!(pics[0].mime, "image/png");
1869        assert_eq!(pics[0].data, png);
1870    }
1871
1872    #[test]
1873    fn read_pictures_reads_all_data_atoms_in_one_covr() {
1874        // iTunes convention: multiple artworks are multiple `data` children of
1875        // one `covr`. An unknown type code skips that child only, not its
1876        // siblings.
1877        let jpeg = [0xFF, 0xD8, 0xFF, 1];
1878        let png = [0x89, b'P', b'N', b'G', 2];
1879        let covr = bx(
1880            b"covr",
1881            &[
1882                data_atom(13, &jpeg),
1883                data_atom(99, b"skipped"), // unknown type code: this child only
1884                data_atom(14, &png),
1885            ]
1886            .concat(),
1887        );
1888        let buf = mp4_with_ilst(&covr, true);
1889        let pics = read_pictures(&buf, usize::MAX);
1890        assert_eq!(pics.len(), 2);
1891        assert_eq!(pics[0].mime, "image/jpeg");
1892        assert_eq!(pics[0].data, jpeg);
1893        assert_eq!(pics[1].mime, "image/png");
1894        assert_eq!(pics[1].data, png);
1895    }
1896
1897    #[test]
1898    fn read_pictures_skips_art_over_budget() {
1899        let over = vec![0xFFu8; 5];
1900        let buf = mp4_with_ilst(&bx(b"covr", &data_atom(13, &over)), true);
1901        assert!(read_pictures(&buf, 4).is_empty());
1902    }
1903
1904    #[test]
1905    fn read_pictures_accepts_art_exactly_at_budget() {
1906        let exact = vec![0xFFu8; 4];
1907        let buf = mp4_with_ilst(&bx(b"covr", &data_atom(13, &exact)), true);
1908        let pics = read_pictures(&buf, 4);
1909        assert_eq!(pics.len(), 1);
1910        assert_eq!(pics[0].data, exact);
1911    }
1912
1913    #[test]
1914    fn read_pictures_skips_non_data_children_of_covr() {
1915        // A non-`data` child inside covr (rare but legal) is silently skipped.
1916        let png = [0x89, b'P', b'N', b'G'];
1917        let covr = bx(
1918            b"covr",
1919            &[bx(b"free", b"pad"), data_atom(14, &png)].concat(),
1920        );
1921        let buf = mp4_with_ilst(&covr, false);
1922        let pics = read_pictures(&buf, usize::MAX);
1923        assert_eq!(pics.len(), 1);
1924        assert_eq!(pics[0].mime, "image/png");
1925        assert_eq!(pics[0].data, png);
1926    }
1927
1928    #[test]
1929    fn build_udta_png_art_uses_type_code_14() {
1930        // PNG art => covr/data type code 14; JPEG => 13. `== -> !=` flips them.
1931        for (mime, expected) in [("image/png", 14u32), ("image/jpeg", 13u32)] {
1932            let art = ArtInput {
1933                art_id: 1,
1934                mime: mime.into(),
1935                description: String::new(),
1936                picture_type: PictureType::new(3).unwrap(),
1937                width: 0,
1938                height: 0,
1939                data_len: BlobLen::new(10).unwrap(),
1940            };
1941            let (segs, _) = build_udta(&[TagInput::new("title", "T")], &[], &[art]).unwrap();
1942            let prefix = materialize_udta(&segs);
1943            // covr layout: [covr_size u32]["covr"][data_size u32]["data"][type u32][locale u32]
1944            let cpos = prefix.windows(4).position(|w| w == b"covr").expect("covr");
1945            assert_eq!(&prefix[cpos + 8..cpos + 12], b"data");
1946            let type_code = u32::from_be_bytes(prefix[cpos + 12..cpos + 16].try_into().unwrap());
1947            assert_eq!(type_code, expected, "mime {mime}");
1948        }
1949    }
1950
1951    #[test]
1952    fn build_udta_art_box_sizes_are_exact() {
1953        // data_size = 8 + 8 + data_len; covr_size = 8 + data_size. The `+ -> -`/`+ -> *`
1954        // mutations change the emitted box sizes.
1955        let art = ArtInput {
1956            art_id: 1,
1957            mime: "image/jpeg".into(),
1958            description: String::new(),
1959            picture_type: PictureType::new(3).unwrap(),
1960            width: 0,
1961            height: 0,
1962            data_len: BlobLen::new(10).unwrap(),
1963        };
1964        let (segs, _) = build_udta(&[TagInput::new("title", "T")], &[], &[art]).unwrap();
1965        let prefix = materialize_udta(&segs);
1966        let cpos = prefix.windows(4).position(|w| w == b"covr").expect("covr");
1967        let covr_size = u32::from_be_bytes(prefix[cpos - 4..cpos].try_into().unwrap());
1968        let data_size = u32::from_be_bytes(prefix[cpos + 4..cpos + 8].try_into().unwrap());
1969        assert_eq!(data_size, 8 + 8 + 10); // 26
1970        assert_eq!(covr_size, 8 + data_size); // 34
1971    }
1972
1973    #[test]
1974    fn build_udta_multiple_arts_one_covr_n_data_atoms() {
1975        let art = |id: i64, mime: &str, len: u64| ArtInput {
1976            art_id: id,
1977            mime: mime.into(),
1978            description: String::new(),
1979            picture_type: PictureType::new(3).unwrap(),
1980            width: 0,
1981            height: 0,
1982            data_len: BlobLen::new(len).unwrap(),
1983        };
1984        let arts = [art(1, "image/jpeg", 10), art(2, "image/png", 20)];
1985        let (segs, streamed) = build_udta(&[TagInput::new("title", "T")], &[], &arts).unwrap();
1986        assert_eq!(streamed, 30);
1987
1988        // Exactly one covr atom, sized for both data atoms: 8 + Σ(16 + len).
1989        let prefix = materialize_udta(&segs);
1990        let covr_positions: Vec<usize> = prefix
1991            .windows(4)
1992            .enumerate()
1993            .filter_map(|(i, w)| (w == b"covr").then_some(i))
1994            .collect();
1995        assert_eq!(covr_positions.len(), 1);
1996        let cpos = covr_positions[0];
1997        let covr_size = u32::from_be_bytes(prefix[cpos - 4..cpos].try_into().unwrap());
1998        assert_eq!(covr_size, 8 + (16 + 10) + (16 + 20));
1999
2000        // First data atom: jpeg (type 13), size 16+10; second: png (14), 16+20.
2001        let d1 = cpos + 4;
2002        assert_eq!(&prefix[d1 + 4..d1 + 8], b"data");
2003        assert_eq!(
2004            u32::from_be_bytes(prefix[d1..d1 + 4].try_into().unwrap()),
2005            26
2006        );
2007        assert_eq!(
2008            u32::from_be_bytes(prefix[d1 + 8..d1 + 12].try_into().unwrap()),
2009            13
2010        );
2011        let d2 = d1 + 26;
2012        assert_eq!(&prefix[d2 + 4..d2 + 8], b"data");
2013        assert_eq!(
2014            u32::from_be_bytes(prefix[d2..d2 + 4].try_into().unwrap()),
2015            36
2016        );
2017        assert_eq!(
2018            u32::from_be_bytes(prefix[d2 + 8..d2 + 12].try_into().unwrap()),
2019            14
2020        );
2021
2022        // Streamed segments: one ArtImage per art, in input order.
2023        let art_segs: Vec<(i64, u64)> = segs
2024            .iter()
2025            .filter_map(|s| match s {
2026                Segment::ArtImage { art_id, len } => Some((*art_id, len.get())),
2027                _ => None,
2028            })
2029            .collect();
2030        assert_eq!(art_segs, vec![(1, 10), (2, 20)]);
2031    }
2032
2033    #[test]
2034    fn build_udta_two_arts_round_trips_through_read_pictures() {
2035        // materialize_udta zero-fills streamed payloads, so assert order +
2036        // mime only (mime derives from the inline type code, which survives).
2037        let art = |id: i64, mime: &str, len: u64| ArtInput {
2038            art_id: id,
2039            mime: mime.into(),
2040            description: String::new(),
2041            picture_type: PictureType::new(3).unwrap(),
2042            width: 0,
2043            height: 0,
2044            data_len: BlobLen::new(len).unwrap(),
2045        };
2046        let arts = [art(1, "image/jpeg", 5), art(2, "image/png", 9)];
2047        let (segs, _) = build_udta(&[TagInput::new("title", "Song")], &[], &arts).unwrap();
2048        let prefix = materialize_udta(&segs);
2049        let buf = [
2050            bx(b"ftyp", b"M4A "),
2051            bx(b"moov", &prefix),
2052            bx(b"mdat", b"A"),
2053        ]
2054        .concat();
2055        let pics = read_pictures(&buf, usize::MAX);
2056        assert_eq!(pics.len(), 2);
2057        assert_eq!(pics[0].mime, "image/jpeg");
2058        assert_eq!(pics[0].data.len(), 5);
2059        assert_eq!(pics[1].mime, "image/png");
2060        assert_eq!(pics[1].data.len(), 9);
2061    }
2062
2063    #[test]
2064    fn build_udta_udta_size_exactly_u32_max_is_ok() {
2065        // The guard is `udta_size > u32::MAX` (strict). udta_size == u32::MAX must be
2066        // accepted; `> -> >=` rejects the exact boundary. data_len is reserved as a
2067        // number (no image bytes), so the boundary is cheap to hit.
2068        fn art(data_len: u64) -> ArtInput {
2069            ArtInput {
2070                art_id: 1,
2071                mime: "image/jpeg".into(),
2072                description: String::new(),
2073                picture_type: PictureType::new(3).unwrap(),
2074                width: 0,
2075                height: 0,
2076                data_len: BlobLen::new(data_len).unwrap(),
2077            }
2078        }
2079        // Derive the fixed overhead from the udta size field (segs[0] inline), with
2080        // data_len 1 (BlobLen is non-zero), without materializing any image bytes.
2081        let (segs0, _) = build_udta(&[TagInput::new("title", "T")], &[], &[art(1)]).unwrap();
2082        let Segment::Inline(h0) = &segs0[0] else {
2083            panic!("inline head")
2084        };
2085        let overhead = u64::from(u32::from_be_bytes(h0[0..4].try_into().unwrap())) - 1;
2086        let max_len = u64::from(u32::MAX) - overhead;
2087
2088        let (segs_max, streamed) =
2089            build_udta(&[TagInput::new("title", "T")], &[], &[art(max_len)]).unwrap();
2090        assert_eq!(streamed, max_len);
2091        let Segment::Inline(h_max) = &segs_max[0] else {
2092            panic!("inline head")
2093        };
2094        assert_eq!(
2095            u32::from_be_bytes(h_max[0..4].try_into().unwrap()),
2096            u32::MAX
2097        );
2098
2099        assert!(matches!(
2100            build_udta(&[TagInput::new("title", "T")], &[], &[art(max_len + 1)]),
2101            Err(FormatError::TooLarge)
2102        ));
2103    }
2104
2105    #[test]
2106    fn patch_chunk_offsets_stco_overflow_and_underflow_boundaries() {
2107        // kept = a single soun trak with one stco entry (offset 0). v = 0 + delta is
2108        // guarded by `v < 0 || v > u32::MAX`. Boundary deltas pin every guard mutant;
2109        // delta 0 (accepted) also pins the `:590` `+ -> *` bound at i = 0.
2110        let mut k = soun_trak();
2111        assert!(patch_chunk_offsets(&mut k, 0).is_ok()); // v == 0
2112
2113        let mut k = soun_trak();
2114        assert!(patch_chunk_offsets(&mut k, i64::from(u32::MAX)).is_ok()); // v == u32::MAX
2115
2116        let mut k = soun_trak();
2117        assert!(matches!(
2118            patch_chunk_offsets(&mut k, i64::from(u32::MAX) + 1), // v == u32::MAX + 1
2119            Err(FormatError::TooLarge)
2120        ));
2121
2122        let mut k = soun_trak();
2123        assert!(matches!(
2124            patch_chunk_offsets(&mut k, -1), // v == -1
2125            Err(FormatError::TooLarge)
2126        ));
2127    }
2128
2129    #[test]
2130    fn patch_chunk_offsets_rejects_count_past_table() {
2131        // stco declares 2 entries but only 1 entry's bytes are present (followed by an
2132        // unrelated `free` box for padding). `pos + entry > start + len` must reject
2133        // the 2nd entry. `+ -> -` shrinks the bound and reads into the `free` box
2134        // instead of erroring (returns Ok).
2135        let mut stco = vec![0u8; 4]; // version/flags
2136        stco.extend_from_slice(&2u32.to_be_bytes()); // count = 2 (a lie)
2137        stco.extend_from_slice(&0u32.to_be_bytes()); // only 1 entry present
2138        let stbl = bx(
2139            b"stbl",
2140            &[bx(b"stco", &stco), bx(b"free", &[0u8; 8])].concat(),
2141        );
2142        let mut kept = bx(b"trak", &bx(b"mdia", &bx(b"minf", &stbl)));
2143        assert!(matches!(
2144            patch_chunk_offsets(&mut kept, 0),
2145            Err(FormatError::Malformed)
2146        ));
2147    }
2148
2149    #[test]
2150    fn patch_chunk_offsets_co64_zero_offset_is_ok() {
2151        // co64 path guard is `v < 0`. offset 0 + delta 0 => v == 0 must be accepted;
2152        // `< -> ==`/`<= ` reject the boundary.
2153        let mut co64 = vec![0u8; 4]; // version/flags
2154        co64.extend_from_slice(&1u32.to_be_bytes()); // count 1
2155        co64.extend_from_slice(&0u64.to_be_bytes()); // offset 0
2156        let stbl = bx(b"stbl", &bx(b"co64", &co64));
2157        let mut kept = bx(b"trak", &bx(b"mdia", &bx(b"minf", &stbl)));
2158        assert!(patch_chunk_offsets(&mut kept, 0).is_ok());
2159    }
2160
2161    /// Build a `----` freeform atom with an explicit data `type_code` and raw value.
2162    fn freeform_atom_typed(mean: &str, name: &str, type_code: u32, value: &[u8]) -> Vec<u8> {
2163        let mut mean_body = 0u32.to_be_bytes().to_vec();
2164        mean_body.extend_from_slice(mean.as_bytes());
2165        let mut name_body = 0u32.to_be_bytes().to_vec();
2166        name_body.extend_from_slice(name.as_bytes());
2167        let mut data_body = type_code.to_be_bytes().to_vec();
2168        data_body.extend_from_slice(&0u32.to_be_bytes()); // locale
2169        data_body.extend_from_slice(value);
2170        let mut inner = boxed(b"mean", &mean_body).unwrap();
2171        inner.extend(boxed(b"name", &name_body).unwrap());
2172        inner.extend(boxed(b"data", &data_body).unwrap());
2173        boxed(b"----", &inner).unwrap()
2174    }
2175
2176    /// Wrap an `ilst` body in the moov/udta/meta/ilst boxes `ilst_region` expects.
2177    fn moov_with_ilst(ilst_body: &[u8]) -> Vec<u8> {
2178        let ilst = boxed(b"ilst", ilst_body).unwrap();
2179        let mut meta = 0u32.to_be_bytes().to_vec(); // FullBox version/flags
2180        meta.extend(boxed(b"hdlr", &[0u8; 25]).unwrap());
2181        meta.extend_from_slice(&ilst);
2182        let udta = boxed(b"udta", &boxed(b"meta", &meta).unwrap()).unwrap();
2183        boxed(b"moov", &udta).unwrap()
2184    }
2185
2186    #[test]
2187    fn read_binary_tags_extracts_opaque_freeform_skips_text() {
2188        let serato = vec![0x00, 0xff, 0x10, 0x42, 0x99];
2189        let binary = freeform_atom_typed("com.serato.dj", "analysis", 0, &serato);
2190        let text = freeform_atom_typed("com.apple.iTunes", "MOOD", 1, b"calm");
2191        let moov = moov_with_ilst(&[binary, text].concat());
2192
2193        let tags = read_binary_tags(&moov, usize::MAX);
2194        assert_eq!(tags.len(), 1, "only the binary `----` is opaque");
2195        assert_eq!(tags[0].key, "----:com.serato.dj:analysis");
2196        assert_eq!(tags[0].payload, serato);
2197
2198        // The text `----` is the text path's job, never opaque.
2199        assert!(
2200            read_binary_tags(&moov, usize::MAX)
2201                .iter()
2202                .all(|t| t.key != "----:com.apple.iTunes:MOOD")
2203        );
2204    }
2205
2206    #[test]
2207    fn read_binary_tags_handles_data_box_length_boundary() {
2208        // A `data` box shorter than the 8-byte `[type][locale]` header is malformed:
2209        // it must be skipped, never indexed into (no panic).
2210        let mut short_inner = boxed(b"mean", &{
2211            let mut b = 0u32.to_be_bytes().to_vec();
2212            b.extend_from_slice(b"com.serato.dj");
2213            b
2214        })
2215        .unwrap();
2216        short_inner.extend(
2217            boxed(b"name", &{
2218                let mut b = 0u32.to_be_bytes().to_vec();
2219                b.extend_from_slice(b"short");
2220                b
2221            })
2222            .unwrap(),
2223        );
2224        short_inner.extend(boxed(b"data", &[0u8; 5]).unwrap()); // 5 < 8: truncated header
2225        let short = boxed(b"----", &short_inner).unwrap();
2226
2227        // A `data` box of exactly 8 bytes (binary type 0, no value) is well-formed
2228        // with an empty payload — it must be emitted, not skipped.
2229        let empty = freeform_atom_typed("com.serato.dj", "empty", 0, b"");
2230        let moov = moov_with_ilst(&[short, empty].concat());
2231
2232        let tags = read_binary_tags(&moov, usize::MAX);
2233        assert_eq!(tags.len(), 1, "short data skipped, 8-byte data emitted");
2234        assert_eq!(tags[0].key, "----:com.serato.dj:empty");
2235        assert!(tags[0].payload.is_empty());
2236    }
2237
2238    #[test]
2239    fn read_binary_tags_skips_payload_over_budget() {
2240        // A `----` value of 5 bytes with a budget of 4: skipped before any copy.
2241        let over = vec![0xABu8; 5];
2242        let atom = freeform_atom_typed("com.serato.dj", "analysis", 0, &over);
2243        let moov = moov_with_ilst(&atom);
2244        assert!(read_binary_tags(&moov, 4).is_empty());
2245    }
2246
2247    #[test]
2248    fn read_binary_tags_accepts_payload_exactly_at_budget() {
2249        // Boundary: value length == budget is still extracted.
2250        let exact = vec![0xABu8; 4];
2251        let atom = freeform_atom_typed("com.serato.dj", "analysis", 0, &exact);
2252        let moov = moov_with_ilst(&atom);
2253        let tags = read_binary_tags(&moov, 4);
2254        assert_eq!(tags.len(), 1);
2255        assert_eq!(tags[0].payload, exact);
2256    }
2257
2258    #[test]
2259    fn synthesize_interleaves_binary_freeform_segment() {
2260        let buf = mk_mp4(true, b"AUDIODATA", &[42, 100]);
2261        let scan = read_structure(&buf).unwrap();
2262        let payload = vec![0xde, 0xad, 0xbe, 0xef, 0x00, 0x01];
2263        let bins = vec![BinaryTagInput {
2264            key: "----:com.serato.dj:analysis".into(),
2265            payload_id: 7,
2266            len: BlobLen::new(payload.len() as u64).unwrap(),
2267        }];
2268        let layout = synthesize_layout(&scan, &[TagInput::new("title", "T")], &bins, &[]).unwrap();
2269
2270        // Exactly one streamed BinaryTag carrying our handle + length.
2271        let bt: Vec<_> = layout
2272            .segments()
2273            .iter()
2274            .filter_map(|s| match s {
2275                Segment::BinaryTag { payload_id, len } => Some((*payload_id, len.get())),
2276                _ => None,
2277            })
2278            .collect();
2279        assert_eq!(bt, vec![(7, payload.len() as u64)]);
2280
2281        // Audio is still served verbatim as the trailing BackingAudio run.
2282        match layout.segments().last().unwrap() {
2283            Segment::BackingAudio { offset, len } => {
2284                assert_eq!(*offset, scan.mdat_payload_offset);
2285                assert_eq!(*len, scan.mdat_payload_len);
2286            }
2287            _ => panic!("expected BackingAudio tail"),
2288        }
2289
2290        // Box sizes are self-consistent: materialize the served file (binary payload
2291        // + backing audio substituted) and re-parse. `read_structure` validates every
2292        // moov/mdat box size, so a green re-parse proves the ----/ilst/meta/udta/moov
2293        // sizes all account for the streamed payload — and the opaque `----` survives
2294        // the round trip byte-identically.
2295        //
2296        // NOTE: do NOT use `inline_head`/`find_moov_in_head` here — the moov box now
2297        // spans multiple segments (the streamed BinaryTag splits it), so `read_box`
2298        // on `segments[0]` alone returns `Malformed`. Materialize the whole file.
2299        let mut served = Vec::new();
2300        for seg in layout.segments() {
2301            match seg {
2302                Segment::Inline(b) => served.extend_from_slice(b),
2303                Segment::BinaryTag { .. } => served.extend_from_slice(&payload),
2304                Segment::BackingAudio { offset, len } => {
2305                    let s = usize_from(*offset);
2306                    served.extend_from_slice(&buf[s..s + usize_from(*len)]);
2307                }
2308                other => panic!("unexpected segment: {other:?}"),
2309            }
2310        }
2311        read_structure(&served).expect("synthesized file re-parses to a valid moov/mdat");
2312        // `read_binary_tags` returns a bare Vec (no promotion for MP4) and emits the
2313        // raw `mean:name` key WITHOUT folding through the vocabulary — `com.serato.dj`
2314        // is not in any vocabulary entry, so the key is preserved verbatim.
2315        let reparsed = read_binary_tags(&served, usize::MAX);
2316        assert_eq!(reparsed.len(), 1);
2317        assert_eq!(reparsed[0].key, "----:com.serato.dj:analysis");
2318        assert_eq!(reparsed[0].payload, payload);
2319    }
2320
2321    #[test]
2322    fn synthesize_new_moov_size_exactly_u32_max_is_ok() {
2323        // `if new_moov_size > u32::MAX` is strict. new_moov_size == u32::MAX must be
2324        // accepted; `> -> ==`/`>= ` reject the exact boundary. data_len (the art size)
2325        // is reserved as a number, so the boundary is cheap.
2326        fn art(data_len: u64) -> ArtInput {
2327            ArtInput {
2328                art_id: 1,
2329                mime: "image/jpeg".into(),
2330                description: String::new(),
2331                picture_type: PictureType::new(3).unwrap(),
2332                width: 0,
2333                height: 0,
2334                data_len: BlobLen::new(data_len).unwrap(),
2335            }
2336        }
2337        let buf = mk_mp4(true, b"AUDIO", &[0]);
2338        let scan = read_structure(&buf).unwrap();
2339        let tags = [TagInput::new("title", "T")];
2340
2341        // Synthesize once with a 1-byte art. The head is [ftyp][moov], where the moov
2342        // box header declares new_moov_size = overhead + 1. The actual moov bytes in the
2343        // head are new_moov_size - 1 (the art is a separate ArtImage segment). So the
2344        // head length = ftyp.len() + (overhead + 1 - 1) = ftyp.len() + overhead.
2345        let layout1 = synthesize_layout(&scan, &tags, &[], &[art(1)]).unwrap();
2346        let head_len = inline_head(&layout1).len();
2347        let overhead = (head_len as u64) - (scan.ftyp.len() as u64);
2348        let max_len = u64::from(u32::MAX) - overhead;
2349
2350        assert!(max_len > 0, "overhead {overhead} must be < u32::MAX");
2351        // Boundary accepted
2352        assert!(synthesize_layout(&scan, &tags, &[], &[art(max_len)]).is_ok());
2353        // Boundary+1 rejected
2354        assert!(matches!(
2355            synthesize_layout(&scan, &tags, &[], &[art(max_len + 1)]),
2356            Err(FormatError::TooLarge)
2357        ));
2358    }
2359
2360    #[test]
2361    fn synthesize_layout_emits_all_nonzero_arts() {
2362        // Both non-empty arts stream, in input order.
2363        let art = |id: i64, len: u64| ArtInput {
2364            art_id: id,
2365            mime: "image/jpeg".into(),
2366            description: String::new(),
2367            picture_type: PictureType::new(3).unwrap(),
2368            width: 0,
2369            height: 0,
2370            data_len: BlobLen::new(len).unwrap(),
2371        };
2372        let buf = mk_mp4(true, b"AUDIO", &[0]);
2373        let scan = read_structure(&buf).unwrap();
2374        let layout = synthesize_layout(
2375            &scan,
2376            &[TagInput::new("title", "T")],
2377            &[],
2378            &[art(1, 5), art(3, 7)],
2379        )
2380        .unwrap();
2381        let art_segs: Vec<(i64, u64)> = layout
2382            .segments()
2383            .iter()
2384            .filter_map(|s| match s {
2385                Segment::ArtImage { art_id, len } => Some((*art_id, len.get())),
2386                _ => None,
2387            })
2388            .collect();
2389        assert_eq!(art_segs, vec![(1, 5), (3, 7)]);
2390    }
2391
2392    #[test]
2393    fn read_structure_from_rejects_oversized_moov() {
2394        use std::io::Cursor;
2395        let moov_size: u32 = 600 * 1024 * 1024;
2396        let mut buf = Vec::new();
2397        buf.extend_from_slice(&16u32.to_be_bytes());
2398        buf.extend_from_slice(b"ftyp");
2399        buf.extend_from_slice(&[0u8; 8]);
2400        buf.extend_from_slice(&16u32.to_be_bytes());
2401        buf.extend_from_slice(b"mdat");
2402        buf.extend_from_slice(&[0u8; 8]);
2403        buf.extend_from_slice(&moov_size.to_be_bytes());
2404        buf.extend_from_slice(b"moov");
2405        assert_eq!(buf.len(), 40);
2406        let file_len = 32 + u64::from(moov_size);
2407        let mut cur = Cursor::new(buf);
2408        match read_structure_from(&mut cur, file_len).unwrap_err() {
2409            Mp4ScanError::MetadataTooLarge {
2410                box_kind,
2411                size,
2412                cap,
2413            } => {
2414                assert_eq!(box_kind, "moov");
2415                assert_eq!(size, u64::from(moov_size));
2416                assert_eq!(cap, 256 * 1024 * 1024);
2417            }
2418            other => panic!("expected MetadataTooLarge, got {other:?}"),
2419        }
2420    }
2421
2422    #[test]
2423    fn read_structure_from_admits_box_at_exactly_the_cap() {
2424        use std::io::Cursor;
2425        let cap: u32 = 256 * 1024 * 1024;
2426        let mut buf = Vec::new();
2427        buf.extend_from_slice(&16u32.to_be_bytes());
2428        buf.extend_from_slice(b"ftyp");
2429        buf.extend_from_slice(&[0u8; 8]);
2430        buf.extend_from_slice(&16u32.to_be_bytes());
2431        buf.extend_from_slice(b"mdat");
2432        buf.extend_from_slice(&[0u8; 8]);
2433        buf.extend_from_slice(&cap.to_be_bytes());
2434        buf.extend_from_slice(b"moov");
2435        let file_len = 32 + u64::from(cap);
2436        let mut cur = Cursor::new(buf);
2437        let err = read_structure_from(&mut cur, file_len).unwrap_err();
2438        assert!(
2439            matches!(err, Mp4ScanError::Io(_)),
2440            "exact-cap box must pass the strict `>` guard (got {err:?})"
2441        );
2442    }
2443
2444    #[test]
2445    fn build_udta_checked_art_len_rejects_overflow() {
2446        // A hostile art data_len near u64::MAX must fail closed with TooLarge at
2447        // the covr_size fold, not panic (debug) / wrap (release).
2448        let mk = |data_len: u64| crate::input::ArtInput {
2449            art_id: 1,
2450            mime: "image/png".to_string(),
2451            description: String::new(),
2452            picture_type: PictureType::new(3).unwrap(),
2453            width: 0,
2454            height: 0,
2455            data_len: BlobLen::new(data_len).unwrap(),
2456        };
2457        assert_eq!(
2458            build_udta(&[], &[], &[mk(u64::MAX)]).err(),
2459            Some(FormatError::TooLarge)
2460        );
2461    }
2462
2463    #[test]
2464    fn build_udta_checked_binary_tag_len_rejects_overflow() {
2465        // A hostile freeform binary-tag len near u64::MAX must fail closed with
2466        // TooLarge inside freeform_binary_prefix's data_size/inner_len arithmetic,
2467        // not panic (debug) / wrap (release) before the u32 box-size narrowing.
2468        let bins = vec![crate::input::BinaryTagInput {
2469            key: "----:com.example:x".to_string(),
2470            payload_id: 1,
2471            len: BlobLen::new(u64::MAX).unwrap(),
2472        }];
2473        assert_eq!(
2474            build_udta(&[], &bins, &[]).err(),
2475            Some(FormatError::TooLarge)
2476        );
2477    }
2478
2479    #[test]
2480    fn freeform_binary_prefix_checked_outer_box_size_rejects_overflow() {
2481        // A payload_len that slips past the data_size and inner_len checks can
2482        // still overflow the outer `8 + inner_len` box-size add. With 1-char
2483        // mean/name each boxed mean/name is 13 bytes, so inner_len = 26 + data_size
2484        // = 42 + payload_len; payload_len = u64::MAX - 42 drives inner_len to exactly
2485        // u64::MAX, so the outer add must fail closed, not panic (debug) / wrap (release).
2486        assert_eq!(
2487            freeform_binary_prefix("m", "n", u64::MAX - 42).err(),
2488            Some(FormatError::TooLarge)
2489        );
2490    }
2491}