%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2006-2024. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%
-module(zip).
-moduledoc """
Utility for reading and creating 'zip' archives.

This module archives and extracts files to and from a zip archive. The zip
format is specified by the "ZIP Appnote.txt" file, available on the PKWARE web
site [www.pkware.com](http://www.pkware.com).

The zip module supports zip archive versions up to 6.1. However,
password-protection and Zip64 are not supported.

By convention, the name of a zip file is to end with `.zip`. To abide to the
convention, add `.zip` to the filename.

- To create zip archives, use function `zip/2` or [`zip/3`](`zip/2`). They are
  also available as [`create/2,3`](`create/3`), to resemble the `m:erl_tar` module.
- To extract files from a zip archive, use function `unzip/1` or `unzip/2`. They
  are also available as [`extract/1,2`](`extract/1`), to resemble the `m:erl_tar` module.
- To fold a function over all files in a zip archive, use function `foldl/3`.
- To return a list of the files in a zip archive, use function `list_dir/1` or
  `list_dir/2`. They are also available as [`table/1,2`](`table/1`), to resemble the
  `m:erl_tar` module.
- To print a list of files to the Erlang shell, use function `t/1` or `tt/1`.
- Sometimes it is desirable to open a zip archive, and to unzip files from it
  file by file, without having to reopen the archive. This can be done by
  functions [`zip_open/1,2`](`zip_open/1`), [`zip_get/1,2`](`zip_get/1`),
  `zip_list_dir/1`, and `zip_close/1`.

## Limitations

- Zip64 archives are not supported.
- Password-protected and encrypted archives are not supported.
- Only the DEFLATE (zlib-compression) and the STORE (uncompressed data) zip
  methods are supported.
- The archive size is limited to 2 GB (32 bits).
- Comments for individual files are not supported when creating zip archives.
  The zip archive comment for the whole zip archive is supported.
- Changing a zip archive is not supported. To add or remove a file from an
  archive, the whole archive must be recreated.
""".
-define(ERL_TAR_COMPATIBILITY, ~"erl_tar compatibility functions").
-moduledoc(#{ titles => [{function, ?ERL_TAR_COMPATIBILITY}]}).

%% Basic api
-export([unzip/1, unzip/2, extract/1, extract/2,
	 zip/2, zip/3, create/2, create/3, foldl/3,
	 list_dir/1, list_dir/2, table/1, table/2,
	 t/1, tt/1]).

%% unzipping piecemeal
-export([openzip_open/1, openzip_open/2,
	 openzip_get/1, openzip_get/2,
	 openzip_t/1, openzip_tt/1,
	 openzip_list_dir/1, openzip_list_dir/2,
	 openzip_close/1]).
%% 	 openzip_add/2]).

%% zip server
-export([zip_open/1, zip_open/2,
	 zip_get/1, zip_get/2, zip_get_crc32/2,
	 zip_t/1, zip_tt/1,
	 zip_list_dir/1, zip_list_dir/2,
	 zip_close/1]).

%% just for debugging zip server, not documented, not tested, not to be used
-export([zip_get_state/1]).

%% includes
-include("file.hrl").		 % #file_info
-include("zip.hrl").	         % #zip_file, #zip_comment

%% max bytes fed to zlib
-define(WRITE_BLOCK_SIZE, 8*1024).

%% for debugging, to turn off catch
-define(CATCH(Expr), (catch (Expr))).

%% Debug.
-define(SHOW_GP_BIT_11(B, F), ok).
%%-define(SHOW_GP_BIT_11(B, F), io:format("F = ~.16#, B = ~lp\n", [F, B])).

%% option sets
-record(unzip_opts, {
	  output,      % output object (fun)
	  input,       % input object (fun)
	  file_filter, % file filter (boolean fun)
	  open_opts,   % options passed to file:open
	  feedback,    % feeback (fun)
	  cwd          % directory to relate paths to
	 }).

-record(zip_opts, {
	  output,      % output object (fun)
	  input,       % input object (fun)
	  comment,     % zip-file comment
	  open_opts,   % options passed to file:open
	  feedback,    % feeback (fun)
	  cwd,         % directory to relate paths to
	  compress,    % compress files with these suffixes
	  uncompress   % uncompress files with these suffixes
	 }).

-record(list_dir_opts, {
	  input,       % input object (fun)
	  raw_iterator, % applied to each dir entry
	  open_opts    % options passed to file:open
	 }).

-record(openzip_opts, {
	  output,      % output object (fun)
	  open_opts,   % file:open options
	  cwd	       % directory to relate paths to
	 }).

% openzip record, state for an open zip-file
-record(openzip, {
	  zip_comment, % zip archive comment
	  files,       % filenames, infos, comments and offsets
	  in,          % archive handle
	  input,       % archive io object (fun)
	  output,      % output io object (fun)
	  zlib,	       % handle to open zlib
	  cwd	       % directory to relate paths to
	 }).

