mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 08:52:32 +00:00
* craft-video: VP9 decoder from FilmCraft (`vp9-decode` feature) FilmCraft's crates/vp9 at 4c0d2ad7 becomes craft_video::vp9::decode. The encoder moves to vp9/encode and is still exported from craft_video::vp9. Feature vp9 now turns on vp9-decode and vp9-encode, so either half can be built without the other. The decoder's tests find ffmpeg and the fixture directory through craft-media-testkit; a fixture that cannot be generated fails under CRAFT_MEDIA_REQUIRE_ORACLES=1. craft-video is built at opt-level 3 in the dev profile, as FilmCraft built its codecs. * craft-video: VP9 decoder checks its picture store against a memory budget DeckCraft's copy of the decoder refuses a frame size whose pictures (eight reference slots, the new frame and those in flight) would not fit a byte budget. The budget type comes with it as craft_video::budget; FilmCraft's 2^26 luma sample cap stays.
132 lines
5.0 KiB
Rust
132 lines
5.0 KiB
Rust
//! Damaged, truncated and random input must never panic or hang (errors are fine).
|
|
|
|
use crate::common;
|
|
|
|
use craft_video::vp9::decode::Decoder;
|
|
use std::time::{Duration, Instant};
|
|
|
|
fn xorshift(seed: &mut u64) -> u64 {
|
|
*seed ^= *seed << 13;
|
|
*seed ^= *seed >> 7;
|
|
*seed ^= *seed << 17;
|
|
*seed
|
|
}
|
|
|
|
fn decode_all(frames: &[Vec<u8>], threads: usize) {
|
|
let mut dec = Decoder::with_threads(threads);
|
|
for (i, f) in frames.iter().enumerate() {
|
|
let _ = dec.decode(f, i as i64);
|
|
}
|
|
let _ = dec.flush();
|
|
}
|
|
|
|
#[test]
|
|
fn corrupted_streams_do_not_panic() {
|
|
for name in ["default_good", "altref_hidden", "profile2_10bit", "tiles_cols4", "aq_segmentation", "lossless", "profile1_444", "tiles_rows4"] {
|
|
let f = common::fixture(name);
|
|
let Some((ivf, _)) = common::ensure(f) else { return };
|
|
let data = std::fs::read(ivf).unwrap();
|
|
let frames: Vec<Vec<u8>> = common::ivf_frames(&data).into_iter().map(|f| f.to_vec()).collect();
|
|
let mut seed = 0x1234_5678_9abc_def0u64 ^ name.len() as u64;
|
|
for round in 0..10 {
|
|
let t0 = Instant::now();
|
|
let mut d = frames.clone();
|
|
for _ in 0..(1 + round * 4) {
|
|
let fi = (xorshift(&mut seed) as usize) % d.len();
|
|
let fr = &mut d[fi];
|
|
if fr.is_empty() {
|
|
continue;
|
|
}
|
|
let pos = (xorshift(&mut seed) as usize) % fr.len();
|
|
match round % 3 {
|
|
0 => fr[pos] ^= 1 << (xorshift(&mut seed) % 8),
|
|
1 => fr[pos] = xorshift(&mut seed) as u8,
|
|
_ => fr.truncate(pos),
|
|
}
|
|
}
|
|
for threads in [1, 4] {
|
|
decode_all(&d, threads);
|
|
}
|
|
assert!(t0.elapsed() < Duration::from_secs(60), "{name} round {round} too slow");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn truncated_frames_do_not_panic() {
|
|
let f = common::fixture("default_good");
|
|
let Some((ivf, _)) = common::ensure(f) else { return };
|
|
let data = std::fs::read(ivf).unwrap();
|
|
let frames: Vec<Vec<u8>> = common::ivf_frames(&data).into_iter().map(|f| f.to_vec()).collect();
|
|
for cut in [1usize, 2, 3, 5, 8, 13, 21, 40, 100, 400] {
|
|
let d: Vec<Vec<u8>> = frames.iter().map(|f| f[..cut.min(f.len())].to_vec()).collect();
|
|
decode_all(&d, 1);
|
|
decode_all(&d, 3);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn garbage_input_is_rejected_gracefully() {
|
|
let mut seed = 42u64;
|
|
for len in [0usize, 1, 2, 5, 30, 100, 5000] {
|
|
for k in 0..40 {
|
|
let mut g: Vec<u8> = (0..len).map(|_| xorshift(&mut seed) as u8).collect();
|
|
if len > 10 && k % 2 == 0 {
|
|
// A plausible key frame start: frame marker, profile 0, key frame, sync code.
|
|
g[0] = 0x82;
|
|
g[1] = 0x49;
|
|
g[2] = 0x83;
|
|
g[3] = 0x42;
|
|
}
|
|
let mut dec = Decoder::with_threads(1 + k % 3);
|
|
let _ = dec.decode(&g, 0);
|
|
let _ = dec.decode(&g, 1);
|
|
}
|
|
}
|
|
// Inter frame / show_existing without references.
|
|
let mut dec = Decoder::new();
|
|
assert!(dec.decode(&[0x88], 0).is_err());
|
|
assert!(dec.decode(&[0x86, 0, 0, 0, 0], 0).is_err());
|
|
}
|
|
|
|
/// Huge declared frame sizes must not allocate absurd amounts or panic.
|
|
#[test]
|
|
fn oversized_frame_is_rejected() {
|
|
// Key frame header: marker 2, profile 0, show_existing 0, key, show, no error res,
|
|
// sync, color (4 bits), width-1 = 0xffff, height-1 = 0xffff.
|
|
let bits = "10".to_string() + "00" + "0" + "0" + "1" + "0" + "010010011000001101000010" + "0000" + &"1".repeat(32);
|
|
let mut v = vec![0u8; bits.len().div_ceil(8) + 8];
|
|
for (i, c) in bits.chars().enumerate() {
|
|
if c == '1' {
|
|
v[i / 8] |= 0x80 >> (i % 8);
|
|
}
|
|
}
|
|
let mut dec = Decoder::new();
|
|
assert!(dec.decode(&v, 0).is_err());
|
|
}
|
|
|
|
/// A key frame whose picture store would not fit the decoder's memory budget is refused before
|
|
/// anything is allocated for it.
|
|
#[test]
|
|
fn frame_over_the_memory_budget_is_rejected() {
|
|
// The header of `oversized_frame_is_rejected` with width-1 = height-1 = 63, then the rest of
|
|
// the uncompressed header as zeros (28 bits) and header_size_in_bytes = 1.
|
|
let size = "0000000000111111";
|
|
let bits =
|
|
"10".to_string() + "00" + "0" + "0" + "1" + "0" + "010010011000001101000010" + "0000" + size + size + &"0".repeat(28) + "0000000000000001";
|
|
let mut v = vec![0u8; bits.len().div_ceil(8) + 8];
|
|
for (i, c) in bits.chars().enumerate() {
|
|
if c == '1' {
|
|
v[i / 8] |= 0x80 >> (i % 8);
|
|
}
|
|
}
|
|
let over_budget = |e: craft_video::vp9::decode::Error| e.to_string().contains("memory budget");
|
|
// One 64x64 4:2:0 picture is 12288 bytes at two bytes a sample; one thread holds 12 of them.
|
|
let mut dec = Decoder::with_threads(1);
|
|
dec.set_memory_budget(12 * 12288 - 1);
|
|
assert!(dec.decode(&v, 0).is_err_and(over_budget));
|
|
let mut dec = Decoder::with_threads(1);
|
|
dec.set_memory_budget(12 * 12288);
|
|
assert!(!dec.decode(&v, 0).is_err_and(over_budget));
|
|
}
|