Skip to content

Commit 1a2c0a2

Browse files
committed
Add checksum to manifest files
Closes elixir-lang#14866.
1 parent bd3d68c commit 1a2c0a2

File tree

3 files changed

+56
-5
lines changed

3 files changed

+56
-5
lines changed

lib/mix/lib/mix/compilers/elixir.ex

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,8 @@ defmodule Mix.Compilers.Elixir do
311311
"""
312312
def read_manifest(manifest) do
313313
try do
314-
manifest |> File.read!() |> :erlang.binary_to_term()
314+
{:ok, contents} = Mix.Task.Compiler.read_checksumed_file(manifest)
315+
:erlang.binary_to_term(contents)
315316
rescue
316317
_ -> {[], []}
317318
else
@@ -888,7 +889,8 @@ defmodule Mix.Compilers.Elixir do
888889
# Similar to read_manifest, but for internal consumption and with data migration support.
889890
defp parse_manifest(manifest, compile_path) do
890891
try do
891-
manifest |> File.read!() |> :erlang.binary_to_term()
892+
{:ok, contents} = Mix.Task.Compiler.read_checksumed_file(manifest)
893+
:erlang.binary_to_term(contents)
892894
rescue
893895
_ ->
894896
@default_manifest
@@ -958,7 +960,7 @@ defmodule Mix.Compilers.Elixir do
958960
project_mtime, config_mtime, protocols_and_impls}
959961

960962
manifest_data = :erlang.term_to_binary(term, [:compressed])
961-
File.write!(manifest, manifest_data)
963+
:ok = Mix.Task.Compiler.write_checksumed_file(manifest, manifest_data)
962964
File.touch!(manifest, timestamp)
963965
delete_checkpoints(manifest)
964966

lib/mix/lib/mix/task.compiler.ex

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,4 +370,51 @@ defmodule Mix.Task.Compiler do
370370
Mix.Task.reenable("compile.all")
371371
Enum.each(compilers, &Mix.Task.reenable("compile.#{&1}"))
372372
end
373+
374+
@checksum_version 1
375+
376+
@doc """
377+
Reads a checksumed file.
378+
379+
This function is useful to read files with checksums
380+
and validate they won't get corrupted with time.
381+
"""
382+
def read_checksumed_file(path) do
383+
case File.read(path) do
384+
{:ok, <<@checksum_version, size::64, checksum::binary-size(size), contents::binary>>} ->
385+
if checksum(contents) == checksum do
386+
{:ok, contents}
387+
else
388+
{:error, :echecksum}
389+
end
390+
391+
{:error, reason} ->
392+
{:error, reason}
393+
end
394+
end
395+
396+
@doc """
397+
Writes a checksumed file.
398+
399+
This function is useful to write compilation manifests
400+
and validate they won't get corrupted with time.
401+
"""
402+
def write_checksumed_file(path, contents) do
403+
checksum = checksum(contents)
404+
405+
File.write(
406+
path,
407+
<<@checksum_version, byte_size(checksum)::64, checksum::binary, contents::binary>>
408+
)
409+
end
410+
411+
defp checksum(contents) do
412+
case :erlang.system_info(:wordsize) do
413+
8 -> :crypto.hash(:blake2b, contents)
414+
_ -> :crypto.hash(:blake2s, contents)
415+
end
416+
rescue
417+
# Blake may not be available on all OpenSSL distribution
418+
_ -> :erlang.md5(contents)
419+
end
373420
end

lib/mix/test/mix/tasks/compile.elixir_test.exs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -653,11 +653,13 @@ defmodule Mix.Tasks.Compile.ElixirTest do
653653

654654
manifest = "_build/dev/lib/sample/.mix/compile.elixir"
655655

656-
File.read!(manifest)
656+
{:ok, contents} = Mix.Task.Compiler.read_checksumed_file(manifest)
657+
658+
contents
657659
|> :erlang.binary_to_term()
658660
|> put_elem(0, 9)
659661
|> :erlang.term_to_binary()
660-
|> then(&File.write!(manifest, &1))
662+
|> then(&Mix.Task.Compiler.write_checksumed_file(manifest, &1))
661663

662664
Mix.Task.clear()
663665
assert Mix.Task.run("compile", ["--verbose"]) == {:ok, []}

0 commit comments

Comments
 (0)