Bit/Byte Vector
Overview
ribs_binary provides two complementary types for working with binary data:
ByteVector— an indexed, immutable sequence of bytes backed byUint8ListBitVector— an indexed, immutable sequence of bits backed byByteVector
Both types support efficient concatenation through an internal tree structure, meaning
you can build up large vectors from smaller pieces without copying data at every step.
A compact() call materialises the tree into a flat array when you need it.
Motivation
Dart's built-in Uint8List is mutable and byte-oriented. When you need to:
- work at bit granularity (protocol frames, codecs, compression)
- parse or serialise binary formats with a clean, composable API
- build binary data incrementally without copy costs
- encode/decode to/from hex, base64, binary strings reliably
…raw Uint8List can quickly become awkward. ByteVector and BitVector give you
value-semantics (immutable, structurally equal), a rich API, and safe indexed
access that returns Option rather than throwing.
ByteVector
Creating ByteVectors
// Creating ByteVectors
final bytesA = ByteVector.empty;
final bytesB = ByteVector([0, 12, 32]);
final bytesC = ByteVector.low(10); // 10 bytes with all bits set to 0
final bytesD = ByteVector.high(10); // 10 bytes with all bits set to 1
final bytesE = ByteVector(Uint8List(10));
// Creating BitVectors
final bitsA = BitVector.empty;
final bitsB = BitVector.fromByteVector(bytesA);
final bitsC = BitVector.low(10); // 10 bits all set to 0
final bitsD = BitVector.high(10); // 10 bits all set to 1
You can also parse from string representations:
// Parsing from string representations
final fromHex = ByteVector.fromValidHex('cafebabe');
final fromBin = ByteVector.fromValidBin('10110011');
final fromB64 = ByteVector.fromValidBase64('AAEC');
// Encoding to string representations
print(fromHex.toHex()); // cafebabe
print(fromBin.toBin()); // 10110011
print(fromB64.toBase64()); // AAEC
// Hex dump for debugging
ByteVector([0xde, 0xad, 0xbe, 0xef]).printHexDump();
// 00000000 de ad be ef |....|
Working with ByteVectors
ByteVector provides the standard slice/drop/take/split operations you'd expect from an immutable
collection, plus safe indexed access via lift:
final header = ByteVector([0xca, 0xfe, 0xba, 0xbe]);
final payload = ByteVector([0x01, 0x02, 0x03]);
// Concatenate two vectors
final packet = header.concat(payload); // 7 bytes
// Slice, drop, and take
final first4 = packet.take(4); // 0xcafebabe
final rest = packet.drop(4); // 0x010203
final middle = packet.slice(1, 3); // 0xfeba
// Split at a position
final (head, tail) = packet.splitAt(4);
// Index into individual bytes
final secondByte = packet[1]; // 0xfe
final safe = packet.lift(99); // None — index out of range
Numeric conversions let you round-trip integers through binary representations:
// Integer ↔ ByteVector (big-endian by default)
final encoded = ByteVector.fromInt(0x0102, size: 2); // 0x0102
final value = encoded.toInt(); // 258
// Unsigned decode (no sign extension)
final unsigned = ByteVector([0xff]).toUnsignedInt(); // 255
Bitwise operations (&, |, ^, ~, <<, >>) work element-wise across two vectors of the
same length:
final a = ByteVector.fromValidHex('0f');
final b = ByteVector.fromValidHex('aa');
print((a & b).toHex()); // 0a — AND
print((a | b).toHex()); // af — OR
print((a ^ b).toHex()); // a5 — XOR
print((~a).toHex()); // f0 — NOT
print((a << 2).toHex()); // 3c — left shift
print((b >> 1).toHex()); // 55 — right shift
BitVector
BitVector gives you all the same structural operations as ByteVector, but at bit granularity.
This is useful when you need to construct or inspect individual bits — for example when building
wire-format frames, implementing binary codecs, or working with compressed data.
Creating and Manipulating BitVectors
final bits = BitVector.bits([true, false, true, true, false, false, true, true]);
print(bits.toBin()); // 10110011
print('0x${bits.toHex()}'); // 0xb3
bits.concat(bits); // combine 2 BitVectors
bits.drop(6); // drop the first 6 bits
bits.get(7); // get 7th bit
bits.clear(3); // set bit at index 3 to 0
bits.set(3); // set bit at index 3 to 1
You can also build and manipulate bit fields one bit at a time:
// Build a 13-bit control field manually
var frame = BitVector.low(13); // 0000000000000
frame = frame.set(0); // set the first flag
frame = frame.set(3); // set a second flag
frame = frame.clear(0); // clear the first flag again
// Pad to a byte boundary before serialising
final aligned = frame.padRight(16); // 16 bits (2 bytes)
print(aligned.toHex()); // 0008
// Bitwise operations work identically to ByteVector
final mask = BitVector.fromValidBin('1010');
final data = BitVector.fromValidBin('1100');
print(data.and(mask).toBin()); // 1000
print((data | mask).toBin()); // 1110
Converting Between ByteVector and BitVector
The two types are closely related and convert losslessly in both directions:
| Direction | How | Notes |
|---|---|---|
ByteVector → BitVector | .bits / .toBitVector() | Each byte expands to exactly 8 bits |
BitVector → ByteVector | .bytes / .toByteVector() | Last byte is zero-padded if size % 8 ≠ 0 |
// ByteVector → BitVector: every byte expands to 8 bits
final bytes = ByteVector([0xb3]); // 1 byte
final bits = bytes.bits; // 8 bits: 10110011
print(bits.size); // 8
print(bits.toBin()); // 10110011
// BitVector → ByteVector: bits are grouped into bytes (last byte zero-padded)
final tenBits = BitVector.fromValidBin('1011001101'); // 10 bits
final asBytes = tenBits.bytes; // 2 bytes (last 6 bits padded with zeros)
print(asBytes.size); // 2
print(asBytes.toHex()); // b340
The padding in the BitVector → ByteVector direction means the conversion is not always
round-trip safe at the bit level: a 10-bit vector becomes 2 bytes (16 bits), and calling
.bits on the result gives 16 bits, not the original 10. If you need to preserve an exact
bit count across serialisation, store the size separately alongside the bytes.