Files
RedBear-OS/drivers/storage/usbscsid/src/protocol/uas.rs
T
Red Bear OS 6571df7802 acpi-rs: eliminate all AML stubs (resource descriptors, ConnectionField, Match, Index ref)
resource.rs — implement all ~20 stubbed resource descriptor parsers:
  - QWord/DWord/Word AddressSpace, IRQ, DMA, I/O, FixedI/O, FixedDMA
  - StartDependentFunctions, VendorDefined (small+large)
  - GPIOConnection, GenericSerialBus (I2C/SPI/UART subtypes)
  - PinFunction, PinConfiguration, PinGroup, PinGroupFunction, PinGroupConfiguration
  - ExtendedAddressSpace, GenericRegister
  - Both large/small dispatch tables now call real parsers
  - ACPI 6.5 §6.4 coverage complete, cross-referenced with ACPICA amlresrc.h

mod.rs — eliminate 3 remaining stubs:
  - ConnectionField in parse_field_list: namestring + inline buffer forms
  - Opcode::Match: full opcode handler with ResolveBehaviour::ByteData intercept,
    10-arg OpInFlight, do_match executor with 7 match operators
  - ReferenceKind::Index in do_copy_object: merged with Named/Local path

virtio-core: replace arch stubs (aarch64, riscv64) with real Error::Probe returns

usbscsid/uas: full UAS transport implementation (1006 lines) replacing
stub-heavy version — IUs, pipe detection, stream/non-stream modes,
task tag management, cross-referenced with Linux 7.1 uas.c

initnsmgr: Rc<RefCell<>> -> Arc<Mutex<>> for namespace concurrency safety

xhcid/quirks: fix comment (3 hci_version-dependent entries, not 2)

cargo check -p acpi: clean (3 pre-existing warnings only)
cargo test -p acpi --lib: 5/5 pass
2026-07-22 05:00:29 +09:00

1007 lines
39 KiB
Rust

