unlock: split Unlocker into unlock_features/unlock_bus; add Renesas unlocker

Replace the single matches()+unlock() contract with two capability methods — unlock_features (drive riplock/speed/OEM VID at drive-prep) and unlock_bus (AACS/CSS bus-encryption removal for the mounted disc) — each defaulting to NotApplicable so an unlocker implements only what it does.

Add the renesis module: the Renesas-platform unlocker (Pioneer + HL-DT-ST Renesas), detected via the READ_BUFFER 0x02/0xF1 identity probe (ASCII "SAT" marker). Features only; the cert handles the bus.

Add product_id to DriveId. Bump to 1.2.3.
This commit is contained in:
Matthew Jackson
2026-07-07 16:35:57 -07:00
parent a96b675e7a
commit d07d8f0dac
9 changed files with 630 additions and 110 deletions
+44 -27
View File
@@ -116,26 +116,17 @@ impl LibreDrive {
}
}
impl Unlocker for LibreDrive {
/// Firmware unlock is a DRIVE-PREP concern: it runs before the disc kind is
/// probed (`kind == Unknown`) and keys off the drive identity. It must NOT
/// fire during the later content-keyed dispatch (Aacs/Css), or a DVD/Blu-ray
/// in a profiled drive would be re-firmware-unlocked.
fn name(&self) -> &'static str {
"LibreDrive"
}
fn matches(&self, ctx: &UnlockCtx) -> bool {
ctx.kind == crate::DiscKind::Unknown && profile::find_bundled(ctx.drive_id).is_some()
}
/// Firmware-unlock the drive and report its OEM Volume ID. The unlocked drive
/// serves CLEAR content, so `drive_unlocked: true` and there is no bus key.
impl LibreDrive {
/// The MediaTek firmware unlock. Because LibreDrive removes AACS bus
/// encryption AT THE DRIVE (the unlocked drive serves CLEAR content), this ONE
/// operation satisfies BOTH the drive-features and the bus-removal capability
/// — so `unlock_features` and `unlock_bus` both delegate here. The result
/// carries `drive_unlocked: true` (no bus key needed) and the OEM Volume ID.
///
/// A no-firmware-route drive (Renesas) returns `NotApplicable` (fall through);
/// a transport fault propagates as `Transport`; a firmware failure that isn't
/// a dead bus also falls through (`NotApplicable`).
fn unlock(
/// A no-firmware-route drive (Renesas / no profile) returns `NotApplicable`; a
/// transport fault propagates as `Transport`; any other firmware failure also
/// falls through as `NotApplicable`.
fn firmware_unlock(
&self,
scsi: &mut dyn ScsiTransport,
ctx: &UnlockCtx,
@@ -145,19 +136,17 @@ impl Unlocker for LibreDrive {
return Err(UnlockError::NotApplicable);
};
if matches!(m.platform, profile::Platform::Renesas) {
// Renesas firmware unlock is not implemented — fall through to cert.
// Renesas is a different platform (handled by the Renesas unlocker).
return Err(UnlockError::NotApplicable);
}
let is_variant_b = matches!(m.platform, profile::Platform::Mt1959B);
use platform::PlatformDriver;
let mut mt = platform::mt1959::Mt1959::new(m.profile, is_variant_b);
// Firmware unlock. A transport fault → UnlockError::Transport; any other
// firmware failure → NotApplicable (via From<error::Error>).
// A transport fault → UnlockError::Transport; any other firmware failure
// → NotApplicable (via From<error::Error>).
mt.init(scsi)?;
// Prime the per-region speed table (best-effort — must not fail the unlock).
let _ = mt.probe_disc(scsi);
// The unlocked drive hands back its Volume ID; firmware serves clear
// content, so no bus key and drive_unlocked = true.
let vid = self.read_oem_vid(scsi, id)?;
Ok(Unlocked {
vid,
@@ -167,6 +156,33 @@ impl Unlocker for LibreDrive {
}
}
impl Unlocker for LibreDrive {
fn name(&self) -> &'static str {
"LibreDrive"
}
/// LibreDrive provides drive features (riplock/speed, OEM VID) — and, because
/// its firmware unlock serves clear content, bus removal comes free with it.
fn unlock_features(
&self,
scsi: &mut dyn ScsiTransport,
ctx: &UnlockCtx,
) -> std::result::Result<Unlocked, UnlockError> {
self.firmware_unlock(scsi, ctx)
}
/// Same firmware code as [`unlock_features`]: LibreDrive removes the bus at
/// the drive. In practice the consumer skips this because drive-prep already
/// set `drive_unlocked`; it's here for completeness / a bus-first call order.
fn unlock_bus(
&self,
scsi: &mut dyn ScsiTransport,
ctx: &UnlockCtx,
) -> std::result::Result<Unlocked, UnlockError> {
self.firmware_unlock(scsi, ctx)
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -212,6 +228,7 @@ mod tests {
fn make_drive_id(vendor: &str, rev: &str, vs: &str, date: &str) -> DriveId {
DriveId {
vendor_id: vendor.to_string(),
product_id: String::new(),
product_revision: rev.to_string(),
vendor_specific: vs.to_string(),
firmware_date: date.to_string(),
@@ -283,8 +300,8 @@ mod tests {
assert_eq!(got, None);
}
/// `unlock` on a drive with no matching profile → `NotApplicable` (fall
/// through), short-circuiting before any firmware handshake.
/// `unlock_features` on a drive with no matching profile → `NotApplicable`
/// (fall through), short-circuiting before any firmware handshake.
#[test]
fn unlock_no_profile_is_not_applicable() {
let mut t = FakeTransport {
@@ -292,7 +309,7 @@ mod tests {
bytes_transferred: 36,
};
let err = LibreDrive::new()
.unlock(
.unlock_features(
&mut t,
&ctx(&make_drive_id("FAKE-VND", "9.99", "XX12345", "")),
)
+1
View File
@@ -487,6 +487,7 @@ mod tests {
DriveProfile {
identity: Identity {
vendor_id: "TEST".into(),
product_id: String::new(),
product_revision: String::new(),
vendor_specific: String::new(),
firmware_date: String::new(),
+69 -31
View File
@@ -31,6 +31,8 @@ pub struct Identity {
#[serde(default)]
pub vendor_id: String,
#[serde(default)]
pub product_id: String,
#[serde(default)]
pub product_revision: String,
#[serde(default)]
pub vendor_specific: String,
@@ -283,15 +285,16 @@ fn load_from_str(data: &str) -> Result<Profiles> {
/// Find a profile matching a drive's INQUIRY fields.
///
/// Two-pass per platform section (MT1959-A, then MT1959-B, then Renesas):
/// first an exact match including `firmware_date`, then — if none — a
/// looser match on vendor / revision / vendor-specific only. The exact
/// pass wins so a drive with a known firmware date binds to its precise
/// profile; the looser pass lets a drive whose firmware date we don't have
/// on file still match a same-model profile. All comparisons are
/// whitespace-trimmed. Returns the first section that yields a match.
/// Per platform section (MT1959-A, then MT1959-B, then Renesas), tried in
/// order of decreasing specificity: full match including `product_id` and
/// `firmware_date` (skipped when the drive reports no product), then the same
/// without `product_id`. Both require `firmware_date`; every profile carries a
/// unique identity under these fields, so a match is exact — no looser
/// vendor/revision fallback that could pick the wrong same-model variant. All
/// comparisons are whitespace-trimmed. Returns the first section that matches.
pub fn find_by_drive_id(profiles: &Profiles, drive_id: &crate::DriveId) -> Option<ProfileMatch> {
let v = drive_id.vendor_id.trim();
let prod = drive_id.product_id.trim();
let r = drive_id.product_revision.trim();
let vs = drive_id.vendor_specific.trim();
let date = drive_id.firmware_date.trim();
@@ -301,22 +304,26 @@ pub fn find_by_drive_id(profiles: &Profiles, drive_id: &crate::DriveId) -> Optio
(Platform::Mt1959B, &profiles.mt1959_b),
(Platform::Renesas, &profiles.renesas),
] {
if let Some(p) = list.iter().find(|p| {
p.identity.vendor_id.trim() == v
&& p.identity.product_revision.trim() == r
&& p.identity.vendor_specific.trim() == vs
&& p.identity.firmware_date.trim() == date
}) {
return Some(ProfileMatch {
profile: p.clone(),
platform,
});
if !prod.is_empty() {
if let Some(p) = list.iter().find(|p| {
p.identity.vendor_id.trim() == v
&& p.identity.product_id.trim() == prod
&& p.identity.product_revision.trim() == r
&& p.identity.vendor_specific.trim() == vs
&& p.identity.firmware_date.trim() == date
}) {
return Some(ProfileMatch {
profile: p.clone(),
platform,
});
}
}
if let Some(p) = list.iter().find(|p| {
p.identity.vendor_id.trim() == v
&& p.identity.product_revision.trim() == r
&& p.identity.vendor_specific.trim() == vs
&& p.identity.firmware_date.trim() == date
}) {
return Some(ProfileMatch {
profile: p.clone(),
@@ -336,12 +343,51 @@ mod tests {
fn make_drive_id(vendor: &str, rev: &str, vs: &str, date: &str) -> DriveId {
DriveId {
vendor_id: vendor.to_string(),
product_id: String::new(),
product_revision: rev.to_string(),
vendor_specific: vs.to_string(),
firmware_date: date.to_string(),
}
}
/// When two profiles share vendor/rev/vs/date and differ only in product_id,
/// the full pass binds to the one whose product matches; the looser passes
/// would return the first regardless.
#[test]
fn find_by_drive_id_product_id_breaks_a_tie() {
use serde_json::json;
let profiles: Profiles = serde_json::from_str(
&json!({
"mt1959_a": [
{"identity": {"vendor_id":"TIE","product_id":"MODEL-A",
"product_revision":"1.00","vendor_specific":"XX00000",
"firmware_date":"200001010000"},
"signature":"aaaaaaaa","firmware":""},
{"identity": {"vendor_id":"TIE","product_id":"MODEL-B",
"product_revision":"1.00","vendor_specific":"XX00000",
"firmware_date":"200001010000"},
"signature":"bbbbbbbb","firmware":""}
]
})
.to_string(),
)
.unwrap();
let mut id = make_drive_id("TIE", "1.00", "XX00000", "200001010000");
id.product_id = "MODEL-B".to_string();
let m = find_by_drive_id(&profiles, &id).unwrap();
assert_eq!(
m.profile.signature,
[0xbb, 0xbb, 0xbb, 0xbb],
"product_id must select MODEL-B over the first entry"
);
// No product_id → falls back to the 4-field pass → first entry.
let id0 = make_drive_id("TIE", "1.00", "XX00000", "200001010000");
let m0 = find_by_drive_id(&profiles, &id0).unwrap();
assert_eq!(m0.profile.signature, [0xaa, 0xaa, 0xaa, 0xaa]);
}
#[test]
fn test_find_known_drive() {
let profiles = load_bundled().unwrap();
@@ -512,14 +558,12 @@ mod tests {
);
}
/// find_by_drive_id: loose match (no date) still works when an entry has
/// the same vendor/revision/vs but an unknown firmware_date.
/// Mutation: making the loose pass require a date match means "no date" drives
/// always return None even though a same-model profile exists.
/// find_by_drive_id: a drive whose firmware_date does NOT match any profile
/// gets no match — there is no loose vendor/rev/vs fallback that could bind
/// the wrong same-model variant.
#[test]
fn find_by_drive_id_loose_match_when_date_unknown() {
fn find_by_drive_id_no_match_when_date_differs() {
use serde_json::json;
// "LOOSEDR " fills 8 bytes; trim() → "LOOSEDR".
let profiles_json = json!({
"mt1959_a": [
{
@@ -537,15 +581,9 @@ mod tests {
.to_string();
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.
// Same vendor/rev/vs but a different date — must NOT match.
let id = make_drive_id("LOOSEDR ", "2.00", "YY11111", "000000000000");
let m = find_by_drive_id(&profiles, &id).unwrap();
assert_eq!(
m.profile.signature,
[0xde, 0xad, 0xbe, 0xef],
"loose match must bind the same-model profile when date differs"
);
assert!(find_by_drive_id(&profiles, &id).is_none());
}
/// load_from_str (via load_bundled) returns ProfileParse on invalid JSON.
+207 -1
View File
File diff suppressed because it is too large Load Diff