A comprehensive module providing type-safe binary structure encoding and decoding utilities for TypeScript.
Reading and writing WAV (RIFF) file format:
Reading and writing WAV (RIFF) file format:
import { assertEquals } from "@std/assert"; import { struct, array, string } from "@hertzg/binstruct"; import { u16le, u32le, u8le } from "@hertzg/binstruct/numeric"; // Define WAV file structure following RIFF format specification const riffChunkCoder = struct({ chunkID: string(4), // "RIFF" (fixed 4-byte string) chunkSize: u32le(), // File size - 8 bytes format: string(4), // "WAVE" (fixed 4-byte string) }); const fmtChunkCoder = struct({ chunkID: string(4), // "fmt " (fixed 4-byte string) chunkSize: u32le(), // Size of fmt chunk (16 for PCM) audioFormat: u16le(), // Audio format (1 = PCM) numChannels: u16le(), // Number of channels (1 = mono, 2 = stereo) sampleRate: u32le(), // Sample rate (e.g., 44100 Hz) byteRate: u32le(), // Byte rate (sampleRate * numChannels * bitsPerSample / 8) blockAlign: u16le(), // Block align (numChannels * bitsPerSample / 8) bitsPerSample: u16le(), // Bits per sample (8, 16, 24, 32) }); const dataChunkCoder = struct({ chunkID: string(4), // "data" (fixed 4-byte string) chunkSize: u32le(), // Size of audio data audioData: array(u8le(), u32le()), // Audio samples as length-prefixed array }); // Complete WAV file structure const wavFileCoder = struct({ riff: riffChunkCoder, fmt: fmtChunkCoder, data: dataChunkCoder, }); // Create sample WAV data (8kHz, 8-bit, mono, 0.1 second - small example) const sampleRate = 8000; const numChannels = 1; const bitsPerSample = 8; const durationSeconds = 0.1; const numSamples = Math.floor(sampleRate * durationSeconds); // Generate a simple sine wave (440 Hz) with 8-bit samples const audioData = new Array(numSamples); for (let i = 0; i < numSamples; i++) { const t = i / sampleRate; audioData[i] = Math.floor(Math.sin(2 * Math.PI * 440 * t) * 127) + 128; // 8-bit amplitude (0-255) } const wavData = { riff: { chunkID: "RIFF", chunkSize: 0, // Will be calculated format: "WAVE", }, fmt: { chunkID: "fmt ", chunkSize: 16, audioFormat: 1, // PCM numChannels, sampleRate, byteRate: sampleRate * numChannels * bitsPerSample / 8, blockAlign: numChannels * bitsPerSample / 8, bitsPerSample, }, data: { chunkID: "data", chunkSize: 0, // Will be calculated audioData, }, }; // Calculate chunk sizes const dataSize = audioData.length * (bitsPerSample / 8); wavData.data.chunkSize = dataSize; wavData.riff.chunkSize = 36 + dataSize; // 36 = 12 (RIFF) + 24 (fmt) + data size // Encode WAV data to binary const buffer = new Uint8Array(1024); const bytesWritten = wavFileCoder.encode(wavData, buffer); // Decode WAV data from binary const [decoded, bytesRead] = wavFileCoder.decode(buffer); // Verify the data matches using assertions assertEquals(decoded.riff.chunkID, "RIFF", 'RIFF chunk ID should be "RIFF"'); assertEquals(decoded.riff.format, "WAVE", 'RIFF format should be "WAVE"'); assertEquals(decoded.fmt.chunkID, "fmt ", 'fmt chunk ID should be "fmt "'); assertEquals(decoded.fmt.audioFormat, 1, 'Audio format should be PCM (1)'); assertEquals(decoded.fmt.numChannels, numChannels, 'Number of channels should match'); assertEquals(decoded.fmt.sampleRate, sampleRate, 'Sample rate should match'); assertEquals(decoded.fmt.bitsPerSample, bitsPerSample, 'Bits per sample should match'); assertEquals(decoded.data.chunkID, "data", 'Data chunk ID should be "data"'); assertEquals(decoded.data.audioData.length, audioData.length, 'Audio data length should match'); assertEquals(bytesWritten, bytesRead, 'Bytes written should equal bytes read'); assertEquals(decoded.riff.chunkSize, wavData.riff.chunkSize, 'RIFF chunk size should match'); assertEquals(decoded.data.chunkSize, wavData.data.chunkSize, 'Data chunk size should match'); // Verify audio data integrity for (let i = 0; i < Math.min(100, audioData.length); i++) { assertEquals(decoded.data.audioData[i], audioData[i], `Audio sample at index ${i} should match`); }
Parsing complete network stack (Ethernet + IP + TCP):
Parsing complete network stack (Ethernet + IP + TCP):
import { assertEquals } from "@std/assert"; import { struct, array, string, refine } from "@hertzg/binstruct"; import { u16be, u32be, u8be } from "@hertzg/binstruct/numeric"; const macAddr = refine(array(u8be(), 6), { refine: (arr: number[]) => arr.map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(':'), unrefine: (mac: string) => mac.split(':').map(hex => parseInt(hex, 16)), }); const ipAddr = refine(array(u8be(), 4), { refine: (arr: number[]) => arr.join('.'), unrefine: (ip: string) => ip.split('.').map(octet => parseInt(octet, 10)), }); // Define Ethernet frame structure (IEEE 802.3) const ethernetFrameCoder = struct({ destinationMAC: macAddr(), // MAC address as colon-separated hex string sourceMAC: macAddr(), // MAC address as colon-separated hex string etherType: u16be(), // EtherType (0x0800 for IPv4) }); // Define IPv4 header structure (RFC 791) const ipv4HeaderCoder = struct({ version: u8be(), // Version (4) and IHL (5 words = 20 bytes) tos: u8be(), // Type of Service totalLength: u16be(), // Total packet length identification: u16be(), // Packet identification flags: u16be(), // Flags and fragment offset ttl: u8be(), // Time to Live protocol: u8be(), // Protocol (6 for TCP) checksum: u16be(), // Header checksum sourceIP: ipAddr(), // IP address as dot-separated decimal string destIP: ipAddr(), // IP address as dot-separated decimal string options: array(u8be(), 0), // IP options (empty for this example) }); // Define TCP header structure (RFC 793) const tcpHeaderCoder = struct({ sourcePort: u16be(), // Source port destPort: u16be(), // Destination port sequenceNumber: u32be(), // Sequence number ackNumber: u32be(), // Acknowledgment number dataOffset: u8be(), // Data offset and flags flags: u8be(), // Control flags windowSize: u16be(), // Window size checksum: u16be(), // TCP checksum urgentPointer: u16be(), // Urgent pointer options: array(u8be(), 0), // TCP options (empty for this example) }); // Define complete network packet structure const networkPacketCoder = struct({ ethernet: ethernetFrameCoder, ip: ipv4HeaderCoder, tcp: tcpHeaderCoder, payload: array(u8be(), u16be()), // Length-prefixed payload }); // Create sample network packet data const networkPacket = { ethernet: { destinationMAC: "00:1B:21:BB:0F:3B", // Router MAC as string sourceMAC: "00:0C:29:2E:84:5A", // Host MAC as string etherType: 0x0800, // IPv4 }, ip: { version: 0x45, // IPv4, 5 words header tos: 0x00, // Normal precedence totalLength: 0, // Will be calculated identification: 0x1234, // Packet ID flags: 0x4000, // Don't fragment ttl: 64, // Time to live protocol: 6, // TCP checksum: 0, // Will be calculated sourceIP: "192.168.1.100", // Source IP as string destIP: "10.0.0.50", // Destination IP as string options: [], // No IP options }, tcp: { sourcePort: 49152, // Dynamic port destPort: 80, // HTTP port sequenceNumber: 0x12345678, // Initial sequence ackNumber: 0, // No acknowledgment dataOffset: 0x50, // 5 words header flags: 0x02, // SYN flag windowSize: 65535, // Maximum window checksum: 0, // Will be calculated urgentPointer: 0, // No urgent data options: [], // No TCP options }, payload: [0x48, 0x65, 0x6c, 0x6c, 0x6f], // "Hello" payload }; // Calculate packet lengths const tcpHeaderLength = 20; // Standard TCP header const payloadLength = networkPacket.payload.length; const payloadLengthPrefix = 2; // u16 length prefix for array const totalTcpLength = tcpHeaderLength + payloadLength + payloadLengthPrefix; const ipHeaderLength = 20; // Standard IP header const totalPacketLength = ipHeaderLength + totalTcpLength; const ethernetHeaderLength = 14; // Ethernet header (6+6+2 bytes) const totalFrameLength = ethernetHeaderLength + totalPacketLength; // Update length fields networkPacket.ip.totalLength = totalPacketLength; networkPacket.tcp.dataOffset = 0x50; // 5 words header // Encode complete network packet const buffer = new Uint8Array(2048); const bytesWritten = networkPacketCoder.encode(networkPacket, buffer); // Decode complete network packet const [decoded, bytesRead] = networkPacketCoder.decode(buffer); // Verify Ethernet frame using assertions assertEquals(decoded.ethernet.destinationMAC, "00:1B:21:BB:0F:3B", 'Destination MAC should match router'); assertEquals(decoded.ethernet.sourceMAC, "00:0C:29:2E:84:5A", 'Source MAC should match host'); assertEquals(decoded.ethernet.etherType, 0x0800, 'EtherType should be IPv4 (0x0800)'); // Verify IP header using assertions assertEquals(decoded.ip.version, 0x45, 'IP version should be IPv4 with 5 words header'); assertEquals(decoded.ip.protocol, 6, 'Protocol should be TCP (6)'); assertEquals(decoded.ip.ttl, 64, 'TTL should be 64'); assertEquals(decoded.ip.sourceIP, "192.168.1.100", 'Source IP should match'); assertEquals(decoded.ip.destIP, "10.0.0.50", 'Destination IP should match'); assertEquals(decoded.ip.totalLength, totalPacketLength, 'IP total length should match calculated value'); assertEquals(decoded.ip.identification, 0x1234, 'Packet ID should match'); assertEquals(decoded.ip.flags, 0x4000, 'Flags should indicate no fragmentation'); // Verify TCP header using assertions assertEquals(decoded.tcp.sourcePort, 49152, 'Source port should be 49152'); assertEquals(decoded.tcp.destPort, 80, 'Destination port should be 80 (HTTP)'); assertEquals(decoded.tcp.sequenceNumber, 0x12345678, 'Sequence number should match'); assertEquals(decoded.tcp.ackNumber, 0, 'Acknowledgment number should be 0'); assertEquals(decoded.tcp.dataOffset, 0x50, 'Data offset should be 5 words'); assertEquals(decoded.tcp.flags, 0x02, 'SYN flag should be set'); assertEquals(decoded.tcp.windowSize, 65535, 'Window size should be maximum'); assertEquals(decoded.tcp.urgentPointer, 0, 'Urgent pointer should be 0'); // Verify payload using assertions assertEquals(decoded.payload.length, 5, 'Payload should have 5 bytes'); assertEquals(decoded.payload, [0x48, 0x65, 0x6c, 0x6c, 0x6f], 'Payload should be "Hello"'); assertEquals(String.fromCharCode(...decoded.payload), "Hello", 'Payload should decode to "Hello"'); // Verify complete packet integrity using assertions assertEquals(bytesWritten, bytesRead, 'Bytes written should equal bytes read'); assertEquals(decoded.ip.totalLength, totalPacketLength, 'IP total length should match calculated value'); assertEquals(decoded.tcp.dataOffset & 0xf0, 0x50, 'TCP data offset should be 5 words'); // Verify frame size calculations const calculatedFrameSize = ethernetHeaderLength + ipHeaderLength + tcpHeaderLength + payloadLength + payloadLengthPrefix; assertEquals(bytesWritten, calculatedFrameSize, 'Total frame size should match calculated value'); assertEquals(calculatedFrameSize, 61, 'Frame size should be 61 bytes (14+20+20+2+5)'); // Verify protocol stack hierarchy assertEquals(decoded.ethernet.etherType, 0x0800, 'Ethernet should carry IPv4'); assertEquals(decoded.ip.protocol, 6, 'IPv4 should carry TCP'); assertEquals(decoded.tcp.destPort, 80, 'TCP should be destined for HTTP');
Creates a Coder for arrays that automatically chooses between length-prefixed and fixed-length based on the arguments provided.
Creates a Coder for fixed-length arrays of a given element type.
Creates a Coder for length-prefixed arrays of a given element type.
Creates a Coder for arrays using a custom condition function to determine when to stop.
Automatically grows a buffer until the encoding function succeeds.
Creates a Coder for bit-packed structures with MSB-first ordering.
Creates a Coder for byte slices.
Creates a computed reference that depends on multiple other references.
Creates a default context for encoding or decoding operations.
Decodes data using the provided coder, returning the decoded value.
Encodes data using the provided coder, handling buffer allocation automatically.
Creates a coder for 16-bit floating-point numbers.
Convenience function for 16-bit floating point with big-endian byte order.
Convenience function for 16-bit floating point with little-endian byte order.
Creates a coder for 32-bit floating-point numbers.
Convenience function for 32-bit floating point with big-endian byte order.
Convenience function for 32-bit floating point with little-endian byte order.
Creates a coder for 64-bit floating-point numbers.
Convenience function for 64-bit floating point with big-endian byte order.
Convenience function for 64-bit floating point with little-endian byte order.
Type guard to check if a value is a Coder.
Type guard to check if a value is a valid length or a reference to a length.
Checks if a value is a reference created by the ref function.
Validates if a length value is valid for binary encoding.
Wraps a coder factory so the coder it produces is built on first use
instead of when lazy() is called, and only ever built once.
Attempts to resolve a length value from a reference or literal.
Sets a length value in the context for the given coder.
Creates a reference value that can be resolved during encoding/decoding.
Retrieves the value from a reference or returns the value directly if it's not a reference.
Creates a refined coder that applies transformations during encoding and decoding.
Creates a Refiner that swaps named Uint8Array fields of a
host record for the typed values produced by their sub-coders. Fields not
in coders are passed through unchanged.
Creates a coder that conditionally applies refiners based on selector functions, using switch-like semantics for bidirectional encoding and decoding.
Sets a value in the context for a specific coder reference.
Creates a coder for 16-bit signed integers.
Convenience function for 16-bit signed integer with big-endian byte order.
Convenience function for 16-bit signed integer with little-endian byte order.
Creates a coder for 32-bit signed integers.
Convenience function for 32-bit signed integer with big-endian byte order.
Convenience function for 32-bit signed integer with little-endian byte order.
Creates a coder for 64-bit signed integers.
Convenience function for 64-bit signed integer with big-endian byte order.
Convenience function for 64-bit signed integer with little-endian byte order.
Creates a coder for 8-bit signed integers.
Convenience function for 8-bit signed integer with big-endian byte order.
Convenience function for 8-bit signed integer with little-endian byte order.
Creates a Coder for strings that automatically chooses between length-prefixed, null-terminated, and fixed-length based on the arguments provided.
Creates a Coder for fixed-length strings.
Creates a Coder for length-prefixed strings.
Creates a Coder for null-terminated strings.
Creates a Coder for structured data from an object of property names to coders.
Creates a coder for 16-bit unsigned integers.
Convenience function for 16-bit unsigned integer with big-endian byte order.
Convenience function for 16-bit unsigned integer with little-endian byte order.
Creates a coder for 32-bit unsigned integers.
Convenience function for 32-bit unsigned integer with big-endian byte order.
Convenience function for 32-bit unsigned integer with little-endian byte order.
Creates a coder for 64-bit unsigned integers.
Convenience function for 64-bit unsigned integer with big-endian byte order.
Convenience function for 64-bit unsigned integer with little-endian byte order.
Creates a coder for 8-bit unsigned integers.
Convenience function for 8-bit unsigned integer with big-endian byte order.
Convenience function for 8-bit unsigned integer with little-endian byte order.
Ensures that a context has the necessary reference storage initialized.
Configuration options for automatic buffer growth.
-
growthFactor: number
Growth factor multiplier for buffer resizing. Must be greater than 1. Each resize multiplies the current size by this factor. Defaults to 2 (doubling).
-
initialSize: number
Initial buffer size in bytes. Must be greater than 0 and less than or equal to maxByteLength. Defaults to 4096 bytes (4KB).
-
maxByteLength: number
Maximum buffer size in bytes. The buffer will not grow beyond this limit. When reached, a RangeError will be thrown. Defaults to 400MB.
Context for encoding/decoding operations.
-
direction: "encode" | "decode"
The direction of the operation
-
kCtxRefs: RefsWeakMap
Optional references storage
A weak map interface for storing references in the encoding/decoding context.
-
get<T>(coder: Coder<T>): T | undefined
Gets a reference value for the given coder.
-
has<T>(coder: Coder<T>): boolean
Checks if a reference value exists for the given coder.
-
set<T>(): thiscoder: Coder<T>,value: T
Sets a reference value for the given coder.
Condition function type for arrayWhile that determines when to continue processing array elements.
Schema type for bitStruct, mapping field names to bit counts.
Type of the decoded value from a bitStruct. All fields are decoded as numbers.
Interface for coders that can encode and decode values.
-
decode: Decoder<TDecoded>
Decodes a value from a byte buffer and returns the value with the number of bytes consumed.
-
encode: Encoder<TDecoded>
Encodes a value into a byte buffer and returns the number of bytes written.
-
kCoderKind: symbol
Symbol tag identifying the kind of coder; used to distinguish coder variants without string comparisons.
Mapped type extracting the decoded value type for each entry in a
FieldCoders map. Given { payload: Coder<Ipv4> } it yields
{ payload: Ipv4 }.
Function type for decoding values.
Function type for encoding values.
Endianness type for numeric data encoding and decoding.
Map of host field names to the sub-coder that decodes/encodes that field's
bytes. Used as the input to refineFields.
A type representing a length value that can be either a number or a reference to a number.
Extracts the union of all refined types from a record of refiners.
A refiner that transforms decoded values into refined types and vice versa.
-
refine: () => TRefinedunrefined: TUnrefined,context: Context,...args: TArgs
Transforms a decoded value into a refined value.
-
unrefine: () => TUnrefinedrefined: TRefined,context: Context,...args: TArgs
Transforms a refined value back to the original decoded format.
A type representing a reference value that can be resolved during encoding/decoding.
-
kIsRefValue: true
Brand marker identifying the function as a reference value.
The decoded object type of a struct schema: each property's coder
Coder<U> contributes a property of type U.
Type utility to unwrap reference types from a tuple of references.
Type representing a value with its byte count.
Symbol identifier for coder kind.
Symbol identifier for context references.
Symbol identifier for reference values.
Symbol identifier for fixed-length array coders.
Symbol identifier for length-prefixed array coders.
Symbol identifier for conditional while-loop array coders.
Symbol identifier for fixed-length string coders.
Symbol identifier for length-prefixed string coders.
Symbol identifier for null-terminated string coders.
Bit-level encoding and decoding utilities for binary structures.
Basic bit-packed structure
Basic bit-packed structure
import { assertEquals } from "@std/assert"; import { bitStruct } from "@hertzg/binstruct/bits"; // Define a bit-packed header (total 8 bits = 1 byte) const flags = bitStruct({ enabled: 1, // 1 bit priority: 3, // 3 bits category: 4, // 4 bits }); // Encode const value = { enabled: 1, priority: 5, category: 2 }; const buffer = new Uint8Array(1); const bytesWritten = flags.encode(value, buffer); // Buffer contains: 0b1_101_0010 = 0xD2 assertEquals(buffer[0], 0xD2); assertEquals(bytesWritten, 1); // Decode const [decoded, bytesRead] = flags.decode(buffer); assertEquals(decoded, value); assertEquals(bytesRead, 1);
Network protocol header (multi-byte)
Network protocol header (multi-byte)
import { assertEquals } from "@std/assert"; import { bitStruct } from "@hertzg/binstruct/bits"; // 32-bit protocol header const header = bitStruct({ version: 4, // 4 bits type: 4, // 4 bits flags: 8, // 8 bits length: 16, // 16 bits }); // Total: 32 bits = 4 bytes const buffer = new Uint8Array(4); const data = { version: 1, type: 2, flags: 0xFF, length: 1024 }; header.encode(data, buffer); const [decoded, bytesRead] = header.decode(buffer); assertEquals(decoded, data); assertEquals(bytesRead, 4);
Padding to byte boundaries
Padding to byte boundaries
import { assertEquals } from "@std/assert"; import { bitStruct } from "@hertzg/binstruct/bits"; // Fields don't align to byte boundary - add explicit padding const flags = bitStruct({ ready: 1, error: 1, mode: 2, _reserved: 4, // Padding to reach 8 bits }); const buffer = new Uint8Array(1); flags.encode({ ready: 1, error: 0, mode: 3, _reserved: 0 }, buffer); assertEquals(buffer[0], 0b1_0_11_0000);
Integration with struct
Integration with struct
import { assertEquals } from "@std/assert"; import { bitStruct } from "@hertzg/binstruct/bits"; import { struct } from "@hertzg/binstruct/struct"; import { u32le } from "@hertzg/binstruct/numeric"; const flags = bitStruct({ compressed: 1, encrypted: 1, version: 6, }); const packet = struct({ flags: flags, payloadSize: u32le(), }); const buffer = new Uint8Array(5); const data = { flags: { compressed: 1, encrypted: 0, version: 2 }, payloadSize: 1024, }; packet.encode(data, buffer); const [decoded] = packet.decode(buffer); assertEquals(decoded.flags.compressed, 1); assertEquals(decoded.flags.version, 2); assertEquals(decoded.payloadSize, 1024);
Real-world: PNG/Zlib header
Real-world: PNG/Zlib header
import { assertEquals } from "@std/assert"; import { bitStruct } from "@hertzg/binstruct/bits"; // Zlib header (RFC 1950): 2 bytes with bit fields // MSB-first ordering means fields are listed in bit order (7→0) const zlibHeader = bitStruct({ compressionInfo: 4, // CMF bits 7-4: Compression info compressionMethod: 4, // CMF bits 3-0: Compression method fcheck: 5, // FLG bits 7-3: Check bits fdict: 1, // FLG bit 2: Preset dictionary flevel: 2, // FLG bits 1-0: Compression level }); // Decode a common zlib header: 0x78 0x9C const header = new Uint8Array([0x78, 0x9C]); const [decoded] = zlibHeader.decode(header); assertEquals(decoded.compressionMethod, 8); // Deflate assertEquals(decoded.compressionInfo, 7); // 32KB window
Real-world: Ethernet VLAN tag
Real-world: Ethernet VLAN tag
import { assertEquals } from "@std/assert"; import { bitStruct } from "@hertzg/binstruct/bits"; // 802.1Q VLAN tag: 2 bytes const vlanTCI = bitStruct({ pcp: 3, // Priority Code Point (0-7) dei: 1, // Drop Eligible Indicator vid: 12, // VLAN Identifier (0-4095) }); // VLAN 100 with priority 5 const value = { pcp: 5, dei: 0, vid: 100 }; const buffer = new Uint8Array(2); vlanTCI.encode(value, buffer); const [decoded] = vlanTCI.decode(buffer); assertEquals(decoded.pcp, 5); assertEquals(decoded.vid, 100);
Creates a Coder for bit-packed structures with MSB-first ordering.
Schema type for bitStruct, mapping field names to bit counts.
Type of the decoded value from a bitStruct. All fields are decoded as numbers.
Buffer management utilities for automatic buffer growth during encoding operations.
Automatically grows a buffer until the encoding function succeeds.
Configuration options for automatic buffer growth.
-
growthFactor: number
Growth factor multiplier for buffer resizing. Must be greater than 1. Each resize multiplies the current size by this factor. Defaults to 2 (doubling).
-
initialSize: number
Initial buffer size in bytes. Must be greater than 0 and less than or equal to maxByteLength. Defaults to 4096 bytes (4KB).
-
maxByteLength: number
Maximum buffer size in bytes. The buffer will not grow beyond this limit. When reached, a RangeError will be thrown. Defaults to 400MB.
Numeric data encoding and decoding utilities for binary structures.
Basic numeric encoding and decoding:
Basic numeric encoding and decoding:
import { assertEquals } from "@std/assert"; import { u16le, s32be, f64le } from "@hertzg/binstruct/numeric"; import { struct } from "@hertzg/binstruct/struct"; // Create a simple numeric structure const numericStruct = struct({ smallNumber: u16le(), // 16-bit unsigned, little-endian signedNumber: s32be(), // 32-bit signed, big-endian floatNumber: f64le(), // 64-bit float, little-endian }); // Test data const testData = { smallNumber: 12345, // Fits in 16-bit unsigned (0-65535) signedNumber: -1000000, // 32-bit signed range floatNumber: 3.14159, // Pi approximation }; // Encode to binary const buffer = new Uint8Array(100); const bytesWritten = numericStruct.encode(testData, buffer); // Decode from binary const [decoded, bytesRead] = numericStruct.decode(buffer); // Verify the results assertEquals(decoded.smallNumber, testData.smallNumber); assertEquals(decoded.signedNumber, testData.signedNumber); assertEquals(decoded.floatNumber, testData.floatNumber); assertEquals(bytesWritten, bytesRead);
Network protocol with mixed endianness:
Network protocol with mixed endianness:
import { assertEquals } from "@std/assert"; import { u16be, u32le, u64be } from "@hertzg/binstruct/numeric"; import { struct } from "@hertzg/binstruct/struct"; // Network packet header with mixed endianness const packetHeader = struct({ magic: u32le(), // Magic number (little-endian) version: u16be(), // Protocol version (big-endian, network order) flags: u16be(), // Control flags (big-endian) timestamp: u64be(), // Timestamp (big-endian) payloadSize: u32le(), // Payload size (little-endian) }); const testPacket = { magic: 0x12345678, version: 1, flags: 0x8000, timestamp: 1234567890n, payloadSize: 1024, }; const buffer = new Uint8Array(100); const bytesWritten = packetHeader.encode(testPacket, buffer); const [decoded, bytesRead] = packetHeader.decode(buffer); assertEquals(decoded.magic, testPacket.magic); assertEquals(decoded.version, testPacket.version); assertEquals(decoded.flags, testPacket.flags); assertEquals(decoded.timestamp, testPacket.timestamp); assertEquals(decoded.payloadSize, testPacket.payloadSize); assertEquals(bytesWritten, bytesRead);
Creates a coder for 16-bit floating-point numbers.
Convenience function for 16-bit floating point with big-endian byte order.
Convenience function for 16-bit floating point with little-endian byte order.
Creates a coder for 32-bit floating-point numbers.
Convenience function for 32-bit floating point with big-endian byte order.
Convenience function for 32-bit floating point with little-endian byte order.
Creates a coder for 64-bit floating-point numbers.
Convenience function for 64-bit floating point with big-endian byte order.
Convenience function for 64-bit floating point with little-endian byte order.
Creates a coder for 16-bit signed integers.
Convenience function for 16-bit signed integer with big-endian byte order.
Convenience function for 16-bit signed integer with little-endian byte order.
Creates a coder for 32-bit signed integers.
Convenience function for 32-bit signed integer with big-endian byte order.
Convenience function for 32-bit signed integer with little-endian byte order.
Creates a coder for 64-bit signed integers.
Convenience function for 64-bit signed integer with big-endian byte order.
Convenience function for 64-bit signed integer with little-endian byte order.
Creates a coder for 8-bit signed integers.
Convenience function for 8-bit signed integer with big-endian byte order.
Convenience function for 8-bit signed integer with little-endian byte order.
Creates a coder for 16-bit unsigned integers.
Convenience function for 16-bit unsigned integer with big-endian byte order.
Convenience function for 16-bit unsigned integer with little-endian byte order.
Creates a coder for 32-bit unsigned integers.
Convenience function for 32-bit unsigned integer with big-endian byte order.
Convenience function for 32-bit unsigned integer with little-endian byte order.
Creates a coder for 64-bit unsigned integers.
Convenience function for 64-bit unsigned integer with big-endian byte order.
Convenience function for 64-bit unsigned integer with little-endian byte order.
Creates a coder for 8-bit unsigned integers.
Convenience function for 8-bit unsigned integer with big-endian byte order.
Convenience function for 8-bit unsigned integer with little-endian byte order.
Endianness type for numeric data encoding and decoding.
Array coders for binary structures.
Length-prefixed and fixed-length arrays
Length-prefixed and fixed-length arrays
import { assertEquals } from "@std/assert"; import { array, arrayLP, arrayFL } from "@hertzg/binstruct/array"; import { struct } from "@hertzg/binstruct/struct"; import { u8le, u16le } from "@hertzg/binstruct/numeric"; // A structure mixing both array kinds const coder = struct({ lenPref: arrayLP(u8le(), u16le()), // [len:u16] followed by items fixed: arrayFL(u8le(), 3), // exactly 3 items auto: array(u8le(), 2), // auto-selects fixed-length while: array(u8le(), ({ index }) => index < 2), // while index < 2 }); const value = { lenPref: [1, 2, 3], fixed: [4, 5, 6], auto: [7, 8], while: [9, 10] }; const buf = new Uint8Array(1024); const written = coder.encode(value, buf); const [decoded, read] = coder.decode(buf); assertEquals(decoded, value); assertEquals(written, read);
Creates a Coder for arrays that automatically chooses between length-prefixed and fixed-length based on the arguments provided.
Creates a Coder for fixed-length arrays of a given element type.
Creates a Coder for length-prefixed arrays of a given element type.
Creates a Coder for arrays using a custom condition function to determine when to stop.
Condition function type for arrayWhile that determines when to continue processing array elements.
Symbol identifier for fixed-length array coders.
Symbol identifier for length-prefixed array coders.
Symbol identifier for conditional while-loop array coders.
Struct coder for binary structures.
Minimal usage
Minimal usage
import { assertEquals } from "@std/assert"; import { struct } from "@hertzg/binstruct/struct"; import { u16le, u8le } from "@hertzg/binstruct/numeric"; const coder = struct({ id: u16le(), flag: u8le() }); const value = { id: 513, flag: 7 }; const buf = new Uint8Array(32); const written = coder.encode(value, buf); const [decoded, read] = coder.decode(buf); assertEquals(decoded, value); assertEquals(written, read);
Creates a Coder for structured data from an object of property names to coders.
The decoded object type of a struct schema: each property's coder
Coder<U> contributes a property of type U.
String coders for binary structures.
Using all string variants
Using all string variants
import { assertEquals } from "@std/assert"; import { string, stringLP, stringNT, stringFL } from "@hertzg/binstruct/string"; import { struct } from "@hertzg/binstruct/struct"; import { u8le, u16le } from "@hertzg/binstruct/numeric"; const coder = struct({ lp: stringLP(u16le()), // [len:u16] followed by UTF-8 nt: stringNT(), // UTF-8 bytes followed by 0x00 fl: stringFL(5), // exactly 5 bytes age: u8le(), }); const value = { lp: "alpha", nt: "beta", fl: "gamma", age: 42 }; const buf = new Uint8Array(256); const written = coder.encode(value, buf); const [decoded, read] = coder.decode(buf); assertEquals(decoded.lp, value.lp); assertEquals(decoded.nt, value.nt); assertEquals(decoded.fl, value.fl); assertEquals(decoded.age, value.age); assertEquals(written, read);
Creates a Coder for strings that automatically chooses between length-prefixed, null-terminated, and fixed-length based on the arguments provided.
Creates a Coder for fixed-length strings.
Creates a Coder for length-prefixed strings.
Creates a Coder for null-terminated strings.
Symbol identifier for fixed-length string coders.
Symbol identifier for length-prefixed string coders.
Symbol identifier for null-terminated string coders.
Byte-slice coder for binary structures.
Fixed and variable length
Fixed and variable length
import { assertEquals } from "@std/assert"; import { bytes } from "@hertzg/binstruct/bytes"; const fixed = bytes(4); const variable = bytes(); const input = new Uint8Array([1, 2, 3, 4, 5]); const buf = new Uint8Array(32); const w1 = fixed.encode(input, buf); const [d1] = fixed.decode(buf); assertEquals(Array.from(d1), [1, 2, 3, 4]); const w2 = variable.encode(input, buf); const [d2] = variable.decode(buf); assertEquals(Array.from(d2.slice(0, input.length)), Array.from(input)); assertEquals(typeof w1, "number"); assertEquals(typeof w2, "number");
Creates a Coder for byte slices.
This module provides a refinement system for binary structure coders. It allows you to transform decoded values into refined types and vice versa during encoding/decoding operations.
Example 1
Example 1
import { assertEquals } from "@std/assert"; import { u8, refine } from "@hertzg/binstruct"; const bitfield = refine(u8(), { refine: (unrefined: number) => unrefined.toString(2) .padStart(8, "0") .split("") .map(Number), unrefine: (refined) => parseInt(refined.join(""), 2), }); const coder = bitfield(); const buffer = new Uint8Array(10); const bytesWritten = coder.encode([1, 0, 1, 0, 1, 0, 1, 0], buffer); const [decoded, bytesRead] = coder.decode(buffer); assertEquals(bytesWritten, 1); assertEquals(bytesRead, bytesWritten); assertEquals(buffer[0], 0b10101010); assertEquals(decoded, [1, 0, 1, 0, 1, 0, 1, 0]);
Example 2
Example 2
import { assertEquals } from "@std/assert"; import { u8, refine } from "@hertzg/binstruct"; import type { Context } from "@hertzg/binstruct"; const u8Mapped = refine(u8(), { refine: (unrefined, _context, min: number, max: number) : number => (min + (max - min) * unrefined / 0xff) >>> 0, unrefine: (refined, _context, min: number, max: number) => ((refined - min) / (max - min) * 0xff) >>> 0, }); const coder = u8Mapped(-100, 100); const buffer = new Uint8Array(100); const bytesWritten = coder.encode(0, buffer); const [decoded, bytesRead] = coder.decode(buffer); assertEquals(bytesWritten, 1); assertEquals(bytesRead, bytesWritten); assertEquals(buffer[0], 0x7f); assertEquals(decoded, 0);
Creates a refined coder that applies transformations during encoding and decoding.
Creates a Refiner that swaps named Uint8Array fields of a
host record for the typed values produced by their sub-coders. Fields not
in coders are passed through unchanged.
Creates a coder that conditionally applies refiners based on selector functions, using switch-like semantics for bidirectional encoding and decoding.
Mapped type extracting the decoded value type for each entry in a
FieldCoders map. Given { payload: Coder<Ipv4> } it yields
{ payload: Ipv4 }.
Map of host field names to the sub-coder that decodes/encodes that field's
bytes. Used as the input to refineFields.
Extracts the union of all refined types from a record of refiners.
A refiner that transforms decoded values into refined types and vice versa.
-
refine: () => TRefinedunrefined: TUnrefined,context: Context,...args: TArgs
Transforms a decoded value into a refined value.
-
unrefine: () => TUnrefinedrefined: TRefined,context: Context,...args: TArgs
Transforms a refined value back to the original decoded format.
Reference system for binary data encoding and decoding.
Example 1
Example 1
import { assertEquals } from "@std/assert"; import { ref, computedRef, struct, u16le, u8, array } from "@hertzg/binstruct"; // Create references for shared lengths const channelsLength = u16le(); const pointsLength = u16le(); // Define a structure with shared length references const coder = struct({ channelsLength: channelsLength, channels: struct({ r: array(u8(), ref(channelsLength)), g: array(u8(), ref(channelsLength)), b: array(u8(), ref(channelsLength)), }), pointsLength: pointsLength, points: array(u16le(), ref(pointsLength)), }); // Test data const testData = { channelsLength: 3, channels: { r: [255, 128, 64], g: [0, 255, 128], b: [0, 0, 255], }, pointsLength: 2, points: [100, 200], }; // Encode and decode const buffer = new Uint8Array(1000); const bytesWritten = coder.encode(testData, buffer); const [decoded, bytesRead] = coder.decode(buffer); // Verify the data assertEquals(decoded.channelsLength, testData.channelsLength); assertEquals(decoded.channels.r, testData.channels.r); assertEquals(decoded.channels.g, testData.channels.g); assertEquals(decoded.channels.b, testData.channels.b); assertEquals(decoded.pointsLength, testData.pointsLength); assertEquals(decoded.points, testData.points); assertEquals(bytesWritten, bytesRead);
Creates a computed reference that depends on multiple other references.
Checks if a value is a reference created by the ref function.
Creates a reference value that can be resolved during encoding/decoding.
Retrieves the value from a reference or returns the value directly if it's not a reference.
Sets a value in the context for a specific coder reference.
Ensures that a context has the necessary reference storage initialized.
A weak map interface for storing references in the encoding/decoding context.
-
get<T>(coder: Coder<T>): T | undefined
Gets a reference value for the given coder.
-
has<T>(coder: Coder<T>): boolean
Checks if a reference value exists for the given coder.
-
set<T>(): thiscoder: Coder<T>,value: T
Sets a reference value for the given coder.
A type representing a reference value that can be resolved during encoding/decoding.
-
kIsRefValue: true
Brand marker identifying the function as a reference value.
Type utility to unwrap reference types from a tuple of references.
Symbol identifier for reference values.
Lazily-built coder for mutually-recursive coder graphs.
Breaking a build-time cycle between two struct coders
Breaking a build-time cycle between two struct coders
import { assertEquals } from "@std/assert"; import { struct, lazy, type Coder } from "@hertzg/binstruct"; import { u8 } from "@hertzg/binstruct/numeric"; interface Ping { kind: 0; next: Pong; } interface Pong { kind: 1; ttl: number; } // `pingCoder` needs `pongCoder` while building its own `next` field, but // `pongCoder` is declared afterwards and does not exist yet at that point. // Without `lazy()` this is a ReferenceError (reading `pongCoder` inside // its own temporal dead zone) once the graph grows past two coders and // closes an actual cycle, as it does for tunneling protocols. const pingCoder: Coder<Ping> = struct({ kind: u8() as unknown as Coder<0>, next: lazy(() => pongCoder), }); const pongCoder: Coder<Pong> = struct({ kind: u8() as unknown as Coder<1>, ttl: u8(), }); const buffer = new Uint8Array(8); const value: Ping = { kind: 0, next: { kind: 1, ttl: 64 } }; const written = pingCoder.encode(value, buffer); const [decoded, read] = pingCoder.decode(buffer); assertEquals(decoded, value); assertEquals(written, read);
Wraps a coder factory so the coder it produces is built on first use
instead of when lazy() is called, and only ever built once.
Helper functions for simplified binary encoding and decoding operations.
Basic encoding and decoding
Basic encoding and decoding
import { assertEquals } from "@std/assert"; import { encode, decode, struct, u16le, u8le } from "@hertzg/binstruct"; const coder = struct({ id: u16le(), flag: u8le() }); const data = { id: 42, flag: 7 }; // Encode without providing a buffer - auto-allocates const encoded = encode(coder, data); assertEquals(encoded.length, 3); // Decode and get the decoded data const decodedData = decode(coder, encoded); assertEquals(decodedData.id, 42); assertEquals(decodedData.flag, 7);
Using provided target buffer
Using provided target buffer
import { assertEquals } from "@std/assert"; import { encode, decode, struct, u32le } from "@hertzg/binstruct"; const coder = struct({ value: u32le() }); const data = { value: 12345 }; const buffer = new Uint8Array(100); // Encode to provided buffer const encoded = encode(coder, data, undefined, buffer); assertEquals(encoded.length, 4); assertEquals(encoded.buffer, buffer.buffer); // Decode from buffer const decodedData = decode(coder, encoded); assertEquals(decodedData.value, 12345);
Round-trip encoding and decoding
Round-trip encoding and decoding
import { assertEquals } from "@std/assert"; import { encode, decode, struct, u16le, u8le, string } from "@hertzg/binstruct"; const coder = struct({ id: u16le(), name: string(u16le()), // Length-prefixed string active: u8le(), }); const originalData = { id: 1001, name: "test", active: 1 }; // Encode const encoded = encode(coder, originalData); assertEquals(encoded.length, 9); // 2 (id) + 2 (name length) + 4 (name bytes) + 1 (active) // Decode const decodedData = decode(coder, encoded); assertEquals(decodedData.id, originalData.id); assertEquals(decodedData.name, originalData.name); assertEquals(decodedData.active, originalData.active);
Using custom context
Using custom context
import { assertEquals } from "@std/assert"; import { encode, decode, createContext, struct, u16le } from "@hertzg/binstruct"; const coder = struct({ value: u16le() }); const data = { value: 42 }; const context = createContext("encode"); const encoded = encode(coder, data, context); assertEquals(encoded.length, 2); const decodeContext = createContext("decode"); const decodedData = decode(coder, encoded, decodeContext); assertEquals(decodedData.value, 42);
Decodes data using the provided coder, returning the decoded value.
Encodes data using the provided coder, handling buffer allocation automatically.