1use crate::regex::Regex;
10use std::{
11 collections::BTreeMap,
12 env, fs, io,
13 process::{Command, Stdio},
14 str::FromStr,
15};
16use tracing::debug;
17
18#[derive(Debug, Default, PartialEq, Eq)]
20pub struct PlumbingRules {
21 rules: Vec<Rule>,
22 vars: BTreeMap<String, String>,
23}
24
25impl FromStr for PlumbingRules {
26 type Err = String;
27
28 fn from_str(s: &str) -> Result<Self, Self::Err> {
29 let mut prs = Self::default();
30
31 for raw_block in s.split("\n\n") {
32 let lines: Vec<_> = raw_block
33 .trim()
34 .lines()
35 .filter(|l| !l.starts_with('#'))
36 .collect();
37
38 let block = lines.join("\n");
39 if block.is_empty() {
40 continue;
41 }
42
43 match block.split_once(' ') {
45 Some((_, s)) if s.starts_with("=") => {
46 for line in block.lines() {
47 match line.split_once("=") {
48 Some((var, val)) => {
49 prs.vars.insert(
50 non_empty_string(var.trim(), line)?,
51 non_empty_string(val.trim(), line)?,
52 );
53 }
54 _ => return Err(format!("malformed line: {line:?}")),
55 }
56 }
57 }
58
59 _ => prs.rules.push(Rule::from_str(&block)?),
60 }
61 }
62
63 Ok(prs)
64 }
65}
66
67impl PlumbingRules {
68 pub fn try_load() -> Result<Self, String> {
70 let home = env::var("HOME").unwrap();
71
72 let s = match fs::read_to_string(format!("{home}/.ad/plumbing.rules")) {
73 Ok(s) => s,
74 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Self::default()),
75 Err(e) => return Err(format!("Unable to load plumbing rules: {e}")),
76 };
77
78 match Self::from_str(&s) {
79 Ok(cfg) => Ok(cfg),
80 Err(e) => Err(format!("Invalid plumbing rules: {e}")),
81 }
82 }
83
84 pub fn plumb(&mut self, msg: PlumbingMessage) -> Option<MatchOutcome> {
89 let vars = msg.initial_vars();
90 debug!("plumbing message: {msg:?}");
91
92 for (n, rule) in self.rules.iter_mut().enumerate() {
93 debug!("checking rule {n}");
94 let mut rule_vars = vars.clone();
95 if let Some(msg) = rule.try_match(msg.clone(), &mut rule_vars) {
96 debug!("rule matched");
97 return Some(msg);
98 }
99 }
100
101 debug!("no matching rules");
102 None
103 }
104}
105
106#[derive(Debug, Default, PartialEq, Eq, Clone)]
108pub struct PlumbingMessage {
109 pub src: Option<String>,
111 pub dst: Option<String>,
113 pub wdir: Option<String>,
115 pub attrs: BTreeMap<String, String>,
117 pub data: String,
119}
120
121impl PlumbingMessage {
122 fn initial_vars(&self) -> BTreeMap<String, String> {
123 let mut vars = BTreeMap::new();
124 if let Some(s) = self.src.clone() {
125 vars.insert("$src".to_string(), s);
126 }
127 if let Some(s) = self.dst.clone() {
128 vars.insert("$dst".to_string(), s);
129 }
130 if let Some(s) = self.wdir.clone() {
131 vars.insert("$wdir".to_string(), s);
132 }
133 vars.insert("$data".to_string(), self.data.clone());
134
135 vars
136 }
137}
138
139macro_rules! parse_field {
140 ($line:expr, $field_name:expr, $prefix:expr, $field:expr) => {
141 match ($line.strip_prefix($prefix), &mut $field) {
142 (Some(val), None) => {
143 $field = Some(val.to_string());
144 continue;
145 }
146 (Some(_), Some(_)) => return Err(format!("duplicate {} field", $field_name)),
147 (None, _) => (),
148 }
149 };
150}
151
152fn parse_attr_list(s: &str) -> Result<BTreeMap<String, String>, String> {
153 let mut attrs = BTreeMap::new();
154 for pair in s.split(' ') {
155 match pair.split_once('=') {
156 Some((k, v)) => {
157 if k.is_empty() || v.is_empty() {
158 return Err(format!("malformed attrs: {pair:?}"));
159 }
160 attrs.insert(k.to_string(), v.to_string());
161 }
162 None => return Err(format!("malformed attrs: {pair:?}")),
163 }
164 }
165
166 Ok(attrs)
167}
168
169impl FromStr for PlumbingMessage {
170 type Err = String;
171
172 fn from_str(s: &str) -> Result<Self, Self::Err> {
173 let mut msg = Self::default();
174 let mut it = s.lines();
175 let mut ndata = 0;
176
177 for line in &mut it {
178 parse_field!(line, "src", "src: ", msg.src);
179 parse_field!(line, "dst", "dst: ", msg.dst);
180 parse_field!(line, "wdir", "wdir: ", msg.wdir);
181
182 match (line.strip_prefix("attrs: "), msg.attrs.is_empty()) {
183 (Some(s), true) => {
184 msg.attrs = parse_attr_list(s)?;
185 continue;
186 }
187 (Some(_), false) => return Err("duplicate attrs field".to_string()),
188 (None, _) => (),
189 }
190
191 match line.strip_prefix("ndata: ") {
192 Some(n) => {
193 ndata = match n.parse() {
194 Ok(ndata) => ndata,
195 Err(_) => return Err(format!("invalid ndata field {n:?}")),
196 };
197 break;
198 }
199 None => return Err(format!("malformed message: {line:?}")),
200 }
201 }
202
203 if ndata > 0 {
204 let stripped_lines: Vec<&str> = it.collect();
205 let joined = stripped_lines.join("\n");
206 match joined.strip_prefix("data: ") {
207 Some(data) => msg.data = data.to_string(),
208 None => return Err("malformed message: missing data field".to_string()),
209 }
210 if msg.data.len() != ndata {
211 return Err(format!(
212 "malformed data. Expected {ndata} bytes but received {}: {:?}",
213 msg.data.len(),
214 msg.data
215 ));
216 }
217 }
218
219 Ok(msg)
220 }
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
224enum Pattern {
225 AddAttrs(BTreeMap<String, String>),
226 DelAttr(String),
227 DataFrom(String),
228 IsFile(String),
229 IsDir(String),
230 Is(Field, String),
231 Matches(Field, Regex),
232 Set(Field, String),
233}
234
235impl Pattern {
236 fn kind_str(&self) -> &'static str {
237 match self {
238 Self::AddAttrs(_) => "add-attrs",
239 Self::DelAttr(_) => "del-attr",
240 Self::DataFrom(_) => "data-from",
241 Self::IsFile(_) => "is-file",
242 Self::IsDir(_) => "is-dir",
243 Self::Is(_, _) => "is",
244 Self::Matches(_, _) => "matches",
245 Self::Set(_, _) => "set",
246 }
247 }
248
249 fn match_and_update(
250 &mut self,
251 msg: &mut PlumbingMessage,
252 vars: &mut BTreeMap<String, String>,
253 ) -> bool {
254 let apply_vars = |mut s: String| {
255 for (k, v) in vars.iter() {
256 s = s.replace(k, v);
257 }
258 s
259 };
260
261 let re_match_and_update =
262 |f: Field, re: &mut Regex, vars: &mut BTreeMap<String, String>| {
263 debug!("regex match against {}", f.name());
264 let opt = match f {
265 Field::Src => msg.src.as_ref(),
266 Field::Dst => msg.dst.as_ref(),
267 Field::Wdir => msg.wdir.as_ref(),
268 Field::Data => Some(&msg.data),
269 };
270 let s = match opt {
271 Some(s) => s,
272 None => {
273 debug!("unable to match against {} (not set in message)", f.name());
274 return false;
275 }
276 };
277
278 if let Some(m) = re.match_str(s) {
279 debug!("matched: updating vars");
280 vars.insert("$0".to_string(), m.str_match_text(s));
281 for n in 1..10 {
282 match m.str_submatch_text(n, s) {
283 Some(txt) => {
284 vars.insert(format!("${}", n), txt);
285 }
286 None => return true,
287 }
288 }
289 return true;
290 }
291
292 debug!("message data did not match the provided regex");
293 false
294 };
295
296 debug!("checking {} pattern", self.kind_str());
297 match self {
298 Self::AddAttrs(attrs) => {
299 debug!("adding attrs: {attrs:?}");
300 msg.attrs.extend(
301 attrs
302 .clone()
303 .into_iter()
304 .map(|(k, v)| (apply_vars(k), apply_vars(v))),
305 );
306 }
307
308 Self::DelAttr(a) => {
309 debug!("removing attr: {a}");
310 msg.attrs.remove(a);
311 }
312
313 Self::IsFile(s) => {
314 debug!("checking if {s:?} is a file");
315 let path = apply_vars(s.clone());
316 match fs::metadata(&path) {
317 Ok(m) => {
318 if m.is_file() {
319 debug!("{path:?} exists and is a file");
320 vars.insert("$file".to_string(), path);
321 } else {
322 debug!("{path:?} exists but is not a file");
323 return false;
324 }
325 }
326
327 Err(e) => {
328 debug!("unable to check {path:?}: {e}");
329 return false;
330 }
331 }
332 }
333
334 Self::IsDir(s) => {
335 debug!("checking if {s:?} is a directory");
336 let path = apply_vars(s.clone());
337 match fs::metadata(&path) {
338 Ok(m) => {
339 if m.is_dir() {
340 debug!("{path:?} exists and is a directory");
341 vars.insert("$dir".to_string(), path);
342 } else {
343 debug!("{path:?} exists but is not a directory");
344 return false;
345 }
346 }
347
348 Err(e) => {
349 debug!("unable to check {path:?}: {e}");
350 return false;
351 }
352 }
353 }
354
355 Self::Is(Field::Src, s) => {
356 let res = msg.src.as_ref() == Some(s);
357 debug!("checking src == {s:?}: {res}");
358 return res;
359 }
360 Self::Is(Field::Dst, s) => {
361 let res = msg.dst.as_ref() == Some(s);
362 debug!("checking dst == {s:?}: {res}");
363 return res;
364 }
365 Self::Is(Field::Wdir, s) => {
366 let res = msg.wdir.as_ref() == Some(s);
367 debug!("checking wdir == {s:?}: {res}");
368 return res;
369 }
370 Self::Is(Field::Data, s) => {
371 let res = &msg.data == s;
372 debug!("checking data == {s:?}: {res}");
373 return res;
374 }
375
376 Self::Set(Field::Src, s) => {
377 let updated = apply_vars(s.clone());
378 debug!("setting src to {updated:?}");
379 msg.src = Some(updated.clone());
380 vars.insert("$src".to_string(), updated);
381 }
382 Self::Set(Field::Dst, s) => {
383 let updated = apply_vars(s.clone());
384 debug!("setting dst to {updated:?}");
385 msg.dst = Some(updated.clone());
386 vars.insert("$dst".to_string(), updated);
387 }
388 Self::Set(Field::Wdir, s) => {
389 let updated = apply_vars(s.clone());
390 debug!("setting wdir to {updated:?}");
391 msg.wdir = Some(updated.clone());
392 vars.insert("$wdir".to_string(), updated);
393 }
394 Self::Set(Field::Data, s) => {
395 let updated = apply_vars(s.clone());
396 debug!("setting data to {updated:?}");
397 msg.data = updated.clone();
398 vars.insert("$data".to_string(), updated);
399 }
400
401 Self::Matches(f, re) => return re_match_and_update(*f, re, vars),
402 Self::DataFrom(cmd) => {
403 debug!("running {cmd:?} to set message data");
404 let mut command = Command::new("sh");
405 command
406 .args(["-c", apply_vars(cmd.clone()).as_str()])
407 .stderr(Stdio::null());
408 let output = match command.output() {
409 Ok(output) => output,
410 Err(_) => return false,
411 };
412 msg.data = String::from_utf8(output.stdout).unwrap_or_default();
413 }
414 }
415
416 true
417 }
418}
419
420#[derive(Debug, Clone, Copy, PartialEq, Eq)]
421enum Field {
422 Src,
423 Dst,
424 Wdir,
425 Data,
426}
427
428impl Field {
429 fn name(&self) -> &'static str {
430 match self {
431 Self::Src => "src",
432 Self::Dst => "dst",
433 Self::Wdir => "wdir",
434 Self::Data => "data",
435 }
436 }
437}
438
439impl FromStr for Field {
440 type Err = String;
441
442 fn from_str(s: &str) -> Result<Self, Self::Err> {
443 match s {
444 "src" => Ok(Self::Src),
445 "dst" => Ok(Self::Dst),
446 "wdir" => Ok(Self::Wdir),
447 "data" => Ok(Self::Data),
448 s => Err(format!("unknown field: {s:?}")),
449 }
450 }
451}
452
453#[derive(Debug, Clone, PartialEq, Eq)]
454enum Action {
455 To(String),
456 Start(String),
457}
458
459#[derive(Debug, Clone, PartialEq, Eq)]
461pub enum MatchOutcome {
462 Message(PlumbingMessage),
464 Run(String),
466}
467
468#[derive(Debug, Default, Clone, PartialEq, Eq)]
472pub struct Rule {
473 patterns: Vec<Pattern>,
474 actions: Vec<Action>,
475}
476
477impl FromStr for Rule {
478 type Err = String;
479
480 fn from_str(s: &str) -> Result<Self, Self::Err> {
481 let mut rule = Self::default();
482
483 for line in s.lines() {
484 if line.starts_with('#') {
485 continue;
486 }
487
488 if let Some(s) = line.strip_prefix("attr add ") {
490 rule.patterns.push(Pattern::AddAttrs(parse_attr_list(s)?));
491 } else if let Some(s) = line.strip_prefix("attr delete ") {
492 rule.patterns
493 .push(Pattern::DelAttr(non_empty_string(s, line)?));
494 } else if let Some(s) = line.strip_prefix("arg isfile ") {
495 rule.patterns
496 .push(Pattern::IsFile(non_empty_string(s, line)?));
497 } else if let Some(s) = line.strip_prefix("arg isdir ") {
498 rule.patterns
499 .push(Pattern::IsDir(non_empty_string(s, line)?));
500 } else if let Some(s) = line.strip_prefix("data from ") {
501 rule.patterns
502 .push(Pattern::DataFrom(non_empty_string(s, line)?));
503 } else if let Some(s) = line.strip_prefix("plumb to ") {
504 rule.actions.push(Action::To(non_empty_string(s, line)?));
505 } else if let Some(s) = line.strip_prefix("plumb start ") {
506 rule.actions.push(Action::Start(non_empty_string(s, line)?));
507 } else {
508 let (field, rest) = line
510 .split_once(' ')
511 .ok_or_else(|| format!("malformed rule line: {line}"))?;
512 let field = Field::from_str(field)?;
513 let (op, value) = rest
514 .split_once(' ')
515 .ok_or_else(|| format!("malformed rule line: {line}"))?;
516 let value = non_empty_string(value, line)?;
517
518 match op {
519 "is" => rule.patterns.push(Pattern::Is(field, value)),
520 "set" => rule.patterns.push(Pattern::Set(field, value)),
521 "matches" => rule.patterns.push(Pattern::Matches(
522 field,
523 Regex::compile(&value)
524 .map_err(|e| format!("malformed regex ({e:?}): {value}"))?,
525 )),
526 _ => return Err(format!("unknown rule operation: {op}")),
527 }
528 }
529 }
530
531 if rule.patterns.is_empty() {
532 Err("rule without patterns".to_string())
533 } else if rule.actions.is_empty() {
534 Err("rule without actions".to_string())
535 } else {
536 Ok(rule)
537 }
538 }
539}
540
541impl Rule {
542 fn try_match(
543 &mut self,
544 mut msg: PlumbingMessage,
545 vars: &mut BTreeMap<String, String>,
546 ) -> Option<MatchOutcome> {
547 for p in self.patterns.iter_mut() {
548 if !p.match_and_update(&mut msg, vars) {
549 debug!("pattern failed to match");
550 return None;
551 }
552 }
553
554 for a in self.actions.iter() {
555 match a {
556 Action::To(port) if port == "edit" => return Some(MatchOutcome::Message(msg)),
558 Action::Start(cmd) => {
559 let mut s = cmd.clone();
560 for (k, v) in vars.iter() {
561 s = s.replace(k, v);
562 }
563
564 return Some(MatchOutcome::Run(s));
565 }
566 _ => continue,
567 }
568 }
569
570 None
571 }
572}
573
574fn non_empty_string(s: &str, line: &str) -> Result<String, String> {
575 if s.is_empty() {
576 Err(format!("malformed rule line: {line:?}"))
577 } else {
578 Ok(s.to_string())
579 }
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585 use simple_test_case::dir_cases;
586
587 #[test]
588 fn parse_default_rules_works() {
589 let rules = PlumbingRules::from_str(include_str!("../data/plumbing.rules"));
590 assert!(rules.is_ok(), "{rules:?}");
591 }
592
593 #[test]
594 fn happy_path_plumb_works() {
595 let mut rules = PlumbingRules::from_str(include_str!("../data/plumbing.rules")).unwrap();
596 let m = PlumbingMessage {
597 data: "data/plumbing.rules:5:17:".to_string(),
598 ..Default::default()
599 };
600
601 let outcome = rules.plumb(m);
602 let m = match outcome {
603 Some(MatchOutcome::Message(m)) => m,
604 _ => panic!("expected message, got {outcome:?}"),
605 };
606
607 let expected = PlumbingMessage {
608 data: "data/plumbing.rules".to_string(),
609 attrs: [("addr".to_string(), "5:17".to_string())]
610 .into_iter()
611 .collect(),
612 ..Default::default()
613 };
614
615 assert_eq!(m, expected);
616 }
617
618 #[test]
619 fn parse_message_works() {
620 let m = PlumbingMessage::from_str(include_str!(
621 "../data/plumbing_tests/messages/valid/all-fields.txt"
622 ))
623 .unwrap();
624
625 let expected = PlumbingMessage {
626 src: Some("bash".to_string()),
627 dst: Some("ad".to_string()),
628 wdir: Some("/home/foo/bar".to_string()),
629 attrs: [
630 ("a".to_string(), "b".to_string()),
631 ("c".to_string(), "d".to_string()),
632 ]
633 .into_iter()
634 .collect(),
635 data: "hello, world!".to_string(),
636 };
637
638 assert_eq!(m, expected);
639 }
640
641 #[dir_cases("data/plumbing_tests/messages/valid")]
642 #[test]
643 fn parse_valid_message(_: &str, content: &str) {
644 let res = PlumbingMessage::from_str(content);
645 assert!(res.is_ok(), "{res:?}");
646 }
647
648 #[dir_cases("data/plumbing_tests/messages/invalid")]
649 #[test]
650 fn parse_invalid_message(_: &str, content: &str) {
651 let res = PlumbingMessage::from_str(content);
652 assert!(res.is_err(), "{res:?}");
653 }
654
655 #[test]
656 fn parse_rule_works() {
657 let m = Rule::from_str(include_str!(
658 "../data/plumbing_tests/rules/valid/everything.txt"
659 ))
660 .unwrap();
661
662 let expected = Rule {
663 patterns: vec![
664 Pattern::AddAttrs([("a".to_string(), "b".to_string())].into_iter().collect()),
665 Pattern::DelAttr("c".to_string()),
666 Pattern::IsFile("$data".to_string()),
667 Pattern::IsDir("/var/lib".to_string()),
668 Pattern::Is(Field::Src, "ad".to_string()),
669 Pattern::Is(Field::Dst, "editor".to_string()),
670 Pattern::Set(Field::Wdir, "/foo/bar".to_string()),
671 Pattern::Matches(Field::Data, Regex::compile(r#"(.+):(\d+):(\d+):"#).unwrap()),
672 Pattern::Set(Field::Data, "$1:$2,$3".to_string()),
673 ],
674 actions: vec![
675 Action::To("editor".to_string()),
676 Action::Start("ad $file".to_string()),
677 ],
678 };
679
680 assert_eq!(m, expected);
681 }
682
683 #[dir_cases("data/plumbing_tests/rules/valid")]
684 #[test]
685 fn parse_valid_rule(_: &str, content: &str) {
686 let res = Rule::from_str(content);
687 assert!(res.is_ok(), "{res:?}");
688 }
689
690 #[dir_cases("data/plumbing_tests/rules/invalid")]
691 #[test]
692 fn parse_invalid_rule(_: &str, content: &str) {
693 let res = Rule::from_str(content);
694 assert!(res.is_err(), "{res:?}");
695 }
696}