1#![cfg(unix)]
2
3pub use evdev_upstream::*;
4
5use std::path::{Path, PathBuf};
6
7pub fn enumerate() -> EnumerateDevices {
12 enumerate_directory(Path::new("/dev/input"))
13}
14
15pub struct EnumerateDevices {
17 paths: std::vec::IntoIter<PathBuf>,
18}
19
20impl Iterator for EnumerateDevices {
21 type Item = (PathBuf, ());
22
23 fn next(&mut self) -> Option<Self::Item> {
24 self.paths.next().map(|path| (path, ()))
25 }
26}
27
28fn enumerate_directory(directory: &Path) -> EnumerateDevices {
29 let mut paths = std::fs::read_dir(directory)
30 .into_iter()
31 .flatten()
32 .filter_map(Result::ok)
33 .map(|entry| entry.path())
34 .filter(|path| is_event_node(path))
35 .collect::<Vec<_>>();
36 paths.sort();
37 EnumerateDevices {
38 paths: paths.into_iter(),
39 }
40}
41
42fn is_event_node(path: &Path) -> bool {
43 let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
44 return false;
45 };
46 let Some(index) = name.strip_prefix("event") else {
47 return false;
48 };
49 !index.is_empty() && index.bytes().all(|byte| byte.is_ascii_digit())
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55 use std::fs;
56 use std::os::unix::fs::symlink;
57 use std::time::{SystemTime, UNIX_EPOCH};
58
59 struct TestDirectory(PathBuf);
60
61 impl TestDirectory {
62 fn new() -> Self {
63 let nonce = SystemTime::now()
64 .duration_since(UNIX_EPOCH)
65 .expect("system clock precedes the Unix epoch")
66 .as_nanos();
67 let path = std::env::temp_dir().join(format!(
68 "libinput-rs-discovery-{}-{nonce}",
69 std::process::id()
70 ));
71 fs::create_dir(&path).expect("create test input directory");
72 Self(path)
73 }
74 }
75
76 impl Drop for TestDirectory {
77 fn drop(&mut self) {
78 let _ = fs::remove_dir_all(&self.0);
79 }
80 }
81
82 #[test]
83 fn enumeration_does_not_open_event_nodes() {
84 let directory = TestDirectory::new();
85 symlink(
86 directory.0.join("missing-target"),
87 directory.0.join("event0"),
88 )
89 .expect("create unopenable event node");
90 fs::write(directory.0.join("event12"), b"").expect("create event node");
91 fs::write(directory.0.join("eventx"), b"").expect("create invalid event name");
92 fs::write(directory.0.join("mouse0"), b"").expect("create non-event node");
93
94 let names = enumerate_directory(&directory.0)
95 .map(|(path, ())| {
96 path.file_name()
97 .expect("event path has a file name")
98 .to_string_lossy()
99 .into_owned()
100 })
101 .collect::<Vec<_>>();
102
103 assert_eq!(names, vec!["event0".to_string(), "event12".to_string()]);
104 }
105
106 #[test]
107 fn missing_input_directory_is_empty() {
108 let directory = TestDirectory::new();
109 let missing = directory.0.join("missing");
110 assert_eq!(enumerate_directory(&missing).count(), 0);
111 }
112}