bytecode_filter/
loader.rs1use std::fs;
25use std::path::Path;
26
27use crate::compiler::{compile, CompileError};
28use crate::parser::ParserConfig;
29use crate::vm::CompiledFilter;
30
31#[derive(Debug)]
33pub enum LoadError {
34 Io(std::io::Error),
36 Compile(CompileError),
38 InvalidDirective(String),
40 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
77pub 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
113pub 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 if trimmed.is_empty() || trimmed.starts_with('#') {
138 continue;
139 }
140
141 if trimmed.starts_with('@') {
143 parse_directive(trimmed, &mut local_config)?;
144 } else {
145 expression_lines.push(trimmed);
147 }
148 }
149
150 let expression = expression_lines.join(" ");
151 Ok(compile(&expression, &local_config)?)
152}
153
154fn 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 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 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 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}