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.
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:
|
2. | SetupEx.exe | Core module #2, which will go through the following (post-distribute) stages:
|
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 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:
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.
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;
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 { // Running index value of the current file out of the // total distributed file count. DWORD dwIndex; // Original file created time. FILETIME CreateTime; // Original file last read/written time. FILETIME LastAcessTime; // Original file last written time. FILETIME LastWriteTime; // Specifies the high-order DWORD value // of the file size, in bytes. DWORD dwFileSizeHigh; // Specifies the low-order DWORD // value of the file size, in bytes. DWORD dwFileSizeLow; // A null-terminated string that // is the name of the original file. 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 // Original file attributes. DWORD dwFileAttributes; // Indicate the file require // (most likely is *.DLL) // is require register or not. BOOL bDllSelfRegister; } EXTRACTFILEINFO, FAR * LPEXTRACTFILEINFO;
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}; // Get the specified file information by using // the FindFirstFile instead of GetFileAttributesEx hFile = FindFirstFile("C://SelfExtract.exe", &wfs); // Varify the reutn find file handle if (NULL != hFile || INVALID_HANDLE_VALUE != hFile) { // Copy those file information you need // into your define local/global variable here. } // Close the search file handle if (NULL != hFile) {FindClose(hFile);} hFile = NULL;
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}; // Start scaning the directory hFile = FindFirstFile("C://*.*", &wfs); // Check the return handle value do { // Check is the current found file is directory? if (!(FILE_ATTRIBUTE_DIRECTORY & wfs.dwFileAttributes)) { // Put your code to read the current file information here. } // Scan the next match item in the directory if (!FindNextFile(hFile, &wfs)) { if (ERROR_NO_MORE_FILES == GetLastError()) {break;} } } while (NULL != hFile || INVALID_HANDLE_VALUE != hFile); // Close the search file handle if (NULL != hFile) {FindClose(hFile);} hFile = NULL;
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. |
// Create new file and write data into it. HANDLE hFile1 = NULL; DWORD dwByteWrite = 0; // Create a new text file hFile1 = CreateFile("C://sample.txt", GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); // Check the return handle value if (NULL != hFile1 && INVALID_HANDLE_VALUE != hFile1) { // Move the file pointer to the begin of the file SetFilePointer(hFile1, 0, 0, FILE_BEGIN); // Write the data into newly create file WriteFile(hFile1, "Hello world", strlen("Hello world"), &dwByteWrite, NULL); } else { // Notify user about the error MessageBox(hWnd, "Fail to create file!", APP_TITLE, MB_OK | MB_ICONSTOP); } // Close the current open data file if (NULL != hFile1) {CloseHandle(hFile1);} hFile1 = NULL;
// Open existing file and read the contents. HANDLE hFile = NULL; DWORD dwByteRead = 0; DWORD dwFileSize = 0; LPBYTE lpData = NULL; // Open the existing file hFile = CreateFile("C://sample.txt", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); // Check the return handle value if (NULL != hFile && INVALID_HANDLE_VALUE != hFile) { // Get the current file size dwFileSize = GetFileSize(hFile, NULL); // Allocate local data buffer lpData = (LPBYTE)LocalAlloc(LPTR, dwFileSize); // Reset local data buffer ZeroMemory(lpData, dwFileSize); // Move the file pointer to the begining SetFilePointer(hFile, 0, 0, FILE_BEGIN); // Read the data from the current open file ReadFile(hFile, lpData, dwFileSize, &dwByteRead, NULL); } else { // Notify user about the error MessageBox(hWnd, "Fail to open file!", APP_TITLE, MB_OK | MB_ICONSTOP); } // Close the open file handle if (NULL != hFile) {CloseHandle(hFile);} hFile = NULL;
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:
// Get the current user define source folder GetDlgItemText(hWnd, IDC_EDIT1, szBuffer1, sizeof(szBuffer1)); // Format the full path for the source folder sprintf(szBuffer2, "%s//*.*", szBuffer1); // Start scaning the directory hFile = FindFirstFile(szBuffer2, &wfs); // Check the return handle value do { // Check is the current found file is directory? if (!(FILE_ATTRIBUTE_DIRECTORY & wfs.dwFileAttributes)) { // Save the information into data file WriteSelfExtractHeader (hWnd, &wfs); // Save the information into data file WriteSelfExtractBinData (hWnd, &wfs); } // Scan the next match item in the directory if (!FindNextFile(hFile, &wfs)) { if (ERROR_NO_MORE_FILES == GetLastError()) {break;} } } while (NULL != hFile || INVALID_HANDLE_VALUE != hFile); // Close the search handle 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 { // Open the existing temporary data file hFile = CreateFile(szTmpBinFile1, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); // Check the return handle value if (NULL == hFile || INVALID_HANDLE_VALUE == hFile) { // Open the existing temporary data file hFile = CreateFile(szTmpBinFile1, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL); } // Check the return handle value if (NULL != hFile && INVALID_HANDLE_VALUE != hFile) { // Reset the local EXTRACTFILEINFO structure ZeroMemory(&efi, sizeof(EXTRACTFILEINFO)); // Initialize the EXTRACTFILEINFO structure 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)); // Check current file count & move the file pointer if (0 == dwFileCount) SetFilePointer(hFile, sizeof(UPDATEINFO), 0, FILE_BEGIN); else SetFilePointer(hFile, 0, 0, FILE_END); // Write the data into setup list file WriteFile(hFile, &efi, sizeof(EXTRACTFILEINFO), &dwByteWrite, NULL); // Increate the counter dwFileCount++; } } __except (EXCEPTION_EXECUTE_HANDLER) { // PUT YOUR ERROR HANDLING CODE HERE } // Close the open file handle 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 { // Get the user define source folder GetDlgItemText(hWnd, IDC_EDIT1, szBuffer1, sizeof(szBuffer1)); // Format the full file path sprintf(szBuffer2, "%s//%s", szBuffer1, lpFindFileData->cFileName); // STAGE #1 // Read the current binary file data hFile = CreateFile(szBuffer2, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); // Check the return handle value if (NULL != hFile && INVALID_HANDLE_VALUE != hFile) { // Get the current file size dwSize = (lpFindFileData->nFileSizeHigh*(MAXDWORD+1)) + lpFindFileData->nFileSizeLow; // Allocate local data buffer lpData = (LPBYTE)LocalAlloc(LPTR, dwSize); // Reset local data buffer ZeroMemory(lpData, dwSize); // Move the file pointer to the begining SetFilePointer(hFile, 0, 0, FILE_BEGIN); // Read the binary data ReadFile(hFile, lpData, dwSize, &dwByteRead, NULL); } // Close the open file handle if (NULL != hFile) {CloseHandle(hFile);} hFile = NULL; // STAGE #2 // WRITE THE READ BINDARY DATA INTO THE TEMPORARY FILE // Open the existing setup data file (szTmpBinFile2) hFile = CreateFile(szTmpBinFile2, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); // Check the return handle value if (NULL == hFile || INVALID_HANDLE_VALUE == hFile) { // Open the existing setup.lst data file hFile = CreateFile(szTmpBinFile2, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL); } // Check the return handle value if (NULL != hFile && INVALID_HANDLE_VALUE != hFile) { // Move the file pointer SetFilePointer(hFile, 0, 0, FILE_END); // Write the data into setup list file WriteFile(hFile, lpData, dwSize, &dwByteWrite, NULL); } } __except (EXCEPTION_EXECUTE_HANDLER) { // PUT YOUR ERROR HANDLING CODE HERE } // Release the allocated data buffer if (NULL != lpData){LocalFree((LPBYTE)lpData);} lpData = NULL; // Close the open file handle 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.
// Begin to merge the header and data file if (TRUE == MergeSelfExtractData (hWnd)) { // Compress the current data file to the user define location & // Notify user about the process is completed if (0 == Compress(szTmpBinFile1, szTmpBinFile3)) { // Proceed to spawn the SetupEx.exe // Proceed to inject the merge data // file into the spawned SetupEx.exe } } else // Notify user about the error 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 { // STAGE #1 { // Read the current binary file data hFile = CreateFile(szTmpBinFile2, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); // Check the return handle value if (NULL != hFile && INVALID_HANDLE_VALUE != hFile) { // Get the current file size dwSize = GetFileSize(hFile, 0); // Allocate local data buffer lpData = (LPBYTE)LocalAlloc(LPTR, dwSize); // Reset local data buffer ZeroMemory(lpData, dwSize); // Move the file pointer to the begining SetFilePointer(hFile, 0, 0, FILE_BEGIN); // Read the binary data ReadFile(hFile, lpData, dwSize, &dwByteRead, NULL); } // Close the open file handle if (NULL != hFile) {CloseHandle(hFile);} hFile = NULL; } // STAGE #2 // Open the existing temporary data file hFile = CreateFile(szTmpBinFile1, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); // Check the return handle value if (NULL != hFile && INVALID_HANDLE_VALUE != hFile) { // Setup the UPDATEINFO structure ui.dwFileCount = dwFileCount; // Move the file pointer SetFilePointer(hFile, 0, 0, FILE_BEGIN); // Write the total binary data file being included WriteFile(hFile, &ui, sizeof(UPDATEINFO), &dwByteWrite, NULL); // Move the file pointer SetFilePointer(hFile, 0, 0, FILE_END); // Append the actual binary data from the temp file WriteFile(hFile, lpData, dwSize, &dwByteWrite, NULL); // Set return value bResult = TRUE; } else // Set return value bResult = FALSE; } __except (EXCEPTION_EXECUTE_HANDLER) { // PUT YOUR ERROR HANDLING CODE HERE // Set default return value bResult = FALSE; } // Release the allocated data buffer if (NULL != lpData){LocalFree((LPBYTE)lpData);} lpData = NULL; // Close the open file handle if (NULL != hFile) {CloseHandle(hFile);} hFile = NULL; // Return local result return bResult; }
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):
// Get the current data file size base on the information // store in the lpefi: // lpefi[dwIndex1].dwFileSizeHigh // lpefi[dwIndex1].dwFileSizeLow // dwDataSize = (lpefi[dwIndex1].dwFileSizeHigh*(MAXDWORD+1)) + lpefi[dwIndex1].dwFileSizeLow; // Ensure the file size of not 0Byte if (0 < dwDataSize) { // Move the file pointer to the begin of the file SetFilePointer(hFile, 0, 0, FILE_BEGIN); // Write data into a temp file WriteFile(hFile, &lpBinData[dwDataPtr], dwDataSize, &dwByteWrite, NULL); // Ensure the byte write is equal to the calculated data size if (dwByteWrite != dwDataSize) { // Notify user about the error MessageBox(hWnd, "Writing data error, updating process aborted!", APP_TITLE, MB_OK | MB_ICONSTOP); // Jump the the "RollBack" routine goto RollBack; } // Update the filetime information SetFileTime(hFile, &lpefi[dwIndex1].CreateTime, &lpefi[dwIndex1].LastAcessTime, &lpefi[dwIndex1].LastWriteTime); // Close the current open file handle if (NULL != hFile) {CloseHandle(hFile);} hFile = NULL; // Increate the local data pointer (dwDataPtr) 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).
// Get the total resource size dwResSize = SizeofResource(hInst, hResource); // Load the resource content hResData = LoadResource(hInst, hResource); // Checking if (hResData != NULL && dwResSize != 0) { // Ensure the lpData is NULL lpData = NULL; // Obtain the string pointer from the loaded resource handle lpData = LockResource(hResData); // Save the current read data into a file hFile2 = CreateFile(szTmpBinFile4, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); // Check the return handle value if (NULL != hFile2 && INVALID_HANDLE_VALUE != hFile2) { // Move the file pointer to the begin of the file SetFilePointer(hFile2, 0, 0, FILE_BEGIN); // Write the read data into a temp file WriteFile(hFile2, (LPBYTE)lpData, dwResSize, &dwByteWrite, NULL); // Close the current open data file if (NULL != hFile2) {CloseHandle(hFile2);} hFile2 = NULL; } else { // Notify user about the error MessageBox(hWnd, "Fail to spawning the self-extract kernel!", APP_TITLE, MB_OK | MB_ICONSTOP); // Jump the the "CleanExit" routine goto CleanExit; } } else { // Notify user about the error MessageBox(hWnd, "Fail to read the self-extract kernel binary data!", APP_TITLE, MB_OK | MB_ICONSTOP); // Jump the the "CleanExit" routine goto CleanExit; }
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.
// Open the file require to alter the resource table hFile2 = BeginUpdateResource (szTmpBinFile4, FALSE); // Check the return handle value if (NULL != hFile2 && INVALID_HANDLE_VALUE != hFile2) { // Update the file resource table if (FALSE == UpdateResource (hFile2, "SETUP", MAKEINTRESOURCE(IDR_SETUP1), MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), lpBinData, dwFileSize)) { // Notify user about the error MessageBox(hWnd, "Fail to update the resource table" + " of the self-extract executable file!", APP_TITLE, MB_OK | MB_ICONSTOP); // Reset the local variable hFile2 = NULL; // Jump the the "CleanExit" routine goto CleanExit; } // Close the modify file if (FALSE == EndUpdateResource (hFile2, FALSE)) { // Notify user about the error MessageBox(hWnd, "Fail to modify the resource table" + " of the self-extract executable file!", APP_TITLE, MB_OK | MB_ICONSTOP); // Reset the local variable hFile2 = NULL; // Jump the the "CleanExit" routine goto CleanExit; } // Reset the local variable hFile2 = NULL; // Release the allocated memory if (NULL != lpBinData) {LocalFree((LPBYTE)lpBinData);} lpBinData = NULL; } else { // Notify user about the error MessageBox(hWnd, "Fail to open the resource table " + "of the self-extract executable file!", APP_TITLE, MB_OK | MB_ICONSTOP); // Jump the the "CleanExit" routine 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.
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.
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.
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.
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.
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.
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.
I have encountered some problem in uploading the modified source code, please be patient & will get it done ASAP.
Finally, updated source code was posted.
This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.
A list of licenses authors might use can be found here