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:
+13
-9
@@ -753,17 +753,21 @@ pub fn derive_media_key_from_dk(
|
||||
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).
|
||||
/// Returns the concatenated MKB data from all packs.
|
||||
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 = [
|
||||
0xAD, 0x01, // REPORT DISC STRUCTURE, Blu-ray
|
||||
0x00, 0x00, 0x00, 0x00, // address = 0 (pack 0)
|
||||
0x00, 0x83, // format = 0x83 (MKB)
|
||||
0x80, 0x04, // allocation length = 32772
|
||||
SCSI_READ_DISC_STRUCTURE, 0x01,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, MKB_DISC_STRUCTURE_FORMAT,
|
||||
(MKB_PACK_SIZE >> 8) as u8, (MKB_PACK_SIZE & 0xFF) as u8,
|
||||
0x00, 0x00,
|
||||
];
|
||||
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
|
||||
for pack in 1..num_packs {
|
||||
let mut cdb = [
|
||||
0xAD, 0x01,
|
||||
SCSI_READ_DISC_STRUCTURE, 0x01,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x83,
|
||||
0x80, 0x04,
|
||||
0x00, MKB_DISC_STRUCTURE_FORMAT,
|
||||
(MKB_PACK_SIZE >> 8) as u8, (MKB_PACK_SIZE & 0xFF) as u8,
|
||||
0x00, 0x00,
|
||||
];
|
||||
// Pack number goes in address field
|
||||
|
||||
@@ -459,8 +459,8 @@ fn aes_cmac_16(data: &[u8; 16], key: &[u8; 16]) -> [u8; 16] {
|
||||
/// Build REPORT KEY CDB (0xA4).
|
||||
fn cdb_report_key(agid: u8, format: u8, len: u16) -> [u8; 12] {
|
||||
let mut cdb = [0u8; 12];
|
||||
cdb[0] = 0xA4;
|
||||
cdb[7] = 0x02; // key class = AACS
|
||||
cdb[0] = crate::scsi::SCSI_REPORT_KEY;
|
||||
cdb[7] = crate::scsi::AACS_KEY_CLASS;
|
||||
cdb[8] = (len >> 8) as u8;
|
||||
cdb[9] = (len & 0xFF) as u8;
|
||||
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).
|
||||
fn cdb_send_key(agid: u8, format: u8, len: u16) -> [u8; 12] {
|
||||
let mut cdb = [0u8; 12];
|
||||
cdb[0] = 0xA3;
|
||||
cdb[7] = 0x02; // key class = AACS
|
||||
cdb[0] = crate::scsi::SCSI_SEND_KEY;
|
||||
cdb[7] = crate::scsi::AACS_KEY_CLASS;
|
||||
cdb[8] = (len >> 8) as u8;
|
||||
cdb[9] = (len & 0xFF) as u8;
|
||||
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).
|
||||
fn cdb_report_disc_structure(agid: u8, format: u8, len: u16) -> [u8; 12] {
|
||||
let mut cdb = [0u8; 12];
|
||||
cdb[0] = 0xAD;
|
||||
cdb[0] = crate::scsi::SCSI_READ_DISC_STRUCTURE;
|
||||
cdb[1] = 0x01; // Blu-ray
|
||||
cdb[7] = format;
|
||||
cdb[8] = (len >> 8) as u8;
|
||||
|
||||
+1
-1
@@ -691,7 +691,7 @@ impl<'a> ContentReader<'a> {
|
||||
|
||||
fn session_read_sector(session: &mut DriveSession, lba: u32, buf: &mut [u8; 2048]) -> Result<()> {
|
||||
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,
|
||||
0x00, 0x00, 0x01, 0x00,
|
||||
];
|
||||
|
||||
+1
-1
@@ -133,7 +133,7 @@ impl DriveSession {
|
||||
/// 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> {
|
||||
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,
|
||||
0x00,
|
||||
(count >> 8) as u8, count as u8,
|
||||
|
||||
+82
-64
@@ -1,102 +1,120 @@
|
||||
/// libfreemkv error codes.
|
||||
///
|
||||
/// The library returns structured error codes with context data.
|
||||
/// Applications are responsible for formatting user-facing messages.
|
||||
/// This keeps the library locale-independent and testable.
|
||||
//! Error types for libfreemkv.
|
||||
//!
|
||||
//! Every error carries a numeric code for programmatic handling.
|
||||
//! No user-facing English text — applications format their own messages.
|
||||
//! 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.
|
||||
///
|
||||
/// | Code | Name | Meaning |
|
||||
/// |------|------|---------|
|
||||
/// | 1000 | DeviceNotFound | Device path doesn't exist or can't be opened |
|
||||
/// | 1001 | DevicePermission | Device exists but permission denied |
|
||||
/// | 2000 | UnsupportedDrive | Drive not in profile database |
|
||||
/// | 2001 | ProfileNotFound | Specific firmware version not in database |
|
||||
/// | 2002 | ProfileParse | Profile database is malformed |
|
||||
/// | 3000 | UnlockFailed | Drive rejected unlock command |
|
||||
/// | 3001 | SignatureMismatch | Wrong signature returned by drive |
|
||||
/// | 3002 | NotUnlocked | Raw read attempted before unlock |
|
||||
/// | 3003 | NotCalibrated | Raw read attempted before calibrate |
|
||||
/// | 4000 | ScsiError | SCSI command failed |
|
||||
/// | 4001 | ScsiTimeout | SCSI command timed out |
|
||||
/// | 5000 | IoError | OS-level I/O error |
|
||||
// ── Error codes (single source of truth) ────────────────────────────────────
|
||||
|
||||
pub const E_DEVICE_NOT_FOUND: u16 = 1000;
|
||||
pub const E_DEVICE_PERMISSION: u16 = 1001;
|
||||
pub const E_UNSUPPORTED_DRIVE: u16 = 2000;
|
||||
pub const E_PROFILE_NOT_FOUND: u16 = 2001;
|
||||
pub const E_PROFILE_PARSE: u16 = 2002;
|
||||
pub const E_UNLOCK_FAILED: u16 = 3000;
|
||||
pub const E_SIGNATURE_MISMATCH: u16 = 3001;
|
||||
pub const E_NOT_UNLOCKED: u16 = 3002;
|
||||
pub const E_NOT_CALIBRATED: u16 = 3003;
|
||||
pub const E_SCSI_ERROR: u16 = 4000;
|
||||
pub const E_SCSI_TIMEOUT: u16 = 4001;
|
||||
pub const E_IO_ERROR: u16 = 5000;
|
||||
pub const E_DISC_ERROR: u16 = 6000;
|
||||
pub const E_AACS_ERROR: u16 = 7000;
|
||||
|
||||
// ── Error enum ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// Structured error with numeric code and context data.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
// 1xxx — Device errors
|
||||
DeviceNotFound { path: String },
|
||||
DevicePermission { path: String },
|
||||
|
||||
// 2xxx — Profile errors
|
||||
UnsupportedDrive { vendor_id: String, product_id: String, product_revision: String },
|
||||
ProfileNotFound { vendor_id: String, product_revision: String, vendor_specific: String },
|
||||
ProfileParse { detail: String },
|
||||
|
||||
// 3xxx — Unlock errors
|
||||
UnlockFailed { detail: String },
|
||||
SignatureMismatch { expected: [u8; 4], got: [u8; 4] },
|
||||
NotUnlocked,
|
||||
NotCalibrated,
|
||||
|
||||
// 4xxx — SCSI errors
|
||||
ScsiError { opcode: u8, status: u8, sense_key: u8 },
|
||||
ScsiTimeout { opcode: u8 },
|
||||
|
||||
// 5xxx — I/O errors
|
||||
IoError { source: std::io::Error },
|
||||
|
||||
// 6xxx — Disc format errors
|
||||
DiscError { detail: String },
|
||||
|
||||
// 7xxx — AACS errors
|
||||
AacsError { detail: String },
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Numeric error code for programmatic handling.
|
||||
/// Numeric error code.
|
||||
pub fn code(&self) -> u16 {
|
||||
match self {
|
||||
Error::DeviceNotFound { .. } => 1000,
|
||||
Error::DevicePermission { .. } => 1001,
|
||||
Error::UnsupportedDrive { .. } => 2000,
|
||||
Error::ProfileNotFound { .. } => 2001,
|
||||
Error::ProfileParse { .. } => 2002,
|
||||
Error::UnlockFailed { .. } => 3000,
|
||||
Error::SignatureMismatch { .. } => 3001,
|
||||
Error::NotUnlocked => 3002,
|
||||
Error::NotCalibrated => 3003,
|
||||
Error::ScsiError { .. } => 4000,
|
||||
Error::ScsiTimeout { .. } => 4001,
|
||||
Error::IoError { .. } => 5000,
|
||||
Error::DiscError { .. } => 6000,
|
||||
Error::AacsError { .. } => 7000,
|
||||
Error::DeviceNotFound { .. } => E_DEVICE_NOT_FOUND,
|
||||
Error::DevicePermission { .. } => E_DEVICE_PERMISSION,
|
||||
Error::UnsupportedDrive { .. } => E_UNSUPPORTED_DRIVE,
|
||||
Error::ProfileNotFound { .. } => E_PROFILE_NOT_FOUND,
|
||||
Error::ProfileParse { .. } => E_PROFILE_PARSE,
|
||||
Error::UnlockFailed { .. } => E_UNLOCK_FAILED,
|
||||
Error::SignatureMismatch { .. } => E_SIGNATURE_MISMATCH,
|
||||
Error::NotUnlocked => E_NOT_UNLOCKED,
|
||||
Error::NotCalibrated => E_NOT_CALIBRATED,
|
||||
Error::ScsiError { .. } => E_SCSI_ERROR,
|
||||
Error::ScsiTimeout { .. } => E_SCSI_TIMEOUT,
|
||||
Error::IoError { .. } => E_IO_ERROR,
|
||||
Error::DiscError { .. } => E_DISC_ERROR,
|
||||
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 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::DeviceNotFound { path } => write!(f, "E1000: device not found: {path}"),
|
||||
Error::DevicePermission { path } => write!(f, "E1001: permission denied: {path}"),
|
||||
Error::DeviceNotFound { 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 } =>
|
||||
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 } =>
|
||||
write!(f, "E2001: no profile: {} {} {}", vendor_id.trim(), product_revision.trim(), vendor_specific.trim()),
|
||||
Error::ProfileParse { detail } => write!(f, "E2002: profile parse: {detail}"),
|
||||
Error::UnlockFailed { detail } => write!(f, "E3000: unlock failed: {detail}"),
|
||||
write!(f, "E{}: {} {} {}", E_PROFILE_NOT_FOUND,
|
||||
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 } =>
|
||||
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],
|
||||
got[0], got[1], got[2], got[3]),
|
||||
Error::NotUnlocked => write!(f, "E3002: not unlocked"),
|
||||
Error::NotCalibrated => write!(f, "E3003: not calibrated"),
|
||||
Error::NotUnlocked =>
|
||||
write!(f, "E{}", E_NOT_UNLOCKED),
|
||||
Error::NotCalibrated =>
|
||||
write!(f, "E{}", E_NOT_CALIBRATED),
|
||||
Error::ScsiError { opcode, status, sense_key } =>
|
||||
write!(f, "E4000: SCSI 0x{opcode:02x} failed: status=0x{status:02x} sense=0x{sense_key:02x}"),
|
||||
Error::ScsiTimeout { opcode } => write!(f, "E4001: SCSI 0x{opcode:02x} timeout"),
|
||||
Error::IoError { source } => write!(f, "E5000: {source}"),
|
||||
Error::DiscError { detail } => write!(f, "E6000: disc: {detail}"),
|
||||
Error::AacsError { detail } => write!(f, "E7000: AACS: {detail}"),
|
||||
write!(f, "E{}: opcode=0x{:02x} status=0x{:02x} sense=0x{:02x}",
|
||||
E_SCSI_ERROR, opcode, status, sense_key),
|
||||
Error::ScsiTimeout { opcode } =>
|
||||
write!(f, "E{}: opcode=0x{:02x}", E_SCSI_TIMEOUT, opcode),
|
||||
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
@@ -8,6 +8,22 @@
|
||||
use crate::error::{Error, Result};
|
||||
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)]
|
||||
pub enum DataDirection {
|
||||
None,
|
||||
|
||||
Reference in New Issue
Block a user