Click here to Skip to main content
15,895,557 members
Articles / Web Development / HTML

Cabinet File (*.CAB) Compression and Extraction

Rate me:
Please Sign up or sign in to vote.
4.93/5 (217 votes)
23 Mar 2012CPOL38 min read 3.2M   26.5K   573  
How to implement creation and extraction of Microsoft CAB files
//////////////////////////////////////////////////////////////////////////////////
// Author: Elm� (www.netcult.ch/elmue)
// Date: 19-04-2008
//
// Filename: Compress.hpp
//
// Classes:
// - CCompress
//
// Purpose: This class compresses files into a cabinet file (CAB).
//
/////////////////////////////////////////////////////////////////////////////////
/*
----------------------------------------------------------------------------------
Using these conventions results in better readable code and less coding errors !
----------------------------------------------------------------------------------

     cName  for generic class definitions
     CName  for MFC     class definitions
     tName  for type    definitions
     eName  for enum    definitions
     kName  for struct  definitions

    e_Name  for enum variables
    E_Name  for enum constant values

    i_Name  for instances of classes
    h_Name  for handles

    T_Name  for Templates
    t_Name  for TCHAR or LPTSTR

    s_Name  for strings
   sa_Name  for Ascii strings
   sw_Name  for Wide (Unicode) strings
   bs_Name  for BSTR
    f_Name  for function pointers
    k_Name  for constructs (struct)

    b_Name  bool,BOOL 1 Bit

   s8_Name    signed  8 Bit (char)
  s16_Name    signed 16 Bit (SHORT, WCHAR)
  s32_Name    signed 32 Bit (LONG, int)
  s64_Name    signed 64 Bit (LONGLONG)

   u8_Name  unsigned  8 Bit (BYTE)
  u16_Name  unsigned 16 bit (WORD)
  u32_Name  unsigned 32 Bit (DWORD, UINT)
  u64_Name  unsigned 64 Bit (ULONGLONG)

    d_Name  for double

  ----------------

    m_Name  for member variables of a class (e.g. ms32_Name for int member variable)
    g_Name  for global (static) variables   (e.g. gu16_Name for global WORD)
    p_Name  for pointer                     (e.g.   ps_Name  *pointer to string)
   pp_Name  for pointer to pointer          (e.g.  ppd_Name **pointer to double)

*************************************************************************************/

#pragma once


#include <io.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <errno.h>
#include "FCI.h"

#include "Defines.h"
#include "Static.hpp"
#include "File.hpp"
#include "Trace.hpp"
#include "Error.hpp"
#include "Blowfish.hpp"

#pragma warning(disable: 4996)

namespace Cabinet
{

class CCompress
{
public:

    enum eCompress
    {
        E_ComprNONE  = tcompTYPE_NONE,                     // store files in CAB without compression
        E_ComprMSZIP = tcompTYPE_MSZIP,                    // MSZIP compression
        E_ComprLZX   = tcompTYPE_LZX | tcompLZX_WINDOW_HI, // highest LZX compression
        // Quantum is not implemented in Cabinet.dll
    };

    typedef struct kCurStatus
    {
        ULONG u32_TotCompressedSize;
        ULONG u32_TotUncompressedSize;
        ULONG cb1;
        ULONG cb2;
        ULONG FolderPercent;
    };

    struct kCallbacks
    {
        typedef int  (*t_FilePlaced)    (PCCAB, WCHAR*, int, BOOL, void*);
        typedef BOOL (*t_GetNextCabinet)(PCCAB, ULONG, char*, void*);
        typedef int  (*t_UpdateStatus)  (UINT, kCurStatus*, void*);
        typedef BOOL (*t_OnBeforePack)  (const WCHAR*);
        
        t_FilePlaced     f_FilePlaced;
        t_GetNextCabinet f_GetNextCabinet;
        t_UpdateStatus   f_UpdateStatus;
        t_OnBeforePack   f_OnBeforePack; // only used for Folder compression (return FALSE -> omit file)

        kCallbacks()
        {
            f_FilePlaced     = 0;
            f_GetNextCabinet = 0;
            f_UpdateStatus   = 0;
            f_OnBeforePack   = 0;
        }
    };

    // Constructor
    CCompress() : mi_Error(FALSE)
    { 
        #if _TraceCompress
            CTrace::TraceW(L"Constructor CCompress()");
        #endif

        mh_FCIContext = 0;
        mh_CabinetDll = 0;
        mu32_ThreadID = 0;

        SetTempDirectoryW(L""); // Set Windows default Temp directory

        // The following two variables are used to generate the filename of the temp files
        // TempCounter is incremented with every new temp file
        mu32_TempCounter = 1;

        // ClassID is incremented with every new class instance.
        // There may be multiple class instances in the same thread, so we don't use the ThreadID here
        mu32_ClassID = InterlockedIncrement(&CStatic::ms32_CompressClassID);

        SetCryptBOM("CRYP"); // Set the default value
    }

    // Destructor
    // Deletes all created temp files and flushes the cabinet from memory to disk if
    // the user did not yet call FlushCabinet()
    ~CCompress()
    { 
        #if _TraceCompress
            CTrace::TraceW(L"Destructor ~CCompress()");
        #endif

        DestroyFCIContext();
        // Decrement the reference count for the DLL.
        // If another instance is still using the DLL the DLL will not be unloaded!
        if (mh_CabinetDll) FreeLibrary(mh_CabinetDll);
    }

    // This function aborts the currently active operation.
    void AbortOperation()
    { 
        mb_Abort = TRUE;
    }

    // You can specify where to store the temp files which can become very huge if you compress huge files
    // If this function is never called or called with "" or 0, the default Windows Temp directory will be used
    BOOL SetTempDirectoryW(const CStrW& sw_TempDir)
    {
        if (sw_TempDir[0])
        {
            msw_TempDir = sw_TempDir;
        }
        else
        {
            msw_TempDir.Allocate(MAX_PATH);
            GetTempPathW(MAX_PATH, msw_TempDir);
        }
        
        CFile::TerminatePathW(msw_TempDir);

        UINT u32_Error = CFile::CreateFolderTreeW(msw_TempDir);
        if  (u32_Error)
        {
            mi_Error.Set(FCIERR_CREATE_DIR, u32_Error,0);
            return FALSE;
        }
        return TRUE;
    }
    
