Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/hex_core.erl
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@
%% * `tarball_max_uncompressed_size' - Maximum size of uncompressed package tarball, defaults to
%% `134_217_728' (128 MiB). Set to `infinity' to not enforce the limit.
%%
%% * `tarball_files_root' - Root directory for source files when creating tarballs.
%% Required for filesystem source paths, which must be relative and must resolve inside
%% this root after following symlinks. Set to `undefined' when all tarball contents are
%% provided as binaries and no filesystem source paths are used (default: `undefined').
%%
%% * `docs_tarball_max_size' - Maximum size of docs tarball, defaults to
%% `16_777_216' (16 MiB). Set to `infinity' to not enforce the limit.
%%
Expand Down Expand Up @@ -113,6 +118,7 @@
repo_verify => boolean(),
repo_verify_origin => boolean(),
send_100_continue => boolean(),
tarball_files_root => file:filename() | undefined,
tarball_max_size => pos_integer() | infinity,
tarball_max_uncompressed_size => pos_integer() | infinity,
docs_tarball_max_size => pos_integer() | infinity,
Expand Down Expand Up @@ -140,6 +146,7 @@ default_config() ->
repo_verify => true,
repo_verify_origin => true,
send_100_continue => true,
tarball_files_root => undefined,
tarball_max_size => 16 * 1024 * 1024,
tarball_max_uncompressed_size => 128 * 1024 * 1024,
docs_tarball_max_size => 16 * 1024 * 1024,
Expand Down
232 changes: 197 additions & 35 deletions src/hex_tarball.erl
Original file line number Diff line number Diff line change
Expand Up @@ -67,42 +67,50 @@ create(Metadata, Files, Config) ->
tarball_max_size := TarballMaxSize,
tarball_max_uncompressed_size := TarballMaxUncompressedSize
} = Config,
FilesRoot = maps:get(tarball_files_root, Config, undefined),

MetadataBinary = encode_metadata(Metadata),

case valid_size(MetadataBinary, ?MAX_METADATA_SIZE) of
false ->
{error, {tarball, {file_too_big, "metadata.config"}}};
true ->
ContentsTarball = create_memory_tarball(Files),
ContentsTarballCompressed = gzip(ContentsTarball),
InnerChecksum = inner_checksum(?VERSION, MetadataBinary, ContentsTarballCompressed),
InnerChecksumBase16 = encode_base16(InnerChecksum),

OuterFiles = [
{"VERSION", ?VERSION},
{"CHECKSUM", InnerChecksumBase16},
{"metadata.config", MetadataBinary},
{"contents.tar.gz", ContentsTarballCompressed}
],

case valid_size(ContentsTarball, TarballMaxUncompressedSize) of
true ->
Tarball = create_memory_tarball(OuterFiles),
OuterChecksum = checksum(Tarball),
case validate_create_files(Files, FilesRoot) of
{ok, ValidatedFiles} ->
ContentsTarball = create_memory_tarball(ValidatedFiles),
ContentsTarballCompressed = gzip(ContentsTarball),
InnerChecksum = inner_checksum(
?VERSION, MetadataBinary, ContentsTarballCompressed
),
InnerChecksumBase16 = encode_base16(InnerChecksum),

OuterFiles = [
{"VERSION", ?VERSION},
{"CHECKSUM", InnerChecksumBase16},
{"metadata.config", MetadataBinary},
{"contents.tar.gz", ContentsTarballCompressed}
],

case valid_size(Tarball, TarballMaxSize) of
case valid_size(ContentsTarball, TarballMaxUncompressedSize) of
true ->
{ok, #{
tarball => Tarball,
outer_checksum => OuterChecksum,
inner_checksum => InnerChecksum
}};
Tarball = create_memory_tarball(OuterFiles),
OuterChecksum = checksum(Tarball),

case valid_size(Tarball, TarballMaxSize) of
true ->
{ok, #{
tarball => Tarball,
outer_checksum => OuterChecksum,
inner_checksum => InnerChecksum
}};
false ->
{error, {tarball, {too_big_compressed, TarballMaxSize}}}
end;
false ->
{error, {tarball, {too_big_compressed, TarballMaxSize}}}
{error, {tarball, {too_big_uncompressed, TarballMaxUncompressedSize}}}
end;
false ->
{error, {tarball, {too_big_uncompressed, TarballMaxUncompressedSize}}}
{error, _} = Error ->
Error
end
end.