//! USB Attached SCSI (UAS) transport, per the USB-IF "Universal Serial Bus
//! Attached SCSI (UAS)" specification and cross-referenced line-by-line with
//! Linux 7.1 `drivers/usb/storage/uas.c` and `include/linux/usb/uas.h`.
//!
//! # Protocol overview
//!
//! UAS replaces BOT's CBW/CSW framing with **Information Units (IUs)** sent
//! over four dedicated bulk pipes, identified by Pipe Usage descriptors
//! (bDescriptorType 0x24) embedded in each endpoint's extra data:
//!
//! | Pipe ID | Role | Direction | Linux name |
//! |---------|-----------|-----------|-------------------|
//! | 1 | Command | BULK OUT | `cmd_pipe` |
//! | 2 | Status | BULK IN | `status_pipe` |
//! | 3 | Data-in | BULK IN | `data_in_pipe` |
//! | 4 | Data-out | BULK OUT | `data_out_pipe` |
//!
//! On USB 3.x devices, the data and status pipes carry xHCI **streams** so
//! that up to 256 commands (`MAX_CMNDS`) can be outstanding concurrently,
//! each identified by a 1-based **task tag** that doubles as the stream ID
//! (Linux `uas.c:679`: `cmdinfo->uas_tag = idx + 1; /* uas-tag == usb-stream-id, so 1 based */`).
//!
//! On USB 2.0 (or whenever streams are unavailable) UAS falls back to a
//! serialized, single-command model: the device emits a READ_READY /
//! WRITE_READY IU on the status pipe to gate the data phase, then a final
//! Sense (STATUS) or Response IU. This serialized mode is
//! protocol-correct — it is how Linux runs UAS on streamless controllers
//! (`devinfo->use_streams = false`, `qdepth = 1`).
//!
//! # Implementation notes
//!
//! xhcid's client interface is synchronous (each `transfer_*` blocks until
//! the transfer completes), so this transport runs exactly one command at a
//! time regardless of stream support. Stream IDs are still emitted on the
//! data and status pipes when the endpoint is stream-capable, which keeps the
//! device-side stream matching correct and is the prerequisite for future
//! async/overlapped submission.
use xhcid_interface::{
ConfDesc, DeviceReqData, EndpDirection, EndpointStatus, IfDesc, PortReqRecipient, PortReqTy,
PortTransferStatus, PortTransferStatusKind, XhciClientHandle, XhciClientHandleError,
XhciEndpHandle,
};
use super::{Protocol, ProtocolError, SendCommandStatus, SendCommandStatusKind};
// ──────────────────────────────── IU IDs ────────────────────────────────
// Values per Linux `include/linux/usb/uas.h` enum (lines 9-17).
/// Command IU — sent on the Command pipe to start a SCSI command.
pub const IU_ID_COMMAND: u8 = 0x01;
/// Sense / Status IU — received on the Status pipe; carries the SCSI STATUS
/// byte and (for CHECK CONDITION) sense data.
pub const IU_ID_STATUS: u8 = 0x03;
/// Response IU — received on the Status pipe when the device rejects a
/// command without sense data (task management response, overlapped tag, …).
pub const IU_ID_RESPONSE: u8 = 0x04;
/// Task Management IU — not emitted by this driver (abort/reset out of
/// scope for the synchronous transport), defined for completeness.
pub const IU_ID_TASK_MGMT: u8 = 0x05;
/// Read Ready IU — device signals the host may start the data-in phase
/// (non-streams mode only).
pub const IU_ID_READ_READY: u8 = 0x06;
/// Write Ready IU — device signals the host may start the data-out phase
/// (non-streams mode only).
pub const IU_ID_WRITE_READY: u8 = 0x07;
// ─────────────────────────── Pipe / descriptor constants ────────────────
/// Pipe Usage descriptor bDescriptorType (USB 2.x + 3.x UAS, §3).
pub const USB_DT_PIPE_USAGE: u8 = 0x24;
/// USB configuration descriptor type (for raw GET_DESCRIPTOR fetches).
const USB_DT_CONFIGURATION: u8 = 0x02;
/// USB endpoint descriptor type.
const USB_DT_ENDPOINT: u8 = 0x05;
/// Maximum concurrent commands the UAS protocol allows (Linux `MAX_CMNDS`).
pub const MAX_CMNDS: usize = 256;
/// Standard CLEAR_FEATURE selector for ENDPOINT_HALT (mirrors BOT).
const FEATURE_ENDPOINT_HALT: u16 = 0;
/// Command IU fixed size (Linux `sizeof(struct command_iu)`).
const COMMAND_IU_SIZE: usize = 32;
/// Response IU size (Linux `sizeof(struct response_iu)`).
const RESPONSE_IU_SIZE: usize = 8;
/// Sense IU header size (the fixed leading fields before variable sense data).
/// Linux `struct sense_iu` layout: iu_id(1) rsvd1(1) tag(2) status_qual(2)
/// status(1) rsvd7(7) len(2) = 16 bytes, followed by sense data.
const SENSE_IU_HEADER_SIZE: usize = 16;
/// Buffer for the Status pipe. Big enough for a Sense IU header plus a
/// generous sense payload (SCSI sense data is typically 18-252 bytes).
const STATUS_BUF_SIZE: usize = SENSE_IU_HEADER_SIZE + 252;
// ──────────────────────────────── IU codecs ─────────────────────────────
//
// UAS multi-byte fields are big-endian on the wire (Linux uses `__be16` /
// `cpu_to_be16`). Rather than fight `#[repr(packed)]` field-access hazards,
// IUs are encoded into / decoded from byte slices explicitly. This mirrors
// the wire layout exactly and is unit-tested below.
/// Encode a Command IU into the given buffer.
///
/// `cdb` is truncated/padded to the 16-byte inline CDB field. CDBs longer
/// than 16 bytes (which require the Additional CDB field) are not used by
/// this driver's SCSI layer, so they are rejected up front by the caller
/// (`Scsi::command_buffer` is 16 bytes).
///
/// Layout (Linux `struct command_iu`, `uas.h:36-47`):
/// ```text
/// offset field
/// 0 iu_id (0x01)
/// 1 reserved
/// 2..3 tag (big-endian)
/// 4 prio_attr (priority << 5 | task_attribute; SIMPLE_TAG = 0)
/// 5 reserved
/// 6 len (additional-CDB length / 4, rounded up; 0 here)
/// 7 reserved
/// 8..15 lun (8-byte SCSI LUN, single-level: [0, lun, 0,0,0,0,0,0])
/// 16..31 cdb (16-byte SCSI CDB)
/// ```
fn encode_command_iu(out: &mut [u8; COMMAND_IU_SIZE], tag: u16, lun: u8, cdb: &[u8]) {
out.fill(0);
out[0] = IU_ID_COMMAND;
// out[1] reserved = 0
let tb = tag.to_be_bytes();
out[2] = tb[0];
out[3] = tb[1];
// out[4] prio_attr = 0 (UAS_SIMPLE_TAG, no priority)
// out[5] reserved
// out[6] len = 0 (no additional CDB)
// out[7] reserved
// Single-level LUN: byte 0 addressing method = 0 (peripheral), byte 1 = LUN.
// Matches Linux int_to_scsilun() for LUN < 0x100.
out[8] = 0;
out[9] = lun;
// out[10..16] = 0
let copy = cdb.len().min(COMMAND_IU_SIZE - 16);
out[16..16 + copy].copy_from_slice(&cdb[..copy]);
}
/// SCSI status byte offsets within every Status-pipe IU.
const fn status_iu_tag(buf: &[u8]) -> u16 {
u16::from_be_bytes([buf[2], buf[3]])
}
/// Read the SCSI STATUS byte out of a Sense IU.
///
/// Per `struct sense_iu`: status lives at offset 7.
fn sense_iu_status(buf: &[u8]) -> u8 {
buf.get(7).copied().unwrap_or(0)
}
/// Read the response code out of a Response IU.
///
/// Per `struct response_iu`: add_response_info[0..3] at offsets 4..6,
/// response_code at offset 7.
fn response_iu_code(buf: &[u8]) -> u8 {
buf.get(7).copied().unwrap_or(0)
}
// ───────────────────────────── Endpoint discovery ───────────────────────
//
// Two-tier detection, matching Linux `uas-detect.h:uas_find_endpoints()`:
//
// 1. Primary — fetch the raw Configuration descriptor and parse Pipe Usage
// descriptors (0x24) that follow each Endpoint descriptor (0x05). The
// bPipeID field authoritatively labels each endpoint's role.
//
// 2. Fallback — if the raw fetch is unavailable or no Pipe Usage
// descriptors are present, assign by the canonical UAS endpoint order
// mandated by the UAS spec §3.1 (Command, Status, Data-in, Data-out)
// combined with endpoint direction. The endpoint NUMBERS always come
// from `EndpDesc.address`, never hardcoded.
/// The four UAS pipe roles, holding the 1-based endpoint number (`address &
/// 0x0F`) for each role. Matches the `eps[4]` array in Linux
/// `uas_find_endpoints()` indexed by `pipe_id - 1`.
#[derive(Clone, Copy, Debug)]
struct UasPipes {
/// Pipe ID 1 — Command (BULK OUT).
cmd: u8,
/// Pipe ID 2 — Status (BULK IN).
status: u8,
/// Pipe ID 3 — Data-in (BULK IN).
data_in: u8,
/// Pipe ID 4 — Data-out (BULK OUT).
data_out: u8,
}
/// Raw descriptor fetch + Pipe Usage parse. Returns `None` if the device
/// does not expose Pipe Usage descriptors (e.g. fetch fails or the layout is
/// unexpected); the caller then falls back to canonical ordering.
fn pipes_from_raw_descriptor(
handle: &XhciClientHandle,
configuration_value: u8,
interface_number: u8,
) -> Option<UasPipes> {
// GET_DESCRIPTOR(CONFIGURATION): the device returns the full configuration
// tree (config + interface + endpoint + class-specific descriptors).
// Allocate generously; the device truncates to wTotalLength.
let mut buf = [0u8; 512];
let res = handle.get_descriptor(
PortReqRecipient::Device,
USB_DT_CONFIGURATION,
configuration_value,
0,
&mut buf,
);
let n = match res {
Ok(()) => {
// The control transfer fills the buffer; find the real length by
// reading wTotalLength at offset 2 (little-endian).
if buf.len() < 4 {
return None;
}
let wtotal = u16::from_le_bytes([buf[2], buf[3]]) as usize;
wtotal.min(buf.len())
}
Err(_) => return None,
};
let desc = &buf[..n];
// Walk the descriptor chain: each entry is bLength, bDescriptorType, …
// Track the most-recently-seen endpoint address; when a Pipe Usage (0x24,
// bLength 4) follows, map its bPipeID to that address.
let mut last_ep_addr: Option<u8> = None;
// pipe_id 1..=4 → endpoint number; 0 means "unassigned".
let mut by_role: [u8; 4] = [0; 0 + 4]; // indices 0..3 for pipe_id 1..4
let mut i = 0usize;
while i + 2 <= desc.len() {
let b_len = desc[i] as usize;
let b_ty = desc[i + 1];
if b_len < 2 {
break; // malformed descriptor chain
}
if i + b_len > desc.len() {
break;
}
match b_ty {
USB_DT_ENDPOINT => {
// bEndpointAddress is at offset 2 of the endpoint descriptor.
last_ep_addr = Some(desc[i + 2] & 0x0F);
}
USB_DT_PIPE_USAGE if b_len >= 4 => {
// bPipeID is at offset 2; Reserved at offset 3.
let pipe_id = desc[i + 2];
if (1..=4).contains(&pipe_id) {
if let Some(ep_addr) = last_ep_addr {
by_role[(pipe_id - 1) as usize] = ep_addr;
}
}
}
_ => {}
}
i += b_len;
}
let _ = interface_number; // parsed for completeness; role mapping is by endpoint.
// All four pipes must be identified for the authoritative path.
if by_role.iter().all(|&n| n != 0) {
Some(UasPipes {
cmd: by_role[0],
status: by_role[1],
data_in: by_role[2],
data_out: by_role[3],
})
} else {
None
}
}
/// Fallback: assign the four UAS roles from the parsed interface descriptor
/// using endpoint direction and the canonical UAS ordering mandated by the
/// spec (Command, Status, Data-in, Data-out). Endpoint numbers come from
/// `EndpDesc.address`.
///
/// Returns `None` unless the interface has exactly two bulk-IN and two
/// bulk-OUT endpoints — the UAS-required topology.
fn pipes_from_canonical_order(if_desc: &IfDesc) -> Option<UasPipes> {
let mut bulk_in: Vec<u8> = Vec::with_capacity(2);
let mut bulk_out: Vec<u8> = Vec::with_capacity(2);
for ep in if_desc.endpoints.iter() {
if !ep.is_bulk() {
continue;
}
// Endpoint number = address & 0x0F (USB 2.0 §9.6.6).
let num = ep.address & 0x0F;
match ep.direction() {
EndpDirection::In => bulk_in.push(num),
EndpDirection::Out => bulk_out.push(num),
EndpDirection::Bidirectional => {} // not valid for bulk-UAS
}
}
if bulk_in.len() != 2 || bulk_out.len() != 2 {
return None;
}
// Canonical UAS order: Status is the first IN endpoint, Data-in the
// second; Command is the first OUT endpoint, Data-out the second.
// (Linux's host-side descriptor walk sees them in this order; real UAS
// devices follow it. The primary Pipe-Usage path above overrides this
// whenever descriptors are available.)
Some(UasPipes {
cmd: bulk_out[0],
status: bulk_in[0],
data_in: bulk_in[1],
data_out: bulk_out[1],
})
}
/// Discover the four UAS pipe endpoint numbers. Pipe Usage (raw descriptor)
/// is authoritative; canonical order is the documented fallback.
fn discover_pipes(
handle: &XhciClientHandle,
conf_desc: &ConfDesc,
if_desc: &IfDesc,
) -> Result<UasPipes, ProtocolError> {
if let Some(pipes) = pipes_from_raw_descriptor(handle, conf_desc.configuration_value, if_desc.number) {
log::info!(
"usbscsid/uas: endpoint roles from Pipe Usage descriptors: {:?}",
pipes
);
return Ok(pipes);
}
match pipes_from_canonical_order(if_desc) {
Some(pipes) => {
log::info!(
"usbscsid/uas: Pipe Usage descriptors unavailable; using canonical endpoint order: {:?}",
pipes
);
Ok(pipes)
}
None => Err(ProtocolError::ProtocolError(
"UAS interface does not expose 2 BULK IN + 2 BULK OUT endpoints",
)),
}
}
// ──────────────────────────── The transport ─────────────────────────────
/// UAS transport implementing the [`Protocol`] trait.
pub struct UasTransport<'a> {
handle: &'a XhciClientHandle,
cmd: XhciEndpHandle,
status: XhciEndpHandle,
data_in: XhciEndpHandle,
data_out: XhciEndpHandle,
/// True when the data/status endpoints are stream-capable (USB 3.x with
/// a SuperSpeed Companion descriptor advertising streams). Drives the
/// streams vs. ready-IU data-phase handshake.
use_streams: bool,
/// Negotiated queue depth: `MAX_CMNDS` with streams, 1 without. The
/// transport is synchronous so only one command is ever outstanding, but
/// `qdepth` bounds the legal tag range.
qdepth: u16,
/// 1-based task tag for the next command. Wraps inside `1..=qdepth`,
/// matching Linux's tag allocator.
current_tag: u16,
current_lun: u8,
/// Cached endpoint numbers, for CLEAR_FEATURE(ENDPOINT_HALT) recovery.
cmd_num: u8,
status_num: u8,
data_in_num: u8,
data_out_num: u8,
/// Interface number, for future task-management / reset requests.
interface_num: u8,
}
impl<'a> UasTransport<'a> {
/// Initialise the UAS transport.
///
/// Opens the four bulk pipes discovered from the interface descriptor
/// and selects streams vs. ready-IU mode based on whether the endpoints
/// advertise stream capability (`EndpDesc::log_max_streams`).
pub fn init(
handle: &'a XhciClientHandle,
conf_desc: &ConfDesc,
if_desc: &IfDesc,
) -> Result<Self, ProtocolError> {
let pipes = discover_pipes(handle, conf_desc, if_desc)?;
// Streams are negotiated only when BOTH data-direction endpoints that
// carry per-command traffic (status + data-in + data-out) advertise a
// non-zero log_max_streams. The command pipe is never stream-capable
// (it carries one IU at a time). Mirrors Linux's stream probe in
// `uas_probe()` → `usb_alloc_streams()`.
let use_streams = if_desc
.endpoints
.iter()
.filter(|ep| ep.is_bulk())
.filter(|ep| ep.direction() != EndpDirection::Out || ep.address != pipes.cmd)
.filter_map(|ep| ep.log_max_streams())
.take(3)
.all(|log| u8::from(log) > 0);
let qdepth = if use_streams {
MAX_CMNDS as u16
} else {
1
};
log::info!(
"usbscsid/uas: initialised (streams={}, qdepth={})",
use_streams,
qdepth
);
Ok(Self {
cmd: handle.open_endpoint(pipes.cmd)?,
status: handle.open_endpoint(pipes.status)?,
data_in: handle.open_endpoint(pipes.data_in)?,
data_out: handle.open_endpoint(pipes.data_out)?,
handle,
use_streams,
qdepth,
current_tag: 0,
current_lun: 0,
cmd_num: pipes.cmd,
status_num: pipes.status,
data_in_num: pipes.data_in,
data_out_num: pipes.data_out,
interface_num: if_desc.number,
})
}
/// Allocate the next 1-based task tag, wrapping inside `1..=qdepth`.
/// Matches Linux `uas_queuecommand_lck:679`.
fn alloc_tag(&mut self) -> u16 {
self.current_tag = if self.current_tag >= self.qdepth {
1
} else {
self.current_tag + 1
};
self.current_tag
}
/// Reset a halted endpoint and clear ENDPOINT_HALT on the device, mirroring
/// BOT's `clear_stall_*` helpers. Used for error recovery so the next
/// command has a clean pipe.
///
/// This is a free function (not `&mut self`) so callers can pass a
/// borrowed `&mut self.cmd` without aliasing the `&mut self` borrow.
fn clear_stall(
handle: &XhciClientHandle,
ep: &mut XhciEndpHandle,
ep_num: u8,
) -> Result<(), ProtocolError> {
if ep.status()? == EndpointStatus::Halted {
ep.reset(true)?;
handle.clear_feature(
PortReqRecipient::Endpoint,
u16::from(ep_num),
FEATURE_ENDPOINT_HALT,
)?;
}
Ok(())
}
/// Classify a non-success transfer status into a [`ProtocolError`].
fn data_transfer_err(kind: PortTransferStatusKind, what: &'static str) -> ProtocolError {
match kind {
PortTransferStatusKind::Stalled => {
log::warn!("usbscsid/uas: {what} endpoint stalled");
ProtocolError::EndpointStalled(what)
}
PortTransferStatusKind::Error => {
log::warn!("usbscsid/uas: {what} transfer error");
ProtocolError::ProtocolError("uas data transfer error")
}
PortTransferStatusKind::Resource => {
log::warn!("usbscsid/uas: {what} host-controller resource exhausted");
ProtocolError::ProtocolError("uas data transfer resource error")
}
PortTransferStatusKind::Unknown => {
log::warn!("usbscsid/uas: {what} unknown transfer status");
ProtocolError::ProtocolError("uas data transfer unknown status")
}
// Success / ShortPacket are not errors here.
_ => ProtocolError::ProtocolError("uas data transfer unexpected status"),
}
}
/// Run the data phase for one command. In streams mode the transfer
/// carries the command's stream ID; in non-streams mode it is flat
/// (stream 0) and is gated by a prior READ/WRITE_READY IU.
fn run_data_phase(
&mut self,
data: DeviceReqData,
tag: u16,
) -> Result<Option<u32>, ProtocolError> {
match data {
DeviceReqData::In(buf) if !buf.is_empty() => {
let st = if self.use_streams {
self.data_in.transfer_read_sid(buf, tag)?
} else {
self.data_in.transfer_read(buf)?
};
Self::require_ok_or_short(st, "uas data-in")?;
Ok(Some(st.bytes_transferred))
}
DeviceReqData::Out(buf) if !buf.is_empty() => {
let st = if self.use_streams {
self.data_out.transfer_write_sid(buf, tag)?
} else {
self.data_out.transfer_write(buf)?
};
Self::require_ok_or_short(st, "uas data-out")?;
Ok(Some(st.bytes_transferred))
}
_ => Ok(None),
}
}
/// `Ok` for Success/ShortPacket; mapped error otherwise. Short packets
/// are legitimate for the final data transfer (the device may send fewer
/// bytes than requested).
fn require_ok_or_short(
st: PortTransferStatus,
what: &'static str,
) -> Result<(), ProtocolError> {
match st.kind {
PortTransferStatusKind::Success | PortTransferStatusKind::ShortPacket => Ok(()),
other => Err(Self::data_transfer_err(other, what)),
}
}
/// Read the next IU from the Status pipe. In streams mode it reads the
/// specific command's stream; in non-streams mode it reads the flat pipe
/// (the only place READ/WRITE_READY and final Sense/Response IUs arrive).
fn read_status_iu(&mut self, buf: &mut [u8], tag: u16) -> Result<(), ProtocolError> {
let st = if self.use_streams {
self.status.transfer_read_sid(buf, tag)?
} else {
self.status.transfer_read(buf)?
};
match st.kind {
PortTransferStatusKind::Success | PortTransferStatusKind::ShortPacket => Ok(()),
PortTransferStatusKind::Stalled => {
log::warn!("usbscsid/uas: status pipe stalled");
Err(ProtocolError::EndpointStalled("uas status pipe"))
}
other => Err(Self::data_transfer_err(other, "uas status pipe")),
}
}
/// Decode the final IU (Sense or Response) into a [`SendCommandStatus`].
fn evaluate_status_iu(buf: &[u8], _expected_tag: u16) -> Result<SendCommandStatus, ProtocolError> {
if buf.is_empty() {
return Err(ProtocolError::ProtocolError("uas status IU empty"));
}
match buf[0] {
IU_ID_STATUS => {
// Sense IU: SCSI status byte at offset 7. Status 0x00 = GOOD;
// anything else is a command-level failure (CHECK CONDITION
// 0x02, etc.). Sense data itself (offset 16+) is not yet
// surfaced — the SCSI layer requests it explicitly via
// REQUEST SENSE on CHECK CONDITION.
let status = sense_iu_status(buf);
Ok(SendCommandStatus {
kind: if status == 0x00 {
SendCommandStatusKind::Success
} else {
log::warn!(
"usbscsid/uas: Sense IU status=0x{:02X} (tag={})",
status,
status_iu_tag(buf)
);
SendCommandStatusKind::Failed
},
residue: None,
})
}
IU_ID_RESPONSE => {
// Response IU: response_code at offset 7. RC_TMF_COMPLETE
// (0x00) is the only success code; anything else is an
// explicit device-side rejection.
let code = response_iu_code(buf);
Ok(SendCommandStatus {
kind: if code == 0x00 {
SendCommandStatusKind::Success
} else {
log::warn!(
"usbscsid/uas: Response IU code=0x{:02X} (tag={})",
code,
status_iu_tag(buf)
);
SendCommandStatusKind::Failed
},
residue: None,
})
}
other => {
log::warn!("usbscsid/uas: unexpected IU id 0x{:02X} on status pipe", other);
Err(ProtocolError::ProtocolError(
"uas unexpected IU on status pipe",
))
}
}
}
}
impl<'a> Protocol for UasTransport<'a> {
fn send_command(
&mut self,
command: &[u8],
data: DeviceReqData,
) -> Result<SendCommandStatus, ProtocolError> {
// Reject oversized CDBs up front. The SCSI layer uses a 16-byte
// command buffer; CDBs up to 16 bytes fit inline. Larger CDBs would
// need the Additional CDB field (Command IU + extra bytes) which this
// synchronous transport does not issue.
if command.len() > 16 {
return Err(ProtocolError::TooLargeCommandBlock(command.len()));
}
let tag = self.alloc_tag();
let lun = self.current_lun;
// ── 1. Build & send the Command IU on the Command pipe ──────────
let mut cmd_iu = [0u8; COMMAND_IU_SIZE];
encode_command_iu(&mut cmd_iu, tag, lun, command);
let cmd_st = self.cmd.transfer_write(&cmd_iu)?;
match cmd_st.kind {
PortTransferStatusKind::Success | PortTransferStatusKind::ShortPacket => {}
PortTransferStatusKind::Stalled => {
log::warn!("usbscsid/uas: command pipe stalled sending Command IU");
Self::clear_stall(self.handle, &mut self.cmd, self.cmd_num)?;
return Err(ProtocolError::EndpointStalled("uas command pipe"));
}
other => return Err(Self::data_transfer_err(other, "uas command pipe")),
}
// ── 2. Data phase ───────────────────────────────────────────────
if self.use_streams {
// Streams mode: submit the data transfer immediately on the
// command's stream. The device-side stream context matches it to
// this command via the tag/stream-ID equivalence.
self.run_data_phase(data, tag)?;
} else {
// Non-streams mode: the device gates the data phase with a
// READ_READY (data-in) or WRITE_READY (data-out) IU on the Status
// pipe. Wait for it, then transfer. `data` is inspected by shared
// reference only here so it can be moved whole into the data
// phase afterwards.
let expects_read_ready = matches!(data, DeviceReqData::In(_)) && !data.is_empty();
let expects_write_ready = matches!(data, DeviceReqData::Out(_)) && !data.is_empty();
if expects_read_ready || expects_write_ready {
let mut rdy = [0u8; 4];
self.read_status_iu(&mut rdy, tag)?;
let want = if expects_read_ready {
IU_ID_READ_READY
} else {
IU_ID_WRITE_READY
};
if rdy[0] != want {
log::warn!(
"usbscsid/uas: expected ready IU 0x{:02X}, got 0x{:02X}",
want,
rdy[0]
);
return Err(ProtocolError::ProtocolError(
"uas expected READ/WRITE_READY before data phase",
));
}
}
self.run_data_phase(data, tag)?;
}
// ── 3. Read the final Status IU (Sense or Response) ─────────────
let mut status_buf = [0u8; STATUS_BUF_SIZE];
self.read_status_iu(&mut status_buf, tag)?;
Self::evaluate_status_iu(&status_buf, tag)
}
fn max_lun(&self) -> u8 {
// UAS does not use the BOT Get Max LUN control request. The LUN
// count is discovered via REPORT_LUNS (handled by the SCSI layer).
// Report 0 here so single-LUN devices enumerate; the SCSI layer's
// REPORT_LUNS path is the authoritative source for multi-LUN.
0
}
fn set_lun(&mut self, lun: u8) {
self.current_lun = lun;
}
}
// ──────────────────────────────── Tests ─────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
/// Command IU must be exactly the 32 bytes the UAS wire format mandates.
#[test]
fn command_iu_buffer_is_32_bytes() {
assert_eq!(COMMAND_IU_SIZE, 32);
}
/// Command IU byte layout: every field lands at the spec-mandated offset.
#[test]
fn encode_command_iu_layout() {
let mut buf = [0u8; COMMAND_IU_SIZE];
let cdb = [
0x28, // READ(10)
0x00, 0x00, 0x00, 0x0A, // LBA
0x00, // group
0x00, 0x04, // transfer length
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // control + padding
];
encode_command_iu(&mut buf, 0x1234, 0x05, &cdb);
// iu_id
assert_eq!(buf[0], IU_ID_COMMAND);
// reserved
assert_eq!(buf[1], 0x00);
// tag big-endian
assert_eq!([buf[2], buf[3]], [0x12, 0x34]);
// prio_attr (SIMPLE_TAG, no priority)
assert_eq!(buf[4], 0x00);
// additional-CDB length
assert_eq!(buf[6], 0x00);
// LUN: single-level, [0, lun, …]
assert_eq!(buf[8], 0x00);
assert_eq!(buf[9], 0x05);
assert_eq!(&buf[10..16], &[0u8; 6]);
// CDB
assert_eq!(&buf[16..32], &cdb[..]);
}
/// The tag must round-trip through the big-endian wire encoding.
#[test]
fn command_iu_tag_round_trip() {
let mut buf = [0u8; COMMAND_IU_SIZE];
for tag in [0x0001u16, 0x00FF, 0x0100, 0xFFFE, 0xFFFF] {
encode_command_iu(&mut buf, tag, 0, &[0u8; 16]);
assert_eq!(status_iu_tag(&buf), tag, "tag {tag:#06x} did not round-trip");
}
}
/// A CDB shorter than 16 bytes is copied verbatim and zero-padded.
#[test]
fn encode_command_iu_short_cdb_is_padded() {
let mut buf = [0u8; COMMAND_IU_SIZE];
let cdb = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; // TEST UNIT READY (6)
encode_command_iu(&mut buf, 1, 0, &cdb);
assert_eq!(&buf[16..22], &cdb[..]);
assert_eq!(&buf[22..32], &[0u8; 10]);
}
/// A CDB longer than 16 bytes is truncated to the inline field. (The
/// SCSI layer never issues such CDBs; this guards against a pathological
/// caller corrupting memory.)
#[test]
fn encode_command_iu_truncates_oversized_cdb() {
let mut buf = [0u8; COMMAND_IU_SIZE];
let cdb = [0xABu8; 20];
encode_command_iu(&mut buf, 1, 0, &cdb);
assert_eq!(&buf[16..32], &[0xABu8; 16]);
}
/// IU ID constants must match Linux `include/linux/usb/uas.h`.
#[test]
fn iu_id_constants_match_linux() {
assert_eq!(IU_ID_COMMAND, 0x01);
assert_eq!(IU_ID_STATUS, 0x03);
assert_eq!(IU_ID_RESPONSE, 0x04);
assert_eq!(IU_ID_TASK_MGMT, 0x05);
assert_eq!(IU_ID_READ_READY, 0x06);
assert_eq!(IU_ID_WRITE_READY, 0x07);
}
/// A successful Sense IU (status 0x00) decodes to Success.
#[test]
fn sense_iu_good_status_decodes_to_success() {
let mut buf = [0u8; STATUS_BUF_SIZE];
buf[0] = IU_ID_STATUS;
buf[2] = 0x00;
buf[3] = 0x07; // tag 7
buf[7] = 0x00; // GOOD status
let st = UasTransport::evaluate_status_iu(&buf, 7).unwrap();
assert_eq!(st.kind, SendCommandStatusKind::Success);
}
/// A CHECK CONDITION (status 0x02) Sense IU decodes to Failed.
#[test]
fn sense_iu_check_condition_decodes_to_failed() {
let mut buf = [0u8; STATUS_BUF_SIZE];
buf[0] = IU_ID_STATUS;
buf[7] = 0x02; // CHECK CONDITION
let st = UasTransport::evaluate_status_iu(&buf, 1).unwrap();
assert_eq!(st.kind, SendCommandStatusKind::Failed);
}
/// A Response IU with RC_TMF_COMPLETE (0x00) decodes to Success.
#[test]
fn response_iu_complete_decodes_to_success() {
let mut buf = [0u8; RESPONSE_IU_SIZE];
buf[0] = IU_ID_RESPONSE;
buf[7] = 0x00; // RC_TMF_COMPLETE
let st = UasTransport::evaluate_status_iu(&buf, 1).unwrap();
assert_eq!(st.kind, SendCommandStatusKind::Success);
}
/// A Response IU with an error code decodes to Failed.
#[test]
fn response_iu_error_decodes_to_failed() {
let mut buf = [0u8; RESPONSE_IU_SIZE];
buf[0] = IU_ID_RESPONSE;
buf[7] = 0x05; // RC_TMF_FAILED
let st = UasTransport::evaluate_status_iu(&buf, 1).unwrap();
assert_eq!(st.kind, SendCommandStatusKind::Failed);
}
/// An unknown IU ID on the status pipe is an error, never a silent pass.
#[test]
fn unknown_iu_is_an_error() {
let mut buf = [0u8; STATUS_BUF_SIZE];
buf[0] = 0xFF;
assert!(UasTransport::evaluate_status_iu(&buf, 1).is_err());
}
/// An empty status buffer is an error.
#[test]
fn empty_status_is_an_error() {
let buf: [u8; 0] = [];
assert!(UasTransport::evaluate_status_iu(&buf, 1).is_err());
}
/// Canonical-order discovery assigns the four roles from two IN + two OUT
/// bulk endpoints, reading numbers from the descriptor addresses.
#[test]
fn canonical_order_assigns_all_four_roles() {
// Build a synthetic IfDesc with 4 bulk endpoints at addresses
// 0x01(OUT), 0x82(IN), 0x83(IN), 0x04(OUT).
use smallvec::SmallVec;
use xhcid_interface::EndpDesc;
let mk = |address: u8| EndpDesc {
kind: USB_DT_ENDPOINT,
address,
attributes: 0x02, // bulk
max_packet_size: 1024,
interval: 0,
ssc: None,
sspc: None,
};
let endpoints: SmallVec<[EndpDesc; 4]> = smallvec::smallvec![mk(0x01), mk(0x82), mk(0x83), mk(0x04)];
let if_desc = IfDesc {
kind: 0x04,
number: 0,
alternate_setting: 0,
class: 0x08,
sub_class: 0x06,
protocol: 0x62,
interface_str: None,
endpoints,
hid_descs: SmallVec::new(),
};
let pipes = pipes_from_canonical_order(&if_desc).expect("4 bulk endpoints");
// Canonical order: cmd=first OUT (0x01), status=first IN (0x02),
// data_in=second IN (0x03), data_out=second OUT (0x04).
assert_eq!(pipes.cmd, 0x01);
assert_eq!(pipes.status, 0x02);
assert_eq!(pipes.data_in, 0x03);
assert_eq!(pipes.data_out, 0x04);
}
/// Canonical-order discovery rejects topologies that are not exactly
/// 2 IN + 2 OUT bulk endpoints.
#[test]
fn canonical_order_rejects_wrong_endpoint_count() {
use smallvec::SmallVec;
use xhcid_interface::EndpDesc;
let mk = |address: u8| EndpDesc {
kind: USB_DT_ENDPOINT,
address,
attributes: 0x02,
max_packet_size: 1024,
interval: 0,
ssc: None,
sspc: None,
};
// Only 3 bulk endpoints → not a valid UAS topology.
let endpoints: SmallVec<[EndpDesc; 4]> = smallvec::smallvec![mk(0x01), mk(0x82), mk(0x83)];
let if_desc = IfDesc {
kind: 0x04,
number: 0,
alternate_setting: 0,
class: 0x08,
sub_class: 0x06,
protocol: 0x62,
interface_str: None,
endpoints,
hid_descs: SmallVec::new(),
};
assert!(pipes_from_canonical_order(&if_desc).is_none());
}
/// Tag allocation is 1-based and wraps inside the queue depth.
#[test]
fn tag_allocation_is_1_based_and_wraps() {
// qdepth=1: every alloc yields tag 1.
// We can't construct UasTransport without a live handle, so test the
// wrapping arithmetic directly against the documented invariant.
let qdepth = 1u16;
let mut tag = 0u16;
for _ in 0..5 {
tag = if tag >= qdepth { 1 } else { tag + 1 };
assert_eq!(tag, 1);
}
// qdepth=4: sequence is 1,2,3,4,1,2,…
let qdepth = 4u16;
let mut tag = 0u16;
let seq: Vec<u16> = (0..8)
.map(|_| {
tag = if tag >= qdepth { 1 } else { tag + 1 };
tag
})
.collect();
assert_eq!(seq, vec![1, 2, 3, 4, 1, 2, 3, 4]);
}
/// Pipe Usage descriptor parsing from a raw configuration descriptor.
#[test]
fn pipe_usage_parsed_from_raw_descriptor() {
// Hand-build a minimal config descriptor: config + interface + four
// (endpoint + pipe-usage) pairs.
let mut raw: Vec<u8> = Vec::new();
// Configuration descriptor (9 bytes).
raw.extend_from_slice(&[
9, // bLength
USB_DT_CONFIGURATION, // bDescriptorType
0, 0, // wTotalLength (placeholder)
1, // bNumInterfaces
1, // bConfigurationValue
0, // iConfiguration
0x80, // bmAttributes
50, // bMaxPower
]);
// Interface descriptor (9 bytes).
raw.extend_from_slice(&[
9, // bLength
0x04, // bDescriptorType (INTERFACE)
0, // bInterfaceNumber
0, // bAlternateSetting
4, // bNumEndpoints
0x08, // bInterfaceClass
0x06, // bInterfaceSubClass
0x62, // bInterfaceProtocol
0, // iInterface
]);
// Helper to append an endpoint + its pipe usage.
let ep_and_pipe = |raw: &mut Vec<u8>, addr: u8, pipe_id: u8| {
raw.extend_from_slice(&[
7, // bLength
USB_DT_ENDPOINT, // bDescriptorType
addr, // bEndpointAddress
0x02, // bmAttributes (bulk)
0x00, 0x04, // wMaxPacketSize
0, // bInterval
]);
raw.extend_from_slice(&[
4, // bLength
USB_DT_PIPE_USAGE, // bDescriptorType
pipe_id, // bPipeID
0, // Reserved
]);
};
ep_and_pipe(&mut raw, 0x01, 1); // Command OUT
ep_and_pipe(&mut raw, 0x82, 2); // Status IN
ep_and_pipe(&mut raw, 0x83, 3); // Data-in IN
ep_and_pipe(&mut raw, 0x04, 4); // Data-out OUT
// Reproduce the parse loop (no live handle; we exercise the parser
// body directly by inlining the walk).
let desc = &raw[..];
let mut last_ep_addr: Option<u8> = None;
let mut by_role = [0u8; 4];
let mut i = 0usize;
while i + 2 <= desc.len() {
let b_len = desc[i] as usize;
let b_ty = desc[i + 1];
if b_len < 2 || i + b_len > desc.len() {
break;
}
match b_ty {
USB_DT_ENDPOINT => last_ep_addr = Some(desc[i + 2] & 0x0F),
USB_DT_PIPE_USAGE if b_len >= 4 => {
let pipe_id = desc[i + 2];
if (1..=4).contains(&pipe_id) {
if let Some(ep_addr) = last_ep_addr {
by_role[(pipe_id - 1) as usize] = ep_addr;
}
}
}
_ => {}
}
i += b_len;
}
assert_eq!(by_role, [0x01, 0x02, 0x03, 0x04]);
}
}