sentinel_modsec/transformations/
pipeline.rs

1//! Transformation pipeline.
2
3use super::{create_transformation, Transformation};
4use crate::error::Result;
5use std::borrow::Cow;
6use std::sync::Arc;
7
8/// A pipeline of transformations to apply in sequence.
9#[derive(Clone)]
10pub struct TransformationPipeline {
11    transformations: Vec<Arc<dyn Transformation>>,
12}
13
14impl TransformationPipeline {
15    /// Create an empty pipeline.
16    pub fn new() -> Self {
17        Self {
18            transformations: Vec::new(),
19        }
20    }
21
22    /// Create a pipeline from transformation names.
23    pub fn from_names(names: &[String]) -> Result<Self> {
24        let mut transformations = Vec::new();
25
26        for name in names {
27            // Handle "none" specially - it clears the pipeline
28            if name.eq_ignore_ascii_case("none") {
29                transformations.clear();
30                continue;
31            }
32
33            let t = create_transformation(name)?;
34            transformations.push(t);
35        }
36
37        Ok(Self { transformations })
38    }
39
40    /// Add a transformation to the pipeline.
41    pub fn add(&mut self, transformation: Arc<dyn Transformation>) {
42        self.transformations.push(transformation);
43    }
44
45    /// Apply all transformations in sequence.
46    pub fn apply<'a>(&self, input: &'a str) -> Cow<'a, str> {
47        if self.transformations.is_empty() {
48            return Cow::Borrowed(input);
49        }
50
51        let mut current: Cow<str> = Cow::Borrowed(input);
52
53        for t in &self.transformations {
54            current = match current {
55                Cow::Borrowed(s) => t.transform(s),
56                Cow::Owned(s) => {
57                    let transformed = t.transform(&s);
58                    match transformed {
59                        Cow::Borrowed(_) => Cow::Owned(s),
60                        Cow::Owned(new) => Cow::Owned(new),
61                    }
62                }
63            };
64        }
65
66        current
67    }
68
69    /// Check if the pipeline is empty.
70    pub fn is_empty(&self) -> bool {
71        self.transformations.is_empty()
72    }
73
74    /// Get the number of transformations.
75    pub fn len(&self) -> usize {
76        self.transformations.len()
77    }
78}
79
80impl Default for TransformationPipeline {
81    fn default() -> Self {
82        Self::new()
83    }
84}
85
86impl std::fmt::Debug for TransformationPipeline {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        f.debug_struct("TransformationPipeline")
89            .field(
90                "transformations",
91                &self
92                    .transformations
93                    .iter()
94                    .map(|t| t.name())
95                    .collect::<Vec<_>>(),
96            )
97            .finish()
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_empty_pipeline() {
107        let pipeline = TransformationPipeline::new();
108        assert_eq!(pipeline.apply("hello"), "hello");
109    }
110
111    #[test]
112    fn test_single_transformation() {
113        let pipeline =
114            TransformationPipeline::from_names(&["lowercase".to_string()]).unwrap();
115        assert_eq!(pipeline.apply("HELLO"), "hello");
116    }
117
118    #[test]
119    fn test_multiple_transformations() {
120        let pipeline = TransformationPipeline::from_names(&[
121            "urlDecode".to_string(),
122            "lowercase".to_string(),
123        ])
124        .unwrap();
125        assert_eq!(pipeline.apply("HELLO%20WORLD"), "hello world");
126    }
127
128    #[test]
129    fn test_none_clears_pipeline() {
130        let pipeline = TransformationPipeline::from_names(&[
131            "lowercase".to_string(),
132            "none".to_string(),
133            "uppercase".to_string(),
134        ])
135        .unwrap();
136        // Only uppercase should be applied
137        assert_eq!(pipeline.apply("hello"), "HELLO");
138    }
139}