(********************************************************************)
(*                                                                  *)
(*  zip.s7i       Zip compression support library                   *)
(*  Copyright (C) 2009, 2016, 2017, 2020 - 2023  Thomas Mertes      *)
(*                                                                  *)
(*  This file is part of the Seed7 Runtime Library.                 *)
(*                                                                  *)
(*  The Seed7 Runtime Library is free software; you can             *)
(*  redistribute it and/or modify it under the terms of the GNU     *)
(*  Lesser General Public License as published by the Free Software *)
(*  Foundation; either version 2.1 of the License, or (at your      *)
(*  option) any later version.                                      *)
(*                                                                  *)
(*  The Seed7 Runtime Library is distributed in the hope that it    *)
(*  will be useful, but WITHOUT ANY WARRANTY; without even the      *)
(*  implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR *)
(*  PURPOSE.  See the GNU Lesser General Public License for more    *)
(*  details.                                                        *)
(*                                                                  *)
(*  You should have received a copy of the GNU Lesser General       *)
(*  Public License along with this program; if not, write to the    *)
(*  Free Software Foundation, Inc., 51 Franklin Street,             *)
(*  Fifth Floor, Boston, MA  02110-1301, USA.                       *)
(*                                                                  *)
(********************************************************************)


include "stdio.s7i";
include "inflate.s7i";
include "deflate.s7i";
include "unicode.s7i";
include "bytedata.s7i";
include "bin32.s7i";
include "time.s7i";
include "crc32.s7i";
include "filesys.s7i";
include "filebits.s7i";
include "fileutil.s7i";
include "archive_base.s7i";


const string: ZIP_CENTRAL_HEADER_SIGNATURE               is "PK\1;\2;";
const string: ZIP_LOCAL_HEADER_SIGNATURE                 is "PK\3;\4;";
const string: ZIP_END_OF_CENTRAL_DIRECTORY_SIGNATURE     is "PK\5;\6;";
const string: ZIP_DATA_DESCRIPTOR_SIGNATURE              is "PK\7;\8;";

const string: ZIP64_END_OF_CENTRAL_DIRECTORY_SIGNATURE   is "PK\6;\6;";
const string: ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE is "PK\6;\7;";

const integer: ZIP_CENTRAL_HEADER_FIXED_SIZE             is 46;
const integer: ZIP_LOCAL_HEADER_FIXED_SIZE               is 30;
const integer: ZIP_END_OF_CENTRAL_DIRECTORY_FIXED_SIZE   is 22;

const integer: ZIP64_END_OF_CENTRAL_DIRECTORY_FIXED_SIZE is 56;
const integer: ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE     is 20;

