Extract drive unlock behind pluggable Unlocker seam

libfreemkv must stay firmware-clean for crates.io. Move ALL drive-unlock
knowledge — firmware blobs, WRITE_BUFFER/MODE SELECT upload, unlock CDBs,
the MT1959 variant-A/B handshake, the 800 KB profiles.json database, and
the DriveProfile parsing — out into the freemkv-unlock-ld crate.

libfreemkv now keeps only the seam:
  - Unlocker trait (name/matches/unlock) + a process-wide ordered registry
    (register_unlocker / route_unlock) in src/unlock.rs
  - Drive::init() walks the registry; the first unlocker whose matches(id)
    is true runs unlock(scsi, id); if none match the drive is left in
    stock mode and the host-cert AACS handshake (the OEM route) carries
    the disc.

The unlocker issues its own CDBs through the public ScsiTransport::execute,
so libfreemkv knows nothing about how unlocking happens.

Removed:
  - profiles.json
  - src/platform/mt1959/{mod,variant_a,variant_b}.rs
  - src/profile.rs (DriveProfile, ProfilesFile, find_by_drive_id, ...)
  - the PlatformDriver trait

Because the Unlocker seam reports only success/failure (no extended-access
marker), VID acquisition is now always via the cert-based handshake; the
per-drive OEM-VID-CDB shortcut and Drive::is_unlocked() (now const false)
are removed/neutralized. Disc-speed calibration moved into the unlocker's
unlock(); Drive::probe_disc() is a no-op.