    // Sets the key for encryption of the CAB file
    // You can pass ANY binary data here, an ANSII string or an Unicode string
    // If the password is longer  than 72 Byte, the remaining bytes will be ignored
    // If the password is shorter than 72 Byte, its bytes will be reused multiple times
    // u32_KeyLen is always the count in Bytes (for Unicode strings = characters * 2)
    // Passing u32_KeyLen = 0 turns off encryption
    // ATTENTION:
    // It is recommended not to pass the password directly to this function!
    // You should derive for example a HASH from the password and use this instead.
    // See http://en.wikipedia.org/wiki/Key_derivation_function
    void SetEncryptionKey(void* p_Key, DWORD u32_KeyLen)
    {
        mk_Encrypt.i_Blowfish.SetPassword(p_Key, u32_KeyLen);

        if (!mk_Encrypt.u8_Buf)
        {
            mk_Encrypt.u8_Buf = new BYTE[CRYPT_BUFFER_SIZE];
            if (!mk_Encrypt.u8_Buf) throw "Fatal error: Out of memory!"; // Required for older Visual Studio versions
        }
    }

    // Set the BOM = first 4 Bytes in the CAB file that identify an encrypted file.
    // You may change the default "CRYP" to other characters here but don't forget to do the same in CExtract, too!
    // NOTE: A not encrypted CAB file is identified by "MSCF" (MicroSoftCabFile)
    BOOL SetCryptBOM(char* s8_BOM)
    {
        if (strlen(s8_BOM) != 4 || strcmp(s8_BOM, "MSCF") == 0)
            return FALSE;

        memcpy(mk_Encrypt.s8_BOM, s8_BOM, 4);
        return TRUE;
    }

    // Sets up to 3 static callbacks which are called from Cabinet.dll during the compression process
    void SetCallbacks(kCallbacks* pk_Callbacks)
    {
        mk_Callbacks = *pk_Callbacks;
    }

    // Creates a new FCI context for a single CAB file 
    // You can modify the specific settings for the sub-CAB files in GetNextCabinet()
    // b_UtcTime = TRUE -> store the files with UTC time in the CAB file (recommended)
    // Windows stores files with UTC time. If you compress with local time you will have
    // a discrepancy of one hour or more after daylight saving or timezone changes
    // b_EncodeUTF=TRUE  -> If a filename has characters > 0x7F, encode it using UTF8
    // b_EncodeUTF=FALSE -> Store ANSI filenames unchanged, read documentation!
    BOOL CreateFCIContextW(const CStrW& sw_CabFile,                   // "C:\Temp\Packed.cab"
                           BOOL          b_UtcTime   = TRUE,          // Store files with UTC time (recommended)
                           BOOL          b_EncodeUtf = TRUE,          // Store filenames UTF8 encoded (recommended)
                           ULONG       u32_CabSplitSize = 0x7FFFFFFF, // The split filesize where to start a new CAB file
                           USHORT      u16_CabID = 0,                 // an ID to be stored in the CAB file
                           void*       pParam=NULL)                  // optional user parameter passed to all callbacks
    {
        #if _TraceCompress
            CTrace::TraceW(L"*** FCICreate(File='%s', UTC=%d, Split=0x%X, ID=%d) ***", (WCHAR*)sw_File, b_UtcTime, u32_CabSplitSize, u16_CabID);
        #endif

        // Every class must only be accessed by one and the same thread. See "Microsoft Cabinet.dll Doku.doc"
        if (mu32_ThreadID != 0)
        {
            mi_Error.Set(FCIERR_INVAL_THREAD,0,0);
            return FALSE;
        }

        mu32_ThreadID = GetCurrentThreadId();

        CStrW sw_Folder, sw_File;
        CFile::SplitPathW(sw_CabFile, &sw_Folder, &sw_File);

        mi_Error.Reset();

        if (mh_FCIContext || !sw_File.Len() || !sw_Folder.Len())
        {
            mi_Error.Set(FCIERR_INVAL_PARAM,0,0);
            return FALSE;
        }

        #if STATIC_LINK_CABINET_DLL // Use precompiled functions in FCI.LIB

            mf_FciCreate       = FCICreate;
            mf_FciAddFile      = FCIAddFile;
            mf_FciFlushFolder  = FCIFlushFolder;
            mf_FciFlushCabinet = FCIFlushCabinet;
            mf_FciDestroy      = FCIDestroy;

        #else // Dynamic Link (execute functions in Cabinet.dll in Windows directory)

            if (!mh_CabinetDll)
            {
                // If the DLL is already attached to this process it will not be loaded a second time!
                mh_CabinetDll = LoadLibraryW(L"Cabinet.dll");
                mf_FciCreate       = (fFciCreate)       GetProcAddress(mh_CabinetDll, "FCICreate");
                mf_FciAddFile      = (fFciAddFile)      GetProcAddress(mh_CabinetDll, "FCIAddFile");
                mf_FciFlushFolder  = (fFciFlushFolder)  GetProcAddress(mh_CabinetDll, "FCIFlushFolder");
                mf_FciFlushCabinet = (fFciFlushCabinet) GetProcAddress(mh_CabinetDll, "FCIFlushCabinet");
                mf_FciDestroy      = (fFciDestroy)      GetProcAddress(mh_CabinetDll, "FCIDestroy");

                if (!mf_FciCreate || !mf_FciAddFile || !mf_FciFlushFolder || !mf_FciFlushCabinet || !mf_FciDestroy)
                {
                    mh_CabinetDll  = 0;
                    mi_Error.Set(FCIERR_LOAD_DLL,0,0);
                    return FALSE;
                }
            }

        #endif

        // Create the tree of folders where to store the CAB file if they do not yet exist
        UINT u32_Error = CFile::CreateFolderTreeW(sw_Folder);
        if  (u32_Error)
        {
            mi_Error.Set(FCIERR_CREATE_DIR, u32_Error,0);
            return FALSE;
        }

        mp_Param     = pParam;
        mb_UtcTime   = b_UtcTime;
        mb_EncodeUtf = b_EncodeUtf;
        mb_Abort     = FALSE; 
        mb_AutoFlush = TRUE; // Flushing must still be done
        
        mk_Encrypt.h_CabHandle = 0;
        mk_Encrypt.sw_CabFiles = L"|";

        if (!SetCabParametersW(&mk_CabParams, sw_Folder, sw_File, u16_CabID, u32_CabSplitSize))
            return FALSE;

        memset(&mk_CurStatus, 0, sizeof(kCurStatus));

        mh_FCIContext = mf_FciCreate(&mi_Error.k_ERF,
                                    (PFNFCIFILEPLACED) (FCIFilePlaced),
                                    (PFNFCIALLOC)      (FCIAlloc),
                                    (PFNFCIFREE)       (FCIFree),
                                    (PFNFCIOPEN)       (FCIOpen),
                                    (PFNFCIREAD)       (FCIRead),
                                    (PFNFCIWRITE)      (FCIWrite),
                                    (PFNFCICLOSE)      (FCIClose),
                                    (PFNFCISEEK)       (FCISeek),
                                    (PFNFCIDELETE)     (FCIDelete),
                                    (PFNFCIGETTEMPFILE)(FCIGetTempFile),
                                    &mk_CabParams,
                                    this); // passed to the static callbacks as "pThis"

        return (mh_FCIContext != NULL);
    }


