#include "msix.h" #pragma comment(lib, "msi.lib") // Entry point. int _tmain(int argc, _TCHAR* argv[]) { DWORD dwError = NOERROR; HRESULT hr = NOERROR; ARGS args = { 0 }; IStorage* pRootStorage = NULL; IEnumSTATSTG* pEnum = NULL; LPCTSTR pszPersist = (LPTSTR)MSIDBOPEN_READONLY; STATSTG stg = { 0 }; PMSIHANDLE hDatabase = NULL; PMSIHANDLE hView = NULL; PMSIHANDLE hRecord = NULL; dwError = ParseArguments(argc, argv, &args); if (ERROR_SUCCESS != dwError) { return dwError; } // Open the root storage file and extract storages first. Storages cannot // be extracted using MSI APIs so we must use the compound file implementation // for IStorage. hr = StgOpenStorage( CT2W(args.Path), NULL, STGM_READ | STGM_SHARE_EXCLUSIVE, NULL, 0, &pRootStorage); if (SUCCEEDED(hr) && pRootStorage) { // Determine if the file path specifies an MSP file. // This will be used later to open the database with MSI APIs. if (IsPatch(pRootStorage)) { pszPersist = MSIDBOPEN_READONLY + MSIDBOPEN_PATCHFILE; } hr = pRootStorage->EnumElements(0, NULL, 0, &pEnum); if (SUCCEEDED(hr)) { while (S_OK == (hr = pEnum->Next(1, &stg, NULL))) { if (STGTY_STORAGE == stg.type) { hr = SaveStorage(pRootStorage, args.Directory, stg.pwcsName, args.IncludeExtension ? TEXT(".mst") : NULL); if (FAILED(hr)) { break; } } } SAFE_RELEASE(pEnum); } } SAFE_RELEASE(pRootStorage); // Now open the database using MSI APIs. Patches cannot be opened simultaneously // since exclusive access is required and no MSI APIs are exported that accept // an IStorage pointer. if (SUCCEEDED(hr)) { dwError = MsiOpenDatabase(args.Path, pszPersist, &hDatabase); if (ERROR_SUCCESS == dwError) { dwError = MsiDatabaseOpenView(hDatabase, TEXT("SELECT `Name`, `Data` FROM `_Streams`"), &hView); if (ERROR_SUCCESS == dwError) { dwError = MsiViewExecute(hView, NULL); if (ERROR_SUCCESS == dwError) { while (ERROR_SUCCESS == (dwError = MsiViewFetch(hView, &hRecord))) { dwError = SaveStream(hRecord, args.Directory, args.IncludeExtension); if (ERROR_SUCCESS != dwError) { break; } } // If there are no more records indicate success. if (ERROR_NO_MORE_ITEMS == dwError) { dwError = ERROR_SUCCESS; } } } } } // If a Win32 error has occurred return only the win32 error portion. if (FACILITY_WIN32 == HRESULT_FACILITY(hr)) { dwError = HRESULT_CODE(hr); } else if (FAILED(hr)) { // Just set it to the HRESULT. Many common HRESULTs // will yield an error string from FormatMessage. dwError = hr; } // Print the error to the console. if (ERROR_SUCCESS != dwError) { win32_error(dwError); } return dwError; } // Parse the file path, and optionally output directory and extension guessing switch. DWORD ParseArguments(int argc, [Pre(Null=No)] _TCHAR* argv[], [Pre(Null=No)] LPARGS args) { _ASSERTE(argv); _ASSERTE(args); int iParamIndex = 1; // Validate arguments. if (2 > argc) { error(TEXT("Error: you must specify a Windows Installer file from which to extract files.\n")); usage(argv[0], stderr); return ERROR_INVALID_PARAMETER; } if (0 == _tcsicmp(TEXT("/?"), argv[iParamIndex]) || 0 == _tcsicmp(TEXT("-?"), argv[iParamIndex])) { // Display the usage text. usage(argv[0], stdout); return ERROR_SUCCESS; } else if (TEXT('/') == argv[iParamIndex][0] || TEXT('-') == argv[iParamIndex][0]) { // Filename should not begin with a command-switch character. error(TEXT("Error: invalid file name.\n")); usage(argv[0], stderr); return ERROR_INVALID_PARAMETER; } else { // Set the path argument. args->Path = const_cast(argv[iParamIndex]); } // Get the output directory if requested. while (++iParamIndex < argc) { // The directory in which files are extracted. if (0 == _tcsicmp(TEXT("/out"), argv[iParamIndex]) || 0 == _tcsicmp(TEXT("-out"), argv[iParamIndex])) { if (++iParamIndex < argc && TEXT('/') != argv[iParamIndex][0] && TEXT('-') != argv[iParamIndex][0]) { args->Directory = const_cast(argv[iParamIndex]); } else { error(TEXT("Error: you must specify an output directory with /out.\n")); usage(argv[0], stderr); return ERROR_INVALID_PARAMETER; } } // Whether or not to include or guess at extensions for output file names. else if (0 == _tcsicmp(TEXT("/ext"), argv[iParamIndex]) || 0 == _tcsicmp(TEXT("-ext"), argv[iParamIndex])) { args->IncludeExtension = TRUE; } else { error(TEXT("Error: unknown option: %s.\n"), argv[iParamIndex]); usage (argv[0], stderr); return ERROR_INVALID_PARAMETER; } } return ERROR_SUCCESS; } // Prints usage to the given output file stream. void usage(LPCTSTR pszPath, FILE* out) { _ASSERTE(pszPath); _ASSERTE(out); LPTSTR pszName = NULL; pszName = (LPTSTR)_tcsrchr(pszPath, TEXT('\\')); if (pszName) { // Advance past the backslash. pszName++; } else { // Set the executable name. pszName = const_cast(pszPath); } _ftprintf(out, TEXT("Usage: %s [/out ] [/ext]\n\n"), pszName); _ftprintf(out, TEXT("\tfile - Path to an MSI, MSM, MSP, or PCP file.\n")); _ftprintf(out, TEXT("\tout - Extract streams and storages to the directory.\n")); _ftprintf(out, TEXT("\text - Append appropriate extensions to output files.\n")); _ftprintf(out, TEXT("\nExtracts transforms and cabinets from a Windows Installer file.\n")); } // Colors errors on the console red and prints the formatted error. // You can use positional format specifiers with CRT8. void error(LPCTSTR pszFormat, ...) { _ASSERTE(pszFormat); CONSOLE_SCREEN_BUFFER_INFO csbi; HANDLE hStdErr = INVALID_HANDLE_VALUE; va_list args; // Set console colors to error values. hStdErr = GetStdHandle(STD_ERROR_HANDLE); if (INVALID_HANDLE_VALUE != hStdErr) { if (GetConsoleScreenBufferInfo(hStdErr, &csbi)) { // Set new console colors. SetConsoleTextAttribute(hStdErr, COLOR_ERROR); } } // Print error. va_start(args, pszFormat); _vftprintf_p(stderr, pszFormat, args); va_end(args); // Reset the console colors to original values. if (INVALID_HANDLE_VALUE != hStdErr) { SetConsoleTextAttribute(hStdErr, csbi.wAttributes); } } // Wrapper around FormatMessage for getting error text. // Calls error() to print the error to the console. void win32_error(DWORD dwError) { LPTSTR pszError; // Format the error. Error ends with new line. if (FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &pszError, 0, NULL)) { error(TEXT("Error 0x%1$08x (%1$d): %2$s"), dwError, pszError); LocalFree(pszError); } } // Creates a patch from components, using the current working // directory if pszDir is NULL. // pszExt should be either NULL or start with a ".". LPTSTR MakePath(LPTSTR pszDest, size_t cchDest, LPCTSTR pszDir, LPCTSTR pszName, LPCTSTR pszExt) { size_t len = 0; _ASSERTE(pszDest); _ASSERTE(cchDest); _ASSERTE(pszName); // Make sure pszDest is NULL-terminated. pszDest[0] = TEXT('\0'); if (pszDir) { // Get the length of pszDir. len = _tcslen(pszDir); if (len && 0 != _tcsncpy_s(pszDest, cchDest, pszDir, len)) { return NULL; } if (len && TEXT('\\') != pszDest[len - 1]) { // Make sure the path ends with a "\". if (0 != _tcsncat_s(pszDest, cchDest, TEXT("\\"), _TRUNCATE)) { return NULL; } } } // Append the file name. if (0 != _tcsncat_s(pszDest, cchDest, pszName, _TRUNCATE)) { return NULL; } // Append the extension. if (pszExt) { if (0 != _tcsncat_s(pszDest, cchDest, pszExt, _TRUNCATE)) { return NULL; } } return pszDest; } // Wrapper around allocating and filling a buffer using MsiRecordGetString(). UINT GetString(MSIHANDLE hRecord, UINT iField, LPTSTR* ppszProperty, DWORD* pcchProperty) { _ASSERTE(hRecord); _ASSERTE(iField > 0); _ASSERTE(ppszProperty); _ASSERTE(pcchProperty); UINT iErr = NOERROR; DWORD cchProperty = 0; iErr = MsiRecordGetString(hRecord, iField, TEXT(""), &cchProperty); if (ERROR_MORE_DATA == iErr) { *ppszProperty = new TCHAR[++cchProperty]; *pcchProperty = cchProperty; iErr = MsiRecordGetString(hRecord, iField, *ppszProperty, &cchProperty); if (ERROR_SUCCESS != iErr) { delete [] *ppszProperty; *ppszProperty = NULL; *pcchProperty = 0; } } return iErr; } // Determines if the given IStorage* is for a patch // using the STATSTG for the IStorage object. BOOL IsPatch(IStorage* pStorage) { _ASSERTE(pStorage); HRESULT hr = NOERROR; STATSTG stg = { 0 }; hr = pStorage->Stat(&stg, STATFLAG_NONAME); if (SUCCEEDED(hr)) { return !memcmp(&stg.clsid, &CLSID_MsiPatch, sizeof(CLSID)); } return FALSE; } // Creates a new storage file and saves the named sub-storage of pRootStorage // to the new storage file. HRESULT SaveStorage(IStorage* pRootStorage, LPCTSTR pszDir, PCWSTR pszName, LPCTSTR pszExt) { HRESULT hr = NOERROR; TCHAR szPath[MAX_PATH] = { TEXT('\0') }; IStorage* pStg = NULL; IStorage* pFileStg = NULL; _ASSERTE(pRootStorage); _ASSERTE(pszName); hr = pRootStorage->OpenStorage( pszName, NULL, STGM_READ | STGM_SHARE_EXCLUSIVE, NULL, 0, &pStg); if (SUCCEEDED(hr) && pStg) { if (!MakePath(szPath, MAX_PATH, pszDir, CW2T(pszName), pszExt)) { hr = E_INVALIDARG; } else { _tprintf(TEXT("%s\n"), szPath); // Create the storage file. hr = StgCreateDocfile( CT2W(szPath), STGM_WRITE | STGM_SHARE_EXCLUSIVE | STGM_CREATE, 0, &pFileStg); if (SUCCEEDED(hr) && pFileStg) { hr = pStg->CopyTo(0, NULL, NULL, pFileStg); } } } SAFE_RELEASE(pFileStg); SAFE_RELEASE(pStg); if (FAILED(hr)) { error(TEXT("Error: failed to save storage '%s'.\n"), CW2T(pszName)); } return hr; } // Saves the stream from the given record to a file with or without // an extension based on whether or not fIncludeExt is set to TRUE. UINT SaveStream(MSIHANDLE hRecord, [Pre(Null=Maybe)] LPCTSTR pszDir, BOOL fIncludeExt) { UINT uiError = NOERROR; TCHAR szPath[MAX_PATH]; LPTSTR pszName = NULL; DWORD cchName = 0; CHAR szBuffer[256]; DWORD cbBuffer = sizeof(szBuffer); std::ofstream file; try { // Get the name of the stream but skip if \005SummaryInformation stream. if (ERROR_SUCCESS == GetString(hRecord, 1, &pszName, &cchName) && 0 != _tcsncmp(pszName, TEXT("\005"), 1)) { // Create the local file with the simple CFile write-only class. do { uiError = MsiRecordReadStream(hRecord, 2, szBuffer, &cbBuffer); if (ERROR_SUCCESS == uiError) { if (!file.is_open()) { // Create the file path if the file is not created and assume the extension // if requested by fIncludeExt. if (!MakePathForData(szPath, MAX_PATH, pszDir, pszName, szBuffer, cbBuffer, fIncludeExt)) { throw std::exception("Could not create the output path name"); } // Create the local file in which data is written. _tprintf(TEXT("%s\n"), szPath); file.open(CT2A(szPath), std::ios_base::binary); } file.write(szBuffer, cbBuffer ); } else { throw std::exception("Could not read from stream."); } } while (cbBuffer); } } catch (std::exception& ex) { error(TEXT("Error: %s\n"), CA2T(ex.what())); uiError = ERROR_CANNOT_MAKE; } file.close(); if (pszName) { delete [] pszName; pszName = NULL; } return uiError; } // Creates a patch for the given file using MakePath, but uses what of the // buffer it can to guess the file type and infer a common file extension. LPTSTR MakePathForData(LPTSTR pszDest, size_t cchDest, LPCTSTR pszDir, LPCTSTR pszName, LPCVOID pBuffer, size_t cbBuffer, BOOL fIncludeExt) { LPTSTR pszExt = NULL; if (fIncludeExt) { // Cabinet (*.cab) files. if (0 == memcmp(pBuffer, "MSCF", 4)) { pszExt = TEXT(".cab"); } // Executable files. Assumed to be .dll (more common). else if (0 == memcmp(pBuffer, "MZ", 2)) { pszExt = TEXT(".dll"); } // Icon (*.ico) files. Only assumed because they're common. else if (0 == memcmp(pBuffer, "\0\0\1\0", 4)) { pszExt = TEXT(".ico"); } // Bitmap (*.bmp) files. else if (0 == memcmp(pBuffer, "BM", 2)) { pszExt = TEXT(".bmp"); } // GIF (*.gif) files. else if (0 == memcmp(pBuffer, "GIF", 3)) { pszExt = TEXT(".gif"); } // PING (*.png) files. else if (0 == memcmp(pBuffer, "\x89PNG", 4)) { pszExt = TEXT(".png"); } // TIFF (*.tif) files. else if (0 == memcmp(pBuffer, "II", 2)) { pszExt = TEXT(".tif"); } } return MakePath(pszDest, cchDest, pszDir, pszName, pszExt); }