The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
// Relocate.cpp : Defines the Relocate custom action.
//
// Copyright (c) Curtis Jewell 2009, 2010
//
// This code is free software; you can redistribute it and/or modify it
// under the same terms as Perl itself.

// ##### EXPORTED FUCTIONS: RelocateMSI RelocateMSM

#include "stdafx.h"
#include "ErrorMsg.h"

UINT Blip(
	MSIHANDLE hModule) // Handle of MSI being installed. [in]
{
	// Set up variables.
	PMSIHANDLE hRecord = ::MsiCreateRecord(4);
    RETURN_IF_NULL(hRecord, hModule, _T("ERROR: Blip - MsiCreateRecord FAILED"));

	UINT uiAnswer = ::MsiRecordSetInteger(hRecord, 1, 2);
	RETURN_ON_ERROR(uiAnswer, hModule, _T("ERROR: Blip - MsiRecordSetInteger.1 FAILED"));
	
	uiAnswer = ::MsiRecordSetInteger(hRecord, 2, 0);
	RETURN_ON_ERROR(uiAnswer, hModule, _T("ERROR: Blip - MsiRecordSetInteger.2 FAILED"));

	uiAnswer = ::MsiRecordSetInteger(hRecord, 3, 0);
	RETURN_ON_ERROR(uiAnswer, hModule, _T("ERROR: Blip - MsiRecordSetInteger.3 FAILED"));

	uiAnswer = ::MsiRecordSetInteger(hRecord, 4, 0);
	RETURN_ON_ERROR(uiAnswer, hModule, _T("ERROR: Blip - MsiRecordSetInteger.4 FAILED"));

	// Send the message
	uiAnswer = ::MsiProcessMessage(hModule, INSTALLMESSAGE(INSTALLMESSAGE_PROGRESS), hRecord);

	// Corrects return value for use with MSI_OK.
	switch (uiAnswer) {
	case IDOK:
	case 0: // Means no action was taken...
		return ERROR_SUCCESS;
	case IDCANCEL:
		return ERROR_INSTALL_USEREXIT;
	default:
		return ERROR_INSTALL_FAILURE;
	}
}

UINT _stdcall Relocate_MoveFile(
	MSIHANDLE hModule,
	const TCHAR *sFileTo,   // Original name of the file 
	const TCHAR *sFileFrom) // File to rename into its place.
{
	// Create "to-the-side" location for original file.
	TCHAR sFileSpec[_MAX_PATH];
	_tcscpy_s(sFileSpec, _MAX_PATH, sFileTo);
	_tcscat_s(sFileSpec, _MAX_PATH, _T(".old"));

	BOOL bAnswer = TRUE;

	// Move original file out of the way.
	bAnswer = ::MoveFileEx(sFileTo, sFileSpec, MOVEFILE_WRITE_THROUGH);
	if (bAnswer == FALSE) {
		SimpleLogString4(hModule, _T("ERROR: Relocate_MoveFile - failed to move.1 "), sFileTo, _T(" to "), sFileSpec);
		return ERROR_INSTALL_FAILURE;
	}

	// Move new file into the old file's place.
	bAnswer = ::MoveFileEx(sFileFrom, sFileTo, MOVEFILE_WRITE_THROUGH);
	if (bAnswer == FALSE) {
		SimpleLogString4(hModule, _T("ERROR: Relocate_MoveFile - failed to move.2 "), sFileFrom, _T(" to "), sFileTo);
		return ERROR_INSTALL_FAILURE;
	}

	// Remove the old file.
	bAnswer = ::DeleteFile(sFileSpec);
	if (bAnswer == FALSE) {
		SimpleLogString2(hModule, _T("ERROR: Relocate_MoveFile - failed to delete "), sFileSpec);
		return ERROR_INSTALL_FAILURE;
	}
	
	return ERROR_SUCCESS;
}


