diff --git a/Cargo.toml b/Cargo.toml index a8c1ec9c..c349c9dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,3 +51,4 @@ default = ["libudev"] # TODO: Make the feature unconditionally available with the next major release # (5.0) and remove this feature gate. usbportinfo-interface = [] +iocontrol = ["winapi/ioapiset", "winapi/usbioctl", "winapi/winnls"] diff --git a/src/windows/enumerate.rs b/src/windows/enumerate.rs index 05c830a0..9ebb4ad8 100644 --- a/src/windows/enumerate.rs +++ b/src/windows/enumerate.rs @@ -14,6 +14,12 @@ use winapi::um::winreg::*; use crate::{Error, ErrorKind, Result, SerialPortInfo, SerialPortType, UsbPortInfo}; +#[cfg(feature = "iocontrol")] +mod iocontrol; + +#[cfg(feature = "iocontrol")] +use iocontrol::{IoControl, IoDescriptor}; + // 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 @@ -94,7 +100,11 @@ fn get_ports_guids() -> Result> { /// - BlackMagic GDB Server: USB\VID_1D50&PID_6018&MI_00\6&A694CA9&0&0000 /// - 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 { +fn parse_usb_port_info( + hardware_id: &str, + parent_hardware_id: Option<&str>, + #[cfg(feature = "iocontrol")] device_location_info: Option<&str>, +) -> Option { let re = Regex::new(concat!( r"VID_(?P[[:xdigit:]]{4})", r"[&+]PID_(?P[[:xdigit:]]{4})", @@ -109,12 +119,31 @@ 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?)?; } - Some(UsbPortInfo { + #[cfg(not(feature = "iocontrol"))] + let usb_port_info = 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() + } + }), + manufacturer: None, + product: None, + #[cfg(feature = "usbportinfo-interface")] + interface, + }; + + #[cfg(feature = "iocontrol")] + let mut usb_port_info = 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| { @@ -128,8 +157,37 @@ fn parse_usb_port_info(hardware_id: &str, parent_hardware_id: Option<&str>) -> O manufacturer: None, product: None, #[cfg(feature = "usbportinfo-interface")] - interface: interface, - }) + interface, + }; + + #[cfg(feature = "iocontrol")] + if parent_hardware_id.is_some() && device_location_info.is_some() { + let re = Regex::new(concat!(r"Port_#(?P[[:xdigit:]]{4})",)).unwrap(); + + caps = re.captures(device_location_info?)?; + let port_number = u8::from_str_radix(&caps[1], 8).ok()?; + + let hub_name = format!( + "{}#{{f18a0e88-c30c-11d0-8815-00a0c906bed8}}", + parent_hardware_id?.replace('\\', "#"), + ); + + let hdevice = IoControl::get_handle(&mut hub_name.clone()).ok()?; + + let imanufacturer = IoControl::get_usb_string_descriptor( + &hdevice, + port_number, + &IoDescriptor::Manufacturer, + ); + + let iproduct = + IoControl::get_usb_string_descriptor(&hdevice, port_number, &IoDescriptor::Product); + + usb_port_info.manufacturer = imanufacturer; + usb_port_info.product = iproduct; + } + + Some(usb_port_info) } struct PortDevices { @@ -320,11 +378,32 @@ impl PortDevice { // Determines the port_type for this device, and if it's a USB port populate the various fields. pub fn port_type(&mut self) -> SerialPortType { self.instance_id() - .map(|s| (s, self.parent_instance_id())) // Get parent instance id if it exists. - .and_then(|(d, p)| parse_usb_port_info(&d, p.as_deref())) + .map(|s| { + ( + s, + self.parent_instance_id(), + #[cfg(feature = "iocontrol")] + self.property(SPDRP_LOCATION_INFORMATION), + ) + }) // Get parent instance id if it exists. + .and_then( + |#[cfg(not(feature = "iocontrol"))] (d, p), + #[cfg(feature = "iocontrol")] (d, p, l)| { + parse_usb_port_info( + &d, + p.as_deref(), + #[cfg(feature = "iocontrol")] + l.as_deref(), + ) + }, + ) .map(|mut info: UsbPortInfo| { - info.manufacturer = self.property(SPDRP_MFG); - info.product = self.property(SPDRP_FRIENDLYNAME); + if info.manufacturer.is_none() { + info.manufacturer = self.property(SPDRP_MFG) + }; + if info.product.is_none() { + info.product = self.property(SPDRP_FRIENDLYNAME) + }; SerialPortType::UsbPort(info) }) .unwrap_or(SerialPortType::Unknown) @@ -399,7 +478,13 @@ pub fn available_ports() -> Result> { 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(); + let info = parse_usb_port_info( + bm_uart_hwid, + Some(bm_parent_hwid), + #[cfg(feature = "iocontrol")] + None, + ) + .unwrap(); assert_eq!(info.vid, 0x1D50); assert_eq!(info.pid, 0x6018); @@ -408,7 +493,13 @@ fn test_parsing_usb_port_information() { 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(); + let info = parse_usb_port_info( + ftdi_serial_hwid, + None, + #[cfg(feature = "iocontrol")] + None, + ) + .unwrap(); assert_eq!(info.vid, 0x0403); assert_eq!(info.pid, 0x6001); @@ -417,7 +508,13 @@ fn test_parsing_usb_port_information() { 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(); + let info = parse_usb_port_info( + pyboard_hwid, + None, + #[cfg(feature = "iocontrol")] + None, + ) + .unwrap(); assert_eq!(info.vid, 0xF055); assert_eq!(info.pid, 0x9802); diff --git a/src/windows/enumerate/iocontrol.rs b/src/windows/enumerate/iocontrol.rs new file mode 100644 index 00000000..88bc7439 --- /dev/null +++ b/src/windows/enumerate/iocontrol.rs @@ -0,0 +1,244 @@ +use std::{ffi::OsStr, os::windows::ffi::OsStrExt, ptr}; +use winapi::ctypes::c_void; +use winapi::shared::minwindef::{UCHAR, ULONG, USHORT}; +use winapi::shared::usbioctl::IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION; +use winapi::shared::usbioctl::IOCTL_USB_GET_NODE_CONNECTION_INFORMATION; +use winapi::shared::usbioctl::USB_CONNECTION_STATUS; +use winapi::shared::usbspec::USB_STRING_DESCRIPTOR_TYPE; +use winapi::um::fileapi::CreateFileW; +use winapi::um::fileapi::OPEN_EXISTING; +use winapi::um::handleapi::INVALID_HANDLE_VALUE; +use winapi::um::ioapiset::DeviceIoControl; +use winapi::um::winbase::SECURITY_ANONYMOUS; +use winapi::um::winnls::GetSystemDefaultLangID; +use winapi::um::winnt::FILE_GENERIC_WRITE; +use winapi::um::winnt::FILE_SHARE_WRITE; +use winapi::um::winnt::{BOOLEAN, HANDLE, LPCWSTR}; + +use crate::windows::error; +use crate::Error; + +#[derive(Default)] +#[allow(non_snake_case)] +#[repr(C)] +#[repr(packed)] +struct UsbEndpointDescriptor { + bLength: UCHAR, + bDescriptorType: UCHAR, + bEndpointAddress: UCHAR, + bmAttributes: UCHAR, + wMaxPacketSize: USHORT, + bInterval: UCHAR, +} + +#[derive(Default)] +#[allow(non_snake_case)] +#[repr(C)] +#[repr(packed)] +struct UsbPipeInfo { + EndpointDescriptor: UsbEndpointDescriptor, + ScheduleOffset: ULONG, +} + +#[derive(Default)] +#[allow(non_snake_case)] +#[repr(C)] +#[repr(packed)] +struct UsbDeviceDescriptor { + bLength: UCHAR, + bDescriptorType: UCHAR, + bcdUSB: USHORT, + bDeviceClass: UCHAR, + bDeviceSubClass: UCHAR, + bDeviceProtocol: UCHAR, + bMaxPacketSize0: UCHAR, + idVendor: USHORT, + idProduct: USHORT, + bcdDevice: USHORT, + iManufacturer: UCHAR, + iProduct: UCHAR, + iSerialNumber: UCHAR, + bNumConfigurations: UCHAR, +} + +#[derive(Default)] +#[allow(non_snake_case)] +#[repr(C)] +#[repr(packed)] +struct UsbNodeConnectionInformation { + ConnectionIndex: ULONG, + DeviceDescriptor: UsbDeviceDescriptor, + CurrentConfigurationValue: UCHAR, + LowSpeed: BOOLEAN, + DeviceIsHub: BOOLEAN, + DeviceAddress: USHORT, + NumberOfOpenPipes: ULONG, + ConnectionStatus: USB_CONNECTION_STATUS, + PipeList: [UsbPipeInfo; 0], +} + +#[derive(Default)] +#[allow(non_snake_case)] +#[repr(C)] +#[repr(packed)] +struct UsbDescriptorRequestSetupPacket { + bmRequest: UCHAR, + bRequest: UCHAR, + wValue: USHORT, + wIndex: USHORT, + wLength: USHORT, +} + +#[derive(Default)] +#[allow(non_snake_case)] +#[repr(C)] +#[repr(packed)] +struct UsbDescriptorRequest { + ConnectionIndex: ULONG, + SetupPacket: UsbDescriptorRequestSetupPacket, + Data: [UCHAR; 0], +} + +#[derive(Debug)] +pub enum IoDescriptor { + Manufacturer, + Product, + #[allow(dead_code)] + SerialNumber, +} + +pub struct IoControl; + +impl IoControl { + pub fn get_usb_string_descriptor( + hdevice: &HANDLE, + port_number: u8, + descriptor: &IoDescriptor, + ) -> Option { + let mut lpinbuffer = UsbNodeConnectionInformation { + ConnectionIndex: port_number as ULONG, + ..Default::default() + }; + let lpinbuffer_ptr: *mut c_void = IoControl::get_mut_ptr(&mut lpinbuffer); + + let mut lpoutbuffer = UsbNodeConnectionInformation::default(); + let lpoutbuffer_ptr: *mut c_void = IoControl::get_mut_ptr(&mut lpoutbuffer); + + if unsafe { + // https://learn.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-deviceiocontrol + DeviceIoControl( + *hdevice, + IOCTL_USB_GET_NODE_CONNECTION_INFORMATION, + lpinbuffer_ptr, + (std::mem::size_of::()) as u32, + lpoutbuffer_ptr, + (std::mem::size_of::()) as u32, + ptr::null_mut(), + ptr::null_mut(), + ) + } != 0 + { + let descriptor_id: u8 = match descriptor { + IoDescriptor::Manufacturer => lpoutbuffer.DeviceDescriptor.iManufacturer, + IoDescriptor::Product => lpoutbuffer.DeviceDescriptor.iProduct, + IoDescriptor::SerialNumber => lpoutbuffer.DeviceDescriptor.iSerialNumber, + }; + + let mut lang_id: u16 = unsafe { GetSystemDefaultLangID() }; + + loop { + const MAX_USB_STRING_LENGTH: u32 = 255u32; + + let mut lpinbuffer = UsbDescriptorRequest { + ConnectionIndex: port_number as ULONG, + ..Default::default() + }; + + lpinbuffer.SetupPacket.wValue = + (((USB_STRING_DESCRIPTOR_TYPE as u32) << 8) | descriptor_id as u32) as USHORT; + lpinbuffer.SetupPacket.wIndex = lang_id; + lpinbuffer.SetupPacket.wLength = MAX_USB_STRING_LENGTH as USHORT; + + let lpinbuffer_ptr = IoControl::get_mut_ptr(&mut lpinbuffer); + let ninbuffersize = (std::mem::size_of::()) as u32; + + let mut lpoutbuffer: [u16; (MAX_USB_STRING_LENGTH) as usize] = + [0; (MAX_USB_STRING_LENGTH) as usize]; + let lpoutbuffer_ptr = IoControl::get_mut_ptr(&mut lpoutbuffer); + let noutbuffersize = (lpoutbuffer.len() * std::mem::size_of::()) as u32; + + let result = unsafe { + // https://learn.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-deviceiocontrol + DeviceIoControl( + *hdevice, + IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION, + lpinbuffer_ptr, + ninbuffersize, + lpoutbuffer_ptr, + noutbuffersize, + ptr::null_mut(), + ptr::null_mut(), + ) + }; + + if result != 0 { + if lang_id == 0 { + // Get first language from descriptor + lang_id = lpoutbuffer[0]; + continue; + } + + let start = (std::mem::size_of::() as u32) + / std::mem::size_of::() as u32; + let b = String::from_utf16_lossy(&lpoutbuffer[(start + 1) as usize..]); // +1 for alignment + let b = b.trim_end_matches('\0'); + return Some(b.to_string()); + } else if lang_id != 0 && lang_id != 0x0409 { + // If there is not localized descriptor try the first one from the list of supported languages + lang_id = 0; + continue; + } else if lang_id == 0 { + lang_id = 0x0409; // Fallback to us-en + continue; + } else { + return None; + } + } + } else { + None + } + } + + pub fn get_handle(device_name: &mut String) -> Result { + device_name.insert_str(0, r"\\.\"); + let lpfilename = OsStr::new(device_name) + .encode_wide() + .chain(Some(0)) + .collect::>(); + let lpfilename_ptr: LPCWSTR = lpfilename.as_ptr(); + + let handle = unsafe { + // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew + CreateFileW( + lpfilename_ptr, + FILE_GENERIC_WRITE, + FILE_SHARE_WRITE, + ptr::null_mut(), + OPEN_EXISTING, + SECURITY_ANONYMOUS, + 0 as HANDLE, + ) + }; + + if handle == INVALID_HANDLE_VALUE { + Err(error::last_os_error()) + } else { + Ok(handle) + } + } + + fn get_mut_ptr(buf: &mut T) -> *mut c_void { + let ptr: *mut c_void = buf as *mut _ as *mut c_void; + ptr + } +}