Pin read_moov's box-size boundaries and MAX_ALLOC_BYTES exactly

read_moov's forward-progress guard (box_size < header_len, OR'd with the
EOF check) and the MAX_ALLOC_BYTES cap were only exercised on inputs well
away from their boundaries, so a mutation testing pass found the exact
edges unasserted: a size-8 (header-only) moov, a box that overruns the
file by exactly the amount the OR/AND distinction can see, and a payload
of precisely MAX_ALLOC_BYTES. Added a shared FakeBigReader (lifted out of
an existing test's local struct so a new test can reuse it) to exercise
the MAX_ALLOC_BYTES boundary without a multi-hundred-MiB backing file.
This commit is contained in:
Matthew Jackson
2026-08-01 14:39:39 -07:00
parent 8189da1b0c
commit f8ed0b99f4
+102
View File
@@ -998,6 +998,44 @@ fn parse_stss(b: &[u8]) -> std::collections::HashSet<u32> {
mod tests { mod tests {
use super::*; use super::*;
/// A `Read + Seek` backed by a small crafted prefix followed by an endless
/// run of zeros, reporting `len` bytes total on `seek(End)`. Lets a test
/// exercise a hundred-MiB-to-multi-GiB boundary (`MAX_ALLOC_BYTES`, a
/// sparse-file-inflated `file_len`) without a real backing allocation of
/// that size for the SOURCE side — only the destination buffer the code
/// under test allocates is real, which is the point of the test.
struct FakeBigReader {
prefix: Vec<u8>,
pos: u64,
len: u64,
}
impl Read for FakeBigReader {
fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
let remaining = self.len.saturating_sub(self.pos);
let n = (out.len() as u64).min(remaining) as usize;
for (i, byte) in out[..n].iter_mut().enumerate() {
let idx = self.pos + i as u64;
*byte = if idx < self.prefix.len() as u64 {
self.prefix[idx as usize]
} else {
0 // endless zero fill past the crafted prefix
};
}
self.pos += n as u64;
Ok(n)
}
}
impl Seek for FakeBigReader {
fn seek(&mut self, from: SeekFrom) -> io::Result<u64> {
self.pos = match from {
SeekFrom::Start(p) => p,
SeekFrom::End(off) => (self.len as i64 + off) as u64,
SeekFrom::Current(off) => (self.pos as i64 + off) as u64,
};
Ok(self.pos)
}
}
#[test] #[test]
fn stsc_offsets_one_sample_per_chunk() { fn stsc_offsets_one_sample_per_chunk() {
// Our writer's layout: 1 sample/chunk, co64 lists every offset. // Our writer's layout: 1 sample/chunk, co64 lists every offset.
@@ -1827,6 +1865,70 @@ mod tests {
assert!(read_moov(&mut Cursor::new(b)).is_err()); assert!(read_moov(&mut Cursor::new(b)).is_err());
} }
/// `box_size < header_len` rejects a box that cannot even hold its own
/// header. A box whose size is EXACTLY `header_len` (8, no 64-bit
/// largesize) is the boundary itself: legal (an empty box), so an empty
/// `moov` — size 8, zero payload bytes — must parse to `Ok(vec![])`, not
/// be rejected by a `<`→`<=`/`==` mutant that also rejects the boundary.
#[test]
fn read_moov_exactly_header_sized_is_a_valid_empty_box() {
use std::io::Cursor;
let mut b = Vec::new();
b.extend_from_slice(&8u32.to_be_bytes());
b.extend_from_slice(b"moov");
assert_eq!(
read_moov(&mut Cursor::new(b)).unwrap(),
Vec::<u8>::new(),
"a size-8 moov has an 8-byte header and zero payload bytes"
);
}
/// The forward-progress / EOF guard is `box_size < header_len ||
/// pos.checked_add(box_size).is_none_or(|end| end > file_end)` — an `||`↔
/// `&&` mutant only shows up on an input where EXACTLY ONE side is true.
/// A box that claims to run 992 bytes past a file that ends right after
/// its header satisfies only the second clause (`box_size >= header_len`,
/// so the first is false); under `&&` the guard would not fire, and
/// `read_moov` would instead fail later inside `read_exact` with a plain
/// `UnexpectedEof` — a different, uncoded `io::Error` rather than this
/// crate's `Mp4Invalid` (`ErrorKind::InvalidData`), which the guard exists
/// to produce uniformly for every malformed box.
#[test]
fn read_moov_rejects_a_box_that_overruns_the_file_via_the_or_not_and_guard() {
use std::io::Cursor;
let mut b = Vec::new();
b.extend_from_slice(&1000u32.to_be_bytes()); // claims 1000 bytes total
b.extend_from_slice(b"moov");
// No more bytes: the file ends right after the 8-byte header, 992
// bytes short of the claim.
let err = read_moov(&mut Cursor::new(b)).unwrap_err();
assert_eq!(
err.kind(),
std::io::ErrorKind::InvalidData,
"an overrunning box must be rejected by the guard itself (Mp4Invalid), \
not fall through to a bare EOF error from read_exact"
);
}
/// `payload_len > MAX_ALLOC_BYTES` is a SEPARATE, sparse-file-proof cap from
/// the EOF check above (see `read_moov_over_cap_rejected_despite_inflated_file_len`).
/// A `>`→`>=` mutant would reject a `moov` whose payload is EXACTLY
/// `MAX_ALLOC_BYTES`, which must be allowed.
#[test]
fn read_moov_allows_a_payload_of_exactly_max_alloc_bytes() {
let box_size = MAX_ALLOC_BYTES as u32 + 8; // header + exactly the cap
let mut prefix = Vec::new();
prefix.extend_from_slice(&box_size.to_be_bytes());
prefix.extend_from_slice(b"moov");
let mut rd = FakeBigReader {
prefix,
pos: 0,
len: MAX_ALLOC_BYTES + 1024, // real enough bytes to satisfy read_exact
};
let moov = read_moov(&mut rd).expect("exactly MAX_ALLOC_BYTES must be allowed");
assert_eq!(moov.len() as u64, MAX_ALLOC_BYTES);
}
/// Wrap `payload` in an ISO-BMFF box with the given 4-byte type. /// Wrap `payload` in an ISO-BMFF box with the given 4-byte type.
fn mp4_box(typ: &[u8; 4], payload: &[u8]) -> Vec<u8> { fn mp4_box(typ: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let size = (payload.len() + 8) as u32; let size = (payload.len() + 8) as u32;