Refactor error types: replace generic AacsError/DiscError with typed variants

- Split AacsError { detail } into 13 specific error variants (AacsCertShort,
  AacsAgidAlloc, AacsCertRejected, etc.) with unique error codes E7001-E7012
- Split DiscError { detail } into 7 specific variants (DiscRead, MplsParse,
  ClpiParse, UdfNotFound, DiscNoTitles, DiscTitleRange, DiscNoExtents)
- Add WriteError (E5001), KeydbLoad (E8005), MuxLookahead (E9000), MuxWrite (E9001)
- Add OpenDisc API for single-call open+scan+rip workflow
- Remove all English text from error Display impl (code-only output)
- Normalize doc comments to use -- instead of em dash for ASCII consistency
This commit is contained in:
MattJackson
2026-04-10 08:19:28 -07:00
parent 9241d767ee
commit 074f21ba58
10 changed files with 302 additions and 202 deletions
+26 -26
View File
@@ -203,7 +203,9 @@ fn ec_add(p1: &EcPoint, p2: &EcPoint, a: &BigUint, p: &BigUint) -> EcPoint {
(p - (&p1.x - &p2.x) % p) % p (p - (&p1.x - &p2.x) % p) % p
}; };
let dx_inv = mod_inv(&dx, p).unwrap(); // Safety: mod_inv only returns None if dx == 0 (points identical),
// which is prevented by the caller using ec_double for that case.
let dx_inv = mod_inv(&dx, p).expect("ec_add: dx has no inverse");
let lam = (&dy * &dx_inv) % p; let lam = (&dy * &dx_inv) % p;
// x3 = λ² - x1 - x2 mod p // x3 = λ² - x1 - x2 mod p
@@ -247,7 +249,9 @@ fn ec_double(pt: &EcPoint, a: &BigUint, p: &BigUint) -> EcPoint {
let numerator = (&three * &pt.x * &pt.x + a) % p; let numerator = (&three * &pt.x * &pt.x + a) % p;
let denominator = (&two * &pt.y) % p; let denominator = (&two * &pt.y) % p;
let denom_inv = mod_inv(&denominator, p).unwrap(); // Safety: mod_inv only returns None if 2*y == 0 (point at infinity),
// which shouldn't occur with valid curve points.
let denom_inv = mod_inv(&denominator, p).expect("ec_double: denominator has no inverse");
let lam = (&numerator * &denom_inv) % p; let lam = (&numerator * &denom_inv) % p;
// x3 = λ² - 2x mod p // x3 = λ² - 2x mod p
@@ -715,7 +719,7 @@ pub fn aacs_authenticate(
host_cert: &[u8], host_cert: &[u8],
) -> Result<AacsAuth> { ) -> Result<AacsAuth> {
if host_cert.len() < 92 { if host_cert.len() < 92 {
return Err(Error::AacsError { detail: "host certificate too short".into() }); return Err(Error::AacsCertShort);
} }
// Step 1: Invalidate all AGIDs // Step 1: Invalidate all AGIDs
@@ -727,7 +731,7 @@ pub fn aacs_authenticate(
// Step 2: Allocate AGID // Step 2: Allocate AGID
let cdb = cdb_report_key(0, 0x00, 8); let cdb = cdb_report_key(0, 0x00, 8);
let response = scsi_read(session, &cdb, 8) let response = scsi_read(session, &cdb, 8)
.map_err(|e| Error::AacsError { detail: format!("failed to allocate AGID: {}", e) })?; .map_err(|_| Error::AacsAgidAlloc)?;
let agid = (response[7] >> 6) & 0x03; let agid = (response[7] >> 6) & 0x03;
// Step 3: Generate host nonce and ephemeral key pair // Step 3: Generate host nonce and ephemeral key pair
@@ -744,12 +748,12 @@ pub fn aacs_authenticate(
let cdb = cdb_send_key(agid, 0x01, 116); let cdb = cdb_send_key(agid, 0x01, 116);
scsi_write(session, &cdb, &send_buf) scsi_write(session, &cdb, &send_buf)
.map_err(|_| Error::AacsError { detail: "drive rejected host certificate".into() })?; .map_err(|_| Error::AacsCertRejected)?;
// Step 5: Read drive certificate + nonce (REPORT KEY format 0x01) // Step 5: Read drive certificate + nonce (REPORT KEY format 0x01)
let cdb = cdb_report_key(agid, 0x01, 116); let cdb = cdb_report_key(agid, 0x01, 116);
let response = scsi_read(session, &cdb, 116) let response = scsi_read(session, &cdb, 116)
.map_err(|_| Error::AacsError { detail: "failed to read drive certificate".into() })?; .map_err(|_| Error::AacsCertRead)?;
let mut drive_nonce = [0u8; 20]; let mut drive_nonce = [0u8; 20];
let mut drive_cert = [0u8; 92]; let mut drive_cert = [0u8; 92];
@@ -760,7 +764,7 @@ pub fn aacs_authenticate(
if drive_cert[0] == 0x01 { if drive_cert[0] == 0x01 {
// AACS 1.0 certificate // AACS 1.0 certificate
if !verify_cert(&drive_cert) { if !verify_cert(&drive_cert) {
return Err(Error::AacsError { detail: "drive certificate verification failed".into() }); return Err(Error::AacsCertVerify);
} }
} else if drive_cert[0] == 0x11 { } else if drive_cert[0] == 0x11 {
// AACS 2.0 certificate — verify with P-256 LA key // AACS 2.0 certificate — verify with P-256 LA key
@@ -771,7 +775,7 @@ pub fn aacs_authenticate(
// Step 6: Read drive key point + signature (REPORT KEY format 0x02) // Step 6: Read drive key point + signature (REPORT KEY format 0x02)
let cdb = cdb_report_key(agid, 0x02, 84); let cdb = cdb_report_key(agid, 0x02, 84);
let response = scsi_read(session, &cdb, 84) let response = scsi_read(session, &cdb, 84)
.map_err(|_| Error::AacsError { detail: "failed to read drive key".into() })?; .map_err(|_| Error::AacsKeyRead)?;
let mut drive_key_point = [0u8; 40]; // x(20) + y(20) let mut drive_key_point = [0u8; 40]; // x(20) + y(20)
let mut drive_key_sig = [0u8; 40]; // r(20) + s(20) let mut drive_key_sig = [0u8; 40]; // r(20) + s(20)
@@ -790,7 +794,7 @@ pub fn aacs_authenticate(
sig_s.copy_from_slice(&drive_key_sig[20..40]); sig_s.copy_from_slice(&drive_key_sig[20..40]);
if !ecdsa_verify(&drive_pub_x, &drive_pub_y, &sig_r, &sig_s, &verify_data) { if !ecdsa_verify(&drive_pub_x, &drive_pub_y, &sig_r, &sig_s, &verify_data) {
return Err(Error::AacsError { detail: "drive key signature verification failed".into() }); return Err(Error::AacsKeyVerify);
} }
// Step 7: Sign host key point (ECDSA over drive_nonce || host_key_point) // Step 7: Sign host key point (ECDSA over drive_nonce || host_key_point)
@@ -811,7 +815,7 @@ pub fn aacs_authenticate(
let cdb = cdb_send_key(agid, 0x02, 84); let cdb = cdb_send_key(agid, 0x02, 84);
scsi_write(session, &cdb, &send_buf) scsi_write(session, &cdb, &send_buf)
.map_err(|_| Error::AacsError { detail: "drive rejected host key".into() })?; .map_err(|_| Error::AacsKeyRejected)?;
// Step 9: Compute bus key via ECDH // Step 9: Compute bus key via ECDH
let mut dkp_x = [0u8; 20]; let mut dkp_x = [0u8; 20];
@@ -851,12 +855,8 @@ pub fn aacs2_authenticate(
} }
// AACS 2.0 native P-256 handshake // AACS 2.0 native P-256 handshake
let host_priv_v2 = host_priv_key_v2.ok_or_else(|| Error::AacsError { let host_priv_v2 = host_priv_key_v2.ok_or(Error::AacsCertShort)?;
detail: "AACS 2.0 host credentials required but not available".into(), let host_cert_v2 = host_cert_v2.ok_or(Error::AacsCertShort)?;
})?;
let host_cert_v2 = host_cert_v2.ok_or_else(|| Error::AacsError {
detail: "AACS 2.0 host certificate required but not available".into(),
})?;
aacs2_authenticate_p256(session, host_priv_v2, host_cert_v2) aacs2_authenticate_p256(session, host_priv_v2, host_cert_v2)
} }
@@ -869,7 +869,7 @@ fn aacs2_authenticate_p256(
host_cert: &[u8], host_cert: &[u8],
) -> Result<AacsAuth> { ) -> Result<AacsAuth> {
if host_cert.len() < 132 { if host_cert.len() < 132 {
return Err(Error::AacsError { detail: "AACS 2.0 host cert too short".into() }); return Err(Error::AacsCertShort);
} }
// Step 1: Invalidate all AGIDs // Step 1: Invalidate all AGIDs
@@ -881,7 +881,7 @@ fn aacs2_authenticate_p256(
// Step 2: Allocate AGID // Step 2: Allocate AGID
let cdb = cdb_report_key(0, 0x00, 8); let cdb = cdb_report_key(0, 0x00, 8);
let response = scsi_read(session, &cdb, 8) let response = scsi_read(session, &cdb, 8)
.map_err(|_| Error::AacsError { detail: "AGID allocation failed".into() })?; .map_err(|_| Error::AacsAgidAlloc)?;
let agid = (response[7] >> 6) & 0x03; let agid = (response[7] >> 6) & 0x03;
// Step 3: Generate host nonce + P-256 ephemeral key pair // Step 3: Generate host nonce + P-256 ephemeral key pair
@@ -899,13 +899,13 @@ fn aacs2_authenticate_p256(
let cdb = cdb_send_key(agid, 0x01, 156); let cdb = cdb_send_key(agid, 0x01, 156);
scsi_write(session, &cdb, &send_buf) scsi_write(session, &cdb, &send_buf)
.map_err(|_| Error::AacsError { detail: "drive rejected AACS 2.0 host cert".into() })?; .map_err(|_| Error::AacsCertRejected)?;
// Step 5: Read drive certificate + nonce // Step 5: Read drive certificate + nonce
// AACS 2.0 drive cert is also 132 bytes // AACS 2.0 drive cert is also 132 bytes
let cdb = cdb_report_key(agid, 0x01, 156); let cdb = cdb_report_key(agid, 0x01, 156);
let response = scsi_read(session, &cdb, 156) let response = scsi_read(session, &cdb, 156)
.map_err(|_| Error::AacsError { detail: "failed to read AACS 2.0 drive cert".into() })?; .map_err(|_| Error::AacsCertRead)?;
let mut drive_nonce = [0u8; 20]; let mut drive_nonce = [0u8; 20];
drive_nonce.copy_from_slice(&response[4..24]); drive_nonce.copy_from_slice(&response[4..24]);
@@ -919,7 +919,7 @@ fn aacs2_authenticate_p256(
// Step 6: Read drive key point + signature (P-256: 64+64 = 128 bytes) // Step 6: Read drive key point + signature (P-256: 64+64 = 128 bytes)
let cdb = cdb_report_key(agid, 0x02, 132); let cdb = cdb_report_key(agid, 0x02, 132);
let response = scsi_read(session, &cdb, 132) let response = scsi_read(session, &cdb, 132)
.map_err(|_| Error::AacsError { detail: "failed to read AACS 2.0 drive key".into() })?; .map_err(|_| Error::AacsKeyRead)?;
let drive_key_x = &response[4..36]; let drive_key_x = &response[4..36];
let drive_key_y = &response[36..68]; let drive_key_y = &response[36..68];
@@ -934,7 +934,7 @@ fn aacs2_authenticate_p256(
verify_data.extend_from_slice(drive_key_y); verify_data.extend_from_slice(drive_key_y);
if !ecdsa_verify_p256(&drive_pub_x, &drive_pub_y, drive_sig_r, drive_sig_s, &verify_data) { if !ecdsa_verify_p256(&drive_pub_x, &drive_pub_y, drive_sig_r, drive_sig_s, &verify_data) {
return Err(Error::AacsError { detail: "AACS 2.0 drive key verification failed".into() }); return Err(Error::AacsKeyVerify);
} }
// Step 7: Sign host key point // Step 7: Sign host key point
@@ -955,7 +955,7 @@ fn aacs2_authenticate_p256(
let cdb = cdb_send_key(agid, 0x02, 132); let cdb = cdb_send_key(agid, 0x02, 132);
scsi_write(session, &cdb, &send_buf) scsi_write(session, &cdb, &send_buf)
.map_err(|_| Error::AacsError { detail: "drive rejected AACS 2.0 host key".into() })?; .map_err(|_| Error::AacsKeyRejected)?;
// Step 9: Compute bus key via P-256 ECDH // Step 9: Compute bus key via P-256 ECDH
let bus_key = compute_bus_key_p256(&host_eph_key, drive_key_x, drive_key_y); let bus_key = compute_bus_key_p256(&host_eph_key, drive_key_x, drive_key_y);
@@ -978,7 +978,7 @@ pub fn read_volume_id(session: &mut DriveSession, auth: &mut AacsAuth) -> Result
// REPORT DISC STRUCTURE format 0x80 // REPORT DISC STRUCTURE format 0x80
let cdb = cdb_report_disc_structure(auth.agid, 0x80, 36); let cdb = cdb_report_disc_structure(auth.agid, 0x80, 36);
let response = scsi_read(session, &cdb, 36) let response = scsi_read(session, &cdb, 36)
.map_err(|_| Error::AacsError { detail: "failed to read Volume ID".into() })?; .map_err(|_| Error::AacsVidRead)?;
let mut vid = [0u8; 16]; let mut vid = [0u8; 16];
let mut mac = [0u8; 16]; let mut mac = [0u8; 16];
@@ -988,7 +988,7 @@ pub fn read_volume_id(session: &mut DriveSession, auth: &mut AacsAuth) -> Result
// Verify MAC: AES-CMAC(VID, bus_key) should equal mac // Verify MAC: AES-CMAC(VID, bus_key) should equal mac
let calc_mac = aes_cmac_16(&vid, &auth.bus_key); let calc_mac = aes_cmac_16(&vid, &auth.bus_key);
if calc_mac != mac { if calc_mac != mac {
return Err(Error::AacsError { detail: "VID MAC verification failed".into() }); return Err(Error::AacsVidMac);
} }
auth.volume_id = Some(vid); auth.volume_id = Some(vid);
@@ -1000,7 +1000,7 @@ pub fn read_data_keys(session: &mut DriveSession, auth: &mut AacsAuth) -> Result
// REPORT DISC STRUCTURE format 0x84 // REPORT DISC STRUCTURE format 0x84
let cdb = cdb_report_disc_structure(auth.agid, 0x84, 36); let cdb = cdb_report_disc_structure(auth.agid, 0x84, 36);
let response = scsi_read(session, &cdb, 36) let response = scsi_read(session, &cdb, 36)
.map_err(|_| Error::AacsError { detail: "failed to read data keys".into() })?; .map_err(|_| Error::AacsDataKey)?;
let mut enc_rdk = [0u8; 16]; let mut enc_rdk = [0u8; 16];
let mut enc_wdk = [0u8; 16]; let mut enc_wdk = [0u8; 16];
+2 -2
View File
@@ -114,11 +114,11 @@ impl ClipInfo {
/// Parse a CLPI file from raw bytes. /// Parse a CLPI file from raw bytes.
pub fn parse(data: &[u8]) -> Result<ClipInfo> { pub fn parse(data: &[u8]) -> Result<ClipInfo> {
if data.len() < 40 { if data.len() < 40 {
return Err(Error::DiscError { detail: "CLPI too short".into() }); return Err(Error::ClpiParse);
} }
if &data[0..4] != b"HDMV" { if &data[0..4] != b"HDMV" {
return Err(Error::DiscError { detail: "not a CLPI file".into() }); return Err(Error::ClpiParse);
} }
let version = String::from_utf8_lossy(&data[4..8]).to_string(); let version = String::from_utf8_lossy(&data[4..8]).to_string();
+87 -52
View File
@@ -1,4 +1,4 @@
//! Disc structure scan titles, streams, and sector ranges from a Blu-ray disc. //! Disc structure -- scan titles, streams, and sector ranges from a Blu-ray disc.
//! //!
//! This is the high-level API for disc content. The CLI calls this, //! This is the high-level API for disc content. The CLI calls this,
//! never parses MPLS/CLPI/UDF directly. //! never parses MPLS/CLPI/UDF directly.
@@ -37,7 +37,7 @@ pub struct Disc {
pub titles: Vec<Title>, pub titles: Vec<Title>,
/// Disc region /// Disc region
pub region: DiscRegion, pub region: DiscRegion,
/// AACS state None if disc is unencrypted or keys unavailable /// AACS state -- None if disc is unencrypted or keys unavailable
pub aacs: Option<AacsState>, pub aacs: Option<AacsState>,
/// Whether this disc requires AACS decryption /// Whether this disc requires AACS decryption
pub encrypted: bool, pub encrypted: bool,
@@ -70,11 +70,11 @@ pub enum DiscRegion {
/// Blu-ray region codes. /// Blu-ray region codes.
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum BdRegion { pub enum BdRegion {
/// Region A/1 Americas, East Asia (Japan, Korea, Southeast Asia) /// Region A/1 -- Americas, East Asia (Japan, Korea, Southeast Asia)
A, A,
/// Region B/2 Europe, Africa, Australia, Middle East /// Region B/2 -- Europe, Africa, Australia, Middle East
B, B,
/// Region C/3 Central/South Asia, China, Russia /// Region C/3 -- Central/South Asia, China, Russia
C, C,
} }
@@ -310,7 +310,7 @@ pub struct AacsState {
pub bus_encryption: bool, pub bus_encryption: bool,
/// MKB version from disc (e.g. 68, 77) /// MKB version from disc (e.g. 68, 77)
pub mkb_version: Option<u32>, pub mkb_version: Option<u32>,
/// Disc hash (SHA1 of Unit_Key_RO.inf) hex string with 0x prefix /// Disc hash (SHA1 of Unit_Key_RO.inf) -- hex string with 0x prefix
pub disc_hash: String, pub disc_hash: String,
/// How keys were resolved /// How keys were resolved
pub key_source: KeySource, pub key_source: KeySource,
@@ -318,9 +318,9 @@ pub struct AacsState {
pub vuk: [u8; 16], pub vuk: [u8; 16],
/// Decrypted unit keys (CPS unit number, key) /// Decrypted unit keys (CPS unit number, key)
pub unit_keys: Vec<(u32, [u8; 16])>, pub unit_keys: Vec<(u32, [u8; 16])>,
/// Read data key for AACS 2.0 bus decryption None for AACS 1.0 /// Read data key for AACS 2.0 bus decryption -- None for AACS 1.0
pub read_data_key: Option<[u8; 16]>, pub read_data_key: Option<[u8; 16]>,
/// Volume ID (16 bytes) from SCSI handshake /// Volume ID (16 bytes) -- from SCSI handshake
pub volume_id: [u8; 16], pub volume_id: [u8; 16],
} }
@@ -392,30 +392,84 @@ impl ScanOptions {
} }
} }
/// A disc with an active drive session -- the main API.
///
/// Owns both the disc metadata and the drive connection.
/// Created by `Disc::open()`. Provides `rip()` to read title data.
pub struct OpenDisc {
pub disc: Disc,
pub session: DriveSession,
}
impl OpenDisc {
/// Open a drive, wait for disc, initialize, probe, and scan.
/// This is the single entry point -- one call does everything.
///
pub fn open(device: &str, keydb_path: Option<&str>) -> Result<Self> {
use std::path::Path;
let mut session = DriveSession::open(Path::new(device))?;
session.wait_ready()?;
// Init (unlock + firmware) -- non-fatal if fails
let _ = session.init();
let _ = session.probe_disc();
let opts = if let Some(kp) = keydb_path {
ScanOptions::with_keydb(kp)
} else {
ScanOptions::default()
};
let disc = Disc::scan(&mut session, &opts)?;
Ok(Self { disc, session })
}
/// Rip a title to any output stream.
///
/// Reads sectors from disc, decrypts AACS, handles errors/retries,
/// and writes decrypted BD-TS bytes to the output.
/// Knows nothing about the output format -- just calls `write_all()`.
///
pub fn rip(&mut self, title_idx: usize, mut output: impl std::io::Write) -> Result<()> {
let mut reader = self.disc.open_title(&mut self.session, title_idx)?;
loop {
match reader.read_batch() {
Ok(Some(batch)) => {
output.write_all(batch).map_err(|_| Error::WriteError)?;
}
Ok(None) => break,
Err(_) => {
// ContentReader handles retries internally
}
}
}
Ok(())
}
/// Total bytes for a title (for progress tracking).
pub fn title_size(&self, title_idx: usize) -> u64 {
self.disc.titles.get(title_idx)
.map(|t| t.size_bytes)
.unwrap_or(0)
}
}
impl Disc { impl Disc {
/// Disc capacity in GB /// Disc capacity in GB
pub fn capacity_gb(&self) -> f64 { pub fn capacity_gb(&self) -> f64 {
self.capacity_sectors as f64 * 2048.0 / (1024.0 * 1024.0 * 1024.0) self.capacity_sectors as f64 * 2048.0 / (1024.0 * 1024.0 * 1024.0)
} }
/// Scan a disc parse filesystem, playlists, streams, and set up AACS decryption. /// Scan a disc -- parse filesystem, playlists, streams, and set up AACS decryption.
/// ///
/// This is the main entry point. After scan(), the Disc is ready: /// This is the main entry point. After scan(), the Disc is ready:
/// - titles are populated with streams /// - titles are populated with streams
/// - AACS keys are derived (if KEYDB available) /// - AACS keys are derived (if KEYDB available)
/// - content can be read and decrypted transparently /// - content can be read and decrypted transparently
/// ///
/// ```no_run
/// use libfreemkv::{DriveSession, Disc};
/// use libfreemkv::disc::ScanOptions;
/// use std::path::Path;
///
/// let mut session = DriveSession::open(Path::new("/dev/sr0")).unwrap();
/// let disc = Disc::scan(&mut session, &ScanOptions::default()).unwrap();
/// for title in &disc.titles {
/// println!("{} — {} streams", title.duration_display(), title.streams.len());
/// }
/// ```
/// Scan a disc. One pipeline, one order: /// Scan a disc. One pipeline, one order:
/// 1. Read capacity /// 1. Read capacity
/// 2. Read UDF filesystem /// 2. Read UDF filesystem
@@ -424,7 +478,7 @@ impl Disc {
/// 5. Apply labels /// 5. Apply labels
/// ///
/// The session must be open and unlocked (DriveSession::open handles this). /// The session must be open and unlocked (DriveSession::open handles this).
/// All disc reads use standard READ(10) via UDF no vendor SCSI commands. /// All disc reads use standard READ(10) via UDF -- no vendor SCSI commands.
pub fn scan(session: &mut DriveSession, opts: &ScanOptions) -> Result<Self> { pub fn scan(session: &mut DriveSession, opts: &ScanOptions) -> Result<Self> {
use crate::aacs::{self, KeyDb}; use crate::aacs::{self, KeyDb};
@@ -434,7 +488,7 @@ impl Disc {
// 2. UDF filesystem // 2. UDF filesystem
let udf_fs = udf::read_filesystem(session)?; let udf_fs = udf::read_filesystem(session)?;
// 3. AACS read files from disc via UDF, resolve keys via KEYDB // 3. AACS -- read files from disc via UDF, resolve keys via KEYDB
let encrypted = udf_fs.find_dir("/AACS").is_some() let encrypted = udf_fs.find_dir("/AACS").is_some()
|| udf_fs.find_dir("/BDMV/AACS").is_some(); || udf_fs.find_dir("/BDMV/AACS").is_some();
@@ -496,16 +550,12 @@ impl Disc {
) -> Result<AacsState> { ) -> Result<AacsState> {
use crate::aacs::{self, KeyDb}; use crate::aacs::{self, KeyDb};
let keydb = KeyDb::load(keydb_path).map_err(|e| Error::AacsError { let keydb = KeyDb::load(keydb_path).map_err(|_| Error::KeydbLoad { path: keydb_path.display().to_string() })?;
detail: format!("failed to load KEYDB: {}", e),
})?;
// Read AACS files from disc via UDF (standard READ(10), no vendor commands) // Read AACS files from disc via UDF (standard READ(10), no vendor commands)
let uk_ro_data = udf_fs.read_file(session, "/AACS/Unit_Key_RO.inf") let uk_ro_data = udf_fs.read_file(session, "/AACS/Unit_Key_RO.inf")
.or_else(|_| udf_fs.read_file(session, "/AACS/DUPLICATE/Unit_Key_RO.inf")) .or_else(|_| udf_fs.read_file(session, "/AACS/DUPLICATE/Unit_Key_RO.inf"))
.map_err(|_| Error::AacsError { .map_err(|_| Error::AacsNoKeys)?;
detail: "Unit_Key_RO.inf not found on disc".into(),
})?;
let cc_data = udf_fs.read_file(session, "/AACS/Content000.cer") let cc_data = udf_fs.read_file(session, "/AACS/Content000.cer")
.or_else(|_| udf_fs.read_file(session, "/AACS/Content001.cer")) .or_else(|_| udf_fs.read_file(session, "/AACS/Content001.cer"))
@@ -524,9 +574,7 @@ impl Disc {
&vid_zero, &vid_zero,
&keydb, &keydb,
mkb_data.as_deref(), mkb_data.as_deref(),
).ok_or_else(|| Error::AacsError { ).ok_or_else(|| Error::AacsNoKeys)?;
detail: "disc not in KEYDB".into(),
})?;
Ok(AacsState { Ok(AacsState {
version: if resolved.aacs2 { 2 } else { 1 }, version: if resolved.aacs2 { 2 } else { 1 },
@@ -606,7 +654,7 @@ impl Disc {
} }
fn read_capacity(session: &mut DriveSession) -> Result<u32> { fn read_capacity(session: &mut DriveSession) -> Result<u32> {
let cdb = [0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; let cdb = [crate::scsi::SCSI_READ_CAPACITY, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
let mut buf = [0u8; 8]; let mut buf = [0u8; 8];
session.scsi_execute(&cdb, crate::scsi::DataDirection::FromDevice, &mut buf, 5_000)?; session.scsi_execute(&cdb, crate::scsi::DataDirection::FromDevice, &mut buf, 5_000)?;
let lba = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]); let lba = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
@@ -700,7 +748,7 @@ impl Disc {
})), })),
2 | 5 => { 2 | 5 => {
// Guard: if coding_type is a subtitle codec (PGS 0x90/0x91), // Guard: if coding_type is a subtitle codec (PGS 0x90/0x91),
// this is a misaligned stream treat as subtitle, not audio // this is a misaligned stream -- treat as subtitle, not audio
if matches!(codec, Codec::Pgs) { if matches!(codec, Codec::Pgs) {
Some(Stream::Subtitle(SubtitleStream { Some(Stream::Subtitle(SubtitleStream {
pid: s.pid, pid: s.pid,
@@ -726,7 +774,7 @@ impl Disc {
language: s.language.clone(), language: s.language.clone(),
forced: false, forced: false,
})), })),
// Stream type 4 = IG, unknown types skip // Stream type 4 = IG, unknown types -- skip
_ => None, _ => None,
} }
}).collect(); }).collect();
@@ -781,25 +829,12 @@ pub struct ContentReader<'a> {
} }
impl Disc { impl Disc {
/// Open a title for reading. Decryption is automatic if the disc /// Open a title for reading. Decryption is automatic -- if the disc
/// is encrypted and keys were found during scan(), content is decrypted /// is encrypted and keys were found during scan(), content is decrypted
/// on the fly. Unencrypted discs pass through unchanged. /// on the fly. Unencrypted discs pass through unchanged.
/// ///
/// ```no_run
/// # use libfreemkv::{DriveSession, Disc};
/// # use libfreemkv::disc::ScanOptions;
/// # use std::path::Path;
/// # let mut session = DriveSession::open(Path::new("/dev/sr0")).unwrap();
/// let disc = Disc::scan(&mut session, &ScanOptions::default()).unwrap();
/// let mut reader = disc.open_title(&mut session, 0).unwrap();
/// while let Some(unit) = reader.read_unit().unwrap() {
/// // unit is 6144 bytes of decrypted content
/// }
/// ```
pub fn open_title<'a>(&'a self, session: &'a mut DriveSession, title_idx: usize) -> Result<ContentReader<'a>> { pub fn open_title<'a>(&'a self, session: &'a mut DriveSession, title_idx: usize) -> Result<ContentReader<'a>> {
let title = self.titles.get(title_idx).ok_or_else(|| Error::DiscError { let title = self.titles.get(title_idx).ok_or_else(|| Error::DiscTitleRange { index: title_idx, count: self.titles.len() })?;
detail: format!("title index {} out of range (have {})", title_idx, self.titles.len()),
})?;
// Let the drive manage its own read speed after init. // Let the drive manage its own read speed after init.
// SET_CD_SPEED is only used reactively by the error handler to slow // SET_CD_SPEED is only used reactively by the error handler to slow
@@ -905,7 +940,7 @@ impl<'a> ContentReader<'a> {
/// Read the next batch of aligned units, decrypted in-place. /// Read the next batch of aligned units, decrypted in-place.
/// Returns the decrypted data as a single contiguous slice. /// Returns the decrypted data as a single contiguous slice.
/// More efficient than read_unit() one write_all() per batch instead of per unit. /// More efficient than read_unit() -- one write_all() per batch instead of per unit.
/// Returns None when all extents are exhausted. /// Returns None when all extents are exhausted.
pub fn read_batch(&mut self) -> Result<Option<&[u8]>> { pub fn read_batch(&mut self) -> Result<Option<&[u8]>> {
if !self.fill_buffer()? { if !self.fill_buffer()? {
@@ -1037,7 +1072,7 @@ impl<'a> ContentReader<'a> {
self.batch_sectors = (self.batch_sectors / 2).max(MIN_BATCH_SECTORS); self.batch_sectors = (self.batch_sectors / 2).max(MIN_BATCH_SECTORS);
std::thread::sleep(std::time::Duration::from_millis(100)); std::thread::sleep(std::time::Duration::from_millis(100));
} else { } else {
// At minimum batch retry once with longer pause // At minimum batch -- retry once with longer pause
std::thread::sleep(std::time::Duration::from_millis(500)); std::thread::sleep(std::time::Duration::from_millis(500));
self.read_buf.resize(MIN_BATCH_SECTORS as usize * 2048, 0); self.read_buf.resize(MIN_BATCH_SECTORS as usize * 2048, 0);
if self.read_sectors(lba, MIN_BATCH_SECTORS).is_ok() { if self.read_sectors(lba, MIN_BATCH_SECTORS).is_ok() {
@@ -1051,7 +1086,7 @@ impl<'a> ContentReader<'a> {
} }
return Ok(true); return Ok(true);
} }
// Still failing skip this unit (zero-fill) // Still failing -- skip this unit (zero-fill)
self.current_offset += 3; self.current_offset += 3;
if self.current_offset >= ext_sectors { if self.current_offset >= ext_sectors {
self.current_extent += 1; self.current_extent += 1;
+126 -47
View File
@@ -1,8 +1,7 @@
//! Error types for libfreemkv. //! Error types for libfreemkv.
//! //!
//! Every error carries a numeric code for programmatic handling. //! Every error is a code with structured data. No English text.
//! No user-facing English text — applications format their own messages. //! Applications map codes to localized messages.
//! This keeps the library locale-independent and testable.
//! //!
//! # Error Code Ranges //! # Error Code Ranges
//! //!
@@ -15,8 +14,10 @@
//! | E5xxx | I/O errors | //! | E5xxx | I/O errors |
//! | E6xxx | Disc format errors | //! | E6xxx | Disc format errors |
//! | E7xxx | AACS errors | //! | E7xxx | AACS errors |
//! | E8xxx | Keydb errors |
//! | E9xxx | Mux errors |
// ── Error codes (single source of truth) ──────────────────────────────────── // ── Error codes ─────────────────────────────────────────────────────────────
pub const E_DEVICE_NOT_FOUND: u16 = 1000; pub const E_DEVICE_NOT_FOUND: u16 = 1000;
pub const E_DEVICE_PERMISSION: u16 = 1001; pub const E_DEVICE_PERMISSION: u16 = 1001;
@@ -30,116 +31,194 @@ pub const E_NOT_CALIBRATED: u16 = 3003;
pub const E_SCSI_ERROR: u16 = 4000; pub const E_SCSI_ERROR: u16 = 4000;
pub const E_SCSI_TIMEOUT: u16 = 4001; pub const E_SCSI_TIMEOUT: u16 = 4001;
pub const E_IO_ERROR: u16 = 5000; pub const E_IO_ERROR: u16 = 5000;
pub const E_DISC_ERROR: u16 = 6000; pub const E_WRITE_ERROR: u16 = 5001;
pub const E_AACS_ERROR: u16 = 7000; // Disc format (6xxx)
pub const E_DISC_READ: u16 = 6000;
pub const E_MPLS_PARSE: u16 = 6001;
pub const E_CLPI_PARSE: u16 = 6002;
pub const E_UDF_NOT_FOUND: u16 = 6003;
pub const E_DISC_NO_TITLES: u16 = 6004;
pub const E_DISC_TITLE_RANGE: u16 = 6005;
pub const E_DISC_NO_EXTENTS: u16 = 6006;
// AACS (7xxx)
pub const E_AACS_NO_KEYS: u16 = 7000;
pub const E_AACS_CERT_SHORT: u16 = 7001;
pub const E_AACS_AGID_ALLOC: u16 = 7002;
pub const E_AACS_CERT_REJECTED: u16 = 7003;
pub const E_AACS_CERT_READ: u16 = 7004;
pub const E_AACS_CERT_VERIFY: u16 = 7005;
pub const E_AACS_KEY_READ: u16 = 7006;
pub const E_AACS_KEY_REJECTED: u16 = 7007;
pub const E_AACS_KEY_VERIFY: u16 = 7008;
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;
// 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;
pub const E_KEYDB_INVALID: u16 = 8002; pub const E_KEYDB_INVALID: u16 = 8002;
pub const E_KEYDB_WRITE: u16 = 8003; pub const E_KEYDB_WRITE: u16 = 8003;
pub const E_KEYDB_PARSE: u16 = 8004; pub const E_KEYDB_PARSE: u16 = 8004;
pub const E_KEYDB_LOAD: u16 = 8005;
// Mux (9xxx)
pub const E_MUX_LOOKAHEAD: u16 = 9000;
pub const E_MUX_WRITE: u16 = 9001;
// ── Error enum ────────────────────────────────────────────────────────────── // ── Error enum ──────────────────────────────────────────────────────────────
/// Structured error with numeric code and context data. /// Structured error with numeric code and context data. No English text.
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
// Device
DeviceNotFound { path: String }, DeviceNotFound { path: String },
DevicePermission { path: String }, DevicePermission { path: String },
// Profile
UnsupportedDrive { vendor_id: String, product_id: String, product_revision: String }, UnsupportedDrive { vendor_id: String, product_id: String, product_revision: String },
ProfileNotFound { vendor_id: String, product_revision: String, vendor_specific: String }, ProfileNotFound { vendor_id: String, product_revision: String, vendor_specific: String },
ProfileParse { detail: String }, ProfileParse,
UnlockFailed { detail: String },
// Unlock
UnlockFailed,
SignatureMismatch { expected: [u8; 4], got: [u8; 4] }, SignatureMismatch { expected: [u8; 4], got: [u8; 4] },
NotUnlocked, NotUnlocked,
NotCalibrated, NotCalibrated,
// SCSI
ScsiError { opcode: u8, status: u8, sense_key: u8 }, ScsiError { opcode: u8, status: u8, sense_key: u8 },
ScsiTimeout { opcode: u8 }, ScsiTimeout { opcode: u8 },
// I/O
IoError { source: std::io::Error }, IoError { source: std::io::Error },
DiscError { detail: String }, WriteError,
AacsError { detail: String },
// Disc format
DiscRead { sector: u64 },
MplsParse,
ClpiParse,
UdfNotFound { path: String },
DiscNoTitles,
DiscTitleRange { index: usize, count: usize },
DiscNoExtents,
// AACS
AacsNoKeys,
AacsCertShort,
AacsAgidAlloc,
AacsCertRejected,
AacsCertRead,
AacsCertVerify,
AacsKeyRead,
AacsKeyRejected,
AacsKeyVerify,
AacsVidRead,
AacsVidMac,
AacsDataKey,
AacsVukDerive,
// Keydb
KeydbConnect { host: String }, KeydbConnect { host: String },
KeydbHttp { status: u16 }, KeydbHttp { status: u16 },
KeydbInvalid, KeydbInvalid,
KeydbWrite { path: String }, KeydbWrite { path: String },
KeydbParse, KeydbParse,
KeydbLoad { path: String },
// Mux
MuxLookahead,
MuxWrite,
} }
impl Error { impl Error {
/// Numeric error code.
pub fn code(&self) -> u16 { pub fn code(&self) -> u16 {
match self { match self {
Error::DeviceNotFound { .. } => E_DEVICE_NOT_FOUND, Error::DeviceNotFound { .. } => E_DEVICE_NOT_FOUND,
Error::DevicePermission { .. } => E_DEVICE_PERMISSION, Error::DevicePermission { .. } => E_DEVICE_PERMISSION,
Error::UnsupportedDrive { .. } => E_UNSUPPORTED_DRIVE, Error::UnsupportedDrive { .. } => E_UNSUPPORTED_DRIVE,
Error::ProfileNotFound { .. } => E_PROFILE_NOT_FOUND, Error::ProfileNotFound { .. } => E_PROFILE_NOT_FOUND,
Error::ProfileParse { .. } => E_PROFILE_PARSE, Error::ProfileParse => E_PROFILE_PARSE,
Error::UnlockFailed { .. } => E_UNLOCK_FAILED, Error::UnlockFailed => E_UNLOCK_FAILED,
Error::SignatureMismatch { .. } => E_SIGNATURE_MISMATCH, Error::SignatureMismatch { .. } => E_SIGNATURE_MISMATCH,
Error::NotUnlocked => E_NOT_UNLOCKED, Error::NotUnlocked => E_NOT_UNLOCKED,
Error::NotCalibrated => E_NOT_CALIBRATED, Error::NotCalibrated => E_NOT_CALIBRATED,
Error::ScsiError { .. } => E_SCSI_ERROR, Error::ScsiError { .. } => E_SCSI_ERROR,
Error::ScsiTimeout { .. } => E_SCSI_TIMEOUT, Error::ScsiTimeout { .. } => E_SCSI_TIMEOUT,
Error::IoError { .. } => E_IO_ERROR, Error::IoError { .. } => E_IO_ERROR,
Error::DiscError { .. } => E_DISC_ERROR, Error::WriteError => E_WRITE_ERROR,
Error::AacsError { .. } => E_AACS_ERROR, Error::DiscRead { .. } => E_DISC_READ,
Error::MplsParse => E_MPLS_PARSE,
Error::ClpiParse => E_CLPI_PARSE,
Error::UdfNotFound { .. } => E_UDF_NOT_FOUND,
Error::DiscNoTitles => E_DISC_NO_TITLES,
Error::DiscTitleRange { .. } => E_DISC_TITLE_RANGE,
Error::DiscNoExtents => E_DISC_NO_EXTENTS,
Error::AacsNoKeys => E_AACS_NO_KEYS,
Error::AacsCertShort => E_AACS_CERT_SHORT,
Error::AacsAgidAlloc => E_AACS_AGID_ALLOC,
Error::AacsCertRejected => E_AACS_CERT_REJECTED,
Error::AacsCertRead => E_AACS_CERT_READ,
Error::AacsCertVerify => E_AACS_CERT_VERIFY,
Error::AacsKeyRead => E_AACS_KEY_READ,
Error::AacsKeyRejected => E_AACS_KEY_REJECTED,
Error::AacsKeyVerify => E_AACS_KEY_VERIFY,
Error::AacsVidRead => E_AACS_VID_READ,
Error::AacsVidMac => E_AACS_VID_MAC,
Error::AacsDataKey => E_AACS_DATA_KEY,
Error::AacsVukDerive => E_AACS_VUK_DERIVE,
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,
Error::KeydbWrite { .. } => E_KEYDB_WRITE, Error::KeydbWrite { .. } => E_KEYDB_WRITE,
Error::KeydbParse => E_KEYDB_PARSE, Error::KeydbParse => E_KEYDB_PARSE,
Error::KeydbLoad { .. } => E_KEYDB_LOAD,
Error::MuxLookahead => E_MUX_LOOKAHEAD,
Error::MuxWrite => E_MUX_WRITE,
} }
} }
} }
/// Display format: "E{code}: {context}" — terse, for logs. /// Display: "E{code}" with structured data. No English words.
/// Applications should format their own user-facing messages using code() and fields.
impl std::fmt::Display for Error { impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Error::DeviceNotFound { path } => Error::DeviceNotFound { path } =>
write!(f, "E{}: {}", E_DEVICE_NOT_FOUND, path), write!(f, "E{}: {}", self.code(), path),
Error::DevicePermission { path } => Error::DevicePermission { path } =>
write!(f, "E{}: {}", E_DEVICE_PERMISSION, path), write!(f, "E{}: {}", self.code(), path),
Error::UnsupportedDrive { vendor_id, product_id, product_revision } => Error::UnsupportedDrive { vendor_id, product_id, product_revision } =>
write!(f, "E{}: {} {} {}", E_UNSUPPORTED_DRIVE, write!(f, "E{}: {} {} {}", self.code(),
vendor_id.trim(), product_id.trim(), product_revision.trim()), vendor_id.trim(), product_id.trim(), product_revision.trim()),
Error::ProfileNotFound { vendor_id, product_revision, vendor_specific } => Error::ProfileNotFound { vendor_id, product_revision, vendor_specific } =>
write!(f, "E{}: {} {} {}", E_PROFILE_NOT_FOUND, write!(f, "E{}: {} {} {}", self.code(),
vendor_id.trim(), product_revision.trim(), vendor_specific.trim()), vendor_id.trim(), product_revision.trim(), vendor_specific.trim()),
Error::ProfileParse { detail } =>
write!(f, "E{}: {}", E_PROFILE_PARSE, detail),
Error::UnlockFailed { detail } =>
write!(f, "E{}: {}", E_UNLOCK_FAILED, detail),
Error::SignatureMismatch { expected, got } => Error::SignatureMismatch { expected, got } =>
write!(f, "E{}: expected {:02x}{:02x}{:02x}{:02x} got {:02x}{:02x}{:02x}{:02x}", write!(f, "E{}: {:02x}{:02x}{:02x}{:02x}!={:02x}{:02x}{:02x}{:02x}",
E_SIGNATURE_MISMATCH, self.code(),
expected[0], expected[1], expected[2], expected[3], expected[0], expected[1], expected[2], expected[3],
got[0], got[1], got[2], got[3]), got[0], got[1], got[2], got[3]),
Error::NotUnlocked =>
write!(f, "E{}", E_NOT_UNLOCKED),
Error::NotCalibrated =>
write!(f, "E{}", E_NOT_CALIBRATED),
Error::ScsiError { opcode, status, sense_key } => Error::ScsiError { opcode, status, sense_key } =>
write!(f, "E{}: opcode=0x{:02x} status=0x{:02x} sense=0x{:02x}", write!(f, "E{}: 0x{:02x}/0x{:02x}/0x{:02x}", self.code(), opcode, status, sense_key),
E_SCSI_ERROR, opcode, status, sense_key),
Error::ScsiTimeout { opcode } => Error::ScsiTimeout { opcode } =>
write!(f, "E{}: opcode=0x{:02x}", E_SCSI_TIMEOUT, opcode), write!(f, "E{}: 0x{:02x}", self.code(), opcode),
Error::IoError { source } => Error::IoError { source } =>
write!(f, "E{}: {}", E_IO_ERROR, source), write!(f, "E{}: {}", self.code(), source),
Error::DiscError { detail } => Error::DiscRead { sector } =>
write!(f, "E{}: {}", E_DISC_ERROR, detail), write!(f, "E{}: {}", self.code(), sector),
Error::AacsError { detail } => Error::UdfNotFound { path } =>
write!(f, "E{}: {}", E_AACS_ERROR, detail), write!(f, "E{}: {}", self.code(), path),
Error::DiscTitleRange { index, count } =>
write!(f, "E{}: {}/{}", self.code(), index, count),
Error::KeydbConnect { host } => Error::KeydbConnect { host } =>
write!(f, "E{}: {}", E_KEYDB_CONNECT, host), write!(f, "E{}: {}", self.code(), host),
Error::KeydbHttp { status } => Error::KeydbHttp { status } =>
write!(f, "E{}: {}", E_KEYDB_HTTP, status), write!(f, "E{}: {}", self.code(), status),
Error::KeydbInvalid =>
write!(f, "E{}", E_KEYDB_INVALID),
Error::KeydbWrite { path } => Error::KeydbWrite { path } =>
write!(f, "E{}: {}", E_KEYDB_WRITE, path), write!(f, "E{}: {}", self.code(), path),
Error::KeydbParse => Error::KeydbLoad { path } =>
write!(f, "E{}", E_KEYDB_PARSE), write!(f, "E{}: {}", self.code(), path),
// Simple codes — no extra data
_ => write!(f, "E{}", self.code()),
} }
} }
} }
+21 -17
View File
@@ -1,4 +1,4 @@
//! libfreemkv Open source optical drive library for 4K UHD / Blu-ray / DVD. //! libfreemkv -- Open source optical drive library for 4K UHD / Blu-ray / DVD.
//! //!
//! Handles drive access, disc structure parsing, AACS decryption, and raw //! Handles drive access, disc structure parsing, AACS decryption, and raw
//! sector reading. 206 bundled drive profiles. No external files needed. //! sector reading. 206 bundled drive profiles. No external files needed.
@@ -14,7 +14,7 @@
//! let disc = Disc::scan(&mut session, &ScanOptions::default()).unwrap(); //! let disc = Disc::scan(&mut session, &ScanOptions::default()).unwrap();
//! //!
//! for title in &disc.titles { //! for title in &disc.titles {
//! println!("{} {} streams", title.duration_display(), title.streams.len()); //! println!("{} -- {} streams", title.duration_display(), title.streams.len());
//! } //! }
//! //!
//! // Read content (decrypted automatically if AACS keys available) //! // Read content (decrypted automatically if AACS keys available)
@@ -27,21 +27,21 @@
//! # Architecture //! # Architecture
//! //!
//! ```text //! ```text
//! DriveSession open, identify, unlock, read sectors //! DriveSession -- open, identify, unlock, read sectors
//! ├── ScsiTransport SG_IO (Linux), IOKit (macOS) //! ├── ScsiTransport -- SG_IO (Linux), IOKit (macOS)
//! ├── DriveProfile per-drive unlock parameters (206 bundled) //! ├── DriveProfile -- per-drive unlock parameters (206 bundled)
//! ├── DriveId INQUIRY + GET_CONFIG identification //! ├── DriveId -- INQUIRY + GET_CONFIG identification
//! └── Platform //! └── Platform
//! └── Mt1959 MediaTek unlock/read (Renesas planned) //! └── Mt1959 -- MediaTek unlock/read (Renesas planned)
//! //!
//! Disc scan titles, streams, AACS state //! Disc -- scan titles, streams, AACS state
//! ├── UDF reader Blu-ray UDF 2.50 with metadata partitions //! ├── UDF reader -- Blu-ray UDF 2.50 with metadata partitions
//! ├── MPLS parser playlists → titles + clips + STN streams //! ├── MPLS parser -- playlists → titles + clips + STN streams
//! ├── CLPI parser clip info → EP map → sector extents //! ├── CLPI parser -- clip info → EP map → sector extents
//! ├── JAR parser BD-J audio track labels //! ├── JAR parser -- BD-J audio track labels
//! └── AACS encryption: key resolution + content decrypt //! └── AACS -- encryption: key resolution + content decrypt
//! ├── aacs KEYDB, VUK, MKB, unit decrypt //! ├── aacs -- KEYDB, VUK, MKB, unit decrypt
//! └── handshake SCSI auth, ECDH, bus key //! └── handshake -- SCSI auth, ECDH, bus key
//! ``` //! ```
//! //!
//! # AACS Encryption //! # AACS Encryption
@@ -55,7 +55,7 @@
//! # Error Codes //! # Error Codes
//! //!
//! All errors are structured with numeric codes. No user-facing English //! All errors are structured with numeric codes. No user-facing English
//! text applications format their own messages. //! text -- applications format their own messages.
//! //!
//! | Range | Category | //! | Range | Category |
//! |-------|----------| //! |-------|----------|
@@ -81,14 +81,18 @@ pub mod disc;
pub mod aacs; pub mod aacs;
pub mod labels; pub mod labels;
pub mod keydb; pub mod keydb;
pub mod event;
pub mod mux;
pub use error::{Error, Result}; pub use error::{Error, Result};
pub use event::{Event, EventKind};
pub use drive::{DriveSession, find_drive, find_drives, resolve_device}; pub use drive::{DriveSession, find_drive, find_drives, resolve_device};
pub use identity::DriveId; pub use identity::DriveId;
pub use profile::DriveProfile; pub use profile::DriveProfile;
// Platform trait is pub(crate) callers use DriveSession, not Platform directly // Platform trait is pub(crate) -- callers use DriveSession, not Platform directly
pub use scsi::ScsiTransport; pub use scsi::ScsiTransport;
pub use speed::DriveSpeed; pub use speed::DriveSpeed;
pub use disc::{Disc, DiscFormat, Title, Clip, Stream, VideoStream, AudioStream, SubtitleStream, pub use disc::{Disc, DiscFormat, Title, Clip, Stream, VideoStream, AudioStream, SubtitleStream,
Codec, HdrFormat, ColorSpace, Codec, HdrFormat, ColorSpace,
Extent, ContentReader, AacsState, KeySource, ScanOptions}; Extent, ContentReader, AacsState, KeySource, ScanOptions};
pub use mux::MkvStream;
+3 -3
View File
@@ -62,17 +62,17 @@ pub struct StreamEntry {
/// Parse an MPLS file from raw bytes. /// Parse an MPLS file from raw bytes.
pub fn parse(data: &[u8]) -> Result<Playlist> { pub fn parse(data: &[u8]) -> Result<Playlist> {
if data.len() < 40 { if data.len() < 40 {
return Err(Error::DiscError { detail: "MPLS too short".into() }); return Err(Error::MplsParse);
} }
if &data[0..4] != b"MPLS" { if &data[0..4] != b"MPLS" {
return Err(Error::DiscError { detail: "not an MPLS file".into() }); return Err(Error::MplsParse);
} }
let version = String::from_utf8_lossy(&data[4..8]).to_string(); let version = String::from_utf8_lossy(&data[4..8]).to_string();
let playlist_start = u32::from_be_bytes([data[8], data[9], data[10], data[11]]) as usize; let playlist_start = u32::from_be_bytes([data[8], data[9], data[10], data[11]]) as usize;
if playlist_start + 10 > data.len() { if playlist_start + 10 > data.len() {
return Err(Error::DiscError { detail: "MPLS playlist offset out of range".into() }); return Err(Error::MplsParse);
} }
let pl = &data[playlist_start..]; let pl = &data[playlist_start..];
+3 -10
View File
@@ -113,12 +113,7 @@ impl Mt1959 {
if response.len() >= FIRMWARE_ACTIVE_OFFSET + 4 if response.len() >= FIRMWARE_ACTIVE_OFFSET + 4
&& response[FIRMWARE_ACTIVE_OFFSET..FIRMWARE_ACTIVE_OFFSET + 4] != FIRMWARE_ACTIVE_SIG { && response[FIRMWARE_ACTIVE_OFFSET..FIRMWARE_ACTIVE_OFFSET + 4] != FIRMWARE_ACTIVE_SIG {
return Err(Error::UnlockFailed { return Err(Error::UnlockFailed);
detail: format!(
"mode not active: {:02x}{:02x}{:02x}{:02x}",
response[12], response[13], response[14], response[15]
),
});
} }
self.unlocked = true; self.unlocked = true;
@@ -148,9 +143,7 @@ impl Mt1959 {
match self.do_unlock(scsi) { match self.do_unlock(scsi) {
Ok(_) => { unlocked = true; break; } Ok(_) => { unlocked = true; break; }
Err(Error::SignatureMismatch { .. }) => { Err(Error::SignatureMismatch { .. }) => {
return Err(Error::UnlockFailed { return Err(Error::UnlockFailed);
detail: "signature mismatch — wrong profile for this drive".into(),
});
} }
Err(_) => { Err(_) => {
let ok = if self.mode == MODE_A { let ok = if self.mode == MODE_A {
@@ -163,7 +156,7 @@ impl Mt1959 {
} }
} }
if !unlocked { if !unlocked {
return Err(Error::UnlockFailed { detail: "failed after 6 attempts".into() }); return Err(Error::UnlockFailed);
} }
Ok(()) Ok(())
} }
+1 -3
View File
@@ -12,9 +12,7 @@ const VERIFY_BUFFER_ID: u8 = 0x45;
pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Result<()> { pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Result<()> {
let firmware = &mt.profile.firmware; let firmware = &mt.profile.firmware;
if firmware.is_empty() { if firmware.is_empty() {
return Err(crate::error::Error::UnlockFailed { return Err(crate::error::Error::UnlockFailed);
detail: "no firmware in profile".into(),
});
} }
// Upload firmware via WRITE_BUFFER // Upload firmware via WRITE_BUFFER
+1 -3
View File
@@ -16,9 +16,7 @@ const VENDOR_VERIFY: [u8; 10] = [0xF1, 0x01, 0x02, 0x00, 0x0D, 0x30, 0x01, 0xF3,
pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Result<()> { pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Result<()> {
let firmware = &mt.profile.firmware; let firmware = &mt.profile.firmware;
if firmware.is_empty() { if firmware.is_empty() {
return Err(crate::error::Error::UnlockFailed { return Err(crate::error::Error::UnlockFailed);
detail: "no firmware in profile".into(),
});
} }
// Step 1: Upload firmware via MODE SELECT // Step 1: Upload firmware via MODE SELECT
+32 -39
View File
@@ -80,16 +80,17 @@ impl UdfFs {
for part in &parts[..parts.len() - 1] { for part in &parts[..parts.len() - 1] {
current = current.entries.iter().find(|e| { current = current.entries.iter().find(|e| {
e.is_dir && e.name.eq_ignore_ascii_case(part) e.is_dir && e.name.eq_ignore_ascii_case(part)
}).ok_or_else(|| Error::DiscError { }).ok_or_else(|| Error::UdfNotFound { path: part.to_string() }
detail: format!("directory not found: {}", part), )?;
})?;
} }
let filename = parts.last().unwrap(); let filename = match parts.last() {
Some(f) => f,
None => return Err(Error::UdfNotFound { path: path.to_string() }),
};
let entry = current.entries.iter().find(|e| { let entry = current.entries.iter().find(|e| {
!e.is_dir && e.name.eq_ignore_ascii_case(filename) !e.is_dir && e.name.eq_ignore_ascii_case(filename)
}).ok_or_else(|| Error::DiscError { }).ok_or_else(|| Error::UdfNotFound { path: path.to_string() }
detail: format!("file not found: {}", path), )?;
})?;
let (data_lba, _) = self.read_icb_extent(session, entry.meta_lba)?; let (data_lba, _) = self.read_icb_extent(session, entry.meta_lba)?;
Ok(self.partition_start + data_lba) Ok(self.partition_start + data_lba)
} }
@@ -102,18 +103,19 @@ impl UdfFs {
for part in &parts[..parts.len() - 1] { for part in &parts[..parts.len() - 1] {
current = current.entries.iter().find(|e| { current = current.entries.iter().find(|e| {
e.is_dir && e.name.eq_ignore_ascii_case(part) e.is_dir && e.name.eq_ignore_ascii_case(part)
}).ok_or_else(|| Error::DiscError { }).ok_or_else(|| Error::UdfNotFound { path: part.to_string() }
detail: format!("directory not found: {}", part), )?;
})?;
} }
// Find the file // Find the file
let filename = parts.last().unwrap(); let filename = match parts.last() {
Some(f) => f,
None => return Err(Error::UdfNotFound { path: path.to_string() }),
};
let entry = current.entries.iter().find(|e| { let entry = current.entries.iter().find(|e| {
!e.is_dir && e.name.eq_ignore_ascii_case(filename) !e.is_dir && e.name.eq_ignore_ascii_case(filename)
}).ok_or_else(|| Error::DiscError { }).ok_or_else(|| Error::UdfNotFound { path: path.to_string() }
detail: format!("file not found: {}", path), )?;
})?;
// Read the file's ICB to get its data extent // Read the file's ICB to get its data extent
let (data_lba, data_len) = self.read_icb_extent(session, entry.meta_lba)?; let (data_lba, data_len) = self.read_icb_extent(session, entry.meta_lba)?;
@@ -197,9 +199,7 @@ impl UdfFs {
/// The data_lba is partition-relative. /// The data_lba is partition-relative.
fn read_icb_extent(&self, session: &mut DriveSession, meta_lba: u32) -> Result<(u32, u32)> { fn read_icb_extent(&self, session: &mut DriveSession, meta_lba: u32) -> Result<(u32, u32)> {
let extents = self.read_icb_extents(session, meta_lba)?; let extents = self.read_icb_extents(session, meta_lba)?;
extents.first().copied().ok_or_else(|| Error::DiscError { extents.first().copied().ok_or_else(|| Error::DiscRead { sector: 0 })
detail: "no allocation descriptors in ICB".into(),
})
} }
/// Read ALL allocation extents for a file from its ICB. /// Read ALL allocation extents for a file from its ICB.
@@ -225,9 +225,7 @@ impl UdfFs {
let l_ad = u32::from_le_bytes([icb[172], icb[173], icb[174], icb[175]]) as usize; let l_ad = u32::from_le_bytes([icb[172], icb[173], icb[174], icb[175]]) as usize;
(176 + l_ea, l_ad) (176 + l_ea, l_ad)
} }
_ => return Err(Error::DiscError { _ => return Err(Error::DiscRead { sector: 0 }),
detail: format!("unexpected ICB tag {} at meta_lba {}", tag, meta_lba),
}),
}; };
let mut extents = Vec::new(); let mut extents = Vec::new();
@@ -235,8 +233,8 @@ impl UdfFs {
for i in 0..num_descriptors { for i in 0..num_descriptors {
let off = ad_offset + i * 8; let off = ad_offset + i * 8;
if off + 8 > 2048 { if off + 8 > icb.len() {
break; // TODO: follow Allocation Extent Descriptors (tag 258) for overflow break;
} }
let raw_len = u32::from_le_bytes([icb[off], icb[off + 1], icb[off + 2], icb[off + 3]]); let raw_len = u32::from_le_bytes([icb[off], icb[off + 1], icb[off + 2], icb[off + 3]]);
@@ -263,16 +261,17 @@ impl UdfFs {
for part in &parts[..parts.len() - 1] { for part in &parts[..parts.len() - 1] {
current = current.entries.iter().find(|e| { current = current.entries.iter().find(|e| {
e.is_dir && e.name.eq_ignore_ascii_case(part) e.is_dir && e.name.eq_ignore_ascii_case(part)
}).ok_or_else(|| Error::DiscError { }).ok_or_else(|| Error::UdfNotFound { path: part.to_string() }
detail: format!("directory not found: {}", part), )?;
})?;
} }
let filename = parts.last().unwrap(); let filename = match parts.last() {
Some(f) => f,
None => return Err(Error::UdfNotFound { path: path.to_string() }),
};
let entry = current.entries.iter().find(|e| { let entry = current.entries.iter().find(|e| {
!e.is_dir && e.name.eq_ignore_ascii_case(filename) !e.is_dir && e.name.eq_ignore_ascii_case(filename)
}).ok_or_else(|| Error::DiscError { }).ok_or_else(|| Error::UdfNotFound { path: path.to_string() }
detail: format!("file not found: {}", path), )?;
})?;
let alloc_extents = self.read_icb_extents(session, entry.meta_lba)?; let alloc_extents = self.read_icb_extents(session, entry.meta_lba)?;
let mut disc_extents = Vec::new(); let mut disc_extents = Vec::new();
@@ -302,9 +301,7 @@ pub fn read_filesystem(session: &mut DriveSession) -> Result<UdfFs> {
let tag_id = u16::from_le_bytes([avdp[0], avdp[1]]); let tag_id = u16::from_le_bytes([avdp[0], avdp[1]]);
if tag_id != 2 { if tag_id != 2 {
return Err(Error::DiscError { return Err(Error::DiscRead { sector: 0 });
detail: format!("AVDP: expected tag 2, got {} at sector 256", tag_id),
});
} }
// Main VDS extent location: bytes [16:20] = LBA, [20:24] = length // Main VDS extent location: bytes [16:20] = LBA, [20:24] = length
@@ -344,16 +341,14 @@ pub fn read_filesystem(session: &mut DriveSession) -> Result<UdfFs> {
} }
if partition_start == 0 { if partition_start == 0 {
return Err(Error::DiscError { detail: "UDF: no Partition Descriptor found".into() }); return Err(Error::DiscRead { sector: 0 });
} }
// Step 3: Parse partition maps from LVD to find metadata partition // Step 3: Parse partition maps from LVD to find metadata partition
// BD-ROM discs (UDF 2.50) use a metadata partition (Type 2 map with "*UDF Metadata Partition") // BD-ROM discs (UDF 2.50) use a metadata partition (Type 2 map with "*UDF Metadata Partition")
// The metadata file is stored at lba=0 of the physical partition // The metadata file is stored at lba=0 of the physical partition
let metadata_start = if num_partition_maps >= 2 { let metadata_start = if num_partition_maps >= 2 {
let lvd_sec = lvd_sector.ok_or_else(|| Error::DiscError { let lvd_sec = lvd_sector.ok_or_else(|| Error::DiscRead { sector: 0 })?;
detail: "UDF: no LVD found".into(),
})?;
// Read LVD to check partition map type // Read LVD to check partition map type
let mut lvd = [0u8; 2048]; let mut lvd = [0u8; 2048];
@@ -410,9 +405,7 @@ pub fn read_filesystem(session: &mut DriveSession) -> Result<UdfFs> {
let fsd_tag = u16::from_le_bytes([fsd[0], fsd[1]]); let fsd_tag = u16::from_le_bytes([fsd[0], fsd[1]]);
if fsd_tag != 256 { if fsd_tag != 256 {
return Err(Error::DiscError { return Err(Error::DiscRead { sector: 0 });
detail: format!("FSD: expected tag 256, got {} at sector {}", fsd_tag, metadata_start),
});
} }
// Root Directory ICB: long_ad at FSD offset 400 // Root Directory ICB: long_ad at FSD offset 400