Audit fixes + DVD support foundation (IFO, PS demux, MPEG-2, CSS crack)

Audit fixes (14 critical, 22 warnings):
- UDF: bounds checks on all ICB/FID parsing from disc data
- SCSI Linux: saturating_sub on residual, CDB length guard, buffer size guard
- SCSI macOS: SCSITaskStatus u32 (was u8 — stack corruption)
- AACS: EC mod_inv returns infinity instead of panic, key reduced mod n
- AACS: do_handshake tries all host certs (was returning on first failure)
- H.264: bounds check on SPS < 4 bytes
- ContentReader: error on missing unit key (was zero-fill)
- KEYDB: flat redirect loop (was recursive), 100MB response limit, Windows HOME fallback
- ISO writer: AVDP extent order, partition length, allocation cap
- Network: removed TCP_NODELAY on bulk stream
- MKV: guard on u64::MAX seek
- disc.rs: saturating_sub on extent offset, simplified dead region code
- cargo fmt (610 violations), cargo clippy --fix (55 auto-fixes)

DVD support (new files):
- src/ifo.rs — IFO parser (VIDEO_TS.IFO, VTS_XX_0.IFO, PGC chains, cells, streams) — 13 tests
- src/mux/ps.rs — MPEG-2 Program Stream demuxer (pack headers, PES, private stream 1) — 12 tests
- src/mux/codec/mpeg2.rs — MPEG-2 video parser (sequence headers, I-frame detection) — 15 tests
- src/css/crack.rs — split-attack algorithm (LFSR cipher needs verification — test ignored)

