fix: USB spec conformance and reliability improvements

Spec conformance:
- Add missing standard control requests for simulated devices:
  GetConfiguration, GetStatus (device/interface/endpoint),
  ClearFeature, SetFeature, SetAddress
- Replace debug_assert with truncate for path/bus_id wire format
  to prevent protocol desync in release builds

Reliability:
- server() now returns Result instead of panicking on bind failure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Davíð Steinn Geirsson 2026-03-25 00:12:36 +00:00
parent 3f4e8effce
commit 804a4910a0
3 changed files with 41 additions and 7 deletions

View file

@ -42,7 +42,7 @@
### 7. Missing standard control requests (simulated devices)
- **File:** `lib/src/device.rs:346-778`
- **Issue:** `GetStatus`, `ClearFeature`, `SetFeature`, `GetConfiguration`, `SetAddress` are unhandled for simulated devices.
- **Status:** [ ] TODO
- **Status:** [x] DONE — Added GetConfiguration, GetStatus (device/interface/endpoint), ClearFeature, SetFeature, SetAddress handlers.
### 8. ISO `actual_length` sum not validated
- **File:** `lib/src/lib.rs:551-552`
@ -60,7 +60,7 @@
### 10. `server()` panics on bind failure
- **File:** `lib/src/lib.rs:773`
- **Issue:** `expect("bind to addr")` panics instead of returning `Result`.
- **Status:** [ ] TODO
- **Status:** [x] DONE — Changed return type to `Result<()>`, replaced `expect` with `?`.
### 11. Error status mapping via string matching
- **File:** `lib/src/lib.rs:584-592`
@ -75,4 +75,4 @@
### 13. `debug_assert` for path/bus_id length
- **File:** `lib/src/device.rs:252-260`
- **Issue:** `debug_assert!` compiled out in release; silent truncation if path > 256 bytes.
- **Status:** [ ] TODO
- **Status:** [x] DONE — Replaced `debug_assert!` + `resize` with `truncate` + `resize`.

View file

@ -257,12 +257,12 @@ impl UsbDevice {
let mut result = Vec::with_capacity(312);
let mut path = self.path.as_bytes().to_vec();
debug_assert!(path.len() <= 256);
path.truncate(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.truncate(32);
bus_id.resize(32, 0);
result.extend_from_slice(bus_id.as_slice());
@ -609,6 +609,25 @@ impl UsbDevice {
}
}
}
(0b10000000, Some(GetConfiguration)) => {
debug!("Get configuration value");
Ok(UrbResponse { data: vec![self.configuration_value], ..Default::default() })
}
(0b10000000, Some(GetStatus)) => {
// Device recipient: self-powered=0, remote-wakeup=0
debug!("Get status (device)");
Ok(UrbResponse { data: vec![0x00, 0x00], ..Default::default() })
}
(0b10000001, Some(GetStatus)) => {
// Interface recipient: reserved, always zero
debug!("Get status (interface)");
Ok(UrbResponse { data: vec![0x00, 0x00], ..Default::default() })
}
(0b10000010, Some(GetStatus)) => {
// Endpoint recipient: halt=0
debug!("Get status (endpoint)");
Ok(UrbResponse { data: vec![0x00, 0x00], ..Default::default() })
}
(0b10000001, Some(GetInterface)) => {
let intf_index = setup_packet.index as usize & 0xFF;
match self.interface_states.get(intf_index) {
@ -718,6 +737,21 @@ impl UsbDevice {
)),
}
}
(0b00000010, Some(ClearFeature)) => {
// Endpoint recipient: no-op (simulated device doesn't stall)
debug!("Clear feature (endpoint)");
Ok(UrbResponse::default())
}
(0b00000010, Some(SetFeature)) => {
// Endpoint recipient: no-op
debug!("Set feature (endpoint)");
Ok(UrbResponse::default())
}
(0b00000000, Some(SetAddress)) => {
// No-op: address already assigned by bus
debug!("Set address (no-op)");
Ok(UrbResponse::default())
}
_ if setup_packet.request_type & 0xF == 1 => {
// to interface
// see https://www.beyondlogic.org/usbnutshell/usb6.shtml

View file

@ -874,8 +874,8 @@ pub async fn handler<T: AsyncReadExt + AsyncWriteExt + Unpin + Send + 'static>(
}
/// 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");
pub async fn server(addr: SocketAddr, server: Arc<UsbIpServer>) -> Result<()> {
let listener = TcpListener::bind(addr).await?;
let server = async move {
loop {