/*****************************************************************************/ /* SFileOpenArchive.cpp Copyright Ladislav Zezula 1999 */ /* */ /* Author : Ladislav Zezula */ /* E-mail : ladik@zezula.net */ /* WWW : www.zezula.net */ /*---------------------------------------------------------------------------*/ /* Implementation of archive functions */ /*---------------------------------------------------------------------------*/ /* Date Ver Who Comment */ /* -------- ---- --- ------- */ /* xx.xx.xx 1.00 Lad Created */ /* 19.11.03 1.01 Dan Big endian handling */ /*****************************************************************************/ #define __STORMLIB_SELF__ #include "StormLib.h" #include "StormCommon.h" #define HEADER_SEARCH_BUFFER_SIZE 0x1000 //----------------------------------------------------------------------------- // Local functions static MTYPE CheckMapType(LPCTSTR szFileName, LPBYTE pbHeaderBuffer, size_t cbHeaderBuffer) { LPDWORD HeaderInt32 = (LPDWORD)pbHeaderBuffer; LPCTSTR szExtension; // Don't do any checks if there is not at least 16 bytes if(cbHeaderBuffer > 0x10) { DWORD DwordValue0 = BSWAP_INT32_UNSIGNED(HeaderInt32[0]); DWORD DwordValue1 = BSWAP_INT32_UNSIGNED(HeaderInt32[1]); DWORD DwordValue2 = BSWAP_INT32_UNSIGNED(HeaderInt32[2]); DWORD DwordValue3 = BSWAP_INT32_UNSIGNED(HeaderInt32[3]); // Test for AVI files (Warcraft III cinematics) - 'RIFF', 'AVI ' or 'LIST' if(DwordValue0 == 0x46464952 && DwordValue2 == 0x20495641 && DwordValue3 == 0x5453494C) return MapTypeAviFile; // Check for Starcraft II maps if((szExtension = _tcsrchr(szFileName, _T('.'))) != NULL) { // The "NP_Protect" protector places fake Warcraft III header // into the Starcraft II maps, whilst SC2 maps have no other header but MPQ v4 if(!_tcsicmp(szExtension, _T(".s2ma")) || !_tcsicmp(szExtension, _T(".SC2Map")) || !_tcsicmp(szExtension, _T(".SC2Mod"))) { return MapTypeStarcraft2; } } // Check for Warcraft III maps if(DwordValue0 == 0x57334D48 && DwordValue1 == 0x00000000) return MapTypeWarcraft3; } // MIX files are DLL files that contain MPQ in overlay. // Only Warcraft III is able to load them, so we consider them Warcraft III maps if(cbHeaderBuffer > 0x200 && pbHeaderBuffer[0] == 'M' && pbHeaderBuffer[1] == 'Z') { // Check the value of IMAGE_DOS_HEADER::e_lfanew at offset 0x3C if(0 < HeaderInt32[0x0F] && HeaderInt32[0x0F] < 0x10000) return MapTypeWarcraft3; } // No special map type recognized return MapTypeNotRecognized; } static TMPQUserData * IsValidMpqUserData(ULONGLONG ByteOffset, ULONGLONG FileSize, void * pvUserData) { TMPQUserData * pUserData; // BSWAP the source data and copy them to our buffer BSWAP_ARRAY32_UNSIGNED(pvUserData, sizeof(TMPQUserData)); pUserData = (TMPQUserData *)pvUserData; // Check the sizes if(pUserData->cbUserDataHeader <= pUserData->cbUserDataSize && pUserData->cbUserDataSize <= pUserData->dwHeaderOffs) { // Move to the position given by the userdata ByteOffset += pUserData->dwHeaderOffs; // The MPQ header should be within range of the file size if((ByteOffset + MPQ_HEADER_SIZE_V1) < FileSize) { // Note: We should verify if there is the MPQ header. // However, the header could be at any position below that // that is multiplier of 0x200 return (TMPQUserData *)pvUserData; } } return NULL; } // This function gets the right positions of the hash table and the block table. static DWORD VerifyMpqTablePositions(TMPQArchive * ha, ULONGLONG FileSize) { TMPQHeader * pHeader = ha->pHeader; ULONGLONG ByteOffset; //bool bMalformed = (ha->dwFlags & MPQ_FLAG_MALFORMED) ? true : false; // Check the begin of HET table if(pHeader->HetTablePos64) { ByteOffset = ha->MpqPos + pHeader->HetTablePos64; if(ByteOffset > FileSize) return ERROR_BAD_FORMAT; } // Check the begin of BET table if(pHeader->BetTablePos64) { ByteOffset = ha->MpqPos + pHeader->BetTablePos64; if(ByteOffset > FileSize) return ERROR_BAD_FORMAT; } // Check the begin of hash table if(pHeader->wHashTablePosHi || pHeader->dwHashTablePos) { ByteOffset = FileOffsetFromMpqOffset(ha, MAKE_OFFSET64(pHeader->wHashTablePosHi, pHeader->dwHashTablePos)); if(ByteOffset > FileSize) return ERROR_BAD_FORMAT; } // Check the begin of block table if(pHeader->wBlockTablePosHi || pHeader->dwBlockTablePos) { ByteOffset = FileOffsetFromMpqOffset(ha, MAKE_OFFSET64(pHeader->wBlockTablePosHi, pHeader->dwBlockTablePos)); if(ByteOffset > FileSize) return ERROR_BAD_FORMAT; } // Check the begin of hi-block table //if(pHeader->HiBlockTablePos64 != 0) //{ // ByteOffset = ha->MpqPos + pHeader->HiBlockTablePos64; // if(ByteOffset > FileSize) // return ERROR_BAD_FORMAT; //} // All OK. return ERROR_SUCCESS; } //----------------------------------------------------------------------------- // Support for alternate markers. Call before opening an archive #define SFILE_MARKERS_MIN_SIZE (sizeof(DWORD) + sizeof(DWORD) + sizeof(const char *) + sizeof(const char *)) bool WINAPI SFileSetArchiveMarkers(PSFILE_MARKERS pMarkers) { // Check structure minimum size if(pMarkers == NULL || pMarkers->dwSize < SFILE_MARKERS_MIN_SIZE) { SetLastError(ERROR_INVALID_PARAMETER); return false; } // Make sure that the MPQ cryptography is initialized at this time InitializeMpqCryptography(); // Remember the marker for MPQ header if(pMarkers->dwSignature != 0) g_dwMpqSignature = pMarkers->dwSignature; // Remember the encryption key for hash table if(pMarkers->szHashTableKey != NULL) g_dwHashTableKey = HashString(pMarkers->szHashTableKey, MPQ_HASH_FILE_KEY); // Remember the encryption key for block table if(pMarkers->szBlockTableKey != NULL) g_dwBlockTableKey = HashString(pMarkers->szBlockTableKey, MPQ_HASH_FILE_KEY); // Succeeded return true; } //----------------------------------------------------------------------------- // SFileGetLocale and SFileSetLocale // Set the locale for all newly opened files LCID WINAPI SFileGetLocale() { return g_lcFileLocale; } LCID WINAPI SFileSetLocale(LCID lcNewLocale) { g_lcFileLocale = lcNewLocale; return g_lcFileLocale; } //----------------------------------------------------------------------------- // SFileOpenArchive // // szFileName - MPQ archive file name to open // dwPriority - When SFileOpenFileEx called, this contains the search priority for searched archives // dwFlags - See MPQ_OPEN_XXX in StormLib.h // phMpq - Pointer to store open archive handle bool WINAPI SFileOpenArchive( const TCHAR * szMpqName, DWORD dwPriority, DWORD dwFlags, HANDLE * phMpq) { TMPQUserData * pUserData; TFileStream * pStream = NULL; // Open file stream TMPQArchive * ha = NULL; // Archive handle TFileEntry * pFileEntry; ULONGLONG FileSize = 0; // Size of the file LPBYTE pbHeaderBuffer = NULL; // Buffer for searching MPQ header DWORD dwStreamFlags = (dwFlags & STREAM_FLAGS_MASK); MTYPE MapType = MapTypeNotChecked; DWORD dwErrCode = ERROR_SUCCESS; // Verify the parameters if(szMpqName == NULL || *szMpqName == 0 || phMpq == NULL) { SetLastError(ERROR_INVALID_PARAMETER); return false; } // One time initialization of MPQ cryptography InitializeMpqCryptography(); dwPriority = dwPriority; // If not forcing MPQ v 1.0, also use file bitmap dwStreamFlags |= (dwFlags & MPQ_OPEN_FORCE_MPQ_V1) ? 0 : STREAM_FLAG_USE_BITMAP; // Open the MPQ archive file pStream = FileStream_OpenFile(szMpqName, dwStreamFlags); if(pStream == NULL) return false; // Check the file size. There must be at least 0x20 bytes if(dwErrCode == ERROR_SUCCESS) { FileStream_GetSize(pStream, &FileSize); if(FileSize < MPQ_HEADER_SIZE_V1) dwErrCode = ERROR_BAD_FORMAT; } // Allocate the MPQhandle if(dwErrCode == ERROR_SUCCESS) { if((ha = STORM_ALLOC(TMPQArchive, 1)) == NULL) dwErrCode = ERROR_NOT_ENOUGH_MEMORY; } // Allocate buffer for searching MPQ header if(dwErrCode == ERROR_SUCCESS) { pbHeaderBuffer = STORM_ALLOC(BYTE, HEADER_SEARCH_BUFFER_SIZE); if(pbHeaderBuffer == NULL) dwErrCode = ERROR_NOT_ENOUGH_MEMORY; } // Find the position of MPQ header if(dwErrCode == ERROR_SUCCESS) { ULONGLONG ByteOffset = 0; ULONGLONG EndOfSearch = FileSize; DWORD dwStrmFlags = 0; DWORD dwHeaderSize; DWORD dwHeaderID; bool bSearchComplete = false; memset(ha, 0, sizeof(TMPQArchive)); ha->pfnHashString = HashStringSlash; ha->pStream = pStream; pStream = NULL; // Set the archive read only if the stream is read-only FileStream_GetFlags(ha->pStream, &dwStrmFlags); ha->dwFlags |= (dwStrmFlags & STREAM_FLAG_READ_ONLY) ? MPQ_FLAG_READ_ONLY : 0; // Also remember if we shall check sector CRCs when reading file ha->dwFlags |= (dwFlags & MPQ_OPEN_CHECK_SECTOR_CRC) ? MPQ_FLAG_CHECK_SECTOR_CRC : 0; // Also remember if this MPQ is a patch ha->dwFlags |= (dwFlags & MPQ_OPEN_PATCH) ? MPQ_FLAG_PATCH : 0; // Limit the header searching to about 130 MB of data if(EndOfSearch > 0x08000000) EndOfSearch = 0x08000000; // Find the offset of MPQ header within the file while(bSearchComplete == false && ByteOffset < EndOfSearch) { // Always read at least 0x1000 bytes for performance. // This is what Storm.dll (2002) does. DWORD dwBytesAvailable = HEADER_SEARCH_BUFFER_SIZE; // Cut the bytes available, if needed if((FileSize - ByteOffset) < HEADER_SEARCH_BUFFER_SIZE) dwBytesAvailable = (DWORD)(FileSize - ByteOffset); // Read the eventual MPQ header if(!FileStream_Read(ha->pStream, &ByteOffset, pbHeaderBuffer, dwBytesAvailable)) { dwErrCode = GetLastError(); break; } // Check whether the file is AVI file or a Warcraft III/Starcraft II map if(MapType == MapTypeNotChecked) { // Do nothing if the file is an AVI file if((MapType = CheckMapType(szMpqName, pbHeaderBuffer, dwBytesAvailable)) == MapTypeAviFile) { dwErrCode = ERROR_AVI_FILE; break; } } // Search the header buffer for(DWORD dwInBufferOffset = 0; dwInBufferOffset < dwBytesAvailable; dwInBufferOffset += 0x200) { // Copy the data from the potential header buffer to the MPQ header memcpy(ha->HeaderData, pbHeaderBuffer + dwInBufferOffset, sizeof(ha->HeaderData)); // If there is the MPQ user data, process it // Note that Warcraft III does not check for user data, which is abused by many map protectors dwHeaderID = BSWAP_INT32_UNSIGNED(ha->HeaderData[0]); if(MapType != MapTypeWarcraft3 && (dwFlags & MPQ_OPEN_FORCE_MPQ_V1) == 0) { if(ha->pUserData == NULL && dwHeaderID == ID_MPQ_USERDATA) { // Verify if this looks like a valid user data pUserData = IsValidMpqUserData(ByteOffset, FileSize, ha->HeaderData); if(pUserData != NULL) { // Fill the user data header ha->UserDataPos = ByteOffset; ha->pUserData = &ha->UserData; memcpy(ha->pUserData, pUserData, sizeof(TMPQUserData)); // Continue searching from that position ByteOffset += ha->pUserData->dwHeaderOffs; break; } } } // There must be MPQ header signature. Note that STORM.dll from Warcraft III actually // tests the MPQ header size. It must be at least 0x20 bytes in order to load it // Abused by Spazzler Map protector. Note that the size check is not present // in Storm.dll v 1.00, so Diablo I code would load the MPQ anyway. dwHeaderSize = BSWAP_INT32_UNSIGNED(ha->HeaderData[1]); if(dwHeaderID == g_dwMpqSignature && dwHeaderSize >= MPQ_HEADER_SIZE_V1) { // Now convert the header to version 4 dwErrCode = ConvertMpqHeaderToFormat4(ha, ByteOffset, FileSize, dwFlags, MapType); if(dwErrCode != ERROR_FAKE_MPQ_HEADER) { bSearchComplete = true; break; } } // Check for MPK archives (Longwu Online - MPQ fork) if(MapType == MapTypeNotRecognized && dwHeaderID == ID_MPK) { // Now convert the MPK header to MPQ Header version 4 dwErrCode = ConvertMpkHeaderToFormat4(ha, FileSize, dwFlags); bSearchComplete = true; break; } // If searching for the MPQ header is disabled, return an error if(dwFlags & MPQ_OPEN_NO_HEADER_SEARCH) { dwErrCode = ERROR_NOT_SUPPORTED; bSearchComplete = true; break; } // Move the pointers ByteOffset += 0x200; } } // Did we identify one of the supported headers? if(dwErrCode == ERROR_SUCCESS) { // Set the user data position to the MPQ header, if none if(ha->pUserData == NULL) ha->UserDataPos = ByteOffset; // Set the position of the MPQ header ha->pHeader = (TMPQHeader *)ha->HeaderData; ha->MpqPos = ByteOffset; ha->FileSize = FileSize; // Sector size must be nonzero. if(ByteOffset >= FileSize || ha->pHeader->wSectorSize == 0) dwErrCode = ERROR_BAD_FORMAT; } } // Fix table positions according to format if(dwErrCode == ERROR_SUCCESS) { // Dump the header // DumpMpqHeader(ha->pHeader); // W3x Map Protectors use the fact that War3's Storm.dll ignores the MPQ user data, // and ignores the MPQ format version as well. The trick is to // fake MPQ format 2, with an improper hi-word position of hash table and block table // We can overcome such protectors by forcing opening the archive as MPQ v 1.0 if(dwFlags & MPQ_OPEN_FORCE_MPQ_V1) { ha->pHeader->wFormatVersion = MPQ_FORMAT_VERSION_1; ha->pHeader->dwHeaderSize = MPQ_HEADER_SIZE_V1; ha->dwFlags |= MPQ_FLAG_READ_ONLY; ha->pUserData = NULL; } // Anti-overflow. If the hash table size in the header is // higher than 0x10000000, it would overflow in 32-bit version // Observed in the malformed Warcraft III maps // Example map: MPQ_2016_v1_ProtectedMap_TableSizeOverflow.w3x ha->pHeader->dwBlockTableSize = (ha->pHeader->dwBlockTableSize & BLOCK_INDEX_MASK); ha->pHeader->dwHashTableSize = (ha->pHeader->dwHashTableSize & BLOCK_INDEX_MASK); // Both MPQ_OPEN_NO_LISTFILE or MPQ_OPEN_NO_ATTRIBUTES trigger read only mode if(dwFlags & (MPQ_OPEN_NO_LISTFILE | MPQ_OPEN_NO_ATTRIBUTES)) ha->dwFlags |= MPQ_FLAG_READ_ONLY; // Check if the caller wants to force adding listfile if(dwFlags & MPQ_OPEN_FORCE_LISTFILE) ha->dwFlags |= MPQ_FLAG_LISTFILE_FORCE; // Remember whether whis is a map for Warcraft III if(MapType == MapTypeWarcraft3) ha->dwFlags |= MPQ_FLAG_WAR3_MAP; // Set the size of file sector ha->dwSectorSize = (0x200 << ha->pHeader->wSectorSize); // Verify if any of the tables doesn't start beyond the end of the file dwErrCode = VerifyMpqTablePositions(ha, FileSize); } // Read the hash table. Ignore the result, as hash table is no longer required // Read HET table. Ignore the result, as HET table is no longer required if(dwErrCode == ERROR_SUCCESS) { dwErrCode = LoadAnyHashTable(ha); } // Now, build the file table. It will be built by combining // the block table, BET table, hi-block table, (attributes) and (listfile). if(dwErrCode == ERROR_SUCCESS) { dwErrCode = BuildFileTable(ha); } // Load the internal listfile and include it to the file table if(dwErrCode == ERROR_SUCCESS && (dwFlags & MPQ_OPEN_NO_LISTFILE) == 0) { // Quick check for (listfile) pFileEntry = GetFileEntryLocale(ha, LISTFILE_NAME, LANG_NEUTRAL); if(pFileEntry != NULL) { // Ignore result of the operation. (listfile) is optional. SFileAddListFile((HANDLE)ha, NULL); ha->dwFileFlags1 = pFileEntry->dwFlags; } } // Load the "(attributes)" file and merge it to the file table if(dwErrCode == ERROR_SUCCESS && (dwFlags & MPQ_OPEN_NO_ATTRIBUTES) == 0 && (ha->dwFlags & MPQ_FLAG_BLOCK_TABLE_CUT) == 0) { // Quick check for (attributes) pFileEntry = GetFileEntryLocale(ha, ATTRIBUTES_NAME, LANG_NEUTRAL); if(pFileEntry != NULL) { // Ignore result of the operation. (attributes) is optional. SAttrLoadAttributes(ha); ha->dwFileFlags2 = pFileEntry->dwFlags; } } // Remember whether the archive has weak signature. Only for MPQs format 1.0. if(dwErrCode == ERROR_SUCCESS) { // Quick check for (signature) pFileEntry = GetFileEntryLocale(ha, SIGNATURE_NAME, LANG_NEUTRAL); if(pFileEntry != NULL) { // Just remember that the archive is weak-signed assert((pFileEntry->dwFlags & MPQ_FILE_EXISTS) != 0); ha->dwFileFlags3 = pFileEntry->dwFlags; } // Finally, set the MPQ_FLAG_READ_ONLY if the MPQ was found malformed ha->dwFlags |= (ha->dwFlags & MPQ_FLAG_MALFORMED) ? MPQ_FLAG_READ_ONLY : 0; } // Cleanup and exit if(dwErrCode != ERROR_SUCCESS) { FileStream_Close(pStream); FreeArchiveHandle(ha); SetLastError(dwErrCode); ha = NULL; } // Free the header buffer if(pbHeaderBuffer != NULL) STORM_FREE(pbHeaderBuffer); if(phMpq != NULL) *phMpq = ha; return (dwErrCode == ERROR_SUCCESS); } //----------------------------------------------------------------------------- // bool WINAPI SFileSetDownloadCallback(HANDLE, SFILE_DOWNLOAD_CALLBACK, void *); // // Sets a callback that is called when content is downloaded from the master MPQ // bool WINAPI SFileSetDownloadCallback(HANDLE hMpq, SFILE_DOWNLOAD_CALLBACK DownloadCB, void * pvUserData) { TMPQArchive * ha = (TMPQArchive *)hMpq; // Do nothing if 'hMpq' is bad parameter if(!IsValidMpqHandle(hMpq)) { SetLastError(ERROR_INVALID_HANDLE); return false; } return FileStream_SetCallback(ha->pStream, DownloadCB, pvUserData); } //----------------------------------------------------------------------------- // bool SFileFlushArchive(HANDLE hMpq) // // Saves all dirty data into MPQ archive. // Has similar effect like SFileCloseArchive, but the archive is not closed. // Use on clients who keep MPQ archive open even for write operations, // and terminating without calling SFileCloseArchive might corrupt the archive. // bool WINAPI SFileFlushArchive(HANDLE hMpq) { TMPQArchive * ha; DWORD dwResultError = ERROR_SUCCESS; DWORD dwErrCode; // Do nothing if 'hMpq' is bad parameter if((ha = IsValidMpqHandle(hMpq)) == NULL) { SetLastError(ERROR_INVALID_HANDLE); return false; } // Only if the MPQ was changed if(ha->dwFlags & MPQ_FLAG_CHANGED) { // Indicate that we are saving MPQ internal structures ha->dwFlags |= MPQ_FLAG_SAVING_TABLES; // Defragment the file table. This will allow us to put the internal files to the end DefragmentFileTable(ha); // // Create each internal file // Note that the (signature) file is usually before (listfile) in the file table // if(ha->dwFlags & MPQ_FLAG_SIGNATURE_NEW) { dwErrCode = SSignFileCreate(ha); if(dwErrCode != ERROR_SUCCESS) dwResultError = dwErrCode; } if(ha->dwFlags & (MPQ_FLAG_LISTFILE_NEW | MPQ_FLAG_LISTFILE_FORCE)) { dwErrCode = SListFileSaveToMpq(ha); if(dwErrCode != ERROR_SUCCESS) dwResultError = dwErrCode; } if(ha->dwFlags & MPQ_FLAG_ATTRIBUTES_NEW) { dwErrCode = SAttrFileSaveToMpq(ha); if(dwErrCode != ERROR_SUCCESS) dwResultError = dwErrCode; } // Save HET table, BET table, hash table, block table, hi-block table if(ha->dwFlags & MPQ_FLAG_CHANGED) { // Rebuild the HET table if(ha->pHetTable != NULL) RebuildHetTable(ha); // Save all MPQ tables first dwErrCode = SaveMPQTables(ha); if(dwErrCode != ERROR_SUCCESS) dwResultError = dwErrCode; // If the archive has weak signature, we need to finish it if(ha->dwFileFlags3 != 0) { dwErrCode = SSignFileFinish(ha); if(dwErrCode != ERROR_SUCCESS) dwResultError = dwErrCode; } } // We are no longer saving internal MPQ structures ha->dwFlags &= ~MPQ_FLAG_SAVING_TABLES; } // Return the error if(dwResultError != ERROR_SUCCESS) SetLastError(dwResultError); return (dwResultError == ERROR_SUCCESS); } //----------------------------------------------------------------------------- // bool SFileCloseArchive(HANDLE hMpq); // bool WINAPI SFileCloseArchive(HANDLE hMpq) { TMPQArchive * ha = IsValidMpqHandle(hMpq); bool bResult = false; // Only if the handle is valid if(ha == NULL) { SetLastError(ERROR_INVALID_HANDLE); return false; } // Invalidate the add file callback so it won't be called // when saving (listfile) and (attributes) ha->pfnAddFileCB = NULL; ha->pvAddFileUserData = NULL; // Flush all unsaved data to the storage bResult = SFileFlushArchive(hMpq); // Free all memory used by MPQ archive FreeArchiveHandle(ha); return bResult; }