(* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is TurboPower Abbrevia
 *
 * The Initial Developer of the Original Code is
 * TurboPower Software
 *
 * Portions created by the Initial Developer are Copyright (C) 1997-2002
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 * Craig Peterson <capeterson@users.sourceforge.net>
 *
 * ***** END LICENSE BLOCK ***** *)

{*********************************************************}
{* ABBREVIA: AbGzTyp.pas                                 *}
{*********************************************************}
{* ABBREVIA: TAbGzipArchive, TAbGzipItem classes         *}
{*********************************************************}
{* Misc. constants, types, and routines for working      *}
{* with GZip files                                       *}
{* See: RFC 1952                                         *}
{* "GZIP file format specification version 4.3"          *}
{* for more information on GZip                          *}
{* See "algorithm.doc" in Gzip source and "format.txt"   *}
{* on gzip.org for differences from RFC                  *}
{*********************************************************}

unit AbGzTyp;

{$I AbDefine.inc}

interface

uses
  Classes, AbUtils, AbArcTyp, AbTarTyp, AbVMStrm;

type
  { pre-defined "operating system" (really more FILE system)
    types for the Gzip header }
  TAbGzFileSystem =
    (osFat, osAmiga, osVMS, osUnix, osVM_CMS, osAtariTOS,
    osHPFS, osMacintosh, osZSystem, osCP_M, osTOPS20,
    osNTFS, osQDOS, osAcornRISCOS, osVFAT, osMVS, osBeOS,
    osTandem, osTHEOS, osUnknown, osUndefined);

