use anyhow::{Context, Result};
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::Path;
use time::OffsetDateTime;
const BLOCK_TYPE_SHB: u32 = 0x0A0D_0D0A;
const BLOCK_TYPE_IDB: u32 = 0x0000_0001;
const BLOCK_TYPE_EPB: u32 = 0x0000_0006;
const BYTE_ORDER_MAGIC: u32 = 0x1A2B_3C4D;
pub struct PcapSink {
file: BufWriter<File>,
}
impl PcapSink {
pub fn create(path: &Path, link_type: u16, snaplen: u32) -> Result<Self> {
let f = File::create(path).with_context(|| format!("creating {}", path.display()))?;
let mut s = Self {
file: BufWriter::new(f),
};
s.write_section_header()?;
s.write_interface_description(link_type, snaplen)?;
s.file.flush().context("flushing pcapng header")?;
Ok(s)
}
pub fn write_packet(&mut self, ts: OffsetDateTime, packet: &[u8]) -> Result<()> {
let captured = packet.len() as u32;
let original = captured;
let micros: u64 = ts
.unix_timestamp_nanos()
.max(0)
.div_euclid(1_000)
.try_into()
.unwrap_or(0);
let ts_high = (micros >> 32) as u32;
let ts_low = (micros & 0xFFFF_FFFF) as u32;
let pad = (4 - (captured & 3)) & 3;
let total_len: u32 = 32 + captured + pad;
let f = &mut self.file;
f.write_all(&BLOCK_TYPE_EPB.to_le_bytes())?;
f.write_all(&total_len.to_le_bytes())?;
f.write_all(&0u32.to_le_bytes())?; f.write_all(&ts_high.to_le_bytes())?;
f.write_all(&ts_low.to_le_bytes())?;
f.write_all(&captured.to_le_bytes())?;
f.write_all(&original.to_le_bytes())?;
f.write_all(packet)?;
if pad > 0 {
f.write_all(&[0u8; 3][..pad as usize])?;
}
f.write_all(&total_len.to_le_bytes())?;
f.flush().context("flushing pcapng EPB")?;
Ok(())
}
fn write_section_header(&mut self) -> Result<()> {
let total_len: u32 = 28;
let f = &mut self.file;
f.write_all(&BLOCK_TYPE_SHB.to_le_bytes())?;
f.write_all(&total_len.to_le_bytes())?;
f.write_all(&BYTE_ORDER_MAGIC.to_le_bytes())?;
f.write_all(&1u16.to_le_bytes())?; f.write_all(&0u16.to_le_bytes())?; f.write_all(&(-1i64).to_le_bytes())?; f.write_all(&total_len.to_le_bytes())?;
Ok(())
}
fn write_interface_description(&mut self, link_type: u16, snaplen: u32) -> Result<()> {
let total_len: u32 = 20;
let f = &mut self.file;
f.write_all(&BLOCK_TYPE_IDB.to_le_bytes())?;
f.write_all(&total_len.to_le_bytes())?;
f.write_all(&link_type.to_le_bytes())?;
f.write_all(&0u16.to_le_bytes())?; f.write_all(&snaplen.to_le_bytes())?;
f.write_all(&total_len.to_le_bytes())?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use time::macros::datetime;
#[test]
fn shb_layout() {
let dir = tempdir();
let path = dir.join("test.pcapng");
let _ = PcapSink::create(&path, 220, 65535).unwrap();
let bytes = std::fs::read(&path).unwrap();
assert_eq!(bytes.len(), 48);
assert_eq!(&bytes[0..4], &0x0A0D_0D0A_u32.to_le_bytes());
assert_eq!(&bytes[4..8], &28u32.to_le_bytes());
assert_eq!(&bytes[8..12], &0x1A2B_3C4D_u32.to_le_bytes());
assert_eq!(&bytes[24..28], &28u32.to_le_bytes());
assert_eq!(&bytes[28..32], &0x0000_0001_u32.to_le_bytes());
assert_eq!(&bytes[32..36], &20u32.to_le_bytes());
assert_eq!(&bytes[36..38], &220u16.to_le_bytes());
}
#[test]
fn epb_payload_and_padding() {
let dir = tempdir();
let path = dir.join("test.pcapng");
let mut s = PcapSink::create(&path, 220, 65535).unwrap();
s.write_packet(datetime!(2026-04-25 12:30:45 UTC), &[1, 2, 3, 4, 5])
.unwrap();
drop(s);
let bytes = std::fs::read(&path).unwrap();
assert!(bytes.len() > 48);
let epb = &bytes[48..];
assert_eq!(&epb[0..4], &0x0000_0006_u32.to_le_bytes());
assert_eq!(&epb[4..8], &40u32.to_le_bytes());
assert_eq!(&epb[20..24], &5u32.to_le_bytes());
assert_eq!(&epb[24..28], &5u32.to_le_bytes());
assert_eq!(&epb[28..33], &[1, 2, 3, 4, 5]);
assert_eq!(&epb[33..36], &[0, 0, 0]);
assert_eq!(&epb[36..40], &40u32.to_le_bytes());
}
#[test]
fn epb_no_padding_when_aligned() {
let dir = tempdir();
let path = dir.join("test.pcapng");
let mut s = PcapSink::create(&path, 220, 65535).unwrap();
let payload = [0xAAu8; 8];
s.write_packet(datetime!(2026-04-25 0:0:0 UTC), &payload)
.unwrap();
drop(s);
let bytes = std::fs::read(&path).unwrap();
let epb = &bytes[48..];
assert_eq!(&epb[4..8], &40u32.to_le_bytes());
assert_eq!(&epb[36..40], &40u32.to_le_bytes());
}
fn tempdir() -> std::path::PathBuf {
let mut dir = std::env::temp_dir();
let token = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
dir.push(format!("serial-capture-test-{token}"));
std::fs::create_dir_all(&dir).unwrap();
dir
}
}