defmodule QuickBEAM.BeamAPI do @moduledoc false import Bitwise require Record Record.defrecord(:xml_element, Record.extract(:xmlElement, from_lib: "xmerl/include/xmerl.hrl")) Record.defrecord(:xml_text, Record.extract(:xmlText, from_lib: "xmerl/include/xmerl.hrl")) Record.defrecord( :xml_attribute, Record.extract(:xmlAttribute, from_lib: "xmerl/include/xmerl.hrl") ) @version Mix.Project.config()[:version] @spec version([]) :: String.t() def version([]) do @version end @spec sleep_sync([number()]) :: nil def sleep_sync([ms]) when is_number(ms) do Process.sleep(trunc(ms)) nil end @spec hash([term()]) :: non_neg_integer() def hash([data]) do :erlang.phash2(data) end @spec hash(list()) :: non_neg_integer() def hash([data, range]) when is_integer(range) and range > 0 do :erlang.phash2(data, range) end @spec escape_html([String.t()]) :: String.t() def escape_html([str]) when is_binary(str) do escape_html_binary(str, <<>>) end @spec which([String.t()]) :: String.t() | nil def which([bin]) when is_binary(bin) do System.find_executable(bin) end @spec random_uuid_v7([]) :: String.t() def random_uuid_v7([]) do {counter, last_ms} = uuid_atomics() ms = System.system_time(:millisecond) prev_ms = :atomics.get(last_ms, 1) seq = if ms != prev_ms do :atomics.put(last_ms, 1, ms) rand_seq = :rand.uniform(4096) - 1 :atomics.put(counter, 1, rand_seq) rand_seq else :atomics.add_get(counter, 1, 1) end <> = :crypto.strong_rand_bytes(8) <> = <> :io_lib.format( "~8.16.0b-~4.16.0b-~4.16.0b-~4.16.0b-~12.16.0b", [a, b, c, d, e] ) |> IO.iodata_to_binary() end defp uuid_atomics do case :persistent_term.get({__MODULE__, :uuid_atomics}, nil) do nil -> counter = :atomics.new(1, signed: false) last_ms = :atomics.new(1, signed: true) ref = {counter, last_ms} :persistent_term.put({__MODULE__, :uuid_atomics}, ref) ref ref -> ref end end @spec semver_satisfies(list()) :: boolean() def semver_satisfies([version, requirement]) do case {Version.parse(version), Version.parse_requirement(requirement)} do {{:ok, v}, {:ok, r}} -> Version.match?(v, r) _ -> false end end @spec semver_order(list()) :: -1 | 0 | 1 | nil def semver_order([a, b]) do case {Version.parse(a), Version.parse(b)} do {{:ok, va}, {:ok, vb}} -> case Version.compare(va, vb) do :lt -> -1 :eq -> 0 :gt -> 1 end _ -> nil end end @spec nodes([]) :: [String.t()] def nodes([]) do [node() | Node.list()] |> Enum.map(&Atom.to_string/1) end @spec spawn_runtime([String.t()], pid()) :: pid() def spawn_runtime([script], _caller) do {:ok, pid} = QuickBEAM.start() QuickBEAM.eval(pid, script) pid end @spec rpc(list(), pid()) :: term() def rpc([node_name, runtime_name, fn_name | args], _caller) when is_binary(node_name) do target = String.to_existing_atom(node_name) name = String.to_existing_atom(runtime_name) :erpc.call(target, QuickBEAM, :call, [name, fn_name, args]) rescue e -> reraise RuntimeError, [message: "RPC failed: #{Exception.message(e)}"], __STACKTRACE__ end @spec register_name([String.t()], pid()) :: boolean() def register_name([name], caller) when is_binary(name) do atom = String.to_atom(name) Process.register(caller, atom) true rescue _ -> false end @spec whereis([String.t()]) :: pid() | nil def whereis([name]) when is_binary(name) do Process.whereis(String.to_existing_atom(name)) rescue ArgumentError -> nil end @spec link_process([pid()], pid()) :: boolean() def link_process([pid], _caller) when is_pid(pid) do Process.link(pid) true rescue _ -> false end @spec unlink_process([pid()], pid()) :: boolean() def unlink_process([pid], _caller) when is_pid(pid) do Process.unlink(pid) true end @spec system_info([]) :: map() def system_info([]) do %{ schedulers: :erlang.system_info(:schedulers), schedulers_online: :erlang.system_info(:schedulers_online), process_count: :erlang.system_info(:process_count), process_limit: :erlang.system_info(:process_limit), atom_count: :erlang.system_info(:atom_count), atom_limit: :erlang.system_info(:atom_limit), otp_release: :erlang.system_info(:otp_release) |> List.to_string(), memory: :erlang.memory() |> Map.new(fn {k, v} -> {Atom.to_string(k), v} end) } end @pbkdf2_salt_length 16 @pbkdf2_key_length 32 @spec nanoseconds([]) :: integer() def nanoseconds([]) do :erlang.monotonic_time(:nanosecond) end @spec unique_integer([]) :: integer() def unique_integer([]) do :erlang.unique_integer([:monotonic, :positive]) end @spec make_ref([]) :: reference() def make_ref([]) do Kernel.make_ref() end @spec inspect_value([term()]) :: String.t() def inspect_value([value]) do Kernel.inspect(value, pretty: true, width: 80) end @spec xml_parse([String.t()]) :: map() def xml_parse([xml]) when is_binary(xml) do {document, _rest} = :xmerl_scan.string(String.to_charlist(xml), quiet: true) %{element_name(document) => convert_element(document)} catch :exit, reason -> raise ArgumentError, "invalid XML: #{Exception.format_exit(reason)}" end @spec password_hash(list()) :: String.t() def password_hash([password, iterations]) when is_binary(password) and is_integer(iterations) and iterations > 0 do salt = :crypto.strong_rand_bytes(@pbkdf2_salt_length) hash = :crypto.pbkdf2_hmac(:sha256, password, salt, iterations, @pbkdf2_key_length) "$pbkdf2-sha256$#{iterations}$#{Base.encode64(salt)}$#{Base.encode64(hash)}" end @spec password_verify(list()) :: boolean() def password_verify([password, hash_string]) when is_binary(password) and is_binary(hash_string) do case String.split(hash_string, "$", trim: true) do ["pbkdf2-sha256", iterations_str, salt_b64, hash_b64] -> with {iterations, ""} <- Integer.parse(iterations_str), {:ok, salt} <- Base.decode64(salt_b64), {:ok, expected} <- Base.decode64(hash_b64) do derived = :crypto.pbkdf2_hmac(:sha256, password, salt, iterations, byte_size(expected)) :crypto.hash_equals(derived, expected) else _ -> false end _ -> false end end @spec process_info([], pid()) :: map() | nil def process_info([], caller) do case Process.info(caller, [ :memory, :message_queue_len, :reductions, :status, :registered_name, :heap_size, :stack_size, :total_heap_size ]) do nil -> nil info -> %{ memory: Keyword.get(info, :memory), message_queue_len: Keyword.get(info, :message_queue_len), reductions: Keyword.get(info, :reductions), heap_size: Keyword.get(info, :heap_size), stack_size: Keyword.get(info, :stack_size), total_heap_size: Keyword.get(info, :total_heap_size), status: Keyword.get(info, :status) |> Atom.to_string(), registered_name: case Keyword.get(info, :registered_name) do nil -> nil [] -> nil name -> Atom.to_string(name) end } end end defp convert_element(element) do attributes = element |> xml_element(:attributes) |> Enum.reduce(%{}, fn attribute, acc -> Map.put(acc, "@#{attribute_name(attribute)}", attribute_value(attribute)) end) children = element |> xml_element(:content) |> Enum.reduce(%{text: [], elements: %{}}, &reduce_xml_content/2) children = %{children | elements: finalize_children(children.elements)} text = children.text |> Enum.reverse() |> Enum.join(" ") |> String.trim() cond do map_size(attributes) == 0 and map_size(children.elements) == 0 -> text text == "" -> Map.merge(attributes, children.elements) true -> Map.merge(attributes, Map.put(children.elements, "#text", text)) end end defp reduce_xml_content(content, acc) when is_tuple(content) do case elem(content, 0) do :xmlText -> case text_value(content) do "" -> acc value -> %{acc | text: [value | acc.text]} end :xmlElement -> name = element_name(content) value = convert_element(content) %{acc | elements: Map.update(acc.elements, name, value, &prepend_child(&1, value))} _ -> acc end end defp reduce_xml_content(_content, acc), do: acc defp finalize_children(elements) do Map.new(elements, fn {key, [_ | _] = list} -> {key, Enum.reverse(list)} pair -> pair end) end defp prepend_child(existing, value) when is_list(existing), do: [value | existing] defp prepend_child(existing, value), do: [value, existing] defp element_name(element) do case xml_element(element, :nsinfo) do {prefix, local} -> "#{prefix}:#{local}" _ -> xml_element(element, :name) |> Atom.to_string() end end defp attribute_name(attribute) do case xml_attribute(attribute, :nsinfo) do {prefix, local} -> "#{prefix}:#{local}" _ -> xml_attribute(attribute, :name) |> Atom.to_string() end end defp attribute_value(attribute) do attribute |> xml_attribute(:value) |> to_string() end defp text_value(text) do text |> xml_text(:value) |> to_string() |> String.replace(~r/\s+/, " ") |> String.trim() end defp escape_html_binary(<<>>, acc), do: acc defp escape_html_binary(<<"&", rest::binary>>, acc), do: escape_html_binary(rest, <>) defp escape_html_binary(<<"<", rest::binary>>, acc), do: escape_html_binary(rest, <>) defp escape_html_binary(<<">", rest::binary>>, acc), do: escape_html_binary(rest, <>) defp escape_html_binary(<<"\"", rest::binary>>, acc), do: escape_html_binary(rest, <>) defp escape_html_binary(<<"'", rest::binary>>, acc), do: escape_html_binary(rest, <>) defp escape_html_binary(<>, acc), do: escape_html_binary(rest, <>) end