
(********************************************************************)
(*                                                                  *)
(*  zip.s7i       Zip compression support library                   *)
(*  Copyright (C) 2009  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 "inflate.s7i";
include "bytedata.s7i";


const func integer: rpos (inout file: inFile, in string: stri) is func
  result
    var integer: posFound is 0;
  local
    var integer: pos is 0;
    var string: test is "";
  begin
    pos := length(inFile) - length(stri) + 1;
    while pos > 0 and posFound = 0 do
      seek(inFile, pos);
      test := gets(inFile, length(stri));
      # writeln(literal(test));
      if test = stri then
        posFound := pos;
      end if;
      decr(pos);
    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 integer: general_purpose_bit_flag   is 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 0;   # 2 bytes
    var integer: crc_32                     is 0;   # 4 bytes
    var integer: compressed_size            is 0;   # 4 bytes
    var integer: uncompressed_size          is 0;   # 4 bytes
    var integer: file_name_length           is 0;   # 2 bytes
    var integer: extra_field_length         is 0;   # 2 bytes
    var string:  file_name                  is "";  # variable size
    var string:  extra_field                is "";  # variable size
  end struct;


const func local_file_header: get_local_header (inout file: inFile) is func
  result
    var local_file_header: header is local_file_header.value;
  begin
    header.signature := gets(inFile, 4);
    if header.signature = "PK\3;\4;" then
      header.version_needed_to_extract   := getUInt16Le(inFile);
      header.general_purpose_bit_flag    := getUInt16Le(inFile);
      header.compression_method          := getUInt16Le(inFile);
      header.last_mod_file_time          := getUInt16Le(inFile);
      header.last_mod_file_date          := getUInt16Le(inFile);
      header.crc_32                      := getUInt32Le(inFile);
      header.compressed_size             := getUInt32Le(inFile);
      header.uncompressed_size           := getUInt32Le(inFile);
      header.file_name_length            := getUInt16Le(inFile);
      header.extra_field_length          := getUInt16Le(inFile);
      header.file_name                   := gets(inFile, header.file_name_length);
      header.extra_field                 := gets(inFile, header.extra_field_length);
    end if;
  end func;


const proc: write (in local_file_header: header) is func
  begin
    writeln("signature: "                  rpad 50 <& literal(header.signature) lpad 10);
    writeln("version_needed_to_extract: "  rpad 50 <& header.version_needed_to_extract lpad 10);
    writeln("general_purpose_bit_flag: "   rpad 50 <& header.general_purpose_bit_flag lpad 10);
    writeln("compression_method: "         rpad 50 <& header.compression_method lpad 10);
    writeln("last_mod_file_time: "         rpad 50 <& header.last_mod_file_time lpad 10);
    writeln("last_mod_file_date: "         rpad 50 <& header.last_mod_file_date lpad 10);
    writeln("crc_32: "                     rpad 50 <& header.crc_32 lpad 10);
    writeln("compressed_size: "            rpad 50 <& header.compressed_size lpad 10);
    writeln("uncompressed_size: "          rpad 50 <& header.uncompressed_size lpad 10);
    writeln("file_name_length: "           rpad 50 <& header.file_name_length lpad 10);
    writeln("extra_field_length: "         rpad 50 <& header.extra_field_length lpad 10);
    writeln("file_name: "                  rpad 50 <& literal(header.file_name) lpad 10);
    writeln("extra_field: "                rpad 50 <& literal(header.extra_field) lpad 10);
  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
    var integer: file_comment_length                          is 0;   # 2 bytes
    var string:  file_comment                                 is "";  # variable size
  end struct;


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;
  begin
    endOfCentDir.signature := gets(inFile, 4);
    if endOfCentDir.signature = "PK\5;\6;" then
      endOfCentDir.number_of_this_disk                          := getUInt16Le(inFile);
      endOfCentDir.disk_number_with_start_of_central_directory  := getUInt16Le(inFile);
      endOfCentDir.entries_in_central_directory_on_this_disk    := getUInt16Le(inFile);
      endOfCentDir.entries_in_central_directory                 := getUInt16Le(inFile);
      endOfCentDir.size_of_central_directory                    := getUInt32Le(inFile);
      endOfCentDir.offset_of_start_of_central_directory         := getUInt32Le(inFile);
      endOfCentDir.file_comment_length                          := getUInt16Le(inFile);
      endOfCentDir.file_comment                                 := gets(inFile, endOfCentDir.file_comment_length);
    end if;
  end func;


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


const func end_of_central_directory: read_end_of_central_directory (inout file: inFile) is func
  result
    var end_of_central_directory: endOfCentDir is end_of_central_directory.value;
  local
    var integer: pos is 0;
  begin
    pos := rpos(inFile, "PK\5;\6;");
    if pos <> 0 then
      seek(inFile, pos);
      endOfCentDir := get_end_of_central_directory(inFile);
    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 integer: general_purpose_bit_flag        is 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 0;   # 2 bytes
    var integer: crc_32                          is 0;   # 4 bytes
    var integer: compressed_size                 is 0;   # 4 bytes
    var integer: uncompressed_size               is 0;   # 4 bytes
    var integer: file_name_length                is 0;   # 2 bytes
    var integer: extra_field_length              is 0;   # 2 bytes
    var 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
  end struct;


const func central_file_header: get_central_header (inout file: inFile) is func
  result
    var central_file_header: centralHeader is central_file_header.value;
  begin
    centralHeader.signature := gets(inFile, 4);
    if centralHeader.signature = "PK\1;\2;" then
      centralHeader.version_made_by                 := getUInt16Le(inFile);
      centralHeader.version_needed_to_extract       := getUInt16Le(inFile);
      centralHeader.general_purpose_bit_flag        := getUInt16Le(inFile);
      centralHeader.compression_method              := getUInt16Le(inFile);
      centralHeader.last_mod_file_time              := getUInt16Le(inFile);
      centralHeader.last_mod_file_date              := getUInt16Le(inFile);
      centralHeader.crc_32                          := getUInt32Le(inFile);
      centralHeader.compressed_size                 := getUInt32Le(inFile);
      centralHeader.uncompressed_size               := getUInt32Le(inFile);
      centralHeader.file_name_length                := getUInt16Le(inFile);
      centralHeader.extra_field_length              := getUInt16Le(inFile);
      centralHeader.file_comment_length             := getUInt16Le(inFile);
      centralHeader.disk_number_start               := getUInt16Le(inFile);
      centralHeader.internal_file_attributes        := getUInt16Le(inFile);
      centralHeader.external_file_attributes        := getUInt32Le(inFile);
      centralHeader.relative_offset_of_local_header := getUInt32Le(inFile);
      centralHeader.file_name                       := gets(inFile, centralHeader.file_name_length);
      centralHeader.extra_field                     := gets(inFile, centralHeader.extra_field_length);
      centralHeader.file_comment                    := gets(inFile, centralHeader.file_comment_length);
    end if;
  end func;


const proc: list_zip_dir (inout file: inFile) is func
  local
    var end_of_central_directory: endOfCentDir is end_of_central_directory.value;
    var central_file_header: centralHeader is central_file_header.value;
  begin
    endOfCentDir := read_end_of_central_directory(inFile);
    # write(endOfCentDir);
    seek(inFile, succ(endOfCentDir.offset_of_start_of_central_directory));
    centralHeader := get_central_header(inFile);
    while centralHeader.signature = "PK\1;\2;" do
      writeln(centralHeader.file_name rpad 50 <& centralHeader.compressed_size lpad 8 <& centralHeader.uncompressed_size lpad 8 <&
          centralHeader.extra_field_length lpad 6 <& centralHeader.general_purpose_bit_flag lpad 6);
      centralHeader := get_central_header(inFile);
    end while;
    # writeln(literal(centralHeader.signature));
(*
    header := get_local_header(inFile);
    while header.signature = "PK\3;\4;" do
      writeln(header.file_name rpad 50 <& header.compressed_size lpad 8 <& header.uncompressed_size lpad 8 <&
          header.extra_field_length lpad 6 <& header.general_purpose_bit_flag lpad 6);
      writeln(length(header.extra_field) <& " " <& literal(header.extra_field));
      if header.file_name = "Monitor/Registered Protocols/GAPReceiver.dat" then
        writeln(literal(gets(inFile, 25)));
      else
        ignore(gets(inFile, header.compressed_size));
      end if;
      header := get_local_header(inFile);
    end while;
    writeln(literal(header.signature));
*)
  end func;


(**
 *  Determine the filenames in a zip archive.
 *  Note that the function returns only the filenames.
 *  Additional information must be obtained with other calls.
 *)
const func array string: read_zip_dir (in string: zipFileName) is func
  result
    var array string: filenames is 0 times "";
  local
    var file: zipFile is STD_NULL;
    var end_of_central_directory: endOfCentDir is end_of_central_directory.value;
    var central_file_header: centralHeader is central_file_header.value;
  begin
    zipFile := open(zipFileName, "r");
    if zipFile <> STD_NULL then
      endOfCentDir := read_end_of_central_directory(zipFile);
      # write(endOfCentDir);
      seek(zipFile, succ(endOfCentDir.offset_of_start_of_central_directory));
      centralHeader := get_central_header(zipFile);
      while centralHeader.signature = "PK\1;\2;" do
        filenames &:= [] (centralHeader.file_name);
        centralHeader := get_central_header(zipFile);
      end while;
      # writeln(literal(centralHeader.signature));
      close(zipFile);
    end if;
  end func;


const func string: getZipContent (inout file: inFile, in string: filePath) is func
  result
    var string: content is "";
  local
    var end_of_central_directory: endOfCentDir is end_of_central_directory.value;
    var central_file_header: centralHeader is central_file_header.value;
    var local_file_header: header is local_file_header.value;
    var boolean: found is FALSE;
    var string: fileData is "";
  begin
    endOfCentDir := read_end_of_central_directory(inFile);
    seek(inFile, succ(endOfCentDir.offset_of_start_of_central_directory));
    centralHeader := get_central_header(inFile);
    while centralHeader.signature = "PK\1;\2;" and not found do
      centralHeader := get_central_header(inFile);
      if centralHeader.file_name = filePath then
        found := TRUE;
      end if;
    end while;
    if found then
      seek(inFile, succ(centralHeader.relative_offset_of_local_header));
      header := get_local_header(inFile);
      # write(header);
      fileData := gets(inFile, header.compressed_size);
      if header.compression_method = 0 then
        content := fileData;
      elsif header.compression_method = 8 then
        # writeln(literal(fileData[ .. 50]));
        content := inflate(fileData);
        # writeln(literal(content[ .. 50]));
      end if;
    end if;
  end func;


const func string: getZipContent (in string: zipFileName, in string: filePath) is func
  result
    var string: content is "";
  local
    var file: inFile is STD_NULL;
  begin
    inFile := open(zipFileName, "r");
    if inFile <> STD_NULL then
      content := getZipContent(inFile, filePath);
      close(inFile);
    end if;
  end func;
