use std::io::{self, Write};
use crate::{FlowEvent, KeyFields};
pub struct FlowEventCsvWriter<W: Write> {
sink: W,
options: CsvOptions,
}
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub struct CsvOptions {
pub emit_started: bool,
pub tab_separated: bool,
}
impl CsvOptions {
fn sep(&self) -> char {
if self.tab_separated { '\t' } else { ',' }
}
}
impl<W: Write> FlowEventCsvWriter<W> {
pub fn new(sink: W) -> io::Result<Self> {
Self::with_options(sink, CsvOptions::default())
}
pub fn with_options(sink: W, options: CsvOptions) -> io::Result<Self> {
let mut writer = Self { sink, options };
writer.write_header()?;
Ok(writer)
}
fn write_header(&mut self) -> io::Result<()> {
let sep = self.options.sep();
if self.options.emit_started {
write!(self.sink, "flow_event_kind{sep}")?;
}
writeln!(
self.sink,
"start_sec{sep}end_sec{sep}duration_sec{sep}proto{sep}src_ip{sep}src_port{sep}\
dst_ip{sep}dst_port{sep}pkts_init{sep}pkts_resp{sep}bytes_init{sep}bytes_resp{sep}\
retransmits_init{sep}retransmits_resp{sep}end_reason",
)?;
Ok(())
}
pub fn write_event<K>(&mut self, ev: &FlowEvent<K>) -> io::Result<()>
where
K: KeyFields,
{
match ev {
FlowEvent::Ended {
key, reason, stats, ..
} => {
#[cfg(feature = "ipfix")]
{
let rec = crate::FlowRecord::from_key_fields(stats, key, Some(*reason));
return self.write_flow_record(&rec);
}
#[cfg(not(feature = "ipfix"))]
{
let sep = self.options.sep();
if self.options.emit_started {
write!(self.sink, "ended{sep}")?;
}
let start = stats.started.to_unix_f64();
let end = stats.last_seen.to_unix_f64();
let dur = stats.duration_secs();
let proto = key.proto_str().unwrap_or("").to_lowercase();
let src_ip = key.src_ip().map(|ip| ip.to_string()).unwrap_or_default();
let src_port = key.src_port().unwrap_or(0);
let dst_ip = key.dest_ip().map(|ip| ip.to_string()).unwrap_or_default();
let dst_port = key.dest_port().unwrap_or(0);
writeln!(
self.sink,
"{start:.6}{sep}{end:.6}{sep}{dur:.6}{sep}{}{sep}{}{sep}{}{sep}{}{sep}{}{sep}\
{}{sep}{}{sep}{}{sep}{}{sep}{}{sep}{}{sep}{}",
quote(&proto, sep),
quote(&src_ip, sep),
src_port,
quote(&dst_ip, sep),
dst_port,
stats.packets_initiator,
stats.packets_responder,
stats.bytes_initiator,
stats.bytes_responder,
stats.retransmits_initiator,
stats.retransmits_responder,
reason.as_str(),
)?;
}
}
FlowEvent::Started { key, ts, .. } if self.options.emit_started => {
let sep = self.options.sep();
let start = ts.to_unix_f64();
let proto = key.proto_str().unwrap_or("").to_lowercase();
let src_ip = key.src_ip().map(|ip| ip.to_string()).unwrap_or_default();
let src_port = key.src_port().unwrap_or(0);
let dst_ip = key.dest_ip().map(|ip| ip.to_string()).unwrap_or_default();
let dst_port = key.dest_port().unwrap_or(0);
writeln!(
self.sink,
"started{sep}{start:.6}{sep}{start:.6}{sep}0.000000{sep}{}{sep}{}{sep}{}{sep}{}{sep}{}{sep}\
0{sep}0{sep}0{sep}0{sep}0{sep}0{sep}",
quote(&proto, sep),
quote(&src_ip, sep),
src_port,
quote(&dst_ip, sep),
dst_port,
)?;
}
_ => {}
}
Ok(())
}
#[cfg(feature = "ipfix")]
pub fn write_flow_record(&mut self, rec: &crate::FlowRecord) -> io::Result<()> {
let sep = self.options.sep();
if self.options.emit_started {
write!(self.sink, "ended{sep}")?;
}
let start = (rec.flow_start_milliseconds as f64) / 1000.0;
let end = (rec.flow_end_milliseconds as f64) / 1000.0;
let dur = (end - start).max(0.0);
let proto = flow_record_proto_str(rec).to_string();
let src_ip = flow_record_src_ip(rec);
let src_port = rec.source_transport_port;
let dst_ip = flow_record_dst_ip(rec);
let dst_port = rec.destination_transport_port;
let reason = flow_record_reason_str(rec);
writeln!(
self.sink,
"{start:.6}{sep}{end:.6}{sep}{dur:.6}{sep}{}{sep}{}{sep}{}{sep}{}{sep}{}{sep}\
{}{sep}{}{sep}{}{sep}{}{sep}{}{sep}{}{sep}{}",
quote(&proto, sep),
quote(&src_ip, sep),
src_port,
quote(&dst_ip, sep),
dst_port,
rec.packet_delta_count_initiator,
rec.packet_delta_count_responder,
rec.octet_delta_count_initiator,
rec.octet_delta_count_responder,
rec.retransmits_initiator,
rec.retransmits_responder,
reason,
)?;
Ok(())
}
pub fn flush(&mut self) -> io::Result<()> {
self.sink.flush()
}
pub fn finish(mut self) -> io::Result<W> {
self.flush()?;
Ok(self.sink)
}
}
#[cfg(feature = "ipfix")]
pub(super) fn flow_record_proto_str(rec: &crate::FlowRecord) -> &'static str {
match rec.protocol_identifier {
6 => "tcp",
17 => "udp",
1 => "icmp",
58 => "icmpv6",
132 => "sctp",
_ => "",
}
}
#[cfg(feature = "ipfix")]
pub(super) fn flow_record_src_ip(rec: &crate::FlowRecord) -> String {
rec.source_ipv4_address
.map(|ip| ip.to_string())
.or_else(|| rec.source_ipv6_address.map(|ip| ip.to_string()))
.unwrap_or_default()
}
#[cfg(feature = "ipfix")]
pub(super) fn flow_record_dst_ip(rec: &crate::FlowRecord) -> String {
rec.destination_ipv4_address
.map(|ip| ip.to_string())
.or_else(|| rec.destination_ipv6_address.map(|ip| ip.to_string()))
.unwrap_or_default()
}
#[cfg(feature = "ipfix")]
pub(super) fn flow_record_reason_str(rec: &crate::FlowRecord) -> &'static str {
if let Some(reason) = rec.original_end_reason {
return reason.as_str();
}
use crate::ipfix::FlowEndReason as R;
match rec.flow_end_reason {
Some(R::IdleTimeout) => "idle_timeout",
Some(R::ActiveTimeout) => "idle_timeout",
Some(R::EndOfFlowDetected) => "fin",
Some(R::ForcedEnd) => "force_closed",
Some(R::LackOfResources) => "evicted",
None => "",
}
}
fn quote(field: &str, sep: char) -> String {
let needs_quoting =
field.contains(sep) || field.contains('"') || field.contains('\n') || field.contains('\r');
if !needs_quoting {
return field.to_string();
}
let mut out = String::with_capacity(field.len() + 2);
out.push('"');
for c in field.chars() {
if c == '"' {
out.push('"');
}
out.push(c);
}
out.push('"');
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quote_passthrough_when_no_special_chars() {
assert_eq!(quote("plain", ','), "plain");
assert_eq!(quote("10.0.0.1", ','), "10.0.0.1");
}
#[test]
fn quote_wraps_when_separator_in_field() {
assert_eq!(quote("a,b", ','), "\"a,b\"");
}
#[test]
fn quote_doubles_internal_quotes() {
assert_eq!(quote("hi \"there\"", ','), "\"hi \"\"there\"\"\"");
}
#[test]
fn quote_wraps_when_newline_or_cr() {
assert_eq!(quote("a\nb", ','), "\"a\nb\"");
assert_eq!(quote("a\rb", ','), "\"a\rb\"");
}
#[test]
fn quote_respects_tab_separator() {
assert_eq!(quote("a\tb", '\t'), "\"a\tb\"");
assert_eq!(quote("a,b", '\t'), "a,b");
}
}