type
  PAbGzHeader = ^TAbGzHeader;
  TAbGzHeader = packed record  { SizeOf(TGzHeader) = 10}
    ID1        : Byte;  { ID Byte, should always be $1F}
    ID2        : Byte;  { ID Byte, should always be $8B}
    CompMethod : Byte;  { compression method used}
    { 0..7 reserved, 8 = deflate, others undefined as of this writing (4/27/2001)}
    Flags      : Byte; { misc flags}
      { Bit 0: FTEXT    compressed file contains text, can be used for}
                      { cross platform line termination translation}
      { Bit 1: FCONTINUATION file is a continuation of a multi-part gzip file}
                      { RFC 1952 says this is the header CRC16 flag, but gzip}
                      { reserves it and won't extract the file if this is set}
                      { header data includes part number after header record}
      { Bit 2: FEXTRA   header data contains Extra Data, starts after part}
                      { number (if any)}
      { Bit 3: FNAME    header data contains FileName, null terminated}
                      { string starting immediately after Extra Data (if any)}
                      { RFC 1952 says this is ISO 8859-1 encoded, but gzip}
                      { always uses the system encoding}
      { Bit 4: FCOMMENT header data contains Comment, null terminated string}
                      { starting immediately after FileName (if any)}
      { Bit 5: FENCRYPTED file is encrypted using zip-1.9 encryption }
                      { header data contains a 12-byte encryption header }
                      { starting immediately after Comment.  Documented in}
                      { "algorithm.doc", but unsupported in gzip}
      { Bits 6..7 are undefined and reserved as of this writing (8/25/2009)}
    ModTime    : LongInt; { File Modification (Creation) time,}
                          { UNIX cdate format}
    XtraFlags  : Byte;   { additional flags}
      { XtraFlags = 2  -- Deflate compressor used maximum compression algorithm}
      { XtraFlags = 4  -- Deflate compressor used fastest algorithm}
    OS         : Byte; { Operating system that created file,}
                       { see GZOsToStr routine for values}
  end;

  TAbGzTailRec = packed record
    CRC32 : LongInt;  { crc for uncompressed data }
    ISize : LongWord;  { size of uncompressed data }
  end;

  TAbGzExtraFieldSubID = array[0..1] of AnsiChar;

type
  TAbGzipExtraField = class(TAbExtraField)
  private
    FGZHeader : PAbGzHeader;
    function GetID(aIndex : Integer): TAbGzExtraFieldSubID;
  protected
    procedure Changed; override;
  public
    constructor Create(aGZHeader : PAbGzHeader);
    procedure Delete(aID : TAbGzExtraFieldSubID);
    function Get(aID : TAbGzExtraFieldSubID;
      out aData : Pointer; out aDataSize : Word) : Boolean;
    procedure Put(aID : TAbGzExtraFieldSubID; const aData; aDataSize : Word);
  public
    property IDs[aIndex : Integer]: TAbGzExtraFieldSubID
      read GetID;
  end;

  TAbGzipItem = class(TAbArchiveItem)
  protected {private}
    FGZHeader : TAbGzHeader;
    FExtraField : TAbGzipExtraField;
    FFileComment : AnsiString;
    FRawFileName : AnsiString;

  protected
    function GetFileSystem: TAbGzFileSystem;
    function GetHasExtraField: Boolean;
    function GetHasFileComment: Boolean;
    function GetHasFileName: Boolean;
    function GetIsText: Boolean;

    procedure SetFileComment(const Value : AnsiString);
    procedure SetFileSystem(const Value: TAbGzFileSystem);
    procedure SetIsText(const Value: Boolean);

    function GetExternalFileAttributes : LongWord; override;
    function GetIsEncrypted : Boolean; override;
    function GetLastModFileDate : Word; override;
    function GetLastModFileTime : Word; override;
    function GetLastModTimeAsDateTime: TDateTime; override;

    procedure SetExternalFileAttributes( Value : LongWord ); override;
    procedure SetFileName(const Value : string); override;
    procedure SetIsEncrypted(Value : Boolean); override;
    procedure SetLastModFileDate(const Value : Word); override;
    procedure SetLastModFileTime(const Value : Word); override;
    procedure SetLastModTimeAsDateTime(const Value: TDateTime); override;

    procedure SaveGzHeaderToStream(AStream : TStream);
    procedure LoadGzHeaderFromStream(AStream : TStream);
  public
    property CompressionMethod : Byte
      read FGZHeader.CompMethod;

    property ExtraFlags : Byte {Default: 2}
      read FGZHeader.XtraFlags write FGZHeader.XtraFlags;

    property Flags : Byte
      read FGZHeader.Flags;

    property FileComment : AnsiString
      read FFileComment write SetFileComment;

    property FileSystem : TAbGzFileSystem {Default: osFat (Windows); osUnix (Linux)}
      read GetFileSystem write SetFileSystem;

    property ExtraField : TAbGzipExtraField
      read FExtraField;

    property IsEncrypted : Boolean
      read GetIsEncrypted;

    property HasExtraField : Boolean
      read GetHasExtraField;

    property HasFileName : Boolean
      read GetHasFileName;

    property HasFileComment : Boolean
      read GetHasFileComment;

    property IsText : Boolean
      read GetIsText write SetIsText;

    property GZHeader : TAbGzHeader
      read FGZHeader;

    constructor Create;
    destructor Destroy; override;
  end;

  TAbGzipStreamHelper = class(TAbArchiveStreamHelper)
  private
    function GetGzCRC: LongInt;
    function GetFileSize: LongInt;
  protected {private}
    FItem : TAbGzipItem;
    FTail : TAbGzTailRec;
  public
    constructor Create(AStream : TStream);
    destructor Destroy; override;

    procedure ExtractItemData(AStream : TStream); override;
    function FindFirstItem : Boolean; override;
    function FindNextItem : Boolean; override;
    function SeekItem(Index : Integer): Boolean; override;
    procedure SeekToItemData;
    procedure WriteArchiveHeader; override;
    procedure WriteArchiveItem(AStream : TStream); override;
    procedure WriteArchiveTail; override;
    function GetItemCount : Integer; override;
    procedure ReadHeader; override;
    procedure ReadTail; override;

    property CRC : LongInt
      read GetGzCRC;
    property FileSize : LongInt
      read GetFileSize;
    property TailCRC : LongInt
      read FTail.CRC32;
    property TailSize : LongWord
      read FTail.ISize;
  end;

  TAbGzipArchiveState = (gsGzip, gsTar);

  TAbGzipArchive = class(TAbTarArchive)
  private
    FGZStream  : TStream;        { stream for GZip file}
    FGZItem    : TAbArchiveList; { item in Gzip (only one, but need polymorphism of class)}
    FTarStream : TAbVirtualMemoryStream; { stream for possible contained Tar }
    FTarList   : TAbArchiveList; { items in possible contained Tar }
    FTarAutoHandle: Boolean;
    FState     : TAbGzipArchiveState;
    FIsGzippedTar : Boolean;

    procedure SetTarAutoHandle(const Value: Boolean);
    function GetIsGzippedTar: Boolean;
    procedure SwapToGzip;
    procedure SwapToTar;

  protected
    function CreateItem(const FileSpec : string): TAbArchiveItem;
      override;
    procedure ExtractItemAt(Index : Integer; const UseName : string);
      override;
    procedure ExtractItemToStreamAt(Index : Integer; aStream : TStream);
      override;
    procedure LoadArchive;
      override;
    procedure SaveArchive;
      override;
    procedure TestItemAt(Index : Integer);
      override;
    function FixName(const Value : string) : string;
      override;
    function GetSupportsEmptyFolders : Boolean;
      override;

    function GetItem(Index: Integer): TAbGzipItem;
    procedure PutItem(Index: Integer; const Value: TAbGzipItem);
  public {methods}
    constructor CreateFromStream(aStream : TStream; const aArchiveName : string);
      override;
    destructor  Destroy;
      override;

    procedure DoSpanningMediaRequest(Sender : TObject; ImageNumber : Integer;
      var ImageName : string; var Abort : Boolean); override;

    property TarAutoHandle : Boolean
      read FTarAutoHandle write SetTarAutoHandle;

    property IsGzippedTar : Boolean
      read GetIsGzippedTar write FIsGzippedTar;

    property Items[Index : Integer] : TAbGzipItem
      read GetItem
      write PutItem; default;
  end;

function VerifyGZip(Strm : TStream) : TAbArchiveType;
function GZOsToStr(OS: Byte) : string;

implementation

uses
  {$IFDEF MSWINDOWS}
  Windows,
  {$ENDIF}
{$IFDEF HasAnsiStrings}
  System.AnsiStrings,
{$ENDIF}
  SysUtils, AbBitBkt, AbCharset, AbDfBase, AbDfDec, AbDfEnc, AbExcept, AbResString;

const
  { Header Signature Values}
  AB_GZ_HDR_ID1 = $1F;
  AB_GZ_HDR_ID2 = $8B;

  { Test bits for TGzHeader.Flags field }
  AB_GZ_FLAG_FTEXT         = $01;
  AB_GZ_FLAG_FCONTINUATION = $02;
  AB_GZ_FLAG_FEXTRA        = $04;
  AB_GZ_FLAG_FNAME         = $08;
  AB_GZ_FLAG_FCOMMENT      = $10;
  AB_GZ_FLAG_FENCRYPTED    = $20;
  AB_GZ_UNSUPPORTED_FLAGS  = $E2;

  { GZip OS source flags }
  AB_GZ_OS_ID_FAT         = 0;
  AB_GZ_OS_ID_Amiga       = 1;
  AB_GZ_OS_ID_VMS         = 2;
  AB_GZ_OS_ID_Unix        = 3;
  AB_GZ_OS_ID_VM_CMS      = 4;
  AB_GZ_OS_ID_AtariTOS    = 5;
  AB_GZ_OS_ID_HPFS        = 6;
  AB_GZ_OS_ID_Macintosh   = 7;
  AB_GZ_OS_ID_Z_System    = 8;
  AB_GZ_OS_ID_CP_M        = 9;
  AB_GZ_OS_ID_TOPS20      = 10;
  AB_GZ_OS_ID_NTFS        = 11;
  AB_GZ_OS_ID_QDOS        = 12;
  AB_GZ_OS_ID_AcornRISCOS = 13;
  AB_GZ_OS_ID_VFAT        = 14;
  AB_GZ_OS_ID_MVS         = 15;
  AB_GZ_OS_ID_BEOS        = 16;
  AB_GZ_OS_ID_TANDEM      = 17;
  AB_GZ_OS_ID_THEOS       = 18;
  AB_GZ_OS_ID_unknown     = 255;

function GZOsToStr(OS: Byte) : string;
{
Return a descriptive string for TGzHeader.OS field
}
begin
  case OS of
    AB_GZ_OS_ID_FAT         : Result := AbGzOsFat;
    AB_GZ_OS_ID_Amiga       : Result := AbGzOsAmiga;
    AB_GZ_OS_ID_VMS         : Result := AbGzOsVMS;
    AB_GZ_OS_ID_Unix        : Result := AbGzOsUnix;
    AB_GZ_OS_ID_VM_CMS      : Result := AbGzOsVM_CMS;
    AB_GZ_OS_ID_AtariTOS    : Result := AbGzOsAtari;
    AB_GZ_OS_ID_HPFS        : Result := AbGzOsHPFS;
    AB_GZ_OS_ID_Macintosh   : Result := AbGzOsMacintosh;
    AB_GZ_OS_ID_Z_System    : Result := AbGzOsZ_System;
    AB_GZ_OS_ID_CP_M        : Result := AbGzOsCP_M;
    AB_GZ_OS_ID_TOPS20      : Result := AbGzOsTOPS_20;
    AB_GZ_OS_ID_NTFS        : Result := AbGzOsNTFS;
    AB_GZ_OS_ID_QDOS        : Result := AbGzOsQDOS;
    AB_GZ_OS_ID_AcornRISCOS : Result := AbGzOsAcornRISCOS;
    AB_GZ_OS_ID_VFAT        : Result := AbGzOsVFAT;
    AB_GZ_OS_ID_MVS         : Result := AbGzOsMVS;
    AB_GZ_OS_ID_BEOS        : Result := AbGzOsBeOS;
    AB_GZ_OS_ID_TANDEM      : Result := AbGzOsTandem;
    AB_GZ_OS_ID_THEOS       : Result := AbGzOsTHEOS;
    AB_GZ_OS_ID_unknown     : Result := AbGzOsunknown;
  else
    Result := AbGzOsUndefined;
  end;
end;


function VerifyHeader(const Header : TAbGzHeader) : Boolean;
begin
  { check id fields and if deflated (only handle deflate anyway)}
  Result := (Header.ID1 = AB_GZ_HDR_ID1) and
     (Header.ID2 = AB_GZ_HDR_ID2) and
     (Header.CompMethod = 8 {deflate});
end;

function VerifyGZip(Strm : TStream) : TAbArchiveType;
var
  GHlp : TAbGzipStreamHelper;
  Hlpr : TAbDeflateHelper;
  PartialTarData : TMemoryStream;
  CurPos : Int64;
begin
  Result := atUnknown;
  CurPos := Strm.Position;
  try
    Strm.Seek(0, soBeginning);

    {prepare for the try..finally}
    Hlpr := nil;
    PartialTarData := nil;

    GHlp := TAbGzipStreamHelper.Create(Strm);
    try
      {create the stream helper and read the item header}
      GHlp.ReadHeader;

      { check id fields and if deflated (only handle deflate anyway)}
      if VerifyHeader(GHlp.FItem.FGZHeader) then begin
        Result := atGZip; { provisional }

        { check if is actually a Gzipped Tar }
        { partial extract contents, verify vs. Tar }
        PartialTarData := TMemoryStream.Create;
        GHlp.SeekToItemData;
        Hlpr := TAbDeflateHelper.Create;
        Hlpr.PartialSize := 512;
        PartialTarData.SetSize(512 * 2);
        Inflate(Strm, PartialTarData, Hlpr);

        {set to beginning of extracted data}
        PartialTarData.Position := 0;

        if (VerifyTar(PartialTarData) = atTar) then
          Result := atGZippedTar;
      end;
    finally
      GHlp.Free;
      Hlpr.Free;
      PartialTarData.Free;
    end;
  except
    on EReadError do
      Result := atUnknown;
  end;
  Strm.Position := CurPos;
end;

{ TAbGzipExtraField }

constructor TAbGzipExtraField.Create(aGZHeader : PAbGzHeader);
begin
  inherited Create;
  FGZHeader := aGZHeader;
end;

procedure TAbGzipExtraField.Changed;
begin
  if Buffer = nil then
    FGzHeader.Flags := FGzHeader.Flags and not AB_GZ_FLAG_FEXTRA
  else
    FGzHeader.Flags := FGzHeader.Flags or AB_GZ_FLAG_FEXTRA;
end;

procedure TAbGzipExtraField.Delete(aID : TAbGzExtraFieldSubID);
begin
  inherited Delete(Word(aID));
end;

function TAbGzipExtraField.GetID(aIndex : Integer): TAbGzExtraFieldSubID;
begin
  Result := TAbGzExtraFieldSubID(inherited IDs[aIndex]);
end;

function TAbGzipExtraField.Get(aID : TAbGzExtraFieldSubID; out aData : Pointer;
  out aDataSize : Word) : Boolean;
begin
  Result := inherited Get(Word(aID), aData, aDataSize);
end;

procedure TAbGzipExtraField.Put(aID : TAbGzExtraFieldSubID; const aData; aDataSize : Word);
begin
  inherited Put(Word(aID), aData, aDataSize);
end;


{ TAbGzipStreamHelper }

constructor TAbGzipStreamHelper.Create(AStream : TStream);
begin
  inherited Create(AStream);
  FItem := TAbGzipItem.Create;
end;

destructor TAbGzipStreamHelper.Destroy;
begin
  FItem.Free;
  inherited;
end;

function ReadCStringInStream(AStream: TStream): AnsiString;
{
locate next instance of a null character in a stream
leaves stream positioned just past that,
or at end of stream if not found or null is last byte in stream.
Result is the entire read string.
}
const
  BuffSiz = 1024;
var
  Buff   : array [0..BuffSiz-1] of AnsiChar;
  Len, DataRead : LongInt;
begin
{ basically what this is supposed to do is...}
{
  repeat
    AStream.Read(C, 1);
    Result := Result + C;
  until (AStream.Position = AStream.Size) or (C = #0);
}
  Result := '';
  repeat
    DataRead := AStream.Read(Buff, BuffSiz - 1);
    Buff[DataRead] := #0;
    Len := AbStrLen(Buff);
    if Len > 0 then begin
      SetLength(Result, Length(Result) + Len);
      Move(Buff, Result[Length(Result) - Len + 1], Len);
    end;
    if Len < DataRead then begin
      AStream.Seek(Len - DataRead + 1, soCurrent);
      Break;
    end;
  until DataRead = 0;
end;

procedure TAbGzipStreamHelper.SeekToItemData;
{find end of header data, including FileName etc.}
begin
  {** Seek to Compressed Data **}
  FStream.Seek(0, soBeginning);
  FItem.LoadGzHeaderFromStream(FStream);
end;

procedure TAbGzipStreamHelper.ExtractItemData(AStream: TStream);
var
  Helper : TAbDeflateHelper;
begin
  Helper := TAbDeflateHelper.Create;
  try
    SeekToItemData;
    if (AStream is TAbBitBucketStream) then
      Helper.Options := Helper.Options or dfc_TestOnly;
    FItem.CRC32 := Inflate(FStream, AStream, Helper);
    FItem.UncompressedSize := AStream.Size{Helper.NormalSize};
  finally
    Helper.Free;
  end;
end;

function TAbGzipStreamHelper.FindFirstItem: Boolean;
var
  GZH : TAbGzHeader;
  DataRead : Integer;
begin
  Result := False;
  FStream.Seek(0, soBeginning);
  DataRead := FStream.Read(GZH, SizeOf(TAbGzHeader));
  if (DataRead = SizeOf(TAbGzHeader)) and VerifyHeader(GZH) then begin
    FItem.FGZHeader := GZH;
    Result := True;
  end;
  FStream.Seek(0, soBeginning);
end;

function TAbGzipStreamHelper.FindNextItem: Boolean;
begin
  { only one item in a GZip }
  Result := False;
end;

function TAbGzipStreamHelper.SeekItem(Index: Integer): Boolean;
begin
  if Index > 0 then
    Result := False
  else
    Result := FindFirstItem;
end;

procedure TAbGzipStreamHelper.WriteArchiveHeader;
begin
  FItem.SaveGzHeaderToStream(FStream);
end;

procedure TAbGzipStreamHelper.WriteArchiveItem(AStream: TStream);
var
  Helper : TAbDeflateHelper;
begin
  Helper := TAbDeflateHelper.Create;
  try
    FItem.CRC32 := Deflate(AStream, FStream, Helper);
    FItem.UncompressedSize := AStream.Size;
  finally
    Helper.Free;
  end;
end;

procedure TAbGzipStreamHelper.WriteArchiveTail;
var
  Tail : TAbGzTailRec;
begin
  Tail.CRC32 := FItem.CRC32;
  Tail.ISize := FItem.UncompressedSize;
  FStream.Write(Tail, SizeOf(TAbGzTailRec));
end;

function TAbGzipStreamHelper.GetItemCount: Integer;
begin
  { only one item in a gzip }
  Result := 1;
end;

procedure TAbGzipStreamHelper.ReadHeader;
begin
  FItem.LoadGzHeaderFromStream(FStream);
end;

procedure TAbGzipStreamHelper.ReadTail;
begin
  FStream.Read(FTail, SizeOf(TAbGzTailRec));
end;

function TAbGzipStreamHelper.GetGzCRC: LongInt;
begin
  Result := FItem.CRC32;
end;

function TAbGzipStreamHelper.GetFileSize: LongInt;
begin
  Result := FItem.UncompressedSize;
end;

{ TAbGzipItem }

constructor TAbGzipItem.Create;
begin
  inherited Create;

  { default ID fields }
  FGzHeader.ID1 := AB_GZ_HDR_ID1;
  FGzHeader.ID2 := AB_GZ_HDR_ID2;

  { compression method }
  FGzHeader.CompMethod := 8;  { deflate }

  { Maxium Compression }
  FGzHeader.XtraFlags := 2;

  FFileName := '';
  FFileComment := '';
  FExtraField := TAbGzipExtraField.Create(@FGzHeader);

  { source OS ID }
{$IFDEF LINUX } {assume EXT2 system }
  FGzHeader.OS := AB_GZ_OS_ID_Unix;
{$ENDIF LINUX }
{$IFDEF MSWINDOWS } {assume FAT system }
  FGzHeader.OS := AB_GZ_OS_ID_FAT;
{$ENDIF MSWINDOWS }
end;

destructor TAbGzipItem.Destroy;
begin
  FExtraField.Free;
  inherited;
end;

function TAbGzipItem.GetExternalFileAttributes: LongWord;
begin
  { GZip has no provision for storing attributes }
  Result := 0;
end;

function TAbGzipItem.GetFileSystem: TAbGzFileSystem;
begin
  case FGzHeader.OS of
    0..18: Result := TAbGzFileSystem(FGzHeader.OS);
    255:   Result := osUnknown;
    else
      Result := osUndefined;
  end; { case }
end;

function TAbGzipItem.GetIsEncrypted: Boolean;
begin
  Result := (FGZHeader.Flags and AB_GZ_FLAG_FENCRYPTED) = AB_GZ_FLAG_FENCRYPTED;
end;

function TAbGzipItem.GetHasExtraField: Boolean;
begin
  Result := (FGZHeader.Flags and AB_GZ_FLAG_FEXTRA) = AB_GZ_FLAG_FEXTRA;
end;

function TAbGzipItem.GetHasFileComment: Boolean;
begin
  Result := (FGZHeader.Flags and AB_GZ_FLAG_FCOMMENT) = AB_GZ_FLAG_FCOMMENT;
end;

function TAbGzipItem.GetHasFileName: Boolean;
begin
  Result := (FGZHeader.Flags and AB_GZ_FLAG_FNAME) = AB_GZ_FLAG_FNAME;
end;

function TAbGzipItem.GetIsText: Boolean;
begin
  Result := (FGZHeader.Flags and AB_GZ_FLAG_FTEXT) = AB_GZ_FLAG_FTEXT;
end;

function TAbGzipItem.GetLastModFileDate: Word;
begin
  { convert to local DOS file Date }
  Result := LongRec(AbDateTimeToDosFileDate(LastModTimeAsDateTime)).Hi;
end;

function TAbGzipItem.GetLastModFileTime: Word;
begin
  { convert to local DOS file Time }
  Result := LongRec(AbDateTimeToDosFileDate(LastModTimeAsDateTime)).Lo;
end;

function TAbGzipItem.GetLastModTimeAsDateTime: TDateTime;
begin
  Result := AbUnixTimeToLocalDateTime(FGZHeader.ModTime);
end;

procedure TAbGzipItem.LoadGzHeaderFromStream(AStream: TStream);
var
  LenW : Word;
begin
  AStream.Read(FGzHeader, SizeOf(TAbGzHeader));
  if not VerifyHeader(FGzHeader) then
    Exit;

  { Skip part number, if any  }
  if (FGzHeader.Flags and AB_GZ_FLAG_FCONTINUATION) = AB_GZ_FLAG_FCONTINUATION then
    AStream.Seek(SizeOf(Word), soCurrent);

  if HasExtraField then begin
    { get length of extra data }
    AStream.Read(LenW, SizeOf(Word));
    FExtraField.LoadFromStream(AStream, LenW);
  end
  else
    FExtraField.Clear;

  { Get Filename, if any }
  if HasFileName then begin
    FRawFileName := ReadCStringInStream(AStream);
    FFileName := AbRawBytesToString(FRawFileName)
  end
  else
    FFileName := 'unknown';

  { any comment present? }
  if HasFileComment then
    FFileComment := ReadCStringInStream(AStream)
  else
    FFileComment := '';


  {Assert: stream should now be located at start of compressed data }
  {If file was compressed with 3.3 spec this will be invalid so use with care}
  CompressedSize := AStream.Size - AStream.Position - SizeOf(TAbGzTailRec);

  FDiskFileName := FileName;
  AbUnfixName(FDiskFileName);
  Action := aaNone;
  Tagged := False;
end;

procedure TAbGzipItem.SaveGzHeaderToStream(AStream: TStream);
var
  LenW : Word;
begin
  { default ID fields }
  FGzHeader.ID1 := AB_GZ_HDR_ID1;
  FGzHeader.ID2 := AB_GZ_HDR_ID2;

  { compression method }
  FGzHeader.CompMethod := 8;  { deflate }

  { reset unsupported flags }
  FGzHeader.Flags := FGzHeader.Flags and not AB_GZ_UNSUPPORTED_FLAGS;

  { main header data }
  AStream.Write(FGzHeader, SizeOf(TAbGzHeader));

  { add extra field if any }
  if HasExtraField then begin
    LenW := Length(FExtraField.Buffer);
    AStream.Write(LenW, SizeOf(LenW));
    if LenW > 0 then
      AStream.Write(FExtraField.Buffer[0], LenW);
  end;

  { add filename if any (and include final #0 from string) }
  if HasFileName then
    AStream.Write(FRawFileName[1], Length(FRawFileName) + 1);

  { add file comment if any (and include final #0 from string) }
  if HasFileComment then
    AStream.Write(FFileComment[1], Length(FFileComment) + 1);
end;

procedure TAbGzipItem.SetExternalFileAttributes(Value: LongWord);
begin
  { do nothing }
end;

procedure TAbGzipItem.SetFileComment(const Value: AnsiString);
begin
  FFileComment := Value;
  if FFileComment <> '' then
    FGzHeader.Flags := FGzHeader.Flags or AB_GZ_FLAG_FCOMMENT
  else
    FGzHeader.Flags := FGzHeader.Flags and not AB_GZ_FLAG_FCOMMENT;
end;

procedure TAbGzipItem.SetFileName(const Value: string);
begin
  FFileName := Value;
  FRawFileName := AbStringToUnixBytes(Value);
  if Value <> '' then
    FGzHeader.Flags := FGzHeader.Flags or AB_GZ_FLAG_FNAME
  else
    FGzHeader.Flags := FGzHeader.Flags and not AB_GZ_FLAG_FNAME;
end;

procedure TAbGzipItem.SetFileSystem(const Value: TAbGzFileSystem);
begin
  if Value = osUnknown then
    FGzHeader.OS := 255
  else
    FGzHeader.OS := Ord(Value);
end;

procedure TAbGzipItem.SetIsEncrypted(Value: Boolean);
begin
  { do nothing }
end;

procedure TAbGzipItem.SetIsText(const Value: Boolean);
begin
  if Value then
    FGzHeader.Flags := FGzHeader.Flags or AB_GZ_FLAG_FTEXT
  else
    FGzHeader.Flags := FGzHeader.Flags and not AB_GZ_FLAG_FTEXT;
end;

procedure TAbGzipItem.SetLastModFileDate(const Value: Word);
begin
  { replace date, keep existing time }
  LastModTimeAsDateTime :=
    EncodeDate(
      Value shr 9 + 1980,
      Value shr 5 and 15,
      Value and 31) +
    Frac(LastModTimeAsDateTime);
end;

procedure TAbGzipItem.SetLastModFileTime(const Value: Word);
begin
  { keep current date, replace time }
  LastModTimeAsDateTime :=
    Trunc(LastModTimeAsDateTime) +
    EncodeTime(
      Value shr 11,
      Value shr 5 and 63,
      Value and 31 shl 1, 0);
end;

procedure TAbGzipItem.SetLastModTimeAsDateTime(const Value: TDateTime);
begin
  FGZHeader.ModTime := AbLocalDateTimeToUnixTime(Value);
end;

{ TAbGzipArchive }

constructor TAbGzipArchive.CreateFromStream(aStream : TStream;
  const aArchiveName : string);
begin
  inherited CreateFromStream(aStream, aArchiveName);
  FState     := gsGzip;
  FGZStream  := FStream;
  FGZItem    := FItemList;
  FTarStream := TAbVirtualMemoryStream.Create;
  FTarList   := TAbArchiveList.Create(True);
end;

procedure TAbGzipArchive.SwapToTar;
begin
  FStream := FTarStream;
  FItemList := FTarList;
  FState := gsTar;
end;

procedure TAbGzipArchive.SwapToGzip;
begin
  FStream := FGzStream;
  FItemList := FGzItem;
  FState := gsGzip;
end;

function TAbGzipArchive.CreateItem(const FileSpec: string): TAbArchiveItem;
var
  GzItem : TAbGzipItem;
begin
  if IsGZippedTar and TarAutoHandle then begin
    SwapToTar;
    Result := inherited CreateItem(FileSpec);
  end
  else begin
    SwapToGzip;
    GzItem := TAbGzipItem.Create;
    try
      GzItem.CompressedSize := 0;
      GzItem.CRC32 := 0;
      GzItem.DiskFileName := ExpandFileName(FileSpec);
      GzItem.FileName := FixName(FileSpec);
      Result := GzItem;
    except
      Result := nil;
    end;
  end;
end;

destructor TAbGzipArchive.Destroy;
begin
  SwapToGzip;
  FTarList.Free;
  FTarStream.Free;
  inherited Destroy;
end;


procedure TAbGzipArchive.ExtractItemAt(Index: Integer;
  const UseName: string);
var
  OutStream : TFileStream;
  CurItem : TAbGzipItem;
begin
  if IsGZippedTar and TarAutoHandle then begin
    SwapToTar;
    inherited ExtractItemAt(Index, UseName);
  end
  else begin
    SwapToGzip;
    if Index > 0 then Index := 0; { only one item in a GZip}

    CurItem := TAbGzipItem(ItemList[Index]);

    OutStream := TFileStream.Create(UseName, fmCreate or fmShareDenyNone);
    try
      try {OutStream}
        ExtractItemToStreamAt(Index, OutStream);
      finally {OutStream}
        OutStream.Free;
      end; {OutStream}
      AbSetFileTime(UseName, CurItem.LastModTimeAsDateTime);
      AbSetFileAttr(UseName, CurItem.NativeFileAttributes);
    except
      on E : EAbUserAbort do begin
        FStatus := asInvalid;
        if FileExists(UseName) then
          DeleteFile(UseName);
        raise;
      end else begin
        if FileExists(UseName) then
          DeleteFile(UseName);
        raise;
      end;
    end;
  end;
end;

procedure TAbGzipArchive.ExtractItemToStreamAt(Index: Integer;
  aStream: TStream);
var
  GzHelp  : TAbGzipStreamHelper;
begin
  if IsGzippedTar and TarAutoHandle then begin
    SwapToTar;
    inherited ExtractItemToStreamAt(Index, aStream);
  end
  else begin
    SwapToGzip;
    { note Index ignored as there's only one item in a GZip }

    GZHelp := TAbGzipStreamHelper.Create(FGzStream);
    try
      { read GZip Header }
      GzHelp.ReadHeader;

      { extract copy data from GZip}
      GzHelp.ExtractItemData(aStream);

      { Get validation data }
      GzHelp.ReadTail;

      {$IFDEF STRICTGZIP}
      { According to
          http://www.gzip.org/zlib/rfc1952.txt

       A compliant gzip compressor should calculate and set the CRC32 and ISIZE.
       However, a compliant decompressor should not check these values.

       If you want to check the the values of the CRC32 and ISIZE in a GZIP file
       when decompressing enable the STRICTGZIP define contained in AbDefine.inc }

      { validate against CRC }
      if GzHelp.FItem.Crc32 <> GzHelp.TailCRC then
        raise EAbGzipBadCRC.Create;

      { validate against file size }
      if GzHelp.FItem.UncompressedSize <> GZHelp.TailSize then
        raise EAbGzipBadFileSize.Create;
      {$ENDIF}
    finally
      GzHelp.Free;
    end;
  end;
end;

function TAbGzipArchive.FixName(const Value: string): string;
{ fix up fileaname for storage }
begin
  if FState = gsTar then
    Result := inherited FixName( Value )
  else begin
    {GZip files Always strip the file path}
    StoreOptions := StoreOptions + [soStripDrive, soStripPath];
    Result := '';
    if Value <> '' then
      Result := ExtractFileName(Value);
  end;
end;

function TAbGzipArchive.GetIsGzippedTar: Boolean;
begin
  Result := FIsGzippedTar;
end;

function TAbGzipArchive.GetItem(Index: Integer): TAbGzipItem;
begin
  Result := nil;
  if Index = 0 then
    Result := TAbGzipItem(FItemList.Items[Index]);
end;

function TAbGzipArchive.GetSupportsEmptyFolders : Boolean;
begin
  Result := IsGzippedTar and TarAutoHandle;
end;

procedure TAbGzipArchive.LoadArchive;
var
  GzHelp : TAbGzipStreamHelper;
  Item   : TAbGzipItem;
  Abort  : Boolean;
begin
  SwapToGzip;
  if FGzStream.Size > 0 then begin
    GzHelp := TAbGzipStreamHelper.Create(FGzStream);
    try
      if GzHelp.FindFirstItem then begin
        Item := TAbGzipItem.Create;
        Item.LoadGzHeaderFromStream(FGzStream);
        FGzStream.Seek(-SizeOf(TAbGzTailRec), soEnd);
        GZHelp.ReadTail;
        Item.CRC32 := GZHelp.TailCRC;
        Item.UncompressedSize := GZHelp.TailSize;

        Item.Action := aaNone;
        FGZItem.Add(Item);

        if IsGzippedTar and TarAutoHandle then begin
          { extract Tar and set stream up }
          FTarStream.SwapFileDirectory := FTempDir;
          GzHelp.SeekToItemData;
          GzHelp.ExtractItemData(FTarStream);
          SwapToTar;
          inherited LoadArchive;
        end;
      end;

      DoArchiveProgress(100, Abort);
      FIsDirty := False;
    finally
      { Clean Up }
      GzHelp.Free;
    end;
  end;
end;

procedure TAbGzipArchive.PutItem(Index: Integer; const Value: TAbGzipItem);
begin
  if Index = 0 then
    FItemList.Items[Index] := Value;
end;

procedure TAbGzipArchive.SaveArchive;
var
  InGzHelp, OutGzHelp : TAbGzipStreamHelper;
  Abort               : Boolean;
  i                   : Integer;
  NewStream           : TAbVirtualMemoryStream;
  UncompressedStream  : TStream;
  SaveDir             : string;
  CurItem             : TAbGzipItem;
begin
  {prepare for the try..finally}
  OutGzHelp := nil;
  NewStream := nil;

  try
    InGzHelp := TAbGzipStreamHelper.Create(FGzStream);

    try
      {init new archive stream}
      NewStream := TAbVirtualMemoryStream.Create;
      OutGzHelp := TAbGzipStreamHelper.Create(NewStream);

      { create helper }
      NewStream.SwapFileDirectory := FTempDir;

      { save the Tar data }
      if IsGzippedTar and TarAutoHandle then begin
        SwapToTar;
        inherited SaveArchive;
        if FGZItem.Count = 0 then begin
          CurItem := TAbGzipItem.Create;
          FGZItem.Add(CurItem);
        end;
        CurItem := FGZItem[0] as TAbGzipItem;
        CurItem.Action := aaNone;
        CurItem.LastModTimeAsDateTime := Now;
        CurItem.SaveGzHeaderToStream(NewStream);
        FTarStream.Position := 0;
        OutGzHelp.WriteArchiveItem(FTarStream);
        CurItem.CRC32 := OutGzHelp.CRC;
        CurItem.UncompressedSize := OutGzHelp.FileSize;
        OutGzHelp.WriteArchiveTail;
      end
      else begin
        SwapToGzip;

        {build new archive from existing archive}
        for i := 0 to pred(Count) do begin
          FCurrentItem := ItemList[i];
          CurItem      := TAbGzipItem(ItemList[i]);
          InGzHelp.SeekToItemData;

          case CurItem.Action of
            aaNone, aaMove : begin
            {just copy the file to new stream}
              CurItem.SaveGzHeaderToStream(NewStream);
              InGzHelp.SeekToItemData;
              NewStream.CopyFrom(FGZStream, FGZStream.Size - FGZStream.Position);
            end;

            aaDelete: {doing nothing omits file from new stream} ;

            aaAdd, aaFreshen, aaReplace, aaStreamAdd: begin
              try
                if (CurItem.Action = aaStreamAdd) then begin
                { adding from a stream }
                  CurItem.SaveGzHeaderToStream(NewStream);
                  CurItem.UncompressedSize := InStream.Size;
                  OutGzHelp.WriteArchiveItem(InStream);
                  OutGzHelp.WriteArchiveTail;
                end
                else begin
                { it's coming from a file }
                  GetDir(0, SaveDir);
                  try {SaveDir}
                    if (BaseDirectory <> '') then
                      ChDir(BaseDirectory);
                    CurItem.LastModTimeAsDateTime := AbGetFileTime(CurItem.DiskFileName);
                    UncompressedStream := TFileStream.Create(CurItem.DiskFileName,
                      fmOpenRead or fmShareDenyWrite );
                  finally {SaveDir}
                    ChDir( SaveDir );
                  end; {SaveDir}

                  try
                    CurItem.UncompressedSize := UncompressedStream.Size;
                    CurItem.SaveGzHeaderToStream(NewStream);
                    OutGzHelp.WriteArchiveItem(UncompressedStream);
                    OutGzHelp.WriteArchiveTail;

                  finally {UncompressedStream}
                    UncompressedStream.Free;
                  end; {UncompressedStream}
                end;
              except
                ItemList[i].Action := aaDelete;
                DoProcessItemFailure(ItemList[i], ptAdd, ecFileOpenError, 0);
              end;
            end;
          end; {case}
        end; { for }
      end;
    finally
      InGzHelp.Free;
    end;

    {copy new stream to FStream}
    SwapToGzip;
    NewStream.Position := 0;
    if (FStream is TMemoryStream) then
      TMemoryStream(FStream).LoadFromStream(NewStream)
    else if FOwnsStream then begin
      { need new stream to write }
      FreeAndNil(FStream);
      FGZStream := nil;
      FStream := TFileStream.Create(FArchiveName, fmCreate or fmShareDenyWrite);
      FGZStream := FStream;
      FStream.CopyFrom(NewStream, NewStream.Size);
    end
    else begin
      FStream.Size := 0;
      FStream.Position := 0;
      FStream.CopyFrom(NewStream, NewStream.Size);
    end;

    {update Items list}
    for i := pred( Count ) downto 0 do begin
      if ItemList[i].Action = aaDelete then
        FItemList.Delete( i )
      else if ItemList[i].Action <> aaFailed then
        ItemList[i].Action := aaNone;
    end;

    if IsGzippedTar and TarAutoHandle then
      SwapToTar;

    DoArchiveSaveProgress( 100, Abort );
    DoArchiveProgress( 100, Abort );
  finally {NewStream}
    OutGzHelp.Free;
    NewStream.Free;
  end;
end;

procedure TAbGzipArchive.SetTarAutoHandle(const Value: Boolean);
begin
  if Value then
    SwapToTar
  else
    SwapToGzip;
  FTarAutoHandle := Value;
end;

procedure TAbGzipArchive.TestItemAt(Index: Integer);
var
  SavePos   : LongInt;
  GZType    : TAbArchiveType;
  BitBucket : TAbBitBucketStream;
  GZHelp    : TAbGzipStreamHelper;
begin
  if IsGzippedTar and TarAutoHandle then begin
    inherited TestItemAt(Index);
  end
  else begin
    { note Index ignored as there's only one item in a GZip }
    SavePos := FGzStream.Position;
    GZType := VerifyGZip(FGZStream);
    if not (GZType in [atGZip, atGZippedTar]) then
      raise EAbGzipInvalid.Create;

    BitBucket := nil;
    GZHelp := nil;
    try
      BitBucket := TAbBitBucketStream.Create(1024);
      GZHelp := TAbGzipStreamHelper.Create(FGZStream);

      GZHelp.ExtractItemData(BitBucket);
      GZHelp.ReadTail;

      { validate against CRC }
      if GzHelp.FItem.Crc32 <> GZHelp.TailCRC then
        raise EAbGzipBadCRC.Create;

      { validate against file size }
      if GzHelp.FItem.UncompressedSize <> GZHelp.TailSize then
        raise EAbGzipBadFileSize.Create;

    finally
      GZHelp.Free;
      BitBucket.Free;
    end;

    FGzStream.Position := SavePos;
  end;
end;

procedure TAbGzipArchive.DoSpanningMediaRequest(Sender: TObject;
  ImageNumber: Integer; var ImageName: string; var Abort: Boolean);
begin
  Abort := False;
end;

end.
