diff --git a/src/decrypt.rs b/src/decrypt.rs index da7c4e0..7398504 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -10,6 +10,7 @@ use crate::css; /// Resolved decryption state from disc scanning. /// Passed to `decrypt_sectors()` — the caller doesn't need to know /// which encryption scheme is in use. +#[derive(Clone)] pub enum DecryptKeys { /// No encryption on this disc. None, @@ -19,9 +20,7 @@ pub enum DecryptKeys { read_data_key: Option<[u8; 16]>, }, /// CSS (DVD). Title key for sector descrambling. - Css { - title_key: [u8; 5], - }, + Css { title_key: [u8; 5] }, } impl DecryptKeys { @@ -38,14 +37,32 @@ impl DecryptKeys { /// For None: no-op. /// /// `unit_key_idx` selects which AACS unit key to use (0 for most discs). -pub fn decrypt_sectors(buf: &mut [u8], keys: &DecryptKeys, unit_key_idx: usize) { +/// +/// Returns `Err` if decryption was expected but keys are missing or invalid. +/// Never produces silently corrupted output. +pub fn decrypt_sectors( + buf: &mut [u8], + keys: &DecryptKeys, + unit_key_idx: usize, +) -> Result<(), crate::error::Error> { match keys { DecryptKeys::None => {} - DecryptKeys::Aacs { unit_keys, read_data_key } => { - let uk = unit_keys - .get(unit_key_idx) - .map(|(_, k)| *k) - .unwrap_or([0u8; 16]); + DecryptKeys::Aacs { + unit_keys, + read_data_key, + } => { + let uk = match unit_keys.get(unit_key_idx) { + Some((_, k)) => *k, + None => { + return Err(crate::error::Error::DecryptFailed { + reason: format!( + "unit_key_idx {} out of range (have {} keys)", + unit_key_idx, + unit_keys.len() + ), + }); + } + }; let rdk = read_data_key.as_ref(); let unit_len = aacs::ALIGNED_UNIT_LEN; @@ -61,4 +78,5 @@ pub fn decrypt_sectors(buf: &mut [u8], keys: &DecryptKeys, unit_key_idx: usize) } } } + Ok(()) } diff --git a/src/disc/mod.rs b/src/disc/mod.rs index 459f794..4e00694 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -148,10 +148,10 @@ pub struct VideoStream { pub pid: u16, /// Codec (HEVC, H.264, VC-1, MPEG-2) pub codec: Codec, - /// Resolution (e.g. "2160p", "1080p", "1080i") - pub resolution: String, - /// Frame rate (e.g. "23.976", "25") - pub frame_rate: String, + /// Resolution + pub resolution: Resolution, + /// Frame rate + pub frame_rate: FrameRate, /// HDR format pub hdr: HdrFormat, /// Color space @@ -169,12 +169,12 @@ pub struct AudioStream { pub pid: u16, /// Codec (TrueHD, DTS-HD MA, DD, LPCM, etc.) pub codec: Codec, - /// Channel layout (e.g. "5.1", "7.1", "stereo", "mono") - pub channels: String, + /// Channel layout + pub channels: AudioChannels, /// ISO 639-2 language code (e.g. "eng", "fra") pub language: String, - /// Sample rate (e.g. "48kHz", "96kHz") - pub sample_rate: String, + /// Sample rate + pub sample_rate: SampleRate, /// Whether this is a secondary stream (commentary) pub secondary: bool, /// Extra label @@ -204,6 +204,8 @@ pub enum Codec { H264, Vc1, Mpeg2, + Mpeg1, + Av1, // Audio TrueHd, DtsHdMa, @@ -212,19 +214,118 @@ pub enum Codec { Ac3, Ac3Plus, Lpcm, + Aac, + Mp2, + Mp3, + Flac, + Opus, // Subtitle Pgs, DvdSub, + Srt, + Ssa, // Unknown Unknown(u8), } +/// Video resolution. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Resolution { + /// 480i (720x480 interlaced) — NTSC DVD + R480i, + /// 480p (720x480 progressive) + R480p, + /// 576i (720x576 interlaced) — PAL DVD + R576i, + /// 576p (720x576 progressive) + R576p, + /// 720p (1280x720 progressive) — some Blu-rays + R720p, + /// 1080i (1920x1080 interlaced) — broadcast, some BD + R1080i, + /// 1080p (1920x1080 progressive) — standard Blu-ray + R1080p, + /// 2160p (3840x2160 progressive) — 4K UHD Blu-ray + R2160p, + /// 4320p (7680x4320 progressive) — 8K, future-proof + R4320p, + /// Unknown resolution + Unknown, +} + +/// Video frame rate. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum FrameRate { + /// 23.976 fps — film-based BD/UHD (NTSC pulldown) + F23_976, + /// 24.000 fps — true film rate + F24, + /// 25.000 fps — PAL standard + F25, + /// 29.970 fps — NTSC standard + F29_97, + /// 30.000 fps + F30, + /// 50.000 fps — PAL high frame rate + F50, + /// 59.940 fps — NTSC high frame rate + F59_94, + /// 60.000 fps + F60, + /// Unknown frame rate + Unknown, +} + +/// Audio channel layout. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AudioChannels { + /// 1.0 mono + Mono, + /// 2.0 stereo + Stereo, + /// 2.1 (stereo + LFE) + Stereo21, + /// 4.0 quadraphonic + Quad, + /// 5.0 surround (no LFE) + Surround50, + /// 5.1 surround — standard BD/DVD surround + Surround51, + /// 6.1 surround (DTS-ES, Dolby EX) + Surround61, + /// 7.1 surround — UHD Atmos beds, DTS:X + Surround71, + /// Unknown channel layout + Unknown, +} + +/// Audio sample rate. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SampleRate { + /// 44.1 kHz — CD audio (rare on disc) + S44_1, + /// 48 kHz — standard BD/DVD/UHD audio + S48, + /// 96 kHz — high-res BD audio + S96, + /// 192 kHz — highest BD audio (LPCM) + S192, + /// 48/96 kHz combo (secondary audio resampled) + S48_96, + /// 48/192 kHz combo (secondary audio resampled) + S48_192, + /// Unknown sample rate + Unknown, +} + /// HDR format. #[derive(Debug, Clone, Copy, PartialEq)] pub enum HdrFormat { Sdr, Hdr10, + Hdr10Plus, DolbyVision, + Hlg, } /// Color space. @@ -254,25 +355,51 @@ pub struct Extent { // ─── Display helpers ──────────────────────────────────────────────────────── impl Codec { + /// Human-readable display name. pub fn name(&self) -> &'static str { - match self { - Codec::Hevc => "HEVC", - Codec::H264 => "H.264", - Codec::Vc1 => "VC-1", - Codec::Mpeg2 => "MPEG-2", - Codec::TrueHd => "TrueHD", - Codec::DtsHdMa => "DTS-HD MA", - Codec::DtsHdHr => "DTS-HD HR", - Codec::Dts => "DTS", - Codec::Ac3 => "AC-3", - Codec::Ac3Plus => "AC-3+", - Codec::Lpcm => "LPCM", - Codec::Pgs => "PGS", - Codec::DvdSub => "DVD Subtitle", - Codec::Unknown(_) => "Unknown", + for (_, name, v) in Self::ALL_CODECS { + if v == self { + return name; + } } + "Unknown" } + /// Compact identifier for serialization (lowercase, no spaces). + pub fn id(&self) -> &'static str { + for (id, _, v) in Self::ALL_CODECS { + if v == self { + return id; + } + } + "unknown" + } + + const ALL_CODECS: &[(&'static str, &'static str, Codec)] = &[ + ("hevc", "HEVC", Codec::Hevc), + ("h264", "H.264", Codec::H264), + ("vc1", "VC-1", Codec::Vc1), + ("mpeg2", "MPEG-2", Codec::Mpeg2), + ("mpeg1", "MPEG-1", Codec::Mpeg1), + ("av1", "AV1", Codec::Av1), + ("truehd", "TrueHD", Codec::TrueHd), + ("dtshd_ma", "DTS-HD MA", Codec::DtsHdMa), + ("dtshd_hr", "DTS-HD HR", Codec::DtsHdHr), + ("dts", "DTS", Codec::Dts), + ("ac3", "AC-3", Codec::Ac3), + ("eac3", "EAC-3", Codec::Ac3Plus), + ("lpcm", "LPCM", Codec::Lpcm), + ("aac", "AAC", Codec::Aac), + ("mp2", "MP2", Codec::Mp2), + ("mp3", "MP3", Codec::Mp3), + ("flac", "FLAC", Codec::Flac), + ("opus", "Opus", Codec::Opus), + ("pgs", "PGS", Codec::Pgs), + ("dvdsub", "DVD Subtitle", Codec::DvdSub), + ("srt", "SRT", Codec::Srt), + ("ssa", "SSA", Codec::Ssa), + ]; + fn from_coding_type(ct: u8) -> Self { match ct { 0x24 => Codec::Hevc, @@ -293,14 +420,233 @@ impl Codec { } } +impl std::fmt::Display for Codec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.name()) + } +} + +impl Resolution { + /// Parse from MPLS video_format byte. + pub fn from_video_format(vf: u8) -> Self { + match vf { + 1 => Resolution::R480i, + 2 => Resolution::R576i, + 3 => Resolution::R480p, + 4 => Resolution::R1080i, + 5 => Resolution::R720p, + 6 => Resolution::R1080p, + 7 => Resolution::R576p, + 8 => Resolution::R2160p, + _ => Resolution::Unknown, + } + } + + /// Pixel dimensions (width, height). + pub fn pixels(&self) -> (u32, u32) { + match self { + Resolution::R480i | Resolution::R480p => (720, 480), + Resolution::R576i | Resolution::R576p => (720, 576), + Resolution::R720p => (1280, 720), + Resolution::R1080i | Resolution::R1080p => (1920, 1080), + Resolution::R2160p => (3840, 2160), + Resolution::R4320p => (7680, 4320), + Resolution::Unknown => (1920, 1080), + } + } + + /// True if this is a UHD (4K+) resolution. + pub fn is_uhd(&self) -> bool { + matches!(self, Resolution::R2160p | Resolution::R4320p) + } + + /// True if this is an HD (720p+) resolution. + pub fn is_hd(&self) -> bool { + !matches!( + self, + Resolution::R480i + | Resolution::R480p + | Resolution::R576i + | Resolution::R576p + | Resolution::Unknown + ) + } + + /// True if this is an SD (480/576) resolution. + pub fn is_sd(&self) -> bool { + matches!( + self, + Resolution::R480i | Resolution::R480p | Resolution::R576i | Resolution::R576p + ) + } + + /// Parse from pixel height (e.g. from MKV track). + pub fn from_height(h: u32) -> Self { + match h { + 0..=480 => Resolution::R480p, + 481..=576 => Resolution::R576p, + 577..=720 => Resolution::R720p, + 721..=1080 => Resolution::R1080p, + 1081..=2160 => Resolution::R2160p, + _ => Resolution::R4320p, + } + } +} + +// Display for Resolution is generated by enum_str! macro + +impl FrameRate { + /// Parse from MPLS video_rate byte. + pub fn from_video_rate(vr: u8) -> Self { + match vr { + 1 => FrameRate::F23_976, + 2 => FrameRate::F24, + 3 => FrameRate::F25, + 4 => FrameRate::F29_97, + 5 => FrameRate::F30, + 6 => FrameRate::F50, + 7 => FrameRate::F59_94, + 8 => FrameRate::F60, + _ => FrameRate::Unknown, + } + } + + /// Frame rate as (numerator, denominator) for precise representation. + pub fn as_fraction(&self) -> (u32, u32) { + match self { + FrameRate::F23_976 => (24000, 1001), + FrameRate::F24 => (24, 1), + FrameRate::F25 => (25, 1), + FrameRate::F29_97 => (30000, 1001), + FrameRate::F30 => (30, 1), + FrameRate::F50 => (50, 1), + FrameRate::F59_94 => (60000, 1001), + FrameRate::F60 => (60, 1), + FrameRate::Unknown => (0, 1), + } + } +} + +// Display for FrameRate is generated by enum_str! macro + +impl AudioChannels { + /// Parse from MPLS audio_format byte. + pub fn from_audio_format(af: u8) -> Self { + match af { + 1 => AudioChannels::Mono, + 3 => AudioChannels::Stereo, + 6 => AudioChannels::Surround51, + 12 => AudioChannels::Surround71, + _ if af > 0 => AudioChannels::Unknown, + _ => AudioChannels::Unknown, + } + } + + /// Channel count as a number. + pub fn count(&self) -> u8 { + match self { + AudioChannels::Mono => 1, + AudioChannels::Stereo => 2, + AudioChannels::Stereo21 => 3, + AudioChannels::Quad => 4, + AudioChannels::Surround50 => 5, + AudioChannels::Surround51 => 6, + AudioChannels::Surround61 => 7, + AudioChannels::Surround71 => 8, + AudioChannels::Unknown => 6, + } + } + + /// Parse from channel count number. + pub fn from_count(n: u8) -> Self { + match n { + 1 => AudioChannels::Mono, + 2 => AudioChannels::Stereo, + 3 => AudioChannels::Stereo21, + 4 => AudioChannels::Quad, + 5 => AudioChannels::Surround50, + 6 => AudioChannels::Surround51, + 7 => AudioChannels::Surround61, + 8 => AudioChannels::Surround71, + _ => AudioChannels::Unknown, + } + } +} + +// Display for AudioChannels is generated by enum_str! macro + +impl SampleRate { + /// Parse from MPLS audio_rate byte. + pub fn from_audio_rate(ar: u8) -> Self { + match ar { + 1 => SampleRate::S48, + 4 => SampleRate::S96, + 5 => SampleRate::S192, + 12 => SampleRate::S48_192, + 14 => SampleRate::S48_96, + _ => SampleRate::Unknown, + } + } + + /// Sample rate in Hz (primary rate for combo rates). + pub fn hz(&self) -> f64 { + match self { + SampleRate::S44_1 => 44100.0, + SampleRate::S48 | SampleRate::S48_96 | SampleRate::S48_192 => 48000.0, + SampleRate::S96 => 96000.0, + SampleRate::S192 => 192000.0, + SampleRate::Unknown => 48000.0, + } + } + + /// Parse from Hz value. + pub fn from_hz(hz: u32) -> Self { + match hz { + 44100 => SampleRate::S44_1, + 48000 => SampleRate::S48, + 96000 => SampleRate::S96, + 192000 => SampleRate::S192, + _ => SampleRate::Unknown, + } + } +} + +// Display for SampleRate is generated by enum_str! macro + impl HdrFormat { pub fn name(&self) -> &'static str { match self { HdrFormat::Sdr => "SDR", HdrFormat::Hdr10 => "HDR10", + HdrFormat::Hdr10Plus => "HDR10+", HdrFormat::DolbyVision => "Dolby Vision", + HdrFormat::Hlg => "HLG", } } + + const ALL_HDR: &[(&'static str, HdrFormat)] = &[ + ("sdr", HdrFormat::Sdr), + ("hdr10", HdrFormat::Hdr10), + ("hdr10+", HdrFormat::Hdr10Plus), + ("dv", HdrFormat::DolbyVision), + ("hlg", HdrFormat::Hlg), + ]; + + /// Compact identifier for serialization. + pub fn id(&self) -> &'static str { + for (id, v) in Self::ALL_HDR { + if v == self { + return id; + } + } + "sdr" + } +} + +impl std::fmt::Display for HdrFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.name()) + } } impl ColorSpace { @@ -313,6 +659,131 @@ impl ColorSpace { } } +impl std::fmt::Display for ColorSpace { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.name()) + } +} + +// ─── FromStr impls — single source of truth via ALL_* arrays ─────────────── +// +// Each enum defines a const array of (str, variant) pairs. Display, FromStr, +// and id() all derive from this one table — no string appears twice. + +macro_rules! enum_str { + ($name:ident, $default:expr, [ $( ($s:expr, $v:expr) ),* $(,)? ]) => { + impl $name { + const ALL: &[(&'static str, $name)] = &[ $( ($s, $v), )* ]; + } + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (s, v) in $name::ALL { + if v == self { return f.write_str(s); } + } + f.write_str("") + } + } + impl std::str::FromStr for $name { + type Err = (); + fn from_str(s: &str) -> std::result::Result { + for (k, v) in $name::ALL { + if *k == s { return Ok(*v); } + } + Ok($default) + } + } + }; +} + +enum_str!( + Resolution, + Resolution::Unknown, + [ + ("480i", Resolution::R480i), + ("480p", Resolution::R480p), + ("576i", Resolution::R576i), + ("576p", Resolution::R576p), + ("720p", Resolution::R720p), + ("1080i", Resolution::R1080i), + ("1080p", Resolution::R1080p), + ("2160p", Resolution::R2160p), + ("4320p", Resolution::R4320p), + ] +); + +enum_str!( + FrameRate, + FrameRate::Unknown, + [ + ("23.976", FrameRate::F23_976), + ("24", FrameRate::F24), + ("25", FrameRate::F25), + ("29.97", FrameRate::F29_97), + ("30", FrameRate::F30), + ("50", FrameRate::F50), + ("59.94", FrameRate::F59_94), + ("60", FrameRate::F60), + ] +); + +enum_str!( + AudioChannels, + AudioChannels::Unknown, + [ + ("mono", AudioChannels::Mono), + ("stereo", AudioChannels::Stereo), + ("2.1", AudioChannels::Stereo21), + ("4.0", AudioChannels::Quad), + ("5.0", AudioChannels::Surround50), + ("5.1", AudioChannels::Surround51), + ("6.1", AudioChannels::Surround61), + ("7.1", AudioChannels::Surround71), + ] +); + +enum_str!( + SampleRate, + SampleRate::Unknown, + [ + ("44.1kHz", SampleRate::S44_1), + ("48kHz", SampleRate::S48), + ("96kHz", SampleRate::S96), + ("192kHz", SampleRate::S192), + ("48/96kHz", SampleRate::S48_96), + ("48/192kHz", SampleRate::S48_192), + ] +); + +impl std::str::FromStr for Codec { + type Err = (); + fn from_str(s: &str) -> std::result::Result { + for (id, _, v) in Codec::ALL_CODECS { + if *id == s { + return Ok(*v); + } + } + Ok(Codec::Unknown(0)) + } +} + +impl std::str::FromStr for HdrFormat { + type Err = (); + fn from_str(s: &str) -> std::result::Result { + for (id, v) in HdrFormat::ALL_HDR { + if *id == s { + return Ok(*v); + } + } + // Also accept display names + for (_id, v) in HdrFormat::ALL_HDR { + if HdrFormat::name(v) == s { + return Ok(*v); + } + } + Ok(HdrFormat::Sdr) + } +} + impl DiscTitle { /// Empty DiscTitle with no streams. pub fn empty() -> Self { @@ -633,13 +1104,13 @@ impl Disc { for title in titles.iter().take(3) { for stream in &title.streams { if let Stream::Video(v) = stream { - if v.resolution.contains("2160") { + if v.resolution.is_uhd() { return DiscFormat::Uhd; } - if v.resolution.contains("1080") || v.resolution.contains("720") { + if v.resolution.is_hd() { return DiscFormat::BluRay; } - if v.resolution.contains("480") || v.resolution.contains("576") { + if v.resolution.is_sd() { return DiscFormat::Dvd; } } @@ -782,7 +1253,7 @@ impl Disc { /// Reads /sys/block//queue/max_hw_sectors_kb on Linux. /// For sg devices, resolves the corresponding block device via sysfs. /// Returns a value aligned to 3 sectors (one aligned unit). -pub(crate) fn detect_max_batch_sectors(device_path: &str) -> u16 { +pub fn detect_max_batch_sectors(device_path: &str) -> u16 { let dev_name = device_path.rsplit('/').next().unwrap_or(""); if dev_name.is_empty() { return DEFAULT_BATCH_SECTORS; @@ -850,7 +1321,7 @@ impl<'a> ContentReader<'a> { let mut unit = self.read_buf[start..end].to_vec(); // Decrypt if needed - self.decrypt_unit(&mut unit); + self.decrypt_unit(&mut unit)?; self.buf_pos += 1; Ok(Some(unit)) } @@ -871,14 +1342,14 @@ impl<'a> ContentReader<'a> { &mut self.read_buf[..total_bytes], &self.decrypt_keys, self.unit_key_idx, - ); + )?; self.buf_pos = self.buf_len; Ok(Some(&self.read_buf[..total_bytes])) } /// Decrypt a single aligned unit in-place if needed. - fn decrypt_unit(&self, unit: &mut [u8]) { - crate::decrypt::decrypt_sectors(unit, &self.decrypt_keys, self.unit_key_idx); + fn decrypt_unit(&self, unit: &mut [u8]) -> Result<()> { + crate::decrypt::decrypt_sectors(unit, &self.decrypt_keys, self.unit_key_idx) } /// Read sectors via standard READ(10) 0x00. @@ -1004,60 +1475,14 @@ impl<'a> ContentReader<'a> { // ─── Format helpers ──────────────────────────────────────────────────────── -fn format_resolution(video_format: u8, _video_rate: u8) -> String { - match video_format { - 1 => "480i".into(), - 2 => "576i".into(), - 3 => "480p".into(), - 4 => "1080i".into(), - 5 => "720p".into(), - 6 => "1080p".into(), - 7 => "576p".into(), - 8 => "2160p".into(), - _ => String::new(), - } -} - -fn format_framerate(video_rate: u8) -> String { - match video_rate { - 1 => "23.976".into(), - 2 => "24".into(), - 3 => "25".into(), - 4 => "29.97".into(), - 6 => "50".into(), - 7 => "59.94".into(), - _ => String::new(), - } -} - -fn format_channels(audio_format: u8) -> String { - match audio_format { - 1 => "mono".into(), - 3 => "stereo".into(), - 6 => "5.1".into(), - 12 => "7.1".into(), - _ if audio_format > 0 => format!("{audio_format}ch"), - _ => String::new(), - } -} - -fn format_samplerate(audio_rate: u8) -> String { - match audio_rate { - 1 => "48kHz".into(), - 4 => "96kHz".into(), - 5 => "192kHz".into(), - 12 => "48/192kHz".into(), - 14 => "48/96kHz".into(), - _ => String::new(), - } -} +// Old format_* functions replaced by Resolution/FrameRate/AudioChannels/SampleRate enums #[cfg(test)] mod tests { use super::*; /// Helper: build a DiscTitle with a single video stream at the given resolution. - fn title_with_video(codec: Codec, resolution: &str) -> DiscTitle { + fn title_with_video(codec: Codec, resolution: Resolution) -> DiscTitle { DiscTitle { playlist: "00800.mpls".into(), playlist_id: 800, @@ -1067,8 +1492,8 @@ mod tests { streams: vec![Stream::Video(VideoStream { pid: 0x1011, codec, - resolution: resolution.into(), - frame_rate: "23.976".into(), + resolution, + frame_rate: FrameRate::F23_976, hdr: HdrFormat::Sdr, color_space: ColorSpace::Bt709, secondary: false, @@ -1082,19 +1507,19 @@ mod tests { #[test] fn detect_format_uhd() { - let titles = vec![title_with_video(Codec::Hevc, "2160p")]; + let titles = vec![title_with_video(Codec::Hevc, Resolution::R2160p)]; assert_eq!(Disc::detect_format(&titles), DiscFormat::Uhd); } #[test] fn detect_format_bluray() { - let titles = vec![title_with_video(Codec::H264, "1080p")]; + let titles = vec![title_with_video(Codec::H264, Resolution::R1080p)]; assert_eq!(Disc::detect_format(&titles), DiscFormat::BluRay); } #[test] fn detect_format_dvd() { - let titles = vec![title_with_video(Codec::Mpeg2, "480i")]; + let titles = vec![title_with_video(Codec::Mpeg2, Resolution::R480i)]; assert_eq!(Disc::detect_format(&titles), DiscFormat::Dvd); } @@ -1106,7 +1531,7 @@ mod tests { #[test] fn content_format_default_bdts() { - let t = title_with_video(Codec::H264, "1080p"); + let t = title_with_video(Codec::H264, Resolution::R1080p); assert_eq!(t.content_format, ContentFormat::BdTs); } @@ -1114,7 +1539,7 @@ mod tests { fn content_format_dvd_mpegps() { let t = DiscTitle { content_format: ContentFormat::MpegPs, - ..title_with_video(Codec::Mpeg2, "480i") + ..title_with_video(Codec::Mpeg2, Resolution::R480i) }; assert_eq!(t.content_format, ContentFormat::MpegPs); } diff --git a/src/error.rs b/src/error.rs index 7f485db..bd1be50 100644 --- a/src/error.rs +++ b/src/error.rs @@ -55,6 +55,7 @@ pub const E_AACS_VID_READ: u16 = 7009; pub const E_AACS_VID_MAC: u16 = 7010; pub const E_AACS_DATA_KEY: u16 = 7011; pub const E_AACS_VUK_DERIVE: u16 = 7012; +pub const E_DECRYPT_FAILED: u16 = 7013; // Keydb (8xxx) pub const E_KEYDB_CONNECT: u16 = 8000; pub const E_KEYDB_HTTP: u16 = 8001; @@ -72,13 +73,9 @@ pub const E_MUX_WRITE: u16 = 9001; #[derive(Debug)] pub enum Error { /// Device not found at the given path. - DeviceNotFound { - path: String, - }, + DeviceNotFound { path: String }, /// Insufficient permissions to open the device. - DevicePermission { - path: String, - }, + DevicePermission { path: String }, /// Drive model is not in the profile database. UnsupportedDrive { @@ -98,10 +95,7 @@ pub enum Error { /// Drive unlock (firmware upload) failed. UnlockFailed, /// Firmware signature verification failed. - SignatureMismatch { - expected: [u8; 4], - got: [u8; 4], - }, + SignatureMismatch { expected: [u8; 4], got: [u8; 4] }, /// Operation requires an unlocked drive. NotUnlocked, /// Operation requires a calibrated drive. @@ -114,36 +108,25 @@ pub enum Error { sense_key: u8, }, /// SCSI command timed out. - ScsiTimeout { - opcode: u8, - }, + ScsiTimeout { opcode: u8 }, /// Underlying I/O error. - IoError { - source: std::io::Error, - }, + IoError { source: std::io::Error }, /// Write operation failed. WriteError, /// Failed to read disc sector. - DiscRead { - sector: u64, - }, + DiscRead { sector: u64 }, /// MPLS playlist parsing failed. MplsParse, /// CLPI clip info parsing failed. ClpiParse, /// File not found on the UDF filesystem. - UdfNotFound { - path: String, - }, + UdfNotFound { path: String }, /// Disc contains no playable titles. DiscNoTitles, /// Title index out of range. - DiscTitleRange { - index: usize, - count: usize, - }, + DiscTitleRange { index: usize, count: usize }, /// Title has no sector extents to read. DiscNoExtents, /// DVD IFO file parsing failed. @@ -175,27 +158,21 @@ pub enum Error { AacsDataKey, /// Failed to derive the Volume Unique Key. AacsVukDerive, + /// Decryption failed — missing or invalid keys. + DecryptFailed { reason: String }, /// Failed to connect to the KEYDB server. - KeydbConnect { - host: String, - }, + KeydbConnect { host: String }, /// KEYDB server returned an HTTP error. - KeydbHttp { - status: u16, - }, + KeydbHttp { status: u16 }, /// Downloaded KEYDB file is invalid (no entries found). KeydbInvalid, /// Failed to write KEYDB to disk. - KeydbWrite { - path: String, - }, + KeydbWrite { path: String }, /// Failed to parse KEYDB file. KeydbParse, /// Failed to load KEYDB from disk. - KeydbLoad { - path: String, - }, + KeydbLoad { path: String }, /// Lookahead buffer exhausted before codec headers found. MuxLookahead, @@ -240,6 +217,7 @@ impl Error { Error::AacsVidMac => E_AACS_VID_MAC, Error::AacsDataKey => E_AACS_DATA_KEY, Error::AacsVukDerive => E_AACS_VUK_DERIVE, + Error::DecryptFailed { .. } => E_DECRYPT_FAILED, Error::KeydbConnect { .. } => E_KEYDB_CONNECT, Error::KeydbHttp { .. } => E_KEYDB_HTTP, Error::KeydbInvalid => E_KEYDB_INVALID, @@ -318,6 +296,7 @@ impl std::fmt::Display for Error { Error::KeydbHttp { status } => write!(f, "E{}: {}", self.code(), status), Error::KeydbWrite { path } => write!(f, "E{}: {}", self.code(), path), Error::KeydbLoad { path } => write!(f, "E{}: {}", self.code(), path), + Error::DecryptFailed { reason } => write!(f, "E{}: {}", self.code(), reason), // Simple codes — no extra data _ => write!(f, "E{}", self.code()), } diff --git a/src/mux/iso.rs b/src/mux/iso.rs index 1513b33..3b83da1 100644 --- a/src/mux/iso.rs +++ b/src/mux/iso.rs @@ -205,7 +205,8 @@ impl IsoStream { // Decrypt after read — stream handles its own decryption let bytes = count as usize * SECTOR_SIZE as usize; - decrypt_sectors(&mut self.batch_buf[..bytes], &self.decrypt_keys, 0); + decrypt_sectors(&mut self.batch_buf[..bytes], &self.decrypt_keys, 0) + .map_err(|e| io::Error::other(e.to_string()))?; self.buf_pos = 0; self.buf_len = bytes;