pcapsql_core/pcap/
reader.rs

1//! PCAP file reader with automatic compression handling.
2//!
3//! This module provides [`PcapReader`], a convenience wrapper around
4//! [`GenericPcapReader`](crate::io::GenericPcapReader) that handles:
5//! - File I/O
6//! - Automatic compression detection and decompression
7//! - PCAP format detection (Legacy vs PCAPNG)
8
9use std::fs::File;
10use std::io::{Read, Seek, SeekFrom};
11use std::path::Path;
12
13use crate::error::{Error, PcapError as OurPcapError};
14use crate::io::{Compression, FileDecoder, GenericPcapReader, PacketRef, PcapFormat, RawPacket};
15
16/// Reader for PCAP and PCAPNG files, with optional decompression.
17///
18/// This is a thin wrapper around [`GenericPcapReader`] that adds:
19/// - File opening with path-based API
20/// - Automatic compression detection (gzip, zstd, etc.)
21/// - Automatic PCAP format detection
22///
23/// # Supported Compression Formats
24///
25/// - Gzip (.gz) - always enabled
26/// - Zstd (.zst) - `compress-zstd` feature
27/// - LZ4 (.lz4) - `compress-lz4` feature
28/// - Bzip2 (.bz2) - `compress-bzip2` feature
29/// - XZ (.xz) - `compress-xz` feature
30///
31/// # Example
32///
33/// ```ignore
34/// use pcapsql_core::pcap::PcapReader;
35///
36/// let mut reader = PcapReader::open("capture.pcap.gz")?;
37/// while let Some(packet) = reader.next_packet()? {
38///     println!("Frame {}: {} bytes", packet.frame_number, packet.data.len());
39/// }
40/// ```
41pub struct PcapReader {
42    inner: GenericPcapReader<FileDecoder>,
43}
44
45impl PcapReader {
46    /// Open a PCAP file for reading.
47    ///
48    /// Automatically detects and handles compressed files.
49    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
50        let path = path.as_ref();
51
52        // Read first bytes to detect compression
53        let mut file = File::open(path).map_err(|_| {
54            Error::Pcap(OurPcapError::FileNotFound {
55                path: path.display().to_string(),
56            })
57        })?;
58
59        let mut header = [0u8; 6];
60        let bytes_read = file.read(&mut header).map_err(|_| {
61            Error::Pcap(OurPcapError::InvalidFormat {
62                reason: "File too short to read header".to_string(),
63            })
64        })?;
65
66        if bytes_read < 4 {
67            return Err(Error::Pcap(OurPcapError::InvalidFormat {
68                reason: "File too short".to_string(),
69            }));
70        }
71
72        // Detect compression format
73        let compression = Compression::detect(&header);
74
75        // Seek back to start
76        file.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
77
78        // Create decoder
79        let decoder = FileDecoder::new(file, compression).map_err(|e| {
80            Error::Pcap(OurPcapError::InvalidFormat {
81                reason: format!("Failed to create decoder: {e}"),
82            })
83        })?;
84
85        // We need to read the magic bytes after decompression to detect PCAP format.
86        // Unfortunately, this requires us to re-open the file since we consumed bytes.
87        // First, let's read the magic from a temporary decoder.
88        let mut temp_decoder = decoder;
89        let mut magic = [0u8; 4];
90        temp_decoder.read_exact(&mut magic).map_err(|_| {
91            Error::Pcap(OurPcapError::InvalidFormat {
92                reason: "File too short to read magic number".to_string(),
93            })
94        })?;
95
96        // Detect PCAP format from magic bytes
97        let format = PcapFormat::detect(&magic)?;
98
99        // Re-open file and create fresh decoder
100        drop(temp_decoder);
101        let file = File::open(path)?;
102        let decoder = FileDecoder::new(file, compression).map_err(|e| {
103            Error::Pcap(OurPcapError::InvalidFormat {
104                reason: format!("Failed to create decoder: {e}"),
105            })
106        })?;
107
108        // Create the generic reader
109        let inner = GenericPcapReader::with_format(decoder, format)?;
110
111        Ok(Self { inner })
112    }
113
114    /// Get the link type of the capture (e.g., 1 = Ethernet).
115    #[inline]
116    pub fn link_type(&self) -> u16 {
117        self.inner.link_type() as u16
118    }
119
120    /// Get the current frame count.
121    #[inline]
122    pub fn frame_count(&self) -> u64 {
123        self.inner.frame_count()
124    }
125
126    /// Read the next packet.
127    ///
128    /// Returns `Ok(None)` at end of file.
129    #[inline]
130    pub fn next_packet(&mut self) -> Result<Option<RawPacket>, Error> {
131        self.inner.next_packet()
132    }
133
134    /// Process packets with zero-copy borrowed data.
135    ///
136    /// The callback receives borrowed packet data. The borrow is valid
137    /// only during the callback - data must be processed before returning.
138    /// This eliminates the copy overhead of `next_packet()`.
139    ///
140    /// Returns the number of packets processed.
141    #[inline]
142    pub fn process_packets<F>(&mut self, max: usize, f: F) -> Result<usize, Error>
143    where
144        F: FnMut(PacketRef<'_>) -> Result<(), Error>,
145    {
146        self.inner.process_packets(max, f)
147    }
148}
149
150/// Iterator adapter for PcapReader.
151impl Iterator for PcapReader {
152    type Item = Result<RawPacket, Error>;
153
154    fn next(&mut self) -> Option<Self::Item> {
155        match self.next_packet() {
156            Ok(Some(packet)) => Some(Ok(packet)),
157            Ok(None) => None,
158            Err(e) => Some(Err(e)),
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use flate2::write::GzEncoder;
167    use flate2::Compression as GzCompression;
168    use std::io::Write;
169    use tempfile::NamedTempFile;
170
171    #[test]
172    fn test_compression_detection() {
173        // Gzip magic
174        let gzip_data = [0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00];
175        assert_eq!(Compression::detect(&gzip_data), Compression::Gzip);
176
177        // PCAP magic (no compression)
178        let pcap_data = [0xd4, 0xc3, 0xb2, 0xa1, 0x00, 0x00];
179        assert_eq!(Compression::detect(&pcap_data), Compression::None);
180    }
181
182    #[test]
183    fn test_create_and_read_gzip_pcap() {
184        // Create a minimal valid PCAP file
185        let pcap_data = create_minimal_pcap();
186
187        // Compress it with gzip
188        let temp = NamedTempFile::with_suffix(".pcap.gz").unwrap();
189        {
190            let file = File::create(temp.path()).unwrap();
191            let mut encoder = GzEncoder::new(file, GzCompression::default());
192            encoder.write_all(&pcap_data).unwrap();
193            encoder.finish().unwrap();
194        }
195
196        // Try to open it
197        let reader = PcapReader::open(temp.path());
198        assert!(
199            reader.is_ok(),
200            "Failed to open gzipped PCAP: {:?}",
201            reader.err()
202        );
203    }
204
205    #[cfg(feature = "compress-zstd")]
206    #[test]
207    fn test_create_and_read_zstd_pcap() {
208        // Create a minimal valid PCAP file
209        let pcap_data = create_minimal_pcap();
210
211        // Compress it with zstd
212        let temp = NamedTempFile::with_suffix(".pcap.zst").unwrap();
213        {
214            let file = File::create(temp.path()).unwrap();
215            let mut encoder = zstd::Encoder::new(file, 3).unwrap();
216            encoder.write_all(&pcap_data).unwrap();
217            encoder.finish().unwrap();
218        }
219
220        // Try to open it
221        let reader = PcapReader::open(temp.path());
222        assert!(
223            reader.is_ok(),
224            "Failed to open zstd PCAP: {:?}",
225            reader.err()
226        );
227    }
228
229    /// Create a minimal valid PCAP file with one packet.
230    fn create_minimal_pcap() -> Vec<u8> {
231        let mut data = Vec::new();
232
233        // PCAP global header
234        data.extend_from_slice(&[0xd4, 0xc3, 0xb2, 0xa1]); // Magic (little endian)
235        data.extend_from_slice(&[0x02, 0x00]); // Version major (2)
236        data.extend_from_slice(&[0x04, 0x00]); // Version minor (4)
237        data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Thiszone
238        data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Sigfigs
239        data.extend_from_slice(&[0xff, 0xff, 0x00, 0x00]); // Snaplen (65535)
240        data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Network (Ethernet)
241
242        // One packet header + minimal Ethernet frame
243        let packet_data = [
244            // Ethernet header (14 bytes)
245            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // Dst MAC
246            0x00, 0x11, 0x22, 0x33, 0x44, 0x55, // Src MAC
247            0x08, 0x00, // EtherType (IPv4)
248        ];
249
250        let ts_sec: u32 = 1000000000;
251        let ts_usec: u32 = 0;
252        let caplen: u32 = packet_data.len() as u32;
253        let origlen: u32 = packet_data.len() as u32;
254
255        data.extend_from_slice(&ts_sec.to_le_bytes());
256        data.extend_from_slice(&ts_usec.to_le_bytes());
257        data.extend_from_slice(&caplen.to_le_bytes());
258        data.extend_from_slice(&origlen.to_le_bytes());
259        data.extend_from_slice(&packet_data);
260
261        data
262    }
263}