#![allow(unsafe_code)]
use mp4_track::{Error, Mp4Reader, TrackId};
use serde_json::Value;
use std::alloc::{GlobalAlloc, Layout, System};
use std::fs::File;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc;
use std::time::Duration;
struct CountingAllocator;
static CURRENT: AtomicUsize = AtomicUsize::new(0);
static PEAK: AtomicUsize = AtomicUsize::new(0);
fn record_allocation(size: usize) {
let current = CURRENT
.fetch_add(size, Ordering::Relaxed)
.saturating_add(size);
PEAK.fetch_max(current, Ordering::Relaxed);
}
unsafe impl GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let pointer = unsafe { System.alloc(layout) };
if !pointer.is_null() {
record_allocation(layout.size());
}
pointer
}
unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
CURRENT.fetch_sub(layout.size(), Ordering::Relaxed);
unsafe { System.dealloc(pointer, layout) };
}
unsafe fn realloc(&self, pointer: *mut u8, old: Layout, new_size: usize) -> *mut u8 {
let resized = unsafe { System.realloc(pointer, old, new_size) };
if !resized.is_null() {
if new_size >= old.size() {
record_allocation(new_size - old.size());
} else {
CURRENT.fetch_sub(old.size() - new_size, Ordering::Relaxed);
}
}
resized
}
}
#[global_allocator]
static ALLOCATOR: CountingAllocator = CountingAllocator;
fn data() -> Value {
serde_json::from_slice(include_bytes!("data/conformance-vectors.json"))
.expect("conformance data must be valid JSON")
}
fn fixture_path(entry: &Value) -> PathBuf {
let name = Path::new(entry["file"].as_str().expect("file"))
.file_name()
.expect("file name");
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}
fn error_kind(error: &Error) -> &'static str {
match error {
Error::Truncated { .. } => "Truncated",
Error::BoxSizeTooSmall { .. } => "BoxSizeTooSmall",
Error::BoxExceedsFile { .. } => "BoxExceedsFile",
Error::ChildExceedsParent { .. } => "ChildExceedsParent",
Error::ZeroSizeNested { .. } => "ZeroSizeNested",
Error::NestingTooDeep { .. } => "NestingTooDeep",
Error::MissingBox { .. } => "MissingBox",
Error::Malformed { .. } => "Malformed",
Error::InvalidSampleTable { .. } => "InvalidSampleTable",
Error::InvalidTimescale { .. } => "InvalidTimescale",
Error::SampleOutOfFile { .. } => "SampleOutOfFile",
Error::LimitExceeded { .. } => "LimitExceeded",
_ => "Other",
}
}
fn run_case(path: PathBuf, id: String, stage: String) -> Result<String, String> {
let file = File::open(path).map_err(|error| error.to_string())?;
let outcome = std::panic::catch_unwind(|| {
let opened = Mp4Reader::open(file);
if stage == "open" {
return opened
.map(|_| "Ok".to_owned())
.map_err(|error| error_kind(&error).to_owned());
}
let mut reader = opened.map_err(|error| format!("early:{}", error_kind(&error)))?;
let (track, index) = match id.as_str() {
"m10_co64_offset_above_u32" | "m11_stsz_sum_overflows_u32" => (TrackId(1), 0),
"m16_truncated_mdat" => {
let track = reader
.tracks()
.last()
.ok_or_else(|| "missing track".to_owned())?;
(track.id, track.sample_count.saturating_sub(1))
}
_ => return Err("unknown sample-read fixture".to_owned()),
};
reader
.read_sample(track, index)
.map(|_| "Ok".to_owned())
.map_err(|error| error_kind(&error).to_owned())
});
match outcome {
Ok(Ok(value)) => Ok(value),
Ok(Err(kind)) => Ok(kind),
Err(_) => Err("panic".to_owned()),
}
}
#[test]
fn d4_every_malformed_fixture_is_typed_error_in_bounds() {
let vectors = data();
for entry in vectors["fixtures"]
.as_array()
.expect("fixtures")
.iter()
.filter(|entry| entry["kind"] == "malformed")
{
let id = entry["id"].as_str().expect("id").to_owned();
let stage = entry["expect"]["stage"].as_str().expect("stage").to_owned();
let expected = entry["expect"]["error_kind"].as_str().expect("error kind");
let allocation_limit = entry["expect"]["bounds"]["peak_alloc_bytes"]
.as_u64()
.expect("allocation limit") as usize;
let timeout = Duration::from_millis(
entry["expect"]["bounds"]["wall_time_ms"]
.as_u64()
.expect("time limit"),
);
let baseline = CURRENT.load(Ordering::Relaxed);
PEAK.store(baseline, Ordering::Relaxed);
let path = fixture_path(entry);
let (sender, receiver) = mpsc::channel();
let case_id = id.clone();
let handle = std::thread::spawn(move || {
let _ = sender.send(run_case(path, case_id, stage));
});
let result = receiver
.recv_timeout(timeout)
.unwrap_or_else(|_| panic!("{id}: exceeded wall-time bound"));
handle
.join()
.unwrap_or_else(|_| panic!("{id}: worker panicked"));
let kind = result.unwrap_or_else(|error| panic!("{id}: {error}"));
assert_eq!(kind, expected, "{id}");
let peak = PEAK.load(Ordering::Relaxed).saturating_sub(baseline);
assert!(
peak <= allocation_limit,
"{id}: peak {peak} exceeds {allocation_limit}"
);
}
}