IPv4 datagram encoding and decoding (RFC 791).

A datagram is a 20-byte fixed header, a variable-length options trailer (0–40 bytes per IHL), and the transport-layer payload:

 0      7 8     15 16    23 24    31
+--------+--------+--------+--------+
|Ver/IHL |  ToS   |   Total Length  |
+--------+--------+--------+--------+
| Identification  |Flags + FragOffs |
+--------+--------+--------+--------+
|  TTL   |Protocol|  Header Cksum   |
+--------+--------+--------+--------+
|          Source Address           |
+--------+--------+--------+--------+
|       Destination Address         |
+--------+--------+--------+--------+
|       Options (0-40 bytes)        |
+-----------------------------------+
|        Payload (variable)         |
+-----------------------------------+

Provides a single-pass coder for the full datagram — fixed header, optional options trailer, and the transport payload sized via totalLength. Bit-packed fields (version/IHL, flags/fragment offset) are exposed as nested objects via bitStruct, keeping the on-wire layout faithful while preserving named-field access.

IPv4 addresses are surfaced as raw 32-bit unsigned integers, mirroring how ARP exposes the same field. Use @hertzg/ip's parseAddressv4 / stringifyAddressv4 for human-readable conversion.

Design notes:

  • The header checksum is not computed automatically. Callers are expected to set the headerChecksum field to a valid RFC 1071 16-bit one's complement sum before encoding (or zero when the receiver tolerates it, e.g. for testing). This matches the @hertzg/binstruct "no defensive programming" stance — the coder describes layout, not semantics.
  • Options are encoded as a raw byte slice whose length is derived from versionIhl.ihl: (ihl - 5) * 4. For a datagram with no options pass ihl = 5 and options = new Uint8Array(0).
  • The payload size is derived from totalLength - ihl * 4. Callers are responsible for setting totalLength to match ihl * 4 + payload.length.

Examples

Round-trip a minimal IPv4 datagram (no options, empty payload)

import { assertEquals } from "@std/assert";
import { parseAddressv4 } from "@hertzg/ip/addressv4";
import { ipv4Packet } from "@binstruct/ipv4";

const coder = ipv4Packet();
const datagram = {
  versionIhl: { version: 4, ihl: 5 },
  typeOfService: 0,
  totalLength: 20,
  identification: 0x1234,
  flagsFragmentOffset: {
    reserved: 0,
    dontFragment: 1,
    moreFragments: 0,
    fragmentOffset: 0,
  },
  timeToLive: 64,
  protocol: 6,
  headerChecksum: 0,
  sourceAddress: parseAddressv4("192.168.1.100").address,
  destinationAddress: parseAddressv4("10.0.0.50").address,
  options: new Uint8Array(0),
  payload: new Uint8Array(0),
};

const buffer = new Uint8Array(20);
const bytesWritten = coder.encode(datagram, buffer);
const [decoded, bytesRead] = coder.decode(buffer);

assertEquals(bytesWritten, 20);
assertEquals(bytesRead, 20);
assertEquals(decoded.sourceAddress, parseAddressv4("192.168.1.100").address);
assertEquals(decoded.destinationAddress, parseAddressv4("10.0.0.50").address);
assertEquals(decoded.flagsFragmentOffset.dontFragment, 1);