%%% @doc %%% Base encoding and decoding module. %%% %%% This module provides functions for encoding and decoding data using %%% various base encoding schemes: base16 (hex), base32, base64, and their %%% URL-safe variants. %%% %%% Examples: %%% ``` %%% Encoded = base:encode64("Hello, World!"), %%% Decoded = base:decode64(Encoded), %%% HexEncoded = base:encode16("binary data"), %%% UrlSafe = base:url_encode64("data with + and /"). %%% ''' %%% @end -module(base). %% API - Base64 -export([encode64/1, decode64/1, decode64/2]). -export([url_encode64/1, url_decode64/1, url_decode64/2]). %% API - Base32 -export([encode32/1, decode32/1, decode32/2]). -export([hex_encode32/1, hex_decode32/1, hex_decode32/2]). %% API - Base16 -export([encode16/1, decode16/1, decode16/2]). %% Types -type encode_case() :: upper | lower. -type padding() :: true | false. -type decode_options() :: #{ignore => whitespace | none, letter_case => mixed | upper | lower}. -export_type([encode_case/0, padding/0, decode_options/0]). %%%============================================================================= %%% Base64 Functions %%%============================================================================= %%% @doc %%% Encodes binary data using Base64 encoding. %%% %%% Returns a binary containing the Base64-encoded data with padding. %%% @end -spec encode64(binary() | iodata()) -> binary(). encode64(Data) -> base64:encode(Data). %%% @doc %%% Decodes Base64-encoded data. %%% %%% Returns the decoded binary data. Raises an error if the input is invalid. %%% @end -spec decode64(binary() | string()) -> binary(). decode64(Data) -> try base64:decode(Data) catch error:Reason -> error({invalid_base64, Reason}) end. %%% @doc %%% Decodes Base64-encoded data with options. %%% %%% Options: %%% - ignore: whitespace | none - whether to ignore whitespace characters %%% Returns {ok, Decoded} on success, or error on invalid input. %%% @end -spec decode64(binary() | string(), decode_options()) -> {ok, binary()} | error. decode64(Data, Options) -> try CleanData = maybe_ignore_whitespace(Data, Options), Decoded = base64:decode(CleanData), {ok, Decoded} catch throw:has_whitespace -> error; _:_ -> error end. %%% @doc %%% Encodes binary data using URL-safe Base64 encoding. %%% %%% URL-safe Base64 uses '-' and '_' instead of '+' and '/', and omits padding. %%% @end -spec url_encode64(binary() | iodata()) -> binary(). url_encode64(Data) -> Encoded = base64:encode(Data), make_url_safe(Encoded). %%% @doc %%% Decodes URL-safe Base64-encoded data. %%% %%% Handles both padded and unpadded URL-safe Base64. %%% @end -spec url_decode64(binary() | string()) -> binary(). url_decode64(Data) -> try StandardData = make_standard_base64(Data), base64:decode(StandardData) catch error:Reason -> error({invalid_base64, Reason}) end. %%% @doc %%% Decodes URL-safe Base64-encoded data with options. %%% %%% Returns {ok, Decoded} on success, or error on invalid input. %%% @end -spec url_decode64(binary() | string(), decode_options()) -> {ok, binary()} | error. url_decode64(Data, Options) -> try CleanData = maybe_ignore_whitespace(Data, Options), StandardData = make_standard_base64(CleanData), Decoded = base64:decode(StandardData), {ok, Decoded} catch throw:has_whitespace -> error; _:_ -> error end. %%%============================================================================= %%% Base32 Functions %%%============================================================================= %%% @doc %%% Encodes binary data using standard Base32 encoding. %%% %%% Uses the standard Base32 alphabet (A-Z, 2-7) with padding. %%% @end -spec encode32(binary() | iodata()) -> binary(). encode32(Data) -> encode_base32(iolist_to_binary(Data), standard_base32_alphabet()). %%% @doc %%% Decodes standard Base32-encoded data. %%% %%% Returns the decoded binary data. Raises an error if the input is invalid. %%% @end -spec decode32(binary() | string()) -> binary(). decode32(Data) -> try decode_base32(to_binary(Data), standard_base32_alphabet()) catch error:Reason -> error({invalid_base32, Reason}) end. %%% @doc %%% Decodes standard Base32-encoded data with options. %%% %%% Returns {ok, Decoded} on success, or error on invalid input. %%% @end -spec decode32(binary() | string(), decode_options()) -> {ok, binary()} | error. decode32(Data, Options) -> try CleanData = maybe_ignore_whitespace(to_binary(Data), Options), Decoded = decode_base32(CleanData, standard_base32_alphabet()), {ok, Decoded} catch throw:has_whitespace -> error; _:_ -> error end. %%% @doc %%% Encodes binary data using Base32 hex encoding. %%% %%% Uses the extended hex alphabet (0-9, A-V) with padding. %%% @end -spec hex_encode32(binary() | iodata()) -> binary(). hex_encode32(Data) -> encode_base32(iolist_to_binary(Data), hex_base32_alphabet()). %%% @doc %%% Decodes Base32 hex-encoded data. %%% %%% Returns the decoded binary data. Raises an error if the input is invalid. %%% @end -spec hex_decode32(binary() | string()) -> binary(). hex_decode32(Data) -> try decode_base32(to_binary(Data), hex_base32_alphabet()) catch error:Reason -> error({invalid_base32, Reason}) end. %%% @doc %%% Decodes Base32 hex-encoded data with options. %%% %%% Returns {ok, Decoded} on success, or error on invalid input. %%% @end -spec hex_decode32(binary() | string(), decode_options()) -> {ok, binary()} | error. hex_decode32(Data, Options) -> try CleanData = maybe_ignore_whitespace(to_binary(Data), Options), Decoded = decode_base32(CleanData, hex_base32_alphabet()), {ok, Decoded} catch throw:has_whitespace -> error; _:_ -> error end. %%%============================================================================= %%% Base16 Functions %%%============================================================================= %%% @doc %%% Encodes binary data using Base16 (hexadecimal) encoding. %%% %%% Returns uppercase hexadecimal by default. %%% @end -spec encode16(binary() | iodata()) -> binary(). encode16(Data) -> encode_hex(iolist_to_binary(Data), upper). %%% @doc %%% Decodes Base16 (hexadecimal) encoded data. %%% %%% Accepts both uppercase and lowercase hexadecimal. %%% @end -spec decode16(binary() | string()) -> binary(). decode16(Data) -> try decode_hex(to_binary(Data)) catch error:Reason -> error({invalid_base16, Reason}) end. %%% @doc %%% Decodes Base16 (hexadecimal) encoded data with options. %%% %%% Options: %%% - letter_case: mixed | upper | lower - expected case of hex digits %%% Returns {ok, Decoded} on success, or error on invalid input. %%% @end -spec decode16(binary() | string(), decode_options()) -> {ok, binary()} | error. decode16(Data, Options) -> try CleanData = maybe_ignore_whitespace(to_binary(Data), Options), validate_hex_case(CleanData, Options), Decoded = decode_hex(CleanData), {ok, Decoded} catch throw:has_whitespace -> error; _:_ -> error end. %%%============================================================================= %%% Internal functions - Base64 %%%============================================================================= %% @private make_url_safe(Base64) -> NoPadding = binary:replace(Base64, <<"=">>, <<>>, [global]), WithDash = binary:replace(NoPadding, <<"+">>, <<"-">>, [global]), binary:replace(WithDash, <<"/">>, <<"_">>, [global]). %% @private make_standard_base64(UrlSafeBase64) -> WithPlus = binary:replace(UrlSafeBase64, <<"-">>, <<"+">>, [global]), WithSlash = binary:replace(WithPlus, <<"_">>, <<"/">>, [global]), add_base64_padding(WithSlash). %% @private add_base64_padding(Data) -> case byte_size(Data) rem 4 of 0 -> Data; 2 -> <>; 3 -> <>. %% @private hex_base32_alphabet() -> <<"0123456789ABCDEFGHIJKLMNOPQRSTUV">>. %% @private encode_base32(<<>>, _Alphabet) -> <<>>; encode_base32(Data, Alphabet) -> encode_base32_chunks(Data, Alphabet, <<>>). %% @private encode_base32_chunks(<<>>, _Alphabet, Acc) -> Acc; encode_base32_chunks(Data, Alphabet, Acc) when byte_size(Data) < 5 -> % Pad the last chunk PaddedData = pad_to_5_bytes(Data), Chunk = encode_base32_chunk(PaddedData, Alphabet), PaddingSize = (5 - byte_size(Data)) * 8 div 5, TrimmedChunk = binary:part(Chunk, 0, 8 - PaddingSize), Padding = binary:copy(<<"=">>, PaddingSize), <>; encode_base32_chunks(<>, Alphabet, Acc) -> EncodedChunk = encode_base32_chunk(Chunk, Alphabet), encode_base32_chunks(Rest, Alphabet, <>). %% @private encode_base32_chunk(<>, Alphabet) -> % Convert 5 bytes (40 bits) to 8 base32 characters <> = <>, <<(binary:at(Alphabet, I1)), (binary:at(Alphabet, I2)), (binary:at(Alphabet, I3)), (binary:at(Alphabet, I4)), (binary:at(Alphabet, I5)), (binary:at(Alphabet, I6)), (binary:at(Alphabet, I7)), (binary:at(Alphabet, I8))>>. %% @private pad_to_5_bytes(Data) -> PaddingSize = 5 - byte_size(Data), <>. %% @private decode_base32(Data, Alphabet) -> CleanData = remove_padding(Data), decode_base32_chunks(CleanData, Alphabet, <<>>). %% @private decode_base32_chunks(<<>>, _Alphabet, Acc) -> Acc; decode_base32_chunks(Data, Alphabet, Acc) when byte_size(Data) < 8 -> % Handle partial chunk PaddedData = <>, 8 - byte_size(Data)))/binary>>, DecodedChunk = decode_base32_chunk(PaddedData, Alphabet), OutputSize = byte_size(Data) * 5 div 8, TrimmedChunk = binary:part(DecodedChunk, 0, OutputSize), <>; decode_base32_chunks(<>, Alphabet, Acc) -> DecodedChunk = decode_base32_chunk(Chunk, Alphabet), decode_base32_chunks(Rest, Alphabet, <>). %% @private decode_base32_chunk(<>, Alphabet) -> I1 = base32_char_to_int(C1, Alphabet), I2 = base32_char_to_int(C2, Alphabet), I3 = base32_char_to_int(C3, Alphabet), I4 = base32_char_to_int(C4, Alphabet), I5 = base32_char_to_int(C5, Alphabet), I6 = base32_char_to_int(C6, Alphabet), I7 = base32_char_to_int(C7, Alphabet), I8 = base32_char_to_int(C8, Alphabet), <> = <>, <>. %% @private base32_char_to_int(Char, Alphabet) -> case binary:match(Alphabet, <>) of {Index, 1} -> Index; nomatch -> error({invalid_base32_char, Char}) end. %%%============================================================================= %%% Internal functions - Base16 %%%============================================================================= %% @private encode_hex(Data, upper) -> binary:encode_hex(Data). %% @private decode_hex(HexData) -> try binary:decode_hex(HexData) catch error:badarg -> error(invalid_hex) end. %% @private validate_hex_case(Data, Options) -> case maps:get(letter_case, Options, mixed) of mixed -> ok; upper -> validate_all_upper(Data); lower -> validate_all_lower(Data) end. %% @private validate_all_upper(<<>>) -> ok; validate_all_upper(<>) when C >= $A, C =< $F; C >= $0, C =< $9 -> validate_all_upper(Rest); validate_all_upper(_) -> error(invalid_case). %% @private validate_all_lower(<<>>) -> ok; validate_all_lower(<>) when C >= $a, C =< $f; C >= $0, C =< $9 -> validate_all_lower(Rest); validate_all_lower(_) -> error(invalid_case). %%%============================================================================= %%% Internal functions - Common %%%============================================================================= %% @private to_binary(Data) when is_binary(Data) -> Data; to_binary(Data) when is_list(Data) -> list_to_binary(Data). %% @private maybe_ignore_whitespace(Data, Options) -> case maps:get(ignore, Options, none) of whitespace -> remove_whitespace(Data); none -> case has_whitespace(Data) of true -> throw(has_whitespace); false -> Data end end. %% @private remove_whitespace(Data) -> binary:replace(Data, [<<" ">>, <<"\t">>, <<"\n">>, <<"\r">>], <<>>, [global]). %% @private has_whitespace(Data) -> binary:match(Data, [<<" ">>, <<"\t">>, <<"\n">>, <<"\r">>]) =/= nomatch. %% @private remove_padding(Data) -> binary:replace(Data, <<"=">>, <<>>, [global]).