%%% @doc z-base-32 codec (Phil Zimmermann's "Human-Oriented Base-32 %%% Encoding"). Alphabet: `ybndrfg8ejkmcpqxot1uwisza345h769'. %%% %%% Used by Macula to encode 32-byte Ed25519 pubkeys as DNS-label- %%% friendly strings: 32 bytes → 52 ASCII characters, comfortably %%% within DNS's 63-char per-label cap. PKARR (the %%% public-key-addressable resource record convention) and projects %%% like Pubky use this same encoding for the same reason. %%% %%% Encoding direction: bit stream is taken MSB-first from the %%% input bytes, grouped into 5-bit chunks (the final chunk %%% zero-padded on the right if the input length is not a multiple %%% of 5 bits). Each chunk indexes into the alphabet. %%% %%% Decoding is the inverse: characters → 5-bit values → bit %%% stream → 8-bit byte stream, dropping any trailing bits left %%% over from the encoder's right-padding. %%% %%% Length convention (the cases that matter for Macula): %%% - 32 bytes (Ed25519 pubkey) → 52 chars %%% - 16 bytes (UUID-like) → 26 chars %%% - empty → empty %%% %%% Reference: Phil Zimmermann's z-base-32 spec. %%% @end -module(macula_z32). -export([encode/1, decode/1, is_valid_label/1]). %% z-base-32 alphabet, indexed 0..31. -define(ALPHABET, <<"ybndrfg8ejkmcpqxot1uwisza345h769">>). %%==================================================================== %% Public API %%==================================================================== %% @doc Encode a binary into its z-base-32 representation. -spec encode(binary()) -> binary(). encode(<<>>) -> <<>>; encode(Bytes) when is_binary(Bytes) -> BitCount = byte_size(Bytes) * 8, PaddedLen = ((BitCount + 4) div 5) * 5, PadBits = PaddedLen - BitCount, Padded = <>, encode_chunks(Padded, []). encode_chunks(<<>>, Acc) -> list_to_binary(lists:reverse(Acc)); encode_chunks(<>, Acc) -> Char = binary:at(?ALPHABET, Chunk), encode_chunks(Rest, [Char | Acc]). %% @doc Decode a z-base-32 string back into its original bytes. %% Returns `{error, invalid_z32}' on any character outside the %% alphabet, or when the input length is not a valid encoding of %% a whole-byte payload (i.e., the bit length isn't a multiple of 8 %% within the rounding-up tolerance). -spec decode(binary()) -> {ok, binary()} | {error, invalid_z32}. decode(<<>>) -> {ok, <<>>}; decode(Z32) when is_binary(Z32) -> decode_chars(Z32, <<>>). decode_chars(<<>>, BitAcc) -> %% BitAcc may be N bits long where N is a multiple of 5 but %% may not be a multiple of 8. We accept up to 4 trailing %% padding bits (those produced by encode/1 when the original %% input wasn't a multiple of 5 bits long); they MUST be zero. BitLen = bit_size(BitAcc), ByteLen = BitLen div 8, PadBits = BitLen rem 8, case PadBits =< 4 of false -> {error, invalid_z32}; true -> <> = BitAcc, case Pad of 0 -> {ok, Bytes}; _ -> {error, invalid_z32} end end; decode_chars(<>, BitAcc) -> case char_to_value(Char) of {ok, Value} -> decode_chars(Rest, <>); error -> {error, invalid_z32} end. %% @doc Check whether a binary is a syntactically valid DNS label %% under the z-base-32 alphabet (every character is in the %% alphabet, length is between 1 and 63 octets, and it decodes %% cleanly to a whole-byte payload). -spec is_valid_label(binary()) -> boolean(). is_valid_label(Bin) when is_binary(Bin) -> Size = byte_size(Bin), Size >= 1 andalso Size =< 63 andalso case decode(Bin) of {ok, _} -> true; {error, _} -> false end; is_valid_label(_) -> false. %%==================================================================== %% Alphabet lookup. Inline guard clauses for speed; the alphabet is %% small enough that the compiler's case-tree is faster than a map. %%==================================================================== char_to_value($y) -> {ok, 0}; char_to_value($b) -> {ok, 1}; char_to_value($n) -> {ok, 2}; char_to_value($d) -> {ok, 3}; char_to_value($r) -> {ok, 4}; char_to_value($f) -> {ok, 5}; char_to_value($g) -> {ok, 6}; char_to_value($8) -> {ok, 7}; char_to_value($e) -> {ok, 8}; char_to_value($j) -> {ok, 9}; char_to_value($k) -> {ok, 10}; char_to_value($m) -> {ok, 11}; char_to_value($c) -> {ok, 12}; char_to_value($p) -> {ok, 13}; char_to_value($q) -> {ok, 14}; char_to_value($x) -> {ok, 15}; char_to_value($o) -> {ok, 16}; char_to_value($t) -> {ok, 17}; char_to_value($1) -> {ok, 18}; char_to_value($u) -> {ok, 19}; char_to_value($w) -> {ok, 20}; char_to_value($i) -> {ok, 21}; char_to_value($s) -> {ok, 22}; char_to_value($z) -> {ok, 23}; char_to_value($a) -> {ok, 24}; char_to_value($3) -> {ok, 25}; char_to_value($4) -> {ok, 26}; char_to_value($5) -> {ok, 27}; char_to_value($h) -> {ok, 28}; char_to_value($7) -> {ok, 29}; char_to_value($6) -> {ok, 30}; char_to_value($9) -> {ok, 31}; char_to_value(_) -> error.