@@ -67,42 +67,50 @@ create(Metadata, Files, Config) ->
6767 tarball_max_size := TarballMaxSize ,
6868 tarball_max_uncompressed_size := TarballMaxUncompressedSize
6969 } = Config ,
70+ FilesRoot = maps :get (tarball_files_root , Config , undefined ),
7071
7172 MetadataBinary = encode_metadata (Metadata ),
7273
7374 case valid_size (MetadataBinary , ? MAX_METADATA_SIZE ) of
7475 false ->
7576 {error , {tarball , {file_too_big , " metadata.config" }}};
7677 true ->
77- ContentsTarball = create_memory_tarball (Files ),
78- ContentsTarballCompressed = gzip (ContentsTarball ),
79- InnerChecksum = inner_checksum (? VERSION , MetadataBinary , ContentsTarballCompressed ),
80- InnerChecksumBase16 = encode_base16 (InnerChecksum ),
81-
82- OuterFiles = [
83- {" VERSION" , ? VERSION },
84- {" CHECKSUM" , InnerChecksumBase16 },
85- {" metadata.config" , MetadataBinary },
86- {" contents.tar.gz" , ContentsTarballCompressed }
87- ],
88-
89- case valid_size (ContentsTarball , TarballMaxUncompressedSize ) of
90- true ->
91- Tarball = create_memory_tarball (OuterFiles ),
92- OuterChecksum = checksum (Tarball ),
78+ case validate_create_files (Files , FilesRoot ) of
79+ {ok , ValidatedFiles } ->
80+ ContentsTarball = create_memory_tarball (ValidatedFiles ),
81+ ContentsTarballCompressed = gzip (ContentsTarball ),
82+ InnerChecksum = inner_checksum (
83+ ? VERSION , MetadataBinary , ContentsTarballCompressed
84+ ),
85+ InnerChecksumBase16 = encode_base16 (InnerChecksum ),
86+
87+ OuterFiles = [
88+ {" VERSION" , ? VERSION },
89+ {" CHECKSUM" , InnerChecksumBase16 },
90+ {" metadata.config" , MetadataBinary },
91+ {" contents.tar.gz" , ContentsTarballCompressed }
92+ ],
9393
94- case valid_size (Tarball , TarballMaxSize ) of
94+ case valid_size (ContentsTarball , TarballMaxUncompressedSize ) of
9595 true ->
96- {ok , #{
97- tarball => Tarball ,
98- outer_checksum => OuterChecksum ,
99- inner_checksum => InnerChecksum
100- }};
96+ Tarball = create_memory_tarball (OuterFiles ),
97+ OuterChecksum = checksum (Tarball ),
98+
99+ case valid_size (Tarball , TarballMaxSize ) of
100+ true ->
101+ {ok , #{
102+ tarball => Tarball ,
103+ outer_checksum => OuterChecksum ,
104+ inner_checksum => InnerChecksum
105+ }};
106+ false ->
107+ {error , {tarball , {too_big_compressed , TarballMaxSize }}}
108+ end ;
101109 false ->
102- {error , {tarball , {too_big_compressed , TarballMaxSize }}}
110+ {error , {tarball , {too_big_uncompressed , TarballMaxUncompressedSize }}}
103111 end ;
104- false ->
105- { error , { tarball , { too_big_uncompressed , TarballMaxUncompressedSize }}}
112+ { error , _ } = Error ->
113+ Error
106114 end
107115 end .
108116
@@ -133,21 +141,27 @@ create_docs(Files, Config) ->
133141 docs_tarball_max_size := TarballMaxSize ,
134142 docs_tarball_max_uncompressed_size := TarballMaxUncompressedSize
135143 } = Config ,
144+ FilesRoot = maps :get (tarball_files_root , Config , undefined ),
136145
137- UncompressedTarball = create_memory_tarball (Files ),
146+ case validate_create_files (Files , FilesRoot ) of
147+ {ok , ValidatedFiles } ->
148+ UncompressedTarball = create_memory_tarball (ValidatedFiles ),
138149
139- case valid_size (UncompressedTarball , TarballMaxUncompressedSize ) of
140- true ->
141- Tarball = gzip (UncompressedTarball ),
142-
143- case valid_size (Tarball , TarballMaxSize ) of
150+ case valid_size (UncompressedTarball , TarballMaxUncompressedSize ) of
144151 true ->
145- {ok , Tarball };
152+ Tarball = gzip (UncompressedTarball ),
153+
154+ case valid_size (Tarball , TarballMaxSize ) of
155+ true ->
156+ {ok , Tarball };
157+ false ->
158+ {error , {tarball , {too_big_compressed , TarballMaxSize }}}
159+ end ;
146160 false ->
147- {error , {tarball , {too_big_compressed , TarballMaxSize }}}
161+ {error , {tarball , {too_big_uncompressed , TarballMaxUncompressedSize }}}
148162 end ;
149- false ->
150- { error , { tarball , { too_big_uncompressed , TarballMaxUncompressedSize }}}
163+ { error , _ } = Error ->
164+ Error
151165 end .
152166
153167-spec create_docs (files ()) -> {ok , tarball ()} | {error , term ()}.
@@ -340,10 +354,18 @@ format_error({tarball, {too_big_compressed, Size}}) ->
340354 io_lib :format (" package exceeds max compressed size ~w ~s " , [format_byte_size (Size ), " MB" ]);
341355format_error ({tarball , {missing_files , Files }}) ->
342356 io_lib :format (" missing files: ~p " , [Files ]);
357+ format_error ({tarball , missing_files_root }) ->
358+ " tarball files root is required when creating tarballs from filesystem paths" ;
343359format_error ({tarball , {bad_version , Vsn }}) ->
344360 io_lib :format (" unsupported version: ~p " , [Vsn ]);
345361format_error ({tarball , invalid_checksum }) ->
346362 " invalid tarball checksum" ;
363+ format_error ({tarball , {unsafe_path , Name }}) ->
364+ io_lib :format (" unsafe path in tarball: ~s " , [Name ]);
365+ format_error ({tarball , {unsafe_symlink , Name , LinkTarget }}) ->
366+ io_lib :format (" unsafe symlink in tarball: ~s -> ~s " , [Name , LinkTarget ]);
367+ format_error ({tarball , {unsupported_file_type , Name , Type }}) ->
368+ io_lib :format (" unsupported file type in tarball: ~s (~p )" , [Name , Type ]);
347369format_error ({tarball , Reason }) ->
348370 " tarball error, " ++ hex_erl_tar :format_error (Reason );
349371format_error ({inner_tarball , Reason }) ->
@@ -881,6 +903,146 @@ guess_build_tools(Metadata) ->
881903% % Tar Helpers
882904% %====================================================================
883905
906+ % % @private
907+ validate_create_files (Files , FilesRoot ) when is_list (Files ) ->
908+ validate_create_files (Files , FilesRoot , []).
909+
910+ validate_create_files ([], _FilesRoot , Acc ) ->
911+ {ok , lists :reverse (Acc )};
912+ validate_create_files ([File | Rest ], FilesRoot , Acc ) ->
913+ case validate_create_file (File , FilesRoot ) of
914+ {ok , ValidatedFile } -> validate_create_files (Rest , FilesRoot , [ValidatedFile | Acc ]);
915+ {error , _ } = Error -> Error
916+ end .
917+
918+ validate_create_file ({Filename , Contents }, _FilesRoot ) when
919+ is_list (Filename ), is_binary (Contents )
920+ ->
921+ case validate_archive_path (Filename ) of
922+ ok -> {ok , {Filename , Contents }};
923+ {error , _ } = Error -> Error
924+ end ;
925+ validate_create_file (Filename , FilesRoot ) when is_list (Filename ) ->
926+ validate_create_file ({Filename , Filename }, FilesRoot );
927+ validate_create_file ({Filename , AbsFilename }, FilesRoot ) when
928+ is_list (Filename ), is_list (AbsFilename )
929+ ->
930+ case validate_archive_path (Filename ) of
931+ ok -> validate_source_file (Filename , AbsFilename , FilesRoot );
932+ {error , _ } = Error -> Error
933+ end .
934+
935+ validate_archive_path (Filename ) ->
936+ case safe_relative_archive_path (Filename ) of
937+ false -> {error , {tarball , {unsafe_path , Filename }}};
938+ true -> ok
939+ end .
940+
941+ validate_source_file (ArchiveName , SourcePath , FilesRoot ) ->
942+ case validate_source_path (SourcePath ) of
943+ ok -> validate_source_file_root (ArchiveName , SourcePath , FilesRoot );
944+ {error , _ } = Error -> Error
945+ end .
946+
947+ validate_source_path (SourcePath ) ->
948+ case safe_relative_archive_path (SourcePath ) of
949+ false -> {error , {tarball , {unsafe_path , SourcePath }}};
950+ true -> ok
951+ end .
952+
953+ validate_source_file_root (_ArchiveName , _SourcePath , undefined ) ->
954+ {error , {tarball , missing_files_root }};
955+ validate_source_file_root (ArchiveName , SourcePath , FilesRoot ) ->
956+ Root = filename :absname (FilesRoot ),
957+ DiskPath = filename :join (Root , SourcePath ),
958+ case file :read_link_info (DiskPath , []) of
959+ {ok , # file_info {type = Type }} when Type =:= regular ; Type =:= directory ->
960+ case validate_source_root (ArchiveName , SourcePath , Root ) of
961+ ok -> {ok , {ArchiveName , DiskPath }};
962+ {error , _ } = Error -> Error
963+ end ;
964+ {ok , # file_info {type = symlink }} ->
965+ {ok , LinkTarget } = file :read_link (DiskPath ),
966+ ResolvedTarget = archive_join (archive_dirname (ArchiveName ), LinkTarget ),
967+ case safe_relative_archive_path (ResolvedTarget ) of
968+ false ->
969+ {error , {tarball , {unsafe_symlink , ArchiveName , LinkTarget }}};
970+ true ->
971+ case validate_source_root (ArchiveName , SourcePath , Root ) of
972+ ok -> {ok , {ArchiveName , DiskPath }};
973+ {error , _ } = Error -> Error
974+ end
975+ end ;
976+ {ok , # file_info {type = Type }} ->
977+ {error , {tarball , {unsupported_file_type , ArchiveName , Type }}};
978+ _ ->
979+ {ok , {ArchiveName , DiskPath }}
980+ end .
981+
982+ validate_source_root (ArchiveName , SourcePath , FilesRoot ) ->
983+ case filelib :safe_relative_path (SourcePath , FilesRoot ) of
984+ unsafe -> {error , {tarball , {unsafe_path , ArchiveName }}};
985+ _ -> ok
986+ end .
987+
988+ safe_relative_archive_path (Path ) ->
989+ case archive_path_absolute (Path ) orelse archive_path_drive (Path ) of
990+ true -> false ;
991+ false -> safe_relative_archive_path (archive_path_split (Path ), [])
992+ end .
993+
994+ safe_relative_archive_path ([], _Acc ) ->
995+ true ;
996+ safe_relative_archive_path ([" ." | Rest ], Acc ) ->
997+ safe_relative_archive_path (Rest , Acc );
998+ safe_relative_archive_path ([" .." | _Rest ], []) ->
999+ false ;
1000+ safe_relative_archive_path ([" .." | Rest ], [_ | Acc ]) ->
1001+ safe_relative_archive_path (Rest , Acc );
1002+ safe_relative_archive_path ([_Part | Rest ], Acc ) ->
1003+ safe_relative_archive_path (Rest , [ok | Acc ]).
1004+
1005+ archive_path_absolute ([$/ | _Rest ]) ->
1006+ true ;
1007+ archive_path_absolute ([$\\ | _Rest ]) ->
1008+ true ;
1009+ archive_path_absolute (_Path ) ->
1010+ false .
1011+
1012+ archive_path_drive ([Drive , $: | _Rest ]) when
1013+ Drive >= $a , Drive =< $z ;
1014+ Drive >= $A , Drive =< $Z
1015+ ->
1016+ true ;
1017+ archive_path_drive (_Path ) ->
1018+ false .
1019+
1020+ archive_path_split (Path ) ->
1021+ string :tokens (Path , " /\\ " ).
1022+
1023+ archive_dirname (Path ) ->
1024+ case archive_path_split (Path ) of
1025+ [] -> " ." ;
1026+ [_Name ] -> " ." ;
1027+ Parts -> string :join (lists :droplast (Parts ), " /" )
1028+ end .
1029+
1030+ archive_join (_Dir , Path ) when Path =:= [] ->
1031+ Path ;
1032+ archive_join (_Dir , Path = [$/ | _Rest ]) ->
1033+ Path ;
1034+ archive_join (_Dir , Path = [$\\ | _Rest ]) ->
1035+ Path ;
1036+ archive_join (_Dir , Path = [Drive , $: | _Rest ]) when
1037+ Drive >= $a , Drive =< $z ;
1038+ Drive >= $A , Drive =< $Z
1039+ ->
1040+ Path ;
1041+ archive_join (" ." , Path ) ->
1042+ Path ;
1043+ archive_join (Dir , Path ) ->
1044+ Dir ++ " /" ++ Path .
1045+
8841046% % @private
8851047unpack_tarball (Source , memory , MaxSize ) ->
8861048 case hex_erl_tar :extract (Source , [memory , compressed , {max_size , MaxSize }]) of
0 commit comments