• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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