% Things that I would like to add to the public record #zip_file,
% but can't as it would make things fail at upgrade.
% Instead we use {#zip_file,#zip_file_extra} internally.
-record(zip_file_extra, {
	  crc32        % checksum
	 }).

%% max bytes read from files and archives (and fed to zlib)
-define(READ_BLOCK_SIZE, 16*1024).

%% -record(primzip_file, {
%% 	  name,
%% 	  offset,
%% 	  chunk_size
%% 	 }).

%% -record(primzip, {
%% 	  zlib,		% handle to the zlib port from zlib:open
%% 	  input,        % fun/2 for file/memory input
%% 	  in,		% input (file handle or binary)
%% 	  files		% [#primzip_file]
%% 	 }).

%% ZIP-file format records and defines

%% compression methods
-define(STORED, 0).
-define(UNCOMPRESSED, 0).
-define(SHRUNK, 1).
-define(REDUCED_1, 2).
-define(REDUCED_2, 3).
-define(REDUCED_3, 4).
-define(REDUCED_4, 5).
-define(IMPLODED, 6).
-define(TOKENIZED, 7).
-define(DEFLATED, 8).
-define(DEFLATED_64, 9).
-define(PKWARE_IMPLODED, 10).
-define(PKWARE_RESERVED, 11).
-define(BZIP2_COMPRESSED, 12).

%% Version 2.0, attribute compatibility type 3 (Unix)
-define(VERSION_MADE_BY, 20 bor (3 bsl 8)).
-define(GP_BIT_11, 16#800). % Filename and file comment UTF-8 encoded.

%% zip-file records
-define(LOCAL_FILE_MAGIC,16#04034b50).
-define(LOCAL_FILE_HEADER_SZ,(4+2+2+2+2+2+4+4+4+2+2)).
-define(LOCAL_FILE_HEADER_CRC32_OFFSET, 4+2+2+2+2+2).
-record(local_file_header, {version_needed,
			    gp_flag,
			    comp_method,
			    last_mod_time,
			    last_mod_date,
			    crc32,
			    comp_size,
			    uncomp_size,
			    file_name_length,
			    extra_field_length,
                            type}).

-define(CENTRAL_FILE_HEADER_SZ,(4+2+2+2+2+2+2+4+4+4+2+2+2+2+2+4+4)).

-define(CENTRAL_DIR_MAGIC, 16#06054b50).
-define(CENTRAL_DIR_SZ, (4+2+2+2+2+4+4+2)).
-define(CENTRAL_DIR_DIGITAL_SIG_MAGIC, 16#05054b50).
-define(CENTRAL_DIR_DIGITAL_SIG_SZ, (4+2)).
-define(CENTRAL_REGULAR_FILE_EXT_ATTRIBUTES, 8#644 bsl 16).
-define(CENTRAL_DIRECTORY_FILE_EXT_ATTRIBUTES, 8#744 bsl 16).
-define(CENTRAL_FILE_MAGIC, 16#02014b50).

-record(cd_file_header, {version_made_by,
			 version_needed,
			 gp_flag,
			 comp_method,
			 last_mod_time,
			 last_mod_date,
			 crc32,
			 comp_size,
			 uncomp_size,
			 file_name_length,
			 extra_field_length,
			 file_comment_length,
			 disk_num_start,
			 internal_attr,
			 external_attr,
			 local_header_offset}).

-define(END_OF_CENTRAL_DIR_MAGIC, 16#06054b50).
-define(END_OF_CENTRAL_DIR_SZ, (4+2+2+2+2+4+4+2)).

-record(eocd, {disk_num,
	       start_disk_num,
	       entries_on_disk,
	       entries,
	       size,
	       offset,
	       zip_comment_length}).


-doc "These options are described in [`create/3`](`m:zip#zip_options`).".
-type create_option() :: memory | cooked | verbose
                       | {comment, Comment ::string()}
                       | {cwd, CWD :: file:filename()}
                       | {compress, What :: extension_spec()}
                       | {uncompress, What :: extension_spec()}.
-type extension() :: string().
-type extension_spec() :: all
                        | [Extension :: extension()]
                        | {add, [Extension :: extension()]}
                        | {del, [Extension :: extension()]}.
-doc "The name of a zip file.".
-type filename() :: file:filename().

-doc "The record `zip_comment` only contains the archive comment for a zip archive.".
-type zip_comment() :: #zip_comment{}.
-doc """
The record `zip_file` contains the following fields:

- **`name`** - The filename

- **`info`** - File information as in `file:read_file_info/1` in Kernel

- **`comment`** - The comment for the file in the zip archive

- **`offset`** - The file offset in the zip archive (used internally)

- **`comp_size`** - The size of the compressed file (the size of the
  uncompressed file is found in `info`)
""".
-type zip_file() :: #zip_file{}.

-doc "As returned by `zip_open/2`.".
-opaque handle() :: pid().

-export_type([create_option/0, filename/0, handle/0]).

%% Open a zip archive with options
%%

-doc false.
openzip_open(F) ->
    openzip_open(F, []).

-doc false.
openzip_open(F, Options) ->
    case ?CATCH(do_openzip_open(F, Options)) of
	{ok, OpenZip} ->
	    {ok, OpenZip};
	Error ->
	    {error, Error}
    end.

do_openzip_open(F, Options) ->
    Opts = get_openzip_options(Options),
    #openzip_opts{output = Output, open_opts = OpO, cwd = CWD} = Opts,
    Input = get_input(F),
    In0 = Input({open, F, OpO -- [write]}, []),
    {[#zip_comment{comment = C} | Files], In1} =
	get_central_dir(In0, fun raw_file_info_etc/5, Input),
    Z = zlib:open(),
    {ok, #openzip{zip_comment = C,
		  files = Files,
		  in = In1,
		  input = Input,
		  output = Output,
		  zlib = Z,
		  cwd = CWD}}.

%% retrieve all files from an open archive
-doc false.
openzip_get(OpenZip) ->
    case ?CATCH(do_openzip_get(OpenZip)) of
	{ok, Result} -> {ok, Result};
	Error -> {error, Error}
    end.

do_openzip_get(#openzip{files = Files, in = In0, input = Input,
			output = Output, zlib = Z, cwd = CWD}) ->
    ZipOpts = #unzip_opts{output = Output, input = Input,
			  file_filter = fun all/1, open_opts = [],
			  feedback = fun silent/1, cwd = CWD},
    R = get_z_files(Files, Z, In0, ZipOpts, []),
    {ok, R};
do_openzip_get(_) ->
    throw(einval).

%% retrieve the crc32 checksum from an open archive
openzip_get_crc32(FileName, #openzip{files = Files}) ->
    case file_name_search(FileName, Files) of
	{_,#zip_file_extra{crc32=CRC}} -> {ok, CRC};
	_ -> throw(file_not_found)
    end.

%% retrieve a file from an open archive
-doc false.
openzip_get(FileName, OpenZip) ->
    case ?CATCH(do_openzip_get(FileName, OpenZip)) of
	{ok, Result} -> {ok, Result};
	Error -> {error, Error}
    end.

do_openzip_get(F, #openzip{files = Files, in = In0, input = Input,
			   output = Output, zlib = Z, cwd = CWD}) ->
    %%case lists:keysearch(F, #zip_file.name, Files) of
    case file_name_search(F, Files) of
	{#zip_file{offset = Offset},_}=ZFile ->
	    In1 = Input({seek, bof, Offset}, In0),
	    case get_z_file(In1, Z, Input, Output, [], fun silent/1,
			    CWD, ZFile, fun all/1) of
		{file, R, _In2} -> {ok, R};
		_ -> throw(file_not_found)
	    end;
	_ -> throw(file_not_found)
    end;
do_openzip_get(_, _) ->
    throw(einval).

file_name_search(Name,Files) ->
    Fun = fun({ZipFile,_}) ->
                  not string:equal(ZipFile#zip_file.name, Name,
                                   _IgnoreCase = false, _Norm = nfc)
          end,
    case lists:dropwhile(Fun, Files) of
	[ZFile|_] -> ZFile;
	[] -> false
    end.

%% %% add a file to an open archive
%% openzip_add(File, OpenZip) ->
%%     case ?CATCH do_openzip_add(File, OpenZip) of
%% 	{ok, Result} -> {ok, Result};
%% 	Error -> {error, Error}
%%     end.

%% do_openzip_add(File, #open_zip{files = Files, in = In0,
%% 			       opts = Opts} = OpenZip0) ->
%%     throw(nyi),
%%     Z = zlib:open(),
%%     R = get_z_files(Files, In0, Z, Opts, []),
%%     zlib:close(Z),
%%     {ok, R};
%% do_openzip_add(_, _) ->
%%     throw(einval).

%% get file list from open archive
-doc false.
openzip_list_dir(#openzip{zip_comment = Comment,
			  files = Files}) ->
    {ZipFiles,_Extras} = lists:unzip(Files),
    {ok, [#zip_comment{comment = Comment} | ZipFiles]};
openzip_list_dir(_) ->
    {error, einval}.

-doc false.
openzip_list_dir(#openzip{files = Files}, [names_only]) ->
    {ZipFiles,_Extras} = lists:unzip(Files),
    Names = [Name || {#zip_file{name=Name},_} <- ZipFiles],
    {ok, Names};
openzip_list_dir(_, _) ->
    {error, einval}.

%% close an open archive
-doc false.
openzip_close(#openzip{in = In0, input = Input, zlib = Z}) ->
    Input(close, In0),
    zlib:close(Z);
openzip_close(_) ->
    {error, einval}.

%% Extract from a zip archive with options
%%
%% Accepted options:
%% verbose, cooked, file_list, keep_old_files, file_filter, memory

-doc(#{equiv => unzip(Archive, [])}).
-spec(unzip(Archive) -> RetValue when
      Archive :: file:name() | binary(),
      RetValue :: {ok, FileList}
                | {ok, FileBinList}
                | {error, Reason :: term()}
                | {error, {Name :: file:name(), Reason :: term()}},
      FileList :: [file:name()],
      FileBinList :: [{file:name(),binary()}]).

unzip(F) -> unzip(F, []).

-doc """
Extracts all files from a zip archive.

If argument `Archive` is specified as a `t:binary/0`, the contents of the binary is
assumed to be a zip archive, otherwise a filename.

Options:

- **`{file_list, FileList}`** - By default, all files are extracted from the zip
  archive. With option `{file_list, FileList}`, function [`unzip/2`](`unzip/2`)
  only extracts the files whose names are included in `FileList`. The full
  paths, including the names of all subdirectories within the zip archive, must
  be specified.

- **`cooked`** - By default, this function opens the zip file in `raw` mode,
  which is faster but does not allow a remote (Erlang) file server to be used.
  Adding `cooked` to the mode list overrides the default and opens the zip file
  without option `raw`. The same applies for the files extracted.

- **`keep_old_files`** - By default, all files with the same name as files in
  the zip archive are overwritten. With option `keep_old_files` set, function
  [`unzip/2`](`unzip/2`) does not overwrite existing files. Notice that even
  with option `memory` specified, which means that no files are overwritten,
  existing files are excluded from the result.

- **`verbose`** - Prints an informational message for each extracted file.

- **`memory`** - Instead of extracting to the current directory, the result is
  given as a list of tuples `{Filename, Binary}`, where `Binary` is a binary
  containing the extracted data of file `Filename` in the zip archive.

- **`{cwd, CWD}`** - Uses the specified directory as current directory. It is
  prepended to filenames when extracting them from the zip archive. (Acting like
  `file:set_cwd/1` in Kernel, but without changing the global `cwd` property.)
""".
-spec(unzip(Archive, Options) -> RetValue when
      Archive :: file:name() | binary(),
      Options :: [Option],
      Option  :: {file_list, FileList} | cooked
               | keep_old_files | verbose | memory |
                 {file_filter, FileFilter} | {cwd, CWD},
      FileList :: [file:name()],
      FileBinList :: [{file:name(),binary()}],
      FileFilter :: fun((ZipFile) -> boolean()),
      CWD :: file:filename(),
      ZipFile :: zip_file(),
      RetValue :: {ok, FileList}
                | {ok, FileBinList}
                | {error, Reason :: term()}
                | {error, {Name :: file:name(), Reason :: term()}}).

unzip(F, Options) ->
    case ?CATCH(do_unzip(F, Options)) of
	{ok, R} -> {ok, R};
	Error -> {error, Error}
    end.

do_unzip(F, Options) ->
    Opts = get_unzip_options(F, Options),
    #unzip_opts{input = Input, open_opts = OpO} = Opts,
    In0 = Input({open, F, OpO -- [write]}, []),
    RawIterator = fun raw_file_info_etc/5,
    {Info, In1} = get_central_dir(In0, RawIterator, Input),
    %% get rid of zip-comment
    Z = zlib:open(),
    Files = try
                get_z_files(Info, Z, In1, Opts, [])
            after
                zlib:close(Z),
                Input(close, In1)
            end,
    {ok, Files}.

%% Iterate over all files in a zip archive
-doc """
Calls `Fun(FileInArchive, GetInfo , GetBin, AccIn)` on successive files in the
`Archive`, starting with `AccIn == Acc0`.

`FileInArchive` is the name that the file has in the archive.

`GetInfo` is a fun that returns information about the file.

`GetBin` returns the file contents.

Both `GetInfo` and `GetBin` must be called within the `Fun`. Their behavior is
undefined if they are called outside the context of `Fun`.

The `Fun` must return a new accumulator, which is passed to the next call.
[`foldl/3`](`foldl/3`) returns the final accumulator value. `Acc0` is returned
if the archive is empty. It is not necessary to iterate over all files in the
archive. The iteration can be ended prematurely in a controlled manner by
throwing an exception.

_Example:_

```erlang
> Name = "dummy.zip".
"dummy.zip"
> {ok, {Name, Bin}} = zip:create(Name, [{"foo", <<"FOO">>}, {"bar", <<"BAR">>}], [memory]).
{ok,{"dummy.zip",
     <<80,75,3,4,20,0,0,0,0,0,74,152,97,60,171,39,212,26,3,0,
       0,0,3,0,0,...>>}}
> {ok, FileSpec} = zip:foldl(fun(N, I, B, Acc) -> [{N, B(), I()} | Acc] end, [], {Name, Bin}).
{ok,[{"bar",<<"BAR">>,
      {file_info,3,regular,read_write,
                 {{2010,3,1},{19,2,10}},
                 {{2010,3,1},{19,2,10}},
                 {{2010,3,1},{19,2,10}},
                 54,1,0,0,0,0,0}},
     {"foo",<<"FOO">>,
      {file_info,3,regular,read_write,
                 {{2010,3,1},{19,2,10}},
                 {{2010,3,1},{19,2,10}},
                 {{2010,3,1},{19,2,10}},
                 54,1,0,0,0,0,0}}]}
> {ok, {Name, Bin}} = zip:create(Name, lists:reverse(FileSpec), [memory]).
{ok,{"dummy.zip",
     <<80,75,3,4,20,0,0,0,0,0,74,152,97,60,171,39,212,26,3,0,
       0,0,3,0,0,...>>}}
> catch zip:foldl(fun("foo", _, B, _) -> throw(B()); (_,_,_,Acc) -> Acc end, [], {Name, Bin}).
<<"FOO">>
```
""".
-doc(#{since => <<"OTP R14B">>}).
-spec(foldl(Fun, Acc0, Archive) -> {ok, Acc1} | {error, Reason} when
      Fun :: fun((FileInArchive, GetInfo, GetBin, AccIn) -> AccOut),
      FileInArchive :: file:name(),
      GetInfo :: fun(() -> file:file_info()),
      GetBin :: fun(() -> binary()),
      Acc0 :: term(),
      Acc1 :: term(),
      AccIn :: term(),
      AccOut :: term(),
      Archive :: file:name() | {file:name(), binary()},
      Reason :: term()).

foldl(Fun, Acc0, Archive) when is_function(Fun, 4) ->
    ZipFun =
	fun({Name, GetInfo, GetBin}, A) ->
		A2 = Fun(Name, GetInfo, GetBin, A),
		{true, false, A2}
	end,
    case prim_zip:open(ZipFun, Acc0, Archive) of
	{ok, PrimZip, Acc1} ->
	    ok = prim_zip:close(PrimZip),
	    {ok, Acc1};
	{error, bad_eocd} ->
	    {error, "Not an archive file"};
	{error, Reason} ->
	    {error, Reason}
    end;
foldl(_,_, _) ->
    {error, einval}.

%% Create zip archive name F from Files or binaries
%%
%% Accepted options:
%% verbose, cooked, memory, comment

-doc(#{equiv => zip(Name, FileList, [])}).
-spec(zip(Name, FileList) -> RetValue when
      Name     :: file:name(),
      FileList :: [FileSpec],
      FileSpec :: file:name() | {file:name(), binary()}
                | {file:name(), binary(), file:file_info()},
      RetValue :: {ok, FileName :: file:name()}
                | {ok, {FileName :: file:name(), binary()}}
                | {error, Reason :: term()}).

zip(F, Files) -> zip(F, Files, []).

-doc """
Creates a zip archive containing the files specified in `FileList`.

`FileList` is a list of files, with paths relative to the current directory,
which are stored with this path in the archive. File system operations are
performed to read the file metadata and, when compression is enabled, to stream
the file contents without loading whole files into memory. Files can also be
specified as binaries to create an archive directly from data. In such cases, no
metadata or file system reads are performed.

Files are compressed using the DEFLATE compression, as described in the
"Appnote.txt" file. However, files are stored without compression if they are
already compressed. [`zip/2`](`zip/2`) and [`zip/3`](`zip/3`) check the file
extension to determine if the file is to be stored without compression. Files
with the following extensions are not compressed: `.Z`, `.zip`, `.zoo`, `.arc`,
`.lzh`, `.arj`.

It is possible to override the default behavior and control what types of files
that are to be compressed by using options `{compress, What}` and
`{uncompress, What}`. It is also possible to use many `compress` and
`uncompress` options.

To trigger file compression, its extension must match with the `compress`
condition and must not match the `uncompress` condition. For example, if
`compress` is set to `["gif", "jpg"]` and `uncompress` is set to `["jpg"]`, only
files with extension `"gif"` are compressed.

[](){: #zip_options }

Options:

- **`cooked`** - By default, this function opens the zip file in mode `raw`,
  which is faster but does not allow a remote (Erlang) file server to be used.
  Adding `cooked` to the mode list overrides the default and opens the zip file
  without the `raw` option. The same applies for the files added.

- **`verbose`** - Prints an informational message about each added file.

- **`memory`** - The output is not to a file, but instead as a tuple
  `{FileName, binary()}`. The binary is a full zip archive with header and can
  be extracted with, for example, `unzip/2`.

- **`{comment, Comment}`** - Adds a comment to the zip archive.

- **`{cwd, CWD}`** - Uses the specified directory as current work directory
  (`cwd`). This is prepended to filenames when adding them, although not in the
  zip archive (acting like `file:set_cwd/1` in Kernel, but without changing the
  global `cwd` property.).

- **`{compress, What}`** - Controls what types of files to be compressed.
  Defaults to `all`. The following values of `What` are allowed:

  - **`all`** - All files are compressed (as long as they pass the `uncompress`
    condition).

  - **`[Extension]`** - Only files with exactly these extensions are compressed.

  - **`{add,[Extension]}`** - Adds these extensions to the list of compress
    extensions.

  - **`{del,[Extension]}`** - Deletes these extensions from the list of compress
    extensions.

- **`{uncompress, What}`** - Controls what types of files to be uncompressed.
  Defaults to `[".Z", ".zip", ".zoo", ".arc", ".lzh", ".arj"]`. The following
  values of `What` are allowed:

  - **`all`** - No files are compressed.

  - **`[Extension]`** - Files with these extensions are uncompressed.

  - **`{add,[Extension]}`** - Adds these extensions to the list of uncompress
    extensions.

  - **`{del,[Extension]}`** - Deletes these extensions from the list of
    uncompress extensions.
""".
-spec(zip(Name, FileList, Options) -> RetValue when
      Name     :: file:name(),
      FileList :: [FileSpec],
      FileSpec :: file:name() | {file:name(), binary()}
                | {file:name(), binary(), file:file_info()},
      Options  :: [Option],
      Option   :: create_option(),
      RetValue :: {ok, FileName :: file:name()}
                | {ok, {FileName :: file:name(), binary()}}
                | {error, Reason :: term()}).

zip(F, Files, Options) ->
    case ?CATCH(do_zip(F, Files, Options)) of
	{ok, R} -> {ok, R};
	Error -> {error, Error}
    end.

do_zip(F, Files, Options) ->
    Opts = get_zip_options(Files, Options),
    #zip_opts{output = Output, open_opts = OpO} = Opts,
    Out0 = Output({open, F, OpO}, []),
    Z = zlib:open(),
    try
        {Out1, LHS, Pos} = put_z_files(Files, Z, Out0, 0, Opts, []),
        zlib:close(Z),
        Out2 = put_central_dir(LHS, Pos, Out1, Opts),
        Out3 = Output({close, F}, Out2),
        {ok, Out3}
    catch
        C:R:Stk ->
            zlib:close(Z),
            Output({close, F}, Out0),
            erlang:raise(C, R, Stk)
    end.


%% List zip directory contents
%%
%% Accepted options:
%% cooked, file_filter, file_output (latter 2 undocumented)

-doc(#{equiv => list_dir(Archive, [])}).
-spec(list_dir(Archive) -> RetValue when
      Archive :: file:name() | binary(),
      RetValue :: {ok, CommentAndFiles} | {error, Reason :: term()},
      CommentAndFiles :: [zip_comment() | zip_file()]).

list_dir(F) -> list_dir(F, []).

-doc """
Retrieves all filenames in the zip archive `Archive`.

The result value is the tuple `{ok, List}`, where `List` contains the zip
archive comment as the first element.

One option is available:

- **`cooked`** - By default, this function opens the zip file in `raw` mode,
  which is faster but does not allow a remote (Erlang) file server to be used.
  Adding `cooked` to the mode list overrides the default and opens the zip file
  without option `raw`.
""".
-spec(list_dir(Archive, Options) -> RetValue when
      Archive :: file:name() | binary(),
      RetValue :: {ok, CommentAndFiles} | {error, Reason :: term()},
      CommentAndFiles :: [zip_comment() | zip_file()],
      Options :: [Option],
      Option :: cooked).

list_dir(F, Options) ->
    case ?CATCH(do_list_dir(F, Options)) of
	{ok, R} -> {ok, R};
	Error -> {error, Error}
    end.

do_list_dir(F, Options) ->
    Opts = get_list_dir_options(F, Options),
    #list_dir_opts{input = Input, open_opts = OpO,
		   raw_iterator = RawIterator} = Opts,
    In0 = Input({open, F, OpO}, []),
    {Info, In1} = get_central_dir(In0, RawIterator, Input),
    Input(close, In1),
    {ok, Info}.

%% Print zip directory in short form

-doc """
Prints all filenames in the zip archive `Archive` to the Erlang shell. (Similar
to `tar t`.)
""".
-spec(t(Archive) -> ok when
      Archive :: file:name() | binary() | ZipHandle,
      ZipHandle :: handle()).

t(F) when is_pid(F) -> zip_t(F);
t(F) when is_record(F, openzip) -> openzip_t(F);
t(F) -> t(F, fun raw_short_print_info_etc/5).

t(F, RawPrint) ->
    case ?CATCH(do_t(F, RawPrint)) of
	ok -> ok;
	Error -> {error, Error}
    end.

do_t(F, RawPrint) ->
    Input = get_input(F),
    OpO = [raw],
    In0 = Input({open, F, OpO}, []),
    {_Info, In1} = get_central_dir(In0, RawPrint, Input),
    Input(close, In1),
    ok.

%% Print zip directory in long form (like ls -l)

-doc """
Prints filenames and information about all files in the zip archive `Archive` to
the Erlang shell. (Similar to `tar tv`.)
""".
-spec(tt(Archive) -> ok when
      Archive :: file:name() | binary() | ZipHandle,
      ZipHandle :: handle()).

tt(F) when is_pid(F) -> zip_tt(F);
tt(F) when is_record(F, openzip) -> openzip_tt(F);
tt(F) -> t(F, fun raw_long_print_info_etc/5).


%% option utils
get_unzip_opt([], Opts) ->
    Opts;
get_unzip_opt([verbose | Rest], Opts) ->
    get_unzip_opt(Rest, Opts#unzip_opts{feedback = fun verbose_unzip/1});
get_unzip_opt([cooked | Rest], #unzip_opts{open_opts = OpO} = Opts) ->
    get_unzip_opt(Rest, Opts#unzip_opts{open_opts = OpO -- [raw]});
get_unzip_opt([memory | Rest], Opts) ->
    get_unzip_opt(Rest, Opts#unzip_opts{output = fun binary_io/2});
get_unzip_opt([{cwd, CWD} | Rest], Opts) ->
    get_unzip_opt(Rest, Opts#unzip_opts{cwd = CWD});
get_unzip_opt([{file_filter, F} | Rest], Opts) ->
    Filter1 = fun({ZipFile,_Extra}) -> F(ZipFile) end,
    Filter2 = fun_and_1(Filter1, Opts#unzip_opts.file_filter),
    get_unzip_opt(Rest, Opts#unzip_opts{file_filter = Filter2});
get_unzip_opt([{file_list, L} | Rest], Opts) ->
    FileInList = fun(F) -> file_in_list(F, L) end,
    Filter = fun_and_1(FileInList, Opts#unzip_opts.file_filter),
    get_unzip_opt(Rest, Opts#unzip_opts{file_filter = Filter});
get_unzip_opt([keep_old_files | Rest], Opts) ->
    Keep = fun keep_old_file/1,
    Filter = fun_and_1(Keep, Opts#unzip_opts.file_filter),
    get_unzip_opt(Rest, Opts#unzip_opts{file_filter = Filter});
get_unzip_opt([Unknown | _Rest], _Opts) ->
    throw({bad_option, Unknown}).

get_list_dir_opt([], Opts) ->
    Opts;
get_list_dir_opt([cooked | Rest], #list_dir_opts{open_opts = OpO} = Opts) ->
    get_list_dir_opt(Rest, Opts#list_dir_opts{open_opts = OpO -- [raw]});
get_list_dir_opt([names_only | Rest], Opts) ->
    get_list_dir_opt(Rest, Opts#list_dir_opts{
			     raw_iterator = fun(A, B, C, D, E) -> raw_name_only(A, B, C, D, E) end});
%% get_list_dir_opt([{file_output, F} | Rest], Opts) ->
%%     get_list_dir_opt(Rest, Opts#list_dir_opts{file_output = F});
%% get_list_dir_opt([{file_filter, F} | Rest], Opts) ->
%%     get_list_dir_opt(Rest, Opts#list_dir_opts{file_filter = F});
get_list_dir_opt([Unknown | _Rest], _Opts) ->
    throw({bad_option, Unknown}).

get_zip_opt([], Opts) ->
    Opts;
get_zip_opt([verbose | Rest], Opts) ->
    get_zip_opt(Rest, Opts#zip_opts{feedback = fun verbose_zip/1});
get_zip_opt([cooked | Rest], #zip_opts{open_opts = OpO} = Opts) ->
    get_zip_opt(Rest, Opts#zip_opts{open_opts = OpO -- [raw]});
get_zip_opt([memory | Rest], Opts) ->
    get_zip_opt(Rest, Opts#zip_opts{output = fun binary_io/2});
get_zip_opt([{cwd, CWD} | Rest], Opts) ->
    get_zip_opt(Rest, Opts#zip_opts{cwd = CWD});
get_zip_opt([{comment, C} | Rest], Opts) ->
    get_zip_opt(Rest, Opts#zip_opts{comment = C});
get_zip_opt([{compress, Which} = O| Rest], Opts) ->
    Which2 =
	case Which of
	    all ->
		all;
	    Suffixes when is_list(Suffixes) ->
		lists:usort(Suffixes);
	    {add, Suffixes} when is_list(Suffixes) ->
		lists:usort(Opts#zip_opts.compress ++ Suffixes);
	    {del, Suffixes} when is_list(Suffixes) ->
		lists:usort(Opts#zip_opts.compress -- Suffixes);
	    _ ->
		throw({bad_option, O})
	end,
    get_zip_opt(Rest, Opts#zip_opts{compress = Which2});
get_zip_opt([{uncompress, Which} = O| Rest], Opts) ->
    Which2 =
	case Which of
	    all ->
		all;
	    Suffixes when is_list(Suffixes) ->
		lists:usort(Suffixes);
	    {add, Suffixes} when is_list(Suffixes) ->
		lists:usort(Opts#zip_opts.uncompress ++ Suffixes);
	    {del, Suffixes} when is_list(Suffixes) ->
		lists:usort(Opts#zip_opts.uncompress -- Suffixes);
	    _ ->
		throw({bad_option, O})
	end,
    get_zip_opt(Rest, Opts#zip_opts{uncompress = Which2});
get_zip_opt([Unknown | _Rest], _Opts) ->
    throw({bad_option, Unknown}).


%% feedback funs
silent(_) -> ok.

verbose_unzip(FN) ->
    io:format("extracting: ~ts\n", [io_lib:write_string(FN)]).

verbose_zip(FN) ->
    io:format("adding: ~ts\n", [io_lib:write_string(FN)]).

%% file filter funs
all(_) -> true.

file_in_list({#zip_file{name = FileName},_}, List) ->
    lists:member(FileName, List);
file_in_list(_, _) ->
    false.

keep_old_file({#zip_file{name = FileName},_}) ->
    not (filelib:is_file(FileName) orelse filelib:is_dir(FileName));
keep_old_file(_) ->
    false.

%% fun combiner
fun_and_1(Fun1, Fun2) ->
    fun(A) -> Fun1(A) andalso Fun2(A) end.

%% getting options
get_zip_options(Files, Options) ->
    Suffixes = [".Z", ".zip", ".zoo", ".arc", ".lzh", ".arj"],
    Opts = #zip_opts{output = fun file_io/2,
		     input = get_zip_input({files, Files}),
		     open_opts = [raw, write],
		     comment = "",
		     feedback = fun silent/1,
		     cwd = "",
		     compress = all,
		     uncompress = Suffixes
		    },
    Opts1 = #zip_opts{comment = Comment} = get_zip_opt(Options, Opts),
    %% UTF-8 encode characters in the interval from 127 to 255.
    {Comment1, _} = encode_string(Comment),
    Opts1#zip_opts{comment = Comment1}.

get_unzip_options(F, Options) ->
    Opts = #unzip_opts{file_filter = fun all/1,
		       output = fun file_io/2,
		       input = get_input(F),
		       open_opts = [raw],
		       feedback = fun silent/1,
		       cwd = ""
		      },
    get_unzip_opt(Options, Opts).

get_openzip_options(Options) ->
    Opts = #openzip_opts{open_opts = [raw, read],
			 output = fun file_io/2,
			 cwd = ""},
    get_openzip_opt(Options, Opts).

get_input(F) when is_binary(F) ->
    fun binary_io/2;
get_input(F) when is_list(F) ->
    fun file_io/2;
get_input(_) ->
    throw(einval).

get_zip_input({F, B}) when is_binary(B), is_list(F) ->
    fun binary_io/2;
get_zip_input({F, B, #file_info{}}) when is_binary(B), is_list(F) ->
    fun binary_io/2;
get_zip_input({F, #file_info{}, B}) when is_binary(B), is_list(F) ->
    fun binary_io/2;
get_zip_input(F) when is_list(F) ->
    fun file_io/2;
get_zip_input({files, []}) ->
    fun binary_io/2;
get_zip_input({files, [File | _]}) ->
    get_zip_input(File);
get_zip_input(_) ->
    throw(einval).

get_list_dir_options(F, Options) ->
    Opts = #list_dir_opts{raw_iterator = fun raw_file_info_public/5,
			  input = get_input(F),
			  open_opts = [raw]},
    get_list_dir_opt(Options, Opts).

%% aliases for erl_tar compatibility
-doc #{ title => ?ERL_TAR_COMPATIBILITY }.
-doc #{ equiv => list_dir(Archive, []) }.
-spec(table(Archive) -> RetValue when
      Archive :: file:name() | binary(),
      RetValue :: {ok, CommentAndFiles} | {error, Reason :: term()},
      CommentAndFiles :: [zip_comment() | zip_file()]).

table(F) -> list_dir(F).

-doc #{ title => ?ERL_TAR_COMPATIBILITY }.
-doc #{ equiv => list_dir(Archive, Options) }.
-spec(table(Archive, Options) -> RetValue when
      Archive :: file:name() | binary(),
      RetValue :: {ok, CommentAndFiles} | {error, Reason :: term()},
      CommentAndFiles :: [zip_comment() | zip_file()],

      Options :: [Option],
      Option :: cooked).

table(F, O) -> list_dir(F, O).

-doc #{ title => ?ERL_TAR_COMPATIBILITY }.
-doc(#{ equiv => zip(Name, FileList)} ).
-spec(create(Name, FileList) -> RetValue when
      Name     :: file:name(),
      FileList :: [FileSpec],
      FileSpec :: file:name() | {file:name(), binary()}
                | {file:name(), binary(), file:file_info()},
      RetValue :: {ok, FileName :: filename()}
                | {ok, {FileName :: filename(), binary()}}
                | {error, Reason :: term()}).

create(F, Fs) -> zip(F, Fs).

-doc #{ title => ?ERL_TAR_COMPATIBILITY }.
-doc(#{ equiv => zip(Name, FileList, Options) }).
-spec(create(Name, FileList, Options) -> RetValue when
      Name     :: file:name(),
      FileList :: [FileSpec],
      FileSpec :: file:name() | {file:name(), binary()}
                | {file:name(), binary(), file:file_info()},
      Options  :: [Option],
      Option   :: create_option(),
      RetValue :: {ok, FileName :: filename()}
                | {ok, {FileName :: filename(), binary()}}
                | {error, Reason :: term()}).
create(F, Fs, O) -> zip(F, Fs, O).

-doc #{ title => ?ERL_TAR_COMPATIBILITY }.
-doc(#{ equiv => unzip(Archive)} ).
-spec(extract(Archive) -> RetValue when
      Archive :: file:name() | binary(),
      RetValue :: {ok, FileList}
                | {ok, FileBinList}
                | {error, Reason :: term()}
                | {error, {Name :: file:name(), Reason :: term()}},
      FileList :: [file:name()],
      FileBinList :: [{file:name(),binary()}]).

extract(F) -> unzip(F).

-doc #{ title => ?ERL_TAR_COMPATIBILITY }.
-doc(#{ equiv => unzip(Archive, Options) }).
-spec(extract(Archive, Options) -> RetValue when
      Archive :: file:name() | binary(),
      Options :: [Option],
      Option  :: {file_list, FileList}
               | keep_old_files | verbose | memory |
                 {file_filter, FileFilter} | {cwd, CWD},
      FileList :: [file:name()],
      FileBinList :: [{file:name(),binary()}],
      FileFilter :: fun((ZipFile) -> boolean()),
      CWD :: file:filename(),
      ZipFile :: zip_file(),
      RetValue :: {ok, FileList}
                | {ok, FileBinList}
                | {error, Reason :: term()}
                | {error, {Name :: file:name(), Reason :: term()}}).

extract(F, O) -> unzip(F, O).


%% put the central directory, at the end of the zip archive
put_central_dir(LHS, Pos, Out0,
		#zip_opts{output = Output, comment = Comment}) ->
    {Out1, Sz} = put_cd_files_loop(LHS, Output, Out0, 0),
    put_eocd(length(LHS), Pos, Sz, Comment, Output, Out1).

put_cd_files_loop([], _Output, Out, Sz) ->
    {Out, Sz};
put_cd_files_loop([{LH, Name, Pos} | LHRest], Output, Out0, Sz0) ->
    CDFH = cd_file_header_from_lh_and_pos(LH, Pos),
    BCDFH = cd_file_header_to_bin(CDFH),
    B = [<<?CENTRAL_FILE_MAGIC:32/little>>, BCDFH, Name],
    Out1 = Output({write, B}, Out0),
    Sz1 = Sz0 + ?CENTRAL_FILE_HEADER_SZ +
	LH#local_file_header.file_name_length,
    put_cd_files_loop(LHRest, Output, Out1, Sz1).

%% put end marker of central directory, the last record in the archive
put_eocd(N, Pos, Sz, Comment, Output, Out0) ->
    %% BComment = list_to_binary(Comment),
    CommentSz = length(Comment), % size(BComment),
    EOCD = #eocd{disk_num = 0,
		 start_disk_num = 0,
		 entries_on_disk = N,
		 entries = N,
		 size = Sz,
		 offset = Pos,
		 zip_comment_length = CommentSz},
    BEOCD = eocd_to_bin(EOCD),
    B = [<<?END_OF_CENTRAL_DIR_MAGIC:32/little>>, BEOCD, Comment], % BComment],
    Output({write, B}, Out0).

get_filename({Name, _}, Type) ->
    get_filename(Name, Type);
get_filename({Name, _, _}, Type) ->
    get_filename(Name, Type);
get_filename(Name, regular) ->
    Name;
get_filename(Name, directory) ->
    %% Ensure trailing slash
    case lists:reverse(Name) of
	[$/ | _Rev] -> Name;
	Rev         -> lists:reverse([$/ | Rev])
    end.

add_cwd(_CWD, {_Name, _} = F) -> F;
add_cwd("", F) -> F;
add_cwd(CWD, F) -> filename:join(CWD, F).

%% already compressed data should be stored as is in archive,
%% a simple name-match is used to check for this
%% files smaller than 10 bytes are also stored, not compressed
get_comp_method(_, N, _, _) when is_integer(N), N < 10 ->
    ?STORED;
get_comp_method(_, _, _, directory) ->
    ?STORED;
get_comp_method(F, _, #zip_opts{compress = Compress, uncompress = Uncompress}, _) ->
    Ext = filename:extension(F),
    Test = fun(Which) -> (Which =:= all) orelse lists:member(Ext, Which) end,
    case Test(Compress) andalso not Test(Uncompress) of
	true  -> ?DEFLATED;
	false -> ?STORED
    end.

put_z_files([], _Z, Out, Pos, _Opts, Acc) ->
    {Out, lists:reverse(Acc, []), Pos};
put_z_files([F | Rest], Z, Out0, Pos0,
	    #zip_opts{input = Input, output = Output, open_opts = OpO,
		      feedback = FB, cwd = CWD} = Opts, Acc) ->
    In0 = [],
    F1 = add_cwd(CWD, F),
    FileInfo = Input({file_info, F1}, In0),
    Type = FileInfo#file_info.type,
    UncompSize =
	case Type of
	    regular -> FileInfo#file_info.size;
	    directory -> 0
	end,
    FileName0 = get_filename(F, Type),
    %% UTF-8 encode characters in the interval from 127 to 255.
    {FileName, GPFlag} = encode_string(FileName0),
    CompMethod = get_comp_method(FileName, UncompSize, Opts, Type),
    LH = local_file_header_from_info_method_name(FileInfo, UncompSize, CompMethod, FileName, GPFlag),
    BLH = local_file_header_to_bin(LH),
    B = [<<?LOCAL_FILE_MAGIC:32/little>>, BLH],
    Out1 = Output({write, B}, Out0),
    Out2 = Output({write, FileName}, Out1),
    {Out3, CompSize, CRC} = put_z_file(CompMethod, UncompSize, Out2, F1,
				       0, Input, Output, OpO, Z, Type),
    FB(FileName0),
    Patch = <<CRC:32/little, CompSize:32/little>>,
    Out4 = Output({pwrite, Pos0 + ?LOCAL_FILE_HEADER_CRC32_OFFSET, Patch}, Out3),
    Out5 = Output({seek, eof, 0}, Out4),
    Pos1 = Pos0 + ?LOCAL_FILE_HEADER_SZ	+ LH#local_file_header.file_name_length,
    Pos2 = Pos1 + CompSize,
    LH2 = LH#local_file_header{comp_size = CompSize, crc32 = CRC},
    ThisAcc = [{LH2, FileName, Pos0}],
    {Out6, SubAcc, Pos3} =
	case Type of
	    regular ->
		{Out5, ThisAcc, Pos2};
	    directory ->
		Files = Input({list_dir, F1}, []),
		RevFiles = reverse_join_files(F, Files, []),
		put_z_files(RevFiles, Z, Out5, Pos2, Opts, ThisAcc)
	end,
    Acc2 = lists:reverse(SubAcc) ++ Acc,
    put_z_files(Rest, Z, Out6, Pos3, Opts, Acc2).

reverse_join_files(Dir, [File | Files], Acc) ->
    reverse_join_files(Dir, Files, [filename:join([Dir, File]) | Acc]);
reverse_join_files(_Dir, [], Acc) ->
    Acc.

%% flag for zlib
-define(MAX_WBITS, 15).

%% compress a file
put_z_file(_Method, Sz, Out, _F, Pos, _Input, _Output, _OpO, _Z, directory) ->
    {Out, Pos + Sz, 0};
put_z_file(_Method, 0, Out, _F, Pos, _Input, _Output, _OpO, _Z, regular) ->
    {Out, Pos, 0};
put_z_file(?STORED, UncompSize, Out0, F, Pos0, Input, Output, OpO, _Z, regular) ->
    In0 = [],
    In1 = Input({open, F, OpO -- [write]}, In0),
    {Data, In2} = Input({read, UncompSize}, In1),
    Out1 = Output({write, Data}, Out0),
    CRC = erlang:crc32(Data),
    Input(close, In2),
    {Out1, Pos0+erlang:iolist_size(Data), CRC};
put_z_file(?DEFLATED, UncompSize, Out0, F, Pos0, Input, Output, OpO, Z, regular) ->
    In0 = [],
    In1 = Input({open, F, OpO -- [write]}, In0),
    ok = zlib:deflateInit(Z, default, deflated, -?MAX_WBITS, 8, default),
    CRC0 = 0,
    {Out1, Pos1, CRC} =
        put_z_data_loop(UncompSize, In1, Out0, Pos0, Input, Output, CRC0, Z),
    ok = zlib:deflateEnd(Z),
    Input(close, In1),
    {Out1, Pos1, CRC}.

%%  zlib is finished with the last chunk compressed
get_sync(N, N) -> finish;
get_sync(_, _) -> full.

%% compress data
put_z_data_loop(0, _In, Out, Pos, _Input, _Output, CRC0, _Z) ->
    {Out, Pos, CRC0};
put_z_data_loop(UncompSize, In0, Out0, Pos0, Input, Output, CRC0, Z) ->
    N = erlang:min(?WRITE_BLOCK_SIZE, UncompSize),
    case Input({read, N}, In0) of
        {eof, _In1} ->
            {Out0, Pos0};
        {Uncompressed, In1} ->
            CRC1 = erlang:crc32(CRC0, Uncompressed),
            Compressed = zlib:deflate(Z, Uncompressed, get_sync(N, UncompSize)),
            Sz = erlang:iolist_size(Compressed),
            Out1 = Output({write, Compressed}, Out0),
            put_z_data_loop(UncompSize - N, In1, Out1, Pos0 + Sz,
                Input, Output, CRC1, Z)
    end.

%% raw iterators over central dir

%% name only
raw_name_only(CD, FileName, _FileComment, _BExtraField, Acc)
  when is_record(CD, cd_file_header) ->
    [FileName | Acc];
raw_name_only(EOCD, _, _Comment, _, Acc) when is_record(EOCD, eocd) ->
    Acc.

%% for printing directory (t/1)
raw_short_print_info_etc(CD, FileName, _FileComment, _BExtraField, Acc)
  when is_record(CD, cd_file_header) ->
    print_file_name(FileName),
    Acc;
raw_short_print_info_etc(EOCD, X, Comment, Y, Acc) when is_record(EOCD, eocd) ->
    raw_long_print_info_etc(EOCD, X, Comment, Y, Acc).

print_file_name(FileName) ->
    io:format("~ts\n", [FileName]).


%% for printing directory (tt/1)
raw_long_print_info_etc(#cd_file_header{comp_size = CompSize,
					uncomp_size = UncompSize,
					last_mod_date = LMDate,
					last_mod_time = LMTime},
			FileName, FileComment, _BExtraField, Acc) ->
    MTime = dos_date_time_to_datetime(LMDate, LMTime),
    print_header(CompSize, MTime, UncompSize, FileName, FileComment),
    Acc;
raw_long_print_info_etc(EOCD, _, Comment, _, Acc) when is_record(EOCD, eocd) ->
    print_comment(Comment),
    Acc.

print_header(CompSize, MTime, UncompSize, FileName, FileComment) ->
    io:format("~8w ~s ~8w ~2w% ~ts ~ts\n",
	      [CompSize, time_to_string(MTime), UncompSize,
	       get_percent(CompSize, UncompSize), FileName, FileComment]).

print_comment("") ->
    ok;
print_comment(Comment) ->
    io:format("Archive comment: ~ts\n", [Comment]).

get_percent(_, 0) -> 100;
get_percent(CompSize, Size) -> round(CompSize * 100 / Size).

%% time formatting ("borrowed" from erl_tar.erl)
time_to_string({{Y, Mon, Day}, {H, Min, _}}) ->
    io_lib:format("~s ~2w ~s:~s ~w",
		  [month(Mon), Day, two_d(H), two_d(Min), Y]).

two_d(N) ->
    tl(integer_to_list(N + 100)).

month(1) -> "Jan";
month(2) -> "Feb";
month(3) -> "Mar";
month(4) -> "Apr";
month(5) -> "May";
month(6) -> "Jun";
month(7) -> "Jul";
month(8) -> "Aug";
month(9) -> "Sep";
month(10) -> "Oct";
month(11) -> "Nov";
month(12) -> "Dec".

%% zip header functions
cd_file_header_from_lh_and_pos(LH, Pos) ->
    #local_file_header{version_needed = VersionNeeded,
		       gp_flag = GPFlag,
		       comp_method = CompMethod,
		       last_mod_time = LastModTime,
		       last_mod_date = LastModDate,
		       crc32 = CRC32,
		       comp_size = CompSize,
		       uncomp_size = UncompSize,
		       file_name_length = FileNameLength,
		       extra_field_length = ExtraFieldLength,
                       type = Type} = LH,
    #cd_file_header{version_made_by = ?VERSION_MADE_BY,
		    version_needed = VersionNeeded,
		    gp_flag = GPFlag,
		    comp_method = CompMethod,
		    last_mod_time = LastModTime,
		    last_mod_date = LastModDate,
		    crc32 = CRC32,
		    comp_size = CompSize,
		    uncomp_size = UncompSize,
		    file_name_length = FileNameLength,
		    extra_field_length = ExtraFieldLength,
		    file_comment_length = 0, % FileCommentLength,
		    disk_num_start = 0, % DiskNumStart,
		    internal_attr = 0, % InternalAttr,
		    external_attr = % ExternalAttr
                        case Type of
                            regular -> ?CENTRAL_REGULAR_FILE_EXT_ATTRIBUTES;
                            directory -> ?CENTRAL_DIRECTORY_FILE_EXT_ATTRIBUTES
                        end,
		    local_header_offset = Pos}.

cd_file_header_to_bin(
  #cd_file_header{version_made_by = VersionMadeBy,
		  version_needed = VersionNeeded,
		  gp_flag = GPFlag,
		  comp_method = CompMethod,
		  last_mod_time = LastModTime,
		  last_mod_date = LastModDate,
		  crc32 = CRC32,
		  comp_size = CompSize,
		  uncomp_size = UncompSize,
		  file_name_length = FileNameLength,
		  extra_field_length = ExtraFieldLength,
		  file_comment_length = FileCommentLength,
		  disk_num_start = DiskNumStart,
		  internal_attr = InternalAttr,
		  external_attr = ExternalAttr,
		  local_header_offset = LocalHeaderOffset}) ->
    <<VersionMadeBy:16/little,
     VersionNeeded:16/little,
     GPFlag:16/little,
     CompMethod:16/little,
     LastModTime:16/little,
     LastModDate:16/little,
     CRC32:32/little,
     CompSize:32/little,
     UncompSize:32/little,
     FileNameLength:16/little,
     ExtraFieldLength:16/little,
     FileCommentLength:16/little,
     DiskNumStart:16/little,
     InternalAttr:16/little,
     ExternalAttr:32/little,
     LocalHeaderOffset:32/little>>.

local_file_header_to_bin(
  #local_file_header{version_needed = VersionNeeded,
		     gp_flag = GPFlag,
		     comp_method = CompMethod,
		     last_mod_time = LastModTime,
		     last_mod_date = LastModDate,
		     crc32 = CRC32,
		     comp_size = CompSize,
		     uncomp_size = UncompSize,
		     file_name_length = FileNameLength,
		     extra_field_length = ExtraFieldLength}) ->
    <<VersionNeeded:16/little,
     GPFlag:16/little,
     CompMethod:16/little,
     LastModTime:16/little,
     LastModDate:16/little,
     CRC32:32/little,
     CompSize:32/little,
     UncompSize:32/little,
     FileNameLength:16/little,
     ExtraFieldLength:16/little>>.

eocd_to_bin(#eocd{disk_num = DiskNum,
	   start_disk_num = StartDiskNum,
	   entries_on_disk = EntriesOnDisk,
	   entries = Entries,
	   size = Size,
	   offset = Offset,
	   zip_comment_length = ZipCommentLength}) ->
    <<DiskNum:16/little,
     StartDiskNum:16/little,
     EntriesOnDisk:16/little,
     Entries:16/little,
     Size:32/little,
     Offset:32/little,
     ZipCommentLength:16/little>>.

%% put together a local file header
local_file_header_from_info_method_name(#file_info{mtime = MTime, type = Type},
					UncompSize,
					CompMethod, Name, GPFlag) ->
    {ModDate, ModTime} = dos_date_time_from_datetime(MTime),
    #local_file_header{version_needed = 20,
		       gp_flag = GPFlag,
		       comp_method = CompMethod,
		       last_mod_time = ModTime,
		       last_mod_date = ModDate,
		       crc32 = -1,
		       comp_size = -1,
		       uncomp_size = UncompSize,
		       file_name_length = length(Name),
		       extra_field_length = 0,
                       type = Type}.

server_init(Parent) ->
    %% we want to know if our parent dies
    process_flag(trap_exit, true),
    server_loop(Parent, not_open).

%% small, simple, stupid zip-archive server
server_loop(Parent, OpenZip) ->
    receive
	{From, {open, Archive, Options}} ->
	    case openzip_open(Archive, Options) of
		{ok, NewOpenZip} ->
		    From ! {self(), {ok, self()}},
		    server_loop(Parent, NewOpenZip);
		Error ->
		    From ! {self(), Error}
	    end;
	{From, close} ->
	    From ! {self(), openzip_close(OpenZip)};
	{From, get} ->
	    From ! {self(), openzip_get(OpenZip)},
	    server_loop(Parent, OpenZip);
	{From, {get, FileName}} ->
	    From ! {self(), openzip_get(FileName, OpenZip)},
	    server_loop(Parent, OpenZip);
	{From, {get_crc32, FileName}} ->
	    From ! {self(), openzip_get_crc32(FileName, OpenZip)},
	    server_loop(Parent, OpenZip);
	{From, list_dir} ->
	    From ! {self(), openzip_list_dir(OpenZip)},
	    server_loop(Parent, OpenZip);
	{From, {list_dir, Opts}} ->
	    From ! {self(), openzip_list_dir(OpenZip, Opts)},
	    server_loop(Parent, OpenZip);
	{From, get_state} ->
	    From ! {self(), OpenZip},
	    server_loop(Parent, OpenZip);
        {'EXIT', Parent, Reason} ->
            _ = openzip_close(OpenZip),
            exit({parent_died, Reason});
	_ ->
	    {error, bad_msg}
    end.

-doc(#{equiv => zip_open/2}).
-spec(zip_open(Archive) -> {ok, ZipHandle} | {error, Reason} when
      Archive :: file:name() | binary(),
      ZipHandle :: handle(),
      Reason :: term()).

zip_open(Archive) -> zip_open(Archive, []).

-doc """
Opens a zip archive, and reads and saves its directory. This means that later
reading files from the archive is faster than unzipping files one at a time with
[`unzip/1,2`](`unzip/1`).

The archive must be closed with `zip_close/1`.

The `ZipHandle` is closed if the process that originally opened the archive
dies.
""".
-spec(zip_open(Archive, Options) -> {ok, ZipHandle} | {error, Reason} when
      Archive :: file:name() | binary(),
      ZipHandle :: handle(),
      Options :: [Option],
      Option :: cooked | memory | {cwd, CWD :: file:filename()},
      Reason :: term()).

zip_open(Archive, Options) ->
    Self = self(),
    Pid = spawn_link(fun() -> server_init(Self) end),
    request(Self, Pid, {open, Archive, Options}).

-doc(#{equiv => zip_get/2}).
-spec(zip_get(ZipHandle) -> {ok, [Result]} | {error, Reason} when
      ZipHandle :: handle(),
      Result :: file:name() | {file:name(), binary()},
      Reason :: term()).

zip_get(Pid) when is_pid(Pid) ->
    request(self(), Pid, get).

-doc """
Closes a zip archive, previously opened with [`zip_open/1,2`](`zip_open/1`). All
resources are closed, and the handle is not to be used after closing.
""".
-spec(zip_close(ZipHandle) -> ok | {error, einval} when
      ZipHandle :: handle()).

zip_close(Pid) when is_pid(Pid) ->
    request(self(), Pid, close).

-doc """
Extracts one or all files from an open archive.

The files are unzipped to memory or to file, depending on the options specified
to function [`zip_open/1,2`](`zip_open/1`) when opening the archive.
""".
-spec(zip_get(FileName, ZipHandle) -> {ok, Result} | {error, Reason} when
      FileName :: file:name(),
      ZipHandle :: handle(),
      Result :: file:name() | {file:name(), binary()},
      Reason :: term()).

zip_get(FileName, Pid) when is_pid(Pid) ->
    request(self(), Pid, {get, FileName}).

-doc "Extracts one crc32 checksum from an open archive.".
-doc(#{since => <<"OTP 26.0">>}).
-spec(zip_get_crc32(FileName, ZipHandle) -> {ok, CRC} | {error, Reason} when
      FileName :: file:name(),
      ZipHandle :: handle(),
      CRC :: non_neg_integer(),
      Reason :: term()).

zip_get_crc32(FileName, Pid) when is_pid(Pid) ->
    request(self(), Pid, {get_crc32, FileName}).

-doc """
Returns the file list of an open zip archive. The first returned element is the
zip archive comment.
""".
-spec(zip_list_dir(ZipHandle) -> {ok, Result} | {error, Reason} when
      Result :: [zip_comment() | zip_file()],
      ZipHandle :: handle(),
      Reason :: term()).

zip_list_dir(Pid) when is_pid(Pid) ->
    request(self(), Pid, list_dir).

-doc false.
zip_list_dir(Pid, Opts) when is_pid(Pid) ->
    request(self(), Pid, {list_dir, Opts}).

-doc false.
zip_get_state(Pid) when is_pid(Pid) ->
    request(self(), Pid, get_state).

request(Self, Pid, Req) ->
    Pid ! {Self, Req},
    receive
	{Pid, R} -> R
    end.

-doc false.
zip_t(Pid) when is_pid(Pid) ->
    Openzip = request(self(), Pid, get_state),
    openzip_t(Openzip).

-doc false.
zip_tt(Pid) when is_pid(Pid) ->
    Openzip = request(self(), Pid, get_state),
    openzip_tt(Openzip).

-doc false.
openzip_tt(#openzip{zip_comment = ZipComment, files = Files}) ->
    print_comment(ZipComment),
    lists_foreach(fun({#zip_file{comp_size = CompSize,
				name = FileName,
				comment = FileComment,
				info = FI},_}) ->
			  #file_info{size = UncompSize, mtime = MTime} = FI,
			  print_header(CompSize, MTime, UncompSize,
				       FileName, FileComment)
		  end, Files),
    ok.

-doc false.
openzip_t(#openzip{zip_comment = ZipComment, files = Files}) ->
    print_comment(ZipComment),
    lists_foreach(fun({#zip_file{name = FileName},_}) ->
			  print_file_name(FileName)
		  end, Files),
    ok.

lists_foreach(_, []) ->
    ok;
lists_foreach(F, [Hd|Tl]) ->
    F(Hd),
    lists_foreach(F, Tl).

%% option utils
get_openzip_opt([], Opts) ->
    Opts;
get_openzip_opt([cooked | Rest], #openzip_opts{open_opts = OO} = Opts) ->
    get_openzip_opt(Rest, Opts#openzip_opts{open_opts = OO -- [raw]});
get_openzip_opt([memory | Rest], Opts) ->
    get_openzip_opt(Rest, Opts#openzip_opts{output = fun binary_io/2});
get_openzip_opt([{cwd, CWD} | Rest], Opts) ->
    get_openzip_opt(Rest, Opts#openzip_opts{cwd = CWD});
get_openzip_opt([Unknown | _Rest], _Opts) ->
    throw({bad_option, Unknown}).

%% get the central directory from the archive
get_central_dir(In0, RawIterator, Input) ->
    {B, In1} = get_end_of_central_dir(In0, ?END_OF_CENTRAL_DIR_SZ, Input),
    {EOCD, BComment} = eocd_and_comment_from_bin(B),
    In2 = Input({seek, bof, EOCD#eocd.offset}, In1),
    N = EOCD#eocd.entries,
    Acc0 = [],
    %% There is no encoding flag for the archive comment.
    Comment = heuristic_to_string(BComment),
    Out0 = RawIterator(EOCD, "", Comment, <<>>, Acc0),
    get_cd_loop(N, In2, RawIterator, Input, Out0).

get_cd_loop(0, In, _RawIterator, _Input, Acc) ->
    {lists:reverse(Acc), In};
get_cd_loop(N, In0, RawIterator, Input, Acc0) ->
    {B, In1} = Input({read, ?CENTRAL_FILE_HEADER_SZ}, In0),
    BCD = case B of
	      <<?CENTRAL_FILE_MAGIC:32/little, XBCD/binary>> -> XBCD;
	      _ -> throw(bad_central_directory)
	  end,
    CD = cd_file_header_from_bin(BCD),
    FileNameLen = CD#cd_file_header.file_name_length,
    ExtraLen = CD#cd_file_header.extra_field_length,
    CommentLen = CD#cd_file_header.file_comment_length,
    ToRead = FileNameLen + ExtraLen + CommentLen,
    GPFlag = CD#cd_file_header.gp_flag,
    {B2, In2} = Input({read, ToRead}, In1),
    {FileName, Comment, BExtra} =
	get_name_extra_comment(B2, FileNameLen, ExtraLen, CommentLen, GPFlag),
    Acc1 = RawIterator(CD, FileName, Comment, BExtra, Acc0),
    get_cd_loop(N-1, In2, RawIterator, Input, Acc1).

get_name_extra_comment(B, FileNameLen, ExtraLen, CommentLen, GPFlag) ->
    try
        <<BFileName:FileNameLen/binary,
          BExtra:ExtraLen/binary,
          BComment:CommentLen/binary>> = B,
        {binary_to_chars(BFileName, GPFlag),
         %% Appendix D says: "If general purpose bit 11 is unset, the
         %% file name and comment should conform to the original ZIP
         %% character encoding." However, it seems that at least Linux
         %% zip(1) encodes the comment without setting bit 11 if the
         %% filename is 7-bit ASCII. If bit 11 is set,
         %% binary_to_chars/1 could (should?) be called (it can fail),
         %% but the choice is to employ heuristics in this case too
         %% (it does not fail).
         heuristic_to_string(BComment),
         BExtra}
    catch
        _:_ ->
            throw(bad_central_directory)
    end.

%% get end record, containing the offset to the central directory
%% the end record is always at the end of the file BUT alas it is
%% of variable size (yes that's dumb!)
get_end_of_central_dir(_In, Sz, _Input) when Sz > 16#ffff ->
    throw(bad_eocd);
get_end_of_central_dir(In0, Sz, Input) ->
    In1 = Input({seek, eof, -Sz}, In0),
    {B, In2} = Input({read, Sz}, In1),
    case find_eocd_header(B) of
	none ->
	    get_end_of_central_dir(In2, Sz+Sz, Input);
	Header ->
	    {Header, In2}
    end.

%% find the end record by matching for it
find_eocd_header(<<?END_OF_CENTRAL_DIR_MAGIC:32/little, Rest/binary>>) ->
    Rest;
find_eocd_header(<<_:8, Rest/binary>>)
  when byte_size(Rest) > ?END_OF_CENTRAL_DIR_SZ-4 ->
    find_eocd_header(Rest);
find_eocd_header(_) ->
    none.

%% from a central directory record, filter and accumulate what we need

%% with zip_file_extra
raw_file_info_etc(CD, FileName, FileComment, BExtraField, Acc)
  when is_record(CD, cd_file_header) ->
    #cd_file_header{comp_size = CompSize,
		    local_header_offset = Offset,
		    crc32 = CRC} = CD,
    FileInfo = cd_file_header_to_file_info(FileName, CD, BExtraField),
    [{#zip_file{name = FileName, info = FileInfo, comment = FileComment,
		offset = Offset, comp_size = CompSize}, #zip_file_extra{crc32 = CRC}} | Acc];
raw_file_info_etc(EOCD, _, Comment, _, Acc) when is_record(EOCD, eocd) ->
    [#zip_comment{comment = Comment} | Acc].

%% without zip_file_extra
raw_file_info_public(CD, FileName, FileComment, BExtraField, Acc0) ->
    [H1|T] = raw_file_info_etc(CD,FileName,FileComment,BExtraField,Acc0),
    H2 = case H1 of
	     {ZF,Extra} when is_record(Extra,zip_file_extra) -> ZF;
	     Other -> Other
	 end,
    [H2|T].


%% make a file_info from a central directory header
cd_file_header_to_file_info(FileName,
			    #cd_file_header{uncomp_size = UncompSize,
					    last_mod_time = ModTime,
					    last_mod_date = ModDate},
			    ExtraField) ->
    T = dos_date_time_to_datetime(ModDate, ModTime),
    Type =
	case lists:last(FileName) of
	    $/ -> directory;
	    _  -> regular
	end,
    FI = #file_info{size = UncompSize,
		    type = Type,
		    access = read_write,
		    atime = T,
		    mtime = T,
		    ctime = T,
		    mode = 8#066,
		    links = 1,
		    major_device = 0,
		    minor_device = 0,
		    inode = 0,
		    uid = 0,
		    gid = 0},
    add_extra_info(FI, ExtraField).

%% Currently, we ignore all the extra fields.
add_extra_info(FI, _) ->
    FI.



%% get all files using file list
%% (the offset list is already filtered on which file to get... isn't it?)
get_z_files([], _Z, _In, _Opts, Acc) ->
    lists:reverse(Acc);
get_z_files([#zip_comment{comment = _} | Rest], Z, In, Opts, Acc) ->
    get_z_files(Rest, Z, In, Opts, Acc);
get_z_files([{#zip_file{offset = Offset},_} = ZFile | Rest], Z, In0,
	    #unzip_opts{input = Input, output = Output, open_opts = OpO,
			file_filter = Filter, feedback = FB,
			cwd = CWD} = Opts, Acc0) ->
    case Filter(ZFile) of
	true ->
	    In1 = Input({seek, bof, Offset}, In0),
	    {In2, Acc1} =
		case get_z_file(In1, Z, Input, Output, OpO, FB,
				CWD, ZFile, Filter) of
		    {file, GZD, Inx} -> {Inx, [GZD | Acc0]};
		    {_, Inx}       -> {Inx, Acc0}
		end,
	    get_z_files(Rest, Z, In2, Opts, Acc1);
	_ ->
	    get_z_files(Rest, Z, In0, Opts, Acc0)
    end.

%% get a file from the archive, reading chunks
get_z_file(In0, Z, Input, Output, OpO, FB,
	   CWD, {ZipFile,Extra}, Filter) ->
    case Input({read, ?LOCAL_FILE_HEADER_SZ}, In0) of
	{eof, In1} ->
	    {eof, In1};
	%% Local File Header
	{<<?LOCAL_FILE_MAGIC:32/little, B/binary>>, In1} ->
	    LH = local_file_header_from_bin(B),
	    #local_file_header{gp_flag = GPFlag,
			       comp_method = CompMethod,
			       file_name_length = FileNameLen,
			       extra_field_length = ExtraLen} = LH,

	    {CompSize,CRC32} = case GPFlag band 8 =:= 8 of
				   true -> {ZipFile#zip_file.comp_size,
					    Extra#zip_file_extra.crc32};
				   false -> {LH#local_file_header.comp_size,
					     LH#local_file_header.crc32}
			       end,
	    {BFileN, In3} = Input({read, FileNameLen + ExtraLen}, In1),
	    {FileName, _} =
                get_file_name_extra(FileNameLen, ExtraLen, BFileN, GPFlag),
	    ReadAndWrite =
		case check_valid_location(CWD, FileName) of
		    {true,FileName1} ->
			true;
		    {false,FileName1} ->
			Filter({ZipFile#zip_file{name = FileName1},Extra})
		end,
	    case ReadAndWrite of
		true ->
		    case lists:last(FileName) of
			$/ ->
			    %% perhaps this should always be done?
			    Output({ensure_dir,FileName1},[]),
			    {dir, In3};
			_ ->
			    %% FileInfo = local_file_header_to_file_info(LH)
			    %%{Out, In4, CRC, UncompSize} =
			    {Out, In4, CRC, _UncompSize} =
				get_z_data(CompMethod, In3, FileName1,
					   CompSize, Input, Output, OpO, Z),
			    In5 = skip_z_data_descriptor(GPFlag, Input, In4),
			    %% TODO This should be fixed some day:
			    %% In5 = Input({set_file_info, FileName, 
			    %% FileInfo#file_info{size=UncompSize}}, In4),
			    FB(FileName),
			    CRC =:= CRC32 orelse throw({bad_crc, FileName}),
			    {file, Out, In5}
		    end;
		false ->
		    {ignore, In3}
	    end;
	_ ->
	    throw(bad_local_file_header)
    end.

%% make sure FileName doesn't have relative path that points over CWD
check_valid_location(CWD, FileName) ->
    %% check for directory traversal exploit
    case check_dir_level(filename:split(FileName), 0) of
	{FileOrDir,Level} when Level < 0 ->
	    CWD1 = if CWD == "" -> "./";
		      true      -> CWD
		   end,
	    error_logger:format("Illegal path: ~ts, extracting in ~ts~n",
				[add_cwd(CWD,FileName),CWD1]),
	    {false,add_cwd(CWD, FileOrDir)};
        _ ->
	    {true,add_cwd(CWD, FileName)}
    end.

check_dir_level([FileOrDir], Level) ->
    {FileOrDir,Level};
check_dir_level(["." | Parts], Level) ->
    check_dir_level(Parts, Level);
check_dir_level([".." | Parts], Level) ->
    check_dir_level(Parts, Level-1);
check_dir_level([_Dir | Parts], Level) ->
    check_dir_level(Parts, Level+1).

get_file_name_extra(FileNameLen, ExtraLen, B, GPFlag) ->
    try
        <<BFileName:FileNameLen/binary, BExtra:ExtraLen/binary>> = B,
        {binary_to_chars(BFileName, GPFlag), BExtra}
    catch
        _:_ ->
            throw(bad_file_header)
    end.

%% get compressed or stored data
get_z_data(?DEFLATED, In0, FileName, CompSize, Input, Output, OpO, Z) ->
    ok = zlib:inflateInit(Z, -?MAX_WBITS),
    Out0 = Output({open, FileName, [write | OpO]}, []),
    CRC0 = 0,
    {In1, Out1, UncompSize, CRC} = get_z_data_loop(CompSize, 0, In0, Out0, Input, Output, CRC0, Z),
    _ = ?CATCH(zlib:inflateEnd(Z)),
    Out2 = Output({close, FileName}, Out1),
    {Out2, In1, CRC, UncompSize};
get_z_data(?STORED, In0, FileName, CompSize, Input, Output, OpO, _Z) ->
    Out0 = Output({open, FileName, [write | OpO]}, []),
    CRC0 = 0,
    {In1, Out1, CRC} = copy_data_loop(CompSize, In0, Out0, Input, Output, CRC0),
    Out2 = Output({close, FileName}, Out1),
    {Out2, In1, CRC, CompSize};
get_z_data(_, _, _, _, _, _, _, _) ->
    throw(bad_file_header).

copy_data_loop(0, In, Out, _Input, _Output, CRC) ->
    {In, Out, CRC};
copy_data_loop(CompSize, In0, Out0, Input, Output, CRC0) ->
    N = erlang:min(?READ_BLOCK_SIZE, CompSize),
    case Input({read, N}, In0) of
        {eof, In1} ->
            {Out0, In1};
        {Uncompressed, In1} ->
            CRC1 = erlang:crc32(CRC0, Uncompressed),
            Out1 = Output({write, Uncompressed}, Out0),
            copy_data_loop(CompSize-N, In1, Out1, Input, Output, CRC1)
    end.

get_z_data_loop(0, UncompSize, In, Out, _Input, _Output, CRC0, _Z) ->
    {In, Out, UncompSize, CRC0};
get_z_data_loop(CompSize, UncompSize, In0, Out0, Input, Output, CRC0, Z) ->
    N = erlang:min(?READ_BLOCK_SIZE, CompSize),
    case Input({read, N}, In0) of
        {eof, In1} ->
            {Out0, In1};
        {Compressed, In1} ->
            Uncompressed = zlib:inflate(Z, Compressed),
            CRC1 = erlang:crc32(CRC0, Uncompressed),
            Out1 = Output({write, Uncompressed}, Out0),
            get_z_data_loop(CompSize-N, UncompSize + iolist_size(Uncompressed),
                In1, Out1, Input, Output, CRC1, Z)
    end.


%% skip data descriptor if any
skip_z_data_descriptor(GPFlag, Input, In0) when GPFlag band 8 =:= 8 ->
    Input({seek, cur, 12}, In0);
skip_z_data_descriptor(_GPFlag, _Input, In0) ->
    In0.

%% convert between erlang datetime and the MSDOS date and time
%% that's stored in the zip archive
%%    	 MSDOS Time  	           MSDOS Date
%% bit   0 - 4 	 5 - 10 11 - 15    16 - 20      21 - 24        25 - 31
%% value second  minute hour 	   day (1 - 31) month (1 - 12) years from 1980
dos_date_time_to_datetime(DosDate, DosTime) ->
    <<Hour:5, Min:6, Sec:5>> = <<DosTime:16>>,
    <<YearFrom1980:7, Month:4, Day:5>> = <<DosDate:16>>,
    {{YearFrom1980+1980, Month, Day},
     {Hour, Min, Sec}}.

dos_date_time_from_datetime(Seconds) when is_integer(Seconds) ->
    DateTime = calendar:now_to_datetime({0, Seconds, 0}),
    dos_date_time_from_datetime(DateTime);
dos_date_time_from_datetime({{Year, Month, Day}, {Hour, Min, Sec}}) ->
    YearFrom1980 = Year-1980,
    <<DosTime:16>> = <<Hour:5, Min:6, Sec:5>>,
    <<DosDate:16>> = <<YearFrom1980:7, Month:4, Day:5>>,
    {DosDate, DosTime}.

%% A pwrite-like function for iolists (used by memory-option)

pwrite_binary(B, Pos, Bin) when byte_size(B) =:= Pos ->
    append_bins(Bin, B);
pwrite_binary(B, Pos, Bin) ->
    erlang:iolist_to_binary(pwrite_iolist(B, Pos, Bin)).

append_bins([Bin|Bins], B) when is_binary(Bin) ->
    append_bins(Bins, <<B/binary, Bin/binary>>);
append_bins([List|Bins], B) when is_list(List) ->
    append_bins(Bins, append_bins(List, B));
append_bins(Bin, B) when is_binary(Bin) ->
    <<B/binary, Bin/binary>>;
append_bins([_|_]=List, B) ->
    <<B/binary, (iolist_to_binary(List))/binary>>;
append_bins([], B) ->
    B.

-dialyzer({no_improper_lists, pwrite_iolist/3}).

pwrite_iolist(B, Pos, Bin) ->
    {Left, Right} = split_binary(B, Pos),
    Sz = erlang:iolist_size(Bin),
    R = skip_bin(Right, Sz),
    [Left, Bin | R].

skip_bin(B, Pos) when is_binary(B) ->
    case B of
	<<_:Pos/binary, Bin/binary>> -> Bin;
	_ -> <<>>
    end.

binary_to_chars(B, GPFlag) ->
    ?SHOW_GP_BIT_11(B, GPFlag band ?GP_BIT_11),
    case GPFlag band ?GP_BIT_11 of
        0 ->
            binary_to_list(B);
        ?GP_BIT_11 ->
            case unicode:characters_to_list(B) of
                List when is_list(List) ->
                    List
            end
    end.

heuristic_to_string(B) when is_binary(B) ->
    case unicode:characters_to_binary(B) of
	B ->
            unicode:characters_to_list(B);
	_ ->
            binary_to_list(B)
    end.

encode_string(String) ->
    case lists:any(fun(C) -> C > 127 end, String) of
        true ->
            case unicode:characters_to_binary(String) of
                B when is_binary(B) ->
                    {binary_to_list(B), ?GP_BIT_11};
                _ ->
                    throw({bad_unicode, String})
            end;
        false ->
            {String, 0}
    end.

%% ZIP header manipulations
eocd_and_comment_from_bin(<<DiskNum:16/little,
			   StartDiskNum:16/little,
			   EntriesOnDisk:16/little,
			   Entries:16/little,
			   Size:32/little,
			   Offset:32/little,
			   ZipCommentLength:16/little,
			   Comment:ZipCommentLength/binary>>) ->
    {#eocd{disk_num = DiskNum,
	   start_disk_num = StartDiskNum,
	   entries_on_disk = EntriesOnDisk,
	   entries = Entries,
	   size = Size,
	   offset = Offset,
	   zip_comment_length = ZipCommentLength},
     Comment};
eocd_and_comment_from_bin(_) ->
    throw(bad_eocd).

cd_file_header_from_bin(<<VersionMadeBy:16/little,
			 VersionNeeded:16/little,
			 GPFlag:16/little,
			 CompMethod:16/little,
			 LastModTime:16/little,
			 LastModDate:16/little,
			 CRC32:32/little,
			 CompSize:32/little,
			 UncompSize:32/little,
			 FileNameLength:16/little,
			 ExtraFieldLength:16/little,
			 FileCommentLength:16/little,
			 DiskNumStart:16/little,
			 InternalAttr:16/little,
			 ExternalAttr:32/little,
			 LocalHeaderOffset:32/little>>) ->
    #cd_file_header{version_made_by = VersionMadeBy,
		    version_needed = VersionNeeded,
		    gp_flag = GPFlag,
		    comp_method = CompMethod,
		    last_mod_time = LastModTime,
		    last_mod_date = LastModDate,
		    crc32 = CRC32,
		    comp_size = CompSize,
		    uncomp_size = UncompSize,
		    file_name_length = FileNameLength,
		    extra_field_length = ExtraFieldLength,
		    file_comment_length = FileCommentLength,
		    disk_num_start = DiskNumStart,
		    internal_attr = InternalAttr,
		    external_attr = ExternalAttr,
		    local_header_offset = LocalHeaderOffset};
cd_file_header_from_bin(_) ->
    throw(bad_cd_file_header).

local_file_header_from_bin(<<VersionNeeded:16/little,
			    GPFlag:16/little,
			    CompMethod:16/little,
			    LastModTime:16/little,
			    LastModDate:16/little,
			    CRC32:32/little,
			    CompSize:32/little,
			    UncompSize:32/little,
			    FileNameLength:16/little,
			    ExtraFieldLength:16/little>>) ->
    #local_file_header{version_needed = VersionNeeded,
		       gp_flag = GPFlag,
		       comp_method = CompMethod,
		       last_mod_time = LastModTime,
		       last_mod_date = LastModDate,
		       crc32 = CRC32,
		       comp_size = CompSize,
		       uncomp_size = UncompSize,
		       file_name_length = FileNameLength,
		       extra_field_length = ExtraFieldLength};
local_file_header_from_bin(_) ->
    throw(bad_local_file_header).

%% make a file_info from a local directory header
%% local_file_header_to_file_info(
%%   #local_file_header{last_mod_time = ModTime,
%% 		     last_mod_date = ModDate,
%% 		     uncomp_size = UncompSize}) ->
%%     T = dos_date_time_to_datetime(ModDate, ModTime),
%%     FI = #file_info{size = UncompSize,
%% 		    type = regular,
%% 		    access = read_write,
%% 		    atime = T,
%% 		    mtime = T,
%% 		    ctime = T,
%% 		    mode = 8#066,
%% 		    links = 1,
%% 		    major_device = 0,
%% 		    minor_device = 0,
%% 		    inode = 0,
%% 		    uid = 0,
%% 		    gid = 0},
%%     FI.

%% io functions
binary_io({file_info, {_Filename, _B, #file_info{} = FI}}, _A) ->
    FI;
binary_io({file_info, {_Filename, #file_info{} = FI, _B}}, _A) ->
    FI;
binary_io({file_info, {_Filename, B}}, A) ->
    binary_io({file_info, B}, A);
binary_io({file_info, B}, _) ->
    {Type, Size} =
	if
	    is_binary(B) -> {regular, byte_size(B)};
	    B =:= directory -> {directory, 0}
	end,
    Now = calendar:local_time(),
    #file_info{size = Size, type = Type,
	       access = read_write, atime = Now,
	       mtime = Now, ctime = Now, mode = 0,
	       links = 1, major_device = 0,
	       minor_device = 0, inode = 0,
	       uid = 0, gid = 0};
binary_io({open, {_Filename, B, _FI}, _Opts}, _) when is_binary(B) ->
    {0, B};
binary_io({open, {_Filename, _FI, B}, _Opts}, _) when is_binary(B) ->
    {0, B};
binary_io({open, {_Filename, B}, _Opts}, _) when is_binary(B) ->
    {0, B};
binary_io({open, B, _Opts}, _) when is_binary(B) ->
    {0, B};
binary_io({open, Filename, _Opts}, _) when is_list(Filename) ->
    {0, <<>>};
binary_io({read, N}, {Pos, B}) when Pos >= byte_size(B) ->
    {eof, {Pos+N, B}};
binary_io({read, N}, {Pos, B}) when Pos + N > byte_size(B) ->
    <<_:Pos/binary, Read/binary>> = B,
    {Read, {byte_size(B), B}};
binary_io({pread, Pos, N}, {OldPos, B}) ->
    case B of
	<<_:Pos/binary, Read:N/binary, _Rest/binary>> ->
	    {Read, {Pos+N, B}};
	_ ->
	    {eof, {OldPos, B}}
    end;
binary_io({read, N}, {Pos, B}) ->
    <<_:Pos/binary, Read:N/binary, _/binary>> = B,
    {Read, {Pos+N, B}};
binary_io({seek, bof, Pos}, {_OldPos, B}) ->
    {Pos, B};
binary_io({seek, cur, Pos}, {OldPos, B}) ->
    {OldPos + Pos, B};
binary_io({seek, eof, Pos}, {_OldPos, B}) ->
    {byte_size(B) + Pos, B};
binary_io({pwrite, Pos, Data}, {OldPos, B}) ->
    {OldPos, pwrite_binary(B, Pos, Data)};
binary_io({write, Data}, {Pos, B}) ->
    {Pos + erlang:iolist_size(Data), pwrite_binary(B, Pos, Data)};
binary_io(close, {_Pos, B}) ->
    B;
binary_io({close, FN}, {_Pos, B}) ->
    {FN, B};
binary_io({list_dir, _F}, _B) ->
    [];
binary_io({set_file_info, _F, _FI}, B) ->
    B;
binary_io({ensure_dir, _Dir}, B) ->
    B.

file_io({file_info, F}, _) ->
    case file:read_file_info(F) of
	{ok, Info} -> Info;
	{error, E} -> throw(E)
    end;
file_io({open, FN, Opts}, _) ->
    case lists:member(write, Opts) of
	true -> ok = filelib:ensure_dir(FN);
	_ -> ok
    end,
    case file:open(FN, Opts++[binary]) of
	{ok, H} -> H;
	{error, E} -> throw(E)
    end;
file_io({read, N}, H) ->
    case file:read(H, N) of
	{ok, B} -> {B, H};
	eof -> {eof, H};
	{error, E} -> throw(E)
    end;
file_io({pread, Pos, N}, H) ->
    case file:pread(H, Pos, N) of
	{ok, B} -> {B, H};
	eof -> {eof, H};
	{error, E} -> throw(E)
    end;
file_io({seek, S, Pos}, H) ->
    case file:position(H, {S, Pos}) of
	{ok, _NewPos} -> H;
	{error, Error} -> throw(Error)
    end;
file_io({write, Data}, H) ->
    case file:write(H, Data) of
	ok -> H;
	{error, Error} -> throw(Error)
    end;
file_io({pwrite, Pos, Data}, H) ->
    case file:pwrite(H, Pos, Data) of
	ok -> H;
	{error, Error} -> throw(Error)
    end;
file_io({close, FN}, H) ->
    case file:close(H) of
	ok -> FN;
	{error, Error} -> throw(Error)
    end;
file_io(close, H) ->
    file_io({close, ok}, H);
file_io({list_dir, F}, _H) ->
    case file:list_dir(F) of
	{ok, Files} -> Files;
	{error, Error} -> throw(Error)
    end;
file_io({set_file_info, F, FI}, H) ->
    case file:write_file_info(F, FI) of
	ok -> H;
	{error, Error} -> throw(Error)
    end;
file_io({ensure_dir, Dir}, H) ->
    ok = filelib:ensure_dir(Dir),
    H.
