Skip to content

Commit e3a91b4

Browse files
committed
Delegate package file validation to hex_core
1 parent f982824 commit e3a91b4

7 files changed

Lines changed: 142 additions & 110 deletions

File tree

lib/hex/tar.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ defmodule Hex.Tar do
1414
filename -> String.to_charlist(filename)
1515
end)
1616

17-
case :mix_hex_tarball.create(metadata, files) do
17+
config =
18+
:mix_hex_core.default_config()
19+
|> Map.put(:tarball_files_root, File.cwd!() |> String.to_charlist())
20+
21+
case :mix_hex_tarball.create(metadata, files, config) do
1822
{:ok, %{tarball: tarball} = result} ->
1923
if output != :memory, do: File.write!(output, tarball)
2024
result

lib/mix/tasks/hex.build.ex

Lines changed: 23 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ defmodule Mix.Tasks.Hex.Build do
5555
exclude_deps = build.exclude_deps
5656

5757
Hex.Shell.info("Building #{meta.name} #{meta.version}")
58-
print_info(meta, organization, exclude_deps, package[:files], build.file_errors)
58+
print_info(meta, organization, exclude_deps, package[:files])
5959

6060
if opts[:unpack] do
6161
output = Keyword.get(opts, :output, "#{meta.name}-#{meta.version}")
@@ -100,21 +100,20 @@ defmodule Mix.Tasks.Hex.Build do
100100
check_misspellings!(package)
101101
{organization, package} = Map.pop(package, :organization)
102102
{deps, exclude_deps} = dependencies()
103-
{meta, file_errors} = meta_for(config, package, deps)
103+
meta = meta_for(config, package, deps)
104104

105105
%{
106106
config: config,
107107
package: package,
108108
deps: deps,
109109
exclude_deps: exclude_deps,
110-
file_errors: file_errors,
111110
meta: meta,
112111
organization: organization
113112
}
114113
end
115114

116115
@doc false
117-
def print_info(meta, organization, exclude_deps, package_files, file_errors \\ []) do
116+
def print_info(meta, organization, exclude_deps, package_files) do
118117
if meta[:requirements] != [] do
119118
Hex.Shell.info(" Dependencies:")
120119

