Bits need a place to put their hats
A packet has arrived with a small header and a payload it would rather not explain twice.
Bit-array syntax lets the program describe where those pieces sit.
Write the layout down
Here is a byte-sized tag, a sixteen-bit length and a UTF-8 payload:
import pooka/bit_array
main() {
let packet = <<1:8, 3:16, "owl":utf8>>
assert bit_array.byte_size(packet) == 6
}
The length field occupies two bytes; its value is three because the payload contains three bytes. The size of the field and the number stored in it are different quantities.
That small distinction is responsible for a respectable number of broken envelopes.
Read the layout as a pattern
A pattern can bind the length before using it to read the body:
import pooka/bit_array
read(packet: BitArray) -> Result(String, String) {
match packet {
<<1:8, length:16, body:bytes-size(length)>> ->
match bit_array.to_string(body) {
Ok(text) -> Ok(text)
Error(_) -> Error("payload is not text")
}
_ -> Error("bad envelope")
}
}
main() {
assert read(<<1:8, 3:16, "owl":utf8>>) == Ok("owl")
assert read(<<1:8, 4:16, "owl":utf8>>) == Error("bad envelope")
}
The second assertion matters. A declaration of four bytes does not make a three-byte payload longer. The pattern fails, and the fallback reports the malformed input.
Let binary data remain binary
The conversion to String is a separate decision. A payload can have the right byte count while containing invalid text, so that conversion has its own error branch.
Other protocols may keep the payload as bytes. They may also allow trailing fields, require a different endianness, or work with values that are not byte-aligned. Those choices belong in the format’s explicit rules rather than in guesses made by the reader.
For the compiler, bit-array construction and matching must agree on sizes and units. A convenient syntax is no comfort if the writer and reader quietly count different things.
The packet can keep its hat. We have finally labelled which seat belongs to the header.
— Peregrine Finch