diff --git a/examples/blocking.rs b/examples/blocking.rs new file mode 100644 index 00000000..01e8bb44 --- /dev/null +++ b/examples/blocking.rs @@ -0,0 +1,83 @@ +use std::time::Duration; + +use nusb::transfer::{Control, ControlType, Recipient}; + +fn main() { + env_logger::init(); + let di = nusb::list_devices() + .unwrap() + .find(|d| d.vendor_id() == 0x59e3 && d.product_id() == 0x0a23) + .expect("device should be connected"); + + println!("Device info: {di:?}"); + + let device = di.open().unwrap(); + + // Linux can make control transfers without claiming an interface + #[cfg(any(target_os = "linux", target_os = "macos"))] + { + let result = device.control_out_blocking( + Control { + control_type: ControlType::Vendor, + recipient: Recipient::Device, + request: 0x81, + value: 0x9999, + index: 0x9999, + }, + &[1, 2, 3, 4], + Duration::from_secs(1), + ); + println!("{result:?}"); + + let mut buf = [0; 64]; + + let len = device + .control_in_blocking( + Control { + control_type: ControlType::Vendor, + recipient: Recipient::Device, + request: 0x81, + value: 0x9999, + index: 0x9999, + }, + &mut buf, + Duration::from_secs(1), + ) + .unwrap(); + + println!("{result:?}, {data:?}", data = &buf[..len]); + } + + // but we also provide an API on the `Interface` to support Windows + let interface = device.claim_interface(0).unwrap(); + + let result = interface.control_out_blocking( + Control { + control_type: ControlType::Vendor, + recipient: Recipient::Device, + request: 0x81, + value: 0x9999, + index: 0x9999, + }, + &[1, 2, 3, 4, 5], + Duration::from_secs(1), + ); + println!("{result:?}"); + + let mut buf = [0; 64]; + + let len = interface + .control_in_blocking( + Control { + control_type: ControlType::Vendor, + recipient: Recipient::Device, + request: 0x81, + value: 0x9999, + index: 0x9999, + }, + &mut buf, + Duration::from_secs(1), + ) + .unwrap(); + println!("{data:?}", data = &buf[..len]); +} diff --git a/examples/string_descriptors.rs b/examples/string_descriptors.rs new file mode 100644 index 00000000..f21752f7 --- /dev/null +++ b/examples/string_descriptors.rs @@ -0,0 +1,68 @@ +use std::time::Duration; + +use nusb::{descriptors::language_id::US_ENGLISH, DeviceInfo}; + +fn main() { + env_logger::init(); + for dev in nusb::list_devices().unwrap() { + inspect_device(dev); + } +} + +fn inspect_device(dev: DeviceInfo) { + println!( + "Device {:03}.{:03} ({:04x}:{:04x}) {} {}", + dev.bus_number(), + dev.device_address(), + dev.vendor_id(), + dev.product_id(), + dev.manufacturer_string().unwrap_or(""), + dev.product_string().unwrap_or("") + ); + let dev = match dev.open() { + Ok(dev) => dev, + Err(e) => { + println!("Failed to open device: {}", e); + return; + } + }; + + let timeout = Duration::from_millis(100); + + let dev_descriptor = dev.get_descriptor(0x01, 0, 0, timeout).unwrap(); + if dev_descriptor.len() < 18 + || dev_descriptor[0] as usize > dev_descriptor.len() + || dev_descriptor[1] != 0x01 + { + println!(" Invalid device descriptor: {dev_descriptor:?}"); + return; + } + + let languages: Vec = dev + .get_string_descriptor_supported_languages(timeout) + .map(|i| i.collect()) + .unwrap_or_default(); + println!(" Languages: {languages:02x?}"); + + let language = languages.first().copied().unwrap_or(US_ENGLISH); + + let i_manufacturer = dev_descriptor[14]; + if i_manufacturer != 0 { + let s = dev.get_string_descriptor(i_manufacturer, language, timeout); + println!(" Manufacturer({i_manufacturer}): {s:?}"); + } + + let i_product = dev_descriptor[15]; + if i_product != 0 { + let s = dev.get_string_descriptor(i_product, language, timeout); + println!(" Product({i_product}): {s:?}"); + } + + let i_serial = dev_descriptor[16]; + if i_serial != 0 { + let s = dev.get_string_descriptor(i_serial, language, timeout); + println!(" Serial({i_serial}): {s:?}"); + } + + println!(""); +} diff --git a/src/descriptors.rs b/src/descriptors.rs index 4ae1a4f7..a48e6b7a 100644 --- a/src/descriptors.rs +++ b/src/descriptors.rs @@ -2,7 +2,13 @@ //! //! Descriptors are blocks of data that describe the functionality of a USB device. -use std::{collections::BTreeMap, fmt::{Display, Debug}, io::ErrorKind, iter, ops::Deref}; +use std::{ + collections::BTreeMap, + fmt::{Debug, Display}, + io::ErrorKind, + iter, + ops::Deref, +}; use log::warn; @@ -22,6 +28,16 @@ pub(crate) const DESCRIPTOR_LEN_INTERFACE: u8 = 9; pub(crate) const DESCRIPTOR_TYPE_ENDPOINT: u8 = 0x05; pub(crate) const DESCRIPTOR_LEN_ENDPOINT: u8 = 7; +/// USB defined language IDs for string descriptors. +/// +/// In practice, different language IDs are not used, +/// and device string descriptors are only provided +/// with [`language_id::US_ENGLISH`]. +pub mod language_id { + /// US English + pub const US_ENGLISH: u16 = 0x0409; +} + /// A raw USB descriptor. /// /// Wraps a byte slice to provide access to the bytes of a descriptor by implementing `Deref` to `[u8]`, @@ -287,7 +303,12 @@ impl<'a> Configuration<'a> { struct DebugEntries(F); -impl Debug for DebugEntries where F: Fn() -> I, I: Iterator, I::Item: Debug { +impl Debug for DebugEntries +where + F: Fn() -> I, + I: Iterator, + I::Item: Debug, +{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_list().entries(self.0()).finish() } @@ -301,7 +322,10 @@ impl<'a> Debug for Configuration<'a> { .field("attributes", &self.attributes()) .field("max_power", &self.max_power()) .field("string_index", &self.string_index()) - .field("interface_alt_settings", &DebugEntries(|| self.interface_alt_settings())) + .field( + "interface_alt_settings", + &DebugEntries(|| self.interface_alt_settings()), + ) .finish() } } diff --git a/src/device.rs b/src/device.rs index 1d4873dc..55a87b2d 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,12 +1,20 @@ -use std::sync::Arc; +use std::{io::ErrorKind, sync::Arc, time::Duration}; + +use log::error; use crate::{ descriptors::{ActiveConfigurationError, Configuration}, platform, - transfer::{ControlIn, ControlOut, EndpointType, Queue, RequestBuffer, TransferFuture}, + transfer::{ + Control, ControlIn, ControlOut, EndpointType, Queue, RequestBuffer, TransferError, + TransferFuture, + }, DeviceInfo, Error, }; +const STANDARD_REQUEST_GET_DESCRIPTOR: u8 = 0x06; +const DESCRIPTOR_TYPE_STRING: u8 = 0x03; + /// An opened USB device. /// /// Obtain a `Device` by calling [`DeviceInfo::open`]: @@ -74,6 +82,103 @@ impl Device { self.backend.set_configuration(configuration) } + /// Request a descriptor from the device. + /// + /// The `language_id` should be `0` unless you are requesting a string descriptor. + /// + /// ### Platform-specific details + /// + /// * On Windows, the timeout argument is ignored, and an OS-defined timeout is used. + /// * On Windows, this does not wake suspended devices. Reading their + /// descriptors will return an error. + pub fn get_descriptor( + &self, + desc_type: u8, + desc_index: u8, + language_id: u16, + timeout: Duration, + ) -> Result, Error> { + #[cfg(target_os = "windows")] + { + let _ = timeout; + self.backend + .get_descriptor(desc_type, desc_index, language_id) + } + + #[cfg(not(target_os = "windows"))] + { + use crate::transfer::{ControlType, Recipient}; + + let mut buf = vec![0; 4096]; + let len = self.control_in_blocking( + Control { + control_type: ControlType::Standard, + recipient: Recipient::Device, + request: STANDARD_REQUEST_GET_DESCRIPTOR, + value: ((desc_type as u16) << 8) | desc_index as u16, + index: language_id, + }, + &mut buf, + timeout, + )?; + + buf.truncate(len); + Ok(buf) + } + } + + /// Request the list of supported languages for string descriptors. + /// + /// ### Platform-specific details + /// + /// See notes on [`get_descriptor`][`Self::get_descriptor`]. + pub fn get_string_descriptor_supported_languages( + &self, + timeout: Duration, + ) -> Result, Error> { + let data = self.get_descriptor(DESCRIPTOR_TYPE_STRING, 0, 0, timeout)?; + validate_string_descriptor(&data)?; + + //TODO: Use array_chunks once stable + let mut iter = data.into_iter().skip(2); + Ok(std::iter::from_fn(move || { + Some(u16::from_le_bytes([iter.next()?, iter.next()?])) + })) + } + + /// Request a string descriptor from the device. + /// + /// Almost all devices support only the language ID [`US_ENGLISH`][`crate::descriptors::language_id::US_ENGLISH`]. + /// + /// Unpaired UTF-16 surrogates will be replaced with `�`, like [`String::from_utf16_lossy`]. + /// + /// ### Platform-specific details + /// + /// See notes on [`get_descriptor`][`Self::get_descriptor`]. + pub fn get_string_descriptor( + &self, + desc_index: u8, + language_id: u16, + timeout: Duration, + ) -> Result { + if desc_index == 0 { + return Err(Error::new( + ErrorKind::InvalidInput, + "string descriptor index 0 is reserved for the language table", + )); + } + let data = self.get_descriptor(DESCRIPTOR_TYPE_STRING, desc_index, language_id, timeout)?; + validate_string_descriptor(&data)?; + + Ok(char::decode_utf16( + data[2..] + .chunks_exact(2) + .map(|c| u16::from_le_bytes(c.try_into().unwrap())), + ) + .map(|r| r.unwrap_or(char::REPLACEMENT_CHARACTER)) + .collect::()) + } + /// Reset the device, forcing it to re-enumerate. /// /// This `Device` will no longer be usable, and you should drop it and call @@ -85,7 +190,43 @@ impl Device { self.backend.reset() } - /// Submit a single **IN (device-to-host)** transfer on the default **control** endpoint. + /// Synchronously submit a single **IN (device-to-host)** transfer on the default **control** endpoint. + /// + /// ### Platform-specific notes + /// + /// * Not supported on Windows. You must [claim an interface][`Device::claim_interface`] + /// and use the interface handle to submit transfers. + /// * On Linux, this takes a device-wide lock, so if you have multiple threads, you + /// are better off using the async methods. + #[cfg(any(target_os = "linux", target_os = "macos"))] + pub fn control_in_blocking( + &self, + control: Control, + data: &mut [u8], + timeout: Duration, + ) -> Result { + self.backend.control_in_blocking(control, data, timeout) + } + + /// Synchronously submit a single **OUT (host-to-device)** transfer on the default **control** endpoint. + /// + /// ### Platform-specific notes + /// + /// * Not supported on Windows. You must [claim an interface][`Device::claim_interface`] + /// and use the interface handle to submit transfers. + /// * On Linux, this takes a device-wide lock, so if you have multiple threads, you + /// are better off using the async methods. + #[cfg(any(target_os = "linux", target_os = "macos"))] + pub fn control_out_blocking( + &self, + control: Control, + data: &[u8], + timeout: Duration, + ) -> Result { + self.backend.control_out_blocking(control, data, timeout) + } + + /// Asynchronously submit a single **IN (device-to-host)** transfer on the default **control** endpoint. /// /// ### Example /// @@ -152,6 +293,17 @@ impl Device { } } +fn validate_string_descriptor(data: &[u8]) -> Result<(), Error> { + if data.len() < 2 || data[0] as usize != data.len() || data[1] != DESCRIPTOR_TYPE_STRING { + error!("String descriptor language list read {data:?}, not a valid string descriptor"); + return Err(Error::new( + ErrorKind::InvalidData, + "string descriptor data was invalid", + )); + } + Ok(()) +} + /// An opened interface of a USB device. /// /// Obtain an `Interface` with the [`Device::claim_interface`] method. @@ -174,6 +326,36 @@ impl Interface { self.backend.set_alt_setting(alt_setting) } + /// Synchronously submit a single **IN (device-to-host)** transfer on the default **control** endpoint. + /// + /// ### Platform-specific notes + /// + /// * On Linux, this takes a device-wide lock, so if you have multiple threads, you + /// are better off using the async methods. + pub fn control_in_blocking( + &self, + control: Control, + data: &mut [u8], + timeout: Duration, + ) -> Result { + self.backend.control_in_blocking(control, data, timeout) + } + + /// Synchronously submit a single **OUT (host-to-device)** transfer on the default **control** endpoint. + /// + /// ### Platform-specific notes + /// + /// * On Linux, this takes a device-wide lock, so if you have multiple threads, you + /// are better off using the async methods. + pub fn control_out_blocking( + &self, + control: Control, + data: &[u8], + timeout: Duration, + ) -> Result { + self.backend.control_out_blocking(control, data, timeout) + } + /// Submit a single **IN (device-to-host)** transfer on the default **control** endpoint. /// /// ### Example diff --git a/src/platform/linux_usbfs/device.rs b/src/platform/linux_usbfs/device.rs index 9be49a6c..590a49d4 100644 --- a/src/platform/linux_usbfs/device.rs +++ b/src/platform/linux_usbfs/device.rs @@ -1,3 +1,4 @@ +use std::{ffi::c_void, time::Duration}; use std::{ fs::File, io::Read, @@ -17,13 +18,15 @@ use rustix::{ }; use super::{ - events, + errno_to_transfer_error, events, usbfs::{self, Urb}, SysfsPath, }; use crate::{ descriptors::{parse_concatenated_config_descriptors, DESCRIPTOR_LEN_DEVICE}, - transfer::{notify_completion, EndpointType, TransferHandle}, + transfer::{ + notify_completion, Control, Direction, EndpointType, TransferError, TransferHandle, + }, DeviceInfo, Error, }; @@ -144,6 +147,68 @@ impl LinuxDevice { Ok(()) } + /// SAFETY: `data` must be valid for `len` bytes to read or write, depending on `Direction` + unsafe fn control_blocking( + &self, + direction: Direction, + control: Control, + data: *mut u8, + len: usize, + timeout: Duration, + ) -> Result { + let r = usbfs::control( + &self.fd, + usbfs::CtrlTransfer { + bRequestType: control.request_type(direction), + bRequest: control.request, + wValue: control.value, + wIndex: control.index, + wLength: len.try_into().expect("length must fit in u16"), + timeout: timeout + .as_millis() + .try_into() + .expect("timeout must fit in u32 ms"), + data: data as *mut c_void, + }, + ); + + r.map_err(errno_to_transfer_error) + } + + pub fn control_in_blocking( + &self, + control: Control, + data: &mut [u8], + timeout: Duration, + ) -> Result { + unsafe { + self.control_blocking( + Direction::In, + control, + data.as_mut_ptr(), + data.len(), + timeout, + ) + } + } + + pub fn control_out_blocking( + &self, + control: Control, + data: &[u8], + timeout: Duration, + ) -> Result { + unsafe { + self.control_blocking( + Direction::Out, + control, + data.as_ptr() as *mut u8, + data.len(), + timeout, + ) + } + } + pub(crate) fn make_control_transfer(self: &Arc) -> TransferHandle { TransferHandle::new(super::TransferData::new( self.clone(), @@ -223,6 +288,24 @@ impl LinuxInterface { )) } + pub fn control_in_blocking( + &self, + control: Control, + data: &mut [u8], + timeout: Duration, + ) -> Result { + self.device.control_in_blocking(control, data, timeout) + } + + pub fn control_out_blocking( + &self, + control: Control, + data: &[u8], + timeout: Duration, + ) -> Result { + self.device.control_out_blocking(control, data, timeout) + } + pub fn set_alt_setting(&self, alt_setting: u8) -> Result<(), Error> { debug!( "Set interface {} alt setting to {alt_setting}", diff --git a/src/platform/linux_usbfs/mod.rs b/src/platform/linux_usbfs/mod.rs index a6488059..9671aa05 100644 --- a/src/platform/linux_usbfs/mod.rs +++ b/src/platform/linux_usbfs/mod.rs @@ -1,4 +1,5 @@ mod transfer; +use rustix::io::Errno; pub(crate) use transfer::TransferData; mod usbfs; @@ -9,3 +10,17 @@ pub use enumeration::{list_devices, SysfsPath}; mod device; pub(crate) use device::LinuxDevice as Device; pub(crate) use device::LinuxInterface as Interface; + +use crate::transfer::TransferError; + +fn errno_to_transfer_error(e: Errno) -> TransferError { + match e { + Errno::NODEV | Errno::SHUTDOWN => TransferError::Disconnected, + Errno::PIPE => TransferError::Stall, + Errno::NOENT | Errno::CONNRESET | Errno::TIMEDOUT => TransferError::Cancelled, + Errno::PROTO | Errno::ILSEQ | Errno::OVERFLOW | Errno::COMM | Errno::TIME => { + TransferError::Fault + } + _ => TransferError::Unknown, + } +} diff --git a/src/platform/linux_usbfs/transfer.rs b/src/platform/linux_usbfs/transfer.rs index e4a379bb..8cc85100 100644 --- a/src/platform/linux_usbfs/transfer.rs +++ b/src/platform/linux_usbfs/transfer.rs @@ -12,9 +12,12 @@ use crate::transfer::{ RequestBuffer, ResponseBuffer, TransferError, SETUP_PACKET_SIZE, }; -use super::usbfs::{ - Urb, USBDEVFS_URB_TYPE_BULK, USBDEVFS_URB_TYPE_CONTROL, USBDEVFS_URB_TYPE_INTERRUPT, - USBDEVFS_URB_TYPE_ISO, +use super::{ + errno_to_transfer_error, + usbfs::{ + Urb, USBDEVFS_URB_TYPE_BULK, USBDEVFS_URB_TYPE_CONTROL, USBDEVFS_URB_TYPE_INTERRUPT, + USBDEVFS_URB_TYPE_ISO, + }, }; /// Linux-specific transfer state. @@ -213,13 +216,7 @@ fn urb_status(urb: &Urb) -> Result<(), TransferError> { } // It's sometimes positive, sometimes negative, but rustix panics if negative. - Err(match Errno::from_raw_os_error(urb.status.abs()) { - Errno::NODEV | Errno::SHUTDOWN => TransferError::Disconnected, - Errno::PIPE => TransferError::Stall, - Errno::NOENT | Errno::CONNRESET => TransferError::Cancelled, - Errno::PROTO | Errno::ILSEQ | Errno::OVERFLOW | Errno::COMM | Errno::TIME => { - TransferError::Fault - } - _ => TransferError::Unknown, - }) + Err(errno_to_transfer_error(Errno::from_raw_os_error( + urb.status.abs(), + ))) } diff --git a/src/platform/linux_usbfs/usbfs.rs b/src/platform/linux_usbfs/usbfs.rs index 5accdb4e..85880958 100644 --- a/src/platform/linux_usbfs/usbfs.rs +++ b/src/platform/linux_usbfs/usbfs.rs @@ -150,3 +150,53 @@ pub struct Urb { pub usercontext: *mut c_void, // + variable size array of iso_packet_desc } + +pub struct Transfer { + input: Input, + _opcode: PhantomData, +} + +impl Transfer { + #[inline] + pub unsafe fn new(input: Input) -> Self { + Self { + input, + _opcode: PhantomData, + } + } +} + +unsafe impl Ioctl for Transfer { + type Output = usize; + + const IS_MUTATING: bool = true; + const OPCODE: rustix::ioctl::Opcode = Opcode::OPCODE; + + fn as_ptr(&mut self) -> *mut c_void { + &mut self.input as *mut Input as *mut c_void + } + + unsafe fn output_from_ptr(r: IoctlOutput, _: *mut c_void) -> io::Result { + Ok(r as usize) + } +} + +#[repr(C)] +#[allow(non_snake_case)] +pub struct CtrlTransfer { + pub bRequestType: u8, + pub bRequest: u8, + pub wValue: u16, + pub wIndex: u16, + pub wLength: u16, + pub timeout: u32, /* in milliseconds */ + pub data: *mut c_void, +} + +pub fn control(fd: Fd, transfer: CtrlTransfer) -> io::Result { + unsafe { + let ctl = + Transfer::, CtrlTransfer>::new(transfer); + ioctl::ioctl(fd, ctl) + } +} diff --git a/src/platform/macos_iokit/device.rs b/src/platform/macos_iokit/device.rs index a8897ff0..8f849c2a 100644 --- a/src/platform/macos_iokit/device.rs +++ b/src/platform/macos_iokit/device.rs @@ -1,17 +1,19 @@ use std::{ collections::BTreeMap, + ffi::c_void, io::ErrorKind, sync::{ atomic::{AtomicU8, Ordering}, Arc, }, + time::Duration, }; use log::{debug, error}; use crate::{ platform::macos_iokit::events::add_event_source, - transfer::{EndpointType, TransferHandle}, + transfer::{Control, Direction, EndpointType, TransferError, TransferHandle}, DeviceInfo, Error, }; @@ -19,7 +21,9 @@ use super::{ enumeration::service_by_registry_id, events::EventRegistration, iokit::{call_iokit_function, check_iokit_return}, + iokit_c::IOUSBDevRequestTO, iokit_usb::{EndpointInfo, IoKitDevice, IoKitInterface}, + status_to_transfer_result, }; pub(crate) struct MacDevice { @@ -93,6 +97,67 @@ impl MacDevice { } } + /// SAFETY: `data` must be valid for `len` bytes to read or write, depending on `Direction` + unsafe fn control_blocking( + &self, + direction: Direction, + control: Control, + data: *mut u8, + len: usize, + timeout: Duration, + ) -> Result { + let timeout_ms = timeout.as_millis().min(u32::MAX as u128) as u32; + let mut req = IOUSBDevRequestTO { + bmRequestType: control.request_type(direction), + bRequest: control.request, + wValue: control.value, + wIndex: control.index, + wLength: len.try_into().expect("length must fit in u16"), + pData: data.cast::(), + wLenDone: 0, + noDataTimeout: timeout_ms, + completionTimeout: timeout_ms, + }; + + let r = unsafe { call_iokit_function!(self.device.raw, DeviceRequestTO(&mut req)) }; + + status_to_transfer_result(r).map(|()| req.wLenDone as usize) + } + + pub fn control_in_blocking( + &self, + control: Control, + data: &mut [u8], + timeout: Duration, + ) -> Result { + unsafe { + self.control_blocking( + Direction::In, + control, + data.as_mut_ptr(), + data.len(), + timeout, + ) + } + } + + pub fn control_out_blocking( + &self, + control: Control, + data: &[u8], + timeout: Duration, + ) -> Result { + unsafe { + self.control_blocking( + Direction::Out, + control, + data.as_ptr() as *mut u8, + data.len(), + timeout, + ) + } + } + pub(crate) fn make_control_transfer(self: &Arc) -> TransferHandle { TransferHandle::new(super::TransferData::new_control(self.clone())) } @@ -154,6 +219,24 @@ impl MacInterface { } } + pub fn control_in_blocking( + &self, + control: Control, + data: &mut [u8], + timeout: Duration, + ) -> Result { + self.device.control_in_blocking(control, data, timeout) + } + + pub fn control_out_blocking( + &self, + control: Control, + data: &[u8], + timeout: Duration, + ) -> Result { + self.device.control_out_blocking(control, data, timeout) + } + pub fn set_alt_setting(&self, alt_setting: u8) -> Result<(), Error> { debug!( "Set interface {} alt setting to {alt_setting}", diff --git a/src/platform/macos_iokit/mod.rs b/src/platform/macos_iokit/mod.rs index 4242fd43..e29a099a 100644 --- a/src/platform/macos_iokit/mod.rs +++ b/src/platform/macos_iokit/mod.rs @@ -1,4 +1,7 @@ mod transfer; +use io_kit_sys::ret::{ + kIOReturnAborted, kIOReturnNoDevice, kIOReturnSuccess, kIOReturnUnderrun, IOReturn, +}; pub(crate) use transfer::TransferData; mod enumeration; @@ -9,6 +12,19 @@ mod device; pub(crate) use device::MacDevice as Device; pub(crate) use device::MacInterface as Interface; +use crate::transfer::TransferError; + mod iokit; mod iokit_c; mod iokit_usb; + +fn status_to_transfer_result(status: IOReturn) -> Result<(), TransferError> { + #[allow(non_upper_case_globals)] + #[deny(unreachable_patterns)] + match status { + kIOReturnSuccess | kIOReturnUnderrun => Ok(()), + kIOReturnNoDevice => Err(TransferError::Disconnected), + kIOReturnAborted => Err(TransferError::Cancelled), + _ => Err(TransferError::Unknown), + } +} diff --git a/src/platform/macos_iokit/transfer.rs b/src/platform/macos_iokit/transfer.rs index db11e1eb..80b23502 100644 --- a/src/platform/macos_iokit/transfer.rs +++ b/src/platform/macos_iokit/transfer.rs @@ -5,9 +5,7 @@ use std::{ sync::Arc, }; -use io_kit_sys::ret::{ - kIOReturnAborted, kIOReturnNoDevice, kIOReturnSuccess, kIOReturnUnderrun, IOReturn, -}; +use io_kit_sys::ret::{kIOReturnSuccess, IOReturn}; use log::{error, info}; use crate::{ @@ -18,7 +16,7 @@ use crate::{ }, }; -use super::{iokit::call_iokit_function, iokit_usb::EndpointInfo}; +use super::{iokit::call_iokit_function, iokit_usb::EndpointInfo, status_to_transfer_result}; extern "C" fn transfer_callback(refcon: *mut c_void, result: IOReturn, len: *mut c_void) { info!( @@ -146,16 +144,7 @@ impl TransferData { unsafe fn take_status(&mut self) -> (Result<(), TransferError>, usize) { let inner = unsafe { &*self.inner }; - #[allow(non_upper_case_globals)] - #[deny(unreachable_patterns)] - let status = match inner.status { - kIOReturnSuccess | kIOReturnUnderrun => Ok(()), - kIOReturnNoDevice => Err(TransferError::Disconnected), - kIOReturnAborted => Err(TransferError::Cancelled), - _ => Err(TransferError::Unknown), - }; - - (status, inner.actual_len) + (status_to_transfer_result(inner.status), inner.actual_len) } } diff --git a/src/platform/windows_winusb/device.rs b/src/platform/windows_winusb/device.rs index e32ba4f9..eebb7285 100644 --- a/src/platform/windows_winusb/device.rs +++ b/src/platform/windows_winusb/device.rs @@ -1,26 +1,30 @@ use std::{ collections::HashMap, - ffi::OsString, + ffi::{c_void, OsString}, io::{self, ErrorKind}, + mem::size_of_val, os::windows::prelude::OwnedHandle, + ptr::null_mut, sync::Arc, + time::Duration, }; -use log::{debug, error}; +use log::{debug, error, info}; use windows_sys::Win32::{ Devices::{ Properties::DEVPKEY_Device_Address, Usb::{ - WinUsb_Free, WinUsb_Initialize, WinUsb_SetCurrentAlternateSetting, - GUID_DEVINTERFACE_USB_DEVICE, WINUSB_INTERFACE_HANDLE, + WinUsb_ControlTransfer, WinUsb_Free, WinUsb_Initialize, + WinUsb_SetCurrentAlternateSetting, WinUsb_SetPipePolicy, GUID_DEVINTERFACE_USB_DEVICE, + PIPE_TRANSFER_TIMEOUT, WINUSB_INTERFACE_HANDLE, WINUSB_SETUP_PACKET, }, }, - Foundation::{FALSE, TRUE}, + Foundation::{GetLastError, FALSE, TRUE}, }; use crate::{ descriptors::{validate_config_descriptor, DESCRIPTOR_TYPE_CONFIGURATION}, - transfer::{EndpointType, TransferHandle}, + transfer::{Control, Direction, EndpointType, TransferError, TransferHandle}, DeviceInfo, Error, }; @@ -34,6 +38,8 @@ pub(crate) struct WindowsDevice { config_descriptors: Vec>, interface_paths: HashMap, active_config: u8, + hub_handle: HubHandle, + hub_port_number: u32, } impl WindowsDevice { @@ -79,6 +85,8 @@ impl WindowsDevice { interface_paths: d.interfaces.clone(), config_descriptors, active_config: connection_info.CurrentConfigurationValue, + hub_handle, + hub_port_number, })) } @@ -97,6 +105,19 @@ impl WindowsDevice { )) } + pub(crate) fn get_descriptor( + &self, + desc_type: u8, + desc_index: u8, + language_id: u16, + ) -> Result, Error> { + //TODO: this has a race condition in that the device is identified only by port number. If the device is + // disconnected and another device connects to the same port while this `Device` exists, it may return + // descriptors from the new device. + self.hub_handle + .get_descriptor(self.hub_port_number, desc_type, desc_index, language_id) + } + pub(crate) fn reset(&self) -> Result<(), Error> { Err(io::Error::new( ErrorKind::Unsupported, @@ -146,6 +167,111 @@ impl WindowsInterface { TransferHandle::new(super::TransferData::new(self.clone(), endpoint, ep_type)) } + /// SAFETY: `data` must be valid for `len` bytes to read or write, depending on `Direction` + unsafe fn control_blocking( + &self, + direction: Direction, + control: Control, + data: *mut u8, + len: usize, + timeout: Duration, + ) -> Result { + info!("Blocking control {direction:?}, {len} bytes"); + let timeout_ms = timeout.as_millis().min(u32::MAX as u128) as u32; + let r = WinUsb_SetPipePolicy( + self.winusb_handle, + 0, + PIPE_TRANSFER_TIMEOUT, + size_of_val(&timeout_ms) as u32, + &timeout_ms as *const u32 as *const c_void, + ); + + if r != TRUE { + error!( + "WinUsb_SetPipePolicy PIPE_TRANSFER_TIMEOUT failed: {}", + io::Error::last_os_error() + ); + } + + let pkt = WINUSB_SETUP_PACKET { + RequestType: control.request_type(direction), + Request: control.request, + Value: control.value, + Index: control.index, + Length: len.try_into().expect("request size too large"), + }; + + let mut actual_len = 0; + + let r = WinUsb_ControlTransfer( + self.winusb_handle, + pkt, + data, + len.try_into().expect("request size too large"), + &mut actual_len, + null_mut(), + ); + + if r == TRUE { + Ok(actual_len as usize) + } else { + error!( + "WinUsb_ControlTransfer failed: {}", + io::Error::last_os_error() + ); + Err(super::transfer::map_error(GetLastError())) + } + } + + pub fn control_in_blocking( + &self, + control: Control, + data: &mut [u8], + timeout: Duration, + ) -> Result { + unsafe { + self.control_blocking( + Direction::In, + control, + data.as_mut_ptr(), + data.len(), + timeout, + ) + } + } + + pub fn control_out_blocking( + &self, + control: Control, + data: &[u8], + timeout: Duration, + ) -> Result { + // When passed a pointer to read-only memory (e.g. a constant slice), + // WinUSB fails with "Invalid access to memory location. (os error 998)". + // I assume the kernel is checking the pointer for write access + // regardless of the transfer direction. Copy the data to the stack to ensure + // we give it a pointer to writable memory. + let mut buf = [0; 4096]; + let Some(buf) = buf.get_mut(..data.len()) else { + error!( + "Control transfer length {} exceeds limit of 4096", + data.len() + ); + return Err(TransferError::Unknown); + }; + buf.copy_from_slice(data); + + unsafe { + self.control_blocking( + Direction::Out, + control, + buf.as_mut_ptr(), + buf.len(), + timeout, + ) + } + } + pub fn set_alt_setting(&self, alt_setting: u8) -> Result<(), Error> { unsafe { let r = WinUsb_SetCurrentAlternateSetting(raw_handle(&self.handle), alt_setting.into()); diff --git a/src/platform/windows_winusb/hub.rs b/src/platform/windows_winusb/hub.rs index 5a078750..193211bf 100644 --- a/src/platform/windows_winusb/hub.rs +++ b/src/platform/windows_winusb/hub.rs @@ -1,20 +1,21 @@ use std::{ alloc::{self, Layout}, ffi::{c_void, OsStr}, - io, mem, + io::ErrorKind, + mem, os::windows::prelude::OwnedHandle, ptr::{addr_of, null_mut}, slice, }; -use log::error; +use log::{error, warn}; use windows_sys::Win32::{ Devices::Usb::{ GUID_DEVINTERFACE_USB_HUB, IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION, IOCTL_USB_GET_NODE_CONNECTION_INFORMATION_EX, USB_DESCRIPTOR_REQUEST, USB_DESCRIPTOR_REQUEST_0, USB_NODE_CONNECTION_INFORMATION_EX, }, - Foundation::TRUE, + Foundation::{GetLastError, ERROR_GEN_FAILURE, TRUE}, System::IO::DeviceIoControl, }; @@ -69,7 +70,7 @@ impl HubHandle { if r == TRUE { Ok(info) } else { - let err = io::Error::last_os_error(); + let err = Error::last_os_error(); error!("Hub DeviceIoControl failed: {err:?}"); Err(err) } @@ -83,7 +84,10 @@ impl HubHandle { descriptor_index: u8, language_id: u16, ) -> Result, Error> { - let length = 4096; + // Experimentally determined on Windows 10 19045.3803 that this fails + // with ERROR_INVALID_PARAMETER for non-cached descriptors when + // requesting length greater than 4095. + let length = 4095; unsafe { let layout = Layout::from_size_align( @@ -125,9 +129,15 @@ impl HubHandle { let vec = slice::from_raw_parts(start, len).to_owned(); Ok(vec) } else { - let err = io::Error::last_os_error(); - error!("Hub get descriptor failed: {err:?}"); - Err(err) + let err = GetLastError(); + warn!("IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION failed: type={descriptor_type} index={descriptor_index} error={err:?}"); + Err(match err { + ERROR_GEN_FAILURE => Error::new( + ErrorKind::Other, + "Descriptor request failed. Device might be suspended.", + ), + _ => Error::from_raw_os_error(err as i32), + }) }; alloc::dealloc(req as *mut _, layout); diff --git a/src/platform/windows_winusb/transfer.rs b/src/platform/windows_winusb/transfer.rs index 4628520b..a7b73f75 100644 --- a/src/platform/windows_winusb/transfer.rs +++ b/src/platform/windows_winusb/transfer.rs @@ -14,8 +14,8 @@ use windows_sys::Win32::{ }, Foundation::{ GetLastError, ERROR_DEVICE_NOT_CONNECTED, ERROR_FILE_NOT_FOUND, ERROR_GEN_FAILURE, - ERROR_IO_PENDING, ERROR_NOT_FOUND, ERROR_NO_SUCH_DEVICE, ERROR_REQUEST_ABORTED, FALSE, - TRUE, WIN32_ERROR, + ERROR_IO_PENDING, ERROR_NOT_FOUND, ERROR_NO_SUCH_DEVICE, ERROR_REQUEST_ABORTED, + ERROR_SEM_TIMEOUT, ERROR_TIMEOUT, FALSE, TRUE, WIN32_ERROR, }, System::IO::{CancelIoEx, OVERLAPPED}, }; @@ -321,10 +321,10 @@ pub(super) fn handle_event(completion: *mut OVERLAPPED) { } } -fn map_error(err: WIN32_ERROR) -> TransferError { +pub(crate) fn map_error(err: WIN32_ERROR) -> TransferError { match err { ERROR_GEN_FAILURE => TransferError::Stall, - ERROR_REQUEST_ABORTED => TransferError::Cancelled, + ERROR_REQUEST_ABORTED | ERROR_TIMEOUT | ERROR_SEM_TIMEOUT => TransferError::Cancelled, ERROR_FILE_NOT_FOUND | ERROR_DEVICE_NOT_CONNECTED | ERROR_NO_SUCH_DEVICE => { TransferError::Disconnected } diff --git a/src/transfer/control.rs b/src/transfer/control.rs index 2c3bdd8e..710cc2d3 100644 --- a/src/transfer/control.rs +++ b/src/transfer/control.rs @@ -42,6 +42,37 @@ pub enum Recipient { Other = 3, } +/// SETUP packet without direction or buffers +pub struct Control { + /// Request type used for the `bmRequestType` field sent in the SETUP packet. + #[doc(alias = "bmRequestType")] + pub control_type: ControlType, + + /// Recipient used for the `bmRequestType` field sent in the SETUP packet. + #[doc(alias = "bmRequestType")] + pub recipient: Recipient, + + /// `bRequest` field sent in the SETUP packet. + #[doc(alias = "bRequest")] + pub request: u8, + + /// `wValue` field sent in the SETUP packet. + #[doc(alias = "wValue")] + pub value: u16, + + /// `wIndex` field sent in the SETUP packet. + /// + /// For [`Recipient::Interface`] this is the interface number. For [`Recipient::Endpoint`] this is the endpoint number. + #[doc(alias = "wIndex")] + pub index: u16, +} + +impl Control { + pub(crate) fn request_type(&self, direction: Direction) -> u8 { + request_type(direction, self.control_type, self.recipient) + } +} + /// SETUP packet and associated data to make an **OUT** request on a control endpoint. pub struct ControlOut<'a> { /// Request type used for the `bmRequestType` field sent in the SETUP packet. diff --git a/src/transfer/mod.rs b/src/transfer/mod.rs index 6ddd7f52..56b65a84 100644 --- a/src/transfer/mod.rs +++ b/src/transfer/mod.rs @@ -22,7 +22,7 @@ pub use buffer::{RequestBuffer, ResponseBuffer}; mod control; #[allow(unused)] pub(crate) use control::SETUP_PACKET_SIZE; -pub use control::{ControlIn, ControlOut, ControlType, Direction, Recipient}; +pub use control::{Control, ControlIn, ControlOut, ControlType, Direction, Recipient}; mod internal; pub(crate) use internal::{