scsir 0.3.0

A simple library for issuing SCSI commands
Documentation
#![allow(dead_code)]

use std::{
    borrow::BorrowMut,
    fs::OpenOptions,
    io,
    mem::size_of_val,
    path::{Path, PathBuf},
    time::Duration,
};

use crate::{file_descriptor::FileDescriptor, Command};

#[derive(Debug)]
pub struct Scsi {
    path: PathBuf,
    file_descriptor: FileDescriptor,
    timeout: Duration,
}

impl Scsi {
    pub fn new<P: AsRef<Path> + ?Sized>(path: &P) -> crate::Result<Scsi> {
        let mut options = OpenOptions::new();
        options.read(true).write(true);
        let file_descriptor = FileDescriptor::open(&path, options)?;
        Self::from_descriptor(path, file_descriptor)
    }

    pub fn new_readonly<P: AsRef<Path> + ?Sized>(path: &P) -> crate::Result<Scsi> {
        let mut options = OpenOptions::new();
        options.read(true);
        let file_descriptor = FileDescriptor::open(&path, options)?;
        Self::from_descriptor(path, file_descriptor)
    }

    #[cfg(target_os = "linux")]
    pub fn issue<T: Command>(&self, command: &T) -> T::ReturnType {
        use nix::libc;

        use crate::{
            command::sense::{SenseData, MAX_SENSE_BUFFER_LENGTH},
            os::linux::{AccessFlags, AuxiliaryInfo, DriverStatus, SgIoHeader},
            result_data::{ResultData, Status},
        };

        const SG_IO: u32 = 0x2285;

        let command_buffer = command.command();
        let mut data_buffer = command.data();
        let raw_pointer_to_data_buffer = &mut data_buffer as *mut _;
        let mut sense_buffer = [0u8; MAX_SENSE_BUFFER_LENGTH];

        let size_of_command_buffer = size_of_val(&command_buffer) as u8;
        let size_of_data_buffer = command.data_size();
        let size_of_sense_buffer = size_of_val(&sense_buffer) as u8;

        let pointer_of_command_buffer = Some(&command_buffer);

        let pointer_of_data_buffer = if size_of_data_buffer == 0 {
            None
        } else {
            Some(data_buffer.borrow_mut())
        };

        let pointer_of_sense_buffer = Some(&mut sense_buffer);

        let mut sg_header = SgIoHeader {
            interface_id: b'S' as i32,
            data_direction: command.direction().into(),
            command_length: size_of_command_buffer,
            max_sense_buffer_length: size_of_sense_buffer,
            iovec_count: 0,
            data_length: size_of_data_buffer,
            data: pointer_of_data_buffer,
            command: pointer_of_command_buffer,
            sense_buffer: pointer_of_sense_buffer,
            timeout: self
                .timeout
                .as_millis()
                .clamp(u32::MIN as u128, u32::MAX as u128) as u32,
            flags: AccessFlags::DEFAULT,
            pack_id: 0,
            user_pointer: 0,
            status: 0,
            masked_status: 0,
            message_status: 0,
            sense_buffer_written: 0,
            host_status: 0,
            driver_status: DriverStatus::OK,
            residual_count: 0,
            duration: 0,
            info: AuxiliaryInfo::OK,
        };

        let ioctl_result = unsafe {
            libc::ioctl(
                self.file_descriptor.raw(),
                SG_IO.try_into().unwrap(),
                &mut sg_header,
            )
        };

        let sense_buffer_written = sg_header.sense_buffer_written as usize;
        let sense_data = sg_header
            .sense_buffer
            .map(|b| SenseData::parse(b, sense_buffer_written))
            .unwrap_or(SenseData::None);

        let result_data = ResultData {
            ioctl_result,
            transfered_data_length: sg_header.data_length as usize
                - sg_header.residual_count as usize,
            data: unsafe { &mut *raw_pointer_to_data_buffer },
            transfered_sense_length: sense_buffer_written,
            sense_buffer: &sense_data,
            status: Status::from(sg_header.status),
            host_status: sg_header.host_status.into(),
            driver_status: sg_header.driver_status,
        };

        command.process_result(result_data)
    }

