1 use cfg_if::cfg_if;
2
3 cfg_if! {
4 if #[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))]{
5 use std::ffi::OsStr;
6 }
7 }
8
9 cfg_if! {
10 if #[cfg(any(target_os = "ios", target_os = "macos"))] {
11 use core_foundation::base::CFType;
12 use core_foundation::base::TCFType;
13 use core_foundation::dictionary::CFDictionary;
14 use core_foundation::dictionary::CFMutableDictionary;
15 use core_foundation::number::CFNumber;
16 use core_foundation::string::CFString;
17 use core_foundation_sys::base::{kCFAllocatorDefault, CFRetain};
18 use io_kit_sys::*;
19 use io_kit_sys::keys::*;
20 use io_kit_sys::serial::keys::*;
21 use io_kit_sys::types::*;
22 use io_kit_sys::usb::lib::*;
23 use nix::libc::{c_char, c_void};
24 use std::ffi::CStr;
25 use std::mem::MaybeUninit;
26 }
27 }
28
29 #[cfg(any(
30 target_os = "freebsd",
31 target_os = "ios",
32 target_os = "linux",
33 target_os = "macos"
34 ))]
35 use crate::SerialPortType;
36 #[cfg(any(target_os = "ios", target_os = "linux", target_os = "macos"))]
37 use crate::UsbPortInfo;
38 #[cfg(any(
39 target_os = "android",
40 target_os = "ios",
41 all(target_os = "linux", not(target_env = "musl"), feature = "libudev"),
42 target_os = "macos",
43 target_os = "netbsd",
44 target_os = "openbsd",
45 ))]
46 use crate::{Error, ErrorKind};
47 use crate::{Result, SerialPortInfo};
48
49 /// Retrieves the udev property value named by `key`. If the value exists, then it will be
50 /// converted to a String, otherwise None will be returned.
51 #[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))]
udev_property_as_string(d: &libudev::Device, key: &str) -> Option<String>52 fn udev_property_as_string(d: &libudev::Device, key: &str) -> Option<String> {
53 d.property_value(key)
54 .and_then(OsStr::to_str)
55 .map(|s| s.to_string())
56 }
57
58 /// Retrieves the udev property value named by `key`. This function assumes that the retrieved
59 /// string is comprised of hex digits and the integer value of this will be returned as a u16.
60 /// If the property value doesn't exist or doesn't contain valid hex digits, then an error
61 /// will be returned.
62 /// This function uses a built-in type's `from_str_radix` to implementation to perform the
63 /// actual conversion.
64 #[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))]
udev_hex_property_as_int<T>( d: &libudev::Device, key: &str, from_str_radix: &dyn Fn(&str, u32) -> std::result::Result<T, std::num::ParseIntError>, ) -> Result<T>65 fn udev_hex_property_as_int<T>(
66 d: &libudev::Device,
67 key: &str,
68 from_str_radix: &dyn Fn(&str, u32) -> std::result::Result<T, std::num::ParseIntError>,
69 ) -> Result<T> {
70 if let Some(hex_str) = d.property_value(key).and_then(OsStr::to_str) {
71 if let Ok(num) = from_str_radix(hex_str, 16) {
72 Ok(num)
73 } else {
74 Err(Error::new(ErrorKind::Unknown, "value not hex string"))
75 }
76 } else {
77 Err(Error::new(ErrorKind::Unknown, "key not found"))
78 }
79 }
80
81 /// Looks up a property which is provided in two "flavors": Where special charaters and whitespaces
82 /// are encoded/escaped and where they are replaced (with underscores). This is for example done
83 /// by udev for manufacturer and model information.
84 ///
85 /// See
86 /// https://github.com/systemd/systemd/blob/38c258398427d1f497268e615906759025e51ea6/src/udev/udev-builtin-usb_id.c#L432
87 /// for details.
88 #[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))]
udev_property_encoded_or_replaced_as_string( d: &libudev::Device, encoded_key: &str, replaced_key: &str, ) -> Option<String>89 fn udev_property_encoded_or_replaced_as_string(
90 d: &libudev::Device,
91 encoded_key: &str,
92 replaced_key: &str,
93 ) -> Option<String> {
94 udev_property_as_string(d, encoded_key)
95 .and_then(|s| unescaper::unescape(&s).ok())
96 .or_else(|| udev_property_as_string(d, replaced_key))
97 .map(udev_restore_spaces)
98 }
99
100 /// Converts the underscores from `udev_replace_whitespace` back to spaces quick and dirtily. We
101 /// are ignoring the different types of whitespaces and the substitutions from `udev_replace_chars`
102 /// deliberately for keeping a low profile.
103 ///
104 /// See
105 /// https://github.com/systemd/systemd/blob/38c258398427d1f497268e615906759025e51ea6/src/shared/udev-util.c#L281
106 /// for more details.
107 #[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))]
udev_restore_spaces(source: String) -> String108 fn udev_restore_spaces(source: String) -> String {
109 source.replace('_', " ")
110 }
111
112 #[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))]
port_type(d: &libudev::Device) -> Result<SerialPortType>113 fn port_type(d: &libudev::Device) -> Result<SerialPortType> {
114 match d.property_value("ID_BUS").and_then(OsStr::to_str) {
115 Some("usb") => {
116 let serial_number = udev_property_as_string(d, "ID_SERIAL_SHORT");
117 // For devices on the USB, udev also provides manufacturer and product information from
118 // its hardware dataase. Use this as a fallback if this information is not provided
119 // from the device itself.
120 let manufacturer =
121 udev_property_encoded_or_replaced_as_string(d, "ID_VENDOR_ENC", "ID_VENDOR")
122 .or_else(|| udev_property_as_string(d, "ID_VENDOR_FROM_DATABASE"));
123 let product =
124 udev_property_encoded_or_replaced_as_string(d, "ID_MODEL_ENC", "ID_MODEL")
125 .or_else(|| udev_property_as_string(d, "ID_MODEL_FROM_DATABASE"));
126 Ok(SerialPortType::UsbPort(UsbPortInfo {
127 vid: udev_hex_property_as_int(d, "ID_VENDOR_ID", &u16::from_str_radix)?,
128 pid: udev_hex_property_as_int(d, "ID_MODEL_ID", &u16::from_str_radix)?,
129 serial_number,
130 manufacturer,
131 product,
132 #[cfg(feature = "usbportinfo-interface")]
133 interface: udev_hex_property_as_int(d, "ID_USB_INTERFACE_NUM", &u8::from_str_radix)
134 .ok(),
135 }))
136 }
137 Some("pci") => {
138 let usb_properties = vec![
139 d.property_value("ID_USB_VENDOR_ID"),
140 d.property_value("ID_USB_MODEL_ID"),
141 ]
142 .into_iter()
143 .collect::<Option<Vec<_>>>();
144 if usb_properties.is_some() {
145 // For USB devices reported at a PCI bus, there is apparently no fallback
146 // information from udevs hardware database provided.
147 let manufacturer = udev_property_encoded_or_replaced_as_string(
148 d,
149 "ID_USB_VENDOR_ENC",
150 "ID_USB_VENDOR",
151 );
152 let product = udev_property_encoded_or_replaced_as_string(
153 d,
154 "ID_USB_MODEL_ENC",
155 "ID_USB_MODEL",
156 );
157 Ok(SerialPortType::UsbPort(UsbPortInfo {
158 vid: udev_hex_property_as_int(d, "ID_USB_VENDOR_ID", &u16::from_str_radix)?,
159 pid: udev_hex_property_as_int(d, "ID_USB_MODEL_ID", &u16::from_str_radix)?,
160 serial_number: udev_property_as_string(d, "ID_USB_SERIAL_SHORT"),
161 manufacturer,
162 product,
163 #[cfg(feature = "usbportinfo-interface")]
164 interface: udev_hex_property_as_int(
165 d,
166 "ID_USB_INTERFACE_NUM",
167 &u8::from_str_radix,
168 )
169 .ok(),
170 }))
171 } else {
172 Ok(SerialPortType::PciPort)
173 }
174 }
175 None => find_usb_interface_from_parents(d.parent())
176 .and_then(get_modalias_from_device)
177 .as_deref()
178 .and_then(parse_modalias)
179 .map_or(Ok(SerialPortType::Unknown), |port_info| {
180 Ok(SerialPortType::UsbPort(port_info))
181 }),
182 _ => Ok(SerialPortType::Unknown),
183 }
184 }
185
186 #[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))]
find_usb_interface_from_parents(parent: Option<libudev::Device>) -> Option<libudev::Device>187 fn find_usb_interface_from_parents(parent: Option<libudev::Device>) -> Option<libudev::Device> {
188 let mut p = parent?;
189
190 // limit the query depth
191 for _ in 1..4 {
192 match p.devtype() {
193 None => match p.parent() {
194 None => break,
195 Some(x) => p = x,
196 },
197 Some(s) => {
198 if s.to_str()? == "usb_interface" {
199 break;
200 } else {
201 match p.parent() {
202 None => break,
203 Some(x) => p = x,
204 }
205 }
206 }
207 }
208 }
209
210 Some(p)
211 }
212
213 #[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))]
get_modalias_from_device(d: libudev::Device) -> Option<String>214 fn get_modalias_from_device(d: libudev::Device) -> Option<String> {
215 Some(
216 d.property_value("MODALIAS")
217 .and_then(OsStr::to_str)?
218 .to_owned(),
219 )
220 }
221
222 // MODALIAS = usb:v303Ap1001d0101dcEFdsc02dp01ic02isc02ip00in00
223 // v 303A (device vendor)
224 // p 1001 (device product)
225 // d 0101 (bcddevice)
226 // dc EF (device class)
227 // dsc 02 (device subclass)
228 // dp 01 (device protocol)
229 // ic 02 (interface class)
230 // isc 02 (interface subclass)
231 // ip 00 (interface protocol)
232 // in 00 (interface number)
233 #[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))]
parse_modalias(moda: &str) -> Option<UsbPortInfo>234 fn parse_modalias(moda: &str) -> Option<UsbPortInfo> {
235 // Find the start of the string, will start with "usb:"
236 let mod_start = moda.find("usb:v")?;
237
238 // Tail to update while searching.
239 let mut mod_tail = moda.get(mod_start + 5..)?;
240
241 // The next four characters should be hex values of the vendor.
242 let vid = mod_tail.get(..4)?;
243 mod_tail = mod_tail.get(4..)?;
244
245 // The next portion we care about is the device product ID.
246 let pid_start = mod_tail.find('p')?;
247 let pid = mod_tail.get(pid_start + 1..pid_start + 5)?;
248
249 Some(UsbPortInfo {
250 vid: u16::from_str_radix(vid, 16).ok()?,
251 pid: u16::from_str_radix(pid, 16).ok()?,
252 serial_number: None,
253 manufacturer: None,
254 product: None,
255 // Only attempt to find the interface if the feature is enabled.
256 #[cfg(feature = "usbportinfo-interface")]
257 interface: mod_tail.get(pid_start + 4..).and_then(|mod_tail| {
258 mod_tail.find("in").and_then(|i_start| {
259 mod_tail
260 .get(i_start + 2..i_start + 4)
261 .and_then(|interface| u8::from_str_radix(interface, 16).ok())
262 })
263 }),
264 })
265 }
266
267 #[cfg(any(target_os = "ios", target_os = "macos"))]
get_parent_device_by_type( device: io_object_t, parent_type: *const c_char, ) -> Option<io_registry_entry_t>268 fn get_parent_device_by_type(
269 device: io_object_t,
270 parent_type: *const c_char,
271 ) -> Option<io_registry_entry_t> {
272 let parent_type = unsafe { CStr::from_ptr(parent_type) };
273 use mach2::kern_return::KERN_SUCCESS;
274 let mut device = device;
275 loop {
276 let mut class_name = MaybeUninit::<[c_char; 128]>::uninit();
277 unsafe { IOObjectGetClass(device, class_name.as_mut_ptr() as *mut c_char) };
278 let class_name = unsafe { class_name.assume_init() };
279 let name = unsafe { CStr::from_ptr(&class_name[0]) };
280 if name == parent_type {
281 return Some(device);
282 }
283 let mut parent = MaybeUninit::uninit();
284 if unsafe {
285 IORegistryEntryGetParentEntry(device, kIOServiceClass, parent.as_mut_ptr())
286 != KERN_SUCCESS
287 } {
288 return None;
289 }
290 device = unsafe { parent.assume_init() };
291 }
292 }
293
294 #[cfg(any(target_os = "ios", target_os = "macos"))]
295 #[allow(non_upper_case_globals)]
296 /// Returns a specific property of the given device as an integer.
get_int_property(device_type: io_registry_entry_t, property: &str) -> Result<u32>297 fn get_int_property(device_type: io_registry_entry_t, property: &str) -> Result<u32> {
298 let cf_property = CFString::new(property);
299
300 let cf_type_ref = unsafe {
301 IORegistryEntryCreateCFProperty(
302 device_type,
303 cf_property.as_concrete_TypeRef(),
304 kCFAllocatorDefault,
305 0,
306 )
307 };
308 if cf_type_ref.is_null() {
309 return Err(Error::new(ErrorKind::Unknown, "Failed to get property"));
310 }
311
312 let cf_type = unsafe { CFType::wrap_under_create_rule(cf_type_ref) };
313 cf_type
314 .downcast::<CFNumber>()
315 .and_then(|n| n.to_i64())
316 .map(|n| n as u32)
317 .ok_or(Error::new(
318 ErrorKind::Unknown,
319 "Failed to get numerical value",
320 ))
321 }
322
323 #[cfg(any(target_os = "ios", target_os = "macos"))]
324 /// Returns a specific property of the given device as a string.
get_string_property(device_type: io_registry_entry_t, property: &str) -> Result<String>325 fn get_string_property(device_type: io_registry_entry_t, property: &str) -> Result<String> {
326 let cf_property = CFString::new(property);
327
328 let cf_type_ref = unsafe {
329 IORegistryEntryCreateCFProperty(
330 device_type,
331 cf_property.as_concrete_TypeRef(),
332 kCFAllocatorDefault,
333 0,
334 )
335 };
336 if cf_type_ref.is_null() {
337 return Err(Error::new(ErrorKind::Unknown, "Failed to get property"));
338 }
339
340 let cf_type = unsafe { CFType::wrap_under_create_rule(cf_type_ref) };
341 cf_type
342 .downcast::<CFString>()
343 .map(|s| s.to_string())
344 .ok_or(Error::new(ErrorKind::Unknown, "Failed to get string value"))
345 }
346
347 #[cfg(any(target_os = "ios", target_os = "macos"))]
348 /// Determine the serial port type based on the service object (like that returned by
349 /// `IOIteratorNext`). Specific properties are extracted for USB devices.
port_type(service: io_object_t) -> SerialPortType350 fn port_type(service: io_object_t) -> SerialPortType {
351 let bluetooth_device_class_name = b"IOBluetoothSerialClient\0".as_ptr() as *const c_char;
352 let usb_device_class_name = b"IOUSBHostInterface\0".as_ptr() as *const c_char;
353 let legacy_usb_device_class_name = kIOUSBDeviceClassName;
354
355 let maybe_usb_device = get_parent_device_by_type(service, usb_device_class_name)
356 .or_else(|| get_parent_device_by_type(service, legacy_usb_device_class_name));
357 if let Some(usb_device) = maybe_usb_device {
358 SerialPortType::UsbPort(UsbPortInfo {
359 vid: get_int_property(usb_device, "idVendor").unwrap_or_default() as u16,
360 pid: get_int_property(usb_device, "idProduct").unwrap_or_default() as u16,
361 serial_number: get_string_property(usb_device, "USB Serial Number").ok(),
362 manufacturer: get_string_property(usb_device, "USB Vendor Name").ok(),
363 product: get_string_property(usb_device, "USB Product Name").ok(),
364 // Apple developer documentation indicates `bInterfaceNumber` is the supported key for
365 // looking up the composite usb interface id. `idVendor` and `idProduct` are included in the same tables, so
366 // we will lookup the interface number using the same method. See:
367 //
368 // https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_driverkit_transport_usb
369 // https://developer.apple.com/library/archive/documentation/DeviceDrivers/Conceptual/USBBook/USBOverview/USBOverview.html#//apple_ref/doc/uid/TP40002644-BBCEACAJ
370 #[cfg(feature = "usbportinfo-interface")]
371 interface: get_int_property(usb_device, "bInterfaceNumber")
372 .map(|x| x as u8)
373 .ok(),
374 })
375 } else if get_parent_device_by_type(service, bluetooth_device_class_name).is_some() {
376 SerialPortType::BluetoothPort
377 } else {
378 SerialPortType::PciPort
379 }
380 }
381
382 cfg_if! {
383 if #[cfg(any(target_os = "ios", target_os = "macos"))] {
384 /// Scans the system for serial ports and returns a list of them.
385 /// The `SerialPortInfo` struct contains the name of the port which can be used for opening it.
386 pub fn available_ports() -> Result<Vec<SerialPortInfo>> {
387 use mach2::kern_return::KERN_SUCCESS;
388 use mach2::port::{mach_port_t, MACH_PORT_NULL};
389
390 let mut vec = Vec::new();
391 unsafe {
392 // Create a dictionary for specifying the search terms against the IOService
393 let classes_to_match = IOServiceMatching(kIOSerialBSDServiceValue);
394 if classes_to_match.is_null() {
395 return Err(Error::new(
396 ErrorKind::Unknown,
397 "IOServiceMatching returned a NULL dictionary.",
398 ));
399 }
400 let mut classes_to_match = CFMutableDictionary::wrap_under_create_rule(classes_to_match);
401
402 // Populate the search dictionary with a single key/value pair indicating that we're
403 // searching for serial devices matching the RS232 device type.
404 let search_key = CStr::from_ptr(kIOSerialBSDTypeKey);
405 let search_key = CFString::from_static_string(search_key.to_str().map_err(|_| Error::new(ErrorKind::Unknown, "Failed to convert search key string"))?);
406 let search_value = CStr::from_ptr(kIOSerialBSDAllTypes);
407 let search_value = CFString::from_static_string(search_value.to_str().map_err(|_| Error::new(ErrorKind::Unknown, "Failed to convert search key string"))?);
408 classes_to_match.set(search_key, search_value);
409
410 // Get an interface to IOKit
411 let mut master_port: mach_port_t = MACH_PORT_NULL;
412 let mut kern_result = IOMasterPort(MACH_PORT_NULL, &mut master_port);
413 if kern_result != KERN_SUCCESS {
414 return Err(Error::new(
415 ErrorKind::Unknown,
416 format!("ERROR: {}", kern_result),
417 ));
418 }
419
420 // Run the search. IOServiceGetMatchingServices consumes one reference count of
421 // classes_to_match, so explicitly retain.
422 //
423 // TODO: We could also just mem::forget classes_to_match like in
424 // TCFType::into_CFType. Is there a special reason that there is no
425 // TCFType::into_concrete_TypeRef()?
426 CFRetain(classes_to_match.as_CFTypeRef());
427 let mut matching_services = MaybeUninit::uninit();
428 kern_result = IOServiceGetMatchingServices(
429 kIOMasterPortDefault,
430 classes_to_match.as_concrete_TypeRef(),
431 matching_services.as_mut_ptr(),
432 );
433 if kern_result != KERN_SUCCESS {
434 return Err(Error::new(
435 ErrorKind::Unknown,
436 format!("ERROR: {}", kern_result),
437 ));
438 }
439 let matching_services = matching_services.assume_init();
440 let _matching_services_guard = scopeguard::guard((), |_| {
441 IOObjectRelease(matching_services);
442 });
443
444 loop {
445 // Grab the next result.
446 let modem_service = IOIteratorNext(matching_services);
447 // Break out if we've reached the end of the iterator
448 if modem_service == MACH_PORT_NULL {
449 break;
450 }
451 let _modem_service_guard = scopeguard::guard((), |_| {
452 IOObjectRelease(modem_service);
453 });
454
455 // Fetch all properties of the current search result item.
456 let mut props = MaybeUninit::uninit();
457 let result = IORegistryEntryCreateCFProperties(
458 modem_service,
459 props.as_mut_ptr(),
460 kCFAllocatorDefault,
461 0,
462 );
463 if result == KERN_SUCCESS {
464 // A successful call to IORegistryEntryCreateCFProperties indicates that a
465 // properties dict has been allocated and we as the caller are in charge of
466 // releasing it.
467 let props = props.assume_init();
468 let props: CFDictionary<CFString, *const c_void> = CFDictionary::wrap_under_create_rule(props);
469
470 for key in ["IOCalloutDevice", "IODialinDevice"].iter() {
471 let cf_key = CFString::new(key);
472
473 if let Some(cf_ref) = props.find(cf_key) {
474 let cf_type = CFType::wrap_under_get_rule(*cf_ref);
475 match cf_type
476 .downcast::<CFString>()
477 .map(|s| s.to_string())
478 {
479 Some(path) => {
480 vec.push(SerialPortInfo {
481 port_name: path,
482 port_type: port_type(modem_service),
483 });
484 }
485 None => return Err(Error::new(ErrorKind::Unknown, format!("Failed to get string value for {}", key))),
486 }
487 } else {
488 return Err(Error::new(ErrorKind::Unknown, format!("Key {} missing in dict", key)));
489 }
490 }
491 } else {
492 return Err(Error::new(ErrorKind::Unknown, format!("ERROR: {}", result)));
493 }
494 }
495 }
496 Ok(vec)
497 }
498 } else if #[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))] {
499 /// Scans the system for serial ports and returns a list of them.
500 /// The `SerialPortInfo` struct contains the name of the port
501 /// which can be used for opening it.
502 pub fn available_ports() -> Result<Vec<SerialPortInfo>> {
503 let mut vec = Vec::new();
504 if let Ok(context) = libudev::Context::new() {
505 let mut enumerator = libudev::Enumerator::new(&context)?;
506 enumerator.match_subsystem("tty")?;
507 let devices = enumerator.scan_devices()?;
508 for d in devices {
509 if let Some(p) = d.parent() {
510 if let Some(devnode) = d.devnode() {
511 if let Some(path) = devnode.to_str() {
512 if let Some(driver) = p.driver() {
513 if driver == "serial8250" && crate::new(path, 9600).open().is_err() {
514 continue;
515 }
516 }
517 // Stop bubbling up port_type errors here so problematic ports are just
518 // skipped instead of causing no ports to be returned.
519 if let Ok(pt) = port_type(&d) {
520 vec.push(SerialPortInfo {
521 port_name: String::from(path),
522 port_type: pt,
523 });
524 }
525 }
526 }
527 }
528 }
529 }
530 Ok(vec)
531 }
532 } else if #[cfg(target_os = "linux")] {
533 use std::fs::File;
534 use std::io::Read;
535 use std::path::Path;
536
537 fn read_file_to_trimmed_string(dir: &Path, file: &str) -> Option<String> {
538 let path = dir.join(file);
539 let mut s = String::new();
540 File::open(path).ok()?.read_to_string(&mut s).ok()?;
541 Some(s.trim().to_owned())
542 }
543
544 fn read_file_to_u16(dir: &Path, file: &str) -> Option<u16> {
545 u16::from_str_radix(&read_file_to_trimmed_string(dir, file)?, 16).ok()
546 }
547
548 #[cfg(feature = "usbportinfo-interface")]
549 fn read_file_to_u8(dir: &Path, file: &str) -> Option<u8> {
550 u8::from_str_radix(&read_file_to_trimmed_string(dir, file)?, 16).ok()
551 }
552
553 fn read_port_type(path: &Path) -> Option<SerialPortType> {
554 let path = path
555 .canonicalize()
556 .ok()?;
557 let subsystem = path.join("subsystem").canonicalize().ok()?;
558 let subsystem = subsystem.file_name()?.to_string_lossy();
559
560 match subsystem.as_ref() {
561 // Broadcom SoC UARTs (of Raspberry Pi devices).
562 "amba" => Some(SerialPortType::Unknown),
563 "pci" => Some(SerialPortType::PciPort),
564 "pnp" => Some(SerialPortType::Unknown),
565 "usb" => usb_port_type(&path),
566 "usb-serial" => usb_port_type(path.parent()?),
567 _ => None,
568 }
569 }
570
571 fn usb_port_type(interface_path: &Path) -> Option<SerialPortType> {
572 let info = read_usb_port_info(interface_path)?;
573 Some(SerialPortType::UsbPort(info))
574 }
575
576 fn read_usb_port_info(interface_path: &Path) -> Option<UsbPortInfo> {
577 let device_path = interface_path.parent()?;
578
579 let vid = read_file_to_u16(&device_path, "idVendor")?;
580 let pid = read_file_to_u16(&device_path, "idProduct")?;
581 #[cfg(feature = "usbportinfo-interface")]
582 let interface = read_file_to_u8(&interface_path, &"bInterfaceNumber");
583 let serial_number = read_file_to_trimmed_string(&device_path, &"serial");
584 let product = read_file_to_trimmed_string(&device_path, &"product");
585 let manufacturer = read_file_to_trimmed_string(&device_path, &"manufacturer");
586
587 Some(UsbPortInfo {
588 vid,
589 pid,
590 serial_number,
591 manufacturer,
592 product,
593 #[cfg(feature = "usbportinfo-interface")]
594 interface,
595 })
596 }
597
598 /// Scans `/sys/class/tty` for serial devices (on Linux systems without libudev).
599 pub fn available_ports() -> Result<Vec<SerialPortInfo>> {
600 let mut vec = Vec::new();
601 let sys_path = Path::new("/sys/class/tty/");
602 let dev_path = Path::new("/dev");
603 for path in sys_path.read_dir().expect("/sys/class/tty/ doesn't exist on this system") {
604 let raw_path = path?.path().clone();
605 let mut path = raw_path.clone();
606
607 path.push("device");
608 if !path.is_dir() {
609 continue;
610 }
611
612 // Determine port type and proceed, if it's a known.
613 //
614 // TODO: Switch to a likely more readable let-else statement when our MSRV supports
615 // it.
616 let port_type = read_port_type(&path);
617 let port_type = if let Some(port_type) = port_type {
618 port_type
619 } else {
620 continue;
621 };
622
623 // Generate the device file path `/dev/DEVICE` from the TTY class path
624 // `/sys/class/tty/DEVICE` and emit a serial device if this path exists. There are
625 // no further checks (yet) due to `Path::is_file` reports only regular files.
626 //
627 // See https://github.com/serialport/serialport-rs/issues/66 for details.
628 if let Some(file_name) = raw_path.file_name() {
629 let device_file = dev_path.join(file_name);
630 if !device_file.exists() {
631 continue;
632 }
633
634 vec.push(SerialPortInfo {
635 port_name: device_file.to_string_lossy().to_string(),
636 port_type,
637 });
638 }
639 }
640 Ok(vec)
641 }
642 } else if #[cfg(target_os = "freebsd")] {
643 use std::path::Path;
644
645 /// Scans the system for serial ports and returns a list of them.
646 /// The `SerialPortInfo` struct contains the name of the port
647 /// which can be used for opening it.
648 pub fn available_ports() -> Result<Vec<SerialPortInfo>> {
649 let mut vec = Vec::new();
650 let dev_path = Path::new("/dev/");
651 for path in dev_path.read_dir()? {
652 let path = path?;
653 let filename = path.file_name();
654 let filename_string = filename.to_string_lossy();
655 if filename_string.starts_with("cuaU") || filename_string.starts_with("cuau") || filename_string.starts_with("cuad") {
656 if !filename_string.ends_with(".init") && !filename_string.ends_with(".lock") {
657 vec.push(SerialPortInfo {
658 port_name: path.path().to_string_lossy().to_string(),
659 port_type: SerialPortType::Unknown,
660 });
661 }
662 }
663 }
664 Ok(vec)
665 }
666 } else {
667 /// Enumerating serial ports on this platform is not supported
668 pub fn available_ports() -> Result<Vec<SerialPortInfo>> {
669 Err(Error::new(
670 ErrorKind::Unknown,
671 "Not implemented for this OS",
672 ))
673 }
674 }
675 }
676
677 #[cfg(all(
678 test,
679 target_os = "linux",
680 not(target_env = "musl"),
681 feature = "libudev"
682 ))]
683 mod tests {
684 use super::*;
685
686 use quickcheck_macros::quickcheck;
687
688 #[quickcheck]
quickcheck_parse_modalias_does_not_panic_from_random_data(modalias: String) -> bool689 fn quickcheck_parse_modalias_does_not_panic_from_random_data(modalias: String) -> bool {
690 let _ = parse_modalias(&modalias);
691 true
692 }
693
694 #[test]
parse_modalias_canonical()695 fn parse_modalias_canonical() {
696 const MODALIAS: &str = "usb:v303Ap1001d0101dcEFdsc02dp01ic02isc02ip00in0C";
697
698 let port_info = parse_modalias(MODALIAS).expect("parse failed");
699
700 assert_eq!(port_info.vid, 0x303A, "vendor parse invalid");
701 assert_eq!(port_info.pid, 0x1001, "product parse invalid");
702
703 #[cfg(feature = "usbportinfo-interface")]
704 assert_eq!(port_info.interface, Some(0x0C), "interface parse invalid");
705 }
706
707 #[test]
parse_modalias_corner_cases()708 fn parse_modalias_corner_cases() {
709 assert!(parse_modalias("").is_none());
710 assert!(parse_modalias("usb").is_none());
711 assert!(parse_modalias("usb:").is_none());
712 assert!(parse_modalias("usb:vdcdc").is_none());
713 assert!(parse_modalias("usb:pdcdc").is_none());
714
715 // Just vendor and product IDs.
716 let info = parse_modalias("usb:vdcdcpabcd").unwrap();
717 assert_eq!(info.vid, 0xdcdc);
718 assert_eq!(info.pid, 0xabcd);
719 #[cfg(feature = "usbportinfo-interface")]
720 assert!(info.interface.is_none());
721
722 // Vendor and product ID plus an interface number.
723 let info = parse_modalias("usb:v1234p5678indc").unwrap();
724 assert_eq!(info.vid, 0x1234);
725 assert_eq!(info.pid, 0x5678);
726 #[cfg(feature = "usbportinfo-interface")]
727 assert_eq!(info.interface, Some(0xdc));
728 }
729 }
730