From 956d56716e2831924b5f2f101e331e3ca6c8b77b Mon Sep 17 00:00:00 2001 From: Pierre Dubouilh Date: Mon, 25 May 2026 19:24:15 +0200 Subject: [PATCH] bump serde & rework status parsing --- Cargo.lock | 70 ++++++++++++++++++++------------------ Cargo.toml | 2 +- src/main.rs | 17 +++++++--- src/utils.rs | 96 ++++++++++++++++++++++++++++++++++++++++------------ 4 files changed, 126 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 635a498..f7f73c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -170,12 +170,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "dtoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" - [[package]] name = "filetime" version = "0.2.17" @@ -256,9 +250,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "0.3.4" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "kqueue" @@ -348,24 +342,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "num-traits" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" -dependencies = [ - "num-traits 0.2.19", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - [[package]] name = "num_cpus" version = "1.13.1" @@ -449,20 +425,44 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.185" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be9b6f69f1dfd54c3b568ffa45c310d6973a5e5148fd40cf515acaf38cf5bc31" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] [[package]] name = "serde_json" -version = "1.0.1" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c62115693d0a9ed8c32d1c760f0fdbe7d4b05cb13c135b9b54137ac0d59fccb" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ - "dtoa", "itoa", - "num-traits 0.1.43", + "memchr", "serde", + "serde_core", + "zmij", ] [[package]] @@ -703,3 +703,9 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 5ae063d..ec0db8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,5 +12,5 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros"] } lazy_static = "1.4.0" regex = "1.5.4" anyhow = "1.0.44" -serde_json = "=1.0.1" +serde_json = "1" clap = { version = "4.5.27", features = ["derive"] } diff --git a/src/main.rs b/src/main.rs index 5c4fc5a..063122e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -110,13 +110,20 @@ async fn run() -> Result<()> { Ok(()) }; - while let Ok(Some(line)) = ml.next_line().await { - if let Err(e) = assess_line(line) { - log!("ERR: {:?}", e); + loop { + match ml.next_line().await { + Ok(Some(line)) => { + if let Err(e) = assess_line(line) { + log!("ERR: {:?}", e); + } + } + Ok(None) => { + log!("log stream ended, exiting"); + return Ok(()); + } + Err(e) => return Err(e.into()), } } - - Ok(()) } #[tokio::main] diff --git a/src/utils.rs b/src/utils.rs index 73e7917..48ad9f5 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,5 @@ -use anyhow::{anyhow, Context, Result}; -use clap::Parser; +use anyhow::{anyhow, bail, Context, Result}; +use clap::{ArgGroup, Parser}; use regex::Regex; use std::{ net::IpAddr, @@ -43,28 +43,30 @@ pub fn parse_regex(a: &str) -> Result { Ok(r) } +/// Parse a comma-separated list of HTTP statuses with trailing-`x` wildcards. +/// +/// Accepted forms per token: `NNN` (literal), `NNx` (10-wide range), `Nxx` (100-wide range). +/// All expanded codes must fall in the valid HTTP range 100..=599. pub fn parse_statuses(a: &str) -> Result> { + if a.trim().is_empty() { + bail!("invalid_http_statuses: empty input"); + } let mut statuses = vec![]; - for s in a.split(',') { - if s.contains("xx") { - let range = s.replace("xx", ""); - let range = range.parse::().context("invalid range")?; - let range = range * 100; - for i in 0..100 { - let status = range + i; - statuses.push(status); + for raw in a.split(',') { + let s = raw.trim(); + let xs = s.chars().rev().take_while(|c| *c == 'x').count(); + let digits = &s[..s.len() - xs]; + if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) { + bail!("invalid http status `{}`", s); + } + let base: u32 = digits.parse().context("invalid status")?; + let span = 10u32.pow(xs as u32); + let from = base * span; + for code in from..(from + span) { + if !(100..=599).contains(&code) { + bail!("status {} out of valid HTTP range (100..=599)", code); } - } else if s.contains("x") { - let range = s.replace("x", ""); - let range = range.parse::().context("invalid range")?; - let range = range * 10; - for i in 0..10 { - let status = range + i; - statuses.push(status); - } - } else { - let status = s.parse::().context("invalid status")?; - statuses.push(status); + statuses.push(code); } } Ok(statuses) @@ -93,6 +95,7 @@ Example: # generic log parser example with a log text to flag, and a regex to parse the offending IP. ./blockfast --generic-logpath=/tmp/generictest --generic-positive='Failed password' --generic-ip='from ([0-9a-fA-F:.]+) port'", verbatim_doc_comment, + group(ArgGroup::new("generic_match").args(["generic_positive", "generic_negative"])), )] pub struct Args { @@ -140,3 +143,54 @@ pub struct Args { #[clap(long, default_value = "400,401,402,403")] pub invalid_http_statuses: String, } + +#[cfg(test)] +mod tests { + use super::parse_statuses; + + #[test] + fn literal() { + assert_eq!(parse_statuses("401").unwrap(), vec![401]); + assert_eq!(parse_statuses("401,404,429").unwrap(), vec![401, 404, 429]); + } + + #[test] + fn ten_range() { + assert_eq!( + parse_statuses("40x").unwrap(), + (400..410).collect::>() + ); + } + + #[test] + fn hundred_range() { + assert_eq!( + parse_statuses("4xx").unwrap(), + (400..500).collect::>() + ); + } + + #[test] + fn mixed() { + let got = parse_statuses("401, 40x, 5xx").unwrap(); + assert_eq!(got.len(), 1 + 10 + 100); + assert_eq!(got[0], 401); + assert!(got.contains(&500)); + assert!(got.contains(&599)); + } + + #[test] + fn rejects_out_of_range() { + // 5x → 50..60, none of which are valid HTTP + assert!(parse_statuses("5x").is_err()); + } + + #[test] + fn rejects_garbage() { + assert!(parse_statuses("").is_err()); + assert!(parse_statuses("xxx").is_err()); + assert!(parse_statuses("1x2").is_err()); + assert!(parse_statuses("abc").is_err()); + assert!(parse_statuses("401,,402").is_err()); + } +}