Skip to main content

fail2ban_log_parser_core/
lib.rs

1#![warn(clippy::pedantic)]
2
3#[cfg(all(feature = "parallel", target_arch = "wasm32"))]
4compile_error!(
5    "The `parallel` feature is not supported on wasm32 targets. Rayon requires OS threads which are not available in WASM."
6);
7
8pub use crate::parser::{Fail2BanEvent, Fail2BanHeaderType, Fail2BanLevel, Fail2BanStructuredLog};
9use std::fmt;
10
11#[cfg(feature = "parallel")]
12use rayon::prelude::*;
13
14mod parser;
15
16/// Error returned when a log line fails to parse.
17#[derive(Debug, Clone)]
18pub struct ParseError {
19    pub line_number: usize,
20    pub line: String,
21}
22
23impl fmt::Display for ParseError {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        write!(
26            f,
27            "failed to parse line {}: {}",
28            self.line_number, self.line
29        )
30    }
31}
32
33impl std::error::Error for ParseError {}
34
35/// Parse fail2ban log input. Returns an iterator over parse results.
36///
37/// Each line yields `Ok(Fail2BanStructuredLog)` on success or `Err(ParseError)` on failure.
38///
39/// When the `parallel` feature is enabled, all lines are parsed concurrently
40/// using Rayon and results are collected upfront. Without it, lines are parsed
41/// lazily on demand. The API is identical in both cases.
42///
43/// # Examples
44///
45/// Parse a single line:
46/// ```ignore
47/// let log = parse("2024-01-01 12:00:00,123 fail2ban.filter [1] INFO [sshd] Found 1.2.3.4").next();
48/// ```
49///
50/// Parse an entire file, skipping errors:
51/// ```ignore
52/// let content = std::fs::read_to_string("fail2ban.log").unwrap();
53/// for log in parse(&content).flatten() {
54///     println!("{:?}", log.jail());
55/// }
56/// ```
57///
58/// Collect all errors:
59/// ```ignore
60/// let content = std::fs::read_to_string("fail2ban.log").unwrap();
61/// let (ok, err): (Vec<_>, Vec<_>) = parse(&content).partition(Result::is_ok);
62/// ```
63#[cfg(not(feature = "parallel"))]
64pub fn parse(
65    input: &str,
66) -> impl Iterator<Item = Result<Fail2BanStructuredLog<'_>, ParseError>> + '_ {
67    input.lines().enumerate().map(|(i, line)| {
68        parser::parse_log_line(&mut &*line).map_err(|_| ParseError {
69            line_number: i + 1,
70            line: line.to_string(),
71        })
72    })
73}
74
75/// Parse fail2ban log input in parallel using Rayon.
76///
77/// All lines are parsed concurrently, then yielded as an iterator.
78/// Line order is preserved. The API is identical to the sequential version.
79#[cfg(feature = "parallel")]
80pub fn parse(input: &str) -> impl Iterator<Item = Result<Fail2BanStructuredLog<'_>, ParseError>> {
81    let lines: Vec<&str> = input.lines().collect();
82    lines
83        .par_iter()
84        .enumerate()
85        .map(|(i, &line)| {
86            parser::parse_log_line(&mut &*line).map_err(|_| ParseError {
87                line_number: i + 1,
88                line: line.to_string(),
89            })
90        })
91        .collect::<Vec<_>>()
92        .into_iter()
93}