diff options
| author | Nicholas Estelami <NEstelami@users.noreply.github.com> | 2022-06-01 16:05:05 -0400 |
|---|---|---|
| committer | Nicholas Estelami <NEstelami@users.noreply.github.com> | 2022-06-16 20:35:52 -0400 |
| commit | b3e299dbdee829c12ada556a1c3aa1f109a0e05b (patch) | |
| tree | a6e647da673ba7df9389cb1f382a11fc8350bc23 /StormLib/src/SFilePatchArchives.cpp | |
| parent | 941c19f2c33a0d1af75a10a64fe0905387d1e440 (diff) | |
Replaced StormLib .lib files with source code.
Some modifications to handle backslashes and forward slashes, along with some optimizations to speed up OTR generation.
Diffstat (limited to 'StormLib/src/SFilePatchArchives.cpp')
| -rw-r--r-- | StormLib/src/SFilePatchArchives.cpp | 1175 |
1 files changed, 1175 insertions, 0 deletions
diff --git a/StormLib/src/SFilePatchArchives.cpp b/StormLib/src/SFilePatchArchives.cpp new file mode 100644 index 000000000..d8c104f7c --- /dev/null +++ b/StormLib/src/SFilePatchArchives.cpp @@ -0,0 +1,1175 @@ +/*****************************************************************************/ +/* 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); +} |
