use std::os::windows::fs::OpenOptionsExt as _;
use std::os::windows::io::AsRawHandle as _;
use windows_sys::Win32::Foundation::FALSE;
use windows_sys::Win32::System::IO::{DeviceIoControl, OVERLAPPED};
use crate::types::{
Direction, MAX_CDB_LEN, MAX_SENSE_LEN, OpenOpts, ScsiCommand, ScsiStatus, Sense,
};
use crate::{Error, ScsiResult};
const IOCTL_SCSI_PASS_THROUGH_DIRECT: u32 = 0x0004_D014;
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 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 struct Device {
file: std::fs::File,
}
impl Device {
pub 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 fn execute(&mut self, mut cmd: ScsiCommand) -> Result<ScsiResult, Error> {
let timeout = cmd.timeout_secs.unwrap_or(30);
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 cdb_arr = cmd.cdb.as_array();
let sense_offset = core::mem::offset_of!(SptdBuffer, sense) as u32;
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() as u8,
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: cdb_arr,
},
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 == FALSE {
return Err(std::io::Error::last_os_error().into());
}
let transferred = buf.sptd.data_transfer_length as usize;
if transferred > cmd.data.len() {
return Err(Error::Internal(
"device reported more transferred bytes than the data buffer capacity",
));
}
cmd.data.truncate(transferred);
let sense_len = buf.sptd.sense_info_length;
let mut sense_data = [0u8; MAX_SENSE_LEN];
let copy_len = (sense_len as usize).min(MAX_SENSE_LEN);
sense_data[..copy_len].copy_from_slice(&buf.sense[..copy_len]);
Ok(ScsiResult {
status: ScsiStatus::from(buf.sptd.scsi_status),
sense: Sense::new(sense_data, sense_len),
data: cmd.data,
})
}
}
#[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"
);
}
}