Files
freemkv-unlock/src/firmware/mod_tests.rs
T
Matthew Jackson e107c699d1 firmware: retire Ake+Bus into one Encryption lever (fw 0.9.0 ABI)
Firmware 0.9.0 consolidates the Ake (0x06) and Bus (0x07) features into a
single Encryption (0x06) lever; wire id 0x07 no longer exists. The separate
Bus lever was proven inert on BU40N/MT1959 — the one Encryption lever
de-busses on its own — so the unlock recipe now sets one flag where it used
to set two, and the trailing best-effort Set(Bus, off) is gone.

Set(Encryption, STATE_OFF) is the load-bearing step: the drive acts
pre-authenticated, so a bare 0xAD returns the VID with no cert and no AKE,
and content comes back de-bussed. Failure still maps to VidUnavailable.

Hardware-validated against fw 0.9.0 on BU40N 1.00 with a UHD disc: every
sampled unit returns de-bussed and opens with the AACS unit key resolved
from the live keyserver.
2026-09-22 20:32:22 -07:00

626 lines
22 KiB
Rust

//! Tests for the firmware vendor-command mirror + [`FirmwareControl`].
use super::*;
use crate::scsi::mock::{MockTransport, Reply};
use crate::scsi::{DataDirection, Result as ScsiTResult, ScsiResult, ScsiTransport};
// ── Pinned-value drift guards (vs. freemkv-fw/src/abi.rs) ────────────────────
#[test]
fn verb_values_match_abi() {
assert_eq!(Verb::Identity as u8, 0x01);
assert_eq!(Verb::Set as u8, 0x02);
assert_eq!(Verb::Get as u8, 0x03);
assert_eq!(Verb::Reset as u8, 0x04);
assert_eq!(Verb::DumpAll as u8, 0x09);
assert_eq!(Verb::FlashWrite as u8, 0x0A);
assert_eq!(Verb::Save as u8, 0x0B);
assert_eq!(Verb::Call as u8, 0x0C);
assert_eq!(Verb::Poke as u8, 0x0D);
assert_eq!(Verb::Reboot as u8, 0x0F);
}
#[test]
fn feature_values_match_abi() {
assert_eq!(Feature::Speed as u8, 0x01);
assert_eq!(Feature::Region as u8, 0x02);
assert_eq!(Feature::Uhd as u8, 0x03);
assert_eq!(Feature::Bd as u8, 0x04);
assert_eq!(Feature::Hrl as u8, 0x05);
assert_eq!(Feature::Encryption as u8, 0x06);
// Wire id 0x07 (was `Bus`) is retired — proven inert on BU40N/MT1959.
assert_eq!(ALL_FEATURES.len(), 6);
}
#[test]
fn debug_knock_distinct_from_safe_knock() {
// The fw dispatches Call/Poke ONLY under DEBUG_KNOCK; a typo of a safe verb
// vs a debug verb under the wrong knock never crosses the line.
assert_ne!(KNOCK, DEBUG_KNOCK);
assert_eq!(DEBUG_KNOCK, [0xDE, 0xB9]);
}
#[test]
fn state_and_frame_constants_match_abi() {
assert_eq!(READ_BUFFER_OPCODE, 0x3C);
assert_eq!(KNOCK_MODE, 0x0E);
assert_eq!(KNOCK, [0xC0, 0xDE]);
assert_eq!(RESP_MAGIC, b"freemkv");
assert_eq!(CDB_LEN, 10);
assert_eq!(MEMREAD_LEN, 64);
assert_eq!(MIN_ALLOC_LEN, 64);
assert_eq!(STATE_PASSTHROUGH, 0xFF);
assert_eq!(STATE_OFF, 0x00);
assert_eq!(STATE_ON, 0x01);
assert_eq!(SPEED_MAX, 0x00);
assert_eq!([REGION_BD_A, REGION_BD_B, REGION_BD_C], [0x0A, 0x0B, 0x0C]);
assert_eq!(REGION_DVD_BASE, 0x00);
assert_eq!(REGION_FREE, 0x0F);
assert_eq!(RESET_TO_FLASH, 0x00);
assert_eq!(RESET_TO_DEFAULTS, 0x01);
assert_eq!(RESET_TO_OEM, 0xFF);
}
// ── CDB builders ─────────────────────────────────────────────────────────────
#[test]
fn build_set_cdb_exact_bytes() {
// SET Encryption = ON: verb 02, feature 06, state 01, alloc MIN_ALLOC_LEN
// (0x0040) at cdb[7..9] big-endian — the drive aborts a sub-16-byte data-in.
assert_eq!(
build_set_cdb(Feature::Encryption, STATE_ON),
[0x3C, 0x0E, 0xC0, 0xDE, 0x02, 0x06, 0x01, 0x00, 0x40, 0x00]
);
}
#[test]
fn build_get_cdb_exact_bytes() {
// GET Encryption: verb 03, feature 06, alloc MIN_ALLOC_LEN (0x0040) at
// cdb[7..9] big-endian — the drive aborts a 1-byte data-in; state read
// from offset 0.
assert_eq!(
build_get_cdb(Feature::Encryption),
[0x3C, 0x0E, 0xC0, 0xDE, 0x03, 0x06, 0x00, 0x00, 0x40, 0x00]
);
}
#[test]
fn vendor_commands_floor_alloc_len_at_min_for_hw() {
// HW-confirmed: the drive aborts a vendor command with a sub-16-byte data-in
// allocation, so SET/GET/RESET all request at least MIN_ALLOC_LEN (= 64,
// comfortably above the ~16-byte hardware floor).
assert_eq!(MIN_ALLOC_LEN, 64);
let alloc =
|cdb: &[u8; CDB_LEN]| u16::from_be_bytes([cdb[CDB_ALLOC_LEN], cdb[CDB_ALLOC_LEN + 1]]);
assert_eq!(
alloc(&build_set_cdb(Feature::Encryption, STATE_ON)),
MIN_ALLOC_LEN
);
assert_eq!(alloc(&build_get_cdb(Feature::Encryption)), MIN_ALLOC_LEN);
assert_eq!(alloc(&build_reset_cdb(RESET_TO_OEM)), MIN_ALLOC_LEN);
assert_eq!(alloc(&build_save_cdb()), MIN_ALLOC_LEN);
}
#[test]
fn build_call_and_poke_cdb_exact_bytes() {
// Call: verb 0x0C at cdb[4], DEBUG_KNOCK at cdb[2..4], target big-endian
// in cdb[5..9], r0 in cdb[9].
assert_eq!(
build_call_cdb(0x0102_0304, 0xAB),
[0x3C, 0x0E, 0xDE, 0xB9, 0x0C, 0x01, 0x02, 0x03, 0x04, 0xAB]
);
// Poke: verb 0x0D at cdb[4], DEBUG_KNOCK at cdb[2..4], target big-endian
// in cdb[5..9], value in cdb[9].
assert_eq!(
build_poke_cdb(0x0200_0E40, 0xAA),
[0x3C, 0x0E, 0xDE, 0xB9, 0x0D, 0x02, 0x00, 0x0E, 0x40, 0xAA]
);
// Reboot: verb 0x0F at cdb[4], DEBUG_KNOCK at cdb[2..4], no on-wire target/arg
// (the boot-function-entry VA is baked into the emitted handler at build time).
// Alloc floors at MIN_ALLOC_LEN (0x0040 big-endian at cdb[7..9]) so the drive
// does not abort the data-in before the verb executes.
assert_eq!(
build_reboot_cdb(),
[0x3C, 0x0E, 0xDE, 0xB9, 0x0F, 0x00, 0x00, 0x00, 0x40, 0x00]
);
}
#[test]
fn build_reset_and_identity_cdb_exact_bytes() {
// RESET carries its mode in the state slot (cdb[6]) and floors its data-in at
// MIN_ALLOC_LEN (0x0040) for the same HW reason: to-OEM = 0xFF, to-flash = 0x00.
assert_eq!(
build_reset_cdb(RESET_TO_OEM),
[0x3C, 0x0E, 0xC0, 0xDE, 0x04, 0x00, 0xFF, 0x00, 0x40, 0x00]
);
assert_eq!(
build_reset_cdb(RESET_TO_FLASH),
[0x3C, 0x0E, 0xC0, 0xDE, 0x04, 0x00, 0x00, 0x00, 0x40, 0x00]
);
// SAVE takes no feature/state and floors its data-in at MIN_ALLOC_LEN.
assert_eq!(
build_save_cdb(),
[0x3C, 0x0E, 0xC0, 0xDE, 0x0B, 0x00, 0x00, 0x00, 0x40, 0x00]
);
// IDENTITY with a 64-byte allocation (0x0040 big-endian at cdb[7..9]).
assert_eq!(
build_identity_cdb(MEMREAD_LEN as u16),
[0x3C, 0x0E, 0xC0, 0xDE, 0x01, 0x00, 0x00, 0x00, 0x40, 0x00]
);
}
#[test]
fn build_cdb_encodes_alloc_len_16bit_big_endian() {
let cdb = build_cdb(Verb::Identity, None, None, 0x1234);
assert_eq!([cdb[CDB_ALLOC_LEN], cdb[CDB_ALLOC_LEN + 1]], [0x12, 0x34]);
}
#[test]
fn build_memread_cdb_packs_address_big_endian_at_5_to_9() {
let cdb = build_memread_cdb(0xDEAD_BEEF);
assert_eq!(cdb[CDB_VERB], Verb::DumpAll as u8);
assert_eq!([cdb[5], cdb[6], cdb[7], cdb[8]], [0xDE, 0xAD, 0xBE, 0xEF]);
assert_eq!(cdb[9], 0x00);
}
#[test]
fn region_state_bytes() {
assert_eq!(BdRegion::A.state(), 0x0A);
assert_eq!(BdRegion::B.state(), 0x0B);
assert_eq!(BdRegion::C.state(), 0x0C);
}
// ── Identity / states parsing ────────────────────────────────────────────────
#[test]
fn identity_parse_reads_version_banner() {
// The firmware answers IDENTITY with the ASCII banner `freemkv <ver>`,
// NUL-padded — there is no binary state table.
let mut payload = Vec::new();
payload.extend_from_slice(RESP_MAGIC);
payload.extend_from_slice(b" 0.7.1\0");
payload.resize(64, 0); // NUL-pad to the allocation window
let id = FirmwareIdentity::parse(&payload).expect("has magic");
assert_eq!(id.version, "0.7.1");
}
#[test]
fn identity_parse_none_without_magic() {
assert!(FirmwareIdentity::parse(&[0u8; 64]).is_none());
}
// ── A stateful firmware mock ─────────────────────────────────────────────────
/// A mock freemkv drive: answers IDENTITY with the `freemkv <ver>` banner, GET
/// with the requested feature's state, applies SET, and RESET → all-passthrough.
/// Records every CDB so tests can assert the exact command order.
///
/// It also models the real hardware constraint: any vendor verb executed with
/// [`DataDirection::None`] (no data-in phase) is REJECTED with a non-zero
/// status, exactly as the drive's READ BUFFER hijack aborts a no-data verb.
struct FwMock {
version: &'static str,
states: FeatureStates,
/// The saved flash config block: SAVE snapshots `states` into it, RESET
/// with RESET_TO_FLASH reloads it, RESET_TO_OEM forces all-passthrough.
saved: FeatureStates,
is_freemkv: bool,
cdbs: Vec<Vec<u8>>,
}
impl FwMock {
fn new() -> Self {
FwMock {
version: "0.6.6",
states: FeatureStates::all_passthrough(),
saved: FeatureStates::all_passthrough(),
is_freemkv: true,
cdbs: Vec::new(),
}
}
fn feature_of(id: u8) -> Feature {
*ALL_FEATURES
.iter()
.find(|f| **f as u8 == id)
.expect("known feature id")
}
/// The verbs issued, in order (cdb[4] of each recorded CDB).
fn verbs(&self) -> Vec<u8> {
self.cdbs.iter().map(|c| c[CDB_VERB]).collect()
}
}
impl ScsiTransport for FwMock {
fn execute(
&mut self,
cdb: &[u8],
dir: DataDirection,
data: &mut [u8],
_timeout_ms: u32,
) -> ScsiTResult<ScsiResult> {
self.cdbs.push(cdb.to_vec());
// Model the hardware: a vendor verb with no data-in phase is aborted.
// This is what makes the old `exec_none` SET/RESET path a test failure.
let is_vendor = cdb[CDB_OPCODE] == READ_BUFFER_OPCODE && cdb[CDB_MODE] == KNOCK_MODE;
if is_vendor && matches!(dir, DataDirection::None) {
return Ok(ScsiResult {
status: 0x02, // CHECK CONDITION — rejected
bytes_transferred: 0,
sense: [0u8; 32],
});
}
let mut transferred = 0usize;
match cdb[CDB_VERB] {
v if v == Verb::Identity as u8 => {
if self.is_freemkv {
// The `freemkv <ver>` banner, NUL-terminated.
let mut resp = Vec::new();
resp.extend_from_slice(RESP_MAGIC);
resp.push(b' ');
resp.extend_from_slice(self.version.as_bytes());
resp.push(0);
let n = resp.len().min(data.len());
data[..n].copy_from_slice(&resp[..n]);
transferred = n;
}
// non-freemkv: GOOD status but no magic (buffer stays zero)
if !self.is_freemkv {
transferred = data.len();
}
}
v if v == Verb::Get as u8 => {
let f = Self::feature_of(cdb[CDB_FEATURE]);
if !data.is_empty() {
data[0] = self.states.get(f);
transferred = 1;
}
}
v if v == Verb::Set as u8 => {
let f = Self::feature_of(cdb[CDB_FEATURE]);
self.states.set(f, cdb[CDB_STATE]);
}
v if v == Verb::Reset as u8 => {
// The mode rides in the state slot (cdb[6]): to-OEM forces
// all-passthrough, to-flash reloads the saved config block.
self.states = if cdb[CDB_STATE] == RESET_TO_FLASH {
self.saved
} else {
FeatureStates::all_passthrough()
};
}
v if v == Verb::Save as u8 => {
self.saved = self.states;
}
v if v == Verb::DumpAll as u8 => {
for (i, b) in data.iter_mut().enumerate() {
*b = i as u8;
}
transferred = data.len();
}
_ => {}
}
Ok(ScsiResult {
status: 0,
bytes_transferred: transferred,
sense: [0u8; 32],
})
}
}
// ── FirmwareControl behaviour ────────────────────────────────────────────────
#[test]
fn identity_detects_freemkv_and_issues_identity_cdb() {
let mut m = FwMock::new();
m.states.encryption = STATE_ON;
{
let mut fw = FirmwareControl::new(&mut m);
let id = fw.identity().expect("no fault").expect("is freemkv");
assert_eq!(id.version, "0.6.6");
// States no longer ride in IDENTITY — read them via GET.
assert_eq!(fw.get(Feature::Encryption).expect("get"), STATE_ON);
assert!(fw.is_freemkv().expect("no fault"));
}
assert_eq!(m.cdbs[0], build_identity_cdb(MEMREAD_LEN as u16));
}
#[test]
fn identity_none_on_non_freemkv() {
let mut m = FwMock::new();
m.is_freemkv = false;
let mut fw = FirmwareControl::new(&mut m);
assert!(fw.identity().expect("no fault").is_none());
assert!(!fw.is_freemkv().expect("no fault"));
}
#[test]
fn identity_none_when_drive_rejects() {
let mut t = MockTransport::always(Reply::illegal_request());
let mut fw = FirmwareControl::new(&mut t);
assert!(fw.identity().expect("rejection is not a fault").is_none());
}
#[test]
fn identity_transport_fault_propagates() {
let mut t = MockTransport::always(Reply::TransportFault);
let mut fw = FirmwareControl::new(&mut t);
assert_eq!(fw.identity().unwrap_err(), FirmwareError::Transport);
}
#[test]
fn get_and_set_roundtrip_issue_the_right_cdbs() {
let mut m = FwMock::new();
{
let mut fw = FirmwareControl::new(&mut m);
assert_eq!(fw.get(Feature::Encryption).expect("get"), STATE_PASSTHROUGH);
fw.set(Feature::Encryption, STATE_ON).expect("set");
assert_eq!(fw.get(Feature::Encryption).expect("get"), STATE_ON);
}
assert_eq!(m.cdbs[0], build_get_cdb(Feature::Encryption));
assert_eq!(m.cdbs[1], build_set_cdb(Feature::Encryption, STATE_ON));
assert_eq!(m.cdbs[2], build_get_cdb(Feature::Encryption));
}
#[test]
fn reset_sends_reset_cdb_and_clears_state() {
let mut m = FwMock::new();
m.states.encryption = STATE_ON;
{
let mut fw = FirmwareControl::new(&mut m);
fw.reset().expect("reset");
}
assert_eq!(m.cdbs[0], build_reset_cdb(RESET_TO_OEM));
assert_eq!(m.states, FeatureStates::all_passthrough());
}
#[test]
fn reset_to_defaults_sends_the_defaults_mode_cdb() {
let mut m = FwMock::new();
{
let mut fw = FirmwareControl::new(&mut m);
fw.reset_to_defaults().expect("reset to defaults");
}
assert_eq!(m.cdbs[0], build_reset_cdb(RESET_TO_DEFAULTS));
}
#[test]
fn save_then_reset_to_flash_restores_the_saved_value() {
// SET a feature, SAVE it, force all-passthrough with RESET(to-OEM), then
// RESET(to-flash) must reload the saved value (not passthrough).
let mut m = FwMock::new();
{
let mut fw = FirmwareControl::new(&mut m);
fw.set(Feature::Encryption, STATE_OFF).expect("set");
fw.save().expect("save");
fw.reset().expect("reset to OEM");
assert_eq!(fw.get(Feature::Encryption).expect("get"), STATE_PASSTHROUGH);
fw.reset_to_flash().expect("reset to flash");
assert_eq!(fw.get(Feature::Encryption).expect("get"), STATE_OFF);
}
// The verbs issued, in order: SET, SAVE, RESET, GET, RESET, GET.
assert_eq!(
m.verbs(),
vec![
Verb::Set as u8,
Verb::Save as u8,
Verb::Reset as u8,
Verb::Get as u8,
Verb::Reset as u8,
Verb::Get as u8,
]
);
assert_eq!(m.cdbs[1], build_save_cdb());
assert_eq!(m.cdbs[2], build_reset_cdb(RESET_TO_OEM));
assert_eq!(m.cdbs[4], build_reset_cdb(RESET_TO_FLASH));
}
#[test]
fn states_probes_every_feature_in_order() {
let mut m = FwMock::new();
m.states.speed = SPEED_MAX;
m.states.hrl = STATE_ON;
let states;
{
let mut fw = FirmwareControl::new(&mut m);
states = fw.states().expect("states");
}
assert_eq!(states.speed, SPEED_MAX);
assert_eq!(states.hrl, STATE_ON);
// Six GETs, one per feature, in ALL_FEATURES order (Bus id 0x07 retired).
assert_eq!(m.cdbs.len(), 6);
let features: Vec<u8> = m.cdbs.iter().map(|c| c[CDB_FEATURE]).collect();
assert_eq!(features, vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
assert!(m.verbs().iter().all(|&v| v == Verb::Get as u8));
}
#[test]
fn dump_issues_memread_and_returns_the_window() {
let mut m = FwMock::new();
let got;
{
let mut fw = FirmwareControl::new(&mut m);
got = fw.dump(0x0102_0304).expect("dump");
}
assert_eq!(got[0], 0x00);
assert_eq!(got[63], 63);
assert_eq!(m.cdbs[0], build_memread_cdb(0x0102_0304));
}
// ── Typed setters ────────────────────────────────────────────────────────────
#[test]
fn typed_setters_issue_expected_set_cdbs() {
let mut m = FwMock::new();
{
let mut fw = FirmwareControl::new(&mut m);
fw.enable_uhd().unwrap();
fw.disable_bd().unwrap();
fw.skip_hrl().unwrap();
fw.disable_encryption().unwrap();
fw.region_free().unwrap();
fw.force_region_bd(BdRegion::B).unwrap();
fw.force_region_dvd(2).unwrap();
fw.unlock_speed().unwrap();
}
assert_eq!(m.cdbs[0], build_set_cdb(Feature::Uhd, STATE_ON));
assert_eq!(m.cdbs[1], build_set_cdb(Feature::Bd, STATE_OFF));
// The HRL/Encryption unlock direction is STATE_OFF (0x00), not STATE_ON.
assert_eq!(m.cdbs[2], build_set_cdb(Feature::Hrl, STATE_OFF));
assert_eq!(m.cdbs[3], build_set_cdb(Feature::Encryption, STATE_OFF));
// Region-free is REGION_FREE (0x0F) — 0x01 now forces DVD region 1.
assert_eq!(m.cdbs[4], build_set_cdb(Feature::Region, REGION_FREE));
assert_eq!(m.cdbs[5], build_set_cdb(Feature::Region, REGION_BD_B));
// force_region_dvd(2) = REGION_DVD_BASE + 2 = 0x02 under the new base.
assert_eq!(m.cdbs[6], build_set_cdb(Feature::Region, 0x02));
assert_eq!(m.cdbs[7], build_set_cdb(Feature::Speed, SPEED_MAX));
}
#[test]
fn force_region_dvd_rejects_out_of_range() {
let mut m = FwMock::new();
let mut fw = FirmwareControl::new(&mut m);
assert_eq!(fw.force_region_dvd(0).unwrap_err(), FirmwareError::Rejected);
assert_eq!(fw.force_region_dvd(9).unwrap_err(), FirmwareError::Rejected);
}
// ── Recipes ──────────────────────────────────────────────────────────────────
#[test]
fn arm_oem_bd_sets_and_verifies_hrl_skip() {
let mut m = FwMock::new();
{
let mut fw = FirmwareControl::new(&mut m);
fw.arm_oem_bd().expect("armed");
}
// SET Hrl=OFF (the migrated skip direction), then GET Hrl to verify.
assert_eq!(m.cdbs[0], build_set_cdb(Feature::Hrl, STATE_OFF));
assert_eq!(m.cdbs[1], build_get_cdb(Feature::Hrl));
assert_eq!(m.states.hrl, STATE_OFF);
}
#[test]
fn arm_oem_uhd_sets_uhd_hrl_encryption_each_verified() {
let mut m = FwMock::new();
{
let mut fw = FirmwareControl::new(&mut m);
fw.arm_oem_uhd().expect("armed");
}
let verbs = m.verbs();
// set,get, set,get, set,get
assert_eq!(
verbs,
vec![0x02, 0x03, 0x02, 0x03, 0x02, 0x03],
"each set is verified by a get"
);
assert_eq!(m.cdbs[0], build_set_cdb(Feature::Uhd, STATE_ON));
assert_eq!(m.cdbs[2], build_set_cdb(Feature::Hrl, STATE_OFF));
assert_eq!(m.cdbs[4], build_set_cdb(Feature::Encryption, STATE_OFF));
assert_eq!(m.states.uhd, STATE_ON);
assert_eq!(m.states.hrl, STATE_OFF);
assert_eq!(m.states.encryption, STATE_OFF);
}
#[test]
fn arm_bypass_bd_disables_encryption() {
let mut m = FwMock::new();
{
let mut fw = FirmwareControl::new(&mut m);
fw.arm_bypass_bd().expect("armed");
}
// Hrl=off (revocation skip) is set+verified FIRST, then Encryption=off.
assert_eq!(m.cdbs[0], build_set_cdb(Feature::Hrl, STATE_OFF));
assert_eq!(m.cdbs[1], build_get_cdb(Feature::Hrl));
assert_eq!(m.cdbs[2], build_set_cdb(Feature::Encryption, STATE_OFF));
assert_eq!(m.cdbs[3], build_get_cdb(Feature::Encryption));
assert_eq!(m.states.hrl, STATE_OFF);
assert_eq!(m.states.encryption, STATE_OFF);
}
#[test]
fn arm_bypass_uhd_sets_uhd_hrl_encryption() {
let mut m = FwMock::new();
{
let mut fw = FirmwareControl::new(&mut m);
fw.arm_bypass_uhd().expect("armed");
}
// Recipe order: Uhd=on, Hrl=off, Encryption=off (each SET then verifying GET).
assert_eq!(m.cdbs[0], build_set_cdb(Feature::Uhd, STATE_ON));
assert_eq!(m.cdbs[2], build_set_cdb(Feature::Hrl, STATE_OFF));
assert_eq!(m.cdbs[4], build_set_cdb(Feature::Encryption, STATE_OFF));
assert_eq!(m.states.uhd, STATE_ON);
assert_eq!(m.states.hrl, STATE_OFF);
assert_eq!(m.states.encryption, STATE_OFF);
}
#[test]
fn arm_stealth_oem_resets_then_verifies_all_passthrough() {
let mut m = FwMock::new();
m.states.encryption = STATE_ON;
m.states.uhd = STATE_ON;
{
let mut fw = FirmwareControl::new(&mut m);
fw.arm_stealth_oem().expect("disarmed");
}
assert_eq!(m.cdbs[0], build_reset_cdb(RESET_TO_OEM));
// then a GET of every feature
assert_eq!(m.cdbs.len(), 1 + ALL_FEATURES.len());
assert!(m.cdbs[1..].iter().all(|c| c[CDB_VERB] == Verb::Get as u8));
assert_eq!(m.states, FeatureStates::all_passthrough());
}
#[test]
fn arm_by_recipe_enum_dispatches() {
let mut m = FwMock::new();
{
let mut fw = FirmwareControl::new(&mut m);
fw.arm(ArmRecipe::BypassBd).expect("armed");
}
assert_eq!(m.states.encryption, STATE_OFF);
}
#[test]
fn set_verify_reports_mismatch() {
// A drive that ACKs the SET but never actually changes the state: GET reads
// back the old value, so the verify fails.
struct StuckDrive;
impl ScsiTransport for StuckDrive {
fn execute(
&mut self,
cdb: &[u8],
_dir: DataDirection,
data: &mut [u8],
_timeout_ms: u32,
) -> ScsiTResult<ScsiResult> {
let mut n = 0;
if cdb[CDB_VERB] == Verb::Get as u8 && !data.is_empty() {
data[0] = STATE_PASSTHROUGH; // never changes
n = 1;
}
Ok(ScsiResult {
status: 0,
bytes_transferred: n,
sense: [0u8; 32],
})
}
}
let mut d = StuckDrive;
let mut fw = FirmwareControl::new(&mut d);
let err = fw.arm_bypass_bd().unwrap_err();
// arm_bypass_bd now verifies Hrl=off FIRST, so the stuck drive trips on Hrl.
assert_eq!(
err,
FirmwareError::VerifyFailed {
feature: Feature::Hrl,
wanted: STATE_OFF,
got: STATE_PASSTHROUGH,
}
);
}
#[test]
fn recipe_transport_fault_propagates() {
let mut t = MockTransport::always(Reply::TransportFault);
let mut fw = FirmwareControl::new(&mut t);
assert_eq!(fw.arm_oem_bd().unwrap_err(), FirmwareError::Transport);
}