iscsi-client-rs 0.0.4

A pure-Rust iSCSI initiator library and CLI
Documentation
use anyhow::{Context, Result, anyhow};

pub const FIXED_MIN_LEN: usize = 18;

#[repr(C)]
#[derive(Default, PartialEq)]
pub struct SenseData {
    pub valid: bool,
    pub response_code: u8,
    pub sense_key: u8,
    pub ili: bool,
    pub eom: bool,
    pub filemark: bool,
    pub information: u32,
    pub additional_len: u8,
    pub cmd_specific: u32,
    pub asc: u8,
    pub ascq: u8,
}

impl SenseData {
    /// Парсим sense, допускаем 2-байтовый BE-префикс длины: [len_hi,len_lo] +
    /// sense...
    pub fn parse(buf: &[u8]) -> Result<Self> {
        if buf.len() < FIXED_MIN_LEN {
            return Err(anyhow!("sense buffer too small: {}", buf.len()));
        }

        let sense = if buf.len() >= 3 {
            let maybe_len = u16::from_be_bytes([buf[0], buf[1]]) as usize;
            let rc = buf[2] & 0x7F;
            if maybe_len + 2 == buf.len() && matches!(rc, 0x70..=0x73) {
                &buf[2..]
            } else {
                buf
            }
        } else {
            buf
        };

        if sense.len() < FIXED_MIN_LEN {
            return Err(anyhow!(
                "sense payload too small after prefix stripping: {}",
                sense.len()
            ));
        }

        let response_code = sense[0] & 0x7F;

        match response_code {
            0x70 | 0x71 => Self::parse_fixed(sense),
            0x72 | 0x73 => Err(anyhow!(
                "descriptor-format sense (0x{:02x}) is not supported yet",
                response_code
            )),
            other => Err(anyhow!("unknown sense response code 0x{:02x}", other)),
        }
    }

    fn parse_fixed(sense: &[u8]) -> Result<Self> {
        if sense.len() < FIXED_MIN_LEN {
            return Err(anyhow!("fixed sense too small: {}", sense.len()));
        }

        let valid = sense[0] & 0x80 != 0;
        let response_code = sense[0] & 0x7F;

        let filemark = sense[2] & 0x80 != 0;
        let eom = sense[2] & 0x40 != 0;
        let ili = sense[2] & 0x20 != 0;
        let sense_key = sense[2] & 0x0F;

        let information = u32::from_be_bytes(
            sense[3..7]
                .try_into()
                .context("failed to read Information (3..6)")?,
        );

        let additional_len = sense[7];

        let needed = 8usize + (additional_len as usize);
        if sense.len() < needed {
            return Err(anyhow!(
                "sense length mismatch: have {}, need at least {} (additional_len={})",
                sense.len(),
                needed,
                additional_len
            ));
        }

        let cmd_specific = u32::from_be_bytes(
            sense[8..12]
                .try_into()
                .context("failed to read Cmd-specific (8..11)")?,
        );

        let asc = sense[12];
        let ascq = sense[13];

        Ok(SenseData {
            valid,
            response_code,
            sense_key,
            ili,
            eom,
            filemark,
            information,
            additional_len,
            cmd_specific,
            asc,
            ascq,
        })
    }
}

impl core::fmt::Debug for SenseData {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("SenseData")
            .field("valid", &self.valid)
            .field(
                "response_code",
                &format_args!("{:#04x}", self.response_code),
            )
            .field("sense_key", &format_args!("{:#x}", self.sense_key))
            .field("filemark", &self.filemark)
            .field("eom", &self.eom)
            .field("ili", &self.ili)
            .field("information", &self.information)
            .field("additional_len", &self.additional_len)
            .field("cmd_specific", &self.cmd_specific)
            .field("asc", &format_args!("{:#04x}", self.asc))
            .field("ascq", &format_args!("{:#04x}", self.ascq))
            .field("description", &asc_ascq_to_str(self.asc, self.ascq))
            .finish()
    }
}

/// Return the SPC-4 description for a given ASC/ASCQ pair.
///
/// * If the pair is not present in the official table, returns `"UNSPECIFIED /
///   vendor specific"`.
#[inline]
pub fn asc_ascq_to_str(asc: u8, ascq: u8) -> &'static str {
    hot_table(asc, ascq).unwrap_or("UNSPECIFIED / vendor specific")
}

fn hot_table(asc: u8, ascq: u8) -> Option<&'static str> {
    Some(match (asc, ascq) {
        (0x00, 0x00) => "No additional sense information",
        (0x02, 0x04) => "Not ready – LUN not ready, format in progress",
        (0x03, 0x11) => "Medium error – unrecovered read error",
        (0x04, 0x01) => "Logical unit is in process of becoming ready",
        (0x05, 0x20) => "Illegal request – invalid command information field",
        (0x24, 0x00) => "Illegal request – invalid field in CDB",
        (0x25, 0x00) => "Illegal request – logical unit not supported",
        (0x29, 0x00) => "Unit attention – power on, reset, or bus device reset occurred",
        (0x3A, 0x00) => "Medium not present",
        (0x40, 0x00) => "Data integrity error",
        _ => return None,
    })
}