Skip to content

Commit

Permalink
Merge pull request #29 from kevinmehall/interfaces
Browse files Browse the repository at this point in the history
Add DeviceInfo::interfaces for OS cached interface information
  • Loading branch information
kevinmehall authored Jan 15, 2024
2 parents ffdc32f + d7fdda3 commit c1232a6
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 26 deletions.
86 changes: 79 additions & 7 deletions src/enumeration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ pub struct DeviceInfo {
pub(crate) manufacturer_string: Option<String>,
pub(crate) product_string: Option<String>,
pub(crate) serial_number: Option<String>,

pub(crate) interfaces: Vec<InterfaceInfo>,
}

impl DeviceInfo {
Expand Down Expand Up @@ -176,10 +178,6 @@ impl DeviceInfo {
}

/// Product string, if available without device IO
///
/// ### Platform-specific notes
/// * Windows: this comes from the driver, and for some drivers
/// does not match the string returned from the device.
#[doc(alias = "iProduct")]
pub fn product_string(&self) -> Option<&str> {
self.product_string.as_deref()
Expand All @@ -191,6 +189,28 @@ impl DeviceInfo {
self.serial_number.as_deref()
}

/// Iterator over the device's interfaces.
///
/// This returns summary information about the interfaces in the device's
/// active configuration for the purposes of matching devices prior to
/// opening them.
///
/// Additional information about interfaces can be found in the
/// configuration descriptor after opening the device by calling
/// [`Device::active_configuration`].
///
/// ### Platform-specific notes:
/// * Windows: this is only available for composite devices bound to the
/// `usbccgp` driver, and will be empty if the entire device is bound to
/// a specific driver.
/// * Windows: When interfaces are grouped by an interface
/// association descriptor, this returns details from the interface
/// association descriptor and does not include each of the associated
/// interfaces.
pub fn interfaces(&self) -> impl Iterator<Item = &InterfaceInfo> {
self.interfaces.iter()
}

/// Open the device
pub fn open(&self) -> Result<Device, Error> {
Device::open(self)
Expand All @@ -210,9 +230,9 @@ impl std::fmt::Debug for DeviceInfo {
"device_version",
&format_args!("0x{:04X}", self.device_version),
)
.field("class", &self.class)
.field("subclass", &self.subclass)
.field("protocol", &self.protocol)
.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("speed", &self.speed)
.field("manufacturer_string", &self.manufacturer_string)
.field("product_string", &self.product_string)
Expand All @@ -237,6 +257,8 @@ impl std::fmt::Debug for DeviceInfo {
s.field("registry_entry_id", &self.registry_id);
}

s.field("interfaces", &self.interfaces);

s.finish()
}
}
Expand Down Expand Up @@ -274,3 +296,53 @@ impl Speed {
}
}
}

/// Summary information about a device's interface, available before opening a device.
#[derive(Clone)]
pub struct InterfaceInfo {
pub(crate) interface_number: u8,
pub(crate) class: u8,
pub(crate) subclass: u8,
pub(crate) protocol: u8,
pub(crate) interface_string: Option<String>,
}

impl InterfaceInfo {
/// Identifier for the interface from the `bInterfaceNumber` descriptor field.
pub fn interface_number(&self) -> u8 {
self.interface_number
}

/// Code identifying the standard interface class, from the `bInterfaceClass` interface descriptor field.
pub fn class(&self) -> u8 {
self.class
}

/// Standard subclass, from the `bInterfaceSubClass` interface descriptor field.
pub fn subclass(&self) -> u8 {
self.subclass
}

/// Standard protocol, from the `bInterfaceProtocol` interface descriptor field.
pub fn protocol(&self) -> u8 {
self.protocol
}

/// Interface string descriptor value as cached by the OS.
pub fn interface_string(&self) -> Option<&str> {
self.interface_string.as_deref()
}
}

// Not derived so that we can format some fields in hex
impl std::fmt::Debug for InterfaceInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InterfaceInfo")
.field("interface_number", &self.interface_number)
.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("interface_string", &self.interface_string)
.finish()
}
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ mod platform;

pub mod descriptors;
mod enumeration;
pub use enumeration::{DeviceInfo, Speed};
pub use enumeration::{DeviceInfo, InterfaceInfo, Speed};

mod device;
pub use device::{Device, Interface};
Expand Down
36 changes: 36 additions & 0 deletions src/platform/linux_usbfs/enumeration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::str::FromStr;

use log::debug;

use crate::enumeration::InterfaceInfo;
use crate::DeviceInfo;
use crate::Error;
use crate::Speed;
Expand Down Expand Up @@ -34,6 +35,16 @@ impl SysfsPath {
T::from_hex_str(&s)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid hex str"))
}

fn children(&self) -> impl Iterator<Item = SysfsPath> {
fs::read_dir(&self.0)
.ok()
.into_iter()
.flat_map(|x| x)
.filter_map(|f| f.ok())
.filter(|f| f.file_type().ok().is_some_and(|t| t.is_dir()))
.map(|f| SysfsPath(f.path()))
}
}

