mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-10 12:29:34 -06:00
What was wrong: when a page failed its CRC the demuxer had already read the whole page (up to about 64 KiB), then moved on by one byte and tried again. Nothing was charged for it, and only the scan for the next capture pattern had a budget, which a candidate that matches "OggS" at the current position never reaches. A file made of capture patterns 64 bytes apart, each claiming a full page, made a 1 MB file read about 957 MB and compute a CRC over all of it. The warnings list also grew with every damaged page, without limit. What changed: the per-file recovery budget (32 MiB, the scan's existing one) is now also charged with the length of every page read only to find its CRC wrong. When it is spent the pages found so far are returned with a warning saying the rest was not searched. At most 256 warnings are kept, plus one saying the rest were dropped. How it was tested: two new tests failed before the fix and pass after it, a counting ByteSource over 1 MB of dense fake pages (bytes read stay within the budget; before, 957 MB were read) and 100,000 bad-CRC pages (before, 199,999 warnings). A file of good pages is not charged and has no warnings. The other ogg tests still pass, and clippy is clean with -D warnings. Completes the Ogg part of storytold/filmcraft#2794
277 lines
9.6 KiB
Rust
277 lines
9.6 KiB
Rust
//! Synthetic Ogg streams: a small page writer (RFC 3533) and Opus-like packets.
|
||
|
||
use std::cell::Cell;
|
||
use std::io;
|
||
|
||
use super::{ByteSource, CaptureScanner, Codec, Error, OpusTiming, Packet, Stream, crc32, open, opus_packet_samples, sniff};
|
||
|
||
/// Write one page holding `segments` (lacing already computed by `lace`).
|
||
fn page(out: &mut Vec<u8>, flags: u8, granule: i64, serial: u32, seq: u32, lacing: &[u8], body: &[u8]) {
|
||
let start = out.len();
|
||
out.extend_from_slice(b"OggS");
|
||
out.push(0);
|
||
out.push(flags);
|
||
out.extend_from_slice(&granule.to_le_bytes());
|
||
out.extend_from_slice(&serial.to_le_bytes());
|
||
out.extend_from_slice(&seq.to_le_bytes());
|
||
out.extend_from_slice(&[0; 4]);
|
||
out.push(lacing.len() as u8);
|
||
out.extend_from_slice(lacing);
|
||
out.extend_from_slice(body);
|
||
let crc = crc32(&out[start..]);
|
||
out[start + 22..start + 26].copy_from_slice(&crc.to_le_bytes());
|
||
}
|
||
|
||
fn lace(packets: &[&[u8]], last_continues: bool) -> (Vec<u8>, Vec<u8>) {
|
||
let mut l = Vec::new();
|
||
let mut b = Vec::new();
|
||
for (i, p) in packets.iter().enumerate() {
|
||
let mut n = p.len();
|
||
while n >= 255 {
|
||
l.push(255);
|
||
n -= 255;
|
||
}
|
||
if !(last_continues && i + 1 == packets.len()) {
|
||
l.push(n as u8);
|
||
}
|
||
b.extend_from_slice(p);
|
||
}
|
||
(l, b)
|
||
}
|
||
|
||
fn opus_head(pre_skip: u16) -> Vec<u8> {
|
||
let mut h = b"OpusHead".to_vec();
|
||
h.push(1);
|
||
h.push(2);
|
||
h.extend_from_slice(&pre_skip.to_le_bytes());
|
||
h.extend_from_slice(&48_000u32.to_le_bytes());
|
||
h.extend_from_slice(&0i16.to_le_bytes());
|
||
h.push(0);
|
||
h
|
||
}
|
||
|
||
/// A CELT 20 ms packet (960 samples): TOC config 31, code 0.
|
||
fn pkt(i: usize, len: usize) -> Vec<u8> {
|
||
let mut p = vec![0xF8];
|
||
p.extend((0..len.saturating_sub(1)).map(|k| (i * 7 + k) as u8));
|
||
p
|
||
}
|
||
|
||
/// Header pages, then pages of 3 packets each; the last page's granule trims `trim` samples.
|
||
fn opus_file(npk: usize, pre_skip: u16, trim: i64) -> Vec<u8> {
|
||
let mut f = Vec::new();
|
||
let head = opus_head(pre_skip);
|
||
let (l, b) = lace(&[&head], false);
|
||
page(&mut f, 0x02, 0, 9, 0, &l, &b);
|
||
let tags = b"OpusTags\x00\x00\x00\x00\x00\x00\x00\x00".to_vec();
|
||
let (l, b) = lace(&[&tags], false);
|
||
page(&mut f, 0, 0, 9, 1, &l, &b);
|
||
let pkts: Vec<Vec<u8>> = (0..npk).map(|i| pkt(i, 40 + i % 5)).collect();
|
||
let mut done = 0;
|
||
for (seq, chunk) in (2..).zip(pkts.chunks(3)) {
|
||
done += chunk.len();
|
||
let last = done == npk;
|
||
let mut g = done as i64 * 960;
|
||
if last {
|
||
g -= trim;
|
||
}
|
||
let refs: Vec<&[u8]> = chunk.iter().map(Vec::as_slice).collect();
|
||
let (l, b) = lace(&refs, false);
|
||
page(&mut f, if last { 0x04 } else { 0 }, g, 9, seq, &l, &b);
|
||
}
|
||
f
|
||
}
|
||
|
||
#[test]
|
||
fn crc_matches_known_value() {
|
||
// CRC-32/CKSUM parameters without its final XOR (check value 0x765E7680 ^ 0xFFFFFFFF)
|
||
assert_eq!(crc32(b"123456789"), 0x89A1_897F);
|
||
assert_eq!(crc32(b""), 0);
|
||
assert_ne!(crc32(b"OggS"), crc32(b"OggT"));
|
||
}
|
||
|
||
#[test]
|
||
fn opus_pages_packets_and_timing() {
|
||
let f = opus_file(10, 312, 500);
|
||
assert!(sniff(&f));
|
||
let o = open(&f).unwrap();
|
||
assert!(o.warnings.is_empty(), "{:?}", o.warnings);
|
||
let s = o.stream_of(Codec::Opus).unwrap();
|
||
let st = &o.streams[s];
|
||
assert_eq!(st.headers.len(), 2);
|
||
assert_eq!(st.packets.len(), 10);
|
||
assert!(st.saw_eos);
|
||
assert_eq!(o.read_packet(&f, s, 4).unwrap(), pkt(4, 44));
|
||
let t = OpusTiming::of(st, 312).unwrap();
|
||
assert_eq!(t.durations, vec![960; 10]);
|
||
assert_eq!(t.starts[0], -312);
|
||
assert_eq!(t.starts[9], 9 * 960 - 312);
|
||
// end trimming: the last page granule is 500 short
|
||
assert_eq!(t.total, 10 * 960 - 500 - 312);
|
||
}
|
||
|
||
#[test]
|
||
fn packets_spanning_pages() {
|
||
let mut f = Vec::new();
|
||
let head = opus_head(0);
|
||
let (l, b) = lace(&[&head], false);
|
||
page(&mut f, 0x02, 0, 1, 0, &l, &b);
|
||
let (l, b) = lace(&[b"OpusTags"], false);
|
||
page(&mut f, 0, 0, 1, 1, &l, &b);
|
||
let big = pkt(0, 700);
|
||
// first 510 bytes on one page (two 255 lacing values, packet continues), rest on the next
|
||
let (l, b) = (vec![255, 255], big[..510].to_vec());
|
||
page(&mut f, 0, -1, 1, 2, &l, &b);
|
||
let rest = &big[510..];
|
||
let small = pkt(1, 30);
|
||
let (l, b) = lace(&[rest, &small], false);
|
||
page(&mut f, 0x01 | 0x04, 1920, 1, 3, &l, &b);
|
||
let o = open(&f).unwrap();
|
||
let st = &o.streams[0];
|
||
assert_eq!(st.packets.len(), 2);
|
||
assert_eq!(st.packets[0].parts.len(), 2);
|
||
assert_eq!(o.read_packet(&f, 0, 0).unwrap(), big);
|
||
assert_eq!(o.read_packet(&f, 0, 1).unwrap(), small);
|
||
let t = OpusTiming::of(st, 0).unwrap();
|
||
assert_eq!(t.starts, vec![0, 960]);
|
||
assert_eq!(t.total, 1920);
|
||
}
|
||
|
||
#[test]
|
||
fn corrupt_pages_are_skipped_and_truncation_is_safe() {
|
||
let f = opus_file(12, 312, 0);
|
||
// flip a byte inside the third data page's body: that page fails its CRC and is dropped
|
||
let mut g = f.clone();
|
||
let third = g.windows(4).enumerate().filter(|(_, w)| *w == b"OggS").map(|(i, _)| i).nth(4).unwrap();
|
||
g[third + 40] ^= 0xFF;
|
||
let o = open(&g).unwrap();
|
||
assert!(o.warnings.iter().any(|w| w.contains("CRC")), "{:?}", o.warnings);
|
||
assert_eq!(o.streams[0].packets.len(), 9);
|
||
let t = OpusTiming::of(&o.streams[0], 312).unwrap();
|
||
// the granules after the gap put the later packets at their true positions
|
||
assert_eq!(*t.starts.last().unwrap(), 11 * 960 - 312);
|
||
for cut in 0..f.len() {
|
||
if let Ok(o) = open(&f[..cut].to_vec()) {
|
||
for st in &o.streams {
|
||
let t = OpusTiming::of(st, 312).unwrap();
|
||
assert_eq!(t.starts.len(), st.packets.len());
|
||
}
|
||
}
|
||
}
|
||
let mut seed = 99u64;
|
||
for _ in 0..300 {
|
||
let mut g = f.clone();
|
||
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
||
let at = (seed >> 33) as usize % g.len();
|
||
g[at] = (seed >> 20) as u8;
|
||
if let Ok(o) = open(&g) {
|
||
for st in &o.streams {
|
||
let _ = OpusTiming::of(st, 312);
|
||
}
|
||
}
|
||
}
|
||
assert!(matches!(open(&b"not ogg at all, no capture pattern".to_vec()), Err(Error::NotOgg)));
|
||
}
|
||
|
||
#[test]
|
||
fn opus_toc_durations() {
|
||
assert_eq!(opus_packet_samples([0x00, 0], 10), Some(480)); // SILK NB 10 ms
|
||
assert_eq!(opus_packet_samples([0x18, 0], 10), Some(2880)); // SILK NB 60 ms
|
||
assert_eq!(opus_packet_samples([0x78 | 1, 0], 10), Some(1920)); // hybrid 20 ms × 2
|
||
assert_eq!(opus_packet_samples([0x80 | 3, 6], 10), Some(720)); // CELT 2.5 ms × 6
|
||
assert_eq!(opus_packet_samples([0xF8 | 3, 7], 10), None); // 140 ms: invalid
|
||
assert_eq!(opus_packet_samples([0xF8, 0], 0), None);
|
||
}
|
||
|
||
#[test]
|
||
fn opus_timing_rejects_a_granule_that_overflows() {
|
||
let packet = |granule: i64| Stream {
|
||
serial: 1,
|
||
codec: Codec::Opus,
|
||
headers: vec![],
|
||
packets: vec![Packet { parts: vec![], size: 10, granule: Some(granule), page: 0, eos: true, prefix: [0x00, 0] }],
|
||
pages: 1,
|
||
last_granule: Some(granule),
|
||
saw_eos: true,
|
||
};
|
||
let ok = OpusTiming::of(&packet(480), 0).unwrap();
|
||
assert_eq!(ok.starts, vec![0]);
|
||
assert_eq!(ok.total, 480);
|
||
assert!(OpusTiming::of(&packet(i64::MIN), 0).is_err());
|
||
}
|
||
|
||
/// A source that counts the bytes read from it.
|
||
struct Counting<'a> {
|
||
data: &'a [u8],
|
||
read: Cell<u64>,
|
||
}
|
||
|
||
impl ByteSource for Counting<'_> {
|
||
fn len(&self) -> u64 {
|
||
self.data.len() as u64
|
||
}
|
||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
|
||
self.read.set(self.read.get() + buf.len() as u64);
|
||
self.data.read_at(offset, buf)
|
||
}
|
||
}
|
||
|
||
/// A valid file followed by `len` bytes of 0xFF with a capture pattern every 64 bytes. Each one
|
||
/// claims 255 full lacing values, so it reads as a page of about 64 KiB that fails its CRC.
|
||
fn dense_fake_pages(len: usize) -> Vec<u8> {
|
||
let mut f = opus_file(6, 312, 0);
|
||
let start = f.len();
|
||
f.resize(start + len, 0xFF);
|
||
for at in (start..start + len - 64).step_by(64) {
|
||
f[at..at + 5].copy_from_slice(b"OggS\0");
|
||
f[at + 26] = 255;
|
||
}
|
||
f
|
||
}
|
||
|
||
#[test]
|
||
fn recovery_reads_a_bounded_amount_of_a_damaged_file() {
|
||
let f = dense_fake_pages(1 << 20);
|
||
let src = Counting { data: &f, read: Cell::new(0) };
|
||
let o = open(&src).unwrap();
|
||
assert_eq!(o.streams[0].packets.len(), 6, "the good pages are kept");
|
||
let read = src.read.get();
|
||
assert!(read <= 40 << 20, "read {read} bytes of a {} byte file", f.len());
|
||
assert!(o.warnings.iter().any(|w| w.contains("recovery budget")), "{:?}", o.warnings);
|
||
}
|
||
|
||
#[test]
|
||
fn warnings_are_capped() {
|
||
// 100,000 pages with no segments and a wrong CRC, 27 bytes each
|
||
let mut f = opus_file(6, 312, 0);
|
||
for _ in 0..100_000 {
|
||
let at = f.len();
|
||
f.extend_from_slice(b"OggS\0\0");
|
||
f.resize(at + 27, 0);
|
||
f[at + 22] = 1;
|
||
}
|
||
let o = open(&f).unwrap();
|
||
assert!(o.warnings.len() <= 257, "{} warnings", o.warnings.len());
|
||
assert!(o.warnings.iter().any(|w| w.contains("omitted")), "{:?}", o.warnings.last());
|
||
}
|
||
|
||
#[test]
|
||
fn a_file_of_good_pages_is_not_charged_for_recovery() {
|
||
let f = opus_file(2000, 312, 0);
|
||
let src = Counting { data: &f, read: Cell::new(0) };
|
||
let o = open(&src).unwrap();
|
||
assert_eq!(o.streams[0].packets.len(), 2000);
|
||
assert!(o.warnings.is_empty(), "{:?}", o.warnings);
|
||
}
|
||
|
||
#[test]
|
||
fn a_budget_too_small_to_search_is_spent() {
|
||
// 1 to 3 bytes left cannot hold a capture pattern: the scan would stop without saying why
|
||
let mut scanner = CaptureScanner::new();
|
||
assert!(!scanner.spent());
|
||
assert!(scanner.charge(super::RECOVERY_BUDGET - 3));
|
||
assert!(scanner.spent());
|
||
let f = opus_file(6, 312, 0);
|
||
assert_eq!(scanner.find(&f, 0, f.len() as u64).unwrap(), None);
|
||
}
|