mp4-track 0.1.0

Track-level MP4 reader and progressive writer with bounded parsing
Documentation
//! Bounded malformed-input checks.

#![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}"
        );
    }
}