@@ -138,7 +137,6 @@ defmodule Mix.Tasks.Hex.Build do
138137
Enum.concat([
139138
check_missing_fields(meta, organization),
140139
check_description_length(meta),
141-
file_errors,
142140
check_missing_files(package_files || []),
143141
check_reserved_files(package_files || []),
144142
check_excluded_deps(exclude_deps)
@@ -180,14 +178,12 @@ defmodule Mix.Tasks.Hex.Build do
180178
end
181179

182180
defp meta_for(config, package, deps) do
183-
{package, file_errors} =
184-
config
185-
|> Keyword.take(@root_fields)
186-
|> Map.new()
187-
|> Map.merge(package)
188-
|> package_with_file_errors(config)
189-
190-
{Map.put(package, :requirements, deps), file_errors}
181+
config
182+
|> Keyword.take(@root_fields)
183+
|> Map.new()
184+
|> Map.merge(package)
185+
|> package(config)
186+
|> Map.put(:requirements, deps)
191187
end
192188

193189
defp dependencies() do
@@ -254,36 +250,22 @@ defmodule Mix.Tasks.Hex.Build do
254250

255251
@doc false
256252
def package(package, config) do
257-
{package, _file_errors} = package_with_file_errors(package, config)
258-
package
259-
end
260-
261-
defp package_with_file_errors(package, config) do
262253
files = package[:files] || Hex.Package.default_files()
263254
exclude_patterns = (package[:exclude_patterns] || []) ++ [~r/\W\.DS_Store$/]
264-
root = File.cwd!()
265-
266-
{files, path_errors} =
267-
files
268-
|> expand_paths(root)
269255

270256
files =
271257
files
258+
|> expand_paths(File.cwd!())
272259
|> Enum.reject(fn path ->
273260
Enum.any?(exclude_patterns, &(path =~ &1))
274261
end)
275262

276-
file_errors = path_errors ++ check_symlinks(files, root)
277-
278-
package =
279-
package
280-
|> Map.put(:files, files)
281-
|> maybe_put(:description, package[:description], &String.trim/1)
282-
|> maybe_put(:name, package[:name] || config[:app], &to_string(&1))
283-
|> maybe_put(:build_tools, !package[:build_tools] && guess_build_tools(files), & &1)
284-
|> Map.take(@meta_fields)
285-
286-
{package, file_errors}
263+
package
264+
|> Map.put(:files, files)
265+
|> maybe_put(:description, package[:description], &String.trim/1)
266+
|> maybe_put(:name, package[:name] || config[:app], &to_string(&1))
267+
|> maybe_put(:build_tools, !package[:build_tools] && guess_build_tools(files), & &1)
268+
|> Map.take(@meta_fields)
287269
end
288270

289271
defp maybe_put(map, key, value, transform) do
@@ -335,56 +317,13 @@ defmodule Mix.Tasks.Hex.Build do
335317
defp expand_paths(paths, dir) do
336318
expand_dir = Path.expand(dir)
337319

338-
paths =
339-
paths
340-
|> Enum.flat_map(fn pattern ->
341-
dir
342-
|> Path.join(pattern)
343-
|> Path.wildcard()
344-
|> Enum.flat_map(&dir_files/1)
345-
|> Enum.map(&{pattern, Path.expand(&1)})
346-
end)
347-
|> Enum.uniq_by(fn {_pattern, path} -> path end)
348-
349-
path_errors =
350-
paths
351-
|> Enum.reject(fn {_pattern, path} -> in_path?(path, expand_dir) end)
352-
|> Enum.map(fn {pattern, _path} -> "Path escapes package root: #{pattern}" end)
353-
|> Enum.uniq()
354-
355-
files =
356-
paths
357-
|> Enum.filter(fn {_pattern, path} -> in_path?(path, expand_dir) end)
358-
|> Enum.map(fn {_pattern, path} -> Path.relative_to(path, expand_dir) end)
359-
360-
{files, path_errors}
361-
end
362-
363-
defp check_symlinks(files, root) do
364-
Enum.flat_map(files, fn file ->
365-
path = Path.join(root, file)
366-
367-
case File.lstat(path) do
368-
{:ok, %File.Stat{type: :symlink}} ->
369-
link_target = File.read_link!(path)
370-
resolved_target = Path.expand(link_target, Path.dirname(path))
371-
372-
if in_path?(resolved_target, root) do
373-
[]
374-
else
375-
["Symlink points outside package root: #{file} -> #{link_target}"]
376-
end
377-
378-
_ ->
379-
[]
380-
end
381-
end)
382-
end
383-
384-
defp in_path?(path, root) do
385-
path = Path.expand(path)
386-
root = Path.expand(root)
387-
path == root or String.starts_with?(path, root <> "/")
320+
paths
321+
|> Enum.map(&Path.join(dir, &1))
322+
|> Enum.flat_map(&Path.wildcard/1)
323+
|> Enum.flat_map(&dir_files/1)
324+
|> Enum.map(&Path.expand/1)
325+
|> Enum.uniq()
326+
|> Enum.map(&Path.relative_to(&1, expand_dir))
388327
end
389328

390329
defp dir_files(path) do

lib/mix/tasks/hex.publish.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ defmodule Mix.Tasks.Hex.Publish do
213213
package = build.package
214214

215215
Hex.Shell.info("Building #{meta.name} #{meta.version}")
216-
Build.print_info(meta, organization, exclude_deps, package[:files], build.file_errors)
216+
Build.print_info(meta, organization, exclude_deps, package[:files])
217217

218218
print_link_to_coc()
219219
print_public_private(organization)

src/mix_hex_core.erl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@
6868
%% * `tarball_max_uncompressed_size' - Maximum size of uncompressed package tarball, defaults to
6969
%% `134_217_728' (128 MiB). Set to `infinity' to not enforce the limit.
7070
%%
71+
%% * `tarball_files_root' - Root directory source files must resolve inside when creating tarballs.
72+
%% Set to `undefined' to skip source root validation (default: `undefined').
73+
%%
7174
%% * `docs_tarball_max_size' - Maximum size of docs tarball, defaults to
7275
%% `16_777_216' (16 MiB). Set to `infinity' to not enforce the limit.
7376
%%
@@ -110,6 +113,7 @@
110113
repo_verify => boolean(),
111114
repo_verify_origin => boolean(),
112115
send_100_continue => boolean(),
116+
tarball_files_root => file:filename() | undefined,
113117
tarball_max_size => pos_integer() | infinity,
114118
tarball_max_uncompressed_size => pos_integer() | infinity,
115119
docs_tarball_max_size => pos_integer() | infinity,
@@ -136,6 +140,7 @@ default_config() ->
136140
repo_verify => true,
137141
repo_verify_origin => true,
138142
send_100_continue => true,
143+
tarball_files_root => undefined,
139144
tarball_max_size => 16 * 1024 * 1024,
140145
tarball_max_uncompressed_size => 128 * 1024 * 1024,
141146
docs_tarball_max_size => 16 * 1024 * 1024,

src/mix_hex_tarball.erl

Lines changed: 70 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,15 @@ create(Metadata, Files, Config) ->
6868
tarball_max_size := TarballMaxSize,
6969
tarball_max_uncompressed_size := TarballMaxUncompressedSize
7070
} = Config,
71+
FilesRoot = maps:get(tarball_files_root, Config, undefined),
7172

