Skip to content

bitanon/convertlib

Repository files navigation

convertlib

package version dart support likes pub points codecov test Ask 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

  • 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/from pair.
  • 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 with hashlib, for one-call re-encoding.

Install

dependencies:
  convertlib: ^3.5.0

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 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

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
  • 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)

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, 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

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

Decoding

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

BigInt

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"

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

License

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Sponsor this project

Packages

 
 
 

Contributors

Languages