use std::os::windows::fs::OpenOptionsExt as _;
use std::os::windows::io::AsRawHandle as _;
use windows_sys::Win32::Devices::DeviceAndDriverInstallation::{
DIGCF_DEVICEINTERFACE, DIGCF_PRESENT, HDEVINFO, SP_DEVICE_INTERFACE_DATA,
SP_DEVICE_INTERFACE_DETAIL_DATA_W, SetupDiDestroyDeviceInfoList, SetupDiEnumDeviceInterfaces,
SetupDiGetClassDevsW, SetupDiGetDeviceInterfaceDetailW,
};
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
use windows_sys::Win32::System::IO::{DeviceIoControl, OVERLAPPED};
use windows_sys::core::GUID;
use crate::types::{
Direction, MAX_CDB_LEN, MAX_SENSE_LEN, OpenOpts, ScsiCommand, ScsiDeviceInfo, ScsiDeviceType,
ScsiStatus,
};
use crate::{Error, ScsiResult};
const IOCTL_SCSI_PASS_THROUGH_DIRECT: u32 = 0x0004_D014;
const IOCTL_STORAGE_GET_DEVICE_NUMBER: u32 = 0x002D_1080;
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 = 0x8000_0000;
const GENERIC_WRITE: u32 = 0x4000_0000;
const FILE_SHARE_READ: u32 = 0x0000_0001;
const FILE_SHARE_WRITE: u32 = 0x0000_0002;
#[repr(C)]
struct StorageDeviceNumber {
device_type: u32,
device_number: u32,
partition_number: u32,
}
#[repr(C)]
struct ScsiPassThroughDirect {
length: u16,
scsi_status: u8,
path_id: u8,
target_id: u8,
lun: u8,
cdb_length: u8,
sense_info_length: u8, data_in: u8,
data_transfer_length: u32,
timeout_value: u32,
data_buffer: *mut core::ffi::c_void,
sense_info_offset: u32, cdb: [u8; MAX_CDB_LEN],
}
#[repr(C)]
struct SptdBuffer {
sptd: ScsiPassThroughDirect,
sense: [u8; MAX_SENSE_LEN],
}
pub(crate) struct Device {
file: std::fs::File,
}
impl Device {
pub(crate) fn open(path: &std::path::Path, opts: &OpenOpts) -> Result<Self, Error> {
let share = if opts.exclusive {
0
} else {
FILE_SHARE_READ | FILE_SHARE_WRITE
};
let file = std::fs::OpenOptions::new()
.share_mode(share)
.access_mode(GENERIC_READ | GENERIC_WRITE)
.open(path)?;
Ok(Device { file })
}
pub(crate) fn execute(&mut self, mut cmd: ScsiCommand) -> Result<ScsiResult, Error> {
let timeout = cmd.timeout_secs.unwrap_or(30);
let sense_offset = core::mem::offset_of!(SptdBuffer, sense) as u32;
let data_in_flag = match cmd.direction {
Direction::In => SCSI_IOCTL_DATA_IN,
Direction::Out => SCSI_IOCTL_DATA_OUT,
Direction::None => SCSI_IOCTL_DATA_UNSPECIFIED,
};
let data_len = u32::try_from(cmd.data.len())
.map_err(|_| Error::InvalidParameter("data buffer exceeds u32::MAX bytes"))?;
let data_ptr: *mut core::ffi::c_void = if cmd.data.is_empty() {
core::ptr::null_mut()
} else {
cmd.data.as_mut_ptr().cast()
};
let mut buf = SptdBuffer {
sptd: ScsiPassThroughDirect {
length: size_of::<ScsiPassThroughDirect>() as u16,
scsi_status: 0,
path_id: 0,
target_id: 0,
lun: 0,
cdb_length: cmd.cdb_len,
sense_info_length: MAX_SENSE_LEN as u8,
data_in: data_in_flag,
data_transfer_length: data_len,
timeout_value: timeout,
data_buffer: data_ptr,
sense_info_offset: sense_offset,
cdb: cmd.cdb,
},
sense: [0u8; MAX_SENSE_LEN],
};
let buf_len = size_of::<SptdBuffer>() as u32;
let buf_ptr = (&mut buf as *mut SptdBuffer).cast::<core::ffi::c_void>();
let mut bytes_returned: u32 = 0;
let handle = self.file.as_raw_handle();
let ok = unsafe {
DeviceIoControl(
handle,
IOCTL_SCSI_PASS_THROUGH_DIRECT,
buf_ptr, buf_len, buf_ptr, buf_len, &mut bytes_returned,
core::ptr::null_mut::<OVERLAPPED>(),
)
};
if ok == 0 {
return Err(std::io::Error::last_os_error().into());
}
let sense_len = buf.sptd.sense_info_length.min(MAX_SENSE_LEN as u8);
let mut sense = [0u8; MAX_SENSE_LEN];
sense[..sense_len as usize].copy_from_slice(&buf.sense[..sense_len as usize]);
Ok(ScsiResult {
status: ScsiStatus::from(buf.sptd.scsi_status),
sense,
sense_len,
data: cmd.data,
transferred: buf.sptd.data_transfer_length,
})
}
}
const SCSI_RAW_INTERFACE_GUID: GUID = GUID::from_u128(0x53f56309_b6bf_11d0_94f2_00a0c91efb8b);
pub(crate) fn list_devices() -> Result<Vec<ScsiDeviceInfo>, Error> {
let mut devices = Vec::new();
eprintln!("[DEBUG] Starting SetupDi enumeration...");
let hdevinfo: HDEVINFO = unsafe {
SetupDiGetClassDevsW(
&SCSI_RAW_INTERFACE_GUID as *const GUID,
core::ptr::null(),
core::ptr::null_mut(),
DIGCF_DEVICEINTERFACE | DIGCF_PRESENT,
)
};
if hdevinfo == INVALID_HANDLE_VALUE as HDEVINFO {
let err = std::io::Error::last_os_error();
eprintln!("[DEBUG] SetupDiGetClassDevsW failed: {}", err);
return Err(err.into());
}
let _guard = DevInfoGuard(hdevinfo);
eprintln!("[DEBUG] SetupDiGetClassDevsW succeeded, enumerating interfaces...");
let mut index = 0u32;
loop {
let mut iface_data = SP_DEVICE_INTERFACE_DATA {
cbSize: size_of::<SP_DEVICE_INTERFACE_DATA>() as u32,
InterfaceClassGuid: SCSI_RAW_INTERFACE_GUID,
Flags: 0,
Reserved: 0,
};
let ok = unsafe {
SetupDiEnumDeviceInterfaces(
hdevinfo,
core::ptr::null_mut(),
&SCSI_RAW_INTERFACE_GUID as *const GUID,
index,
&mut iface_data,
)
};
if ok == 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(259) {
eprintln!("[DEBUG] Enumeration complete at index {}.", index);
break;
}
eprintln!(
"[DEBUG] SetupDiEnumDeviceInterfaces failed at index {}: {}",
index, err
);
return Err(err.into());
}
eprintln!("[DEBUG] Interface {} found, getting detail...", index);
let mut required_size = 0u32;
unsafe {
SetupDiGetDeviceInterfaceDetailW(
hdevinfo,
&iface_data,
core::ptr::null_mut(),
0,
&mut required_size,
core::ptr::null_mut(),
)
};
if required_size == 0 {
eprintln!(
"[DEBUG] Failed to get required size for interface {}",
index
);
index += 1;
continue;
}
let mut buffer = vec![0u8; required_size as usize];
let detail = buffer.as_mut_ptr() as *mut SP_DEVICE_INTERFACE_DETAIL_DATA_W;
#[cfg(target_pointer_width = "64")]
let cb_size = 8u32;
#[cfg(target_pointer_width = "32")]
let cb_size = 6u32;
unsafe {
(*detail).cbSize = cb_size;
}
let ok = unsafe {
SetupDiGetDeviceInterfaceDetailW(
hdevinfo,
&iface_data,
detail,
required_size,
core::ptr::null_mut(),
core::ptr::null_mut(),
)
};
if ok == 0 {
eprintln!(
"[DEBUG] SetupDiGetDeviceInterfaceDetailW failed for interface {}: {}",
index,
std::io::Error::last_os_error()
);
index += 1;
continue;
}
let device_path_ptr = unsafe { (*detail).DevicePath.as_ptr() };
let mut len = 0;
while unsafe { *device_path_ptr.add(len) } != 0 {
len += 1;
}
let device_path_wide = unsafe { std::slice::from_raw_parts(device_path_ptr, len) };
let device_path = String::from_utf16_lossy(device_path_wide);
eprintln!("[DEBUG] Interface {} path: {}", index, device_path);
let path = std::path::Path::new(&device_path);
let physical_drive_path = match get_physical_drive_path(path) {
Ok(p) => {
eprintln!("[DEBUG] -> Mapped to: {}", p);
p
}
Err(e) => {
eprintln!("[DEBUG] -> Failed to map: {}", e);
index += 1;
continue;
}
};
let physical_path = std::path::Path::new(&physical_drive_path);
match Device::open(physical_path, &OpenOpts::default()) {
Ok(mut dev) => {
eprintln!("[DEBUG] -> Opened successfully, probing...");
match probe_device_handle(&mut dev, physical_drive_path.clone()) {
Ok(info) => {
eprintln!(
"[DEBUG] -> INQUIRY success: {} {}",
info.vendor, info.product
);
devices.push(info);
}
Err(e) => {
eprintln!("[DEBUG] -> INQUIRY failed: {}", e);
}
}
}
Err(e) => {
eprintln!("[DEBUG] -> Open failed: {}", e);
}
}
index += 1;
}
eprintln!("[DEBUG] Found {} devices total.", devices.len());
Ok(devices)
}
fn get_physical_drive_path(device_path: &std::path::Path) -> Result<String, Error> {
let file = std::fs::OpenOptions::new()
.read(true)
.share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
.open(device_path)?;
let handle = file.as_raw_handle();
let mut dev_num = StorageDeviceNumber {
device_type: 0,
device_number: 0,
partition_number: 0,
};
let mut bytes_returned: u32 = 0;
let ok = unsafe {
DeviceIoControl(
handle,
IOCTL_STORAGE_GET_DEVICE_NUMBER,
core::ptr::null_mut(),
0,
(&mut dev_num as *mut StorageDeviceNumber).cast::<core::ffi::c_void>(),
size_of::<StorageDeviceNumber>() as u32,
&mut bytes_returned,
core::ptr::null_mut::<OVERLAPPED>(),
)
};
if ok == 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(format!(r"\\.\PhysicalDrive{}", dev_num.device_number))
}
struct DevInfoGuard(HDEVINFO);
impl Drop for DevInfoGuard {
fn drop(&mut self) {
unsafe { SetupDiDestroyDeviceInfoList(self.0) };
}
}
fn probe_device_handle(dev: &mut Device, path: String) -> Result<ScsiDeviceInfo, Error> {
let mut cdb = [0u8; MAX_CDB_LEN];
cdb[0] = 0x12; cdb[4] = 96;
let cmd = ScsiCommand {
cdb,
cdb_len: 6,
direction: Direction::In,
data: vec![0u8; 96],
timeout_secs: Some(5),
};
let result = dev.execute(cmd)?;
if result.status != ScsiStatus::Good || result.data.len() < 36 {
return Err(Error::InvalidParameter(
"INQUIRY failed or response too short",
));
}
let inq = &result.data;
let device_type = ScsiDeviceType::from(inq[0] & 0x1F);
let vendor = String::from_utf8_lossy(&inq[8..16]).trim().to_string();
let product = String::from_utf8_lossy(&inq[16..32]).trim().to_string();
let revision = String::from_utf8_lossy(&inq[32..36]).trim().to_string();
Ok(ScsiDeviceInfo {
path,
device_type,
vendor,
product,
revision,
})
}
#[cfg(test)]
impl Device {
pub(crate) fn new_test() -> std::io::Result<Self> {
let path = std::env::temp_dir().join("libscsi_test_dummy.tmp");
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&path)?;
Ok(Device { file })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sptd_size_matches_sdk() {
assert_eq!(
size_of::<ScsiPassThroughDirect>(),
56,
"ScsiPassThroughDirect must be 56 bytes on 64-bit Windows"
);
}
#[test]
fn sptd_buffer_sense_offset() {
assert_eq!(
core::mem::offset_of!(SptdBuffer, sense),
56,
"sense data must start immediately after ScsiPassThroughDirect"
);
}
}