- scsi/windows.rs: SCSI_PASS_THROUGH_DIRECT via DeviceIoControl - Device path normalization (D:, \\.\CdRom0, \\.\D:) - Windows drive discovery (CdRom0-15 + drive letters) - CI: cargo check on windows-latest - Platform table: Linux + macOS + Windows all supported
278 lines
8.7 KiB
Rust
278 lines
8.7 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 crate::error::{Error, Result};
|
|
use super::{DataDirection, ScsiResult, ScsiTransport};
|
|
use std::path::Path;
|
|
|
|
// ── Windows constants ──────────────────────────────────────────────────────
|
|
|
|
const IOCTL_SCSI_PASS_THROUGH_DIRECT: u32 = 0x4D014;
|
|
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],
|
|
}
|
|
|
|
// ── Windows FFI ────────────────────────────────────────────────────────────
|
|
|
|
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 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,
|
|
}
|
|
|
|
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 {
|
|
return Err(Error::DeviceNotFound {
|
|
path: format!("{}: cannot open device (run as administrator)", dev_str),
|
|
});
|
|
}
|
|
|
|
Ok(SptiTransport { handle })
|
|
}
|
|
}
|
|
|
|
impl Drop for SptiTransport {
|
|
fn drop(&mut self) {
|
|
unsafe { CloseHandle(self.handle); }
|
|
}
|
|
}
|
|
|
|
impl ScsiTransport for SptiTransport {
|
|
fn execute(
|
|
&mut self,
|
|
cdb: &[u8],
|
|
direction: DataDirection,
|
|
data: &mut [u8],
|
|
timeout_ms: u32,
|
|
) -> Result<ScsiResult> {
|
|
let mut sptwb: SptwbDirect = unsafe { std::mem::zeroed() };
|
|
|
|
let cdb_len = cdb.len().min(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,
|
|
};
|
|
sptwb.spt.DataTransferLength = data.len() as u32;
|
|
sptwb.spt.TimeOutValue = (timeout_ms / 1000).max(1) as u32;
|
|
sptwb.spt.DataBuffer = if data.is_empty() { std::ptr::null_mut() } else { data.as_mut_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 {
|
|
return Err(Error::ScsiError {
|
|
opcode: cdb[0],
|
|
status: 0xFF,
|
|
sense_key: 0,
|
|
});
|
|
}
|
|
|
|
if sptwb.spt.ScsiStatus != 0 {
|
|
let sense_key = if sptwb.sense[2] != 0 { sptwb.sense[2] & 0x0F } else { 0 };
|
|
return Err(Error::ScsiError {
|
|
opcode: cdb[0],
|
|
status: sptwb.spt.ScsiStatus,
|
|
sense_key,
|
|
});
|
|
}
|
|
|
|
let mut sense = [0u8; 32];
|
|
sense.copy_from_slice(&sptwb.sense);
|
|
|
|
Ok(ScsiResult {
|
|
status: sptwb.spt.ScsiStatus,
|
|
bytes_transferred: sptwb.spt.DataTransferLength as usize,
|
|
sense,
|
|
})
|
|
}
|
|
}
|
|
|
|
// ── Device path helpers ────────────────────────────────────────────────────
|
|
|
|
/// Normalize a device path to Windows \\.\X: format.
|
|
///
|
|
/// Accepts: "D:", "D:\\", "\\.\D:", "\\.\CdRom0"
|
|
pub(crate) fn normalize_device_path(path: &str) -> String {
|
|
// Already in \\.\X format
|
|
if path.starts_with("\\\\.\\") {
|
|
return path.to_string();
|
|
}
|
|
|
|
// Single drive letter: "D:" or "D:\"
|
|
let trimmed = path.trim_end_matches('\\');
|
|
if trimmed.len() == 2 && trimmed.as_bytes()[1] == b':' {
|
|
return format!("\\\\.\\{}", trimmed);
|
|
}
|
|
|
|
// CdRomN format
|
|
if path.to_lowercase().starts_with("cdrom") {
|
|
return format!("\\\\.\\{}", path);
|
|
}
|
|
|
|
// Fallback: wrap in \\.\
|
|
format!("\\\\.\\{}", path)
|
|
}
|
|
|
|
/// Find all optical drives on Windows.
|
|
/// Scans drive letters A-Z and CdRom0-15.
|
|
pub fn find_drives() -> Vec<(String, crate::identity::DriveId)> {
|
|
let mut drives = Vec::new();
|
|
|
|
// Try CdRom0..CdRom15
|
|
for i in 0..16 {
|
|
let path = format!("\\\\.\\CdRom{}", i);
|
|
if let Ok(mut transport) = SptiTransport::open(Path::new(&path)) {
|
|
if let Ok(id) = crate::identity::DriveId::from_drive(&mut transport) {
|
|
if !id.raw_inquiry.is_empty() && (id.raw_inquiry[0] & 0x1F) == 0x05 {
|
|
drives.push((path, id));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also try drive letters if CdRom didn't find anything
|
|
if drives.is_empty() {
|
|
for letter in b'D'..=b'Z' {
|
|
let path = format!("{}:", letter as char);
|
|
if let Ok(mut transport) = SptiTransport::open(Path::new(&path)) {
|
|
if let Ok(id) = crate::identity::DriveId::from_drive(&mut transport) {
|
|
if !id.raw_inquiry.is_empty() && (id.raw_inquiry[0] & 0x1F) == 0x05 {
|
|
drives.push((path, id));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
drives
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn normalize_drive_letter() {
|
|
assert_eq!(normalize_device_path("D:"), "\\\\.\\D:");
|
|
assert_eq!(normalize_device_path("E:\\"), "\\\\.\\E:");
|
|
}
|
|
|
|
#[test]
|
|
fn normalize_already_prefixed() {
|
|
assert_eq!(normalize_device_path("\\\\.\\D:"), "\\\\.\\D:");
|
|
assert_eq!(normalize_device_path("\\\\.\\CdRom0"), "\\\\.\\CdRom0");
|
|
}
|
|
|
|
#[test]
|
|
fn normalize_cdrom() {
|
|
assert_eq!(normalize_device_path("CdRom0"), "\\\\.\\CdRom0");
|
|
}
|
|
}
|