void _stdcall Relocate_GetSearchString(
	MSIHANDLE hModule,
		  TCHAR *sString,   // String to search for [out] 
	const TCHAR *sDir,      // Directory to search for.
	const TCHAR *sType)     // Type of search to do.
{
	if (0 == _tcscmp(sType, _T("backslash"))) {
		_tcscpy_s(sString, _MAX_PATH, sDir);
		return;
	}

	TCHAR *sWorkP = NULL;
	if (0 == _tcscmp(sType, _T("slash"))) {
		_tcscpy_s(sString, _MAX_PATH, sDir);

		// Change each backslash to a slash.
		sWorkP = _tcschr(sString, _T('\\'));
		while(sWorkP) {
			*sWorkP = _T('/');
			sWorkP = _tcschr(sString, _T('\\'));
		}

		return;
	}

	if (0 == _tcscmp(sType, _T("doublebackslash"))) {
		TCHAR *sWork1 = (TCHAR *)malloc((_MAX_PATH + 1) * sizeof(TCHAR));
		TCHAR *sWork2 = (TCHAR *)malloc((_MAX_PATH + 1) * sizeof(TCHAR));

		_tcscpy_s(sString, _MAX_PATH, sDir);

		// Change to slashes first.
		sWorkP = _tcschr(sString, _T('\\'));
		while(sWorkP) {
			*sWorkP = _T('/');
			sWorkP = _tcschr(sString, _T('\\'));
		}

		_tcscpy_s(sWork1, _MAX_PATH, sString);
		sWorkP = _tcschr(sWork1, _T('/'));
		while (sWorkP) {
			// Fix to make sure that we don't attempt to buffer-overrun.
			if (1 == _tcslen(sWorkP)) {
				// Add the second slash.
				_tcscat_s(sWork1, _MAX_PATH, _T("\\"));

				// Set the first one.
				*sWorkP = _T('\\');

				// We're done.
				break;
			}

			// Block it to copying before the slash.
			*sWorkP = _T('\0');
			_tcscpy_s(sWork2, _MAX_PATH, sWork1);

			// Append the backslashes, and then the rest of the string.
			_tcscat_s(sWork2, _MAX_PATH, _T("\\\\"));
			_tcscat_s(sWork2, _MAX_PATH, ++sWorkP);

			// Copy back out of our work area, so that they match.
			_tcscpy_s(sWork1,  _MAX_PATH, sWork2);

			// Try another search.
			sWorkP = _tcschr(sWork1, _T('/'));
		}

		// Copy our answer out.
		_tcscpy_s(sString, _MAX_PATH, sWork1);
		free((void*)sWork1);
		free((void*)sWork2);
		return;
	}

	if (0 == _tcscmp(sType, _T("url"))) {
		TCHAR sWork3[_MAX_PATH + 1];
		_tcscpy_s(sWork3, _MAX_PATH, sDir);

		// Change each backslash to a slash.
		sWorkP = _tcschr(sWork3, _T('\\'));
		while(sWorkP) {
			*sWorkP = _T('/');
			sWorkP = _tcschr(sWork3, _T('\\'));
		}

		_tcscpy_s(sString, _MAX_PATH, _T("file:///"));
		_tcscat_s(sString, _MAX_PATH, sWork3);
		return;
	}

	// Error: Return an empty string.
	_tcscpy_s(sString, _MAX_PATH, _T(""));
	return;

}


