1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2006-2019. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20-module(zip).
21
22%% Basic api
23-export([unzip/1, unzip/2, extract/1, extract/2,
24	 zip/2, zip/3, create/2, create/3, foldl/3,
25	 list_dir/1, list_dir/2, table/1, table/2,
26	 t/1, tt/1]).
27
28%% unzipping piecemeal
29-export([openzip_open/1, openzip_open/2,
30	 openzip_get/1, openzip_get/2,
31	 openzip_t/1, openzip_tt/1,
32	 openzip_list_dir/1, openzip_list_dir/2,
33	 openzip_close/1]).
34%% 	 openzip_add/2]).
35
36%% zip server
37-export([zip_open/1, zip_open/2,
38	 zip_get/1, zip_get/2,
39	 zip_t/1, zip_tt/1,
40	 zip_list_dir/1, zip_list_dir/2,
41	 zip_close/1]).
42
43%% just for debugging zip server, not documented, not tested, not to be used
44-export([zip_get_state/1]).
45
46%% includes
47-include("file.hrl").		 % #file_info
48-include("zip.hrl").	         % #zip_file, #zip_comment
49
50%% max bytes fed to zlib
51-define(WRITE_BLOCK_SIZE, 8*1024).
52
53%% for debugging, to turn off catch
54-define(CATCH, catch).
55
56%% Debug.
57-define(SHOW_GP_BIT_11(B, F), ok).
58%%-define(SHOW_GP_BIT_11(B, F), io:format("F = ~.16#, B = ~lp\n", [F, B])).
59
60%% option sets
61-record(unzip_opts, {
62	  output,      % output object (fun)
63	  input,       % input object (fun)
64	  file_filter, % file filter (boolean fun)
65	  open_opts,   % options passed to file:open
66	  feedback,    % feeback (fun)
67	  cwd          % directory to relate paths to
68	 }).
69
70-record(zip_opts, {
71	  output,      % output object (fun)
72	  input,       % input object (fun)
73	  comment,     % zip-file comment
74	  open_opts,   % options passed to file:open
75	  feedback,    % feeback (fun)
76	  cwd,         % directory to relate paths to
77	  compress,    % compress files with these suffixes
78	  uncompress   % uncompress files with these suffixes
79	 }).
80
81-record(list_dir_opts, {
82	  input,       % input object (fun)
83	  raw_iterator, % applied to each dir entry
84	  open_opts    % options passed to file:open
85	 }).
86
87-record(openzip_opts, {
88	  output,      % output object (fun)
89	  open_opts,   % file:open options
90	  cwd	       % directory to relate paths to
91	 }).
92
93% openzip record, state for an open zip-file
94-record(openzip, {
95	  zip_comment, % zip archive comment
96	  files,       % filenames, infos, comments and offsets
97	  in,          % archive handle
98	  input,       % archive io object (fun)
99	  output,      % output io object (fun)
100	  zlib,	       % handle to open zlib
101	  cwd	       % directory to relate paths to
102	 }).
103
104% Things that I would like to add to the public record #zip_file,
105% but can't as it would make things fail at upgrade.
106% Instead we use {#zip_file,#zip_file_extra} internally.
107-record(zip_file_extra, {
108	  crc32        % checksum
109	 }).
110
111%% max bytes read from files and archives (and fed to zlib)
112-define(READ_BLOCK_SIZE, 16*1024).
113
114%% -record(primzip_file, {
115%% 	  name,
116%% 	  offset,
117%% 	  chunk_size
118%% 	 }).
119
120%% -record(primzip, {
121%% 	  zlib,		% handle to the zlib port from zlib:open
122%% 	  input,        % fun/2 for file/memory input
123%% 	  in,		% input (file handle or binary)
124%% 	  files		% [#primzip_file]
125%% 	 }).
126
127%% ZIP-file format records and defines
128
129%% compression methods
130-define(STORED, 0).
131-define(UNCOMPRESSED, 0).
132-define(SHRUNK, 1).
133-define(REDUCED_1, 2).
134-define(REDUCED_2, 3).
135-define(REDUCED_3, 4).
136-define(REDUCED_4, 5).
137-define(IMPLODED, 6).
138-define(TOKENIZED, 7).
139-define(DEFLATED, 8).
140-define(DEFLATED_64, 9).
141-define(PKWARE_IMPLODED, 10).
142-define(PKWARE_RESERVED, 11).
143-define(BZIP2_COMPRESSED, 12).
144
145%% Version 2.0, attribute compatibility type 3 (Unix)
146-define(VERSION_MADE_BY, 20 bor (3 bsl 8)).
147-define(GP_BIT_11, 16#800). % Filename and file comment UTF-8 encoded.
148
149%% zip-file records
150-define(LOCAL_FILE_MAGIC,16#04034b50).
151-define(LOCAL_FILE_HEADER_SZ,(4+2+2+2+2+2+4+4+4+2+2)).
152-define(LOCAL_FILE_HEADER_CRC32_OFFSET, 4+2+2+2+2+2).
153-record(local_file_header, {version_needed,
154			    gp_flag,
155			    comp_method,
156			    last_mod_time,
157			    last_mod_date,
158			    crc32,
159			    comp_size,
160			    uncomp_size,
161			    file_name_length,
162			    extra_field_length,
163                            type}).
164
165-define(CENTRAL_FILE_HEADER_SZ,(4+2+2+2+2+2+2+4+4+4+2+2+2+2+2+4+4)).
166
167-define(CENTRAL_DIR_MAGIC, 16#06054b50).
168-define(CENTRAL_DIR_SZ, (4+2+2+2+2+4+4+2)).
169-define(CENTRAL_DIR_DIGITAL_SIG_MAGIC, 16#05054b50).
170-define(CENTRAL_DIR_DIGITAL_SIG_SZ, (4+2)).
171-define(CENTRAL_REGULAR_FILE_EXT_ATTRIBUTES, 8#644 bsl 16).
172-define(CENTRAL_DIRECTORY_FILE_EXT_ATTRIBUTES, 8#744 bsl 16).
173-define(CENTRAL_FILE_MAGIC, 16#02014b50).
174
175-record(cd_file_header, {version_made_by,
176			 version_needed,
177			 gp_flag,
178			 comp_method,
179			 last_mod_time,
180			 last_mod_date,
181			 crc32,
182			 comp_size,
183			 uncomp_size,
184			 file_name_length,
185			 extra_field_length,
186			 file_comment_length,
187			 disk_num_start,
188			 internal_attr,
189			 external_attr,
190			 local_header_offset}).
191
192-define(END_OF_CENTRAL_DIR_MAGIC, 16#06054b50).
193-define(END_OF_CENTRAL_DIR_SZ, (4+2+2+2+2+4+4+2)).
194
195-record(eocd, {disk_num,
196	       start_disk_num,
197	       entries_on_disk,
198	       entries,
199	       size,
200	       offset,
201	       zip_comment_length}).
202
203
204-type create_option() :: memory | cooked | verbose
205                       | {comment, Comment ::string()}
206                       | {cwd, CWD :: file:filename()}
207                       | {compress, What :: extension_spec()}
208                       | {uncompress, What :: extension_spec()}.
209-type extension() :: string().
210-type extension_spec() :: all
211                        | [Extension :: extension()]
212                        | {add, [Extension :: extension()]}
213                        | {del, [Extension :: extension()]}.
214-type filename() :: file:filename().
215
216-type zip_comment() :: #zip_comment{}.
217-type zip_file() :: #zip_file{}.
218
219-opaque handle() :: pid().
220
221-export_type([create_option/0, filename/0, handle/0]).
222
223%% Open a zip archive with options
224%%
225
226openzip_open(F) ->
227    openzip_open(F, []).
228
229openzip_open(F, Options) ->
230    case ?CATCH do_openzip_open(F, Options) of
231	{ok, OpenZip} ->
232	    {ok, OpenZip};
233	Error ->
234	    {error, Error}
235    end.
236
237do_openzip_open(F, Options) ->
238    Opts = get_openzip_options(Options),
239    #openzip_opts{output = Output, open_opts = OpO, cwd = CWD} = Opts,
240    Input = get_input(F),
241    In0 = Input({open, F, OpO -- [write]}, []),
242    {[#zip_comment{comment = C} | Files], In1} =
243	get_central_dir(In0, fun raw_file_info_etc/5, Input),
244    Z = zlib:open(),
245    {ok, #openzip{zip_comment = C,
246		  files = Files,
247		  in = In1,
248		  input = Input,
249		  output = Output,
250		  zlib = Z,
251		  cwd = CWD}}.
252
253%% retrieve all files from an open archive
254openzip_get(OpenZip) ->
255    case ?CATCH do_openzip_get(OpenZip) of
256	{ok, Result} -> {ok, Result};
257	Error -> {error, Error}
258    end.
259
260do_openzip_get(#openzip{files = Files, in = In0, input = Input,
261			output = Output, zlib = Z, cwd = CWD}) ->
262    ZipOpts = #unzip_opts{output = Output, input = Input,
263			  file_filter = fun all/1, open_opts = [],
264			  feedback = fun silent/1, cwd = CWD},
265    R = get_z_files(Files, Z, In0, ZipOpts, []),
266    {ok, R};
267do_openzip_get(_) ->
268    throw(einval).
269
270%% retrieve a file from an open archive
271openzip_get(FileName, OpenZip) ->
272    case ?CATCH do_openzip_get(FileName, OpenZip) of
273	{ok, Result} -> {ok, Result};
274	Error -> {error, Error}
275    end.
276
277do_openzip_get(F, #openzip{files = Files, in = In0, input = Input,
278			   output = Output, zlib = Z, cwd = CWD}) ->
279    %%case lists:keysearch(F, #zip_file.name, Files) of
280    case file_name_search(F, Files) of
281	{#zip_file{offset = Offset},_}=ZFile ->
282	    In1 = Input({seek, bof, Offset}, In0),
283	    case get_z_file(In1, Z, Input, Output, [], fun silent/1,
284			    CWD, ZFile, fun all/1) of
285		{file, R, _In2} -> {ok, R};
286		_ -> throw(file_not_found)
287	    end;
288	_ -> throw(file_not_found)
289    end;
290do_openzip_get(_, _) ->
291    throw(einval).
292
293file_name_search(Name,Files) ->
294    Fun = fun({ZipFile,_}) ->
295                  not string:equal(ZipFile#zip_file.name, Name,
296                                   _IgnoreCase = false, _Norm = nfc)
297          end,
298    case lists:dropwhile(Fun, Files) of
299	[ZFile|_] -> ZFile;
300	[] -> false
301    end.
302
303%% %% add a file to an open archive
304%% openzip_add(File, OpenZip) ->
305%%     case ?CATCH do_openzip_add(File, OpenZip) of
306%% 	{ok, Result} -> {ok, Result};
307%% 	Error -> {error, Error}
308%%     end.
309
310%% do_openzip_add(File, #open_zip{files = Files, in = In0,
311%% 			       opts = Opts} = OpenZip0) ->
312%%     throw(nyi),
313%%     Z = zlib:open(),
314%%     R = get_z_files(Files, In0, Z, Opts, []),
315%%     zlib:close(Z),
316%%     {ok, R};
317%% do_openzip_add(_, _) ->
318%%     throw(einval).
319
320%% get file list from open archive
321openzip_list_dir(#openzip{zip_comment = Comment,
322			  files = Files}) ->
323    {ZipFiles,_Extras} = lists:unzip(Files),
324    {ok, [#zip_comment{comment = Comment} | ZipFiles]};
325openzip_list_dir(_) ->
326    {error, einval}.
327
328openzip_list_dir(#openzip{files = Files}, [names_only]) ->
329    {ZipFiles,_Extras} = lists:unzip(Files),
330    Names = [Name || {#zip_file{name=Name},_} <- ZipFiles],
331    {ok, Names};
332openzip_list_dir(_, _) ->
333    {error, einval}.
334
335%% close an open archive
336openzip_close(#openzip{in = In0, input = Input, zlib = Z}) ->
337    Input(close, In0),
338    zlib:close(Z);
339openzip_close(_) ->
340    {error, einval}.
341
342%% Extract from a zip archive with options
343%%
344%% Accepted options:
345%% verbose, cooked, file_list, keep_old_files, file_filter, memory
346
347-spec(unzip(Archive) -> RetValue when
348      Archive :: file:name() | binary(),
349      RetValue :: {ok, FileList}
350                | {ok, FileBinList}
351                | {error, Reason :: term()}
352                | {error, {Name :: file:name(), Reason :: term()}},
353      FileList :: [file:name()],
354      FileBinList :: [{file:name(),binary()}]).
355
356unzip(F) -> unzip(F, []).
357
358-spec(unzip(Archive, Options) -> RetValue when
359      Archive :: file:name() | binary(),
360      Options :: [Option],
361      Option  :: {file_list, FileList} | cooked
362               | keep_old_files | verbose | memory |
363                 {file_filter, FileFilter} | {cwd, CWD},
364      FileList :: [file:name()],
365      FileBinList :: [{file:name(),binary()}],
366      FileFilter :: fun((ZipFile) -> boolean()),
367      CWD :: file:filename(),
368      ZipFile :: zip_file(),
369      RetValue :: {ok, FileList}
370                | {ok, FileBinList}
371                | {error, Reason :: term()}
372                | {error, {Name :: file:name(), Reason :: term()}}).
373
374unzip(F, Options) ->
375    case ?CATCH do_unzip(F, Options) of
376	{ok, R} -> {ok, R};
377	Error -> {error, Error}
378    end.
379
380do_unzip(F, Options) ->
381    Opts = get_unzip_options(F, Options),
382    #unzip_opts{input = Input, open_opts = OpO} = Opts,
383    In0 = Input({open, F, OpO -- [write]}, []),
384    RawIterator = fun raw_file_info_etc/5,
385    {Info, In1} = get_central_dir(In0, RawIterator, Input),
386    %% get rid of zip-comment
387    Z = zlib:open(),
388    Files = try
389                get_z_files(Info, Z, In1, Opts, [])
390            after
391                zlib:close(Z),
392                Input(close, In1)
393            end,
394    {ok, Files}.
395
396%% Iterate over all files in a zip archive
397-spec(foldl(Fun, Acc0, Archive) -> {ok, Acc1} | {error, Reason} when
398      Fun :: fun((FileInArchive, GetInfo, GetBin, AccIn) -> AccOut),
399      FileInArchive :: file:name(),
400      GetInfo :: fun(() -> file:file_info()),
401      GetBin :: fun(() -> binary()),
402      Acc0 :: term(),
403      Acc1 :: term(),
404      AccIn :: term(),
405      AccOut :: term(),
406      Archive :: file:name() | {file:name(), binary()},
407      Reason :: term()).
408
409foldl(Fun, Acc0, Archive) when is_function(Fun, 4) ->
410    ZipFun =
411	fun({Name, GetInfo, GetBin}, A) ->
412		A2 = Fun(Name, GetInfo, GetBin, A),
413		{true, false, A2}
414	end,
415    case prim_zip:open(ZipFun, Acc0, Archive) of
416	{ok, PrimZip, Acc1} ->
417	    ok = prim_zip:close(PrimZip),
418	    {ok, Acc1};
419	{error, bad_eocd} ->
420	    {error, "Not an archive file"};
421	{error, Reason} ->
422	    {error, Reason}
423    end;
424foldl(_,_, _) ->
425    {error, einval}.
426
427%% Create zip archive name F from Files or binaries
428%%
429%% Accepted options:
430%% verbose, cooked, memory, comment
431
432-spec(zip(Name, FileList) -> RetValue when
433      Name     :: file:name(),
434      FileList :: [FileSpec],
435      FileSpec :: file:name() | {file:name(), binary()}
436                | {file:name(), binary(), file:file_info()},
437      RetValue :: {ok, FileName :: file:name()}
438                | {ok, {FileName :: file:name(), binary()}}
439                | {error, Reason :: term()}).
440
441zip(F, Files) -> zip(F, Files, []).
442
443-spec(zip(Name, FileList, Options) -> RetValue when
444      Name     :: file:name(),
445      FileList :: [FileSpec],
446      FileSpec :: file:name() | {file:name(), binary()}
447                | {file:name(), binary(), file:file_info()},
448      Options  :: [Option],
449      Option   :: create_option(),
450      RetValue :: {ok, FileName :: file:name()}
451                | {ok, {FileName :: file:name(), binary()}}
452                | {error, Reason :: term()}).
453
454zip(F, Files, Options) ->
455    case ?CATCH do_zip(F, Files, Options) of
456	{ok, R} -> {ok, R};
457	Error -> {error, Error}
458    end.
459
460do_zip(F, Files, Options) ->
461    Opts = get_zip_options(Files, Options),
462    #zip_opts{output = Output, open_opts = OpO} = Opts,
463    Out0 = Output({open, F, OpO}, []),
464    Z = zlib:open(),
465    try
466        {Out1, LHS, Pos} = put_z_files(Files, Z, Out0, 0, Opts, []),
467        zlib:close(Z),
468        Out2 = put_central_dir(LHS, Pos, Out1, Opts),
469        Out3 = Output({close, F}, Out2),
470        {ok, Out3}
471    catch
472        C:R:Stk ->
473            zlib:close(Z),
474            Output({close, F}, Out0),
475            erlang:raise(C, R, Stk)
476    end.
477
478
479%% List zip directory contents
480%%
481%% Accepted options:
482%% cooked, file_filter, file_output (latter 2 undocumented)
483
484-spec(list_dir(Archive) -> RetValue when
485      Archive :: file:name() | binary(),
486      RetValue :: {ok, CommentAndFiles} | {error, Reason :: term()},
487      CommentAndFiles :: [zip_comment() | zip_file()]).
488
489list_dir(F) -> list_dir(F, []).
490
491-spec(list_dir(Archive, Options) -> RetValue when
492      Archive :: file:name() | binary(),
493      RetValue :: {ok, CommentAndFiles} | {error, Reason :: term()},
494      CommentAndFiles :: [zip_comment() | zip_file()],
495      Options :: [Option],
496      Option :: cooked).
497
498list_dir(F, Options) ->
499    case ?CATCH do_list_dir(F, Options) of
500	{ok, R} -> {ok, R};
501	Error -> {error, Error}
502    end.
503
504do_list_dir(F, Options) ->
505    Opts = get_list_dir_options(F, Options),
506    #list_dir_opts{input = Input, open_opts = OpO,
507		   raw_iterator = RawIterator} = Opts,
508    In0 = Input({open, F, OpO}, []),
509    {Info, In1} = get_central_dir(In0, RawIterator, Input),
510    Input(close, In1),
511    {ok, Info}.
512
513%% Print zip directory in short form
514
515-spec(t(Archive) -> ok when
516      Archive :: file:name() | binary() | ZipHandle,
517      ZipHandle :: handle()).
518
519t(F) when is_pid(F) -> zip_t(F);
520t(F) when is_record(F, openzip) -> openzip_t(F);
521t(F) -> t(F, fun raw_short_print_info_etc/5).
522
523t(F, RawPrint) ->
524    case ?CATCH do_t(F, RawPrint) of
525	ok -> ok;
526	Error -> {error, Error}
527    end.
528
529do_t(F, RawPrint) ->
530    Input = get_input(F),
531    OpO = [raw],
532    In0 = Input({open, F, OpO}, []),
533    {_Info, In1} = get_central_dir(In0, RawPrint, Input),
534    Input(close, In1),
535    ok.
536
537%% Print zip directory in long form (like ls -l)
538
539-spec(tt(Archive) -> ok when
540      Archive :: file:name() | binary() | ZipHandle,
541      ZipHandle :: handle()).
542
543tt(F) when is_pid(F) -> zip_tt(F);
544tt(F) when is_record(F, openzip) -> openzip_tt(F);
545tt(F) -> t(F, fun raw_long_print_info_etc/5).
546
547
548%% option utils
549get_unzip_opt([], Opts) ->
550    Opts;
551get_unzip_opt([verbose | Rest], Opts) ->
552    get_unzip_opt(Rest, Opts#unzip_opts{feedback = fun verbose_unzip/1});
553get_unzip_opt([cooked | Rest], #unzip_opts{open_opts = OpO} = Opts) ->
554    get_unzip_opt(Rest, Opts#unzip_opts{open_opts = OpO -- [raw]});
555get_unzip_opt([memory | Rest], Opts) ->
556    get_unzip_opt(Rest, Opts#unzip_opts{output = fun binary_io/2});
557get_unzip_opt([{cwd, CWD} | Rest], Opts) ->
558    get_unzip_opt(Rest, Opts#unzip_opts{cwd = CWD});
559get_unzip_opt([{file_filter, F} | Rest], Opts) ->
560    Filter1 = fun({ZipFile,_Extra}) -> F(ZipFile) end,
561    Filter2 = fun_and_1(Filter1, Opts#unzip_opts.file_filter),
562    get_unzip_opt(Rest, Opts#unzip_opts{file_filter = Filter2});
563get_unzip_opt([{file_list, L} | Rest], Opts) ->
564    FileInList = fun(F) -> file_in_list(F, L) end,
565    Filter = fun_and_1(FileInList, Opts#unzip_opts.file_filter),
566    get_unzip_opt(Rest, Opts#unzip_opts{file_filter = Filter});
567get_unzip_opt([keep_old_files | Rest], Opts) ->
568    Keep = fun keep_old_file/1,
569    Filter = fun_and_1(Keep, Opts#unzip_opts.file_filter),
570    get_unzip_opt(Rest, Opts#unzip_opts{file_filter = Filter});
571get_unzip_opt([Unknown | _Rest], _Opts) ->
572    throw({bad_option, Unknown}).
573
574get_list_dir_opt([], Opts) ->
575    Opts;
576get_list_dir_opt([cooked | Rest], #list_dir_opts{open_opts = OpO} = Opts) ->
577    get_list_dir_opt(Rest, Opts#list_dir_opts{open_opts = OpO -- [raw]});
578get_list_dir_opt([names_only | Rest], Opts) ->
579    get_list_dir_opt(Rest, Opts#list_dir_opts{
580			     raw_iterator = fun(A, B, C, D, E) -> raw_name_only(A, B, C, D, E) end});
581%% get_list_dir_opt([{file_output, F} | Rest], Opts) ->
582%%     get_list_dir_opt(Rest, Opts#list_dir_opts{file_output = F});
583%% get_list_dir_opt([{file_filter, F} | Rest], Opts) ->
584%%     get_list_dir_opt(Rest, Opts#list_dir_opts{file_filter = F});
585get_list_dir_opt([Unknown | _Rest], _Opts) ->
586    throw({bad_option, Unknown}).
587
588get_zip_opt([], Opts) ->
589    Opts;
590get_zip_opt([verbose | Rest], Opts) ->
591    get_zip_opt(Rest, Opts#zip_opts{feedback = fun verbose_zip/1});
592get_zip_opt([cooked | Rest], #zip_opts{open_opts = OpO} = Opts) ->
593    get_zip_opt(Rest, Opts#zip_opts{open_opts = OpO -- [raw]});
594get_zip_opt([memory | Rest], Opts) ->
595    get_zip_opt(Rest, Opts#zip_opts{output = fun binary_io/2});
596get_zip_opt([{cwd, CWD} | Rest], Opts) ->
597    get_zip_opt(Rest, Opts#zip_opts{cwd = CWD});
598get_zip_opt([{comment, C} | Rest], Opts) ->
599    get_zip_opt(Rest, Opts#zip_opts{comment = C});
600get_zip_opt([{compress, Which} = O| Rest], Opts) ->
601    Which2 =
602	case Which of
603	    all ->
604		all;
605	    Suffixes when is_list(Suffixes) ->
606		lists:usort(Suffixes);
607	    {add, Suffixes} when is_list(Suffixes) ->
608		lists:usort(Opts#zip_opts.compress ++ Suffixes);
609	    {del, Suffixes} when is_list(Suffixes) ->
610		lists:usort(Opts#zip_opts.compress -- Suffixes);
611	    _ ->
612		throw({bad_option, O})
613	end,
614    get_zip_opt(Rest, Opts#zip_opts{compress = Which2});
615get_zip_opt([{uncompress, Which} = O| Rest], Opts) ->
616    Which2 =
617	case Which of
618	    all ->
619		all;
620	    Suffixes when is_list(Suffixes) ->
621		lists:usort(Suffixes);
622	    {add, Suffixes} when is_list(Suffixes) ->
623		lists:usort(Opts#zip_opts.uncompress ++ Suffixes);
624	    {del, Suffixes} when is_list(Suffixes) ->
625		lists:usort(Opts#zip_opts.uncompress -- Suffixes);
626	    _ ->
627		throw({bad_option, O})
628	end,
629    get_zip_opt(Rest, Opts#zip_opts{uncompress = Which2});
630get_zip_opt([Unknown | _Rest], _Opts) ->
631    throw({bad_option, Unknown}).
632
633
634%% feedback funs
635silent(_) -> ok.
636
637verbose_unzip(FN) ->
638    io:format("extracting: ~ts\n", [io_lib:write_string(FN)]).
639
640verbose_zip(FN) ->
641    io:format("adding: ~ts\n", [io_lib:write_string(FN)]).
642
643%% file filter funs
644all(_) -> true.
645
646file_in_list({#zip_file{name = FileName},_}, List) ->
647    lists:member(FileName, List);
648file_in_list(_, _) ->
649    false.
650
651keep_old_file({#zip_file{name = FileName},_}) ->
652    not (filelib:is_file(FileName) orelse filelib:is_dir(FileName));
653keep_old_file(_) ->
654    false.
655
656%% fun combiner
657fun_and_1(Fun1, Fun2) ->
658    fun(A) -> Fun1(A) andalso Fun2(A) end.
659
660%% getting options
661get_zip_options(Files, Options) ->
662    Suffixes = [".Z", ".zip", ".zoo", ".arc", ".lzh", ".arj"],
663    Opts = #zip_opts{output = fun file_io/2,
664		     input = get_zip_input({files, Files}),
665		     open_opts = [raw, write],
666		     comment = "",
667		     feedback = fun silent/1,
668		     cwd = "",
669		     compress = all,
670		     uncompress = Suffixes
671		    },
672    Opts1 = #zip_opts{comment = Comment} = get_zip_opt(Options, Opts),
673    %% UTF-8 encode characters in the interval from 127 to 255.
674    {Comment1, _} = encode_string(Comment),
675    Opts1#zip_opts{comment = Comment1}.
676
677get_unzip_options(F, Options) ->
678    Opts = #unzip_opts{file_filter = fun all/1,
679		       output = fun file_io/2,
680		       input = get_input(F),
681		       open_opts = [raw],
682		       feedback = fun silent/1,
683		       cwd = ""
684		      },
685    get_unzip_opt(Options, Opts).
686
687get_openzip_options(Options) ->
688    Opts = #openzip_opts{open_opts = [raw, read],
689			 output = fun file_io/2,
690			 cwd = ""},
691    get_openzip_opt(Options, Opts).
692
693get_input(F) when is_binary(F) ->
694    fun binary_io/2;
695get_input(F) when is_list(F) ->
696    fun file_io/2;
697get_input(_) ->
698    throw(einval).
699
700get_zip_input({F, B}) when is_binary(B), is_list(F) ->
701    fun binary_io/2;
702get_zip_input({F, B, #file_info{}}) when is_binary(B), is_list(F) ->
703    fun binary_io/2;
704get_zip_input({F, #file_info{}, B}) when is_binary(B), is_list(F) ->
705    fun binary_io/2;
706get_zip_input(F) when is_list(F) ->
707    fun file_io/2;
708get_zip_input({files, []}) ->
709    fun binary_io/2;
710get_zip_input({files, [File | _]}) ->
711    get_zip_input(File);
712get_zip_input(_) ->
713    throw(einval).
714
715get_list_dir_options(F, Options) ->
716    Opts = #list_dir_opts{raw_iterator = fun raw_file_info_public/5,
717			  input = get_input(F),
718			  open_opts = [raw]},
719    get_list_dir_opt(Options, Opts).
720
721%% aliases for erl_tar compatibility
722-spec(table(Archive) -> RetValue when
723      Archive :: file:name() | binary(),
724      RetValue :: {ok, CommentAndFiles} | {error, Reason :: term()},
725      CommentAndFiles :: [zip_comment() | zip_file()]).
726
727table(F) -> list_dir(F).
728
729-spec(table(Archive, Options) -> RetValue when
730      Archive :: file:name() | binary(),
731      RetValue :: {ok, CommentAndFiles} | {error, Reason :: term()},
732      CommentAndFiles :: [zip_comment() | zip_file()],
733
734      Options :: [Option],
735      Option :: cooked).
736
737table(F, O) -> list_dir(F, O).
738
739-spec(create(Name, FileList) -> RetValue when
740      Name     :: file:name(),
741      FileList :: [FileSpec],
742      FileSpec :: file:name() | {file:name(), binary()}
743                | {file:name(), binary(), file:file_info()},
744      RetValue :: {ok, FileName :: filename()}
745                | {ok, {FileName :: filename(), binary()}}
746                | {error, Reason :: term()}).
747
748create(F, Fs) -> zip(F, Fs).
749
750-spec(create(Name, FileList, Options) -> RetValue when
751      Name     :: file:name(),
752      FileList :: [FileSpec],
753      FileSpec :: file:name() | {file:name(), binary()}
754                | {file:name(), binary(), file:file_info()},
755      Options  :: [Option],
756      Option   :: create_option(),
757      RetValue :: {ok, FileName :: filename()}
758                | {ok, {FileName :: filename(), binary()}}
759                | {error, Reason :: term()}).
760create(F, Fs, O) -> zip(F, Fs, O).
761
762-spec(extract(Archive) -> RetValue when
763      Archive :: file:name() | binary(),
764      RetValue :: {ok, FileList}
765                | {ok, FileBinList}
766                | {error, Reason :: term()}
767                | {error, {Name :: file:name(), Reason :: term()}},
768      FileList :: [file:name()],
769      FileBinList :: [{file:name(),binary()}]).
770
771extract(F) -> unzip(F).
772
773-spec(extract(Archive, Options) -> RetValue when
774      Archive :: file:name() | binary(),
775      Options :: [Option],
776      Option  :: {file_list, FileList}
777               | keep_old_files | verbose | memory |
778                 {file_filter, FileFilter} | {cwd, CWD},
779      FileList :: [file:name()],
780      FileBinList :: [{file:name(),binary()}],
781      FileFilter :: fun((ZipFile) -> boolean()),
782      CWD :: file:filename(),
783      ZipFile :: zip_file(),
784      RetValue :: {ok, FileList}
785                | {ok, FileBinList}
786                | {error, Reason :: term()}
787                | {error, {Name :: file:name(), Reason :: term()}}).
788
789extract(F, O) -> unzip(F, O).
790
791
792%% put the central directory, at the end of the zip archive
793put_central_dir(LHS, Pos, Out0,
794		#zip_opts{output = Output, comment = Comment}) ->
795    {Out1, Sz} = put_cd_files_loop(LHS, Output, Out0, 0),
796    put_eocd(length(LHS), Pos, Sz, Comment, Output, Out1).
797
798put_cd_files_loop([], _Output, Out, Sz) ->
799    {Out, Sz};
800put_cd_files_loop([{LH, Name, Pos} | LHRest], Output, Out0, Sz0) ->
801    CDFH = cd_file_header_from_lh_and_pos(LH, Pos),
802    BCDFH = cd_file_header_to_bin(CDFH),
803    B = [<<?CENTRAL_FILE_MAGIC:32/little>>, BCDFH, Name],
804    Out1 = Output({write, B}, Out0),
805    Sz1 = Sz0 + ?CENTRAL_FILE_HEADER_SZ +
806	LH#local_file_header.file_name_length,
807    put_cd_files_loop(LHRest, Output, Out1, Sz1).
808
809%% put end marker of central directory, the last record in the archive
810put_eocd(N, Pos, Sz, Comment, Output, Out0) ->
811    %% BComment = list_to_binary(Comment),
812    CommentSz = length(Comment), % size(BComment),
813    EOCD = #eocd{disk_num = 0,
814		 start_disk_num = 0,
815		 entries_on_disk = N,
816		 entries = N,
817		 size = Sz,
818		 offset = Pos,
819		 zip_comment_length = CommentSz},
820    BEOCD = eocd_to_bin(EOCD),
821    B = [<<?END_OF_CENTRAL_DIR_MAGIC:32/little>>, BEOCD, Comment], % BComment],
822    Output({write, B}, Out0).
823
824get_filename({Name, _}, Type) ->
825    get_filename(Name, Type);
826get_filename({Name, _, _}, Type) ->
827    get_filename(Name, Type);
828get_filename(Name, regular) ->
829    Name;
830get_filename(Name, directory) ->
831    %% Ensure trailing slash
832    case lists:reverse(Name) of
833	[$/ | _Rev] -> Name;
834	Rev         -> lists:reverse([$/ | Rev])
835    end.
836
837add_cwd(_CWD, {_Name, _} = F) -> F;
838add_cwd("", F) -> F;
839add_cwd(CWD, F) -> filename:join(CWD, F).
840
841%% already compressed data should be stored as is in archive,
842%% a simple name-match is used to check for this
843%% files smaller than 10 bytes are also stored, not compressed
844get_comp_method(_, N, _, _) when is_integer(N), N < 10 ->
845    ?STORED;
846get_comp_method(_, _, _, directory) ->
847    ?STORED;
848get_comp_method(F, _, #zip_opts{compress = Compress, uncompress = Uncompress}, _) ->
849    Ext = filename:extension(F),
850    Test = fun(Which) -> (Which =:= all) orelse lists:member(Ext, Which) end,
851    case Test(Compress) andalso not Test(Uncompress) of
852	true  -> ?DEFLATED;
853	false -> ?STORED
854    end.
855
856put_z_files([], _Z, Out, Pos, _Opts, Acc) ->
857    {Out, lists:reverse(Acc, []), Pos};
858put_z_files([F | Rest], Z, Out0, Pos0,
859	    #zip_opts{input = Input, output = Output, open_opts = OpO,
860		      feedback = FB, cwd = CWD} = Opts, Acc) ->
861    In0 = [],
862    F1 = add_cwd(CWD, F),
863    FileInfo = Input({file_info, F1}, In0),
864    Type = FileInfo#file_info.type,
865    UncompSize =
866	case Type of
867	    regular -> FileInfo#file_info.size;
868	    directory -> 0
869	end,
870    FileName0 = get_filename(F, Type),
871    %% UTF-8 encode characters in the interval from 127 to 255.
872    {FileName, GPFlag} = encode_string(FileName0),
873    CompMethod = get_comp_method(FileName, UncompSize, Opts, Type),
874    LH = local_file_header_from_info_method_name(FileInfo, UncompSize, CompMethod, FileName, GPFlag),
875    BLH = local_file_header_to_bin(LH),
876    B = [<<?LOCAL_FILE_MAGIC:32/little>>, BLH],
877    Out1 = Output({write, B}, Out0),
878    Out2 = Output({write, FileName}, Out1),
879    {Out3, CompSize, CRC} = put_z_file(CompMethod, UncompSize, Out2, F1,
880				       0, Input, Output, OpO, Z, Type),
881    FB(FileName0),
882    Patch = <<CRC:32/little, CompSize:32/little>>,
883    Out4 = Output({pwrite, Pos0 + ?LOCAL_FILE_HEADER_CRC32_OFFSET, Patch}, Out3),
884    Out5 = Output({seek, eof, 0}, Out4),
885    Pos1 = Pos0 + ?LOCAL_FILE_HEADER_SZ	+ LH#local_file_header.file_name_length,
886    Pos2 = Pos1 + CompSize,
887    LH2 = LH#local_file_header{comp_size = CompSize, crc32 = CRC},
888    ThisAcc = [{LH2, FileName, Pos0}],
889    {Out6, SubAcc, Pos3} =
890	case Type of
891	    regular ->
892		{Out5, ThisAcc, Pos2};
893	    directory ->
894		Files = Input({list_dir, F1}, []),
895		RevFiles = reverse_join_files(F, Files, []),
896		put_z_files(RevFiles, Z, Out5, Pos2, Opts, ThisAcc)
897	end,
898    Acc2 = lists:reverse(SubAcc) ++ Acc,
899    put_z_files(Rest, Z, Out6, Pos3, Opts, Acc2).
900
901reverse_join_files(Dir, [File | Files], Acc) ->
902    reverse_join_files(Dir, Files, [filename:join([Dir, File]) | Acc]);
903reverse_join_files(_Dir, [], Acc) ->
904    Acc.
905
906%% flag for zlib
907-define(MAX_WBITS, 15).
908
909%% compress a file
910put_z_file(_Method, Sz, Out, _F, Pos, _Input, _Output, _OpO, _Z, directory) ->
911    {Out, Pos + Sz, 0};
912put_z_file(_Method, 0, Out, _F, Pos, _Input, _Output, _OpO, _Z, regular) ->
913    {Out, Pos, 0};
914put_z_file(?STORED, UncompSize, Out0, F, Pos0, Input, Output, OpO, Z, regular) ->
915    In0 = [],
916    In1 = Input({open, F, OpO -- [write]}, In0),
917    CRC0 = zlib:crc32(Z, <<>>),
918    {Data, In2} = Input({read, UncompSize}, In1),
919    Out1 = Output({write, Data}, Out0),
920    CRC = zlib:crc32(Z, CRC0, Data),
921    Input(close, In2),
922    {Out1, Pos0+erlang:iolist_size(Data), CRC};
923put_z_file(?DEFLATED, UncompSize, Out0, F, Pos0, Input, Output, OpO, Z, regular) ->
924    In0 = [],
925    In1 = Input({open, F, OpO -- [write]}, In0),
926    ok = zlib:deflateInit(Z, default, deflated, -?MAX_WBITS, 8, default),
927    {Out1, Pos1} =
928	put_z_data_loop(UncompSize, In1, Out0, Pos0, Input, Output, Z),
929    CRC = zlib:crc32(Z),
930    ok = zlib:deflateEnd(Z),
931    Input(close, In1),
932    {Out1, Pos1, CRC}.
933
934%%  zlib is finished with the last chunk compressed
935get_sync(N, N) -> finish;
936get_sync(_, _) -> full.
937
938%% compress data
939put_z_data_loop(0, _In, Out, Pos, _Input, _Output, _Z) ->
940    {Out, Pos};
941put_z_data_loop(UncompSize, In0, Out0, Pos0, Input, Output, Z) ->
942    N = erlang:min(?WRITE_BLOCK_SIZE, UncompSize),
943    case Input({read, N}, In0) of
944	{eof, _In1} ->
945	    {Out0, Pos0};
946	{Uncompressed, In1} ->
947	    Compressed = zlib:deflate(Z, Uncompressed, get_sync(N, UncompSize)),
948	    Sz = erlang:iolist_size(Compressed),
949	    Out1 = Output({write, Compressed}, Out0),
950	    put_z_data_loop(UncompSize - N, In1, Out1, Pos0 + Sz,
951			      Input, Output, Z)
952    end.
953
954%% raw iterators over central dir
955
956%% name only
957raw_name_only(CD, FileName, _FileComment, _BExtraField, Acc)
958  when is_record(CD, cd_file_header) ->
959    [FileName | Acc];
960raw_name_only(EOCD, _, _Comment, _, Acc) when is_record(EOCD, eocd) ->
961    Acc.
962
963%% for printing directory (t/1)
964raw_short_print_info_etc(CD, FileName, _FileComment, _BExtraField, Acc)
965  when is_record(CD, cd_file_header) ->
966    print_file_name(FileName),
967    Acc;
968raw_short_print_info_etc(EOCD, X, Comment, Y, Acc) when is_record(EOCD, eocd) ->
969    raw_long_print_info_etc(EOCD, X, Comment, Y, Acc).
970
971print_file_name(FileName) ->
972    io:format("~ts\n", [FileName]).
973
974
975%% for printing directory (tt/1)
976raw_long_print_info_etc(#cd_file_header{comp_size = CompSize,
977					uncomp_size = UncompSize,
978					last_mod_date = LMDate,
979					last_mod_time = LMTime},
980			FileName, FileComment, _BExtraField, Acc) ->
981    MTime = dos_date_time_to_datetime(LMDate, LMTime),
982    print_header(CompSize, MTime, UncompSize, FileName, FileComment),
983    Acc;
984raw_long_print_info_etc(EOCD, _, Comment, _, Acc) when is_record(EOCD, eocd) ->
985    print_comment(Comment),
986    Acc.
987
988print_header(CompSize, MTime, UncompSize, FileName, FileComment) ->
989    io:format("~8w ~s ~8w ~2w% ~ts ~ts\n",
990	      [CompSize, time_to_string(MTime), UncompSize,
991	       get_percent(CompSize, UncompSize), FileName, FileComment]).
992
993print_comment("") ->
994    ok;
995print_comment(Comment) ->
996    io:format("Archive comment: ~ts\n", [Comment]).
997
998get_percent(_, 0) -> 100;
999get_percent(CompSize, Size) -> round(CompSize * 100 / Size).
1000
1001%% time formatting ("borrowed" from erl_tar.erl)
1002time_to_string({{Y, Mon, Day}, {H, Min, _}}) ->
1003    io_lib:format("~s ~2w ~s:~s ~w",
1004		  [month(Mon), Day, two_d(H), two_d(Min), Y]).
1005
1006two_d(N) ->
1007    tl(integer_to_list(N + 100)).
1008
1009month(1) -> "Jan";
1010month(2) -> "Feb";
1011month(3) -> "Mar";
1012month(4) -> "Apr";
1013month(5) -> "May";
1014month(6) -> "Jun";
1015month(7) -> "Jul";
1016month(8) -> "Aug";
1017month(9) -> "Sep";
1018month(10) -> "Oct";
1019month(11) -> "Nov";
1020month(12) -> "Dec".
1021
1022%% zip header functions
1023cd_file_header_from_lh_and_pos(LH, Pos) ->
1024    #local_file_header{version_needed = VersionNeeded,
1025		       gp_flag = GPFlag,
1026		       comp_method = CompMethod,
1027		       last_mod_time = LastModTime,
1028		       last_mod_date = LastModDate,
1029		       crc32 = CRC32,
1030		       comp_size = CompSize,
1031		       uncomp_size = UncompSize,
1032		       file_name_length = FileNameLength,
1033		       extra_field_length = ExtraFieldLength,
1034                       type = Type} = LH,
1035    #cd_file_header{version_made_by = ?VERSION_MADE_BY,
1036		    version_needed = VersionNeeded,
1037		    gp_flag = GPFlag,
1038		    comp_method = CompMethod,
1039		    last_mod_time = LastModTime,
1040		    last_mod_date = LastModDate,
1041		    crc32 = CRC32,
1042		    comp_size = CompSize,
1043		    uncomp_size = UncompSize,
1044		    file_name_length = FileNameLength,
1045		    extra_field_length = ExtraFieldLength,
1046		    file_comment_length = 0, % FileCommentLength,
1047		    disk_num_start = 0, % DiskNumStart,
1048		    internal_attr = 0, % InternalAttr,
1049		    external_attr = % ExternalAttr
1050                        case Type of
1051                            regular -> ?CENTRAL_REGULAR_FILE_EXT_ATTRIBUTES;
1052                            directory -> ?CENTRAL_DIRECTORY_FILE_EXT_ATTRIBUTES
1053                        end,
1054		    local_header_offset = Pos}.
1055
1056cd_file_header_to_bin(
1057  #cd_file_header{version_made_by = VersionMadeBy,
1058		  version_needed = VersionNeeded,
1059		  gp_flag = GPFlag,
1060		  comp_method = CompMethod,
1061		  last_mod_time = LastModTime,
1062		  last_mod_date = LastModDate,
1063		  crc32 = CRC32,
1064		  comp_size = CompSize,
1065		  uncomp_size = UncompSize,
1066		  file_name_length = FileNameLength,
1067		  extra_field_length = ExtraFieldLength,
1068		  file_comment_length = FileCommentLength,
1069		  disk_num_start = DiskNumStart,
1070		  internal_attr = InternalAttr,
1071		  external_attr = ExternalAttr,
1072		  local_header_offset = LocalHeaderOffset}) ->
1073    <<VersionMadeBy:16/little,
1074     VersionNeeded:16/little,
1075     GPFlag:16/little,
1076     CompMethod:16/little,
1077     LastModTime:16/little,
1078     LastModDate:16/little,
1079     CRC32:32/little,
1080     CompSize:32/little,
1081     UncompSize:32/little,
1082     FileNameLength:16/little,
1083     ExtraFieldLength:16/little,
1084     FileCommentLength:16/little,
1085     DiskNumStart:16/little,
1086     InternalAttr:16/little,
1087     ExternalAttr:32/little,
1088     LocalHeaderOffset:32/little>>.
1089
1090local_file_header_to_bin(
1091  #local_file_header{version_needed = VersionNeeded,
1092		     gp_flag = GPFlag,
1093		     comp_method = CompMethod,
1094		     last_mod_time = LastModTime,
1095		     last_mod_date = LastModDate,
1096		     crc32 = CRC32,
1097		     comp_size = CompSize,
1098		     uncomp_size = UncompSize,
1099		     file_name_length = FileNameLength,
1100		     extra_field_length = ExtraFieldLength}) ->
1101    <<VersionNeeded:16/little,
1102     GPFlag:16/little,
1103     CompMethod:16/little,
1104     LastModTime:16/little,
1105     LastModDate:16/little,
1106     CRC32:32/little,
1107     CompSize:32/little,
1108     UncompSize:32/little,
1109     FileNameLength:16/little,
1110     ExtraFieldLength:16/little>>.
1111
1112eocd_to_bin(#eocd{disk_num = DiskNum,
1113	   start_disk_num = StartDiskNum,
1114	   entries_on_disk = EntriesOnDisk,
1115	   entries = Entries,
1116	   size = Size,
1117	   offset = Offset,
1118	   zip_comment_length = ZipCommentLength}) ->
1119    <<DiskNum:16/little,
1120     StartDiskNum:16/little,
1121     EntriesOnDisk:16/little,
1122     Entries:16/little,
1123     Size:32/little,
1124     Offset:32/little,
1125     ZipCommentLength:16/little>>.
1126
1127%% put together a local file header
1128local_file_header_from_info_method_name(#file_info{mtime = MTime, type = Type},
1129					UncompSize,
1130					CompMethod, Name, GPFlag) ->
1131    {ModDate, ModTime} = dos_date_time_from_datetime(MTime),
1132    #local_file_header{version_needed = 20,
1133		       gp_flag = GPFlag,
1134		       comp_method = CompMethod,
1135		       last_mod_time = ModTime,
1136		       last_mod_date = ModDate,
1137		       crc32 = -1,
1138		       comp_size = -1,
1139		       uncomp_size = UncompSize,
1140		       file_name_length = length(Name),
1141		       extra_field_length = 0,
1142                       type = Type}.
1143
1144server_init(Parent) ->
1145    %% we want to know if our parent dies
1146    process_flag(trap_exit, true),
1147    server_loop(Parent, not_open).
1148
1149%% small, simple, stupid zip-archive server
1150server_loop(Parent, OpenZip) ->
1151    receive
1152	{From, {open, Archive, Options}} ->
1153	    case openzip_open(Archive, Options) of
1154		{ok, NewOpenZip} ->
1155		    From ! {self(), {ok, self()}},
1156		    server_loop(Parent, NewOpenZip);
1157		Error ->
1158		    From ! {self(), Error}
1159	    end;
1160	{From, close} ->
1161	    From ! {self(), openzip_close(OpenZip)};
1162	{From, get} ->
1163	    From ! {self(), openzip_get(OpenZip)},
1164	    server_loop(Parent, OpenZip);
1165	{From, {get, FileName}} ->
1166	    From ! {self(), openzip_get(FileName, OpenZip)},
1167	    server_loop(Parent, OpenZip);
1168	{From, list_dir} ->
1169	    From ! {self(), openzip_list_dir(OpenZip)},
1170	    server_loop(Parent, OpenZip);
1171	{From, {list_dir, Opts}} ->
1172	    From ! {self(), openzip_list_dir(OpenZip, Opts)},
1173	    server_loop(Parent, OpenZip);
1174	{From, get_state} ->
1175	    From ! {self(), OpenZip},
1176	    server_loop(Parent, OpenZip);
1177        {'EXIT', Parent, Reason} ->
1178            _ = openzip_close(OpenZip),
1179            exit({parent_died, Reason});
1180	_ ->
1181	    {error, bad_msg}
1182    end.
1183
1184-spec(zip_open(Archive) -> {ok, ZipHandle} | {error, Reason} when
1185      Archive :: file:name() | binary(),
1186      ZipHandle :: handle(),
1187      Reason :: term()).
1188
1189zip_open(Archive) -> zip_open(Archive, []).
1190
1191-spec(zip_open(Archive, Options) -> {ok, ZipHandle} | {error, Reason} when
1192      Archive :: file:name() | binary(),
1193      ZipHandle :: handle(),
1194      Options :: [Option],
1195      Option :: cooked | memory | {cwd, CWD :: file:filename()},
1196      Reason :: term()).
1197
1198zip_open(Archive, Options) ->
1199    Self = self(),
1200    Pid = spawn_link(fun() -> server_init(Self) end),
1201    request(Self, Pid, {open, Archive, Options}).
1202
1203-spec(zip_get(ZipHandle) -> {ok, [Result]} | {error, Reason} when
1204      ZipHandle :: handle(),
1205      Result :: file:name() | {file:name(), binary()},
1206      Reason :: term()).
1207
1208zip_get(Pid) when is_pid(Pid) ->
1209    request(self(), Pid, get).
1210
1211-spec(zip_close(ZipHandle) -> ok | {error, einval} when
1212      ZipHandle :: handle()).
1213
1214zip_close(Pid) when is_pid(Pid) ->
1215    request(self(), Pid, close).
1216
1217-spec(zip_get(FileName, ZipHandle) -> {ok, Result} | {error, Reason} when
1218      FileName :: file:name(),
1219      ZipHandle :: handle(),
1220      Result :: file:name() | {file:name(), binary()},
1221      Reason :: term()).
1222
1223zip_get(FileName, Pid) when is_pid(Pid) ->
1224    request(self(), Pid, {get, FileName}).
1225
1226-spec(zip_list_dir(ZipHandle) -> {ok, Result} | {error, Reason} when
1227      Result :: [zip_comment() | zip_file()],
1228      ZipHandle :: handle(),
1229      Reason :: term()).
1230
1231zip_list_dir(Pid) when is_pid(Pid) ->
1232    request(self(), Pid, list_dir).
1233
1234zip_list_dir(Pid, Opts) when is_pid(Pid) ->
1235    request(self(), Pid, {list_dir, Opts}).
1236
1237zip_get_state(Pid) when is_pid(Pid) ->
1238    request(self(), Pid, get_state).
1239
1240request(Self, Pid, Req) ->
1241    Pid ! {Self, Req},
1242    receive
1243	{Pid, R} -> R
1244    end.
1245
1246zip_t(Pid) when is_pid(Pid) ->
1247    Openzip = request(self(), Pid, get_state),
1248    openzip_t(Openzip).
1249
1250zip_tt(Pid) when is_pid(Pid) ->
1251    Openzip = request(self(), Pid, get_state),
1252    openzip_tt(Openzip).
1253
1254openzip_tt(#openzip{zip_comment = ZipComment, files = Files}) ->
1255    print_comment(ZipComment),
1256    lists_foreach(fun({#zip_file{comp_size = CompSize,
1257				name = FileName,
1258				comment = FileComment,
1259				info = FI},_}) ->
1260			  #file_info{size = UncompSize, mtime = MTime} = FI,
1261			  print_header(CompSize, MTime, UncompSize,
1262				       FileName, FileComment)
1263		  end, Files),
1264    ok.
1265
1266openzip_t(#openzip{zip_comment = ZipComment, files = Files}) ->
1267    print_comment(ZipComment),
1268    lists_foreach(fun({#zip_file{name = FileName},_}) ->
1269			  print_file_name(FileName)
1270		  end, Files),
1271    ok.
1272
1273lists_foreach(_, []) ->
1274    ok;
1275lists_foreach(F, [Hd|Tl]) ->
1276    F(Hd),
1277    lists_foreach(F, Tl).
1278
1279%% option utils
1280get_openzip_opt([], Opts) ->
1281    Opts;
1282get_openzip_opt([cooked | Rest], #openzip_opts{open_opts = OO} = Opts) ->
1283    get_openzip_opt(Rest, Opts#openzip_opts{open_opts = OO -- [raw]});
1284get_openzip_opt([memory | Rest], Opts) ->
1285    get_openzip_opt(Rest, Opts#openzip_opts{output = fun binary_io/2});
1286get_openzip_opt([{cwd, CWD} | Rest], Opts) ->
1287    get_openzip_opt(Rest, Opts#openzip_opts{cwd = CWD});
1288get_openzip_opt([Unknown | _Rest], _Opts) ->
1289    throw({bad_option, Unknown}).
1290
1291%% get the central directory from the archive
1292get_central_dir(In0, RawIterator, Input) ->
1293    {B, In1} = get_end_of_central_dir(In0, ?END_OF_CENTRAL_DIR_SZ, Input),
1294    {EOCD, BComment} = eocd_and_comment_from_bin(B),
1295    In2 = Input({seek, bof, EOCD#eocd.offset}, In1),
1296    N = EOCD#eocd.entries,
1297    Acc0 = [],
1298    %% There is no encoding flag for the archive comment.
1299    Comment = heuristic_to_string(BComment),
1300    Out0 = RawIterator(EOCD, "", Comment, <<>>, Acc0),
1301    get_cd_loop(N, In2, RawIterator, Input, Out0).
1302
1303get_cd_loop(0, In, _RawIterator, _Input, Acc) ->
1304    {lists:reverse(Acc), In};
1305get_cd_loop(N, In0, RawIterator, Input, Acc0) ->
1306    {B, In1} = Input({read, ?CENTRAL_FILE_HEADER_SZ}, In0),
1307    BCD = case B of
1308	      <<?CENTRAL_FILE_MAGIC:32/little, XBCD/binary>> -> XBCD;
1309	      _ -> throw(bad_central_directory)
1310	  end,
1311    CD = cd_file_header_from_bin(BCD),
1312    FileNameLen = CD#cd_file_header.file_name_length,
1313    ExtraLen = CD#cd_file_header.extra_field_length,
1314    CommentLen = CD#cd_file_header.file_comment_length,
1315    ToRead = FileNameLen + ExtraLen + CommentLen,
1316    GPFlag = CD#cd_file_header.gp_flag,
1317    {B2, In2} = Input({read, ToRead}, In1),
1318    {FileName, Comment, BExtra} =
1319	get_name_extra_comment(B2, FileNameLen, ExtraLen, CommentLen, GPFlag),
1320    Acc1 = RawIterator(CD, FileName, Comment, BExtra, Acc0),
1321    get_cd_loop(N-1, In2, RawIterator, Input, Acc1).
1322
1323get_name_extra_comment(B, FileNameLen, ExtraLen, CommentLen, GPFlag) ->
1324    try
1325        <<BFileName:FileNameLen/binary,
1326          BExtra:ExtraLen/binary,
1327          BComment:CommentLen/binary>> = B,
1328        {binary_to_chars(BFileName, GPFlag),
1329         %% Appendix D says: "If general purpose bit 11 is unset, the
1330         %% file name and comment should conform to the original ZIP
1331         %% character encoding." However, it seems that at least Linux
1332         %% zip(1) encodes the comment without setting bit 11 if the
1333         %% filename is 7-bit ASCII. If bit 11 is set,
1334         %% binary_to_chars/1 could (should?) be called (it can fail),
1335         %% but the choice is to employ heuristics in this case too
1336         %% (it does not fail).
1337         heuristic_to_string(BComment),
1338         BExtra}
1339    catch
1340        _:_ ->
1341            throw(bad_central_directory)
1342    end.
1343
1344%% get end record, containing the offset to the central directory
1345%% the end record is always at the end of the file BUT alas it is
1346%% of variable size (yes that's dumb!)
1347get_end_of_central_dir(_In, Sz, _Input) when Sz > 16#ffff ->
1348    throw(bad_eocd);
1349get_end_of_central_dir(In0, Sz, Input) ->
1350    In1 = Input({seek, eof, -Sz}, In0),
1351    {B, In2} = Input({read, Sz}, In1),
1352    case find_eocd_header(B) of
1353	none ->
1354	    get_end_of_central_dir(In2, Sz+Sz, Input);
1355	Header ->
1356	    {Header, In2}
1357    end.
1358
1359%% find the end record by matching for it
1360find_eocd_header(<<?END_OF_CENTRAL_DIR_MAGIC:32/little, Rest/binary>>) ->
1361    Rest;
1362find_eocd_header(<<_:8, Rest/binary>>)
1363  when byte_size(Rest) > ?END_OF_CENTRAL_DIR_SZ-4 ->
1364    find_eocd_header(Rest);
1365find_eocd_header(_) ->
1366    none.
1367
1368%% from a central directory record, filter and accumulate what we need
1369
1370%% with zip_file_extra
1371raw_file_info_etc(CD, FileName, FileComment, BExtraField, Acc)
1372  when is_record(CD, cd_file_header) ->
1373    #cd_file_header{comp_size = CompSize,
1374		    local_header_offset = Offset,
1375		    crc32 = CRC} = CD,
1376    FileInfo = cd_file_header_to_file_info(FileName, CD, BExtraField),
1377    [{#zip_file{name = FileName, info = FileInfo, comment = FileComment,
1378		offset = Offset, comp_size = CompSize}, #zip_file_extra{crc32 = CRC}} | Acc];
1379raw_file_info_etc(EOCD, _, Comment, _, Acc) when is_record(EOCD, eocd) ->
1380    [#zip_comment{comment = Comment} | Acc].
1381
1382%% without zip_file_extra
1383raw_file_info_public(CD, FileName, FileComment, BExtraField, Acc0) ->
1384    [H1|T] = raw_file_info_etc(CD,FileName,FileComment,BExtraField,Acc0),
1385    H2 = case H1 of
1386	     {ZF,Extra} when is_record(Extra,zip_file_extra) -> ZF;
1387	     Other -> Other
1388	 end,
1389    [H2|T].
1390
1391
1392%% make a file_info from a central directory header
1393cd_file_header_to_file_info(FileName,
1394			    #cd_file_header{uncomp_size = UncompSize,
1395					    last_mod_time = ModTime,
1396					    last_mod_date = ModDate},
1397			    ExtraField) ->
1398    T = dos_date_time_to_datetime(ModDate, ModTime),
1399    Type =
1400	case lists:last(FileName) of
1401	    $/ -> directory;
1402	    _  -> regular
1403	end,
1404    FI = #file_info{size = UncompSize,
1405		    type = Type,
1406		    access = read_write,
1407		    atime = T,
1408		    mtime = T,
1409		    ctime = T,
1410		    mode = 8#066,
1411		    links = 1,
1412		    major_device = 0,
1413		    minor_device = 0,
1414		    inode = 0,
1415		    uid = 0,
1416		    gid = 0},
1417    add_extra_info(FI, ExtraField).
1418
1419%% Currently, we ignore all the extra fields.
1420add_extra_info(FI, _) ->
1421    FI.
1422
1423
1424
1425%% get all files using file list
1426%% (the offset list is already filtered on which file to get... isn't it?)
1427get_z_files([], _Z, _In, _Opts, Acc) ->
1428    lists:reverse(Acc);
1429get_z_files([#zip_comment{comment = _} | Rest], Z, In, Opts, Acc) ->
1430    get_z_files(Rest, Z, In, Opts, Acc);
1431get_z_files([{#zip_file{offset = Offset},_} = ZFile | Rest], Z, In0,
1432	    #unzip_opts{input = Input, output = Output, open_opts = OpO,
1433			file_filter = Filter, feedback = FB,
1434			cwd = CWD} = Opts, Acc0) ->
1435    case Filter(ZFile) of
1436	true ->
1437	    In1 = Input({seek, bof, Offset}, In0),
1438	    {In2, Acc1} =
1439		case get_z_file(In1, Z, Input, Output, OpO, FB,
1440				CWD, ZFile, Filter) of
1441		    {file, GZD, Inx} -> {Inx, [GZD | Acc0]};
1442		    {_, Inx}       -> {Inx, Acc0}
1443		end,
1444	    get_z_files(Rest, Z, In2, Opts, Acc1);
1445	_ ->
1446	    get_z_files(Rest, Z, In0, Opts, Acc0)
1447    end.
1448
1449%% get a file from the archive, reading chunks
1450get_z_file(In0, Z, Input, Output, OpO, FB,
1451	   CWD, {ZipFile,Extra}, Filter) ->
1452    case Input({read, ?LOCAL_FILE_HEADER_SZ}, In0) of
1453	{eof, In1} ->
1454	    {eof, In1};
1455	%% Local File Header
1456	{<<?LOCAL_FILE_MAGIC:32/little, B/binary>>, In1} ->
1457	    LH = local_file_header_from_bin(B),
1458	    #local_file_header{gp_flag = GPFlag,
1459			       comp_method = CompMethod,
1460			       file_name_length = FileNameLen,
1461			       extra_field_length = ExtraLen} = LH,
1462
1463	    {CompSize,CRC32} = case GPFlag band 8 =:= 8 of
1464				   true -> {ZipFile#zip_file.comp_size,
1465					    Extra#zip_file_extra.crc32};
1466				   false -> {LH#local_file_header.comp_size,
1467					     LH#local_file_header.crc32}
1468			       end,
1469	    {BFileN, In3} = Input({read, FileNameLen + ExtraLen}, In1),
1470	    {FileName, _} =
1471                get_file_name_extra(FileNameLen, ExtraLen, BFileN, GPFlag),
1472	    ReadAndWrite =
1473		case check_valid_location(CWD, FileName) of
1474		    {true,FileName1} ->
1475			true;
1476		    {false,FileName1} ->
1477			Filter({ZipFile#zip_file{name = FileName1},Extra})
1478		end,
1479	    case ReadAndWrite of
1480		true ->
1481		    case lists:last(FileName) of
1482			$/ ->
1483			    %% perhaps this should always be done?
1484			    Output({ensure_dir,FileName1},[]),
1485			    {dir, In3};
1486			_ ->
1487			    %% FileInfo = local_file_header_to_file_info(LH)
1488			    %%{Out, In4, CRC, UncompSize} =
1489			    {Out, In4, CRC, _UncompSize} =
1490				get_z_data(CompMethod, In3, FileName1,
1491					   CompSize, Input, Output, OpO, Z),
1492			    In5 = skip_z_data_descriptor(GPFlag, Input, In4),
1493			    %% TODO This should be fixed some day:
1494			    %% In5 = Input({set_file_info, FileName,
1495			    %% FileInfo#file_info{size=UncompSize}}, In4),
1496			    FB(FileName),
1497			    CRC =:= CRC32 orelse throw({bad_crc, FileName}),
1498			    {file, Out, In5}
1499		    end;
1500		false ->
1501		    {ignore, In3}
1502	    end;
1503	_ ->
1504	    throw(bad_local_file_header)
1505    end.
1506
1507%% make sure FileName doesn't have relative path that points over CWD
1508check_valid_location(CWD, FileName) ->
1509    %% check for directory traversal exploit
1510    case check_dir_level(filename:split(FileName), 0) of
1511	{FileOrDir,Level} when Level < 0 ->
1512	    CWD1 = if CWD == "" -> "./";
1513		      true      -> CWD
1514		   end,
1515	    error_logger:format("Illegal path: ~ts, extracting in ~ts~n",
1516				[add_cwd(CWD,FileName),CWD1]),
1517	    {false,add_cwd(CWD, FileOrDir)};
1518        _ ->
1519	    {true,add_cwd(CWD, FileName)}
1520    end.
1521
1522check_dir_level([FileOrDir], Level) ->
1523    {FileOrDir,Level};
1524check_dir_level(["." | Parts], Level) ->
1525    check_dir_level(Parts, Level);
1526check_dir_level([".." | Parts], Level) ->
1527    check_dir_level(Parts, Level-1);
1528check_dir_level([_Dir | Parts], Level) ->
1529    check_dir_level(Parts, Level+1).
1530
1531get_file_name_extra(FileNameLen, ExtraLen, B, GPFlag) ->
1532    try
1533        <<BFileName:FileNameLen/binary, BExtra:ExtraLen/binary>> = B,
1534        {binary_to_chars(BFileName, GPFlag), BExtra}
1535    catch
1536        _:_ ->
1537            throw(bad_file_header)
1538    end.
1539
1540%% get compressed or stored data
1541get_z_data(?DEFLATED, In0, FileName, CompSize, Input, Output, OpO, Z) ->
1542    ok = zlib:inflateInit(Z, -?MAX_WBITS),
1543    Out0 = Output({open, FileName, [write | OpO]}, []),
1544    {In1, Out1, UncompSize} = get_z_data_loop(CompSize, 0, In0, Out0, Input, Output, Z),
1545    CRC = zlib:crc32(Z),
1546    ?CATCH zlib:inflateEnd(Z),
1547    Out2 = Output({close, FileName}, Out1),
1548    {Out2, In1, CRC, UncompSize};
1549get_z_data(?STORED, In0, FileName, CompSize, Input, Output, OpO, Z) ->
1550    Out0 = Output({open, FileName, [write | OpO]}, []),
1551    CRC0 = zlib:crc32(Z, <<>>),
1552    {In1, Out1, CRC} = copy_data_loop(CompSize, In0, Out0, Input, Output,
1553				      CRC0, Z),
1554    Out2 = Output({close, FileName}, Out1),
1555    {Out2, In1, CRC, CompSize};
1556get_z_data(_, _, _, _, _, _, _, _) ->
1557    throw(bad_file_header).
1558
1559copy_data_loop(0, In, Out, _Input, _Output, CRC, _Z) ->
1560    {In, Out, CRC};
1561copy_data_loop(CompSize, In0, Out0, Input, Output, CRC0, Z) ->
1562    N = erlang:min(?READ_BLOCK_SIZE, CompSize),
1563    case Input({read, N}, In0) of
1564	{eof, In1} -> {Out0, In1};
1565	{Uncompressed, In1} ->
1566	    CRC1 = zlib:crc32(Z, CRC0, Uncompressed),
1567	    Out1 = Output({write, Uncompressed}, Out0),
1568	    copy_data_loop(CompSize-N, In1, Out1, Input, Output, CRC1, Z)
1569    end.
1570
1571get_z_data_loop(0, UncompSize, In, Out, _Input, _Output, _Z) ->
1572    {In, Out, UncompSize};
1573get_z_data_loop(CompSize, UncompSize, In0, Out0, Input, Output, Z) ->
1574    N = erlang:min(?READ_BLOCK_SIZE, CompSize),
1575    case Input({read, N}, In0) of
1576	{eof, In1} ->
1577	    {Out0, In1};
1578	{Compressed, In1} ->
1579	    Uncompressed = zlib:inflate(Z, Compressed),
1580	    Out1 = Output({write, Uncompressed}, Out0),
1581	    get_z_data_loop(CompSize-N, UncompSize + iolist_size(Uncompressed),
1582			    In1, Out1, Input, Output, Z)
1583    end.
1584
1585
1586%% skip data descriptor if any
1587skip_z_data_descriptor(GPFlag, Input, In0) when GPFlag band 8 =:= 8 ->
1588    Input({seek, cur, 12}, In0);
1589skip_z_data_descriptor(_GPFlag, _Input, In0) ->
1590    In0.
1591
1592%% convert between erlang datetime and the MSDOS date and time
1593%% that's stored in the zip archive
1594%%    	 MSDOS Time  	           MSDOS Date
1595%% bit   0 - 4 	 5 - 10 11 - 15    16 - 20      21 - 24        25 - 31
1596%% value second  minute hour 	   day (1 - 31) month (1 - 12) years from 1980
1597dos_date_time_to_datetime(DosDate, DosTime) ->
1598    <<Hour:5, Min:6, Sec:5>> = <<DosTime:16>>,
1599    <<YearFrom1980:7, Month:4, Day:5>> = <<DosDate:16>>,
1600    {{YearFrom1980+1980, Month, Day},
1601     {Hour, Min, Sec}}.
1602
1603dos_date_time_from_datetime({{Year, Month, Day}, {Hour, Min, Sec}}) ->
1604    YearFrom1980 = Year-1980,
1605    <<DosTime:16>> = <<Hour:5, Min:6, Sec:5>>,
1606    <<DosDate:16>> = <<YearFrom1980:7, Month:4, Day:5>>,
1607    {DosDate, DosTime}.
1608
1609%% A pwrite-like function for iolists (used by memory-option)
1610
1611pwrite_binary(B, Pos, Bin) when byte_size(B) =:= Pos ->
1612    append_bins(Bin, B);
1613pwrite_binary(B, Pos, Bin) ->
1614    erlang:iolist_to_binary(pwrite_iolist(B, Pos, Bin)).
1615
1616append_bins([Bin|Bins], B) when is_binary(Bin) ->
1617    append_bins(Bins, <<B/binary, Bin/binary>>);
1618append_bins([List|Bins], B) when is_list(List) ->
1619    append_bins(Bins, append_bins(List, B));
1620append_bins(Bin, B) when is_binary(Bin) ->
1621    <<B/binary, Bin/binary>>;
1622append_bins([_|_]=List, B) ->
1623    <<B/binary, (iolist_to_binary(List))/binary>>;
1624append_bins([], B) ->
1625    B.
1626
1627-dialyzer({no_improper_lists, pwrite_iolist/3}).
1628
1629pwrite_iolist(B, Pos, Bin) ->
1630    {Left, Right} = split_binary(B, Pos),
1631    Sz = erlang:iolist_size(Bin),
1632    R = skip_bin(Right, Sz),
1633    [Left, Bin | R].
1634
1635skip_bin(B, Pos) when is_binary(B) ->
1636    case B of
1637	<<_:Pos/binary, Bin/binary>> -> Bin;
1638	_ -> <<>>
1639    end.
1640
1641binary_to_chars(B, GPFlag) ->
1642    ?SHOW_GP_BIT_11(B, GPFlag band ?GP_BIT_11),
1643    case GPFlag band ?GP_BIT_11 of
1644        0 ->
1645            binary_to_list(B);
1646        ?GP_BIT_11 ->
1647            case unicode:characters_to_list(B) of
1648                List when is_list(List) ->
1649                    List
1650            end
1651    end.
1652
1653heuristic_to_string(B) when is_binary(B) ->
1654    case unicode:characters_to_binary(B) of
1655	B ->
1656            unicode:characters_to_list(B);
1657	_ ->
1658            binary_to_list(B)
1659    end.
1660
1661encode_string(String) ->
1662    case lists:any(fun(C) -> C > 127 end, String) of
1663        true ->
1664            case unicode:characters_to_binary(String) of
1665                B when is_binary(B) ->
1666                    {binary_to_list(B), ?GP_BIT_11};
1667                _ ->
1668                    throw({bad_unicode, String})
1669            end;
1670        false ->
1671            {String, 0}
1672    end.
1673
1674%% ZIP header manipulations
1675eocd_and_comment_from_bin(<<DiskNum:16/little,
1676			   StartDiskNum:16/little,
1677			   EntriesOnDisk:16/little,
1678			   Entries:16/little,
1679			   Size:32/little,
1680			   Offset:32/little,
1681			   ZipCommentLength:16/little,
1682			   Comment:ZipCommentLength/binary>>) ->
1683    {#eocd{disk_num = DiskNum,
1684	   start_disk_num = StartDiskNum,
1685	   entries_on_disk = EntriesOnDisk,
1686	   entries = Entries,
1687	   size = Size,
1688	   offset = Offset,
1689	   zip_comment_length = ZipCommentLength},
1690     Comment};
1691eocd_and_comment_from_bin(_) ->
1692    throw(bad_eocd).
1693
1694cd_file_header_from_bin(<<VersionMadeBy:16/little,
1695			 VersionNeeded:16/little,
1696			 GPFlag:16/little,
1697			 CompMethod:16/little,
1698			 LastModTime:16/little,
1699			 LastModDate:16/little,
1700			 CRC32:32/little,
1701			 CompSize:32/little,
1702			 UncompSize:32/little,
1703			 FileNameLength:16/little,
1704			 ExtraFieldLength:16/little,
1705			 FileCommentLength:16/little,
1706			 DiskNumStart:16/little,
1707			 InternalAttr:16/little,
1708			 ExternalAttr:32/little,
1709			 LocalHeaderOffset:32/little>>) ->
1710    #cd_file_header{version_made_by = VersionMadeBy,
1711		    version_needed = VersionNeeded,
1712		    gp_flag = GPFlag,
1713		    comp_method = CompMethod,
1714		    last_mod_time = LastModTime,
1715		    last_mod_date = LastModDate,
1716		    crc32 = CRC32,
1717		    comp_size = CompSize,
1718		    uncomp_size = UncompSize,
1719		    file_name_length = FileNameLength,
1720		    extra_field_length = ExtraFieldLength,
1721		    file_comment_length = FileCommentLength,
1722		    disk_num_start = DiskNumStart,
1723		    internal_attr = InternalAttr,
1724		    external_attr = ExternalAttr,
1725		    local_header_offset = LocalHeaderOffset};
1726cd_file_header_from_bin(_) ->
1727    throw(bad_cd_file_header).
1728
1729local_file_header_from_bin(<<VersionNeeded:16/little,
1730			    GPFlag:16/little,
1731			    CompMethod:16/little,
1732			    LastModTime:16/little,
1733			    LastModDate:16/little,
1734			    CRC32:32/little,
1735			    CompSize:32/little,
1736			    UncompSize:32/little,
1737			    FileNameLength:16/little,
1738			    ExtraFieldLength:16/little>>) ->
1739    #local_file_header{version_needed = VersionNeeded,
1740		       gp_flag = GPFlag,
1741		       comp_method = CompMethod,
1742		       last_mod_time = LastModTime,
1743		       last_mod_date = LastModDate,
1744		       crc32 = CRC32,
1745		       comp_size = CompSize,
1746		       uncomp_size = UncompSize,
1747		       file_name_length = FileNameLength,
1748		       extra_field_length = ExtraFieldLength};
1749local_file_header_from_bin(_) ->
1750    throw(bad_local_file_header).
1751
1752%% make a file_info from a local directory header
1753%% local_file_header_to_file_info(
1754%%   #local_file_header{last_mod_time = ModTime,
1755%% 		     last_mod_date = ModDate,
1756%% 		     uncomp_size = UncompSize}) ->
1757%%     T = dos_date_time_to_datetime(ModDate, ModTime),
1758%%     FI = #file_info{size = UncompSize,
1759%% 		    type = regular,
1760%% 		    access = read_write,
1761%% 		    atime = T,
1762%% 		    mtime = T,
1763%% 		    ctime = T,
1764%% 		    mode = 8#066,
1765%% 		    links = 1,
1766%% 		    major_device = 0,
1767%% 		    minor_device = 0,
1768%% 		    inode = 0,
1769%% 		    uid = 0,
1770%% 		    gid = 0},
1771%%     FI.
1772
1773%% io functions
1774binary_io({file_info, {_Filename, _B, #file_info{} = FI}}, _A) ->
1775    FI;
1776binary_io({file_info, {_Filename, #file_info{} = FI, _B}}, _A) ->
1777    FI;
1778binary_io({file_info, {_Filename, B}}, A) ->
1779    binary_io({file_info, B}, A);
1780binary_io({file_info, B}, _) ->
1781    {Type, Size} =
1782	if
1783	    is_binary(B) -> {regular, byte_size(B)};
1784	    B =:= directory -> {directory, 0}
1785	end,
1786    Now = calendar:local_time(),
1787    #file_info{size = Size, type = Type,
1788	       access = read_write, atime = Now,
1789	       mtime = Now, ctime = Now, mode = 0,
1790	       links = 1, major_device = 0,
1791	       minor_device = 0, inode = 0,
1792	       uid = 0, gid = 0};
1793binary_io({open, {_Filename, B, _FI}, _Opts}, _) when is_binary(B) ->
1794    {0, B};
1795binary_io({open, {_Filename, _FI, B}, _Opts}, _) when is_binary(B) ->
1796    {0, B};
1797binary_io({open, {_Filename, B}, _Opts}, _) when is_binary(B) ->
1798    {0, B};
1799binary_io({open, B, _Opts}, _) when is_binary(B) ->
1800    {0, B};
1801binary_io({open, Filename, _Opts}, _) when is_list(Filename) ->
1802    {0, <<>>};
1803binary_io({read, N}, {Pos, B}) when Pos >= byte_size(B) ->
1804    {eof, {Pos+N, B}};
1805binary_io({read, N}, {Pos, B}) when Pos + N > byte_size(B) ->
1806    <<_:Pos/binary, Read/binary>> = B,
1807    {Read, {byte_size(B), B}};
1808binary_io({pread, Pos, N}, {OldPos, B}) ->
1809    case B of
1810	<<_:Pos/binary, Read:N/binary, _Rest/binary>> ->
1811	    {Read, {Pos+N, B}};
1812	_ ->
1813	    {eof, {OldPos, B}}
1814    end;
1815binary_io({read, N}, {Pos, B}) ->
1816    <<_:Pos/binary, Read:N/binary, _/binary>> = B,
1817    {Read, {Pos+N, B}};
1818binary_io({seek, bof, Pos}, {_OldPos, B}) ->
1819    {Pos, B};
1820binary_io({seek, cur, Pos}, {OldPos, B}) ->
1821    {OldPos + Pos, B};
1822binary_io({seek, eof, Pos}, {_OldPos, B}) ->
1823    {byte_size(B) + Pos, B};
1824binary_io({pwrite, Pos, Data}, {OldPos, B}) ->
1825    {OldPos, pwrite_binary(B, Pos, Data)};
1826binary_io({write, Data}, {Pos, B}) ->
1827    {Pos + erlang:iolist_size(Data), pwrite_binary(B, Pos, Data)};
1828binary_io(close, {_Pos, B}) ->
1829    B;
1830binary_io({close, FN}, {_Pos, B}) ->
1831    {FN, B};
1832binary_io({list_dir, _F}, _B) ->
1833    [];
1834binary_io({set_file_info, _F, _FI}, B) ->
1835    B;
1836binary_io({ensure_dir, _Dir}, B) ->
1837    B.
1838
1839file_io({file_info, F}, _) ->
1840    case file:read_file_info(F) of
1841	{ok, Info} -> Info;
1842	{error, E} -> throw(E)
1843    end;
1844file_io({open, FN, Opts}, _) ->
1845    case lists:member(write, Opts) of
1846	true -> ok = filelib:ensure_dir(FN);
1847	_ -> ok
1848    end,
1849    case file:open(FN, Opts++[binary]) of
1850	{ok, H} -> H;
1851	{error, E} -> throw(E)
1852    end;
1853file_io({read, N}, H) ->
1854    case file:read(H, N) of
1855	{ok, B} -> {B, H};
1856	eof -> {eof, H};
1857	{error, E} -> throw(E)
1858    end;
1859file_io({pread, Pos, N}, H) ->
1860    case file:pread(H, Pos, N) of
1861	{ok, B} -> {B, H};
1862	eof -> {eof, H};
1863	{error, E} -> throw(E)
1864    end;
1865file_io({seek, S, Pos}, H) ->
1866    case file:position(H, {S, Pos}) of
1867	{ok, _NewPos} -> H;
1868	{error, Error} -> throw(Error)
1869    end;
1870file_io({write, Data}, H) ->
1871    case file:write(H, Data) of
1872	ok -> H;
1873	{error, Error} -> throw(Error)
1874    end;
1875file_io({pwrite, Pos, Data}, H) ->
1876    case file:pwrite(H, Pos, Data) of
1877	ok -> H;
1878	{error, Error} -> throw(Error)
1879    end;
1880file_io({close, FN}, H) ->
1881    case file:close(H) of
1882	ok -> FN;
1883	{error, Error} -> throw(Error)
1884    end;
1885file_io(close, H) ->
1886    file_io({close, ok}, H);
1887file_io({list_dir, F}, _H) ->
1888    case file:list_dir(F) of
1889	{ok, Files} -> Files;
1890	{error, Error} -> throw(Error)
1891    end;
1892file_io({set_file_info, F, FI}, H) ->
1893    case file:write_file_info(F, FI) of
1894	ok -> H;
1895	{error, Error} -> throw(Error)
1896    end;
1897file_io({ensure_dir, Dir}, H) ->
1898    ok = filelib:ensure_dir(Dir),
1899    H.
1900