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.

Examples

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

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)

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)

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]));