use super::{Completion, Data, Error, Status, Transport, sense_from_fixed};
use std::{io, os::windows::ffi::OsStrExt, path::Path, ptr, thread::sleep, time::Duration};
use tracing::*;
use windows_sys::Win32::{
Foundation::{CloseHandle, ERROR_WORKING_SET_QUOTA, HANDLE, INVALID_HANDLE_VALUE},
Storage::FileSystem::{
CreateFileW, FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_SHARE_READ, FILE_SHARE_WRITE,
OPEN_EXISTING,
},
System::IO::DeviceIoControl,
};
const IOCTL_SCSISCAN_CMD: u32 = 0x0019_0012;
const SRB_FLAGS_NO_DATA: u32 = 0x00;
const SRB_FLAGS_DATA_IN: u32 = 0x40;
const SRB_FLAGS_DATA_OUT: u32 = 0x80;
const SRB_STATUS_SUCCESS: u8 = 0x01;
const SRB_STATUS_ERROR: u8 = 0x04;
const SRB_STATUS_BUSY: u8 = 0x05;
const SRB_STATUS_AUTOSENSE_VALID: u8 = 0x80;
const SRB_STATUS_MASK: u8 = 0x3F;
const SENSE_LENGTH: usize = 32;
const QUOTA_RETRIES: usize = 200;
const QUOTA_RETRY_DELAY: Duration = Duration::from_millis(50);
const MAX_TRANSFER: usize = 128 * 1024;
#[repr(C)]
struct ScsiScanCmd {
reserved1: u32,
size: u32,
srb_flags: u32,
cdb_length: u8,
sense_length: u8,
reserved2: u8,
reserved3: u8,
transfer_length: u32,
cdb: [u8; 16],
srb_status: *mut u8,
sense_buffer: *mut u8,
}
pub struct ScsiScanDevice {
handle: HANDLE,
}
unsafe impl Send for ScsiScanDevice {}
impl ScsiScanDevice {
pub fn open(path: impl AsRef<Path>) -> io::Result<Self> {
let path = path.as_ref();
let wide: Vec<u16> = path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
let handle = unsafe {
CreateFileW(
wide.as_ptr(),
FILE_GENERIC_READ | FILE_GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
ptr::null(),
OPEN_EXISTING,
0,
ptr::null_mut(),
)
};
if handle == INVALID_HANDLE_VALUE {
return Err(io::Error::last_os_error());
}
debug!(?path, "Opened scanner");
Ok(Self { handle })
}
}
impl Drop for ScsiScanDevice {
fn drop(&mut self) {
let _ = unsafe { CloseHandle(self.handle) };
}
}
impl Transport for ScsiScanDevice {
fn max_transfer(&self) -> usize {
MAX_TRANSFER
}
#[instrument(skip_all, fields(cdb = ?cdb, ?data))]
fn execute(&mut self, cdb: &[u8], data: Data, _timeout: Duration) -> Result<Completion, Error> {
if cdb.len() > 16 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"SCSISCAN_CMD carries at most a 16-byte CDB",
)
.into());
}
let mut srb_status = 0u8;
let mut sb = [0u8; SENSE_LENGTH];
let mut padded = [0u8; 16];
padded[..cdb.len()].copy_from_slice(cdb);
let (srb_flags, data_ptr, data_len) = match data {
Data::None => (SRB_FLAGS_NO_DATA, ptr::null_mut(), 0usize),
Data::In(x) => (SRB_FLAGS_DATA_IN, x.as_mut_ptr(), x.len()),
Data::Out(x) => (SRB_FLAGS_DATA_OUT, x.as_ptr() as *mut u8, x.len()),
};
let mut cmd = ScsiScanCmd {
reserved1: 0,
size: size_of::<ScsiScanCmd>() as u32,
srb_flags,
cdb_length: cdb.len() as u8,
sense_length: SENSE_LENGTH as u8,
reserved2: 0,
reserved3: 0,
transfer_length: data_len as u32,
cdb: padded,
srb_status: &mut srb_status,
sense_buffer: sb.as_mut_ptr(),
};
let mut returned = 0u32;
let mut attempt = 0;
loop {
let ok = unsafe {
DeviceIoControl(
self.handle,
IOCTL_SCSISCAN_CMD,
(&raw mut cmd).cast(),
size_of::<ScsiScanCmd>() as u32,
data_ptr.cast(),
data_len as u32,
&mut returned,
ptr::null_mut(),
)
};
if ok != 0 {
break;
}
let e = io::Error::last_os_error();
if e.raw_os_error() != Some(ERROR_WORKING_SET_QUOTA as i32) || attempt >= QUOTA_RETRIES
{
return Err(e.into());
}
attempt += 1;
debug!(attempt, "Working set quota exceeded, retrying");
sleep(QUOTA_RETRY_DELAY);
}
trace!(
srb_status = format!("0x{srb_status:02x}"),
returned, attempt, "SCSISCAN_CMD completed"
);
let status = match srb_status & SRB_STATUS_MASK {
SRB_STATUS_SUCCESS => Status::Good,
SRB_STATUS_ERROR => Status::CheckCondition,
SRB_STATUS_BUSY => Status::Busy,
_ => {
return Err(io::Error::other(format!(
"SCSI bus fault: srb_status {srb_status:#04x}"
))
.into());
}
};
let sense = if srb_status & SRB_STATUS_AUTOSENSE_VALID != 0 {
debug!(sense_raw = ?sb, "raw sense buffer");
if !matches!(sb[0] & 0x7F, 0x70 | 0x71) {
warn!(
response_code = sb[0],
"autosense buffer is not fixed-format"
);
}
Some(sense_from_fixed(&sb, Some(sb[15])))
} else {
None
};
if status == Status::CheckCondition && sense.is_none() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"CHECK CONDITION without autosense",
)
.into());
}
Ok(Completion {
status,
sense,
transferred: returned as usize,
})
}
}