defmodule BeamFile do @moduledoc """ An interface to the BEAM file format. This module is mainly a wrapper around Erlangs `:beam_lib`. For more information see the Erlang documentation for the module [`beam_lib`](https://www.erlang.org/doc/man/beam_lib.html) Furthermore, different code representations can be generated from the file. - `BeamFile.abstract_code/1` - `BeamFile.byte_code/1` - `BeamFile.erl_code/1` - `BeamFile.elixir_code/2` - `BeamFile.elixir_quoted/1` To use the functions above with a module name the module must be compiled and loaded. The functions can also be used with the binary of a module. """ alias BeamFile.DebugInfo alias BeamFile.Error @type info :: [ file: Path.t() | binary(), module: module(), chunks: [{charlist(), non_neg_integer(), non_neg_integer()}] ] @type path :: charlist() @type beam :: path() | binary() @type input :: beam() | module() | {:module, module(), binary(), any()} | [{module(), binary()}] @type reason :: any() @typedoc """ Chunk ID. ``` 'Abst' | 'Attr' | 'AtU8' | 'CInf' | 'Dbgi' | 'Dcos' | 'ExCk' | 'ExpT' | 'ImpT' | 'LocT' ``` """ @type chunk_id :: charlist() @type chunk_name :: :abstract_code | :atoms | :attributes | :compile_info | :debug_info | :docs | :elixir_checker | :exports | :imports | :indexed_imports | :labeled_exports | :labeled_locals | :locals @type chunk_ref :: chunk_name | chunk_id @chunk_ids [ ~c"Abst", ~c"AtU8", ~c"Attr", ~c"CInf", ~c"Dbgi", ~c"Docs", ~c"ExCk", ~c"ExpT", ~c"ImpT", ~c"LocT" ] @chunk_names [ :abstract_code, :atoms, :attributes, :compile_info, :debug_info, :docs, :elixir_checker, :exports, :imports, :indexed_imports, :labeled_exports, :labeled_locals, :locals ] @since_default Version.parse_requirement!(">= 0.0.0") @doc """ Returns the `:abstract_code` chunk. ## Examples iex> BeamFile.abstract_code(BeamFile.Example) { :ok, [ {:attribute, 1, :file, {~c"test/fixtures/example.ex", 1}}, {:attribute, 1, :module, BeamFile.Example}, {:attribute, 1, :compile, [:no_auto_import]}, {:attribute, 1, :export, [__info__: 1, hello: 0]}, { :attribute, 1, :spec, { {:__info__, 1}, [ { :type, 1, :fun, [ { :type, 1, :product, [ { :type, 1, :union, [ {:atom, 1, :attributes}, {:atom, 1, :compile}, {:atom, 1, :functions}, {:atom, 1, :macros}, {:atom, 1, :md5}, {:atom, 1, :exports_md5}, {:atom, 1, :module}, {:atom, 1, :deprecated}, {:atom, 1, :struct} ] } ] }, {:type, 1, :any, []} ] } ] } }, { :function, 0, :__info__, 1, [ {:clause, 0, [{:atom, 0, :module}], [], [{:atom, 0, BeamFile.Example}]}, { :clause, 0, [{:atom, 0, :functions}], [], [{:cons, 0, {:tuple, 0, [{:atom, 0, :hello}, {:integer, 0, 0}]}, {nil, 0}}] }, {:clause, 0, [{:atom, 0, :macros}], [], [nil: 0]}, {:clause, 0, [{:atom, 0, :struct}], [], [{:atom, 0, nil}]}, { :clause, 0, [{:atom, 0, :exports_md5}], [], [ { :bin, 0, [ { :bin_element, 0, {:string, 0, [166, 117, 1, 22, 146, 56, 30, 199, 203, 141, 158, 223, 3, 11, 225, 190]}, :default, :default } ] } ] }, { :clause, 0, [{:match, 0, {:var, 0, :Key}, {:atom, 0, :attributes}}], [], [ { :call, 0, {:remote, 0, {:atom, 0, :erlang}, {:atom, 0, :get_module_info}}, [{:atom, 0, BeamFile.Example}, {:var, 0, :Key}] } ] }, { :clause, 0, [{:match, 0, {:var, 0, :Key}, {:atom, 0, :compile}}], [], [ { :call, 0, {:remote, 0, {:atom, 0, :erlang}, {:atom, 0, :get_module_info}}, [{:atom, 0, BeamFile.Example}, {:var, 0, :Key}] } ] }, { :clause, 0, [{:match, 0, {:var, 0, :Key}, {:atom, 0, :md5}}], [], [ { :call, 0, {:remote, 0, {:atom, 0, :erlang}, {:atom, 0, :get_module_info}}, [{:atom, 0, BeamFile.Example}, {:var, 0, :Key}] } ] }, {:clause, 0, [{:atom, 0, :deprecated}], [], [nil: 0]} ] }, {:function, {2, 7}, :hello, 0, [{:clause, {2, 7}, [], [], [{:atom, {2, 7}, :world}]}]} ] } """ @spec abstract_code(input()) :: {:ok, term()} | {:error, any()} def abstract_code(input) do with {:ok, {:raw_abstract_v1, abstract_code}} <- chunk(input, :abstract_code) do {:ok, abstract_code} end end @doc """ Same as `abstract_code/1` but raises `BeamFile.Error` """ @spec abstract_code!(input()) :: term() def abstract_code!(input) do case abstract_code(input) do {:ok, abstract_code} -> abstract_code {:error, reason} -> raise Error, """ Abstract code for #{inspect(input, binaries: :as_binaries)} not available, \ reason: #{inspect(reason, binaries: :as_binaries)}\ """ end end @doc """ Returns chunk data for all chunks. The `type` argument forces the use of `:ids` or `:names`, defaults to `:names`. ## Examples iex> {:ok, chunks} = BeamFile.all_chunks(BeamFile.Example, :names) iex> chunks |> Map.keys() |> Enum.sort() [ :abstract_code, :atoms, :attributes, :compile_info, :debug_info, :docs, :elixir_checker, :exports, :imports, :indexed_imports, :labeled_exports, :labeled_locals, :locals ] iex> {:docs_v1, 1, :elixir, "text/markdown", :none, _meta, docs} = Map.get(chunks, :docs) iex> docs [{{:function, :hello, 0}, 2, ["hello()"], :none, %{source_annos: [{2, 7}]}}] iex> {:ok, chunks} = BeamFile.all_chunks(BeamFile.Example, :ids) iex> chunks |> Map.keys() |> Enum.sort() [~c"Abst", ~c"AtU8", ~c"Attr", ~c"CInf", ~c"Dbgi", ~c"Docs", ~c"ExCk", ~c"ExpT", ~c"ImpT", ~c"LocT"] iex> chunks |> Map.get(~c"Docs") |> is_binary() true """ @spec all_chunks(input(), type :: :names | :ids) :: {:ok, map()} | {:error, any()} def all_chunks(input, type \\ :names) when type in [:ids, :names] do with {:ok, input} <- binary(input) do chunks = case type do :names -> @chunk_names :ids -> @chunk_ids end fetch_chunks(input, chunks, type) end end @doc """ Same as `all_chunks/` but raises `BeamFile.Error` """ @spec all_chunks!(input(), type :: :names | :ids) :: map def all_chunks!(input, type \\ :names) do case all_chunks(input, type) do {:ok, chunks} -> chunks {:error, reason} -> raise Error, """ Chunks for #{inspect(input, binaries: :as_binaries)} not available, \ reason: #{inspect(reason, binaries: :as_binaries)}\ """ end end @doc """ Returns the byte code for the BEAM file. ## Examples iex> {:ok, byte_code} = BeamFile.byte_code(BeamFile.Example) iex> byte_code |> Tuple.to_list() |> Enum.take(3) [ :beam_file, BeamFile.Example, [{:__info__, 1, 2}, {:hello, 0, 11}, {:module_info, 0, 13}, {:module_info, 1, 15}] ] """ @spec byte_code(input()) :: {:ok, term()} | {:error, any()} def byte_code(input) do with {:ok, data} <- binary(input) do case :beam_disasm.file(data) do {:error, mod, reason} -> {:error, {mod, reason}} byte_code -> {:ok, byte_code} end end end @doc """ Same as `byte_code/1` but raises `BeamFile.Error` """ @spec byte_code!(input()) :: term() def byte_code!(input) do case byte_code(input) do {:ok, byte_code} -> byte_code {:error, reason} -> raise Error, """ Byte code for #{inspect(input, binaries: :as_binaries)} not available, \ reason: #{inspect(reason, binaries: :as_binaries)}\ """ end end @doc """ Returns infos for the given chunk reference. ## Examples iex> BeamFile.chunk(BeamFile.Example, :exports) {:ok, [__info__: 1, hello: 0, module_info: 0, module_info: 1]} iex> {:ok, chunk} = BeamFile.chunk(BeamFile.Example, ~c"Dbgi") iex> is_binary(chunk) true """ @spec chunk(input(), chunk_ref()) :: {:ok, term()} | {:error, any()} def chunk(input, chunk) when chunk in @chunk_ids or chunk in @chunk_names do with {:ok, input} <- binary(input) do type = cond do chunk in @chunk_names -> :names chunk in @chunk_ids -> :ids end with {:ok, data} <- fetch_chunks(input, [chunk], type) do [{_key, value}] = Map.to_list(data) {:ok, value} end end end @doc """ Same as `chunk/2` but raises `BeamFile.Error` """ @spec chunk!(input(), chunk_ref()) :: term def chunk!(input, chunk) do case chunk(input, chunk) do {:ok, data} -> data {:error, reason} -> raise Error, """ Chunk #{inspect(chunk)} for #{inspect(input, binaries: :as_binaries)} not available, \ reason: #{inspect(reason, binaries: :as_binaries)}\ """ end end @doc """ Returns the infos from the `:debug_info` chunk. Examples: iex> {:ok, info} = BeamFile.debug_info(BeamFile.Example) iex> info |> Map.get(:definitions) |> hd() |> elem(0) {:hello, 0} iex> Map.get(info, :relative_file) "test/fixtures/example.ex" """ @spec debug_info(input()) :: {:ok, term()} | {:error, any} def debug_info(input) do with {:ok, {:debug_info_v1, backend, data}} <- chunk(input, :debug_info) do case data do {:elixir_v1, debug_info, _meta} -> backend.debug_info(:elixir_v1, debug_info.module, data, []) :none -> {:error, :no_debug_info} end end end @doc """ Same as `debug_info/1` but raises `BeamFile.Error` """ @spec debug_info!(input()) :: term() def debug_info!(input) do case debug_info(input) do {:ok, debug_info} -> debug_info {:error, reason} -> raise Error, """ Debug info for #{inspect(input, binaries: :as_binaries)} not available, \ reason: #{inspect(reason, binaries: :as_binaries)}\ """ end end @doc """ Returns the infos from the `:docs` chunk. ## Options * `format` - the output format. The format `:info` reduces the output to the type and the infos `:since`, `:hidden` and `:deprecated`. The format `since` reduces the output to the type and the info `:since`. * `hidden` - indicates whether only hidden or none hidden items are returned. Without the option hidden and none hidden items are returned. * `deprecated` - indicates whether only deprecated or none deprecated items are returned. Without the option deprecated and none deprecated items are returned. * `since` - expected a version requirement. With `since`, the function returns all items whose since attribute matches `since`. When `since` does not match the module attribute since and not any item in the module matches, the tuple `{:ok, nil}` is returned. ## Examples iex> {:ok, {:none, _meta, docs}} = BeamFile.docs(BeamFile.Example) iex> docs [{{:function, :hello, 0}, 2, ["hello()"], :none, %{source_annos: [{2, 7}]}}] Examples with options: iex> BeamFile.docs(Float, format: :info, deprecated: true) {:ok, {[ {{:function, :to_char_list, 1}, [since: nil, hidden: true, deprecated: true]}, {{:function, :to_char_list, 2}, [since: nil, hidden: true, deprecated: true]}, {{:function, :to_string, 2}, [since: nil, hidden: true, deprecated: true]} ], [since: nil, hidden: false, deprecated: false]}} iex> BeamFile.docs(Date, format: :since, since: "~> 1.12") {:ok, {[ {{:function, :after?, 2}, [since: "1.15.0"]}, {{:function, :before?, 2}, [since: "1.15.0"]}, {{:function, :range, 3}, [since: "1.12.0"]}, {{:function, :shift, 2}, [since: "1.17.0"]} ], [since: nil]}} """ @spec docs(input(), options :: keyword()) :: {:ok, term()} | {:error, any()} def docs(input, options \\ []) do with {:ok, {:docs_v1, _, :elixir, "text/markdown", doc, meta, docs}} <- chunk(input, :docs) do {:ok, docs(doc, meta, docs, options)} end end defp docs(doc, meta, docs, []) do {doc, meta, docs} end defp docs(doc, meta, docs, options) do since = Keyword.get(options, :since, @since_default) deprecated = Keyword.get(options, :deprecated) hidden = Keyword.get(options, :hidden, true) format = Keyword.get(options, :format) docs = docs |> docs_since(since) |> docs_hidden(hidden) |> docs_deprecated(deprecated) if not Enum.empty?(docs) or since_match?(meta, since) do docs_format(doc, meta, docs, format) else nil end end defp docs_since(docs, @since_default), do: docs defp docs_since(docs, since) do Enum.filter(docs, fn doc -> doc |> elem(4) |> since_match?(since) end) end defp docs_hidden(docs, hidden) do Enum.reject(docs, fn doc -> elem(doc, 3) == :hidden && !hidden end) end defp docs_deprecated(docs, nil), do: docs defp docs_deprecated(docs, deprecated) do Enum.filter(docs, fn doc -> meta = elem(doc, 4) deprecated?(meta) == deprecated end) end defp docs_format(doc, meta, docs, nil) do {doc, meta, docs} end defp docs_format(doc, meta, docs, format) do docs = Enum.map(docs, fn {info, _line, _name, doc, meta} -> {info, info(format, doc, meta)} end) {docs, info(format, doc, meta)} end defp info(:info, doc, meta) do [since: Map.get(meta, :since), hidden: doc == :hidden, deprecated: deprecated?(meta)] end defp info(:since, _doc, meta) do [since: Map.get(meta, :since)] end defp deprecated?(meta) do meta |> Map.get(:deprecated) |> is_binary() end defp since_match?(meta, since) when is_map(meta) do meta |> Map.get(:since, "0.0.0") |> normalize_version() |> Version.match?(since) end defp normalize_version(version) do if Regex.match?(~r/^\d+\.\d+$/, version), do: "#{version}.0", else: version end @doc """ Same as `docs/2` but raises `BeamFile.Error` """ @spec docs!(input(), options: keyword()) :: term() def docs!(input, options \\ []) do case docs(input, options) do {:ok, docs} -> docs {:error, reason} -> raise Error, """ Docs for #{inspect(input, binaries: :as_binaries)} not available, \ reason: #{inspect(reason, binaries: :as_binaries)}\ """ end end @doc ~S''' Returns elixir code recreated from the `debug_info` chunk. The recreated code comes with resolved macros and references. For now, types and specs will not be recreated. Options: `:docs`: With `docs: true` the docs will be created. ## Examples iex> BeamFile.elixir_code(BeamFile.Example) { :ok, """ defmodule Elixir.BeamFile.Example do def hello do :world end end\ """ } ''' @spec elixir_code(input(), opts :: keyword()) :: {:ok, String.t()} | {:error, any} def elixir_code(input, opts \\ []) do with {:ok, debug_info} <- debug_info(input), {:ok, docs} <- maybe_docs(input, opts) do code = if docs do DebugInfo.code(debug_info, docs) else debug_info |> DebugInfo.ast(:code) |> Macro.to_string() end {:ok, code} end end defp maybe_docs(input, opts) do if Keyword.get(opts, :docs, false), do: docs(input), else: {:ok, nil} end @doc """ Same as `elixir_code/1` but raises `BeamFile.Error` """ @spec elixir_code!(input(), opts :: keyword()) :: String.t() def elixir_code!(input, opts \\ []) do case elixir_code(input, opts) do {:ok, code} -> code {:error, reason} -> raise Error, """ Elixir code for #{inspect(input, binaries: :as_binaries)} not available, \ reason: #{inspect(reason, binaries: :as_binaries)}\ """ end end @doc """ Returns the extended Elixir AST. """ @spec elixir_quoted(input()) :: {:ok, Macro.t()} | {:error, any()} def elixir_quoted(input) do with {:ok, debug_info} <- debug_info(input) do {:ok, DebugInfo.ast(debug_info)} end end @doc """ Same as `elixir_quoted/1` but raises `BeamFile.Error` """ @spec elixir_quoted!(input()) :: Macro.t() def elixir_quoted!(input) do case elixir_quoted(input) do {:ok, ast} -> ast {:error, reason} -> raise Error, """ Elixir AST for #{inspect(input, binaries: :as_binaries)} not available, \ reason: #{inspect(reason, binaries: :as_binaries)}\ """ end end @doc """ Returns the Erlang code for the BEAM file. ## Examples iex> {:ok, code} = BeamFile.erl_code(BeamFile.Example) iex> code =~ "-module('Elixir.BeamFile.Example')" true """ @spec erl_code(input()) :: {:ok, String.t()} | {:error, any()} def erl_code(input) do with {:ok, abstract_code} <- abstract_code(input) do code = abstract_code |> :erl_syntax.form_list() |> :erl_prettypr.format() |> to_string() {:ok, code} end end @doc """ Same as `erl_code/1` but raises `BeamFile.Error` """ @spec erl_code!(input()) :: String.t() def erl_code!(input) do case erl_code(input) do {:ok, code} -> code {:error, reason} -> raise Error, """ Erlang code for #{inspect(input, binaries: :as_binaries)} not available, \ reason: #{inspect(reason, binaries: :as_binaries)}\ """ end end @doc """ Returns `true` if a BEAM file for the given `module` exists. ## Examples iex> BeamFile.exists?(BeamFile.Example) true iex> BeamFile.exists?(Physics.TOE) false """ @spec exists?(module()) :: boolean() def exists?(module) do case :code.which(module) do :non_existing -> false _path -> true end end @doc """ Returns a keyword list containing some information about a BEAM file. - `:file`: The name of the BEAM file, or the binary from which the information was extracted. - `:module`: The name of the module. - `:chunks`: For each chunk, the identifier and the position and size of the chunk data, in bytes. ## Examples iex> {:ok, info} = BeamFile.info(BeamFile.Example) iex> info[:module] BeamFile.Example iex> info[:chunks] ...> |> Enum.map(fn {id, _pos, _size} -> id end) ...> |> Enum.sort() [ ~c"AtU8", ~c"Attr", ~c"CInf", ~c"Code", ~c"Dbgi", ~c"Docs", ~c"ExCk", ~c"ExpT", ~c"ImpT", ~c"Line", ~c"LitT", ~c"LocT", ~c"StrT", ~c"Type" ] """ @spec info(input()) :: {:ok, info()} | {:error, reason()} def info(input) do with {:ok, input} <- beam(input), {:ok, info} <- do_info(input) do {:ok, info_file(info, input)} end end defp info_file(info, input) when is_list(input) do Keyword.put(info, :file, IO.chardata_to_string(input)) end defp info_file(info, _input), do: info defp do_info(input) do case :beam_lib.info(input) do {:error, :beam_lib, {:file_error, _path, reason}} -> {:error, reason} {:error, :beam_lib, reason} -> {:error, reason} info -> {:ok, info} end end @doc """ Returns the absolute filename for the `module`. If the module cannot be found, `{:error, :non_existing}` is returned. If the module is preloaded, `{:error, :preloaded}` is returned. If the module is Cover-compiled, `{:error, :cover_compiled}` is returned. ## Examples iex> {:ok, path} = BeamFile.which(BeamFile.Example) iex> path =~ "/_build/test/lib/beam_file/ebin/Elixir.BeamFile.Example.beam" """ @spec which(module()) :: {:ok, Path.t()} | {:error, :non_existing | :preloaded | :cover_compiled} def which(module) when is_atom(module) do with {:ok, path} <- path(module) do {:ok, path |> to_string() |> Path.relative_to_cwd()} end end @doc """ Same as `which/1` but raises `BeamFile.Error` """ @spec which!(module()) :: Path.t() def which!(module) when is_atom(module) do case which(module) do {:ok, path} -> path {:error, reason} -> raise Error, "Path for #{module} not available, reason: #{inspect(reason)}." end end @doc """ Returns the `binary` for the given input. """ @spec binary(input()) :: {:ok, binary()} | {:error, File.posix()} | {:error, :non_existing | :preloaded | :cover_compiled} def binary(input) do with {:ok, data} <- beam(input) do if is_list(data) do data |> List.to_string() |> File.read() else {:ok, data} end end end @doc """ Same as `binary/1` but raises `BeamFile.Error` """ @spec binary!(input()) :: binary() def binary!(input) do case binary(input) do {:ok, binary} -> binary {:error, reason} -> raise Error, """ Can not fetch #{inspect(input, binaries: :as_binaries)}, \ reason: #{inspect(reason, binaries: :as_binaries)}\ """ end end @doc false @deprecated "use binary/1 instead" def read(input), do: binary(input) @doc false @deprecated "use binary!/1 instead" def read!(input), do: binary!(input) defp fetch_chunks(input, chunks, type) when type in [:ids, :names] do chunks = Enum.map(chunks, &to_erl_chunk/1) case :beam_lib.chunks(input, chunks, [:allow_missing_chunks]) do {:ok, {_module, data}} -> data = Enum.into(data, %{}, fn item -> to_elixir_data(item, type) end) {:ok, data} {:error, :beam_lib, reason} -> {:error, reason} end end # Some chunks are not available via a chunk-name. For this chunks we need the # chunk-id. defp to_erl_chunk(:docs), do: ~c"Docs" defp to_erl_chunk(:elixir_checker), do: ~c"ExCk" defp to_erl_chunk(chunk), do: chunk # For some chunks we need a transformation. defp to_elixir_data({~c"Docs", :missing_chunk}, :names), do: {:docs, :missing_chunk} defp to_elixir_data({~c"Docs", data}, :names), do: {:docs, :erlang.binary_to_term(data)} defp to_elixir_data({~c"ExCk", :missing_chunk}, :names), do: {:elixir_checker, :missing_chunk} defp to_elixir_data({~c"ExCk", data}, :names), do: {:elixir_checker, :erlang.binary_to_term(data)} defp to_elixir_data(item, _type), do: item defp path(module) when is_atom(module) do case :code.which(module) do [] -> {:error, :non_existing} [_ | _] = path -> {:ok, path} error -> {:error, error} end end defp beam(input) when is_atom(input), do: path(input) defp beam({:module, module, binary, _context}) when is_atom(module), do: {:ok, binary} defp beam(input) when is_list(input), do: chardata(input) defp beam(input) when is_binary(input), do: {:ok, input} defp beam(_input), do: {:error, :invalid_input} defp chardata(input) do _string = IO.chardata_to_string(input) {:ok, input} rescue _error -> {:error, :invalid_input} end end