
Why I developed it
Sometimes, those off-the-shelf installation tools might require you to write some script in order to properly copy those distributed files into the defined folder. Yet, it may not best fit your requirement as in our real software development community, there is always some demanding boss keeping on asking more but paying less for the development tools. That's the reason why I have started to design & develop my first self-extract executable file to support the new LiveUpdate module in my application.
After I developed it, I found I can make it even better by reusing the same code (EXE) more efficiently in the future. So finally, I have come-out with my own Self-Extract EXE builder which will scan the specified folder and accumulate each file (binary data) into a single binary file, followed by compressed it using the Zlib* algorithm and subsequently injecting it into the embedded self-extract executable file by the UpdateResource
API.
Note: This API is only supported on Windows 2000/XP/NT but not Windows 95/98/ME.
*Zlib compression algorithm was Copyright � 1995-2003 Jean-loup Gailly and Mark Adler. Please visit Zlib to learn more about the Zlib compression algorithm.
Methodology behind the scene
The methodology applied in this project is simple and straight forward. As the entire project was split into 3 separate modules, each module takes place at different stages in order to achieve the final goal (distribute any files via a self-extract executable file)!
Item |
Module |
Description |
1. |
SelfExtract.exe |
Core module #1, which will go through the following (pre-distribute) stages:
- Provide GUI which enables the user to specify the source folder and output executable filename.
- Scan the source file from the user specified folder.
- Build the source file information block and combine individual source files data into a binary file.
- Merge the source file information block and binary data into a single file.
- Compress the merged binary file.
- Spawn the SetupEx.exe from the embedded custom resource table (EXTRACTOR).
- Inject compressed binary data into the SetupEx.exe custom resource table (SETUP).
- Copy the final file into the user specified file path and name.
- Delete all the created temporary files and release all the allocated memory.
- Notify user about the process completed.
|
2. |
SetupEx.exe |
Core module #2, which will go through the following (post-distribute) stages:
- Provide GUI which enables the user to specify the output folder.
- Read the compressed binary data from custom resource table (SETUP) and write into a temporary file.
- Uncompress the binary data.
- Read the source file information block from the temporary file.
- Read individual block of binary data based on the file size specified in the source file information block.
- Write the data into the temporary file using the filename stored under the source file information block.
- Update the temporary file's file time with the value stored under the source file information block.
- Copy the temporary file from temporary folder into the user specified output folder.
- Delete all the created temporary files and release all the allocated memory.
- Notify user about the process completed.
|
3. |
Zlib10.lib |
Core module #3, which is used to compress and uncompress the distributed data at both (pre/post-distribute) stages:
Although the code of this module is free to distribute, it is Copyright � 1995-2003 Jean-loup Gailly and Mark Adler. I just recompiled it onto a static library which will make the first 2 module code look more clean. You can always refer to the Zlib for detail information about how the compress and uncompress algorithm work, because I am not going to explain this complicated algorithm code and it is beyond my capability. :) |
I also included the sample source code, which will offer you a better understanding on the process involved in the above first 2 modules (SelfExtract.exe and SetupEx.exe).
The format I adopted
The format I used to merge multiple files into a single file is simple. In general, it will split into 3 parts:
Section |
Description |
Setup Info |
Include the first 4 bytes (DWORD ) value which indicate how many files are available in the following Header and Data section. The following 260 bytes will be stored the Auto-Exec filename, which will eventually be launched by the Self-Extract module right after successfully extracting the embedded data into the selected destination folder. |
Header |
N block of EXTRACTFILEINFO , where N will be the value (dwFileCount ) stored in the Setup Info section. |
Data |
N block of file content data, where N will be the value (dwFileCount ) stored in the Setup Info section. |
First part will indicate how many files are available in the merge data file. Second and third parts will then carry a multiple block of information, as this will vary with how many files (0, 1, 2, ...N) you are going to distribute using the self-extract executable file. In summary, the overview of the merge data file format will be like the figure shown below:

