use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
use chrono::{DateTime, TimeZone, Utc};
use crate::error::SortError;
#[derive(Debug, Clone, Copy)]
pub struct GlobalHeader {
pub magic_number: u32,
pub version_major: u16,
pub version_minor: u16,
pub thiszone: i32,
pub sigfigs: u32,
pub snaplen: u32,
pub network: i32,
}
impl GlobalHeader {
pub fn is_big_endian(self) -> bool {
matches!(self.magic_number, 0xd4c3_b2a1 | 0x4d3c_b2a1)
}
pub fn is_nanosecond(self) -> bool {
matches!(self.magic_number, 0xa1b2_3c4d | 0x4d3c_b2a1)
}
pub fn to_bytes(self) -> [u8; 24] {
let mut buf = [0u8; 24];
if self.is_big_endian() {
buf[0..4].copy_from_slice(&self.magic_number.to_be_bytes());
buf[4..6].copy_from_slice(&self.version_major.to_be_bytes());
buf[6..8].copy_from_slice(&self.version_minor.to_be_bytes());
buf[8..12].copy_from_slice(&self.thiszone.to_be_bytes());
buf[12..16].copy_from_slice(&self.sigfigs.to_be_bytes());
buf[16..20].copy_from_slice(&self.snaplen.to_be_bytes());
buf[20..24].copy_from_slice(&self.network.to_be_bytes());
} else {
buf[0..4].copy_from_slice(&self.magic_number.to_le_bytes());
buf[4..6].copy_from_slice(&self.version_major.to_le_bytes());
buf[6..8].copy_from_slice(&self.version_minor.to_le_bytes());
buf[8..12].copy_from_slice(&self.thiszone.to_le_bytes());
buf[12..16].copy_from_slice(&self.sigfigs.to_le_bytes());
buf[16..20].copy_from_slice(&self.snaplen.to_le_bytes());
buf[20..24].copy_from_slice(&self.network.to_le_bytes());
}
buf
}
}
pub struct PcapWriter {
inner: BufWriter<File>,
header: GlobalHeader,
path: PathBuf,
packets: u64,
}
impl PcapWriter {
pub fn create(path: &Path, header: GlobalHeader) -> Result<Self, SortError> {
if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
std::fs::create_dir_all(parent)?;
}
let file = File::create(path)?;
let mut writer = BufWriter::with_capacity(64 * 1024, file);
writer.write_all(&header.to_bytes())?;
Ok(Self {
inner: writer,
header,
path: path.to_owned(),
packets: 0,
})
}
pub fn write_packet(
&mut self,
timestamp_ns: u64,
caplen: u32,
origlen: u32,
data: &[u8],
) -> Result<(), SortError> {
let (ts_sec, ts_frac) = if self.header.is_nanosecond() {
(
(timestamp_ns / 1_000_000_000) as u32,
(timestamp_ns % 1_000_000_000) as u32,
)
} else {
(
(timestamp_ns / 1_000_000_000) as u32,
((timestamp_ns % 1_000_000_000) / 1_000) as u32,
)
};
let mut rec = [0u8; 16];
if self.header.is_big_endian() {
rec[0..4].copy_from_slice(&ts_sec.to_be_bytes());
rec[4..8].copy_from_slice(&ts_frac.to_be_bytes());
rec[8..12].copy_from_slice(&caplen.to_be_bytes());
rec[12..16].copy_from_slice(&origlen.to_be_bytes());
} else {
rec[0..4].copy_from_slice(&ts_sec.to_le_bytes());
rec[4..8].copy_from_slice(&ts_frac.to_le_bytes());
rec[8..12].copy_from_slice(&caplen.to_le_bytes());
rec[12..16].copy_from_slice(&origlen.to_le_bytes());
}
self.inner.write_all(&rec)?;
self.inner.write_all(data)?;
self.packets += 1;
Ok(())
}
pub fn finish(mut self) -> Result<(PathBuf, u64), SortError> {
self.inner.flush()?;
Ok((self.path, self.packets))
}
}
pub struct SlicedWriter {
header: GlobalHeader,
output_base: PathBuf,
slice_secs: Option<u64>,
current: Option<PcapWriter>,
current_slice_start_ns: u64,
pub files: Vec<PathBuf>,
pub total_packets: u64,
}
impl SlicedWriter {
pub fn new(output_base: PathBuf, header: GlobalHeader, slice_secs: Option<u64>) -> Self {
Self {
header,
output_base,
slice_secs,
current: None,
current_slice_start_ns: 0,
files: Vec::new(),
total_packets: 0,
}
}
pub fn write_packet(
&mut self,
timestamp_ns: u64,
caplen: u32,
origlen: u32,
data: &[u8],
) -> Result<(), SortError> {
let need_new = match self.slice_secs {
None => self.current.is_none(),
Some(secs) => {
let slice_ns = secs * 1_000_000_000;
self.current.is_none() || timestamp_ns >= self.current_slice_start_ns + slice_ns
}
};
if need_new {
self.rotate(timestamp_ns)?;
}
self.current
.as_mut()
.unwrap()
.write_packet(timestamp_ns, caplen, origlen, data)?;
self.total_packets += 1;
Ok(())
}
pub fn finish(mut self) -> Result<(Vec<PathBuf>, u64), SortError> {
if let Some(w) = self.current.take() {
let (path, _) = w.finish()?;
self.files.push(path);
}
Ok((self.files, self.total_packets))
}
fn rotate(&mut self, timestamp_ns: u64) -> Result<(), SortError> {
if let Some(w) = self.current.take() {
let (path, _) = w.finish()?;
self.files.push(path);
}
let path = self.output_path(timestamp_ns);
self.current = Some(PcapWriter::create(&path, self.header)?);
self.current_slice_start_ns = match self.slice_secs {
None => timestamp_ns,
Some(secs) => {
let slice_ns = secs * 1_000_000_000;
(timestamp_ns / slice_ns) * slice_ns
}
};
Ok(())
}
fn output_path(&self, timestamp_ns: u64) -> PathBuf {
if self.slice_secs.is_none() {
return self.output_base.clone();
}
let dt = ns_to_datetime(timestamp_ns);
let name = dt.format("part_%Y%m%dT%H%M%SZ.pcap").to_string();
self.output_base.join(name)
}
}
fn ns_to_datetime(ns: u64) -> DateTime<Utc> {
let secs = (ns / 1_000_000_000) as i64;
let nanos = (ns % 1_000_000_000) as u32;
Utc.timestamp_opt(secs, nanos)
.single()
.unwrap_or(DateTime::UNIX_EPOCH)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_header() -> GlobalHeader {
GlobalHeader {
magic_number: 0xa1b2_c3d4,
version_major: 2,
version_minor: 4,
thiszone: 0,
sigfigs: 0,
snaplen: 65535,
network: 1,
}
}
#[test]
fn test_global_header_roundtrip_le() {
let hdr = make_header();
let bytes = hdr.to_bytes();
assert_eq!(bytes.len(), 24);
assert_eq!(&bytes[0..4], &0xa1b2_c3d4u32.to_le_bytes());
assert_eq!(u32::from_le_bytes(bytes[16..20].try_into().unwrap()), 65535);
}
#[test]
fn test_global_header_big_endian() {
let hdr = GlobalHeader {
magic_number: 0xd4c3_b2a1,
..make_header()
};
assert!(hdr.is_big_endian());
let bytes = hdr.to_bytes();
assert_eq!(&bytes[0..4], &0xd4c3_b2a1u32.to_be_bytes());
}
#[test]
fn test_pcap_writer_produces_valid_file() {
let path = std::env::temp_dir().join("pcap_toolkit_writer_test.pcap");
let hdr = make_header();
let mut w = PcapWriter::create(&path, hdr).unwrap();
let data = vec![0xAAu8; 60];
w.write_packet(1_700_000_000_000_000_000, 60, 60, &data)
.unwrap();
let (_, count) = w.finish().unwrap();
assert_eq!(count, 1);
let bytes = std::fs::read(&path).unwrap();
assert_eq!(&bytes[0..4], &0xa1b2_c3d4u32.to_le_bytes());
assert_eq!(bytes.len(), 100);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_sliced_writer_single_file() {
let path = std::env::temp_dir().join("pcap_toolkit_sliced_single.pcap");
let mut sw = SlicedWriter::new(path.clone(), make_header(), None);
let data = vec![0u8; 40];
sw.write_packet(1_000_000_000_000, 40, 40, &data).unwrap();
sw.write_packet(2_000_000_000_000, 40, 40, &data).unwrap();
let (files, total) = sw.finish().unwrap();
assert_eq!(total, 2);
assert_eq!(files.len(), 1);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_sliced_writer_splits_by_hour() {
let dir = std::env::temp_dir().join("pcap_toolkit_sliced_hour");
std::fs::create_dir_all(&dir).unwrap();
let mut sw = SlicedWriter::new(dir.clone(), make_header(), Some(3600));
let data = vec![0u8; 40];
sw.write_packet(0, 40, 40, &data).unwrap();
sw.write_packet(3_601 * 1_000_000_000, 40, 40, &data)
.unwrap();
let (files, total) = sw.finish().unwrap();
assert_eq!(total, 2);
assert_eq!(files.len(), 2);
for f in &files {
let _ = std::fs::remove_file(f);
}
let _ = std::fs::remove_dir(&dir);
}
}