Merge branch 'mux-codec' into dev

This commit is contained in:
Matthew Jackson
2026-08-01 13:47:15 -07:00
4 changed files with 321 additions and 19 deletions
+120
View File
@@ -863,6 +863,54 @@ mod tests {
// frames advance by. (512 * 1e9 + 24000) / 48000 = 10_666_667 ns.
const DTS_CORE_DUR_NS: i64 = (512 * 1_000_000_000 + 48_000 / 2) / 48_000;
// --- stamp_pts: the "same PES, no fresh front timestamp" branch ---
/// `stamp_pts`'s third arm (`else if front != PTS_UNSET`) is only reached
/// once the first arm's `front != self.last_front_pts` has already failed
/// (i.e. `front == self.last_front_pts`) and no within-PES running cursor
/// is available yet (`next_pts_ns == PTS_UNSET`). In that state the AU must
/// still be stamped with its own (repeated) front timestamp, not silently
/// collapsed to 0 — a `!=` -> `==` typo on the guard would only take this
/// arm when `front` IS the unset sentinel, producing 0 instead here and the
/// sentinel value itself if front really were unset.
#[test]
fn stamp_pts_reuses_front_when_no_running_cursor_yet() {
let mut parser = DtsParser::new();
parser.last_front_pts = 500;
parser.next_pts_ns = PTS_UNSET;
assert_eq!(
parser.stamp_pts(500, DTS_CORE_DUR_NS),
500,
"front == last_front_pts and no cursor yet: stamp with front itself"
);
}
// --- drain_front: collapsing duplicate offset-0 PTS markers must not leak ---
/// Every drained access unit that shares its PES with the previous one
/// pushes a new `(0, pts)` marker once rebased; `drain_front` must collapse
/// those down to the single most-recent one each time, or `pts_marks` grows
/// once per access unit for the life of the track — an unbounded allocation
/// on a multi-hour disc. `front_pts()` alone can't observe this: it always
/// finds the last offset-0 entry regardless of how many duplicates precede
/// it, so the leak is invisible unless something checks that the collapse
/// actually ran.
#[test]
fn drain_front_collapses_offset_zero_markers_instead_of_leaking() {
let mut parser = DtsParser::new();
for i in 0..200i64 {
parser.buf.extend_from_slice(&[0u8; 5]);
parser.pts_marks.push_back((5, i));
parser.pts_marks.push_back((5, i));
parser.drain_front(5);
}
assert!(
parser.pts_marks.len() <= 2,
"pts_marks must stay bounded across repeated drains, got {}",
parser.pts_marks.len()
);
}
/// A real DTS-HD EXSS substream of `total` bytes (short header form), with an
/// optional false DTS core syncword embedded in its payload (decoding to a
/// plausible core size) — to prove precise sizing, not a payload scan, bounds
@@ -1589,6 +1637,78 @@ mod tests {
assert_eq!(dts_core_frame_size(&d), 0x3FFF + 1);
}
// --- dts_core_samples / dts_core_sample_rate: header-length boundary ---
#[test]
fn dts_core_samples_reads_nblks_high_bit_from_byte4_bit0() {
// NBLKS is 7 bits: byte4 bit0 is the HIGH bit (<<6), byte5>>2 the low 6.
// With byte4 bit0 set and byte5 = 0, nblks = 64 (not 0), so a `<<` -> `>>`
// typo on the byte4 contribution (which always yields 0, since a 1-bit
// value has nothing to shift right into) collapses samples from 2080
// down to 32.
let mut d = vec![0u8; CORE_HEADER_MIN_BYTES];
d[4] = 0x01;
d[5] = 0x00;
assert_eq!(dts_core_samples(&d), (64 + 1) * 32);
}
#[test]
fn dts_core_samples_and_sample_rate_decode_at_exact_header_min_length() {
// The length guard is `data.len() < CORE_HEADER_MIN_BYTES`; exactly
// CORE_HEADER_MIN_BYTES (10) bytes must still be read as a real header
// (index 8, SFREQ, is in bounds), not treated as truncated. Use NBLKS
// and SFREQ values that differ from the truncated-header fallbacks
// (512 samples, 48 kHz) so a `<` -> `<=` typo that takes the fallback
// path one byte early is visible in the output, not masked by a
// coincidental match.
let mut d = vec![0u8; CORE_HEADER_MIN_BYTES];
d[4] = 0x00;
d[5] = 0x00; // nblks = 0 -> 32 samples, not the 512 fallback
d[8] = 6u8 << 2; // SFREQ = 6 -> 11_025 Hz, not the 48_000 fallback
assert_eq!(d.len(), CORE_HEADER_MIN_BYTES);
assert_eq!(dts_core_samples(&d), 32);
assert_eq!(dts_core_sample_rate(&d), 11_025);
}
#[test]
fn dts_core_samples_and_sample_rate_decode_past_header_min_length_too() {
// A `<` -> `>` typo on the same guard takes the FALLBACK path once
// `data.len()` exceeds CORE_HEADER_MIN_BYTES instead of never — i.e.
// every real, normally-sized core frame (always well past 10 bytes)
// would silently report the 512-sample/48kHz fallback. Use a buffer
// twice the minimum with non-fallback NBLKS/SFREQ values.
let mut d = vec![0u8; CORE_HEADER_MIN_BYTES * 2];
d[4] = 0x00;
d[5] = 0x00; // nblks = 0 -> 32 samples
d[8] = 6u8 << 2; // SFREQ = 6 -> 11_025 Hz
assert!(d.len() > CORE_HEADER_MIN_BYTES);
assert_eq!(dts_core_samples(&d), 32);
assert_eq!(dts_core_sample_rate(&d), 11_025);
}
// --- next_core_boundary: SYNCWORD_BYTES length guard ---
/// Only the 4-byte EXSS syncword is buffered after the core frame — no
/// header fields at all. The guard `buf.len() < pos + SYNCWORD_BYTES` must
/// be strict `<`: at exactly `pos + SYNCWORD_BYTES` the sync bytes ARE
/// fully present, so the function proceeds to identify them as an
/// extension sync (then fails to size the header and falls back, ending
/// in `NextCore::None` here since nothing after it looks like a core sync
/// either). A `<=` typo would instead return `NeedMore` at this exact
/// length without ever inspecting what the 4 buffered bytes are.
#[test]
fn next_core_boundary_exact_syncword_length_is_not_need_more() {
let core = make_dts_core(MIN_CORE_FRAME_BYTES);
let mut buf = core.clone();
buf.extend_from_slice(&DTS_HD_EXT_SYNC); // exactly 4 bytes, nothing more
let result = next_core_boundary(&buf, core.len());
assert!(
matches!(result, NextCore::None),
"expected None (recognized-but-unsizeable extension sync with \
nothing after it), got a different variant"
);
}
// --- find_sync ---
#[test]
+69 -18
View File
@@ -141,10 +141,29 @@ thread_local! {
/// off the per-frame allocation path for anything larger.
fn unescape_ebsp_prefix(ebsp: &[u8]) -> Vec<u8> {
const PREFIX_OCTETS: usize = 16;
let mut out = Vec::with_capacity(PREFIX_OCTETS);
unescape_ebsp(ebsp, PREFIX_OCTETS)
}
/// Copy `ebsp` with emulation-prevention bytes removed, stopping after at
/// most `max_octets` OUTPUT bytes (pass `ebsp.len()` for "no cap").
///
/// The zero run-length counter is cumulative across the whole scan and is
/// reset only when an escape byte is actually dropped — matching the
/// reference decoding process (ITU-T H.264 §7.3.1 / the libavcodec RBSP
/// extractor), which discards a 0x03 following ANY run of two-or-more 0x00
/// bytes, not just an exact `00 00 03` sliding window. A window-based scanner
/// that advances by 3 on a match and by 1 otherwise disagrees with this on a
/// run of 3+ real zero bytes ahead of an 0x03: `00 00 00 03` decodes here as
/// `00 00 00` (the run is escaped, matching every other H.264 decoder), where
/// a window scan starting fresh after each non-match would keep the 0x03 as
/// real payload. Only one implementation of this rule may exist in this
/// module — see `parse_sps_high_profile_ext`, which used to keep its own
/// window-scan copy that disagreed with this one on exactly that input.
fn unescape_ebsp(ebsp: &[u8], max_octets: usize) -> Vec<u8> {
let mut out = Vec::with_capacity(max_octets.min(ebsp.len()));
let mut zeros = 0usize;
for &b in ebsp {
if out.len() == PREFIX_OCTETS {
if out.len() == max_octets {
break;
}
// Drop the escape octet itself, but only in the 00 00 03 position.
@@ -493,23 +512,12 @@ impl CodecParser for H264Parser {
/// omits the extension in that case.
fn parse_sps_high_profile_ext(sps: &[u8]) -> Option<(u8, u8, u8)> {
// Strip emulation-prevention bytes: 00 00 03 xx → 00 00 xx (drop the 03).
// We skip byte 0 (NAL header) and start the RBSP from byte 1.
let rbsp: Vec<u8> = {
// We skip byte 0 (NAL header) and start the RBSP from byte 1. Shares
// `unescape_ebsp` with the slice-header prefix reader rather than
// re-deriving the same rule — see that function's doc comment for why a
// second, window-based copy of this rule used to disagree with it.
let raw = &sps[1..]; // skip NAL header byte
let mut out = Vec::with_capacity(raw.len());
let mut i = 0;
while i < raw.len() {
if i + 2 < raw.len() && raw[i] == 0x00 && raw[i + 1] == 0x00 && raw[i + 2] == 0x03 {
out.push(0x00);
out.push(0x00);
i += 3; // skip the 0x03 emulation-prevention byte
} else {
out.push(raw[i]);
i += 1;
}
}
out
};
let rbsp: Vec<u8> = unescape_ebsp(raw, raw.len());
// RBSP layout after stripping the NAL header byte:
// [0] profile_idc (already checked by caller)
@@ -747,6 +755,26 @@ mod tests {
);
}
/// Regression: `parse_sps_high_profile_ext` used to re-derive the
/// emulation-prevention rule with its own window scanner (match `00 00
/// 03` at position i, advance by 3; else advance by 1) instead of calling
/// the shared `unescape_ebsp`. On a run of 3+ real 0x00 bytes ahead of an
/// 0x03 — non-conformant, but this is untrusted disc input, not a
/// spec-clean encoder — the two disagreed: `unescape_ebsp`'s cumulative
/// zero counter (the same rule libavcodec's RBSP extractor and the H.264
/// reference decoding process use) drops the 0x03 as an escape, while a
/// fresh 3-byte window starting right after the non-matching first byte
/// kept it as real payload. Pin the shared function's behaviour here so a
/// second hand-rolled copy doesn't quietly reappear.
#[test]
fn unescape_ebsp_drops_escape_after_a_run_of_three_zeros() {
assert_eq!(
super::unescape_ebsp(&[0x00, 0x00, 0x00, 0x03, 0x42], 5),
vec![0x00, 0x00, 0x00, 0x42],
"the 0x03 after a 3-zero run is an escape byte, not payload"
);
}
// --- keyframe parameter-set re-assert: exact bytes + no whole-frame copy ---
/// The keyframe SPS/PPS re-assert must produce EXACTLY these bytes: the
@@ -2011,4 +2039,27 @@ mod tests {
expected_len
);
}
/// `SpsReader::read_bits` shifts each new bit into the low end of the
/// accumulator (`val << 1 | bit`). Pins the direction directly: a
/// `<<` -> `>>` typo would leave every accumulated bit shifted out and
/// the result would collapse towards 0 instead of building up the value.
#[test]
fn sps_reader_read_bits_builds_value_msb_first() {
// 0b1011_0000 read 4 bits MSB-first -> 0b1011 = 11.
let mut r = super::SpsReader::new(&[0b1011_0000]);
assert_eq!(r.read_bits(4), Some(0b1011));
}
/// `SpsReader::read_ue`'s truncation guard is `leading_zeros > 31`: 31
/// leading zero bits is the longest legal code and must decode, not
/// abort. Mirrors the equivalent guard in the shared `BitReader` in
/// `startcode.rs`, kept here because `SpsReader` is a separate,
/// unshared implementation used only for SPS parsing.
#[test]
fn sps_reader_read_ue_thirty_one_leading_zeros_is_still_valid() {
let data = [0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00];
let mut r = super::SpsReader::new(&data);
assert_eq!(r.read_ue(), Some(u32::MAX >> 1));
}
}
+106
View File
@@ -1250,6 +1250,76 @@ mod tests {
assert_eq!(h.max_pic_average_light_level, maxfall);
}
/// The `scan_sei` match arms are guarded with `self.sei_mastering.is_none()`
/// / `self.sei_content_light.is_none()`, so the FIRST mastering-display and
/// content-light SEI a title carries wins and later repeats (every AU of a
/// real HDR10 stream repeats both) are ignored. A splice or a corrupt later
/// AU that carries different (or garbage) HDR10 numbers must not overwrite
/// the value already latched from the title's first AU.
///
/// Each AU below carries only ONE of the two messages, so `sei_mastering`
/// and `sei_content_light` are never BOTH `Some` at once — the whole-scan
/// early return a few lines above these two match arms
/// (`if self.sei_mastering.is_some() && self.sei_content_light.is_some()`)
/// would otherwise fire on the second AU and make the per-arm guards
/// unreachable, proving nothing about them.
#[test]
fn hevc_hdr10_sei_keeps_the_first_value_and_ignores_later_repeats() {
let pps = {
let mut v = vec![0x00, 0x00, 0x01];
v.extend_from_slice(&hevc_nal_header(NAL_PPS));
v.push(0xC0);
v
};
let idr = {
let mut v = vec![0x00, 0x00, 0x01];
v.extend_from_slice(&hevc_nal_header(19));
v.push(0xEC);
v
};
let mastering_au = |max_lum: u32| {
let mut data = pps.clone();
data.extend_from_slice(&sei_nal(&[sei_message(
SEI_MASTERING_DISPLAY_COLOUR_VOLUME,
&mastering_payload([1, 2, 3], [4, 5, 6], 7, 8, max_lum, 10),
)]));
data.extend_from_slice(&idr);
data
};
let cll_au = |maxcll: u16, maxfall: u16| {
let mut data = pps.clone();
data.extend_from_slice(&sei_nal(&[sei_message(
SEI_CONTENT_LIGHT_LEVEL_INFO,
&cll_payload(maxcll, maxfall),
)]));
data.extend_from_slice(&idr);
data
};
let mut mastering_only = HevcParser::new();
mastering_only.parse(&make_pes(mastering_au(10_000_000), Some(0)));
mastering_only.parse(&make_pes(mastering_au(1), Some(3750)));
assert_eq!(
mastering_only
.sei_mastering
.map(|m| m.max_display_mastering_luminance),
Some(10_000_000),
"the SECOND AU's mastering-luminance must be ignored, not adopted"
);
let mut cll_only = HevcParser::new();
cll_only.parse(&make_pes(cll_au(1000, 400), Some(0)));
cll_only.parse(&make_pes(cll_au(9999, 9999), Some(3750)));
assert_eq!(
cll_only.sei_content_light,
Some(ContentLightLevel {
max_content_light_level: 1000,
max_pic_average_light_level: 400
}),
"the SECOND AU's content-light numbers must be ignored, not adopted"
);
}
/// MEASURED, not reasoned: an HDR10 stream carries a prefix SEI per access
/// unit, and `scan_sei` allocated + byte-copied the whole SEI RBSP through
/// `strip_emulation_prevention` on EVERY one — including after both HDR10
@@ -2520,6 +2590,26 @@ mod tests {
assert_eq!(len + 4, fd.len(), "exactly one NAL in frame data");
}
/// The trailing-zero strip after the last NAL in the buffer (`while end >
/// nal_start && data[end - 1] == 0x00 { end -= 1; }`) must make strictly
/// positive progress: this NAL has no start code after it (`next` falls
/// back to `data.len()`), so a step in the wrong direction walks `end`
/// past the end of `data` and the very next iteration indexes
/// `data[end - 1]` out of bounds. Zero-padding after the last NAL is
/// exactly what a damaged trailing sector on disc looks like.
#[test]
fn trailing_zero_strip_on_last_nal_does_not_run_past_the_buffer() {
let mut parser = HevcParser::new();
let mut data = vec![0x00, 0x00, 0x01];
data.extend_from_slice(&hevc_nal_header(NAL_AUD));
data.push(0x00); // damaged/zero-padded trailing byte, no start code follows
let frames = parser.parse(&make_pes(data, Some(0)));
// AUD is dropped and the payload is otherwise empty once the padding
// is stripped, so there is nothing to emit — the assertion that
// matters is that `parse` returned at all instead of panicking.
assert!(frames.is_empty());
}
// --- empty PES ---
#[test]
@@ -3189,6 +3279,22 @@ mod tests {
assert!(parse_sps_chroma(&[0x42, 0x01]).is_none());
}
/// A mastering-display SEI payload one byte short of the fixed 24-byte
/// layout must be rejected, not read out of bounds. This is the guard a
/// crafted/truncated SEI on a damaged disc hits directly.
#[test]
fn parse_mastering_display_one_byte_short_is_none() {
assert!(parse_mastering_display(&[0u8; 23]).is_none());
assert!(parse_mastering_display(&[0u8; 24]).is_some());
}
/// Same guard, content-light-level's 4-byte layout.
#[test]
fn parse_content_light_level_one_byte_short_is_none() {
assert!(parse_content_light_level(&[0u8; 3]).is_none());
assert!(parse_content_light_level(&[0u8; 4]).is_some());
}
#[test]
fn hvcc_falls_back_to_8bit_420_on_unparseable_sps() {
// An SPS whose RBSP is truncated mid-parse (can't reach the bit depths)
+25
View File
@@ -315,4 +315,29 @@ mod tests {
let data = [0x00, 0x01, 0x01, 0x65];
assert_eq!(skip_start_code(&data, 0), None);
}
#[test]
fn skip_start_code_three_zeros_no_terminator_is_not_a_code() {
// `00 00 00` with nothing after it is neither a 3-byte code (3rd byte
// isn't 0x01) nor a complete 4-byte code (there is no 4th byte at all).
// Pins the `pos + 3 < data.len()` guard: on a 3-byte all-zero buffer
// `pos + 3 == data.len()`, so the guard must be strict `<` and reject
// the 4-byte branch before it would index one past the end. A `<=`
// (or a `pos * 3` typo, which also evaluates to a value not exceeding
// `data.len()` here) lets the 4-byte branch run and read `data[pos+3]`
// out of bounds.
let data = [0x00, 0x00, 0x00];
assert_eq!(skip_start_code(&data, 0), None);
}
#[test]
fn read_ue_thirty_one_leading_zeros_is_still_a_valid_code() {
// Exactly 31 leading zero bits, a stop bit, then 31 zero info bits:
// a legal (if enormous) ue(v) code with code_num = 2^31 - 1. The
// truncation guard is `leading_zeros > 31`, so 31 must NOT trip it —
// only 32 does. A guard mutated to `>= 31` or `== 31` aborts one bit
// early and returns None instead of the real value.
let data = [0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00];
assert_eq!(BitReader::new(&data).read_ue(), Some(u32::MAX >> 1));
}
}