feat: Implement base32 (and variations)
2 files changed, 482 insertions(+), 30 deletions(-)

M src/baseenc.c
M src/baseenc.zig
M src/baseenc.c +120 -22
@@ 1,16 1,133 @@ 
 #include <stdint.h>
 #include <janet.h>
 
+enum base_32_scheme {
+  BASE_32_SCHEME_ZERO,
+  BASE_32_SCHEME_RFC4648,
+  BASE_32_SCHEME_Z_BASE_32,
+  BASE_32_SCHEME_CROCKFORD,
+  BASE_32_SCHEME_HEX,
+  BASE_32_SCHEME_GEOHASH,
+  BASE_32_SCHEME_WORDSAFE,
+};
+extern ssize_t base_32_encode(const uint8_t*, size_t, uint8_t*, size_t, enum base_32_scheme);
+extern ssize_t base_32_decode(const uint8_t*, size_t, uint8_t*, size_t, enum base_32_scheme);
 extern ssize_t base_64_encode(const uint8_t*, size_t, uint8_t*, size_t);
+extern ssize_t base_64_decode(const uint8_t*, size_t, uint8_t*, size_t);
+
+static enum base_32_scheme _base32_scheme(Janet v) {
+  if (janet_keyeq(v, "rfc4648")) {
+    return BASE_32_SCHEME_RFC4648;
+  } else if (janet_keyeq(v, "z-base-32")) {
+    return BASE_32_SCHEME_Z_BASE_32;
+  } else if (janet_keyeq(v, "crockford")) {
+    return BASE_32_SCHEME_CROCKFORD;
+  } else if (janet_keyeq(v, "hex")) {
+    return BASE_32_SCHEME_HEX;
+  } else if (janet_keyeq(v, "geohash")) {
+    return BASE_32_SCHEME_GEOHASH;
+  } else if (janet_keyeq(v, "wordsafe")) {
+    return BASE_32_SCHEME_WORDSAFE;
+  } else {
+    return BASE_32_SCHEME_ZERO;
+  }
+}
+
+static Janet Baseenc_base32Decode(int argc, Janet argv[]) {
+  janet_arity(argc, 1, 3);
+  JanetByteView data = janet_getbytes(argv, 0);
+  JanetBuffer *buf;
+  size_t out_len = (data.len + 7) / 8 * 5;
+  if (argc >= 2 && janet_type(argv[1]) != JANET_NIL) {
+    buf = janet_getbuffer(argv, 1);
+    janet_buffer_ensure(buf, out_len, 1);
+  } else {
+    buf = janet_buffer(out_len);
+  }
+
+  enum base_32_scheme scheme = BASE_32_SCHEME_RFC4648;
+  if (argc >= 3) {
+    scheme = _base32_scheme(argv[2]);
+    if (scheme == BASE_32_SCHEME_ZERO) {
+      janet_panicf("invalid base32 scheme: %q", argv[2]);
+    }
+  }
+
+  ssize_t len = base_32_decode(&data.bytes[0], (size_t)data.len,
+    &buf->data[0], (size_t)buf->capacity, scheme);
+  if (len == -1) {
+    janet_panic("buffer overflow");
+  } else if (len == -2) {
+    janet_panic("invalid base32 data");
+  } else if (len == -3) {
+    janet_panic("unknown scheme");
+  }
+  buf->count = (int32_t)len;
+  return janet_wrap_buffer(buf);
+}
+
+static Janet Baseenc_base32Encode(int argc, Janet argv[]) {
+  janet_arity(argc, 1, 3);
+  JanetByteView data = janet_getbytes(argv, 0);
+  JanetBuffer *buf;
+  size_t out_len = (data.len + 4) / 5 * 8;
+  if (argc >= 2 && janet_type(argv[1]) != JANET_NIL) {
+    buf = janet_getbuffer(argv, 1);
+    janet_buffer_ensure(buf, out_len, 1);
+  } else {
+    buf = janet_buffer(out_len);
+  }
+
+  enum base_32_scheme scheme = BASE_32_SCHEME_RFC4648;
+  if (argc >= 3) {
+    scheme = _base32_scheme(argv[2]);
+    if (scheme == BASE_32_SCHEME_ZERO) {
+      janet_panicf("invalid base32 scheme: %q", argv[2]);
+    }
+  }
+
+  ssize_t len = base_32_encode(&data.bytes[0], (size_t)data.len,
+    &buf->data[0], (size_t)buf->capacity, scheme);
+  if (len == -1) {
+    janet_panic("buffer overflow");
+  }
+  buf->count = (int32_t)len;
+  return janet_wrap_buffer(buf);
+}
+
+static Janet Baseenc_base64Decode(int argc, Janet argv[]) {
+  janet_arity(argc, 1, 2);
+  JanetByteView data = janet_getbytes(argv, 0);
+  JanetBuffer *buf;
+  size_t out_len = (data.len + 3) / 4 * 3;
+  if (argc > 1) {
+    buf = janet_getbuffer(argv, 1);
+    janet_buffer_ensure(buf, out_len, 1);
+  } else {
+    buf = janet_buffer(out_len);
+  }
+
+  ssize_t len = base_64_decode(&data.bytes[0], (size_t)data.len,
+    &buf->data[0], (size_t)buf->capacity);
+  if (len == -1) {
+    janet_panic("buffer overflow");
+  } else if (len == -2) {
+    janet_panic("invalid base64 data");
+  }
+  buf->count = (int32_t)len;
+  return janet_wrap_buffer(buf);
+}
 
 static Janet Baseenc_base64Encode(int argc, Janet argv[]) {
   janet_arity(argc, 1, 2);
   JanetByteView data = janet_getbytes(argv, 0);
   JanetBuffer *buf;
+  size_t out_len = (data.len + 2) / 3 * 4;
   if (argc > 1) {
     buf = janet_getbuffer(argv, 1);
+    janet_buffer_ensure(buf, out_len, 1);
   } else {
-    buf = janet_buffer((data.len + 2) / 3 * 4);
+    buf = janet_buffer(out_len);
   }
 
   ssize_t len = base_64_encode(&data.bytes[0], (size_t)data.len,

          
@@ 22,28 139,9 @@ static Janet Baseenc_base64Encode(int ar
   return janet_wrap_buffer(buf);
 }
 
-static Janet Baseenc_base64Decode(int argc, Janet argv[]) {
-  janet_arity(argc, 1, 2);
-  JanetByteView data = janet_getbytes(argv, 0);
-  JanetBuffer *buf;
-  if (argc > 1) {
-    buf = janet_getbuffer(argv, 1);
-  } else {
-    buf = janet_buffer((data.len + 3) / 4 * 3);
-  }
-
-  ssize_t len = base_64_decode(&data.bytes[0], (size_t)data.len,
-    &buf->data[0], (size_t)buf->capacity);
-  if (len == -1) {
-    janet_panic("buffer overflow");
-  } else if (len == -2) {
-    janet_panic("invalid base64 string");
-  }
-  buf->count = (int32_t)len;
-  return janet_wrap_buffer(buf);
-}
-
 static const JanetReg Baseenc_cfuns[] = {
+  { "base32-encode", Baseenc_base32Encode, NULL },
+  { "base32-decode", Baseenc_base32Decode, NULL },
   { "base64-encode", Baseenc_base64Encode, NULL },
   { "base64-decode", Baseenc_base64Decode, NULL },
   { NULL, NULL, NULL },

          
M src/baseenc.zig +362 -8
@@ 1,8 1,221 @@ 
 const std = @import("std");
 
-const EncDecError = error{
-    BufferOverflow,
-    InvalidEncoding,
+const Base32 = struct {
+    const pad = @as(u8, '=');
+    const alphabet_rfc4648: *const [32]u8 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+    const alphabet_z_base_32: *const [32]u8 = "ybndrfg8ejkmcpqxot1uwisza345h769";
+    const alphabet_crockford: *const [32]u8 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
+    const alphabet_hex: *const [32]u8 = "0123456789ABCDEFGHIJKLMNOPQRSTUV";
+    const alphabet_geohash: *const [32]u8 = "0123456789bcdefghjkmnpqrstuvwxyz";
+    const alphabet_wordsafe: *const [32]u8 = "23456789CFGHJMPQRVWXcfghjmpqrvwx";
+
+    const ReverseTransform = enum {
+        None,
+        Lowercase,
+        Uppercase,
+        Crockford,
+    };
+    fn buildReverse(src: *const [32]u8, transform: ReverseTransform) [256]u8 {
+        var rev: [256]u8 = .{255} ** 256;
+        for (src) |x, i| {
+            const i_ = @intCast(u8, i);
+            rev[x] = i_;
+            if (transform == .Lowercase and x >= 'A' and x <= 'Z') {
+                rev[x + ('a' - 'A')] = i_;
+            } else if (transform == .Uppercase and x >= 'a' and x <= 'z') {
+                rev[x - ('a' - 'A')] = i_;
+            } else if (transform == .Crockford) {
+                switch (x) {
+                    '0' => {
+                        rev['O'] = i_;
+                        rev['o'] = i_;
+                    },
+                    '1' => {
+                        rev['I'] = i_;
+                        rev['i'] = i_;
+                        rev['L'] = i_;
+                        rev['l'] = i_;
+                        rev['1'] = i_;
+                    },
+                    else => {},
+                }
+                if (x >= 'A' and x <= 'Z') {
+                    rev[x + ('a' - 'A')] = i_;
+                }
+            }
+        }
+        rev[pad] = 0;
+        return rev;
+    }
+    const dict_rfc4648 = buildReverse(alphabet_rfc4648, .Lowercase);
+    const dict_z_base_32 = buildReverse(alphabet_z_base_32, .Uppercase);
+    const dict_crockford = buildReverse(alphabet_crockford, .Crockford);
+    const dict_hex = buildReverse(alphabet_hex, .Lowercase);
+    const dict_geohash = buildReverse(alphabet_geohash, .Uppercase);
+    const dict_wordsafe = buildReverse(alphabet_wordsafe, .None);
+
+    const Scheme = enum { rfc4648, z_base_32, crockford, hex, geohash, wordsafe };
+
+    inline fn encodeMask(alphabet: *const [32]u8, out: *[8]u8, in: *const [5]u8) void {
+        // 0         1         2         3
+        // 0123456789012345678901234567890123456789
+        // <===0==><===1==><===2==><===3==><===4==>
+        // <=a=><=b=><=c=><=d=><=e=><=f=><=g=><=h=>
+        const a = in[0] >> 3;
+        const b = (in[0] << 2 & 0b11100) | (in[1] >> 6 & 0b00011);
+        const c = in[1] >> 1 & 0b11111;
+        const d = (in[1] << 4 & 0b10000) | (in[2] >> 4 & 0b01111);
+        const e = (in[2] << 1 & 0b11110) | (in[3] >> 7 & 0b00001);
+        const f = in[3] >> 2 & 0b11111;
+        const g = (in[3] << 3 & 0b11000) | (in[4] >> 5 & 0b00111);
+        const h = in[4] & 0b11111;
+        out[0] = alphabet[a];
+        out[1] = alphabet[b];
+        out[2] = alphabet[c];
+        out[3] = alphabet[d];
+        out[4] = alphabet[e];
+        out[5] = alphabet[f];
+        out[6] = alphabet[g];
+        out[7] = alphabet[h];
+    }
+    /// How many characters with partial data will be emitted for a message with
+    /// length `i` mod 5.
+    const incomplete: [5]usize = .{ 0, 2, 4, 5, 7 };
+    fn encode(in: []const u8, out: []u8, scheme: Scheme) !usize {
+        const uses_padding = switch (scheme) {
+            // Slightly opinionated, pardon.
+            .rfc4648, .hex => true,
+            else => false,
+        };
+        const alphabet = switch (scheme) {
+            .rfc4648 => alphabet_rfc4648,
+            .z_base_32 => alphabet_z_base_32,
+            .crockford => alphabet_crockford,
+            .hex => alphabet_hex,
+            .geohash => alphabet_geohash,
+            .wordsafe => alphabet_wordsafe,
+        };
+
+        const odd = @mod(in.len, 5);
+        var enc_len = if (uses_padding)
+            @divTrunc(in.len + 4, 5) * 8
+        else
+            @divTrunc(in.len, 5) * 8 + incomplete[odd];
+        if (out.len < enc_len) return error.BufferOverflow;
+
+        const limit = in.len - odd;
+        var i = @as(usize, 0);
+        var j = @as(usize, 0);
+        while (i < limit) : ({
+            i += 5;
+            j += 8;
+        }) {
+            encodeMask(alphabet, out[j..][0..8], in[i..][0..5]);
+        }
+
+        const partials = incomplete[odd];
+        var partial_in: [5]u8 = .{0} ** 5;
+        var tmp: [8]u8 = undefined;
+        std.mem.copy(u8, partial_in[0..odd], in[i..(i + odd)]);
+        encodeMask(alphabet, &tmp, &partial_in);
+        if (uses_padding and odd > 0) {
+            std.mem.set(u8, tmp[partials..], pad);
+            std.mem.copy(u8, out[j..], &tmp);
+            j += tmp.len;
+        } else {
+            std.mem.copy(u8, out[j..], tmp[0..partials]);
+            j += partials;
+        }
+        return j;
+    }
+
+    inline fn decodeUnmask(out: *[5]u8, in: *[8]u8) void {
+        // 0         1         2         3
+        // 0123456789012345678901234567890123456789
+        // <===a==><===b==><===c==><===d==><===e==>
+        // <=0=><=1=><=2=><=3=><=4=><=5=><=6=><=7=>
+        const a = in[0] << 3 | in[1] >> 2;
+        const b = (in[1] << 6 & 0b1100_0000) | (in[2] << 1) | (in[3] >> 4 & 1);
+        const c = (in[3] << 4 & 0b1111_0000) | (in[4] >> 1 & 0b0000_1111);
+        const d = (in[4] << 7 & 0b1000_0000) | (in[5] << 2) | (in[6] >> 3 & 0b111);
+        const e = (in[6] << 5) | in[7];
+        out[0] = a;
+        out[1] = b;
+        out[2] = c;
+        out[3] = d;
+        out[4] = e;
+    }
+
+    // Given `i` odd characters, how many complete bytes do they form.
+    const decode_incomplete: [8]u8 = .{ 0, 0, 1, 1, 2, 3, 3, 4 };
+
+    fn decode(in: []const u8, out: []u8, scheme: Scheme) !usize {
+        if (in.len == 0) {
+            return 0;
+        }
+        var in_len = in.len;
+        var padding = @as(usize, 0);
+        var i = in.len - 1;
+        while (i >= 0) : (i -= 1) {
+            if (in[i] == pad) {
+                padding += 1;
+            } else {
+                break;
+            }
+        }
+        in_len -= padding;
+        const odd = @mod(in_len, 8);
+
+        var dec_len = @divTrunc(in_len, 8) * 5;
+        dec_len += decode_incomplete[odd];
+        if (out.len < dec_len) return error.BufferOverflow;
+
+        const dict = switch (scheme) {
+            .rfc4648 => dict_rfc4648,
+            .z_base_32 => dict_z_base_32,
+            .crockford => dict_crockford,
+            .hex => dict_hex,
+            .geohash => dict_geohash,
+            .wordsafe => dict_wordsafe,
+        };
+
+        var block: [8]u8 = undefined;
+        const limit = in_len - odd;
+        i = 0;
+        var j = @as(usize, 0);
+        while (i < limit) : ({
+            i += 8;
+            j += 5;
+        }) {
+            std.mem.copy(u8, block[0..], in[i..][0..8]);
+            var set = @as(u8, 0);
+            for (block) |*x| {
+                x.* = dict[x.*];
+                set |= x.*;
+            }
+            if (set & 0x80 != 0) return error.InvalidEncoding;
+
+            decodeUnmask(out[j..][0..5], &block);
+        }
+
+        std.mem.set(u8, block[0..], pad);
+        std.mem.copy(u8, block[0..], in[i..(i + odd)]);
+
+        var set = @as(u8, 0);
+        for (block[0..odd]) |*x| {
+            x.* = dict[x.*];
+            set |= x.*;
+        }
+        if (set & 0x80 != 0) return error.InvalidEncoding;
+
+        var out_partial: [5]u8 = undefined;
+        decodeUnmask(out_partial[0..5], &block);
+        //std.debug.print("[decode] {s} (partial: {
+        std.mem.copy(u8, out[j..], out_partial[0..decode_incomplete[odd]]);
+        j += decode_incomplete[odd];
+
+        return j;
+    }
 };
 
 const Base64 = struct {

          
@@ 29,7 242,7 @@ const Base64 = struct {
         out[2] = alphabet[(b << 2 & 0b111100) | (c >> 6 & 0b000011)];
         out[3] = alphabet[c & 0b111111];
     }
-    fn encode(in: []const u8, out: []u8) EncDecError!usize {
+    fn encode(in: []const u8, out: []u8) !usize {
         // Pre-check sizes to avoid unnecessary branching.
         const sym = @divTrunc(in.len + 2, 3) * 4;
         if (out.len < sym) {

          
@@ 77,7 290,7 @@ const Base64 = struct {
         tgt[1] = (b << 4 & 0b1111_0000) | (c >> 2 & 0b0000_1111);
         tgt[2] = (c << 6 & 0b1100_0000) | d;
     }
-    fn decode(in: []const u8, out: []u8) EncDecError!usize {
+    fn decode(in: []const u8, out: []u8) !usize {
         if (in.len == 0) {
             return 0;
         }

          
@@ 166,13 379,66 @@ const Base64 = struct {
     }
 };
 
+const Base32Scheme = enum(c_uint) {
+    zero,
+    rfc4648,
+    z_base_32,
+    crockford,
+    hex,
+    geohash,
+    wordsafe,
+    _,
+
+    fn fromInternal(scheme: Base32.Scheme) Base32Scheme {
+        return switch (scheme) {
+            .rfc4648 => .rfc4648,
+            .z_base_32 => .z_base_32,
+            .crockford => .crockford,
+            .hex => .hex,
+            .geohash => .geohash,
+            .wordsafe => .wordsafe,
+        };
+    }
+    fn toInternal(self: Base32Scheme) ?Base32.Scheme {
+        return switch (self) {
+            .rfc4648 => .rfc4648,
+            .z_base_32 => .z_base_32,
+            .crockford => .crockford,
+            .hex => .hex,
+            .geohash => .geohash,
+            .wordsafe => .wordsafe,
+            else => null,
+        };
+    }
+};
+
+export fn base_32_encode(in_buf: [*]const u8, in_len: usize, out_buf: [*]u8, out_len: usize, scheme: Base32Scheme) isize {
+    const in = in_buf[0..in_len];
+    const out = out_buf[0..out_len];
+    const sch = scheme.toInternal() orelse return -3;
+
+    return @intCast(isize, Base32.encode(in, out, sch) catch |err| switch (err) {
+        error.BufferOverflow => return -1,
+    });
+}
+
+export fn base_32_decode(in_buf: [*]const u8, in_len: usize, out_buf: [*]u8, out_len: usize, scheme: Base32Scheme) isize {
+    const in = in_buf[0..in_len];
+    const out = out_buf[0..out_len];
+    const sch = scheme.toInternal() orelse return -3;
+
+    return @intCast(isize, Base32.decode(in, out, sch) catch |err| switch (err) {
+        error.BufferOverflow => return -1,
+        error.InvalidEncoding => return -2,
+    });
+}
+
 export fn base_64_encode(in_buf: [*]const u8, in_len: usize, out_buf: [*]u8, out_len: usize) isize {
     const in = in_buf[0..in_len];
     const out = out_buf[0..out_len];
 
     return @intCast(isize, Base64.encode(in, out) catch |err| switch (err) {
         error.BufferOverflow => return -1,
-        error.InvalidEncoding => unreachable,
     });
 }
 

          
@@ 211,6 477,94 @@ fn fromHex(comptime src: []const u8) []c
 }
 const alloc = std.testing.allocator;
 const expectEqualSlices = std.testing.expectEqualSlices;
+
+const discard_medicine = "Discard medicine more than two years old.";
+
+test "base32 encode" {
+    const TestCase = struct {
+        scheme: Base32Scheme,
+        vector: []const u8,
+        expected: []const u8,
+
+        const Self = @This();
+        fn tc(scheme: Base32Scheme, vector: []const u8, expected: []const u8) Self {
+            return .{
+                .scheme = scheme,
+                .vector = vector,
+                .expected = expected,
+            };
+        }
+    };
+    const tc = TestCase.tc;
+
+    for ([_]TestCase{
+        tc(.rfc4648, "", ""),
+        tc(.rfc4648, &.{0}, "AA======"),
+        tc(.rfc4648, &.{ 0, 0 }, "AAAA===="),
+        tc(.rfc4648, &.{ 0, 0, 0 }, "AAAAA==="),
+        tc(.rfc4648, &.{ 0, 0, 0, 0 }, "AAAAAAA="),
+        tc(.rfc4648, &.{ 0, 0, 0, 0, 0 }, "AAAAAAAA"),
+        tc(.rfc4648, "hello", "NBSWY3DP"),
+        tc(.rfc4648, discard_medicine, "IRUXGY3BOJSCA3LFMRUWG2LOMUQG233SMUQHI2DBNYQHI53PEB4WKYLSOMQG63DEFY======"),
+        tc(.z_base_32, discard_medicine, "etwzga5bqj1ny5mfctwsg4mqcwog4551cwo8e4dbpao8e75xrbhskam1qcog65drfa"),
+        tc(.crockford, discard_medicine, "8HMQ6RV1E9J20VB5CHMP6TBECMG6TVVJCMG78T31DRG78XVF41WPARBJECG6YV345R"),
+        tc(.hex, discard_medicine, "8HKN6OR1E9I20RB5CHKM6QBECKG6QRRICKG78Q31DOG78TRF41SMAOBIECG6UR345O======"),
+        tc(.geohash, discard_medicine, "8jnr6sv1f9k20vc5djnq6ucfdnh6uvvkdnh78u31esh78xvg41wqbsckfdh6yv345s"),
+        tc(.wordsafe, discard_medicine, "CVch8jq3PFW42qH7JVcg8pHPJcR8pqqWJcR9Cp53MjR9CvqQ63rgGjHWPJR8wq567j"),
+    }) |c| {
+        const actual = try alloc.alloc(u8, @maximum(c.expected.len, 1));
+        defer alloc.free(actual);
+        const len = base_32_encode(c.vector.ptr, c.vector.len, actual.ptr, actual.len, c.scheme);
+        if (len < 0) {
+            std.debug.print("base32 encode: buffer overflow `{s}` ({})\n", .{ c.vector, c.scheme });
+            return error.BufferOverflow;
+        }
+        try expectEqualSlices(u8, c.expected, actual[0..@intCast(usize, len)]);
+    }
+}
+
+test "base32 decode" {
+    const TestCase = struct {
+        scheme: Base32.Scheme,
+        vector: []const u8,
+        expected: []const u8,
+        const Self = @This();
+        fn tc(scheme: Base32.Scheme, vector: []const u8, expected: []const u8) Self {
+            return .{ .scheme = scheme, .vector = vector, .expected = expected };
+        }
+    };
+    const tc = TestCase.tc;
+
+    for ([_]TestCase{
+        tc(.rfc4648, "", ""),
+        tc(.rfc4648, "AA======", &.{0}),
+        tc(.rfc4648, "AAAA====", &.{ 0, 0 }),
+        tc(.rfc4648, "AAAAA===", &.{ 0, 0, 0 }),
+        tc(.rfc4648, "AAAAAAA=", &.{ 0, 0, 0, 0 }),
+        tc(.rfc4648, "AAAAAAAA", &(.{0} ** 5)),
+        tc(.rfc4648, "NBSWY3DP", "hello"),
+        tc(.rfc4648, "nbswy3dp", "hello"),
+        tc(.rfc4648, "NBXWYYI=", "hola"),
+        tc(.rfc4648, "NBXWYYI", "hola"),
+        tc(.rfc4648, "nbxwyyi", "hola"),
+        tc(.crockford, "D1QPRR8", "hola"),
+        tc(.crockford, "DIQPRR8", "hola"),
+        tc(.crockford, "DLQPRR8", "hola"),
+        tc(.crockford, "8i04og2O", "@@@@@"),
+        tc(.rfc4648, "IRUXGY3BOJSCA3LFMRUWG2LOMUQG233SMUQHI2DBNYQHI53PEB4WKYLSOMQG63DEFY======", discard_medicine),
+        tc(.z_base_32, "etwzga5bqj1ny5mfctwsg4mqcwog4551cwo8e4dbpao8e75xrbhskam1qcog65drfa", discard_medicine),
+        tc(.crockford, "8HMQ6RV1E9J20VB5CHMP6TBECMG6TVVJCMG78T31DRG78XVF41WPARBJECG6YV345R", discard_medicine),
+        tc(.hex, "8HKN6OR1E9I20RB5CHKM6QBECKG6QRRICKG78Q31DOG78TRF41SMAOBIECG6UR345O======", discard_medicine),
+        tc(.geohash, "8jnr6sv1f9k20vc5djnq6ucfdnh6uvvkdnh78u31esh78xvg41wqbsckfdh6yv345s", discard_medicine),
+        tc(.wordsafe, "CVch8jq3PFW42qH7JVcg8pHPJcR8pqqWJcR9Cp53MjR9CvqQ63rgGjHWPJR8wq567j", discard_medicine),
+    }) |c| {
+        const actual = try alloc.alloc(u8, @maximum(c.expected.len, 1));
+        defer alloc.free(actual);
+        const len = try Base32.decode(c.vector, actual, c.scheme);
+        try expectEqualSlices(u8, c.expected, actual[0..len]);
+    }
+}
+
 test "base64 encode" {
     for ([_][2][]const u8{
         .{ "", "" },

          
@@ 218,7 572,7 @@ test "base64 encode" {
         .{ ([_]u8{0})[0..] ** 2, "AAA=" },
         .{ ([_]u8{0})[0..] ** 3, "AAAA" },
         .{ "hey", "aGV5" },
-        .{ "Discard medicine more than two years old.", "RGlzY2FyZCBtZWRpY2luZSBtb3JlIHRoYW4gdHdvIHllYXJzIG9sZC4=" },
+        .{ discard_medicine, "RGlzY2FyZCBtZWRpY2luZSBtb3JlIHRoYW4gdHdvIHllYXJzIG9sZC4=" },
     }) |c| {
         const vector = c[0];
         const expected = c[1];

          
@@ 246,7 600,7 @@ test "base64 decode" {
         .{ "AAAAAAA=", &.{ 0, 0, 0, 0, 0 } },
         .{ "AAAAAAAA", &.{ 0, 0, 0, 0, 0, 0 } },
         .{ "aGV5", "hey" },
-        .{ "RGlzY2FyZCBtZWRpY2luZSBtb3JlIHRoYW4gdHdvIHllYXJzIG9sZC4=", "Discard medicine more than two years old." },
+        .{ "RGlzY2FyZCBtZWRpY2luZSBtb3JlIHRoYW4gdHdvIHllYXJzIG9sZC4=", discard_medicine },
     }) |c| {
         const vector = c[0];
         const expected = c[1];