1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29struct BoxRef {
30 kind: [u8; 4],
31 start: usize,
32 header_len: usize, total_len: usize, }
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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub struct BoxHeader {
58 pub kind: [u8; 4],
60 pub header_len: u64,
62 pub total_len: u64,
64}
65
66pub 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#[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
150fn 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub struct Mp4Bounds {
170 pub audio_offset: u64,
171 pub audio_length: u64,
172}
173
174fn 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
196fn 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
219pub 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#[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
249pub 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 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 let first8 = region(r, pos, 8)?;
278 let size32 = u32::from_be_bytes(first8[0..4].try_into().unwrap());
279 let hdr = if size32 == 1 {
282 let mut h = first8;
283 h.extend_from_slice(®ion(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 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
336fn 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
351fn 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 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 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
387pub 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..]; 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
433pub 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
481pub 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 let type_code = u32::from_be_bytes([dp[0], dp[1], dp[2], dp[3]]);
516 if type_code == 1 {
517 continue;
518 }
519 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(); data.extend_from_slice(&0u32.to_be_bytes()); 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(); data.extend_from_slice(&0u32.to_be_bytes()); 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
580fn 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(); 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(); data.extend_from_slice(&0u32.to_be_bytes()); data.extend_from_slice(v.as_bytes());
596 inner.extend(boxed(b"data", &data)?);
597 }
598 boxed(b"----", &inner)
599}
600
601fn parse_freeform_key(key: &str) -> Option<(&str, &str)> {
604 key.strip_prefix("----:")?.split_once(':')
605}
606
607fn freeform_binary_prefix(mean: &str, name: &str, payload_len: u64) -> Result<Vec<u8>> {
613 let mut mean_body = 0u32.to_be_bytes().to_vec(); 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)?; 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()); out.extend_from_slice(&0u32.to_be_bytes()); Ok(out)
640}
641
642fn build_udta(
650 tags: &[TagInput],
651 binary_tags: &[BinaryTagInput],
652 arts: &[ArtInput],
653) -> Result<(Vec<Segment>, u64)> {
654 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 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 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 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())?; 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()); 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 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; let meta_size = size::checked_sum([8, meta_inline_len, streamed_total])?;
755 let udta_inline_len = 8 + meta_inline_len; let udta_size = size::checked_sum([8, udta_inline_len, streamed_total])?;
757
758 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 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()); header.extend_from_slice(&hdlr);
772 header.extend_from_slice(&ilst_size.to_be_bytes());
773 header.extend_from_slice(b"ilst");
774
775 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 if !lead.is_empty() {
793 segments.push(Segment::Inline(lead));
794 }
795 Ok((segments, streamed_total))
796}
797
798fn 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
830pub 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 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 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 let mut udta_iter = udta_segments.into_iter();
881 let Some(Segment::Inline(first)) = udta_iter.next() else {
882 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 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']; assert!(child_boxes(&buf).is_err());
949 }
950
951 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 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 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 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]); 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()); p.extend_from_slice(value);
1056 bx(b"data", &p)
1057 }
1058
1059 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]; 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 assert!(read_tags(&[]).is_empty());
1121 assert!(read_pictures(&[], usize::MAX).is_empty());
1122
1123 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let moov = find_moov_in_head(&head);
1356 assert_eq!(moov.end(), head.len() - scan.mdat_header.len());
1357 }
1358
1359 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 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 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(_))); assert!(matches!(segs.last().unwrap(), Segment::BackingAudio { .. }));
1434 }
1435
1436 #[test]
1437 fn synthesize_picks_first_nonempty_art() {
1438 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]); 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 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 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 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 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 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 assert_eq!(box_header(&[0u8; 4], 1000), Err(FormatError::Malformed));
1513 }
1514
1515 #[test]
1516 fn read_structure_from_matches_buffer_path() {
1517 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 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 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 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(); data.extend_from_slice(&0u32.to_be_bytes()); 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"); 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(); data.extend_from_slice(&0u32.to_be_bytes()); 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"); 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(); data.extend_from_slice(&0u32.to_be_bytes()); 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()); }
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"), TagInput::new("musicbrainz_albumid", "abc-123"), ];
1626 let (segs, _streamed) = build_udta(&tags, &[], &[]).unwrap();
1627 let udta = materialize_udta(&segs);
1628 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 let mut bytes = 1u32.to_be_bytes().to_vec(); bytes.extend_from_slice(b"moov");
1656 bytes.extend_from_slice(&u64::MAX.to_be_bytes()); 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 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(); v.extend_from_slice(b"mdat");
1671 v.extend_from_slice(&total.to_be_bytes()); v.extend_from_slice(payload);
1673 v
1674 }
1675 let normal = mk_mp4(true, &[0xABu8; 64], &[0]); 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(); let payload = normal[payload_start..].to_vec();
1680 let mut buf = normal[..mdat_box_start].to_vec(); 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); assert_eq!(from_stream.mdat_payload_len, payload.len() as u64);
1689 }
1690
1691 #[test]
1692 fn box_header_accepts_empty_payload_box() {
1693 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 let mut buf = bx(b"free", b""); buf.extend_from_slice(&0u32.to_be_bytes()); buf.extend_from_slice(b"mdat"); buf.extend_from_slice(b"AUDIOPAYLOAD"); 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); }
1717
1718 #[test]
1719 fn read_structure_from_rejects_box_overrunning_eof() {
1720 let mut buf = mk_mp4(true, b"AUDIO", &[0]); 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 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 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"); 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"); assert!(dup(bx(b"mdat", b"Y")), "duplicate mdat must reject"); }
1763
1764 #[test]
1765 fn read_freeform_accepts_minimal_name_and_data() {
1766 let name_body = 0u32.to_be_bytes().to_vec(); let mut data = 1u32.to_be_bytes().to_vec(); data.extend_from_slice(&0u32.to_be_bytes()); 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, ""); assert_eq!(value, "");
1777 }
1778
1779 #[test]
1780 fn read_freeform_short_name_returns_none() {
1781 let name_body = vec![0u8, 0, 0]; 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 let mean_body = vec![0u8, 0, 0, 0]; 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"); assert_eq!(value, "abc-123");
1809 }
1810
1811 #[test]
1812 fn read_tags_data_payload_exactly_8_is_read() {
1813 let atoms = bx(b"\xa9nam", &data_atom(1, b"")); 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 let atoms = bx(b"disk", &data_atom(0, &[0, 0, 0, 2])); 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 let atoms = bx(b"disk", &data_atom(0, &[0, 0])); 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 let atoms = bx(b"trkn", &data_atom(0, &[0, 0])); 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 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 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 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"), 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 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 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 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 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); assert_eq!(covr_size, 8 + data_size); }
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 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 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 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 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 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 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 let mut k = soun_trak();
2111 assert!(patch_chunk_offsets(&mut k, 0).is_ok()); let mut k = soun_trak();
2114 assert!(patch_chunk_offsets(&mut k, i64::from(u32::MAX)).is_ok()); let mut k = soun_trak();
2117 assert!(matches!(
2118 patch_chunk_offsets(&mut k, i64::from(u32::MAX) + 1), Err(FormatError::TooLarge)
2120 ));
2121
2122 let mut k = soun_trak();
2123 assert!(matches!(
2124 patch_chunk_offsets(&mut k, -1), Err(FormatError::TooLarge)
2126 ));
2127 }
2128
2129 #[test]
2130 fn patch_chunk_offsets_rejects_count_past_table() {
2131 let mut stco = vec![0u8; 4]; stco.extend_from_slice(&2u32.to_be_bytes()); stco.extend_from_slice(&0u32.to_be_bytes()); 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 let mut co64 = vec![0u8; 4]; co64.extend_from_slice(&1u32.to_be_bytes()); co64.extend_from_slice(&0u64.to_be_bytes()); 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 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()); 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 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(); 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 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 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()); let short = boxed(b"----", &short_inner).unwrap();
2226
2227 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 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 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 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 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 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 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 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 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 assert!(synthesize_layout(&scan, &tags, &[], &[art(max_len)]).is_ok());
2353 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 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 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 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 assert_eq!(
2487 freeform_binary_prefix("m", "n", u64::MAX - 42).err(),
2488 Some(FormatError::TooLarge)
2489 );
2490 }
2491}