Skip to content
Merged
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ project adheres to [Semantic Versioning](https://semver.org/).
## [Unreleased]
### Added
### Changed
* Replace using regex crate for parsing device identification strings for
`available_ports` on Windows. This is now done by some bespoke code to
significantly reduce build times.
[#201](https://github.com/serialport/serialport-rs/pull/201)
### Fixed
### Removed

Expand Down
5 changes: 2 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,6 @@ core-foundation-sys = "0.8.4"
io-kit-sys = "0.4.0"
mach2 = "0.4.1"

[target."cfg(windows)".dependencies]
regex = "1.5.5"

[target."cfg(windows)".dependencies.winapi]
version = "0.3.9"
features = [
Expand All @@ -51,6 +48,8 @@ envconfig = "0.10.0"
# 6.6.0) Until then we are tricking the dependency resolver into using a
# compatible version by adding it as a direct dependency here.
os_str_bytes = ">=6.0, <6.6.0"
quickcheck = "1.0.3"
quickcheck_macros = "1.0.0"
rstest = { version = "0.12.0", default-features = false }
rustversion = "1.0.16"

Expand Down
308 changes: 254 additions & 54 deletions src/windows/enumerate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use std::collections::HashSet;
use std::ffi::{CStr, CString};
use std::{mem, ptr};

use regex::Regex;
use winapi::shared::guiddef::*;
use winapi::shared::minwindef::*;
use winapi::shared::winerror::*;
Expand Down Expand Up @@ -69,6 +68,73 @@ fn get_ports_guids() -> Result<Vec<GUID>> {
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> {
// When we match something, update this so that we are always looking forward
let mut hwid_tail = hwid;

// VID_(?P<vid>[[:xdigit:]]{4})
let vid_start = hwid.find("VID_")?;

// We won't match for hex characters here. That can be done when parsed.
let vid = hwid_tail.get(vid_start + 4..vid_start + 8)?;
hwid_tail = hwid_tail.get(vid_start + 8..)?;

// [&+]PID_(?P<pid>[[:xdigit:]]{4})
let pid = if hwid_tail.starts_with("&PID_") || hwid_tail.starts_with("+PID_") {
// We will let the hex parser fail if there are not hex digits.
hwid_tail.get(5..9)?
} else {
return None;
};
hwid_tail = hwid_tail.get(9..)?;

// (?:[&+]MI_(?P<iid>[[:xdigit:]]{2})){0,1}
let iid = if hwid_tail.starts_with("&MI_") || hwid_tail.starts_with("+MI_") {
// We will let the hex parser fail if there are not hex digits.
let iid = hwid_tail.get(4..6);
hwid_tail = hwid_tail.get(6..).unwrap_or(hwid_tail);

iid
} else {
None
};

// ([\\+](?P<serial>\w+))? with slightly modified check for alphanumeric characters instead
// of regex word character
//
// TODO: Fix returning no serial number at all for devices without one. The previous regex
// and the code below return the first thing from the intance ID. See issue #203.
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())
.map(|(index, _)| index)
.unwrap_or(tail.len());
tail.get(..index)
})
} else {
None
};

Some(Self {
vid,
pid,
serial,
interface: iid,
})
}
}

/// 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 All @@ -86,40 +152,24 @@ fn get_ports_guids() -> Result<Vec<GUID>> {
/// - BlackMagic UART port: USB\VID_1D50&PID_6018&MI_02\6&A694CA9&0&0002
/// - FTDI Serial Adapter: FTDIBUS\VID_0403+PID_6001+A702TB52A\0000
fn parse_usb_port_info(hardware_id: &str, parent_hardware_id: Option<&str>) -> Option<UsbPortInfo> {
let re = Regex::new(concat!(
r"VID_(?P<vid>[[:xdigit:]]{4})",
r"[&+]PID_(?P<pid>[[:xdigit:]]{4})",
r"(?:[&+]MI_(?P<iid>[[:xdigit:]]{2})){0,1}",
r"([\\+](?P<serial>\w+))?"
))
.unwrap();

let mut caps = re.captures(hardware_id)?;
let mut caps = HwidMatches::new(hardware_id)?;

let interface = caps
.name("iid")
.and_then(|m| u8::from_str_radix(m.as_str(), 16).ok());
let interface = caps.interface.and_then(|m| u8::from_str_radix(m, 16).ok());

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?)?;
caps = HwidMatches::new(parent_hardware_id?)?;
}

Some(UsbPortInfo {
vid: u16::from_str_radix(&caps[1], 16).ok()?,
pid: u16::from_str_radix(&caps[2], 16).ok()?,
serial_number: caps.name("serial").map(|m| {
let m = m.as_str();
if m.contains('&') {
m.split('&').nth(1).unwrap().to_string()
} else {
m.to_string()
}
}),
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: interface,
interface,
})
}

Expand Down Expand Up @@ -487,33 +537,183 @@ pub fn available_ports() -> Result<Vec<SerialPortInfo>> {
Ok(ports)
}

#[test]
fn test_parsing_usb_port_information() {
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);
#[cfg(test)]
mod test {
use super::*;

use quickcheck_macros::quickcheck;

// Check that passing some random data to HwidMatches::new() does not cause a panic.
#[quickcheck]
fn quickcheck_hwidmatches_new_does_not_panic_from_random_input(hwid: String) -> bool {
let _ = HwidMatches::new(&hwid);
true
}

// Corner cases which might not always represent what we want to/should parse. But they at
// least illustrate how we are parsing device identification strings today.
#[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"),
}
);
}

#[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();
// TODO: Fix returning no serial at all for devices without one. See issue #203.
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β"));
}
}