//! 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 { 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 = 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 = 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 { // 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) { 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 { 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::() as u32, &mut desc as *mut _ as *mut std::ffi::c_void, std::mem::size_of::() 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::(); 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::(); 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 { // 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::() 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 = 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::() 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::(), 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::(), 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::(), 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::(), 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); } }