/// high-level access to .zip archive file compression // - this unit is a part of the freeware Synopse framework, // licensed under a MPL/GPL/LGPL tri-license; version 1.18 unit SynZipFiles; (* This file is part of Synopse framework. Synopse framework. Copyright (C) 2022 Arnaud Bouchez Synopse Informatique - https://synopse.info *** BEGIN LICENSE BLOCK ***** Version: MPL 1.1/GPL 2.0/LGPL 2.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 Synopse framework. The Initial Developer of the Original Code is Arnaud Bouchez. Portions created by the Initial Developer are Copyright (C) 2022 the Initial Developer. All Rights Reserved. Contributor(s): Alternatively, the contents of this file may be used under the terms of either the GNU General Public License Version 2 or later (the "GPL"), or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), in which case the provisions of the GPL or the LGPL are applicable instead of those above. If you wish to allow use of your version of this file only under the terms of either the GPL or the LGPL, and not to allow others to use your version of this file under the terms of the MPL, indicate your decision by deleting the provisions above and replace them with the notice and other provisions required by the GPL or the LGPL. If you do not delete the provisions above, a recipient may use your version of this file under the terms of any one of the MPL, the GPL or the LGPL. ***** END LICENSE BLOCK ***** *) interface {$I Synopse.inc} // define HASINLINE CPU32 CPU64 uses {$ifdef MSWINDOWS} Windows, // USEPDF: under Windows: get ZLib 1.2.3 functions from libpdf.dll {$ifdef USEPDF}pdf,{$endif} {$else} {$ifdef FPC} SynFPCLinux, {$endif} {$ifdef KYLIX3} Types, {$endif} {$endif} SynCommons, Classes, SysUtils, SynZip; { Proprietary compression/encryption adding to standard zip files - the .zip file structure is 100% compatible with standard - data is stored uncompressed, but passed via TSynCompressionAlgo class - algorithms are ID-identified (from 1 to 15) - algorithm registration is made with SynCompressionAlgos.AlgoRegister - ID is stored in unused 7..10 bits of "flags" zip entry (cf. PKware appnote) - TSynCompressionAlgo is called by 64KB chunks or once for whole data - inherit from TSynCompressionAlgoBuf to simply handle 64KB chunks - Synopse has registered several TSynCompressionAlgo IDs: 1=SynLZ-chunked 2=SynLZ-whole 3=LzoAsm-chunked 4=LzoAsm-whole 5=Bz2-chunked 6=AES-chunked 7=AES+Zip-chunked 8=AES+SynLz-chunked so you can use 9..15 for your own purpose - most of this unit functions are TSynCompressionAlgo aware } type TZipException = class(Exception); TSynCompressionAlgo = class protected fDestStream: TStream; public /// initialize compression into OutStream procedure CompressInit(OutStream: TStream); virtual; /// compress InP[InLen] into OutStream + update CRC, return compressed length function Compress(InP: pointer; InLen: cardinal; CRC: PCardinal): cardinal; virtual; abstract; /// called once at the end for compression flush, return compressed length // (default implementation: just do nothing) function CompressFinish: cardinal; virtual; /// return uncompressed length of InP[InLen] for proper mem allocation function UnCompressedLength(InP: pointer; InLen: cardinal): cardinal; virtual; abstract; /// uncompress InP[InLen] into OutP, return uncompressed length (called once for decompression) function UnCompress(InP: pointer; InLen: cardinal; OutP: pointer): cardinal; virtual; abstract; end; TSynCompressionAlgoClass = class of TSynCompressionAlgo; {$ifdef USERECORDWITHMETHODS}TSynCompressionAlgos = record {$else}TSynCompressionAlgos = object{$endif} public Values: array of record ID, WholeID: integer; func: TSynCompressionAlgoClass; end; procedure AlgoRegister(aAlgo: TSynCompressionAlgoClass; aID,aWholeID: integer); function Algo(aID: integer): TSynCompressionAlgoClass; function WholeAlgoID(aID: integer): integer; end; /// template class for 64KB chunked (not whole) algorithm (SynLZ, LZO...) // which forces storing as uncompressed if compression ratio has no gain TSynCompressionAlgoBuf = class(TSynCompressionAlgo) protected // fast tmp buffer (size=worse case with 64KB chunk) fCompressBuf: PAnsiChar; function AlgoCompress(src: PAnsiChar; size: integer; dst: PAnsiChar): integer; virtual; abstract; function AlgoCompressLength(size: integer): integer; virtual; abstract; function AlgoUnCompress(src: PAnsiChar; size: integer; dst: PAnsiChar): integer; virtual; abstract; function AlgoUnCompressLength(src: PAnsiChar; size: integer): integer; virtual; abstract; public /// initialize compression into OutStream procedure CompressInit(OutStream: TStream); override; /// free fCompressBuf memory if allocated destructor Destroy; override; /// compress InP[InLen] into OutStream + update CRC, return compressed length function Compress(InP: pointer; InLen: cardinal; CRC: PCardinal): cardinal; override; /// return uncompressed length of InP[InLen] for proper mem allocation function UnCompressedLength(InP: pointer; InLen: cardinal): cardinal; override; /// uncompress InP[InLen] into OutP, return uncompressed length function UnCompress(InP: pointer; InLen: cardinal; OutP: pointer): cardinal; override; end; /// template class for whole algorithm (SynLZ, LZO...) // which forces storing as uncompressed if compression ratio has no gain TSynCompressionAlgoWhole = class(TSynCompressionAlgo) protected function AlgoCompress(src: PAnsiChar; size: integer; dst: PAnsiChar): integer; virtual; abstract; function AlgoCompressLength(size: integer): integer; virtual; abstract; function AlgoUnCompress(src: PAnsiChar; size: integer; dst: PAnsiChar): integer; virtual; abstract; function AlgoUnCompressLength(src: PAnsiChar; size: integer): integer; virtual; abstract; public /// compress InP[InLen] into OutStream + update CRC, return compressed length function Compress(InP: pointer; InLen: cardinal; CRC: PCardinal): cardinal; override; /// return uncompressed length of InP[InLen] for proper mem allocation function UnCompressedLength(InP: pointer; InLen: cardinal): cardinal; override; /// uncompress InP[InLen] into OutP, return uncompressed length function UnCompress(InP: pointer; InLen: cardinal; OutP: pointer): cardinal; override; end; TZipCompressor = class(TStream) private fInitialized: Boolean; fDestStream: TStream; fStrm: TZStream; fAlgorithm: TSynCompressionAlgo; fAlgorithmStream: THeapMemoryStream; fAlgorithmID: integer; // =0 if not Assigned(fAlgorithm) fCRC: Cardinal; fBlobDataHeaderPosition: Int64; fBufferIn, fBufferOut: array[word] of byte; // two 64kb buffers procedure Finish; function FlushBufferOut: integer; function InFlateDeflate: boolean; // return true if error function GetSizeIn: cardinal; function GetSizeOut: cardinal; public constructor Create(outStream: TStream; CompressionLevel: Integer; Algorithm: integer=0); constructor CreateAsBlobData(outStream: TStream; CompressionLevel: Integer; Algorithm: integer=0); destructor Destroy; override; function Read(var Buffer; Count: Longint): Longint; override; function Write(const Buffer; Count: Longint): Longint; override; function WriteOnce(const Buffer; Count: Longint): Longint; function Seek(Offset: Longint; Origin: Word): Longint; override; property SizeIn: cardinal read GetSizeIn; property SizeOut: cardinal read GetSizeOut; property CRC: cardinal read fCRC; end; TGzWriter = class(TZipCompressor) private outFile: TStream; outFileToBeFree: boolean; public constructor Create(const aFileName: TFileName); overload; constructor Create(const aDestStream: TStream); overload; // use Write() to add some data destructor Destroy; override; end; PZipEntry = ^TZipEntry; TZipEntry = {$ifdef UNICODE} record {$else} object {$endif} ZipName: RawUTF8; Header: TFileHeader; // as stored in standard .zip file function SameAs(const aEntry: TZipEntry): boolean; // test if algo is registered, perform crc32 check and create one instance function AlgoCreate(data: pointer; const FileName: TFileName): TSynCompressionAlgo; function LocalHeader(ZipStart: PAnsiChar): PLocalFileHeader; function LocalDataPosition(ZipStart: PAnsiChar): PtrUInt; end; TZipCommon = class private fCount: integer; fFileName: TFileName; public Entry: array of TZipEntry; constructor Create(const aFileName: TFileName); destructor Destroy; override; function ZipNameIndexOf(const aZipName: RawUTF8): integer; property Count: integer read fCount; property FileName: TFileName read fFileName; end; // used to transfert Blob Data from/to Client without compress/uncompress: {$A-} PBlobData = ^TBlobData; {$ifdef USERECORDWITHMETHODS}TBlobData = record {$else}TBlobData = object{$endif} private // test if algo is registered, perform crc32 check and create one instance function AlgoCreate(data: pointer): TSynCompressionAlgo; public dataSize, // always used dataFullSize, dataCRC: cardinal; // used only if AlreadyCompressed dataMethod: byte; // 0=stored 8=inflate >=16: AlgoID=dataMethod shr 4 databuf: AnsiChar; function AlgoID: cardinal; procedure SetFrom(const FileInfo: TFileInfo); // uncompress if necessary: function Expand: RawByteString; // do freemem() if dataMethod<>0; no direct AES since may be mapped function ExpandBuf(out destSize: cardinal): pointer; procedure ExpandStream(Stream: TStream); function Next: PAnsiChar; // points to next bloc end; {$A8} const BLOBDATA_HEADSIZE = sizeof(TBlobData)-sizeof(AnsiChar); // databuf: AnsiChar type TZipReader = class(TZipCommon) protected fMap: TMemoryMap; public constructor Create(const aFileName: TFileName); destructor Destroy; override; procedure Clear; // force Count=0 function GetData(aIndex: integer; aStream: TStream=nil; CheckCRC: boolean=false; asBlobDataStored: boolean=false; withAlgoDataLen: boolean=false): PAnsiChar; function GetString(aIndex: integer): RawByteString; function GetBuffer(aIndex: integer; out Dest: PAnsiChar): integer; function GetBlobData(aIndex: integer): RawByteString; overload; // PBlobData(result) procedure GetBlobData(aIndex: integer; aStream: TStream); overload; // TBlobData->aStream procedure DeleteLastEntry; // don't use inside TZipValues: already done in Create procedure SaveToStream(aStream: TStream); // save uncompressed to stream function SameAs(aReader: TZipReader): boolean; property Map: TMemoryMap read fMap; end; TZipWriter = class(TZipCommon) private outFile: TFileStream; fNow: integer; fDestFileName: TFileName; function AddEntry(const aZipName: RawUTF8; FileAge: integer = 0): PZipEntry; public Zip: TZipCompressor; forceFileAge: integer; // <>0 -> will be used in Destroy to dest file constructor Create(const aFileName: TFileName); overload; constructor Create(AppendTo: TZipReader; ReCreate: boolean=false); overload; constructor Create(fromStream: TStream; const DestFilename: TFileName=''); overload; // restore after TZipReader.SaveToStream() destructor Destroy; override; /// this Creates a TZipCompressor -> user Zip.Write() to send data: // if CompressionLevel<0: direct copy procedure ZipCreate(const aZipName: RawUTF8; CompressionLevel: integer; FileAge: integer = 0; Algorithm: integer=0); /// compression finish, fileInfo update+save, Zip.Free; // after ZipCreate: let aIndex=-1 will update Entry[Count]+inc(fCount) procedure ZipClose(aIndex: integer=-1); procedure Add(const aZipName: RawUTF8; data: PAnsiChar; dataSize: cardinal; CompressionLevel: integer; dataCRC: pCardinal=nil; FileAge: integer = 0; Algorithm: integer=0); overload; procedure Add(const aZipName: RawUTF8; p: PBlobData); overload; procedure Add(aReader: TZipReader; aReaderIndex: integer); overload; procedure AddFile(const aFileName: TFileName; const aZipName: RawUTF8; CompressionLevel: integer; Algorithm: integer=0); function LastCRC32Added: cardinal; end; // TZip handles ZIP standard files on disk TZip = class private FileQueue: TStringList; SomeDeleted: boolean; public Reader: TZipReader; Writer: TZipWriter; constructor Create(const aFileName: TFileName); destructor Destroy; override; function FileName: TFileName; function MarkDeleted(aReaderIndex: integer): boolean; virtual; // before any ZipCreate function MarkDeletedBefore(aDate: TDateTime; aBackup: TZip=nil): boolean; procedure BeginWriter; virtual; function ZipCreate(const aZipName: RawUTF8; CompressionLevel: integer; Algorithm: integer=0): TZipCompressor; // use Zip.Write() to send data before ZipClose procedure ZipClose; function AddBuf(const aZipName: RawUTF8; CompressionLevel: integer; data: pointer; dataSize: cardinal; Algorithm: integer=0): boolean; function AddToFileQueue(const aFileName: TFileName; const aZipName: RawUTF8): boolean; // flushed at Destroy function SameAs(aZip: TZip): boolean; function FileQueueCount: integer; end; TZipValues = class(TZip) // store some Values[] in a .zip file (TBlob, TBlobDiff, TDC4...) protected // all this must be overrided according to Values[]: procedure LoadValues(data: PAnsiChar); virtual; abstract; // SetLength(Values,Count+10); move(data,Values[0],Count*sizeof(Values[0])); procedure SaveValues(Zip: TZipCompressor); virtual; abstract; public Count: integer; modified: boolean; constructor Create(const aFileName: TFileName); // make Reader.Create destructor Destroy; override; function GetValue(aReaderIndex: integer; aStream: TStream=nil): PAnsiChar; procedure CopyValue(source, dest: integer); virtual; abstract; // Values[dest] := Values[source]; procedure BeginWriter; override; function MarkDeleted(aReaderIndex: integer): boolean; override; // before any AddValue end; var BlobDataNull: TBlobData; SynCompressionAlgos: TSynCompressionAlgos; procedure CompressAsBlobData(const data; size: integer; aStream: TStream; CompressionLevel: integer=6; Algorithm: integer=0); // create a TBlobData in aStream - can use encryption with algo // 7=AES+Zip-chunked and 8=AES+SynLz-chunked function GZRead(const aFileName: TFileName): RawByteString; overload; function GZRead(gz: PAnsiChar; gzLen: integer): RawByteString; overload; procedure GZRead(const aFileName: TFileName; aStream: TStream; StoreLen: boolean); overload; // direct uncompress .gz file into string or TStream implementation const // TZipException messages: sZlibInternalError = 'zlib: Internal error'; sIncorrectZipFormatN = 'Incorrect zip format in file %s: %s'; sZipAlgoIDNUnknownN = 'Algo ID %d unknown for %s'; sZipCrcErrorNN = 'crc32 checksum error for %s in %s'; { TZipCommon } constructor TZipCommon.Create(const aFileName: TFileName); begin fFileName := aFileName; end; destructor TZipCommon.Destroy; begin Finalize(Entry); inherited; end; function TZipCommon.ZipNameIndexOf(const aZipName: RawUTF8): integer; begin for result := 0 to Count-1 do if SameTextU(Entry[result].ZipName,aZipName) then exit; result := -1; end; { TZipReader } procedure TZipReader.Clear; begin fCount := 0; Map.UnMap; end; constructor TZipReader.Create(const aFileName: TFileName); var i: integer; lhr: ^TLastHeader; H: ^TFileHeader; tmp: WinAnsiString; LastHeaderPosition: integer; procedure Error(const msg: string); begin // MessageBox(0,pointer(fFileName),'Incorrect format',MB_ICONERROR); Map.UnMap; fCount := 0; raise TZipException.CreateFmt(sIncorrectZipFormatN,[fFileName,msg]); end; begin // 1. open aFileName inherited; // fFileName := aFileName; Map.Map(fFileName); if Map.Buffer=nil then exit; // 2. find last header, in order to reach the TFileHeader entries if Map.Size$06054b50 then begin Error('missing trailing signature'); exit; end; fCount := thisFiles; SetLength(Entry,Count); LastHeaderPosition := headerOffset; end; // 3. read all TFileHeader entries and fill Entry[] with its values H := @Map.Buffer[LastHeaderPosition]; for i := 0 to Count-1 do with H^ do begin if signature<>$02014b50 then begin Error('missing local signature'); break; end; if (fileInfo.flags and (1 shl 3)<>0) or // crc+sizes in "data descriptor" (fileInfo.zzipSize=0) or (fileInfo.zfullSize=0) then begin Error('unexpected "data descriptor"'); break; // not handled yet: use SynZip's TZipRead to access this archive end; with Entry[i] do begin {$ifdef MSWINDOWS} if FileInfo.GetUTF8FileName then SetString(ZipName,PAnsiChar(H)+sizeof(H^),fileInfo.nameLen) else begin SetLength(tmp,fileInfo.nameLen); // convert from DOS/OEM into WinAnsi OemToCharBuffA(PAnsiChar(H)+sizeof(H^),pointer(tmp),fileInfo.nameLen); ZipName := WinAnsiToUtf8(tmp); end; {$else} SetString(ZipName,PAnsiChar(H)+sizeof(H^),fileInfo.nameLen); {$endif} ZipName := StringReplaceChars(ZipName,'/','\'); Header := H^; end; // next entry is after the ZipNname and some extra/comment inc(PByte(H),sizeof(H^)+fileInfo.nameLen+fileInfo.extraLen+commentLen); end; end; procedure TZipReader.DeleteLastEntry; // don't use inside TZipValues: already done in TZipValues.Create begin if Count<=0 then exit; dec(fCount); end; destructor TZipReader.Destroy; begin Map.UnMap; inherited; end; function TZipReader.GetBlobData(aIndex: integer): RawByteString; begin result := ''; if (self=nil) or (cardinal(aIndex)>=cardinal(Count)) or (Map.Buffer=nil) then exit; with Entry[aIndex], LocalHeader(Map.Buffer)^ do if not Header.IsFolder then begin SetLength(result,fileInfo.zzipSize+BLOBDATA_HEADSIZE); with PBlobData(result)^ do begin SetFrom(fileInfo); move(LocalData^,databuf,datasize); end; end; end; procedure TZipReader.GetBlobData(aIndex: integer; aStream: TStream); // put TBlobData in aStream var blob: TBlobData; begin if (self=nil) or (aIndex<0) or (aIndex>=Count) or (Map.Buffer=nil) then aStream.WriteBuffer(BlobDataNull,BLOBDATA_HEADSIZE) else with Entry[aIndex], LocalHeader(Map.Buffer)^ do begin blob.SetFrom(fileInfo); aStream.WriteBuffer(blob,BLOBDATA_HEADSIZE); aStream.WriteBuffer(LocalData^,blob.dataSize); end; end; function TZipReader.GetBuffer(aIndex: integer; out Dest: PAnsiChar): integer; begin result := 0; Dest := nil; if (self=nil) or (aIndex<0) or (aIndex>=Count) or (Map.Buffer=nil) then exit; with Entry[aIndex], LocalHeader(Map.Buffer)^, fileInfo do if not Header.IsFolder then begin result := zfullSize; if AlgoID<>0 then begin with AlgoCreate(LocalData,FileName) do // crc32+algo object create try // algo registered (no TZipException raised) -> uncompress result := UnCompressedLength(LocalData,zfullsize); Getmem(Dest,result); UnCompress(LocalData,zfullsize,Dest); // direct uncompress into Dest finally Free; end; exit; end; Getmem(Dest,zfullSize); if zfullSize>0 then case zzipMethod of 0: move(LocalData^,Dest^,result); // stored = direct copy 8: if (UnCompressMem(LocalData,Dest,zzipSize,zfullSize)<>integer(zfullSize)) or (crc32(0,Dest,zfullSize)<>zcrc32) then begin Freemem(Dest); Dest := nil; raise TZipException.CreateFmt(sZipCrcErrorNN,[Entry[aIndex].ZipName,FileName]); end; end; end; end; function TZipReader.GetData(aIndex: integer; aStream: TStream=nil; CheckCRC: boolean=false; asBlobDataStored: boolean=false; withAlgoDataLen: boolean=false): PAnsiChar; // aStream=nil -> return bulk memory data position in mapped file // aStream<>nil -> uncompress and un-algo into aStream; CheckCRC=true -> force check CRC // asBlobDataStored=true -> PBlobData stored format into aStream // withAlgoDataLen=true -> unCompressed algo length stored into aStream var CRC: cardinal; CRCP: PCardinal; Blob: TBlobData; tmp: PAnsiChar; L: cardinal; begin result := nil; if (self=nil) or (aIndex<0) or (aIndex>=Count) or (Map.Buffer=nil) then exit; with Entry[aIndex], LocalHeader(Map.Buffer)^ do if not Header.IsFolder then begin result := LocalData; if aStream=nil then exit; // no decompress to stream: only get result PAnsiChar [and DataLen^] if fileInfo.AlgoID<>0 then begin // bits 7..10 are used for algo // un-algo specific uncompression L := GetBuffer(aIndex,tmp); // uncompress with algo (always check crc) if tmp<>nil then try if asBlobDataStored then begin // algo: add uncompressed data header Blob.dataSize := L; Blob.dataFullSize := L; Blob.dataCRC := crc32(0,tmp,L); Blob.dataMethod := fileInfo.AlgoID shl 4; // 0=stored + AlgoID aStream.WriteBuffer(Blob,BLOBDATA_HEADSIZE); end else if withAlgoDataLen then aStream.WriteBuffer(L,4); aStream.WriteBuffer(tmp^,L); // write uncompressed finally freemem(tmp); end; end else begin // standard zip format if asBlobDataStored then begin Blob.dataSize := FileInfo.zfullSize; Blob.dataFullSize := FileInfo.zfullSize; Blob.dataCRC := FileInfo.zcrc32; Blob.dataMethod := 0; // stored, since will be uncompressed below aStream.WriteBuffer(Blob,BLOBDATA_HEADSIZE); end; case fileInfo.zzipMethod of 0: begin aStream.WriteBuffer(result^,fileInfo.zfullSize); // stored = direct copy if CheckCRC then CRC := crc32(0,result,fileInfo.zfullSize); end; 8: begin // deflate if CheckCRC then CRCP := @CRC else CRCP := nil; if UnCompressStream(result,fileInfo.zzipSize,aStream,CRCP) <>fileInfo.zfullSize then result := nil; end; end; // case fileInfo.zzipMethod of if CheckCRC and (CRC<>fileInfo.zcrc32) then result := nil; end; end; end; function TZipReader.GetString(aIndex: integer): RawByteString; begin result := ''; if (self=nil) or (aIndex<0) or (aIndex>=Count) or (Map.Buffer=nil) then exit; with Entry[aIndex], LocalHeader(Map.Buffer)^, fileInfo do if not Header.IsFolder then begin if AlgoID<>0 then begin // special algo with AlgoCreate(LocalData,FileName) do // crc32+algo object create try // algo registered (no TZipException raised) -> uncompress SetLength(result,UnCompressedLength(LocalData,zfullsize)); if UnCompress(LocalData,zfullsize,pointer(result))<> // direct uncompress into string cardinal(length(result)) then raise TZipException.CreateFmt(sZipCrcErrorNN,[Entry[aIndex].ZipName,FileName]); finally Free; end; end else // no algo: normal .zip file case zzipMethod of 0: SetString(result,LocalData,zfullSize); // stored = direct copy 8: begin // deflate: SetLength(result,zfullSize); if (UnCompressMem(LocalData,pointer(result),zzipSize,zfullSize)<>integer(zfullSize)) or (crc32(0,pointer(result),zfullSize)<>zcrc32) then raise TZipException.CreateFmt(sZipCrcErrorNN,[Entry[aIndex].ZipName,FileName]); end; end; end; end; function TZipReader.SameAs(aReader: TZipReader): boolean; var i: integer; begin result := self=aReader; if result then exit; if (self=nil) or (aReader=nil) or (Count<>aReader.Count) then exit; for i := 0 to Count-1 do if not Entry[i].SameAs(aReader.Entry[i]) then exit; for i := 0 to Count-1 do with Entry[i], LocalHeader(Map.Buffer)^ do if not Header.IsFolder then if not Comparemem(LocalData, aReader.Entry[i].LocalHeader(aReader.Map.Buffer).LocalData,fileInfo.zzipSize) then exit; result := true; end; procedure TZipReader.SaveToStream(aStream: TStream); var i: integer; L: cardinal; aName: RawUTF8; begin aName := StringToUTF8(ExtractFileName(fFileName)); // 1. write global params L := length(aName); aStream.WriteBuffer(L,1); aStream.WriteBuffer(aName[1],L); // UTF-8 encoded file name aStream.WriteBuffer(fCount,4); // 2. write Entry[].ZipName for i := 0 to Count-1 do with Entry[i] do begin assert(not Header.IsFolder,'empty folders streaming is not implemented'); aStream.WriteBuffer(Header.fileInfo,sizeof(Header.fileInfo)); aStream.WriteBuffer(pointer(ZipName)^,Header.fileInfo.NameLen); end; // 3. write all uncompressed data for i := 0 to Count-1 do with Entry[i] do // withAlgoDataLen=true: algo -> uncompressed length stored GetData(i,aStream,false,false,true); // deflate and un-algo if necessary end; { TZip } function TZip.AddBuf(const aZipName: RawUTF8; CompressionLevel: integer; data: pointer; dataSize: cardinal; Algorithm: integer=0): boolean; var Z: TZipCompressor; begin if self=nil then begin result := false; exit; end; if dataSize<512 then CompressionLevel := -1; // force store if too small Z := ZipCreate(aZipName,CompressionLevel,Algorithm); // Z=Writer.Zip result := Z<>nil; if not result then exit; Z.WriteOnce(data^,dataSize); ZipClose; end; function TZip.AddToFileQueue(const aFileName: TFileName; const aZipName: RawUTF8): boolean; // flushed at Destroy var i: integer; begin result := false; if self=nil then exit; if Writer=nil then begin i := Reader.ZipNameIndexOf(aZipName); if i>=0 then MarkDeleted(i); end else if Writer.ZipNameIndexOf(aZipName)>=0 then exit; if FileQueue=nil then FileQueue := TStringList.Create; FileQueue.Values[aFileName] := UTF8ToString(aZipName); result := true; end; procedure TZip.BeginWriter; begin if (self<>nil) and (Writer=nil) then Writer := TZipWriter.Create(Reader); // append Reader, recreate=false end; constructor TZip.Create(const aFileName: TFileName); begin Reader := TZipReader.Create(aFileName); // Writer will be created as necessary end; function GetValueFromIndex(List: TStrings; Index: Integer): string; begin // not defined before Delphi 7 if Index >= 0 then Result := Copy(List[Index],Length(List.Names[Index])+2,MaxInt) else Result := ''; end; destructor TZip.Destroy; var i, method: integer; zipName: TFileName; begin ZipClose; // close pending Writer.Zip if any if SomeDeleted and (Writer=nil) and ((FileQueue=nil) or (FileQueue.Count=0)) then BeginWriter else if FileQueue<>nil then for i := 0 to FileQueue.Count-1 do begin zipName := GetValueFromIndex(FileQueue,i); if zipName='' then continue; if GetFileNameExtIndex(zipName,'zip,jpg,jpeg,gz,bz2,bZ,7z,gif,bj,bjt')>=0 then method := -1 else // store already compressed file method := 6; // normal deflate compression if Writer=nil then BeginWriter; Writer.AddFile(FileQueue.Names[i],StringToUTF8(zipName),method); end; FreeAndNil(FileQueue); FreeAndNil(Reader); FreeAndNil(Writer); inherited; end; function TZip.FileName: TFileName; begin if (self=nil) or (Reader=nil) then result := '' else result := Reader.fFileName; end; function TZip.FileQueueCount: integer; begin if FileQueue=nil then result := 0 else result := FileQueue.Count; end; function TZip.MarkDeleted(aReaderIndex: integer): boolean; begin result := (self<>nil)and(Writer=nil)and(aReaderIndex>=0)and(aReaderIndex 2107) then Result := 0 else begin DecodeTime(DateTime, Hour, Min, Sec, MSec); LongRec(Result).Lo := (Sec shr 1) or (Min shl 5) or (Hour shl 11); LongRec(Result).Hi := Day or (Month shl 5) or ((Year - 1980) shl 9); end; end; function NowToFileDateWindows: Integer; begin result := DateTimeToFileDateWindows(Now); end; {$endif} function TZip.MarkDeletedBefore(aDate: TDateTime; aBackup: TZip=nil): boolean; var dt, i: integer; begin result := false; if (self=nil) or (Writer<>nil) then exit; dt := {$ifdef MSWINDOWS}DateTimeToFileDate{$else}DateTimeToFileDateWindows{$endif}(aDate); for i := 0 to Reader.Count-1 do with Reader.Entry[i].Header do if (signature<>0) and (fileInfo.zlastModnil then begin aBackup.BeginWriter; aBackup.Writer.Add(Reader,i); end; MarkDeleted(i); result := true; end; end; function TZip.SameAs(aZip: TZip): boolean; begin result := self=aZip; if result then exit; if (self=nil) or (aZip=nil) then exit; assert(Writer=nil); if Writer<>nil then exit; result := Reader.SameAs(aZip.Reader); end; procedure TZip.ZipClose; begin if (self=nil) or (Writer=nil) then exit; Writer.ZipClose; end; function TZip.ZipCreate(const aZipName: RawUTF8; CompressionLevel: integer; Algorithm: integer=0): TZipCompressor; // use TZipCompressor.Write() to send data - CompressionLevel<0 -> force store var i: integer; ZipName: string; begin result := nil; if self=nil then exit; if Writer=nil then begin i := Reader.ZipNameIndexOf(aZipName); if (i>=0) then MarkDeleted(i); end else if Writer.ZipNameIndexOf(aZipName)>=0 then exit; if FileQueue<>nil then begin ZipName := UTF8ToString(aZipName); for i := 0 to FileQueue.Count-1 do if GetValueFromIndex(FileQueue,i)=ZipName then begin FileQueue.Delete(i); break; end; end; BeginWriter; if Writer.Zip<>nil then exit; Writer.ZipCreate(aZipName,CompressionLevel,0,Algorithm); result := Writer.Zip; end; { TCompressorDecompressor } constructor TZipCompressor.Create(outStream: TStream; CompressionLevel, Algorithm: Integer); var Algo: TSynCompressionAlgoClass; begin fDestStream := outStream; fBlobDataHeaderPosition := -1; // not AsBlobData StreamInit(FStrm); FStrm.next_out := @FBufferOut; FStrm.avail_out := SizeOf(FBufferOut); FStrm.next_in := @FBufferIn; if Algorithm<>0 then begin Algo := SynCompressionAlgos.Algo(Algorithm); if not Assigned(Algo) then // unknown algo -> error raise TZipException.CreateFmt(sZipAlgoIDNUnknownN,[Algorithm,ClassName]); fAlgorithm := Algo.Create; fAlgorithmID := Algorithm; fAlgorithm.CompressInit(fDestStream); if SynCompressionAlgos.WholeAlgoID(Algorithm)=Algorithm then // whole algo = not a 64KB chunked algo fAlgorithmStream := THeapMemoryStream.Create; // create temp buffer end else begin if CompressionLevel>=0 then // FInitialized=false -> direct copy to FDestStream fInitialized := Check(deflateInit2_(FStrm, CompressionLevel, Z_DEFLATED, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,ZLIB_VERSION, sizeof(FStrm)), [Z_OK])=Z_OK; // -MAX_WBITS -> no zLib header => .zip compatible ! end; end; constructor TZipCompressor.CreateAsBlobData(outStream: TStream; CompressionLevel, Algorithm: Integer); begin Create(outStream,CompressionLevel,Algorithm); fBlobDataHeaderPosition := outStream.Seek(0,soCurrent); outStream.WriteBuffer(FBufferOut,BLOBDATA_HEADSIZE); // save Bulk header end; destructor TZipCompressor.Destroy; var p: Int64; blob: TBlobData; begin if FInitialized then begin FStrm.next_out := nil; FStrm.avail_out := 0; deflateEnd(FStrm); end else begin FreeAndNil(fAlgorithmStream); FreeAndNil(fAlgorithm); end; if fBlobDataHeaderPosition>=0 then begin // CreateAsBlobData() -> update header p := fDestStream.Seek(0,soCurrent); with blob do begin dataFullSize := SizeIn; dataSize := p-fBlobDataHeaderPosition-BLOBDATA_HEADSIZE; assert(dataSize=SizeOut); dataCRC := CRC; // dataMethod: 0=stored 8=inflate >16: AlgoID=dataMethod shr 4 if FInitialized then dataMethod := 8 else dataMethod := fAlgorithmID shl 4; // stored + AlgoID end; fDestStream.Seek(fBlobDataHeaderPosition,soBeginning); fDestStream.WriteBuffer(blob,BLOBDATA_HEADSIZE); fDestStream.Seek(p,soBeginning); end; inherited; end; procedure TZipCompressor.Finish; begin if (self=nil) then exit; if assigned(fAlgorithm) then begin if assigned(fAlgorithmStream) then begin fAlgorithmStream.WriteBuffer(fBufferIn,fStrm.avail_in); // write pending data fStrm.total_in := fAlgorithm.Compress( // compress whole data at once fAlgorithmStream.Memory,fAlgorithmStream.Seek(0,soCurrent),@fCRC); end else if FStrm.avail_in>0 then inc(fStrm.total_in,fAlgorithm.Compress(@fBufferIn,FStrm.avail_in,@fCRC)); inc(fStrm.total_in,fAlgorithm.CompressFinish); // finish compression fStrm.total_out := fStrm.total_in; // .zip file compression mode = stored exit; end; if not FInitialized then exit; while FStrm.avail_in > 0 do begin // compress pending data if InFlateDeflate then raise TZipException.Create(SZlibInternalError); if FStrm.avail_out = 0 then FlushBufferOut; end; FStrm.next_in := nil; FStrm.avail_in := 0; while (Check(deflate(FStrm, Z_FINISH), [Z_OK, Z_STREAM_END]) <> Z_STREAM_END) and (FStrm.avail_out = 0) do FlushBufferOut; FlushBufferOut; end; function TZipCompressor.FlushBufferOut: integer; begin Result := 0; if not FInitialized then exit; if FStrm.avail_out < SizeOf(FBufferOut) then begin Result := SizeOf(FBufferOut) - FStrm.avail_out; FDestStream.WriteBuffer(FBufferOut, Result); FStrm.next_out := @FBufferOut; FStrm.avail_out := SizeOf(FBufferOut); end; end; function TZipCompressor.GetSizeIn: cardinal; begin result := FStrm.total_in; end; function TZipCompressor.GetSizeOut: cardinal; begin result := FStrm.total_out; end; function TZipCompressor.InFlateDeflate: boolean; begin Result := Check(deflate(FStrm, Z_NO_FLUSH), [Z_OK])<>Z_OK; end; function TZipCompressor.Read(var Buffer; Count: Longint): Longint; begin assert(false); result := 0; end; function TZipCompressor.Seek(Offset: Longint; Origin: Word): Longint; begin if not FInitialized then // CompressionLevel<0: direct copy to result := 0 else if (Offset = 0) and (Origin = soFromCurrent) then // for TStream.Position if assigned(fAlgorithmStream) then Result := fAlgorithmStream.Seek(0,soFromCurrent) else Result := FStrm.total_in else begin Result := 0; assert((Offset = 0) and (Origin = soFromBeginning) and (FStrm.total_in = 0)); end; end; function TZipCompressor.Write(const Buffer; Count: integer): integer; begin if self<>nil then begin result := Count; if FInitialized then begin if Count=0 then exit; fCRC := crc32(fCRC,@Buffer,Count); if cardinal(Count)+FStrm.avail_in>sizeof(fBufferIn)-1 then begin while FStrm.avail_in > 0 do begin if InflateDeflate then raise TZipException.Create(SZlibInternalError); if FStrm.avail_out = 0 then FlushBufferOut; end; FStrm.avail_in := 0; FStrm.next_in := @fBufferIn; end; if Count 0 do begin if InFlateDeflate then raise TZipException.Create(SZlibInternalError); if FStrm.avail_out = 0 then FlushBufferOut; end; FStrm.avail_in := 0; FStrm.next_in := @fBufferIn; end; end else begin // if not FIinitialized: CompressionLevel<0: direct copy to if Count=0 then exit; if Assigned(fAlgorithmStream) then begin // algo -> copy into fAlgorithmStream by fBufferIn[] chunks if cardinal(Count)+FStrm.avail_in>sizeof(fBufferIn)-1 then begin fAlgorithmStream.WriteBuffer(fBufferIn,fStrm.avail_in); // flush buffer FStrm.avail_in := 0; FStrm.next_in := @fBufferIn; end; if Count in fBufferIn[] move(Buffer,fBufferIn[FStrm.avail_in],Count); inc(FStrm.avail_in,Count); end else fAlgorithmStream.WriteBuffer(Buffer,Count); // big block -> direct store end else if Assigned(fAlgorithm) then begin // algo without fAlgorithmStream -> direct compress in fBufferIn[] chunks FStrm.next_out := @Buffer; repeat FStrm.avail_out := sizeof(fBufferIn)-FStrm.avail_in; if cardinal(Count)<=FStrm.avail_out then begin // count fit in fBufferIn[] move(FStrm.next_out^,fBufferIn[FStrm.avail_in],Count); // -> copy bytes inc(FStrm.avail_in,Count); break; end else begin // Count too big for fBufferIn[] -> compress chunk if FStrm.avail_in=0 then begin // direct compress from buffer inc(fStrm.total_in,fAlgorithm.Compress(FStrm.next_out,sizeof(fBufferIn),@fCRC)); inc(FStrm.next_out,sizeof(fBufferIn)); dec(Count,sizeof(fBufferIn)); end else begin // compress with data already in fBufferIn[] move(FStrm.next_out,fBufferIn[FStrm.avail_in],FStrm.avail_out); inc(fStrm.total_in,fAlgorithm.Compress(@fBufferIn,sizeof(fBufferIn),@fCRC)); FStrm.avail_in := 0; FStrm.next_in := @fBufferIn; inc(FStrm.next_out,FStrm.avail_out); dec(Count,FStrm.avail_out); end; end; until Count=0; end else begin // normal store -> direct copy to fDestStream inc(FStrm.total_in,Count); inc(FStrm.total_out,Count); fCRC := crc32(fCRC,@Buffer,Count); fDestStream.WriteBuffer(Buffer,Count); end; end; end else result := 0; // self=nil end; function TZipCompressor.WriteOnce(const Buffer; Count: Integer): Longint; // same as Write, but optimized for one call of Write() begin if Count=0 then result := Count else if Assigned(fAlgorithmStream) then begin // whole: avoid fAlgorithmStream use FreeAndNil(fAlgorithmStream); // very fast, since memory already allocated=0 result := fAlgorithm.Compress(@Buffer,Count,@fCRC)+fAlgorithm.CompressFinish; fStrm.total_in := result; end else result := Write(Buffer,Count); end; { TGzWriter } constructor TGzWriter.Create(const aFileName: TFileName); begin if FileExists(aFilename) then begin Create(TFileStream.Create(aFileName,fmOpenWrite)); outFile.Size := outFile.Position; end else Create(TFileStream.Create(aFileName,fmCreate)); outFileToBeFree := true; end; constructor TGzWriter.Create(const aDestStream: TStream); const gzheader : array [0..2] of cardinal = ($88B1F,0,0); begin outFile := aDestStream; outFile.WriteBuffer(gzHeader,10); inherited Create(outFile, 6); end; destructor TGzWriter.Destroy; begin Finish; outFile.WriteBuffer(CRC,4); outFile.WriteBuffer(FStrm.total_in,4); if outFileToBeFree then FreeAndNil(outFile); inherited; end; function GZRead(const aFileName: TFileName): RawByteString; overload; var Map: TMemoryMap; begin if not Map.Map(aFileName) then result := '' else try result := SynZipFiles.GzRead(pointer(Map.Buffer),Map.Size); finally Map.UnMap; end; end; function GZRead(gz: PAnsiChar; gzLen: integer): RawByteString; overload; var Len: integer; begin if PCardinal(gz)^<>$88B1F then SetString(result,gz,gzLen) else begin Len := pInteger(@gz[gzLen-4])^; assert(Len>=0); SetString(result,nil,Len); UnCompressMem(@gz[10],pointer(result),gzLen-18,Len); end; end; procedure GZRead(const aFileName: TFileName; aStream: TStream; StoreLen: boolean); overload; // just add an ungz file contents, storing len:Integer first if StoreLen=true var Map: TMemoryMap; Len: integer; begin if not Map.Map(aFileName) then begin if StoreLen then aStream.WriteBuffer(Map.Buffer,4); // no file -> store len=0 end else try if PCardinal(Map.Buffer)^<>$88B1F then begin if StoreLen then aStream.WriteBuffer(Map.Size,4); aStream.WriteBuffer(Map.Buffer^,Map.Size); // not a .gz -> store as is end else begin Len := pInteger(@Map.Buffer[Map.Size-4])^; // .gz -> uncompress assert(Len>=0); if StoreLen then aStream.WriteBuffer(Len,4); UnCompressStream(@Map.Buffer[10],Map.Size-18,aStream,nil); end; finally Map.UnMap; end; end; { TZipWriter } constructor TZipWriter.Create(AppendTo: TZipReader; ReCreate: boolean=false); procedure InitTmp; begin fDestFileName := AppendTo.fFileName; fFileName := ChangeFileExt(fDestFileName,'.tmp'); outFile := TFileStream.Create(fFileName,fmCreate); end; var i, firstDeleted: integer; posi: PtrUInt; begin fNow := {$ifdef MSWINDOWS}DateTimeToFileDate{$else}DateTimeToFileDateWindows{$endif}(Now); if AppendTo=nil then exit; SetLength(Entry,AppendTo.Count+32); if ReCreate then begin // force full file recreate from AppendTo data InitTmp; // don't call AppendTo.Map.UnMap since we will need to read the data! exit; // all the data will be copied from AppendTo.Map manually by caller end; firstDeleted := -1; for i := 0 to AppendTo.Count-1 do if AppendTo.Entry[i].Header.signature<>0 then begin // file not deleted Entry[Count] := AppendTo.Entry[i]; // -> add Entry[] inc(fCount); end else // file deleted if firstDeleted<0 then // -> update first deleted index firstDeleted := i; if (Count=0) then begin // nothing to read from old file -> just reopen AppendTo.Map.UnMap; fFileName := AppendTo.fFileName; DeleteFile(fFileName); // avoid win32 bug if filesize=0 outFile := TFileStream.Create(fFileName,fmCreate); end else if (Count=AppendTo.Count) or (firstDeleted=AppendTo.Count-1) then begin // no delete or only the last one: append to end of file fFileName := AppendTo.fFileName; if AppendTo.Map.Buffer=nil then // AppendTo file doesn't exists outFile := TFileStream.Create(fFileName,fmCreate) // new void file else begin // AppendTo file exists with AppendTo.Entry[Count-1] do posi := LocalDataPosition(AppendTo.Map.Buffer)+Header.fileInfo.zzipSize; AppendTo.Map.UnMap; // outFile seek to end of AppendTo file data outFile := TFileStream.Create(fFileName,fmOpenReadWrite); outFile.Position := posi; end; end else begin // some deleted: copy entries from mapped file to .tmp file InitTmp; fCount := 0; // recreate Entry[] in Add(AppendTo,i) below for i := 0 to AppendTo.Count-1 do if AppendTo.Entry[i].Header.signature<>0 then Add(AppendTo,i); // add not deleted entries AppendTo.Map.UnMap; // we won't use AppendTo any more end; end; constructor TZipWriter.Create(fromStream: TStream; const DestFileName: TFileName=''); // used to restore data uncompressed+bz-compressed with TZipReader.SaveToStream() var i, sign: integer; L, srcLen: cardinal; src: pointer; // temporary buffer for CompressMem aAlgo, wAlgo: integer; fromMemory: PAnsiChar; begin fNow := {$ifdef MSWINDOWS}DateTimeToFileDate{$else}DateTimeToFileDateWindows{$endif}(Now); // 1. read global params L := 0; fromStream.Read(L,1); if DestFileName<>'' then begin fFileName := DestFileName; fromStream.Seek(L,soCurrent); // ignore file name stored in fromStream end else begin SetLength(fFileName,L); fromStream.Read(fFileName[1],L); // fromStream -> dest file name end; fromStream.Read(fCount,4); // 2. read Entry[] SetLength(Entry,fCount); for i := 0 to Count-1 do with Entry[i] do begin Header.Init; // signature, madeBy, extFileAttr init fromStream.Read(Header.FileInfo,sizeof(Header.fileInfo)); SetLength(ZipName,Header.FileInfo.nameLen); fromStream.Read(ZipName[1],Header.FileInfo.nameLen); end; // 3. read and recompress all data outFile := TFileStream.Create(fFileName,fmCreate); if fromStream.InheritsFrom(TMemoryStream) then fromMemory := PAnsiChar(TMemoryStream(fromStream).Memory)+ fromStream.Seek(0,soCurrent) else fromMemory := nil; srcLen := 0; src := nil; for i := 0 to Count-1 do with Entry[i],Header.fileInfo do begin Header.localHeadOff := outFile.Position; // position can change, as we recompress sign := $04034b50; outFile.WriteBuffer(sign,4); // write .zip fileinfo signature aAlgo := AlgoID; if (aAlgo<>0) or (zzipMethod=8) then begin // reuse same compression/algo // special ZipCreate(), without AddEntry(): if aAlgo<>0 then begin // reuse same algo wAlgo := SynCompressionAlgos.WholeAlgoID(aAlgo); // whole algo is prefered here if wAlgo<>0 then begin aAlgo := wAlgo; Header.fileInfo.SetAlgoID(aAlgo); // update Header.fileInfo.flags end; end; Zip := TZipCompressor.Create(outFile, 6, aAlgo); outFile.WriteBuffer(neededVersion,sizeof(Header.fileInfo)); // save bulk fileInfo outFile.WriteBuffer(ZipName[1],nameLen); if fromMemory<>nil then begin Zip.WriteOnce(fromMemory[4],pInteger(fromMemory)^); // direct recompress using algo L := pInteger(fromMemory)^+4; inc(fromMemory,L); // jump uncompressed data fromStream.Seek(L,soCurrent); // synchronize fromStream position end else begin fromStream.Read(L,4); // SaveStream(..,withAlgoDataLen=true) if L>srcLen then begin if srcLen<>0 then Freemem(src); // Freemem+Getmem is better than Reallocmem (no move) srcLen := succ(L shr 12) shl 12; // 4KB size boundary Getmem(src,srcLen); end; fromStream.Read(src^,L); // read uncompressed data Zip.WriteOnce(src^,L); // recompress using algo end; ZipClose(i); // fileInfo update+Zip.Free; aIndex>=0 -> no inc(fCount) end else begin assert(zzipMethod=0); zzipSize := zfullSize; outFile.WriteBuffer(neededVersion,sizeof(Header.fileInfo)); // save new fileInfo outFile.WriteBuffer(ZipName[1],nameLen); outFile.CopyFrom(fromStream,zzipSize); end; end; end; constructor TZipWriter.Create(const aFileName: TFileName); begin fNow := {$ifdef MSWINDOWS}DateTimeToFileDate{$else}DateTimeToFileDateWindows{$endif}(Now); SetLength(Entry,100); fFileName := aFileName; outFile := TFileStream.Create(fFileName,fmCreate); end; destructor TZipWriter.Destroy; var i: integer; lhr: TLastHeader; begin if not Assigned(outFile) then begin // an error occured during outfile creation inherited; // -> just free memory and leave exit; end; // 1. prepare last header with lhr do begin signature := $06054b50; thisDisk := 0; headerDisk := 0; thisFiles := Count; totalFiles := Count; headerSize := 0; headerOffset := outFile.seek(0,soCurrent); // position of file entries commentLen := 0; end; // 2. write file entries from Entry[] for i := 0 to Count-1 do with Entry[i] do begin inc(lhr.headerSize, sizeOf(TFileHeader)+length(ZipName)); outFile.WriteBuffer(Header,sizeof(TFileHeader)); outFile.WriteBuffer(ZipName[1],length(ZipName)); end; // 3. write last header outFile.WriteBuffer(lhr,sizeof(lhr)); // 4. truncate and close file {$ifdef KYLIX3} ftruncate(outFile.Handle, outFile.seek(0,soFromCurrent)); {$else} SetEndOfFile(outFile.Handle); {$endif} if forceFileAge<>0 then FileSetDate(outFile.Handle,forceFileAge); outFile.Free; // 5. if we worked on a .tmp file (recreated from a TZipReader) -> make it new if fDestFileName<>'' then begin if not DeleteFile(fDestFileName) then begin SleepHiRes(100); if not DeleteFile(fDestFileName) then assert(false); end; RenameFile(fFileName,fDestFileName); // '.tmp' -> '.bjt' ou '.zip' end; // 6. free memory: Finalize(Entry) inherited; end; procedure TZipWriter.AddFile(const aFileName: TFileName; const aZipName: RawUTF8; CompressionLevel: integer; Algorithm: integer=0); // direct compress or store of a file content, using memory mapped file var Map: TMemoryMap; begin if not Map.Map(aFileName) then exit; if Map.Size<64 then CompressionLevel := -1; // store if too small ZipCreate(aZipName,CompressionLevel,0,Algorithm); // initialize Zip object Entry[Count].Header.fileInfo.zlastMod := {$ifdef MSWINDOWS}FileGetDate(Map.FileHandle){$else} DateTimeToFileDateWindows(FileDateToDateTime(FileGetDate(Map.FileHandle))){$endif}; Zip.WriteOnce(Map.Buffer^,Map.Size); Map.UnMap; ZipClose; end; procedure TZipWriter.Add(const aZipName: RawUTF8; data: PAnsiChar; dataSize: cardinal; CompressionLevel: integer; dataCRC: pCardinal=nil; FileAge: integer = 0; Algorithm: integer=0); begin if (self<>nil) and (aZipName<>'') then if (CompressionLevel<0) and (Algorithm=0) then with AddEntry(aZipName,FileAge)^.Header,FileInfo do begin zzipSize := dataSize; zfullSize := dataSize; if dataCRC<>nil then zcrc32 := dataCRC^ else zcrc32 := crc32(0,data,dataSize); outFile.WriteBuffer(fileInfo,sizeof(fileInfo)); outFile.WriteBuffer(aZipName[1],length(aZipName)); outFile.WriteBuffer(data^,dataSize); inc(fCount); end else begin ZipCreate(aZipName,CompressionLevel,FileAge,Algorithm); Zip.WriteOnce(data^,dataSize); ZipClose; // fileInfo update+save, Zip.Free, inc(Count) end; end; procedure TZipWriter.Add(const aZipName: RawUTF8; p: PBlobData); begin with AddEntry(aZipName)^.Header,FileInfo do begin if p^.AlgoID=0 then begin zzipMethod := p^.dataMethod; zzipSize := p^.dataSize; zfullSize := p^.dataFullSize; zcrc32 := p^.dataCRC; end else if Assigned(SynCompressionAlgos.Algo(p^.AlgoID)) then SetAlgoID(p^.AlgoID) else raise TZipException.CreateFmt(sZipAlgoIDNUnknownN,[p^.AlgoID,aZipName]); outFile.WriteBuffer(fileInfo,sizeof(fileInfo)); outFile.WriteBuffer(aZipName[1],length(aZipName)); outFile.WriteBuffer(p^.databuf,p^.dataSize); inc(fCount); end; end; procedure TZipWriter.Add(aReader: TZipReader; aReaderIndex: integer); // this copy directly from a TZipReader var sign: integer; E: PZipEntry; begin if (aReader=nil) or (aReaderIndex<0) or (aReaderIndex>=aReader.Count) then exit; if Count=length(Entry) then SetLength(Entry,Count+100); with Entry[Count] do begin E := @aReader.Entry[aReaderIndex]; ZipName := E^.ZipName; // may be different after delete or new Header := E^.Header; // direct whole header copy with Header do begin // update Entry[Count]: fileInfo.nameLen := length(ZipName); // recalc length, as may be updated localHeadOff := outFile.Position; // only changed data = position in file sign := $04034b50; outFile.Write(sign,4); outFile.WriteBuffer(fileInfo,sizeof(fileInfo)); // save new fileInfo outFile.WriteBuffer(ZipName[1],fileInfo.nameLen); // append file name outFile.WriteBuffer(E^.LocalHeader(aReader.Map.Buffer).LocalData^,fileInfo.zzipSize); // data copy end; end; inc(fCount); end; procedure TZipWriter.ZipClose(aIndex: integer=-1); // compression finish, fileInfo update+save, Zip.Free // after ZipCreate: aIndex=-1 -> update Entry[Count] + inc(Count) // in TZipWriter.Create(fromStream..): aIndex>=0 -> update Entry[aIndex] only var p: cardinal; i: integer; begin if Zip=nil then exit; Zip.Finish; if aIndex<0 then i := Count else i := aIndex; with Entry[i] do begin Header.fileInfo.zcrc32 := Zip.CRC; Header.fileInfo.zfullSize := Zip.SizeIn; // if algo -> SizeIn=SizeOut if Zip.FInitialized then Header.fileInfo.zzipSize := Zip.SizeOut else Header.fileInfo.zzipSize := Header.fileInfo.zfullSize; p := outFile.Seek(0,soCurrent); outFile.Seek(Header.localHeadOff+sizeof(dword),soBeginning); outFile.WriteBuffer(Header.fileInfo,sizeof(Header.fileInfo)); // save updated fileInfo outFile.Seek(p,soBeginning); end; FreeAndNil(Zip); if aIndex<0 then inc(fCount); end; procedure TZipWriter.ZipCreate(const aZipName: RawUTF8; CompressionLevel: integer; FileAge: integer = 0; Algorithm: integer=0); begin assert(Zip=nil); with AddEntry(aZipName,FileAge)^.Header do begin if Algorithm>0 then fileInfo.SetAlgoID(Algorithm) else if CompressionLevel>=0 then fileInfo.zzipMethod := 8; Zip := TZipCompressor.Create(outFile, CompressionLevel,Algorithm); outFile.WriteBuffer(fileInfo,sizeof(fileInfo)); // save bulk fileInfo outFile.WriteBuffer(aZipName[1],length(aZipName)); // now the caller will use Zip.Write to compress data into outFile // and will end compression with ZipClose end; end; function TZipWriter.AddEntry(const aZipName: RawUTF8; FileAge: integer = 0): PZipEntry; var sign: integer; tmp: WinAnsiString; begin if Count=length(Entry) then SetLength(Entry,Count+100); result := @Entry[Count]; with result^ do begin Header.Init; // signature, madeBy, extFileAttr, fileInfo.neededVersion init {$ifdef MSWINDOWS} if IsWinAnsiU(pointer(aZipName)) then begin // Win-Ansi code page -> encode as DOS/OEM charset (old format) tmp := Utf8ToWinAnsi(aZipName); SetLength(ZipName,length(tmp)); CharToOemBuffA(pointer(tmp),pointer(ZipName),length(tmp)); end else {$endif} // Linux will use only UTF-8 encoding begin ZipName := aZipName; Header.fileInfo.SetUTF8FileName; // mark file name is UTF-8 encoded end; with Header do begin localHeadOff := outFile.Position; with fileInfo do begin if FileAge=0 then zlastMod := fNow else zlastMod := FileAge; nameLen := length(ZipName); end; end; end; sign := $04034b50; outFile.WriteBuffer(sign,sizeof(dword)); end; function TZipWriter.LastCRC32Added: cardinal; begin if Count>0 then result := Entry[Count-1].Header.fileInfo.zcrc32 else result := 0; end; { TZipValues } procedure TZipValues.BeginWriter; var i: integer; begin if Writer<>nil then exit; Modified := true; Count := 0; for i := 0 to Reader.Count-1 do // update Values[] with MarkDeleted if Reader.Entry[i].Header.signature<>0 then begin CopyValue(i,Count); inc(Count); end; inherited BeginWriter; // Writer := TZipWriter.Create(Reader) = calc MarkDeleted assert(Writer.Count=Count); end; constructor TZipValues.Create(const aFileName: TFileName); var n: integer; begin inherited Create(aFileName); // Reader := TZipReader.Create n := Reader.Count-1; // '-index-' must be last Entry[n] -> otherwise gap in Values[] if n=0 then Reader.Clear; // must contains at least: Values[0] + '-index-' if n<1 then exit; with Reader.Entry[n] do // read Values[] from last Entry[]: if (ZipName='-index-') and (Header.fileInfo.zzipMethod=0) then begin Count := n; LoadValues(LocalHeader(Reader.Map.Buffer).LocalData); Reader.DeleteLastEntry; // ignore '-index-' from now end else begin Count := 0; Assert(false,'wrong file format for '+FileName); end; end; destructor TZipValues.Destroy; begin if Modified and (Count>0) then begin BeginWriter; // will truncate to the last block with Writer do begin ZipCreate('-index-',-1); SaveValues(Zip); ZipClose; end; end; inherited; end; function TZipValues.GetValue(aReaderIndex: integer; aStream: TStream): PAnsiChar; begin if Writer<>nil then result := nil else result := Reader.GetData(aReaderIndex,aStream); end; function TZipValues.MarkDeleted(aReaderIndex: integer): boolean; begin if aReaderIndex<0 then begin result := false; exit; end; Modified := true; result := inherited MarkDeleted(aReaderIndex); end; { TBlobData } procedure CompressAsBlobData(const data; size: integer; aStream: TStream; CompressionLevel: integer=6; Algorithm: integer=0); // create a TBlobData in aStream (encryption algo: 6=AES 7=AES+Zip 8=AES+SynLz) begin with TZipCompressor.CreateAsBlobData(aStream,CompressionLevel,Algorithm) do try Write(data,size); Finish; finally Free; end; end; { use algo 6=AES 7=AES+Zip-chunked 8=AES+SynLz-chunked function CompressAsBlobData(data: PAnsiChar; size: integer; AESKey: pointer=nil; AESKeySize: integer=0): string; // optional AES-encrypt AFTER compression -> 0% ZIP compatible but security safe begin SetLength(result,sizeof(TBlobData)+(size*11)div 10+12); with PBlobData(pointer(result))^ do begin dataFullSize := size; dataCRC := crc32(0,data,size); dataSize := CompressMem(data,@dataBuf,size,length(result)-sizeof(TBlobData)); if dataSize>=dataFullSize then begin // compress only if efficient dataMethod := 0; // store dataSize := dataFullSize; if (AESKey<>nil) and (AESKeySize>0) then AES(AESKey^,AESKeySize,data,@databuf,dataSize,true) else move(data^,databuf,dataSize); end else begin dataMethod := 8; if (AESKey<>nil) and (AESKeySize>0) then AES(AESKey^,AESKeySize,@databuf,@databuf,dataSize,true); end; SetLength(result,dataSize+BLOBDATA_HEADSIZE); end; end;} function TBlobData.AlgoCreate(data: pointer): TSynCompressionAlgo; // test if algo is registered, perform crc32 check and create one instance var Algo: TSynCompressionAlgoClass; begin if DataMethod<15 then result := nil else begin Algo := SynCompressionAlgos.Algo(AlgoID); // registered? if not Assigned(Algo) then // error: unregistered algo raise TZipException.CreateFmt(sZipAlgoIDNUnknownN,[ AlgoID,'TBlobData']); if crc32(0,data,dataFullSize)<>dataCRC then // always check integrity raise TZipException.CreateFmt(sZipCrcErrorNN,[IntToStr(AlgoID),'TBlobData']); result := Algo.Create; // create algo instance end; end; function TBlobData.AlgoID: cardinal; begin // 0=stored 8=inflate >=16: AlgoID=dataMethod shr 4 result := (dataMethod shr 4) and 15; end; function TBlobData.Expand: RawByteString; begin case DataMethod of // 0=stored 8=inflate >16: AlgoID=dataMethod shr 4 16..31: with AlgoCreate(@dataBuf) do // crc32+algo object create try SetString(result,nil,UnCompressedLength(@dataBuf,dataFullSize)); UnCompress(@dataBuf,dataFullSize,pointer(result)); finally Free; end; 8: begin SetString(result,nil,dataFullSize); if (UnCompressMem( @dataBuf,pointer(result),dataSize,dataFullSize)<>integer(dataFullSize)) or (crc32(0,pointer(result),dataFullSize)<>dataCRC) then begin assert(false); result := ''; end; end; 0: if dataSize=0 then result := '' else SetString(result,PAnsiChar(@dataBuf),dataSize); else begin assert(false); // impossible dataMethod -> probably bad PBlobData result := ''; end; end; end; function TBlobData.ExpandBuf(out destSize: cardinal): pointer; // uncompress and alloc memory if necessary (i.e. DataMethod<>0); // no direct AES since may be mapped and DataMethod=0 begin case DataMethod of // 0=stored 8=inflate >16: AlgoID=dataMethod shr 4 16..31: with AlgoCreate(@dataBuf) do // crc32+algo object create try destsize := UnCompressedLength(@dataBuf,dataFullSize); Getmem(result,destSize); UnCompress(@dataBuf,dataFullSize,result); finally Free; end; 0: result := @dataBuf; 8: begin GetMem(result,dataFullSize); if (UnCompressMem(@dataBuf,result,dataSize,dataFullSize)<>integer(dataFullSize)) or (crc32(0,result,dataFullSize)<>dataCRC) then begin Freemem(result); assert(false); result := nil; end; end; else begin assert(false); // impossible dataMethod -> probably bad PBlobData result := nil; end; end; end; procedure TBlobData.ExpandStream(Stream: TStream); var tmp: RawByteString; begin case DataMethod of 16..31: begin tmp := Expand; Stream.WriteBuffer(pointer(tmp)^,length(tmp)); end; 0: Stream.WriteBuffer(dataBuf,dataFullsize); 8: UnCompressStream(@dataBuf,dataSize,Stream,nil); else assert(false); end; end; function TBlobData.Next: PAnsiChar; {$ifdef PUREPASCAL} begin result := PAnsiChar(@databuf)+dataSize; end; {$else} {$ifdef FPC} nostackframe; assembler; {$endif} asm lea ecx,[eax+TBlobData.databuf] mov eax,[eax].TBlobData.datasize add eax,ecx end; {$endif} procedure TBlobData.SetFrom(const FileInfo: TFileInfo); begin dataSize := FileInfo.zzipsize; dataFullSize := FileInfo.zfullSize; dataCRC := FileInfo.zcrc32; // dataMethod: 0=stored 8=inflate >16: AlgoID=dataMethod shr 4 if FileInfo.AlgoID<>0 then dataMethod := FileInfo.AlgoID shl 4 else // stored + AlgoID dataMethod := FileInfo.zzipMethod; end; { TZipEntry } function TZipEntry.AlgoCreate(data: pointer; const FileName: TFileName): TSynCompressionAlgo; // test if algo is registered, perform crc32 check and create one instance var Algo: TSynCompressionAlgoClass; begin if Header.fileInfo.AlgoID=0 then result := nil else begin Algo := SynCompressionAlgos.Algo(Header.fileInfo.AlgoID); // registered? if not Assigned(Algo) then // error: unregistered algo raise TZipException.CreateFmt(sZipAlgoIDNUnknownN,[ Header.fileInfo.AlgoID,ZipName]); if crc32(0,data,Header.fileInfo.zfullSize)<> Header.fileInfo.zcrc32 then // always check integrity raise TZipException.CreateFmt(sZipCrcErrorNN,[ZipName,FileName]); result := Algo.Create; // create algo instance end; end; function TZipEntry.SameAs(const aEntry: TZipEntry): boolean; begin result := (ZipName=aEntry.ZipName) and Header.fileInfo.SameAs(@aEntry.Header.fileInfo); end; function TZipEntry.LocalHeader(ZipStart: PAnsiChar): PLocalFileHeader; begin result := @ZipStart[Header.localHeadOff]; end; function TZipEntry.LocalDataPosition(ZipStart: PAnsiChar): PtrUInt; begin with LocalHeader(ZipStart)^ do result := PtrUInt(LocalData)-PtrUInt(ZipStart); end; { TSynCompressionAlgos } function TSynCompressionAlgos.Algo(aID: integer): TSynCompressionAlgoClass; var i: integer; begin for i := 0 to length(Values)-1 do with Values[i] do if ID=aID then begin result := func; exit; end; result := nil; end; procedure TSynCompressionAlgos.AlgoRegister(aAlgo: TSynCompressionAlgoClass; aID, aWholeID: integer); var L: integer; begin if not Assigned(aAlgo) then exit; aID := aID and 15; if (aID=0) or Assigned(Algo(aID)) then exit; L := length(Values); SetLength(Values,L+1); with Values[L] do begin ID := aID; WholeID := aWholeID; func := aAlgo; end; end; function TSynCompressionAlgos.WholeAlgoID(aID: integer): integer; var i: integer; begin for i := 0 to length(Values)-1 do with Values[i] do if ID=aID then begin result := WholeID; exit; end; result := 0; end; { TSynCompressionAlgo } function TSynCompressionAlgo.CompressFinish: cardinal; begin result := 0; // default implementation: just do nothing end; procedure TSynCompressionAlgo.CompressInit(OutStream: TStream); begin fDestStream := OutStream; end; { TSynCompressionAlgoBuf } function TSynCompressionAlgoBuf.Compress(InP: pointer; InLen: cardinal; CRC: PCardinal): cardinal; begin if InLen=0 then begin result := 0; exit; end; assert(InLen<=65536); // // fCompressBuf[] size if fixed result := AlgoCompress(InP,InLen,fCompressBuf+3); // compress InP[InLen] if result+128>InLen then begin // compression was not effective -> store pWord(fCompressBuf)^ := 0; // fCompressBuf[0..2] = 0 = no compression pInteger(@fCompressBuf[2])^ := (InLen-1)shl 8; // [3..4]=uncompressed len-1 move(InP^,fCompressBuf[5],InLen); // store after uncompressed len result := InLen+5; end else begin // compression was effective -> store compressed chunk len pWord(fCompressBuf)^ := result; // fCompressBuf[0..2] = chunk len fCompressBuf[2] := AnsiChar(result shr 16); inc(result,3); end; fDestStream.WriteBuffer(fCompressBuf^,result); if CRC<>nil then CRC^ := crc32(CRC^,fCompressBuf,result); end; procedure TSynCompressionAlgoBuf.CompressInit(OutStream: TStream); begin inherited; // fDestSteram := OutStream // size = worse case with 64KB chunk Getmem(fCompressBuf,AlgoCompressLength(65536)); // = 73744 for SynLZ, e.g. end; destructor TSynCompressionAlgoBuf.Destroy; begin Freemem(fCompressBuf); inherited; end; function TSynCompressionAlgoBuf.UnCompress(InP: pointer; InLen: cardinal; OutP: pointer): cardinal; var sP,sEnd, dP: PAnsiChar; L: integer; begin sP := InP; sEnd := sP+InLen; dP := OutP; while sP uncompress InP[InLen] into PAnsiChar(OutStream) L := PInteger(sP)^ and $ffffff; if L=0 then begin // no compression inc(sP,3); L := pWord(sP)^+1; inc(sP,2); move(sP^,dP^,L); inc(sP,L); inc(dp,L); end else begin // SynLZ compression inc(dP,AlgoUnCompress(sP+3,L,dP)); inc(sP,L+3); end; end; result := dp-PAnsiChar(OutP); end; function TSynCompressionAlgoBuf.UnCompressedLength(InP: pointer; InLen: cardinal): cardinal; var sP,sEnd: PAnsiChar; L: integer; begin sP := InP; sEnd := sP+InLen; result := 0; while sPInLen then begin // compression not effective tmp[0] := #0; // mark stored move(InP^,tmp[1],InLen); result := InLen; end else // compression was effective tmp[0] := #1; // mark compressed inc(result); fDestStream.WriteBuffer(tmp^,result); if CRC<>nil then CRC^ := crc32(CRC^,tmp,result); finally freemem(tmp); end; end; function TSynCompressionAlgoWhole.UnCompress(InP: pointer; InLen: cardinal; OutP: pointer): cardinal; var tmp: PAnsiChar absolute InP; begin case tmp[0] of #0: begin result := InLen-1; move(tmp[1],OutP^,result); end; #1: result := AlgoUnCompress(tmp+1,InLen-1,OutP); else result := 0; end; end; function TSynCompressionAlgoWhole.UnCompressedLength(InP: pointer; InLen: cardinal): cardinal; var tmp: PAnsiChar absolute InP; begin case tmp[0] of #0: result := InLen-1; #1: result := AlgoUnCompressLength(tmp+1,InLen-1); else result := 0; end; end; end.