226 tests total (was 186), 1 ignored (CSS crack needs cipher verification).
This commit is contained in:
MattJackson
2026-04-11 16:52:22 +00:00
parent 6de4ee4b5c
commit 01235ff347
57 changed files with 6189 additions and 1519 deletions
+124 -54
View File
@@ -100,7 +100,7 @@ impl PesAssembler {
pub struct TsDemuxer {
assemblers: Vec<PesAssembler>,
pid_index: [i16; 8192], // PID → index into assemblers, -1 = not tracked
remainder: Vec<u8>, // leftover bytes from previous feed() call
remainder: Vec<u8>, // leftover bytes from previous feed() call
}
impl TsDemuxer {
@@ -112,7 +112,11 @@ impl TsDemuxer {
pid_index[pid as usize] = i as i16;
assemblers.push(PesAssembler::new(pid));
}
Self { assemblers, pid_index, remainder: Vec::new() }
Self {
assemblers,
pid_index,
remainder: Vec::new(),
}
}
/// Feed a chunk of BD transport stream data. Handles non-192-byte-aligned input
@@ -225,8 +229,12 @@ fn parse_pes_header(data: &[u8]) -> (Option<i64>, Option<i64>, usize) {
// Some stream IDs don't have the standard PES header extension
// (program_stream_map, padding, private_stream_2, ECM, EMM, etc.)
if stream_id == 0xBC || stream_id == 0xBE || stream_id == 0xBF
|| stream_id == 0xF0 || stream_id == 0xF1 || stream_id == 0xFF
if stream_id == 0xBC
|| stream_id == 0xBE
|| stream_id == 0xBF
|| stream_id == 0xF0
|| stream_id == 0xF1
|| stream_id == 0xFF
{
return (None, None, 6);
}
@@ -261,11 +269,7 @@ fn parse_timestamp(data: &[u8]) -> i64 {
let b3 = data[3] as i64;
let b4 = data[4] as i64;
((b0 >> 1) & 0x07) << 30
| b1 << 22
| (b2 >> 1) << 15
| b3 << 7
| b4 >> 1
((b0 >> 1) & 0x07) << 30 | b1 << 22 | (b2 >> 1) << 15 | b3 << 7 | b4 >> 1
}
// ============================================================
@@ -281,7 +285,10 @@ pub fn scan_streams(data: &[u8]) -> Option<Vec<crate::disc::Stream>> {
let mut pat_pmt_pid: Option<u16> = None;
let mut offset = 0;
while offset + BD_TS_PACKET_SIZE <= data.len() {
if data[offset + 4] != SYNC_BYTE { offset += 1; continue; }
if data[offset + 4] != SYNC_BYTE {
offset += 1;
continue;
}
let pid = (((data[offset + 5] & 0x1F) as u16) << 8) | data[offset + 6] as u16;
let pusi = data[offset + 5] & 0x40 != 0;
@@ -291,7 +298,8 @@ pub fn scan_streams(data: &[u8]) -> Option<Vec<crate::disc::Stream>> {
let pointer = data[payload_start] as usize;
let pat_start = payload_start + 1 + pointer;
if pat_start + 12 < data.len() && data[pat_start] == 0x00 {
let section_len = (((data[pat_start + 1] & 0x0F) as usize) << 8) | data[pat_start + 2] as usize;
let section_len = (((data[pat_start + 1] & 0x0F) as usize) << 8)
| data[pat_start + 2] as usize;
let entries_start = pat_start + 8;
let entries_end = pat_start + 3 + section_len - 4;
let mut e = entries_start;
@@ -316,20 +324,34 @@ pub fn scan_streams(data: &[u8]) -> Option<Vec<crate::disc::Stream>> {
let mut streams = Vec::new();
offset = 0;
while offset + BD_TS_PACKET_SIZE <= data.len() {
if data[offset + 4] != SYNC_BYTE { offset += 1; continue; }
if data[offset + 4] != SYNC_BYTE {
offset += 1;
continue;
}
let pid = (((data[offset + 5] & 0x1F) as u16) << 8) | data[offset + 6] as u16;
let pusi = data[offset + 5] & 0x40 != 0;
if pid == pmt_pid && pusi {
let payload_start = offset + 4 + 4;
if payload_start + 1 >= data.len() { offset += BD_TS_PACKET_SIZE; continue; }
if payload_start + 1 >= data.len() {
offset += BD_TS_PACKET_SIZE;
continue;
}
let pointer = data[payload_start] as usize;
let pmt_start = payload_start + 1 + pointer;
if pmt_start + 12 >= data.len() { offset += BD_TS_PACKET_SIZE; continue; }
if data[pmt_start] != 0x02 { offset += BD_TS_PACKET_SIZE; continue; }
if pmt_start + 12 >= data.len() {
offset += BD_TS_PACKET_SIZE;
continue;
}
if data[pmt_start] != 0x02 {
offset += BD_TS_PACKET_SIZE;
continue;
}
let section_len = (((data[pmt_start + 1] & 0x0F) as usize) << 8) | data[pmt_start + 2] as usize;
let prog_info_len = (((data[pmt_start + 10] & 0x0F) as usize) << 8) | data[pmt_start + 11] as usize;
let section_len =
(((data[pmt_start + 1] & 0x0F) as usize) << 8) | data[pmt_start + 2] as usize;
let prog_info_len =
(((data[pmt_start + 10] & 0x0F) as usize) << 8) | data[pmt_start + 11] as usize;
let mut pos = pmt_start + 12 + prog_info_len;
let end = pmt_start + 3 + section_len - 4;
@@ -340,57 +362,95 @@ pub fn scan_streams(data: &[u8]) -> Option<Vec<crate::disc::Stream>> {
let stream = match stream_type {
0x1B => Some(Stream::Video(VideoStream {
pid: es_pid, codec: Codec::H264,
resolution: "1080p".into(), frame_rate: String::new(),
hdr: HdrFormat::Sdr, color_space: ColorSpace::Bt709,
secondary: false, label: String::new(),
pid: es_pid,
codec: Codec::H264,
resolution: "1080p".into(),
frame_rate: String::new(),
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt709,
secondary: false,
label: String::new(),
})),
0x24 => Some(Stream::Video(VideoStream {
pid: es_pid, codec: Codec::Hevc,
resolution: "2160p".into(), frame_rate: String::new(),
hdr: HdrFormat::Sdr, color_space: ColorSpace::Bt709,
secondary: false, label: String::new(),
pid: es_pid,
codec: Codec::Hevc,
resolution: "2160p".into(),
frame_rate: String::new(),
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt709,
secondary: false,
label: String::new(),
})),
0xEA => Some(Stream::Video(VideoStream {
pid: es_pid, codec: Codec::Vc1,
resolution: "1080p".into(), frame_rate: String::new(),
hdr: HdrFormat::Sdr, color_space: ColorSpace::Bt709,
secondary: false, label: String::new(),
pid: es_pid,
codec: Codec::Vc1,
resolution: "1080p".into(),
frame_rate: String::new(),
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt709,
secondary: false,
label: String::new(),
})),
0x02 => Some(Stream::Video(VideoStream {
pid: es_pid, codec: Codec::Mpeg2,
resolution: "1080i".into(), frame_rate: String::new(),
hdr: HdrFormat::Sdr, color_space: ColorSpace::Bt709,
secondary: false, label: String::new(),
pid: es_pid,
codec: Codec::Mpeg2,
resolution: "1080i".into(),
frame_rate: String::new(),
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt709,
secondary: false,
label: String::new(),
})),
0x81 => Some(Stream::Audio(AudioStream {
pid: es_pid, codec: Codec::Ac3,
channels: "5.1".into(), language: "und".into(),
sample_rate: "48kHz".into(), secondary: false, label: String::new(),
pid: es_pid,
codec: Codec::Ac3,
channels: "5.1".into(),
language: "und".into(),
sample_rate: "48kHz".into(),
secondary: false,
label: String::new(),
})),
0x83 => Some(Stream::Audio(AudioStream {
pid: es_pid, codec: Codec::TrueHd,
channels: "5.1".into(), language: "und".into(),
sample_rate: "48kHz".into(), secondary: false, label: String::new(),
pid: es_pid,
codec: Codec::TrueHd,
channels: "5.1".into(),
language: "und".into(),
sample_rate: "48kHz".into(),
secondary: false,
label: String::new(),
})),
0x84 | 0xA1 => Some(Stream::Audio(AudioStream {
pid: es_pid, codec: Codec::Ac3Plus,
channels: "5.1".into(), language: "und".into(),
sample_rate: "48kHz".into(), secondary: false, label: String::new(),
pid: es_pid,
codec: Codec::Ac3Plus,
channels: "5.1".into(),
language: "und".into(),
sample_rate: "48kHz".into(),
secondary: false,
label: String::new(),
})),
0x85 | 0x86 => Some(Stream::Audio(AudioStream {
pid: es_pid, codec: Codec::DtsHdMa,
channels: "5.1".into(), language: "und".into(),
sample_rate: "48kHz".into(), secondary: false, label: String::new(),
pid: es_pid,
codec: Codec::DtsHdMa,
channels: "5.1".into(),
language: "und".into(),
sample_rate: "48kHz".into(),
secondary: false,
label: String::new(),
})),
0x82 => Some(Stream::Audio(AudioStream {
pid: es_pid, codec: Codec::Dts,
channels: "5.1".into(), language: "und".into(),
sample_rate: "48kHz".into(), secondary: false, label: String::new(),
pid: es_pid,
codec: Codec::Dts,
channels: "5.1".into(),
language: "und".into(),
sample_rate: "48kHz".into(),
secondary: false,
label: String::new(),
})),
0x90 => Some(Stream::Subtitle(SubtitleStream {
pid: es_pid, codec: Codec::Pgs,
language: "und".into(), forced: false,
pid: es_pid,
codec: Codec::Pgs,
language: "und".into(),
forced: false,
})),
_ => None,
};
@@ -405,7 +465,11 @@ pub fn scan_streams(data: &[u8]) -> Option<Vec<crate::disc::Stream>> {
offset += BD_TS_PACKET_SIZE;
}
if streams.is_empty() { None } else { Some(streams) }
if streams.is_empty() {
None
} else {
Some(streams)
}
}
// ============================================================
@@ -416,7 +480,10 @@ pub fn scan_streams(data: &[u8]) -> Option<Vec<crate::disc::Stream>> {
pub fn scan_first_pts(data: &[u8], target_pid: u16) -> Option<i64> {
let mut offset = 0;
while offset + BD_TS_PACKET_SIZE <= data.len() {
if data[offset + 4] != SYNC_BYTE { offset += 1; continue; }
if data[offset + 4] != SYNC_BYTE {
offset += 1;
continue;
}
let pid = (((data[offset + 5] & 0x1F) as u16) << 8) | data[offset + 6] as u16;
let pusi = data[offset + 5] & 0x40 != 0;
if pid == target_pid && pusi {
@@ -443,7 +510,10 @@ pub fn scan_last_pts(data: &[u8], target_pid: u16) -> Option<i64> {
let mut last_pts = None;
let mut offset = 0;
while offset + BD_TS_PACKET_SIZE <= data.len() {
if data[offset + 4] != SYNC_BYTE { offset += 1; continue; }
if data[offset + 4] != SYNC_BYTE {
offset += 1;
continue;
}
let pid = (((data[offset + 5] & 0x1F) as u16) << 8) | data[offset + 6] as u16;
let pusi = data[offset + 5] & 0x40 != 0;
if pid == target_pid && pusi {
@@ -482,7 +552,7 @@ pub fn scan_duration<R: std::io::Read + std::io::Seek>(r: &mut R, video_pid: u16
// Read last 2MB for last PTS (aligned to 192-byte boundary)
let file_size = r.seek(SeekFrom::End(0)).ok()?;
let tail_size: u64 = SCAN_TAIL_SIZE as u64;
let raw_pos = if file_size > tail_size { file_size - tail_size } else { 0 };
let raw_pos = file_size.saturating_sub(tail_size);
let seek_pos = (raw_pos / BD_TS_PACKET_SIZE as u64) * BD_TS_PACKET_SIZE as u64;
r.seek(SeekFrom::Start(seek_pos)).ok()?;
let mut tail_buf = vec![0u8; tail_size as usize];