Skip to content

Commit 57898be

Browse files
author
José Valim
committed
Do not normalize paths on join, closes #2500
1 parent 6a6ba9f commit 57898be

File tree

2 files changed

+111
-77
lines changed

2 files changed

+111
-77
lines changed

lib/elixir/lib/path.ex

Lines changed: 99 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -60,40 +60,70 @@ defmodule Path do
6060
def absname(path, relative_to) do
6161
path = IO.chardata_to_string(path)
6262
case type(path) do
63-
:relative -> join(relative_to, path)
64-
:absolute ->
65-
cond do
66-
path == "/" ->
67-
path
68-
:binary.last(path) == ?/ ->
69-
binary_part(path, 0, byte_size(path) - 1)
70-
true ->
71-
path
72-
end
63+
:relative -> absname_join(relative_to, path)
64+
:absolute -> absname_join([path])
7365
:volumerelative ->
7466
relative_to = IO.chardata_to_string(relative_to)
7567
absname_vr(split(path), split(relative_to), relative_to)
7668
end
7769
end
7870

79-
## Absolute path on current drive
71+
# Absolute path on current drive
8072
defp absname_vr(["/"|rest], [volume|_], _relative),
81-
do: join([volume|rest])
73+
do: absname_join([volume|rest])
8274

83-
## Relative to current directory on current drive.
75+
# Relative to current directory on current drive.
8476
defp absname_vr([<<x, ?:>>|rest], [<<x, _ :: binary>>|_], relative),
85-
do: absname(join(rest), relative)
77+
do: absname(absname_join(rest), relative)
8678

87-
## Relative to current directory on another drive.
79+
# Relative to current directory on another drive.
8880
defp absname_vr([<<x, ?:>>|name], _, _relative) do
8981
cwd =
9082
case :file.get_cwd([x, ?:]) do
9183
{:ok, dir} -> IO.chardata_to_string(dir)
9284
{:error, _} -> <<x, ?:, ?/>>
9385
end
94-
absname(join(name), cwd)
86+
absname(absname_join(name), cwd)
9587
end
9688

89+
# Joins a list
90+
defp absname_join([name1, name2|rest]), do:
91+
absname_join([absname_join(name1, name2)|rest])
92+
defp absname_join([name]), do:
93+
do_absname_join(IO.chardata_to_string(name), <<>>, [], major_os_type())
94+
95+
# Joins two paths
96+
defp absname_join(left, right),
97+
do: do_absname_join(IO.chardata_to_string(left), relative(right), [], major_os_type())
98+
99+
defp do_absname_join(<<uc_letter, ?:, rest :: binary>>, relativename, [], :win32) when uc_letter in ?A..?Z, do:
100+
do_absname_join(rest, relativename, [?:, uc_letter+?a-?A], :win32)
101+
defp do_absname_join(<<?\\, rest :: binary>>, relativename, result, :win32), do:
102+
do_absname_join(<<?/, rest :: binary>>, relativename, result, :win32)
103+
defp do_absname_join(<<?/, rest :: binary>>, relativename, [?., ?/|result], os_type), do:
104+
do_absname_join(rest, relativename, [?/|result], os_type)
105+
defp do_absname_join(<<?/, rest :: binary>>, relativename, [?/|result], os_type), do:
106+
do_absname_join(rest, relativename, [?/|result], os_type)
107+
defp do_absname_join(<<>>, <<>>, result, os_type), do:
108+
IO.iodata_to_binary(reverse_maybe_remove_dirsep(result, os_type))
109+
defp do_absname_join(<<>>, relativename, [?:|rest], :win32), do:
110+
do_absname_join(relativename, <<>>, [?:|rest], :win32)
111+
defp do_absname_join(<<>>, relativename, [?/|result], os_type), do:
112+
do_absname_join(relativename, <<>>, [?/|result], os_type)
113+
defp do_absname_join(<<>>, relativename, result, os_type), do:
114+
do_absname_join(relativename, <<>>, [?/|result], os_type)
115+
defp do_absname_join(<<char, rest :: binary>>, relativename, result, os_type), do:
116+
do_absname_join(rest, relativename, [char|result], os_type)
117+
118+
defp reverse_maybe_remove_dirsep([?/, ?:, letter], :win32), do:
119+
[letter, ?:, ?/]
120+
defp reverse_maybe_remove_dirsep([?/], _), do:
121+
[?/]
122+
defp reverse_maybe_remove_dirsep([?/|name], _), do:
123+
:lists.reverse(name)
124+
defp reverse_maybe_remove_dirsep(name, _), do:
125+
:lists.reverse(name)
126+
97127
@doc """
98128
Converts the path to an absolute one and expands
99129
any `.` and `..` characters and a leading `~`.
@@ -106,7 +136,7 @@ defmodule Path do
106136
"""
107137
@spec expand(t) :: binary
108138
def expand(path) do
109-
normalize absname(expand_home(path), System.cwd!)
139+
expand_dot absname(expand_home(path), System.cwd!)
110140
end
111141

