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)]
14pub struct Conf {
16 pub stop: bool,
18 pub envmatches: Vec<EnvMatch>,
19 pub filter: Filter,
21 pub user: String,
23 pub group: String,
25 pub mode: u32,
27 pub on_creation: Option<OnCreation>,
30 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)]
170pub 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)]
189pub struct DeviceRegex {
191 pub envvar: Option<String>,
192 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)]
220pub 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)]
239pub enum OnCreation {
241 Move(String),
243 SymLink(String),
246 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)]
264pub enum WhenToRun {
266 After,
268 Before,
270 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 pub when: WhenToRun,
290 pub path: String,
292 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
351pub 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}