Skip to content

Repository files navigation

convertlib

Fast, error-resilient, zero-dependency codecs for Dart.

Binary · Octal · Hex · Base-32 · Base-64 · BigInt · UTF-8 · PHC / Modular Crypt

package versiondart supportlikespub pointscodecovtestAsk DeepWiki

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:

convertlibhashlibcipherlib

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

FeatureWhat you get
🌍Runs on every platformPure Dart with no native code or FFI — the same library works on the VM, Flutter (Android, iOS, Windows, macOS, Linux), and the web (dart2js and dart2wasm).
📦Zero dependenciesNothing is pulled into your dependency tree.
🔡Broad coverageBase-2, Base-8, Base-16, Base-32, Base-64, BigInt, UTF-8, and the PHC / Modular Crypt Format, each with a symmetrical to/from pair.
🎛️Many alphabet variantsRFC 4648 standard, base32hex, Crockford, z-base-32, geohash, word-safe, URL/filename-safe Base-64, and bcrypt.
✳️Optional paddingEncode and decode with or without = padding.
🛡️Error resilientDecoders accept case and alphabet variations where the encoding allows it, and raise typed errors on genuinely invalid input.
🧩Convenient outputResults integrate with ByteCollector, the digest container shared with hashlib, for one-call re-encoding.
⚙️Bytes or strings, throwing or notEvery encoder has a to<Name>Bytes twin returning a Uint8List, every decoder a non-throwing tryFrom<Name> returning null, plus a top-level constantTimeEquals.

📦 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';
voidmain() {
final data =toUtf8('convertlib');
// Encode the same bytes in a few common waysfinal 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 inverseprint('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

EncodingClassEncodeDecodeSource
Binary (Base-2)Base2CodectoBinary, toBinaryBytesfromBinary, tryFromBinary
Octal (Base-8)Base8CodectoOctal, toOctalBytesfromOctal, tryFromOctal
Hexadecimal (Base-16)Base16CodectoHex, toHexBytesfromHex, tryFromHexRFC-4648
Base-32Base32CodectoBase32, toBase32BytesfromBase32, tryFromBase32RFC-4648
Base-64Base64CodectoBase64, toBase64BytesfromBase64, tryFromBase64RFC-4648
BigIntBigIntCodectoBigIntfromBigInt, tryFromBigInt
UTF-8UTF8CodectoUtf8fromUtf8, tryFromUtf8RFC-3629
Modular Crypt FormatCryptFormattoCryptfromCryptPHC string format

🔤 Alphabet variants

Base-16toHex(..., upper: true) selects the uppercase alphabet:

  • Base16Codec.upper0123456789ABCDEF
  • Base16Codec.lower0123456789abcdef (default)

Base-32 — pass codec: to pick an alphabet, lower:/padding: to tweak it:

  • Base32Codec.standard (RFC-4648) — ABCDEFGHIJKLMNOPQRSTUVWXYZ234567 (default)
  • Base32Codec.lowercaseabcdefghijklmnopqrstuvwxyz234567
  • Base32Codec.hex / .hexLower — base32hex, 0-9A-V / 0-9a-v
  • Base32Codec.crockford0123456789ABCDEFGHJKMNPQRSTVWXYZ (decoding is case-insensitive and accepts I/i/L/l as 1 and O/o as 0)
  • Base32Codec.geohash0123456789bcdefghjkmnpqrstuvwxyz
  • Base32Codec.z — z-base-32, ybndrfg8ejkmcpqxot1uwisza345h769
  • Base32Codec.wordSafe23456789CFGHJMPQRVWXcfghjmpqrvwx

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.urlSafeA-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 order
  • BigIntCodec.lsbFirst — treats bytes in little-endian order (default)

🧰 Bytes, non-throwing decode, and constant-time compare

  • Byte output — the to<Name>Bytes encoders in the table above return the encoded ASCII as a Uint8List, skipping the intermediate String.
  • Non-throwing decoders — the tryFrom<Name> decoders return null instead of throwing a FormatException on invalid input.
  • Constant-time compareconstantTimeEquals(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, and AlphabetEncoder/ AlphabetDecoder are 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 / getterDescription
bytesRaw bytes as Uint8List
lengthNumber of bytes
bufferThe 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:convertEncoding
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';
voidmain() {
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 encodingfinal 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';
voidmain() {
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 bytesfinal 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';
voidmain() {
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';
voidmain() {
// 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, and base32, 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 & 0x3F mask in the UTF-8 decoder, a ByteCollector.isEqual self-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

UTF-8 throughput is measured per source code point, not per byte.

Encoding

CodecLibrary1MB message1KB message32B message
Base-2convertlib████████████████
1.86 Gbps 🌟
████████████████
2.11 Gbps 🌟
████████████████
1.94 Gbps 🌟
Base-8convertlib████████████████
4.48 Gbps 🌟
████████████████
5.06 Gbps 🌟
████████████████
4.4 Gbps 🌟
Base-16convertlib████████████████
4.98 Gbps 🌟
████████████████
5.59 Gbps 🌟
████████████████
4.51 Gbps 🌟
base_codecs█░░░░░░░░░░░░░░░
284 Mbps 🔻17.55x
█░░░░░░░░░░░░░░░
247 Mbps 🔻22.59x
█░░░░░░░░░░░░░░░
247 Mbps 🔻18.24x
Base-32convertlib████████████████
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-64convertlib████████████████
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-8convertlib████████████████
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

CodecLibrary1MB message1KB message32B message
Base-2convertlib████████████████
1.74 Gbps 🌟
████████████████
1.76 Gbps 🌟
████████████████
1.64 Gbps 🌟
Base-8convertlib████████████████
4.15 Gbps 🌟
████████████████
4.22 Gbps 🌟
████████████████
3.68 Gbps 🌟
Base-16convertlib████████████████
3.65 Gbps 🌟
████████████████
3.71 Gbps 🌟
████████████████
3.26 Gbps 🌟
base_codecs██░░░░░░░░░░░░░░
406 Mbps 🔻8.98x
██░░░░░░░░░░░░░░
410 Mbps 🔻9.04x
██░░░░░░░░░░░░░░
396 Mbps 🔻8.24x
Base-32convertlib████████████████
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-64convertlib████████████████
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-8convertlib███████████████░
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

CodecLibrary4KB message256B message32B message
bytes → BigIntconvertlib████████████████
119 Mbps 🌟
████████████████
115 Mbps 🌟
████████████████
98.21 Mbps 🌟
BigInt → bytesconvertlib████████████████
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.

About

Fast and error resilient codecs in pure Dart

Resources

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages