mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-28 20:47:15 -06:00
77 lines
3.3 KiB
Rust
77 lines
3.3 KiB
Rust
//! Checks an MP4's box tree before symphonia reads it. Symphonia's MP4 reader reserves memory
|
|
//! from the entry counts in the sample tables (and reads each sample whole at its `stsz` size)
|
|
//! without checking them against the file, so a damaged count asks for gigabytes and the process
|
|
//! aborts — an allocation failure cannot be caught. A file passes when every box fits inside its
|
|
//! parent, every table's entries fit inside its box and every sample fits inside the file.
|
|
|
|
/// Boxes whose payload is a sequence of boxes.
|
|
const CONTAINERS: [&[u8; 4]; 11] = [b"moov", b"trak", b"mdia", b"minf", b"stbl", b"edts", b"dinf", b"udta", b"mvex", b"moof", b"traf"];
|
|
|
|
/// Whether symphonia can be given `b` (an MP4) without a damaged size reaching an allocation.
|
|
pub(crate) fn tables_fit(b: &[u8]) -> bool {
|
|
boxes_fit(b, b.len() as u64, 0)
|
|
}
|
|
|
|
fn be32(b: &[u8], at: usize) -> Option<u64> {
|
|
b.get(at..at.checked_add(4)?).map(|s| u32::from_be_bytes([s[0], s[1], s[2], s[3]]) as u64)
|
|
}
|
|
|
|
fn boxes_fit(b: &[u8], file_len: u64, depth: u32) -> bool {
|
|
if depth > 16 {
|
|
return false;
|
|
}
|
|
let mut at = 0usize;
|
|
while b.len() - at >= 8 {
|
|
let Some(size32) = be32(b, at) else { return false };
|
|
let kind = &b[at + 4..at + 8];
|
|
let (header, size) = match size32 {
|
|
0 => (8, (b.len() - at) as u64),
|
|
1 => match b.get(at + 8..at + 16) {
|
|
Some(s) => (16, u64::from_be_bytes(s.try_into().unwrap_or_default())),
|
|
None => return false,
|
|
},
|
|
n => (8, n),
|
|
};
|
|
if size < header as u64 || size > (b.len() - at) as u64 {
|
|
// A box that runs past the end of the file is fine at the top level (the file was cut
|
|
// short; symphonia stops there), but never inside a parent.
|
|
return depth == 0 && !matches!(kind, b"moov" | b"moof");
|
|
}
|
|
let body = &b[at + header..at + size as usize];
|
|
let ok = if CONTAINERS.iter().any(|c| c.as_slice() == kind) {
|
|
boxes_fit(body, file_len, depth + 1)
|
|
} else if kind == b"meta" {
|
|
// ISO `meta` is a full box (4 bytes of version and flags first); QuickTime's is not.
|
|
let skip = if be32(body, 0) == Some(0) { 4 } else { 0 };
|
|
boxes_fit(&body[skip.min(body.len())..], file_len, depth + 1)
|
|
} else {
|
|
table_fits(kind, body, file_len)
|
|
};
|
|
if !ok {
|
|
return false;
|
|
}
|
|
at += size as usize;
|
|
}
|
|
true
|
|
}
|
|
|
|
/// A sample table (a full box: version and flags, then a count and that many entries).
|
|
fn table_fits(kind: &[u8], body: &[u8], file_len: u64) -> bool {
|
|
let entry = match kind {
|
|
b"stts" | b"ctts" | b"co64" => 8,
|
|
b"stsc" => 12,
|
|
b"stco" | b"stss" => 4,
|
|
b"stsz" => {
|
|
let (Some(size), Some(count)) = (be32(body, 4), be32(body, 8)) else { return false };
|
|
if size != 0 {
|
|
return size <= file_len;
|
|
}
|
|
let Some(entries) = body.get(12..) else { return false };
|
|
return count <= entries.len() as u64 / 4
|
|
&& entries.as_chunks::<4>().0.iter().take(count as usize).all(|s| u32::from_be_bytes(*s) as u64 <= file_len);
|
|
}
|
|
_ => return true,
|
|
};
|
|
be32(body, 4).is_some_and(|count| count <= body.len().saturating_sub(8) as u64 / entry)
|
|
}
|