diff --git a/src/drive.rs b/src/drive.rs index 8d0a5d4..e8f1ffa 100644 --- a/src/drive.rs +++ b/src/drive.rs @@ -36,12 +36,18 @@ impl DriveSession { // Match drive to a profile by INQUIRY fields let profile = profile::find_by_drive_id(&profiles, &drive_id) .cloned() - .ok_or_else(|| Error::UnsupportedDrive(format!("{}", drive_id)))?; + .ok_or_else(|| Error::UnsupportedDrive { + vendor_id: drive_id.vendor_id.trim().to_string(), + product_id: drive_id.product_id.trim().to_string(), + product_revision: drive_id.product_revision.trim().to_string(), + })?; if !profile.supported { - return Err(Error::UnsupportedDrive(format!( - "{} — status: {:?}", drive_id, profile.status - ))); + return Err(Error::UnsupportedDrive { + vendor_id: drive_id.vendor_id.trim().to_string(), + product_id: drive_id.product_id.trim().to_string(), + product_revision: drive_id.product_revision.trim().to_string(), + }); } let platform: Box = match profile.platform { @@ -49,7 +55,11 @@ impl DriveSession { Box::new(Mt1959::new(profile.clone())) } PlatformType::Pioneer => { - return Err(Error::UnsupportedDrive("Pioneer not yet implemented".into())); + return Err(Error::UnsupportedDrive { + vendor_id: drive_id.vendor_id.trim().to_string(), + product_id: drive_id.product_id.trim().to_string(), + product_revision: "Pioneer not yet implemented".to_string(), + }); } }; @@ -71,7 +81,11 @@ impl DriveSession { Box::new(Mt1959::new(profile.clone())) } PlatformType::Pioneer => { - return Err(Error::UnsupportedDrive("Pioneer not yet implemented".into())); + return Err(Error::UnsupportedDrive { + vendor_id: drive_id.vendor_id.trim().to_string(), + product_id: drive_id.product_id.trim().to_string(), + product_revision: "Pioneer not yet implemented".to_string(), + }); } }; diff --git a/src/error.rs b/src/error.rs index 696d907..cc2fab5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,45 +1,108 @@ -use std::fmt; +/// 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 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 | #[derive(Debug)] pub enum Error { - DeviceNotFound(String), - UnsupportedDrive(String), - ScsiError { cdb: Vec, status: u8, sense: Vec }, - UnlockFailed(String), + // 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, - ProfileNotFound(String), - ProfileParse(String), - SignatureMismatch { expected: [u8; 4], got: [u8; 4] }, - Io(std::io::Error), + + // 4xxx — SCSI errors + ScsiError { opcode: u8, status: u8, sense_key: u8 }, + ScsiTimeout { opcode: u8 }, + + // 5xxx — I/O errors + IoError { source: std::io::Error }, } -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl Error { + /// Numeric error code for programmatic handling. + pub fn code(&self) -> u16 { match self { - Error::DeviceNotFound(s) => write!(f, "device not found: {s}"), - Error::UnsupportedDrive(s) => write!(f, "unsupported drive: {s}"), - Error::ScsiError { status, .. } => write!(f, "SCSI error: status 0x{status:02x}"), - Error::UnlockFailed(s) => write!(f, "unlock failed: {s}"), - Error::NotUnlocked => write!(f, "drive not unlocked, call unlock() first"), - Error::NotCalibrated => write!(f, "speed not calibrated, call calibrate() first"), - Error::ProfileNotFound(s) => write!(f, "no profile for: {s}"), - Error::ProfileParse(s) => write!(f, "profile parse error: {s}"), - Error::SignatureMismatch { expected, got } => { - write!(f, "signature mismatch: expected {:02x}{:02x}{:02x}{:02x}, got {:02x}{:02x}{:02x}{:02x}", - expected[0], expected[1], expected[2], expected[3], - got[0], got[1], got[2], got[3]) - } - Error::Io(e) => write!(f, "I/O error: {e}"), + 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, } } } -impl std::error::Error for Error {} +/// Default Display — terse, for logs. Applications should format their own messages. +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::UnsupportedDrive { vendor_id, product_id, product_revision } => + write!(f, "E2000: 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}"), + Error::SignatureMismatch { expected, got } => + write!(f, "E3001: signature mismatch: expected {:02x}{:02x}{:02x}{:02x} got {:02x}{:02x}{:02x}{:02x}", + 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::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}"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::IoError { source } => Some(source), + _ => None, + } + } +} impl From for Error { fn from(e: std::io::Error) -> Self { - Error::Io(e) + Error::IoError { source: e } } } diff --git a/src/platform/mt1959.rs b/src/platform/mt1959.rs index b18ed05..77ee2a5 100644 --- a/src/platform/mt1959.rs +++ b/src/platform/mt1959.rs @@ -78,10 +78,10 @@ impl Mt1959 { // Check verification bytes at response[12:16] if &response[12..16] != self.profile.verify.as_slice() { - return Err(Error::UnlockFailed(format!( + return Err(Error::UnlockFailed { detail: format!( "verify mismatch at [12:16]: {:02x}{:02x}{:02x}{:02x}", response[12], response[13], response[14], response[15] - ))); + ) }); } self.unlocked = true; @@ -110,9 +110,9 @@ impl Mt1959 { } } Err(Error::ScsiError { - cdb: vec![0x3C], + opcode: 0x3C, status: 0xFF, - sense: vec![], + sense_key: 0, }) } @@ -180,10 +180,10 @@ impl Platform for Mt1959 { self.validate(scsi)?; let offset = *self.profile.register_offsets.get(index as usize) - .ok_or_else(|| Error::ScsiError { - cdb: vec![], - status: 0, - sense: vec![], + .ok_or_else(|| Error::ProfileNotFound { + vendor_id: self.profile.vendor_id.clone(), + product_revision: self.profile.product_revision.clone(), + vendor_specific: format!("register index {} out of range", index), })?; let cdb = scsi::build_read_buffer(self.mode, self.buffer_id, offset, 36); diff --git a/src/profile.rs b/src/profile.rs index 68eb448..6781749 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -158,12 +158,12 @@ impl PlatformType { /// Parse a hex string like "999ec375" into [u8; 4]. fn parse_hex4(s: &str) -> Result<[u8; 4]> { if s.len() != 8 { - return Err(Error::ProfileParse(format!("expected 8 hex chars, got {}", s.len()))); + return Err(Error::ProfileParse { detail: format!("expected 8 hex chars, got {}", s.len()) }); } let mut out = [0u8; 4]; for i in 0..4 { out[i] = u8::from_str_radix(&s[i*2..i*2+2], 16) - .map_err(|e| Error::ProfileParse(format!("bad hex: {e}")))?; + .map_err(|e| Error::ProfileParse { detail: format!("bad hex: {e}") })?; } Ok(out) } @@ -171,12 +171,12 @@ fn parse_hex4(s: &str) -> Result<[u8; 4]> { /// Parse a hex string into a byte vector. fn parse_hex(s: &str) -> Result> { if s.len() % 2 != 0 { - return Err(Error::ProfileParse("odd hex length".into())); + return Err(Error::ProfileParse { detail: "odd hex length".into() }); } let mut out = Vec::with_capacity(s.len() / 2); for i in (0..s.len()).step_by(2) { out.push(u8::from_str_radix(&s[i..i+2], 16) - .map_err(|e| Error::ProfileParse(format!("bad hex: {e}")))?); + .map_err(|e| Error::ProfileParse { detail: format!("bad hex: {e}") })?); } Ok(out) } @@ -294,10 +294,10 @@ pub fn load_all(path: &std::path::Path) -> Result> { /// Parse profiles from a JSON string. fn load_from_str(data: &str) -> Result> { let json: serde_json::Value = serde_json::from_str(data) - .map_err(|e| Error::ProfileParse(format!("JSON: {e}")))?; + .map_err(|e| Error::ProfileParse { detail: format!("JSON: {e}") })?; let arr = json.as_array() - .ok_or_else(|| Error::ProfileParse("expected array".into()))?; + .ok_or_else(|| Error::ProfileParse { detail: "expected array".into() })?; let mut profiles = Vec::with_capacity(arr.len()); for entry in arr { diff --git a/src/scsi.rs b/src/scsi.rs index a8e1ea4..b67fac8 100644 --- a/src/scsi.rs +++ b/src/scsi.rs @@ -76,7 +76,7 @@ impl SgIoTransport { let fd = unsafe { libc::open(c_path.as_ptr() as *const libc::c_char, libc::O_RDWR | libc::O_NONBLOCK) }; if fd < 0 { - return Err(Error::DeviceNotFound(device.display().to_string())); + return Err(Error::DeviceNotFound { path: device.display().to_string() }); } Ok(SgIoTransport { fd }) } @@ -120,16 +120,17 @@ impl ScsiTransport for SgIoTransport { }; if ret < 0 { - return Err(Error::Io(std::io::Error::last_os_error())); + return Err(Error::IoError { source: std::io::Error::last_os_error() }); } let bytes_transferred = (data.len() as i32 - hdr.resid) as usize; if hdr.status != 0 { + let sense_key = if hdr.sb_len_wr > 2 { sense[2] & 0x0F } else { 0 }; return Err(Error::ScsiError { - cdb: cdb.to_vec(), + opcode: cdb[0], status: hdr.status, - sense: sense[..hdr.sb_len_wr as usize].to_vec(), + sense_key, }); }