Skip to content

Commit e89b371

Browse files
author
José Valim
committed
Ensure File.cp_r/3 respects trailing / in sources
1 parent ff61a74 commit e89b371

File tree

7 files changed

+63
-26
lines changed

7 files changed

+63
-26
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
* Bug fixes
1010
* [Atom] Inspect `:...` and `:foo@bar` without quoting
11+
* [File] Respect source directories terminating with "/" in `File.cp_r/3` with the same semantics as Unix
1112
* [Kernel] Guarantee nullary funs/macros are allowed in guards
1213
* [Process] Ensure monitoring functions are inlined by the compiler
1314

lib/elixir/lib/file.ex

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ defmodule File do
349349
output =
350350
if dir?(destination) do
351351
mkdir(destination)
352-
FN.join(destination, FN.basename(source))
352+
do_cp_join(destination, source)
353353
else
354354
destination
355355
end
@@ -376,9 +376,10 @@ defmodule File do
376376

377377
@doc %S"""
378378
Copies the contents in source to destination.
379+
379380
Similar to the command `cp -r` in Unix systems,
380381
this function behaves differently depending
381-
if `source` and `destination` are a file or a directory.
382+
if `source` and `destination` are files or directories.
382383
383384
If both are files, it simply copies `source` to
384385
`destination`. However, if `destination` is a directory,
@@ -412,10 +413,10 @@ defmodule File do
412413
File.cp_r "samples", "tmp"
413414
414415
# Copies all files in "samples" to "tmp"
415-
File.cp_r "samples/.", "tmp"
416+
File.cp_r "samples/", "tmp"
416417
417418
# Same as before, but asks the user how to proceed in case of conflicts
418-
File.cp_r "samples/.", "tmp", fn(source, destination) ->
419+
File.cp_r "samples/", "tmp", fn(source, destination) ->
419420
IO.gets("Overwriting #{destination} by #{source}. Type y to confirm.") == "y"
420421
end
421422
@@ -424,7 +425,10 @@ defmodule File do
424425
output =
425426
if dir?(destination) || dir?(source) do
426427
mkdir(destination)
427-
FN.join(destination, FN.basename(source))
428+
case do_cp_last(source) do
429+
?/ -> destination
430+
_ -> do_cp_join(destination, source)
431+
end
428432
else
429433
destination
430434
end
@@ -450,6 +454,23 @@ defmodule File do
450454
end
451455
end
452456

457+
defp do_cp_last(source) when is_atom(source), do: :lists.last(atom_to_list(source))
458+
defp do_cp_last(source) when is_list(source), do: do_cp_last(:lists.last(source))
459+
defp do_cp_last(source) when is_binary(source), do: :binary.last(source)
460+
defp do_cp_last(source) when is_integer(source), do: source
461+
462+
defp do_cp_join(destination, source) when source in [".", '.'] do
463+
destination
464+
end
465+
466+
defp do_cp_join(destination, source) do
467+
case FN.basename(source) do
468+
dot when dot in ["..", '..'] -> do_cp_join(destination, FN.dirname(FN.dirname(source)))
469+
dot when dot in [".", '.'] -> do_cp_join(destination, FN.dirname(source))
470+
base -> FN.join(destination, base)
471+
end
472+
end
473+
453474
# src may be a file or a directory, dest is definitely
454475
# a directory. Returns nil unless an error is found.
455476
defp do_cp_r(src, dest, callback, acc) when is_list(acc) do

lib/elixir/lib/path.ex

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -408,8 +408,8 @@ defmodule Path do
408408
do_join(rest, relativename, [?/|result], os_type)
409409
defp do_join(<<?/, rest :: binary>>, relativename, [?/|result], os_type), do:
410410
do_join(rest, relativename, [?/|result], os_type)
411-
defp do_join(<<>>, <<>>, result, os_type), do:
412-
iolist_to_binary(maybe_remove_dirsep(result, os_type))
411+
defp do_join(<<>>, <<>>, result, _os_type), do:
412+
iolist_to_binary(:lists.reverse(result))
413413
defp do_join(<<>>, relativename, [?:|rest], :win32), do:
414414
do_join(relativename, <<>>, [?:|rest], :win32)
415415
defp do_join(<<>>, relativename, [?/|result], os_type), do:
@@ -419,15 +419,6 @@ defmodule Path do
419419
defp do_join(<<char, rest :: binary>>, relativename, result, os_type) when is_integer(char), do:
420420
do_join(rest, relativename, [char|result], os_type)
421421

422-
defp maybe_remove_dirsep([?/, ?:, letter], :win32), do:
423-
[letter, ?:, ?/]
424-
defp maybe_remove_dirsep([?/], _), do:
425-
[?/]
426-
defp maybe_remove_dirsep([?/|name], _), do:
427-
:lists.reverse(name)
428-
defp maybe_remove_dirsep(name, _), do:
429-
:lists.reverse(name)
430-
431422
@doc """
432423
Returns a list with the path split by the path separator.
433424
If an empty string is given, returns the root path.

lib/elixir/test/elixir/file_test.exs

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,28 @@ defmodule FileTest do
210210

211211
File.mkdir(dest)
212212

