1use std::path::PathBuf;
7
8use toml_span::{DeserError, Deserialize, Value, de_helpers::TableHelper};
9
10use crate::error::ConfigError;
11
12#[derive(Debug, Default)]
14pub struct Config {
15 pub input: Vec<InputConfig>,
17
18 pub sort: SortConfig,
20
21 pub filter: FilterConfig,
23
24 pub output: Vec<OutputConfig>,
26
27 pub transform: TransformConfig,
29
30 pub export: ExportConfig,
32
33 pub replay: ReplayConfig,
35}
36
37#[derive(Debug)]
39pub struct InputConfig {
40 pub path: String,
42}
43
44#[derive(Debug, Default)]
46pub struct SortConfig {
47 pub enabled: bool,
49
50 pub slice: Option<String>,
52}
53
54#[derive(Debug, Default)]
56pub struct FilterConfig {
57 pub negate: bool,
59
60 pub rules: Vec<FilterRuleConfig>,
62
63 pub proto: Vec<String>,
65
66 pub src_ip: Vec<String>,
68
69 pub dst_ip: Vec<String>,
71
72 pub ip: Vec<String>,
74
75 pub src_port: Vec<String>,
77
78 pub dst_port: Vec<String>,
80
81 pub port: Vec<String>,
83
84 pub flow_id: Vec<String>,
86
87 pub from: Option<String>,
89
90 pub to: Option<String>,
92
93 pub tcp_flags: Option<String>,
95
96 pub min_len: Option<u32>,
98
99 pub max_len: Option<u32>,
101
102 pub unidirectional: bool,
104
105 pub min_flow_packets: Option<u64>,
108}
109
110#[derive(Debug, Default)]
115pub struct FilterRuleConfig {
116 pub op: String,
118 pub proto: Vec<String>,
119 pub src_ip: Vec<String>,
120 pub dst_ip: Vec<String>,
121 pub ip: Vec<String>,
122 pub src_port: Vec<String>,
123 pub dst_port: Vec<String>,
124 pub port: Vec<String>,
125 pub flow_id: Vec<String>,
126 pub from: Option<String>,
127 pub to: Option<String>,
128 pub tcp_flags: Option<String>,
129 pub min_len: Option<u32>,
130 pub max_len: Option<u32>,
131 pub unidirectional: bool,
132}
133
134#[derive(Debug)]
136pub struct ProtocolTruncationConfig {
137 pub proto: String,
139 pub max_payload_bytes: u32,
141}
142
143#[derive(Debug, Default)]
145pub struct TransformConfig {
146 pub max_payload_bytes: Option<u32>,
148 pub timestamp_start: Option<String>,
150 pub replace_ip: Vec<String>,
152 pub truncate_by_proto: Vec<ProtocolTruncationConfig>,
154}
155
156#[derive(Debug)]
158pub struct ExportOutputConfig {
159 pub path: PathBuf,
161
162 pub format: Option<String>,
164
165 pub compress_payload: bool,
167}
168
169#[derive(Debug, Default)]
171pub struct ExportConfig {
172 pub outputs: Vec<ExportOutputConfig>,
174
175 pub path: Option<PathBuf>,
177
178 pub format: Option<String>,
180
181 pub compress_payload: bool,
183
184 pub unidirectional: bool,
186}
187
188#[derive(Debug)]
190pub struct OutputConfig {
191 pub format: String,
193
194 pub path: PathBuf,
196
197 pub compress_payload: bool,
199}
200
201#[derive(Debug)]
203pub struct ReplayConfig {
204 pub interfaces: Vec<String>,
207
208 pub speed: f64,
211
212 pub pps: Option<u64>,
215}
216
217impl Default for ReplayConfig {
218 fn default() -> Self {
219 Self {
220 interfaces: Vec::new(),
221 speed: 1.0,
222 pps: None,
223 }
224 }
225}
226
227impl<'de> Deserialize<'de> for Config {
230 fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
231 let mut th = TableHelper::new(value)?;
232 let input = th.optional::<Vec<InputConfig>>("input").unwrap_or_default();
233 let sort = th.optional::<SortConfig>("sort").unwrap_or_default();
234 let filter = th.optional::<FilterConfig>("filter").unwrap_or_default();
235 let output = th
236 .optional::<Vec<OutputConfig>>("output")
237 .unwrap_or_default();
238 let transform = th
239 .optional::<TransformConfig>("transform")
240 .unwrap_or_default();
241 let export = th.optional::<ExportConfig>("export").unwrap_or_default();
242 let replay = th.optional::<ReplayConfig>("replay").unwrap_or_default();
243 th.finalize(None)?;
244 Ok(Config {
245 input,
246 sort,
247 filter,
248 output,
249 transform,
250 export,
251 replay,
252 })
253 }
254}
255
256impl<'de> Deserialize<'de> for InputConfig {
257 fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
258 let mut th = TableHelper::new(value)?;
259 let path = th.required::<String>("path")?;
260 th.finalize(None)?;
261 Ok(InputConfig { path })
262 }
263}
264
265impl<'de> Deserialize<'de> for SortConfig {
266 fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
267 let mut th = TableHelper::new(value)?;
268 let enabled = th.optional::<bool>("enabled").unwrap_or(false);
269 let slice = th.optional::<String>("slice");
270 th.finalize(None)?;
271 Ok(SortConfig { enabled, slice })
272 }
273}
274
275impl<'de> Deserialize<'de> for FilterConfig {
276 fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
277 let mut th = TableHelper::new(value)?;
278 let negate = th.optional::<bool>("negate").unwrap_or(false);
279 let rules = th
280 .optional::<Vec<FilterRuleConfig>>("rules")
281 .unwrap_or_default();
282 let proto = th.optional::<Vec<String>>("proto").unwrap_or_default();
283 let src_ip = th.optional::<Vec<String>>("src_ip").unwrap_or_default();
284 let dst_ip = th.optional::<Vec<String>>("dst_ip").unwrap_or_default();
285 let ip = th.optional::<Vec<String>>("ip").unwrap_or_default();
286 let src_port = th.optional::<Vec<String>>("src_port").unwrap_or_default();
287 let dst_port = th.optional::<Vec<String>>("dst_port").unwrap_or_default();
288 let port = th.optional::<Vec<String>>("port").unwrap_or_default();
289 let flow_id = th.optional::<Vec<String>>("flow_id").unwrap_or_default();
290 let from = th.optional::<String>("from");
291 let to = th.optional::<String>("to");
292 let tcp_flags = th.optional::<String>("tcp_flags");
293 let min_len = th.optional::<u32>("min_len");
294 let max_len = th.optional::<u32>("max_len");
295 let unidirectional = th.optional::<bool>("unidirectional").unwrap_or(false);
296 let min_flow_packets = th.optional::<u64>("min_flow_packets");
297 th.finalize(None)?;
298 Ok(FilterConfig {
299 negate,
300 rules,
301 proto,
302 src_ip,
303 dst_ip,
304 ip,
305 src_port,
306 dst_port,
307 port,
308 flow_id,
309 from,
310 to,
311 tcp_flags,
312 min_len,
313 max_len,
314 unidirectional,
315 min_flow_packets,
316 })
317 }
318}
319
320impl<'de> Deserialize<'de> for FilterRuleConfig {
321 fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
322 let mut th = TableHelper::new(value)?;
323 let op = th.optional::<String>("op").unwrap_or_default();
324 let proto = th.optional::<Vec<String>>("proto").unwrap_or_default();
325 let src_ip = th.optional::<Vec<String>>("src_ip").unwrap_or_default();
326 let dst_ip = th.optional::<Vec<String>>("dst_ip").unwrap_or_default();
327 let ip = th.optional::<Vec<String>>("ip").unwrap_or_default();
328 let src_port = th.optional::<Vec<String>>("src_port").unwrap_or_default();
329 let dst_port = th.optional::<Vec<String>>("dst_port").unwrap_or_default();
330 let port = th.optional::<Vec<String>>("port").unwrap_or_default();
331 let flow_id = th.optional::<Vec<String>>("flow_id").unwrap_or_default();
332 let from = th.optional::<String>("from");
333 let to = th.optional::<String>("to");
334 let tcp_flags = th.optional::<String>("tcp_flags");
335 let min_len = th.optional::<u32>("min_len");
336 let max_len = th.optional::<u32>("max_len");
337 let unidirectional = th.optional::<bool>("unidirectional").unwrap_or(false);
338 th.finalize(None)?;
339 Ok(FilterRuleConfig {
340 op,
341 proto,
342 src_ip,
343 dst_ip,
344 ip,
345 src_port,
346 dst_port,
347 port,
348 flow_id,
349 from,
350 to,
351 tcp_flags,
352 min_len,
353 max_len,
354 unidirectional,
355 })
356 }
357}
358
359impl<'de> Deserialize<'de> for ProtocolTruncationConfig {
360 fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
361 let mut th = TableHelper::new(value)?;
362 let proto = th.required::<String>("proto")?;
363 let max_payload_bytes = th.required::<u32>("max_payload_bytes")?;
364 th.finalize(None)?;
365 Ok(ProtocolTruncationConfig {
366 proto,
367 max_payload_bytes,
368 })
369 }
370}
371
372impl<'de> Deserialize<'de> for TransformConfig {
373 fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
374 let mut th = TableHelper::new(value)?;
375 let max_payload_bytes = th.optional::<u32>("max_payload_bytes");
376 let timestamp_start = th.optional::<String>("timestamp_start");
377 let replace_ip = th.optional::<Vec<String>>("replace_ip").unwrap_or_default();
378 let truncate_by_proto = th
379 .optional::<Vec<ProtocolTruncationConfig>>("truncate_by_proto")
380 .unwrap_or_default();
381 th.finalize(None)?;
382 Ok(TransformConfig {
383 max_payload_bytes,
384 timestamp_start,
385 replace_ip,
386 truncate_by_proto,
387 })
388 }
389}
390
391impl<'de> Deserialize<'de> for ExportOutputConfig {
392 fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
393 let mut th = TableHelper::new(value)?;
394 let path = th.required::<String>("path").map(PathBuf::from)?;
395 let format = th.optional::<String>("format");
396 let compress_payload = th.optional::<bool>("compress_payload").unwrap_or(false);
397 th.finalize(None)?;
398 Ok(ExportOutputConfig {
399 path,
400 format,
401 compress_payload,
402 })
403 }
404}
405
406impl<'de> Deserialize<'de> for ExportConfig {
407 fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
408 let mut th = TableHelper::new(value)?;
409 let outputs = th
410 .optional::<Vec<ExportOutputConfig>>("outputs")
411 .unwrap_or_default();
412 let path = th.optional::<String>("path").map(PathBuf::from);
413 let format = th.optional::<String>("format");
414 let compress_payload = th.optional::<bool>("compress_payload").unwrap_or(false);
415 let unidirectional = th.optional::<bool>("unidirectional").unwrap_or(false);
416 th.finalize(None)?;
417 Ok(ExportConfig {
418 outputs,
419 path,
420 format,
421 compress_payload,
422 unidirectional,
423 })
424 }
425}
426
427impl<'de> Deserialize<'de> for OutputConfig {
428 fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
429 let mut th = TableHelper::new(value)?;
430 let format = th.required::<String>("format")?;
431 let path = th.required::<String>("path").map(PathBuf::from)?;
432 let compress_payload = th.optional::<bool>("compress_payload").unwrap_or(false);
433 th.finalize(None)?;
434 Ok(OutputConfig {
435 format,
436 path,
437 compress_payload,
438 })
439 }
440}
441
442impl<'de> Deserialize<'de> for ReplayConfig {
443 fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
444 let mut th = TableHelper::new(value)?;
445 let mut interfaces = th.optional::<Vec<String>>("interfaces").unwrap_or_default();
447 if interfaces.is_empty() {
448 if let Some(single) = th.optional::<String>("interface") {
449 interfaces.push(single);
450 }
451 } else {
452 let _ = th.optional::<String>("interface");
454 }
455 let speed = th.optional::<f64>("speed").unwrap_or(1.0);
456 let pps = th.optional::<u64>("pps");
457 th.finalize(None)?;
458 Ok(ReplayConfig {
459 interfaces,
460 speed,
461 pps,
462 })
463 }
464}
465
466impl Config {
469 pub fn from_file(path: &std::path::Path) -> Result<Self, ConfigError> {
474 let text = std::fs::read_to_string(path)?;
475 let mut value = toml_span::parse(&text)?;
476 let config = Config::deserialize(&mut value)?;
477 Ok(config)
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484
485 #[test]
486 fn test_default_config_is_valid() {
487 let config = Config::default();
488 assert!(config.input.is_empty());
489 assert!(!config.sort.enabled);
490 assert!(config.filter.proto.is_empty());
491 assert!(!config.filter.unidirectional);
492 assert_eq!(config.replay.speed, 1.0);
493 }
494
495 #[test]
496 fn test_from_toml_str_parses_correctly() {
497 let toml = r#"
498[[input]]
499path = "captures/*.pcap"
500
501[sort]
502enabled = true
503slice = "1h"
504
505[filter]
506proto = ["tcp", "udp"]
507dst_port = ["443", "80"]
508src_ip = ["10.0.0.0/8"]
509
510[[output]]
511format = "parquet"
512path = "out/traffic.parquet"
513
514[replay]
515interface = "eth0"
516speed = 2.0
517"#;
518 let mut value = toml_span::parse(toml).unwrap();
519 let config = Config::deserialize(&mut value).unwrap();
520
521 assert_eq!(config.input.len(), 1);
522 assert_eq!(config.input[0].path, "captures/*.pcap");
523 assert!(config.sort.enabled);
524 assert_eq!(config.sort.slice.as_deref(), Some("1h"));
525 assert_eq!(config.filter.proto, ["tcp", "udp"]);
526 assert_eq!(config.filter.dst_port, ["443", "80"]);
527 assert_eq!(config.output.len(), 1);
528 assert_eq!(config.output[0].format, "parquet");
529 assert_eq!(config.replay.speed, 2.0);
530 assert_eq!(config.replay.interfaces, ["eth0"]);
531 }
532
533 #[test]
534 fn test_filter_rules_toml() {
535 let toml = r#"
536[filter]
537proto = ["tcp"]
538
539[[filter.rules]]
540op = "or"
541proto = ["udp"]
542
543[[filter.rules]]
544op = "not"
545dst_ip = ["10.0.0.0/8"]
546"#;
547 let mut value = toml_span::parse(toml).unwrap();
548 let config = Config::deserialize(&mut value).unwrap();
549 assert_eq!(config.filter.proto, ["tcp"]);
550 assert_eq!(config.filter.rules.len(), 2);
551 assert_eq!(config.filter.rules[0].op, "or");
552 assert_eq!(config.filter.rules[0].proto, ["udp"]);
553 assert_eq!(config.filter.rules[1].op, "not");
554 assert_eq!(config.filter.rules[1].dst_ip, ["10.0.0.0/8"]);
555 }
556
557 #[test]
558 fn test_filter_negate_toml() {
559 let toml = r#"
560[filter]
561negate = true
562proto = ["tcp"]
563"#;
564 let mut value = toml_span::parse(toml).unwrap();
565 let config = Config::deserialize(&mut value).unwrap();
566 assert!(config.filter.negate);
567 assert_eq!(config.filter.proto, ["tcp"]);
568 }
569
570 #[test]
571 fn test_empty_toml_produces_default_config() {
572 let mut value = toml_span::parse("").unwrap();
573 let config = Config::deserialize(&mut value).unwrap();
574 assert!(config.input.is_empty());
575 assert!(!config.sort.enabled);
576 }
577
578 #[test]
579 fn test_unknown_keys_error() {
580 let toml = r#"
581[sort]
582enabled = true
583bogus_key = "oops"
584"#;
585 let mut value = toml_span::parse(toml).unwrap();
586 let result = Config::deserialize(&mut value);
587 assert!(result.is_err());
588 }
589
590 #[test]
591 fn test_transform_config_global_truncation() {
592 let toml = r#"
593[transform]
594max_payload_bytes = 256
595timestamp_start = "2024-01-01T00:00:00Z"
596replace_ip = ["10.0.0.1=192.168.1.1"]
597"#;
598 let mut value = toml_span::parse(toml).unwrap();
599 let config = Config::deserialize(&mut value).unwrap();
600 assert_eq!(config.transform.max_payload_bytes, Some(256));
601 assert_eq!(
602 config.transform.timestamp_start.as_deref(),
603 Some("2024-01-01T00:00:00Z")
604 );
605 assert_eq!(config.transform.replace_ip, ["10.0.0.1=192.168.1.1"]);
606 assert!(config.transform.truncate_by_proto.is_empty());
607 }
608
609 #[test]
610 fn test_transform_config_per_proto_truncation() {
611 let toml = r#"
612[transform]
613max_payload_bytes = 512
614
615[[transform.truncate_by_proto]]
616proto = "tcp"
617max_payload_bytes = 128
618
619[[transform.truncate_by_proto]]
620proto = "udp"
621max_payload_bytes = 64
622"#;
623 let mut value = toml_span::parse(toml).unwrap();
624 let config = Config::deserialize(&mut value).unwrap();
625 assert_eq!(config.transform.max_payload_bytes, Some(512));
626 assert_eq!(config.transform.truncate_by_proto.len(), 2);
627 assert_eq!(config.transform.truncate_by_proto[0].proto, "tcp");
628 assert_eq!(config.transform.truncate_by_proto[0].max_payload_bytes, 128);
629 assert_eq!(config.transform.truncate_by_proto[1].proto, "udp");
630 assert_eq!(config.transform.truncate_by_proto[1].max_payload_bytes, 64);
631 }
632
633 #[test]
634 fn test_transform_config_proto_only_no_global() {
635 let toml = r#"
636[[transform.truncate_by_proto]]
637proto = "17"
638max_payload_bytes = 32
639"#;
640 let mut value = toml_span::parse(toml).unwrap();
641 let config = Config::deserialize(&mut value).unwrap();
642 assert!(config.transform.max_payload_bytes.is_none());
643 assert_eq!(config.transform.truncate_by_proto.len(), 1);
644 assert_eq!(config.transform.truncate_by_proto[0].proto, "17");
645 assert_eq!(config.transform.truncate_by_proto[0].max_payload_bytes, 32);
646 }
647
648 #[test]
649 fn test_empty_transform_section() {
650 let toml = r#"
651[transform]
652"#;
653 let mut value = toml_span::parse(toml).unwrap();
654 let config = Config::deserialize(&mut value).unwrap();
655 assert!(config.transform.max_payload_bytes.is_none());
656 assert!(config.transform.truncate_by_proto.is_empty());
657 }
658}