Structured error codes: E1000-E5000, no user-facing text in library
Error codes: 1000-1001: device errors (not found, permission) 2000-2002: profile errors (unsupported, not found, parse) 3000-3003: unlock errors (failed, signature, not unlocked, not calibrated) 4000-4001: SCSI errors (command failed, timeout) 5000: I/O errors All errors carry structured data (vendor_id, opcode, etc). Applications format their own user-facing messages. Library returns code + data, never English text.
This commit is contained in:
+20
-6
@@ -36,12 +36,18 @@ impl DriveSession {
|
|||||||
// Match drive to a profile by INQUIRY fields
|
// Match drive to a profile by INQUIRY fields
|
||||||
let profile = profile::find_by_drive_id(&profiles, &drive_id)
|
let profile = profile::find_by_drive_id(&profiles, &drive_id)
|
||||||
.cloned()
|
.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 {
|
if !profile.supported {
|
||||||
return Err(Error::UnsupportedDrive(format!(
|
return Err(Error::UnsupportedDrive {
|
||||||
"{} — status: {:?}", drive_id, profile.status
|
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<dyn Platform> = match profile.platform {
|
let platform: Box<dyn Platform> = match profile.platform {
|
||||||
@@ -49,7 +55,11 @@ impl DriveSession {
|
|||||||
Box::new(Mt1959::new(profile.clone()))
|
Box::new(Mt1959::new(profile.clone()))
|
||||||
}
|
}
|
||||||
PlatformType::Pioneer => {
|
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()))
|
Box::new(Mt1959::new(profile.clone()))
|
||||||
}
|
}
|
||||||
PlatformType::Pioneer => {
|
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(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+90
-27
@@ -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)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
DeviceNotFound(String),
|
// 1xxx — Device errors
|
||||||
UnsupportedDrive(String),
|
DeviceNotFound { path: String },
|
||||||
ScsiError { cdb: Vec<u8>, status: u8, sense: Vec<u8> },
|
DevicePermission { path: String },
|
||||||
UnlockFailed(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,
|
NotUnlocked,
|
||||||
NotCalibrated,
|
NotCalibrated,
|
||||||
ProfileNotFound(String),
|
|
||||||
ProfileParse(String),
|
// 4xxx — SCSI errors
|
||||||
SignatureMismatch { expected: [u8; 4], got: [u8; 4] },
|
ScsiError { opcode: u8, status: u8, sense_key: u8 },
|
||||||
Io(std::io::Error),
|
ScsiTimeout { opcode: u8 },
|
||||||
|
|
||||||
|
// 5xxx — I/O errors
|
||||||
|
IoError { source: std::io::Error },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
impl Error {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
/// Numeric error code for programmatic handling.
|
||||||
|
pub fn code(&self) -> u16 {
|
||||||
match self {
|
match self {
|
||||||
Error::DeviceNotFound(s) => write!(f, "device not found: {s}"),
|
Error::DeviceNotFound { .. } => 1000,
|
||||||
Error::UnsupportedDrive(s) => write!(f, "unsupported drive: {s}"),
|
Error::DevicePermission { .. } => 1001,
|
||||||
Error::ScsiError { status, .. } => write!(f, "SCSI error: status 0x{status:02x}"),
|
Error::UnsupportedDrive { .. } => 2000,
|
||||||
Error::UnlockFailed(s) => write!(f, "unlock failed: {s}"),
|
Error::ProfileNotFound { .. } => 2001,
|
||||||
Error::NotUnlocked => write!(f, "drive not unlocked, call unlock() first"),
|
Error::ProfileParse { .. } => 2002,
|
||||||
Error::NotCalibrated => write!(f, "speed not calibrated, call calibrate() first"),
|
Error::UnlockFailed { .. } => 3000,
|
||||||
Error::ProfileNotFound(s) => write!(f, "no profile for: {s}"),
|
Error::SignatureMismatch { .. } => 3001,
|
||||||
Error::ProfileParse(s) => write!(f, "profile parse error: {s}"),
|
Error::NotUnlocked => 3002,
|
||||||
Error::SignatureMismatch { expected, got } => {
|
Error::NotCalibrated => 3003,
|
||||||
write!(f, "signature mismatch: expected {:02x}{:02x}{:02x}{:02x}, got {:02x}{:02x}{:02x}{:02x}",
|
Error::ScsiError { .. } => 4000,
|
||||||
expected[0], expected[1], expected[2], expected[3],
|
Error::ScsiTimeout { .. } => 4001,
|
||||||
got[0], got[1], got[2], got[3])
|
Error::IoError { .. } => 5000,
|
||||||
}
|
|
||||||
Error::Io(e) => write!(f, "I/O error: {e}"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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<std::io::Error> for Error {
|
impl From<std::io::Error> for Error {
|
||||||
fn from(e: std::io::Error) -> Self {
|
fn from(e: std::io::Error) -> Self {
|
||||||
Error::Io(e)
|
Error::IoError { source: e }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,10 +78,10 @@ impl Mt1959 {
|
|||||||
|
|
||||||
// Check verification bytes at response[12:16]
|
// Check verification bytes at response[12:16]
|
||||||
if &response[12..16] != self.profile.verify.as_slice() {
|
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}",
|
"verify mismatch at [12:16]: {:02x}{:02x}{:02x}{:02x}",
|
||||||
response[12], response[13], response[14], response[15]
|
response[12], response[13], response[14], response[15]
|
||||||
)));
|
) });
|
||||||
}
|
}
|
||||||
|
|
||||||
self.unlocked = true;
|
self.unlocked = true;
|
||||||
@@ -110,9 +110,9 @@ impl Mt1959 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(Error::ScsiError {
|
Err(Error::ScsiError {
|
||||||
cdb: vec![0x3C],
|
opcode: 0x3C,
|
||||||
status: 0xFF,
|
status: 0xFF,
|
||||||
sense: vec![],
|
sense_key: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,10 +180,10 @@ impl Platform for Mt1959 {
|
|||||||
self.validate(scsi)?;
|
self.validate(scsi)?;
|
||||||
|
|
||||||
let offset = *self.profile.register_offsets.get(index as usize)
|
let offset = *self.profile.register_offsets.get(index as usize)
|
||||||
.ok_or_else(|| Error::ScsiError {
|
.ok_or_else(|| Error::ProfileNotFound {
|
||||||
cdb: vec![],
|
vendor_id: self.profile.vendor_id.clone(),
|
||||||
status: 0,
|
product_revision: self.profile.product_revision.clone(),
|
||||||
sense: vec![],
|
vendor_specific: format!("register index {} out of range", index),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let cdb = scsi::build_read_buffer(self.mode, self.buffer_id, offset, 36);
|
let cdb = scsi::build_read_buffer(self.mode, self.buffer_id, offset, 36);
|
||||||
|
|||||||
+6
-6
@@ -158,12 +158,12 @@ impl PlatformType {
|
|||||||
/// Parse a hex string like "999ec375" into [u8; 4].
|
/// Parse a hex string like "999ec375" into [u8; 4].
|
||||||
fn parse_hex4(s: &str) -> Result<[u8; 4]> {
|
fn parse_hex4(s: &str) -> Result<[u8; 4]> {
|
||||||
if s.len() != 8 {
|
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];
|
let mut out = [0u8; 4];
|
||||||
for i in 0..4 {
|
for i in 0..4 {
|
||||||
out[i] = u8::from_str_radix(&s[i*2..i*2+2], 16)
|
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)
|
Ok(out)
|
||||||
}
|
}
|
||||||
@@ -171,12 +171,12 @@ fn parse_hex4(s: &str) -> Result<[u8; 4]> {
|
|||||||
/// Parse a hex string into a byte vector.
|
/// Parse a hex string into a byte vector.
|
||||||
fn parse_hex(s: &str) -> Result<Vec<u8>> {
|
fn parse_hex(s: &str) -> Result<Vec<u8>> {
|
||||||
if s.len() % 2 != 0 {
|
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);
|
let mut out = Vec::with_capacity(s.len() / 2);
|
||||||
for i in (0..s.len()).step_by(2) {
|
for i in (0..s.len()).step_by(2) {
|
||||||
out.push(u8::from_str_radix(&s[i..i+2], 16)
|
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)
|
Ok(out)
|
||||||
}
|
}
|
||||||
@@ -294,10 +294,10 @@ pub fn load_all(path: &std::path::Path) -> Result<Vec<DriveProfile>> {
|
|||||||
/// Parse profiles from a JSON string.
|
/// Parse profiles from a JSON string.
|
||||||
fn load_from_str(data: &str) -> Result<Vec<DriveProfile>> {
|
fn load_from_str(data: &str) -> Result<Vec<DriveProfile>> {
|
||||||
let json: serde_json::Value = serde_json::from_str(data)
|
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()
|
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());
|
let mut profiles = Vec::with_capacity(arr.len());
|
||||||
for entry in arr {
|
for entry in arr {
|
||||||
|
|||||||
+5
-4
@@ -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) };
|
let fd = unsafe { libc::open(c_path.as_ptr() as *const libc::c_char, libc::O_RDWR | libc::O_NONBLOCK) };
|
||||||
if fd < 0 {
|
if fd < 0 {
|
||||||
return Err(Error::DeviceNotFound(device.display().to_string()));
|
return Err(Error::DeviceNotFound { path: device.display().to_string() });
|
||||||
}
|
}
|
||||||
Ok(SgIoTransport { fd })
|
Ok(SgIoTransport { fd })
|
||||||
}
|
}
|
||||||
@@ -120,16 +120,17 @@ impl ScsiTransport for SgIoTransport {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if ret < 0 {
|
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;
|
let bytes_transferred = (data.len() as i32 - hdr.resid) as usize;
|
||||||
|
|
||||||
if hdr.status != 0 {
|
if hdr.status != 0 {
|
||||||
|
let sense_key = if hdr.sb_len_wr > 2 { sense[2] & 0x0F } else { 0 };
|
||||||
return Err(Error::ScsiError {
|
return Err(Error::ScsiError {
|
||||||
cdb: cdb.to_vec(),
|
opcode: cdb[0],
|
||||||
status: hdr.status,
|
status: hdr.status,
|
||||||
sense: sense[..hdr.sb_len_wr as usize].to_vec(),
|
sense_key,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user