diff --git a/enumerator/enumerator.go b/enumerator/enumerator.go index 2bd5043..d885cc7 100644 --- a/enumerator/enumerator.go +++ b/enumerator/enumerator.go @@ -11,11 +11,12 @@ package enumerator // PortDetails contains detailed information about USB serial port. // Use GetDetailedPortsList function to retrieve it. type PortDetails struct { - Name string - IsUSB bool - VID string - PID string - SerialNumber string + Name string + IsUSB bool + VID string + PID string + SerialNumber string + Configuration string // Manufacturer string diff --git a/enumerator/syscall_windows.go b/enumerator/syscall_windows.go index 2c6b379..9b02ed5 100644 --- a/enumerator/syscall_windows.go +++ b/enumerator/syscall_windows.go @@ -51,6 +51,7 @@ var ( procCM_Get_Device_ID_Size = modcfgmgr32.NewProc("CM_Get_Device_ID_Size") procCM_Get_Device_IDW = modcfgmgr32.NewProc("CM_Get_Device_IDW") procCM_MapCrToWin32Err = modcfgmgr32.NewProc("CM_MapCrToWin32Err") + procCM_Get_DevNode_Registry_PropertyW = modcfgmgr32.NewProc("CM_Get_DevNode_Registry_PropertyW") ) func setupDiClassGuidsFromNameInternal(class string, guid *guid, guidSize uint32, requiredSize *uint32) (err error) { @@ -165,3 +166,9 @@ func cmMapCrToWin32Err(cmErr cmError, defaultErr uint32) (err uint32) { err = uint32(r0) return } + +func cmGetDevNodeRegistryProperty(dev devInstance, property uint32, regDataType *uint32, buffer *byte, bufferLen *uint32, flags uint32) (cmErr cmError) { + r0, _, _ := syscall.Syscall6(procCM_Get_DevNode_Registry_PropertyW.Addr(), 6, uintptr(dev), uintptr(property), uintptr(unsafe.Pointer(regDataType)), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(bufferLen)), uintptr(flags)) + cmErr = cmError(r0) + return +} diff --git a/enumerator/usb_darwin.go b/enumerator/usb_darwin.go index 1f51c47..7d6f2b8 100644 --- a/enumerator/usb_darwin.go +++ b/enumerator/usb_darwin.go @@ -8,8 +8,139 @@ package enumerator // #cgo LDFLAGS: -framework CoreFoundation -framework IOKit // #include +// #include +// #include // #include // #include +// +// static char *copyUSBConfigurationString(io_service_t service) { +// IOCFPlugInInterface **plugin = NULL; +// IOUSBDeviceInterface **device = NULL; +// IOUSBConfigurationDescriptorPtr configDesc = NULL; +// SInt32 score = 0; +// HRESULT result = S_OK; +// kern_return_t kr; +// UInt8 currentConfig = 0; +// UInt8 numConfigs = 0; +// UInt8 stringIndex = 0; +// UInt8 index; +// UInt16 langID = 0x0409; +// IOUSBDevRequest request; +// UInt8 buffer[1024]; +// UInt16 descriptorLength; +// CFStringRef configuration = NULL; +// CFIndex outputSize; +// char *output = NULL; +// UInt8 isOpen = 0; +// +// kr = IOCreatePlugInInterfaceForService(service, +// kIOUSBDeviceUserClientTypeID, +// kIOCFPlugInInterfaceID, +// &plugin, +// &score); +// if (kr != kIOReturnSuccess || plugin == NULL) { +// goto cleanup; +// } +// +// result = (*plugin)->QueryInterface(plugin, +// CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID), +// (LPVOID *)&device); +// (*plugin)->Release(plugin); +// plugin = NULL; +// if (result != S_OK || device == NULL) { +// goto cleanup; +// } +// +// kr = (*device)->USBDeviceOpen(device); +// if (kr != kIOReturnSuccess) { +// goto cleanup; +// } +// isOpen = 1; +// +// kr = (*device)->GetConfiguration(device, ¤tConfig); +// if (kr != kIOReturnSuccess || currentConfig == 0) { +// goto cleanup; +// } +// +// kr = (*device)->GetNumberOfConfigurations(device, &numConfigs); +// if (kr != kIOReturnSuccess) { +// goto cleanup; +// } +// for (index = 0; index < numConfigs; index++) { +// kr = (*device)->GetConfigurationDescriptorPtr(device, index, &configDesc); +// if (kr == kIOReturnSuccess && configDesc != NULL && configDesc->bConfigurationValue == currentConfig) { +// stringIndex = configDesc->iConfiguration; +// break; +// } +// } +// if (stringIndex == 0) { +// goto cleanup; +// } +// +// request.bmRequestType = USBmakebmRequestType(kUSBIn, kUSBStandard, kUSBDevice); +// request.bRequest = kUSBRqGetDescriptor; +// request.wValue = (kUSBStringDesc << 8); +// request.wIndex = 0; +// request.wLength = sizeof(buffer); +// request.pData = buffer; +// kr = (*device)->DeviceRequest(device, &request); +// if (kr == kIOReturnSuccess && request.wLenDone >= 4) { +// langID = (UInt16)buffer[2] | ((UInt16)buffer[3] << 8); +// } +// +// request.bmRequestType = USBmakebmRequestType(kUSBIn, kUSBStandard, kUSBDevice); +// request.bRequest = kUSBRqGetDescriptor; +// request.wValue = ((UInt16)kUSBStringDesc << 8) | stringIndex; +// request.wIndex = langID; +// request.wLength = sizeof(buffer); +// request.pData = buffer; +// kr = (*device)->DeviceRequest(device, &request); +// if (kr != kIOReturnSuccess || request.wLenDone < 2) { +// goto cleanup; +// } +// +// descriptorLength = buffer[0]; +// if (descriptorLength > request.wLenDone) { +// descriptorLength = request.wLenDone; +// } +// if (descriptorLength <= 2) { +// goto cleanup; +// } +// +// configuration = CFStringCreateWithBytes(kCFAllocatorDefault, +// buffer + 2, +// descriptorLength - 2, +// kCFStringEncodingUTF16LE, +// false); +// if (configuration == NULL) { +// goto cleanup; +// } +// +// outputSize = CFStringGetMaximumSizeForEncoding(CFStringGetLength(configuration), kCFStringEncodingUTF8) + 1; +// output = (char *)malloc((size_t)outputSize); +// if (output == NULL) { +// goto cleanup; +// } +// if (!CFStringGetCString(configuration, output, outputSize, kCFStringEncodingUTF8)) { +// free(output); +// output = NULL; +// } +// +// cleanup: +// if (configuration != NULL) { +// CFRelease(configuration); +// } +// if (device != NULL && isOpen) { +// (*device)->USBDeviceClose(device); +// } +// if (device != NULL) { +// (*device)->Release(device); +// } +// if (plugin != NULL) { +// (*plugin)->Release(plugin); +// } +// return output; +// } import "C" import ( "errors" @@ -70,6 +201,7 @@ func extractPortInfo(service io_registry_entry_t) (*PortDetails, error) { vid, _ := usbDevice.GetIntProperty("idVendor", C.kCFNumberSInt16Type) pid, _ := usbDevice.GetIntProperty("idProduct", C.kCFNumberSInt16Type) serialNumber, _ := usbDevice.GetStringProperty("USB Serial Number") + configuration, _ := usbDevice.GetUSBConfigurationString() //product, _ := usbDevice.GetStringProperty("USB Product Name") //manufacturer, _ := usbDevice.GetStringProperty("USB Vendor Name") //fmt.Println(product + " - " + manufacturer) @@ -78,6 +210,7 @@ func extractPortInfo(service io_registry_entry_t) (*PortDetails, error) { port.VID = fmt.Sprintf("%04X", vid) port.PID = fmt.Sprintf("%04X", pid) port.SerialNumber = serialNumber + port.Configuration = configuration } return port, nil } @@ -197,6 +330,15 @@ func (me *io_registry_entry_t) GetStringProperty(key string) (string, error) { return C.GoString(&buff[0]), nil } +func (me *io_registry_entry_t) GetUSBConfigurationString() (string, error) { + configuration := C.copyUSBConfigurationString(C.io_service_t(*me)) + if configuration == nil { + return "", errors.New("USB configuration string not available") + } + defer C.free(unsafe.Pointer(configuration)) + return C.GoString(configuration), nil +} + func (me *io_registry_entry_t) GetIntProperty(key string, intType C.CFNumberType) (int, error) { property, err := me.CreateCFProperty(key) if err != nil { diff --git a/enumerator/usb_linux.go b/enumerator/usb_linux.go index 2e3737e..25118fd 100644 --- a/enumerator/usb_linux.go +++ b/enumerator/usb_linux.go @@ -76,6 +76,10 @@ func parseUSBSysFS(usbDevicePath string, details *PortDetails) error { if err != nil { return err } + + configuration, _ := readLine(filepath.Join(usbDevicePath, "configuration")) + // It's not an error if the configuration file is not present, so we ignore it. + //manufacturer, err := readLine(filepath.Join(usbDevicePath, "manufacturer")) //if err != nil { // return err @@ -89,6 +93,7 @@ func parseUSBSysFS(usbDevicePath string, details *PortDetails) error { details.VID = vid details.PID = pid details.SerialNumber = serial + details.Configuration = configuration //details.Manufacturer = manufacturer //details.Product = product return nil diff --git a/enumerator/usb_windows.go b/enumerator/usb_windows.go index 2d3793b..6ba9d8b 100644 --- a/enumerator/usb_windows.go +++ b/enumerator/usb_windows.go @@ -9,6 +9,7 @@ package enumerator import ( "fmt" "regexp" + "strings" "syscall" "unsafe" @@ -66,6 +67,7 @@ func parseDeviceID(deviceID string, details *PortDetails) { //sys cmGetDeviceIDSize(outLen *uint32, dev devInstance, flags uint32) (cmErr cmError) = cfgmgr32.CM_Get_Device_ID_Size //sys cmGetDeviceID(dev devInstance, buffer unsafe.Pointer, bufferSize uint32, flags uint32) (err cmError) = cfgmgr32.CM_Get_Device_IDW //sys cmMapCrToWin32Err(cmErr cmError, defaultErr uint32) (err uint32) = cfgmgr32.CM_MapCrToWin32Err +//sys cmGetDevNodeRegistryProperty(dev devInstance, property uint32, regDataType *uint32, buffer *byte, bufferLen *uint32, flags uint32) (cmErr cmError) = cfgmgr32.CM_Get_DevNode_Registry_PropertyW // Device registry property codes // (Codes marked as read-only (R) may only be used for @@ -355,5 +357,351 @@ func retrievePortDetailsFromDevInfo(device *deviceInfo, details *PortDetails) er } } + if details.IsUSB { + details.Configuration = retrieveConfigurationViaHubIOCTL(device) + } + return nil } + +// ---- USB hub IOCTL path for iConfiguration retrieval ---- +// +// Mirrors the approach used by USBView (Windows-driver-samples/usb/usbview/enum.c): +// 1. Enumerate all USB hub device interfaces. +// 2. For each hub, iterate its ports and match by driver key name. +// 3. On match, read the USB Configuration Descriptor to get iConfiguration index. +// 4. Fetch the String Descriptor at that index (language 0x0409, English). + +// GUID_DEVINTERFACE_USB_HUB = {f18a0e88-c30c-11d0-8815-00a0c906bed8} +var guidDevInterfaceUSBHub = windows.GUID{ + Data1: 0xf18a0e88, + Data2: 0xc30c, + Data3: 0x11d0, + Data4: [8]byte{0x88, 0x15, 0x00, 0xa0, 0xc9, 0x06, 0xbe, 0xd8}, +} + +const ( + // CTL_CODE(FILE_DEVICE_USB=0x22, fn, METHOD_BUFFERED=0, FILE_ANY_ACCESS=0) + ioctlUsbGetNodeInformation = 0x220404 // fn=0x101 + ioctlUsbGetNodeConnectionDriverkeyName = 0x220420 // fn=0x108 + ioctlUsbGetDescriptorFromNodeConnection = 0x220410 // fn=0x104 + + usbConfigurationDescriptorType = 0x02 + usbStringDescriptorType = 0x03 + maximumUsbStringLength = 255 + langIDEnglishUS = 0x0409 +) + +// usbHubInfoHeader overlaps the beginning of USB_NODE_INFORMATION for hubs +// to extract bNumberOfPorts. Layout (no padding): +// +// [0..3] NodeType (uint32) +// [4] bDescriptorLength (uint8) +// [5] bDescriptorType (uint8) +// [6] bNumberOfPorts (uint8) +type usbHubInfoHeader struct { + NodeType uint32 + DescriptorLength uint8 + DescriptorType uint8 + NumberOfPorts uint8 +} + +// usbNodeConnectionDriverkeyName corresponds to USB_NODE_CONNECTION_DRIVERKEY_NAME. +// The DriverKeyName array is variable length; we allocate extra bytes at runtime. +type usbNodeConnectionDriverkeyName struct { + ConnectionIndex uint32 + ActualLength uint32 + DriverKeyName [1]uint16 // variable; allocate more bytes as needed +} + +// usbDescriptorRequest corresponds to USB_DESCRIPTOR_REQUEST. +// SetupPacket fields follow ConnectionIndex directly (no padding in the C struct). +type usbDescriptorRequest struct { + ConnectionIndex uint32 + BmRequest uint8 + BRequest uint8 + WValue uint16 + WIndex uint16 + WLength uint16 +} + +// usbConfigurationDescriptor is the 9-byte USB Configuration Descriptor header. +type usbConfigurationDescriptor struct { + BLength uint8 + BDescriptorType uint8 + WTotalLength uint16 + BNumInterfaces uint8 + BConfigurationValue uint8 + IConfiguration uint8 + BmAttributes uint8 + MaxPower uint8 +} + +// usbStringDescriptorHeader is the 2-byte prefix of a USB String Descriptor. +type usbStringDescriptorHeader struct { + BLength uint8 + BDescriptorType uint8 +} + +// cmDrpDriver is the CM_DRP_DRIVER property code (1-based, unlike SPDRP which is 0-based). +const cmDrpDriver = 0xA + +// devInstanceGetDriverKey retrieves the SPDRP_DRIVER equivalent for a raw devInstance +// using CM_Get_DevNode_Registry_PropertyW. +func devInstanceGetDriverKey(inst devInstance) string { + var size uint32 + cmGetDevNodeRegistryProperty(inst, cmDrpDriver, nil, nil, &size, 0) + if size == 0 { + return "" + } + buf := make([]byte, size) + if err := cmConvertError(cmGetDevNodeRegistryProperty(inst, cmDrpDriver, nil, &buf[0], &size, 0)); err != nil { + return "" + } + nChars := size / 2 + if nChars == 0 { + return "" + } + return windows.UTF16ToString(unsafe.Slice((*uint16)(unsafe.Pointer(&buf[0])), nChars)) +} + +// findUSBPortDriverKey walks up the devnode tree from inst to find the device +// directly attached to a USB hub port (instance ID starts with "USB\") and +// returns its driver key. This is the key that IOCTL_USB_GET_NODE_CONNECTION_DRIVERKEY_NAME +// returns. For single-function USB serial devices the COM port IS that device; +// for composite USB devices the COM port is a child and we need the parent. +func findUSBPortDriverKey(inst devInstance) string { + for i := 0; i < 5; i++ { + id, err := inst.GetDeviceID() + if err != nil { + return "" + } + uid := strings.ToUpper(id) + // Skip composite device interfaces (e.g. USB\VID_...&MI_00\...). + // The device directly on the hub port has no &MI_ in its instance ID. + if strings.HasPrefix(uid, "USB\\") && !strings.Contains(uid, "&MI_") { + if dk := devInstanceGetDriverKey(inst); dk != "" { + return dk + } + } + parent, err := inst.getParent() + if err != nil { + return "" + } + inst = parent + } + return "" +} + +// retrieveConfigurationViaHubIOCTL looks up the USB configuration name string +// for device by matching its driver key against hub port driver keys, then +// reading the configuration descriptor and string descriptor via hub IOCTLs. +func retrieveConfigurationViaHubIOCTL(device *deviceInfo) string { + // Find the driver key of the USB device directly attached to the hub port. + // For composite USB devices the COM port is a child; we need the parent's key. + targetDriverKey := findUSBPortDriverKey(device.data.devInst) + if targetDriverKey == "" { + return "" + } + + // Enumerate all USB hub device interface paths. + // Passing "" as deviceID asks for all interfaces of this class. + hubPaths, err := windows.CM_Get_Device_Interface_List("", &guidDevInterfaceUSBHub, windows.CM_GET_DEVICE_INTERFACE_LIST_PRESENT) + if err != nil { + return "" + } + + for _, hubPath := range hubPaths { + if conf := retrieveConfigFromHub(hubPath, targetDriverKey); conf != "" { + return conf + } + } + return "" +} + +// retrieveConfigFromHub opens a single hub and scans its ports for targetDriverKey. +func retrieveConfigFromHub(hubPath, targetDriverKey string) string { + hubPathPtr, err := syscall.UTF16PtrFromString(hubPath) + if err != nil { + return "" + } + hHub, err := windows.CreateFile( + hubPathPtr, + windows.GENERIC_READ|windows.GENERIC_WRITE, + windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE, + nil, + windows.OPEN_EXISTING, + 0, + 0, + ) + if err != nil { + return "" + } + defer windows.CloseHandle(hHub) + + // Ask hub for its number of ports via IOCTL_USB_GET_NODE_INFORMATION. + var hubInfo usbHubInfoHeader + var bytesReturned uint32 + err = windows.DeviceIoControl( + hHub, + ioctlUsbGetNodeInformation, + (*byte)(unsafe.Pointer(&hubInfo)), + uint32(unsafe.Sizeof(hubInfo)), + (*byte)(unsafe.Pointer(&hubInfo)), + uint32(unsafe.Sizeof(hubInfo)), + &bytesReturned, + nil, + ) + numPorts := uint32(hubInfo.NumberOfPorts) + if err != nil || numPorts == 0 { + // Fall back to scanning a reasonable maximum. + numPorts = 16 + } + + for portIndex := uint32(1); portIndex <= numPorts; portIndex++ { + driverKey, err := hubPortDriverKey(hHub, portIndex) + if err != nil { + continue + } + if strings.EqualFold(driverKey, targetDriverKey) { + iConfIdx, err := hubConfigDescriptorIConfiguration(hHub, portIndex) + if err != nil || iConfIdx == 0 { + return "" + } + return hubStringDescriptor(hHub, portIndex, iConfIdx, langIDEnglishUS) + } + } + return "" +} + +// hubPortDriverKey retrieves the driver key name of the device at portIndex on hHub +// using IOCTL_USB_GET_NODE_CONNECTION_DRIVERKEY_NAME (mirrors GetDriverKeyName in enum.c). +func hubPortDriverKey(hHub windows.Handle, portIndex uint32) (string, error) { + // First call: get the required ActualLength. + var req usbNodeConnectionDriverkeyName + req.ConnectionIndex = portIndex + var nBytes uint32 + _ = windows.DeviceIoControl( + hHub, + ioctlUsbGetNodeConnectionDriverkeyName, + (*byte)(unsafe.Pointer(&req)), + uint32(unsafe.Sizeof(req)), + (*byte)(unsafe.Pointer(&req)), + uint32(unsafe.Sizeof(req)), + &nBytes, + nil, + ) + if req.ActualLength <= uint32(unsafe.Sizeof(req)) { + return "", fmt.Errorf("no driver key") + } + + // Second call: allocate a buffer large enough for the full name. + buf := make([]byte, req.ActualLength) + reqFull := (*usbNodeConnectionDriverkeyName)(unsafe.Pointer(&buf[0])) + reqFull.ConnectionIndex = portIndex + err := windows.DeviceIoControl( + hHub, + ioctlUsbGetNodeConnectionDriverkeyName, + &buf[0], + uint32(len(buf)), + &buf[0], + uint32(len(buf)), + &nBytes, + nil, + ) + if err != nil { + return "", err + } + + // DriverKeyName starts at offset 8 (after ConnectionIndex + ActualLength). + nameStart := 8 + if len(buf) <= nameStart+1 { + return "", fmt.Errorf("buffer too small") + } + nameSlice := unsafe.Slice((*uint16)(unsafe.Pointer(&buf[nameStart])), (len(buf)-nameStart)/2) + return windows.UTF16ToString(nameSlice), nil +} + +// hubConfigDescriptorIConfiguration retrieves the iConfiguration index byte from the +// USB Configuration Descriptor for the device at portIndex (mirrors GetConfigDescriptor). +func hubConfigDescriptorIConfiguration(hHub windows.Handle, portIndex uint32) (uint8, error) { + // First pass: use a fixed-size buffer for the config descriptor header. + headerSize := uint32(unsafe.Sizeof(usbDescriptorRequest{}) + unsafe.Sizeof(usbConfigurationDescriptor{})) + buf := make([]byte, headerSize) + + req := (*usbDescriptorRequest)(unsafe.Pointer(&buf[0])) + req.ConnectionIndex = portIndex + req.WValue = usbConfigurationDescriptorType << 8 // descriptor type in high byte, index 0 in low byte + req.WLength = uint16(unsafe.Sizeof(usbConfigurationDescriptor{})) + + var nBytes uint32 + err := windows.DeviceIoControl( + hHub, + ioctlUsbGetDescriptorFromNodeConnection, + &buf[0], + headerSize, + &buf[0], + headerSize, + &nBytes, + nil, + ) + if err != nil || nBytes < headerSize { + return 0, fmt.Errorf("config descriptor IOCTL failed: %w", err) + } + + reqSize := uint32(unsafe.Sizeof(usbDescriptorRequest{})) + confDesc := (*usbConfigurationDescriptor)(unsafe.Pointer(&buf[reqSize])) + if confDesc.BDescriptorType != usbConfigurationDescriptorType { + return 0, fmt.Errorf("unexpected descriptor type %d", confDesc.BDescriptorType) + } + return confDesc.IConfiguration, nil +} + +// hubStringDescriptor fetches the USB String Descriptor at descriptorIndex / languageID +// and returns the decoded UTF-16 string (mirrors GetStringDescriptor in enum.c). +func hubStringDescriptor(hHub windows.Handle, portIndex uint32, descriptorIndex uint8, languageID uint16) string { + reqHeaderSize := uint32(unsafe.Sizeof(usbDescriptorRequest{})) + totalSize := reqHeaderSize + uint32(maximumUsbStringLength) + 2 // +2 for safety + buf := make([]byte, totalSize) + + req := (*usbDescriptorRequest)(unsafe.Pointer(&buf[0])) + req.ConnectionIndex = portIndex + req.WValue = (usbStringDescriptorType << 8) | uint16(descriptorIndex) + req.WIndex = languageID + req.WLength = uint16(maximumUsbStringLength) + + var nBytes uint32 + err := windows.DeviceIoControl( + hHub, + ioctlUsbGetDescriptorFromNodeConnection, + &buf[0], + totalSize, + &buf[0], + totalSize, + &nBytes, + nil, + ) + if err != nil || nBytes < reqHeaderSize+2 { + return "" + } + + strDesc := (*usbStringDescriptorHeader)(unsafe.Pointer(&buf[reqHeaderSize])) + if strDesc.BDescriptorType != usbStringDescriptorType { + return "" + } + strLen := uint32(strDesc.BLength) + if strLen < 2 || strLen%2 != 0 { + return "" + } + // The string data (UTF-16LE) starts 2 bytes after the header. + strDataOffset := reqHeaderSize + 2 + if uint32(nBytes) < strDataOffset+strLen-2 { + return "" + } + numChars := (strLen - 2) / 2 + if numChars == 0 { + return "" + } + chars := unsafe.Slice((*uint16)(unsafe.Pointer(&buf[strDataOffset])), numChars) + return windows.UTF16ToString(chars) +}