213+
try do
214+
refute File.exists?(tmp_path("tmp/cp_r/a/1.txt"))
215+
refute File.exists?(tmp_path("tmp/cp_r/a/a/2.txt"))
216+
refute File.exists?(tmp_path("tmp/cp_r/b/3.txt"))
217+
218+
{ :ok, files } = File.cp_r(src, dest)
219+
assert length(files) == 7
220+
221+
assert File.exists?(tmp_path("tmp/cp_r/a/1.txt"))
222+
assert File.exists?(tmp_path("tmp/cp_r/a/a/2.txt"))
223+
assert File.exists?(tmp_path("tmp/cp_r/b/3.txt"))
224+
after
225+
File.rm_rf dest
226+
end
227+
end
228+
229+
test :cp_r_with_src_dir_slash_and_dest_dir do
230+
src = fixture_path("cp_r") <> "/"
231+
dest = tmp_path("tmp")
232+
233+
File.mkdir(dest)
234+
213235
try do
214236
refute File.exists?(tmp_path("tmp/a/1.txt"))
215237
refute File.exists?(tmp_path("tmp/a/a/2.txt"))
@@ -265,7 +287,7 @@ defmodule FileTest do
265287
end
266288

267289
test :cp_r_with_dir_and_file_conflict do
268-
src = fixture_path("cp_r/.")
290+
src = fixture_path("cp_r") <> "/"
269291
dest = tmp_path("tmp")
270292

271293
try do
@@ -278,7 +300,7 @@ defmodule FileTest do
278300
end
279301

280302
test :cp_r_with_src_dir_and_dest_dir_using_lists do
281-
src = fixture_path("cp_r/.") |> to_char_list
303+
src = (fixture_path("cp_r") <> "/") |> to_char_list
282304
dest = tmp_path("tmp") |> to_char_list
283305

284306
File.mkdir(dest)
@@ -301,7 +323,7 @@ defmodule FileTest do
301323
end
302324

303325
test :cp_r_with_src_with_file_conflict do
304-
src = fixture_path("cp_r/.")
326+
src = fixture_path("cp_r") <> "/"
305327
dest = tmp_path("tmp")
306328

307329
File.mkdir_p tmp_path("tmp/a")
@@ -317,7 +339,7 @@ defmodule FileTest do
317339
end
318340

319341
test :cp_r_with_src_with_file_conflict_callback do
320-
src = fixture_path("cp_r/.")
342+
src = fixture_path("cp_r") <> "/"
321343
dest = tmp_path("tmp")
322344

323345
File.mkdir_p tmp_path("tmp/a")
@@ -337,7 +359,7 @@ defmodule FileTest do
337359
end
338360

339361
test :cp_r! do
340-
src = fixture_path("cp_r/.")
362+
src = fixture_path("cp_r") <> "/"
341363
dest = tmp_path("tmp")
342364

343365
File.mkdir(dest)
@@ -770,7 +792,7 @@ defmodule FileTest do
770792
test :rm_rf do
771793
fixture = tmp_path("tmp")
772794
File.mkdir(fixture)
773-
File.cp_r!(fixture_path("cp_r/."), fixture)
795+
File.cp_r!(fixture_path("cp_r") <> "/", fixture)
774796

775797
assert File.exists?(tmp_path("tmp/a/1.txt"))
776798
assert File.exists?(tmp_path("tmp/a/a/2.txt"))
@@ -811,7 +833,7 @@ defmodule FileTest do
811833
test :rm_rf_with_char_list do
812834
fixture = tmp_path("tmp") |> to_char_list
813835
File.mkdir(fixture)
814-
File.cp_r!(fixture_path("cp_r/."), fixture)
836+
File.cp_r!(fixture_path("cp_r") <> "/", fixture)
815837

816838
assert File.exists?(tmp_path("tmp/a/1.txt"))
817839
assert File.exists?(tmp_path("tmp/a/a/2.txt"))
@@ -847,7 +869,7 @@ defmodule FileTest do
847869
test :rm_rf! do
848870
fixture = tmp_path("tmp")
849871
File.mkdir(fixture)
850-
File.cp_r!(fixture_path("cp_r/."), fixture)
872+
File.cp_r!(fixture_path("cp_r") <> "/", fixture)
851873

852874
assert File.exists?(tmp_path("tmp/a/1.txt"))
853875
assert File.exists?(tmp_path("tmp/a/a/2.txt"))

lib/elixir/test/elixir/path_test.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,13 +223,15 @@ defmodule PathTest do
223223
assert Path.join(["foo"]) == "foo"
224224
assert Path.join(["/", "foo", "bar"]) == "/foo/bar"
225225
assert Path.join(["~", "foo", "bar"]) == "~/foo/bar"
226+
assert Path.join(["/foo/", "/bar/"]) == "/foo/bar/"
226227
end
227228

228229
test :join_with_list do
229230
assert Path.join(['']) == ''
230231
assert Path.join(['foo']) == 'foo'
231232
assert Path.join(['/', 'foo', 'bar']) == '/foo/bar'
232233
assert Path.join(['~', 'foo', 'bar']) == '~/foo/bar'
234+
assert Path.join(['/foo/', '/bar/']) == '/foo/bar/'
233235
end
234236

235237
test :join_two_with_binary do

lib/mix/lib/mix/utils.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ defmodule Mix.Utils do
343343
symlink_source = make_relative_path(source, target)
344344
case :file.make_symlink(symlink_source, target) do
345345
:ok -> :ok
346-
{ :error, _ } -> File.cp_r!(source, Path.dirname(target))
346+
{ :error, _ } -> File.cp_r!(source <> "/", target)
347347
end
348348
end
349349

lib/mix/test/test_helper.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ defmodule MixTest.Case do
7575
end
7676

7777
def in_fixture(which, tmp, function) do
78-
src = Path.join fixture_path(which), "."
78+
src = fixture_path(which) <> "/"
7979
dest = tmp_path(tmp)
8080
flag = tmp_path |> String.to_char_list!
8181

0 commit comments

Comments
 (0)