Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/elixir_sense/core/exck_reader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ defmodule ElixirSense.Core.ExCkReader do
_ ->
{:error, :not_found}
end
catch
# `:beam_lib.chunks/2` raises on malformed beams instead of returning an
# error tuple; fall back to the binary path (which also handles that case).
_, _ -> {:error, :not_found}
end

defp fetch_from_binary(module) do
Expand Down
12 changes: 9 additions & 3 deletions lib/elixir_sense/core/normalized/code.ex
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ defmodule ElixirSense.Core.Normalized.Code do
def callback_documentation(module) do
app = ElixirSense.Core.Applications.get_application(module)

case Code.fetch_docs(module) do
case fetch_docs(module) do
{:docs_v1, _moduledoc_anno, _language, mime_type, _moduledoc, _metadata, docs}
when mime_type in @supported_mime_types ->
docs
Expand Down Expand Up @@ -247,6 +247,12 @@ defmodule ElixirSense.Core.Normalized.Code do
metadata: map
def fetch_docs(module_or_path)

def fetch_docs(module) when is_atom(module), do: Code.fetch_docs(module)
def fetch_docs(path) when is_binary(path), do: Code.fetch_docs(path)
def fetch_docs(module_or_path) when is_atom(module_or_path) or is_binary(module_or_path) do
Code.fetch_docs(module_or_path)
catch
# `Code.fetch_docs/1` delegates to `:beam_lib`, which only catches throws.
# A malformed or truncated .beam raises (e.g. MatchError) instead of
# returning an error tuple, so treat any failure as missing docs.
_, _ -> {:error, :chunk_not_found}
end
end
25 changes: 25 additions & 0 deletions test/elixir_sense/core/exck_reader_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,31 @@ defmodule ElixirSense.Core.ExCkReaderTest do
test "handles invalid chunk gracefully" do
assert {:error, :invalid_chunk} = ExCkReader.read_chunk(:fake, chunk: <<1, 2, 3>>)
end

@tag :tmp_dir
test "returns error instead of raising when the beam file is corrupt", %{tmp_dir: tmp_dir} do
module = :ElixirSenseCorruptBeamExCkProbe
{_, beam, _} = :code.get_object_code(ElixirSenseExample.ModuleWithTypes)

# Flip a byte inside the atom table chunk so `:beam_lib.chunks/2` raises
# on the on-disk path instead of returning an error tuple.
<<head::binary-size(25), byte, rest::binary>> = beam
corrupt = <<head::binary, Bitwise.bxor(byte, 0xFF), rest::binary>>

File.write!(Path.join(tmp_dir, "#{module}.beam"), corrupt)
tmp_dir_charlist = String.to_charlist(tmp_dir)
:code.add_patha(tmp_dir_charlist)
on_exit(fn -> :code.del_path(tmp_dir_charlist) end)

# The ExCk chunk itself is intact, so the raw-binary fallback may still
# decode it; the contract under test is only that nothing raises.
assert match?(
{tag, _} when tag in [:ok, :error],
ExCkReader.read_chunk(module, cache: false)
)

assert :error == ExCkReader.lookup_signature(module, :foo, 0)
end
end

test "lookup_signature returns cached entries" do
Expand Down
25 changes: 25 additions & 0 deletions test/elixir_sense/core/normalized/code_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,29 @@ defmodule ElixirSense.Core.Normalized.CodeTest do
assert is_list(Code.get_docs(:gen_server, :callback_docs))
assert is_tuple(Code.get_docs(:lists, :moduledoc))
end

describe "malformed beam files" do
@tag :tmp_dir
test "return nil instead of raising when the beam is corrupt", %{tmp_dir: tmp_dir} do
module = :ElixirSenseCorruptBeamDocsProbe
{_, beam, _} = :code.get_object_code(ElixirSenseExample.ModuleWithTypes)

# Flip a byte inside the atom table chunk: `:beam_lib.read_info/1` raises a
# MatchError here instead of returning an error tuple, which used to escape
# `Code.fetch_docs/1`.
<<head::binary-size(25), byte, rest::binary>> = beam
corrupt = <<head::binary, Bitwise.bxor(byte, 0xFF), rest::binary>>

File.write!(Path.join(tmp_dir, "#{module}.beam"), corrupt)
tmp_dir_charlist = String.to_charlist(tmp_dir)
:code.add_patha(tmp_dir_charlist)
on_exit(fn -> :code.del_path(tmp_dir_charlist) end)

assert {:error, _} = Code.fetch_docs(module)
assert nil == Code.get_docs(module, :docs)
assert nil == Code.get_docs(module, :type_docs)
assert nil == Code.get_docs(module, :callback_docs)
assert nil == Code.get_docs(module, :moduledoc)
end
end
end
Loading