v1.0.0-rc.1

CSS keyless decrypt (Stevenson), AACS 1.0/2.0/2.1, MPEG-2 DVD, multi-OS SCSI, multipass recovery, mux highway, audit hardening
This commit is contained in:
Matthew Jackson
2026-06-21 21:06:07 -07:00
parent e8bb6225ac
commit 5941c059c6
53 changed files with 7439 additions and 2340 deletions
+92 -27
View File
@@ -23,14 +23,22 @@ pub struct PesFrame {
pub keyframe: bool,
/// Raw elementary stream data (NAL units, audio samples, etc).
pub data: Vec<u8>,
/// Optional duration in nanoseconds. In-memory only; not part of
/// the on-wire serialization. Currently set by the PGS parser so
/// the MKV muxer can emit `BlockDuration`.
/// Optional duration in nanoseconds. Carried on the wire (8 bytes
/// little-endian, `u64::MAX` as the sentinel for `None`). Set by
/// the PGS parser so the MKV muxer can emit `BlockDuration`; also
/// preserved across network:// and stdio:// hops.
pub duration_ns: Option<u64>,
}
/// Sentinel value for `duration_ns` on the wire: `u64::MAX` means `None`.
/// Valid durations are always much smaller (u64::MAX ns ≈ 584 years).
const DURATION_NONE_SENTINEL: u64 = u64::MAX;
impl PesFrame {
/// Serialize to bytes: track(1) | pts(8) | keyframe(1) | len(4) | data
/// Serialize to bytes:
/// track(1) | pts(8 LE) | keyframe(1) | duration_ns(8 LE) | len(4 LE) | data
///
/// `duration_ns` is encoded as `u64::MAX` when `None`, or the value when `Some`.
pub fn serialize(&self, w: &mut dyn std::io::Write) -> std::io::Result<()> {
if self.track > 255 {
return Err(crate::error::Error::PesTrackTooLarge { track: self.track }.into());
@@ -43,9 +51,11 @@ impl PesFrame {
}
.into());
}
let duration_wire = self.duration_ns.unwrap_or(DURATION_NONE_SENTINEL);
w.write_all(&[self.track as u8])?;
w.write_all(&self.pts.to_le_bytes())?;
w.write_all(&[if self.keyframe { 1 } else { 0 }])?;
w.write_all(&duration_wire.to_le_bytes())?;
w.write_all(&(self.data.len() as u32).to_le_bytes())?;
w.write_all(&self.data)
}
@@ -53,7 +63,7 @@ impl PesFrame {
/// 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
/// A partial header (1-21 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>> {
@@ -74,9 +84,9 @@ impl PesFrame {
Err(e) => return Err(e),
}
let mut header = [0u8; 14]; // 1 + 8 + 1 + 4
let mut header = [0u8; 22]; // 1 + 8 + 1 + 8 + 4
header[0] = first[0];
// The remaining 13 header bytes must be present; a short read here is
// The remaining 21 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;
@@ -84,7 +94,16 @@ impl PesFrame {
header[1], header[2], header[3], header[4], header[5], header[6], header[7], header[8],
]);
let keyframe = header[9] != 0;
let len = u32::from_le_bytes([header[10], header[11], header[12], header[13]]) as usize;
let duration_wire = u64::from_le_bytes([
header[10], header[11], header[12], header[13], header[14], header[15], header[16],
header[17],
]);
let duration_ns = if duration_wire == DURATION_NONE_SENTINEL {
None
} else {
Some(duration_wire)
};
let len = u32::from_le_bytes([header[18], header[19], header[20], header[21]]) as usize;
if len > MAX_FRAME_SIZE {
return Err(crate::error::Error::PesFrameTooLarge { size: len }.into());
}
@@ -95,7 +114,7 @@ impl PesFrame {
pts,
keyframe,
data,
duration_ns: None,
duration_ns,
}))
}
@@ -343,7 +362,7 @@ mod tests {
#[test]
fn truncated_header_is_error_not_eof() {
// A partial 14-byte header (here 5 bytes) must surface as an error,
// A partial 22-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");
@@ -403,23 +422,24 @@ mod tests {
// ── New comprehensive tests ────────────────────────────────────────────────
/// PesFrame serialize layout: track(1) | pts(8 LE) | keyframe(1) | len(4 LE) | data.
/// PesFrame serialize layout:
/// track(1) | pts(8 LE) | keyframe(1) | duration_ns(8 LE) | len(4 LE) | data.
/// Mutation: using big-endian for pts changes bytes [1..9] and deserialization fails.
#[test]
fn serialize_wire_format_matches_spec() {
// Wire format: [track(1)][pts_le(8)][keyframe(1)][len_le(4)][data...]
// Wire format: [track(1)][pts_le(8)][keyframe(1)][duration_le(8)][len_le(4)][data...]
let frame = PesFrame {
track: 2,
pts: 0x0102030405060708_i64,
keyframe: true,
data: vec![0xAA, 0xBB, 0xCC],
duration_ns: None,
duration_ns: Some(0xDEADBEEF_u64),
};
let mut buf = Vec::new();
frame.serialize(&mut buf).unwrap();
// Byte 0: track
assert_eq!(buf[0], 2, "byte 0 must be track");
// Bytes 1..9: pts as little-endian i64 (ECMA-262 serialisation convention)
// Bytes 1..9: pts as little-endian i64
let pts_bytes = 0x0102030405060708_i64.to_le_bytes();
assert_eq!(
&buf[1..9],
@@ -428,16 +448,23 @@ mod tests {
);
// Byte 9: keyframe flag (1 = true)
assert_eq!(buf[9], 1, "byte 9 must be 1 for keyframe=true");
// Bytes 10..14: data length as little-endian u32
// Bytes 10..18: duration_ns as little-endian u64
let dur_bytes = 0xDEADBEEF_u64.to_le_bytes();
assert_eq!(
&buf[10..18],
&dur_bytes,
"bytes 10..18 must be duration_ns in little-endian"
);
// Bytes 18..22: data length as little-endian u32
let len_bytes = 3_u32.to_le_bytes();
assert_eq!(
&buf[10..14],
&buf[18..22],
&len_bytes,
"bytes 10..14 must be data length LE u32"
"bytes 18..22 must be data length LE u32"
);
// Bytes 14..: data
// Bytes 22..: data
assert_eq!(
&buf[14..],
&buf[22..],
&[0xAA, 0xBB, 0xCC],
"data must follow header verbatim"
);
@@ -536,25 +563,63 @@ mod tests {
);
}
/// duration_ns is not serialized — deserialized frames always have duration_ns=None.
/// Spec: doc says "In-memory only; not part of the on-wire serialization."
/// Mutation: serializing duration_ns would add bytes and break deserialization.
/// duration_ns is serialized as 8 LE bytes; None encodes as u64::MAX sentinel.
/// Spec: duration_ns is part of the wire format so network/stdio hops preserve it.
/// Mutation: dropping duration_ns from serialize would zero-fill the field and
/// silently lose PGS subtitle durations on the network:// path.
#[test]
fn deserialize_duration_ns_is_always_none() {
let frame = PesFrame {
fn deserialize_duration_ns_roundtrips() {
// None encodes as u64::MAX sentinel and decodes back to None.
let frame_none = PesFrame {
track: 0,
pts: 0,
keyframe: false,
data: vec![1, 2, 3],
duration_ns: Some(999_999),
duration_ns: None,
};
let mut buf = Vec::new();
frame.serialize(&mut buf).unwrap();
frame_none.serialize(&mut buf).unwrap();
let mut cursor = std::io::Cursor::new(buf);
let got = PesFrame::deserialize(&mut cursor).unwrap().unwrap();
assert!(
got.duration_ns.is_none(),
"duration_ns must not be on the wire — deserialized frame must have None"
"None duration_ns must round-trip as None"
);
// Some(0) must survive — 0 is a valid zero-length duration, not the sentinel.
let frame_zero = PesFrame {
track: 1,
pts: 1000,
keyframe: false,
data: vec![4, 5],
duration_ns: Some(0),
};
let mut buf2 = Vec::new();
frame_zero.serialize(&mut buf2).unwrap();
let mut cursor2 = std::io::Cursor::new(buf2);
let got2 = PesFrame::deserialize(&mut cursor2).unwrap().unwrap();
assert_eq!(
got2.duration_ns,
Some(0),
"Some(0) duration_ns must round-trip as Some(0)"
);
// Some(N) for a typical PGS duration (~3 seconds).
let frame_n = PesFrame {
track: 2,
pts: 5_000_000_000,
keyframe: false,
data: vec![6],
duration_ns: Some(3_000_000_000),
};
let mut buf3 = Vec::new();
frame_n.serialize(&mut buf3).unwrap();
let mut cursor3 = std::io::Cursor::new(buf3);
let got3 = PesFrame::deserialize(&mut cursor3).unwrap().unwrap();
assert_eq!(
got3.duration_ns,
Some(3_000_000_000),
"Some(3_000_000_000) duration_ns must round-trip"
);
}