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 6e771a1867
commit ff5547363b
57 changed files with 6189 additions and 1519 deletions
+99 -62
View File
@@ -3,10 +3,11 @@
//! Format: [8B magic] [4B json_len] [JSON] [padding to 192B boundary] [BD-TS data...]
//! Other tools skip the header during TS sync recovery (scan for 0x47).
use crate::disc::{
AudioStream, Codec, ColorSpace, DiscTitle, HdrFormat, Stream, SubtitleStream, VideoStream,
};
use serde::{Deserialize, Serialize};
use std::io::{self, Read, Seek, SeekFrom, Write};
use serde::{Serialize, Deserialize};
use crate::disc::{DiscTitle, Stream, VideoStream, AudioStream, SubtitleStream,
Codec, HdrFormat, ColorSpace};
/// Magic bytes: "FMKV" + version 1 + 2 reserved bytes.
const MAGIC: [u8; 8] = [b'F', b'M', b'K', b'V', 0x00, 0x01, 0x00, 0x00];
@@ -37,60 +38,76 @@ pub enum MetaStream {
Video {
pid: u16,
codec: String,
#[serde(default)] resolution: String,
#[serde(default)] frame_rate: String,
#[serde(default)] hdr: String,
#[serde(default)] label: String,
#[serde(default)] secondary: bool,
#[serde(default)]
resolution: String,
#[serde(default)]
frame_rate: String,
#[serde(default)]
hdr: String,
#[serde(default)]
label: String,
#[serde(default)]
secondary: bool,
},
#[serde(rename = "audio")]
Audio {
pid: u16,
codec: String,
#[serde(default)] channels: String,
#[serde(default)] language: String,
#[serde(default)] sample_rate: String,
#[serde(default)] label: String,
#[serde(default)] secondary: bool,
#[serde(default)]
channels: String,
#[serde(default)]
language: String,
#[serde(default)]
sample_rate: String,
#[serde(default)]
label: String,
#[serde(default)]
secondary: bool,
},
#[serde(rename = "subtitle")]
Subtitle {
pid: u16,
codec: String,
#[serde(default)] language: String,
#[serde(default)] forced: bool,
#[serde(default)]
language: String,
#[serde(default)]
forced: bool,
},
}
impl M2tsMeta {
/// Build metadata from a disc Title.
pub fn from_title(title: &DiscTitle) -> Self {
let streams = title.streams.iter().map(|s| match s {
Stream::Video(v) => MetaStream::Video {
pid: v.pid,
codec: codec_to_str(v.codec),
resolution: v.resolution.clone(),
frame_rate: v.frame_rate.clone(),
hdr: hdr_to_str(v.hdr),
label: v.label.clone(),
secondary: v.secondary,
},
Stream::Audio(a) => MetaStream::Audio {
pid: a.pid,
codec: codec_to_str(a.codec),
channels: a.channels.clone(),
language: a.language.clone(),
sample_rate: a.sample_rate.clone(),
label: a.label.clone(),
secondary: a.secondary,
},
Stream::Subtitle(s) => MetaStream::Subtitle {
pid: s.pid,
codec: codec_to_str(s.codec),
language: s.language.clone(),
forced: s.forced,
},
}).collect();
let streams = title
.streams
.iter()
.map(|s| match s {
Stream::Video(v) => MetaStream::Video {
pid: v.pid,
codec: codec_to_str(v.codec),
resolution: v.resolution.clone(),
frame_rate: v.frame_rate.clone(),
hdr: hdr_to_str(v.hdr),
label: v.label.clone(),
secondary: v.secondary,
},
Stream::Audio(a) => MetaStream::Audio {
pid: a.pid,
codec: codec_to_str(a.codec),
channels: a.channels.clone(),
language: a.language.clone(),
sample_rate: a.sample_rate.clone(),
label: a.label.clone(),
secondary: a.secondary,
},
Stream::Subtitle(s) => MetaStream::Subtitle {
pid: s.pid,
codec: codec_to_str(s.codec),
language: s.language.clone(),
forced: s.forced,
},
})
.collect();
Self {
v: 1,
@@ -102,9 +119,19 @@ impl M2tsMeta {
/// Convert back to a library Title (for remux).
pub fn to_title(&self) -> DiscTitle {
let streams = self.streams.iter().map(|s| match s {
MetaStream::Video { pid, codec, resolution, frame_rate, hdr, label, secondary } => {
Stream::Video(VideoStream {
let streams = self
.streams
.iter()
.map(|s| match s {
MetaStream::Video {
pid,
codec,
resolution,
frame_rate,
hdr,
label,
secondary,
} => Stream::Video(VideoStream {
pid: *pid,
codec: str_to_codec(codec),
resolution: resolution.clone(),
@@ -113,10 +140,16 @@ impl M2tsMeta {
color_space: ColorSpace::Bt709,
secondary: *secondary,
label: label.clone(),
})
}
MetaStream::Audio { pid, codec, channels, language, sample_rate, label, secondary } => {
Stream::Audio(AudioStream {
}),
MetaStream::Audio {
pid,
codec,
channels,
language,
sample_rate,
label,
secondary,
} => Stream::Audio(AudioStream {
pid: *pid,
codec: str_to_codec(codec),
channels: channels.clone(),
@@ -124,17 +157,20 @@ impl M2tsMeta {
sample_rate: sample_rate.clone(),
secondary: *secondary,
label: label.clone(),
})
}
MetaStream::Subtitle { pid, codec, language, forced } => {
Stream::Subtitle(SubtitleStream {
}),
MetaStream::Subtitle {
pid,
codec,
language,
forced,
} => Stream::Subtitle(SubtitleStream {
pid: *pid,
codec: str_to_codec(codec),
language: language.clone(),
forced: *forced,
})
}
}).collect();
}),
})
.collect();
DiscTitle {
playlist: self.title.clone(),
@@ -150,12 +186,11 @@ impl M2tsMeta {
/// Write the metadata header to a writer. Padded to 192-byte boundary.
pub fn write_header(w: &mut impl Write, meta: &M2tsMeta) -> io::Result<()> {
let json = serde_json::to_vec(meta)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let json = serde_json::to_vec(meta).map_err(|e| io::Error::other(e))?;
let json_len = json.len() as u32;
let raw_len = 8 + 4 + json.len(); // magic + len + json
let padded_len = ((raw_len + PACKET_SIZE - 1) / PACKET_SIZE) * PACKET_SIZE;
let padded_len = raw_len.div_ceil(PACKET_SIZE) * PACKET_SIZE;
let padding = padded_len - raw_len;
w.write_all(&MAGIC)?;
@@ -198,7 +233,7 @@ pub fn read_header<R: Read + Seek>(r: &mut R) -> io::Result<Option<M2tsMeta>> {
// Skip padding to next 192-byte boundary
let raw_len = 8 + 4 + json_len;
let padded_len = ((raw_len + PACKET_SIZE - 1) / PACKET_SIZE) * PACKET_SIZE;
let padded_len = raw_len.div_ceil(PACKET_SIZE) * PACKET_SIZE;
let padding = padded_len - raw_len;
if padding > 0 {
r.seek(SeekFrom::Current(padding as i64))?;
@@ -229,7 +264,7 @@ pub fn read_header_from_stream(r: &mut impl Read) -> io::Result<Option<M2tsMeta>
// Skip padding
let raw_len = 8 + 4 + json_len;
let padded_len = ((raw_len + PACKET_SIZE - 1) / PACKET_SIZE) * PACKET_SIZE;
let padded_len = raw_len.div_ceil(PACKET_SIZE) * PACKET_SIZE;
let padding = padded_len - raw_len;
if padding > 0 {
let mut skip = vec![0u8; padding];
@@ -255,7 +290,8 @@ fn codec_to_str(c: Codec) -> String {
Codec::Lpcm => "lpcm",
Codec::Pgs => "pgs",
Codec::Unknown(_) => "unknown",
}.into()
}
.into()
}
fn str_to_codec(s: &str) -> Codec {
@@ -281,7 +317,8 @@ fn hdr_to_str(h: HdrFormat) -> String {
HdrFormat::Sdr => "sdr",
HdrFormat::Hdr10 => "hdr10",
HdrFormat::DolbyVision => "dv",
}.into()
}
.into()
}
fn str_to_hdr(s: &str) -> HdrFormat {