mirror of
https://github.com/freemkv/freemkv-unlock.git
synced 2026-10-07 18:13:11 +00:00
Firmware recipes and the freemkv unlock run only for a supported IDENTITY, with an accurate fw 0.7/0.8 rationale and reply shape. css and renesas share is_dead_bus; no P-256 retry is claimed for 0x11 drive certs; handshake diagnostics log below WARN; tests pin the error variants, the v2 cert gate and the 132-byte v2 boundary. CI: doc-links, doctests, a consumers job, read-only token; rand 0.10.3.
867 lines
38 KiB
Rust
867 lines
38 KiB
Rust
//! Typed mirror of the freemkv firmware vendor-command ABI.
|
|
//!
|
|
//! Numeric values must match `freemkv-firmware/crates/freemkv-fw/src/abi.rs`.
|
|
//! Tests pin the verbs, features, state bytes and CDB offsets.
|
|
//!
|
|
//! ```text
|
|
//! cdb[0] = 0x3C (READ BUFFER) ← standard opcode; bridge-safe
|
|
//! cdb[1] = 0x0E (KNOCK_MODE) ← OEM's jump table rejects modes >= 0x0E
|
|
//! cdb[2..4] = 0xC0 0xDE (KNOCK) ← defence-in-depth signature
|
|
//! cdb[4] = Verb ← IDENTITY / SET / GET / RESET / DUMPALL
|
|
//! cdb[5] = Feature id ← SET/GET only (else 0)
|
|
//! cdb[6] = State byte ← SET only (else 0)
|
|
//! cdb[7..9] = allocation length (16-bit big-endian) ← data-returning verbs
|
|
//! cdb[9] = control (0)
|
|
//! ```
|
|
//!
|
|
//! DumpAll instead encodes a 32-bit address in `cdb[5..9]` and returns MEMREAD_LEN bytes.
|
|
//! Features default to passthrough. Reset selects OEM, built-in defaults or saved flash state.
|
|
|
|
use crate::scsi::{DataDirection, ScsiTransport, is_dead_bus};
|
|
|
|
// ── Wire constants (mirror of abi.rs) ────────────────────────────────────────
|
|
|
|
/// Standard SCSI `READ BUFFER` opcode — the command freemkv hijacks (`cdb[0]`).
|
|
pub const READ_BUFFER_OPCODE: u8 = 0x3C;
|
|
/// The freemkv sub-command mode at `cdb[1]`; OEM rejects modes `>= 0x0E`.
|
|
pub const KNOCK_MODE: u8 = 0x0E;
|
|
/// Two-byte knock at `cdb[2..4]` ("C0DE") — the *safe* knock carried by every
|
|
/// durable verb (Identity/Set/Get/Reset/DumpAll/Save/FlashWrite).
|
|
pub const KNOCK: [u8; 2] = [0xC0, 0xDE];
|
|
/// Two-byte knock at `cdb[2..4]` — the *debug* knock, distinct from [`KNOCK`]
|
|
/// by construction. The only frame the fw honours for the debug-only verbs
|
|
/// ([`Verb::Call`], [`Verb::Poke`], [`Verb::Reboot`]).
|
|
pub const DEBUG_KNOCK: [u8; 2] = [0xDE, 0xB9];
|
|
/// Response-framing magic leading the [`Verb::Identity`] reply (`b"freemkv"`).
|
|
pub const RESP_MAGIC: &[u8] = b"freemkv";
|
|
|
|
/// Oldest firmware `(major, minor)` this mirror arms: 0.9 made wire id `0x06`
|
|
/// the consolidated `Encryption` lever. Released 0.8.x (`0x06` = `Ake`, separate
|
|
/// `0x07 Bus`) de-busses via `0x06` alone only as a hardware-dependent side
|
|
/// effect (not validated), and 0.7.x uses a sub-function grammar: both refused.
|
|
pub const MIN_FW_VERSION: (u32, u32) = (0, 9);
|
|
|
|
/// Length of a freemkv (READ BUFFER) CDB.
|
|
pub const CDB_LEN: usize = 10;
|
|
/// Offset of the opcode byte (`cdb[0]`).
|
|
pub const CDB_OPCODE: usize = 0;
|
|
/// Offset of the mode/knock byte (`cdb[1]`).
|
|
pub const CDB_MODE: usize = 1;
|
|
/// Offset of the first knock byte (`cdb[2]`, `cdb[3]`).
|
|
pub const CDB_KNOCK: usize = 2;
|
|
/// Offset of the verb byte (`cdb[4]`).
|
|
pub const CDB_VERB: usize = 4;
|
|
/// Offset of the feature-id byte (`cdb[5]`) — SET/GET only.
|
|
pub const CDB_FEATURE: usize = 5;
|
|
/// Offset of the state byte (`cdb[6]`) — SET only.
|
|
pub const CDB_STATE: usize = 6;
|
|
/// Offset of the 16-bit big-endian allocation length (`cdb[7..9]`).
|
|
pub const CDB_ALLOC_LEN: usize = 7;
|
|
|
|
/// Bytes returned by one [`Verb::DumpAll`] read (fixed 64-byte window).
|
|
pub const MEMREAD_LEN: usize = 64;
|
|
|
|
/// Minimum data-in allocation length any freemkv vendor command may request.
|
|
///
|
|
/// **Hardware-confirmed (LG BU40N on freemkv firmware):** the drive's `READ
|
|
/// BUFFER` hijack ABORTS (Check Condition, sense key Aborted Command) any vendor
|
|
/// command whose data-in allocation length is under ~16 bytes (0/1/2 all abort;
|
|
/// 16 and 64 both succeed). Every builder floors its allocation at `64` — it
|
|
/// matches [`MEMREAD_LEN`] and is safely above the minimum. The verb/feature/
|
|
/// state ride in the CDB, so a larger data-in is harmless; a [`Verb::Get`] still
|
|
/// reads its state byte from data offset 0. Mirrors firmware ABI `MIN_ALLOC_LEN`.
|
|
pub const MIN_ALLOC_LEN: u16 = 64;
|
|
|
|
/// Feature state: **passthrough** — firmware does not touch this subsystem
|
|
/// (OEM behaviour). The boot default of every feature and what [`Verb::Reset`]
|
|
/// restores. `0xFF`.
|
|
pub const STATE_PASSTHROUGH: u8 = 0xFF;
|
|
/// Feature state: explicit **off / disabled** (`0x00`).
|
|
pub const STATE_OFF: u8 = 0x00;
|
|
/// Feature state: explicit **on / enabled** (`0x01`).
|
|
pub const STATE_ON: u8 = 0x01;
|
|
|
|
/// [`Feature::Speed`] state: speed control OFF — the read-speed cap is lifted and
|
|
/// the drive runs uncapped at maximum throughput. This is the [`STATE_OFF`]
|
|
/// (`0x00`) leg of Speed: "off" is the *limiter* being off, i.e. maximum speed.
|
|
/// `0x01..=0xFE` are explicit caps and `0xFF` is the OEM ramp.
|
|
pub const SPEED_MAX: u8 = 0x00;
|
|
|
|
/// [`Feature::Region`] state: force BD region A (`0x0A`). See [`BdRegion`]. BD
|
|
/// regions A/B/C share the low-nibble block with the DVD `0x0N` scheme
|
|
/// ([`REGION_DVD_BASE`]) and [`REGION_FREE`] (`0x0F`).
|
|
pub const REGION_BD_A: u8 = 0x0A;
|
|
/// [`Feature::Region`] state: force BD region B (`0x0B`).
|
|
pub const REGION_BD_B: u8 = 0x0B;
|
|
/// [`Feature::Region`] state: force BD region C (`0x0C`).
|
|
pub const REGION_BD_C: u8 = 0x0C;
|
|
|
|
/// [`Feature::Region`] state base for "force DVD region N": `REGION_DVD_BASE + N`,
|
|
/// so regions 1..=8 are `0x01..=0x08`. `0x00` itself is region-locked (nothing
|
|
/// plays).
|
|
pub const REGION_DVD_BASE: u8 = 0x00;
|
|
|
|
/// [`Feature::Region`] state: region-free — any disc plays regardless of its
|
|
/// region code. Sits in the region low-nibble block above the DVD (`0x01..=0x08`)
|
|
/// and BD (`0x0A..=0x0C`) values.
|
|
pub const REGION_FREE: u8 = 0x0F;
|
|
|
|
/// [`Verb::Reset`] mode (rides in the state slot `cdb[6]`): reload the saved flash
|
|
/// config block into the RAM feature-state table, discarding un-saved RAM changes
|
|
/// — the non-destructive "revert to last SAVE". Marker-gated in firmware ≥0.8.3: a
|
|
/// never-saved drive loads the baked create-time defaults instead.
|
|
pub const RESET_TO_FLASH: u8 = 0x00;
|
|
|
|
/// [`Verb::Reset`] mode (rides in the state slot `cdb[6]`): restore the baked
|
|
/// per-create **defaults** (the create-time flags, e.g. UHD/BD on) into the RAM
|
|
/// feature-state table. RAM-only — does not touch flash. This is what a never-saved
|
|
/// drive boots to. Firmware ≥0.8.3.
|
|
pub const RESET_TO_DEFAULTS: u8 = 0x01;
|
|
|
|
/// [`Verb::Reset`] mode (rides in the state slot `cdb[6]`): TRUE OEM. Forces every
|
|
/// feature to [`STATE_PASSTHROUGH`] in RAM **and** (firmware ≥0.8.3) writes the NV
|
|
/// config block all-`0xFF` — marker included — so the drive is byte-for-byte like a
|
|
/// never-saved/never-configured drive (no trace it was ever set). Because the marker
|
|
/// is then `0xFF`, the NEXT boot loads the baked defaults, exactly like a fresh drive.
|
|
pub const RESET_TO_OEM: u8 = 0xFF;
|
|
|
|
/// The verb selector in `cdb[4]`. These numeric values ARE the wire protocol.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
#[repr(u8)]
|
|
pub enum Verb {
|
|
/// Status/ping — returns `freemkv <version>` (no NUL), then the raw
|
|
/// feature-flag bytes `flag[0x01..=0x06]`, zero-padded to the window.
|
|
Identity = 0x01,
|
|
/// Set one feature (`cdb[5]`) to a state (`cdb[6]`).
|
|
Set = 0x02,
|
|
/// Read one feature's (`cdb[5]`) current state back in the data-in payload.
|
|
Get = 0x03,
|
|
/// Restore the RAM feature-state table. The mode rides in `cdb[6]` (the state
|
|
/// slot): [`RESET_TO_FLASH`] (`0x00`) reloads the saved flash config block back
|
|
/// into RAM (discarding un-saved changes; marker-gated in fw ≥0.8.3),
|
|
/// [`RESET_TO_DEFAULTS`] (`0x01`) restores the baked create-time defaults, and
|
|
/// [`RESET_TO_OEM`] (`0xFF`) forces every feature to [`STATE_PASSTHROUGH`] AND
|
|
/// blanks the NV block (traceless, fw ≥0.8.3). Ignores feature.
|
|
Reset = 0x04,
|
|
/// Diagnostic RAM peek: [`MEMREAD_LEN`] bytes at the 32-bit address in
|
|
/// `cdb[5..9]` (big-endian).
|
|
DumpAll = 0x09,
|
|
/// **TEMPORARY** diagnostic probe: program a single byte to an allowlisted
|
|
/// flash offset via the OEM PROGRAM routine. `CDB[5..9]` = 32-bit flash offset
|
|
/// (big-endian); `CDB[9]` = value byte. The firmware refuses any offset outside
|
|
/// the compile-time allowlist window (`0x1EA000..0x1EB000` — the corpus-proven-
|
|
/// unlocked NV/SAVE block) with a distinct `"REFU"` status word (PROGRAM not
|
|
/// called). Reply: 4-byte offset echo (BE) + 4-byte PROGRAM/REFU status (BE).
|
|
/// Not part of the durable feature grammar; subsumed by [`Verb::Save`].
|
|
FlashWrite = 0x0A,
|
|
/// Persist the whole RAM feature-state table to the flash config block. This is
|
|
/// the ONLY verb that writes the config to flash: [`Verb::Set`] and
|
|
/// [`Verb::Reset`] touch RAM only, so a host's changes stay volatile until a
|
|
/// `Save` commits them (and survive a power cycle only once saved). Ignores
|
|
/// feature/state.
|
|
Save = 0x0B,
|
|
/// **Debug-knock only.** Interactive fw-exploration `blx target` primitive.
|
|
/// `CDB[5..9]` = 32-bit target VA (big-endian); `CDB[9]` = single u8 that
|
|
/// is loaded into r0 as the sole register argument. Refused under the safe
|
|
/// [`KNOCK`]; only executed under [`DEBUG_KNOCK`]. See [`build_call_cdb`].
|
|
Call = 0x0C,
|
|
/// **Debug-knock only.** Poke a single byte to an arbitrary address.
|
|
/// `CDB[5..9]` = 32-bit target address (big-endian, RAM or MMIO);
|
|
/// `CDB[9]` = byte value to store. Refused under the safe [`KNOCK`]; only
|
|
/// executed under [`DEBUG_KNOCK`]. See [`build_poke_cdb`].
|
|
Poke = 0x0D,
|
|
/// **Debug-knock only.** Invokes the firmware's boot function entry with
|
|
/// r0=4 to force the cold path (BSS clear + C-runtime data init + full
|
|
/// post-init), recovering a wedged drive without a power cycle. The target
|
|
/// briefly returns Aborted Command mid-reboot, then comes back ~5s later
|
|
/// with the safe-knock verb chain re-armed to its power-on defaults. The
|
|
/// target VA is baked into the handler at build time, so no CDB arguments
|
|
/// are carried beyond the verb byte. Refused under the safe [`KNOCK`];
|
|
/// only executed under [`DEBUG_KNOCK`]. See [`build_reboot_cdb`].
|
|
Reboot = 0x0F,
|
|
}
|
|
|
|
/// The feature selector in `cdb[5]` for [`Verb::Set`] / [`Verb::Get`]. These
|
|
/// numeric values ARE the wire protocol.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
#[repr(u8)]
|
|
pub enum Feature {
|
|
/// Read-speed / riplock ceiling. [`STATE_PASSTHROUGH`] (`0xFF`) = OEM ramp;
|
|
/// [`STATE_OFF`] (`0x00`) = speed control off, i.e. the cap is lifted and the
|
|
/// drive runs uncapped at maximum throughput (see [`SPEED_MAX`]); `0x01..=0xFE`
|
|
/// = an explicit speed cap (the byte IS the cap, lower is slower). Note the OFF
|
|
/// direction: "off" means the *limiter* is off, so OFF is the fastest state.
|
|
Speed = 0x01,
|
|
/// Region (RPC) control. [`STATE_PASSTHROUGH`] (`0xFF`) = drive's own region
|
|
/// logic; [`STATE_OFF`] (`0x00`) = region-locked (nothing plays);
|
|
/// [`REGION_DVD_BASE`]` + N` (`0x01..=0x08`) = force DVD region 1..8;
|
|
/// [`REGION_BD_A`]/`_B`/`_C` (`0x0A`/`0x0B`/`0x0C`) = force BD region A/B/C;
|
|
/// [`REGION_FREE`] (`0x0F`) = region-free (any disc plays).
|
|
Region = 0x02,
|
|
/// Unrestricted (BD/UHD) capability widen gate. [`STATE_PASSTHROUGH`]
|
|
/// (`0xFF`) = OEM; [`STATE_OFF`] (`0x00`) = No (currently no-op on
|
|
/// byte-extraction classifiers like BU40N, pending RE); [`STATE_ON`] (`0x01`)
|
|
/// = Yes. fw 0.9.2 folds BD into this gate by name only: its BD-refuse detour
|
|
/// still reads wire id `0x04` (`Bd`, deprecated), which this mirror does not
|
|
/// expose, so on 0.9.2 this lever governs UHD acceptance.
|
|
Unrestricted = 0x03,
|
|
/// Host Revocation List handling on the cert path. [`STATE_PASSTHROUGH`]
|
|
/// (`0xFF`) = OEM enforce; [`STATE_OFF`] (`0x00`) = off (skip the HRL lookup —
|
|
/// revoked certs accepted, non-destructive, the unlock direction); [`STATE_ON`]
|
|
/// (`0x01`) = on (enforce the HRL).
|
|
Hrl = 0x05,
|
|
/// The one master encryption/cert bypass. [`STATE_OFF`] (`0x00`) = every
|
|
/// drive-side encryption/cert requirement gone (cert-AKE relaxed, bus-wrap
|
|
/// off — what used to need a cert now doesn't). [`STATE_PASSTHROUGH`] (`0xFF`)
|
|
/// = OEM behavior (real handshake, bus-encrypted content). [`STATE_ON`]
|
|
/// (`0x01`) = on (require the real handshake). Wire id `0x07` is retired
|
|
/// (was `Bus`, empirically proved inert as a separate datapath lever on
|
|
/// BU40N/MT1959 — the single Encryption lever de-busses on its own).
|
|
Encryption = 0x06,
|
|
}
|
|
|
|
/// Every exposed feature, in wire-id order (the order [`FirmwareControl::states`]
|
|
/// probes). The IDENTITY flag table also carries the unexposed `0x04` slot.
|
|
pub const ALL_FEATURES: [Feature; 5] = [
|
|
Feature::Speed,
|
|
Feature::Region,
|
|
Feature::Unrestricted,
|
|
Feature::Hrl,
|
|
Feature::Encryption,
|
|
];
|
|
|
|
/// A Blu-ray region for [`FirmwareControl::force_region_bd`].
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum BdRegion {
|
|
/// Region A (`0x0A`).
|
|
A,
|
|
/// Region B (`0x0B`).
|
|
B,
|
|
/// Region C (`0x0C`).
|
|
C,
|
|
}
|
|
|
|
impl BdRegion {
|
|
/// The `Feature::Region` state byte that forces this BD region.
|
|
pub fn state(self) -> u8 {
|
|
match self {
|
|
BdRegion::A => REGION_BD_A,
|
|
BdRegion::B => REGION_BD_B,
|
|
BdRegion::C => REGION_BD_C,
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── CDB builders (mirror of abi.rs) ──────────────────────────────────────────
|
|
|
|
/// Build a 10-byte host CDB for a verb over the `3C 0E C0 DE …` frame.
|
|
///
|
|
/// `feature`/`state` land at `cdb[5]`/`cdb[6]` (0 when the verb ignores them);
|
|
/// `alloc_len` is the data-in buffer size, 16-bit big-endian at `cdb[7..9]`.
|
|
/// Use [`build_memread_cdb`] for [`Verb::DumpAll`] (different field layout).
|
|
pub fn build_cdb(
|
|
verb: Verb,
|
|
feature: Option<Feature>,
|
|
state: Option<u8>,
|
|
alloc_len: u16,
|
|
) -> [u8; CDB_LEN] {
|
|
let mut cdb = [0u8; CDB_LEN];
|
|
cdb[CDB_OPCODE] = READ_BUFFER_OPCODE;
|
|
cdb[CDB_MODE] = KNOCK_MODE;
|
|
cdb[CDB_KNOCK..CDB_KNOCK + 2].copy_from_slice(&KNOCK);
|
|
cdb[CDB_VERB] = verb as u8;
|
|
cdb[CDB_FEATURE] = feature.map(|f| f as u8).unwrap_or(0);
|
|
cdb[CDB_STATE] = state.unwrap_or(0);
|
|
cdb[CDB_ALLOC_LEN] = (alloc_len >> 8) as u8;
|
|
cdb[CDB_ALLOC_LEN + 1] = alloc_len as u8;
|
|
cdb
|
|
}
|
|
|
|
/// Build a `SET feature = state` CDB. Requests a [`MIN_ALLOC_LEN`]-byte data-in
|
|
/// (the drive aborts sub-16-byte transfers — HW-confirmed, see [`MIN_ALLOC_LEN`]);
|
|
/// the feature/state ride in the CDB, so the returned payload is ignored.
|
|
pub fn build_set_cdb(feature: Feature, state: u8) -> [u8; CDB_LEN] {
|
|
build_cdb(Verb::Set, Some(feature), Some(state), MIN_ALLOC_LEN)
|
|
}
|
|
|
|
/// Build a `GET feature` CDB. Requests a [`MIN_ALLOC_LEN`]-byte data-in (the
|
|
/// drive aborts a 1-byte transfer — HW-confirmed, see [`MIN_ALLOC_LEN`]); the
|
|
/// current state byte is read back from data offset 0.
|
|
pub fn build_get_cdb(feature: Feature) -> [u8; CDB_LEN] {
|
|
build_cdb(Verb::Get, Some(feature), None, MIN_ALLOC_LEN)
|
|
}
|
|
|
|
/// Build a `RESET` CDB. `mode` rides in the state slot (`cdb[6]`):
|
|
/// [`RESET_TO_FLASH`] (`0x00`) reloads the saved flash config into RAM,
|
|
/// [`RESET_TO_DEFAULTS`] (`0x01`) restores the baked create-time defaults, and
|
|
/// [`RESET_TO_OEM`] (`0xFF`) forces every feature to passthrough (and blanks NV).
|
|
/// Floors its data-in at [`MIN_ALLOC_LEN`] for the same HW min-transfer reason as
|
|
/// [`build_set_cdb`].
|
|
pub fn build_reset_cdb(mode: u8) -> [u8; CDB_LEN] {
|
|
build_cdb(Verb::Reset, None, Some(mode), MIN_ALLOC_LEN)
|
|
}
|
|
|
|
/// Build a `SAVE` CDB (persist the RAM feature-state table to the flash config
|
|
/// block — the only verb that writes config to flash). Floors its data-in at
|
|
/// [`MIN_ALLOC_LEN`] for the same HW min-transfer reason as [`build_reset_cdb`].
|
|
pub fn build_save_cdb() -> [u8; CDB_LEN] {
|
|
build_cdb(Verb::Save, None, None, MIN_ALLOC_LEN)
|
|
}
|
|
|
|
/// Build an `IDENTITY` CDB. `alloc_len` sizes the magic+version+state reply.
|
|
pub fn build_identity_cdb(alloc_len: u16) -> [u8; CDB_LEN] {
|
|
build_cdb(Verb::Identity, None, None, alloc_len)
|
|
}
|
|
|
|
/// Build a [`Verb::DumpAll`] CDB: read [`MEMREAD_LEN`] bytes at the 32-bit
|
|
/// `addr`, packed big-endian into `cdb[5..9]`.
|
|
pub fn build_memread_cdb(addr: u32) -> [u8; CDB_LEN] {
|
|
let mut cdb = [0u8; CDB_LEN];
|
|
cdb[CDB_OPCODE] = READ_BUFFER_OPCODE;
|
|
cdb[CDB_MODE] = KNOCK_MODE;
|
|
cdb[CDB_KNOCK..CDB_KNOCK + 2].copy_from_slice(&KNOCK);
|
|
cdb[CDB_VERB] = Verb::DumpAll as u8;
|
|
cdb[5] = (addr >> 24) as u8;
|
|
cdb[6] = (addr >> 16) as u8;
|
|
cdb[7] = (addr >> 8) as u8;
|
|
cdb[8] = addr as u8;
|
|
cdb
|
|
}
|
|
|
|
/// Build a 10-byte CDB for [`Verb::Call`]: `blx target(r0)` where `target` is
|
|
/// packed big-endian in `cdb[5..9]` and the u8 `r0` register argument rides in
|
|
/// `cdb[9]`. Carries the [`DEBUG_KNOCK`] at `cdb[2..4]` — the ONLY frame the
|
|
/// fw honours for Call. The handler ORs the thumb bit into the address at
|
|
/// runtime. r1..r3 are undefined at callee entry.
|
|
pub fn build_call_cdb(target: u32, r0: u8) -> [u8; CDB_LEN] {
|
|
let mut cdb = [0u8; CDB_LEN];
|
|
cdb[CDB_OPCODE] = READ_BUFFER_OPCODE;
|
|
cdb[CDB_MODE] = KNOCK_MODE;
|
|
cdb[CDB_KNOCK..CDB_KNOCK + 2].copy_from_slice(&DEBUG_KNOCK);
|
|
cdb[CDB_VERB] = Verb::Call as u8;
|
|
cdb[5] = (target >> 24) as u8;
|
|
cdb[6] = (target >> 16) as u8;
|
|
cdb[7] = (target >> 8) as u8;
|
|
cdb[8] = target as u8;
|
|
cdb[9] = r0;
|
|
cdb
|
|
}
|
|
|
|
/// Build a 10-byte CDB for [`Verb::Poke`]: write a single byte `val` to the
|
|
/// arbitrary 32-bit address `target` (RAM or MMIO). Target is packed
|
|
/// big-endian in `cdb[5..9]` and `val` rides in `cdb[9]`. Carries the
|
|
/// [`DEBUG_KNOCK`] at `cdb[2..4]`. NO bounds check on the address — this is a
|
|
/// diagnostic primitive for on-drive state discovery, not a durable verb.
|
|
pub fn build_poke_cdb(target: u32, val: u8) -> [u8; CDB_LEN] {
|
|
let mut cdb = [0u8; CDB_LEN];
|
|
cdb[CDB_OPCODE] = READ_BUFFER_OPCODE;
|
|
cdb[CDB_MODE] = KNOCK_MODE;
|
|
cdb[CDB_KNOCK..CDB_KNOCK + 2].copy_from_slice(&DEBUG_KNOCK);
|
|
cdb[CDB_VERB] = Verb::Poke as u8;
|
|
cdb[5] = (target >> 24) as u8;
|
|
cdb[6] = (target >> 16) as u8;
|
|
cdb[7] = (target >> 8) as u8;
|
|
cdb[8] = target as u8;
|
|
cdb[9] = val;
|
|
cdb
|
|
}
|
|
|
|
/// Build a 10-byte CDB for [`Verb::Reboot`]: force the firmware boot function's
|
|
/// cold path (soft-reboot the controller). Carries the [`DEBUG_KNOCK`] at
|
|
/// `cdb[2..4]`. NO arguments are transmitted on the wire — the target VA is
|
|
/// baked into the emitted handler at build time (per-image, resolved from the
|
|
/// boot-init signature). Requests a [`MIN_ALLOC_LEN`]-byte data-in like every
|
|
/// other durable-shape verb: the drive returns Aborted Command mid-reboot
|
|
/// anyway, so callers should tolerate a rejection on this send and re-probe
|
|
/// identity after a short delay.
|
|
pub fn build_reboot_cdb() -> [u8; CDB_LEN] {
|
|
let mut cdb = [0u8; CDB_LEN];
|
|
cdb[CDB_OPCODE] = READ_BUFFER_OPCODE;
|
|
cdb[CDB_MODE] = KNOCK_MODE;
|
|
cdb[CDB_KNOCK..CDB_KNOCK + 2].copy_from_slice(&DEBUG_KNOCK);
|
|
cdb[CDB_VERB] = Verb::Reboot as u8;
|
|
cdb[CDB_ALLOC_LEN] = (MIN_ALLOC_LEN >> 8) as u8;
|
|
cdb[CDB_ALLOC_LEN + 1] = MIN_ALLOC_LEN as u8;
|
|
cdb
|
|
}
|
|
|
|
/// Whether a device data response leads with [`RESP_MAGIC`].
|
|
pub fn verify_response(bytes: &[u8]) -> bool {
|
|
bytes.starts_with(RESP_MAGIC)
|
|
}
|
|
|
|
// ── Errors ───────────────────────────────────────────────────────────────────
|
|
|
|
/// Why a firmware command did not succeed.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum FirmwareError {
|
|
/// The drive rejected the command (CHECK CONDITION / vendor refusal). Not a
|
|
/// freemkv drive, or the feature/state is unsupported.
|
|
Rejected,
|
|
/// IDENTITY did not report freemkv firmware at or above [`MIN_FW_VERSION`];
|
|
/// a recipe was refused before any SET/RESET was sent.
|
|
UnsupportedFirmware,
|
|
/// A caller argument was out of range; nothing was sent to the drive.
|
|
InvalidArgument,
|
|
/// A verify-after-set (`SET` then `GET`) read back a state that did not
|
|
/// match what was written. Carries `(feature, wanted, got)`.
|
|
VerifyFailed {
|
|
feature: Feature,
|
|
wanted: u8,
|
|
got: u8,
|
|
},
|
|
/// A genuine SCSI transport fault (bus dead). Callers must abort.
|
|
Transport,
|
|
}
|
|
|
|
/// Result of a firmware command.
|
|
pub type Result<T> = std::result::Result<T, FirmwareError>;
|
|
|
|
// ── Parsed replies ───────────────────────────────────────────────────────────
|
|
|
|
/// The current state of every feature, indexed by [`Feature`].
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct FeatureStates {
|
|
/// [`Feature::Speed`] state.
|
|
pub speed: u8,
|
|
/// [`Feature::Region`] state.
|
|
pub region: u8,
|
|
/// [`Feature::Unrestricted`] state.
|
|
pub unrestricted: u8,
|
|
/// [`Feature::Hrl`] state.
|
|
pub hrl: u8,
|
|
/// [`Feature::Encryption`] state (the consolidated cert/bus bypass).
|
|
pub encryption: u8,
|
|
}
|
|
|
|
impl FeatureStates {
|
|
/// All features at [`STATE_PASSTHROUGH`] (the OEM / freshly-reset drive).
|
|
pub fn all_passthrough() -> Self {
|
|
FeatureStates {
|
|
speed: STATE_PASSTHROUGH,
|
|
region: STATE_PASSTHROUGH,
|
|
unrestricted: STATE_PASSTHROUGH,
|
|
hrl: STATE_PASSTHROUGH,
|
|
encryption: STATE_PASSTHROUGH,
|
|
}
|
|
}
|
|
|
|
/// The state byte for one feature.
|
|
pub fn get(&self, feature: Feature) -> u8 {
|
|
match feature {
|
|
Feature::Speed => self.speed,
|
|
Feature::Region => self.region,
|
|
Feature::Unrestricted => self.unrestricted,
|
|
Feature::Hrl => self.hrl,
|
|
Feature::Encryption => self.encryption,
|
|
}
|
|
}
|
|
|
|
fn set(&mut self, feature: Feature, state: u8) {
|
|
match feature {
|
|
Feature::Speed => self.speed = state,
|
|
Feature::Region => self.region = state,
|
|
Feature::Unrestricted => self.unrestricted = state,
|
|
Feature::Hrl => self.hrl = state,
|
|
Feature::Encryption => self.encryption = state,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A parsed [`Verb::Identity`] reply. Only present when the reply led with
|
|
/// [`RESP_MAGIC`]. The reply is `freemkv <version>` with no terminator, then the
|
|
/// raw feature-flag bytes; only the version is parsed — read the live states
|
|
/// via [`FirmwareControl::states`]/[`get`](FirmwareControl::get).
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct FirmwareIdentity {
|
|
/// The version token after `freemkv ` (printable non-space ASCII), e.g.
|
|
/// `"0.9.2"`; empty if no `freemkv ` token follows the magic. A Speed cap in the
|
|
/// printable range can append one character after the patch number.
|
|
pub version: String,
|
|
}
|
|
|
|
impl FirmwareIdentity {
|
|
/// Parse an IDENTITY data-in payload. `None` unless it leads with
|
|
/// [`RESP_MAGIC`] (i.e. not freemkv firmware).
|
|
pub fn parse(bytes: &[u8]) -> Option<Self> {
|
|
if !verify_response(bytes) {
|
|
return None;
|
|
}
|
|
let version = match bytes[RESP_MAGIC.len()..].split_first() {
|
|
Some((b' ', rest)) => rest
|
|
.iter()
|
|
.take_while(|b| b.is_ascii_graphic())
|
|
.map(|&b| char::from(b))
|
|
.collect(),
|
|
_ => String::new(),
|
|
};
|
|
Some(FirmwareIdentity { version })
|
|
}
|
|
|
|
/// Whether this firmware speaks the grammar this mirror sends (version at
|
|
/// or above [`MIN_FW_VERSION`]). `false` for an unparseable version.
|
|
pub fn is_supported(&self) -> bool {
|
|
self.major_minor().is_some_and(|v| v >= MIN_FW_VERSION)
|
|
}
|
|
|
|
fn major_minor(&self) -> Option<(u32, u32)> {
|
|
let mut parts = self.version.split('.');
|
|
let major = parts.next()?.parse().ok()?;
|
|
let minor = parts.next()?;
|
|
let digits = minor
|
|
.find(|c: char| !c.is_ascii_digit())
|
|
.map_or(minor, |end| &minor[..end]);
|
|
Some((major, digits.parse().ok()?))
|
|
}
|
|
}
|
|
|
|
/// A named arming recipe (see [`FirmwareControl`] `arm_*` helpers). Lets a
|
|
/// caller pick a mode by value (e.g. from a CLI flag or a config field) and
|
|
/// apply it uniformly.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ArmRecipe {
|
|
/// [`FirmwareControl::arm_oem_bd`].
|
|
OemBd,
|
|
/// [`FirmwareControl::arm_oem_uhd`].
|
|
OemUhd,
|
|
/// [`FirmwareControl::arm_bypass_bd`].
|
|
BypassBd,
|
|
/// [`FirmwareControl::arm_bypass_uhd`].
|
|
BypassUhd,
|
|
// NOTE: Feature::Unrestricted unifies former BD+UHD gates (fw 0.9.2); recipe
|
|
// names above name the disc TYPE being ripped, not the retired per-format
|
|
// feature — see `arm_oem_uhd`/`arm_bypass_uhd`.
|
|
/// [`FirmwareControl::arm_stealth_oem`].
|
|
StealthOem,
|
|
}
|
|
|
|
impl ArmRecipe {
|
|
/// Whether the recipe sets `Encryption = off`: the drive then acts
|
|
/// pre-authenticated and serves de-bussed content (no cert AKE needed).
|
|
pub(crate) fn disables_encryption(self) -> bool {
|
|
matches!(
|
|
self,
|
|
ArmRecipe::OemUhd | ArmRecipe::BypassBd | ArmRecipe::BypassUhd
|
|
)
|
|
}
|
|
}
|
|
|
|
// ── FirmwareControl ──────────────────────────────────────────────────────────
|
|
|
|
/// A timeout every firmware command uses (ms). Matches the wider unlocker.
|
|
const CMD_TIMEOUT_MS: u32 = 5_000;
|
|
|
|
/// An ergonomic, typed driver for the freemkv vendor-command grammar over any
|
|
/// [`ScsiTransport`]. Borrows the transport for the lifetime of the control.
|
|
///
|
|
/// ```no_run
|
|
/// # use freemkv_unlock::firmware::{FirmwareControl, FirmwareError};
|
|
/// # fn demo(scsi: &mut dyn freemkv_unlock::scsi::ScsiTransport) -> Result<(), FirmwareError> {
|
|
/// let mut fw = FirmwareControl::new(scsi);
|
|
/// // Encryption off on a UHD disc, revocation ignored. Refused with
|
|
/// // `UnsupportedFirmware` unless IDENTITY reports a supported freemkv drive;
|
|
/// // `Transport` (dead bus) must abort the caller.
|
|
/// fw.arm_oem_uhd()?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub struct FirmwareControl<'a> {
|
|
scsi: &'a mut dyn ScsiTransport,
|
|
}
|
|
|
|
impl<'a> FirmwareControl<'a> {
|
|
/// Wrap a transport.
|
|
pub fn new(scsi: &'a mut dyn ScsiTransport) -> Self {
|
|
FirmwareControl { scsi }
|
|
}
|
|
|
|
/// Issue a CDB with a data-in phase, returning the bytes transferred into a
|
|
/// fixed [`MEMREAD_LEN`]-byte buffer.
|
|
fn exec_in(&mut self, cdb: &[u8; CDB_LEN]) -> Result<([u8; MEMREAD_LEN], usize)> {
|
|
let mut buf = [0u8; MEMREAD_LEN];
|
|
match self
|
|
.scsi
|
|
.execute(cdb, DataDirection::FromDevice, &mut buf, CMD_TIMEOUT_MS)
|
|
{
|
|
Ok(r) if r.status == 0 => Ok((buf, r.bytes_transferred)),
|
|
Ok(_) => Err(FirmwareError::Rejected),
|
|
Err(e) => Err(if is_dead_bus(&e) {
|
|
FirmwareError::Transport
|
|
} else {
|
|
FirmwareError::Rejected
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// Send IDENTITY and parse the magic + version banner.
|
|
/// `Ok(None)` if the reply lacked [`RESP_MAGIC`] or the drive refused the
|
|
/// command (not freemkv firmware); `Err(Transport)` only on a dead bus.
|
|
pub fn identity(&mut self) -> Result<Option<FirmwareIdentity>> {
|
|
let cdb = build_identity_cdb(MEMREAD_LEN as u16);
|
|
match self.exec_in(&cdb) {
|
|
Ok((buf, n)) => Ok(FirmwareIdentity::parse(&buf[..n.min(buf.len())])),
|
|
// A drive that rejects the knock is simply not freemkv firmware.
|
|
Err(FirmwareError::Rejected) => Ok(None),
|
|
Err(e) => Err(e),
|
|
}
|
|
}
|
|
|
|
/// Convenience: is this a freemkv-firmware drive? `false` on a rejection or a
|
|
/// no-magic reply; `Err(Transport)` on a dead bus.
|
|
pub fn is_freemkv(&mut self) -> Result<bool> {
|
|
Ok(self.identity()?.is_some())
|
|
}
|
|
|
|
/// Read one feature's current state byte (GET).
|
|
pub fn get(&mut self, feature: Feature) -> Result<u8> {
|
|
let cdb = build_get_cdb(feature);
|
|
let (buf, n) = self.exec_in(&cdb)?;
|
|
if n == 0 {
|
|
return Err(FirmwareError::Rejected);
|
|
}
|
|
Ok(buf[0])
|
|
}
|
|
|
|
/// Set one feature to an explicit state byte (SET).
|
|
///
|
|
/// Issued with a data-in phase (`exec_in`), not `exec_none`: the drive's
|
|
/// READ BUFFER hijack aborts ANY vendor verb that carries no data-in
|
|
/// transfer — the `MIN_ALLOC_LEN` floor in the CDB is only honored when the
|
|
/// host actually reads that many bytes back. The returned bytes are the
|
|
/// echoed state frame; we discard them (GET is the read path).
|
|
pub fn set(&mut self, feature: Feature, state: u8) -> Result<()> {
|
|
let cdb = build_set_cdb(feature, state);
|
|
self.exec_in(&cdb).map(|_| ())
|
|
}
|
|
|
|
/// Restore every feature to [`STATE_PASSTHROUGH`] (RESET with [`RESET_TO_OEM`])
|
|
/// — TRUE OEM: firmware ≥0.8.3 also blanks the NV block so the drive is
|
|
/// traceless. Data-in phase for the same reason as [`set`](Self::set) — a no-data
|
|
/// RESET is aborted.
|
|
pub fn reset(&mut self) -> Result<()> {
|
|
let cdb = build_reset_cdb(RESET_TO_OEM);
|
|
self.exec_in(&cdb).map(|_| ())
|
|
}
|
|
|
|
/// Restore the baked create-time defaults into the RAM feature-state table
|
|
/// (RESET with [`RESET_TO_DEFAULTS`]) — what a never-saved drive boots to; does
|
|
/// not touch flash. Firmware ≥0.8.3. Data-in phase for the same reason as
|
|
/// [`reset`](Self::reset).
|
|
pub fn reset_to_defaults(&mut self) -> Result<()> {
|
|
let cdb = build_reset_cdb(RESET_TO_DEFAULTS);
|
|
self.exec_in(&cdb).map(|_| ())
|
|
}
|
|
|
|
/// Reload the saved flash config block back into the RAM feature-state table
|
|
/// (RESET with [`RESET_TO_FLASH`]), discarding any un-saved RAM changes — the
|
|
/// non-destructive "revert to last SAVE". Data-in phase for the same reason as
|
|
/// [`reset`](Self::reset).
|
|
pub fn reset_to_flash(&mut self) -> Result<()> {
|
|
let cdb = build_reset_cdb(RESET_TO_FLASH);
|
|
self.exec_in(&cdb).map(|_| ())
|
|
}
|
|
|
|
/// Persist the whole RAM feature-state table to the flash config block (SAVE)
|
|
/// — the only verb that writes config to flash, so a host's SET changes survive
|
|
/// a power cycle only once saved. Data-in phase for the same reason as
|
|
/// [`reset`](Self::reset).
|
|
pub fn save(&mut self) -> Result<()> {
|
|
let cdb = build_save_cdb();
|
|
self.exec_in(&cdb).map(|_| ())
|
|
}
|
|
|
|
/// SET a feature, then GET it back and confirm the state stuck. Blind spot:
|
|
/// fw answers a refused/out-of-range GET with GOOD + zeros, which reads as
|
|
/// [`STATE_OFF`]; the recipes gate on [`Self::require_supported`] first.
|
|
fn set_verify(&mut self, feature: Feature, state: u8) -> Result<()> {
|
|
self.set(feature, state)?;
|
|
let got = self.get(feature)?;
|
|
if got == state {
|
|
Ok(())
|
|
} else {
|
|
Err(FirmwareError::VerifyFailed {
|
|
feature,
|
|
wanted: state,
|
|
got,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Refuse (`UnsupportedFirmware`) unless IDENTITY reports a supported
|
|
/// freemkv firmware; every recipe calls this before its first SET/RESET.
|
|
fn require_supported(&mut self) -> Result<()> {
|
|
match self.identity()? {
|
|
Some(id) if id.is_supported() => Ok(()),
|
|
_ => Err(FirmwareError::UnsupportedFirmware),
|
|
}
|
|
}
|
|
|
|
/// Read the current state of every feature via repeated GET.
|
|
pub fn states(&mut self) -> Result<FeatureStates> {
|
|
let mut s = FeatureStates::all_passthrough();
|
|
for feature in ALL_FEATURES {
|
|
s.set(feature, self.get(feature)?);
|
|
}
|
|
Ok(s)
|
|
}
|
|
|
|
/// Diagnostic RAM peek: [`MEMREAD_LEN`] bytes at `addr` (DUMPALL).
|
|
pub fn dump(&mut self, addr: u32) -> Result<[u8; MEMREAD_LEN]> {
|
|
let cdb = build_memread_cdb(addr);
|
|
let (buf, n) = self.exec_in(&cdb)?;
|
|
if n < MEMREAD_LEN {
|
|
return Err(FirmwareError::Rejected);
|
|
}
|
|
Ok(buf)
|
|
}
|
|
|
|
// ── Typed setters ────────────────────────────────────────────────────────
|
|
|
|
/// Force the Unrestricted (BD/UHD) capability gate on (drive engages both
|
|
/// BD and UHD discs). fw 0.9.2 unified the former separate UHD/BD gates
|
|
/// into this single lever.
|
|
pub fn enable_unrestricted(&mut self) -> Result<()> {
|
|
self.set(Feature::Unrestricted, STATE_ON)
|
|
}
|
|
/// Force the Unrestricted (BD/UHD) capability gate off.
|
|
pub fn disable_unrestricted(&mut self) -> Result<()> {
|
|
self.set(Feature::Unrestricted, STATE_OFF)
|
|
}
|
|
/// Skip the HRL lookup (revoked certs accepted; non-destructive). The unlock
|
|
/// direction is now [`STATE_OFF`] (`0x00` = HRL enforcement off).
|
|
pub fn skip_hrl(&mut self) -> Result<()> {
|
|
self.set(Feature::Hrl, STATE_OFF)
|
|
}
|
|
/// Disable every drive-side encryption/cert requirement in one shot: the
|
|
/// drive acts pre-authenticated (no handshake) AND content returns de-bussed
|
|
/// (no in-transit bus encryption). The consolidated `Encryption` lever
|
|
/// replaces the old separate `Ake`/`Bus` levers — wire id `0x07` (`Bus`) is
|
|
/// retired. The unlock direction is [`STATE_OFF`] (`0x00`).
|
|
pub fn disable_encryption(&mut self) -> Result<()> {
|
|
self.set(Feature::Encryption, STATE_OFF)
|
|
}
|
|
/// Region-free (RPC-1). Sends [`REGION_FREE`] (`0x0F`) — under the migrated
|
|
/// region scheme `0x01` now means "force DVD region 1", not region-free.
|
|
pub fn region_free(&mut self) -> Result<()> {
|
|
self.set(Feature::Region, REGION_FREE)
|
|
}
|
|
/// Force a specific Blu-ray region.
|
|
pub fn force_region_bd(&mut self, region: BdRegion) -> Result<()> {
|
|
self.set(Feature::Region, region.state())
|
|
}
|
|
/// Force a specific DVD region (1..=8). Returns
|
|
/// [`FirmwareError::InvalidArgument`] for an out-of-range region without
|
|
/// touching the drive.
|
|
pub fn force_region_dvd(&mut self, region: u8) -> Result<()> {
|
|
if !(1..=8).contains(®ion) {
|
|
return Err(FirmwareError::InvalidArgument);
|
|
}
|
|
self.set(Feature::Region, REGION_DVD_BASE + region)
|
|
}
|
|
/// Lift the read-speed / riplock ceiling to maximum.
|
|
pub fn unlock_speed(&mut self) -> Result<()> {
|
|
self.set(Feature::Speed, SPEED_MAX)
|
|
}
|
|
|
|
// ── Named recipes (the "modes" table) ─────────────────────────────────────
|
|
// Each recipe first issues IDENTITY and refuses unsupported firmware. A
|
|
// recipe that fails midway leaves its earlier SETs applied (no rollback).
|
|
|
|
/// **arm_oem_bd** — OEM-style Blu-ray (AACS 1.0) rip with a REAL AKE, only
|
|
/// revocation disabled.
|
|
///
|
|
/// Sets: `Hrl = skip` (revoked host certs accepted; non-destructive).
|
|
/// Leaves everything else at passthrough — the drive still runs the real
|
|
/// host-cert handshake and returns bus-encrypted content, so the host
|
|
/// performs the AKE and de-busses. Rips: BD with an otherwise-revoked cert.
|
|
pub fn arm_oem_bd(&mut self) -> Result<()> {
|
|
self.require_supported()?;
|
|
self.set_verify(Feature::Hrl, STATE_OFF)
|
|
}
|
|
|
|
/// **arm_oem_uhd** — OEM-style UHD (AACS 2.0) rip.
|
|
///
|
|
/// Sets: `Unrestricted = on` (mode-gate neutralized so the drive engages
|
|
/// the UHD disc), `Hrl = skip` (revocation off), `Encryption = off`
|
|
/// (content de-bussed — the consolidated cert/bus bypass). Rips: UHD via
|
|
/// the OEM path.
|
|
pub fn arm_oem_uhd(&mut self) -> Result<()> {
|
|
self.require_supported()?;
|
|
self.set_verify(Feature::Unrestricted, STATE_ON)?;
|
|
self.set_verify(Feature::Hrl, STATE_OFF)?;
|
|
self.set_verify(Feature::Encryption, STATE_OFF)
|
|
}
|
|
|
|
/// **arm_bypass_bd** — full-bypass Blu-ray rip (NO host cert needed).
|
|
///
|
|
/// Sets: `Encryption = off` (the consolidated cert/bus bypass — drive acts
|
|
/// pre-authenticated, no handshake, content de-bussed) and `Hrl = skip`
|
|
/// (revocation off). Rips: BD with no cert.
|
|
pub fn arm_bypass_bd(&mut self) -> Result<()> {
|
|
self.require_supported()?;
|
|
self.set_verify(Feature::Hrl, STATE_OFF)?;
|
|
self.set_verify(Feature::Encryption, STATE_OFF)
|
|
}
|
|
|
|
/// **arm_bypass_uhd** — full-bypass UHD rip (NO host cert needed).
|
|
///
|
|
/// Sets: `Unrestricted = on` (engage the UHD disc), `Encryption = off`
|
|
/// (the consolidated cert/bus bypass), and `Hrl = skip` (revocation off).
|
|
/// Rips: UHD with no cert.
|
|
pub fn arm_bypass_uhd(&mut self) -> Result<()> {
|
|
self.require_supported()?;
|
|
self.set_verify(Feature::Unrestricted, STATE_ON)?;
|
|
self.set_verify(Feature::Hrl, STATE_OFF)?;
|
|
self.set_verify(Feature::Encryption, STATE_OFF)
|
|
}
|
|
|
|
/// **arm_stealth_oem** — return the drive to byte-for-byte OEM behaviour.
|
|
///
|
|
/// Issues RESET (every feature → passthrough) and verifies each feature read
|
|
/// back as [`STATE_PASSTHROUGH`]. Rips: nothing — this DISARMS the drive.
|
|
/// Needs fw >= [`MIN_FW_VERSION`]; raw [`reset`](Self::reset) has no gate.
|
|
pub fn arm_stealth_oem(&mut self) -> Result<()> {
|
|
self.require_supported()?;
|
|
self.reset()?;
|
|
for feature in ALL_FEATURES {
|
|
let got = self.get(feature)?;
|
|
if got != STATE_PASSTHROUGH {
|
|
return Err(FirmwareError::VerifyFailed {
|
|
feature,
|
|
wanted: STATE_PASSTHROUGH,
|
|
got,
|
|
});
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Apply a named [`ArmRecipe`].
|
|
pub fn arm(&mut self, recipe: ArmRecipe) -> Result<()> {
|
|
match recipe {
|
|
ArmRecipe::OemBd => self.arm_oem_bd(),
|
|
ArmRecipe::OemUhd => self.arm_oem_uhd(),
|
|
ArmRecipe::BypassBd => self.arm_bypass_bd(),
|
|
ArmRecipe::BypassUhd => self.arm_bypass_uhd(),
|
|
ArmRecipe::StealthOem => self.arm_stealth_oem(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Test fixture: the fw 0.9.x IDENTITY wire shape (core.rs `identity_blob` +
|
|
/// handler): `freemkv <ver>` with NO NUL, then the 6 raw flag bytes
|
|
/// `flag[0x01..=0x06]`, then zero padding to the 64-byte window.
|
|
#[cfg(test)]
|
|
pub(crate) fn identity_reply(version: &str, flags: [u8; 6]) -> Vec<u8> {
|
|
let mut p = RESP_MAGIC.to_vec();
|
|
p.push(b' ');
|
|
p.extend_from_slice(version.as_bytes());
|
|
p.extend_from_slice(&flags);
|
|
p.resize(MEMREAD_LEN, 0);
|
|
p
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "mod_tests.rs"]
|
|
mod tests;
|