Binary Codec Package¶
A combinator library for bidirectional binary parsing and building.
Define a Codec[A] once and use it to both decode bytes into A
and encode A back to bytes.
Codecs are val — immutable and shareable across actors. Small
codecs compose into larger ones:
Seq2throughSeq6sequence fields into a tuple.Mappedmaps a wire type to a domain type and back.BitsU8,BitsU16,BitsU32,BitsU64read sub-byte fields.FlatZipdecodes a value, then uses it to pick the next codec.Countreads a count and decodes that many elements.Manyrepeats a codec until the input runs out.Discriminateddispatches on a tag to decode a tagged union.Literal,ConstThen,ThenConstmatch or emit fixed bytes.VariableSizereads a length, then decodes a body of that size.Conditionalincludes a field only when a flag is true.SkipBitsskips padding bits.FrameDecoderwraps any codec for TCP stream framing.
Every combinator is itself a Codec, so compositions nest: the
codec inside a Count can be a Discriminated that contains an
Mapped wrapping a Seq4.
decode takes an Array[U8] val and a bit offset. It returns
(A, USize) — the value and the number of bits consumed — or a
DecodeError. The input array is val, so byte-slice codecs
return zero-copy views via Array.trim.
encode converts a value to bytes, returning Array[U8] val or
an EncodeError.
Errors are data: InsufficientData means the input is incomplete
(wait for more bytes); MalformedData means the data is
structurally wrong (a literal mismatch, an unknown tag, a
constraint violation). FrameDecoder treats InsufficientData as
"wait for more" and MalformedData as a protocol error.
Example: fixed-layout struct¶
Seq4 sequences four fields into a tuple. Mapped maps the tuple to
a domain class and back. The result is a single codec that both
decodes and encodes UDP headers.
use "binary_codec"
class val UdpHeader
let src_port: U16
let dst_port: U16
let length: U16
let checksum: U16
new val create(s: U16, d: U16, l: U16, c: U16) =>
src_port = s; dst_port = d; length = l; checksum = c
actor Main
new create(env: Env) =>
let codec: Codec[UdpHeader] =
Mapped[(U16, U16, U16, U16), UdpHeader](
Seq4[U16, U16, U16, U16](U16BE, U16BE, U16BE, U16BE),
{(t) => UdpHeader(t._1, t._2, t._3, t._4)},
{(h) => (h.src_port, h.dst_port, h.length, h.checksum)})
// Decode
let data: Array[U8] val =
[0x00; 0x50; 0x00; 0x51; 0x00; 0x08; 0x12; 0x34]
match codec.decode(data, 0)
| (let h: UdpHeader, _) =>
env.out.print("src=" + h.src_port.string()) // 80
env.out.print("dst=" + h.dst_port.string()) // 81
| InsufficientData =>
env.out.print("need more data")
| let e: MalformedData =>
env.out.print("bad data: " + e.string())
end
// Encode
match codec.encode(UdpHeader(80, 81, 8, 0x1234))
| let bytes: Array[U8] val =>
// bytes is the same 8 bytes as data above
None
| let e: EncodeError =>
env.out.print("encode failed: " + e.string())
end
Example: sub-byte bit fields¶
BitsU8(n) reads n bits (1–8) as a U8. Fields at non-byte
widths compose with Seq the same way byte-aligned fields do.
The first two bytes of an IPv4 header contain four fields that
do not fall on byte boundaries:
// version(4 bits) + IHL(4) + DSCP(6) + ECN(2) = 16 bits
let ipv4_prefix =
Seq4[U8, U8, U8, U8](
BitsU8(4), BitsU8(4), BitsU8(6), BitsU8(2))
// 0x45 = version 4, IHL 5; 0x00 = DSCP 0, ECN 0
let data: Array[U8] val = [0x45; 0x00]
match ipv4_prefix.decode(data, 0)
| ((let ver: U8, let ihl: U8, let dscp: U8, let ecn: U8), _) =>
// ver == 4, ihl == 5, dscp == 0, ecn == 0
end
For wider sub-byte fields, use BitsU16(n) (1–16 bits),
BitsU32(n) (1–32), or BitsU64(n) (1–64).
Example: dependent parsing (FlatZip)¶
FlatZip decodes a value and passes it to a function that returns
the codec for the next field. This handles formats where a length
or type field determines what follows.
// A U8 byte count followed by that many bytes
let length_prefixed =
FlatZip[U8, Array[U8] val](
U8Codec,
{(len: U8): Codec[Array[U8] val] =>
Bytes(len.usize())})
// 0x03 means 3 bytes follow
let data: Array[U8] val = [0x03; 0x41; 0x42; 0x43]
match length_prefixed.decode(data, 0)
| ((let len: U8, let body: Array[U8] val), _) =>
// len == 3, body == [0x41; 0x42; 0x43]
end
Example: count-prefixed array (Count)¶
Count reads a U16 count field, then decodes that many
instances of a body codec into an Array.
let records = Count[U32](U16BE, U32BE)
// Count = 2, then two U32BE values (10 and 20)
let data: Array[U8] val =
[0x00; 0x02; 0x00; 0x00; 0x00; 0x0A; 0x00; 0x00; 0x00; 0x14]
match records.decode(data, 0)
| (let arr: Array[U32] val, _) =>
// arr.size() == 2, arr(0)? == 10, arr(1)? == 20
end
Example: fixed byte sequence (ConstThen)¶
Literal matches a fixed byte sequence on decode and emits it on
encode. ConstThen pairs a Literal before a value codec,
keeping only the value. ThenConst does the reverse — value
first, then literal.
// Match MPEG sync byte 0x47, then read a payload byte
let sync = ConstThen[U8](Literal([as U8: 0x47]), U8Codec)
match sync.decode([as U8: 0x47; 0xFF], 0)
| (let v: U8, _) => // v == 0xFF
end
// Wrong sync byte produces MalformedData
match sync.decode([as U8: 0x00; 0xFF], 0)
| let e: MalformedData => // literal mismatch
end
Example: tagged union (Discriminated)¶
Discriminated reads a tag and dispatches to a variant codec.
Register each variant with DiscriminatedBuilder: a tag value,
a codec for the variant body, and a narrowing function that casts
from the union type to the variant.
use "binary_codec"
type Command is (Ping | Data)
class val Ping
let seq: U16
new val create(s: U16) => seq = s
class val Data
let payload: Array[U8] val
new val create(p: Array[U8] val) => payload = p
// Tag 0x01 → Ping (U16BE body)
// Tag 0x02 → Data (remaining bytes)
let builder = DiscriminatedBuilder[U8, Command](U8Codec)
builder
.case_of[Ping](0x01,
Mapped[U16, Ping](U16BE,
{(s) => Ping(s)}, {(p) => p.seq}),
{(c: Command): Ping ? => c as Ping})
.case_of[Data](0x02,
Mapped[Array[U8] val, Data](BytesRest,
{(b) => Data(b)}, {(d) => d.payload}),
{(c: Command): Data ? => c as Data})
let codec = builder.build()
match codec.decode([as U8: 0x01; 0x00; 0x42], 0)
| (let p: Ping, _) => // p.seq == 66
| (let d: Data, _) => // not this branch
end
// Encode round-trips through the same codec
match codec.encode(Ping(66))
| let bytes: Array[U8] val =>
// bytes == [0x01; 0x00; 0x42]
end
Example: TCP stream framing (FrameDecoder)¶
FrameDecoder wraps a codec for TCP streams. Feed it iso byte
chunks as they arrive; it buffers incomplete data across calls and
delivers complete decoded values through FrameNotify.
use "binary_codec"
class ref MyNotify is FrameNotify[U16]
fun ref frame_received(value: U16) =>
// called once per complete U16 from the stream
None
fun ref frame_error(err: MalformedData) =>
None
// Wrap U16BE as a stream codec
let framer = FrameDecoder[U16](U16BE)
let notify = MyNotify
// First chunk: one byte — not enough for a U16
let c1: Array[U8] iso = recover [as U8: 0x00] end
framer.received(consume c1, notify)
// Nothing fires. The byte is buffered.
// Second chunk: two more bytes
let c2: Array[U8] iso = recover [as U8: 0x42; 0x00] end
framer.received(consume c2, notify)
// frame_received(0x0042) fires (from buffered 0x00 + new 0x42).
// Remaining 0x00 is buffered for the next call.
Public Types¶
- class BitsU16
- class BitsU32
- class BitsU64
- class BitsU8
- primitive BoolBit
- class Bytes
- primitive BytesRest
- trait Codec
- class Conditional
- class ConstThen
- class Count
- type DecodeError
- class Discriminated
- class DiscriminatedBuilder
- type EncodeError
- class EncodeViolation
- primitive F32BE
- primitive F32LE
- primitive F64BE
- primitive F64LE
- class FlatZip
- class FrameDecoder
- interface FrameNotify
- primitive I128BE
- primitive I128LE
- primitive I16BE
- primitive I16LE
- primitive I32BE
- primitive I32LE
- primitive I64BE
- primitive I64LE
- primitive I8Codec
- primitive InsufficientData
- class Literal
- class MalformedData
- class Many
- class Mapped
- class Seq2
- class Seq3
- class Seq4
- class Seq5
- class Seq6
- class SkipBits
- class ThenConst
- primitive U128BE
- primitive U128LE
- primitive U16BE
- primitive U16LE
- primitive U32BE
- primitive U32LE
- primitive U64BE
- primitive U64LE
- primitive U8Codec
- class VariableSize
- class Widen