use bgpflux::DataType;
use bgpkit_parser::Filter;
use clap::{Parser, ValueEnum};
use ipnet::IpNet;
use itertools::Itertools;
use std::net::IpAddr;
#[derive(Parser, Debug, Clone)]
#[command(
author,
version,
about = "A CLI to stream ordered BGP elements from multiple collectors and arbitrary time ranges"
)]
pub struct Args {
#[arg(short, long)]
pub live: bool,
#[arg(
short,
long,
value_delimiter = ',',
required = true,
help_heading = "Required argument"
)]
pub collector: Vec<String>,
#[arg(
short = 'b',
long,
help_heading = "Archive mode required arguments",
required_unless_present = "live"
)]
pub start: Option<String>,
#[arg(
short = 'e',
long,
help_heading = "Archive mode required arguments",
required_unless_present = "live"
)]
pub end: Option<String>,
#[arg(
short = 't',
long,
value_delimiter = ',',
help_heading = "Archive mode required arguments",
required_unless_present = "live"
)]
pub data_type: Option<Vec<DataTypeArg>>,
#[arg(
long,
help_heading = "Archive mode optional arguments",
conflicts_with = "live"
)]
pub cache_dir: Option<String>,
#[arg(
long,
help_heading = "Archive mode optional arguments",
conflicts_with = "live"
)]
pub broker_url: Option<String>,
#[arg(
short,
long,
requires = "live",
help_heading = "Live mode optional arguments"
)]
pub delay: Option<f64>,
#[clap(flatten)]
pub filters: Filters,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
pub enum DataTypeArg {
#[value(alias = "updates")]
Update,
#[value(alias = "ribs")]
Rib,
}
impl DataTypeArg {
pub fn to_data_type(args: &[DataTypeArg]) -> Result<DataType, &'static str> {
let has_update = args.contains(&DataTypeArg::Update);
let has_rib = args.contains(&DataTypeArg::Rib);
match (has_update, has_rib) {
(true, true) => Ok(DataType::Both),
(true, false) => Ok(DataType::Update),
(false, true) => Ok(DataType::Rib),
_ => Err("Could not parse data type"),
}
}
}
#[derive(Parser, Debug, Clone)]
#[command(next_help_heading = "Optional filters")]
pub struct Filters {
#[clap(short = 'o', long)]
pub origin_asn: Option<u32>,
#[clap(short = 'f', long = "filter")]
pub filters: Vec<String>,
#[clap(short = 'p', long)]
pub prefix: Option<IpNet>,
#[clap(short = 's', long)]
pub include_super: bool,
#[clap(short = 'S', long)]
pub include_sub: bool,
#[clap(short = '4', long)]
pub ipv4_only: bool,
#[clap(short = '6', long)]
pub ipv6_only: bool,
#[clap(short = 'j', long)]
pub peer_ip: Vec<IpAddr>,
#[clap(short = 'J', long)]
pub peer_asn: Option<u32>,
#[clap(short = 'm', long)]
pub elem_type: Option<String>,
#[clap(short = 'a', long)]
pub as_path: Option<String>,
#[clap(short = 'C', long)]
pub community: Option<String>,
}
impl Filters {
pub fn parse(self) -> Result<Vec<Filter>, bgpkit_parser::ParserError> {
let mut filters: Vec<Filter> = Vec::new();
if let Some(v) = &self.as_path {
filters.push(Filter::new("as_path", v.as_str())?);
}
if let Some(v) = &self.community {
filters.push(Filter::new("community", v.as_str())?);
}
if let Some(v) = &self.origin_asn {
filters.push(Filter::new("origin_asn", v.to_string().as_str())?);
}
if let Some(v) = self.prefix {
let filter_type = match (self.include_super, self.include_sub) {
(false, false) => "prefix",
(true, false) => "prefix_super",
(false, true) => "prefix_sub",
(true, true) => "prefix_super_sub",
};
filters.push(Filter::new(filter_type, v.to_string().as_str())?);
}
if !self.peer_ip.is_empty() {
let v = self.peer_ip.iter().map(|p| p.to_string()).join(",");
filters.push(Filter::new("peer_ips", v.as_str())?);
}
if let Some(v) = self.peer_asn {
filters.push(Filter::new("peer_asn", v.to_string().as_str())?);
}
if let Some(v) = &self.elem_type {
filters.push(Filter::new("type", v.as_str())?);
}
for filter_expr in &self.filters {
match parse_filter_expression(filter_expr) {
Ok((filter_type, filter_value)) => match Filter::new(&filter_type, &filter_value) {
Ok(f) => filters.push(f),
Err(e) => {
eprintln!("Error adding filter '{}': {}", filter_expr, e);
std::process::exit(1);
}
},
Err(e) => {
eprintln!("Invalid filter expression '{}': {}", filter_expr, e);
std::process::exit(1);
}
}
}
match (self.ipv4_only, self.ipv6_only) {
(true, true) => {
eprintln!("Error: --ipv4-only and --ipv6-only cannot be used together");
std::process::exit(1);
}
(false, false) => {
}
(true, false) => {
filters.push(Filter::new("ip_version", "ipv4")?);
}
(false, true) => {
filters.push(Filter::new("ip_version", "ipv6")?);
}
}
Ok(filters)
}
}
pub fn parse_filter_expression(expr: &str) -> Result<(String, String), String> {
let multi_value_filters = [
"origin_asns",
"prefixes",
"prefixes_super",
"prefixes_sub",
"prefixes_super_sub",
"peer_ips",
"peer_asns",
];
if let Some(pos) = expr.find("!=") {
let key = expr[..pos].trim();
let value = expr[pos + 2..].trim();
if key.is_empty() {
return Err("filter key cannot be empty".to_string());
}
if value.is_empty() {
return Err("filter value cannot be empty".to_string());
}
if multi_value_filters.contains(&key) {
let negated_values: Vec<String> =
value.split(',').map(|v| format!("!{}", v.trim())).collect();
Ok((key.to_string(), negated_values.join(",")))
} else {
Ok((key.to_string(), format!("!{}", value)))
}
}
else if let Some(pos) = expr.find('=') {
let key = expr[..pos].trim();
let value = expr[pos + 1..].trim();
if key.is_empty() {
return Err("filter key cannot be empty".to_string());
}
if value.is_empty() {
return Err("filter value cannot be empty".to_string());
}
Ok((key.to_string(), value.to_string()))
} else {
Err("filter expression must contain '=' or '!=' (e.g., 'origin_asn=13335' or 'origin_asn!=13335')".to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_data_type_arg_single_update() {
let args = vec![DataTypeArg::Update];
let result = DataTypeArg::to_data_type(&args);
assert!(result.is_ok());
assert_eq!(result.unwrap(), DataType::Update);
}
#[test]
fn test_data_type_arg_single_rib() {
let args = vec![DataTypeArg::Rib];
let result = DataTypeArg::to_data_type(&args);
assert!(result.is_ok());
assert_eq!(result.unwrap(), DataType::Rib);
}
#[test]
fn test_data_type_arg_both() {
let args = vec![DataTypeArg::Update, DataTypeArg::Rib];
let result = DataTypeArg::to_data_type(&args);
assert!(result.is_ok());
assert_eq!(result.unwrap(), DataType::Both);
}
#[test]
fn test_data_type_arg_empty() {
let args: Vec<DataTypeArg> = vec![];
let result = DataTypeArg::to_data_type(&args);
assert!(result.is_err());
}
#[test]
fn test_parse_filter_expression_single_value_positive() {
let result = parse_filter_expression("origin_asn=13335");
assert!(result.is_ok());
let (key, value) = result.unwrap();
assert_eq!(key, "origin_asn");
assert_eq!(value, "13335");
}
#[test]
fn test_parse_filter_expression_multi_value_negative() {
let result = parse_filter_expression("origin_asns!=13335,15169");
assert!(result.is_ok());
let (key, value) = result.unwrap();
assert_eq!(key, "origin_asns");
assert_eq!(value, "!13335,!15169");
}
#[test]
fn test_parse_filter_expression_invalid_no_operator() {
let result = parse_filter_expression("origin_asn13335");
assert!(result.is_err());
}
}