Turn a release-only slice panic into an error, and stop calling 32 kHz 48 kHz

Four round-6 findings.

FileSectorSource::read_sectors guarded its output buffer with a
debug_assert, which is compiled out in release — so an undersized buffer
panicked with 'range end index out of range' instead of returning an
error, out of a public SectorSource impl where the length is caller input.
Drive::read_fua already carries this exact guard, with a comment recording
the same panic being fixed there, and PrefetchedSectorSource has a
regression test for the same case; this impl had been given neither. The
new test is red in release for precisely the predicted reason: 'range end
index 8192 out of range for slice of length 2049'.

parse_track's sample-rate ladder ended in an unconditional S48, so any
SamplingFrequency below 44100 was recorded as 48 kHz. A 32000 Hz AC-3 or
DTS track is legal and common in broadcast-sourced content, and the wrong
rate then propagated into the reconstructed AudioStream. Anything below
the lowest mapped rate is now Unknown, which is what the crate's canonical
SampleRate::from_hz already returned — the ladder disagreed with it. The
ladder itself stays, because the MKV element is a float and wants
tolerance rather than exact equality.

shim_open_exclusive used the mach port from IOMainPort without checking
the return; on failure the port is left untouched and every IOKit call
below ran against an uninitialised value. shim_list_drives in the same
file does check it.

build.rs treated cc and ar as successful if the process merely SPAWNED, so
a genuine compile error in the macOS C shim produced no object file and
surfaced later as an unexplained link failure against a missing symbol.
The shim is macOS-only and is neither linted nor compiled on the other two
platforms, so a mistake in it has exactly one chance to be noticed.

The last two have no test: one needs IOMainPort to fail, the other needs a
deliberately broken C shim, and neither is reachable from the test
harness. Both mirror a correct sibling in the same file, which is the
evidence available.
This commit is contained in:
Matthew Jackson
2026-07-29 22:56:33 -07:00
parent 921404d135
commit d444afbdfc
4 changed files with 127 additions and 12 deletions
+64 -1
View File
@@ -1226,8 +1226,18 @@ fn parse_track(r: &mut impl Read, size: u64) -> io::Result<ParsedTrack> {
SampleRate::S88_2
} else if (44100.0..48000.0).contains(&sr) {
SampleRate::S44_1
} else {
} else if sr >= 48000.0 {
SampleRate::S48
} else {
// Anything below the lowest rate this enum maps is UNKNOWN, not 48 kHz.
// The ladder's final `else` used to be S48, so a legal 32000 Hz AC-3 or
// DTS track — common in broadcast-sourced content — was recorded as
// 48 kHz, and the wrong rate then propagated into the reconstructed
// AudioStream. `SampleRate::from_hz` in disc/mod.rs is the crate's
// canonical mapping and returns Unknown here; this ladder exists only
// because the MKV element is a float and needs tolerance rather than
// exact equality.
SampleRate::Unknown
};
// Map MKV track numbers to BD-TS PIDs. A 13-bit TS PID tops out at
@@ -3301,6 +3311,59 @@ mod tests {
/// `TrackNumber - 1`, so the audio blocks of TrackNumber 3 resolved to index
/// 2 in a 2-stream title and were DISCARDED — a remux with no audio, reported
/// as success.
/// A legal SamplingFrequency below the lowest rate this enum maps must come
/// back as Unknown, not silently as 48 kHz.
///
/// The ladder's final `else` was `SampleRate::S48`, so a 32000 Hz AC-3 or DTS
/// track — legal, and common in broadcast-sourced content — was recorded as
/// 48 kHz and the wrong rate propagated into the reconstructed AudioStream.
/// The crate's canonical mapping, `SampleRate::from_hz`, returns Unknown for
/// 32000; this ladder disagreed with it.
#[test]
fn a_sub_44100_sampling_frequency_is_unknown_not_48k() {
/// One TrackEntry body: an audio track with the given sampling frequency.
fn audio_track_body(freq: f64) -> Vec<u8> {
let mut audio = Vec::new();
audio.push(super::ebml::SAMPLING_FREQUENCY as u8);
audio.push(0x88); // 8-byte float payload
audio.extend_from_slice(&freq.to_be_bytes());
audio.push(super::ebml::CHANNELS as u8);
audio.extend_from_slice(&[0x81, 0x02]);
let mut body = Vec::new();
body.push(super::ebml::TRACK_NUMBER as u8);
body.extend_from_slice(&[0x81, 0x01]);
body.push(super::ebml::TRACK_TYPE as u8);
body.extend_from_slice(&[0x81, super::ebml::TRACK_TYPE_AUDIO as u8]);
body.push(super::ebml::CODEC_ID as u8);
let cid = b"A_AC3";
body.push(0x80 | cid.len() as u8);
body.extend_from_slice(cid);
body.push(super::ebml::AUDIO as u8);
body.push(0x80 | audio.len() as u8);
body.extend_from_slice(&audio);
body
}
for (freq, want) in [
(32000.0f64, SampleRate::Unknown),
(16000.0, SampleRate::Unknown),
(44100.0, SampleRate::S44_1),
(48000.0, SampleRate::S48),
(96000.0, SampleRate::S96),
] {
let body = audio_track_body(freq);
let mut cur = std::io::Cursor::new(body.clone());
let parsed = super::parse_track(&mut cur, body.len() as u64)
.unwrap_or_else(|e| panic!("track with {freq} Hz must parse: {e}"));
let got = match parsed.0.as_ref().expect("an audio track yields a stream") {
Stream::Audio(a) => a.sample_rate,
other => panic!("expected an audio stream, got {other:?}"),
};
assert_eq!(got, want, "{freq} Hz must map to {want:?}, got {got:?}");
}
}
#[test]
fn sparse_track_numbers_route_to_the_right_stream() {
let video = [0x81u8, 0x00, 0x00, 0x80, 0x11]; // TrackNumber 1