1#![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
15const ISCSI_PORT: u16 = 3260;
21
22const BHS_LENGTH: usize = 48;
24
25const DEFAULT_MAX_RECV_DATA_SEGMENT_LENGTH: u32 = 8192;
27
28const DEFAULT_MAX_BURST_LENGTH: u32 = 262144;
30
31const DEFAULT_FIRST_BURST_LENGTH: u32 = 65536;
33
34#[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 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 pub fn is_initiator(&self) -> bool {
80 (*self as u8) & 0x20 == 0
81 }
82}
83
84#[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#[derive(Debug, Clone)]
102pub struct ScsiCommand {
103 pub cdb: [u8; 16],
105 pub data_length: u32,
107 pub lun: u64,
109 pub is_read: bool,
111 pub is_write: bool,
113}
114
115impl ScsiCommand {
116 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 pub fn inquiry(lun: u64) -> Self {
131 let mut cdb = [0u8; 16];
132 cdb[0] = ScsiOpcode::Inquiry as u8;
133 cdb[4] = 96; Self {
135 cdb,
136 data_length: 96,
137 lun,
138 is_read: true,
139 is_write: false,
140 }
141 }
142
143 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 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 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 pub fn request_sense(lun: u64) -> Self {
188 let mut cdb = [0u8; 16];
189 cdb[0] = ScsiOpcode::RequestSense as u8;
190 cdb[4] = 252; Self {
192 cdb,
193 data_length: 252,
194 lun,
195 is_read: true,
196 is_write: false,
197 }
198 }
199}
200
201#[derive(Debug, Clone)]
207pub struct BhsHeader {
208 pub opcode: IscsiOpcode,
210 pub immediate: bool,
212 pub is_final: bool,
214 pub flags: u8,
216 pub total_ahs_length: u8,
218 pub data_segment_length: u32,
220 pub lun: u64,
222 pub initiator_task_tag: u32,
224 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 pub fn new(opcode: IscsiOpcode) -> Self {
247 Self {
248 opcode,
249 is_final: true,
250 ..Default::default()
251 }
252 }
253
254 pub fn serialize(&self) -> [u8; BHS_LENGTH] {
256 let mut buf = [0u8; BHS_LENGTH];
257
258 buf[0] = (self.opcode as u8) & 0x3F;
260 if self.immediate {
261 buf[0] |= 0x40;
262 }
263
264 buf[1] = self.flags;
266 if self.is_final {
267 buf[1] |= 0x80;
268 }
269
270 buf[4] = self.total_ahs_length;
272
273 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 buf[8..16].copy_from_slice(&self.lun.to_be_bytes());
280
281 buf[16..20].copy_from_slice(&self.initiator_task_tag.to_be_bytes());
283
284 buf[20..48].copy_from_slice(&self.specific);
286
287 buf
288 }
289
290 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 pub fn padded_data_length(&self) -> usize {
327 let len = self.data_segment_length as usize;
328 (len + 3) & !3
329 }
330}
331
332#[derive(Debug, Clone, Copy, PartialEq, Eq)]
338pub enum SessionState {
339 Free,
341 Login,
343 FullFeature,
345 Logout,
347}
348
349#[cfg(feature = "alloc")]
351#[derive(Debug)]
352pub struct IscsiSession {
353 pub target_name: String,
355 pub initiator_name: String,
357 pub isid: u64,
359 pub tsih: u16,
361 pub cmd_sn: u32,
363 pub exp_stat_sn: u32,
365 pub state: SessionState,
367 pub max_recv_data_segment_length: u32,
369 pub max_burst_length: u32,
371 pub first_burst_length: u32,
373}
374
375#[cfg(feature = "alloc")]
376impl IscsiSession {
377 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, 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
407pub enum IscsiError {
408 NotLoggedIn,
410 LoginFailed,
412 SessionNotFound,
414 PduError,
416 ScsiError,
418 TransportError,
420 TargetError,
422 InvalidParameter,
424 Timeout,
426}
427
428#[cfg(feature = "alloc")]
434#[derive(Debug, Clone)]
435pub struct ScsiResponse {
436 pub status: u8,
438 pub data: Vec<u8>,
440 pub sense: Vec<u8>,
442}
443
444#[cfg(feature = "alloc")]
446#[derive(Debug, Clone)]
447pub struct InquiryData {
448 pub device_type: u8,
450 pub vendor: String,
452 pub product: String,
454 pub revision: String,
456}
457
458#[derive(Debug, Clone, Copy)]
460pub struct DiskCapacity {
461 pub last_lba: u32,
463 pub block_size: u32,
465 pub total_bytes: u64,
467}
468
469#[cfg(feature = "alloc")]
475pub struct IscsiInitiator {
476 sessions: Vec<IscsiSession>,
478 target_portal: String,
480 next_tag: u32,
482}
483
484#[cfg(feature = "alloc")]
485impl IscsiInitiator {
486 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 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 let mut bhs = BhsHeader::new(IscsiOpcode::LoginReq);
502 bhs.immediate = true;
503 bhs.flags = 0x07; bhs.initiator_task_tag = self.next_tag();
505
506 let isid_bytes = session.isid.to_be_bytes();
508 bhs.specific[0..6].copy_from_slice(&isid_bytes[2..8]);
509
510 let cmd_sn = session.next_cmd_sn();
512 bhs.specific[8..12].copy_from_slice(&cmd_sn.to_be_bytes());
513
514 let params = self.build_login_params(&session);
516 bhs.data_segment_length = params.len() as u32;
517
518 let _pdu = self.build_pdu(&bhs, ¶ms);
519
520 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 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; 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 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 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 let mut flags: u8 = 0;
591 if cmd.is_read {
592 flags |= 0x40;
593 }
594 if cmd.is_write {
595 flags |= 0x20;
596 }
597 bhs.flags = flags;
599
600 bhs.specific[0..4].copy_from_slice(&cmd.data_length.to_be_bytes());
602
603 bhs.specific[4..8].copy_from_slice(&cmd_sn.to_be_bytes());
605
606 bhs.specific[8..12].copy_from_slice(&exp_stat_sn.to_be_bytes());
608
609 bhs.specific[12..28].copy_from_slice(&cmd.cdb);
611
612 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 Ok(ScsiResponse {
622 status: 0x00, data: Vec::new(),
624 sense: Vec::new(),
625 })
626 }
627
628 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 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 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 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 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 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 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 Ok(Vec::new())
764 }
765
766 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 while pdu.len() < BHS_LENGTH + padded_len {
777 pdu.push(0);
778 }
779
780 pdu
781 }
782
783 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 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 ¶ms {
809 let entry = format!("{}={}\0", key, value);
810 buf.extend_from_slice(entry.as_bytes());
811 }
812 buf
813 }
814
815 #[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 ¶ms {
834 let entry = format!("{}={}\0", key, value);
835 buf.extend_from_slice(entry.as_bytes());
836 }
837 buf
838 }
839
840 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 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 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 pub fn portal(&self) -> &str {
865 &self.target_portal
866 }
867
868 pub fn session_count(&self) -> usize {
870 self.sessions.len()
871 }
872
873 pub fn session(&self, idx: usize) -> Option<&IscsiSession> {
875 self.sessions.get(idx)
876 }
877}
878
879#[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 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 assert_eq!(pdu.len(), 52);
1013 }
1014}