unlock: finalize Unlocker 3-capability contract
Rename the trait to a generic, drive-neutral capability contract so future unlockers don't conform to LibreDrive specifics: - unlock(...) -> unlock_drive(...) (the one required capability) - read_vid(...) -> read_volume_id(...) (no-op default) - add set_max_read_speed(...) (no-op default) The trait doc now states the contract in one place: unlockers are optional drive-capability providers; the AACS layer is the always-present baseline and falls back to the full cert handshake when no unlocker matches. Implement only the capabilities your drive supports. Registry: route_unlock now calls unlock_drive; unlocker_read_vid renamed to unlocker_read_volume_id; add unlocker_set_max_read_speed (mirrors route_unlock resolution, first matching unlocker, no-op if none match). drive::init calls it on a matched drive in the post-unlock path; a speed-set failure is logged and does not fail the rip. encrypt.rs handshake updated to the new VID helper. Tests updated for the renames; added a set_max_read_speed routing test (match invokes, no-match is a safe no-op).
This commit is contained in:
+1
-1
@@ -76,7 +76,7 @@ impl Disc {
|
|||||||
// DriveId first releases the immutable borrow before we hand the
|
// DriveId first releases the immutable borrow before we hand the
|
||||||
// mutable transport to the registry.
|
// mutable transport to the registry.
|
||||||
let drive_id = session.drive_id.clone();
|
let drive_id = session.drive_id.clone();
|
||||||
match crate::unlock::unlocker_read_vid(session.scsi_mut(), &drive_id) {
|
match crate::unlock::unlocker_read_volume_id(session.scsi_mut(), &drive_id) {
|
||||||
Ok(Some(volume_id)) => {
|
Ok(Some(volume_id)) => {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
target: "freemkv::disc",
|
target: "freemkv::disc",
|
||||||
|
|||||||
@@ -399,6 +399,19 @@ impl Drive {
|
|||||||
let r = match r {
|
let r = match r {
|
||||||
Ok(Some(name)) => {
|
Ok(Some(name)) => {
|
||||||
self.unlocker_name = Some(name);
|
self.unlocker_name = Some(name);
|
||||||
|
// The matched unlocker may also be able to raise the drive to
|
||||||
|
// its maximum read speed. Best-effort: a failure here must NOT
|
||||||
|
// fail the rip — a slow drive still rips. Log and continue.
|
||||||
|
if let Err(e) =
|
||||||
|
crate::unlock::unlocker_set_max_read_speed(self.scsi.as_mut(), &self.drive_id)
|
||||||
|
{
|
||||||
|
tracing::warn!(
|
||||||
|
target: "freemkv::drive",
|
||||||
|
phase = "init",
|
||||||
|
error = ?e,
|
||||||
|
"unlocker set_max_read_speed failed; continuing at current speed"
|
||||||
|
);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
// No unlocker matched: not an error — fall through to OEM route.
|
// No unlocker matched: not an error — fall through to OEM route.
|
||||||
|
|||||||
+127
-48
@@ -4,8 +4,8 @@
|
|||||||
//! supplied by an external crate (e.g. `freemkv-unlock-ld`) and registered
|
//! supplied by an external crate (e.g. `freemkv-unlock-ld`) and registered
|
||||||
//! once at process start via [`register_unlocker`]. At drive-prep the
|
//! once at process start via [`register_unlocker`]. At drive-prep the
|
||||||
//! registry is walked in registration order; the first unlocker whose
|
//! registry is walked in registration order; the first unlocker whose
|
||||||
//! [`Unlocker::matches`] returns true is asked to [`Unlocker::unlock`] the
|
//! [`Unlocker::matches`] returns true is asked to [`Unlocker::unlock_drive`]
|
||||||
//! drive by issuing its own CDBs through the raw [`ScsiTransport`].
|
//! the drive by issuing its own CDBs through the raw [`ScsiTransport`].
|
||||||
//!
|
//!
|
||||||
//! No firmware blobs, no unlock CDBs, no drive profiles live here — only
|
//! No firmware blobs, no unlock CDBs, no drive profiles live here — only
|
||||||
//! the trait, the registry, and the routing. If no unlocker matches, the
|
//! the trait, the registry, and the routing. If no unlocker matches, the
|
||||||
@@ -17,10 +17,16 @@ use crate::identity::DriveId;
|
|||||||
use crate::scsi::ScsiTransport;
|
use crate::scsi::ScsiTransport;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
/// A pluggable drive unlocker.
|
/// A pluggable drive-capability provider.
|
||||||
|
///
|
||||||
|
/// Unlockers are optional drive-capability providers. libfreemkv's AACS
|
||||||
|
/// layer is the always-present baseline; it uses an unlocker's capabilities
|
||||||
|
/// when one matches, and does the full cert handshake when none do.
|
||||||
|
/// Implement only the capabilities your drive supports — the rest default
|
||||||
|
/// to no-op.
|
||||||
///
|
///
|
||||||
/// Implementors own everything about *how* a particular drive family is
|
/// Implementors own everything about *how* a particular drive family is
|
||||||
/// unlocked: firmware upload, vendor CDBs, variant logic. libfreemkv only
|
/// driven: firmware upload, vendor CDBs, variant logic. libfreemkv only
|
||||||
/// hands over the raw SCSI transport and the drive identity.
|
/// hands over the raw SCSI transport and the drive identity.
|
||||||
pub trait Unlocker: Send + Sync {
|
pub trait Unlocker: Send + Sync {
|
||||||
/// Stable, language-neutral identifier for this unlocker (logged).
|
/// Stable, language-neutral identifier for this unlocker (logged).
|
||||||
@@ -29,27 +35,24 @@ pub trait Unlocker: Send + Sync {
|
|||||||
/// True if this unlocker handles the given drive.
|
/// True if this unlocker handles the given drive.
|
||||||
fn matches(&self, id: &DriveId) -> bool;
|
fn matches(&self, id: &DriveId) -> bool;
|
||||||
|
|
||||||
/// Unlock the drive. The unlocker issues its own CDBs through `scsi`.
|
/// Put the drive into extended-access mode (firmware/bootloader/whatever THIS
|
||||||
/// Returns `Ok(())` once the drive is prepared for reads.
|
/// unlocker needs). The one required capability.
|
||||||
fn unlock(&self, scsi: &mut dyn ScsiTransport, id: &DriveId) -> Result<()>;
|
fn unlock_drive(&self, scsi: &mut dyn ScsiTransport, id: &DriveId) -> Result<()>;
|
||||||
|
|
||||||
/// Read the AACS Volume ID via this unlocker's OEM mechanism, if it
|
/// Read the disc Volume ID directly, bypassing the AACS cert handshake.
|
||||||
/// has one.
|
/// None → libfreemkv falls back to the cert-based read. Default: no-op.
|
||||||
///
|
fn read_volume_id(
|
||||||
/// An [`Unlocker`] unlocks *drive functionality*, not just the disc:
|
&self,
|
||||||
/// `unlock` is one capability, OEM VID retrieval is another. Once the
|
_scsi: &mut dyn ScsiTransport,
|
||||||
/// matching unlocker is identified for a drive, libfreemkv uses it for
|
_id: &DriveId,
|
||||||
/// BOTH unlock and VID. The OEM path returns the VID *without* the host
|
) -> Result<Option<[u8; 16]>> {
|
||||||
/// certificate + HRL, decoupling VID from the cert handshake.
|
|
||||||
///
|
|
||||||
/// Default is a no-op: an unlocker that provides no OEM VID path (or
|
|
||||||
/// any unlocker that doesn't override this) returns `Ok(None)`, and
|
|
||||||
/// libfreemkv falls back to the cert-based VID read. Implementors that
|
|
||||||
/// can serve the VID directly (e.g. a per-drive OEM CDB) return
|
|
||||||
/// `Ok(Some(vid))`.
|
|
||||||
fn read_vid(&self, _scsi: &mut dyn ScsiTransport, _id: &DriveId) -> Result<Option<[u8; 16]>> {
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Raise the drive to its maximum read speed. Default: no-op.
|
||||||
|
fn set_max_read_speed(&self, _scsi: &mut dyn ScsiTransport, _id: &DriveId) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process-wide ordered registry of unlockers.
|
/// Process-wide ordered registry of unlockers.
|
||||||
@@ -75,7 +78,7 @@ pub fn register_unlocker(u: Box<dyn Unlocker>) {
|
|||||||
/// drive; `name` is its [`Unlocker::name`].
|
/// drive; `name` is its [`Unlocker::name`].
|
||||||
/// * `Ok(None)` — no unlocker matched; the drive was left untouched and
|
/// * `Ok(None)` — no unlocker matched; the drive was left untouched and
|
||||||
/// the caller should fall through to the host-cert handshake.
|
/// the caller should fall through to the host-cert handshake.
|
||||||
/// * `Err(_)` — an unlocker matched but its `unlock` failed.
|
/// * `Err(_)` — an unlocker matched but its `unlock_drive` failed.
|
||||||
pub(crate) fn route_unlock(scsi: &mut dyn ScsiTransport, id: &DriveId) -> Result<Option<String>> {
|
pub(crate) fn route_unlock(scsi: &mut dyn ScsiTransport, id: &DriveId) -> Result<Option<String>> {
|
||||||
let reg = match REGISTRY.read() {
|
let reg = match REGISTRY.read() {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
@@ -86,7 +89,7 @@ pub(crate) fn route_unlock(scsi: &mut dyn ScsiTransport, id: &DriveId) -> Result
|
|||||||
for u in reg.iter() {
|
for u in reg.iter() {
|
||||||
if u.matches(id) {
|
if u.matches(id) {
|
||||||
let name = u.name().to_string();
|
let name = u.name().to_string();
|
||||||
u.unlock(scsi, id)?;
|
u.unlock_drive(scsi, id)?;
|
||||||
return Ok(Some(name));
|
return Ok(Some(name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,9 +105,9 @@ pub(crate) fn route_unlock(scsi: &mut dyn ScsiTransport, id: &DriveId) -> Result
|
|||||||
/// path (no cert handshake needed; VID is decoupled from the HRL).
|
/// path (no cert handshake needed; VID is decoupled from the HRL).
|
||||||
/// * `Ok(None)` — no unlocker matched, or the matching unlocker has no
|
/// * `Ok(None)` — no unlocker matched, or the matching unlocker has no
|
||||||
/// OEM VID path; the caller falls through to the cert-based VID read.
|
/// OEM VID path; the caller falls through to the cert-based VID read.
|
||||||
/// * `Err(_)` — the matching unlocker's `read_vid` failed (e.g. the OEM
|
/// * `Err(_)` — the matching unlocker's `read_volume_id` failed (e.g. the
|
||||||
/// CDB returned a malformed response).
|
/// OEM CDB returned a malformed response).
|
||||||
pub(crate) fn unlocker_read_vid(
|
pub(crate) fn unlocker_read_volume_id(
|
||||||
scsi: &mut dyn ScsiTransport,
|
scsi: &mut dyn ScsiTransport,
|
||||||
id: &DriveId,
|
id: &DriveId,
|
||||||
) -> Result<Option<[u8; 16]>> {
|
) -> Result<Option<[u8; 16]>> {
|
||||||
@@ -116,12 +119,40 @@ pub(crate) fn unlocker_read_vid(
|
|||||||
};
|
};
|
||||||
for u in reg.iter() {
|
for u in reg.iter() {
|
||||||
if u.matches(id) {
|
if u.matches(id) {
|
||||||
return u.read_vid(scsi, id);
|
return u.read_volume_id(scsi, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Walk the registry in order and ask the first matching unlocker to raise
|
||||||
|
/// the drive to its maximum read speed.
|
||||||
|
///
|
||||||
|
/// Mirrors [`route_unlock`]'s resolution so the SAME identified unlocker
|
||||||
|
/// that unlocks the drive is the one asked to set speed. Returns:
|
||||||
|
/// * `Ok(())` — the matching unlocker set max speed, or no unlocker
|
||||||
|
/// matched (no-op), or the matching unlocker has no speed capability
|
||||||
|
/// (its default no-op).
|
||||||
|
/// * `Err(_)` — the matching unlocker's `set_max_read_speed` failed. The
|
||||||
|
/// caller treats this as non-fatal (log and continue): a slow drive
|
||||||
|
/// still rips.
|
||||||
|
pub(crate) fn unlocker_set_max_read_speed(
|
||||||
|
scsi: &mut dyn ScsiTransport,
|
||||||
|
id: &DriveId,
|
||||||
|
) -> Result<()> {
|
||||||
|
let reg = match REGISTRY.read() {
|
||||||
|
Ok(r) => r,
|
||||||
|
// Poisoned lock ⇒ treat as "no unlocker available" (no-op).
|
||||||
|
Err(_) => return Ok(()),
|
||||||
|
};
|
||||||
|
for u in reg.iter() {
|
||||||
|
if u.matches(id) {
|
||||||
|
return u.set_max_read_speed(scsi, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Number of registered unlockers — test/introspection helper.
|
/// Number of registered unlockers — test/introspection helper.
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn registered_count() -> usize {
|
pub fn registered_count() -> usize {
|
||||||
@@ -170,15 +201,18 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Fake unlocker that records whether it ran, matches on vendor id, and
|
/// Fake unlocker that records whether it ran, matches on vendor id, and
|
||||||
/// optionally serves an OEM VID (mirroring the read_vid capability).
|
/// optionally serves a Volume ID (mirroring the read_volume_id capability)
|
||||||
|
/// or records a set_max_read_speed call.
|
||||||
struct FakeUnlocker {
|
struct FakeUnlocker {
|
||||||
want_vendor: String,
|
want_vendor: String,
|
||||||
ran: Arc<AtomicBool>,
|
ran: Arc<AtomicBool>,
|
||||||
/// VID this unlocker's OEM path returns: `Some(vid)` (capability
|
/// VID this unlocker returns: `Some(vid)` (capability present),
|
||||||
/// present), `None` (no OEM path → cert fallback). `vid_ran` records
|
/// `None` (no OEM path → cert fallback). `vid_ran` records whether
|
||||||
/// whether read_vid was consulted.
|
/// read_volume_id was consulted.
|
||||||
vid: Option<[u8; 16]>,
|
vid: Option<[u8; 16]>,
|
||||||
vid_ran: Arc<AtomicBool>,
|
vid_ran: Arc<AtomicBool>,
|
||||||
|
/// Records whether set_max_read_speed was invoked.
|
||||||
|
speed_ran: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
impl FakeUnlocker {
|
impl FakeUnlocker {
|
||||||
fn new(vendor: &str, ran: Arc<AtomicBool>) -> Self {
|
fn new(vendor: &str, ran: Arc<AtomicBool>) -> Self {
|
||||||
@@ -187,6 +221,7 @@ mod tests {
|
|||||||
ran,
|
ran,
|
||||||
vid: None,
|
vid: None,
|
||||||
vid_ran: Arc::new(AtomicBool::new(false)),
|
vid_ran: Arc::new(AtomicBool::new(false)),
|
||||||
|
speed_ran: Arc::new(AtomicBool::new(false)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn with_vid(mut self, vid: Option<[u8; 16]>, vid_ran: Arc<AtomicBool>) -> Self {
|
fn with_vid(mut self, vid: Option<[u8; 16]>, vid_ran: Arc<AtomicBool>) -> Self {
|
||||||
@@ -194,6 +229,10 @@ mod tests {
|
|||||||
self.vid_ran = vid_ran;
|
self.vid_ran = vid_ran;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
fn with_speed(mut self, speed_ran: Arc<AtomicBool>) -> Self {
|
||||||
|
self.speed_ran = speed_ran;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
impl Unlocker for FakeUnlocker {
|
impl Unlocker for FakeUnlocker {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
@@ -202,11 +241,11 @@ mod tests {
|
|||||||
fn matches(&self, id: &DriveId) -> bool {
|
fn matches(&self, id: &DriveId) -> bool {
|
||||||
id.vendor_id.trim() == self.want_vendor
|
id.vendor_id.trim() == self.want_vendor
|
||||||
}
|
}
|
||||||
fn unlock(&self, _scsi: &mut dyn ScsiTransport, _id: &DriveId) -> Result<()> {
|
fn unlock_drive(&self, _scsi: &mut dyn ScsiTransport, _id: &DriveId) -> Result<()> {
|
||||||
self.ran.store(true, Ordering::SeqCst);
|
self.ran.store(true, Ordering::SeqCst);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn read_vid(
|
fn read_volume_id(
|
||||||
&self,
|
&self,
|
||||||
_scsi: &mut dyn ScsiTransport,
|
_scsi: &mut dyn ScsiTransport,
|
||||||
_id: &DriveId,
|
_id: &DriveId,
|
||||||
@@ -214,6 +253,10 @@ mod tests {
|
|||||||
self.vid_ran.store(true, Ordering::SeqCst);
|
self.vid_ran.store(true, Ordering::SeqCst);
|
||||||
Ok(self.vid)
|
Ok(self.vid)
|
||||||
}
|
}
|
||||||
|
fn set_max_read_speed(&self, _scsi: &mut dyn ScsiTransport, _id: &DriveId) -> Result<()> {
|
||||||
|
self.speed_ran.store(true, Ordering::SeqCst);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A registered, matching unlocker runs; a non-matching identity leaves
|
/// A registered, matching unlocker runs; a non-matching identity leaves
|
||||||
@@ -231,7 +274,7 @@ mod tests {
|
|||||||
let mut scsi = NoopTransport;
|
let mut scsi = NoopTransport;
|
||||||
let matched = route_unlock(&mut scsi, &fake_id("MATCHVND")).unwrap();
|
let matched = route_unlock(&mut scsi, &fake_id("MATCHVND")).unwrap();
|
||||||
assert_eq!(matched.as_deref(), Some("fake"), "matching unlocker runs");
|
assert_eq!(matched.as_deref(), Some("fake"), "matching unlocker runs");
|
||||||
assert!(ran.load(Ordering::SeqCst), "unlock() was invoked");
|
assert!(ran.load(Ordering::SeqCst), "unlock_drive() was invoked");
|
||||||
|
|
||||||
// Non-matching identity → no unlocker runs, OEM path (None).
|
// Non-matching identity → no unlocker runs, OEM path (None).
|
||||||
ran.store(false, Ordering::SeqCst);
|
ran.store(false, Ordering::SeqCst);
|
||||||
@@ -239,19 +282,20 @@ mod tests {
|
|||||||
assert!(none.is_none(), "no match → OEM/cert fallback");
|
assert!(none.is_none(), "no match → OEM/cert fallback");
|
||||||
assert!(
|
assert!(
|
||||||
!ran.load(Ordering::SeqCst),
|
!ran.load(Ordering::SeqCst),
|
||||||
"unlock() not invoked on no-match"
|
"unlock_drive() not invoked on no-match"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `unlocker_read_vid` consults the FIRST matching unlocker's `read_vid`.
|
/// `unlocker_read_volume_id` consults the FIRST matching unlocker's
|
||||||
/// A matching unlocker that returns `Some(vid)` yields that VID (the OEM
|
/// `read_volume_id`. A matching unlocker that returns `Some(vid)` yields
|
||||||
/// path — cert handshake skipped). A matching unlocker that returns
|
/// that VID (the OEM path — cert handshake skipped). A matching unlocker
|
||||||
/// `None`, or no match at all, yields `Ok(None)` (cert fallback).
|
/// that returns `None`, or no match at all, yields `Ok(None)` (cert
|
||||||
|
/// fallback).
|
||||||
///
|
///
|
||||||
/// Distinct vendor ids keep this independent of the other registry test
|
/// Distinct vendor ids keep this independent of the other registry test
|
||||||
/// despite the process-wide shared registry.
|
/// despite the process-wide shared registry.
|
||||||
#[test]
|
#[test]
|
||||||
fn unlocker_read_vid_routes_match_else_cert() {
|
fn unlocker_read_volume_id_routes_match_else_cert() {
|
||||||
let mut scsi = NoopTransport;
|
let mut scsi = NoopTransport;
|
||||||
|
|
||||||
// Unlocker WITH an OEM VID capability. Vendor ids are exactly 8
|
// Unlocker WITH an OEM VID capability. Vendor ids are exactly 8
|
||||||
@@ -264,29 +308,64 @@ mod tests {
|
|||||||
.with_vid(Some(vid), vid_ran.clone()),
|
.with_vid(Some(vid), vid_ran.clone()),
|
||||||
));
|
));
|
||||||
|
|
||||||
// Matching identity → read_vid consulted, its VID used.
|
// Matching identity → read_volume_id consulted, its VID used.
|
||||||
let got = unlocker_read_vid(&mut scsi, &fake_id("VIDVNDOR")).unwrap();
|
let got = unlocker_read_volume_id(&mut scsi, &fake_id("VIDVNDOR")).unwrap();
|
||||||
assert_eq!(got, Some(vid), "matching unlocker's OEM VID is used");
|
assert_eq!(got, Some(vid), "matching unlocker's OEM VID is used");
|
||||||
assert!(vid_ran.load(Ordering::SeqCst), "read_vid() was consulted");
|
assert!(
|
||||||
|
vid_ran.load(Ordering::SeqCst),
|
||||||
|
"read_volume_id() was consulted"
|
||||||
|
);
|
||||||
|
|
||||||
// Unlocker that MATCHES but has NO OEM VID path (read_vid → None).
|
// Unlocker that MATCHES but has NO OEM VID path (read_volume_id → None).
|
||||||
let none_ran = Arc::new(AtomicBool::new(false));
|
let none_ran = Arc::new(AtomicBool::new(false));
|
||||||
register_unlocker(Box::new(
|
register_unlocker(Box::new(
|
||||||
FakeUnlocker::new("NOVIDVND", Arc::new(AtomicBool::new(false)))
|
FakeUnlocker::new("NOVIDVND", Arc::new(AtomicBool::new(false)))
|
||||||
.with_vid(None, none_ran.clone()),
|
.with_vid(None, none_ran.clone()),
|
||||||
));
|
));
|
||||||
let got = unlocker_read_vid(&mut scsi, &fake_id("NOVIDVND")).unwrap();
|
let got = unlocker_read_volume_id(&mut scsi, &fake_id("NOVIDVND")).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
got.is_none(),
|
got.is_none(),
|
||||||
"unlocker without OEM VID falls through to cert"
|
"unlocker without OEM VID falls through to cert"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
none_ran.load(Ordering::SeqCst),
|
none_ran.load(Ordering::SeqCst),
|
||||||
"read_vid() consulted even when it returns None"
|
"read_volume_id() consulted even when it returns None"
|
||||||
);
|
);
|
||||||
|
|
||||||
// No matching unlocker → Ok(None), nothing consulted.
|
// No matching unlocker → Ok(None), nothing consulted.
|
||||||
let got = unlocker_read_vid(&mut scsi, &fake_id("UNKNWNVD")).unwrap();
|
let got = unlocker_read_volume_id(&mut scsi, &fake_id("UNKNWNVD")).unwrap();
|
||||||
assert!(got.is_none(), "no match → cert fallback");
|
assert!(got.is_none(), "no match → cert fallback");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `unlocker_set_max_read_speed` consults the FIRST matching unlocker's
|
||||||
|
/// `set_max_read_speed`. A matching unlocker is invoked; a non-match is a
|
||||||
|
/// safe no-op (nothing invoked, `Ok(())`).
|
||||||
|
///
|
||||||
|
/// Distinct vendor ids keep this independent of the other registry tests
|
||||||
|
/// despite the process-wide shared registry.
|
||||||
|
#[test]
|
||||||
|
fn unlocker_set_max_read_speed_routes_match_else_noop() {
|
||||||
|
let mut scsi = NoopTransport;
|
||||||
|
|
||||||
|
let speed_ran = Arc::new(AtomicBool::new(false));
|
||||||
|
register_unlocker(Box::new(
|
||||||
|
FakeUnlocker::new("SPEEDVND", Arc::new(AtomicBool::new(false)))
|
||||||
|
.with_speed(speed_ran.clone()),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Matching identity → set_max_read_speed invoked.
|
||||||
|
unlocker_set_max_read_speed(&mut scsi, &fake_id("SPEEDVND")).unwrap();
|
||||||
|
assert!(
|
||||||
|
speed_ran.load(Ordering::SeqCst),
|
||||||
|
"set_max_read_speed() invoked on match"
|
||||||
|
);
|
||||||
|
|
||||||
|
// No matching unlocker → Ok(()), nothing invoked (safe no-op).
|
||||||
|
speed_ran.store(false, Ordering::SeqCst);
|
||||||
|
unlocker_set_max_read_speed(&mut scsi, &fake_id("NOSPEEDV")).unwrap();
|
||||||
|
assert!(
|
||||||
|
!speed_ran.load(Ordering::SeqCst),
|
||||||
|
"no match → safe no-op, nothing invoked"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user