Mutation testing over src/mux/. No production change — 49 survivors killed, all proven red before green. The MP4 composition-time chain was entirely unconstrained: VideoTiming::ctts, build_ctts and parse_ctts could each return a constant and the suite stayed green. Confirmed on HEAD: build_ctts -> vec![] passes all 1,220 mux tests. A demuxed B-frame title presenting in decode order would have shipped. The cause is a test whose name asserts coverage its body does not deliver — stts_and_ctts_expand builds an stts box and never touches ctts, and write_then_read_round_trip asserts sample sizes and keyframe flags but not one PTS. Same shape as the set_speed forwarding finding, different disguise. mlp_num_substreams / mlp_substr_header_size: every TrueHD fixture in the crate uses one substream and no extraword, so both could return a constant and agree with all of them. These position mlp_parity_ok's window over the AU header, so a constant mis-windows the parity check on exactly the multi-substream AUs that carry 7.1 and Atmos. CodecPrivate absent vs empty: mkv.rs writes Some(bytes) verbatim and omits the element on None (RFC 9559 5.1.4.1.24), so a zero-length Some emits a track header asserting the config IS empty. Four parsers could return Some(vec![]) before any frame. Also: mandatory ISO/IEC 14496-12 boxes (tkhd, vmhd, smhd, dinf, mdhd) could each build empty; HEVC num_extra_slice_header_bits (H.265 7.3.2.3) was never non-zero in any fixture, so the slice-type offset skip was unexercised; chapter names from the disc go straight into <ChapterString> and the & escape must run first; a stray 0x47 in a payload must not latch a TS resync. Documented as equivalent rather than killed: CodecParser::flush and the three parser flush bodies that differ from the mutant only by a tracing call, and DropTally::log_summary.
291 lines
10 KiB
Rust
291 lines
10 KiB
Rust
//! MPEG-1/2/2.5 audio (MP1/MP2/MP3) decodability gate.
|
|
//!
|
|
//! Per ISO/IEC 11172-3 / ISO/IEC 13818-3, an MPEG-audio frame is validated by
|
|
//! header sanity + framing resync, not a payload CRC (the optional 16-bit CRC in
|
|
//! the header protects only the side-information and is absent unless the
|
|
//! protection bit says otherwise). The gate mirrors that header-only check and
|
|
//! ACCEPTS free-format (`bitrate_index == 0`) as a legal decodable mode — it
|
|
//! deliberately does NOT apply the stricter free-format reject that a full
|
|
//! decoder would (see the note at the `bitrate_index` check). So the gate rejects
|
|
//! only the truly invalid headers: a packet that begins with the 11-bit
|
|
//! MPEG-audio sync but whose version / layer / sample-rate fields (or the
|
|
//! reserved bitrate index 15) are reserved/invalid is undecodable → drop it (a
|
|
//! silence gap; each packet keeps its own PTS). A packet with no leading sync is
|
|
//! not a frame we can validate (raw payload / continuation), so it passes through
|
|
//! unchanged — never false-dropped.
|
|
|
|
use super::dropgate::DropTally;
|
|
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
|
|
|
|
/// Decoded validity of a candidate MPEG-audio header.
|
|
enum MpaVerdict {
|
|
/// No 11-bit sync at the packet head — not a frame we can validate.
|
|
NoSync,
|
|
/// Sync present and every field is legal — decodable.
|
|
Valid,
|
|
/// Sync present but a field is reserved/invalid — a conformant header parser
|
|
/// rejects this exactly.
|
|
Invalid,
|
|
}
|
|
|
|
/// Header-only validity check per ISO/IEC 11172-3 / ISO/IEC 13818-3 (which
|
|
/// ACCEPTS free-format, `bitrate_index == 0`) — deliberately NOT the stricter
|
|
/// free-format reject a full decoder applies. A dropped MPEG-audio frame has a
|
|
/// corrupt header, so no duration is computed (the fields it would come from are
|
|
/// the invalid ones).
|
|
fn mpa_verdict(data: &[u8]) -> MpaVerdict {
|
|
if data.len() < 4 {
|
|
return MpaVerdict::NoSync;
|
|
}
|
|
let h = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
|
|
// 11-bit sync (0x7FF at the top).
|
|
if (h & 0xffe0_0000) != 0xffe0_0000 {
|
|
return MpaVerdict::NoSync;
|
|
}
|
|
// Reject per spec: version field 01, layer field 00, bitrate_index 15,
|
|
// sample-rate field 3.
|
|
if (h & (3 << 19)) == (1 << 19)
|
|
|| (h & (3 << 17)) == 0
|
|
|| (h & (0xf << 12)) == (0xf << 12)
|
|
|| (h & (3 << 10)) == (3 << 10)
|
|
{
|
|
return MpaVerdict::Invalid;
|
|
}
|
|
// NOTE: bitrate_index == 0 (free format) is NOT rejected. It is a legal,
|
|
// decodable MPEG-audio mode (the spec permits it and a decoder derives the
|
|
// frame size from the sync spacing). Dropping it would be a false positive on
|
|
// a clean stream, so it passes the gate.
|
|
MpaVerdict::Valid
|
|
}
|
|
|
|
pub struct MpegAudioParser {
|
|
tally: DropTally,
|
|
/// Last emitted PTS (ns), carried forward across a PES with no PTS rather than
|
|
/// resetting the timeline to 0 (see the AC-3/DTS parsers) — preserves A/V sync.
|
|
last_pts_ns: i64,
|
|
}
|
|
|
|
impl Default for MpegAudioParser {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl MpegAudioParser {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
tally: DropTally::new("mpegaudio"),
|
|
last_pts_ns: 0,
|
|
}
|
|
}
|
|
|
|
pub fn dropped_frames(&self) -> u64 {
|
|
self.tally.dropped_frames()
|
|
}
|
|
|
|
pub fn dropped_duration_ns(&self) -> u64 {
|
|
self.tally.dropped_duration_ns()
|
|
}
|
|
}
|
|
|
|
impl CodecParser for MpegAudioParser {
|
|
fn parse(&mut self, pes: &PesPacket) -> Vec<Frame> {
|
|
if pes.data.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
let pts_ns = pes
|
|
.pts
|
|
.or(pes.dts)
|
|
.map(pts_to_ns)
|
|
.unwrap_or(self.last_pts_ns);
|
|
self.last_pts_ns = pts_ns;
|
|
|
|
let drop =
|
|
self.tally.is_poisoned() || matches!(mpa_verdict(&pes.data), MpaVerdict::Invalid);
|
|
if drop {
|
|
let reason = if self.tally.is_poisoned() {
|
|
"track-poisoned"
|
|
} else {
|
|
"header"
|
|
};
|
|
self.tally.record_drop(pts_ns, 0, pes.data.len(), reason);
|
|
return Vec::new();
|
|
}
|
|
|
|
self.tally.record_kept();
|
|
vec![Frame {
|
|
discontinuity: pes.discontinuity,
|
|
coding: None,
|
|
source: None,
|
|
pts_ns,
|
|
keyframe: true,
|
|
data: pes.data.clone(),
|
|
duration_ns: None,
|
|
}]
|
|
}
|
|
|
|
fn flush(&mut self) -> Vec<Frame> {
|
|
self.tally.log_summary();
|
|
Vec::new()
|
|
}
|
|
|
|
fn codec_private(&self) -> Option<Vec<u8>> {
|
|
None
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
|
|
PesPacket {
|
|
source: None,
|
|
pid: 0x1100,
|
|
pts,
|
|
dts: None,
|
|
data,
|
|
discontinuity: false,
|
|
}
|
|
}
|
|
|
|
/// A valid MPEG-1 Layer III header: sync 0xFFF, version MPEG-1 (11), layer
|
|
/// III (01), bitrate_index 9, sample-rate 0 (44.1 kHz), no CRC. Bytes:
|
|
/// 0xFF 0xFB 0x90 0x00 — the canonical MP3 frame header.
|
|
fn mp3_frame(payload: usize) -> Vec<u8> {
|
|
let mut f = vec![0xFF, 0xFB, 0x90, 0x00];
|
|
f.extend(std::iter::repeat(0xAA).take(payload));
|
|
f
|
|
}
|
|
|
|
#[test]
|
|
fn valid_header_is_kept() {
|
|
let mut p = MpegAudioParser::new();
|
|
let f = p.parse(&make_pes(mp3_frame(400), Some(90000)));
|
|
assert_eq!(f.len(), 1);
|
|
assert_eq!(f[0].pts_ns, pts_to_ns(90000));
|
|
assert_eq!(p.dropped_frames(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn pes_without_pts_carries_last_timestamp_not_zero() {
|
|
// A PES with no PTS (legal for audio, e.g. after a discontinuity) must
|
|
// carry the last known timestamp forward — resetting to 0 would corrupt
|
|
// A/V sync. Mirrors the adts.rs guard test.
|
|
let mut p = MpegAudioParser::new();
|
|
p.parse(&make_pes(mp3_frame(400), Some(90000)));
|
|
let f = p.parse(&make_pes(mp3_frame(400), None));
|
|
assert_eq!(f.len(), 1);
|
|
assert_eq!(
|
|
f[0].pts_ns,
|
|
pts_to_ns(90000),
|
|
"carried forward, not reset to 0"
|
|
);
|
|
}
|
|
|
|
/// Same contract as the ADTS gate: an MPEG-audio frame is dropped because
|
|
/// its header is invalid, and the header is exactly where a frame duration
|
|
/// (samples-per-frame for the layer, divided by the sampling rate) would
|
|
/// have to come from — see `mpa_verdict`'s doc comment. So the drop's
|
|
/// duration is reported as zero, not derived from rejected fields.
|
|
#[test]
|
|
fn dropped_frames_are_counted_but_their_duration_is_not_invented() {
|
|
let mut parser = MpegAudioParser::new();
|
|
// Reserved layer field (00) — rejected per ISO/IEC 11172-3.
|
|
let mut bad = mp3_frame(32);
|
|
bad[1] &= !0b0000_0110;
|
|
for i in 0..3 {
|
|
let out = parser.parse(&make_pes(bad.clone(), Some(i * 90_000)));
|
|
assert!(out.is_empty(), "an invalid MPEG-audio frame is not emitted");
|
|
}
|
|
assert_eq!(parser.dropped_frames(), 3, "every drop is counted");
|
|
assert_eq!(
|
|
parser.dropped_duration_ns(),
|
|
0,
|
|
"the duration comes from the header that just failed validation, so \
|
|
it is reported as unmeasured rather than guessed"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn reserved_version_field_is_dropped() {
|
|
// version field = 01 (reserved) → rejected. byte1 = 111_01_01_1 = 0xEB
|
|
// keeps the 11-bit sync (0xFF + top 3 bits 111) but sets version bits to 01.
|
|
let mut p = MpegAudioParser::new();
|
|
let mut frame = mp3_frame(400);
|
|
frame[1] = 0xEB;
|
|
let f = p.parse(&make_pes(frame, Some(90000)));
|
|
assert!(f.is_empty(), "reserved version dropped");
|
|
assert_eq!(p.dropped_frames(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn reserved_sample_rate_is_dropped() {
|
|
// Sync present but sample-rate field = 3 (reserved) → rejected per spec.
|
|
// 0xFF 0xFB then byte2 with bits 11..10 = 11: 0x9C.
|
|
let mut p = MpegAudioParser::new();
|
|
let mut frame = mp3_frame(400);
|
|
frame[2] = 0x9C; // freq field = 3
|
|
let f = p.parse(&make_pes(frame, Some(90000)));
|
|
assert!(f.is_empty(), "reserved sample rate dropped");
|
|
assert_eq!(p.dropped_frames(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn reserved_layer_is_dropped() {
|
|
// Layer field 00 (reserved). byte1 bits 2..1 = 00 → 0xF9 keeps sync
|
|
// (0xFFF needs byte1 top 3 bits set) and sets layer=00.
|
|
let mut p = MpegAudioParser::new();
|
|
let mut frame = mp3_frame(400);
|
|
frame[1] = 0xF9; // 1111_1001: sync ok (top 3 =111), version 11, layer 00
|
|
let f = p.parse(&make_pes(frame, Some(0)));
|
|
assert!(f.is_empty(), "reserved layer dropped");
|
|
assert_eq!(p.dropped_frames(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn bad_bitrate_index_15_is_dropped() {
|
|
let mut p = MpegAudioParser::new();
|
|
let mut frame = mp3_frame(400);
|
|
frame[2] = 0xF0; // bitrate_index = 1111
|
|
assert!(p.parse(&make_pes(frame, Some(0))).is_empty());
|
|
assert_eq!(p.dropped_frames(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn free_format_bitrate_zero_is_kept() {
|
|
// Free format (bitrate_index == 0) is legal and decodable — it must NOT
|
|
// be dropped (that would be a false positive on a clean stream).
|
|
let mut p = MpegAudioParser::new();
|
|
let mut frame = mp3_frame(400);
|
|
frame[2] = 0x00; // bitrate_index = 0000 (free format); sync/layer/rate ok
|
|
let f = p.parse(&make_pes(frame, Some(0)));
|
|
assert_eq!(f.len(), 1, "free-format frame kept");
|
|
assert_eq!(p.dropped_frames(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn non_sync_packet_passes_through() {
|
|
// No 11-bit sync → not a validatable frame → keep (conservative).
|
|
let mut p = MpegAudioParser::new();
|
|
let f = p.parse(&make_pes(vec![0x00, 0x11, 0x22, 0x33, 0x44], Some(0)));
|
|
assert_eq!(f.len(), 1);
|
|
assert_eq!(p.dropped_frames(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn drop_preserves_sync_via_own_pts() {
|
|
let mut p = MpegAudioParser::new();
|
|
let mut bad = mp3_frame(400);
|
|
bad[2] = 0x9C; // reserved sample rate
|
|
assert!(p.parse(&make_pes(bad, Some(90000))).is_empty());
|
|
let f = p.parse(&make_pes(mp3_frame(400), Some(96000)));
|
|
assert_eq!(f.len(), 1);
|
|
assert_eq!(
|
|
f[0].pts_ns,
|
|
pts_to_ns(96000),
|
|
"next frame keeps its own PTS"
|
|
);
|
|
}
|
|
}
|