Named constants, error codes, SCSI opcodes, flow documentation

- Error codes as public constants (E_DEVICE_NOT_FOUND etc.) — single source of truth
- SCSI opcodes as named constants (SCSI_READ_10, SCSI_REPORT_KEY, etc.)
- AACS key class constant, MKB format constant
- No magic numbers in CDB construction
- docs/disc-to-rip.md — end-to-end flow from disc insert to decrypted content
- Links all module docs together as a starting point
This commit is contained in:
MattJackson
2026-04-07 12:26:30 -07:00
parent 6ab355245f
commit d0c5c7fb97
7 changed files with 232 additions and 80 deletions
+114
View File
@@ -0,0 +1,114 @@
# Disc to Rip: End-to-End Flow
How libfreemkv goes from a disc in the drive to decrypted content ready for backup.
This is the starting point for understanding the library.
## The Pipeline
```
Insert disc
1. Open drive (drive.rs)
│ INQUIRY → identify drive
│ Match bundled profile → chipset, unlock parameters
2. AACS handshake (aacs_handshake.rs) — optional, separate transport
│ Allocate AGID
│ Exchange certificates + nonces (ECDH)
│ Derive bus key
│ Read Volume ID + read_data_key
│ (fails gracefully if drive doesn't support AACS for this disc)
3. Unlock drive (drive.rs → platform/mt1959.rs)
│ Vendor-specific command activates raw read mode
│ Required — drive firmware blocks all reads without it
4. Read UDF filesystem (udf.rs)
│ Sector 256: AVDP → find Volume Descriptor Sequence
│ VDS: Partition Descriptor (physical start) + Logical Volume (metadata start)
│ Metadata partition → File Set Descriptor → Root directory
│ Walk directory tree: BDMV/, AACS/, CERTIFICATE/
│ → docs/udf.md
5. Read AACS files from disc (aacs.rs)
│ AACS/Unit_Key_RO.inf → SHA1 = disc hash
│ AACS/Content000.cer → AACS version (1.0 or 2.0), bus encryption flag
│ MKB via SCSI → for key derivation fallback
6. Resolve AACS keys (aacs.rs → resolve_keys)
│ Path 1: disc hash → KEYDB.cfg → VUK (fast, 99% of discs)
│ Path 2: KEYDB media key + Volume ID → VUK
│ Path 3: MKB + processing keys → media key → VUK
│ Path 4: MKB + device keys → subset-difference tree → VUK
│ VUK → decrypt unit keys from Unit_Key_RO.inf
│ → docs/aacs.md
7. Parse playlists (mpls.rs)
│ BDMV/PLAYLIST/*.mpls → titles with play items
│ Each play item: clip ID, in/out timestamps
│ STN table: video, audio, subtitle streams with codec + language
│ → docs/mpls.md
8. Parse clip info (clpi.rs)
│ BDMV/CLIPINF/*.clpi → EP map (timestamp → sector mapping)
│ Coarse + fine entries → full PTS and SPN
│ SPN → byte offset → sector extents for reading
│ → docs/clpi.md
9. Parse BD-J labels (jar.rs) — optional
│ BDMV/JAR/*.jar → Java class constant pool strings
│ Audio track labels: "English Descriptive Audio", "French 5.1", etc.
10. Read + decrypt content (disc.rs → ContentReader)
│ For each aligned unit (6144 bytes = 3 sectors):
│ Read 3 sectors from disc
│ If AACS 2.0: bus decrypt (read_data_key, per-sector AES-CBC)
│ If encrypted: unit decrypt (per-unit key derivation + AES-CBC)
│ Output decrypted content
Decrypted m2ts stream → ready for muxing/backup
```
## API Summary
```rust
// Steps 1 + 3 (open + unlock)
let mut session = DriveSession::open(Path::new("/dev/sr0"))?;
// Steps 2 + 4-9 (AACS + scan)
let disc = Disc::scan(&mut session, &ScanOptions::with_keydb("keydb.cfg"))?;
// Step 10 (read + decrypt)
let mut reader = disc.open_title(&mut session, 0)?;
while let Some(unit) = reader.read_unit()? {
output.write_all(&unit)?;
}
```
Three lines. Everything else is internal.
## Module Reference
| Module | Doc | Purpose |
|--------|-----|---------|
| drive.rs | [drive-access.md](drive-access.md) | Open, identify, unlock, read |
| scsi.rs | [drive-access.md](drive-access.md) | Platform SCSI transport |
| udf.rs | [udf.md](udf.md) | UDF 2.50 filesystem |
| mpls.rs | [mpls.md](mpls.md) | MPLS playlists + STN streams |
| clpi.rs | [clpi.md](clpi.md) | CLPI clip info + EP map |
| aacs.rs | [aacs.md](aacs.md) | Key resolution + content decrypt |
| aacs_handshake.rs | [aacs.md](aacs.md) | SCSI bus authentication |
| disc.rs | -- | High-level scan + read API |
| jar.rs | -- | BD-J audio track labels |
| error.rs | -- | Error codes (E1xxx-E7xxx) |
+13 -9
View File
@@ -753,17 +753,21 @@ pub fn derive_media_key_from_dk(
None None
} }
/// MKB disc structure format code.
const MKB_DISC_STRUCTURE_FORMAT: u8 = 0x83;
/// MKB pack buffer size.
const MKB_PACK_SIZE: usize = 32772;
/// Read MKB from drive via SCSI (REPORT DISC STRUCTURE format 0x83). /// Read MKB from drive via SCSI (REPORT DISC STRUCTURE format 0x83).
/// Returns the concatenated MKB data from all packs. /// Returns the concatenated MKB data from all packs.
pub fn read_mkb_from_drive(session: &mut crate::drive::DriveSession) -> crate::error::Result<Vec<u8>> { pub fn read_mkb_from_drive(session: &mut crate::drive::DriveSession) -> crate::error::Result<Vec<u8>> {
use crate::scsi::DataDirection; use crate::scsi::{DataDirection, SCSI_READ_DISC_STRUCTURE};
// First pack: get pack count and initial data
let cdb = [ let cdb = [
0xAD, 0x01, // REPORT DISC STRUCTURE, Blu-ray SCSI_READ_DISC_STRUCTURE, 0x01,
0x00, 0x00, 0x00, 0x00, // address = 0 (pack 0) 0x00, 0x00, 0x00, 0x00,
0x00, 0x83, // format = 0x83 (MKB) 0x00, MKB_DISC_STRUCTURE_FORMAT,
0x80, 0x04, // allocation length = 32772 (MKB_PACK_SIZE >> 8) as u8, (MKB_PACK_SIZE & 0xFF) as u8,
0x00, 0x00, 0x00, 0x00,
]; ];
let mut buf = vec![0u8; 32772]; let mut buf = vec![0u8; 32772];
@@ -782,10 +786,10 @@ pub fn read_mkb_from_drive(session: &mut crate::drive::DriveSession) -> crate::e
// Read remaining packs // Read remaining packs
for pack in 1..num_packs { for pack in 1..num_packs {
let mut cdb = [ let mut cdb = [
0xAD, 0x01, SCSI_READ_DISC_STRUCTURE, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x83, 0x00, MKB_DISC_STRUCTURE_FORMAT,
0x80, 0x04, (MKB_PACK_SIZE >> 8) as u8, (MKB_PACK_SIZE & 0xFF) as u8,
0x00, 0x00, 0x00, 0x00,
]; ];
// Pack number goes in address field // Pack number goes in address field
+5 -5
View File
@@ -459,8 +459,8 @@ fn aes_cmac_16(data: &[u8; 16], key: &[u8; 16]) -> [u8; 16] {
/// Build REPORT KEY CDB (0xA4). /// Build REPORT KEY CDB (0xA4).
fn cdb_report_key(agid: u8, format: u8, len: u16) -> [u8; 12] { fn cdb_report_key(agid: u8, format: u8, len: u16) -> [u8; 12] {
let mut cdb = [0u8; 12]; let mut cdb = [0u8; 12];
cdb[0] = 0xA4; cdb[0] = crate::scsi::SCSI_REPORT_KEY;
cdb[7] = 0x02; // key class = AACS cdb[7] = crate::scsi::AACS_KEY_CLASS;
cdb[8] = (len >> 8) as u8; cdb[8] = (len >> 8) as u8;
cdb[9] = (len & 0xFF) as u8; cdb[9] = (len & 0xFF) as u8;
cdb[10] = (agid << 6) | (format & 0x3F); cdb[10] = (agid << 6) | (format & 0x3F);
@@ -470,8 +470,8 @@ fn cdb_report_key(agid: u8, format: u8, len: u16) -> [u8; 12] {
/// Build SEND KEY CDB (0xA3). /// Build SEND KEY CDB (0xA3).
fn cdb_send_key(agid: u8, format: u8, len: u16) -> [u8; 12] { fn cdb_send_key(agid: u8, format: u8, len: u16) -> [u8; 12] {
let mut cdb = [0u8; 12]; let mut cdb = [0u8; 12];
cdb[0] = 0xA3; cdb[0] = crate::scsi::SCSI_SEND_KEY;
cdb[7] = 0x02; // key class = AACS cdb[7] = crate::scsi::AACS_KEY_CLASS;
cdb[8] = (len >> 8) as u8; cdb[8] = (len >> 8) as u8;
cdb[9] = (len & 0xFF) as u8; cdb[9] = (len & 0xFF) as u8;
cdb[10] = (agid << 6) | (format & 0x3F); cdb[10] = (agid << 6) | (format & 0x3F);
@@ -481,7 +481,7 @@ fn cdb_send_key(agid: u8, format: u8, len: u16) -> [u8; 12] {
/// Build REPORT DISC STRUCTURE CDB (0xAD). /// Build REPORT DISC STRUCTURE CDB (0xAD).
fn cdb_report_disc_structure(agid: u8, format: u8, len: u16) -> [u8; 12] { fn cdb_report_disc_structure(agid: u8, format: u8, len: u16) -> [u8; 12] {
let mut cdb = [0u8; 12]; let mut cdb = [0u8; 12];
cdb[0] = 0xAD; cdb[0] = crate::scsi::SCSI_READ_DISC_STRUCTURE;
cdb[1] = 0x01; // Blu-ray cdb[1] = 0x01; // Blu-ray
cdb[7] = format; cdb[7] = format;
cdb[8] = (len >> 8) as u8; cdb[8] = (len >> 8) as u8;
+1 -1
View File
@@ -691,7 +691,7 @@ impl<'a> ContentReader<'a> {
fn session_read_sector(session: &mut DriveSession, lba: u32, buf: &mut [u8; 2048]) -> Result<()> { fn session_read_sector(session: &mut DriveSession, lba: u32, buf: &mut [u8; 2048]) -> Result<()> {
let cdb = [ let cdb = [
0x28, 0x00, crate::scsi::SCSI_READ_10, 0x00,
(lba >> 24) as u8, (lba >> 16) as u8, (lba >> 8) as u8, lba as u8, (lba >> 24) as u8, (lba >> 16) as u8, (lba >> 8) as u8, lba as u8,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00,
]; ];
+1 -1
View File
@@ -133,7 +133,7 @@ impl DriveSession {
/// Standard SCSI READ(10) for disc filesystem data (UDF, MPLS, CLPI). /// Standard SCSI READ(10) for disc filesystem data (UDF, MPLS, CLPI).
pub fn read_disc(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> { pub fn read_disc(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
let cdb = [ let cdb = [
0x28, 0x00, crate::scsi::SCSI_READ_10, 0x00,
(lba >> 24) as u8, (lba >> 16) as u8, (lba >> 8) as u8, lba as u8, (lba >> 24) as u8, (lba >> 16) as u8, (lba >> 8) as u8, lba as u8,
0x00, 0x00,
(count >> 8) as u8, count as u8, (count >> 8) as u8, count as u8,
+82 -64
View File
@@ -1,102 +1,120 @@
/// libfreemkv error codes. //! Error types for libfreemkv.
/// //!
/// The library returns structured error codes with context data. //! Every error carries a numeric code for programmatic handling.
/// Applications are responsible for formatting user-facing messages. //! No user-facing English text — applications format their own messages.
/// This keeps the library locale-independent and testable. //! This keeps the library locale-independent and testable.
//!
//! # Error Code Ranges
//!
//! | Range | Category |
//! |-------|----------|
//! | E1xxx | Device errors |
//! | E2xxx | Profile errors |
//! | E3xxx | Unlock errors |
//! | E4xxx | SCSI errors |
//! | E5xxx | I/O errors |
//! | E6xxx | Disc format errors |
//! | E7xxx | AACS errors |
/// Error code table. // ── Error codes (single source of truth) ────────────────────────────────────
///
/// | Code | Name | Meaning | pub const E_DEVICE_NOT_FOUND: u16 = 1000;
/// |------|------|---------| pub const E_DEVICE_PERMISSION: u16 = 1001;
/// | 1000 | DeviceNotFound | Device path doesn't exist or can't be opened | pub const E_UNSUPPORTED_DRIVE: u16 = 2000;
/// | 1001 | DevicePermission | Device exists but permission denied | pub const E_PROFILE_NOT_FOUND: u16 = 2001;
/// | 2000 | UnsupportedDrive | Drive not in profile database | pub const E_PROFILE_PARSE: u16 = 2002;
/// | 2001 | ProfileNotFound | Specific firmware version not in database | pub const E_UNLOCK_FAILED: u16 = 3000;
/// | 2002 | ProfileParse | Profile database is malformed | pub const E_SIGNATURE_MISMATCH: u16 = 3001;
/// | 3000 | UnlockFailed | Drive rejected unlock command | pub const E_NOT_UNLOCKED: u16 = 3002;
/// | 3001 | SignatureMismatch | Wrong signature returned by drive | pub const E_NOT_CALIBRATED: u16 = 3003;
/// | 3002 | NotUnlocked | Raw read attempted before unlock | pub const E_SCSI_ERROR: u16 = 4000;
/// | 3003 | NotCalibrated | Raw read attempted before calibrate | pub const E_SCSI_TIMEOUT: u16 = 4001;
/// | 4000 | ScsiError | SCSI command failed | pub const E_IO_ERROR: u16 = 5000;
/// | 4001 | ScsiTimeout | SCSI command timed out | pub const E_DISC_ERROR: u16 = 6000;
/// | 5000 | IoError | OS-level I/O error | pub const E_AACS_ERROR: u16 = 7000;
// ── Error enum ──────────────────────────────────────────────────────────────
/// Structured error with numeric code and context data.
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
// 1xxx — Device errors
DeviceNotFound { path: String }, DeviceNotFound { path: String },
DevicePermission { path: String }, DevicePermission { path: String },
// 2xxx — Profile errors
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 { detail: String },
// 3xxx — Unlock errors
UnlockFailed { detail: String }, UnlockFailed { detail: String },
SignatureMismatch { expected: [u8; 4], got: [u8; 4] }, SignatureMismatch { expected: [u8; 4], got: [u8; 4] },
NotUnlocked, NotUnlocked,
NotCalibrated, NotCalibrated,
// 4xxx — SCSI errors
ScsiError { opcode: u8, status: u8, sense_key: u8 }, ScsiError { opcode: u8, status: u8, sense_key: u8 },
ScsiTimeout { opcode: u8 }, ScsiTimeout { opcode: u8 },
// 5xxx — I/O errors
IoError { source: std::io::Error }, IoError { source: std::io::Error },
// 6xxx — Disc format errors
DiscError { detail: String }, DiscError { detail: String },
// 7xxx — AACS errors
AacsError { detail: String }, AacsError { detail: String },
} }
impl Error { impl Error {
/// Numeric error code for programmatic handling. /// Numeric error code.
pub fn code(&self) -> u16 { pub fn code(&self) -> u16 {
match self { match self {
Error::DeviceNotFound { .. } => 1000, Error::DeviceNotFound { .. } => E_DEVICE_NOT_FOUND,
Error::DevicePermission { .. } => 1001, Error::DevicePermission { .. } => E_DEVICE_PERMISSION,
Error::UnsupportedDrive { .. } => 2000, Error::UnsupportedDrive { .. } => E_UNSUPPORTED_DRIVE,
Error::ProfileNotFound { .. } => 2001, Error::ProfileNotFound { .. } => E_PROFILE_NOT_FOUND,
Error::ProfileParse { .. } => 2002, Error::ProfileParse { .. } => E_PROFILE_PARSE,
Error::UnlockFailed { .. } => 3000, Error::UnlockFailed { .. } => E_UNLOCK_FAILED,
Error::SignatureMismatch { .. } => 3001, Error::SignatureMismatch { .. } => E_SIGNATURE_MISMATCH,
Error::NotUnlocked => 3002, Error::NotUnlocked => E_NOT_UNLOCKED,
Error::NotCalibrated => 3003, Error::NotCalibrated => E_NOT_CALIBRATED,
Error::ScsiError { .. } => 4000, Error::ScsiError { .. } => E_SCSI_ERROR,
Error::ScsiTimeout { .. } => 4001, Error::ScsiTimeout { .. } => E_SCSI_TIMEOUT,
Error::IoError { .. } => 5000, Error::IoError { .. } => E_IO_ERROR,
Error::DiscError { .. } => 6000, Error::DiscError { .. } => E_DISC_ERROR,
Error::AacsError { .. } => 7000, Error::AacsError { .. } => E_AACS_ERROR,
} }
} }
} }
/// Default Display — terse, for logs. Applications should format their own messages. /// Display format: "E{code}: {context}" — terse, for logs.
/// 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 } => write!(f, "E1000: device not found: {path}"), Error::DeviceNotFound { path } =>
Error::DevicePermission { path } => write!(f, "E1001: permission denied: {path}"), write!(f, "E{}: {}", E_DEVICE_NOT_FOUND, path),
Error::DevicePermission { path } =>
write!(f, "E{}: {}", E_DEVICE_PERMISSION, path),
Error::UnsupportedDrive { vendor_id, product_id, product_revision } => Error::UnsupportedDrive { vendor_id, product_id, product_revision } =>
write!(f, "E2000: unsupported drive: {} {} {}", vendor_id.trim(), product_id.trim(), product_revision.trim()), write!(f, "E{}: {} {} {}", E_UNSUPPORTED_DRIVE,
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, "E2001: no profile: {} {} {}", vendor_id.trim(), product_revision.trim(), vendor_specific.trim()), write!(f, "E{}: {} {} {}", E_PROFILE_NOT_FOUND,
Error::ProfileParse { detail } => write!(f, "E2002: profile parse: {detail}"), vendor_id.trim(), product_revision.trim(), vendor_specific.trim()),
Error::UnlockFailed { detail } => write!(f, "E3000: unlock failed: {detail}"), 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, "E3001: signature mismatch: expected {:02x}{:02x}{:02x}{:02x} got {:02x}{:02x}{:02x}{:02x}", write!(f, "E{}: expected {:02x}{:02x}{:02x}{:02x} got {:02x}{:02x}{:02x}{:02x}",
E_SIGNATURE_MISMATCH,
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, "E3002: not unlocked"), Error::NotUnlocked =>
Error::NotCalibrated => write!(f, "E3003: not calibrated"), 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, "E4000: SCSI 0x{opcode:02x} failed: status=0x{status:02x} sense=0x{sense_key:02x}"), write!(f, "E{}: opcode=0x{:02x} status=0x{:02x} sense=0x{:02x}",
Error::ScsiTimeout { opcode } => write!(f, "E4001: SCSI 0x{opcode:02x} timeout"), E_SCSI_ERROR, opcode, status, sense_key),
Error::IoError { source } => write!(f, "E5000: {source}"), Error::ScsiTimeout { opcode } =>
Error::DiscError { detail } => write!(f, "E6000: disc: {detail}"), write!(f, "E{}: opcode=0x{:02x}", E_SCSI_TIMEOUT, opcode),
Error::AacsError { detail } => write!(f, "E7000: AACS: {detail}"), Error::IoError { source } =>
write!(f, "E{}: {}", E_IO_ERROR, source),
Error::DiscError { detail } =>
write!(f, "E{}: {}", E_DISC_ERROR, detail),
Error::AacsError { detail } =>
write!(f, "E{}: {}", E_AACS_ERROR, detail),
} }
} }
} }
+16
View File
@@ -8,6 +8,22 @@
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use std::path::Path; use std::path::Path;
// ── SCSI opcodes (SPC-4, MMC-6) ────────────────────────────────────────────
pub const SCSI_INQUIRY: u8 = 0x12;
pub const SCSI_READ_CAPACITY: u8 = 0x25;
pub const SCSI_READ_10: u8 = 0x28;
pub const SCSI_READ_BUFFER: u8 = 0x3C;
pub const SCSI_READ_TOC: u8 = 0x43;
pub const SCSI_GET_CONFIGURATION: u8 = 0x46;
pub const SCSI_SEND_KEY: u8 = 0xA3;
pub const SCSI_REPORT_KEY: u8 = 0xA4;
pub const SCSI_READ_12: u8 = 0xA8;
pub const SCSI_READ_DISC_STRUCTURE: u8 = 0xAD;
/// AACS key class for REPORT KEY / SEND KEY commands.
pub const AACS_KEY_CLASS: u8 = 0x02;
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum DataDirection { pub enum DataDirection {
None, None,