    // Flushes the cabinet if the user did not yet call FlushCabinet()
    // Destroys the FCI context used by this instance and returns TRUE if succesful or FALSE otherwise.
    // Deletes all temp files
    BOOL DestroyFCIContext()
    {
        #if _TraceCompress
            CTrace::TraceW(L"*** FCIDestroy() ***");
        #endif

        BOOL b_Ret = TRUE;

        if (!mh_FCIContext)
            return b_Ret;

        if (mb_AutoFlush) // The user has not yet flushed the cabinet
        {
            if (!FlushCabinet(FALSE))
                b_Ret = FALSE;
        }

        if (!mf_FciDestroy(mh_FCIContext))
            b_Ret = FALSE;

        mu32_ThreadID = 0;
        mh_FCIContext = NULL;
        return b_Ret;
    }

    // This function adds one file to the current cabinet.
    // Parameters:
    // sw_FileToAdd = Full path to the file to be added to the cabinet.
    // sw_NameInCab = path and filename under which to store the file in the cabinet
    //                "Install\Setup.exe" will create a subfolder "Install" inside the cabinet
    // e_Compress   = Compression algorithm (NONE, MSZIP, LZX)
    BOOL AddFileW(const CStrW& sw_FileToAdd, const CStrW& sw_NameInCab, eCompress e_Compress)
    {
        // Every class must only be accessed by one and the same thread. See "Microsoft Cabinet.dll Doku.doc"
        if (mu32_ThreadID != GetCurrentThreadId())
        {
            mi_Error.Set(FCIERR_INVAL_THREAD,0,0);
            return FALSE;
        }

        if (!mh_FCIContext)
            return FALSE;

        mb_AutoFlush = TRUE; // Flushing must be done

        #if _TraceCompress
            CTrace::TraceW(L"*** FCIAddFile('%s') ***", (WCHAR*)sw_FileToAdd);
        #endif

        ULONGLONG u64_Size;
        DWORD u32_Error = CFile::GetFileSizeW(sw_FileToAdd, &u64_Size);
        if (u32_Error)
        {
            mi_Error.Set(FCIERR_OPEN_SRC, u32_Error,0);
            return FALSE;
        }

        if (u64_Size > 0x7FFF0000) // Cabinet.dll supports max 2 GB
        {
            mi_Error.Set(FCIERR_FILE_TOO_BIG,0,0);
            return FALSE;
        }

        CStrA s_File, s_Name;
        if (mb_EncodeUtf) 
        {
            mb_NameInCabIsUtf = !sw_NameInCab.IsAscii();
            s_Name.EncodeUtf8(sw_NameInCab);
        }
        else // UTF8 encoding disabled by the caller
        {
            // Unicode filenames cannot be stored with UTF8 encoding disabled
            if (sw_NameInCab.IsUnicode())
            {
                mi_Error.Set(FCIERR_UNICODE_NEEDS_UTF8, 0,0);
                return FALSE;
            }

            mb_NameInCabIsUtf = FALSE;
            s_Name = sw_NameInCab;
        }

        // When mf_FciAddFile returns FALSE the error code has already been written into mi_Error in one of the 3 callbacks.
        // If you want to debug with a Debugger to trap an error you MUST set a breakpoint in FCIGetNextCabinet, FCIUpdateStatus, FCIGetAttribsAndDate!
        // But it is much easier to enable _TraceCompress and observe in DebugView what's happening.
        return mf_FciAddFile(mh_FCIContext, 
                             s_File.EncodeUtf8(sw_FileToAdd),
                             s_Name,
                             FALSE, 
                             (PFNFCIGETNEXTCABINET) FCIGetNextCabinet,
                             (PFNFCISTATUS)         FCIUpdateStatus,
                             (PFNFCIGETOPENINFO)    FCIGetAttribsAndDate,
                             e_Compress);
    }

