Coders for the classic libpcap (.pcap) capture file format.
This package decodes and encodes the original libpcap layout — a 24-byte global header followed by a stream of 16-byte record headers each carrying a captured packet payload. The newer pcapng format is intentionally not supported.
The link-layer payload is preserved as raw bytes. Decoding it (Ethernet, raw IP, Linux SLL, …) is left to the caller, so this package has no protocol dependencies and stays focused on the file envelope.
Endianness
Pcap stores numbers in either little- or big-endian, signalled by the magic
value at offset zero. Callers may pick the byte order explicitly via the
endianness argument ("le" or "be").
Omitting it is the easy path: pcapFile then reads that magic and
decodes the whole capture in whichever order the file itself declares, so a
single pcapFile() handles little- and big-endian captures alike. Encoding
has no file to inspect and writes PCAP_DEFAULT_ENDIANNESS. The
building blocks pcapGlobalHeader and pcapRecord are fixed to
one byte order and default to that same constant — detectPcapMagic
probes a buffer when you drive them yourself.
To pin the encoded byte order without passing an argument, call
pcapFileLe or pcapFileBe. They exist for callers that can only
invoke a factory with no arguments, and writing a big-endian capture is the
case pcapFile() alone cannot cover.
Timestamp resolution
Two magic values exist: one for microsecond timestamps
(PCAP_MAGIC_MICROS) and one for nanosecond timestamps
(PCAP_MAGIC_NANOS). The on-disk layout is identical; only the
interpretation of tsUsec differs.
Round-trip a complete little-endian capture file
Round-trip a complete little-endian capture file
import { assertEquals } from "@std/assert"; import { pcapFile, PCAP_MAGIC_MICROS, LINKTYPE, } from "@binstruct/pcap"; const coder = pcapFile("le"); const value = { header: { magic: PCAP_MAGIC_MICROS, versionMajor: 2, versionMinor: 4, thisZone: 0, sigFigs: 0, snapLen: 65535, network: LINKTYPE.ETHERNET, }, records: [ { tsSec: 1_700_000_000, tsUsec: 250_000, inclLen: 4, origLen: 4, data: new Uint8Array([0xde, 0xad, 0xbe, 0xef]), }, { tsSec: 1_700_000_001, tsUsec: 0, inclLen: 2, origLen: 1500, data: new Uint8Array([0x12, 0x34]), }, ], }; const buffer = new Uint8Array(128); const written = coder.encode(value, buffer); const [decoded] = coder.decode(buffer.subarray(0, written)); assertEquals(written, 24 + 16 + 4 + 16 + 2); assertEquals(decoded.records.length, 2); assertEquals(decoded.records[0].data, value.records[0].data); assertEquals(decoded.records[1].origLen, 1500);
Detect endianness, then decode
Detect endianness, then decode
import { assertEquals } from "@std/assert"; import { detectPcapMagic, pcapFile, PCAP_MAGIC_MICROS, LINKTYPE, } from "@binstruct/pcap"; const original = pcapFile("be"); const buffer = new Uint8Array(64); original.encode({ header: { magic: PCAP_MAGIC_MICROS, versionMajor: 2, versionMinor: 4, thisZone: 0, sigFigs: 0, snapLen: 1500, network: LINKTYPE.RAW, }, records: [], }, buffer); const info = detectPcapMagic(buffer); assertEquals(info, { endianness: "be", nanos: false }); const reader = pcapFile(info!.endianness); const [decoded] = reader.decode(buffer); assertEquals(decoded.header.network, LINKTYPE.RAW);
Composition with the rest of @binstruct/*
Pcap stores raw link-layer payloads. The natural use is to read a capture,
then hand each record.data to a sibling coder for the link type advertised
in the global header.
Walk an inet stack: pcap → IPv4 → UDP (LINKTYPE.RAW)
Walk an inet stack: pcap → IPv4 → UDP (LINKTYPE.RAW)
import { assertEquals } from "@std/assert"; import { LINKTYPE, PCAP_MAGIC_MICROS, pcapFile } from "@binstruct/pcap"; import { ipv4Packet } from "@binstruct/ipv4"; import { udpPacket } from "@binstruct/udp"; import { parseAddressv4 } from "@hertzg/ip/addressv4"; const ip = ipv4Packet(); const udp = udpPacket(); // Synth a UDP-over-IPv4 packet to put in the capture. const udpBytes = new Uint8Array(12); udp.encode({ srcPort: 53, dstPort: 49152, length: 12, checksum: 0, payload: new Uint8Array([0xde, 0xad, 0xbe, 0xef]), }, udpBytes); const packet = new Uint8Array(32); ip.encode({ versionIhl: { version: 4, ihl: 5 }, typeOfService: 0, totalLength: 32, identification: 0, flagsFragmentOffset: { reserved: 0, dontFragment: 0, moreFragments: 0, fragmentOffset: 0, }, timeToLive: 64, protocol: 17, headerChecksum: 0, sourceAddress: parseAddressv4("192.0.2.1").address, destinationAddress: parseAddressv4("192.0.2.2").address, options: new Uint8Array(0), payload: udpBytes, }, packet); const cap = pcapFile("le"); const buf = new Uint8Array(24 + 16 + packet.length); const written = cap.encode({ header: { magic: PCAP_MAGIC_MICROS, versionMajor: 2, versionMinor: 4, thisZone: 0, sigFigs: 0, snapLen: 65535, network: LINKTYPE.RAW, }, records: [{ tsSec: 0, tsUsec: 0, inclLen: packet.length, origLen: packet.length, data: packet, }], }, buf); // Walk the stack on read. const [{ records }] = cap.decode(buf.subarray(0, written)); const [parsedIp] = ip.decode(records[0].data); const [parsedUdp] = udp.decode(parsedIp.payload); assertEquals(parsedIp.sourceAddress, parseAddressv4("192.0.2.1").address); assertEquals(parsedUdp.srcPort, 53); assertEquals(parsedUdp.payload, new Uint8Array([0xde, 0xad, 0xbe, 0xef]));
Walk the full inet stack (LINKTYPE.ETHERNET via @binstruct/inet)
Walk the full inet stack (LINKTYPE.ETHERNET via @binstruct/inet)
import { assert, assertEquals } from "@std/assert"; import { LINKTYPE, PCAP_MAGIC_MICROS, pcapFile } from "@binstruct/pcap"; import { inetFrame } from "@binstruct/inet"; import { ETHERTYPE_IPV4 } from "@binstruct/ipv4"; import { IP_PROTOCOL_UDP } from "@binstruct/udp"; import { parseAddressv4 } from "@hertzg/ip/addressv4"; const inet = inetFrame(); // Build an Ethernet → IPv4 → UDP frame in one pass. const frame = new Uint8Array(14 + 32); inet.encode({ dstMac: new Uint8Array([0x00, 0x11, 0x22, 0x33, 0x44, 0x55]), srcMac: new Uint8Array([0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb]), etherType: ETHERTYPE_IPV4, payload: { versionIhl: { version: 4, ihl: 5 }, typeOfService: 0, totalLength: 32, identification: 0, flagsFragmentOffset: { reserved: 0, dontFragment: 0, moreFragments: 0, fragmentOffset: 0 }, timeToLive: 64, protocol: IP_PROTOCOL_UDP, headerChecksum: 0, sourceAddress: parseAddressv4("192.0.2.1").address, destinationAddress: parseAddressv4("192.0.2.2").address, options: new Uint8Array(0), payload: { srcPort: 53, dstPort: 49152, length: 12, checksum: 0, payload: new Uint8Array([0xde, 0xad, 0xbe, 0xef]), }, }, }, frame); const cap = pcapFile("le"); const buf = new Uint8Array(24 + 16 + frame.length); const written = cap.encode({ header: { magic: PCAP_MAGIC_MICROS, versionMajor: 2, versionMinor: 4, thisZone: 0, sigFigs: 0, snapLen: 65535, network: LINKTYPE.ETHERNET, }, records: [{ tsSec: 0, tsUsec: 0, inclLen: frame.length, origLen: frame.length, data: frame, }], }, buf); // Read back and walk the stack in one shot. const [{ records }] = cap.decode(buf.subarray(0, written)); const [decoded] = inet.decode(records[0].data); assert(!(decoded.payload instanceof Uint8Array)); assert("protocol" in decoded.payload); assertEquals(decoded.payload.sourceAddress, parseAddressv4("192.0.2.1").address); assert(!(decoded.payload.payload instanceof Uint8Array)); assert("srcPort" in decoded.payload.payload); assertEquals(decoded.payload.payload.srcPort, 53); assertEquals(decoded.payload.payload.payload, new Uint8Array([0xde, 0xad, 0xbe, 0xef]));