近日看到XE2上的Zip单元用起很方便,但我目前一直都有在用XE,所以就将它移植到XE上。
unit ZipFile;
interface
uses
SysUtils,
IOUtils,
Generics.Collections,
Classes;
type
/// <summary> Zip Compression Method Enumeration </summary>
TZipCompression = (
zcStored = 0,
zcShrunk,
zcReduce1,
zcReduce2,
zcReduce3,
zcReduce4,
zcImplode,
zcTokenize,
zcDeflate,
zcDeflate64,
zcPKImplode,
{11 RESERVED}
zcBZIP2 = 12,
{13 RESERVED}
zcLZMA = 14,
{15-17 RESERVED}
zcTERSE = 18,
zcLZ77,
zcWavePack = 97,
zcPPMdI1
);
/// <summary> Converts ZIP compression method value to string </summary>
function TZipCompressionToString(Compression: TZipCompression): string;
const
SIGNATURE_ZIPENDOFHEADER: UInt32 = $06054B50;
SIGNATURE_CENTRALHEADER: UInt32 = $02014B50;
SIGNATURE_LOCALHEADER: UInt32 = $04034B50;
LOCALHEADERSIZE = 26;
CENTRALHEADERSIZE = 42;
type
/// <summary> Final block written to zip file</summary>
TZipEndOfCentralHeader = packed record
DiskNumber: UInt16;
CentralDirStartDisk: UInt16;
NumEntriesThisDisk: UInt16;
CentralDirEntries: UInt16;
CentralDirSize: UInt32;
CentralDirOffset: UInt32;
CommentLength: UInt16;
{Comment: RawByteString}
end;
/// <summary> TZipHeader contains information about a file in a zip archive.
/// </summary>
/// <remarks>
/// <para>
/// This record is overloaded for use in reading/writing ZIP
/// [Local file header] and the Central Directory's [file header].
/// </para>
/// <para> See PKZIP Application Note section V. General Format of a .ZIP file
/// sub section J. Explanation of fields for more detailed description
// of each field's usage.
/// </para>
/// </remarks>
TZipHeader = packed record
MadeByVersion: UInt16; // Start of Central Header
RequiredVersion: UInt16; // Start of Local Header
Flag: UInt16;
CompressionMethod: UInt16;
ModifiedDateTime: UInt32;
CRC32: UInt32;
CompressedSize: UInt32;
UncompressedSize: UInt32;
FileNameLength: UInt16;
ExtraFieldLength: UInt16; // End of Local Header
FileCommentLength: UInt16;
DiskNumberStart: UInt16;
InternalAttributes: UInt16;
ExternalAttributes: UInt32;
LocalHeaderOffset: UInt32; // End of Central Header
FileName: RawByteString;
ExtraField: TBytes;
FileComment: RawByteString;
end;
PZipHeader = ^TZipHeader;
/// <summary> Exception type for all Zip errors. </summary>
EZipException = class( Exception );
TZipMode = (zmClosed, zmRead, zmReadWrite, zmWrite);
TZipFile = class;
/// <summary> Function to Create a Compression/Decompression stream </summary>
/// <remarks>
/// Call <c>RegisterCompressionHandler</c> to register a compression type that
/// can Compress/Decompress a stream. The output stream reads/write from/to InStream.
/// </remarks>
TStreamConstructor = reference to function(InStream: TStream; const ZipFile: TZipFile; const Item: TZipHeader): TStream;
/// <summary> Class for creating and reading .ZIP files.
/// </summary>
TZipFile = class
private type
TCompressionDict = TDictionary< TZipCompression , TPair<TStreamConstructor, TStreamConstructor > >;
private class var
FCompressionHandler: TCompressionDict;
private
FMode: TZipMode;
FStream: TStream;
FFileStream: TFileStream;
FStartFileData: Int64;
FEndFileData: Int64;
FFiles: TList<TZipHeader>;
FComment: String;
FUTF8Support: Boolean;
function GetFileComment(Index: Integer): string;
function GetFileCount: Integer;
function GetFileInfo(Index: Integer): TZipHeader;
function GetFileInfos: TArray<TZipHeader>;
function GetFileName(Index: Integer): string;
function GetFileNames: TArray<string>;
procedure ReadCentralHeader;
procedure SetFileComment(Index: Integer; Value: string);
procedure SetUTF8Support(const Value: Boolean);
function LocateEndOfCentralHeader(var Header: TZipEndOfCentralHeader): Boolean;
public
class constructor Create;
class destructor Destroy;
/// <remarks>
/// Call <c>RegisterCompressionHandler</c> to register a compression type that
/// can Compress/Decompress a stream. The output stream reads/write from/to InStream.
/// </remarks>
class procedure RegisterCompressionHandler(Compression: TZipCompression;
CompressStream, DecompressStream: TStreamConstructor);
/// <param name="ZipFileName">Path to Zip File</param>
/// <returns>Is the .ZIP file valid</returns>
class function IsValid(ZipFileName: string): Boolean; static;
/// <summary> Extract a ZipFile</summary>
/// <param name="ZipFileName">File name of the ZIP file</param>
/// <param name="Path">Path to extract to disk</param>
class procedure ExtractZipFile(ZipFileName: string; Path: string); static;
/// <summary> Zip the contents of a directory </summary>
/// <param name="ZipFileName">File name of the ZIP file</param>
/// <param name="Path">Path of directory to zip</param>
/// <param name="Compression">Compression mode.</param>
class procedure ZipDirectoryContents(ZipFileName: string; Path: string;
Compression: TZipCompression = zcDeflate); static;
/// <summary> Create a TZipFile</summary>
constructor Create;
/// <remarks> Destroy will close an open zipfile before disposing of it</remarks>
destructor Destroy; override;
/// <summary> Opens a ZIP file for reading or writing.</summary>
/// <param name="ZipFileName">Path to ZipFile</param>
/// <param name="OpenMode"> File Mode to open file.
/// <c>zmWrite</c> Creates a new ZIP file for writing.
/// <c>zmReadWrite</c> Opens the file for reading and allows adding
/// additional new files.
/// <c>zmRead</c> Opens the file for reading.
///</param>
procedure Open(ZipFileName: string; OpenMode: TZipMode); overload;
procedure Open(ZipFileStream: TStream; OpenMode: TZipMode); overload;
/// <remarks>
/// Closing is required to write the ZipFile's
/// Central Directory to disk. Closing a file that is open for writing
/// writes additonal metadata that is required for reading the file.
/// </remarks>
procedure Close;
/// <summary> Extract a single file </summary>
/// <remarks>
/// <c>FileName</c> specifies a file in the ZIP file. All slashes
/// in ZIP file names should be '/'.
/// The overload that takes an Integer may be useful when a ZIP file
/// has duplicate filenames.
/// </remarks>
/// <param name="FileName">File name in the archive</param>
/// <param name="Path">Path to extract to disk</param>
/// <param name="CreateSubdirs">The output should create sub directories specified in the ZIP file</param>
procedure Extract(FileName: string; Path: string = ''; CreateSubdirs: Boolean=True); overload;
procedure Extract(Index: Integer; Path: string = ''; CreateSubdirs: Boolean=True); overload;
/// <summary> Extract All files </summary>
/// <param name="Path">Path to extract to.</param>
procedure ExtractAll(Path: string = '');
/// <summary> Read a file from arcive to an array of Bytes </summary>
/// <remarks>
/// The overload that takes an Integer may be useful when a ZIP file
/// has duplicate filenames.
/// </remarks>
/// <param name="FileName">ZIP file FileName</param>
/// <param name="Bytes">Output bytes</param>
procedure Read(FileName: string; out Bytes: TBytes); overload;
procedure Read(Index: Integer; out Bytes: TBytes); overload;
/// <summary> Get a stream to read a file from disk </summary>
/// <remarks>
/// The Stream returned by this function is a decomression stream
/// wrapper around the interal Stream reading the zip file. You must
/// Free this stream before using other TZipFile methods that change the
/// contents of the ZipFile, such as Read or Add.
/// The overload that takes an Integer may be useful when a ZIP file
/// has duplicate filenames.
/// </remarks>
/// <param name="FileName">ZIP file FileName</param>
/// <param name="Stream">Output Stream</param>
/// <param name="LocalHeader">Local File header</param>
procedure Read(FileName: string; out Stream: TStream; out LocalHeader: TZipHeader); overload;
procedure Read(Index: Integer; out Stream: TStream; out LocalHeader: TZipHeader); overload;
/// <summary> Add a file to the ZIP file </summary>
/// <param name="FileName">FileName to be added</param>
/// <param name="ArchiveFileName">Path + Name of file in the arcive.
/// If Ommitted, <C>ExtractFileName(FileName)</C> will be used.</param>
/// <param name="Compression">Compression mode.</param>
procedure Add(FileName: string; ArchiveFileName: string = '';
Compression: TZipCompression = zcDeflate); overload;
/// <summary> Add a memory file to the ZIP file </summary>
/// <param name="Data">Bytes to be added</param>
/// <param name="ArchiveFileName">Path + Name of file in the arcive.</param>
/// <param name="Compression">Compression mode.</param>
procedure Add(Data: TBytes; ArchiveFileName: string;
Compression: TZipCompression = zcDeflate); overload;
/// <summary> Add a memory file to the ZIP file </summary>
/// <param name="Data">Stream of file to be added</param>
/// <param name="ArchiveFileName">Path + Name of file in the arcive.</param>
/// <param name="Compression">Compression mode.</param>
procedure Add(Data: TStream; ArchiveFileName: string;
Compression: TZipCompression = zcDeflate); overload;
/// <summary> Add a memory file to the ZIP file. Allows programmer to specify
/// the Local and Central Header data for more flexibility on what gets written.
/// Minimal vailidation is done on the Header parameters; speficying bad options
/// could result in a corrupted zip file. </summary>
/// <param name="Data">Stream of file to be added</param>
/// <param name="LocalHeader">The local header data</param>
/// <param name="CentralHeader">A Pointer to an optional central header. If no
/// central Header is provided, the Local Header information is used. </param>
procedure Add(Data: TStream; LocalHeader: TZipHeader;
CentralHeader: PZipHeader = nil); overload;
/// <summary> Translate from FileName to index in ZIP Central Header
/// </summary>
/// <remarks>
/// A ZIP file may have dupicate entries with the same name. This
/// function will return the index of the first.
/// </remarks>
/// <param name="FileName">Path + Name of file in the arcive.</param>
/// <returns>The index of the file in the archive, or -1 on failure.
/// </returns>
function IndexOf(FileName: string): Integer;
/// <returns> The mode the TZipFile is opened to</returns>
property Mode: TZipMode read FMode;
/// <returns>Total files in ZIP File</returns>
property FileCount: Integer read GetFileCount;
/// <returns>An array of FileNames in the ZIP file</returns>
property FileNames: TArray<string> read GetFileNames;
/// <returns>An array of the TZipHeader of the files in the ZIP file</returns>
property FileInfos: TArray<TZipHeader> read GetFileInfos;
/// <returns>FileName of a File in the ZipFile</returns>
property FileName[Index: Integer]: string read GetFileName;
/// <returns>TZipHeader of a File in the ZipFile</returns>
property FileInfo[Index: Integer]: TZipHeader read GetFileInfo;
/// <remarks>
/// File Comments can be changed for files opened in write mode at any point.
/// The comment is written when the Central Directory is written to disk.
/// Comments can be a maximum of 65535 bytes long. If a longer comment is supplied,
/// It is truncated before writing to the ZIP File.
/// </remarks>
property FileComment[Index: Integer]: string read GetFileComment write SetFileComment;
/// <remarks>
/// Comments can be a maximum of 65535 bytes long. If a longer comment is supplied,
/// It is truncated before writing to the ZIP File.
/// </remarks>
property Comment: string read FComment write FComment;
property UTF8Support: Boolean read FUTF8Support write SetUTF8Support default True;
end;
resourcestring
{ Zip Strings}
SZipErrorRead = 'Error reading zip file';
SZipErrorWrite = 'Error writing zip file';
SZipInvalidLocalHeader = 'Invalid Zip Local Header signature';
SZipInvalidCentralHeader = 'Invalid Zip Central Header signature';
SZipNotSupported = 'Support for compression method not registered: %s';
SZipNotOpen = 'File must be open';
SZipNoWrite = 'File must be open for writing';
SZipNoRead = 'File must be open for reading';
SZipNotEmpty = 'Zip file must be empty';
sArgumentInvalid = 'Invalid argument';
SReadError = 'Stream read error';
SFileNotFound = 'The specified file was not found';
implementation
uses ZLib,CRC;
type
TOem437String = type AnsiString(437);
procedure VerifyRead(Stream: TStream; var Buffer; Count: Integer);
begin
if Stream.Read(Buffer, Count) <> Count then
raise EZipException.CreateRes(@SZipErrorRead);// at ReturnAddress;
end;
procedure VerifyWrite(Stream: TStream; var Buffer; Count: Integer);
begin
if Stream.Write(Buffer, Count) <> Count then
raise EZipException.CreateRes(@SZipErrorWrite);// at ReturnAddress;
end;
type
/// <summary> Helper class for reading a segment of another stream.</summary>
TStoredStream = class( TStream )
private
FStream: TStream;
FPos: Int64;
protected
function GetSize: Int64; override;
public
constructor Create( Stream: TStream );
function Read(var Buffer; Count: Longint): Longint; override;
function Write(const Buffer; Count: Longint): Longint; override;
function Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; override;
end;
{ TStoredStream }
constructor TStoredStream.Create(Stream: TStream);
begin
FStream := Stream;
FPos := FStream.Position;
end;
function TStoredStream.GetSize: Int64;
begin
Result := FStream.Size;
end;
function TStoredStream.Read(var Buffer; Count: Integer): Longint;
begin
Result := FStream.Read(Buffer, Count);
end;
function TStoredStream.Seek(const Offset: Int64; Origin: TSeekOrigin): Int64;
begin
Result := FStream.Seek(Offset, Origin)
end;
function TStoredStream.Write(const Buffer; Count: Integer): Longint;
begin
Result := FStream.Write(Buffer, Count);
end;
function TZipCompressionToString(Compression: TZipCompression): string;
begin
case Compression of
zcStored: Result := 'Stored'; // do not localize
zcShrunk: Result := 'Shrunk'; // do not localize
zcReduce1: Result := 'Reduced1'; // do not localize
zcReduce2: Result := 'Reduced2'; // do not localize
zcReduce3: Result := 'Reduced3'; // do not localize
zcReduce4: Result := 'Reduced4'; // do not localize
zcImplode: Result := 'Imploded'; // do not localize
zcTokenize: Result := 'Tokenized'; // do not localize
zcDeflate: Result := 'Deflated'; // do not localize
zcDeflate64: Result := 'Deflated64'; // do not localize
zcPKImplode: Result := 'Imploded(TERSE)'; // do not localize
zcBZIP2: Result := 'BZIP2'; // do not localize
zcLZMA: Result := 'LZMA'; // do not localize
zcTERSE: Result := 'TERSE'; // do not localize
zcLZ77: Result := 'LZ77'; // do not localize
zcWavePack: Result := 'WavPack'; // do not localize
zcPPMdI1: Result := 'PPMd version I, Rev 1'; // do not localize
else
Result := 'Unknown';
end;
end;
{ TZipFile }
function TZipFile.GetFileComment(Index: Integer): string;
begin
if FMode = zmClosed then
raise EZipException.CreateRes(@SZipNotOpen);
Result := string(FFiles[Index].FileComment);
end;
function TZipFile.GetFileCount: Integer;
begin
if FMode = zmClosed then
raise EZipException.CreateRes(@SZipNotOpen);
Result := FFiles.Count;
end;
function TZipFile.GetFileInfo(Index: Integer): TZipHeader;
begin
if FMode = zmClosed then
raise EZipException.CreateRes(@SZipNotOpen);
Result := FFiles[Index];
end;
function TZipFile.GetFileInfos: TArray<TZipHeader>;
begin
if FMode = zmClosed then
raise EZipException.CreateRes(@SZipNotOpen);
Result := FFiles.ToArray;
end;
function TZipFile.GetFileName(Index: Integer): string;
begin
if FMode = zmClosed then
raise EZipException.CreateRes(@SZipNotOpen);
Result := string(FFiles[Index].FileName);
end;
function TZipFile.GetFileNames: TArray<string>;
var
I: Integer;
begin
if FMode = zmClosed then
raise EZipException.CreateRes(@SZipNotOpen);
SetLength(Result, FFiles.Count);
for I := 0 to High(Result) do
Result[I] := string(FFiles[I].FileName);
end;
procedure TZipFile.ReadCentralHeader;
var
I: Integer;
Signature: UInt32;