use std::borrow::Cow;
use std::io::Write;
use std::time::Duration;
use pcap_file::DataLink;
use pcap_file::pcap::{PcapHeader, PcapPacket, PcapWriter};
use pcap_file::pcapng::PcapNgWriter;
use pcap_file::pcapng::blocks::enhanced_packet::EnhancedPacketBlock;
use pcap_file::pcapng::blocks::interface_description::{
InterfaceDescriptionBlock, InterfaceDescriptionOption,
};
use crate::packet::{OwnedPacket, Packet};
const LINKTYPE_ETHERNET: u32 = 1;
const IF_TSRESOL_NANOS: u8 = 9;
pub struct CaptureWriter<W: Write> {
inner: PcapWriter<W>,
}
impl<W: Write> CaptureWriter<W> {
pub fn create(out: W) -> Result<Self, pcap_file::PcapError> {
Self::new_with_linktype(out, LINKTYPE_ETHERNET)
}
pub fn new_with_linktype(out: W, linktype: u32) -> Result<Self, pcap_file::PcapError> {
let header = PcapHeader {
version_major: 2,
version_minor: 4,
ts_correction: 0,
ts_accuracy: 0,
snaplen: u32::MAX,
datalink: pcap_file::DataLink::from(linktype),
ts_resolution: pcap_file::TsResolution::NanoSecond,
endianness: pcap_file::Endianness::native(),
};
Ok(Self {
inner: PcapWriter::with_header(out, header)?,
})
}
pub fn write_packet(&mut self, pkt: &Packet<'_>) -> Result<(), pcap_file::PcapError> {
let ts = pkt.timestamp();
let record = PcapPacket::new_owned(
Duration::new(ts.sec as u64, ts.nsec),
pkt.original_len() as u32,
pkt.data().to_vec(),
);
self.inner.write_packet(&record).map(|_| ())
}
pub fn write_packet_truncated(
&mut self,
pkt: &Packet<'_>,
caplen: usize,
) -> Result<(), pcap_file::PcapError> {
let ts = pkt.timestamp();
let data = pkt.data();
let truncated = if caplen < data.len() {
data[..caplen].to_vec()
} else {
data.to_vec()
};
let record = PcapPacket::new_owned(
Duration::new(ts.sec as u64, ts.nsec),
pkt.original_len() as u32,
truncated,
);
self.inner.write_packet(&record).map(|_| ())
}
pub fn write_owned(&mut self, pkt: &OwnedPacket) -> Result<(), pcap_file::PcapError> {
let record = PcapPacket::new(
Duration::new(pkt.timestamp.sec as u64, pkt.timestamp.nsec),
pkt.original_len as u32,
&pkt.data,
);
self.inner.write_packet(&record).map(|_| ())
}
pub fn into_inner(self) -> W {
self.inner.into_writer()
}
}
pub struct CaptureWriterNg<W: Write> {
inner: PcapNgWriter<W>,
}
impl<W: Write> CaptureWriterNg<W> {
pub fn create(out: W) -> Result<Self, pcap_file::PcapError> {
Self::new_with_linktype(out, LINKTYPE_ETHERNET)
}
pub fn new_with_linktype(out: W, linktype: u32) -> Result<Self, pcap_file::PcapError> {
let mut inner = PcapNgWriter::new(out)?;
let mut idb = InterfaceDescriptionBlock::new(DataLink::from(linktype), 0);
idb.options
.push(InterfaceDescriptionOption::IfTsResol(IF_TSRESOL_NANOS));
inner.write_pcapng_block(idb)?;
Ok(Self { inner })
}
pub fn write_packet(&mut self, pkt: &Packet<'_>) -> Result<(), pcap_file::PcapError> {
self.write_epb(
pkt.timestamp(),
pkt.original_len(),
Cow::Borrowed(pkt.data()),
)
}
pub fn write_packet_truncated(
&mut self,
pkt: &Packet<'_>,
caplen: usize,
) -> Result<(), pcap_file::PcapError> {
let data = pkt.data();
let captured = if caplen < data.len() {
&data[..caplen]
} else {
data
};
self.write_epb(pkt.timestamp(), pkt.original_len(), Cow::Borrowed(captured))
}
pub fn write_owned(&mut self, pkt: &OwnedPacket) -> Result<(), pcap_file::PcapError> {
self.write_epb(pkt.timestamp, pkt.original_len, Cow::Borrowed(&pkt.data))
}
fn write_epb(
&mut self,
ts: crate::packet::Timestamp,
original_len: usize,
data: Cow<'_, [u8]>,
) -> Result<(), pcap_file::PcapError> {
let block = EnhancedPacketBlock {
interface_id: 0,
timestamp: Duration::new(ts.sec as u64, ts.nsec),
original_len: original_len as u32,
data,
options: vec![],
};
self.inner.write_pcapng_block(block).map(|_| ())
}
pub fn into_inner(self) -> W {
self.inner.into_inner()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::packet::{PacketDirection, PacketStatus, Timestamp};
use std::io::Cursor;
fn make_owned(data: Vec<u8>) -> OwnedPacket {
OwnedPacket {
data,
timestamp: Timestamp::new(1234, 567_890_000),
timestamp_clock: crate::packet::TimestampClock::None,
original_len: 100,
status: PacketStatus::default(),
direction: PacketDirection::Host,
rxhash: 0,
vlan_tci: 0,
vlan_tpid: 0,
ll_protocol: 0x0800,
source_ll_addr: [0; 8],
source_ll_addr_len: 0,
}
}
#[test]
fn writes_pcap_header() {
let mut buf = Vec::new();
{
let cursor = Cursor::new(&mut buf);
let _w = CaptureWriter::create(cursor).expect("create");
}
let magic = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
let valid = matches!(magic, 0xa1b2_c3d4 | 0xd4c3_b2a1 | 0xa1b2_3c4d | 0x4d3c_b2a1);
assert!(valid, "PCAP magic missing: 0x{magic:08x}");
}
#[test]
fn round_trip_owned_packet() {
let mut buf = Vec::new();
{
let cursor = Cursor::new(&mut buf);
let mut w = CaptureWriter::create(cursor).expect("create");
let pkt = make_owned(vec![1, 2, 3, 4, 5]);
w.write_owned(&pkt).expect("write");
}
let cursor = Cursor::new(&buf);
let mut reader = pcap_file::pcap::PcapReader::new(cursor).expect("reader");
let record = reader.next_packet().expect("first").expect("record");
assert_eq!(record.data.as_ref(), &[1, 2, 3, 4, 5]);
assert_eq!(record.orig_len, 100);
}
#[test]
fn round_trip_preserves_nanosecond_timestamp() {
let ts_in = Timestamp::new(1_700_000_000, 123_456_789);
let mut buf = Vec::new();
{
let cursor = Cursor::new(&mut buf);
let mut w = CaptureWriter::create(cursor).expect("create");
let mut pkt = make_owned(vec![1, 2, 3, 4, 5]);
pkt.timestamp = ts_in;
w.write_owned(&pkt).expect("write");
}
let cursor = Cursor::new(&buf);
let mut reader = pcap_file::pcap::PcapReader::new(cursor).expect("reader");
let record = reader.next_packet().expect("first").expect("record");
let expected = Duration::new(ts_in.sec as u64, ts_in.nsec);
assert_eq!(
record.timestamp, expected,
"nanosecond precision lost across pcap round-trip"
);
}
#[test]
fn pcapng_writes_shb_then_idb_with_nanosecond_resolution() {
use pcap_file::pcapng::{Block, PcapNgReader};
let mut buf = Vec::new();
{
let cursor = Cursor::new(&mut buf);
let _w = CaptureWriterNg::create(cursor).expect("create");
}
let block_type = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
assert_eq!(block_type, 0x0A0D_0D0A, "pcapng SHB magic missing");
let cursor = Cursor::new(&buf);
let mut reader = PcapNgReader::new(cursor).expect("reader");
let mut saw_idb = false;
while let Some(block) = reader.next_block() {
if let Block::InterfaceDescription(idb) = block.expect("block") {
assert_eq!(idb.linktype, DataLink::ETHERNET);
assert!(
idb.options
.iter()
.any(|o| matches!(o, InterfaceDescriptionOption::IfTsResol(9))),
"IDB missing nanosecond if_tsresol (=9)"
);
saw_idb = true;
break;
}
}
assert!(saw_idb, "no Interface Description Block written");
}
#[test]
fn pcapng_round_trip_owned_packet_preserves_data_ts_and_len() {
use pcap_file::pcapng::{Block, PcapNgReader};
let ts_in = Timestamp::new(1_700_000_000, 123_456_789);
let mut buf = Vec::new();
{
let cursor = Cursor::new(&mut buf);
let mut w = CaptureWriterNg::create(cursor).expect("create");
let mut pkt = make_owned(vec![9, 8, 7, 6, 5]);
pkt.timestamp = ts_in;
w.write_owned(&pkt).expect("write");
}
let cursor = Cursor::new(&buf);
let mut reader = PcapNgReader::new(cursor).expect("reader");
let mut epb_seen = 0;
while let Some(block) = reader.next_block() {
if let Block::EnhancedPacket(epb) = block.expect("block") {
assert_eq!(epb.interface_id, 0);
assert_eq!(epb.data.as_ref(), &[9, 8, 7, 6, 5]);
assert_eq!(epb.original_len, 100);
assert_eq!(
epb.timestamp,
Duration::new(ts_in.sec as u64, ts_in.nsec),
"nanosecond timestamp lost across pcapng round-trip"
);
epb_seen += 1;
}
}
assert_eq!(epb_seen, 1, "expected exactly one Enhanced Packet Block");
}
}