UINT _stdcall Relocate_File(
	MSIHANDLE hModule,
	const TCHAR *sDirectoryFrom, // Directory to relocate from
	const TCHAR *sDirectoryTo,   // Directory to relocate to
	const TCHAR *sFile,          // File to relocate
	const TCHAR *sType)          // Type of relocation to do.
{
	UINT uiAnswer = ERROR_SUCCESS;
	TCHAR sFileIn[_MAX_PATH];
	TCHAR sFileOut[_MAX_PATH];
	_tcscpy_s(sFileIn, _MAX_PATH, sDirectoryTo);
	_tcscat_s(sFileIn, _MAX_PATH, sFile);
	_tcscpy_s(sFileOut, _MAX_PATH, sFileIn);
	_tcscat_s(sFileOut, _MAX_PATH, _T(".new"));

	TCHAR sStringIn[_MAX_PATH];
	TCHAR sStringOut[_MAX_PATH];

	// Log the fact that we're relocating a file.
	SimpleLogString4(hModule, _T("Relocate_File file="), sFile, _T(" type="), sType);

	// Get the strings to look for.
	Relocate_GetSearchString(hModule, sStringIn,  sDirectoryFrom, sType);
	Relocate_GetSearchString(hModule, sStringOut, sDirectoryTo,   sType);

	if (0 == _tcscmp(sStringIn, _T(""))) {
		SimpleLogString1(hModule, _T("ERROR: Relocate_File - empty In"));
		return ERROR_INSTALL_FAILURE;
	}

	if (0 == _tcscmp(sStringOut, _T(""))) {
		SimpleLogString1(hModule, _T("ERROR: Relocate_File - empty Out"));
		return ERROR_INSTALL_FAILURE;
	}

	// Open our files.
	FILE *fFileIn;
	FILE *fFileOut;
	errno_t eAnswer = 0;
	eAnswer = _tfopen_s(&fFileIn, sFileIn, _T("rtS"));
	if (eAnswer != 0) {
		SimpleLogString3(hModule, _T("WARNING: Relocate_File - probably non-existing file "), sFileIn, _T(" - gonna continue, just skipping this one"));
		return ERROR_SUCCESS;
	}
	eAnswer = _tfopen_s(&fFileOut, sFileOut, _T("wt"));
	if (eAnswer != 0) {
		SimpleLogString2(hModule, _T("ERROR: Relocate_File - open.1 'wt' failed for "), sFileOut);
		fclose(fFileIn);
		return ERROR_INSTALL_FAILURE;
	}

	// Set up our variables for the relocation.
	TCHAR  sLine[32767];
	TCHAR  sWork1[32767];
	TCHAR  sWork2[32767];
	TCHAR *sLoc   = NULL;
	size_t iStringInLength = _tcslen(sStringIn);
	size_t iStringOutLength = _tcslen(sStringOut);
	int iErrorFlag = 0;
	long lLine = 0;
	// Do the relocation.
	while (!feof(fFileIn)) {

		// Deal with errors. 
		if( _fgetts( sLine, 32766, fFileIn ) == NULL) {
			if (iErrorFlag) {
				fclose(fFileIn);
				fclose(fFileOut);
				::DeleteFile(sFileOut);
				uiAnswer = ERROR_INSTALL_FAILURE;
				break;
			}
			iErrorFlag++;
			continue;
		}
		_tcscpy_s(sWork1, 32766, sLine);
		sLoc = _tcsstr(sWork1, sStringIn);
		while (sLoc) {
			// "Cap" the initial string and copy it to sWork2, then append sStringOut to it.
			*sLoc = _T('\0');
			_tcscpy_s(sWork2, 32766, sWork1);
			_tcscat_s(sWork2, 32766, sStringOut);

			// Append the rest of the line.
			sLoc += iStringInLength;
			_tcscat_s(sWork2, 32766, sLoc);

			// Copy back out of our work area, so that they match.
			_tcscpy_s(sWork1, 32766, sWork2);

			// Advance along the string.
			sLoc -= iStringInLength;
			sLoc += iStringInLength;

			// Try another search.
			sLoc = _tcsstr(sLoc, sStringIn);

			// If we're done, copy to sLine so it can be written out.
			if (!sLoc) {
				_tcscpy_s(sLine, 32766, sWork1);
			}
		}
		
		// Write the line out.
		_fputts(sLine, fFileOut);

		// Check every so often for cancel button.
		lLine++;
		if (0 == (lLine % 100)) {
			uiAnswer = Blip(hModule);
			if (ERROR_SUCCESS != uiAnswer) {
				fclose(fFileIn);
				fclose(fFileOut);
				::DeleteFile(sFileOut);
				return uiAnswer;
			}
		}

	}

	fflush(fFileOut);
	fclose(fFileIn);
	fclose(fFileOut);

	RETURN_ON_ERROR(uiAnswer, hModule, _T("ERROR: Relocate_File.1 FAILED"));

	BOOL bAnswer = TRUE;
	// Check for readonly status on the file.
	DWORD dwAttributes = ::GetFileAttributes(sFileIn);
	if (dwAttributes && FILE_ATTRIBUTE_READONLY) {
		bAnswer = ::SetFileAttributes(sFileIn, dwAttributes && !FILE_ATTRIBUTE_READONLY);
		if (bAnswer == FALSE) { 
			::DeleteFile(sFileOut);
			SimpleLogString2(hModule, _T("ERROR: Relocate_File - failed to unset ReadOnly on "), sFileIn);
			uiAnswer = ERROR_INSTALL_FAILURE; 
		}
		RETURN_ON_ERROR(uiAnswer, hModule, _T("ERROR: Relocate_File.2 FAILED"));
	}

	uiAnswer = Relocate_MoveFile(hModule, sFileIn, sFileOut);
	RETURN_ON_ERROR(uiAnswer, hModule, _T("ERROR: Relocate_File.3 FAILED"));

	// Set readonly status back.
	if (dwAttributes && FILE_ATTRIBUTE_READONLY) {
		::SetFileAttributes(sFileIn, dwAttributes);
	}

	return uiAnswer;
}


