test: constrain the DiscStream loss surface and the empty-title guards

Second mutation pass over src/mux/. 26 survivors killed, no production
change. Verified on HEAD before landing: each mutation below passes all
1,237 mux tests unmutated-suite.

The priority item was the honest-loss-reporting surface. Both
DiscStream::errors and DiscStream::lost_bytes could return a constant
with nothing failing — a rip that lost sectors would report zero loss
to the caller. This project has already shipped one defect of that
shape (a total decryption failure reported as an empty title, exit 0).
Driven now through two short-read fills so both land on values that are
neither 0 nor 1 and differ from each other; no constant and no field
swap survives.

MkvStream::finish -> Ok(()) also survived. MkvMuxer::finish has the
zero-frame MkvInvalid guard and two tests cover it, but the Stream
wrapper above it could return Ok unconditionally and bypass the guard
entirely — the empty-title defence was one layer thinner than it looked.

au_assembly: pinned au_opener_from behaviourally to the normative byte
values for all four modes, with negative cases for codes that are
explicitly not openers (MPEG-2 slice 0x01..0xAF, user data 0xB2,
extension 0xB5, sequence end 0xB7 per 13818-2 Table 6-1; VC-1
0x0A/0x0B/0x0C; H.264 SPS/PPS/IDR-slice). au_assembly and codec/ hold
independent copies of these constants; they agree today, and comparing
constants would not catch logic drifting apart, so both sides are now
pinned to the spec instead of to each other.

demux_sink::sanitize: every filename component demux:// writes comes
from disc-controlled text, so the path-separator arm is a traversal
guard. Deleting it now fails, including an end-to-end case where
base = "../evil/Title" must produce exactly one file inside the
chosen directory.

stts_and_ctts_expand renamed to stts_expands_runs_to_per_sample_deltas_in_order
and given runs with distinct deltas AND distinct lengths. Its old name
claimed ctts coverage it never had, which is why the composition-time
chain went unconstrained for eight rounds; the doc comment now points
at the tests that do cover ctts.

Correction to the previous pass: codec/truehd.rs flush -> vec![] IS
equivalent. Applied it, full mux suite green. TrueHD buffers across PES
but parse emits every complete unit immediately, so a residual buffer
at EOF is a truncated access unit and is correctly discarded. The
vec![Default::default()] variants are genuinely different and are
killed.

Deliberately not constrained: mkv::set_opening_capture (diagnostics
behind a process-global tracing check, flaky under the parallel
runner), and the three stdio.rs header paths (StdioStream holds
concrete io::Stdin/Stdout and cannot be driven without a production
refactor to injectable Read/Write).
This commit is contained in:
Matthew Jackson
2026-07-30 14:13:33 -07:00
parent 170fd0c064
commit 9de88969ca
11 changed files with 1387 additions and 7 deletions
+54
View File
@@ -902,4 +902,58 @@ mod tests {
assert_eq!(sink[11], 0xAA);
assert_eq!(sink[17], 0xAA);
}
/// A sink that records only what actually reaches it, so "was flush called"
/// is MEASURED rather than assumed. Its own `flush` is a no-op — the whole
/// point is that the intermediate `BufWriter` must be told to hand its bytes
/// over.
#[derive(Clone, Default)]
struct SharedSink(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
impl SharedSink {
fn bytes(&self) -> Vec<u8> {
self.0.lock().unwrap().clone()
}
}
impl Write for SharedSink {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
/// `finish()` is the ONLY thing that pushes a buffered sink's tail to the
/// file. Real sinks (`LocalFileSink`, `SocketSink`) buffer, and this muxer
/// deliberately adds none of its own — so a `finish` that skipped the flush
/// truncates every `.hevc` output by up to a whole sink buffer of trailing
/// NAL units, producing a file whose last GOP simply is not there.
#[test]
fn finish_flushes_the_buffered_sink_or_the_stream_tail_is_lost() {
let sink = SharedSink::default();
let mut mux = HevcMux::new(io::BufWriter::new(sink.clone()));
// One length-prefixed NAL: [len:u32-BE][NAL bytes] → Annex B.
let nal = [0x26u8, 0x01, 0xAA, 0xBB]; // (0x26 >> 1) = 19 = IDR_W_RADL
let mut frame = (nal.len() as u32).to_be_bytes().to_vec();
frame.extend_from_slice(&nal);
mux.write_frame(0, &frame).unwrap();
assert!(
sink.bytes().is_empty(),
"the fixture must actually buffer, or this test proves nothing"
);
mux.finish().unwrap();
let out = sink.bytes();
let mut expected = START_CODE.to_vec();
expected.extend_from_slice(&nal);
assert_eq!(
out, expected,
"finish must deliver the whole Annex B stream to the sink"
);
}
}