-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 <> -> 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 <> -> _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); <> -> encode_remainder(B0@1, B1@1, B2@1, B3@1, 7, Alphabet, Acc); <> -> encode_remainder(B0@2, B1@2, B2@2, 0, 5, Alphabet, Acc); <> -> encode_remainder(B0@3, B1@3, 0, 0, 4, Alphabet, Acc); <> -> 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 = <>, {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, [<> | 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() -> < 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 ), <>; 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>>).