112142
@doc """
@@ -133,7 +163,7 @@ defmodule Path do
133163
"""
134164
@spec expand(t, t) :: binary
135165
def expand(path, relative_to) do
136-
normalize absname(absname(expand_home(path), expand_home(relative_to)), System.cwd!)
166+
expand_dot absname(absname(expand_home(path), expand_home(relative_to)), System.cwd!)
137167
end
138168

139169
@doc """
@@ -182,9 +212,13 @@ defmodule Path do
182212
"""
183213
@spec relative(t) :: binary
184214
def relative(name) do
185-
case :os.type() do
186-
{:win32, _} -> win32_pathtype(name)
187-
_ -> unix_pathtype(name)
215+
relative(name, major_os_type())
216+
end
217+
218+
defp relative(name, major_os_type) do
219+
case major_os_type do
220+
:win32 -> win32_pathtype(name)
221+
_ -> unix_pathtype(name)
188222
end |> elem(1) |> IO.chardata_to_string
189223
end
190224

@@ -390,7 +424,7 @@ defmodule Path do
390424
end
391425

392426
@doc """
393-
Returns a string with one or more path components joined by the path separator.
427+
Joins a list of strings.
394428
395429
This function should be used to convert a list of strings to a path.
396430
Note that any trailing slash is removed on join.
@@ -411,52 +445,40 @@ defmodule Path do
411445
def join([name1, name2|rest]), do:
412446
join([join(name1, name2)|rest])
413447
def join([name]), do:
414-
do_join(IO.chardata_to_string(name), <<>>, [], major_os_type())
448+
name
415449

416450
@doc """
417451
Joins two paths.
418452
453+
The right path will always be expanded to its relative format
454+
and any trailing slash is removed on join.
455+
419456
## Examples
420457
421458
iex> Path.join("foo", "bar")
422459
"foo/bar"
423460
424461
"""
425462
@spec join(t, t) :: binary
426-
def join(left, right),
427-
do: do_join(IO.chardata_to_string(left), relative(right), [], major_os_type())
428-
429-
defp major_os_type do
430-
:os.type |> elem(0)
463+
def join(left, right) do
464+
left = IO.chardata_to_string(left)
465+
os_type = major_os_type()
466+
do_join(left, right, os_type) |> remove_dirsep(os_type)
431467
end
432468

433-
defp do_join(<<uc_letter, ?:, rest :: binary>>, relativename, [], :win32) when uc_letter in ?A..?Z, do:
434-
do_join(rest, relativename, [?:, uc_letter+?a-?A], :win32)
435-
defp do_join(<<?\\, rest :: binary>>, relativename, result, :win32), do:
436-
do_join(<<?/, rest :: binary>>, relativename, result, :win32)
437-
defp do_join(<<?/, rest :: binary>>, relativename, [?., ?/|result], os_type), do:
438-
do_join(rest, relativename, [?/|result], os_type)
439-
defp do_join(<<?/, rest :: binary>>, relativename, [?/|result], os_type), do:
440-
do_join(rest, relativename, [?/|result], os_type)
441-
defp do_join(<<>>, <<>>, result, os_type), do:
442-
IO.iodata_to_binary(maybe_remove_dirsep(result, os_type))
443-
defp do_join(<<>>, relativename, [?:|rest], :win32), do:
444-
do_join(relativename, <<>>, [?:|rest], :win32)
445-
defp do_join(<<>>, relativename, [?/|result], os_type), do:
446-
do_join(relativename, <<>>, [?/|result], os_type)
447-
defp do_join(<<>>, relativename, result, os_type), do:
448-
do_join(relativename, <<>>, [?/|result], os_type)
449-
defp do_join(<<char, rest :: binary>>, relativename, result, os_type), do:
450-
do_join(rest, relativename, [char|result], os_type)
451-
452-
defp maybe_remove_dirsep([?/, ?:, letter], :win32), do:
453-
[letter, ?:, ?/]
454-
defp maybe_remove_dirsep([?/], _), do:
455-
[?/]
456-
defp maybe_remove_dirsep([?/|name], _), do:
457-
:lists.reverse(name)
458-
defp maybe_remove_dirsep(name, _), do:
459-
:lists.reverse(name)
469+
defp do_join("", right, os_type), do: relative(right, os_type)
470+
defp do_join(left, "", _os_type), do: left
471+
defp do_join(left, right, os_type), do: remove_dirsep(left, os_type) <> "/" <> relative(right, os_type)
472+
473+
defp remove_dirsep("", _os_type), do: ""
474+
defp remove_dirsep(bin, os_type) do
475+
last = :binary.last(bin)
476+
if last == ?/ or (last == ?\\ and os_type == :win32) do
477+
binary_part(bin, 0, byte_size(bin) - 1)
478+
else
479+
bin
480+
end
481+
end
460482

461483
@doc ~S"""
462484
Splits the path into a list at the path separator.
@@ -568,7 +590,7 @@ defmodule Path do
568590
|> Enum.map(&IO.chardata_to_string/1)
569591
end
570592

571-
# Normalize the given path by expanding "..", "." and "~".
593+
# expand_dot the given path by expanding "..", "." and "~".
572594

