mirror of
https://github.com/freemkv/freemkv-unlock.git
synced 2026-10-07 18:13:11 +00:00
docs: move drive/firmware internals notes to the private repo; drop their pointers [skip-media]
This commit is contained in:
+13
-19
@@ -14,7 +14,7 @@ use crate::scsi::{DataDirection, ScsiTransport};
|
||||
|
||||
// Issue ONE CSS bus-auth CDB, honouring the transport contract: treats a
|
||||
// non-zero status as CssAuthFailed and checks bytes_transferred >= min_bytes
|
||||
// rather than trusting an unfilled buffer. See docs/css-mod.md#css_scsi.
|
||||
// rather than trusting an unfilled buffer.
|
||||
fn css_scsi(
|
||||
scsi: &mut dyn ScsiTransport,
|
||||
cdb: &[u8],
|
||||
@@ -202,7 +202,7 @@ impl crate::Unlocker for DvdUnlocker {
|
||||
fn name(&self) -> &'static str {
|
||||
// "DVD" names the medium this bus-auth unlocks, not the CSS scheme:
|
||||
// the bus-auth runs on any DVD regardless of whether the content is
|
||||
// actually scrambled. See docs/css-mod.md#dvdunlocker-name.
|
||||
// actually scrambled.
|
||||
"DVD"
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ impl crate::Unlocker for DvdUnlocker {
|
||||
|
||||
// Probes GET CONFIGURATION current-profile (DVD family 0x0010..=0x001F) so
|
||||
// DvdUnlocker can self-verify against the drive rather than trust the
|
||||
// caller's DiscKind. See docs/css-mod.md#mounted_disc_is_dvd.
|
||||
// caller's DiscKind.
|
||||
fn mounted_disc_is_dvd(
|
||||
scsi: &mut dyn ScsiTransport,
|
||||
) -> std::result::Result<bool, crate::UnlockError> {
|
||||
@@ -300,7 +300,6 @@ fn unlock_css_reads_inner(scsi: &mut dyn ScsiTransport, _lba: u32) -> Result<()>
|
||||
tracing::debug!(target: "freemkv::css", agid, "css unlock: bus authentication ok");
|
||||
// Disc-key REPORT KEY: best-effort only, for firmware that ties part of
|
||||
// its read-unlock to it; bytes are unused and failure is non-fatal.
|
||||
// See docs/css-mod.md#disc-key-report-key (7014 bug history).
|
||||
if let Err(e) = read_disc_key(scsi, agid) {
|
||||
tracing::debug!(target: "freemkv::css", error_code = e.code(), "css unlock: disc-key REPORT KEY skipped (non-fatal)");
|
||||
}
|
||||
@@ -311,7 +310,7 @@ fn unlock_css_reads_inner(scsi: &mut dyn ScsiTransport, _lba: u32) -> Result<()>
|
||||
// ── Step 1: Bus Authentication ────────────────────────────────────────────
|
||||
|
||||
// Runs the CSS bus-auth handshake to set ASF=1 (invalidate AGIDs, allocate,
|
||||
// challenge/response). Returns the AGID; no bus key is derived. See docs/css-mod.md.
|
||||
// challenge/response). Returns the AGID; no bus key is derived.
|
||||
fn establish_authenticated_session(scsi: &mut dyn ScsiTransport) -> Result<u8> {
|
||||
// Invalidate all AGIDs via REPORT KEY format 0x3F. This is also what makes
|
||||
// an abandoned AGID self-heal — which is not a reason to abandon one.
|
||||
@@ -438,7 +437,7 @@ fn authenticate_with_agid(scsi: &mut dyn ScsiTransport, agid: u8) -> Result<()>
|
||||
// ── Step 2: Disc Key ──────────────────────────────────────────────────────
|
||||
|
||||
// Issues READ DVD STRUCTURE format 0x02 (opcode 0xAD) purely for its
|
||||
// bus-auth side effect; returned bytes are unused. See docs/css-mod.md#read_disc_key.
|
||||
// bus-auth side effect; returned bytes are unused.
|
||||
fn read_disc_key(scsi: &mut dyn ScsiTransport, agid: u8) -> Result<()> {
|
||||
// READ DVD STRUCTURE, format 0x02 (disc key), 2048+4 bytes
|
||||
let alloc_len: u16 = 2048 + 4;
|
||||
@@ -464,7 +463,7 @@ fn read_disc_key(scsi: &mut dyn ScsiTransport, agid: u8) -> Result<()> {
|
||||
fn crypt_key(key_type: usize, variant: u8, challenge: &[u8; 10]) -> [u8; 5] {
|
||||
// key_type indexes PERM_CHALLENGE ([_;3]); variant indexes VARIANTS/
|
||||
// PERM_VARIANT ([_;32]). Real (not debug-only) asserts catch an OOB index even
|
||||
// in release; cost is nil on this once-per-disc path. See docs/css-mod.md.
|
||||
// in release; cost is nil on this once-per-disc path.
|
||||
assert!(key_type < 3, "crypt_key: key_type out of range");
|
||||
assert!((variant as usize) < 32, "crypt_key: variant out of range");
|
||||
let perm = &PERM_CHALLENGE[key_type];
|
||||
@@ -620,7 +619,7 @@ mod tests {
|
||||
|
||||
// SECURITY REGRESSION GUARD: scans source files for a `tracing` field
|
||||
// binding a forbidden key name to a value expression (only a string
|
||||
// literal or `_fp` field is allowed). See docs/css-mod.md#key-guard.
|
||||
// literal or `_fp` field is allowed).
|
||||
#[test]
|
||||
fn no_key_bytes_in_instrumentation() {
|
||||
use std::path::Path;
|
||||
@@ -776,7 +775,7 @@ mod tests {
|
||||
// ── CSS constant-table integrity ───────────────────────────────────────
|
||||
|
||||
// Each PERM_CHALLENGE row must be a permutation of indices 0..10; a
|
||||
// non-permutation would drop/duplicate bytes. See docs/css-mod.md#perm-challenge-rows.
|
||||
// non-permutation would drop/duplicate bytes.
|
||||
#[test]
|
||||
fn perm_challenge_rows_are_permutations() {
|
||||
for (row, perm) in PERM_CHALLENGE.iter().enumerate() {
|
||||
@@ -795,7 +794,6 @@ mod tests {
|
||||
|
||||
// Each PERM_VARIANT row must map the 32 variants to 32 distinct 5-bit
|
||||
// values; a collision would make two variants indistinguishable.
|
||||
// See docs/css-mod.md#perm-variant-rows.
|
||||
#[test]
|
||||
fn perm_variant_rows_are_permutations_of_0_31() {
|
||||
for (row, perm) in PERM_VARIANT.iter().enumerate() {
|
||||
@@ -816,7 +814,7 @@ mod tests {
|
||||
// ── crypt_key behaviour ────────────────────────────────────────────────
|
||||
|
||||
// crypt_key's result must depend on every challenge byte: flipping any
|
||||
// single byte must change the output. See docs/css-mod.md#crypt-key-byte-dependence.
|
||||
// single byte must change the output.
|
||||
#[test]
|
||||
fn crypt_key_depends_on_every_challenge_byte() {
|
||||
let base: [u8; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
@@ -834,7 +832,7 @@ mod tests {
|
||||
|
||||
// crypt_key(0, v, ..) must be distinct for each of the 32 variants:
|
||||
// bus-auth brute-forces the variant by matching against key1, so a
|
||||
// collision could select the wrong one. See docs/css-mod.md.
|
||||
// collision could select the wrong one.
|
||||
#[test]
|
||||
fn crypt_key_type0_distinct_per_variant() {
|
||||
let challenge: [u8; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
@@ -851,7 +849,6 @@ mod tests {
|
||||
|
||||
// crypt_key's `key_type < 3` debug_assert must fire for key_type 3
|
||||
// (which would otherwise index PERM_CHALLENGE out of bounds).
|
||||
// See docs/css-mod.md#crypt-key-preconditions.
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn crypt_key_rejects_out_of_range_key_type() {
|
||||
@@ -861,7 +858,6 @@ mod tests {
|
||||
|
||||
// crypt_key's `variant < 32` debug_assert must fire for variant 32
|
||||
// (which would otherwise index VARIANTS/PERM_VARIANT out of bounds).
|
||||
// See docs/css-mod.md#crypt-key-preconditions.
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn crypt_key_rejects_out_of_range_variant() {
|
||||
@@ -895,7 +891,6 @@ mod tests {
|
||||
|
||||
// The key-format field is masked to 6 bits so a format with high bits
|
||||
// set (e.g. 0xFF) cannot corrupt the AGID bits of byte 10.
|
||||
// See docs/css-mod.md#cdb-builders.
|
||||
#[test]
|
||||
fn report_key_cdb_masks_format_to_6_bits() {
|
||||
let cdb = report_key_cdb(0, 0xFF, 8);
|
||||
@@ -904,7 +899,7 @@ mod tests {
|
||||
|
||||
// send_key_cdb encodes a 12-byte MMC SEND KEY (opcode 0xA3) CDB with the
|
||||
// parameter-list length at bytes 8-9 (big-endian) and AGID/format at
|
||||
// byte 10. See docs/css-mod.md#cdb-builders.
|
||||
// byte 10.
|
||||
#[test]
|
||||
fn send_key_cdb_matches_mmc_layout() {
|
||||
let cdb = send_key_cdb(0b11, 0x03, 0x000C); // AGID=3, format=3, param_len=12
|
||||
@@ -921,7 +916,6 @@ mod tests {
|
||||
|
||||
// Allocation length > 255 must split across bytes 8 (high) and 9 (low)
|
||||
// as a 16-bit big-endian field, e.g. 0x0804 (the disc-key block size).
|
||||
// See docs/css-mod.md#cdb-builders.
|
||||
#[test]
|
||||
fn report_key_cdb_alloc_len_is_16bit_big_endian() {
|
||||
let cdb = report_key_cdb(0, 0x00, 0x0804);
|
||||
@@ -1012,7 +1006,7 @@ mod tests {
|
||||
|
||||
// Defect-7 regression: a transport fault on the first probe command
|
||||
// must abort as Transport, not fall through to NotApplicable (which
|
||||
// let the consumer keep probing a dead bus). See docs/css-mod.md.
|
||||
// let the consumer keep probing a dead bus).
|
||||
#[test]
|
||||
fn transport_fault_probing_for_a_dvd_aborts() {
|
||||
let id = DriveId::default();
|
||||
@@ -1039,7 +1033,7 @@ mod tests {
|
||||
|
||||
// Defect-2 regression: a DVD is mounted, then the bus dies mid
|
||||
// bus-auth — must abort as Transport, not collapse to CssAuthFailed /
|
||||
// NotApplicable. See docs/css-mod.md.
|
||||
// NotApplicable.
|
||||
#[test]
|
||||
fn transport_fault_during_bus_auth_aborts() {
|
||||
let id = DriveId::default();
|
||||
|
||||
+5
-6
@@ -8,7 +8,7 @@
|
||||
|
||||
// `cdb` is only the bdemu emulator's wire format (real unlocking uses profile
|
||||
// templates), gated behind `emulation`; also compiled under `cfg(test)` so its
|
||||
// wire-format tests run in default-feature CI. See docs/ld-mod.md — cdb gating.
|
||||
// wire-format tests run in default-feature CI.
|
||||
#[cfg(any(feature = "emulation", test))]
|
||||
mod cdb;
|
||||
mod error;
|
||||
@@ -21,7 +21,7 @@ use crate::{DriveId, UnlockCtx, UnlockError, Unlocked, Unlocker};
|
||||
|
||||
// ── Public profile catalog ──────────────────────────────────────────────────
|
||||
// Only the catalog is public (supported-drive lookup, used by bdemu); the
|
||||
// unlock mechanism stays private. See docs/ld-mod.md — public catalog design.
|
||||
// unlock mechanism stays private.
|
||||
|
||||
pub use profile::{DriveProfile as Profile, Identity, Platform, ProfileMatch, Profiles};
|
||||
|
||||
@@ -122,7 +122,7 @@ impl LdUnlocker {
|
||||
impl LdUnlocker {
|
||||
// The MediaTek firmware unlock. Since it removes AACS at the drive (clear
|
||||
// content), this one op satisfies both features and bus-removal, so both
|
||||
// trait methods delegate here. See docs/ld-mod.md — firmware_unlock contract.
|
||||
// trait methods delegate here.
|
||||
fn firmware_unlock(
|
||||
&self,
|
||||
scsi: &mut dyn ScsiTransport,
|
||||
@@ -144,7 +144,7 @@ impl LdUnlocker {
|
||||
mt.init(scsi)?;
|
||||
// `init` only proves the handshake completed, not that the drive reached
|
||||
// extended-access state. Reporting unlocked off `init` alone shipped
|
||||
// ciphertext at rc=0. See docs/ld-mod.md — half-unlock fallback.
|
||||
// ciphertext at rc=0.
|
||||
if !mt.is_unlocked() {
|
||||
tracing::warn!(
|
||||
target: "freemkv::disc",
|
||||
@@ -159,7 +159,7 @@ impl LdUnlocker {
|
||||
if let Err(e) = mt.probe_disc(scsi) {
|
||||
// A transport fault here is a dead bus, not a calibration miss — most
|
||||
// profiles never touch the bus again, so this was the only dead-bus
|
||||
// signal. See docs/ld-mod.md — probe_disc dead-bus classification.
|
||||
// signal.
|
||||
if e.is_transport_failure() {
|
||||
tracing::warn!(
|
||||
target: "freemkv::disc",
|
||||
@@ -432,7 +432,6 @@ mod tests {
|
||||
|
||||
// THE probe-disc dead-bus test: bus dies during speed calibration after a
|
||||
// full unlock; must abort with Transport, not report a successful unlock.
|
||||
// See docs/ld-mod.md — probe_disc dead-bus test / mutation notes.
|
||||
#[test]
|
||||
fn transport_fault_during_probe_disc_is_transport_not_a_successful_unlock() {
|
||||
let id = known_vid_drive_id();
|
||||
|
||||
@@ -127,7 +127,7 @@ impl Mt1959 {
|
||||
let result = scsi.execute(&cdb, DataDirection::FromDevice, buf, 5_000)?;
|
||||
// A drive sense arrives as `Ok` with a non-zero status; a merely
|
||||
// SHORT response is the drive answering badly — neither is a dead
|
||||
// bus. See docs/mt1959.md — short-probe-not-transport-failure.
|
||||
// bus.
|
||||
if result.status != 0 || result.bytes_transferred != expected {
|
||||
return Err(Error::Scsi {
|
||||
opcode: SCSI_READ_BUFFER,
|
||||
@@ -181,7 +181,7 @@ impl Mt1959 {
|
||||
|
||||
// `response` is a fixed 64-byte buffer; the meaningful bound is how
|
||||
// many bytes the drive actually delivered. Require enough bytes to
|
||||
// validate before checking markers, up front. See docs/mt1959.md — min-unlock-response-length.
|
||||
// validate before checking markers, up front.
|
||||
let n = result.bytes_transferred.min(response.len());
|
||||
if n < MIN_UNLOCK_RESPONSE {
|
||||
tracing::debug!(
|
||||
@@ -206,7 +206,6 @@ impl Mt1959 {
|
||||
|
||||
// Extended-access state requires the signature match AND both the
|
||||
// primary marker at [12..16] AND the secondary marker at [16..20].
|
||||
// See docs/mt1959.md — unlock-marker-gating.
|
||||
self.unlocked =
|
||||
response[FIRMWARE_MODE_OFFSET..FIRMWARE_MODE_OFFSET + 4] == FIRMWARE_MODE_SIG;
|
||||
|
||||
@@ -282,7 +281,7 @@ impl Mt1959 {
|
||||
return Err(Error::UnlockFailed);
|
||||
}
|
||||
// A dead bus must abort here, not be retried three times as
|
||||
// firmware reloads. See docs/mt1959.md — run-init-transport-fault.
|
||||
// firmware reloads.
|
||||
Err(e) if e.is_transport_failure() => {
|
||||
tracing::warn!(
|
||||
target: "freemkv::disc",
|
||||
@@ -349,7 +348,7 @@ impl Mt1959 {
|
||||
}
|
||||
|
||||
// Detect disc type from capacity to select probe mode (BD vs UHD
|
||||
// init address). See docs/mt1959.md — probe-init-address-detection.
|
||||
// init address).
|
||||
let cap_cdb = [
|
||||
SCSI_READ_CAPACITY,
|
||||
0x00,
|
||||
@@ -368,7 +367,7 @@ impl Mt1959 {
|
||||
let cap = scsi.execute(&cap_cdb, DataDirection::FromDevice, &mut cap_buf, 5_000)?;
|
||||
let disc_sectors = if cap.status == 0 && cap.bytes_transferred >= 4 {
|
||||
// last_lba + 1 = sector count; saturate on the 32-bit-overflow
|
||||
// sentinel rather than wrap to 0. See docs/mt1959.md — capacity-saturation.
|
||||
// sentinel rather than wrap to 0.
|
||||
u32::from_be_bytes([cap_buf[0], cap_buf[1], cap_buf[2], cap_buf[3]]).saturating_add(1)
|
||||
} else {
|
||||
0
|
||||
@@ -413,7 +412,7 @@ impl Mt1959 {
|
||||
{
|
||||
// A dead bus MUST keep its transport classification; a
|
||||
// short/rejected reply is the drive's own failure and stays
|
||||
// `UnlockFailed`. See docs/mt1959.md — probe-failure-classification.
|
||||
// `UnlockFailed`.
|
||||
if e.is_transport_failure() {
|
||||
return Err(e);
|
||||
}
|
||||
@@ -423,7 +422,6 @@ impl Mt1959 {
|
||||
}
|
||||
|
||||
// Pass 2: continue past the coarse range (do not restart at 0).
|
||||
// See docs/mt1959.md — probe-pass-overlap-fix.
|
||||
let mut addr: u32 = PROBE_COARSE_END as u32;
|
||||
while addr < PROBE_FINE_END {
|
||||
let mut resp = [0u8; PROBE_RESPONSE_SIZE as usize];
|
||||
@@ -436,7 +434,7 @@ impl Mt1959 {
|
||||
) {
|
||||
// Fine probing is best-effort — a short/rejected reply just ends
|
||||
// the sweep early, but a transport fault must keep its
|
||||
// classification and abort. See docs/mt1959.md — probe-pass2-transport-fault.
|
||||
// classification and abort.
|
||||
if e.is_transport_failure() {
|
||||
tracing::warn!(
|
||||
target: "freemkv::disc",
|
||||
@@ -696,7 +694,7 @@ mod tests {
|
||||
|
||||
// THE defect-15 test: a zero-byte response used to skip every marker
|
||||
// check (each individually `n >= ..` guarded) and report a fully
|
||||
// unlocked drive. See docs/mt1959.md — defect-15.
|
||||
// unlocked drive.
|
||||
#[test]
|
||||
fn do_unlock_rejects_a_zero_length_response() {
|
||||
let sig = [0x99, 0x9E, 0xC3, 0x75];
|
||||
@@ -776,7 +774,7 @@ mod tests {
|
||||
|
||||
// THE defect-4 test: a dead bus must abort `run_init` at once, not
|
||||
// re-upload firmware on all three attempts and return the generic
|
||||
// `UnlockFailed`. See docs/mt1959.md — defect-4.
|
||||
// `UnlockFailed`.
|
||||
#[test]
|
||||
fn run_init_aborts_on_a_transport_fault_without_reloading_firmware() {
|
||||
let mut t =
|
||||
@@ -894,7 +892,7 @@ mod tests {
|
||||
|
||||
// THE probe pass-1 dead-bus test: a bus that dies during the coarse
|
||||
// probe must keep its transport classification so the caller aborts
|
||||
// with `Transport`. See docs/mt1959.md — pass1-dead-bus-mutation.
|
||||
// with `Transport`.
|
||||
#[test]
|
||||
fn transport_fault_during_pass1_probe_stays_a_transport_failure() {
|
||||
let mut t = ProbeFaultsTransport { fault_at: 0 };
|
||||
@@ -909,7 +907,7 @@ mod tests {
|
||||
|
||||
// THE probe pass-2 dead-bus test: the loop used to `break` on ANY
|
||||
// error and return `Ok(())`, swallowing a dead bus mid-pass-2. It
|
||||
// must now propagate. See docs/mt1959.md — pass2-dead-bus-mutation.
|
||||
// must now propagate.
|
||||
#[test]
|
||||
fn transport_fault_during_pass2_probe_stays_a_transport_failure() {
|
||||
let mut t = ProbeFaultsTransport {
|
||||
@@ -952,7 +950,7 @@ mod tests {
|
||||
|
||||
// variant_a's firmware-verify-read transport-abort branch: WRITE_BUFFER
|
||||
// succeeds, the 0x45 verify read faults — a dead bus must abort there,
|
||||
// not be swallowed into the unlock retries. See docs/mt1959.md — variant-a-verify-read-transport-abort.
|
||||
// not be swallowed into the unlock retries.
|
||||
#[test]
|
||||
fn variant_a_verify_read_transport_fault_aborts() {
|
||||
let mut profile = fixture_profile([0; 4]);
|
||||
@@ -1013,7 +1011,7 @@ mod tests {
|
||||
|
||||
// variant_b's firmware-upload step (here the metadata read) hitting a
|
||||
// dead bus must abort via `trace_step`, not be swallowed into the
|
||||
// unlock retries. See docs/mt1959.md — variant-b-upload-step-transport-abort.
|
||||
// unlock retries.
|
||||
#[test]
|
||||
fn variant_b_upload_step_transport_fault_aborts() {
|
||||
let mut profile = fixture_profile([0; 4]);
|
||||
|
||||
@@ -13,7 +13,6 @@ const FIRMWARE_EXTRA: [u8; 16] = [0; 16];
|
||||
/// that drive — real profiles must supply their own (see `DriveProfile`).
|
||||
const VENDOR_VERIFY: [u8; 10] = [0xF1, 0x01, 0x02, 0x00, 0x0D, 0x30, 0x01, 0xF3, 0xAD, 0x23];
|
||||
|
||||
// See docs/variant-b-firmware-upload.md — trace_step rationale.
|
||||
// Swallows everything except a transport fault; these steps are advisory,
|
||||
// the unlock retries below are the real gate.
|
||||
fn trace_step(phase: &'static str, r: crate::scsi::Result<crate::scsi::ScsiResult>) -> Result<()> {
|
||||
@@ -58,7 +57,7 @@ pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Re
|
||||
return Err(crate::ld::error::Error::UnlockFailed);
|
||||
}
|
||||
|
||||
// Step 1: Upload the firmware via MODE SELECT (see docs/variant-b-firmware-upload.md).
|
||||
// Step 1: Upload the firmware via MODE SELECT.
|
||||
// MODE SELECT(10)'s parameter-list length is 16-bit, so reject only a
|
||||
// blob that can't be expressed in the CDB.
|
||||
let write_len = firmware.len();
|
||||
@@ -124,8 +123,6 @@ pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Re
|
||||
)?;
|
||||
|
||||
// Step 4: Vendor verify (0xF1 — B-only, not standard SCSI), per-drive.
|
||||
// See docs/variant-b-firmware-upload.md for why the outcome is traced
|
||||
// rather than discarded.
|
||||
let verify_cdb = mt.profile.fw_verify_cdb.unwrap_or(VENDOR_VERIFY);
|
||||
let mut dummy = [0u8; 0];
|
||||
trace_step(
|
||||
@@ -134,8 +131,7 @@ pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Re
|
||||
)?;
|
||||
|
||||
// Step 5: Unlock retries (up to 5, then a final fatal attempt). The
|
||||
// confirmation pass after success is intentionally best-effort — see
|
||||
// docs/variant-b-firmware-upload.md.
|
||||
// confirmation pass after success is intentionally best-effort.
|
||||
for _attempt in 0..5 {
|
||||
match mt.do_unlock(scsi) {
|
||||
Ok(_) => {
|
||||
|
||||
+7
-9
@@ -24,8 +24,7 @@ const RENESAS_MARKER_OFFSET: usize = 16;
|
||||
/// interface tag. A rejection (CHECK CONDITION or `Err` with a sense) is
|
||||
/// `Ok(false)`: not a Renesas drive.
|
||||
///
|
||||
/// `Err(Transport)` on a dead bus. See docs/renesas-mod.md for why a
|
||||
/// transport fault must not be folded into `Ok(false)`.
|
||||
/// `Err(Transport)` on a dead bus.
|
||||
pub fn is_renesas(scsi: &mut dyn ScsiTransport) -> std::result::Result<bool, UnlockError> {
|
||||
let mut buf = [0u8; RB_F1_LEN];
|
||||
match scsi.execute(&RB_F1_CDB, DataDirection::FromDevice, &mut buf, 5_000) {
|
||||
@@ -71,8 +70,7 @@ impl Renesas {
|
||||
}
|
||||
|
||||
// MakeMKV's vendor "open" sequence: primary read (A), and on refusal a
|
||||
// knock + second read (B). See docs/renesas-mod.md for the full sequence
|
||||
// and why we run A→knock→B rather than bailing after A.
|
||||
// knock + second read (B).
|
||||
fn vendor_open(scsi: &mut dyn ScsiTransport) -> std::result::Result<bool, UnlockError> {
|
||||
const RB_B0_04_CDB: [u8; 10] = [0x3C, 0x02, 0xB0, 0x00, 0x00, 0x04, 0x00, 0x00, 0xA4, 0x00];
|
||||
const KNOCK_A5AAAA_CDB: [u8; 10] =
|
||||
@@ -128,7 +126,7 @@ impl Unlocker for Renesas {
|
||||
/// best-effort `0xAD` reader. `Some` once the open succeeds — the drive
|
||||
/// serves clear content whether or not the bonus VID read did; `None` if it
|
||||
/// isn't a Renesas drive or the open is refused; `Err(Transport)` on a dead
|
||||
/// bus. See docs/renesas-mod.md for the MakeMKV-parity rationale.
|
||||
/// bus.
|
||||
fn unlock(
|
||||
&self,
|
||||
scsi: &mut dyn ScsiTransport,
|
||||
@@ -188,7 +186,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// Rejects like a MediaTek drive: ILLEGAL REQUEST with a sense, which is
|
||||
// what distinguishes this from a dead bus. See docs/renesas-mod.md.
|
||||
// what distinguishes this from a dead bus.
|
||||
struct RejectingTransport;
|
||||
impl ScsiTransport for RejectingTransport {
|
||||
fn execute(
|
||||
@@ -280,7 +278,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// Same rejection via a CONFORMING transport (`Ok` + CHECK CONDITION);
|
||||
// must reach the same answer. See docs/renesas-mod.md.
|
||||
// must reach the same answer.
|
||||
#[test]
|
||||
fn check_condition_is_not_a_renesas_drive() {
|
||||
use crate::scsi::mock::{MockTransport, Reply};
|
||||
@@ -308,7 +306,7 @@ mod tests {
|
||||
|
||||
// A recognized Renesas drive whose A read (RB 0xB0@0x04) is REFUSED but
|
||||
// whose B read (RB 0xB0@0x50) SUCCEEDS after the knock must unlock — and the
|
||||
// knock CDB has to be issued BETWEEN A and B. See docs/renesas-mod.md.
|
||||
// knock CDB has to be issued BETWEEN A and B.
|
||||
#[test]
|
||||
fn knock_runs_between_a_and_b_and_b_opens() {
|
||||
// The exact knock CDB the code issues (pinned wire format).
|
||||
@@ -395,7 +393,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// A recognized Renesas drive that REFUSES the vendor open read must
|
||||
// defer to the next unlocker, not claim the drive. See docs/renesas-mod.md.
|
||||
// defer to the next unlocker, not claim the drive.
|
||||
#[test]
|
||||
fn open_rejection_defers_to_next_unlocker() {
|
||||
struct GateOkOpenRefused;
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
//! BEST-EFFORT: only a dead bus is an `Err(Transport)`. A CHECK CONDITION, a
|
||||
//! short response, or an all-zero VID all yield `Ok(None)` — a VID miss must
|
||||
//! never discard an unlock that already removed the bus (a key source can still
|
||||
//! supply the key). See docs/freemkv-abi.md for the fixed-length reply rules.
|
||||
//! supply the key).
|
||||
|
||||
use crate::UnlockError;
|
||||
use crate::scsi::{DataDirection, ScsiTransport};
|
||||
|
||||
Reference in New Issue
Block a user