Reason I adopted this format
The thousand and one reason I adopted this format is because it was so easy to build (write) and extract (read). All it needs is refer to individual file information stored under the header block. So, you can say the header block is the heart of the entire merge data file in this project. Hence, a single silly mistake made in the header block will cause the SetupEx.exe to fail to extract each individual data and restore with its original file property.
Information stored in the setup info block
In this project, information available in this section will be used by the spawned SetupEx.exe module to identify how many files' information are available in the Header section, as well as the auto-exec filename also stored under this section. No doubt, you can add in more specified information that fits into your own application needs. Because, this project will act as the framework which will speed up your development.
typedef struct tagSETUPINFO
{
DWORD dwFileCount;
char szAutoExecFile[MAX_PATH];
} SETUPINFO, FAR * LPSETUPINFO;
What should be stored inside the header block
The original file information will be stored under this header block. As this information is subject to change for different application needs, some might need full range of original file information, and some may not. For this project, I stored the information which is just nice for distributing those files that do not require extra process (like DLL Register) after deployment.
Again, all this information must be stored in a pre-defined structure which will be fully understood between both the SelfExtract.exe and SetupEx.exe. Other wise, you'll have no problem in building the merged data file. But later, you'll have problem in extracting it. As the data location pointer (memory location) was running out, it will cause the SetupEx.exe over-read or read less data as compared with the original file. So, this structure is very important in the entire merge data file.
Below is the pre-defined EXTRACTFILEINFO
structure, which will be stored under the header block and used throughout this project.
typedef struct tagEXTRACTFILEINFO
{
DWORD dwIndex;
FILETIME CreateTime;
FILETIME LastAcessTime;
FILETIME LastWriteTime;
DWORD dwFileSizeHigh;
DWORD dwFileSizeLow;
char szBinFileName[MAX_PATH];
} EXTRACTFILEINFO, FAR * LPEXTRACTFILEINFO;
The source of all this information comes from the WIN32_FIND_DATA
structure as shown below:
typedef struct _WIN32_FIND_DATA {
DWORD dwFileAttributes;
FILETIME ftCreationTime;
FILETIME ftLastAccessTime;
FILETIME ftLastWriteTime;
DWORD nFileSizeHigh;
DWORD nFileSizeLow;
DWORD dwReserved0;
DWORD dwReserved1;
TCHAR cFileName[ MAX_PATH ];
TCHAR cAlternateFileName[ 14 ];
} WIN32_FIND_DATA, *PWIN32_FIND_DATA;
Other file information you can include into EXTRACTFILEINFO
structure (to enhance the current project) are like those 2 shown below. But like I say, this is subject to your application needs.
For instance, if you wish to distribute an ActiveX, then most likely you cannot skip the DLL registration (by executing the regsvr32.exe) process right after deployment to the target machine. Therefore, based on the bDllSelfRegister
value, the self-extract executable will automate the registering of the ActiveX DLL for you.
If you wish to include sub-folder into your distribution package, you will require to include the dwAttributes
information. This indication will instruct the self-extract executable file to create the respective sub-folder before it proceeds to deploy those files which sits inside this sub-folder. Else, the entire process will fail.
typedef struct tagEXTRACTFILEINFO
DWORD dwFileAttributes;
BOOL bDllSelfRegister;
} EXTRACTFILEINFO, FAR * LPEXTRACTFILEINFO;
Retrieve file information
Before proceeding to create the merge data file, it is important to know how to get the respective file information. If we fail to achieve this, no point for us to proceed further from here. But to achieve this simply, the WIN32 SDK does provide us the FindFirstFile
or GetFileAttributesEx
API. These APIs will return all the information we need.
But, this project was scanning the specified folder without knowing the filename at the first place. So, GetFileAttributesEx
will be returning duplicate file information. The FindFirstFile
does return those information which is returned by GetFileAttributesEx
. Below is the code snippet:
HANDLE hFile = NULL;
WIN32_FIND_DATA wfs = {NULL};
hFile = FindFirstFile("C:\\SelfExtract.exe", &wfs);
if (NULL != hFile || INVALID_HANDLE_VALUE != hFile)
{
}
if (NULL != hFile) {FindClose(hFile);}
hFile = NULL;
The way I scan files
Since the current GUI of the SelfExtract.exe module only supports user to specify the folder (full path) whereby those source files are saved, there is a need that the SelfExtract.exe module be able to scan through the specified folder and retrieve each available file information as well as its content before the merge data file can be successfully built.
The code I use to scan a folder is exactly the same as the code I use to retrieve the file information. The only difference is now, it requires a do
...while
loop instead of just calling the API once, and the code snippet is shown below:
HANDLE hFile = NULL;
WIN32_FIND_DATA wfs = {NULL};
hFile = FindFirstFile("C:\\*.*", &wfs);
do
{
if (!(FILE_ATTRIBUTE_DIRECTORY & wfs.dwFileAttributes))
{
}
if (!FindNextFile(hFile, &wfs))
{
if (ERROR_NO_MORE_FILES == GetLastError()) {break;}
}
} while (NULL != hFile || INVALID_HANDLE_VALUE != hFile);
if (NULL != hFile) {FindClose(hFile);}
hFile = NULL;
Basic file access
Now, you have no problem in scanning any specified folder. You still need to know how to open the file and read its content into a local variable. Without this, this project will not be complete, as it fails to create the merge data file. But, life is easy. Since the WIN32 SDK does provide us the necessary file I/O API, all you need to understand and remember are the following few APIs:
WIN32 API |
description |
CreateFile |
Open or create a file on the disk drive. |
SetFilePointer |
Move the current open or create file pointer. |
ReadFile |
Read partial or full of the current open file content. |
WriteFile |
Write data into an open file. |
CloseHandle |
Close the current open file when it is no longer used. |
HANDLE hFile1 = NULL;
DWORD dwByteWrite = 0;
hFile1 = CreateFile("C:\\sample.txt",
GENERIC_WRITE,
FILE_SHARE_WRITE,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (NULL != hFile1 && INVALID_HANDLE_VALUE != hFile1)
{
SetFilePointer(hFile1, 0, 0, FILE_BEGIN);
WriteFile(hFile1,
"Hello world",
strlen("Hello world"),
&dwByteWrite,
NULL);
}
else
{
MessageBox(hWnd, "Fail to create file!",
APP_TITLE, MB_OK | MB_ICONSTOP);
}
if (NULL != hFile1) {CloseHandle(hFile1);}
hFile1 = NULL;
HANDLE hFile = NULL;
DWORD dwByteRead = 0;
DWORD dwFileSize = 0;
LPBYTE lpData = NULL;
hFile = CreateFile("C:\\sample.txt",
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (NULL != hFile && INVALID_HANDLE_VALUE != hFile)
{
dwFileSize = GetFileSize(hFile, NULL);
lpData = (LPBYTE)LocalAlloc(LPTR, dwFileSize);
ZeroMemory(lpData, dwFileSize);
SetFilePointer(hFile, 0, 0, FILE_BEGIN);
ReadFile(hFile, lpData, dwFileSize, &dwByteRead, NULL);
}
else
{
MessageBox(hWnd, "Fail to open file!",
APP_TITLE, MB_OK | MB_ICONSTOP);
}
if (NULL != hFile) {CloseHandle(hFile);}
hFile = NULL;
How to build the merge data file
At this stage, you have gain all the minimum knowledge on accessing the file I/O. Also, you get the idea how the final merge data file is being structured. So, it is the big time for the core task of the entire project... building the merge data file. Before I start explaining the most interesting part (program code) step by step, let's recap some of the important steps to build the merge data file.
Step |
Action |
1. |
Scan the specified folder to retrieve the individual file information. |
2. |
Save these read file information into a temporary file (let's say, temp1.tmp, note that the data writing sequence will be FIFO). |
3. |
Save these content into another temporary file (let's say, temp2.tmp, note that the data writing sequence will be FIFO). |
4. |
Repeat step #2-#3 until there is no more file available from the specified folder. |
5. |
Append the content in the second temporary file (temp2.tmp) into the end of the first temporary file (temp1.tmp). |
6. |
Compress the merge data file (temp1.tmp) into a new temporary file (temp3.tmp). |
7. |
Congratulations, you have successfully built your first merge data file. |
Note: In the following code snippet, I was filtering out those code related to GUI update. This GUI update is mainly to keep the user informed about the status of the entire building of the self-extract executable file process. If you wish to get more about this GUI update code, you can always refer to the full source code enclosed together with this article.
Basically, the following code snippet will cover step #1 and #4, which is a do
...while
loop to scan through the folder:
GetDlgItemText(hWnd, IDC_EDIT1, szBuffer1, sizeof(szBuffer1));
sprintf(szBuffer2,
"%s\\*.*",
szBuffer1);
hFile = FindFirstFile(szBuffer2, &wfs);
do
{
if (!(FILE_ATTRIBUTE_DIRECTORY & wfs.dwFileAttributes))
{
WriteSelfExtractHeader (hWnd, &wfs);
WriteSelfExtractBinData (hWnd, &wfs);
}
if (!FindNextFile(hFile, &wfs))
{
if (ERROR_NO_MORE_FILES == GetLastError()) {break;}
}
} while (NULL != hFile || INVALID_HANDLE_VALUE != hFile);
if (NULL != hFile) {FindClose(hFile);}
hFile = NULL;
Next block of code snippet will cover the reading & writing of the file information into the header block, as well as the code for reading & writing the content of the current file.
Note: The file size information is very important in the (SetupEx.exe) module, because this will be the key information that instructs the SetupEx.exe how much it should move the file pointer in order to get back to the original staring point of each individual distributed file.
void WriteSelfExtractHeader (HWND hWnd, LPWIN32_FIND_DATA lpFindFileData)
{
EXTRACTFILEINFO efi = {NULL};
HANDLE hFile = NULL;
DWORD dwByteWrite = 0;
__try
{
hFile = CreateFile(szTmpBinFile1,
GENERIC_WRITE,
FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (NULL == hFile || INVALID_HANDLE_VALUE == hFile)
{
hFile = CreateFile(szTmpBinFile1,
GENERIC_WRITE,
FILE_SHARE_WRITE,
NULL,
CREATE_NEW,
FILE_ATTRIBUTE_NORMAL,
NULL);
}
if (NULL != hFile && INVALID_HANDLE_VALUE != hFile)
{
ZeroMemory(&efi, sizeof(EXTRACTFILEINFO));
efi.dwIndex = dwFileCount;
CopyMemory(&efi.CreateTime,
&lpFindFileData->ftCreationTime, sizeof(FILETIME));
CopyMemory(&efi.LastAcessTime,
&lpFindFileData->ftLastAccessTime, sizeof(FILETIME));
CopyMemory(&efi.LastWriteTime,
&lpFindFileData->ftLastWriteTime, sizeof(FILETIME));
CopyMemory(&efi.dwFileSizeHigh,
&lpFindFileData->nFileSizeHigh , sizeof(DWORD));
CopyMemory(&efi.dwFileSizeLow,
&lpFindFileData->nFileSizeLow, sizeof(DWORD));
CopyMemory(&efi.szBinFileName,
&lpFindFileData->cFileName,
strlen(lpFindFileData->cFileName));
if (0 == dwFileCount)
SetFilePointer(hFile, sizeof(UPDATEINFO), 0, FILE_BEGIN);
else
SetFilePointer(hFile, 0, 0, FILE_END);
WriteFile(hFile,
&efi,
sizeof(EXTRACTFILEINFO),
&dwByteWrite,
NULL);
dwFileCount++;
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
}
if (NULL != hFile) {CloseHandle(hFile);}
hFile = NULL;
}
void WriteSelfExtractBinData (HWND hWnd, LPWIN32_FIND_DATA lpFindFileData)
{
HANDLE hFile = NULL;
LPBYTE lpData = NULL;
DWORD dwSize = 0;
DWORD dwByteRead = 0;
DWORD dwByteWrite = 0;
char szBuffer1[MAX_PATH] = {NULL};
char szBuffer2[MAX_PATH] = {NULL};
__try
{
GetDlgItemText(hWnd, IDC_EDIT1, szBuffer1, sizeof(szBuffer1));
sprintf(szBuffer2,
"%s\\%s",
szBuffer1, lpFindFileData->cFileName);
hFile = CreateFile(szBuffer2,
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (NULL != hFile && INVALID_HANDLE_VALUE != hFile)
{
dwSize = (lpFindFileData->nFileSizeHigh*(MAXDWORD+1)) +
lpFindFileData->nFileSizeLow;
lpData = (LPBYTE)LocalAlloc(LPTR, dwSize);
ZeroMemory(lpData, dwSize);
SetFilePointer(hFile, 0, 0, FILE_BEGIN);
ReadFile(hFile, lpData, dwSize, &dwByteRead, NULL);
}
if (NULL != hFile) {CloseHandle(hFile);}
hFile = NULL;
hFile = CreateFile(szTmpBinFile2,
GENERIC_WRITE,
FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (NULL == hFile || INVALID_HANDLE_VALUE == hFile)
{
hFile = CreateFile(szTmpBinFile2,
GENERIC_WRITE,
FILE_SHARE_WRITE,
NULL,
CREATE_NEW,
FILE_ATTRIBUTE_NORMAL,
NULL);
}
if (NULL != hFile && INVALID_HANDLE_VALUE != hFile)
{
SetFilePointer(hFile, 0, 0, FILE_END);
WriteFile(hFile,
lpData,
dwSize,
&dwByteWrite,
NULL);
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
}
if (NULL != lpData){LocalFree((LPBYTE)lpData);}
lpData = NULL;
if (NULL != hFile) {CloseHandle(hFile);}
hFile = NULL;
}
Now, we have both the header and content data file, and it is time to merge these 2 files into a single file before proceeding to compress it using the Zlib* algorithm.
if (TRUE == MergeSelfExtractData (hWnd))
{
if (0 == Compress(szTmpBinFile1, szTmpBinFile3))
{
}
}
else
MessageBox(hWnd,
"Fail to compress the self-extract file!",
APP_TITLE, MB_OK | MB_ICONSTOP);
BOOL MergeSelfExtractData (HWND hWnd)
{
UPDATEINFO ui = {NULL};
BOOL bResult = FALSE;
LPBYTE lpData = NULL;
HANDLE hFile = NULL;
DWORD dwSize = 0;
DWORD dwByteWrite = 0;
DWORD dwByteRead = 0;
__try
{
{
hFile = CreateFile(szTmpBinFile2,
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (NULL != hFile && INVALID_HANDLE_VALUE != hFile)
{
dwSize = GetFileSize(hFile, 0);
lpData = (LPBYTE)LocalAlloc(LPTR, dwSize);
ZeroMemory(lpData, dwSize);
SetFilePointer(hFile, 0, 0, FILE_BEGIN);
ReadFile(hFile, lpData, dwSize, &dwByteRead, NULL);
}
if (NULL != hFile) {CloseHandle(hFile);}
hFile = NULL;
}
hFile = CreateFile(szTmpBinFile1,
GENERIC_WRITE,
FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (NULL != hFile && INVALID_HANDLE_VALUE != hFile)
{
ui.dwFileCount = dwFileCount;
SetFilePointer(hFile, 0, 0, FILE_BEGIN);
WriteFile(hFile,
&ui,
sizeof(UPDATEINFO),
&dwByteWrite,
NULL);
SetFilePointer(hFile, 0, 0, FILE_END);
WriteFile(hFile,
lpData,
dwSize,
&dwByteWrite,
NULL);
bResult = TRUE;
}
else
bResult = FALSE;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
bResult = FALSE;
}
if (NULL != lpData){LocalFree((LPBYTE)lpData);}
lpData = NULL;
if (NULL != hFile) {CloseHandle(hFile);}
hFile = NULL;
return bResult;
}
Read data from custom resource table
Now, we have done the compressed merge data file. But before we can further make it become self-extract, we must require another module (SetupEx.exe) to perform a series of reverse processes. As listed at the beginning of this article, I'll not discuss more on this module. Because, what it does is basically the reverse process of what we have discussed above. If you know how to build the merge data file, sure it will not be a problem to understand the code (SetupEx.exe) that is used to restore the original file from a single compressed merge data file.
But before I move further, I have an important note for those wish to modify the SetupEx.exe. The following code snippet is the key code of the entire SetupEx.exe (restoring individual file from the merge data without distortion or loss of its original contents):
dwDataSize = (lpefi[dwIndex1].dwFileSizeHigh*(MAXDWORD+1)) +
lpefi[dwIndex1].dwFileSizeLow;
if (0 < dwDataSize)
{
SetFilePointer(hFile, 0, 0, FILE_BEGIN);
WriteFile(hFile,
&lpBinData[dwDataPtr],
dwDataSize,
&dwByteWrite,
NULL);
if (dwByteWrite != dwDataSize)
{
MessageBox(hWnd,
"Writing data error, updating process aborted!",
APP_TITLE, MB_OK | MB_ICONSTOP);
goto RollBack;
}
SetFileTime(hFile,
&lpefi[dwIndex1].CreateTime,
&lpefi[dwIndex1].LastAcessTime,
&lpefi[dwIndex1].LastWriteTime);
if (NULL != hFile) {CloseHandle(hFile);}
hFile = NULL;
dwDataPtr += dwDataSize;
}
The SetupEx.exe was embedded inside the SelfExtact.exe (the program that will create the self-extract executable file). So, we must first spawn the SetupEx.exe by reading its binary data from the custom resource table under the SelfExtract.exe (as shown in the figure below), then write it into a newly created file:

The code snippet below shows how we can spawn the SetupEx.exe from the binary data (IDR_EXTRACTOR1
) stored inside the custom resource table (EXTRACTOR).
dwResSize = SizeofResource(hInst, hResource);
hResData = LoadResource(hInst, hResource);
if (hResData != NULL && dwResSize != 0)
{
lpData = NULL;
lpData = LockResource(hResData);
hFile2 = CreateFile(szTmpBinFile4,
GENERIC_WRITE,
FILE_SHARE_WRITE,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (NULL != hFile2 && INVALID_HANDLE_VALUE != hFile2)
{
SetFilePointer(hFile2, 0, 0, FILE_BEGIN);
WriteFile(hFile2,
(LPBYTE)lpData,
dwResSize,
&dwByteWrite,
NULL);
if (NULL != hFile2) {CloseHandle(hFile2);}
hFile2 = NULL;
}
else
{
MessageBox(hWnd,
"Fail to spawning the self-extract kernel!",
APP_TITLE, MB_OK | MB_ICONSTOP);
goto CleanExit;
}
}
else
{
MessageBox(hWnd,
"Fail to read the self-extract kernel binary data!",
APP_TITLE, MB_OK | MB_ICONSTOP);
goto CleanExit;
}
Inject data into spawned SetupEx.exe
Now, we have spawned the SetupEx.exe from the custom resource table and we should go further by injecting the compressed merge data file into the custom resource table (SETUP) of SetupEx.exe as IDC_SETUP1
(shown in figure below):

From this figure, you will see the initial compressed merge data content within the SetupEx.exe was just 1 byte. This is just a dummy entry mainly for debugging purpose during coding stage (it must be replaced with an actual compressed merge data file). Also, with this IDR_SETUP1
entry, I believe it will help everyone to have a better understanding on how the SetupEx.exe works. After so much talking here and there about the custom resource, here is the code snippet on injecting the compressed merge data into the custom resource table (SETUP) of the spawned SetupEx.exe.
hFile2 = BeginUpdateResource (szTmpBinFile4, FALSE);
if (NULL != hFile2 && INVALID_HANDLE_VALUE != hFile2)
{
if (FALSE == UpdateResource (hFile2,
"SETUP",
MAKEINTRESOURCE(IDR_SETUP1),
MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
lpBinData,
dwFileSize))
{
MessageBox(hWnd,
"Fail to update the resource table" +
" of the self-extract executable file!",
APP_TITLE,
MB_OK | MB_ICONSTOP);
hFile2 = NULL;
goto CleanExit;
}
if (FALSE == EndUpdateResource (hFile2, FALSE))
{
MessageBox(hWnd,
"Fail to modify the resource table" +
" of the self-extract executable file!",
APP_TITLE,
MB_OK | MB_ICONSTOP);
hFile2 = NULL;
goto CleanExit;
}
hFile2 = NULL;
if (NULL != lpBinData) {LocalFree((LPBYTE)lpBinData);}
lpBinData = NULL;
}
else
{
MessageBox(hWnd,
"Fail to open the resource table " +
"of the self-extract executable file!",
APP_TITLE,
MB_OK | MB_ICONSTOP);
goto CleanExit;
}
After the injection, you should see the compressed merge data sit inside the SetupEx.exe by using the freeware tool (a tool you cannot miss!!!) Resource Hacker (freeware) Copyright � 1999-2002 Agnus Johnson.
The screen shot below shows the difference of the custom resource (IDR_SETUP1
) as compared to the initial 1 byte data only.


Note: The value of 2000 you saw in the above 2 screenshots was equivalent to IDR_SETUP1
, as this was pre-defined under the resource.h of both the SetupEx.exe and SelfExtract.exe project files. So, the merge data file must be injected into this entry. Otherwise, the SetupEx.exe will not be able to extract the injected merge data. Because it will remain locating the initial IDR_SETUP1
from the newly spawned SetupEx.exe which only has 1 byte data inside.
#define IDR_SETUP1 2000
Also, the value of 1033 you saw from the screenshot above is another key item, and you must carefully check and verify this value before you start modifying either SetupEx.exe or SelfExtract.exe. Because, this value means the current custom resource table language identifier. For instance, 1033 means primary language is ENGLISH (0x09), and sublanguage is ENGLISH US (0x01). Please refer to the MSDN Library (by searching for the MAKELANGID
API) to get more information about the available language identifier.
Chances for injecting data into different resource tables
UpdateResource
API was used in this project for injecting the compressed merge data file into the spawned SetupEx.exe in order to complete the self-extract executable file.
if (FALSE == UpdateResource (hFile2,
"SETUP",
MAKEINTRESOURCE(IDR_SETUP1),
MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
lpBinData,
dwFileSize))
From this code snippet, the second, third, and forth arguments will be the key values which will cause merge data file being injected into a wrong resource table. Hence, I will explain each of these scenario one-by-one.
For the first scenario, if "SETUP" was changed to something like "ABC", the output will be like the 2 screenshots below. The differences can be clearly identified (there was an extra custom resource name "ABC"). As a result, the spawned self-extract executable will remain locating and loading the original dummy data stored inside "SETUP" (custom resource). Because, the compressed merge data was injected into "ABC" custom resource table instead of the pre-defined "SETUP".
Basically, "SETUP" represents the custom resource group name and it is very important when calling the UpdateResource
API.
if (FALSE == UpdateResource (hFile2,
"ABC",
MAKEINTRESOURCE(IDR_SETUP1),
MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
lpBinData,
dwFileSize))


In the second scenario, if the MAKEINTRESOURCE(IDR_SETUP1)
was changed to something like 2001 (MAKEINTRESOURCE(IDR_SETUP1)
will return the value of 2000, as IDR_SETUP1
was defined as 2000 under the resource.h file), the output will be like the 2 screenshots below. The differences can be clearly identified (there was an extra custom resource identifier 2001). As a result, the spawned self-extract executable will remain locating and loading the original dummy data stored inside the 2000 custom resource. Because, the compressed merge data was injected into the 2001 custom resource instead of the pre-defined 2000.
The 2001 represents the custom resource identifier and it is also very important when calling the UpdateResource
API. A wrong value will collapse the entire self-extract executable file.
if (FALSE == UpdateResource (hFile2,
"SETUP",
"2001",
MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
lpBinData,
dwFileSize))


For the last scenario, if the MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US)
was changed to something like MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_UK)
, the output will be like the 2 screenshots below. The differences can be clearly identified (there was an extra custom resource identifier 2000 with the value of 2057 on the right hand side instead of 1033). As a result, the spawned self-extract executable will remain locating and loading the original dummy data stored inside the 2000 custom resource with language ID of 1033, and not the newly injected one with the same custom resource identifier but different language ID (2057).
The MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US)
represents the language ID for the respective custom resource. So, it is another key factor when calling the UpdateResource
API. A wrong value will collapse the entire self-extract executable file as well.
if (FALSE == UpdateResource (hFile2,
"SETUP",
MAKEINTRESOURCE(IDR_SETUP1),
MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_UK),
lpBinData,
dwFileSize))


Besides the actual language identifier, you also can use the language neutral identifier to inject the compressed merge data into the spawned SetupEx.exe. It still will work, although the compressed merge data was injected into the same group of custom resource table but under different language identifier 0 instead of 1033.
For this, I am still searching for the answer about why it will work even though the LANG_NEUTRAL
and SUBLANG_NEUTRAL
language identifiers were used during the injection with UpdateResource
API, which does not match with the language identifier specified inside the SetupEx.exe.
if (FALSE == UpdateResource (hFile2,
"SETUP",
MAKEINTRESOURCE(IDR_SETUP1),
MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL),
lpBinData,
dwFileSize))


Please refer to the MAKELANGID
API in MDSN library for the full list of Primary Language Identifier and Sub Language Identifier.
The language identifier
Since there is a dummy custom resource embedded inside the SetupEx.exe module, we can find out what language identifier is used by this custom resource, via the VC++ 6.0 IDE during design time, or the Resource Hacker (freeware) during runtime.


Verify the injected data
Everything we coded must go through a verification process and the tool that best fits to this project need is the Resource Hacker (freeware). The contribution of this tool in this project was shown on those screenshots in the previous 2 sections.
Good and bad things in this project
When it comes to real world, everything will have its own good and bad sides. As for this project, I foresee the following shortage and some enhancements which can be implemented in the next phase.
The bad thing is, the SelfExtract.exe will not be able run under Windows 95/98/ME platforms. Because the UpdateResource
is not supported in these 3 platforms.
While the good things are, it leaves room for you to keep upgrading this project to support sub-folder scanning features. If you apply this project for application updating module like what I did, you are always given a chance to design your own update module GUI (modify the SetupEx.exe) or do extra processing like unloading the relevant application prior to updating the program file as well as updating the necessary registry which is related to your application.
The tool you must download
Last but not least, you must have the Resource Hacker (freeware) tool with you when you are reading, using or modifying this project's source code. Otherwise, you will be lost and will have no idea about what I have written in this article.
Revision history
Thanks for John's suggestion, because this update was based on his good suggestion.
Okay, I have implemented the "auto-exec" capability for the user defined file after extraction process was completed. Basically, I just replace the first 4 bytes (DWORD
) value into a defined structure SETUPINFO
which will be including both the original file count information as well as the "auto-exec" filename information (please refer to the new section Information stored in the setup info block for details). Subsequently the next change made was adding the ShellExecute
API code right after successfully extracting all the files into the user defined destination folder.
Note, I only resubmit the source code (selfextract_src.zip), and those files inside the demo zip file will remain unchanged. Which means it does not have the "auto-exec" capability. So, you are required to build at least 1 sample self-extract EXE in order to see how this new implemented capability looks like.
P/s: John, your first suggestion will need more time to implement. Because, it involves the change of the GUI to support destination folder selection. So, will have another round of update for this.
History
- Updated @ 18th May, 2004 09:09:45 AM UTC Time
I have encountered some problem in uploading the modified source code, please be patient & will get it done ASAP.
- Updated @ 18th May, 2004 10:04:58 AM UTC Time
Finally, updated source code was posted.
- Updated @ 19th May, 2004 01:18:45 AM UTC Time