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
+109 -49
View File
@@ -83,12 +83,19 @@ pub fn parse(data: &[u8]) -> Result<Playlist> {
let mut pos = 10;
for item_idx in 0..num_play_items {
if pos + 2 > pl.len() { break; }
if pos + 2 > pl.len() {
break;
}
let item_length = u16::from_be_bytes([pl[pos], pl[pos + 1]]) as usize;
if pos + 2 + item_length > pl.len() { break; }
if pos + 2 + item_length > pl.len() {
break;
}
let item = &pl[pos + 2..pos + 2 + item_length];
if item.len() < 20 { pos += 2 + item_length; continue; }
if item.len() < 20 {
pos += 2 + item_length;
continue;
}
let clip_id = String::from_utf8_lossy(&item[0..5]).to_string();
let connection_condition = item[9] & 0x0F;
@@ -121,27 +128,35 @@ pub fn parse(data: &[u8]) -> Result<Playlist> {
if let Some((entry, next)) = parse_stream_entry(item, spos, 1) {
streams.push(entry);
spos = next;
} else { break; }
} else {
break;
}
}
// Primary audio
for _ in 0..n_audio {
if let Some((entry, next)) = parse_stream_entry(item, spos, 2) {
streams.push(entry);
spos = next;
} else { break; }
} else {
break;
}
}
// PG subtitles
for _ in 0..n_pg {
if let Some((entry, next)) = parse_stream_entry(item, spos, 3) {
streams.push(entry);
spos = next;
} else { break; }
} else {
break;
}
}
// IG (skip but advance)
for _ in 0..n_ig {
if let Some((_, next)) = parse_stream_entry(item, spos, 4) {
spos = next;
} else { break; }
} else {
break;
}
}
// Secondary audio
for _ in 0..n_sec_audio {
@@ -153,8 +168,12 @@ pub fn parse(data: &[u8]) -> Result<Playlist> {
if next < item.len() {
let n_refs = item[next] as usize;
spos = next + 2 + n_refs + (n_refs % 2);
} else { spos = next; }
} else { break; }
} else {
spos = next;
}
} else {
break;
}
}
// Secondary video (PiP)
for _ in 0..n_sec_video {
@@ -169,9 +188,15 @@ pub fn parse(data: &[u8]) -> Result<Playlist> {
if after_arefs < item.len() {
let n_prefs = item[after_arefs] as usize;
spos = after_arefs + 2 + n_prefs + (n_prefs % 2);
} else { spos = after_arefs; }
} else { spos = next; }
} else { break; }
} else {
spos = after_arefs;
}
} else {
spos = next;
}
} else {
break;
}
}
// Secondary PG (PiP subtitles) — must consume to keep spos aligned
for _ in 0..n_pip_pg {
@@ -182,8 +207,12 @@ pub fn parse(data: &[u8]) -> Result<Playlist> {
if next < item.len() {
let n_refs = item[next] as usize;
spos = next + 2 + n_refs + (n_refs % 2);
} else { spos = next; }
} else { break; }
} else {
spos = next;
}
} else {
break;
}
}
// Dolby Vision enhancement layer
for _ in 0..n_dv {
@@ -192,7 +221,9 @@ pub fn parse(data: &[u8]) -> Result<Playlist> {
entry.secondary = true;
streams.push(entry);
spos = next;
} else { break; }
} else {
break;
}
}
}
@@ -216,12 +247,16 @@ pub fn parse(data: &[u8]) -> Result<Playlist> {
/// Parse one stream entry from the STN table.
/// Returns (StreamEntry, next position) or None.
fn parse_stream_entry(item: &[u8], pos: usize, stream_type: u8) -> Option<(StreamEntry, usize)> {
if pos + 2 > item.len() { return None; }
if pos + 2 > item.len() {
return None;
}
// Stream entry: length(1) + data
let se_len = item[pos] as usize;
let se_end = pos + 1 + se_len;
if se_end > item.len() { return None; }
if se_end > item.len() {
return None;
}
// PID from stream entry (type 0x01 = PlayItem stream: PID at bytes 2-3)
let pid = if item[pos + 1] == 0x01 && pos + 4 <= item.len() {
@@ -231,15 +266,18 @@ fn parse_stream_entry(item: &[u8], pos: usize, stream_type: u8) -> Option<(Strea
};
// Stream attributes: length(1) + coding_type(1) + format-specific data
if se_end + 2 > item.len() { return None; }
if se_end + 2 > item.len() {
return None;
}
let sa_len = item[se_end] as usize;
let sa_end = se_end + 1 + sa_len;
if sa_end > item.len() || sa_len < 1 { return None; }
if sa_end > item.len() || sa_len < 1 {
return None;
}
let sa = &item[se_end + 1..se_end + 1 + sa_len];
let coding_type = sa[0];
let mut video_format = 0u8;
let mut video_rate = 0u8;
let mut audio_format = 0u8;
@@ -307,19 +345,22 @@ fn parse_stream_entry(item: &[u8], pos: usize, stream_type: u8) -> Option<(Strea
_ => {}
}
Some((StreamEntry {
stream_type,
pid,
coding_type,
video_format,
video_rate,
audio_format,
audio_rate,
language,
dynamic_range,
color_space: color_space_val,
secondary: false,
}, sa_end))
Some((
StreamEntry {
stream_type,
pid,
coding_type,
video_format,
video_rate,
audio_format,
audio_rate,
language,
dynamic_range,
color_space: color_space_val,
secondary: false,
},
sa_end,
))
}
#[cfg(test)]
@@ -329,7 +370,12 @@ mod tests {
/// Build a minimal MPLS binary with given play items and STN streams on the first item.
/// STN counts: (n_video, n_audio, n_pg, n_ig, n_sec_audio, n_sec_video, n_pip_pg, n_dv)
fn build_mpls(
play_items_data: &[(/*clip_id*/&[u8;5], /*conn*/u8, /*in_time*/u32, /*out_time*/u32)],
play_items_data: &[(
/*clip_id*/ &[u8; 5],
/*conn*/ u8,
/*in_time*/ u32,
/*out_time*/ u32,
)],
stn_counts: (u8, u8, u8, u8, u8, u8, u8, u8),
stream_entries: &[Vec<u8>], // raw stream entry + attributes bytes for each stream
) -> Vec<u8> {
@@ -424,7 +470,13 @@ mod tests {
/// For video: attrs = coding_type(1) + format_rate(1) [+ hdr_byte if HEVC]
/// For audio: attrs = coding_type(1) + format_rate(1) + language(3)
/// For PG: attrs = coding_type(1) + language(3)
fn build_stream_entry_video(pid: u16, coding_type: u8, format: u8, rate: u8, hdr: Option<u8>) -> Vec<u8> {
fn build_stream_entry_video(
pid: u16,
coding_type: u8,
format: u8,
rate: u8,
hdr: Option<u8>,
) -> Vec<u8> {
let mut out = Vec::new();
// Stream entry: length(1) + sub_path_type(1) + pid(2)
out.push(3); // se_len = 3 bytes (type + pid_hi + pid_lo)
@@ -440,13 +492,25 @@ mod tests {
out
}
fn build_stream_entry_audio(pid: u16, coding_type: u8, ch_layout: u8, sample_rate: u8, lang: &[u8; 3]) -> Vec<u8> {
fn build_stream_entry_audio(
pid: u16,
coding_type: u8,
ch_layout: u8,
sample_rate: u8,
lang: &[u8; 3],
) -> Vec<u8> {
let mut out = Vec::new();
out.push(3);
out.push(0x01);
out.extend_from_slice(&pid.to_be_bytes());
// attrs: coding_type(1) + format_rate(1) + language(3)
let attrs = vec![coding_type, (ch_layout << 4) | sample_rate, lang[0], lang[1], lang[2]];
let attrs = vec![
coding_type,
(ch_layout << 4) | sample_rate,
lang[0],
lang[1],
lang[2],
];
out.push(attrs.len() as u8);
out.extend_from_slice(&attrs);
out
@@ -466,7 +530,7 @@ mod tests {
#[test]
fn parse_valid_mpls() {
let in_time: u32 = 90000; // 2 seconds at 45kHz
let in_time: u32 = 90000; // 2 seconds at 45kHz
let out_time: u32 = 4500000; // 100 seconds
let video = build_stream_entry_video(0x1011, 0x1B, 6, 1, None); // H264, 1080p, 23.976
@@ -508,10 +572,10 @@ mod tests {
assert_eq!(v.stream_type, 1);
assert_eq!(v.pid, 0x1011);
assert_eq!(v.coding_type, 0x24); // HEVC
assert_eq!(v.video_format, 8); // 2160p
assert_eq!(v.video_rate, 1); // 23.976
assert_eq!(v.dynamic_range, 1); // HDR10
assert_eq!(v.color_space, 2); // BT.2020
assert_eq!(v.video_format, 8); // 2160p
assert_eq!(v.video_rate, 1); // 23.976
assert_eq!(v.dynamic_range, 1); // HDR10
assert_eq!(v.color_space, 2); // BT.2020
assert!(!v.secondary);
// Audio stream
@@ -519,8 +583,8 @@ mod tests {
assert_eq!(a.stream_type, 2);
assert_eq!(a.pid, 0x1100);
assert_eq!(a.coding_type, 0x83); // TrueHD
assert_eq!(a.audio_format, 6); // 5.1
assert_eq!(a.audio_rate, 1); // 48kHz
assert_eq!(a.audio_format, 6); // 5.1
assert_eq!(a.audio_rate, 1); // 48kHz
assert_eq!(a.language, "eng");
assert!(!a.secondary);
@@ -535,11 +599,7 @@ mod tests {
#[test]
fn parse_invalid_magic() {
let mut data = build_mpls(
&[(b"00001", 1, 0, 9000000)],
(0, 0, 0, 0, 0, 0, 0, 0),
&[],
);
let mut data = build_mpls(&[(b"00001", 1, 0, 9000000)], (0, 0, 0, 0, 0, 0, 0, 0), &[]);
data[0] = b'X';
data[1] = b'X';
data[2] = b'X';