Expand Down Expand Up @@ -133,21 +141,27 @@ create_docs(Files, Config) ->
docs_tarball_max_size := TarballMaxSize,
docs_tarball_max_uncompressed_size := TarballMaxUncompressedSize
} = Config,
FilesRoot = maps:get(tarball_files_root, Config, undefined),

UncompressedTarball = create_memory_tarball(Files),
case validate_create_files(Files, FilesRoot) of
{ok, ValidatedFiles} ->
UncompressedTarball = create_memory_tarball(ValidatedFiles),

case valid_size(UncompressedTarball, TarballMaxUncompressedSize) of
true ->
Tarball = gzip(UncompressedTarball),

case valid_size(Tarball, TarballMaxSize) of
case valid_size(UncompressedTarball, TarballMaxUncompressedSize) of
true ->
{ok, Tarball};
Tarball = gzip(UncompressedTarball),

case valid_size(Tarball, TarballMaxSize) of
true ->
{ok, Tarball};
false ->
{error, {tarball, {too_big_compressed, TarballMaxSize}}}
end;
false ->
{error, {tarball, {too_big_compressed, TarballMaxSize}}}
{error, {tarball, {too_big_uncompressed, TarballMaxUncompressedSize}}}
end;
false ->
{error, {tarball, {too_big_uncompressed, TarballMaxUncompressedSize}}}
{error, _} = Error ->
Error
end.

-spec create_docs(files()) -> {ok, tarball()} | {error, term()}.
Expand Down Expand Up @@ -340,10 +354,18 @@ format_error({tarball, {too_big_compressed, Size}}) ->
io_lib:format("package exceeds max compressed size ~w ~s", [format_byte_size(Size), "MB"]);
format_error({tarball, {missing_files, Files}}) ->
io_lib:format("missing files: ~p", [Files]);
format_error({tarball, missing_files_root}) ->
"tarball files root is required when creating tarballs from filesystem paths";
format_error({tarball, {bad_version, Vsn}}) ->
io_lib:format("unsupported version: ~p", [Vsn]);
format_error({tarball, invalid_checksum}) ->
"invalid tarball checksum";
format_error({tarball, {unsafe_path, Name}}) ->
io_lib:format("unsafe path in tarball: ~s", [Name]);
format_error({tarball, {unsafe_symlink, Name, LinkTarget}}) ->
io_lib:format("unsafe symlink in tarball: ~s -> ~s", [Name, LinkTarget]);
format_error({tarball, {unsupported_file_type, Name, Type}}) ->
io_lib:format("unsupported file type in tarball: ~s (~p)", [Name, Type]);
format_error({tarball, Reason}) ->
"tarball error, " ++ hex_erl_tar:format_error(Reason);
format_error({inner_tarball, Reason}) ->
Expand Down Expand Up @@ -881,6 +903,146 @@ guess_build_tools(Metadata) ->
%% Tar Helpers
%%====================================================================

%% @private
validate_create_files(Files, FilesRoot) when is_list(Files) ->
validate_create_files(Files, FilesRoot, []).

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
end.

validate_create_file({Filename, Contents}, _FilesRoot) when
is_list(Filename), is_binary(Contents)
->
case validate_archive_path(Filename) of
ok -> {ok, {Filename, Contents}};
{error, _} = Error -> Error
end;
validate_create_file(Filename, FilesRoot) when is_list(Filename) ->
validate_create_file({Filename, Filename}, FilesRoot);
validate_create_file({Filename, AbsFilename}, FilesRoot) when
is_list(Filename), is_list(AbsFilename)
->
case validate_archive_path(Filename) of
ok -> validate_source_file(Filename, AbsFilename, FilesRoot);
{error, _} = Error -> Error
end.

validate_archive_path(Filename) ->
case safe_relative_archive_path(Filename) of
false -> {error, {tarball, {unsafe_path, Filename}}};
true -> ok
end.

