convertlib
A pure-Dart library of fast, error-resilient codecs: binary, octal, hex, Base-32, Base-64, BigInt, UTF-8, and the PHC / Modular Crypt Format — with a rich set of alphabet variants, optional padding, and zero dependencies.
convertlib is the foundation layer of a three-package family:
Both hashlib (hashes, MACs, KDFs) and cipherlib (ciphers, AEAD, ML-KEM)
build on it, reusing its codecs to render digests, keys, and ciphertext as
text. It carries no runtime dependencies of its own.
Highlights
- Runs on every platform: pure Dart with no native code or FFI, so the same library works everywhere Dart does — the VM, Flutter (Android, iOS, Windows, macOS, Linux), and the web (dart2js and dart2wasm).
- Zero dependencies: nothing is pulled into your dependency tree.
- Broad coverage: Base-2, Base-8, Base-16, Base-32, Base-64, BigInt, UTF-8,
and the PHC / Modular Crypt Format, each with a symmetrical
to/frompair. - Many alphabet variants: RFC 4648 standard, base32hex, Crockford, z-base-32, geohash, word-safe, URL/filename-safe Base-64, and bcrypt.
- Optional padding: encode and decode with or without
=padding. - Error resilient: decoders accept case and alphabet variations where the encoding allows it (either Base-64 alphabet, upper or lower Base-32), and raise typed errors on genuinely invalid input.
- Convenient output: results integrate with
ByteCollector, the digest container shared withhashlib, for one-call re-encoding. - Bytes or strings, throwing or not: every encoder has a
to<Name>Bytestwin returning aUint8List, every decoder a non-throwingtryFrom<Name>returningnullon bad input, plus a top-levelconstantTimeEqualsfor MAC/digest checks.
Install
dependencies:
convertlib: ^3.6.1
or run dart pub add convertlib. A single import exposes every codec:
import 'package:convertlib/convertlib.dart';
Full API reference: convertlib library.
Quickstart
Every codec exposes a to<Name> encoder and a from<Name> decoder that are
exact inverses of each other:
import 'package:convertlib/convertlib.dart';
void main() {
final data = toUtf8('convertlib');
// Encode the same bytes in a few common ways
final hex = toHex(data);
final b64 = toBase64(data);
final b32 = toBase32(data);
print('bytes : $data');
print('hex : $hex');
print('base64 : $b64');
print('base32 : $b32');
// Every encoder has an exact inverse
print('decoded: ${fromUtf8(fromHex(hex))}');
print('match : ${fromUtf8(fromBase64(b64)) == 'convertlib'}');
}
Every snippet in this README is also a runnable program in the example folder.
Supported codecs
| Encoding | Class | Encode | Decode | Source |
|---|---|---|---|---|
| Binary (Base-2) | Base2Codec |
toBinary, toBinaryBytes |
fromBinary, tryFromBinary |
— |
| Octal (Base-8) | Base8Codec |
toOctal, toOctalBytes |
fromOctal, tryFromOctal |
— |
| Hexadecimal (Base-16) | Base16Codec |
toHex, toHexBytes |
fromHex, tryFromHex |
RFC-4648 |
| Base-32 | Base32Codec |
toBase32, toBase32Bytes |
fromBase32, tryFromBase32 |
RFC-4648 |
| Base-64 | Base64Codec |
toBase64, toBase64Bytes |
fromBase64, tryFromBase64 |
RFC-4648 |
| BigInt | BigIntCodec |
toBigInt |
fromBigInt, tryFromBigInt |
— |
| UTF-8 | UTF8Codec |
toUtf8 |
fromUtf8, tryFromUtf8 |
RFC-3629 |
| Modular Crypt Format | CryptFormat |
toCrypt |
fromCrypt |
PHC string format |
Alphabet variants
Base-16 — toHex(..., upper: true) selects the uppercase alphabet:
Base16Codec.upper—0123456789ABCDEFBase16Codec.lower—0123456789abcdef(default)
Base-32 — pass codec: to pick an alphabet, lower:/padding: to tweak it:
Base32Codec.standard(RFC-4648) —ABCDEFGHIJKLMNOPQRSTUVWXYZ234567(default)Base32Codec.lowercase—abcdefghijklmnopqrstuvwxyz234567Base32Codec.hex/.hexLower— base32hex,0-9A-V/0-9a-vBase32Codec.crockford—0123456789ABCDEFGHJKMNPQRSTVWXYZ(decoding is case-insensitive and acceptsI/i/L/las1andO/oas0)Base32Codec.geohash—0123456789bcdefghjkmnpqrstuvwxyzBase32Codec.z— z-base-32,ybndrfg8ejkmcpqxot1uwisza345h769Base32Codec.wordSafe—23456789CFGHJMPQRVWXcfghjmpqrvwx
Base-64 — pass url: true for URL/filename-safe output, padding: false
to drop =, or a codec::
Base64Codec.standard(RFC-4648) —A-Za-z0-9+/(default)Base64Codec.urlSafe—A-Za-z0-9-_Base64Codec.bcrypt—./A-Za-z0-9
BigInt — endianness is selectable via msbFirst or an explicit codec:
BigIntCodec.msbFirst— treats bytes in big-endian orderBigIntCodec.lsbFirst— treats bytes in little-endian order (default)
Bytes, non-throwing decode, and constant-time compare
- Byte output — the
to<Name>Bytesencoders in the table above return the encoded ASCII as aUint8List, skipping the intermediateString. - Non-throwing decoders — the
tryFrom<Name>decoders returnnullinstead of throwing aFormatExceptionon invalid input. - Constant-time compare —
constantTimeEquals(a, b)compares two byte lists without exiting early on the first mismatch, for verifying MACs and digests. - Low-level building blocks — the generic converters
BitEncoder/BitDecoder,ByteEncoder/ByteDecoder, andAlphabetEncoder/AlphabetDecoderare exported for building custom codecs.
ByteCollector
ByteCollector is the byte container shared with hashlib, holding the output
of a hash or encoding function and re-encoding it on demand:
| Method / getter | Description |
|---|---|
bytes |
Raw bytes as Uint8List |
length |
Number of bytes |
buffer |
The backing ByteBuffer |
hex([upper]) |
Hexadecimal string (optionally uppercase) |
binary() / octal() |
Binary / octal string representation |
base32({upper, padding}) |
Base-32 encoding |
base64({urlSafe, padding}) |
Base-64 encoding |
bigInt({endian}) |
Interprets the bytes as a BigInt |
number([bitLength, endian]) |
Reads an unsigned integer of the given bit-length |
ascii() / utf8() |
Decodes bytes as ASCII / UTF-8 |
to(encoding) |
Decodes bytes with a given dart:convert Encoding |
isEqual(other) |
Constant-time compare against bytes, buffer, or hex text |
Recipes
Base-32 alphabet variants
The same bytes, rendered in each supported Base-32 alphabet — useful for human-friendly identifiers (Crockford), URLs (z-base-32), or geospatial codes (geohash):
import 'package:convertlib/convertlib.dart';
void main() {
final data = toUtf8('Hello, convertlib!');
print('standard : ${toBase32(data)}');
print('lowercase : ${toBase32(data, lower: true)}');
print('no padding : ${toBase32(data, padding: false)}');
print('base32hex : ${toBase32(data, codec: Base32Codec.hex)}');
print('crockford : ${toBase32(data, codec: Base32Codec.crockford)}');
print('z-base-32 : ${toBase32(data, codec: Base32Codec.z)}');
print('geohash : ${toBase32(data, codec: Base32Codec.geohash)}');
print('word-safe : ${toBase32(data, codec: Base32Codec.wordSafe)}');
// Decoding is the exact inverse of encoding
final back = fromBase32(toBase32(data));
print('roundtrip : ${fromUtf8(back)}');
}
URL-safe and unpadded Base-64
For tokens embedded in URLs or JSON, drop the padding and switch to the URL/filename-safe alphabet:
import 'package:convertlib/convertlib.dart';
void main() {
final data = toUtf8('a >> b, c/d');
print('standard : ${toBase64(data)}');
print('url-safe : ${toBase64(data, url: true)}');
print('no padding : ${toBase64(data, url: true, padding: false)}');
print('bcrypt : ${toBase64(data, codec: Base64Codec.bcrypt)}');
// Decode back to the original bytes
final back = fromBase64(toBase64(data, url: true));
print('roundtrip : ${fromUtf8(back)}');
}
BigInt ↔ bytes
Read a byte sequence as an arbitrary-precision integer and back. Combine with
BigInt.toRadixString for a decimal (or any-base) string representation:
import 'package:convertlib/convertlib.dart';
void main() {
var input = [0x3, 0xF1];
print("input => $input");
var encoded = toBigInt(input).toRadixString(10);
print("to decimal => $encoded");
var decoded = fromBigInt(BigInt.parse(encoded, radix: 10));
print("from decimal => $decoded");
}
PHC / Modular Crypt Format
Build and parse password-hash strings such as
$argon2id$v=19$m=65536,t=3,p=4$...$.... The builder Base-64 encodes salt and
hash bytes for you, and fromCrypt gives the fields back as typed accessors:
import 'package:convertlib/convertlib.dart';
void main() {
// Build a PHC / Modular Crypt Format string from its parts.
// `saltBytes` and `hashBytes` are Base-64 encoded (no padding) for you.
final data = CryptData.builder('argon2id')
.version('19')
.param('m', 65536)
.param('t', 3)
.param('p', 4)
.saltBytes(toUtf8('a-16-byte-salt!!'))
.hashBytes(List.generate(32, (i) => i))
.build();
final encoded = toCrypt(data);
print('encoded : $encoded');
// Parse the string back into its structured fields.
final parsed = fromCrypt(encoded);
print('id : ${parsed.id}');
print('version : ${parsed.versionInt()}');
print('m,t,p : ${parsed.getIntParam('m')}, '
'${parsed.getIntParam('t')}, ${parsed.getIntParam('p')}');
print('salt : ${fromUtf8(parsed.saltBytes()!)}');
print('hash : ${toHex(parsed.hashBytes()!)}');
}
Testing and reliability
Codecs are trivial to get subtly wrong. It is not enough to check just the
decode(encode(x)) == x round-trips alone. For example: a dropped bit mask
can produce output that still passes round-trips cleanly. This package is
tested against that failure mode directly:
- Expectations come from outside the code. Every codec is checked against
official vectors (RFC 4648 "foobar", RFC 3629 UTF-8 boundaries, PHC / bcrypt
strings) and differentially against independent reference implementations:
dart:convert,base_codecs, andbase32, so a consistently-wrong codec cannot hide behind a passing round-trip. - Wide input coverage.
- Randomized round-trips at every length from 0 to 99 for each alphabet variant
- Full-range Unicode code points and surrogate-pair handling for UTF-8
- Per-instance assertions of the exact alphabet string each codec emits, which catch miscopied lookup tables.
- Failure paths are asserted, not assumed. Malformed UTF-8, invalid characters, and exhaustive invalid-length sweeps all verify that a typed error is raised (with the exact message and offset), never a silent wrong result.
- Every platform. The suite runs on the Dart VM, Node.js (JavaScript), and Chrome (WASM) in CI across Dart SDK 2.19 and stable on Linux, macOS, and Windows. Special care has been given for the web, where integers behave differently.
- Regressions stay fixed. Real bugs found in the past (a word-safe Base-32
table accidentally copied from z-base-32, a dropped
& 0x3Fmask in the UTF-8 decoder, aByteCollector.isEqualself-comparison) each have a permanent, externally-anchored regression test, and a differential fuzz harness re-checks the codecs against the reference implementations.
Because correctness is anchored to independent references rather than the
package's own agreement with itself, convertlib is safe to rely on in
production. If you do hit a discrepancy, please open an issue and include
your input and the expected output.
Benchmarks
Libraries
- Convertlib : https://pub.dev/packages/convertlib
- Base Codecs : https://pub.dev/packages/base_codecs
- Base32 : https://pub.dev/packages/base32
- Dart Convert : https://api.dart.dev/stable/dart-convert/dart-convert-library.html
UTF-8 throughput is measured per source code point, not per byte.
Encoding
| Codec | Library | 1MB message | 1KB message | 32B message |
|---|---|---|---|---|
| Base-2 | convertlib | ████████████████ 1.86 Gbps 🌟 |
████████████████ 2.11 Gbps 🌟 |
████████████████ 1.94 Gbps 🌟 |
| Base-8 | convertlib | ████████████████ 4.48 Gbps 🌟 |
████████████████ 5.06 Gbps 🌟 |
████████████████ 4.4 Gbps 🌟 |
| Base-16 | convertlib | ████████████████ 4.98 Gbps 🌟 |
████████████████ 5.59 Gbps 🌟 |
████████████████ 4.51 Gbps 🌟 |
| base_codecs | █░░░░░░░░░░░░░░░ 284 Mbps 🔻17.55x |
█░░░░░░░░░░░░░░░ 247 Mbps 🔻22.59x |
█░░░░░░░░░░░░░░░ 247 Mbps 🔻18.24x |
|
| Base-32 | convertlib | ████████████████ 5.44 Gbps 🌟 |
████████████████ 5.99 Gbps 🌟 |
████████████████ 4.53 Gbps 🌟 |
| base_codecs | ██░░░░░░░░░░░░░░ 577 Mbps 🔻9.43x |
█░░░░░░░░░░░░░░░ 421 Mbps 🔻14.23x |
█░░░░░░░░░░░░░░░ 387 Mbps 🔻11.7x |
|
| base32 | █░░░░░░░░░░░░░░░ 631 Kbps 🔻8618.49x |
█░░░░░░░░░░░░░░░ 132 Mbps 🔻45.35x |
█░░░░░░░░░░░░░░░ 137 Mbps 🔻33.02x |
|
| Base-64 | convertlib | ████████████████ 6.04 Gbps 🌟 |
████████████████ 6.68 Gbps 🌟 |
████████████████ 5.48 Gbps 🌟 |
| dart:convert | ███████████████░ 5.61 Gbps 🔻1.08x |
███████████████░ 6.11 Gbps 🔻1.09x |
█████████████░░░ 4.36 Gbps 🔻1.26x |
|
| UTF-8 | convertlib | ████████████████ 3.14 Gbps 🌟 |
████████████████ 3.4 Gbps 🌟 |
████████████████ 2.78 Gbps 🌟 |
| dart:convert | █████████████░░░ 2.6 Gbps 🔻1.21x |
█████████████░░░ 2.73 Gbps 🔻1.25x |
█████████████░░░ 2.22 Gbps 🔻1.25x |
Decoding
| Codec | Library | 1MB message | 1KB message | 32B message |
|---|---|---|---|---|
| Base-2 | convertlib | ████████████████ 1.74 Gbps 🌟 |
████████████████ 1.76 Gbps 🌟 |
████████████████ 1.64 Gbps 🌟 |
| Base-8 | convertlib | ████████████████ 4.15 Gbps 🌟 |
████████████████ 4.22 Gbps 🌟 |
████████████████ 3.68 Gbps 🌟 |
| Base-16 | convertlib | ████████████████ 3.65 Gbps 🌟 |
████████████████ 3.71 Gbps 🌟 |
████████████████ 3.26 Gbps 🌟 |
| base_codecs | ██░░░░░░░░░░░░░░ 406 Mbps 🔻8.98x |
██░░░░░░░░░░░░░░ 410 Mbps 🔻9.04x |
██░░░░░░░░░░░░░░ 396 Mbps 🔻8.24x |
|
| Base-32 | convertlib | ████████████████ 4.03 Gbps 🌟 |
████████████████ 4.09 Gbps 🌟 |
████████████████ 3.38 Gbps 🌟 |
| base_codecs | █░░░░░░░░░░░░░░░ 266 Mbps 🔻15.13x |
█░░░░░░░░░░░░░░░ 267 Mbps 🔻15.33x |
█░░░░░░░░░░░░░░░ 239 Mbps 🔻14.13x |
|
| base32 | █░░░░░░░░░░░░░░░ 176 Mbps 🔻22.84x |
█░░░░░░░░░░░░░░░ 199 Mbps 🔻20.62x |
█░░░░░░░░░░░░░░░ 153 Mbps 🔻22.14x |
|
| Base-64 | convertlib | ████████████████ 4.98 Gbps 🌟 |
████████████████ 5.16 Gbps 🌟 |
████████████████ 4.32 Gbps 🌟 |
| dart:convert | ███████████░░░░░ 3.5 Gbps 🔻1.42x |
███████████░░░░░ 3.6 Gbps 🔻1.43x |
███████████░░░░░ 2.87 Gbps 🔻1.51x |
|
| UTF-8 | convertlib | ███████████████░ 1.65 Gbps |
███████████████░ 1.68 Gbps |
████████████░░░░ 1.28 Gbps |
| dart:convert | ████████████████ 1.75 Gbps 🔺1.06x |
████████████████ 1.81 Gbps 🔺1.08x |
████████████████ 1.72 Gbps 🔺1.35x |
BigInt
| Codec | Library | 4KB message | 256B message | 32B message |
|---|---|---|---|---|
| bytes → BigInt | convertlib | ████████████████ 119 Mbps 🌟 |
████████████████ 115 Mbps 🌟 |
████████████████ 98.21 Mbps 🌟 |
| BigInt → bytes | convertlib | ████████████████ 256 Mbps 🌟 |
████████████████ 250 Mbps 🌟 |
████████████████ 223 Mbps 🌟 |
All benchmarks are done on 36GB Apple M3 Pro using compiled exe
Dart SDK version: 3.12.2 (stable) (Tue Jun 9 01:11:39 2026 -0700) on "macos_arm64"
License
BSD 3-Clause License. See the LICENSE file for details. Issues and contributions are welcome at github.com/bitanon/convertlib.
Libraries
- convertlib
- Implementations of fast and error resilient codecs in pure Dart.