diff --git a/src/lib.rs b/src/lib.rs index 79d0f377..6b60b86a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -787,6 +787,10 @@ pub struct UsbPortInfo { pub manufacturer: Option, /// Product name (arbitrary string) pub product: Option, + /// Device's bus id + pub bus_id: String, + /// Physycal port hierarchy + pub port_chain: Vec, /// The interface index of the USB serial port. This can be either the interface number of /// the communication interface (as is the case on Windows and Linux) or the data /// interface (as is the case on macOS), so you should recognize both interface numbers. diff --git a/src/posix/enumerate.rs b/src/posix/enumerate.rs index 87dbdfe6..fc6fdd05 100644 --- a/src/posix/enumerate.rs +++ b/src/posix/enumerate.rs @@ -109,6 +109,21 @@ fn udev_restore_spaces(source: String) -> String { source.replace('_', " ") } +#[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))] +fn device_location(d: &libudev::Device) -> String { + match d.devpath() { + Some(path) => path + .to_str() + .unwrap_or_default() + .split("/") + .take(8) + .last() + .unwrap_or_default() + .to_string(), + None => "".to_string(), + } +} + #[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))] fn port_type(d: &libudev::Device) -> Result { match d.property_value("ID_BUS").and_then(OsStr::to_str) { @@ -123,12 +138,26 @@ fn port_type(d: &libudev::Device) -> Result { let product = udev_property_encoded_or_replaced_as_string(d, "ID_MODEL_ENC", "ID_MODEL") .or_else(|| udev_property_as_string(d, "ID_MODEL_FROM_DATABASE")); + + let location = device_location(d); + let [busnum, path] = location.split("-"); + let port_chain = path + .filter(|p| p != "0") // root hub should be empty but devpath is 0 + .and_then(|p| { + p.split('.') + .map(|v| v.parse::().ok()) + .collect::>>() + }) + .unwrap_or_default(); + Ok(SerialPortType::UsbPort(UsbPortInfo { vid: udev_hex_property_as_int(d, "ID_VENDOR_ID", &u16::from_str_radix)?, pid: udev_hex_property_as_int(d, "ID_MODEL_ID", &u16::from_str_radix)?, serial_number, manufacturer, product, + bus_id: format!("{busnum:03}"), + port_chain, #[cfg(feature = "usbportinfo-interface")] interface: udev_hex_property_as_int(d, "ID_USB_INTERFACE_NUM", &u8::from_str_radix) .ok(), @@ -154,12 +183,26 @@ fn port_type(d: &libudev::Device) -> Result { "ID_USB_MODEL_ENC", "ID_USB_MODEL", ); + + let location = device_location(d); + let [busnum, path] = location.split("-"); + let port_chain = path + .filter(|p| p != "0") // root hub should be empty but devpath is 0 + .and_then(|p| { + p.split('.') + .map(|v| v.parse::().ok()) + .collect::>>() + }) + .unwrap_or_default(); + Ok(SerialPortType::UsbPort(UsbPortInfo { vid: udev_hex_property_as_int(d, "ID_USB_VENDOR_ID", &u16::from_str_radix)?, pid: udev_hex_property_as_int(d, "ID_USB_MODEL_ID", &u16::from_str_radix)?, serial_number: udev_property_as_string(d, "ID_USB_SERIAL_SHORT"), manufacturer, product, + bus_id: format!("{busnum:03}"), + port_chain, #[cfg(feature = "usbportinfo-interface")] interface: udev_hex_property_as_int( d, @@ -252,6 +295,8 @@ fn parse_modalias(moda: &str) -> Option { serial_number: None, manufacturer: None, product: None, + bus_id: "".to_string(), + port_chain: vec![], // Only attempt to find the interface if the feature is enabled. #[cfg(feature = "usbportinfo-interface")] interface: mod_tail.get(pid_start + 4..).and_then(|mod_tail| { @@ -344,6 +389,23 @@ fn get_string_property(device_type: io_registry_entry_t, property: &str) -> Resu .ok_or(Error::new(ErrorKind::Unknown, "Failed to get string value")) } +/// Parse location_id by extracting bits that represent specific parts of +/// the USB device’s location within the USB topology, such as the bus and +/// port numbers, +/// Returns port chain as a vector of u8 bytes. +fn parse_location_id(id: u32) -> Vec { + let mut chain = vec![]; + let mut shift = id << 8; + + while shift != 0 { + let port = shift >> 28; + chain.push(port as u8); + shift = shift << 4; + } + + chain +} + #[cfg(any(target_os = "ios", target_os = "macos"))] /// Determine the serial port type based on the service object (like that returned by /// `IOIteratorNext`). Specific properties are extracted for USB devices. @@ -355,12 +417,15 @@ fn port_type(service: io_object_t) -> SerialPortType { let maybe_usb_device = get_parent_device_by_type(service, usb_device_class_name) .or_else(|| get_parent_device_by_type(service, legacy_usb_device_class_name)); if let Some(usb_device) = maybe_usb_device { + let location_id = get_int_property(usb_device, "locationID").unwrap_or_default() as u32; SerialPortType::UsbPort(UsbPortInfo { vid: get_int_property(usb_device, "idVendor").unwrap_or_default() as u16, pid: get_int_property(usb_device, "idProduct").unwrap_or_default() as u16, serial_number: get_string_property(usb_device, "USB Serial Number").ok(), manufacturer: get_string_property(usb_device, "USB Vendor Name").ok(), product: get_string_property(usb_device, "USB Product Name").ok(), + bus_id: format!("{:02x}", (location_id >> 24) as u8), + port_chain: parse_location_id(location_id), // Apple developer documentation indicates `bInterfaceNumber` is the supported key for // looking up the composite usb interface id. `idVendor` and `idProduct` are included in the same tables, so // we will lookup the interface number using the same method. See: diff --git a/src/windows/enumerate.rs b/src/windows/enumerate.rs index c303e246..13516e41 100644 --- a/src/windows/enumerate.rs +++ b/src/windows/enumerate.rs @@ -144,6 +144,23 @@ impl<'hwid> HwidMatches<'hwid> { } } +fn parse_location_path(s: &str) -> Option<(String, Vec)> { + let usbroot = "#USBROOT("; + let start_i = s.find(usbroot)?; + let close_i = s[start_i + usbroot.len()..].find(')')?; + let (bus, mut s) = s.split_at(start_i + usbroot.len() + close_i + 1); + + let mut path = vec![]; + + while let Some((_, next)) = s.split_once("#USB(") { + let (port_num, next) = next.split_once(")")?; + path.push(port_num.parse().ok()?); + s = next; + } + + Some((bus.to_owned(), path)) +} + /// Windows usb port information can be determined by the port's HWID string. /// /// This function parses the HWID string using regex, and returns the USB port @@ -386,6 +403,16 @@ impl PortDevice { .map(|mut info: UsbPortInfo| { info.manufacturer = self.property(SPDRP_MFG); info.product = self.property(SPDRP_FRIENDLYNAME); + + let location_paths = self.property(SPDRP_LOCATION_PATHS); + + let (bus_id, port_chain) = location_paths + .iter() + .find_map(|p| parse_location_path(p)) + .unwrap_or_default(); + + info.bus_id = bus_id; + info.port_chain = port_chain; SerialPortType::UsbPort(info) }) .unwrap_or(SerialPortType::Unknown) @@ -409,17 +436,19 @@ impl PortDevice { ) }; - if res == FALSE || value_type != REG_SZ { + if res == FALSE || (value_type != REG_SZ && value_type != REG_MULTI_SZ) { return None; } + let mut property_val = from_utf16_lossy_trimmed(&property_buf); + if value_type == REG_MULTI_SZ { + property_val = property_val.split('\0').next().unwrap_or("").to_string(); + } + // Using the unicode version of 'SetupDiGetDeviceRegistryProperty' seems to report the // entire mfg registry string. This typically includes some driver information that we should discard. // Example string: 'FTDI5.inf,%ftdi%;FTDI' - from_utf16_lossy_trimmed(&property_buf) - .split(';') - .last() - .map(str::to_string) + property_val.split(';').last().map(str::to_string) } }