7273
MetadataBinary = encode_metadata(Metadata),
7374

7475
case valid_size(MetadataBinary, ?MAX_METADATA_SIZE) of
7576
false ->
7677
{error, {tarball, {file_too_big, "metadata.config"}}};
7778
true ->
78-
case validate_create_files(Files) of
79+
case validate_create_files(Files, FilesRoot) of
7980
ok ->
8081
ContentsTarball = create_memory_tarball(Files),
8182
ContentsTarballCompressed = gzip(ContentsTarball),
@@ -139,8 +140,9 @@ create_docs(Files, Config) ->
139140
docs_tarball_max_size := TarballMaxSize,
140141
docs_tarball_max_uncompressed_size := TarballMaxUncompressedSize
141142
} = Config,
143+
FilesRoot = maps:get(tarball_files_root, Config, undefined),
142144

143-
case validate_create_files(Files) of
145+
case validate_create_files(Files, FilesRoot) of
144146
ok ->
145147
UncompressedTarball = create_memory_tarball(Files),
146148

@@ -648,23 +650,23 @@ guess_build_tools(Metadata) ->
648650
%%====================================================================
649651

650652
%% @private
651-
validate_create_files(Files) when is_list(Files) ->
652-
validate_create_files(Files, ok).
653+
validate_create_files(Files, FilesRoot) when is_list(Files) ->
654+
validate_create_files(Files, FilesRoot, ok).
653655

654-
validate_create_files(_Files, {error, _} = Error) ->
656+
validate_create_files(_Files, _FilesRoot, {error, _} = Error) ->
655657
Error;
656-
validate_create_files([], ok) ->
658+
validate_create_files([], _FilesRoot, ok) ->
657659
ok;
658-
validate_create_files([File | Rest], ok) ->
659-
validate_create_files(Rest, validate_create_file(File)).
660+
validate_create_files([File | Rest], FilesRoot, ok) ->
661+
validate_create_files(Rest, FilesRoot, validate_create_file(File, FilesRoot)).
660662

661-
validate_create_file({Filename, Contents}) when is_list(Filename), is_binary(Contents) ->
663+
validate_create_file({Filename, Contents}, _FilesRoot) when is_list(Filename), is_binary(Contents) ->
662664
validate_archive_path(Filename);
663-
validate_create_file(Filename) when is_list(Filename) ->
664-
validate_create_file({Filename, Filename});
665-
validate_create_file({Filename, AbsFilename}) when is_list(Filename), is_list(AbsFilename) ->
665+
validate_create_file(Filename, FilesRoot) when is_list(Filename) ->
666+
validate_create_file({Filename, Filename}, FilesRoot);
667+
validate_create_file({Filename, AbsFilename}, FilesRoot) when is_list(Filename), is_list(AbsFilename) ->
666668
case validate_archive_path(Filename) of
667-
ok -> validate_source_file(Filename, AbsFilename);
669+
ok -> validate_source_file(Filename, AbsFilename, FilesRoot);
668670
{error, _} = Error -> Error
669671
end.
670672

