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
+25 -25
View File
@@ -164,11 +164,10 @@ impl crate::Unlocker for CssUnlocker {
"CSS"
}
fn matches(&self, ctx: &crate::UnlockCtx) -> bool {
ctx.kind == crate::DiscKind::Css
}
fn unlock(
/// CSS removes the scrambled-sector barrier (a bus-level concern); it
/// provides no drive features. Self-guards against the hardware (below), so
/// it declines cleanly when the consumer iterates it on a non-DVD.
fn unlock_bus(
&self,
scsi: &mut dyn ScsiTransport,
_ctx: &crate::UnlockCtx,
@@ -923,27 +922,28 @@ mod tests {
assert_eq!(cdb[9], 0x04, "low byte of 2052-byte transfer");
}
/// The CssUnlocker matches ONLY `DiscKind::Css` (never fires during
/// drive-prep or on a Blu-ray).
/// CssUnlocker provides bus removal only — it never provides drive features.
#[test]
fn css_unlocker_matches_only_css_kind() {
use crate::{DiscKind, DriveId, UnlockCtx, Unlocker};
let id = DriveId {
vendor_id: "FAKEVNDR".to_string(),
..Default::default()
};
let u = CssUnlocker::new();
assert!(
u.matches(&UnlockCtx::new(&id, DiscKind::Css, &[])),
"matches a CSS DVD"
);
for k in [DiscKind::Unknown, DiscKind::Unencrypted, DiscKind::Aacs] {
assert!(
!u.matches(&UnlockCtx::new(&id, k, &[])),
"CssUnlocker must not match {k:?}"
);
fn css_unlocker_provides_no_features() {
use crate::scsi::{DataDirection, ScsiResult};
use crate::{DiscKind, DriveId, UnlockCtx, UnlockError, Unlocker};
struct DeadTransport;
impl ScsiTransport for DeadTransport {
fn execute(
&mut self,
_cdb: &[u8],
_dir: DataDirection,
_data: &mut [u8],
_timeout_ms: u32,
) -> crate::scsi::Result<ScsiResult> {
panic!("unlock_features must not touch the transport");
}
}
let id = DriveId::default();
let mut t = DeadTransport;
let r =
CssUnlocker::new().unlock_features(&mut t, &UnlockCtx::new(&id, DiscKind::Css, &[]));
assert_eq!(r.unwrap_err(), UnlockError::NotApplicable);
}
/// Defense in depth: even when the caller declares `DiscKind::Css`, the
@@ -994,7 +994,7 @@ mod tests {
};
let mut t = BdTransport { non_config_cdbs: 0 };
let r = CssUnlocker::new().unlock(&mut t, &UnlockCtx::new(&id, DiscKind::Css, &[]));
let r = CssUnlocker::new().unlock_bus(&mut t, &UnlockCtx::new(&id, DiscKind::Css, &[]));
assert_eq!(
r.unwrap_err(),
UnlockError::NotApplicable,