# Bits of the general_purpose_bit_flag:
const bin32: ZIP_HAS_DATA_DESCRIPTOR is bin32(16#0008);
const bin32: ZIP_FILE_NAME_IS_UTF8   is bin32(16#0800);

const integer: ZIP_HOST_SYSTEM_MS_DOS is 0;
const integer: ZIP_HOST_SYSTEM_UNIX   is 3;

const integer: DOS_EPOCH is 2#100001;  # 1980-01-01 in DOS 2 byte date encoding.


const func integer: rposOfMagic (inout file: inFile, in string: magic,
    in integer: minRecLen, in integer: maxRecLen) is func
  result
    var integer: posFound is 0;
  local
    var integer: pos is 0;
    var string: data is "";
    var integer: magicPos is 0;
  begin
    pos := length(inFile) - maxRecLen + 1;
    if pos <= 0 then
      pos := 1;
    end if;
    seek(inFile, pos);
    data := gets(inFile, maxRecLen);
    magicPos := rpos(data, magic);
    if magicPos <> 0 then
      posFound := pos + magicPos - 1;
    end if;
  end func;


const type: zipExtraFieldType is hash [integer] string;


const func zipExtraFieldType: getExtraFieldMap (in string: field) is func
  result
    var zipExtraFieldType: extraFieldMap is zipExtraFieldType.value;
  local
    var integer: pos is 1;
    var integer: id is 0;
    var integer: length is 0;
    var string: value is "";
  begin
    while pos <= length(field) - 3 do
      id     := bytes2Int(field[pos     fixLen 2], UNSIGNED, LE);
      length := bytes2Int(field[pos + 2 fixLen 2], UNSIGNED, LE);
      if length <> 0 then
        value := field[pos + 4 fixLen length];
      else
        value := "";
      end if;
      extraFieldMap @:= [id] value;
      pos +:= 4 + length;
    end while;
  end func;


const func string: extraFieldFromMap (in zipExtraFieldType: extraFieldMap) is func
  result
    var string: field is "";
  local
    var integer: id is 0;
    var string: value is "";
  begin
    for id range sort(keys(extraFieldMap)) do
      value := extraFieldMap[id];
      field &:= bytes(id, UNSIGNED, LE, 2) & bytes(length(value), UNSIGNED, LE, 2) & value;
    end for;
  end func;


const proc: writeExtraField (in string: field) is func
  local
    var integer: pos is 1;
    var integer: id is 0;
    var integer: length is 0;
    var string: value is "";
  begin
    while pos <= length(field) - 3 do
      id     := bytes2Int(field[pos     fixLen 2], UNSIGNED, LE);
      length := bytes2Int(field[pos + 2 fixLen 2], UNSIGNED, LE);
      if length <> 0 then
        value := field[pos + 4 fixLen length];
      else
        value := "";
      end if;
      writeln("field: " <& id radix 16 lpad0 4 <& " " <& length <& " " <& literal(value));
      pos +:= 4 + length;
    end while;
  end func;


const type: local_file_header is new struct
    var string:  signature                  is "";        # 4 bytes ("PK\3;\4;")
    var integer: version_needed_to_extract  is 0;         # 2 bytes
    var bin32:   general_purpose_bit_flag   is bin32(0);  # 2 bytes
    var integer: compression_method         is 0;         # 2 bytes
    var integer: last_mod_file_time         is 0;         # 2 bytes
    var integer: last_mod_file_date         is DOS_EPOCH; # 2 bytes
    var bin32:   crc_32                     is bin32(0);  # 4 bytes
    var integer: compressed_size            is 0;         # 4 bytes
    var integer: uncompressed_size          is 0;         # 4 bytes
    #   integer: file_name_length           is 0;         # 2 bytes
    #   integer: extra_field_length         is 0;         # 2 bytes
    var string:  file_name                  is "";        # variable size
    var string:  extra_field                is "";        # variable size
    var string:  filePath is "";
    var zipExtraFieldType: extraFieldMap is zipExtraFieldType.value;
  end struct;


const proc: write (in local_file_header: header) is func
  begin
    writeln("signature: "                 rpad 45 <& literal(header.signature) lpad 16);
    writeln("version_needed_to_extract: " rpad 45 <& header.version_needed_to_extract lpad 16);
    writeln("general_purpose_bit_flag: "  rpad 45 <& header.general_purpose_bit_flag radix 2 lpad0 16);
    writeln("compression_method: "        rpad 45 <& header.compression_method lpad 16);
    writeln("last_mod_file_time: "        rpad 45 <& header.last_mod_file_time lpad 16);
    writeln("last_mod_file_date: "        rpad 45 <& header.last_mod_file_date lpad 16);
    writeln("crc_32: "                    rpad 45 <& header.crc_32 lpad 16);
    writeln("compressed_size: "           rpad 45 <& header.compressed_size lpad 16);
    writeln("uncompressed_size: "         rpad 45 <& header.uncompressed_size lpad 16);
    writeln("file_name_length: "          rpad 45 <& length(header.file_name) lpad 16);
    writeln("extra_field_length: "        rpad 45 <& length(header.extra_field) lpad 16);
    writeln("file_name: "                 rpad 45 <& literal(header.file_name) lpad 16);
    writeln("extra_field: "                       <& literal(header.extra_field) lpad 16);
    writeExtraField(header.extra_field);
  end func;


const proc: considerZip64ExtraField (inout local_file_header: header,
    in string: zip64ExtraField) is func
  local
    var integer: pos is 1;
  begin
    if header.uncompressed_size = 16#ffffffff and length(zip64ExtraField) >= pos + 7 then
      header.uncompressed_size := bytes2Int(zip64ExtraField[pos fixLen 8], UNSIGNED, LE);
      # writeln("uncompressed_size: " <& header.uncompressed_size);
      pos +:= 8;
    end if;
    if header.compressed_size = 16#ffffffff and length(zip64ExtraField) >= pos + 7 then
      header.compressed_size := bytes2Int(zip64ExtraField[pos fixLen 8], UNSIGNED, LE);
      # writeln("compressed_size: " <& header.compressed_size);
      pos +:= 8;
    end if;
  end func;


const func local_file_header: get_local_header (inout file: inFile) is func
  result
    var local_file_header: header is local_file_header.value;
  local
    var string: stri is "";
    var integer: file_name_length is 0;
    var integer: extra_field_length is 0;
  begin
    stri := gets(inFile, ZIP_LOCAL_HEADER_FIXED_SIZE);
    if length(stri) = ZIP_LOCAL_HEADER_FIXED_SIZE and
        stri[.. 4] = ZIP_LOCAL_HEADER_SIGNATURE then
      header.signature := ZIP_LOCAL_HEADER_SIGNATURE;
      header.version_needed_to_extract       := bytes2Int(stri[ 5 fixLen 2], UNSIGNED, LE);
      header.general_purpose_bit_flag  := bin32(bytes2Int(stri[ 7 fixLen 2], UNSIGNED, LE));
      header.compression_method              := bytes2Int(stri[ 9 fixLen 2], UNSIGNED, LE);
      header.last_mod_file_time              := bytes2Int(stri[11 fixLen 2], UNSIGNED, LE);
      header.last_mod_file_date              := bytes2Int(stri[13 fixLen 2], UNSIGNED, LE);
      header.crc_32                    := bin32(bytes2Int(stri[15 fixLen 4], UNSIGNED, LE));
      header.compressed_size                 := bytes2Int(stri[19 fixLen 4], UNSIGNED, LE);
      header.uncompressed_size               := bytes2Int(stri[23 fixLen 4], UNSIGNED, LE);
      file_name_length                       := bytes2Int(stri[27 fixLen 2], UNSIGNED, LE);
      extra_field_length                     := bytes2Int(stri[29 fixLen 2], UNSIGNED, LE);
      header.file_name                       := gets(inFile, file_name_length);
      header.extra_field                     := gets(inFile, extra_field_length);
      header.extraFieldMap := getExtraFieldMap(header.extra_field);
      if header.general_purpose_bit_flag & ZIP_FILE_NAME_IS_UTF8 <> bin32(0) then
        block
          header.filePath := fromUtf8(header.file_name);
        exception
          catch RANGE_ERROR:
            header.filePath := header.file_name;
        end block;
      else
        header.filePath := header.file_name;
      end if;
      if header.filePath <> "/" and endsWith(header.filePath, "/") then
        header.filePath := header.filePath[.. pred(length(header.filePath))];
      end if;
      if 16#0001 in header.extraFieldMap then
        considerZip64ExtraField(header, header.extraFieldMap[16#0001]);
      end if;
      # write(header);
    end if;
  end func;


const func string: str (in local_file_header: header) is func
  result
    var string: stri is "";
  begin
    stri := ZIP_LOCAL_HEADER_SIGNATURE &
            bytes(       header.version_needed_to_extract, UNSIGNED, LE, 2) &
            bytes(ord(   header.general_purpose_bit_flag), UNSIGNED, LE, 2) &
            bytes(       header.compression_method,        UNSIGNED, LE, 2) &
            bytes(       header.last_mod_file_time,        UNSIGNED, LE, 2) &
            bytes(       header.last_mod_file_date,        UNSIGNED, LE, 2) &
            bytes(ord(   header.crc_32),                   UNSIGNED, LE, 4) &
            bytes(       header.compressed_size,           UNSIGNED, LE, 4) &
            bytes(       header.uncompressed_size,         UNSIGNED, LE, 4) &
            bytes(length(header.file_name),                UNSIGNED, LE, 2) &
            bytes(length(header.extra_field),              UNSIGNED, LE, 2) &
            header.file_name &
            header.extra_field;
  end func;


const proc: writeHead (inout file: outFile, in local_file_header: header) is func
  begin
    write(outFile, str(header));
  end func;


const type: zip64_end_of_central_dir_locator is new struct
    var string:  signature                                        is "";  # 4 bytes ("PK\6;\7;")
    var integer: disk_number_with_zip64_end_of_central_directory  is 0;   # 4 bytes
    var integer: offset_of_zip64_end_of_central_directory         is 0;   # 8 bytes
    var integer: total_number_of_disks                            is 0;   # 4 bytes
  end struct;


const proc: write (in zip64_end_of_central_dir_locator: endOfCentDirLocator) is func
  begin
    writeln("signature: "                                       rpad 50 <& literal(endOfCentDirLocator.signature) lpad 16);
    writeln("disk_number_with_zip64_end_of_central_directory: " rpad 50 <& endOfCentDirLocator.disk_number_with_zip64_end_of_central_directory);
    writeln("offset_of_zip64_end_of_central_directory: "        rpad 50 <& endOfCentDirLocator.offset_of_zip64_end_of_central_directory);
    writeln("total_number_of_disks: "                           rpad 50 <& endOfCentDirLocator.total_number_of_disks);
  end func;


const func zip64_end_of_central_dir_locator: get_zip64_end_of_central_dir_locator (inout file: inFile) is func
  result
    var zip64_end_of_central_dir_locator: endOfCentDirLocator is zip64_end_of_central_dir_locator.value;
  local
    var string: stri is "";
  begin
    stri := gets(inFile, ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE);
    if length(stri) = ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE and
        stri[.. 4] = ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE then
      endOfCentDirLocator.signature := ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE;
      endOfCentDirLocator.disk_number_with_zip64_end_of_central_directory := bytes2Int(stri[ 5 fixLen 4], UNSIGNED, LE);
      endOfCentDirLocator.offset_of_zip64_end_of_central_directory        := bytes2Int(stri[ 9 fixLen 8], UNSIGNED, LE);
      endOfCentDirLocator.total_number_of_disks                           := bytes2Int(stri[17 fixLen 4], UNSIGNED, LE);
      # write(endOfCentDirLocator);
    end if;
  end func;


const type: zip64_end_of_central_directory is new struct
    var string:  signature                                    is "";  # 4 bytes ("PK\6;\6;")
    var integer: size_of_eocd64_minus_12                      is 0;   # 8 bytes
    var integer: version_made_by                              is 0;   # 2 bytes
    var integer: version_needed_to_extract                    is 0;   # 2 bytes
    var integer: number_of_this_disk                          is 0;   # 4 bytes
    var integer: disk_number_with_start_of_central_directory  is 0;   # 4 bytes
    var integer: entries_in_central_directory_on_this_disk    is 0;   # 8 bytes
    var integer: entries_in_central_directory                 is 0;   # 8 bytes
    var integer: size_of_central_directory                    is 0;   # 8 bytes
    var integer: offset_of_start_of_central_directory         is 0;   # 8 bytes
    var string:  comment                                      is "";  # variable size
  end struct;


const proc: write (in zip64_end_of_central_directory: endOfCentDir) is func
  begin
    writeln("signature: "                                   rpad 45 <& literal(endOfCentDir.signature) lpad 16);
    writeln("size_of_eocd64_minus_12: "                     rpad 45 <& endOfCentDir.size_of_eocd64_minus_12 lpad 16);
    writeln("version_made_by: "                             rpad 45 <& endOfCentDir.version_made_by lpad 16);
    writeln("version_needed_to_extract: "                   rpad 45 <& endOfCentDir.version_needed_to_extract lpad 16);
    writeln("number_of_this_disk: "                         rpad 45 <& endOfCentDir.number_of_this_disk lpad 16);
    writeln("disk_number_with_start_of_central_directory: " rpad 45 <& endOfCentDir.disk_number_with_start_of_central_directory lpad 16);
    writeln("entries_in_central_directory_on_this_disk: "   rpad 45 <& endOfCentDir.entries_in_central_directory_on_this_disk lpad 16);
    writeln("entries_in_central_directory: "                rpad 45 <& endOfCentDir.entries_in_central_directory lpad 16);
    writeln("size_of_central_directory: "                   rpad 45 <& endOfCentDir.size_of_central_directory lpad 16);
    writeln("offset_of_start_of_central_directory: "        rpad 45 <& endOfCentDir.offset_of_start_of_central_directory lpad 16);
    writeln("comment: "                                     rpad 45 <& literal(endOfCentDir.comment) lpad 16);
  end func;


const func zip64_end_of_central_directory: get_zip64_end_of_central_directory (inout file: inFile) is func
  result
    var zip64_end_of_central_directory: endOfCentDir is zip64_end_of_central_directory.value;
  local
    var string: stri is "";
    var integer: commentLength is 0;
  begin
    stri := gets(inFile, ZIP64_END_OF_CENTRAL_DIRECTORY_FIXED_SIZE);
    if length(stri) = ZIP64_END_OF_CENTRAL_DIRECTORY_FIXED_SIZE and
        stri[.. 4] = ZIP64_END_OF_CENTRAL_DIRECTORY_SIGNATURE then
      endOfCentDir.signature := ZIP64_END_OF_CENTRAL_DIRECTORY_SIGNATURE;
      endOfCentDir.size_of_eocd64_minus_12                     := bytes2Int(stri[ 5 fixLen 8], UNSIGNED, LE);
      endOfCentDir.version_made_by                             := bytes2Int(stri[13 fixLen 2], UNSIGNED, LE);
      endOfCentDir.version_needed_to_extract                   := bytes2Int(stri[15 fixLen 2], UNSIGNED, LE);
      endOfCentDir.number_of_this_disk                         := bytes2Int(stri[17 fixLen 4], UNSIGNED, LE);
      endOfCentDir.disk_number_with_start_of_central_directory := bytes2Int(stri[21 fixLen 4], UNSIGNED, LE);
      endOfCentDir.entries_in_central_directory_on_this_disk   := bytes2Int(stri[25 fixLen 8], UNSIGNED, LE);
      endOfCentDir.entries_in_central_directory                := bytes2Int(stri[33 fixLen 8], UNSIGNED, LE);
      endOfCentDir.size_of_central_directory                   := bytes2Int(stri[41 fixLen 8], UNSIGNED, LE);
      endOfCentDir.offset_of_start_of_central_directory        := bytes2Int(stri[49 fixLen 8], UNSIGNED, LE);
      commentLength := endOfCentDir.size_of_eocd64_minus_12 + 12 - ZIP64_END_OF_CENTRAL_DIRECTORY_FIXED_SIZE;
      endOfCentDir.comment                                     := gets(inFile, commentLength);
      # write(endOfCentDir);
    end if;
  end func;


const type: end_of_central_directory is new struct
    var string:  signature                                    is "";  # 4 bytes ("PK\5;\6;")
    var integer: number_of_this_disk                          is 0;   # 2 bytes
    var integer: disk_number_with_start_of_central_directory  is 0;   # 2 bytes
    var integer: entries_in_central_directory_on_this_disk    is 0;   # 2 bytes
    var integer: entries_in_central_directory                 is 0;   # 2 bytes
    var integer: size_of_central_directory                    is 0;   # 4 bytes
    var integer: offset_of_start_of_central_directory         is 0;   # 4 bytes
    #   integer: file_comment_length                          is 0;     2 bytes
    var string:  file_comment                                 is "";  # variable size
  end struct;


const proc: write (in end_of_central_directory: endOfCentDir) is func
  begin
    writeln("signature: "                                   rpad 45 <& literal(endOfCentDir.signature) lpad 16);
    writeln("number_of_this_disk: "                         rpad 45 <& endOfCentDir.number_of_this_disk lpad 16);
    writeln("disk_number_with_start_of_central_directory: " rpad 45 <& endOfCentDir.disk_number_with_start_of_central_directory lpad 16);
    writeln("entries_in_central_directory_on_this_disk: "   rpad 45 <& endOfCentDir.entries_in_central_directory_on_this_disk lpad 16);
    writeln("entries_in_central_directory: "                rpad 45 <& endOfCentDir.entries_in_central_directory lpad 16);
    writeln("size_of_central_directory: "                   rpad 45 <& endOfCentDir.size_of_central_directory lpad 16);
    writeln("offset_of_start_of_central_directory: "        rpad 45 <& endOfCentDir.offset_of_start_of_central_directory lpad 16);
    writeln("file_comment_length: "                         rpad 45 <& length(endOfCentDir.file_comment) lpad 16);
    writeln("file_comment: "                                rpad 45 <& literal(endOfCentDir.file_comment) lpad 16);
  end func;


const func end_of_central_directory: get_end_of_central_directory (inout file: inFile) is func
  result
    var end_of_central_directory: endOfCentDir is end_of_central_directory.value;
  local
    var string: stri is "";
    var integer: file_comment_length is 0;
  begin
    stri := gets(inFile, ZIP_END_OF_CENTRAL_DIRECTORY_FIXED_SIZE);
    if length(stri) = ZIP_END_OF_CENTRAL_DIRECTORY_FIXED_SIZE and
        stri[.. 4] = ZIP_END_OF_CENTRAL_DIRECTORY_SIGNATURE then
      endOfCentDir.signature := ZIP_END_OF_CENTRAL_DIRECTORY_SIGNATURE;
      endOfCentDir.number_of_this_disk                         := bytes2Int(stri[ 5 fixLen 2], UNSIGNED, LE);
      endOfCentDir.disk_number_with_start_of_central_directory := bytes2Int(stri[ 7 fixLen 2], UNSIGNED, LE);
      endOfCentDir.entries_in_central_directory_on_this_disk   := bytes2Int(stri[ 9 fixLen 2], UNSIGNED, LE);
      endOfCentDir.entries_in_central_directory                := bytes2Int(stri[11 fixLen 2], UNSIGNED, LE);
      endOfCentDir.size_of_central_directory                   := bytes2Int(stri[13 fixLen 4], UNSIGNED, LE);
      endOfCentDir.offset_of_start_of_central_directory        := bytes2Int(stri[17 fixLen 4], UNSIGNED, LE);
      file_comment_length                                      := bytes2Int(stri[21 fixLen 2], UNSIGNED, LE);
      endOfCentDir.file_comment                                := gets(inFile, file_comment_length);
      # write(endOfCentDir);
    end if;
  end func;


const func string: str (in end_of_central_directory: endOfCentDir) is func
  result
    var string: stri is "";
  begin
    stri := ZIP_END_OF_CENTRAL_DIRECTORY_SIGNATURE &
            bytes(       endOfCentDir.number_of_this_disk,                         UNSIGNED, LE, 2) &
            bytes(       endOfCentDir.disk_number_with_start_of_central_directory, UNSIGNED, LE, 2) &
            bytes(       endOfCentDir.entries_in_central_directory_on_this_disk,   UNSIGNED, LE, 2) &
            bytes(       endOfCentDir.entries_in_central_directory,                UNSIGNED, LE, 2) &
            bytes(       endOfCentDir.size_of_central_directory,                   UNSIGNED, LE, 4) &
            bytes(       endOfCentDir.offset_of_start_of_central_directory,        UNSIGNED, LE, 4) &
            bytes(length(endOfCentDir.file_comment),                               UNSIGNED, LE, 2) &
            endOfCentDir.file_comment;
  end func;


const func end_of_central_directory: readEndOfCentralDir (inout file: inFile,
    inout integer: endOfCentralDirPos) is func
  result
    var end_of_central_directory: endOfCentralDir is end_of_central_directory.value;
  local
    const integer: MIN_RECORD_LEN is 22;
    const integer: MAX_RECORD_LEN is MIN_RECORD_LEN + 2**16 - 1;
    var zip64_end_of_central_dir_locator: endOfCentDirLocator is zip64_end_of_central_dir_locator.value;
    var zip64_end_of_central_directory: endOfCentralDir64 is zip64_end_of_central_directory.value;
  begin
    endOfCentralDirPos := rposOfMagic(inFile, ZIP_END_OF_CENTRAL_DIRECTORY_SIGNATURE,
                                      MIN_RECORD_LEN, MAX_RECORD_LEN);
    if endOfCentralDirPos <> 0 then
      if endOfCentralDirPos > ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE then
        seek(inFile, endOfCentralDirPos - ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE);
        endOfCentDirLocator := get_zip64_end_of_central_dir_locator(inFile);
        if endOfCentDirLocator.signature = ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE then
          seek(inFile, succ(endOfCentDirLocator.offset_of_zip64_end_of_central_directory));
          endOfCentralDir64 := get_zip64_end_of_central_directory(inFile);
        end if;
      end if;
      seek(inFile, endOfCentralDirPos);
      endOfCentralDir := get_end_of_central_directory(inFile);
      if endOfCentDirLocator.signature = ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE then
        if endOfCentralDir64.signature = ZIP64_END_OF_CENTRAL_DIRECTORY_SIGNATURE then
          if endOfCentralDir.number_of_this_disk = 16#ffff then
            endOfCentralDir.number_of_this_disk := endOfCentralDir64.number_of_this_disk;
          end if;
          if endOfCentralDir.disk_number_with_start_of_central_directory = 16#ffff then
            endOfCentralDir.disk_number_with_start_of_central_directory :=
                endOfCentralDir64.disk_number_with_start_of_central_directory;
          end if;
          if endOfCentralDir.entries_in_central_directory_on_this_disk = 16#ffff then
            endOfCentralDir.entries_in_central_directory_on_this_disk :=
                endOfCentralDir64.entries_in_central_directory_on_this_disk;
          end if;
          if endOfCentralDir.entries_in_central_directory = 16#ffff then
            endOfCentralDir.entries_in_central_directory := endOfCentralDir64.entries_in_central_directory;
          end if;
          if endOfCentralDir.size_of_central_directory = 16#ffffffff then
            endOfCentralDir.size_of_central_directory := endOfCentralDir64.size_of_central_directory;
          end if;
          if endOfCentralDir.offset_of_start_of_central_directory = 16#ffffffff then
            endOfCentralDir.offset_of_start_of_central_directory :=
                endOfCentralDir64.offset_of_start_of_central_directory;
          end if;
        end if;
      end if;
      if tell(inFile) <> succ(length(inFile)) then
        # The end_of_central_directory record is not at the end of the file.
        # writeln("curr pos: " <& tell(inFile) <& " length: " <& length(inFile));
        endOfCentralDir := end_of_central_directory.value;
      end if;
    end if;
  end func;


const type: central_file_header is new struct
    var string: signature                        is "";        # 4 bytes ("PK\1;\2;")
    var integer: version_made_by                 is 0;         # 2 bytes
    var integer: version_needed_to_extract       is 0;         # 2 bytes
    var bin32:   general_purpose_bit_flag        is bin32(0);  # 2 bytes
    var integer: compression_method              is 0;         # 2 bytes
    var integer: last_mod_file_time              is 0;         # 2 bytes
    var integer: last_mod_file_date              is DOS_EPOCH; # 2 bytes
    var bin32:   crc_32                          is bin32(0);  # 4 bytes
    var integer: compressed_size                 is 0;         # 4 bytes
    var integer: uncompressed_size               is 0;         # 4 bytes
    #   integer: file_name_length                is 0;         # 2 bytes
    #   integer: extra_field_length              is 0;         # 2 bytes
    #   integer: file_comment_length             is 0;         # 2 bytes
    var integer: disk_number_start               is 0;         # 2 bytes
    var integer: internal_file_attributes        is 0;         # 2 bytes
    var integer: external_file_attributes        is 0;         # 4 bytes
    var integer: relative_offset_of_local_header is 0;         # 4 bytes
    var string: file_name                        is "";        # variable size
    var string: extra_field                      is "";        # variable size
    var string: file_comment                     is "";        # variable size
    var string: filePath is "";
    var zipExtraFieldType: extraFieldMap is zipExtraFieldType.value;
  end struct;


const proc: write (in central_file_header: header) is func
  begin
    writeln("signature: "                       rpad 45 <& literal(header.signature) lpad 16);
    writeln("version_made_by: "                 rpad 45 <& (header.version_made_by radix 16 lpad0 4) lpad 16);
    writeln("version_needed_to_extract: "       rpad 45 <& header.version_needed_to_extract lpad 16);
    writeln("general_purpose_bit_flag: "        rpad 45 <& header.general_purpose_bit_flag radix 2 lpad0 16);
    writeln("compression_method: "              rpad 45 <& header.compression_method lpad 16);
    writeln("last_mod_file_time: "              rpad 45 <& header.last_mod_file_time lpad 16);
    writeln("last_mod_file_date: "              rpad 45 <& header.last_mod_file_date lpad 16);
    writeln("crc_32: "                          rpad 45 <& header.crc_32 lpad 16);
    writeln("compressed_size: "                 rpad 45 <& header.compressed_size lpad 16);
    writeln("uncompressed_size: "               rpad 45 <& header.uncompressed_size lpad 16);
    writeln("file_name_length: "                rpad 45 <& length(header.file_name) lpad 16);
    writeln("extra_field_length: "              rpad 45 <& length(header.extra_field) lpad 16);
    writeln("file_comment_length: "             rpad 45 <& length(header.file_comment) lpad 16);
    writeln("disk_number_start: "               rpad 45 <& header.disk_number_start lpad 16);
    writeln("internal_file_attributes: "        rpad 45 <& header.internal_file_attributes lpad 16);
    writeln("external_file_attributes: "        rpad 45 <& header.external_file_attributes radix 16 lpad 16);
    writeln("relative_offset_of_local_header: " rpad 45 <& header.relative_offset_of_local_header lpad 16);
    writeln("file_name: "                       rpad 45 <& literal(header.file_name) lpad 16);
    writeln("extra_field: "                     rpad 45 <& literal(header.extra_field) lpad 16);
    writeExtraField(header.extra_field);
    writeln("file_comment: "                    rpad 45 <& literal(header.file_comment) lpad 16);
    writeln("filePath: "                        rpad 45 <& header.filePath);
  end func;


const proc: considerZip64ExtraField (inout central_file_header: header,
    in string: zip64ExtraField) is func
  local
    var integer: pos is 1;
  begin
    if header.uncompressed_size = 16#ffffffff and length(zip64ExtraField) >= pos + 7 then
      header.uncompressed_size := bytes2Int(zip64ExtraField[pos fixLen 8], UNSIGNED, LE);
      # writeln("uncompressed_size: " <& header.uncompressed_size);
      pos +:= 8;
    end if;
    if header.compressed_size = 16#ffffffff and length(zip64ExtraField) >= pos + 7 then
      header.compressed_size := bytes2Int(zip64ExtraField[pos fixLen 8], UNSIGNED, LE);
      # writeln("compressed_size: " <& header.compressed_size);
      pos +:= 8;
    end if;
    if header.relative_offset_of_local_header = 16#ffffffff and length(zip64ExtraField) >= pos + 7 then
      header.relative_offset_of_local_header := bytes2Int(zip64ExtraField[pos fixLen 8], UNSIGNED, LE);
      # writeln("relative_offset_of_local_header: " <& header.relative_offset_of_local_header);
      pos +:= 8;
    end if;
    if header.disk_number_start = 16#ffff and length(zip64ExtraField) >= pos + 3 then
      header.disk_number_start := bytes2Int(zip64ExtraField[pos fixLen 4], UNSIGNED, LE);
      # writeln("disk_number_start: " <& header.disk_number_start);
      pos +:= 4;
    end if;
  end func;


const func central_file_header: get_central_header (inout file: inFile) is func
  result
    var central_file_header: header is central_file_header.value;
  local
    var string: stri is "";
    var integer: file_name_length is 0;
    var integer: extra_field_length is 0;
    var integer: file_comment_length is 0;
  begin
    stri := gets(inFile, ZIP_CENTRAL_HEADER_FIXED_SIZE);
    if length(stri) = ZIP_CENTRAL_HEADER_FIXED_SIZE and
        stri[.. 4] = ZIP_CENTRAL_HEADER_SIGNATURE then
      header.signature := ZIP_CENTRAL_HEADER_SIGNATURE;
      header.version_made_by                 := bytes2Int(stri[ 5 fixLen 2], UNSIGNED, LE);
      header.version_needed_to_extract       := bytes2Int(stri[ 7 fixLen 2], UNSIGNED, LE);
      header.general_purpose_bit_flag  := bin32(bytes2Int(stri[ 9 fixLen 2], UNSIGNED, LE));
      header.compression_method              := bytes2Int(stri[11 fixLen 2], UNSIGNED, LE);
      header.last_mod_file_time              := bytes2Int(stri[13 fixLen 2], UNSIGNED, LE);
      header.last_mod_file_date              := bytes2Int(stri[15 fixLen 2], UNSIGNED, LE);
      header.crc_32                    := bin32(bytes2Int(stri[17 fixLen 4], UNSIGNED, LE));
      header.compressed_size                 := bytes2Int(stri[21 fixLen 4], UNSIGNED, LE);
      header.uncompressed_size               := bytes2Int(stri[25 fixLen 4], UNSIGNED, LE);
      file_name_length                       := bytes2Int(stri[29 fixLen 2], UNSIGNED, LE);
      extra_field_length                     := bytes2Int(stri[31 fixLen 2], UNSIGNED, LE);
      file_comment_length                    := bytes2Int(stri[33 fixLen 2], UNSIGNED, LE);
      header.disk_number_start               := bytes2Int(stri[35 fixLen 2], UNSIGNED, LE);
      header.internal_file_attributes        := bytes2Int(stri[37 fixLen 2], UNSIGNED, LE);
      header.external_file_attributes        := bytes2Int(stri[39 fixLen 4], UNSIGNED, LE);
      header.relative_offset_of_local_header := bytes2Int(stri[43 fixLen 4], UNSIGNED, LE);
      header.file_name                       := gets(inFile, file_name_length);
      header.extra_field                     := gets(inFile, extra_field_length);
      header.file_comment                    := gets(inFile, file_comment_length);
      header.extraFieldMap := getExtraFieldMap(header.extra_field);
      if header.general_purpose_bit_flag & ZIP_FILE_NAME_IS_UTF8 <> bin32(0) then
        block
          header.filePath:= fromUtf8(header.file_name);
        exception
          catch RANGE_ERROR:
            header.filePath := header.file_name;
        end block;
      else
        header.filePath := header.file_name;
      end if;
      if header.filePath <> "/" and endsWith(header.filePath, "/") then
        header.filePath := header.filePath[.. pred(length(header.filePath))];
      end if;
      if 16#0001 in header.extraFieldMap then
        considerZip64ExtraField(header, header.extraFieldMap[16#0001]);
      end if;
      # write(header);
      # writeExtraField(header.extra_field);
    end if;
  end func;


const func string: str (in central_file_header: header) is func
  result
    var string: stri is "";
  begin
    stri := ZIP_CENTRAL_HEADER_SIGNATURE &
            bytes(       header.version_made_by,                 UNSIGNED, LE, 2) &
            bytes(       header.version_needed_to_extract,       UNSIGNED, LE, 2) &
            bytes(   ord(header.general_purpose_bit_flag),       UNSIGNED, LE, 2) &
            bytes(       header.compression_method,              UNSIGNED, LE, 2) &
            bytes(       header.last_mod_file_time,              UNSIGNED, LE, 2) &
            bytes(       header.last_mod_file_date,              UNSIGNED, LE, 2) &
            bytes(   ord(header.crc_32),                         UNSIGNED, LE, 4) &
            bytes(       header.compressed_size,                 UNSIGNED, LE, 4) &
            bytes(       header.uncompressed_size,               UNSIGNED, LE, 4) &
            bytes(length(header.file_name),                      UNSIGNED, LE, 2) &
            bytes(length(header.extra_field),                    UNSIGNED, LE, 2) &
            bytes(length(header.file_comment),                   UNSIGNED, LE, 2) &
            bytes(       header.disk_number_start,               UNSIGNED, LE, 2) &
            bytes(       header.internal_file_attributes,        UNSIGNED, LE, 2) &
            bytes(       header.external_file_attributes,        UNSIGNED, LE, 4) &
            bytes(       header.relative_offset_of_local_header, UNSIGNED, LE, 4) &
            header.file_name &
            header.extra_field &
            header.file_comment;
  end func;


const proc: writeHead (inout file: outFile, in central_file_header: header) is func
  begin
    write(outFile, str(header));
  end func;


const func string: getCentralHeaderFilePath (inout file: inFile) is func
  result
    var string: filePath is "";
  local
    var string: stri is "";
    var bin32: general_purpose_bit_flag is bin32(0);
    var integer: file_name_length is 0;
    var integer: extra_field_length is 0;
    var integer: file_comment_length is 0;
  begin
    stri := gets(inFile, ZIP_CENTRAL_HEADER_FIXED_SIZE);
    if length(stri) = ZIP_CENTRAL_HEADER_FIXED_SIZE and
        stri[.. 4] = ZIP_CENTRAL_HEADER_SIGNATURE then
      general_purpose_bit_flag  := bin32(bytes2Int(stri[ 9 fixLen 2], UNSIGNED, LE));
      file_name_length                := bytes2Int(stri[29 fixLen 2], UNSIGNED, LE);
      extra_field_length              := bytes2Int(stri[31 fixLen 2], UNSIGNED, LE);
      file_comment_length             := bytes2Int(stri[33 fixLen 2], UNSIGNED, LE);
      filePath := gets(inFile, file_name_length);
      # seek(inFile, tell(inFile) + extra_field_length + file_comment_length);
      ignore(gets(inFile, extra_field_length + file_comment_length));
      if general_purpose_bit_flag & ZIP_FILE_NAME_IS_UTF8 <> bin32(0) then
        block
          filePath := fromUtf8(filePath);
        exception
          catch RANGE_ERROR: noop;
        end block;
      end if;
      if filePath <> "/" and endsWith(filePath, "/") then
        filePath := filePath[.. pred(length(filePath))];
      end if;
    end if;
  end func;


const func local_file_header: toLocalHeader (in central_file_header: header) is func
  result
    var local_file_header: localHeader is local_file_header.value;
  begin
    localHeader.signature                 := ZIP_LOCAL_HEADER_SIGNATURE;
    localHeader.version_needed_to_extract := header.version_needed_to_extract;
    localHeader.general_purpose_bit_flag  := header.general_purpose_bit_flag;
    localHeader.compression_method        := header.compression_method;
    localHeader.last_mod_file_time        := header.last_mod_file_time;
    localHeader.last_mod_file_date        := header.last_mod_file_date;
    localHeader.crc_32                    := header.crc_32;
    localHeader.compressed_size           := header.compressed_size;
    localHeader.uncompressed_size         := header.uncompressed_size;
    localHeader.file_name                 := header.file_name;
    localHeader.extra_field               := header.extra_field;
    # write(localHeader);
  end func;


const proc: updateLocalHeader (inout local_file_header: localHeader,
    in central_file_header: header) is func
  begin
    localHeader.compression_method := header.compression_method;
    localHeader.last_mod_file_time := header.last_mod_file_time;
    localHeader.last_mod_file_date := header.last_mod_file_date;
    localHeader.crc_32             := header.crc_32;
    localHeader.compressed_size    := header.compressed_size;
    localHeader.uncompressed_size  := header.uncompressed_size;
    # write(localHeader);
  end func;


const proc: initLastModFileTime (inout central_file_header: header,
    in time: modificationTime) is func
  local
    var integer: timestamp is 0;
    var string: unixExtraField is "";
  begin
    timestamp := timestamp1970(modificationTime);
    unixExtraField :=
        bytes(timestamp, UNSIGNED, LE, 4) mult 2 &
        bytes(        0, UNSIGNED, LE, 2) mult 2;
    header.extraFieldMap @:= [16#000d] unixExtraField;
    header.extra_field := extraFieldFromMap(header.extraFieldMap);
    header.last_mod_file_time := (modificationTime.hour << 11) +
                                 (modificationTime.minute << 5) +
                                 (modificationTime.second >> 1);
    header.last_mod_file_date := ((modificationTime.year - 1980) << 9) +
                                  (modificationTime.month << 5) +
                                   modificationTime.day;
  end func;


const proc: assignLastModFileTime (inout local_file_header: localHeader,
    in time: modificationTime) is func
  local
    var integer: timestamp is 0;
  begin
    if 16#5455 in localHeader.extraFieldMap then
      # Extended Timestamp Extra Field:
      timestamp := timestamp1970(modificationTime);
      localHeader.extraFieldMap[16#5455] @:= [2] bytes(timestamp, UNSIGNED, LE, 4);
      localHeader.extra_field := extraFieldFromMap(localHeader.extraFieldMap);
    elsif 16#000d in localHeader.extraFieldMap then
      # UNIX Extra Field
      timestamp := timestamp1970(modificationTime);
      # Update last access time and modification time.
      localHeader.extraFieldMap[16#000d] @:= [1] bytes(timestamp, UNSIGNED, LE, 4) mult 2;
      localHeader.extra_field := extraFieldFromMap(localHeader.extraFieldMap);
    elsif 16#000a in localHeader.extraFieldMap then
      # NTFS Extra Field:
      timestamp := timestamp1601(modificationTime);
      localHeader.extraFieldMap[16#000a] @:= [9] bytes(timestamp, UNSIGNED, LE, 8);
      localHeader.extra_field := extraFieldFromMap(localHeader.extraFieldMap);
    end if;
    localHeader.last_mod_file_time := (modificationTime.hour << 11) +
                                      (modificationTime.minute << 5) +
                                      (modificationTime.second >> 1);
    localHeader.last_mod_file_date := ((modificationTime.year - 1980) << 9) +
                                       (modificationTime.month << 5) +
                                        modificationTime.day;
  end func;


const proc: assignLastModFileTime (inout central_file_header: header,
    in time: modificationTime) is func
  local
    var integer: timestamp is 0;
  begin
    if 16#5455 in header.extraFieldMap then
      # Extended Timestamp Extra Field:
      timestamp := timestamp1970(modificationTime);
      header.extraFieldMap[16#5455] @:= [2] bytes(timestamp, UNSIGNED, LE, 4);
      header.extra_field := extraFieldFromMap(header.extraFieldMap);
    elsif 16#000d in header.extraFieldMap then
      # UNIX Extra Field
      timestamp := timestamp1970(modificationTime);
      # Update last access time and modification time.
      header.extraFieldMap[16#000d] @:= [1] bytes(timestamp, UNSIGNED, LE, 4) mult 2;
      header.extra_field := extraFieldFromMap(header.extraFieldMap);
    elsif 16#000a in header.extraFieldMap then
      # NTFS Extra Field:
      timestamp := timestamp1601(modificationTime);
      header.extraFieldMap[16#000a] @:= [9] bytes(timestamp, UNSIGNED, LE, 8);
      header.extra_field := extraFieldFromMap(header.extraFieldMap);
    end if;
    header.last_mod_file_time := (modificationTime.hour << 11) +
                                 (modificationTime.minute << 5) +
                                 (modificationTime.second >> 1);
    header.last_mod_file_date := ((modificationTime.year - 1980) << 9) +
                                  (modificationTime.month << 5) +
                                   modificationTime.day;
  end func;


const proc: assignUserId (inout local_file_header: localHeader,
    in integer: uid) is func
  local
    var integer: size is 0;
  begin
    if 16#000d in localHeader.extraFieldMap then
      # UNIX Extra Field
      localHeader.extraFieldMap[16#000d] @:= [9] bytes(uid, UNSIGNED, LE, 2);
      localHeader.extra_field := extraFieldFromMap(localHeader.extraFieldMap);
    elsif 16#756e in localHeader.extraFieldMap then
      # ASi Unix Extra Field:
      localHeader.extraFieldMap[16#756e] @:= [11] bytes(uid, UNSIGNED, LE, 2);
      localHeader.extra_field := extraFieldFromMap(localHeader.extraFieldMap);
    elsif 16#7875 in localHeader.extraFieldMap then
      # New Unix Extra Field:
      size := ord(localHeader.extraFieldMap[16#7875][2]);
      localHeader.extraFieldMap[16#7875] @:= [3] bytes(uid, UNSIGNED, LE, size);
      localHeader.extra_field := extraFieldFromMap(localHeader.extraFieldMap);
    end if;
  end func;


const proc: assignUserId (inout central_file_header: header,
    in integer: uid) is func
  local
    var integer: size is 0;
  begin
    if 16#000d in header.extraFieldMap then
      # UNIX Extra Field
      header.extraFieldMap[16#000d] @:= [9] bytes(uid, UNSIGNED, LE, 2);
      header.extra_field := extraFieldFromMap(header.extraFieldMap);
    elsif 16#756e in header.extraFieldMap then
      # ASi Unix Extra Field:
      header.extraFieldMap[16#756e] @:= [11] bytes(uid, UNSIGNED, LE, 2);
      header.extra_field := extraFieldFromMap(header.extraFieldMap);
    elsif 16#7875 in header.extraFieldMap then
      # New Unix Extra Field:
      size := ord(header.extraFieldMap[16#7875][2]);
      header.extraFieldMap[16#7875] @:= [3] bytes(uid, UNSIGNED, LE, size);
      header.extra_field := extraFieldFromMap(header.extraFieldMap);
    end if;
  end func;


const proc: assignGroupId (inout local_file_header: localHeader,
    in integer: gid) is func
  local
    var integer: pos is 0;
    var integer: size is 0;
  begin
    if 16#000d in localHeader.extraFieldMap then
      # UNIX Extra Field
      localHeader.extraFieldMap[16#000d] @:= [11] bytes(gid, UNSIGNED, LE, 2);
      localHeader.extra_field := extraFieldFromMap(localHeader.extraFieldMap);
    elsif 16#756e in localHeader.extraFieldMap then
      # ASi Unix Extra Field:
      localHeader.extraFieldMap[16#756e] @:= [13] bytes(gid, UNSIGNED, LE, 2);
      localHeader.extra_field := extraFieldFromMap(localHeader.extraFieldMap);
    elsif 16#7875 in localHeader.extraFieldMap then
      # New Unix Extra Field:
      pos := 3 + ord(localHeader.extraFieldMap[16#7875][2]);
      size := ord(localHeader.extraFieldMap[16#7875][pos]);
      localHeader.extraFieldMap[16#7875] @:= [succ(pos)] bytes(gid, UNSIGNED, LE, size);
      localHeader.extra_field := extraFieldFromMap(localHeader.extraFieldMap);
    end if;
  end func;


const proc: assignGroupId (inout central_file_header: header,
    in integer: gid) is func
  local
    var integer: pos is 0;
    var integer: size is 0;
  begin
    if 16#000d in header.extraFieldMap then
      # UNIX Extra Field
      header.extraFieldMap[16#000d] @:= [11] bytes(gid, UNSIGNED, LE, 2);
      header.extra_field := extraFieldFromMap(header.extraFieldMap);
    elsif 16#756e in header.extraFieldMap then
      # ASi Unix Extra Field:
      header.extraFieldMap[16#756e] @:= [13] bytes(gid, UNSIGNED, LE, 2);
      header.extra_field := extraFieldFromMap(header.extraFieldMap);
    elsif 16#7875 in header.extraFieldMap then
      # New Unix Extra Field:
      pos := 3 + ord(header.extraFieldMap[16#7875][2]);
      size := ord(header.extraFieldMap[16#7875][pos]);
      header.extraFieldMap[16#7875] @:= [succ(pos)] bytes(gid, UNSIGNED, LE, size);
      header.extra_field := extraFieldFromMap(header.extraFieldMap);
    end if;
  end func;


const type: zipCatalogType is hash [string] central_file_header;


(**
 *  [[filesys#fileSys|FileSys]] implementation type to access ZIP and JAR archives.
 *  The zip file system does not support the concept of a current
 *  working directory. The functions chdir and getcwd are not supported
 *  by the zip file system. The root path of a zip file system is "".
 *)
const type: zipArchive is sub emptyFileSys struct
    var file: zipFile is STD_NULL;
    var archiveRegisterType: register is archiveRegisterType.value;
    var zipCatalogType: catalog is zipCatalogType.value;
    var end_of_central_directory: endOfCentralDir is end_of_central_directory.value;
    var integer: endOfCentralDirPos is 1;
    var integer: startOfCentralDirPos is 1;
  end struct;


(**
 *  Open a ZIP archive with the given zipFile.
 *  @param zipFile File that contains a ZIP archive.
 *  @return a file system that accesses the ZIP archive, or
 *          fileSys.value if it could not be opened.
 *)
const func fileSys: openZip (inout file: zipFile) is func
  result
    var fileSys: newFileSys is fileSys.value;
  local
    var end_of_central_directory: endOfCentralDir is end_of_central_directory.value;
    var integer: endOfCentralDirPos is 0;
    var integer: centralHeaderPos is 0;
    var string: filePath is "";
    var zipArchive: zip is zipArchive.value;
  begin
    if length(zipFile) = 0 then
      zip.zipFile := zipFile;
      zip.endOfCentralDir.signature := ZIP_END_OF_CENTRAL_DIRECTORY_SIGNATURE;
      newFileSys := toInterface(zip);
    else
      endOfCentralDir := readEndOfCentralDir(zipFile, endOfCentralDirPos);
      if endOfCentralDir.signature = ZIP_END_OF_CENTRAL_DIRECTORY_SIGNATURE then
        zip.zipFile := zipFile;
        zip.endOfCentralDir := endOfCentralDir;
        zip.endOfCentralDirPos := endOfCentralDirPos;
        zip.startOfCentralDirPos := succ(endOfCentralDir.offset_of_start_of_central_directory);
        # writeln("startOfCentralDirPos: " <& zip.startOfCentralDirPos);
        centralHeaderPos := zip.startOfCentralDirPos;
        seek(zip.zipFile, centralHeaderPos);
        filePath := getCentralHeaderFilePath(zip.zipFile);
        while filePath <> "" do
          # writeln(filePath <& " " <& centralHeaderPos);
          zip.register @:= [filePath] centralHeaderPos;
          centralHeaderPos := tell(zip.zipFile);
          filePath := getCentralHeaderFilePath(zip.zipFile);
        end while;
        newFileSys := toInterface(zip);
      end if;
    end if;
  end func;


(**
 *  Open a ZIP archive with the given zipFileName.
 *  @param zipFileName Name of the ZIP archive to be opened.
 *  @return a file system that accesses the ZIP archive, or
 *          fileSys.value if it could not be opened.
 *)
const func fileSys: openZip (in string: zipFileName) is func
  result
    var fileSys: zip is fileSys.value;
  local
    var file: zipFile is STD_NULL;
  begin
    zipFile := open(zipFileName, "r");
    zip := openZip(zipFile);
  end func;


(**
 *  Close a ZIP archive. The ZIP file below stays open.
 *)
const proc: close (inout zipArchive: zip) is func
  begin
    zip := zipArchive.value;
  end func;


const func central_file_header: addToCatalog (inout zipArchive: zip, in string: filePath) is func
  result
    var central_file_header: header is central_file_header.value;
  begin
    seek(zip.zipFile, zip.register[filePath]);
    header := get_central_header(zip.zipFile);
    zip.catalog @:= [filePath] header;
  end func;


const func central_file_header: addImplicitDir (inout zipArchive: zip,
    in string: dirPath) is func
  result
    var central_file_header: header is central_file_header.value;
  begin
    header.file_name := dirPath & "/";
    header.filePath := dirPath;
    header.relative_offset_of_local_header := -1;
    zip.catalog @:= [dirPath] header;
  end func;


const func boolean: isRegularFile (in central_file_header: header) is func
  result
    var boolean: isRegularFile is FALSE;
  begin
    if header.relative_offset_of_local_header <> -1 then
      # It is not an implicit directory.
      if header.version_made_by >> 8 = ZIP_HOST_SYSTEM_UNIX then
        isRegularFile := bin32(header.external_file_attributes >> 16) &
                         MODE_FILE_TYPE_MASK = MODE_FILE_REGULAR;
      else
        isRegularFile := not endsWith(header.file_name, "/");
      end if;
    end if;
  end func;


const func boolean: isSymlink (in central_file_header: header) is
  return header.version_made_by >> 8 = ZIP_HOST_SYSTEM_UNIX and
            bin32(header.external_file_attributes >> 16) &
                MODE_FILE_TYPE_MASK = MODE_FILE_SYMLINK;


const func string: followSymlink (inout zipArchive: zip, in var string: filePath,
    inout central_file_header: header) is func
  result
    var string: missingPath is "";
  local
    var integer: symlinkCount is MAX_SYMLINK_CHAIN_LENGTH;
    var boolean: isSymlink is TRUE;
    var local_file_header: localHeader is local_file_header.value;
    var string: targetPath is "";
  begin
    # writeln("followSymlink: " <& filePath);
    repeat
      if filePath in zip.catalog then
        header := zip.catalog[filePath];
      elsif filePath in zip.register then
        header := addToCatalog(zip, filePath);
      elsif implicitDir(zip.register, filePath) then
        header := addImplicitDir(zip, filePath);
      else
        # The file does not exist.
        missingPath := filePath;
        isSymlink := FALSE;
        # writeln("missing: " <& missingPath);
      end if;
      if missingPath = "" then
        if isSymlink(header) then
          decr(symlinkCount);
          seek(zip.zipFile, succ(header.relative_offset_of_local_header));
          localHeader := get_local_header(zip.zipFile);
          # write(localHeader);
          if localHeader.compression_method = 0 then
            # The link destination is stored (no compression).
            targetPath := gets(zip.zipFile, localHeader.compressed_size);
            filePath := symlinkDestination(filePath, targetPath);
          else
            # writeln("unsupported compression method: " <& localHeader.compression_method);
            raise FILE_ERROR;
          end if;
        else
          isSymlink := FALSE;
          # writeln("found: " <& header.filePath);
        end if;
      end if;
    until not isSymlink or symlinkCount < 0;
    if isSymlink then
      # Too many symbolic links.
      raise FILE_ERROR;
    end if;
  end func;


const func central_file_header: followSymlink (inout zipArchive: zip, in var string: filePath) is func
  result
    var central_file_header: header is central_file_header.value;
  local
    var string: missingPath is "";
  begin
    missingPath := followSymlink(zip, filePath, header);
    if missingPath <> "" then
      # The file does not exist.
      raise FILE_ERROR;
    end if;
  end func;


const proc: fixRegisterAndCatalog (inout zipArchive: zip, in integer: insertPos,
    in integer: numChars) is func
  local
    var integer: headerPos is 1;
    var string: filePath is "";
  begin
    for key filePath range zip.register do
      if zip.register[filePath] >= insertPos then
        zip.register[filePath] +:= numChars;
      end if;
    end for;
    for key filePath range zip.catalog do
      if succ(zip.catalog[filePath].relative_offset_of_local_header) >= insertPos then
        zip.catalog[filePath].relative_offset_of_local_header +:= numChars;
        seek(zip.zipFile, zip.register[filePath]);
        writeHead(zip.zipFile, zip.catalog[filePath]);
      end if;
    end for;
    if zip.startOfCentralDirPos >= insertPos then
      zip.startOfCentralDirPos +:= numChars;
    end if;
    if zip.endOfCentralDirPos >= insertPos then
      zip.endOfCentralDirPos +:= numChars;
    end if;
  end func;


(**
 *  Determine the file names in a directory inside a ZIP archive.
 *  Note that the function returns only the file names.
 *  Additional information must be obtained with other calls.
 *  @param zip Open ZIP archive.
 *  @param dirPath Path of a directory in the ZIP archive.
 *  @return an array with the file names.
 *  @exception RANGE_ERROR ''dirPath'' does not use the standard path
 *             representation.
 *  @exception FILE_ERROR ''dirPath'' is not present in the ZIP archive.
 *)
const func array string: readDir (inout zipArchive: zip, in var string: dirPath) is
  return readDir(zip.register, dirPath);


(**
 *  Determine the file paths in a ZIP archive.
 *  Note that the function returns only the file paths.
 *  Additional information must be obtained with other calls.
 *  @param zip Open ZIP archive.
 *  @return an array with the file paths.
 *)
const func array string: readDir (inout zipArchive: zip, RECURSIVE) is
  return sort(keys(zip.register));


(**
 *  Determine the type of a file in a ZIP archive.
 *  The function follows symbolic links. If the chain of
 *  symbolic links is too long the function returns ''FILE_SYMLINK''.
 *  A return value of ''FILE_ABSENT'' does not imply that a file
 *  with this name can be created, since missing directories and
 *  invalid file names cause also ''FILE_ABSENT''.
 *  @return the type of the file.
 *  @exception RANGE_ERROR ''filePath'' does not use the standard path
 *             representation.
 *)
const func fileType: fileType (inout zipArchive: zip, in var string: filePath) is func
  result
    var fileType: aFileType is FILE_UNKNOWN;
  local
    var central_file_header: header is central_file_header.value;
    var local_file_header: localHeader is local_file_header.value;
    var integer: symlinkCount is MAX_SYMLINK_CHAIN_LENGTH;
    var boolean: isSymlink is FALSE;
    var string: targetPath is "";
  begin
    # writeln("fileType: " <& filePath);
    if filePath <> "/" and endsWith(filePath, "/") then
      raise RANGE_ERROR;
    elsif filePath = "" then
      aFileType := FILE_DIR;
    else
      repeat
        isSymlink := FALSE;
        if filePath in zip.catalog then
          header := zip.catalog[filePath];
        elsif filePath in zip.register then
          header := addToCatalog(zip, filePath);
        elsif implicitDir(zip.register, filePath) then
          header := addImplicitDir(zip, filePath);
        else
          aFileType := FILE_ABSENT;
        end if;
        if aFileType = FILE_UNKNOWN then
          case header.version_made_by >> 8 of
            when {ZIP_HOST_SYSTEM_UNIX}:
              case bin32(header.external_file_attributes >> 16) & MODE_FILE_TYPE_MASK of
                when {MODE_FILE_REGULAR}: aFileType := FILE_REGULAR;
                when {MODE_FILE_DIR}:     aFileType := FILE_DIR;
                when {MODE_FILE_CHAR}:    aFileType := FILE_CHAR;
                when {MODE_FILE_BLOCK}:   aFileType := FILE_BLOCK;
                when {MODE_FILE_FIFO}:    aFileType := FILE_FIFO;
                when {MODE_FILE_SOCKET}:  aFileType := FILE_SOCKET;
                when {MODE_FILE_SYMLINK}:
                  isSymlink := TRUE;
                  decr(symlinkCount);
                  seek(zip.zipFile, succ(header.relative_offset_of_local_header));
                  localHeader := get_local_header(zip.zipFile);
                  # write(localHeader);
                  if localHeader.compression_method = 0 then
                    # The link destination is stored (no compression).
                    targetPath := gets(zip.zipFile, localHeader.compressed_size);
                    filePath := symlinkDestination(filePath, targetPath);
                  else
                    # writeln("unsupported compression method: " <& localHeader.compression_method);
                    raise FILE_ERROR;
                  end if;
                otherwise:                aFileType := FILE_UNKNOWN;
              end case;
            otherwise:
              if endsWith(header.file_name, "/") then
                aFileType := FILE_DIR;
              else
                aFileType := FILE_REGULAR;
              end if;
          end case;
        end if;
      until not isSymlink or symlinkCount < 0;
      if isSymlink then
        aFileType := FILE_SYMLINK;
      end if;
    end if;
  end func;


(**
 *  Determine the type of a file in a ZIP archive.
 *  A return value of ''FILE_ABSENT'' does not imply that a file
 *  with this name can be created, since missing directories and
 *  invalid file names cause also ''FILE_ABSENT''.
 *  @return the type of the file.
 *  @exception RANGE_ERROR ''filePath'' does not use the standard path
 *             representation.
 *)
const func fileType: fileTypeSL (inout zipArchive: zip, in string: filePath) is func
  result
    var fileType: aFileType is FILE_UNKNOWN;
  local
    var central_file_header: header is central_file_header.value;
  begin
    if filePath <> "/" and endsWith(filePath, "/") then
      raise RANGE_ERROR;
    else
      if filePath in zip.catalog then
        header := zip.catalog[filePath];
      elsif filePath in zip.register then
        header := addToCatalog(zip, filePath);
      elsif implicitDir(zip.register, filePath) then
        header := addImplicitDir(zip, filePath);
      else
        aFileType := FILE_ABSENT;
      end if;
      if aFileType = FILE_UNKNOWN then
        case header.version_made_by >> 8 of
          when {ZIP_HOST_SYSTEM_UNIX}:
            case bin32(header.external_file_attributes >> 16) & MODE_FILE_TYPE_MASK of
              when {MODE_FILE_REGULAR}: aFileType := FILE_REGULAR;
              when {MODE_FILE_DIR}:     aFileType := FILE_DIR;
              when {MODE_FILE_CHAR}:    aFileType := FILE_CHAR;
              when {MODE_FILE_BLOCK}:   aFileType := FILE_BLOCK;
              when {MODE_FILE_FIFO}:    aFileType := FILE_FIFO;
              when {MODE_FILE_SOCKET}:  aFileType := FILE_SOCKET;
              when {MODE_FILE_SYMLINK}: aFileType := FILE_SYMLINK;
              otherwise:                aFileType := FILE_UNKNOWN;
            end case;
          otherwise:
            if endsWith(header.file_name, "/") then
              aFileType := FILE_DIR;
            else
              aFileType := FILE_REGULAR;
            end if;
        end case;
      end if;
    end if;
  end func;


(**
 *  Determine the file mode (permissions) of a file in a ZIP archive.
 *  The function follows symbolic links.
 *  @return the file mode.
 *  @exception RANGE_ERROR ''filePath'' does not use the standard path
 *             representation.
 *  @exception FILE_ERROR ''filePath'' is not present in the ZIP archive.
 *)
const func fileMode: getFileMode (inout zipArchive: zip, in string: filePath) is func
  result
    var fileMode: mode is fileMode.value;
  local
    const bin32: FAT_READ_ONLY    is bin32(16#01);
    const bin32: FAT_HIDDEN       is bin32(16#02);
    const bin32: FAT_SYSTEM       is bin32(16#04);
    const bin32: FAT_VOLUME_LABEL is bin32(16#08);
    const bin32: FAT_DIRECTORY    is bin32(16#10);
    const bin32: FAT_ARCHIVE      is bin32(16#20);
    const bin32: FAT_DEVICE       is bin32(16#40);
    var central_file_header: header is central_file_header.value;
    var string: extension is "";
  begin
    if filePath <> "/" and endsWith(filePath, "/") then
      raise RANGE_ERROR;
    else
      header := followSymlink(zip, filePath);
      case header.version_made_by >> 8 of
        when {ZIP_HOST_SYSTEM_MS_DOS}:
          mode := {READ_USER, READ_GROUP, READ_OTHER};
          if bin32(header.external_file_attributes) & FAT_READ_ONLY = bin32(0) then
            mode |:= {WRITE_USER, WRITE_GROUP, WRITE_OTHER};
          end if;
          if bin32(header.external_file_attributes) & FAT_DIRECTORY <> bin32(0) then
            mode |:= {EXEC_USER, EXEC_GROUP, EXEC_OTHER};
          end if;
          if length(header.file_name) >= 5 then
            extension := lower(header.file_name[length(header.file_name) - 3 ..]);
            if extension in {".bat", ".cmd", ".com", ".exe"} then
              mode |:= {EXEC_USER, EXEC_GROUP, EXEC_OTHER};
            end if;
          end if;
        when {ZIP_HOST_SYSTEM_UNIX}:
          # The unix mode is in the high 16 bits of the attributes.
          mode := fileMode((header.external_file_attributes >> 16) mod 8#1000);
        otherwise:
          mode := {READ_USER, READ_GROUP, READ_OTHER,
                   WRITE_USER, WRITE_GROUP, WRITE_OTHER};
          if endsWith(header.file_name, "/") then
            mode |:= {EXEC_USER, EXEC_GROUP, EXEC_OTHER};
          end if;
      end case;
    end if;
  end func;


(**
 *  Change the file mode (permissions) of a file in an ZIP archive.
 *  The function follows symbolic links.
 *  @exception RANGE_ERROR ''filePath'' does not use the standard path
 *             representation.
 *  @exception FILE_ERROR ''filePath'' is not present in the ZIP archive.
 *)
const proc: setFileMode (inout zipArchive: zip, in string: filePath,
    in fileMode: mode) is func
  local
    var central_file_header: header is central_file_header.value;
  begin
    if filePath <> "/" and endsWith(filePath, "/") then
      raise RANGE_ERROR;
    else
      header := followSymlink(zip, filePath);
      header.external_file_attributes :=
          header.external_file_attributes mod 16#10000 +
          (((header.external_file_attributes >> 25 << 9) + integer(mode)) << 16);
      zip.catalog @:= [filePath] header;
      seek(zip.zipFile, zip.register[filePath]);
      writeHead(zip.zipFile, header);
    end if;
  end func;


(**
 *  Determine the size of a file in a ZIP archive.
 *  The file size is measured in bytes.
 *  For directories a size of 0 is returned.
 *  The function follows symbolic links.
 *  @return the size of the file.
 *  @exception RANGE_ERROR ''filePath'' does not use the standard path
 *             representation.
 *  @exception FILE_ERROR ''filePath'' is not present in the ZIP archive.
 *)
const func integer: fileSize (inout zipArchive: zip, in string: filePath) is func
  result
    var integer: size is 0;
  local
    var central_file_header: header is central_file_header.value;
  begin
    if filePath <> "/" and endsWith(filePath, "/") then
      raise RANGE_ERROR;
    else
      size := followSymlink(zip, filePath).uncompressed_size;
    end if;
  end func;


(**
 *  Determine the modification time of a file in a ZIP archive.
 *  The function follows symbolic links.
 *  @return the modification time of the file.
 *  @exception RANGE_ERROR ''filePath'' does not use the standard path
 *             representation.
 *  @exception FILE_ERROR ''filePath'' is not present in the ZIP archive.
 *)
const func time: getMTime (inout zipArchive: zip, in string: filePath) is func
  result
    var time: modificationTime is time.value;
  local
    var central_file_header: header is central_file_header.value;
    var integer: timestamp is 0;
  begin
    if filePath <> "/" and endsWith(filePath, "/") then
      raise RANGE_ERROR;
    else
      header := followSymlink(zip, filePath);
      if 16#5455 in header.extraFieldMap then
        # Extended Timestamp Extra Field
        timestamp := bytes2Int(header.extraFieldMap[16#5455][2 fixLen 4], UNSIGNED, LE);
        modificationTime := timestamp1970ToTime(timestamp);
      elsif 16#5855 in header.extraFieldMap then
        # Info-ZIP Unix Extra Field (type 1)
        timestamp := bytes2Int(header.extraFieldMap[16#5855][5 fixLen 4], UNSIGNED, LE);
        modificationTime := timestamp1970ToTime(timestamp);
      elsif 16#000d in header.extraFieldMap then
        # UNIX Extra Field
        timestamp := bytes2Int(header.extraFieldMap[16#000d][5 fixLen 4], UNSIGNED, LE);
        modificationTime := timestamp1970ToTime(timestamp);
      elsif 16#000a in header.extraFieldMap then
        # NTFS Extra Field:
        timestamp := bytes2Int(header.extraFieldMap[16#000a][9 fixLen 8], UNSIGNED, LE);
        modificationTime := timestamp1601ToTime(timestamp);
      else
        modificationTime.year   := (header.last_mod_file_date >>  9) + 1980;
        modificationTime.month  := (header.last_mod_file_date >>  5) mod 16;
        modificationTime.day    :=  header.last_mod_file_date        mod 32;
        modificationTime.hour   :=  header.last_mod_file_time >> 11;
        modificationTime.minute := (header.last_mod_file_time >>  5) mod 64;
        modificationTime.second := (header.last_mod_file_time        mod 32) * 2;
        modificationTime := setLocalTZ(modificationTime);
      end if;
    end if;
  end func;


(**
 *  Set the modification time of a file in an ZIP archive.
 *  The function follows symbolic links.
 *  @exception RANGE_ERROR ''filePath'' does not use the standard path
 *             representation.
 *  @exception RANGE_ERROR ''aTime'' is invalid or cannot be
 *             converted to the system file time.
 *  @exception FILE_ERROR ''filePath'' is not present in the ZIP archive.
 *)
const proc: setMTime (inout zipArchive: zip, in string: filePath,
    in time: modificationTime) is func
  local
    var central_file_header: header is central_file_header.value;
    var local_file_header: localHeader is local_file_header.value;
  begin
    if filePath <> "/" and endsWith(filePath, "/") then
      raise RANGE_ERROR;
    else
      header := followSymlink(zip, filePath);
      assignLastModFileTime(header, modificationTime);
      zip.catalog @:= [filePath] header;
      seek(zip.zipFile, zip.register[filePath]);
      writeHead(zip.zipFile, header);
      seek(zip.zipFile, succ(header.relative_offset_of_local_header));
      localHeader := get_local_header(zip.zipFile);
      assignLastModFileTime(localHeader, modificationTime);
      seek(zip.zipFile, succ(header.relative_offset_of_local_header));
      writeHead(zip.zipFile, localHeader);
    end if;
  end func;


(**
 *  Determine the name of the owner (UID) of a file in a ZIP archive.
 *  The function follows symbolic links.
 *  @return the name of the file owner.
 *  @exception RANGE_ERROR ''filePath'' does not use the standard path
 *             representation.
 *  @exception FILE_ERROR ''filePath'' is not present in the ZIP archive, or
 *             the chain of symbolic links is too long.
 *)
const func string: getOwner (inout zipArchive: zip, in string: filePath) is func
  result
    var string: owner is "";
  local
    var central_file_header: header is central_file_header.value;
    var integer: size is 0;
    var integer: uid is 0;
  begin
    if filePath <> "/" and endsWith(filePath, "/") then
      raise RANGE_ERROR;
    else
      header := followSymlink(zip, filePath);
      if 16#000d in header.extraFieldMap then
        # UNIX Extra Field
        uid := bytes2Int(header.extraFieldMap[16#000d][9 fixLen 2], UNSIGNED, LE);
        owner := str(uid);
      elsif 16#756e in header.extraFieldMap then
        # ASi Unix Extra Field:
        uid := bytes2Int(header.extraFieldMap[16#756e][11 fixLen 2], UNSIGNED, LE);
        owner := str(uid);
      elsif 16#7875 in header.extraFieldMap then
        # New Unix Extra Field:
        size := ord(header.extraFieldMap[16#7875][2]);
        uid := bytes2Int(header.extraFieldMap[16#7875][3 fixLen size], UNSIGNED, LE);
        owner := str(uid);
      end if;
    end if;
  end func;


(**
 *  Set the owner of a file in a ZIP archive.
 *  The function follows symbolic links. The ZIP archive format allows
 *  only a numeric UID. The ''owner'' "root" is mapped to the UID 0. Other
 *  ''owner'' names raise a RANGE_ERROR.
 *  @exception RANGE_ERROR ''filePath'' does not use the standard path
 *             representation, or the ''owner'' cannot be mapped to a UID.
 *  @exception FILE_ERROR ''filePath'' is not present in the ZIP archive, or
 *             the chain of symbolic links is too long.
 *)
const proc: setOwner (inout zipArchive: zip, in string: filePath,
    in string: owner) is func
  local
    var integer: uid is 0;
    var central_file_header: header is central_file_header.value;
    var local_file_header: localHeader is local_file_header.value;
  begin
    if isDigitString(owner) then
      uid := integer(owner);
    elsif owner <> "root" then
      raise RANGE_ERROR;
    end if;
    if filePath <> "/" and endsWith(filePath, "/") then
      raise RANGE_ERROR;
    else
      header := followSymlink(zip, filePath);
      assignUserId(header, uid);
      zip.catalog @:= [filePath] header;
      seek(zip.zipFile, zip.register[filePath]);
      writeHead(zip.zipFile, header);
      seek(zip.zipFile, succ(header.relative_offset_of_local_header));
      localHeader := get_local_header(zip.zipFile);
      assignUserId(localHeader, uid);
      seek(zip.zipFile, succ(header.relative_offset_of_local_header));
      writeHead(zip.zipFile, localHeader);
    end if;
  end func;


(**
 *  Determine the name of the group (GID) of a file in a ZIP archive.
 *  The function follows symbolic links.
 *  @return the name of the file group.
 *  @exception RANGE_ERROR ''filePath'' does not use the standard path
 *             representation.
 *  @exception FILE_ERROR ''filePath'' is not present in the ZIP archive, or
 *             the chain of symbolic links is too long.
 *)
const func string: getGroup (inout zipArchive: zip, in string: filePath) is func
  result
    var string: group is "";
  local
    var central_file_header: header is central_file_header.value;
    var integer: pos is 0;
    var integer: size is 0;
    var integer: gid is 0;
  begin
    if filePath <> "/" and endsWith(filePath, "/") then
      raise RANGE_ERROR;
    else
      header := followSymlink(zip, filePath);
      if 16#000d in header.extraFieldMap then
        # UNIX Extra Field
        gid := bytes2Int(header.extraFieldMap[16#000d][11 fixLen 2], UNSIGNED, LE);
        group := str(gid);
      elsif 16#756e in header.extraFieldMap then
        # ASi Unix Extra Field:
        gid := bytes2Int(header.extraFieldMap[16#756e][13 fixLen 2], UNSIGNED, LE);
        group := str(gid);
      elsif 16#7875 in header.extraFieldMap then
        # New Unix Extra Field:
        pos := 3 + ord(header.extraFieldMap[16#7875][2]);
        size := ord(header.extraFieldMap[16#7875][pos]);
        gid := bytes2Int(header.extraFieldMap[16#7875][succ(pos) fixLen size], UNSIGNED, LE);
        group := str(gid);
      end if;
    end if;
  end func;


(**
 *  Set the group of a file in a ZIP archive.
 *  The function follows symbolic links. The ZIP archive format allows
 *  only a numeric GID. The ''group'' "root" is mapped to the GID 0. Other
 *  ''group'' names raise a RANGE_ERROR.
 *  @exception RANGE_ERROR ''filePath'' does not use the standard path
 *             representation, or the ''group'' cannot be mapped to a GID.
 *  @exception FILE_ERROR ''filePath'' is not present in the ZIP archive, or
 *             the chain of symbolic links is too long.
 *)
const proc: setGroup (inout zipArchive: zip, in string: filePath,
    in string: group) is func
  local
    var integer: gid is 0;
    var central_file_header: header is central_file_header.value;
    var local_file_header: localHeader is local_file_header.value;
  begin
    if isDigitString(group) then
      gid := integer(group);
    elsif group <> "root" then
      raise RANGE_ERROR;
    end if;
    if filePath <> "/" and endsWith(filePath, "/") then
      raise RANGE_ERROR;
    else
      header := followSymlink(zip, filePath);
      assignGroupId(header, gid);
      zip.catalog @:= [filePath] header;
      seek(zip.zipFile, zip.register[filePath]);
      writeHead(zip.zipFile, header);
      seek(zip.zipFile, succ(header.relative_offset_of_local_header));
      localHeader := get_local_header(zip.zipFile);
      assignGroupId(localHeader, gid);
      seek(zip.zipFile, succ(header.relative_offset_of_local_header));
      writeHead(zip.zipFile, localHeader);
    end if;
  end func;


(**
 *  Determine the file mode (permissions) of a symbolic link in a ZIP archive.
 *  The function only works for symbolic links and does not follow the
 *  symbolic link.
 *  @return the file mode.
 *  @exception RANGE_ERROR ''filePath'' does not use the standard path
 *             representation.
 *  @exception FILE_ERROR The file described with ''filePath'' is not
 *             present in the ZIP archive, or it is not a symbolic link.
 *)
const func fileMode: getFileMode (inout zipArchive: zip, in string: filePath, SYMLINK) is func
  result
    var fileMode: mode is fileMode.value;
  local
    var central_file_header: header is central_file_header.value;
  begin
    # writeln("getFileMode: " <& filePath);
    if filePath <> "/" and endsWith(filePath, "/") then
      raise RANGE_ERROR;
    elsif filePath = "" then
      raise FILE_ERROR;
    else
      if filePath in zip.catalog then
        header := zip.catalog[filePath];
      elsif filePath in zip.register then
        header := addToCatalog(zip, filePath);
      else
        raise FILE_ERROR;
      end if;
      if isSymlink(header) then
        mode := fileMode((header.external_file_attributes >> 16) mod 8#1000);
      else
        raise FILE_ERROR;
      end if;
    end if;
  end func;


(**
 *  Determine the modification time of a symbolic link in a ZIP archive.
 *  The function only works for symbolic links and does not follow the
 *  symbolic link.
 *  @return the modification time of the symbolic link.
 *  @exception RANGE_ERROR ''filePath'' does not use the standard path
 *             representation.
 *  @exception FILE_ERROR The file described with ''filePath'' is not
 *             present in the ZIP archive, or it is not a symbolic link.
 *)
const func time: getMTime (inout zipArchive: zip, in string: filePath, SYMLINK) is func
  result
    var time: modificationTime is time.value;
  local
    var central_file_header: header is central_file_header.value;
    var integer: timestamp is 0;
  begin
    # writeln("getMTime: " <& filePath);
    if filePath <> "/" and endsWith(filePath, "/") then
      raise RANGE_ERROR;
    elsif filePath = "" then
      raise FILE_ERROR;
    else
      if filePath in zip.catalog then
        header := zip.catalog[filePath];
      elsif filePath in zip.register then
        header := addToCatalog(zip, filePath);
      else
        raise FILE_ERROR;
      end if;
      if isSymlink(header) then
        if 16#5455 in header.extraFieldMap then
          # Extended Timestamp Extra Field
          timestamp := bytes2Int(header.extraFieldMap[16#5455][2 fixLen 4], UNSIGNED, LE);
          modificationTime := timestamp1970ToTime(timestamp);
        elsif 16#5855 in header.extraFieldMap then
          # Info-ZIP Unix Extra Field (type 1)
          timestamp := bytes2Int(header.extraFieldMap[16#5855][5 fixLen 4], UNSIGNED, LE);
          modificationTime := timestamp1970ToTime(timestamp);
        elsif 16#000d in header.extraFieldMap then
          # UNIX Extra Field
          timestamp := bytes2Int(header.extraFieldMap[16#000d][5 fixLen 4], UNSIGNED, LE);
          modificationTime := timestamp1970ToTime(timestamp);
        elsif 16#000a in header.extraFieldMap then
          # NTFS Extra Field:
          timestamp := bytes2Int(header.extraFieldMap[16#000a][9 fixLen 8], UNSIGNED, LE);
          modificationTime := timestamp1601ToTime(timestamp);
        else
          modificationTime.year   := (header.last_mod_file_date >>  9) + 1980;
          modificationTime.month  := (header.last_mod_file_date >>  5) mod 16;
          modificationTime.day    :=  header.last_mod_file_date        mod 32;
          modificationTime.hour   :=  header.last_mod_file_time >> 11;
          modificationTime.minute := (header.last_mod_file_time >>  5) mod 64;
          modificationTime.second := (header.last_mod_file_time        mod 32) * 2;
          modificationTime := setLocalTZ(modificationTime);
        end if;
      else
        raise FILE_ERROR;
      end if;
    end if;
  end func;


(**
 *  Set the modification time of a symbolic link in a ZIP archive.
 *  The function only works for symbolic links and does not follow the
 *  symbolic link.
 *  @exception RANGE_ERROR ''filePath'' does not use the standard path
 *             representation.
 *  @exception RANGE_ERROR ''modificationTime'' is invalid or it cannot be
 *             converted to the system file time.
 *  @exception FILE_ERROR The file described with ''filePath'' is not
 *             present in the ZIP archive, or it is not a symbolic link.
 *)
const proc: setMTime (inout zipArchive: zip, in string: filePath,
    in time: modificationTime, SYMLINK) is func
  local
    var central_file_header: header is central_file_header.value;
    var local_file_header: localHeader is local_file_header.value;
  begin
    if filePath <> "/" and endsWith(filePath, "/") then
      raise RANGE_ERROR;
    elsif filePath = "" then
      raise FILE_ERROR;
    else
      if filePath in zip.catalog then
        header := zip.catalog[filePath];
      elsif filePath in zip.register then
        header := addToCatalog(zip, filePath);
      else
        raise FILE_ERROR;
      end if;
      if isSymlink(header) then
        assignLastModFileTime(header, modificationTime);
        zip.catalog @:= [filePath] header;
        seek(zip.zipFile, zip.register[filePath]);
        writeHead(zip.zipFile, header);
        seek(zip.zipFile, succ(header.relative_offset_of_local_header));
        localHeader := get_local_header(zip.zipFile);
        assignLastModFileTime(localHeader, modificationTime);
        seek(zip.zipFile, succ(header.relative_offset_of_local_header));
        writeHead(zip.zipFile, localHeader);
      else
        raise FILE_ERROR;
      end if;
    end if;
  end func;


(**
 *  Determine the name of the owner (UID) of a symbolic link in a ZIP archive.
 *  The function only works for symbolic links and does not follow the
 *  symbolic link.
 *  @return the name of the file owner.
 *  @exception RANGE_ERROR ''filePath'' does not use the standard path
 *             representation.
 *  @exception FILE_ERROR The file described with ''filePath'' is not
 *             present in the ZIP archive, or it is not a symbolic link.
 *)
const func string: getOwner (inout zipArchive: zip, in string: filePath, SYMLINK) is func
  result
    var string: owner is "";
  local
    var central_file_header: header is central_file_header.value;
    var integer: size is 0;
    var integer: uid is 0;
  begin
    # writeln("getOwner: " <& filePath);
    if filePath <> "/" and endsWith(filePath, "/") then
      raise RANGE_ERROR;
    elsif filePath = "" then
      raise FILE_ERROR;
    else
      if filePath in zip.catalog then
        header := zip.catalog[filePath];
      elsif filePath in zip.register then
        header := addToCatalog(zip, filePath);
      else
        raise FILE_ERROR;
      end if;
      if isSymlink(header) then
        if 16#000d in header.extraFieldMap then
          # UNIX Extra Field
          uid := bytes2Int(header.extraFieldMap[16#000d][9 fixLen 2], UNSIGNED, LE);
          owner := str(uid);
        elsif 16#756e in header.extraFieldMap then
          # ASi Unix Extra Field:
          uid := bytes2Int(header.extraFieldMap[16#756e][11 fixLen 2], UNSIGNED, LE);
          owner := str(uid);
        elsif 16#7875 in header.extraFieldMap then
          # New Unix Extra Field:
          size := ord(header.extraFieldMap[16#7875][2]);
          uid := bytes2Int(header.extraFieldMap[16#7875][3 fixLen size], UNSIGNED, LE);
          owner := str(uid);
        end if;
      else
        raise FILE_ERROR;
      end if;
    end if;
  end func;


(**
 *  Set the owner of a symbolic link in a ZIP archive.
 *  The function only works for symbolic links and does not follow the
 *  symbolic link. The ZIP archive format allows only a numeric UID.
 *  The ''owner'' "root" is mapped to the UID 0. Other ''owner'' names
 *  raise a RANGE_ERROR.
 *  @exception RANGE_ERROR ''filePath'' does not use the standard path
 *             representation, or the ''owner'' cannot be mapped to a UID.
 *  @exception FILE_ERROR The file described with ''filePath'' is not
 *             present in the ZIP archive, or it is not a symbolic link.
 *)
const proc: setOwner (inout zipArchive: zip, in string: filePath,
    in string: owner, SYMLINK) is func
  local
    var integer: uid is 0;
    var central_file_header: header is central_file_header.value;
    var local_file_header: localHeader is local_file_header.value;
  begin
    if isDigitString(owner) then
      uid := integer(owner);
    elsif owner <> "root" then
      raise RANGE_ERROR;
    end if;
    if filePath <> "/" and endsWith(filePath, "/") then
      raise RANGE_ERROR;
    elsif filePath = "" then
      raise FILE_ERROR;
    else
      if filePath in zip.catalog then
        header := zip.catalog[filePath];
      elsif filePath in zip.register then
        header := addToCatalog(zip, filePath);
      else
        raise FILE_ERROR;
      end if;
      if isSymlink(header) then
        assignUserId(header, uid);
        zip.catalog @:= [filePath] header;
        seek(zip.zipFile, zip.register[filePath]);
        writeHead(zip.zipFile, header);
        seek(zip.zipFile, succ(header.relative_offset_of_local_header));
        localHeader := get_local_header(zip.zipFile);
        assignUserId(localHeader, uid);
        seek(zip.zipFile, succ(header.relative_offset_of_local_header));
        writeHead(zip.zipFile, localHeader);
      else
        raise FILE_ERROR;
      end if;
    end if;
  end func;


(**
 *  Determine the name of the group (GID) of a symbolic link in a ZIP archive.
 *  The function only works for symbolic links and does not follow the
 *  symbolic link.
 *  @return the name of the file group.
 *  @exception RANGE_ERROR ''filePath'' does not use the standard path
 *             representation.
 *  @exception FILE_ERROR The file described with ''filePath'' is not
 *             present in the ZIP archive, or it is not a symbolic link.
 *)
const func string: getGroup (inout zipArchive: zip, in string: filePath, SYMLINK) is func
  result
    var string: group is "";
  local
    var central_file_header: header is central_file_header.value;
    var integer: pos is 0;
    var integer: size is 0;
    var integer: gid is 0;
  begin
    # writeln("getGroup: " <& filePath);
    if filePath <> "/" and endsWith(filePath, "/") then
      raise RANGE_ERROR;
    elsif filePath = "" then
      raise FILE_ERROR;
    else
      if filePath in zip.catalog then
        header := zip.catalog[filePath];
      elsif filePath in zip.register then
        header := addToCatalog(zip, filePath);
      else
        raise FILE_ERROR;
      end if;
      if isSymlink(header) then
        if 16#000d in header.extraFieldMap then
          # UNIX Extra Field
          gid := bytes2Int(header.extraFieldMap[16#000d][11 fixLen 2], UNSIGNED, LE);
          group := str(gid);
        elsif 16#756e in header.extraFieldMap then
          # ASi Unix Extra Field:
          gid := bytes2Int(header.extraFieldMap[16#756e][13 fixLen 2], UNSIGNED, LE);
          group := str(gid);
        elsif 16#7875 in header.extraFieldMap then
          # New Unix Extra Field:
          pos := 3 + ord(header.extraFieldMap[16#7875][2]);
          size := ord(header.extraFieldMap[16#7875][pos]);
          gid := bytes2Int(header.extraFieldMap[16#7875][succ(pos) fixLen size], UNSIGNED, LE);
          group := str(gid);
        end if;
      else
        raise FILE_ERROR;
      end if;
    end if;
  end func;


(**
 *  Set the group of a symbolic link in a ZIP archive.
 *  The function only works for symbolic links and does not follow the
 *  symbolic link. The ZIP archive format allows only a numeric GID.
 *  The ''group'' "root" is mapped to the GID 0. Other ''group'' names
 *  raise a RANGE_ERROR.
 *  @exception RANGE_ERROR ''filePath'' does not use the standard path
 *             representation, or the ''group'' cannot be mapped to a GID.
 *  @exception FILE_ERROR The file described with ''filePath'' is not
 *             present in the ZIP archive, or it is not a symbolic link.
 *)
const proc: setGroup (inout zipArchive: zip, in string: filePath,
    in string: group, SYMLINK) is func
  local
    var integer: gid is 0;
    var central_file_header: header is central_file_header.value;
    var local_file_header: localHeader is local_file_header.value;
  begin
    if isDigitString(group) then
      gid := integer(group);
    elsif group <> "root" then
      raise RANGE_ERROR;
    end if;
    if filePath <> "/" and endsWith(filePath, "/") then
      raise RANGE_ERROR;
    elsif filePath = "" then
      raise FILE_ERROR;
    else
      if filePath in zip.catalog then
        header := zip.catalog[filePath];
      elsif filePath in zip.register then
        header := addToCatalog(zip, filePath);
      else
        raise FILE_ERROR;
      end if;
      if isSymlink(header) then
        assignGroupId(header, gid);
        zip.catalog @:= [filePath] header;
        seek(zip.zipFile, zip.register[filePath]);
        writeHead(zip.zipFile, header);
        seek(zip.zipFile, succ(header.relative_offset_of_local_header));
        localHeader := get_local_header(zip.zipFile);
        assignGroupId(localHeader, gid);
        seek(zip.zipFile, succ(header.relative_offset_of_local_header));
        writeHead(zip.zipFile, localHeader);
      else
        raise FILE_ERROR;
      end if;
    end if;
  end func;


(**
 *  Reads the destination of a symbolic link in a ZIP archive.
 *  @return The destination referred by the symbolic link.
 *  @exception RANGE_ERROR ''filePath'' does not use the standard path
 *             representation.
 *  @exception FILE_ERROR ''filePath'' is not present in the ZIP archive,
 *             or is not a symbolic link.
 *)
const func string: readLink (inout zipArchive: zip, in string: filePath) is func
  result
    var string: linkPath is "";
  local
    var central_file_header: header is central_file_header.value;
    var local_file_header: localHeader is local_file_header.value;
    var string: linkPath8 is "";
    var bin32: crc_32 is bin32(0);
  begin
    if filePath <> "/" and endsWith(filePath, "/") then
      raise RANGE_ERROR;
    elsif filePath in zip.catalog then
      header := zip.catalog[filePath];
    elsif filePath in zip.register then
      header := addToCatalog(zip, filePath);
    else
      raise FILE_ERROR;
    end if;
    if isSymlink(header) then
      seek(zip.zipFile, succ(header.relative_offset_of_local_header));
      localHeader := get_local_header(zip.zipFile);
      # write(localHeader);
      if localHeader.compression_method = 0 then
        # The link destination is stored (no compression).
        linkPath8 := gets(zip.zipFile, localHeader.compressed_size);
      else
        # writeln("unsupported compression method: " <& localHeader.compression_method);
        raise FILE_ERROR;
      end if;
      crc_32 := crc32(linkPath8);
      if localHeader.crc_32 <> crc_32 or
          localHeader.uncompressed_size <> length(linkPath8) or
          header.crc_32 <> crc_32 or
          header.uncompressed_size <> length(linkPath8) then
        raise FILE_ERROR;
      end if;
      block
        linkPath := fromUtf8(linkPath8);
      exception
        catch RANGE_ERROR:
          linkPath := linkPath8;
      end block;
    else
      raise FILE_ERROR;
    end if;
  end func;


(**
 *  Create a symbolic link in a ZIP archive.
 *  The symbolic link ''symlinkPath'' will refer to ''targetPath'' afterwards.
 *  The function does not follow symbolic links.
 *  @param zip Open ZIP archive.
 *  @param symlinkPath Name of the symbolic link to be created.
 *  @param targetPath String to be contained in the symbolic link.
 *  @exception RANGE_ERROR ''targetPath'' or ''symlinkPath'' does not use the
 *             standard path representation.
 *  @exception FILE_ERROR A system function returns an error.
 *)
const proc: makeLink (inout zipArchive: zip, in string: symlinkPath,
    in string: targetPath) is func
  local
    var central_file_header: header is central_file_header.value;
    var local_file_header: localHeader is local_file_header.value;
    var string: symlinkPath8 is "";
    var string: targetPath8 is "";
    var integer: roomForNewFile is 0;
  begin
    # writeln("makeLink: " <& literal(symlinkPath) <& " " <& literal(targetPath));
    if symlinkPath <> "/" and endsWith(symlinkPath, "/") then
      raise RANGE_ERROR;
    elsif symlinkPath = "" or symlinkPath in zip.catalog or
        symlinkPath in zip.register or implicitDir(zip.register, symlinkPath) then
      raise FILE_ERROR;
    else
      symlinkPath8 := toUtf8(symlinkPath);
      targetPath8 := toUtf8(targetPath);
      header.signature := ZIP_CENTRAL_HEADER_SIGNATURE;
      header.version_made_by            := (ZIP_HOST_SYSTEM_UNIX << 8) + 16#1e;
      header.version_needed_to_extract  := 10;
      if symlinkPath8 <> symlinkPath then
        header.general_purpose_bit_flag := ZIP_FILE_NAME_IS_UTF8;
      else
        header.general_purpose_bit_flag := bin32(0);
      end if;
      header.compression_method         := 0;
      header.crc_32                     := crc32(targetPath8);
      header.compressed_size            := length(targetPath8);
      header.uncompressed_size          := length(targetPath8);
      header.disk_number_start          := 0;
      header.internal_file_attributes   := 0;
      # The unix mode is in the high 16 bits of the attributes.
      header.external_file_attributes   := (ord(MODE_FILE_SYMLINK) + 8#777) << 16;
      header.file_name                  := symlinkPath8;
      initLastModFileTime(header, time(NOW));
      header.file_comment               := "";
      header.relative_offset_of_local_header := pred(zip.startOfCentralDirPos);
      roomForNewFile := ZIP_LOCAL_HEADER_FIXED_SIZE + length(header.file_name) +
          length(header.extra_field) + header.compressed_size;
      insertArea(zip.zipFile, zip.startOfCentralDirPos, roomForNewFile);
      fixRegisterAndCatalog(zip, zip.startOfCentralDirPos, roomForNewFile);
      localHeader := toLocalHeader(header);
      seek(zip.zipFile, succ(header.relative_offset_of_local_header));
      writeHead(zip.zipFile, localHeader);
      write(zip.zipFile, targetPath8);
      seek(zip.zipFile, zip.endOfCentralDirPos);
      zip.register @:= [symlinkPath] zip.endOfCentralDirPos;
      writeHead(zip.zipFile, header);
      zip.catalog @:= [symlinkPath] header;
      zip.endOfCentralDirPos := tell(zip.zipFile);
      incr(zip.endOfCentralDir.entries_in_central_directory_on_this_disk);
      incr(zip.endOfCentralDir.entries_in_central_directory);
      zip.endOfCentralDir.size_of_central_directory := zip.endOfCentralDirPos - zip.startOfCentralDirPos;
      zip.endOfCentralDir.offset_of_start_of_central_directory := pred(zip.startOfCentralDirPos);
      write(zip.zipFile, str(zip.endOfCentralDir));
    end if;
  end func;


(**
 *  Get the contents of a file in a ZIP archive.
 *  The function follows symbolic links.
 *  @return the specified file as string.
 *  @exception RANGE_ERROR ''filePath'' does not use the standard path
 *             representation.
 *  @exception FILE_ERROR ''filePath'' is not present in the ZIP archive,
 *             or the crc-32 checksum is not okay.
 *)
const func string: getFile (inout zipArchive: zip, in string: filePath) is func
  result
    var string: content is "";
  local
    var central_file_header: header is central_file_header.value;
    var local_file_header: localHeader is local_file_header.value;
    var integer: dataDescriptorSize is 0;
    var integer: signaturePos is 0;
    var string: stri is "";
    var bin32: crc_32 is bin32(0);
  begin
    if filePath <> "/" and endsWith(filePath, "/") then
      raise RANGE_ERROR;
    else
      header := followSymlink(zip, filePath);
      if isRegularFile(header) then
        seek(zip.zipFile, succ(header.relative_offset_of_local_header));
        localHeader := get_local_header(zip.zipFile);
        # write(localHeader);
        if localHeader.compression_method = 0 then
          # The file is stored (no compression).
          content := gets(zip.zipFile, localHeader.compressed_size);
        elsif localHeader.compression_method = 8 then
          # The file is Deflated.
          if localHeader.general_purpose_bit_flag & ZIP_HAS_DATA_DESCRIPTOR <> bin32(0) then
            # The fields crc_32, compressed_size and uncompressed_size are 0.
            # Instead there is a data descriptor after the compressed data.
            content := inflate(zip.zipFile);
            dataDescriptorSize := 16#0001 in header.extraFieldMap ? 20 : 12;
            stri := gets(zip.zipFile, 4);
            if stri = ZIP_DATA_DESCRIPTOR_SIGNATURE then
              stri := gets(zip.zipFile, dataDescriptorSize);
            else
              stri &:= gets(zip.zipFile, dataDescriptorSize - 4);
              if length(stri) = dataDescriptorSize then
                signaturePos := pos(stri, ZIP_DATA_DESCRIPTOR_SIGNATURE);
                if signaturePos <> 0 then
                  stri := stri[signaturePos + 4 .. ] &
                          gets(zip.zipFile, signaturePos + 3);
                end if;
              end if;
            end if;
            if length(stri) = dataDescriptorSize then
              if dataDescriptorSize = 12 then
                localHeader.crc_32            := bin32(bytes2Int(stri[1 fixLen 4], UNSIGNED, LE));
                localHeader.compressed_size   :=       bytes2Int(stri[5 fixLen 4], UNSIGNED, LE);
                localHeader.uncompressed_size :=       bytes2Int(stri[9 fixLen 4], UNSIGNED, LE);
              else
                localHeader.crc_32            := bin32(bytes2Int(stri[1 fixLen 4], UNSIGNED, LE));
                localHeader.compressed_size   :=       bytes2Int(stri[5 fixLen 8], UNSIGNED, LE);
                localHeader.uncompressed_size :=       bytes2Int(stri[9 fixLen 8], UNSIGNED, LE);
              end if;
            else
              raise RANGE_ERROR;
            end if;
          else
            content := gets(zip.zipFile, localHeader.compressed_size);
            content := inflate(content);
          end if;
        else
          # writeln("unsupported compression method: " <& localHeader.compression_method);
          raise FILE_ERROR;
        end if;
        crc_32 := crc32(content);
        if localHeader.crc_32 <> crc_32 or
            localHeader.uncompressed_size <> length(content) or
            header.crc_32 <> crc_32 or
            header.uncompressed_size <> length(content) then
          raise FILE_ERROR;
        end if;
      else
        raise FILE_ERROR;
      end if;
    end if;
  end func;


(**
 *  Write ''data'' to a ZIP archive with the given ''filePath''.
 *  If the file exists already, it is overwritten.
 *  @exception RANGE_ERROR ''filePath'' does not use the standard path
 *             representation.
 *)
const proc: putFile (inout zipArchive: zip, in string: filePath,
    in string: data) is func
  local
    var central_file_header: header is central_file_header.value;
    var local_file_header: localHeader is local_file_header.value;
    var boolean: fileExists is TRUE;
    var time: modificationTime is time.value;
    var integer: oldSize is 0;
    var integer: newSize is 0;
    var integer: localHeaderPos is 0;
    var string: filePath8 is "";
    var string: compressed is "";
    var integer: roomForNewFile is 0;
  begin
    if filePath = "" or filePath <> "/" and endsWith(filePath, "/") then
      raise RANGE_ERROR;
    elsif filePath in zip.catalog then
      header := zip.catalog[filePath];
    elsif filePath in zip.register then
      header := addToCatalog(zip, filePath);
    elsif implicitDir(zip.register, filePath) then
      raise FILE_ERROR;
    else
      fileExists := FALSE;
    end if;
    compressed := deflate(data);
    oldSize := header.compressed_size;
    header.crc_32 := crc32(data);
    header.uncompressed_size := length(data);
    if length(compressed) >= length(data) then
      header.compression_method := 0;
      header.compressed_size := length(data);
      compressed := data;
    else
      header.compression_method := 8;
      header.compressed_size := length(compressed);
    end if;
    if fileExists then
      if endsWith(header.file_name, "/") then
        raise FILE_ERROR;
      else
        modificationTime := time(NOW);
        assignLastModFileTime(header, modificationTime);
        localHeaderPos := succ(header.relative_offset_of_local_header);
        seek(zip.zipFile, localHeaderPos);
        localHeader := get_local_header(zip.zipFile);
        newSize := header.compressed_size;
        # writeln("oldSize: " <& oldSize);
        # writeln("newSize: " <& newSize);
        if newSize > oldSize then
          insertArea(zip.zipFile, localHeaderPos, newSize - oldSize);
          fixRegisterAndCatalog(zip, localHeaderPos, newSize - oldSize);
        elsif newSize < oldSize then
          deleteArea(zip.zipFile, localHeaderPos, oldSize - newSize);
          fixRegisterAndCatalog(zip, localHeaderPos + (oldSize - newSize),
                                newSize - oldSize);
        end if;
        # Local header and file data are rewritten in place.
        updateLocalHeader(localHeader, header);
        assignLastModFileTime(localHeader, modificationTime);
        seek(zip.zipFile, localHeaderPos);
        writeHead(zip.zipFile, localHeader);
        write(zip.zipFile, compressed);
        zip.catalog @:= [filePath] header;
        seek(zip.zipFile, zip.register[filePath]);
        writeHead(zip.zipFile, header);
        zip.endOfCentralDir.offset_of_start_of_central_directory := pred(zip.startOfCentralDirPos);
        seek(zip.zipFile, zip.endOfCentralDirPos);
        write(zip.zipFile, str(zip.endOfCentralDir));
        flush(zip.zipFile);
      end if;
    else
      filePath8 := toUtf8(filePath);
      header.signature := ZIP_CENTRAL_HEADER_SIGNATURE;
      header.version_made_by            := (ZIP_HOST_SYSTEM_UNIX << 8) + 16#1e;
      header.version_needed_to_extract  := 10;
      if filePath8 <> filePath then
        header.general_purpose_bit_flag := ZIP_FILE_NAME_IS_UTF8;
      else
        header.general_purpose_bit_flag := bin32(0);
      end if;
      header.disk_number_start          := 0;
      header.internal_file_attributes   := 0;
      # The unix mode is in the high 16 bits of the attributes.
      header.external_file_attributes   := (ord(MODE_FILE_REGULAR) + 8#664) << 16;
      header.file_name                  := filePath8;
      initLastModFileTime(header, time(NOW));
      header.file_comment               := "";
      header.relative_offset_of_local_header := pred(zip.startOfCentralDirPos);
      roomForNewFile := ZIP_LOCAL_HEADER_FIXED_SIZE + length(header.file_name) +
          length(header.extra_field) + header.compressed_size;
      insertArea(zip.zipFile, zip.startOfCentralDirPos, roomForNewFile);
      fixRegisterAndCatalog(zip, zip.startOfCentralDirPos, roomForNewFile);
      localHeader := toLocalHeader(header);
      seek(zip.zipFile, succ(header.relative_offset_of_local_header));
      writeHead(zip.zipFile, localHeader);
      write(zip.zipFile, compressed);
      seek(zip.zipFile, zip.endOfCentralDirPos);
      zip.register @:= [filePath] zip.endOfCentralDirPos;
      writeHead(zip.zipFile, header);
      zip.catalog @:= [filePath] header;
      zip.endOfCentralDirPos := tell(zip.zipFile);
      incr(zip.endOfCentralDir.entries_in_central_directory_on_this_disk);
      incr(zip.endOfCentralDir.entries_in_central_directory);
      zip.endOfCentralDir.size_of_central_directory := zip.endOfCentralDirPos - zip.startOfCentralDirPos;
      zip.endOfCentralDir.offset_of_start_of_central_directory := pred(zip.startOfCentralDirPos);
      write(zip.zipFile, str(zip.endOfCentralDir));
    end if;
  end func;


(**
 *  Create a new directory in a ZIP archive.
 *  The function does not follow symbolic links.
 *  @param zip Open ZIP archive.
 *  @param dirPath Name of the directory to be created.
 *  @exception RANGE_ERROR ''dirPath'' does not use the standard path
 *             representation.
 *  @exception FILE_ERROR The file ''dirPath'' already exists.
 *)
const proc: makeDir (inout zipArchive: zip, in string: dirPath) is func
  local
    var central_file_header: header is central_file_header.value;
    var local_file_header: localHeader is local_file_header.value;
    var boolean: fileExists is TRUE;
    var integer: relative_offset_of_local_header is 0;
    var string: dirPath8 is "";
    var integer: roomForNewFile is 0;
  begin
    if dirPath = "" or dirPath <> "/" and endsWith(dirPath, "/") then
      raise RANGE_ERROR;
    elsif dirPath in zip.catalog then
      relative_offset_of_local_header := zip.catalog[dirPath].relative_offset_of_local_header;
    elsif dirPath in zip.register then
      relative_offset_of_local_header := addToCatalog(zip, dirPath).relative_offset_of_local_header;
    elsif implicitDir(zip.register, dirPath) then
      relative_offset_of_local_header := addImplicitDir(zip, dirPath).relative_offset_of_local_header;
    else
      fileExists := FALSE;
    end if;
    if fileExists and relative_offset_of_local_header <> -1 then
      # The file exists and it is not an implicit directory.
      raise FILE_ERROR;
    else
      dirPath8 := toUtf8(dirPath);
      header.signature := ZIP_CENTRAL_HEADER_SIGNATURE;
      header.version_made_by            := (ZIP_HOST_SYSTEM_UNIX << 8) + 16#1e;
      header.version_needed_to_extract  := 10;
      if dirPath8 <> dirPath then
        header.general_purpose_bit_flag := ZIP_FILE_NAME_IS_UTF8;
      else
        header.general_purpose_bit_flag := bin32(0);
      end if;
      header.compression_method         := 0;
      header.compressed_size            := 0;
      header.uncompressed_size          := 0;
      header.disk_number_start          := 0;
      header.internal_file_attributes   := 0;
      # The unix mode is in the high 16 bits of the attributes.
      header.external_file_attributes   := (ord(MODE_FILE_DIR) + 8#775) << 16;
      header.file_name                  := dirPath8 & "/";
      initLastModFileTime(header, time(NOW));
      header.file_comment               := "";
      header.relative_offset_of_local_header := pred(zip.startOfCentralDirPos);
      roomForNewFile := ZIP_LOCAL_HEADER_FIXED_SIZE + length(header.file_name) +
          length(header.extra_field);
      insertArea(zip.zipFile, zip.startOfCentralDirPos, roomForNewFile);
      fixRegisterAndCatalog(zip, zip.startOfCentralDirPos, roomForNewFile);
      localHeader := toLocalHeader(header);
      seek(zip.zipFile, succ(header.relative_offset_of_local_header));
      writeHead(zip.zipFile, localHeader);
      seek(zip.zipFile, zip.endOfCentralDirPos);
      zip.register @:= [dirPath] zip.endOfCentralDirPos;
      writeHead(zip.zipFile, header);
      zip.catalog @:= [dirPath] header;
      zip.endOfCentralDirPos := tell(zip.zipFile);
      incr(zip.endOfCentralDir.entries_in_central_directory_on_this_disk);
      incr(zip.endOfCentralDir.entries_in_central_directory);
      zip.endOfCentralDir.size_of_central_directory := zip.endOfCentralDirPos - zip.startOfCentralDirPos;
      zip.endOfCentralDir.offset_of_start_of_central_directory := pred(zip.startOfCentralDirPos);
      write(zip.zipFile, str(zip.endOfCentralDir));
    end if;
  end func;


(**
 *  Remove any file except non-empty directories from a ZIP archive.
 *  The function does not follow symbolic links. An attempt to remove a
 *  directory that is not empty triggers FILE_ERROR.
 *  @param zip Open ZIP archive.
 *  @param filePath Name of the file to be removed.
 *  @exception RANGE_ERROR ''filePath'' does not use the standard path
 *             representation.
 *  @exception FILE_ERROR The file does not exist or it is a directory
 *             that is not empty.
 *)
const proc: removeFile (inout zipArchive: zip, in string: filePath) is func
  local
    var central_file_header: header is central_file_header.value;
    var local_file_header: localHeader is local_file_header.value;
    var boolean: fileExists is TRUE;
    var integer: posOfHeaderToBeRemoved is 0;
    var integer: numCharsToBeRemoved is 0;
  begin
    # writeln("removeFile(" <& literal(filePath) <& ")");
    if filePath <> "/" and endsWith(filePath, "/") then
      raise RANGE_ERROR;
    elsif filePath in zip.catalog then
      header := zip.catalog[filePath];
    elsif filePath in zip.register then
      header := addToCatalog(zip, filePath);
    elsif implicitDir(zip.register, filePath) then
      header := addImplicitDir(zip, filePath);
    else
      fileExists := FALSE;
    end if;
    if fileExists and
        (not endsWith(header.file_name, "/") or
         isEmptyDir(zip.register, filePath)) then
      # Remove local header and file content.
      posOfHeaderToBeRemoved := succ(header.relative_offset_of_local_header);
      numCharsToBeRemoved := ZIP_LOCAL_HEADER_FIXED_SIZE + length(header.file_name) +
        length(header.extra_field) + header.compressed_size;
      # writeln("numCharsToBeRemoved: " <& numCharsToBeRemoved);
      deleteArea(zip.zipFile, posOfHeaderToBeRemoved, numCharsToBeRemoved);
      fixRegisterAndCatalog(zip, posOfHeaderToBeRemoved + numCharsToBeRemoved,
                            -numCharsToBeRemoved);
      # Remove central header.
      posOfHeaderToBeRemoved := zip.register[filePath];
      numCharsToBeRemoved := ZIP_CENTRAL_HEADER_FIXED_SIZE + length(header.file_name) +
        length(header.extra_field) + length(header.file_comment);
      # writeln("numCharsToBeRemoved: " <& numCharsToBeRemoved);
      deleteArea(zip.zipFile, posOfHeaderToBeRemoved, numCharsToBeRemoved);
      excl(zip.register, filePath);
      excl(zip.catalog, filePath);
      fixRegisterAndCatalog(zip, posOfHeaderToBeRemoved + numCharsToBeRemoved,
                            -numCharsToBeRemoved);
      decr(zip.endOfCentralDir.entries_in_central_directory_on_this_disk);
      decr(zip.endOfCentralDir.entries_in_central_directory);
      zip.endOfCentralDir.size_of_central_directory := zip.endOfCentralDirPos - zip.startOfCentralDirPos;
      zip.endOfCentralDir.offset_of_start_of_central_directory := pred(zip.startOfCentralDirPos);
      seek(zip.zipFile, zip.endOfCentralDirPos);
      write(zip.zipFile, str(zip.endOfCentralDir));
      flush(zip.zipFile);
    else
      raise FILE_ERROR;
    end if;
  end func;


const func string: getZipContent (in string: zipFilePath, in string: filePath) is func
  result
    var string: content is "";
  local
    var fileSys: zip is fileSys.value;
  begin
    zip := openZip(zipFilePath);
    if zip <> fileSys.value then
      content := getFile(zip, filePath);
      close(zip);
    end if;
  end func;


(**
 *  For-loop which loops recursively over the paths in a ZIP archive.
 *)
const proc: for (inout string: filePath) range (inout zipArchive: zip) do
              (in proc: statements)
            end for is func
  begin
    for key filePath range zip.register do
      statements;
    end for;
  end func;