unit Compression.SevenZipDLLDecoder; { Inno Setup Copyright (C) 1997-2025 Jordan Russell Portions by Martijn Laan For conditions of distribution and use, see LICENSE.TXT. Interface to the 7-Zip Decoder DLLs, used by Setup Based on the 7-Zip source code and the 7-Zip Delphi API by Henri Gourvest https://github.com/geoffsmith82/d7zip MPL 1.1 licensed } interface uses Windows, Shared.FileClass, Shared.VerInfoFunc, Shared.Int64Em, Compression.SevenZipDecoder; function SevenZipDLLInit(const SevenZipLibrary: HMODULE; [ref] const VersionNumbers: TFileVersionNumbers): Boolean; procedure SevenZipDLLDeInit; procedure ExtractArchiveRedir(const DisableFsRedir: Boolean; const ArchiveFilename, DestDir, Password: String; const FullPaths: Boolean; const OnExtractionProgress: TOnExtractionProgress); { These functions work similar to Windows' FindFirstFile, FindNextFile, and FindClose with the exception that recursion is built-in and that the resulting FindFileData.cFilename contains not just a filename but also the subdir. Also, ArchiveFindFirstFileRedir throws an exception for most errors: INVALID_HANDLE_VALUE is only used if the archive is ok but no suitable file was found. } type TArchiveFindHandle = type NativeUInt; TOnExtractToHandleProgress = procedure(const Bytes, Param: Integer64); function ArchiveFindFirstFileRedir(const DisableFsRedir: Boolean; const ArchiveFilename, DestDir, Password: String; const RecurseSubDirs, ExtractIntent: Boolean; out FindFileData: TWin32FindData): TArchiveFindHandle; function ArchiveFindNextFile(const FindFile: TArchiveFindHandle; out FindFileData: TWin32FindData): Boolean; function ArchiveFindClose(const FindFile: TArchiveFindHandle): Boolean; procedure ArchiveFindExtract(const FindFile: TArchiveFindHandle; const DestF: TFile; const OnExtractToHandleProgress: TOnExtractToHandleProgress; const OnExtractToHandleProgressParam: Integer64); type TFileTimeHelper = record helper for TFileTime procedure Clear; function HasTime: Boolean; end; implementation uses Classes, SysUtils, Forms, Variants, ActiveX, ComObj, Generics.Collections, Compression.SevenZipDLLDecoder.Interfaces, PathFunc, Shared.SetupMessageIDs, Shared.CommonFunc, SetupLdrAndSetup.Messages, SetupLdrAndSetup.RedirFunc, Setup.LoggingFunc, Setup.MainFunc, Setup.InstFunc; type TInStream = class(TInterfacedObject, IInStream) private FFile: TFile; protected function Read(data: Pointer; size: UInt32; processedSize: PUInt32): HRESULT; stdcall; function Seek(offset: Int64; seekOrigin: UInt32; newPosition: PUInt64): HRESULT; stdcall; public constructor Create(const AFile: TFile); destructor Destroy; override; end; TSequentialOutStream = class(TInterfacedObject, ISequentialOutStream) private FFile: TFile; FOwnsFile: Boolean; protected function Write(data: Pointer; size: UInt32; processedSize: PUInt32): HRESULT; stdcall; public constructor Create(const AFile: TFile; const AOwnsFile: Boolean = True); destructor Destroy; override; end; TArchiveCallback = class(TInterfacedObject, ICryptoGetTextPassword) private FPassword: String; protected { ICryptoGetTextPassword - queried for by 7-Zip both on IArchiveOpenCallback and IArchiveExtractCallback instances - note: have not yet seen 7-Zip actually call it on an IArchiveOpenCallback instance } function CryptoGetTextPassword(out password: WideString): HRESULT; stdcall; public constructor Create(const Password: String); end; TArchiveOpenCallback = class(TArchiveCallback, IArchiveOpenCallback) protected { IArchiveOpenCallback } function SetTotal(files, bytes: PUInt64): HRESULT; stdcall; function SetCompleted(files, bytes: PUInt64): HRESULT; stdcall; end; TArchiveExtractBaseCallback = class(TArchiveCallback, IArchiveExtractCallback) private type TResult = record SavedFatalException: TObject; Res: HRESULT; OpRes: TNOperationResult; end; TArrayOfUInt32 = array of UInt32; var FInArchive: IInArchive; FnumItems: UInt32; FLock: TObject; FProgress, FProgressMax: UInt64; FAbort: Boolean; FResult: TResult; protected { IProgress } function SetTotal(total: UInt64): HRESULT; stdcall; function SetCompleted(completeValue: PUInt64): HRESULT; stdcall; { IArchiveExtractCallback } function GetStream(index: UInt32; out outStream: ISequentialOutStream; askExtractMode: Int32): HRESULT; virtual; stdcall; abstract; function PrepareOperation(askExtractMode: Int32): HRESULT; stdcall; function SetOperationResult(opRes: TNOperationResult): HRESULT; stdcall; { Other } function GetIndices: TArrayOfUInt32; virtual; abstract; procedure Extract; procedure HandleProgress; virtual; abstract; procedure HandleResult; public constructor Create(const InArchive: IInArchive; const numItems: UInt32; const Password: String); destructor Destroy; override; end; TArchiveExtractAllCallback = class(TArchiveExtractBaseCallback) private type TCurrent = record Path, ExpandedPath: String; HasAttrib: Boolean; Attrib: DWORD; CTime, MTime: TFileTime; outStream: ISequentialOutStream; procedure SetAttrib(const AAttrib: DWORD); end; var FDisableFsRedir: Boolean; FExpandedDestDir: String; FFullPaths: Boolean; FExtractedArchiveName: String; FOnExtractionProgress: TOnExtractionProgress; FCurrent: TCurrent; { Protected by base's FLock } FLogQueue: TStrings; { Same } protected { IArchiveExtractCallback } function GetStream(index: UInt32; out outStream: ISequentialOutStream; askExtractMode: Int32): HRESULT; override; stdcall; function SetOperationResult(opRes: TNOperationResult): HRESULT; stdcall; { Other } function GetIndices: TArchiveExtractBaseCallback.TArrayOfUInt32; override; procedure HandleProgress; override; public constructor Create(const InArchive: IInArchive; const numItems: UInt32; const DisableFsRedir: Boolean; const ArchiveFileName, DestDir, Password: String; const FullPaths: Boolean; const OnExtractionProgress: TOnExtractionProgress); destructor Destroy; override; end; TArchiveExtractToHandleCallback = class(TArchiveExtractBaseCallback) private FIndex: UInt32; FDestF: TFile; FOnExtractToHandleProgress: TOnExtractToHandleProgress; FOnExtractToHandleProgressParam: Integer64; FPreviousProgress: UInt64; protected { IArchiveExtractCallback } function GetStream(index: UInt32; out outStream: ISequentialOutStream; askExtractMode: Int32): HRESULT; override; stdcall; { Other } function GetIndices: TArchiveExtractBaseCallback.TArrayOfUInt32; override; procedure HandleProgress; override; public constructor Create(const InArchive: IInArchive; const numItems: UInt32; const Password: String; const Index: UInt32; const DestF: TFile; const OnExtractToHandleProgress: TOnExtractToHandleProgress; const OnExtractToHandleProgressParam: Integer64); end; { Helper functions } procedure SevenZipWin32Error(const FunctionName: String; ErrorCode: DWORD = 0); overload; begin if ErrorCode = 0 then ErrorCode := GetLastError; const ExceptMessage = FmtSetupMessage(msgErrorFunctionFailedWithMessage, [FunctionName, IntToStr(ErrorCode), Win32ErrorString(ErrorCode)]); const LogMessage = Format('Function %s returned error code %d', [FunctionName, ErrorCode]); SevenZipError(ExceptMessage, LogMessage); end; const varFileTime = 64; { Delphi lacks proper VT_FILETIME support } type TVarTypeSet = set of varEmpty..varFileTime; { Incomplete but don't need others } function GetProperty(const InArchive: IInArchive; const index: UInt32; const propID: PROPID; const allowedTypes: TVarTypeSet; out value: OleVariant): Boolean; overload; { Raises an EOleSysError exception on error but otherwise always sets value, returning True if it's not empty. Set index to $FFFF to query an archive property instead of an item propery } begin var Res: HRESULT; if index = $FFFF then Res := InArchive.GetArchiveProperty(propID, value) else Res := InArchive.GetProperty(index, propID, value); if Res <> S_OK then OleError(Res); Result := not VarIsEmpty(Value); if Result and not (VarType(value) in allowedTypes) then OleError(E_FAIL); end; function GetProperty(const InArchive: IInArchive; index: UInt32; propID: PROPID; out value: String): Boolean; overload; begin var varValue: OleVariant; Result := GetProperty(InArchive, index, propID, [varOleStr], varValue); value := varValue; end; function GetProperty(const InArchive: IInArchive; index: UInt32; propID: PROPID; out value: Cardinal): Boolean; overload; begin var varValue: OleVariant; Result := GetProperty(InArchive, index, propID, [varUInt32], varValue); value := varValue; end; function GetProperty(const InArchive: IInArchive; index: UInt32; propID: PROPID; out value: Boolean): Boolean; overload; begin var varValue: OleVariant; Result := GetProperty(InArchive, index, propID, [varBoolean], varValue); value := varValue; end; function GetProperty(const InArchive: IInArchive; index: UInt32; propID: PROPID; out value: Integer64): Boolean; overload; begin var varValue: OleVariant; Result := GetProperty(InArchive, index, propID, [varUInt64], varValue); value := Integer64(UInt64(varValue)); end; function GetProperty(const InArchive: IInArchive; index: UInt32; propID: PROPID; out value: TFileTime): Boolean; overload; begin var varValue: OleVariant; Result := GetProperty(InArchive, index, propID, [varFileTime], varValue); if Result then value := TFileTime(TVarData(varValue).VInt64) else value.Clear; end; procedure PosixHighDetect(var Attrib: DWORD); begin { "PosixHighDetect", just like FileDir.cpp and similar to 7zMain.c } if Attrib and $F0000000 <> 0 then Attrib := Attrib and $3FFF; end; { TInStream } constructor TInStream.Create(const AFile: TFile); begin inherited Create; FFile := AFile; end; destructor TInStream.Destroy; begin FFile.Free; inherited; end; function TInStream.Read(data: Pointer; size: UInt32; processedSize: PUInt32): HRESULT; begin try var BytesRead := FFile.Read(data^, size); if processedSize <> nil then processedSize^ := BytesRead; Result := S_OK; except on E: EAbort do Result := E_ABORT else Result := E_FAIL; end; end; function TInStream.Seek(offset: Int64; seekOrigin: UInt32; newPosition: PUInt64): HRESULT; begin try case seekOrigin of STREAM_SEEK_SET: FFile.Seek64(Integer64(offset)); STREAM_SEEK_CUR: FFile.Seek64(Integer64(Int64(FFile.Position) + offset)); STREAM_SEEK_END: FFile.Seek64(Integer64(Int64(FFile.Size) + offset)); end; if newPosition <> nil then newPosition^ := UInt64(FFile.Position); Result := S_OK; except on E: EAbort do Result := E_ABORT else Result := E_FAIL; end; end; { TSequentialOutStream } constructor TSequentialOutStream.Create(const AFile: TFile; const AOwnsFile: Boolean); begin inherited Create; FFile := AFile; FOwnsFile := AOwnsFile; end; destructor TSequentialOutStream.Destroy; begin if FOwnsFile then FFile.Free; inherited; end; function TSequentialOutStream.Write(data: Pointer; size: UInt32; processedSize: PUInt32): HRESULT; begin try FFile.WriteBuffer(data^, size); if processedSize <> nil then processedSize^ := size; Result := S_OK; except on E: EAbort do Result := E_ABORT else Result := E_FAIL; end; end; { TArchiveCallback } constructor TArchiveCallback.Create(const Password: String); begin inherited Create; FPassword := Password; end; function TArchiveCallback.CryptoGetTextPassword( out password: WideString): HRESULT; begin try password := FPassword; Result := S_OK; except on E: EAbort do Result := E_ABORT else Result := E_FAIL; end end; { TArchiveOpenCallback } function TArchiveOpenCallback.SetCompleted(files, bytes: PUInt64): HRESULT; begin Result := S_OK; end; function TArchiveOpenCallback.SetTotal(files, bytes: PUInt64): HRESULT; begin Result := S_OK; end; { TArchiveExtractBaseCallback } constructor TArchiveExtractBaseCallback.Create(const InArchive: IInArchive; const numItems: UInt32; const Password: String); begin inherited Create(Password); FInArchive := InArchive; FnumItems := numItems; FLock := TObject.Create; FResult.OpRes := kOK; end; destructor TArchiveExtractBaseCallback.Destroy; begin FResult.SavedFatalException.Free; FLock.Free; inherited; end; function TArchiveExtractBaseCallback.SetTotal(total: UInt64): HRESULT; begin { From IArchive.h: 7-Zip can call functions for IProgress or ICompressProgressInfo functions from another threads simultaneously with calls for IArchiveExtractCallback interface } try System.TMonitor.Enter(FLock); try FProgressMax := total; finally System.TMonitor.Exit(FLock); end; Result := S_OK; except on E: EAbort do Result := E_ABORT else Result := E_FAIL; end; end; function TArchiveExtractBaseCallback.SetCompleted(completeValue: PUInt64): HRESULT; begin try if FAbort then SysUtils.Abort; System.TMonitor.Enter(FLock); try FProgress := completeValue^; finally System.TMonitor.Exit(FLock); end; Result := S_OK; except on E: EAbort do Result := E_ABORT else Result := E_FAIL; end; end; function TArchiveExtractBaseCallback.PrepareOperation(askExtractMode: Int32): HRESULT; begin { From Client7z.cpp: PrepareOperation is called *after* GetStream has been called } Result := S_OK; end; function TArchiveExtractBaseCallback.SetOperationResult( opRes: TNOperationResult): HRESULT; begin try if opRes <> kOK then begin FResult.OpRes := opRes; Result := E_FAIL; { Make sure it doesn't continue with the next file } end else Result := S_OK; except on E: EAbort do Result := E_ABORT else Result := E_FAIL; end; end; function ExtractThreadFunc(Parameter: Pointer): Integer; begin const E = TArchiveExtractBaseCallback(Parameter); try const Indices = E.GetIndices; const NIndices = Length(Indices); if NIndices > 0 then begin { From IArchive.h: indices must be sorted. Also: 7-Zip's code crashes if sent an invalid index. So we check them fully. } for var I := 0 to NIndices-1 do if (Indices[I] >= E.FnumItems) or ((I > 0) and (Indices[I-1] >= Indices[I])) then InternalError('NIndices invalid'); E.FResult.Res := E.FInArchive.Extract(@Indices[0], NIndices, 0, E) end else E.FResult.Res := E.FInArchive.Extract(nil, $FFFFFFFF, 0, E) except const Ex = AcquireExceptionObject; MemoryBarrier; E.FResult.SavedFatalException := Ex; end; { Be extra sure FSavedFatalException (and everything else) is made visible prior to thread termination. (Likely redundant, but you never know...) } MemoryBarrier; Result := 0; end; procedure TArchiveExtractBaseCallback.Extract; begin { We're calling 7-Zip's Extract in a separate thread. This is because packing our example MyProg.exe into a (tiny) .7z and extracting it caused a problem: GetStream and PrepareOperation and SetOperationResult were *all* called by 7-Zip from a secondary thread. So we can't block our main thread as well because then we can't communicate progress to it. Having this extra thread has the added bonus of being able to communicate progress more often from SetCompleted. } var ThreadID: TThreadID; { Not used but BeginThread requires it } const ThreadHandle = BeginThread(nil, 0, ExtractThreadFunc, Self, 0, ThreadID); if ThreadHandle = 0 then SevenZipWin32Error('BeginThread'); try try while True do begin case WaitForSingleObject(ThreadHandle, 50) of WAIT_OBJECT_0: Break; WAIT_TIMEOUT: HandleProgress; { This calls the user's OnExtractionProgress handler! } else SevenZipWin32Error('WaitForSingleObject'); end; end; except { If an exception was raised during the loop (most likely it would be from the user's OnExtractionProgress handler), request abort and make one more attempt to wait on the thread. If we don't get definitive confirmation that the thread terminated (WAIT_OBJECT_0), then bump the object's reference count to prevent it from being freed, because the thread could still be running and accessing the object. Leaking memory isn't ideal, but a use-after-free problem is worse. Realisitically, though, WaitForSingleObject should never fail if given a valid handle. } FAbort := True; { Atomic so no lock } if WaitForSingleObject(ThreadHandle, INFINITE) <> WAIT_OBJECT_0 then _AddRef; raise; end; finally CloseHandle(ThreadHandle); end; HandleProgress; HandleResult; end; procedure TArchiveExtractBaseCallback.HandleResult; procedure BadOperationResultError(const opRes: TNOperationResult); begin var LogMessage: String; case opRes of kUnsupportedMethod: LogMessage := 'Unsupported method'; kDataError: LogMessage := 'Data error'; kCRCError: LogMessage := 'CRC error'; kUnavailable: LogMessage := 'Unavailable data'; kUnexpectedEnd: LogMessage := 'Unexpected end'; kDataAfterEnd: LogMessage := 'Data after end'; kIsNotArc: LogMessage := 'Is not an archive'; kHeadersError: LogMessage := 'Headers error'; kWrongPassword: LogMessage := 'Wrong password'; else LogMessage := Format('Unknown operation result: %d', [Ord(opRes)]); end; case opRes of kUnsupportedMethod: SevenZipError(SetupMessages[msgArchiveUnsupportedFormat], LogMessage); kDataError, kCRCError, kUnavailable, kUnexpectedEnd, kDataAfterEnd, kIsNotArc, kHeadersError: SevenZipError(SetupMessages[msgArchiveIsCorrupted], LogMessage); kWrongPassword: SevenZipError(SetupMessages[msgArchiveIncorrectPassword], LogMessage); else SevenZipError(Ord(opRes).ToString, LogMessage); end; end; procedure BadResultError(const Res: HRESULT); begin if Res = E_OUTOFMEMORY then SevenZipError(Win32ErrorString(E_OUTOFMEMORY)) else SevenZipWin32Error('Extract', FResult.Res); end; begin if Assigned(FResult.SavedFatalException) then begin var Msg: String; if FResult.SavedFatalException is Exception then Msg := (FResult.SavedFatalException as Exception).Message else Msg := FResult.SavedFatalException.ClassName; InternalErrorFmt('Worker thread terminated unexpectedly with exception: %s', [Msg]); end else if FResult.Res = E_ABORT then Abort else begin var OpRes := FResult.OpRes; if OpRes <> kOK then BadOperationResultError(OpRes) else if FResult.Res <> S_OK then BadResultError(FResult.Res); end; end; { TArchiveExtractAllCallback } procedure TArchiveExtractAllCallback.TCurrent.SetAttrib(const AAttrib: DWORD); begin Attrib := AAttrib; HasAttrib := True; end; constructor TArchiveExtractAllCallback.Create(const InArchive: IInArchive; const numItems: UInt32; const DisableFsRedir: Boolean; const ArchiveFileName, DestDir, Password: String; const FullPaths: Boolean; const OnExtractionProgress: TOnExtractionProgress); begin inherited Create(InArchive, numItems, Password); FDisableFsRedir := DisableFsRedir; FExpandedDestDir := AddBackslash(PathExpand(DestDir)); FFullPaths := FullPaths; FExtractedArchiveName := PathExtractName(ArchiveFileName); FOnExtractionProgress := OnExtractionProgress; FLogQueue := TStringList.Create; end; destructor TArchiveExtractAllCallback.Destroy; begin FLogQueue.Free; end; function TArchiveExtractAllCallback.GetIndices: TArchiveExtractBaseCallback.TArrayOfUInt32; begin SetLength(Result, 0); { No indices = extract all } end; function TArchiveExtractAllCallback.GetStream(index: UInt32; out outStream: ISequentialOutStream; askExtractMode: Int32): HRESULT; begin try if FAbort then SysUtils.Abort; var NewCurrent := Default(TCurrent); if askExtractMode = kExtract then begin var Path: String; if not GetProperty(FInArchive, index, kpidPath, Path) then Path := PathChangeExt(FExtractedArchiveName, ''); var IsDir: Boolean; GetProperty(FInArchive, index, kpidIsDir, IsDir); if IsDir then begin if FFullPaths then begin NewCurrent.Path := Path + '\'; if not ValidateAndCombinePath(FExpandedDestDir, Path, NewCurrent.ExpandedPath) then OleError(E_ACCESSDENIED); ForceDirectories(FDisableFsRedir, NewCurrent.ExpandedPath); end; outStream := nil; end else begin var Attrib: DWORD; if GetProperty(FInArchive, index, kpidAttrib, Attrib) then begin PosixHighDetect(Attrib); NewCurrent.SetAttrib(Attrib); end; GetProperty(FInArchive, index, kpidCTime, NewCurrent.CTime); GetProperty(FInArchive, index, kpidMTime, NewCurrent.MTime); if not FFullPaths then Path := PathExtractName(Path); NewCurrent.Path := Path; if not ValidateAndCombinePath(FExpandedDestDir, Path, NewCurrent.ExpandedPath) then OleError(E_ACCESSDENIED); ForceDirectories(FDisableFsRedir, PathExtractPath(NewCurrent.ExpandedPath)); const ExistingFileAttr = GetFileAttributesRedir(FDisableFsRedir, NewCurrent.ExpandedPath); if (ExistingFileAttr <> INVALID_FILE_ATTRIBUTES) and (ExistingFileAttr and FILE_ATTRIBUTE_READONLY <> 0) then SetFileAttributesRedir(FDisableFsRedir, NewCurrent.ExpandedPath, ExistingFileAttr and not FILE_ATTRIBUTE_READONLY); const DestF = TFileRedir.Create(FDisableFsRedir, NewCurrent.ExpandedPath, fdCreateAlways, faWrite, fsNone); var BytesLeft: Integer64; if GetProperty(FInArchive, index, kpidSize, BytesLeft) then begin { To avoid file system fragmentation, preallocate all of the bytes in the destination file } DestF.Seek64(BytesLeft); DestF.Truncate; DestF.Seek(0); end; { From IArchive.h: can also set outstream to nil to tell 7zip to skip the file } outstream := TSequentialOutStream.Create(DestF); NewCurrent.outStream := outStream; end; end; System.TMonitor.Enter(FLock); try FCurrent := NewCurrent; if NewCurrent.Path <> '' then FLogQueue.Append(NewCurrent.Path) finally System.TMonitor.Exit(FLock); end; Result := S_OK; except on E: EOleSysError do Result := E.ErrorCode; on E: EAbort do Result := E_ABORT else Result := E_FAIL; end; end; function TArchiveExtractAllCallback.SetOperationResult(opRes: TNOperationResult): HRESULT; begin { From IArchive.h: Can now can close the file, set attributes, timestamps and security information } try try Result := inherited; if Result = S_OK then begin { GetStream is the only writer to outStream and ExpandedPath and HasAttrib so we don't need a lock because of this note from IArchive.h: 7-Zip doesn't call GetStream/PrepareOperation/SetOperationResult from different threads simultaneously } if (FCurrent.outStream <> nil) and (FCurrent.CTime.HasTime or FCurrent.MTime.HasTime) then SetFileTime((FCurrent.outStream as TSequentialOutStream).FFile.Handle, @FCurrent.CTime, nil, @FCurrent.MTime); FCurrent.outStream := nil; { Like 7zMain.c close the file before setting attributes - note that 7-Zip has cleared its own reference as well already } if (FCurrent.ExpandedPath <> '') and FCurrent.HasAttrib then SetFileAttributesRedir(FDisableFsRedir, FCurrent.ExpandedPath, FCurrent.Attrib); end; finally FCurrent.outStream := nil; end; except on E: EAbort do Result := E_ABORT else Result := E_FAIL; end; end; procedure TArchiveExtractAllCallback.HandleProgress; begin var CurrentPath: String; var Progress, ProgressMax: UInt64; System.TMonitor.Enter(FLock); try CurrentPath := FCurrent.Path; Progress := FProgress; ProgressMax := FProgressMax; for var S in FLogQueue do LogFmt('- %s', [S]); { Just like 7zMain.c } FLogQueue.Clear; finally System.TMonitor.Exit(FLock); end; var Abort := FAbort; if Abort then Exit; if (CurrentPath <> '') and Assigned(FOnExtractionProgress) then begin { Calls to HandleProgress are already throttled so here we don't have to worry about calling the script to often } if not FOnExtractionProgress(FExtractedArchiveName, CurrentPath, Progress, ProgressMax) then Abort := True; end; if not Abort and DownloadTemporaryFileOrExtractArchiveProcessMessages then Application.ProcessMessages; if Abort then FAbort := Abort; { Atomic so no lock } end; { TArchiveExtractToHandleCallback } constructor TArchiveExtractToHandleCallback.Create(const InArchive: IInArchive; const numItems: UInt32; const Password: String; const Index: UInt32; const DestF: TFile; const OnExtractToHandleProgress: TOnExtractToHandleProgress; const OnExtractToHandleProgressParam: Integer64); begin inherited Create(InArchive, numItems, Password); FIndex := Index; FDestF := DestF; FOnExtractToHandleProgress := OnExtractToHandleProgress; FOnExtractToHandleProgressParam := OnExtractToHandleProgressParam; end; function TArchiveExtractToHandleCallback.GetIndices: TArchiveExtractBaseCallback.TArrayOfUInt32; begin SetLength(Result, 1); Result[0] := FIndex; end; function TArchiveExtractToHandleCallback.GetStream(index: UInt32; out outStream: ISequentialOutStream; askExtractMode: Int32): HRESULT; begin try if askExtractMode = kExtract then begin if index <> FIndex then OleError(E_INVALIDARG); var IsDir: Boolean; GetProperty(FInArchive, index, kpidIsDir, IsDir); if IsDir then OleError(E_INVALIDARG); var BytesLeft: Integer64; if GetProperty(FInArchive, index, kpidSize, BytesLeft) then begin { To avoid file system fragmentation, preallocate all of the bytes in the destination file } FDestF.Seek64(BytesLeft); FDestF.Truncate; FDestF.Seek(0); end; outstream := TSequentialOutStream.Create(FDestF, False); end; Result := S_OK; except on E: EOleSysError do Result := E.ErrorCode; on E: EAbort do Result := E_ABORT else Result := E_FAIL; end; end; procedure TArchiveExtractToHandleCallback.HandleProgress; begin if Assigned(FOnExtractToHandleProgress) then begin var Progress: UInt64; System.TMonitor.Enter(FLock); try Progress := FProgress; finally System.TMonitor.Exit(FLock); end; FOnExtractToHandleProgress(Integer64(Progress-FPreviousProgress), FOnExtractToHandleProgressParam); FPreviousProgress := Progress; end; end; { Additional helper functions } var CreateSevenZipObject: function(const clsid, iid: TGUID; var outObject): HRESULT; stdcall; VersionBanner: String; function SevenZipDLLInit(const SevenZipLibrary: HMODULE; [ref] const VersionNumbers: TFileVersionNumbers): Boolean; begin CreateSevenZipObject := GetProcAddress(SevenZipLibrary, 'CreateObject'); Result := Assigned(CreateSevenZipObject); if (VersionNumbers.MS <> 0) or (VersionNumbers.LS <> 0) then VersionBanner := Format(' %u.%.2u', [(VersionNumbers.MS shr 16) and $FFFF, VersionNumbers.MS and $FFFF]) else VersionBanner := ''; end; function GetHandler(const Ext, NotFoundErrorMsg: String): TGUID; begin if SameText(Ext, '.7z') then Result := CLSID_Handler7z else if SameText(Ext, '.zip') then Result := CLSID_HandlerZip else if SameText(Ext, '.gz') then Result := CLSID_HandlerGzip else if SameText(Ext, '.bz2') then Result := CLSID_HandlerBZip2 else if SameText(Ext, '.xz') then Result := CLSID_HandlerXz else if SameText(Ext, '.tar') then Result := CLSID_HandlerTar else if SameText(Ext, '.rar') then Result := CLSID_HandlerRar else if SameText(Ext, '.iso') then Result := CLSID_HandlerIso else if SameText(Ext, '.msi') then Result := CLSID_HandlerCompound else if SameText(Ext, '.cab') then Result := CLSID_HandlerCab else if SameText(Ext, '.rpm') then Result := CLSID_HandlerRpm else if SameText(Ext, '.vhd') then Result := CLSID_HandlerVhd else if SameText(Ext, '.vhdx') then Result := CLSID_HandlerVhdx else if SameText(Ext, '.vdi') then Result := CLSID_HandlerVDI else if SameText(Ext, '.vmdk') then Result := CLSID_HandlerVMDK else if SameText(Ext, '.wim') then Result := CLSID_HandlerWim else if SameText(Ext, '.dmg') then Result := CLSID_HandlerDmg else InternalError(NotFoundErrorMsg); end; var LoggedBanner: Boolean; procedure LogBannerOnce; begin if not LoggedBanner then begin LogFmt('%s Decoder%s : Igor Pavlov', [SetupHeader.SevenZipLibraryName, VersionBanner]); { Just like 7zMain.c } LoggedBanner := True; end; end; function OpenArchiveRedir(const DisableFsRedir: Boolean; const ArchiveFilename, Password: String; const clsid: TGUID; out numItems: UInt32): IInArchive; begin { CreateObject } if CreateSevenZipObject(clsid, IInArchive, Result) <> S_OK then SevenZipError(SetupMessages[msgArchiveUnsupportedFormat], 'Cannot get class object' { Just like Client7z.cpp }); { Open } var F: TFile := nil; { Set to nil to silence compiler } try F := TFileRedir.Create(DisableFsRedir, ArchiveFilename, fdOpenExisting, faRead, fsRead); except SevenZipWin32Error('CreateFile'); end; const InStream: IInStream = TInStream.Create(F); var ScanSize: Int64 := 1 shl 23; { From Client7z.cpp } const OpenCallback: IArchiveOpenCallback = TArchiveOpenCallback.Create(Password); if Result.Open(InStream, @ScanSize, OpenCallback) <> S_OK then SevenZipError(SetupMessages[msgArchiveIsCorrupted], 'Cannot open file as archive' { Just like Client7z.cpp }); if Result.GetNumberOfItems(numItems) <> S_OK then SevenZipError(SetupMessages[msgArchiveIsCorrupted], 'Cannot get number of items'); end; { ExtractArchiveRedir } procedure ExtractArchiveRedir(const DisableFsRedir: Boolean; const ArchiveFilename, DestDir, Password: String; const FullPaths: Boolean; const OnExtractionProgress: TOnExtractionProgress); begin LogArchiveExtractionModeOnce; if ArchiveFileName = '' then InternalError('ExtractArchive: Invalid ArchiveFileName value'); const clsid = GetHandler(PathExtractExt(ArchiveFilename), 'ExtractArchive: Unknown ArchiveFileName extension'); if DestDir = '' then InternalError('ExtractArchive: Invalid DestDir value'); LogFmt('Extracting archive %s to %s. Full paths? %s', [ArchiveFileName, RemoveBackslashUnlessRoot(DestDir), SYesNo[FullPaths]]); LogBannerOnce; { Open } var numItems: UInt32; const InArchive = OpenArchiveRedir(DisableFsRedir, ArchiveFilename, Password, clsid, numItems); { Extract } const ExtractCallback: IArchiveExtractCallback = TArchiveExtractAllCallback.Create(InArchive, numItems, DisableFsRedir, ArchiveFilename, DestDir, Password, FullPaths, OnExtractionProgress); (ExtractCallback as TArchiveExtractAllCallback).Extract; Log('Everything is Ok'); { Just like 7zMain.c } end; { ArchiveFindFirstFileRedir & co } type TArchiveFindState = record InArchive: IInArchive; ExpandedDestDir, ExtractedArchiveName, Password: String; RecurseSubDirs: Boolean; currentIndex, numItems: UInt32; function GetInitialCurrentFindData(out FindData: TWin32FindData): Boolean; procedure FinishCurrentFindData(var FindData: TWin32FindData); end; TArchiveFindStates = TList; var ArchiveFindStates: TArchiveFindStates; function TArchiveFindState.GetInitialCurrentFindData(out FindData: TWin32FindData): Boolean; function SkipFile(const Path: String; const IsDir: Boolean): Boolean; begin Result := (not RecurseSubDirs and (IsDir or (PathPos('\', Path) <> 0))) or not ValidateAndCombinePath(ExpandedDestDir, Path); end; begin var Path: String; if not GetProperty(InArchive, currentIndex, kpidPath, Path) then Path := PathChangeExt(ExtractedArchiveName, ''); var IsDir: Boolean; GetProperty(InArchive, currentIndex, kpidIsDir, IsDir); Result := not SkipFile(Path, IsDir); if Result then begin FindData := Default(TWin32FindData); if Length(Path) >= MAX_PATH then InternalError('GetInitialCurrentFindData: Length(Path) >= MAX_PATH'); StrPCopy(FindData.cFileName, Path); if IsDir then FindData.dwFileAttributes := FindData.dwFileAttributes or FILE_ATTRIBUTE_DIRECTORY; end; end; procedure TArchiveFindState.FinishCurrentFindData(var FindData: TWin32FindData); begin if FindData.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY = 0 then begin var Attrib: DWORD; GetProperty(InArchive, currentIndex, kpidAttrib, Attrib); PosixHighDetect(Attrib); FindData.dwFileAttributes := FindData.dwFileAttributes or Attrib; GetProperty(InArchive, currentIndex, kpidCTime, FindData.ftCreationTime); GetProperty(InArchive, currentIndex, kpidMTime, FindData.ftLastWriteTime); var Size: Integer64; GetProperty(InArchive, currentIndex, kpidSize, Size); FindData.nFileSizeHigh := Size.Hi; FindData.nFileSizeLow := Size.Lo; end; end; function ArchiveFindFirstFileRedir(const DisableFsRedir: Boolean; const ArchiveFilename, DestDir, Password: String; const RecurseSubDirs, ExtractIntent: Boolean; out FindFileData: TWin32FindData): TArchiveFindHandle; begin LogArchiveExtractionModeOnce; if ArchiveFileName = '' then InternalError('ArchiveFindFirstFile: Invalid ArchiveFileName value'); const clsid = GetHandler(PathExtractExt(ArchiveFilename), 'ArchiveFindFirstFile: Unknown ArchiveFileName extension'); LogBannerOnce; { Open } var State := Default(TArchiveFindState); State.InArchive := OpenArchiveRedir(DisableFsRedir, ArchiveFilename, Password, clsid, State.numItems); if DestDir <> '' then State.ExpandedDestDir := AddBackslash(PathExpand(DestDir)); State.ExtractedArchiveName := PathExtractName(ArchiveFilename); State.Password := Password; State.RecurseSubDirs := RecurseSubDirs; if State.numItems > 0 then begin for var currentIndex: UInt32 := 0 to State.numItems-1 do begin if State.GetInitialCurrentFindData(FindFileData) then begin { Finish state } State.currentIndex := currentIndex; { Save state } if ArchiveFindStates = nil then ArchiveFindStates := TArchiveFindStates.Create; ArchiveFindStates.Add(State); { Log start of extraction } if ExtractIntent then begin LogFmt('Start extracting archive %s to %s. Recurse subdirs? %s', [ArchiveFilename, RemoveBackslashUnlessRoot(DestDir), SYesNo[RecurseSubDirs]]); var Solid: Boolean; if GetProperty(State.InArchive, $FFFF, kpidSolid, Solid) and Solid then Log('Archive is solid; extraction performance may degrade'); end; { Finish find data & exit } State.FinishCurrentFindData(FindFileData); Exit(ArchiveFindStates.Count-1); end; end; end; Result := INVALID_HANDLE_VALUE; end; function CheckFindFileHandle(const FindFile: TArchiveFindHandle): Integer; begin Result := Integer(FindFile); if (Result < 0) or (Result >= ArchiveFindStates.Count) or (ArchiveFindStates[Result].InArchive = nil) then InternalError('CheckFindFileHandle failed'); end; function ArchiveFindNextFile(const FindFile: TArchiveFindHandle; out FindFileData: TWin32FindData): Boolean; begin const I = CheckFindFileHandle(FindFile); var State := ArchiveFindStates[I]; for var currentIndex := State.currentIndex+1 to State.numItems-1 do begin State.currentIndex := currentIndex; if State.GetInitialCurrentFindData(FindFileData) then begin { Update state } ArchiveFindStates[I] := State; { This just updates currentIndex } { Finish find data & exit } State.FinishCurrentFindData(FindFileData); Exit(True); end; end; Result := False; end; function ArchiveFindClose(const FindFile: TArchiveFindHandle): Boolean; begin const I = CheckFindFileHandle(FindFile); var State := ArchiveFindStates[I]; State.InArchive := nil; ArchiveFindStates[I] := State; { This just updates InArchive } Result := True; end; procedure ArchiveFindExtract(const FindFile: TArchiveFindHandle; const DestF: TFile; const OnExtractToHandleProgress: TOnExtractToHandleProgress; const OnExtractToHandleProgressParam: Integer64); begin const State = ArchiveFindStates[CheckFindFileHandle(FindFile)]; var FindData: TWin32FindData; if not State.GetInitialCurrentFindData(FindData) or (FindData.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY <> 0) then InternalError('ArchiveFindExtract: Invalid current'); const ExtractCallback: IArchiveExtractCallback = TArchiveExtractToHandleCallback.Create(State.InArchive, State.numItems, State.Password, State.currentIndex, DestF, OnExtractToHandleProgress, OnExtractToHandleProgressParam); (ExtractCallback as TArchiveExtractToHandleCallback).Extract; end; { TFileTimeHelper } procedure TFileTimeHelper.Clear; begin { SetFileTime regards a pointer to a FILETIME structure with both members set to 0 the same as a NULL pointer and we make use of that. Note that 7-Zip may return a value with both members set to 0 as well. } dwLowDateTime := 0; dwHighDateTime := 0; end; function TFileTimeHelper.HasTime: Boolean; begin Result := (dwLowDateTime <> 0) or (dwHighDateTime <> 0); end; { SevenZipDLLDeInit } procedure SevenZipDLLDeInit; begin { ArchiveFindStates has references to 7-Zip so must be cleared before the DLL is unloaded } ArchiveFindStates.Free; end; end.