const SHB_MAGIC: u32 = 0x0A0D_0D0A;
const BYTE_ORDER_MAGIC: u32 = 0x1A2B_3C4D;
const BYTE_ORDER_MAGIC_SWAPPED: u32 = 0x4D3C_2B1A;
const BLOCK_SHB: u32 = 0x0A0D_0D0A;
const BLOCK_IDB: u32 = 0x0000_0001;
const BLOCK_OPB: u32 = 0x0000_0002; const BLOCK_SPB: u32 = 0x0000_0003;
const BLOCK_EPB: u32 = 0x0000_0006;
#[derive(Debug, Clone)]
struct Interface {
link_type: u16,
ts_resol: u8,
ts_offset: u64,
}
impl Interface {
fn ts_to_secs_usecs(&self, ts: u64) -> (u32, u32) {
let resol = self.ts_resol;
let (sec, frac) = if resol == 6 {
(ts / 1_000_000, ts % 1_000_000)
} else if resol == 9 {
(ts / 1_000_000_000, (ts % 1_000_000_000) / 1_000)
} else {
let factor = 10u64.pow(resol as u32);
(ts / factor, (ts % factor) * 1_000_000 / factor)
};
((sec + self.ts_offset) as u32, frac as u32)
}
}
fn read_u16(data: &[u8], offset: usize, big_endian: bool) -> Option<u16> {
if offset + 2 > data.len() {
return None;
}
let b = &data[offset..offset + 2];
Some(if big_endian { u16::from_be_bytes([b[0], b[1]]) } else { u16::from_le_bytes([b[0], b[1]]) })
}
fn read_u32(data: &[u8], offset: usize, big_endian: bool) -> Option<u32> {
if offset + 4 > data.len() {
return None;
}
let b = &data[offset..offset + 4];
Some(if big_endian {
u32::from_be_bytes([b[0], b[1], b[2], b[3]])
} else {
u32::from_le_bytes([b[0], b[1], b[2], b[3]])
})
}
fn read_u64(data: &[u8], offset: usize, big_endian: bool) -> Option<u64> {
if offset + 8 > data.len() {
return None;
}
let b = &data[offset..offset + 8];
Some(if big_endian {
u64::from_be_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])
} else {
u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])
})
}
pub fn parse_pcapng(data: &[u8]) -> Result<Vec<(u32, u32, Vec<u8>)>, String> {
if data.len() < 12 {
return Err("file too short for PCAPng".into());
}
let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
if magic != SHB_MAGIC {
return Err(format!("not a PCAPng file (magic: 0x{magic:08x})"));
}
let mut pos = 0usize;
let mut big_endian = false;
let mut interfaces: Vec<Interface> = Vec::new();
let mut packets: Vec<(u32, u32, Vec<u8>)> = Vec::new();
while pos + 8 <= data.len() {
let block_type = read_u32(data, pos, big_endian).ok_or("failed to read block type")?;
let block_len_raw = read_u32(data, pos + 4, false).ok_or("failed to read block length")?;
let block_len = if block_type == BLOCK_SHB {
block_len_raw
} else {
read_u32(data, pos + 4, big_endian).ok_or("failed to read block length")?
};
if block_len < 12 || pos + block_len as usize > data.len() {
break;
}
let block_data = &data[pos..pos + block_len as usize];
match block_type {
BLOCK_SHB => {
if block_len < 16 {
pos += block_len as usize;
continue;
}
let bom =
u32::from_le_bytes([block_data[8], block_data[9], block_data[10], block_data[11]]);
big_endian = match bom {
BYTE_ORDER_MAGIC => false,
BYTE_ORDER_MAGIC_SWAPPED => true,
_ => return Err(format!("invalid byte-order magic: 0x{bom:08x}")),
};
interfaces.clear();
}
BLOCK_IDB => {
if block_len < 20 {
pos += block_len as usize;
continue;
}
let link_type = read_u16(block_data, 8, big_endian).unwrap_or(1);
let mut ts_resol = 6u8; let mut ts_offset = 0u64;
let opts_start = 16usize;
let opts_end = block_len as usize - 4;
let mut opos = opts_start;
while opos + 4 <= opts_end {
let opt_code = read_u16(block_data, opos, big_endian).unwrap_or(0);
let opt_len = read_u16(block_data, opos + 2, big_endian).unwrap_or(0) as usize;
opos += 4;
if opt_code == 0 {
break; }
if opos + opt_len <= opts_end {
match opt_code {
9 => {
if opt_len >= 1 {
ts_resol = block_data[opos];
}
}
14 => {
if opt_len >= 8 {
ts_offset = read_u64(block_data, opos, big_endian).unwrap_or(0);
}
}
_ => {}
}
}
opos += (opt_len + 3) & !3;
}
interfaces.push(Interface { link_type, ts_resol, ts_offset });
}
BLOCK_EPB => {
if block_len < 32 {
pos += block_len as usize;
continue;
}
let iface_id = read_u32(block_data, 8, big_endian).unwrap_or(0) as usize;
let ts_high = read_u32(block_data, 12, big_endian).unwrap_or(0) as u64;
let ts_low = read_u32(block_data, 16, big_endian).unwrap_or(0) as u64;
let cap_len = read_u32(block_data, 20, big_endian).unwrap_or(0) as usize;
let ts = (ts_high << 32) | ts_low;
let iface = interfaces
.get(iface_id)
.cloned()
.unwrap_or(Interface { link_type: 1, ts_resol: 6, ts_offset: 0 });
if iface.link_type != 1 {
pos += block_len as usize;
continue;
}
let data_start = 28usize;
if data_start + cap_len <= block_len as usize - 4 {
let pkt_data = block_data[data_start..data_start + cap_len].to_vec();
let (ts_sec, ts_usec) = iface.ts_to_secs_usecs(ts);
packets.push((ts_sec, ts_usec, pkt_data));
}
}
BLOCK_OPB => {
if block_len < 32 {
pos += block_len as usize;
continue;
}
let iface_id = read_u16(block_data, 8, big_endian).unwrap_or(0) as usize;
let ts_high = read_u32(block_data, 12, big_endian).unwrap_or(0) as u64;
let ts_low = read_u32(block_data, 16, big_endian).unwrap_or(0) as u64;
let cap_len = read_u32(block_data, 20, big_endian).unwrap_or(0) as usize;
let ts = (ts_high << 32) | ts_low;
let iface = interfaces
.get(iface_id)
.cloned()
.unwrap_or(Interface { link_type: 1, ts_resol: 6, ts_offset: 0 });
if iface.link_type != 1 {
pos += block_len as usize;
continue;
}
let data_start = 28usize;
if data_start + cap_len <= block_len as usize - 4 {
let pkt_data = block_data[data_start..data_start + cap_len].to_vec();
let (ts_sec, ts_usec) = iface.ts_to_secs_usecs(ts);
packets.push((ts_sec, ts_usec, pkt_data));
}
}
BLOCK_SPB => {
if block_len < 16 {
pos += block_len as usize;
continue;
}
let orig_len = read_u32(block_data, 8, big_endian).unwrap_or(0) as usize;
let cap_len = orig_len.min(block_len as usize - 16);
let iface = interfaces
.first()
.cloned()
.unwrap_or(Interface { link_type: 1, ts_resol: 6, ts_offset: 0 });
if iface.link_type == 1 && cap_len > 0 {
let pkt_data = block_data[12..12 + cap_len].to_vec();
packets.push((0, 0, pkt_data));
}
}
_ => {
}
}
pos += block_len as usize;
}
Ok(packets)
}
#[cfg(test)]
mod tests {
use super::*;
fn pad4(len: usize) -> usize {
(len + 3) & !3
}
fn build_pcapng_le(frames: &[&[u8]]) -> Vec<u8> {
let mut buf = Vec::new();
let shb_len: u32 = 28;
buf.extend_from_slice(&BLOCK_SHB.to_le_bytes());
buf.extend_from_slice(&shb_len.to_le_bytes());
buf.extend_from_slice(&BYTE_ORDER_MAGIC.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&(-1i64).to_le_bytes()); buf.extend_from_slice(&shb_len.to_le_bytes());
let idb_len: u32 = 20;
buf.extend_from_slice(&BLOCK_IDB.to_le_bytes());
buf.extend_from_slice(&idb_len.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&65535u32.to_le_bytes()); buf.extend_from_slice(&idb_len.to_le_bytes());
for frame in frames {
let padded = pad4(frame.len());
let epb_len = 32 + padded as u32;
buf.extend_from_slice(&BLOCK_EPB.to_le_bytes());
buf.extend_from_slice(&epb_len.to_le_bytes());
buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&1_000_000u32.to_le_bytes()); buf.extend_from_slice(&(frame.len() as u32).to_le_bytes()); buf.extend_from_slice(&(frame.len() as u32).to_le_bytes()); buf.extend_from_slice(frame);
buf.extend(std::iter::repeat(0u8).take(padded - frame.len()));
buf.extend_from_slice(&epb_len.to_le_bytes());
}
buf
}
#[test]
fn rejects_bad_magic() {
let mut file = build_pcapng_le(&[]);
file[0] = 0x00;
assert!(parse_pcapng(&file).is_err());
}
#[test]
fn parses_single_packet() {
let frame = [0xAAu8; 40];
let file = build_pcapng_le(&[&frame]);
let packets = parse_pcapng(&file).unwrap();
assert_eq!(packets.len(), 1);
let (ts_sec, ts_usec, data) = &packets[0];
assert_eq!(*ts_sec, 1);
assert_eq!(*ts_usec, 0);
assert_eq!(data.as_slice(), &frame[..]);
}
#[test]
fn parses_multiple_packets_with_unaligned_length() {
let frame_a = [0x11u8; 7]; let frame_b = [0x22u8; 60];
let file = build_pcapng_le(&[&frame_a, &frame_b]);
let packets = parse_pcapng(&file).unwrap();
assert_eq!(packets.len(), 2);
assert_eq!(packets[0].2, frame_a.to_vec());
assert_eq!(packets[1].2, frame_b.to_vec());
}
#[test]
fn empty_capture_yields_no_packets() {
let file = build_pcapng_le(&[]);
let packets = parse_pcapng(&file).unwrap();
assert!(packets.is_empty());
}
}