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:
+26
-26
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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..];
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user