diff options
Diffstat (limited to 'lib/stdlib/src/zip.erl')
-rw-r--r-- | lib/stdlib/src/zip.erl | 1600 |
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. |