Files
craft-libs/crates/video/tests/h264_decode/robustness.rs
Dylan M. Taylor 0226f1d03e craft-video: H.264 decoder from FilmCraft, with DeckCraft's hardening (h264-decode) (#834)
* craft-video: H.264 decoder from FilmCraft (`h264-decode` feature)

FilmCraft's crates/h264 at 4c0d2ad7 becomes craft_video::h264::decode. The encoder moves to
h264/encode and is still exported from craft_video::h264. Feature h264 now turns on h264-decode
and h264-encode, so either half can be built without the other.

Without the threads feature a reference row that was never published read through expect();
it now reads as a gray row. craft_video::budget (the picture memory budget from DeckCraft's
bitstream crate) is added for the next commit. craft-video is built at opt-level 3 in the dev
profile, as FilmCraft built its codecs.

* craft-video: H.264 decoder hardening from DeckCraft's copy

- The picture store (DPB, current picture, pictures in flight or queued for output) is checked
  against a memory budget before a picture is allocated; Decoder::set_memory_budget.
- configure_avcc checks every entry before handling any: SPS NAL units in the first list, PPS in
  the second. A picture slice in the record starts no picture.
- The inverse transforms clamp coefficients to the spec's r range instead of overflowing.
- Temporal direct motion vectors are scaled in i32 and clipped to the spec range.
- slice_qp_delta is added with a checked add.
- A picture job sets up its picture state inside the panic guard.
2026-11-11 15:25:33 -05:00

215 lines
8.0 KiB
Rust

