use std::io::{Read, Seek, SeekFrom};
use oxideav_core::{Error, Result};
#[derive(Clone, Copy, Debug)]
pub struct BoxHeader {
pub fourcc: [u8; 4],
pub total_size: Option<u64>,
pub header_len: u64,
}
impl BoxHeader {
pub fn type_str(&self) -> &str {
std::str::from_utf8(&self.fourcc).unwrap_or("????")
}
pub fn payload_size(&self) -> Option<u64> {
self.total_size.map(|t| t - self.header_len)
}
}
pub fn read_box_header<R: Read + Seek + ?Sized>(r: &mut R) -> Result<Option<BoxHeader>> {
let start = r.stream_position()?;
let mut hdr = [0u8; 8];
let mut got = 0;
while got < 8 {
match r.read(&mut hdr[got..]) {
Ok(0) => {
if got == 0 {
return Ok(None);
} else {
return Err(Error::invalid("MP4: truncated box header"));
}
}
Ok(n) => got += n,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e.into()),
}
}
let size32 = u32::from_be_bytes([hdr[0], hdr[1], hdr[2], hdr[3]]);
let mut fourcc = [0u8; 4];
fourcc.copy_from_slice(&hdr[4..8]);
let (total_size, header_len) = match size32 {
0 => (None, 8u64),
1 => {
let mut ext = [0u8; 8];
r.read_exact(&mut ext)?;
let large = u64::from_be_bytes(ext);
if large < 16 {
return Err(Error::invalid("MP4: box largesize < 16"));
}
(Some(large), 16u64)
}
n if n < 8 => {
return Err(Error::invalid("MP4: box size < 8"));
}
n => (Some(n as u64), 8u64),
};
if let Some(t) = total_size {
if start.checked_add(t).is_none() {
return Err(Error::invalid(format!(
"MP4: box '{}' declared size {t} from offset {start} overflows u64",
std::str::from_utf8(&fourcc).unwrap_or("????"),
)));
}
}
Ok(Some(BoxHeader {
fourcc,
total_size,
header_len,
}))
}
pub fn read_box_body<R: Read + ?Sized>(r: &mut R, h: &BoxHeader) -> Result<Vec<u8>> {
let payload = h
.payload_size()
.ok_or_else(|| Error::invalid("MP4: cannot read open-ended box body"))?;
let mut buf = Vec::new();
r.take(payload).read_to_end(&mut buf)?;
if buf.len() as u64 != payload {
return Err(Error::invalid("MP4: truncated box body"));
}
Ok(buf)
}
pub fn skip_box_body<R: Seek + ?Sized>(r: &mut R, h: &BoxHeader) -> Result<()> {
if let Some(payload) = h.payload_size() {
if payload > 0 {
r.seek(SeekFrom::Current(payload as i64))?;
}
} else {
r.seek(SeekFrom::End(0))?;
}
Ok(())
}
pub const fn fourcc(s: &str) -> [u8; 4] {
let b = s.as_bytes();
[b[0], b[1], b[2], b[3]]
}
pub const FTYP: [u8; 4] = fourcc("ftyp");
pub const MOOV: [u8; 4] = fourcc("moov");
pub const MVHD: [u8; 4] = fourcc("mvhd");
pub const TRAK: [u8; 4] = fourcc("trak");
pub const TKHD: [u8; 4] = fourcc("tkhd");
pub const TREF: [u8; 4] = fourcc("tref");
pub const EDTS: [u8; 4] = fourcc("edts");
pub const MDIA: [u8; 4] = fourcc("mdia");
pub const MDHD: [u8; 4] = fourcc("mdhd");
pub const ELNG: [u8; 4] = fourcc("elng");
pub const HDLR: [u8; 4] = fourcc("hdlr");
pub const MINF: [u8; 4] = fourcc("minf");
pub const DINF: [u8; 4] = fourcc("dinf");
pub const STBL: [u8; 4] = fourcc("stbl");
pub const STSD: [u8; 4] = fourcc("stsd");
pub const STTS: [u8; 4] = fourcc("stts");
pub const STSS: [u8; 4] = fourcc("stss");
pub const STSC: [u8; 4] = fourcc("stsc");
pub const STSZ: [u8; 4] = fourcc("stsz");
pub const STZ2: [u8; 4] = fourcc("stz2");
pub const STCO: [u8; 4] = fourcc("stco");
pub const STSH: [u8; 4] = fourcc("stsh");
pub const SDTP: [u8; 4] = fourcc("sdtp");
pub const CTTS: [u8; 4] = fourcc("ctts");
pub const CSLG: [u8; 4] = fourcc("cslg");
pub const CO64: [u8; 4] = fourcc("co64");
pub const SBGP: [u8; 4] = fourcc("sbgp");
pub const SGPD: [u8; 4] = fourcc("sgpd");
pub const SUBS: [u8; 4] = fourcc("subs");
pub const ELST: [u8; 4] = fourcc("elst");
pub const MDAT: [u8; 4] = fourcc("mdat");
pub const FREE: [u8; 4] = fourcc("free");
pub const SKIP: [u8; 4] = fourcc("skip");
pub const UDTA: [u8; 4] = fourcc("udta");
pub const META: [u8; 4] = fourcc("meta");
pub const ILST: [u8; 4] = fourcc("ilst");
pub const DATA: [u8; 4] = fourcc("data");
pub const KIND: [u8; 4] = fourcc("kind");
pub const MVEX: [u8; 4] = fourcc("mvex");
pub const TREX: [u8; 4] = fourcc("trex");
pub const MOOF: [u8; 4] = fourcc("moof");
pub const MFHD: [u8; 4] = fourcc("mfhd");
pub const TRAF: [u8; 4] = fourcc("traf");
pub const TFHD: [u8; 4] = fourcc("tfhd");
pub const TFDT: [u8; 4] = fourcc("tfdt");
pub const TRUN: [u8; 4] = fourcc("trun");
pub const SIDX: [u8; 4] = fourcc("sidx");
pub const STYP: [u8; 4] = fourcc("styp");
pub const PRFT: [u8; 4] = fourcc("prft");
pub const MFRA: [u8; 4] = fourcc("mfra");
pub const TFRA: [u8; 4] = fourcc("tfra");
pub const MFRO: [u8; 4] = fourcc("mfro");
pub const HANDLER_SOUN: [u8; 4] = fourcc("soun");
pub const HANDLER_VIDE: [u8; 4] = fourcc("vide");
pub const HANDLER_SUBT: [u8; 4] = fourcc("subt");
pub const HANDLER_TEXT: [u8; 4] = fourcc("text");
pub const HANDLER_SBTL: [u8; 4] = fourcc("sbtl");
pub const HANDLER_META: [u8; 4] = fourcc("meta");
pub const SINF: [u8; 4] = fourcc("sinf");
pub const FRMA: [u8; 4] = fourcc("frma");
pub const SCHM: [u8; 4] = fourcc("schm");
pub const SCHI: [u8; 4] = fourcc("schi");
pub const ENCV: [u8; 4] = fourcc("encv");
pub const ENCA: [u8; 4] = fourcc("enca");
pub const ENCT: [u8; 4] = fourcc("enct");
pub const ENCS: [u8; 4] = fourcc("encs");
pub const TENC: [u8; 4] = fourcc("tenc");
pub const PSSH: [u8; 4] = fourcc("pssh");
pub const SENC: [u8; 4] = fourcc("senc");
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn box_size_below_eight_is_rejected_not_underflow() {
for bad in 2u32..=7 {
let mut buf = Vec::with_capacity(8);
buf.extend_from_slice(&bad.to_be_bytes());
buf.extend_from_slice(b"junk");
let err = read_box_header(&mut Cursor::new(buf)).expect_err("size < 8 must be invalid");
assert!(format!("{err}").contains("MP4"), "{err}");
}
}
#[test]
fn box_largesize_below_sixteen_is_rejected_not_underflow() {
for bad in 0u64..=15 {
let mut buf = Vec::with_capacity(16);
buf.extend_from_slice(&1u32.to_be_bytes());
buf.extend_from_slice(b"junk");
buf.extend_from_slice(&bad.to_be_bytes());
let err =
read_box_header(&mut Cursor::new(buf)).expect_err("largesize < 16 must be invalid");
assert!(format!("{err}").contains("MP4"), "{err}");
}
}
#[test]
fn box_largesize_overflowing_u64_from_nonzero_start_is_rejected() {
let mut buf = Vec::new();
buf.extend_from_slice(&8u32.to_be_bytes());
buf.extend_from_slice(b"free");
buf.extend_from_slice(&1u32.to_be_bytes());
buf.extend_from_slice(b"mdat");
buf.extend_from_slice(&u64::MAX.to_be_bytes());
let mut cur = Cursor::new(buf);
let h1 = read_box_header(&mut cur)
.expect("first box parses")
.expect("first box present");
assert_eq!(h1.total_size, Some(8));
let err =
read_box_header(&mut cur).expect_err("u64 overflow must be rejected at header read");
let msg = format!("{err}");
assert!(
msg.contains("overflow") && msg.contains("mdat"),
"expected u64-overflow rejection naming the box, got: {msg}"
);
}
#[test]
fn box_largesize_one_below_overflow_is_accepted() {
let mut buf = Vec::new();
buf.extend_from_slice(&1u32.to_be_bytes());
buf.extend_from_slice(b"mdat");
buf.extend_from_slice(&u64::MAX.to_be_bytes());
let mut cur = Cursor::new(buf);
let hdr = read_box_header(&mut cur)
.expect("header at start=0 with largesize=u64::MAX does not overflow")
.expect("a 16-byte header is present");
assert_eq!(hdr.fourcc, *b"mdat");
assert_eq!(hdr.total_size, Some(u64::MAX));
assert_eq!(hdr.header_len, 16);
}
#[test]
fn box_size_eight_is_a_valid_empty_box() {
let mut buf = Vec::with_capacity(8);
buf.extend_from_slice(&8u32.to_be_bytes());
buf.extend_from_slice(b"free");
let hdr = read_box_header(&mut Cursor::new(buf))
.unwrap()
.expect("size = 8 must parse");
assert_eq!(hdr.total_size, Some(8));
assert_eq!(hdr.header_len, 8);
assert_eq!(hdr.payload_size(), Some(0));
assert_eq!(&hdr.fourcc, b"free");
}
}