diff --git a/examples/descriptors.rs b/examples/descriptors.rs index fbc2d03..98f46ff 100644 --- a/examples/descriptors.rs +++ b/examples/descriptors.rs @@ -25,6 +25,10 @@ fn inspect_device(dev: DeviceInfo) { } }; + println!("{:#?}", dev.device_descriptor()); + + println!("Speed: {:?}", dev.speed()); + match dev.active_configuration() { Ok(config) => println!("Active configuration is {}", config.configuration_value()), Err(e) => println!("Unknown active configuration: {e}"), diff --git a/examples/string_descriptors.rs b/examples/string_descriptors.rs index 8d81cb0..a2fdcc2 100644 --- a/examples/string_descriptors.rs +++ b/examples/string_descriptors.rs @@ -29,14 +29,7 @@ fn inspect_device(dev: DeviceInfo) { 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 dev_descriptor = dev.device_descriptor(); let languages: Vec = dev .get_string_descriptor_supported_languages(timeout) @@ -46,20 +39,17 @@ fn inspect_device(dev: DeviceInfo) { let language = languages.first().copied().unwrap_or(US_ENGLISH); - let i_manufacturer = dev_descriptor[14]; - if i_manufacturer != 0 { + if let Some(i_manufacturer) = dev_descriptor.manufacturer_string_index() { 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 { + if let Some(i_product) = dev_descriptor.product_string_index() { 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 { + if let Some(i_serial) = dev_descriptor.serial_number_string_index() { let s = dev.get_string_descriptor(i_serial, language, timeout); println!(" Serial({i_serial}): {s:?}"); } diff --git a/src/descriptors.rs b/src/descriptors.rs index 6d8a62f..2f64e16 100644 --- a/src/descriptors.rs +++ b/src/descriptors.rs @@ -17,6 +17,7 @@ use crate::{ Error, }; +pub(crate) const DESCRIPTOR_TYPE_DEVICE: u8 = 0x01; pub(crate) const DESCRIPTOR_LEN_DEVICE: u8 = 18; pub(crate) const DESCRIPTOR_TYPE_CONFIGURATION: u8 = 0x02; @@ -164,13 +165,13 @@ impl<'a> Iterator for Descriptors<'a> { } macro_rules! descriptor_fields { - (impl<'a> $tname:ident<'a> { + (impl $(<$( $i_lt:lifetime ),+>)? $tname:ident $(<$( $t_lt:lifetime ),+>)? { $( $(#[$attr:meta])* $vis:vis fn $name:ident at $pos:literal -> $ty:ty; )* }) => { - impl<'a> $tname<'a> { + impl $(<$( $i_lt ),+>)? $tname $(<$( $t_lt ),+>)? { $( $(#[$attr])* #[inline] @@ -180,6 +181,189 @@ macro_rules! descriptor_fields { } } +/// Check whether the buffer contains a valid device descriptor. +/// On success, it will return length of the descriptor, or returns `None`. +#[allow(unused)] +pub(crate) fn validate_device_descriptor(buf: &[u8]) -> Option { + if buf.len() < DESCRIPTOR_LEN_DEVICE as usize { + if buf.len() != 0 { + warn!( + "device descriptor buffer is {} bytes, need {}", + buf.len(), + DESCRIPTOR_LEN_DEVICE + ); + } + return None; + } + + if buf[0] < DESCRIPTOR_LEN_DEVICE { + warn!("invalid device descriptor bLength"); + return None; + } + + if buf[1] != DESCRIPTOR_TYPE_DEVICE { + warn!( + "device bDescriptorType is {}, not a device descriptor", + buf[1] + ); + return None; + } + + return Some(buf[0] as usize); +} + +/// Information about a USB device. +#[derive(Clone)] +pub struct DeviceDescriptor([u8; DESCRIPTOR_LEN_DEVICE as usize]); + +impl DeviceDescriptor { + /// Create a `DeviceDescriptor` from a buffer beginning with a device descriptor. + /// + /// You normally obtain a `DeviceDescriptor` from a [`Device`][crate::Device], but this allows creating + /// one from your own descriptor bytes for tests. + /// + /// ### Panics + /// * when the buffer is too short for a device descriptor + /// * when the first descriptor is not a device descriptor + pub fn new(buf: &[u8]) -> Self { + assert!(buf.len() >= DESCRIPTOR_LEN_DEVICE as usize); + assert!(buf[0] as usize >= DESCRIPTOR_LEN_DEVICE as usize); + assert!(buf[1] == DESCRIPTOR_TYPE_DEVICE); + Self(buf[0..DESCRIPTOR_LEN_DEVICE as usize].try_into().unwrap()) + } + + /// Get the bytes of the descriptor. + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + #[allow(unused)] + pub(crate) fn from_fields( + usb_version: u16, + class: u8, + subclass: u8, + protocol: u8, + max_packet_size_0: u8, + vendor_id: u16, + product_id: u16, + device_version: u16, + manufacturer_string_index: u8, + product_string_index: u8, + serial_number_string_index: u8, + num_configurations: u8, + ) -> DeviceDescriptor { + DeviceDescriptor([ + DESCRIPTOR_LEN_DEVICE, + DESCRIPTOR_TYPE_DEVICE, + usb_version.to_le_bytes()[0], + usb_version.to_le_bytes()[1], + class, + subclass, + protocol, + max_packet_size_0, + vendor_id.to_le_bytes()[0], + vendor_id.to_le_bytes()[1], + product_id.to_le_bytes()[0], + product_id.to_le_bytes()[1], + device_version.to_le_bytes()[0], + device_version.to_le_bytes()[1], + manufacturer_string_index, + product_string_index, + serial_number_string_index, + num_configurations, + ]) + } +} + +descriptor_fields! { + impl DeviceDescriptor { + /// `bcdUSB` descriptor field: USB Specification Number. + #[doc(alias = "bcdUSB")] + pub fn usb_version at 2 -> u16; + + /// `bDeviceClass` descriptor field: Class code, assigned by USB-IF. + #[doc(alias = "bDeviceClass")] + pub fn class at 4 -> u8; + + /// `bDeviceSubClass` descriptor field: Subclass code, assigned by USB-IF. + #[doc(alias = "bDeviceSubClass")] + pub fn subclass at 5 -> u8; + + /// `bDeviceProtocol` descriptor field: Protocol code, assigned by USB-IF. + #[doc(alias = "bDeviceProtocol")] + pub fn protocol at 6 -> u8; + + /// `bMaxPacketSize0` descriptor field: Maximum packet size for 0 Endpoint. + #[doc(alias = "bMaxPacketSize0")] + pub fn max_packet_size_0 at 7 -> u8; + + /// `idVendor` descriptor field: Vendor ID, assigned by USB-IF. + #[doc(alias = "idVendor")] + pub fn vendor_id at 8 -> u16; + + /// `idProduct` descriptor field: Product ID, assigned by the manufacturer. + #[doc(alias = "idProduct")] + pub fn product_id at 10 -> u16; + + /// `bcdDevice` descriptor field: Device release number. + #[doc(alias = "bcdDevice")] + pub fn device_version at 12 -> u16; + + fn manufacturer_string_index_raw at 14 -> u8; + fn product_string_index_raw at 15 -> u8; + fn serial_number_string_index_raw at 16 -> u8; + + /// `bNumConfigurations` descriptor field: Number of configurations + #[doc(alias = "bNumConfigurations")] + pub fn num_configurations at 17 -> u8; + } +} + +impl DeviceDescriptor { + /// `iManufacturer` descriptor field: Index for manufacturer description string. + pub fn manufacturer_string_index(&self) -> Option { + Some(self.manufacturer_string_index_raw()).filter(|&i| i != 0) + } + + /// `iProduct` descriptor field: Index for product description string. + pub fn product_string_index(&self) -> Option { + Some(self.product_string_index_raw()).filter(|&i| i != 0) + } + + /// `iSerialNumber` descriptor field: Index for serial number string. + pub fn serial_number_string_index(&self) -> Option { + Some(self.serial_number_string_index_raw()).filter(|&i| i != 0) + } +} +impl Debug for DeviceDescriptor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DeviceDescriptor") + .field("usb_version", &format_args!("0x{:04X}", self.usb_version())) + .field("class", &format_args!("0x{:02X}", self.class())) + .field("subclass", &format_args!("0x{:02X}", self.subclass())) + .field("protocol", &format_args!("0x{:02X}", self.protocol())) + .field("max_packet_size_0", &self.max_packet_size_0()) + .field("vendor_id", &format_args!("0x{:04X}", self.vendor_id())) + .field("product_id", &format_args!("0x{:04X}", self.product_id())) + .field( + "device_version", + &format_args!("0x{:04X}", self.device_version()), + ) + .field( + "manufacturer_string_index", + &self.manufacturer_string_index(), + ) + .field("product_string_index", &self.product_string_index()) + .field( + "serial_number_string_index", + &self.serial_number_string_index(), + ) + .field("num_configurations", &self.num_configurations()) + .finish() + } +} + +#[allow(unused)] pub(crate) fn validate_config_descriptor(buf: &[u8]) -> Option { if buf.len() < DESCRIPTOR_LEN_CONFIGURATION as usize { if buf.len() != 0 { @@ -549,6 +733,7 @@ impl From for Error { } /// Split a chain of concatenated configuration descriptors by `wTotalLength` +#[allow(unused)] pub(crate) fn parse_concatenated_config_descriptors(mut buf: &[u8]) -> impl Iterator { iter::from_fn(move || { let total_len = validate_config_descriptor(buf)?; @@ -659,6 +844,23 @@ fn test_malformed() { #[test] #[rustfmt::skip] fn test_linux_root_hub() { + let dev = DeviceDescriptor::new(&[ + 0x12, 0x01, 0x00, 0x02, 0x09, 0x00, 0x01, 0x40, 0x6b, + 0x1d, 0x02, 0x00, 0x10, 0x05, 0x03, 0x02, 0x01, 0x01 + ]); + assert_eq!(dev.usb_version(), 0x0200); + assert_eq!(dev.class(), 0x09); + assert_eq!(dev.subclass(), 0x00); + assert_eq!(dev.protocol(), 0x01); + assert_eq!(dev.max_packet_size_0(), 64); + assert_eq!(dev.vendor_id(), 0x1d6b); + assert_eq!(dev.product_id(), 0x0002); + assert_eq!(dev.device_version(), 0x0510); + assert_eq!(dev.manufacturer_string_index(), Some(3)); + assert_eq!(dev.product_string_index(), Some(2)); + assert_eq!(dev.serial_number_string_index(), Some(1)); + assert_eq!(dev.num_configurations(), 1); + let c = Configuration(&[ 0x09, 0x02, 0x19, 0x00, 0x01, 0x01, 0x00, 0xe0, 0x00, 0x09, 0x04, 0x00, 0x00, 0x01, 0x09, 0x00, 0x00, 0x00, diff --git a/src/device.rs b/src/device.rs index b3d6227..a54d0ec 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,14 +1,14 @@ use crate::{ descriptors::{ decode_string_descriptor, validate_string_descriptor, ActiveConfigurationError, - Configuration, InterfaceAltSetting, DESCRIPTOR_TYPE_STRING, + Configuration, DeviceDescriptor, InterfaceAltSetting, DESCRIPTOR_TYPE_STRING, }, platform, transfer::{ Control, ControlIn, ControlOut, EndpointType, Queue, RequestBuffer, TransferError, TransferFuture, }, - DeviceInfo, Error, + DeviceInfo, Error, Speed, }; use log::error; use std::{io::ErrorKind, sync::Arc, time::Duration}; @@ -94,6 +94,18 @@ impl Device { Ok(()) } + /// Get the device descriptor. + /// + /// This returns cached data and does not perform IO. + pub fn device_descriptor(&self) -> DeviceDescriptor { + self.backend.device_descriptor() + } + + /// Get device speed. + pub fn speed(&self) -> Option { + self.backend.speed() + } + /// Get information about the active configuration. /// /// This returns cached data and does not perform IO. However, it can fail if the diff --git a/src/platform/linux_usbfs/device.rs b/src/platform/linux_usbfs/device.rs index 0db566f..a078e04 100644 --- a/src/platform/linux_usbfs/device.rs +++ b/src/platform/linux_usbfs/device.rs @@ -25,7 +25,7 @@ use super::{ usbfs::{self, Urb}, SysfsPath, }; -use crate::descriptors::Configuration; +use crate::descriptors::{validate_device_descriptor, Configuration, DeviceDescriptor}; use crate::platform::linux_usbfs::events::Watch; use crate::transfer::{ControlType, Recipient}; use crate::{ @@ -33,7 +33,7 @@ use crate::{ transfer::{ notify_completion, Control, Direction, EndpointType, TransferError, TransferHandle, }, - DeviceInfo, Error, + DeviceInfo, Error, Speed, }; pub(crate) struct LinuxDevice { @@ -84,6 +84,13 @@ impl LinuxDevice { buf }; + let Some(_) = validate_device_descriptor(&descriptors) else { + return Err(Error::new( + ErrorKind::InvalidData, + "invalid device descriptor", + )); + }; + let active_config = if let Some(active_config) = active_config { active_config } else { @@ -152,6 +159,10 @@ impl LinuxDevice { } } + pub(crate) fn device_descriptor(&self) -> DeviceDescriptor { + DeviceDescriptor::new(&self.descriptors) + } + pub(crate) fn configuration_descriptors(&self) -> impl Iterator { parse_concatenated_config_descriptors(&self.descriptors[DESCRIPTOR_LEN_DEVICE as usize..]) } @@ -405,6 +416,21 @@ impl LinuxDevice { ); return Err(ErrorKind::Other.into()); } + + pub(crate) fn speed(&self) -> Option { + usbfs::get_speed(&self.fd) + .inspect_err(|e| log::error!("USBDEVFS_GET_SPEED failed: {e}")) + .ok() + .and_then(|raw_speed| match raw_speed { + 1 => Some(Speed::Low), + 2 => Some(Speed::Full), + 3 => Some(Speed::High), + // 4 is wireless USB, but we don't support it + 5 => Some(Speed::Super), + 6 => Some(Speed::SuperPlus), + _ => None, + }) + } } impl Drop for LinuxDevice { diff --git a/src/platform/linux_usbfs/usbfs.rs b/src/platform/linux_usbfs/usbfs.rs index 22725db..20485d5 100644 --- a/src/platform/linux_usbfs/usbfs.rs +++ b/src/platform/linux_usbfs/usbfs.rs @@ -286,3 +286,10 @@ pub fn clear_halt(fd: Fd, endpoint: u8) -> io::Result<()> { ioctl::ioctl(fd, ctl) } } + +pub fn get_speed(fd: Fd) -> io::Result { + unsafe { + let ctl = Transfer::, ()>::new(()); + ioctl::ioctl(fd, ctl) + } +} diff --git a/src/platform/macos_iokit/device.rs b/src/platform/macos_iokit/device.rs index 0f00297..65eb3c9 100644 --- a/src/platform/macos_iokit/device.rs +++ b/src/platform/macos_iokit/device.rs @@ -12,9 +12,10 @@ use std::{ use log::{debug, error}; use crate::{ - platform::macos_iokit::events::add_event_source, + descriptors::DeviceDescriptor, + platform::macos_iokit::{enumeration::device_descriptor_from_fields, events::add_event_source}, transfer::{Control, Direction, EndpointType, TransferError, TransferHandle}, - DeviceInfo, Error, + DeviceInfo, Error, Speed, }; use super::{ @@ -29,6 +30,8 @@ use super::{ pub(crate) struct MacDevice { _event_registration: EventRegistration, pub(super) device: IoKitDevice, + device_descriptor: DeviceDescriptor, + speed: Option, active_config: AtomicU8, is_open_exclusive: Mutex, claimed_interfaces: AtomicUsize, @@ -51,7 +54,7 @@ impl MacDevice { pub(crate) fn from_device_info(d: &DeviceInfo) -> Result, Error> { log::info!("Opening device from registry id {}", d.registry_id); let service = service_by_registry_id(d.registry_id)?; - let device = IoKitDevice::new(service)?; + let device = IoKitDevice::new(&service)?; let _event_registration = add_event_source(device.create_async_event_source()?); let opened = match unsafe { call_iokit_function!(device.raw, USBDeviceOpen()) } { @@ -64,6 +67,13 @@ impl MacDevice { } }; + let device_descriptor = device_descriptor_from_fields(&service).ok_or_else(|| { + Error::new( + ErrorKind::Other, + "could not read properties for device descriptor", + ) + })?; + let active_config = if let Some(active_config) = guess_active_config(&device) { log::debug!("Active config from single descriptor is {}", active_config); active_config @@ -76,12 +86,22 @@ impl MacDevice { Ok(Arc::new(MacDevice { _event_registration, device, + device_descriptor, + speed: d.speed, active_config: AtomicU8::new(active_config), is_open_exclusive: Mutex::new(opened), claimed_interfaces: AtomicUsize::new(0), })) } + pub(crate) fn device_descriptor(&self) -> DeviceDescriptor { + self.device_descriptor.clone() + } + + pub(crate) fn speed(&self) -> Option { + self.speed + } + pub(crate) fn active_configuration_value(&self) -> u8 { self.active_config.load(Ordering::SeqCst) } diff --git a/src/platform/macos_iokit/enumeration.rs b/src/platform/macos_iokit/enumeration.rs index 1f9f850..0db3fd7 100644 --- a/src/platform/macos_iokit/enumeration.rs +++ b/src/platform/macos_iokit/enumeration.rs @@ -15,7 +15,10 @@ use io_kit_sys::{ }; use log::debug; -use crate::{BusInfo, DeviceInfo, Error, InterfaceInfo, Speed, UsbControllerType}; +use crate::{ + descriptors::DeviceDescriptor, BusInfo, DeviceInfo, Error, InterfaceInfo, Speed, + UsbControllerType, +}; use super::iokit::{IoService, IoServiceIterator}; /// IOKit class name for PCI USB XHCI high-speed controllers (USB 3.0+) @@ -268,6 +271,25 @@ fn parse_location_id(id: u32) -> Vec { chain } +/// There is no API in iokit to get the cached device descriptor as bytes, but +/// we have all the fields to rebuild it exactly. +pub(crate) fn device_descriptor_from_fields(device: &IoService) -> Option { + Some(DeviceDescriptor::from_fields( + get_integer_property(&device, "bcdUSB")? as u16, + get_integer_property(&device, "bDeviceClass")? as u8, + get_integer_property(&device, "bDeviceSubClass")? as u8, + get_integer_property(&device, "bDeviceProtocol")? as u8, + get_integer_property(&device, "bMaxPacketSize0")? as u8, + get_integer_property(&device, "idVendor")? as u16, + get_integer_property(&device, "idProduct")? as u16, + get_integer_property(&device, "bcdDevice")? as u16, + get_integer_property(&device, "iManufacturer")? as u8, + get_integer_property(&device, "iProduct")? as u8, + get_integer_property(&device, "iSerialNumber")? as u8, + get_integer_property(&device, "bNumConfigurations")? as u8, + )) +} + #[test] fn test_parse_location_id() { assert_eq!(parse_location_id(0x01234567), vec![2, 3, 4, 5, 6, 7]); diff --git a/src/platform/macos_iokit/iokit_usb.rs b/src/platform/macos_iokit/iokit_usb.rs index 30de38f..68758dd 100644 --- a/src/platform/macos_iokit/iokit_usb.rs +++ b/src/platform/macos_iokit/iokit_usb.rs @@ -39,7 +39,7 @@ pub(crate) struct IoKitDevice { impl IoKitDevice { /// Get the raw USB device associated with the service. - pub(crate) fn new(service: IoService) -> Result { + pub(crate) fn new(service: &IoService) -> Result { unsafe { // According to the libusb maintainers, this will sometimes spuriously // return `kIOReturnNoResources` for reasons Apple won't explain, usually diff --git a/src/platform/windows_winusb/device.rs b/src/platform/windows_winusb/device.rs index 12273e4..e0a3b2f 100644 --- a/src/platform/windows_winusb/device.rs +++ b/src/platform/windows_winusb/device.rs @@ -2,7 +2,7 @@ use std::{ collections::{btree_map::Entry, BTreeMap}, ffi::c_void, io::{self, ErrorKind}, - mem::size_of_val, + mem::{size_of_val, transmute}, os::windows::{ io::{AsRawHandle, RawHandle}, prelude::OwnedHandle, @@ -23,9 +23,12 @@ use windows_sys::Win32::{ }; use crate::{ - descriptors::{validate_config_descriptor, DESCRIPTOR_TYPE_CONFIGURATION}, + descriptors::{ + validate_config_descriptor, DeviceDescriptor, DESCRIPTOR_LEN_DEVICE, + DESCRIPTOR_TYPE_CONFIGURATION, + }, transfer::{Control, Direction, EndpointType, Recipient, TransferError, TransferHandle}, - DeviceInfo, Error, + DeviceInfo, Error, Speed, }; use super::{ @@ -38,8 +41,10 @@ use super::{ }; pub(crate) struct WindowsDevice { + device_descriptor: DeviceDescriptor, config_descriptors: Vec>, active_config: u8, + speed: Option, devinst: DevInst, handles: Mutex>, } @@ -54,8 +59,15 @@ impl WindowsDevice { // instead. let hub_port = HubPort::by_child_devinst(d.devinst)?; let connection_info = hub_port.get_info()?; - let num_configurations = connection_info.device_desc.bNumConfigurations; + // Safety: Windows API struct is repr(C), packed, and we're assuming Windows is little-endian + let device_descriptor = unsafe { + DeviceDescriptor::new(&transmute::<_, [u8; DESCRIPTOR_LEN_DEVICE as usize]>( + connection_info.device_desc, + )) + }; + + let num_configurations = connection_info.device_desc.bNumConfigurations; let config_descriptors = (0..num_configurations) .flat_map(|i| { let res = hub_port.get_descriptor(DESCRIPTOR_TYPE_CONFIGURATION, i, 0); @@ -70,13 +82,23 @@ impl WindowsDevice { .collect(); Ok(Arc::new(WindowsDevice { + device_descriptor, config_descriptors, + speed: connection_info.speed, active_config: connection_info.active_config, devinst: d.devinst, handles: Mutex::new(BTreeMap::new()), })) } + pub(crate) fn device_descriptor(&self) -> DeviceDescriptor { + self.device_descriptor.clone() + } + + pub(crate) fn speed(&self) -> Option { + self.speed + } + pub(crate) fn active_configuration_value(&self) -> u8 { self.active_config }