mdev_parser/
lib.rs

1#[macro_use]
2extern crate pest_derive;
3use pest::{iterators::Pair, Parser};
4use regex::Regex;
5use std::iter::once;
6use std::{fmt::Display, num::ParseIntError};
7use tracing::error;
8
9#[derive(Parser)]
10#[grammar = "../assets/conf_grammar.pest"]
11struct ConfParser;
12
13#[derive(Debug, PartialEq)]
14/// A line in the configuration file
15pub struct Conf {
16    /// Whether to stop is this filter matches
17    pub stop: bool,
18    pub envmatches: Vec<EnvMatch>,
19    /// Filter used to match the devices
20    pub filter: Filter,
21    /// User that will own the device
22    pub user: String,
23    /// Group that will own the device
24    pub group: String,
25    /// Permissions that the specified user and group have on the device
26    pub mode: u32,
27    /// What to do with the device node, if [`None`] it gets placed in `/dev/` with its
28    /// original name
29    pub on_creation: Option<OnCreation>,
30    /// Additional command that has to be executed when creating and/or removing the node
31    pub command: Option<Command>,
32}
33
34impl Conf {
35    fn from_rule(v: Pair<'_, Rule>) -> anyhow::Result<Self> {
36        debug_assert_eq!(v.as_rule(), Rule::rule);
37        let mut conf = v.into_inner();
38        let matcher = conf.next().unwrap();
39        debug_assert_eq!(matcher.as_rule(), Rule::matcher);
40        let mut matcher = matcher.into_inner();
41        let stop = matcher
42            .peek()
43            .filter(|r| r.as_rule() != Rule::stop)
44            .is_some();
45        if !stop {
46            matcher.next();
47        }
48        let mut envmatches = Vec::new();
49        while matcher.peek().unwrap().as_rule() == Rule::env_match {
50            let envmatch = EnvMatch::from_rule(matcher.next().unwrap())?;
51            envmatches.push(envmatch);
52        }
53        let filter = matcher.next().unwrap();
54        let filter = match filter.as_rule() {
55            Rule::majmin => Filter::MajMin(MajMin::from_rule(filter)?),
56            Rule::device_regex => Filter::DeviceRegex(DeviceRegex::from_rule(filter)?),
57            _ => unreachable!(),
58        };
59        let (user, group) = user_group_from_rule(conf.next().unwrap());
60        let mode = mode_from_rule(conf.next().unwrap());
61
62        let (on_creation, command) = match conf.next() {
63            Some(next) if next.as_rule() == Rule::on_creation => (
64                Some(OnCreation::from_rule(next)),
65                conf.next().map(Command::from_rule),
66            ),
67            Some(next) if next.as_rule() == Rule::command => (None, Some(Command::from_rule(next))),
68            None => (None, None),
69            _ => unreachable!(),
70        };
71        Ok(Self {
72            stop,
73            envmatches,
74            filter,
75            user,
76            group,
77            mode,
78            on_creation,
79            command,
80        })
81    }
82}
83
84impl Display for Conf {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        if !self.stop {
87            write!(f, "-")?;
88        }
89        for envmatch in &self.envmatches {
90            write!(f, "{}={};", envmatch.envvar, envmatch.regex)?;
91        }
92        match &self.filter {
93            Filter::DeviceRegex(DeviceRegex {
94                regex,
95                envvar: Some(var),
96            }) => write!(f, "${}={}", var, regex),
97            Filter::DeviceRegex(v) => write!(f, "{}", v.regex),
98            Filter::MajMin(MajMin {
99                maj,
100                min,
101                min2: Some(min2),
102            }) => write!(f, "@{},{}-{}", maj, min, min2),
103            Filter::MajMin(v) => write!(f, "@{},{}", v.maj, v.min),
104        }?;
105        write!(f, " {}:{} {:03o}", self.user, self.group, self.mode,)?;
106        if let Some(on_creation) = &self.on_creation {
107            match on_creation {
108                OnCreation::Move(p) => write!(f, " ={}", p),
109                OnCreation::SymLink(p) => write!(f, " >{}", p),
110                OnCreation::Prevent => write!(f, " !"),
111            }?;
112        }
113        if let Some(command) = &self.command {
114            let when = match command.when {
115                WhenToRun::After => '@',
116                WhenToRun::Before => '$',
117                WhenToRun::Both => '*',
118            };
119            write!(f, " {}{}", when, command.path)?;
120            for arg in &command.args {
121                write!(f, " {}", arg)?;
122            }
123        }
124        Ok(())
125    }
126}
127
128impl Default for Conf {
129    fn default() -> Self {
130        let filter = Filter::DeviceRegex(DeviceRegex {
131            envvar: None,
132            regex: Regex::new(".*").unwrap(),
133        });
134        Conf {
135            stop: false,
136            envmatches: vec![],
137            filter,
138            user: "root".to_string(),
139            group: "root".to_string(),
140            mode: 0o660,
141            on_creation: None,
142            command: None,
143        }
144    }
145}
146
147#[derive(Debug)]
148pub struct EnvMatch {
149    pub envvar: String,
150    pub regex: Regex,
151}
152
153impl EnvMatch {
154    fn from_rule(v: Pair<'_, Rule>) -> Result<Self, regex::Error> {
155        debug_assert_eq!(v.as_rule(), Rule::env_match);
156        let mut envmatch = v.into_inner();
157        let envvar = envvar_from_rule(envmatch.next().unwrap()).into();
158        let regex = regex_from_rule(envmatch.next().unwrap())?;
159        Ok(Self { envvar, regex })
160    }
161}
162
163impl PartialEq for EnvMatch {
164    fn eq(&self, other: &Self) -> bool {
165        self.envvar == other.envvar && self.regex.as_str() == other.regex.as_str()
166    }
167}
168
169#[derive(Debug, PartialEq)]
170/// Filter used for matching the devices
171pub enum Filter {
172    DeviceRegex(DeviceRegex),
173    MajMin(MajMin),
174}
175
176impl From<DeviceRegex> for Filter {
177    fn from(v: DeviceRegex) -> Self {
178        Self::DeviceRegex(v)
179    }
180}
181
182impl From<MajMin> for Filter {
183    fn from(v: MajMin) -> Self {
184        Self::MajMin(v)
185    }
186}
187
188#[derive(Debug)]
189/// A regex used for matching devices based on their names
190pub struct DeviceRegex {
191    pub envvar: Option<String>,
192    /// [`Regex`] used for matching
193    pub regex: Regex,
194}
195
196impl DeviceRegex {
197    fn from_rule(v: Pair<'_, Rule>) -> Result<Self, regex::Error> {
198        debug_assert_eq!(v.as_rule(), Rule::device_regex);
199        let mut devregex = v.into_inner();
200        let envvar = devregex.next().unwrap();
201        let (envvar, regex) = match envvar.as_rule() {
202            Rule::envvar => (
203                Some(envvar_from_rule(envvar).into()),
204                regex_from_rule(devregex.next().unwrap())?,
205            ),
206            Rule::regex => (None, regex_from_rule(envvar)?),
207            _ => unreachable!(),
208        };
209        Ok(Self { envvar, regex })
210    }
211}
212
213impl PartialEq for DeviceRegex {
214    fn eq(&self, other: &Self) -> bool {
215        self.envvar == other.envvar && self.regex.as_str() == other.regex.as_str()
216    }
217}
218
219#[derive(Debug, PartialEq)]
220/// TODO: add docs
221pub struct MajMin {
222    pub maj: u32,
223    pub min: u32,
224    pub min2: Option<u32>,
225}
226
227impl MajMin {
228    fn from_rule(v: Pair<'_, Rule>) -> anyhow::Result<Self> {
229        debug_assert_eq!(v.as_rule(), Rule::majmin);
230        let mut majmin = v.into_inner();
231        let maj = u32_from_rule(majmin.next().unwrap())?;
232        let min = u32_from_rule(majmin.next().unwrap())?;
233        let min2 = majmin.next().map(u32_from_rule).transpose()?;
234        Ok(Self { maj, min, min2 })
235    }
236}
237
238#[derive(Clone, Debug, PartialEq)]
239/// Additional actions to take on creation of the device node
240pub enum OnCreation {
241    /// Moves/renames the device. If the path ends with `/` then the name will be stay the same
242    Move(String),
243    /// Same as [`OnCreation::Move`] but also creates a symlink in `/dev/` to the
244    /// renamed/moved device
245    SymLink(String),
246    /// Prevents the creation of the device node
247    Prevent,
248}
249
250impl OnCreation {
251    fn from_rule(v: Pair<'_, Rule>) -> Self {
252        debug_assert_eq!(v.as_rule(), Rule::on_creation);
253        let oc = v.into_inner().next().unwrap();
254        match oc.as_rule() {
255            Rule::move_to => Self::Move(path_from_rule(oc.into_inner().next().unwrap()).into()),
256            Rule::symlink => Self::SymLink(path_from_rule(oc.into_inner().next().unwrap()).into()),
257            Rule::prevent => Self::Prevent,
258            _ => unreachable!(),
259        }
260    }
261}
262
263#[derive(Debug, PartialEq)]
264/// When to run the [`Command`]
265pub enum WhenToRun {
266    /// After creating the device
267    After,
268    /// Before removing the device
269    Before,
270    /// Both after the creation and before removing
271    Both,
272}
273
274impl WhenToRun {
275    fn from_rule(v: Pair<'_, Rule>) -> Self {
276        debug_assert_eq!(v.as_rule(), Rule::when);
277        match v.into_inner().next().unwrap().as_rule() {
278            Rule::after => Self::After,
279            Rule::before => Self::Before,
280            Rule::both => Self::Both,
281            _ => unreachable!(),
282        }
283    }
284}
285
286#[derive(Debug, PartialEq)]
287pub struct Command {
288    /// When to run the command
289    pub when: WhenToRun,
290    /// Path to the executable
291    pub path: String,
292    /// Command line arguments
293    pub args: Vec<String>,
294}
295
296impl Command {
297    fn from_rule(v: Pair<'_, Rule>) -> Self {
298        debug_assert_eq!(v.as_rule(), Rule::command);
299        let mut command = v.into_inner();
300        let mut exec = command.next().unwrap().into_inner();
301        let when = WhenToRun::from_rule(exec.next().unwrap());
302        let path = path_from_rule(exec.next().unwrap()).into();
303        let args = command.map(arg_from_rule).map(String::from).collect();
304        Self { when, path, args }
305    }
306}
307
308fn path_from_rule(v: Pair<'_, Rule>) -> &str {
309    debug_assert_eq!(v.as_rule(), Rule::path);
310    v.as_str()
311}
312
313fn arg_from_rule(v: Pair<'_, Rule>) -> &str {
314    debug_assert_eq!(v.as_rule(), Rule::arg);
315    v.as_str()
316}
317
318fn name_from_rule(v: Pair<'_, Rule>) -> &str {
319    debug_assert_eq!(v.as_rule(), Rule::name);
320    v.as_str()
321}
322
323fn envvar_from_rule(v: Pair<'_, Rule>) -> &str {
324    debug_assert_eq!(v.as_rule(), Rule::envvar);
325    v.as_str()
326}
327
328fn regex_from_rule(v: Pair<'_, Rule>) -> Result<Regex, regex::Error> {
329    debug_assert_eq!(v.as_rule(), Rule::regex);
330    Regex::new(v.as_str())
331}
332
333fn u32_from_rule(v: Pair<'_, Rule>) -> Result<u32, ParseIntError> {
334    debug_assert_eq!(v.as_rule(), Rule::number);
335    v.as_str().parse()
336}
337
338fn user_group_from_rule(v: Pair<'_, Rule>) -> (String, String) {
339    debug_assert_eq!(v.as_rule(), Rule::usergroup);
340    let mut usergroup = v.into_inner();
341    let user = name_from_rule(usergroup.next().unwrap()).into();
342    let group = name_from_rule(usergroup.next().unwrap()).into();
343    (user, group)
344}
345
346fn mode_from_rule(v: Pair<'_, Rule>) -> u32 {
347    debug_assert_eq!(v.as_rule(), Rule::mode);
348    u32::from_str_radix(v.as_str(), 8).unwrap()
349}
350
351/// Parses every line of the configuration contained in `input` excluding invalid ones.
352pub fn parse(input: &str) -> Vec<Conf> {
353    let filter_map = |line| {
354        let mut v = ConfParser::parse(Rule::line, line)
355            .map_err(|err| error!("parsing error: {}", err))
356            .ok()?;
357        let rule = Some(v.next().unwrap().into_inner().next().unwrap())
358            .filter(|r| r.as_rule() == Rule::rule)?;
359        Conf::from_rule(rule)
360            .map_err(|err| error!("regex error: {}", err))
361            .ok()
362    };
363    input
364        .lines()
365        .filter_map(filter_map)
366        .chain(once(Conf::default()))
367        .collect()
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373
374    macro_rules! in_out_test {
375        ($($in:literal <===> $out:expr),* $(,)?) => {
376            const INPUT: &str = concat!($($in, "\n",)*);
377
378            fn outs() -> Vec<Conf> {
379                vec![$($out),*]
380            }
381        };
382    }
383
384    fn common_case(r: &str) -> Conf {
385        Conf {
386            stop: true,
387            envmatches: vec![],
388            filter: DeviceRegex {
389                envvar: None,
390                regex: regex(r),
391            }
392            .into(),
393            user: "root".into(),
394            group: "root".into(),
395            mode: 0o660,
396            on_creation: None,
397            command: None,
398        }
399    }
400
401    fn regex(s: &str) -> Regex {
402        Regex::new(s).unwrap()
403    }
404
405    in_out_test! {
406        "SYSTEM=usb;DEVTYPE=usb_device;.*\troot:root\t660  */opt/dev-bus-usb" <===> Conf {
407            envmatches: vec![
408                EnvMatch { envvar: "SYSTEM".into(), regex: regex("usb") },
409                EnvMatch { envvar: "DEVTYPE".into(), regex: regex("usb_device") },
410            ],
411            command: Command {
412                when: WhenToRun::Both,
413                path: "/opt/dev-bus-usb".into(),
414                args: vec![],
415            }.into(),
416            ..common_case(".*")
417        },
418        "$MODALIAS=.*\troot:root\t660 @modprobe -b \"$MODALIAS\" " <===> Conf {
419            filter: DeviceRegex {
420                envvar: Some("MODALIAS".into()),
421                regex: regex(".*"),
422            }.into(),
423            command: Command {
424                when: WhenToRun::After,
425                path: "modprobe".into(),
426                args: vec!["-b".into(), "\"$MODALIAS\"".into()],
427            }.into(),
428            ..common_case(".*")
429        },
430        "@42,17-125 root:root 660" <===> Conf {
431            filter: MajMin { maj: 42, min: 17, min2: Some(125) }.into(),
432            ..common_case(".*")
433        },
434        "@42,17     root:root 660" <===> Conf {
435            filter: MajMin { maj: 42, min: 17, min2: None }.into(),
436            ..common_case(".*")
437        },
438        "loop([0-9]+)\troot:disk 660\t>loop/%1" <===> Conf {
439            user: "root".into(), group: "disk".into(),
440            on_creation: OnCreation::SymLink("loop/%1".into()).into(),
441            ..common_case("loop([0-9]+)")
442        },
443        "SUBSYSTEM=usb;DEVTYPE=usb_device;.* root:root 660 */opt/mdev/helpers/dev-bus-usb" <===> Conf {
444            envmatches: vec![
445                EnvMatch { envvar: "SUBSYSTEM".into(), regex: regex("usb"), },
446                EnvMatch { envvar: "DEVTYPE".into(), regex: regex("usb_device"), },
447            ],
448            command: Command {
449                when: WhenToRun::Both,
450                path: "/opt/mdev/helpers/dev-bus-usb".into(),
451                args: vec![],
452            }.into(),
453            ..common_case(".*")
454        },
455        "-SUBSYSTEM=net;DEVPATH=.*/net/.*;.*\troot:root 600 @/opt/mdev/helpers/settle-nics --write-mactab" <===> Conf {
456            stop: false,
457            envmatches: vec![
458                EnvMatch { envvar: "SUBSYSTEM".into(), regex: regex("net"), },
459                EnvMatch { envvar: "DEVPATH".into(), regex: regex(".*/net/.*"), },
460            ],
461            mode: 0o600,
462            command: Command {
463                when: WhenToRun::After,
464                path: "/opt/mdev/helpers/settle-nics".into(),
465                args: vec!["--write-mactab".into()],
466            }.into(),
467            ..common_case(".*")
468        },
469        "SUBSYSTEM=sound;.*  root:audio 660 @/opt/mdev/helpers/sound-control" <===> Conf {
470            envmatches: vec![EnvMatch { envvar: "SUBSYSTEM".into(), regex: regex("sound"), }],
471            user: "root".into(), group: "audio".into(),
472            command: Command {
473                when: WhenToRun::After,
474                path: "/opt/mdev/helpers/sound-control".into(),
475                args: vec![],
476            }.into(),
477            ..common_case(".*")
478        },
479        "cpu([0-9]+)\troot:root 600\t=cpu/%1/cpuid" <===> Conf {
480            mode: 0o600,
481            on_creation: OnCreation::Move("cpu/%1/cpuid".into()).into(),
482            ..common_case("cpu([0-9]+)")
483        },
484        "SUBSYSTEM=input;.* root:input 660" <===> Conf {
485            envmatches: vec![EnvMatch { envvar: "SUBSYSTEM".into(), regex: regex("input"), }],
486            user: "root".into(), group: "input".into(),
487            ..common_case(".*")
488        },
489        "[0-9]+:[0-9]+:[0-9]+:[0-9]+ root:root 660 !" <===> Conf {
490            on_creation: OnCreation::Prevent.into(),
491            ..common_case("[0-9]+:[0-9]+:[0-9]+:[0-9]+")
492        },
493    }
494
495    #[test]
496    fn test_all() {
497        let conf = parse(INPUT);
498        let hardcoded = outs();
499
500        for (a, b) in conf.iter().zip(hardcoded.iter()) {
501            assert_eq!(a, b);
502        }
503
504        for (source, parsed) in INPUT.lines().zip(conf.iter().map(ToString::to_string)) {
505            let parts = source.split_whitespace().zip(parsed.split_whitespace());
506            for (source, parsed) in parts {
507                assert_eq!(source, parsed)
508            }
509        }
510    }
511}