ld: public profile catalog API + emulation feature; clippy-clean

Expose the LibreDrive profile catalog as a curated public API on `ld`:
`ld::profiles()` returns the `Profiles` object (with `.get(drive_id)`) and
`ld::profile(filter)` finds one — the catalog of supported drives, queried
without unlocking. The unlock mechanism (firmware blobs, upload sequence, CDB
wire format) stays private; `LibreDrive` is pub(crate), reached only via
`all_unlockers()`.

The unlock-handshake wire format the bdemu test-emulator needs to impersonate a
drive (`UNLOCK_MARKER`, `is_unlock_read_buffer`) is behind a non-default
`emulation` feature; the `cdb` module compiles only under it. Rename the
`ScsiError` error variant to `Scsi` (matches css), gate the test-only
`load_bundled` and `SCSI_STATUS_CHECK_CONDITION`. Clippy-clean in both feature
configs; 86/89 tests pass.
This commit is contained in:
Matthew Jackson
2026-06-29 20:44:52 -07:00
parent 5ea0e2cc5a
commit 30f653f7f4
9 changed files with 109 additions and 51 deletions
+6
View File
@@ -7,6 +7,12 @@ license = "AGPL-3.0-only"
description = "Unlock layer for the freemkv toolchain: the Unlocker contract + self-contained firmware/AACS/CSS unlocker modules. libfreemkv depends on this and dispatches via all_unlockers()." description = "Unlock layer for the freemkv toolchain: the Unlocker contract + self-contained firmware/AACS/CSS unlocker modules. libfreemkv depends on this and dispatches via all_unlockers()."
repository = "https://github.com/freemkv/freemkv-unlock" repository = "https://github.com/freemkv/freemkv-unlock"
[features]
# Off by default. Exposes ld's unlock-handshake wire format (`UNLOCK_MARKER`,
# `is_unlock_read_buffer`) for the bdemu test-emulator, which impersonates an
# ld-unlockable drive. Real clients never enable it and keep the generic API.
emulation = []
[dependencies] [dependencies]
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
+5 -5
View File
@@ -28,7 +28,7 @@ pub enum Error {
/// A SCSI command failed. `status == SCSI_STATUS_TRANSPORT_FAILURE` with /// A SCSI command failed. `status == SCSI_STATUS_TRANSPORT_FAILURE` with
/// `sense: None` is a transport-layer fault; a CHECK CONDITION carries the /// `sense: None` is a transport-layer fault; a CHECK CONDITION carries the
/// parsed [`ScsiSense`]. /// parsed [`ScsiSense`].
ScsiError { Scsi {
/// CDB opcode that failed — diagnostic, carried for future logging. /// CDB opcode that failed — diagnostic, carried for future logging.
opcode: u8, opcode: u8,
status: u8, status: u8,
@@ -54,14 +54,14 @@ impl Error {
Error::AacsVidRead => 7012, Error::AacsVidRead => 7012,
Error::HandshakeRejected => 7013, Error::HandshakeRejected => 7013,
Error::VidUnavailable => 7014, Error::VidUnavailable => 7014,
Error::ScsiError { .. } => 7099, Error::Scsi { .. } => 7099,
} }
} }
/// The parsed sense for a CHECK CONDITION SCSI error, else `None`. /// The parsed sense for a CHECK CONDITION SCSI error, else `None`.
pub fn scsi_sense(&self) -> Option<ScsiSense> { pub fn scsi_sense(&self) -> Option<ScsiSense> {
match self { match self {
Error::ScsiError { sense, .. } => *sense, Error::Scsi { sense, .. } => *sense,
_ => None, _ => None,
} }
} }
@@ -70,7 +70,7 @@ impl Error {
pub fn is_scsi_transport_failure(&self) -> bool { pub fn is_scsi_transport_failure(&self) -> bool {
matches!( matches!(
self, self,
Error::ScsiError { status, sense: None, .. } if *status == SCSI_STATUS_TRANSPORT_FAILURE Error::Scsi { status, sense: None, .. } if *status == SCSI_STATUS_TRANSPORT_FAILURE
) )
} }
} }
@@ -79,7 +79,7 @@ impl Error {
/// at the transport level; sense parsed from the raw buffer when present). /// at the transport level; sense parsed from the raw buffer when present).
impl From<crate::scsi::ScsiError> for Error { impl From<crate::scsi::ScsiError> for Error {
fn from(e: crate::scsi::ScsiError) -> Self { fn from(e: crate::scsi::ScsiError) -> Self {
Error::ScsiError { Error::Scsi {
opcode: 0, opcode: 0,
status: e.status, status: e.status,
sense: e.sense.map(|s| ScsiSense::from_buf(&s)), sense: e.sense.map(|s| ScsiSense::from_buf(&s)),
+2 -2
View File
@@ -1350,7 +1350,7 @@ mod tests {
// A transport wedge mid-handshake must NOT be reported as a cert/key // A transport wedge mid-handshake must NOT be reported as a cert/key
// rejection — the operator needs to see the real (replug) cause, not // rejection — the operator needs to see the real (replug) cause, not
// be sent down a keydb/host-cert rabbit hole. // be sent down a keydb/host-cert rabbit hole.
let transport = Error::ScsiError { let transport = Error::Scsi {
opcode: 0xA3, // SEND KEY opcode: 0xA3, // SEND KEY
status: SCSI_STATUS_TRANSPORT_FAILURE, status: SCSI_STATUS_TRANSPORT_FAILURE,
sense: None, sense: None,
@@ -1363,7 +1363,7 @@ mod tests {
// A genuine SCSI rejection (CHECK CONDITION) IS the drive saying no, so // A genuine SCSI rejection (CHECK CONDITION) IS the drive saying no, so
// it maps to the handshake-specific code as before. // it maps to the handshake-specific code as before.
let rejected = Error::ScsiError { let rejected = Error::Scsi {
opcode: 0xA3, opcode: 0xA3,
status: SCSI_STATUS_CHECK_CONDITION, status: SCSI_STATUS_CHECK_CONDITION,
sense: Some(crate::scsi::ScsiSense { sense: Some(crate::scsi::ScsiSense {
+4 -4
View File
@@ -16,7 +16,7 @@ pub enum Error {
SignatureMismatch { expected: [u8; 4], got: [u8; 4] }, SignatureMismatch { expected: [u8; 4], got: [u8; 4] },
/// A SCSI command failed. `status == SCSI_STATUS_TRANSPORT_FAILURE` with /// A SCSI command failed. `status == SCSI_STATUS_TRANSPORT_FAILURE` with
/// `sense: None` is a transport-layer fault (bridge crash / disconnect). /// `sense: None` is a transport-layer fault (bridge crash / disconnect).
ScsiError { Scsi {
opcode: u8, opcode: u8,
status: u8, status: u8,
sense: Option<[u8; 32]>, sense: Option<[u8; 32]>,
@@ -29,7 +29,7 @@ impl std::fmt::Display for Error {
Error::ProfileParse => write!(f, "drive profile parse error"), Error::ProfileParse => write!(f, "drive profile parse error"),
Error::UnlockFailed => write!(f, "firmware unlock failed"), Error::UnlockFailed => write!(f, "firmware unlock failed"),
Error::SignatureMismatch { .. } => write!(f, "signature mismatch"), Error::SignatureMismatch { .. } => write!(f, "signature mismatch"),
Error::ScsiError { opcode, status, .. } => { Error::Scsi { opcode, status, .. } => {
write!(f, "SCSI error (opcode {opcode:#04x}, status {status:#04x})") write!(f, "SCSI error (opcode {opcode:#04x}, status {status:#04x})")
} }
} }
@@ -44,7 +44,7 @@ impl Error {
pub(crate) fn is_transport_failure(&self) -> bool { pub(crate) fn is_transport_failure(&self) -> bool {
matches!( matches!(
self, self,
Error::ScsiError { status, sense: None, .. } Error::Scsi { status, sense: None, .. }
if *status == crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE if *status == crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE
) )
} }
@@ -54,7 +54,7 @@ impl Error {
/// (the opcode is unknown at the transport level). /// (the opcode is unknown at the transport level).
impl From<crate::scsi::ScsiError> for Error { impl From<crate::scsi::ScsiError> for Error {
fn from(e: crate::scsi::ScsiError) -> Self { fn from(e: crate::scsi::ScsiError) -> Self {
Error::ScsiError { Error::Scsi {
opcode: 0, opcode: 0,
status: e.status, status: e.status,
sense: e.sense, sense: e.sense,
+40 -9
View File
@@ -6,6 +6,11 @@
//! encryption AT THE DRIVE (the unlocked drive serves clear content) and //! encryption AT THE DRIVE (the unlocked drive serves clear content) and
//! reporting the OEM Volume ID. //! reporting the OEM Volume ID.
// `cdb` carries ONLY the unlock-handshake wire format that the bdemu emulator
// needs (the real unlocker drives its CDBs from per-drive profile templates, not
// these constants). Compile it only when the `emulation` feature exposes it, so
// it never dead-codes in a normal build.
#[cfg(feature = "emulation")]
mod cdb; mod cdb;
mod error; mod error;
mod platform; mod platform;
@@ -15,25 +20,51 @@ use crate::ld::error::Result;
use crate::scsi::{DataDirection, ScsiTransport}; use crate::scsi::{DataDirection, ScsiTransport};
use crate::{DriveId, UnlockCtx, UnlockError, Unlocked, Unlocker}; use crate::{DriveId, UnlockCtx, UnlockError, Unlocked, Unlocker};
/// The LibreDrive unlocker. // ── Public profile catalog ──────────────────────────────────────────────────
//
// The catalog of drives the LibreDrive unlocker recognizes is the one piece of
// ld worth exposing publicly: it answers "is this drive supported?" without
// unlocking, and the bdemu test-emulator reads it to impersonate a supported
// drive. The unlock *mechanism* (firmware blobs, upload sequence, CDB wire
// format) stays private — only the catalog and its match result are public.
pub use profile::{DriveProfile as Profile, Identity, Platform, ProfileMatch, Profiles};
/// The bundled LibreDrive profile catalog (parsed once, process-cached), or
/// `None` if the embedded JSON fails to parse (a build-time bug). Pair with
/// [`Profiles::get`] to look up a specific drive:
/// `freemkv_unlock::ld::profiles().and_then(|p| p.get(&drive_id))`.
pub fn profiles() -> Option<&'static Profiles> {
profile::bundled()
}
/// The bundled profile matching a drive identity, if the drive is supported.
/// Convenience over [`profiles`] + [`Profiles::get`].
pub fn profile(drive_id: &DriveId) -> Option<ProfileMatch> {
profile::find_bundled(drive_id)
}
/// The unlock-handshake wire format the bdemu test-emulator needs to impersonate
/// an ld-unlockable drive: the marker an unlocked drive returns and the
/// READ BUFFER mode/buf-id that constitutes an unlock request. Behind the
/// non-default `emulation` feature so real clients never see ld's wire format.
#[cfg(feature = "emulation")]
pub use cdb::{UNLOCK_MARKER, is_unlock_read_buffer};
/// The LibreDrive unlocker. `pub(crate)` — clients reach it only through
/// [`crate::all_unlockers`], never by name (the locked-design contract).
/// ///
/// Matches a drive against the bundled profile database and, on a hit, /// Matches a drive against the bundled profile database and, on a hit,
/// runs the MediaTek MT1959 firmware-unlock (and disc-speed calibration) /// runs the MediaTek MT1959 firmware-unlock (and disc-speed calibration)
/// handshake over the raw SCSI transport. /// handshake over the raw SCSI transport.
pub struct LibreDrive; pub(crate) struct LibreDrive;
impl LibreDrive { impl LibreDrive {
pub fn new() -> Self { pub(crate) fn new() -> Self {
LibreDrive LibreDrive
} }
} }
impl Default for LibreDrive {
fn default() -> Self {
Self::new()
}
}
/// The firmware-unlocker name for a drive that has a bundled profile (for /// The firmware-unlocker name for a drive that has a bundled profile (for
/// drive-info "is this drive supported?" display), or `None`. A pure profile /// drive-info "is this drive supported?" display), or `None`. A pure profile
/// lookup — does NOT touch the drive or unlock anything. /// lookup — does NOT touch the drive or unlock anything.
+3 -3
View File
@@ -124,7 +124,7 @@ impl Mt1959 {
let cdb = self.read_buffer_sub(sub_cmd, address, expected as u8); let cdb = self.read_buffer_sub(sub_cmd, address, expected as u8);
let result = scsi.execute(&cdb, DataDirection::FromDevice, buf, 5_000)?; let result = scsi.execute(&cdb, DataDirection::FromDevice, buf, 5_000)?;
if result.bytes_transferred != expected { if result.bytes_transferred != expected {
return Err(Error::ScsiError { return Err(Error::Scsi {
opcode: SCSI_READ_BUFFER, opcode: SCSI_READ_BUFFER,
status: crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE, status: crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE,
sense: None, sense: None,
@@ -217,7 +217,7 @@ impl Mt1959 {
return Ok(()); return Ok(());
} }
} }
Err(Error::ScsiError { Err(Error::Scsi {
opcode: SCSI_READ_BUFFER, opcode: SCSI_READ_BUFFER,
status: crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE, status: crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE,
sense: None, sense: None,
@@ -327,7 +327,7 @@ impl Mt1959 {
) )
.is_err() .is_err()
{ {
return Err(Error::ScsiError { return Err(Error::Scsi {
opcode: SCSI_READ_BUFFER, opcode: SCSI_READ_BUFFER,
status: crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE, status: crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE,
sense: None, sense: None,
+32 -22
View File
@@ -3,9 +3,11 @@
use crate::ld::error::{Error, Result}; use crate::ld::error::{Error, Result};
use serde::Deserialize; use serde::Deserialize;
/// Top-level profiles file — keyed by chipset + variant. /// The LibreDrive profile catalog — the set of optical drives the firmware
/// unlocker recognizes, keyed by chipset + variant. Loaded from the bundled
/// JSON; the public entry point is [`crate::ld::profiles`].
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct ProfilesFile { pub struct Profiles {
#[serde(default)] #[serde(default)]
pub mt1959_a: Vec<DriveProfile>, pub mt1959_a: Vec<DriveProfile>,
#[serde(default)] #[serde(default)]
@@ -14,6 +16,15 @@ pub struct ProfilesFile {
pub renesas: Vec<DriveProfile>, pub renesas: Vec<DriveProfile>,
} }
impl Profiles {
/// The profile matching a drive identity, if this catalog supports that
/// drive. Two-pass per platform section: exact (incl. firmware date) then a
/// looser vendor/revision/vendor-specific match. See [`find_by_drive_id`].
pub fn get(&self, drive_id: &crate::DriveId) -> Option<ProfileMatch> {
find_by_drive_id(self, drive_id)
}
}
/// Drive identity — matched against INQUIRY data. /// Drive identity — matched against INQUIRY data.
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Identity { pub struct Identity {
@@ -235,12 +246,11 @@ where
const BUNDLED_PROFILES: &str = include_str!("profiles.json"); const BUNDLED_PROFILES: &str = include_str!("profiles.json");
/// Parse the bundled profiles fresh into an owned [`ProfilesFile`]. /// Parse the bundled profiles fresh into an owned [`Profiles`]. Test-only — the
/// /// library hot path uses the cached [`bundled`]; tests use this owned form for
/// Re-parses the embedded JSON (~800 KB) on every call; prefer /// independent copies.
/// [`bundled`] for the hot path, which parses once and caches. This #[cfg(test)]
/// owned form is kept for callers that need a mutable / independent copy. pub fn load_bundled() -> Result<Profiles> {
pub fn load_bundled() -> Result<ProfilesFile> {
load_from_str(BUNDLED_PROFILES) load_from_str(BUNDLED_PROFILES)
} }
@@ -250,9 +260,9 @@ pub fn load_bundled() -> Result<ProfilesFile> {
/// Returns `None` if the embedded JSON fails to parse (a build-time bug — /// Returns `None` if the embedded JSON fails to parse (a build-time bug —
/// the bundled blob is fixed at compile time, so the first successful call /// the bundled blob is fixed at compile time, so the first successful call
/// guarantees all later calls succeed too). /// guarantees all later calls succeed too).
pub fn bundled() -> Option<&'static ProfilesFile> { pub fn bundled() -> Option<&'static Profiles> {
use std::sync::OnceLock; use std::sync::OnceLock;
static CACHE: OnceLock<Option<ProfilesFile>> = OnceLock::new(); static CACHE: OnceLock<Option<Profiles>> = OnceLock::new();
CACHE CACHE
.get_or_init(|| load_from_str(BUNDLED_PROFILES).ok()) .get_or_init(|| load_from_str(BUNDLED_PROFILES).ok())
.as_ref() .as_ref()
@@ -267,7 +277,7 @@ pub fn find_bundled(drive_id: &crate::DriveId) -> Option<ProfileMatch> {
find_by_drive_id(bundled()?, drive_id) find_by_drive_id(bundled()?, drive_id)
} }
fn load_from_str(data: &str) -> Result<ProfilesFile> { fn load_from_str(data: &str) -> Result<Profiles> {
serde_json::from_str(data).map_err(|_| Error::ProfileParse) serde_json::from_str(data).map_err(|_| Error::ProfileParse)
} }
@@ -281,7 +291,7 @@ fn load_from_str(data: &str) -> Result<ProfilesFile> {
/// on file still match a same-model profile. All comparisons are /// on file still match a same-model profile. All comparisons are
/// whitespace-trimmed. Returns the first section that yields a match. /// whitespace-trimmed. Returns the first section that yields a match.
pub fn find_by_drive_id( pub fn find_by_drive_id(
profiles: &ProfilesFile, profiles: &Profiles,
drive_id: &crate::DriveId, drive_id: &crate::DriveId,
) -> Option<ProfileMatch> { ) -> Option<ProfileMatch> {
let v = drive_id.vendor_id.trim(); let v = drive_id.vendor_id.trim();
@@ -374,11 +384,11 @@ mod tests {
fn bundled_is_cached_and_matches_fresh_parse() { fn bundled_is_cached_and_matches_fresh_parse() {
let cached = bundled().expect("bundled profiles parse"); let cached = bundled().expect("bundled profiles parse");
let fresh = load_bundled().unwrap(); let fresh = load_bundled().unwrap();
// Same data either way (compare section sizes — ProfilesFile isn't Eq). // Same data either way (compare section sizes — Profiles isn't Eq).
assert_eq!(cached.mt1959_a.len(), fresh.mt1959_a.len()); assert_eq!(cached.mt1959_a.len(), fresh.mt1959_a.len());
// Cached accessor returns a stable address across calls. // Cached accessor returns a stable address across calls.
let a = bundled().unwrap() as *const ProfilesFile; let a = bundled().unwrap() as *const Profiles;
let b = bundled().unwrap() as *const ProfilesFile; let b = bundled().unwrap() as *const Profiles;
assert_eq!(a, b); assert_eq!(a, b);
} }
@@ -449,7 +459,7 @@ mod tests {
/// find_by_drive_id: exact match (including firmware_date) wins over loose match. /// find_by_drive_id: exact match (including firmware_date) wins over loose match.
/// Spec: two-pass — first an exact match including firmware_date, then looser. /// Spec: two-pass — first an exact match including firmware_date, then looser.
/// Build two synthetic ProfilesFile entries that differ only by firmware_date, /// Build two synthetic Profiles entries that differ only by firmware_date,
/// and verify the correct one is selected. /// and verify the correct one is selected.
/// Mutation: doing only the loose pass would return the first entry regardless of date. /// Mutation: doing only the loose pass would return the first entry regardless of date.
#[test] #[test]
@@ -483,7 +493,7 @@ mod tests {
] ]
}) })
.to_string(); .to_string();
let profiles: ProfilesFile = serde_json::from_str(&profiles_json).unwrap(); let profiles: Profiles = serde_json::from_str(&profiles_json).unwrap();
// "TESTDRV " (with space) fills 8 bytes; trim() → "TESTDRV" on both sides. // "TESTDRV " (with space) fills 8 bytes; trim() → "TESTDRV" on both sides.
let id_date1 = make_drive_id("TESTDRV ", "1.00", "XX00000", "200001010000"); let id_date1 = make_drive_id("TESTDRV ", "1.00", "XX00000", "200001010000");
@@ -528,7 +538,7 @@ mod tests {
] ]
}) })
.to_string(); .to_string();
let profiles: ProfilesFile = serde_json::from_str(&profiles_json).unwrap(); let profiles: Profiles = serde_json::from_str(&profiles_json).unwrap();
// Drive with an unknown firmware date — no exact match, loose match should work. // Drive with an unknown firmware date — no exact match, loose match should work.
// "LOOSEDR " fills 8 bytes; "000000000000" is the unknown date. // "LOOSEDR " fills 8 bytes; "000000000000" is the unknown date.
@@ -542,11 +552,11 @@ mod tests {
} }
/// load_from_str (via load_bundled) returns ProfileParse on invalid JSON. /// load_from_str (via load_bundled) returns ProfileParse on invalid JSON.
/// Mutation: returning an empty ProfilesFile instead of an error silently /// Mutation: returning an empty Profiles instead of an error silently
/// leaves the drive-profile database empty. /// leaves the drive-profile database empty.
#[test] #[test]
fn load_from_str_returns_profile_parse_on_bad_json() { fn load_from_str_returns_profile_parse_on_bad_json() {
let result: Result<ProfilesFile> = let result: Result<Profiles> =
serde_json::from_str("not valid json {{{{").map_err(|_| Error::ProfileParse); serde_json::from_str("not valid json {{{{").map_err(|_| Error::ProfileParse);
assert!(matches!(result, Err(Error::ProfileParse))); assert!(matches!(result, Err(Error::ProfileParse)));
} }
@@ -586,7 +596,7 @@ mod tests {
] ]
}) })
.to_string(); .to_string();
let profiles: ProfilesFile = serde_json::from_str(&json_str).unwrap(); let profiles: Profiles = serde_json::from_str(&json_str).unwrap();
let p = &profiles.mt1959_a[0]; // DriveProfile directly let p = &profiles.mt1959_a[0]; // DriveProfile directly
// All optional CDB fields must be None when absent from JSON. // All optional CDB fields must be None when absent from JSON.
assert!( assert!(
@@ -637,7 +647,7 @@ mod tests {
] ]
}) })
.to_string(); .to_string();
let profiles: ProfilesFile = serde_json::from_str(&json_str).unwrap(); let profiles: Profiles = serde_json::from_str(&json_str).unwrap();
assert_eq!( assert_eq!(
profiles.mt1959_a[0].signature, [0u8; 4], profiles.mt1959_a[0].signature, [0u8; 4],
"empty signature must deserialise as [0;4]" "empty signature must deserialise as [0;4]"
+13 -5
View File
@@ -14,7 +14,13 @@ pub mod scsi;
mod aacs; mod aacs;
mod css; mod css;
mod ld; // `ld` is public ONLY for its drive-profile catalog (`ld::profiles` / the
// `Profiles` object) and, under the `emulation` feature, the unlock-handshake
// wire format the bdemu test-emulator needs. The unlocker impl itself
// (`LibreDrive`) is `pub(crate)` — clients still reach unlockers only through
// [`all_unlockers`]. `aacs` and `css` carry no such public catalog, so they
// stay fully private.
pub mod ld;
use scsi::ScsiTransport; use scsi::ScsiTransport;
@@ -114,10 +120,12 @@ pub trait Unlocker: Send + Sync {
) -> std::result::Result<Unlocked, UnlockError>; ) -> std::result::Result<Unlocked, UnlockError>;
} }
/// Name of the firmware unlocker that supports this drive (for drive-info "is /// Name of the unlocker that claims this drive by identity (for drive-info "is
/// this drive supported?" display), or `None`. A pure profile lookup — does NOT /// this drive supported?" display), or `None`. A pure lookup — does NOT touch
/// touch the drive or unlock anything. /// the drive or unlock anything. Only the identity-keyed (drive-prep) unlocker
pub fn firmware_unlocker_name(drive_id: &DriveId) -> Option<&'static str> { /// can answer from a `DriveId` alone; the disc-kind-keyed unlockers (AACS / CSS)
/// don't claim a drive sight-unseen, so they never match here.
pub fn unlocker_name(drive_id: &DriveId) -> Option<&'static str> {
ld::firmware_name(drive_id) ld::firmware_name(drive_id)
} }
+4 -1
View File
@@ -66,7 +66,10 @@ impl ScsiSense {
/// SCSI status byte for a transport-layer failure (bridge crash / disconnect). /// SCSI status byte for a transport-layer failure (bridge crash / disconnect).
pub(crate) const SCSI_STATUS_TRANSPORT_FAILURE: u8 = 0xFF; pub(crate) const SCSI_STATUS_TRANSPORT_FAILURE: u8 = 0xFF;
/// SCSI status byte CHECK CONDITION (a drive sense is available). /// SCSI status byte CHECK CONDITION (a drive sense is available). Part of the
/// status contract; currently referenced only by tests asserting the
/// transport-vs-check-condition distinction.
#[allow(dead_code)]
pub(crate) const SCSI_STATUS_CHECK_CONDITION: u8 = 0x02; pub(crate) const SCSI_STATUS_CHECK_CONDITION: u8 = 0x02;
// Common opcodes used by the unlocker modules. // Common opcodes used by the unlocker modules.