From bead4869a9eef688d45d3f6278074289f9f67517 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Mon, 5 Oct 2026 22:28:46 -0700 Subject: [PATCH] Read Renesas VID directly from hardware registers --- src/renesas/mod.rs | 159 ++++------------- src/renesas/mod_tests.rs | 356 +++++++++++---------------------------- 2 files changed, 132 insertions(+), 383 deletions(-) diff --git a/src/renesas/mod.rs b/src/renesas/mod.rs index d021ed9..4592d37 100644 --- a/src/renesas/mod.rs +++ b/src/renesas/mod.rs @@ -4,7 +4,7 @@ //! [`crate::ld`]) and Renesas. This module identifies the Renesas side via a //! single vendor identity probe (see [`is_renesas`]). Renesas drives are //! OEM-unlocked: identity establishes unlocked status without a host certificate. -//! Vendor memory access is used only for optional VID discovery; a discovery +//! Vendor register access is used only for optional VID retrieval; a read //! miss preserves unlocked status, while transport faults and cancellation abort. use crate::scsi::{DataDirection, ScsiTransport, is_dead_bus}; @@ -58,7 +58,7 @@ pub fn is_renesas(scsi: &mut dyn ScsiTransport) -> std::result::Result std::result::Result, UnlockError> { +// Selector 0x92 maps offset - 0x2000 to high CPU memory; no AAAA gate. +const VID_STATUS_CDB: [u8; 10] = [0x3c, 2, 0x92, 0, 0x0d, 0x3c, 0, 0, 1, 0]; +const VID_CDB: [u8; 10] = [0x3c, 2, 0x92, 0, 0x0d, 0x20, 0, 0, 16, 0]; + +fn get_vid(scsi: &mut dyn ScsiTransport) -> std::result::Result, UnlockError> { + // Mirror the OEM hardware-copy gate. Other status bits are not interpreted. + let mut status = [0]; + let result = scsi.execute( + &VID_STATUS_CDB, + DataDirection::FromDevice, + &mut status, + 5_000, + ); + tracing::debug!(target: "freemkv::disc", phase = "renesas_vid_status", + cdb = ?VID_STATUS_CDB, requested = 1, ?result, payload = ?status, + "Reading Renesas VID hardware status"); + match result { + Ok(r) if r.status == 0 && r.bytes_transferred == 1 && status[0] & 2 != 0 => {} + Err(e) if is_dead_bus(&e) => return Err(UnlockError::Transport), + _ => return Ok(None), + } let mut vid = [0u8; 16]; - let cdb = pioneer_optical::cdb::read_memory(address, vid.len() as u32); + let cdb = VID_CDB; + let address = 0xffff_ed20u32; tracing::debug!(target: "freemkv::disc", phase = "renesas_vid_request", - address = format_args!("{address:#x}"), cdb = format_args!("{cdb:02x?}"), requested = vid.len(), "Reading VID from Renesas memory"); + address = format_args!("{address:#x}"), cdb = format_args!("{cdb:02x?}"), requested = vid.len(), "Reading VID from Renesas hardware registers"); let result = match scsi.execute(&cdb, DataDirection::FromDevice, &mut vid, 5_000) { Ok(r) => r, Err(e) => { tracing::debug!(target: "freemkv::disc", phase = "renesas_vid_error", address = format_args!("{address:#x}"), status = e.status, sense = ?e.sense, dead_bus = is_dead_bus(&e), - "Renesas memory VID read failed"); + "Renesas hardware VID read failed"); return if is_dead_bus(&e) { Err(UnlockError::Transport) } else { @@ -108,7 +126,7 @@ fn get_vid( address = format_args!("{address:#x}"), status = result.status, bytes_transferred = result.bytes_transferred, sense = ?result.sense, payload = format_args!("{payload:02x?}"), valid, rejection, - "Renesas memory VID response"); + "Renesas hardware VID response"); Ok(valid.then_some(vid)) } @@ -117,8 +135,8 @@ impl Unlocker for Renesas { "Renesas" } - /// SAT identity establishes OEM-unlocked status. Extended reads and RAM VID - /// discovery are best-effort; a missing VID preserves success, dead buses abort. + /// SAT identity establishes OEM-unlocked status. Hardware VID reads are + /// best-effort; a missing VID preserves success, dead buses abort. fn unlock( &self, scsi: &mut dyn ScsiTransport, @@ -127,14 +145,7 @@ impl Unlocker for Renesas { if !is_renesas(scsi)? { return Ok(None); } - let vid = if enable_memory_reads(scsi)? { - match find_vid_addr(scsi)? { - Some(address) => get_vid(scsi, address)?, - None => None, - } - } else { - None - }; + let vid = get_vid(scsi)?; tracing::debug!( target: "freemkv::disc", phase = "renesas_opened", @@ -145,114 +156,6 @@ impl Unlocker for Renesas { } } -const FIRMWARE_BASE: u32 = 0x41_0000; -const WINDOW_START: u32 = 0x55_a000; -const WINDOW_LEN: usize = 0x20000; -const CHUNK: usize = 0x8000; - -/// Locate the source operand of the known 16-byte VID response copy loop. -/// Uniqueness is within this window; this is not a general H8 disassembler. -fn find_slot(code: &[u8]) -> Option { - let contains = |hay: &[u8], needle: &[u8]| hay.windows(needle.len()).any(|w| w == needle); - let mut found = None; - for (offset, op) in code.windows(26).enumerate().step_by(2) { - if op[..8] != [0x19, 0x33, 0x0d, 0x31, 0x17, 0x71, 0x6e, 0x1c] - || op[10..14] != [0x78, 0x10, 0x6a, 0xac] - || op[18..] != [0x0b, 0x53, 0x79, 0x23, 0, 0x10, 0x45, 0xe8] - { - continue; - } - let address = u16::from_be_bytes([op[8], op[9]]) as u32; - let pre = &code[offset.saturating_sub(80)..offset]; - let post = &code[offset + 26..(offset + 26 + 240).min(code.len())]; - let work = [0x7a, 0, op[14], op[15], op[16], op[17]]; - let checks = [ - pre.windows(12).any(|w| { - w[..2] == [0x79, 1] && w[4..] == [0x69, 0xf1, 0x18, 0x99, 0x6e, 0xf9, 0, 2] - }), - contains(post, &[0x79, 8, 0, 0x10]), - contains(post, &work), - contains(post, &[0x79, 0x24, 0, 0x20]), - contains(post, &[0x7a, 3, 0, 0x22, 0, 0]), - contains(post, &[0x1a, 0x80, 0xf8, 0x24, 1, 0, 0x6f, 0xa0, 0, 4]), - (1..=0x7ff0).contains(&address), - ]; - tracing::debug!(target: "freemkv::disc", phase = "renesas_vid_signature", - offset, address, ?checks, instruction = ?op, before = ?pre, after = ?post); - if checks.iter().all(|&v| v) { - if found.is_some() { - tracing::debug!(target: "freemkv::disc", "Ambiguous Renesas VID signature"); - return None; - } - found = Some(address); - } - } - tracing::debug!(target: "freemkv::disc", phase = "renesas_vid_slot", ?found); - found -} - -fn read_exact( - scsi: &mut dyn ScsiTransport, - address: u32, - buf: &mut [u8], -) -> Result { - let cdb = pioneer_optical::cdb::read_memory(address, buf.len() as u32); - for attempt in 1..=3 { - buf.fill(0); - let result = scsi.execute(&cdb, DataDirection::FromDevice, buf, 5_000); - tracing::debug!(target: "freemkv::disc", phase = "renesas_vid_firmware_read", - address, requested = buf.len(), attempt, ?cdb, ?result); - match result { - Ok(r) if r.status == 0 && r.bytes_transferred == buf.len() => return Ok(true), - Ok(r) if r.status == 0 && r.bytes_transferred < buf.len() && attempt < 3 => continue, - Err(e) if is_dead_bus(&e) => return Err(UnlockError::Transport), - _ => return Ok(false), - } - } - Ok(false) -} - -/// Called once at unlock entry, after identifying the OEM-unlocked platform. -fn enable_memory_reads(scsi: &mut dyn ScsiTransport) -> Result { - let result = scsi.execute( - &pioneer_optical::cdb::knock(), - DataDirection::None, - &mut [], - 5_000, - ); - tracing::debug!(target: "freemkv::disc", phase = "renesas_vid_enable", ?result); - match result { - Ok(r) => Ok(r.status == 0 && r.bytes_transferred == 0), - Err(e) if is_dead_bus(&e) => Err(UnlockError::Transport), - Err(_) => Ok(false), - } -} - -/// Discover the slot using memory access already enabled by the caller. -fn find_vid_addr(scsi: &mut dyn ScsiTransport) -> Result, UnlockError> { - let mut header = [0; 24]; - if !read_exact(scsi, FIRMWARE_BASE, &mut header)? { - return Ok(None); - } - let length = u32::from_be_bytes(header[20..24].try_into().unwrap()); - // The advertised image must contain the entire discovery window. - let minimum_length = WINDOW_START - FIRMWARE_BASE + WINDOW_LEN as u32; - let valid = header[..8] == *b"PIONEER " - && (minimum_length..=0x3f0000).contains(&length) - && length % 256 == 0; - tracing::debug!(target: "freemkv::disc", phase = "renesas_vid_header", ?header, length, valid); - if !valid { - return Ok(None); - } - let mut code = vec![0; WINDOW_LEN]; - for (i, chunk) in code.chunks_mut(CHUNK).enumerate() { - if !read_exact(scsi, WINDOW_START + (i * CHUNK) as u32, chunk)? { - return Ok(None); - } - } - Ok(find_slot(&code)) -} - #[cfg(test)] #[path = "mod_tests.rs"] mod mod_tests; diff --git a/src/renesas/mod_tests.rs b/src/renesas/mod_tests.rs index 4203634..de70f3b 100644 --- a/src/renesas/mod_tests.rs +++ b/src/renesas/mod_tests.rs @@ -16,11 +16,12 @@ mod tests { Reply::illegal_request(), Reply::illegal_request_as_err(), ] { - let mut t = MockTransport::always(reply.clone()); - assert_eq!(get_vid(&mut t, 0x2a10).unwrap(), None); + let mut t = + MockTransport::scripted(vec![Reply::good(vec![2]), reply], Reply::TransportFault); + assert_eq!(get_vid(&mut t).unwrap(), None); } let mut t = MockTransport::always(Reply::TransportFault); - assert_eq!(get_vid(&mut t, 0x2a10).unwrap_err(), UnlockError::Transport); + assert_eq!(get_vid(&mut t).unwrap_err(), UnlockError::Transport); } #[test] @@ -38,6 +39,14 @@ mod tests { timeout: u32, ) -> Result { assert_eq!(dir, DataDirection::FromDevice); + if data.len() == 1 { + data[0] = 2; + return Ok(ScsiResult { + status: 0, + bytes_transferred: 1, + sense: [0; 32], + }); + } assert_eq!(data.len(), 16); assert_eq!(timeout, 5_000); data.fill(0x25); @@ -50,13 +59,10 @@ mod tests { } for (status, transferred) in [(2, 16), (0, 17), (0, usize::MAX)] { assert_eq!( - get_vid( - &mut Response { - status, - transferred - }, - 0x2a10 - ) + get_vid(&mut Response { + status, + transferred + }) .unwrap(), None ); @@ -64,7 +70,7 @@ mod tests { } #[test] - fn cancellation_after_identity_aborts_before_enable() { + fn cancellation_after_identity_aborts_before_register_read() { use crate::scsi::mock::{MockTransport, Reply, StopFake}; let mut t = StopFake::new(MockTransport::always(Reply::good(renesas_payload()))); t.cancel_after = Some(|cdb| cdb == pioneer_optical::cdb::vendor_identity()); @@ -183,14 +189,14 @@ mod tests { }; let id = crate::DriveId::default(); let ctx = UnlockCtx::new(&id, DiscKind::Unknown); - // Recognized, but the served bytes do not contain a supported firmware header. + // Recognized, but the hardware status bit is clear. let out = Renesas::new() .unlock(&mut t, &ctx) .expect("no fault") .expect("renesas → unlocked"); assert!( out.vid.is_none(), - "unknown firmware must not fabricate a VID" + "clear hardware status must not fabricate a VID" ); assert_eq!(out.bus_key, None, "raw-read route needs no bus key"); } @@ -238,263 +244,103 @@ mod tests { } } -mod discovery_tests { +mod hardware_tests { use super::*; use crate::scsi::mock::{MockTransport, Reply}; - fn fixture(address: u16) -> Vec { - let mut code = vec![0; WINDOW_LEN]; - let mut block = vec![0x79, 1, 1, 0x3a, 0x69, 0xf1, 0x18, 0x99, 0x6e, 0xf9, 0, 2]; - block.extend([0x19, 0x33, 0x0d, 0x31, 0x17, 0x71, 0x6e, 0x1c]); - block.extend(address.to_be_bytes()); - block.extend([ - 0x78, 0x10, 0x6a, 0xac, 0, 0xa0, 0x27, 0xf4, 0x0b, 0x53, 0x79, 0x23, 0, 0x10, 0x45, - 0xe8, - ]); - block.extend([ - 0x79, 8, 0, 0x10, 0x7a, 0, 0, 0xa0, 0x27, 0xf4, 0x79, 0x24, 0, 0x20, 0x7a, 3, 0, 0x22, - 0, 0, 0x1a, 0x80, 0xf8, 0x24, 1, 0, 0x6f, 0xa0, 0, 4, - ]); - // Deliberately straddles a transport chunk boundary. - code[CHUNK - 20..CHUNK - 20 + block.len()].copy_from_slice(&block); - code - } - #[test] - fn validates_context_and_requires_unique_positive_slot() { - for address in [0x2832, 0x2a10, 0x2aa2] { - assert_eq!(find_slot(&fixture(address)), Some(address as u32)); - } - for address in [0, 0x8000, 0xfffe] { - assert_eq!(find_slot(&fixture(address)), None); - } - for relative in [10, 38, 42, 48, 52, 58] { - let mut code = fixture(0x2a10); - code[CHUNK - 20 + relative] ^= 1; - assert_eq!(find_slot(&code), None, "context mutation {relative}"); - } - let mut code = fixture(0x2a10); - code.copy_within(CHUNK - 20..CHUNK + 100, 1000); - assert_eq!(find_slot(&code), None); - assert_eq!(find_slot(&[]), None); - } - - fn script(vid: [u8; 16]) -> Vec { - let mut header = vec![0; 24]; - header[..8].copy_from_slice(b"PIONEER "); - header[20..].copy_from_slice(&0x1c7500u32.to_be_bytes()); - let mut replies = vec![Reply::good(vec![]), Reply::good(header)]; - replies.extend( - fixture(0x2aa2) - .chunks(CHUNK) - .map(|c| Reply::good(c.to_vec())), - ); - replies.push(Reply::good(vid.to_vec())); - replies - } - - #[test] - fn unlock_discovers_address_then_get_vid_only_reads_ram() { - let vid = [0x25; 16]; - let mut identity = vec![0x20; 48]; - identity[16..19].copy_from_slice(b"SAT"); - let mut replies = vec![Reply::good(identity)]; - replies.extend(script(vid)); - let mut t = MockTransport::scripted(replies, Reply::TransportFault); - let id = crate::DriveId::default(); - let ctx = UnlockCtx::new(&id, crate::DiscKind::Unknown); - let unlocker = Renesas::new(); - assert_eq!( - unlocker.unlock(&mut t, &ctx).unwrap().unwrap().vid, - Some(vid) - ); - assert_eq!(t.cdbs.len(), 8); - assert_eq!( - t.cdbs - .iter() - .filter(|cdb| cdb.as_slice() == pioneer_optical::cdb::knock()) - .count(), - 1 - ); - assert_eq!(t.cdbs[1], pioneer_optical::cdb::knock()); - for i in 0..4 { - assert_eq!( - t.cdbs[i + 3], - pioneer_optical::cdb::read_memory(WINDOW_START + (i * CHUNK) as u32, CHUNK as u32) - ); - } - assert_eq!(t.cdbs[7], pioneer_optical::cdb::read_memory(0x2aa2, 16)); - let mut t = MockTransport::always(Reply::good(vid.to_vec())); - assert_eq!(get_vid(&mut t, 0x2aa2).unwrap(), Some(vid)); - assert_eq!(t.cdbs, vec![pioneer_optical::cdb::read_memory(0x2aa2, 16)]); - } - - #[test] - fn discovery_does_not_enable_memory_again() { - let mut replies = script([0x25; 16]); - replies.remove(0); - let mut t = MockTransport::scripted(replies, Reply::TransportFault); - assert_eq!(find_vid_addr(&mut t).unwrap(), Some(0x2aa2)); - assert_eq!(t.calls(), 5); - assert!(t.cdbs.iter().all(|cdb| cdb.first() == Some(&0x3c))); - } - - #[test] - fn discovery_failures_stop_and_short_reads_retry_boundedly() { - for step in 0..5 { - let mut replies = script([0x25; 16]); - replies.remove(0); - replies[step] = Reply::TransportFault; - let mut t = MockTransport::scripted(replies, Reply::TransportFault); - assert_eq!(find_vid_addr(&mut t).unwrap_err(), UnlockError::Transport); - assert_eq!(t.calls(), step + 1); - let mut replies = script([0x25; 16]); - replies.remove(0); - replies[step] = Reply::illegal_request(); - assert_eq!( - find_vid_addr(&mut MockTransport::scripted(replies, Reply::TransportFault)) - .unwrap(), - None - ); - } - let mut replies = script([0x25; 16]); - replies.remove(0); - replies.insert(1, Reply::short(vec![0; CHUNK], 1)); - let mut t = MockTransport::scripted(replies, Reply::TransportFault); - assert!(find_vid_addr(&mut t).unwrap().is_some()); - assert_eq!(t.calls(), 6); - let mut replies = script([0x25; 16]); - replies.remove(0); - replies.splice( - 1..2, - [ - Reply::zero_transfer(CHUNK), - Reply::zero_transfer(CHUNK), - Reply::zero_transfer(CHUNK), + fn reads_fresh_vid_with_status_mask_and_exact_wire_commands() { + let mut t = MockTransport::scripted( + vec![ + Reply::good(vec![3]), + Reply::good(vec![0x25; 16]), + Reply::good(vec![7]), + Reply::good(vec![0x42; 16]), ], + Reply::TransportFault, ); - let mut t = MockTransport::scripted(replies, Reply::TransportFault); - assert_eq!(find_vid_addr(&mut t).unwrap(), None); - assert_eq!(t.calls(), 4); - } - fn with_identity(replies: Vec) -> MockTransport { - let mut identity = vec![0; RB_F1_LEN]; - identity[16..19].copy_from_slice(b"SAT"); - let mut all = vec![Reply::good(identity)]; - all.extend(replies); - MockTransport::scripted(all, Reply::TransportFault) - } - - #[test] - fn oem_unlock_survives_rejection_at_every_optional_step() { - let id = crate::DriveId::default(); - let ctx = UnlockCtx::new(&id, crate::DiscKind::Unknown); - for step in 0..7 { - for refusal in [Reply::illegal_request(), Reply::illegal_request_as_err()] { - let mut replies = script([0x25; 16]); - replies[step] = refusal; - let mut t = with_identity(replies); - let out = Renesas::new() - .unlock(&mut t, &ctx) - .unwrap() - .expect("OEM unlocked"); - assert_eq!(out.vid, None); - assert_eq!(out.bus_key, None); - assert_eq!(t.calls(), step + 2, "no commands after rejection at {step}"); - } - } - for vid in [[0; 16], [0xff; 16]] { - let out = Renesas::new() - .unlock(&mut with_identity(script(vid)), &ctx) - .unwrap() - .unwrap(); - assert_eq!(out.vid, None); - } - } - - #[test] - fn transport_faults_abort_full_unlock_at_every_optional_step() { - let id = crate::DriveId::default(); - let ctx = UnlockCtx::new(&id, crate::DiscKind::Unknown); - for step in 0..7 { - let mut replies = script([0x25; 16]); - replies[step] = Reply::TransportFault; - let mut t = with_identity(replies); - assert_eq!( - Renesas::new().unlock(&mut t, &ctx).unwrap_err(), - UnlockError::Transport - ); - assert_eq!(t.calls(), step + 2); - } - } - - #[test] - fn cancellation_during_firmware_read_prevents_further_commands() { - use crate::scsi::mock::StopFake; - let id = crate::DriveId::default(); - let ctx = UnlockCtx::new(&id, crate::DiscKind::Unknown); - let mut t = StopFake::new(with_identity(script([0x25; 16]))); - t.cancel_after = - Some(|cdb| cdb == pioneer_optical::cdb::read_memory(WINDOW_START, CHUNK as u32)); + assert_eq!(get_vid(&mut t).unwrap(), Some([0x25; 16])); + assert_eq!(get_vid(&mut t).unwrap(), Some([0x42; 16])); assert_eq!( - Renesas::new().unlock(&mut t, &ctx).unwrap_err(), - UnlockError::Transport + t.cdbs, + vec![ + vec![0x3c, 2, 0x92, 0, 0x0d, 0x3c, 0, 0, 1, 0], + vec![0x3c, 2, 0x92, 0, 0x0d, 0x20, 0, 0, 16, 0], + VID_STATUS_CDB.to_vec(), + VID_CDB.to_vec(), + ] ); - assert_eq!(t.inner.calls(), 4); } #[test] - fn invalid_headers_never_read_firmware_and_preserve_oem_unlock() { - let id = crate::DriveId::default(); - let ctx = UnlockCtx::new(&id, crate::DiscKind::Unknown); - for length in [0, 0x169f00, 0x16a001, 0x3f0100, u32::MAX] { - let mut replies = script([0x25; 16]); - let mut header = vec![0; 24]; - header[..8].copy_from_slice(b"PIONEER "); - header[20..].copy_from_slice(&length.to_be_bytes()); - replies[1] = Reply::good(header); - let mut t = with_identity(replies); - assert!( - Renesas::new() - .unlock(&mut t, &ctx) - .unwrap() - .unwrap() - .vid - .is_none() - ); - assert_eq!(t.calls(), 3); + fn invalid_status_stops_before_vid() { + for reply in [ + Reply::good(vec![0]), + Reply::good(vec![1]), + Reply::zero_transfer(1), + Reply::short(vec![2], 2), + Reply::illegal_request(), + Reply::illegal_request_as_err(), + ] { + let mut t = MockTransport::scripted(vec![reply], Reply::TransportFault); + assert_eq!(get_vid(&mut t).unwrap(), None); + assert_eq!(t.calls(), 1); } } #[test] - fn missing_or_ambiguous_signature_preserves_oem_unlock_without_ram_read() { - let id = crate::DriveId::default(); - let ctx = UnlockCtx::new(&id, crate::DiscKind::Unknown); - let mut ambiguous = fixture(0x2aa2); - ambiguous.copy_within(CHUNK - 20..CHUNK + 100, 1000); - for code in [vec![0; WINDOW_LEN], ambiguous] { - let mut replies = script([0x25; 16]); - for (i, chunk) in code.chunks(CHUNK).enumerate() { - replies[i + 2] = Reply::good(chunk.to_vec()); - } - let mut t = with_identity(replies); - assert!( - Renesas::new() - .unlock(&mut t, &ctx) - .unwrap() - .unwrap() - .vid - .is_none() - ); - assert_eq!(t.calls(), 7); - } - } - - #[test] - fn identity_rejects_overreported_transfer() { - let mut identity = vec![0; RB_F1_LEN]; + fn unlock_preserves_oem_success_on_rejections_and_aborts_transport_faults() { + let mut identity = vec![0; 48]; identity[16..19].copy_from_slice(b"SAT"); - let mut t = MockTransport::always(Reply::short(identity, RB_F1_LEN + 1)); - assert!(!is_renesas(&mut t).unwrap()); + let id = crate::DriveId::default(); + let ctx = UnlockCtx::new(&id, crate::DiscKind::Unknown); + for step in 1..=2 { + for reply in [ + Reply::illegal_request(), + Reply::illegal_request_as_err(), + Reply::TransportFault, + ] { + let fault = matches!(reply, Reply::TransportFault); + let mut replies = vec![ + Reply::good(identity.clone()), + Reply::good(vec![2]), + Reply::good(vec![0x25; 16]), + ]; + replies[step] = reply; + let mut t = MockTransport::scripted(replies, Reply::TransportFault); + let result = Renesas::new().unlock(&mut t, &ctx); + if fault { + assert_eq!(result.unwrap_err(), UnlockError::Transport); + } else { + let out = result.unwrap().unwrap(); + assert_eq!(out.vid, None); + assert_eq!(out.bus_key, None); + } + assert_eq!(t.calls(), step + 1); + } + } + let mut t = MockTransport::scripted( + vec![ + Reply::good(identity), + Reply::good(vec![2]), + Reply::good(vec![0x25; 16]), + ], + Reply::TransportFault, + ); + assert_eq!( + Renesas::new().unlock(&mut t, &ctx).unwrap().unwrap().vid, + Some([0x25; 16]) + ); + assert_eq!(t.calls(), 3); + assert!(t.cdbs.iter().all(|cdb| cdb[0] == 0x3c)); + } + + #[test] + fn cancellation_after_status_prevents_vid_command() { + use crate::scsi::mock::StopFake; + let mut t = StopFake::new(MockTransport::always(Reply::good(vec![2]))); + t.cancel_after = Some(|cdb| cdb == VID_STATUS_CDB); + assert_eq!(get_vid(&mut t).unwrap_err(), UnlockError::Transport); + assert_eq!(t.inner.calls(), 1); } }