/*****************************************************************************/ /* CascOpenStorage.cpp Copyright (c) Ladislav Zezula 2014 */ /*---------------------------------------------------------------------------*/ /* Storage functions for CASC */ /* Note: WoW6 offsets refer to WoW.exe 6.0.3.19116 (32-bit) */ /* SHA1: c10e9ffb7d040a37a356b96042657e1a0c95c0dd */ /*---------------------------------------------------------------------------*/ /* Date Ver Who Comment */ /* -------- ---- --- ------- */ /* 29.04.14 1.00 Lad The first version of CascOpenStorage.cpp */ /*****************************************************************************/ #define __CASCLIB_SELF__ #include "CascLib.h" #include "CascCommon.h" //----------------------------------------------------------------------------- // Local structures #define ROOT_SEARCH_PHASE_INITIALIZING 0 #define ROOT_SEARCH_PHASE_LISTFILE 1 #define ROOT_SEARCH_PHASE_NAMELESS 2 #define ROOT_SEARCH_PHASE_FINISHED 2 // On-disk version of locale block typedef struct _FILE_LOCALE_BLOCK { DWORD NumberOfFiles; // Number of entries DWORD Flags; DWORD Locales; // File locale mask (CASC_LOCALE_XXX) // Followed by a block of 32-bit integers (count: NumberOfFiles) // Followed by the MD5 and file name hash (count: NumberOfFiles) } FILE_LOCALE_BLOCK, *PFILE_LOCALE_BLOCK; // On-disk version of root entry typedef struct _FILE_ROOT_ENTRY { ENCODING_KEY EncodingKey; // MD5 of the file ULONGLONG FileNameHash; // Jenkins hash of the file name } FILE_ROOT_ENTRY, *PFILE_ROOT_ENTRY; typedef struct _CASC_ROOT_BLOCK { PFILE_LOCALE_BLOCK pLocaleBlockHdr; // Pointer to the locale block PDWORD FileDataIds; // Pointer to the array of File Data IDs PFILE_ROOT_ENTRY pRootEntries; } CASC_ROOT_BLOCK, *PCASC_ROOT_BLOCK; // Root file entry for CASC storages without MNDX root file (World of Warcraft 6.0+) // Does not match to the in-file structure of the root entry typedef struct _CASC_FILE_ENTRY { ENCODING_KEY EncodingKey; // File encoding key (MD5) ULONGLONG FileNameHash; // Jenkins hash of the file name DWORD FileDataId; // File Data Index DWORD Locales; // Locale flags of the file } CASC_FILE_ENTRY, *PCASC_FILE_ENTRY; struct TRootHandler_WoW6 : public TRootHandler { // Linear global list of file entries DYNAMIC_ARRAY FileTable; DYNAMIC_ARRAY FileDataIdLookupTable; // Global map of FileName -> FileEntry PCASC_MAP pRootMap; // For counting files DWORD dwTotalFileCount; }; // Prototype for root file parsing routine typedef int (*PARSE_ROOT)(TRootHandler_WoW6 * pRootHandler, PCASC_ROOT_BLOCK pBlockInfo); //----------------------------------------------------------------------------- // Local functions static bool IsFileDataIdName(const char * szFileName) { BYTE BinaryValue[4]; // File name must begin with "File", case insensitive if(AsciiToUpperTable_BkSlash[szFileName[0]] == 'F' && AsciiToUpperTable_BkSlash[szFileName[1]] == 'I' && AsciiToUpperTable_BkSlash[szFileName[2]] == 'L' && AsciiToUpperTable_BkSlash[szFileName[3]] == 'E') { // Then, 8 hexadecimal digits must follow if(ConvertStringToBinary(szFileName + 4, 8, BinaryValue) == ERROR_SUCCESS) { // Must be followed by an extension or end-of-string return (szFileName[0x0C] == 0 || szFileName[0x0C] == '.'); } } return false; } static int FileDataIdCompare(const void *, const void * pvFile1, const void * pvFile2) { return ((PCASC_FILE_ENTRY)pvFile1)->FileDataId - ((PCASC_FILE_ENTRY)pvFile2)->FileDataId; } // Search by FileDataId PCASC_FILE_ENTRY FindRootEntry(DYNAMIC_ARRAY & FileTable, DWORD FileDataId) { PCASC_FILE_ENTRY* pStartEntry = (PCASC_FILE_ENTRY*)FileTable.ItemArray; PCASC_FILE_ENTRY* pMidlEntry; PCASC_FILE_ENTRY* pEndEntry = pStartEntry + FileTable.ItemCount - 1; int nResult; // Perform binary search on the table while(pStartEntry < pEndEntry) { // Calculate the middle of the interval pMidlEntry = pStartEntry + ((pEndEntry - pStartEntry) / 2); // Did we find it? nResult = (int)FileDataId - (int)(*pMidlEntry)->FileDataId; if(nResult == 0) return *pMidlEntry; // Move the interval to the left or right (nResult < 0) ? pEndEntry = pMidlEntry : pStartEntry = pMidlEntry + 1; } return NULL; } // Search by file name hash // Also used in CascSearchFile PCASC_FILE_ENTRY FindRootEntry(PCASC_MAP pRootMap, const char * szFileName, DWORD * PtrTableIndex) { // Calculate the HASH value of the normalized file name ULONGLONG FileNameHash = CalcFileNameHash(szFileName); // Perform the hash search return (PCASC_FILE_ENTRY)Map_FindObject(pRootMap, &FileNameHash, PtrTableIndex); } LPBYTE VerifyLocaleBlock(PCASC_ROOT_BLOCK pBlockInfo, LPBYTE pbFilePointer, LPBYTE pbFileEnd) { // Validate the file locale block pBlockInfo->pLocaleBlockHdr = (PFILE_LOCALE_BLOCK)pbFilePointer; pbFilePointer = (LPBYTE)(pBlockInfo->pLocaleBlockHdr + 1); if(pbFilePointer > pbFileEnd) return NULL; // Validate the array of 32-bit integers pBlockInfo->FileDataIds = (PDWORD)pbFilePointer; pbFilePointer = (LPBYTE)(pBlockInfo->FileDataIds + pBlockInfo->pLocaleBlockHdr->NumberOfFiles); if(pbFilePointer > pbFileEnd) return NULL; // Validate the array of root entries pBlockInfo->pRootEntries = (PFILE_ROOT_ENTRY)pbFilePointer; pbFilePointer = (LPBYTE)(pBlockInfo->pRootEntries + pBlockInfo->pLocaleBlockHdr->NumberOfFiles); if(pbFilePointer > pbFileEnd) return NULL; // Return the position of the next block return pbFilePointer; } static int ParseRoot_CountFiles( TRootHandler_WoW6 * pRootHandler, PCASC_ROOT_BLOCK pRootBlock) { // Add the file count to the total file count pRootHandler->dwTotalFileCount += pRootBlock->pLocaleBlockHdr->NumberOfFiles; return ERROR_SUCCESS; } static int ParseRoot_AddRootEntries( TRootHandler_WoW6 * pRootHandler, PCASC_ROOT_BLOCK pRootBlock) { PCASC_FILE_ENTRY pFileEntry; DWORD dwFileDataId = 0; // Sanity checks assert(pRootHandler->FileTable.ItemArray != NULL); assert(pRootHandler->FileTable.ItemCountMax != 0); assert(pRootHandler->FileDataIdLookupTable.ItemArray != NULL); assert(pRootHandler->FileDataIdLookupTable.ItemCountMax != 0); // WoW.exe (build 19116): Blocks with zero files are skipped for(DWORD i = 0; i < pRootBlock->pLocaleBlockHdr->NumberOfFiles; i++) { // Create new entry, with overflow check if(pRootHandler->FileTable.ItemCount >= pRootHandler->FileTable.ItemCountMax) return ERROR_INSUFFICIENT_BUFFER; pFileEntry = (PCASC_FILE_ENTRY)Array_Insert(&pRootHandler->FileTable, NULL, 1); if (pRootHandler->FileDataIdLookupTable.ItemCount >= pRootHandler->FileDataIdLookupTable.ItemCountMax) return ERROR_INSUFFICIENT_BUFFER; Array_Insert(&pRootHandler->FileDataIdLookupTable, &pFileEntry, 1); // (004147A3) Prepare the CASC_FILE_ENTRY structure pFileEntry->FileNameHash = pRootBlock->pRootEntries[i].FileNameHash; pFileEntry->FileDataId = dwFileDataId + pRootBlock->FileDataIds[i]; pFileEntry->Locales = pRootBlock->pLocaleBlockHdr->Locales; pFileEntry->EncodingKey = pRootBlock->pRootEntries[i].EncodingKey; // Also, insert the entry to the map Map_InsertObject(pRootHandler->pRootMap, pFileEntry, &pFileEntry->FileNameHash); // Update the local File Data Id assert((pFileEntry->FileDataId + 1) > pFileEntry->FileDataId); dwFileDataId = pFileEntry->FileDataId + 1; // Move to the next root entry pFileEntry++; } return ERROR_SUCCESS; } static int ParseWowRootFileInternal( TRootHandler_WoW6 * pRootHandler, PARSE_ROOT pfnParseRoot, LPBYTE pbRootFile, LPBYTE pbRootFileEnd, DWORD dwLocaleMask, BYTE bOverrideArchive, BYTE bAudioLocale) { CASC_ROOT_BLOCK RootBlock; // Now parse the root file while(pbRootFile < pbRootFileEnd) { // Validate the file locale block pbRootFile = VerifyLocaleBlock(&RootBlock, pbRootFile, pbRootFileEnd); if(pbRootFile == NULL) break; // WoW.exe (build 19116): Entries with flag 0x100 set are skipped if(RootBlock.pLocaleBlockHdr->Flags & 0x100) continue; // WoW.exe (build 19116): Entries with flag 0x80 set are skipped if overrideArchive CVAR is set to FALSE (which is by default in non-chinese clients) if((RootBlock.pLocaleBlockHdr->Flags & 0x80) && bOverrideArchive == 0) continue; // WoW.exe (build 19116): Entries with (flags >> 0x1F) not equal to bAudioLocale are skipped if((RootBlock.pLocaleBlockHdr->Flags >> 0x1F) != bAudioLocale) continue; // WoW.exe (build 19116): Locales other than defined mask are skipped too if((RootBlock.pLocaleBlockHdr->Locales & dwLocaleMask) == 0) continue; // Now call the custom function pfnParseRoot(pRootHandler, &RootBlock); } return ERROR_SUCCESS; } /* // known dwRegion values returned from sub_661316 (7.0.3.22210 x86 win), also referred by lua GetCurrentRegion #define WOW_REGION_US 0x01 #define WOW_REGION_KR 0x02 #define WOW_REGION_EU 0x03 #define WOW_REGION_TW 0x04 #define WOW_REGION_CN 0x05 #define WOW_LOCALE_ENUS 0x00 #define WOW_LOCALE_KOKR 0x01 #define WOW_LOCALE_FRFR 0x02 #define WOW_LOCALE_DEDE 0x03 #define WOW_LOCALE_ZHCN 0x04 #define WOW_LOCALE_ZHTW 0x05 #define WOW_LOCALE_ESES 0x06 #define WOW_LOCALE_ESMX 0x07 #define WOW_LOCALE_RURU 0x08 #define WOW_LOCALE_PTBR 0x0A #define WOW_LOCALE_ITIT 0x0B // dwLocale is obtained from a WOW_LOCALE_* to CASC_LOCALE_BIT_* mapping (sub_6615D0 in 7.0.3.22210 x86 win) // because (ENUS, ENGB) and (PTBR, PTPT) pairs share the same value on WOW_LOCALE_* enum // dwRegion is used to distinguish them if(dwRegion == WOW_REGION_EU) { // Is this english version of WoW? if(dwLocale == CASC_LOCALE_BIT_ENUS) { LoadWowRootFileLocales(hs, pbRootFile, cbRootFile, CASC_LOCALE_ENGB, bOverrideArchive, bAudioLocale); LoadWowRootFileLocales(hs, pbRootFile, cbRootFile, CASC_LOCALE_ENUS, bOverrideArchive, bAudioLocale); return ERROR_SUCCESS; } // Is this portuguese version of WoW? if(dwLocale == CASC_LOCALE_BIT_PTBR) { LoadWowRootFileLocales(hs, pbRootFile, cbRootFile, CASC_LOCALE_PTPT, bOverrideArchive, bAudioLocale); LoadWowRootFileLocales(hs, pbRootFile, cbRootFile, CASC_LOCALE_PTBR, bOverrideArchive, bAudioLocale); } } else LoadWowRootFileLocales(hs, pbRootFile, cbRootFile, (1 << dwLocale), bOverrideArchive, bAudioLocale); */ static int ParseWowRootFile2( TRootHandler_WoW6 * pRootHandler, PARSE_ROOT pfnParseRoot, LPBYTE pbRootFile, LPBYTE pbRootFileEnd, DWORD dwLocaleMask, BYTE bAudioLocale) { // Load the locale as-is ParseWowRootFileInternal(pRootHandler, pfnParseRoot, pbRootFile, pbRootFileEnd, dwLocaleMask, false, bAudioLocale); // If we wanted enGB, we also load enUS for the missing files if(dwLocaleMask == CASC_LOCALE_ENGB) ParseWowRootFileInternal(pRootHandler, pfnParseRoot, pbRootFile, pbRootFileEnd, CASC_LOCALE_ENUS, false, bAudioLocale); if(dwLocaleMask == CASC_LOCALE_PTPT) ParseWowRootFileInternal(pRootHandler, pfnParseRoot, pbRootFile, pbRootFileEnd, CASC_LOCALE_PTBR, false, bAudioLocale); return ERROR_SUCCESS; } // WoW.exe: 004146C7 (BuildManifest::Load) static int ParseWowRootFile( TRootHandler_WoW6 * pRootHandler, PARSE_ROOT pfnParseRoot, LPBYTE pbRootFile, LPBYTE pbRootFileEnd, DWORD dwLocaleMask) { ParseWowRootFile2(pRootHandler, pfnParseRoot, pbRootFile, pbRootFileEnd, dwLocaleMask, 0); ParseWowRootFile2(pRootHandler, pfnParseRoot, pbRootFile, pbRootFileEnd, dwLocaleMask, 1); return ERROR_SUCCESS; } //----------------------------------------------------------------------------- // Implementation of WoW6 root file static int WowHandler_Insert( TRootHandler_WoW6 * pRootHandler, const char * szFileName, LPBYTE pbEncodingKey) { PCASC_FILE_ENTRY pFileEntry; DWORD FileDataId = 0; // Don't let the number of items to overflow if(pRootHandler->FileTable.ItemCount >= pRootHandler->FileTable.ItemCountMax) return ERROR_NOT_ENOUGH_MEMORY; if (pRootHandler->FileDataIdLookupTable.ItemCount >= pRootHandler->FileDataIdLookupTable.ItemCountMax) return ERROR_NOT_ENOUGH_MEMORY; // Insert the item to the linear file list pFileEntry = (PCASC_FILE_ENTRY)Array_Insert(&pRootHandler->FileTable, NULL, 1); if(pFileEntry != NULL) { Array_Insert(&pRootHandler->FileDataIdLookupTable, &pFileEntry, 1); // Get the file data ID of the previous item (0 if this is the first one) if(pRootHandler->FileTable.ItemCount > 1) FileDataId = pFileEntry[-1].FileDataId; // Fill-in the new entry pFileEntry->EncodingKey = *(PENCODING_KEY)pbEncodingKey; pFileEntry->FileNameHash = CalcFileNameHash(szFileName); pFileEntry->FileDataId = FileDataId + 1; pFileEntry->Locales = CASC_LOCALE_ALL; // Verify collisions (debug version only) assert(Map_FindObject(pRootHandler->pRootMap, &pFileEntry->FileNameHash, NULL) == NULL); // Insert the entry to the map Map_InsertObject(pRootHandler->pRootMap, pFileEntry, &pFileEntry->FileNameHash); } return ERROR_SUCCESS; } static LPBYTE WowHandler_Search( TRootHandler_WoW6 * pRootHandler, TCascSearch * pSearch, PDWORD /* PtrFileSize */, PDWORD PtrLocaleFlags, PDWORD PtrFileDataId) { PCASC_FILE_ENTRY pFileEntry; // Only if we have a listfile if(pSearch->pCache != NULL) { // Keep going through the listfile while(ListFile_GetNext(pSearch->pCache, pSearch->szMask, pSearch->szFileName, MAX_PATH)) { // Find the root entry pFileEntry = FindRootEntry(pRootHandler->pRootMap, pSearch->szFileName, NULL); if(pFileEntry != NULL) { // Give the caller the locale mask if(PtrLocaleFlags != NULL) PtrLocaleFlags[0] = pFileEntry->Locales; if(PtrFileDataId != NULL) PtrFileDataId[0] = pFileEntry->FileDataId; return pFileEntry->EncodingKey.Value; } } } // No more files return NULL; } static LPBYTE WowHandler_GetKey(TRootHandler_WoW6 * pRootHandler, const char * szFileName) { PCASC_FILE_ENTRY pFileEntry; DWORD FileDataId; BYTE FileDataIdLE[4]; // Open by FileDataId. The file name must be as following: // File########.unk, where '#' are hexa-decimal numbers (case insensitive). // Extension is ignored in that case if(IsFileDataIdName(szFileName)) { ConvertStringToBinary(szFileName + 4, 8, FileDataIdLE); FileDataId = ConvertBytesToInteger_4(FileDataIdLE); pFileEntry = FindRootEntry(pRootHandler->FileDataIdLookupTable, FileDataId); } else { // Find by the file name hash pFileEntry = FindRootEntry(pRootHandler->pRootMap, szFileName, NULL); } return (pFileEntry != NULL) ? pFileEntry->EncodingKey.Value : NULL; } static void WowHandler_EndSearch(TRootHandler_WoW6 * /* pRootHandler */, TCascSearch * pSearch) { if(pSearch->pRootContext != NULL) CASC_FREE(pSearch->pRootContext); pSearch->pRootContext = NULL; } static DWORD WowHandler_GetFileId(TRootHandler_WoW6 * pRootHandler, const char * szFileName) { PCASC_FILE_ENTRY pFileEntry; // Find by the file name hash pFileEntry = FindRootEntry(pRootHandler->pRootMap, szFileName, NULL); return (pFileEntry != NULL) ? pFileEntry->FileDataId : 0; } static void WowHandler_Close(TRootHandler_WoW6 * pRootHandler) { if(pRootHandler != NULL) { Array_Free(&pRootHandler->FileTable); Array_Free(&pRootHandler->FileDataIdLookupTable); Map_Free(pRootHandler->pRootMap); CASC_FREE(pRootHandler); } } #ifdef _DEBUG static void TRootHandlerWoW6_Dump( TCascStorage * hs, TDumpContext * dc, // Pointer to an opened file LPBYTE pbRootFile, DWORD cbRootFile, const TCHAR * szListFile, int nDumpLevel) { PCASC_ENCODING_ENTRY pEncodingEntry; CASC_ROOT_BLOCK BlockInfo; PLISTFILE_MAP pListMap; QUERY_KEY EncodingKey; LPBYTE pbRootFileEnd = pbRootFile + cbRootFile; LPBYTE pbFilePointer; char szOneLine[0x100]; DWORD i; // Create the listfile map pListMap = ListFile_CreateMap(szListFile); // Dump the root entries for(pbFilePointer = pbRootFile; pbFilePointer <= pbRootFileEnd; ) { // Validate the root block pbFilePointer = VerifyLocaleBlock(&BlockInfo, pbFilePointer, pbRootFileEnd); if(pbFilePointer == NULL) break; // Dump the locale block dump_print(dc, "Flags: %08X Locales: %08X NumberOfFiles: %u\n" "=========================================================\n", BlockInfo.pLocaleBlockHdr->Flags, BlockInfo.pLocaleBlockHdr->Locales, BlockInfo.pLocaleBlockHdr->NumberOfFiles); // Dump the hashes and encoding keys for(i = 0; i < BlockInfo.pLocaleBlockHdr->NumberOfFiles; i++) { // Dump the entry dump_print(dc, "%08X %08X-%08X %s %s\n", (DWORD)(BlockInfo.FileDataIds[i]), (DWORD)(BlockInfo.pRootEntries[i].FileNameHash >> 0x20), (DWORD)(BlockInfo.pRootEntries[i].FileNameHash), StringFromMD5(BlockInfo.pRootEntries[i].EncodingKey.Value, szOneLine), ListFile_FindName(pListMap, BlockInfo.pRootEntries[i].FileNameHash)); // Find the encoding entry in the encoding table if(nDumpLevel >= DUMP_LEVEL_ENCODING_FILE) { EncodingKey.pbData = BlockInfo.pRootEntries[i].EncodingKey.Value; EncodingKey.cbData = MD5_HASH_SIZE; pEncodingEntry = FindEncodingEntry(hs, &EncodingKey, NULL); CascDumpEncodingEntry(hs, dc, pEncodingEntry, nDumpLevel); } } // Put extra newline dump_print(dc, "\n"); } ListFile_FreeMap(pListMap); } #endif //----------------------------------------------------------------------------- // Public functions int RootHandler_CreateWoW6(TCascStorage * hs, LPBYTE pbRootFile, DWORD cbRootFile, DWORD dwLocaleMask) { TRootHandler_WoW6 * pRootHandler; LPBYTE pbRootFileEnd = pbRootFile + cbRootFile; int nError; // Verify the size if(pbRootFile == NULL || cbRootFile <= sizeof(PFILE_LOCALE_BLOCK)) nError = ERROR_FILE_CORRUPT; // Allocate the root handler object hs->pRootHandler = pRootHandler = CASC_ALLOC(TRootHandler_WoW6, 1); if(pRootHandler == NULL) return ERROR_NOT_ENOUGH_MEMORY; // Fill-in the handler functions memset(pRootHandler, 0, sizeof(TRootHandler_WoW6)); pRootHandler->Insert = (ROOT_INSERT)WowHandler_Insert; pRootHandler->Search = (ROOT_SEARCH)WowHandler_Search; pRootHandler->EndSearch = (ROOT_ENDSEARCH)WowHandler_EndSearch; pRootHandler->GetKey = (ROOT_GETKEY)WowHandler_GetKey; pRootHandler->Close = (ROOT_CLOSE)WowHandler_Close; pRootHandler->GetFileId = (ROOT_GETFILEID)WowHandler_GetFileId; #ifdef _DEBUG pRootHandler->Dump = TRootHandlerWoW6_Dump; // Support for ROOT file dump #endif // _DEBUG // Count the files that are going to be loaded ParseWowRootFile(pRootHandler, ParseRoot_CountFiles, pbRootFile, pbRootFileEnd, dwLocaleMask); pRootHandler->dwTotalFileCount += CASC_EXTRA_FILES; // Create linear table that will contain all root items nError = Array_Create(&pRootHandler->FileTable, CASC_FILE_ENTRY, pRootHandler->dwTotalFileCount); if(nError != ERROR_SUCCESS) return nError; // Create sorted table that will contain all root items to lookup by FileDataId nError = Array_Create(&pRootHandler->FileDataIdLookupTable, PCASC_FILE_ENTRY, pRootHandler->dwTotalFileCount); if (nError != ERROR_SUCCESS) return nError; // Create the map of FileHash ->FileEntry pRootHandler->pRootMap = Map_Create(pRootHandler->dwTotalFileCount, sizeof(ULONGLONG), FIELD_OFFSET(CASC_FILE_ENTRY, FileNameHash)); if(pRootHandler->pRootMap == NULL) return ERROR_NOT_ENOUGH_MEMORY; // Parse the root file again and insert all files to the map ParseWowRootFile(pRootHandler, ParseRoot_AddRootEntries, pbRootFile, pbRootFileEnd, dwLocaleMask); // Sort entries by FileDataId for searches qsort_pointer_array((void**)pRootHandler->FileDataIdLookupTable.ItemArray, pRootHandler->FileDataIdLookupTable.ItemCount, &FileDataIdCompare, NULL); return ERROR_SUCCESS; }