UINT Relocate_Worker(
	MSIHANDLE hModule,				// Handle of MSI being installed. [in]
									// Passed to most other routines.
	const TCHAR *sInstallDirectory,	// Directory being installed into.
	const TCHAR *sRelocationFile)	// File to use to relocate.
{
	UINT uiAnswer;
	FILE *fRelocationFileIn;
	FILE *fRelocationFileOut;
	TCHAR sLine[_MAX_PATH + 12];
	TCHAR sFileFrom[_MAX_PATH + 1];
	TCHAR sFileTo[_MAX_PATH + 1];
	TCHAR sDirectoryFrom[_MAX_PATH + 1];
	TCHAR sDirectoryTo[_MAX_PATH + 1];

	// Log what we're doing.
	SimpleLogString4(hModule, _T("Relocate_Worker dir="), sInstallDirectory, _T(" file="), sRelocationFile);

	// Get filename to open
	_tcscpy_s(sFileFrom, _MAX_PATH, sRelocationFile);
	_tcscpy_s(sFileTo, _MAX_PATH, sRelocationFile);
	_tcscat_s(sFileTo, _MAX_PATH, _T(".new"));

	// Open our files.
	errno_t eAnswer = 0;
	eAnswer = _tfopen_s(&fRelocationFileIn, sFileFrom, _T("rtS"));
	if (eAnswer != 0) {
		SimpleLogString3(hModule, _T("WARNING: Relocate_Worker - probably non-existing file "), sFileFrom, _T(" - gonna continue, just skipping this one"));
		return ERROR_SUCCESS;
	}

	// First line of relocation file has where to relocate from.
	if( _fgetts( sDirectoryFrom, _MAX_PATH + 1, fRelocationFileIn ) == NULL) {
		fclose(fRelocationFileIn);
		SimpleLogString1(hModule, _T("ERROR: Relocate_Worker - invalid first line"));
		return ERROR_INSTALL_FAILURE;
	}

	// Take off the line ending.
	*(sDirectoryFrom + _tcslen(sDirectoryFrom) - 1) = _T('\0');
	SimpleLogString2(hModule, _T("Relocate_Worker sDirectoryFrom="), sDirectoryFrom);

	// Second parameter is where to relocate to.
	_tcscpy_s(sDirectoryTo, _MAX_PATH, sInstallDirectory);

	// Make sire it ends in a slash.
	if (*(sDirectoryTo + _tcslen(sDirectoryTo) - 1) != _T('\\')) {
		_tcscat_s(sDirectoryTo, _MAX_PATH, _T("\\"));
	}

	// We don't need to relocate if the directories are identical, right? right.
	// It's not an error, however.
	if (0 == _tcscmp(sDirectoryFrom, sDirectoryTo)) {
		fclose(fRelocationFileIn);
		SimpleLogString2(hModule, _T("WARNING: Relocate_Worker, from/to dirs are the same - "),  sDirectoryTo);
		return ERROR_SUCCESS;
	}

	// Open up the file to write relocating to.
	eAnswer = _tfopen_s(&fRelocationFileOut, sFileTo, _T("wt"));
	if (eAnswer != 0) {
		fclose(fRelocationFileIn);
		SimpleLogString2(hModule, _T("ERROR: open.2 'wt' failed for "), sFileTo);
		return ERROR_INSTALL_FAILURE;
	}

	// Log what we're doing.
	SimpleLogString2(hModule, _T("Relocating from "), sDirectoryFrom);

	// Put where to relocate to in the file.
	_fputts(sDirectoryTo, fRelocationFileOut);
	_fputts(_T("\n"), fRelocationFileOut);

	int iErrorFlag = 0;
	// Go into the relocation loop.
	while (!feof(fRelocationFileIn)) {

		// Deal with errors. 
		if( _fgetts( sLine, _MAX_PATH + 11, fRelocationFileIn ) == NULL) {
			if (iErrorFlag) {
				SimpleLogString1(hModule, _T("ERROR: cannot fgetts line"));
				uiAnswer = ERROR_INSTALL_FAILURE;
				break;
			}
			iErrorFlag++;
			continue;
		}

		// Deal with comments.
		if ('#' == *sLine) {
			_fputts(sLine, fRelocationFileOut);
			continue;
		}

		// Deal with lines that contain only whitespace.
		if (_tcslen(sLine) <= _tcsspn(sLine, _T(" \t\n")))
			continue;

		// Check for the colon.
		if (NULL == _tcschr(sLine, _T(':')))
			continue;

		// We have a good line. So put it back out before we tokenize it.
		_fputts(sLine, fRelocationFileOut);

		// Take off the line ending for tokenizing purposes.
		*(sLine + _tcslen(sLine) - 1) = _T('\0');

		// Tokenize the line.
		TCHAR  sFileToRelocate[_MAX_PATH + 1];
		TCHAR  sRelocationType[17];
		TCHAR *sTokenContext = NULL;
		TCHAR *sToken = NULL;

		sToken = _tcstok_s(sLine, _T(":"), &sTokenContext);
		_tcscpy_s(sFileToRelocate, _MAX_PATH, sToken);
		sToken = _tcstok_s(NULL, _T("\n"), &sTokenContext);
		_tcscpy_s(sRelocationType, 16, sToken);

		// Actually relocate the file.
		uiAnswer = Relocate_File(hModule, sDirectoryFrom, sDirectoryTo, sFileToRelocate, sRelocationType);
		if (uiAnswer != ERROR_SUCCESS) {
			SimpleLogString1(hModule, _T("ERROR: Relocate_File failure"));
			break;
		}
	}

	fflush(fRelocationFileOut);
	fclose(fRelocationFileIn);
	fclose(fRelocationFileOut);

	RETURN_ON_ERROR(uiAnswer, hModule, _T("ERROR: Relocate_Worker.1 FAILED"));

    uiAnswer = Relocate_MoveFile(hModule, sFileFrom, sFileTo);	
	RETURN_ON_ERROR(uiAnswer, hModule, _T("ERROR: Relocate_Worker.2 FAILED"));

    SimpleLogString1(hModule, _T("Relocate_Worker end"));
	return uiAnswer;
}



