Skip to content

Commit

Permalink
Don't follow symlinks when adding files to tarballs
Browse files Browse the repository at this point in the history
Path.wildcard("**") follows symlinks when collecting all of the files
under a path. This can lead to files in the tarball containing paths
with the symlink in them. If the directory corresponding to the symlink
hasn't been created, then the extraction will fail.

As an example, the "create with files" unit test now contains a symlink
in a directory. Without the fix, it can fail like this:

```
  1) test create with files (Mix.Tasks.Hex.BuildTest)
     test/mix/tasks/hex.build_test.exs:42
     ** (MatchError) no match of right hand side value: {:error, :eexist}
     code: in_tmp(fn ->
     stacktrace:
       test/mix/tasks/hex.build_test.exs:13: Mix.Tasks.Hex.BuildTest.extract/2
       test/mix/tasks/hex.build_test.exs:69: anonymous fn/0 in Mix.Tasks.Hex.BuildTest."test create with files"/1
       (elixir) lib/file.ex:1443: File.cd!/2
       test/mix/tasks/hex.build_test.exs:45: (test)
```

Untaring the `contents.tar.gz` shows the problem.

```sh
$ tar tfz contents.tar.gz
myfile.txt
executable.sh
dir/.dotfile
dir/a_link_to_dir2
dir/a_link_to_dir2/test.txt
dir/dir2/test.txt
empty_dir/
link_dir
```

`dir/a_link_to_dir2` is created as a symlink to `dir/dir2`. The
`test.txt` file is then extracted to it. This fails since `dir2` hasn't
been created yet so `a_link_to_dir2` is dangling. It's also not
desirable that `test.txt` was included twice.

After the fix, the `contents.tar.gz` looks like this:

```sh
$ tar tfz contents.tar.gz
myfile.txt
executable.sh
dir/a_link_to_dir2
dir/dir2/test.txt
dir/.dotfile
empty_dir/
link_dir
```

Fixes hexpm#631.
  • Loading branch information
fhunleth committed Nov 28, 2018
1 parent 0c5b2fa commit 42ac290
Show file tree
Hide file tree
Showing 2 changed files with 12 additions and 1 deletion.
7 changes: 6 additions & 1 deletion lib/mix/tasks/hex.build.ex
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,12 @@ defmodule Mix.Tasks.Hex.Build do
defp dir_files(path) do
case Hex.file_lstat(path) do
{:ok, %File.Stat{type: :directory}} ->
[path | Path.wildcard(Path.join(path, "**"), match_dot: true)]
new_paths =
path
|> File.ls!()
|> Enum.map(&Path.join(path, &1))
|> Enum.flat_map(&dir_files/1)
[path | new_paths]

_ ->
[path]
Expand Down
6 changes: 6 additions & 0 deletions test/mix/tasks/hex.build_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ defmodule Mix.Tasks.Hex.BuildTest do
File.mkdir!("dir")
File.mkdir!("empty_dir")
File.write!("dir/.dotfile", "")
File.ln_s("dir2", "dir/a_link_to_dir2")
File.mkdir!("dir/dir2")
File.ln_s("empty_dir", "link_dir")

# mtime_dir = File.stat!("dir").mtime
Expand All @@ -57,8 +59,10 @@ defmodule Mix.Tasks.Hex.BuildTest do

File.write!("myfile.txt", "hello")
File.write!("executable.sh", "world")
File.write!("dir/dir2/test.txt", "and")
File.chmod!("myfile.txt", 0o100644)
File.chmod!("executable.sh", 0o100755)
File.chmod!("dir/dir2/test.txt", 0o100644)

Mix.Tasks.Hex.Build.run([])

Expand All @@ -72,9 +76,11 @@ defmodule Mix.Tasks.Hex.BuildTest do
assert File.stat!("unzip/link_dir").mtime != mtime_link

assert Hex.file_lstat!("unzip/link_dir").type == :symlink
assert Hex.file_lstat!("unzip/dir/a_link_to_dir2").type == :symlink
assert Hex.file_lstat!("unzip/empty_dir").type == :directory
assert File.read!("unzip/myfile.txt") == "hello"
assert File.read!("unzip/dir/.dotfile") == ""
assert File.read!("unzip/dir/dir2/test.txt") == "and"
assert File.stat!("unzip/myfile.txt").mode == 0o100644
assert File.stat!("unzip/executable.sh").mode == 0o100755
end)
Expand Down

0 comments on commit 42ac290

Please sign in to comment.