Skip to main content

iscsi_client_rs/models/data/
sense_data.rs

1//! This module defines the structures for SCSI Sense Data.
2//! It provides a parser for sense data and a function to convert ASC/ASCQ codes
3//! to strings.
4
5// SPDX-License-Identifier: AGPL-3.0-or-later
6// Copyright (C) 2012-2025 Andrei Maltsev
7
8use core::fmt;
9
10use anyhow::{Context, Result, anyhow};
11
12use crate::models::data::Entry;
13
14/// The minimum length of a fixed-format sense data structure.
15pub const FIXED_MIN_LEN: usize = 18;
16
17/// Represents SCSI Sense Data, providing detailed error information.
18#[repr(C)]
19#[derive(Default, PartialEq)]
20pub struct SenseData {
21    /// Indicates if the information field is valid.
22    pub valid: bool,
23    /// The response code, indicating the format of the sense data.
24    pub response_code: u8,
25    /// The general category of the error.
26    pub sense_key: u8,
27    /// Incorrect Length Indicator.
28    pub ili: bool,
29    /// End-of-Medium indicator.
30    pub eom: bool,
31    /// Filemark indicator.
32    pub filemark: bool,
33    /// Command-specific information.
34    pub information: u32,
35    /// The length of the additional sense data.
36    pub additional_len: u8,
37    /// Command-specific information.
38    pub cmd_specific: u32,
39    /// Additional Sense Code.
40    pub asc: u8,
41    /// Additional Sense Code Qualifier.
42    pub ascq: u8,
43}
44
45impl SenseData {
46    /// Parses a byte buffer into a `SenseData` structure.
47    pub fn parse(buf: &[u8]) -> Result<Self> {
48        if buf.len() < FIXED_MIN_LEN {
49            return Err(anyhow!("sense buffer too small: {}", buf.len()));
50        }
51
52        let sense = if buf.len() >= 3 {
53            let maybe_len = u16::from_be_bytes([buf[0], buf[1]]) as usize;
54            let rc = buf[2] & 0x7F;
55            if maybe_len + 2 == buf.len() && matches!(rc, 0x70..=0x73) {
56                &buf[2..]
57            } else {
58                buf
59            }
60        } else {
61            buf
62        };
63
64        if sense.len() < FIXED_MIN_LEN {
65            return Err(anyhow!(
66                "sense payload too small after prefix stripping: {}",
67                sense.len()
68            ));
69        }
70
71        let response_code = sense[0] & 0x7F;
72
73        match response_code {
74            0x70 | 0x71 => Self::parse_fixed(sense),
75            0x72 | 0x73 => Err(anyhow!(
76                "descriptor-format sense (0x{:02x}) is not supported yet",
77                response_code
78            )),
79            other => Err(anyhow!("unknown sense response code 0x{:02x}", other)),
80        }
81    }
82
83    fn parse_fixed(sense: &[u8]) -> Result<Self> {
84        if sense.len() < FIXED_MIN_LEN {
85            return Err(anyhow!("fixed sense too small: {}", sense.len()));
86        }
87
88        let valid = sense[0] & 0x80 != 0;
89        let response_code = sense[0] & 0x7F;
90
91        let filemark = sense[2] & 0x80 != 0;
92        let eom = sense[2] & 0x40 != 0;
93        let ili = sense[2] & 0x20 != 0;
94        let sense_key = sense[2] & 0x0F;
95
96        let information = u32::from_be_bytes(
97            sense[3..7]
98                .try_into()
99                .context("failed to read Information (3..6)")?,
100        );
101
102        let additional_len = sense[7];
103
104        let needed = 8usize + (additional_len as usize);
105        if sense.len() < needed {
106            return Err(anyhow!(
107                "sense length mismatch: have {}, need at least {} (additional_len={})",
108                sense.len(),
109                needed,
110                additional_len
111            ));
112        }
113
114        let cmd_specific = u32::from_be_bytes(
115            sense[8..12]
116                .try_into()
117                .context("failed to read Cmd-specific (8..11)")?,
118        );
119
120        let asc = sense[12];
121        let ascq = sense[13];
122
123        Ok(SenseData {
124            valid,
125            response_code,
126            sense_key,
127            ili,
128            eom,
129            filemark,
130            information,
131            additional_len,
132            cmd_specific,
133            asc,
134            ascq,
135        })
136    }
137}
138
139impl fmt::Debug for SenseData {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        f.debug_struct("SenseData")
142            .field("valid", &self.valid)
143            .field(
144                "response_code",
145                &format_args!("{:#04x}", self.response_code),
146            )
147            .field("sense_key", &format_args!("{:#x}", self.sense_key))
148            .field("filemark", &self.filemark)
149            .field("eom", &self.eom)
150            .field("ili", &self.ili)
151            .field("information", &self.information)
152            .field("additional_len", &self.additional_len)
153            .field("cmd_specific", &self.cmd_specific)
154            .field("asc", &format_args!("{:#04x}", self.asc))
155            .field("ascq", &format_args!("{:#04x}", self.ascq))
156            .field("description", &asc_ascq_to_str(self.asc, self.ascq))
157            .finish()
158    }
159}
160
161/// Converts an ASC/ASCQ code pair to a human-readable string.
162#[inline]
163pub fn asc_ascq_to_str(asc: u8, ascq: u8) -> &'static str {
164    Entry::lookup(asc, ascq).unwrap_or("UNSPECIFIED / vendor specific")
165}