573595
defp chardata_to_list(chardata) do
574596
case :unicode.characters_to_list(chardata) do
@@ -602,29 +624,34 @@ defmodule Path do
602624
end
603625
end
604626

605-
defp normalize(path), do: normalize(split(path), [])
606-
607-
defp normalize([".."|t], ["/"|_] = acc) do
608-
normalize t, acc
609-
end
610-
611-
defp normalize([".."|t], [<<letter, ?:, ?/>>|_] = acc) when letter in ?a..?z do
612-
normalize t, acc
627+
defp expand_dot(<<"/../", rest::binary>>),
628+
do: expand_dot("/" <> rest)
629+
defp expand_dot(<<letter, ":/../", rest::binary>>) when letter in ?a..?z,
630+
do: expand_dot(<<letter, ":/", rest::binary>>)
631+
defp expand_dot("/.."),
632+
do: "/"
633+
defp expand_dot(<<letter, ":/..">>) when letter in ?a..?z,
634+
do: expand_dot(<<letter, ":/">>)
635+
defp expand_dot(path),
636+
do: expand_dot(:binary.split(path, "/", [:global]), [])
637+
638+
defp expand_dot([".."|t], [_, _|acc]) do
639+
expand_dot t, acc
613640
end
614641

615-
defp normalize([".."|t], [_|acc]) do
616-
normalize t, acc
642+
defp expand_dot(["."|t], acc) do
643+
expand_dot t, acc
617644
end
618645

619-
defp normalize(["."|t], acc) do
620-
normalize t, acc
646+
defp expand_dot([h|t], acc) do
647+
expand_dot t, ["/", h|acc]
621648
end
622649

623-
defp normalize([h|t], acc) do
624-
normalize t, [h|acc]
650+
defp expand_dot([], ["/"|acc]) do
651+
IO.iodata_to_binary(:lists.reverse(acc))
625652
end
626653

627-
defp normalize([], acc) do
628-
join :lists.reverse(acc)
654+
defp major_os_type do
655+
:os.type |> elem(0)
629656
end
630657
end

lib/elixir/test/elixir/path_test.exs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ defmodule PathTest do
8888
test :absname do
8989
assert (Path.absname("/") |> strip_drive_letter_if_windows) == "/"
9090
assert (Path.absname("/foo") |> strip_drive_letter_if_windows) == "/foo"
91+
assert (Path.absname("/./foo") |> strip_drive_letter_if_windows) == "/foo"
9192
assert (Path.absname("/foo/bar") |> strip_drive_letter_if_windows) == "/foo/bar"
9293
assert (Path.absname("/foo/bar/") |> strip_drive_letter_if_windows) == "/foo/bar"
9394
assert (Path.absname("/foo/bar/../bar") |> strip_drive_letter_if_windows) == "/foo/bar/../bar"
@@ -118,6 +119,7 @@ defmodule PathTest do
118119
test :expand_path do
119120
assert (Path.expand("/") |> strip_drive_letter_if_windows) == "/"
120121
assert (Path.expand("/foo") |> strip_drive_letter_if_windows) == "/foo"
122+
assert (Path.expand("/./foo") |> strip_drive_letter_if_windows) == "/foo"
121123
assert (Path.expand("/foo/bar") |> strip_drive_letter_if_windows) == "/foo/bar"
122124
assert (Path.expand("/foo/bar/") |> strip_drive_letter_if_windows) == "/foo/bar"
123125
assert (Path.expand("/foo/bar/.") |> strip_drive_letter_if_windows)== "/foo/bar"
@@ -131,10 +133,9 @@ defmodule PathTest do
131133

132134
assert (Path.expand(['..', ?/, "bar/../bar"], '/foo/../foo/../foo') |>
133135
strip_drive_letter_if_windows) == "/bar"
136+
assert (Path.expand("/..") |> strip_drive_letter_if_windows) == "/"
134137

135138
assert Path.expand("bar/../bar", "foo") == Path.expand("foo/bar")
136-
137-
assert (Path.expand("/..") |> strip_drive_letter_if_windows) == "/"
138139
end
139140

140141
test :relative_to do
@@ -200,11 +201,17 @@ defmodule PathTest do
200201
assert Path.join("/foo", "bar") == "/foo/bar"
201202
assert Path.join("~", "foo") == "~/foo"
202203

203-
assert Path.join("", "bar") == "/bar"
204+
assert Path.join("", "bar") == "bar"
205+
assert Path.join("bar", "") == "bar"
206+
assert Path.join("", "/bar") == "bar"
207+
assert Path.join("/bar", "") == "/bar"
208+
209+
assert Path.join("foo", "/bar") == "foo/bar"
210+
assert Path.join("/foo", "/bar") == "/foo/bar"
204211
assert Path.join("/foo", "/bar") == "/foo/bar"
205-
assert Path.join("/foo", "./bar") == "/foo/bar"
212+
assert Path.join("/foo", "./bar") == "/foo/./bar"
206213

207-
assert Path.join([?/, "foo"], "./bar") == "/foo/bar"
214+
assert Path.join([?/, "foo"], "./bar") == "/foo/./bar"
208215
end
209216

210217
test :split do

0 commit comments

Comments
 (0)