Files
libfreemkv/src/scsi/windows.rs
T
Matthew Jackson 695569d29a scsi(windows): make list_drives self-contained (build under scsi-only)
scsi::windows::list_drives delegated to crate::drive::windows::find_drives(),
but the drive and identity modules are #[cfg(feature = "rip")]. A scsi-only
build (default-features = false, features = ["scsi"]) — which is exactly how
freemkv-firmware consumes libfreemkv — therefore failed to compile: 'could not
find drive in the crate root'. The --all-features CI never saw it (rip is on).

Enumerate directly at the SCSI layer via scsi::open + scsi::inquiry, mirroring
the Linux and macOS backends, which already build their DriveInfo without the
rip-gated modules. The rip-side drive::windows::find_drives() is untouched.

Verified: cargo check --target x86_64-pc-windows-msvc passes both scsi-only and
with default features.
2026-09-05 15:54:45 -07:00

702 lines
28 KiB
Rust

//! Windows SCSI transport via SPTI (SCSI Pass-Through Interface).
//!
//! Sends SCSI commands through DeviceIoControl with IOCTL_SCSI_PASS_THROUGH_DIRECT.
//! Accepts device paths like `D:`, `E:`, `\\.\CdRom0`, or `\\.\D:`.
//!
//! Requires administrator privileges for raw SCSI access.
use super::{DataDirection, ScsiResult, ScsiTransport};
use crate::error::{Error, Result};
use std::path::Path;
// ── Windows constants ──────────────────────────────────────────────────────
const IOCTL_SCSI_PASS_THROUGH_DIRECT: u32 = 0x4D014;
/// IOCTL_STORAGE_QUERY_PROPERTY — CTL_CODE(IOCTL_STORAGE_BASE(0x2D),
/// 0x500, METHOD_BUFFERED(0), FILE_ANY_ACCESS(0)) = 0x002D1400.
const IOCTL_STORAGE_QUERY_PROPERTY: u32 = 0x002D1400;
// IOCTL_STORAGE_RESET_DEVICE (ntddstor.h) = 0x002D5004. Two earlier values
// silently no-op'd the reset (ERROR_INVALID_FUNCTION) instead of failing
// loud. See docs/scsi-windows.md — IOCTL_STORAGE_RESET_DEVICE derivation.
const IOCTL_STORAGE_RESET_DEVICE: u32 = 0x002D_5004;
/// STORAGE_PROPERTY_ID::StorageAdapterProperty.
const STORAGE_ADAPTER_PROPERTY: u32 = 1;
/// STORAGE_QUERY_TYPE::PropertyStandardQuery.
const PROPERTY_STANDARD_QUERY: u32 = 0;
/// Conservative fallback when the adapter MaximumTransferLength query
/// fails — 64 KiB is universally safe for SPTD on any Windows storage
/// stack. Also the floor we clamp a reported value up to.
const WINDOWS_MIN_TRANSFER_BYTES: usize = 64 * 1024;
const SCSI_IOCTL_DATA_OUT: u8 = 0;
const SCSI_IOCTL_DATA_IN: u8 = 1;
const SCSI_IOCTL_DATA_UNSPECIFIED: u8 = 2;
const GENERIC_READ: u32 = 0x80000000;
const GENERIC_WRITE: u32 = 0x40000000;
const FILE_SHARE_READ: u32 = 0x00000001;
const FILE_SHARE_WRITE: u32 = 0x00000002;
const OPEN_EXISTING: u32 = 3;
const FILE_ATTRIBUTE_NORMAL: u32 = 0x80;
const INVALID_HANDLE_VALUE: isize = -1;
const K_MAX_CDB_SIZE: usize = 16;
const K_SENSE_SIZE: usize = 32;
// ── SCSI_PASS_THROUGH_DIRECT structure ─────────────────────────────────────
#[repr(C)]
#[allow(non_snake_case)]
struct ScsiPassThroughDirect {
Length: u16,
ScsiStatus: u8,
PathId: u8,
TargetId: u8,
Lun: u8,
CdbLength: u8,
SenseInfoLength: u8,
DataIn: u8,
_padding1: [u8; 3],
DataTransferLength: u32,
TimeOutValue: u32,
DataBuffer: *mut u8,
SenseInfoOffset: u32,
Cdb: [u8; K_MAX_CDB_SIZE],
}
#[repr(C)]
struct SptwbDirect {
spt: ScsiPassThroughDirect,
sense: [u8; K_SENSE_SIZE],
}
// ── STORAGE_QUERY_PROPERTY structures (winioctl.h) ─────────────────────────
/// Input to IOCTL_STORAGE_QUERY_PROPERTY. Mirrors `STORAGE_PROPERTY_QUERY`:
/// `{ PropertyId: u32, QueryType: u32, AdditionalParameters: [u8; 1] }`.
#[repr(C)]
#[allow(non_snake_case)]
struct StoragePropertyQuery {
PropertyId: u32,
QueryType: u32,
AdditionalParameters: [u8; 1],
}
// Prefix of `STORAGE_ADAPTER_DESCRIPTOR` (winioctl.h) up to
// `MaximumTransferLength`; the driver fills the whole thing, we only read
// this prefix. See docs/scsi-windows.md — StorageAdapterDescriptor field layout.
#[repr(C)]
#[allow(non_snake_case)]
struct StorageAdapterDescriptor {
Version: u32,
Size: u32,
MaximumTransferLength: u32,
MaximumPhysicalPages: u32,
AlignmentMask: u32,
AdapterUsesPio: u8,
AdapterScansDown: u8,
CommandQueueing: u8,
AcceleratedTransfer: u8,
// STORAGE_BUS_TYPE is an `int`-sized enum (4 bytes), not a byte: with
// the preceding BOOLEANs filling 20..24, `BusType` sits at offset 24
// and the USHORT version fields follow at 28/30, matching winioctl.h.
BusType: u32,
BusMajorVersion: u16,
BusMinorVersion: u16,
}
// ── Windows FFI ────────────────────────────────────────────────────────────
unsafe extern "system" {
fn CreateFileW(
lpFileName: *const u16,
dwDesiredAccess: u32,
dwShareMode: u32,
lpSecurityAttributes: *const std::ffi::c_void,
dwCreationDisposition: u32,
dwFlagsAndAttributes: u32,
hTemplateFile: *const std::ffi::c_void,
) -> isize;
fn CloseHandle(hObject: isize) -> i32;
fn GetLastError() -> u32;
fn DeviceIoControl(
hDevice: isize,
dwIoControlCode: u32,
lpInBuffer: *mut std::ffi::c_void,
nInBufferSize: u32,
lpOutBuffer: *mut std::ffi::c_void,
nOutBufferSize: u32,
lpBytesReturned: *mut u32,
lpOverlapped: *mut std::ffi::c_void,
) -> i32;
}
// ── Transport implementation ───────────────────────────────────────────────
pub struct SptiTransport {
handle: isize,
/// Adapter MaximumTransferLength in bytes, queried once at open via
/// IOCTL_STORAGE_QUERY_PROPERTY and clamped to at least
/// [`WINDOWS_MIN_TRANSFER_BYTES`]. A single READ larger than this fails
/// `DeviceIoControl` outright, so [`crate::Drive::read`] chunks to it.
max_transfer: usize,
/// Adapter `AlignmentMask` (STORAGE_ADAPTER_DESCRIPTOR, ntddscsi.h /
/// winioctl.h), queried alongside `max_transfer`. `0` (common on USB
/// bridges) means no alignment requirement; a nonzero mask (SCSI/SAS
/// HBAs) means a misaligned `DataBuffer` makes `DeviceIoControl` fail.
/// When set and the caller's buffer is misaligned, `execute()` bounces
/// through an aligned scratch buffer (see there).
/// See docs/scsi-windows.md — alignment_mask / bounce-buffer rationale.
alignment_mask: u32,
}
// SptiTransport's only field is an isize HANDLE: Send+Sync auto-derive;
// `&mut self` on execute() enforces exclusive use, not absence of Sync.
// Normalize a device path to \\.\X: format; see docs/scsi-windows.md — normalize_device_path duplication.
fn normalize_device_path(path: &str) -> String {
if path.starts_with("\\\\.\\") {
return path.to_string();
}
let trimmed = path.trim_end_matches('\\');
if trimmed.len() == 2 && trimmed.as_bytes()[1] == b':' {
return format!("\\\\.\\{}", trimmed);
}
if path.to_lowercase().starts_with("cdrom") {
return format!("\\\\.\\{}", path);
}
format!("\\\\.\\{}", path)
}
impl SptiTransport {
pub fn open(device: &Path) -> Result<Self> {
let dev_str = device.to_str().ok_or_else(|| Error::DeviceNotFound {
path: device.display().to_string(),
})?;
// Normalize device path to \\.\X: format
let win_path = normalize_device_path(dev_str);
let wide: Vec<u16> = win_path.encode_utf16().chain(std::iter::once(0)).collect();
let handle = unsafe {
CreateFileW(
wide.as_ptr(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null(),
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
std::ptr::null(),
)
};
if handle == INVALID_HANDLE_VALUE {
// Map last-os-error → Error variant; don't embed English hints
// in the path field (the CLI handles localization).
let err = std::io::Error::last_os_error();
return Err(if err.kind() == std::io::ErrorKind::PermissionDenied {
Error::DevicePermission {
path: dev_str.to_string(),
}
} else {
Error::DeviceNotFound {
path: dev_str.to_string(),
}
});
}
let (max_transfer, alignment_mask) = query_adapter_descriptor(handle);
Ok(SptiTransport {
handle,
max_transfer,
alignment_mask,
})
}
/// Reset the drive to a known good state.
/// Opens the device, sends IOCTL_STORAGE_RESET_DEVICE to reset
/// the USB/SCSI bus, then closes. Same concept as SG_SCSI_RESET on Linux.
pub fn reset(device: &Path) -> Result<()> {
let dev_str = device.to_str().ok_or_else(|| Error::DeviceNotFound {
path: device.display().to_string(),
})?;
let win_path = normalize_device_path(dev_str);
let wide: Vec<u16> = win_path.encode_utf16().chain(std::iter::once(0)).collect();
// Open
let handle = unsafe {
CreateFileW(
wide.as_ptr(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null(),
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
std::ptr::null(),
)
};
if handle == INVALID_HANDLE_VALUE {
return Ok(()); // can't open — skip reset, not fatal
}
// Send device reset. Result must be checked: a wrong/unsupported
// IOCTL fails with ERROR_INVALID_FUNCTION and no-ops silently —
// surface failures so a non-functional reset isn't masked below.
let mut returned: u32 = 0;
let ok = unsafe {
DeviceIoControl(
handle,
IOCTL_STORAGE_RESET_DEVICE,
std::ptr::null_mut(),
0,
std::ptr::null_mut(),
0,
&mut returned,
std::ptr::null_mut(),
)
};
let reset_ok = ok != 0;
if reset_ok {
tracing::debug!("IOCTL_STORAGE_RESET_DEVICE succeeded");
} else {
let err = unsafe { GetLastError() };
// Not fatal — reset is best-effort — but a failing reset means
// the device was NOT reset, so skip the settle-sleep below.
tracing::warn!(
last_error = err,
ioctl = format_args!("{IOCTL_STORAGE_RESET_DEVICE:#010x}"),
"IOCTL_STORAGE_RESET_DEVICE failed; drive not reset"
);
}
// Close the handle, then — only if the reset actually happened — wait
// for the drive to settle. A failed IOCTL reset performed no reset, so
// sleeping would burn 2 s for nothing.
unsafe { CloseHandle(handle) };
if reset_ok {
std::thread::sleep(std::time::Duration::from_secs(2));
}
Ok(())
}
}
/// Enumerate optical drives on Windows: probe `\\.\CdRom0..15`, falling back to
/// drive letters `D..Z` only if none match, INQUIRY each, and keep the optical
/// ones (peripheral device type 0x05).
///
/// Self-contained at the SCSI layer via `scsi::open` + `scsi::inquiry` (as the
/// Linux backend is), with no dependency on the `rip`-gated `drive`/`identity`
/// modules — `scsi` builds without `rip` (freemkv-firmware), where the old
/// `crate::drive::windows::find_drives()` delegation failed to compile.
pub(super) fn list_drives() -> Vec<super::DriveInfo> {
// SCSI peripheral device type 5 = MMC / optical, low 5 bits of INQUIRY byte 0.
const SCSI_PERIPHERAL_TYPE_OPTICAL: u8 = 0x05;
fn probe(path: &str, out: &mut Vec<super::DriveInfo>) {
let Ok(mut transport) = crate::scsi::open(Path::new(path)) else {
return;
};
let Ok(r) = super::inquiry(transport.as_mut()) else {
return;
};
if !r.raw.is_empty() && (r.raw[0] & 0x1F) == SCSI_PERIPHERAL_TYPE_OPTICAL {
out.push(super::DriveInfo {
path: normalize_device_path(path),
vendor: r.vendor_id,
model: r.model,
firmware: r.firmware,
});
}
}
let mut drives = Vec::new();
for i in 0..16 {
probe(&format!("\\\\.\\CdRom{}", i), &mut drives);
}
// Only fall back to drive letters if the CdRom scan found nothing.
if drives.is_empty() {
for letter in b'D'..=b'Z' {
probe(&format!("{}:", letter as char), &mut drives);
}
}
drives
}
/// TEST UNIT READY probe on Windows. No in-library recovery — see the
/// Linux `drive_has_disc` doc block for the rationale.
pub(super) fn drive_has_disc(path: &Path) -> Result<bool> {
let mut transport = SptiTransport::open(path)?;
let cdb = [crate::scsi::SCSI_TEST_UNIT_READY, 0, 0, 0, 0, 0];
let mut buf = [0u8; 0];
match transport.execute(
&cdb,
crate::scsi::DataDirection::None,
&mut buf,
crate::scsi::TUR_TIMEOUT_MS,
) {
Ok(_) => Ok(true),
Err(ref e) if e.scsi_sense().is_some_and(|s| s.is_not_ready()) => Ok(false),
Err(e) => Err(e),
}
}
impl Drop for SptiTransport {
fn drop(&mut self) {
if self.handle != INVALID_HANDLE_VALUE {
unsafe {
CloseHandle(self.handle);
}
}
}
}
// Query the adapter descriptor via IOCTL_STORAGE_QUERY_PROPERTY; returns
// (max_transfer_bytes, alignment_mask), both safely defaulted on failure.
// See docs/scsi-windows.md — query_adapter_descriptor contract.
fn query_adapter_descriptor(handle: isize) -> (usize, u32) {
if handle == INVALID_HANDLE_VALUE {
return (WINDOWS_MIN_TRANSFER_BYTES, 0);
}
let query = StoragePropertyQuery {
PropertyId: STORAGE_ADAPTER_PROPERTY,
QueryType: PROPERTY_STANDARD_QUERY,
AdditionalParameters: [0u8; 1],
};
let mut desc: StorageAdapterDescriptor = unsafe { std::mem::zeroed() };
let mut bytes_returned: u32 = 0;
let ok = unsafe {
DeviceIoControl(
handle,
IOCTL_STORAGE_QUERY_PROPERTY,
&query as *const _ as *mut std::ffi::c_void,
std::mem::size_of::<StoragePropertyQuery>() as u32,
&mut desc as *mut _ as *mut std::ffi::c_void,
std::mem::size_of::<StorageAdapterDescriptor>() as u32,
&mut bytes_returned,
std::ptr::null_mut(),
)
};
// MaximumTransferLength sits at offset 8; need at least that many bytes
// written for the field to be valid.
let max_valid = ok != 0
&& bytes_returned as usize
>= std::mem::offset_of!(StorageAdapterDescriptor, MaximumTransferLength)
+ std::mem::size_of::<u32>();
let max_transfer = if !max_valid || desc.MaximumTransferLength == 0 {
WINDOWS_MIN_TRANSFER_BYTES
} else {
(desc.MaximumTransferLength as usize).max(WINDOWS_MIN_TRANSFER_BYTES)
};
// AlignmentMask sits at offset 16; only trust it if the reply is long
// enough. Otherwise assume 0 (no alignment requirement).
let align_valid = ok != 0
&& bytes_returned as usize
>= std::mem::offset_of!(StorageAdapterDescriptor, AlignmentMask)
+ std::mem::size_of::<u32>();
let alignment_mask = if align_valid { desc.AlignmentMask } else { 0 };
(max_transfer, alignment_mask)
}
impl ScsiTransport for SptiTransport {
fn max_transfer_bytes(&self) -> usize {
self.max_transfer
}
fn execute(
&mut self,
cdb: &[u8],
direction: DataDirection,
data: &mut [u8],
timeout_ms: u32,
) -> Result<ScsiResult> {
// Zero the data buffer for reads to prevent returning uninitialized data
// if the driver doesn't fully update DataTransferLength.
if direction == DataDirection::FromDevice {
data.fill(0);
}
let mut sptwb: SptwbDirect = unsafe { std::mem::zeroed() };
// Reject an over-length CDB rather than truncating it: SPC-4 fixes
// a command's length by opcode group code, so a truncated CDB is a
// DIFFERENT command the drive would execute and answer as GOOD.
let cdb_len = usize::from(super::checked_cdb_len(cdb, K_MAX_CDB_SIZE)?);
sptwb.spt.Length = std::mem::size_of::<ScsiPassThroughDirect>() as u16;
sptwb.spt.CdbLength = cdb_len as u8;
sptwb.spt.SenseInfoLength = K_SENSE_SIZE as u8;
sptwb.spt.DataIn = match direction {
DataDirection::None => SCSI_IOCTL_DATA_UNSPECIFIED,
DataDirection::FromDevice => SCSI_IOCTL_DATA_IN,
DataDirection::ToDevice => SCSI_IOCTL_DATA_OUT,
};
// Match the macOS/Linux guard: a >=4 GiB buffer would wrap when cast to
// u32 below, producing a short transfer reported as success with the
// wrong byte count.
if data.len() > u32::MAX as usize {
return Err(Error::ScsiError {
opcode: cdb.first().copied().unwrap_or(0),
status: super::SCSI_STATUS_TRANSPORT_FAILURE,
sense: None,
});
}
sptwb.spt.DataTransferLength = data.len() as u32;
// Round up to the next whole second (SPTI's TimeOutValue has no
// sub-second resolution) rather than truncating — truncation
// broke 1500ms fast-reads on Drive::read.
sptwb.spt.TimeOutValue = ((timeout_ms + 999) / 1000).max(1);
// AlignmentMask bounce buffer: `DataBuffer` must satisfy the
// adapter's `AlignmentMask` (0 on USB — zero-copy; nonzero on
// SCSI/SAS HBAs, where misalignment fails the IOCTL outright).
let mask = self.alignment_mask as usize;
let needs_bounce =
!data.is_empty() && mask != 0 && (data.as_mut_ptr() as usize) & mask != 0;
let mut bounce: Vec<u8> = Vec::new();
let data_ptr: *mut u8 = if data.is_empty() {
std::ptr::null_mut()
} else if needs_bounce {
// Over-allocate by `mask` so an aligned start exists within.
bounce = vec![0u8; data.len() + mask];
let base = bounce.as_mut_ptr() as usize;
let aligned = crate::scsi::align_up(base, mask);
let aligned_ptr = aligned as *mut u8;
// For writes (ToDevice) prime the aligned region with the
// caller's payload before the IOCTL. (FromDevice copies back
// after.)
if direction == DataDirection::ToDevice {
unsafe {
std::ptr::copy_nonoverlapping(data.as_ptr(), aligned_ptr, data.len());
}
}
aligned_ptr
} else {
data.as_mut_ptr()
};
sptwb.spt.DataBuffer = data_ptr;
sptwb.spt.SenseInfoOffset = std::mem::offset_of!(SptwbDirect, sense) as u32;
sptwb.spt.Cdb[..cdb_len].copy_from_slice(&cdb[..cdb_len]);
let buf_size = std::mem::size_of::<SptwbDirect>() as u32;
let mut bytes_returned: u32 = 0;
let ok = unsafe {
DeviceIoControl(
self.handle,
IOCTL_SCSI_PASS_THROUGH_DIRECT,
&mut sptwb as *mut _ as *mut std::ffi::c_void,
buf_size,
&mut sptwb as *mut _ as *mut std::ffi::c_void,
buf_size,
&mut bytes_returned,
std::ptr::null_mut(),
)
};
if ok == 0 {
// Capture the Win32 error before returning — this hot path used
// to omit it, collapsing struct-layout bugs, access-denied, and
// a genuinely wedged drive into the same status-0xFF with no log.
let last_error = unsafe { GetLastError() };
tracing::warn!(
target: "freemkv::scsi",
opcode = cdb.first().copied().unwrap_or(0),
last_error,
"DeviceIoControl(IOCTL_SCSI_PASS_THROUGH_DIRECT) failed"
);
// Driver-level failure. Bubble up; in-library handle recovery
// was removed in 0.13.20 — the kernel mid-layer already escalated
// by the time DeviceIoControl returned. Caller surfaces to UX.
return Err(Error::ScsiError {
opcode: cdb.first().copied().unwrap_or(0),
status: super::SCSI_STATUS_TRANSPORT_FAILURE,
sense: None,
});
}
if sptwb.spt.ScsiStatus != 0 {
// SPTI doesn't surface a written-bytes count separate from
// SenseInfoLength; pass the full K_SENSE_SIZE and let
// parse_sense key off byte 0's response code (fixed vs descriptor).
let parsed = super::parse_sense(&sptwb.sense, K_SENSE_SIZE as u8);
return Err(Error::ScsiError {
opcode: cdb.first().copied().unwrap_or(0),
status: sptwb.spt.ScsiStatus,
sense: Some(parsed),
});
}
// Clamp to the caller's buffer length, matching Linux/macOS: a
// driver that reports DataTransferLength > data.len() must never let
// callers read past the buffer they handed in.
let transferred = (sptwb.spt.DataTransferLength as usize).min(data.len());
// If we bounced a FROM-device read, copy the aligned scratch back
// into the caller's buffer (only the bytes actually transferred).
if needs_bounce && direction == DataDirection::FromDevice {
let aligned_ptr = data_ptr; // points inside `bounce`
unsafe {
std::ptr::copy_nonoverlapping(aligned_ptr, data.as_mut_ptr(), transferred);
}
}
// `bounce` is dropped here, after the last use of `data_ptr`.
drop(bounce);
let mut sense = [0u8; 32];
sense.copy_from_slice(&sptwb.sense);
Ok(ScsiResult {
status: sptwb.spt.ScsiStatus,
bytes_transferred: transferred,
sense,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
// Regression guard for `SptiTransport::reset()`: recomputes
// IOCTL_STORAGE_RESET_DEVICE from the CTL_CODE formula, independently of
// the hardcoded constant. See docs/scsi-windows.md — IOCTL_STORAGE_RESET_DEVICE derivation.
#[test]
fn ioctl_storage_reset_device_value_is_correct() {
// CTL_CODE(DeviceType, Function, Method, Access) =
// (DeviceType << 16) | (Access << 14) | (Function << 2) | Method
const fn ctl_code(device_type: u32, function: u32, method: u32, access: u32) -> u32 {
(device_type << 16) | (access << 14) | (function << 2) | method
}
const IOCTL_STORAGE_BASE: u32 = 0x2D;
const METHOD_BUFFERED: u32 = 0;
const FILE_READ_ACCESS: u32 = 1;
let expected = ctl_code(
IOCTL_STORAGE_BASE,
0x0401,
METHOD_BUFFERED,
FILE_READ_ACCESS,
);
assert_eq!(
IOCTL_STORAGE_RESET_DEVICE, expected,
"IOCTL_STORAGE_RESET_DEVICE must equal CTL_CODE(0x2D, 0x0401, \
METHOD_BUFFERED, FILE_READ_ACCESS); a wrong value fails \
ERROR_INVALID_FUNCTION and silently no-ops the reset"
);
assert_eq!(IOCTL_STORAGE_RESET_DEVICE, 0x002D_5004);
// The two historically wrong values must never reappear.
assert_ne!(IOCTL_STORAGE_RESET_DEVICE, 0x002D_1004);
assert_ne!(IOCTL_STORAGE_RESET_DEVICE, 0x002D_D000);
}
// Regression guard: `StorageAdapterDescriptor` must match
// `STORAGE_ADAPTER_DESCRIPTOR` (winioctl.h) field-for-field. See
// docs/scsi-windows.md — StorageAdapterDescriptor field layout.
#[test]
fn storage_adapter_descriptor_matches_sdk_layout() {
use std::mem::{offset_of, size_of};
assert_eq!(offset_of!(StorageAdapterDescriptor, Version), 0);
assert_eq!(offset_of!(StorageAdapterDescriptor, Size), 4);
assert_eq!(
offset_of!(StorageAdapterDescriptor, MaximumTransferLength),
8
);
assert_eq!(
offset_of!(StorageAdapterDescriptor, MaximumPhysicalPages),
12
);
assert_eq!(offset_of!(StorageAdapterDescriptor, AlignmentMask), 16);
assert_eq!(offset_of!(StorageAdapterDescriptor, AdapterUsesPio), 20);
assert_eq!(offset_of!(StorageAdapterDescriptor, AdapterScansDown), 21);
assert_eq!(offset_of!(StorageAdapterDescriptor, CommandQueueing), 22);
assert_eq!(
offset_of!(StorageAdapterDescriptor, AcceleratedTransfer),
23
);
// The fields that were misplaced by the old `u8` BusType.
assert_eq!(offset_of!(StorageAdapterDescriptor, BusType), 24);
assert_eq!(offset_of!(StorageAdapterDescriptor, BusMajorVersion), 28);
assert_eq!(offset_of!(StorageAdapterDescriptor, BusMinorVersion), 30);
assert_eq!(size_of::<StorageAdapterDescriptor>(), 32);
}
// Regression guard for `ScsiPassThroughDirect`, cross-checked against
// the SDK's `SCSI_PASS_THROUGH_DIRECT` (natural alignment, no pack).
// Do NOT add `packed(4)` — see docs/scsi-windows.md — ScsiPassThroughDirect layout, cross-checked against SDK.
#[test]
#[cfg(target_pointer_width = "64")]
fn scsi_pass_through_direct_matches_sdk_layout_win64() {
use std::mem::{offset_of, size_of};
assert_eq!(offset_of!(ScsiPassThroughDirect, Length), 0);
assert_eq!(offset_of!(ScsiPassThroughDirect, DataTransferLength), 12);
assert_eq!(offset_of!(ScsiPassThroughDirect, TimeOutValue), 16);
assert_eq!(offset_of!(ScsiPassThroughDirect, DataBuffer), 24);
assert_eq!(offset_of!(ScsiPassThroughDirect, SenseInfoOffset), 32);
assert_eq!(offset_of!(ScsiPassThroughDirect, Cdb), 36);
assert_eq!(size_of::<ScsiPassThroughDirect>(), 56);
// `sense` immediately follows the 56-byte spt; SenseInfoOffset points
// here via offset_of! in execute(), so this must stay consistent.
assert_eq!(offset_of!(SptwbDirect, spt), 0);
assert_eq!(offset_of!(SptwbDirect, sense), 56);
assert_eq!(size_of::<SptwbDirect>(), 56 + K_SENSE_SIZE);
}
// Cross-checked against `STORAGE_PROPERTY_QUERY` (winioctl.h): both
// enums are `int`-sized, so PropertyId@0, QueryType@4,
// AdditionalParameters@8, size 12.
#[test]
fn storage_property_query_matches_sdk_layout() {
use std::mem::{offset_of, size_of};
assert_eq!(offset_of!(StoragePropertyQuery, PropertyId), 0);
assert_eq!(offset_of!(StoragePropertyQuery, QueryType), 4);
assert_eq!(offset_of!(StoragePropertyQuery, AdditionalParameters), 8);
assert_eq!(size_of::<StoragePropertyQuery>(), 12);
}
// Cross-checked against the Windows SDK headers: each IOCTL is asserted
// against an independent re-derivation of `CTL_CODE`, not just its
// literal. See docs/scsi-windows.md — windows_ffi_constants_match_sdk test rationale.
#[test]
fn windows_ffi_constants_match_sdk() {
// CTL_CODE re-derivation. FILE_DEVICE_CONTROLLER=0x4,
// FILE_DEVICE_MASS_STORAGE=0x2D, METHOD_BUFFERED=0,
// FILE_ANY_ACCESS=0, FILE_READ_ACCESS=1, FILE_WRITE_ACCESS=2.
const fn ctl_code(dev: u32, func: u32, method: u32, access: u32) -> u32 {
(dev << 16) | (access << 14) | (func << 2) | method
}
// ntddscsi.h: CTL_CODE(IOCTL_SCSI_BASE, 0x0405, METHOD_BUFFERED, READ|WRITE)
assert_eq!(
IOCTL_SCSI_PASS_THROUGH_DIRECT,
ctl_code(0x4, 0x405, 0, 1 | 2)
);
// winioctl.h: CTL_CODE(IOCTL_STORAGE_BASE, 0x0500, METHOD_BUFFERED, ANY)
assert_eq!(IOCTL_STORAGE_QUERY_PROPERTY, ctl_code(0x2D, 0x500, 0, 0));
// winioctl.h: CTL_CODE(IOCTL_STORAGE_BASE, 0x0401, METHOD_BUFFERED, READ)
assert_eq!(IOCTL_STORAGE_RESET_DEVICE, ctl_code(0x2D, 0x401, 0, 1));
// winioctl.h STORAGE_PROPERTY_ID / STORAGE_QUERY_TYPE enums.
assert_eq!(STORAGE_ADAPTER_PROPERTY, 1); // StorageAdapterProperty (Device=0, Adapter=1)
assert_eq!(PROPERTY_STANDARD_QUERY, 0); // PropertyStandardQuery
// ntddscsi.h SCSI_IOCTL_DATA_*.
assert_eq!(SCSI_IOCTL_DATA_OUT, 0);
assert_eq!(SCSI_IOCTL_DATA_IN, 1);
assert_eq!(SCSI_IOCTL_DATA_UNSPECIFIED, 2);
// winnt.h GENERIC_* / FILE_SHARE_* / FILE_ATTRIBUTE_*; fileapi.h OPEN_EXISTING.
assert_eq!(GENERIC_READ, 0x8000_0000);
assert_eq!(GENERIC_WRITE, 0x4000_0000);
assert_eq!(FILE_SHARE_READ, 0x1);
assert_eq!(FILE_SHARE_WRITE, 0x2);
assert_eq!(OPEN_EXISTING, 3);
assert_eq!(FILE_ATTRIBUTE_NORMAL, 0x80);
assert_eq!(INVALID_HANDLE_VALUE, -1);
}
}