Skip to main content

pcap_frame_parser/
pcap.rs

1//! Legacy PCAP file format parser.
2//!
3//! Spec: <https://wiki.wireshark.org/Development/LibpcapFileFormat>
4//!
5//! Only Ethernet-linked captures (`network == 1`) are supported; this
6//! matches the vast majority of real-world captures produced by
7//! `tcpdump`/Wireshark on wired or bridged interfaces.
8
9use nom::{
10    bytes::complete::take,
11    number::complete::{be_u32, le_u16, le_u32},
12    IResult,
13};
14
15/// Magic numbers identifying a legacy PCAP file and its byte order.
16const MAGIC_LE: u32 = 0xa1b2c3d4; // little-endian, microsecond resolution
17const MAGIC_BE: u32 = 0xd4c3b2a1; // big-endian, microsecond resolution
18
19/// Global file header of a legacy PCAP capture.
20#[derive(Debug, Clone)]
21pub struct PcapHeader {
22    pub magic: u32,
23    pub version_major: u16,
24    pub version_minor: u16,
25    pub snaplen: u32,
26    /// Link-layer header type (LINKTYPE_*). Only `1` (Ethernet) is
27    /// supported by [`iter_packets`].
28    pub network: u32,
29    pub big_endian: bool,
30}
31
32/// A single captured frame with its timestamp.
33#[derive(Debug, Clone)]
34pub struct RawPacket<'a> {
35    pub ts_sec: u32,
36    pub ts_usec: u32,
37    pub data: &'a [u8],
38}
39
40/// Parses the 24-byte global PCAP header.
41pub fn parse_pcap_header(input: &[u8]) -> IResult<&[u8], PcapHeader> {
42    let (input, magic) = le_u32(input)?;
43
44    let big_endian = match magic {
45        MAGIC_LE => false,
46        MAGIC_BE => true,
47        _ => {
48            return Err(nom::Err::Error(nom::error::Error::new(
49                input,
50                nom::error::ErrorKind::Tag,
51            )))
52        }
53    };
54
55    if big_endian {
56        let (input, version_major) = nom::number::complete::be_u16(input)?;
57        let (input, version_minor) = nom::number::complete::be_u16(input)?;
58        let (input, _thiszone) = be_u32(input)?;
59        let (input, _sigfigs) = be_u32(input)?;
60        let (input, snaplen) = be_u32(input)?;
61        let (input, network) = be_u32(input)?;
62        Ok((
63            input,
64            PcapHeader {
65                magic,
66                version_major,
67                version_minor,
68                snaplen,
69                network,
70                big_endian,
71            },
72        ))
73    } else {
74        let (input, version_major) = le_u16(input)?;
75        let (input, version_minor) = le_u16(input)?;
76        let (input, _thiszone) = le_u32(input)?;
77        let (input, _sigfigs) = le_u32(input)?;
78        let (input, snaplen) = le_u32(input)?;
79        let (input, network) = le_u32(input)?;
80        Ok((
81            input,
82            PcapHeader {
83                magic,
84                version_major,
85                version_minor,
86                snaplen,
87                network,
88                big_endian,
89            },
90        ))
91    }
92}
93
94/// Parses a single packet record (16-byte record header + captured bytes).
95pub fn parse_packet(input: &[u8], big_endian: bool) -> IResult<&[u8], RawPacket<'_>> {
96    let (input, ts_sec) = if big_endian {
97        be_u32(input)?
98    } else {
99        le_u32(input)?
100    };
101    let (input, ts_usec) = if big_endian {
102        be_u32(input)?
103    } else {
104        le_u32(input)?
105    };
106    let (input, incl_len) = if big_endian {
107        be_u32(input)?
108    } else {
109        le_u32(input)?
110    };
111    let (input, _orig_len) = if big_endian {
112        be_u32(input)?
113    } else {
114        le_u32(input)?
115    };
116    let (input, data) = take(incl_len)(input)?;
117
118    Ok((
119        input,
120        RawPacket {
121            ts_sec,
122            ts_usec,
123            data,
124        },
125    ))
126}
127
128/// Parses a full legacy PCAP buffer and returns the header plus every
129/// packet it contains. Truncated trailing records are silently dropped
130/// rather than causing the whole capture to be rejected, matching the
131/// tolerant behavior of Wireshark.
132///
133/// # Errors
134/// Returns `Err` if the global header is malformed or the link-layer
135/// type is not Ethernet (`network != 1`).
136pub fn iter_packets(mut input: &[u8]) -> Result<(PcapHeader, Vec<RawPacket<'_>>), String> {
137    let (rest, header) =
138        parse_pcap_header(input).map_err(|e| format!("failed to parse PCAP header: {e:?}"))?;
139
140    if header.network != 1 {
141        return Err(format!(
142            "unsupported link type: {} (only Ethernet/1 is supported)",
143            header.network
144        ));
145    }
146
147    input = rest;
148    let mut packets = Vec::new();
149
150    while !input.is_empty() {
151        match parse_packet(input, header.big_endian) {
152            Ok((rest, pkt)) => {
153                packets.push(pkt);
154                input = rest;
155            }
156            Err(_) => break,
157        }
158    }
159
160    Ok((header, packets))
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    fn le_u32_bytes(v: u32) -> [u8; 4] {
168        v.to_le_bytes()
169    }
170    fn le_u16_bytes(v: u16) -> [u8; 2] {
171        v.to_le_bytes()
172    }
173
174    /// Builds a minimal little-endian PCAP file with the given raw frames.
175    fn build_pcap_le(frames: &[&[u8]]) -> Vec<u8> {
176        let mut buf = Vec::new();
177        buf.extend_from_slice(&le_u32_bytes(MAGIC_LE));
178        buf.extend_from_slice(&le_u16_bytes(2)); // version_major
179        buf.extend_from_slice(&le_u16_bytes(4)); // version_minor
180        buf.extend_from_slice(&le_u32_bytes(0)); // thiszone
181        buf.extend_from_slice(&le_u32_bytes(0)); // sigfigs
182        buf.extend_from_slice(&le_u32_bytes(65535)); // snaplen
183        buf.extend_from_slice(&le_u32_bytes(1)); // network = Ethernet
184
185        for (i, frame) in frames.iter().enumerate() {
186            buf.extend_from_slice(&le_u32_bytes(1_700_000_000 + i as u32)); // ts_sec
187            buf.extend_from_slice(&le_u32_bytes(0)); // ts_usec
188            buf.extend_from_slice(&le_u32_bytes(frame.len() as u32)); // incl_len
189            buf.extend_from_slice(&le_u32_bytes(frame.len() as u32)); // orig_len
190            buf.extend_from_slice(frame);
191        }
192        buf
193    }
194
195    #[test]
196    fn parses_header_little_endian() {
197        let file = build_pcap_le(&[]);
198        let (_, header) = parse_pcap_header(&file).unwrap();
199        assert!(!header.big_endian);
200        assert_eq!(header.network, 1);
201        assert_eq!(header.snaplen, 65535);
202    }
203
204    #[test]
205    fn rejects_bad_magic() {
206        let mut file = build_pcap_le(&[]);
207        file[0] = 0xff;
208        assert!(parse_pcap_header(&file).is_err());
209    }
210
211    #[test]
212    fn iterates_multiple_packets() {
213        let frame_a = [0xAAu8; 20];
214        let frame_b = [0xBBu8; 34];
215        let file = build_pcap_le(&[&frame_a, &frame_b]);
216
217        let (header, packets) = iter_packets(&file).unwrap();
218        assert_eq!(header.network, 1);
219        assert_eq!(packets.len(), 2);
220        assert_eq!(packets[0].data, &frame_a[..]);
221        assert_eq!(packets[1].data, &frame_b[..]);
222    }
223
224    #[test]
225    fn rejects_non_ethernet_linktype() {
226        let mut file = build_pcap_le(&[]);
227        // Patch network field (last 4 bytes of the 24-byte global header) to
228        // LINKTYPE_RAW (101) instead of Ethernet (1).
229        file[20..24].copy_from_slice(&le_u32_bytes(101));
230        assert!(iter_packets(&file).is_err());
231    }
232
233    #[test]
234    fn truncated_trailing_record_is_dropped_not_fatal() {
235        let frame_a = [0xCCu8; 16];
236        let mut file = build_pcap_le(&[&frame_a]);
237        // Append a truncated, incomplete record header at the end.
238        file.extend_from_slice(&[0u8; 5]);
239
240        let (_, packets) = iter_packets(&file).unwrap();
241        assert_eq!(packets.len(), 1);
242    }
243}