@@ -674,13 +676,13 @@ validate_archive_path(Filename) ->
674676
true -> ok
675677
end.
676678

677-
validate_source_file(ArchiveName, SourcePath) ->
679+
validate_source_file(ArchiveName, SourcePath, FilesRoot) ->
678680
case file:read_link_info(SourcePath, []) of
679681
{ok, #file_info{type = Type}} when Type =:= regular; Type =:= directory ->
680-
ok;
682+
validate_source_root(ArchiveName, SourcePath, FilesRoot);
681683
{ok, #file_info{type = symlink}} ->
682684
{ok, LinkTarget} = file:read_link(SourcePath),
683-
ResolvedTarget = filename:join(filename:dirname(ArchiveName), LinkTarget),
685+
ResolvedTarget = archive_join(archive_dirname(ArchiveName), LinkTarget),
684686
case safe_relative_archive_path(ResolvedTarget) of
685687
false -> {error, {tarball, {unsafe_symlink, ArchiveName, LinkTarget}}};
686688
true -> ok
@@ -691,10 +693,18 @@ validate_source_file(ArchiveName, SourcePath) ->
691693
ok
692694
end.
693695

696+
validate_source_root(_ArchiveName, _SourcePath, undefined) ->
697+
ok;
698+
validate_source_root(ArchiveName, SourcePath, FilesRoot) ->
699+
case filelib:safe_relative_path(SourcePath, filename:absname(FilesRoot)) of
700+
unsafe -> {error, {tarball, {unsafe_path, ArchiveName}}};
701+
_ -> ok
702+
end.
703+
694704
safe_relative_archive_path(Path) ->
695-
case filename:pathtype(Path) of
696-
relative -> safe_relative_archive_path(filename:split(Path), []);
697-
_ -> false
705+
case archive_path_absolute(Path) orelse archive_path_drive(Path) of
706+
true -> false;
707+
false -> safe_relative_archive_path(archive_path_split(Path), [])
698708
end.
699709

700710
safe_relative_archive_path([], _Acc) ->
@@ -708,6 +718,47 @@ safe_relative_archive_path([".." | Rest], [_ | Acc]) ->
708718
safe_relative_archive_path([_Part | Rest], Acc) ->
709719
safe_relative_archive_path(Rest, [ok | Acc]).
710720

721+
archive_path_absolute([$/ | _Rest]) ->
722+
true;
723+
archive_path_absolute([$\\ | _Rest]) ->
724+
true;
725+
archive_path_absolute(_Path) ->
726+
false.
727+
728+
archive_path_drive([Drive, $: | _Rest]) when
729+
Drive >= $a, Drive =< $z;
730+
Drive >= $A, Drive =< $Z
731+
->
732+
true;
733+
archive_path_drive(_Path) ->
734+
false.
735+
736+
archive_path_split(Path) ->
737+
string:tokens(Path, "/\\").
738+
739+
archive_dirname(Path) ->
740+
case archive_path_split(Path) of
741+
[] -> ".";
742+
[_Name] -> ".";
743+
Parts -> string:join(lists:droplast(Parts), "/")
744+
end.
745+
746+
archive_join(_Dir, Path) when Path =:= [] ->
747+
Path;
748+
archive_join(_Dir, Path = [$/ | _Rest]) ->
749+
Path;
750+
archive_join(_Dir, Path = [$\\ | _Rest]) ->
751+
Path;
752+
archive_join(_Dir, Path = [Drive, $: | _Rest]) when
753+
Drive >= $a, Drive =< $z;
754+
Drive >= $A, Drive =< $Z
755+
->
756+
Path;
757+
archive_join(".", Path) ->
758+
Path;
759+
archive_join(Dir, Path) ->
760+
Dir ++ "/" ++ Path.
761+
711762
%% @private
712763
unpack_tarball(Source, memory, MaxSize) ->
713764
case mix_hex_erl_tar:extract(Source, [memory, compressed, {max_size, MaxSize}]) of

0 commit comments

Comments
 (0)