use std::os::unix::fs::OpenOptionsExt as _;
use std::os::unix::io::AsRawFd as _;
use crate::types::{Direction, MAX_SENSE_LEN, OpenOpts, ScsiCommand, ScsiStatus, Sense};
use crate::{Error, ScsiResult};
use std::path::Path;
const SG_IO: libc::c_ulong = 0x2285;
const SG_INTERFACE_ID_ORIG: libc::c_int = b'S' as libc::c_int; const SG_DXFER_NONE: libc::c_int = -1; const SG_DXFER_TO_DEV: libc::c_int = -2; const SG_DXFER_FROM_DEV: libc::c_int = -3;
#[repr(C)]
struct SgIoHdr {
interface_id: libc::c_int, dxfer_direction: libc::c_int, cmd_len: libc::c_uchar, mx_sb_len: libc::c_uchar, iovec_count: libc::c_ushort, dxfer_len: libc::c_uint, dxferp: *mut libc::c_void, cmdp: *mut libc::c_uchar, sbp: *mut libc::c_uchar, timeout: libc::c_uint, flags: libc::c_uint, pack_id: libc::c_int, usr_ptr: *mut libc::c_void, status: libc::c_uchar, masked_status: libc::c_uchar, msg_status: libc::c_uchar, sb_len_wr: libc::c_uchar, host_status: libc::c_ushort, driver_status: libc::c_ushort, resid: libc::c_int, duration: libc::c_uint, info: libc::c_uint, }
pub struct Device {
fd: std::fs::File,
}
impl Device {
pub fn open(path: &Path, opts: &OpenOpts) -> Result<Self, Error> {
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.custom_flags(libc::O_NONBLOCK)
.open(path)?;
let lock_op = if opts.exclusive {
libc::LOCK_EX | libc::LOCK_NB
} else {
libc::LOCK_SH | libc::LOCK_NB
};
let rc = unsafe { libc::flock(file.as_raw_fd(), lock_op) };
if rc != 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(Device { fd: file })
}
pub fn execute(&mut self, mut cmd: ScsiCommand) -> Result<ScsiResult, Error> {
let timeout_ms = cmd.timeout_secs.unwrap_or(30).saturating_mul(1000);
let dxfer_direction = match cmd.direction {
Direction::In => SG_DXFER_FROM_DEV,
Direction::Out => SG_DXFER_TO_DEV,
Direction::None => SG_DXFER_NONE,
};
let dxfer_len = u32::try_from(cmd.data.len())
.map_err(|_| Error::InvalidParameter("data buffer exceeds u32::MAX bytes"))?;
let dxferp: *mut libc::c_void = if cmd.data.is_empty() {
core::ptr::null_mut()
} else {
cmd.data.as_mut_ptr().cast()
};
let mut cdb = cmd.cdb.as_array();
let mut sense = [0u8; MAX_SENSE_LEN];
let mut hdr = SgIoHdr {
interface_id: SG_INTERFACE_ID_ORIG,
dxfer_direction,
cmd_len: cmd.cdb.len() as libc::c_uchar,
mx_sb_len: MAX_SENSE_LEN as libc::c_uchar,
iovec_count: 0,
dxfer_len,
dxferp,
cmdp: cdb.as_mut_ptr(),
sbp: sense.as_mut_ptr(),
timeout: timeout_ms,
flags: 0,
pack_id: 0,
usr_ptr: core::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: 0,
};
let rc = unsafe { libc::ioctl(self.fd.as_raw_fd(), SG_IO, &mut hdr as *mut SgIoHdr) };
if rc < 0 {
return Err(std::io::Error::last_os_error().into());
}
let transferred = if hdr.resid >= 0 {
(dxfer_len as usize).saturating_sub(hdr.resid as usize)
} else {
cmd.data.len()
};
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 = hdr.sb_len_wr;
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(&sense[..copy_len]);
Ok(ScsiResult {
status: ScsiStatus::from(hdr.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_linux.tmp");
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&path)?;
Ok(Device { fd: file })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Cdb, Direction, OpenOpts, ScsiCommand};
#[test]
fn sg_io_hdr_size() {
assert_eq!(
size_of::<SgIoHdr>(),
88,
"SgIoHdr must be 88 bytes on 64-bit Linux"
);
}
#[test]
fn sg_io_hdr_status_offset() {
assert_eq!(
core::mem::offset_of!(SgIoHdr, status),
36,
"status byte must be at offset 36 in SgIoHdr"
);
}
#[test]
fn open_nonexistent_path_returns_io_error() {
let result = Device::open(
Path::new("/dev/libscsi_nonexistent_999"),
&OpenOpts::default(),
);
assert!(matches!(result, Err(Error::Io(_))));
}
#[test]
fn open_shared_twice_succeeds() {
let path = std::env::temp_dir().join("libscsi_test_shared.tmp");
let _ = std::fs::File::create(&path).unwrap();
let opts = OpenOpts { exclusive: false };
let _d1 = Device::open(&path, &opts).expect("first shared open");
let _d2 = Device::open(&path, &opts).expect("second shared open");
}
#[test]
fn open_exclusive_blocked_by_shared() {
let path = std::env::temp_dir().join("libscsi_test_excl_block.tmp");
let _ = std::fs::File::create(&path).unwrap();
let _shared = Device::open(&path, &OpenOpts { exclusive: false }).expect("shared open");
let result = Device::open(&path, &OpenOpts { exclusive: true });
assert!(
matches!(result, Err(Error::Io(_))),
"exclusive open should be rejected while shared lock is held"
);
}
#[test]
fn open_shared_blocked_by_exclusive() {
let path = std::env::temp_dir().join("libscsi_test_shared_block.tmp");
let _ = std::fs::File::create(&path).unwrap();
let _excl = Device::open(&path, &OpenOpts { exclusive: true }).expect("exclusive open");
let result = Device::open(&path, &OpenOpts { exclusive: false });
assert!(
matches!(result, Err(Error::Io(_))),
"shared open should be rejected while exclusive lock is held"
);
}
#[test]
fn execute_valid_cdb_reaches_ioctl() {
let mut dev = Device::new_test().expect("temp file");
let cmd = ScsiCommand {
cdb: Cdb::new([0u8; 6]).unwrap(),
direction: Direction::None,
data: vec![],
timeout_secs: None,
};
assert!(matches!(dev.execute(cmd), Err(Error::Io(_))));
}
}