diff --git a/Cargo.lock b/Cargo.lock index cc0b4e744..d4c4a4f7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2889,6 +2889,7 @@ dependencies = [ "ironrdp-dvc", "ironrdp-pdu", "ironrdp-str", + "ironrdp-usb", ] [[package]] @@ -2959,9 +2960,11 @@ dependencies = [ "ironrdp-graphics", "ironrdp-nscodec", "ironrdp-pdu", + "ironrdp-rdpeusb", "ironrdp-rdpsnd", "ironrdp-svc", "ironrdp-tokio", + "ironrdp-usb", "qoicoubeh", "rand 0.9.4", "rayon", @@ -3047,6 +3050,7 @@ dependencies = [ "ironrdp-session", "ironrdp-str", "ironrdp-svc", + "ironrdp-usb", "openh264", "paste", "png", @@ -3109,6 +3113,10 @@ dependencies = [ "url", ] +[[package]] +name = "ironrdp-usb" +version = "0.1.0" + [[package]] name = "ironrdp-viewer" version = "0.1.0" diff --git a/crates/ironrdp-dvc/src/server.rs b/crates/ironrdp-dvc/src/server.rs index b34bd58fc..fdda4a49b 100644 --- a/crates/ironrdp-dvc/src/server.rs +++ b/crates/ironrdp-dvc/src/server.rs @@ -1,8 +1,10 @@ use alloc::boxed::Box; use alloc::collections::BTreeMap; +use alloc::collections::btree_map::Entry; use alloc::vec::Vec; use core::any::TypeId; use core::fmt; +use core::sync::atomic::AtomicUsize; use ironrdp_core::{Decode as _, DecodeResult, ReadCursor, impl_as_any, invalid_field_err}; use ironrdp_pdu::{self as pdu, decode_err, encode_err, pdu_other_err}; @@ -42,6 +44,54 @@ impl Drop for DynamicChannel { } } +/// A reserved dynamic channel ID awaiting a channel processor. +/// +/// The ID is provisional until [`Self::create`] is called. Reserved IDs increase +/// monotonically and are never reused for the lifetime of the server. Dropping a +/// reservation without creating the channel permanently skips its ID. +#[derive(Debug)] +pub struct DynamicChannelReservation { + server_id: usize, + channel_id: u32, +} + +impl DynamicChannelReservation { + /// Returns the reserved dynamic channel ID. + pub fn channel_id(&self) -> u32 { + self.channel_id + } + + /// Registers the channel and returns its DVC Create Request message. + /// + /// # Panics + /// + /// Panics if reservation finalized on a different DrdynvcServer than the one that minted it. + pub fn create(self, server: &mut DrdynvcServer, channel: T) -> PduResult + where + T: DvcServerProcessor + 'static, + { + assert_eq!( + self.server_id, server.id, + "reservation finalized on a different DrdynvcServer than the one that minted it" + ); + let channel_name = channel.channel_name().into(); + let channel_id = self.channel_id(); + server + .dynamic_channels + .insert_reserved(channel_id, channel, ChannelState::Creation); + + // TODO: Align the TypeId lookup semantics of DrdynvcServer::create_channel and + // DrdynvcServer::with_dynamic_channel in a separate public-contract PR, then make + // DrdynvcServer::create_channel use the reservation path. + server + .type_id_to_channel_id + .entry(TypeId::of::()) + .or_insert(channel_id); + let req = DrdynvcServerPdu::Create(CreateRequestPdu::new(channel_id, channel_name)); + as_svc_msg_with_flag(req) + } +} + struct DynamicChannelAllocator { dynamic_channels: BTreeMap, next_channel_id: u32, @@ -77,13 +127,27 @@ impl DynamicChannelAllocator { where T: DvcServerProcessor + 'static, { + let channel_id = self.reserve_channel(); + self.insert_reserved(channel_id, processor, state); + channel_id + } + + fn insert_reserved(&mut self, channel_id: u32, processor: T, state: ChannelState) + where + T: DvcServerProcessor + 'static, + { + let Entry::Vacant(entry) = self.dynamic_channels.entry(channel_id) else { + unreachable!("reserved dynamic channel ID must be vacant") + }; + entry.insert(DynamicChannel::new(processor, channel_id, state)); + } + + fn reserve_channel(&mut self) -> u32 { let channel_id = self.next_channel_id; - self.dynamic_channels - .insert(channel_id, DynamicChannel::new(processor, channel_id, state)); self.next_channel_id = self .next_channel_id .checked_add(1) - .expect("dynamic channels reaches `u32::MAX`"); + .expect("dynamic channels reached `u32::MAX`"); channel_id } @@ -117,10 +181,14 @@ impl DynamicChannel { self.processor.as_any().type_id() } } + +static NEXT_SERVER_ID: AtomicUsize = AtomicUsize::new(0); + /// DRDYNVC Static Virtual Channel (the Remote Desktop Protocol: Dynamic Virtual Channel Extension) /// /// It adds support for dynamic virtual channels (DVC). pub struct DrdynvcServer { + id: usize, dynamic_channels: DynamicChannelAllocator, type_id_to_channel_id: BTreeMap, } @@ -145,6 +213,7 @@ impl DrdynvcServer { pub fn new() -> Self { Self { + id: NEXT_SERVER_ID.fetch_add(1, core::sync::atomic::Ordering::Relaxed), dynamic_channels: DynamicChannelAllocator::new(), type_id_to_channel_id: BTreeMap::new(), } @@ -169,7 +238,7 @@ impl DrdynvcServer { /// /// # Panics /// - /// Panics if the number of registered dynamic channels reaches `u32::MAX`. + /// Panics if the number of registered dynamic channels reached `u32::MAX`. #[must_use] pub fn with_dynamic_channel(mut self, channel: T) -> Self where @@ -223,17 +292,36 @@ impl DrdynvcServer { { let channel_name = channel.channel_name().into(); + // TODO: Reuse self.reserve_channel() after the TypeId lookup contract is aligned + // in a separate PR. let channel_id = self.dynamic_channels.insert_channel(channel, ChannelState::Creation); let req = DrdynvcServerPdu::Create(CreateRequestPdu::new(channel_id, channel_name)); as_svc_msg_with_flag(req) } + /// Reserves the next dynamic channel ID without creating the channel. + /// + /// Reserved IDs increase monotonically and are never reused. Dropping the returned + /// reservation without creating the channel skips its ID, preventing ABA issues. + /// + /// # Panics + /// + /// Panics if the dynamic channel ID space is exhausted. + #[must_use] + pub fn reserve_channel(&mut self) -> DynamicChannelReservation { + let channel_id = self.dynamic_channels.reserve_channel(); + DynamicChannelReservation { + channel_id, + server_id: self.id, + } + } + fn remove_by_channel_id(&mut self, id: u32) -> Option { self.dynamic_channels.remove(id).inspect(|dvc| { let type_id = dvc.processor_type_id(); // Only matters for pre-registered channels - if let alloc::collections::btree_map::Entry::Occupied(entry) = self.type_id_to_channel_id.entry(type_id) + if let Entry::Occupied(entry) = self.type_id_to_channel_id.entry(type_id) && entry.get() == &id { entry.remove(); diff --git a/crates/ironrdp-rdpeusb/Cargo.toml b/crates/ironrdp-rdpeusb/Cargo.toml index dbc7c5477..d3e58f93c 100644 --- a/crates/ironrdp-rdpeusb/Cargo.toml +++ b/crates/ironrdp-rdpeusb/Cargo.toml @@ -24,6 +24,7 @@ std = [] ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["alloc"] } # public ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9", features = ["alloc"] } # public ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.8" } # public +ironrdp-usb = { path = "../ironrdp-usb", version = "0.1" } # public ironrdp-str = { path = "../ironrdp-str", version = "0.1" } [lints] diff --git a/crates/ironrdp-rdpeusb/README.md b/crates/ironrdp-rdpeusb/README.md index 583fb2429..d004abf4b 100644 --- a/crates/ironrdp-rdpeusb/README.md +++ b/crates/ironrdp-rdpeusb/README.md @@ -3,6 +3,11 @@ Implements [Remote Desktop Protocol: USB Devices Virtual Channel Extension][spec] used to redirect USB devices from a terminal client to a terminal server. +The `usb` module translates protocol-independent operations and descriptor +semantics from `ironrdp-usb` into complete backend-facing RDPEUSB transfer +requests. It is sans-I/O and does not allocate request IDs or manage pending +request lifetimes. + [spec]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/a1004d0e-99e9-4968-894b-0b924ef2f125 This crate is part of the [IronRDP] project. diff --git a/crates/ironrdp-rdpeusb/src/client.rs b/crates/ironrdp-rdpeusb/src/client.rs index 1bed52bbb..dfdd59eef 100644 --- a/crates/ironrdp-rdpeusb/src/client.rs +++ b/crates/ironrdp-rdpeusb/src/client.rs @@ -41,7 +41,7 @@ pub trait DeviceManagerBackend: Send { pub struct UrbdrcListener { on_capability_exchanged: Option, device_man: Box, - iface_man: InterfaceAlloc, + iface_man: crate::InterfaceAlloc, } impl UrbdrcListener { @@ -49,28 +49,7 @@ impl UrbdrcListener { Self { on_capability_exchanged: Some(callback), device_man, - iface_man: InterfaceAlloc::new(), - } - } -} - -struct InterfaceAlloc { - id: u32, -} - -impl InterfaceAlloc { - #[inline] - const fn new() -> Self { - Self { id: 3 } - } - - #[inline] - const fn alloc(&mut self) -> Option { - self.id += 1; - if self.id > 0x3F_FF_FF_FF { - None - } else { - Some(InterfaceId::from_raw(self.id)) + iface_man: crate::InterfaceAlloc::default(), } } } diff --git a/crates/ironrdp-rdpeusb/src/io/mod.rs b/crates/ironrdp-rdpeusb/src/io/mod.rs index bb58e2d64..54b12e4cb 100644 --- a/crates/ironrdp-rdpeusb/src/io/mod.rs +++ b/crates/ironrdp-rdpeusb/src/io/mod.rs @@ -57,6 +57,15 @@ pub struct DeviceText { pub description: String, } +/// Wrapper for the completion of RDPEUSB request. +#[derive(Debug)] +pub enum CompletionData { + IoControl(IoControlCompletionResult), + InternalIoControl(IoControlCompletionResult), + TransferIn(TransferInCompletionResult), + TransferOut(TransferOutCompletionResult), +} + /// Completion of an I/O control request. /// /// This completes either an [`IoControlPacket`] or an [`InternalIoControlPacket`]. The request ID diff --git a/crates/ironrdp-rdpeusb/src/lib.rs b/crates/ironrdp-rdpeusb/src/lib.rs index 3855acb0d..d5ca09481 100644 --- a/crates/ironrdp-rdpeusb/src/lib.rs +++ b/crates/ironrdp-rdpeusb/src/lib.rs @@ -9,6 +9,7 @@ pub mod client; pub mod io; pub mod pdu; pub mod server; +pub mod usb; /// Error returned when a per-device USB interface ID conflicts with an RDPEUSB default interface. /// @@ -46,3 +47,25 @@ impl core::fmt::Display for InvalidDeviceInterfaceId { ) } } + +pub struct InterfaceAlloc { + id: u32, +} + +impl Default for InterfaceAlloc { + fn default() -> Self { + Self { id: 3 } + } +} + +impl InterfaceAlloc { + #[inline] + pub const fn alloc(&mut self) -> Option { + self.id += 1; + if self.id > 0x3F_FF_FF_FF { + None + } else { + Some(pdu::header::InterfaceId::from_raw(self.id)) + } + } +} diff --git a/crates/ironrdp-rdpeusb/src/usb.rs b/crates/ironrdp-rdpeusb/src/usb.rs new file mode 100644 index 000000000..a69c53f17 --- /dev/null +++ b/crates/ironrdp-rdpeusb/src/usb.rs @@ -0,0 +1,1317 @@ +//! Translation from protocol-independent USB semantics to RDPEUSB requests. +//! +//! This module is deliberately sans-I/O. It does not know about usbredir +//! packets, RDPEUSB request IDs, device state, completion routing, or async +//! execution. Each function returns a complete backend-facing RDPEUSB transfer +//! packet so that the TS_URB payload, URB function, transfer envelope, flags, +//! and buffer shape cannot disagree. +//! +//! The adapter belongs to RDPEUSB because it selects TS_URB forms and +//! Windows USBD conventions; the input types remain protocol-independent. + +use alloc::vec::Vec; +use core::fmt; + +use crate::{ + io::{ + CompletionData, IoControlPacket, TransferInCompletionResult, TransferInPacket, TransferOutPacket, TsUrbInKind, + TsUrbInPacket, TsUrbOutKind, TsUrbOutPacket, UrbFunction, + }, + pdu::{ + completion::ts_urb_result::{TsUrbResultPayload, TsUrbSelectConfigResult, TsUrbSelectInterfaceResult}, + usb_dev::IoctlInternalUsb, + usb_dev::ts_urb::{ + TsUrbBulkOrInterruptTransfer, TsUrbControlDescRequest, TsUrbControlFeatRequest, + TsUrbControlGetConfigRequest, TsUrbControlGetInterfaceRequest, TsUrbControlGetStatusRequest, + TsUrbControlTransfer, TsUrbControlVendorClassRequest, TsUrbGetCurrFrameNum, TsUrbIsochTransfer, + TsUrbPipeRequest, TsUrbSelectConfig, TsUrbSelectInterface, + utils::{SetupPacket as RdpeusbSetupPacket, TsUsbdInterfaceInfo, TsUsbdPipeInfo, UsbConfigDesc}, + }, + utils::{ConfigHandle, MAX_NON_DEFAULT_EP_COUNT, PipeHandle, UsbdIsoPacketDesc}, + }, +}; +use ironrdp_usb::{ + control::{GetDescriptorRequest, Recipient, RequestKind, SetupPacket, standard_request}, + descriptor::{ConfigurationDescriptorSet, InterfaceDescriptor}, + transfer::{ + DataTransferRequest, FrameNumber, IsoCompletion, IsochronousPacketCompletion, IsochronousTransferOutput, + IsochronousTransferRequest, TransferCompletion, UsbError, UsbResult, + }, + value::{Direction, InterfaceSelection, TransferType, UsbSpeed}, +}; + +// WDK USBD transfer flags. RDPEUSB carries these values unchanged in TS_URB. +const USBD_TRANSFER_DIRECTION_IN: u32 = 0x0000_0001; +const USBD_SHORT_TRANSFER_OK: u32 = 0x0000_0002; +const USBD_DEFAULT_PIPE_TRANSFER: u32 = 0x0000_0008; +const USBD_START_ISO_TRANSFER_ASAP: u32 = 0x0000_0004; + +// Windows USBD status values carried unchanged in TS_URB_RESULT_HEADER. +const USBD_STATUS_SUCCESS: u32 = 0x0000_0000; +const USBD_STATUS_STALL_PID: u32 = 0xc000_0004; +const USBD_STATUS_BABBLE_DETECTED: u32 = 0xc000_0012; +const USBD_STATUS_CANCELED: u32 = 0xc001_0000; +const USBD_STATUS_TIMEOUT: u32 = 0xc000_6000; +const USBD_STATUS_DEVICE_GONE: u32 = 0xc000_7000; + +// MS-ERREF 2.1 defines bit 31 as the HRESULT severity bit. +const HRESULT_FAILURE_BIT: u32 = 0x8000_0000; + +// A Windows URB addresses the default control endpoint with a null pipe +// handle and USBD_DEFAULT_PIPE_TRANSFER. This is a Windows URB convention, +// rather than an RDPEUSB-assigned pipe handle. +const DEFAULT_CONTROL_PIPE_HANDLE: PipeHandle = 0; + +/// A complete RDPEUSB transfer request, before request-ID allocation and I/O. +#[derive(Debug, Clone)] +pub enum TransferRequest { + /// An RDPEUSB `TRANSFER_IN_REQUEST` envelope. + In(TransferInPacket), + /// An RDPEUSB `TRANSFER_OUT_REQUEST` envelope. + Out(TransferOutPacket), +} + +/// A USB operation that cannot be represented by the requested RDPEUSB form. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum ConversionError { + InTransferHasData { + actual: usize, + }, + OutTransferLengthMismatch { + expected: usize, + actual: usize, + }, + TransferLengthNotRepresentable { + length: u32, + }, + UnsupportedDescriptorRecipient { + recipient: Recipient, + }, + StatefulStandardRequest { + request: u8, + }, + InvalidConfigurationHeaderLength { + actual: u8, + }, + InterfaceCountMismatch { + declared: u8, + actual: usize, + }, + DuplicateInterface { + interface: u8, + }, + InterfaceNotFound { + selection: InterfaceSelection, + }, + MissingInterfaceSelection { + interface: u8, + }, + TooManyPipes { + selection: InterfaceSelection, + actual: usize, + }, + TooManyActivePipes { + actual: usize, + }, + EndpointCountMismatch { + selection: InterfaceSelection, + declared: u8, + actual: usize, + }, + InvalidMaximumPacketSize { + selection: InterfaceSelection, + raw: u16, + }, + EmptyIsochronousTransfer, + IsochronousTransferLengthOverflow { + packet: usize, + }, + NoAckIsochronousIn, +} + +impl fmt::Display for ConversionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InTransferHasData { actual } => { + write!(f, "USB IN transfer carries {actual} bytes of output data") + } + Self::OutTransferLengthMismatch { expected, actual } => write!( + f, + "USB OUT transfer declares {expected} bytes but carries {actual} bytes" + ), + Self::TransferLengthNotRepresentable { length } => { + write!(f, "USB transfer length {length} does not fit in usize") + } + Self::UnsupportedDescriptorRecipient { recipient } => write!( + f, + "RDPEUSB has no typed descriptor request for USB recipient {}", + recipient.raw() + ), + Self::StatefulStandardRequest { request } => write!( + f, + "USB standard request {:#04x} requires host-controller state and cannot use a generic RDPEUSB control transfer", + request + ), + Self::InvalidConfigurationHeaderLength { actual } => write!( + f, + "RDPEUSB requires a 9-byte USB configuration header, got {actual} bytes" + ), + Self::InterfaceCountMismatch { declared, actual } => write!( + f, + "USB configuration declares {declared} interfaces but contains {actual}" + ), + Self::DuplicateInterface { interface } => { + write!(f, "USB interface {interface} is selected more than once") + } + Self::InterfaceNotFound { selection } => write!( + f, + "USB interface {} alternate setting {} is absent from the configuration", + selection.interface, selection.alternate_setting + ), + Self::MissingInterfaceSelection { interface } => write!( + f, + "USB configuration has no selected alternate setting for interface {}", + interface + ), + Self::TooManyPipes { selection, actual } => write!( + f, + "USB interface {} alternate setting {} has {actual} pipes; RDPEUSB supports at most {MAX_NON_DEFAULT_EP_COUNT}", + selection.interface, selection.alternate_setting + ), + Self::TooManyActivePipes { actual } => write!( + f, + "USB configuration selects {actual} pipes; a USB device supports at most {MAX_NON_DEFAULT_EP_COUNT} non-default endpoints" + ), + Self::EndpointCountMismatch { + selection, + declared, + actual, + } => write!( + f, + "USB interface {} alternate setting {} declares {declared} endpoints but contains {actual}", + selection.interface, selection.alternate_setting + ), + Self::InvalidMaximumPacketSize { selection, raw } => write!( + f, + "USB interface {} alternate setting {} has invalid wMaxPacketSize {raw:#06x}", + selection.interface, selection.alternate_setting + ), + Self::EmptyIsochronousTransfer => f.write_str("USB isochronous transfer contains no packets"), + Self::IsochronousTransferLengthOverflow { packet } => { + write!(f, "USB isochronous transfer length overflows u32 at packet {packet}") + } + Self::NoAckIsochronousIn => f.write_str("RDPEUSB NoAck is not valid for an isochronous IN transfer"), + } + } +} + +impl core::error::Error for ConversionError {} + +/// An RDPEUSB completion that does not match the submitted USB operation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum CompletionError { + ExpectedTransferIn, + ExpectedTransfer, + ExpectedIoControl, + UnexpectedUrbResultPayload, + MissingSelectConfigurationResult, + MissingSelectInterfaceResult, + MissingCurrentFrameNumberResult, + MissingIsochronousResult, + UnexpectedOutputData { actual: usize }, + ExpectedOneByteOutput { actual: usize }, + OutputLengthTooLarge { actual: usize }, + IsochronousPacketCountMismatch { expected: usize, actual: usize }, + IsochronousPacketLengthExceeded { packet: usize, requested: u32, actual: u32 }, + IsochronousTransferLengthOverflow, + IsochronousDataLengthMismatch { expected: u32, actual: u32 }, +} + +impl fmt::Display for CompletionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ExpectedTransferIn => f.write_str("expected an RDPEUSB transfer-in completion"), + Self::ExpectedTransfer => f.write_str("expected an RDPEUSB transfer completion"), + Self::ExpectedIoControl => f.write_str("expected an RDPEUSB IO-control completion"), + Self::UnexpectedUrbResultPayload => { + f.write_str("RDPEUSB completion contains an unexpected TS_URB result payload") + } + Self::MissingSelectConfigurationResult => { + f.write_str("RDPEUSB completion does not contain a select-configuration result") + } + Self::MissingSelectInterfaceResult => { + f.write_str("RDPEUSB completion does not contain a select-interface result") + } + Self::MissingCurrentFrameNumberResult => { + f.write_str("RDPEUSB completion does not contain a current-frame-number result") + } + Self::MissingIsochronousResult => { + f.write_str("RDPEUSB completion does not contain an isochronous-transfer result") + } + Self::UnexpectedOutputData { actual } => { + write!(f, "RDPEUSB completion unexpectedly contains {actual} output bytes") + } + Self::ExpectedOneByteOutput { actual } => { + write!(f, "RDPEUSB completion contains {actual} output bytes instead of one") + } + Self::OutputLengthTooLarge { actual } => { + write!(f, "RDPEUSB completion output length {actual} does not fit in u32") + } + Self::IsochronousPacketCountMismatch { expected, actual } => write!( + f, + "RDPEUSB isochronous completion contains {actual} packets instead of {expected}" + ), + Self::IsochronousPacketLengthExceeded { + packet, + requested, + actual, + } => write!( + f, + "RDPEUSB isochronous packet {packet} returned {actual} bytes for a {requested}-byte request" + ), + Self::IsochronousTransferLengthOverflow => { + f.write_str("RDPEUSB isochronous completion length overflows u32") + } + Self::IsochronousDataLengthMismatch { expected, actual } => write!( + f, + "RDPEUSB isochronous completion carries {actual} bytes instead of {expected}" + ), + } + } +} + +impl core::error::Error for CompletionError {} + +/// Build a typed RDPEUSB GET_DESCRIPTOR request. +/// +/// RDPEUSB only defines typed descriptor URBs for device, interface, and +/// endpoint recipients. [`control_transfer`] falls back to a generic control +/// URB for other recipients, while this explicitly typed helper returns an +/// error. +pub fn get_descriptor(request: GetDescriptorRequest) -> Result { + let func = descriptor_function(request.recipient, DescriptorOperation::Get).ok_or( + ConversionError::UnsupportedDescriptorRecipient { + recipient: request.recipient, + }, + )?; + let urb = TsUrbControlDescRequest { + index: request.descriptor_index, + desc_type: request.descriptor_type, + lang_id: request.index, + }; + + Ok(transfer_in( + TsUrbInKind::CtlDescReq(urb), + func, + u32::from(request.requested_length), + )) +} + +/// Translate an RDPEUSB `GET_DESCRIPTOR` completion into general USB semantics. +/// +/// A USB operation failure is returned as the [`UsbResult`] error. This +/// function returns [`CompletionError`] only when the completion envelope does +/// not match the submitted operation. +/// +/// [MS-RDPEUSB 2.2.9.9] requires descriptor reads to use +/// `TRANSFER_IN_REQUEST`; the corresponding result has only +/// `TS_URB_RESULT_HEADER` plus the optional data buffer carried by +/// [`URB_COMPLETION`][MS-RDPEUSB 2.2.7.2]. +/// +/// [MS-RDPEUSB 2.2.9.9]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/a1004d0e-99e9-4968-894b-0b924ef2f125 +/// [MS-RDPEUSB 2.2.7.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/5bfa9c84-a74b-4942-9d09-e770b21081eb +pub fn get_descriptor_completion(completion: CompletionData) -> Result>, CompletionError> { + let completion = header_only_transfer_in(completion)?; + + let status = completion_status(completion.hresult, completion.ts_urb_result.header.usbd_status); + Ok(status.map(|()| completion.output_buffer)) +} + +/// Translate a header-only RDPEUSB transfer completion into general USB data +/// transfer semantics. +/// +/// This covers control, bulk, and interrupt transfers. The completion +/// envelope determines the direction: transfer-in data is returned verbatim, +/// while transfer-out reports its completed byte count and an empty data +/// buffer. +pub fn transfer_completion(completion: CompletionData) -> Result>, CompletionError> { + match completion { + CompletionData::TransferIn(completion) => { + if completion.ts_urb_result.payload.is_some() { + return Err(CompletionError::UnexpectedUrbResultPayload); + } + let actual_length = + u32::try_from(completion.output_buffer.len()).map_err(|_| CompletionError::OutputLengthTooLarge { + actual: completion.output_buffer.len(), + })?; + + Ok(TransferCompletion { + status: completion_status(completion.hresult, completion.ts_urb_result.header.usbd_status), + actual_length, + data: completion.output_buffer, + }) + } + CompletionData::TransferOut(completion) => { + if completion.ts_urb_result.payload.is_some() { + return Err(CompletionError::UnexpectedUrbResultPayload); + } + + Ok(TransferCompletion { + status: completion_status(completion.hresult, completion.ts_urb_result.header.usbd_status), + actual_length: completion.output_buffer_size, + data: Vec::new(), + }) + } + CompletionData::IoControl(_) | CompletionData::InternalIoControl(_) => Err(CompletionError::ExpectedTransfer), + } +} + +/// Build an RDPEUSB `GET_CONFIGURATION` request. +#[must_use] +pub fn get_configuration() -> TransferInPacket { + transfer_in( + TsUrbInKind::CtlGetConfig(TsUrbControlGetConfigRequest), + UrbFunction::URB_FUNCTION_GET_CONFIGURATION, + 1, + ) +} + +/// Translate an RDPEUSB `GET_CONFIGURATION` completion. +pub fn get_configuration_completion(completion: CompletionData) -> Result, CompletionError> { + byte_transfer_in_completion(completion) +} + +/// Build an RDPEUSB `GET_INTERFACE` request. +#[must_use] +pub fn get_interface(interface: u8) -> TransferInPacket { + transfer_in( + TsUrbInKind::CtlGetIface(TsUrbControlGetInterfaceRequest { + interface: u16::from(interface), + }), + UrbFunction::URB_FUNCTION_GET_INTERFACE, + 1, + ) +} + +/// Translate an RDPEUSB `GET_INTERFACE` completion. +pub fn get_interface_completion(completion: CompletionData) -> Result, CompletionError> { + byte_transfer_in_completion(completion) +} + +/// Translate a default-control-pipe USB request into an RDPEUSB transfer. +/// +/// Canonical standard, class, and vendor requests use the operation-specific +/// TS_URB forms from MS-RDPEUSB sections 2.2.9.9 through 2.2.9.14. Other setup +/// packets are preserved losslessly in `TS_URB_CONTROL_TRANSFER`. Requests +/// whose execution changes host-controller state must be handled by the +/// owning state layer instead of this function. +pub fn control_transfer(setup: SetupPacket, data: Vec) -> Result { + validate_control_data(setup, &data)?; + if let Some(request) = setup.standard_request() { + if matches!( + request, + standard_request::SET_ADDRESS | standard_request::SET_CONFIGURATION | standard_request::SET_INTERFACE + ) { + return Err(ConversionError::StatefulStandardRequest { request }); + } + } + + let data = if setup.request_type.kind() == RequestKind::STANDARD { + match standard_control_transfer(setup, data) { + Ok(request) => return Ok(request), + Err(data) => data, + } + } else if matches!(setup.request_type.kind(), RequestKind::CLASS | RequestKind::VENDOR) { + if let Some(func) = vendor_or_class_function(setup.request_type.kind(), setup.request_type.recipient()) { + let urb = TsUrbControlVendorClassRequest { + transfer_flags: in_transfer_flags(setup.request_type.direction()), + request: setup.request, + value: setup.value, + index: setup.index, + }; + return Ok(match setup.request_type.direction() { + Direction::In => TransferRequest::In(transfer_in( + TsUrbInKind::VendorClassReq(urb), + func, + u32::from(setup.length), + )), + Direction::Out => TransferRequest::Out(transfer_out(TsUrbOutKind::VendorClassReq(urb), func, data)), + }); + } + data + } else { + data + }; + + Ok(generic_control_transfer(setup, data)) +} + +/// Build an RDPEUSB bulk or interrupt IN transfer. +#[must_use] +pub fn bulk_or_interrupt_in(pipe_handle: PipeHandle, requested_length: u32) -> TransferInPacket { + transfer_in( + TsUrbInKind::BulkInterruptTransfer(TsUrbBulkOrInterruptTransfer { + pipe_handle, + transfer_flags: USBD_TRANSFER_DIRECTION_IN | USBD_SHORT_TRANSFER_OK, + }), + UrbFunction::URB_FUNCTION_BULK_OR_INTERRUPT_TRANSFER, + requested_length, + ) +} + +/// Build an RDPEUSB bulk or interrupt OUT transfer. +#[must_use] +pub fn bulk_or_interrupt_out(pipe_handle: PipeHandle, data: Vec) -> TransferOutPacket { + transfer_out( + TsUrbOutKind::BulkInterruptTransfer(TsUrbBulkOrInterruptTransfer { + pipe_handle, + transfer_flags: 0, + }), + UrbFunction::URB_FUNCTION_BULK_OR_INTERRUPT_TRANSFER, + data, + ) +} + +/// Build an RDPEUSB bulk or interrupt transfer from general USB semantics. +pub fn bulk_or_interrupt( + pipe_handle: PipeHandle, + request: DataTransferRequest>, +) -> Result { + match request.endpoint.direction() { + Direction::In => { + if !request.data.is_empty() { + return Err(ConversionError::InTransferHasData { + actual: request.data.len(), + }); + } + + Ok(TransferRequest::In(bulk_or_interrupt_in(pipe_handle, request.length))) + } + Direction::Out => { + let expected = usize::try_from(request.length) + .map_err(|_| ConversionError::TransferLengthNotRepresentable { length: request.length })?; + if request.data.len() != expected { + return Err(ConversionError::OutTransferLengthMismatch { + expected, + actual: request.data.len(), + }); + } + + Ok(TransferRequest::Out(bulk_or_interrupt_out(pipe_handle, request.data))) + } + } +} + +/// Build an RDPEUSB isochronous transfer. +/// +/// Packet lengths are translated into the prefix offsets required by +/// `USBD_ISO_PACKET_DESCRIPTOR`. Its request-side `Length` and `Status` fields +/// are output-only and are therefore set to zero. +pub fn isochronous( + pipe_handle: PipeHandle, + request: IsochronousTransferRequest, Vec>, + no_ack: bool, +) -> Result { + let IsochronousTransferRequest { + endpoint, + start_frame, + data, + packets, + } = request; + if packets.is_empty() { + return Err(ConversionError::EmptyIsochronousTransfer); + } + + let mut total_length = 0u32; + let mut iso_packet = Vec::with_capacity(packets.len()); + for (packet, length) in packets.into_iter().enumerate() { + iso_packet.push(UsbdIsoPacketDesc { + offset: total_length, + length: 0, + status: 0, + }); + total_length = total_length + .checked_add(length) + .ok_or(ConversionError::IsochronousTransferLengthOverflow { packet })?; + } + + let direction = endpoint.direction(); + if no_ack && direction == Direction::In { + return Err(ConversionError::NoAckIsochronousIn); + } + match direction { + Direction::In if !data.is_empty() => { + return Err(ConversionError::InTransferHasData { actual: data.len() }); + } + Direction::Out => { + let expected = usize::try_from(total_length) + .map_err(|_| ConversionError::TransferLengthNotRepresentable { length: total_length })?; + if data.len() != expected { + return Err(ConversionError::OutTransferLengthMismatch { + expected, + actual: data.len(), + }); + } + } + Direction::In => {} + } + + let transfer_flags = match direction { + Direction::In => USBD_TRANSFER_DIRECTION_IN, + Direction::Out => 0, + } | if start_frame.is_none() { + USBD_START_ISO_TRANSFER_ASAP + } else { + 0 + }; + let urb = TsUrbIsochTransfer { + pipe_handle, + transfer_flags, + start_frame: start_frame.unwrap_or(0), + error_count: 0, + iso_packet, + }; + + Ok(match direction { + Direction::In => TransferRequest::In(transfer_in( + TsUrbInKind::IsochTransfer(urb), + UrbFunction::URB_FUNCTION_ISOCH_TRANSFER, + total_length, + )), + Direction::Out => { + let mut packet = transfer_out( + TsUrbOutKind::IsochTransfer(urb), + UrbFunction::URB_FUNCTION_ISOCH_TRANSFER, + data, + ); + packet.ts_urb.no_ack = no_ack; + TransferRequest::Out(packet) + } + }) +} + +/// Build an RDPEUSB unconfigure request selecting configuration zero. +#[must_use] +pub fn unconfigure() -> TransferInPacket { + transfer_in( + TsUrbInKind::SelectConfig(TsUrbSelectConfig { + usbd_ifaces: Vec::new(), + desc: None, + }), + UrbFunction::URB_FUNCTION_SELECT_CONFIGURATION, + 0, + ) +} + +/// Build an RDPEUSB select-configuration request. +/// +/// The complete descriptor set is included, and `active_interfaces` determines +/// the alternate setting requested for every interface number. The interface +/// array preserves caller order. Configuration zero is selected by +/// [`unconfigure`] instead. +pub fn select_configuration( + descriptor: ConfigurationDescriptorSet<'_>, + active_interfaces: &[InterfaceSelection], + speed: UsbSpeed, +) -> Result { + let actual_interface_count = descriptor + .interfaces() + .filter(|interface| { + !descriptor + .interfaces() + .any(|previous| previous.offset() < interface.offset() && previous.number() == interface.number()) + }) + .count(); + let declared_interface_count = descriptor.configuration().num_interfaces(); + if actual_interface_count != usize::from(declared_interface_count) { + return Err(ConversionError::InterfaceCountMismatch { + declared: declared_interface_count, + actual: actual_interface_count, + }); + } + + let mut usbd_ifaces = Vec::with_capacity(active_interfaces.len()); + for (index, selection) in active_interfaces.iter().copied().enumerate() { + if active_interfaces[..index] + .iter() + .any(|previous| previous.interface == selection.interface) + { + return Err(ConversionError::DuplicateInterface { + interface: selection.interface, + }); + } + let interface = descriptor + .interface(selection.interface, selection.alternate_setting) + .ok_or(ConversionError::InterfaceNotFound { selection })?; + usbd_ifaces.push(interface_information(interface, speed)?); + } + for interface in descriptor.interfaces() { + if !active_interfaces + .iter() + .any(|selection| selection.interface == interface.number()) + { + return Err(ConversionError::MissingInterfaceSelection { + interface: interface.number(), + }); + } + } + let active_pipe_count = usbd_ifaces + .iter() + .map(|interface| interface.ts_usbd_pipe_info.len()) + .sum::(); + if active_pipe_count > MAX_NON_DEFAULT_EP_COUNT { + return Err(ConversionError::TooManyActivePipes { + actual: active_pipe_count, + }); + } + + Ok(transfer_in( + TsUrbInKind::SelectConfig(TsUrbSelectConfig { + usbd_ifaces, + desc: Some(configuration_descriptor(descriptor)?), + }), + UrbFunction::URB_FUNCTION_SELECT_CONFIGURATION, + 0, + )) +} + +/// Build an RDPEUSB select-interface request for one resolved descriptor. +pub fn select_interface( + config_handle: ConfigHandle, + interface: InterfaceDescriptor<'_>, + speed: UsbSpeed, +) -> Result { + let urb = TsUrbSelectInterface { + config_handle, + usbd_iface: interface_information(interface, speed)?, + }; + Ok(transfer_in( + TsUrbInKind::SelectIface(urb), + UrbFunction::URB_FUNCTION_SELECT_INTERFACE, + 0, + )) +} + +/// Translate an RDPEUSB select-configuration completion. +/// +/// The returned configuration and pipe handles are RDPEUSB-private opaque +/// values. A server facade is expected to validate and retain them, then +/// expose only the general USB success or failure to its caller. +pub fn select_configuration_completion( + completion: CompletionData, +) -> Result, CompletionError> { + let CompletionData::TransferIn(completion) = completion else { + return Err(CompletionError::ExpectedTransferIn); + }; + ensure_empty_output(&completion)?; + let Some(TsUrbResultPayload::SelectConfig(result)) = completion.ts_urb_result.payload else { + return Err(CompletionError::MissingSelectConfigurationResult); + }; + + let status = completion_status(completion.hresult, completion.ts_urb_result.header.usbd_status); + Ok(status.map(|()| result)) +} + +/// Translate an RDPEUSB select-interface completion. +pub fn select_interface_completion( + completion: CompletionData, +) -> Result, CompletionError> { + let CompletionData::TransferIn(completion) = completion else { + return Err(CompletionError::ExpectedTransferIn); + }; + ensure_empty_output(&completion)?; + let Some(TsUrbResultPayload::SelectIface(result)) = completion.ts_urb_result.payload else { + return Err(CompletionError::MissingSelectInterfaceResult); + }; + + let status = completion_status(completion.hresult, completion.ts_urb_result.header.usbd_status); + Ok(status.map(|()| result)) +} + +/// Build a pipe reset that also clears the endpoint stall state. +/// +/// [MS-RDPEUSB 2.2.9.4] carries `URB_PIPE_REQUEST` in a transfer-in request +/// with an empty output buffer. +/// +/// [MS-RDPEUSB 2.2.9.4]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/dcba564e-de14-4d60-82ac-a0fe7a52b312 +#[must_use] +pub fn reset_pipe_and_clear_stall(pipe_handle: PipeHandle) -> TransferInPacket { + transfer_in( + TsUrbInKind::PipeReq(TsUrbPipeRequest { pipe_handle }), + UrbFunction::URB_FUNCTION_SYNC_RESET_PIPE_AND_CLEAR_STALL, + 0, + ) +} + +/// Translate a header-only RDPEUSB pipe-request completion. +pub fn pipe_request_completion(completion: CompletionData) -> Result, CompletionError> { + unit_transfer_in_completion(completion) +} + +/// Build an RDPEUSB current-frame-number request. +/// +/// [MS-RDPEUSB 2.2.9.5] carries this URB in a transfer-in request whose output +/// buffer size is zero; the frame number is part of the TS_URB result. +/// +/// [MS-RDPEUSB 2.2.9.5]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/4985b1dc-5bd9-4988-97a6-063969dc26b4 +#[must_use] +pub fn current_frame_number() -> TransferInPacket { + transfer_in( + TsUrbInKind::GetCurFrameNum(TsUrbGetCurrFrameNum), + UrbFunction::URB_FUNCTION_GET_CURRENT_FRAME_NUMBER, + 0, + ) +} + +/// Translate an RDPEUSB current-frame-number completion. +pub fn current_frame_number_completion(completion: CompletionData) -> Result, CompletionError> { + let CompletionData::TransferIn(completion) = completion else { + return Err(CompletionError::ExpectedTransferIn); + }; + ensure_empty_output(&completion)?; + let Some(TsUrbResultPayload::FrameNum(result)) = completion.ts_urb_result.payload else { + return Err(CompletionError::MissingCurrentFrameNumberResult); + }; + + let status = completion_status(completion.hresult, completion.ts_urb_result.header.usbd_status); + Ok(status.map(|()| result.frame_number)) +} + +/// Build an RDPEUSB upstream-port reset request. +/// +/// [MS-RDPEUSB 2.2.12.1] requires an `IO_CONTROL` request with empty input and +/// output buffers. +/// +/// [MS-RDPEUSB 2.2.12.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/8f13c014-2ece-481d-a843-9ae9b03d45fe +#[must_use] +pub fn reset_device() -> IoControlPacket { + IoControlPacket { + ioctl_code: IoctlInternalUsb::ResetPort, + input_buffer: Vec::new(), + output_buffer_size: 0, + } +} + +/// Translate an RDPEUSB upstream-port reset completion. +pub fn reset_device_completion(completion: CompletionData) -> Result, CompletionError> { + let CompletionData::IoControl(completion) = completion else { + return Err(CompletionError::ExpectedIoControl); + }; + if !completion.output_buffer.is_empty() { + return Err(CompletionError::UnexpectedOutputData { + actual: completion.output_buffer.len(), + }); + } + + if hresult_succeeded(completion.hresult) { + Ok(Ok(())) + } else { + Ok(Err(UsbError::Error)) + } +} + +/// Translate an acknowledged RDPEUSB isochronous completion. +/// +/// For transfer-in, [MS-RDPEUSB 2.2.10.5] packs only successful packet data +/// into the output buffer. Packet offsets continue to describe the original +/// transfer buffer and therefore are not used to slice that packed data. +/// +/// [MS-RDPEUSB 2.2.10.5]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/6f072673-52b8-4750-ac91-9d2313f13b17 +pub fn isochronous_completion( + completion: CompletionData, + requested_packet_lengths: &[u32], +) -> Result, Vec>, CompletionError> { + let (ts_urb_result, hresult, output_buffer, envelope_actual_length, transfer_in) = match completion { + CompletionData::TransferIn(completion) => { + let actual_length = + u32::try_from(completion.output_buffer.len()).map_err(|_| CompletionError::OutputLengthTooLarge { + actual: completion.output_buffer.len(), + })?; + ( + completion.ts_urb_result, + completion.hresult, + completion.output_buffer, + actual_length, + true, + ) + } + CompletionData::TransferOut(completion) => ( + completion.ts_urb_result, + completion.hresult, + Vec::new(), + completion.output_buffer_size, + false, + ), + CompletionData::IoControl(_) | CompletionData::InternalIoControl(_) => { + return Err(CompletionError::ExpectedTransfer); + } + }; + let Some(TsUrbResultPayload::Isoch(result)) = ts_urb_result.payload else { + return Err(CompletionError::MissingIsochronousResult); + }; + + // Clients such as FreeRDP return an empty packet list when the overall + // transfer failed. Packet details are meaningful only for a successful + // URB, so preserve the USB failure instead of upgrading it to a shape + // error. + let status = completion_status(hresult, ts_urb_result.header.usbd_status); + if let Err(error) = status { + return Ok(Err(error)); + } + + if result.iso_packet.len() != requested_packet_lengths.len() { + return Err(CompletionError::IsochronousPacketCountMismatch { + expected: requested_packet_lengths.len(), + actual: result.iso_packet.len(), + }); + } + + let mut successful_length = 0u32; + let mut packets = Vec::with_capacity(result.iso_packet.len()); + for (packet, (result, requested)) in result + .iso_packet + .into_iter() + .zip(requested_packet_lengths.iter().copied()) + .enumerate() + { + if result.length > requested { + return Err(CompletionError::IsochronousPacketLengthExceeded { + packet, + requested, + actual: result.length, + }); + } + + let packet_status = usb_status(packet_status_raw(result.status)); + if packet_status.is_ok() { + successful_length = successful_length + .checked_add(result.length) + .ok_or(CompletionError::IsochronousTransferLengthOverflow)?; + } + packets.push(IsochronousPacketCompletion { + status: packet_status, + actual_length: result.length, + }); + } + + // Only transfer-in completions contain the packed successful-packet data + // described by MS-RDPEUSB 2.2.10.5. For transfer-out, OutputBufferSize is + // the number of bytes sent and is not required to equal that packet sum. + if transfer_in && envelope_actual_length != successful_length { + return Err(CompletionError::IsochronousDataLengthMismatch { + expected: successful_length, + actual: envelope_actual_length, + }); + } + Ok(Ok(IsochronousTransferOutput { + start_frame: result.start_frame, + actual_length: envelope_actual_length, + data: output_buffer, + packets, + })) +} + +fn standard_control_transfer(setup: SetupPacket, data: Vec) -> Result> { + let Some(standard_request) = setup.standard_request() else { + return Err(data); + }; + let recipient = setup.request_type.recipient(); + let direction = setup.request_type.direction(); + + match standard_request { + standard_request::GET_DESCRIPTOR if setup.length == 0 || direction == Direction::In => { + let Some(func) = descriptor_function(recipient, DescriptorOperation::Get) else { + return Err(data); + }; + let [index, desc_type] = setup.value.to_le_bytes(); + Ok(TransferRequest::In(transfer_in( + TsUrbInKind::CtlDescReq(TsUrbControlDescRequest { + index, + desc_type, + lang_id: setup.index, + }), + func, + u32::from(setup.length), + ))) + } + standard_request::SET_DESCRIPTOR if setup.length == 0 || direction == Direction::Out => { + let Some(func) = descriptor_function(recipient, DescriptorOperation::Set) else { + return Err(data); + }; + let [index, desc_type] = setup.value.to_le_bytes(); + Ok(TransferRequest::Out(transfer_out( + TsUrbOutKind::CtlDescReq(TsUrbControlDescRequest { + index, + desc_type, + lang_id: setup.index, + }), + func, + data, + ))) + } + standard_request::GET_STATUS if direction == Direction::In && setup.value == 0 && setup.length == 2 => { + let Some(func) = get_status_function(recipient) else { + return Err(data); + }; + Ok(TransferRequest::In(transfer_in( + TsUrbInKind::CtlGetStatus(TsUrbControlGetStatusRequest { index: setup.index }), + func, + 2, + ))) + } + standard_request::CLEAR_FEATURE | standard_request::SET_FEATURE + if direction == Direction::Out && setup.length == 0 => + { + let Some(func) = feature_function(standard_request, recipient) else { + return Err(data); + }; + // MS-RDPEUSB 2.2.9.10 requires this USB host-to-device operation + // in TRANSFER_IN_REQUEST with an empty output buffer. + Ok(TransferRequest::In(transfer_in( + TsUrbInKind::CtlFeatReq(TsUrbControlFeatRequest { + feat_selector: setup.value, + index: setup.index, + }), + func, + 0, + ))) + } + standard_request::GET_CONFIGURATION + if direction == Direction::In + && recipient == Recipient::DEVICE + && setup.value == 0 + && setup.index == 0 + && setup.length == 1 => + { + Ok(TransferRequest::In(transfer_in( + TsUrbInKind::CtlGetConfig(TsUrbControlGetConfigRequest), + UrbFunction::URB_FUNCTION_GET_CONFIGURATION, + 1, + ))) + } + standard_request::GET_INTERFACE + if direction == Direction::In + && recipient == Recipient::INTERFACE + && setup.value == 0 + && setup.length == 1 => + { + Ok(TransferRequest::In(transfer_in( + TsUrbInKind::CtlGetIface(TsUrbControlGetInterfaceRequest { interface: setup.index }), + UrbFunction::URB_FUNCTION_GET_INTERFACE, + 1, + ))) + } + _ => Err(data), + } +} + +fn generic_control_transfer(setup: SetupPacket, data: Vec) -> TransferRequest { + let direction = setup.request_type.direction(); + let urb = TsUrbControlTransfer { + pipe: DEFAULT_CONTROL_PIPE_HANDLE, + transfer_flags: in_transfer_flags(direction) | USBD_DEFAULT_PIPE_TRANSFER, + setup_packet: rdpeusb_setup_packet(setup), + }; + + match direction { + Direction::In => TransferRequest::In(transfer_in( + TsUrbInKind::CtlTransfer(urb), + UrbFunction::URB_FUNCTION_CONTROL_TRANSFER, + u32::from(setup.length), + )), + Direction::Out => TransferRequest::Out(transfer_out( + TsUrbOutKind::CtlTransfer(urb), + UrbFunction::URB_FUNCTION_CONTROL_TRANSFER, + data, + )), + } +} + +fn validate_control_data(setup: SetupPacket, data: &[u8]) -> Result<(), ConversionError> { + match setup.request_type.direction() { + Direction::In if !data.is_empty() => Err(ConversionError::InTransferHasData { actual: data.len() }), + Direction::Out if data.len() != usize::from(setup.length) => Err(ConversionError::OutTransferLengthMismatch { + expected: usize::from(setup.length), + actual: data.len(), + }), + Direction::In | Direction::Out => Ok(()), + } +} + +#[derive(Debug, Clone, Copy)] +enum DescriptorOperation { + Get, + Set, +} + +fn descriptor_function(recipient: Recipient, operation: DescriptorOperation) -> Option { + match (operation, recipient) { + (DescriptorOperation::Get, Recipient::DEVICE) => Some(UrbFunction::URB_FUNCTION_GET_DESCRIPTOR_FROM_DEVICE), + (DescriptorOperation::Get, Recipient::INTERFACE) => { + Some(UrbFunction::URB_FUNCTION_GET_DESCRIPTOR_FROM_INTERFACE) + } + (DescriptorOperation::Get, Recipient::ENDPOINT) => Some(UrbFunction::URB_FUNCTION_GET_DESCRIPTOR_FROM_ENDPOINT), + (DescriptorOperation::Set, Recipient::DEVICE) => Some(UrbFunction::URB_FUNCTION_SET_DESCRIPTOR_TO_DEVICE), + (DescriptorOperation::Set, Recipient::INTERFACE) => Some(UrbFunction::URB_FUNCTION_SET_DESCRIPTOR_TO_INTERFACE), + (DescriptorOperation::Set, Recipient::ENDPOINT) => Some(UrbFunction::URB_FUNCTION_SET_DESCRIPTOR_TO_ENDPOINT), + _ => None, + } +} + +fn feature_function(request: u8, recipient: Recipient) -> Option { + match (request, recipient) { + (standard_request::CLEAR_FEATURE, Recipient::DEVICE) => Some(UrbFunction::URB_FUNCTION_CLEAR_FEATURE_TO_DEVICE), + (standard_request::CLEAR_FEATURE, Recipient::INTERFACE) => { + Some(UrbFunction::URB_FUNCTION_CLEAR_FEATURE_TO_INTERFACE) + } + (standard_request::CLEAR_FEATURE, Recipient::ENDPOINT) => { + Some(UrbFunction::URB_FUNCTION_CLEAR_FEATURE_TO_ENDPOINT) + } + (standard_request::CLEAR_FEATURE, Recipient::OTHER) => Some(UrbFunction::URB_FUNCTION_CLEAR_FEATURE_TO_OTHER), + (standard_request::SET_FEATURE, Recipient::DEVICE) => Some(UrbFunction::URB_FUNCTION_SET_FEATURE_TO_DEVICE), + (standard_request::SET_FEATURE, Recipient::INTERFACE) => { + Some(UrbFunction::URB_FUNCTION_SET_FEATURE_TO_INTERFACE) + } + (standard_request::SET_FEATURE, Recipient::ENDPOINT) => Some(UrbFunction::URB_FUNCTION_SET_FEATURE_TO_ENDPOINT), + (standard_request::SET_FEATURE, Recipient::OTHER) => Some(UrbFunction::URB_FUNCTION_SET_FEATURE_TO_OTHER), + _ => None, + } +} + +fn get_status_function(recipient: Recipient) -> Option { + match recipient { + Recipient::DEVICE => Some(UrbFunction::URB_FUNCTION_GET_STATUS_FROM_DEVICE), + Recipient::INTERFACE => Some(UrbFunction::URB_FUNCTION_GET_STATUS_FROM_INTERFACE), + Recipient::ENDPOINT => Some(UrbFunction::URB_FUNCTION_GET_STATUS_FROM_ENDPOINT), + Recipient::OTHER => Some(UrbFunction::URB_FUNCTION_GET_STATUS_FROM_OTHER), + _ => None, + } +} + +fn vendor_or_class_function(kind: RequestKind, recipient: Recipient) -> Option { + match (kind, recipient) { + (RequestKind::VENDOR, Recipient::DEVICE) => Some(UrbFunction::URB_FUNCTION_VENDOR_DEVICE), + (RequestKind::VENDOR, Recipient::INTERFACE) => Some(UrbFunction::URB_FUNCTION_VENDOR_INTERFACE), + (RequestKind::VENDOR, Recipient::ENDPOINT) => Some(UrbFunction::URB_FUNCTION_VENDOR_ENDPOINT), + (RequestKind::VENDOR, Recipient::OTHER) => Some(UrbFunction::URB_FUNCTION_VENDOR_OTHER), + (RequestKind::CLASS, Recipient::DEVICE) => Some(UrbFunction::URB_FUNCTION_CLASS_DEVICE), + (RequestKind::CLASS, Recipient::INTERFACE) => Some(UrbFunction::URB_FUNCTION_CLASS_INTERFACE), + (RequestKind::CLASS, Recipient::ENDPOINT) => Some(UrbFunction::URB_FUNCTION_CLASS_ENDPOINT), + (RequestKind::CLASS, Recipient::OTHER) => Some(UrbFunction::URB_FUNCTION_CLASS_OTHER), + _ => None, + } +} + +fn configuration_descriptor(descriptor: ConfigurationDescriptorSet<'_>) -> Result { + let configuration = descriptor.configuration(); + if usize::from(configuration.length()) != UsbConfigDesc::FIXED_PART_SIZE { + return Err(ConversionError::InvalidConfigurationHeaderLength { + actual: configuration.length(), + }); + } + + Ok(UsbConfigDesc { + length: configuration.length(), + descriptor_type: configuration.raw_descriptor().descriptor_type(), + total_length: configuration.total_length(), + num_interfaces: configuration.num_interfaces(), + configuration_value: configuration.configuration_value(), + configuration: configuration.configuration_string(), + attributes: configuration.attributes().raw(), + max_power: configuration.max_power_raw(), + trailing: descriptor.as_bytes()[UsbConfigDesc::FIXED_PART_SIZE..].to_vec(), + }) +} + +fn interface_information( + interface: InterfaceDescriptor<'_>, + speed: UsbSpeed, +) -> Result { + let selection = InterfaceSelection { + interface: interface.number(), + alternate_setting: interface.alternate_setting(), + }; + let pipe_count = interface.endpoints().count(); + if pipe_count != usize::from(interface.num_endpoints()) { + return Err(ConversionError::EndpointCountMismatch { + selection, + declared: interface.num_endpoints(), + actual: pipe_count, + }); + } + if pipe_count > MAX_NON_DEFAULT_EP_COUNT { + return Err(ConversionError::TooManyPipes { + selection, + actual: pipe_count, + }); + } + + let mut pipes = Vec::with_capacity(pipe_count); + for endpoint in interface.endpoints() { + let raw_max_packet_size = endpoint.max_packet_size().raw(); + if interface.alternate_setting() == 0 + && endpoint.transfer_type() == TransferType::Isochronous + && raw_max_packet_size != 0 + { + return Err(ConversionError::InvalidMaximumPacketSize { + selection, + raw: raw_max_packet_size, + }); + } + let max_packet_size = maximum_packet_size(endpoint.max_packet_size(), endpoint.transfer_type(), speed) + .map_err(|raw| ConversionError::InvalidMaximumPacketSize { selection, raw })?; + pipes.push(TsUsbdPipeInfo { + max_packet_size, + // MS-RDPEUSB 2.2.9.1.3 notes that the client ignores this + // obsolete USBD field. Zero requests the client default. + max_transfer_size: 0, + // No general USB semantic requests a Windows pipe override. + pipe_flags: 0, + }); + } + + Ok(TsUsbdInterfaceInfo { + interface_number: selection.interface, + alternate_setting: selection.alternate_setting, + ts_usbd_pipe_info: pipes, + }) +} + +/// Convert USB `wMaxPacketSize` semantics into RDPEUSB's effective pipe size. +/// +/// High-speed isochronous and interrupt endpoints include the additional +/// transactions per microframe. Other endpoint kinds and speeds reject those +/// high-bandwidth bits instead of silently changing their meaning. +/// +/// This function has no interface context, so zero-bandwidth isochronous +/// endpoints are accepted. A caller validating a complete interface must also +/// enforce the alternate-setting-zero requirement from USB 2.0 Section 5.6.3. +pub fn maximum_packet_size( + max_packet_size: ironrdp_usb::endpoint::MaxPacketSize, + transfer_type: TransferType, + speed: UsbSpeed, +) -> Result { + let raw = max_packet_size.raw(); + if !max_packet_size.is_valid_for_usb2(speed, transfer_type) { + return Err(raw); + } + + let high_speed_periodic = + speed == UsbSpeed::High && matches!(transfer_type, TransferType::Isochronous | TransferType::Interrupt); + if high_speed_periodic { + let bytes = max_packet_size + .high_speed_payload_per_microframe() + .map_err(|error| error.raw())?; + // At most 3 * 0x7ff, which is representable as u16. + Ok(bytes as u16) + } else { + Ok(max_packet_size.packet_size()) + } +} + +fn in_transfer_flags(direction: Direction) -> u32 { + match direction { + Direction::In => USBD_TRANSFER_DIRECTION_IN | USBD_SHORT_TRANSFER_OK, + Direction::Out => 0, + } +} + +fn completion_status(hresult: u32, usbd_status: u32) -> UsbResult<()> { + if !hresult_succeeded(hresult) { + return Err(UsbError::Error); + } + + usb_status(usbd_status) +} + +fn hresult_succeeded(hresult: u32) -> bool { + hresult & HRESULT_FAILURE_BIT == 0 +} + +fn usb_status(usbd_status: u32) -> UsbResult<()> { + match usbd_status { + USBD_STATUS_SUCCESS => Ok(()), + USBD_STATUS_CANCELED => Err(UsbError::Cancelled), + USBD_STATUS_STALL_PID => Err(UsbError::Stall), + USBD_STATUS_TIMEOUT => Err(UsbError::Timeout), + USBD_STATUS_BABBLE_DETECTED => Err(UsbError::Overflow), + USBD_STATUS_DEVICE_GONE => Err(UsbError::NoDevice), + _ => Err(UsbError::Error), + } +} + +fn packet_status_raw(status: i32) -> u32 { + u32::from_ne_bytes(status.to_ne_bytes()) +} + +fn header_only_transfer_in(completion: CompletionData) -> Result { + let CompletionData::TransferIn(completion) = completion else { + return Err(CompletionError::ExpectedTransferIn); + }; + if completion.ts_urb_result.payload.is_some() { + return Err(CompletionError::UnexpectedUrbResultPayload); + } + Ok(completion) +} + +fn ensure_empty_output(completion: &TransferInCompletionResult) -> Result<(), CompletionError> { + if completion.output_buffer.is_empty() { + Ok(()) + } else { + Err(CompletionError::UnexpectedOutputData { + actual: completion.output_buffer.len(), + }) + } +} + +fn byte_transfer_in_completion(completion: CompletionData) -> Result, CompletionError> { + let completion = header_only_transfer_in(completion)?; + let status = completion_status(completion.hresult, completion.ts_urb_result.header.usbd_status); + if let Err(error) = status { + return Ok(Err(error)); + } + if completion.output_buffer.len() != 1 { + return Err(CompletionError::ExpectedOneByteOutput { + actual: completion.output_buffer.len(), + }); + } + + Ok(Ok(completion.output_buffer[0])) +} + +fn unit_transfer_in_completion(completion: CompletionData) -> Result, CompletionError> { + let completion = header_only_transfer_in(completion)?; + ensure_empty_output(&completion)?; + let status = completion_status(completion.hresult, completion.ts_urb_result.header.usbd_status); + Ok(status) +} + +fn rdpeusb_setup_packet(setup: SetupPacket) -> RdpeusbSetupPacket { + RdpeusbSetupPacket { + request_type: setup.request_type.raw(), + request: setup.request, + value: setup.value, + index: setup.index, + length: setup.length, + } +} + +fn transfer_in(kind: TsUrbInKind, func: UrbFunction, output_buffer_size: u32) -> TransferInPacket { + TransferInPacket { + ts_urb: TsUrbInPacket { kind, func }, + output_buffer_size, + } +} + +fn transfer_out(kind: TsUrbOutKind, func: UrbFunction, output_buffer: Vec) -> TransferOutPacket { + TransferOutPacket { + ts_urb: TsUrbOutPacket { + kind, + no_ack: false, + func, + }, + output_buffer, + } +} diff --git a/crates/ironrdp-server/Cargo.toml b/crates/ironrdp-server/Cargo.toml index 2b16df9e0..db3963856 100644 --- a/crates/ironrdp-server/Cargo.toml +++ b/crates/ironrdp-server/Cargo.toml @@ -23,6 +23,7 @@ rayon = ["dep:rayon"] qoi = ["dep:qoicoubeh", "ironrdp-pdu/qoi"] qoiz = ["dep:zstd-safe", "qoi", "ironrdp-pdu/qoiz"] egfx = ["dep:ironrdp-egfx"] +usb = ["dep:ironrdp-rdpeusb", "dep:ironrdp-usb"] # Opt-in NSCodec encoder. Off by default so consumers that don't need a legacy # bitmap codec fallback (e.g., RemoteFX-only or H.264-capable clients) don't # pay the extra build cost. @@ -53,6 +54,8 @@ ironrdp-tokio = { path = "../ironrdp-tokio", version = "0.10", features = ["reqw ironrdp-acceptor = { path = "../ironrdp-acceptor", version = "0.10" } # public ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.9" } # public ironrdp-rdpsnd = { path = "../ironrdp-rdpsnd", version = "0.9" } # public +ironrdp-rdpeusb = { path = "../ironrdp-rdpeusb", version = "0.1", optional = true } +ironrdp-usb = { path = "../ironrdp-usb", version = "0.1", optional = true } tracing = { version = "0.1", features = ["log"] } x509-cert = { version = "0.3", optional = true } rustls-pemfile = { version = "2.2", optional = true } diff --git a/crates/ironrdp-server/src/builder.rs b/crates/ironrdp-server/src/builder.rs index 228507900..f21c68f1b 100644 --- a/crates/ironrdp-server/src/builder.rs +++ b/crates/ironrdp-server/src/builder.rs @@ -13,6 +13,8 @@ use super::display::{DesktopSize, RdpServerDisplay}; use super::gfx::GfxServerFactory; use super::handler::{KeyboardEvent, MouseEvent, RdpServerInputHandler}; use super::server::{ConnectionHandler, CredentialValidator, RdpServer, RdpServerOptions, RdpServerSecurity}; +#[cfg(feature = "usb")] +use crate::urbdrc::DeviceFactory; use crate::{DisplayUpdate, RdpServerDisplayUpdates, SoundServerFactory}; pub struct WantsAddr {} @@ -41,6 +43,8 @@ pub struct BuilderDone { credential_validator: Option>, #[cfg(feature = "egfx")] gfx_factory: Option>, + #[cfg(feature = "usb")] + usb_factory: Option>, display_suppressed: Option>, autodetect_rtt: Option>, honor_client_desktop_size: Option, @@ -143,6 +147,8 @@ impl RdpServerBuilder { max_request_size: RdpServerOptions::DEFAULT_MAX_REQUEST_SIZE, #[cfg(feature = "egfx")] gfx_factory: None, + #[cfg(feature = "usb")] + usb_factory: None, display_suppressed: None, autodetect_rtt: None, honor_client_desktop_size: None, @@ -166,6 +172,8 @@ impl RdpServerBuilder { max_request_size: RdpServerOptions::DEFAULT_MAX_REQUEST_SIZE, #[cfg(feature = "egfx")] gfx_factory: None, + #[cfg(feature = "usb")] + usb_factory: None, display_suppressed: None, autodetect_rtt: None, honor_client_desktop_size: None, @@ -193,6 +201,12 @@ impl RdpServerBuilder { self } + #[cfg(feature = "usb")] + pub fn with_usb_factory(mut self, usb_factory: Option>) -> Self { + self.state.usb_factory = usb_factory; + self + } + pub fn with_bitmap_codecs(mut self, codecs: BitmapCodecs) -> Self { self.state.codecs = codecs; self @@ -340,6 +354,8 @@ impl RdpServerBuilder { #[cfg(feature = "egfx")] self.state.gfx_factory, self.state.display_suppressed, + #[cfg(feature = "usb")] + self.state.usb_factory, self.state.autodetect_rtt, ); server.set_credential_validator(self.state.credential_validator); diff --git a/crates/ironrdp-server/src/lib.rs b/crates/ironrdp-server/src/lib.rs index 112d4b235..db8a9c5aa 100644 --- a/crates/ironrdp-server/src/lib.rs +++ b/crates/ironrdp-server/src/lib.rs @@ -20,6 +20,8 @@ mod handler; mod helper; mod server; mod sound; +#[cfg(feature = "usb")] +mod urbdrc; pub use clipboard::CliprdrServerFactory; pub use display::{ @@ -33,13 +35,18 @@ pub use handler::{KeyboardEvent, MouseEvent, RdpServerInputHandler}; #[cfg(feature = "helper")] pub use helper::TlsIdentityCtx; pub use ironrdp_pdu::rdp::session_info::ServerAutoReconnect; +#[cfg(feature = "usb")] +pub use ironrdp_rdpeusb::io::{CompletionData, DeviceAnnounce, DeviceText, InternalIoControlPacket}; pub use server::{ AutoReconnectCookieHandle, ConnectionHandler, CredentialDecision, CredentialValidationError, CredentialValidator, Credentials, ExactMatchCredentialValidator, PostConnectionAction, RdpServer, RdpServerOptions, RdpServerSecurity, ServerEvent, ServerEventSender, TransportTls, }; pub use sound::{RdpsndServerHandler, RdpsndServerMessage, SoundServerFactory}; - +#[cfg(feature = "usb")] +pub use urbdrc::{ + DeviceFactory, PendingHandle, PendingRequest, RawPending, RdpUsbDeviceAnnounceInfo, UsbDeviceHandle, UsbRedirDevice, +}; #[cfg(feature = "__bench")] pub mod bench { pub mod encoder { diff --git a/crates/ironrdp-server/src/server.rs b/crates/ironrdp-server/src/server.rs index 6ce6e54f5..a075d706e 100644 --- a/crates/ironrdp-server/src/server.rs +++ b/crates/ironrdp-server/src/server.rs @@ -2,6 +2,8 @@ use core::fmt; use core::net::SocketAddr; use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use core::time::Duration; +#[cfg(feature = "usb")] +use std::collections::HashMap; use std::rc::Rc; use std::sync::Arc; use std::sync::LazyLock; @@ -16,6 +18,8 @@ use ironrdp_core::{decode, encode_vec, impl_as_any}; use ironrdp_displaycontrol::pdu::DisplayControlMonitorLayout; use ironrdp_displaycontrol::server::{DisplayControlHandler, DisplayControlServer}; use ironrdp_dvc as dvc; +#[cfg(feature = "usb")] +use ironrdp_dvc::DynamicChannelId; use ironrdp_pdu::input::InputEventPdu; use ironrdp_pdu::input::fast_path::{FastPathInput, FastPathInputEvent}; use ironrdp_pdu::mcs::{SendDataIndication, SendDataRequest}; @@ -25,6 +29,8 @@ use ironrdp_pdu::rdp::headers::{ServerDeactivateAll, ShareControlPdu}; use ironrdp_pdu::rdp::server_error_info::{ErrorInfo, ProtocolIndependentCode, ServerSetErrorInfoPdu}; use ironrdp_pdu::x224::X224; use ironrdp_pdu::{Action, PduResult, decode_err, mcs, nego, rdp}; +#[cfg(feature = "usb")] +use ironrdp_rdpeusb::io::RequestId; use ironrdp_rdpsnd as rdpsnd; use ironrdp_svc::{ChannelFlags, StaticChannelId, StaticChannelSet, SvcProcessor, server_encode_svc_messages}; use ironrdp_tokio::{FramedRead, FramedWrite, TokioFramed, split_tokio_framed, unsplit_tokio_framed}; @@ -45,7 +51,14 @@ use crate::encoder::{UpdateEncoder, UpdateEncoderCodecs}; #[cfg(feature = "egfx")] use crate::gfx::{EgfxServerMessage, GfxServerFactory}; use crate::handler::RdpServerInputHandler; +#[cfg(feature = "usb")] +use crate::urbdrc::{ + DeviceFactory, RawPending, ServerDeviceIoReq, UrbdrcDeviceServerMessage, UrbdrcServerMessage, UsbControlHandle, + UsbDeviceHandle, UsbDeviceLifecycle, +}; use crate::{SoundServerFactory, builder, capabilities}; +#[cfg(feature = "usb")] +use ironrdp_rdpeusb::{InterfaceAlloc, io::CompletionData, server::UrbdrcControlServer, server::UrbdrcDeviceServer}; /// TCP listen backlog size for the RDP server socket. const LISTENER_BACKLOG: u32 = 1024; @@ -373,6 +386,30 @@ impl DisplayControlHandler for DisplayControlBackend { } } +#[cfg(feature = "usb")] +struct ServerUsbManager { + factory: Box, + comp_iface_alloc: InterfaceAlloc, + router: HashMap, +} + +#[cfg(feature = "usb")] +struct ServerUsbDevice { + lifecycle: Arc, + pending: HashMap>, +} + +#[cfg(feature = "usb")] +impl ServerUsbManager { + fn new(inner: Box) -> Self { + Self { + factory: inner, + comp_iface_alloc: InterfaceAlloc::default(), + router: HashMap::new(), + } + } +} + /// Selects who performs the TLS handshake for a connection accepted via /// [`RdpServer::run_connection_with`]. #[derive(Debug, Clone, Copy)] @@ -461,6 +498,8 @@ pub struct RdpServer { gfx_factory: Option>, #[cfg(feature = "egfx")] gfx_handle: Option, + #[cfg(feature = "usb")] + usb_man: Option, ev_sender: mpsc::UnboundedSender, ev_receiver: Arc>>, creds: Option, @@ -543,6 +582,8 @@ pub enum ServerEvent { Egfx(EgfxServerMessage), /// Trigger an RTT measurement probe (requires auto-detect enabled). AutoDetectRttRequest, + #[cfg(feature = "usb")] + Usb(UrbdrcServerMessage), } impl fmt::Debug for ServerEvent { @@ -558,6 +599,8 @@ impl fmt::Debug for ServerEvent { Self::GetLocalAddr(..) => f.write_str("GetLocalAddr(..)"), #[cfg(feature = "egfx")] Self::Egfx(..) => f.write_str("Egfx(..)"), + #[cfg(feature = "usb")] + Self::Usb(..) => f.write_str("Usb(..)"), Self::AutoDetectRttRequest => f.write_str("AutoDetectRttRequest"), } } @@ -594,6 +637,7 @@ impl RdpServer { connection_handler: Option>, #[cfg(feature = "egfx")] mut gfx_factory: Option>, display_suppressed: Option>, + #[cfg(feature = "usb")] usb_factory: Option>, autodetect_rtt: Option>, ) -> Self { let (ev_sender, ev_receiver) = ServerEvent::create_channel(); @@ -607,6 +651,7 @@ impl RdpServer { if let Some(gfx) = gfx_factory.as_mut() { gfx.set_sender(ev_sender.clone()); } + Self { opts, handler: Arc::new(Mutex::new(handler)), @@ -619,6 +664,8 @@ impl RdpServer { gfx_factory, #[cfg(feature = "egfx")] gfx_handle: None, + #[cfg(feature = "usb")] + usb_man: usb_factory.map(ServerUsbManager::new), ev_sender, ev_receiver: Arc::new(Mutex::new(ev_receiver)), creds: None, @@ -834,6 +881,27 @@ impl RdpServer { &self.ev_sender } + #[cfg(feature = "usb")] + fn remove_usb_device(&mut self, dvc_id: DynamicChannelId) { + let Some(usb_man) = self.usb_man.as_mut() else { + warn!("Missing USB device factory"); + return; + }; + + if let Some(device) = usb_man.router.remove(&dvc_id) { + // Set the terminal state before dropping completion senders. A woken + // PendingRequest must not enqueue CANCEL_REQUEST for a removed DVC. + device.lifecycle.mark_closed(); + debug!( + dvc_id, + pending_requests = device.pending.len(), + "Removed closed USB device from request router" + ); + } else { + trace!(dvc_id, "Closed USB device is absent from request router"); + } + } + /// Returns the shared "display suppressed" flag — `true` while the /// connected client has sent `SuppressOutput { desktop_rect: None }` /// (e.g., mstsc minimized). @@ -956,6 +1024,17 @@ impl RdpServer { dvc }; + #[cfg(feature = "usb")] + let dvc = { + let mut dvc = dvc; + if self.usb_man.is_some() { + dvc = dvc.with_dynamic_channel(UrbdrcControlServer::new(Box::new(UsbControlHandle::new( + self.ev_sender.clone(), + )))); + } + dvc + }; + acceptor.attach_static_channel(dvc); } @@ -1448,6 +1527,222 @@ impl RdpServer { writer.write_all(&data).await?; } }, + #[cfg(feature = "usb")] + ServerEvent::Usb(msg) => match msg { + UrbdrcServerMessage::AddChan => { + let create_dvc_msg = { + use crate::urbdrc::UsbRedirServer; + + let Some(usb_man) = self.usb_man.as_mut() else { + warn!("Missing USB device factory"); + continue; + }; + let Some(drdynvc) = self + .static_channels + .get_by_type_mut::() + .and_then(|svc| svc.channel_processor_downcast_mut::()) + else { + warn!("No drdynvc channel, dropping URBDRC request"); + continue; + }; + let dvc_reservation = drdynvc.reserve_channel(); + let Some(comp_iface) = usb_man.comp_iface_alloc.alloc() else { + warn!("Run out of URBDRC interface IDs"); + continue; + }; + + let dvc_channel_id = dvc_reservation.channel_id(); + let lifecycle = Arc::new(UsbDeviceLifecycle::new()); + let handle = + UsbDeviceHandle::new(self.ev_sender.clone(), dvc_channel_id, Arc::clone(&lifecycle)); + let Some(device_backend) = usb_man.factory.create_device() else { + warn!("Failed to create USB device backend"); + continue; + }; + + let create_dvc_msg = dvc_reservation.create( + drdynvc, + UrbdrcDeviceServer::new( + Box::new(UsbRedirServer::new(device_backend, handle)), + comp_iface, + ) + .expect("interface ID allocated by InterfaceAlloc must be valid"), + )?; + + let device = ServerUsbDevice { + lifecycle, + pending: HashMap::new(), + }; + if usb_man.router.insert(dvc_channel_id, device).is_some() { + warn!(dvc_id = dvc_channel_id, "Replacing USB device pending-request map"); + } + + create_dvc_msg + }; + + let drdynvc_channel_id = self + .get_channel_id_by_type::() + .context("DRDYNVC channel not found")?; + let data = + server_encode_svc_messages(vec![create_dvc_msg], drdynvc_channel_id, user_channel_id)?; + + writer.write_all(&data).await?; + } + UrbdrcServerMessage::Device { dvc_id, dev_msg } => { + let Some(lifecycle) = self + .usb_man + .as_ref() + .and_then(|usb_man| usb_man.router.get(&dvc_id)) + .map(|device| Arc::clone(&device.lifecycle)) + else { + warn!(dvc_id, "Missing USB device state"); + continue; + }; + + // Handle checks are an early rejection for callers. This event-loop check + // is authoritative because a request may already be queued when retract or + // channel close changes the shared lifecycle state. + if !lifecycle.is_open() { + trace!(dvc_id, "Dropping request for closing or closed USB device"); + continue; + } + + let (dvc_msgs, io_reply, close_dev) = { + let Some(drdynvc) = self.get_svc_processor::() else { + warn!("No drdynvc channel, dropping URBDRC request"); + continue; + }; + + let Some(mut dvc) = drdynvc.dvc_by_id_mut::(dvc_id) else { + warn!(dvc_id, "USB dynamic channel ID mismatch"); + continue; + }; + let processor = dvc.processor_mut(); + + match dev_msg { + UrbdrcDeviceServerMessage::QueryDeviceText { text_type, locale_id } => { + (vec![processor.query_device_text(text_type, locale_id)?], None, false) + } + UrbdrcDeviceServerMessage::IoComp { request_id, completion } => { + let Some(usb_man) = self.usb_man.as_mut() else { + warn!("Missing USB device factory"); + continue; + }; + let Some(device) = usb_man.router.get_mut(&dvc_id) else { + warn!(dvc_id, "Missing USB device state"); + continue; + }; + let Some(sender) = device.pending.remove(&request_id) else { + warn!(dvc_id, request_id, "Missing pending USB I/O request"); + continue; + }; + + if sender.send(completion).is_err() { + trace!(dvc_id, request_id, "USB I/O completion receiver dropped"); + } + (Vec::new(), None, false) + } + UrbdrcDeviceServerMessage::IoReq { data, handle, tx } => { + if tx.is_closed() { + continue; + } + + let request = match data { + ServerDeviceIoReq::IoControl(packet) => processor.io_control(packet), + ServerDeviceIoReq::InternalIoControl(packet) => { + processor.internal_io_control(packet) + } + ServerDeviceIoReq::TransferOut(packet) => processor.transfer_out(packet), + ServerDeviceIoReq::TransferIn(packet) => processor.transfer_in(packet), + }?; + + let pending = if request.expects_completion { + let Some(usb_man) = self.usb_man.as_mut() else { + warn!("Missing USB device factory"); + continue; + }; + let Some(device) = usb_man.router.get_mut(&dvc_id) else { + error!(dvc_id, "Missing USB device state"); + continue; + }; + + let (comp_tx, comp_rx) = oneshot::channel(); + if device.pending.insert(request.request_id, comp_tx).is_some() { + warn!( + dvc_id, + request_id = request.request_id, + "Replacing pending USB I/O request" + ); + } + + Some(RawPending { + rx: comp_rx, + id: request.request_id, + handle, + }) + } else { + None + }; + + (vec![request.message], Some((tx, pending)), false) + } + UrbdrcDeviceServerMessage::Retract(reason) => { + let request = processor.retract_device(reason)?; + lifecycle.mark_retracting(); + (vec![request], None, true) + } + UrbdrcDeviceServerMessage::CancelRequest(request_id) => { + let request = processor.cancel_request(request_id)?; + let Some(usb_man) = self.usb_man.as_mut() else { + warn!("Missing USB device factory"); + continue; + }; + let Some(device) = usb_man.router.get_mut(&dvc_id) else { + warn!(dvc_id, "Missing USB device state"); + continue; + }; + + // A completion may have won the race with PendingRequest::drop. + // Only emit CANCEL_REQUEST while the request is still pending. + if device.pending.remove(&request_id).is_none() { + trace!(dvc_id, request_id, "USB I/O request is no longer pending"); + continue; + } + + (vec![request], None, false) + } + } + }; + + let mut messages = dvc::encode_dvc_messages(dvc_id, dvc_msgs, ChannelFlags::SHOW_PROTOCOL)?; + + if close_dev { + let close_message = self + .get_svc_processor::() + .and_then(|drdynvc| drdynvc.close_channel(dvc_id)) + .context("URBDRC dynamic channel disappeared before close")?; + self.remove_usb_device(dvc_id); + messages.push(close_message); + } + + let drdynvc_channel_id = self + .get_channel_id_by_type::() + .context("DRDYNVC channel not found")?; + + let data = server_encode_svc_messages(messages, drdynvc_channel_id, user_channel_id)?; + writer.write_all(&data).await?; + + if let Some((tx, pending)) = io_reply { + if let Err(pending) = tx.send(pending) { + trace!(dvc_id, "USB I/O request receiver dropped"); + drop(pending); + } + } + } + UrbdrcServerMessage::DeviceClosed { dvc_id } => { + self.remove_usb_device(dvc_id); + } + }, #[cfg(feature = "egfx")] ServerEvent::Egfx(msg) => match msg { EgfxServerMessage::SendMessages { messages } => { diff --git a/crates/ironrdp-server/src/urbdrc.rs b/crates/ironrdp-server/src/urbdrc.rs new file mode 100644 index 000000000..6d3062eee --- /dev/null +++ b/crates/ironrdp-server/src/urbdrc.rs @@ -0,0 +1,1444 @@ +use core::{ + marker::PhantomData, + sync::atomic::{AtomicU8, Ordering}, +}; +use std::{ + collections::BTreeMap, + sync::{Arc, Mutex, MutexGuard}, +}; + +use crate::ServerEvent; +use anyhow::Context as _; +use ironrdp_dvc::DynamicChannelId; +use ironrdp_pdu::{PduResult, pdu_other_err}; +use ironrdp_rdpeusb::{ + io::{ + CompletionData, DeviceAnnounce, DeviceText, InternalIoControlPacket, IoControlCompletionResult, + IoControlPacket, RequestId, TransferInCompletionResult, TransferInPacket, TransferOutCompletionResult, + TransferOutPacket, UsbRetractReason, + }, + pdu::{ + completion::ts_urb_result::{TsUrbSelectConfigResult, TsUrbSelectInterfaceResult, TsUsbdInterfaceInfoResult}, + sink::DeviceSpeed, + utils::{ConfigHandle, PipeHandle}, + }, + server::{UrbdrcControlServerBackend, UrbdrcDeviceServerBackend}, +}; +use ironrdp_usb::{ + control::GetDescriptorRequest, + descriptor::{ConfigurationDescriptorSet, InterfaceDescriptor}, + endpoint::EndpointAddress, + transfer::{ + BulkTransferRequest, ControlCompletion, ControlTransferRequest, DataCompletion, FrameNumber, + InterruptTransferRequest, IsoCompletion, IsochronousPacketCompletion, IsochronousTransferRequest, + TransferCompletion, UsbResult, + }, + value::{InterfaceSelection, TransferType, UsbSpeed}, +}; +use tokio::sync::oneshot::error::TryRecvError; +use tokio::sync::{mpsc::UnboundedSender, oneshot}; + +#[derive(Debug)] +pub enum UrbdrcServerMessage { + AddChan, + Device { + dvc_id: u32, + dev_msg: UrbdrcDeviceServerMessage, + }, + DeviceClosed { + dvc_id: DynamicChannelId, + }, +} + +#[derive(Debug)] +pub enum UrbdrcDeviceServerMessage { + QueryDeviceText { + text_type: u32, + locale_id: u32, + }, + IoReq { + data: ServerDeviceIoReq, + handle: UsbDeviceHandle, + /// Sender used to return the pending request after the request is + /// written. `None` when the request expects no completion. + tx: oneshot::Sender>, + }, + IoComp { + request_id: RequestId, + completion: CompletionData, + }, + CancelRequest(RequestId), + Retract(UsbRetractReason), +} + +/// Server-to-client USB I/O request data. +#[derive(Debug)] +pub enum ServerDeviceIoReq { + IoControl(IoControlPacket), + InternalIoControl(InternalIoControlPacket), + TransferIn(TransferInPacket), + TransferOut(TransferOutPacket), +} + +/// Creates per-device URBDRC backends. +pub trait DeviceFactory { + /// Creates the [UrbdrcDeviceServerBackend] for a newly opened device channel. + fn create_device(&mut self) -> Option>; +} + +#[derive(Debug, Clone)] +pub(crate) struct UsbControlHandle { + event_sender: UnboundedSender, +} + +impl UsbControlHandle { + pub(crate) fn new(event_sender: UnboundedSender) -> Self { + Self { event_sender } + } +} + +impl UrbdrcControlServerBackend for UsbControlHandle { + fn create_device_chan(&mut self) -> PduResult<()> { + let _ = self.event_sender.send(ServerEvent::Usb(UrbdrcServerMessage::AddChan)); + Ok(()) + } +} + +/// Handle used by a device backend to send server-to-client requests. +#[derive(Debug, Clone)] +pub struct UsbDeviceHandle { + sender: UnboundedSender, + channel_id: DynamicChannelId, + lifecycle: Arc, + usb_state: Arc>, +} + +impl UsbDeviceHandle { + pub(crate) fn new( + sender: UnboundedSender, + channel_id: DynamicChannelId, + lifecycle: Arc, + ) -> Self { + Self { + sender, + channel_id, + lifecycle, + usb_state: Arc::new(Mutex::new(UsbSharedState::default())), + } + } + + fn send_device_message(&self, dev_msg: UrbdrcDeviceServerMessage) -> anyhow::Result<()> { + self.sender + .send(ServerEvent::Usb(UrbdrcServerMessage::Device { + dvc_id: self.channel_id, + dev_msg, + })) + .map_err(|_error| anyhow::anyhow!("usb device channel is closing or closed")) + } + + fn enqueue_io_message(&self, data: ServerDeviceIoReq) -> anyhow::Result>> { + self.ensure_open()?; + + let (tx, rx) = oneshot::channel(); + self.send_device_message(UrbdrcDeviceServerMessage::IoReq { + data, + handle: self.clone(), + tx, + })?; + + Ok(rx) + } + + async fn send_io_message(&self, data: ServerDeviceIoReq) -> anyhow::Result { + let rx = self.enqueue_io_message(data)?; + resolve_pending(rx).await + } + + /// Sends a transfer-in request and returns its request metadata once written. + async fn transfer_in_request(&self, packet: TransferInPacket) -> anyhow::Result { + self.send_io_message(ServerDeviceIoReq::TransferIn(packet)).await + } + + /// Sends a transfer-out request and returns its request metadata once written. + async fn transfer_out_request(&self, packet: TransferOutPacket) -> anyhow::Result { + self.send_io_message(ServerDeviceIoReq::TransferOut(packet)).await + } + + /// Sends a cancel request for a pending I/O request. + pub(crate) fn cancel_request(&self, request_id: RequestId) -> anyhow::Result<()> { + if !self.lifecycle.is_open() { + return Ok(()); + } + + self.send_device_message(UrbdrcDeviceServerMessage::CancelRequest(request_id)) + } + + /// Sends a query-device-text request. + pub fn query_device_text_request(&self, text_type: u32, locale_id: u32) -> anyhow::Result<()> { + self.ensure_open()?; + self.send_device_message(UrbdrcDeviceServerMessage::QueryDeviceText { text_type, locale_id }) + } + + /// Sends a device-retract request. + pub fn retract_request(&self, reason: UsbRetractReason) -> anyhow::Result<()> { + self.ensure_open()?; + self.send_device_message(UrbdrcDeviceServerMessage::Retract(reason)) + } + + fn ensure_open(&self) -> anyhow::Result<()> { + if self.lifecycle.is_open() { + Ok(()) + } else { + anyhow::bail!("usb device channel is closing or closed") + } + } + + /// Submits a transfer on the default control pipe. + pub async fn control_transfer( + &self, + request: ControlTransferRequest>, + ) -> anyhow::Result>>> { + let transfer = ironrdp_rdpeusb::usb::control_transfer(request.setup, request.data) + .context("failed to translate usb control transfer")?; + let inner = match transfer { + ironrdp_rdpeusb::usb::TransferRequest::In(packet) => self.transfer_in_request(packet).await?, + ironrdp_rdpeusb::usb::TransferRequest::Out(packet) => self.transfer_out_request(packet).await?, + }; + + Ok(PendingRequest::new(inner, PendingOperation::Transfer)) + } + + /// Submits a bulk transfer on an endpoint in the active configuration. + pub async fn bulk_transfer( + &self, + request: BulkTransferRequest>, + ) -> anyhow::Result>>> { + self.submit_data_transfer(request, TransferType::Bulk).await + } + + /// Submits an interrupt transfer on an endpoint in the active configuration. + pub async fn interrupt_transfer( + &self, + request: InterruptTransferRequest>, + ) -> anyhow::Result>>> { + self.submit_data_transfer(request, TransferType::Interrupt).await + } + + /// Submits an isochronous transfer and returns its completion. + pub async fn isochronous_transfer( + &self, + request: IsochronousTransferRequest, Vec>, + ) -> anyhow::Result, Vec>>> { + let packet_lengths = request.packets.clone(); + let rx = { + let state = self.lock_usb_state(); + let pipe = state.resolve_pipe(request.endpoint)?; + anyhow::ensure!( + pipe.transfer_type == TransferType::Isochronous, + "usb endpoint is not isochronous" + ); + let transfer = ironrdp_rdpeusb::usb::isochronous(pipe.handle, request, false) + .context("failed to translate usb isochronous transfer")?; + let data = match transfer { + ironrdp_rdpeusb::usb::TransferRequest::In(packet) => ServerDeviceIoReq::TransferIn(packet), + ironrdp_rdpeusb::usb::TransferRequest::Out(packet) => ServerDeviceIoReq::TransferOut(packet), + }; + self.enqueue_io_message(data)? + }; + let inner = resolve_pending(rx).await?; + + Ok(PendingRequest::new( + inner, + PendingOperation::Isochronous { packet_lengths }, + )) + } + + /// Submits a standard USB `GET_DESCRIPTOR` request. + pub async fn get_descriptor( + &self, + request: GetDescriptorRequest, + ) -> anyhow::Result>>> { + let packet = ironrdp_rdpeusb::usb::get_descriptor(request).context("failed to translate usb get descriptor")?; + let inner = self.transfer_in_request(packet).await?; + + Ok(PendingRequest::new(inner, PendingOperation::GetDescriptor)) + } + + /// Queries the active USB configuration value. + pub async fn get_configuration(&self) -> anyhow::Result>> { + let packet = ironrdp_rdpeusb::usb::get_configuration(); + let inner = self.transfer_in_request(packet).await?; + Ok(PendingRequest::new(inner, PendingOperation::GetConfiguration)) + } + + /// Selects a USB configuration described by `descriptor`. + /// + /// `None` selects configuration zero. `Some` selects the descriptor's + /// `bConfigurationValue`, with alternate setting zero for every interface. + /// The descriptor is validated and used only to build the RDPEUSB request + /// and validate its completion; it is not retained by the handle. + pub async fn select_configuration( + &self, + descriptor: Option>, + ) -> anyhow::Result>> { + // Validation, translation, and plan construction depend only on the + // caller's descriptor and the immutable announced speed, so they are + // deliberately kept outside the state critical section. + let (packet, plan) = match descriptor { + // Unconfiguring does not depend on the announced device speed. + None => ( + ironrdp_rdpeusb::usb::unconfigure(), + ConfigurationSelectionPlan::Unconfigure, + ), + Some(descriptor) => { + descriptor + .validate() + .context("invalid usb configuration selection descriptor")?; + let active_interfaces: Vec = descriptor + .default_interfaces() + .map(|interface| InterfaceSelection { + interface: interface.number(), + alternate_setting: interface.alternate_setting(), + }) + .collect(); + let speed = self.lock_usb_state().speed()?; + let packet = ironrdp_rdpeusb::usb::select_configuration(descriptor, &active_interfaces, speed) + .context("failed to translate usb configuration selection")?; + let plan = ConfigurationSelectionPlan::Configure { + descriptor: descriptor.as_bytes().to_vec(), + active_interfaces, + }; + (packet, plan) + } + }; + + let (rx, transition) = { + let mut state = self.lock_usb_state(); + let transition = state.reserve_transition(StatefulTransitionKind::SelectConfiguration)?; + let rx = self.enqueue_io_message(ServerDeviceIoReq::TransferIn(packet))?; + state.activate_configuration_transition(transition); + (rx, transition) + }; + + let mut submission = TransitionGuard::poison_on_drop(self, transition); + let inner = resolve_pending(rx).await?; + submission.disarm(); + Ok(PendingRequest::new( + inner, + PendingOperation::SelectConfiguration { transition, plan }, + )) + } + + /// Queries the active alternate setting for an interface. + pub async fn get_interface(&self, interface: u8) -> anyhow::Result>> { + let packet = ironrdp_rdpeusb::usb::get_interface(interface); + let inner = self.transfer_in_request(packet).await?; + Ok(PendingRequest::new(inner, PendingOperation::GetInterface)) + } + + /// Selects an alternate setting for an interface. + /// + /// `descriptor` must describe the active configuration. It is validated + /// and used only to build the RDPEUSB request and validate its completion; + /// it is not retained by the handle. + pub async fn select_interface( + &self, + descriptor: ConfigurationDescriptorSet<'_>, + selection: InterfaceSelection, + ) -> anyhow::Result>> { + // Descriptor validation is state-independent and deliberately kept + // outside the state critical section. + descriptor + .validate() + .context("invalid usb interface selection descriptor")?; + + let (rx, transition) = { + let mut state = self.lock_usb_state(); + let speed = state.speed()?; + let (config_handle, interface) = state.interface_plan(descriptor, selection)?; + let transition = state.reserve_transition(StatefulTransitionKind::SelectInterface { + interface: selection.interface, + })?; + let packet = ironrdp_rdpeusb::usb::select_interface(config_handle, interface, speed) + .context("failed to translate usb interface selection")?; + let rx = self.enqueue_io_message(ServerDeviceIoReq::TransferIn(packet))?; + if let Err(error) = state.activate_interface_transition(transition, selection.interface) { + state.binding = BindingState::Unknown; + state.transition = TransitionState::Poisoned; + return Err(error); + } + (rx, transition) + }; + // The plan is only read at completion time; the descriptor copy is + // deliberately made outside the critical section. + let plan = InterfaceSelectionPlan { + selection, + descriptor: descriptor.as_bytes().to_vec(), + }; + + let mut submission = TransitionGuard::poison_on_drop(self, transition); + let inner = resolve_pending(rx).await?; + submission.disarm(); + Ok(PendingRequest::new( + inner, + PendingOperation::SelectInterface { transition, plan }, + )) + } + + /// Clears an endpoint halt and resets its pipe state. + /// + /// The endpoint may use any transfer type: Windows + /// `SYNC_RESET_PIPE_AND_CLEAR_STALL` applies to every non-default pipe. + pub async fn clear_halt(&self, endpoint: EndpointAddress) -> anyhow::Result>> { + let rx = { + let state = self.lock_usb_state(); + let pipe = state.resolve_pipe(endpoint)?; + let packet = ironrdp_rdpeusb::usb::reset_pipe_and_clear_stall(pipe.handle); + self.enqueue_io_message(ServerDeviceIoReq::TransferIn(packet))? + }; + let inner = resolve_pending(rx).await?; + Ok(PendingRequest::new(inner, PendingOperation::ClearHalt)) + } + + /// Resets the redirected USB device. + pub async fn reset_device(&self) -> anyhow::Result>> { + let (rx, transition, previous_binding) = { + let mut state = self.lock_usb_state(); + let transition = state.reserve_transition(StatefulTransitionKind::ResetDevice)?; + let previous_binding = match &state.binding { + BindingState::Unconfigured | BindingState::Configured(_) => state.binding.clone(), + BindingState::Unknown => anyhow::bail!("usb binding state is unknown"), + }; + let packet = ironrdp_rdpeusb::usb::reset_device(); + let rx = self.enqueue_io_message(ServerDeviceIoReq::IoControl(packet))?; + state.binding = BindingState::Unknown; + state.transition = TransitionState::InFlight(transition); + (rx, transition, previous_binding) + }; + + let mut submission = TransitionGuard::poison_on_drop(self, transition); + let inner = resolve_pending(rx).await?; + submission.disarm(); + Ok(PendingRequest::new( + inner, + PendingOperation::ResetDevice { + transition, + previous_binding, + }, + )) + } + + /// Queries the host controller's current USB frame number. + pub async fn current_frame_number(&self) -> anyhow::Result>> { + let packet = ironrdp_rdpeusb::usb::current_frame_number(); + let inner = self.transfer_in_request(packet).await?; + Ok(PendingRequest::new(inner, PendingOperation::CurrentFrameNumber)) + } + + async fn submit_data_transfer( + &self, + request: BulkTransferRequest>, + expected_type: TransferType, + ) -> anyhow::Result>>> { + let rx = { + let state = self.lock_usb_state(); + let pipe = state.resolve_pipe(request.endpoint)?; + anyhow::ensure!( + pipe.transfer_type == expected_type, + "usb endpoint {:#04x} is {:?}, not {:?}", + request.endpoint.raw(), + pipe.transfer_type, + expected_type + ); + let transfer = ironrdp_rdpeusb::usb::bulk_or_interrupt(pipe.handle, request) + .context("failed to translate usb data transfer")?; + let data = match transfer { + ironrdp_rdpeusb::usb::TransferRequest::In(packet) => ServerDeviceIoReq::TransferIn(packet), + ironrdp_rdpeusb::usb::TransferRequest::Out(packet) => ServerDeviceIoReq::TransferOut(packet), + }; + self.enqueue_io_message(data)? + }; + let inner = resolve_pending(rx).await?; + Ok(PendingRequest::new(inner, PendingOperation::Transfer)) + } + + fn initialize_usb_capabilities(&self, device_speed: DeviceSpeed) { + let speed = match device_speed.to_u32() { + value if value == DeviceSpeed::FULL_SPEED.to_u32() => Some(UsbSpeed::Full), + value if value == DeviceSpeed::HIGH_SPEED.to_u32() => Some(UsbSpeed::High), + value => { + tracing::warn!(value, "RDPEUSB device reported an unsupported USB speed"); + None + } + }; + self.lock_usb_state().capabilities = Some(UsbCapabilities { speed }); + } + + fn lock_usb_state(&self) -> MutexGuard<'_, UsbSharedState> { + match self.usb_state.lock() { + Ok(state) => state, + Err(poisoned) => poisoned.into_inner(), + } + } +} + +#[derive(Debug)] +struct UsbSharedState { + capabilities: Option, + binding: BindingState, + transition: TransitionState, + next_generation: u64, +} + +impl Default for UsbSharedState { + fn default() -> Self { + Self { + capabilities: None, + binding: BindingState::Unconfigured, + transition: TransitionState::Idle, + next_generation: 0, + } + } +} + +#[derive(Debug, Clone, Copy)] +struct UsbCapabilities { + speed: Option, +} + +#[derive(Debug, Clone)] +enum BindingState { + Unconfigured, + Configured(ConfigurationBinding), + /// A state-changing request may have invalidated every opaque handle. + Unknown, +} + +#[derive(Debug, Clone)] +struct ConfigurationBinding { + configuration_value: u8, + config_handle: ConfigHandle, + interfaces: BTreeMap, +} + +#[derive(Debug, Clone)] +enum InterfaceBindingState { + Bound(InterfaceBinding), + /// The configuration handle remains usable, but this interface's prior + /// pipe handles must not be reused. + Unknown, +} + +#[derive(Debug, Clone)] +struct InterfaceBinding { + alternate_setting: u8, + pipes: BTreeMap, +} + +#[derive(Debug, Clone, Copy)] +struct PipeBinding { + handle: PipeHandle, + transfer_type: TransferType, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct StatefulTransition { + generation: u64, + kind: StatefulTransitionKind, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum StatefulTransitionKind { + SelectConfiguration, + SelectInterface { interface: u8 }, + ResetDevice, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum TransitionState { + Idle, + InFlight(StatefulTransition), + /// The request was submitted but no terminal completion was observed. + /// CANCEL_REQUEST has no acknowledgement fence, so another state-changing + /// request cannot safely recover this channel. + Poisoned, +} + +#[derive(Debug)] +enum ConfigurationSelectionPlan { + Unconfigure, + Configure { + descriptor: Vec, + active_interfaces: Vec, + }, +} + +#[derive(Debug)] +struct InterfaceSelectionPlan { + selection: InterfaceSelection, + descriptor: Vec, +} + +impl UsbSharedState { + fn speed(&self) -> anyhow::Result { + self.capabilities + .context("usb device capabilities have not been announced")? + .speed + .context("usb device speed is not supported by the facade") + } + + fn reserve_transition(&mut self, kind: StatefulTransitionKind) -> anyhow::Result { + match self.transition { + TransitionState::Idle => {} + TransitionState::InFlight(_) => anyhow::bail!("another usb state transition is in progress"), + TransitionState::Poisoned => { + anyhow::bail!("usb binding state is indeterminate after an abandoned transition") + } + } + + self.next_generation = self + .next_generation + .checked_add(1) + .context("usb state transition generation exhausted")?; + Ok(StatefulTransition { + generation: self.next_generation, + kind, + }) + } + + fn activate_configuration_transition(&mut self, transition: StatefulTransition) { + self.binding = BindingState::Unknown; + self.transition = TransitionState::InFlight(transition); + } + + fn activate_interface_transition(&mut self, transition: StatefulTransition, interface: u8) -> anyhow::Result<()> { + let BindingState::Configured(binding) = &mut self.binding else { + anyhow::bail!("usb device is not configured") + }; + let interface_binding = binding + .interfaces + .get_mut(&interface) + .with_context(|| format!("usb interface {interface} is not active"))?; + *interface_binding = InterfaceBindingState::Unknown; + self.transition = TransitionState::InFlight(transition); + Ok(()) + } + + /// Returns a failed in-flight transition to idle. + /// + /// A transition that is no longer current is left untouched, so the caller + /// can report the original failure without a bookkeeping error masking it. + fn finish_failed_transition(&mut self, transition: StatefulTransition) { + if self.transition == TransitionState::InFlight(transition) { + self.transition = TransitionState::Idle; + } + } + + fn poison_transition(&mut self, transition: StatefulTransition) { + if self.transition == TransitionState::InFlight(transition) { + self.transition = TransitionState::Poisoned; + } + } + + /// Looks up the active pipe binding for `endpoint`. + fn resolve_pipe(&self, endpoint: EndpointAddress) -> anyhow::Result { + if endpoint.is_default_control() { + anyhow::bail!("usb endpoint zero is not a data pipe") + } + let BindingState::Configured(binding) = &self.binding else { + anyhow::bail!("usb device has no usable configuration binding") + }; + + let mut found = None; + for interface in binding.interfaces.values() { + let InterfaceBindingState::Bound(interface) = interface else { + continue; + }; + let Some(pipe) = interface.pipes.get(&endpoint) else { + continue; + }; + if found.replace(*pipe).is_some() { + anyhow::bail!("usb endpoint {:#04x} has multiple active pipe bindings", endpoint.raw()) + } + } + + found.with_context(|| format!("usb endpoint {:#04x} has no active pipe binding", endpoint.raw())) + } + + /// Validates `selection` against the active binding and returns the + /// configuration handle plus the selected interface descriptor. + fn interface_plan<'a>( + &self, + descriptor: ConfigurationDescriptorSet<'a>, + selection: InterfaceSelection, + ) -> anyhow::Result<(ConfigHandle, InterfaceDescriptor<'a>)> { + let BindingState::Configured(binding) = &self.binding else { + anyhow::bail!("usb device is not configured") + }; + anyhow::ensure!( + binding.interfaces.contains_key(&selection.interface), + "usb interface {} is not active", + selection.interface + ); + anyhow::ensure!( + descriptor.configuration().configuration_value() == binding.configuration_value, + "descriptor is not the active usb configuration" + ); + for (&interface_number, interface_binding) in &binding.interfaces { + let InterfaceBindingState::Bound(interface_binding) = interface_binding else { + continue; + }; + anyhow::ensure!( + descriptor + .interface(interface_number, interface_binding.alternate_setting) + .is_some(), + "active usb interface binding does not match its descriptor" + ); + } + let interface = descriptor + .interface(selection.interface, selection.alternate_setting) + .with_context(|| { + format!( + "usb interface {} has no alternate setting {}", + selection.interface, selection.alternate_setting + ) + })?; + + Ok((binding.config_handle, interface)) + } + + fn ensure_current(&self, transition: StatefulTransition) -> anyhow::Result<()> { + anyhow::ensure!( + self.transition == TransitionState::InFlight(transition), + "usb transition is no longer current" + ); + Ok(()) + } + + // The commit methods below poison an in-flight transition themselves when + // its expected binding shape has been lost: without an acknowledgement + // fence for CANCEL_REQUEST the binding cannot be recovered, so the channel + // must fail closed. + + /// Commits a whole-device binding change (configuration selection or + /// device reset). + fn commit_binding(&mut self, transition: StatefulTransition, binding: BindingState) -> anyhow::Result<()> { + self.ensure_current(transition)?; + if !matches!(self.binding, BindingState::Unknown) { + self.transition = TransitionState::Poisoned; + anyhow::bail!("usb binding changed during the transition") + } + self.binding = binding; + self.transition = TransitionState::Idle; + Ok(()) + } + + fn commit_interface( + &mut self, + transition: StatefulTransition, + interface_number: u8, + interface: InterfaceBinding, + ) -> anyhow::Result<()> { + self.ensure_current(transition)?; + let BindingState::Configured(binding) = &mut self.binding else { + self.transition = TransitionState::Poisoned; + anyhow::bail!("usb configuration binding disappeared during interface selection") + }; + if !matches!( + binding.interfaces.get(&interface_number), + Some(InterfaceBindingState::Unknown) + ) { + self.transition = TransitionState::Poisoned; + anyhow::bail!("usb interface binding is no longer current") + } + binding + .interfaces + .insert(interface_number, InterfaceBindingState::Bound(interface)); + self.transition = TransitionState::Idle; + Ok(()) + } +} + +/// Drop guard for a stateful transition still owned by an in-progress +/// operation. +/// +/// Unless disarmed, dropping the guard applies its exit state to a +/// still-current transition: [`Self::poison_on_drop`] fails the channel closed +/// when a submitted request loses its wait path, while [`Self::finish_on_drop`] +/// returns the transition to idle on completion paths that did not commit. +struct TransitionGuard<'a> { + handle: &'a UsbDeviceHandle, + transition: StatefulTransition, + poison: bool, + armed: bool, +} + +impl<'a> TransitionGuard<'a> { + fn poison_on_drop(handle: &'a UsbDeviceHandle, transition: StatefulTransition) -> Self { + Self { + handle, + transition, + poison: true, + armed: true, + } + } + + fn finish_on_drop(handle: &'a UsbDeviceHandle, transition: StatefulTransition) -> Self { + Self { + handle, + transition, + poison: false, + armed: true, + } + } + + /// Hands transition bookkeeping over to the caller. + fn disarm(&mut self) { + self.armed = false; + } +} + +impl Drop for TransitionGuard<'_> { + fn drop(&mut self) { + if !self.armed { + return; + } + let mut state = self.handle.lock_usb_state(); + if self.poison { + state.poison_transition(self.transition); + } else { + state.finish_failed_transition(self.transition); + } + } +} + +/// Resolves the server's submission reply into pending-request metadata. +async fn resolve_pending(rx: oneshot::Receiver>) -> anyhow::Result { + match rx.await { + Ok(Some(pending)) => Ok(pending), + // The public facade only submits acknowledged requests, so a + // completion-less (NoAck) reply is an internal contract violation. + Ok(None) => Err(anyhow::anyhow!("usb request unexpectedly has no pending completion")), + Err(_) => Err(anyhow::anyhow!("usb device channel is closing or closed")), + } +} + +fn configuration_binding_from_result( + plan: &ConfigurationSelectionPlan, + result: TsUrbSelectConfigResult, +) -> anyhow::Result { + let ConfigurationSelectionPlan::Configure { + descriptor: descriptor_bytes, + active_interfaces, + } = plan + else { + anyhow::ensure!( + result.interface.is_empty(), + "usb unconfigure returned {} interface bindings", + result.interface.len() + ); + return Ok(BindingState::Unconfigured); + }; + // The plan bytes were validated at submission, so a parse failure here is + // a facade bug rather than caller or client input. + let descriptor = + ConfigurationDescriptorSet::parse(descriptor_bytes).context("stored usb configuration descriptor")?; + anyhow::ensure!( + result.interface.len() == active_interfaces.len(), + "usb selection result returned {} interfaces, expected {}", + result.interface.len(), + active_interfaces.len() + ); + + let expected = active_interfaces + .iter() + .map(|selection| (selection.interface, *selection)) + .collect::>(); + let mut interfaces = BTreeMap::new(); + for interface_result in result.interface { + let selection = expected.get(&interface_result.interface_number).with_context(|| { + format!( + "usb selection result returned unexpected interface {}", + interface_result.interface_number + ) + })?; + let interface = descriptor + .interface(selection.interface, selection.alternate_setting) + .context("selected usb interface descriptor is absent")?; + let number = interface_result.interface_number; + let binding = interface_binding_from_result(interface, interface_result)?; + anyhow::ensure!( + interfaces + .insert(number, InterfaceBindingState::Bound(binding)) + .is_none(), + "usb selection result returned duplicate interface {number}" + ); + } + + Ok(BindingState::Configured(ConfigurationBinding { + configuration_value: descriptor.configuration().configuration_value(), + config_handle: result.config_handle, + interfaces, + })) +} + +fn interface_binding_from_selection_result( + plan: &InterfaceSelectionPlan, + result: TsUrbSelectInterfaceResult, +) -> anyhow::Result { + // The plan bytes and selection were validated at submission. + let descriptor = + ConfigurationDescriptorSet::parse(&plan.descriptor).context("stored usb configuration descriptor")?; + let interface = descriptor + .interface(plan.selection.interface, plan.selection.alternate_setting) + .context("selected usb interface descriptor is absent")?; + interface_binding_from_result(interface, result.interface) +} + +fn interface_binding_from_result( + descriptor: InterfaceDescriptor<'_>, + result: TsUsbdInterfaceInfoResult, +) -> anyhow::Result { + anyhow::ensure!( + result.interface_number == descriptor.number() && result.alternate_setting == descriptor.alternate_setting(), + "usb selection result identifies interface {} alternate {}, expected interface {} alternate {}", + result.interface_number, + result.alternate_setting, + descriptor.number(), + descriptor.alternate_setting() + ); + let endpoint_count = descriptor.endpoints().count(); + anyhow::ensure!( + result.pipes.len() == endpoint_count, + "usb selection result returned {} pipes, expected {endpoint_count}", + result.pipes.len() + ); + + let mut pipes = BTreeMap::new(); + for result_pipe in result.pipes { + let endpoint = EndpointAddress::from_raw(result_pipe.endpoint_address) + .context("usb selection result contains an invalid endpoint")?; + let endpoint_descriptor = descriptor.endpoint(endpoint).with_context(|| { + format!( + "usb selection result returned unexpected endpoint {:#04x}", + endpoint.raw() + ) + })?; + anyhow::ensure!( + pipes + .insert( + endpoint, + PipeBinding { + handle: result_pipe.pipe_handle, + transfer_type: endpoint_descriptor.transfer_type(), + }, + ) + .is_none(), + "usb selection result returned duplicate endpoint {:#04x}", + endpoint.raw() + ); + } + + Ok(InterfaceBinding { + alternate_setting: descriptor.alternate_setting(), + pipes, + }) +} + +#[derive(Debug)] +enum PendingOperation { + GetDescriptor, + Transfer, + Isochronous { + packet_lengths: Vec, + }, + GetConfiguration, + GetInterface, + SelectConfiguration { + transition: StatefulTransition, + plan: ConfigurationSelectionPlan, + }, + SelectInterface { + transition: StatefulTransition, + plan: InterfaceSelectionPlan, + }, + ClearHalt, + ResetDevice { + transition: StatefulTransition, + previous_binding: BindingState, + }, + CurrentFrameNumber, +} + +/// A submitted USB request awaiting its typed completion. +/// +/// Dropping a request before its completion is available requests +/// cancellation while the device channel remains open. +#[derive(Debug)] +#[must_use = "dropping a pending USB request cancels it"] +pub struct PendingRequest { + inner: RawPending, + operation: Option, + _marker: PhantomData, +} + +pub struct PendingHandle { + usb_handle: UsbDeviceHandle, + req_id: u32, +} + +impl PendingHandle { + pub fn cancel(self) { + let _ = self.usb_handle.cancel_request(self.req_id); + } +} + +impl PendingRequest { + fn new(inner: RawPending, operation: PendingOperation) -> Self { + Self { + inner, + operation: Some(operation), + _marker: PhantomData, + } + } + + pub fn pending_handle(&self) -> PendingHandle { + PendingHandle { + usb_handle: self.inner.handle.clone(), + req_id: self.inner.id, + } + } + + fn take_operation(&mut self) -> anyhow::Result { + self.operation.take().context("usb pending request already completed") + } +} + +impl Drop for PendingRequest { + fn drop(&mut self) { + if let Some(operation) = self.operation.take() { + operation.abandon(&self.inner.handle); + } + } +} + +impl PendingOperation { + fn abandon(self, handle: &UsbDeviceHandle) { + let transition = match self { + Self::SelectConfiguration { transition, .. } + | Self::SelectInterface { transition, .. } + | Self::ResetDevice { transition, .. } => transition, + Self::GetDescriptor + | Self::Transfer + | Self::Isochronous { .. } + | Self::GetConfiguration + | Self::GetInterface + | Self::ClearHalt + | Self::CurrentFrameNumber => return, + }; + handle.lock_usb_state().poison_transition(transition); + } +} + +impl PendingRequest>> { + /// Waits for a `GET_DESCRIPTOR` completion. + /// + /// USB operation failures are represented by the inner [`UsbResult`]; the + /// outer error covers cancellation, channel closure, and completion-shape + /// failures. + pub async fn wait(mut self) -> anyhow::Result>> { + let completion = self.inner.recv().await?; + let PendingOperation::GetDescriptor = self.take_operation()? else { + anyhow::bail!("usb pending operation does not produce a descriptor completion") + }; + ironrdp_rdpeusb::usb::get_descriptor_completion(completion).context("malformed usb get descriptor completion") + } +} + +impl PendingRequest>> { + /// Waits for a control, bulk, or interrupt transfer completion. + pub async fn wait(mut self) -> anyhow::Result>> { + let completion = self.inner.recv().await?; + let PendingOperation::Transfer = self.take_operation()? else { + anyhow::bail!("usb pending operation does not produce a transfer completion") + }; + ironrdp_rdpeusb::usb::transfer_completion(completion).context("malformed usb transfer completion") + } +} + +impl PendingRequest> { + /// Waits for a one-byte USB state query completion. + pub async fn wait(mut self) -> anyhow::Result> { + let completion = self.inner.recv().await?; + match self.take_operation()? { + PendingOperation::GetConfiguration => ironrdp_rdpeusb::usb::get_configuration_completion(completion) + .context("malformed usb get configuration completion"), + PendingOperation::GetInterface => ironrdp_rdpeusb::usb::get_interface_completion(completion) + .context("malformed usb get interface completion"), + _ => anyhow::bail!("usb pending operation does not produce a one-byte completion"), + } + } +} + +impl PendingRequest> { + /// Waits for a state-changing or pipe-operation completion. + pub async fn wait(mut self) -> anyhow::Result> { + let completion = self.inner.recv().await?; + match self.take_operation()? { + PendingOperation::SelectConfiguration { transition, plan } => { + let mut finish = TransitionGuard::finish_on_drop(&self.inner.handle, transition); + let completion = ironrdp_rdpeusb::usb::select_configuration_completion(completion) + .context("malformed usb select configuration completion")?; + let result = match completion { + Ok(result) => result, + Err(usb_error) => return Ok(Err(usb_error)), + }; + let binding = configuration_binding_from_result(&plan, result)?; + finish.disarm(); + self.inner.handle.lock_usb_state().commit_binding(transition, binding)?; + Ok(Ok(())) + } + PendingOperation::SelectInterface { transition, plan } => { + let mut finish = TransitionGuard::finish_on_drop(&self.inner.handle, transition); + let completion = ironrdp_rdpeusb::usb::select_interface_completion(completion) + .context("malformed usb select interface completion")?; + let result = match completion { + Ok(result) => result, + Err(usb_error) => return Ok(Err(usb_error)), + }; + let binding = interface_binding_from_selection_result(&plan, result)?; + finish.disarm(); + self.inner + .handle + .lock_usb_state() + .commit_interface(transition, plan.selection.interface, binding)?; + Ok(Ok(())) + } + PendingOperation::ClearHalt => { + ironrdp_rdpeusb::usb::pipe_request_completion(completion).context("malformed usb clear halt completion") + } + PendingOperation::ResetDevice { + transition, + previous_binding, + } => { + let mut finish = TransitionGuard::finish_on_drop(&self.inner.handle, transition); + let completion = ironrdp_rdpeusb::usb::reset_device_completion(completion) + .context("malformed usb reset device completion")?; + if let Err(usb_error) = completion { + return Ok(Err(usb_error)); + } + finish.disarm(); + self.inner + .handle + .lock_usb_state() + .commit_binding(transition, previous_binding)?; + Ok(Ok(())) + } + _ => anyhow::bail!("usb pending operation does not produce a unit completion"), + } + } +} + +impl PendingRequest> { + /// Waits for a current-frame-number completion. + pub async fn wait(mut self) -> anyhow::Result> { + let completion = self.inner.recv().await?; + let PendingOperation::CurrentFrameNumber = self.take_operation()? else { + anyhow::bail!("usb pending operation does not produce a frame-number completion") + }; + ironrdp_rdpeusb::usb::current_frame_number_completion(completion) + .context("malformed usb current frame number completion") + } +} + +impl PendingRequest, Vec>> { + /// Waits for an acknowledged isochronous transfer completion. + pub async fn wait(mut self) -> anyhow::Result, Vec>> { + let completion = self.inner.recv().await?; + let PendingOperation::Isochronous { packet_lengths } = self.take_operation()? else { + anyhow::bail!("usb pending operation does not produce an isochronous completion") + }; + ironrdp_rdpeusb::usb::isochronous_completion(completion, &packet_lengths) + .context("malformed usb isochronous completion") + } +} + +#[derive(Debug)] +pub struct RawPending { + pub(super) rx: oneshot::Receiver, + pub(super) id: RequestId, + pub(super) handle: UsbDeviceHandle, +} + +impl Drop for RawPending { + fn drop(&mut self) { + if matches!(self.rx.try_recv(), Err(TryRecvError::Empty)) && self.handle.lifecycle.is_open() { + let _ = self.handle.cancel_request(self.id); + } + } +} + +impl RawPending { + async fn recv(&mut self) -> anyhow::Result { + match (&mut self.rx).await { + Ok(completion) => Ok(completion), + // The sender was dropped without delivering a completion: either a + // local cancel won the race against it, or the device channel was + // torn down. The distinction is currently observable only through + // the message text; a typed server error is planned to restore it. + Err(_) if self.handle.lifecycle.is_open() => { + Err(anyhow::anyhow!("usb request was cancelled before a completion")) + } + Err(_) => Err(anyhow::anyhow!("usb device channel is closing or closed")), + } + } + + pub async fn wait(mut self) -> anyhow::Result { + self.recv().await + } + + pub fn cancel(self) { + drop(self); + } +} + +pub(crate) struct UsbRedirServer { + handle: UsbDeviceHandle, + device: Box, +} + +pub trait UsbRedirDevice: Send { + /// Called when the client announces the device with `ADD_DEVICE`. + fn device_added(&mut self, info: RdpUsbDeviceAnnounceInfo); + + fn device_text(&mut self, device_text: DeviceText); + + /// Called when the redirected USB device channel is closed. + /// + /// This is invoked exactly once on every teardown path (client-initiated + /// channel close, server-initiated retract, and connection teardown) and is + /// the final callback: no other method is called afterwards. + /// + /// Runs on the server task; implementations must not block and should only + /// enqueue or spawn follow-up work. + fn close(&mut self) {} +} + +#[derive(Debug)] +pub struct RdpUsbDeviceAnnounceInfo { + pub announce: DeviceAnnounce, + pub usb_handle: UsbDeviceHandle, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(u8)] +enum UsbDeviceState { + Open, + Retracting, + Closed, +} + +impl UsbDeviceState { + const fn code(self) -> u8 { + match self { + Self::Open => 0, + Self::Retracting => 1, + Self::Closed => 2, + } + } +} + +#[derive(Debug)] +pub(crate) struct UsbDeviceLifecycle { + state: AtomicU8, +} + +impl UsbDeviceLifecycle { + pub(crate) fn new() -> Self { + Self { + state: AtomicU8::new(UsbDeviceState::Open.code()), + } + } + + pub(crate) fn is_open(&self) -> bool { + self.state.load(Ordering::Acquire) == UsbDeviceState::Open.code() + } + + pub(crate) fn mark_retracting(&self) { + let _ = self.state.compare_exchange( + UsbDeviceState::Open.code(), + UsbDeviceState::Retracting.code(), + Ordering::AcqRel, + Ordering::Acquire, + ); + } + + pub(crate) fn mark_closed(&self) { + self.state.store(UsbDeviceState::Closed.code(), Ordering::Release); + } +} + +impl UsbRedirServer { + pub(crate) fn new(device: Box, handle: UsbDeviceHandle) -> Self { + Self { handle, device } + } + + fn send_io_completion(&self, request_id: RequestId, completion: CompletionData) -> PduResult<()> { + self.handle + .send_device_message(UrbdrcDeviceServerMessage::IoComp { request_id, completion }) + .map_err(|_| pdu_other_err!("failed to send usb I/O completion")) + } +} + +impl UrbdrcDeviceServerBackend for UsbRedirServer { + fn add_device(&mut self, device: DeviceAnnounce) -> PduResult<()> { + self.handle + .initialize_usb_capabilities(device.usb_device_caps.device_speed); + self.device.device_added(RdpUsbDeviceAnnounceInfo { + announce: device, + usb_handle: self.handle.clone(), + }); + Ok(()) + } + + fn device_text(&mut self, device_text: DeviceText) { + self.device.device_text(device_text); + } + + fn io_control_completed( + &mut self, + _channel_id: u32, + request_id: RequestId, + completion: IoControlCompletionResult, + ) -> PduResult<()> { + self.send_io_completion(request_id, CompletionData::IoControl(completion)) + } + + fn internal_io_control_completed( + &mut self, + _channel_id: u32, + request_id: RequestId, + completion: IoControlCompletionResult, + ) -> PduResult<()> { + self.send_io_completion(request_id, CompletionData::InternalIoControl(completion)) + } + + fn transfer_in_completed( + &mut self, + _channel_id: u32, + request_id: RequestId, + completion: TransferInCompletionResult, + ) -> PduResult<()> { + self.send_io_completion(request_id, CompletionData::TransferIn(completion)) + } + + fn transfer_out_completed( + &mut self, + _channel_id: u32, + request_id: RequestId, + completion: TransferOutCompletionResult, + ) -> PduResult<()> { + self.send_io_completion(request_id, CompletionData::TransferOut(completion)) + } + + fn close(&mut self, channel_id: u32) { + self.handle.lifecycle.mark_closed(); + let _ = self + .handle + .sender + .send(ServerEvent::Usb(UrbdrcServerMessage::DeviceClosed { + dvc_id: channel_id, + })); + self.device.close(); + } +} + +#[cfg(test)] +mod tests { + use ironrdp_rdpeusb::pdu::completion::ts_urb_result::{ + TsUsbdInterfaceInfoResult, TsUsbdPipeInfoResult, UsbdPipeType, + }; + use ironrdp_usb::{descriptor::ConfigurationDescriptorSet, endpoint::EndpointAddress, value::TransferType}; + + use super::interface_binding_from_result; + + const CONFIGURATION: [u8; 32] = [ + 9, 2, 32, 0, 1, 1, 0, 0x80, 50, // configuration + 9, 4, 0, 0, 2, 8, 6, 0x50, 0, // interface 0, alternate 0 + 7, 5, 0x01, 2, 64, 0, 0, // bulk OUT endpoint 1 + 7, 5, 0x82, 2, 64, 0, 0, // bulk IN endpoint 2 + ]; + + #[test] + fn pipe_bindings_use_descriptor_semantics() { + let binding = interface_binding_from_result( + interface(), + interface_result(vec![ + pipe_result(0x01, UsbdPipeType::Interrupt, 101), + pipe_result(0x82, UsbdPipeType::Isochronous, 102), + ]), + ) + .unwrap(); + + let out = binding.pipes.get(&EndpointAddress::from_raw(0x01).unwrap()).unwrap(); + assert_eq!(out.handle, 101); + assert_eq!(out.transfer_type, TransferType::Bulk); + let input = binding.pipes.get(&EndpointAddress::from_raw(0x82).unwrap()).unwrap(); + assert_eq!(input.handle, 102); + assert_eq!(input.transfer_type, TransferType::Bulk); + } + + #[test] + fn interface_identity_must_match_selection() { + let mut result = interface_result(vec![ + pipe_result(0x01, UsbdPipeType::Bulk, 101), + pipe_result(0x82, UsbdPipeType::Bulk, 102), + ]); + result.alternate_setting = 1; + + let error = interface_binding_from_result(interface(), result).unwrap_err(); + assert!(error.to_string().contains("identifies interface 0 alternate 1")); + } + + #[test] + fn duplicate_endpoint_bindings_are_rejected() { + let result = interface_result(vec![ + pipe_result(0x01, UsbdPipeType::Bulk, 101), + pipe_result(0x01, UsbdPipeType::Bulk, 102), + ]); + + let error = interface_binding_from_result(interface(), result).unwrap_err(); + assert!(error.to_string().contains("duplicate endpoint 0x01")); + } + + fn interface() -> ironrdp_usb::descriptor::InterfaceDescriptor<'static> { + ConfigurationDescriptorSet::parse(&CONFIGURATION) + .unwrap() + .interface(0, 0) + .unwrap() + } + + fn interface_result(pipes: Vec) -> TsUsbdInterfaceInfoResult { + TsUsbdInterfaceInfoResult { + interface_number: 0, + alternate_setting: 0, + class: 0xff, + sub_class: 0xff, + protocol: 0xff, + interface_handle: 7, + pipes, + } + } + + fn pipe_result(endpoint_address: u8, pipe_type: UsbdPipeType, pipe_handle: u32) -> TsUsbdPipeInfoResult { + TsUsbdPipeInfoResult { + max_packet_size: 1, + endpoint_address, + interval: 0xff, + pipe_type, + pipe_handle, + max_transfer_size: 1, + pipe_flags: u32::MAX, + } + } +} diff --git a/crates/ironrdp-testsuite-core/Cargo.toml b/crates/ironrdp-testsuite-core/Cargo.toml index 883ec3757..171ee603b 100644 --- a/crates/ironrdp-testsuite-core/Cargo.toml +++ b/crates/ironrdp-testsuite-core/Cargo.toml @@ -54,6 +54,7 @@ ironrdp-input.path = "../ironrdp-input" ironrdp-rdcleanpath.path = "../ironrdp-rdcleanpath" ironrdp-rdpdr.path = "../ironrdp-rdpdr" ironrdp-rdpeusb.path = "../ironrdp-rdpeusb" +ironrdp-usb.path = "../ironrdp-usb" ironrdp-rdpsnd = { path = "../ironrdp-rdpsnd", features = ["__test"] } ironrdp-server.path = "../ironrdp-server" ironrdp-session = { path = "../ironrdp-session", features = ["qoi"] } diff --git a/crates/ironrdp-testsuite-core/tests/rdpeusb/mod.rs b/crates/ironrdp-testsuite-core/tests/rdpeusb/mod.rs index 823cab487..d01ebae46 100644 --- a/crates/ironrdp-testsuite-core/tests/rdpeusb/mod.rs +++ b/crates/ironrdp-testsuite-core/tests/rdpeusb/mod.rs @@ -51,3 +51,4 @@ mod io; mod server; mod sink; mod ts_urb; +mod usb; diff --git a/crates/ironrdp-testsuite-core/tests/rdpeusb/usb.rs b/crates/ironrdp-testsuite-core/tests/rdpeusb/usb.rs new file mode 100644 index 000000000..c0c863e40 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/rdpeusb/usb.rs @@ -0,0 +1,1087 @@ +use ironrdp_rdpeusb::{ + io::{ + CompletionData, IoControlCompletionResult, TransferInCompletionResult, TransferOutCompletionResult, + TsUrbInKind, TsUrbOutKind, UrbFunction, + }, + pdu::{ + completion::ts_urb_result::{ + TsUrbGetCurrFrameNumResult, TsUrbIsochTransferResult, TsUrbResult, TsUrbResultHeader, TsUrbResultPayload, + TsUrbSelectConfigResult, TsUrbSelectInterfaceResult, TsUsbdInterfaceInfoResult, + }, + usb_dev::IoctlInternalUsb, + utils::UsbdIsoPacketDesc, + }, + usb::{ + CompletionError, ConversionError, TransferRequest, bulk_or_interrupt, bulk_or_interrupt_in, + bulk_or_interrupt_out, control_transfer, current_frame_number, current_frame_number_completion, + get_configuration, get_configuration_completion, get_descriptor, get_descriptor_completion, get_interface, + get_interface_completion, isochronous, isochronous_completion, pipe_request_completion, reset_device, + reset_device_completion, reset_pipe_and_clear_stall, select_configuration, select_configuration_completion, + select_interface, select_interface_completion, transfer_completion, unconfigure, + }, +}; +use ironrdp_usb::{ + control::{GetDescriptorRequest, Recipient, RequestKind, RequestType, SetupPacket, standard_request}, + descriptor::{ConfigurationDescriptorSet, descriptor_type}, + endpoint::EndpointAddress, + transfer::{ + DataTransferRequest, IsochronousPacketCompletion, IsochronousTransferOutput, IsochronousTransferRequest, + TransferCompletion, UsbError, + }, + value::{Direction, InterfaceSelection, UsbSpeed}, +}; + +const USBD_TRANSFER_DIRECTION_IN: u32 = 0x0000_0001; +const USBD_SHORT_TRANSFER_OK: u32 = 0x0000_0002; +const USBD_DEFAULT_PIPE_TRANSFER: u32 = 0x0000_0008; +const USBD_START_ISO_TRANSFER_ASAP: u32 = 0x0000_0004; +const DEFAULT_CONTROL_PIPE_HANDLE: u32 = 0; + +const USBD_STATUS_CANCELED: u32 = 0xc001_0000; +const USBD_STATUS_STALL_PID: u32 = 0xc000_0004; +const USBD_STATUS_BABBLE_DETECTED: u32 = 0xc000_0012; +const USBD_STATUS_TIMEOUT: u32 = 0xc000_6000; +const USBD_STATUS_DEVICE_GONE: u32 = 0xc000_7000; + +const CONFIGURATION: [u8; 57] = [ + 9, 2, 57, 0, 2, 1, 0, 0x80, 50, // configuration + 9, 4, 0, 0, 2, 0xff, 0, 0, 0, // interface 0, alternate 0 + 7, 5, 0x81, 3, 0x40, 0x10, 1, // high-bandwidth interrupt IN + 7, 5, 0x02, 2, 0x00, 0x02, 0, // bulk OUT + 9, 4, 1, 0, 0, 0xff, 0, 0, 0, // interface 1, alternate 0 + 9, 4, 1, 1, 1, 0xff, 0, 0, 0, // interface 1, alternate 1 + 7, 5, 0x83, 3, 32, 0, 4, // interrupt IN +]; + +fn setup( + direction: Direction, + kind: RequestKind, + recipient: Recipient, + request: u8, + value: u16, + index: u16, + length: u16, +) -> SetupPacket { + SetupPacket { + request_type: RequestType::new(direction, kind, recipient), + request, + value, + index, + length, + } +} + +fn selection(interface: u8, alternate_setting: u8) -> InterfaceSelection { + InterfaceSelection { + interface, + alternate_setting, + } +} + +#[test] +fn feature_out_uses_rdpeusb_transfer_in() { + let setup = setup( + Direction::Out, + RequestKind::STANDARD, + Recipient::ENDPOINT, + standard_request::SET_FEATURE, + 0, + 0x81, + 0, + ); + + let TransferRequest::In(request) = control_transfer(setup, Vec::new()).unwrap() else { + panic!("feature request used TRANSFER_OUT_REQUEST"); + }; + assert_eq!(request.output_buffer_size, 0); + assert_eq!(request.ts_urb.func, UrbFunction::URB_FUNCTION_SET_FEATURE_TO_ENDPOINT); + let TsUrbInKind::CtlFeatReq(urb) = request.ts_urb.kind else { + panic!("feature request used the wrong TS_URB variant"); + }; + assert_eq!(urb.feat_selector, 0); + assert_eq!(urb.index, 0x81); +} + +#[test] +fn descriptor_request_preserves_typed_fields() { + let setup = setup( + Direction::In, + RequestKind::STANDARD, + Recipient::INTERFACE, + standard_request::GET_DESCRIPTOR, + 0x2203, + 0x0409, + 64, + ); + + let TransferRequest::In(request) = control_transfer(setup, Vec::new()).unwrap() else { + panic!("GET_DESCRIPTOR used TRANSFER_OUT_REQUEST"); + }; + assert_eq!(request.output_buffer_size, 64); + assert_eq!( + request.ts_urb.func, + UrbFunction::URB_FUNCTION_GET_DESCRIPTOR_FROM_INTERFACE + ); + let TsUrbInKind::CtlDescReq(urb) = request.ts_urb.kind else { + panic!("GET_DESCRIPTOR used the wrong TS_URB variant"); + }; + assert_eq!(urb.index, 3); + assert_eq!(urb.desc_type, 0x22); + assert_eq!(urb.lang_id, 0x0409); +} + +#[test] +fn typed_get_descriptor_rejects_an_unrepresentable_recipient() { + let error = get_descriptor(GetDescriptorRequest { + recipient: Recipient::VENDOR_SPECIFIC, + descriptor_type: descriptor_type::DEVICE, + descriptor_index: 0, + index: 0, + requested_length: 18, + }) + .unwrap_err(); + + assert_eq!( + error, + ConversionError::UnsupportedDescriptorRecipient { + recipient: Recipient::VENDOR_SPECIFIC + } + ); +} + +#[test] +fn get_descriptor_completion_returns_usb_data() { + let output = vec![18, descriptor_type::DEVICE, 0, 2]; + + assert_eq!( + get_descriptor_completion(transfer_in_completion(0, 0, output.clone())).unwrap(), + Ok(output.clone()) + ); + assert_eq!( + get_descriptor_completion(transfer_in_completion(1, 0, output.clone())).unwrap(), + Ok(output) + ); + assert_eq!( + get_descriptor_completion(transfer_in_completion(0, 0, Vec::new())).unwrap(), + Ok(Vec::new()) + ); +} + +#[test] +fn get_descriptor_completion_maps_usb_failures() { + for (usbd_status, expected) in [ + (USBD_STATUS_CANCELED, UsbError::Cancelled), + (USBD_STATUS_STALL_PID, UsbError::Stall), + (USBD_STATUS_BABBLE_DETECTED, UsbError::Overflow), + (USBD_STATUS_TIMEOUT, UsbError::Timeout), + (USBD_STATUS_DEVICE_GONE, UsbError::NoDevice), + (0xdead_beef, UsbError::Error), + ] { + assert_eq!( + get_descriptor_completion(transfer_in_completion(0, usbd_status, vec![1, 2, 3])).unwrap(), + Err(expected) + ); + } + + assert_eq!( + get_descriptor_completion(transfer_in_completion(0x8000_4005, 0, vec![1, 2, 3])).unwrap(), + Err(UsbError::Error) + ); +} + +#[test] +fn get_descriptor_completion_rejects_a_mismatched_result() { + let result = TsUrbResult { + header: TsUrbResultHeader { usbd_status: 0 }, + payload: None, + }; + assert_eq!( + get_descriptor_completion(CompletionData::TransferOut(TransferOutCompletionResult { + ts_urb_result: result, + hresult: 0, + output_buffer_size: 0, + })) + .unwrap_err(), + CompletionError::ExpectedTransferIn + ); + + let result = TsUrbResult { + header: TsUrbResultHeader { usbd_status: 0 }, + payload: Some(TsUrbResultPayload::FrameNum(TsUrbGetCurrFrameNumResult { + frame_number: 42, + })), + }; + assert_eq!( + get_descriptor_completion(CompletionData::TransferIn(TransferInCompletionResult { + ts_urb_result: result, + hresult: 0, + output_buffer: Vec::new(), + })) + .unwrap_err(), + CompletionError::UnexpectedUrbResultPayload + ); +} + +fn transfer_in_completion(hresult: u32, usbd_status: u32, output_buffer: Vec) -> CompletionData { + CompletionData::TransferIn(TransferInCompletionResult { + ts_urb_result: TsUrbResult { + header: TsUrbResultHeader { usbd_status }, + payload: None, + }, + hresult, + output_buffer, + }) +} + +#[test] +fn class_in_accepts_short_control_responses() { + let setup = SetupPacket { + request_type: RequestType::new(Direction::In, RequestKind::CLASS, Recipient::INTERFACE), + request: 0x81, + value: 0x0200, + index: 3, + length: 8, + }; + + let TransferRequest::In(request) = control_transfer(setup, Vec::new()).unwrap() else { + panic!("class IN request used TRANSFER_OUT_REQUEST"); + }; + let TsUrbInKind::VendorClassReq(urb) = request.ts_urb.kind else { + panic!("class request used the wrong TS_URB variant"); + }; + assert_eq!(urb.transfer_flags, USBD_TRANSFER_DIRECTION_IN | USBD_SHORT_TRANSFER_OK); +} + +#[test] +fn unknown_recipient_falls_back_without_losing_setup_fields() { + let setup = SetupPacket { + request_type: RequestType::new(Direction::In, RequestKind::VENDOR, Recipient::VENDOR_SPECIFIC), + request: 0xa5, + value: 0x1234, + index: 0x5678, + length: 17, + }; + + let TransferRequest::In(request) = control_transfer(setup, Vec::new()).unwrap() else { + panic!("vendor IN request used TRANSFER_OUT_REQUEST"); + }; + assert_eq!(request.output_buffer_size, 17); + assert_eq!(request.ts_urb.func, UrbFunction::URB_FUNCTION_CONTROL_TRANSFER); + let TsUrbInKind::CtlTransfer(urb) = request.ts_urb.kind else { + panic!("unknown recipient did not use generic control transfer"); + }; + assert_eq!(urb.pipe, DEFAULT_CONTROL_PIPE_HANDLE); + assert_eq!( + urb.transfer_flags, + USBD_TRANSFER_DIRECTION_IN | USBD_SHORT_TRANSFER_OK | USBD_DEFAULT_PIPE_TRANSFER + ); + assert_eq!(urb.setup_packet.request_type, setup.request_type.raw()); + assert_eq!(urb.setup_packet.request, setup.request); + assert_eq!(urb.setup_packet.value, setup.value); + assert_eq!(urb.setup_packet.index, setup.index); + assert_eq!(urb.setup_packet.length, setup.length); +} + +#[test] +fn noncanonical_typed_requests_fall_back_without_losing_fields() { + let requests = [ + setup( + Direction::In, + RequestKind::STANDARD, + Recipient::DEVICE, + standard_request::GET_STATUS, + 1, + 0, + 2, + ), + setup( + Direction::In, + RequestKind::STANDARD, + Recipient::DEVICE, + standard_request::GET_CONFIGURATION, + 0, + 1, + 1, + ), + setup( + Direction::In, + RequestKind::STANDARD, + Recipient::INTERFACE, + standard_request::GET_INTERFACE, + 1, + 3, + 1, + ), + setup( + Direction::In, + RequestKind::STANDARD, + Recipient::ENDPOINT, + standard_request::CLEAR_FEATURE, + 0, + 0x81, + 0, + ), + ]; + + for setup in requests { + let TransferRequest::In(request) = control_transfer(setup, Vec::new()).unwrap() else { + panic!("noncanonical USB IN request used TRANSFER_OUT_REQUEST"); + }; + let TsUrbInKind::CtlTransfer(urb) = request.ts_urb.kind else { + panic!("noncanonical request lost fields in a typed TS_URB"); + }; + assert_eq!(urb.setup_packet.request_type, setup.request_type.raw()); + assert_eq!(urb.setup_packet.request, setup.request); + assert_eq!(urb.setup_packet.value, setup.value); + assert_eq!(urb.setup_packet.index, setup.index); + assert_eq!(urb.setup_packet.length, setup.length); + } +} + +#[test] +fn stateful_standard_requests_do_not_use_generic_control() { + let requests = [ + setup( + Direction::Out, + RequestKind::STANDARD, + Recipient::DEVICE, + standard_request::SET_ADDRESS, + 5, + 0, + 0, + ), + setup( + Direction::Out, + RequestKind::STANDARD, + Recipient::DEVICE, + standard_request::SET_CONFIGURATION, + 1, + 0, + 0, + ), + setup( + Direction::Out, + RequestKind::STANDARD, + Recipient::INTERFACE, + standard_request::SET_INTERFACE, + 1, + 0, + 0, + ), + ]; + + for setup in requests { + assert_eq!( + control_transfer(setup, Vec::new()).unwrap_err(), + ConversionError::StatefulStandardRequest { request: setup.request } + ); + } +} + +#[test] +fn control_data_shape_is_validated_before_translation() { + let input = setup( + Direction::In, + RequestKind::STANDARD, + Recipient::DEVICE, + standard_request::GET_STATUS, + 0, + 0, + 2, + ); + assert_eq!( + control_transfer(input, vec![0]).unwrap_err(), + ConversionError::InTransferHasData { actual: 1 } + ); + + let output = SetupPacket { + request_type: RequestType::new(Direction::Out, RequestKind::VENDOR, Recipient::DEVICE), + request: 1, + value: 0, + index: 0, + length: 2, + }; + assert_eq!( + control_transfer(output, vec![0]).unwrap_err(), + ConversionError::OutTransferLengthMismatch { expected: 2, actual: 1 } + ); +} + +#[test] +fn bulk_builders_own_direction_and_buffer_shape() { + let input = bulk_or_interrupt_in(7, 4096); + assert_eq!(input.output_buffer_size, 4096); + let TsUrbInKind::BulkInterruptTransfer(urb) = input.ts_urb.kind else { + panic!("bulk IN used the wrong TS_URB variant"); + }; + assert_eq!(urb.pipe_handle, 7); + assert_eq!(urb.transfer_flags, USBD_TRANSFER_DIRECTION_IN | USBD_SHORT_TRANSFER_OK); + + let output = bulk_or_interrupt_out(9, vec![1, 2, 3]); + assert_eq!(output.output_buffer, [1, 2, 3]); + assert!(!output.ts_urb.no_ack); + let TsUrbOutKind::BulkInterruptTransfer(urb) = output.ts_urb.kind else { + panic!("bulk OUT used the wrong TS_URB variant"); + }; + assert_eq!(urb.pipe_handle, 9); + assert_eq!(urb.transfer_flags, 0); +} + +#[test] +fn general_bulk_or_interrupt_validates_directional_data() { + let endpoint = EndpointAddress::from_raw(0x81).unwrap(); + let TransferRequest::In(input) = bulk_or_interrupt( + 7, + DataTransferRequest { + endpoint, + length: 512, + data: Vec::new(), + }, + ) + .unwrap() else { + panic!("bulk IN used TRANSFER_OUT_REQUEST"); + }; + assert_eq!(input.output_buffer_size, 512); + + assert_eq!( + bulk_or_interrupt( + 7, + DataTransferRequest { + endpoint, + length: 1, + data: vec![1], + }, + ) + .unwrap_err(), + ConversionError::InTransferHasData { actual: 1 } + ); + + let endpoint = EndpointAddress::from_raw(0x02).unwrap(); + assert_eq!( + bulk_or_interrupt( + 9, + DataTransferRequest { + endpoint, + length: 3, + data: vec![1, 2], + }, + ) + .unwrap_err(), + ConversionError::OutTransferLengthMismatch { expected: 3, actual: 2 } + ); +} + +#[test] +fn general_transfer_completion_preserves_directional_results() { + assert_eq!( + transfer_completion(transfer_in_completion(0, 0, vec![1, 2, 3])).unwrap(), + TransferCompletion { + status: Ok(()), + actual_length: 3, + data: vec![1, 2, 3] + } + ); + + assert_eq!( + transfer_completion(CompletionData::TransferOut(TransferOutCompletionResult { + ts_urb_result: TsUrbResult { + header: TsUrbResultHeader { + usbd_status: USBD_STATUS_STALL_PID, + }, + payload: None, + }, + hresult: 0, + output_buffer_size: 17, + })) + .unwrap(), + TransferCompletion { + status: Err(UsbError::Stall), + actual_length: 17, + data: Vec::new() + } + ); +} + +#[test] +fn configuration_and_interface_queries_use_typed_urbs() { + let request = get_configuration(); + assert_eq!(request.output_buffer_size, 1); + assert_eq!(request.ts_urb.func, UrbFunction::URB_FUNCTION_GET_CONFIGURATION); + assert!(matches!(request.ts_urb.kind, TsUrbInKind::CtlGetConfig(_))); + assert_eq!( + get_configuration_completion(transfer_in_completion(0, 0, vec![3])).unwrap(), + Ok(3) + ); + + let request = get_interface(5); + assert_eq!(request.output_buffer_size, 1); + assert_eq!(request.ts_urb.func, UrbFunction::URB_FUNCTION_GET_INTERFACE); + let TsUrbInKind::CtlGetIface(urb) = request.ts_urb.kind else { + panic!("GET_INTERFACE used the wrong TS_URB variant"); + }; + assert_eq!(urb.interface, 5); + assert_eq!( + get_interface_completion(transfer_in_completion(0, 0, vec![2])).unwrap(), + Ok(2) + ); + assert_eq!( + get_interface_completion(transfer_in_completion(0, USBD_STATUS_STALL_PID, Vec::new())).unwrap(), + Err(UsbError::Stall) + ); + assert_eq!( + get_configuration_completion(transfer_in_completion(0, 0, Vec::new())).unwrap_err(), + CompletionError::ExpectedOneByteOutput { actual: 0 } + ); +} + +#[test] +fn isochronous_builder_uses_prefix_offsets_and_no_ack_only_for_out() { + let output_endpoint = EndpointAddress::from_raw(0x02).unwrap(); + let TransferRequest::Out(output) = isochronous( + 19, + IsochronousTransferRequest { + endpoint: output_endpoint, + start_frame: None, + data: vec![1, 2, 3, 4, 5, 6], + packets: vec![2, 1, 3], + }, + true, + ) + .unwrap() else { + panic!("isochronous OUT used TRANSFER_IN_REQUEST"); + }; + assert!(output.ts_urb.no_ack); + let TsUrbOutKind::IsochTransfer(urb) = output.ts_urb.kind else { + panic!("isochronous OUT used the wrong TS_URB variant"); + }; + assert_eq!(urb.pipe_handle, 19); + assert_eq!(urb.transfer_flags, USBD_START_ISO_TRANSFER_ASAP); + assert_eq!(urb.start_frame, 0); + assert_eq!( + urb.iso_packet + .iter() + .map(|packet| (packet.offset, packet.length, packet.status)) + .collect::>(), + [(0, 0, 0), (2, 0, 0), (3, 0, 0)] + ); + + let input_endpoint = EndpointAddress::from_raw(0x82).unwrap(); + assert_eq!( + isochronous( + 20, + IsochronousTransferRequest { + endpoint: input_endpoint, + start_frame: Some(42), + data: Vec::new(), + packets: vec![4], + }, + true, + ) + .unwrap_err(), + ConversionError::NoAckIsochronousIn + ); + let TransferRequest::In(input) = isochronous( + 20, + IsochronousTransferRequest { + endpoint: input_endpoint, + start_frame: Some(42), + data: Vec::new(), + packets: vec![4, 8], + }, + false, + ) + .unwrap() else { + panic!("isochronous IN used TRANSFER_OUT_REQUEST"); + }; + assert_eq!(input.output_buffer_size, 12); + let TsUrbInKind::IsochTransfer(urb) = input.ts_urb.kind else { + panic!("isochronous IN used the wrong TS_URB variant"); + }; + assert_eq!(urb.transfer_flags, USBD_TRANSFER_DIRECTION_IN); + assert_eq!(urb.start_frame, 42); +} + +#[test] +fn isochronous_completion_validates_and_unpacks_packet_results() { + let stall = i32::from_ne_bytes(USBD_STATUS_STALL_PID.to_ne_bytes()); + let completion = CompletionData::TransferIn(TransferInCompletionResult { + ts_urb_result: TsUrbResult { + header: TsUrbResultHeader { usbd_status: 0 }, + payload: Some(TsUrbResultPayload::Isoch(TsUrbIsochTransferResult { + start_frame: 101, + error_count: 1, + iso_packet: vec![ + UsbdIsoPacketDesc { + offset: 0, + length: 3, + status: 0, + }, + UsbdIsoPacketDesc { + offset: 3, + length: 0, + status: stall, + }, + UsbdIsoPacketDesc { + offset: 3, + length: 2, + status: 0, + }, + ], + })), + }, + hresult: 0, + // Failed packet data is omitted by RDPEUSB and successful packet data + // is packed in packet order. + output_buffer: vec![1, 2, 3, 8, 9], + }); + + assert_eq!( + isochronous_completion(completion, &[4, 3, 5]).unwrap(), + Ok(IsochronousTransferOutput { + start_frame: 101, + actual_length: 5, + data: vec![1, 2, 3, 8, 9], + packets: vec![ + IsochronousPacketCompletion { + status: Ok(()), + actual_length: 3, + }, + IsochronousPacketCompletion { + status: Err(UsbError::Stall), + actual_length: 0, + }, + IsochronousPacketCompletion { + status: Ok(()), + actual_length: 2, + }, + ], + }) + ); +} + +#[test] +fn isochronous_completion_rejects_inconsistent_input_data() { + let completion = |output_buffer| { + CompletionData::TransferIn(TransferInCompletionResult { + ts_urb_result: TsUrbResult { + header: TsUrbResultHeader { usbd_status: 0 }, + payload: Some(TsUrbResultPayload::Isoch(TsUrbIsochTransferResult { + start_frame: 1, + error_count: 0, + iso_packet: vec![UsbdIsoPacketDesc { + offset: 17, + length: 2, + status: 0, + }], + })), + }, + hresult: 0, + output_buffer, + }) + }; + + assert_eq!( + isochronous_completion(completion(vec![1]), &[4]).unwrap_err(), + CompletionError::IsochronousDataLengthMismatch { expected: 2, actual: 1 } + ); +} + +#[test] +fn isochronous_completion_preserves_overall_failure_without_packet_validation() { + let completion = CompletionData::TransferIn(TransferInCompletionResult { + ts_urb_result: TsUrbResult { + header: TsUrbResultHeader { + usbd_status: USBD_STATUS_STALL_PID, + }, + payload: Some(TsUrbResultPayload::Isoch(TsUrbIsochTransferResult { + start_frame: 0, + error_count: 0, + iso_packet: Vec::new(), + })), + }, + hresult: 0, + output_buffer: Vec::new(), + }); + + assert_eq!( + isochronous_completion(completion, &[4, 4]).unwrap(), + Err(UsbError::Stall) + ); +} + +#[test] +fn isochronous_out_completion_uses_the_envelope_actual_length() { + let stall = i32::from_ne_bytes(USBD_STATUS_STALL_PID.to_ne_bytes()); + let completion = CompletionData::TransferOut(TransferOutCompletionResult { + ts_urb_result: TsUrbResult { + header: TsUrbResultHeader { usbd_status: 0 }, + payload: Some(TsUrbResultPayload::Isoch(TsUrbIsochTransferResult { + start_frame: 101, + error_count: 1, + iso_packet: vec![ + UsbdIsoPacketDesc { + offset: 0, + length: 4, + status: 0, + }, + UsbdIsoPacketDesc { + offset: 4, + length: 0, + status: stall, + }, + ], + })), + }, + hresult: 0, + output_buffer_size: 7, + }); + + assert_eq!( + isochronous_completion(completion, &[4, 3]).unwrap(), + Ok(IsochronousTransferOutput { + start_frame: 101, + actual_length: 7, + data: Vec::new(), + packets: vec![ + IsochronousPacketCompletion { + status: Ok(()), + actual_length: 4, + }, + IsochronousPacketCompletion { + status: Err(UsbError::Stall), + actual_length: 0, + }, + ], + }) + ); +} + +#[test] +fn select_configuration_carries_full_descriptor_and_selected_interfaces() { + let descriptor = ConfigurationDescriptorSet::parse(&CONFIGURATION).unwrap(); + descriptor.validate().unwrap(); + let selections = [selection(0, 0), selection(1, 1)]; + + let request = select_configuration(descriptor, &selections, UsbSpeed::High).unwrap(); + assert_eq!(request.output_buffer_size, 0); + assert_eq!(request.ts_urb.func, UrbFunction::URB_FUNCTION_SELECT_CONFIGURATION); + let TsUrbInKind::SelectConfig(urb) = request.ts_urb.kind else { + panic!("selection used the wrong TS_URB variant"); + }; + let config = urb.desc.expect("configuration descriptor is present"); + assert_eq!(config.total_length, CONFIGURATION.len() as u16); + assert_eq!(config.trailing, CONFIGURATION[9..]); + assert_eq!(urb.usbd_ifaces.len(), 2); + assert_eq!(urb.usbd_ifaces[0].interface_number, 0); + assert_eq!(urb.usbd_ifaces[0].alternate_setting, 0); + assert_eq!(urb.usbd_ifaces[0].ts_usbd_pipe_info.len(), 2); + assert_eq!(urb.usbd_ifaces[0].ts_usbd_pipe_info[0].max_packet_size, 192); + assert_eq!(urb.usbd_ifaces[0].ts_usbd_pipe_info[1].max_packet_size, 512); + assert_eq!(urb.usbd_ifaces[1].interface_number, 1); + assert_eq!(urb.usbd_ifaces[1].alternate_setting, 1); +} + +#[test] +fn select_interface_uses_the_resolved_interface_descriptor() { + let descriptor = ConfigurationDescriptorSet::parse(&CONFIGURATION).unwrap(); + let interface = descriptor.interface(1, 1).unwrap(); + + let request = select_interface(42, interface, UsbSpeed::High).unwrap(); + assert_eq!(request.output_buffer_size, 0); + assert_eq!(request.ts_urb.func, UrbFunction::URB_FUNCTION_SELECT_INTERFACE); + let TsUrbInKind::SelectIface(urb) = request.ts_urb.kind else { + panic!("interface selection used the wrong TS_URB variant"); + }; + assert_eq!(urb.config_handle, 42); + assert_eq!(urb.usbd_iface.interface_number, 1); + assert_eq!(urb.usbd_iface.alternate_setting, 1); + assert_eq!(urb.usbd_iface.ts_usbd_pipe_info[0].max_packet_size, 32); +} + +#[test] +fn selection_completions_return_opaque_rdpeusb_results() { + let configuration_result = TsUrbSelectConfigResult { + config_handle: 42, + interface: Vec::new(), + }; + let completion = CompletionData::TransferIn(TransferInCompletionResult { + ts_urb_result: TsUrbResult { + header: TsUrbResultHeader { usbd_status: 0 }, + payload: Some(TsUrbResultPayload::SelectConfig(configuration_result.clone())), + }, + hresult: 0, + output_buffer: Vec::new(), + }); + assert_eq!( + select_configuration_completion(completion).unwrap(), + Ok(configuration_result) + ); + + let interface_result = TsUrbSelectInterfaceResult { + interface: TsUsbdInterfaceInfoResult { + interface_number: 3, + alternate_setting: 1, + class: 0xff, + sub_class: 0, + protocol: 0, + interface_handle: 77, + pipes: Vec::new(), + }, + }; + let completion = CompletionData::TransferIn(TransferInCompletionResult { + ts_urb_result: TsUrbResult { + header: TsUrbResultHeader { usbd_status: 0 }, + payload: Some(TsUrbResultPayload::SelectIface(interface_result.clone())), + }, + hresult: 0, + output_buffer: Vec::new(), + }); + assert_eq!(select_interface_completion(completion).unwrap(), Ok(interface_result)); +} + +#[test] +fn pipe_frame_and_reset_helpers_use_the_required_rdpeusb_envelopes() { + let request = reset_pipe_and_clear_stall(55); + assert_eq!(request.output_buffer_size, 0); + assert_eq!( + request.ts_urb.func, + UrbFunction::URB_FUNCTION_SYNC_RESET_PIPE_AND_CLEAR_STALL + ); + let TsUrbInKind::PipeReq(urb) = request.ts_urb.kind else { + panic!("pipe reset used the wrong TS_URB variant"); + }; + assert_eq!(urb.pipe_handle, 55); + assert_eq!( + pipe_request_completion(transfer_in_completion(0, 0, Vec::new())).unwrap(), + Ok(()) + ); + + let request = current_frame_number(); + assert_eq!(request.output_buffer_size, 0); + assert_eq!(request.ts_urb.func, UrbFunction::URB_FUNCTION_GET_CURRENT_FRAME_NUMBER); + assert!(matches!(request.ts_urb.kind, TsUrbInKind::GetCurFrameNum(_))); + let completion = CompletionData::TransferIn(TransferInCompletionResult { + ts_urb_result: TsUrbResult { + header: TsUrbResultHeader { usbd_status: 0 }, + payload: Some(TsUrbResultPayload::FrameNum(TsUrbGetCurrFrameNumResult { + frame_number: 1234, + })), + }, + hresult: 0, + output_buffer: Vec::new(), + }); + assert_eq!(current_frame_number_completion(completion).unwrap(), Ok(1234)); + + let request = reset_device(); + assert_eq!(request.ioctl_code, IoctlInternalUsb::ResetPort); + assert!(request.input_buffer.is_empty()); + assert_eq!(request.output_buffer_size, 0); + assert_eq!( + reset_device_completion(CompletionData::IoControl(IoControlCompletionResult { + hresult: 0, + information: 0, + output_buffer: Vec::new(), + })) + .unwrap(), + Ok(()) + ); +} + +#[test] +fn selection_rejects_inconsistent_interface_sets() { + let descriptor = ConfigurationDescriptorSet::parse(&CONFIGURATION).unwrap(); + assert_eq!( + select_configuration(descriptor, &[selection(0, 0), selection(0, 0)], UsbSpeed::High).unwrap_err(), + ConversionError::DuplicateInterface { interface: 0 } + ); + assert_eq!( + select_configuration(descriptor, &[selection(0, 0)], UsbSpeed::High).unwrap_err(), + ConversionError::MissingInterfaceSelection { interface: 1 } + ); + let missing = selection(7, 0); + assert_eq!( + select_configuration(descriptor, &[missing], UsbSpeed::Full).unwrap_err(), + ConversionError::InterfaceNotFound { selection: missing } + ); +} + +#[test] +fn unconfigure_has_an_empty_transfer_in_request() { + let request = unconfigure(); + assert_eq!(request.output_buffer_size, 0); + let TsUrbInKind::SelectConfig(urb) = request.ts_urb.kind else { + panic!("unconfigure used the wrong TS_URB variant"); + }; + assert!(urb.desc.is_none()); + assert!(urb.usbd_ifaces.is_empty()); +} + +#[test] +fn selection_rejects_reserved_high_bandwidth_encoding() { + let mut bytes = CONFIGURATION; + bytes[22] = 0x40; + bytes[23] = 0x18; + let descriptor = ConfigurationDescriptorSet::parse(&bytes).unwrap(); + + assert_eq!( + select_configuration(descriptor, &[selection(0, 0), selection(1, 1)], UsbSpeed::High).unwrap_err(), + ConversionError::InvalidMaximumPacketSize { + selection: selection(0, 0), + raw: 0x1840 + } + ); +} + +#[test] +fn selection_rejects_high_bandwidth_bits_at_other_speeds() { + let mut bytes = CONFIGURATION; + bytes[22] = 0x40; + bytes[23] = 0x08; + let descriptor = ConfigurationDescriptorSet::parse(&bytes).unwrap(); + + assert_eq!( + select_configuration(descriptor, &[selection(0, 0), selection(1, 1)], UsbSpeed::Full).unwrap_err(), + ConversionError::InvalidMaximumPacketSize { + selection: selection(0, 0), + raw: 0x0840 + } + ); +} + +#[test] +fn selection_enforces_usb2_speed_dependent_packet_sizes() { + let mut full_speed_bulk = CONFIGURATION; + full_speed_bulk[22] = 64; + full_speed_bulk[23] = 0; + full_speed_bulk[29] = 64; + full_speed_bulk[30] = 0; + let descriptor = ConfigurationDescriptorSet::parse(&full_speed_bulk).unwrap(); + select_configuration(descriptor, &[selection(0, 0), selection(1, 1)], UsbSpeed::Full).unwrap(); + + let mut invalid_full_speed_bulk = full_speed_bulk; + invalid_full_speed_bulk[29] = 0; + invalid_full_speed_bulk[30] = 2; + let descriptor = ConfigurationDescriptorSet::parse(&invalid_full_speed_bulk).unwrap(); + assert_eq!( + select_configuration(descriptor, &[selection(0, 0), selection(1, 1)], UsbSpeed::Full).unwrap_err(), + ConversionError::InvalidMaximumPacketSize { + selection: selection(0, 0), + raw: 512 + } + ); + + let mut invalid_high_speed_bulk = CONFIGURATION; + invalid_high_speed_bulk[29] = 64; + invalid_high_speed_bulk[30] = 0; + let descriptor = ConfigurationDescriptorSet::parse(&invalid_high_speed_bulk).unwrap(); + assert_eq!( + select_configuration(descriptor, &[selection(0, 0), selection(1, 1)], UsbSpeed::High).unwrap_err(), + ConversionError::InvalidMaximumPacketSize { + selection: selection(0, 0), + raw: 64 + } + ); +} + +#[test] +fn selection_enforces_default_interface_zero_isochronous_bandwidth() { + let mut zero_bandwidth = CONFIGURATION; + zero_bandwidth[21] = 1; + zero_bandwidth[22] = 0; + zero_bandwidth[23] = 0; + let descriptor = ConfigurationDescriptorSet::parse(&zero_bandwidth).unwrap(); + let request = select_configuration(descriptor, &[selection(0, 0), selection(1, 1)], UsbSpeed::High).unwrap(); + let TsUrbInKind::SelectConfig(urb) = request.ts_urb.kind else { + panic!("configuration selection used the wrong TS_URB variant"); + }; + assert_eq!(urb.usbd_ifaces[0].ts_usbd_pipe_info[0].max_packet_size, 0); + + let mut nonzero_bandwidth = zero_bandwidth; + nonzero_bandwidth[22] = 1; + let descriptor = ConfigurationDescriptorSet::parse(&nonzero_bandwidth).unwrap(); + assert_eq!( + select_configuration(descriptor, &[selection(0, 0), selection(1, 1)], UsbSpeed::High).unwrap_err(), + ConversionError::InvalidMaximumPacketSize { + selection: selection(0, 0), + raw: 1 + } + ); +} + +#[test] +fn selection_rejects_descriptor_count_mismatches() { + let mut interface_count = CONFIGURATION; + interface_count[4] = 3; + let descriptor = ConfigurationDescriptorSet::parse(&interface_count).unwrap(); + assert_eq!( + select_configuration(descriptor, &[selection(0, 0), selection(1, 1)], UsbSpeed::Full).unwrap_err(), + ConversionError::InterfaceCountMismatch { declared: 3, actual: 2 } + ); + + let mut endpoint_count = CONFIGURATION; + endpoint_count[13] = 1; + let descriptor = ConfigurationDescriptorSet::parse(&endpoint_count).unwrap(); + assert_eq!( + select_configuration(descriptor, &[selection(0, 0), selection(1, 1)], UsbSpeed::Full).unwrap_err(), + ConversionError::EndpointCountMismatch { + selection: selection(0, 0), + declared: 1, + actual: 2 + } + ); +} + +#[test] +fn selection_rejects_more_than_thirty_pipes() { + let total_length = 9 + 9 + 31 * 7; + let mut bytes = vec![ + 9, + 2, + total_length as u8, + (total_length >> 8) as u8, + 1, + 1, + 0, + 0x80, + 50, + 9, + 4, + 0, + 0, + 31, + 0xff, + 0, + 0, + 0, + ]; + for endpoint in 0..31 { + bytes.extend_from_slice(&[7, 5, (endpoint % 15 + 1) as u8, 2, 64, 0, 0]); + } + let descriptor = ConfigurationDescriptorSet::parse(&bytes).unwrap(); + + assert_eq!( + select_configuration(descriptor, &[selection(0, 0)], UsbSpeed::Full).unwrap_err(), + ConversionError::TooManyPipes { + selection: selection(0, 0), + actual: 31 + } + ); +} + +#[test] +fn selection_rejects_more_than_thirty_active_pipes() { + let total_length = 9 + 2 * 9 + 32 * 7; + let mut bytes = vec![9, 2, total_length as u8, (total_length >> 8) as u8, 2, 1, 0, 0x80, 50]; + for interface in 0..2 { + bytes.extend_from_slice(&[9, 4, interface, 0, 16, 0xff, 0, 0, 0]); + for endpoint in 0..16 { + bytes.extend_from_slice(&[7, 5, endpoint % 15 + 1, 2, 64, 0, 0]); + } + } + let descriptor = ConfigurationDescriptorSet::parse(&bytes).unwrap(); + + assert_eq!( + select_configuration(descriptor, &[selection(0, 0), selection(1, 0)], UsbSpeed::Full).unwrap_err(), + ConversionError::TooManyActivePipes { actual: 32 } + ); +} diff --git a/crates/ironrdp-usb/Cargo.toml b/crates/ironrdp-usb/Cargo.toml new file mode 100644 index 000000000..feee5f02f --- /dev/null +++ b/crates/ironrdp-usb/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "ironrdp-usb" +version = "0.1.0" +readme = "README.md" +description = "Protocol-independent USB data structures and semantics" +edition.workspace = true +rust-version = "1.94" +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lints] +workspace = true diff --git a/crates/ironrdp-usb/README.md b/crates/ironrdp-usb/README.md new file mode 100644 index 000000000..f55de85bc --- /dev/null +++ b/crates/ironrdp-usb/README.md @@ -0,0 +1,10 @@ +# IronRDP USB + +Protocol-independent USB data structures and sans-I/O semantics for IronRDP. + +This foundational crate is intended to provide USB-standard data models, parsing, validation, and query operations shared by protocol adapters. +It does not perform device I/O or define RDPEUSB-, usbredir-, or runtime-specific behavior. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-usb/src/control.rs b/crates/ironrdp-usb/src/control.rs new file mode 100644 index 000000000..3d4e84412 --- /dev/null +++ b/crates/ironrdp-usb/src/control.rs @@ -0,0 +1,295 @@ +//! USB control-request setup packets. + +use core::fmt; + +use super::descriptor::descriptor_type; +use super::value::Direction; + +/// Type field in `bmRequestType` bits 6..5. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct RequestKind(u8); + +impl RequestKind { + pub const STANDARD: Self = Self(0); + pub const CLASS: Self = Self(1); + pub const VENDOR: Self = Self(2); + /// Value reserved by USB 2.0 and USB 3.2. + pub const RESERVED: Self = Self(3); + + /// Construct a request kind from its unshifted two-bit value. + #[must_use] + pub const fn from_raw(raw: u8) -> Option { + if raw <= 0x03 { Some(Self(raw)) } else { None } + } + + #[must_use] + pub const fn raw(self) -> u8 { + self.0 + } + + const fn bits(self) -> u8 { + self.0 << 5 + } +} + +/// Recipient field in `bmRequestType` bits 4..0. +/// +/// Values not defined by the common USB 2.0/3.x core set are preserved rather +/// than rejected. In particular, recipient 31 is vendor-specific in USB 3.2 +/// but reserved by USB 2.0. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct Recipient(u8); + +impl Recipient { + pub const DEVICE: Self = Self(0); + pub const INTERFACE: Self = Self(1); + pub const ENDPOINT: Self = Self(2); + pub const OTHER: Self = Self(3); + /// USB 3.x vendor-specific recipient (value 31); reserved by USB 2.0. + pub const VENDOR_SPECIFIC: Self = Self(31); + + /// Construct a recipient from its five-bit field value. + #[must_use] + pub const fn from_raw(raw: u8) -> Option { + if raw <= 0x1f { Some(Self(raw)) } else { None } + } + + #[must_use] + pub const fn raw(self) -> u8 { + self.0 + } +} + +/// Complete USB `bmRequestType` byte. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct RequestType(u8); + +impl RequestType { + const DIRECTION_IN: u8 = 0x80; + + #[must_use] + pub const fn from_raw(raw: u8) -> Self { + Self(raw) + } + + #[must_use] + pub const fn new(direction: Direction, kind: RequestKind, recipient: Recipient) -> Self { + let direction = match direction { + Direction::Out => 0, + Direction::In => Self::DIRECTION_IN, + }; + Self(direction | kind.bits() | recipient.raw()) + } + + #[must_use] + pub const fn raw(self) -> u8 { + self.0 + } + + #[must_use] + pub const fn direction(self) -> Direction { + if self.0 & Self::DIRECTION_IN == 0 { + Direction::Out + } else { + Direction::In + } + } + + #[must_use] + pub const fn kind(self) -> RequestKind { + RequestKind((self.0 >> 5) & 0x03) + } + + #[must_use] + pub const fn recipient(self) -> Recipient { + Recipient(self.0 & 0x1f) + } +} + +/// Standard request codes from the USB device framework. +/// +/// These values have this meaning only when [`RequestType::kind`] is +/// [`RequestKind::STANDARD`]. Class and vendor requests own their request-code +/// namespaces independently. +pub mod standard_request { + pub const GET_STATUS: u8 = 0; + pub const CLEAR_FEATURE: u8 = 1; + pub const SET_FEATURE: u8 = 3; + pub const SET_ADDRESS: u8 = 5; + pub const GET_DESCRIPTOR: u8 = 6; + pub const SET_DESCRIPTOR: u8 = 7; + pub const GET_CONFIGURATION: u8 = 8; + pub const SET_CONFIGURATION: u8 = 9; + pub const GET_INTERFACE: u8 = 10; + pub const SET_INTERFACE: u8 = 11; + pub const SYNCH_FRAME: u8 = 12; + /// USB 3.x system-exit-latency parameters. + pub const SET_SEL: u8 = 0x30; + /// USB 3.x isochronous delay from host transmission to device receipt. + pub const SET_ISOCHRONOUS_DELAY: u8 = 0x31; +} + +/// A USB control-transfer setup packet (exactly eight bytes on the wire). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct SetupPacket { + pub request_type: RequestType, + pub request: u8, + pub value: u16, + pub index: u16, + pub length: u16, +} + +impl SetupPacket { + pub const WIRE_SIZE: usize = 8; + + #[must_use] + pub const fn from_bytes(bytes: [u8; Self::WIRE_SIZE]) -> Self { + Self { + request_type: RequestType::from_raw(bytes[0]), + request: bytes[1], + value: u16::from_le_bytes([bytes[2], bytes[3]]), + index: u16::from_le_bytes([bytes[4], bytes[5]]), + length: u16::from_le_bytes([bytes[6], bytes[7]]), + } + } + + pub fn parse(bytes: &[u8]) -> Result { + let bytes: [u8; Self::WIRE_SIZE] = bytes.try_into().map_err(|_| SetupPacketError { actual: bytes.len() })?; + Ok(Self::from_bytes(bytes)) + } + + #[must_use] + pub const fn to_bytes(self) -> [u8; Self::WIRE_SIZE] { + let value = self.value.to_le_bytes(); + let index = self.index.to_le_bytes(); + let length = self.length.to_le_bytes(); + [ + self.request_type.raw(), + self.request, + value[0], + value[1], + index[0], + index[1], + length[0], + length[1], + ] + } + + /// Decode `bRequest` in the standard-request namespace. + #[must_use] + pub const fn standard_request(self) -> Option { + if self.request_type.kind().raw() == RequestKind::STANDARD.raw() { + Some(self.request) + } else { + None + } + } +} + +/// A slice whose length is not one USB setup packet. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SetupPacketError { + actual: usize, +} + +impl SetupPacketError { + #[must_use] + pub const fn actual_length(self) -> usize { + self.actual + } +} + +impl fmt::Display for SetupPacketError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "USB setup packet is {} bytes; expected {}", + self.actual, + SetupPacket::WIRE_SIZE + ) + } +} + +impl core::error::Error for SetupPacketError {} + +/// Typed view of the fields shared by standard GET_DESCRIPTOR requests. +/// +/// This view intentionally preserves the recipient and `wIndex`. Class +/// specifications can issue GET_DESCRIPTOR to an interface, and only string +/// descriptors universally interpret `wIndex` as a LANGID. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct GetDescriptorRequest { + /// Recipient encoded in `bmRequestType`. + pub recipient: Recipient, + /// Descriptor-type byte from the high byte of `wValue`. + pub descriptor_type: u8, + /// Descriptor index from the low byte of `wValue`. + pub descriptor_index: u8, + /// Raw `wIndex`; this is a LANGID for string descriptors. + pub index: u16, + /// Maximum response length from `wLength`. + pub requested_length: u16, +} + +impl GetDescriptorRequest { + #[must_use] + pub const fn string_language_id(self) -> Option { + if self.descriptor_type == descriptor_type::STRING { + Some(self.index) + } else { + None + } + } +} + +/// Why a setup packet cannot be viewed as a standard GET_DESCRIPTOR request. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum GetDescriptorRequestError { + NotStandard, + WrongRequest(u8), + WrongDirection, +} + +impl fmt::Display for GetDescriptorRequestError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NotStandard => f.write_str("GET_DESCRIPTOR request type is not standard"), + Self::WrongRequest(request) => { + write!(f, "USB request code {request:#04x} is not GET_DESCRIPTOR") + } + Self::WrongDirection => f.write_str("GET_DESCRIPTOR with a data stage is not device-to-host"), + } + } +} + +impl core::error::Error for GetDescriptorRequestError {} + +impl TryFrom for GetDescriptorRequest { + type Error = GetDescriptorRequestError; + + fn try_from(setup: SetupPacket) -> Result { + if setup.request_type.kind() != RequestKind::STANDARD { + return Err(GetDescriptorRequestError::NotStandard); + } + if setup.request != standard_request::GET_DESCRIPTOR { + return Err(GetDescriptorRequestError::WrongRequest(setup.request)); + } + // USB ignores the direction bit when wLength is zero. + if setup.length != 0 && !matches!(setup.request_type.direction(), Direction::In) { + return Err(GetDescriptorRequestError::WrongDirection); + } + + let [descriptor_index, descriptor_type] = setup.value.to_le_bytes(); + Ok(Self { + recipient: setup.request_type.recipient(), + descriptor_type, + descriptor_index, + index: setup.index, + requested_length: setup.length, + }) + } +} diff --git a/crates/ironrdp-usb/src/descriptor/configuration.rs b/crates/ironrdp-usb/src/descriptor/configuration.rs new file mode 100644 index 000000000..2a2553a9b --- /dev/null +++ b/crates/ironrdp-usb/src/descriptor/configuration.rs @@ -0,0 +1,675 @@ +//! Borrowed configuration descriptor views and explicit topology validation. + +use super::super::endpoint::{ + EndpointAddress, EndpointAddressError, EndpointAttributes, IsochronousUsageType, MaxPacketSize, +}; +use super::super::value::{ClassCode, TransferType, UsbSpeed}; +use super::{ + CONFIGURATION_DESCRIPTOR_MIN_LENGTH, DescriptorError, DescriptorErrorKind, DescriptorField, DescriptorIter, + ENDPOINT_DESCRIPTOR_MIN_LENGTH, INTERFACE_DESCRIPTOR_MIN_LENGTH, RawDescriptor, descriptor_type, invalid_field, + le_u16, require_minimum_length, validate_class_code, +}; + +/// Raw configuration `bmAttributes` with non-destructive field accessors. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct ConfigurationAttributes(u8); + +impl ConfigurationAttributes { + #[must_use] + pub const fn from_raw(raw: u8) -> Self { + Self(raw) + } + + #[must_use] + pub const fn raw(self) -> u8 { + self.0 + } + + #[must_use] + pub const fn self_powered(self) -> bool { + self.0 & 0x40 != 0 + } + + #[must_use] + pub const fn remote_wakeup(self) -> bool { + self.0 & 0x20 != 0 + } +} + +/// Borrowed view of a standard configuration descriptor header. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ConfigurationDescriptor<'a> { + raw: RawDescriptor<'a>, +} + +impl<'a> ConfigurationDescriptor<'a> { + #[must_use] + pub const fn raw_descriptor(self) -> RawDescriptor<'a> { + self.raw + } + + #[must_use] + pub const fn as_bytes(self) -> &'a [u8] { + self.raw.as_bytes() + } + + #[must_use] + pub const fn length(self) -> u8 { + self.as_bytes()[0] + } + + #[must_use] + pub fn total_length(self) -> u16 { + le_u16(self.as_bytes(), 2) + } + + #[must_use] + pub const fn num_interfaces(self) -> u8 { + self.as_bytes()[4] + } + + #[must_use] + pub const fn configuration_value(self) -> u8 { + self.as_bytes()[5] + } + + #[must_use] + pub const fn configuration_string(self) -> u8 { + self.as_bytes()[6] + } + + #[must_use] + pub const fn attributes(self) -> ConfigurationAttributes { + ConfigurationAttributes::from_raw(self.as_bytes()[7]) + } + + #[must_use] + pub const fn max_power_raw(self) -> u8 { + self.as_bytes()[8] + } + + /// Maximum bus current in milliamperes, interpreted for negotiated speed. + #[must_use] + pub const fn max_power_milliamps(self, speed: UsbSpeed) -> u16 { + let unit = if speed.is_superspeed() { 8 } else { 2 }; + self.max_power_raw() as u16 * unit + } +} + +/// Borrowed view of an interface descriptor and its subordinate descriptors. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct InterfaceDescriptor<'a> { + raw: RawDescriptor<'a>, + configuration_bytes: &'a [u8], +} + +impl<'a> InterfaceDescriptor<'a> { + #[must_use] + pub const fn raw_descriptor(self) -> RawDescriptor<'a> { + self.raw + } + + #[must_use] + pub const fn as_bytes(self) -> &'a [u8] { + self.raw.as_bytes() + } + + #[must_use] + pub const fn offset(self) -> usize { + self.raw.offset() + } + + #[must_use] + pub const fn length(self) -> u8 { + self.as_bytes()[0] + } + + #[must_use] + pub const fn number(self) -> u8 { + self.as_bytes()[2] + } + + #[must_use] + pub const fn alternate_setting(self) -> u8 { + self.as_bytes()[3] + } + + #[must_use] + pub const fn num_endpoints(self) -> u8 { + self.as_bytes()[4] + } + + #[must_use] + pub const fn class(self) -> ClassCode { + let bytes = self.as_bytes(); + ClassCode::new(bytes[5], bytes[6], bytes[7]) + } + + #[must_use] + pub const fn interface_string(self) -> u8 { + self.as_bytes()[8] + } + + /// Descriptors belonging to this alternate setting, excluding the + /// interface descriptor itself. + #[must_use] + pub fn descriptors(self) -> DescriptorIter<'a> { + let start = self.offset() + self.raw.len(); + let mut end = self.configuration_bytes.len(); + for descriptor in DescriptorIter::new_framed(&self.configuration_bytes[start..], start) { + if matches!( + descriptor.descriptor_type(), + descriptor_type::INTERFACE | descriptor_type::INTERFACE_ASSOCIATION + ) { + end = descriptor.offset(); + break; + } + } + DescriptorIter::new_framed(&self.configuration_bytes[start..end], start) + } + + #[must_use] + pub fn endpoints(self) -> EndpointIter<'a> { + EndpointIter { + descriptors: self.descriptors(), + } + } + + #[must_use] + pub fn endpoint(self, address: EndpointAddress) -> Option> { + self.endpoints().find(|endpoint| endpoint.address() == Ok(address)) + } +} + +/// Iterator over standard endpoint descriptors in one interface alternate. +#[derive(Debug, Clone)] +pub struct EndpointIter<'a> { + descriptors: DescriptorIter<'a>, +} + +impl<'a> Iterator for EndpointIter<'a> { + type Item = EndpointDescriptor<'a>; + + fn next(&mut self) -> Option { + self.descriptors + .find_map(|raw| (raw.descriptor_type() == descriptor_type::ENDPOINT).then_some(EndpointDescriptor { raw })) + } +} + +/// Borrowed view of a standard endpoint descriptor prefix. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EndpointDescriptor<'a> { + raw: RawDescriptor<'a>, +} + +impl<'a> EndpointDescriptor<'a> { + #[must_use] + pub const fn raw_descriptor(self) -> RawDescriptor<'a> { + self.raw + } + + #[must_use] + pub const fn as_bytes(self) -> &'a [u8] { + self.raw.as_bytes() + } + + #[must_use] + pub const fn offset(self) -> usize { + self.raw.offset() + } + + #[must_use] + pub const fn length(self) -> u8 { + self.as_bytes()[0] + } + + #[must_use] + pub const fn address_raw(self) -> u8 { + self.as_bytes()[2] + } + + pub const fn address(self) -> Result { + EndpointAddress::from_raw(self.address_raw()) + } + + #[must_use] + pub const fn attributes(self) -> EndpointAttributes { + EndpointAttributes::from_raw(self.as_bytes()[3]) + } + + #[must_use] + pub const fn transfer_type(self) -> TransferType { + self.attributes().transfer_type() + } + + #[must_use] + pub fn max_packet_size(self) -> MaxPacketSize { + MaxPacketSize::from_raw(le_u16(self.as_bytes(), 4)) + } + + #[must_use] + pub const fn interval(self) -> u8 { + self.as_bytes()[6] + } +} + +/// Iterator over every interface descriptor in a configuration set. +#[derive(Debug, Clone)] +pub struct InterfaceIter<'a> { + descriptors: DescriptorIter<'a>, + configuration_bytes: &'a [u8], +} + +impl<'a> Iterator for InterfaceIter<'a> { + type Item = InterfaceDescriptor<'a>; + + fn next(&mut self) -> Option { + self.descriptors.find_map(|raw| { + (raw.descriptor_type() == descriptor_type::INTERFACE).then_some(InterfaceDescriptor { + raw, + configuration_bytes: self.configuration_bytes, + }) + }) + } +} + +/// Borrowed complete configuration descriptor set through `wTotalLength`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ConfigurationDescriptorSet<'a> { + bytes: &'a [u8], + configuration: ConfigurationDescriptor<'a>, +} + +impl<'a> ConfigurationDescriptorSet<'a> { + /// Read `wTotalLength` once enough of a configuration prefix is present. + /// + /// `Ok(None)` means more prefix bytes are required. An invalid type or an + /// already-observable invalid length is reported immediately. + pub fn required_length(prefix: &[u8]) -> Result, DescriptorError> { + if prefix.is_empty() { + return Ok(None); + } + let declared_length = usize::from(prefix[0]); + if declared_length < CONFIGURATION_DESCRIPTOR_MIN_LENGTH { + return Err(DescriptorError::new( + 0, + DescriptorErrorKind::InvalidLength { + descriptor_type: descriptor_type::CONFIGURATION, + declared: declared_length, + minimum: CONFIGURATION_DESCRIPTOR_MIN_LENGTH, + }, + )); + } + if prefix.len() < 2 { + return Ok(None); + } + let actual = prefix[1]; + if actual != descriptor_type::CONFIGURATION { + return Err(DescriptorError::new( + 0, + DescriptorErrorKind::UnexpectedType { + expected: descriptor_type::CONFIGURATION, + actual, + }, + )); + } + if prefix.len() < 4 { + return Ok(None); + } + + let total_length = usize::from(le_u16(prefix, 2)); + if total_length < declared_length { + return Err(invalid_field( + 0, + descriptor_type::CONFIGURATION, + DescriptorField::TotalLength, + total_length as u32, + )); + } + Ok(Some(total_length)) + } + + /// Parse when a GET_DESCRIPTOR response contains all of `wTotalLength`. + pub fn parse_if_complete(bytes: &'a [u8]) -> Result, DescriptorError> { + let Some(required) = Self::required_length(bytes)? else { + return Ok(None); + }; + if bytes.len() < required { + return Ok(None); + } + Self::parse(bytes).map(Some) + } + + /// Frame one complete configuration descriptor set. + /// + /// Bytes after `wTotalLength` are not consumed. Parsing checks boundaries + /// and the minimum sizes required by the typed views, but deliberately does + /// not reject semantic quirks such as a zero configuration value, endpoint + /// topology mismatch, or reserved field values. Use [`Self::validate`] when + /// conformance is required. + pub fn parse(bytes: &'a [u8]) -> Result { + let Some(total_length) = Self::required_length(bytes)? else { + return Err(DescriptorError::new( + 0, + DescriptorErrorKind::BufferTooShort { + needed: 4, + available: bytes.len(), + }, + )); + }; + if bytes.len() < total_length { + return Err(DescriptorError::new( + 0, + DescriptorErrorKind::BufferTooShort { + needed: total_length, + available: bytes.len(), + }, + )); + } + + let bytes = &bytes[..total_length]; + let mut descriptors = DescriptorIter::new(bytes)?; + let configuration_raw = descriptors.next().ok_or_else(|| { + DescriptorError::new( + 0, + DescriptorErrorKind::BufferTooShort { + needed: CONFIGURATION_DESCRIPTOR_MIN_LENGTH, + available: 0, + }, + ) + })?; + + for descriptor in descriptors { + let minimum = match descriptor.descriptor_type() { + descriptor_type::INTERFACE => Some(INTERFACE_DESCRIPTOR_MIN_LENGTH), + descriptor_type::ENDPOINT => Some(ENDPOINT_DESCRIPTOR_MIN_LENGTH), + _ => None, + }; + if let Some(minimum) = minimum { + require_minimum_length( + descriptor.offset(), + descriptor.descriptor_type(), + descriptor.len(), + minimum, + )?; + } + } + + Ok(Self { + bytes, + configuration: ConfigurationDescriptor { raw: configuration_raw }, + }) + } + + /// Validate context-independent Chapter 9 fields and USB endpoint topology. + /// + /// Speed-dependent packet and interval rules, class-specific descriptors, + /// and the detailed SuperSpeed companion rules require additional context + /// and are intentionally not asserted here. + pub fn validate(self) -> Result<(), DescriptorError> { + self.validate_configuration_header()?; + + let mut has_current_interface = false; + for descriptor in self.descriptors().skip(1) { + let descriptor_type = descriptor.descriptor_type(); + match descriptor_type { + descriptor_type::INTERFACE => { + let interface = InterfaceDescriptor { + raw: descriptor, + configuration_bytes: self.bytes, + }; + validate_class_code(interface.offset(), descriptor_type, interface.class())?; + has_current_interface = true; + } + descriptor_type::ENDPOINT => { + if !has_current_interface { + return Err(DescriptorError::new( + descriptor.offset(), + DescriptorErrorKind::EndpointBeforeInterface, + )); + } + validate_endpoint(EndpointDescriptor { raw: descriptor })?; + } + descriptor_type::INTERFACE_ASSOCIATION => has_current_interface = false, + descriptor_type::DEVICE | descriptor_type::CONFIGURATION | descriptor_type::STRING => { + return Err(DescriptorError::new( + descriptor.offset(), + DescriptorErrorKind::UnexpectedDescriptor { descriptor_type }, + )); + } + _ => {} + } + } + + self.validate_interface_topology() + } + + fn validate_configuration_header(self) -> Result<(), DescriptorError> { + let configuration = self.configuration(); + if configuration.configuration_value() == 0 { + return Err(invalid_field( + 0, + descriptor_type::CONFIGURATION, + DescriptorField::ConfigurationValue, + 0, + )); + } + let attributes = configuration.attributes().raw(); + if attributes & 0x80 == 0 || attributes & 0x1f != 0 { + return Err(invalid_field( + 0, + descriptor_type::CONFIGURATION, + DescriptorField::Attributes, + u32::from(attributes), + )); + } + Ok(()) + } + + fn validate_interface_topology(self) -> Result<(), DescriptorError> { + // The nested re-iteration below is quadratic. This crate cannot + // allocate lookup tables, and the input is bounded by the 16-bit + // wTotalLength, which keeps the worst case acceptable for a + // hostile-input path. + let mut distinct_interfaces = 0usize; + for interface in self.interfaces() { + if self.interfaces().any(|previous| { + previous.offset() < interface.offset() + && previous.number() == interface.number() + && previous.alternate_setting() == interface.alternate_setting() + }) { + return Err(DescriptorError::new( + interface.offset(), + DescriptorErrorKind::DuplicateInterface { + interface: interface.number(), + alternate_setting: interface.alternate_setting(), + }, + )); + } + + if !self + .interfaces() + .any(|previous| previous.offset() < interface.offset() && previous.number() == interface.number()) + { + distinct_interfaces += 1; + if !self + .interfaces() + .any(|candidate| candidate.number() == interface.number() && candidate.alternate_setting() == 0) + { + return Err(DescriptorError::new( + interface.offset(), + DescriptorErrorKind::MissingDefaultAlternate { + interface: interface.number(), + }, + )); + } + } + + let actual_endpoints = interface.endpoints().count(); + if actual_endpoints != usize::from(interface.num_endpoints()) { + return Err(DescriptorError::new( + interface.offset(), + DescriptorErrorKind::EndpointCountMismatch { + interface: interface.number(), + alternate_setting: interface.alternate_setting(), + declared: interface.num_endpoints(), + actual: actual_endpoints, + }, + )); + } + + for endpoint in interface.endpoints() { + let Ok(address) = endpoint.address() else { + // The field-level pass above reports the precise error. + continue; + }; + if interface + .endpoints() + .any(|previous| previous.offset() < endpoint.offset() && previous.address() == Ok(address)) + { + return Err(DescriptorError::new( + endpoint.offset(), + DescriptorErrorKind::DuplicateEndpoint { + interface: interface.number(), + alternate_setting: interface.alternate_setting(), + address, + }, + )); + } + + for other in self + .interfaces() + .filter(|other| other.offset() < interface.offset() && other.number() != interface.number()) + { + if other + .endpoints() + .any(|other_endpoint| other_endpoint.address() == Ok(address)) + { + return Err(DescriptorError::new( + endpoint.offset(), + DescriptorErrorKind::EndpointSharedAcrossInterfaces { + address, + first: other.number(), + second: interface.number(), + }, + )); + } + } + } + } + + if distinct_interfaces != usize::from(self.configuration().num_interfaces()) { + return Err(DescriptorError::new( + 0, + DescriptorErrorKind::InterfaceCountMismatch { + declared: self.configuration().num_interfaces(), + actual: distinct_interfaces, + }, + )); + } + Ok(()) + } + + #[must_use] + pub const fn as_bytes(self) -> &'a [u8] { + self.bytes + } + + #[must_use] + pub const fn configuration(self) -> ConfigurationDescriptor<'a> { + self.configuration + } + + #[must_use] + pub fn interfaces(self) -> InterfaceIter<'a> { + InterfaceIter { + descriptors: self.descriptors(), + configuration_bytes: self.bytes, + } + } + + #[must_use] + pub fn interface(self, number: u8, alternate_setting: u8) -> Option> { + self.interfaces() + .find(|interface| interface.number() == number && interface.alternate_setting() == alternate_setting) + } + + pub fn default_interfaces(self) -> impl Iterator> { + self.interfaces().filter(|interface| interface.alternate_setting() == 0) + } + + #[must_use] + pub const fn descriptors(self) -> DescriptorIter<'a> { + DescriptorIter::new_framed(self.bytes, 0) + } +} + +fn validate_endpoint(endpoint: EndpointDescriptor<'_>) -> Result<(), DescriptorError> { + let address = endpoint.address().map_err(|_| { + invalid_field( + endpoint.offset(), + descriptor_type::ENDPOINT, + DescriptorField::EndpointAddress, + u32::from(endpoint.address_raw()), + ) + })?; + if address.is_default_control() { + return Err(invalid_field( + endpoint.offset(), + descriptor_type::ENDPOINT, + DescriptorField::EndpointAddress, + u32::from(address.raw()), + )); + } + + validate_endpoint_attributes(endpoint.offset(), endpoint.attributes())?; + validate_max_packet_size(endpoint.offset(), endpoint.transfer_type(), endpoint.max_packet_size()) +} + +fn validate_endpoint_attributes(offset: usize, attributes: EndpointAttributes) -> Result<(), DescriptorError> { + let raw = attributes.raw(); + let invalid = if raw & 0xc0 != 0 { + true + } else { + match attributes.transfer_type() { + TransferType::Control | TransferType::Bulk => raw & 0x3c != 0, + TransferType::Isochronous => matches!(attributes.isochronous_usage(), Some(IsochronousUsageType::RESERVED)), + // The notification subtype is defined by USB 3.x. Accept it + // without speed context, but reject the two encodings reserved by + // every currently modeled USB revision. + TransferType::Interrupt => attributes.notification_interrupt().is_none(), + } + }; + if invalid { + Err(invalid_field( + offset, + descriptor_type::ENDPOINT, + DescriptorField::EndpointAttributes, + u32::from(raw), + )) + } else { + Ok(()) + } +} + +fn validate_max_packet_size( + offset: usize, + transfer_type: TransferType, + max_packet_size: MaxPacketSize, +) -> Result<(), DescriptorError> { + let raw = max_packet_size.raw(); + let invalid = raw & 0xe000 != 0 + || max_packet_size.additional_transactions().is_err() + || (matches!(transfer_type, TransferType::Control | TransferType::Bulk) && raw & 0x1800 != 0); + if invalid { + Err(invalid_field( + offset, + descriptor_type::ENDPOINT, + DescriptorField::MaxPacketSize, + u32::from(raw), + )) + } else { + Ok(()) + } +} diff --git a/crates/ironrdp-usb/src/descriptor/device.rs b/crates/ironrdp-usb/src/descriptor/device.rs new file mode 100644 index 000000000..e8769d3f0 --- /dev/null +++ b/crates/ironrdp-usb/src/descriptor/device.rs @@ -0,0 +1,230 @@ +//! Borrowed device and string descriptor views. + +use super::super::value::{ClassCode, UsbSpeed, is_valid_bcd_version}; +use super::{ + DEVICE_DESCRIPTOR_MIN_LENGTH, DescriptorError, DescriptorErrorKind, DescriptorField, DeviceDescriptorError, + RawDescriptor, descriptor_type, invalid_field, le_u16, require_minimum_length, validate_class_code, +}; + +/// Standard USB device descriptor. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DeviceDescriptor<'a> { + raw: RawDescriptor<'a>, +} + +impl<'a> DeviceDescriptor<'a> { + pub fn parse(bytes: &'a [u8]) -> Result { + let available = bytes.len(); + let (descriptor, consumed) = Self::parse_prefix(bytes)?; + if consumed != available { + return Err(DescriptorError::new( + consumed, + DescriptorErrorKind::TrailingBytes { consumed, available }, + )); + } + Ok(descriptor) + } + + /// Parse the first device descriptor in a larger buffer and report its + /// declared `bLength`. + pub fn parse_prefix(bytes: &'a [u8]) -> Result<(Self, usize), DescriptorError> { + let raw = parse_standalone_descriptor(bytes, descriptor_type::DEVICE, DEVICE_DESCRIPTOR_MIN_LENGTH)?; + let consumed = raw.len(); + Ok((Self { raw }, consumed)) + } + + /// Validate context-independent device descriptor fields. + /// + /// Endpoint-zero packet size depends on the negotiated speed and is + /// therefore validated separately by [`Self::endpoint_zero_max_packet_size`]. + pub fn validate(self) -> Result<(), DescriptorError> { + let usb_version = self.usb_version(); + if !is_valid_bcd_version(usb_version) { + return Err(invalid_field( + 0, + descriptor_type::DEVICE, + DescriptorField::UsbVersion, + u32::from(usb_version), + )); + } + let device_release = self.device_release(); + if !is_valid_bcd_version(device_release) { + return Err(invalid_field( + 0, + descriptor_type::DEVICE, + DescriptorField::DeviceRelease, + u32::from(device_release), + )); + } + validate_class_code(0, descriptor_type::DEVICE, self.class()) + } + + #[must_use] + pub const fn raw_descriptor(self) -> RawDescriptor<'a> { + self.raw + } + + #[must_use] + pub const fn as_bytes(self) -> &'a [u8] { + self.raw.as_bytes() + } + + #[must_use] + pub fn usb_version(self) -> u16 { + le_u16(self.as_bytes(), 2) + } + + #[must_use] + pub fn class(self) -> ClassCode { + let bytes = self.as_bytes(); + ClassCode::new(bytes[4], bytes[5], bytes[6]) + } + + /// Raw `bMaxPacketSize0`; use [`Self::endpoint_zero_max_packet_size`] to + /// interpret it for a negotiated speed. + #[must_use] + pub fn max_packet_size_0_raw(self) -> u8 { + self.as_bytes()[7] + } + + #[must_use] + pub fn vendor_id(self) -> u16 { + le_u16(self.as_bytes(), 8) + } + + #[must_use] + pub fn product_id(self) -> u16 { + le_u16(self.as_bytes(), 10) + } + + #[must_use] + pub fn device_release(self) -> u16 { + le_u16(self.as_bytes(), 12) + } + + #[must_use] + pub fn manufacturer_string(self) -> u8 { + self.as_bytes()[14] + } + + #[must_use] + pub fn product_string(self) -> u8 { + self.as_bytes()[15] + } + + #[must_use] + pub fn serial_number_string(self) -> u8 { + self.as_bytes()[16] + } + + #[must_use] + pub fn num_configurations(self) -> u8 { + self.as_bytes()[17] + } + + /// Interpret `bMaxPacketSize0` using negotiated bus speed. + /// + /// `bcdUSB` is deliberately not used as a substitute for speed: a USB 3.x + /// capable device can enumerate using USB 2.0 endpoint-zero semantics. + pub fn endpoint_zero_max_packet_size(self, speed: UsbSpeed) -> Result { + let encoded = self.max_packet_size_0_raw(); + let valid = match speed { + UsbSpeed::Low => encoded == 8, + UsbSpeed::Full => matches!(encoded, 8 | 16 | 32 | 64), + UsbSpeed::High => encoded == 64, + UsbSpeed::Super | UsbSpeed::SuperPlus => encoded == 9, + }; + if !valid { + return Err(DeviceDescriptorError::new(speed, encoded)); + } + + Ok(if speed.is_superspeed() { + 1u16 << encoded + } else { + u16::from(encoded) + }) + } +} + +/// Standard USB string descriptor payload. +/// +/// Parsing only frames the descriptor. Call [`Self::validate`] before treating +/// every payload byte as UTF-16LE; a non-conforming final byte remains visible +/// through [`Self::trailing_byte`]. String index zero uses the same 16-bit units +/// for LANGIDs rather than Unicode text. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct StringDescriptor<'a> { + raw: RawDescriptor<'a>, +} + +impl<'a> StringDescriptor<'a> { + pub fn parse(bytes: &'a [u8]) -> Result { + let available = bytes.len(); + let (descriptor, consumed) = Self::parse_prefix(bytes)?; + if consumed != available { + return Err(DescriptorError::new( + consumed, + DescriptorErrorKind::TrailingBytes { consumed, available }, + )); + } + Ok(descriptor) + } + + pub fn parse_prefix(bytes: &'a [u8]) -> Result<(Self, usize), DescriptorError> { + let raw = parse_standalone_descriptor(bytes, descriptor_type::STRING, 2)?; + let consumed = raw.len(); + Ok((Self { raw }, consumed)) + } + + pub fn validate(self) -> Result<(), DescriptorError> { + let payload_len = self.as_bytes().len() - 2; + if payload_len % 2 != 0 { + return Err(invalid_field( + 0, + descriptor_type::STRING, + DescriptorField::StringLength, + payload_len as u32, + )); + } + Ok(()) + } + + #[must_use] + pub const fn raw_descriptor(self) -> RawDescriptor<'a> { + self.raw + } + + #[must_use] + pub const fn as_bytes(self) -> &'a [u8] { + self.raw.as_bytes() + } + + pub fn code_units(self) -> impl ExactSizeIterator + 'a { + self.as_bytes()[2..] + .chunks_exact(2) + .map(|bytes| u16::from_le_bytes([bytes[0], bytes[1]])) + } + + #[must_use] + pub fn trailing_byte(self) -> Option { + let payload = &self.as_bytes()[2..]; + (payload.len() % 2 != 0).then(|| payload[payload.len() - 1]) + } +} + +fn parse_standalone_descriptor<'a>( + bytes: &'a [u8], + expected: u8, + minimum: usize, +) -> Result, DescriptorError> { + let raw = RawDescriptor::parse_prefix(bytes)?; + let actual = raw.descriptor_type(); + if actual != expected { + return Err(DescriptorError::new( + 0, + DescriptorErrorKind::UnexpectedType { expected, actual }, + )); + } + require_minimum_length(0, actual, raw.len(), minimum)?; + Ok(raw) +} diff --git a/crates/ironrdp-usb/src/descriptor/error.rs b/crates/ironrdp-usb/src/descriptor/error.rs new file mode 100644 index 000000000..216c3530b --- /dev/null +++ b/crates/ironrdp-usb/src/descriptor/error.rs @@ -0,0 +1,261 @@ +//! Typed errors for USB descriptor framing and explicit validation. + +use core::fmt; + +use super::super::endpoint::EndpointAddress; +use super::super::value::UsbSpeed; + +/// Field associated with an invalid descriptor value. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum DescriptorField { + UsbVersion, + DeviceRelease, + Subclass, + Protocol, + TotalLength, + ConfigurationValue, + Attributes, + StringLength, + EndpointAddress, + EndpointAttributes, + MaxPacketSize, +} + +impl fmt::Display for DescriptorField { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UsbVersion => "bcdUSB", + Self::DeviceRelease => "bcdDevice", + Self::Subclass => "subclass", + Self::Protocol => "protocol", + Self::TotalLength => "wTotalLength", + Self::ConfigurationValue => "bConfigurationValue", + Self::Attributes => "bmAttributes", + Self::StringLength => "string payload length", + Self::EndpointAddress => "bEndpointAddress", + Self::EndpointAttributes => "endpoint bmAttributes", + Self::MaxPacketSize => "wMaxPacketSize", + }) + } +} + +/// Precise reason why a descriptor stream cannot be parsed or validated. +/// +/// Parsing only emits framing and minimum-length errors. Semantic and topology +/// variants are emitted by the explicit `validate` methods. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum DescriptorErrorKind { + BufferTooShort { + needed: usize, + available: usize, + }, + UnexpectedType { + expected: u8, + actual: u8, + }, + InvalidLength { + descriptor_type: u8, + declared: usize, + minimum: usize, + }, + DescriptorExceedsBoundary { + descriptor_type: u8, + declared: usize, + remaining: usize, + }, + TrailingBytes { + consumed: usize, + available: usize, + }, + InvalidField { + descriptor_type: u8, + field: DescriptorField, + value: u32, + }, + UnexpectedDescriptor { + descriptor_type: u8, + }, + EndpointBeforeInterface, + DuplicateInterface { + interface: u8, + alternate_setting: u8, + }, + MissingDefaultAlternate { + interface: u8, + }, + InterfaceCountMismatch { + declared: u8, + actual: usize, + }, + EndpointCountMismatch { + interface: u8, + alternate_setting: u8, + declared: u8, + actual: usize, + }, + DuplicateEndpoint { + interface: u8, + alternate_setting: u8, + address: EndpointAddress, + }, + EndpointSharedAcrossInterfaces { + address: EndpointAddress, + first: u8, + second: u8, + }, +} + +/// Descriptor error with a byte offset into the supplied buffer. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DescriptorError { + offset: usize, + kind: DescriptorErrorKind, +} + +impl DescriptorError { + pub(super) const fn new(offset: usize, kind: DescriptorErrorKind) -> Self { + Self { offset, kind } + } + + #[must_use] + pub const fn offset(self) -> usize { + self.offset + } + + #[must_use] + pub const fn kind(self) -> DescriptorErrorKind { + self.kind + } +} + +impl fmt::Display for DescriptorError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "USB descriptor error at offset {}: ", self.offset)?; + match self.kind { + DescriptorErrorKind::BufferTooShort { needed, available } => { + write!(f, "need {needed} bytes but only {available} are available") + } + DescriptorErrorKind::UnexpectedType { expected, actual } => { + write!(f, "expected descriptor type {expected:#04x}, got {actual:#04x}") + } + DescriptorErrorKind::InvalidLength { + descriptor_type, + declared, + minimum, + } => write!( + f, + "descriptor {descriptor_type:#04x} declares length {declared}, minimum is {minimum}" + ), + DescriptorErrorKind::DescriptorExceedsBoundary { + descriptor_type, + declared, + remaining, + } => write!( + f, + "descriptor {descriptor_type:#04x} declares {declared} bytes with only {remaining} remaining" + ), + DescriptorErrorKind::TrailingBytes { consumed, available } => { + write!(f, "descriptor consumes {consumed} bytes but {available} were supplied") + } + DescriptorErrorKind::InvalidField { + descriptor_type, + field, + value, + } => write!( + f, + "descriptor {descriptor_type:#04x} has invalid {field} value {value:#x}" + ), + DescriptorErrorKind::UnexpectedDescriptor { descriptor_type } => { + write!( + f, + "descriptor type {descriptor_type:#04x} is not valid in a configuration set" + ) + } + DescriptorErrorKind::EndpointBeforeInterface => f.write_str("endpoint appears before an interface"), + DescriptorErrorKind::DuplicateInterface { + interface, + alternate_setting, + } => write!( + f, + "duplicate interface {} alternate setting {}", + interface, alternate_setting + ), + DescriptorErrorKind::MissingDefaultAlternate { interface } => { + write!(f, "interface {} has no alternate setting zero", interface) + } + DescriptorErrorKind::InterfaceCountMismatch { declared, actual } => { + write!(f, "configuration declares {declared} interfaces but contains {actual}") + } + DescriptorErrorKind::EndpointCountMismatch { + interface, + alternate_setting, + declared, + actual, + } => write!( + f, + "interface {} alternate {} declares {declared} endpoints but contains {actual}", + interface, alternate_setting + ), + DescriptorErrorKind::DuplicateEndpoint { + interface, + alternate_setting, + address, + } => write!( + f, + "interface {} alternate {} contains duplicate endpoint {:#04x}", + interface, + alternate_setting, + address.raw() + ), + DescriptorErrorKind::EndpointSharedAcrossInterfaces { address, first, second } => write!( + f, + "endpoint {:#04x} is shared by interfaces {} and {}", + address.raw(), + first, + second + ), + } + } +} + +impl core::error::Error for DescriptorError {} + +/// Speed-dependent device-descriptor semantic error. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DeviceDescriptorError { + speed: UsbSpeed, + encoded_max_packet_size_0: u8, +} + +impl DeviceDescriptorError { + pub(super) const fn new(speed: UsbSpeed, encoded_max_packet_size_0: u8) -> Self { + Self { + speed, + encoded_max_packet_size_0, + } + } + + #[must_use] + pub const fn speed(self) -> UsbSpeed { + self.speed + } + + #[must_use] + pub const fn encoded_max_packet_size_0(self) -> u8 { + self.encoded_max_packet_size_0 + } +} + +impl fmt::Display for DeviceDescriptorError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "invalid bMaxPacketSize0 {} for {:?} speed", + self.encoded_max_packet_size_0, self.speed + ) + } +} + +impl core::error::Error for DeviceDescriptorError {} diff --git a/crates/ironrdp-usb/src/descriptor/mod.rs b/crates/ironrdp-usb/src/descriptor/mod.rs new file mode 100644 index 000000000..771a34624 --- /dev/null +++ b/crates/ironrdp-usb/src/descriptor/mod.rs @@ -0,0 +1,93 @@ +//! USB standard descriptors, lossless framing, typed views, and validation. +//! +//! Parsing and validation are intentionally separate: +//! +//! - parsing checks wire framing and the minimum lengths needed for safe typed +//! access; +//! - validation checks context-independent Chapter 9 field and topology rules. +//! +//! This distinction matters for bridges: a device can successfully return a +//! quirky descriptor which still has to be forwarded byte-for-byte even when a +//! consumer declines to cache or interpret it. +//! +//! Unknown, class-specific, and not-yet-typed standard descriptors remain +//! available in wire order through [`ConfigurationDescriptorSet::descriptors`]. +//! Descriptor views borrow the caller's bytes and do not allocate. + +mod configuration; +mod device; +mod error; +mod raw; + +pub use configuration::{ + ConfigurationAttributes, ConfigurationDescriptor, ConfigurationDescriptorSet, EndpointDescriptor, EndpointIter, + InterfaceDescriptor, InterfaceIter, +}; +pub use device::{DeviceDescriptor, StringDescriptor}; +pub use error::{DescriptorError, DescriptorErrorKind, DescriptorField, DeviceDescriptorError}; +pub use raw::{DescriptorIter, RawDescriptor, descriptor_type}; + +use super::value::ClassCode; + +const DEVICE_DESCRIPTOR_MIN_LENGTH: usize = 18; +const CONFIGURATION_DESCRIPTOR_MIN_LENGTH: usize = 9; +const INTERFACE_DESCRIPTOR_MIN_LENGTH: usize = 9; +const ENDPOINT_DESCRIPTOR_MIN_LENGTH: usize = 7; + +fn require_minimum_length( + offset: usize, + descriptor_type: u8, + declared: usize, + minimum: usize, +) -> Result<(), DescriptorError> { + if declared < minimum { + Err(DescriptorError::new( + offset, + DescriptorErrorKind::InvalidLength { + descriptor_type, + declared, + minimum, + }, + )) + } else { + Ok(()) + } +} + +fn invalid_field(offset: usize, descriptor_type: u8, field: DescriptorField, value: u32) -> DescriptorError { + DescriptorError::new( + offset, + DescriptorErrorKind::InvalidField { + descriptor_type, + field, + value, + }, + ) +} + +fn le_u16(bytes: &[u8], offset: usize) -> u16 { + u16::from_le_bytes([bytes[offset], bytes[offset + 1]]) +} + +fn validate_class_code(offset: usize, descriptor_type: u8, class: ClassCode) -> Result<(), DescriptorError> { + if class.class != 0 { + return Ok(()); + } + if class.subclass != 0 { + return Err(invalid_field( + offset, + descriptor_type, + DescriptorField::Subclass, + u32::from(class.subclass), + )); + } + if class.protocol != 0 { + return Err(invalid_field( + offset, + descriptor_type, + DescriptorField::Protocol, + u32::from(class.protocol), + )); + } + Ok(()) +} diff --git a/crates/ironrdp-usb/src/descriptor/raw.rs b/crates/ironrdp-usb/src/descriptor/raw.rs new file mode 100644 index 000000000..274c72f9f --- /dev/null +++ b/crates/ironrdp-usb/src/descriptor/raw.rs @@ -0,0 +1,174 @@ +//! Raw descriptor identities and lossless traversal. + +use super::{DescriptorError, DescriptorErrorKind}; + +/// Standard values in the open USB `bDescriptorType` namespace. +/// +/// Descriptor types themselves remain plain `u8` so class-specific, vendor, +/// and future values can be preserved without wrapping every byte. +pub mod descriptor_type { + pub const DEVICE: u8 = 0x01; + pub const CONFIGURATION: u8 = 0x02; + pub const STRING: u8 = 0x03; + pub const INTERFACE: u8 = 0x04; + pub const ENDPOINT: u8 = 0x05; + pub const DEVICE_QUALIFIER: u8 = 0x06; + pub const OTHER_SPEED_CONFIGURATION: u8 = 0x07; + pub const INTERFACE_POWER: u8 = 0x08; + pub const OTG: u8 = 0x09; + pub const DEBUG: u8 = 0x0a; + pub const INTERFACE_ASSOCIATION: u8 = 0x0b; + pub const BOS: u8 = 0x0f; + pub const DEVICE_CAPABILITY: u8 = 0x10; + pub const SUPER_SPEED_ENDPOINT_COMPANION: u8 = 0x30; + pub const SUPER_SPEED_PLUS_ISOCHRONOUS_ENDPOINT_COMPANION: u8 = 0x31; +} + +/// Borrowed descriptor preserving its original bytes and wire offset. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RawDescriptor<'a> { + offset: usize, + bytes: &'a [u8], +} + +impl<'a> RawDescriptor<'a> { + /// Frame the first descriptor in `bytes`. + /// + /// Only the descriptor header and declared `bLength` are interpreted. The + /// descriptor type and payload are deliberately left open. + pub fn parse_prefix(bytes: &'a [u8]) -> Result { + Self::parse_at(bytes, 0, 0) + } + + pub(super) fn parse_at( + bytes: &'a [u8], + relative_offset: usize, + base_offset: usize, + ) -> Result { + let offset = base_offset + relative_offset; + let remaining = bytes.len().saturating_sub(relative_offset); + if remaining < 2 { + return Err(DescriptorError::new( + offset, + DescriptorErrorKind::BufferTooShort { + needed: 2, + available: remaining, + }, + )); + } + + let length = usize::from(bytes[relative_offset]); + let descriptor_type = bytes[relative_offset + 1]; + if length < 2 { + return Err(DescriptorError::new( + offset, + DescriptorErrorKind::InvalidLength { + descriptor_type, + declared: length, + minimum: 2, + }, + )); + } + if length > remaining { + return Err(DescriptorError::new( + offset, + DescriptorErrorKind::DescriptorExceedsBoundary { + descriptor_type, + declared: length, + remaining, + }, + )); + } + + Ok(Self { + offset, + bytes: &bytes[relative_offset..relative_offset + length], + }) + } + + #[must_use] + pub const fn offset(self) -> usize { + self.offset + } + + #[must_use] + pub const fn descriptor_type(self) -> u8 { + self.bytes[1] + } + + #[must_use] + pub const fn len(self) -> usize { + self.bytes.len() + } + + /// Always `false`: framing guarantees at least the two header bytes. + #[must_use] + pub const fn is_empty(self) -> bool { + false + } + + #[must_use] + pub const fn as_bytes(self) -> &'a [u8] { + self.bytes + } + + /// Bytes following `bLength` and `bDescriptorType`. + #[must_use] + pub fn payload(self) -> &'a [u8] { + &self.bytes[2..] + } +} + +/// Iterator over a structurally framed descriptor stream. +/// +/// Construction scans the stream once. Iteration is then infallible and +/// preserves unknown and class-specific descriptors in wire order. +#[derive(Debug, Clone)] +pub struct DescriptorIter<'a> { + bytes: &'a [u8], + base_offset: usize, + relative_offset: usize, +} + +impl<'a> DescriptorIter<'a> { + pub fn new(bytes: &'a [u8]) -> Result { + Self::with_offset(bytes, 0) + } + + pub(super) fn with_offset(bytes: &'a [u8], base_offset: usize) -> Result { + let mut relative_offset = 0; + while relative_offset < bytes.len() { + let descriptor = RawDescriptor::parse_at(bytes, relative_offset, base_offset)?; + relative_offset += descriptor.len(); + } + Ok(Self::new_framed(bytes, base_offset)) + } + + pub(super) const fn new_framed(bytes: &'a [u8], base_offset: usize) -> Self { + Self { + bytes, + base_offset, + relative_offset: 0, + } + } +} + +impl<'a> Iterator for DescriptorIter<'a> { + type Item = RawDescriptor<'a>; + + fn next(&mut self) -> Option { + if self.relative_offset >= self.bytes.len() { + return None; + } + + // `DescriptorIter` can only be constructed after the complete stream + // has been framed, so this cannot fail unless its invariant is broken. + let descriptor = RawDescriptor::parse_at(self.bytes, self.relative_offset, self.base_offset).ok()?; + self.relative_offset += descriptor.len(); + Some(descriptor) + } + + fn size_hint(&self) -> (usize, Option) { + (0, Some(self.bytes.len().saturating_sub(self.relative_offset) / 2)) + } +} diff --git a/crates/ironrdp-usb/src/endpoint.rs b/crates/ironrdp-usb/src/endpoint.rs new file mode 100644 index 000000000..4b67060fa --- /dev/null +++ b/crates/ironrdp-usb/src/endpoint.rs @@ -0,0 +1,430 @@ +//! Endpoint identifiers and descriptor bit-field semantics. + +use core::fmt; + +use super::value::{Direction, TransferType, UsbSpeed}; + +/// Error returned when an endpoint number is outside the four-bit USB range. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EndpointAddressError { + raw: u8, + kind: EndpointAddressErrorKind, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum EndpointAddressErrorKind { + NumberOutOfRange, + ReservedBitsSet, +} + +impl EndpointAddressError { + #[must_use] + pub const fn raw(self) -> u8 { + self.raw + } + + #[must_use] + pub const fn kind(self) -> EndpointAddressErrorKind { + self.kind + } +} + +impl fmt::Display for EndpointAddressError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.kind { + EndpointAddressErrorKind::NumberOutOfRange => { + write!(f, "USB endpoint number {} is greater than 15", self.raw) + } + EndpointAddressErrorKind::ReservedBitsSet => { + write!(f, "USB endpoint address {:#04x} has reserved bits set", self.raw) + } + } + } +} + +impl core::error::Error for EndpointAddressError {} + +/// Endpoint number in the inclusive range 0..=15. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct EndpointNumber(u8); + +impl EndpointNumber { + pub const DEFAULT_CONTROL: Self = Self(0); + + pub const fn new(number: u8) -> Result { + if number <= 15 { + Ok(Self(number)) + } else { + Err(EndpointAddressError { + raw: number, + kind: EndpointAddressErrorKind::NumberOutOfRange, + }) + } + } + + #[must_use] + pub const fn raw(self) -> u8 { + self.0 + } +} + +/// Complete endpoint address: endpoint number plus host-relative direction. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct EndpointAddress(u8); + +impl EndpointAddress { + const DIRECTION_IN: u8 = 0x80; + const RESERVED_MASK: u8 = 0x70; + + pub const fn from_raw(raw: u8) -> Result { + if raw & Self::RESERVED_MASK != 0 { + Err(EndpointAddressError { + raw, + kind: EndpointAddressErrorKind::ReservedBitsSet, + }) + } else { + Ok(Self(raw)) + } + } + + #[must_use] + pub const fn from_parts(number: EndpointNumber, direction: Direction) -> Self { + let direction = match direction { + Direction::Out => 0, + Direction::In => Self::DIRECTION_IN, + }; + Self(number.raw() | direction) + } + + #[must_use] + pub const fn raw(self) -> u8 { + self.0 + } + + #[must_use] + pub const fn number(self) -> EndpointNumber { + // The mask guarantees the invariant established by EndpointNumber. + EndpointNumber(self.0 & 0x0f) + } + + #[must_use] + pub const fn direction(self) -> Direction { + if self.0 & Self::DIRECTION_IN == 0 { + Direction::Out + } else { + Direction::In + } + } + + #[must_use] + pub const fn is_default_control(self) -> bool { + self.number().raw() == 0 + } +} + +impl TryFrom for EndpointAddress { + type Error = EndpointAddressError; + + fn try_from(raw: u8) -> Result { + Self::from_raw(raw) + } +} + +impl From for u8 { + fn from(address: EndpointAddress) -> Self { + address.raw() + } +} + +/// Synchronization field of an isochronous endpoint. +/// +/// All four possible two-bit encodings are defined by USB, so decoding cannot +/// encounter an unknown synchronization type. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum IsochronousSynchronizationType { + None = 0, + Asynchronous = 1, + Adaptive = 2, + Synchronous = 3, +} + +/// Usage field of an isochronous endpoint. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct IsochronousUsageType(u8); + +impl IsochronousUsageType { + pub const DATA: Self = Self(0); + pub const FEEDBACK: Self = Self(1); + pub const IMPLICIT_FEEDBACK: Self = Self(2); + /// Value reserved by USB 2.0. + pub const RESERVED: Self = Self(3); + + /// Construct a usage type from its unshifted two-bit field value. + #[must_use] + pub const fn from_raw(raw: u8) -> Option { + if raw <= 0x03 { Some(Self(raw)) } else { None } + } + + #[must_use] + pub const fn raw(self) -> u8 { + self.0 + } +} + +/// Raw `bmAttributes` from an endpoint descriptor. +/// +/// The full byte is retained because bits 5..2 have contextual meanings for +/// isochronous endpoints and USB 3.x interrupt endpoints. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct EndpointAttributes(u8); + +impl EndpointAttributes { + #[must_use] + pub const fn from_raw(raw: u8) -> Self { + Self(raw) + } + + #[must_use] + pub const fn raw(self) -> u8 { + self.0 + } + + #[must_use] + pub const fn transfer_type(self) -> TransferType { + TransferType::from_endpoint_attributes(self.0) + } + + #[must_use] + pub const fn isochronous_synchronization(self) -> Option { + if !matches!(self.transfer_type(), TransferType::Isochronous) { + return None; + } + + Some(match (self.0 >> 2) & 0x03 { + 0 => IsochronousSynchronizationType::None, + 1 => IsochronousSynchronizationType::Asynchronous, + 2 => IsochronousSynchronizationType::Adaptive, + 3 => IsochronousSynchronizationType::Synchronous, + _ => unreachable!(), + }) + } + + #[must_use] + pub const fn isochronous_usage(self) -> Option { + if !matches!(self.transfer_type(), TransferType::Isochronous) { + return None; + } + + Some(IsochronousUsageType((self.0 >> 4) & 0x03)) + } + + /// Whether a USB 3.x interrupt endpoint uses the notification subtype. + /// + /// `None` means the endpoint is not interrupt or uses a reserved encoding. + #[must_use] + pub const fn notification_interrupt(self) -> Option { + if !matches!(self.transfer_type(), TransferType::Interrupt) || self.0 & 0x0c != 0 { + return None; + } + + match (self.0 >> 4) & 0x03 { + 0 => Some(false), + 1 => Some(true), + _ => None, + } + } +} + +/// Invalid USB 2.0 high-bandwidth transaction encoding. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MaxPacketSizeError { + raw: u16, +} + +impl MaxPacketSizeError { + #[must_use] + pub const fn raw(self) -> u16 { + self.raw + } +} + +impl fmt::Display for MaxPacketSizeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "USB wMaxPacketSize {:#06x} uses the reserved high-bandwidth encoding", + self.raw + ) + } +} + +impl core::error::Error for MaxPacketSizeError {} + +/// Raw endpoint `wMaxPacketSize` with non-destructive field accessors. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct MaxPacketSize(u16); + +impl MaxPacketSize { + #[must_use] + pub const fn from_raw(raw: u16) -> Self { + Self(raw) + } + + #[must_use] + pub const fn raw(self) -> u16 { + self.0 + } + + /// Maximum payload bytes in one transaction (`wMaxPacketSize` bits 10..0). + #[must_use] + pub const fn packet_size(self) -> u16 { + self.0 & 0x07ff + } + + /// USB 2.0 high-speed additional transactions encoded in bits 12..11. + /// + /// This field is meaningful only for high-speed isochronous and interrupt + /// endpoints. The return value is additional transactions (0, 1, or 2), + /// not the total transaction count. + pub const fn additional_transactions(self) -> Result { + match (self.0 >> 11) & 0x03 { + value @ 0..=2 => Ok(value as u8), + _ => Err(MaxPacketSizeError { raw: self.0 }), + } + } + + /// Maximum payload across a USB 2.0 high-speed service interval. + /// + /// The caller is responsible for using this only with high-speed periodic + /// endpoints; other speeds encode bandwidth in different fields. + pub const fn high_speed_payload_per_microframe(self) -> Result { + match self.additional_transactions() { + Ok(additional) => Ok(self.packet_size() as u32 * (additional as u32 + 1)), + Err(error) => Err(error), + } + } + + /// Whether this value satisfies the speed- and transfer-type-dependent + /// endpoint limits defined by [USB 2.0]. + /// + /// This includes the packet-size limits from USB 2.0 Table 5-1 and the + /// high-bandwidth encoding from Section 9.6.6. SuperSpeed values return + /// `false`: their endpoint semantics are defined by USB 3.x instead. + /// Interface-level rules are intentionally outside this predicate; in + /// particular, callers must separately enforce the zero-bandwidth + /// requirement for isochronous endpoints in alternate setting zero. + /// + /// [USB 2.0]: https://www.usb.org/document-library/usb-20-specification + #[must_use] + pub const fn is_valid_for_usb2(self, speed: UsbSpeed, transfer_type: TransferType) -> bool { + let raw = self.raw(); + let packet_size = self.packet_size(); + let additional_transactions = (raw >> 11) & 0x03; + + if raw & 0xe000 != 0 { + return false; + } + + match (speed, transfer_type) { + (UsbSpeed::Low, TransferType::Control) => raw == 8, + (UsbSpeed::Low, TransferType::Interrupt) => additional_transactions == 0 && matches!(packet_size, 1..=8), + (UsbSpeed::Low, TransferType::Isochronous | TransferType::Bulk) => false, + (UsbSpeed::Full, TransferType::Control | TransferType::Bulk) => { + additional_transactions == 0 && matches!(packet_size, 8 | 16 | 32 | 64) + } + (UsbSpeed::Full, TransferType::Isochronous) => additional_transactions == 0 && packet_size <= 1023, + (UsbSpeed::Full, TransferType::Interrupt) => additional_transactions == 0 && matches!(packet_size, 1..=64), + (UsbSpeed::High, TransferType::Control) => raw == 64, + (UsbSpeed::High, TransferType::Bulk) => raw == 512, + (UsbSpeed::High, TransferType::Isochronous) => { + raw == 0 || (additional_transactions <= 2 && matches!(packet_size, 1..=1024)) + } + (UsbSpeed::High, TransferType::Interrupt) => { + additional_transactions <= 2 && matches!(packet_size, 1..=1024) + } + (UsbSpeed::Super | UsbSpeed::SuperPlus, _) => false, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validates_low_speed_packet_sizes() { + assert!(MaxPacketSize::from_raw(8).is_valid_for_usb2(UsbSpeed::Low, TransferType::Control)); + assert!(!MaxPacketSize::from_raw(16).is_valid_for_usb2(UsbSpeed::Low, TransferType::Control)); + + assert!(MaxPacketSize::from_raw(1).is_valid_for_usb2(UsbSpeed::Low, TransferType::Interrupt)); + assert!(MaxPacketSize::from_raw(8).is_valid_for_usb2(UsbSpeed::Low, TransferType::Interrupt)); + assert!(!MaxPacketSize::from_raw(0).is_valid_for_usb2(UsbSpeed::Low, TransferType::Interrupt)); + assert!(!MaxPacketSize::from_raw(9).is_valid_for_usb2(UsbSpeed::Low, TransferType::Interrupt)); + assert!(!MaxPacketSize::from_raw(0x0808).is_valid_for_usb2(UsbSpeed::Low, TransferType::Interrupt)); + + assert!(!MaxPacketSize::from_raw(8).is_valid_for_usb2(UsbSpeed::Low, TransferType::Bulk)); + assert!(!MaxPacketSize::from_raw(8).is_valid_for_usb2(UsbSpeed::Low, TransferType::Isochronous)); + } + + #[test] + fn validates_full_speed_packet_sizes() { + for packet_size in [8, 16, 32, 64] { + assert!(MaxPacketSize::from_raw(packet_size).is_valid_for_usb2(UsbSpeed::Full, TransferType::Control)); + assert!(MaxPacketSize::from_raw(packet_size).is_valid_for_usb2(UsbSpeed::Full, TransferType::Bulk)); + } + for packet_size in [0, 1, 7, 9, 63, 65, 512] { + assert!(!MaxPacketSize::from_raw(packet_size).is_valid_for_usb2(UsbSpeed::Full, TransferType::Control)); + assert!(!MaxPacketSize::from_raw(packet_size).is_valid_for_usb2(UsbSpeed::Full, TransferType::Bulk)); + } + + assert!(MaxPacketSize::from_raw(0).is_valid_for_usb2(UsbSpeed::Full, TransferType::Isochronous)); + assert!(MaxPacketSize::from_raw(1023).is_valid_for_usb2(UsbSpeed::Full, TransferType::Isochronous)); + assert!(!MaxPacketSize::from_raw(1024).is_valid_for_usb2(UsbSpeed::Full, TransferType::Isochronous)); + + assert!(MaxPacketSize::from_raw(1).is_valid_for_usb2(UsbSpeed::Full, TransferType::Interrupt)); + assert!(MaxPacketSize::from_raw(64).is_valid_for_usb2(UsbSpeed::Full, TransferType::Interrupt)); + assert!(!MaxPacketSize::from_raw(0).is_valid_for_usb2(UsbSpeed::Full, TransferType::Interrupt)); + assert!(!MaxPacketSize::from_raw(65).is_valid_for_usb2(UsbSpeed::Full, TransferType::Interrupt)); + assert!(!MaxPacketSize::from_raw(0x0840).is_valid_for_usb2(UsbSpeed::Full, TransferType::Interrupt)); + } + + #[test] + fn validates_high_speed_packet_sizes() { + assert!(MaxPacketSize::from_raw(64).is_valid_for_usb2(UsbSpeed::High, TransferType::Control)); + assert!(!MaxPacketSize::from_raw(32).is_valid_for_usb2(UsbSpeed::High, TransferType::Control)); + + assert!(MaxPacketSize::from_raw(512).is_valid_for_usb2(UsbSpeed::High, TransferType::Bulk)); + assert!(!MaxPacketSize::from_raw(64).is_valid_for_usb2(UsbSpeed::High, TransferType::Bulk)); + assert!(!MaxPacketSize::from_raw(1024).is_valid_for_usb2(UsbSpeed::High, TransferType::Bulk)); + + for transfer_type in [TransferType::Isochronous, TransferType::Interrupt] { + assert!(MaxPacketSize::from_raw(1).is_valid_for_usb2(UsbSpeed::High, transfer_type)); + assert!(MaxPacketSize::from_raw(1024).is_valid_for_usb2(UsbSpeed::High, transfer_type)); + assert!(MaxPacketSize::from_raw(0x0c00).is_valid_for_usb2(UsbSpeed::High, transfer_type)); + assert!(MaxPacketSize::from_raw(0x1400).is_valid_for_usb2(UsbSpeed::High, transfer_type)); + assert!(!MaxPacketSize::from_raw(1025).is_valid_for_usb2(UsbSpeed::High, transfer_type)); + assert!(!MaxPacketSize::from_raw(0x1c00).is_valid_for_usb2(UsbSpeed::High, transfer_type)); + assert!(!MaxPacketSize::from_raw(0x2400).is_valid_for_usb2(UsbSpeed::High, transfer_type)); + } + assert!(MaxPacketSize::from_raw(0).is_valid_for_usb2(UsbSpeed::High, TransferType::Isochronous)); + assert!(!MaxPacketSize::from_raw(0x0800).is_valid_for_usb2(UsbSpeed::High, TransferType::Isochronous)); + assert!(!MaxPacketSize::from_raw(0).is_valid_for_usb2(UsbSpeed::High, TransferType::Interrupt)); + } + + #[test] + fn usb2_validation_rejects_superspeed_semantics() { + for speed in [UsbSpeed::Super, UsbSpeed::SuperPlus] { + assert!(!MaxPacketSize::from_raw(512).is_valid_for_usb2(speed, TransferType::Control)); + assert!(!MaxPacketSize::from_raw(1024).is_valid_for_usb2(speed, TransferType::Isochronous)); + assert!(!MaxPacketSize::from_raw(1024).is_valid_for_usb2(speed, TransferType::Bulk)); + assert!(!MaxPacketSize::from_raw(1024).is_valid_for_usb2(speed, TransferType::Interrupt)); + } + } +} diff --git a/crates/ironrdp-usb/src/lib.rs b/crates/ironrdp-usb/src/lib.rs new file mode 100644 index 000000000..2d3b5afaf --- /dev/null +++ b/crates/ironrdp-usb/src/lib.rs @@ -0,0 +1,25 @@ +//! Protocol-independent USB data types and descriptor semantics. +//! +//! This crate is sans-I/O and intentionally knows nothing about usbredir, +//! RDPEUSB, async runtimes, request routing, or server state. +//! +//! The crate is `no_std` and dependency-free. Descriptor views borrow their +//! input, while transfer types are generic over caller-owned buffer and packet +//! storage. The crate itself does not allocate. +//! +//! Parsing is limited to byte layouts defined by USB itself, such as setup +//! packets and descriptors. Parsing usbredir packets, RDPEUSB PDUs, or any +//! other transport framing belongs in the corresponding protocol crate. +//! +//! The typed descriptor model currently follows the USB 2.0 Chapter 9 device +//! framework. USB 3.x values and descriptor identities remain representable and +//! losslessly traversable, but detailed SuperSpeed descriptor semantics are +//! intentionally deferred until a non-RDPEUSB consumer requires them. +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![no_std] + +pub mod control; +pub mod descriptor; +pub mod endpoint; +pub mod transfer; +pub mod value; diff --git a/crates/ironrdp-usb/src/transfer.rs b/crates/ironrdp-usb/src/transfer.rs new file mode 100644 index 000000000..d34cb50ff --- /dev/null +++ b/crates/ironrdp-usb/src/transfer.rs @@ -0,0 +1,144 @@ +//! Protocol-independent USB transfer requests and completions. +//! +//! These are deliberately plain data structures. They describe USB operations +//! without parsing transport packets, allocating buffers, resolving endpoint +//! state, or managing request lifetimes. + +use core::fmt; + +use super::{control::SetupPacket, endpoint::EndpointAddress}; + +/// Non-success outcome of a submitted USB operation. +/// +/// This is unrelated to the payload returned by the USB standard `GET_STATUS` +/// request. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum UsbError { + Cancelled, + Stall, + Timeout, + Overflow, + NoDevice, + Error, +} + +impl fmt::Display for UsbError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Cancelled => "cancelled", + Self::Stall => "stall", + Self::Timeout => "timeout", + Self::Overflow => "overflow", + Self::NoDevice => "no-device", + Self::Error => "error", + }) + } +} + +impl core::error::Error for UsbError {} + +/// Result of a USB operation. +/// +/// For an all-or-nothing operation, `Ok` carries the operation's output. +/// Operations which can fail while still producing data, such as +/// [`TransferCompletion`], report a `UsbResult<()>` status alongside their +/// payload instead. +pub type UsbResult = Result; + +/// One default-control-pipe transfer. +/// +/// Direction and requested length are carried by `setup`. For an IN transfer, +/// `data` is empty. For an OUT transfer, it contains the data stage. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ControlTransferRequest { + pub setup: SetupPacket, + pub data: B, +} + +/// One bulk or interrupt transfer on a non-control endpoint. +/// +/// Direction is carried by `endpoint`. For an IN endpoint, `length` is the +/// maximum requested response and `data` is empty. For an OUT endpoint, +/// `length` describes `data`. +/// +/// INVARIANT: for an OUT transfer, `length` equals the `data` length. +/// Transports validate this at translation time. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct DataTransferRequest { + pub endpoint: EndpointAddress, + pub length: u32, + pub data: B, +} + +/// A bulk transfer request. +/// +/// Whether the active endpoint is actually bulk is device state checked by the +/// handle, not a property duplicated in this transport-independent type. +pub type BulkTransferRequest = DataTransferRequest; + +/// An interrupt transfer request. +/// +/// Whether the active endpoint is actually interrupt is device state checked +/// by the handle. +pub type InterruptTransferRequest = DataTransferRequest; + +/// Completion of a control, bulk, or interrupt transfer. +/// +/// For an IN transfer, `data` contains the received bytes. For an OUT transfer, +/// it is empty. `actual_length` is valid in both directions. +/// +/// INVARIANT: for an IN transfer, `actual_length` equals `data` length; the +/// field carries independent information only for OUT transfers. +/// +/// A failed transfer can still carry partial data, so `status` is reported +/// alongside the payload rather than replacing it. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct TransferCompletion { + pub status: UsbResult<()>, + pub actual_length: u32, + pub data: B, +} + +pub type ControlCompletion = TransferCompletion; +pub type DataCompletion = TransferCompletion; + +/// Host-controller frame number used for isochronous scheduling. +pub type FrameNumber = u32; + +/// Result of one packet in an isochronous transfer. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct IsochronousPacketCompletion { + pub status: UsbResult<()>, + pub actual_length: u32, +} + +/// One isochronous transfer containing one or more packets. +/// +/// `start_frame == None` asks the host controller to schedule the transfer as +/// soon as possible. Packet payload slots are packed in request order. For an +/// IN endpoint `data` is empty; for an OUT endpoint it contains the packet +/// payloads in the same order. Each item in `packets` is a `u32` requested +/// packet length. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct IsochronousTransferRequest { + pub endpoint: EndpointAddress, + pub start_frame: Option, + pub data: B, + pub packets: P, +} + +/// Direction-independent output of an isochronous transfer. +/// +/// For IN, successful packet payloads are concatenated in packet order in +/// `data`; failed packets contribute no bytes. The packet `actual_length` +/// values split the buffer. For OUT, `data` is empty. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct IsochronousTransferOutput { + pub start_frame: FrameNumber, + pub actual_length: u32, + pub data: B, + pub packets: P, +} + +pub type IsoCompletion = UsbResult>; diff --git a/crates/ironrdp-usb/src/value.rs b/crates/ironrdp-usb/src/value.rs new file mode 100644 index 000000000..f56fd7deb --- /dev/null +++ b/crates/ironrdp-usb/src/value.rs @@ -0,0 +1,103 @@ +//! Fundamental USB values which are independent of a transport protocol. + +/// USB transfer direction, always expressed from the host's point of view. +/// +/// Direction is closed because the corresponding USB fields are one bit wide +/// and both possible encodings are defined. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Direction { + /// Host to device. + Out, + /// Device to host. + In, +} + +/// USB bus speed used to interpret speed-dependent descriptor fields. +/// +/// This is a high-level semantic value, not a directly decoded USB wire field. +/// Transports must explicitly map their own speed representation into it. An +/// enum is intentional here because interpreting an unknown future speed as an +/// existing speed would produce incorrect packet-size and power semantics. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum UsbSpeed { + Low, + Full, + High, + Super, + SuperPlus, +} + +impl UsbSpeed { + #[must_use] + pub const fn is_superspeed(self) -> bool { + matches!(self, Self::Super | Self::SuperPlus) + } +} + +/// Transfer type encoded in endpoint descriptor `bmAttributes` bits 1..0. +/// +/// All four possible two-bit encodings are defined by USB, so decoding cannot +/// encounter an unknown transfer type. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum TransferType { + Control = 0, + Isochronous = 1, + Bulk = 2, + Interrupt = 3, +} + +impl TransferType { + #[must_use] + pub const fn from_endpoint_attributes(attributes: u8) -> Self { + match attributes & 0x03 { + 0 => Self::Control, + 1 => Self::Isochronous, + 2 => Self::Bulk, + 3 => Self::Interrupt, + _ => unreachable!(), + } + } +} + +/// Whether every nibble in a USB binary-coded-decimal version is valid. +/// +/// Descriptor accessors expose `bcdUSB` and `bcdDevice` directly as `u16`: +/// wrapping the value would not make it valid, while this predicate states the +/// actual USB constraint. +#[must_use] +pub const fn is_valid_bcd_version(raw: u16) -> bool { + (raw & 0x000f) <= 9 && ((raw >> 4) & 0x000f) <= 9 && ((raw >> 8) & 0x000f) <= 9 && ((raw >> 12) & 0x000f) <= 9 +} + +/// One selected alternate setting of an interface. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct InterfaceSelection { + /// `bInterfaceNumber` of the selected interface. + pub interface: u8, + /// `bAlternateSetting` of the selected alternate. + pub alternate_setting: u8, +} + +/// USB class, subclass, and protocol values. +/// +/// These values deliberately remain open integers: USB-IF and device-class +/// specifications can assign values independently of this crate's release. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ClassCode { + pub class: u8, + pub subclass: u8, + pub protocol: u8, +} + +impl ClassCode { + #[must_use] + pub const fn new(class: u8, subclass: u8, protocol: u8) -> Self { + Self { + class, + subclass, + protocol, + } + } +}