feat(rdpeusb): add the usb translation module - #1683
Conversation
The channel delivers four distinct completion result types, and a caller correlating completions with its own pending requests has to name them uniformly to store or dispatch them. Add `CompletionData`, an enum over the I/O control, internal I/O control, and transfer in/out completion results, so a single type can carry any RDPEUSB completion payload.
…EUSB requests RDPEUSB carries USB operations as TS_URB structures with Windows USBD conventions: URB function codes, USBD transfer flags, pipe and config handles, and USBD status values. A caller that already speaks USB in protocol-independent terms should not have to assemble those by hand, and assembling them by hand is where the TS_URB payload, URB function, transfer envelope, flags, and buffer shape can silently disagree. Add an `usb` module that maps `ironrdp-usb` requests onto complete backend-facing RDPEUSB packets, and maps completions back: - descriptor, configuration, interface, status, feature, and vendor/class control requests; - bulk, interrupt, and isochronous transfers, in both directions; - configuration and interface selection, including the pipe information the client returns; - pipe reset, device reset, and current frame number; - USBD status to `UsbError` mapping on the completion path. The module is sans-I/O and does not allocate request IDs, track device state, or manage pending-request lifetimes. It lives in `ironrdp-rdpeusb` rather than `ironrdp-usb` because selecting TS_URB forms is RDPEUSB knowledge, while its inputs stay protocol-independent. Depend on `ironrdp-usb` and cover the translation with tests in `ironrdp-testsuite-core`.
`UsbBcdVersion` wraps a raw `bcdUSB` value and preserves it without validating its digits. That is a plain USB concept: nothing about it is explained by RDPEUSB, and `ironrdp-usb` now models it as `BcdVersion` so that descriptor accessors can return it directly. Drop the RDPEUSB copy and use the shared type. The two behaviours that really are RDPEUSB-specific stay here as module-private helpers: mapping `bcdUSB` onto the `SupportedUsbVer` capability field, which models only USB 1.0, 1.1, and 2.0, and the USB 2.0 threshold used to guess a device speed the backend did not report.
9900cbf to
0ab0307
Compare
There was a problem hiding this comment.
Adds a new sans-I/O `usb` module to ironrdp-rdpeusb translating protocol-independent ironrdp-usb requests into complete RDPEUSB TS_URB packets and back, plus a small, explicitly-justified bundled refactor (BcdVersion reuse, CompletionData enum). The module is self-contained, extensively tested, has no unwrap/panic paths, and matches MS-RDPEUSB wire conventions everywhere I independently checked against the diff. The two bundled changes (dropping the crate-local UsbBcdVersion for ironrdp_usb::BcdVersion, and adding CompletionData) are both directly consumed by the new module and explicitly called out with rationale in the PR description, so they don't read as unrelated scope. The only issues found are a real, narrow test-coverage gap and one design question about failure-payload handling consistency; neither undermines correctness of the shipped code.
Protocol analysis: accepted — Independently spot-checked the highest-risk mappings (control-transfer dispatch/fallback, feature/status/descriptor URB function selection, bulk/isochronous direction and flag handling, select-configuration/interface pipe-info derivation, completion status mapping, and the bcdUSB-to-SupportedUsbVer refactor) against the diff and found no contradictions with the handoff's conformance assessments. The handoff's own uncertainty notes (WDK constant values, request-side ISO packet descriptor semantics) are reasonable given those values originate outside the local MS-RDPEUSB corpus. The handoff's flagged test gaps for control_transfer()'s canonical GET_CONFIGURATION/GET_INTERFACE/SET_DESCRIPTOR dispatch paths are confirmed real by inspection of the added test file.
| }; | ||
| let TsUrbInKind::CtlTransfer(urb) = request.ts_urb.kind else { | ||
| panic!("noncanonical request lost fields in a typed TS_URB"); | ||
| }; |
There was a problem hiding this comment.
non_blocking / low: The only tests exercising control_transfer() with GET_CONFIGURATION/GET_INTERFACE/CLEAR_FEATURE setup packets deliberately use non-canonical field values so the noncanonical fallback path is hit (asserting they fall back to the generic TsUrbInKind::CtlTransfer). A separate test (configuration_and_interface_queries_use_typed_urbs, line 510) exercises the typed get_configuration()/get_interface() builder functions directly, but nothing calls control_transfer() with a canonical GET_CONFIGURATION/GET_INTERFACE (or the standard SET_DESCRIPTOR) setup packet to confirm control_transfer() itself reaches the typed branch in standard_control_transfer rather than always falling through to the generic URB. A latent guard-condition typo in one of those match arms (e.g. wrong recipient/value/index/length comparison) would not be caught by the current suite.
| let Some(func) = get_status_function(recipient) else { | ||
| return Err(data); | ||
| }; | ||
| Ok(TransferRequest::In(transfer_in( |
There was a problem hiding this comment.
question / low: isochronous_completion() special-cases a failed URB by returning the USB error before requiring the iso_packet list to match the requested packet count, with a comment noting real clients (e.g. FreeRDP) can return an empty packet list on overall transfer failure. select_configuration_completion, select_interface_completion, and current_frame_number_completion have no equivalent allowance: they require the TS_URB_*_RESULT payload variant to be present via ensure_empty_output/pattern match regardless of whether the completion's HRESULT/USBD status indicates failure. If any real client behaves the same way for these completions (omitting the result substructure on failure) as it does for isochronous transfers, translate would hard-fail with a CompletionError on an otherwise valid failed URB completion instead of surfacing the USB error to the caller. Is there evidence (from a reference client) that these payloads are guaranteed present on failure, unlike the isochronous case?
Part of #1516, also see: #1417
What this adds
RDPEUSB carries USB operations as
TS_URBstructures with Windows USBD conventions: URB function codes, USBD transfer flags, pipe and configuration handles, and USBD status values. A caller that already speaks USB in protocol-independent terms should not have to assemble those by hand.More to the point, assembling them by hand is where things go quietly wrong. The
TS_URBpayload, the URB function code, the transfer envelope, the flags, and the buffer shape all have to agree with each other, and nothing in the type system says so.The new
usbmodule mapsironrdp-usbrequests onto complete backend-facing RDPEUSB packets, so those five cannot disagree, and maps completions back:UsbErroron the completion path.The module is sans-I/O. It does not allocate request IDs, track device state, or manage pending-request lifetimes — those belong to the layer above, in #1417.
Why this lives in
ironrdp-rdpeusbChoosing
TS_URBforms and applying Windows USBD conventions is RDPEUSB knowledge, so the adapter belongs here. Its inputs stay protocol-independent, which is exactly what lets #1417 expose a device facade expressed in plain USB terms.Also in this PR
CompletionData— the channel delivers four distinct completion result types, and a caller correlating completions against its own pending requests needs to name them uniformly. This enum lets a single type carry any RDPEUSB completion payload.BcdVersionreuse —io::device::UsbBcdVersionis removed in favour ofironrdp_usb::BcdVersion, which is the same type with the sameis_valid()semantics.ironrdp-rdpeusbispublish = false, so this is not a break in any released API.Tests are in
ironrdp-testsuite-core(tests/rdpeusb/usb.rs) and cover the translation contract: which URB form each request maps to, the flag and handle values carried, and how completions are validated and unpacked.