Provide a correct implementation of user-configured match line parsing

This commit is contained in:
ppom 2025-11-27 12:00:00 +01:00
commit da5c3afefb
No known key found for this signature in database
2 changed files with 244 additions and 1 deletions

View file

@ -53,6 +53,17 @@
//! ERROR plugin test: Freeeee errrooooorrr
//! ```
//!
//! ## Helpers
//!
//! Those helpers permits to easily maintain similar configuration interfaces accross plugins:
//!
//! - [`line::PatternLine`], to permit users to use templated lines (ie. "\<ip> bad password").
//! - [`time::parse_duration`] to parse durations (ie. "6h", "3 days").
//!
//! Those helpers solve common issues for reaction plugins:
//!
//! - The [`shutdown`] module provides structures to ease the quitting process when having multiple tokio tasks.
//!
//! ## Starting template
//!
//! ```bash
@ -101,6 +112,7 @@ use serde::{Deserialize, Serialize};
use serde_json::{Number, Value as JValue};
use tokio::io::{stdin, stdout};
pub mod line;
pub mod shutdown;
pub mod time;
@ -248,7 +260,7 @@ impl Into<JValue> for Value {
pub type Line = (String, DateTime<Local>);
#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct StreamImpl {
pub stream: rch::mpsc::Receiver<Line>,
/// Whether this stream works standalone, or if it needs other streams to be fed.

View file

@ -0,0 +1,231 @@
#[derive(Debug, PartialEq, Eq)]
enum SendItem {
Index(usize),
Str(String),
}
impl SendItem {
fn min_size(&self) -> usize {
match self {
Self::Index(_) => 0,
Self::Str(s) => s.len(),
}
}
}
/// Helper struct that permits to transform a template line with patterns into an instantiated line from a match.
///
/// Useful when you permit the user to reconstruct lines from an action, like in reaction's native actions and in the virtual plugin:
/// ```yaml
/// actions:
/// native:
/// cmd: ["iptables", "...", "<ip>"]
///
/// virtual:
/// type: virtual
/// options:
/// send: "<ip>: bad password on user <user>"
/// to: "my_virtual_stream"
/// ```
///
/// Usage example:
/// ```
/// # use reaction_plugin::line::PatternLine;
/// #
/// let template = "<ip>: bad password on user <user>".to_string();
/// let patterns = vec!["ip".to_string(), "user".to_string()];
/// let pattern_line = PatternLine::new(template, patterns);
///
/// assert_eq!(
/// pattern_line.line(vec!["1.2.3.4".to_string(), "root".to_string()]),
/// "1.2.3.4: bad password on user root".to_string(),
/// );
/// ```
///
/// You can find full examples in those plugins:
/// `reaction-plugin-virtual`,
/// `reaction-plugin-cluster`.
///
#[derive(Debug)]
pub struct PatternLine {
line: Vec<SendItem>,
min_size: usize,
}
impl PatternLine {
/// Construct [`PatternLine`] from a template line and the list of patterns of the underlying [Filter](https://reaction.ppom.me/reference.html#filter).
///
/// This list of patterns comes from [`PluginInfo::action_impl`].
pub fn new(template: String, patterns: Vec<String>) -> Self {
let line = Self::_from(patterns, Vec::from([SendItem::Str(template)]));
Self {
min_size: line.iter().map(SendItem::min_size).sum(),
line,
}
}
fn _from(mut patterns: Vec<String>, acc: Vec<SendItem>) -> Vec<SendItem> {
match patterns.pop() {
None => acc,
Some(pattern) => {
let enclosed_pattern = format!("<{pattern}>");
let acc = acc
.into_iter()
.flat_map(|item| match &item {
SendItem::Index(_) => vec![item],
SendItem::Str(str) => match str.find(&enclosed_pattern) {
Some(i) => {
let pattern_index = patterns.len();
let mut ret = vec![];
let (left, mid) = str.split_at(i);
if !left.is_empty() {
ret.push(SendItem::Str(left.into()))
}
ret.push(SendItem::Index(pattern_index));
if mid.len() > enclosed_pattern.len() {
let (_, right) = mid.split_at(enclosed_pattern.len());
ret.push(SendItem::Str(right.into()))
}
ret
}
None => vec![item],
},
})
.collect();
Self::_from(patterns, acc)
}
}
}
pub fn line(&self, match_: Vec<String>) -> String {
let mut res = String::with_capacity(self.min_size);
for item in &self.line {
match item {
SendItem::Index(i) => {
if let Some(element) = match_.get(*i) {
res.push_str(element);
}
}
SendItem::Str(str) => res.push_str(str),
}
}
res
}
}
#[cfg(test)]
mod tests {
use crate::line::{PatternLine, SendItem};
#[test]
fn line_0_pattern() {
let msg = "my message".to_string();
let line = PatternLine::new(msg.clone(), vec![]);
assert_eq!(line.line, vec![SendItem::Str(msg.clone())]);
assert_eq!(line.min_size, msg.len());
assert_eq!(line.line(vec![]), msg.clone());
}
#[test]
fn line_1_pattern() {
let patterns = vec![
"ignored".into(),
"oh".into(),
"ignored".into(),
"my".into(),
"test".into(),
];
let matches = vec!["yay", "oh", "my", "test", "<oh>", "<my>", "<test>"];
let tests = [
(
"<oh> my test",
1,
vec![SendItem::Index(1), SendItem::Str(" my test".into())],
vec![
("yay", "yay my test"),
("oh", "oh my test"),
("my", "my my test"),
("test", "test my test"),
("<oh>", "<oh> my test"),
("<my>", "<my> my test"),
("<test>", "<test> my test"),
],
),
(
"oh <my> test",
3,
vec![
SendItem::Str("oh ".into()),
SendItem::Index(3),
SendItem::Str(" test".into()),
],
vec![
("yay", "oh yay test"),
("oh", "oh oh test"),
("my", "oh my test"),
("test", "oh test test"),
("<oh>", "oh <oh> test"),
("<my>", "oh <my> test"),
("<test>", "oh <test> test"),
],
),
(
"oh my <test>",
4,
vec![SendItem::Str("oh my ".into()), SendItem::Index(4)],
vec![
("yay", "oh my yay"),
("oh", "oh my oh"),
("my", "oh my my"),
("test", "oh my test"),
("<oh>", "oh my <oh>"),
("<my>", "oh my <my>"),
("<test>", "oh my <test>"),
],
),
];
for (msg, index, expected_pl, lines) in tests {
let pattern_line = PatternLine::new(msg.to_string(), patterns.clone());
assert_eq!(pattern_line.line, expected_pl);
for (match_element, line) in lines {
for match_default in &matches {
let mut match_ = vec![
match_default.to_string(),
match_default.to_string(),
match_default.to_string(),
match_default.to_string(),
match_default.to_string(),
];
match_[index] = match_element.to_string();
assert_eq!(
pattern_line.line(match_.clone()),
line,
"match: {match_:?}, pattern_line: {pattern_line:?}"
);
}
}
}
}
#[test]
fn line_2_pattern() {
let pattern_line = PatternLine::new("<a> ; <b>".into(), vec!["a".into(), "b".into()]);
let matches = ["a", "b", "ab", "<a>", "<b>"];
for a in &matches {
for b in &matches {
assert_eq!(
pattern_line.line(vec![a.to_string(), b.to_string()]),
format!("{a} ; {b}"),
);
}
}
}
}