moonbase

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"
bW9vbmJhc2U= bW9vbmJhc2U

To go back, decode returns the bytes and raises Malformed on bad input.

@base64.decode("bW9vbmJhc2U=")   // b"moonbase"
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 text

decode / 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"
6d6f6f6e62617365 6D6F6F6E62617365 DLNMURJ2C5PMA=== DNQPYVK2C5SPA zeTugvQTs? 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"
bW9vbmJhc2U= b"moon" 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")   // 123456789
21i3v9 8M0kX 123456789

Anything 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)                            // 123456789
75bcd15 123456789

Self-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"
zKJfFLz7PRba bnvxw63tcmfzwk Some(Base58Btc) 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"
moonbase1d4hk7mnzv9ek2s84pra moonbase1d4hk7mnzv9ek29m9dxl 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"
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)
16 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") }
bad char * at 2

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"
ab:cd:ef b"\xab\xcd\xef" 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.