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()."
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]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
+5 -5
View File
@@ -28,7 +28,7 @@ pub enum Error {
/// A SCSI command failed. `status == SCSI_STATUS_TRANSPORT_FAILURE` with
/// `sense: None` is a transport-layer fault; a CHECK CONDITION carries the
/// parsed [`ScsiSense`].
ScsiError {
Scsi {
/// CDB opcode that failed — diagnostic, carried for future logging.
opcode: u8,
status: u8,
@@ -54,14 +54,14 @@ impl Error {
Error::AacsVidRead => 7012,
Error::HandshakeRejected => 7013,
Error::VidUnavailable => 7014,
Error::ScsiError { .. } => 7099,
Error::Scsi { .. } => 7099,
}
}
/// The parsed sense for a CHECK CONDITION SCSI error, else `None`.
pub fn scsi_sense(&self) -> Option<ScsiSense> {
match self {
Error::ScsiError { sense, .. } => *sense,
Error::Scsi { sense, .. } => *sense,
_ => None,
}
}
@@ -70,7 +70,7 @@ impl Error {
pub fn is_scsi_transport_failure(&self) -> bool {
matches!(
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).
impl From<crate::scsi::ScsiError> for Error {
fn from(e: crate::scsi::ScsiError) -> Self {
Error::ScsiError {
Error::Scsi {
opcode: 0,
status: e.status,
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
// rejection — the operator needs to see the real (replug) cause, not
// be sent down a keydb/host-cert rabbit hole.
let transport = Error::ScsiError {
let transport = Error::Scsi {
opcode: 0xA3, // SEND KEY
status: SCSI_STATUS_TRANSPORT_FAILURE,
sense: None,
@@ -1363,7 +1363,7 @@ mod tests {
// A genuine SCSI rejection (CHECK CONDITION) IS the drive saying no, so
// it maps to the handshake-specific code as before.
let rejected = Error::ScsiError {
let rejected = Error::Scsi {
opcode: 0xA3,
status: SCSI_STATUS_CHECK_CONDITION,
sense: Some(crate::scsi::ScsiSense {
+4 -4
View File
@@ -16,7 +16,7 @@ pub enum Error {
SignatureMismatch { expected: [u8; 4], got: [u8; 4] },
/// A SCSI command failed. `status == SCSI_STATUS_TRANSPORT_FAILURE` with
/// `sense: None` is a transport-layer fault (bridge crash / disconnect).
ScsiError {
Scsi {
opcode: u8,
status: u8,
sense: Option<[u8; 32]>,
@@ -29,7 +29,7 @@ impl std::fmt::Display for Error {
Error::ProfileParse => write!(f, "drive profile parse error"),
Error::UnlockFailed => write!(f, "firmware unlock failed"),
Error::SignatureMismatch { .. } => write!(f, "signature mismatch"),
Error::ScsiError { opcode, status, .. } => {
Error::Scsi { opcode, status, .. } => {
write!(f, "SCSI error (opcode {opcode:#04x}, status {status:#04x})")
}
}
@@ -44,7 +44,7 @@ impl Error {
pub(crate) fn is_transport_failure(&self) -> bool {
matches!(
self,
Error::ScsiError { status, sense: None, .. }
Error::Scsi { status, sense: None, .. }
if *status == crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE
)
}
@@ -54,7 +54,7 @@ impl Error {
/// (the opcode is unknown at the transport level).
impl From<crate::scsi::ScsiError> for Error {
fn from(e: crate::scsi::ScsiError) -> Self {
Error::ScsiError {
Error::Scsi {
opcode: 0,
status: e.status,
sense: e.sense,
+40 -9
View File
@@ -6,6 +6,11 @@
//! encryption AT THE DRIVE (the unlocked drive serves clear content) and
//! 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 error;
mod platform;
@@ -15,25 +20,51 @@ use crate::ld::error::Result;
use crate::scsi::{DataDirection, ScsiTransport};
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,
/// runs the MediaTek MT1959 firmware-unlock (and disc-speed calibration)
/// handshake over the raw SCSI transport.
pub struct LibreDrive;
pub(crate) struct LibreDrive;
impl LibreDrive {
pub fn new() -> Self {
pub(crate) fn new() -> Self {
LibreDrive
}
}
impl Default for LibreDrive {
fn default() -> Self {
Self::new()
}
}
/// 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
/// 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 result = scsi.execute(&cdb, DataDirection::FromDevice, buf, 5_000)?;
if result.bytes_transferred != expected {
return Err(Error::ScsiError {
return Err(Error::Scsi {
opcode: SCSI_READ_BUFFER,
status: crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE,
sense: None,
@@ -217,7 +217,7 @@ impl Mt1959 {
return Ok(());
}
}
Err(Error::ScsiError {
Err(Error::Scsi {
opcode: SCSI_READ_BUFFER,
status: crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE,
sense: None,
@@ -327,7 +327,7 @@ impl Mt1959 {
)
.is_err()
{
return Err(Error::ScsiError {
return Err(Error::Scsi {
opcode: SCSI_READ_BUFFER,
status: crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE,
sense: None,
+32 -22
View File
@@ -3,9 +3,11 @@
use crate::ld::error::{Error, Result};
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)]
pub struct ProfilesFile {
pub struct Profiles {
#[serde(default)]
pub mt1959_a: Vec<DriveProfile>,
#[serde(default)]
@@ -14,6 +16,15 @@ pub struct ProfilesFile {
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.
#[derive(Debug, Clone, Deserialize)]
pub struct Identity {
@@ -235,12 +246,11 @@ where
const BUNDLED_PROFILES: &str = include_str!("profiles.json");
/// Parse the bundled profiles fresh into an owned [`ProfilesFile`].
///
/// Re-parses the embedded JSON (~800 KB) on every call; prefer
/// [`bundled`] for the hot path, which parses once and caches. This
/// owned form is kept for callers that need a mutable / independent copy.
pub fn load_bundled() -> Result<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
/// independent copies.
#[cfg(test)]
pub fn load_bundled() -> Result<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 —
/// the bundled blob is fixed at compile time, so the first successful call
/// guarantees all later calls succeed too).
pub fn bundled() -> Option<&'static ProfilesFile> {
pub fn bundled() -> Option<&'static Profiles> {
use std::sync::OnceLock;
static CACHE: OnceLock<Option<ProfilesFile>> = OnceLock::new();
static CACHE: OnceLock<Option<Profiles>> = OnceLock::new();
CACHE
.get_or_init(|| load_from_str(BUNDLED_PROFILES).ok())
.as_ref()
@@ -267,7 +277,7 @@ pub fn find_bundled(drive_id: &crate::DriveId) -> Option<ProfileMatch> {
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)
}
@@ -281,7 +291,7 @@ fn load_from_str(data: &str) -> Result<ProfilesFile> {
/// on file still match a same-model profile. All comparisons are
/// whitespace-trimmed. Returns the first section that yields a match.
pub fn find_by_drive_id(
profiles: &ProfilesFile,
profiles: &Profiles,
drive_id: &crate::DriveId,
) -> Option<ProfileMatch> {
let v = drive_id.vendor_id.trim();
@@ -374,11 +384,11 @@ mod tests {
fn bundled_is_cached_and_matches_fresh_parse() {
let cached = bundled().expect("bundled profiles parse");
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());
// Cached accessor returns a stable address across calls.
let a = bundled().unwrap() as *const ProfilesFile;
let b = bundled().unwrap() as *const ProfilesFile;
let a = bundled().unwrap() as *const Profiles;
let b = bundled().unwrap() as *const Profiles;
assert_eq!(a, b);
}
@@ -449,7 +459,7 @@ mod tests {
/// 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.
/// 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.
/// Mutation: doing only the loose pass would return the first entry regardless of date.
#[test]
@@ -483,7 +493,7 @@ mod tests {
]
})
.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.
let id_date1 = make_drive_id("TESTDRV ", "1.00", "XX00000", "200001010000");
@@ -528,7 +538,7 @@ mod tests {
]
})
.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.
// "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.
/// 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.
#[test]
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);
assert!(matches!(result, Err(Error::ProfileParse)));
}
@@ -586,7 +596,7 @@ mod tests {
]
})
.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
// All optional CDB fields must be None when absent from JSON.
assert!(
@@ -637,7 +647,7 @@ mod tests {
]
})
.to_string();
let profiles: ProfilesFile = serde_json::from_str(&json_str).unwrap();
let profiles: Profiles = serde_json::from_str(&json_str).unwrap();
assert_eq!(
profiles.mt1959_a[0].signature, [0u8; 4],
"empty signature must deserialise as [0;4]"
+13 -5
View File
@@ -14,7 +14,13 @@ pub mod scsi;
mod aacs;
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;
@@ -114,10 +120,12 @@ pub trait Unlocker: Send + Sync {
) -> std::result::Result<Unlocked, UnlockError>;
}
/// Name of the firmware unlocker that supports this drive (for drive-info "is
/// this drive supported?" display), or `None`. A pure profile lookup — does NOT
/// touch the drive or unlock anything.
pub fn firmware_unlocker_name(drive_id: &DriveId) -> Option<&'static str> {
/// Name of the unlocker that claims this drive by identity (for drive-info "is
/// this drive supported?" display), or `None`. A pure lookup — does NOT touch
/// the drive or unlock anything. Only the identity-keyed (drive-prep) unlocker
/// 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)
}
+4 -1
View File
@@ -66,7 +66,10 @@ impl ScsiSense {
/// SCSI status byte for a transport-layer failure (bridge crash / disconnect).
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;
// Common opcodes used by the unlocker modules.