use nom::bytes::complete::take_until;
use std::collections::HashMap;
use chrono::offset::TimeZone;
use chrono::{DateTime, Datelike, Utc};
use nom::{
bytes::complete::{tag, take},
IResult,
};
fn parse_month<'a>((i, s): (&'a str, &str)) -> IResult<&'a str, u32> {
match s {
"Jan" => Ok((i, 1)),
"Feb" => Ok((i, 2)),
"Mar" => Ok((i, 3)),
"Apr" => Ok((i, 4)),
"May" => Ok((i, 5)),
"Jun" => Ok((i, 6)),
"Jul" => Ok((i, 7)),
"Aug" => Ok((i, 8)),
"Sep" => Ok((i, 9)),
"Oct" => Ok((i, 10)),
"Nov" => Ok((i, 11)),
"Dec" => Ok((i, 12)),
_ => Err(nom::Err::Failure(nom::error_position!(
"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec",
nom::error::ErrorKind::OneOf
))),
}
}
fn take_n_digits(i: &str, n: usize) -> IResult<&str, u32> {
let (i, digits) = take(n)(i)?;
match digits.parse() {
Ok(res) => Ok((i, res)),
Err(_) => Err(nom::Err::Failure(nom::error_position!(
"Invalid string, expected ASCII representation of a number",
nom::error::ErrorKind::Digit
))),
}
}
fn parse_date_time(i: &str) -> IResult<&str, DateTime<Utc>> {
let (i, month) = parse_month(take(3_usize)(i)?)?;
let (i, _) = tag(" ")(i)?;
let (i, day) = take_n_digits(i, 2)?;
let (i, _) = tag(" ")(i)?;
let (i, hour) = take_n_digits(i, 2)?;
let (i, _) = tag(":")(i)?;
let (i, min) = take_n_digits(i, 2)?;
let (i, _) = tag(":")(i)?;
let (i, sec) = take_n_digits(i, 2)?;
Ok((
i,
chrono::Utc
.ymd(chrono::Utc::today().year(), month, day)
.and_hms(hour, min, sec),
))
}
fn parse_hostname(i: &str) -> IResult<&str, &str> {
take_until(" ")(i)
}
fn parse_bracketed_param(i: &str) -> IResult<&str, &str> {
let (i, _) = tag("[")(i)?;
let (i, time) = take_until("]")(i)?;
let (i, _) = tag("]")(i)?;
Ok((i, time))
}
fn parse_kvs(i: &str) -> IResult<&str, HashMap<&str, &str>> {
let (i, kvs) = nom::multi::separated_list0(
nom::character::complete::char(' '),
nom::sequence::separated_pair(take_until("="), tag("="), take_until(" ")),
)(i)?;
let mut map = HashMap::new();
for (key, value) in kvs {
map.insert(key, value);
}
Ok((i, map))
}
pub fn parse_log_line(i: &str) -> IResult<&str, Log> {
let (i, time) = parse_date_time(i)?;
let (i, _) = tag(" ")(i)?;
let (i, hostname) = parse_hostname(i)?;
let (i, _) = tag(" kernel: ")(i)?;
let (i, _) = parse_bracketed_param(i)?;
let (i, _) = tag(" ")(i)?;
let (i, rule) = parse_bracketed_param(i)?;
let (i, values) = parse_kvs(i)?;
Ok((
i,
Log {
time,
hostname,
rule,
values,
},
))
}
#[derive(Debug)]
pub struct Log<'a> {
pub hostname: &'a str,
pub time: DateTime<Utc>,
pub rule: &'a str,
pub values: HashMap<&'a str, &'a str>,
}
#[cfg(test)]
mod tests {
use chrono::Datelike;
use chrono::Timelike;
use crate::parser::parse_date_time;
use crate::parser::parse_hostname;
use crate::parser::parse_log_line;
#[test]
fn test_parse_date_time() {
let (_, parsed) = parse_date_time("Apr 19 12:31:53").unwrap();
assert_eq!(
parsed.to_rfc3339(),
format!("{}-04-19T12:31:53+00:00", chrono::Utc::today().year())
);
}
#[test]
fn test_parse_hostname() {
let (_, parsed) = parse_hostname("vyos ignore the rest of this").unwrap();
assert_eq!(parsed, "vyos");
}
#[test]
fn test_parse_log_line() {
let (rest, parsed) = parse_log_line("May 23 12:31:53 vyos kernel: [213370.255870] [OUTSIDE-LOCAL-default-D]IN=pppoe0 OUT= MAC= SRC=125.166.96.62 DST=80.80.80.80 LEN=143 TOS=0x00 PREC=0x00 TTL=110 ID=9398 PROTO=UDP SPT=1025 DPT=7140 LEN=123").unwrap();
assert_eq!(parsed.time.hour(), 12);
assert_eq!(parsed.hostname, "vyos");
assert_eq!(parsed.rule, "OUTSIDE-LOCAL-default-D");
assert_eq!(parsed.values.get("IN"), Some(&"pppoe0"));
assert_eq!(parsed.values.get("MAC"), Some(&""));
assert_eq!(parsed.values.get("DST"), Some(&"80.80.80.80"));
assert_eq!(rest, "");
}
}