//! avcC / length-prefixed input and resilience against damaged streams.
use crate::common;
use craft_media::bitstream::annexb_nals;
use craft_video::h264::decode::Decoder;
/// Build an avcC record and length-prefixed samples from an Annex-B stream.
fn to_avcc(data: &[u8]) -> (Vec<u8>, Vec<Vec<u8>>) {
let mut sps = Vec::new();
let mut pps = Vec::new();
let mut samples = Vec::new();
for au in common::split_access_units(data) {
let mut sample = Vec::new();
for nal in annexb_nals(au) {
match nal[0] & 0x1f {
7 => sps.push(nal.to_vec()),
8 => pps.push(nal.to_vec()),
_ => {
sample.extend((nal.len() as u32).to_be_bytes());
sample.extend_from_slice(nal);
}
}
}
if !sample.is_empty() {
samples.push(sample);
}
}
sps.dedup();
pps.dedup();
let s0 = &sps[0];
let mut avcc = vec![1, s0[1], s0[2], s0[3], 0xff, 0xe0 | sps.len() as u8];
for s in &sps {
avcc.extend((s.len() as u16).to_be_bytes());
avcc.extend_from_slice(s);
}
avcc.push(pps.len() as u8);
for p in &pps {
avcc.extend((p.len() as u16).to_be_bytes());
avcc.extend_from_slice(p);
}
(avcc, samples)
}
#[test]
fn avcc_input_matches_reference() {
for name in ["main_cabac_b", "baseline_qcif"] {
let f = common::fixture(name);
let Some((h264, yuv)) = common::ensure(f) else { return };
let (avcc, samples) = to_avcc(&std::fs::read(h264).unwrap());
let mut dec = Decoder::from_avcc(&avcc).unwrap();
assert_eq!(dec.nal_length_size(), Some(4));
let mut pics = Vec::new();
for (i, s) in samples.iter().enumerate() {
pics.extend(dec.decode(s, i as i64 * 1000).unwrap());
}
pics.extend(dec.flush());
let reference = std::fs::read(yuv).unwrap();
common::compare(&pics, &reference, f.width as usize, f.height as usize).unwrap();
// pts are passed through reordering
let mut pts: Vec<i64> = pics.iter().map(|p| p.pts).collect();
pts.sort();
assert_eq!(pts, (0..samples.len() as i64).map(|i| i * 1000).collect::<Vec<_>>());
let p0 = &pics[0];
assert!(p0.key);
assert_eq!((p0.y_stride, p0.uv_stride), (f.width as usize, f.width as usize / 2));
}
}
fn xorshift(seed: &mut u64) -> u64 {
*seed ^= *seed << 13;
*seed ^= *seed >> 7;
*seed ^= *seed << 17;
*seed
}
/// Damaged streams must never panic or hang; errors are fine.
#[test]
fn corrupted_streams_do_not_panic() {
for name in ["main_cabac_b", "baseline_cif_noise", "cavlc_b", "bpyramid"] {
let f = common::fixture(name);
let Some((h264, _)) = common::ensure(f) else { return };
let data = std::fs::read(h264).unwrap();
let mut seed = 0x1234_5678_9abc_def0u64 ^ name.len() as u64;
for round in 0..12 {
let mut d = data.clone();
// flip bytes after the parameter sets
for _ in 0..(1 + round * 3) {
let pos = 64 + (xorshift(&mut seed) as usize) % (d.len() - 64);
d[pos] ^= 1 << (xorshift(&mut seed) % 8);
}
if round % 4 == 3 {
d.truncate(d.len() * 2 / 3);
}
for threads in [1, 4] {
let mut dec = Decoder::with_threads(threads);
for au in common::split_access_units(&d) {
let _ = dec.decode(au, 0);
}
let _ = dec.flush();
let _ = dec.take_error();
}
}
}
}
#[test]
fn garbage_input_is_rejected_gracefully() {
let mut seed = 42u64;
for len in [0usize, 1, 5, 100, 5000] {
let mut g: Vec<u8> = (0..len).map(|_| xorshift(&mut seed) as u8).collect();
if len > 10 {
g[..4].copy_from_slice(&[0, 0, 0, 1]);
}
let mut dec = Decoder::new();
let _ = dec.decode(&g, 0);
let _ = dec.flush();
}
assert!(Decoder::from_avcc(&[1, 2, 3]).is_err());
}
fn is_idr(au: &[u8]) -> bool {
annexb_nals(au).iter().any(|n| n.first().is_some_and(|h| h & 0x1f == 5))
}
fn decode_all(dec: &mut Decoder, aus: &[&[u8]], first_pts: usize) -> Vec<craft_video::h264::decode::Picture> {
let mut pics = Vec::new();
for (i, au) in aus.iter().enumerate() {
pics.extend(dec.decode(au, (first_pts + i) as i64).unwrap());
}
pics.extend(dec.flush());
assert_eq!(dec.take_error(), None);
pics
}
fn assert_same(a: &[craft_video::h264::decode::Picture], b: &[craft_video::h264::decode::Picture]) {
assert_eq!(a.len(), b.len());
for (p, q) in a.iter().zip(b) {
assert_eq!((p.pts, p.poc, p.key, p.width, p.height), (q.pts, q.poc, q.key, q.width, q.height));
assert!(p.y == q.y && p.u == q.u && p.v == q.v, "picture {} differs", p.pts);
}
}
#[test]
fn reset_mid_stream_matches_a_new_decoder() {
for name in ["keyint10", "open_gop", "slices4"] {
let f = common::fixture(name);
let Some((h264, _)) = common::ensure(f) else { return };
let data = std::fs::read(h264).unwrap();
let aus = common::split_access_units(&data);
let idrs: Vec<usize> = (0..aus.len()).filter(|&i| is_idr(aus[i])).collect();
for (threads, draft) in [(1, false), (3, false), (8, false), (8, true)] {
for stop in [1, 4, 9, 13, 22].map(|s| s.min(aus.len())) {
// seek to the next IDR, or back to the start
let next = idrs.iter().copied().find(|&i| i >= stop).unwrap_or(0);
let mut dec = Decoder::with_threads(threads);
dec.set_draft(draft);
for (i, au) in aus[..stop].iter().enumerate() {
let _ = dec.decode(au, i as i64);
}
dec.reset();
assert_eq!(dec.draft(), draft);
let got = decode_all(&mut dec, &aus[next..], next);
let mut fresh = Decoder::with_threads(threads);
fresh.set_draft(draft);
let want = decode_all(&mut fresh, &aus[next..], next);
assert!(want.len() >= aus.len() - next - 1);
assert_same(&got, &want);
}
}
// length-prefixed input: the avcC configuration is reapplied after the reset
let (avcc, samples) = to_avcc(&data);
let samples: Vec<&[u8]> = samples.iter().map(|s| s.as_slice()).collect();
let mut dec = Decoder::from_avcc(&avcc).unwrap();
for s in &samples[..7] {
let _ = dec.decode(s, 0);
}
dec.reset();
assert_eq!(dec.nal_length_size(), None);
dec.configure_avcc(&avcc).unwrap();
let got = decode_all(&mut dec, &samples, 0);
let want = decode_all(&mut Decoder::from_avcc(&avcc).unwrap(), &samples, 0);
assert_same(&got, &want);
}
}
#[test]
fn rapid_resets_do_not_hang() {
let f = common::fixture("bench_1080p");
let Some((h264, _)) = common::ensure(f) else { return };
let data = std::fs::read(h264).unwrap();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let aus = common::split_access_units(&data);
let mut seed = 7u64;
for threads in [2, 8, 16] {
let mut dec = Decoder::with_threads(threads);
for round in 0..40 {
// mostly from the IDR (pictures in flight at the reset), sometimes from a random point
let start = if round % 4 == 3 { xorshift(&mut seed) as usize % aus.len() } else { 0 };
let n = 1 + xorshift(&mut seed) as usize % 12;
for au in aus.iter().skip(start).take(n) {
let _ = dec.decode(au, 0);
}
dec.reset();
}
let got = decode_all(&mut dec, &aus, 0);
let want = decode_all(&mut Decoder::with_threads(threads), &aus, 0);
assert_same(&got, &want);
}
tx.send(()).unwrap();
});
rx.recv_timeout(std::time::Duration::from_secs(300)).expect("decoder hung after reset");
}