pcap-frame-parser 0.1.0

Zero-copy parser for legacy PCAP and PCAPng capture files, plus Ethernet/VLAN/IP/UDP/TCP frame dissection. No libpcap dependency.
Documentation
//! PCAPng file format parser.
//!
//! Spec: <https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-05.txt>
//!
//! Only the block types produced by common capture tools are handled:
//! Section Header Block, Interface Description Block, Enhanced/Obsolete
//! Packet Block, and Simple Packet Block. Unknown block types are
//! skipped using their declared length, so unrecognized-but-well-formed
//! files still parse successfully.

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; // Obsolete Packet Block
const BLOCK_SPB: u32 = 0x0000_0003;
const BLOCK_EPB: u32 = 0x0000_0006;

/// Per-interface metadata parsed out of an Interface Description Block,
/// needed to convert packet timestamps to seconds/microseconds.
#[derive(Debug, Clone)]
struct Interface {
    link_type: u16,
    /// Timestamp resolution: interpreted as 10^-resol seconds, except
    /// the common defaults 6 (microseconds) and 9 (nanoseconds) which
    /// are handled directly for clarity and precision.
    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]])
    })
}

/// Parses a PCAPng buffer and returns every Ethernet-linked packet found
/// as `(ts_sec, ts_usec, frame_bytes)`. Non-Ethernet interfaces are
/// skipped. Packet data is copied out (owned `Vec<u8>`) because PCAPng
/// blocks are not guaranteed to be contiguous with a single packet's
/// logical payload once padding/options are accounted for.
///
/// # Errors
/// Returns `Err` if the buffer is too short, doesn't start with a
/// Section Header Block, or declares an invalid byte-order magic.
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() {
        // SHB's magic (0x0A0D0D0A) reads identically in both byte orders,
        // so it's always safe to read block_type with the current
        // endianness guess before it's been confirmed.
        let block_type = read_u32(data, pos, big_endian).ok_or("failed to read block type")?;
        // block_len for an SHB is always read little-endian first, since
        // the byte-order magic that disambiguates it lives inside the
        // block itself, at offset 8.
        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 => {
                // offset 8: Byte-Order Magic
                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}")),
                };
                // A new section resets the interface table.
                interfaces.clear();
            }

            BLOCK_IDB => {
                // offset 8: LinkType (2), Reserved (2), SnapLen (4), then options
                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; // default: microseconds
                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; // opt_endofopt
                    }
                    if opos + opt_len <= opts_end {
                        match opt_code {
                            9 => {
                                // if_tsresol
                                if opt_len >= 1 {
                                    ts_resol = block_data[opos];
                                }
                            }
                            14 => {
                                // if_tsoffset
                                if opt_len >= 8 {
                                    ts_offset = read_u64(block_data, opos, big_endian).unwrap_or(0);
                                }
                            }
                            _ => {}
                        }
                    }
                    // Options are padded to 4-byte boundaries.
                    opos += (opt_len + 3) & !3;
                }

                interfaces.push(Interface { link_type, ts_resol, ts_offset });
            }

            BLOCK_EPB => {
                // offset 8:  Interface ID (4)
                // offset 12: Timestamp High (4)
                // offset 16: Timestamp Low (4)
                // offset 20: Captured Packet Length (4)
                // offset 24: Original Packet Length (4)
                // offset 28: Packet Data
                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 => {
                // Obsolete Packet Block (pre-EPB format).
                // offset 8:  Interface ID (2), Drops Count (2)
                // offset 12: Timestamp High (4)
                // offset 16: Timestamp Low (4)
                // offset 20: Captured Length (4)
                // offset 24: Original Length (4)
                // offset 28: Packet Data
                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 => {
                // Simple Packet Block — no timestamp available, reported as 0.
                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));
                }
            }

            _ => {
                // Unknown block type — skip using its declared length.
            }
        }

        pos += block_len as usize;
    }

    Ok(packets)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn pad4(len: usize) -> usize {
        (len + 3) & !3
    }

    /// Builds a minimal little-endian PCAPng file with one interface and
    /// the given Ethernet frames, each wrapped in an Enhanced Packet Block.
    fn build_pcapng_le(frames: &[&[u8]]) -> Vec<u8> {
        let mut buf = Vec::new();

        // Section Header Block: type, len, BOM, major, minor, section_len(-1), len(again)
        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()); // major
        buf.extend_from_slice(&0u16.to_le_bytes()); // minor
        buf.extend_from_slice(&(-1i64).to_le_bytes()); // section length unknown
        buf.extend_from_slice(&shb_len.to_le_bytes());

        // Interface Description Block: type, len, linktype+reserved, snaplen, len(again)
        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()); // LinkType = Ethernet
        buf.extend_from_slice(&0u16.to_le_bytes()); // reserved
        buf.extend_from_slice(&65535u32.to_le_bytes()); // snaplen
        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()); // interface id
            buf.extend_from_slice(&0u32.to_le_bytes()); // ts high
            buf.extend_from_slice(&1_000_000u32.to_le_bytes()); // ts low = 1s in microseconds
            buf.extend_from_slice(&(frame.len() as u32).to_le_bytes()); // captured len
            buf.extend_from_slice(&(frame.len() as u32).to_le_bytes()); // original len
            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]; // not a multiple of 4, exercises padding
        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());
    }
}