diff --git a/apps/emqx_rule_engine/src/emqx_rule_funcs.erl b/apps/emqx_rule_engine/src/emqx_rule_funcs.erl index 414a3d620..4e28efb5f 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_funcs.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_funcs.erl @@ -202,7 +202,8 @@ -export([ md5/1, sha/1, - sha256/1 + sha256/1, + hash/2 ]). %% zip Funcs @@ -710,24 +711,11 @@ map(Map = #{}) -> map(Data) -> error(badarg, [Data]). -bin2hexstr(Bin) when is_binary(Bin) -> - emqx_utils:bin_to_hexstr(Bin, upper); -%% If Bin is a bitstring which is not divisible by 8, we pad it and then do the -%% conversion -bin2hexstr(Bin) when is_bitstring(Bin), (8 - (bit_size(Bin) rem 8)) >= 4 -> - PadSize = 8 - (bit_size(Bin) rem 8), - Padding = <<0:PadSize>>, - BinToConvert = <>, - <<_FirstByte:8, HexStr/binary>> = emqx_utils:bin_to_hexstr(BinToConvert, upper), - HexStr; -bin2hexstr(Bin) when is_bitstring(Bin) -> - PadSize = 8 - (bit_size(Bin) rem 8), - Padding = <<0:PadSize>>, - BinToConvert = <>, - emqx_utils:bin_to_hexstr(BinToConvert, upper). +bin2hexstr(Bin) -> + emqx_variform_bif:bin2hexstr(Bin). -hexstr2bin(Str) when is_binary(Str) -> - emqx_utils:hexstr_to_bin(Str). +hexstr2bin(Str) -> + emqx_variform_bif:hexstr2bin(Str). %%------------------------------------------------------------------------------ %% NULL Funcs @@ -1001,7 +989,7 @@ sha256(S) when is_binary(S) -> hash(sha256, S). hash(Type, Data) -> - emqx_utils:bin_to_hexstr(crypto:hash(Type, Data), lower). + emqx_variform_bif:hash(Type, Data). %%------------------------------------------------------------------------------ %% gzip Funcs diff --git a/apps/emqx_utils/src/emqx_variform_bif.erl b/apps/emqx_utils/src/emqx_variform_bif.erl index fe5cb2369..5c598efbd 100644 --- a/apps/emqx_utils/src/emqx_variform_bif.erl +++ b/apps/emqx_utils/src/emqx_variform_bif.erl @@ -61,6 +61,21 @@ %% Control functions -export([coalesce/1, coalesce/2]). +%% Random functions +-export([rand_str/1, rand_int/1]). + +%% Schema-less encod/decode +-export([ + bin2hexstr/1, + hexstr2bin/1, + int2hexstr/1, + base64_encode/1, + base64_decode/1 +]). + +%% Hash functions +-export([hash/2, hash_to_range/3, map_to_range/3]). + -define(IS_EMPTY(X), (X =:= <<>> orelse X =:= "" orelse X =:= undefined)). %%------------------------------------------------------------------------------ @@ -389,3 +404,122 @@ is_hex_digit(_) -> false. any_to_str(Data) -> emqx_utils_conv:bin(Data). + +%%------------------------------------------------------------------------------ +%% Random functions +%%------------------------------------------------------------------------------ + +%% @doc Make a random string with urlsafe-base64 charset. +rand_str(Length) when is_integer(Length) andalso Length > 0 -> + RawBytes = erlang:ceil((Length * 3) / 4), + RandomData = rand:bytes(RawBytes), + urlsafe(binary:part(base64_encode(RandomData), 0, Length)); +rand_str(_) -> + throw(#{reason => badarg, function => ?FUNCTION_NAME}). + +%% @doc Make a random integer in the range `[1, N]`. +rand_int(N) when is_integer(N) andalso N >= 1 -> + rand:uniform(N); +rand_int(N) -> + throw(#{reason => badarg, function => ?FUNCTION_NAME, expected => "positive integer", got => N}). + +%% TODO: call base64:encode(Bin, #{mode => urlsafe, padding => false}) +%% when oldest OTP to support is 26 or newer. +urlsafe(Str0) -> + Str = replace(Str0, <<"+">>, <<"-">>), + replace(Str, <<"/">>, <<"_">>). + +%%------------------------------------------------------------------------------ +%% Data encoding +%%------------------------------------------------------------------------------ + +%% @doc Encode an integer to hex string. e.g. 15 as 'f' +int2hexstr(Int) -> + erlang:integer_to_binary(Int, 16). + +%% @doc Encode bytes in hex string format. +bin2hexstr(Bin) when is_binary(Bin) -> + emqx_utils:bin_to_hexstr(Bin, upper); +%% If Bin is a bitstring which is not divisible by 8, we pad it and then do the +%% conversion +bin2hexstr(Bin) when is_bitstring(Bin), (8 - (bit_size(Bin) rem 8)) >= 4 -> + PadSize = 8 - (bit_size(Bin) rem 8), + Padding = <<0:PadSize>>, + BinToConvert = <>, + <<_FirstByte:8, HexStr/binary>> = emqx_utils:bin_to_hexstr(BinToConvert, upper), + HexStr; +bin2hexstr(Bin) when is_bitstring(Bin) -> + PadSize = 8 - (bit_size(Bin) rem 8), + Padding = <<0:PadSize>>, + BinToConvert = <>, + emqx_utils:bin_to_hexstr(BinToConvert, upper). + +%% @doc Decode hex string into its original bytes. +hexstr2bin(Str) when is_binary(Str) -> + emqx_utils:hexstr_to_bin(Str). + +%% @doc Encode any bytes to base64. +base64_encode(Bin) -> + base64:encode(Bin). + +%% @doc Decode base64 encoded string. +base64_decode(Bin) -> + base64:decode(Bin). + +%%------------------------------------------------------------------------------ +%% Hash functions +%%------------------------------------------------------------------------------ + +%% @doc Hash with all available algorithm provided by crypto module. +%% Return hex format string. +%% - md4 | md5 +%% - sha (sha1) +%% - sha224 | sha256 | sha384 | sha512 +%% - sha3_224 | sha3_256 | sha3_384 | sha3_512 +%% - shake128 | shake256 +%% - blake2b | blake2s +hash(<<"sha1">>, Bin) -> + hash(sha, Bin); +hash(Algorithm, Bin) when is_binary(Algorithm) -> + Type = + try + binary_to_existing_atom(Algorithm) + catch + _:_ -> + throw(#{ + reason => unknown_hash_algorithm, + algorithm => Algorithm + }) + end, + hash(Type, Bin); +hash(Type, Bin) when is_atom(Type) -> + %% lower is for backward compatibility + emqx_utils:bin_to_hexstr(crypto:hash(Type, Bin), lower). + +%% @doc Hash binary data to an integer within a specified range [Min, Max] +hash_to_range(Bin, Min, Max) when + is_binary(Bin) andalso + size(Bin) > 0 andalso + is_integer(Min) andalso + is_integer(Max) andalso + Min =< Max +-> + Hash = hash(sha256, Bin), + HashNum = binary_to_integer(Hash, 16), + map_to_range(HashNum, Min, Max); +hash_to_range(_, _, _) -> + throw(#{reason => badarg, function => ?FUNCTION_NAME}). + +map_to_range(Bin, Min, Max) when is_binary(Bin) andalso size(Bin) > 0 -> + HashNum = binary:decode_unsigned(Bin), + map_to_range(HashNum, Min, Max); +map_to_range(Int, Min, Max) when + is_integer(Int) andalso + is_integer(Min) andalso + is_integer(Max) andalso + Min =< Max +-> + Range = Max - Min + 1, + Min + (Int rem Range); +map_to_range(_, _, _) -> + throw(#{reason => badarg, function => ?FUNCTION_NAME}). diff --git a/apps/emqx_utils/test/emqx_variform_bif_tests.erl b/apps/emqx_utils/test/emqx_variform_bif_tests.erl index b74f6fcac..92144ff43 100644 --- a/apps/emqx_utils/test/emqx_variform_bif_tests.erl +++ b/apps/emqx_utils/test/emqx_variform_bif_tests.erl @@ -57,3 +57,18 @@ regex_extract_test_() -> regex_extract(Str, RegEx) -> emqx_variform_bif:regex_extract(Str, RegEx). + +rand_str_test() -> + ?assertEqual(3, size(emqx_variform_bif:rand_str(3))), + ?assertThrow(#{reason := badarg}, size(emqx_variform_bif:rand_str(0))). + +rand_int_test() -> + N = emqx_variform_bif:rand_int(10), + ?assert(N =< 10 andalso N >= 1), + ?assertThrow(#{reason := badarg}, emqx_variform_bif:rand_int(0)), + ?assertThrow(#{reason := badarg}, emqx_variform_bif:rand_int(-1)). + +base64_encode_decode_test() -> + RandBytes = crypto:strong_rand_bytes(100), + Encoded = emqx_variform_bif:base64_encode(RandBytes), + ?assertEqual(RandBytes, emqx_variform_bif:base64_decode(Encoded)). diff --git a/apps/emqx_utils/test/emqx_variform_tests.erl b/apps/emqx_utils/test/emqx_variform_tests.erl index 91da471c9..5f9a13326 100644 --- a/apps/emqx_utils/test/emqx_variform_tests.erl +++ b/apps/emqx_utils/test/emqx_variform_tests.erl @@ -182,3 +182,39 @@ syntax_error_test_() -> render(Expression, Bindings) -> emqx_variform:render(Expression, Bindings). + +hash_pick_test() -> + lists:foreach( + fun(_) -> + {ok, Res} = render("nth(hash_to_range(rand_str(10),1,5),[1,2,3,4,5])", #{}), + ?assert(Res >= <<"1">> andalso Res =< <<"5">>) + end, + lists:seq(1, 100) + ). + +map_to_range_pick_test() -> + lists:foreach( + fun(_) -> + {ok, Res} = render("nth(map_to_range(rand_str(10),1,5),[1,2,3,4,5])", #{}), + ?assert(Res >= <<"1">> andalso Res =< <<"5">>) + end, + lists:seq(1, 100) + ). + +-define(ASSERT_BADARG(FUNC, ARGS), + ?_assertEqual( + {error, #{reason => badarg, function => FUNC}}, + render(atom_to_list(FUNC) ++ ARGS, #{}) + ) +). + +to_range_badarg_test_() -> + [ + ?ASSERT_BADARG(hash_to_range, "(1,1,2)"), + ?ASSERT_BADARG(hash_to_range, "('',1,2)"), + ?ASSERT_BADARG(hash_to_range, "('a','1',2)"), + ?ASSERT_BADARG(hash_to_range, "('a',2,1)"), + ?ASSERT_BADARG(map_to_range, "('',1,2)"), + ?ASSERT_BADARG(map_to_range, "('a','1',2)"), + ?ASSERT_BADARG(map_to_range, "('a',2,1)") + ].