trait FromHexStr: Sized {
Expand Down Expand Up @@ -83,6 +94,31 @@ pub fn probe_device(path: SysfsPath) -> Result<DeviceInfo, Error> {
manufacturer_string: path.read_attr("manufacturer").ok(),
product_string: path.read_attr("product").ok(),
serial_number: path.read_attr("serial").ok(),
interfaces: {
let mut interfaces: Vec<_> = path
.children()
.filter(|i| {
// Skip subdirectories like `power` that aren't interfaces
// (they would be skipped when missing required properties,
// but might as well not open them)
i.0.file_name()
.unwrap_or_default()
.as_encoded_bytes()
.contains(&b':')
})
.flat_map(|i| {
Some(InterfaceInfo {
interface_number: i.read_attr_hex("bInterfaceNumber").ok()?,
class: i.read_attr_hex("bInterfaceClass").ok()?,
subclass: i.read_attr_hex("bInterfaceSubClass").ok()?,
protocol: i.read_attr_hex("bInterfaceProtocol").ok()?,
interface_string: i.read_attr("interface").ok(),
})
})
.collect();
interfaces.sort_unstable_by_key(|i| i.interface_number);
interfaces
},
path,
})
}
34 changes: 30 additions & 4 deletions src/platform/macos_iokit/enumeration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ use core_foundation::{
use io_kit_sys::{
kIOMasterPortDefault, kIORegistryIterateParents, kIORegistryIterateRecursively,
keys::kIOServicePlane, ret::kIOReturnSuccess, usb::lib::kIOUSBDeviceClassName,
IORegistryEntryGetRegistryEntryID, IORegistryEntrySearchCFProperty,
IOServiceGetMatchingServices, IOServiceMatching,
IORegistryEntryGetChildIterator, IORegistryEntryGetRegistryEntryID,
IORegistryEntrySearchCFProperty, IOServiceGetMatchingServices, IOServiceMatching,
};
use log::{error, info};
use log::{error, info, warn};

use crate::{DeviceInfo, Error, Speed};
use crate::{DeviceInfo, Error, InterfaceInfo, Speed};

use super::iokit::{IoService, IoServiceIterator};

Expand Down Expand Up @@ -65,6 +65,18 @@ fn probe_device(device: IoService) -> Option<DeviceInfo> {
manufacturer_string: get_string_property(&device, "USB Vendor Name"),
product_string: get_string_property(&device, "USB Product Name"),
serial_number: get_string_property(&device, "USB Serial Number"),
interfaces: get_children(&device).map_or(Vec::new(), |iter| {
iter.flat_map(|child| {
Some(InterfaceInfo {
interface_number: get_integer_property(&child, "bInterfaceNumber")?,
class: get_integer_property(&child, "bInterfaceClass")?,
subclass: get_integer_property(&child, "bInterfaceSubClass")?,
protocol: get_integer_property(&child, "bInterfaceProtocol")?,
interface_string: get_string_property(&child, "USB Interface Name"),
})
})
.collect()
}),
})
}

Expand Down Expand Up @@ -120,6 +132,20 @@ fn get_integer_property<T: TryFrom<i64>>(device: &IoService, property: &'static
.and_then(|n| n.try_into().ok())
}

fn get_children(device: &IoService) -> Result<IoServiceIterator, Error> {
unsafe {
let mut iterator = 0;
let r =
IORegistryEntryGetChildIterator(device.get(), kIOServicePlane as *mut _, &mut iterator);
if r != kIOReturnSuccess {
warn!("IORegistryEntryGetChildIterator failed: {r}");
return Err(Error::from_raw_os_error(r));
}

Ok(IoServiceIterator::new(iterator))
}
}

