mirror of
https://github.com/HarbourMasters/Shipwright.git
synced 2025-01-30 23:10:14 -05:00
b3e299dbde
Some modifications to handle backslashes and forward slashes, along with some optimizations to speed up OTR generation.
1176 lines
40 KiB
C++
1176 lines
40 KiB
C++
/*****************************************************************************/
|
|
/* SFilePatchArchives.cpp Copyright (c) Ladislav Zezula 2010 */
|
|
/*---------------------------------------------------------------------------*/
|
|
/* Description: */
|
|
/*---------------------------------------------------------------------------*/
|
|
/* Date Ver Who Comment */
|
|
/* -------- ---- --- ------- */
|
|
/* 18.08.10 1.00 Lad The first version of SFilePatchArchives.cpp */
|
|
/*****************************************************************************/
|
|
|
|
#define __STORMLIB_SELF__
|
|
#include "StormLib.h"
|
|
#include "StormCommon.h"
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Local structures
|
|
|
|
#define MAX_SC2_PATCH_PREFIX 0x80
|
|
|
|
#define PATCH_SIGNATURE_HEADER 0x48435450
|
|
#define PATCH_SIGNATURE_MD5 0x5f35444d
|
|
#define PATCH_SIGNATURE_XFRM 0x4d524658
|
|
|
|
#define SIZE_OF_XFRM_HEADER 0x0C
|
|
|
|
// Header for incremental patch files
|
|
typedef struct _MPQ_PATCH_HEADER
|
|
{
|
|
//-- PATCH header -----------------------------------
|
|
DWORD dwSignature; // 'PTCH'
|
|
DWORD dwSizeOfPatchData; // Size of the entire patch (decompressed)
|
|
DWORD dwSizeBeforePatch; // Size of the file before patch
|
|
DWORD dwSizeAfterPatch; // Size of file after patch
|
|
|
|
//-- MD5 block --------------------------------------
|
|
DWORD dwMD5; // 'MD5_'
|
|
DWORD dwMd5BlockSize; // Size of the MD5 block, including the signature and size itself
|
|
BYTE md5_before_patch[0x10]; // MD5 of the original (unpached) file
|
|
BYTE md5_after_patch[0x10]; // MD5 of the patched file
|
|
|
|
//-- XFRM block -------------------------------------
|
|
DWORD dwXFRM; // 'XFRM'
|
|
DWORD dwXfrmBlockSize; // Size of the XFRM block, includes XFRM header and patch data
|
|
DWORD dwPatchType; // Type of patch ('BSD0' or 'COPY')
|
|
|
|
// Followed by the patch data
|
|
} MPQ_PATCH_HEADER, *PMPQ_PATCH_HEADER;
|
|
|
|
typedef struct _BLIZZARD_BSDIFF40_FILE
|
|
{
|
|
ULONGLONG Signature;
|
|
ULONGLONG CtrlBlockSize;
|
|
ULONGLONG DataBlockSize;
|
|
ULONGLONG NewFileSize;
|
|
} BLIZZARD_BSDIFF40_FILE, *PBLIZZARD_BSDIFF40_FILE;
|
|
|
|
typedef struct _BSDIFF_CTRL_BLOCK
|
|
{
|
|
DWORD dwAddDataLength;
|
|
DWORD dwMovDataLength;
|
|
DWORD dwOldMoveLength;
|
|
|
|
} BSDIFF_CTRL_BLOCK, *PBSDIFF_CTRL_BLOCK;
|
|
|
|
typedef struct _LOCALIZED_MPQ_INFO
|
|
{
|
|
const char * szNameTemplate; // Name template
|
|
size_t nLangOffset; // Offset of the language
|
|
size_t nLength; // Length of the name template
|
|
} LOCALIZED_MPQ_INFO, *PLOCALIZED_MPQ_INFO;
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Local variables
|
|
|
|
// 4-byte groups for all languages
|
|
static const char * LanguageList = "baseteenenUSenGBenCNenTWdeDEesESesMXfrFRitITkoKRptBRptPTruRUzhCNzhTW";
|
|
|
|
// List of localized MPQs for World of Warcraft
|
|
static LOCALIZED_MPQ_INFO LocaleMpqs_WoW[] =
|
|
{
|
|
{"expansion1-locale-####", 18, 22},
|
|
{"expansion1-speech-####", 18, 22},
|
|
{"expansion2-locale-####", 18, 22},
|
|
{"expansion2-speech-####", 18, 22},
|
|
{"expansion3-locale-####", 18, 22},
|
|
{"expansion3-speech-####", 18, 22},
|
|
{"locale-####", 7, 11},
|
|
{"speech-####", 7, 11},
|
|
{NULL, 0, 0}
|
|
};
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Local functions
|
|
|
|
static inline bool IsPatchMetadataFile(TFileEntry * pFileEntry)
|
|
{
|
|
// The file must ave a name
|
|
if(pFileEntry->szFileName != NULL && (pFileEntry->dwFlags & MPQ_FILE_PATCH_FILE) == 0)
|
|
{
|
|
// The file must be small
|
|
if(0 < pFileEntry->dwFileSize && pFileEntry->dwFileSize < 0x40)
|
|
{
|
|
// Compare the plain name
|
|
return (_stricmp(GetPlainFileName(pFileEntry->szFileName), PATCH_METADATA_NAME) == 0);
|
|
}
|
|
}
|
|
|
|
// Not a patch_metadata
|
|
return false;
|
|
}
|
|
|
|
static void Decompress_RLE(LPBYTE pbDecompressed, DWORD cbDecompressed, LPBYTE pbCompressed, DWORD cbCompressed)
|
|
{
|
|
LPBYTE pbDecompressedEnd = pbDecompressed + cbDecompressed;
|
|
LPBYTE pbCompressedEnd = pbCompressed + cbCompressed;
|
|
BYTE RepeatCount;
|
|
BYTE OneByte;
|
|
|
|
// Cut the initial DWORD from the compressed chunk
|
|
pbCompressed += sizeof(DWORD);
|
|
|
|
// Pre-fill decompressed buffer with zeros
|
|
memset(pbDecompressed, 0, cbDecompressed);
|
|
|
|
// Unpack
|
|
while(pbCompressed < pbCompressedEnd && pbDecompressed < pbDecompressedEnd)
|
|
{
|
|
OneByte = *pbCompressed++;
|
|
|
|
// Is it a repetition byte ?
|
|
if(OneByte & 0x80)
|
|
{
|
|
RepeatCount = (OneByte & 0x7F) + 1;
|
|
for(BYTE i = 0; i < RepeatCount; i++)
|
|
{
|
|
if(pbDecompressed == pbDecompressedEnd || pbCompressed == pbCompressedEnd)
|
|
break;
|
|
|
|
*pbDecompressed++ = *pbCompressed++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
pbDecompressed += (OneByte + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
static DWORD LoadFilePatch_COPY(TMPQFile * hf, PMPQ_PATCH_HEADER pFullPatch)
|
|
{
|
|
DWORD cbBytesToRead = pFullPatch->dwSizeOfPatchData - sizeof(MPQ_PATCH_HEADER);
|
|
DWORD cbBytesRead = 0;
|
|
|
|
// Simply load the rest of the patch
|
|
SFileReadFile((HANDLE)hf, (pFullPatch + 1), cbBytesToRead, &cbBytesRead, NULL);
|
|
return (cbBytesRead == cbBytesToRead) ? ERROR_SUCCESS : ERROR_FILE_CORRUPT;
|
|
}
|
|
|
|
static DWORD LoadFilePatch_BSD0(TMPQFile * hf, PMPQ_PATCH_HEADER pFullPatch)
|
|
{
|
|
LPBYTE pbDecompressed = (LPBYTE)(pFullPatch + 1);
|
|
LPBYTE pbCompressed = NULL;
|
|
DWORD cbDecompressed = 0;
|
|
DWORD cbCompressed = 0;
|
|
DWORD dwBytesRead = 0;
|
|
DWORD dwErrCode = ERROR_SUCCESS;
|
|
|
|
// Calculate the size of compressed data
|
|
cbDecompressed = pFullPatch->dwSizeOfPatchData - sizeof(MPQ_PATCH_HEADER);
|
|
cbCompressed = pFullPatch->dwXfrmBlockSize - SIZE_OF_XFRM_HEADER;
|
|
|
|
// Is that file compressed?
|
|
if(cbCompressed < cbDecompressed)
|
|
{
|
|
pbCompressed = STORM_ALLOC(BYTE, cbCompressed);
|
|
if(pbCompressed == NULL)
|
|
dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
|
|
|
|
// Read the compressed patch data
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
SFileReadFile((HANDLE)hf, pbCompressed, cbCompressed, &dwBytesRead, NULL);
|
|
if(dwBytesRead != cbCompressed)
|
|
dwErrCode = ERROR_FILE_CORRUPT;
|
|
}
|
|
|
|
// Decompress the data
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
Decompress_RLE(pbDecompressed, cbDecompressed, pbCompressed, cbCompressed);
|
|
|
|
if(pbCompressed != NULL)
|
|
STORM_FREE(pbCompressed);
|
|
}
|
|
else
|
|
{
|
|
SFileReadFile((HANDLE)hf, pbDecompressed, cbDecompressed, &dwBytesRead, NULL);
|
|
if(dwBytesRead != cbDecompressed)
|
|
dwErrCode = ERROR_FILE_CORRUPT;
|
|
}
|
|
|
|
return dwErrCode;
|
|
}
|
|
|
|
static DWORD ApplyFilePatch_COPY(
|
|
TMPQPatcher * pPatcher,
|
|
PMPQ_PATCH_HEADER pFullPatch,
|
|
LPBYTE pbTarget,
|
|
LPBYTE pbSource)
|
|
{
|
|
// Sanity checks
|
|
assert(pPatcher->cbMaxFileData >= pPatcher->cbFileData);
|
|
pFullPatch = pFullPatch;
|
|
|
|
// Copy the patch data as-is
|
|
memcpy(pbTarget, pbSource, pPatcher->cbFileData);
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
static DWORD ApplyFilePatch_BSD0(
|
|
TMPQPatcher * pPatcher,
|
|
PMPQ_PATCH_HEADER pFullPatch,
|
|
LPBYTE pbTarget,
|
|
LPBYTE pbSource)
|
|
{
|
|
PBLIZZARD_BSDIFF40_FILE pBsdiff;
|
|
PBSDIFF_CTRL_BLOCK pCtrlBlock;
|
|
LPBYTE pbPatchData = (LPBYTE)(pFullPatch + 1);
|
|
LPBYTE pDataBlock;
|
|
LPBYTE pExtraBlock;
|
|
LPBYTE pbOldData = pbSource;
|
|
LPBYTE pbNewData = pbTarget;
|
|
DWORD dwCombineSize;
|
|
DWORD dwNewOffset = 0; // Current position to patch
|
|
DWORD dwOldOffset = 0; // Current source position
|
|
DWORD dwNewSize; // Patched file size
|
|
DWORD dwOldSize = pPatcher->cbFileData; // File size before patch
|
|
|
|
// Get pointer to the patch header
|
|
// Format of BSDIFF header corresponds to original BSDIFF, which is:
|
|
// 0000 8 bytes signature "BSDIFF40"
|
|
// 0008 8 bytes size of the control block
|
|
// 0010 8 bytes size of the data block
|
|
// 0018 8 bytes new size of the patched file
|
|
pBsdiff = (PBLIZZARD_BSDIFF40_FILE)pbPatchData;
|
|
pbPatchData += sizeof(BLIZZARD_BSDIFF40_FILE);
|
|
|
|
// Get pointer to the 32-bit BSDIFF control block
|
|
// The control block follows immediately after the BSDIFF header
|
|
// and consists of three 32-bit integers
|
|
// 0000 4 bytes Length to copy from the BSDIFF data block the new file
|
|
// 0004 4 bytes Length to copy from the BSDIFF extra block
|
|
// 0008 4 bytes Size to increment source file offset
|
|
pCtrlBlock = (PBSDIFF_CTRL_BLOCK)pbPatchData;
|
|
pbPatchData += (size_t)BSWAP_INT64_UNSIGNED(pBsdiff->CtrlBlockSize);
|
|
|
|
// Get the pointer to the data block
|
|
pDataBlock = (LPBYTE)pbPatchData;
|
|
pbPatchData += (size_t)BSWAP_INT64_UNSIGNED(pBsdiff->DataBlockSize);
|
|
|
|
// Get the pointer to the extra block
|
|
pExtraBlock = (LPBYTE)pbPatchData;
|
|
dwNewSize = (DWORD)BSWAP_INT64_UNSIGNED(pBsdiff->NewFileSize);
|
|
|
|
// Now patch the file
|
|
while(dwNewOffset < dwNewSize)
|
|
{
|
|
DWORD dwAddDataLength = BSWAP_INT32_UNSIGNED(pCtrlBlock->dwAddDataLength);
|
|
DWORD dwMovDataLength = BSWAP_INT32_UNSIGNED(pCtrlBlock->dwMovDataLength);
|
|
DWORD dwOldMoveLength = BSWAP_INT32_UNSIGNED(pCtrlBlock->dwOldMoveLength);
|
|
DWORD i;
|
|
|
|
// Sanity check
|
|
if((dwNewOffset + dwAddDataLength) > dwNewSize)
|
|
return ERROR_FILE_CORRUPT;
|
|
|
|
// Read the diff string to the target buffer
|
|
memcpy(pbNewData + dwNewOffset, pDataBlock, dwAddDataLength);
|
|
pDataBlock += dwAddDataLength;
|
|
|
|
// Get the longest block that we can combine
|
|
dwCombineSize = ((dwOldOffset + dwAddDataLength) >= dwOldSize) ? (dwOldSize - dwOldOffset) : dwAddDataLength;
|
|
if((dwNewOffset + dwCombineSize) > dwNewSize || (dwNewOffset + dwCombineSize) < dwNewOffset)
|
|
return ERROR_FILE_CORRUPT;
|
|
|
|
// Now combine the patch data with the original file
|
|
for(i = 0; i < dwCombineSize; i++)
|
|
pbNewData[dwNewOffset + i] = pbNewData[dwNewOffset + i] + pbOldData[dwOldOffset + i];
|
|
|
|
// Move the offsets
|
|
dwNewOffset += dwAddDataLength;
|
|
dwOldOffset += dwAddDataLength;
|
|
|
|
// Sanity check
|
|
if((dwNewOffset + dwMovDataLength) > dwNewSize)
|
|
return ERROR_FILE_CORRUPT;
|
|
|
|
// Copy the data from the extra block in BSDIFF patch
|
|
memcpy(pbNewData + dwNewOffset, pExtraBlock, dwMovDataLength);
|
|
pExtraBlock += dwMovDataLength;
|
|
dwNewOffset += dwMovDataLength;
|
|
|
|
// Move the old offset
|
|
if(dwOldMoveLength & 0x80000000)
|
|
dwOldMoveLength = 0x80000000 - dwOldMoveLength;
|
|
dwOldOffset += dwOldMoveLength;
|
|
pCtrlBlock++;
|
|
}
|
|
|
|
// The size after patch must match
|
|
if(dwNewOffset != pFullPatch->dwSizeAfterPatch)
|
|
return ERROR_FILE_CORRUPT;
|
|
|
|
// Update the new data size
|
|
pPatcher->cbFileData = dwNewOffset;
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
static PMPQ_PATCH_HEADER LoadFullFilePatch(TMPQFile * hf, MPQ_PATCH_HEADER & PatchHeader)
|
|
{
|
|
PMPQ_PATCH_HEADER pFullPatch;
|
|
DWORD dwErrCode = ERROR_SUCCESS;
|
|
|
|
// BSWAP the entire header, if needed
|
|
BSWAP_ARRAY32_UNSIGNED(&PatchHeader, sizeof(DWORD) * 6);
|
|
BSWAP_ARRAY32_UNSIGNED(&PatchHeader.dwXFRM, sizeof(DWORD) * 3);
|
|
|
|
// Verify the signatures in the patch header
|
|
if(PatchHeader.dwSignature != PATCH_SIGNATURE_HEADER || PatchHeader.dwMD5 != PATCH_SIGNATURE_MD5 || PatchHeader.dwXFRM != PATCH_SIGNATURE_XFRM)
|
|
return NULL;
|
|
|
|
// Allocate space for patch header and compressed data
|
|
pFullPatch = (PMPQ_PATCH_HEADER)STORM_ALLOC(BYTE, PatchHeader.dwSizeOfPatchData);
|
|
if(pFullPatch != NULL)
|
|
{
|
|
// Copy the patch header
|
|
memcpy(pFullPatch, &PatchHeader, sizeof(MPQ_PATCH_HEADER));
|
|
|
|
// Read the patch, depending on patch type
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
switch(PatchHeader.dwPatchType)
|
|
{
|
|
case 0x59504f43: // 'COPY'
|
|
dwErrCode = LoadFilePatch_COPY(hf, pFullPatch);
|
|
break;
|
|
|
|
case 0x30445342: // 'BSD0'
|
|
dwErrCode = LoadFilePatch_BSD0(hf, pFullPatch);
|
|
break;
|
|
|
|
default:
|
|
dwErrCode = ERROR_FILE_CORRUPT;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If something failed, free the patch buffer
|
|
if(dwErrCode != ERROR_SUCCESS)
|
|
{
|
|
STORM_FREE(pFullPatch);
|
|
pFullPatch = NULL;
|
|
}
|
|
}
|
|
|
|
// Give the result to the caller
|
|
return pFullPatch;
|
|
}
|
|
|
|
static DWORD ApplyFilePatch(
|
|
TMPQPatcher * pPatcher,
|
|
PMPQ_PATCH_HEADER pFullPatch)
|
|
{
|
|
LPBYTE pbSource = (pPatcher->nCounter & 0x1) ? pPatcher->pbFileData2 : pPatcher->pbFileData1;
|
|
LPBYTE pbTarget = (pPatcher->nCounter & 0x1) ? pPatcher->pbFileData1 : pPatcher->pbFileData2;
|
|
DWORD dwErrCode;
|
|
|
|
// Sanity checks
|
|
assert(pFullPatch->dwSizeAfterPatch <= pPatcher->cbMaxFileData);
|
|
|
|
// Apply the patch according to the type
|
|
switch(pFullPatch->dwPatchType)
|
|
{
|
|
case 0x59504f43: // 'COPY'
|
|
dwErrCode = ApplyFilePatch_COPY(pPatcher, pFullPatch, pbTarget, pbSource);
|
|
break;
|
|
|
|
case 0x30445342: // 'BSD0'
|
|
dwErrCode = ApplyFilePatch_BSD0(pPatcher, pFullPatch, pbTarget, pbSource);
|
|
break;
|
|
|
|
default:
|
|
dwErrCode = ERROR_FILE_CORRUPT;
|
|
break;
|
|
}
|
|
|
|
// Verify MD5 after patch
|
|
if(dwErrCode == ERROR_SUCCESS && pFullPatch->dwSizeAfterPatch != 0)
|
|
{
|
|
// Verify the patched file
|
|
if(!VerifyDataBlockHash(pbTarget, pFullPatch->dwSizeAfterPatch, pFullPatch->md5_after_patch))
|
|
dwErrCode = ERROR_FILE_CORRUPT;
|
|
|
|
// Copy the MD5 of the new block
|
|
memcpy(pPatcher->this_md5, pFullPatch->md5_after_patch, MD5_DIGEST_SIZE);
|
|
}
|
|
|
|
return dwErrCode;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Local functions (patch prefix matching)
|
|
|
|
static bool CreatePatchPrefix(TMPQArchive * ha, const char * szFileName, size_t nLength)
|
|
{
|
|
TMPQNamePrefix * pNewPrefix;
|
|
|
|
// If the length of the patch prefix was not entered, find it
|
|
// Not that the patch prefix must always begin with backslash
|
|
if(szFileName != NULL && nLength == 0)
|
|
nLength = strlen(szFileName);
|
|
|
|
// Create the patch prefix
|
|
pNewPrefix = (TMPQNamePrefix *)STORM_ALLOC(BYTE, sizeof(TMPQNamePrefix) + nLength + 1);
|
|
if(pNewPrefix != NULL)
|
|
{
|
|
// Fill the name prefix. Also add the backslash
|
|
if(szFileName && nLength)
|
|
{
|
|
memcpy(pNewPrefix->szPatchPrefix, szFileName, nLength);
|
|
if(pNewPrefix->szPatchPrefix[nLength - 1] != '\\')
|
|
pNewPrefix->szPatchPrefix[nLength++] = '\\';
|
|
}
|
|
|
|
// Terminate the string and fill the length
|
|
pNewPrefix->szPatchPrefix[nLength] = 0;
|
|
pNewPrefix->nLength = nLength;
|
|
}
|
|
|
|
ha->pPatchPrefix = pNewPrefix;
|
|
return (pNewPrefix != NULL);
|
|
}
|
|
|
|
static bool CheckAndCreatePatchPrefix(TMPQArchive * ha, const char * szPatchPrefix, size_t nLength)
|
|
{
|
|
char szTempName[MAX_SC2_PATCH_PREFIX + 0x41];
|
|
bool bResult = false;
|
|
|
|
// Prepare the patch file name
|
|
if(nLength > MAX_SC2_PATCH_PREFIX)
|
|
return false;
|
|
|
|
// Prepare the patched file name
|
|
memcpy(szTempName, szPatchPrefix, nLength);
|
|
memcpy(&szTempName[nLength], "\\(patch_metadata)", 18);
|
|
|
|
// Verifywhether that file exists
|
|
if(GetFileEntryLocale(ha, szTempName, 0) != NULL)
|
|
bResult = CreatePatchPrefix(ha, szPatchPrefix, nLength);
|
|
|
|
return bResult;
|
|
}
|
|
|
|
static bool IsMatchingPatchFile(
|
|
TMPQArchive * ha,
|
|
const char * szFileName,
|
|
LPBYTE pbBaseFileMd5)
|
|
{
|
|
MPQ_PATCH_HEADER PatchHeader = {0};
|
|
HANDLE hFile = NULL;
|
|
DWORD dwTransferred = 0;
|
|
DWORD dwFlags = 0;
|
|
bool bResult = false;
|
|
|
|
// Open the file and load the patch header
|
|
if(SFileOpenFileEx((HANDLE)ha, szFileName, SFILE_OPEN_BASE_FILE, &hFile))
|
|
{
|
|
// Retrieve the flags. We need to know whether the file is a patch or not
|
|
SFileGetFileInfo(hFile, SFileInfoFlags, &dwFlags, sizeof(DWORD), &dwTransferred);
|
|
if(dwFlags & MPQ_FILE_PATCH_FILE)
|
|
{
|
|
// Load the patch header
|
|
SFileReadFile(hFile, &PatchHeader, sizeof(MPQ_PATCH_HEADER), &dwTransferred, NULL);
|
|
BSWAP_ARRAY32_UNSIGNED(pPatchHeader, sizeof(DWORD) * 6);
|
|
|
|
// If the file contains an incremental patch,
|
|
// compare the "MD5 before patching" with the base file MD5
|
|
if(dwTransferred == sizeof(MPQ_PATCH_HEADER) && PatchHeader.dwSignature == PATCH_SIGNATURE_HEADER)
|
|
bResult = (!memcmp(PatchHeader.md5_before_patch, pbBaseFileMd5, MD5_DIGEST_SIZE));
|
|
}
|
|
else
|
|
{
|
|
// TODO: How to match it if it's not an incremental patch?
|
|
// Example: StarCraft II\Updates\enGB\s2-update-enGB-23258.MPQ:
|
|
// Mods\Core.SC2Mod\enGB.SC2Assets\StreamingBuckets.txt"
|
|
bResult = false;
|
|
}
|
|
|
|
// Close the file
|
|
SFileCloseFile(hFile);
|
|
}
|
|
|
|
return bResult;
|
|
}
|
|
|
|
static const char * FindArchiveLanguage(TMPQArchive * ha, PLOCALIZED_MPQ_INFO pMpqInfo)
|
|
{
|
|
TFileEntry * pFileEntry;
|
|
const char * szLanguage = LanguageList;
|
|
char szFileName[0x40];
|
|
|
|
// Iterate through all localized languages
|
|
while(pMpqInfo->szNameTemplate != NULL)
|
|
{
|
|
// Iterate through all languages
|
|
for(szLanguage = LanguageList; szLanguage[0] != 0; szLanguage += 4)
|
|
{
|
|
// Construct the file name
|
|
memcpy(szFileName, pMpqInfo->szNameTemplate, pMpqInfo->nLength);
|
|
szFileName[pMpqInfo->nLangOffset + 0] = szLanguage[0];
|
|
szFileName[pMpqInfo->nLangOffset + 1] = szLanguage[1];
|
|
szFileName[pMpqInfo->nLangOffset + 2] = szLanguage[2];
|
|
szFileName[pMpqInfo->nLangOffset + 3] = szLanguage[3];
|
|
|
|
// Append the suffix
|
|
memcpy(szFileName + pMpqInfo->nLength, "-md5.lst", 9);
|
|
|
|
// Check whether the name exists
|
|
pFileEntry = GetFileEntryLocale(ha, szFileName, 0);
|
|
if(pFileEntry != NULL)
|
|
return szLanguage;
|
|
}
|
|
|
|
// Move to the next language name
|
|
pMpqInfo++;
|
|
}
|
|
|
|
// Not found
|
|
return NULL;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Finding ratch prefix for an temporary build of WoW (Pre-Cataclysm)
|
|
|
|
static bool FindPatchPrefix_WoW_13164_13623(TMPQArchive * haBase, TMPQArchive * haPatch)
|
|
{
|
|
const char * szPatchPrefix;
|
|
char szNamePrefix[0x08];
|
|
|
|
// Try to find the language of the MPQ archive
|
|
szPatchPrefix = FindArchiveLanguage(haBase, LocaleMpqs_WoW);
|
|
if(szPatchPrefix == NULL)
|
|
szPatchPrefix = "Base";
|
|
|
|
// Format the patch prefix
|
|
szNamePrefix[0] = szPatchPrefix[0];
|
|
szNamePrefix[1] = szPatchPrefix[1];
|
|
szNamePrefix[2] = szPatchPrefix[2];
|
|
szNamePrefix[3] = szPatchPrefix[3];
|
|
szNamePrefix[4] = '\\';
|
|
szNamePrefix[5] = 0;
|
|
return CreatePatchPrefix(haPatch, szNamePrefix, 5);
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Finding patch prefix for Starcraft II (Pre-Legacy of the Void)
|
|
|
|
//
|
|
// This method tries to match the patch by placement of the archive (in the game subdirectory)
|
|
//
|
|
// Archive Path: %GAME_DIR%\Mods\SwarmMulti.SC2Mod\Base.SC2Data
|
|
// Patch Prefix: Mods\SwarmMulti.SC2Mod\Base.SC2Data
|
|
//
|
|
// Archive Path: %ANY_DIR%\MPQ_2013_v4_Mods#Liberty.SC2Mod#enGB.SC2Data
|
|
// Patch Prefix: Mods\Liberty.SC2Mod\enGB.SC2Data
|
|
//
|
|
|
|
static bool CheckPatchPrefix_SC2_ArchiveName(
|
|
TMPQArchive * haPatch,
|
|
const TCHAR * szPathPtr,
|
|
const TCHAR * szSeparator,
|
|
const TCHAR * szPathEnd,
|
|
const TCHAR * szExpectedString,
|
|
size_t cchExpectedString)
|
|
{
|
|
char szPatchPrefix[MAX_SC2_PATCH_PREFIX+0x41];
|
|
size_t nLength = 0;
|
|
bool bResult = false;
|
|
|
|
// Check whether the length is equal to the length of the expected string
|
|
if((size_t)(szSeparator - szPathPtr) == cchExpectedString)
|
|
{
|
|
// Now check the string itself
|
|
if(!_tcsnicmp(szPathPtr, szExpectedString, szSeparator - szPathPtr))
|
|
{
|
|
// Copy the name string
|
|
for(; szPathPtr < szPathEnd; szPathPtr++)
|
|
{
|
|
if(szPathPtr[0] != _T('/') && szPathPtr[0] != _T('#'))
|
|
szPatchPrefix[nLength++] = (char)szPathPtr[0];
|
|
else
|
|
szPatchPrefix[nLength++] = '\\';
|
|
}
|
|
|
|
// Check and create the patch prefix
|
|
bResult = CheckAndCreatePatchPrefix(haPatch, szPatchPrefix, nLength);
|
|
}
|
|
}
|
|
|
|
return bResult;
|
|
}
|
|
|
|
static bool FindPatchPrefix_SC2_ArchiveName(TMPQArchive * haBase, TMPQArchive * haPatch)
|
|
{
|
|
const TCHAR * szPathBegin = FileStream_GetFileName(haBase->pStream);
|
|
const TCHAR * szSeparator = NULL;
|
|
const TCHAR * szPathEnd = szPathBegin + _tcslen(szPathBegin);
|
|
const TCHAR * szPathPtr;
|
|
int nSlashCount = 0;
|
|
int nDotCount = 0;
|
|
|
|
// Skip the part where the patch prefix would be too long
|
|
if((szPathEnd - szPathBegin) > MAX_SC2_PATCH_PREFIX)
|
|
szPathBegin = szPathEnd - MAX_SC2_PATCH_PREFIX;
|
|
|
|
// Search for the file extension
|
|
for(szPathPtr = szPathEnd; szPathPtr > szPathBegin; szPathPtr--)
|
|
{
|
|
if(szPathPtr[0] == _T('.'))
|
|
{
|
|
nDotCount++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Search for the possible begin of the prefix name
|
|
for(/* NOTHING */; szPathPtr > szPathBegin; szPathPtr--)
|
|
{
|
|
// Check the slashes, backslashes and hashes
|
|
if(szPathPtr[0] == _T('\\') || szPathPtr[0] == _T('/') || szPathPtr[0] == _T('#'))
|
|
{
|
|
if(nDotCount == 0)
|
|
return false;
|
|
szSeparator = szPathPtr;
|
|
nSlashCount++;
|
|
}
|
|
|
|
// Check the path parts
|
|
if(szSeparator != NULL && nSlashCount >= nDotCount)
|
|
{
|
|
if(CheckPatchPrefix_SC2_ArchiveName(haPatch, szPathPtr, szSeparator, szPathEnd, _T("Battle.net"), 10))
|
|
return true;
|
|
if(CheckPatchPrefix_SC2_ArchiveName(haPatch, szPathPtr, szSeparator, szPathEnd, _T("Campaigns"), 9))
|
|
return true;
|
|
if(CheckPatchPrefix_SC2_ArchiveName(haPatch, szPathPtr, szSeparator, szPathEnd, _T("Mods"), 4))
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Not matched, sorry
|
|
return false;
|
|
}
|
|
|
|
//
|
|
// This method tries to read the patch prefix from a helper file
|
|
//
|
|
// Example
|
|
// =========================================================
|
|
// MPQ File Name: MPQ_2013_v4_Base1.SC2Data
|
|
// Helper File : MPQ_2013_v4_Base1.SC2Data-PATCH
|
|
// File Contains: PatchPrefix=Mods\Core.SC2Mod\Base.SC2Data
|
|
// Patch Prefix : Mods\Core.SC2Mod\Base.SC2Data
|
|
//
|
|
|
|
static bool ExtractPatchPrefixFromFile(const TCHAR * szHelperFile, char * szPatchPrefix, size_t nMaxChars, size_t * PtrLength)
|
|
{
|
|
TFileStream * pStream;
|
|
ULONGLONG FileSize = 0;
|
|
size_t nLength;
|
|
char szFileData[MAX_PATH+1];
|
|
bool bResult = false;
|
|
|
|
pStream = FileStream_OpenFile(szHelperFile, STREAM_FLAG_READ_ONLY);
|
|
if(pStream != NULL)
|
|
{
|
|
// Retrieve and check the file size
|
|
FileStream_GetSize(pStream, &FileSize);
|
|
if(12 <= FileSize && FileSize < MAX_PATH)
|
|
{
|
|
// Read the entire file to memory
|
|
if(FileStream_Read(pStream, NULL, szFileData, (DWORD)FileSize))
|
|
{
|
|
// Terminate the buffer with zero
|
|
szFileData[(DWORD)FileSize] = 0;
|
|
|
|
// The file data must begin with the "PatchPrefix" variable
|
|
if(!_strnicmp(szFileData, "PatchPrefix", 11))
|
|
{
|
|
char * szLinePtr = szFileData + 11;
|
|
char * szLineEnd;
|
|
|
|
// Skip spaces or '='
|
|
while(szLinePtr[0] == ' ' || szLinePtr[0] == '=')
|
|
szLinePtr++;
|
|
szLineEnd = szLinePtr;
|
|
|
|
// Find the end
|
|
while(szLineEnd[0] != 0 && szLineEnd[0] != 0x0A && szLineEnd[0] != 0x0D)
|
|
szLineEnd++;
|
|
nLength = (size_t)(szLineEnd - szLinePtr);
|
|
|
|
// Copy the variable
|
|
if(szLineEnd > szLinePtr && nLength <= nMaxChars)
|
|
{
|
|
memcpy(szPatchPrefix, szLinePtr, nLength);
|
|
szPatchPrefix[nLength] = 0;
|
|
PtrLength[0] = nLength;
|
|
bResult = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Close the stream
|
|
FileStream_Close(pStream);
|
|
}
|
|
|
|
return bResult;
|
|
}
|
|
|
|
|
|
static bool FindPatchPrefix_SC2_HelperFile(TMPQArchive * haBase, TMPQArchive * haPatch)
|
|
{
|
|
TCHAR szHelperFile[MAX_PATH+1];
|
|
char szPatchPrefix[MAX_SC2_PATCH_PREFIX+0x41];
|
|
size_t nLength = 0;
|
|
bool bResult = false;
|
|
|
|
// Create the name of the patch helper file
|
|
_tcscpy(szHelperFile, FileStream_GetFileName(haBase->pStream));
|
|
if(_tcslen(szHelperFile) + 6 > MAX_PATH)
|
|
return false;
|
|
_tcscat(szHelperFile, _T("-PATCH"));
|
|
|
|
// Open the patch helper file and read the line
|
|
if(ExtractPatchPrefixFromFile(szHelperFile, szPatchPrefix, MAX_SC2_PATCH_PREFIX, &nLength))
|
|
bResult = CheckAndCreatePatchPrefix(haPatch, szPatchPrefix, nLength);
|
|
|
|
return bResult;
|
|
}
|
|
|
|
//
|
|
// Find match in Starcraft II patch MPQs
|
|
// Match a LST file in the root directory if the MPQ with any of the file in subdirectories
|
|
//
|
|
// The problem:
|
|
// File in the base MPQ: enGB-md5.lst
|
|
// File in the patch MPQ: Campaigns\Liberty.SC2Campaign\enGB.SC2Assets\enGB-md5.lst
|
|
// Campaigns\Liberty.SC2Campaign\enGB.SC2Data\enGB-md5.lst
|
|
// Campaigns\LibertyStory.SC2Campaign\enGB.SC2Data\enGB-md5.lst
|
|
// Campaigns\LibertyStory.SC2Campaign\enGB.SC2Data\enGB-md5.lst Mods\Core.SC2Mod\enGB.SC2Assets\enGB-md5.lst
|
|
// Mods\Core.SC2Mod\enGB.SC2Data\enGB-md5.lst
|
|
// Mods\Liberty.SC2Mod\enGB.SC2Assets\enGB-md5.lst
|
|
// Mods\Liberty.SC2Mod\enGB.SC2Data\enGB-md5.lst
|
|
// Mods\LibertyMulti.SC2Mod\enGB.SC2Data\enGB-md5.lst
|
|
//
|
|
// Solution:
|
|
// We need to match the file by its MD5
|
|
//
|
|
|
|
static bool FindPatchPrefix_SC2_MatchFiles(TMPQArchive * haBase, TMPQArchive * haPatch, TFileEntry * pBaseEntry)
|
|
{
|
|
TMPQNamePrefix * pPatchPrefix;
|
|
char * szPatchFileName;
|
|
char * szPlainName;
|
|
size_t cchWorkBuffer = 0x400;
|
|
bool bResult = false;
|
|
|
|
// First-level patches: Find the same file within the patch archive
|
|
// and verify by MD5-before-patch
|
|
if(haBase->haPatch == NULL)
|
|
{
|
|
TFileEntry * pFileTableEnd = haPatch->pFileTable + haPatch->dwFileTableSize;
|
|
TFileEntry * pFileEntry;
|
|
|
|
// Allocate working buffer for merging LST file
|
|
szPatchFileName = STORM_ALLOC(char, cchWorkBuffer);
|
|
if(szPatchFileName != NULL)
|
|
{
|
|
// Parse the entire file table
|
|
for(pFileEntry = haPatch->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++)
|
|
{
|
|
// Look for "patch_metadata" file
|
|
if(IsPatchMetadataFile(pFileEntry))
|
|
{
|
|
// Construct the name of the MD5 file
|
|
strcpy(szPatchFileName, pFileEntry->szFileName);
|
|
szPlainName = (char *)GetPlainFileName(szPatchFileName);
|
|
strcpy(szPlainName, pBaseEntry->szFileName);
|
|
|
|
// Check for matching MD5 file
|
|
if(IsMatchingPatchFile(haPatch, szPatchFileName, pBaseEntry->md5))
|
|
{
|
|
bResult = CreatePatchPrefix(haPatch, szPatchFileName, (size_t)(szPlainName - szPatchFileName));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete the merge buffer
|
|
STORM_FREE(szPatchFileName);
|
|
}
|
|
}
|
|
|
|
// For second-level patches, just take the patch prefix from the lower level patch
|
|
else
|
|
{
|
|
// There must be at least two patches in the chain
|
|
assert(haBase->haPatch->pPatchPrefix != NULL);
|
|
pPatchPrefix = haBase->haPatch->pPatchPrefix;
|
|
|
|
// Copy the patch prefix
|
|
bResult = CreatePatchPrefix(haPatch,
|
|
pPatchPrefix->szPatchPrefix,
|
|
pPatchPrefix->nLength);
|
|
}
|
|
|
|
return bResult;
|
|
}
|
|
|
|
// Note: pBaseEntry is the file entry of the base version of "StreamingBuckets.txt"
|
|
static bool FindPatchPrefix_SC2(TMPQArchive * haBase, TMPQArchive * haPatch, TFileEntry * pBaseEntry)
|
|
{
|
|
// Method 1: Try it by the placement of the archive.
|
|
// Works when someone is opening an archive in the game (sub)directory
|
|
if(FindPatchPrefix_SC2_ArchiveName(haBase, haPatch))
|
|
return true;
|
|
|
|
// Method 2: Try to locate the Name.Ext-PATCH file and read the patch prefix from it
|
|
if(FindPatchPrefix_SC2_HelperFile(haBase, haPatch))
|
|
return true;
|
|
|
|
// Method 3: Try to pair any version of "StreamingBuckets.txt" from the patch MPQ
|
|
// with the "StreamingBuckets.txt" in the base MPQ. Does not always work
|
|
if(FindPatchPrefix_SC2_MatchFiles(haBase, haPatch, pBaseEntry))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
//
|
|
// Patch prefix is the path subdirectory where the patched files are within MPQ.
|
|
//
|
|
// Example 1:
|
|
// Main MPQ: locale-enGB.MPQ
|
|
// Patch MPQ: wow-update-12694.MPQ
|
|
// File in main MPQ: DBFilesClient\Achievement.dbc
|
|
// File in patch MPQ: enGB\DBFilesClient\Achievement.dbc
|
|
// Path prefix: enGB
|
|
//
|
|
// Example 2:
|
|
// Main MPQ: expansion1.MPQ
|
|
// Patch MPQ: wow-update-12694.MPQ
|
|
// File in main MPQ: DBFilesClient\Achievement.dbc
|
|
// File in patch MPQ: Base\DBFilesClient\Achievement.dbc
|
|
// Path prefix: Base
|
|
//
|
|
// Example 3:
|
|
// Main MPQ: %GAME%\Battle.net\Battle.net.MPQ
|
|
// Patch MPQ: s2-update-base-26147.MPQ
|
|
// File in main MPQ: Battle.net\i18n\deDE\String\CLIENT_ACHIEVEMENTS.xml
|
|
// File in patch MPQ: Battle.net\Battle.net.MPQ\Battle.net\i18n\deDE\String\CLIENT_ACHIEVEMENTS.xml
|
|
// Path prefix: Battle.net\Battle.net.MPQ
|
|
//
|
|
// Example 4:
|
|
// Main MPQ: %GAME%\Campaigns\Liberty.SC2Campaign\enGB.SC2Data
|
|
// *OR* %ANY_DIR%\%ANY_NAME%Campaigns#Liberty.SC2Campaign#enGB.SC2Data
|
|
// Patch MPQ: s2-update-enGB-23258.MPQ
|
|
// File in main MPQ: LocalizedData\GameHotkeys.txt
|
|
// File in patch MPQ: Campaigns\Liberty.SC2Campaign\enGB.SC2Data\LocalizedData\GameHotkeys.txt
|
|
// Patch Prefix: Campaigns\Liberty.SC2Campaign\enGB.SC2Data
|
|
//
|
|
|
|
static bool FindPatchPrefix(TMPQArchive * haBase, TMPQArchive * haPatch, const char * szPatchPathPrefix)
|
|
{
|
|
TFileEntry * pFileEntry;
|
|
|
|
// If the patch prefix was explicitly entered, we use that one
|
|
if(szPatchPathPrefix != NULL)
|
|
return CreatePatchPrefix(haPatch, szPatchPathPrefix, 0);
|
|
|
|
// Patches for World of Warcraft - they mostly do not use prefix.
|
|
// All patches that use patch prefix have the "base\\(patch_metadata) file present
|
|
if(GetFileEntryLocale(haPatch, "base\\" PATCH_METADATA_NAME, 0))
|
|
return FindPatchPrefix_WoW_13164_13623(haBase, haPatch);
|
|
|
|
// Updates for Starcraft II
|
|
// Match: LocalizedData\GameHotkeys.txt <==> Campaigns\Liberty.SC2Campaign\enGB.SC2Data\LocalizedData\GameHotkeys.txt
|
|
// All Starcraft II base archives seem to have the file "StreamingBuckets.txt" present
|
|
pFileEntry = GetFileEntryLocale(haBase, "StreamingBuckets.txt", 0);
|
|
if(pFileEntry != NULL)
|
|
return FindPatchPrefix_SC2(haBase, haPatch, pFileEntry);
|
|
|
|
// Diablo III patch MPQs don't use patch prefix
|
|
// Hearthstone MPQs don't use patch prefix
|
|
CreatePatchPrefix(haPatch, NULL, 0);
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Public functions (StormLib internals)
|
|
|
|
bool IsIncrementalPatchFile(const void * pvData, DWORD cbData, LPDWORD pdwPatchedFileSize)
|
|
{
|
|
PMPQ_PATCH_HEADER pPatchHeader = (PMPQ_PATCH_HEADER)pvData;
|
|
BLIZZARD_BSDIFF40_FILE DiffFile;
|
|
DWORD dwPatchType;
|
|
|
|
if(cbData >= sizeof(MPQ_PATCH_HEADER) + sizeof(BLIZZARD_BSDIFF40_FILE))
|
|
{
|
|
dwPatchType = BSWAP_INT32_UNSIGNED(pPatchHeader->dwPatchType);
|
|
if(dwPatchType == 0x30445342)
|
|
{
|
|
// Give the caller the patch file size
|
|
if(pdwPatchedFileSize != NULL)
|
|
{
|
|
Decompress_RLE((LPBYTE)&DiffFile, sizeof(BLIZZARD_BSDIFF40_FILE), (LPBYTE)(pPatchHeader + 1), sizeof(BLIZZARD_BSDIFF40_FILE));
|
|
DiffFile.NewFileSize = BSWAP_INT64_UNSIGNED(DiffFile.NewFileSize);
|
|
*pdwPatchedFileSize = (DWORD)DiffFile.NewFileSize;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
DWORD Patch_InitPatcher(TMPQPatcher * pPatcher, TMPQFile * hf)
|
|
{
|
|
DWORD cbMaxFileData = 0;
|
|
|
|
// Overflow check
|
|
if((cbMaxFileData + (DWORD)sizeof(MPQ_PATCH_HEADER)) < cbMaxFileData)
|
|
return ERROR_NOT_ENOUGH_MEMORY;
|
|
if(hf->hfPatch == NULL)
|
|
return ERROR_INVALID_PARAMETER;
|
|
|
|
// Initialize the entire structure with zeros
|
|
memset(pPatcher, 0, sizeof(TMPQPatcher));
|
|
|
|
// Copy the MD5 of the current file
|
|
memcpy(pPatcher->this_md5, hf->pFileEntry->md5, MD5_DIGEST_SIZE);
|
|
|
|
// Find out the biggest data size needed during the patching process
|
|
while(hf != NULL)
|
|
{
|
|
if(hf->pFileEntry->dwFileSize > cbMaxFileData)
|
|
cbMaxFileData = hf->pFileEntry->dwFileSize;
|
|
hf = hf->hfPatch;
|
|
}
|
|
|
|
// Allocate primary and secondary buffer
|
|
pPatcher->pbFileData1 = STORM_ALLOC(BYTE, cbMaxFileData);
|
|
pPatcher->pbFileData2 = STORM_ALLOC(BYTE, cbMaxFileData);
|
|
if(!pPatcher->pbFileData1 || !pPatcher->pbFileData2)
|
|
return ERROR_NOT_ENOUGH_MEMORY;
|
|
|
|
pPatcher->cbMaxFileData = cbMaxFileData;
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
//
|
|
// Note: The patch may either be applied to the base file or to the previous version
|
|
// In Starcraft II, Mods\Core.SC2Mod\Base.SC2Data, file StreamingBuckets.txt:
|
|
//
|
|
// Base file MD5: 31376b0344b6df59ad009d4296125539
|
|
//
|
|
// s2-update-base-23258: from 31376b0344b6df59ad009d4296125539 to 941a82683452e54bf024a8d491501824
|
|
// s2-update-base-24540: from 31376b0344b6df59ad009d4296125539 to 941a82683452e54bf024a8d491501824
|
|
// s2-update-base-26147: from 31376b0344b6df59ad009d4296125539 to d5d5253c762fac6b9761240288a0771a
|
|
// s2-update-base-28522: from 31376b0344b6df59ad009d4296125539 to 5a76c4b356920aab7afd22e0e1913d7a
|
|
// s2-update-base-30508: from 31376b0344b6df59ad009d4296125539 to 8cb0d4799893fe801cc78ae4488a3671
|
|
// s2-update-base-32283: from 31376b0344b6df59ad009d4296125539 to 8cb0d4799893fe801cc78ae4488a3671
|
|
//
|
|
// We don't keep all intermediate versions in memory, as it would cause massive
|
|
// memory usage during patching process. A prime example is the file
|
|
// DBFilesClient\\Item-Sparse.db2 from locale-enGB.MPQ (WoW 16965), which has
|
|
// 9 patches in a row, each requiring 70 MB memory (35 MB patch data + 35 MB work buffer)
|
|
//
|
|
|
|
DWORD Patch_Process(TMPQPatcher * pPatcher, TMPQFile * hf)
|
|
{
|
|
PMPQ_PATCH_HEADER pFullPatch;
|
|
MPQ_PATCH_HEADER PatchHeader1;
|
|
MPQ_PATCH_HEADER PatchHeader2 = {0};
|
|
TMPQFile * hfBase = hf;
|
|
DWORD cbBytesRead = 0;
|
|
DWORD dwErrCode = ERROR_SUCCESS;
|
|
|
|
// Move to the first patch
|
|
assert(hfBase->pbFileData == NULL);
|
|
assert(hfBase->cbFileData == 0);
|
|
hf = hf->hfPatch;
|
|
|
|
// Read the header of the current patch
|
|
SFileReadFile((HANDLE)hf, &PatchHeader1, sizeof(MPQ_PATCH_HEADER), &cbBytesRead, NULL);
|
|
if(cbBytesRead != sizeof(MPQ_PATCH_HEADER))
|
|
return ERROR_FILE_CORRUPT;
|
|
|
|
// Perform the patching process
|
|
while(dwErrCode == ERROR_SUCCESS && hf != NULL)
|
|
{
|
|
// Try to read the next patch header. If the md5_before_patch
|
|
// still matches we go directly to the next one and repeat
|
|
while(hf->hfPatch != NULL)
|
|
{
|
|
// Attempt to read the patch header
|
|
SFileReadFile((HANDLE)hf->hfPatch, &PatchHeader2, sizeof(MPQ_PATCH_HEADER), &cbBytesRead, NULL);
|
|
if(cbBytesRead != sizeof(MPQ_PATCH_HEADER))
|
|
return ERROR_FILE_CORRUPT;
|
|
|
|
// Compare the md5_before_patch
|
|
if(memcmp(PatchHeader2.md5_before_patch, pPatcher->this_md5, MD5_DIGEST_SIZE))
|
|
break;
|
|
|
|
// Move one patch fuhrter
|
|
PatchHeader1 = PatchHeader2;
|
|
hf = hf->hfPatch;
|
|
}
|
|
|
|
// Allocate memory for the patch data
|
|
pFullPatch = LoadFullFilePatch(hf, PatchHeader1);
|
|
if(pFullPatch != NULL)
|
|
{
|
|
// Apply the patch
|
|
dwErrCode = ApplyFilePatch(pPatcher, pFullPatch);
|
|
STORM_FREE(pFullPatch);
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = ERROR_FILE_CORRUPT;
|
|
}
|
|
|
|
// Move to the next patch
|
|
PatchHeader1 = PatchHeader2;
|
|
pPatcher->nCounter++;
|
|
hf = hf->hfPatch;
|
|
}
|
|
|
|
// Put the result data to the file structure
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
// Swap the pointer to the file data structure
|
|
if(pPatcher->nCounter & 0x01)
|
|
{
|
|
hfBase->pbFileData = pPatcher->pbFileData2;
|
|
pPatcher->pbFileData2 = NULL;
|
|
}
|
|
else
|
|
{
|
|
hfBase->pbFileData = pPatcher->pbFileData1;
|
|
pPatcher->pbFileData1 = NULL;
|
|
}
|
|
|
|
// Also supply the data size
|
|
hfBase->cbFileData = pPatcher->cbFileData;
|
|
}
|
|
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
void Patch_Finalize(TMPQPatcher * pPatcher)
|
|
{
|
|
if(pPatcher != NULL)
|
|
{
|
|
if(pPatcher->pbFileData1 != NULL)
|
|
STORM_FREE(pPatcher->pbFileData1);
|
|
if(pPatcher->pbFileData2 != NULL)
|
|
STORM_FREE(pPatcher->pbFileData2);
|
|
|
|
memset(pPatcher, 0, sizeof(TMPQPatcher));
|
|
}
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Public functions
|
|
|
|
bool WINAPI SFileOpenPatchArchive(
|
|
HANDLE hMpq,
|
|
const TCHAR * szPatchMpqName,
|
|
const char * szPatchPathPrefix,
|
|
DWORD dwFlags)
|
|
{
|
|
TMPQArchive * haPatch;
|
|
TMPQArchive * ha = (TMPQArchive *)hMpq;
|
|
HANDLE hPatchMpq = NULL;
|
|
DWORD dwErrCode = ERROR_SUCCESS;
|
|
|
|
// Keep compiler happy
|
|
dwFlags = dwFlags;
|
|
|
|
// Verify input parameters
|
|
if(!IsValidMpqHandle(hMpq))
|
|
dwErrCode = ERROR_INVALID_HANDLE;
|
|
if(szPatchMpqName == NULL || *szPatchMpqName == 0)
|
|
dwErrCode = ERROR_INVALID_PARAMETER;
|
|
|
|
//
|
|
// We don't allow adding patches to archives that have been open for write
|
|
//
|
|
// Error scenario:
|
|
//
|
|
// 1) Open archive for writing
|
|
// 2) Modify or replace a file
|
|
// 3) Add patch archive to the opened MPQ
|
|
// 4) Read patched file
|
|
// 5) Now what ?
|
|
//
|
|
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
if(!(ha->dwFlags & MPQ_FLAG_READ_ONLY))
|
|
dwErrCode = ERROR_ACCESS_DENIED;
|
|
}
|
|
|
|
// Open the archive like it is normal archive
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
if(SFileOpenArchive(szPatchMpqName, 0, MPQ_OPEN_READ_ONLY | MPQ_OPEN_PATCH, &hPatchMpq))
|
|
{
|
|
// Cast the archive handle to structure pointer
|
|
haPatch = (TMPQArchive *)hPatchMpq;
|
|
|
|
// We need to remember the proper patch prefix to match names of patched files
|
|
if(FindPatchPrefix(ha, (TMPQArchive *)hPatchMpq, szPatchPathPrefix))
|
|
{
|
|
// Now add the patch archive to the list of patches to the original MPQ
|
|
while(ha != NULL)
|
|
{
|
|
if(ha->haPatch == NULL)
|
|
{
|
|
haPatch->haBase = ha;
|
|
ha->haPatch = haPatch;
|
|
return true;
|
|
}
|
|
|
|
// Move to the next archive
|
|
ha = ha->haPatch;
|
|
}
|
|
}
|
|
|
|
// Close the archive
|
|
SFileCloseArchive(hPatchMpq);
|
|
dwErrCode = ERROR_CANT_FIND_PATCH_PREFIX;
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = GetLastError();
|
|
}
|
|
}
|
|
|
|
SetLastError(dwErrCode);
|
|
return false;
|
|
}
|
|
|
|
bool WINAPI SFileIsPatchedArchive(HANDLE hMpq)
|
|
{
|
|
TMPQArchive * ha = (TMPQArchive *)hMpq;
|
|
|
|
// Verify input parameters
|
|
if(!IsValidMpqHandle(hMpq))
|
|
return false;
|
|
|
|
return (ha->haPatch != NULL);
|
|
}
|