Guide
From install to a custom base-N system. Every snippet compiles on its own and its output is the real thing.
Install
Add the package, then import only the encodings you need. An application that uses base64 does not link base58's big-integer core.
// moon.mod
import {
"moonbitstack/moonbase@0.4.1",
}
// moon.pkg
import {
"moonbitstack/moonbase/base64",
}Your first encode
encode takes a BytesView and returns a String.
let data = b"moonbase"
@base64.encode(data[:]) // "bW9vbmJhc2U="
@base64.encode(data[:], kind=Url, padding=false) // "bW9vbmJhc2U"To go back, decode returns the bytes and raises Malformed on bad
input.
@base64.decode("bW9vbmJhc2U=") // b"moonbase"The five faces
All thirteen packages share the same five functions. They differ only in their alphabet, their bit width, and whether they read the payload as bits or as a number.
encode / encode_bytes
Wire formats — an HTTP header, a gRPC metadata value — hold bytes. encode_bytes
hands you the ASCII bytes directly, with no String on the way.
@base64.encode_bytes(data[:]) // ASCII bytes, same textdecode / decode_bytes
The string and bytes forms are identical except for the input type, for callers who never had a string.
@base64.decode_bytes(@base64.encode_bytes(data[:]))decode_lossy
Skips anything outside the alphabet. Never fails — what a PEM body full of newlines needs. See Lossy decode.
Variants
Variants are a named argument, not a new function. base16 switches case, base32 has four alphabets, base58 three, base85 three, and base64 can switch alphabet or drop padding.
@base16.encode(data[:]) // "6d6f6f6e62617365"
@base16.encode(data[:], kind=Upper) // "6D6F6F6E62617365"
@base32.encode(data[:], kind=Hex) // "DLNMURJ2C5PMA==="
@base32.encode(data[:], kind=Crockford) // "DNQPYVK2C5SPA"
@base85.encode(data[:], kind=Z85) // "zeTugvQTs?"
@base64.encode(data[:], padding=false) // "bW9vbmJhc2U"The two base32 alphabets that exist for people rather than for machines are
Crockford — which leaves out I, L, O and
U, and reads those back as the digits they look like — and Z,
which puts the letters a word is likely to need at the cheap end.
Streaming
The bit-packing families — base2, base8, base16, base32, base45, base64, base85 — have a
sink() and a source(). Feed them whatever chunks the transport
hands you; the bytes that come out are the bytes one call would have produced.
let sink = @base64.sink()
sink.write(b"moon"[:])
sink.write(b"base"[:])
sink.finish() // "bW9vbmJhc2U="
let source = @base64.source()
source.write("bW9vbm") // six characters, four bytes and four bits
source.drain() // b"moon" — the leftover bits stay behind
source.write("Jhc2U=")
source.finish() // b"base"drain hands over what is already decided and keeps the rest; finish
flushes the tail. A failure names the index in the whole input, counted across every chunk,
so splitting the input never moves an error somewhere the caller cannot find it.
The big-integer families — base10, base36, base58, base62 — have no streaming face on purpose: their first digit depends on the last byte.
Whole numbers
base36, base58 and base62 add encode_int / decode_int for
unsigned integers, where leading zeros have no meaning.
@base36.encode_int(123456789) // "21i3v9"
@base62.encode_int(123456789) // "8M0kX"
@base36.decode_int("21i3v9") // 123456789Anything larger goes through BigInt, on any alphabet — including the ones
whose own packages only speak bytes.
let hex = @moonbase.Alphabet::of("0123456789abcdef")
@moonbase.encode_big(@bigint.BigInt::from_int(123456789), hex) // "75bcd15"
@moonbase.decode_big("75bcd15", hex) // 123456789Self-identifying strings
multibase puts one character in front of any of the encodings above, so the
string says how to read itself. detect answers without decoding, and the
failure index of a bad body counts from the start of the whole string, prefix included.
@multibase.encode(data[:], base=Base58Btc) // "zKJfFLz7PRba"
@multibase.encode(data[:], base=Base32) // "bnvxw63tcmfzwk"
@multibase.detect("zKJfFLz7PRba") // Some(Base58Btc)
@multibase.decode("zKJfFLz7PRba") // b"moonbase"bech32 is the other way round: the prefix is part of the checksum, so a typo
in it is as visible as a typo in the data. The payload is a run of five-bit groups, which
is why there are two entry points — encode for a payload that fills whole
bytes, and encode_words for one that does not, such as a witness program.
@bech32.encode("moonbase", data[:])
// "moonbase1d4hk7mnzv9ek2s84pra"
@bech32.encode("moonbase", data[:], kind=Bech32m)
// "moonbase1d4hk7mnzv9ek29m9dxl"
@bech32.decode_words("moonbase1d4hk7mnzv9ek2s84pra").hrp
// "moonbase"The two kinds never overlap: a string valid as Bech32 fails as
Bech32m and the other way round, which is how a wallet knows it is looking at
the address form it supports.
Custom alphabets
The positional core is public. Any base-N system is two lines: build an
Alphabet, then call the core.
let crockford = @moonbase.Alphabet::new(
"0123456789ABCDEFGHJKMNPQRSTVWXYZ", // no I, L, O, U
)
@moonbase.encode(b"moonbase"[:], crockford) // "6TVVFDSH62WV5"Alphabet::new raises BadAlphabet on a wrong alphabet — a repeated
character, a non-ASCII character, or fewer than two digits — because that usually comes
from configuration. Alphabet::of is the same thing for a literal in your own
source, and aborts instead.
let a = @moonbase.Alphabet::of("0123456789abcdef")
a.base() // 16
a.char_of(15) // 'f'
a.value_of('f') // Some(15)Errors
A bad input names where it broke. There are four reasons, each carrying the index.
try @base64.decode("Zm*vYg==") catch {
failure => match failure {
Bad(at~, char~) => println("bad char " + char.to_string() + " at " + at.to_string())
Truncated(at~) => println("ends mid-group at " + at.to_string())
Padding(at~) => println("stray '=' at " + at.to_string())
Checksum(at~) => println("checksum off at " + at.to_string())
}
} noraise { _ => println("decoded") }Want only a yes-or-no? try? gives you a Result instead of raising.
let r = try? @base64.decode(s) // Result[Bytes, Malformed]Lossy decode
A certificate fingerprint is hexadecimal with colons. encode_colons writes
it; decode_lossy reads it back without being told about the colons.
let print = @base16.encode_colons(b"\xab\xcd\xef"[:])
print // "ab:cd:ef"
@base16.decode_lossy(print) // b"\xab\xcd\xef"
@base16.encode(@base16.decode_lossy(print)[:]) // "abcdef"The same lossy reader swallows the newlines in a PEM body and the spaces in a pasted token. Every package has it.
Strict decode
The default decoder reads Zg==, Zg and Zh as the
same byte. A format that signs or compares the text needs one spelling per value, and base32
and base64 take two arguments for it: padding requires (true) or
forbids (false) the trailing =, and strict refuses an
incomplete run of = and spare bits that are not zero (RFC 4648 §3.5).
@base64.decode("Zg==", padding=true, strict=true) // b"f"
@base64.decode("Zh", padding=false, strict=true) // Padding(at=1)
@base64.decode("Zg=", strict=true) // Padding(at=2)The four combinations of the two match Go's StdEncoding,
RawStdEncoding and their Strict() forms, input for input.