fn map_speed(speed: u32) -> Option<Speed> {
// https://developer.apple.com/documentation/iokit/1425357-usbdevicespeed
match speed {
Expand Down
106 changes: 92 additions & 14 deletions src/platform/windows_winusb/enumeration.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
use std::{collections::HashMap, ffi::OsString};
use std::{
collections::HashMap,
ffi::{OsStr, OsString},
};

use log::{debug, error};
use windows_sys::Win32::Devices::{
Properties::{
DEVPKEY_Device_Address, DEVPKEY_Device_BusNumber, DEVPKEY_Device_BusReportedDeviceDesc,
DEVPKEY_Device_HardwareIds, DEVPKEY_Device_InstanceId, DEVPKEY_Device_Manufacturer,
DEVPKEY_Device_Parent, DEVPKEY_Device_Service,
DEVPKEY_Device_CompatibleIds, DEVPKEY_Device_HardwareIds, DEVPKEY_Device_InstanceId,
DEVPKEY_Device_Manufacturer, DEVPKEY_Device_Parent, DEVPKEY_Device_Service,
},
Usb::{GUID_DEVINTERFACE_USB_DEVICE, USB_DEVICE_SPEED},
};

use crate::{DeviceInfo, Error, Speed};
use crate::{DeviceInfo, Error, InterfaceInfo, Speed};

use super::{
cfgmgr32::{self, get_device_interface_property, DevInst},
Expand Down Expand Up @@ -70,6 +73,34 @@ pub fn probe_device(devinst: DevInst) -> Option<DeviceInfo> {
.and_then(|s| s.into_string().ok())
.unwrap_or_default();

let mut interfaces = if driver.eq_ignore_ascii_case("usbccgp") {
devinst
.children()
.flat_map(|intf| {
let interface_number = get_interface_number(intf)?;
let (class, subclass, protocol) = intf
.get_property::<Vec<OsString>>(DEVPKEY_Device_CompatibleIds)?
.iter()
.find_map(|s| parse_compatible_id(s))?;
let interface_string = intf
.get_property::<OsString>(DEVPKEY_Device_BusReportedDeviceDesc)
.and_then(|s| s.into_string().ok());

Some(InterfaceInfo {
interface_number,
class,
subclass,
protocol,
interface_string,
})
})
.collect()
} else {
Vec::new()
};

interfaces.sort_unstable_by_key(|i| i.interface_number);

Some(DeviceInfo {
instance_id,
parent_instance_id,
Expand All @@ -88,6 +119,7 @@ pub fn probe_device(devinst: DevInst) -> Option<DeviceInfo> {
manufacturer_string,
product_string,
serial_number,
interfaces,
})
}

Expand Down Expand Up @@ -119,6 +151,61 @@ pub(crate) fn find_device_interfaces(dev: DevInst) -> HashMap<u8, WCString> {
interfaces
}

fn get_interface_number(intf_dev: DevInst) -> Option<u8> {
let hw_ids = intf_dev.get_property::<Vec<OsString>>(DEVPKEY_Device_HardwareIds);
hw_ids
.as_deref()
.unwrap_or_default()
.iter()
.find_map(|id| parse_hardware_id(id))
.or_else(|| {
error!("Failed to parse interface number in hardware IDs: {hw_ids:?}");
None
})
}

/// Parse interface number from a Hardware ID value
fn parse_hardware_id(s: &OsStr) -> Option<u8> {
let s = s.to_str()?;
let s = s.rsplit_once("&MI_")?.1;
u8::from_str_radix(s.get(0..2)?, 16).ok()
}

#[test]
fn test_parse_hardware_id() {
assert_eq!(parse_hardware_id(OsStr::new("")), None);
assert_eq!(
parse_hardware_id(OsStr::new("USB\\VID_1234&PID_5678&MI_0A")),
Some(10)
);
assert_eq!(
parse_hardware_id(OsStr::new("USB\\VID_9999&PID_AAAA&REV_0101&MI_01")),
Some(1)
);
}

/// Parse class, subclass, protocol from a Compatible ID value
fn parse_compatible_id(s: &OsStr) -> Option<(u8, u8, u8)> {
let s = s.to_str()?;
let s = s.strip_prefix("USB\\Class_")?;
let class = u8::from_str_radix(s.get(0..2)?, 16).ok()?;
let s = s.get(2..)?.strip_prefix("&SubClass_")?;
let subclass = u8::from_str_radix(s.get(0..2)?, 16).ok()?;
let s = s.get(2..)?.strip_prefix("&Prot_")?;
let protocol = u8::from_str_radix(s.get(0..2)?, 16).ok()?;
Some((class, subclass, protocol))
}

#[test]
fn test_parse_compatible_id() {
assert_eq!(parse_compatible_id(OsStr::new("")), None);
assert_eq!(parse_compatible_id(OsStr::new("USB\\Class_03")), None);
assert_eq!(
parse_compatible_id(OsStr::new("USB\\Class_03&SubClass_11&Prot_22")),
Some((3, 17, 34))
);
}

/// Probe a device node for a child device (USB interface) of a composite device
/// to see if it is usable with WinUSB and find the Windows interface used to
/// open it.
Expand All @@ -135,16 +222,7 @@ fn probe_interface(cdev: DevInst) -> Option<(u8, WCString)> {
return None;
}

let hw_ids = cdev.get_property::<Vec<OsString>>(DEVPKEY_Device_HardwareIds);
let Some(intf_num) = hw_ids
.as_deref()
.unwrap_or_default()
.iter()
.find_map(|id| id.to_str()?.rsplit_once("&MI_")?.1.parse::<u8>().ok())
else {
error!("Failed to parse interface number in hardware IDs: {hw_ids:?}");
return None;
};
let intf_num = get_interface_number(cdev)?;

let reg_key = cdev.registry_key().unwrap();
let guid = match reg_key.query_value_guid("DeviceInterfaceGUIDs") {
Expand Down

0 comments on commit c1232a6

Please sign in to comment.