Skip to content

Commit ae94e6e

Browse files
committed
Add checksum to manifest files
Closes #14866.
1 parent 661e537 commit ae94e6e

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
@@ -310,7 +310,8 @@ defmodule Mix.Compilers.Elixir do
310310
"""
311311
def read_manifest(manifest) do
312312
try do
313-
manifest |> File.read!() |> :erlang.binary_to_term()
313+
{:ok, contents} = Mix.Task.Compiler.read_checksumed_file(manifest)
314+
:erlang.binary_to_term(contents)
314315
rescue
315316
_ -> {[], []}
316317
else
@@ -887,7 +888,8 @@ defmodule Mix.Compilers.Elixir do
887888
# Similar to read_manifest, but for internal consumption and with data migration support.
888889
defp parse_manifest(manifest, compile_path) do
889890
try do
890-
manifest |> File.read!() |> :erlang.binary_to_term()
891+
{:ok, contents} = Mix.Task.Compiler.read_checksumed_file(manifest)
892+
:erlang.binary_to_term(contents)
891893
rescue
892894
_ ->
893895
@default_manifest
@@ -957,7 +959,7 @@ defmodule Mix.Compilers.Elixir do
957959
project_mtime, config_mtime, protocols_and_impls}
958960

959961
manifest_data = :erlang.term_to_binary(term, [:compressed])
960-
File.write!(manifest, manifest_data)
962+
:ok = Mix.Task.Compiler.write_checksumed_file(manifest, manifest_data)
961963
File.touch!(manifest, timestamp)
962964
delete_checkpoints(manifest)
963965

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)