Audit-driven fixes (rounds 1–3):
- hevc: correct hvcC profile/level SPS offsets (HEVC has a 2-byte NAL header)
- mkv: map all DTS variants to the registered A_DTS codec id; force a new
cluster before the i16 cluster-relative timestamp can overflow
- ebml/mkvstream: bound untrusted EBML sizes (no multi-GB allocs); reject
uint>8 (was an OOB panic) and non-{0,4,8} float widths (were a desync)
- ts: skip PES-header bytes that span a TS packet boundary; add the PMT
section_len/prog_info_len bounds the PAT parser already had
- ac3: preserve a 0x0B77 syncword split across a PES boundary; cap buffer
- dts: validate each next-core boundary by decoded core size (a 0x7FFE8001
pattern inside XLL payload no longer false-splits/drops the lossless
extension); reject sub-minimum core frames; fix forced-emit PTS base
- lpcm: DVD program-stream PCM no longer double-strips the BD LPCM header
- vc1/mpeg2: do not emit a parameter-set-only PES as a standalone frame
- pgs/truehd: cap the pending reassembly buffer (parity with ac3/dts)
- aacs: ts_syncs_intact uses the exact packet count
- prefetched: capacity-guard the recycled-buffer set_len
- Cargo.toml: exclude project docs from the published crate
Convergence: a third independent audit pass found no remaining material
(CRITICAL/HIGH/MEDIUM) issues. Full precommit (fmt + clippy -D + tests,
Rust 1.86) green.
399 lines
12 KiB
Rust
399 lines
12 KiB
Rust
//! AC3 (Dolby Digital) / EAC3 (Dolby Digital Plus) frame parser.
|
|
//!
|
|
//! AC3 frames are self-contained and always start with syncword 0x0B77.
|
|
//! Buffers across PES boundaries so frames that span two PES packets
|
|
//! are emitted complete, not truncated.
|
|
|
|
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
|
|
|
|
/// Hard cap on the carry-over buffer. An AC-3/E-AC-3 frame is at most 8192
|
|
/// bytes (the `frame_size > 8192` reject below), so a single straddling frame
|
|
/// plus a little slack never needs more than this. If the buffer grows past
|
|
/// the cap without yielding a frame (pathological / never-syncing input) we
|
|
/// drop it and resync rather than accumulate one PES worth of data per call
|
|
/// for the whole title.
|
|
const MAX_AC3_BUF: usize = 64 * 1024;
|
|
|
|
pub struct Ac3Parser {
|
|
/// Leftover bytes from previous PES (incomplete frame at end).
|
|
buf: Vec<u8>,
|
|
}
|
|
|
|
impl Default for Ac3Parser {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl Ac3Parser {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
buf: Vec::with_capacity(4096),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CodecParser for Ac3Parser {
|
|
fn parse(&mut self, pes: &PesPacket) -> Vec<Frame> {
|
|
if pes.data.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
|
|
let pts_ns = pes.pts.map(pts_to_ns).unwrap_or(0);
|
|
|
|
// Prepend leftover from previous PES
|
|
self.buf.extend_from_slice(&pes.data);
|
|
|
|
let data = &self.buf;
|
|
let mut frames = Vec::new();
|
|
let mut pos = 0;
|
|
|
|
while pos < data.len() {
|
|
let sync = find_ac3_sync(&data[pos..]);
|
|
let start = match sync {
|
|
Some(offset) => pos + offset,
|
|
None => break,
|
|
};
|
|
|
|
let remaining = &data[start..];
|
|
|
|
if remaining.len() < 6 {
|
|
// Not enough data to determine frame size — keep for next PES
|
|
break;
|
|
}
|
|
|
|
let bsid = get_bsid(remaining);
|
|
let frame_size = if bsid >= 11 {
|
|
eac3_frame_size(remaining)
|
|
} else {
|
|
ac3_frame_size(remaining)
|
|
};
|
|
|
|
if frame_size == 0 || frame_size > 8192 {
|
|
// Invalid frame size — skip this sync word
|
|
pos = start + 2;
|
|
continue;
|
|
}
|
|
|
|
if start + frame_size > data.len() {
|
|
// Incomplete frame — keep for next PES
|
|
break;
|
|
}
|
|
|
|
frames.push(Frame {
|
|
pts_ns,
|
|
keyframe: true,
|
|
data: data[start..start + frame_size].to_vec(),
|
|
duration_ns: None,
|
|
});
|
|
pos = start + frame_size;
|
|
}
|
|
|
|
// Keep unconsumed data for the next call. `pos` is the start of the
|
|
// unconsumed region: either a partial frame that straddles this PES
|
|
// boundary — which, by construction, begins at a syncword (every byte
|
|
// before `pos` was emitted as a frame or skipped as pre-sync junk) — or
|
|
// trailing bytes too short to size/complete a frame. Carry from `pos`,
|
|
// NOT from the next syncword: discarding bytes between `pos` and the
|
|
// next sync would drop the partial frame we are deliberately keeping
|
|
// across the boundary.
|
|
let keep_from = if pos < data.len() {
|
|
// A syncword at/after `pos` marks the carry-over start (anything
|
|
// before it is junk with no sync). With no full sync, retain the
|
|
// whole tail — including a lone trailing 0x0B that may be the first
|
|
// half of a syncword split across the PES boundary.
|
|
match find_ac3_sync(&data[pos..]) {
|
|
Some(o) => pos + o,
|
|
None if data.last() == Some(&0x0B) => data.len() - 1,
|
|
None => data.len(),
|
|
}
|
|
} else {
|
|
data.len()
|
|
};
|
|
|
|
if keep_from < data.len() {
|
|
let tail = &data[keep_from..];
|
|
if tail.len() > MAX_AC3_BUF {
|
|
// No frame could be parsed out of a buffer this large — this is
|
|
// not valid AC-3 here. Drop it and resync on the next PES rather
|
|
// than grow without bound on pathological input.
|
|
self.buf.clear();
|
|
} else {
|
|
self.buf = tail.to_vec();
|
|
}
|
|
} else {
|
|
self.buf.clear();
|
|
}
|
|
|
|
frames
|
|
}
|
|
|
|
fn codec_private(&self) -> Option<Vec<u8>> {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Find AC3/E-AC-3 syncword (0x0B77) in data.
|
|
fn find_ac3_sync(data: &[u8]) -> Option<usize> {
|
|
(0..data.len().saturating_sub(1)).find(|&i| data[i] == 0x0B && data[i + 1] == 0x77)
|
|
}
|
|
|
|
/// Extract bsid from an AC-3/E-AC-3 frame starting at the syncword.
|
|
/// bsid is at byte 5, bits 7..3.
|
|
pub fn get_bsid(data: &[u8]) -> u8 {
|
|
if data.len() < 6 {
|
|
return 0;
|
|
}
|
|
(data[5] >> 3) & 0x1F
|
|
}
|
|
|
|
/// Calculate E-AC-3 frame size in bytes from the frmsiz field.
|
|
fn eac3_frame_size(data: &[u8]) -> usize {
|
|
if data.len() < 4 {
|
|
return 0;
|
|
}
|
|
let frmsiz = ((data[2] as usize & 0x07) << 8) | data[3] as usize;
|
|
(frmsiz + 1) * 2
|
|
}
|
|
|
|
/// Calculate AC-3 frame size in bytes from fscod and frmsizecod.
|
|
fn ac3_frame_size(data: &[u8]) -> usize {
|
|
if data.len() < 5 {
|
|
return 0;
|
|
}
|
|
let fscod = (data[4] >> 6) & 0x03;
|
|
let frmsizecod = (data[4] & 0x3F) as usize;
|
|
if frmsizecod >= AC3_FRAME_SIZES.len() {
|
|
return 0;
|
|
}
|
|
let words = AC3_FRAME_SIZES[frmsizecod];
|
|
match fscod {
|
|
0 => words[0] * 2,
|
|
1 => words[1] * 2,
|
|
2 => words[2] * 2,
|
|
_ => 0,
|
|
}
|
|
}
|
|
|
|
/// AC-3 frame size table: [frmsizecod] -> [48kHz words, 44.1kHz words, 32kHz words]
|
|
const AC3_FRAME_SIZES: [[usize; 3]; 38] = [
|
|
[64, 69, 96],
|
|
[64, 70, 96],
|
|
[80, 87, 120],
|
|
[80, 88, 120],
|
|
[96, 104, 144],
|
|
[96, 105, 144],
|
|
[112, 121, 168],
|
|
[112, 122, 168],
|
|
[128, 139, 192],
|
|
[128, 140, 192],
|
|
[160, 174, 240],
|
|
[160, 175, 240],
|
|
[192, 208, 288],
|
|
[192, 209, 288],
|
|
[224, 243, 336],
|
|
[224, 244, 336],
|
|
[256, 278, 384],
|
|
[256, 279, 384],
|
|
[320, 348, 480],
|
|
[320, 349, 480],
|
|
[384, 417, 576],
|
|
[384, 418, 576],
|
|
[448, 487, 672],
|
|
[448, 488, 672],
|
|
[512, 557, 768],
|
|
[512, 558, 768],
|
|
[640, 696, 960],
|
|
[640, 697, 960],
|
|
[768, 835, 1152],
|
|
[768, 836, 1152],
|
|
[896, 975, 1344],
|
|
[896, 976, 1344],
|
|
[1024, 1114, 1536],
|
|
[1024, 1115, 1536],
|
|
[1152, 1253, 1728],
|
|
[1152, 1254, 1728],
|
|
[1280, 1393, 1920],
|
|
[1280, 1394, 1920],
|
|
];
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn make_ac3_frame(fscod: u8, frmsizecod: u8) -> Vec<u8> {
|
|
let size = AC3_FRAME_SIZES[frmsizecod as usize][fscod as usize] * 2;
|
|
let mut frame = vec![0u8; size];
|
|
frame[0] = 0x0B;
|
|
frame[1] = 0x77;
|
|
frame[4] = (fscod << 6) | frmsizecod;
|
|
frame[5] = 0x08 << 3; // bsid = 8 (AC-3)
|
|
frame
|
|
}
|
|
|
|
#[test]
|
|
fn parse_empty_pes() {
|
|
let mut parser = Ac3Parser::new();
|
|
let pes = PesPacket {
|
|
pid: 0,
|
|
pts: None,
|
|
dts: None,
|
|
data: vec![],
|
|
};
|
|
assert!(parser.parse(&pes).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_single_frame() {
|
|
let mut parser = Ac3Parser::new();
|
|
let frame_data = make_ac3_frame(0, 2); // 48kHz, 80 words = 160 bytes
|
|
let pes = PesPacket {
|
|
pid: 0,
|
|
pts: Some(90000),
|
|
dts: None,
|
|
data: frame_data.clone(),
|
|
};
|
|
let frames = parser.parse(&pes);
|
|
assert_eq!(frames.len(), 1);
|
|
assert_eq!(frames[0].data.len(), 160);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_frame_spanning_two_pes() {
|
|
let mut parser = Ac3Parser::new();
|
|
let frame_data = make_ac3_frame(0, 2); // 160 bytes
|
|
let mid = 80;
|
|
|
|
// First PES: first half of frame
|
|
let pes1 = PesPacket {
|
|
pid: 0,
|
|
pts: Some(90000),
|
|
dts: None,
|
|
data: frame_data[..mid].to_vec(),
|
|
};
|
|
let frames1 = parser.parse(&pes1);
|
|
assert!(frames1.is_empty(), "partial frame should not emit");
|
|
|
|
// Second PES: second half
|
|
let pes2 = PesPacket {
|
|
pid: 0,
|
|
pts: Some(93000),
|
|
dts: None,
|
|
data: frame_data[mid..].to_vec(),
|
|
};
|
|
let frames2 = parser.parse(&pes2);
|
|
assert_eq!(frames2.len(), 1);
|
|
assert_eq!(frames2[0].data.len(), 160);
|
|
}
|
|
|
|
#[test]
|
|
fn skip_garbage_before_sync() {
|
|
let mut parser = Ac3Parser::new();
|
|
let frame_data = make_ac3_frame(0, 2);
|
|
let mut data = vec![0xDE, 0xAD, 0xBE, 0xEF]; // garbage
|
|
data.extend_from_slice(&frame_data);
|
|
let pes = PesPacket {
|
|
pid: 0,
|
|
pts: None,
|
|
dts: None,
|
|
data,
|
|
};
|
|
let frames = parser.parse(&pes);
|
|
assert_eq!(frames.len(), 1);
|
|
assert_eq!(frames[0].data.len(), 160);
|
|
}
|
|
|
|
#[test]
|
|
fn sync_word_split_across_pes_is_preserved() {
|
|
// A frame whose 0x0B77 syncword straddles the PES boundary (0x0B at the
|
|
// tail of PES 1, 0x77 at the head of PES 2) must still be emitted whole.
|
|
// Previously the lone trailing 0x0B was dropped and the frame lost.
|
|
let mut parser = Ac3Parser::new();
|
|
let frame_data = make_ac3_frame(0, 2); // 160 bytes, starts with 0x0B 0x77
|
|
|
|
// PES 1: a complete frame, then a single 0x0B (first half of next sync).
|
|
let mut pes1_data = frame_data.clone();
|
|
pes1_data.push(0x0B);
|
|
let pes1 = PesPacket {
|
|
pid: 0,
|
|
pts: Some(90000),
|
|
dts: None,
|
|
data: pes1_data,
|
|
};
|
|
let frames1 = parser.parse(&pes1);
|
|
assert_eq!(frames1.len(), 1, "first complete frame emitted");
|
|
|
|
// PES 2: 0x77 (second half of sync) + rest of the second frame.
|
|
let mut pes2_data = vec![0x77];
|
|
pes2_data.extend_from_slice(&frame_data[2..]);
|
|
let pes2 = PesPacket {
|
|
pid: 0,
|
|
pts: Some(93000),
|
|
dts: None,
|
|
data: pes2_data,
|
|
};
|
|
let frames2 = parser.parse(&pes2);
|
|
assert_eq!(frames2.len(), 1, "split-sync frame must be recovered");
|
|
assert_eq!(frames2[0].data.len(), 160);
|
|
}
|
|
|
|
#[test]
|
|
fn buffer_stays_bounded_across_many_garbage_pes() {
|
|
// Finding 14: the carry-over buffer must never grow without bound. Feed
|
|
// many large PES packets that contain no usable frame and assert the
|
|
// retained buffer stays tiny — carry-from-`pos` drops all pre-sync junk,
|
|
// and a never-completing frame is bounded by the 8192-byte frame cap and
|
|
// the MAX_AC3_BUF resync guard.
|
|
let mut parser = Ac3Parser::new();
|
|
for i in 0..256 {
|
|
// Vary the trailing byte so we also exercise the lone-0x0B retain.
|
|
let mut data = vec![0x55u8; 8192];
|
|
if i % 3 == 0 {
|
|
*data.last_mut().unwrap() = 0x0B;
|
|
}
|
|
let pes = PesPacket {
|
|
pid: 0,
|
|
pts: None,
|
|
dts: None,
|
|
data,
|
|
};
|
|
let frames = parser.parse(&pes);
|
|
assert!(frames.is_empty());
|
|
assert!(
|
|
parser.buf.len() <= MAX_AC3_BUF,
|
|
"buffer grew to {} (cap {})",
|
|
parser.buf.len(),
|
|
MAX_AC3_BUF
|
|
);
|
|
}
|
|
// After all that garbage the retained tail is at most a single partial
|
|
// syncword byte — never an accumulation of whole PES packets.
|
|
assert!(parser.buf.len() <= 1, "retained {} bytes", parser.buf.len());
|
|
}
|
|
|
|
#[test]
|
|
fn split_sync_below_cap_is_still_retained() {
|
|
// The cap must not break the normal split-sync straddle: a short tail
|
|
// ending in 0x0B (well under the cap) is retained so the next PES can
|
|
// complete the syncword.
|
|
let mut parser = Ac3Parser::new();
|
|
let data = vec![0x00, 0x00, 0x0B];
|
|
let pes = PesPacket {
|
|
pid: 0,
|
|
pts: None,
|
|
dts: None,
|
|
data,
|
|
};
|
|
assert!(parser.parse(&pes).is_empty());
|
|
assert_eq!(parser.buf, vec![0x0B], "lone trailing 0x0B retained");
|
|
}
|
|
|
|
#[test]
|
|
fn ac3_frame_size_table() {
|
|
// fscod=0 (48kHz), frmsizecod=0: 64 words = 128 bytes
|
|
assert_eq!(ac3_frame_size(&[0x0B, 0x77, 0, 0, 0x00, 0x40]), 128);
|
|
// fscod=0 (48kHz), frmsizecod=2: 80 words = 160 bytes
|
|
assert_eq!(ac3_frame_size(&[0x0B, 0x77, 0, 0, 0x02, 0x40]), 160);
|
|
}
|
|
}
|