Skip to main content

iscsi_client_rs/state_machine/
write_states.rs

1//! This module defines the state machine for the iSCSI SCSI Write command.
2//! It includes the states, context, and transitions for handling the write
3//! operation.
4
5// SPDX-License-Identifier: AGPL-3.0-or-later
6// Copyright (C) 2012-2025 Andrei Maltsev
7
8use std::{
9    marker::PhantomData,
10    pin::Pin,
11    sync::{
12        Arc,
13        atomic::{AtomicU32, Ordering},
14    },
15};
16
17use anyhow::{Context, Result, anyhow, bail};
18use tokio_util::sync::CancellationToken;
19use tracing::debug;
20
21use crate::{
22    cfg::enums::YesNo,
23    client::client::ClientConnection,
24    models::{
25        command::{
26            common::{ResponseCode, ScsiStatus, TaskAttribute},
27            request::{ScsiCommandRequest, ScsiCommandRequestBuilder},
28            response::ScsiCommandResponse,
29        },
30        common::{BasicHeaderSegment, Builder, HEADER_LEN, SendingData},
31        data::{
32            request::{ScsiDataOut, ScsiDataOutBuilder},
33            sense_data::SenseData,
34        },
35        data_fromat::{PduRequest, PduResponse},
36        ready_2_transfer::response::ReadyToTransfer,
37    },
38    state_machine::common::{StateMachine, StateMachineCtx, Transition},
39};
40
41/// This structure represents the context for a SCSI Write operation.
42#[derive(Debug)]
43pub struct WriteCtx<'a> {
44    _lt: PhantomData<&'a ()>,
45
46    /// The client connection.
47    pub conn: Arc<ClientConnection>,
48    /// The Logical Unit Number.
49    pub lun: u64,
50    /// The Initiator Task Tag.
51    pub itt: u32,
52    /// The Command Sequence Number.
53    pub cmd_sn: Arc<AtomicU32>,
54    /// The Expected Status Sequence Number.
55    pub exp_stat_sn: Arc<AtomicU32>,
56
57    /// The SCSI Command Descriptor Block.
58    pub cdb: [u8; 16],
59    /// The data to be written.
60    pub payload: Vec<u8>,
61    /// A buffer for the BHS.
62    pub buf: [u8; HEADER_LEN],
63
64    /// The number of bytes that have been sent.
65    pub sent_bytes: usize,
66    /// The total number of bytes to be sent.
67    pub total_bytes: usize,
68
69    /// The last received command response.
70    pub last_response: Option<PduResponse<ScsiCommandResponse>>,
71    state: Option<WriteStates>,
72}
73
74#[allow(clippy::too_many_arguments)]
75impl<'a> WriteCtx<'a> {
76    /// Creates a new `WriteCtx` for a SCSI Write operation.
77    pub fn new(
78        conn: Arc<ClientConnection>,
79        lun: u64,
80        itt: Arc<AtomicU32>,
81        cmd_sn: Arc<AtomicU32>,
82        exp_stat_sn: Arc<AtomicU32>,
83        cdb: [u8; 16],
84        payload: impl Into<Vec<u8>>,
85    ) -> Self {
86        Self {
87            conn,
88            lun,
89            itt: itt.fetch_add(1, Ordering::SeqCst),
90            cmd_sn,
91            exp_stat_sn,
92            cdb,
93            payload: payload.into(),
94            buf: [0u8; HEADER_LEN],
95            sent_bytes: 0,
96            total_bytes: 0,
97            last_response: None,
98            state: Some(WriteStates::Start(Start)),
99            _lt: PhantomData,
100        }
101    }
102
103    /// Sends the SCSI Write command.
104    async fn send_write_command(&mut self) -> Result<()> {
105        let cmd_sn = self.cmd_sn.fetch_add(1, Ordering::SeqCst);
106        let esn = self.exp_stat_sn.load(Ordering::SeqCst);
107
108        self.total_bytes = self.payload.len();
109
110        let header = ScsiCommandRequestBuilder::new()
111            .lun(self.lun)
112            .initiator_task_tag(self.itt)
113            .cmd_sn(cmd_sn)
114            .exp_stat_sn(esn)
115            .expected_data_transfer_length(self.total_bytes as u32)
116            .scsi_descriptor_block(&self.cdb)
117            .write()
118            .task_attribute(TaskAttribute::Simple);
119
120        header.header.to_bhs_bytes(&mut self.buf)?;
121        let pdu = PduRequest::<ScsiCommandRequest>::new_request(self.buf, &self.conn.cfg);
122        self.conn.send_request(self.itt, pdu).await?;
123
124        Ok(())
125    }
126
127    /// Receives a Ready To Transfer (R2T) PDU.
128    async fn recv_r2t(&self, itt: u32) -> Result<PduResponse<ReadyToTransfer>> {
129        let r2t: PduResponse<ReadyToTransfer> = self.conn.read_response(itt).await?;
130        let header = r2t.header_view()?;
131        self.exp_stat_sn
132            .store(header.stat_sn.get().wrapping_add(1), Ordering::SeqCst);
133        Ok(r2t)
134    }
135
136    /// Sends a window of data to the target.
137    async fn send_data(
138        &mut self,
139        itt: u32,
140        ttt: u32,
141        offset: usize,
142        len: usize,
143    ) -> Result<usize> {
144        let mut next_data_sn = 0;
145        if len == 0 {
146            bail!("Refuse to send Data-Out with zero length");
147        }
148        let end = offset
149            .checked_add(len)
150            .ok_or_else(|| anyhow!("offset+len overflow"))?;
151        if end > self.payload.len() {
152            bail!(
153                "Data window [{offset}..{end}) exceeds payload {}",
154                self.payload.len()
155            );
156        }
157
158        let mrdsl = self.peer_mrdsl();
159        if mrdsl == 0 {
160            bail!("MRDSL is zero");
161        }
162        let to_send_total = len;
163
164        let mut sent = 0usize;
165        while sent < to_send_total {
166            let take = (to_send_total - sent).min(mrdsl);
167            let off = offset + sent;
168            let last_chunk_in_window = sent + take == to_send_total;
169
170            let header = ScsiDataOutBuilder::new()
171                .lun(self.lun)
172                .initiator_task_tag(itt)
173                .target_transfer_tag(ttt)
174                .exp_stat_sn(self.exp_stat_sn.load(Ordering::SeqCst))
175                .buffer_offset(off as u32)
176                .data_sn(next_data_sn);
177
178            header.header.to_bhs_bytes(self.buf.as_mut_slice())?;
179
180            let mut pdu =
181                PduRequest::<ScsiDataOut>::new_request(self.buf, &self.conn.cfg);
182
183            let header = pdu.header_view_mut()?;
184
185            if last_chunk_in_window {
186                header.set_final_bit();
187            } else {
188                header.set_continue_bit();
189            }
190
191            pdu.append_data(&self.payload[off..off + take]);
192
193            self.conn.send_request(itt, pdu).await?;
194
195            next_data_sn = next_data_sn.wrapping_add(1);
196            sent += take;
197        }
198
199        Ok(sent)
200    }
201
202    /// Waits for the final SCSI response and validates it.
203    async fn wait_scsi_response(&mut self, itt: u32) -> Result<()> {
204        let rsp: PduResponse<ScsiCommandResponse> = self.conn.read_response(itt).await?;
205        let header = rsp.header_view()?;
206        self.exp_stat_sn
207            .store(header.stat_sn.get().wrapping_add(1), Ordering::SeqCst);
208
209        if header.response.decode()? != ResponseCode::CommandCompleted {
210            bail!("WRITE failed: response={:?}", header.response);
211        }
212        if header.status.decode()? != ScsiStatus::Good {
213            let sense = SenseData::parse(rsp.data()?)?;
214            bail!("WRITE failed: {:?}", sense);
215        }
216
217        self.last_response = Some(rsp);
218
219        Ok(())
220    }
221
222    /// Returns whether the peer expects an initial R2T.
223    #[inline]
224    fn peer_initial_r2t(&self) -> bool {
225        self.conn.cfg.login.write_flow.initial_r2t == YesNo::Yes
226    }
227
228    /// Returns whether the peer accepts immediate data.
229    #[inline]
230    fn peer_immediate_data(&self) -> bool {
231        self.conn.cfg.login.write_flow.immediate_data == YesNo::Yes
232    }
233
234    /// Returns the peer's first burst length.
235    #[inline]
236    fn peer_first_burst(&self) -> usize {
237        self.conn.cfg.login.flow.first_burst_length as usize
238    }
239
240    /// Returns the peer's maximum burst length.
241    #[inline]
242    fn peer_max_burst(&self) -> usize {
243        self.conn.cfg.login.flow.max_burst_length as usize
244    }
245
246    /// Returns the peer's maximum receive data segment length.
247    #[inline]
248    fn peer_mrdsl(&self) -> usize {
249        self.conn.cfg.login.flow.max_recv_data_segment_length as usize
250    }
251
252    /// Sends the SCSI Write command with immediate data.
253    async fn send_write_cmd_with_immediate(&mut self, imm_len: usize) -> Result<()> {
254        let cmd_sn = self.cmd_sn.fetch_add(1, Ordering::SeqCst);
255        let esn = self.exp_stat_sn.load(Ordering::SeqCst);
256        self.total_bytes = self.payload.len();
257
258        let header = ScsiCommandRequestBuilder::new()
259            .lun(self.lun)
260            .initiator_task_tag(self.itt)
261            .cmd_sn(cmd_sn)
262            .exp_stat_sn(esn)
263            .expected_data_transfer_length(self.total_bytes as u32)
264            .scsi_descriptor_block(&self.cdb)
265            .write()
266            .task_attribute(TaskAttribute::Simple);
267
268        header.header.to_bhs_bytes(&mut self.buf)?;
269        let mut pdu =
270            PduRequest::<ScsiCommandRequest>::new_request(self.buf, &self.conn.cfg);
271
272        if imm_len > 0 {
273            pdu.append_data(&self.payload[0..imm_len]);
274        }
275
276        self.conn.send_request(self.itt, pdu).await?;
277        self.sent_bytes = imm_len;
278        Ok(())
279    }
280
281    /// Sends an unsolicited window of data.
282    async fn send_unsolicited_window(
283        &mut self,
284        offset: usize,
285        len: usize,
286    ) -> Result<usize> {
287        let mrdsl = self.peer_mrdsl();
288        if len == 0 {
289            return Ok(0);
290        }
291        if offset + len > self.payload.len() {
292            bail!(
293                "unsolicited window [{offset}..{}) exceeds payload {}",
294                offset + len,
295                self.payload.len()
296            );
297        }
298
299        let mut next_data_sn = 0u32;
300        let mut sent = 0usize;
301        while sent < len {
302            let take = (len - sent).min(mrdsl);
303            let off = offset + sent;
304            let last = sent + take == len;
305
306            let header = ScsiDataOutBuilder::new()
307                .lun(self.lun)
308                .initiator_task_tag(self.itt)
309                .target_transfer_tag(u32::MAX)
310                .exp_stat_sn(self.exp_stat_sn.load(Ordering::SeqCst))
311                .buffer_offset(off as u32)
312                .data_sn(next_data_sn);
313
314            header.header.to_bhs_bytes(self.buf.as_mut_slice())?;
315
316            let mut pdu =
317                PduRequest::<ScsiDataOut>::new_request(self.buf, &self.conn.cfg);
318            {
319                let h = pdu.header_view_mut()?;
320                h.set_data_length_bytes(take as u32);
321                if last {
322                    h.set_final_bit();
323                } else {
324                    h.set_continue_bit();
325                }
326            }
327            pdu.append_data(&self.payload[off..off + take]);
328            self.conn.send_request(self.itt, pdu).await?;
329
330            next_data_sn = next_data_sn.wrapping_add(1);
331            sent += take;
332        }
333        Ok(sent)
334    }
335}
336
337/// Represents the initial state of a write operation.
338#[derive(Debug)]
339pub struct Start;
340
341/// Represents the state of waiting for a Ready To Transfer (R2T) PDU.
342#[derive(Debug)]
343pub struct WaitR2T;
344
345/// Represents the final state of a write operation.
346#[derive(Debug)]
347pub struct Finish;
348
349/// Defines the possible states for a SCSI Write operation state machine.
350#[derive(Debug)]
351pub enum WriteStates {
352    /// The initial state.
353    Start(Start),
354    /// Waiting for an R2T PDU.
355    WaitR2T(WaitR2T),
356    /// The final state.
357    Finish(Finish),
358}
359
360pub type WriteStep = Transition<WriteStates, Result<()>>;
361
362/// IssueCmd
363///
364/// 1) Send SCSI Command (WRITE) with *no* data in the command PDU.
365/// 2) If payload is empty → go straight to waiting for SCSI Response. Otherwise
366///    → wait for R2T.
367impl<'ctx> StateMachine<WriteCtx<'ctx>, WriteStep> for Start {
368    type StepResult<'a>
369        = Pin<Box<dyn Future<Output = WriteStep> + Send + 'a>>
370    where
371        Self: 'a,
372        WriteCtx<'ctx>: 'a;
373
374    fn step<'a>(&'a self, ctx: &'a mut WriteCtx<'ctx>) -> Self::StepResult<'a> {
375        Box::pin(async move {
376            ctx.total_bytes = ctx.payload.len();
377
378            let use_immediate = !ctx.peer_initial_r2t() && ctx.peer_immediate_data();
379            if use_immediate && ctx.total_bytes > 0 {
380                let fbl = ctx
381                    .peer_first_burst()
382                    .min(ctx.peer_max_burst())
383                    .min(ctx.total_bytes);
384                let imm_len = fbl.min(ctx.peer_mrdsl());
385                if let Err(e) = ctx.send_write_cmd_with_immediate(imm_len).await {
386                    return Transition::Done(Err(e));
387                }
388
389                if fbl > imm_len {
390                    match ctx.send_unsolicited_window(imm_len, fbl - imm_len).await {
391                        Ok(s) => {
392                            ctx.sent_bytes += s;
393                        },
394                        Err(e) => return Transition::Done(Err(e)),
395                    }
396                }
397
398                if ctx.sent_bytes >= ctx.total_bytes {
399                    Transition::Next(WriteStates::Finish(Finish), Ok(()))
400                } else {
401                    Transition::Next(WriteStates::WaitR2T(WaitR2T), Ok(()))
402                }
403            } else {
404                if let Err(e) = ctx.send_write_command().await {
405                    return Transition::Done(Err(e));
406                }
407                if ctx.total_bytes == 0 {
408                    Transition::Next(WriteStates::Finish(Finish), Ok(()))
409                } else {
410                    Transition::Next(WriteStates::WaitR2T(WaitR2T), Ok(()))
411                }
412            }
413        })
414    }
415}
416
417/// WaitR2T
418///
419/// Await an R2T. Compute the data window (offset,len) safely,
420/// then move to SendWindow. We assume sequential windows.
421impl<'ctx> StateMachine<WriteCtx<'ctx>, WriteStep> for WaitR2T {
422    type StepResult<'a>
423        = Pin<Box<dyn Future<Output = WriteStep> + Send + 'a>>
424    where
425        Self: 'a,
426        WriteCtx<'ctx>: 'a;
427
428    fn step<'a>(&'a self, ctx: &'a mut WriteCtx<'ctx>) -> Self::StepResult<'a> {
429        Box::pin(async move {
430            let itt = ctx.itt;
431            let r2t = match ctx.recv_r2t(itt).await {
432                Ok(v) => v,
433                Err(e) => return Transition::Done(Err(e)),
434            };
435            let h = match r2t.header_view() {
436                Ok(h) => h,
437                Err(e) => {
438                    return Transition::Done(Err(anyhow!(
439                        "failed read ReadyToTransfer: {e}"
440                    )));
441                },
442            };
443
444            let ttt = h.target_transfer_tag.get();
445            let offset = h.buffer_offset.get() as usize;
446            let want = h.desired_data_transfer_length.get() as usize;
447
448            if offset >= ctx.payload.len() {
449                return Transition::Done(Err(anyhow!(
450                    "R2T buffer_offset {} beyond payload {}",
451                    offset,
452                    ctx.payload.len()
453                )));
454            }
455            let remaining = ctx.payload.len() - offset;
456            let len = want.min(remaining);
457            if len == 0 {
458                return Transition::Done(Err(anyhow!(
459                    "R2T window has zero DesiredDataTransferLength (offset={offset}, \
460                     want={want})"
461                )));
462            }
463
464            let sent = match ctx.send_data(itt, ttt, offset, len).await {
465                Ok(x) => x,
466                Err(e) => return Transition::Done(Err(e)),
467            };
468            ctx.sent_bytes = ctx.sent_bytes.saturating_add(sent);
469
470            if ctx.sent_bytes >= ctx.total_bytes {
471                Transition::Next(WriteStates::Finish(Finish), Ok(()))
472            } else {
473                Transition::Stay(Ok(()))
474            }
475        })
476    }
477}
478
479/// WaitResp
480///
481/// Final step: wait for SCSI Command Response and validate GOOD status.
482impl<'ctx> StateMachine<WriteCtx<'ctx>, WriteStep> for Finish {
483    type StepResult<'a>
484        = Pin<Box<dyn Future<Output = WriteStep> + Send + 'a>>
485    where
486        Self: 'a,
487        WriteCtx<'ctx>: 'a;
488
489    fn step<'a>(&'a self, ctx: &'a mut WriteCtx<'ctx>) -> Self::StepResult<'a> {
490        Box::pin(async move {
491            let itt = ctx.itt;
492            match ctx.wait_scsi_response(itt).await {
493                Ok(()) => Transition::Done(Ok(())),
494                Err(e) => Transition::Done(Err(e)),
495            }
496        })
497    }
498}
499
500/// Represents the outcome of a completed SCSI Write operation.
501#[derive(Debug)]
502pub struct WriteOutcome {
503    /// The final SCSI Command Response.
504    pub last_response: PduResponse<ScsiCommandResponse>,
505    /// The number of bytes that were sent.
506    pub sent_bytes: usize,
507    /// The total number of bytes that were intended to be sent.
508    pub total_bytes: usize,
509}
510
511impl<'ctx> StateMachineCtx<WriteCtx<'ctx>, WriteOutcome> for WriteCtx<'ctx> {
512    async fn execute(&mut self, _cancel: &CancellationToken) -> Result<WriteOutcome> {
513        debug!("Loop WRITE");
514
515        loop {
516            let state = self.state.take().context("state must be set WriteCtx")?;
517            let tr = match &state {
518                WriteStates::Start(s) => s.step(self).await,
519                WriteStates::WaitR2T(s) => s.step(self).await,
520                WriteStates::Finish(s) => s.step(self).await,
521            };
522
523            match tr {
524                Transition::Next(next, r) => {
525                    r?;
526                    self.state = Some(next);
527                },
528                Transition::Stay(Ok(_)) => {
529                    self.state = Some(match state {
530                        WriteStates::Start(_) => WriteStates::Start(Start),
531                        WriteStates::WaitR2T(_) => WriteStates::WaitR2T(WaitR2T),
532                        WriteStates::Finish(_) => WriteStates::Finish(Finish),
533                    });
534                },
535                Transition::Stay(Err(e)) => return Err(e),
536                Transition::Done(r) => {
537                    r?;
538                    return Ok(WriteOutcome {
539                        last_response: self
540                            .last_response
541                            .take()
542                            .ok_or_else(|| anyhow!("no last response in ctx"))?,
543                        sent_bytes: self.sent_bytes,
544                        total_bytes: self.total_bytes,
545                    });
546                },
547            }
548        }
549    }
550}