diff --git a/src/hex_tarball.erl b/src/hex_tarball.erl index 2baaf3e..fb6f5eb 100644 --- a/src/hex_tarball.erl +++ b/src/hex_tarball.erl @@ -1128,8 +1128,16 @@ validate_create_files([], _FilesRoot, Acc) -> {ok, lists:reverse(Acc)}; validate_create_files([File | Rest], FilesRoot, Acc) -> case validate_create_file(File, FilesRoot) of - {ok, ValidatedFile} -> validate_create_files(Rest, FilesRoot, [ValidatedFile | Acc]); - {error, _} = Error -> Error + {ok, {ArchiveName, Entry} = ValidatedFile} -> + %% The archive root always exists, so a directory named after it + %% isn't stored and other entries can't take its name + case {archive_path_parts(ArchiveName), Entry} of + {[], directory} -> validate_create_files(Rest, FilesRoot, Acc); + {[], _} -> {error, {tarball, {unsafe_path, ArchiveName}}}; + _ -> validate_create_files(Rest, FilesRoot, [ValidatedFile | Acc]) + end; + {error, _} = Error -> + Error end. validate_create_file({Filename, Source, Options}, FilesRoot) when is_map(Options) -> diff --git a/test/hex_tarball_SUITE.erl b/test/hex_tarball_SUITE.erl index b8c4f76..e89144d 100644 --- a/test/hex_tarball_SUITE.erl +++ b/test/hex_tarball_SUITE.erl @@ -15,6 +15,7 @@ all() -> unicode_normalization_test, latin1_filename_encoding_test, empty_directories_test, + root_entries_test, read_only_cwd_test, read_only_output_parent_test, output_under_regular_file_test, @@ -340,6 +341,50 @@ empty_directories_test(Config) -> [{Name, Type} || {Name, Type, _Size, _Mtime, _Mode, _Uid, _Gid} <- Entries] ). +root_entries_test(Config) -> + BaseDir = ?config(priv_dir, Config), + Dir = filename:absname(filename:join(BaseDir, "root_entries")), + ok = filelib:ensure_dir(filename:join([Dir, "api", "x"])), + ok = file:write_file(filename:join(Dir, "index.html"), <<"index">>), + ok = file:make_symlink("index.html", filename:join(Dir, "link")), + + Metadata = #{<<"name">> => <<"foo">>, <<"version">> => <<"1.0.0">>}, + CreateConfig = maps:put(tarball_files_root, Dir, hex_core:default_config()), + Files = [{"index.html", filename:join(Dir, "index.html")}, {"api", filename:join(Dir, "api")}], + {ok, Docs} = hex_tarball:create_docs(Files, CreateConfig), + {ok, #{tarball := Package}} = hex_tarball:create(Metadata, Files, CreateConfig), + ?assertEqual({ok, ["api", "index.html"]}, hex_erl_tar:table({binary, Docs}, [compressed])), + + lists:foreach( + fun(RootFiles) -> + {ok, Docs} = hex_tarball:create_docs(RootFiles ++ Files, CreateConfig), + {ok, #{tarball := Package}} = + hex_tarball:create(Metadata, RootFiles ++ Files, CreateConfig) + end, + [ + [{".", Dir}], + [{"./", Dir}], + [{"api/..", Dir}], + [{"", Dir}], + [{".", "."}], + [{".", Dir, #{executable => true}}], + [{".", Dir}, {"./", Dir}] + ] + ), + + {error, {tarball, {unsafe_path, "."}}} = hex_tarball:create_docs([{".", <<"index">>}]), + {error, {tarball, {unsafe_path, "api/.."}}} = + hex_tarball:create(Metadata, [{"api/..", <<"index">>}]), + {error, {tarball, {unsafe_path, "."}}} = + hex_tarball:create_docs([{".", filename:join(Dir, "index.html")}], CreateConfig), + {error, {tarball, {unsafe_path, "."}}} = + hex_tarball:create(Metadata, [{".", filename:join(Dir, "index.html")}], CreateConfig), + {error, {tarball, {unsafe_path, "."}}} = + hex_tarball:create_docs([{".", filename:join(Dir, "link")}], CreateConfig), + {error, {tarball, {unsafe_path, "."}}} = + hex_tarball:create(Metadata, [{".", filename:join(Dir, "link")}], CreateConfig), + ok. + read_only_cwd_test(Config) -> BaseDir = ?config(priv_dir, Config), Dir = filename:join(BaseDir, "read_only_cwd"),