use crate::bytes::{read_u32_be, read_u64_be};
use crate::convert::usize_from;
use crate::error::{FormatError, Result};
use crate::input::{
ArtInput, BinaryTagInput, EmbeddedBinaryTag, EmbeddedPicture, PictureType, TagInput,
};
use crate::layout::{RegionLayout, Segment};
use crate::size;
use std::io::{self, Read, Seek, SeekFrom};
const MAX_MP4_METADATA_BYTES: u64 = 256 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct BoxRef {
kind: [u8; 4],
start: usize,
header_len: usize, total_len: usize, }
impl BoxRef {
fn payload_start(&self) -> usize {
self.start + self.header_len
}
fn end(&self) -> usize {
self.start + self.total_len
}
fn payload<'a>(&self, buf: &'a [u8]) -> &'a [u8] {
debug_assert!(
self.end() <= buf.len(),
"BoxRef::payload called with a buffer it was not parsed from"
);
&buf[self.payload_start()..self.end()]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BoxHeader {
pub kind: [u8; 4],
pub header_len: u64,
pub total_len: u64,
}
pub fn box_header(hdr: &[u8], remaining: u64) -> Result<BoxHeader> {
let size32 = u64::from(read_u32_be(hdr, 0)?);
let kind: [u8; 4] = hdr
.get(4..8)
.ok_or(FormatError::Malformed)?
.try_into()
.unwrap();
let (header_len, total_len) = match size32 {
1 => (16u64, read_u64_be(hdr, 8)?),
0 => (8u64, remaining),
n => (8u64, n),
};
if total_len < header_len || total_len > remaining {
return Err(FormatError::Malformed);
}
Ok(BoxHeader {
kind,
header_len,
total_len,
})
}
#[derive(Debug, thiserror::Error)]
pub enum Mp4ScanError {
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Format(#[from] FormatError),
#[error("MP4 {box_kind} box is {size} bytes, exceeds the {cap}-byte metadata cap")]
MetadataTooLarge {
box_kind: &'static str,
size: u64,
cap: u64,
},
}
fn read_box(buf: &[u8], pos: usize) -> Result<BoxRef> {
let size32 = u64::from(read_u32_be(buf, pos)?);
let kind: [u8; 4] = buf
.get(pos + 4..pos + 8)
.ok_or(FormatError::Malformed)?
.try_into()
.unwrap();
let (header_len, total) = match size32 {
1 => (16usize, read_u64_be(buf, pos + 8)?),
0 => (8usize, (buf.len() - pos) as u64),
n => (8usize, n),
};
let total = usize_from(total);
let Some(end) = pos.checked_add(total) else {
return Err(FormatError::Malformed);
};
if total < header_len || end > buf.len() {
return Err(FormatError::Malformed);
}
Ok(BoxRef {
kind,
start: pos,
header_len,
total_len: total,
})
}
fn child_boxes(buf: &[u8]) -> Result<Vec<BoxRef>> {
let mut out = Vec::new();
let mut pos = 0;
while pos + 8 <= buf.len() {
let b = read_box(buf, pos)?;
pos = b.end();
out.push(b);
}
Ok(out)
}
fn child_boxes_lenient(buf: &[u8]) -> Vec<BoxRef> {
let mut out = Vec::new();
let mut pos = 0;
while pos + 8 <= buf.len() {
let Ok(b) = read_box(buf, pos) else { break };
pos = b.end();
out.push(b);
}
out
}
fn find_box(buf: &[u8], kind: &[u8; 4]) -> Result<Option<BoxRef>> {
Ok(child_boxes(buf)?.into_iter().find(|b| &b.kind == kind))
}
fn find_box_lenient(buf: &[u8], kind: &[u8; 4]) -> Option<BoxRef> {
child_boxes_lenient(buf)
.into_iter()
.find(|b| &b.kind == kind)
}
fn find_path(buf: &[u8], path: &[&[u8; 4]]) -> Result<Option<(usize, usize)>> {
let mut base = 0usize;
let mut last = None;
for kind in path {
let region = &buf[base..];
let Some(b) = find_box(region, kind)? else {
return Ok(None);
};
let ps = base + b.payload_start();
last = Some((ps, b.total_len - b.header_len));
base = ps;
}
Ok(last)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Mp4Bounds {
pub audio_offset: u64,
pub audio_length: u64,
}
fn validate_moov(moov_payload: &[u8]) -> Result<()> {
if find_box(moov_payload, b"mvex")?.is_some() {
return Err(FormatError::NotMp4);
}
let traks: Vec<_> = child_boxes(moov_payload)?
.into_iter()
.filter(|b| &b.kind == b"trak")
.collect();
if traks.len() != 1 {
return Err(FormatError::NotMp4);
}
let trak = traks[0].payload(moov_payload);
let (hp, hl) = find_path(trak, &[b"mdia", b"hdlr"])?.ok_or(FormatError::NotMp4)?;
if trak[hp..hp + hl].get(8..12) != Some(b"soun") {
return Err(FormatError::NotMp4);
}
Ok(())
}
fn locate(buf: &[u8]) -> Result<(BoxRef, BoxRef, BoxRef)> {
let top = child_boxes(buf).map_err(|_| FormatError::NotMp4)?;
if top.iter().any(|b| &b.kind == b"moof") {
return Err(FormatError::NotMp4);
}
let one = |kind: &[u8; 4]| -> Result<BoxRef> {
let mut it = top.iter().filter(|b| &b.kind == kind);
let first = it.next().copied().ok_or(FormatError::NotMp4)?;
if it.next().is_some() {
return Err(FormatError::NotMp4);
}
Ok(first)
};
let ftyp = one(b"ftyp")?;
let moov = one(b"moov")?;
let mdat = one(b"mdat")?;
validate_moov(moov.payload(buf))?;
Ok((ftyp, moov, mdat))
}
pub fn locate_audio(buf: &[u8]) -> Result<Mp4Bounds> {
let (_ftyp, _moov, mdat) = locate(buf)?;
Ok(Mp4Bounds {
audio_offset: mdat.payload_start() as u64,
audio_length: (mdat.total_len - mdat.header_len) as u64,
})
}
#[derive(Debug, Clone, PartialEq)]
pub struct Mp4Scan {
pub ftyp: Vec<u8>,
pub moov: Vec<u8>,
pub mdat_header: Vec<u8>,
pub mdat_payload_offset: u64,
pub mdat_payload_len: u64,
}
pub fn read_structure(buf: &[u8]) -> Result<Mp4Scan> {
let (ftyp, moov, mdat) = locate(buf)?;
Ok(Mp4Scan {
ftyp: buf[ftyp.start..ftyp.end()].to_vec(),
moov: buf[moov.start..moov.end()].to_vec(),
mdat_header: buf[mdat.start..mdat.payload_start()].to_vec(),
mdat_payload_offset: mdat.payload_start() as u64,
mdat_payload_len: (mdat.total_len - mdat.header_len) as u64,
})
}
pub fn read_structure_from<R: Read + Seek>(
r: &mut R,
file_len: u64,
) -> std::result::Result<Mp4Scan, Mp4ScanError> {
fn region<R: Read + Seek>(r: &mut R, off: u64, len: usize) -> io::Result<Vec<u8>> {
r.seek(SeekFrom::Start(off))?;
let mut buf = vec![0u8; len];
r.read_exact(&mut buf)?;
Ok(buf)
}
let mut ftyp: Option<(u64, BoxHeader)> = None;
let mut moov: Option<(u64, BoxHeader)> = None;
let mut mdat: Option<(u64, BoxHeader)> = None;
let mut dup = false;
let mut pos = 0u64;
while pos + 8 <= file_len {
let first8 = region(r, pos, 8)?;
let size32 = u32::from_be_bytes(first8[0..4].try_into().unwrap());
let hdr = if size32 == 1 {
let mut h = first8;
h.extend_from_slice(®ion(r, pos + 8, 8)?);
h
} else {
first8
};
let bh = box_header(&hdr, file_len - pos)?;
let total = bh.total_len;
match &bh.kind {
b"moof" => return Err(FormatError::NotMp4.into()),
b"ftyp" => dup |= ftyp.replace((pos, bh)).is_some(),
b"moov" => dup |= moov.replace((pos, bh)).is_some(),
b"mdat" => dup |= mdat.replace((pos, bh)).is_some(),
_ => {}
}
pos += total;
}
if dup {
return Err(FormatError::NotMp4.into());
}
let (ftyp_s, ftyp_h) = ftyp.ok_or(FormatError::NotMp4)?;
let (moov_s, moov_h) = moov.ok_or(FormatError::NotMp4)?;
let (mdat_s, mdat_h) = mdat.ok_or(FormatError::NotMp4)?;
for (box_kind, total_len) in [("ftyp", ftyp_h.total_len), ("moov", moov_h.total_len)] {
if total_len > MAX_MP4_METADATA_BYTES {
return Err(Mp4ScanError::MetadataTooLarge {
box_kind,
size: total_len,
cap: MAX_MP4_METADATA_BYTES,
});
}
}
let ftyp_len = usize::try_from(ftyp_h.total_len).map_err(|_| FormatError::Malformed)?;
let moov_len = usize::try_from(moov_h.total_len).map_err(|_| FormatError::Malformed)?;
let ftyp_bytes = region(r, ftyp_s, ftyp_len)?;
let moov_bytes = region(r, moov_s, moov_len)?;
let mdat_header = region(r, mdat_s, usize_from(mdat_h.header_len))?;
validate_moov(&moov_bytes[usize_from(moov_h.header_len)..])?;
Ok(Mp4Scan {
ftyp: ftyp_bytes,
moov: moov_bytes,
mdat_header,
mdat_payload_offset: mdat_s + mdat_h.header_len,
mdat_payload_len: mdat_h.total_len - mdat_h.header_len,
})
}
fn ilst_region(buf: &[u8]) -> Option<(usize, usize)> {
let moov = find_box_lenient(buf, b"moov")?;
let mp = moov.payload(buf);
let base = moov.payload_start();
let udta = find_box_lenient(mp, b"udta")?;
let up = udta.payload_start();
let udta_payload = udta.payload(mp);
let meta = find_box_lenient(udta_payload, b"meta")?;
let meta_payload = meta.payload(udta_payload);
let prefix = if meta_payload.get(..4) == Some(&[0, 0, 0, 0][..]) {
4
} else {
0
};
let meta_children = meta_payload.get(prefix..)?;
let il = find_box_lenient(meta_children, b"ilst")?;
let start = base + up + meta.payload_start() + prefix + il.payload_start();
Some((start, il.total_len - il.header_len))
}
fn read_freeform(inner: &[u8]) -> Vec<(String, String)> {
let Some(name_box) = find_box_lenient(inner, b"name") else {
return Vec::new();
};
let np = name_box.payload(inner);
if np.len() < 4 {
return Vec::new();
}
let Ok(name) = std::str::from_utf8(&np[4..]) else {
return Vec::new();
};
let mean = find_box_lenient(inner, b"mean").map_or("com.apple.iTunes", |m| {
let p = m.payload(inner);
if p.len() >= 4 {
std::str::from_utf8(&p[4..]).unwrap_or("com.apple.iTunes")
} else {
"com.apple.iTunes"
}
});
let key = crate::tagmap::mp4_freeform_to_key(mean, name)
.map_or_else(|| name.to_string(), str::to_string);
let mut out = Vec::new();
for data in child_boxes_lenient(inner) {
if &data.kind != b"data" {
continue;
}
let dp = data.payload(inner);
if dp.len() < 8 {
continue;
}
let type_code = u32::from_be_bytes([dp[0], dp[1], dp[2], dp[3]]);
if type_code != 1 {
continue;
}
if let Ok(value) = std::str::from_utf8(&dp[8..]) {
out.push((key.clone(), value.to_string()));
}
}
out
}
fn number_total(value: &[u8]) -> String {
debug_assert!(
value.len() >= 4,
"number_total requires the 4-byte number prefix"
);
let number = u16::from_be_bytes([value[2], value[3]]);
let total = if value.len() >= 6 {
u16::from_be_bytes([value[4], value[5]])
} else {
0
};
if total != 0 {
format!("{number}/{total}")
} else {
number.to_string()
}
}
pub fn read_tags(buf: &[u8]) -> Vec<(String, String)> {
let Some((start, len)) = ilst_region(buf) else {
return Vec::new();
};
let ilst = &buf[start..start + len];
let mut out = Vec::new();
for atom in child_boxes_lenient(ilst) {
let inner = atom.payload(ilst);
if &atom.kind == b"----" {
out.extend(read_freeform(inner));
continue;
}
let text_key = crate::tagmap::mp4_atom_to_key(&atom.kind);
for data in child_boxes_lenient(inner) {
if &data.kind != b"data" {
continue;
}
let dp = data.payload(inner);
if dp.len() < 8 {
continue;
}
let value = &dp[8..]; if let Some(key) = text_key {
if let Ok(s) = std::str::from_utf8(value) {
out.push((key.to_string(), s.to_string()));
}
} else if &atom.kind == b"trkn" && value.len() >= 4 {
out.push(("tracknumber".into(), number_total(value)));
} else if &atom.kind == b"disk" && value.len() >= 4 {
out.push(("discnumber".into(), number_total(value)));
} else if let Some(key) = crate::tagmap::mp4_integer_atom_to_key(&atom.kind) {
let mut n: u64 = 0;
for &b in value.iter().take(8) {
n = (n << 8) | u64::from(b);
}
out.push((key.to_string(), n.to_string()));
}
}
}
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OversizeDrop {
pub descriptor: String,
pub bytes: usize,
}
pub fn read_pictures_reporting(
buf: &[u8],
max_art_bytes: usize,
) -> (Vec<EmbeddedPicture>, Vec<OversizeDrop>) {
let Some((start, len)) = ilst_region(buf) else {
return (Vec::new(), Vec::new());
};
let ilst = &buf[start..start + len];
let mut out = Vec::new();
let mut dropped = Vec::new();
for atom in child_boxes_lenient(ilst) {
if &atom.kind != b"covr" {
continue;
}
let inner = atom.payload(ilst);
for data in child_boxes_lenient(inner) {
if &data.kind != b"data" {
continue;
}
let dp = data.payload(inner);
if dp.len() < 8 {
continue;
}
let mime = match u32::from_be_bytes([dp[0], dp[1], dp[2], dp[3]]) {
13 => "image/jpeg",
14 => "image/png",
_ => continue,
};
if dp.len() - 8 > max_art_bytes {
dropped.push(OversizeDrop {
descriptor: mime.to_string(),
bytes: dp.len() - 8,
});
continue;
}
out.push(EmbeddedPicture {
mime: mime.to_string(),
picture_type: PictureType::new(3).expect("3 is in range"),
description: String::new(),
width: 0,
height: 0,
data: dp[8..].to_vec(),
});
}
}
(out, dropped)
}
pub fn read_pictures(buf: &[u8], max_art_bytes: usize) -> Vec<EmbeddedPicture> {
read_pictures_reporting(buf, max_art_bytes).0
}
pub fn read_binary_tags_reporting(
buf: &[u8],
max_binary_tag_bytes: usize,
) -> (Vec<EmbeddedBinaryTag>, Vec<OversizeDrop>) {
let Some((start, len)) = ilst_region(buf) else {
return (Vec::new(), Vec::new());
};
let ilst = &buf[start..start + len];
let mut out = Vec::new();
let mut dropped = Vec::new();
for atom in child_boxes_lenient(ilst) {
if &atom.kind != b"----" {
continue;
}
let inner = atom.payload(ilst);
let Some(name) = find_box_lenient(inner, b"name").and_then(|n| {
let p = n.payload(inner);
(p.len() >= 4)
.then(|| std::str::from_utf8(&p[4..]).ok())
.flatten()
}) else {
continue;
};
let mean = find_box_lenient(inner, b"mean").map_or("com.apple.iTunes", |m| {
let p = m.payload(inner);
if p.len() >= 4 {
std::str::from_utf8(&p[4..]).unwrap_or("com.apple.iTunes")
} else {
"com.apple.iTunes"
}
});
let key = format!("----:{mean}:{name}");
for data in child_boxes_lenient(inner) {
if &data.kind != b"data" {
continue;
}
let dp = data.payload(inner);
if dp.len() < 8 {
continue;
}
let type_code = u32::from_be_bytes([dp[0], dp[1], dp[2], dp[3]]);
if type_code == 1 {
continue;
}
if dp.len() - 8 > max_binary_tag_bytes {
dropped.push(OversizeDrop {
descriptor: key.clone(),
bytes: dp.len() - 8,
});
continue;
}
out.push(EmbeddedBinaryTag {
key: key.clone(),
payload: dp[8..].to_vec(),
});
}
}
(out, dropped)
}
pub fn read_binary_tags(buf: &[u8], max_binary_tag_bytes: usize) -> Vec<EmbeddedBinaryTag> {
read_binary_tags_reporting(buf, max_binary_tag_bytes).0
}
mod synth;
pub use synth::synthesize_layout;
#[cfg(test)]
mod tests;