Skip to main content

embassy_usb_host/class/
msc.rs

1//! USB Mass Storage Class host driver (Bulk-Only Transport, SCSI transparent).
2//!
3//! Implements the MSC BBB transport (USB MSC BBB r1.0): every SCSI
4//! command runs as a CBW → optional data phase → CSW cycle on a pair
5//! of bulk endpoints, with stall recovery via `CLEAR_FEATURE` and
6//! phase-error recovery via the class-specific Bulk-Only Mass Storage
7//! Reset. Only subclass `0x06` (SCSI transparent) / protocol `0x50`
8//! (BBB) interfaces are recognized.
9//!
10//! An [`MscDevice`] owns the control and bulk pipes for one MSC
11//! interface. Per-LUN handles are opened with [`MscDevice::lun`];
12//! all LUNs share the same transport and their commands serialize
13//! through the device's internal async mutex.
14//!
15//! [`MscDevice::new`] guards the transport with a [`NoopRawMutex`];
16//! use [`MscDevice::new_with_raw_mutex`] with a `Sync` raw mutex
17//! (e.g. `CriticalSectionRawMutex`) when LUNs are driven from
18//! multiple tasks.
19//!
20//! # Example
21//!
22//! ```rust,ignore
23//! use embassy_usb_host::class::msc::MscDevice;
24//!
25//! let device = MscDevice::new(&bus, &enum_info, &config_buf[..config_len]).await?;
26//! let mut lun = device.lun(0)?;
27//!
28//! let mut inq = [0u8; 36];
29//! let info = lun.inquiry(&mut inq).await?;
30//! let cap = lun.capacity().await?;
31//!
32//! let mut block = [0u8; 512];
33//! lun.read_blocks(0, &mut block).await?;
34//! ```
35
36use core::marker::PhantomData;
37
38use embassy_sync::blocking_mutex::raw::{NoopRawMutex, RawMutex};
39use embassy_sync::mutex::Mutex;
40use embassy_usb_driver::host::{PipeError, SplitInfo, UsbHostAllocator, UsbPipe, pipe};
41use embassy_usb_driver::{Direction as UsbDirection, EndpointAddress, EndpointInfo, EndpointType};
42
43use crate::control::{ControlType, Recipient, RequestType, SetupPacket};
44use crate::descriptor::ConfigurationDescriptor;
45use crate::handler::EnumerationInfo;
46
47// MSC BBB r1.0 §4.
48const CLASS_MSC: u8 = 0x08;
49const SUBCLASS_SCSI: u8 = 0x06;
50const PROTOCOL_BBB: u8 = 0x50;
51
52// Class-specific requests (MSC BBB r1.0 §3).
53const REQ_GET_MAX_LUN: u8 = 0xFE;
54const REQ_BULK_ONLY_RESET: u8 = 0xFF;
55
56// Standard endpoint requests for stall recovery (USB 2.0 §9.4).
57const REQ_CLEAR_FEATURE: u8 = 0x01;
58const FEATURE_ENDPOINT_HALT: u16 = 0x0000;
59
60// CBW / CSW (MSC BBB r1.0 §5).
61const CBW_SIGNATURE: u32 = 0x43425355; // "USBC"
62const CSW_SIGNATURE: u32 = 0x53425355; // "USBS"
63const CBW_LEN: usize = 31;
64const CSW_LEN: usize = 13;
65const CBW_FLAG_IN: u8 = 0x80;
66
67// CSW status values.
68const CSW_PASSED: u8 = 0x00;
69const CSW_FAILED: u8 = 0x01;
70const CSW_PHASE_ERROR: u8 = 0x02;
71
72// SCSI opcodes (SPC-3 / SBC-3).
73const SCSI_TEST_UNIT_READY: u8 = 0x00;
74const SCSI_REQUEST_SENSE: u8 = 0x03;
75const SCSI_INQUIRY: u8 = 0x12;
76const SCSI_PREVENT_ALLOW_REMOVAL: u8 = 0x1E;
77const SCSI_READ_CAPACITY_10: u8 = 0x25;
78const SCSI_READ_10: u8 = 0x28;
79const SCSI_WRITE_10: u8 = 0x2A;
80const SCSI_SYNCHRONIZE_CACHE_10: u8 = 0x35;
81const SCSI_READ_16: u8 = 0x88;
82const SCSI_WRITE_16: u8 = 0x8A;
83const SCSI_SERVICE_ACTION_IN_16: u8 = 0x9E;
84const SCSI_SA_READ_CAPACITY_16: u8 = 0x10;
85
86/// MSC host driver error.
87#[derive(Debug)]
88#[cfg_attr(feature = "defmt", derive(defmt::Format))]
89pub enum MscError {
90    /// Transfer error.
91    Transfer(PipeError),
92    /// No SCSI/BBB interface in the configuration descriptor.
93    NoInterface,
94    /// Failed to allocate a pipe.
95    NoPipe,
96    /// Device response had unexpected length or out-of-range field.
97    InvalidResponse,
98    /// CBW/CSW signature or tag mismatch.
99    Protocol,
100    /// Device reported CSW status = 2 (phase error). The transport has
101    /// been reset; retry the command.
102    PhaseError,
103    /// SCSI command failed; sense data was fetched via `REQUEST SENSE`.
104    Scsi(SenseData),
105    /// Buffer length is not a multiple of the LUN's block size.
106    Unaligned,
107    /// LBA or block count is out of range for the LUN's capacity.
108    OutOfRange,
109    /// LUN index >= `num_luns()`.
110    NoSuchLun,
111    /// CDB length must be in `1..=16`.
112    InvalidCdb,
113    /// The LUN's reported block size does not match the size requested
114    /// by the caller (e.g. the `SIZE` generic on a
115    /// [`block_device_driver::BlockDevice`] impl).
116    #[cfg(feature = "block-device-driver")]
117    BlockSizeMismatch,
118}
119
120impl From<PipeError> for MscError {
121    fn from(e: PipeError) -> Self {
122        Self::Transfer(e)
123    }
124}
125
126impl core::fmt::Display for MscError {
127    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
128        match self {
129            Self::Transfer(_) => write!(f, "Transfer error"),
130            Self::NoInterface => write!(f, "No MSC BBB/SCSI interface found"),
131            Self::NoPipe => write!(f, "No free pipe"),
132            Self::InvalidResponse => write!(f, "Invalid response from device"),
133            Self::Protocol => write!(f, "BBB protocol violation"),
134            Self::PhaseError => write!(f, "BBB phase error"),
135            Self::Scsi(_) => write!(f, "SCSI command failed"),
136            Self::Unaligned => write!(f, "Buffer is not block-aligned"),
137            Self::OutOfRange => write!(f, "LBA out of range"),
138            Self::NoSuchLun => write!(f, "No such LUN"),
139            Self::InvalidCdb => write!(f, "Invalid CDB length"),
140            #[cfg(feature = "block-device-driver")]
141            Self::BlockSizeMismatch => write!(f, "Block size mismatch"),
142        }
143    }
144}
145
146impl core::error::Error for MscError {}
147
148/// Direction and buffer for the optional data phase of a SCSI command.
149pub enum DataDir<'a> {
150    /// No data phase (e.g. `TEST UNIT READY`).
151    None,
152    /// Device-to-host: bytes are read into `buf`.
153    In(&'a mut [u8]),
154    /// Host-to-device: bytes in `buf` are sent.
155    Out(&'a [u8]),
156}
157
158impl DataDir<'_> {
159    fn len(&self) -> u32 {
160        match self {
161            Self::None => 0,
162            Self::In(b) => b.len() as u32,
163            Self::Out(b) => b.len() as u32,
164        }
165    }
166
167    fn cbw_flags(&self) -> u8 {
168        match self {
169            Self::In(_) => CBW_FLAG_IN,
170            _ => 0,
171        }
172    }
173}
174
175/// Result of a [`MscDevice::command`] cycle whose CSW was received.
176///
177/// Transport-level problems (stall without recovery, protocol violation,
178/// phase error) return [`MscError`] instead.
179#[derive(Copy, Clone, Debug, Eq, PartialEq)]
180#[cfg_attr(feature = "defmt", derive(defmt::Format))]
181pub enum CommandOutcome {
182    /// CSW status = `0x00` (Passed).
183    Ok {
184        /// `dCSWDataResidue`: bytes of the announced data transfer that
185        /// were not consumed.
186        residue: u32,
187    },
188    /// CSW status = `0x01` (Failed). Issue `REQUEST SENSE` for detail.
189    Failed {
190        /// `dCSWDataResidue`.
191        residue: u32,
192    },
193}
194
195// --------------------------------------------------------------------------
196// SCSI types.
197// --------------------------------------------------------------------------
198
199/// SCSI peripheral-device type (SPC-3 §6.4.2, bits 0..4 of INQUIRY byte 0).
200#[derive(Copy, Clone, Debug, Eq, PartialEq)]
201#[cfg_attr(feature = "defmt", derive(defmt::Format))]
202pub enum PeripheralType {
203    /// Direct-access block device (USB sticks, HDDs, SSDs, SD cards).
204    DirectAccess,
205    /// Sequential-access device (tape).
206    SequentialAccess,
207    /// CD/DVD.
208    CdDvd,
209    /// Optical memory device.
210    Optical,
211    /// Reduced-block-command (RBC) direct-access device.
212    SimplifiedDirectAccess,
213    /// Any other peripheral type.
214    Other(u8),
215}
216
217impl PeripheralType {
218    fn from_bits(b: u8) -> Self {
219        match b & 0x1F {
220            0x00 => Self::DirectAccess,
221            0x01 => Self::SequentialAccess,
222            0x05 => Self::CdDvd,
223            0x07 => Self::Optical,
224            0x0E => Self::SimplifiedDirectAccess,
225            v => Self::Other(v),
226        }
227    }
228}
229
230/// Decoded `INQUIRY` standard response (SPC-3 §6.4.2).
231///
232/// String fields reference the 36-byte buffer passed to
233/// [`MscLun::inquiry`]. They are ASCII, space-padded, not
234/// NUL-terminated.
235#[derive(Copy, Clone, Debug)]
236pub struct InquiryData<'a> {
237    /// Peripheral-device type (byte 0, bits 0..4).
238    pub peripheral: PeripheralType,
239    /// RMB bit (byte 1, bit 7): `true` if the medium can be removed.
240    pub removable: bool,
241    /// Vendor identification (8 bytes).
242    pub vendor: &'a [u8],
243    /// Product identification (16 bytes).
244    pub product: &'a [u8],
245    /// Product revision level (4 bytes).
246    pub revision: &'a [u8],
247}
248
249/// SCSI sense key (SPC-3 §4.5.6, Table 27).
250#[derive(Copy, Clone, Debug, Eq, PartialEq)]
251#[cfg_attr(feature = "defmt", derive(defmt::Format))]
252#[repr(u8)]
253pub enum SenseKey {
254    /// `0x0` — no error.
255    NoSense = 0x0,
256    /// `0x1` — command succeeded with automatic recovery.
257    RecoveredError = 0x1,
258    /// `0x2` — the medium is not ready.
259    NotReady = 0x2,
260    /// `0x3` — unrecoverable medium error.
261    MediumError = 0x3,
262    /// `0x4` — non-medium hardware error.
263    HardwareError = 0x4,
264    /// `0x5` — illegal CDB or parameter.
265    IllegalRequest = 0x5,
266    /// `0x6` — reset, medium change, or parameter change.
267    UnitAttention = 0x6,
268    /// `0x7` — write-protected medium.
269    DataProtect = 0x7,
270    /// `0x8` — blank medium on a device that expected data.
271    BlankCheck = 0x8,
272    /// `0x9` — vendor-specific.
273    VendorSpecific = 0x9,
274    /// `0xA` — COPY/COMPARE aborted.
275    CopyAborted = 0xA,
276    /// `0xB` — target aborted the command.
277    AbortedCommand = 0xB,
278    /// `0xD` — volume overflow on a sequential device.
279    VolumeOverflow = 0xD,
280    /// `0xE` — data did not match expected values.
281    Miscompare = 0xE,
282    /// Any reserved sense key value.
283    Reserved = 0xF,
284}
285
286impl SenseKey {
287    fn from_bits(b: u8) -> Self {
288        match b & 0x0F {
289            0x0 => Self::NoSense,
290            0x1 => Self::RecoveredError,
291            0x2 => Self::NotReady,
292            0x3 => Self::MediumError,
293            0x4 => Self::HardwareError,
294            0x5 => Self::IllegalRequest,
295            0x6 => Self::UnitAttention,
296            0x7 => Self::DataProtect,
297            0x8 => Self::BlankCheck,
298            0x9 => Self::VendorSpecific,
299            0xA => Self::CopyAborted,
300            0xB => Self::AbortedCommand,
301            0xD => Self::VolumeOverflow,
302            0xE => Self::Miscompare,
303            _ => Self::Reserved,
304        }
305    }
306}
307
308/// Decoded fixed-format sense data (SPC-3 §4.5.3).
309///
310/// Use the raw REQUEST SENSE response if more detail is needed.
311#[derive(Copy, Clone, Debug, Eq, PartialEq)]
312#[cfg_attr(feature = "defmt", derive(defmt::Format))]
313pub struct SenseData {
314    /// Sense key (byte 2, bits 0..3).
315    pub key: SenseKey,
316    /// Additional Sense Code (byte 12).
317    pub asc: u8,
318    /// Additional Sense Code Qualifier (byte 13).
319    pub ascq: u8,
320}
321
322/// Block-device capacity derived from `READ CAPACITY(10)` or `(16)`.
323#[derive(Copy, Clone, Debug, Eq, PartialEq)]
324#[cfg_attr(feature = "defmt", derive(defmt::Format))]
325pub struct BlockCapacity {
326    /// Total number of addressable blocks.
327    pub block_count: u64,
328    /// Block size in bytes.
329    pub block_size: u32,
330}
331
332// --------------------------------------------------------------------------
333// Descriptor walker.
334// --------------------------------------------------------------------------
335
336/// Descriptor-located info for the MSC interface.
337#[derive(Copy, Clone, Debug)]
338#[cfg_attr(feature = "defmt", derive(defmt::Format))]
339pub struct MscInfo {
340    /// USB interface number.
341    pub interface: u8,
342    /// Bulk IN endpoint address (with direction bit).
343    pub bulk_in_ep: u8,
344    /// Bulk IN max packet size.
345    pub bulk_in_mps: u16,
346    /// Bulk OUT endpoint address.
347    pub bulk_out_ep: u8,
348    /// Bulk OUT max packet size.
349    pub bulk_out_mps: u16,
350}
351
352/// Locate the first SCSI/BBB interface in `config_desc`.
353pub fn find_msc(config_desc: &[u8]) -> Option<MscInfo> {
354    let cfg = ConfigurationDescriptor::try_from_slice(config_desc).ok()?;
355
356    for iface in cfg.iter_interface() {
357        if iface.interface_class != CLASS_MSC
358            || iface.interface_subclass != SUBCLASS_SCSI
359            || iface.interface_protocol != PROTOCOL_BBB
360            || iface.alternate_setting != 0
361        {
362            continue;
363        }
364
365        let mut in_ep = None;
366        let mut out_ep = None;
367        for ep in iface.iter_endpoints() {
368            if ep.ep_type() != EndpointType::Bulk {
369                continue;
370            }
371            if ep.is_in() {
372                in_ep = Some((ep.endpoint_address, ep.max_packet_size));
373            } else {
374                out_ep = Some((ep.endpoint_address, ep.max_packet_size));
375            }
376        }
377
378        if let (Some((in_a, in_m)), Some((out_a, out_m))) = (in_ep, out_ep) {
379            return Some(MscInfo {
380                interface: iface.interface_number,
381                bulk_in_ep: in_a,
382                bulk_in_mps: in_m,
383                bulk_out_ep: out_a,
384                bulk_out_mps: out_m,
385            });
386        }
387    }
388
389    None
390}
391
392// --------------------------------------------------------------------------
393// Transport.
394// --------------------------------------------------------------------------
395
396/// Shared, mutex-protected BBB transport state.
397struct Transport<'d, A>
398where
399    A: UsbHostAllocator<'d>,
400{
401    ctrl: A::Pipe<pipe::Control, pipe::InOut>,
402    bulk_in: A::Pipe<pipe::Bulk, pipe::In>,
403    bulk_out: A::Pipe<pipe::Bulk, pipe::Out>,
404    interface: u8,
405    bulk_in_ep: u8,
406    bulk_out_ep: u8,
407    next_tag: u32,
408    /// Set when a command was cancelled mid-transfer; the next command
409    /// resets the transport before running.
410    dirty: bool,
411    _phantom: PhantomData<&'d ()>,
412}
413
414impl<'d, A> Transport<'d, A>
415where
416    A: UsbHostAllocator<'d>,
417{
418    async fn clear_halt_in(&mut self) -> Result<(), MscError> {
419        clear_endpoint_halt(&mut self.ctrl, self.bulk_in_ep).await?;
420        self.bulk_in.reset_data_toggle();
421        Ok(())
422    }
423
424    async fn clear_halt_out(&mut self) -> Result<(), MscError> {
425        clear_endpoint_halt(&mut self.ctrl, self.bulk_out_ep).await?;
426        self.bulk_out.reset_data_toggle();
427        Ok(())
428    }
429
430    async fn mass_storage_reset(&mut self) -> Result<(), MscError> {
431        let setup = SetupPacket::class_interface_out(REQ_BULK_ONLY_RESET, 0, self.interface as u16, 0);
432        self.ctrl.control_out(&setup.to_bytes(), &[]).await?;
433        self.clear_halt_in().await?;
434        self.clear_halt_out().await?;
435        Ok(())
436    }
437}
438
439async fn clear_endpoint_halt<P>(ctrl: &mut P, ep_addr: u8) -> Result<(), MscError>
440where
441    P: UsbPipe<pipe::Control, pipe::InOut>,
442{
443    let setup = SetupPacket {
444        request_type: RequestType {
445            direction: UsbDirection::Out,
446            control_type: ControlType::Standard,
447            recipient: Recipient::Endpoint,
448        },
449        request: REQ_CLEAR_FEATURE,
450        value: FEATURE_ENDPOINT_HALT,
451        index: ep_addr as u16,
452        length: 0,
453    };
454    ctrl.control_out(&setup.to_bytes(), &[]).await?;
455    Ok(())
456}
457
458async fn get_max_lun<P>(ctrl: &mut P, interface: u8) -> Result<u8, MscError>
459where
460    P: UsbPipe<pipe::Control, pipe::InOut>,
461{
462    let setup = SetupPacket::class_interface_in(REQ_GET_MAX_LUN, 0, interface as u16, 1);
463    let mut buf = [0u8; 1];
464    // Many devices stall GET_MAX_LUN — treat that as "single LUN".
465    match ctrl.control_in(&setup.to_bytes(), &mut buf).await {
466        Ok(1) if buf[0] <= 15 => Ok(buf[0]),
467        Ok(_) => Err(MscError::InvalidResponse),
468        Err(PipeError::Stall) => Ok(0),
469        Err(e) => Err(e.into()),
470    }
471}
472
473/// Run one CBW → data → CSW cycle. Caller holds the transport lock.
474async fn run_cycle<'d, A>(
475    t: &mut Transport<'d, A>,
476    lun: u8,
477    cdb: &[u8],
478    data: DataDir<'_>,
479) -> Result<CommandOutcome, MscError>
480where
481    A: UsbHostAllocator<'d>,
482{
483    let tag = {
484        let tag = t.next_tag;
485        t.next_tag = t.next_tag.wrapping_add(1);
486        tag
487    };
488
489    let mut cbw = [0u8; CBW_LEN];
490    cbw[0..4].copy_from_slice(&CBW_SIGNATURE.to_le_bytes());
491    cbw[4..8].copy_from_slice(&tag.to_le_bytes());
492    cbw[8..12].copy_from_slice(&data.len().to_le_bytes());
493    cbw[12] = data.cbw_flags();
494    cbw[13] = lun & 0x0F;
495    cbw[14] = cdb.len() as u8;
496    cbw[15..15 + cdb.len()].copy_from_slice(cdb);
497
498    trace!(
499        "MSC: CBW tag={:#010x} lun={} op={:#04x} data_len={} dir={}",
500        tag,
501        lun,
502        cdb[0],
503        data.len(),
504        match data {
505            DataDir::None => "none",
506            DataDir::In(_) => "in",
507            DataDir::Out(_) => "out",
508        },
509    );
510
511    // CBW phase.
512    if let Err(e) = t.bulk_out.request_out(&cbw, false).await {
513        if matches!(e, PipeError::Stall) {
514            t.clear_halt_out().await?;
515        }
516        return Err(e.into());
517    }
518
519    // Data phase. A stall here is recoverable: clear the halt, then
520    // read the CSW.
521    match data {
522        DataDir::None => {}
523        DataDir::In(buf) => match t.bulk_in.request_in(buf).await {
524            Ok(_) => {}
525            Err(PipeError::Stall) => t.clear_halt_in().await?,
526            Err(e) => return Err(e.into()),
527        },
528        DataDir::Out(buf) => match t.bulk_out.request_out(buf, false).await {
529            Ok(()) => {}
530            Err(PipeError::Stall) => t.clear_halt_out().await?,
531            Err(e) => return Err(e.into()),
532        },
533    }
534
535    // CSW phase. A stall here gets one retry after clearing.
536    let csw = match read_csw(t).await {
537        Ok(b) => b,
538        Err(MscError::Transfer(PipeError::Stall)) => {
539            t.clear_halt_in().await?;
540            read_csw(t).await?
541        }
542        Err(e) => return Err(e),
543    };
544
545    let signature = u32::from_le_bytes([csw[0], csw[1], csw[2], csw[3]]);
546    let csw_tag = u32::from_le_bytes([csw[4], csw[5], csw[6], csw[7]]);
547    let residue = u32::from_le_bytes([csw[8], csw[9], csw[10], csw[11]]);
548    let status = csw[12];
549
550    if signature != CSW_SIGNATURE || csw_tag != tag {
551        warn!(
552            "MSC: CSW mismatch (expected sig={:#010x} tag={:#010x}, got sig={:#010x} tag={:#010x} residue={} status={:#04x}, raw={:?})",
553            CSW_SIGNATURE, tag, signature, csw_tag, residue, status, csw,
554        );
555        t.mass_storage_reset().await.ok();
556        return Err(MscError::Protocol);
557    }
558
559    match status {
560        CSW_PASSED => Ok(CommandOutcome::Ok { residue }),
561        CSW_FAILED => Ok(CommandOutcome::Failed { residue }),
562        CSW_PHASE_ERROR => {
563            t.mass_storage_reset().await?;
564            Err(MscError::PhaseError)
565        }
566        _ => {
567            t.mass_storage_reset().await.ok();
568            Err(MscError::Protocol)
569        }
570    }
571}
572
573async fn read_csw<'d, A>(t: &mut Transport<'d, A>) -> Result<[u8; CSW_LEN], MscError>
574where
575    A: UsbHostAllocator<'d>,
576{
577    let mut buf = [0u8; CSW_LEN];
578    let n = t.bulk_in.request_in(&mut buf).await?;
579    if n == CSW_LEN {
580        trace!("MSC: CSW raw={:?}", buf);
581        Ok(buf)
582    } else {
583        warn!(
584            "MSC: short CSW ({} bytes, expected {}), data={:?}",
585            n,
586            CSW_LEN,
587            &buf[..n]
588        );
589        Err(MscError::Protocol)
590    }
591}
592
593/// Run one command on an already-locked transport: dirty-check,
594/// recover if needed, run the cycle, and clear dirty on success.
595async fn command_locked<'d, A>(
596    t: &mut Transport<'d, A>,
597    lun: u8,
598    cdb: &[u8],
599    data: DataDir<'_>,
600) -> Result<CommandOutcome, MscError>
601where
602    A: UsbHostAllocator<'d>,
603{
604    if t.dirty {
605        // A previous command was cancelled or wedged the transport;
606        // re-sync before starting.
607        t.mass_storage_reset().await?;
608        t.dirty = false;
609    }
610    t.dirty = true;
611    let result = run_cycle(t, lun, cdb, data).await;
612    if result.is_ok() {
613        t.dirty = false;
614    }
615    result
616}
617
618/// Issue `REQUEST SENSE` on a locked transport.
619async fn request_sense_locked<'d, A>(t: &mut Transport<'d, A>, lun: u8) -> Result<SenseData, MscError>
620where
621    A: UsbHostAllocator<'d>,
622{
623    let cdb = [SCSI_REQUEST_SENSE, 0, 0, 0, 18, 0];
624    let mut buf = [0u8; 18];
625    // Accept Failed here too — a second failure would loop.
626    let _ = command_locked(t, lun, &cdb, DataDir::In(&mut buf)).await?;
627    Ok(SenseData {
628        key: SenseKey::from_bits(buf[2]),
629        asc: buf[12],
630        ascq: buf[13],
631    })
632}
633
634/// Run a command on an already-locked transport, auto-issuing
635/// `REQUEST SENSE` on CSW Failed so the sense data reports the
636/// command we just ran (no other task can interleave).
637async fn run_with_sense_locked<'d, A>(
638    t: &mut Transport<'d, A>,
639    lun: u8,
640    cdb: &[u8],
641    data: DataDir<'_>,
642) -> Result<u32, MscError>
643where
644    A: UsbHostAllocator<'d>,
645{
646    match command_locked(t, lun, cdb, data).await? {
647        CommandOutcome::Ok { residue } => Ok(residue),
648        CommandOutcome::Failed { .. } => Err(MscError::Scsi(request_sense_locked(t, lun).await?)),
649    }
650}
651
652// --------------------------------------------------------------------------
653// MscDevice.
654// --------------------------------------------------------------------------
655
656/// USB Mass Storage Class host device.
657///
658/// Owns the control and bulk pipes for one BBB/SCSI interface and
659/// serializes all command traffic through an internal async mutex.
660/// Open per-LUN handles with [`MscDevice::lun`].
661///
662/// Use [`MscDevice::new`] for the common single-task case; the
663/// transport mutex is a [`NoopRawMutex`] with no runtime cost. To
664/// share LUNs across tasks, construct with
665/// [`MscDevice::new_with_raw_mutex`] and pick a `Sync` raw mutex.
666pub struct MscDevice<'d, A, M = NoopRawMutex>
667where
668    A: UsbHostAllocator<'d>,
669    M: RawMutex,
670{
671    transport: Mutex<M, Transport<'d, A>>,
672    interface: u8,
673    max_lun: u8,
674    _phantom: PhantomData<&'d ()>,
675}
676
677impl<'d, A> MscDevice<'d, A, NoopRawMutex>
678where
679    A: UsbHostAllocator<'d>,
680{
681    /// Allocate the control and bulk pipes for the first BBB/SCSI
682    /// interface in `config_desc`, probe `GET_MAX_LUN`, and wrap the
683    /// transport in a [`NoopRawMutex`].
684    ///
685    /// The resulting device is `!Sync`. Use this constructor when all
686    /// LUNs stay in one task. For multi-task sharing, use
687    /// [`MscDevice::new_with_raw_mutex`] instead.
688    pub async fn new(alloc: &A, enum_info: &EnumerationInfo, config_desc: &[u8]) -> Result<Self, MscError> {
689        Self::new_with_raw_mutex(alloc, enum_info, config_desc).await
690    }
691}
692
693impl<'d, A, M> MscDevice<'d, A, M>
694where
695    A: UsbHostAllocator<'d>,
696    M: RawMutex,
697{
698    /// Allocate the control and bulk pipes for the first BBB/SCSI
699    /// interface in `config_desc`, probe `GET_MAX_LUN`, and wrap the
700    /// transport in the caller-chosen raw mutex `M`.
701    ///
702    /// Pick a `Sync` raw mutex (e.g. `CriticalSectionRawMutex`) to
703    /// drive LUNs from multiple tasks. For single-task use, prefer
704    /// [`MscDevice::new`].
705    pub async fn new_with_raw_mutex(
706        alloc: &A,
707        enum_info: &EnumerationInfo,
708        config_desc: &[u8],
709    ) -> Result<Self, MscError> {
710        let info = find_msc(config_desc).ok_or(MscError::NoInterface)?;
711
712        let ctrl_ep_info = EndpointInfo {
713            addr: EndpointAddress::from_parts(0, UsbDirection::In),
714            ep_type: EndpointType::Control,
715            max_packet_size: enum_info.device_desc.max_packet_size0 as u16,
716            interval_ms: 0,
717        };
718
719        let in_ep_info = EndpointInfo {
720            addr: EndpointAddress::from_parts((info.bulk_in_ep & 0x0F) as usize, UsbDirection::In),
721            ep_type: EndpointType::Bulk,
722            max_packet_size: info.bulk_in_mps,
723            interval_ms: 0,
724        };
725
726        let out_ep_info = EndpointInfo {
727            addr: EndpointAddress::from_parts((info.bulk_out_ep & 0x0F) as usize, UsbDirection::Out),
728            ep_type: EndpointType::Bulk,
729            max_packet_size: info.bulk_out_mps,
730            interval_ms: 0,
731        };
732
733        let device_address = enum_info.device_address;
734        let split: Option<SplitInfo> = enum_info.split();
735
736        let mut ctrl = alloc
737            .alloc_pipe::<pipe::Control, pipe::InOut>(device_address, &ctrl_ep_info, split)
738            .map_err(|_| MscError::NoPipe)?;
739        let bulk_in = alloc
740            .alloc_pipe::<pipe::Bulk, pipe::In>(device_address, &in_ep_info, split)
741            .map_err(|_| MscError::NoPipe)?;
742        let bulk_out = alloc
743            .alloc_pipe::<pipe::Bulk, pipe::Out>(device_address, &out_ep_info, split)
744            .map_err(|_| MscError::NoPipe)?;
745
746        let max_lun = get_max_lun(&mut ctrl, info.interface).await?;
747
748        let device = Self {
749            transport: Mutex::new(Transport {
750                ctrl,
751                bulk_in,
752                bulk_out,
753                interface: info.interface,
754                bulk_in_ep: info.bulk_in_ep,
755                bulk_out_ep: info.bulk_out_ep,
756                next_tag: 1,
757                dirty: false,
758                _phantom: PhantomData,
759            }),
760            interface: info.interface,
761            max_lun,
762            _phantom: PhantomData,
763        };
764
765        // Put the BBB transport into a known-clean state before any real
766        // command. Best-effort: a few devices stall Bulk-Only Reset — that's
767        // fine, the state they care about is whatever they reset during
768        // enumeration.
769        if let Err(e) = device.reset().await {
770            debug!("MSC: initial Bulk-Only Reset failed ({:?}); continuing anyway", e);
771        }
772
773        Ok(device)
774    }
775
776    /// USB interface number this device is bound to.
777    pub fn interface(&self) -> u8 {
778        self.interface
779    }
780
781    /// Highest valid LUN index (as returned by `GET_MAX_LUN`).
782    pub fn max_lun(&self) -> u8 {
783        self.max_lun
784    }
785
786    /// Number of LUNs exposed by the device (`max_lun() + 1`).
787    pub fn num_luns(&self) -> u8 {
788        self.max_lun + 1
789    }
790
791    /// Handle to the given LUN.
792    ///
793    /// LUN handles are cheap and do not reserve any transport
794    /// resource; issuing more than one for the same LUN is permitted
795    /// but only useful if the caller manages the split state between
796    /// them.
797    pub fn lun(&self, lun: u8) -> Result<MscLun<'_, 'd, A, M>, MscError> {
798        if lun > self.max_lun {
799            return Err(MscError::NoSuchLun);
800        }
801        Ok(MscLun {
802            device: self,
803            lun,
804            capacity: None,
805        })
806    }
807
808    /// Run one Bulk-Only command cycle and return the outcome.
809    ///
810    /// `cdb` must be 1..=16 bytes. The length of `data` is reported as
811    /// `dCBWDataTransferLength` and drives the data phase.
812    ///
813    /// Recovers from endpoint stalls (via `CLEAR_FEATURE(ENDPOINT_HALT)`
814    /// plus data-toggle reset) and from CSW signature/tag mismatches
815    /// (via Bulk-Only Mass Storage Reset). A CSW status of `0x02`
816    /// (phase error) triggers a reset and returns
817    /// [`MscError::PhaseError`].
818    ///
819    /// # Cancellation
820    ///
821    /// Not cancel-safe: dropping the future mid-cycle leaves the
822    /// device in an undefined state. The transport marks itself dirty
823    /// and issues a Mass Storage Reset before the next command.
824    pub async fn command(&self, lun: u8, cdb: &[u8], data: DataDir<'_>) -> Result<CommandOutcome, MscError> {
825        if cdb.is_empty() || cdb.len() > 16 {
826            return Err(MscError::InvalidCdb);
827        }
828        if lun > self.max_lun {
829            return Err(MscError::NoSuchLun);
830        }
831
832        let mut t = self.transport.lock().await;
833        command_locked(&mut t, lun, cdb, data).await
834    }
835
836    /// Issue a Bulk-Only Mass Storage Reset followed by
837    /// `CLEAR_FEATURE(ENDPOINT_HALT)` on both bulk endpoints.
838    pub async fn reset(&self) -> Result<(), MscError> {
839        let mut t = self.transport.lock().await;
840        t.mass_storage_reset().await?;
841        t.dirty = false;
842        Ok(())
843    }
844}
845
846// --------------------------------------------------------------------------
847// MscLun.
848// --------------------------------------------------------------------------
849
850/// Handle for a single Logical Unit.
851///
852/// Borrows the [`MscDevice`] for transport access. Convenience methods
853/// wrap the SCSI commands a storage host typically needs; power users
854/// can issue arbitrary CDBs via [`MscDevice::command`].
855///
856/// Block-I/O methods use the LUN's cached capacity. The first call to
857/// [`MscLun::capacity`] fills it; [`MscLun::invalidate_capacity`]
858/// clears it (useful after a `UnitAttention` indicating media change).
859pub struct MscLun<'dev, 'd, A, M = NoopRawMutex>
860where
861    A: UsbHostAllocator<'d>,
862    M: RawMutex,
863{
864    device: &'dev MscDevice<'d, A, M>,
865    lun: u8,
866    capacity: Option<BlockCapacity>,
867}
868
869impl<'dev, 'd, A, M> MscLun<'dev, 'd, A, M>
870where
871    A: UsbHostAllocator<'d>,
872    M: RawMutex,
873{
874    /// LUN index.
875    pub fn lun(&self) -> u8 {
876        self.lun
877    }
878
879    /// Cached [`BlockCapacity`], if [`capacity`](Self::capacity) has
880    /// been called.
881    pub fn cached_capacity(&self) -> Option<BlockCapacity> {
882        self.capacity
883    }
884
885    /// Clear the cached capacity so the next I/O re-fetches it.
886    pub fn invalidate_capacity(&mut self) {
887        self.capacity = None;
888    }
889
890    /// Run a single command with auto-sense on failure. Takes and
891    /// releases the transport lock.
892    async fn run(&mut self, cdb: &[u8], data: DataDir<'_>) -> Result<u32, MscError> {
893        let mut t = self.device.transport.lock().await;
894        run_with_sense_locked(&mut t, self.lun, cdb, data).await
895    }
896
897    /// Run `INQUIRY` (standard data, 36 bytes).
898    pub async fn inquiry<'a>(&mut self, buf: &'a mut [u8; 36]) -> Result<InquiryData<'a>, MscError> {
899        let cdb = [SCSI_INQUIRY, 0, 0, 0, 36, 0];
900        self.run(&cdb, DataDir::In(&mut buf[..])).await?;
901        Ok(InquiryData {
902            peripheral: PeripheralType::from_bits(buf[0]),
903            removable: buf[1] & 0x80 != 0,
904            vendor: &buf[8..16],
905            product: &buf[16..32],
906            revision: &buf[32..36],
907        })
908    }
909
910    /// Read SCSI sense data via `REQUEST SENSE`.
911    pub async fn request_sense(&mut self) -> Result<SenseData, MscError> {
912        let mut t = self.device.transport.lock().await;
913        request_sense_locked(&mut t, self.lun).await
914    }
915
916    /// Probe with `TEST UNIT READY`.
917    ///
918    /// Returns `Ok(true)` when the unit is ready, `Ok(false)` on a
919    /// transient `NotReady` or `UnitAttention` sense (e.g. medium
920    /// not yet spun up or just inserted). Other failures surface as
921    /// [`MscError::Scsi`].
922    pub async fn test_unit_ready(&mut self) -> Result<bool, MscError> {
923        let cdb = [SCSI_TEST_UNIT_READY, 0, 0, 0, 0, 0];
924        // Hold the lock across the TUR → REQUEST SENSE pair so the
925        // sense data reports our TUR rather than any interleaved
926        // command.
927        let sense = {
928            let mut t = self.device.transport.lock().await;
929            match command_locked(&mut t, self.lun, &cdb, DataDir::None).await? {
930                CommandOutcome::Ok { .. } => return Ok(true),
931                CommandOutcome::Failed { .. } => request_sense_locked(&mut t, self.lun).await?,
932            }
933        };
934        match sense.key {
935            SenseKey::NotReady | SenseKey::UnitAttention => {
936                self.invalidate_capacity();
937                Ok(false)
938            }
939            _ => Err(MscError::Scsi(sense)),
940        }
941    }
942
943    /// Enable or disable medium removal by the user.
944    pub async fn prevent_medium_removal(&mut self, prevent: bool) -> Result<(), MscError> {
945        let cdb = [SCSI_PREVENT_ALLOW_REMOVAL, 0, 0, 0, prevent as u8, 0];
946        self.run(&cdb, DataDir::None).await?;
947        Ok(())
948    }
949
950    /// Fetch and cache the LUN's block capacity.
951    ///
952    /// Uses `READ CAPACITY(10)`; falls back to `READ CAPACITY(16)` when
953    /// the device reports the sentinel `0xFFFFFFFF` (i.e. the LUN is
954    /// larger than 2 TiB at 512-byte blocks). The two probes run
955    /// under a single lock hold.
956    pub async fn capacity(&mut self) -> Result<BlockCapacity, MscError> {
957        if let Some(c) = self.capacity {
958            return Ok(c);
959        }
960
961        let cap = {
962            let mut t = self.device.transport.lock().await;
963
964            let cdb = [SCSI_READ_CAPACITY_10, 0, 0, 0, 0, 0, 0, 0, 0, 0];
965            let mut buf = [0u8; 8];
966            run_with_sense_locked(&mut t, self.lun, &cdb, DataDir::In(&mut buf)).await?;
967            let last_lba = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
968            let block_size = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
969
970            if last_lba == 0xFFFF_FFFF {
971                let mut cdb16 = [0u8; 16];
972                cdb16[0] = SCSI_SERVICE_ACTION_IN_16;
973                cdb16[1] = SCSI_SA_READ_CAPACITY_16;
974                cdb16[13] = 32;
975                let mut buf16 = [0u8; 32];
976                run_with_sense_locked(&mut t, self.lun, &cdb16, DataDir::In(&mut buf16)).await?;
977                let last_lba = u64::from_be_bytes([
978                    buf16[0], buf16[1], buf16[2], buf16[3], buf16[4], buf16[5], buf16[6], buf16[7],
979                ]);
980                let block_size = u32::from_be_bytes([buf16[8], buf16[9], buf16[10], buf16[11]]);
981                BlockCapacity {
982                    block_count: last_lba.saturating_add(1),
983                    block_size,
984                }
985            } else {
986                BlockCapacity {
987                    block_count: last_lba as u64 + 1,
988                    block_size,
989                }
990            }
991        };
992
993        if cap.block_size == 0 {
994            return Err(MscError::InvalidResponse);
995        }
996
997        self.capacity = Some(cap);
998        Ok(cap)
999    }
1000
1001    /// Read `buf.len() / block_size` blocks starting at `lba`.
1002    ///
1003    /// `buf.len()` must be a non-zero multiple of `block_size`.
1004    ///
1005    /// Uses `READ(10)` when `lba` fits in `u32` and the chunk count
1006    /// fits in `u16`, else `READ(16)`. Large reads are transparently
1007    /// split into chunks of up to 65 535 blocks per command, all
1008    /// issued under a single transport lock hold.
1009    pub async fn read_blocks(&mut self, lba: u64, buf: &mut [u8]) -> Result<(), MscError> {
1010        let cap = self.capacity().await?;
1011        let (block_size, total_blocks) = check_block_args(lba, buf.len(), &cap)?;
1012
1013        let mut t = self.device.transport.lock().await;
1014
1015        let mut cur_lba = lba;
1016        let mut offset = 0usize;
1017        let mut remaining = total_blocks;
1018
1019        while remaining > 0 {
1020            let (n, use_10) = chunk_blocks(cur_lba, remaining);
1021            let bytes = n as usize * block_size;
1022            let chunk = &mut buf[offset..offset + bytes];
1023
1024            let residue = if use_10 {
1025                let cdb = read10_cdb(cur_lba as u32, n as u16);
1026                run_with_sense_locked(&mut t, self.lun, &cdb, DataDir::In(chunk)).await?
1027            } else {
1028                let cdb = read16_cdb(cur_lba, n);
1029                run_with_sense_locked(&mut t, self.lun, &cdb, DataDir::In(chunk)).await?
1030            };
1031            if residue != 0 {
1032                return Err(MscError::InvalidResponse);
1033            }
1034
1035            offset += bytes;
1036            cur_lba += n as u64;
1037            remaining -= n as u64;
1038        }
1039        Ok(())
1040    }
1041
1042    /// Write `buf.len() / block_size` blocks starting at `lba`.
1043    ///
1044    /// `buf.len()` must be a non-zero multiple of `block_size`.
1045    ///
1046    /// Uses `WRITE(10)` when `lba` fits in `u32` and the chunk count
1047    /// fits in `u16`, else `WRITE(16)`. Large writes are transparently
1048    /// split into chunks of up to 65 535 blocks per command, all
1049    /// issued under a single transport lock hold.
1050    pub async fn write_blocks(&mut self, lba: u64, buf: &[u8]) -> Result<(), MscError> {
1051        let cap = self.capacity().await?;
1052        let (block_size, total_blocks) = check_block_args(lba, buf.len(), &cap)?;
1053
1054        let mut t = self.device.transport.lock().await;
1055
1056        let mut cur_lba = lba;
1057        let mut offset = 0usize;
1058        let mut remaining = total_blocks;
1059
1060        while remaining > 0 {
1061            let (n, use_10) = chunk_blocks(cur_lba, remaining);
1062            let bytes = n as usize * block_size;
1063            let chunk = &buf[offset..offset + bytes];
1064
1065            let residue = if use_10 {
1066                let cdb = write10_cdb(cur_lba as u32, n as u16);
1067                run_with_sense_locked(&mut t, self.lun, &cdb, DataDir::Out(chunk)).await?
1068            } else {
1069                let cdb = write16_cdb(cur_lba, n);
1070                run_with_sense_locked(&mut t, self.lun, &cdb, DataDir::Out(chunk)).await?
1071            };
1072            if residue != 0 {
1073                return Err(MscError::InvalidResponse);
1074            }
1075
1076            offset += bytes;
1077            cur_lba += n as u64;
1078            remaining -= n as u64;
1079        }
1080        Ok(())
1081    }
1082
1083    /// Flush the device's write cache (`SYNCHRONIZE CACHE(10)` over
1084    /// the entire LUN).
1085    pub async fn flush(&mut self) -> Result<(), MscError> {
1086        let cdb = [SCSI_SYNCHRONIZE_CACHE_10, 0, 0, 0, 0, 0, 0, 0, 0, 0];
1087        self.run(&cdb, DataDir::None).await?;
1088        Ok(())
1089    }
1090
1091    /// Wrap this LUN as a [`block_device_driver::BlockDevice`].
1092    ///
1093    /// `ALIGN` selects the alignment of the caller's block buffers
1094    /// (must divide `SIZE`). The `SIZE` const generic on the trait
1095    /// impl is the block size in bytes; it is checked against the
1096    /// LUN's reported block size on every call and
1097    /// [`MscError::BlockSizeMismatch`] is returned on mismatch.
1098    #[cfg(feature = "block-device-driver")]
1099    pub fn as_block_device<ALIGN>(&mut self) -> MscBlockDevice<'_, 'dev, 'd, A, M, ALIGN>
1100    where
1101        ALIGN: aligned::Alignment,
1102    {
1103        MscBlockDevice {
1104            lun: self,
1105            _align: PhantomData,
1106        }
1107    }
1108}
1109
1110// --------------------------------------------------------------------------
1111// block-device-driver integration.
1112// --------------------------------------------------------------------------
1113
1114/// [`block_device_driver::BlockDevice`] adapter for an [`MscLun`].
1115///
1116/// Constructed by [`MscLun::as_block_device`]. The `ALIGN` type
1117/// parameter picks the buffer alignment required by the caller (for
1118/// example the DMA alignment of the downstream consumer). The
1119/// `BlockDevice` trait is implemented for every `SIZE` — the LUN's
1120/// reported block size is validated on every call.
1121#[cfg(feature = "block-device-driver")]
1122pub struct MscBlockDevice<'lun, 'dev, 'd, A, M, ALIGN>
1123where
1124    A: UsbHostAllocator<'d>,
1125    M: RawMutex,
1126    ALIGN: aligned::Alignment,
1127{
1128    lun: &'lun mut MscLun<'dev, 'd, A, M>,
1129    _align: PhantomData<fn() -> ALIGN>,
1130}
1131
1132#[cfg(feature = "block-device-driver")]
1133impl<'lun, 'dev, 'd, A, M, ALIGN, const SIZE: usize> block_device_driver::BlockDevice<SIZE>
1134    for MscBlockDevice<'lun, 'dev, 'd, A, M, ALIGN>
1135where
1136    A: UsbHostAllocator<'d>,
1137    M: RawMutex,
1138    ALIGN: aligned::Alignment,
1139{
1140    type Error = MscError;
1141    type Align = ALIGN;
1142
1143    async fn read(
1144        &mut self,
1145        block_address: u32,
1146        data: &mut [aligned::Aligned<ALIGN, [u8; SIZE]>],
1147    ) -> Result<(), MscError> {
1148        let cap = self.lun.capacity().await?;
1149        if cap.block_size as usize != SIZE {
1150            return Err(MscError::BlockSizeMismatch);
1151        }
1152        let bytes = block_device_driver::blocks_to_slice_mut(data);
1153        self.lun.read_blocks(block_address as u64, bytes).await
1154    }
1155
1156    async fn write(
1157        &mut self,
1158        block_address: u32,
1159        data: &[aligned::Aligned<ALIGN, [u8; SIZE]>],
1160    ) -> Result<(), MscError> {
1161        let cap = self.lun.capacity().await?;
1162        if cap.block_size as usize != SIZE {
1163            return Err(MscError::BlockSizeMismatch);
1164        }
1165        let bytes = block_device_driver::blocks_to_slice(data);
1166        self.lun.write_blocks(block_address as u64, bytes).await
1167    }
1168
1169    async fn size(&mut self) -> Result<u64, MscError> {
1170        let cap = self.lun.capacity().await?;
1171        Ok(cap.block_count.saturating_mul(cap.block_size as u64))
1172    }
1173}
1174
1175/// Pick the next chunk size and CDB flavour.
1176///
1177/// Prefers `READ/WRITE(10)` whenever the starting LBA fits in a `u32`
1178/// (clamping the chunk to `u16::MAX` blocks) for maximum device
1179/// compatibility; only falls back to the 16-byte variants when the LBA
1180/// itself exceeds the 32-bit range.
1181fn chunk_blocks(lba: u64, remaining: u64) -> (u32, bool) {
1182    const MAX_BLOCKS_10: u64 = u16::MAX as u64;
1183    let use_10 = lba <= u32::MAX as u64;
1184    let n = if use_10 {
1185        remaining.min(MAX_BLOCKS_10) as u32
1186    } else {
1187        remaining.min(u32::MAX as u64) as u32
1188    };
1189    (n, use_10)
1190}
1191
1192fn check_block_args(lba: u64, bytes: usize, cap: &BlockCapacity) -> Result<(usize, u64), MscError> {
1193    let block_size = cap.block_size as usize;
1194    if bytes == 0 || !bytes.is_multiple_of(block_size) {
1195        return Err(MscError::Unaligned);
1196    }
1197    let total_blocks = (bytes / block_size) as u64;
1198    if lba.checked_add(total_blocks).is_none_or(|end| end > cap.block_count) {
1199        return Err(MscError::OutOfRange);
1200    }
1201    Ok((block_size, total_blocks))
1202}
1203
1204fn read10_cdb(lba: u32, blocks: u16) -> [u8; 10] {
1205    let lba = lba.to_be_bytes();
1206    let bl = blocks.to_be_bytes();
1207    [SCSI_READ_10, 0, lba[0], lba[1], lba[2], lba[3], 0, bl[0], bl[1], 0]
1208}
1209
1210fn write10_cdb(lba: u32, blocks: u16) -> [u8; 10] {
1211    let lba = lba.to_be_bytes();
1212    let bl = blocks.to_be_bytes();
1213    [SCSI_WRITE_10, 0, lba[0], lba[1], lba[2], lba[3], 0, bl[0], bl[1], 0]
1214}
1215
1216fn read16_cdb(lba: u64, blocks: u32) -> [u8; 16] {
1217    let lba = lba.to_be_bytes();
1218    let bl = blocks.to_be_bytes();
1219    [
1220        SCSI_READ_16,
1221        0,
1222        lba[0],
1223        lba[1],
1224        lba[2],
1225        lba[3],
1226        lba[4],
1227        lba[5],
1228        lba[6],
1229        lba[7],
1230        bl[0],
1231        bl[1],
1232        bl[2],
1233        bl[3],
1234        0,
1235        0,
1236    ]
1237}
1238
1239fn write16_cdb(lba: u64, blocks: u32) -> [u8; 16] {
1240    let lba = lba.to_be_bytes();
1241    let bl = blocks.to_be_bytes();
1242    [
1243        SCSI_WRITE_16,
1244        0,
1245        lba[0],
1246        lba[1],
1247        lba[2],
1248        lba[3],
1249        lba[4],
1250        lba[5],
1251        lba[6],
1252        lba[7],
1253        bl[0],
1254        bl[1],
1255        bl[2],
1256        bl[3],
1257        0,
1258        0,
1259    ]
1260}
1261
1262#[cfg(test)]
1263mod tests {
1264    use super::*;
1265
1266    // ----------------------------------------------------------------------
1267    // find_msc
1268    // ----------------------------------------------------------------------
1269
1270    /// Single MSC/SCSI/BBB interface, bulk IN (0x81, mps 64) + bulk OUT (0x01, mps 64).
1271    #[rustfmt::skip]
1272    const CFG_SIMPLE_MSC: [u8; 32] = [
1273        9, 0x02, 32, 0, 1, 1, 0, 0x80, 50,
1274        9, 0x04, 0, 0, 2, 0x08, 0x06, 0x50, 0,
1275        7, 0x05, 0x81, 0x02, 0x40, 0x00, 0,
1276        7, 0x05, 0x01, 0x02, 0x40, 0x00, 0,
1277    ];
1278
1279    #[test]
1280    fn find_msc_simple() {
1281        let info = find_msc(&CFG_SIMPLE_MSC).unwrap();
1282        assert_eq!(info.interface, 0);
1283        assert_eq!(info.bulk_in_ep, 0x81);
1284        assert_eq!(info.bulk_in_mps, 64);
1285        assert_eq!(info.bulk_out_ep, 0x01);
1286        assert_eq!(info.bulk_out_mps, 64);
1287    }
1288
1289    #[test]
1290    fn find_msc_rejects_non_matching_interface() {
1291        // Empty / header-only.
1292        assert!(find_msc(&[]).is_none());
1293
1294        // HID interface, no MSC anywhere.
1295        #[rustfmt::skip]
1296        let hid: [u8; 25] = [
1297            9, 0x02, 25, 0, 1, 1, 0, 0x80, 50,
1298            9, 0x04, 0, 0, 1, 0x03, 0x01, 0x01, 0,
1299            7, 0x05, 0x81, 0x03, 0x08, 0x00, 10,
1300        ];
1301        assert!(find_msc(&hid).is_none());
1302
1303        // MSC class but wrong subclass (UFI), wrong protocol (CBI), or non-zero alt.
1304        for (offset, value) in [(6, 0x08), (7, 0x01), (3, 1)] {
1305            let mut cfg = CFG_SIMPLE_MSC;
1306            cfg[9 + offset] = value;
1307            assert!(find_msc(&cfg).is_none());
1308        }
1309    }
1310
1311    #[test]
1312    fn find_msc_requires_both_bulk_endpoints() {
1313        // Only bulk OUT.
1314        #[rustfmt::skip]
1315        let out_only: [u8; 25] = [
1316            9, 0x02, 25, 0, 1, 1, 0, 0x80, 50,
1317            9, 0x04, 0, 0, 1, 0x08, 0x06, 0x50, 0,
1318            7, 0x05, 0x01, 0x02, 0x40, 0x00, 0,
1319        ];
1320        assert!(find_msc(&out_only).is_none());
1321
1322        // Only bulk IN.
1323        #[rustfmt::skip]
1324        let in_only: [u8; 25] = [
1325            9, 0x02, 25, 0, 1, 1, 0, 0x80, 50,
1326            9, 0x04, 0, 0, 1, 0x08, 0x06, 0x50, 0,
1327            7, 0x05, 0x81, 0x02, 0x40, 0x00, 0,
1328        ];
1329        assert!(find_msc(&in_only).is_none());
1330
1331        // Interrupt endpoints only (not bulk).
1332        #[rustfmt::skip]
1333        let intr: [u8; 32] = [
1334            9, 0x02, 32, 0, 1, 1, 0, 0x80, 50,
1335            9, 0x04, 0, 0, 2, 0x08, 0x06, 0x50, 0,
1336            7, 0x05, 0x81, 0x03, 0x08, 0x00, 10,
1337            7, 0x05, 0x01, 0x03, 0x08, 0x00, 10,
1338        ];
1339        assert!(find_msc(&intr).is_none());
1340    }
1341
1342    #[test]
1343    fn find_msc_skips_preceding_interfaces() {
1344        // Composite: iface 0 = HID, iface 1 alt 1 = MSC (ignored because alt != 0),
1345        // iface 1 alt 0 = MSC (selected).
1346        #[rustfmt::skip]
1347        let cfg: [u8; 71] = [
1348            9, 0x02, 71, 0, 2, 1, 0, 0x80, 50,
1349            9, 0x04, 0, 0, 1, 0x03, 0x01, 0x01, 0,
1350            7, 0x05, 0x82, 0x03, 0x08, 0x00, 10,
1351            9, 0x04, 1, 1, 2, 0x08, 0x06, 0x50, 0,
1352            7, 0x05, 0x83, 0x02, 0x20, 0x00, 0,
1353            7, 0x05, 0x03, 0x02, 0x20, 0x00, 0,
1354            9, 0x04, 1, 0, 2, 0x08, 0x06, 0x50, 0,
1355            7, 0x05, 0x81, 0x02, 0x40, 0x00, 0,
1356            7, 0x05, 0x01, 0x02, 0x40, 0x00, 0,
1357        ];
1358        let info = find_msc(&cfg).unwrap();
1359        assert_eq!(info.interface, 1);
1360        assert_eq!(info.bulk_in_ep, 0x81);
1361        assert_eq!(info.bulk_in_mps, 64);
1362        assert_eq!(info.bulk_out_ep, 0x01);
1363    }
1364
1365    // ----------------------------------------------------------------------
1366    // chunk_blocks
1367    // ----------------------------------------------------------------------
1368
1369    #[test]
1370    fn chunk_blocks_prefers_read10_while_lba_fits_u32() {
1371        // Small chunk passes through verbatim.
1372        assert_eq!(chunk_blocks(0, 1), (1, true));
1373        // Clamped to u16::MAX even for huge remaining counts and high LBA.
1374        assert_eq!(chunk_blocks(0, u16::MAX as u64), (u16::MAX as u32, true));
1375        assert_eq!(chunk_blocks(0, u64::MAX), (u16::MAX as u32, true));
1376        assert_eq!(chunk_blocks(u32::MAX as u64, u64::MAX), (u16::MAX as u32, true));
1377    }
1378
1379    #[test]
1380    fn chunk_blocks_falls_back_to_read16_above_u32_max() {
1381        assert_eq!(chunk_blocks(u32::MAX as u64 + 1, 100), (100, false));
1382        assert_eq!(chunk_blocks(u64::MAX - 10, u64::MAX), (u32::MAX, false));
1383    }
1384
1385    // ----------------------------------------------------------------------
1386    // check_block_args
1387    // ----------------------------------------------------------------------
1388
1389    const CAP_1K_512: BlockCapacity = BlockCapacity {
1390        block_count: 1000,
1391        block_size: 512,
1392    };
1393
1394    #[test]
1395    fn check_block_args_accepts_aligned_in_range() {
1396        assert_eq!(check_block_args(0, 512, &CAP_1K_512).unwrap(), (512, 1));
1397        assert_eq!(check_block_args(0, 10 * 512, &CAP_1K_512).unwrap(), (512, 10));
1398        // Exact fit at end of device.
1399        assert_eq!(check_block_args(999, 512, &CAP_1K_512).unwrap(), (512, 1));
1400    }
1401
1402    #[test]
1403    fn check_block_args_rejects_unaligned() {
1404        for bytes in [0, 511, 513] {
1405            assert!(matches!(
1406                check_block_args(0, bytes, &CAP_1K_512),
1407                Err(MscError::Unaligned)
1408            ));
1409        }
1410    }
1411
1412    #[test]
1413    fn check_block_args_rejects_out_of_range() {
1414        for (lba, bytes) in [(1000, 512), (999, 1024), (u64::MAX, 512)] {
1415            assert!(matches!(
1416                check_block_args(lba, bytes, &CAP_1K_512),
1417                Err(MscError::OutOfRange)
1418            ));
1419        }
1420    }
1421
1422    // ----------------------------------------------------------------------
1423    // CDB encoders
1424    // ----------------------------------------------------------------------
1425
1426    #[test]
1427    fn read_write_10_cdb_encoding() {
1428        let expected = [0, 0, 0x12, 0x34, 0x56, 0x78, 0, 0x12, 0x34, 0];
1429        for (op, cdb) in [
1430            (SCSI_READ_10, read10_cdb(0x1234_5678, 0x1234)),
1431            (SCSI_WRITE_10, write10_cdb(0x1234_5678, 0x1234)),
1432        ] {
1433            let mut want = expected;
1434            want[0] = op;
1435            assert_eq!(cdb, want);
1436        }
1437    }
1438
1439    #[test]
1440    fn read_write_16_cdb_encoding() {
1441        #[rustfmt::skip]
1442        let expected = [
1443            0, 0,
1444            0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
1445            0xDE, 0xAD, 0xBE, 0xEF,
1446            0, 0,
1447        ];
1448        for (op, cdb) in [
1449            (SCSI_READ_16, read16_cdb(0x0123_4567_89AB_CDEF, 0xDEAD_BEEF)),
1450            (SCSI_WRITE_16, write16_cdb(0x0123_4567_89AB_CDEF, 0xDEAD_BEEF)),
1451        ] {
1452            let mut want = expected;
1453            want[0] = op;
1454            assert_eq!(cdb, want);
1455        }
1456    }
1457}