pcap-frame-parser 0.2.1

Zero-copy parser for legacy PCAP and PCAPng capture files, plus Ethernet/VLAN/IP/UDP/TCP frame dissection. No libpcap dependency.
Documentation
//! Legacy PCAP file format parser.
//!
//! Spec: <https://wiki.wireshark.org/Development/LibpcapFileFormat>
//!
//! Only Ethernet-linked captures (`network == 1`) are supported; this
//! matches the vast majority of real-world captures produced by
//! `tcpdump`/Wireshark on wired or bridged interfaces.

use nom::{
    bytes::complete::take,
    number::complete::{be_u32, le_u16, le_u32},
    IResult,
};

/// Magic numbers identifying a legacy PCAP file and its byte order.
const MAGIC_LE: u32 = 0xa1b2c3d4; // little-endian, microsecond resolution
const MAGIC_BE: u32 = 0xd4c3b2a1; // big-endian, microsecond resolution

/// Global file header of a legacy PCAP capture.
#[derive(Debug, Clone)]
pub struct PcapHeader {
    pub magic: u32,
    pub version_major: u16,
    pub version_minor: u16,
    pub snaplen: u32,
    /// Link-layer header type (LINKTYPE_*). Only `1` (Ethernet) is
    /// supported by [`iter_packets`].
    pub network: u32,
    pub big_endian: bool,
}

/// A single captured frame with its timestamp.
#[derive(Debug, Clone)]
pub struct RawPacket<'a> {
    pub ts_sec: u32,
    pub ts_usec: u32,
    pub data: &'a [u8],
}

/// Parses the 24-byte global PCAP header.
pub fn parse_pcap_header(input: &[u8]) -> IResult<&[u8], PcapHeader> {
    let (input, magic) = le_u32(input)?;

    let big_endian = match magic {
        MAGIC_LE => false,
        MAGIC_BE => true,
        _ => {
            return Err(nom::Err::Error(nom::error::Error::new(
                input,
                nom::error::ErrorKind::Tag,
            )))
        }
    };

    if big_endian {
        let (input, version_major) = nom::number::complete::be_u16(input)?;
        let (input, version_minor) = nom::number::complete::be_u16(input)?;
        let (input, _thiszone) = be_u32(input)?;
        let (input, _sigfigs) = be_u32(input)?;
        let (input, snaplen) = be_u32(input)?;
        let (input, network) = be_u32(input)?;
        Ok((
            input,
            PcapHeader {
                magic,
                version_major,
                version_minor,
                snaplen,
                network,
                big_endian,
            },
        ))
    } else {
        let (input, version_major) = le_u16(input)?;
        let (input, version_minor) = le_u16(input)?;
        let (input, _thiszone) = le_u32(input)?;
        let (input, _sigfigs) = le_u32(input)?;
        let (input, snaplen) = le_u32(input)?;
        let (input, network) = le_u32(input)?;
        Ok((
            input,
            PcapHeader {
                magic,
                version_major,
                version_minor,
                snaplen,
                network,
                big_endian,
            },
        ))
    }
}

/// Parses a single packet record (16-byte record header + captured bytes).
pub fn parse_packet(input: &[u8], big_endian: bool) -> IResult<&[u8], RawPacket<'_>> {
    let (input, ts_sec) = if big_endian {
        be_u32(input)?
    } else {
        le_u32(input)?
    };
    let (input, ts_usec) = if big_endian {
        be_u32(input)?
    } else {
        le_u32(input)?
    };
    let (input, incl_len) = if big_endian {
        be_u32(input)?
    } else {
        le_u32(input)?
    };
    let (input, _orig_len) = if big_endian {
        be_u32(input)?
    } else {
        le_u32(input)?
    };
    let (input, data) = take(incl_len)(input)?;

    Ok((
        input,
        RawPacket {
            ts_sec,
            ts_usec,
            data,
        },
    ))
}

