Revert "unlock: drive-features capability (stock riplock lift for any disc)"
This reverts commit 00e56dbb7d.
This commit is contained in:
@@ -125,33 +125,6 @@ impl Unlocker for LibreDrive {
|
|||||||
ctx.kind == crate::DiscKind::Unknown && profile::find_bundled(ctx.drive_id).is_some()
|
ctx.kind == crate::DiscKind::Unknown && profile::find_bundled(ctx.drive_id).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lift riplock on a profiled drive with STOCK MMC commands — no firmware
|
|
||||||
/// unlock, so it is safe for ANY disc including a CSS DVD (which must stay in
|
|
||||||
/// stock mode). We issue SET STREAMING (0xB6) — the modern command a slot-
|
|
||||||
/// loading BD combo honors when it ignores the legacy SET CD SPEED — and then
|
|
||||||
/// SET CD SPEED (0xBB) as a fallback for drives that only take the old one.
|
|
||||||
/// Self-gating: no bundled profile → no-op. Best-effort throughout: a drive
|
|
||||||
/// that rejects either command still rips, just slower, so every error is
|
|
||||||
/// swallowed (including a transport fault — the real reads that follow will
|
|
||||||
/// surface a dead bus).
|
|
||||||
fn apply_drive_features(
|
|
||||||
&self,
|
|
||||||
scsi: &mut dyn ScsiTransport,
|
|
||||||
ctx: &UnlockCtx,
|
|
||||||
) -> std::result::Result<(), UnlockError> {
|
|
||||||
if profile::find_bundled(ctx.drive_id).is_none() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
// SET STREAMING (max): CDB + 28-byte performance descriptor data-out.
|
|
||||||
let (cdb, mut descriptor) = crate::scsi::build_set_streaming(0xFFFF_FFFF);
|
|
||||||
let _ = scsi.execute(&cdb, DataDirection::ToDevice, &mut descriptor, 5_000);
|
|
||||||
// SET CD SPEED (max): no data-out.
|
|
||||||
let cdb = crate::scsi::build_set_cd_speed(0xFFFF);
|
|
||||||
let mut empty = [0u8; 0];
|
|
||||||
let _ = scsi.execute(&cdb, DataDirection::None, &mut empty, 5_000);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Firmware-unlock the drive and report its OEM Volume ID. The unlocked drive
|
/// 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.
|
/// serves CLEAR content, so `drive_unlocked: true` and there is no bus key.
|
||||||
///
|
///
|
||||||
@@ -322,60 +295,4 @@ mod tests {
|
|||||||
.expect_err("no profile → NotApplicable");
|
.expect_err("no profile → NotApplicable");
|
||||||
assert_eq!(err, UnlockError::NotApplicable);
|
assert_eq!(err, UnlockError::NotApplicable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A transport that records the opcode of every CDB it is handed.
|
|
||||||
struct RecordingTransport {
|
|
||||||
opcodes: Vec<u8>,
|
|
||||||
}
|
|
||||||
impl ScsiTransport for RecordingTransport {
|
|
||||||
fn execute(
|
|
||||||
&mut self,
|
|
||||||
cdb: &[u8],
|
|
||||||
_dir: DataDirection,
|
|
||||||
_data: &mut [u8],
|
|
||||||
_timeout_ms: u32,
|
|
||||||
) -> crate::scsi::Result<ScsiResult> {
|
|
||||||
self.opcodes.push(cdb[0]);
|
|
||||||
Ok(ScsiResult {
|
|
||||||
status: 0,
|
|
||||||
bytes_transferred: 0,
|
|
||||||
sense: [0u8; 32],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A profiled drive gets the stock speed commands (SET STREAMING first, then
|
|
||||||
/// SET CD SPEED) — the riplock lift, for any disc.
|
|
||||||
#[test]
|
|
||||||
fn drive_features_issues_stock_speed_commands_on_profiled_drive() {
|
|
||||||
let mut t = RecordingTransport { opcodes: vec![] };
|
|
||||||
LibreDrive::new()
|
|
||||||
.apply_drive_features(&mut t, &ctx(&known_vid_drive_id()))
|
|
||||||
.expect("best-effort ok");
|
|
||||||
assert!(
|
|
||||||
t.opcodes.contains(&crate::scsi::SCSI_SET_STREAMING),
|
|
||||||
"SET STREAMING (0xB6) issued"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
t.opcodes.contains(&crate::scsi::SCSI_SET_CD_SPEED),
|
|
||||||
"SET CD SPEED (0xBB) issued"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An unrecognised drive: drive-features is a no-op (self-gating), so the
|
|
||||||
/// consumer can call it on every unlocker without side effects.
|
|
||||||
#[test]
|
|
||||||
fn drive_features_is_noop_on_unprofiled_drive() {
|
|
||||||
let mut t = RecordingTransport { opcodes: vec![] };
|
|
||||||
LibreDrive::new()
|
|
||||||
.apply_drive_features(
|
|
||||||
&mut t,
|
|
||||||
&ctx(&make_drive_id("FAKE-VND", "9.99", "XX12345", "")),
|
|
||||||
)
|
|
||||||
.expect("best-effort ok");
|
|
||||||
assert!(
|
|
||||||
t.opcodes.is_empty(),
|
|
||||||
"no commands issued for an unrecognised drive"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-30
@@ -102,39 +102,16 @@ pub enum UnlockError {
|
|||||||
Transport,
|
Transport,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An unlocker provides drive/disc capabilities: **drive features** (speed /
|
/// An unlocker removes a drive-level bus-encryption barrier. Implementors are
|
||||||
/// riplock lift — a property of the DRIVE, applied for any disc) and **bus
|
/// the self-contained modules in this crate; the consumer only ever sees the
|
||||||
/// removal** (AACS bus-decrypt + VID, or a CSS handshake — gated on the disc).
|
/// trait, via [`all_unlockers`]. (Each module owns its own conversion from its
|
||||||
/// Implementors are the self-contained modules in this crate; the consumer only
|
/// internal error to [`UnlockError`].)
|
||||||
/// ever sees the trait, via [`all_unlockers`]. (Each module owns its own
|
|
||||||
/// conversion from its internal error to [`UnlockError`].)
|
|
||||||
///
|
///
|
||||||
/// The two capabilities are independent so a disc can take one without the
|
/// NOTE: drive tuning (e.g. SET CD SPEED to lift riplock) is deliberately NOT
|
||||||
/// other. A CSS DVD, for example, wants a matched drive's [`apply_drive_features`]
|
/// here — that is the consumer's concern, not bus removal.
|
||||||
/// (speed) but must NOT take its firmware bus-unlock, which would break stock CSS
|
|
||||||
/// auth. Keeping them separate lets the consumer apply only what the current
|
|
||||||
/// (drive, disc) context needs.
|
|
||||||
///
|
|
||||||
/// [`apply_drive_features`]: Unlocker::apply_drive_features
|
|
||||||
pub trait Unlocker: Send + Sync {
|
pub trait Unlocker: Send + Sync {
|
||||||
/// True if this unlocker's bus-removal [`unlock`](Unlocker::unlock) applies to
|
/// True if this unlocker applies to the given context (drive id + disc kind).
|
||||||
/// the given context (drive id + disc kind).
|
|
||||||
fn matches(&self, ctx: &UnlockCtx) -> bool;
|
fn matches(&self, ctx: &UnlockCtx) -> bool;
|
||||||
|
|
||||||
/// Apply drive-level feature tuning (max read speed / riplock lift) that this
|
|
||||||
/// unlocker enables purely by virtue of the DRIVE — **safe for any disc,
|
|
||||||
/// touches no bus encryption**. Self-gating: an unlocker that doesn't
|
|
||||||
/// recognise the drive returns `Ok(())` (a no-op), so the consumer can call
|
|
||||||
/// this on every unlocker regardless of disc kind. Best-effort: a rejected
|
|
||||||
/// command must NOT fail the rip (a slow drive still rips). Default: no-op.
|
|
||||||
fn apply_drive_features(
|
|
||||||
&self,
|
|
||||||
_scsi: &mut dyn ScsiTransport,
|
|
||||||
_ctx: &UnlockCtx,
|
|
||||||
) -> std::result::Result<(), UnlockError> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove the bus-encryption barrier, returning what was learned.
|
/// Remove the bus-encryption barrier, returning what was learned.
|
||||||
fn unlock(
|
fn unlock(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
-81
@@ -74,7 +74,6 @@ pub(crate) const SCSI_STATUS_CHECK_CONDITION: u8 = 0x02;
|
|||||||
|
|
||||||
// Common opcodes used by the unlocker modules.
|
// Common opcodes used by the unlocker modules.
|
||||||
pub(crate) const SCSI_SET_CD_SPEED: u8 = 0xBB;
|
pub(crate) const SCSI_SET_CD_SPEED: u8 = 0xBB;
|
||||||
pub(crate) const SCSI_SET_STREAMING: u8 = 0xB6;
|
|
||||||
pub(crate) const SCSI_SEND_KEY: u8 = 0xA3;
|
pub(crate) const SCSI_SEND_KEY: u8 = 0xA3;
|
||||||
pub(crate) const SCSI_REPORT_KEY: u8 = 0xA4;
|
pub(crate) const SCSI_REPORT_KEY: u8 = 0xA4;
|
||||||
pub(crate) const SCSI_READ_DISC_STRUCTURE: u8 = 0xAD;
|
pub(crate) const SCSI_READ_DISC_STRUCTURE: u8 = 0xAD;
|
||||||
@@ -99,83 +98,3 @@ pub(crate) fn build_set_cd_speed(read_speed: u16) -> [u8; 12] {
|
|||||||
0x00,
|
0x00,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Length of a SET STREAMING Performance Descriptor (MMC-6 §6.42).
|
|
||||||
pub(crate) const SET_STREAMING_DESCRIPTOR_LEN: usize = 28;
|
|
||||||
|
|
||||||
/// Build a SET STREAMING (0xB6) CDB + its 28-byte Performance Descriptor
|
|
||||||
/// requesting maximum read performance across the whole disc. This is the modern
|
|
||||||
/// riplock lift: many drives (notably slot-loading BD combos over a USB bridge)
|
|
||||||
/// ignore the legacy SET CD SPEED (0xBB) but honor SET STREAMING, and unlike a
|
|
||||||
/// firmware unlock it is a STOCK MMC command — safe to issue on a non-unlocked
|
|
||||||
/// drive, so it never disturbs stock CSS auth.
|
|
||||||
///
|
|
||||||
/// `read_kbps` is the requested read size per 1000 ms window; `0xFFFF_FFFF` asks
|
|
||||||
/// the drive for its maximum. Returns `(cdb, descriptor)`; the caller sends the
|
|
||||||
/// descriptor as the CDB's data-out payload. The CDB's Parameter List Length
|
|
||||||
/// (bytes 9–10, big-endian) is the descriptor length.
|
|
||||||
pub(crate) fn build_set_streaming(
|
|
||||||
read_kbps: u32,
|
|
||||||
) -> ([u8; 12], [u8; SET_STREAMING_DESCRIPTOR_LEN]) {
|
|
||||||
let len = SET_STREAMING_DESCRIPTOR_LEN as u16;
|
|
||||||
let cdb = [
|
|
||||||
SCSI_SET_STREAMING,
|
|
||||||
0x00,
|
|
||||||
0x00,
|
|
||||||
0x00,
|
|
||||||
0x00,
|
|
||||||
0x00,
|
|
||||||
0x00,
|
|
||||||
0x00,
|
|
||||||
0x00,
|
|
||||||
(len >> 8) as u8, // Parameter List Length (MSB)
|
|
||||||
len as u8, // Parameter List Length (LSB)
|
|
||||||
0x00,
|
|
||||||
];
|
|
||||||
let mut d = [0u8; SET_STREAMING_DESCRIPTOR_LEN];
|
|
||||||
// byte 0: flags — RDD=0 (SET performance, don't restore defaults), Exact=0,
|
|
||||||
// RA=0. bytes 1–3 reserved.
|
|
||||||
// bytes 4..8: Start LBA = 0.
|
|
||||||
// bytes 8..12: End LBA = 0xFFFFFFFF (apply across the whole disc).
|
|
||||||
d[8..12].copy_from_slice(&0xFFFF_FFFFu32.to_be_bytes());
|
|
||||||
// bytes 12..16: Read Size (kB per Read Time window).
|
|
||||||
d[12..16].copy_from_slice(&read_kbps.to_be_bytes());
|
|
||||||
// bytes 16..20: Read Time = 1000 ms.
|
|
||||||
d[16..20].copy_from_slice(&1000u32.to_be_bytes());
|
|
||||||
// bytes 20..24 / 24..28: Write Size / Write Time (mirror read; unused for a
|
|
||||||
// read-only rip but the descriptor requires them).
|
|
||||||
d[20..24].copy_from_slice(&read_kbps.to_be_bytes());
|
|
||||||
d[24..28].copy_from_slice(&1000u32.to_be_bytes());
|
|
||||||
(cdb, d)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
/// SET STREAMING (0xB6) wire format: opcode, the 28-byte Parameter List
|
|
||||||
/// Length in CDB bytes 9–10, and the max-speed performance descriptor
|
|
||||||
/// (whole-disc End LBA + the requested read size).
|
|
||||||
#[test]
|
|
||||||
fn set_streaming_cdb_layout() {
|
|
||||||
let (cdb, d) = build_set_streaming(0xFFFF_FFFF);
|
|
||||||
assert_eq!(cdb[0], SCSI_SET_STREAMING);
|
|
||||||
assert_eq!(
|
|
||||||
u16::from_be_bytes([cdb[9], cdb[10]]) as usize,
|
|
||||||
SET_STREAMING_DESCRIPTOR_LEN,
|
|
||||||
"param list length = descriptor length"
|
|
||||||
);
|
|
||||||
assert_eq!(d.len(), SET_STREAMING_DESCRIPTOR_LEN);
|
|
||||||
assert_eq!(&d[8..12], &[0xFF, 0xFF, 0xFF, 0xFF], "whole-disc End LBA");
|
|
||||||
assert_eq!(
|
|
||||||
u32::from_be_bytes([d[12], d[13], d[14], d[15]]),
|
|
||||||
0xFFFF_FFFF,
|
|
||||||
"read size requests max"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
u32::from_be_bytes([d[16], d[17], d[18], d[19]]),
|
|
||||||
1000,
|
|
||||||
"read time window = 1000 ms"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user