From 9853d4477b60448098bf5f583a2e81a1c654e88d Mon Sep 17 00:00:00 2001 From: JC <8765278+Crzyrndm@users.noreply.github.com> Date: Sat, 18 Mar 2023 12:04:56 +1300 Subject: [PATCH 1/9] query windows registry for all COM ports --- src/windows/enumerate.rs | 90 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/src/windows/enumerate.rs b/src/windows/enumerate.rs index 05c830a0..2422bbfe 100644 --- a/src/windows/enumerate.rs +++ b/src/windows/enumerate.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::ffi::{CStr, CString}; use std::{mem, ptr}; @@ -362,6 +363,82 @@ impl PortDevice { } } +/// Not all COM ports are listed under the "Ports" device class +/// The full list of COM ports is available from the registry at +/// HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM +/// +/// port of https://learn.microsoft.com/en-us/windows/win32/sysinfo/enumerating-registry-subkeys +fn get_registry_com_ports() -> HashSet { + let mut ports_list = HashSet::new(); + + let reg_key = b"HARDWARE\\DEVICEMAP\\SERIALCOMM\0"; + let key_ptr = reg_key.as_ptr() as *const i8; + let mut ports_key = std::ptr::null_mut(); + unsafe { + let open_res = RegOpenKeyExA(HKEY_LOCAL_MACHINE, key_ptr, 0, KEY_READ, &mut ports_key); + if SUCCEEDED(open_res) { + let mut class_name_buff = [0i8; MAX_PATH]; + let mut class_name_size = MAX_PATH as u32; + 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, + }; + RegQueryInfoKeyA( + ports_key, + class_name_buff.as_mut_ptr(), + &mut class_name_size, + std::ptr::null_mut(), + &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, + ); + for idx in 0..num_key_values { + let mut val_name_buff = [0i8; MAX_PATH]; + let mut val_name_size = MAX_PATH as u32; + let mut value_type = 0; + // if 100 chars is not enough for COM something is very wrong + let mut val_data = [0; 100]; + let mut data_size = val_data.len() as u32; + let res = RegEnumValueA( + ports_key, + idx, + val_name_buff.as_mut_ptr(), + &mut val_name_size, + std::ptr::null_mut(), + &mut value_type, + val_data.as_mut_ptr(), + &mut data_size, + ); + if FAILED(res) { + break; + } + let val_data = CStr::from_bytes_with_nul(std::slice::from_raw_parts( + val_data.as_ptr(), + data_size as usize, + )); + + if let Ok(port) = val_data { + ports_list.insert(port.to_string_lossy().into_owned()); + } + } + } + RegCloseKey(ports_key); + } + ports_list +} + /// List available serial ports on the system. pub fn available_ports() -> Result> { let mut ports = Vec::new(); @@ -392,6 +469,19 @@ pub fn available_ports() -> Result> { }); } } + // ports identified through the registry have no additional information + let mut raw_ports_set = get_registry_com_ports(); + // remove any duplicates. HashSet makes this relatively cheap + for port in ports.iter() { + raw_ports_set.remove(&port.port_name); + } + // add remaining ports as "unknown" type + for raw_port in raw_ports_set { + ports.push(SerialPortInfo { + port_name: raw_port, + port_type: SerialPortType::Unknown, + }) + } Ok(ports) } From 23151caef1083820dfdb73d78720de73953bd415 Mon Sep 17 00:00:00 2001 From: JC <8765278+Crzyrndm@users.noreply.github.com> Date: Sat, 18 Mar 2023 12:19:29 +1300 Subject: [PATCH 2/9] individual unsafe blocks --- src/windows/enumerate.rs | 76 +++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/src/windows/enumerate.rs b/src/windows/enumerate.rs index 2422bbfe..2f5f52fc 100644 --- a/src/windows/enumerate.rs +++ b/src/windows/enumerate.rs @@ -374,22 +374,26 @@ fn get_registry_com_ports() -> HashSet { let reg_key = b"HARDWARE\\DEVICEMAP\\SERIALCOMM\0"; let key_ptr = reg_key.as_ptr() as *const i8; let mut ports_key = std::ptr::null_mut(); - unsafe { - let open_res = RegOpenKeyExA(HKEY_LOCAL_MACHINE, key_ptr, 0, KEY_READ, &mut ports_key); - if SUCCEEDED(open_res) { - let mut class_name_buff = [0i8; MAX_PATH]; - let mut class_name_size = MAX_PATH as u32; - 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, - }; + + // SAFETY: ffi, all inputs are correct + let open_res = + unsafe { RegOpenKeyExA(HKEY_LOCAL_MACHINE, key_ptr, 0, KEY_READ, &mut ports_key) }; + if SUCCEEDED(open_res) { + let mut class_name_buff = [0i8; MAX_PATH]; + let mut class_name_size = MAX_PATH as u32; + 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, + }; + // SAFETY: ffi, all inputs are correct + let query_res = unsafe { RegQueryInfoKeyA( ports_key, class_name_buff.as_mut_ptr(), @@ -403,7 +407,9 @@ fn get_registry_com_ports() -> HashSet { &mut longest_value_data, &mut size_security_desc, &mut last_write_time, - ); + ) + }; + if SUCCEEDED(query_res) { for idx in 0..num_key_values { let mut val_name_buff = [0i8; MAX_PATH]; let mut val_name_size = MAX_PATH as u32; @@ -411,30 +417,34 @@ fn get_registry_com_ports() -> HashSet { // if 100 chars is not enough for COM something is very wrong let mut val_data = [0; 100]; let mut data_size = val_data.len() as u32; - let res = RegEnumValueA( - ports_key, - idx, - val_name_buff.as_mut_ptr(), - &mut val_name_size, - std::ptr::null_mut(), - &mut value_type, - val_data.as_mut_ptr(), - &mut data_size, - ); - if FAILED(res) { + // SAFETY: ffi, all inputs are correct + let res = unsafe { + RegEnumValueA( + ports_key, + idx, + val_name_buff.as_mut_ptr(), + &mut val_name_size, + std::ptr::null_mut(), + &mut value_type, + val_data.as_mut_ptr(), + &mut data_size, + ) + }; + if FAILED(res) || val_data.len() < data_size as usize { break; } - let val_data = CStr::from_bytes_with_nul(std::slice::from_raw_parts( - val_data.as_ptr(), - data_size as usize, - )); + // SAFETY: data_size is checked and pointer is valid + let val_data = CStr::from_bytes_with_nul(unsafe { + std::slice::from_raw_parts(val_data.as_ptr(), data_size as usize) + }); if let Ok(port) = val_data { ports_list.insert(port.to_string_lossy().into_owned()); } } } - RegCloseKey(ports_key); + // SAFETY: ffi, all inputs are correct + unsafe { RegCloseKey(ports_key) }; } ports_list } From cb84ceb8a5f9aacd23d0f59248f58b23863401a0 Mon Sep 17 00:00:00 2001 From: JC <8765278+Crzyrndm@users.noreply.github.com> Date: Sat, 25 Mar 2023 16:04:16 +1300 Subject: [PATCH 3/9] add `Modem` as a device class to query for COM ports some refactoring of `get_ports_guids` to handle multiple class names --- src/windows/enumerate.rs | 86 ++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 48 deletions(-) diff --git a/src/windows/enumerate.rs b/src/windows/enumerate.rs index 2f5f52fc..0e517ffb 100644 --- a/src/windows/enumerate.rs +++ b/src/windows/enumerate.rs @@ -25,55 +25,45 @@ use crate::{Error, ErrorKind, Result, SerialPortInfo, SerialPortType, UsbPortInf // // get_pots_guids returns all of the classes (guids) associated with the name "Ports". fn get_ports_guids() -> Result> { - // Note; unwrap can't fail, since "Ports" is valid UTF-8. - let ports_class_name = CString::new("Ports").unwrap(); - - // Size vector to hold 1 result (which is the most common result). - let mut num_guids: DWORD = 0; + // Note; unwrap can't fail, since names are valid UTF-8. + let class_names = [ + CString::new("Ports").unwrap(), + CString::new("Modem").unwrap(), + ]; let mut guids: Vec = Vec::new(); - guids.push(GUID_NULL); // Placeholder for first result - - // Find out how many GUIDs are associated with "Ports". Initially we assume - // that there is only 1. num_guids will tell us how many there actually are. - let res = unsafe { - SetupDiClassGuidsFromNameA( - ports_class_name.as_ptr(), - guids.as_mut_ptr(), - guids.len() as DWORD, - &mut num_guids, - ) - }; - if res == FALSE { - return Err(Error::new( - ErrorKind::Unknown, - "Unable to determine number of Ports GUIDs", - )); - } - if num_guids == 0 { - // We got a successful result of no GUIDs, so pop the placeholder that - // we created before. - guids.pop(); - } - - if num_guids as usize > guids.len() { - // It turns out we needed more that one slot. num_guids will contain the number of slots - // that we actually need, so go ahead and expand the vector to the correct size. - while guids.len() < num_guids as usize { - guids.push(GUID_NULL); - } - let res = unsafe { - SetupDiClassGuidsFromNameA( - ports_class_name.as_ptr(), - guids.as_mut_ptr(), - guids.len() as DWORD, - &mut num_guids, - ) - }; - if res == FALSE { - return Err(Error::new( - ErrorKind::Unknown, - "Unable to retrieve Ports GUIDs", - )); + for class_name in class_names { + let mut num_guids: DWORD = 1; // Initially assume that there is only 1 guid per name. + let class_start_idx = guids.len(); // start idx for this name (for potential resize with multiple guids) + + // first attempt with size == 1, second with the size returned from the first try + for _ in 0..2 { + guids.resize(class_start_idx + num_guids as usize, GUID_NULL); + let guid_buffer = &mut guids[class_start_idx..]; + // Find out how many GUIDs are associated with this class name. num_guids will tell us how many there actually are. + let res = unsafe { + SetupDiClassGuidsFromNameA( + class_name.as_ptr(), + guid_buffer.as_mut_ptr(), + guid_buffer.len() as DWORD, + &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)); + // under allocated + if len_cmp == std::cmp::Ordering::Less { + continue; // retry + } + // allocation > required len + else if len_cmp == std::cmp::Ordering::Greater { + guids.truncate(class_start_idx + num_guids as usize); + } + break; // next guid } } Ok(guids) From b0eb322663513a0a9ea1468fe3e2e945e33e75bf Mon Sep 17 00:00:00 2001 From: JC <8765278+Crzyrndm@users.noreply.github.com> Date: Sat, 25 Mar 2023 16:04:38 +1300 Subject: [PATCH 4/9] clarifying comment --- src/windows/enumerate.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/windows/enumerate.rs b/src/windows/enumerate.rs index 0e517ffb..e576f178 100644 --- a/src/windows/enumerate.rs +++ b/src/windows/enumerate.rs @@ -15,18 +15,18 @@ use winapi::um::winreg::*; use crate::{Error, ErrorKind, Result, SerialPortInfo, SerialPortType, UsbPortInfo}; -// According to the MSDN docs, we should use SetupDiGetClassDevs, SetupDiEnumDeviceInfo -// and SetupDiGetDeviceInstanceId in order to enumerate devices. -// https://msdn.microsoft.com/en-us/windows/hardware/drivers/install/enumerating-installed-devices -// -// SetupDiGetClassDevs returns the devices associated with a particular class of devices. -// We want the list of devices which shows up in the Device Manager as "Ports (COM & LPT)" -// which is otherwise known as the "Ports" class. -// -// get_pots_guids returns all of the classes (guids) associated with the name "Ports". +/// According to the MSDN docs, we should use SetupDiGetClassDevs, SetupDiEnumDeviceInfo +/// and SetupDiGetDeviceInstanceId in order to enumerate devices. +/// https://msdn.microsoft.com/en-us/windows/hardware/drivers/install/enumerating-installed-devices fn get_ports_guids() -> Result> { - // Note; unwrap can't fail, since names are valid UTF-8. + // SetupDiGetClassDevs returns the devices associated with a particular class of devices. + // We want the list of devices which are listed as COM ports (generally those that show up in the + // Device Manager as "Ports (COM & LPT)" which is otherwise known as the "Ports" class). + // + // The list of system defined classes can be found here: + // https://learn.microsoft.com/en-us/windows-hardware/drivers/install/system-defined-device-setup-classes-available-to-vendors let class_names = [ + // Note; since names are valid UTF-8, unwrap can't fail CString::new("Ports").unwrap(), CString::new("Modem").unwrap(), ]; From b30aa8d0588fe1bee7a2ded0043a4857c47133cf Mon Sep 17 00:00:00 2001 From: JC <8765278+Crzyrndm@users.noreply.github.com> Date: Sat, 25 Mar 2023 16:05:22 +1300 Subject: [PATCH 5/9] filter to COM ports rather than removing LPT (unknown what else modems could show up as) --- src/windows/enumerate.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/windows/enumerate.rs b/src/windows/enumerate.rs index e576f178..59c01457 100644 --- a/src/windows/enumerate.rs +++ b/src/windows/enumerate.rs @@ -459,7 +459,7 @@ pub fn available_ports() -> Result> { ); // This technique also returns parallel ports, so we filter these out. - if port_name.starts_with("LPT") { + if !port_name.starts_with("COM") { continue; } From 301f0357e090a58a2e02bf2227727a6ae1d4ba4f Mon Sep 17 00:00:00 2001 From: JC <8765278+Crzyrndm@users.noreply.github.com> Date: Sat, 25 Mar 2023 16:06:00 +1300 Subject: [PATCH 6/9] raw ports only need to be iterated if additional ports have been found --- src/windows/enumerate.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/windows/enumerate.rs b/src/windows/enumerate.rs index 59c01457..b7f9e00d 100644 --- a/src/windows/enumerate.rs +++ b/src/windows/enumerate.rs @@ -471,16 +471,18 @@ pub fn available_ports() -> Result> { } // ports identified through the registry have no additional information let mut raw_ports_set = get_registry_com_ports(); - // remove any duplicates. HashSet makes this relatively cheap - for port in ports.iter() { - raw_ports_set.remove(&port.port_name); - } - // add remaining ports as "unknown" type - for raw_port in raw_ports_set { - ports.push(SerialPortInfo { - port_name: raw_port, - port_type: SerialPortType::Unknown, - }) + if raw_ports_set.len() > ports.len() { + // remove any duplicates. HashSet makes this relatively cheap + for port in ports.iter() { + raw_ports_set.remove(&port.port_name); + } + // add remaining ports as "unknown" type + for raw_port in raw_ports_set { + ports.push(SerialPortInfo { + port_name: raw_port, + port_type: SerialPortType::Unknown, + }) + } } Ok(ports) } From eae981c8f7eb18cdd1c4b2653e6502c27b50e0dc Mon Sep 17 00:00:00 2001 From: JC <8765278+Crzyrndm@users.noreply.github.com> Date: Sat, 25 Mar 2023 16:38:39 +1300 Subject: [PATCH 7/9] resolve typo in comment --- src/windows/enumerate.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/windows/enumerate.rs b/src/windows/enumerate.rs index b7f9e00d..4dcd46ae 100644 --- a/src/windows/enumerate.rs +++ b/src/windows/enumerate.rs @@ -63,7 +63,7 @@ fn get_ports_guids() -> Result> { else if len_cmp == std::cmp::Ordering::Greater { guids.truncate(class_start_idx + num_guids as usize); } - break; // next guid + break; // next name } } Ok(guids) From b8f065dc238df1c13556e1ed69ef5a3e8f3088d1 Mon Sep 17 00:00:00 2001 From: Christian Meusel Date: Sun, 12 May 2024 11:11:07 +0200 Subject: [PATCH 8/9] Clean up Clippy hint for checking for some interface --- src/windows/enumerate.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/windows/enumerate.rs b/src/windows/enumerate.rs index 4dcd46ae..c9599b7e 100644 --- a/src/windows/enumerate.rs +++ b/src/windows/enumerate.rs @@ -100,7 +100,7 @@ fn parse_usb_port_info(hardware_id: &str, parent_hardware_id: Option<&str>) -> O .name("iid") .and_then(|m| u8::from_str_radix(m.as_str(), 16).ok()); - if let Some(_) = interface { + if interface.is_some() { // If this is a composite device, we need to parse the parent's HWID to get the correct information. caps = re.captures(parent_hardware_id?)?; } From 0720d5a64109d43b2d5fa5fb170e3ed052dbf9b1 Mon Sep 17 00:00:00 2001 From: Christian Meusel Date: Sun, 12 May 2024 11:25:05 +0200 Subject: [PATCH 9/9] Stick to filtering out LPT ports on Windows According to issue #187 null-modem ports are appearing as CNCxy and there have not been complaints about other devices showing up unintendedly so far. --- src/windows/enumerate.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/windows/enumerate.rs b/src/windows/enumerate.rs index c9599b7e..0a27992b 100644 --- a/src/windows/enumerate.rs +++ b/src/windows/enumerate.rs @@ -459,7 +459,7 @@ pub fn available_ports() -> Result> { ); // This technique also returns parallel ports, so we filter these out. - if !port_name.starts_with("COM") { + if port_name.starts_with("LPT") { continue; }