use std::collections::HashSet;
use std::ptr;
use windows_sys::core::GUID;
use windows_sys::Win32::Devices::DeviceAndDriverInstallation::{
CM_Get_DevNode_Status, CM_Get_Device_IDW, CM_Get_Parent, SetupDiClassGuidsFromNameW,
SetupDiDestroyDeviceInfoList, SetupDiEnumDeviceInfo, SetupDiGetClassDevsW,
SetupDiGetDeviceInstanceIdW, SetupDiGetDeviceRegistryPropertyW, SetupDiOpenDevRegKey,
CR_SUCCESS, DICS_FLAG_GLOBAL, DIGCF_PRESENT, DIREG_DEV, HDEVINFO, MAX_DEVICE_ID_LEN,
SPDRP_FRIENDLYNAME, SPDRP_HARDWAREID, SPDRP_MFG, SP_DEVINFO_DATA,
};
use windows_sys::Win32::Foundation::{FALSE, FILETIME, INVALID_HANDLE_VALUE, MAX_PATH};
use windows_sys::Win32::System::Registry::{
RegCloseKey, RegEnumValueW, RegOpenKeyExW, RegQueryInfoKeyW, RegQueryValueExW, HKEY,
HKEY_LOCAL_MACHINE, KEY_READ, REG_SZ,
};
use crate::{Error, ErrorKind, Result, SerialPortInfo, SerialPortType, UsbPortInfo};
const CONNECTOR_PUNCTUATION_SELECTION: &[char] = &[':', '_', '\u{ff3f}'];
fn as_utf16(utf8: &str) -> Vec<u16> {
utf8.encode_utf16().chain(Some(0)).collect()
}
fn from_utf16_lossy_trimmed(utf16: &[u16]) -> String {
String::from_utf16_lossy(utf16)
.trim_end_matches(0 as char)
.to_string()
}
fn get_ports_guids() -> Result<Vec<GUID>> {
let class_names = ["Ports", "Modem"];
let mut guids: Vec<GUID> = Vec::new();
for class_name in class_names {
let class_name_w = as_utf16(class_name);
let mut num_guids: u32 = 1; let class_start_idx = guids.len();
for _ in 0..2 {
guids.resize(class_start_idx + num_guids as usize, GUID::from_u128(0));
let guid_buffer = &mut guids[class_start_idx..];
let res = unsafe {
SetupDiClassGuidsFromNameW(
class_name_w.as_ptr(),
guid_buffer.as_mut_ptr(),
guid_buffer.len() as u32,
&mut num_guids,
)
};
if res == FALSE {
return Err(Error::new(
ErrorKind::Unknown,
"Unable to determine number of Ports GUIDs",
));
}
let len_cmp = guid_buffer.len().cmp(&(num_guids as usize));
if len_cmp == std::cmp::Ordering::Less {
continue; }
else if len_cmp == std::cmp::Ordering::Greater {
guids.truncate(class_start_idx + num_guids as usize);
}
break; }
}
Ok(guids)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct HwidMatches<'hwid> {
vid: &'hwid str,
pid: &'hwid str,
serial: Option<&'hwid str>,
interface: Option<&'hwid str>,
}
impl<'hwid> HwidMatches<'hwid> {
fn new(hwid: &'hwid str) -> Option<Self> {
let mut hwid_tail = hwid;
let vid_start = hwid.find("VID_")?;
let vid = hwid_tail.get(vid_start + 4..vid_start + 8)?;
hwid_tail = hwid_tail.get(vid_start + 8..)?;
let pid = if hwid_tail.starts_with("&PID_") || hwid_tail.starts_with("+PID_") {
hwid_tail.get(5..9)?
} else {
return None;
};
hwid_tail = hwid_tail.get(9..)?;
let iid = if hwid_tail.starts_with("&MI_") || hwid_tail.starts_with("+MI_") {
let iid = hwid_tail.get(4..6);
hwid_tail = hwid_tail.get(6..).unwrap_or(hwid_tail);
iid
} else {
None
};
let serial = if hwid_tail.starts_with('\\') || hwid_tail.starts_with('+') {
hwid_tail.get(1..).and_then(|tail| {
let index = tail
.char_indices()
.find(|&(_, char)| {
!(char.is_alphanumeric() || CONNECTOR_PUNCTUATION_SELECTION.contains(&char))
})
.map(|(index, _)| index)
.unwrap_or(tail.len());
tail.get(..index)
})
} else {
None
};
Some(Self {
vid,
pid,
serial,
interface: iid,
})
}
}
fn parse_usb_port_info(hardware_id: &str, parent_hardware_id: Option<&str>) -> Option<UsbPortInfo> {
let mut caps = HwidMatches::new(hardware_id)?;
let interface = caps.interface.and_then(|m| u8::from_str_radix(m, 16).ok());
if interface.is_some() {
caps = HwidMatches::new(parent_hardware_id?)?;
}
Some(UsbPortInfo {
vid: u16::from_str_radix(caps.vid, 16).ok()?,
pid: u16::from_str_radix(caps.pid, 16).ok()?,
serial_number: caps.serial.map(str::to_string),
manufacturer: None,
product: None,
#[cfg(feature = "usbportinfo-interface")]
interface,
})
}
struct PortDevices {
hdi: HDEVINFO,
dev_idx: u32,
}
impl PortDevices {
pub fn new(guid: &GUID) -> Self {
PortDevices {
hdi: unsafe { SetupDiGetClassDevsW(guid, ptr::null(), 0, DIGCF_PRESENT) },
dev_idx: 0,
}
}
}
impl Iterator for PortDevices {
type Item = PortDevice;
fn next(&mut self) -> Option<PortDevice> {
let mut port_dev = PortDevice {
hdi: self.hdi,
devinfo_data: SP_DEVINFO_DATA {
cbSize: std::mem::size_of::<SP_DEVINFO_DATA>() as u32,
ClassGuid: GUID::from_u128(0),
DevInst: 0,
Reserved: 0,
},
};
let res =
unsafe { SetupDiEnumDeviceInfo(self.hdi, self.dev_idx, &mut port_dev.devinfo_data) };
if res == FALSE {
None
} else {
self.dev_idx += 1;
Some(port_dev)
}
}
}
impl Drop for PortDevices {
fn drop(&mut self) {
unsafe {
SetupDiDestroyDeviceInfoList(self.hdi);
}
}
}
struct PortDevice {
hdi: HDEVINFO,
pub devinfo_data: SP_DEVINFO_DATA,
}
impl PortDevice {
fn parent_instance_id(&mut self) -> Option<String> {
let mut result_buf = [0u16; MAX_PATH as usize];
let mut parent_device_instance_id = 0;
let res =
unsafe { CM_Get_Parent(&mut parent_device_instance_id, self.devinfo_data.DevInst, 0) };
if res == CR_SUCCESS {
let buffer_len = result_buf.len() - 1;
let res = unsafe {
CM_Get_Device_IDW(
parent_device_instance_id,
result_buf.as_mut_ptr(),
buffer_len as u32,
0,
)
};
if res == CR_SUCCESS {
Some(from_utf16_lossy_trimmed(&result_buf))
} else {
None
}
} else {
None
}
}
fn instance_id(&mut self) -> Option<String> {
let mut result_buf = [0u16; MAX_DEVICE_ID_LEN as usize];
let working_buffer_len = result_buf.len() - 1; let mut desired_result_len = 0; let res = unsafe {
SetupDiGetDeviceInstanceIdW(
self.hdi,
&self.devinfo_data,
result_buf.as_mut_ptr(),
working_buffer_len as u32,
&mut desired_result_len,
)
};
if res == FALSE {
self.property(SPDRP_HARDWAREID)
} else {
let actual_result_len = working_buffer_len.min(desired_result_len as usize);
Some(from_utf16_lossy_trimmed(&result_buf[..actual_result_len]))
}
}
fn problem(&mut self) -> Option<u32> {
let mut status = 0;
let mut problem_number = 0;
let res = unsafe {
CM_Get_DevNode_Status(
&mut status,
&mut problem_number,
self.devinfo_data.DevInst,
0,
)
};
if res == CR_SUCCESS {
Some(problem_number)
} else {
None
}
}
pub fn name(&mut self) -> String {
let hkey = unsafe {
SetupDiOpenDevRegKey(
self.hdi,
&self.devinfo_data,
DICS_FLAG_GLOBAL,
0,
DIREG_DEV,
KEY_READ,
)
};
if hkey == INVALID_HANDLE_VALUE {
return String::new();
}
let mut port_name_buffer = [0u16; MAX_PATH as usize];
let buffer_byte_len = 2 * port_name_buffer.len() as u32;
let mut byte_len = buffer_byte_len;
let mut value_type = 0;
let value_name = as_utf16("PortName");
let err = unsafe {
RegQueryValueExW(
hkey,
value_name.as_ptr(),
ptr::null_mut(),
&mut value_type,
port_name_buffer.as_mut_ptr() as *mut u8,
&mut byte_len,
)
};
unsafe { RegCloseKey(hkey) };
if err != 0 {
return String::new();
}
if value_type != REG_SZ || byte_len % 2 != 0 || byte_len > buffer_byte_len {
return String::new();
}
let len = buffer_byte_len as usize / 2;
let port_name = &port_name_buffer[0..len];
from_utf16_lossy_trimmed(port_name)
}
pub fn port_type(&mut self) -> SerialPortType {
self.instance_id()
.map(|s| (s, self.parent_instance_id())) .and_then(|(d, p)| parse_usb_port_info(&d, p.as_deref()))
.map(|mut info: UsbPortInfo| {
info.manufacturer = self.property(SPDRP_MFG);
info.product = self.property(SPDRP_FRIENDLYNAME);
SerialPortType::UsbPort(info)
})
.unwrap_or(SerialPortType::Unknown)
}
fn property(&mut self, property_id: u32) -> Option<String> {
let mut value_type = 0;
let mut property_buf = [0u16; MAX_PATH as usize];
let res = unsafe {
SetupDiGetDeviceRegistryPropertyW(
self.hdi,
&self.devinfo_data,
property_id,
&mut value_type,
property_buf.as_mut_ptr() as *mut u8,
property_buf.len() as u32,
ptr::null_mut(),
)
};
if res == FALSE || value_type != REG_SZ {
return None;
}
from_utf16_lossy_trimmed(&property_buf)
.split(';')
.next_back()
.map(str::to_string)
}
}
fn get_registry_com_ports() -> HashSet<String> {
let mut ports_list = HashSet::new();
let reg_key = as_utf16("HARDWARE\\DEVICEMAP\\SERIALCOMM");
let key_ptr = reg_key.as_ptr();
let mut ports_key: HKEY = 0;
let open_res =
unsafe { RegOpenKeyExW(HKEY_LOCAL_MACHINE, key_ptr, 0, KEY_READ, &mut ports_key) };
if open_res == 0 {
let mut class_name_buff = [0u16; MAX_PATH as usize];
let mut class_name_size = MAX_PATH;
let mut sub_key_count = 0;
let mut largest_sub_key = 0;
let mut largest_class_string = 0;
let mut num_key_values = 0;
let mut longest_value_name = 0;
let mut longest_value_data = 0;
let mut size_security_desc = 0;
let mut last_write_time = FILETIME {
dwLowDateTime: 0,
dwHighDateTime: 0,
};
let query_res = unsafe {
RegQueryInfoKeyW(
ports_key,
class_name_buff.as_mut_ptr(),
&mut class_name_size,
ptr::null(),
&mut sub_key_count,
&mut largest_sub_key,
&mut largest_class_string,
&mut num_key_values,
&mut longest_value_name,
&mut longest_value_data,
&mut size_security_desc,
&mut last_write_time,
)
};
if query_res == 0 {
for idx in 0..num_key_values {
let mut val_name_buff = [0u16; MAX_PATH as usize];
let mut val_name_size = MAX_PATH;
let mut value_type = 0;
let mut val_data = [0u16; MAX_PATH as usize];
let buffer_byte_len = 2 * val_data.len() as u32; let mut byte_len = buffer_byte_len;
let res = unsafe {
RegEnumValueW(
ports_key,
idx,
val_name_buff.as_mut_ptr(),
&mut val_name_size,
ptr::null(),
&mut value_type,
val_data.as_mut_ptr() as *mut u8,
&mut byte_len,
)
};
if res != 0
|| value_type != REG_SZ || byte_len % 2 != 0 || byte_len > buffer_byte_len
{
break;
}
let val_data = from_utf16_lossy_trimmed(unsafe {
let utf16_len = byte_len / 2; std::slice::from_raw_parts(val_data.as_ptr(), utf16_len as usize)
});
ports_list.insert(val_data);
}
}
unsafe { RegCloseKey(ports_key) };
}
ports_list
}
pub fn available_ports() -> Result<Vec<SerialPortInfo>> {
let mut ports = Vec::new();
for guid in get_ports_guids()? {
let port_devices = PortDevices::new(&guid);
for mut port_device in port_devices {
if port_device.problem() != Some(0) {
continue;
}
let port_name = port_device.name();
debug_assert!(
port_name.as_bytes().last().map_or(true, |c| *c != b'\0'),
"port_name has a trailing nul: {:?}",
port_name
);
if port_name.starts_with("LPT") {
continue;
}
ports.push(SerialPortInfo {
port_name,
port_type: port_device.port_type(),
});
}
}
let mut raw_ports_set = get_registry_com_ports();
if raw_ports_set.len() > ports.len() {
for port in ports.iter() {
raw_ports_set.remove(&port.port_name);
}
for raw_port in raw_ports_set {
ports.push(SerialPortInfo {
port_name: raw_port,
port_type: SerialPortType::Unknown,
})
}
}
Ok(ports)
}
#[cfg(test)]
mod tests {
use super::*;
use quickcheck_macros::quickcheck;
#[test]
fn from_utf16_lossy_trimmed_trimming_empty() {
assert_eq!("", from_utf16_lossy_trimmed(&[]));
assert_eq!("", from_utf16_lossy_trimmed(&[0]));
}
#[test]
fn from_utf16_lossy_trimmed_trimming() {
let test_str = "Testing";
let wtest_str: Vec<u16> = as_utf16(test_str);
let wtest_str_trailing = wtest_str
.iter()
.copied()
.chain([0, 0, 0, 0]) .collect::<Vec<_>>();
let and_back = from_utf16_lossy_trimmed(&wtest_str_trailing);
assert_eq!(test_str, and_back);
}
#[quickcheck]
fn quickcheck_hwidmatches_new_does_not_panic_from_random_input(hwid: String) -> bool {
let _ = HwidMatches::new(&hwid);
true
}
#[test]
fn test_hwidmatches_new_corner_cases() {
assert!(HwidMatches::new("").is_none());
assert!(HwidMatches::new("ROOT").is_none());
assert!(HwidMatches::new("ROOT\\").is_none());
assert!(HwidMatches::new("USB\\").is_none());
assert!(HwidMatches::new("USB\\VID_1234").is_none());
assert!(HwidMatches::new("USB\\PID_1234").is_none());
assert!(HwidMatches::new("USB\\MI_12").is_none());
assert_eq!(
HwidMatches::new("VID_1234&PID_5678").unwrap(),
HwidMatches {
vid: "1234",
pid: "5678",
serial: None,
interface: None,
}
);
assert_eq!(
HwidMatches::new("ABC\\VID_1234&PID_5678&MI_90").unwrap(),
HwidMatches {
vid: "1234",
pid: "5678",
serial: None,
interface: Some("90"),
}
);
assert_eq!(
HwidMatches::new("FTDIBUS\\VID_1234&PID_5678&MI_90").unwrap(),
HwidMatches {
vid: "1234",
pid: "5678",
serial: None,
interface: Some("90"),
}
);
assert_eq!(
HwidMatches::new("USB\\VID_1234+PID_5678+MI_90").unwrap(),
HwidMatches {
vid: "1234",
pid: "5678",
serial: None,
interface: Some("90"),
}
);
assert_eq!(
HwidMatches::new("FTDIBUS\\VID_1234+PID_5678\\0000").unwrap(),
HwidMatches {
vid: "1234",
pid: "5678",
serial: Some("0000"),
interface: None,
}
);
}
#[test]
fn test_hwidmatches_new_standard_cases_ftdi() {
assert_eq!(
HwidMatches::new("FTDIBUS\\VID_1234+PID_5678+SERIAL123\\0000").unwrap(),
HwidMatches {
vid: "1234",
pid: "5678",
serial: Some("SERIAL123"),
interface: None,
}
);
}
#[test]
fn test_hwidmatches_new_standard_cases_usb() {
assert_eq!(
HwidMatches::new("USB\\VID_1234&PID_5678").unwrap(),
HwidMatches {
vid: "1234",
pid: "5678",
serial: None,
interface: None,
}
);
assert_eq!(
HwidMatches::new("USB\\VID_1234&PID_5678&MI_90").unwrap(),
HwidMatches {
vid: "1234",
pid: "5678",
serial: None,
interface: Some("90"),
}
);
assert_eq!(
HwidMatches::new("USB\\VID_1234&PID_5678\\SERIAL123").unwrap(),
HwidMatches {
vid: "1234",
pid: "5678",
serial: Some("SERIAL123"),
interface: None,
}
);
assert_eq!(
HwidMatches::new("USB\\VID_1234&PID_5678&MI_90\\SERIAL123").unwrap(),
HwidMatches {
vid: "1234",
pid: "5678",
serial: Some("SERIAL123"),
interface: Some("90"),
}
);
assert_eq!(
HwidMatches::new("USB\\VID_303A&PID_1001\\B4:3A:45:B0:08:24").unwrap(),
HwidMatches {
vid: "303A",
pid: "1001",
serial: Some("B4:3A:45:B0:08:24"),
interface: None,
},
)
}
#[test]
fn test_parsing_usb_port_information() {
let madeup_hwid = r"USB\VID_1D50&PID_6018+6&A694CA9&0&0000";
let info = parse_usb_port_info(madeup_hwid, None).unwrap();
assert_eq!(info.serial_number, Some("6".to_string()));
let bm_uart_hwid = r"USB\VID_1D50&PID_6018&MI_02\6&A694CA9&0&0000";
let bm_parent_hwid = r"USB\VID_1D50&PID_6018\85A12F01";
let info = parse_usb_port_info(bm_uart_hwid, Some(bm_parent_hwid)).unwrap();
assert_eq!(info.vid, 0x1D50);
assert_eq!(info.pid, 0x6018);
assert_eq!(info.serial_number, Some("85A12F01".to_string()));
#[cfg(feature = "usbportinfo-interface")]
assert_eq!(info.interface, Some(2));
let ftdi_serial_hwid = r"FTDIBUS\VID_0403+PID_6001+A702TB52A\0000";
let info = parse_usb_port_info(ftdi_serial_hwid, None).unwrap();
assert_eq!(info.vid, 0x0403);
assert_eq!(info.pid, 0x6001);
assert_eq!(info.serial_number, Some("A702TB52A".to_string()));
#[cfg(feature = "usbportinfo-interface")]
assert_eq!(info.interface, None);
let pyboard_hwid = r"USB\VID_F055&PID_9802\385435603432";
let info = parse_usb_port_info(pyboard_hwid, None).unwrap();
assert_eq!(info.vid, 0xF055);
assert_eq!(info.pid, 0x9802);
assert_eq!(info.serial_number, Some("385435603432".to_string()));
#[cfg(feature = "usbportinfo-interface")]
assert_eq!(info.interface, None);
let unicode_serial = r"USB\VID_F055&PID_9802\3854356β03432&test";
let info = parse_usb_port_info(unicode_serial, None).unwrap();
assert_eq!(info.serial_number.as_deref(), Some("3854356β03432"));
let unicode_serial = r"USB\VID_F055&PID_9802\3854356β03432";
let info = parse_usb_port_info(unicode_serial, None).unwrap();
assert_eq!(info.serial_number.as_deref(), Some("3854356β03432"));
let unicode_serial = r"USB\VID_F055&PID_9802\3854356β";
let info = parse_usb_port_info(unicode_serial, None).unwrap();
assert_eq!(info.serial_number.as_deref(), Some("3854356β"));
let serial_with_underscore_hwid = r"USB\VID_0483&PID_5740\TMCS_B000000000";
let info = parse_usb_port_info(serial_with_underscore_hwid, None).unwrap();
assert_eq!(info.serial_number.as_deref(), Some("TMCS_B000000000"));
}
}