feat(host/steam): shippable usbip/vhci_hcd virtual Deck + client leave-shortcuts
apple / screenshots (push) Has been cancelled
android / android (push) Has been cancelled
apple / swift (push) Has been cancelled
audit / cargo-audit (push) Has been cancelled
ci / web (push) Has been cancelled
ci / docs-site (push) Has been cancelled
ci / bench (push) Has been cancelled
ci / rust (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
flatpak / build-publish (push) Has been cancelled
release / apple (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
windows-host / package (push) Has been cancelled
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Has been cancelled
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Has been cancelled
windows / build (aarch64-pc-windows-msvc) (push) Has been cancelled
windows / build (x86_64-pc-windows-msvc) (push) Has been cancelled
apple / screenshots (push) Has been cancelled
android / android (push) Has been cancelled
apple / swift (push) Has been cancelled
audit / cargo-audit (push) Has been cancelled
ci / web (push) Has been cancelled
ci / docs-site (push) Has been cancelled
ci / bench (push) Has been cancelled
ci / rust (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
flatpak / build-publish (push) Has been cancelled
release / apple (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
windows-host / package (push) Has been cancelled
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Has been cancelled
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Has been cancelled
windows / build (aarch64-pc-windows-msvc) (push) Has been cancelled
windows / build (x86_64-pc-windows-msvc) (push) Has been cancelled
Steam Deck pass-through (design/steam-deck-passthrough-plan.md), code-complete + all CI checks green on Linux + adversarially reviewed; on-glass validation pending: - usbip/`vhci_hcd` virtual Deck transport (inject/linux/steam_usbip.rs) for non-SteamOS hosts (Bazzite/generic) — presents a real interface-2 USB Deck so Steam Input promotes it. In-process vhci attach (loopback OP_REQ_IMPORT handshake → sysfs attach) with a bounded `usbip`-CLI fallback; detach on drop. - Backed by a vendored, libusb-free trim of the `usbip` crate (crates/punktfunk-host/vendor/usbip-sim, MIT + NOTICE; host/cdc/hid + rusb/nusb removed; interrupt-IN paced by bInterval). - Selection ladder raw_gadget (SteamOS fast-path) → usbip (universal) → UHID, with PUNKTFUNK_STEAM_USBIP / PUNKTFUNK_USBIP_ATTACH knobs. - Shared Deck descriptors + the 0x83/0xAE feature contract + a Steam-accepted serial consolidated into steam_proto.rs; the raw_gadget backend reuses them. - Linux client leave-shortcuts: Ctrl+Alt+Shift+D + holding the escape chord (L1+R1+Start+Select) >=1.5s end the session (short press still exits fullscreen); the chord state resets across sessions. Also bundles in-progress work already staged in the tree: - host(kwin): xdg-output logical-geometry mapping so the KWin fake_input backend places absolute coordinates correctly under display scaling. - docs: design/README index entries + design/controller-only-mode.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
# Vendored + trimmed copy of the `usbip` crate (jiegec/usbip v0.8.0, MIT), reduced to the
|
||||
# USB/IP *server simulation* path only: we present a virtual Steam Deck and let the local
|
||||
# `vhci_hcd` attach it. The upstream crate hard-depends on `rusb`→`libusb1-sys` (for its USB
|
||||
# *host* mode, which we do not use and which would add a libusb runtime dep + break `musl`),
|
||||
# so the host modules (`host.rs`, the `rusb`/`nusb` device constructors) and the helper
|
||||
# interface handlers (`cdc.rs`/`hid.rs`) are removed. What remains — the device model, the
|
||||
# USB/IP protocol framing, and the `UsbInterfaceHandler` trait — is pure `std` + `tokio` and
|
||||
# carries no libusb dependency. See `NOTICE` for upstream attribution.
|
||||
[package]
|
||||
name = "usbip-sim"
|
||||
version = "0.8.0"
|
||||
edition = "2021"
|
||||
description = "Trimmed usbip server-simulation core (no libusb) — vendored for the virtual Steam Deck"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "usbip_sim"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
num-derive = "0.4"
|
||||
num-traits = "0.2"
|
||||
# `time` is for the interrupt-IN pacing added in device.rs (punktfunk modification — see NOTICE).
|
||||
tokio = { version = "1", features = ["rt", "net", "io-util", "sync", "time"] }
|
||||
# Upstream gated its struct derives behind a `serde` feature; kept (off by default) so the
|
||||
# `#[cfg(feature = "serde")]` attributes stay valid and the vendored diff stays minimal.
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
serde = ["dep:serde"]
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2025 Jiajie Chen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
This crate (`usbip-sim`) is a vendored, trimmed copy of:
|
||||
|
||||
usbip v0.8.0
|
||||
Copyright (c) Jiajie Chen <c@jia.je> and contributors
|
||||
https://github.com/jiegec/usbip
|
||||
Licensed under the MIT License.
|
||||
|
||||
Modifications by the punktfunk project:
|
||||
- Removed the USB host modules (`src/host.rs`) and the `rusb`/`nusb` device
|
||||
constructors in `src/lib.rs` (`with_rusb_*`, `with_nusb_*`, `new_from_host*`),
|
||||
eliminating the libusb runtime dependency (which also broke `musl`).
|
||||
- Removed the example helper interface handlers `src/cdc.rs` and `src/hid.rs`.
|
||||
- Replaced the `rusb::Direction` re-export and `rusb::Version` conversions with
|
||||
local definitions.
|
||||
- Dropped the in-crate test modules (kept the library surface only).
|
||||
- Paced interrupt/bulk IN endpoint transfers by bInterval in `device.rs`
|
||||
`handle_urb` (so a simulated interrupt-IN mimics a real device's
|
||||
NAK-until-bInterval behaviour rather than free-running over the loopback
|
||||
link); added the tokio `time` feature for it.
|
||||
|
||||
Only the USB/IP server *simulation* path is retained: the device model, the
|
||||
USB/IP wire protocol, and the `UsbInterfaceHandler` trait. The original MIT
|
||||
license text is reproduced in LICENSE-MIT.
|
||||
@@ -0,0 +1,122 @@
|
||||
use super::*;
|
||||
|
||||
/// A list of known USB speeds
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum UsbSpeed {
|
||||
Unknown = 0x0,
|
||||
Low,
|
||||
Full,
|
||||
High,
|
||||
Wireless,
|
||||
Super,
|
||||
SuperPlus,
|
||||
}
|
||||
|
||||
/// A list of defined USB class codes
|
||||
// https://www.usb.org/defined-class-codes
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum ClassCode {
|
||||
SeeInterface = 0,
|
||||
Audio,
|
||||
CDC,
|
||||
HID,
|
||||
Physical = 0x05,
|
||||
Image,
|
||||
Printer,
|
||||
MassStorage,
|
||||
Hub,
|
||||
CDCData,
|
||||
SmartCard,
|
||||
ContentSecurity = 0x0D,
|
||||
Video,
|
||||
PersonalHealthcare,
|
||||
AudioVideo,
|
||||
Billboard,
|
||||
TypeCBridge,
|
||||
Diagnostic = 0xDC,
|
||||
WirelessController = 0xE0,
|
||||
Misc = 0xEF,
|
||||
ApplicationSpecific = 0xFE,
|
||||
VendorSpecific = 0xFF,
|
||||
}
|
||||
|
||||
/// A list of defined USB endpoint attributes
|
||||
#[derive(Copy, Clone, Debug, FromPrimitive)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum EndpointAttributes {
|
||||
Control = 0,
|
||||
Isochronous,
|
||||
Bulk,
|
||||
Interrupt,
|
||||
}
|
||||
|
||||
/// USB endpoint direction: IN or OUT.
|
||||
///
|
||||
/// Upstream re-exported `rusb::Direction`; vendored locally so this crate carries no libusb
|
||||
/// dependency. `UsbEndpoint::direction()` returns this, and `device.rs` matches on the variants.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum Direction {
|
||||
/// Host → device (`bEndpointAddress` bit 7 clear).
|
||||
Out,
|
||||
/// Device → host (`bEndpointAddress` bit 7 set).
|
||||
In,
|
||||
}
|
||||
|
||||
/// Emulated max packet size of EP0
|
||||
pub const EP0_MAX_PACKET_SIZE: u16 = 64;
|
||||
|
||||
/// A list of defined USB standard requests
|
||||
/// from USB 2.0 standard Table 9.4. Standard Request Codes
|
||||
#[derive(Copy, Clone, Debug, FromPrimitive)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum StandardRequest {
|
||||
GetStatus = 0,
|
||||
ClearFeature = 1,
|
||||
SetFeature = 3,
|
||||
SetAddress = 5,
|
||||
GetDescriptor = 6,
|
||||
SetDescriptor = 7,
|
||||
GetConfiguration = 8,
|
||||
SetConfiguration = 9,
|
||||
GetInterface = 10,
|
||||
SetInterface = 11,
|
||||
SynchFrame = 12,
|
||||
}
|
||||
|
||||
/// A list of defined USB descriptor types
|
||||
/// from USB 2.0 standard Table 9.5. Descriptor Types
|
||||
#[derive(Copy, Clone, Debug, FromPrimitive)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum DescriptorType {
|
||||
/// DEVICE
|
||||
Device = 1,
|
||||
/// CONFIGURATION
|
||||
Configuration = 2,
|
||||
/// STRING
|
||||
String = 3,
|
||||
/// INTERFACE
|
||||
Interface = 4,
|
||||
/// ENDPOINT
|
||||
Endpoint = 5,
|
||||
/// DEVICE_QUALIFIER
|
||||
DeviceQualifier = 6,
|
||||
/// OTHER_SPEED_CONFIGURATION
|
||||
OtherSpeedConfiguration = 7,
|
||||
/// INTERFACE_POINTER
|
||||
InterfacePower = 8,
|
||||
/// OTG
|
||||
OTG = 9,
|
||||
/// DEBUG
|
||||
Debug = 0xA,
|
||||
/// INTERFACE_ASSOCIATION
|
||||
InterfaceAssociation = 0xB,
|
||||
/// BOS
|
||||
BOS = 0xF,
|
||||
// DEVICE CAPABILITY
|
||||
DeviceCapability = 0x10,
|
||||
/// SUPERSPEED_USB_ENDPOINT_COMPANION
|
||||
SuperspeedUsbEndpointCompanion = 0x30,
|
||||
}
|
||||
@@ -0,0 +1,555 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct Version {
|
||||
pub major: u8,
|
||||
pub minor: u8,
|
||||
pub patch: u8,
|
||||
}
|
||||
|
||||
// (Upstream's `From<rusb::Version>` conversions removed — this crate has no libusb dependency.)
|
||||
|
||||
/// bcdDevice
|
||||
impl From<u16> for Version {
|
||||
fn from(value: u16) -> Self {
|
||||
Self {
|
||||
major: (value >> 8) as u8,
|
||||
minor: ((value >> 4) & 0xF) as u8,
|
||||
patch: (value & 0xF) as u8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represent a USB device
|
||||
#[derive(Clone, Default, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize))]
|
||||
pub struct UsbDevice {
|
||||
pub path: String,
|
||||
pub bus_id: String,
|
||||
pub bus_num: u32,
|
||||
pub dev_num: u32,
|
||||
pub speed: u32,
|
||||
pub vendor_id: u16,
|
||||
pub product_id: u16,
|
||||
pub device_bcd: Version,
|
||||
pub device_class: u8,
|
||||
pub device_subclass: u8,
|
||||
pub device_protocol: u8,
|
||||
pub configuration_value: u8,
|
||||
pub num_configurations: u8,
|
||||
pub interfaces: Vec<UsbInterface>,
|
||||
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
pub device_handler: Option<Arc<Mutex<Box<dyn UsbDeviceHandler + Send>>>>,
|
||||
|
||||
pub usb_version: Version,
|
||||
|
||||
pub(crate) ep0_in: UsbEndpoint,
|
||||
pub(crate) ep0_out: UsbEndpoint,
|
||||
// strings
|
||||
pub(crate) string_pool: HashMap<u8, String>,
|
||||
pub(crate) string_configuration: u8,
|
||||
pub(crate) string_manufacturer: u8,
|
||||
pub(crate) string_product: u8,
|
||||
pub(crate) string_serial: u8,
|
||||
}
|
||||
|
||||
impl UsbDevice {
|
||||
pub fn new(index: u32) -> Self {
|
||||
let mut res = Self {
|
||||
path: "/sys/bus/0/0/0".to_string(),
|
||||
bus_id: "0-0-0".to_string(),
|
||||
dev_num: index,
|
||||
speed: UsbSpeed::High as u32,
|
||||
ep0_in: UsbEndpoint {
|
||||
address: 0x80,
|
||||
attributes: EndpointAttributes::Control as u8,
|
||||
max_packet_size: EP0_MAX_PACKET_SIZE,
|
||||
interval: 0,
|
||||
},
|
||||
ep0_out: UsbEndpoint {
|
||||
address: 0x00,
|
||||
attributes: EndpointAttributes::Control as u8,
|
||||
max_packet_size: EP0_MAX_PACKET_SIZE,
|
||||
interval: 0,
|
||||
},
|
||||
// configured by default
|
||||
configuration_value: 1,
|
||||
num_configurations: 1,
|
||||
..Self::default()
|
||||
};
|
||||
res.string_configuration = res.new_string("Default Configuration");
|
||||
res.string_manufacturer = res.new_string("Manufacturer");
|
||||
res.string_product = res.new_string("Product");
|
||||
res.string_serial = res.new_string("Serial");
|
||||
res
|
||||
}
|
||||
|
||||
/// Returns the old value, if present.
|
||||
pub fn set_configuration_name(&mut self, name: &str) -> Option<String> {
|
||||
let old = (self.string_configuration != 0)
|
||||
.then(|| self.string_pool.remove(&self.string_configuration))
|
||||
.flatten();
|
||||
self.string_configuration = self.new_string(name);
|
||||
old
|
||||
}
|
||||
|
||||
/// Unset configuration name and returns the old value, if present.
|
||||
pub fn unset_configuration_name(&mut self) -> Option<String> {
|
||||
let old = (self.string_configuration != 0)
|
||||
.then(|| self.string_pool.remove(&self.string_configuration))
|
||||
.flatten();
|
||||
self.string_configuration = 0;
|
||||
old
|
||||
}
|
||||
|
||||
/// Returns the old value, if present.
|
||||
pub fn set_serial_number(&mut self, name: &str) -> Option<String> {
|
||||
let old = (self.string_serial != 0)
|
||||
.then(|| self.string_pool.remove(&self.string_serial))
|
||||
.flatten();
|
||||
self.string_serial = self.new_string(name);
|
||||
old
|
||||
}
|
||||
|
||||
/// Unset serial number and returns the old value, if present.
|
||||
pub fn unset_serial_number(&mut self) -> Option<String> {
|
||||
let old = (self.string_serial != 0)
|
||||
.then(|| self.string_pool.remove(&self.string_serial))
|
||||
.flatten();
|
||||
self.string_serial = 0;
|
||||
old
|
||||
}
|
||||
|
||||
/// Returns the old value, if present.
|
||||
pub fn set_product_name(&mut self, name: &str) -> Option<String> {
|
||||
let old = (self.string_product != 0)
|
||||
.then(|| self.string_pool.remove(&self.string_product))
|
||||
.flatten();
|
||||
self.string_product = self.new_string(name);
|
||||
old
|
||||
}
|
||||
|
||||
/// Unset product name and returns the old value, if present.
|
||||
pub fn unset_product_name(&mut self) -> Option<String> {
|
||||
let old = (self.string_product != 0)
|
||||
.then(|| self.string_pool.remove(&self.string_product))
|
||||
.flatten();
|
||||
self.string_product = 0;
|
||||
old
|
||||
}
|
||||
|
||||
/// Returns the old value, if present.
|
||||
pub fn set_manufacturer_name(&mut self, name: &str) -> Option<String> {
|
||||
let old = (self.string_manufacturer != 0)
|
||||
.then(|| self.string_pool.remove(&self.string_manufacturer))
|
||||
.flatten();
|
||||
self.string_manufacturer = self.new_string(name);
|
||||
old
|
||||
}
|
||||
|
||||
/// Unset manufacturer name and returns the old value, if present.
|
||||
pub fn unset_manufacturer_name(&mut self) -> Option<String> {
|
||||
let old = (self.string_manufacturer != 0)
|
||||
.then(|| self.string_pool.remove(&self.string_manufacturer))
|
||||
.flatten();
|
||||
self.string_manufacturer = 0;
|
||||
old
|
||||
}
|
||||
|
||||
pub fn with_interface(
|
||||
mut self,
|
||||
interface_class: u8,
|
||||
interface_subclass: u8,
|
||||
interface_protocol: u8,
|
||||
name: Option<&str>,
|
||||
endpoints: Vec<UsbEndpoint>,
|
||||
handler: Arc<Mutex<Box<dyn UsbInterfaceHandler + Send>>>,
|
||||
) -> Self {
|
||||
let string_interface = name.map(|name| self.new_string(name)).unwrap_or(0);
|
||||
let class_specific_descriptor = handler.lock().unwrap().get_class_specific_descriptor();
|
||||
self.interfaces.push(UsbInterface {
|
||||
interface_class,
|
||||
interface_subclass,
|
||||
interface_protocol,
|
||||
endpoints,
|
||||
string_interface,
|
||||
class_specific_descriptor,
|
||||
handler,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_device_handler(
|
||||
mut self,
|
||||
handler: Arc<Mutex<Box<dyn UsbDeviceHandler + Send>>>,
|
||||
) -> Self {
|
||||
self.device_handler = Some(handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn new_string(&mut self, s: &str) -> u8 {
|
||||
for i in 1.. {
|
||||
if let std::collections::hash_map::Entry::Vacant(e) = self.string_pool.entry(i) {
|
||||
e.insert(s.to_string());
|
||||
return i;
|
||||
}
|
||||
}
|
||||
panic!("string poll exhausted")
|
||||
}
|
||||
|
||||
pub(crate) fn find_ep(&self, ep: u8) -> Option<(UsbEndpoint, Option<&UsbInterface>)> {
|
||||
if ep == self.ep0_in.address {
|
||||
Some((self.ep0_in, None))
|
||||
} else if ep == self.ep0_out.address {
|
||||
Some((self.ep0_out, None))
|
||||
} else {
|
||||
for intf in &self.interfaces {
|
||||
for endpoint in &intf.endpoints {
|
||||
if endpoint.address == ep {
|
||||
return Some((*endpoint, Some(intf)));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut result = Vec::with_capacity(312);
|
||||
|
||||
let mut path = self.path.as_bytes().to_vec();
|
||||
debug_assert!(path.len() <= 256);
|
||||
path.resize(256, 0);
|
||||
result.extend_from_slice(path.as_slice());
|
||||
|
||||
let mut bus_id = self.bus_id.as_bytes().to_vec();
|
||||
debug_assert!(bus_id.len() <= 32);
|
||||
bus_id.resize(32, 0);
|
||||
result.extend_from_slice(bus_id.as_slice());
|
||||
|
||||
result.extend_from_slice(&self.bus_num.to_be_bytes());
|
||||
result.extend_from_slice(&self.dev_num.to_be_bytes());
|
||||
result.extend_from_slice(&self.speed.to_be_bytes());
|
||||
result.extend_from_slice(&self.vendor_id.to_be_bytes());
|
||||
result.extend_from_slice(&self.product_id.to_be_bytes());
|
||||
result.push(self.device_bcd.major);
|
||||
result.push(self.device_bcd.minor);
|
||||
result.push(self.device_class);
|
||||
result.push(self.device_subclass);
|
||||
result.push(self.device_protocol);
|
||||
result.push(self.configuration_value);
|
||||
result.push(self.num_configurations);
|
||||
result.push(self.interfaces.len() as u8);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn to_bytes_with_interfaces(&self) -> Vec<u8> {
|
||||
let mut result = self.to_bytes();
|
||||
result.reserve(4 * self.interfaces.len());
|
||||
|
||||
for intf in &self.interfaces {
|
||||
result.push(intf.interface_class);
|
||||
result.push(intf.interface_subclass);
|
||||
result.push(intf.interface_protocol);
|
||||
result.push(0); // padding
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_urb(
|
||||
&self,
|
||||
ep: UsbEndpoint,
|
||||
intf: Option<&UsbInterface>,
|
||||
transfer_buffer_length: u32,
|
||||
setup_packet: SetupPacket,
|
||||
out_data: &[u8],
|
||||
) -> Result<Vec<u8>> {
|
||||
use DescriptorType::*;
|
||||
use Direction::*;
|
||||
use EndpointAttributes::*;
|
||||
use StandardRequest::*;
|
||||
|
||||
match (FromPrimitive::from_u8(ep.attributes), ep.direction()) {
|
||||
(Some(Control), In) => {
|
||||
// control in
|
||||
debug!("Control IN setup={setup_packet:x?}");
|
||||
match (
|
||||
setup_packet.request_type,
|
||||
FromPrimitive::from_u8(setup_packet.request),
|
||||
) {
|
||||
(0b10000000, Some(GetDescriptor)) => {
|
||||
// high byte: type
|
||||
match FromPrimitive::from_u16(setup_packet.value >> 8) {
|
||||
Some(Device) => {
|
||||
debug!("Get device descriptor");
|
||||
// Standard Device Descriptor
|
||||
let mut desc = vec![
|
||||
0x12, // bLength
|
||||
Device as u8, // bDescriptorType: Device
|
||||
self.usb_version.minor,
|
||||
self.usb_version.major, // bcdUSB: USB 2.0
|
||||
self.device_class, // bDeviceClass
|
||||
self.device_subclass, // bDeviceSubClass
|
||||
self.device_protocol, // bDeviceProtocol
|
||||
self.ep0_in.max_packet_size as u8, // bMaxPacketSize0
|
||||
self.vendor_id as u8, // idVendor
|
||||
(self.vendor_id >> 8) as u8,
|
||||
self.product_id as u8, // idProduct
|
||||
(self.product_id >> 8) as u8,
|
||||
self.device_bcd.minor, // bcdDevice
|
||||
self.device_bcd.major,
|
||||
self.string_manufacturer, // iManufacturer
|
||||
self.string_product, // iProduct
|
||||
self.string_serial, // iSerial
|
||||
self.num_configurations, // bNumConfigurations
|
||||
];
|
||||
|
||||
// requested len too short: wLength < real length
|
||||
if setup_packet.length < desc.len() as u16 {
|
||||
desc.resize(setup_packet.length as usize, 0);
|
||||
}
|
||||
Ok(desc)
|
||||
}
|
||||
Some(BOS) => {
|
||||
debug!("Get BOS descriptor");
|
||||
let mut desc = vec![
|
||||
0x05, // bLength
|
||||
BOS as u8, // bDescriptorType: BOS
|
||||
0x05, 0x00, // wTotalLength
|
||||
0x00, // bNumCapabilities
|
||||
];
|
||||
|
||||
// requested len too short: wLength < real length
|
||||
if setup_packet.length < desc.len() as u16 {
|
||||
desc.resize(setup_packet.length as usize, 0);
|
||||
}
|
||||
Ok(desc)
|
||||
}
|
||||
Some(Configuration) => {
|
||||
debug!("Get configuration descriptor");
|
||||
// Standard Configuration Descriptor
|
||||
let mut desc = vec![
|
||||
0x09, // bLength
|
||||
Configuration as u8, // bDescriptorType: Configuration
|
||||
0x00,
|
||||
0x00, // wTotalLength: to be filled below
|
||||
self.interfaces.len() as u8, // bNumInterfaces
|
||||
self.configuration_value, // bConfigurationValue
|
||||
self.string_configuration, // iConfiguration
|
||||
0x80, // bmAttributes: Bus Powered
|
||||
0x32, // bMaxPower: 100mA
|
||||
];
|
||||
for (i, intf) in self.interfaces.iter().enumerate() {
|
||||
let mut intf_desc = vec![
|
||||
0x09, // bLength
|
||||
Interface as u8, // bDescriptorType: Interface
|
||||
i as u8, // bInterfaceNum
|
||||
0x00, // bAlternateSettings
|
||||
intf.endpoints.len() as u8, // bNumEndpoints
|
||||
intf.interface_class, // bInterfaceClass
|
||||
intf.interface_subclass, // bInterfaceSubClass
|
||||
intf.interface_protocol, // bInterfaceProtocol
|
||||
intf.string_interface, //iInterface
|
||||
];
|
||||
// class specific endpoint
|
||||
let mut specific = intf.class_specific_descriptor.clone();
|
||||
intf_desc.append(&mut specific);
|
||||
// endpoint descriptors
|
||||
for endpoint in &intf.endpoints {
|
||||
let mut ep_desc = vec![
|
||||
0x07, // bLength
|
||||
Endpoint as u8, // bDescriptorType: Endpoint
|
||||
endpoint.address, // bEndpointAddress
|
||||
endpoint.attributes, // bmAttributes
|
||||
endpoint.max_packet_size as u8,
|
||||
(endpoint.max_packet_size >> 8) as u8, // wMaxPacketSize
|
||||
endpoint.interval, // bInterval
|
||||
];
|
||||
intf_desc.append(&mut ep_desc);
|
||||
}
|
||||
desc.append(&mut intf_desc);
|
||||
}
|
||||
// length
|
||||
let len = desc.len() as u16;
|
||||
desc[2] = len as u8;
|
||||
desc[3] = (len >> 8) as u8;
|
||||
|
||||
// requested len too short: wLength < real length
|
||||
if setup_packet.length < desc.len() as u16 {
|
||||
desc.resize(setup_packet.length as usize, 0);
|
||||
}
|
||||
Ok(desc)
|
||||
}
|
||||
Some(String) => {
|
||||
debug!("Get string descriptor");
|
||||
let index = setup_packet.value as u8;
|
||||
if index == 0 {
|
||||
// String Descriptor Zero, Specifying Languages Supported by the Device
|
||||
// language ids
|
||||
let mut desc = vec![
|
||||
4, // bLength
|
||||
DescriptorType::String as u8, // bDescriptorType
|
||||
0x09,
|
||||
0x04, // wLANGID[0], en-US
|
||||
];
|
||||
// requested len too short: wLength < real length
|
||||
if setup_packet.length < desc.len() as u16 {
|
||||
desc.resize(setup_packet.length as usize, 0);
|
||||
}
|
||||
Ok(desc)
|
||||
} else if let Some(s) = &self.string_pool.get(&index) {
|
||||
// UNICODE String Descriptor
|
||||
let bytes: Vec<u16> = s.encode_utf16().collect();
|
||||
let mut desc = vec![
|
||||
2 + bytes.len() as u8 * 2, // bLength
|
||||
DescriptorType::String as u8, // bDescriptorType
|
||||
];
|
||||
for byte in bytes {
|
||||
desc.push(byte as u8);
|
||||
desc.push((byte >> 8) as u8);
|
||||
}
|
||||
|
||||
// requested len too short: wLength < real length
|
||||
if setup_packet.length < desc.len() as u16 {
|
||||
desc.resize(setup_packet.length as usize, 0);
|
||||
}
|
||||
Ok(desc)
|
||||
} else {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("Invalid string index: {index}"),
|
||||
))
|
||||
}
|
||||
}
|
||||
Some(DeviceQualifier) => {
|
||||
debug!("Get device qualifier descriptor");
|
||||
// Device_Qualifier Descriptor
|
||||
let mut desc = vec![
|
||||
0x0A, // bLength
|
||||
DeviceQualifier as u8, // bDescriptorType: Device Qualifier
|
||||
self.usb_version.minor,
|
||||
self.usb_version.major, // bcdUSB
|
||||
self.device_class, // bDeviceClass
|
||||
self.device_subclass, // bDeviceSUbClass
|
||||
self.device_protocol, // bDeviceProtocol
|
||||
self.ep0_in.max_packet_size as u8, // bMaxPacketSize0
|
||||
self.num_configurations, // bNumConfigurations
|
||||
0x00, // bReserved
|
||||
];
|
||||
|
||||
// requested len too short: wLength < real length
|
||||
if setup_packet.length < desc.len() as u16 {
|
||||
desc.resize(setup_packet.length as usize, 0);
|
||||
}
|
||||
Ok(desc)
|
||||
}
|
||||
_ => {
|
||||
warn!("unknown desc type: {setup_packet:x?}");
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
}
|
||||
_ if setup_packet.request_type & 0xF == 1 => {
|
||||
// to interface
|
||||
// see https://www.beyondlogic.org/usbnutshell/usb6.shtml
|
||||
// only low 8 bits are valid
|
||||
let intf = &self.interfaces[setup_packet.index as usize & 0xFF];
|
||||
let mut handler = intf.handler.lock().unwrap();
|
||||
handler.handle_urb(intf, ep, transfer_buffer_length, setup_packet, out_data)
|
||||
}
|
||||
_ if setup_packet.request_type & 0xF == 0 && self.device_handler.is_some() => {
|
||||
// to device
|
||||
// see https://www.beyondlogic.org/usbnutshell/usb6.shtml
|
||||
let lock = self.device_handler.as_ref().unwrap();
|
||||
let mut handler = lock.lock().unwrap();
|
||||
handler.handle_urb(transfer_buffer_length, setup_packet, out_data)
|
||||
}
|
||||
_ => unimplemented!("control in"),
|
||||
}
|
||||
}
|
||||
(Some(Control), Out) => {
|
||||
// control out
|
||||
debug!("Control OUT setup={setup_packet:x?}");
|
||||
match (
|
||||
setup_packet.request_type,
|
||||
FromPrimitive::from_u8(setup_packet.request),
|
||||
) {
|
||||
(0b00000000, Some(SetConfiguration)) => {
|
||||
let mut desc = vec![
|
||||
self.configuration_value, // bConfigurationValue
|
||||
];
|
||||
|
||||
// requested len too short: wLength < real length
|
||||
if setup_packet.length < desc.len() as u16 {
|
||||
desc.resize(setup_packet.length as usize, 0);
|
||||
}
|
||||
Ok(desc)
|
||||
}
|
||||
_ if setup_packet.request_type & 0xF == 1 => {
|
||||
// to interface
|
||||
// see https://www.beyondlogic.org/usbnutshell/usb6.shtml
|
||||
// only low 8 bits are valid
|
||||
let intf = &self.interfaces[setup_packet.index as usize & 0xFF];
|
||||
let mut handler = intf.handler.lock().unwrap();
|
||||
handler.handle_urb(intf, ep, transfer_buffer_length, setup_packet, out_data)
|
||||
}
|
||||
_ if setup_packet.request_type & 0xF == 0 && self.device_handler.is_some() => {
|
||||
// to device
|
||||
// see https://www.beyondlogic.org/usbnutshell/usb6.shtml
|
||||
let lock = self.device_handler.as_ref().unwrap();
|
||||
let mut handler = lock.lock().unwrap();
|
||||
handler.handle_urb(transfer_buffer_length, setup_packet, out_data)
|
||||
}
|
||||
_ => unimplemented!("control out"),
|
||||
}
|
||||
}
|
||||
(Some(_), _) => {
|
||||
// others (interrupt / bulk / iso transfers to an endpoint)
|
||||
// punktfunk modification: pace IN transfers by bInterval so a virtual interrupt-IN
|
||||
// endpoint mimics a real device's NAK-until-bInterval behaviour instead of
|
||||
// free-running as fast as the transport allows (vhci_hcd does not throttle the
|
||||
// server side, so an unpaced sim would spin the loopback link). HS bInterval N →
|
||||
// 2^(N-1) microframes × 125µs.
|
||||
if let In = ep.direction() {
|
||||
let n = ep.interval.clamp(1, 16) as u32;
|
||||
let period_us = (1u32 << (n - 1)) * 125;
|
||||
tokio::time::sleep(std::time::Duration::from_micros(period_us as u64)).await;
|
||||
}
|
||||
let intf = intf.unwrap();
|
||||
let mut handler = intf.handler.lock().unwrap();
|
||||
handler.handle_urb(intf, ep, transfer_buffer_length, setup_packet, out_data)
|
||||
}
|
||||
_ => unimplemented!("transfer to {:?}", ep),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A handler for URB targeting the device
|
||||
pub trait UsbDeviceHandler: std::fmt::Debug {
|
||||
/// Handle a URB(USB Request Block) targeting at this device
|
||||
///
|
||||
/// When the lower 4 bits of `bmRequestType` is zero and the URB is not handled by the library, this function is called.
|
||||
/// The resulting data should not exceed `transfer_buffer_length`
|
||||
fn handle_urb(
|
||||
&mut self,
|
||||
transfer_buffer_length: u32,
|
||||
setup: SetupPacket,
|
||||
req: &[u8],
|
||||
) -> Result<Vec<u8>>;
|
||||
|
||||
/// Helper to downcast to actual struct
|
||||
///
|
||||
/// Please implement it as:
|
||||
/// ```ignore
|
||||
/// fn as_any(&mut self) -> &mut dyn Any {
|
||||
/// self
|
||||
/// }
|
||||
/// ```
|
||||
fn as_any(&mut self) -> &mut dyn Any;
|
||||
}
|
||||
|
||||
// (In-crate test module removed in the vendored copy — see NOTICE.)
|
||||
@@ -0,0 +1,31 @@
|
||||
use super::*;
|
||||
|
||||
/// Represent a USB endpoint
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct UsbEndpoint {
|
||||
/// bEndpointAddress
|
||||
pub address: u8,
|
||||
/// bmAttributes
|
||||
pub attributes: u8,
|
||||
/// wMaxPacketSize
|
||||
pub max_packet_size: u16,
|
||||
/// bInterval
|
||||
pub interval: u8,
|
||||
}
|
||||
|
||||
impl UsbEndpoint {
|
||||
/// Get direction from MSB of address
|
||||
pub fn direction(&self) -> Direction {
|
||||
if self.address & 0x80 != 0 {
|
||||
Direction::In
|
||||
} else {
|
||||
Direction::Out
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this is endpoint zero
|
||||
pub fn is_ep0(&self) -> bool {
|
||||
self.address & 0x7F == 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use super::*;
|
||||
|
||||
/// Represent a USB interface
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize))]
|
||||
pub struct UsbInterface {
|
||||
pub interface_class: u8,
|
||||
pub interface_subclass: u8,
|
||||
pub interface_protocol: u8,
|
||||
pub endpoints: Vec<UsbEndpoint>,
|
||||
pub string_interface: u8,
|
||||
pub class_specific_descriptor: Vec<u8>,
|
||||
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
pub handler: Arc<Mutex<Box<dyn UsbInterfaceHandler + Send>>>,
|
||||
}
|
||||
|
||||
/// A handler of a custom usb interface
|
||||
pub trait UsbInterfaceHandler: std::fmt::Debug {
|
||||
/// Return the class specific descriptor which is inserted between interface descriptor and endpoint descriptor
|
||||
fn get_class_specific_descriptor(&self) -> Vec<u8>;
|
||||
|
||||
/// Handle a URB(USB Request Block) targeting at this interface
|
||||
///
|
||||
/// Can be one of: control transfer to ep0 or other types of transfer to its endpoint.
|
||||
/// The resulting data should not exceed `transfer_buffer_length`.
|
||||
fn handle_urb(
|
||||
&mut self,
|
||||
interface: &UsbInterface,
|
||||
ep: UsbEndpoint,
|
||||
transfer_buffer_length: u32,
|
||||
setup: SetupPacket,
|
||||
req: &[u8],
|
||||
) -> Result<Vec<u8>>;
|
||||
|
||||
/// Helper to downcast to actual struct
|
||||
///
|
||||
/// Please implement it as:
|
||||
/// ```ignore
|
||||
/// fn as_any(&mut self) -> &mut dyn Any {
|
||||
/// self
|
||||
/// }
|
||||
/// ```
|
||||
fn as_any(&mut self) -> &mut dyn Any;
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
//! A USB/IP server (simulation path only).
|
||||
//!
|
||||
//! Vendored + trimmed from `usbip` v0.8.0 (jiegec/usbip, MIT); the USB *host* modules and the
|
||||
//! `rusb`/`nusb` device constructors are removed so this carries no libusb dependency. See `NOTICE`.
|
||||
|
||||
use log::*;
|
||||
use num_derive::FromPrimitive;
|
||||
use num_traits::FromPrimitive;
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::io::{ErrorKind, Result};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::RwLock;
|
||||
use usbip_protocol::UsbIpCommand;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod consts;
|
||||
mod device;
|
||||
mod endpoint;
|
||||
mod interface;
|
||||
mod setup;
|
||||
pub mod usbip_protocol;
|
||||
mod util;
|
||||
pub use consts::*;
|
||||
pub use device::*;
|
||||
pub use endpoint::*;
|
||||
pub use interface::*;
|
||||
pub use setup::*;
|
||||
pub use util::*;
|
||||
|
||||
use crate::usbip_protocol::{UsbIpResponse, USBIP_RET_SUBMIT, USBIP_RET_UNLINK};
|
||||
|
||||
/// Main struct of a USB/IP server
|
||||
#[derive(Default, Debug)]
|
||||
pub struct UsbIpServer {
|
||||
available_devices: RwLock<Vec<UsbDevice>>,
|
||||
used_devices: RwLock<HashMap<String, UsbDevice>>,
|
||||
}
|
||||
|
||||
impl UsbIpServer {
|
||||
/// Create a [UsbIpServer] with simulated devices
|
||||
pub fn new_simulated(devices: Vec<UsbDevice>) -> Self {
|
||||
Self {
|
||||
available_devices: RwLock::new(devices),
|
||||
used_devices: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn add_device(&self, device: UsbDevice) {
|
||||
self.available_devices.write().await.push(device);
|
||||
}
|
||||
|
||||
pub async fn remove_device(&self, bus_id: &str) -> Result<()> {
|
||||
let mut available_devices = self.available_devices.write().await;
|
||||
|
||||
if let Some(device) = available_devices.iter().position(|d| d.bus_id == bus_id) {
|
||||
available_devices.remove(device);
|
||||
Ok(())
|
||||
} else if let Some(device) = self
|
||||
.used_devices
|
||||
.read()
|
||||
.await
|
||||
.values()
|
||||
.find(|d| d.bus_id == bus_id)
|
||||
{
|
||||
Err(std::io::Error::other(format!(
|
||||
"Device {} is in use",
|
||||
device.bus_id
|
||||
)))
|
||||
} else {
|
||||
Err(std::io::Error::new(
|
||||
ErrorKind::NotFound,
|
||||
format!("Device {bus_id} not found"),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handler<T: AsyncReadExt + AsyncWriteExt + Unpin>(
|
||||
mut socket: &mut T,
|
||||
server: Arc<UsbIpServer>,
|
||||
) -> Result<()> {
|
||||
let mut current_import_device_id: Option<String> = None;
|
||||
loop {
|
||||
let command = UsbIpCommand::read_from_socket(&mut socket).await;
|
||||
if let Err(err) = command {
|
||||
if let Some(dev_id) = current_import_device_id {
|
||||
let mut used_devices = server.used_devices.write().await;
|
||||
let mut available_devices = server.available_devices.write().await;
|
||||
match used_devices.remove(&dev_id) {
|
||||
Some(dev) => available_devices.push(dev),
|
||||
None => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
if err.kind() == ErrorKind::UnexpectedEof {
|
||||
info!("Remote closed the connection");
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
|
||||
let used_devices = server.used_devices.read().await;
|
||||
let mut current_import_device = current_import_device_id
|
||||
.clone()
|
||||
.and_then(|ref id| used_devices.get(id));
|
||||
|
||||
match command.unwrap() {
|
||||
UsbIpCommand::OpReqDevlist { .. } => {
|
||||
trace!("Got OP_REQ_DEVLIST");
|
||||
let devices = server.available_devices.read().await;
|
||||
|
||||
// OP_REP_DEVLIST
|
||||
UsbIpResponse::op_rep_devlist(&devices)
|
||||
.write_to_socket(socket)
|
||||
.await?;
|
||||
trace!("Sent OP_REP_DEVLIST");
|
||||
}
|
||||
UsbIpCommand::OpReqImport { busid, .. } => {
|
||||
trace!("Got OP_REQ_IMPORT");
|
||||
|
||||
current_import_device_id = None;
|
||||
current_import_device = None;
|
||||
std::mem::drop(used_devices);
|
||||
|
||||
let mut used_devices = server.used_devices.write().await;
|
||||
let mut available_devices = server.available_devices.write().await;
|
||||
let busid_compare =
|
||||
&busid[..busid.iter().position(|&x| x == 0).unwrap_or(busid.len())];
|
||||
for (i, dev) in available_devices.iter().enumerate() {
|
||||
if busid_compare == dev.bus_id.as_bytes() {
|
||||
let dev = available_devices.remove(i);
|
||||
let dev_id = dev.bus_id.clone();
|
||||
used_devices.insert(dev.bus_id.clone(), dev);
|
||||
current_import_device_id = dev_id.clone().into();
|
||||
current_import_device = Some(used_devices.get(&dev_id).unwrap());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let res = if let Some(dev) = current_import_device {
|
||||
UsbIpResponse::op_rep_import_success(dev)
|
||||
} else {
|
||||
UsbIpResponse::op_rep_import_fail()
|
||||
};
|
||||
res.write_to_socket(socket).await?;
|
||||
trace!("Sent OP_REP_IMPORT");
|
||||
}
|
||||
UsbIpCommand::UsbIpCmdSubmit {
|
||||
mut header,
|
||||
transfer_buffer_length,
|
||||
setup,
|
||||
data,
|
||||
..
|
||||
} => {
|
||||
trace!("Got USBIP_CMD_SUBMIT");
|
||||
let device = current_import_device.unwrap();
|
||||
|
||||
let out = header.direction == 0;
|
||||
let real_ep = if out { header.ep } else { header.ep | 0x80 };
|
||||
|
||||
header.command = USBIP_RET_SUBMIT.into();
|
||||
|
||||
let res = match device.find_ep(real_ep as u8) {
|
||||
None => {
|
||||
warn!("Endpoint {real_ep:02x?} not found");
|
||||
UsbIpResponse::usbip_ret_submit_fail(&header)
|
||||
}
|
||||
Some((ep, intf)) => {
|
||||
trace!("->Endpoint {ep:02x?}");
|
||||
trace!("->Setup {setup:02x?}");
|
||||
trace!("->Request {data:02x?}");
|
||||
let resp = device
|
||||
.handle_urb(
|
||||
ep,
|
||||
intf,
|
||||
transfer_buffer_length,
|
||||
SetupPacket::parse(&setup),
|
||||
&data,
|
||||
)
|
||||
.await;
|
||||
|
||||
match resp {
|
||||
Ok(resp) => {
|
||||
if out {
|
||||
trace!("<-Wrote {}", data.len());
|
||||
} else {
|
||||
trace!("<-Resp {resp:02x?}");
|
||||
}
|
||||
UsbIpResponse::usbip_ret_submit_success(&header, 0, 0, resp, vec![])
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Error handling URB: {err}");
|
||||
UsbIpResponse::usbip_ret_submit_fail(&header)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
res.write_to_socket(socket).await?;
|
||||
trace!("Sent USBIP_RET_SUBMIT");
|
||||
}
|
||||
UsbIpCommand::UsbIpCmdUnlink {
|
||||
mut header,
|
||||
unlink_seqnum,
|
||||
} => {
|
||||
trace!("Got USBIP_CMD_UNLINK for {unlink_seqnum:10x?}");
|
||||
|
||||
header.command = USBIP_RET_UNLINK.into();
|
||||
|
||||
let res = UsbIpResponse::usbip_ret_unlink_success(&header);
|
||||
res.write_to_socket(socket).await?;
|
||||
trace!("Sent USBIP_RET_UNLINK");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn a USB/IP server at `addr` using [TcpListener]
|
||||
pub async fn server(addr: SocketAddr, server: Arc<UsbIpServer>) {
|
||||
let listener = TcpListener::bind(addr).await.expect("bind to addr");
|
||||
|
||||
let server = async move {
|
||||
loop {
|
||||
match listener.accept().await {
|
||||
Ok((mut socket, _addr)) => {
|
||||
info!("Got connection from {:?}", socket.peer_addr());
|
||||
let new_server = server.clone();
|
||||
tokio::spawn(async move {
|
||||
let res = handler(&mut socket, new_server).await;
|
||||
info!("Handler ended with {res:?}");
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Got error {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
server.await
|
||||
}
|
||||
|
||||
// (Host-mode constructors and in-crate tests removed in the vendored copy — see NOTICE.)
|
||||
@@ -0,0 +1,31 @@
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Parse the SETUP packet of control transfers
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct SetupPacket {
|
||||
/// bmRequestType
|
||||
pub request_type: u8,
|
||||
/// bRequest
|
||||
pub request: u8,
|
||||
/// wValue
|
||||
pub value: u16,
|
||||
/// wIndex
|
||||
pub index: u16,
|
||||
/// wLength
|
||||
pub length: u16,
|
||||
}
|
||||
|
||||
impl SetupPacket {
|
||||
/// Parse a [SetupPacket] from raw setup packet
|
||||
pub fn parse(setup: &[u8; 8]) -> SetupPacket {
|
||||
SetupPacket {
|
||||
request_type: setup[0],
|
||||
request: setup[1],
|
||||
value: ((setup[3] as u16) << 8) | (setup[2] as u16),
|
||||
index: ((setup[5] as u16) << 8) | (setup[4] as u16),
|
||||
length: ((setup[7] as u16) << 8) | (setup[6] as u16),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,498 @@
|
||||
//! USB/IP protocol structs
|
||||
//!
|
||||
//! This module contains declarations of all structs used in the USB/IP protocol,
|
||||
//! as well as functions to serialize and deserialize them to/from byte arrays,
|
||||
//! and functions to send and receive them over a socket.
|
||||
//!
|
||||
//! They are based on the [Linux kernel documentation](https://docs.kernel.org/usb/usbip_protocol.html).
|
||||
|
||||
use log::trace;
|
||||
use std::io::Result;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::UsbDevice;
|
||||
|
||||
/// USB/IP protocol version
|
||||
///
|
||||
/// This is currently the only supported version of USB/IP
|
||||
/// for this library.
|
||||
pub const USBIP_VERSION: u16 = 0x0111;
|
||||
|
||||
/// Command code: Retrieve the list of exported USB devices
|
||||
pub const OP_REQ_DEVLIST: u16 = 0x8005;
|
||||
/// Command code: import a remote USB device
|
||||
pub const OP_REQ_IMPORT: u16 = 0x8003;
|
||||
/// Reply code: The list of exported USB devices
|
||||
pub const OP_REP_DEVLIST: u16 = 0x0005;
|
||||
/// Reply code: Reply to import
|
||||
pub const OP_REP_IMPORT: u16 = 0x0003;
|
||||
|
||||
/// Command code: Submit an URB
|
||||
pub const USBIP_CMD_SUBMIT: u16 = 0x0001;
|
||||
/// Command code: Unlink an URB
|
||||
pub const USBIP_CMD_UNLINK: u16 = 0x0002;
|
||||
/// Reply code: Reply for submitting an URB
|
||||
pub const USBIP_RET_SUBMIT: u16 = 0x0003;
|
||||
/// Reply code: Reply for URB unlink
|
||||
pub const USBIP_RET_UNLINK: u16 = 0x0004;
|
||||
|
||||
/// USB/IP direction
|
||||
///
|
||||
/// NOTE: Must not be confused with rusb::Direction,
|
||||
/// which has the opposite enum values. This is only for
|
||||
/// internal use in the USB/IP protocol.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Direction {
|
||||
Out = 0,
|
||||
In = 1,
|
||||
}
|
||||
|
||||
/// Common header for all context sensitive packets
|
||||
///
|
||||
/// All commands/responses which rely on a device being attached
|
||||
/// to a client use this header.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct UsbIpHeaderBasic {
|
||||
pub command: u32,
|
||||
pub seqnum: u32,
|
||||
pub devid: u32,
|
||||
pub direction: u32,
|
||||
pub ep: u32,
|
||||
}
|
||||
|
||||
impl UsbIpHeaderBasic {
|
||||
/// Converts a byte array into a [UsbIpHeaderBasic].
|
||||
pub fn from_bytes(bytes: &[u8; 20]) -> Self {
|
||||
let result = UsbIpHeaderBasic {
|
||||
command: u32::from_be_bytes(bytes[0..4].try_into().unwrap()),
|
||||
seqnum: u32::from_be_bytes(bytes[4..8].try_into().unwrap()),
|
||||
devid: u32::from_be_bytes(bytes[8..12].try_into().unwrap()),
|
||||
direction: u32::from_be_bytes(bytes[12..16].try_into().unwrap()),
|
||||
ep: u32::from_be_bytes(bytes[16..20].try_into().unwrap()),
|
||||
};
|
||||
// The direction should be 0 or 1
|
||||
debug_assert!(result.direction & 1 == result.direction);
|
||||
result
|
||||
}
|
||||
|
||||
/// Converts the [UsbIpHeaderBasic] into a byte array.
|
||||
pub fn to_bytes(&self) -> [u8; 20] {
|
||||
let mut result = [0u8; 20];
|
||||
result[0..4].copy_from_slice(&self.command.to_be_bytes());
|
||||
result[4..8].copy_from_slice(&self.seqnum.to_be_bytes());
|
||||
result[8..12].copy_from_slice(&self.devid.to_be_bytes());
|
||||
result[12..16].copy_from_slice(&self.direction.to_be_bytes());
|
||||
result[16..20].copy_from_slice(&self.ep.to_be_bytes());
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) async fn read_from_socket_with_command<T: AsyncReadExt + Unpin>(
|
||||
socket: &mut T,
|
||||
command: u16,
|
||||
) -> Result<Self> {
|
||||
let seqnum = socket.read_u32().await?;
|
||||
let devid = socket.read_u32().await?;
|
||||
let direction = socket.read_u32().await?;
|
||||
// The direction should be 0 or 1
|
||||
debug_assert!(direction & 1 == direction);
|
||||
let ep = socket.read_u32().await?;
|
||||
|
||||
Ok(UsbIpHeaderBasic {
|
||||
command: command.into(),
|
||||
seqnum,
|
||||
devid,
|
||||
direction,
|
||||
ep,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Client side commands from the Virtual Host Controller
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum UsbIpCommand {
|
||||
OpReqDevlist {
|
||||
status: u32,
|
||||
},
|
||||
OpReqImport {
|
||||
status: u32,
|
||||
busid: [u8; 32],
|
||||
},
|
||||
UsbIpCmdSubmit {
|
||||
header: UsbIpHeaderBasic,
|
||||
transfer_flags: u32,
|
||||
transfer_buffer_length: u32,
|
||||
start_frame: u32,
|
||||
number_of_packets: u32,
|
||||
interval: u32,
|
||||
setup: [u8; 8],
|
||||
data: Vec<u8>,
|
||||
iso_packet_descriptor: Vec<u8>,
|
||||
},
|
||||
UsbIpCmdUnlink {
|
||||
header: UsbIpHeaderBasic,
|
||||
unlink_seqnum: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl UsbIpCommand {
|
||||
/// Constructs a [UsbIpCommand] from a socket
|
||||
///
|
||||
/// This will consume a variable amount of bytes from the socket.
|
||||
/// It might fail if the bytes does not follow the USB/IP protocol properly.
|
||||
pub async fn read_from_socket<T: AsyncReadExt + Unpin>(socket: &mut T) -> Result<UsbIpCommand> {
|
||||
let version: u16 = socket.read_u16().await?;
|
||||
|
||||
if version != 0 && version != USBIP_VERSION {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"Unknown version: {version:#04X}"
|
||||
)));
|
||||
}
|
||||
|
||||
let command: u16 = socket.read_u16().await?;
|
||||
|
||||
trace!(
|
||||
"Received command: {:#04X} ({}), parsing...",
|
||||
command,
|
||||
match command {
|
||||
OP_REQ_DEVLIST => "OP_REQ_DEVLIST",
|
||||
OP_REQ_IMPORT => "OP_REQ_IMPORT",
|
||||
USBIP_CMD_SUBMIT => "USBIP_CMD_SUBMIT",
|
||||
USBIP_CMD_UNLINK => "USBIP_CMD_UNLINK",
|
||||
_ => "Unknown",
|
||||
}
|
||||
);
|
||||
|
||||
match command {
|
||||
OP_REQ_DEVLIST => {
|
||||
let status = socket.read_u32().await?;
|
||||
debug_assert!(status == 0);
|
||||
|
||||
Ok(UsbIpCommand::OpReqDevlist { status })
|
||||
}
|
||||
OP_REQ_IMPORT => {
|
||||
let status = socket.read_u32().await?;
|
||||
debug_assert!(status == 0);
|
||||
let mut busid = [0; 32];
|
||||
socket.read_exact(&mut busid).await?;
|
||||
|
||||
Ok(UsbIpCommand::OpReqImport { status, busid })
|
||||
}
|
||||
USBIP_CMD_SUBMIT => {
|
||||
let header =
|
||||
UsbIpHeaderBasic::read_from_socket_with_command(socket, USBIP_CMD_SUBMIT)
|
||||
.await?;
|
||||
let transfer_flags = socket.read_u32().await?;
|
||||
let transfer_buffer_length = socket.read_u32().await?;
|
||||
let start_frame = socket.read_u32().await?;
|
||||
let number_of_packets = socket.read_u32().await?;
|
||||
let interval = socket.read_u32().await?;
|
||||
|
||||
let mut setup = [0; 8];
|
||||
socket.read_exact(&mut setup).await?;
|
||||
|
||||
let data = if header.direction == Direction::In as u32 {
|
||||
vec![]
|
||||
} else {
|
||||
let mut data = vec![0; transfer_buffer_length as usize];
|
||||
socket.read_exact(&mut data).await?;
|
||||
data
|
||||
};
|
||||
|
||||
// The kernel docs specifies that this should be set to 0xFFFFFFFF for all
|
||||
// non-ISO packets, however the actual implementation resorts to 0x00000000
|
||||
// https://stackoverflow.com/questions/76899798/usb-ip-what-is-the-size-of-the-iso-packet-descriptor
|
||||
let iso_packet_descriptor =
|
||||
if number_of_packets != 0 && number_of_packets != 0xFFFFFFFF {
|
||||
let mut result = vec![0; 16 * number_of_packets as usize];
|
||||
socket.read_exact(&mut result).await?;
|
||||
result
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
Ok(UsbIpCommand::UsbIpCmdSubmit {
|
||||
header,
|
||||
transfer_flags,
|
||||
transfer_buffer_length,
|
||||
start_frame,
|
||||
number_of_packets,
|
||||
interval,
|
||||
setup,
|
||||
data,
|
||||
iso_packet_descriptor,
|
||||
})
|
||||
}
|
||||
USBIP_CMD_UNLINK => {
|
||||
let header =
|
||||
UsbIpHeaderBasic::read_from_socket_with_command(socket, USBIP_CMD_UNLINK)
|
||||
.await?;
|
||||
let unlink_seqnum = socket.read_u32().await?;
|
||||
|
||||
let mut _padding = [0; 24];
|
||||
socket.read_exact(&mut _padding).await?;
|
||||
|
||||
Ok(UsbIpCommand::UsbIpCmdUnlink {
|
||||
header,
|
||||
unlink_seqnum,
|
||||
})
|
||||
}
|
||||
_ => Err(std::io::Error::other(format!(
|
||||
"Unknown command: {command:#04X}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the [UsbIpCommand] into a byte vector
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
match *self {
|
||||
UsbIpCommand::OpReqDevlist { status } => {
|
||||
let mut result = Vec::with_capacity(8);
|
||||
result.extend_from_slice(&USBIP_VERSION.to_be_bytes());
|
||||
result.extend_from_slice(&OP_REQ_DEVLIST.to_be_bytes());
|
||||
result.extend_from_slice(&status.to_be_bytes());
|
||||
result
|
||||
}
|
||||
UsbIpCommand::OpReqImport { status, busid } => {
|
||||
let mut result = Vec::with_capacity(40);
|
||||
result.extend_from_slice(&USBIP_VERSION.to_be_bytes());
|
||||
result.extend_from_slice(&OP_REQ_IMPORT.to_be_bytes());
|
||||
result.extend_from_slice(&status.to_be_bytes());
|
||||
result.extend_from_slice(&busid);
|
||||
result
|
||||
}
|
||||
UsbIpCommand::UsbIpCmdSubmit {
|
||||
ref header,
|
||||
transfer_flags,
|
||||
transfer_buffer_length,
|
||||
start_frame,
|
||||
number_of_packets,
|
||||
interval,
|
||||
setup,
|
||||
ref data,
|
||||
ref iso_packet_descriptor,
|
||||
} => {
|
||||
debug_assert!(
|
||||
header.direction != Direction::Out as u32
|
||||
|| transfer_buffer_length == data.len() as u32
|
||||
);
|
||||
|
||||
let mut result = Vec::with_capacity(48 + data.len() + iso_packet_descriptor.len());
|
||||
result.extend_from_slice(&header.to_bytes());
|
||||
result.extend_from_slice(&transfer_flags.to_be_bytes());
|
||||
result.extend_from_slice(&transfer_buffer_length.to_be_bytes());
|
||||
result.extend_from_slice(&start_frame.to_be_bytes());
|
||||
result.extend_from_slice(&number_of_packets.to_be_bytes());
|
||||
result.extend_from_slice(&interval.to_be_bytes());
|
||||
result.extend_from_slice(&setup);
|
||||
result.extend_from_slice(data);
|
||||
result.extend_from_slice(iso_packet_descriptor);
|
||||
result
|
||||
}
|
||||
UsbIpCommand::UsbIpCmdUnlink {
|
||||
ref header,
|
||||
unlink_seqnum,
|
||||
} => {
|
||||
let mut result = Vec::with_capacity(48);
|
||||
result.extend_from_slice(&header.to_bytes());
|
||||
result.extend_from_slice(&unlink_seqnum.to_be_bytes());
|
||||
result.extend_from_slice(&[0; 24]);
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Server side responses from the USB Host
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize))]
|
||||
pub enum UsbIpResponse {
|
||||
OpRepDevlist {
|
||||
status: u32,
|
||||
device_count: u32,
|
||||
devices: Vec<UsbDevice>,
|
||||
},
|
||||
OpRepImport {
|
||||
status: u32,
|
||||
device: Option<UsbDevice>,
|
||||
},
|
||||
UsbIpRetSubmit {
|
||||
header: UsbIpHeaderBasic,
|
||||
status: u32,
|
||||
actual_length: u32,
|
||||
start_frame: u32,
|
||||
number_of_packets: u32,
|
||||
error_count: u32,
|
||||
transfer_buffer: Vec<u8>,
|
||||
iso_packet_descriptor: Vec<u8>,
|
||||
},
|
||||
UsbIpRetUnlink {
|
||||
header: UsbIpHeaderBasic,
|
||||
status: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl UsbIpResponse {
|
||||
/// Converts the [UsbIpResponse] into a byte vector
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
match *self {
|
||||
Self::OpRepDevlist {
|
||||
status,
|
||||
device_count,
|
||||
ref devices,
|
||||
} => {
|
||||
let mut result = Vec::with_capacity(
|
||||
12 + devices.len() * 312
|
||||
+ devices
|
||||
.iter()
|
||||
.map(|d| d.interfaces.len() * 4)
|
||||
.sum::<usize>(),
|
||||
);
|
||||
result.extend_from_slice(&USBIP_VERSION.to_be_bytes());
|
||||
result.extend_from_slice(&OP_REP_DEVLIST.to_be_bytes());
|
||||
result.extend_from_slice(&status.to_be_bytes());
|
||||
result.extend_from_slice(&device_count.to_be_bytes());
|
||||
for dev in devices {
|
||||
result.extend_from_slice(&dev.to_bytes_with_interfaces());
|
||||
}
|
||||
result
|
||||
}
|
||||
Self::OpRepImport { status, ref device } => {
|
||||
let mut result = Vec::with_capacity(320);
|
||||
result.extend_from_slice(&USBIP_VERSION.to_be_bytes());
|
||||
result.extend_from_slice(&OP_REP_IMPORT.to_be_bytes());
|
||||
result.extend_from_slice(&status.to_be_bytes());
|
||||
if let Some(device) = device {
|
||||
result.extend_from_slice(&device.to_bytes());
|
||||
}
|
||||
result
|
||||
}
|
||||
Self::UsbIpRetSubmit {
|
||||
ref header,
|
||||
status,
|
||||
actual_length,
|
||||
start_frame,
|
||||
number_of_packets,
|
||||
error_count,
|
||||
ref transfer_buffer,
|
||||
ref iso_packet_descriptor,
|
||||
} => {
|
||||
let mut result =
|
||||
Vec::with_capacity(48 + transfer_buffer.len() + iso_packet_descriptor.len());
|
||||
|
||||
debug_assert!(header.command == USBIP_RET_SUBMIT.into());
|
||||
debug_assert!(if header.direction == Direction::In as u32 {
|
||||
actual_length == transfer_buffer.len() as u32
|
||||
} else {
|
||||
actual_length == 0
|
||||
});
|
||||
|
||||
result.extend_from_slice(&header.to_bytes());
|
||||
result.extend_from_slice(&status.to_be_bytes());
|
||||
result.extend_from_slice(&actual_length.to_be_bytes());
|
||||
result.extend_from_slice(&start_frame.to_be_bytes());
|
||||
result.extend_from_slice(&number_of_packets.to_be_bytes());
|
||||
result.extend_from_slice(&error_count.to_be_bytes());
|
||||
result.extend_from_slice(&[0; 8]);
|
||||
result.extend_from_slice(transfer_buffer);
|
||||
result.extend_from_slice(iso_packet_descriptor);
|
||||
result
|
||||
}
|
||||
Self::UsbIpRetUnlink { ref header, status } => {
|
||||
let mut result = Vec::with_capacity(48);
|
||||
|
||||
debug_assert!(header.command == USBIP_RET_UNLINK.into());
|
||||
|
||||
result.extend_from_slice(&header.to_bytes());
|
||||
result.extend_from_slice(&status.to_be_bytes());
|
||||
result.extend_from_slice(&[0; 24]);
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn write_to_socket<T: AsyncWriteExt + Unpin>(&self, socket: &mut T) -> Result<()> {
|
||||
socket.write_all(&self.to_bytes()).await
|
||||
}
|
||||
|
||||
/// Constructs a OP_REP_DEVLIST response
|
||||
pub fn op_rep_devlist(devices: &[UsbDevice]) -> Self {
|
||||
Self::OpRepDevlist {
|
||||
status: 0,
|
||||
device_count: devices.len() as u32,
|
||||
devices: devices.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a successful OP_REP_IMPORT response
|
||||
pub fn op_rep_import_success(device: &UsbDevice) -> Self {
|
||||
Self::OpRepImport {
|
||||
status: 0,
|
||||
device: Some(device.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a failed OP_REP_IMPORT response
|
||||
pub fn op_rep_import_fail() -> Self {
|
||||
Self::OpRepImport {
|
||||
status: 1,
|
||||
device: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a successful OP_REP_IMPORT response
|
||||
pub fn usbip_ret_submit_success(
|
||||
header: &UsbIpHeaderBasic,
|
||||
start_frame: u32,
|
||||
number_of_packets: u32,
|
||||
transfer_buffer: Vec<u8>,
|
||||
iso_packet_descriptor: Vec<u8>,
|
||||
) -> Self {
|
||||
Self::UsbIpRetSubmit {
|
||||
header: header.clone(),
|
||||
status: 0,
|
||||
actual_length: transfer_buffer.len() as u32,
|
||||
start_frame,
|
||||
number_of_packets,
|
||||
error_count: 0,
|
||||
transfer_buffer,
|
||||
iso_packet_descriptor,
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a failed OP_REP_IMPORT response
|
||||
pub fn usbip_ret_submit_fail(header: &UsbIpHeaderBasic) -> Self {
|
||||
Self::UsbIpRetSubmit {
|
||||
header: header.clone(),
|
||||
status: 1,
|
||||
actual_length: 0,
|
||||
start_frame: 0,
|
||||
number_of_packets: 0,
|
||||
error_count: 0,
|
||||
transfer_buffer: vec![],
|
||||
iso_packet_descriptor: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a successful OP_REP_IMPORT response
|
||||
pub fn usbip_ret_unlink_success(header: &UsbIpHeaderBasic) -> Self {
|
||||
Self::UsbIpRetUnlink {
|
||||
header: header.clone(),
|
||||
status: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a failed OP_REP_IMPORT response.
|
||||
pub fn usbip_ret_unlink_fail(header: &UsbIpHeaderBasic) -> Self {
|
||||
Self::UsbIpRetUnlink {
|
||||
header: header.clone(),
|
||||
status: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// (In-crate test module removed in the vendored copy — see NOTICE.)
|
||||
@@ -0,0 +1,10 @@
|
||||
/// Check validity of a USB descriptor
|
||||
pub fn verify_descriptor(desc: &[u8]) {
|
||||
let mut offset = 0;
|
||||
while offset < desc.len() {
|
||||
offset += desc[offset] as usize; // length
|
||||
}
|
||||
assert_eq!(offset, desc.len());
|
||||
}
|
||||
|
||||
// (In-crate test module removed in the vendored copy — see NOTICE.)
|
||||
Reference in New Issue
Block a user