diff --git a/src/hex_erl_tar.erl b/src/hex_erl_tar.erl index 41c419c..2f412eb 100644 --- a/src/hex_erl_tar.erl +++ b/src/hex_erl_tar.erl @@ -10,6 +10,8 @@ %% reading them from disk %% 7. PAX record lengths are counted in bytes instead of characters %% 8. The ustar prefix check counts the separator between path components +%% 9. parse_string/1 drops the zero padding of names that end in an incomplete +%% UTF-8 sequence %% %% OTP commit: ad05823719d77c8faee87348ea39513d4e2f99c5 (OTP-29.1.1) %% @@ -1493,7 +1495,7 @@ parse_string(Bin) when is_binary(Bin) -> Str when is_list(Str) -> Str; {incomplete, _Str, _Rest} -> - binary_to_list(Bin); + binary_to_list(Prefix); {error, _Str, _Rest} -> throw({error, {bad_header, invalid_string}}) end. diff --git a/test/hex_tarball_SUITE.erl b/test/hex_tarball_SUITE.erl index 071650a..069df00 100644 --- a/test/hex_tarball_SUITE.erl +++ b/test/hex_tarball_SUITE.erl @@ -18,6 +18,7 @@ all() -> read_only_cwd_test, pax_record_length_test, ustar_prefix_test, + incomplete_utf8_name_test, timestamps_and_permissions_test, normalized_permissions_test, executable_option_test, @@ -372,6 +373,29 @@ ustar_prefix_test(_Config) -> {ok, #{contents := Contents}} = hex_tarball:unpack(Tarball, memory), ?assertEqual(lists:sort(Names), [Name || {Name, _} <- Contents]). +%% A ustar name that ends in an incomplete UTF-8 sequence is read as its bytes, +%% without the zero padding of the header field +incomplete_utf8_name_test(_Config) -> + Metadata = #{<<"name">> => <<"foo">>, <<"version">> => <<"1.0.0">>}, + {ok, #{tarball := Tarball}} = hex_tarball:create(Metadata, [{"cafx", <<"x">>}]), + {ok, OuterFileList} = hex_erl_tar:extract({binary, Tarball}, [memory]), + OuterFiles = maps:from_list(OuterFileList), + #{ + "VERSION" := Version, + "metadata.config" := MetadataBinary, + "contents.tar.gz" := ContentsBinary + } = OuterFiles, + + %% 16#E9 starts a three byte UTF-8 sequence, "caf" followed by it is "café" + %% in ISO Latin-1 + Contents = hex_tarball:gzip(set_first_name(zlib:gunzip(ContentsBinary), <<"caf", 16#E9>>)), + Checksum = crypto:hash(sha256, [Version, MetadataBinary, Contents]), + {ok, #{contents := UnpackedContents}} = unpack_files(OuterFiles#{ + "contents.tar.gz" => Contents, + "CHECKSUM" => hex_tarball:format_checksum(Checksum) + }), + ?assertEqual([{"caf" ++ [16#E9], <<"x">>}], UnpackedContents). + timestamps_and_permissions_test(Config) -> BaseDir = ?config(priv_dir, Config), Foo = filename:join(BaseDir, "foo.sh"), @@ -1820,6 +1844,16 @@ ustar_name(Header) -> Prefix -> <> end. +%% Replaces the name in the first header of an uncompressed tar and updates the +%% checksum of the header +set_first_name( + <<_:100/binary, Fields:48/binary, _Checksum:8/binary, Tail:356/binary, Rest/binary>>, Name +) -> + NameField = <>, + Header = <>, + Checksum = iolist_to_binary(io_lib:format("~6.8.0B", [lists:sum(binary_to_list(Header))])), + <>. + tar_string(Field) -> hd(binary:split(Field, <<0>>)).