UINT __stdcall RelocateMSM(
	MSIHANDLE hModule) // Handle of MSI being installed. [in]
	                   // Passed to most other routines.
{
	TCHAR sInstallDirectory[MAX_PATH + 1];
	TCHAR sRelocationFile[MAX_PATH + 1];
	TCHAR sCAData[MAX_PATH * 2 + 6];
	UINT uiAnswer;
	DWORD dwPropLength;

	// Get directory to relocate to.
	dwPropLength = MAX_PATH * 2 + 5; 
	uiAnswer = ::MsiGetProperty(hModule, TEXT("CustomActionData"), sCAData, &dwPropLength); 
	RETURN_ON_ERROR(uiAnswer, hModule, _T("ERROR: MsiGetProperty[CustomActionData] FAILED"));

	TCHAR *sTokenContext = NULL;
	TCHAR *sToken = NULL;

	SimpleLogString2(hModule, _T("RelocateMSM CustomActionData="), sCAData);

	sToken = _tcstok_s(sCAData, _T(";"), &sTokenContext);
	if (!sToken || 0 != _tcscmp(sToken, _T("MSM"))) {
		SimpleLogString1(hModule, _T("ERROR: RelocateMSM CustomActionData - invalid token1"));
		return ERROR_INSTALL_FAILURE;
	}
	
	sToken = _tcstok_s(NULL, _T(";"), &sTokenContext);
	if (!sToken) {
		SimpleLogString1(hModule, _T("ERROR: RelocateMSM CustomActionData - invalid token2"));
		return ERROR_INSTALL_FAILURE;
    }
	_tcscpy_s(sInstallDirectory, _MAX_PATH, sToken);

	sToken = _tcstok_s(NULL, _T(";"), &sTokenContext);
	if (!sToken) {
		SimpleLogString1(hModule, _T("ERROR: RelocateMSM CustomActionData - invalid token3"));
		return ERROR_INSTALL_FAILURE;
	}
	_tcscpy_s(sRelocationFile, _MAX_PATH, sToken);

	uiAnswer = Relocate_Worker(hModule, sInstallDirectory, sRelocationFile);    
	SimpleLogString1(hModule, _T("RelocateMSM end"));
	return uiAnswer;
}


