aboutsummaryrefslogtreecommitdiffstats
path: root/lib/stdlib/src/zip.erl
diff options
context:
space:
mode:
Diffstat (limited to 'lib/stdlib/src/zip.erl')
-rw-r--r--lib/stdlib/src/zip.erl1600
1 files changed, 1600 insertions, 0 deletions
diff --git a/lib/stdlib/src/zip.erl b/lib/stdlib/src/zip.erl
new file mode 100644
index 0000000000..f44d97c227
--- /dev/null
+++ b/lib/stdlib/src/zip.erl
@@ -0,0 +1,1600 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2006-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+-module(zip).
+
+%% Basic api
+-export([unzip/1, unzip/2, extract/1, extract/2,
+ zip/2, zip/3, create/2, create/3,
+ list_dir/1, list_dir/2, table/1, table/2,
+ t/1, tt/1]).
+
+%% unzipping peicemeal
+-export([openzip_open/1, openzip_open/2,
+ openzip_get/1, openzip_get/2,
+ openzip_t/1, openzip_tt/1,
+ openzip_list_dir/1, openzip_list_dir/2,
+ openzip_close/1]).
+%% openzip_add/2]).
+
+%% zip server
+-export([zip_open/1, zip_open/2,
+ zip_get/1, zip_get/2,
+ zip_t/1, zip_tt/1,
+ zip_list_dir/1, zip_list_dir/2,
+ zip_close/1]).
+
+%% just for debugging zip server, not documented, not tested, not to be used
+-export([zip_get_state/1]).
+
+%% includes
+-include("file.hrl"). % #file_info
+-include("zip.hrl"). % #zip_file, #zip_comment
+
+%% max bytes fed to zlib
+-define(WRITE_BLOCK_SIZE, 8*1024).
+
+%% for debugging, to turn off catch
+-define(CATCH, catch).
+
+%% option sets
+-record(unzip_opts, {
+ output, % output object (fun)
+ input, % input object (fun)
+ file_filter, % file filter (boolean fun)
+ open_opts, % options passed to file:open
+ feedback, % feeback (fun)
+ cwd % directory to relate paths to
+ }).
+
+-record(zip_opts, {
+ output, % output object (fun)
+ input, % input object (fun)
+ comment, % zip-file comment
+ open_opts, % options passed to file:open
+ feedback, % feeback (fun)
+ cwd, % directory to relate paths to
+ compress, % compress files with these suffixes
+ uncompress % uncompress files with these suffixes
+ }).
+
+-record(list_dir_opts, {
+ input, % input object (fun)
+ raw_iterator, % applied to each dir entry
+ open_opts % options passed to file:open
+ }).
+
+-record(openzip_opts, {
+ output, % output object (fun)
+ open_opts, % file:open options
+ cwd % directory to relate paths to
+ }).
+
+% openzip record, state for an open zip-file
+-record(openzip, {
+ zip_comment, % zip archive comment
+ files, % filenames, infos, comments and offsets
+ in, % archive handle
+ input, % archive io object (fun)
+ output, % output io object (fun)
+ zlib, % handle to open zlib
+ cwd % directory to relate paths to
+ }).
+
+% Things that I would like to add to the public record #zip_file,
+% but can't as it would make things fail at upgrade.
+% Instead we use {#zip_file,#zip_file_extra} internally.
+-record(zip_file_extra, {
+ crc32 % checksum
+ }).
+
+%% max bytes read from files and archives (and fed to zlib)
+-define(READ_BLOCK_SIZE, 16*1024).
+
+%% -record(primzip_file, {
+%% name,
+%% offset,
+%% chunk_size
+%% }).
+
+%% -record(primzip, {
+%% zlib, % handle to the zlib port from zlib:open
+%% input, % fun/2 for file/memory input
+%% in, % input (file handle or binary)
+%% files % [#primzip_file]
+%% }).
+
+%% ZIP-file format records and defines
+
+%% compression methods
+-define(STORED, 0).
+-define(UNCOMPRESSED, 0).
+-define(SHRUNK, 1).
+-define(REDUCED_1, 2).
+-define(REDUCED_2, 3).
+-define(REDUCED_3, 4).
+-define(REDUCED_4, 5).
+-define(IMPLODED, 6).
+-define(TOKENIZED, 7).
+-define(DEFLATED, 8).
+-define(DEFLATED_64, 9).
+-define(PKWARE_IMPLODED, 10).
+-define(PKWARE_RESERVED, 11).
+-define(BZIP2_COMPRESSED, 12).
+
+%% zip-file records
+-define(LOCAL_FILE_MAGIC,16#04034b50).
+-define(LOCAL_FILE_HEADER_SZ,(4+2+2+2+2+2+4+4+4+2+2)).
+-define(LOCAL_FILE_HEADER_CRC32_OFFSET, 4+2+2+2+2+2).
+-record(local_file_header, {version_needed,
+ gp_flag,
+ comp_method,
+ last_mod_time,
+ last_mod_date,
+ crc32,
+ comp_size,
+ uncomp_size,
+ file_name_length,
+ extra_field_length}).
+
+-define(CENTRAL_FILE_HEADER_SZ,(4+2+2+2+2+2+2+4+4+4+2+2+2+2+2+4+4)).
+
+-define(CENTRAL_DIR_MAGIC, 16#06054b50).
+-define(CENTRAL_DIR_SZ, (4+2+2+2+2+4+4+2)).
+-define(CENTRAL_DIR_DIGITAL_SIG_MAGIC, 16#05054b50).
+-define(CENTRAL_DIR_DIGITAL_SIG_SZ, (4+2)).
+
+-define(CENTRAL_FILE_MAGIC, 16#02014b50).
+
+-record(cd_file_header, {version_made_by,
+ version_needed,
+ gp_flag,
+ comp_method,
+ last_mod_time,
+ last_mod_date,
+ crc32,
+ comp_size,
+ uncomp_size,
+ file_name_length,
+ extra_field_length,
+ file_comment_length,
+ disk_num_start,
+ internal_attr,
+ external_attr,
+ local_header_offset}).
+
+%% Unix extra fields (not yet supported)
+-define(UNIX_EXTRA_FIELD_TAG, 16#000d).
+-record(unix_extra_field, {atime,
+ mtime,
+ uid,
+ gid}).
+
+%% extended timestamps (not yet supported)
+-define(EXTENDED_TIMESTAMP_TAG, 16#5455).
+%% -record(extended_timestamp, {mtime,
+%% atime,
+%% ctime}).
+
+-define(END_OF_CENTRAL_DIR_MAGIC, 16#06054b50).
+-define(END_OF_CENTRAL_DIR_SZ, (4+2+2+2+2+4+4+2)).
+
+-record(eocd, {disk_num,
+ start_disk_num,
+ entries_on_disk,
+ entries,
+ size,
+ offset,
+ zip_comment_length}).
+
+
+%% Open a zip archive with options
+%%
+
+openzip_open(F) ->
+ openzip_open(F, []).
+
+openzip_open(F, Options) ->
+ case ?CATCH do_openzip_open(F, Options) of
+ {ok, OpenZip} ->
+ {ok, OpenZip};
+ Error ->
+ {error, Error}
+ end.
+
+do_openzip_open(F, Options) ->
+ Opts = get_openzip_options(Options),
+ #openzip_opts{output = Output, open_opts = OpO, cwd = CWD} = Opts,
+ Input = get_zip_input(F),
+ In0 = Input({open, F, OpO -- [write]}, []),
+ {[#zip_comment{comment = C} | Files], In1} =
+ get_central_dir(In0, fun raw_file_info_etc/5, Input),
+ Z = zlib:open(),
+ {ok, #openzip{zip_comment = C,
+ files = Files,
+ in = In1,
+ input = Input,
+ output = Output,
+ zlib = Z,
+ cwd = CWD}}.
+
+%% retrieve all files from an open archive
+openzip_get(OpenZip) ->
+ case ?CATCH do_openzip_get(OpenZip) of
+ {ok, Result} -> {ok, Result};
+ Error -> {error, Error}
+ end.
+
+do_openzip_get(#openzip{files = Files, in = In0, input = Input,
+ output = Output, zlib = Z, cwd = CWD}) ->
+ ZipOpts = #unzip_opts{output = Output, input = Input,
+ file_filter = fun all/1, open_opts = [],
+ feedback = fun silent/1, cwd = CWD},
+ R = get_z_files(Files, Z, In0, ZipOpts, []),
+ {ok, R};
+do_openzip_get(_) ->
+ throw(einval).
+
+%% retrieve a file from an open archive
+openzip_get(FileName, OpenZip) ->
+ case ?CATCH do_openzip_get(FileName, OpenZip) of
+ {ok, Result} -> {ok, Result};
+ Error -> {error, Error}
+ end.
+
+do_openzip_get(F, #openzip{files = Files, in = In0, input = Input,
+ output = Output, zlib = Z, cwd = CWD}) ->
+ %%case lists:keysearch(F, #zip_file.name, Files) of
+ case file_name_search(F, Files) of
+ {#zip_file{offset = Offset},_}=ZFile ->
+ In1 = Input({seek, bof, Offset}, In0),
+ case get_z_file(In1, Z, Input, Output, [], fun silent/1, CWD, ZFile) of
+ {file, R, _In2} -> {ok, R};
+ _ -> throw(file_not_found)
+ end;
+ _ -> throw(file_not_found)
+ end;
+do_openzip_get(_, _) ->
+ throw(einval).
+
+file_name_search(Name,Files) ->
+ case lists:dropwhile(fun({ZipFile,_}) -> ZipFile#zip_file.name =/= Name end,
+ Files) of
+ [ZFile|_] -> ZFile;
+ [] -> false
+ end.
+
+%% %% add a file to an open archive
+%% openzip_add(File, OpenZip) ->
+%% case ?CATCH do_openzip_add(File, OpenZip) of
+%% {ok, Result} -> {ok, Result};
+%% Error -> {error, Error}
+%% end.
+
+%% do_openzip_add(File, #open_zip{files = Files, in = In0,
+%% opts = Opts} = OpenZip0) ->
+%% throw(nyi),
+%% Z = zlib:open(),
+%% R = get_z_files(Files, In0, Z, Opts, []),
+%% zlib:close(Z),
+%% {ok, R};
+%% do_openzip_add(_, _) ->
+%% throw(einval).
+
+%% get file list from open archive
+openzip_list_dir(#openzip{zip_comment = Comment,
+ files = Files}) ->
+ {ZipFiles,_Extras} = lists:unzip(Files),
+ {ok, [#zip_comment{comment = Comment} | ZipFiles]};
+openzip_list_dir(_) ->
+ {error, einval}.
+
+openzip_list_dir(#openzip{files = Files}, [names_only]) ->
+ {ZipFiles,_Extras} = lists:unzip(Files),
+ Names = [Name || {#zip_file{name=Name},_} <- ZipFiles],
+ {ok, Names};
+openzip_list_dir(_, _) ->
+ {error, einval}.
+
+%% close an open archive
+openzip_close(#openzip{in = In0, input = Input, zlib = Z}) ->
+ Input(close, In0),
+ zlib:close(Z);
+openzip_close(_) ->
+ {error, einval}.
+
+%% Extract from a zip archive with options
+%%
+%% Accepted options:
+%% verbose, cooked, file_list, keep_old_files, file_filter, memory
+
+unzip(F) -> unzip(F, []).
+
+unzip(F, Options) ->
+ case ?CATCH do_unzip(F, Options) of
+ {ok, R} -> {ok, R};
+ Error -> {error, Error}
+ end.
+
+do_unzip(F, Options) ->
+ Opts = get_unzip_options(F, Options),
+ #unzip_opts{input = Input, open_opts = OpO} = Opts,
+ In0 = Input({open, F, OpO -- [write]}, []),
+ RawIterator = fun raw_file_info_etc/5,
+ {Info, In1} = get_central_dir(In0, RawIterator, Input),
+ %% get rid of zip-comment
+ Z = zlib:open(),
+ Files = get_z_files(Info, Z, In1, Opts, []),
+ zlib:close(Z),
+ Input(close, In1),
+ {ok, Files}.
+
+%% Create zip archive name F from Files or binaries
+%%
+%% Accepted options:
+%% verbose, cooked, memory, comment
+
+zip(F, Files) -> zip(F, Files, []).
+
+zip(F, Files, Options) ->
+ case ?CATCH do_zip(F, Files, Options) of
+ {ok, R} -> {ok, R};
+ Error -> {error, Error}
+ end.
+
+do_zip(F, Files, Options) ->
+ Opts = get_zip_options(Files, Options),
+ #zip_opts{output = Output, open_opts = OpO} = Opts,
+ Out0 = Output({open, F, OpO}, []),
+ Z = zlib:open(),
+ {Out1, LHS, Pos} = put_z_files(Files, Z, Out0, 0, Opts, []),
+ zlib:close(Z),
+ Out2 = put_central_dir(LHS, Pos, Out1, Opts),
+ Out3 = Output({close, F}, Out2),
+ {ok, Out3}.
+
+%% List zip directory contents
+%%
+%% Accepted options:
+%% cooked, file_filter, file_output (latter 2 undocumented)
+
+list_dir(F) -> list_dir(F, []).
+
+list_dir(F, Options) ->
+ case ?CATCH do_list_dir(F, Options) of
+ {ok, R} -> {ok, R};
+ Error -> {error, Error}
+ end.
+
+do_list_dir(F, Options) ->
+ Opts = get_list_dir_options(F, Options),
+ #list_dir_opts{input = Input, open_opts = OpO,
+ raw_iterator = RawIterator} = Opts,
+ In0 = Input({open, F, OpO}, []),
+ {Info, In1} = get_central_dir(In0, RawIterator, Input),
+ Input(close, In1),
+ {ok, Info}.
+
+%% Print zip directory in short form
+
+t(F) when is_pid(F) -> zip_t(F);
+t(F) when is_record(F, openzip) -> openzip_t(F);
+t(F) -> t(F, fun raw_short_print_info_etc/5).
+
+t(F, RawPrint) ->
+ case ?CATCH do_t(F, RawPrint) of
+ ok -> ok;
+ Error -> {error, Error}
+ end.
+
+do_t(F, RawPrint) ->
+ Input = get_input(F),
+ OpO = [raw],
+ In0 = Input({open, F, OpO}, []),
+ {_Info, In1} = get_central_dir(In0, RawPrint, Input),
+ Input(close, In1),
+ ok.
+
+%% Print zip directory in long form (like ls -l)
+
+tt(F) when is_pid(F) -> zip_tt(F);
+tt(F) when is_record(F, openzip) -> openzip_tt(F);
+tt(F) -> t(F, fun raw_long_print_info_etc/5).
+
+
+%% option utils
+get_unzip_opt([], Opts) ->
+ Opts;
+get_unzip_opt([verbose | Rest], Opts) ->
+ get_unzip_opt(Rest, Opts#unzip_opts{feedback = fun verbose_unzip/1});
+get_unzip_opt([cooked | Rest], #unzip_opts{open_opts = OpO} = Opts) ->
+ get_unzip_opt(Rest, Opts#unzip_opts{open_opts = OpO -- [raw]});
+get_unzip_opt([memory | Rest], Opts) ->
+ get_unzip_opt(Rest, Opts#unzip_opts{output = fun binary_io/2});
+get_unzip_opt([{cwd, CWD} | Rest], Opts) ->
+ get_unzip_opt(Rest, Opts#unzip_opts{cwd = CWD});
+get_unzip_opt([{file_filter, F} | Rest], Opts) ->
+ Filter1 = fun({ZipFile,_Extra}) -> F(ZipFile) end,
+ Filter2 = fun_and_1(Filter1, Opts#unzip_opts.file_filter),
+ get_unzip_opt(Rest, Opts#unzip_opts{file_filter = Filter2});
+get_unzip_opt([{file_list, L} | Rest], Opts) ->
+ FileInList = fun(F) -> file_in_list(F, L) end,
+ Filter = fun_and_1(FileInList, Opts#unzip_opts.file_filter),
+ get_unzip_opt(Rest, Opts#unzip_opts{file_filter = Filter});
+get_unzip_opt([keep_old_files | Rest], Opts) ->
+ Keep = fun keep_old_file/1,
+ Filter = fun_and_1(Keep, Opts#unzip_opts.file_filter),
+ get_unzip_opt(Rest, Opts#unzip_opts{file_filter = Filter});
+get_unzip_opt([Unknown | _Rest], _Opts) ->
+ throw({bad_option, Unknown}).
+
+get_list_dir_opt([], Opts) ->
+ Opts;
+get_list_dir_opt([cooked | Rest], #list_dir_opts{open_opts = OpO} = Opts) ->
+ get_list_dir_opt(Rest, Opts#list_dir_opts{open_opts = OpO -- [raw]});
+get_list_dir_opt([names_only | Rest], Opts) ->
+ get_list_dir_opt(Rest, Opts#list_dir_opts{
+ raw_iterator = fun(A, B, C, D, E) -> raw_name_only(A, B, C, D, E) end});
+%% get_list_dir_opt([{file_output, F} | Rest], Opts) ->
+%% get_list_dir_opt(Rest, Opts#list_dir_opts{file_output = F});
+%% get_list_dir_opt([{file_filter, F} | Rest], Opts) ->
+%% get_list_dir_opt(Rest, Opts#list_dir_opts{file_filter = F});
+get_list_dir_opt([Unknown | _Rest], _Opts) ->
+ throw({bad_option, Unknown}).
+
+get_zip_opt([], Opts) ->
+ Opts;
+get_zip_opt([verbose | Rest], Opts) ->
+ get_zip_opt(Rest, Opts#zip_opts{feedback = fun verbose_zip/1});
+get_zip_opt([cooked | Rest], #zip_opts{open_opts = OpO} = Opts) ->
+ get_zip_opt(Rest, Opts#zip_opts{open_opts = OpO -- [raw]});
+get_zip_opt([memory | Rest], Opts) ->
+ get_zip_opt(Rest, Opts#zip_opts{output = fun binary_io/2});
+get_zip_opt([{cwd, CWD} | Rest], Opts) ->
+ get_zip_opt(Rest, Opts#zip_opts{cwd = CWD});
+get_zip_opt([{comment, C} | Rest], Opts) ->
+ get_zip_opt(Rest, Opts#zip_opts{comment = C});
+get_zip_opt([{compress, Which} = O| Rest], Opts) ->
+ Which2 =
+ case Which of
+ all ->
+ all;
+ Suffixes when is_list(Suffixes) ->
+ lists:usort(Suffixes);
+ {add, Suffixes} when is_list(Suffixes) ->
+ lists:usort(Opts#zip_opts.compress ++ Suffixes);
+ {del, Suffixes} when is_list(Suffixes) ->
+ lists:usort(Opts#zip_opts.compress -- Suffixes);
+ _ ->
+ throw({bad_option, O})
+ end,
+ get_zip_opt(Rest, Opts#zip_opts{compress = Which2});
+get_zip_opt([{uncompress, Which} = O| Rest], Opts) ->
+ Which2 =
+ case Which of
+ all ->
+ all;
+ Suffixes when is_list(Suffixes) ->
+ lists:usort(Suffixes);
+ {add, Suffixes} when is_list(Suffixes) ->
+ lists:usort(Opts#zip_opts.uncompress ++ Suffixes);
+ {del, Suffixes} when is_list(Suffixes) ->
+ lists:usort(Opts#zip_opts.uncompress -- Suffixes);
+ _ ->
+ throw({bad_option, O})
+ end,
+ get_zip_opt(Rest, Opts#zip_opts{uncompress = Which2});
+get_zip_opt([Unknown | _Rest], _Opts) ->
+ throw({bad_option, Unknown}).
+
+
+%% feedback funs
+silent(_) -> ok.
+
+verbose_unzip(FN) -> io:format("extracting: ~p\n", [FN]).
+
+verbose_zip(FN) -> io:format("adding: ~p\n", [FN]).
+
+%% file filter funs
+all(_) -> true.
+
+file_in_list({#zip_file{name = FileName},_}, List) ->
+ lists:member(FileName, List);
+file_in_list(_, _) ->
+ false.
+
+keep_old_file({#zip_file{name = FileName},_}) ->
+ not (filelib:is_file(FileName) orelse filelib:is_dir(FileName));
+keep_old_file(_) ->
+ false.
+
+%% fun combiner
+fun_and_1(Fun1, Fun2) ->
+ fun(A) -> Fun1(A) andalso Fun2(A) end.
+
+%% getting options
+get_zip_options(Files, Options) ->
+ Suffixes = [".Z", ".zip", ".zoo", ".arc", ".lzh", ".arj"],
+ Opts = #zip_opts{output = fun file_io/2,
+ input = get_zip_input({files, Files}),
+ open_opts = [raw, write],
+ comment = "",
+ feedback = fun silent/1,
+ cwd = "",
+ compress = all,
+ uncompress = Suffixes
+ },
+ get_zip_opt(Options, Opts).
+
+get_unzip_options(F, Options) ->
+ Opts = #unzip_opts{file_filter = fun all/1,
+ output = fun file_io/2,
+ input = get_input(F),
+ open_opts = [raw],
+ feedback = fun silent/1,
+ cwd = ""
+ },
+ get_unzip_opt(Options, Opts).
+
+get_openzip_options(Options) ->
+ Opts = #openzip_opts{open_opts = [raw, read],
+ output = fun file_io/2,
+ cwd = ""},
+ get_openzip_opt(Options, Opts).
+
+get_input(F) when is_binary(F) ->
+ fun binary_io/2;
+get_input(F) when is_list(F) ->
+ fun file_io/2.
+
+get_zip_input({F, B}) when is_binary(B), is_list(F) ->
+ fun binary_io/2;
+get_zip_input(F) when is_list(F) ->
+ fun file_io/2;
+get_zip_input({files, []}) ->
+ fun binary_io/2;
+get_zip_input({files, [File | _]}) ->
+ get_zip_input(File).
+
+get_list_dir_options(F, Options) ->
+ Opts = #list_dir_opts{raw_iterator = fun raw_file_info_public/5,
+ input = get_input(F),
+ open_opts = [raw]},
+ get_list_dir_opt(Options, Opts).
+
+%% aliases for erl_tar compatibility
+table(F) -> list_dir(F).
+table(F, O) -> list_dir(F, O).
+create(F, Fs) -> zip(F, Fs).
+create(F, Fs, O) -> zip(F, Fs, O).
+extract(F) -> unzip(F).
+extract(F, O) -> unzip(F, O).
+
+
+%% put the central directory, at the end of the zip archive
+put_central_dir(LHS, Pos, Out0,
+ #zip_opts{output = Output, comment = Comment}) ->
+ {Out1, Sz} = put_cd_files_loop(LHS, Output, Out0, 0),
+ put_eocd(length(LHS), Pos, Sz, Comment, Output, Out1).
+
+put_cd_files_loop([], _Output, Out, Sz) ->
+ {Out, Sz};
+put_cd_files_loop([{LH, Name, Pos} | LHRest], Output, Out0, Sz0) ->
+ CDFH = cd_file_header_from_lh_and_pos(LH, Pos),
+ BCDFH = cd_file_header_to_bin(CDFH),
+ B = [<<?CENTRAL_FILE_MAGIC:32/little>>, BCDFH, Name],
+ Out1 = Output({write, B}, Out0),
+ Sz1 = Sz0 + ?CENTRAL_FILE_HEADER_SZ +
+ LH#local_file_header.file_name_length,
+ put_cd_files_loop(LHRest, Output, Out1, Sz1).
+
+%% put end marker of central directory, the last record in the archive
+put_eocd(N, Pos, Sz, Comment, Output, Out0) ->
+ %% BComment = list_to_binary(Comment),
+ CommentSz = length(Comment), % size(BComment),
+ EOCD = #eocd{disk_num = 0,
+ start_disk_num = 0,
+ entries_on_disk = N,
+ entries = N,
+ size = Sz,
+ offset = Pos,
+ zip_comment_length = CommentSz},
+ BEOCD = eocd_to_bin(EOCD),
+ B = [<<?END_OF_CENTRAL_DIR_MAGIC:32/little>>, BEOCD, Comment], % BComment],
+ Output({write, B}, Out0).
+
+get_filename({Name, _}, Type) ->
+ get_filename(Name, Type);
+get_filename(Name, regular) ->
+ Name;
+get_filename(Name, directory) ->
+ %% Ensure trailing slash
+ case lists:reverse(Name) of
+ [$/ | _Rev] -> Name;
+ Rev -> lists:reverse([$/ | Rev])
+ end.
+
+add_cwd(_CWD, {_Name, _} = F) -> F;
+add_cwd("", F) -> F;
+add_cwd(CWD, F) -> filename:join(CWD, F).
+
+%% already compressed data should be stored as is in archive,
+%% a simple name-match is used to check for this
+%% files smaller than 10 bytes are also stored, not compressed
+get_comp_method(_, N, _, _) when is_integer(N), N < 10 ->
+ ?STORED;
+get_comp_method(_, _, _, directory) ->
+ ?STORED;
+get_comp_method(F, _, #zip_opts{compress = Compress, uncompress = Uncompress}, _) ->
+ Ext = filename:extension(F),
+ Test = fun(Which) -> (Which =:= all) orelse lists:member(Ext, Which) end,
+ case Test(Compress) andalso not Test(Uncompress) of
+ true -> ?DEFLATED;
+ false -> ?STORED
+ end.
+
+put_z_files([], _Z, Out, Pos, _Opts, Acc) ->
+ {Out, lists:reverse(Acc, []), Pos};
+put_z_files([F | Rest], Z, Out0, Pos0,
+ #zip_opts{input = Input, output = Output, open_opts = OpO,
+ feedback = FB, cwd = CWD} = Opts, Acc) ->
+ In0 = [],
+ F1 = add_cwd(CWD, F),
+ FileInfo = Input({file_info, F1}, In0),
+ Type = FileInfo#file_info.type,
+ UncompSize =
+ case Type of
+ regular -> FileInfo#file_info.size;
+ directory -> 0
+ end,
+ FileName = get_filename(F, Type),
+ CompMethod = get_comp_method(FileName, UncompSize, Opts, Type),
+ LH = local_file_header_from_info_method_name(FileInfo, UncompSize, CompMethod, FileName),
+ BLH = local_file_header_to_bin(LH),
+ B = [<<?LOCAL_FILE_MAGIC:32/little>>, BLH],
+ Out1 = Output({write, B}, Out0),
+ Out2 = Output({write, FileName}, Out1),
+ {Out3, CompSize, CRC} = put_z_file(CompMethod, UncompSize, Out2, F1,
+ 0, Input, Output, OpO, Z, Type),
+ FB(FileName),
+ Patch = <<CRC:32/little, CompSize:32/little>>,
+ Out4 = Output({pwrite, Pos0 + ?LOCAL_FILE_HEADER_CRC32_OFFSET, Patch}, Out3),
+ Out5 = Output({seek, eof, 0}, Out4),
+ Pos1 = Pos0 + ?LOCAL_FILE_HEADER_SZ + LH#local_file_header.file_name_length,
+ Pos2 = Pos1 + CompSize,
+ LH2 = LH#local_file_header{comp_size = CompSize, crc32 = CRC},
+ ThisAcc = [{LH2, FileName, Pos0}],
+ {Out6, SubAcc, Pos3} =
+ case Type of
+ regular ->
+ {Out5, ThisAcc, Pos2};
+ directory ->
+ Files = Input({list_dir, F1}, []),
+ RevFiles = reverse_join_files(F, Files, []),
+ put_z_files(RevFiles, Z, Out5, Pos2, Opts, ThisAcc)
+ end,
+ Acc2 = lists:reverse(SubAcc) ++ Acc,
+ put_z_files(Rest, Z, Out6, Pos3, Opts, Acc2).
+
+reverse_join_files(Dir, [File | Files], Acc) ->
+ reverse_join_files(Dir, Files, [filename:join([Dir, File]) | Acc]);
+reverse_join_files(_Dir, [], Acc) ->
+ Acc.
+
+%% flag for zlib
+-define(MAX_WBITS, 15).
+
+%% compress a file
+put_z_file(_Method, Sz, Out, _F, Pos, _Input, _Output, _OpO, _Z, directory) ->
+ {Out, Pos + Sz, 0};
+put_z_file(_Method, 0, Out, _F, Pos, _Input, _Output, _OpO, _Z, regular) ->
+ {Out, Pos, 0};
+put_z_file(?STORED, UncompSize, Out0, F, Pos0, Input, Output, OpO, Z, regular) ->
+ In0 = [],
+ In1 = Input({open, F, OpO -- [write]}, In0),
+ CRC0 = zlib:crc32(Z, <<>>),
+ {Data, In2} = Input({read, UncompSize}, In1),
+ Out1 = Output({write, Data}, Out0),
+ CRC = zlib:crc32(Z, CRC0, Data),
+ Input(close, In2),
+ {Out1, Pos0+erlang:iolist_size(Data), CRC};
+put_z_file(?DEFLATED, UncompSize, Out0, F, Pos0, Input, Output, OpO, Z, regular) ->
+ In0 = [],
+ In1 = Input({open, F, OpO -- [write]}, In0),
+ ok = zlib:deflateInit(Z, default, deflated, -?MAX_WBITS, 8, default),
+ {Out1, Pos1} =
+ put_z_data_loop(UncompSize, In1, Out0, Pos0, Input, Output, Z),
+ CRC = zlib:crc32(Z),
+ ok = zlib:deflateEnd(Z),
+ Input(close, In1),
+ {Out1, Pos1, CRC}.
+
+%% zlib is finished with the last chunk compressed
+get_sync(N, N) -> finish;
+get_sync(_, _) -> full.
+
+%% compress data
+put_z_data_loop(0, _In, Out, Pos, _Input, _Output, _Z) ->
+ {Out, Pos};
+put_z_data_loop(UncompSize, In0, Out0, Pos0, Input, Output, Z) ->
+ N = erlang:min(?WRITE_BLOCK_SIZE, UncompSize),
+ case Input({read, N}, In0) of
+ {eof, _In1} ->
+ {Out0, Pos0};
+ {Uncompressed, In1} ->
+ Compressed = zlib:deflate(Z, Uncompressed, get_sync(N, UncompSize)),
+ Sz = erlang:iolist_size(Compressed),
+ Out1 = Output({write, Compressed}, Out0),
+ put_z_data_loop(UncompSize - N, In1, Out1, Pos0 + Sz,
+ Input, Output, Z)
+ end.
+
+%% raw iterators over central dir
+
+%% name only
+raw_name_only(CD, FileName, _FileComment, _BExtraField, Acc)
+ when is_record(CD, cd_file_header) ->
+ [FileName | Acc];
+raw_name_only(EOCD, _, _Comment, _, Acc) when is_record(EOCD, eocd) ->
+ Acc.
+
+%% for printing directory (t/1)
+raw_short_print_info_etc(CD, FileName, _FileComment, _BExtraField, Acc)
+ when is_record(CD, cd_file_header) ->
+ print_file_name(FileName),
+ Acc;
+raw_short_print_info_etc(EOCD, X, Comment, Y, Acc) when is_record(EOCD, eocd) ->
+ raw_long_print_info_etc(EOCD, X, Comment, Y, Acc).
+
+print_file_name(FileName) ->
+ io:format("~s\n", [FileName]).
+
+
+%% for printing directory (tt/1)
+raw_long_print_info_etc(#cd_file_header{comp_size = CompSize,
+ uncomp_size = UncompSize,
+ last_mod_date = LMDate,
+ last_mod_time = LMTime},
+ FileName, FileComment, _BExtraField, Acc) ->
+ MTime = dos_date_time_to_datetime(LMDate, LMTime),
+ print_header(CompSize, MTime, UncompSize, FileName, FileComment),
+ Acc;
+raw_long_print_info_etc(EOCD, _, Comment, _, Acc) when is_record(EOCD, eocd) ->
+ print_comment(Comment),
+ Acc.
+
+print_header(CompSize, MTime, UncompSize, FileName, FileComment) ->
+ io:format("~8w ~s ~8w ~2w% ~s ~s\n",
+ [CompSize, time_to_string(MTime), UncompSize,
+ get_percent(CompSize, UncompSize), FileName, FileComment]).
+
+print_comment("") ->
+ ok;
+print_comment(Comment) ->
+ io:format("Archive comment: ~s\n", [Comment]).
+
+get_percent(_, 0) -> 100;
+get_percent(CompSize, Size) -> round(CompSize * 100 / Size).
+
+%% time formatting ("borrowed" from erl_tar.erl)
+time_to_string({{Y, Mon, Day}, {H, Min, _}}) ->
+ io_lib:format("~s ~2w ~s:~s ~w",
+ [month(Mon), Day, two_d(H), two_d(Min), Y]).
+
+two_d(N) ->
+ tl(integer_to_list(N + 100)).
+
+month(1) -> "Jan";
+month(2) -> "Feb";
+month(3) -> "Mar";
+month(4) -> "Apr";
+month(5) -> "May";
+month(6) -> "Jun";
+month(7) -> "Jul";
+month(8) -> "Aug";
+month(9) -> "Sep";
+month(10) -> "Oct";
+month(11) -> "Nov";
+month(12) -> "Dec".
+
+%% zip header functions
+cd_file_header_from_lh_and_pos(LH, Pos) ->
+ #local_file_header{version_needed = VersionNeeded,
+ gp_flag = GPFlag,
+ comp_method = CompMethod,
+ last_mod_time = LastModTime,
+ last_mod_date = LastModDate,
+ crc32 = CRC32,
+ comp_size = CompSize,
+ uncomp_size = UncompSize,
+ file_name_length = FileNameLength,
+ extra_field_length = ExtraFieldLength} = LH,
+ #cd_file_header{version_made_by = 20,
+ version_needed = VersionNeeded,
+ gp_flag = GPFlag,
+ comp_method = CompMethod,
+ last_mod_time = LastModTime,
+ last_mod_date = LastModDate,
+ crc32 = CRC32,
+ comp_size = CompSize,
+ uncomp_size = UncompSize,
+ file_name_length = FileNameLength,
+ extra_field_length = ExtraFieldLength,
+ file_comment_length = 0, % FileCommentLength,
+ disk_num_start = 1, % DiskNumStart,
+ internal_attr = 0, % InternalAttr,
+ external_attr = 0, % ExternalAttr,
+ local_header_offset = Pos}.
+
+cd_file_header_to_bin(
+ #cd_file_header{version_made_by = VersionMadeBy,
+ version_needed = VersionNeeded,
+ gp_flag = GPFlag,
+ comp_method = CompMethod,
+ last_mod_time = LastModTime,
+ last_mod_date = LastModDate,
+ crc32 = CRC32,
+ comp_size = CompSize,
+ uncomp_size = UncompSize,
+ file_name_length = FileNameLength,
+ extra_field_length = ExtraFieldLength,
+ file_comment_length = FileCommentLength,
+ disk_num_start = DiskNumStart,
+ internal_attr = InternalAttr,
+ external_attr = ExternalAttr,
+ local_header_offset = LocalHeaderOffset}) ->
+ <<VersionMadeBy:16/little,
+ VersionNeeded:16/little,
+ GPFlag:16/little,
+ CompMethod:16/little,
+ LastModTime:16/little,
+ LastModDate:16/little,
+ CRC32:32/little,
+ CompSize:32/little,
+ UncompSize:32/little,
+ FileNameLength:16/little,
+ ExtraFieldLength:16/little,
+ FileCommentLength:16/little,
+ DiskNumStart:16/little,
+ InternalAttr:16/little,
+ ExternalAttr:32/little,
+ LocalHeaderOffset:32/little>>.
+
+local_file_header_to_bin(
+ #local_file_header{version_needed = VersionNeeded,
+ gp_flag = GPFlag,
+ comp_method = CompMethod,
+ last_mod_time = LastModTime,
+ last_mod_date = LastModDate,
+ crc32 = CRC32,
+ comp_size = CompSize,
+ uncomp_size = UncompSize,
+ file_name_length = FileNameLength,
+ extra_field_length = ExtraFieldLength}) ->
+ <<VersionNeeded:16/little,
+ GPFlag:16/little,
+ CompMethod:16/little,
+ LastModTime:16/little,
+ LastModDate:16/little,
+ CRC32:32/little,
+ CompSize:32/little,
+ UncompSize:32/little,
+ FileNameLength:16/little,
+ ExtraFieldLength:16/little>>.
+
+eocd_to_bin(#eocd{disk_num = DiskNum,
+ start_disk_num = StartDiskNum,
+ entries_on_disk = EntriesOnDisk,
+ entries = Entries,
+ size = Size,
+ offset = Offset,
+ zip_comment_length = ZipCommentLength}) ->
+ <<DiskNum:16/little,
+ StartDiskNum:16/little,
+ EntriesOnDisk:16/little,
+ Entries:16/little,
+ Size:32/little,
+ Offset:32/little,
+ ZipCommentLength:16/little>>.
+
+%% put together a local file header
+local_file_header_from_info_method_name(#file_info{mtime = MTime},
+ UncompSize,
+ CompMethod, Name) ->
+ {ModDate, ModTime} = dos_date_time_from_datetime(MTime),
+ #local_file_header{version_needed = 20,
+ gp_flag = 0,
+ comp_method = CompMethod,
+ last_mod_time = ModTime,
+ last_mod_date = ModDate,
+ crc32 = -1,
+ comp_size = -1,
+ uncomp_size = UncompSize,
+ file_name_length = length(Name),
+ extra_field_length = 0}.
+
+
+%% small, simple, stupid zip-archive server
+server_loop(OpenZip) ->
+ receive
+ {From, {open, Archive, Options}} ->
+ case openzip_open(Archive, Options) of
+ {ok, NewOpenZip} ->
+ From ! {self(), {ok, self()}},
+ server_loop(NewOpenZip);
+ Error ->
+ From ! {self(), Error}
+ end;
+ {From, close} ->
+ From ! {self(), openzip_close(OpenZip)};
+ {From, get} ->
+ From ! {self(), openzip_get(OpenZip)},
+ server_loop(OpenZip);
+ {From, {get, FileName}} ->
+ From ! {self(), openzip_get(FileName, OpenZip)},
+ server_loop(OpenZip);
+ {From, list_dir} ->
+ From ! {self(), openzip_list_dir(OpenZip)},
+ server_loop(OpenZip);
+ {From, {list_dir, Opts}} ->
+ From ! {self(), openzip_list_dir(OpenZip, Opts)},
+ server_loop(OpenZip);
+ {From, get_state} ->
+ From ! {self(), OpenZip},
+ server_loop(OpenZip);
+ _ ->
+ {error, bad_msg}
+ end.
+
+zip_open(Archive) -> zip_open(Archive, []).
+
+zip_open(Archive, Options) ->
+ Pid = spawn(fun() -> server_loop(not_open) end),
+ request(self(), Pid, {open, Archive, Options}).
+
+zip_get(Pid) when is_pid(Pid) ->
+ request(self(), Pid, get).
+
+zip_close(Pid) when is_pid(Pid) ->
+ request(self(), Pid, close).
+
+zip_get(FileName, Pid) when is_pid(Pid) ->
+ request(self(), Pid, {get, FileName}).
+
+zip_list_dir(Pid) when is_pid(Pid) ->
+ request(self(), Pid, list_dir).
+
+zip_list_dir(Pid, Opts) when is_pid(Pid) ->
+ request(self(), Pid, {list_dir, Opts}).
+
+zip_get_state(Pid) when is_pid(Pid) ->
+ request(self(), Pid, get_state).
+
+request(Self, Pid, Req) ->
+ Pid ! {Self, Req},
+ receive
+ {Pid, R} -> R
+ end.
+
+zip_t(Pid) when is_pid(Pid) ->
+ Openzip = request(self(), Pid, get_state),
+ openzip_t(Openzip).
+
+zip_tt(Pid) when is_pid(Pid) ->
+ Openzip = request(self(), Pid, get_state),
+ openzip_tt(Openzip).
+
+openzip_tt(#openzip{zip_comment = ZipComment, files = Files}) ->
+ print_comment(ZipComment),
+ lists_foreach(fun({#zip_file{comp_size = CompSize,
+ name = FileName,
+ comment = FileComment,
+ info = FI},_}) ->
+ #file_info{size = UncompSize, mtime = MTime} = FI,
+ print_header(CompSize, MTime, UncompSize,
+ FileName, FileComment)
+ end, Files),
+ ok.
+
+openzip_t(#openzip{zip_comment = ZipComment, files = Files}) ->
+ print_comment(ZipComment),
+ lists_foreach(fun({#zip_file{name = FileName},_}) ->
+ print_file_name(FileName)
+ end, Files),
+ ok.
+
+lists_foreach(_, []) ->
+ ok;
+lists_foreach(F, [Hd|Tl]) ->
+ F(Hd),
+ lists_foreach(F, Tl).
+
+%% option utils
+get_openzip_opt([], Opts) ->
+ Opts;
+get_openzip_opt([cooked | Rest], #openzip_opts{open_opts = OO} = Opts) ->
+ get_openzip_opt(Rest, Opts#openzip_opts{open_opts = OO -- [raw]});
+get_openzip_opt([memory | Rest], Opts) ->
+ get_openzip_opt(Rest, Opts#openzip_opts{output = fun binary_io/2});
+get_openzip_opt([{cwd, CWD} | Rest], Opts) ->
+ get_openzip_opt(Rest, Opts#openzip_opts{cwd = CWD});
+get_openzip_opt([Unknown | _Rest], _Opts) ->
+ throw({bad_option, Unknown}).
+
+%% get the central directory from the archive
+get_central_dir(In0, RawIterator, Input) ->
+ {B, In1} = get_end_of_central_dir(In0, ?END_OF_CENTRAL_DIR_SZ, Input),
+ {EOCD, BComment} = eocd_and_comment_from_bin(B),
+ In2 = Input({seek, bof, EOCD#eocd.offset}, In1),
+ N = EOCD#eocd.entries,
+ Acc0 = [],
+ Out0 = RawIterator(EOCD, "", binary_to_list(BComment), <<>>, Acc0),
+ get_cd_loop(N, In2, RawIterator, Input, Out0).
+
+get_cd_loop(0, In, _RawIterator, _Input, Acc) ->
+ {lists:reverse(Acc), In};
+get_cd_loop(N, In0, RawIterator, Input, Acc0) ->
+ {B, In1} = Input({read, ?CENTRAL_FILE_HEADER_SZ}, In0),
+ BCD = case B of
+ <<?CENTRAL_FILE_MAGIC:32/little, XBCD/binary>> -> XBCD;
+ _ -> throw(bad_central_directory)
+ end,
+ CD = cd_file_header_from_bin(BCD),
+ FileNameLen = CD#cd_file_header.file_name_length,
+ ExtraLen = CD#cd_file_header.extra_field_length,
+ CommentLen = CD#cd_file_header.file_comment_length,
+ ToRead = FileNameLen + ExtraLen + CommentLen,
+ {B2, In2} = Input({read, ToRead}, In1),
+ {FileName, Comment, BExtra} =
+ get_name_extra_comment(B2, FileNameLen, ExtraLen, CommentLen),
+ Acc1 = RawIterator(CD, FileName, Comment, BExtra, Acc0),
+ get_cd_loop(N-1, In2, RawIterator, Input, Acc1).
+
+get_name_extra_comment(B, FileNameLen, ExtraLen, CommentLen) ->
+ case B of
+ <<BFileName:FileNameLen/binary,
+ BExtra:ExtraLen/binary,
+ BComment:CommentLen/binary>> ->
+ {binary_to_list(BFileName), binary_to_list(BComment), BExtra};
+ _ ->
+ throw(bad_central_directory)
+ end.
+
+%% get end record, containing the offset to the central directory
+%% the end record is always at the end of the file BUT alas it is
+%% of variable size (yes that's dumb!)
+get_end_of_central_dir(_In, Sz, _Input) when Sz > 16#ffff ->
+ throw(bad_eocd);
+get_end_of_central_dir(In0, Sz, Input) ->
+ In1 = Input({seek, eof, -Sz}, In0),
+ {B, In2} = Input({read, Sz}, In1),
+ case find_eocd_header(B) of
+ none ->
+ get_end_of_central_dir(In2, Sz+Sz, Input);
+ Header ->
+ {Header, In2}
+ end.
+
+%% find the end record by matching for it
+find_eocd_header(<<?END_OF_CENTRAL_DIR_MAGIC:32/little, Rest/binary>>) ->
+ Rest;
+find_eocd_header(<<_:8, Rest/binary>>)
+ when byte_size(Rest) > ?END_OF_CENTRAL_DIR_SZ-4 ->
+ find_eocd_header(Rest);
+find_eocd_header(_) ->
+ none.
+
+%% from a central directory record, filter and accumulate what we need
+
+%% with zip_file_extra
+raw_file_info_etc(CD, FileName, FileComment, BExtraField, Acc)
+ when is_record(CD, cd_file_header) ->
+ #cd_file_header{comp_size = CompSize,
+ local_header_offset = Offset,
+ crc32 = CRC} = CD,
+ FileInfo = cd_file_header_to_file_info(FileName, CD, BExtraField),
+ [{#zip_file{name = FileName, info = FileInfo, comment = FileComment,
+ offset = Offset, comp_size = CompSize}, #zip_file_extra{crc32 = CRC}} | Acc];
+raw_file_info_etc(EOCD, _, Comment, _, Acc) when is_record(EOCD, eocd) ->
+ [#zip_comment{comment = Comment} | Acc].
+
+%% without zip_file_extra
+raw_file_info_public(CD, FileName, FileComment, BExtraField, Acc0) ->
+ [H1|T] = raw_file_info_etc(CD,FileName,FileComment,BExtraField,Acc0),
+ H2 = case H1 of
+ {ZF,Extra} when is_record(Extra,zip_file_extra) -> ZF;
+ Other -> Other
+ end,
+ [H2|T].
+
+
+%% make a file_info from a central directory header
+cd_file_header_to_file_info(FileName,
+ #cd_file_header{uncomp_size = UncompSize,
+ last_mod_time = ModTime,
+ last_mod_date = ModDate},
+ ExtraField) ->
+ T = dos_date_time_to_datetime(ModDate, ModTime),
+ Type =
+ case lists:last(FileName) of
+ $/ -> directory;
+ _ -> regular
+ end,
+ FI = #file_info{size = UncompSize,
+ type = Type,
+ access = read_write,
+ atime = T,
+ mtime = T,
+ ctime = T,
+ mode = 8#066,
+ links = 1,
+ major_device = 0,
+ minor_device = 0,
+ inode = 0,
+ uid = 0,
+ gid = 0},
+ add_extra_info(FI, ExtraField).
+
+%% add extra info to file (some day when we implement it)
+add_extra_info(FI, <<?EXTENDED_TIMESTAMP_TAG:16/little, _Rest/binary>>) ->
+ FI; % not yet supported, some other day...
+add_extra_info(FI, <<?UNIX_EXTRA_FIELD_TAG:16/little, Rest/binary>>) ->
+ _UnixExtra = unix_extra_field_and_var_from_bin(Rest),
+ FI; % not yet supported, and not widely used
+add_extra_info(FI, _) ->
+ FI.
+
+
+
+%% get all files using file list
+%% (the offset list is already filtered on which file to get... isn't it?)
+get_z_files([], _Z, _In, _Opts, Acc) ->
+ lists:reverse(Acc);
+get_z_files([#zip_comment{comment = _} | Rest], Z, In, Opts, Acc) ->
+ get_z_files(Rest, Z, In, Opts, Acc);
+get_z_files([{#zip_file{offset = Offset},_} = ZFile | Rest], Z, In0,
+ #unzip_opts{input = Input, output = Output, open_opts = OpO,
+ file_filter = Filter, feedback = FB,
+ cwd = CWD} = Opts, Acc0) ->
+ case Filter(ZFile) of
+ true ->
+ In1 = Input({seek, bof, Offset}, In0),
+ {In2, Acc1} =
+ case get_z_file(In1, Z, Input, Output, OpO, FB, CWD, ZFile) of
+ {file, GZD, Inx} -> {Inx, [GZD | Acc0]};
+ {dir, Inx} -> {Inx, Acc0}
+ end,
+ get_z_files(Rest, Z, In2, Opts, Acc1);
+ _ ->
+ get_z_files(Rest, Z, In0, Opts, Acc0)
+ end.
+
+%% get a file from the archive, reading chunks
+get_z_file(In0, Z, Input, Output, OpO, FB, CWD, {ZipFile,Extra}) ->
+ case Input({read, ?LOCAL_FILE_HEADER_SZ}, In0) of
+ {eof, In1} ->
+ {eof, In1};
+ %% Local File Header
+ {<<?LOCAL_FILE_MAGIC:32/little, B/binary>>, In1} ->
+ LH = local_file_header_from_bin(B),
+ #local_file_header{gp_flag = GPFlag,
+ comp_method = CompMethod,
+ file_name_length = FileNameLen,
+ extra_field_length = ExtraLen} = LH,
+
+ {CompSize,CRC32} = case GPFlag band 8 =:= 8 of
+ true -> {ZipFile#zip_file.comp_size,
+ Extra#zip_file_extra.crc32};
+ false -> {LH#local_file_header.comp_size,
+ LH#local_file_header.crc32}
+ end,
+ {BFileN, In3} = Input({read, FileNameLen + ExtraLen}, In1),
+ {FileName, _} = get_file_name_extra(FileNameLen, ExtraLen, BFileN),
+ FileName1 = add_cwd(CWD, FileName),
+ case lists:last(FileName) of
+ $/ ->
+ %% perhaps this should always be done?
+ Output({ensure_dir,FileName1},[]),
+ {dir, In3};
+ _ ->
+ %% FileInfo = local_file_header_to_file_info(LH)
+ %%{Out, In4, CRC, UncompSize} =
+ {Out, In4, CRC, _UncompSize} =
+ get_z_data(CompMethod, In3, FileName1,
+ CompSize, Input, Output, OpO, Z),
+ In5 = skip_z_data_descriptor(GPFlag, Input, In4),
+ %% TODO This should be fixed some day:
+ %% In5 = Input({set_file_info, FileName, FileInfo#file_info{size=UncompSize}}, In4),
+ FB(FileName),
+ CRC =:= CRC32 orelse throw({bad_crc, FileName}),
+ {file, Out, In5}
+ end;
+ _ ->
+ throw(bad_local_file_header)
+ end.
+
+
+get_file_name_extra(FileNameLen, ExtraLen, B) ->
+ case B of
+ <<BFileName:FileNameLen/binary, BExtra:ExtraLen/binary>> ->
+ {binary_to_list(BFileName), BExtra};
+ _ ->
+ throw(bad_file_header)
+ end.
+
+%% get compressed or stored data
+get_z_data(?DEFLATED, In0, FileName, CompSize, Input, Output, OpO, Z) ->
+ ok = zlib:inflateInit(Z, -?MAX_WBITS),
+ Out0 = Output({open, FileName, [write | OpO]}, []),
+ {In1, Out1, UncompSize} = get_z_data_loop(CompSize, 0, In0, Out0, Input, Output, Z),
+ CRC = zlib:crc32(Z),
+ ?CATCH zlib:inflateEnd(Z),
+ Out2 = Output({close, FileName}, Out1),
+ {Out2, In1, CRC, UncompSize};
+get_z_data(?STORED, In0, FileName, CompSize, Input, Output, OpO, Z) ->
+ Out0 = Output({open, FileName, [write | OpO]}, []),
+ CRC0 = zlib:crc32(Z, <<>>),
+ {In1, Out1, CRC} = copy_data_loop(CompSize, In0, Out0, Input, Output,
+ CRC0, Z),
+ Out2 = Output({close, FileName}, Out1),
+ {Out2, In1, CRC, CompSize};
+get_z_data(_, _, _, _, _, _, _, _) ->
+ throw(bad_file_header).
+
+copy_data_loop(0, In, Out, _Input, _Output, CRC, _Z) ->
+ {In, Out, CRC};
+copy_data_loop(CompSize, In0, Out0, Input, Output, CRC0, Z) ->
+ N = erlang:min(?READ_BLOCK_SIZE, CompSize),
+ case Input({read, N}, In0) of
+ {eof, In1} -> {Out0, In1};
+ {Uncompressed, In1} ->
+ CRC1 = zlib:crc32(Z, CRC0, Uncompressed),
+ Out1 = Output({write, Uncompressed}, Out0),
+ copy_data_loop(CompSize-N, In1, Out1, Input, Output, CRC1, Z)
+ end.
+
+get_z_data_loop(0, UncompSize, In, Out, _Input, _Output, _Z) ->
+ {In, Out, UncompSize};
+get_z_data_loop(CompSize, UncompSize, In0, Out0, Input, Output, Z) ->
+ N = erlang:min(?READ_BLOCK_SIZE, CompSize),
+ case Input({read, N}, In0) of
+ {eof, In1} ->
+ {Out0, In1};
+ {Compressed, In1} ->
+ Uncompressed = zlib:inflate(Z, Compressed),
+ Out1 = Output({write, Uncompressed}, Out0),
+ get_z_data_loop(CompSize-N, UncompSize + iolist_size(Uncompressed),
+ In1, Out1, Input, Output, Z)
+ end.
+
+
+%% skip data descriptor if any
+skip_z_data_descriptor(GPFlag, Input, In0) when GPFlag band 8 =:= 8 ->
+ Input({seek, cur, 12}, In0);
+skip_z_data_descriptor(_GPFlag, _Input, In0) ->
+ In0.
+
+%% convert between erlang datetime and the MSDOS date and time
+%% that's stored in the zip archive
+%% MSDOS Time MSDOS Date
+%% bit 0 - 4 5 - 10 11 - 15 16 - 20 21 - 24 25 - 31
+%% value second minute hour day (1 - 31) month (1 - 12) years from 1980
+dos_date_time_to_datetime(DosDate, DosTime) ->
+ <<Hour:5, Min:6, Sec:5>> = <<DosTime:16>>,
+ <<YearFrom1980:7, Month:4, Day:5>> = <<DosDate:16>>,
+ {{YearFrom1980+1980, Month, Day},
+ {Hour, Min, Sec}}.
+
+dos_date_time_from_datetime({{Year, Month, Day}, {Hour, Min, Sec}}) ->
+ YearFrom1980 = Year-1980,
+ <<DosTime:16>> = <<Hour:5, Min:6, Sec:5>>,
+ <<DosDate:16>> = <<YearFrom1980:7, Month:4, Day:5>>,
+ {DosDate, DosTime}.
+
+unix_extra_field_and_var_from_bin(<<TSize:16/little,
+ ATime:32/little,
+ MTime:32/little,
+ UID:16/little,
+ GID:16/little,
+ Var:TSize/binary>>) ->
+ {#unix_extra_field{atime = ATime,
+ mtime = MTime,
+ uid = UID,
+ gid = GID},
+ Var};
+unix_extra_field_and_var_from_bin(_) ->
+ throw(bad_unix_extra_field).
+
+
+%% A pwrite-like function for iolists (used by memory-option)
+
+split_iolist(B, Pos) when is_binary(B) ->
+ split_binary(B, Pos);
+split_iolist(L, Pos) when is_list(L) ->
+ splitter([], L, Pos).
+
+splitter(Left, Right, 0) ->
+ {Left, Right};
+splitter(Left, [A | Right], RelPos) when is_list(A) or is_binary(A) ->
+ Sz = erlang:iolist_size(A),
+ case Sz > RelPos of
+ true ->
+ {Leftx, Rightx} = split_iolist(A, RelPos),
+ {[Left | Leftx], [Rightx, Right]};
+ _ ->
+ splitter([Left | A], Right, RelPos - Sz)
+ end;
+splitter(Left, [A | Right], RelPos) when is_integer(A) ->
+ splitter([Left, A], Right, RelPos - 1);
+splitter(Left, Right, RelPos) when is_binary(Right) ->
+ splitter(Left, [Right], RelPos).
+
+skip_iolist(B, Pos) when is_binary(B) ->
+ case B of
+ <<_:Pos/binary, Bin/binary>> -> Bin;
+ _ -> <<>>
+ end;
+skip_iolist(L, Pos) when is_list(L) ->
+ skipper(L, Pos).
+
+skipper(Right, 0) ->
+ Right;
+skipper([A | Right], RelPos) when is_list(A) or is_binary(A) ->
+ Sz = erlang:iolist_size(A),
+ case Sz > RelPos of
+ true ->
+ Rightx = skip_iolist(A, RelPos),
+ [Rightx, Right];
+ _ ->
+ skip_iolist(Right, RelPos - Sz)
+ end;
+skipper([A | Right], RelPos) when is_integer(A) ->
+ skip_iolist(Right, RelPos - 1).
+
+pwrite_iolist(Iolist, Pos, Bin) ->
+ {Left, Right} = split_iolist(Iolist, Pos),
+ Sz = erlang:iolist_size(Bin),
+ R = skip_iolist(Right, Sz),
+ [Left, Bin | R].
+
+pwrite_binary(B, Pos, Bin) ->
+ erlang:iolist_to_binary(pwrite_iolist(B, Pos, Bin)).
+
+
+%% ZIP header manipulations
+eocd_and_comment_from_bin(<<DiskNum:16/little,
+ StartDiskNum:16/little,
+ EntriesOnDisk:16/little,
+ Entries:16/little,
+ Size:32/little,
+ Offset:32/little,
+ ZipCommentLength:16/little,
+ Comment:ZipCommentLength/binary>>) ->
+ {#eocd{disk_num = DiskNum,
+ start_disk_num = StartDiskNum,
+ entries_on_disk = EntriesOnDisk,
+ entries = Entries,
+ size = Size,
+ offset = Offset,
+ zip_comment_length = ZipCommentLength},
+ Comment};
+eocd_and_comment_from_bin(_) ->
+ throw(bad_eocd).
+
+cd_file_header_from_bin(<<VersionMadeBy:16/little,
+ VersionNeeded:16/little,
+ GPFlag:16/little,
+ CompMethod:16/little,
+ LastModTime:16/little,
+ LastModDate:16/little,
+ CRC32:32/little,
+ CompSize:32/little,
+ UncompSize:32/little,
+ FileNameLength:16/little,
+ ExtraFieldLength:16/little,
+ FileCommentLength:16/little,
+ DiskNumStart:16/little,
+ InternalAttr:16/little,
+ ExternalAttr:32/little,
+ LocalHeaderOffset:32/little>>) ->
+ #cd_file_header{version_made_by = VersionMadeBy,
+ version_needed = VersionNeeded,
+ gp_flag = GPFlag,
+ comp_method = CompMethod,
+ last_mod_time = LastModTime,
+ last_mod_date = LastModDate,
+ crc32 = CRC32,
+ comp_size = CompSize,
+ uncomp_size = UncompSize,
+ file_name_length = FileNameLength,
+ extra_field_length = ExtraFieldLength,
+ file_comment_length = FileCommentLength,
+ disk_num_start = DiskNumStart,
+ internal_attr = InternalAttr,
+ external_attr = ExternalAttr,
+ local_header_offset = LocalHeaderOffset};
+cd_file_header_from_bin(_) ->
+ throw(bad_cd_file_header).
+
+local_file_header_from_bin(<<VersionNeeded:16/little,
+ GPFlag:16/little,
+ CompMethod:16/little,
+ LastModTime:16/little,
+ LastModDate:16/little,
+ CRC32:32/little,
+ CompSize:32/little,
+ UncompSize:32/little,
+ FileNameLength:16/little,
+ ExtraFieldLength:16/little>>) ->
+ #local_file_header{version_needed = VersionNeeded,
+ gp_flag = GPFlag,
+ comp_method = CompMethod,
+ last_mod_time = LastModTime,
+ last_mod_date = LastModDate,
+ crc32 = CRC32,
+ comp_size = CompSize,
+ uncomp_size = UncompSize,
+ file_name_length = FileNameLength,
+ extra_field_length = ExtraFieldLength};
+local_file_header_from_bin(_) ->
+ throw(bad_local_file_header).
+
+%% make a file_info from a local directory header
+%% local_file_header_to_file_info(
+%% #local_file_header{last_mod_time = ModTime,
+%% last_mod_date = ModDate,
+%% uncomp_size = UncompSize}) ->
+%% T = dos_date_time_to_datetime(ModDate, ModTime),
+%% FI = #file_info{size = UncompSize,
+%% type = regular,
+%% access = read_write,
+%% atime = T,
+%% mtime = T,
+%% ctime = T,
+%% mode = 8#066,
+%% links = 1,
+%% major_device = 0,
+%% minor_device = 0,
+%% inode = 0,
+%% uid = 0,
+%% gid = 0},
+%% FI.
+
+%% io functions
+binary_io({file_info, {_Filename, _B, #file_info{} = FI}}, _A) ->
+ FI;
+binary_io({file_info, {_Filename, B}}, A) ->
+ binary_io({file_info, B}, A);
+binary_io({file_info, B}, _) ->
+ {Type, Size} =
+ if
+ is_binary(B) -> {regular, byte_size(B)};
+ B =:= directory -> {directory, 0}
+ end,
+ Now = calendar:local_time(),
+ #file_info{size = Size, type = Type,
+ access = read_write, atime = Now,
+ mtime = Now, ctime = Now, mode = 0,
+ links = 1, major_device = 0,
+ minor_device = 0, inode = 0,
+ uid = 0, gid = 0};
+binary_io({open, {_Filename, B, _FI}, _Opts}, _) ->
+ {0, B};
+binary_io({open, {_Filename, B}, _Opts}, _) ->
+ {0, B};
+binary_io({open, B, _Opts}, _) when is_binary(B) ->
+ {0, B};
+binary_io({open, Filename, _Opts}, _) when is_list(Filename) ->
+ {0, <<>>};
+binary_io({read, N}, {Pos, B}) when Pos >= byte_size(B) ->
+ {eof, {Pos+N, B}};
+binary_io({read, N}, {Pos, B}) when Pos + N > byte_size(B) ->
+ <<_:Pos/binary, Read/binary>> = B,
+ {Read, {byte_size(B), B}};
+binary_io({pread, Pos, N}, {OldPos, B}) ->
+ case B of
+ <<_:Pos/binary, Read:N/binary, _Rest/binary>> ->
+ {Read, {Pos+N, B}};
+ _ ->
+ {eof, {OldPos, B}}
+ end;
+binary_io({read, N}, {Pos, B}) ->
+ <<_:Pos/binary, Read:N/binary, _/binary>> = B,
+ {Read, {Pos+N, B}};
+binary_io({seek, bof, Pos}, {_OldPos, B}) ->
+ {Pos, B};
+binary_io({seek, cur, Pos}, {OldPos, B}) ->
+ {OldPos + Pos, B};
+binary_io({seek, eof, Pos}, {_OldPos, B}) ->
+ {byte_size(B) + Pos, B};
+binary_io({pwrite, Pos, Data}, {OldPos, B}) ->
+ {OldPos, pwrite_binary(B, Pos, Data)};
+binary_io({write, Data}, {Pos, B}) ->
+ {Pos + erlang:iolist_size(Data), pwrite_binary(B, Pos, Data)};
+binary_io(close, {_Pos, B}) ->
+ B;
+binary_io({close, FN}, {_Pos, B}) ->
+ {FN, B};
+binary_io({list_dir, _F}, _B) ->
+ [];
+binary_io({set_file_info, _F, _FI}, B) ->
+ B;
+binary_io({ensure_dir, _Dir}, B) ->
+ B.
+
+file_io({file_info, F}, _) ->
+ case file:read_file_info(F) of
+ {ok, Info} -> Info;
+ {error, E} -> throw(E)
+ end;
+file_io({open, FN, Opts}, _) ->
+ case lists:member(write, Opts) of
+ true -> ok = filelib:ensure_dir(FN);
+ _ -> ok
+ end,
+ case file:open(FN, Opts++[binary]) of
+ {ok, H} -> H;
+ {error, E} -> throw(E)
+ end;
+file_io({read, N}, H) ->
+ case file:read(H, N) of
+ {ok, B} -> {B, H};
+ eof -> {eof, H};
+ {error, E} -> throw(E)
+ end;
+file_io({pread, Pos, N}, H) ->
+ case file:pread(H, Pos, N) of
+ {ok, B} -> {B, H};
+ eof -> {eof, H};
+ {error, E} -> throw(E)
+ end;
+file_io({seek, S, Pos}, H) ->
+ case file:position(H, {S, Pos}) of
+ {ok, _NewPos} -> H;
+ {error, Error} -> throw(Error)
+ end;
+file_io({write, Data}, H) ->
+ case file:write(H, Data) of
+ ok -> H;
+ {error, Error} -> throw(Error)
+ end;
+file_io({pwrite, Pos, Data}, H) ->
+ case file:pwrite(H, Pos, Data) of
+ ok -> H;
+ {error, Error} -> throw(Error)
+ end;
+file_io({close, FN}, H) ->
+ case file:close(H) of
+ ok -> FN;
+ {error, Error} -> throw(Error)
+ end;
+file_io(close, H) ->
+ file_io({close, ok}, H);
+file_io({list_dir, F}, _H) ->
+ case file:list_dir(F) of
+ {ok, Files} -> Files;
+ {error, Error} -> throw(Error)
+ end;
+file_io({set_file_info, F, FI}, H) ->
+ case file:write_file_info(F, FI) of
+ ok -> H;
+ {error, Error} -> throw(Error)
+ end;
+file_io({ensure_dir, Dir}, H) ->
+ ok = filelib:ensure_dir(Dir),
+ H.