NTPv4 packet header encoding and decoding (RFC 5905).
An NTP packet opens with a fixed 48-byte header. This coder covers that header only — the optional extension fields and MAC that can follow it are out of scope for v0.0.1:
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |LI | VN |Mode | Stratum | Poll | Precision | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Root Delay | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Root Dispersion | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Reference ID | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | + Reference Timestamp (64) + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | + Origin Timestamp (64) + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | + Receive Timestamp (64) + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | + Transmit Timestamp (64) + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
leapIndicator, version and mode share the first byte, MSB-first, and
are exposed as a nested object via bitStruct. Everything else is
byte-aligned.
poll and precision are signed 8-bit integers (s8be) per RFC 5905 —
both are exponents of 2 (seconds), and precision in particular is
routinely negative (e.g. -20 for a microsecond-precision clock).
@hertzg/binstruct does export a signed 8-bit coder, so no u8be
workaround is needed here.
rootDelay and rootDispersion are, per the RFC, 32-bit signed fixed-point
numbers (16.16). This v0.0.1 coder surfaces them as raw u32be integers —
the same "no semantic interpretation" stance @binstruct/ipv4 takes with
its header checksum. Fixed-point conversion is left to the caller.
The four timestamp fields (referenceTimestamp, originTimestamp,
receiveTimestamp, transmitTimestamp) are NTP era/second/fraction
64-bit values, surfaced here as raw big-endian bigints (via u64be).
Converting the 32-bit seconds-since-1900 high half and 32-bit fraction low
half to a JavaScript Date (or handling the 2036 rollover) is out of scope
for v0.0.1 and left to the caller.
Round-trip a client request packet
Round-trip a client request packet
import { assertEquals } from "@std/assert"; import { ntpPacket, NTP_MODE, NTP_PACKET_SIZE } from "@binstruct/ntp"; const coder = ntpPacket(); const packet = { leapVersionMode: { leapIndicator: 0, version: 4, mode: NTP_MODE.CLIENT }, stratum: 0, poll: 4, precision: -20, rootDelay: 0, rootDispersion: 0, referenceId: 0, referenceTimestamp: 0n, originTimestamp: 0n, receiveTimestamp: 0n, transmitTimestamp: 0xe4c5c46700000000n, }; const buffer = new Uint8Array(NTP_PACKET_SIZE); const written = coder.encode(packet, buffer); const [decoded, read] = coder.decode(buffer); assertEquals(written, NTP_PACKET_SIZE); assertEquals(read, NTP_PACKET_SIZE); assertEquals(decoded, packet); assertEquals(decoded.leapVersionMode.mode, NTP_MODE.CLIENT);