Skip to main content

iscsi_client_rs/models/
opcode.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright (C) 2012-2025 Andrei Maltsev
3
4//! Helpers for encoding / decoding the very first byte of every iSCSI
5//! **Basic-Header-Segment** (BHS).
6//!
7//! The byte layout is defined by RFC 7143 § 5.3:
8//!
9//! ```text
10//!  7   6   5   4   3   2   1   0      bit position
11//! +---+---+---------------------------+
12//! | . | I |        OPCODE (6 bits)    |  ← first BHS octet
13//! +---+---+---------------------------+
14//! ```
15//!
16//! * **I** – *Immediate* flag.  When set, the PDU is processed by the target
17//!   before any queued commands.
18//! * **OPCODE** – 6-bit operation code identifying the PDU type.
19//!
20//! The utilities below allow you to
21//!
22//! * split the raw byte into a pair `(IfFlags, Opcode)` (`TryFrom<u8>`)
23//! * merge a pair back into the raw byte (`From<&BhsOpcode> for u8`).
24
25use core::fmt;
26use std::convert::TryFrom;
27
28use thiserror::Error;
29use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
30
31/// Mask that selects the lower 6 bits (**OPCODE**) from the first BHS byte.
32const OPCODE_MASK: u8 = 0b0011_1111;
33/// Mask that selects the upper 1 bits (**I**) from the first BHS byte.
34const I_MASK: u8 = 0b0100_0000;
35
36/// All op-codes defined by RFC 3720 & RFC 7143 (§ 9.1).
37#[repr(u8)]
38#[derive(Debug, Default, Clone, PartialEq, Eq)]
39pub enum Opcode {
40    #[default]
41    NopOut = 0x00,
42    ScsiCommandReq = 0x01,
43    ScsiTaskMgmtReq = 0x02,
44    LoginReq = 0x03,
45    TextReq = 0x04,
46    ScsiDataOut = 0x05,
47    LogoutReq = 0x06,
48    /* 0x07–0x1F reserved */
49    NopIn = 0x20,
50    ScsiCommandResp = 0x21,
51    ScsiTaskMgmtResp = 0x22,
52    LoginResp = 0x23,
53    TextResp = 0x24,
54    ScsiDataIn = 0x25,
55    LogoutResp = 0x26,
56    ReadyToTransfer = 0x31,
57    /* 0x27–0x3E reserved */
58    Reject = 0x3F,
59}
60
61impl Opcode {
62    #[inline]
63    pub fn from_u6(v: u8) -> Option<Self> {
64        Some(match v {
65            0x00 => Self::NopOut,
66            0x01 => Self::ScsiCommandReq,
67            0x02 => Self::ScsiTaskMgmtReq,
68            0x03 => Self::LoginReq,
69            0x04 => Self::TextReq,
70            0x05 => Self::ScsiDataOut,
71            0x06 => Self::LogoutReq,
72            0x20 => Self::NopIn,
73            0x21 => Self::ScsiCommandResp,
74            0x22 => Self::ScsiTaskMgmtResp,
75            0x23 => Self::LoginResp,
76            0x24 => Self::TextResp,
77            0x25 => Self::ScsiDataIn,
78            0x26 => Self::LogoutResp,
79            0x31 => Self::ReadyToTransfer,
80            0x3F => Self::Reject,
81            _ => return None,
82        })
83    }
84}
85
86/// Returned when the lower six bits contain an undefined op-code.
87#[derive(Debug, Error)]
88#[error("invalid opcode: 0x{0:02x}")]
89pub struct UnknownOpcode(pub u8);
90
91/// Typed representation of the very first BHS byte.
92///
93/// * `flags`  – high-order **I** bit.
94/// * `opcode` – 6-bit op-code.
95#[derive(Debug, PartialEq, Eq, Default)]
96#[repr(C)]
97pub struct BhsOpcode {
98    pub flags: bool,
99    pub opcode: Opcode,
100}
101
102impl TryFrom<u8> for BhsOpcode {
103    type Error = anyhow::Error;
104
105    fn try_from(byte: u8) -> Result<Self, Self::Error> {
106        let flags = (byte & I_MASK) != 0;
107        let code = byte & OPCODE_MASK;
108        let opcode = Opcode::from_u6(code).ok_or(UnknownOpcode(code))?;
109        Ok(Self { flags, opcode })
110    }
111}
112
113impl From<&BhsOpcode> for u8 {
114    fn from(b: &BhsOpcode) -> u8 {
115        let mut raw = b.opcode.clone() as u8;
116        if b.flags {
117            raw |= I_MASK;
118        }
119        raw
120    }
121}
122
123/// Wire-safe, zero-copy first BHS octet.
124/// Transparent over `u8`, so it can live inside a zerocopy BHS struct.
125#[repr(transparent)]
126#[derive(Clone, Default, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable)]
127pub struct RawBhsOpcode(u8);
128
129impl RawBhsOpcode {
130    #[inline]
131    pub const fn raw(&self) -> u8 {
132        self.0
133    }
134
135    #[inline]
136    pub const fn from_raw(v: u8) -> Self {
137        Self(v)
138    }
139
140    // Flags
141    #[inline]
142    pub const fn i(&self) -> bool {
143        (self.0 & I_MASK) != 0
144    }
145
146    #[inline]
147    pub fn set_i(&mut self) {
148        self.0 |= I_MASK
149    }
150
151    // Opcode (lower 6 bits)
152    #[inline]
153    pub const fn opcode_raw(&self) -> u8 {
154        self.0 & OPCODE_MASK
155    }
156
157    #[inline]
158    pub fn set_opcode_raw(&mut self, v: u8) {
159        self.0 = (self.0 & !OPCODE_MASK) | (v & OPCODE_MASK)
160    }
161
162    #[inline]
163    pub fn opcode_known(&self) -> Option<Opcode> {
164        Opcode::from_u6(self.opcode_raw())
165    }
166
167    #[inline]
168    pub fn set_opcode_known(&mut self, k: Opcode) {
169        self.set_opcode_raw(k as u8);
170    }
171}
172
173impl fmt::Debug for RawBhsOpcode {
174    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175        match BhsOpcode::try_from(self.0) {
176            Ok(bhs) => {
177                let mut tmp = f.debug_struct("RawBhsOpcode");
178                if bhs.flags {
179                    tmp.field("I", &bhs.flags);
180                }
181                tmp.field("opcode", &bhs.opcode).finish()
182            },
183            Err(_) => {
184                let mut tmp = f.debug_struct("RawBhsOpcode");
185                if self.i() {
186                    tmp.field("I", &self.i());
187                }
188                tmp.field("opcode_raw", &format_args!("0x{:02X}", self.opcode_raw()))
189                    .finish()
190            },
191        }
192    }
193}