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.
- 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.
dependencies:
convertlib: ^3.5.0or run dart pub add convertlib. A single import exposes every codec:
import 'package:convertlib/convertlib.dart';Full API reference: convertlib library.
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.
| Encoding | Class | Functions | Source |
|---|---|---|---|
| Binary (Base-2) | Base2Codec |
toBinary, fromBinary |
— |
| Octal (Base-8) | Base8Codec |
toOctal, fromOctal |
— |
| Hexadecimal (Base-16) | Base16Codec |
toHex, fromHex |
RFC-4648 |
| Base-32 | Base32Codec |
toBase32, fromBase32 |
RFC-4648 |
| Base-64 | Base64Codec |
toBase64, fromBase64 |
RFC-4648 |
| BigInt | BigIntCodec |
toBigInt, fromBigInt |
— |
| UTF-8 | UTF8Codec |
toUtf8, fromUtf8 |
RFC-3629 |
| Modular Crypt Format | CryptFormat |
toCrypt, fromCrypt |
PHC string format |
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—0123456789ABCDEFGHJKMNPQRSTVWXYZBase32Codec.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)
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 |
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)}');
}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)}');
}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");
}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()!)}');
}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.
- 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.
| Codec | Library | 1MB message | 1KB message | 32B message |
|---|---|---|---|---|
| Base-2 | convertlib | ████████████████ 1.78 Gbps 🌟 |
████████████████ 2.11 Gbps 🌟 |
████████████████ 1.99 Gbps 🌟 |
| Base-8 | convertlib | ████████████████ 4.44 Gbps 🌟 |
████████████████ 5.01 Gbps 🌟 |
████████████████ 4.34 Gbps 🌟 |
| Base-16 | convertlib | ████████████████ 4.99 Gbps 🌟 |
████████████████ 5.44 Gbps 🌟 |
████████████████ 4.38 Gbps 🌟 |
| base_codecs | █░░░░░░░░░░░░░░░ 285 Mbps 🔻17.5x |
█░░░░░░░░░░░░░░░ 246 Mbps 🔻22.09x |
█░░░░░░░░░░░░░░░ 248 Mbps 🔻17.67x |
|
| Base-32 | convertlib | ████████████████ 5.45 Gbps 🌟 |
████████████████ 5.95 Gbps 🌟 |
████████████████ 4.46 Gbps 🌟 |
| base_codecs | ██░░░░░░░░░░░░░░ 571 Mbps 🔻9.54x |
█░░░░░░░░░░░░░░░ 417 Mbps 🔻14.27x |
█░░░░░░░░░░░░░░░ 384 Mbps 🔻11.63x |
|
| base32 | █░░░░░░░░░░░░░░░ 622 Kbps 🔻8760.49x |
█░░░░░░░░░░░░░░░ 131 Mbps 🔻45.34x |
█░░░░░░░░░░░░░░░ 137 Mbps 🔻32.67x |
|
| Base-64 | convertlib | ████████████████ 6.1 Gbps 🌟 |
████████████████ 6.49 Gbps 🌟 |
████████████████ 5.33 Gbps 🌟 |
| dart:convert | ███████████████░ 5.64 Gbps 🔻1.08x |
███████████████░ 5.99 Gbps 🔻1.08x |
█████████████░░░ 4.32 Gbps 🔻1.23x |
|
| UTF-8 | convertlib | ████████████████ 3.14 Gbps 🌟 |
████████████████ 3.37 Gbps 🌟 |
████████████████ 2.72 Gbps 🌟 |
| dart:convert | █████████████░░░ 2.55 Gbps 🔻1.23x |
█████████████░░░ 2.67 Gbps 🔻1.26x |
█████████████░░░ 2.22 Gbps 🔻1.23x |
| Codec | Library | 1MB message | 1KB message | 32B message |
|---|---|---|---|---|
| Base-2 | convertlib | ████████████████ 1.73 Gbps 🌟 |
████████████████ 1.72 Gbps 🌟 |
████████████████ 1.62 Gbps 🌟 |
| Base-8 | convertlib | ████████████████ 4.1 Gbps 🌟 |
████████████████ 4.17 Gbps 🌟 |
████████████████ 3.66 Gbps 🌟 |
| Base-16 | convertlib | ████████████████ 3.54 Gbps 🌟 |
████████████████ 3.65 Gbps 🌟 |
████████████████ 3.2 Gbps 🌟 |
| base_codecs | ██░░░░░░░░░░░░░░ 446 Mbps 🔻7.94x |
██░░░░░░░░░░░░░░ 449 Mbps 🔻8.12x |
██░░░░░░░░░░░░░░ 438 Mbps 🔻7.3x |
|
| Base-32 | convertlib | ████████████████ 3.47 Gbps 🌟 |
████████████████ 3.95 Gbps 🌟 |
████████████████ 3.12 Gbps 🌟 |
| base_codecs | █░░░░░░░░░░░░░░░ 285 Mbps 🔻12.19x |
█░░░░░░░░░░░░░░░ 287 Mbps 🔻13.76x |
█░░░░░░░░░░░░░░░ 258 Mbps 🔻12.13x |
|
| base32 | █░░░░░░░░░░░░░░░ 177 Mbps 🔻19.61x |
█░░░░░░░░░░░░░░░ 191 Mbps 🔻20.67x |
█░░░░░░░░░░░░░░░ 152 Mbps 🔻20.52x |
|
| Base-64 | convertlib | ████████████████ 4.92 Gbps 🌟 |
████████████████ 4.95 Gbps 🌟 |
████████████████ 3.87 Gbps 🌟 |
| dart:convert | ████████████░░░░ 3.55 Gbps 🔻1.39x |
███████████░░░░░ 3.47 Gbps 🔻1.43x |
████████████░░░░ 2.99 Gbps 🔻1.29x |
|
| UTF-8 | convertlib | ███████████████░ 1.64 Gbps |
███████████████░ 1.69 Gbps |
████████████░░░░ 1.29 Gbps |
| dart:convert | ████████████████ 1.75 Gbps 🔺1.07x |
████████████████ 1.84 Gbps 🔺1.09x |
████████████████ 1.73 Gbps 🔺1.34x |
| Codec | Library | 4KB message | 256B message | 32B message |
|---|---|---|---|---|
| bytes → BigInt | convertlib | ████████████████ 117 Mbps 🌟 |
████████████████ 113 Mbps 🌟 |
████████████████ 97.33 Mbps 🌟 |
| BigInt → bytes | convertlib | ████████████████ 208 Mbps 🌟 |
████████████████ 201 Mbps 🌟 |
████████████████ 168 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"
BSD 3-Clause License. See the LICENSE file for details. Issues and contributions are welcome at github.com/bitanon/convertlib.