Skip to main content

iscsi_client_rs/models/
data_fromat.rs

1//! This module defines the generic PDU container and related traits.
2//! It provides a generic structure for iSCSI PDUs, handling data, headers, and
3//! digests.
4
5// SPDX-License-Identifier: AGPL-3.0-or-later
6// Copyright (C) 2012-2025 Andrei Maltsev
7
8use std::{any::type_name, fmt, marker::PhantomData, ops::Deref};
9
10use anyhow::{Context, Result, anyhow, bail};
11use bytes::{Bytes, BytesMut};
12use crc32c::crc32c_append;
13use zerocopy::{
14    BigEndian, FromBytes as ZFromBytes, Immutable, IntoBytes, KnownLayout, U32,
15};
16
17use crate::{
18    cfg::{
19        config::Config,
20        enums::{Digest, YesNo},
21    },
22    client::pdu_connection::FromBytes,
23    models::{
24        common::{BasicHeaderSegment, Builder, HEADER_LEN, SendingData},
25        data::sense_data::SenseData,
26        opcode::Opcode,
27    },
28};
29
30/// A marker trait for types that can be used with zerocopy and are suitable for
31/// iSCSI PDUs.
32pub trait ZeroCopyType: KnownLayout + Immutable + IntoBytes + ZFromBytes {}
33
34#[inline]
35fn pad_len(n: usize) -> usize {
36    (4 - (n % 4)) % 4
37}
38
39#[inline]
40fn crc32c_of_parts(parts: &[&[u8]]) -> u32 {
41    let mut acc = 0u32;
42    for p in parts {
43        if !p.is_empty() {
44            acc = crc32c_append(acc, p);
45        }
46    }
47    acc
48}
49
50#[inline]
51fn crc32c_with_padding(parts: &[&[u8]], pad: usize) -> u32 {
52    let mut acc = crc32c_of_parts(parts);
53    if pad != 0 {
54        let zeros = [0u8; 3];
55        acc = crc32c_append(acc, &zeros[..pad]);
56    }
57    acc
58}
59
60#[inline]
61fn compute_header_digest(bhs: &[u8], ahs: &[u8]) -> u32 {
62    crc32c_with_padding(&[bhs, ahs], pad_len(ahs.len()))
63}
64
65#[inline]
66fn compute_data_digest(data: &[u8]) -> u32 {
67    crc32c_with_padding(&[data], pad_len(data.len()))
68}
69
70/// A type alias for a PDU request, which uses a mutable `BytesMut` body.
71pub type PduRequest<T> = PDUWithData<T, BytesMut>;
72/// A type alias for a PDU response, which uses an immutable `Bytes` body.
73pub type PduResponse<T> = PDUWithData<T, Bytes>;
74
75/// A generic container for an iSCSI Protocol Data Unit (PDU).
76///
77/// This struct holds the PDU's header, payload (data), and digest information.
78/// It is generic over the body type, allowing it to be used for both requests
79/// (with a mutable body) and responses (with an immutable body).
80#[derive(PartialEq)]
81pub struct PDUWithData<T, Body = Bytes> {
82    /// The raw buffer for the Basic Header Segment (BHS).
83    pub header_buf: [u8; HEADER_LEN],
84    payload: Body,
85
86    enable_header_digest: bool,
87    enable_data_digest: bool,
88    allocated_header_diggest: bool,
89    /// The optional header digest value.
90    pub header_digest: Option<U32<BigEndian>>,
91    /// The optional data digest value.
92    pub data_digest: Option<U32<BigEndian>>,
93
94    pub is_x86: bool,
95
96    _marker: PhantomData<T>,
97}
98
99impl<T, Body: Clone> Clone for PDUWithData<T, Body> {
100    fn clone(&self) -> Self {
101        Self {
102            header_buf: self.header_buf,
103            payload: self.payload.clone(),
104            enable_header_digest: self.enable_header_digest,
105            enable_data_digest: self.enable_data_digest,
106            allocated_header_diggest: self.allocated_header_diggest,
107            header_digest: self.header_digest,
108            data_digest: self.data_digest,
109            is_x86: self.is_x86,
110            _marker: PhantomData,
111        }
112    }
113}
114
115impl<T> Builder for PDUWithData<T, BytesMut>
116where T: BasicHeaderSegment + SendingData + FromBytes + ZeroCopyType
117{
118    type Body = Bytes;
119    type Header = [u8; HEADER_LEN];
120
121    /// On the first call, if HeaderDigest is enabled, reserve exactly one
122    /// 4-byte slot for it right before DATA; then append DATA and update
123    /// DataSegmentLength.
124    fn append_data(&mut self, more: &[u8]) {
125        let hd_len = self
126            .header_view()
127            .expect("uninitialized header")
128            .get_header_diggest(self.enable_header_digest);
129        if !self.allocated_header_diggest && hd_len != 0 {
130            self.payload.extend_from_slice(&[0u8; 4][..hd_len]);
131        }
132        self.allocated_header_diggest = true;
133
134        if !more.is_empty() {
135            self.payload.extend_from_slice(more);
136            // increment DataSegmentLength ONLY by DATA bytes
137            let old = self
138                .header_view()
139                .expect("header_view failed")
140                .get_data_length_bytes();
141            let new_len = old.saturating_add(more.len()) as u32;
142            self.header_view_mut()
143                .expect("header_view_mut failed")
144                .set_data_length_bytes(new_len);
145        }
146    }
147
148    /// Finalize and return the already-laid-out body as Bytes.
149    /// Ensures HeaderDigest slot exists (even for zero DATA), appends pad(DATA)
150    /// and DataDigest.
151    fn build(
152        &mut self,
153        max_recv_data_segment_length: usize,
154    ) -> Result<(Self::Header, Self::Body)> {
155        let (opcode, ahs_len, data_len, hd_len, dd_len) = {
156            let enable_hd = self.enable_header_digest;
157            let enable_dd = self.enable_data_digest;
158
159            let h = self.header_view_mut().expect("building without header_buf");
160            let opcode = h.get_opcode()?.opcode;
161            h.set_final_bit();
162            let ahs_len = h.get_ahs_length_bytes();
163            let data_len = h.get_data_length_bytes();
164            let hd_len = h.get_header_diggest(enable_hd); // 0 or 4
165            let dd_len = h.get_data_diggest(enable_dd); // 0 or 4
166            (opcode, ahs_len, data_len, hd_len, dd_len)
167        };
168
169        if data_len > max_recv_data_segment_length {
170            bail!(
171                "MaxRecvDataSegmentLength({max_recv_data_segment_length}) < \
172                 data_len({data_len})"
173            );
174        }
175
176        // compute pads
177        let ahs_pad = pad_len(ahs_len);
178        let data_pad = pad_len(data_len);
179        self.append_data(&[]); // Allocate place for crc32 header
180        self.payload.extend_from_slice(&[0u8; 4][..data_pad]);
181
182        if hd_len != 0 && opcode != Opcode::LoginReq {
183            let hd = compute_header_digest(&self.header_buf, self.additional_header()?);
184            self.header_digest = Some(U32::<BigEndian>::new(hd));
185            let expected_slice = [hd.to_le_bytes(), hd.to_be_bytes()];
186            self.payload
187                .get_mut(0..hd_len)
188                .context("failed to get slice for crc in payload")?
189                .clone_from_slice(&expected_slice[self.is_x86 as usize]);
190        }
191
192        // current payload should be: [AHS][padAHS][HD?][DATA]
193        // we now append [padDATA][DD?] (exactly once)
194        if dd_len != 0 && opcode != Opcode::LoginReq {
195            let dd = compute_data_digest(self.data()?);
196            self.data_digest = Some(U32::<BigEndian>::new(dd));
197            let expected_slice = [dd.to_le_bytes(), dd.to_be_bytes()];
198            self.payload
199                .extend_from_slice(&expected_slice[self.is_x86 as usize]);
200        }
201
202        let expected = ahs_len + ahs_pad + hd_len + data_len + data_pad + dd_len;
203        let actual = self.payload.len();
204        if actual != expected {
205            bail!(
206                "payload size mismatch: actual={}, expected={} (ahs={} padAHS={} hd={} \
207                 data={} padDATA={} dd={})",
208                actual,
209                expected,
210                ahs_len,
211                ahs_pad,
212                hd_len,
213                data_len,
214                data_pad,
215                dd_len
216            );
217        }
218
219        let body = self.payload.clone();
220        Ok((self.header_buf, body.freeze()))
221    }
222}
223
224impl<T> PDUWithData<T, Bytes> {
225    /// Creates a new `PDUWithData` instance from a header slice and
226    /// configuration.
227    pub fn from_header_slice(header_buf: [u8; HEADER_LEN], cfg: &Config) -> Self {
228        Self {
229            header_buf,
230            payload: Bytes::new(),
231            enable_header_digest: cfg.login.integrity.header_digest == Digest::CRC32C,
232            header_digest: None,
233            allocated_header_diggest: false,
234            enable_data_digest: cfg.login.integrity.data_digest == Digest::CRC32C,
235            data_digest: None,
236            is_x86: cfg.login.identity.is_x86 == YesNo::Yes,
237            _marker: PhantomData,
238        }
239    }
240}
241
242impl<T> PDUWithData<T, BytesMut> {
243    /// Creates a new `PDUWithData` request instance with a mutable body.
244    pub fn new_request(header_buf: [u8; HEADER_LEN], cfg: &Config) -> Self {
245        Self {
246            header_buf,
247            payload: BytesMut::new(),
248            enable_header_digest: cfg.login.integrity.header_digest == Digest::CRC32C,
249            header_digest: None,
250            allocated_header_diggest: false,
251            enable_data_digest: cfg.login.integrity.data_digest == Digest::CRC32C,
252            data_digest: None,
253            is_x86: cfg.login.identity.is_x86 == YesNo::Yes,
254            _marker: PhantomData,
255        }
256    }
257
258    /// Parses the PDU payload from a mutable buffer, verifying digests.
259    pub fn parse_with_buff_mut(&mut self, mut buf: BytesMut) -> Result<()>
260    where T: BasicHeaderSegment + FromBytes + ZeroCopyType {
261        let tn = type_name::<T>();
262        let h = self.header_view().context("parsing without header_buf")?;
263
264        let ahs_len = h.get_ahs_length_bytes();
265        let hd_len = h.get_header_diggest(self.enable_header_digest);
266        let data_len = h.get_data_length_bytes();
267        let dd_len = h.get_data_diggest(self.enable_data_digest);
268
269        let ahs_pad = pad_len(ahs_len);
270        let data_pad = pad_len(data_len);
271
272        let need = ahs_len + ahs_pad + hd_len + data_len + data_pad + dd_len;
273        if buf.len() < need {
274            bail!("{tn}: buffer too small: have {}, need {}", buf.len(), need);
275        }
276
277        if buf.len() > need {
278            buf.truncate(need);
279        }
280
281        self.payload = buf;
282
283        let payload: &[u8] = &self.payload;
284
285        let mut off = ahs_len + ahs_pad;
286
287        self.header_digest = if self.enable_header_digest {
288            let expected_slice = payload[off..off + hd_len].try_into()?;
289            let hd = [
290                u32::from_le_bytes(expected_slice),
291                u32::from_be_bytes(expected_slice),
292            ];
293            off += hd_len;
294            Some(U32::<BigEndian>::new(hd[self.is_x86 as usize]))
295        } else {
296            None
297        };
298
299        off += data_len + data_pad;
300
301        self.data_digest = if self.enable_data_digest {
302            let expected_slice = payload[off..off + dd_len].try_into()?;
303            let dd = [
304                u32::from_le_bytes(expected_slice),
305                u32::from_be_bytes(expected_slice),
306            ];
307            Some(U32::<BigEndian>::new(dd[self.is_x86 as usize]))
308        } else {
309            None
310        };
311
312        if self.enable_header_digest {
313            let want = compute_header_digest(&self.header_buf, self.additional_header()?);
314            if self.header_digest.map(|x| x.get()) != Some(want) {
315                bail!("{tn}: HeaderDigest mismatch");
316            }
317        }
318        if self.enable_data_digest {
319            let data = self.data()?;
320            let want = compute_data_digest(data);
321            if !data.is_empty() && self.data_digest.map(|x| x.get()) != Some(want) {
322                bail!("{tn}: DataDigest mismatch");
323            }
324        }
325
326        Ok(())
327    }
328
329    /// Parses the PDU payload from a reference to a mutable buffer.
330    pub fn parse_with_buff_ref(&mut self, buf: &BytesMut) -> Result<()>
331    where T: BasicHeaderSegment + FromBytes + ZeroCopyType {
332        self.parse_with_buff_mut(buf.clone())
333    }
334}
335
336impl<T, B> PDUWithData<T, B>
337where
338    T: BasicHeaderSegment,
339    B: Deref<Target = [u8]>,
340{
341    /// Returns an immutable view of the PDU's header.
342    #[inline]
343    pub fn header_view(&self) -> Result<&T>
344    where T: FromBytes + ZeroCopyType {
345        T::ref_from_bytes(self.header_buf.as_slice()).map_err(|e| anyhow!("{}", e))
346    }
347
348    /// Returns a mutable view of the PDU's header.
349    #[inline]
350    pub fn header_view_mut(&mut self) -> Result<&mut T>
351    where T: FromBytes + ZeroCopyType {
352        T::mut_from_bytes(self.header_buf.as_mut_slice()).map_err(|e| anyhow!("{}", e))
353    }
354
355    /// Returns a slice of the Additional Header Segment (AHS).
356    pub fn additional_header(&self) -> Result<&[u8]>
357    where T: FromBytes + ZeroCopyType {
358        let ahs_size = self.header_view()?.get_ahs_length_bytes();
359        Ok(&self.payload[0..ahs_size])
360    }
361
362    /// Returns a slice of the PDU's data segment.
363    pub fn data(&self) -> Result<&[u8]>
364    where T: FromBytes + ZeroCopyType {
365        let header = self.header_view()?;
366        let ahs_len = header.get_ahs_length_bytes();
367        let hd = header.get_header_diggest(self.enable_header_digest);
368        let data_len = header.get_data_length_bytes();
369        let total = ahs_len + pad_len(ahs_len) + hd;
370        self.payload
371            .get(total..total + data_len)
372            .context("failed to get slice payload")
373    }
374
375    /// Rebinds the PDU to a different header type.
376    pub fn rebind_pdu<U>(self) -> anyhow::Result<PDUWithData<U, B>>
377    where U: BasicHeaderSegment {
378        Ok(PDUWithData::<U, B> {
379            header_buf: self.header_buf,
380            payload: self.payload,
381            enable_header_digest: self.enable_header_digest,
382            header_digest: self.header_digest,
383            allocated_header_diggest: self.allocated_header_diggest,
384            enable_data_digest: self.enable_data_digest,
385            data_digest: self.data_digest,
386            is_x86: self.is_x86,
387            _marker: PhantomData,
388        })
389    }
390}
391
392impl<T> PDUWithData<T, Bytes>
393where T: BasicHeaderSegment + FromBytes + ZeroCopyType
394{
395    /// Parses the PDU payload from an immutable buffer, verifying digests.
396    pub fn parse_with_buff(&mut self, buf: &Bytes) -> Result<()> {
397        let tn = type_name::<T>();
398
399        let h = self.header_view().context("parsing without header_buf")?;
400
401        let ahs_len = h.get_ahs_length_bytes();
402        let hd_len = h.get_header_diggest(self.enable_header_digest);
403        let data_len = h.get_data_length_bytes();
404        let dd_len = h.get_data_diggest(self.enable_data_digest);
405
406        let ahs_pad = pad_len(ahs_len);
407        let data_pad = pad_len(data_len);
408
409        let need = ahs_len + ahs_pad + hd_len + data_len + data_pad + dd_len;
410        if buf.len() < need {
411            bail!("{tn}: buffer too small: have {}, need {}", buf.len(), need);
412        }
413
414        self.payload = buf.clone();
415
416        let mut off = 0usize;
417        off += ahs_len + ahs_pad;
418
419        self.header_digest = if hd_len != 0 {
420            let hd = u32::from_le_bytes(buf[off..off + hd_len].try_into()?);
421            off += hd_len;
422            Some(U32::<BigEndian>::new(hd))
423        } else {
424            None
425        };
426
427        off += data_len + data_pad;
428
429        self.data_digest = if dd_len != 0 {
430            let dd = u32::from_le_bytes(buf[off..off + dd_len].try_into()?);
431            Some(U32::<BigEndian>::new(dd))
432        } else {
433            None
434        };
435
436        if hd_len != 0 {
437            let want = compute_header_digest(&self.header_buf, self.additional_header()?);
438            if self.header_digest.map(|x| x.get()) != Some(want) {
439                bail!("{tn}: HeaderDigest mismatch");
440            }
441        }
442        if dd_len != 0 {
443            let data = self.data()?;
444            let want = compute_data_digest(data);
445            if !data.is_empty() && self.data_digest.map(|x| x.get()) != Some(want) {
446                bail!("{tn}: DataDigest mismatch");
447            }
448        }
449        Ok(())
450    }
451}
452
453/// A helper struct for providing a debug representation of a byte slice in
454/// hexadecimal format. A helper struct for providing a debug representation of
455/// a byte slice in hexadecimal format.
456struct HexPreview<'a>(&'a [u8]);
457
458impl<'a> fmt::Debug for HexPreview<'a> {
459    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
460        const MAX: usize = 128;
461        let slice = if self.0.len() > MAX {
462            &self.0[..MAX]
463        } else {
464            self.0
465        };
466        let mut first = true;
467        write!(f, "\"")?;
468        for b in slice {
469            if !first {
470                write!(f, " ")?;
471            }
472            write!(f, "{b:02x}")?;
473            first = false;
474        }
475        if self.0.len() > MAX {
476            write!(f, " ... (+{} bytes)", self.0.len() - MAX)?;
477        }
478        write!(f, "\"")
479    }
480}
481
482impl<T, B> fmt::Debug for PDUWithData<T, B>
483where
484    T: BasicHeaderSegment + SendingData + FromBytes + fmt::Debug + ZeroCopyType,
485    B: Deref<Target = [u8]>,
486{
487    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
488        let mut ds = f.debug_struct("PDUWithData");
489        let header = &self.header_view().expect("failed to get header");
490
491        ds.field("header", header);
492
493        let data = &self.data().expect("invlid pdu");
494
495        ds.field("data_len", &data.len());
496
497        match self.header_digest {
498            Some(hd) => ds.field("header_digest", &format_args!("{hd:#010x}")),
499            None => ds.field("header_digest", &r"None"),
500        };
501
502        match self.data_digest {
503            Some(dd) => ds.field("data_digest", &format_args!("{dd:#010x}")),
504            None => ds.field("data_digest", &r"None"),
505        };
506
507        if header.get_opcode().expect("unable to get opcode").opcode
508            == Opcode::ScsiCommandResp
509            && !data.is_empty()
510        {
511            match SenseData::parse(data) {
512                Ok(sense) => {
513                    ds.field("sense", &sense);
514                },
515                Err(_e) => {
516                    ds.field("data_preview", &HexPreview(data));
517                },
518            }
519        } else if !data.is_empty() {
520            ds.field("data_preview", &HexPreview(data));
521        } else {
522            ds.field("data", &r"[]");
523        }
524
525        ds.finish()
526    }
527}