• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 use std::collections::HashSet;
2 use std::{mem, ptr};
3 
4 use winapi::ctypes::c_void;
5 use winapi::shared::guiddef::*;
6 use winapi::shared::minwindef::*;
7 use winapi::shared::winerror::*;
8 use winapi::um::cfgmgr32::*;
9 use winapi::um::cguid::GUID_NULL;
10 use winapi::um::setupapi::*;
11 use winapi::um::winnt::{KEY_READ, REG_SZ};
12 use winapi::um::winreg::*;
13 
14 use crate::{Error, ErrorKind, Result, SerialPortInfo, SerialPortType, UsbPortInfo};
15 
16 /// takes normal Rust `str` and outputs a null terminated UTF-16 encoded string
as_utf16(utf8: &str) -> Vec<u16>17 fn as_utf16(utf8: &str) -> Vec<u16> {
18     utf8.encode_utf16().chain(Some(0)).collect()
19 }
20 
21 /// takes a UTF-16 encoded slice (null termination not required)
22 /// and converts to a UTF8 Rust string. Trailing null chars are removed
from_utf16_lossy_trimmed(utf16: &[u16]) -> String23 fn from_utf16_lossy_trimmed(utf16: &[u16]) -> String {
24     String::from_utf16_lossy(utf16)
25         .trim_end_matches(0 as char)
26         .to_string()
27 }
28 
29 /// According to the MSDN docs, we should use SetupDiGetClassDevs, SetupDiEnumDeviceInfo
30 /// and SetupDiGetDeviceInstanceId in order to enumerate devices.
31 /// https://msdn.microsoft.com/en-us/windows/hardware/drivers/install/enumerating-installed-devices
get_ports_guids() -> Result<Vec<GUID>>32 fn get_ports_guids() -> Result<Vec<GUID>> {
33     // SetupDiGetClassDevs returns the devices associated with a particular class of devices.
34     // We want the list of devices which are listed as COM ports (generally those that show up in the
35     // Device Manager as "Ports (COM & LPT)" which is otherwise known as the "Ports" class).
36     //
37     // The list of system defined classes can be found here:
38     // https://learn.microsoft.com/en-us/windows-hardware/drivers/install/system-defined-device-setup-classes-available-to-vendors
39     let class_names = ["Ports", "Modem"];
40     let mut guids: Vec<GUID> = Vec::new();
41     for class_name in class_names {
42         let class_name_w = as_utf16(class_name);
43         let mut num_guids: DWORD = 1; // Initially assume that there is only 1 guid per name.
44         let class_start_idx = guids.len(); // start idx for this name (for potential resize with multiple guids)
45 
46         // first attempt with size == 1, second with the size returned from the first try
47         for _ in 0..2 {
48             guids.resize(class_start_idx + num_guids as usize, GUID_NULL);
49             let guid_buffer = &mut guids[class_start_idx..];
50             // Find out how many GUIDs are associated with this class name.  num_guids will tell us how many there actually are.
51             let res = unsafe {
52                 SetupDiClassGuidsFromNameW(
53                     class_name_w.as_ptr(),
54                     guid_buffer.as_mut_ptr(),
55                     guid_buffer.len() as DWORD,
56                     &mut num_guids,
57                 )
58             };
59             if res == FALSE {
60                 return Err(Error::new(
61                     ErrorKind::Unknown,
62                     "Unable to determine number of Ports GUIDs",
63                 ));
64             }
65             let len_cmp = guid_buffer.len().cmp(&(num_guids as usize));
66             // under allocated
67             if len_cmp == std::cmp::Ordering::Less {
68                 continue; // retry
69             }
70             // allocation > required len
71             else if len_cmp == std::cmp::Ordering::Greater {
72                 guids.truncate(class_start_idx + num_guids as usize);
73             }
74             break; // next name
75         }
76     }
77     Ok(guids)
78 }
79 
80 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
81 struct HwidMatches<'hwid> {
82     vid: &'hwid str,
83     pid: &'hwid str,
84     serial: Option<&'hwid str>,
85     interface: Option<&'hwid str>,
86 }
87 
88 impl<'hwid> HwidMatches<'hwid> {
new(hwid: &'hwid str) -> Option<Self>89     fn new(hwid: &'hwid str) -> Option<Self> {
90         // When we match something, update this so that we are always looking forward
91         let mut hwid_tail = hwid;
92 
93         // VID_(?P<vid>[[:xdigit:]]{4})
94         let vid_start = hwid.find("VID_")?;
95 
96         // We won't match for hex characters here. That can be done when parsed.
97         let vid = hwid_tail.get(vid_start + 4..vid_start + 8)?;
98         hwid_tail = hwid_tail.get(vid_start + 8..)?;
99 
100         // [&+]PID_(?P<pid>[[:xdigit:]]{4})
101         let pid = if hwid_tail.starts_with("&PID_") || hwid_tail.starts_with("+PID_") {
102             // We will let the hex parser fail if there are not hex digits.
103             hwid_tail.get(5..9)?
104         } else {
105             return None;
106         };
107         hwid_tail = hwid_tail.get(9..)?;
108 
109         // (?:[&+]MI_(?P<iid>[[:xdigit:]]{2})){0,1}
110         let iid = if hwid_tail.starts_with("&MI_") || hwid_tail.starts_with("+MI_") {
111             // We will let the hex parser fail if there are not hex digits.
112             let iid = hwid_tail.get(4..6);
113             hwid_tail = hwid_tail.get(6..).unwrap_or(hwid_tail);
114 
115             iid
116         } else {
117             None
118         };
119 
120         // ([\\+](?P<serial>\w+))? with slightly modified check for alphanumeric characters instead
121         // of regex word character
122         //
123         // TODO: Fix returning no serial number at all for devices without one. The previous regex
124         // and the code below return the first thing from the intance ID. See issue #203.
125         let serial = if hwid_tail.starts_with('\\') || hwid_tail.starts_with('+') {
126             hwid_tail.get(1..).and_then(|tail| {
127                 let index = tail
128                     .char_indices()
129                     .find(|&(_, char)| !char.is_alphanumeric())
130                     .map(|(index, _)| index)
131                     .unwrap_or(tail.len());
132                 tail.get(..index)
133             })
134         } else {
135             None
136         };
137 
138         Some(Self {
139             vid,
140             pid,
141             serial,
142             interface: iid,
143         })
144     }
145 }
146 
147 /// Windows usb port information can be determined by the port's HWID string.
148 ///
149 /// This function parses the HWID string using regex, and returns the USB port
150 /// information if the hardware ID can be parsed correctly. The manufacturer
151 /// and product names cannot be determined from the HWID string, so those are
152 /// set as None.
153 ///
154 /// For composite USB devices, the HWID string will be for the interface. In
155 /// this case, the parent HWID string must be provided so that the correct
156 /// serial number can be determined.
157 ///
158 /// Some HWID examples are:
159 ///   - MicroPython pyboard:    USB\VID_F055&PID_9802\385435603432
160 ///   - BlackMagic GDB Server:  USB\VID_1D50&PID_6018&MI_00\6&A694CA9&0&0000
161 ///   - BlackMagic UART port:   USB\VID_1D50&PID_6018&MI_02\6&A694CA9&0&0002
162 ///   - FTDI Serial Adapter:    FTDIBUS\VID_0403+PID_6001+A702TB52A\0000
parse_usb_port_info(hardware_id: &str, parent_hardware_id: Option<&str>) -> Option<UsbPortInfo>163 fn parse_usb_port_info(hardware_id: &str, parent_hardware_id: Option<&str>) -> Option<UsbPortInfo> {
164     let mut caps = HwidMatches::new(hardware_id)?;
165 
166     let interface = caps.interface.and_then(|m| u8::from_str_radix(m, 16).ok());
167 
168     if interface.is_some() {
169         // If this is a composite device, we need to parse the parent's HWID to get the correct information.
170         caps = HwidMatches::new(parent_hardware_id?)?;
171     }
172 
173     Some(UsbPortInfo {
174         vid: u16::from_str_radix(caps.vid, 16).ok()?,
175         pid: u16::from_str_radix(caps.pid, 16).ok()?,
176         serial_number: caps.serial.map(str::to_string),
177         manufacturer: None,
178         product: None,
179 
180         #[cfg(feature = "usbportinfo-interface")]
181         interface,
182     })
183 }
184 
185 struct PortDevices {
186     /// Handle to a device information set.
187     hdi: HDEVINFO,
188 
189     /// Index used by iterator.
190     dev_idx: DWORD,
191 }
192 
193 impl PortDevices {
194     // Creates PortDevices object which represents the set of devices associated with a particular
195     // Ports class (given by `guid`).
new(guid: &GUID) -> Self196     pub fn new(guid: &GUID) -> Self {
197         PortDevices {
198             hdi: unsafe { SetupDiGetClassDevsW(guid, ptr::null(), ptr::null_mut(), DIGCF_PRESENT) },
199             dev_idx: 0,
200         }
201     }
202 }
203 
204 impl Iterator for PortDevices {
205     type Item = PortDevice;
206 
207     /// Iterator which returns a PortDevice from the set of PortDevices associated with a
208     /// particular PortDevices class (guid).
next(&mut self) -> Option<PortDevice>209     fn next(&mut self) -> Option<PortDevice> {
210         let mut port_dev = PortDevice {
211             hdi: self.hdi,
212             devinfo_data: SP_DEVINFO_DATA {
213                 cbSize: mem::size_of::<SP_DEVINFO_DATA>() as DWORD,
214                 ClassGuid: GUID_NULL,
215                 DevInst: 0,
216                 Reserved: 0,
217             },
218         };
219         let res =
220             unsafe { SetupDiEnumDeviceInfo(self.hdi, self.dev_idx, &mut port_dev.devinfo_data) };
221         if res == FALSE {
222             None
223         } else {
224             self.dev_idx += 1;
225             Some(port_dev)
226         }
227     }
228 }
229 
230 impl Drop for PortDevices {
drop(&mut self)231     fn drop(&mut self) {
232         // Release the PortDevices object allocated in the constructor.
233         unsafe {
234             SetupDiDestroyDeviceInfoList(self.hdi);
235         }
236     }
237 }
238 
239 struct PortDevice {
240     /// Handle to a device information set.
241     hdi: HDEVINFO,
242 
243     /// Information associated with this device.
244     pub devinfo_data: SP_DEVINFO_DATA,
245 }
246 
247 impl PortDevice {
248     /// Retrieves the device instance id string associated with this device's parent.
249     /// This is useful for determining the serial number of a composite USB device.
parent_instance_id(&mut self) -> Option<String>250     fn parent_instance_id(&mut self) -> Option<String> {
251         let mut result_buf = [0u16; MAX_PATH];
252         let mut parent_device_instance_id = 0;
253 
254         let res =
255             unsafe { CM_Get_Parent(&mut parent_device_instance_id, self.devinfo_data.DevInst, 0) };
256         if res == CR_SUCCESS {
257             let buffer_len = result_buf.len() - 1;
258             let res = unsafe {
259                 CM_Get_Device_IDW(
260                     parent_device_instance_id,
261                     result_buf.as_mut_ptr(),
262                     buffer_len as ULONG,
263                     0,
264                 )
265             };
266 
267             if res == CR_SUCCESS {
268                 Some(from_utf16_lossy_trimmed(&result_buf))
269             } else {
270                 None
271             }
272         } else {
273             None
274         }
275     }
276 
277     /// Retrieves the device instance id string associated with this device. Some examples of
278     /// instance id strings are:
279     /// * MicroPython Board:  USB\VID_F055&PID_9802\385435603432
280     /// * FTDI USB Adapter:   FTDIBUS\VID_0403+PID_6001+A702TB52A\0000
281     /// * Black Magic Probe (Composite device with 2 UARTS):
282     ///   * GDB Port:       USB\VID_1D50&PID_6018&MI_00\6&A694CA9&0&0000
283     ///   * UART Port:      USB\VID_1D50&PID_6018&MI_02\6&A694CA9&0&0002
284     ///
285     /// Reference: https://learn.microsoft.com/en-us/windows-hardware/drivers/install/device-instance-ids
instance_id(&mut self) -> Option<String>286     fn instance_id(&mut self) -> Option<String> {
287         let mut result_buf = [0u16; MAX_DEVICE_ID_LEN];
288         let working_buffer_len = result_buf.len() - 1; // always null terminated
289         let mut desired_result_len = 0; // possibly larger than the buffer
290         let res = unsafe {
291             SetupDiGetDeviceInstanceIdW(
292                 self.hdi,
293                 &mut self.devinfo_data,
294                 result_buf.as_mut_ptr(),
295                 working_buffer_len as DWORD,
296                 &mut desired_result_len,
297             )
298         };
299         if res == FALSE {
300             // Try to retrieve hardware id property.
301             self.property(SPDRP_HARDWAREID)
302         } else {
303             let actual_result_len = working_buffer_len.min(desired_result_len as usize);
304             Some(from_utf16_lossy_trimmed(&result_buf[..actual_result_len]))
305         }
306     }
307 
308     /// Retrieves the problem status of this device. For example, `CM_PROB_DISABLED` indicates
309     /// the device has been disabled in Device Manager.
problem(&mut self) -> Option<ULONG>310     fn problem(&mut self) -> Option<ULONG> {
311         let mut status = 0;
312         let mut problem_number = 0;
313         let res = unsafe {
314             CM_Get_DevNode_Status(
315                 &mut status,
316                 &mut problem_number,
317                 self.devinfo_data.DevInst,
318                 0,
319             )
320         };
321         if res == CR_SUCCESS {
322             Some(problem_number)
323         } else {
324             None
325         }
326     }
327 
328     // Retrieves the port name (i.e. COM6) associated with this device.
name(&mut self) -> String329     pub fn name(&mut self) -> String {
330         // https://learn.microsoft.com/en-us/windows/win32/api/setupapi/nf-setupapi-setupdiopendevregkey
331         let hkey = unsafe {
332             SetupDiOpenDevRegKey(
333                 self.hdi,
334                 &mut self.devinfo_data,
335                 DICS_FLAG_GLOBAL,
336                 0,
337                 DIREG_DEV,
338                 KEY_READ,
339             )
340         };
341 
342         if hkey as *mut c_void == winapi::um::handleapi::INVALID_HANDLE_VALUE {
343             // failed to open registry key. Return empty string as the failure case
344             return String::new();
345         }
346 
347         // https://learn.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regqueryvalueexw
348         let mut port_name_buffer = [0u16; MAX_PATH];
349         let buffer_byte_len = 2 * port_name_buffer.len() as DWORD;
350         let mut byte_len = buffer_byte_len;
351         let mut value_type = 0;
352 
353         let value_name = as_utf16("PortName");
354         let err = unsafe {
355             RegQueryValueExW(
356                 hkey,
357                 value_name.as_ptr(),
358                 ptr::null_mut(),
359                 &mut value_type,
360                 port_name_buffer.as_mut_ptr() as *mut u8,
361                 &mut byte_len,
362             )
363         };
364         unsafe { RegCloseKey(hkey) };
365         if FAILED(err) {
366             // failed to query registry for some reason. Return empty string as the failure case
367             return String::new();
368         }
369         // https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry-value-types
370         if value_type != REG_SZ || byte_len % 2 != 0 || byte_len > buffer_byte_len {
371             // read something but it wasn't the expected registry type
372             return String::new();
373         }
374         // len of u16 chars, not bytes
375         let len = buffer_byte_len as usize / 2;
376         let port_name = &port_name_buffer[0..len];
377 
378         from_utf16_lossy_trimmed(port_name)
379     }
380 
381     // Determines the port_type for this device, and if it's a USB port populate the various fields.
port_type(&mut self) -> SerialPortType382     pub fn port_type(&mut self) -> SerialPortType {
383         self.instance_id()
384             .map(|s| (s, self.parent_instance_id())) // Get parent instance id if it exists.
385             .and_then(|(d, p)| parse_usb_port_info(&d, p.as_deref()))
386             .map(|mut info: UsbPortInfo| {
387                 info.manufacturer = self.property(SPDRP_MFG);
388                 info.product = self.property(SPDRP_FRIENDLYNAME);
389                 SerialPortType::UsbPort(info)
390             })
391             .unwrap_or(SerialPortType::Unknown)
392     }
393 
394     // Retrieves a device property and returns it, if it exists. Returns None if the property
395     // doesn't exist.
property(&mut self, property_id: DWORD) -> Option<String>396     fn property(&mut self, property_id: DWORD) -> Option<String> {
397         let mut value_type = 0;
398         let mut property_buf = [0u16; MAX_PATH];
399 
400         let res = unsafe {
401             SetupDiGetDeviceRegistryPropertyW(
402                 self.hdi,
403                 &mut self.devinfo_data,
404                 property_id,
405                 &mut value_type,
406                 property_buf.as_mut_ptr() as PBYTE,
407                 property_buf.len() as DWORD,
408                 ptr::null_mut(),
409             )
410         };
411 
412         if res == FALSE || value_type != REG_SZ {
413             return None;
414         }
415 
416         // Using the unicode version of 'SetupDiGetDeviceRegistryProperty' seems to report the
417         // entire mfg registry string. This typically includes some driver information that we should discard.
418         // Example string: 'FTDI5.inf,%ftdi%;FTDI'
419         from_utf16_lossy_trimmed(&property_buf)
420             .split(';')
421             .last()
422             .map(str::to_string)
423     }
424 }
425 
426 /// Not all COM ports are listed under the "Ports" device class
427 /// The full list of COM ports is available from the registry at
428 /// HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM
429 ///
430 /// port of https://learn.microsoft.com/en-us/windows/win32/sysinfo/enumerating-registry-subkeys
get_registry_com_ports() -> HashSet<String>431 fn get_registry_com_ports() -> HashSet<String> {
432     let mut ports_list = HashSet::new();
433 
434     let reg_key = as_utf16("HARDWARE\\DEVICEMAP\\SERIALCOMM");
435     let key_ptr = reg_key.as_ptr();
436     let mut ports_key = std::ptr::null_mut();
437 
438     // SAFETY: ffi, all inputs are correct
439     let open_res =
440         unsafe { RegOpenKeyExW(HKEY_LOCAL_MACHINE, key_ptr, 0, KEY_READ, &mut ports_key) };
441     if SUCCEEDED(open_res) {
442         let mut class_name_buff = [0u16; MAX_PATH];
443         let mut class_name_size = MAX_PATH as u32;
444         let mut sub_key_count = 0;
445         let mut largest_sub_key = 0;
446         let mut largest_class_string = 0;
447         let mut num_key_values = 0;
448         let mut longest_value_name = 0;
449         let mut longest_value_data = 0;
450         let mut size_security_desc = 0;
451         let mut last_write_time = FILETIME {
452             dwLowDateTime: 0,
453             dwHighDateTime: 0,
454         };
455         // SAFETY: ffi, all inputs are correct
456         let query_res = unsafe {
457             RegQueryInfoKeyW(
458                 ports_key,
459                 class_name_buff.as_mut_ptr(),
460                 &mut class_name_size,
461                 std::ptr::null_mut(),
462                 &mut sub_key_count,
463                 &mut largest_sub_key,
464                 &mut largest_class_string,
465                 &mut num_key_values,
466                 &mut longest_value_name,
467                 &mut longest_value_data,
468                 &mut size_security_desc,
469                 &mut last_write_time,
470             )
471         };
472         if SUCCEEDED(query_res) {
473             for idx in 0..num_key_values {
474                 let mut val_name_buff = [0u16; MAX_PATH];
475                 let mut val_name_size = MAX_PATH as u32;
476                 let mut value_type = 0;
477                 let mut val_data = [0u16; MAX_PATH];
478                 let buffer_byte_len = 2 * val_data.len() as DWORD; // len doubled
479                 let mut byte_len = buffer_byte_len;
480 
481                 // SAFETY: ffi, all inputs are correct
482                 let res = unsafe {
483                     RegEnumValueW(
484                         ports_key,
485                         idx,
486                         val_name_buff.as_mut_ptr(),
487                         &mut val_name_size,
488                         std::ptr::null_mut(),
489                         &mut value_type,
490                         val_data.as_mut_ptr() as *mut u8,
491                         &mut byte_len,
492                     )
493                 };
494                 if FAILED(res)
495                     || value_type != REG_SZ // only valid for text values
496                     || byte_len % 2 != 0 // out byte len should be a multiple of u16 size
497                     || byte_len > buffer_byte_len
498                 {
499                     break;
500                 }
501                 // key data is returned as u16
502                 // SAFETY: data_size is checked and pointer is valid
503                 let val_data = from_utf16_lossy_trimmed(unsafe {
504                     let utf16_len = byte_len / 2; // utf16 len
505                     std::slice::from_raw_parts(val_data.as_ptr(), utf16_len as usize)
506                 });
507                 ports_list.insert(val_data);
508             }
509         }
510         // SAFETY: ffi, all inputs are correct
511         unsafe { RegCloseKey(ports_key) };
512     }
513     ports_list
514 }
515 
516 /// List available serial ports on the system.
available_ports() -> Result<Vec<SerialPortInfo>>517 pub fn available_ports() -> Result<Vec<SerialPortInfo>> {
518     let mut ports = Vec::new();
519     for guid in get_ports_guids()? {
520         let port_devices = PortDevices::new(&guid);
521         for mut port_device in port_devices {
522             // Ignore nonfunctional devices
523             if port_device.problem() != Some(0) {
524                 continue;
525             }
526 
527             let port_name = port_device.name();
528 
529             debug_assert!(
530                 port_name.as_bytes().last().map_or(true, |c| *c != b'\0'),
531                 "port_name has a trailing nul: {:?}",
532                 port_name
533             );
534 
535             // This technique also returns parallel ports, so we filter these out.
536             if port_name.starts_with("LPT") {
537                 continue;
538             }
539 
540             ports.push(SerialPortInfo {
541                 port_name,
542                 port_type: port_device.port_type(),
543             });
544         }
545     }
546     // ports identified through the registry have no additional information
547     let mut raw_ports_set = get_registry_com_ports();
548     if raw_ports_set.len() > ports.len() {
549         // remove any duplicates. HashSet makes this relatively cheap
550         for port in ports.iter() {
551             raw_ports_set.remove(&port.port_name);
552         }
553         // add remaining ports as "unknown" type
554         for raw_port in raw_ports_set {
555             ports.push(SerialPortInfo {
556                 port_name: raw_port,
557                 port_type: SerialPortType::Unknown,
558             })
559         }
560     }
561     Ok(ports)
562 }
563 
564 #[cfg(test)]
565 mod tests {
566     use super::*;
567 
568     use quickcheck_macros::quickcheck;
569 
570     #[test]
from_utf16_lossy_trimmed_trimming_empty()571     fn from_utf16_lossy_trimmed_trimming_empty() {
572         assert_eq!("", from_utf16_lossy_trimmed(&[]));
573         assert_eq!("", from_utf16_lossy_trimmed(&[0]));
574     }
575 
576     #[test]
from_utf16_lossy_trimmed_trimming()577     fn from_utf16_lossy_trimmed_trimming() {
578         let test_str = "Testing";
579         let wtest_str: Vec<u16> = as_utf16(test_str);
580         let wtest_str_trailing = wtest_str
581             .iter()
582             .copied()
583             .chain([0, 0, 0, 0]) // add some null chars
584             .collect::<Vec<_>>();
585         let and_back = from_utf16_lossy_trimmed(&wtest_str_trailing);
586 
587         assert_eq!(test_str, and_back);
588     }
589 
590     // Check that passing some random data to HwidMatches::new() does not cause a panic.
591     #[quickcheck]
quickcheck_hwidmatches_new_does_not_panic_from_random_input(hwid: String) -> bool592     fn quickcheck_hwidmatches_new_does_not_panic_from_random_input(hwid: String) -> bool {
593         let _ = HwidMatches::new(&hwid);
594         true
595     }
596 
597     // Corner cases which might not always represent what we want to/should parse. But they at
598     // least illustrate how we are parsing device identification strings today.
599     #[test]
test_hwidmatches_new_corner_cases()600     fn test_hwidmatches_new_corner_cases() {
601         assert!(HwidMatches::new("").is_none());
602         assert!(HwidMatches::new("ROOT").is_none());
603         assert!(HwidMatches::new("ROOT\\").is_none());
604         assert!(HwidMatches::new("USB\\").is_none());
605         assert!(HwidMatches::new("USB\\VID_1234").is_none());
606         assert!(HwidMatches::new("USB\\PID_1234").is_none());
607         assert!(HwidMatches::new("USB\\MI_12").is_none());
608 
609         assert_eq!(
610             HwidMatches::new("VID_1234&PID_5678").unwrap(),
611             HwidMatches {
612                 vid: "1234",
613                 pid: "5678",
614                 serial: None,
615                 interface: None,
616             }
617         );
618 
619         assert_eq!(
620             HwidMatches::new("ABC\\VID_1234&PID_5678&MI_90").unwrap(),
621             HwidMatches {
622                 vid: "1234",
623                 pid: "5678",
624                 serial: None,
625                 interface: Some("90"),
626             }
627         );
628 
629         assert_eq!(
630             HwidMatches::new("FTDIBUS\\VID_1234&PID_5678&MI_90").unwrap(),
631             HwidMatches {
632                 vid: "1234",
633                 pid: "5678",
634                 serial: None,
635                 interface: Some("90"),
636             }
637         );
638 
639         assert_eq!(
640             HwidMatches::new("USB\\VID_1234+PID_5678+MI_90").unwrap(),
641             HwidMatches {
642                 vid: "1234",
643                 pid: "5678",
644                 serial: None,
645                 interface: Some("90"),
646             }
647         );
648 
649         assert_eq!(
650             HwidMatches::new("FTDIBUS\\VID_1234+PID_5678\\0000").unwrap(),
651             HwidMatches {
652                 vid: "1234",
653                 pid: "5678",
654                 serial: Some("0000"),
655                 interface: None,
656             }
657         );
658     }
659 
660     #[test]
661     fn test_hwidmatches_new_standard_cases_ftdi() {
662         assert_eq!(
663             HwidMatches::new("FTDIBUS\\VID_1234+PID_5678+SERIAL123\\0000").unwrap(),
664             HwidMatches {
665                 vid: "1234",
666                 pid: "5678",
667                 serial: Some("SERIAL123"),
668                 interface: None,
669             }
670         );
671     }
672 
673     #[test]
674     fn test_hwidmatches_new_standard_cases_usb() {
675         assert_eq!(
676             HwidMatches::new("USB\\VID_1234&PID_5678").unwrap(),
677             HwidMatches {
678                 vid: "1234",
679                 pid: "5678",
680                 serial: None,
681                 interface: None,
682             }
683         );
684 
685         assert_eq!(
686             HwidMatches::new("USB\\VID_1234&PID_5678&MI_90").unwrap(),
687             HwidMatches {
688                 vid: "1234",
689                 pid: "5678",
690                 serial: None,
691                 interface: Some("90"),
692             }
693         );
694 
695         assert_eq!(
696             HwidMatches::new("USB\\VID_1234&PID_5678\\SERIAL123").unwrap(),
697             HwidMatches {
698                 vid: "1234",
699                 pid: "5678",
700                 serial: Some("SERIAL123"),
701                 interface: None,
702             }
703         );
704 
705         assert_eq!(
706             HwidMatches::new("USB\\VID_1234&PID_5678&MI_90\\SERIAL123").unwrap(),
707             HwidMatches {
708                 vid: "1234",
709                 pid: "5678",
710                 serial: Some("SERIAL123"),
711                 interface: Some("90"),
712             }
713         );
714     }
715 
716     #[test]
717     fn test_parsing_usb_port_information() {
718         let madeup_hwid = r"USB\VID_1D50&PID_6018+6&A694CA9&0&0000";
719         let info = parse_usb_port_info(madeup_hwid, None).unwrap();
720         // TODO: Fix returning no serial at all for devices without one. See issue #203.
721         assert_eq!(info.serial_number, Some("6".to_string()));
722 
723         let bm_uart_hwid = r"USB\VID_1D50&PID_6018&MI_02\6&A694CA9&0&0000";
724         let bm_parent_hwid = r"USB\VID_1D50&PID_6018\85A12F01";
725         let info = parse_usb_port_info(bm_uart_hwid, Some(bm_parent_hwid)).unwrap();
726 
727         assert_eq!(info.vid, 0x1D50);
728         assert_eq!(info.pid, 0x6018);
729         assert_eq!(info.serial_number, Some("85A12F01".to_string()));
730         #[cfg(feature = "usbportinfo-interface")]
731         assert_eq!(info.interface, Some(2));
732 
733         let ftdi_serial_hwid = r"FTDIBUS\VID_0403+PID_6001+A702TB52A\0000";
734         let info = parse_usb_port_info(ftdi_serial_hwid, None).unwrap();
735 
736         assert_eq!(info.vid, 0x0403);
737         assert_eq!(info.pid, 0x6001);
738         assert_eq!(info.serial_number, Some("A702TB52A".to_string()));
739         #[cfg(feature = "usbportinfo-interface")]
740         assert_eq!(info.interface, None);
741 
742         let pyboard_hwid = r"USB\VID_F055&PID_9802\385435603432";
743         let info = parse_usb_port_info(pyboard_hwid, None).unwrap();
744 
745         assert_eq!(info.vid, 0xF055);
746         assert_eq!(info.pid, 0x9802);
747         assert_eq!(info.serial_number, Some("385435603432".to_string()));
748         #[cfg(feature = "usbportinfo-interface")]
749         assert_eq!(info.interface, None);
750 
751         let unicode_serial = r"USB\VID_F055&PID_9802\3854356β03432&test";
752         let info = parse_usb_port_info(unicode_serial, None).unwrap();
753         assert_eq!(info.serial_number.as_deref(), Some("3854356β03432"));
754 
755         let unicode_serial = r"USB\VID_F055&PID_9802\3854356β03432";
756         let info = parse_usb_port_info(unicode_serial, None).unwrap();
757         assert_eq!(info.serial_number.as_deref(), Some("3854356β03432"));
758 
759         let unicode_serial = r"USB\VID_F055&PID_9802\3854356β";
760         let info = parse_usb_port_info(unicode_serial, None).unwrap();
761         assert_eq!(info.serial_number.as_deref(), Some("3854356β"));
762     }
763 }
764