    // Adds a folder with all its files and all its subfolders to the CAB file
    // sw_Path    = "C:\MyFolder\"
    // sw_Filter  = "*.*  or  "*.dll"
    // b_Compress = FALSE --> store in CAB file without compression
    // sw_Prefix = "Plugins\" -> files will be stored as "Plugins\File1.txt", etc..
    // (sw_Prefix inserts an optional prefix before the relative path in the CAB file)
    // Do never set or modify s32_BaseLen !!
    BOOL AddFolderW(/*NO const*/ CStrW sw_Path, const CStrW& sw_Filter=L"*.*", eCompress e_Compress=E_ComprLZX, 
                    const CStrW& sw_Prefix=L"", int s32_BaseLen=-1) // optional params
    {
        CFile::TerminatePathW(sw_Path);

        int s32_Len = sw_Path.Len();
        if (s32_BaseLen < 0) 
            s32_BaseLen = s32_Len;

        sw_Path += sw_Filter;

        WIN32_FIND_DATAW k_Find;
        HANDLE h_File = FindFirstFileW(sw_Path, &k_Find);

        if (h_File == INVALID_HANDLE_VALUE)
        {
            DWORD u32_Err = GetLastError();
            if (u32_Err == ERROR_FILE_NOT_FOUND)
                return TRUE; // The folder is empty

            mi_Error.Set(0, u32_Err, 0);
            return FALSE;
        }

        CStrW sw_File;
        do
        {
            // skip directories "." and ".." but do not skip a diretory ".NET"
            if (k_Find.cFileName[0] == '.' && 
                k_Find.cFileName[1] == 0)
                continue;

            if (k_Find.cFileName[0] == '.' && 
                k_Find.cFileName[1] == '.' && 
                k_Find.cFileName[2] == 0)
                continue;

            sw_File = sw_Path;
            sw_File[s32_Len] = 0;
            sw_File += k_Find.cFileName;

            if (k_Find.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
            {
                if (!AddFolderW(sw_File, sw_Filter, e_Compress, sw_Prefix, s32_BaseLen))
                {
                    FindClose(h_File);
                    return FALSE;
                }
            }
            else
            {
                CStrW sw_RelPath = sw_Prefix;
                sw_RelPath += (WCHAR*)sw_File + s32_BaseLen;

                if (mk_Callbacks.f_OnBeforePack)
                {
                    if (!mk_Callbacks.f_OnBeforePack(sw_RelPath))
                        continue; // skip file
                }

                if (!AddFileW(sw_File, sw_RelPath, e_Compress))
                {
                    FindClose(h_File);
                    return FALSE;
                }
            }
        }
        while (FindNextFileW(h_File, &k_Find));

        FindClose(h_File);
        return TRUE;
    }


    // Forces the current cabinet under construction to be completed immediately and written to disk.
    // Further calls to AddFile will cause files to be added to another cabinet.
    // It is also possible that there exists pending data in FCI�s internal buffers that 
    // will may require spillover into another cabinet, if the current cabinet 
    // has reached the application-specified media size limit.
    // b_CreateNewCabinetFile = TRUE:  Call GetNextCabinet() to obtain continuation information.
    // b_CreateNewCabinetFile = FALSE: Call GetNextCabinet() only if the cabinet overflows
    BOOL FlushCabinet(BOOL b_CreateNewCabinetFile)
    {
        // Every class must only be accessed by one and the same thread. See "Microsoft Cabinet.dll Doku.doc"
        if (mu32_ThreadID != GetCurrentThreadId())
        {
            mi_Error.Set(FCIERR_INVAL_THREAD,0,0);
            return FALSE;
        }

        if (!mh_FCIContext)
            return FALSE;

        mb_AutoFlush = FALSE; // Flushing already done

        #if _TraceCompress
            CTrace::TraceW(L"*** FCIFlushCabinet() ***");
        #endif

        // When mf_FciFlushCabinet returns FALSE the error code has already been written into mi_Error in one of the 2 callbacks.
        // If you want to debug with a Debugger to trap an error you MUST set a breakpoint in FCIGetNextCabinet and FCIUpdateStatus!
        // But it is much easier to enable _TraceCompress and observe in DebugView what's happening.
        return mf_FciFlushCabinet(mh_FCIContext, 
                                   b_CreateNewCabinetFile,
                                  (PFNFCIGETNEXTCABINET) FCIGetNextCabinet,
                                  (PFNFCISTATUS)         FCIUpdateStatus);
    }

    // Complete the current folder under construction
    // This will force the termination of the current folder, which may or
    // may not cause one or more cabinet files to be completed.
    BOOL FlushFolder()
    {
        // Every class must only be accessed by one and the same thread. See "Microsoft Cabinet.dll Doku.doc"
        if (mu32_ThreadID != GetCurrentThreadId())
        {
            mi_Error.Set(FCIERR_INVAL_THREAD,0,0);
            return FALSE;
        }

        if (!mh_FCIContext)
            return FALSE;

        mb_AutoFlush = TRUE; // Flushing must be done

        #if _TraceCompress
            CTrace::TraceW(L"*** FCIFlushFolder() ***");
        #endif

        return mf_FciFlushFolder(mh_FCIContext, 
                                (PFNFCIGETNEXTCABINET) FCIGetNextCabinet,
                                (PFNFCISTATUS)         FCIUpdateStatus);
    }

    // Returns an Unicode message for the last error that has occured.
    WCHAR* LastErrorW()
    {
        return mi_Error.LastErrorW();
    }

protected:

    // Used to encrypt the CAB file
    struct kEncrypt
    {
        CBlowfish i_Blowfish;    // The Blowfish encryption class
        CStrW    sw_CabFiles;    // The path to all CAB files splitted by pipe characters
        INT_PTR   h_CabHandle;   // The handle of the CAB file which is currently written
        UINT    u32_Pointer;     // Filepointer (modified when writing and modfied by Seek)
        BYTE*    u8_Buf;         // A 64 kB buffer for encryption
        char     s8_BOM[4];      // The first 4 bytes in a CAB file that identify encrypted files

        kEncrypt() // Constructor
        {
              h_CabHandle = 0;
            u32_Pointer   = 0;
             u8_Buf       = 0;
        }
        ~kEncrypt() // Destructor
        {
            if (u8_Buf) delete u8_Buf;
        }
    };

    kEncrypt   mk_Encrypt;
    kCallbacks mk_Callbacks;

    // The count of Bytes which have yet been compressed
    kCurStatus mk_CurStatus;

    // Handle to the FCI context.
    HFCI mh_FCIContext;

    // Stores the last error
    CError mi_Error;

    // The directory where to store the temporary files
    CStrW msw_TempDir;

    // This receives the formatter for the resulting filename for the cabfile
    // For a single cab file -> "MyCabFile.cab"
    // For a spanned cabfile the name must contain "%d" -> "MyCabFile_%d.cab"
    CStrA msa_CabNameFormatter;

    // User defined parameter to pass to the callback function.
    void* mp_Param;

    // Flag that can be set to abort the current operation.
    BOOL mb_Abort;

    // TRUE when the caller did not yet flush the cabinet
    BOOL mb_AutoFlush;

    // TRUE -> store the files with UTC time in the CAB file (recommended)
    // Windows stores files with UTC time. If you compress with local time you will have
    // a discrepancy of one hour or more after daylight saving or timezone changes
    // When extracting you must set the same value for this flag!!
    BOOL mb_UtcTime;

    // TRUE  -> If a filename has characters > 0x7F, encode it using UTF8 and set the flag _A_NAME_IS_UTF
    // FALSE -> Store ANSI filenames unchanged, do not set _A_NAME_IS_UTF, return error if character > 0xFF
    // Read the documentation!
    BOOL mb_EncodeUtf;
    
    // This flag is set when AddFile() adds a file which is to be stored with an Utf name into the cabinet
    BOOL mb_NameInCabIsUtf;

    // The initialization data for the current cabinet
    CCAB mk_CabParams;

    // Every class instance must only be accessed from one thread
    DWORD mu32_ThreadID;

    // A unique identifier for each class instance
    DWORD mu32_ClassID;

    // The current number of the latest temp file
    DWORD mu32_TempCounter;

    typedef HFCI (DIAMONDAPI* fFciCreate)      (PERF, PFNFCIFILEPLACED, PFNFCIALLOC, PFNFCIFREE, PFNFCIOPEN, PFNFCIREAD, PFNFCIWRITE, PFNFCICLOSE, PFNFCISEEK, PFNFCIDELETE, PFNFCIGETTEMPFILE, PCCAB, void*);
    typedef BOOL (DIAMONDAPI* fFciAddFile)     (HFCI, char*, char*, BOOL, PFNFCIGETNEXTCABINET, PFNFCISTATUS, PFNFCIGETOPENINFO, TCOMP);
    typedef BOOL (DIAMONDAPI* fFciFlushFolder) (HFCI,       PFNFCIGETNEXTCABINET, PFNFCISTATUS);
    typedef BOOL (DIAMONDAPI* fFciFlushCabinet)(HFCI, BOOL, PFNFCIGETNEXTCABINET, PFNFCISTATUS);
    typedef BOOL (DIAMONDAPI* fFciDestroy)     (HFCI);

    HINSTANCE        mh_CabinetDll;
    fFciCreate       mf_FciCreate;
    fFciAddFile      mf_FciAddFile;
    fFciFlushFolder  mf_FciFlushFolder;
    fFciFlushCabinet mf_FciFlushCabinet;
    fFciDestroy      mf_FciDestroy;


private:

    // #################### STATIC FCI CALLBACKS ########################

    static void* FCIAlloc(UINT size)
        { return operator new(size); }

    static void FCIFree(void* memblock)
        { operator delete(memblock); }
    
    static INT_PTR FCIOpen(char* s8_File, int oflag, int pmode, int *err, void *pThis)
        { return ((CCompress*)pThis)->FciOpenA(s8_File, oflag, pmode, err); }
    
    static UINT FCIRead(INT_PTR fd, void *memory, UINT count, int *err, void *pThis)
        { return ((CCompress*)pThis)->FciRead(fd, memory, count, err); }
    
    static UINT FCIWrite(INT_PTR fd, void *memory, UINT count, int *err, void *pThis)
        { return ((CCompress*)pThis)->FciWrite(fd, memory, count, err); }
    
    static int FCIClose(INT_PTR fd, int *err, void *pThis)
        { return ((CCompress*)pThis)->FciClose(fd, err); }
    
    static long FCISeek(INT_PTR fd, long offset, int seektype, int *err, void *pThis)
        { return ((CCompress*)pThis)->FciSeek(fd, offset, seektype, err); }
    
    static int FCIDelete(char *s8_File, int *err, void *pThis)
        { return ((CCompress*)pThis)->FciDelete(s8_File, err); }
    
    static BOOL FCIGetTempFile(char *pszTempName, int cbTempName, void *pThis)
        { return ((CCompress*)pThis)->FciGetTempFile(pszTempName, cbTempName); }
    
    static INT_PTR FCIGetAttribsAndDate(char *pszName, USHORT *pdate, USHORT *ptime, USHORT *pattribs, int *err, void *pThis)
        { return ((CCompress*)pThis)->FciGetAttribsAndDate(pszName, pdate, ptime, pattribs, err); }
    
    static int FCIFilePlaced(PCCAB pccab, char *s8_File, int s32_FileSize, BOOL fContinuation, void *pThis)
        { return ((CCompress*)pThis)->FciFilePlaced(pccab, s8_File, s32_FileSize, fContinuation); }
    
    static BOOL FCIGetNextCabinet(PCCAB pccab, ULONG cbPrevCab, void* pThis)
        { return ((CCompress*)pThis)->FciGetNextCabinet(pccab, cbPrevCab); }
    
    static long FCIUpdateStatus(UINT typeStatus, ULONG cb1, ULONG cb2, void *pThis)
        { return ((CCompress*)pThis)->FciUpdateStatus(typeStatus, cb1, cb2); }

    // #################### MEMBER FCI CALLBACKS ########################

    INT_PTR FciOpenA(char* s8_File, int oflag, int pmode, int *err)
    {
        CStrW sw_File;
        return FciOpenW(sw_File.ToUnicode(CP_UTF8, s8_File), oflag, pmode, err);
    }
    INT_PTR FciOpenW(const CStrW& sw_File, int oflag, int pmode, int *err)
    { 
        INT_PTR fd = Open(sw_File, oflag, pmode, err);

        #if _TraceCompress
            // Open for writing: The final CAB file(s)      and the temp files
            // Open for reading: The files to be compressed and the temp files
            BOOL b_Write = (pmode & _S_IWRITE);
            CTrace::TraceW(L"> > > > FCIOpen (%s, %s) --> Handle= 0x%08X", (b_Write ? L"Write" : L"Read"), (WCHAR*)sw_File, fd);
        #endif

        // The output CAB file has just been opened -> remember its handle
        if (mk_Encrypt.i_Blowfish.IsPasswordSet() && wcsstr(mk_Encrypt.sw_CabFiles, sw_File) > 0) 
        {
            mk_Encrypt.h_CabHandle = fd;
            mk_Encrypt.u32_Pointer = 0;
            mk_Encrypt.i_Blowfish.EncryptStreamOpen();

            #if _TraceCompress
                CTrace::TraceW(L"**** Blowfish EncryptStreamOpen");
            #endif
        }
        return fd;
    }

    UINT FciRead(INT_PTR fd, void *memory, UINT count, int *err)
    { 
        int s32_Read = Read(fd, memory, count, err); 

        #if _TraceCompress
            CTrace::TraceW(L"FCIRead (Handle= 0x%08X, Count= %5d) --> %05d Bytes read", fd, count, s32_Read);
        #endif

        // Reading from CAB file does not happen while compressing

        return s32_Read;
    }

    UINT FciWrite(INT_PTR fd, void *memory, UINT count, int *err)
    { 
        // Write without encryption
        if (mk_Encrypt.h_CabHandle != fd)
        {
            // Write data block to disk
            UINT u32_Written = Write(fd, memory, count, err); 

            #if _TraceCompress
                CTrace::TraceW(L"FCIWrite(Handle= 0x%08X, Count= %5d) --> %05d Bytes written", fd, count, u32_Written);
            #endif

            return u32_Written;
        }

        // Write with Encryption
        BYTE* u8_Buffer = (BYTE*)memory;
        UINT u32_Count  = count;

        // Modify the first 4 bytes which identify the CAB file
        // So programs which try to open the file will spit out an error "Invalid CAB file"
        if (mk_Encrypt.u32_Pointer == 0 && strncmp((char*)u8_Buffer, "MSCF", 4) == 0)
        {
            strncpy((char*)u8_Buffer, mk_Encrypt.s8_BOM, 4);
        }

        // The first ENCRYPTION_START Bytes (file header) are written without encryption
        if (mk_Encrypt.u32_Pointer < ENCRYPTION_START)
        {
            UINT u32_UnEncrypted = min(u32_Count, ENCRYPTION_START - mk_Encrypt.u32_Pointer);

            UINT u32_Written = Write(fd, u8_Buffer, u32_UnEncrypted, err);
            if  (u32_Written != u32_UnEncrypted)
                 return -1;

            #if _TraceCompress
                CTrace::TraceW(L"FCIWrite(Handle= 0x%08X) --> %05d unencrypted Bytes written", fd, u32_Written);
            #endif

            u32_Count              -= u32_Written;
            u8_Buffer              += u32_Written;
            mk_Encrypt.u32_Pointer += u32_Written;
        }

        // Write the rest of the bytes (compressed CAB data) encrypted
        if (u32_Count > 0)
        {
            DWORD u32_Encrypted = 0;
            mk_Encrypt.i_Blowfish.EncryptStreamData(u8_Buffer, u32_Count, mk_Encrypt.u8_Buf, CRYPT_BUFFER_SIZE, &u32_Encrypted);

            UINT u32_Written = Write(fd, mk_Encrypt.u8_Buf, u32_Encrypted, err);
            if  (u32_Written != u32_Encrypted)
                 return -1;

            #if _TraceCompress
                CTrace::TraceW(L"FCIWrite(Handle= 0x%08X) --> %05d Bytes encrypted, %05d Bytes written", fd, u32_Count, u32_Written);
            #endif

            mk_Encrypt.u32_Pointer += u32_Written;
        }
        // To avoid an error, return the byte count that Cabinet.dll wanted to write
        // although some of these bytes may still remain in the Blowfish buffer
        return count;
    }

    // Write the last block in the Blowfish buffer to disk before setting the file pointer or closing file
    BOOL FlushBlowfish(INT_PTR fd, int *err)
    {
        DWORD u32_Flushed = 0;
        BYTE   u8_Flush[8];
        mk_Encrypt.i_Blowfish.EncryptStreamClose(u8_Flush, sizeof(u8_Flush), &u32_Flushed);
        if (u32_Flushed > 0)
        {
            #if _TraceCompress
                CTrace::TraceW(L"**** Blowfish EncryptStreamClose (Handle= 0x%08X) --> flushed last block (8 Bytes)", fd);
            #endif

            if (u32_Flushed != Write(fd, u8_Flush, u32_Flushed, err))
                return FALSE;
        }
        return TRUE;
    }

    long FciSeek(INT_PTR fd, long offset, int seektype, int *err)
    { 
        // Cabinet.dll calls FciSeek after each compression before closing the CAB file.
        if (mk_Encrypt.h_CabHandle == fd)
        {
            if (!FlushBlowfish(fd, err))
                return -1;
        }

        // Move the file pointer to the new position
        int Pos = Seek(fd, offset, seektype, err); 

        #if _TraceCompress
            switch (seektype)
            {
                case SEEK_SET: CTrace::TraceW(L"FCISeek (Handle= 0x%08X, SEEK_SET, Offset= %05d) --> Position= %05d", fd, offset, Pos); break;
                case SEEK_CUR: CTrace::TraceW(L"FCISeek (Handle= 0x%08X, SEEK_CUR, Offset= %05d) --> Position= %05d", fd, offset, Pos); break;
                case SEEK_END: CTrace::TraceW(L"FCISeek (Handle= 0x%08X, SEEK_END, Offset= %05d) --> Position= %05d", fd, offset, Pos); break;
            }
        #endif

        // remember the current file pointer in the CAB file
        if (mk_Encrypt.h_CabHandle == fd) 
        {
            mk_Encrypt.u32_Pointer = Pos;
        }
        return Pos;
    }

    int FciClose(INT_PTR fd, int *err)
    { 
        #if _TraceCompress
            CTrace::TraceW(L"FCIClose(Handle= 0x%08X)", fd);
        #endif

        // The output CAB file has just been closed -> reset its handle
        if (mk_Encrypt.h_CabHandle == fd) 
        {
            mk_Encrypt.h_CabHandle = 0;

            if (!FlushBlowfish(fd, err))
                return -1;
        }
        return Close(fd, err); 
    }


    int FciDelete(char* s8_File, int* err)
    { 
        CStrW sw_File;
        sw_File.ToUnicode(CP_UTF8, s8_File);
        
        #if _TraceCompress
            CTrace::TraceW(L"FCIDelete('%s')", (WCHAR*)sw_File);
            CTrace::TraceW(L"-------");
        #endif

        return Delete(sw_File, err); 
    }

    // see comment for GetTempFileW!
    BOOL FciGetTempFile(char *pszTempName, int cbTempName)
    { 
        return GetTempFileA(pszTempName, cbTempName); 
    }

    // see comment for GetAttribsAndDate!
    INT_PTR FciGetAttribsAndDate(char *s8_File, USHORT *pdate, USHORT *ptime, USHORT *pattribs, int *err)
    {
        if (mb_Abort)
            return -1;

        CStrW sw_File;
        return GetAttribsAndDateW(sw_File.ToUnicode(CP_UTF8, s8_File), pdate, ptime, pattribs, err);
    }
    
    // see comment for OnFilePlaced!
    int FciFilePlaced(PCCAB pccab, char *s8_File, int s32_FileSize, BOOL fContinuation)
    { 
        if (mk_Callbacks.f_FilePlaced)
        {
            CStrW sw_File;
            return mk_Callbacks.f_FilePlaced(pccab, sw_File.ToUnicode(CP_UTF8, s8_File), s32_FileSize, fContinuation, mp_Param);
        }
        return 0;
    }

    // see comment for OnGetNextCabinet!
    BOOL FciGetNextCabinet(PCCAB pccab, ULONG cbPrevCab)
    {
        if (mb_Abort)
            return FALSE;

        CStrA sa_CabName;
        sa_CabName.Format(msa_CabNameFormatter, pccab->iCab);

        if (!sa_CabName.SafeExport(pccab->szCab, sizeof(pccab->szCab)))
        {
            mi_Error.Set(FCIERR_PATH_TOO_LONG,0,0);
            return FALSE;
        }

        // sizeof(szDisk) = 256 Bytes
        sprintf(pccab->szDisk, "Disk %d", pccab->iDisk++);

        if (mk_Callbacks.f_GetNextCabinet)
        {
            // the callback may modify the values in pccab here!!
            if (!mk_Callbacks.f_GetNextCabinet(pccab, cbPrevCab, msa_CabNameFormatter, mp_Param))
                return FALSE;
        }

        // Append the Cab path to the list of Cab files
        CStrA sa_CabPath;
        sa_CabPath.Format("%s%s", pccab->szCabPath, pccab->szCab);

        CStrW sw_CabPath;
        sw_CabPath.ToUnicode(CP_UTF8, sa_CabPath);

        #if _TraceCompress
            CTrace::TraceW(L"FCIGetNextCabinet formatted filename: '%s'", (WCHAR*)sw_CabPath);
        #endif

        mk_Encrypt.sw_CabFiles += sw_CabPath;
        mk_Encrypt.sw_CabFiles += L"|";
        return TRUE;
    }

    // see comment for OnUpdateStatus!
    int FciUpdateStatus(UINT typeStatus, ULONG cb1, ULONG cb2)
    {
        if (mb_Abort)
            return -1;

        mk_CurStatus.cb1           = cb1;
        mk_CurStatus.cb2           = cb2;
        mk_CurStatus.FolderPercent = 0;

        if (typeStatus == statusFile)
        {
            // Calculate how many bytes have been compressed totally yet
            mk_CurStatus.u32_TotCompressedSize   += cb1;
            mk_CurStatus.u32_TotUncompressedSize += cb2;
        }
        else if (typeStatus == statusFolder)
        {
            // Calculate percentage of folder compression
            while (cb1 > 10000000)
            {
                cb1 >>= 3;
                cb2 >>= 3;
            }
            if (cb2 != 0 && cb1<=cb2) mk_CurStatus.FolderPercent = ((cb1*100)/cb2);
        }

        if (mk_Callbacks.f_UpdateStatus)
            return mk_Callbacks.f_UpdateStatus(typeStatus, &mk_CurStatus, mp_Param);

        return 0;
    }

protected:

    // #################### OVERRIDABLES ###########################

    // Opens a file
    virtual INT_PTR Open(WCHAR* u16_File, int oflag, int pmode, int *err)
    {
        int result = _wopen(u16_File, oflag, pmode);
        if (result == -1) *err = errno;
        return result;
    }

    // Read a file
    virtual UINT Read(INT_PTR hf, void *memory, UINT cb, int *err)
    { 
        UINT result = (UINT) _read((int)hf, memory, cb);
        if  (result != cb) *err = errno;
        return result;
    }

    // Write a file
    virtual UINT Write(INT_PTR hf, void *memory, UINT cb, int *err)
    { 
        UINT result = (UINT) _write((int)hf, memory, cb);
        if  (result != cb) *err = errno;
        return result;
    }

    // Close a file
    virtual int Close(INT_PTR hf, int *err)
    { 
        int result = _close((int)hf);
        if (result != 0) *err = errno;
        return result;
    }

    // Delete a (temp) file
    virtual int Delete(WCHAR *u16_File, int *err)
    {     
        int result = _wremove(u16_File);
        if (result != 0) *err = errno;
        return result;
    }

    // Seek inside a file (move file pointer)
    virtual long Seek(INT_PTR hf, long dist, int seektype, int *err)
    { 
        long result = _lseek((int)hf, dist, seektype);
        if  (result == -1) *err = errno;
        return result; 
    }


    // #################### FCI FUNCTIONS ########################


    // Load the initialization structure pk_CabParams
    // May be overridden
    BOOL SetCabParametersW(CCAB*   pk_CabParams, 
                           CStrW   sw_CabFolder,   // "C:\Temp"
                           CStrW   sw_CabFileName, // "Packed.cab" or "Packed_Part_%d"
                           USHORT u16_CabID,       // an ID to be stored in the CAB file
                           ULONG  u32_CabSplitSize)// The max filesize for splitted CAB files in Bytes
    {
        memset(pk_CabParams, 0, sizeof(CCAB));

        if (sw_CabFolder.Len() < 3 || sw_CabFileName.Len() < 3 || u32_CabSplitSize < 20000)
        {
            mi_Error.Set(FCIERR_INVAL_PARAM,0,0);
            return FALSE;
        }

        // The filename must be like "Packed_%d.cab" or "Packed_%03u.cab" if splitting
        if (u32_CabSplitSize < 0x7FFF0000 && !wcsstr(sw_CabFileName, L"%"))
        {
            mi_Error.Set(FCIERR_INVAL_PARAM,0,0);
            return FALSE;
        }

        CFile::TerminatePathW(sw_CabFolder);

        CStrA sa_CabPath;
        sa_CabPath.EncodeUtf8(sw_CabFolder);
        
        if (!sa_CabPath.SafeExport(pk_CabParams->szCabPath, sizeof(pk_CabParams->szCabPath)))
        {
            mi_Error.Set(FCIERR_PATH_TOO_LONG,0,0);
            return FALSE;
        }

        // ATTENTION:
        // Altough Microsoft defined "cb" as ULONG it is treated 
        // internally as if it would be signed !!!
        pk_CabParams->cb                = u32_CabSplitSize & 0x7FFFFFFF; // MediaSize
        pk_CabParams->cbFolderThresh    = FOLDER_THRESHOLD;
        // Don't reserve space for any extensions
        pk_CabParams->cbReserveCFHeader = 0;
        pk_CabParams->cbReserveCFFolder = 0;
        pk_CabParams->cbReserveCFData   = 0;
        // We use this to create the cabinet name
        pk_CabParams->iCab  = 1;
        // If you want to use disk names, use this to count disks
        pk_CabParams->iDisk = 1;
        // Chose your own number (e.g. 12345) or set to zero
        pk_CabParams->setID = u16_CabID;

        // The name of the next CAB file is stored inside a splitted CAB file.
        // When extracting splitted CAB files any characters above 127 will make extraction impossible.
        // Cabinet.dll does not support Unicode.
        // There is no way to know during extraction if characters above 127 are UTF-8 or any strange Codepage!
        // There is no flag or anything else that can be set to indicate the encoding of the Cab file name.
        if (!sw_CabFileName.IsAscii())
        {
            mi_Error.Set(FCIERR_CAB_NAME_ASCII,0,0);
            return FALSE;
        }

        // copy the formatter of the Cabfile ("Packed_%d.cab" or "Packed.cab")
        msa_CabNameFormatter = sw_CabFileName;

        // Create the name of first CAB file and the first disk
        return FCIGetNextCabinet(pk_CabParams, 0, this); // passed to the static callbacks as "pThis"
    }


    // A function to obtain temporary file names
    // The filename returned should not occupy more than cbBufSize bytes.
    // FCI may open several temporary files at once, so it is important to ensure 
    // that a different filename is returned each time, and that the file does not already exist.
    BOOL GetTempFileA(char *s8_TempName, int cbBufSize)
    { 
        CStrW sw_TempFile;
        while (TRUE)
        {
            sw_TempFile.Format(L"%sCabTmp_%02X_%02X", (WCHAR*)msw_TempDir, mu32_ClassID, mu32_TempCounter++);

            // Check if the file already exists (-1 = Error reading file attribures)
            if (-1 == GetFileAttributesW(sw_TempFile))
                break;
        }

        CStrA sa_TempFile;
        sa_TempFile.EncodeUtf8(sw_TempFile);

        if (!sa_TempFile.SafeExport(s8_TempName, cbBufSize))
        {
            mi_Error.Set(FCIERR_PATH_TOO_LONG,0,0);
            return FALSE;
        }
        return TRUE;
    }

    // Open source file and return date / time / attributes
    // - pszName:  complete path to filename
    // - pdate:    FAT-style date code
    // - ptime:    FAT-style time code
    // - pattribs: FAT-style attributes
    // - p_Param:  your user defined parameter which you have passed to AddFile(..)
    // Exit-Success: Return file handle of open file to read
    // Exit-Failure: Return -1
    INT_PTR GetAttribsAndDateW(WCHAR* u16_File, USHORT *pdate, USHORT *ptime, USHORT *pattribs, int *err)
    {
        #if _TraceCompress
            CStrW s_FileName;
            CFile::SplitPathW(u16_File, 0, &s_FileName);
            CTrace::TraceW(L"GetAttribsAndDate('%s'), UTC=%d, UTF=%d", (WCHAR*)s_FileName, mb_UtcTime, mb_NameInCabIsUtf);
        #endif

        HANDLE h_File = CreateFileW(u16_File, GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
                                    FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL);

        if (h_File == INVALID_HANDLE_VALUE)
        {
            *err = ::GetLastError();
            return -1;
        }

        BY_HANDLE_FILE_INFORMATION k_FileInfo;
        if (!GetFileInformationByHandle(h_File, &k_FileInfo))
        {
            *err = ::GetLastError();
            CloseHandle(h_File);
            return -1;
        }

        CloseHandle(h_File);

        // The Windows filesystem stores UTC times
        ::FILETIME k_CabTime = k_FileInfo.ftLastWriteTime;
        
        if (!mb_UtcTime) 
            FileTimeToLocalFileTime(&k_FileInfo.ftLastWriteTime, &k_CabTime); // UTC -> local time

        FileTimeToDosDateTime(&k_CabTime, pdate, ptime);

        // Mask out all other bits except these four, since other bits are used 
        // by the cabinet format to indicate a special meaning.
        *pattribs = (USHORT)(k_FileInfo.dwFileAttributes & (FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM | FILE_ATTRIBUTE_ARCHIVE));

        if (mb_UtcTime)        *pattribs |= FILE_ATTR_UTC_TIME;
        if (mb_NameInCabIsUtf) *pattribs |= _A_NAME_IS_UTF;

        // Now return a handle using _wopen()
        return FciOpenW(u16_File, _O_RDONLY | _O_BINARY, _S_IREAD, err);
    }
};

} // Namespace Cabinet

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) ElmüSoft
Chile Chile
Software Engineer since 40 years.

Comments and Discussions