Base codecs (sparkles.base.text.base_codecs)
Binary-to-text codecs for the power-of-two "base" family (RFC 4648 Base16/Base32/Base32hex/Base64/Base64url and alphabet-compatible relatives), plus the Alphabet digit-vocabulary machinery shared with the scalar numeral conversions in readers.d / writers.d.
The code blocks on this page use APIs introduced after the latest registry release; they become runnable (verified) examples once the next version is tagged.
Alphabet
struct Alphabet
{
string digits; // index == symbol value; radix == digits.length
bool caseInsensitive = false; // decode accepts either case
string aliases = ""; // decode-only (aliasChar, canonicalDigit) pairs
char padding = '\0'; // '\0' == none; '=' for RFC base32/base64
ubyte radix() const;
bool isConsistent() const;
}Alphabet is a template value parameter: every per-base behavior — bits per character, group size, padding length, the 256-entry reverse table (makeDecodeTable) — derives from it at compile time. Anonymous literals bind too: encodeBase!(Alphabet(digits: "01")) is a working base-2 codec.
isConsistent is the well-formedness predicate, asserted at compile time wherever an Alphabet is bound: 2–255 digits with no duplicates, a padding character that is not itself a digit, aliases in whole non-digit→digit pairs, and — under caseInsensitive — no digit whose opposite-case twin is also a digit. That last one is the real trap: caseInsensitive: true on a base64-style alphabet, which uses A and a as distinct symbols, would silently clobber half the reverse table. It is now a static assert instead.
Presets
| Preset | Radix | Padding | Notes |
|---|---|---|---|
base16 | 16 | none | RFC 4648 §8; upper-case, decodes either case |
base32 | 32 | = | RFC 4648 §6 |
base32hex | 32 | = | RFC 4648 §7 (encoded order sorts like the bytes) |
base64 | 64 | = | RFC 4648 §4 |
base64url | 64 | = | RFC 4648 §5 (URL- and filename-safe) |
zbase32 | 32 | none | z-base-32 (human-oriented) |
alnum | 36 | none | 0-9a-z, case-insensitive — the digit source for the scalar radix 2–36 conversions |
Encoding and decoding
import sparkles.base.smallbuffer : SmallBuffer;
import sparkles.base.text : encodeBase64, decodeBase64;
import std.string : representation;
SmallBuffer!(char, 64) text;
encodeBase64(text, "Man".representation);
assert(text[] == "TWFu");
SmallBuffer!(ubyte, 64) bytes;
auto r = decodeBase64(bytes, text[]);
assert(r.value == 3 && bytes[] == "Man".representation);The generic spellings are encodeBase!alphabet / decodeBase!alphabet; the presets ship as true aliases (encodeBase16, decodeBase16, encodeBase32, decodeBase32, encodeBase32Hex, decodeBase32Hex, encodeBase64, decodeBase64, encodeBase64Url, decodeBase64Url, encodeZBase32, decodeZBase32).
Each name is one overload set with two forms:
Streaming —
encodeBase!a(w, data)writes chars to any output range;decodeBase!a(w, text)writesubytes and returnsParseExpected!size_t(bytes written). Attributes infer: with aSmallBufferwriter both are@safe pure nothrow @nogc.Fixed-length — when the byte count
Nis compile-time-known:dubyte[32] digest = /* … */; char[encodedLen(base64, 32)] text = void; encodeBase64(digest, text); // unrolled straight-line code ubyte[32] back = void; auto ok = decodeBase64(text, back); // ParseExpected!voidencodedLen(a, n)is the CTFE output-size helper. Fixed-length output is byte-identical to streaming (differential-tested).Note that
encodedLenis not injective for a padding alphabet —encodedLen(base64, 1),…(base64, 2)and…(base64, 3)are all 4 — so the fixed-length decoder's constraint cannot pin the payload to exactlyNbytes. A text that carries fewer ("Zg=="into aubyte[3]) or more ("TWFu"into aubyte[1]) is awidthMismatcherror, not an assert.
Decode strictness
The decoder is strict per RFC 4648 §3.5. Failures are ParseErrors with a machine-readable code and the byte offset:
| Condition | ParseErrorCode | Offset |
|---|---|---|
| character outside the alphabet | unexpectedCharacter | the character |
| data character after padding began | unexpectedCharacter (context "data after padding") | the character |
| final group too short to hold a byte | unexpectedEnd | text.length |
| unused trailing bits not zero | nonCanonicalTrailing | text.length |
| wrong padding character count | paddingMismatch | text.length |
payload is not exactly N bytes | widthMismatch (fixed-length overload only) | src.length |
On failure the streaming decoder's writer — and the fixed-length decoder's dst — may already hold a partial prefix; only the bounds are guaranteed. Callers needing all-or-nothing semantics decode into a throwaway buffer.
See the explanation page for why these three checks exist and what a future lax mode will relax.
Line wrapping
Framing stays out of the kernels; wrap by decorating the writer:
auto lw = lineWrapWriter(w, 76, "\r\n"); // MIME base64
encodeBase64(lw, data);
// PEM: lineWrapWriter(w, 64) — default newline is "\n"Lines are at most width chars (which must be positive); no trailing newline is emitted. The writer takes both a single-character and a bulk put, so a run is forwarded whole rather than element-wise. (This wraps codec output — single-column ASCII. Prose wrapping by terminal cell width lives in sparkles.base.text.wrap.)
Scalar radix conversions (readers.d / writers.d)
The same Alphabet machinery backs the whole-integer numeral conversions, generalized to any radix 2–36 (defaults stay decimal):
ParseExpected!T readInteger(T, ubyte radix = 10)(ref scope const(char)[] s);
void writeInteger(ubyte radix = 10)(ref Writer w, const T val);
void writeIntegerPadded(ubyte radix = 10)(ref Writer w, const T val, size_t minDigits);For radix >= 11, readInteger accepts letter digits in either case; writeInteger emits lower-case (consistent with writeHexByte). Named shorthands are aliases: readHex!T / writeHex, readBinary!T / writeBinary, readOctal!T / writeOctal.
const(char)[] s = "DeadBEEF";
assert(readHex!uint(s).value == 0xDEADBEEF);
SmallBuffer!(char, 16) buf;
writeHex(buf, 0xDEADu);
assert(buf[] == "dead");