diff --git a/lib/elixir/lib/file.ex b/lib/elixir/lib/file.ex index e3b7166eda2..00feb0bf743 100644 --- a/lib/elixir/lib/file.ex +++ b/lib/elixir/lib/file.ex @@ -1116,6 +1116,12 @@ defmodule File do Special files such as device files, sockets, and named pipes are not copied. + Typical error reasons are: + + * `:enoent` - `source` does not exist + * `:eisdir` - `source` is a file and `destination` is a directory + * `:einval` - `destination` is the same as or a subdirectory of `source` + ## Options * `:on_conflict` - (since v1.14.0) Invoked when a file already exists in the destination. @@ -1146,7 +1152,11 @@ defmodule File do #=> {:ok, ["z.txt", "y.txt", "x.txt]} File.cp_r("non_existing.txt", "copy.txt") - #=> {:error, :enoent} + #=> {:error, :enoent, "non_existing.txt"} + + # Copying into a subdirectory of source is not allowed + File.cp_r("src", "src/dest") + #=> {:error, :einval, "src/dest"} """ @spec cp_r(Path.t(), Path.t(), on_conflict: on_conflict_callback, @@ -1183,9 +1193,16 @@ defmodule File do |> IO.chardata_to_string() |> assert_no_null_byte!("File.cp_r/3") - case do_cp_r(source, destination, on_conflict, dereference?, []) do - {:error, _, _} = error -> error - res -> {:ok, res} + source_parts = source |> Path.expand() |> Path.split() + dest_parts = destination |> Path.expand() |> Path.split() + + if source_parts != dest_parts and List.starts_with?(dest_parts, source_parts) do + {:error, :einval, destination} + else + case do_cp_r(source, destination, on_conflict, dereference?, []) do + {:error, _, _} = error -> error + res -> {:ok, res} + end end end diff --git a/lib/elixir/test/elixir/file_test.exs b/lib/elixir/test/elixir/file_test.exs index e8a96aa07de..fbb73fbe0e1 100644 --- a/lib/elixir/test/elixir/file_test.exs +++ b/lib/elixir/test/elixir/file_test.exs @@ -799,6 +799,42 @@ defmodule FileTest do end end + test "cp_r with destination inside source returns error" do + src = tmp_path("tmp/src") + dest = tmp_path("tmp/src/subdir/dest") + + File.mkdir_p!(src) + File.write!(Path.join(src, "file.txt"), "hello") + + try do + assert File.cp_r(src, dest) == {:error, :einval, dest} + refute File.exists?(dest) + after + File.rm_rf(src) + end + end + + test "cp_r! with destination inside source raises" do + src = tmp_path("tmp/src") + dest = tmp_path("tmp/src/subdir/dest") + + File.mkdir_p!(src) + File.write!(Path.join(src, "file.txt"), "hello") + + try do + message = + "could not copy recursively from #{inspect(src)} to #{inspect(dest)}. #{dest}: invalid argument" + + assert_raise File.CopyError, message, fn -> + File.cp_r!(src, dest) + end + + refute File.exists?(dest) + after + File.rm_rf(src) + end + end + test "cp preserves mode" do File.mkdir_p!(tmp_path("tmp")) src = fixture_path("cp_mode")