⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/drivers/iscsi/
initiator.rs

1//! iSCSI Initiator (RFC 7143)
2//!
3//! Implements an iSCSI initiator with login/logout session management,
4//! SCSI command transport over TCP, PDU serialization, and text-mode
5//! parameter negotiation.
6
7#![allow(dead_code)]
8
9#[cfg(feature = "alloc")]
10extern crate alloc;
11
12#[cfg(feature = "alloc")]
13use alloc::{collections::BTreeMap, format, string::String, vec::Vec};
14
15// ---------------------------------------------------------------------------
16// iSCSI Protocol Constants
17// ---------------------------------------------------------------------------
18
19/// iSCSI TCP port (RFC 7143).
20const ISCSI_PORT: u16 = 3260;
21
22/// BHS (Basic Header Segment) length in bytes.
23const BHS_LENGTH: usize = 48;
24
25/// Maximum data segment length (default).
26const DEFAULT_MAX_RECV_DATA_SEGMENT_LENGTH: u32 = 8192;
27
28/// Default max burst length.
29const DEFAULT_MAX_BURST_LENGTH: u32 = 262144;
30
31/// Default first burst length.
32const DEFAULT_FIRST_BURST_LENGTH: u32 = 65536;
33
34// ---------------------------------------------------------------------------
35// iSCSI Opcodes
36// ---------------------------------------------------------------------------
37
38/// iSCSI PDU opcodes (RFC 7143 Section 11.1).
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40#[repr(u8)]
41pub enum IscsiOpcode {
42    NopOut = 0x00,
43    ScsiCommand = 0x01,
44    LoginReq = 0x03,
45    TextReq = 0x04,
46    DataOut = 0x05,
47    Logout = 0x06,
48    NopIn = 0x20,
49    ScsiResponse = 0x21,
50    LoginResp = 0x23,
51    TextResp = 0x24,
52    DataIn = 0x25,
53    LogoutResp = 0x26,
54    Reject = 0x3F,
55}
56
57impl IscsiOpcode {
58    /// Convert from wire value (lower 6 bits).
59    pub fn from_u8(v: u8) -> Option<Self> {
60        match v & 0x3F {
61            0x00 => Some(Self::NopOut),
62            0x01 => Some(Self::ScsiCommand),
63            0x03 => Some(Self::LoginReq),
64            0x04 => Some(Self::TextReq),
65            0x05 => Some(Self::DataOut),
66            0x06 => Some(Self::Logout),
67            0x20 => Some(Self::NopIn),
68            0x21 => Some(Self::ScsiResponse),
69            0x23 => Some(Self::LoginResp),
70            0x24 => Some(Self::TextResp),
71            0x25 => Some(Self::DataIn),
72            0x26 => Some(Self::LogoutResp),
73            0x3F => Some(Self::Reject),
74            _ => None,
75        }
76    }
77
78    /// Whether this is an initiator opcode (bit 5 clear).
79    pub fn is_initiator(&self) -> bool {
80        (*self as u8) & 0x20 == 0
81    }
82}
83
84// ---------------------------------------------------------------------------
85// SCSI Command Definitions
86// ---------------------------------------------------------------------------
87
88/// Common SCSI operation codes.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90#[repr(u8)]
91pub enum ScsiOpcode {
92    TestUnitReady = 0x00,
93    RequestSense = 0x03,
94    Inquiry = 0x12,
95    ReadCapacity10 = 0x25,
96    Read10 = 0x28,
97    Write10 = 0x2A,
98}
99
100/// SCSI Command Descriptor Block (CDB) with metadata.
101#[derive(Debug, Clone)]
102pub struct ScsiCommand {
103    /// Command descriptor block (16 bytes for CDB16 compatibility).
104    pub cdb: [u8; 16],
105    /// Expected data transfer length.
106    pub data_length: u32,
107    /// Logical unit number.
108    pub lun: u64,
109    /// Whether this is a read command.
110    pub is_read: bool,
111    /// Whether this is a write command.
112    pub is_write: bool,
113}
114
115impl ScsiCommand {
116    /// Create TEST UNIT READY command.
117    pub fn test_unit_ready(lun: u64) -> Self {
118        let mut cdb = [0u8; 16];
119        cdb[0] = ScsiOpcode::TestUnitReady as u8;
120        Self {
121            cdb,
122            data_length: 0,
123            lun,
124            is_read: false,
125            is_write: false,
126        }
127    }
128
129    /// Create INQUIRY command.
130    pub fn inquiry(lun: u64) -> Self {
131        let mut cdb = [0u8; 16];
132        cdb[0] = ScsiOpcode::Inquiry as u8;
133        cdb[4] = 96; // Allocation length
134        Self {
135            cdb,
136            data_length: 96,
137            lun,
138            is_read: true,
139            is_write: false,
140        }
141    }
142
143    /// Create READ CAPACITY (10) command.
144    pub fn read_capacity_10(lun: u64) -> Self {
145        let mut cdb = [0u8; 16];
146        cdb[0] = ScsiOpcode::ReadCapacity10 as u8;
147        Self {
148            cdb,
149            data_length: 8,
150            lun,
151            is_read: true,
152            is_write: false,
153        }
154    }
155
156    /// Create READ (10) command.
157    pub fn read_10(lun: u64, lba: u32, block_count: u16, block_size: u32) -> Self {
158        let mut cdb = [0u8; 16];
159        cdb[0] = ScsiOpcode::Read10 as u8;
160        cdb[2..6].copy_from_slice(&lba.to_be_bytes());
161        cdb[7..9].copy_from_slice(&block_count.to_be_bytes());
162        Self {
163            cdb,
164            data_length: block_count as u32 * block_size,
165            lun,
166            is_read: true,
167            is_write: false,
168        }
169    }
170
171    /// Create WRITE (10) command.
172    pub fn write_10(lun: u64, lba: u32, block_count: u16, block_size: u32) -> Self {
173        let mut cdb = [0u8; 16];
174        cdb[0] = ScsiOpcode::Write10 as u8;
175        cdb[2..6].copy_from_slice(&lba.to_be_bytes());
176        cdb[7..9].copy_from_slice(&block_count.to_be_bytes());
177        Self {
178            cdb,
179            data_length: block_count as u32 * block_size,
180            lun,
181            is_read: false,
182            is_write: true,
183        }
184    }
185
186    /// Create REQUEST SENSE command.
187    pub fn request_sense(lun: u64) -> Self {
188        let mut cdb = [0u8; 16];
189        cdb[0] = ScsiOpcode::RequestSense as u8;
190        cdb[4] = 252; // Allocation length
191        Self {
192            cdb,
193            data_length: 252,
194            lun,
195            is_read: true,
196            is_write: false,
197        }
198    }
199}
200
201// ---------------------------------------------------------------------------
202// BHS (Basic Header Segment)
203// ---------------------------------------------------------------------------
204
205/// iSCSI Basic Header Segment (48 bytes, RFC 7143 Section 11.2).
206#[derive(Debug, Clone)]
207pub struct BhsHeader {
208    /// Opcode (lower 6 bits) + flags (upper 2 bits).
209    pub opcode: IscsiOpcode,
210    /// Immediate delivery flag.
211    pub immediate: bool,
212    /// Final PDU flag.
213    pub is_final: bool,
214    /// Opcode-specific flags byte.
215    pub flags: u8,
216    /// Total AHS (Additional Header Segments) length in 4-byte words.
217    pub total_ahs_length: u8,
218    /// Data segment length (24-bit).
219    pub data_segment_length: u32,
220    /// Logical Unit Number.
221    pub lun: u64,
222    /// Initiator Task Tag.
223    pub initiator_task_tag: u32,
224    /// Opcode-specific fields (bytes 20-47, 28 bytes).
225    pub specific: [u8; 28],
226}
227
228impl Default for BhsHeader {
229    fn default() -> Self {
230        Self {
231            opcode: IscsiOpcode::NopOut,
232            immediate: false,
233            is_final: true,
234            flags: 0,
235            total_ahs_length: 0,
236            data_segment_length: 0,
237            lun: 0,
238            initiator_task_tag: 0,
239            specific: [0u8; 28],
240        }
241    }
242}
243
244impl BhsHeader {
245    /// Create a new BHS for a given opcode.
246    pub fn new(opcode: IscsiOpcode) -> Self {
247        Self {
248            opcode,
249            is_final: true,
250            ..Default::default()
251        }
252    }
253
254    /// Serialize BHS to 48 bytes.
255    pub fn serialize(&self) -> [u8; BHS_LENGTH] {
256        let mut buf = [0u8; BHS_LENGTH];
257
258        // Byte 0: immediate(1) + opcode(6)
259        buf[0] = (self.opcode as u8) & 0x3F;
260        if self.immediate {
261            buf[0] |= 0x40;
262        }
263
264        // Byte 1: flags + final
265        buf[1] = self.flags;
266        if self.is_final {
267            buf[1] |= 0x80;
268        }
269
270        // Byte 4: TotalAHSLength
271        buf[4] = self.total_ahs_length;
272
273        // Bytes 5-7: DataSegmentLength (24-bit big-endian)
274        buf[5] = ((self.data_segment_length >> 16) & 0xFF) as u8;
275        buf[6] = ((self.data_segment_length >> 8) & 0xFF) as u8;
276        buf[7] = (self.data_segment_length & 0xFF) as u8;
277
278        // Bytes 8-15: LUN
279        buf[8..16].copy_from_slice(&self.lun.to_be_bytes());
280
281        // Bytes 16-19: Initiator Task Tag
282        buf[16..20].copy_from_slice(&self.initiator_task_tag.to_be_bytes());
283
284        // Bytes 20-47: Opcode-specific
285        buf[20..48].copy_from_slice(&self.specific);
286
287        buf
288    }
289
290    /// Deserialize BHS from 48 bytes.
291    pub fn deserialize(data: &[u8]) -> Option<Self> {
292        if data.len() < BHS_LENGTH {
293            return None;
294        }
295
296        let opcode = IscsiOpcode::from_u8(data[0] & 0x3F)?;
297        let immediate = data[0] & 0x40 != 0;
298        let is_final = data[1] & 0x80 != 0;
299        let flags = data[1] & 0x7F;
300        let total_ahs_length = data[4];
301        let data_segment_length =
302            ((data[5] as u32) << 16) | ((data[6] as u32) << 8) | (data[7] as u32);
303
304        let lun = u64::from_be_bytes([
305            data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15],
306        ]);
307        let initiator_task_tag = u32::from_be_bytes([data[16], data[17], data[18], data[19]]);
308
309        let mut specific = [0u8; 28];
310        specific.copy_from_slice(&data[20..48]);
311
312        Some(Self {
313            opcode,
314            immediate,
315            is_final,
316            flags,
317            total_ahs_length,
318            data_segment_length,
319            lun,
320            initiator_task_tag,
321            specific,
322        })
323    }
324
325    /// Get data segment length including padding to 4-byte boundary.
326    pub fn padded_data_length(&self) -> usize {
327        let len = self.data_segment_length as usize;
328        (len + 3) & !3
329    }
330}
331
332// ---------------------------------------------------------------------------
333// iSCSI Session State
334// ---------------------------------------------------------------------------
335
336/// iSCSI session states (RFC 7143 Section 8).
337#[derive(Debug, Clone, Copy, PartialEq, Eq)]
338pub enum SessionState {
339    /// Not connected.
340    Free,
341    /// Login phase in progress.
342    Login,
343    /// Full Feature Phase (normal operation).
344    FullFeature,
345    /// Logout in progress.
346    Logout,
347}
348
349/// iSCSI session.
350#[cfg(feature = "alloc")]
351#[derive(Debug)]
352pub struct IscsiSession {
353    /// Target name (IQN).
354    pub target_name: String,
355    /// Initiator name (IQN).
356    pub initiator_name: String,
357    /// Initiator Session ID (48-bit).
358    pub isid: u64,
359    /// Target Session Identifying Handle.
360    pub tsih: u16,
361    /// Command Sequence Number.
362    pub cmd_sn: u32,
363    /// Expected Status Sequence Number.
364    pub exp_stat_sn: u32,
365    /// Session state.
366    pub state: SessionState,
367    /// Negotiated MaxRecvDataSegmentLength.
368    pub max_recv_data_segment_length: u32,
369    /// Negotiated MaxBurstLength.
370    pub max_burst_length: u32,
371    /// Negotiated FirstBurstLength.
372    pub first_burst_length: u32,
373}
374
375#[cfg(feature = "alloc")]
376impl IscsiSession {
377    /// Create a new session.
378    pub fn new(initiator_name: &str, target_name: &str) -> Self {
379        Self {
380            target_name: String::from(target_name),
381            initiator_name: String::from(initiator_name),
382            isid: 0x0000_23D0_0000_0001, // Default ISID (EN format)
383            tsih: 0,
384            cmd_sn: 1,
385            exp_stat_sn: 0,
386            state: SessionState::Free,
387            max_recv_data_segment_length: DEFAULT_MAX_RECV_DATA_SEGMENT_LENGTH,
388            max_burst_length: DEFAULT_MAX_BURST_LENGTH,
389            first_burst_length: DEFAULT_FIRST_BURST_LENGTH,
390        }
391    }
392
393    /// Get next command sequence number and increment.
394    pub fn next_cmd_sn(&mut self) -> u32 {
395        let sn = self.cmd_sn;
396        self.cmd_sn = self.cmd_sn.wrapping_add(1);
397        sn
398    }
399}
400
401// ---------------------------------------------------------------------------
402// iSCSI Error
403// ---------------------------------------------------------------------------
404
405/// iSCSI error type.
406#[derive(Debug, Clone, Copy, PartialEq, Eq)]
407pub enum IscsiError {
408    /// Not logged in.
409    NotLoggedIn,
410    /// Login failed.
411    LoginFailed,
412    /// Session not found.
413    SessionNotFound,
414    /// PDU parse error.
415    PduError,
416    /// SCSI command failed with check condition.
417    ScsiError,
418    /// Transport (network) error.
419    TransportError,
420    /// Target rejected the request.
421    TargetError,
422    /// Invalid parameter.
423    InvalidParameter,
424    /// Timeout.
425    Timeout,
426}
427
428// ---------------------------------------------------------------------------
429// SCSI Response
430// ---------------------------------------------------------------------------
431
432/// SCSI command response.
433#[cfg(feature = "alloc")]
434#[derive(Debug, Clone)]
435pub struct ScsiResponse {
436    /// SCSI status code.
437    pub status: u8,
438    /// Response data (for read commands).
439    pub data: Vec<u8>,
440    /// Sense data (if check condition).
441    pub sense: Vec<u8>,
442}
443
444/// Device information from INQUIRY response.
445#[cfg(feature = "alloc")]
446#[derive(Debug, Clone)]
447pub struct InquiryData {
448    /// Peripheral device type (bits 4:0 of byte 0).
449    pub device_type: u8,
450    /// Vendor identification (bytes 8-15).
451    pub vendor: String,
452    /// Product identification (bytes 16-31).
453    pub product: String,
454    /// Product revision (bytes 32-35).
455    pub revision: String,
456}
457
458/// Disk capacity from READ CAPACITY (10) response.
459#[derive(Debug, Clone, Copy)]
460pub struct DiskCapacity {
461    /// Last logical block address.
462    pub last_lba: u32,
463    /// Block size in bytes.
464    pub block_size: u32,
465    /// Total capacity in bytes.
466    pub total_bytes: u64,
467}
468
469// ---------------------------------------------------------------------------
470// iSCSI Initiator
471// ---------------------------------------------------------------------------
472
473/// iSCSI initiator.
474#[cfg(feature = "alloc")]
475pub struct IscsiInitiator {
476    /// Active sessions.
477    sessions: Vec<IscsiSession>,
478    /// Target portal address (IP:port).
479    target_portal: String,
480    /// Next initiator task tag.
481    next_tag: u32,
482}
483
484#[cfg(feature = "alloc")]
485impl IscsiInitiator {
486    /// Create a new iSCSI initiator.
487    pub fn new(portal: &str) -> Self {
488        Self {
489            sessions: Vec::new(),
490            target_portal: String::from(portal),
491            next_tag: 1,
492        }
493    }
494
495    /// Perform login to a target.
496    pub fn login(&mut self, initiator_name: &str, target_name: &str) -> Result<usize, IscsiError> {
497        let mut session = IscsiSession::new(initiator_name, target_name);
498        session.state = SessionState::Login;
499
500        // Build Login Request PDU
501        let mut bhs = BhsHeader::new(IscsiOpcode::LoginReq);
502        bhs.immediate = true;
503        bhs.flags = 0x07; // Transit + CSG=0 (SecurityNegotiation) + NSG=3 (FullFeature)
504        bhs.initiator_task_tag = self.next_tag();
505
506        // ISID in specific bytes 0-5
507        let isid_bytes = session.isid.to_be_bytes();
508        bhs.specific[0..6].copy_from_slice(&isid_bytes[2..8]);
509
510        // CmdSN in specific bytes 8-11
511        let cmd_sn = session.next_cmd_sn();
512        bhs.specific[8..12].copy_from_slice(&cmd_sn.to_be_bytes());
513
514        // Build login parameters
515        let params = self.build_login_params(&session);
516        bhs.data_segment_length = params.len() as u32;
517
518        let _pdu = self.build_pdu(&bhs, &params);
519
520        // In production: send PDU, receive login response, check status.
521        // For security negotiation: may need multiple round-trips.
522        // Stub: mark as logged in.
523        session.state = SessionState::FullFeature;
524        session.tsih = 1;
525
526        let idx = self.sessions.len();
527        self.sessions.push(session);
528        Ok(idx)
529    }
530
531    /// Logout from a session.
532    pub fn logout(&mut self, session_idx: usize) -> Result<(), IscsiError> {
533        let tag = self.next_tag();
534
535        let (cmd_sn, exp_stat_sn) = {
536            let session = self
537                .sessions
538                .get_mut(session_idx)
539                .ok_or(IscsiError::SessionNotFound)?;
540
541            if session.state != SessionState::FullFeature {
542                return Err(IscsiError::NotLoggedIn);
543            }
544
545            let sn = (session.next_cmd_sn(), session.exp_stat_sn);
546            session.state = SessionState::Free;
547            sn
548        };
549
550        let mut bhs = BhsHeader::new(IscsiOpcode::Logout);
551        bhs.immediate = true;
552        bhs.flags = 0x00; // Close session
553        bhs.initiator_task_tag = tag;
554        bhs.specific[0..4].copy_from_slice(&cmd_sn.to_be_bytes());
555        bhs.specific[4..8].copy_from_slice(&exp_stat_sn.to_be_bytes());
556
557        let _pdu = self.build_pdu(&bhs, &[]);
558        Ok(())
559    }
560
561    /// Send a SCSI command and receive response.
562    pub fn scsi_command(
563        &mut self,
564        session_idx: usize,
565        cmd: &ScsiCommand,
566        write_data: Option<&[u8]>,
567    ) -> Result<ScsiResponse, IscsiError> {
568        let tag = self.next_tag();
569
570        // Extract session fields needed for BHS construction, then release borrow
571        let (cmd_sn, exp_stat_sn) = {
572            let session = self
573                .sessions
574                .get_mut(session_idx)
575                .ok_or(IscsiError::SessionNotFound)?;
576
577            if session.state != SessionState::FullFeature {
578                return Err(IscsiError::NotLoggedIn);
579            }
580
581            (session.next_cmd_sn(), session.exp_stat_sn)
582        };
583
584        let mut bhs = BhsHeader::new(IscsiOpcode::ScsiCommand);
585        bhs.is_final = true;
586        bhs.lun = cmd.lun;
587        bhs.initiator_task_tag = tag;
588
589        // Flags: read/write bits
590        let mut flags: u8 = 0;
591        if cmd.is_read {
592            flags |= 0x40;
593        }
594        if cmd.is_write {
595            flags |= 0x20;
596        }
597        // ATTR = Simple (bits 2:0 = 0)
598        bhs.flags = flags;
599
600        // Expected Data Transfer Length in specific[0..4]
601        bhs.specific[0..4].copy_from_slice(&cmd.data_length.to_be_bytes());
602
603        // CmdSN in specific[4..8]
604        bhs.specific[4..8].copy_from_slice(&cmd_sn.to_be_bytes());
605
606        // ExpStatSN in specific[8..12]
607        bhs.specific[8..12].copy_from_slice(&exp_stat_sn.to_be_bytes());
608
609        // CDB in specific[12..28] (first 16 bytes of CDB)
610        bhs.specific[12..28].copy_from_slice(&cmd.cdb);
611
612        // Build PDU with optional write data
613        let data = write_data.unwrap_or(&[]);
614        if !data.is_empty() {
615            bhs.data_segment_length = data.len() as u32;
616        }
617
618        let _pdu = self.build_pdu(&bhs, data);
619
620        // Stub: return empty success response
621        Ok(ScsiResponse {
622            status: 0x00, // GOOD
623            data: Vec::new(),
624            sense: Vec::new(),
625        })
626    }
627
628    /// Send INQUIRY command.
629    pub fn inquiry(&mut self, session_idx: usize, lun: u64) -> Result<InquiryData, IscsiError> {
630        let cmd = ScsiCommand::inquiry(lun);
631        let response = self.scsi_command(session_idx, &cmd, None)?;
632
633        if response.status != 0 {
634            return Err(IscsiError::ScsiError);
635        }
636
637        // Parse INQUIRY data (if we had real data)
638        Ok(InquiryData {
639            device_type: if response.data.is_empty() {
640                0
641            } else {
642                response.data[0] & 0x1F
643            },
644            vendor: Self::extract_string(&response.data, 8, 8),
645            product: Self::extract_string(&response.data, 16, 16),
646            revision: Self::extract_string(&response.data, 32, 4),
647        })
648    }
649
650    /// Send READ CAPACITY (10) command.
651    pub fn read_capacity(
652        &mut self,
653        session_idx: usize,
654        lun: u64,
655    ) -> Result<DiskCapacity, IscsiError> {
656        let cmd = ScsiCommand::read_capacity_10(lun);
657        let response = self.scsi_command(session_idx, &cmd, None)?;
658
659        if response.status != 0 {
660            return Err(IscsiError::ScsiError);
661        }
662
663        // Parse READ CAPACITY (10) response: 4 bytes LBA + 4 bytes block size
664        let (last_lba, block_size) = if response.data.len() >= 8 {
665            let lba = u32::from_be_bytes([
666                response.data[0],
667                response.data[1],
668                response.data[2],
669                response.data[3],
670            ]);
671            let bs = u32::from_be_bytes([
672                response.data[4],
673                response.data[5],
674                response.data[6],
675                response.data[7],
676            ]);
677            (lba, bs)
678        } else {
679            (0, 512)
680        };
681
682        Ok(DiskCapacity {
683            last_lba,
684            block_size,
685            total_bytes: (last_lba as u64 + 1) * block_size as u64,
686        })
687    }
688
689    /// Read blocks from target.
690    pub fn read_blocks(
691        &mut self,
692        session_idx: usize,
693        lun: u64,
694        lba: u32,
695        block_count: u16,
696        block_size: u32,
697    ) -> Result<Vec<u8>, IscsiError> {
698        let cmd = ScsiCommand::read_10(lun, lba, block_count, block_size);
699        let response = self.scsi_command(session_idx, &cmd, None)?;
700
701        if response.status != 0 {
702            return Err(IscsiError::ScsiError);
703        }
704
705        Ok(response.data)
706    }
707
708    /// Write blocks to target.
709    pub fn write_blocks(
710        &mut self,
711        session_idx: usize,
712        lun: u64,
713        lba: u32,
714        block_count: u16,
715        block_size: u32,
716        data: &[u8],
717    ) -> Result<(), IscsiError> {
718        let expected_len = block_count as u32 * block_size;
719        if data.len() != expected_len as usize {
720            return Err(IscsiError::InvalidParameter);
721        }
722
723        let cmd = ScsiCommand::write_10(lun, lba, block_count, block_size);
724        let response = self.scsi_command(session_idx, &cmd, Some(data))?;
725
726        if response.status != 0 {
727            return Err(IscsiError::ScsiError);
728        }
729
730        Ok(())
731    }
732
733    /// Discover targets via SendTargets text request.
734    pub fn discovery(&mut self, session_idx: usize) -> Result<Vec<String>, IscsiError> {
735        let tag = self.next_tag();
736
737        let (cmd_sn, exp_stat_sn) = {
738            let session = self
739                .sessions
740                .get_mut(session_idx)
741                .ok_or(IscsiError::SessionNotFound)?;
742
743            if session.state != SessionState::FullFeature {
744                return Err(IscsiError::NotLoggedIn);
745            }
746
747            (session.next_cmd_sn(), session.exp_stat_sn)
748        };
749
750        let mut bhs = BhsHeader::new(IscsiOpcode::TextReq);
751        bhs.is_final = true;
752        bhs.initiator_task_tag = tag;
753
754        let text = b"SendTargets=All\0";
755        bhs.data_segment_length = text.len() as u32;
756
757        bhs.specific[0..4].copy_from_slice(&cmd_sn.to_be_bytes());
758        bhs.specific[4..8].copy_from_slice(&exp_stat_sn.to_be_bytes());
759
760        let _pdu = self.build_pdu(&bhs, text);
761
762        // Stub: return empty target list
763        Ok(Vec::new())
764    }
765
766    /// Build a complete iSCSI PDU from BHS and data segment.
767    pub fn build_pdu(&self, bhs: &BhsHeader, data: &[u8]) -> Vec<u8> {
768        let bhs_bytes = bhs.serialize();
769        let padded_len = (data.len() + 3) & !3;
770
771        let mut pdu = Vec::with_capacity(BHS_LENGTH + padded_len);
772        pdu.extend_from_slice(&bhs_bytes);
773        pdu.extend_from_slice(data);
774
775        // Pad to 4-byte boundary
776        while pdu.len() < BHS_LENGTH + padded_len {
777            pdu.push(0);
778        }
779
780        pdu
781    }
782
783    /// Parse a PDU from raw bytes.
784    pub fn parse_pdu(&self, data: &[u8]) -> Result<(BhsHeader, Vec<u8>), IscsiError> {
785        let bhs = BhsHeader::deserialize(data).ok_or(IscsiError::PduError)?;
786        let data_len = bhs.data_segment_length as usize;
787
788        let data_start = BHS_LENGTH;
789        let data_end = data_start + data_len;
790
791        if data.len() < data_end {
792            return Err(IscsiError::PduError);
793        }
794
795        let segment = data[data_start..data_end].to_vec();
796        Ok((bhs, segment))
797    }
798
799    /// Build login text parameters.
800    fn build_login_params(&self, session: &IscsiSession) -> Vec<u8> {
801        let mut params = BTreeMap::new();
802        params.insert("InitiatorName", session.initiator_name.as_str());
803        params.insert("TargetName", session.target_name.as_str());
804        params.insert("SessionType", "Normal");
805        params.insert("AuthMethod", "None");
806
807        let mut buf = Vec::with_capacity(256);
808        for (key, value) in &params {
809            let entry = format!("{}={}\0", key, value);
810            buf.extend_from_slice(entry.as_bytes());
811        }
812        buf
813    }
814
815    /// Build operational parameter negotiation text.
816    #[cfg(feature = "alloc")]
817    pub fn build_operational_params(&self) -> Vec<u8> {
818        let params = [
819            ("MaxRecvDataSegmentLength", "65536"),
820            ("MaxBurstLength", "262144"),
821            ("FirstBurstLength", "65536"),
822            ("MaxOutstandingR2T", "1"),
823            ("InitialR2T", "Yes"),
824            ("ImmediateData", "Yes"),
825            ("DataPDUInOrder", "Yes"),
826            ("DataSequenceInOrder", "Yes"),
827            ("DefaultTime2Wait", "2"),
828            ("DefaultTime2Retain", "0"),
829            ("ErrorRecoveryLevel", "0"),
830        ];
831
832        let mut buf = Vec::with_capacity(512);
833        for (key, value) in &params {
834            let entry = format!("{}={}\0", key, value);
835            buf.extend_from_slice(entry.as_bytes());
836        }
837        buf
838    }
839
840    /// Get next initiator task tag.
841    fn next_tag(&mut self) -> u32 {
842        let tag = self.next_tag;
843        self.next_tag = self.next_tag.wrapping_add(1);
844        tag
845    }
846
847    /// Extract an ASCII string from a byte buffer.
848    fn extract_string(data: &[u8], offset: usize, len: usize) -> String {
849        if data.len() < offset + len {
850            return String::new();
851        }
852        let slice = &data[offset..offset + len];
853        // Trim trailing spaces and nulls
854        let trimmed = slice
855            .iter()
856            .rev()
857            .skip_while(|&&b| b == b' ' || b == 0)
858            .count();
859        let end = offset + trimmed;
860        String::from_utf8_lossy(&data[offset..end]).into_owned()
861    }
862
863    /// Get the target portal address.
864    pub fn portal(&self) -> &str {
865        &self.target_portal
866    }
867
868    /// Get the number of active sessions.
869    pub fn session_count(&self) -> usize {
870        self.sessions.len()
871    }
872
873    /// Get a session by index.
874    pub fn session(&self, idx: usize) -> Option<&IscsiSession> {
875        self.sessions.get(idx)
876    }
877}
878
879// ---------------------------------------------------------------------------
880// Tests
881// ---------------------------------------------------------------------------
882
883#[cfg(test)]
884mod tests {
885    use super::*;
886
887    #[test]
888    fn test_iscsi_opcode_from_u8() {
889        assert_eq!(IscsiOpcode::from_u8(0x00), Some(IscsiOpcode::NopOut));
890        assert_eq!(IscsiOpcode::from_u8(0x01), Some(IscsiOpcode::ScsiCommand));
891        assert_eq!(IscsiOpcode::from_u8(0x21), Some(IscsiOpcode::ScsiResponse));
892        assert_eq!(IscsiOpcode::from_u8(0x3F), Some(IscsiOpcode::Reject));
893        assert_eq!(IscsiOpcode::from_u8(0x10), None);
894    }
895
896    #[test]
897    fn test_opcode_is_initiator() {
898        assert!(IscsiOpcode::NopOut.is_initiator());
899        assert!(IscsiOpcode::ScsiCommand.is_initiator());
900        assert!(!IscsiOpcode::NopIn.is_initiator());
901        assert!(!IscsiOpcode::ScsiResponse.is_initiator());
902    }
903
904    #[test]
905    fn test_scsi_test_unit_ready() {
906        let cmd = ScsiCommand::test_unit_ready(0);
907        assert_eq!(cmd.cdb[0], ScsiOpcode::TestUnitReady as u8);
908        assert_eq!(cmd.data_length, 0);
909        assert!(!cmd.is_read);
910        assert!(!cmd.is_write);
911    }
912
913    #[test]
914    fn test_scsi_inquiry() {
915        let cmd = ScsiCommand::inquiry(0);
916        assert_eq!(cmd.cdb[0], ScsiOpcode::Inquiry as u8);
917        assert_eq!(cmd.data_length, 96);
918        assert!(cmd.is_read);
919    }
920
921    #[test]
922    fn test_scsi_read_10() {
923        let cmd = ScsiCommand::read_10(0, 100, 8, 512);
924        assert_eq!(cmd.cdb[0], ScsiOpcode::Read10 as u8);
925        assert_eq!(cmd.data_length, 4096);
926        assert!(cmd.is_read);
927        // Check LBA encoding
928        let lba = u32::from_be_bytes([cmd.cdb[2], cmd.cdb[3], cmd.cdb[4], cmd.cdb[5]]);
929        assert_eq!(lba, 100);
930    }
931
932    #[test]
933    fn test_scsi_write_10() {
934        let cmd = ScsiCommand::write_10(0, 200, 4, 512);
935        assert_eq!(cmd.cdb[0], ScsiOpcode::Write10 as u8);
936        assert_eq!(cmd.data_length, 2048);
937        assert!(cmd.is_write);
938    }
939
940    #[test]
941    fn test_bhs_serialize_deserialize() {
942        let mut bhs = BhsHeader::new(IscsiOpcode::ScsiCommand);
943        bhs.immediate = true;
944        bhs.lun = 42;
945        bhs.initiator_task_tag = 0xDEAD_BEEF;
946        bhs.data_segment_length = 4096;
947
948        let bytes = bhs.serialize();
949        assert_eq!(bytes.len(), BHS_LENGTH);
950
951        let parsed = BhsHeader::deserialize(&bytes).unwrap();
952        assert_eq!(parsed.opcode, IscsiOpcode::ScsiCommand);
953        assert!(parsed.immediate);
954        assert_eq!(parsed.lun, 42);
955        assert_eq!(parsed.initiator_task_tag, 0xDEAD_BEEF);
956        assert_eq!(parsed.data_segment_length, 4096);
957    }
958
959    #[test]
960    fn test_bhs_too_short() {
961        assert!(BhsHeader::deserialize(&[0; 10]).is_none());
962    }
963
964    #[test]
965    fn test_bhs_padded_data_length() {
966        let mut bhs = BhsHeader::new(IscsiOpcode::NopOut);
967        bhs.data_segment_length = 5;
968        assert_eq!(bhs.padded_data_length(), 8);
969
970        bhs.data_segment_length = 8;
971        assert_eq!(bhs.padded_data_length(), 8);
972
973        bhs.data_segment_length = 0;
974        assert_eq!(bhs.padded_data_length(), 0);
975    }
976
977    #[test]
978    fn test_session_new() {
979        let session = IscsiSession::new(
980            "iqn.2026-03.os.veridian:init",
981            "iqn.2026-03.com.target:disk1",
982        );
983        assert_eq!(session.state, SessionState::Free);
984        assert_eq!(session.tsih, 0);
985        assert_eq!(session.cmd_sn, 1);
986    }
987
988    #[test]
989    fn test_session_cmd_sn_increment() {
990        let mut session = IscsiSession::new(
991            "iqn.2026-03.os.veridian:init",
992            "iqn.2026-03.com.target:disk1",
993        );
994        assert_eq!(session.next_cmd_sn(), 1);
995        assert_eq!(session.next_cmd_sn(), 2);
996        assert_eq!(session.next_cmd_sn(), 3);
997    }
998
999    #[test]
1000    fn test_initiator_new() {
1001        let init = IscsiInitiator::new("192.168.1.100:3260");
1002        assert_eq!(init.portal(), "192.168.1.100:3260");
1003        assert_eq!(init.session_count(), 0);
1004    }
1005
1006    #[test]
1007    fn test_initiator_build_pdu() {
1008        let init = IscsiInitiator::new("10.0.0.1:3260");
1009        let bhs = BhsHeader::new(IscsiOpcode::NopOut);
1010        let pdu = init.build_pdu(&bhs, &[1, 2, 3]);
1011        // BHS(48) + data(3) + pad(1) = 52
1012        assert_eq!(pdu.len(), 52);
1013    }
1014}