scsi: promote SenseFamily to a lib-level SCSI-fact primitive

Moved SenseFamily::from_sense_key + is_wedge_family from disc/read_error.rs
into scsi/mod.rs (with its own tests) and re-exported at the crate root.
This is pure SCSI sense-code classification -- objective hardware fact, zero
recovery-policy opinion -- so it belongs in the library primitives, unlike
the retry-DECISION state machine (ReadCtx/PassSummary/ReadAction/
handle_read_error) built on top of it, which is freemkv's specific recovery
strategy and is moving to freemkv-engine next.

disc/read_error.rs and disc/section_recover.rs now import SenseFamily from
crate::scsi instead of defining/re-exporting their own copy. No behavior
change. Precommit green on Rust 1.86 (fmt+clippy+test).
This commit is contained in:
MattJackson
2026-07-28 11:32:13 -07:00
parent 05fd7fcbfa
commit 227545fabc
4 changed files with 73 additions and 32 deletions
+1 -30
View File
@@ -11,6 +11,7 @@
use crate::error::Error;
use crate::scsi;
use crate::scsi::SenseFamily;
/// In-flight bookkeeping a read loop must keep across iterations. The
/// handler reads and mutates this. Caller owns the storage.
@@ -122,36 +123,6 @@ pub struct ReadCtx {
pub marginal_recovered: u64,
}
/// Coarse classification of a SCSI sense key for diagnostic logging.
/// Wedge-family events (Hardware + IllegalRequest) get their own
/// transition log when the sense family changes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SenseFamily {
NotReady,
Medium,
Hardware,
IllegalRequest,
Other,
}
impl SenseFamily {
pub fn from_sense_key(sense_key: u8) -> Self {
match sense_key {
scsi::SENSE_KEY_NOT_READY => SenseFamily::NotReady,
scsi::SENSE_KEY_MEDIUM_ERROR => SenseFamily::Medium,
scsi::SENSE_KEY_HARDWARE_ERROR => SenseFamily::Hardware,
scsi::SENSE_KEY_ILLEGAL_REQUEST => SenseFamily::IllegalRequest,
_ => SenseFamily::Other,
}
}
/// True for the "wedge family" — Hardware + IllegalRequest are
/// the senses the BU40N firmware returns in its fast-fail state.
pub fn is_wedge_family(self) -> bool {
matches!(self, SenseFamily::Hardware | SenseFamily::IllegalRequest)
}
}
impl ReadCtx {
/// Initial context for a Pass 1 sweep. The job is "fast and
/// accurate, get the most data in the shortest time" — Pass N
+1 -1
View File
@@ -33,7 +33,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Instant;
use super::patch::{SubRanges, recovery_read};
use super::read_error::SenseFamily;
use crate::scsi::SenseFamily;
use crate::sector::SectorSource;
/// One 2048-byte sector.
+1 -1
View File
@@ -265,7 +265,7 @@ pub use mux::{Mp4FitReport, Mp4SkipReason, mp4_fit_report};
pub use mux::build_iso_pipeline;
pub use mux::resolve_mux_key_map;
pub use mux::{MuxEvents, MuxInput, MuxOptions, MuxOutcome, NoopEvents, mux_stream};
pub use scsi::{DriveInfo, ScsiSense, ScsiTransport, drive_has_disc, list_drives};
pub use scsi::{DriveInfo, ScsiSense, ScsiTransport, SenseFamily, drive_has_disc, list_drives};
pub use sector::{
DecryptingSectorSource, FileSectorSink, FileSectorSource, KeyFetch, PrefetchedSectorSource,
SectorSink, SectorSource,
+70
View File
@@ -119,6 +119,76 @@ pub const SENSE_KEY_DATA_PROTECT: u8 = 0x07;
pub const SENSE_KEY_BLANK_CHECK: u8 = 0x08;
pub const SENSE_KEY_ABORTED_COMMAND: u8 = 0x0B;
/// Coarse classification of a SCSI sense key, for callers that need to
/// branch on "what kind of failure was this" without a `match` over every
/// `SENSE_KEY_*` constant. Pure hardware-fact translation — no retry policy
/// here; see the recovery/engine layer for what to DO about a given family.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SenseFamily {
NotReady,
Medium,
Hardware,
IllegalRequest,
Other,
}
impl SenseFamily {
pub fn from_sense_key(sense_key: u8) -> Self {
match sense_key {
SENSE_KEY_NOT_READY => SenseFamily::NotReady,
SENSE_KEY_MEDIUM_ERROR => SenseFamily::Medium,
SENSE_KEY_HARDWARE_ERROR => SenseFamily::Hardware,
SENSE_KEY_ILLEGAL_REQUEST => SenseFamily::IllegalRequest,
_ => SenseFamily::Other,
}
}
/// True for the "wedge family" — some drives (e.g. the BU40N over a
/// USB-SATA bridge) return Hardware or IllegalRequest sense once their
/// firmware enters a fast-fail state after sustained bad-media reads.
pub fn is_wedge_family(self) -> bool {
matches!(self, SenseFamily::Hardware | SenseFamily::IllegalRequest)
}
}
#[cfg(test)]
mod sense_family_tests {
use super::*;
#[test]
fn classifies_each_named_sense_key() {
assert_eq!(
SenseFamily::from_sense_key(SENSE_KEY_NOT_READY),
SenseFamily::NotReady
);
assert_eq!(
SenseFamily::from_sense_key(SENSE_KEY_MEDIUM_ERROR),
SenseFamily::Medium
);
assert_eq!(
SenseFamily::from_sense_key(SENSE_KEY_HARDWARE_ERROR),
SenseFamily::Hardware
);
assert_eq!(
SenseFamily::from_sense_key(SENSE_KEY_ILLEGAL_REQUEST),
SenseFamily::IllegalRequest
);
assert_eq!(
SenseFamily::from_sense_key(SENSE_KEY_ABORTED_COMMAND),
SenseFamily::Other
);
}
#[test]
fn wedge_family_is_hardware_and_illegal_request_only() {
assert!(SenseFamily::Hardware.is_wedge_family());
assert!(SenseFamily::IllegalRequest.is_wedge_family());
assert!(!SenseFamily::Medium.is_wedge_family());
assert!(!SenseFamily::NotReady.is_wedge_family());
assert!(!SenseFamily::Other.is_wedge_family());
}
}
// ── Sense parsing ───────────────────────────────────────────────────────────
/// Decoded SPC-4 sense triple — the precise reason a SCSI command failed.