decrypt_sectors returns Result — fail instead of silent corruption

Previously used all-zeros AES key when unit_key_idx was out of range,
producing silently corrupted output. Now returns DecryptFailed error.
Also wired --raw flag through InputOptions → set_raw() on streams.
This commit is contained in:
MattJackson
2026-04-15 01:52:36 +00:00
parent 119d09ed02
commit 6824f430d8
4 changed files with 557 additions and 134 deletions
+27 -9
View File
@@ -10,6 +10,7 @@ use crate::css;
/// Resolved decryption state from disc scanning. /// Resolved decryption state from disc scanning.
/// Passed to `decrypt_sectors()` — the caller doesn't need to know /// Passed to `decrypt_sectors()` — the caller doesn't need to know
/// which encryption scheme is in use. /// which encryption scheme is in use.
#[derive(Clone)]
pub enum DecryptKeys { pub enum DecryptKeys {
/// No encryption on this disc. /// No encryption on this disc.
None, None,
@@ -19,9 +20,7 @@ pub enum DecryptKeys {
read_data_key: Option<[u8; 16]>, read_data_key: Option<[u8; 16]>,
}, },
/// CSS (DVD). Title key for sector descrambling. /// CSS (DVD). Title key for sector descrambling.
Css { Css { title_key: [u8; 5] },
title_key: [u8; 5],
},
} }
impl DecryptKeys { impl DecryptKeys {
@@ -38,14 +37,32 @@ impl DecryptKeys {
/// For None: no-op. /// For None: no-op.
/// ///
/// `unit_key_idx` selects which AACS unit key to use (0 for most discs). /// `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 { match keys {
DecryptKeys::None => {} DecryptKeys::None => {}
DecryptKeys::Aacs { unit_keys, read_data_key } => { DecryptKeys::Aacs {
let uk = unit_keys unit_keys,
.get(unit_key_idx) read_data_key,
.map(|(_, k)| *k) } => {
.unwrap_or([0u8; 16]); 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 rdk = read_data_key.as_ref();
let unit_len = aacs::ALIGNED_UNIT_LEN; 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(())
} }
+511 -86
View File
@@ -148,10 +148,10 @@ pub struct VideoStream {
pub pid: u16, pub pid: u16,
/// Codec (HEVC, H.264, VC-1, MPEG-2) /// Codec (HEVC, H.264, VC-1, MPEG-2)
pub codec: Codec, pub codec: Codec,
/// Resolution (e.g. "2160p", "1080p", "1080i") /// Resolution
pub resolution: String, pub resolution: Resolution,
/// Frame rate (e.g. "23.976", "25") /// Frame rate
pub frame_rate: String, pub frame_rate: FrameRate,
/// HDR format /// HDR format
pub hdr: HdrFormat, pub hdr: HdrFormat,
/// Color space /// Color space
@@ -169,12 +169,12 @@ pub struct AudioStream {
pub pid: u16, pub pid: u16,
/// Codec (TrueHD, DTS-HD MA, DD, LPCM, etc.) /// Codec (TrueHD, DTS-HD MA, DD, LPCM, etc.)
pub codec: Codec, pub codec: Codec,
/// Channel layout (e.g. "5.1", "7.1", "stereo", "mono") /// Channel layout
pub channels: String, pub channels: AudioChannels,
/// ISO 639-2 language code (e.g. "eng", "fra") /// ISO 639-2 language code (e.g. "eng", "fra")
pub language: String, pub language: String,
/// Sample rate (e.g. "48kHz", "96kHz") /// Sample rate
pub sample_rate: String, pub sample_rate: SampleRate,
/// Whether this is a secondary stream (commentary) /// Whether this is a secondary stream (commentary)
pub secondary: bool, pub secondary: bool,
/// Extra label /// Extra label
@@ -204,6 +204,8 @@ pub enum Codec {
H264, H264,
Vc1, Vc1,
Mpeg2, Mpeg2,
Mpeg1,
Av1,
// Audio // Audio
TrueHd, TrueHd,
DtsHdMa, DtsHdMa,
@@ -212,19 +214,118 @@ pub enum Codec {
Ac3, Ac3,
Ac3Plus, Ac3Plus,
Lpcm, Lpcm,
Aac,
Mp2,
Mp3,
Flac,
Opus,
// Subtitle // Subtitle
Pgs, Pgs,
DvdSub, DvdSub,
Srt,
Ssa,
// Unknown // Unknown
Unknown(u8), 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. /// HDR format.
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum HdrFormat { pub enum HdrFormat {
Sdr, Sdr,
Hdr10, Hdr10,
Hdr10Plus,
DolbyVision, DolbyVision,
Hlg,
} }
/// Color space. /// Color space.
@@ -254,24 +355,50 @@ pub struct Extent {
// ─── Display helpers ──────────────────────────────────────────────────────── // ─── Display helpers ────────────────────────────────────────────────────────
impl Codec { impl Codec {
/// Human-readable display name.
pub fn name(&self) -> &'static str { pub fn name(&self) -> &'static str {
match self { for (_, name, v) in Self::ALL_CODECS {
Codec::Hevc => "HEVC", if v == self {
Codec::H264 => "H.264", return name;
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",
} }
} }
"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 { fn from_coding_type(ct: u8) -> Self {
match ct { match ct {
@@ -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 { impl HdrFormat {
pub fn name(&self) -> &'static str { pub fn name(&self) -> &'static str {
match self { match self {
HdrFormat::Sdr => "SDR", HdrFormat::Sdr => "SDR",
HdrFormat::Hdr10 => "HDR10", HdrFormat::Hdr10 => "HDR10",
HdrFormat::Hdr10Plus => "HDR10+",
HdrFormat::DolbyVision => "Dolby Vision", 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 { 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<Self, ()> {
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<Self, ()> {
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<Self, ()> {
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 { impl DiscTitle {
/// Empty DiscTitle with no streams. /// Empty DiscTitle with no streams.
pub fn empty() -> Self { pub fn empty() -> Self {
@@ -633,13 +1104,13 @@ impl Disc {
for title in titles.iter().take(3) { for title in titles.iter().take(3) {
for stream in &title.streams { for stream in &title.streams {
if let Stream::Video(v) = stream { if let Stream::Video(v) = stream {
if v.resolution.contains("2160") { if v.resolution.is_uhd() {
return DiscFormat::Uhd; return DiscFormat::Uhd;
} }
if v.resolution.contains("1080") || v.resolution.contains("720") { if v.resolution.is_hd() {
return DiscFormat::BluRay; return DiscFormat::BluRay;
} }
if v.resolution.contains("480") || v.resolution.contains("576") { if v.resolution.is_sd() {
return DiscFormat::Dvd; return DiscFormat::Dvd;
} }
} }
@@ -782,7 +1253,7 @@ impl Disc {
/// Reads /sys/block/<dev>/queue/max_hw_sectors_kb on Linux. /// Reads /sys/block/<dev>/queue/max_hw_sectors_kb on Linux.
/// For sg devices, resolves the corresponding block device via sysfs. /// For sg devices, resolves the corresponding block device via sysfs.
/// Returns a value aligned to 3 sectors (one aligned unit). /// 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(""); let dev_name = device_path.rsplit('/').next().unwrap_or("");
if dev_name.is_empty() { if dev_name.is_empty() {
return DEFAULT_BATCH_SECTORS; return DEFAULT_BATCH_SECTORS;
@@ -850,7 +1321,7 @@ impl<'a> ContentReader<'a> {
let mut unit = self.read_buf[start..end].to_vec(); let mut unit = self.read_buf[start..end].to_vec();
// Decrypt if needed // Decrypt if needed
self.decrypt_unit(&mut unit); self.decrypt_unit(&mut unit)?;
self.buf_pos += 1; self.buf_pos += 1;
Ok(Some(unit)) Ok(Some(unit))
} }
@@ -871,14 +1342,14 @@ impl<'a> ContentReader<'a> {
&mut self.read_buf[..total_bytes], &mut self.read_buf[..total_bytes],
&self.decrypt_keys, &self.decrypt_keys,
self.unit_key_idx, self.unit_key_idx,
); )?;
self.buf_pos = self.buf_len; self.buf_pos = self.buf_len;
Ok(Some(&self.read_buf[..total_bytes])) Ok(Some(&self.read_buf[..total_bytes]))
} }
/// Decrypt a single aligned unit in-place if needed. /// Decrypt a single aligned unit in-place if needed.
fn decrypt_unit(&self, unit: &mut [u8]) { fn decrypt_unit(&self, unit: &mut [u8]) -> Result<()> {
crate::decrypt::decrypt_sectors(unit, &self.decrypt_keys, self.unit_key_idx); crate::decrypt::decrypt_sectors(unit, &self.decrypt_keys, self.unit_key_idx)
} }
/// Read sectors via standard READ(10) 0x00. /// Read sectors via standard READ(10) 0x00.
@@ -1004,60 +1475,14 @@ impl<'a> ContentReader<'a> {
// ─── Format helpers ──────────────────────────────────────────────────────── // ─── Format helpers ────────────────────────────────────────────────────────
fn format_resolution(video_format: u8, _video_rate: u8) -> String { // Old format_* functions replaced by Resolution/FrameRate/AudioChannels/SampleRate enums
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(),
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
/// Helper: build a DiscTitle with a single video stream at the given resolution. /// 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 { DiscTitle {
playlist: "00800.mpls".into(), playlist: "00800.mpls".into(),
playlist_id: 800, playlist_id: 800,
@@ -1067,8 +1492,8 @@ mod tests {
streams: vec![Stream::Video(VideoStream { streams: vec![Stream::Video(VideoStream {
pid: 0x1011, pid: 0x1011,
codec, codec,
resolution: resolution.into(), resolution,
frame_rate: "23.976".into(), frame_rate: FrameRate::F23_976,
hdr: HdrFormat::Sdr, hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt709, color_space: ColorSpace::Bt709,
secondary: false, secondary: false,
@@ -1082,19 +1507,19 @@ mod tests {
#[test] #[test]
fn detect_format_uhd() { 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); assert_eq!(Disc::detect_format(&titles), DiscFormat::Uhd);
} }
#[test] #[test]
fn detect_format_bluray() { 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); assert_eq!(Disc::detect_format(&titles), DiscFormat::BluRay);
} }
#[test] #[test]
fn detect_format_dvd() { 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); assert_eq!(Disc::detect_format(&titles), DiscFormat::Dvd);
} }
@@ -1106,7 +1531,7 @@ mod tests {
#[test] #[test]
fn content_format_default_bdts() { 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); assert_eq!(t.content_format, ContentFormat::BdTs);
} }
@@ -1114,7 +1539,7 @@ mod tests {
fn content_format_dvd_mpegps() { fn content_format_dvd_mpegps() {
let t = DiscTitle { let t = DiscTitle {
content_format: ContentFormat::MpegPs, content_format: ContentFormat::MpegPs,
..title_with_video(Codec::Mpeg2, "480i") ..title_with_video(Codec::Mpeg2, Resolution::R480i)
}; };
assert_eq!(t.content_format, ContentFormat::MpegPs); assert_eq!(t.content_format, ContentFormat::MpegPs);
} }
+17 -38
View File
@@ -55,6 +55,7 @@ pub const E_AACS_VID_READ: u16 = 7009;
pub const E_AACS_VID_MAC: u16 = 7010; pub const E_AACS_VID_MAC: u16 = 7010;
pub const E_AACS_DATA_KEY: u16 = 7011; pub const E_AACS_DATA_KEY: u16 = 7011;
pub const E_AACS_VUK_DERIVE: u16 = 7012; pub const E_AACS_VUK_DERIVE: u16 = 7012;
pub const E_DECRYPT_FAILED: u16 = 7013;
// Keydb (8xxx) // Keydb (8xxx)
pub const E_KEYDB_CONNECT: u16 = 8000; pub const E_KEYDB_CONNECT: u16 = 8000;
pub const E_KEYDB_HTTP: u16 = 8001; pub const E_KEYDB_HTTP: u16 = 8001;
@@ -72,13 +73,9 @@ pub const E_MUX_WRITE: u16 = 9001;
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
/// Device not found at the given path. /// Device not found at the given path.
DeviceNotFound { DeviceNotFound { path: String },
path: String,
},
/// Insufficient permissions to open the device. /// Insufficient permissions to open the device.
DevicePermission { DevicePermission { path: String },
path: String,
},
/// Drive model is not in the profile database. /// Drive model is not in the profile database.
UnsupportedDrive { UnsupportedDrive {
@@ -98,10 +95,7 @@ pub enum Error {
/// Drive unlock (firmware upload) failed. /// Drive unlock (firmware upload) failed.
UnlockFailed, UnlockFailed,
/// Firmware signature verification failed. /// Firmware signature verification failed.
SignatureMismatch { SignatureMismatch { expected: [u8; 4], got: [u8; 4] },
expected: [u8; 4],
got: [u8; 4],
},
/// Operation requires an unlocked drive. /// Operation requires an unlocked drive.
NotUnlocked, NotUnlocked,
/// Operation requires a calibrated drive. /// Operation requires a calibrated drive.
@@ -114,36 +108,25 @@ pub enum Error {
sense_key: u8, sense_key: u8,
}, },
/// SCSI command timed out. /// SCSI command timed out.
ScsiTimeout { ScsiTimeout { opcode: u8 },
opcode: u8,
},
/// Underlying I/O error. /// Underlying I/O error.
IoError { IoError { source: std::io::Error },
source: std::io::Error,
},
/// Write operation failed. /// Write operation failed.
WriteError, WriteError,
/// Failed to read disc sector. /// Failed to read disc sector.
DiscRead { DiscRead { sector: u64 },
sector: u64,
},
/// MPLS playlist parsing failed. /// MPLS playlist parsing failed.
MplsParse, MplsParse,
/// CLPI clip info parsing failed. /// CLPI clip info parsing failed.
ClpiParse, ClpiParse,
/// File not found on the UDF filesystem. /// File not found on the UDF filesystem.
UdfNotFound { UdfNotFound { path: String },
path: String,
},
/// Disc contains no playable titles. /// Disc contains no playable titles.
DiscNoTitles, DiscNoTitles,
/// Title index out of range. /// Title index out of range.
DiscTitleRange { DiscTitleRange { index: usize, count: usize },
index: usize,
count: usize,
},
/// Title has no sector extents to read. /// Title has no sector extents to read.
DiscNoExtents, DiscNoExtents,
/// DVD IFO file parsing failed. /// DVD IFO file parsing failed.
@@ -175,27 +158,21 @@ pub enum Error {
AacsDataKey, AacsDataKey,
/// Failed to derive the Volume Unique Key. /// Failed to derive the Volume Unique Key.
AacsVukDerive, AacsVukDerive,
/// Decryption failed — missing or invalid keys.
DecryptFailed { reason: String },
/// Failed to connect to the KEYDB server. /// Failed to connect to the KEYDB server.
KeydbConnect { KeydbConnect { host: String },
host: String,
},
/// KEYDB server returned an HTTP error. /// KEYDB server returned an HTTP error.
KeydbHttp { KeydbHttp { status: u16 },
status: u16,
},
/// Downloaded KEYDB file is invalid (no entries found). /// Downloaded KEYDB file is invalid (no entries found).
KeydbInvalid, KeydbInvalid,
/// Failed to write KEYDB to disk. /// Failed to write KEYDB to disk.
KeydbWrite { KeydbWrite { path: String },
path: String,
},
/// Failed to parse KEYDB file. /// Failed to parse KEYDB file.
KeydbParse, KeydbParse,
/// Failed to load KEYDB from disk. /// Failed to load KEYDB from disk.
KeydbLoad { KeydbLoad { path: String },
path: String,
},
/// Lookahead buffer exhausted before codec headers found. /// Lookahead buffer exhausted before codec headers found.
MuxLookahead, MuxLookahead,
@@ -240,6 +217,7 @@ impl Error {
Error::AacsVidMac => E_AACS_VID_MAC, Error::AacsVidMac => E_AACS_VID_MAC,
Error::AacsDataKey => E_AACS_DATA_KEY, Error::AacsDataKey => E_AACS_DATA_KEY,
Error::AacsVukDerive => E_AACS_VUK_DERIVE, Error::AacsVukDerive => E_AACS_VUK_DERIVE,
Error::DecryptFailed { .. } => E_DECRYPT_FAILED,
Error::KeydbConnect { .. } => E_KEYDB_CONNECT, Error::KeydbConnect { .. } => E_KEYDB_CONNECT,
Error::KeydbHttp { .. } => E_KEYDB_HTTP, Error::KeydbHttp { .. } => E_KEYDB_HTTP,
Error::KeydbInvalid => E_KEYDB_INVALID, 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::KeydbHttp { status } => write!(f, "E{}: {}", self.code(), status),
Error::KeydbWrite { path } => write!(f, "E{}: {}", self.code(), path), Error::KeydbWrite { path } => write!(f, "E{}: {}", self.code(), path),
Error::KeydbLoad { 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 // Simple codes — no extra data
_ => write!(f, "E{}", self.code()), _ => write!(f, "E{}", self.code()),
} }
+2 -1
View File
@@ -205,7 +205,8 @@ impl IsoStream {
// Decrypt after read — stream handles its own decryption // Decrypt after read — stream handles its own decryption
let bytes = count as usize * SECTOR_SIZE as usize; 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_pos = 0;
self.buf_len = bytes; self.buf_len = bytes;