Skip to main content

reaction_plugin/
line.rs

1//! Helper module that permits to use templated lines (ie. `bad password for <ip>`), like in Stream's and Action's `cmd`.
2//!
3//! Corresponding reaction core settings:
4//! - [Stream's `cmd`](https://reaction.ppom.me/reference.html#cmd)
5//! - [Action's `cmd`](https://reaction.ppom.me/reference.html#cmd-1)
6//!
7#[derive(Debug, PartialEq, Eq)]
8enum SendItem {
9    Index(usize),
10    Str(String),
11}
12
13impl SendItem {
14    fn min_size(&self) -> usize {
15        match self {
16            Self::Index(_) => 0,
17            Self::Str(s) => s.len(),
18        }
19    }
20}
21
22/// Helper struct that permits to transform a template line with patterns into an instantiated line from a match.
23///
24/// Useful when you permit the user to reconstruct lines from an action, like in reaction's native actions and in the virtual plugin:
25/// ```yaml
26/// actions:
27///   native:
28///     cmd: ["iptables", "...", "<ip>"]
29///
30///   virtual:
31///     type: virtual
32///     options:
33///       send: "<ip>: bad password on user <user>"
34///       to: "my_virtual_stream"
35/// ```
36///
37/// Usage example:
38/// ```
39/// # use reaction_plugin::line::PatternLine;
40/// #
41/// let template = "<ip>: bad password on user <user>".to_string();
42/// let patterns = vec!["ip".to_string(), "user".to_string()];
43/// let pattern_line = PatternLine::new(template, patterns);
44///
45/// assert_eq!(
46///   pattern_line.line(vec!["1.2.3.4".to_string(), "root".to_string()]),
47///   "1.2.3.4: bad password on user root".to_string(),
48/// );
49/// ```
50///
51/// You can find full examples in those plugins:
52/// `reaction-plugin-virtual`,
53/// `reaction-plugin-cluster`.
54///
55#[derive(Debug)]
56pub struct PatternLine {
57    line: Vec<SendItem>,
58    min_size: usize,
59}
60
61impl PatternLine {
62    /// Construct [`PatternLine`] from a template line and the list of patterns of the underlying [Filter](https://reaction.ppom.me/reference.html#filter).
63    ///
64    /// This list of patterns comes from [`super::ActionConfig`].
65    pub fn new(template: String, patterns: Vec<String>) -> Self {
66        let line = Self::_from(patterns, Vec::from([SendItem::Str(template)]));
67        Self {
68            min_size: line.iter().map(SendItem::min_size).sum(),
69            line,
70        }
71    }
72    fn _from(mut patterns: Vec<String>, acc: Vec<SendItem>) -> Vec<SendItem> {
73        match patterns.pop() {
74            None => acc,
75            Some(pattern) => {
76                let enclosed_pattern = format!("<{pattern}>");
77                let acc = acc
78                    .into_iter()
79                    .flat_map(|item| match &item {
80                        SendItem::Index(_) => vec![item],
81                        SendItem::Str(str) => match str.find(&enclosed_pattern) {
82                            Some(i) => {
83                                let pattern_index = patterns.len();
84                                let mut ret = vec![];
85
86                                let (left, mid) = str.split_at(i);
87                                if !left.is_empty() {
88                                    ret.push(SendItem::Str(left.into()))
89                                }
90
91                                ret.push(SendItem::Index(pattern_index));
92
93                                if mid.len() > enclosed_pattern.len() {
94                                    let (_, right) = mid.split_at(enclosed_pattern.len());
95                                    ret.push(SendItem::Str(right.into()))
96                                }
97
98                                ret
99                            }
100                            None => vec![item],
101                        },
102                    })
103                    .collect();
104                Self::_from(patterns, acc)
105            }
106        }
107    }
108
109    pub fn line(&self, match_: Vec<String>) -> String {
110        let mut res = String::with_capacity(self.min_size);
111        for item in &self.line {
112            match item {
113                SendItem::Index(i) => {
114                    if let Some(element) = match_.get(*i) {
115                        res.push_str(element);
116                    }
117                }
118                SendItem::Str(str) => res.push_str(str),
119            }
120        }
121        res
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use crate::line::{PatternLine, SendItem};
128
129    #[test]
130    fn line_0_pattern() {
131        let msg = "my message".to_string();
132        let line = PatternLine::new(msg.clone(), vec![]);
133        assert_eq!(line.line, vec![SendItem::Str(msg.clone())]);
134        assert_eq!(line.min_size, msg.len());
135        assert_eq!(line.line(vec![]), msg.clone());
136    }
137
138    #[test]
139    fn line_1_pattern() {
140        let patterns = vec![
141            "ignored".into(),
142            "oh".into(),
143            "ignored".into(),
144            "my".into(),
145            "test".into(),
146        ];
147
148        let matches = vec!["yay", "oh", "my", "test", "<oh>", "<my>", "<test>"];
149
150        let tests = [
151            (
152                "<oh> my test",
153                1,
154                vec![SendItem::Index(1), SendItem::Str(" my test".into())],
155                vec![
156                    ("yay", "yay my test"),
157                    ("oh", "oh my test"),
158                    ("my", "my my test"),
159                    ("test", "test my test"),
160                    ("<oh>", "<oh> my test"),
161                    ("<my>", "<my> my test"),
162                    ("<test>", "<test> my test"),
163                ],
164            ),
165            (
166                "oh <my> test",
167                3,
168                vec![
169                    SendItem::Str("oh ".into()),
170                    SendItem::Index(3),
171                    SendItem::Str(" test".into()),
172                ],
173                vec![
174                    ("yay", "oh yay test"),
175                    ("oh", "oh oh test"),
176                    ("my", "oh my test"),
177                    ("test", "oh test test"),
178                    ("<oh>", "oh <oh> test"),
179                    ("<my>", "oh <my> test"),
180                    ("<test>", "oh <test> test"),
181                ],
182            ),
183            (
184                "oh my <test>",
185                4,
186                vec![SendItem::Str("oh my ".into()), SendItem::Index(4)],
187                vec![
188                    ("yay", "oh my yay"),
189                    ("oh", "oh my oh"),
190                    ("my", "oh my my"),
191                    ("test", "oh my test"),
192                    ("<oh>", "oh my <oh>"),
193                    ("<my>", "oh my <my>"),
194                    ("<test>", "oh my <test>"),
195                ],
196            ),
197        ];
198
199        for (msg, index, expected_pl, lines) in tests {
200            let pattern_line = PatternLine::new(msg.to_string(), patterns.clone());
201            assert_eq!(pattern_line.line, expected_pl);
202
203            for (match_element, line) in lines {
204                for match_default in &matches {
205                    let mut match_ = vec![
206                        match_default.to_string(),
207                        match_default.to_string(),
208                        match_default.to_string(),
209                        match_default.to_string(),
210                        match_default.to_string(),
211                    ];
212                    match_[index] = match_element.to_string();
213                    assert_eq!(
214                        pattern_line.line(match_.clone()),
215                        line,
216                        "match: {match_:?}, pattern_line: {pattern_line:?}"
217                    );
218                }
219            }
220        }
221    }
222
223    #[test]
224    fn line_2_pattern() {
225        let pattern_line = PatternLine::new("<a> ; <b>".into(), vec!["a".into(), "b".into()]);
226
227        let matches = ["a", "b", "ab", "<a>", "<b>"];
228        for a in &matches {
229            for b in &matches {
230                assert_eq!(
231                    pattern_line.line(vec![a.to_string(), b.to_string()]),
232                    format!("{a} ; {b}"),
233                );
234            }
235        }
236    }
237}