diff --git a/examples/string_descriptors.rs b/examples/string_descriptors.rs new file mode 100644 index 0000000..f21752f --- /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 1478836..a48e6b7 100644 --- a/src/descriptors.rs +++ b/src/descriptors.rs @@ -28,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]`, diff --git a/src/device.rs b/src/device.rs index dec974c..55a87b2 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,4 +1,6 @@ -use std::{sync::Arc, time::Duration}; +use std::{io::ErrorKind, sync::Arc, time::Duration}; + +use log::error; use crate::{ descriptors::{ActiveConfigurationError, Configuration}, @@ -10,6 +12,9 @@ use crate::{ 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`]: @@ -77,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 @@ -191,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. diff --git a/src/platform/windows_winusb/device.rs b/src/platform/windows_winusb/device.rs index ab91ccd..eebb728 100644 --- a/src/platform/windows_winusb/device.rs +++ b/src/platform/windows_winusb/device.rs @@ -38,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 { @@ -83,6 +85,8 @@ impl WindowsDevice { interface_paths: d.interfaces.clone(), config_descriptors, active_config: connection_info.CurrentConfigurationValue, + hub_handle, + hub_port_number, })) } @@ -101,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, diff --git a/src/platform/windows_winusb/hub.rs b/src/platform/windows_winusb/hub.rs index 5a07875..193211b 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);