diff --git a/src/ld/mod.rs b/src/ld/mod.rs index 34c028d..9541595 100644 --- a/src/ld/mod.rs +++ b/src/ld/mod.rs @@ -125,33 +125,6 @@ impl Unlocker for LibreDrive { 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 /// serves CLEAR content, so `drive_unlocked: true` and there is no bus key. /// @@ -322,60 +295,4 @@ mod tests { .expect_err("no profile → NotApplicable"); assert_eq!(err, UnlockError::NotApplicable); } - - /// A transport that records the opcode of every CDB it is handed. - struct RecordingTransport { - opcodes: Vec, - } - impl ScsiTransport for RecordingTransport { - fn execute( - &mut self, - cdb: &[u8], - _dir: DataDirection, - _data: &mut [u8], - _timeout_ms: u32, - ) -> crate::scsi::Result { - 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" - ); - } } diff --git a/src/lib.rs b/src/lib.rs index 293f862..c3a249c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,39 +102,16 @@ pub enum UnlockError { Transport, } -/// An unlocker provides drive/disc capabilities: **drive features** (speed / -/// riplock lift — a property of the DRIVE, applied for any disc) and **bus -/// removal** (AACS bus-decrypt + VID, or a CSS handshake — gated on the disc). -/// Implementors are the self-contained modules in this crate; the consumer only -/// ever sees the trait, via [`all_unlockers`]. (Each module owns its own -/// conversion from its internal error to [`UnlockError`].) +/// An unlocker removes a drive-level bus-encryption barrier. Implementors are +/// the self-contained modules in this crate; the consumer only 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 -/// other. A CSS DVD, for example, wants a matched drive's [`apply_drive_features`] -/// (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 +/// NOTE: drive tuning (e.g. SET CD SPEED to lift riplock) is deliberately NOT +/// here — that is the consumer's concern, not bus removal. pub trait Unlocker: Send + Sync { - /// True if this unlocker's bus-removal [`unlock`](Unlocker::unlock) applies to - /// the given context (drive id + disc kind). + /// True if this unlocker applies to the given context (drive id + disc kind). 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. fn unlock( &self, diff --git a/src/scsi.rs b/src/scsi.rs index 5b4e4bd..d710d9e 100644 --- a/src/scsi.rs +++ b/src/scsi.rs @@ -74,7 +74,6 @@ pub(crate) const SCSI_STATUS_CHECK_CONDITION: u8 = 0x02; // Common opcodes used by the unlocker modules. 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_REPORT_KEY: u8 = 0xA4; 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, ] } - -/// 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" - ); - } -}