From 25acd09504a325e042174a248fe712631b406d18 Mon Sep 17 00:00:00 2001
From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com>
Date: Mon, 22 Jun 2026 11:05:21 -0700
Subject: [PATCH] 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).
---
src/disc/encrypt.rs | 2 +-
src/drive/mod.rs | 13 ++++
src/unlock.rs | 175 ++++++++++++++++++++++++++++++++------------
3 files changed, 141 insertions(+), 49 deletions(-)
diff --git a/src/disc/encrypt.rs b/src/disc/encrypt.rs
index ab8dd4d..fda3fe6 100644
--- a/src/disc/encrypt.rs
+++ b/src/disc/encrypt.rs
@@ -76,7 +76,7 @@ impl Disc {
// DriveId first releases the immutable borrow before we hand the
// mutable transport to the registry.
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)) => {
tracing::debug!(
target: "freemkv::disc",
diff --git a/src/drive/mod.rs b/src/drive/mod.rs
index fb29613..96a6be7 100644
--- a/src/drive/mod.rs
+++ b/src/drive/mod.rs
@@ -399,6 +399,19 @@ impl Drive {
let r = match r {
Ok(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(())
}
// No unlocker matched: not an error — fall through to OEM route.
diff --git a/src/unlock.rs b/src/unlock.rs
index 098bcc5..7d25e12 100644
--- a/src/unlock.rs
+++ b/src/unlock.rs
@@ -4,8 +4,8 @@
//! supplied by an external crate (e.g. `freemkv-unlock-ld`) and registered
//! once at process start via [`register_unlocker`]. At drive-prep the
//! registry is walked in registration order; the first unlocker whose
-//! [`Unlocker::matches`] returns true is asked to [`Unlocker::unlock`] the
-//! drive by issuing its own CDBs through the raw [`ScsiTransport`].
+//! [`Unlocker::matches`] returns true is asked to [`Unlocker::unlock_drive`]
+//! the drive by issuing its own CDBs through the raw [`ScsiTransport`].
//!
//! No firmware blobs, no unlock CDBs, no drive profiles live here — only
//! 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 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
-/// 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.
pub trait Unlocker: Send + Sync {
/// 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.
fn matches(&self, id: &DriveId) -> bool;
- /// Unlock the drive. The unlocker issues its own CDBs through `scsi`.
- /// Returns `Ok(())` once the drive is prepared for reads.
- fn unlock(&self, scsi: &mut dyn ScsiTransport, id: &DriveId) -> Result<()>;
+ /// Put the drive into extended-access mode (firmware/bootloader/whatever THIS
+ /// unlocker needs). The one required capability.
+ fn unlock_drive(&self, scsi: &mut dyn ScsiTransport, id: &DriveId) -> Result<()>;
- /// Read the AACS Volume ID via this unlocker's OEM mechanism, if it
- /// has one.
- ///
- /// An [`Unlocker`] unlocks *drive functionality*, not just the disc:
- /// `unlock` is one capability, OEM VID retrieval is another. Once the
- /// matching unlocker is identified for a drive, libfreemkv uses it for
- /// BOTH unlock and VID. The OEM path returns the VID *without* the host
- /// 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