Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bus_id and port_chain field to UsbPortInfo (all platforms) #227

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,10 @@ pub struct UsbPortInfo {
pub manufacturer: Option<String>,
/// Product name (arbitrary string)
pub product: Option<String>,
/// Device's bus id
pub bus_id: String,
/// Physycal port hierarchy
pub port_chain: Vec<u8>,
/// 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.
Expand Down
65 changes: 65 additions & 0 deletions src/posix/enumerate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SerialPortType> {
match d.property_value("ID_BUS").and_then(OsStr::to_str) {
Expand All @@ -123,12 +138,26 @@ fn port_type(d: &libudev::Device) -> Result<SerialPortType> {
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::<u8>().ok())
.collect::<Option<Vec<u8>>>()
})
.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(),
Expand All @@ -154,12 +183,26 @@ fn port_type(d: &libudev::Device) -> Result<SerialPortType> {
"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::<u8>().ok())
.collect::<Option<Vec<u8>>>()
})
.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,
Expand Down Expand Up @@ -252,6 +295,8 @@ fn parse_modalias(moda: &str) -> Option<UsbPortInfo> {
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| {
Expand Down Expand Up @@ -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<u8> {
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.
Expand All @@ -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:
Expand Down
39 changes: 34 additions & 5 deletions src/windows/enumerate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,23 @@ impl<'hwid> HwidMatches<'hwid> {
}
}

fn parse_location_path(s: &str) -> Option<(String, Vec<u8>)> {
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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
}

Expand Down
Loading