    #[cfg(target_os = "windows")]
    pub fn issue<T: Command>(&self, command: &T) -> T::ReturnType {
        use std::slice;

        use windows::Win32::{
            Foundation::HANDLE,
            Storage::IscsiDisc::{
                IOCTL_SCSI_PASS_THROUGH_DIRECT, SCSI_IOCTL_DATA_BIDIRECTIONAL, SCSI_IOCTL_DATA_IN,
                SCSI_IOCTL_DATA_OUT, SCSI_IOCTL_DATA_UNSPECIFIED,
            },
            System::IO::DeviceIoControl,
        };

        use crate::{
            command::sense::SenseData,
            os::windows::ScsiPassThroughDirectWrapper,
            result_data::{ResultData, Status},
        };

        const MAX_COMMAND_LENGTH: u8 = 16;

        let command_buffer = command.command();
        let mut data_buffer = command.data();

        let size_of_command_buffer = size_of_val(&command_buffer) as u8;
        let size_of_data_buffer = command.data_size();

        if size_of_command_buffer > MAX_COMMAND_LENGTH {
            panic!(
                "Current command length is {}, max command length is {}",
                size_of_command_buffer, MAX_COMMAND_LENGTH
            );
        }
        let command_pointer = &command_buffer as *const _ as *const u8;
        let command_slice =
            unsafe { slice::from_raw_parts(command_pointer, size_of_command_buffer as usize) };

        let mut header = ScsiPassThroughDirectWrapper::default();
        let address_of_header = std::ptr::addr_of!(header) as usize;
        let mut spt = &mut header.scsi_pass_through;
        spt.Length = size_of_val(spt) as u16;
        spt.CdbLength = size_of_command_buffer;
        spt.SenseInfoLength = size_of_val(&header.sense) as u8;
        spt.DataIn = match command.direction() {
            crate::DataDirection::None => SCSI_IOCTL_DATA_UNSPECIFIED,
            crate::DataDirection::ToDevice => SCSI_IOCTL_DATA_OUT,
            crate::DataDirection::FromDevice => SCSI_IOCTL_DATA_IN,
            crate::DataDirection::ToFromDevice => SCSI_IOCTL_DATA_BIDIRECTIONAL,
            crate::DataDirection::Unknown => SCSI_IOCTL_DATA_UNSPECIFIED,
        } as u8;

        spt.DataTransferLength = size_of_data_buffer;

        spt.TimeOutValue = match self
            .timeout
            .as_secs()
            .clamp(u32::MIN as u64, u32::MAX as u64)
        {
            0 => 1,
            n => n as u32,
        };

        spt.DataBuffer = data_buffer.borrow_mut() as *mut _ as _;

        spt.SenseInfoOffset =
            (std::ptr::addr_of!(header.sense) as usize - address_of_header) as u32;

        spt.Cdb[..command_slice.len()].copy_from_slice(command_slice);

        let mut bytes_returned = 0;

        let success = unsafe {
            DeviceIoControl(
                HANDLE(self.file_descriptor.raw() as isize),
                IOCTL_SCSI_PASS_THROUGH_DIRECT,
                Some(&header as *const _ as _),
                size_of_val(&header) as u32,
                Some(&mut header as *mut _ as _),
                size_of_val(&header) as u32,
                Some(&mut bytes_returned),
                None,
            )
        };

        let ioctl_result = match success.as_bool() {
            true => 0,
            false => -1,
        };

        let sense_data = SenseData::parse(
            &header.sense,
            header.scsi_pass_through.SenseInfoLength as usize,
        );

        let result_data = ResultData {
            ioctl_result,
            transfered_data_length: header.scsi_pass_through.DataTransferLength as usize,
            data: &mut data_buffer,
            transfered_sense_length: header.scsi_pass_through.SenseInfoLength as usize,
            sense_buffer: &sense_data,
            status: Status::from(header.scsi_pass_through.ScsiStatus),
        };

        command.process_result(result_data)
    }

    pub fn path(&self) -> &PathBuf {
        &self.path
    }

    pub fn set_timeout(&mut self, timeout: Duration) {
        self.timeout = timeout;
    }

    pub fn timeout(&self) -> Duration {
        self.timeout
    }

    fn from_descriptor<P: AsRef<Path> + ?Sized>(
        path: &P,
        file_descriptor: FileDescriptor,
    ) -> crate::Result<Scsi> {
        if !file_descriptor.is_block()? {
            return Err(crate::Error::NotBlockDevice(path.as_ref().to_owned()));
        }

        if !Self::is_scsi_device(&file_descriptor)? {
            return Err(crate::Error::NotScsiDevice(path.as_ref().to_owned()));
        }

        Ok(Scsi {
            path: path.as_ref().to_owned(),
            file_descriptor,
            timeout: Duration::from_millis(SG_DEFAULT_TIMEOUT),
        })
    }

    #[cfg(target_os = "linux")]
    fn is_scsi_device(file: &FileDescriptor) -> crate::Result<bool> {
        use nix::libc;

        const SG_GET_VERSION_NUM: u32 = 0x2282;

        let mut version = 0_i32;
        let result = unsafe {
            libc::ioctl(
                file.raw(),
                SG_GET_VERSION_NUM.try_into().unwrap(),
                &mut version,
            )
        };

        if result != 0 {
            Err(io::Error::last_os_error())?;
        }

        if version < 30000 {
            Ok(false)
        } else {
            Ok(true)
        }
    }

    #[cfg(target_os = "windows")]
    fn is_scsi_device(file: &FileDescriptor) -> crate::Result<bool> {
        use std::mem::size_of;

        use windows::Win32::Foundation::HANDLE;
        use windows::Win32::Storage::IscsiDisc::{IOCTL_SCSI_GET_ADDRESS, SCSI_ADDRESS};
        use windows::Win32::System::IO::DeviceIoControl;

        let mut scsi_address = SCSI_ADDRESS::default();
        let mut bytes_returned = 0;
        let success = unsafe {
            DeviceIoControl(
                HANDLE(file.raw() as isize),
                IOCTL_SCSI_GET_ADDRESS,
                None,
                0,
                Some(&mut scsi_address as *mut _ as _),
                size_of::<SCSI_ADDRESS>() as u32,
                Some(&mut bytes_returned),
                None,
            )
        };

        if success == false {
            Err(io::Error::last_os_error())?;
        }

        if bytes_returned == 0 {
            Ok(false)
        } else {
            Ok(true)
        }
    }
}

const SG_DEFAULT_TIMEOUT: u64 = 60_000;