basic_udev/
lib.rs

1use std::{
2    collections::HashMap,
3    ffi::{OsStr, OsString},
4    io::{BufRead, BufReader},
5    os::unix::ffi::OsStringExt,
6    path::{Path, PathBuf},
7};
8
9pub struct Enumerator {
10    subsystem: Option<String>,
11}
12
13const UDEV_ROOT: &str = "/sys";
14const DEV_ROOT: &str = "/dev";
15
16impl Enumerator {
17    pub fn new() -> std::io::Result<Enumerator> {
18        Ok(Enumerator { subsystem: None })
19    }
20
21    pub fn match_subsystem(&mut self, subsystem: &str) -> std::io::Result<()> {
22        self.subsystem = Some(subsystem.to_owned());
23        Ok(())
24    }
25
26    pub fn scan_devices(&self) -> std::io::Result<Box<dyn Iterator<Item = Device>>> {
27        let mut devices = vec![];
28        let mut device_path = PathBuf::from(UDEV_ROOT);
29        device_path.push("class");
30        device_path.push(self.subsystem.as_ref().unwrap());
31        for dir_entry in device_path.read_dir()? {
32            let dir_entry = dir_entry?;
33
34            if let Ok(device) = Device::from_syspath(&dir_entry.path()) {
35                devices.push(device);
36            }
37        }
38        Ok(Box::new(devices.into_iter()))
39    }
40}
41
42#[derive(Debug)]
43#[allow(dead_code)]
44pub struct Device {
45    system_path: PathBuf,
46    subsystem: Option<String>,
47    driver: Option<String>,
48    devnode: Option<PathBuf>,
49    attributes: HashMap<String, OsString>,
50    properties: HashMap<String, OsString>,
51    parent: Option<Box<Device>>,
52}
53
54impl Device {
55    pub fn from_syspath(path: &Path) -> std::io::Result<Device> {
56        let path = path.canonicalize()?;
57        let mut parent = None;
58
59        // Keep looking for a parent object.
60        let mut parent_path = path.parent();
61        loop {
62            let Some(path) = parent_path else {
63                break;
64            };
65            let uevent = path.join("uevent");
66            if uevent.exists() && uevent.is_file() {
67                parent = Self::from_syspath(path).ok().map(|parent| Box::new(parent));
68                break;
69            }
70            parent_path = path.parent();
71        }
72
73        let uevent = path.join("uevent");
74        if !uevent.exists() || !uevent.is_file() {
75            return Err(std::io::Error::from(std::io::ErrorKind::NotFound));
76        }
77
78        let mut properties = HashMap::new();
79        let uevent = std::fs::File::open(uevent)?;
80        for line in BufReader::new(uevent).lines() {
81            let Ok(line) = line else {
82                break;
83            };
84            let Some((key, value)) = line.split_once('=') else {
85                continue;
86            };
87            properties.insert(
88                key.to_owned(),
89                OsString::from_vec(value.as_bytes().to_vec()),
90            );
91        }
92
93        let subsystem = path.join("subsystem").canonicalize().ok().and_then(|dir| {
94            dir.file_name()
95                .and_then(|v| v.to_str())
96                .map(|name| name.to_string())
97        });
98        let driver = path.join("driver").canonicalize().ok().and_then(|dir| {
99            dir.file_name()
100                .and_then(|v| v.to_str())
101                .map(|name| name.to_string())
102        });
103
104        let mut attrs = HashMap::new();
105        Self::update_attrs(&path, &path, &mut attrs);
106
107        // TODO: What if /dev isn't where things are mounted?
108        let mut devnode = None;
109        if let Some(devname) = properties.get("DEVNAME") {
110            let mut tmp_dev = Path::new(DEV_ROOT).to_path_buf();
111            tmp_dev.push(devname);
112            devnode = Some(tmp_dev)
113        }
114
115        Ok(Device {
116            system_path: path.to_owned(),
117            subsystem,
118            driver,
119            attributes: attrs,
120            parent,
121            properties,
122            devnode,
123        })
124    }
125
126    fn update_attrs(path: &Path, base: &Path, attrs: &mut HashMap<String, OsString>) {
127        let Ok(read_dir) = path.read_dir() else {
128            return;
129        };
130        for entry in read_dir {
131            let Ok(entry) = entry else {
132                continue;
133            };
134            let entry_path = entry.path();
135            if entry_path.is_symlink() {
136                continue;
137            }
138            let entry_name = entry_path.file_name().and_then(|v| v.to_str());
139            if entry_name == Some("uevent") || entry_name == Some("dev") {
140                continue;
141            }
142
143            if entry_path.is_dir() {
144                let mut sub_dir = entry_path.clone();
145                sub_dir.push("uevent");
146                if !sub_dir.exists() {
147                    Self::update_attrs(&entry_path, base, attrs);
148                    continue;
149                }
150            }
151
152            if entry_path.is_file() {
153                let Some(name) = entry_path
154                    .strip_prefix(base)
155                    .ok()
156                    .map(|name| name.as_os_str())
157                    .map(|name| name.to_str())
158                    .flatten()
159                    .map(|v| v.to_owned())
160                else {
161                    continue;
162                };
163                let Ok(mut value) = std::fs::read(entry_path) else {
164                    continue;
165                };
166                // String values seem to have an additional newline. Remove that.
167                if value.last() == Some(&b'\n') {
168                    value.pop();
169                }
170                attrs.insert(name, OsString::from_vec(value));
171                continue;
172            }
173        }
174    }
175
176    pub fn parent_with_subsystem(
177        &self,
178        parent_subsystem: &str,
179    ) -> std::io::Result<Option<&Device>> {
180        let mut parent = &self.parent;
181        loop {
182            let Some(unwrapped_parent) = parent else {
183                return Ok(None);
184            };
185
186            if unwrapped_parent.subsystem.as_deref() == Some(parent_subsystem) {
187                return Ok(Some(&unwrapped_parent));
188            }
189
190            parent = &unwrapped_parent.parent;
191        }
192    }
193
194    pub fn parent_with_subsystem_devtype(
195        &self,
196        parent_subsystem: &str,
197        devtype: &str,
198    ) -> std::io::Result<Option<&Device>> {
199        let mut parent = &self.parent;
200        loop {
201            let Some(unwrapped_parent) = parent else {
202                println!("Couldn't get parent device");
203                return Ok(None);
204            };
205
206            if unwrapped_parent.subsystem.as_deref() == Some(parent_subsystem)
207                && unwrapped_parent
208                    .property_value("DEVTYPE")
209                    .map(|x| x.to_str())
210                    .flatten()
211                    == Some(devtype)
212            {
213                return Ok(Some(&unwrapped_parent));
214            }
215            parent = &unwrapped_parent.parent;
216        }
217    }
218
219    pub fn property_value(&self, key: &str) -> Option<&OsStr> {
220        self.properties.get(key).map(|x| x.as_os_str())
221    }
222
223    pub fn attribute_value(&self, key: &str) -> Option<&OsStr> {
224        self.attributes.get(key).map(|x| x.as_os_str())
225    }
226
227    pub fn devnode(&self) -> Option<&Path> {
228        self.devnode.as_deref()
229    }
230
231    pub fn syspath(&self) -> &Path {
232        self.system_path.as_path()
233    }
234}