use std::io::{self, Write};
use crate::FlowEvent;
use crate::extract::FiveTupleKey;
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(&mut self, ev: &FlowEvent<FiveTupleKey>) -> io::Result<()> {
match ev {
FlowEvent::Ended {
key, reason, stats, ..
} => {
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 = format!("{:?}", key.proto).to_lowercase();
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(&key.a.ip().to_string(), sep),
key.a.port(),
quote(&key.b.ip().to_string(), sep),
key.b.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 = format!("{:?}", key.proto).to_lowercase();
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(&key.a.ip().to_string(), sep),
key.a.port(),
quote(&key.b.ip().to_string(), sep),
key.b.port(),
)?;
}
_ => {}
}
Ok(())
}
pub fn flush(&mut self) -> io::Result<()> {
self.sink.flush()
}
pub fn finish(mut self) -> io::Result<W> {
self.flush()?;
Ok(self.sink)
}
}
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");
}
}