UINT __stdcall RelocateMSI(
	MSIHANDLE hModule) // Handle of MSI being installed. [in]
	                   // Passed to most other routines.
{
	TCHAR sInstallDirectory[MAX_PATH + 1];
	TCHAR sRelocationFile[MAX_PATH + 1];
	TCHAR sCAData[MAX_PATH * 2 + 7];
	UINT uiAnswer;
	DWORD dwPropLength;

	// Get directory to relocate to.
	dwPropLength = MAX_PATH * 2 + 6; 
	uiAnswer = ::MsiGetProperty(hModule, TEXT("CustomActionData"), sCAData, &dwPropLength); 
	RETURN_ON_ERROR(uiAnswer, hModule, _T("ERROR: RelocateMSI MsiGetProperty[CustomActionData] FAILED"));

	TCHAR *sTokenContext = NULL;
	TCHAR *sToken = NULL;

	SimpleLogString2(hModule, _T("RelocateMSI CustomActionData="), sCAData);	

    sToken = _tcstok_s(sCAData, _T(";"), &sTokenContext);
	if (!sToken || 0 != _tcscmp(sToken, _T("MSI"))) {
		SimpleLogString1(hModule, _T("ERROR: RelocateMSI CustomActionData - invalid token1"));
		return ERROR_INSTALL_FAILURE;
	}

	sToken = _tcstok_s(NULL, _T(";"), &sTokenContext);
	if (!sToken) {
		SimpleLogString1(hModule, _T("ERROR: RelocateMSI CustomActionData - invalid token2"));
		return ERROR_INSTALL_FAILURE;
    }
	_tcscpy_s(sInstallDirectory, _MAX_PATH, sToken);

	sToken = _tcstok_s(NULL, _T(";"), &sTokenContext);
	if (!sToken) {
		SimpleLogString1(hModule, _T("ERROR: RelocateMSI CustomActionData - invalid token3"));
		return ERROR_INSTALL_FAILURE;
	}
	_tcscpy_s(sRelocationFile, _MAX_PATH, sToken);

	uiAnswer = Relocate_Worker(hModule, sInstallDirectory, sRelocationFile);
	SimpleLogString1(hModule, _T("RelocateMSI end"));
	return uiAnswer;
}