* patch by Tom Gregorovic (with some fixes by Alexander Grau) to

* support creating directories and symbolic links while unzipping
    * preserve access rights and date of unzipped files
    * fix the CRC32 control when the local header field is zero
   -> fixes mantis #14106
  * fixed unzipping stored files of zero bytes

git-svn-id: trunk@13378 -
This commit is contained in:
Jonas Maebe 2009-07-11 09:40:22 +00:00
parent d09b2d91a9
commit 292e4200c8

View File

@ -18,6 +18,9 @@ unit zipper;
Interface Interface
Uses Uses
{$IFDEF UNIX}
BaseUnix,
{$ENDIF}
SysUtils,Classes,ZStream; SysUtils,Classes,ZStream;
@ -251,15 +254,21 @@ Type
TZipFileEntry = Class(TCollectionItem) TZipFileEntry = Class(TCollectionItem)
private private
FArchiveFileName: String; FArchiveFileName: String;
FAttributes: LongInt;
FCRC32: LongWord;
FDateTime: TDateTime; FDateTime: TDateTime;
FDiskFileName: String; FDiskFileName: String;
FHeaderPos: Longint; FHeaderPos: Longint;
FOS: Byte;
FSize: Integer; FSize: Integer;
FStream: TStream; FStream: TStream;
function GetArchiveFileName: String; function GetArchiveFileName: String;
Protected Protected
Property HdrPos : Longint Read FHeaderPos Write FheaderPos; Property HdrPos : Longint Read FHeaderPos Write FheaderPos;
Public Public
constructor Create;
function IsDirectory: Boolean;
function IsLink: Boolean;
Procedure Assign(Source : TPersistent); override; Procedure Assign(Source : TPersistent); override;
Property Stream : TStream Read FStream Write FStream; Property Stream : TStream Read FStream Write FStream;
Published Published
@ -267,6 +276,9 @@ Type
Property DiskFileName : String Read FDiskFileName Write FDiskFileName; Property DiskFileName : String Read FDiskFileName Write FDiskFileName;
Property Size : Integer Read FSize Write FSize; Property Size : Integer Read FSize Write FSize;
Property DateTime : TDateTime Read FDateTime Write FDateTime; Property DateTime : TDateTime Read FDateTime Write FDateTime;
property OS: Byte read FOS write FOS;
property Attributes: LongInt read FAttributes write FAttributes;
property CRC32: LongWord read FCRC32 write FCRC32;
end; end;
{ TZipFileEntries } { TZipFileEntries }
@ -337,8 +349,6 @@ Type
Property Entries : TZipFileEntries Read FEntries Write SetEntries; Property Entries : TZipFileEntries Read FEntries Write SetEntries;
end; end;
{ TYbZipper }
{ TUnZipper } { TUnZipper }
TUnZipper = Class(TObject) TUnZipper = Class(TObject)
@ -363,7 +373,7 @@ Type
Procedure OpenInput; Procedure OpenInput;
Procedure CloseOutput; Procedure CloseOutput;
Procedure CloseInput; Procedure CloseInput;
Procedure ReadZipHeader(Item : TZipFileEntry; out ACRC : LongWord;out AMethod : Word); Procedure ReadZipHeader(Item : TZipFileEntry; out AMethod : Word);
Procedure ReadZipDirectory; Procedure ReadZipDirectory;
Procedure DoEndOfFile; Procedure DoEndOfFile;
Procedure UnZipOneFile(Item : TZipFileEntry); virtual; Procedure UnZipOneFile(Item : TZipFileEntry); virtual;
@ -497,6 +507,67 @@ begin
DT:=ComposeDateTime(EncodeDate(Y,M,D),EncodeTime(H,N,S,MS)); DT:=ComposeDateTime(EncodeDate(Y,M,D),EncodeTime(H,N,S,MS));
end; end;
const
OS_FAT = 0;
OS_UNIX = 3;
UNIX_MASK = $F000;
UNIX_FIFO = $1000;
UNIX_CHAR = $2000;
UNIX_DIR = $4000;
UNIX_BLK = $6000;
UNIX_FILE = $8000;
UNIX_LINK = $A000;
UNIX_SOCK = $C000;
UNIX_RUSR = $0100;
UNIX_WUSR = $0080;
UNIX_XUSR = $0040;
UNIX_RGRP = $0020;
UNIX_WGRP = $0010;
UNIX_XGRP = $0008;
UNIX_ROTH = $0004;
UNIX_WOTH = $0002;
UNIX_XOTH = $0001;
UNIX_DEFAULT = UNIX_RUSR or UNIX_WUSR or UNIX_XUSR or UNIX_RGRP or UNIX_ROTH;
function ZipUnixAttrsToFatAttrs(const Name: String; Attrs: Longint): Longint;
begin
Result := faArchive;
if (Pos('.', Name) = 1) and (Name <> '.') and (Name <> '..') then
Result := Result + faHidden;
case (Attrs and UNIX_MASK) of
UNIX_DIR: Result := Result + faDirectory;
UNIX_LINK: Result := Result + faSymLink;
UNIX_FIFO, UNIX_CHAR, UNIX_BLK, UNIX_SOCK:
Result := Result + faSysFile;
end;
if (Attrs and UNIX_WUSR) = 0 then
Result := Result + faReadOnly;
end;
function ZipFatAttrsToUnixAttrs(Attrs: Longint): Longint;
begin
Result := UNIX_DEFAULT;
if (faReadOnly and Attrs) > 0 then
Result := Result and not (UNIX_WUSR);
if (faSymLink and Attrs) > 0 then
Result := Result or UNIX_LINK
else
if (faDirectory and Attrs) > 0 then
Result := Result or UNIX_DIR
else
Result := Result or UNIX_FILE;
end;
{ --------------------------------------------------------------------- { ---------------------------------------------------------------------
TDeCompressor TDeCompressor
---------------------------------------------------------------------} ---------------------------------------------------------------------}
@ -639,7 +710,7 @@ Const
SPECIAL = 256; { Special function code } SPECIAL = 256; { Special function code }
INCSIZE = 1; { Code indicating a jump in code size } INCSIZE = 1; { Code indicating a jump in code size }
CLEARCODE = 2; { Code indicating code table has been cleared } CLEARCODE = 2; { Code indicating code table has been cleared }
STDATTR = $23; { Standard file attribute for DOS Find First/Next } STDATTR = faAnyFile; { Standard file attribute for DOS Find First/Next }
constructor TShrinker.Create(AInFile, AOutFile : TStream; ABufSize : LongWord); constructor TShrinker.Create(AInFile, AOutFile : TStream; ABufSize : LongWord);
begin begin
@ -1001,7 +1072,9 @@ Var
F : TZipFileEntry; F : TZipFileEntry;
Info : TSearchRec; Info : TSearchRec;
I : Longint; I : Longint;
{$IFDEF UNIX}
UnixInfo: Stat;
{$ENDIF}
Begin Begin
For I := 0 to FEntries.Count-1 do For I := 0 to FEntries.Count-1 do
begin begin
@ -1014,6 +1087,12 @@ Begin
try try
F.Size:=Info.Size; F.Size:=Info.Size;
F.DateTime:=FileDateToDateTime(Info.Time); F.DateTime:=FileDateToDateTime(Info.Time);
{$IFDEF UNIX}
if fplstat(F.DiskFileName, @UnixInfo) = 0 then
F.Attributes := UnixInfo.st_mode;
{$ELSE}
F.Attributes := Info.Attr;
{$ENDIF}
finally finally
FindClose(Info); FindClose(Info);
end end
@ -1025,6 +1104,11 @@ Begin
If (F.ArchiveFileName='') then If (F.ArchiveFileName='') then
Raise EZipError.CreateFmt(SErrMissingArchiveName,[I]); Raise EZipError.CreateFmt(SErrMissingArchiveName,[I]);
F.Size:=F.Stream.Size; F.Size:=F.Stream.Size;
{$IFDEF UNIX}
F.Attributes := UNIX_FILE or UNIX_DEFAULT;
{$ELSE}
F.Attributes := faArchive;
{$ENDIF}
end; end;
end; end;
end; end;
@ -1049,7 +1133,10 @@ Begin
If (Item.Stream<>nil) then If (Item.Stream<>nil) then
FInFile:=Item.Stream FInFile:=Item.Stream
else else
FInFile:=TFileStream.Create(Item.DiskFileName,fmOpenRead); if Item.IsDirectory then
FInFile := TStringStream.Create('')
else
FInFile:=TFileStream.Create(Item.DiskFileName,fmOpenRead);
Result:=True; Result:=True;
If Assigned(FOnStartFile) then If Assigned(FOnStartFile) then
FOnStartFile(Self,Item.ArchiveFileName); FOnStartFile(Self,Item.ArchiveFileName);
@ -1143,13 +1230,20 @@ Begin
begin begin
Signature := CENTRAL_FILE_HEADER_SIGNATURE; Signature := CENTRAL_FILE_HEADER_SIGNATURE;
MadeBy_Version := LocalHdr.Extract_Version_Reqd; MadeBy_Version := LocalHdr.Extract_Version_Reqd;
{$IFDEF UNIX}
MadeBy_Version := MadeBy_Version or (OS_UNIX shl 8);
{$ENDIF}
Move(LocalHdr.Extract_Version_Reqd, Extract_Version_Reqd, 26); Move(LocalHdr.Extract_Version_Reqd, Extract_Version_Reqd, 26);
Last_Mod_Time:=localHdr.Last_Mod_Time; Last_Mod_Time:=localHdr.Last_Mod_Time;
Last_Mod_Date:=localHdr.Last_Mod_Date; Last_Mod_Date:=localHdr.Last_Mod_Date;
File_Comment_Length := 0; File_Comment_Length := 0;
Starting_Disk_Num := 0; Starting_Disk_Num := 0;
Internal_Attributes := 0; Internal_Attributes := 0;
External_Attributes := faARCHIVE; {$IFDEF UNIX}
External_Attributes := Entries[ACount].Attributes shl 16;
{$ELSE}
External_Attributes := Entries[ACount].Attributes;
{$ENDIF}
Local_Header_Offset := HdrPos; Local_Header_Offset := HdrPos;
end; end;
FOutFile.Seek(0,soFromEnd); FOutFile.Seek(0,soFromEnd);
@ -1367,9 +1461,21 @@ End;
Function TUnZipper.OpenOutput(OutFileName : String) : Boolean; Function TUnZipper.OpenOutput(OutFileName : String) : Boolean;
Var
Path: String;
OldDirectorySeparators: set of char;
Begin Begin
ForceDirectories(ExtractFilePath(OutFileName)); { the default RTL behaviour is broken on Unix platforms
for Windows compatibility: it allows both '/' and '\'
as directory separator. We don't want that behaviour
here, since 'abc\' is a valid file name under Unix.
}
OldDirectorySeparators:=AllowDirectorySeparators;
AllowDirectorySeparators:=[DirectorySeparator];
Path:=ExtractFilePath(OutFileName);
if (Path<>'') then
ForceDirectories(Path);
AllowDirectorySeparators:=OldDirectorySeparators;
FOutFile:=TFileStream.Create(OutFileName,fmCreate); FOutFile:=TFileStream.Create(OutFileName,fmCreate);
Result:=True; Result:=True;
If Assigned(FOnStartFile) then If Assigned(FOnStartFile) then
@ -1391,12 +1497,10 @@ Begin
end; end;
Procedure TUnZipper.ReadZipHeader(Item : TZipFileEntry; out ACRC : LongWord; out AMethod : Word); Procedure TUnZipper.ReadZipHeader(Item : TZipFileEntry; out AMethod : Word);
Var Var
S : String; S : String;
D : TDateTime; D : TDateTime;
Begin Begin
FZipFile.Seek(Item.HdrPos,soFromBeginning); FZipFile.Seek(Item.HdrPos,soFromBeginning);
FZipFile.ReadBuffer(LocalHdr,SizeOf(LocalHdr)); FZipFile.ReadBuffer(LocalHdr,SizeOf(LocalHdr));
@ -1405,16 +1509,19 @@ Begin
{$ENDIF} {$ENDIF}
With LocalHdr do With LocalHdr do
begin begin
SetLength(S,Filename_Length); SetLength(S,Filename_Length);
FZipFile.ReadBuffer(S[1],Filename_Length); FZipFile.ReadBuffer(S[1],Filename_Length);
FZipFile.Seek(Extra_Field_Length,soCurrent); //SetLength(E,Extra_Field_Length);
Item.ArchiveFileName:=S; //FZipFile.ReadBuffer(E[1],Extra_Field_Length);
Item.DiskFileName:=S; FZipFile.Seek(Extra_Field_Length,soCurrent);
Item.Size:=Uncompressed_Size; Item.ArchiveFileName:=S;
ZipDateTimeToDateTime(Last_Mod_Date,Last_Mod_Time,D); Item.DiskFileName:=S;
Item.DateTime:=D; Item.Size:=Uncompressed_Size;
ACrc:=Crc32; ZipDateTimeToDateTime(Last_Mod_Date,Last_Mod_Time,D);
AMethod:=Compress_method; Item.DateTime:=D;
if Crc32 <> 0 then
Item.CRC32 := Crc32;
AMethod:=Compress_method;
end; end;
End; End;
@ -1427,7 +1534,6 @@ Var
CenDirPos : LongInt; CenDirPos : LongInt;
NewNode : TZipFileEntry; NewNode : TZipFileEntry;
S : String; S : String;
Begin Begin
EndHdrPos:=FZipFile.Size-SizeOf(EndHdr); EndHdrPos:=FZipFile.Size-SizeOf(EndHdr);
if EndHdrPos < 0 then if EndHdrPos < 0 then
@ -1452,14 +1558,23 @@ Begin
{$ENDIF} {$ENDIF}
With CentralHdr do With CentralHdr do
begin begin
if Signature<>CENTRAL_FILE_HEADER_SIGNATURE then if Signature<>CENTRAL_FILE_HEADER_SIGNATURE then
raise EZipError.CreateFmt(SErrCorruptZIP,[FZipFile.FileName]); raise EZipError.CreateFmt(SErrCorruptZIP,[FZipFile.FileName]);
NewNode:=FEntries.Add as TZipFileEntry; NewNode:=FEntries.Add as TZipFileEntry;
NewNode.HdrPos := Local_Header_Offset; NewNode.HdrPos := Local_Header_Offset;
SetLength(S,Filename_Length); SetLength(S,Filename_Length);
FZipFile.ReadBuffer(S[1],Filename_Length); FZipFile.ReadBuffer(S[1],Filename_Length);
NewNode.ArchiveFileName:=S; NewNode.ArchiveFileName:=S;
FZipFile.Seek(Extra_Field_Length+File_Comment_Length,soCurrent); NewNode.OS := MadeBy_Version shr 8;
if NewNode.OS = OS_UNIX then
NewNode.Attributes := External_Attributes shr 16
else
NewNode.Attributes := External_Attributes;
NewNode.CRC32 := Crc32;
FZipFile.Seek(Extra_Field_Length + File_Comment_Length,soCurrent);
end; end;
end; end;
end; end;
@ -1477,35 +1592,104 @@ end;
Procedure TUnZipper.UnZipOneFile(Item : TZipFileEntry); Procedure TUnZipper.UnZipOneFile(Item : TZipFileEntry);
Var Var
Count : Longint; Count, Attrs: Longint;
CRC : LongWord;
ZMethod : Word; ZMethod : Word;
OutputFileName : string; LinkTargetStream: TStringStream;
Begin OutputFileName: string;
Try IsLink: Boolean;
ReadZipHeader(Item,CRC,ZMethod);
OutputFileName:=Item.DiskFileName; procedure DoUnzip(const Dest: TStream);
if FOutputPath<>'' then begin
OutputFileName:=IncludeTrailingPathDelimiter(FOutputPath)+OutputFileName;
OpenOutput(OutputFileName);
if ZMethod=0 then if ZMethod=0 then
begin begin
Count:=FOutFile.CopyFrom(FZipFile,LocalHdr.Compressed_Size); if (LocalHdr.Compressed_Size<>0) then
{$warning TODO: Implement CRC Check} begin
end Count:=Dest.CopyFrom(FZipFile,LocalHdr.Compressed_Size)
{$warning TODO: Implement CRC Check}
end
else
Count:=0;
end
else else
With CreateDecompressor(Item, ZMethod, FZipFile, FOutFile) do With CreateDecompressor(Item, ZMethod, FZipFile, Dest) do
Try Try
OnProgress:=Self.OnProgress; OnProgress:=Self.OnProgress;
OnPercent:=Self.OnPercent; OnPercent:=Self.OnPercent;
DeCompress; DeCompress;
if CRC<>Crc32Val then if Item.CRC32 <> Crc32Val then
raise EZipError.CreateFmt(SErrInvalidCRC,[Item.ArchiveFileName]); raise EZipError.CreateFmt(SErrInvalidCRC,[Item.ArchiveFileName]);
Finally Finally
Free; Free;
end; end;
Finally end;
CloseOutput; Begin
ReadZipHeader(Item, ZMethod);
OutputFileName:=Item.DiskFileName;
if FOutputPath<>'' then
OutputFileName:=IncludeTrailingPathDelimiter(FOutputPath)+OutputFileName;
IsLink := Item.IsLink;
{$IFNDEF UNIX}
if IsLink then
begin
{$warning TODO: Implement symbolic link creation for non-unix}
IsLink := False;
end;
{$ENDIF}
if IsLink then
begin
{$IFDEF UNIX}
LinkTargetStream := TStringStream.Create('');
try
DoUnzip(LinkTargetStream);
fpSymlink(PChar(LinkTargetStream.DataString), PChar(OutputFileName));
finally
LinkTargetStream.Free;
end;
{$ENDIF}
end
else
begin
if Item.IsDirectory then
CreateDir(OutputFileName)
else
begin
try
OpenOutput(OutputFileName);
DoUnzip(FOutFile);
Finally
CloseOutput;
end;
end;
end;
// set attributes
FileSetDate(OutputFileName, DateTimeToFileDate(Item.DateTime));
if (Item.Attributes <> 0) then
begin
Attrs := 0;
{$IFDEF UNIX}
if Item.OS = OS_UNIX then Attrs := Item.Attributes;
if Item.OS = OS_FAT then
Attrs := ZipFatAttrsToUnixAttrs(Item.Attributes);
{$ELSE}
if Item.OS = OS_FAT then Attrs := Item.Attributes;
if Item.OS = OS_UNIX then
Attrs := ZipUnixAttrsToFatAttrs(ExtractFileName(Item.ArchiveFileName), Item.Attributes);
{$ENDIF}
if Attrs <> 0 then
begin
{$IFDEF UNIX}
FpChmod(OutputFileName, Attrs);
{$ELSE}
FileSetAttr(OutputFileName, Attrs);
{$ENDIF}
end;
end; end;
end; end;
@ -1626,6 +1810,39 @@ begin
Result:=FDiskFileName; Result:=FDiskFileName;
end; end;
constructor TZipFileEntry.Create;
begin
{$IFDEF UNIX}
FOS := OS_UNIX;
{$ELSE}
FOS := OS_FAT;
{$ENDIF}
end;
function TZipFileEntry.IsDirectory: Boolean;
begin
Result := (DiskFileName <> '') and (DiskFileName[Length(DiskFileName)] in ['/', '\']);
if Attributes <> 0 then
begin
case OS of
OS_FAT: Result := (faDirectory and Attributes) > 0;
OS_UNIX: Result := (Attributes and UNIX_MASK) = UNIX_DIR;
end;
end;
end;
function TZipFileEntry.IsLink: Boolean;
begin
Result := False;
if Attributes <> 0 then
begin
case OS of
OS_FAT: Result := (faSymLink and Attributes) > 0;
OS_UNIX: Result := (Attributes and UNIX_MASK) = UNIX_LINK;
end;
end;
end;
procedure TZipFileEntry.Assign(Source: TPersistent); procedure TZipFileEntry.Assign(Source: TPersistent);
Var Var
@ -1657,8 +1874,7 @@ begin
Items[AIndex]:=AValue; Items[AIndex]:=AValue;
end; end;
function TZipFileEntries.AddFileEntry(const ADiskFileName: String function TZipFileEntries.AddFileEntry(const ADiskFileName: String): TZipFileEntry;
): TZipFileEntry;
begin begin
Result:=Add as TZipFileEntry; Result:=Add as TZipFileEntry;
Result.DiskFileName:=ADiskFileName; Result.DiskFileName:=ADiskFileName;