pub const E_DEVICE_NOT_FOUND: u16 = 1000;
pub const E_DEVICE_PERMISSION: u16 = 1001;
pub const E_DEVICE_NOT_READY: u16 = 1002;
pub const E_DEVICE_RESET_FAILED: u16 = 1003;
pub const E_SCSI_INTERFACE_UNAVAILABLE: u16 = 1004;
pub const E_DEVICE_LOCKED: u16 = 1005;
pub const E_IOKIT_PLUGIN_FAILED: u16 = 1006;
pub const E_UNSUPPORTED_DRIVE: u16 = 2000;
pub const E_PROFILE_PARSE: u16 = 2002;
pub const E_UNSUPPORTED_PLATFORM: u16 = 2003;
pub const E_PLATFORM_NOT_IMPLEMENTED: u16 = 2004;
pub const E_UNLOCK_FAILED: u16 = 3000;
pub const E_SIGNATURE_MISMATCH: u16 = 3001;
pub const E_SCSI_ERROR: u16 = 4000;
pub const E_INVALID_CDB_LENGTH: u16 = 4001;
pub const E_IO_ERROR: u16 = 5000;
pub const E_DISC_READ: u16 = 6000;
pub const E_MPLS_PARSE: u16 = 6001;
pub const E_CLPI_PARSE: u16 = 6002;
pub const E_UDF_NOT_FOUND: u16 = 6003;
pub const E_DISC_TITLE_RANGE: u16 = 6005;
pub const E_IFO_PARSE: u16 = 6007;
pub const E_MKV_INVALID: u16 = 6008;
pub const E_NO_STREAMS: u16 = 6009;
pub const E_HALTED: u16 = 6010;
pub const E_MAPFILE_INVALID: u16 = 6011;
pub const E_UDF_BUFFER_TOO_SMALL: u16 = 6012;
pub const E_AACS_NO_KEYS: u16 = 7000;
pub const E_AACS_CERT_SHORT: u16 = 7001;
pub const E_AACS_AGID_ALLOC: u16 = 7002;
pub const E_AACS_CERT_REJECTED: u16 = 7003;
pub const E_AACS_CERT_READ: u16 = 7004;
pub const E_AACS_CERT_VERIFY: u16 = 7005;
pub const E_AACS_KEY_READ: u16 = 7006;
pub const E_AACS_KEY_REJECTED: u16 = 7007;
pub const E_AACS_KEY_VERIFY: u16 = 7008;
pub const E_AACS_VID_READ: u16 = 7009;
pub const E_AACS_VID_MAC: u16 = 7010;
pub const E_AACS_DATA_KEY: u16 = 7011;
pub const E_DECRYPT_FAILED: u16 = 7013;
pub const E_CSS_AUTH_FAILED: u16 = 7014;
pub const E_AACS_HOST_CERT_REJECTED: u16 = 7015;
pub const E_AACS_RAW_READ_UNSUPPORTED: u16 = 7016;
pub const E_AACS_VID_UNAVAILABLE: u16 = 7017;
pub const E_AACS_MK_UNAVAILABLE: u16 = 7018;
pub const E_AACS_VUK_NOT_IN_KEYDB: u16 = 7019;
pub const E_DRIVE_PROFILE_MISSING: u16 = 7020;
pub const E_VID_CDB_UNAVAILABLE: u16 = 7021;
pub const E_NO_DISC_KEY: u16 = 7022;
pub const E_CSS_KEY_MISSING: u16 = 7023;
pub const E_AACS_NO_HOST_CERT: u16 = 7024;
pub const E_AACS_BUS_KEY_UNAVAILABLE: u16 = 7025;
pub const E_KEYDB_CONNECT: u16 = 8000;
pub const E_KEYDB_HTTP: u16 = 8001;
pub const E_KEYDB_INVALID: u16 = 8002;
pub const E_KEYDB_WRITE: u16 = 8003;
pub const E_KEYDB_PARSE: u16 = 8004;
pub const E_KEYDB_LOAD: u16 = 8005;
pub const E_KEYDB_UNSUPPORTED_SCHEME: u16 = 8006;
pub const E_KEYDB_TOO_MANY_REDIRECTS: u16 = 8007;
pub const E_STREAM_READ_ONLY: u16 = 9000;
pub const E_STREAM_WRITE_ONLY: u16 = 9001;
pub const E_STREAM_URL_INVALID: u16 = 9002;
pub const E_STREAM_URL_MISSING_PATH: u16 = 9003;
pub const E_STREAM_URL_MISSING_PORT: u16 = 9004;
pub const E_PES_FRAME_TOO_LARGE: u16 = 9005;
pub const E_PES_INVALID_MAGIC: u16 = 9006;
pub const E_ISO_TOO_LARGE: u16 = 9007;
pub const E_NO_METADATA: u16 = 9008;
pub const E_DISC_URL_NOT_DIRECT: u16 = 9009;
pub const E_DIR_RAW_REJECTED: u16 = 9019;
pub const E_HEVC_PARAM_PARSE: u16 = 9010;
pub const E_MUX_TRACK_RANGE: u16 = 9011;
pub const E_FMP4_UNIMPLEMENTED: u16 = 9012;
pub const E_DEMUX_THREAD_PANICKED: u16 = 9013;
pub const E_PIPELINE_JOIN_TIMEOUT: u16 = 9014;
pub const E_PIPELINE_CONSUMER_PANICKED: u16 = 9015;
pub const E_SWEEP_CONSUMER_GONE: u16 = 9016;
pub const E_PES_TRACK_TOO_LARGE: u16 = 9017;
pub const E_PIPELINE_CONSUMER_GONE: u16 = 9018;
pub const E_DISC_CAPACITY_OVERFLOW: u16 = 9020;
pub const E_DIR_MULTIPASS_REJECTED: u16 = 9024;
pub const E_DIR_SOURCE_UNSUPPORTED: u16 = 9025;
pub const E_DIR_NOT_EMPTY: u16 = 9026;
pub const E_DIR_INSUFFICIENT_SPACE: u16 = 9027;
pub const E_DIR_NAME_COLLISION: u16 = 9028;
pub const E_DIR_WRITE_FAILED: u16 = 9029;
pub const E_M2TS_PACKET_MALFORMED: u16 = 9021;
pub const E_NETWORK_ADDR_BLOCKED: u16 = 9022;
pub const E_MUX_EMPTY: u16 = 9023;
pub const E_EXTENT_NOT_UNIT_ALIGNED: u16 = 9030;
pub const E_DISC_CAPACITY_MALFORMED: u16 = 9047;
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
DeviceNotFound {
path: String,
},
DevicePermission {
path: String,
},
DeviceNotReady {
path: String,
},
DeviceResetFailed {
path: String,
},
ScsiInterfaceUnavailable {
path: String,
},
DeviceLocked {
path: String,
kr: u32,
},
IoKitPluginFailed {
path: String,
kr: u32,
},
UnsupportedDrive {
vendor_id: String,
product_id: String,
product_revision: String,
},
ProfileParse,
UnsupportedPlatform {
target: String,
},
PlatformNotImplemented {
platform: String,
},
UnlockFailed,
SignatureMismatch {
expected: [u8; 4],
got: [u8; 4],
},
ScsiError {
opcode: u8,
status: u8,
sense: Option<crate::scsi::ScsiSense>,
},
InvalidCdbLength {
len: usize,
max: usize,
},
IoError {
source: std::io::Error,
},
DiscRead {
sector: u64,
status: Option<u8>,
sense: Option<crate::scsi::ScsiSense>,
},
Halted,
MplsParse,
ClpiParse,
UdfNotFound {
path: String,
},
UdfBufferTooSmall,
DiscTitleRange {
index: usize,
count: usize,
},
IfoParse,
MkvInvalid,
NoStreams,
MapfileInvalid {
kind: &'static str,
},
AacsNoKeys,
AacsCertShort,
AacsAgidAlloc,
AacsCertRejected,
AacsCertRead,
AacsCertVerify,
AacsKeyRead,
AacsKeyRejected,
AacsKeyVerify,
AacsVidRead,
AacsVidMac,
AacsDataKey,
DecryptFailed,
CssAuthFailed,
AacsHostCertRejected,
AacsRawReadUnsupported,
AacsVidUnavailable,
AacsMkUnavailable,
AacsVukNotInKeydb,
DriveProfileMissing,
VidCdbUnavailable,
NoDiscKey {
disc_hash: String,
},
CssKeyMissing,
AacsNoHostCert {
path: String,
},
AacsBusKeyUnavailable,
KeydbConnect {
host: String,
},
KeydbHttp {
status: u16,
},
KeydbInvalid,
KeydbWrite {
path: String,
},
KeydbParse,
KeydbLoad {
path: String,
},
KeydbUnsupportedScheme {
scheme: String,
},
KeydbTooManyRedirects,
StreamReadOnly,
StreamWriteOnly,
StreamUrlInvalid {
url: String,
},
StreamUrlMissingPath {
scheme: String,
},
StreamUrlMissingPort {
addr: String,
},
NetworkAddrBlocked {
addr: String,
},
MuxEmpty,
PesFrameTooLarge {
size: usize,
},
PesInvalidMagic,
PesTrackTooLarge {
track: usize,
},
IsoTooLarge {
path: String,
},
NoMetadata,
DiscUrlNotDirect,
HevcParamParse,
MuxTrackRange {
track: usize,
tracks: usize,
},
Fmp4Unimplemented,
DemuxThreadPanicked,
PipelineJoinTimeout,
PipelineConsumerPanicked,
SweepConsumerGone,
PipelineConsumerGone,
DiscCapacityOverflow,
ExtentNotUnitAligned,
M2tsPacketMalformed,
DiscCapacityMalformed,
DirRawRejected,
DirMultipassRejected,
DirSourceUnsupported,
DirNotEmpty,
DirInsufficientSpace {
required: u64,
available: u64,
},
DirNameCollision {
host: String,
},
DirWriteFailed {
errno: Option<i32>,
},
}
impl Error {
pub fn code(&self) -> u16 {
match self {
Error::DeviceNotFound { .. } => E_DEVICE_NOT_FOUND,
Error::DevicePermission { .. } => E_DEVICE_PERMISSION,
Error::DeviceNotReady { .. } => E_DEVICE_NOT_READY,
Error::DeviceResetFailed { .. } => E_DEVICE_RESET_FAILED,
Error::ScsiInterfaceUnavailable { .. } => E_SCSI_INTERFACE_UNAVAILABLE,
Error::DeviceLocked { .. } => E_DEVICE_LOCKED,
Error::IoKitPluginFailed { .. } => E_IOKIT_PLUGIN_FAILED,
Error::UnsupportedDrive { .. } => E_UNSUPPORTED_DRIVE,
Error::ProfileParse => E_PROFILE_PARSE,
Error::UnsupportedPlatform { .. } => E_UNSUPPORTED_PLATFORM,
Error::PlatformNotImplemented { .. } => E_PLATFORM_NOT_IMPLEMENTED,
Error::UnlockFailed => E_UNLOCK_FAILED,
Error::SignatureMismatch { .. } => E_SIGNATURE_MISMATCH,
Error::ScsiError { .. } => E_SCSI_ERROR,
Error::InvalidCdbLength { .. } => E_INVALID_CDB_LENGTH,
Error::IoError { .. } => E_IO_ERROR,
Error::DiscRead { .. } => E_DISC_READ,
Error::Halted => E_HALTED,
Error::MplsParse => E_MPLS_PARSE,
Error::ClpiParse => E_CLPI_PARSE,
Error::UdfNotFound { .. } => E_UDF_NOT_FOUND,
Error::UdfBufferTooSmall => E_UDF_BUFFER_TOO_SMALL,
Error::DiscTitleRange { .. } => E_DISC_TITLE_RANGE,
Error::IfoParse => E_IFO_PARSE,
Error::MkvInvalid => E_MKV_INVALID,
Error::NoStreams => E_NO_STREAMS,
Error::MapfileInvalid { .. } => E_MAPFILE_INVALID,
Error::AacsNoKeys => E_AACS_NO_KEYS,
Error::AacsCertShort => E_AACS_CERT_SHORT,
Error::AacsAgidAlloc => E_AACS_AGID_ALLOC,
Error::AacsCertRejected => E_AACS_CERT_REJECTED,
Error::AacsCertRead => E_AACS_CERT_READ,
Error::AacsCertVerify => E_AACS_CERT_VERIFY,
Error::AacsKeyRead => E_AACS_KEY_READ,
Error::AacsKeyRejected => E_AACS_KEY_REJECTED,
Error::AacsKeyVerify => E_AACS_KEY_VERIFY,
Error::AacsVidRead => E_AACS_VID_READ,
Error::AacsVidMac => E_AACS_VID_MAC,
Error::AacsDataKey => E_AACS_DATA_KEY,
Error::DecryptFailed => E_DECRYPT_FAILED,
Error::CssAuthFailed => E_CSS_AUTH_FAILED,
Error::AacsHostCertRejected => E_AACS_HOST_CERT_REJECTED,
Error::AacsRawReadUnsupported => E_AACS_RAW_READ_UNSUPPORTED,
Error::AacsVidUnavailable => E_AACS_VID_UNAVAILABLE,
Error::AacsMkUnavailable => E_AACS_MK_UNAVAILABLE,
Error::AacsVukNotInKeydb => E_AACS_VUK_NOT_IN_KEYDB,
Error::DriveProfileMissing => E_DRIVE_PROFILE_MISSING,
Error::VidCdbUnavailable => E_VID_CDB_UNAVAILABLE,
Error::NoDiscKey { .. } => E_NO_DISC_KEY,
Error::CssKeyMissing => E_CSS_KEY_MISSING,
Error::AacsNoHostCert { .. } => E_AACS_NO_HOST_CERT,
Error::AacsBusKeyUnavailable => E_AACS_BUS_KEY_UNAVAILABLE,
Error::KeydbConnect { .. } => E_KEYDB_CONNECT,
Error::KeydbHttp { .. } => E_KEYDB_HTTP,
Error::KeydbInvalid => E_KEYDB_INVALID,
Error::KeydbWrite { .. } => E_KEYDB_WRITE,
Error::KeydbParse => E_KEYDB_PARSE,
Error::KeydbLoad { .. } => E_KEYDB_LOAD,
Error::KeydbUnsupportedScheme { .. } => E_KEYDB_UNSUPPORTED_SCHEME,
Error::KeydbTooManyRedirects => E_KEYDB_TOO_MANY_REDIRECTS,
Error::StreamReadOnly => E_STREAM_READ_ONLY,
Error::StreamWriteOnly => E_STREAM_WRITE_ONLY,
Error::StreamUrlInvalid { .. } => E_STREAM_URL_INVALID,
Error::StreamUrlMissingPath { .. } => E_STREAM_URL_MISSING_PATH,
Error::StreamUrlMissingPort { .. } => E_STREAM_URL_MISSING_PORT,
Error::NetworkAddrBlocked { .. } => E_NETWORK_ADDR_BLOCKED,
Error::MuxEmpty => E_MUX_EMPTY,
Error::PesFrameTooLarge { .. } => E_PES_FRAME_TOO_LARGE,
Error::PesInvalidMagic => E_PES_INVALID_MAGIC,
Error::PesTrackTooLarge { .. } => E_PES_TRACK_TOO_LARGE,
Error::IsoTooLarge { .. } => E_ISO_TOO_LARGE,
Error::NoMetadata => E_NO_METADATA,
Error::DiscUrlNotDirect => E_DISC_URL_NOT_DIRECT,
Error::HevcParamParse => E_HEVC_PARAM_PARSE,
Error::MuxTrackRange { .. } => E_MUX_TRACK_RANGE,
Error::Fmp4Unimplemented => E_FMP4_UNIMPLEMENTED,
Error::DemuxThreadPanicked => E_DEMUX_THREAD_PANICKED,
Error::PipelineJoinTimeout => E_PIPELINE_JOIN_TIMEOUT,
Error::PipelineConsumerPanicked => E_PIPELINE_CONSUMER_PANICKED,
Error::SweepConsumerGone => E_SWEEP_CONSUMER_GONE,
Error::PipelineConsumerGone => E_PIPELINE_CONSUMER_GONE,
Error::DiscCapacityOverflow => E_DISC_CAPACITY_OVERFLOW,
Error::ExtentNotUnitAligned => E_EXTENT_NOT_UNIT_ALIGNED,
Error::M2tsPacketMalformed => E_M2TS_PACKET_MALFORMED,
Error::DiscCapacityMalformed => E_DISC_CAPACITY_MALFORMED,
Error::DirRawRejected => E_DIR_RAW_REJECTED,
Error::DirMultipassRejected => E_DIR_MULTIPASS_REJECTED,
Error::DirSourceUnsupported => E_DIR_SOURCE_UNSUPPORTED,
Error::DirNotEmpty => E_DIR_NOT_EMPTY,
Error::DirInsufficientSpace { .. } => E_DIR_INSUFFICIENT_SPACE,
Error::DirNameCollision { .. } => E_DIR_NAME_COLLISION,
Error::DirWriteFailed { .. } => E_DIR_WRITE_FAILED,
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::DeviceNotFound { path } => write!(f, "E{}: {}", self.code(), path),
Error::DevicePermission { path } => write!(f, "E{}: {}", self.code(), path),
Error::DeviceNotReady { path } => write!(f, "E{}: {}", self.code(), path),
Error::DeviceResetFailed { path } => write!(f, "E{}: {}", self.code(), path),
Error::ScsiInterfaceUnavailable { path } => write!(f, "E{}: {}", self.code(), path),
Error::DeviceLocked { path, kr } => {
write!(f, "E{}: {} 0x{:08x}", self.code(), path, kr)
}
Error::IoKitPluginFailed { path, kr } => {
write!(f, "E{}: {} 0x{:08x}", self.code(), path, kr)
}
Error::UnsupportedPlatform { target } => {
write!(f, "E{}: {}", self.code(), target)
}
Error::PlatformNotImplemented { platform } => {
write!(f, "E{}: {}", self.code(), platform)
}
Error::MapfileInvalid { kind } => {
write!(f, "E{}: {}", self.code(), kind)
}
Error::UnsupportedDrive {
vendor_id,
product_id,
product_revision,
} => write!(
f,
"E{}: {} {} {}",
self.code(),
vendor_id.trim(),
product_id.trim(),
product_revision.trim()
),
Error::SignatureMismatch { expected, got } => write!(
f,
"E{}: {:02x}{:02x}{:02x}{:02x}!={:02x}{:02x}{:02x}{:02x}",
self.code(),
expected[0],
expected[1],
expected[2],
expected[3],
got[0],
got[1],
got[2],
got[3]
),
Error::ScsiError {
opcode,
status,
sense,
} => match sense {
Some(s) => write!(
f,
"E{}: 0x{:02x}/0x{:02x}/0x{:02x}/0x{:02x}/0x{:02x}",
self.code(),
opcode,
status,
s.sense_key,
s.asc,
s.ascq,
),
None => write!(f, "E{}: 0x{:02x}/0x{:02x}", self.code(), opcode, status,),
},
Error::IoError { source } => match source.raw_os_error() {
Some(errno) => write!(f, "E{}: {}", self.code(), errno),
None => write!(f, "E{}: {:?}", self.code(), source.kind()),
},
Error::DiscRead {
sector,
status,
sense,
} => match (status, sense) {
(Some(st), Some(s)) => write!(
f,
"E{}: {} 0x{:02x}/0x{:02x}/0x{:02x}/0x{:02x}",
self.code(),
sector,
st,
s.sense_key,
s.asc,
s.ascq,
),
(Some(st), None) => write!(f, "E{}: {} 0x{:02x}", self.code(), sector, st,),
(None, Some(s)) => write!(
f,
"E{}: {} 0x{:02x}/0x{:02x}/0x{:02x}",
self.code(),
sector,
s.sense_key,
s.asc,
s.ascq,
),
(None, None) => write!(f, "E{}: {}", self.code(), sector),
},
Error::Halted => write!(f, "E{}", self.code()),
Error::UdfNotFound { path } => write!(f, "E{}: {}", self.code(), path),
Error::DiscTitleRange { index, count } => {
write!(f, "E{}: {}/{}", self.code(), index, count)
}
Error::KeydbConnect { host } => write!(f, "E{}: {}", self.code(), host),
Error::KeydbHttp { status } => write!(f, "E{}: {}", self.code(), status),
Error::KeydbWrite { path } => write!(f, "E{}: {}", self.code(), path),
Error::KeydbLoad { path } => write!(f, "E{}: {}", self.code(), path),
Error::AacsNoHostCert { path } => write!(f, "E{}: {}", self.code(), path),
Error::KeydbUnsupportedScheme { scheme } => {
write!(f, "E{}: {}", self.code(), scheme)
}
Error::StreamUrlInvalid { url } => write!(f, "E{}: {}", self.code(), url),
Error::StreamUrlMissingPath { scheme } => write!(f, "E{}: {}", self.code(), scheme),
Error::StreamUrlMissingPort { addr } => write!(f, "E{}: {}", self.code(), addr),
Error::NetworkAddrBlocked { addr } => write!(f, "E{}: {}", self.code(), addr),
Error::PesFrameTooLarge { size } => write!(f, "E{}: {}", self.code(), size),
Error::PesTrackTooLarge { track } => write!(f, "E{}: {}", self.code(), track),
Error::IsoTooLarge { path } => write!(f, "E{}: {}", self.code(), path),
Error::NoDiscKey { disc_hash } => {
if disc_hash.is_empty() {
write!(f, "E{}", self.code())
} else {
write!(f, "E{}: {}", self.code(), disc_hash)
}
}
Error::MuxTrackRange { track, tracks } => {
write!(f, "E{}: {}/{}", self.code(), track, tracks)
}
Error::InvalidCdbLength { len, max } => {
write!(f, "E{}: {}/{}", self.code(), len, max)
}
_ => write!(f, "E{}", self.code()),
}
}
}
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 {
fn from(e: std::io::Error) -> Self {
Error::IoError { source: e }
}
}
impl From<Error> for std::io::Error {
fn from(e: Error) -> Self {
if let Error::IoError { source } = e {
return source;
}
let code = e.code();
let msg = e.to_string();
let kind = match code {
E_DEVICE_PERMISSION | E_DEVICE_LOCKED => std::io::ErrorKind::PermissionDenied,
1000..=1999 => std::io::ErrorKind::NotFound,
2000..=2999 => std::io::ErrorKind::Unsupported,
3000..=3999 => std::io::ErrorKind::PermissionDenied,
4000..=4999 => std::io::ErrorKind::Other,
5000..=5999 => std::io::ErrorKind::Other,
6000..=6999 => std::io::ErrorKind::InvalidData,
7000..=7999 => std::io::ErrorKind::PermissionDenied,
8000..=8999 => std::io::ErrorKind::Other,
9000..=9001 => std::io::ErrorKind::Unsupported,
9002..=9008 => std::io::ErrorKind::InvalidInput,
9009 => std::io::ErrorKind::Unsupported,
9010 => std::io::ErrorKind::InvalidData,
9011 => std::io::ErrorKind::InvalidInput,
9012 => std::io::ErrorKind::Unsupported,
E_PIPELINE_JOIN_TIMEOUT => std::io::ErrorKind::TimedOut,
9017 => std::io::ErrorKind::InvalidInput,
9020 => std::io::ErrorKind::InvalidData,
9021 => std::io::ErrorKind::InvalidData,
E_NETWORK_ADDR_BLOCKED => std::io::ErrorKind::PermissionDenied,
E_MUX_EMPTY => std::io::ErrorKind::InvalidData,
9030 => std::io::ErrorKind::InvalidInput,
9047 => std::io::ErrorKind::InvalidData,
E_DIR_RAW_REJECTED
| E_DIR_MULTIPASS_REJECTED
| E_DIR_SOURCE_UNSUPPORTED
| E_DIR_NOT_EMPTY
| E_DIR_NAME_COLLISION => std::io::ErrorKind::InvalidInput,
E_DIR_INSUFFICIENT_SPACE | E_DIR_WRITE_FAILED => std::io::ErrorKind::Other,
_ => std::io::ErrorKind::Other,
};
std::io::Error::new(kind, msg)
}
}
pub type Result<T> = std::result::Result<T, Error>;
impl Error {
pub fn scsi_sense(&self) -> Option<&crate::scsi::ScsiSense> {
match self {
Error::ScsiError { sense: Some(s), .. } => Some(s),
Error::DiscRead { sense: Some(s), .. } => Some(s),
_ => None,
}
}
pub fn is_scsi_transport_failure(&self) -> bool {
matches!(
self,
Error::ScsiError {
status: crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE,
..
}
) || matches!(
self,
Error::DiscRead {
status: Some(crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE),
..
}
)
}
pub fn is_bridge_degradation(&self) -> bool {
let status = match self {
Error::ScsiError { status, .. } => *status,
Error::DiscRead { status, .. } => status.unwrap_or(0),
_ => return false,
};
status != crate::scsi::SCSI_STATUS_GOOD
&& status != crate::scsi::SCSI_STATUS_CHECK_CONDITION
&& status != crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE
}
pub fn is_marginal_read(&self) -> bool {
self.scsi_sense()
.map(crate::scsi::ScsiSense::is_marginal)
.unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_variants_have_distinct_codes() {
let codes = [
Error::ScsiInterfaceUnavailable { path: "p".into() }.code(),
Error::DeviceLocked {
path: "p".into(),
kr: 0,
}
.code(),
Error::IoKitPluginFailed {
path: "p".into(),
kr: 0,
}
.code(),
Error::UnsupportedPlatform { target: "x".into() }.code(),
Error::PlatformNotImplemented {
platform: "renesas".into(),
}
.code(),
Error::MapfileInvalid { kind: "hex" }.code(),
Error::DiscUrlNotDirect.code(),
Error::ExtentNotUnitAligned.code(),
Error::M2tsPacketMalformed.code(),
Error::DiscCapacityMalformed.code(),
Error::DirRawRejected.code(),
Error::DirMultipassRejected.code(),
Error::DirSourceUnsupported.code(),
Error::DirNotEmpty.code(),
Error::DirInsufficientSpace {
required: 1,
available: 0,
}
.code(),
Error::DirNameCollision { host: "x".into() }.code(),
Error::DirWriteFailed { errno: Some(28) }.code(),
];
let mut sorted = codes.to_vec();
sorted.sort();
sorted.dedup();
assert_eq!(
sorted.len(),
codes.len(),
"two new variants share a code — check error.rs constants"
);
}
#[test]
fn display_emits_no_english_words() {
let cases: &[(Error, u16)] = &[
(
Error::ScsiInterfaceUnavailable {
path: "/dev/sg4".into(),
},
E_SCSI_INTERFACE_UNAVAILABLE,
),
(
Error::DeviceLocked {
path: "/dev/sg4".into(),
kr: 0xE00002C5,
},
E_DEVICE_LOCKED,
),
(
Error::UnsupportedPlatform {
target: "freebsd".into(),
},
E_UNSUPPORTED_PLATFORM,
),
(
Error::PlatformNotImplemented {
platform: "renesas".into(),
},
E_PLATFORM_NOT_IMPLEMENTED,
),
(Error::MapfileInvalid { kind: "hex" }, E_MAPFILE_INVALID),
(Error::DiscUrlNotDirect, E_DISC_URL_NOT_DIRECT),
(Error::ExtentNotUnitAligned, E_EXTENT_NOT_UNIT_ALIGNED),
];
for (e, want_code) in cases {
let s = e.to_string();
assert!(
s.starts_with(&format!("E{}", want_code)),
"{:?} display does not lead with code: {}",
e,
s
);
for word in s.split(|c: char| !c.is_ascii_alphabetic()) {
assert!(
word.len() <= 8,
"Display contains suspicious English-looking word `{word}` in `{s}`"
);
}
}
}
#[test]
fn iokind_mapping_for_new_variants() {
use std::io::ErrorKind;
let mapped = |e: Error| -> ErrorKind {
let io: std::io::Error = e.into();
io.kind()
};
assert_eq!(
mapped(Error::ScsiInterfaceUnavailable { path: "p".into() }),
ErrorKind::NotFound
);
assert_eq!(
mapped(Error::DeviceNotFound { path: "p".into() }),
ErrorKind::NotFound
);
assert_eq!(
mapped(Error::DevicePermission { path: "p".into() }),
ErrorKind::PermissionDenied
);
assert_eq!(
mapped(Error::DeviceLocked {
path: "p".into(),
kr: 0
}),
ErrorKind::PermissionDenied
);
assert_eq!(
mapped(Error::UnsupportedPlatform { target: "x".into() }),
ErrorKind::Unsupported
);
assert_eq!(
mapped(Error::PlatformNotImplemented {
platform: "x".into()
}),
ErrorKind::Unsupported
);
assert_eq!(
mapped(Error::MapfileInvalid { kind: "hex" }),
ErrorKind::InvalidData
);
assert_eq!(mapped(Error::DiscUrlNotDirect), ErrorKind::Unsupported);
assert_eq!(mapped(Error::M2tsPacketMalformed), ErrorKind::InvalidData);
assert_eq!(mapped(Error::DiscCapacityMalformed), ErrorKind::InvalidData);
}
#[test]
fn ioerror_roundtrips_preserving_kind_and_oscode() {
use std::io::ErrorKind;
let original = std::io::Error::from_raw_os_error(13); let original_kind = original.kind();
let wrapped: Error = original.into(); let back: std::io::Error = wrapped.into(); assert_eq!(back.kind(), original_kind);
assert_eq!(back.raw_os_error(), Some(13));
let timeout: Error = std::io::Error::from(ErrorKind::TimedOut).into();
let back2: std::io::Error = timeout.into();
assert_eq!(back2.kind(), ErrorKind::TimedOut);
}
#[test]
fn discread_display_includes_ascq() {
let e = Error::DiscRead {
sector: 42,
status: Some(0x02),
sense: Some(crate::scsi::ScsiSense {
sense_key: 0x02,
asc: 0x04,
ascq: 0x3e,
}),
};
let s = e.to_string();
assert!(s.contains("0x02/0x04/0x3e"), "ascq missing from `{s}`");
}
#[test]
fn nodisckey_empty_hash_has_no_trailing_colon() {
let e = Error::NoDiscKey {
disc_hash: String::new(),
};
assert_eq!(e.to_string(), format!("E{}", E_NO_DISC_KEY));
let e2 = Error::NoDiscKey {
disc_hash: "abc".into(),
};
assert_eq!(e2.to_string(), format!("E{}: abc", E_NO_DISC_KEY));
}
#[test]
fn all_error_code_constants_are_unique() {
let mut codes = vec![
E_DEVICE_NOT_FOUND,
E_DEVICE_PERMISSION,
E_DEVICE_NOT_READY,
E_DEVICE_RESET_FAILED,
E_SCSI_INTERFACE_UNAVAILABLE,
E_DEVICE_LOCKED,
E_IOKIT_PLUGIN_FAILED,
E_UNSUPPORTED_DRIVE,
E_PROFILE_PARSE,
E_UNSUPPORTED_PLATFORM,
E_PLATFORM_NOT_IMPLEMENTED,
E_UNLOCK_FAILED,
E_SIGNATURE_MISMATCH,
E_SCSI_ERROR,
E_INVALID_CDB_LENGTH,
E_IO_ERROR,
E_DISC_READ,
E_MPLS_PARSE,
E_CLPI_PARSE,
E_UDF_NOT_FOUND,
E_DISC_TITLE_RANGE,
E_IFO_PARSE,
E_MKV_INVALID,
E_NO_STREAMS,
E_HALTED,
E_MAPFILE_INVALID,
E_UDF_BUFFER_TOO_SMALL,
E_AACS_NO_KEYS,
E_AACS_CERT_SHORT,
E_AACS_AGID_ALLOC,
E_AACS_CERT_REJECTED,
E_AACS_CERT_READ,
E_AACS_CERT_VERIFY,
E_AACS_KEY_READ,
E_AACS_KEY_REJECTED,
E_AACS_KEY_VERIFY,
E_AACS_VID_READ,
E_AACS_VID_MAC,
E_AACS_DATA_KEY,
E_DECRYPT_FAILED,
E_CSS_AUTH_FAILED,
E_AACS_HOST_CERT_REJECTED,
E_AACS_RAW_READ_UNSUPPORTED,
E_AACS_VID_UNAVAILABLE,
E_AACS_MK_UNAVAILABLE,
E_AACS_VUK_NOT_IN_KEYDB,
E_DRIVE_PROFILE_MISSING,
E_VID_CDB_UNAVAILABLE,
E_NO_DISC_KEY,
E_CSS_KEY_MISSING,
E_AACS_NO_HOST_CERT,
E_AACS_BUS_KEY_UNAVAILABLE,
E_KEYDB_CONNECT,
E_KEYDB_HTTP,
E_KEYDB_INVALID,
E_KEYDB_WRITE,
E_KEYDB_PARSE,
E_KEYDB_LOAD,
E_KEYDB_UNSUPPORTED_SCHEME,
E_KEYDB_TOO_MANY_REDIRECTS,
E_STREAM_READ_ONLY,
E_STREAM_WRITE_ONLY,
E_STREAM_URL_INVALID,
E_STREAM_URL_MISSING_PATH,
E_STREAM_URL_MISSING_PORT,
E_NETWORK_ADDR_BLOCKED,
E_MUX_EMPTY,
E_PES_FRAME_TOO_LARGE,
E_PES_INVALID_MAGIC,
E_PES_TRACK_TOO_LARGE,
E_ISO_TOO_LARGE,
E_NO_METADATA,
E_DISC_URL_NOT_DIRECT,
E_HEVC_PARAM_PARSE,
E_MUX_TRACK_RANGE,
E_FMP4_UNIMPLEMENTED,
E_DEMUX_THREAD_PANICKED,
E_PIPELINE_JOIN_TIMEOUT,
E_PIPELINE_CONSUMER_PANICKED,
E_SWEEP_CONSUMER_GONE,
E_PIPELINE_CONSUMER_GONE,
E_DISC_CAPACITY_OVERFLOW,
E_M2TS_PACKET_MALFORMED,
E_EXTENT_NOT_UNIT_ALIGNED,
E_DISC_CAPACITY_MALFORMED,
];
let original_len = codes.len();
codes.sort();
codes.dedup();
assert_eq!(
codes.len(),
original_len,
"duplicate error code constants detected — check error.rs"
);
}
#[test]
fn error_code_range_buckets_are_correct() {
assert!((1000..2000).contains(&E_DEVICE_NOT_FOUND));
assert!((1000..2000).contains(&E_DEVICE_PERMISSION));
assert!((1000..2000).contains(&E_SCSI_INTERFACE_UNAVAILABLE));
assert!((2000..3000).contains(&E_UNSUPPORTED_DRIVE));
assert!((2000..3000).contains(&E_PROFILE_PARSE));
assert!((3000..4000).contains(&E_UNLOCK_FAILED));
assert!((3000..4000).contains(&E_SIGNATURE_MISMATCH));
assert!((4000..5000).contains(&E_SCSI_ERROR));
assert!((5000..6000).contains(&E_IO_ERROR));
assert!((6000..7000).contains(&E_DISC_READ));
assert!((6000..7000).contains(&E_HALTED));
assert!((6000..7000).contains(&E_MAPFILE_INVALID));
assert!((7000..8000).contains(&E_AACS_NO_KEYS));
assert!((7000..8000).contains(&E_NO_DISC_KEY));
assert!((8000..9000).contains(&E_KEYDB_CONNECT));
assert!((8000..9000).contains(&E_KEYDB_TOO_MANY_REDIRECTS));
assert!((9000..10000).contains(&E_STREAM_READ_ONLY));
assert!((9000..10000).contains(&E_DISC_CAPACITY_MALFORMED));
}
#[test]
fn error_code_matches_constant_for_stream_variants() {
use std::io::ErrorKind;
let cases: &[(Error, u16)] = &[
(Error::StreamReadOnly, E_STREAM_READ_ONLY),
(Error::StreamWriteOnly, E_STREAM_WRITE_ONLY),
(Error::PesInvalidMagic, E_PES_INVALID_MAGIC),
(Error::NoMetadata, E_NO_METADATA),
(Error::DiscUrlNotDirect, E_DISC_URL_NOT_DIRECT),
(Error::HevcParamParse, E_HEVC_PARAM_PARSE),
(Error::Fmp4Unimplemented, E_FMP4_UNIMPLEMENTED),
(Error::DemuxThreadPanicked, E_DEMUX_THREAD_PANICKED),
(
Error::PipelineConsumerPanicked,
E_PIPELINE_CONSUMER_PANICKED,
),
(Error::SweepConsumerGone, E_SWEEP_CONSUMER_GONE),
(Error::PipelineConsumerGone, E_PIPELINE_CONSUMER_GONE),
(Error::DiscCapacityOverflow, E_DISC_CAPACITY_OVERFLOW),
(Error::MuxEmpty, E_MUX_EMPTY),
(Error::M2tsPacketMalformed, E_M2TS_PACKET_MALFORMED),
(Error::ExtentNotUnitAligned, E_EXTENT_NOT_UNIT_ALIGNED),
(Error::DiscCapacityMalformed, E_DISC_CAPACITY_MALFORMED),
(
Error::NetworkAddrBlocked {
addr: String::new(),
},
E_NETWORK_ADDR_BLOCKED,
),
];
for (e, expected_code) in cases {
assert_eq!(
e.code(),
*expected_code,
"{:?}.code() must equal {} (const)",
e,
expected_code
);
}
let to_kind = |e: Error| -> ErrorKind {
let io: std::io::Error = e.into();
io.kind()
};
assert_eq!(to_kind(Error::StreamReadOnly), ErrorKind::Unsupported);
assert_eq!(to_kind(Error::HevcParamParse), ErrorKind::InvalidData);
assert_eq!(to_kind(Error::Fmp4Unimplemented), ErrorKind::Unsupported);
assert_eq!(to_kind(Error::PipelineJoinTimeout), ErrorKind::TimedOut);
assert_eq!(
to_kind(Error::ExtentNotUnitAligned),
ErrorKind::InvalidInput
);
}
#[test]
fn error_code_matches_constant_for_aacs_variants() {
let aacs_cases: &[(Error, u16)] = &[
(Error::AacsNoKeys, E_AACS_NO_KEYS),
(Error::AacsCertShort, E_AACS_CERT_SHORT),
(Error::AacsAgidAlloc, E_AACS_AGID_ALLOC),
(Error::AacsCertRejected, E_AACS_CERT_REJECTED),
(Error::AacsCertRead, E_AACS_CERT_READ),
(Error::AacsCertVerify, E_AACS_CERT_VERIFY),
(Error::AacsKeyRead, E_AACS_KEY_READ),
(Error::AacsKeyRejected, E_AACS_KEY_REJECTED),
(Error::AacsKeyVerify, E_AACS_KEY_VERIFY),
(Error::AacsVidRead, E_AACS_VID_READ),
(Error::AacsVidMac, E_AACS_VID_MAC),
(Error::AacsDataKey, E_AACS_DATA_KEY),
(Error::DecryptFailed, E_DECRYPT_FAILED),
(Error::CssAuthFailed, E_CSS_AUTH_FAILED),
(Error::AacsHostCertRejected, E_AACS_HOST_CERT_REJECTED),
(Error::AacsRawReadUnsupported, E_AACS_RAW_READ_UNSUPPORTED),
(Error::AacsVidUnavailable, E_AACS_VID_UNAVAILABLE),
(Error::AacsMkUnavailable, E_AACS_MK_UNAVAILABLE),
(Error::AacsVukNotInKeydb, E_AACS_VUK_NOT_IN_KEYDB),
(Error::DriveProfileMissing, E_DRIVE_PROFILE_MISSING),
(Error::VidCdbUnavailable, E_VID_CDB_UNAVAILABLE),
];
for (e, expected_code) in aacs_cases {
assert_eq!(
e.code(),
*expected_code,
"{:?}.code() must be {}",
e,
expected_code
);
}
}
#[test]
fn is_scsi_transport_failure_only_for_0xff() {
use crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE;
let tf = Error::ScsiError {
opcode: 0x28,
status: SCSI_STATUS_TRANSPORT_FAILURE,
sense: None,
};
assert!(tf.is_scsi_transport_failure());
let cc = Error::ScsiError {
opcode: 0x28,
status: crate::scsi::SCSI_STATUS_CHECK_CONDITION,
sense: Some(crate::scsi::ScsiSense {
sense_key: 0x03,
asc: 0x11,
ascq: 0x00,
}),
};
assert!(!cc.is_scsi_transport_failure());
assert!(!Error::Halted.is_scsi_transport_failure());
}
#[test]
fn is_marginal_read_sense_key_coverage() {
use crate::scsi::ScsiSense;
let marginal_keys = [
0x00, 0x01, 0x02, 0x03, 0x0B, ];
for sk in marginal_keys {
let e = Error::ScsiError {
opcode: 0x28,
status: crate::scsi::SCSI_STATUS_CHECK_CONDITION,
sense: Some(ScsiSense {
sense_key: sk,
asc: 0x11,
ascq: 0x00,
}),
};
assert!(
e.is_marginal_read(),
"sense_key=0x{:02x} must be marginal",
sk
);
}
let non_marginal_keys = [0x04, 0x05, 0x06, 0x07, 0x08];
for sk in non_marginal_keys {
let e = Error::ScsiError {
opcode: 0x28,
status: crate::scsi::SCSI_STATUS_CHECK_CONDITION,
sense: Some(ScsiSense {
sense_key: sk,
asc: 0x00,
ascq: 0x00,
}),
};
assert!(
!e.is_marginal_read(),
"sense_key=0x{:02x} must NOT be marginal",
sk
);
}
}
#[test]
fn is_bridge_degradation_detects_non_standard_status() {
use crate::scsi::{
SCSI_STATUS_CHECK_CONDITION, SCSI_STATUS_GOOD, SCSI_STATUS_TRANSPORT_FAILURE,
};
for bad_status in [0x04u8, 0x05, 0x08, 0x10] {
let e = Error::ScsiError {
opcode: 0x28,
status: bad_status,
sense: None,
};
assert!(
e.is_bridge_degradation(),
"status=0x{:02x} must be bridge degradation",
bad_status
);
}
assert!(
!Error::ScsiError {
opcode: 0x28,
status: SCSI_STATUS_GOOD,
sense: None
}
.is_bridge_degradation()
);
assert!(
!Error::ScsiError {
opcode: 0x28,
status: SCSI_STATUS_CHECK_CONDITION,
sense: None
}
.is_bridge_degradation()
);
assert!(
!Error::ScsiError {
opcode: 0x28,
status: SCSI_STATUS_TRANSPORT_FAILURE,
sense: None
}
.is_bridge_degradation()
);
}
#[test]
fn scsi_sense_from_disc_read() {
use crate::scsi::ScsiSense;
let sense = ScsiSense {
sense_key: 0x02,
asc: 0x04,
ascq: 0x3e,
};
let disc_read = Error::DiscRead {
sector: 12345,
status: Some(0x02),
sense: Some(sense),
};
let got = disc_read.scsi_sense().unwrap();
assert_eq!(got.sense_key, 0x02);
assert_eq!(got.asc, 0x04);
assert_eq!(got.ascq, 0x3e);
assert!(Error::Halted.scsi_sense().is_none());
assert!(Error::NoMetadata.scsi_sense().is_none());
}
#[test]
fn signature_mismatch_display_includes_both_sides() {
let e = Error::SignatureMismatch {
expected: [0xAA, 0xBB, 0xCC, 0xDD],
got: [0x11, 0x22, 0x33, 0x44],
};
let s = e.to_string();
assert!(
s.starts_with(&format!("E{}", E_SIGNATURE_MISMATCH)),
"must start with code: {s}"
);
assert!(s.contains("aabbccdd"), "must contain expected bytes: {s}");
assert!(s.contains("11223344"), "must contain got bytes: {s}");
assert!(s.contains("!="), "must use '!=' separator: {s}");
}
#[test]
fn disc_title_range_display_is_index_slash_count() {
let e = Error::DiscTitleRange {
index: 3,
count: 10,
};
let expected = format!("E{}: 3/10", E_DISC_TITLE_RANGE);
assert_eq!(e.to_string(), expected);
}
#[test]
fn keydb_errors_include_structured_data_in_display() {
let e_connect = Error::KeydbConnect {
host: "mirror.example".into(),
};
assert!(
e_connect.to_string().contains("mirror.example"),
"KeydbConnect display must include host"
);
let e_http = Error::KeydbHttp { status: 403 };
assert!(
e_http.to_string().contains("403"),
"KeydbHttp display must include status code"
);
let e_write = Error::KeydbWrite {
path: "/root/.config/freemkv/keydb.cfg".into(),
};
assert!(
e_write.to_string().contains("/root"),
"KeydbWrite display must include path"
);
let e_load = Error::KeydbLoad {
path: "<no keydb in search paths>".into(),
};
assert!(
e_load.to_string().contains("<no keydb in search paths>"),
"KeydbLoad display must include the sentinel path"
);
let e_no_cert = Error::AacsNoHostCert {
path: "<no host cert>".into(),
};
assert!(
e_no_cert.to_string().contains("<no host cert>"),
"AacsNoHostCert display must include the sentinel path"
);
let e_scheme = Error::KeydbUnsupportedScheme {
scheme: "ftp".into(),
};
assert!(
e_scheme.to_string().contains("ftp"),
"KeydbUnsupportedScheme display must include scheme"
);
}
#[test]
fn mux_track_range_display_is_track_slash_tracks() {
let e = Error::MuxTrackRange {
track: 5,
tracks: 3,
};
let expected = format!("E{}: 5/3", E_MUX_TRACK_RANGE);
assert_eq!(e.to_string(), expected);
}
}