Writing binary file in Inno Setup - inno-setup

How does one write to a binary file in Inno Setup script? It's a configuration file I want to edit in the PrepareToInstall step. The problem is that I'm looking at the support functions:
TStream = class(TObject)
function Read(Buffer: String; Count: Longint): Longint;
function Write(Buffer: String; Count: Longint): Longint;
function Seek(Offset: Longint; Origin: Word): Longint;
procedure ReadBuffer(Buffer: String; Count: Longint);
procedure WriteBuffer(Buffer: String; Count: Longint);
function CopyFrom(Source: TStream; Count: Longint): Longint;
property Position: Longint; read write;
property Size: Longint; read write;
end;
And it seems even the most basic write function writes strings. Should I just do it in a batch script?

Consider the string in the TStream interface as a buffer of chars/bytes.
It's bit more complicated with Unicode version of Inno Setup, where the string is an array of 2-byte chars (comparing to legacy Ansi version, as there one byte equals one char – Though as of Inno Setup 6, Unicode is actually the only version available anyway).
See also Read bytes from file at desired position with Inno Setup.
To convert a hex string to the actual binary data, you can use the CryptStringToBinary Windows API function.
The following code works both in Ansi and Unicode version of Inno Setup.
#ifndef Unicode
const CharSize = 1;
#define AW "A"
#else
const CharSize = 2;
#define AW "W"
#endif
function CryptStringToBinary(
sz: string; cch: LongWord; flags: LongWord; binary: string; var size: LongWord;
skip: LongWord; flagsused: LongWord): Integer;
external 'CryptStringToBinary{#AW}#crypt32.dll stdcall';
const
CRYPT_STRING_HEX = $04;
procedure WriteHexToFile(Hex: string; FileName: string);
var
Stream: TFileStream;
Buffer: string;
Size: LongWord;
begin
Stream := TFileStream.Create(FileName, fmCreate);
try
SetLength(Buffer, (Length(Hex) div 2*CharSize) + CharSize - 1);
Size := Length(Hex) div 2;
if (CryptStringToBinary(
Hex, Length(Hex), CRYPT_STRING_HEX, Buffer, Size, 0, 0) = 0) or
(Size <> Length(Hex) div 2) then
begin
RaiseException('Error decoding hex string');
end;
Stream.WriteBuffer(Buffer, Size);
finally
Stream.Free;
end;
end;
Use it like:
procedure WriteHexToFileTest;
var
Hex: string;
begin
Hex :=
'000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f' +
'202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f' +
'404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f' +
'606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f' +
'808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f' +
'a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf' +
'c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf' +
'e0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff';
WriteHexToFile(Hex, 'my_binary_file.dat');
end;

Related

Delphi Tokyo - Function: String to Hex and Hex to String

