use itertools::Itertools;
use serde_json::json;
use std::io::Write;
use std::net::IpAddr;
use std::path::PathBuf;
use bgpkit_parser::{BgpElem, BgpkitParser, Elementor};
use clap::{Parser, ValueEnum};
use ipnet::IpNet;
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
enum OutputFormat {
#[default]
Default,
Json,
JsonPretty,
Psv,
}
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
enum OutputLevel {
#[default]
Elems,
Records,
}
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
struct Opts {
#[clap(name = "FILE")]
file_path: PathBuf,
#[clap(short, long)]
cache_dir: Option<PathBuf>,
#[clap(short = 'F', long, value_enum, default_value_t = OutputFormat::Default)]
format: OutputFormat,
#[clap(short = 'L', long, value_enum, default_value_t = OutputLevel::Elems)]
level: OutputLevel,
#[clap(long)]
json: bool,
#[clap(long)]
pretty: bool,
#[clap(long)]
psv: bool,
#[clap(short, long)]
elems_count: bool,
#[clap(short, long)]
records_count: bool,
#[clap(flatten)]
filters: Filters,
}
#[derive(Parser, Debug)]
struct Filters {
#[clap(short = 'o', long)]
origin_asn: Option<u32>,
#[clap(short = 'f', long = "filter")]
filters: Vec<String>,
#[clap(short = 'p', long)]
prefix: Option<IpNet>,
#[clap(short = 's', long)]
include_super: bool,
#[clap(short = 'S', long)]
include_sub: bool,
#[clap(short = '4', long)]
ipv4_only: bool,
#[clap(short = '6', long)]
ipv6_only: bool,
#[clap(short = 'j', long)]
peer_ip: Vec<IpAddr>,
#[clap(short = 'J', long)]
peer_asn: Option<u32>,
#[clap(short = 'm', long)]
elem_type: Option<String>,
#[clap(short = 't', long)]
start_ts: Option<f64>,
#[clap(short = 'T', long)]
end_ts: Option<f64>,
#[clap(short = 'a', long)]
as_path: Option<String>,
#[clap(short = 'C', long)]
community: Option<String>,
}
fn main() {
let opts: Opts = Opts::parse();
env_logger::init();
let file_path = opts.file_path.to_str().unwrap();
let parser_opt = match opts.cache_dir {
None => BgpkitParser::new(file_path),
Some(c) => BgpkitParser::new_cached(file_path, c.to_str().unwrap()),
};
let mut parser = match parser_opt {
Ok(p) => p,
Err(err) => {
eprintln!("{err}");
std::process::exit(1);
}
};
if let Some(v) = opts.filters.as_path {
parser = parser.add_filter("as_path", v.as_str()).unwrap();
}
if let Some(v) = opts.filters.community {
parser = parser.add_filter("community", v.as_str()).unwrap();
}
if let Some(v) = opts.filters.origin_asn {
parser = parser
.add_filter("origin_asn", v.to_string().as_str())
.unwrap();
}
if let Some(v) = opts.filters.prefix {
let filter_type = match (opts.filters.include_super, opts.filters.include_sub) {
(false, false) => "prefix",
(true, false) => "prefix_super",
(false, true) => "prefix_sub",
(true, true) => "prefix_super_sub",
};
parser = parser
.add_filter(filter_type, v.to_string().as_str())
.unwrap();
}
if !opts.filters.peer_ip.is_empty() {
let v = opts.filters.peer_ip.iter().map(|p| p.to_string()).join(",");
parser = parser.add_filter("peer_ips", v.as_str()).unwrap();
}
if let Some(v) = opts.filters.peer_asn {
parser = parser
.add_filter("peer_asn", v.to_string().as_str())
.unwrap();
}
if let Some(v) = opts.filters.elem_type {
parser = parser.add_filter("type", v.as_str()).unwrap();
}
if let Some(v) = opts.filters.start_ts {
parser = parser
.add_filter("start_ts", v.to_string().as_str())
.unwrap();
}
if let Some(v) = opts.filters.end_ts {
parser = parser.add_filter("end_ts", v.to_string().as_str()).unwrap();
}
for filter_expr in &opts.filters.filters {
match parse_filter_expression(filter_expr) {
Ok((filter_type, filter_value)) => {
parser = match parser.add_filter(&filter_type, &filter_value) {
Ok(p) => p,
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 (opts.filters.ipv4_only, opts.filters.ipv6_only) {
(true, true) => {
eprintln!("Error: --ipv4-only and --ipv6-only cannot be used together");
std::process::exit(1);
}
(false, false) => {
}
(true, false) => {
parser = parser.add_filter("ip_version", "ipv4").unwrap();
}
(false, true) => {
parser = parser.add_filter("ip_version", "ipv6").unwrap();
}
}
let output_format = if opts.pretty {
OutputFormat::JsonPretty
} else if opts.json {
OutputFormat::Json
} else if opts.psv {
OutputFormat::Psv
} else {
opts.format
};
match (opts.elems_count, opts.records_count) {
(true, true) => {
let mut elementor = Elementor::new();
let (mut records_count, mut elems_count) = (0, 0);
for record in parser.into_record_iter() {
records_count += 1;
elems_count += elementor.record_to_elems(record).len();
}
println!("total records: {records_count}");
println!("total elems: {elems_count}");
}
(false, true) => {
println!("total records: {}", parser.into_record_iter().count());
}
(true, false) => {
println!("total elems: {}", parser.into_elem_iter().count());
}
(false, false) => {
let mut stdout = std::io::stdout();
match opts.level {
OutputLevel::Elems => {
for (index, elem) in parser.into_elem_iter().enumerate() {
let output_str = format_elem(&elem, output_format, index);
if let Err(e) = writeln!(stdout, "{}", &output_str) {
if e.kind() != std::io::ErrorKind::BrokenPipe {
eprintln!("{e}");
}
std::process::exit(1);
}
}
}
OutputLevel::Records => {
for record in parser.into_record_iter() {
let output_str = format_record(&record, output_format);
if let Err(e) = writeln!(stdout, "{}", &output_str) {
if e.kind() != std::io::ErrorKind::BrokenPipe {
eprintln!("{e}");
}
std::process::exit(1);
}
}
}
}
}
}
}
fn format_elem(elem: &BgpElem, format: OutputFormat, index: usize) -> String {
match format {
OutputFormat::Json => {
let val = json!(elem);
val.to_string()
}
OutputFormat::JsonPretty => {
let val = json!(elem);
serde_json::to_string_pretty(&val).unwrap()
}
OutputFormat::Psv => {
if index == 0 {
format!("{}\n{}", BgpElem::get_psv_header(), elem.to_psv())
} else {
elem.to_psv()
}
}
OutputFormat::Default => elem.to_string(),
}
}
fn format_record(record: &bgpkit_parser::MrtRecord, format: OutputFormat) -> String {
match format {
OutputFormat::Json => {
let val = json!(record);
val.to_string()
}
OutputFormat::JsonPretty => {
let val = json!(record);
serde_json::to_string_pretty(&val).unwrap()
}
OutputFormat::Psv | OutputFormat::Default => {
format!("{}", record)
}
}
}
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())
}
}