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
+82 -64
View File
@@ -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),
}
}
}