validate_source_file(ArchiveName, SourcePath, FilesRoot) ->
case validate_source_path(SourcePath) of
ok -> validate_source_file_root(ArchiveName, SourcePath, FilesRoot);
{error, _} = Error -> Error
end.

validate_source_path(SourcePath) ->
case safe_relative_archive_path(SourcePath) of
false -> {error, {tarball, {unsafe_path, SourcePath}}};
true -> ok
end.

validate_source_file_root(_ArchiveName, _SourcePath, undefined) ->
{error, {tarball, missing_files_root}};
validate_source_file_root(ArchiveName, SourcePath, FilesRoot) ->
Root = filename:absname(FilesRoot),
DiskPath = filename:join(Root, SourcePath),
case file:read_link_info(DiskPath, []) of
{ok, #file_info{type = Type}} when Type =:= regular; Type =:= directory ->
case validate_source_root(ArchiveName, SourcePath, Root) of
ok -> {ok, {ArchiveName, DiskPath}};
{error, _} = Error -> Error
end;
{ok, #file_info{type = symlink}} ->
{ok, LinkTarget} = file:read_link(DiskPath),
ResolvedTarget = archive_join(archive_dirname(ArchiveName), LinkTarget),
case safe_relative_archive_path(ResolvedTarget) of
false ->
{error, {tarball, {unsafe_symlink, ArchiveName, LinkTarget}}};
true ->
case validate_source_root(ArchiveName, SourcePath, Root) of
ok -> {ok, {ArchiveName, DiskPath}};
{error, _} = Error -> Error
end
end;
{ok, #file_info{type = Type}} ->
{error, {tarball, {unsupported_file_type, ArchiveName, Type}}};
_ ->
{ok, {ArchiveName, DiskPath}}
end.

validate_source_root(ArchiveName, SourcePath, FilesRoot) ->
case filelib:safe_relative_path(SourcePath, FilesRoot) of
unsafe -> {error, {tarball, {unsafe_path, ArchiveName}}};
_ -> ok
end.

safe_relative_archive_path(Path) ->
case archive_path_absolute(Path) orelse archive_path_drive(Path) of
true -> false;
false -> safe_relative_archive_path(archive_path_split(Path), [])
end.

safe_relative_archive_path([], _Acc) ->
true;
safe_relative_archive_path(["." | Rest], Acc) ->
safe_relative_archive_path(Rest, Acc);
safe_relative_archive_path([".." | _Rest], []) ->
false;
safe_relative_archive_path([".." | Rest], [_ | Acc]) ->
safe_relative_archive_path(Rest, Acc);
safe_relative_archive_path([_Part | Rest], Acc) ->
safe_relative_archive_path(Rest, [ok | Acc]).

archive_path_absolute([$/ | _Rest]) ->
true;
archive_path_absolute([$\\ | _Rest]) ->
true;
archive_path_absolute(_Path) ->
false.

archive_path_drive([Drive, $: | _Rest]) when
Drive >= $a, Drive =< $z;
Drive >= $A, Drive =< $Z
->
true;
archive_path_drive(_Path) ->
false.

archive_path_split(Path) ->
string:tokens(Path, "/\\").

archive_dirname(Path) ->
case archive_path_split(Path) of
[] -> ".";
[_Name] -> ".";
Parts -> string:join(lists:droplast(Parts), "/")
end.

archive_join(_Dir, Path) when Path =:= [] ->
Path;
archive_join(_Dir, Path = [$/ | _Rest]) ->
Path;
archive_join(_Dir, Path = [$\\ | _Rest]) ->
Path;
archive_join(_Dir, Path = [Drive, $: | _Rest]) when
Drive >= $a, Drive =< $z;
Drive >= $A, Drive =< $Z
->
Path;
archive_join(".", Path) ->
Path;
archive_join(Dir, Path) ->
Dir ++ "/" ++ Path.

%% @private
unpack_tarball(Source, memory, MaxSize) ->
case hex_erl_tar:extract(Source, [memory, compressed, {max_size, MaxSize}]) of
Expand Down
Loading
Loading