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