/// Parses a full legacy PCAP buffer and returns the header plus every
/// packet it contains. Truncated trailing records are silently dropped
/// rather than causing the whole capture to be rejected, matching the
/// tolerant behavior of Wireshark.
///
/// # Errors
/// Returns `Err` if the global header is malformed or the link-layer
/// type is not Ethernet (`network != 1`).
pub fn iter_packets(mut input: &[u8]) -> Result<(PcapHeader, Vec<RawPacket<'_>>), String> {
    let (rest, header) =
        parse_pcap_header(input).map_err(|e| format!("failed to parse PCAP header: {e:?}"))?;

    if header.network != 1 {
        return Err(format!(
            "unsupported link type: {} (only Ethernet/1 is supported)",
            header.network
        ));
    }

    input = rest;
    let mut packets = Vec::new();

    while !input.is_empty() {
        match parse_packet(input, header.big_endian) {
            Ok((rest, pkt)) => {
                packets.push(pkt);
                input = rest;
            }
            Err(_) => break,
        }
    }

    Ok((header, packets))
}

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

    fn le_u32_bytes(v: u32) -> [u8; 4] {
        v.to_le_bytes()
    }
    fn le_u16_bytes(v: u16) -> [u8; 2] {
        v.to_le_bytes()
    }

    /// Builds a minimal little-endian PCAP file with the given raw frames.
    fn build_pcap_le(frames: &[&[u8]]) -> Vec<u8> {
        let mut buf = Vec::new();
        buf.extend_from_slice(&le_u32_bytes(MAGIC_LE));
        buf.extend_from_slice(&le_u16_bytes(2)); // version_major
        buf.extend_from_slice(&le_u16_bytes(4)); // version_minor
        buf.extend_from_slice(&le_u32_bytes(0)); // thiszone
        buf.extend_from_slice(&le_u32_bytes(0)); // sigfigs
        buf.extend_from_slice(&le_u32_bytes(65535)); // snaplen
        buf.extend_from_slice(&le_u32_bytes(1)); // network = Ethernet

        for (i, frame) in frames.iter().enumerate() {
            buf.extend_from_slice(&le_u32_bytes(1_700_000_000 + i as u32)); // ts_sec
            buf.extend_from_slice(&le_u32_bytes(0)); // ts_usec
            buf.extend_from_slice(&le_u32_bytes(frame.len() as u32)); // incl_len
            buf.extend_from_slice(&le_u32_bytes(frame.len() as u32)); // orig_len
            buf.extend_from_slice(frame);
        }
        buf
    }

    #[test]
    fn parses_header_little_endian() {
        let file = build_pcap_le(&[]);
        let (_, header) = parse_pcap_header(&file).unwrap();
        assert!(!header.big_endian);
        assert_eq!(header.network, 1);
        assert_eq!(header.snaplen, 65535);
    }

    #[test]
    fn rejects_bad_magic() {
        let mut file = build_pcap_le(&[]);
        file[0] = 0xff;
        assert!(parse_pcap_header(&file).is_err());
    }

    #[test]
    fn iterates_multiple_packets() {
        let frame_a = [0xAAu8; 20];
        let frame_b = [0xBBu8; 34];
        let file = build_pcap_le(&[&frame_a, &frame_b]);

        let (header, packets) = iter_packets(&file).unwrap();
        assert_eq!(header.network, 1);
        assert_eq!(packets.len(), 2);
        assert_eq!(packets[0].data, &frame_a[..]);
        assert_eq!(packets[1].data, &frame_b[..]);
    }

    #[test]
    fn rejects_non_ethernet_linktype() {
        let mut file = build_pcap_le(&[]);
        // Patch network field (last 4 bytes of the 24-byte global header) to
        // LINKTYPE_RAW (101) instead of Ethernet (1).
        file[20..24].copy_from_slice(&le_u32_bytes(101));
        assert!(iter_packets(&file).is_err());
    }

    #[test]
    fn truncated_trailing_record_is_dropped_not_fatal() {
        let frame_a = [0xCCu8; 16];
        let mut file = build_pcap_le(&[&frame_a]);
        // Append a truncated, incomplete record header at the end.
        file.extend_from_slice(&[0u8; 5]);

        let (_, packets) = iter_packets(&file).unwrap();
        assert_eq!(packets.len(), 1);
    }
}