Click here to Skip to main content
15,867,834 members
Articles / Programming Languages / C++
Article

Pure WIN32 Self-Extract EXE Builder

Rate me:
Please Sign up or sign in to vote.
4.88/5 (39 votes)
15 May 200418 min read 194.4K   7.5K   160   24
Build your first distributed self-extract executable file from ground up.

Sample Image - selfextract70.jpg

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)!

ItemModuleDescription
1.SelfExtract.exe

Core module #1, which will go through the following (pre-distribute) stages:

  1. Provide GUI which enables the user to specify the source folder and output executable filename.
  2. Scan the source file from the user specified folder.
  3. Build the source file information block and combine individual source files data into a binary file.
  4. Merge the source file information block and binary data into a single file.
  5. Compress the merged binary file.
  6. Spawn the SetupEx.exe from the embedded custom resource table (EXTRACTOR).
  7. Inject compressed binary data into the SetupEx.exe custom resource table (SETUP).
  8. Copy the final file into the user specified file path and name.
  9. Delete all the created temporary files and release all the allocated memory.
  10. Notify user about the process completed.
2.SetupEx.exe

Core module #2, which will go through the following (post-distribute) stages:

  1. Provide GUI which enables the user to specify the output folder.
  2. Read the compressed binary data from custom resource table (SETUP) and write into a temporary file.
  3. Uncompress the binary data.
  4. Read the source file information block from the temporary file.
  5. Read individual block of binary data based on the file size specified in the source file information block.
  6. Write the data into the temporary file using the filename stored under the source file information block.
  7. Update the temporary file's file time with the value stored under the source file information block.
  8. Copy the temporary file from temporary folder into the user specified output folder.
  9. Delete all the created temporary files and release all the allocated memory.
  10. 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:

SectionDescription
Setup InfoInclude 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.
HeaderN block of EXTRACTFILEINFO, where N will be the value (dwFileCount) stored in the Setup Info section.
DataN 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:

Merge Data File Format.

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
{
    // 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.

C++
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;

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};

// 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;

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};

// 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;

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 APIdescription
CreateFileOpen or create a file on the disk drive.
SetFilePointerMove the current open or create file pointer.
ReadFileRead partial or full of the current open file content.
WriteFileWrite data into an open file.
CloseHandleClose 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;

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.

StepAction
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;
}

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):

// 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:

Embedded SetupEx.exe within the SelfExtract.exe.

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;
}

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):

Embedded compressed merge data within the SetupEx.exe.

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.

Before inject compressed merge data within the SetupEx.exe.

After inject compressed merge data within the SetupEx.exe.

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))

Before inject compressed merge data within the SetupEx.exe.

After inject compressed merge data within the SetupEx.exe.

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))

Before inject compressed merge data within the SetupEx.exe.

After inject compressed merge data within the SetupEx.exe.

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))

Before inject compressed merge data within the SetupEx.exe.

After inject compressed merge data within the SetupEx.exe.

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))

Before inject compressed merge data within the SetupEx.exe.

Before inject compressed merge data within the SetupEx.exe.

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.

Before inject compressed merge data within the SetupEx.exe.

Before inject compressed merge data within the SetupEx.exe.

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

License

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


Written By
Software Developer
Australia Australia
Passion to be a software architect and solution researcher in enterprise solutions by simplify and unify the existing complex manual paper works into an automated environment friendly, comprehensive and dynamic workflow process system.

Comments and Discussions

 
QuestionNot working in Windows 7 Pin
Member 1023629813-Oct-13 12:58
Member 1023629813-Oct-13 12:58 
Questionperfect Pin
marc ochsenmeier17-Apr-12 20:19
marc ochsenmeier17-Apr-12 20:19 
GeneralYou are simply superb! Pin
csteven16-Jun-08 20:17
csteven16-Jun-08 20:17 
GeneralWarning! No sub-folder scanning! Pin
John Titor24-Feb-06 12:59
John Titor24-Feb-06 12:59 
QuestionLinks for self extract. Pin
mail4johnv8-Dec-05 20:11
mail4johnv8-Dec-05 20:11 
GeneralMemory Allocation Problem Pin
mykroft4628-Aug-05 4:20
mykroft4628-Aug-05 4:20 
GeneralVery nice! Pin
Bob Stanneveld9-Jun-05 9:29
Bob Stanneveld9-Jun-05 9:29 
GeneralRe: Very nice! Pin
CT CHANG9-Jun-05 14:30
CT CHANG9-Jun-05 14:30 
GeneralRe: Very nice! Pin
Bob Stanneveld9-Jun-05 21:56
Bob Stanneveld9-Jun-05 21:56 
GeneralMS CAB SDK Pin
ramensky20-May-04 12:24
ramensky20-May-04 12:24 
GeneralRe: MS CAB SDK Pin
CT CHANG20-May-04 15:14
CT CHANG20-May-04 15:14 
GeneralUpdateResource Problem Pin
Blake Miller18-May-04 5:14
Blake Miller18-May-04 5:14 
GeneralRe: UpdateResource Problem Pin
CT CHANG18-May-04 5:52
CT CHANG18-May-04 5:52 
GeneralNice work! Pin
Abbas_Riazi17-May-04 1:46
professionalAbbas_Riazi17-May-04 1:46 
GeneralRe: Nice work! Pin
CT CHANG17-May-04 5:29
CT CHANG17-May-04 5:29 
Generalreally nice! Pin
andyj11516-May-04 23:36
andyj11516-May-04 23:36 
GeneralRe: really nice! Pin
CT CHANG16-May-04 23:58
CT CHANG16-May-04 23:58 
GeneralRe: really nice! Pin
andyj11517-May-04 0:27
andyj11517-May-04 0:27 
GeneralRe: really nice! Pin
CT CHANG17-May-04 2:17
CT CHANG17-May-04 2:17 
GeneralRe: really nice! (the first point) Pin
andyj11517-May-04 3:17
andyj11517-May-04 3:17 
GeneralRe: really nice! (the first point) Pin
CT CHANG17-May-04 5:37
CT CHANG17-May-04 5:37 
GeneralRe: really nice! (the first point) Pin
Anonymous28-Oct-04 14:36
Anonymous28-Oct-04 14:36 
Questionwhat a pitty? Pin
Anonymous16-May-04 15:19
Anonymous16-May-04 15:19 
AnswerRe: what a pitty? Pin
CT CHANG16-May-04 15:39
CT CHANG16-May-04 15:39 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.