use super::{Completion, Data, Error, SENSE_REQUEST_LEN, Status, Transport, sense_from_fixed};
use bitflags::bitflags;
use nix::{ioctl_read_bad, ioctl_readwrite_bad, ioctl_write_ptr_bad};
use std::{
fmt,
fs::{File, OpenOptions},
io,
os::{fd::AsRawFd, raw::c_void},
path::Path,
ptr::null_mut,
time::Duration,
};
use tracing::*;
#[repr(i32)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum Direction {
None = -1,
ToDev = -2,
FromDev = -3,
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Flags: u32 {
const DIRECT_IO = 1;
const UNUSED_LUN_INHIBIT = 2;
const MMAP_IO = 4;
const NO_DXFER = 0x10000;
const Q_AT_TAIL = 0x10;
const Q_AT_HEAD = 0x20;
}
}
#[repr(u16)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum HostStatus {
Ok = 0x00,
NoConnect = 0x01,
BusBusy = 0x02,
Timeout = 0x03,
BadTarget = 0x04,
Abort = 0x05,
Parity = 0x06,
Error = 0x07,
Reset = 0x08,
BadIntr = 0x09,
Passthrough = 0x0A,
SoftError = 0x0B,
Unknown(u16),
}
impl From<u16> for HostStatus {
fn from(value: u16) -> Self {
match value {
0x00 => Self::Ok,
0x01 => Self::NoConnect,
0x02 => Self::BusBusy,
0x03 => Self::Timeout,
0x04 => Self::BadTarget,
0x05 => Self::Abort,
0x06 => Self::Parity,
0x07 => Self::Error,
0x08 => Self::Reset,
0x09 => Self::BadIntr,
0x0A => Self::Passthrough,
0x0B => Self::SoftError,
x => Self::Unknown(x),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
pub struct Info(u32);
impl Info {
pub const CHECK: u32 = 0x1;
pub const DIRECT_IO: u32 = 0x2;
pub const MIXED_IO: u32 = 0x4;
pub const fn check_status(self) -> u32 {
self.0 & 0x1
}
pub const fn io_type(self) -> u32 {
self.0 & 0x6
}
}
impl fmt::Debug for Info {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let check = match self.check_status() {
Self::CHECK => "CHECK",
_ => "OK",
};
let io = match self.io_type() {
Self::DIRECT_IO => "DIRECT_IO",
Self::MIXED_IO => "MIXED_IO",
_ => "INDIRECT_IO",
};
write!(f, "{check} | {io}")
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
struct SgIoHdr {
interface_id: i32,
dxfer_direction: Direction,
cmd_len: u8,
mx_sb_len: u8,
iovec_count: u16,
dxfer_len: u32,
dxferp: *mut c_void,
cmdp: *mut u8,
sbp: *mut u8,
timeout: u32,
flags: Flags,
pack_id: i32,
usr_ptr: *mut c_void,
status: u8,
masked_status: u8,
msg_status: u8,
sb_len_wr: u8,
host_status: u16,
driver_status: u16,
resid: i32,
duration: u32,
info: Info,
}
const SG_IO: u16 = 0x2285;
const SG_SET_RESERVED_SIZE: u16 = 0x2275;
const SG_GET_RESERVED_SIZE: u16 = 0x2272;
ioctl_readwrite_bad!(sg_io, SG_IO, SgIoHdr);
ioctl_write_ptr_bad!(sg_set_reserved_size, SG_SET_RESERVED_SIZE, i32);
ioctl_read_bad!(sg_get_reserved_size, SG_GET_RESERVED_SIZE, i32);
const WANTED_RESERVED_SIZE: i32 = 512 * 1024;
pub struct SgTransport {
file: File,
max_transfer: u32,
}
impl SgTransport {
pub fn open(path: impl AsRef<Path>) -> io::Result<Self> {
let file = OpenOptions::new().read(true).write(true).open(path)?;
let fd = file.as_raw_fd();
let mut reserved = 0i32;
unsafe {
let _ = sg_set_reserved_size(fd, &WANTED_RESERVED_SIZE);
sg_get_reserved_size(fd, &mut reserved)
.map_err(|e| io::Error::other(format!("SG_GET_RESERVED_SIZE: {e}")))?;
}
let max_transfer = reserved.max(0) as u32;
debug!(max_transfer, "Reserved buffer");
Ok(Self { file, max_transfer })
}
}
impl Transport for SgTransport {
#[instrument(skip_all, fields(cdb = ?cdb, ?data))]
fn execute(&mut self, cdb: &[u8], data: Data, timeout: Duration) -> Result<Completion, Error> {
if tracing::enabled!(target: "nkscan::cdb", Level::TRACE) {
let hex = |b: &[u8]| {
b.iter()
.map(|x| format!("{x:02X}"))
.collect::<Vec<_>>()
.join(" ")
};
match &data {
Data::Out(out) => {
trace!(target: "nkscan::cdb", "CDB ({} bytes): {}\n DATA-OUT ({} bytes): {}",
cdb.len(), hex(cdb), out.len(), hex(out))
}
_ => trace!(target: "nkscan::cdb", "CDB ({} bytes): {}", cdb.len(), hex(cdb)),
}
}
let mut cmd = cdb.to_vec();
let mut sb = [0u8; SENSE_REQUEST_LEN];
let (dir, data, data_len) = match data {
Data::None => (Direction::None, null_mut(), 0),
Data::In(x) => (Direction::FromDev, x.as_mut_ptr() as *mut c_void, x.len()),
Data::Out(x) => (Direction::ToDev, x.as_ptr() as *mut c_void, x.len()),
};
let mut hdr = SgIoHdr {
interface_id: b'S' as i32,
dxfer_direction: dir,
cmd_len: cmd.len() as u8,
mx_sb_len: sb.len() as u8,
iovec_count: 0,
dxfer_len: data_len as u32,
dxferp: data,
cmdp: cmd.as_mut_ptr(),
sbp: sb.as_mut_ptr(),
timeout: timeout.as_millis().min(u32::MAX as u128) as u32,
flags: Flags::empty(),
pack_id: 0,
usr_ptr: null_mut(),
status: 0,
masked_status: 0,
msg_status: 0,
sb_len_wr: 0,
host_status: 0,
driver_status: 0,
resid: 0,
duration: 0,
info: Info(0),
};
unsafe { sg_io(self.file.as_raw_fd(), &mut hdr) }.map_err(io::Error::from)?;
trace!(
host_status = ?HostStatus::from(hdr.host_status),
driver_status = hdr.driver_status,
duration_ms = hdr.duration,
info = ?hdr.info,
"SG_IO completed"
);
match hdr.host_status.into() {
HostStatus::Timeout => return Err(Error::Timeout(timeout)),
HostStatus::Ok => (),
x => return Err(io::Error::other(format!("SCSI bus fault: {x:?}")).into()),
}
let status = Status::from(hdr.status);
let sn = (hdr.sb_len_wr as usize).min(sb.len());
if sn > 0 && sn < 14 {
warn!(sb = ?&sb[..sn], sb_len_wr = hdr.sb_len_wr, "short sense buffer");
}
if status == Status::CheckCondition && sn < 14 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("CHECK CONDITION with {sn} bytes of sense, need 14"),
)
.into());
}
let sense = if sn >= 14 {
trace!(
sb_len_wr = hdr.sb_len_wr,
response_code = format!("{:#04x}", sb[0]),
additional_length = sb[7],
tail = ?&sb[14..sn],
raw = ?&sb[..sn],
"sense"
);
let tsc = (sn >= 16).then_some(sb[15]);
Some(sense_from_fixed(&sb[..sn], tsc))
} else {
None
};
let transferred = data_len.saturating_sub(hdr.resid.max(0) as usize);
Ok(Completion {
status,
sense,
transferred,
})
}
fn max_transfer(&self) -> usize {
self.max_transfer as usize
}
}