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.
This commit is contained in:
Matthew Jackson
2026-07-30 13:39:02 -07:00
parent 55b97ac576
commit 170fd0c064
14 changed files with 1027 additions and 0 deletions
+27
View File
@@ -190,6 +190,33 @@ mod tests {
);
}
/// A dropped ADTS frame is dropped BECAUSE its header failed validation, so
/// the very fields a duration would come from (sampling_frequency_index, and
/// the 1024-samples-per-AAC-frame constant applied to it) are the ones known
/// to be untrustworthy. This gate therefore reports the drop's duration as
/// zero rather than deriving a number from a header it has just rejected —
/// the honest answer, and the one the count alongside it must be read with.
/// A nonzero constant here would report silence that was never measured.
#[test]
fn dropped_frames_are_counted_but_their_duration_is_not_invented() {
let mut parser = AdtsParser::new();
// Three frames whose sampling_frequency_index is a reserved value (13),
// so `adts_verdict` rejects each one.
let mut bad = adts_frame(32);
bad[2] = (bad[2] & 0b1100_0011) | (13 << 2);
for i in 0..3 {
let out = parser.parse(&make_pes(bad.clone(), Some(i * 90_000)));
assert!(out.is_empty(), "an invalid ADTS 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_sample_rate_index_is_dropped() {
// sr_index = 13 (reserved). byte2 bits5..2 = 1101 → 0x34.
+25
View File
@@ -200,6 +200,31 @@ mod tests {
assert!(!t.is_poisoned());
}
/// The poison verdict is a RATIO — verified drops against every AU seen — so
/// the kept count is half of it. `does_not_poison_a_mostly_good_track` above
/// records its keeps AFTER the single drop, and `maybe_poison` only runs
/// inside `record_drop`, so the keeps are never in the denominator when the
/// verdict is actually computed: that test passes even with the kept count
/// never incremented. Interleaving them puts the kept count on the critical
/// path, where losing it turns the ratio into "verified drops vs verified
/// drops" — always >50% — and silently discards a healthy track.
#[test]
fn interleaved_keeps_are_in_the_poison_denominator() {
let mut t = DropTally::new("test");
// 2 kept per 1 dropped, well past the minimum-AU gate: a third of the
// track is undecodable, which is bad but nowhere near the >50% threshold.
for _ in 0..(TRACK_VERDICT_MIN_AUS * 3) {
t.record_kept();
t.record_kept();
t.record_drop(0, 1000, 512, "bad");
assert!(
!t.is_poisoned(),
"33% dropped must never poison, at any point in the run"
);
}
assert_eq!(t.dropped_frames(), TRACK_VERDICT_MIN_AUS * 3);
}
#[test]
fn collateral_drops_never_poison_the_track() {
// A TrueHD resync-forward run collaterally drops a long burst of AUs, but
+75
View File
@@ -1447,6 +1447,81 @@ mod tests {
assert_eq!(h.max_display_mastering_luminance, 10_000_000);
}
/// `num_extra_slice_header_bits` (H.265 §7.3.2.3) is a PPS field, and the
/// `slice_reserved_flag[i]` bits it counts sit BETWEEN
/// `slice_pic_parameter_set_id` and `slice_type` in the slice segment header
/// (§7.3.6.1). Every existing fixture used a PPS with the field == 0, so the
/// skip was never exercised: a parser that ignored the field entirely agreed
/// with all of them, and would then read `slice_type` from the wrong bit
/// offset on any real stream that sets it — mislabelling every picture's
/// coding type.
#[test]
fn nonzero_num_extra_slice_header_bits_shifts_the_slice_type_offset() {
use super::super::coding::CodingType;
// PPS body bits: pps_id ue=0 ('1'), sps_id ue=0 ('1'),
// dependent_slice_segments_enabled_flag 0, output_flag_present_flag 0,
// num_extra_slice_header_bits u(3).
let pps_body = |num_extra: u8| 0b1100_0000u8 | (num_extra << 1);
assert_eq!(pps_body(0), 0xC0, "matches the existing zero-extra fixture");
assert_eq!(pps_body(3), 0xC6);
let pps_nal = |num_extra: u8| {
let mut v = hevc_nal_header(NAL_PPS).to_vec();
v.push(pps_body(num_extra));
v
};
for n in 0..8u8 {
assert_eq!(
hevc_num_extra_slice_header_bits(&pps_nal(n)),
Some(n as u32),
"PPS must yield the value it encodes, for every u(3) code point"
);
}
// A PPS truncated to just its 2-byte NAL header carries no field to read,
// so the answer is absent — never a defaulted zero.
assert_eq!(
hevc_num_extra_slice_header_bits(&hevc_nal_header(NAL_PPS)),
None
);
// Slice segment header for a non-IRAP VCL NAL (TRAIL_R, type 1):
// first_slice_segment_in_pic_flag 1, slice_pic_parameter_set_id ue=0
// ('1'), then THREE reserved bits set to 101 (deliberately not zero, so a
// parser that reads them instead of skipping them cannot agree), then
// slice_type ue(v) = '011' → 2 → I.
let slice_body = 0b1_1_101_011u8;
assert_eq!(slice_body, 0xEB);
let nal = |t: u8, body: u8| {
let mut v = vec![0x00, 0x00, 0x01];
v.extend_from_slice(&hevc_nal_header(t));
v.push(body);
v
};
let mut data = nal(NAL_PPS, pps_body(3));
data.extend_from_slice(&nal(1, slice_body));
let frames = HevcParser::new().parse(&make_pes(data, Some(0)));
assert_eq!(frames.len(), 1);
assert_eq!(
frames[0].coding.expect("PictureInfo").coding_type(),
CodingType::I,
"with num_extra=3 the reserved bits are skipped and slice_type reads 2 (I)"
);
// Control: the SAME slice bytes under a PPS declaring num_extra=0 land on
// a different slice_type — proving the PPS field, not the slice bytes,
// decides the offset.
let mut data0 = nal(NAL_PPS, pps_body(0));
data0.extend_from_slice(&nal(1, slice_body));
let frames0 = HevcParser::new().parse(&make_pes(data0, Some(0)));
assert_eq!(
frames0[0].coding.expect("PictureInfo").coding_type(),
CodingType::B,
"num_extra=0 reads slice_type from bit 2 instead → 0 (B)"
);
}
#[test]
fn hevc_populates_measured_coding_type_and_source() {
use super::super::coding::CodingType;
+57
View File
@@ -284,6 +284,63 @@ mod tests {
}
}
/// A codec parser's `codec_private()` feeds `DiscStream::codec_private`,
/// which the MKV muxer turns directly into the track's `CodecPrivate`
/// element (RFC 9559 §5.1.4.1.24): `Some(bytes)` writes an element holding
/// exactly those bytes, `None` omits the element entirely. The two are NOT
/// interchangeable — a zero-length `CodecPrivate` asserts that the codec's
/// initialisation data IS empty, which is not true of any codec, and a
/// one-byte one asserts a config no decoder can parse.
///
/// The gating parsers (ADTS, MPEG audio, FLAC) and the passthrough parser
/// extract no configuration at all: they validate and forward frames whose
/// configuration is carried in band (ADTS headers, MPEG-1 audio frame
/// headers, FLAC frame headers) or supplied by the source container. Having
/// derived nothing, the only truthful answer they can give is "absent" —
/// any `Some` would be a value they invented. Nothing else in the suite
/// distinguished the two, so each of these impls could have returned a
/// fabricated `Some` and produced a malformed track header unnoticed.
#[test]
fn parsers_that_derive_no_config_report_absent_never_an_empty_codec_private() {
// Codecs whose parsers do no configuration extraction, paired with a
// payload that is a REAL frame of that codec so the gate takes its
// keep-path (a rejected frame proves nothing about the config answer).
let cases: [(Codec, Vec<u8>); 5] = [
// ADTS: syncword FFF1, MPEG-4 AAC-LC, 44.1 kHz, stereo, 7-byte frame.
(Codec::Aac, vec![0xFF, 0xF1, 0x50, 0x80, 0x00, 0xBF, 0xFC]),
// MPEG-1 Layer II, 44.1 kHz, 128 kbit/s, stereo.
(Codec::Mp2, vec![0xFF, 0xFD, 0x70, 0x00, 0x00, 0x00]),
// MPEG-1 Layer III, 44.1 kHz, 128 kbit/s, stereo.
(Codec::Mp3, vec![0xFF, 0xFB, 0x90, 0x00, 0x00, 0x00]),
// Not a FLAC frame sync, so the gate passes it through unvalidated —
// still the keep path, and still no configuration derived.
(Codec::Flac, vec![0x01, 0x02, 0x03, 0x04]),
// Opus rides the all-keyframe passthrough parser.
(Codec::Opus, vec![0x78, 0x01, 0x02, 0x03]),
];
for (codec, payload) in cases {
let mut parser = parser_for_codec(codec, None, false);
assert_eq!(
parser.codec_private(),
None,
"{codec:?}: no config before any frame"
);
parser.parse(&pes(Some(0), payload.clone()));
parser.parse(&pes(Some(90_000), payload));
assert_eq!(
parser.codec_private(),
None,
"{codec:?}: this parser derives no config, so it must report the \
CodecPrivate as ABSENT — an empty or invented Some would be \
written into the track header as if it were real"
);
// Flushing at end of stream must not conjure one either.
parser.flush();
assert_eq!(parser.codec_private(), None, "{codec:?}: after flush");
}
}
#[test]
fn audio_codecs_emit_keyframe_frames() {
// PES = frame audio: every frame is independently decodable → keyframe.
+24
View File
@@ -183,6 +183,30 @@ mod tests {
);
}
/// 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
+34
View File
@@ -341,6 +341,40 @@ mod tests {
assert_eq!(display_set_is_forced(&[]), None);
}
/// `observed()` is the probe's "did I actually see any PGS content?" signal.
/// When it is false the track's forced state is UNKNOWN, and the probe leaves
/// whatever flag the disc's own metadata supplied alone; when it is true the
/// probe overwrites that flag with its own verdict. A tracker that always
/// claims to have observed something therefore lets an unread or undecrypted
/// subtitle track — where `is_forced()` is vacuously false — overwrite a
/// correct vendor "forced" flag with "not forced".
#[test]
fn observed_stays_false_until_a_real_display_set_is_seen() {
let mut t = ForcedTracker::new();
assert!(!t.observed(), "a fresh tracker has seen nothing");
assert!(!t.is_forced(), "and has no verdict to give");
// Blocks that carry no display set must not count as observation: a clear
// PCS (zero composition objects), a non-PCS segment, a truncated PCS, and
// an empty frame.
let mut clear = pcs_display(false);
clear[PCS_NUM_OBJECTS_OFFSET] = 0;
let mut ods = pcs_display(true);
ods[0] = 0x15;
for block in [clear, ods, pcs_display(true)[..15].to_vec(), Vec::new()] {
t.observe(&block);
assert!(
!t.observed(),
"a block with no display set leaves the verdict unknown"
);
}
// The first real display set is what flips it.
t.observe(&pcs_display(true));
assert!(t.observed());
assert!(t.is_forced(), "the only display set seen was forced");
}
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
PesPacket {
source: None,
+73
View File
@@ -752,6 +752,70 @@ mod tests {
}
}
/// The substream count and the substream-directory size are the two numbers
/// that position `mlp_parity_ok`'s check window over the AU header. Every
/// other fixture in this module uses the degenerate shape — one substream,
/// no extraword — so neither function's real behaviour was ever exercised:
/// a constant answer agreed with all of them, and would then mis-window the
/// parity check on the multi-substream AUs that carry 7.1 and Atmos, judging
/// clean audio corrupt (or corrupt audio clean).
#[test]
fn mlp_num_substreams_is_the_top_nibble_of_major_sync_byte_16() {
// Byte 16 of the major sync: top nibble is num_substreams, bottom nibble
// is a different field, so it must not leak into the answer.
for n in 0..16u8 {
let mut ms = vec![0u8; 17];
ms[16] = (n << 4) | 0x0F;
assert_eq!(
mlp_num_substreams(&ms),
Some(n),
"num_substreams is the high nibble only"
);
}
// Real counts: 1 substream for 2.0/5.1 core-only, 4 for the 7.1/Atmos
// layouts this crate has to mux.
let mut ms = vec![0u8; 20];
ms[16] = 0x40;
assert_eq!(mlp_num_substreams(&ms), Some(4));
// A major sync too short to contain byte 16 yields no answer — never a
// defaulted count, which would arm the parity check against garbage.
assert_eq!(mlp_num_substreams(&[0u8; 16]), None);
}
#[test]
fn mlp_substr_header_size_counts_the_extraword_entries() {
// Directory entry: 2 bytes, plus 2 more when the entry's top bit
// (extraword) is set. Build a 4-substream directory that mixes both.
const HDR: usize = 4;
let mut au = vec![0xAAu8; HDR];
au.extend_from_slice(&[0x00, 0x11]); // plain → 2
au.extend_from_slice(&[0x80, 0x22, 0x01, 0x02]); // extraword → 4
au.extend_from_slice(&[0x00, 0x33]); // plain → 2
au.extend_from_slice(&[0x80, 0x44, 0x03, 0x04]); // extraword → 4
au.extend_from_slice(&[0xFFu8; 8]); // payload past the directory
assert_eq!(
mlp_substr_header_size(&au, HDR, 4),
Some(12),
"2 + 4 + 2 + 4"
);
// Same AU, fewer declared substreams → only that many entries counted.
assert_eq!(mlp_substr_header_size(&au, HDR, 1), Some(2));
assert_eq!(mlp_substr_header_size(&au, HDR, 2), Some(6));
assert_eq!(mlp_substr_header_size(&au, HDR, 0), Some(0));
// An all-plain directory is 2 bytes per substream.
let plain = vec![0x00u8; HDR + 8];
assert_eq!(mlp_substr_header_size(&plain, HDR, 4), Some(8));
// A directory that runs past the AU has no answer: the parity window
// would otherwise be placed over bytes that are not there.
let truncated = &au[..HDR + 9];
assert_eq!(mlp_substr_header_size(truncated, HDR, 4), None);
assert_eq!(mlp_substr_header_size(&plain, HDR, 5), None);
}
fn make_truehd_unit(size_bytes: usize) -> Vec<u8> {
let words = size_bytes / 2;
let mut data = vec![0u8; size_bytes];
@@ -994,6 +1058,15 @@ mod tests {
frames.extend(parser.flush());
assert_eq!(frames.len(), 2, "the parity-broken AU is dropped");
assert_eq!(parser.dropped_frames(), 1);
// The drop is a SILENCE GAP, and its length is what the CLI reports as
// lost audio. One TrueHD access unit is 40 samples; at the 48 kHz base
// rate that is 40/48000 s = 833_333 ns. A count without a duration (or a
// duration that ignores the AU rate) understates or invents the loss.
assert_eq!(
parser.dropped_duration_ns(),
833_333,
"one dropped AU = 40 samples at 48 kHz"
);
}
#[test]
+43
View File
@@ -1413,6 +1413,49 @@ mod tests {
assert!(ogm.contains("CHAPTER02NAME=2"));
}
/// Chapter names originate on the disc, which is untrusted input, and the
/// sink drops them straight into an XML document (`<ChapterString>`). Escaping
/// is what keeps a hostile or merely odd name from terminating the element and
/// injecting markup — the document is character data, so XML 1.0 §2.4 requires
/// `&` and `<`, and escaping `>` as well keeps a `]]>` sequence safe too.
///
/// The order matters as much as the set: `&` must be replaced FIRST, otherwise
/// the ampersands introduced by the `<`/`>` replacements get escaped a second
/// time and `<` renders as the literal text `&lt;` instead of a `<`.
#[test]
fn chapter_names_are_xml_escaped_so_a_disc_cannot_inject_markup() {
let chaps = vec![Chapter {
time_secs: 0.0,
name: "</ChapterString><Injected/> Tom & Jerry <3 >:(".to_string(),
}];
let xml = chapters_xml(&chaps);
assert!(
xml.contains(
"<ChapterString>&lt;/ChapterString&gt;&lt;Injected/&gt; \
Tom &amp; Jerry &lt;3 &gt;:(</ChapterString>"
),
"every metacharacter escaped, and `&` escaped first so nothing is \
double-escaped; got:\n{xml}"
);
// The injected element must not survive as markup anywhere in the file.
assert!(
!xml.contains("<Injected/>"),
"a chapter name must not be able to open a new element"
);
// Exactly one ChapterString element pair — the name did not close it early.
assert_eq!(xml.matches("<ChapterString>").count(), 1);
assert_eq!(xml.matches("</ChapterString>").count(), 1);
// A name with no metacharacters passes through byte-identical: escaping
// must not rewrite ordinary text.
let plain = chapters_xml(&[Chapter {
time_secs: 0.0,
name: "Opening Credits".to_string(),
}]);
assert!(plain.contains("<ChapterString>Opening Credits</ChapterString>"));
}
// ── Timeline continuity ──────────────────────────────────────────────────
//
// The corrector itself is tested verbatim in `crate::mux::timeline`. Here we
+87
View File
@@ -630,6 +630,93 @@ mod tests {
assert_eq!(&body[8..12], b"vide", "handler_type must be 'vide'");
}
/// `mdhd` (§8.4.2), `vmhd` (§12.1.2) and `dinf`/`dref` (§8.7.12) are all
/// mandatory in a video track's media tree, and each fixes field values a
/// player relies on. None of them is read back by this crate — the init
/// segment is only ever written — so nothing else constrains them.
#[test]
fn mdhd_vmhd_and_dinf_carry_the_values_the_spec_fixes() {
let buf = init_segment();
let boxes = walk_boxes(&buf);
let (_, moov_start, moov_size) = boxes.iter().find(|(t, _, _)| t == b"moov").unwrap();
let moov_payload = &buf[moov_start + 8..moov_start + moov_size];
let trak = child(moov_payload, b"trak").expect("trak");
let mdia = child(&trak[8..], b"mdia").expect("mdia");
let minf = child(&mdia[8..], b"minf").expect("minf");
// ── mdhd. Version 0 layout: vflags(4) creation(4) modification(4)
// timescale(4) duration(4) language(2) pre_defined(2) = 20 bytes.
let mdhd = child(&mdia[8..], b"mdhd").expect("mdia must carry mdhd");
let body = &mdhd[8..];
assert_eq!(body[0], 0, "mdhd version 0 (32-bit times)");
assert_eq!(
body.len(),
24,
"version-0 mdhd body: vflags(4) creation(4) modification(4) timescale(4) duration(4) language(2) pre_defined(2)"
);
let media_ts = u32::from_be_bytes([body[12], body[13], body[14], body[15]]);
assert_ne!(
media_ts, 0,
"every fragment's tfdt is expressed in this timescale; zero is a divide-by-zero"
);
// The mvhd timescale is the movie clock; this single-track init has one
// clock, so the media timescale must agree with it rather than carry an
// independent copy that can drift.
let mvhd = child(moov_payload, b"mvhd").expect("mvhd");
let mvhd_body = &mvhd[8..];
let movie_ts =
u32::from_be_bytes([mvhd_body[12], mvhd_body[13], mvhd_body[14], mvhd_body[15]]);
assert_eq!(media_ts, movie_ts, "media clock must match the movie clock");
// language: ISO 639-2/T packed as three 5-bit values, bit 15 = 0 (§8.4.2).
let packed = u16::from_be_bytes([body[20], body[21]]);
assert_eq!(packed & 0x8000, 0, "language pad bit must be 0");
let lang: String = (0..3)
.map(|i| (((packed >> (10 - 5 * i)) & 0x1F) as u8 + 0x60) as char)
.collect();
assert_eq!(lang, "und", "unknown language is 'und', not empty or 'eng'");
// ── vmhd. §12.1.2 fixes flags to 1 and the stub picks the neutral
// compositing mode: graphicsmode 0 (copy) with a zero opcolor.
let vmhd = child(&minf[8..], b"vmhd").expect("minf must carry vmhd");
let body = &vmhd[8..];
assert_eq!(body[0], 0, "vmhd version 0");
assert_eq!(
u32::from_be_bytes([0, body[1], body[2], body[3]]),
1,
"vmhd flags must be 1"
);
assert_eq!(body.len(), 12, "vmhd body: vflags(4) mode(2) opcolor(6)");
assert_eq!(
u16::from_be_bytes([body[4], body[5]]),
0,
"graphicsmode copy"
);
assert_eq!(&body[6..12], &[0u8; 6], "opcolor is black");
// ── dinf > dref > 'url ' with flags 1: the media lives in this same
// file/segment, so no external location follows (§8.7.2).
let dinf = child(&minf[8..], b"dinf").expect("minf must carry dinf");
let dref = child(&dinf[8..], b"dref").expect("dinf must carry dref");
let body = &dref[8..];
assert_eq!(
u32::from_be_bytes([body[4], body[5], body[6], body[7]]),
1,
"dref entry_count"
);
let url = child(&body[8..], b"url ").expect("dref must carry a 'url ' entry");
assert_eq!(
u32::from_be_bytes([0, url[9], url[10], url[11]]),
1,
"url flags=1 means self-contained"
);
assert_eq!(
url.len(),
12,
"a self-contained url is header(8) + vflags(4) and no location string"
);
}
#[test]
fn wrap_box_size_includes_header() {
// §4.2: a box's size field counts the full box including the 8-byte
+58
View File
@@ -646,6 +646,64 @@ fn mpegts_crc32(data: &[u8]) -> u32 {
mod tests {
use super::*;
/// Golden vector for an audio PES header (ISO/IEC 13818-1 §2.4.3.7).
///
/// Audio and video differ in TWO fields that no downstream check in this
/// module distinguishes: the stream_id, and whether `PES_packet_length` is
/// filled in or left at the unbounded 0x0000 form. A receiver reads the
/// length to find the end of the access unit without scanning for the next
/// start code, so an audio PES that borrowed the video form (or emitted a
/// stub header) still tiles into valid-looking 188-byte packets and only
/// fails inside a decoder.
#[test]
fn audio_pes_header_is_byte_exact_and_bounded_unlike_video() {
let es = [0xDEu8, 0xAD, 0xBE, 0xEF];
// PTS = 90000 ticks = exactly 1 s at 90 kHz. Encoded across 5 bytes as
// '0010' | PTS[32..30] | marker, PTS[29..15] | marker, PTS[14..0] | marker.
let pes = build_audio_pes(90_000, &es);
assert_eq!(
pes,
vec![
0x00, 0x00, 0x01, // packet_start_code_prefix
0xBD, // stream_id = private_stream_1 (BD audio)
0x00, 0x0C, // PES_packet_length = 3 flag bytes + 5 PTS + 4 ES
0x80, // '10' MPEG-2 marker, no scrambling/priority
0x80, // PTS_DTS_flags = '10' (PTS only)
0x05, // PES_header_data_length = 5
0x21, 0x00, 0x05, 0xBF, 0x21, // PTS 90000 with marker bits
0xDE, 0xAD, 0xBE, 0xEF, // the access unit, verbatim
]
);
// The video builder over the SAME inputs differs in exactly the two
// fields above — proving neither is incidental.
let vid = build_video_pes(90_000, &es);
assert_eq!(vid[3], 0xE0, "video uses the video stream_id");
assert_eq!(
&vid[4..6],
&[0x00, 0x00],
"video length is the unbounded form (a video PES can exceed u16)"
);
assert_eq!(&vid[6..], &pes[6..], "everything after the length matches");
// A different PTS must move the timestamp bytes and nothing else.
let later = build_audio_pes(180_000, &es);
assert_eq!(&later[..9], &pes[..9], "header up to the PTS is unchanged");
assert_ne!(&later[9..14], &pes[9..14], "the PTS bytes change");
assert_eq!(&later[14..], &es, "the access unit is unchanged");
// An access unit too large for the u16 length field falls back to the
// unbounded form rather than writing a truncated (wrapped) length.
let big = vec![0x5Au8; u16::MAX as usize - 7];
let big_pes = build_audio_pes(90_000, &big);
assert_eq!(
&big_pes[4..6],
&[0x00, 0x00],
"8 + es.len() overflows u16 → unbounded length, never a wrapped one"
);
assert_eq!(&big_pes[14..], &big[..]);
}
/// All emitted bytes must align to 188-byte packet boundaries and
/// every packet must start with `0x47`.
fn assert_ts_well_formed(buf: &[u8]) {
+392
View File
@@ -1197,6 +1197,398 @@ mod tests {
assert_eq!(parse_stts(&stts, MAX_SAMPLE_COUNT), vec![1001, 1001, 1001]);
}
// ── Composition offsets (`ctts`) — ISO/IEC 14496-12 §8.6.1.3.
//
// `ctts` is the ONLY place a reordered (B-frame) track's presentation time
// survives the write→read cycle: `stts` carries decode duration and `stss`
// carries sync points, so with the composition table wrong or absent every
// frame's PTS collapses onto its DTS and the picture order is destroyed.
// The writer's builder and this reader's parser must therefore be exact
// inverses, over the SIGNED offsets that make it a version-1 box.
#[test]
fn ctts_build_and_parse_are_exact_inverses_over_signed_offsets() {
// Includes a negative offset (version 1 only), a repeated run, and a
// value repeated non-adjacently — so a parser that loses the sign, drops
// the run expansion, or returns a constant cannot agree.
let offsets: Vec<i32> = vec![0, 2, -1, -1, 0, 3003];
let boxed = crate::mux::mp4::build_ctts(&offsets);
assert_eq!(&boxed[4..8], b"ctts", "box type");
assert_eq!(
boxed[8], 1,
"signed composition offsets require ctts version 1 (§8.6.1.3)"
);
// Run-length coalescing: the two adjacent -1s share one entry, so the
// six offsets become five runs. Size = 8 hdr + 4 ver/flags + 4 count + 5×8.
assert_eq!(
u32::from_be_bytes(boxed[0..4].try_into().unwrap()) as usize,
8 + 4 + 4 + 5 * 8,
"equal ADJACENT offsets coalesce into a single run"
);
assert_eq!(
u32::from_be_bytes(boxed[12..16].try_into().unwrap()),
5,
"entry_count is the run count, not the sample count"
);
// The box header is not part of the parser's input: it takes the payload
// from version/flags onward.
assert_eq!(parse_ctts(&boxed[8..], MAX_SAMPLE_COUNT), offsets);
}
#[test]
fn b_frame_presentation_order_survives_the_mp4_round_trip() {
// Four samples in DECODE order carrying a classic I-P-B-B reorder: the
// second sample presents last. At 25 fps (timescale 25, one tick per
// frame) the composition offsets are [0, +2, -1, -1] — negative, so this
// exercises the signed version-1 path end to end.
//
// Before this test the round trip asserted sample sizes and keyframe
// flags only, so the entire composition-time chain (VideoTiming::ctts,
// build_ctts, parse_ctts) was unconstrained and a demuxed B-frame title
// could have presented in decode order with nothing noticing.
use crate::disc::{
Codec, DiscTitle, FrameRate, HdrFormat, Resolution, Stream as DiscStreamE, VideoStream,
};
use crate::mux::mp4::Mp4Sink;
use crate::pes::{PesFrame, Stream as _};
use std::io::Cursor;
const FRAME_NS: i64 = 40_000_000; // 25 fps, exact
let mut t = DiscTitle::empty();
t.streams = vec![DiscStreamE::Video(VideoStream {
pid: 0x1011,
codec: Codec::Hevc,
resolution: Resolution::R1080p,
frame_rate: FrameRate::F25,
hdr: HdrFormat::Sdr,
color_space: crate::disc::ColorSpace::Unknown,
display_aspect: None,
secondary: false,
label: String::new(),
measured_cicp: None,
})];
t.codec_privates = vec![Some(vec![0xAA, 0xBB, 0xCC, 0xDD, 0xEE])];
// (presentation time, payload byte) in decode order.
let plan: [(i64, u8); 4] = [
(0, 0x10), // I, presents first
(3 * FRAME_NS, 0x20), // P, presents last
(FRAME_NS, 0x30), // B
(2 * FRAME_NS, 0x40), // B
];
let mut buf = Vec::new();
{
let mut sink = Mp4Sink::create(Cursor::new(&mut buf), &t).unwrap();
for (i, &(pts, fill)) in plan.iter().enumerate() {
sink.write(&PesFrame {
track: 0,
pts,
keyframe: i == 0,
data: vec![fill; 64 + i * 8],
duration_ns: None,
source: None,
coding: None,
})
.unwrap();
}
sink.finish().unwrap();
}
let mut rd = Mp4Reader::from_reader(Cursor::new(buf), "reorder".into()).unwrap();
let mut got = Vec::new();
while let Some(f) = rd.read().unwrap() {
got.push((f.pts, f.data[0]));
}
assert_eq!(got.len(), 4);
// Frames come back in decode order (sorted by DTS), each still carrying
// the presentation time it was written with — identified by payload, so
// a reordering of the samples themselves cannot be mistaken for success.
assert_eq!(
got,
vec![
(0, 0x10),
(3 * FRAME_NS, 0x20),
(FRAME_NS, 0x30),
(2 * FRAME_NS, 0x40),
],
"composition times must survive the write→read cycle"
);
// The property that matters to a player: presentation order differs from
// decode order, and sorting by PTS recovers the display sequence.
let mut by_pts = got.clone();
by_pts.sort_by_key(|&(pts, _)| pts);
assert_eq!(
by_pts.iter().map(|&(_, b)| b).collect::<Vec<_>>(),
vec![0x10, 0x30, 0x40, 0x20],
"display order is I,B,B,P — not the decode order I,P,B,B"
);
}
/// A track's declared duration must be its real length. `mdhd.duration` is in
/// the MEDIA timescale and `mvhd.duration` in the movie timescale
/// (ISO/IEC 14496-12 §8.2.2, §8.4.2); a player uses them to draw the seek bar
/// and to decide when the title ends, so a zeroed or constant duration makes
/// a correct file unseekable and apparently empty.
#[test]
fn declared_track_duration_equals_frame_count_times_frame_duration() {
use crate::disc::{
Codec, DiscTitle, FrameRate, HdrFormat, Resolution, Stream as DiscStreamE, VideoStream,
};
use crate::mux::mp4::Mp4Sink;
use crate::pes::{PesFrame, Stream as _};
use std::io::Cursor;
const FRAME_NS: i64 = 40_000_000; // 25 fps, exact
const FRAMES: usize = 5;
let mut t = DiscTitle::empty();
t.streams = vec![DiscStreamE::Video(VideoStream {
pid: 0x1011,
codec: Codec::Hevc,
resolution: Resolution::R1080p,
frame_rate: FrameRate::F25,
hdr: HdrFormat::Sdr,
color_space: crate::disc::ColorSpace::Unknown,
display_aspect: None,
secondary: false,
label: String::new(),
measured_cicp: None,
})];
t.codec_privates = vec![Some(vec![0xAA, 0xBB, 0xCC, 0xDD, 0xEE])];
let mut buf = Vec::new();
{
let mut sink = Mp4Sink::create(Cursor::new(&mut buf), &t).unwrap();
for i in 0..FRAMES {
sink.write(&PesFrame {
track: 0,
pts: i as i64 * FRAME_NS,
keyframe: i == 0,
data: vec![0x55u8; 128],
duration_ns: None,
source: None,
coding: None,
})
.unwrap();
}
sink.finish().unwrap();
}
let moov = find_box(&buf, b"moov").expect("moov");
// mvhd is a version-1 FullBox here: vflags(4) creation(8) modification(8)
// timescale(4) duration(8).
let mvhd = find_box(moov, b"mvhd").expect("mvhd");
assert_eq!(mvhd[0], 1, "mvhd version 1");
let movie_ts = be32(mvhd, 20);
let movie_dur = u64::from_be_bytes(mvhd[24..32].try_into().unwrap());
assert_eq!(movie_ts, 90_000);
assert_eq!(
movie_dur, 18_000,
"5 frames at 25 fps is 0.2 s = 18000 ticks at 90 kHz"
);
// mdhd, same version-1 layout, but in the media timescale the writer chose.
let mdhd = find_box(
find_box(find_box(moov, b"trak").expect("trak"), b"mdia").expect("mdia"),
b"mdhd",
)
.expect("mdhd");
assert_eq!(mdhd[0], 1, "mdhd version 1");
let media_ts = be32(mdhd, 20);
let media_dur = u64::from_be_bytes(mdhd[24..32].try_into().unwrap());
assert_eq!(media_ts, 25, "25 fps snaps to a 25-tick timescale");
assert_eq!(
media_dur, FRAMES as u64,
"duration is one tick per frame in this timescale"
);
assert_eq!(
media_dur as f64 / media_ts as f64,
movie_dur as f64 / movie_ts as f64,
"the two declared durations must describe the same wall-clock length"
);
}
/// The `moov` tree must carry the boxes ISO/IEC 14496-12 makes mandatory for a
/// playable track, with the field values the spec fixes. These live in this
/// module rather than the writer's because the box-walking helpers
/// ([`find_box`]) are here — asserting through them means the test reads the
/// file the way the demuxer does, instead of re-deriving the layout.
///
/// Nothing in the demux path needs `tkhd`/`vmhd`/`smhd`/`dinf`, so an empty
/// one of any of them round-trips through this crate unnoticed while making
/// the file unplayable elsewhere.
#[test]
fn moov_tree_carries_the_mandatory_track_header_and_media_boxes() {
use crate::disc::{
AudioChannels, AudioStream, Codec, DiscTitle, FrameRate, HdrFormat, LabelPurpose,
Resolution, SampleRate, Stream as DiscStreamE, VideoStream,
};
use crate::mux::mp4::Mp4Sink;
use crate::pes::{PesFrame, Stream as _};
use std::io::Cursor;
let mut t = DiscTitle::empty();
t.streams = vec![
DiscStreamE::Video(VideoStream {
pid: 0x1011,
codec: Codec::Hevc,
resolution: Resolution::R1080p,
frame_rate: FrameRate::F25,
hdr: HdrFormat::Sdr,
color_space: crate::disc::ColorSpace::Unknown,
display_aspect: None,
secondary: false,
label: String::new(),
measured_cicp: None,
}),
DiscStreamE::Audio(AudioStream {
pid: 0x1100,
codec: Codec::Ac3,
channels: AudioChannels::Surround51,
language: "eng".into(),
sample_rate: SampleRate::S48,
secondary: false,
purpose: LabelPurpose::Normal,
label: String::new(),
}),
];
t.codec_privates = vec![Some(vec![0xAA, 0xBB, 0xCC, 0xDD, 0xEE]), None];
let ac3 = vec![
0x0B,
0x77,
0x00,
0x00,
0b00_010110,
0b01000_000,
0b111_00_00_1,
0x00,
0xFF,
0xFF,
];
let mut buf = Vec::new();
{
let mut sink = Mp4Sink::create(Cursor::new(&mut buf), &t).unwrap();
for i in 0..4i64 {
sink.write(&PesFrame {
track: 0,
pts: i * 40_000_000,
keyframe: i == 0,
data: vec![0x55u8; 128],
duration_ns: None,
source: None,
coding: None,
})
.unwrap();
sink.write(&PesFrame {
track: 1,
pts: i * 32_000_000,
keyframe: true,
data: ac3.clone(),
duration_ns: None,
source: None,
coding: None,
})
.unwrap();
}
sink.finish().unwrap();
}
let moov = find_box(&buf, b"moov").expect("moov");
let traks = find_boxes_capped(moov, b"trak", usize::MAX);
assert_eq!(traks.len(), 2, "one video trak + one audio trak");
// ── tkhd (§8.3.2). Mandatory in every trak. flags bit 0 = track_enabled;
// a track with flags 0 is ignored by a conforming player. Width/height are
// 16.16 fixed point.
let vid_tkhd = find_box(traks[0], b"tkhd").expect("video tkhd");
assert_eq!(vid_tkhd[0], 1, "tkhd version 1");
let flags = u32::from_be_bytes([0, vid_tkhd[1], vid_tkhd[2], vid_tkhd[3]]);
assert_eq!(flags & 0x1, 0x1, "track_enabled must be set");
assert_eq!(be32(vid_tkhd, 20), 1, "first track_id is 1");
assert_eq!(
be32(vid_tkhd, 88) >> 16,
1920,
"tkhd width is 16.16 fixed point"
);
assert_eq!(be32(vid_tkhd, 92) >> 16, 1080, "tkhd height");
let aud_tkhd = find_box(traks[1], b"tkhd").expect("audio tkhd");
assert_eq!(be32(aud_tkhd, 20), 2, "second track_id is 2");
assert_eq!(
be16(aud_tkhd, 48),
0x0100,
"an audio track's tkhd volume is 1.0 (8.8 fixed), not muted"
);
assert_eq!(
(be32(aud_tkhd, 88), be32(aud_tkhd, 92)),
(0, 0),
"a sound track declares zero visual dimensions"
);
// ── minf media headers: vmhd for video, smhd for audio (§12.1.2, §12.2.2).
// Exactly one of them, and never the wrong one for the handler.
let minf = |trak: &[u8]| -> Vec<u8> {
find_box(find_box(trak, b"mdia").expect("mdia"), b"minf")
.expect("minf")
.to_vec()
};
let vid_minf = minf(traks[0]);
let aud_minf = minf(traks[1]);
let vmhd = find_box(&vid_minf, b"vmhd").expect("video minf must carry vmhd");
assert!(
find_box(&vid_minf, b"smhd").is_none(),
"a video minf must not carry smhd"
);
// §12.1.2 fixes vmhd flags to 1.
assert_eq!(
u32::from_be_bytes([0, vmhd[1], vmhd[2], vmhd[3]]),
1,
"vmhd flags must be 1"
);
assert_eq!(
(be16(vmhd, 4), &vmhd[6..12]),
(0u16, &[0u8; 6][..]),
"graphicsmode 0 (copy) with a zero opcolor"
);
let smhd = find_box(&aud_minf, b"smhd").expect("audio minf must carry smhd");
assert!(
find_box(&aud_minf, b"vmhd").is_none(),
"an audio minf must not carry vmhd"
);
assert_eq!(be16(smhd, 4), 0, "smhd balance is centre");
// ── dinf > dref > "url " with flags 1 = media is in THIS file (§8.7.2).
// Both tracks need it; a missing/empty dref makes the samples unreachable.
for (name, m) in [("video", &vid_minf), ("audio", &aud_minf)] {
let dinf = find_box(m, b"dinf").unwrap_or_else(|| panic!("{name} dinf"));
let dref = find_box(dinf, b"dref").unwrap_or_else(|| panic!("{name} dref"));
assert_eq!(be32(dref, 4), 1, "{name} dref entry_count");
let url = find_box(&dref[8..], b"url ").unwrap_or_else(|| panic!("{name} url "));
// `url ` payload is version(1)+flags(3) only when self-contained.
assert_eq!(
u32::from_be_bytes([0, url[1], url[2], url[3]]),
1,
"{name} url flags=1 (self-contained), so no external name follows"
);
assert_eq!(
url.len(),
4,
"{name} self-contained url carries no location"
);
}
}
// ── Untrusted-input hardening: a crafted MP4 must never panic or over-allocate.
#[test]
+67
View File
@@ -1370,6 +1370,73 @@ mod tests {
assert_eq!(p[0].data, vec![0xDE, 0xAD, 0xBE, 0xEF]);
}
/// `is_nav()` exists to separate the ONE unmappable stream a DVD is expected
/// to contain — private_stream_2 (0xBF), the PCI/DSI navigation packs
/// (ISO/IEC 13818-1 Table 2-22) — from every other packet whose `dvd_pid()`
/// comes back `None`, which is an unexpected, possibly-lost real stream. The
/// mux loops use the distinction to choose between a silent tally and a
/// per-packet WARN, so collapsing it to a constant either buries a genuine
/// stream loss in the nav tally, or floods the log with one warning per
/// navigation pack on every DVD ever ripped.
///
/// The invariant that ties the two together: `is_nav()` may only ever be true
/// where `dvd_pid()` is `None` — a packet that routes to a real track must
/// never be silently classified as navigation.
#[test]
fn only_private_stream_2_is_navigation_and_never_a_routable_stream() {
// Demux a program stream carrying, in order: a navigation pack, MPEG-2
// video, an AC-3 audio substream, and an MPEG audio stream (unmappable on
// DVD, but NOT navigation).
let mut demuxer = PsDemuxer::new();
let mut data = Vec::new();
// private_stream_2: no PES extension, payload follows the 6-byte prefix.
data.extend_from_slice(&[0x00, 0x00, 0x01, 0xBF, 0x00, 0x02, 0x00, 0x01]);
// video 0xE0
data.extend_from_slice(&[
0x00, 0x00, 0x01, 0xE0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x11, 0x22,
]);
// private_stream_1 with AC-3 sub-stream 0x80 (4 bytes of substream header
// follow the sub-id on DVD).
data.extend_from_slice(&[
0x00, 0x00, 0x01, 0xBD, 0x00, 0x0A, 0x80, 0x00, 0x00, 0x80, 0x01, 0x00, 0x03, 0x00,
0xAA, 0xBB,
]);
// MPEG audio 0xC0
data.extend_from_slice(&[
0x00, 0x00, 0x01, 0xC0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x33, 0x44,
]);
data.extend_from_slice(&PROGRAM_END);
let packets = demuxer.feed(&data);
assert_eq!(packets.len(), 4, "four PES packets demuxed");
let nav: Vec<u8> = packets
.iter()
.filter(|p| p.is_nav())
.map(|p| p.stream_id)
.collect();
assert_eq!(
nav,
vec![0xBF],
"exactly the private_stream_2 pack is navigation"
);
for p in &packets {
if p.is_nav() {
assert_eq!(
p.dvd_pid(),
None,
"a navigation pack must not also route to a track"
);
}
}
// The MPEG-audio packet is equally unroutable on DVD, yet must NOT be
// absorbed into the nav tally — that is the distinction being drawn.
let mpa = packets.iter().find(|p| p.stream_id == 0xC0).unwrap();
assert_eq!(mpa.dvd_pid(), None, "MPEG audio is unmappable on DVD");
assert!(!mpa.is_nav(), "...but it is a lost stream, not navigation");
}
#[test]
fn unknown_start_code_is_skipped_not_parsed() {
// A start code with an ID outside the known PS-layer set
+6
View File
@@ -100,7 +100,13 @@ mod tests {
// Gap arrives on the next frame (a P referencing lost data) → drop it
// and every inter frame until the next keyframe.
assert!(!g.admit(true, true, false), "post-gap P dropped");
assert!(
g.is_armed(),
"the gate reports itself armed WHILE it is dropping — this is what \
the consumer reads to know the stream is in a resync hole"
);
assert!(!g.admit(true, false, false), "still dropping (no key yet)");
assert!(g.is_armed(), "still armed mid-run");
assert!(!g.admit(true, false, false));
assert_eq!(g.dropped_in_run(), 3);
// Next keyframe resyncs and is emitted.
+59
View File
@@ -988,6 +988,65 @@ pub fn scan_streams(data: &[u8]) -> Option<Vec<crate::disc::Stream>> {
mod tests {
use super::*;
/// The PSI scanner walks a BD-TS buffer one BYTE at a time until it finds a
/// packet boundary, so `is_resync_point` is the only thing standing between
/// it and a stray 0x47 in a TP_extra_header or a payload. Latching onto one
/// puts every subsequent field read 1..191 bytes out of phase, so the PID and
/// PUSI bits it then decodes belong to nothing — the scanner either invents a
/// stream or loses the real PMT. Accepting every offset (the shape a constant
/// `true` takes) guarantees it latches on the first offset it tries.
#[test]
fn resync_requires_a_corroborating_follower_not_just_one_sync_byte() {
const P: usize = BD_SOURCE_PACKET_BYTES;
// Two well-formed source packets: sync at +4 of each.
let mut two = vec![0u8; 2 * P];
two[4] = SYNC_BYTE;
two[P + 4] = SYNC_BYTE;
assert!(
is_resync_point(&two, 0),
"a real boundary: sync here and 192 bytes on"
);
// No sync byte at all → never a boundary.
two[4] = 0x00;
assert!(!is_resync_point(&two, 0));
two[4] = SYNC_BYTE;
// Sync here, but the next 192-spaced position is not a sync byte: this is
// a 0x47 that happens to sit inside a header or payload, not a boundary.
two[P + 4] = 0x00;
assert!(
!is_resync_point(&two, 0),
"an uncorroborated 0x47 must be rejected"
);
two[P + 4] = SYNC_BYTE;
// The concrete desync the corroboration prevents: a 0x47 byte sitting in
// the payload of the first packet. Its own "sync" test passes, and the
// scanner must still refuse the offset.
let stray = 60usize;
two[stray + 4] = SYNC_BYTE;
assert!(
!is_resync_point(&two, stray),
"a stray 0x47 inside a payload is not a packet boundary"
);
// ...while the genuine boundaries either side still are.
assert!(is_resync_point(&two, 0));
// A lone trailing packet has no follower to corroborate, so it is accepted
// on its own sync byte — otherwise the last packet of every buffer would
// be unreachable.
let mut one = vec![0u8; P];
one[4] = SYNC_BYTE;
assert!(is_resync_point(&one, 0), "last packet in the buffer");
one[4] = 0x00;
assert!(
!is_resync_point(&one, 0),
"...but it still needs its own sync byte"
);
}
#[test]
fn test_parse_timestamp() {
// Example: PTS = 0 → encoded as 21 00 01 00 01