aio-builder/libexec/MsiX/MsiX.cpp
2013-09-11 00:13:08 +02:00

534 lines
14 KiB
C++

#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<LPTSTR>(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<LPTSTR>(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<LPTSTR>(pszPath);
}
_ftprintf(out, TEXT("Usage: %s <file> [/out <output>] [/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 <output> 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);
}