/*****************************************************************************/ /* 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(&PatchHeader, 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); }