0.31.0: hardening and correctness pass across mux, codec, AACS/CSS, UDF/MPLS/CLPI, recovery, drive/SCSI, labels, and I/O

Library-wide review-and-fix pass: tightened AACS keydb/handshake/variant
handling and trailing-partial-unit policy, corrected MPLS mark offset and
added UDF allocation bounds, hardened the mux/codec framing and M2TS paths,
guarded SCSI READ CAPACITY short transfers and unified error mapping, added
overflow guards on untrusted disc input, and made prefetch shutdown
deterministic. Release profile now builds with thin LTO + single codegen unit.
This commit is contained in:
Matthew Jackson
2026-06-07 17:37:38 -07:00
parent 5b6ea8f5c4
commit 061f68594a
128 changed files with 11838 additions and 3831 deletions
+130 -10
View File
@@ -6,6 +6,12 @@
//! disc.read() → PES frame (sectors → decrypt → demux internally)
//! mkv.write(frame) → MKV file (mux internally)
/// Maximum frame payload size, shared by `serialize` and `deserialize`
/// so the wire format round-trips: any frame that serializes can be read
/// back. A frame larger than this is rejected on write rather than written
/// and then hard-erroring mid-stream on read.
const MAX_FRAME_SIZE: usize = 256 * 1024 * 1024; // 256 MiB
/// One frame of elementary stream data.
#[derive(Debug, Clone)]
pub struct PesFrame {
@@ -27,9 +33,11 @@ impl PesFrame {
/// Serialize to bytes: track(1) | pts(8) | keyframe(1) | len(4) | data
pub fn serialize(&self, w: &mut dyn std::io::Write) -> std::io::Result<()> {
if self.track > 255 {
return Err(crate::error::Error::PesInvalidMagic.into());
return Err(crate::error::Error::PesTrackTooLarge { track: self.track }.into());
}
if self.data.len() > u32::MAX as usize {
// Enforce the same ceiling the reader uses, so a frame that writes
// can always be read back (round-trippable wire format).
if self.data.len() > MAX_FRAME_SIZE {
return Err(crate::error::Error::PesFrameTooLarge {
size: self.data.len(),
}
@@ -42,16 +50,35 @@ impl PesFrame {
w.write_all(&self.data)
}
/// Deserialize from bytes. Returns None at EOF.
/// Deserialize from bytes. Returns None at a clean end of stream.
///
/// A clean EOF is exactly zero bytes available before the next frame.
/// A partial header (1-13 bytes, e.g. a crash or short write) is a real
/// error (`UnexpectedEof`), not silently treated as EOF — otherwise
/// truncated `.pes` data would be accepted as a graceful end.
pub fn deserialize(r: &mut dyn std::io::Read) -> std::io::Result<Option<Self>> {
const MAX_FRAME_SIZE: usize = 256 * 1024 * 1024; // 256 MB
let mut header = [0u8; 14]; // 1 + 8 + 1 + 4
match r.read_exact(&mut header) {
// Probe one byte first to distinguish clean EOF from a truncated
// header.
let mut first = [0u8; 1];
match r.read(&mut first) {
Ok(0) => return Ok(None), // clean EOF, no frame started
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {
// Retry-once on EINTR before committing to the header read.
match r.read(&mut first) {
Ok(0) => return Ok(None),
Ok(_) => {}
Err(e) => return Err(e),
}
}
Err(e) => return Err(e),
}
let mut header = [0u8; 14]; // 1 + 8 + 1 + 4
header[0] = first[0];
// The remaining 13 header bytes must be present; a short read here is
// a truncated frame, propagated as UnexpectedEof.
r.read_exact(&mut header[1..])?;
let track = header[0] as usize;
let pts = i64::from_le_bytes([
header[1], header[2], header[3], header[4], header[5], header[6], header[7], header[8],
@@ -73,7 +100,10 @@ impl PesFrame {
}
/// Create from a codec::Frame with a track index.
pub fn from_codec_frame(track: usize, frame: crate::mux::codec::Frame) -> Self {
///
/// `pub(crate)`: takes the internal `mux::codec::Frame` type, so it
/// can't be part of the public API surface.
pub(crate) fn from_codec_frame(track: usize, frame: crate::mux::codec::Frame) -> Self {
Self {
track,
pts: frame.pts_ns,
@@ -167,8 +197,11 @@ impl Stream for CountingStream {
}
fn write(&mut self, frame: &PesFrame) -> std::io::Result<()> {
// Only count bytes that actually made it to the inner sink, so a
// failed write doesn't permanently inflate bytes_written().
self.inner.write(frame)?;
self.written += frame.data.len() as u64;
self.inner.write(frame)
Ok(())
}
fn finish(&mut self) -> std::io::Result<()> {
@@ -186,6 +219,10 @@ impl Stream for CountingStream {
fn headers_ready(&self) -> bool {
self.inner.headers_ready()
}
fn errors(&self) -> u64 {
self.inner.errors()
}
}
#[cfg(test)]
@@ -280,4 +317,87 @@ mod tests {
let _ = s.info();
s.finish().unwrap();
}
#[test]
fn frame_roundtrips_through_bytes() {
let frame = make_frame(3, 123_456);
let mut buf = Vec::new();
frame.serialize(&mut buf).expect("serialize");
let mut cursor = std::io::Cursor::new(buf);
let got = PesFrame::deserialize(&mut cursor)
.expect("deserialize")
.expect("frame present");
assert_eq!(got.track, frame.track);
assert_eq!(got.pts, frame.pts);
assert_eq!(got.keyframe, frame.keyframe);
assert_eq!(got.data, frame.data);
// Next read is a clean EOF.
assert!(PesFrame::deserialize(&mut cursor).unwrap().is_none());
}
#[test]
fn empty_input_is_clean_eof() {
let mut cursor = std::io::Cursor::new(Vec::new());
assert!(PesFrame::deserialize(&mut cursor).unwrap().is_none());
}
#[test]
fn truncated_header_is_error_not_eof() {
// A partial 14-byte header (here 5 bytes) must surface as an error,
// not be swallowed as a graceful end of stream.
let mut cursor = std::io::Cursor::new(vec![1u8, 2, 3, 4, 5]);
let err = PesFrame::deserialize(&mut cursor).expect_err("partial header must error");
assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof);
}
#[test]
fn oversize_track_rejected_on_serialize() {
let frame = make_frame(256, 0);
let mut buf = Vec::new();
let err = frame
.serialize(&mut buf)
.expect_err("track > 255 must fail");
let code = format!("E{}", crate::error::E_PES_TRACK_TOO_LARGE);
assert!(err.to_string().contains(&code), "got: {err}");
}
/// Output stream whose `write` always fails — for CountingStream tests.
struct FailingWriteStream {
title: DiscTitle,
}
impl Stream for FailingWriteStream {
fn read(&mut self) -> std::io::Result<Option<PesFrame>> {
Ok(None)
}
fn write(&mut self, _frame: &PesFrame) -> std::io::Result<()> {
Err(std::io::Error::from(std::io::ErrorKind::BrokenPipe))
}
fn finish(&mut self) -> std::io::Result<()> {
Ok(())
}
fn info(&self) -> &DiscTitle {
&self.title
}
}
#[test]
fn counting_stream_does_not_count_failed_writes() {
let mut cs = CountingStream::new(Box::new(FailingWriteStream {
title: DiscTitle::empty(),
}));
let frame = make_frame(0, 0);
assert!(cs.write(&frame).is_err());
// Failed write must not inflate the byte count.
assert_eq!(cs.bytes_written(), 0);
}
#[test]
fn counting_stream_counts_successful_writes() {
let frame = make_frame(0, 0);
let payload = frame.data.len() as u64;
let mut cs = CountingStream::new(Box::new(MockStream::new(Vec::new())));
cs.write(&frame).unwrap();
assert_eq!(cs.bytes_written(), payload);
}
}