
(********************************************************************)
(*                                                                  *)
(*  gzip.s7i      Gzip compression support library                  *)
(*  Copyright (C) 2008  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 "deflate.s7i";
include "bytedata.s7i";
include "time.s7i";
include "crc32.s7i";


const proc: readGzipHeader (in string: stri, inout integer: bytePos) is func
  local
    var integer: compressionMethod is 0;
    var integer: flags is 0;
    var boolean: flagFtext is FALSE;
    var boolean: flagFhcrc is FALSE;
    var boolean: flagFextra is FALSE;
    var boolean: flagFname is FALSE;
    var boolean: flagFcomment is FALSE;
    var integer: mtime is 0;
    var integer: extraFlags is 0;
    var integer: operatingSystem is 0;
    var integer: extraLength is 0;
    var string: originalFileName is "";
    var string: fileComment is "";
    var integer: crc16 is 0;
  begin
    compressionMethod := ord(stri[3]);
    flags := ord(stri[4]);
    flagFtext    := odd(flags);
    flagFhcrc    := odd(flags >> 1);
    flagFextra   := odd(flags >> 2);
    flagFname    := odd(flags >> 3);
    flagFcomment := odd(flags >> 4);
    mtime := ord(stri[5]) + (ord(stri[6]) << 8) + (ord(stri[7]) << 16) + (ord(stri[8]) << 24);
    extraFlags := ord(stri[9]);
    operatingSystem := ord(stri[10]);
    bytePos := 11;
    if flagFextra then
      extraLength := ord(stri[11]) + (ord(stri[12]) << 8);
      bytePos +:= 2 + extraLength;
    end if;
    if flagFname then
      originalFileName := getAsciiz(stri, bytePos);
    end if;
    if flagFcomment then
      fileComment := getAsciiz(stri, bytePos);
    end if;
    if flagFhcrc then
      crc16 := ord(stri[11]) + (ord(stri[12]) << 8);
    end if;
  end func;


(**
 *  Decompress a string that was compressed with gzip.
 *  Gzip uses the DEFLATE compression algorithm. DEFLATE uses
 *  a combination of the LZ77 algorithm and Huffman coding.
 *  Additionally to DEFLATE gzip uses a magic number and a header.
 *  @return the uncompressed string.
 *  @exception RANGE_ERROR When ''compressed'' is not in gzip format.
 *)
const func string: gunzip (in string: compressed) is func
  result
    var string: uncompressed is "";
  local
    var integer: bytePos is 0;
    var integer: bitPos is 0;
    var boolean: bfinal is FALSE;
  begin
    if compressed[1 len 2] = "\31\\139\" then
      readGzipHeader(compressed, bytePos);
      repeat
        processCompressedBlock(compressed, bytePos, bitPos, uncompressed, bfinal);
      until bfinal;
    end if;
  end func;


const type: gunzipFile is sub null_file struct
    var string: compressed is "";
    var integer: bytePos is 0;
    var integer: bitPos is 0;
    var boolean: bfinal is FALSE;
    var string: uncompressed is "";
    var integer: position is 1;
  end struct;

type_implements_interface(gunzipFile, file);


(**
 *  Open a ''gunzipFile'' to decompress a compressed string.
 *  @return the file opened.
 *)
const func file: openGunzipFile (in string: compressed) is func
  result
    var file: newFile is STD_NULL;
  local
    var gunzipFile: new_gunzipFile is gunzipFile.value;
  begin
    if compressed[1 len 2] = "\31\\139\" then
      new_gunzipFile.compressed := compressed;
      readGzipHeader(new_gunzipFile.compressed, new_gunzipFile.bytePos);
      newFile := toInterface(new_gunzipFile);
    end if;
  end func;


(**
 *  Close a ''gunzipFile''.
 *)
const proc: close (in gunzipFile: aFile) is noop;


(**
 *  Read a character from a ''gunzipFile''.
 *  @return the character read.
 *)
const func char: getc (inout gunzipFile: inFile) is func
  result
    var char: charRead is ' ';
  begin
    while inFile.position > length(inFile.uncompressed) and
        not inFile.bfinal do
      processCompressedBlock(inFile.compressed, inFile.bytePos, inFile.bitPos,
          inFile.uncompressed, inFile.bfinal);
    end while;
    if inFile.position <= length(inFile.uncompressed) then
      charRead := inFile.uncompressed[inFile.position];
      incr(inFile.position);
    else
      charRead := EOF;
    end if;
  end func;


(**
 *  Read a string with maximum length from a ''gunzipFile''.
 *  @return the string read.
 *)
const func string: gets (inout gunzipFile: inFile, in var integer: leng) is func
  result
    var string: striRead is "";
  begin
    while pred(inFile.position + leng) > length(inFile.uncompressed) and
        not inFile.bfinal do
      processCompressedBlock(inFile.compressed, inFile.bytePos, inFile.bitPos,
          inFile.uncompressed, inFile.bfinal);
    end while;
    if pred(inFile.position + leng) <= length(inFile.uncompressed) then
      striRead := inFile.uncompressed[inFile.position len leng];
      inFile.position +:= leng;
    else
      striRead := inFile.uncompressed[inFile.position ..];
      inFile.position := succ(length(inFile.uncompressed));
    end if;
  end func;


(**
 *  Determine the end-of-file indicator.
 *  The end-of-file indicator is set when at least one request to read
 *  from the file failed.
 *  @return TRUE if the end-of-file indicator is set, FALSE otherwise.
 *)
const func boolean: eof (in gunzipFile: inFile) is
  return inFile.position > length(inFile.uncompressed) and inFile.bfinal;


(**
 *  Determine if at least one character can be read successfully.
 *  This function allows a file to be handled like an iterator.
 *  @return FALSE if ''getc'' would return EOF, TRUE otherwise.
 *)
const func boolean: hasNext (inout gunzipFile: inFile) is func
  result
    var boolean: hasNext is FALSE;
  begin
    while inFile.position > length(inFile.uncompressed) and
        not inFile.bfinal do
      processCompressedBlock(inFile.compressed, inFile.bytePos, inFile.bitPos,
          inFile.uncompressed, inFile.bfinal);
    end while;
    hasNext := inFile.position <= length(inFile.uncompressed);
  end func;


(**
 *  Obtain the length of a file.
 *  The file length is measured in bytes.
 *  @return the length of a file, or 0 if it cannot be obtained.
 *)
const func integer: length (inout gunzipFile: aFile) is func
  result
    var integer: length is 0;
  begin
    while not aFile.bfinal do
      processCompressedBlock(aFile.compressed, aFile.bytePos, aFile.bitPos,
          aFile.uncompressed, aFile.bfinal);
    end while;
    length := length(aFile.uncompressed);
  end func;


(**
 *  Set the current file position.
 *  The file position is measured in bytes from the start of the file.
 *  The first byte in the file has the position 1.
 *)
const proc: seek (inout gunzipFile: aFile, in integer: position) is func
  begin
    aFile.position := position;
  end func;


(**
 *  Obtain the current file position.
 *  The file position is measured in bytes from the start of the file.
 *  The first byte in the file has the position 1.
 *  @return the current file position.
 *)
const func integer: tell (in gunzipFile: aFile) is
  return aFile.position;


(**
 *  Open a ''gunzipFile'' to decompress a compressed file.
 *  The file is read into a string and ''openGunzipFile'' is used to
 *  open a ''gunzipFile''.
 *  @return the file opened.
 *)
const func file: gunzip (inout file: aFile) is func
  result
    var file: newFile is STD_NULL;
  local
    var string: stri is "";
  begin
    stri := gets(aFile, length(aFile) - tell(aFile) + 1);
    if stri <> "" then
      newFile := openGunzipFile(stri);
    end if;
  end func;


(**
 *  Compress a string to the gzip format.
 *  Gzip uses the DEFLATE compression algorithm. DEFLATE uses
 *  a combination of the LZ77 algorithm and Huffman coding.
 *  Additionally to DEFLATE gzip uses a magic number and a header.
 *  @return the uncompressed string.
 *  @exception RANGE_ERROR When ''compressed'' is not in gzip format.
 *)
const func string: gzip (in string: uncompressed) is func
  result
    var string: compressed is "\31\\139\";  # Identification for gzip format
  begin
    compressed &:= '\8\';  # Compression method: Deflate
    compressed &:= '\0\';  # Flags
    compressed &:= int32AsFourBytesLe(timestamp1970(time(NOW)));
    compressed &:= '\0\';  # Extra flags
    compressed &:= '\3\';  # Operating system: Unix
    deflateBlock(uncompressed, compressed, TRUE);
    compressed &:= int32AsFourBytesLe(ord(crc32(uncompressed)));
    compressed &:= int32AsFourBytesLe(length(uncompressed));
  end func;


const type: gzipFile is sub null_file struct
    var file: destFile is STD_NULL;
    var deflateData: deflateState is deflateData.value;
    var string: uncompressed is "";
    var string: compressed is "\31\\139\";  # Identification for gzip format
  end struct;

type_implements_interface(gzipFile, file);


(**
 *  Open a ''gzipFile'' to compress data to the gzip format.
 *  A ''gzipFile'' compresses data that is written to it. The
 *  compressed data is written to ''destFile'' in the gzip
 *  format. Gzip uses the DEFLATE compression algorithm. DEFLATE
 *  uses a combination of the LZ77 algorithm and Huffman coding.
 *  Additionally to DEFLATE gzip uses a magic number and a header.
 *  @return the file opened.
 *)
const func file: openGzipFile (inout file: destFile) is func
  result
    var file: newFile is STD_NULL;
  local
    var gzipFile: new_gzipFile is gzipFile.value;
  begin
    new_gzipFile.destFile := destFile;
    new_gzipFile.compressed &:= '\8\';  # Compression method: Deflate
    new_gzipFile.compressed &:= '\0\';  # Flags
    new_gzipFile.compressed &:= int32AsFourBytesLe(timestamp1970(time(NOW)));
    new_gzipFile.compressed &:= '\0\';  # Extra flags
    new_gzipFile.compressed &:= '\3\';  # Operating system: Unix
    beginDeflateBlock(new_gzipFile.deflateState, new_gzipFile.compressed, TRUE);
    newFile := toInterface(new_gzipFile);
  end func;


(**
 *  Close a ''gzipFile''.
 *)
const proc: close (inout gzipFile: aFile) is func
  begin
    closeDeflateBlock(aFile.deflateState, aFile.uncompressed, aFile.compressed);
    aFile.compressed &:= int32AsFourBytesLe(ord(crc32(aFile.uncompressed)));
    aFile.compressed &:= int32AsFourBytesLe(length(aFile.uncompressed));
    write(aFile.destFile, aFile.compressed);
    close(aFile.destFile);
  end func;


(**
 *  Write the [[string]] ''stri'' to a ''gzipFile''.
 *)
const proc: write (inout gzipFile: outFile, in string: stri) is func
  begin
    outFile.uncompressed &:= stri;
    deflate(outFile.deflateState, outFile.uncompressed,
            length(outFile.uncompressed) - 258, outFile.compressed);
  end func;