The function below works perfectly to convert string to hexadecimal:
function String2Hex(const Buffer: AnsiString): string;
begin
SetLength(Result, Length(Buffer) * 2);
BinToHex(#Buffer[1], PWideChar(#Result[1]), Length(Buffer));
end;
ShowMessage(String2Hex('stackoverflow'));
This result: "737461636B6F766572666C6F77"
The problem is in the function of converting hexadecimal to string:
function Hex2String(const Buffer: AnsiString): string;
begin
SetLength(Result, Length(Buffer) div 2);
HexToBin(PWideChar(#Buffer[1]), #Result[1], Length(Buffer));
end;
ShowMessage(Hex2String('737461636B6F766572666C6F77'));
The result should be "stackoverflow" but nothing happens.
Could someone help me?
There are a couple of problems with your code:
You are type-casting your input AnsiString incorrectly to PWideChar, so you are calling the wrong overload of HexToBin(). PWideChar should be PAnsiChar instead.
The BufSize parameter of HexToBin() specifies the number of bytes the output buffer expects to receive, but you are passing it the number of characters in the hex string instead.
Also, since String2Hex() takes an AnsiString and returns a UnicodeString, Hex2String() should take UnicodeString and return an AnsiString to match.
Try this instead:
function String2Hex(const Buffer: AnsiString): string;
begin
SetLength(Result, Length(Buffer) * 2);
BinToHex(PAnsiChar(Buffer), PChar(Result), Length(Buffer));
end;
function Hex2String(const Buffer: string): AnsiString;
begin
SetLength(Result, Length(Buffer) div 2);
HexToBin(PChar(Buffer), PAnsiChar(Result), Length(Result));
end;
var
hex: string;
str: AnsiString;
begin
hex := String2Hex('stackoverflow');
ShowMessage(hex); // shows '737461636B6F766572666C6F77'
str := Hex2String(hex);
ShowMessage(str); // shows 'stackoverflow'
end;

Querying Version Information from executable at runtime

I am trying to query the version details of a file that the installer installs and compare it against the version details of the same file present in the installer being executed. The details are not in the FileVersion or ProductVersion field but can be in other fields like InternalName etc.
I see Win32 APIs for solving this and also some sample code like :
http://delphidabbler.com/articles?article=20
How can I read details of file?
However, some of the data types used in those code samples do not work with Inno Setup. Further, some samples and description seem to indicate that the language and codepage itself will be an array but some samples use it assuming only one entry for language and codepage.
I was stuck at trying to find the language and codepage and based on comments below, I hard coded it for en-us.
I do see this answer which has a code sample for Inno Setup Pascal but the language and codepage calculation not being based on the lplpBufferCP variable makes me doubt its correctness.
Is it possible to read generic version info properties from Inno Setup Pascal script ? If so, please help around how to find the language and code page values.
The code I have written based on the aforesaid solutions is listed below with in-line comments for the problematic portions.
#ifdef UNICODE
#define AW "W"
#else
#define AW "A"
#endif
function GetFileVersionInfoSize(lptstrFilename: String; lpdwHandle: Integer): Integer;
external 'GetFileVersionInfoSize{#AW}#version.dll stdcall delayload';
function GetFileVersionInfo(lptstrFilename: String; dwHandle, dwLen: Integer; var lpData: Byte): Boolean;
external 'GetFileVersionInfo{#AW}#version.dll stdcall delayload';
function VerQueryValue(var pBlock: Byte; lpSubBlock: String; var lplpBuffer: Byte; var puLen: Integer): Boolean;
external 'VerQueryValue{#AW}#version.dll stdcall delayload';
function GetFileVersionProperty(const FileName, PropertyName: String): String;
var
VerSize: Integer;
VerInfo: array of Byte;
Dummy: Integer;
InternalNameArr: array of Byte;
begin
Result := '';
if not FileExists(FileName) then
begin
Log('File ' + FileName + ' does not exist');
Exit;
end;
VerSize := GetFileVersionInfoSize(FileName, 0);
if not VerSize > 0 then
begin
Log('File ' + FileName + ' has no version information');
Exit;
end;
SetArrayLength(VerInfo, VerSize);
if not GetFileVersionInfo(FileName, 0, VerSize, VerInfo[0]) then
begin
Log('Failed to get version info for ' + FileName);
Exit;
end;
if not GetFileVersionInfo(FileName, 0, VerSize, VerInfo[0]) then
begin
Log('Failed to get version info for ' + FileName);
Exit;
end;
{ Getting 'Version size = 2156' }
Log(Format('Version size = %d', [VerSize]));
{ Hard coded value just for testing }
SetArrayLength(InternalNameArr, 512);
{ 040904E4 hard coded for en-us }
{ Is this the correct way of querying the details ? }
{ If not, what needs to be done here }
{ TODO : InternalName hard coded. Use parameter PropertyName }
if VerQueryValue(VerInfo[0], '\StringFileInfo\040904E4\InternalName', InternalNameArr[0], Dummy) then
begin
Log('Failed to query internal name of ' + FileName);
Exit;
end
else
begin
{ What needs to be done here to convert an array of byte to string ? }
{ Do I need to iterate over the array and do the conversion ?}
{ The following does not work because of SetString() being unavailable : }
{ InternalName = SetString(AnsiStr, PAnsiChar(#InternalNameArr[0]), Len);}
{ Getting 'ProductName = 0000' and 'Dummy = 0' }
Log(Format('ProductName = %d%d', [InternalNameArr[0], InternalNameArr[1], InternalNameArr[2], InternalNameArr[3]]));
Log(Format('Dummy = %d', [Dummy]));
end;
{ TODO : Populate Result with appropriate value }
end;
An alternate approach could be to save the file properties of the installed file in registry (I am interested in 1 property of 1 of the files) and have the property available in the installer statically for the new file.
The correct code to retrieve a string from the first language of a file version info is below. The code builds on an answer by #Jens A. Koch to How to write data to an installer on the server?
The code requires Unicode version of Inno Setup.
function GetFileVersionInfoSize(
lptstrFilename: String; lpdwHandle: Integer): Integer;
external 'GetFileVersionInfoSizeW#version.dll stdcall delayload';
function GetFileVersionInfo(
lptstrFilename: String; dwHandle, dwLen: Integer; var lpData: Byte): Boolean;
external 'GetFileVersionInfoW#version.dll stdcall delayload';
function VerQueryValue(
var pBlock: Byte; lpSubBlock: String; var lplpBuffer: DWord;
var Len: Integer): Boolean;
external 'VerQueryValueW#version.dll stdcall delayload';
procedure RtlMoveMemoryAsString(Dest: string; Source: DWord; Len: Integer);
external 'RtlMoveMemory#kernel32.dll stdcall';
procedure RtlMoveMemoryAsBytes(Dest: array of Byte; Source: DWord; Len: Integer);
external 'RtlMoveMemory#kernel32.dll stdcall';
function GetFileVerInfo(FileName, VerName: String): String;
var
Len: Integer;
FileVerInfo: array of Byte;
Lang: array of Byte;
Buffer: DWord;
LangCodepage: string;
SubBlock: string;
begin
Result := '';
if FileExists(FileName) then
begin
Len := GetFileVersionInfoSize(FileName, 0);
if Len > 0 then
begin
SetArrayLength(FileVerInfo, Len);
if GetFileVersionInfo(FileName, 0, Len, FileVerInfo[0]) then
begin
if VerQueryValue(
FileVerInfo[0], '\VarFileInfo\Translation', Buffer, Len) then
begin
if Len >= 4 then
begin
SetArrayLength(Lang, 4);
RtlMoveMemoryAsBytes(Lang, Buffer, 4);
LangCodepage :=
Format('%.2x%.2x%.2x%.2x', [Lang[1], Lang[0], Lang[3], Lang[2]]);
SubBlock :=
Format('\%s\%s\%s', ['StringFileInfo', LangCodepage, VerName]);
if VerQueryValue(FileVerInfo[0], SubBlock, Buffer, Len) then
begin
SetLength(Result, Len - 1);
RtlMoveMemoryAsString(Result, Buffer, (Len - 1) * 2);
end;
end;
end;
end;
end;
end;
end;

Inno Setup Get size of a file over 2GB limit opened in another application

I am trying to return the size of a file using the Public Domain code from ISXKB at vincenzo.net.
function CloseHandle (hHandle: THandle): Boolean;
external 'CloseHandle#kernel32.dll stdcall';
const
{ Some constants for CreateFile (). }
GENERIC_READ = $80000000;
GENERIC_WRITE = $40000000;
GENERIC_EXECUTE = $20000000;
GENERIC_ALL = $10000000;
FILE_SHARE_READ = 1;
FILE_SHARE_WRITE = 2;
FILE_SHARE_DELETE = 4;
CREATE_NEW = 1;
CREATE_ALWAYS = 2;
OPEN_EXISTING = 3;
OPEN_ALWAYS = 4;
TRUNCATE_EXISTING = 5;
FILE_READ_ATTRIBUTES = $80;
FILE_WRITE_ATTRIBUTES = $100;
{ General Win32. }
INVALID_HANDLE_VALUE = -1;
function CreateFile (
lpFileName : String;
dwDesiredAccess : Cardinal;
dwShareMode : Cardinal;
lpSecurityAttributes : Cardinal;
dwCreationDisposition : Cardinal;
dwFlagsAndAttributes : Cardinal;
hTemplateFile : Integer
): Integer;
external 'CreateFileA#kernel32.dll stdcall';
function GetFileSize (hFile: THandle; var lpFileSizeHigh: Integer): Integer;
external 'GetFileSize#kernel32.dll stdcall';
function GetTheFileSize (FileName: String): Integer;
var
hFile: THandle;
iSize: Integer;
hSize: Integer;
begin
hFile := CreateFile (FileName,
GENERIC_READ,// Desired access.
FILE_SHARE_READ + FILE_SHARE_WRITE,
0, { Security attributes. }
OPEN_EXISTING,
FILE_ATTRIBUTE_TEMPORARY,
0);
if (INVALID_HANDLE_VALUE = hFile) then
begin
Result := 0;
Exit;
end;
iSize := GetFileSize (hFile, hSize);
CloseHandle (hFile);
Result := iSize;
end;
However, this does not appear to work as intended and is returning 0, which I believe is because it is exiting at if (INVALID_HANDLE_VALUE = hFile) then Result := 0. The file I am passing to it exists and is accessible. Can anyone shed some light on why this is failing or suggest an alternate method? Note that I cannot use the built in FileSize function as this has a 2GB limit, which is not enough for my purposes.
I assume you are using an Unicode version of Inno Setup.
So you must use an Unicode version of CreateFile, the CreateFileW, not CreateFileA:
external 'CreateFileW#kernel32.dll stdcall';
Anyway, the GettheFileSize implementation (from now defunct ISXKB) has 2 GB limit too:
This declaration works with files up to 2 GB.
...
... retrieves its low 32 bit part of the file size as an integer, and closes the file again.
To support 64-bit sizes, change it like:
function GetTheFileSize (FileName: String): Int64;
...
begin
...
Result := Int64(Cardinal(iSize)) + (Int64(Cardinal(hSize)) shl 32);
end;
Anyway, it's somewhat overkill. And as you have found it does not work if another application has the file opened without allowing others applications to at least read the file (it has not specified FILE_SHARE_READ in its call to CreateFile).
Note that the FileSize won't work either in this case, as it has basically the same implementation as ISXKB's GetTheFileSize.
There's an easy solution using FindFirst support function:
function GetTheFileSize(FileName: String): Int64;
var
FindRec: TFindRec;
begin
if FindFirst(FileName, FindRec) then
begin
Result := Int64(FindRec.SizeHigh) shl 32 + FindRec.SizeLow;
FindClose(FindRec);
end
else
begin
Result := -1;
end;
end;

Inno Setup URLDownloadToCacheFile

Any time i try to compile this code, a get an error msg -> Type mismatch. Why?
The first variant is called with the same "nil" and the compiler has no problem with it, but with the last?
[Setup]
AppName=MyApp
AppVerName=MyApp
DefaultDirName={pf}\MyApp
DefaultGroupName=MyApp
OutputDir=.
[Code]
function URLDownloadToCacheFile(const lpUnkcaller: variant; const szURL: PAnsiChar; var szFileName: PAnsiChar; const cchFileName: DWORD; const dwReserved: DWORD; IBindStatusCallback: variant): HRESULT; external 'URLDownloadToFileW#Urlmon.dll stdcall';
function InitializeSetup(): Boolean;
var
szFileName, szURL : PAnsiChar;
ErrorCode : HRESULT;
MAX_PATH : DWORD;
begin
szURL := 'http://fs2.directupload.net/images/user/150607/x3sugvzy.jpg';
MAX_PATH := 512;
SetLength(szFileName, MAX_PATH + 1);
ErrorCode := URLDownloadToCacheFile(null, szURL, szFileName, MAX_PATH, 0, null);
end;
changed code that way:
[Setup]
AppName=MyApp
AppVerName=MyApp
DefaultDirName={pf}\MyApp
DefaultGroupName=MyApp
OutputDir=.
[Code]
function URLDownloadToCacheFile(const lpUnkcaller: variant; const szURL: AnsiString; var szFileName: String; const cchFileName: DWORD; const dwReserved: DWORD; IBindStatusCallback: variant): HRESULT; external 'URLDownloadToFileW#Urlmon.dll stdcall';
function InitializeSetup(): Boolean;
var
szFileName : String;
szURL : AnsiString;
ErrorCode : HRESULT;
MAX_PATH : DWORD;
begin
szURL := 'http://fs2.directupload.net/images/user/150607/x3sugvzy.jpg';
MAX_PATH := 512;
SetLength(szFileName, 513);
ErrorCode := URLDownloadToCacheFile(null, szURL, szFileName, MAX_PATH, 0, null);
MsgBox(szFileName,mbConfirmation, MB_OK);
end;
The original problem was that you were passing nil to a parameter of type Variant. Null value for the Variant type is NULL in Pascal Script.
But, your prototype needs to be changed. You need to use Unicode data types when you are using Unicode variant of a function, so you cannot use PAnsiChar types there. The next problem is with the Variant types you used for interface type parameters. That won't work. You can use IUnknown types there, because the lpUnkcaller parameter is of a pointer to this type and pBSC is of type IBindStatusCallback which from IUnknown inherits. The last problem is that MSDN is wrong here. The szURL is according to the header files of type LPCTSTR, not LPCSTR. The Unicode variant prototype looks like this in SDK 8.1 urlmon.h header file (formatted and commented by me):
STDAPI URLDownloadToCacheFileW(
_In_opt_ LPUNKNOWN,
_In_ LPCWSTR, // <- this is wrong on MSDN; this type would have to be LPCSTR here
_Out_writes_(cchFileName) LPWSTR,
DWORD cchFileName,
DWORD,
_In_opt_ LPBINDSTATUSCALLBACK
);
So, your script might be written like this (it should support also ANSI version of Inno Setup, but I haven't tested it there):
[Setup]
AppName=My Program
AppVersion=1.5
DefaultDirName={pf}\My Program
[Code]
const
MAX_PATH = 260;
S_OK = $00000000;
#ifdef UNICODE
#define AW "W"
#else
#define AW "A"
#endif
function URLDownloadToCacheFile(lpUnkcaller: IUnknown; szURL: string; szFileName: string;
cchFileName: DWORD; dwReserved: DWORD; pBSC: IUnknown): HRESULT;
external 'URLDownloadToCacheFile{#AW}#urlmon.dll stdcall';
function TryDownloadToCache(const URL: string; out FileName: string): Boolean;
begin
SetLength(FileName, MAX_PATH);
Result := URLDownloadToCacheFile(nil, URL, FileName, Length(FileName), 0, nil) = S_OK;
end;
function InitializeSetup: Boolean;
var
FileName: string;
ErrorCode: Integer;
begin
Result := True;
if TryDownloadToCache('http://i.imgur.com/wKCsei6.png', FileName) then
ShellExec('', FileName, '', '', SW_SHOW, ewNoWait, ErrorCode)
else
MsgBox('Downloading failed.', mbError, MB_OK);
end;

Run a batch file only one time when the installer starts

I'm having an issue here with the following:
function InitializeSetup(): Boolean;
var
ResultCode:Integer;
begin
Result := true;
if MsgBox('Wanna help?',mbConfirmation, MB_YESNO )= IDYES then
begin
CreateBatch();
Exec('cmd.exe', '/c "' +ExpandConstant('{tmp}\batch.bat'),'',SW_HIDE, ewWaitUntilTerminated, ResultCode);
Result:= false;
end;
end;
in the batch file i got the following :
#ECHO OFF
D:
cd D:\_INSTALLER\Output
"installer.exe" /SAVEINF="opt.txt"
So it basically re-open the installer , over and over again ... ( infinite loop )
Is there any way to ask : Wanna help? only for the 1st time ? and if the user clicked yes, the batch should be executed , else if the user clicked no it should continue the install normally.
Thanks in advance for the support ,
BeGiN.
With the help of TLama and his post i achieved my goal by using the following script:
[Setup]
AppName=My Program
AppVersion=1.5
DefaultDirName={pf}\My Program
[Code]
#ifdef UNICODE
#define AW "W"
#else
#define AW "A"
#endif
type
HINSTANCE = THandle;
procedure ExitProcess(exitCode:integer);external 'ExitProcess#kernel32.dll stdcall';
function ShellExecute(hwnd: HWND; lpOperation: string; lpFile: string;lpParameters: string; lpDirectory: string; nShowCmd: Integer): HINSTANCE;
external 'ShellExecute{#AW}#shell32.dll stdcall';
var
withINF: Boolean;
function CmdLineParamExists(const Value: string): Boolean;
var
I: Integer;
begin
Result := False;
for I := 1 to ParamCount do
if CompareText(ParamStr(I), Value) = 0 then
begin
Result := True;
Break;
end;
end;
//Initialize setup
function InitializeSetup(): Boolean;
var
ResultCode:Integer;
Params: string;
RetVal: HINSTANCE;
begin
Result := true;
withINF := CmdLineParamExists('/SAVEINF=opt.txt');
if not withINF then
begin
Params := '/SAVEINF=opt.txt';
ShellExecute(0, 'open',ExpandConstant('{srcexe}'), Params, '', SW_SHOW);
ExitProcess(0);
end;
end;
L.E: A shorter code for doing the same thing also made by TLama(multilingual support added):
[Setup]
AppName=My Program
AppVersion=1.5
DefaultDirName={pf}\My Program
ShowLanguageDialog = yes
[Languages]
Name: "en"; MessagesFile: "compiler:Default.isl"
Name: "nl"; MessagesFile: "compiler:Languages\Dutch.isl"
[Code]
#ifdef UNICODE
#define AW "W"
#else
#define AW "A"
#endif
type
HINSTANCE = THandle;
function ShellExecute(hwnd: HWND; lpOperation: string; lpFile: string;lpParameters: string; lpDirectory: string; nShowCmd: Integer): HINSTANCE;
external 'ShellExecute{#AW}#shell32.dll stdcall';
function CmdLineParamExists(const Value: string): Boolean;
var
I: Integer;
begin
Result := False;
for I := 1 to ParamCount do
if CompareText(ParamStr(I), Value) = 0 then
begin
Result := True;
Break;
end;
end;
function InitializeSetup: Boolean;
var
Params: string;
begin
// prepare Params variable for reusing
if ExpandConstant('{language}') = 'en' then begin
Params := '/SAVEINF=opt.txt /LANG=en';
end;
if ExpandConstant('{language}') = 'nl' then begin
Params := '/SAVEINF=opt.txt /LANG=nl';
end;
// allow this setup to run if the expected parameter is specified; or, if it is not, allow to run it
// when ShellExecute fails; it works like this - first evaluates the CmdLineParamExists function and
// if that returns True, the second part of the statement (ShellExecute) won't run (evaluate); when
// the parameter is not found, the CmdLineParamExists returns False and statement evaluation goes on,
// ShellExecute attempts to run the setup and to the Result returns True (allow this instance to run)
// when the function fails for some reason (the returned value <= 32); in other words, you will allow
// this setup instance to run if executing of the new setup instance fails
Result := CmdLineParamExists('/SAVEINF=opt.txt') or (ShellExecute(0, '', ExpandConstant('{srcexe}'), Params, '', SW_SHOW) <= 32);
end;
Regards,
BeGiN
I see not reason for this but you can simply solve this by creating temp file.
If user clicks Yes (first time) then create some file (any file with some random content) just before CreateBatch() is called.
Then simply check for existence of this file - if it exists user already clicked Yes (once) and do what ever you want.

Resources