use std::{io, time::Duration};
use crate::{mboot::ResultComm, parsers};
use color_print::cstr;
use hidapi::{DeviceInfo, HidApi, HidDevice};
use log::{debug, info};
use std::fmt::Debug;
use super::{CommunicationError, Protocol, ProtocolOpen};
mod report {
pub const CMD_OUT: u8 = 0x01;
pub const DATA_OUT: u8 = 0x02;
pub const CMD_IN: u8 = 0x03;
pub const DATA_IN: u8 = 0x04;
}
const MAX_PACKET_SIZE: usize = 1024;
pub const NXP_USB_VIDS: [u16; 4] = [0x1FC9, 0x15A2, 0x0471, 0x0D28];
fn is_nxp_vid(vid: u16) -> bool {
NXP_USB_VIDS.contains(&vid)
}
#[derive(Debug)]
pub struct USBProtocol {
interface: String,
device: HidDevice,
timeout_ms: i32,
polling_interval: Duration,
}
#[derive(Clone, Debug)]
pub struct UsbDeviceInfo {
pub vid: u16,
pub pid: u16,
pub path: String,
pub topology_path: Option<String>,
pub serial_number: Option<String>,
pub product_string: Option<String>,
pub manufacturer_string: Option<String>,
}
impl std::fmt::Display for UsbDeviceInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"0x{:04X}:0x{:04X} path={} topology={} serial={} product={}",
self.vid,
self.pid,
self.path,
self.topology_path.as_deref().unwrap_or("N/A"),
self.serial_number.as_deref().unwrap_or("N/A"),
self.product_string.as_deref().unwrap_or("N/A"),
)
}
}
impl From<&DeviceInfo> for UsbDeviceInfo {
fn from(info: &DeviceInfo) -> Self {
let path = info.path().to_string_lossy().into_owned();
UsbDeviceInfo {
vid: info.vendor_id(),
pid: info.product_id(),
topology_path: usb_topology_path_from_hid_path(&path),
path,
serial_number: info.serial_number().map(str::to_owned),
product_string: info.product_string().map(str::to_owned),
manufacturer_string: info.manufacturer_string().map(str::to_owned),
}
}
}
impl UsbDeviceInfo {
#[must_use]
pub fn usb_arg(&self) -> String {
format!("0x{:04X},0x{:04X},{}", self.vid, self.pid, self.path)
}
#[must_use]
pub fn usb_topology_arg(&self) -> Option<String> {
self.topology_path
.as_deref()
.map(|topology| format!("0x{:04X},0x{:04X},{}", self.vid, self.pid, topology))
}
}
#[cfg(target_os = "linux")]
fn usb_topology_path_from_hid_path(hid_path: &str) -> Option<String> {
let hidraw_name = hid_path.strip_prefix("/dev/")?;
if !hidraw_name.starts_with("hidraw") {
return None;
}
let device_link = std::path::Path::new("/sys/class/hidraw")
.join(hidraw_name)
.join("device");
let resolved = std::fs::canonicalize(device_link).ok()?;
resolved
.ancestors()
.filter_map(|p| p.file_name())
.filter_map(|name| name.to_str())
.find(|name| is_usb_topology_name(name))
.map(str::to_owned)
}
#[cfg(target_os = "macos")]
fn usb_topology_path_from_hid_path(hid_path: &str) -> Option<String> {
let entry_id = hid_path.strip_prefix("DevSrvsID:")?.parse::<u64>().ok()?;
topology_path_from_location_id(macos::usb_location_id(entry_id)?)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn usb_topology_path_from_hid_path(_hid_path: &str) -> Option<String> {
None
}
#[cfg(any(target_os = "macos", test))]
fn topology_path_from_location_id(location_id: u32) -> Option<String> {
let ports: Vec<String> = (0..6)
.map(|level| (location_id >> (20 - 4 * level)) & 0xF)
.take_while(|&port| port != 0)
.map(|port| port.to_string())
.collect();
if ports.is_empty() {
return None;
}
Some(format!("{}-{}", location_id >> 24, ports.join(".")))
}
#[cfg(target_os = "macos")]
mod macos {
use std::ffi::{c_char, c_void};
type CFTypeRef = *const c_void;
type IoObject = u32;
const K_CF_STRING_ENCODING_UTF8: u32 = 0x0800_0100;
const K_CF_NUMBER_SINT64_TYPE: isize = 4;
const K_IO_REGISTRY_ITERATE_RECURSIVELY: u32 = 1;
const K_IO_REGISTRY_ITERATE_PARENTS: u32 = 2;
#[link(name = "IOKit", kind = "framework")]
unsafe extern "C" {
fn IORegistryEntryIDMatching(entry_id: u64) -> *mut c_void;
fn IOServiceGetMatchingService(main_port: u32, matching: *mut c_void) -> IoObject;
fn IORegistryEntrySearchCFProperty(
entry: IoObject,
plane: *const c_char,
key: CFTypeRef,
allocator: CFTypeRef,
options: u32,
) -> CFTypeRef;
fn IOObjectRelease(object: IoObject) -> i32;
}
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
fn CFStringCreateWithCString(allocator: CFTypeRef, c_str: *const c_char, encoding: u32) -> CFTypeRef;
fn CFGetTypeID(cf: CFTypeRef) -> usize;
fn CFNumberGetTypeID() -> usize;
fn CFNumberGetValue(number: CFTypeRef, number_type: isize, value: *mut c_void) -> u8;
fn CFRelease(cf: CFTypeRef);
}
pub(super) fn usb_location_id(entry_id: u64) -> Option<u32> {
unsafe {
let matching = IORegistryEntryIDMatching(entry_id);
if matching.is_null() {
return None;
}
let service = IOServiceGetMatchingService(0, matching);
if service == 0 {
return None;
}
let key = CFStringCreateWithCString(std::ptr::null(), c"locationID".as_ptr(), K_CF_STRING_ENCODING_UTF8);
let value = if key.is_null() {
std::ptr::null()
} else {
let value = IORegistryEntrySearchCFProperty(
service,
c"IOService".as_ptr(),
key,
std::ptr::null(),
K_IO_REGISTRY_ITERATE_RECURSIVELY | K_IO_REGISTRY_ITERATE_PARENTS,
);
CFRelease(key);
value
};
IOObjectRelease(service);
if value.is_null() {
return None;
}
let mut location_id: i64 = 0;
let ok = CFGetTypeID(value) == CFNumberGetTypeID()
&& CFNumberGetValue(value, K_CF_NUMBER_SINT64_TYPE, (&raw mut location_id).cast()) != 0;
CFRelease(value);
if ok {
u32::try_from(location_id & 0xFFFF_FFFF).ok()
} else {
None
}
}
}
}
#[cfg(target_os = "linux")]
fn is_usb_topology_name(name: &str) -> bool {
let Some((bus, ports)) = name.split_once('-') else {
return false;
};
if bus.is_empty() || !bus.chars().all(|c| c.is_ascii_digit()) {
return false;
}
!ports.is_empty()
&& ports
.split('.')
.all(|port| !port.is_empty() && port.chars().all(|c| c.is_ascii_digit()))
}
#[derive(Debug, PartialEq, Eq)]
enum UsbSelector {
VidPid { vid: u16, pid: u16 },
VidPidWithSelector { vid: u16, pid: u16, selector: String },
PathOrSerial { selector: String },
}
impl ProtocolOpen for USBProtocol {
fn open(identifier: &str) -> ResultComm<Self> {
Self::open_with_options(identifier, 0, Duration::from_secs(5), Duration::from_millis(1))
}
fn open_with_options(
identifier: &str,
_baudrate: u32, timeout: Duration,
polling_interval: Duration,
) -> ResultComm<Self> {
let selector = parse_usb_identifier(identifier)?;
let api =
HidApi::new().map_err(|e| CommunicationError::ParseError(format!("Failed to initialize HID API: {e}")))?;
let device = open_selected_device(&api, &selector)?;
let timeout_ms = timeout.as_millis().try_into().unwrap_or(i32::MAX);
let usb_protocol = USBProtocol {
interface: identifier.to_owned(),
device,
timeout_ms,
polling_interval,
};
info!(
"Opened USB-HID device {} with {}ms timeout",
usb_protocol.interface,
timeout.as_millis()
);
Ok(usb_protocol)
}
}
impl Protocol for USBProtocol {
fn get_polling_interval(&self) -> Duration {
self.polling_interval
}
fn get_timeout(&self) -> Duration {
Duration::from_millis(self.timeout_ms.try_into().expect("negative timeout in USB"))
}
fn get_identifier(&self) -> &str {
&self.interface
}
fn read(&mut self, bytes: usize) -> ResultComm<Vec<u8>> {
let mut buf = vec![0u8; bytes];
self.read_usb(&mut buf)?;
Ok(buf)
}
fn write_packet_raw(&mut self, data: &[u8]) -> ResultComm<()> {
if data.len() < 6 || data[0] != 0x5A {
return Err(CommunicationError::InvalidHeader);
}
let cmd_type = data[1];
let data_len = u16::from_le_bytes([data[2], data[3]]) as usize;
if data.len() < 6 + data_len {
return Err(CommunicationError::InvalidData);
}
let cmd_data = &data[6..6 + data_len];
let report_id = match cmd_type {
0xA4 => report::CMD_OUT, 0xA5 => report::DATA_OUT, _ => return Err(CommunicationError::InvalidHeader),
};
let mut report = vec![0u8; 4 + cmd_data.len()];
report[0] = report_id;
report[1] = 0x00; report[2] = (cmd_data.len() & 0xFF) as u8;
report[3] = ((cmd_data.len() >> 8) & 0xFF) as u8;
report[4..4 + cmd_data.len()].copy_from_slice(cmd_data);
self.write_usb(&report)?;
Ok(())
}
fn read_packet_raw(&mut self, _: u8) -> ResultComm<Vec<u8>> {
let mut report = vec![0u8; MAX_PACKET_SIZE];
let size = self
.device
.read_timeout(&mut report, self.timeout_ms)
.map_err(|e| CommunicationError::IOError(io::Error::other(e.to_string())))?;
debug!("{}: Read {} bytes: {:02X?}", cstr!("<r!>RX"), size, &report[..size]);
if size < 4 {
return Err(CommunicationError::InvalidHeader);
}
let report_id = report[0];
let packet_length = u16::from_le_bytes([report[2], report[3]]) as usize;
if packet_length == 0 {
return Err(CommunicationError::Aborted);
}
if report_id == report::CMD_IN {
let mut response = Vec::new();
response.extend_from_slice(&report[4..4 + packet_length]);
debug!("Constructed response: {response:02X?}");
return Ok(response);
} else if report_id == report::DATA_IN {
if size >= 4 + packet_length {
return Ok(report[4..4 + packet_length].to_vec());
}
}
if size > 4 {
Ok(report[4..size].to_vec())
} else {
Ok(Vec::new())
}
}
}
impl USBProtocol {
pub fn list_devices(all: bool) -> ResultComm<Vec<UsbDeviceInfo>> {
let api =
HidApi::new().map_err(|e| CommunicationError::ParseError(format!("Failed to initialize HID API: {e}")))?;
Ok(api
.device_list()
.filter(|info| all || is_nxp_vid(info.vendor_id()))
.map(UsbDeviceInfo::from)
.collect())
}
fn read_usb(&mut self, buf: &mut [u8]) -> Result<(), io::Error> {
match self.device.read(buf) {
Ok(size) => {
debug!("{}: Read {} bytes: {:02X?}", cstr!("<r!>RX"), size, &buf[..size]);
Ok(())
}
Err(e) => Err(io::Error::other(e.to_string())),
}
}
fn write_usb(&self, buf: &[u8]) -> Result<(), io::Error> {
debug!("{}: {:02X?}", cstr!("<g!>TX"), buf);
match self.device.write(buf) {
Ok(written) => {
#[cfg(target_os = "windows")]
{
if written > 0 {
Ok(())
} else {
Err(io::Error::other("Failed to write to USB device"))
}
}
#[cfg(not(target_os = "windows"))]
{
if written == buf.len() {
Ok(())
} else {
Err(io::Error::other(format!(
"Failed to write all bytes: wrote {} of {}",
written,
buf.len()
)))
}
}
}
Err(e) => Err(io::Error::other(e.to_string())),
}
}
}
fn device_matches_selector(info: &DeviceInfo, selector: &str, selector_lower: &str, exact_path: bool) -> bool {
let path = info.path().to_string_lossy();
let path_matches = if exact_path {
path.eq_ignore_ascii_case(selector)
} else {
path.to_lowercase().contains(selector_lower)
};
path_matches
|| info
.serial_number()
.is_some_and(|serial| serial.eq_ignore_ascii_case(selector))
|| usb_topology_path_from_hid_path(&path).is_some_and(|topology| topology.eq_ignore_ascii_case(selector))
}
fn open_selected_device(api: &HidApi, selector: &UsbSelector) -> ResultComm<HidDevice> {
match *selector {
UsbSelector::VidPid { vid, pid } => {
let matched = api
.device_list()
.find(|info| info.vendor_id() == vid && (pid == 0 || info.product_id() == pid));
match matched {
Some(info) => info
.open_device(api)
.map_err(|e| CommunicationError::ParseError(format!("Failed to open USB device: {e}"))),
None => Err(CommunicationError::ParseError(format!(
"No USB-HID device matching vid=0x{vid:04X}{} found. Use the 'list-usb' command to see all \
connected devices.",
if pid == 0 {
String::new()
} else {
format!(" pid=0x{pid:04X}")
}
))),
}
}
UsbSelector::VidPidWithSelector {
vid,
pid,
selector: ref selector_str,
} => {
let selector_lower = selector_str.to_lowercase();
let candidates: Vec<&DeviceInfo> = api
.device_list()
.filter(|info| info.vendor_id() == vid && (pid == 0 || info.product_id() == pid))
.collect();
open_unique_match(api, &candidates, selector_str, &selector_lower, || {
format!("vid=0x{vid:04X} pid=0x{pid:04X} and selector '{selector_str}'")
})
}
UsbSelector::PathOrSerial {
selector: ref selector_str,
} => {
let selector_lower = selector_str.to_lowercase();
let candidates: Vec<&DeviceInfo> = api.device_list().collect();
open_unique_match(api, &candidates, selector_str, &selector_lower, || {
format!("path/serial/topology '{selector_str}'")
})
}
}
}
fn open_unique_match(
api: &HidApi,
candidates: &[&DeviceInfo],
selector_str: &str,
selector_lower: &str,
describe_target: impl FnOnce() -> String,
) -> ResultComm<HidDevice> {
let find_matches = |exact_path| -> Vec<&&DeviceInfo> {
candidates
.iter()
.filter(|info| device_matches_selector(info, selector_str, selector_lower, exact_path))
.collect()
};
let mut matches = find_matches(true);
if matches.is_empty() {
matches = find_matches(false);
}
match matches.as_slice() {
[info] => info
.open_device(api)
.map_err(|e| CommunicationError::ParseError(format!("Failed to open USB device: {e}"))),
[] => {
let available: Vec<String> = candidates
.iter()
.map(|info| UsbDeviceInfo::from(*info).to_string())
.collect();
Err(CommunicationError::ParseError(format!(
"No USB-HID device matching {}. Use the 'list-usb' command to see all connected devices.{}",
describe_target(),
if available.is_empty() {
String::new()
} else {
format!(" Other connected devices: [{}]", available.join(", "))
}
)))
}
_ => {
let ambiguous: Vec<String> = matches
.iter()
.map(|info| UsbDeviceInfo::from(**info).to_string())
.collect();
Err(CommunicationError::ParseError(format!(
"Selector '{selector_str}' matches {} devices, refusing to guess which one to open: [{}]. Use a \
more specific selector (e.g. the full OS path, serial number, or USB topology path from \
'list-usb').",
matches.len(),
ambiguous.join(", ")
)))
}
}
}
fn parse_usb_identifier(identifier: &str) -> ResultComm<UsbSelector> {
let parts: Vec<&str> = identifier.splitn(3, [':', ',']).collect();
let vid_str = parts[0];
if vid_str.is_empty() {
return Err(CommunicationError::ParseError(format!(
"Invalid USB identifier: {identifier} (empty)"
)));
}
let Ok(vid) = parse_number_string(vid_str) else {
return Ok(UsbSelector::PathOrSerial {
selector: identifier.to_owned(),
});
};
match parts.as_slice() {
[_] => {
Ok(UsbSelector::VidPid { vid, pid: 0 })
}
[_, pid_str] => {
let pid = parse_number_string(pid_str)
.map_err(|_| CommunicationError::ParseError(format!("Invalid PID: {pid_str}")))?;
Ok(UsbSelector::VidPid { vid, pid })
}
[_, pid_str, selector] => {
let pid = parse_number_string(pid_str)
.map_err(|_| CommunicationError::ParseError(format!("Invalid PID: {pid_str}")))?;
if selector.is_empty() {
return Err(CommunicationError::ParseError(format!(
"Invalid USB identifier: {identifier} (selector part is empty)"
)));
}
Ok(UsbSelector::VidPidWithSelector {
vid,
pid,
selector: (*selector).to_owned(),
})
}
_ => Err(CommunicationError::ParseError(format!(
"Invalid USB identifier: {identifier}"
))),
}
}
fn parse_number_string(s: &str) -> Result<u16, String> {
let trimmed = s.trim();
let lowercased_prefix = trimmed.strip_prefix("0X").map(|rest| format!("0x{rest}"));
let normalized = lowercased_prefix.as_deref().unwrap_or(trimmed);
parsers::parse_number::<u16>(normalized).or_else(|_| {
u16::from_str_radix(trimmed, 16).map_err(|_| format!("number '{trimmed}' is invalid or too large!"))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_vid_only() {
let selector = parse_usb_identifier("0x1FC9").unwrap();
assert_eq!(selector, UsbSelector::VidPid { vid: 0x1FC9, pid: 0 });
}
#[test]
fn test_parse_vid_uppercase_prefix() {
let selector = parse_usb_identifier("0X1FC9").unwrap();
assert_eq!(selector, UsbSelector::VidPid { vid: 0x1FC9, pid: 0 });
}
#[test]
fn test_parse_vid_hex_without_prefix() {
let selector = parse_usb_identifier("1FC9").unwrap();
assert_eq!(selector, UsbSelector::VidPid { vid: 0x1FC9, pid: 0 });
}
#[test]
fn test_parse_vid_pid_comma() {
let selector = parse_usb_identifier("0x1FC9,0x0135").unwrap();
assert_eq!(
selector,
UsbSelector::VidPid {
vid: 0x1FC9,
pid: 0x0135
}
);
}
#[test]
fn test_parse_vid_pid_colon() {
let selector = parse_usb_identifier("0x1FC9:0x0135").unwrap();
assert_eq!(
selector,
UsbSelector::VidPid {
vid: 0x1FC9,
pid: 0x0135
}
);
}
#[test]
fn test_parse_vid_pid_decimal() {
let selector = parse_usb_identifier("8137,309").unwrap();
assert_eq!(selector, UsbSelector::VidPid { vid: 8137, pid: 309 });
}
#[test]
fn test_parse_vid_pid_with_path_selector() {
let selector = parse_usb_identifier("0x1FC9,0x0135,HID\\VID_1FC9&PID_0135\\6&7909BF&0&0000").unwrap();
assert_eq!(
selector,
UsbSelector::VidPidWithSelector {
vid: 0x1FC9,
pid: 0x0135,
selector: "HID\\VID_1FC9&PID_0135\\6&7909BF&0&0000".to_owned()
}
);
}
#[test]
fn test_parse_vid_pid_with_serial_selector() {
let selector = parse_usb_identifier("0x1FC9,0x0135,ABCD1234").unwrap();
assert_eq!(
selector,
UsbSelector::VidPidWithSelector {
vid: 0x1FC9,
pid: 0x0135,
selector: "ABCD1234".to_owned()
}
);
}
#[test]
fn test_parse_vid_pid_with_topology_selector() {
let selector = parse_usb_identifier("0x1FC9,0x0135,1-1.4.3").unwrap();
assert_eq!(
selector,
UsbSelector::VidPidWithSelector {
vid: 0x1FC9,
pid: 0x0135,
selector: "1-1.4.3".to_owned()
}
);
}
#[test]
fn test_parse_empty_selector_errors() {
let result = parse_usb_identifier("0x1FC9,0x0135,");
assert!(result.is_err());
}
#[test]
fn test_parse_single_token_non_number_is_path_or_serial() {
let selector = parse_usb_identifier("not_a_number").unwrap();
assert_eq!(
selector,
UsbSelector::PathOrSerial {
selector: "not_a_number".to_owned()
}
);
}
#[test]
fn test_parse_windows_path_as_path_or_serial() {
let path = r"\\?\HID#VID_1FC9&PID_0021#9&1fee2255&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}";
let selector = parse_usb_identifier(path).unwrap();
assert_eq!(
selector,
UsbSelector::PathOrSerial {
selector: path.to_owned()
}
);
}
#[test]
fn test_parse_serial_as_path_or_serial() {
let selector = parse_usb_identifier("ABCD1234").unwrap();
assert_eq!(
selector,
UsbSelector::PathOrSerial {
selector: "ABCD1234".to_owned()
}
);
}
#[test]
fn test_parse_bare_topology_path_as_path_or_serial() {
let selector = parse_usb_identifier("1-1.4.3").unwrap();
assert_eq!(
selector,
UsbSelector::PathOrSerial {
selector: "1-1.4.3".to_owned()
}
);
}
#[test]
fn test_parse_macos_path_with_colon_as_path_or_serial() {
let path = "DevSrvsID:4294969627";
let selector = parse_usb_identifier(path).unwrap();
assert_eq!(
selector,
UsbSelector::PathOrSerial {
selector: path.to_owned()
}
);
}
#[test]
fn test_parse_empty_identifier_errors() {
let result = parse_usb_identifier("");
assert!(result.is_err());
}
#[test]
fn test_nxp_vid_filter_accepts_known_nxp_vids() {
for vid in NXP_USB_VIDS {
assert!(is_nxp_vid(vid));
}
}
#[test]
fn test_nxp_vid_filter_rejects_unknown_vendor() {
for vid in [0x413Cu16, 0x046Du16, 0x8087u16] {
assert!(!is_nxp_vid(vid));
}
}
#[test]
fn test_usb_arg_round_trips_through_parse_usb_identifier() {
let device = UsbDeviceInfo {
vid: 0x1FC9,
pid: 0x0021,
path: r"\\?\HID#VID_1FC9&PID_0021#9&1fee2255&0&0000#{...}".to_owned(),
topology_path: None,
serial_number: None,
product_string: Some("USB COMPOSITE DEVICE".to_owned()),
manufacturer_string: None,
};
let arg = device.usb_arg();
assert_eq!(arg, r"0x1FC9,0x0021,\\?\HID#VID_1FC9&PID_0021#9&1fee2255&0&0000#{...}");
let selector = parse_usb_identifier(&arg).unwrap();
assert_eq!(
selector,
UsbSelector::VidPidWithSelector {
vid: 0x1FC9,
pid: 0x0021,
selector: device.path,
}
);
}
#[test]
fn test_usb_topology_arg_none_without_topology_path() {
let device = UsbDeviceInfo {
vid: 0x1FC9,
pid: 0x0021,
path: "/dev/hidraw3".to_owned(),
topology_path: None,
serial_number: None,
product_string: None,
manufacturer_string: None,
};
assert_eq!(device.usb_topology_arg(), None);
}
#[test]
fn test_usb_topology_arg_formats_like_usb_arg() {
let device = UsbDeviceInfo {
vid: 0x1FC9,
pid: 0x0021,
path: "/dev/hidraw3".to_owned(),
topology_path: Some("1-1.4.3".to_owned()),
serial_number: None,
product_string: None,
manufacturer_string: None,
};
let arg = device.usb_topology_arg().unwrap();
assert_eq!(arg, "0x1FC9,0x0021,1-1.4.3");
let selector = parse_usb_identifier(&arg).unwrap();
assert_eq!(
selector,
UsbSelector::VidPidWithSelector {
vid: 0x1FC9,
pid: 0x0021,
selector: "1-1.4.3".to_owned(),
}
);
}
#[test]
fn test_topology_path_from_location_id() {
assert_eq!(topology_path_from_location_id(0x1432_0000).as_deref(), Some("20-3.2"));
assert_eq!(topology_path_from_location_id(0x0110_0000).as_deref(), Some("1-1"));
assert_eq!(
topology_path_from_location_id(0x8012_3456).as_deref(),
Some("128-1.2.3.4.5.6")
);
assert_eq!(topology_path_from_location_id(0x02FA_0000).as_deref(), Some("2-15.10"));
assert_eq!(topology_path_from_location_id(0x0130_5000).as_deref(), Some("1-3"));
assert_eq!(topology_path_from_location_id(0), None);
assert_eq!(topology_path_from_location_id(0x8000_0000), None);
}
#[test]
fn test_parse_bare_macos_topology_path_as_path_or_serial() {
assert_eq!(
parse_usb_identifier("20-3.2").unwrap(),
UsbSelector::PathOrSerial {
selector: "20-3.2".to_owned()
}
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_is_usb_topology_name_accepts_valid_names() {
for name in ["1-1", "1-1.4", "1-1.4.3", "2-10.3.1.4"] {
assert!(is_usb_topology_name(name), "{name} should be a valid topology name");
}
}
#[cfg(target_os = "linux")]
#[test]
fn test_is_usb_topology_name_rejects_non_topology_names() {
for name in [
"usb1", "hidraw0", "1-1.4.3:1.0", "1-", "-1", "1-1.", "1-.1", "a-1", "1-1.a", "",
] {
assert!(
!is_usb_topology_name(name),
"{name} should not be a valid topology name"
);
}
}
}