%% This Source Code Form is subject to the terms of the Mozilla Public %% License, v. 2.0. If a copy of the MPL was not distributed with this %% file, You can obtain one at https://mozilla.org/MPL/2.0/. %% %% Copyright (c) 2017-2023 Broadcom. All Rights Reserved. The term Broadcom refers to Broadcom Inc. and/or its subsidiaries. %% %% @hidden -module(ra_lib). -export([ ceiling/1, default/2, lazy_default/2, dump/1, dump/2, id/1, % maybe iter_maybe/2, % cohercion to_list/1, to_binary/1, to_string/1, to_atom/1, ra_server_id_to_local_name/1, ra_server_id_node/1, update_element/3, zpad_hex/1, zpad_filename/3, zpad_filename_incr/1, zpad_extract_num/1, recursive_delete/1, make_uid/0, make_uid/1, make_dir/1, derive_safe_string/2, validate_base64uri/1, partition_parallel/2, partition_parallel/3, retry/2, retry/3, write_file/2, write_file/3, sync_file/1, lists_chunk/2, lists_detect_sort/1, lists_shuffle/1, is_dir/1, is_file/1, ensure_dir/1, consult/1, maps_foreach/2, maps_merge_with/3 ]). -type file_err() :: file:posix() | badarg | terminated | system_limit. -export_type([file_err/0]). -include_lib("kernel/include/file.hrl"). ceiling(X) when X < 0 -> trunc(X); ceiling(X) -> T = trunc(X), case X-T =:= 0 of true -> T; false -> T + 1 end. default(undefined, Def) -> Def; default(Value, _Def) -> Value. -spec lazy_default(undefined | term(), fun (() -> term())) -> term(). lazy_default(undefined, DefGen) -> DefGen(); lazy_default(Value, _DefGen) -> Value. dump(Term) -> dump("Dump", Term). dump(Prefix, Term) -> io:format("~p: ~p", [Prefix, Term]), Term. id(X) -> X. -spec iter_maybe(undefined | term(), fun()) -> ok. iter_maybe(undefined, _F) -> ok; iter_maybe(M, F) -> _ = F(M), ok. -spec to_list(atom() | binary() | list() | integer()) -> list(). to_list(A) when is_atom(A) -> atom_to_list(A); to_list(B) when is_binary(B) -> binary_to_list(B); to_list(I) when is_integer(I) -> integer_to_list(I); to_list(L) when is_list(L) -> L. -spec to_binary(atom() | binary() | list() | integer()) -> binary(). to_binary(B) when is_binary(B) -> B; to_binary(A) when is_atom(A) -> atom_to_binary(A, utf8); to_binary(I) when is_integer(I) -> integer_to_binary(I); to_binary(L) when is_list(L) -> list_to_binary(L). -spec to_string(binary() | string()) -> string(). to_string(B) when is_binary(B) -> binary_to_list(B); to_string(L) when is_list(L) -> L. -spec to_atom(atom() | list() | binary()) -> atom(). to_atom(A) when is_atom(A) -> A; to_atom(B) when is_binary(B) -> list_to_atom(binary_to_list(B)); to_atom(L) when is_list(L) -> list_to_atom(L). ra_server_id_to_local_name({Name, _}) -> Name; ra_server_id_to_local_name(Name) when is_atom(Name) -> Name. ra_server_id_node({_Name, Node}) -> Node; ra_server_id_node(Name) when is_atom(Name) -> node(). update_element(Index, T, Update) when is_tuple(T) -> setelement(Index, T, Update(element(Index, T))). zpad_hex(Num) -> lists:flatten(io_lib:format("~16.16.0B", [Num])). zpad_filename("", Ext, Num) -> lists:flatten(io_lib:format("~8..0B.~ts", [Num, Ext])); zpad_filename(Prefix, Ext, Num) -> lists:flatten(io_lib:format("~ts_~8..0B.~ts", [Prefix, Num, Ext])). zpad_filename_incr(Fn) -> Base = filename:basename(Fn), Dir = filename:dirname(Fn), case re:run(Base, "(.*)([0-9]{8})(.*)", [{capture, all_but_first, list}]) of {match, [Prefix, NumStr, Ext]} -> Num = list_to_integer(NumStr), filename:join(Dir, lists:flatten( io_lib:format("~ts~8..0B~ts", [Prefix, Num+1, Ext]))); _ -> undefined end. zpad_extract_num(Fn) -> {match, [_, NumStr, _]} = re:run(Fn, "(.*)([0-9]{8})(.*)", [{capture, all_but_first, list}]), list_to_integer(NumStr). recursive_delete(Dir) -> case is_dir(Dir) of true -> case prim_file:list_dir(Dir) of {ok, Files} -> Fun = fun(F) -> recursive_delete(filename:join([Dir, F])) end, lists:foreach(Fun, Files), delete(Dir, directory); {error, enoent} -> ok; {error, Reason} -> Text = file:format_error(Reason), throw_error("delete file ~ts: ~ts\n", [Dir, Text]) end; false -> delete(Dir, regular) end. delete(File, Type) -> case do_delete(File, Type) of ok -> ok; {error, enoent} -> ok; {error, Reason} -> Text = file:format_error(Reason), throw_error("delete file ~ts: ~ts\n", [File, Text]) end. do_delete(File, regular) -> prim_file:delete(File); do_delete(Dir, directory) -> prim_file:del_dir(Dir). -spec throw_error(string(), list()) -> no_return(). throw_error(Format, Args) -> throw({error, lists:flatten(io_lib:format(Format, Args))}). %% "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" -define(GENERATED_UID_CHARS, {65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57}). -define(BASE64_URI_CHARS, "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "0123456789_-="). -define(UID_LENGTH, 12). -spec make_uid() -> binary(). make_uid() -> make_uid(<<>>). -spec make_uid(atom() | binary() | string()) -> binary(). make_uid(Prefix0) -> ChrsSize = size(?GENERATED_UID_CHARS), F = fun(_, R) -> [element(rand:uniform(ChrsSize), ?GENERATED_UID_CHARS) | R] end, Prefix = to_binary(Prefix0), B = list_to_binary(lists:foldl(F, "", lists:seq(1, ?UID_LENGTH))), <>. -spec make_dir(file:name_all()) -> ok | {error, file:posix() | badarg}. make_dir(Dir) -> case is_dir(Dir) of true -> ok; false -> handle_ensure_dir(ensure_dir(Dir), Dir) end. handle_ensure_dir(ok, Dir) -> handle_make_dir(prim_file:make_dir(Dir)); handle_ensure_dir(Error, _Dir) -> Error. handle_make_dir(ok) -> ok; handle_make_dir({error, eexist}) -> ok; handle_make_dir(Error) -> Error. -spec validate_base64uri(string()) -> boolean(). validate_base64uri(Str) -> catch begin [begin case lists:member(C, ?BASE64_URI_CHARS) of true -> ok; false -> throw(false) end end || C <- string:to_graphemes(Str)], string:is_empty(Str) == false end. derive_safe_string(S, Num) -> F = fun Take([], Acc) -> string:reverse(Acc); Take([G | Rem], Acc) -> case lists:member(G, ?BASE64_URI_CHARS) of true -> Take(string:next_grapheme(Rem), [G | Acc]); false -> Take(string:next_grapheme(Rem), Acc) end end, string:slice(F(string:next_grapheme(S), []), 0, Num). partition_parallel(F, Es) -> partition_parallel(F, Es, 60000). partition_parallel(F, Es, Timeout) -> Parent = self(), Running = [{spawn_monitor(fun() -> Parent ! {self(), F(E)} end), E} || E <- Es], collect(Running, {[], []}, Timeout). collect([], Acc, _Timeout) -> Acc; collect([{{Pid, MRef}, E} | Next], {Left, Right}, Timeout) -> receive {Pid, true} -> erlang:demonitor(MRef, [flush]), collect(Next, {[E | Left], Right}, Timeout); {Pid, false} -> erlang:demonitor(MRef, [flush]), collect(Next, {Left, [E | Right]}, Timeout); {'DOWN', MRef, process, Pid, _Reason} -> collect(Next, {Left, [E | Right]}, Timeout) after Timeout -> exit(partition_parallel_timeout) end. retry(Func, Attempts) -> retry(Func, Attempts, 5000). retry(_Func, 0, _Sleep) -> exhausted; retry(Func, Attempt, Sleep) -> % do not retry immediately timer:sleep(Sleep), case catch Func() of ok -> ok; true -> ok; _ -> retry(Func, Attempt - 1) end. -spec write_file(file:name_all(), iodata()) -> ok | {error, file_err()}. write_file(Name, IOData) -> write_file(Name, IOData, true). -spec write_file(file:name_all(), iodata(), Sync :: boolean()) -> ok | {error, file_err()}. write_file(Name, IOData, Sync) -> case file:open(Name, [binary, write, raw]) of {ok, Fd} -> case file:write(Fd, IOData) of ok -> case Sync of true -> sync_and_close_fd(Fd); false -> ok end; Err -> _ = file:close(Fd), Err end; Err -> Err end. -spec sync_file(file:name_all()) -> ok | {error, file_err()}. sync_file(Name) -> case file:open(Name, [binary, read, write, raw]) of {ok, Fd} -> sync_and_close_fd(Fd); Err -> Err end. -spec sync_and_close_fd(file:fd()) -> ok | {error, file_err()}. sync_and_close_fd(Fd) -> case ra_file:sync(Fd) of ok -> file:close(Fd); Err -> _ = file:close(Fd), Err end. lists_chunk(0, List) -> error(invalid_size, [0, List]); lists_chunk(Size, List) -> lists_chunk(Size, List, []). lists_chunk(_Size, [], Acc) -> lists:reverse(Acc); lists_chunk(Size, List, Acc) when length(List) < Size -> lists:reverse([List | Acc]); lists_chunk(Size, List, Acc) -> {L, Rem} = lists_take(Size, List, []), lists_chunk(Size, Rem, [L | Acc]). lists_take(0, List, Acc) -> {lists:reverse(Acc), List}; lists_take(_N, [], Acc) -> {lists:reverse(Acc), []}; lists_take(N, [H | T], Acc) -> lists_take(N-1, T, [H | Acc]). lists_detect_sort([]) -> undefined; lists_detect_sort([_]) -> undefined; lists_detect_sort([A | [A | _] = Rem]) -> %% direction not determined yet lists_detect_sort(Rem); lists_detect_sort([A, B | Rem]) when A > B -> do_descending(B, Rem); lists_detect_sort([A, B | Rem]) when A < B -> do_ascending(B, Rem). do_descending(_A, []) -> descending; do_descending(A, [B | Rem]) when B =< A -> do_descending(B, Rem); do_descending(_A, _) -> unsorted. do_ascending(_A, []) -> ascending; do_ascending(A, [B | Rem]) when B >= A -> do_ascending(B, Rem); do_ascending(_A, _) -> unsorted. %% Reorder a list randomly. -spec lists_shuffle(list()) -> list(). lists_shuffle(List0) -> List1 = [{rand:uniform(), Elem} || Elem <- List0], [Elem || {_, Elem} <- lists:keysort(1, List1)]. is_dir(Dir) -> case prim_file:read_file_info(Dir) of {ok, #file_info{type=directory}} -> true; _ -> false end. is_file(File) -> case prim_file:read_file_info(File) of {ok, #file_info{type = directory}} -> true; {ok, #file_info{type = regular}} -> true; _ -> false end. -spec consult(file:filename()) -> {ok, term()} | {error, term()}. consult(Path) -> case prim_file:read_file(Path) of {ok, Data} -> Str = erlang:binary_to_list(Data), tokens(Str); Err -> Err end. tokens(Str) -> case erl_scan:string(Str) of {ok, Tokens, _EndLoc} -> erl_parse:parse_term(Tokens); {error, Err, _ErrLoc} -> {error, Err} end. %% raw copy of ensure_dir ensure_dir("/") -> ok; ensure_dir(F) -> Dir = filename:dirname(F), case is_dir(Dir) of true -> ok; false when Dir =:= F -> %% Protect against infinite loop {error, einval}; false -> _ = ensure_dir(Dir), case prim_file:make_dir(Dir) of {error, eexist} = EExist -> case is_dir(Dir) of true -> ok; false -> EExist end; Err -> Err end end. %% because OTP 23 support maps_foreach(Fun, {K, V, I}) -> Fun(K, V), maps_foreach(Fun, maps:next(I)); maps_foreach(_Fun, none) -> ok; maps_foreach(Fun, Map) when is_map(Map) -> maps_foreach(Fun, maps:next(maps:iterator(Map))). %% copied from OTP to provide 23 compat, can be removed when we drop %% support for 23 maps_merge_with(Combiner, Map1, Map2) when is_map(Map1), is_map(Map2), is_function(Combiner, 3) -> case map_size(Map1) > map_size(Map2) of true -> Iterator = maps:iterator(Map2), merge_with_1(maps:next(Iterator), Map1, Map2, Combiner); false -> Iterator = maps:iterator(Map1), merge_with_1(maps:next(Iterator), Map2, Map1, fun(K, V1, V2) -> Combiner(K, V2, V1) end) end. merge_with_1({K, V2, Iterator}, Map1, Map2, Combiner) -> case Map1 of #{ K := V1 } -> NewMap1 = Map1#{ K := Combiner(K, V1, V2) }, merge_with_1(maps:next(Iterator), NewMap1, Map2, Combiner); #{ } -> merge_with_1(maps:next(Iterator), maps:put(K, V2, Map1), Map2, Combiner) end; merge_with_1(none, Result, _, _) -> Result. -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). lists_chunk_test() -> ?assertError(invalid_size, lists_chunk(0, [a])), ?assertMatch([], lists_chunk(2, [])), ?assertMatch([[a]], lists_chunk(2, [a])), ?assertMatch([[a, b]], lists_chunk(2, [a, b])), ?assertMatch([[a, b], [c]], lists_chunk(2, [a, b, c])), ?assertMatch([[a, b], [c, d]], lists_chunk(2, [a, b, c, d])), ok. make_uid_test() -> U1 = make_uid(), U2 = make_uid(), ?debugFmt("U1 ~ts U2 ~s", [U1, U2]), ?assertNotEqual(U1, U2), <<"ABCD", _/binary>> = make_uid("ABCD"), <<"ABCD", _/binary>> = make_uid(<<"ABCD">>), ok. zpad_filename_incr_test() -> Fn = "/lib/blah/prefix_00000001.segment", Ex = "/lib/blah/prefix_00000002.segment", Ex = zpad_filename_incr(Fn), undefined = zpad_filename_incr("0000001"), ok. zpad_filename_incr_utf8_test() -> Fn = "/lib/🐰/prefix/00000001.segment", Ex = "/lib/🐰/prefix/00000002.segment", Ex = zpad_filename_incr(Fn), undefined = zpad_filename_incr("0000001"), ok. derive_safe_string_test() -> S = <<"bønana"/utf8>>, S2 = "bønana", [] = derive_safe_string(<<"">>, 4), "bnan" = derive_safe_string(S, 4), "bnan" = derive_safe_string(S2, 4), ok. validate_base64uri_test() -> false = validate_base64uri(""), %% false true = validate_base64uri(?BASE64_URI_CHARS), false = validate_base64uri("asdføasdf"), false = validate_base64uri("asdf/asdf"), ok. zeropad_test() -> "0000000000000037" = zpad_hex(55), ok. lists_detect_sort_test() -> ?assertEqual(undefined, lists_detect_sort([])), ?assertEqual(undefined, lists_detect_sort([1])), ?assertEqual(undefined, lists_detect_sort([1, 1])), ?assertEqual(ascending, lists_detect_sort([1, 1, 2])), ?assertEqual(unsorted, lists_detect_sort([1, 2, 1])), ?assertEqual(unsorted, lists_detect_sort([2, 1, 2])), ?assertEqual(descending, lists_detect_sort([2, 1])), ?assertEqual(descending, lists_detect_sort([2, 2, 1])), ?assertEqual(ascending, lists_detect_sort([1, 1, 2])), ?assertEqual(ascending, lists_detect_sort([1, 2, 3, 4, 6, 6])), ?assertEqual(unsorted, lists_detect_sort([1, 2, 3, 4, 6, 5])), ok. -endif.