git grep over src/ is firmware-blob/profiles/WRITE_BUFFER/mt1959-free.
All tests pass on Rust 1.86 (precommit green).
This commit is contained in:
Matthew Jackson
2026-06-22 10:31:51 -07:00
parent f74979bdb4
commit 6dc62bcd84
11 changed files with 291 additions and 5978 deletions
+78 -99
View File
@@ -1,8 +1,8 @@
//! Drive session — open, identify, and read from optical drives.
//!
//! A `Drive` is opened from a device path, identifies itself via INQUIRY,
//! optionally unlocks/initializes via a platform driver, and reads sectors.
//! `probe_disc()` primes the firmware's per-region speed table.
//! optionally unlocks/initializes via a registered [`crate::unlock::Unlocker`],
//! and reads sectors.
pub(crate) fn extract_scsi_context(e: &Error) -> (u8, Option<crate::scsi::ScsiSense>) {
match e {
@@ -27,9 +27,6 @@ pub(crate) mod windows;
use crate::error::{Error, Result};
use crate::event::Event;
use crate::identity::DriveId;
use crate::platform::PlatformDriver;
use crate::platform::mt1959::Mt1959;
use crate::profile::{self, DriveProfile};
use crate::scsi::ScsiTransport;
use crate::sector::SectorSource;
use std::path::Path;
@@ -62,9 +59,15 @@ const SCSI_REPORT_KEY: u8 = 0xA4;
/// Optical disc drive session -- open, identify, unlock, and read.
pub struct Drive {
scsi: Box<dyn ScsiTransport>,
driver: Option<Box<dyn PlatformDriver>>,
pub profile: Option<DriveProfile>,
pub platform: Option<profile::Platform>,
/// Name of the [`crate::unlock::Unlocker`] that handled this drive at
/// `init()`, if any matched. `None` means no unlocker matched and the
/// drive runs in stock mode (host-cert AACS handshake carries discs).
unlocker_name: Option<String>,
/// True once `init()` has run (whether or not an unlocker matched).
init_ran: bool,
/// Lazily-computed registry-match name for `platform_name()`'s `&str`
/// return before `init()` has run.
matched_name_cache: std::sync::OnceLock<String>,
pub drive_id: DriveId,
device_path: String,
/// Halt flag — when set, Drive::read() bails at the next check point.
@@ -88,7 +91,6 @@ impl Drive {
let t0 = std::time::Instant::now();
tracing::info!(target: "freemkv::drive", phase = "open", device = %device.display(), "begin");
let mut transport = crate::scsi::open(device)?;
let profiles = profile::load_bundled()?;
let drive_id = DriveId::from_drive(transport.as_mut())?;
tracing::info!(
target: "freemkv::drive",
@@ -100,24 +102,14 @@ impl Drive {
"end"
);
let m = profile::find_by_drive_id(&profiles, &drive_id);
let (driver, platform, profile) = match m {
Some(m) => (
create_driver(m.platform, &m.profile).ok(),
Some(m.platform),
Some(m.profile),
),
None => (None, None, None),
};
#[cfg(target_os = "linux")]
let block_dev_fd = open_block_device_for_sg(device);
Ok(Drive {
scsi: transport,
driver,
platform,
profile,
unlocker_name: None,
init_ran: false,
matched_name_cache: std::sync::OnceLock::new(),
drive_id,
device_path: device.to_string_lossy().to_string(),
halt: Arc::new(AtomicBool::new(false)),
@@ -135,9 +127,9 @@ impl Drive {
fn from_transport_for_test(scsi: Box<dyn ScsiTransport>) -> Self {
Drive {
scsi,
driver: None,
profile: None,
platform: None,
unlocker_name: None,
init_ran: false,
matched_name_cache: std::sync::OnceLock::new(),
drive_id: DriveId {
vendor_id: String::new(),
product_id: String::new(),
@@ -212,16 +204,11 @@ impl Drive {
self.unlock_tray();
}
/// Whether this drive has a known profile (unlock parameters available).
/// Whether a registered unlocker matches this drive (i.e. it can be
/// firmware-unlocked). Queried against the unlock registry by identity;
/// does not require `init()` to have run.
pub fn has_profile(&self) -> bool {
self.profile.is_some()
}
/// Borrow the matched drive profile, if any. Used by callers that
/// need to issue per-drive OEM CDB templates (e.g. the OEM VID
/// retrieval path in `disc::encrypt`).
pub fn drive_profile(&self) -> Option<&DriveProfile> {
self.profile.as_ref()
crate::unlock::matching_name(&self.drive_id).is_some()
}
/// Access the SCSI transport for direct commands (used by CSS/AACS auth).
@@ -330,11 +317,17 @@ impl Drive {
}
}
/// Name of the unlocker handling this drive. After `init()` this is the
/// unlocker that ran; before `init()` it reflects the registry match by
/// identity. `"Unknown"` when no unlocker matches.
pub fn platform_name(&self) -> &str {
match self.platform {
Some(ref p) => p.name(),
None => "Unknown",
if let Some(ref n) = self.unlocker_name {
return n;
}
// Cache the registry match so we can hand out a `&str` borrow.
self.matched_name_cache.get_or_init(|| {
crate::unlock::matching_name(&self.drive_id).unwrap_or_else(|| "Unknown".to_string())
})
}
pub fn device_path(&self) -> &str {
@@ -395,20 +388,28 @@ impl Drive {
tracing::info!(target: "freemkv::drive", phase = "init", "begin");
if self.disc_is_dvd() {
tracing::info!(target: "freemkv::drive", phase = "init", dvd = true, elapsed_ms = t0.elapsed().as_millis() as u64, "end (stock-mode DVD, no unlock)");
self.init_ran = true;
return Ok(());
}
let r = match self.driver {
Some(ref mut d) => d.init(self.scsi.as_mut()),
None => Err(Error::UnsupportedDrive {
vendor_id: self.drive_id.vendor_id.trim().to_string(),
product_id: self.drive_id.product_id.trim().to_string(),
product_revision: self.drive_id.product_revision.trim().to_string(),
}),
// Walk the unlock registry: the first unlocker whose identity
// matches runs; none matching leaves the drive in stock mode so the
// host-cert AACS handshake (the OEM route) carries the disc.
let r = crate::unlock::route_unlock(self.scsi.as_mut(), &self.drive_id);
self.init_ran = true;
let r = match r {
Ok(Some(name)) => {
self.unlocker_name = Some(name);
Ok(())
}
// No unlocker matched: not an error — fall through to OEM route.
Ok(None) => Ok(()),
Err(e) => Err(e),
};
tracing::info!(
target: "freemkv::drive",
phase = "init",
ok = r.is_ok(),
unlocker = self.unlocker_name.as_deref().unwrap_or("none"),
elapsed_ms = t0.elapsed().as_millis() as u64,
"end"
);
@@ -427,22 +428,15 @@ impl Drive {
tracing::info!(target: "freemkv::drive", phase = "probe_disc", dvd = true, elapsed_ms = t0.elapsed().as_millis() as u64, "end (stock-mode DVD, no calibration)");
return Ok(());
}
let r = match self.driver {
Some(ref mut d) => d.probe_disc(self.scsi.as_mut()),
None => Err(Error::UnsupportedDrive {
vendor_id: self.drive_id.vendor_id.trim().to_string(),
product_id: self.drive_id.product_id.trim().to_string(),
product_revision: self.drive_id.product_revision.trim().to_string(),
}),
};
// Disc-speed calibration is firmware-specific and now lives inside
// the unlocker's `unlock()` (run at `init()`). Nothing to do here.
tracing::info!(
target: "freemkv::drive",
phase = "probe_disc",
ok = r.is_ok(),
elapsed_ms = t0.elapsed().as_millis() as u64,
"end"
"end (calibration handled by unlocker at init)"
);
r
Ok(())
}
/// Query a specific GET CONFIGURATION feature by code.
@@ -573,31 +567,21 @@ impl Drive {
}
pub fn is_ready(&self) -> bool {
match self.driver {
Some(ref d) => d.is_ready(),
None => false,
}
// Ready once init() has run and an unlocker handled the drive.
self.init_ran && self.unlocker_name.is_some()
}
/// True if the drive is currently in the extended-access state.
/// Whether libfreemkv should take the OEM extended-access read path.
///
/// Detected by the platform driver during `init()` from the unlock
/// response's mode markers. When true:
/// - SCSI READ_10 returns plaintext sectors (no AACS bus
/// encryption applied)
/// - VID retrieval works via the per-drive OEM CDB in
/// [`DriveProfile`] without the cert-based AACS handshake
/// - Disc-side Host Revocation List enforcement is effectively
/// bypassed by the alternate data path
///
/// AACS layer code branches on this: if true, issue the OEM
/// `read_vid_cdb` to retrieve VID directly; if false, fall back
/// to the cert-based mutual-auth handshake.
/// The pluggable [`crate::unlock::Unlocker`] seam reports only
/// success/failure from `unlock()` — it carries no extended-access
/// marker back into libfreemkv. With no marker channel, libfreemkv
/// always uses the standard host-certificate AACS handshake to acquire
/// the Volume ID (the OEM route), so this is always `false`. A firmware
/// unlocker still removes riplock / enables BD-UHD reads at `init()`;
/// VID acquisition just stays on the cert path.
pub fn is_unlocked(&self) -> bool {
match self.driver {
Some(ref d) => d.is_unlocked(),
None => false,
}
false
}
/// Read sectors from the disc. Single-shot — no inline retries, no
@@ -999,19 +983,6 @@ pub(crate) fn resolve_device(path: &str) -> Result<(String, DeviceResolution)> {
}
}
fn create_driver(
platform: profile::Platform,
profile: &DriveProfile,
) -> Result<Box<dyn PlatformDriver>> {
match platform {
profile::Platform::Mt1959A => Ok(Box::new(Mt1959::new(profile.clone(), false))),
profile::Platform::Mt1959B => Ok(Box::new(Mt1959::new(profile.clone(), true))),
profile::Platform::Renesas => Err(Error::PlatformNotImplemented {
platform: "renesas".to_string(),
}),
}
}
#[cfg(test)]
mod halt_tests {
use super::*;
@@ -1533,23 +1504,31 @@ mod command_tests {
assert_eq!(d.read_buffer(0x02, 0xF1, 16), None);
}
// ── No-driver paths: init/probe surface UnsupportedDrive ────────
// ── No-unlocker paths: init/probe succeed (OEM fallback) ────────
#[test]
fn init_without_driver_is_unsupported_drive() {
// from_transport_for_test has no platform driver; init() must
// return UnsupportedDrive, not panic or silently succeed.
fn init_without_unlocker_is_ok_oem_fallback() {
// The test transport's identity matches no registered unlocker, so
// route_unlock returns None. init() must succeed (leaving the drive
// in stock mode for the host-cert handshake), not error — the OEM
// route is the no-match fallback, not a failure.
let mut d = drive_with(vec![]);
assert!(matches!(d.init(), Err(Error::UnsupportedDrive { .. })));
assert!(
d.init().is_ok(),
"no-match init must succeed (OEM fallback)"
);
assert!(
!d.is_ready(),
"no unlocker ran → not in unlocked-ready state"
);
}
#[test]
fn probe_disc_without_driver_is_unsupported_drive() {
fn probe_disc_without_unlocker_is_ok_noop() {
// Disc-speed calibration moved into the unlocker (run at init).
// With no unlocker, probe_disc is a successful no-op.
let mut d = drive_with(vec![]);
assert!(matches!(
d.probe_disc(),
Err(Error::UnsupportedDrive { .. })
));
assert!(d.probe_disc().is_ok());
}
// ── decode_read_capacity additional boundaries ──────────────────