Forráskód Böngészése

feat(variform): Add more functions

zmstone 1 éve
szülő
commit
f80d078de3

+ 8 - 20
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 = <<Padding/bitstring, Bin/bitstring>>,
-    <<_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 = <<Padding/bitstring, Bin/bitstring>>,
-    emqx_utils:bin_to_hexstr(BinToConvert, upper).
-
-hexstr2bin(Str) when is_binary(Str) ->
-    emqx_utils:hexstr_to_bin(Str).
+bin2hexstr(Bin) ->
+    emqx_variform_bif:bin2hexstr(Bin).
+
+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

+ 134 - 0
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 = <<Padding/bitstring, Bin/bitstring>>,
+    <<_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 = <<Padding/bitstring, Bin/bitstring>>,
+    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}).

+ 15 - 0
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)).

+ 36 - 0
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)")
+    ].