Files
libfreemkv/src/mux/codec/mpegaudio.rs
T
Matthew Jackson 170fd0c064 test: constrain MP4 composition timing, MLP substream directory, and codec-private absence
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.
2026-07-30 13:39:02 -07:00

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"
);
}
}