pcapsql_core/io/
pcap_stream.rs

1//! Generic PCAP/PCAPNG reader over any Read source.
2//!
3//! This module provides a unified PCAP parser that works with any `R: Read` source,
4//! using the battle-tested `pcap_parser` crate. Benchmarks show this approach is
5//! actually 30% faster than custom byte parsing while being more maintainable.
6//!
7//! ## Usage
8//!
9//! ```ignore
10//! use std::fs::File;
11//! use std::io::Cursor;
12//!
13//! // From a file with known format
14//! let file = File::open("capture.pcap")?;
15//! let reader = GenericPcapReader::with_format(file, PcapFormat::LegacyLeMicro)?;
16//!
17//! // From memory-mapped data
18//! let mmap = Arc::new(unsafe { Mmap::map(&file)? });
19//! let cursor = Cursor::new(MmapSlice::new(mmap));
20//! let reader = GenericPcapReader::with_format(cursor, PcapFormat::LegacyLeMicro)?;
21//! ```
22
23use std::io::{BufReader, Read};
24
25use bytes::Bytes;
26use pcap_parser::traits::PcapReaderIterator;
27use pcap_parser::{LegacyPcapReader, PcapBlockOwned, PcapNGReader};
28
29use crate::error::{Error, PcapError};
30use crate::io::{PacketRef, RawPacket};
31
32/// Buffer size for pcap_parser readers (64KB).
33const BUFFER_SIZE: usize = 262144;
34
35/// Format of the PCAP file.
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum PcapFormat {
38    /// Classic PCAP (little-endian, microseconds)
39    LegacyLeMicro,
40    /// Classic PCAP (big-endian, microseconds)
41    LegacyBeMicro,
42    /// Classic PCAP (little-endian, nanoseconds)
43    LegacyLeNano,
44    /// Classic PCAP (big-endian, nanoseconds)
45    LegacyBeNano,
46    /// PCAPNG format
47    PcapNg,
48}
49
50impl PcapFormat {
51    /// Detect PCAP format from magic bytes.
52    pub fn detect(data: &[u8]) -> Result<Self, Error> {
53        if data.len() < 4 {
54            return Err(Error::Pcap(PcapError::InvalidFormat {
55                reason: "Data too small for PCAP magic".into(),
56            }));
57        }
58
59        let magic = u32::from_ne_bytes([data[0], data[1], data[2], data[3]]);
60
61        match magic {
62            0xa1b2c3d4 => Ok(PcapFormat::LegacyLeMicro),
63            0xd4c3b2a1 => Ok(PcapFormat::LegacyBeMicro),
64            0xa1b23c4d => Ok(PcapFormat::LegacyLeNano),
65            0x4d3cb2a1 => Ok(PcapFormat::LegacyBeNano),
66            0x0a0d0d0a => Ok(PcapFormat::PcapNg),
67            _ => Err(Error::Pcap(PcapError::InvalidFormat {
68                reason: format!("Unknown PCAP magic: 0x{magic:08x}"),
69            })),
70        }
71    }
72
73    /// Whether this is a PCAPNG format.
74    pub fn is_pcapng(&self) -> bool {
75        matches!(self, PcapFormat::PcapNg)
76    }
77
78    /// Whether this is a legacy PCAP format.
79    pub fn is_legacy(&self) -> bool {
80        !self.is_pcapng()
81    }
82
83    /// Whether this format uses little-endian byte order.
84    ///
85    /// This is relevant for parsing header fields like link_type.
86    /// For PCAPNG, the section header defines endianness, but we assume
87    /// little-endian as it's the most common case.
88    pub fn is_little_endian(&self) -> bool {
89        match self {
90            PcapFormat::LegacyLeMicro | PcapFormat::LegacyLeNano => true,
91            PcapFormat::LegacyBeMicro | PcapFormat::LegacyBeNano => false,
92            PcapFormat::PcapNg => true, // Most PCAPNG files are little-endian
93        }
94    }
95}
96
97/// Generic PCAP/PCAPNG reader over any Read source.
98///
99/// This is the unified PCAP parser that uses `pcap_parser` for all formats.
100/// It provides a simple `next_packet()` interface that works with any byte source.
101pub struct GenericPcapReader<R: Read> {
102    inner: ReaderInner<R>,
103    frame_number: u64,
104    link_type: u32,
105}
106
107/// Inner reader using enum dispatch for format-specific handling.
108enum ReaderInner<R: Read> {
109    Legacy(LegacyPcapReader<BufReader<R>>),
110    Ng(PcapNGReader<BufReader<R>>),
111}
112
113impl<R: Read> GenericPcapReader<R> {
114    /// Create a reader with known format.
115    ///
116    /// This is the primary constructor. Use `PcapFormat::detect()` to determine
117    /// the format from magic bytes before calling this.
118    ///
119    /// Note: For legacy PCAP, the header is read during construction to extract
120    /// the link_type. For PCAPNG, link_type is initially 1 and gets updated when
121    /// the Interface Description Block is read during packet processing.
122    pub fn with_format(source: R, format: PcapFormat) -> Result<Self, Error> {
123        let buf_reader = BufReader::with_capacity(BUFFER_SIZE, source);
124
125        let (inner, link_type) = if format.is_pcapng() {
126            let reader = PcapNGReader::new(BUFFER_SIZE, buf_reader).map_err(|e| {
127                Error::Pcap(PcapError::InvalidFormat {
128                    reason: format!("Failed to parse PCAPNG: {e}"),
129                })
130            })?;
131            // PCAPNG link_type comes from IDB block, read during packet processing
132            (ReaderInner::Ng(reader), 1u32)
133        } else {
134            let mut reader = LegacyPcapReader::new(BUFFER_SIZE, buf_reader).map_err(|e| {
135                Error::Pcap(PcapError::InvalidFormat {
136                    reason: format!("Failed to parse legacy PCAP: {e}"),
137                })
138            })?;
139            // Read ahead to get link_type from the legacy PCAP header
140            let link_type = Self::read_legacy_header_link_type(&mut reader)?;
141            (ReaderInner::Legacy(reader), link_type)
142        };
143
144        Ok(GenericPcapReader {
145            inner,
146            frame_number: 0,
147            link_type,
148        })
149    }
150
151    /// Read the legacy PCAP header to extract link_type.
152    ///
153    /// This consumes the header block but leaves the reader positioned
154    /// at the first packet.
155    fn read_legacy_header_link_type<S: Read>(
156        reader: &mut LegacyPcapReader<S>,
157    ) -> Result<u32, Error> {
158        use pcap_parser::PcapError as PcapParserError;
159
160        loop {
161            match reader.next() {
162                Ok((offset, block)) => match block {
163                    PcapBlockOwned::LegacyHeader(header) => {
164                        let link_type = header.network.0 as u32;
165                        reader.consume(offset);
166                        return Ok(link_type);
167                    }
168                    PcapBlockOwned::Legacy(_) => {
169                        // Shouldn't happen - header should come first
170                        // Don't consume, let normal reading handle it
171                        return Ok(1); // Default to Ethernet
172                    }
173                    _ => {
174                        reader.consume(offset);
175                        continue;
176                    }
177                },
178                Err(PcapParserError::Eof) => {
179                    return Ok(1); // Empty file, default to Ethernet
180                }
181                Err(PcapParserError::Incomplete(_)) => {
182                    reader.refill().map_err(|e| {
183                        Error::Pcap(PcapError::InvalidFormat {
184                            reason: format!("Failed to read PCAP header: {e}"),
185                        })
186                    })?;
187                    continue;
188                }
189                Err(e) => {
190                    return Err(Error::Pcap(PcapError::InvalidFormat {
191                        reason: format!("Failed to parse PCAP header: {e}"),
192                    }));
193                }
194            }
195        }
196    }
197
198    /// Read the next packet.
199    ///
200    /// Returns `Ok(None)` at end of file.
201    pub fn next_packet(&mut self) -> Result<Option<RawPacket>, Error> {
202        match &mut self.inner {
203            ReaderInner::Legacy(reader) => {
204                read_legacy_packet(reader, &mut self.frame_number, &mut self.link_type)
205            }
206            ReaderInner::Ng(reader) => {
207                read_pcapng_packet(reader, &mut self.frame_number, &mut self.link_type)
208            }
209        }
210    }
211
212    /// Get the link type (e.g., 1 = Ethernet).
213    pub fn link_type(&self) -> u32 {
214        self.link_type
215    }
216
217    /// Get the current frame count.
218    pub fn frame_count(&self) -> u64 {
219        self.frame_number
220    }
221
222    /// Process packets with zero-copy borrowed data.
223    ///
224    /// The callback receives borrowed packet data. The borrow is valid
225    /// only during the callback - data must be processed before returning.
226    /// This eliminates the `Bytes::copy_from_slice()` overhead of `next_packet()`.
227    ///
228    /// Returns the number of packets processed.
229    #[inline]
230    pub fn process_packets<F>(&mut self, max: usize, f: F) -> Result<usize, Error>
231    where
232        F: FnMut(PacketRef<'_>) -> Result<(), Error>,
233    {
234        match &mut self.inner {
235            ReaderInner::Legacy(reader) => {
236                process_legacy_packets(reader, max, &mut self.frame_number, &mut self.link_type, f)
237            }
238            ReaderInner::Ng(reader) => {
239                process_pcapng_packets(reader, max, &mut self.frame_number, &mut self.link_type, f)
240            }
241        }
242    }
243}
244
245/// Read next packet from a legacy PCAP reader.
246fn read_legacy_packet<S: Read>(
247    reader: &mut LegacyPcapReader<S>,
248    frame_number: &mut u64,
249    link_type: &mut u32,
250) -> Result<Option<RawPacket>, Error> {
251    use pcap_parser::PcapError as PcapParserError;
252
253    loop {
254        match reader.next() {
255            Ok((offset, block)) => match block {
256                PcapBlockOwned::Legacy(packet) => {
257                    *frame_number += 1;
258
259                    let timestamp_us = (packet.ts_sec as i64) * 1_000_000 + (packet.ts_usec as i64);
260
261                    let raw = RawPacket {
262                        frame_number: *frame_number,
263                        timestamp_us,
264                        captured_length: packet.caplen,
265                        original_length: packet.origlen,
266                        link_type: *link_type as u16,
267                        data: Bytes::copy_from_slice(packet.data),
268                    };
269
270                    reader.consume(offset);
271                    return Ok(Some(raw));
272                }
273                PcapBlockOwned::LegacyHeader(header) => {
274                    *link_type = header.network.0 as u32;
275                    reader.consume(offset);
276                    continue;
277                }
278                _ => {
279                    reader.consume(offset);
280                    continue;
281                }
282            },
283            Err(PcapParserError::Eof) => return Ok(None),
284            Err(PcapParserError::Incomplete(_)) => {
285                reader.refill().map_err(|e| {
286                    Error::Pcap(PcapError::InvalidFormat {
287                        reason: format!("Legacy PCAP refill error: {e}"),
288                    })
289                })?;
290                continue;
291            }
292            Err(e) => {
293                return Err(Error::Pcap(PcapError::InvalidFormat {
294                    reason: format!("Legacy PCAP parse error: {e}"),
295                }));
296            }
297        }
298    }
299}
300
301/// Read next packet from a PCAPNG reader.
302fn read_pcapng_packet<S: Read>(
303    reader: &mut PcapNGReader<S>,
304    frame_number: &mut u64,
305    link_type: &mut u32,
306) -> Result<Option<RawPacket>, Error> {
307    use pcap_parser::PcapError as PcapParserError;
308
309    loop {
310        match reader.next() {
311            Ok((offset, block)) => match block {
312                PcapBlockOwned::NG(ng_block) => {
313                    use pcap_parser::pcapng::*;
314
315                    match ng_block {
316                        Block::InterfaceDescription(idb) => {
317                            *link_type = idb.linktype.0 as u32;
318                            reader.consume(offset);
319                            continue;
320                        }
321                        Block::EnhancedPacket(epb) => {
322                            *frame_number += 1;
323
324                            let timestamp_us = ((epb.ts_high as i64) << 32) | (epb.ts_low as i64);
325
326                            let packet = RawPacket {
327                                frame_number: *frame_number,
328                                timestamp_us,
329                                captured_length: epb.caplen,
330                                original_length: epb.origlen,
331                                link_type: *link_type as u16,
332                                data: Bytes::copy_from_slice(epb.data),
333                            };
334
335                            reader.consume(offset);
336                            return Ok(Some(packet));
337                        }
338                        Block::SimplePacket(spb) => {
339                            *frame_number += 1;
340
341                            let packet = RawPacket {
342                                frame_number: *frame_number,
343                                timestamp_us: 0,
344                                captured_length: spb.data.len() as u32,
345                                original_length: spb.origlen,
346                                link_type: *link_type as u16,
347                                data: Bytes::copy_from_slice(spb.data),
348                            };
349
350                            reader.consume(offset);
351                            return Ok(Some(packet));
352                        }
353                        _ => {
354                            reader.consume(offset);
355                            continue;
356                        }
357                    }
358                }
359                _ => {
360                    reader.consume(offset);
361                    continue;
362                }
363            },
364            Err(PcapParserError::Eof) => return Ok(None),
365            Err(PcapParserError::Incomplete(_)) => {
366                reader.refill().map_err(|e| {
367                    Error::Pcap(PcapError::InvalidFormat {
368                        reason: format!("PCAPNG refill error: {e}"),
369                    })
370                })?;
371                continue;
372            }
373            Err(e) => {
374                return Err(Error::Pcap(PcapError::InvalidFormat {
375                    reason: format!("PCAPNG parse error: {e}"),
376                }));
377            }
378        }
379    }
380}
381
382/// Process packets from a legacy PCAP reader with zero-copy.
383///
384/// This is the zero-copy version of `read_legacy_packet`. The callback receives
385/// borrowed packet data that must be processed before returning.
386fn process_legacy_packets<S: Read, F>(
387    reader: &mut LegacyPcapReader<S>,
388    max: usize,
389    frame_number: &mut u64,
390    link_type: &mut u32,
391    mut f: F,
392) -> Result<usize, Error>
393where
394    F: FnMut(PacketRef<'_>) -> Result<(), Error>,
395{
396    use pcap_parser::PcapError as PcapParserError;
397
398    let mut count = 0;
399    while count < max {
400        match reader.next() {
401            Ok((offset, block)) => {
402                match block {
403                    PcapBlockOwned::Legacy(packet) => {
404                        *frame_number += 1;
405
406                        let timestamp_us =
407                            (packet.ts_sec as i64) * 1_000_000 + (packet.ts_usec as i64);
408
409                        // Create borrowed packet reference - no copy!
410                        let packet_ref = PacketRef {
411                            frame_number: *frame_number,
412                            timestamp_us,
413                            captured_len: packet.caplen,
414                            original_len: packet.origlen,
415                            link_type: *link_type as u16,
416                            data: packet.data, // Borrowed from pcap_parser buffer
417                        };
418
419                        // Call the callback with borrowed data
420                        f(packet_ref)?;
421
422                        // Only consume after callback completes
423                        reader.consume(offset);
424                        count += 1;
425                    }
426                    PcapBlockOwned::LegacyHeader(header) => {
427                        *link_type = header.network.0 as u32;
428                        reader.consume(offset);
429                        continue;
430                    }
431                    _ => {
432                        reader.consume(offset);
433                        continue;
434                    }
435                }
436            }
437            Err(PcapParserError::Eof) => break,
438            Err(PcapParserError::Incomplete(_)) => {
439                reader.refill().map_err(|e| {
440                    Error::Pcap(PcapError::InvalidFormat {
441                        reason: format!("Legacy PCAP refill error: {e}"),
442                    })
443                })?;
444                continue;
445            }
446            Err(e) => {
447                return Err(Error::Pcap(PcapError::InvalidFormat {
448                    reason: format!("Legacy PCAP parse error: {e}"),
449                }));
450            }
451        }
452    }
453    Ok(count)
454}
455
456/// Process packets from a PCAPNG reader with zero-copy.
457///
458/// This is the zero-copy version of `read_pcapng_packet`. The callback receives
459/// borrowed packet data that must be processed before returning.
460fn process_pcapng_packets<S: Read, F>(
461    reader: &mut PcapNGReader<S>,
462    max: usize,
463    frame_number: &mut u64,
464    link_type: &mut u32,
465    mut f: F,
466) -> Result<usize, Error>
467where
468    F: FnMut(PacketRef<'_>) -> Result<(), Error>,
469{
470    use pcap_parser::PcapError as PcapParserError;
471
472    let mut count = 0;
473    while count < max {
474        match reader.next() {
475            Ok((offset, block)) => {
476                match block {
477                    PcapBlockOwned::NG(ng_block) => {
478                        use pcap_parser::pcapng::*;
479
480                        match ng_block {
481                            Block::InterfaceDescription(idb) => {
482                                *link_type = idb.linktype.0 as u32;
483                                reader.consume(offset);
484                                continue;
485                            }
486                            Block::EnhancedPacket(epb) => {
487                                *frame_number += 1;
488
489                                let timestamp_us =
490                                    ((epb.ts_high as i64) << 32) | (epb.ts_low as i64);
491
492                                // Create borrowed packet reference - no copy!
493                                let packet_ref = PacketRef {
494                                    frame_number: *frame_number,
495                                    timestamp_us,
496                                    captured_len: epb.caplen,
497                                    original_len: epb.origlen,
498                                    link_type: *link_type as u16,
499                                    data: epb.data, // Borrowed from pcap_parser buffer
500                                };
501
502                                // Call the callback with borrowed data
503                                f(packet_ref)?;
504
505                                // Only consume after callback completes
506                                reader.consume(offset);
507                                count += 1;
508                            }
509                            Block::SimplePacket(spb) => {
510                                *frame_number += 1;
511
512                                // Create borrowed packet reference - no copy!
513                                let packet_ref = PacketRef {
514                                    frame_number: *frame_number,
515                                    timestamp_us: 0,
516                                    captured_len: spb.data.len() as u32,
517                                    original_len: spb.origlen,
518                                    link_type: *link_type as u16,
519                                    data: spb.data, // Borrowed from pcap_parser buffer
520                                };
521
522                                // Call the callback with borrowed data
523                                f(packet_ref)?;
524
525                                // Only consume after callback completes
526                                reader.consume(offset);
527                                count += 1;
528                            }
529                            _ => {
530                                reader.consume(offset);
531                                continue;
532                            }
533                        }
534                    }
535                    _ => {
536                        reader.consume(offset);
537                        continue;
538                    }
539                }
540            }
541            Err(PcapParserError::Eof) => break,
542            Err(PcapParserError::Incomplete(_)) => {
543                reader.refill().map_err(|e| {
544                    Error::Pcap(PcapError::InvalidFormat {
545                        reason: format!("PCAPNG refill error: {e}"),
546                    })
547                })?;
548                continue;
549            }
550            Err(e) => {
551                return Err(Error::Pcap(PcapError::InvalidFormat {
552                    reason: format!("PCAPNG parse error: {e}"),
553                }));
554            }
555        }
556    }
557    Ok(count)
558}
559
560// GenericPcapReader is Send when R is Send
561unsafe impl<R: Read + Send> Send for GenericPcapReader<R> {}
562
563// Required for async compatibility
564impl<R: Read> Unpin for GenericPcapReader<R> {}
565
566#[cfg(test)]
567mod tests {
568    use super::*;
569    use std::io::Cursor;
570
571    #[test]
572    fn test_pcap_format_detect() {
573        // Magic bytes are stored as-written by the capturing system.
574        // On a little-endian machine, 0xa1b2c3d4 is stored as [0xd4, 0xc3, 0xb2, 0xa1].
575        // When read back on a little-endian machine with from_ne_bytes(), we get 0xa1b2c3d4.
576
577        // Little-endian microseconds (stored as [0xd4, 0xc3, 0xb2, 0xa1])
578        let le_micro = [0xd4, 0xc3, 0xb2, 0xa1];
579        assert_eq!(
580            PcapFormat::detect(&le_micro).unwrap(),
581            PcapFormat::LegacyLeMicro
582        );
583
584        // Big-endian microseconds (stored as [0xa1, 0xb2, 0xc3, 0xd4])
585        let be_micro = [0xa1, 0xb2, 0xc3, 0xd4];
586        assert_eq!(
587            PcapFormat::detect(&be_micro).unwrap(),
588            PcapFormat::LegacyBeMicro
589        );
590
591        // PCAPNG
592        let pcapng = [0x0a, 0x0d, 0x0d, 0x0a];
593        assert_eq!(PcapFormat::detect(&pcapng).unwrap(), PcapFormat::PcapNg);
594
595        // Unknown magic
596        let unknown = [0xDE, 0xAD, 0xBE, 0xEF];
597        assert!(PcapFormat::detect(&unknown).is_err());
598    }
599
600    #[test]
601    fn test_pcap_format_properties() {
602        assert!(PcapFormat::LegacyLeMicro.is_legacy());
603        assert!(!PcapFormat::LegacyLeMicro.is_pcapng());
604
605        assert!(PcapFormat::PcapNg.is_pcapng());
606        assert!(!PcapFormat::PcapNg.is_legacy());
607    }
608
609    #[test]
610    fn test_pcap_format_endianness() {
611        // Little-endian formats
612        assert!(PcapFormat::LegacyLeMicro.is_little_endian());
613        assert!(PcapFormat::LegacyLeNano.is_little_endian());
614        assert!(PcapFormat::PcapNg.is_little_endian()); // Most PCAPNG files are LE
615
616        // Big-endian formats
617        assert!(!PcapFormat::LegacyBeMicro.is_little_endian());
618        assert!(!PcapFormat::LegacyBeNano.is_little_endian());
619    }
620
621    /// Create a minimal valid PCAP file for testing.
622    fn create_minimal_pcap() -> Vec<u8> {
623        let mut data = Vec::new();
624
625        // PCAP global header
626        data.extend_from_slice(&[0xd4, 0xc3, 0xb2, 0xa1]); // Magic (little endian)
627        data.extend_from_slice(&[0x02, 0x00]); // Version major (2)
628        data.extend_from_slice(&[0x04, 0x00]); // Version minor (4)
629        data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Thiszone
630        data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Sigfigs
631        data.extend_from_slice(&[0xff, 0xff, 0x00, 0x00]); // Snaplen (65535)
632        data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Network (Ethernet)
633
634        // One packet header + minimal Ethernet frame
635        let packet_data = [
636            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // Dst MAC
637            0x00, 0x11, 0x22, 0x33, 0x44, 0x55, // Src MAC
638            0x08, 0x00, // EtherType (IPv4)
639        ];
640
641        let ts_sec: u32 = 1000000000;
642        let ts_usec: u32 = 500000;
643        let caplen: u32 = packet_data.len() as u32;
644        let origlen: u32 = packet_data.len() as u32;
645
646        data.extend_from_slice(&ts_sec.to_le_bytes());
647        data.extend_from_slice(&ts_usec.to_le_bytes());
648        data.extend_from_slice(&caplen.to_le_bytes());
649        data.extend_from_slice(&origlen.to_le_bytes());
650        data.extend_from_slice(&packet_data);
651
652        data
653    }
654
655    #[test]
656    fn test_generic_reader_from_memory() {
657        let pcap_data = create_minimal_pcap();
658
659        // Detect format from magic bytes
660        let format = PcapFormat::detect(&pcap_data).expect("Failed to detect format");
661
662        let cursor = Cursor::new(pcap_data);
663        let mut reader =
664            GenericPcapReader::with_format(cursor, format).expect("Failed to create reader");
665
666        // Read first packet
667        let packet = reader.next_packet().expect("Read error");
668        assert!(packet.is_some());
669
670        let pkt = packet.unwrap();
671        assert_eq!(pkt.frame_number, 1);
672        assert_eq!(pkt.captured_length, 14);
673        assert_eq!(pkt.original_length, 14);
674        assert_eq!(pkt.link_type, 1); // Ethernet
675        assert_eq!(pkt.timestamp_us, 1000000000_500000i64);
676        assert_eq!(pkt.data.len(), 14);
677
678        // No more packets
679        let packet2 = reader.next_packet().expect("Read error");
680        assert!(packet2.is_none());
681    }
682
683    #[test]
684    fn test_generic_reader_link_type() {
685        let pcap_data = create_minimal_pcap();
686        let format = PcapFormat::detect(&pcap_data).expect("Failed to detect format");
687        let cursor = Cursor::new(pcap_data);
688
689        let mut reader =
690            GenericPcapReader::with_format(cursor, format).expect("Failed to create reader");
691
692        // Link type is set after reading header block
693        reader.next_packet().ok();
694        assert_eq!(reader.link_type(), 1); // Ethernet
695    }
696
697    #[test]
698    fn test_generic_reader_frame_count() {
699        let pcap_data = create_minimal_pcap();
700        let format = PcapFormat::detect(&pcap_data).expect("Failed to detect format");
701        let cursor = Cursor::new(pcap_data);
702
703        let mut reader =
704            GenericPcapReader::with_format(cursor, format).expect("Failed to create reader");
705        assert_eq!(reader.frame_count(), 0);
706
707        reader.next_packet().ok();
708        assert_eq!(reader.frame_count(), 1);
709    }
710}