Skip to main content

bytecode_filter/
loader.rs

1//! Filter file loading utilities.
2//!
3//! Provides functions to load and compile filters from files.
4//!
5//! ## Filter File Format
6//!
7//! Filter files support comments, configuration directives, and the filter expression.
8//!
9//! ```text
10//! # Comments start with #
11//!
12//! # Delimiter directive (optional, defaults to ";;;")
13//! @delimiter = ";;;"
14//!
15//! # Field mappings
16//! @field MESSAGE_TYPE = 1
17//! @field MESSAGE_SUB_TYPE = 2
18//! @field REQUEST_HEADERS = 11
19//!
20//! # The filter expression (everything else)
21//! MESSAGE_TYPE == "2" AND MESSAGE_SUB_TYPE == "11"
22//! ```
23
24use std::fs;
25use std::path::Path;
26
27use crate::compiler::{compile, CompileError};
28use crate::parser::ParserConfig;
29use crate::vm::CompiledFilter;
30
31/// Error type for filter loading.
32#[derive(Debug)]
33pub enum LoadError {
34    /// IO error reading the file.
35    Io(std::io::Error),
36    /// Compilation error.
37    Compile(CompileError),
38    /// Invalid directive in filter file.
39    InvalidDirective(String),
40    /// Invalid field index.
41    InvalidFieldIndex(String),
42}
43
44impl std::fmt::Display for LoadError {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        match self {
47            LoadError::Io(e) => write!(f, "IO error: {}", e),
48            LoadError::Compile(e) => write!(f, "Compile error: {}", e),
49            LoadError::InvalidDirective(s) => write!(f, "Invalid directive: {}", s),
50            LoadError::InvalidFieldIndex(s) => write!(f, "Invalid field index: {}", s),
51        }
52    }
53}
54
55impl std::error::Error for LoadError {
56    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
57        match self {
58            LoadError::Io(e) => Some(e),
59            LoadError::Compile(e) => Some(e),
60            _ => None,
61        }
62    }
63}
64
65impl From<std::io::Error> for LoadError {
66    fn from(e: std::io::Error) -> Self {
67        LoadError::Io(e)
68    }
69}
70
71impl From<CompileError> for LoadError {
72    fn from(e: CompileError) -> Self {
73        LoadError::Compile(e)
74    }
75}
76
77/// Load and compile a filter from a file.
78///
79/// The file can contain:
80/// - Comments (lines starting with `#`)
81/// - Delimiter directive: `@delimiter = ";;;"`
82/// - Field mappings: `@field FIELD_NAME = index`
83/// - The filter expression
84///
85/// If the file contains `@delimiter` or `@field` directives, they override
86/// the provided config.
87///
88/// # Arguments
89/// * `path` - Path to the filter file
90/// * `config` - Base parser configuration (can be overridden by file directives)
91///
92/// # Returns
93/// A compiled filter ready for evaluation.
94///
95/// # Example
96/// ```no_run
97/// use bytecode_filter::{load_filter_file, ParserConfig};
98///
99/// let config = ParserConfig::default();
100/// let filter = load_filter_file("filters/my.filter", &config).unwrap();
101/// ```
102///
103/// # Errors
104/// Returns `LoadError` if the file cannot be read or the filter fails to compile.
105pub fn load_filter_file(
106    path: impl AsRef<Path>,
107    config: &ParserConfig,
108) -> Result<CompiledFilter, LoadError> {
109    let content = fs::read_to_string(path)?;
110    load_filter_string(&content, config)
111}
112
113/// Load and compile a filter from a string.
114///
115/// Supports the same format as `load_filter_file`.
116///
117/// # Arguments
118/// * `content` - The filter source string
119/// * `config` - Base parser configuration (can be overridden by directives)
120///
121/// # Returns
122/// A compiled filter ready for evaluation.
123///
124/// # Errors
125/// Returns `LoadError` if parsing or compilation fails.
126pub fn load_filter_string(
127    content: &str,
128    config: &ParserConfig,
129) -> Result<CompiledFilter, LoadError> {
130    let mut local_config = config.clone();
131    let mut expression_lines = Vec::new();
132
133    for line in content.lines() {
134        let trimmed = line.trim();
135
136        // Skip empty lines and comments
137        if trimmed.is_empty() || trimmed.starts_with('#') {
138            continue;
139        }
140
141        // Parse directives
142        if trimmed.starts_with('@') {
143            parse_directive(trimmed, &mut local_config)?;
144        } else {
145            // Regular expression line
146            expression_lines.push(trimmed);
147        }
148    }
149
150    let expression = expression_lines.join(" ");
151    Ok(compile(&expression, &local_config)?)
152}
153
154/// Parse a directive line and update the config.
155fn parse_directive(line: &str, config: &mut ParserConfig) -> Result<(), LoadError> {
156    let line = line.trim_start_matches('@').trim();
157
158    if line.starts_with("delimiter") {
159        // @delimiter = ";;;"
160        let parts: Vec<&str> = line.splitn(2, '=').collect();
161        if parts.len() != 2 {
162            return Err(LoadError::InvalidDirective(format!(
163                "Invalid delimiter directive: {}",
164                line
165            )));
166        }
167        let value = parts[1].trim();
168        // Remove quotes and handle escape sequences
169        let delimiter = value
170            .trim_matches('"')
171            .trim_matches('\'')
172            .replace("\\t", "\t")
173            .replace("\\n", "\n")
174            .replace("\\r", "\r");
175        config.delimiter = delimiter.into_bytes();
176    } else if line.starts_with("field") {
177        // @field FIELD_NAME = index
178        let rest = line.trim_start_matches("field").trim();
179        let parts: Vec<&str> = rest.splitn(2, '=').collect();
180        if parts.len() != 2 {
181            return Err(LoadError::InvalidDirective(format!(
182                "Invalid field directive: {}",
183                line
184            )));
185        }
186        let field_name = parts[0].trim().to_string();
187        let index_str = parts[1].trim();
188        let index: u8 = index_str.parse().map_err(|_| {
189            LoadError::InvalidFieldIndex(format!(
190                "Invalid field index '{}' for field '{}'",
191                index_str, field_name
192            ))
193        })?;
194        config.fields.insert(field_name, index);
195    } else {
196        return Err(LoadError::InvalidDirective(format!(
197            "Unknown directive: @{}",
198            line
199        )));
200    }
201
202    Ok(())
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use bytes::Bytes;
209
210    fn test_config() -> ParserConfig {
211        let mut config = ParserConfig::default();
212        config.add_field("LEVEL", 0);
213        config.add_field("CODE", 1);
214        config.add_field("BODY", 2);
215        config
216    }
217
218    #[test]
219    fn test_load_filter_string_with_comments() {
220        let content = r#"
221            # This is a comment
222            LEVEL == "error"
223            # Another comment
224            AND CODE == "500"
225        "#;
226
227        let config = test_config();
228        let filter = load_filter_string(content, &config).unwrap();
229
230        assert!(filter.evaluate(Bytes::from("error;;;500;;;body")));
231        assert!(!filter.evaluate(Bytes::from("info;;;500;;;body")));
232    }
233
234    #[test]
235    fn test_load_filter_string_empty_lines() {
236        let content = r#"
237            LEVEL == "error"
238
239            OR
240
241            LEVEL == "warn"
242        "#;
243
244        let config = test_config();
245        let filter = load_filter_string(content, &config).unwrap();
246
247        assert!(filter.evaluate(Bytes::from("error;;;500;;;body")));
248        assert!(filter.evaluate(Bytes::from("warn;;;500;;;body")));
249        assert!(!filter.evaluate(Bytes::from("info;;;500;;;body")));
250    }
251
252    #[test]
253    fn test_load_filter_with_directives() {
254        let content = r#"
255            # Test filter with embedded config
256            @delimiter = ";;;"
257            @field STATUS = 0
258            @field CODE = 1
259
260            STATUS == "ok" AND CODE == "200"
261        "#;
262
263        let config = ParserConfig::default();
264        let filter = load_filter_string(content, &config).unwrap();
265
266        assert!(filter.evaluate(Bytes::from("ok;;;200;;;body")));
267        assert!(!filter.evaluate(Bytes::from("err;;;200;;;body")));
268    }
269
270    #[test]
271    fn test_load_filter_with_pipe_delimiter() {
272        let content = r#"
273            @delimiter = "|"
274            @field TYPE = 0
275            @field VALUE = 1
276
277            TYPE == "A" AND VALUE == "100"
278        "#;
279
280        let config = ParserConfig::default();
281        let filter = load_filter_string(content, &config).unwrap();
282
283        assert!(filter.evaluate(Bytes::from("A|100")));
284        assert!(!filter.evaluate(Bytes::from("B|100")));
285        assert!(!filter.evaluate(Bytes::from("A|200")));
286    }
287
288    #[test]
289    fn test_load_filter_override_config() {
290        let content = r#"
291            @field EXTRA = 5
292
293            EXTRA == "test"
294        "#;
295
296        let config = test_config();
297        let filter = load_filter_string(content, &config).unwrap();
298
299        let payload = Bytes::from("0;;;1;;;2;;;3;;;4;;;test");
300        assert!(filter.evaluate(payload));
301    }
302
303    #[test]
304    fn test_invalid_directive() {
305        let content = r#"
306            @unknown_directive = "value"
307            LEVEL == "error"
308        "#;
309
310        let config = test_config();
311        let result = load_filter_string(content, &config);
312        assert!(matches!(result, Err(LoadError::InvalidDirective(_))));
313    }
314
315    #[test]
316    fn test_invalid_field_index() {
317        let content = r#"
318            @field BAD_FIELD = not_a_number
319            LEVEL == "error"
320        "#;
321
322        let config = test_config();
323        let result = load_filter_string(content, &config);
324        assert!(matches!(result, Err(LoadError::InvalidFieldIndex(_))));
325    }
326}