2024-08-06 19:12:28 +02:00
|
|
|
unit Setup.InstFunc;
|
2011-10-06 20:53:09 +02:00
|
|
|
|
|
|
|
{
|
|
|
|
Inno Setup
|
2025-05-17 08:43:41 +02:00
|
|
|
Copyright (C) 1997-2025 Jordan Russell
|
2011-10-06 20:53:09 +02:00
|
|
|
Portions by Martijn Laan
|
|
|
|
For conditions of distribution and use, see LICENSE.TXT.
|
|
|
|
|
2024-08-06 19:12:28 +02:00
|
|
|
Misc. installation functions. Used only by the Setup project.
|
2011-10-06 20:53:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
interface
|
|
|
|
|
|
|
|
uses
|
2025-06-06 18:06:07 +02:00
|
|
|
Windows, SysUtils, Shared.Int64Em, SHA256, Shared.CommonFunc, Shared.FileClass;
|
2011-10-06 20:53:09 +02:00
|
|
|
|
|
|
|
type
|
|
|
|
PSimpleStringListArray = ^TSimpleStringListArray;
|
|
|
|
TSimpleStringListArray = array[0..$1FFFFFFE] of String;
|
|
|
|
TSimpleStringList = class
|
|
|
|
private
|
|
|
|
FList: PSimpleStringListArray;
|
|
|
|
FCount, FCapacity: Integer;
|
|
|
|
function Get(Index: Integer): String;
|
|
|
|
procedure SetCapacity(NewCapacity: Integer);
|
|
|
|
public
|
|
|
|
destructor Destroy; override;
|
|
|
|
procedure Add(const S: String);
|
|
|
|
procedure AddIfDoesntExist(const S: String);
|
|
|
|
procedure Clear;
|
|
|
|
function IndexOf(const S: String): Integer;
|
|
|
|
|
|
|
|
property Count: Integer read FCount;
|
|
|
|
property Items[Index: Integer]: String read Get; default;
|
|
|
|
end;
|
|
|
|
|
|
|
|
TDeleteDirProc = function(const DisableFsRedir: Boolean; const DirName: String;
|
|
|
|
const Param: Pointer): Boolean;
|
|
|
|
TDeleteFileProc = function(const DisableFsRedir: Boolean; const FileName: String;
|
|
|
|
const Param: Pointer): Boolean;
|
|
|
|
|
|
|
|
TEnumFROFilenamesProc = procedure(const Filename: String; Param: Pointer);
|
|
|
|
|
2024-08-04 10:25:42 +02:00
|
|
|
{ Must keep this in synch with Compiler.ScriptFunc.pas: }
|
2011-10-06 20:53:09 +02:00
|
|
|
TExecWait = (ewNoWait, ewWaitUntilTerminated, ewWaitUntilIdle);
|
|
|
|
|
2015-04-29 13:28:09 +02:00
|
|
|
function CheckForMutexes(const Mutexes: String): Boolean;
|
|
|
|
procedure CreateMutexes(const Mutexes: String);
|
2011-10-06 20:53:09 +02:00
|
|
|
function DecrementSharedCount(const RegView: TRegView; const Filename: String): Boolean;
|
2025-05-20 13:31:46 +02:00
|
|
|
function DelTree(const DisableFsRedir: Boolean; const Path: String;
|
2012-01-16 17:32:11 +01:00
|
|
|
const IsDir, DeleteFiles, DeleteSubdirsAlso, BreakOnError: Boolean;
|
2011-10-06 20:53:09 +02:00
|
|
|
const DeleteDirProc: TDeleteDirProc; const DeleteFileProc: TDeleteFileProc;
|
|
|
|
const Param: Pointer): Boolean;
|
|
|
|
procedure EnumFileReplaceOperationsFilenames(const EnumFunc: TEnumFROFilenamesProc;
|
|
|
|
Param: Pointer);
|
2024-05-27 03:42:38 -05:00
|
|
|
function GenerateNonRandomUniqueTempDir(const LimitCurrentUserSidAccess: Boolean;
|
2024-05-25 20:37:59 +02:00
|
|
|
Path: String; var TempDir: String): Boolean;
|
2011-10-06 20:53:09 +02:00
|
|
|
function GetComputerNameString: String;
|
2025-05-20 13:31:46 +02:00
|
|
|
function GetFileDateTime(const DisableFsRedir: Boolean; const Filename: String;
|
2011-10-06 20:53:09 +02:00
|
|
|
var DateTime: TFileTime): Boolean;
|
2025-06-06 18:06:07 +02:00
|
|
|
function GetSHA256OfFile(const DisableFsRedir: Boolean; const Filename: String): TSHA256Digest; overload;
|
|
|
|
function GetSHA256OfFile(const F: TFile): TSHA256Digest; overload;
|
2024-09-26 11:38:41 +02:00
|
|
|
function GetSHA256OfAnsiString(const S: AnsiString): TSHA256Digest;
|
|
|
|
function GetSHA256OfUnicodeString(const S: UnicodeString): TSHA256Digest;
|
2011-10-06 20:53:09 +02:00
|
|
|
function GetRegRootKeyName(const RootKey: HKEY): String;
|
2025-05-20 13:31:46 +02:00
|
|
|
function GetSpaceOnDisk(const DisableFsRedir: Boolean; const DriveRoot: String;
|
2011-10-06 20:53:09 +02:00
|
|
|
var FreeBytes, TotalBytes: Integer64): Boolean;
|
2025-05-20 13:31:46 +02:00
|
|
|
function GetSpaceOnNearestMountPoint(const DisableFsRedir: Boolean;
|
2011-10-06 20:53:09 +02:00
|
|
|
const StartDir: String; var FreeBytes, TotalBytes: Integer64): Boolean;
|
|
|
|
function GetUserNameString: String;
|
|
|
|
procedure IncrementSharedCount(const RegView: TRegView; const Filename: String;
|
|
|
|
const AlreadyExisted: Boolean);
|
2025-05-20 13:31:46 +02:00
|
|
|
function InstExec(const DisableFsRedir: Boolean; const Filename, Params: String;
|
2011-10-06 20:53:09 +02:00
|
|
|
WorkingDir: String; const Wait: TExecWait; const ShowCmd: Integer;
|
2024-07-02 16:02:59 +01:00
|
|
|
const ProcessMessagesProc: TProcedure; const OutputReader: TCreateProcessOutputReader;
|
2024-06-27 11:35:51 +01:00
|
|
|
var ResultCode: Integer): Boolean;
|
2011-10-06 20:53:09 +02:00
|
|
|
function InstShellExec(const Verb, Filename, Params: String; WorkingDir: String;
|
|
|
|
const Wait: TExecWait; const ShowCmd: Integer;
|
|
|
|
const ProcessMessagesProc: TProcedure; var ResultCode: Integer): Boolean;
|
|
|
|
procedure InternalError(const Id: String);
|
|
|
|
procedure InternalErrorFmt(const S: String; const Args: array of const);
|
2025-05-20 13:31:46 +02:00
|
|
|
function IsDirEmpty(const DisableFsRedir: Boolean; const Dir: String): Boolean;
|
|
|
|
function IsProtectedSystemFile(const DisableFsRedir: Boolean;
|
2011-10-06 20:53:09 +02:00
|
|
|
const Filename: String): Boolean;
|
2024-09-26 11:38:41 +02:00
|
|
|
function MakePendingFileRenameOperationsChecksum: TSHA256Digest;
|
2011-10-06 20:53:09 +02:00
|
|
|
function ModifyPifFile(const Filename: String; const CloseOnExit: Boolean): Boolean;
|
|
|
|
procedure RaiseFunctionFailedError(const FunctionName: String);
|
|
|
|
procedure RaiseOleError(const FunctionName: String; const ResultCode: HRESULT);
|
|
|
|
procedure RefreshEnvironment;
|
|
|
|
function ReplaceSystemDirWithSysWow64(const Path: String): String;
|
2012-01-12 11:04:14 +01:00
|
|
|
function ReplaceSystemDirWithSysNative(Path: String; const IsWin64: Boolean): String;
|
2020-07-03 10:05:28 +02:00
|
|
|
procedure UnregisterFont(const FontName, FontFilename: String; const PerUserFont: Boolean);
|
2025-05-20 13:31:46 +02:00
|
|
|
procedure RestartReplace(const DisableFsRedir: Boolean; TempFile, DestFile: String);
|
2011-10-06 20:53:09 +02:00
|
|
|
procedure SplitNewParamStr(const Index: Integer; var AName, AValue: String);
|
|
|
|
procedure Win32ErrorMsg(const FunctionName: String);
|
|
|
|
procedure Win32ErrorMsgEx(const FunctionName: String; const ErrorCode: DWORD);
|
2025-05-20 13:31:46 +02:00
|
|
|
function ForceDirectories(const DisableFsRedir: Boolean; Dir: String): Boolean;
|
2011-10-06 20:53:09 +02:00
|
|
|
|
|
|
|
implementation
|
|
|
|
|
|
|
|
uses
|
2024-08-06 19:12:28 +02:00
|
|
|
Messages, ShellApi, PathFunc, SetupLdrAndSetup.InstFunc, SetupLdrAndSetup.Messages,
|
2025-06-06 18:06:07 +02:00
|
|
|
Shared.SetupMessageIDs, SetupLdrAndSetup.RedirFunc, Shared.SetupTypes,
|
2025-05-13 12:44:59 +02:00
|
|
|
Classes, RegStr, Math;
|
2011-10-06 20:53:09 +02:00
|
|
|
|
|
|
|
procedure InternalError(const Id: String);
|
|
|
|
begin
|
|
|
|
raise Exception.Create(FmtSetupMessage1(msgErrorInternal2, Id));
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure InternalErrorFmt(const S: String; const Args: array of const);
|
|
|
|
begin
|
|
|
|
InternalError(Format(S, Args));
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure Win32ErrorMsgEx(const FunctionName: String; const ErrorCode: DWORD);
|
|
|
|
begin
|
|
|
|
raise Exception.Create(FmtSetupMessage(msgErrorFunctionFailedWithMessage,
|
|
|
|
[FunctionName, IntToStr(ErrorCode), Win32ErrorString(ErrorCode)]));
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure Win32ErrorMsg(const FunctionName: String);
|
|
|
|
begin
|
|
|
|
Win32ErrorMsgEx(FunctionName, GetLastError);
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure RaiseOleError(const FunctionName: String; const ResultCode: HRESULT);
|
|
|
|
begin
|
|
|
|
raise Exception.Create(FmtSetupMessage(msgErrorFunctionFailedWithMessage,
|
|
|
|
[FunctionName, IntToHexStr8(ResultCode), Win32ErrorString(ResultCode)]));
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure RaiseFunctionFailedError(const FunctionName: String);
|
|
|
|
begin
|
|
|
|
raise Exception.Create(FmtSetupMessage1(msgErrorFunctionFailedNoCode,
|
|
|
|
FunctionName));
|
|
|
|
end;
|
|
|
|
|
|
|
|
function GetRegRootKeyName(const RootKey: HKEY): String;
|
|
|
|
begin
|
|
|
|
case RootKey of
|
2018-07-09 20:29:29 +02:00
|
|
|
HKEY_AUTO: InternalError('GetRegRootKeyName called for HKEY_AUTO');
|
2011-10-06 20:53:09 +02:00
|
|
|
HKEY_CLASSES_ROOT: Result := 'HKEY_CLASSES_ROOT';
|
|
|
|
HKEY_CURRENT_USER: Result := 'HKEY_CURRENT_USER';
|
|
|
|
HKEY_LOCAL_MACHINE: Result := 'HKEY_LOCAL_MACHINE';
|
|
|
|
HKEY_USERS: Result := 'HKEY_USERS';
|
|
|
|
HKEY_PERFORMANCE_DATA: Result := 'HKEY_PERFORMANCE_DATA';
|
|
|
|
HKEY_CURRENT_CONFIG: Result := 'HKEY_CURRENT_CONFIG';
|
|
|
|
HKEY_DYN_DATA: Result := 'HKEY_DYN_DATA';
|
|
|
|
else
|
|
|
|
{ unknown - shouldn't get here }
|
|
|
|
Result := Format('[%x]', [Cardinal(RootKey)]);
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
2024-05-27 03:42:38 -05:00
|
|
|
function GenerateNonRandomUniqueTempDir(const LimitCurrentUserSidAccess: Boolean;
|
2024-05-25 20:37:59 +02:00
|
|
|
Path: String; var TempDir: String): Boolean;
|
2022-03-15 22:31:04 +01:00
|
|
|
{ Creates a new temporary directory with a non-random name. Returns True if an
|
2024-05-26 15:16:20 +02:00
|
|
|
existing directory was re-created. This is called by Uninstall. A non-random
|
|
|
|
name is used because the uninstaller EXE isn't able to delete itself; if it were
|
|
|
|
random, there would be one directory added each time an uninstaller is run. }
|
2011-10-06 20:53:09 +02:00
|
|
|
var
|
2022-04-12 18:55:56 +02:00
|
|
|
Rand, RandOrig: Longint; { These are actually NOT random in any way }
|
2022-03-15 22:31:04 +01:00
|
|
|
ErrorCode: DWORD;
|
2011-10-06 20:53:09 +02:00
|
|
|
begin
|
|
|
|
Path := AddBackslash(Path);
|
|
|
|
RandOrig := $123456;
|
|
|
|
Rand := RandOrig;
|
|
|
|
repeat
|
2022-03-15 22:31:04 +01:00
|
|
|
Result := False;
|
2011-10-06 20:53:09 +02:00
|
|
|
Inc(Rand);
|
|
|
|
if Rand > $1FFFFFF then Rand := 0;
|
|
|
|
if Rand = RandOrig then
|
|
|
|
{ practically impossible to go through 33 million possibilities,
|
|
|
|
but check "just in case"... }
|
|
|
|
raise Exception.Create(FmtSetupMessage1(msgErrorTooManyFilesInDir,
|
|
|
|
RemoveBackslashUnlessRoot(Path)));
|
2024-05-26 15:16:20 +02:00
|
|
|
{ Generate a "random" name }
|
2022-03-15 22:31:04 +01:00
|
|
|
TempDir := Path + 'iu-' + IntToBase32(Rand) + '.tmp';
|
|
|
|
if DirExists(TempDir) then begin
|
2022-04-07 10:26:11 +02:00
|
|
|
if not DeleteDirTree(TempDir) then Continue;
|
2022-03-15 22:31:04 +01:00
|
|
|
Result := True;
|
|
|
|
end else if NewFileExists(TempDir) then
|
2022-04-07 10:26:11 +02:00
|
|
|
if not DeleteFile(TempDir) then Continue;
|
2022-03-15 22:31:04 +01:00
|
|
|
|
2024-05-27 03:42:38 -05:00
|
|
|
if CreateSafeDirectory(LimitCurrentUserSidAccess, TempDir, ErrorCode) then Break;
|
2022-03-15 22:31:04 +01:00
|
|
|
if ErrorCode <> ERROR_ALREADY_EXISTS then
|
|
|
|
raise Exception.Create(FmtSetupMessage(msgLastErrorMessage,
|
|
|
|
[FmtSetupMessage1(msgErrorCreatingDir, TempDir), IntToStr(ErrorCode),
|
|
|
|
Win32ErrorString(ErrorCode)]));
|
|
|
|
until False; // continue until a new directory was created
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
|
|
|
|
|
|
|
function ReplaceSystemDirWithSysWow64(const Path: String): String;
|
|
|
|
{ If the user is running 64-bit Windows and Path begins with
|
|
|
|
'x:\windows\system32' it replaces it with 'x:\windows\syswow64', like the
|
|
|
|
file system redirector would do. Otherwise, Path is returned unchanged. }
|
|
|
|
var
|
|
|
|
SysWow64Dir, SysDir: String;
|
|
|
|
L: Integer;
|
|
|
|
begin
|
|
|
|
SysWow64Dir := GetSysWow64Dir;
|
|
|
|
if SysWow64Dir <> '' then begin
|
|
|
|
SysDir := GetSystemDir;
|
|
|
|
{ x:\windows\system32 -> x:\windows\syswow64
|
|
|
|
x:\windows\system32\ -> x:\windows\syswow64\
|
|
|
|
x:\windows\system32\filename -> x:\windows\syswow64\filename
|
|
|
|
x:\windows\system32x -> x:\windows\syswow64x <- yes, like Windows! }
|
|
|
|
L := Length(SysDir);
|
|
|
|
if (Length(Path) = L) or
|
|
|
|
((Length(Path) > L) and not PathCharIsTrailByte(Path, L+1)) then begin
|
|
|
|
{ ^ avoid splitting a double-byte character }
|
|
|
|
if PathCompare(Copy(Path, 1, L), SysDir) = 0 then begin
|
|
|
|
Result := SysWow64Dir + Copy(Path, L+1, Maxint);
|
|
|
|
Exit;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
Result := Path;
|
|
|
|
end;
|
|
|
|
|
2012-01-12 11:04:14 +01:00
|
|
|
function ReplaceSystemDirWithSysNative(Path: String; const IsWin64: Boolean): String;
|
2024-03-27 13:20:38 +01:00
|
|
|
{ If Path begins with 'x:\windows\system32\' it replaces it with
|
2012-01-12 11:04:14 +01:00
|
|
|
'x:\windows\sysnative\' and if Path equals 'x:\windows\system32'
|
|
|
|
it replaces it with 'x:\windows\sysnative'. Otherwise, Path is
|
|
|
|
returned unchanged. }
|
|
|
|
var
|
|
|
|
SysNativeDir, SysDir: String;
|
|
|
|
L: Integer;
|
|
|
|
begin
|
|
|
|
SysNativeDir := GetSysNativeDir(IsWin64);
|
|
|
|
if SysNativeDir <> '' then begin
|
|
|
|
SysDir := GetSystemDir;
|
|
|
|
if PathCompare(Path, SysDir) = 0 then begin
|
|
|
|
{ x:\windows\system32 -> x:\windows\sysnative }
|
|
|
|
Result := SysNativeDir;
|
|
|
|
Exit;
|
|
|
|
end else begin
|
|
|
|
{ x:\windows\system32\ -> x:\windows\sysnative\
|
|
|
|
x:\windows\system32\filename -> x:\windows\sysnative\filename }
|
|
|
|
SysDir := AddBackslash(SysDir);
|
|
|
|
L := Length(SysDir);
|
|
|
|
if (Length(Path) = L) or
|
|
|
|
((Length(Path) > L) and not PathCharIsTrailByte(Path, L+1)) then begin
|
|
|
|
{ ^ avoid splitting a double-byte character }
|
|
|
|
if PathCompare(Copy(Path, 1, L), SysDir) = 0 then begin
|
|
|
|
Result := SysNativeDir + Copy(Path, L, Maxint);
|
|
|
|
Exit;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
Result := Path;
|
|
|
|
end;
|
|
|
|
|
2025-05-20 13:31:46 +02:00
|
|
|
procedure RestartReplace(const DisableFsRedir: Boolean; TempFile, DestFile: String);
|
2011-10-06 20:53:09 +02:00
|
|
|
{ Renames TempFile to DestFile the next time Windows is started. If DestFile
|
|
|
|
already existed, it will be overwritten. If DestFile is '' then TempFile
|
2024-03-29 08:00:28 +01:00
|
|
|
will be deleted.. }
|
2011-10-06 20:53:09 +02:00
|
|
|
begin
|
|
|
|
TempFile := PathExpand(TempFile);
|
|
|
|
if DestFile <> '' then
|
|
|
|
DestFile := PathExpand(DestFile);
|
|
|
|
|
2024-03-29 08:00:28 +01:00
|
|
|
if not DisableFsRedir then begin
|
|
|
|
{ Work around WOW64 bug present in the IA64 and x64 editions of Windows
|
|
|
|
XP (3790) and Server 2003 prior to SP1 RC2: MoveFileEx writes filenames
|
|
|
|
to the registry verbatim without mapping system32->syswow64. }
|
|
|
|
TempFile := ReplaceSystemDirWithSysWow64(TempFile);
|
|
|
|
if DestFile <> '' then
|
|
|
|
DestFile := ReplaceSystemDirWithSysWow64(DestFile);
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
2024-03-29 08:00:28 +01:00
|
|
|
if not MoveFileExRedir(DisableFsRedir, TempFile, DestFile,
|
|
|
|
MOVEFILE_DELAY_UNTIL_REBOOT or MOVEFILE_REPLACE_EXISTING) then
|
|
|
|
Win32ErrorMsg('MoveFileEx');
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
|
|
|
|
2025-05-20 13:31:46 +02:00
|
|
|
function DelTree(const DisableFsRedir: Boolean; const Path: String;
|
2012-01-16 17:32:11 +01:00
|
|
|
const IsDir, DeleteFiles, DeleteSubdirsAlso, BreakOnError: Boolean;
|
2011-10-06 20:53:09 +02:00
|
|
|
const DeleteDirProc: TDeleteDirProc; const DeleteFileProc: TDeleteFileProc;
|
|
|
|
const Param: Pointer): Boolean;
|
|
|
|
{ Deletes the specified directory including all files and subdirectories in
|
|
|
|
it (including those with hidden, system, and read-only attributes). Returns
|
2012-01-16 17:32:11 +01:00
|
|
|
True if it was able to successfully remove everything. If BreakOnError is
|
|
|
|
set to True it will stop and return False the first time a delete failed or
|
|
|
|
DeleteDirProc/DeleteFileProc returned False. }
|
2011-10-06 20:53:09 +02:00
|
|
|
var
|
|
|
|
BasePath, FindSpec: String;
|
|
|
|
H: THandle;
|
|
|
|
FindData: TWin32FindData;
|
|
|
|
S: String;
|
|
|
|
begin
|
|
|
|
Result := True;
|
|
|
|
if DeleteFiles and
|
|
|
|
(not IsDir or IsDirectoryAndNotReparsePointRedir(DisableFsRedir, Path)) then begin
|
|
|
|
if IsDir then begin
|
|
|
|
BasePath := AddBackslash(Path);
|
|
|
|
FindSpec := BasePath + '*';
|
|
|
|
end
|
|
|
|
else begin
|
|
|
|
BasePath := PathExtractPath(Path);
|
|
|
|
FindSpec := Path;
|
|
|
|
end;
|
|
|
|
H := FindFirstFileRedir(DisableFsRedir, FindSpec, FindData);
|
|
|
|
if H <> INVALID_HANDLE_VALUE then begin
|
|
|
|
try
|
|
|
|
repeat
|
|
|
|
S := FindData.cFileName;
|
|
|
|
if (S <> '.') and (S <> '..') then begin
|
|
|
|
if FindData.dwFileAttributes and FILE_ATTRIBUTE_READONLY <> 0 then begin
|
|
|
|
{ Strip the read-only attribute if this is a file, or if it's a
|
|
|
|
directory and we're deleting subdirectories also }
|
|
|
|
if (FindData.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY = 0) or DeleteSubdirsAlso then
|
|
|
|
SetFileAttributesRedir(DisableFsRedir, BasePath + S,
|
|
|
|
FindData.dwFileAttributes and not FILE_ATTRIBUTE_READONLY);
|
|
|
|
end;
|
|
|
|
if FindData.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY = 0 then begin
|
|
|
|
if Assigned(DeleteFileProc) then begin
|
|
|
|
if not DeleteFileProc(DisableFsRedir, BasePath + S, Param) then
|
|
|
|
Result := False;
|
|
|
|
end
|
|
|
|
else begin
|
|
|
|
if not DeleteFileRedir(DisableFsRedir, BasePath + S) then
|
|
|
|
Result := False;
|
|
|
|
end;
|
|
|
|
end
|
|
|
|
else begin
|
|
|
|
if DeleteSubdirsAlso then
|
2025-05-20 13:31:46 +02:00
|
|
|
if not DelTree(DisableFsRedir, BasePath + S, True, True, True, BreakOnError,
|
2011-10-06 20:53:09 +02:00
|
|
|
DeleteDirProc, DeleteFileProc, Param) then
|
|
|
|
Result := False;
|
|
|
|
end;
|
|
|
|
end;
|
2012-01-16 17:32:11 +01:00
|
|
|
until (BreakOnError and not Result) or not FindNextFile(H, FindData);
|
2011-10-06 20:53:09 +02:00
|
|
|
finally
|
|
|
|
Windows.FindClose(H);
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
end;
|
2012-01-16 17:32:11 +01:00
|
|
|
if (not BreakOnError or Result) and IsDir then begin
|
2011-10-06 20:53:09 +02:00
|
|
|
if Assigned(DeleteDirProc) then begin
|
|
|
|
if not DeleteDirProc(DisableFsRedir, Path, Param) then
|
|
|
|
Result := False;
|
|
|
|
end
|
|
|
|
else begin
|
|
|
|
if not RemoveDirectoryRedir(DisableFsRedir, Path) then
|
|
|
|
Result := False;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
2025-05-20 13:31:46 +02:00
|
|
|
function IsDirEmpty(const DisableFsRedir: Boolean; const Dir: String): Boolean;
|
2011-10-06 20:53:09 +02:00
|
|
|
{ Returns True if Dir contains no files or subdirectories.
|
|
|
|
Note: If Dir does not exist or lacks list permission, False will be
|
|
|
|
returned. }
|
|
|
|
var
|
|
|
|
H: THandle;
|
|
|
|
FindData: TWin32FindData;
|
|
|
|
begin
|
|
|
|
H := FindFirstFileRedir(DisableFsRedir, AddBackslash(Dir) + '*', FindData);
|
|
|
|
if H <> INVALID_HANDLE_VALUE then begin
|
|
|
|
try
|
|
|
|
Result := True;
|
|
|
|
while True do begin
|
|
|
|
if FindData.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY = 0 then begin
|
|
|
|
{ Found a file }
|
|
|
|
Result := False;
|
|
|
|
Break;
|
|
|
|
end;
|
|
|
|
if (StrComp(FindData.cFileName, '.') <> 0) and
|
|
|
|
(StrComp(FindData.cFileName, '..') <> 0) then begin
|
|
|
|
{ Found a subdirectory }
|
|
|
|
Result := False;
|
|
|
|
Break;
|
|
|
|
end;
|
|
|
|
if not FindNextFile(H, FindData) then begin
|
|
|
|
if GetLastError <> ERROR_NO_MORE_FILES then begin
|
|
|
|
{ Exited the loop early due to some unexpected error. The directory
|
|
|
|
might not be empty, so return False }
|
|
|
|
Result := False;
|
|
|
|
end;
|
|
|
|
Break;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
finally
|
|
|
|
Windows.FindClose(H);
|
|
|
|
end;
|
|
|
|
end
|
|
|
|
else begin
|
|
|
|
{ The directory may not exist, or it may lack list permission }
|
|
|
|
Result := False;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure IncrementSharedCount(const RegView: TRegView; const Filename: String;
|
|
|
|
const AlreadyExisted: Boolean);
|
|
|
|
const
|
2024-03-29 08:00:28 +01:00
|
|
|
SharedDLLsKey = REGSTR_PATH_SETUP + '\SharedDLLs'; {don't localize}
|
2011-10-06 20:53:09 +02:00
|
|
|
var
|
|
|
|
ErrorCode: Longint;
|
|
|
|
K: HKEY;
|
|
|
|
Disp, Size, Count, CurType, NewType: DWORD;
|
|
|
|
CountStr: String;
|
|
|
|
FilenameP: PChar;
|
|
|
|
begin
|
|
|
|
ErrorCode := RegCreateKeyExView(RegView, HKEY_LOCAL_MACHINE, SharedDLLsKey, 0, nil,
|
|
|
|
REG_OPTION_NON_VOLATILE, KEY_QUERY_VALUE or KEY_SET_VALUE, nil, K, @Disp);
|
|
|
|
if ErrorCode <> ERROR_SUCCESS then
|
|
|
|
raise Exception.Create(FmtSetupMessage(msgErrorRegOpenKey,
|
|
|
|
[GetRegRootKeyName(HKEY_LOCAL_MACHINE), SharedDLLsKey]) + SNewLine2 +
|
|
|
|
FmtSetupMessage(msgErrorFunctionFailedWithMessage,
|
|
|
|
['RegCreateKeyEx', IntToStr(ErrorCode), Win32ErrorString(ErrorCode)]));
|
|
|
|
FilenameP := PChar(Filename);
|
|
|
|
Count := 0;
|
|
|
|
NewType := REG_DWORD;
|
|
|
|
try
|
|
|
|
if RegQueryValueEx(K, FilenameP, nil, @CurType, nil, @Size) = ERROR_SUCCESS then
|
|
|
|
case CurType of
|
|
|
|
REG_SZ:
|
|
|
|
if RegQueryStringValue(K, FilenameP, CountStr) then begin
|
|
|
|
Count := StrToInt(CountStr);
|
|
|
|
NewType := REG_SZ;
|
|
|
|
end;
|
|
|
|
REG_BINARY: begin
|
|
|
|
if (Size >= 1) and (Size <= 4) then begin
|
|
|
|
if RegQueryValueEx(K, FilenameP, nil, nil, @Count, @Size) <> ERROR_SUCCESS then
|
|
|
|
{ ^ relies on the high 3 bytes of Count being initialized to 0 }
|
|
|
|
Abort;
|
|
|
|
NewType := REG_BINARY;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
REG_DWORD: begin
|
|
|
|
Size := SizeOf(DWORD);
|
|
|
|
if RegQueryValueEx(K, FilenameP, nil, nil, @Count, @Size) <> ERROR_SUCCESS then
|
|
|
|
Abort;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
except
|
|
|
|
Count := 0;
|
|
|
|
end;
|
|
|
|
if Integer(Count) < 0 then Count := 0; { just in case... }
|
|
|
|
if (Count = 0) and AlreadyExisted then
|
|
|
|
Inc(Count);
|
|
|
|
Inc(Count);
|
|
|
|
case NewType of
|
|
|
|
REG_SZ: begin
|
|
|
|
CountStr := IntToStr(Count);
|
|
|
|
RegSetValueEx(K, FilenameP, 0, NewType, PChar(CountStr), (Length(CountStr)+1)*SizeOf(CountStr[1]));
|
|
|
|
end;
|
|
|
|
REG_BINARY, REG_DWORD:
|
|
|
|
RegSetValueEx(K, FilenameP, 0, NewType, @Count, SizeOf(Count));
|
|
|
|
end;
|
|
|
|
RegCloseKey(K);
|
|
|
|
end;
|
|
|
|
|
|
|
|
function DecrementSharedCount(const RegView: TRegView;
|
|
|
|
const Filename: String): Boolean;
|
|
|
|
{ Attempts to decrement the shared file reference count of Filename. Returns
|
|
|
|
True if the count reached zero (meaning it's OK to delete the file). }
|
|
|
|
const
|
2024-03-29 08:00:28 +01:00
|
|
|
SharedDLLsKey = REGSTR_PATH_SETUP + '\SharedDLLs'; {don't localize}
|
2011-10-06 20:53:09 +02:00
|
|
|
var
|
|
|
|
ErrorCode: Longint;
|
|
|
|
K: HKEY;
|
|
|
|
CountRead: Boolean;
|
|
|
|
Count, CurType, Size: DWORD;
|
|
|
|
CountStr: String;
|
|
|
|
begin
|
|
|
|
Result := False;
|
|
|
|
|
|
|
|
ErrorCode := RegOpenKeyExView(RegView, HKEY_LOCAL_MACHINE, SharedDLLsKey, 0,
|
|
|
|
KEY_QUERY_VALUE or KEY_SET_VALUE, K);
|
|
|
|
if ErrorCode = ERROR_FILE_NOT_FOUND then
|
|
|
|
Exit;
|
|
|
|
if ErrorCode <> ERROR_SUCCESS then
|
|
|
|
raise Exception.Create(FmtSetupMessage(msgErrorRegOpenKey,
|
|
|
|
[GetRegRootKeyName(HKEY_LOCAL_MACHINE), SharedDLLsKey]) + SNewLine2 +
|
|
|
|
FmtSetupMessage(msgErrorFunctionFailedWithMessage,
|
|
|
|
['RegOpenKeyEx', IntToStr(ErrorCode), Win32ErrorString(ErrorCode)]));
|
|
|
|
try
|
|
|
|
if RegQueryValueEx(K, PChar(Filename), nil, @CurType, nil, @Size) <> ERROR_SUCCESS then
|
|
|
|
Exit;
|
|
|
|
|
|
|
|
CountRead := False;
|
|
|
|
Count := 0;
|
|
|
|
try
|
|
|
|
case CurType of
|
|
|
|
REG_SZ:
|
|
|
|
if RegQueryStringValue(K, PChar(Filename), CountStr) then begin
|
|
|
|
Count := StrToInt(CountStr);
|
|
|
|
CountRead := True;
|
|
|
|
end;
|
|
|
|
REG_BINARY: begin
|
|
|
|
if (Size >= 1) and (Size <= 4) then begin
|
|
|
|
if RegQueryValueEx(K, PChar(Filename), nil, nil, @Count, @Size) = ERROR_SUCCESS then
|
|
|
|
{ ^ relies on the high 3 bytes of Count being initialized to 0 }
|
|
|
|
CountRead := True;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
REG_DWORD: begin
|
|
|
|
Size := SizeOf(DWORD);
|
|
|
|
if RegQueryValueEx(K, PChar(Filename), nil, nil, @Count, @Size) = ERROR_SUCCESS then
|
|
|
|
CountRead := True;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
except
|
2020-08-10 21:00:18 +02:00
|
|
|
{ don't propagate exceptions (e.g. from StrToInt) }
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
|
|
|
{ If we failed to read the count, or it's in some type we don't recognize,
|
|
|
|
don't touch it }
|
|
|
|
if not CountRead then
|
|
|
|
Exit;
|
|
|
|
|
|
|
|
Dec(Count);
|
|
|
|
if Integer(Count) <= 0 then begin
|
|
|
|
Result := True;
|
|
|
|
RegDeleteValue(K, PChar(Filename));
|
|
|
|
end
|
|
|
|
else begin
|
|
|
|
case CurType of
|
|
|
|
REG_SZ: begin
|
|
|
|
CountStr := IntToStr(Count);
|
|
|
|
RegSetValueEx(K, PChar(Filename), 0, REG_SZ, PChar(CountStr), (Length(CountStr)+1)*SizeOf(Char));
|
|
|
|
end;
|
|
|
|
REG_BINARY, REG_DWORD:
|
|
|
|
RegSetValueEx(K, PChar(Filename), 0, CurType, @Count, SizeOf(Count));
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
finally
|
|
|
|
RegCloseKey(K);
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
2025-05-20 13:31:46 +02:00
|
|
|
function GetFileDateTime(const DisableFsRedir: Boolean; const Filename: String;
|
2011-10-06 20:53:09 +02:00
|
|
|
var DateTime: TFileTime): Boolean;
|
|
|
|
var
|
|
|
|
Handle: THandle;
|
|
|
|
FindData: TWin32FindData;
|
|
|
|
begin
|
|
|
|
Handle := FindFirstFileRedir(DisableFsRedir, Filename, FindData);
|
|
|
|
if Handle <> INVALID_HANDLE_VALUE then begin
|
|
|
|
Windows.FindClose(Handle);
|
|
|
|
if FindData.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY = 0 then begin
|
|
|
|
DateTime := FindData.ftLastWriteTime;
|
|
|
|
Result := True;
|
|
|
|
Exit;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
Result := False;
|
|
|
|
DateTime.dwLowDateTime := 0;
|
|
|
|
DateTime.dwHighDateTime := 0;
|
|
|
|
end;
|
|
|
|
|
2025-05-20 13:31:46 +02:00
|
|
|
function GetSHA256OfFile(const DisableFsRedir: Boolean; const Filename: String): TSHA256Digest;
|
2024-09-26 11:38:41 +02:00
|
|
|
{ Gets SHA-256 sum as a string of the file Filename. An exception will be raised upon
|
2011-10-06 20:53:09 +02:00
|
|
|
failure. }
|
2025-06-06 18:06:07 +02:00
|
|
|
begin
|
|
|
|
const F = TFileRedir.Create(DisableFsRedir, Filename, fdOpenExisting, faRead, fsReadWrite);
|
|
|
|
try
|
|
|
|
Result := GetSHA256OfFile(F);
|
|
|
|
finally
|
|
|
|
F.Free;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
|
|
|
function GetSHA256OfFile(const F: TFile): TSHA256Digest;
|
|
|
|
{ Gets SHA-256 sum as a string of the file F. An exception will be raised upon
|
|
|
|
failure. }
|
2011-10-06 20:53:09 +02:00
|
|
|
var
|
|
|
|
Buf: array[0..65535] of Byte;
|
|
|
|
begin
|
2025-06-08 13:11:24 +02:00
|
|
|
F.Seek(0);
|
2024-09-26 11:38:41 +02:00
|
|
|
var Context: TSHA256Context;
|
|
|
|
SHA256Init(Context);
|
2025-06-06 18:06:07 +02:00
|
|
|
while True do begin
|
|
|
|
var NumRead := F.Read(Buf, SizeOf(Buf));
|
|
|
|
if NumRead = 0 then
|
|
|
|
Break;
|
|
|
|
SHA256Update(Context, Buf, NumRead);
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
2024-09-26 11:38:41 +02:00
|
|
|
Result := SHA256Final(Context);
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
|
|
|
|
2024-09-26 11:38:41 +02:00
|
|
|
function GetSHA256OfAnsiString(const S: AnsiString): TSHA256Digest;
|
2020-07-07 18:44:31 +02:00
|
|
|
begin
|
2024-09-26 11:38:41 +02:00
|
|
|
Result := SHA256Buf(Pointer(S)^, Length(S)*SizeOf(S[1]));
|
2020-07-07 18:44:31 +02:00
|
|
|
end;
|
|
|
|
|
2024-09-26 11:38:41 +02:00
|
|
|
function GetSHA256OfUnicodeString(const S: UnicodeString): TSHA256Digest;
|
2011-10-06 20:53:09 +02:00
|
|
|
begin
|
2024-09-26 11:38:41 +02:00
|
|
|
Result := SHA256Buf(Pointer(S)^, Length(S)*SizeOf(S[1]));
|
2020-07-07 18:44:31 +02:00
|
|
|
end;
|
|
|
|
|
2011-10-06 20:53:09 +02:00
|
|
|
var
|
|
|
|
SFCInitialized: Boolean;
|
|
|
|
SfcIsFileProtectedFunc: function(RpcHandle: THandle; ProtFileName: PWideChar): BOOL; stdcall;
|
|
|
|
|
2025-05-20 13:31:46 +02:00
|
|
|
function IsProtectedSystemFile(const DisableFsRedir: Boolean;
|
2011-10-06 20:53:09 +02:00
|
|
|
const Filename: String): Boolean;
|
|
|
|
{ Returns True if the specified file is protected by Windows File Protection
|
|
|
|
(and therefore can't be replaced). }
|
|
|
|
var
|
|
|
|
M: HMODULE;
|
|
|
|
FN: String;
|
|
|
|
begin
|
|
|
|
if not SFCInitialized then begin
|
|
|
|
M := SafeLoadLibrary(PChar(AddBackslash(GetSystemDir) + 'sfc.dll'),
|
|
|
|
SEM_NOOPENFILEERRORBOX);
|
|
|
|
if M <> 0 then
|
|
|
|
SfcIsFileProtectedFunc := GetProcAddress(M, 'SfcIsFileProtected');
|
|
|
|
SFCInitialized := True;
|
|
|
|
end;
|
|
|
|
if Assigned(SfcIsFileProtectedFunc) then begin
|
|
|
|
{ The function only accepts fully qualified paths. Also, as of
|
|
|
|
IA-64 2003 SP1 and x64 XP, it does not respect file system redirection,
|
|
|
|
so a call to ReplaceSystemDirWithSysWow64 is needed. }
|
|
|
|
FN := PathExpand(Filename);
|
|
|
|
if not DisableFsRedir then
|
|
|
|
FN := ReplaceSystemDirWithSysWow64(FN);
|
|
|
|
Result := SfcIsFileProtectedFunc(0, PChar(FN));
|
|
|
|
end
|
2024-03-29 08:00:28 +01:00
|
|
|
else
|
|
|
|
Result := False; { Should never happen }
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
|
|
|
|
2024-05-05 15:50:13 +02:00
|
|
|
procedure HandleProcessWait(ProcessHandle: THandle; const Wait: TExecWait;
|
|
|
|
const ProcessMessagesProc: TProcedure; const OutputReader: TCreateProcessOutputReader;
|
|
|
|
var ResultCode: Integer);
|
2011-10-06 20:53:09 +02:00
|
|
|
begin
|
|
|
|
try
|
|
|
|
if Wait = ewWaitUntilIdle then begin
|
|
|
|
repeat
|
|
|
|
ProcessMessagesProc;
|
|
|
|
until WaitForInputIdle(ProcessHandle, 50) <> WAIT_TIMEOUT;
|
|
|
|
end;
|
|
|
|
if Wait = ewWaitUntilTerminated then begin
|
|
|
|
{ Wait until the process returns, but still process any messages that
|
2024-05-05 10:23:10 +02:00
|
|
|
arrive and read the output if requested. }
|
2024-05-08 19:53:51 +02:00
|
|
|
var WaitMilliseconds := IfThen(OutputReader <> nil, 50, INFINITE);
|
2024-05-05 10:23:10 +02:00
|
|
|
var WaitResult: DWORD := 0;
|
2011-10-06 20:53:09 +02:00
|
|
|
repeat
|
|
|
|
{ Process any pending messages first because MsgWaitForMultipleObjects
|
2024-05-05 10:23:10 +02:00
|
|
|
(called below) only returns when *new* messages arrive, unless there's
|
|
|
|
a timeout }
|
|
|
|
if WaitResult <> WAIT_TIMEOUT then
|
|
|
|
ProcessMessagesProc;
|
2024-05-05 15:50:13 +02:00
|
|
|
if OutputReader <> nil then
|
|
|
|
OutputReader.Read(False);
|
2024-05-05 10:23:10 +02:00
|
|
|
WaitResult := MsgWaitForMultipleObjects(1, ProcessHandle, False,
|
|
|
|
WaitMilliseconds, QS_ALLINPUT);
|
|
|
|
until (WaitResult <> WAIT_OBJECT_0+1) and (WaitResult <> WAIT_TIMEOUT);
|
2011-10-06 20:53:09 +02:00
|
|
|
{ Process messages once more in case MsgWaitForMultipleObjects saw the
|
|
|
|
process terminate and new messages arrive simultaneously. (Can't leave
|
|
|
|
unprocessed messages waiting, or a subsequent call to WaitMessage
|
|
|
|
won't see them.) }
|
|
|
|
ProcessMessagesProc;
|
2024-05-05 15:50:13 +02:00
|
|
|
if OutputReader <> nil then
|
|
|
|
OutputReader.Read(True);
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
|
|
|
{ Get the exit code. Will be set to STILL_ACTIVE if not yet available }
|
|
|
|
if not GetExitCodeProcess(ProcessHandle, DWORD(ResultCode)) then
|
|
|
|
ResultCode := -1; { just in case }
|
|
|
|
finally
|
|
|
|
CloseHandle(ProcessHandle);
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
2025-05-20 13:31:46 +02:00
|
|
|
function InstExec(const DisableFsRedir: Boolean; const Filename, Params: String;
|
2011-10-06 20:53:09 +02:00
|
|
|
WorkingDir: String; const Wait: TExecWait; const ShowCmd: Integer;
|
2024-07-02 16:02:59 +01:00
|
|
|
const ProcessMessagesProc: TProcedure; const OutputReader: TCreateProcessOutputReader;
|
2024-06-27 11:35:51 +01:00
|
|
|
var ResultCode: Integer): Boolean;
|
2011-10-06 20:53:09 +02:00
|
|
|
var
|
|
|
|
CmdLine: String;
|
|
|
|
StartupInfo: TStartupInfo;
|
|
|
|
ProcessInfo: TProcessInformation;
|
|
|
|
begin
|
2024-05-10 11:45:14 +02:00
|
|
|
{Also see IsppFuncs' Exec }
|
2024-05-10 10:05:26 +02:00
|
|
|
|
2011-10-06 20:53:09 +02:00
|
|
|
if Filename = '>' then
|
|
|
|
CmdLine := Params
|
|
|
|
else begin
|
|
|
|
CmdLine := '"' + Filename + '"';
|
|
|
|
if Params <> '' then
|
|
|
|
CmdLine := CmdLine + ' ' + Params;
|
2024-04-29 19:33:37 +02:00
|
|
|
if SameText(PathExtractExt(Filename), '.bat') or
|
|
|
|
SameText(PathExtractExt(Filename), '.cmd') then begin
|
2011-10-06 20:53:09 +02:00
|
|
|
{ Use our own handling for .bat and .cmd files since passing them straight
|
|
|
|
to CreateProcess on Windows NT 4.0 has problems: it doesn't properly
|
|
|
|
quote the command line it passes to cmd.exe. This didn't work before:
|
|
|
|
Filename: "c:\batch.bat"; Parameters: """abc"""
|
|
|
|
And other Windows versions might have unknown quirks too, since
|
|
|
|
CreateProcess isn't documented to accept .bat files in the first place. }
|
2024-03-29 08:00:28 +01:00
|
|
|
{ With cmd.exe, the whole command line must be quoted for quoted
|
|
|
|
parameters to work. For example, this fails:
|
|
|
|
cmd.exe /c "z:\blah.bat" "test"
|
|
|
|
But this works:
|
|
|
|
cmd.exe /c ""z:\blah.bat" "test""
|
|
|
|
}
|
|
|
|
CmdLine := '"' + AddBackslash(GetSystemDir) + 'cmd.exe" /C "' + CmdLine + '"'
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
|
|
|
if WorkingDir = '' then
|
|
|
|
WorkingDir := PathExtractDir(Filename);
|
|
|
|
end;
|
|
|
|
FillChar(StartupInfo, SizeOf(StartupInfo), 0);
|
|
|
|
StartupInfo.cb := SizeOf(StartupInfo);
|
|
|
|
StartupInfo.dwFlags := STARTF_USESHOWWINDOW;
|
|
|
|
StartupInfo.wShowWindow := ShowCmd;
|
|
|
|
if WorkingDir = '' then
|
|
|
|
WorkingDir := GetSystemDir;
|
|
|
|
|
2024-07-02 16:02:59 +01:00
|
|
|
var InheritHandles := False;
|
|
|
|
var dwCreationFlags: DWORD := CREATE_DEFAULT_ERROR_MODE;
|
2024-05-05 10:23:10 +02:00
|
|
|
|
2024-07-02 16:02:59 +01:00
|
|
|
if (OutputReader <> nil) and (Wait = ewWaitUntilTerminated) then begin
|
|
|
|
OutputReader.UpdateStartupInfo(StartupInfo);
|
|
|
|
InheritHandles := True;
|
|
|
|
dwCreationFlags := dwCreationFlags or CREATE_NO_WINDOW;
|
|
|
|
end;
|
2024-05-05 10:23:10 +02:00
|
|
|
|
2024-07-02 16:02:59 +01:00
|
|
|
Result := CreateProcessRedir(DisableFsRedir, nil, PChar(CmdLine), nil, nil,
|
|
|
|
InheritHandles, dwCreationFlags, nil, PChar(WorkingDir),
|
|
|
|
StartupInfo, ProcessInfo);
|
|
|
|
if not Result then begin
|
|
|
|
ResultCode := GetLastError;
|
|
|
|
Exit;
|
2024-05-05 10:23:10 +02:00
|
|
|
end;
|
2024-07-02 16:02:59 +01:00
|
|
|
|
|
|
|
{ Don't need the thread handle, so close it now }
|
|
|
|
CloseHandle(ProcessInfo.hThread);
|
|
|
|
if OutputReader <> nil then
|
|
|
|
OutputReader.NotifyCreateProcessDone;
|
|
|
|
HandleProcessWait(ProcessInfo.hProcess, Wait, ProcessMessagesProc,
|
|
|
|
OutputReader, ResultCode);
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
|
|
|
|
|
|
|
function InstShellExec(const Verb, Filename, Params: String; WorkingDir: String;
|
|
|
|
const Wait: TExecWait; const ShowCmd: Integer;
|
|
|
|
const ProcessMessagesProc: TProcedure; var ResultCode: Integer): Boolean;
|
|
|
|
var
|
|
|
|
Info: TShellExecuteInfo;
|
|
|
|
begin
|
|
|
|
if WorkingDir = '' then begin
|
|
|
|
WorkingDir := PathExtractDir(Filename);
|
|
|
|
if WorkingDir = '' then
|
|
|
|
WorkingDir := GetSystemDir;
|
|
|
|
end;
|
|
|
|
|
|
|
|
FillChar(Info, SizeOf(Info), 0);
|
|
|
|
Info.cbSize := SizeOf(Info);
|
|
|
|
Info.fMask := SEE_MASK_FLAG_NO_UI or SEE_MASK_FLAG_DDEWAIT or
|
|
|
|
SEE_MASK_NOCLOSEPROCESS;
|
|
|
|
if Verb <> '' then
|
|
|
|
Info.lpVerb := PChar(Verb);
|
|
|
|
Info.lpFile := PChar(Filename);
|
|
|
|
Info.lpParameters := PChar(Params);
|
|
|
|
Info.lpDirectory := PChar(WorkingDir);
|
|
|
|
Info.nShow := ShowCmd;
|
|
|
|
Result := ShellExecuteEx(@Info);
|
|
|
|
if not Result then begin
|
|
|
|
ResultCode := GetLastError;
|
|
|
|
Exit;
|
|
|
|
end;
|
|
|
|
|
|
|
|
ResultCode := STILL_ACTIVE;
|
|
|
|
{ A process handle won't always be returned, e.g. if DDE was used }
|
|
|
|
if Info.hProcess <> 0 then
|
2024-05-05 15:50:13 +02:00
|
|
|
HandleProcessWait(Info.hProcess, Wait, ProcessMessagesProc, nil, ResultCode);
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
|
|
|
|
2015-04-29 13:28:09 +02:00
|
|
|
function CheckForOrCreateMutexes(Mutexes: String; const Create: Boolean): Boolean;
|
2011-10-06 20:53:09 +02:00
|
|
|
|
|
|
|
function MutexPos(const S: String): Integer;
|
|
|
|
begin
|
2024-05-05 14:23:40 +02:00
|
|
|
for var I := 1 to Length(S) do
|
|
|
|
if (S[I] = ',') and ((I = 1) or (S[I-1] <> '\')) then
|
|
|
|
Exit(I);
|
2011-10-06 20:53:09 +02:00
|
|
|
Result := 0;
|
|
|
|
end;
|
|
|
|
|
|
|
|
{ Returns True if any of the mutexes in the comma-separated Mutexes string
|
2015-04-29 13:28:09 +02:00
|
|
|
exist and Create is False }
|
2011-10-06 20:53:09 +02:00
|
|
|
var
|
|
|
|
I: Integer;
|
|
|
|
M: String;
|
|
|
|
H: THandle;
|
|
|
|
begin
|
|
|
|
Result := False;
|
|
|
|
repeat
|
|
|
|
I := MutexPos(Mutexes);
|
|
|
|
if I = 0 then I := Maxint;
|
|
|
|
M := Trim(Copy(Mutexes, 1, I-1));
|
|
|
|
if M <> '' then begin
|
|
|
|
StringChange(M, '\,', ',');
|
2015-04-29 13:28:09 +02:00
|
|
|
if Create then begin
|
|
|
|
CreateMutex(M)
|
|
|
|
end else begin
|
|
|
|
H := OpenMutex(SYNCHRONIZE, False, PChar(M));
|
|
|
|
if H <> 0 then begin
|
|
|
|
CloseHandle(H);
|
|
|
|
Result := True;
|
|
|
|
Break;
|
|
|
|
end;
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
|
|
|
end;
|
|
|
|
Delete(Mutexes, 1, I);
|
|
|
|
until Mutexes = '';
|
|
|
|
end;
|
|
|
|
|
2015-04-29 13:28:09 +02:00
|
|
|
function CheckForMutexes(const Mutexes: String): Boolean;
|
|
|
|
begin
|
|
|
|
Result := CheckForOrCreateMutexes(Mutexes, False);
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure CreateMutexes(const Mutexes: String);
|
|
|
|
begin
|
|
|
|
CheckForOrCreateMutexes(Mutexes, True);
|
|
|
|
end;
|
|
|
|
|
2011-10-06 20:53:09 +02:00
|
|
|
function ModifyPifFile(const Filename: String; const CloseOnExit: Boolean): Boolean;
|
|
|
|
{ Changes the "Close on exit" setting of a .pif file. Returns True if it was
|
|
|
|
able to make the change. }
|
|
|
|
var
|
|
|
|
F: TFile;
|
|
|
|
B: Byte;
|
|
|
|
begin
|
|
|
|
{ Note: Specs on the .pif format were taken from
|
|
|
|
http://smsoft.chat.ru/en/pifdoc.htm }
|
|
|
|
Result := False;
|
|
|
|
F := TFile.Create(Filename, fdOpenExisting, faReadWrite, fsNone);
|
|
|
|
try
|
|
|
|
{ Is it a valid .pif file? }
|
|
|
|
if F.Size.Lo >= $171 then begin
|
|
|
|
F.Seek($63);
|
|
|
|
F.ReadBuffer(B, SizeOf(B));
|
|
|
|
{ Toggle the "Close on exit" bit }
|
|
|
|
if (B and $10 <> 0) <> CloseOnExit then begin
|
|
|
|
B := B xor $10;
|
|
|
|
F.Seek($63);
|
|
|
|
F.WriteBuffer(B, SizeOf(B));
|
|
|
|
end;
|
|
|
|
Result := True;
|
|
|
|
end;
|
|
|
|
finally
|
|
|
|
F.Free;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
|
|
|
function GetComputerNameString: String;
|
|
|
|
var
|
|
|
|
Buf: array[0..MAX_COMPUTERNAME_LENGTH] of Char;
|
|
|
|
Size: DWORD;
|
|
|
|
begin
|
|
|
|
Size := SizeOf(Buf) div SizeOf(Buf[0]);
|
|
|
|
if GetComputerName(Buf, Size) then
|
|
|
|
Result := Buf
|
|
|
|
else
|
|
|
|
Result := '';
|
|
|
|
end;
|
|
|
|
|
|
|
|
function GetUserNameString: String;
|
|
|
|
var
|
|
|
|
Buf: array[0..256] of Char; { 256 = UNLEN }
|
|
|
|
BufSize: DWORD;
|
|
|
|
begin
|
|
|
|
BufSize := SizeOf(Buf) div SizeOf(Buf[0]);
|
|
|
|
if GetUserName(Buf, BufSize) then
|
|
|
|
Result := Buf
|
|
|
|
else
|
|
|
|
Result := '';
|
|
|
|
end;
|
|
|
|
|
2024-09-26 11:38:41 +02:00
|
|
|
function MakePendingFileRenameOperationsChecksum: TSHA256Digest;
|
2011-10-06 20:53:09 +02:00
|
|
|
{ Calculates a checksum of the current PendingFileRenameOperations registry
|
2024-03-29 08:00:28 +01:00
|
|
|
value The caller can use this checksum to determine if
|
|
|
|
PendingFileRenameOperations was changed (perhaps by another program). }
|
2011-10-06 20:53:09 +02:00
|
|
|
var
|
2024-09-26 11:38:41 +02:00
|
|
|
Context: TSHA256Context;
|
2011-10-06 20:53:09 +02:00
|
|
|
K: HKEY;
|
|
|
|
S: String;
|
|
|
|
begin
|
2024-09-26 11:38:41 +02:00
|
|
|
SHA256Init(Context);
|
2011-10-06 20:53:09 +02:00
|
|
|
try
|
2024-03-29 08:00:28 +01:00
|
|
|
if RegOpenKeyExView(rvDefault, HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager',
|
|
|
|
0, KEY_QUERY_VALUE, K) = ERROR_SUCCESS then begin
|
|
|
|
if RegQueryMultiStringValue(K, 'PendingFileRenameOperations', S) then
|
2024-09-26 11:38:41 +02:00
|
|
|
SHA256Update(Context, S[1], Length(S)*SizeOf(S[1]));
|
2024-03-29 08:00:28 +01:00
|
|
|
{ When "PendingFileRenameOperations" is full, it spills over into
|
|
|
|
"PendingFileRenameOperations2" }
|
|
|
|
if RegQueryMultiStringValue(K, 'PendingFileRenameOperations2', S) then
|
2024-09-26 11:38:41 +02:00
|
|
|
SHA256Update(Context, S[1], Length(S)*SizeOf(S[1]));
|
2024-03-29 08:00:28 +01:00
|
|
|
RegCloseKey(K);
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
|
|
|
except
|
2020-08-10 21:00:18 +02:00
|
|
|
{ don't propagate exceptions }
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
2024-09-26 11:38:41 +02:00
|
|
|
Result := SHA256Final(Context);
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
|
|
|
|
|
|
|
procedure EnumFileReplaceOperationsFilenames(const EnumFunc: TEnumFROFilenamesProc;
|
|
|
|
Param: Pointer);
|
|
|
|
{ Enumerates all the filenames in the current PendingFileRenameOperations
|
|
|
|
registry value or WININIT.INI file. The function does not distinguish between
|
|
|
|
source and destination filenames; it enumerates both. }
|
|
|
|
|
2024-03-29 08:00:28 +01:00
|
|
|
procedure DoValue(const K: HKEY; const ValueName: PChar);
|
2011-10-06 20:53:09 +02:00
|
|
|
var
|
2024-03-29 08:00:28 +01:00
|
|
|
S: String;
|
|
|
|
P, PEnd: PChar;
|
2011-10-06 20:53:09 +02:00
|
|
|
begin
|
2024-03-29 08:00:28 +01:00
|
|
|
if not RegQueryMultiStringValue(K, ValueName, S) then
|
|
|
|
Exit;
|
|
|
|
P := PChar(S);
|
|
|
|
PEnd := P + Length(S);
|
|
|
|
while P < PEnd do begin
|
|
|
|
if P[0] = '!' then
|
|
|
|
{ Note: '!' means that MoveFileEx was called with the
|
|
|
|
MOVEFILE_REPLACE_EXISTING flag }
|
|
|
|
Inc(P);
|
|
|
|
if StrLComp(P, '\??\', 4) = 0 then begin
|
|
|
|
Inc(P, 4);
|
|
|
|
if P[0] <> #0 then
|
|
|
|
EnumFunc(P, Param);
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
2024-03-29 08:00:28 +01:00
|
|
|
Inc(P, StrLen(P) + 1);
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
2024-03-29 08:00:28 +01:00
|
|
|
var
|
|
|
|
K: HKEY;
|
|
|
|
begin
|
|
|
|
if RegOpenKeyExView(rvDefault, HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager',
|
|
|
|
0, KEY_QUERY_VALUE, K) = ERROR_SUCCESS then begin
|
2011-10-06 20:53:09 +02:00
|
|
|
try
|
2024-03-29 08:00:28 +01:00
|
|
|
DoValue(K, 'PendingFileRenameOperations');
|
|
|
|
{ When "PendingFileRenameOperations" is full, it spills over into
|
|
|
|
"PendingFileRenameOperations2" }
|
|
|
|
DoValue(K, 'PendingFileRenameOperations2');
|
|
|
|
finally
|
|
|
|
RegCloseKey(K);
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
2020-07-03 10:05:28 +02:00
|
|
|
procedure UnregisterFont(const FontName, FontFilename: String; const PerUserFont: Boolean);
|
2011-10-06 20:53:09 +02:00
|
|
|
var
|
2020-07-03 10:05:28 +02:00
|
|
|
RootKey, K: HKEY;
|
2011-10-06 20:53:09 +02:00
|
|
|
begin
|
2020-07-03 10:05:28 +02:00
|
|
|
if PerUserFont then
|
|
|
|
RootKey := HKEY_CURRENT_USER
|
|
|
|
else
|
|
|
|
RootKey := HKEY_LOCAL_MACHINE;
|
|
|
|
|
2024-03-29 08:00:28 +01:00
|
|
|
if RegOpenKeyExView(rvDefault, RootKey, 'Software\Microsoft\Windows NT\CurrentVersion\Fonts',
|
2011-10-06 20:53:09 +02:00
|
|
|
0, KEY_SET_VALUE, K) = ERROR_SUCCESS then begin
|
|
|
|
RegDeleteValue(K, PChar(FontName));
|
|
|
|
RegCloseKey(K);
|
|
|
|
end;
|
|
|
|
if RemoveFontResource(PChar(FontFilename)) then
|
|
|
|
SendNotifyMessage(HWND_BROADCAST, WM_FONTCHANGE, 0, 0);
|
|
|
|
end;
|
|
|
|
|
2025-05-20 13:31:46 +02:00
|
|
|
function GetSpaceOnDisk(const DisableFsRedir: Boolean; const DriveRoot: String;
|
2011-10-06 20:53:09 +02:00
|
|
|
var FreeBytes, TotalBytes: Integer64): Boolean;
|
|
|
|
var
|
|
|
|
GetDiskFreeSpaceExFunc: function(lpDirectoryName: PChar;
|
|
|
|
lpFreeBytesAvailable: PLargeInteger; lpTotalNumberOfBytes: PLargeInteger;
|
|
|
|
lpTotalNumberOfFreeBytes: PLargeInteger): BOOL; stdcall;
|
|
|
|
PrevState: TPreviousFsRedirectionState;
|
|
|
|
SectorsPerCluster, BytesPerSector, FreeClusters, TotalClusters: Cardinal;
|
|
|
|
begin
|
|
|
|
{ NOTE: The docs claim that GetDiskFreeSpace supports UNC paths on
|
|
|
|
Windows 95 OSR2 and later. But that does not seem to be the case in my
|
|
|
|
tests; it fails with error 50 on Windows 95 through Me.
|
|
|
|
GetDiskFreeSpaceEx, however, *does* succeed with UNC paths, so use it
|
|
|
|
if available. }
|
|
|
|
GetDiskFreeSpaceExFunc := GetProcAddress(GetModuleHandle(kernel32),
|
2024-03-29 08:00:28 +01:00
|
|
|
'GetDiskFreeSpaceExW');
|
2011-10-06 20:53:09 +02:00
|
|
|
if not DisableFsRedirectionIf(DisableFsRedir, PrevState) then begin
|
|
|
|
Result := False;
|
|
|
|
Exit;
|
|
|
|
end;
|
|
|
|
try
|
|
|
|
if Assigned(@GetDiskFreeSpaceExFunc) then begin
|
|
|
|
Result := GetDiskFreeSpaceExFunc(PChar(AddBackslash(PathExpand(DriveRoot))),
|
|
|
|
@TLargeInteger(FreeBytes), @TLargeInteger(TotalBytes), nil);
|
|
|
|
end
|
|
|
|
else begin
|
|
|
|
Result := GetDiskFreeSpace(PChar(AddBackslash(PathExtractDrive(PathExpand(DriveRoot)))),
|
|
|
|
DWORD(SectorsPerCluster), DWORD(BytesPerSector), DWORD(FreeClusters),
|
|
|
|
DWORD(TotalClusters));
|
|
|
|
if Result then begin
|
2024-03-29 08:00:28 +01:00
|
|
|
{ The result of GetDiskFreeSpace does not cap at 2GB, so we must use a
|
|
|
|
64-bit multiply operation to avoid an overflow. }
|
2011-10-06 20:53:09 +02:00
|
|
|
Multiply32x32to64(BytesPerSector * SectorsPerCluster, FreeClusters,
|
|
|
|
FreeBytes);
|
|
|
|
Multiply32x32to64(BytesPerSector * SectorsPerCluster, TotalClusters,
|
|
|
|
TotalBytes);
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
finally
|
|
|
|
RestoreFsRedirection(PrevState);
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
2025-05-20 13:31:46 +02:00
|
|
|
function GetSpaceOnNearestMountPoint(const DisableFsRedir: Boolean;
|
2011-10-06 20:53:09 +02:00
|
|
|
const StartDir: String; var FreeBytes, TotalBytes: Integer64): Boolean;
|
|
|
|
{ Gets the free and total space available on the specified directory. If that
|
|
|
|
fails (e.g. if the directory does not exist), then it strips off the last
|
|
|
|
component of the path and tries again. This repeats until it reaches the
|
|
|
|
root. Returns True if successful. }
|
|
|
|
var
|
|
|
|
Dir: String;
|
|
|
|
LastLen: Integer;
|
|
|
|
begin
|
|
|
|
Result := False;
|
|
|
|
Dir := RemoveBackslashUnlessRoot(StartDir);
|
|
|
|
LastLen := 0;
|
|
|
|
while Length(Dir) <> LastLen do begin
|
2025-05-20 13:31:46 +02:00
|
|
|
if GetSpaceOnDisk(DisableFsRedir, Dir, FreeBytes, TotalBytes) then begin
|
2011-10-06 20:53:09 +02:00
|
|
|
Result := True;
|
|
|
|
Break;
|
|
|
|
end;
|
|
|
|
LastLen := Length(Dir);
|
|
|
|
Dir := PathExtractDir(Dir);
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure RefreshEnvironment;
|
|
|
|
{ Notifies other applications (Explorer) that environment variables have
|
2024-03-29 08:20:50 +01:00
|
|
|
changed. Based on code from KB article 104011. }
|
2011-10-06 20:53:09 +02:00
|
|
|
var
|
2019-01-29 20:40:51 +01:00
|
|
|
MsgResult: DWORD_PTR;
|
2011-10-06 20:53:09 +02:00
|
|
|
begin
|
|
|
|
{ Note: We originally used SendNotifyMessage to broadcast the message but it
|
|
|
|
turned out that while it worked fine on NT 4 and 2000 it didn't work on XP
|
|
|
|
-- the string "Environment" in lParam would be garbled on the receiving
|
|
|
|
end (why I'm not exactly sure). We now use SendMessageTimeout as directed
|
|
|
|
in the KB article 104011. It isn't as elegant since it could cause us to
|
|
|
|
be delayed if another app is hung, but it'll have to do. }
|
|
|
|
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0,
|
2019-01-29 20:40:51 +01:00
|
|
|
LPARAM(PChar('Environment')), SMTO_ABORTIFHUNG, 5000, @MsgResult);
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
|
|
|
|
|
|
|
procedure SplitNewParamStr(const Index: Integer; var AName, AValue: String);
|
|
|
|
{ Reads a command line parameter. If it is in the form "/PARAM=VALUE" then
|
|
|
|
AName is set to "/PARAM=" and AValue is set to "VALUE". Otherwise, the full
|
|
|
|
parameter is stored in AName, and AValue is set to an empty string. }
|
|
|
|
var
|
|
|
|
S: String;
|
|
|
|
P: Integer;
|
|
|
|
begin
|
|
|
|
S := NewParamStr(Index);
|
|
|
|
if (S <> '') and (S[1] = '/') then begin
|
|
|
|
P := PathPos('=', S);
|
|
|
|
if P <> 0 then begin
|
|
|
|
AName := Copy(S, 1, P);
|
|
|
|
AValue := Copy(S, P+1, Maxint);
|
|
|
|
Exit;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
AName := S;
|
|
|
|
AValue := '';
|
|
|
|
end;
|
|
|
|
|
2025-05-20 13:31:46 +02:00
|
|
|
function ForceDirectories(const DisableFsRedir: Boolean; Dir: String): Boolean;
|
2013-01-07 22:35:55 +01:00
|
|
|
begin
|
|
|
|
Dir := RemoveBackslashUnlessRoot(Dir);
|
|
|
|
if (PathExtractPath(Dir) = Dir) or DirExistsRedir(DisableFsRedir, Dir) then
|
|
|
|
Result := True
|
|
|
|
else
|
2025-05-20 13:31:46 +02:00
|
|
|
Result := ForceDirectories(DisableFsRedir, PathExtractPath(Dir)) and
|
2013-01-07 22:35:55 +01:00
|
|
|
CreateDirectoryRedir(DisableFsRedir, Dir);
|
|
|
|
end;
|
|
|
|
|
2011-10-06 20:53:09 +02:00
|
|
|
{ TSimpleStringList }
|
|
|
|
|
|
|
|
procedure TSimpleStringList.Add(const S: String);
|
|
|
|
var
|
|
|
|
Delta: Integer;
|
|
|
|
begin
|
|
|
|
if FCount = FCapacity then begin
|
|
|
|
if FCapacity > 64 then Delta := FCapacity div 4 else
|
|
|
|
if FCapacity > 8 then Delta := 16 else
|
|
|
|
Delta := 4;
|
|
|
|
SetCapacity(FCapacity + Delta);
|
|
|
|
end;
|
|
|
|
FList^[FCount] := S;
|
|
|
|
Inc(FCount);
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure TSimpleStringList.AddIfDoesntExist(const S: String);
|
|
|
|
begin
|
|
|
|
if IndexOf(S) = -1 then
|
|
|
|
Add(S);
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure TSimpleStringList.SetCapacity(NewCapacity: Integer);
|
|
|
|
begin
|
|
|
|
ReallocMem(FList, NewCapacity * SizeOf(Pointer));
|
|
|
|
if NewCapacity > FCapacity then
|
|
|
|
FillChar(FList^[FCapacity], (NewCapacity - FCapacity) * SizeOf(Pointer), 0);
|
|
|
|
FCapacity := NewCapacity;
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure TSimpleStringList.Clear;
|
|
|
|
begin
|
|
|
|
if FCount <> 0 then Finalize(FList^[0], FCount);
|
|
|
|
FCount := 0;
|
|
|
|
SetCapacity(0);
|
|
|
|
end;
|
|
|
|
|
|
|
|
function TSimpleStringList.Get(Index: Integer): String;
|
|
|
|
begin
|
|
|
|
Result := FList^[Index];
|
|
|
|
end;
|
|
|
|
|
|
|
|
function TSimpleStringList.IndexOf(const S: String): Integer;
|
|
|
|
{ Note: This is case-sensitive, unlike TStringList.IndexOf }
|
|
|
|
var
|
|
|
|
I: Integer;
|
|
|
|
begin
|
|
|
|
Result := -1;
|
|
|
|
for I := 0 to FCount-1 do
|
|
|
|
if FList^[I] = S then begin
|
|
|
|
Result := I;
|
|
|
|
Break;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
|
|
|
destructor TSimpleStringList.Destroy;
|
|
|
|
begin
|
|
|
|
Clear;
|
|
|
|
inherited Destroy;
|
|
|
|
end;
|
|
|
|
|
|
|
|
end.
|