Current section
Files
Jump to
Current section
Files
src/thirtytwo.erl
-module(thirtytwo).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/thirtytwo.gleam").
-export([crockford_decode/2, z_base_32_decode/1, geohash_decode/1, encode/2, hex_encode/2, crockford_encode/2, z_base_32_encode/1, geohash_encode/1, decode/1, hex_decode/1]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Base32 encoding and decoding for Gleam, targeting both Erlang and JavaScript.\n"
"\n"
" Supports five base32 variants:\n"
"\n"
" - **RFC 4648** (`encode` / `decode`) — the standard alphabet with optional\n"
" padding and case-insensitive decoding.\n"
" - **Extended Hex** (`hex_encode` / `hex_decode`) — preserves sort order of\n"
" the underlying binary data (RFC 4648 §7).\n"
" - **Crockford** (`crockford_encode` / `crockford_decode`) — omits I, L, O, U\n"
" to reduce ambiguity; supports hyphens, character aliases, and an optional\n"
" mod-37 check digit.\n"
" - **z-base-32** (`z_base_32_encode` / `z_base_32_decode`) — human-oriented\n"
" lowercase alphabet optimized for readability.\n"
" - **Geohash** (`geohash_encode` / `geohash_decode`) — the alphabet used by\n"
" the Geohash geocoding system.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" thirtytwo.encode(<<\"wibble\":utf8>>, padding: True)\n"
" // -> \"O5UWEYTMMU======\"\n"
"\n"
" thirtytwo.decode(\"O5UWEYTMMU======\")\n"
" // -> Ok(<<\"wibble\":utf8>>)\n"
"\n"
" thirtytwo.crockford_encode(<<\"wibble\":utf8>>, check: False)\n"
" // -> \"EXMP4RKCCM\"\n"
" ```\n"
).
-file("src/thirtytwo.gleam", 52).
-spec 'bor'(integer(), integer()) -> integer().
'bor'(A, B) ->
erlang:'bor'(A, B).
-file("src/thirtytwo.gleam", 56).
-spec 'band'(integer(), integer()) -> integer().
'band'(A, B) ->
erlang:'band'(A, B).
-file("src/thirtytwo.gleam", 60).
-spec shl(integer(), integer()) -> integer().
shl(A, N) ->
erlang:'bsl'(A, N).
-file("src/thirtytwo.gleam", 64).
-spec shr(integer(), integer()) -> integer().
shr(A, N) ->
erlang:'bsr'(A, N).
-file("src/thirtytwo.gleam", 182).
-spec compute_check_value(bitstring(), integer()) -> integer().
compute_check_value(Input, Acc) ->
case Input of
<<Byte, Rest/binary>> ->
compute_check_value(Rest, ((Acc * 256) + Byte) rem 37);
_ ->
Acc
end.
-file("src/thirtytwo.gleam", 220).
-spec encode_group(integer(), integer(), integer(), integer(), integer()) -> list(integer()).
encode_group(B0, B1, B2, B3, B4) ->
C0 = shr(B0, 3),
C1 = 'bor'(shl('band'(B0, 16#07), 2), shr(B1, 6)),
C2 = 'band'(shr(B1, 1), 16#1F),
C3 = 'bor'(shl('band'(B1, 16#01), 4), shr(B2, 4)),
C4 = 'bor'(shl('band'(B2, 16#0F), 1), shr(B3, 7)),
C5 = 'band'(shr(B3, 2), 16#1F),
C6 = 'bor'(shl('band'(B3, 16#03), 3), shr(B4, 5)),
C7 = 'band'(B4, 16#1F),
[C0, C1, C2, C3, C4, C5, C6, C7].
-file("src/thirtytwo.gleam", 247).
-spec lookup_char(gleam@dict:dict(integer(), binary()), integer()) -> binary().
lookup_char(Alphabet, Index) ->
_pipe = gleam_stdlib:map_get(Alphabet, Index),
gleam@result:unwrap(_pipe, <<""/utf8>>).
-file("src/thirtytwo.gleam", 232).
-spec encode_remainder(
integer(),
integer(),
integer(),
integer(),
integer(),
gleam@dict:dict(integer(), binary()),
list(binary())
) -> list(binary()).
encode_remainder(B0, B1, B2, B3, Count, Alphabet, Acc) ->
_pipe = encode_group(B0, B1, B2, B3, 0),
_pipe@1 = gleam@list:take(_pipe, Count),
_pipe@2 = gleam@list:map(
_pipe@1,
fun(_capture) -> lookup_char(Alphabet, _capture) end
),
gleam@list:fold(_pipe@2, Acc, fun gleam@list:prepend/2).
-file("src/thirtytwo.gleam", 200).
-spec do_encode_bytes(
bitstring(),
gleam@dict:dict(integer(), binary()),
list(binary())
) -> list(binary()).
do_encode_bytes(Input, Alphabet, Acc) ->
case Input of
<<B0, B1, B2, B3, B4, Rest/binary>> ->
_pipe = encode_group(B0, B1, B2, B3, B4),
_pipe@1 = gleam@list:map(
_pipe,
fun(_capture) -> lookup_char(Alphabet, _capture) end
),
_pipe@2 = gleam@list:fold(_pipe@1, Acc, fun gleam@list:prepend/2),
do_encode_bytes(Rest, Alphabet, _pipe@2);
<<B0@1, B1@1, B2@1, B3@1>> ->
encode_remainder(B0@1, B1@1, B2@1, B3@1, 7, Alphabet, Acc);
<<B0@2, B1@2, B2@2>> ->
encode_remainder(B0@2, B1@2, B2@2, 0, 5, Alphabet, Acc);
<<B0@3, B1@3>> ->
encode_remainder(B0@3, B1@3, 0, 0, 4, Alphabet, Acc);
<<B0@4>> ->
encode_remainder(B0@4, 0, 0, 0, 2, Alphabet, Acc);
_ ->
Acc
end.
-file("src/thirtytwo.gleam", 258).
-spec build_decode_map(binary(), boolean()) -> gleam@dict:dict(binary(), integer()).
build_decode_map(Alphabet, Case_insensitive) ->
Chars = gleam@string:to_graphemes(Alphabet),
Pairs = gleam@list:index_map(Chars, fun(Char, Index) -> {Char, Index} end),
Lower_pairs = case Case_insensitive of
true ->
gleam@list:index_map(
Chars,
fun(Char@1, Index@1) -> {string:lowercase(Char@1), Index@1} end
);
false ->
[]
end,
_pipe = lists:append(Pairs, Lower_pairs),
maps:from_list(_pipe).
-file("src/thirtytwo.gleam", 325).
-spec decode_group(
integer(),
integer(),
integer(),
integer(),
integer(),
integer(),
integer(),
integer()
) -> {integer(), integer(), integer(), integer(), integer()}.
decode_group(V0, V1, V2, V3, V4, V5, V6, V7) ->
Byte0 = 'bor'(shl(V0, 3), shr(V1, 2)),
Byte1 = 'bor'(shl('band'(V1, 16#03), 6), 'bor'(shl(V2, 1), shr(V3, 4))),
Byte2 = 'bor'(shl('band'(V3, 16#0F), 4), shr(V4, 1)),
Byte3 = 'bor'(shl('band'(V4, 16#01), 7), 'bor'(shl(V5, 2), shr(V6, 3))),
Byte4 = 'bor'(shl('band'(V6, 16#07), 5), V7),
{Byte0, Byte1, Byte2, Byte3, Byte4}.
-file("src/thirtytwo.gleam", 343).
-spec decode_remainder(list(integer()), list(bitstring())) -> {ok, bitstring()} |
{error, nil}.
decode_remainder(Values, Acc) ->
{Last_value, Mask, Byte_count} = case Values of
[_, V1] ->
{V1, 16#03, 1};
[_, _, _, V3] ->
{V3, 16#0F, 2};
[_, _, _, _, V4] ->
{V4, 16#01, 3};
[_, _, _, _, _, _, V6] ->
{V6, 16#07, 4};
_ ->
{0, 0, 0}
end,
gleam@bool:guard(
Byte_count =:= 0,
{error, nil},
fun() ->
gleam@bool:guard(
'band'(Last_value, Mask) /= 0,
{error, nil},
fun() ->
Padded = lists:append(
Values,
gleam@list:repeat(0, 8 - erlang:length(Values))
),
case Padded of
[V0, V1@1, V2, V3@1, V4@1, V5, V6@1, V7] ->
{B0, B1, B2, B3, _} = decode_group(
V0,
V1@1,
V2,
V3@1,
V4@1,
V5,
V6@1,
V7
),
Bytes = <<B0, B1, B2, B3>>,
{ok,
begin
_pipe@1 = [begin
_pipe = gleam_stdlib:bit_array_slice(
Bytes,
0,
Byte_count
),
gleam@result:unwrap(_pipe, <<>>)
end |
Acc],
_pipe@2 = lists:reverse(_pipe@1),
gleam_stdlib:bit_array_concat(_pipe@2)
end};
_ ->
{error, nil}
end
end
)
end
).
-file("src/thirtytwo.gleam", 310).
-spec decode_values(list(integer()), list(bitstring())) -> {ok, bitstring()} |
{error, nil}.
decode_values(Values, Acc) ->
case Values of
[V0, V1, V2, V3, V4, V5, V6, V7 | Rest] ->
{Byte0, Byte1, Byte2, Byte3, Byte4} = decode_group(
V0,
V1,
V2,
V3,
V4,
V5,
V6,
V7
),
decode_values(Rest, [<<Byte0, Byte1, Byte2, Byte3, Byte4>> | Acc]);
[] ->
{ok,
begin
_pipe = Acc,
_pipe@1 = lists:reverse(_pipe),
gleam_stdlib:bit_array_concat(_pipe@1)
end};
Remaining ->
decode_remainder(Remaining, Acc)
end.
-file("src/thirtytwo.gleam", 273).
-spec do_decode(binary(), gleam@dict:dict(binary(), integer())) -> {ok,
bitstring()} |
{error, nil}.
do_decode(Input, Decode_map) ->
Chars = gleam@string:to_graphemes(Input),
gleam@result:'try'(
gleam@list:try_map(
Chars,
fun(C) -> gleam_stdlib:map_get(Decode_map, C) end
),
fun(Values) ->
Remainder = erlang:length(Values) rem 8,
gleam@bool:guard(
gleam@list:contains([1, 3, 6], Remainder),
{error, nil},
fun() -> decode_values(Values, []) end
)
end
).
-file("src/thirtytwo.gleam", 172).
-spec build_crockford_decode_map() -> gleam@dict:dict(binary(), integer()).
build_crockford_decode_map() ->
_pipe = build_decode_map(<<"0123456789ABCDEFGHJKMNPQRSTVWXYZ"/utf8>>, true),
_pipe@1 = gleam@dict:insert(_pipe, <<"O"/utf8>>, 0),
_pipe@2 = gleam@dict:insert(_pipe@1, <<"o"/utf8>>, 0),
_pipe@3 = gleam@dict:insert(_pipe@2, <<"I"/utf8>>, 1),
_pipe@4 = gleam@dict:insert(_pipe@3, <<"i"/utf8>>, 1),
_pipe@5 = gleam@dict:insert(_pipe@4, <<"L"/utf8>>, 1),
gleam@dict:insert(_pipe@5, <<"l"/utf8>>, 1).
-file("src/thirtytwo.gleam", 116).
?DOC(
" Decode a Crockford base32 string. Hyphens are stripped, decoding is\n"
" case-insensitive, and the aliases O→0, I/L→1 are normalized. When\n"
" `check` is `True`, the trailing check digit is validated.\n"
" Returns `Error(Nil)` on invalid input or a failed check.\n"
).
-spec crockford_decode(binary(), boolean()) -> {ok, bitstring()} | {error, nil}.
crockford_decode(Input, Check) ->
Cleaned = gleam@string:replace(Input, <<"-"/utf8>>, <<""/utf8>>),
Decode_map = build_crockford_decode_map(),
case Check andalso (Cleaned /= <<""/utf8>>) of
true ->
gleam@bool:guard(
string:length(Cleaned) < 2,
{error, nil},
fun() ->
Body = gleam@string:drop_end(Cleaned, 1),
gleam@result:'try'(
gleam@string:last(Cleaned),
fun(Check_char) ->
Check_decode_map = build_decode_map(
<<"0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U"/utf8>>,
true
),
gleam@result:'try'(
gleam_stdlib:map_get(
Check_decode_map,
Check_char
),
fun(Check_index) ->
gleam@result:'try'(
do_decode(Body, Decode_map),
fun(Decoded) ->
Expected = compute_check_value(
Decoded,
0
),
gleam@bool:guard(
Check_index /= Expected,
{error, nil},
fun() -> {ok, Decoded} end
)
end
)
end
)
end
)
end
);
false ->
do_decode(Cleaned, Decode_map)
end.
-file("src/thirtytwo.gleam", 147).
?DOC(
" Decode a z-base-32 string. Decoding is case-sensitive.\n"
" Returns `Error(Nil)` on invalid input.\n"
).
-spec z_base_32_decode(binary()) -> {ok, bitstring()} | {error, nil}.
z_base_32_decode(Input) ->
Decode_map = build_decode_map(
<<"ybndrfg8ejkmcpqxot1uwisza345h769"/utf8>>,
false
),
do_decode(Input, Decode_map).
-file("src/thirtytwo.gleam", 161).
?DOC(
" Decode a Geohash base32 string. Decoding is case-sensitive.\n"
" Returns `Error(Nil)` on invalid input.\n"
).
-spec geohash_decode(binary()) -> {ok, bitstring()} | {error, nil}.
geohash_decode(Input) ->
Decode_map = build_decode_map(
<<"0123456789bcdefghjkmnpqrstuvwxyz"/utf8>>,
false
),
do_decode(Input, Decode_map).
-file("src/thirtytwo.gleam", 251).
-spec apply_padding(binary(), boolean()) -> binary().
apply_padding(Encoded, Padding) ->
gleam@bool:guard(
not Padding,
Encoded,
fun() ->
Remainder = string:length(Encoded) rem 8,
gleam@bool:guard(
Remainder =:= 0,
Encoded,
fun() ->
<<Encoded/binary,
(gleam@string:repeat(<<"="/utf8>>, 8 - Remainder))/binary>>
end
)
end
).
-file("src/thirtytwo.gleam", 189).
-spec do_encode(bitstring(), binary(), boolean()) -> binary().
do_encode(Input, Alphabet, Padding) ->
Alphabet_map = begin
_pipe = gleam@string:to_graphemes(Alphabet),
_pipe@1 = gleam@list:index_map(
_pipe,
fun(Char, Index) -> {Index, Char} end
),
maps:from_list(_pipe@1)
end,
_pipe@2 = do_encode_bytes(Input, Alphabet_map, []),
_pipe@3 = lists:reverse(_pipe@2),
_pipe@4 = erlang:list_to_binary(_pipe@3),
apply_padding(_pipe@4, Padding).
-file("src/thirtytwo.gleam", 72).
?DOC(
" Encode a bit array using the standard RFC 4648 base32 alphabet.\n"
" The input must be byte-aligned; non-byte-aligned bit arrays produce\n"
" undefined results.\n"
" When `padding` is `True`, the output is padded with `=` to a multiple of 8.\n"
).
-spec encode(bitstring(), boolean()) -> binary().
encode(Input, Padding) ->
do_encode(Input, <<"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"/utf8>>, Padding).
-file("src/thirtytwo.gleam", 86).
?DOC(
" Encode a bit array using the base32hex alphabet (RFC 4648 section 7).\n"
" Preserves sort order of the underlying data. The input must be\n"
" byte-aligned; non-byte-aligned bit arrays produce undefined results.\n"
" When `padding` is `True`, the output is padded with `=` to a multiple of 8.\n"
).
-spec hex_encode(bitstring(), boolean()) -> binary().
hex_encode(Input, Padding) ->
do_encode(Input, <<"0123456789ABCDEFGHIJKLMNOPQRSTUV"/utf8>>, Padding).
-file("src/thirtytwo.gleam", 100).
?DOC(
" Encode a bit array using Crockford's base32 alphabet. The output is\n"
" always unpadded and uppercase. The input must be byte-aligned;\n"
" non-byte-aligned bit arrays produce undefined results. When `check`\n"
" is `True`, a mod-37 check digit is appended.\n"
).
-spec crockford_encode(bitstring(), boolean()) -> binary().
crockford_encode(Input, Check) ->
Encoded = do_encode(
Input,
<<"0123456789ABCDEFGHJKMNPQRSTVWXYZ"/utf8>>,
false
),
case Check andalso (Encoded /= <<""/utf8>>) of
true ->
Check_value = compute_check_value(Input, 0),
Check_char = gleam@string:slice(
<<"0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U"/utf8>>,
Check_value,
1
),
<<Encoded/binary, Check_char/binary>>;
false ->
Encoded
end.
-file("src/thirtytwo.gleam", 141).
?DOC(
" Encode a bit array using the z-base-32 alphabet. The output is always\n"
" lowercase and unpadded. The input must be byte-aligned; non-byte-aligned\n"
" bit arrays produce undefined results.\n"
).
-spec z_base_32_encode(bitstring()) -> binary().
z_base_32_encode(Input) ->
do_encode(Input, <<"ybndrfg8ejkmcpqxot1uwisza345h769"/utf8>>, false).
-file("src/thirtytwo.gleam", 155).
?DOC(
" Encode a bit array using the Geohash base32 alphabet. The output is\n"
" always lowercase and unpadded. The input must be byte-aligned;\n"
" non-byte-aligned bit arrays produce undefined results.\n"
).
-spec geohash_encode(bitstring()) -> binary().
geohash_encode(Input) ->
do_encode(Input, <<"0123456789bcdefghjkmnpqrstuvwxyz"/utf8>>, false).
-file("src/thirtytwo.gleam", 303).
-spec count_trailing_pad(binary(), integer()) -> integer().
count_trailing_pad(Input, Remaining) ->
case (Remaining > 0) andalso gleam_stdlib:string_ends_with(
Input,
<<"="/utf8>>
) of
true ->
count_trailing_pad(gleam@string:drop_end(Input, 1), Remaining - 1) + 1;
false ->
0
end.
-file("src/thirtytwo.gleam", 289).
-spec validate_padding(binary()) -> {ok, binary()} | {error, nil}.
validate_padding(Input) ->
Len = string:length(Input),
Pad_count = count_trailing_pad(Input, Len),
gleam@bool:guard(
Pad_count =:= 0,
{ok, Input},
fun() ->
Has_interior_pad = gleam_stdlib:contains_string(
gleam@string:drop_end(Input, Pad_count),
<<"="/utf8>>
),
gleam@bool:guard(
Has_interior_pad,
{error, nil},
fun() ->
gleam@bool:guard(
(Len rem 8) /= 0,
{error, nil},
fun() -> case Pad_count of
1 ->
{ok,
gleam@string:drop_end(Input, Pad_count)};
3 ->
{ok,
gleam@string:drop_end(Input, Pad_count)};
4 ->
{ok,
gleam@string:drop_end(Input, Pad_count)};
6 ->
{ok,
gleam@string:drop_end(Input, Pad_count)};
_ ->
{error, nil}
end end
)
end
)
end
).
-file("src/thirtytwo.gleam", 166).
-spec decode_with_padding(binary(), binary()) -> {ok, bitstring()} |
{error, nil}.
decode_with_padding(Input, Alphabet) ->
Decode_map = build_decode_map(Alphabet, true),
gleam@result:'try'(
validate_padding(Input),
fun(Stripped) -> do_decode(Stripped, Decode_map) end
).
-file("src/thirtytwo.gleam", 78).
?DOC(
" Decode a standard RFC 4648 base32 string. Padding is optional and\n"
" decoding is case-insensitive. Returns `Error(Nil)` on invalid input.\n"
).
-spec decode(binary()) -> {ok, bitstring()} | {error, nil}.
decode(Input) ->
decode_with_padding(Input, <<"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"/utf8>>).
-file("src/thirtytwo.gleam", 92).
?DOC(
" Decode a base32hex string. Padding is optional and decoding is\n"
" case-insensitive. Returns `Error(Nil)` on invalid input.\n"
).
-spec hex_decode(binary()) -> {ok, bitstring()} | {error, nil}.
hex_decode(Input) ->
decode_with_padding(Input, <<"0123456789ABCDEFGHIJKLMNOPQRSTUV"/utf8>>).