diff options
author | Shauren <shauren.trinity@gmail.com> | 2014-10-10 20:17:30 +0200 |
---|---|---|
committer | Shauren <shauren.trinity@gmail.com> | 2014-10-10 20:17:30 +0200 |
commit | 88ae3da6373dee1f04d03b823ee63d6f1db1502e (patch) | |
tree | f9ac27f0a743a57b70e90b37f5971e024992eb00 /dep/CascLib/src/CascOpenStorage.cpp | |
parent | bc97908822c4afa23740ce70151c2486c340e2c2 (diff) |
Tools/Extractors: Updated map extractor
Diffstat (limited to 'dep/CascLib/src/CascOpenStorage.cpp')
-rw-r--r-- | dep/CascLib/src/CascOpenStorage.cpp | 1226 |
1 files changed, 1226 insertions, 0 deletions
diff --git a/dep/CascLib/src/CascOpenStorage.cpp b/dep/CascLib/src/CascOpenStorage.cpp new file mode 100644 index 00000000000..f9e6ba6f4d4 --- /dev/null +++ b/dep/CascLib/src/CascOpenStorage.cpp @@ -0,0 +1,1226 @@ +/*****************************************************************************/ +/* CascOpenStorage.cpp Copyright (c) Ladislav Zezula 2014 */ +/*---------------------------------------------------------------------------*/ +/* Storage functions for CASC */ +/*---------------------------------------------------------------------------*/ +/* 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" +#include "CascMndxRoot.h" + +//----------------------------------------------------------------------------- +// Local structures + +#define CASC_ENCODING_SEGMENT_SIZE 0x1000 + +typedef struct _BLOCK_SIZE_AND_HASH +{ + DWORD cbBlockSize; + DWORD dwBlockHash; + +} BLOCK_SIZE_AND_HASH, *PBLOCK_SIZE_AND_HASH; + +typedef struct _FILE_INDEX_HEADER_V1 +{ + USHORT field_0; + BYTE KeyIndex; // Key index (0 for data.i0x, 1 for data.i1x, 2 for data.i2x etc.) + BYTE align_3; + DWORD field_4; + ULONGLONG field_8; + ULONGLONG MaxFileOffset; + BYTE SpanSizeBytes; + BYTE SpanOffsBytes; + BYTE KeyBytes; + BYTE SegmentBits; // Number of bits for file offset + DWORD KeyCount1; + DWORD KeyCount2; + DWORD KeysHash1; + DWORD KeysHash2; + DWORD dwHeaderHash; +} FILE_INDEX_HEADER_V1, *PFILE_INDEX_HEADER_V1; + +typedef struct _FILE_INDEX_HEADER_V2 +{ + USHORT IndexVersion; // Must be 0x07 + BYTE KeyIndex; // Must be equal to the file key index + BYTE ExtraBytes; // (?) Extra bytes in the key record + BYTE SpanSizeBytes; // Size of field with file size + BYTE SpanOffsBytes; // Size of field with file offset + BYTE KeyBytes; // Size of the file key (bytes) + BYTE SegmentBits; // Number of bits for the file offset (rest is archive index) + ULONGLONG MaxFileOffset; + +} FILE_INDEX_HEADER_V2, *PFILE_INDEX_HEADER_V2; + +typedef struct _FILE_ENCODING_HEADER +{ + BYTE Magic[2]; // "EN" + BYTE field_2; + BYTE field_3; + BYTE field_4; + BYTE field_5[2]; + BYTE field_7[2]; + BYTE NumSegments[4]; // Number of entries (big endian) + BYTE field_D[4]; + BYTE field_11; + BYTE SegmentsPos[4]; // Offset of encoding segments + +} FILE_ENCODING_HEADER, *PFILE_ENCODING_HEADER; + +typedef struct _FILE_ENCODING_SEGMENT +{ + BYTE FirstEncodingKey[MD5_HASH_SIZE]; // The first encoding key in the segment + BYTE SegmentHash[MD5_HASH_SIZE]; // MD5 hash of the entire segment + +} FILE_ENCODING_SEGMENT, *PFILE_ENCODING_SEGMENT; + +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; + +typedef struct _FILE_ROOT_ENTRY +{ + BYTE EncodingKey[MD5_HASH_SIZE]; // MD5 of the file + ULONGLONG FileNameHash; // Jenkins hash of the file name + +} FILE_ROOT_ENTRY, *PFILE_ROOT_ENTRY; + +typedef struct _ROOT_BLOCK_INFO +{ + PFILE_LOCALE_BLOCK pLocaleBlockHdr; // Pointer to the locale block + PDWORD pInt32Array; // Pointer to the array of 32-bit integers + PFILE_ROOT_ENTRY pRootEntries; + +} ROOT_BLOCK_INFO, *PROOT_BLOCK_INFO; + +//----------------------------------------------------------------------------- +// Local variables + +static const TCHAR * szAllowedHexChars = _T("0123456789aAbBcCdDeEfF"); +static const TCHAR * szIndexFormat_V1 = _T("data.i%x%x"); +static const TCHAR * szIndexFormat_V2 = _T("%02x%08x.idx"); + +//----------------------------------------------------------------------------- +// Local functions + +inline void CopyFileKey(LPBYTE Trg, LPBYTE Src) +{ + Trg[0x00] = Src[0x00]; + Trg[0x01] = Src[0x01]; + Trg[0x02] = Src[0x02]; + Trg[0x03] = Src[0x03]; + Trg[0x04] = Src[0x04]; + Trg[0x05] = Src[0x05]; + Trg[0x06] = Src[0x06]; + Trg[0x07] = Src[0x07]; + Trg[0x08] = Src[0x08]; + Trg[0x09] = Src[0x09]; + Trg[0x0A] = Src[0x0A]; + Trg[0x0B] = Src[0x0B]; + Trg[0x0C] = Src[0x0C]; + Trg[0x0D] = Src[0x0D]; + Trg[0x0E] = Src[0x0E]; + Trg[0x0F] = Src[0x0F]; +} + +TCascStorage * IsValidStorageHandle(HANDLE hStorage) +{ + TCascStorage * hs = (TCascStorage *)hStorage; + + return (hs != NULL && hs->szClassName != NULL && !strcmp(hs->szClassName, "TCascStorage")) ? hs : NULL; +} + +// "data.iXY" +static bool IsIndexFileName_V1(const TCHAR * szFileName) +{ + // Check if the name looks like a valid index file + return (_tcslen(szFileName) == 8 && + _tcsnicmp(szFileName, _T("data.i"), 6) == 0 && + _tcsspn(szFileName + 6, szAllowedHexChars) == 2); +} + +static bool IsIndexFileName_V2(const TCHAR * szFileName) +{ + // Check if the name looks like a valid index file + return (_tcslen(szFileName) == 14 && + _tcsspn(szFileName, _T("0123456789aAbBcCdDeEfF")) == 0x0A && + _tcsicmp(szFileName + 0x0A, _T(".idx")) == 0); +} + +static void QUERY_KEY_Free(PQUERY_KEY pBlob) +{ + if(pBlob != NULL) + { + if(pBlob->pbData != NULL) + CASC_FREE(pBlob->pbData); + + pBlob->pbData = NULL; + pBlob->cbData = 0; + } +} + +static void QUERY_KEY_FreeArray(PQUERY_KEY pBlobArray) +{ + // Free the buffer in the first blob + // (will also free all buffers in the array) + QUERY_KEY_Free(pBlobArray); + + // Free the array itself + CASC_FREE(pBlobArray); +} + +static int CompareRootEntries(const void *, const void * pvKeyEntry1, const void * pvKeyEntry2) +{ + PCASC_ROOT_ENTRY pRootEntry1 = (PCASC_ROOT_ENTRY)pvKeyEntry1; + PCASC_ROOT_ENTRY pRootEntry2 = (PCASC_ROOT_ENTRY)pvKeyEntry2; + + // Compare name hash first + if(pRootEntry1->FileNameHash < pRootEntry2->FileNameHash) + return -1; + if(pRootEntry1->FileNameHash > pRootEntry2->FileNameHash) + return +1; + return 0; +} + +static bool IsCascIndexHeader_V1(LPBYTE pbFileData, DWORD cbFileData) +{ + PFILE_INDEX_HEADER_V1 pIndexHeader = (PFILE_INDEX_HEADER_V1)pbFileData; + DWORD dwHeaderHash; + bool bResult = false; + + // Check the size + if(cbFileData >= sizeof(FILE_INDEX_HEADER_V1)) + { + // Save the header hash + dwHeaderHash = pIndexHeader->dwHeaderHash; + pIndexHeader->dwHeaderHash = 0; + + // Calculate the hash + if(hashlittle(pIndexHeader, sizeof(FILE_INDEX_HEADER_V1), 0) == dwHeaderHash) + bResult = true; + + // Put the hash back + pIndexHeader->dwHeaderHash = dwHeaderHash; + } + + return bResult; +} + +static bool IsCascIndexHeader_V2(LPBYTE pbFileData, DWORD cbFileData) +{ + PBLOCK_SIZE_AND_HASH pSizeAndHash = (PBLOCK_SIZE_AND_HASH)pbFileData; + unsigned int HashHigh = 0; + unsigned int HashLow = 0; + + // Check for the header + if(cbFileData < sizeof(BLOCK_SIZE_AND_HASH) || pSizeAndHash->cbBlockSize < 0x10) + return false; + if(cbFileData < pSizeAndHash->cbBlockSize + sizeof(BLOCK_SIZE_AND_HASH)) + return false; + + // The index header for CASC v 2.0 begins with length and checksum + hashlittle2(pSizeAndHash + 1, pSizeAndHash->cbBlockSize, &HashHigh, &HashLow); + return (HashHigh == pSizeAndHash->dwBlockHash); +} + +static LPBYTE VerifyLocaleBlock(PROOT_BLOCK_INFO pBlockInfo, LPBYTE pbFilePointer, LPBYTE pbFileEnd) +{ + // Validate the locale header + pBlockInfo->pLocaleBlockHdr = (PFILE_LOCALE_BLOCK)pbFilePointer; + pbFilePointer += sizeof(FILE_LOCALE_BLOCK); + if(pbFilePointer >= pbFileEnd) + return NULL; + + // Validate the array of 32-bit integers + pBlockInfo->pInt32Array = (PDWORD)pbFilePointer; + pbFilePointer = (LPBYTE)(pBlockInfo->pInt32Array + 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 InitializeCascDirectories(TCascStorage * hs, const TCHAR * szDataPath) +{ + TCHAR * szLastPathPart; + + // Save the game data directory + hs->szDataPath = NewStr(szDataPath, 0); + + // Save the root game directory + hs->szRootPath = NewStr(szDataPath, 0); + + // Find the last part + szLastPathPart = hs->szRootPath; + for(size_t i = 0; hs->szRootPath[i] != 0; i++) + { + if(hs->szRootPath[i] == '\\' || hs->szRootPath[i] == '/') + szLastPathPart = hs->szRootPath + i; + } + + // Cut the last part + if(szLastPathPart != NULL) + szLastPathPart[0] = 0; + return (hs->szRootPath && hs->szDataPath) ? ERROR_SUCCESS : ERROR_NOT_ENOUGH_MEMORY; +} + +static bool IndexDirectory_OnFileFound( + const TCHAR * szFileName, + PDWORD IndexArray, + PDWORD OldIndexArray, + void * pvContext) +{ + TCascStorage * hs = (TCascStorage *)pvContext; + DWORD IndexValue = 0; + DWORD IndexVersion = 0; + + // Auto-detect the format of the index file name + if(hs->szIndexFormat == NULL) + { + if(IsIndexFileName_V1(szFileName)) + hs->szIndexFormat = szIndexFormat_V1; + else if(IsIndexFileName_V2(szFileName)) + hs->szIndexFormat = szIndexFormat_V2; + else + return false; + } + + if(hs->szIndexFormat == szIndexFormat_V1) + { + // Check the index file name format + if(!IsIndexFileName_V1(szFileName)) + return false; + + // Get the main index from the first two digits + if(ConvertDigitToInt32(szFileName + 6, &IndexValue) != ERROR_SUCCESS) + return false; + if(ConvertDigitToInt32(szFileName + 7, &IndexVersion) != ERROR_SUCCESS) + return false; + } + + else if(hs->szIndexFormat == szIndexFormat_V2) + { + // Check the index file name format + if(!IsIndexFileName_V2(szFileName)) + return false; + + // Get the main index from the first two digits + if(ConvertStringToInt32(szFileName, 2, &IndexValue) != ERROR_SUCCESS) + return false; + if(ConvertStringToInt32(szFileName + 2, 8, &IndexVersion) != ERROR_SUCCESS) + return false; + } + else + { + // Should never happen + assert(false); + return false; + } + + // The index value must not be greater than 0x0F + if(IndexValue >= CASC_INDEX_COUNT) + return false; + + // If the new subindex is greater than the previous one, + // use this one instead + if(IndexVersion > IndexArray[IndexValue]) + { + OldIndexArray[IndexValue] = IndexArray[IndexValue]; + IndexArray[IndexValue] = IndexVersion; + } + else if(IndexVersion > OldIndexArray[IndexValue]) + { + OldIndexArray[IndexValue] = IndexVersion; + } + + // Note: WoW6 only keeps last two index files + // Any additional index files are deleted at this point + return true; +} + +static TCHAR * CreateIndexFileName(TCascStorage * hs, DWORD IndexValue, DWORD IndexVersion) +{ + TCHAR szPlainName[0x40]; + + // Sanity checks + assert(hs->szIndexFormat != NULL); + assert(hs->szIndexPath != NULL); + assert(IndexValue <= 0x0F); + + // Create the full path + _stprintf(szPlainName, hs->szIndexFormat, IndexValue, IndexVersion); + return CombinePath(hs->szIndexPath, szPlainName); +} + +static int VerifyAndParseKeyMapping_V1(PCASC_MAPPING_TABLE pKeyMapping, DWORD KeyIndex) +{ + PFILE_INDEX_HEADER_V1 pIndexHeader = (PFILE_INDEX_HEADER_V1)pKeyMapping->pbFileData; + DWORD dwDataHash1; + DWORD dwDataHash2; + + // Verify the format + if(pIndexHeader->field_0 != 0x0005) + return ERROR_NOT_SUPPORTED; + if(pIndexHeader->KeyIndex != KeyIndex) + return ERROR_NOT_SUPPORTED; + if(pIndexHeader->field_8 == 0) + return ERROR_NOT_SUPPORTED; + + // Verofy the bit counts + if(pIndexHeader->SpanSizeBytes != 0x04 || + pIndexHeader->SpanOffsBytes != 0x05 || + pIndexHeader->KeyBytes != 0x09) + return ERROR_NOT_SUPPORTED; + + pKeyMapping->ExtraBytes = 0; + pKeyMapping->SpanSizeBytes = pIndexHeader->SpanSizeBytes; + pKeyMapping->SpanOffsBytes = pIndexHeader->SpanOffsBytes; + pKeyMapping->KeyBytes = pIndexHeader->KeyBytes; + pKeyMapping->SegmentBits = pIndexHeader->SegmentBits; + pKeyMapping->MaxFileOffset = pIndexHeader->MaxFileOffset; + + // Get the pointer to the key entry array + pKeyMapping->nIndexEntries = pIndexHeader->KeyCount1 + pIndexHeader->KeyCount2; + if(pKeyMapping->nIndexEntries != 0) + pKeyMapping->pIndexEntries = (PCASC_INDEX_ENTRY)(pKeyMapping->pbFileData + sizeof(FILE_INDEX_HEADER_V1)); + + // Verify hashes + dwDataHash1 = hashlittle(pKeyMapping->pIndexEntries, pIndexHeader->KeyCount1 * sizeof(CASC_INDEX_ENTRY), 0); + dwDataHash2 = hashlittle(pKeyMapping->pIndexEntries + pIndexHeader->KeyCount1, pIndexHeader->KeyCount2 * sizeof(CASC_INDEX_ENTRY), 0); + if(dwDataHash1 != pIndexHeader->KeysHash1 || dwDataHash2 != pIndexHeader->KeysHash2) + return ERROR_FILE_CORRUPT; + + return ERROR_SUCCESS; +} + +static int VerifyAndParseKeyMapping_V2(PCASC_MAPPING_TABLE pKeyMapping, DWORD KeyIndex) +{ + PFILE_INDEX_HEADER_V2 pIndexHeader = (PFILE_INDEX_HEADER_V2)pKeyMapping->pbFileData; + PBLOCK_SIZE_AND_HASH pSizeAndHash; + LPBYTE pbLastPartEnd; + LPBYTE pbLastPart; + DWORD FilePosition; + DWORD LastPartLength; + unsigned int HashHigh = 0; + unsigned int HashLow = 0; + + // The index header v2 begins after the SizeAndHash + pSizeAndHash = (PBLOCK_SIZE_AND_HASH)pKeyMapping->pbFileData; + pIndexHeader = (PFILE_INDEX_HEADER_V2)(pSizeAndHash + 1); + if(pIndexHeader->IndexVersion != 0x07 || pIndexHeader->KeyIndex != KeyIndex) + return ERROR_BAD_FORMAT; + + if(pIndexHeader->ExtraBytes != 0x00 || + pIndexHeader->SpanSizeBytes != 0x04 || + pIndexHeader->SpanOffsBytes != 0x05 || + pIndexHeader->KeyBytes != CASC_FILE_KEY_SIZE) + return ERROR_BAD_FORMAT; + + // Remember the sizes + pKeyMapping->ExtraBytes = pIndexHeader->ExtraBytes; + pKeyMapping->SpanSizeBytes = pIndexHeader->SpanSizeBytes; + pKeyMapping->SpanOffsBytes = pIndexHeader->SpanOffsBytes; + pKeyMapping->KeyBytes = pIndexHeader->KeyBytes; + pKeyMapping->SegmentBits = pIndexHeader->SegmentBits; + pKeyMapping->MaxFileOffset = pIndexHeader->MaxFileOffset; + + // Get the data position + FilePosition = (sizeof(BLOCK_SIZE_AND_HASH) + pSizeAndHash->cbBlockSize + 0x0F) & 0xFFFFFFF0; + if((FilePosition + 0x08) > pKeyMapping->cbFileData) + return ERROR_BAD_FORMAT; + + // Get the pointer to "size+hash" block + pSizeAndHash = (PBLOCK_SIZE_AND_HASH)(pKeyMapping->pbFileData + FilePosition); + FilePosition += 0x08; + + if((FilePosition + pSizeAndHash->cbBlockSize) > pKeyMapping->cbFileData) + return ERROR_BAD_FORMAT; + if(pSizeAndHash->cbBlockSize < sizeof(CASC_INDEX_ENTRY)) + return ERROR_BAD_FORMAT; + + // Remember the array of file keys + pKeyMapping->pIndexEntries = (PCASC_INDEX_ENTRY)(pKeyMapping->pbFileData + FilePosition); + pKeyMapping->nIndexEntries = pSizeAndHash->cbBlockSize / sizeof(CASC_INDEX_ENTRY); + FilePosition += pSizeAndHash->cbBlockSize; + + // Verify the integrity of the key array + for(DWORD i = 0; i < pKeyMapping->nIndexEntries; i++) + hashlittle2(pKeyMapping->pIndexEntries + i, sizeof(CASC_INDEX_ENTRY), &HashHigh, &HashLow); + if(HashHigh != pSizeAndHash->dwBlockHash) + return ERROR_BAD_FORMAT; + + // Align the data position up to next 0x1000 + FilePosition = ALIGN_TO_SIZE(FilePosition, 0x1000); + if(FilePosition > pKeyMapping->cbFileData) + return ERROR_BAD_FORMAT; + + LastPartLength = pKeyMapping->cbFileData - FilePosition; + if(LastPartLength < 0x7800) + return ERROR_BAD_FORMAT; + + pbLastPart = pKeyMapping->pbFileData + FilePosition; + pbLastPartEnd = pbLastPart + ((LastPartLength >> 0x09) << 0x09); + + while(pbLastPart < pbLastPartEnd) + { + for(int i = 0; i < 0x1F8; i += 0x18) + { + PDWORD PtrLastPart = (PDWORD)pbLastPart; + if(PtrLastPart[0] == 0) + return ERROR_SUCCESS; + + HashLow = hashlittle(PtrLastPart + 1, 0x13, 0) | 0x80000000; + if(HashLow != PtrLastPart[0]) + return ERROR_BAD_FORMAT; + } + + pbLastPart += 0x200; + } + + return ERROR_SUCCESS; +} + +static int VerifyAndParseKeyMapping(PCASC_MAPPING_TABLE pKeyMapping, DWORD KeyIndex) +{ + // Sanity checks + assert(pKeyMapping->pbFileData != NULL); + assert(pKeyMapping->cbFileData != 0); + + // Check for CASC version 2 + if(IsCascIndexHeader_V2(pKeyMapping->pbFileData, pKeyMapping->cbFileData)) + return VerifyAndParseKeyMapping_V2(pKeyMapping, KeyIndex); + + // Check for CASC version 1 + if(IsCascIndexHeader_V1(pKeyMapping->pbFileData, pKeyMapping->cbFileData)) + return VerifyAndParseKeyMapping_V1(pKeyMapping, KeyIndex); + + // Unknown CASC version + assert(false); + return ERROR_BAD_FORMAT; +} + +static int LoadKeyMapping(PCASC_MAPPING_TABLE pKeyMapping, DWORD KeyIndex) +{ + TFileStream * pStream; + ULONGLONG FileSize = 0; + int nError = ERROR_SUCCESS; + + // Sanity checks + assert(pKeyMapping->szFileName != NULL && pKeyMapping->szFileName[0] != 0); + + // Open the stream for read-only access and read the file + pStream = FileStream_OpenFile(pKeyMapping->szFileName, STREAM_FLAG_READ_ONLY | STREAM_PROVIDER_FLAT | BASE_PROVIDER_FILE); + if(pStream != NULL) + { + // Retrieve the file size + FileStream_GetSize(pStream, &FileSize); + if((0 < FileSize && FileSize <= 0x90000) || 1) + { + // WoW6 actually reads THE ENTIRE file to memory + // Verified on Mac build (x64) + pKeyMapping->pbFileData = CASC_ALLOC(BYTE, (DWORD)FileSize); + pKeyMapping->cbFileData = (DWORD)FileSize; + + // Load the data to memory and parse it + if(pKeyMapping->pbFileData != NULL) + { + if(FileStream_Read(pStream, NULL, pKeyMapping->pbFileData, pKeyMapping->cbFileData)) + { + nError = VerifyAndParseKeyMapping(pKeyMapping, KeyIndex); + } + } + else + nError = ERROR_NOT_ENOUGH_MEMORY; + } + else + { + assert(false); + nError = ERROR_BAD_FORMAT; + } + + // Close the file stream + FileStream_Close(pStream); + } + else + nError = GetLastError(); + + return ERROR_SUCCESS; +} + +static int CreateArrayOfIndexEntries(TCascStorage * hs) +{ + PCASC_MAP pMap; + DWORD TotalCount = 0; + int nError = ERROR_NOT_ENOUGH_MEMORY; + + // Count the total number of files in the storage + for(size_t i = 0; i < CASC_INDEX_COUNT; i++) + TotalCount += hs->KeyMapping[i].nIndexEntries; + + // Create the map of all index entries + pMap = Map_Create(TotalCount, CASC_FILE_KEY_SIZE, FIELD_OFFSET(CASC_INDEX_ENTRY, IndexKey)); + if(pMap != NULL) + { + // Put all index entries in the map + for(size_t i = 0; i < CASC_INDEX_COUNT; i++) + { + PCASC_INDEX_ENTRY pIndexEntry = hs->KeyMapping[i].pIndexEntries; + DWORD nIndexEntries = hs->KeyMapping[i].nIndexEntries; + + for(DWORD j = 0; j < nIndexEntries; j++) + { + // Insert the index entry to the map + // Note that duplicate entries will not be inserted to the map + // + // Duplicate entries in WoW-WOD build 18179: + // 9e dc a7 8f e2 09 ad d8 b7 (encoding file) + // f3 5e bb fb d1 2b 3f ef 8b + // c8 69 9f 18 a2 5e df 7e 52 + Map_InsertObject(pMap, pIndexEntry->IndexKey); + + // Move to the next entry + pIndexEntry++; + } + } + + // Store the map to the storage handle + hs->pIndexEntryMap = pMap; + nError = ERROR_SUCCESS; + } + + return nError; +} + +static int CreateMapOfEncodingKeys(TCascStorage * hs, PFILE_ENCODING_SEGMENT pEncodingSegment, DWORD dwNumberOfSegments) +{ + PCASC_ENCODING_ENTRY pEncodingEntry; + size_t nMaxEntries; + size_t nEntries = 0; + int nError = ERROR_SUCCESS; + + // Sanity check + assert(hs->ppEncodingEntries == NULL); + assert(hs->pIndexEntryMap != NULL); + + // Calculate the largest eventual number of encodign entries + nMaxEntries = (dwNumberOfSegments * CASC_ENCODING_SEGMENT_SIZE) / (sizeof(CASC_ENCODING_ENTRY) + MD5_HASH_SIZE); + + // Allocate the array of pointers to encoding entries + hs->ppEncodingEntries = CASC_ALLOC(PCASC_ENCODING_ENTRY, nMaxEntries); + if(hs->ppEncodingEntries != NULL) + { + LPBYTE pbStartOfSegment = (LPBYTE)(pEncodingSegment + dwNumberOfSegments); + + // Parse all segments + for(DWORD i = 0; i < dwNumberOfSegments; i++) + { + LPBYTE pbEncodingEntry = pbStartOfSegment; + LPBYTE pbEndOfSegment = pbStartOfSegment + CASC_ENCODING_SEGMENT_SIZE - sizeof(CASC_ENCODING_ENTRY) - MD5_HASH_SIZE; + + // Parse all encoding entries + while(pbEncodingEntry <= pbEndOfSegment) + { + // Get pointer to the encoding entry + pEncodingEntry = (PCASC_ENCODING_ENTRY)pbEncodingEntry; + if(pEncodingEntry->KeyCount == 0) + break; + + // Insert the pointer the array + hs->ppEncodingEntries[nEntries++] = pEncodingEntry; + + // Move to the next encoding entry + pbEncodingEntry += sizeof(CASC_ENCODING_ENTRY) + (pEncodingEntry->KeyCount * MD5_HASH_SIZE); + } + + // Move to the next segment + pbStartOfSegment += CASC_ENCODING_SEGMENT_SIZE; + } + + // Remember the total number of encoding entries + hs->nEncodingEntries = nEntries; + } + else + nError = ERROR_NOT_ENOUGH_MEMORY; + + return nError; +} + +static DWORD GetSizeOfEncodingFile(HANDLE hFile) +{ + CASC_ENCODING_HEADER EncodingHeader; + DWORD cbEncodingFile = 0; + DWORD dwSegmentPos; + DWORD dwNumSegments; + DWORD dwBytesRead; + + // Read the endoding header + CascReadFile(hFile, &EncodingHeader, sizeof(CASC_ENCODING_HEADER), &dwBytesRead); + if(dwBytesRead == sizeof(CASC_ENCODING_HEADER)) + { + dwNumSegments = ConvertBytesToInteger_4(EncodingHeader.NumSegments); + dwSegmentPos = ConvertBytesToInteger_4(EncodingHeader.SegmentsPos); + + cbEncodingFile = sizeof(CASC_ENCODING_HEADER) + + dwSegmentPos + + dwNumSegments * (sizeof(FILE_ENCODING_SEGMENT) + CASC_ENCODING_SEGMENT_SIZE); + } + + // Reset the position back + CascSetFilePointer(hFile, 0, NULL, FILE_BEGIN); + return cbEncodingFile; +} + +static LPBYTE LoadCascFile(HANDLE hFile, DWORD cbMaxSize, PDWORD pcbFileData) +{ + LPBYTE pbFileData = NULL; + DWORD cbFileData; + DWORD dwBytesRead = 0; + int nError = ERROR_SUCCESS; + + // Retrieve the size of the file + cbFileData = CascGetFileSize(hFile, NULL); + if(cbFileData != 0 && cbFileData != CASC_INVALID_SIZE) + { + // Trim the size to the maximum + cbFileData = CASCLIB_MIN(cbMaxSize, cbFileData); + + // Allocate the buffer that will hold the entire file + pbFileData = CASC_ALLOC(BYTE, cbFileData); + if(pbFileData != NULL) + { + // Read the entire file to memory + CascReadFile(hFile, pbFileData, cbFileData, &dwBytesRead); + if(dwBytesRead != cbFileData) + nError = ERROR_FILE_CORRUPT; + } + else + nError = ERROR_NOT_ENOUGH_MEMORY; + } + else + nError = ERROR_FILE_CORRUPT; + + // If something failed, clean-up the buffers + if(nError != ERROR_SUCCESS) + { + // Clear the file data + if(pbFileData != NULL) + CASC_FREE(pbFileData); + pbFileData = NULL; + cbFileData = 0; + + // Set the last error value + SetLastError(nError); + } + + // Return what we got + if(pcbFileData != NULL) + *pcbFileData = cbFileData; + return pbFileData; +} + +static int LoadIndexFiles(TCascStorage * hs) +{ + DWORD IndexArray[CASC_INDEX_COUNT]; + DWORD OldIndexArray[CASC_INDEX_COUNT]; + int nError; + int i; + + // Scan all index files + memset(IndexArray, 0, sizeof(IndexArray)); + memset(OldIndexArray, 0, sizeof(OldIndexArray)); + nError = ScanIndexDirectory(hs->szIndexPath, IndexDirectory_OnFileFound, IndexArray, OldIndexArray, hs); + if(nError == ERROR_SUCCESS) + { + // Load each index file + for(i = 0; i < CASC_INDEX_COUNT; i++) + { + hs->KeyMapping[i].szFileName = CreateIndexFileName(hs, i, IndexArray[i]); + if(hs->KeyMapping[i].szFileName != NULL) + { + nError = LoadKeyMapping(&hs->KeyMapping[i], i); + if(nError != ERROR_SUCCESS) + break; + } + } + } + + // Now we need to build the map of the index entries + if(nError == ERROR_SUCCESS) + { + nError = CreateArrayOfIndexEntries(hs); + } + + return nError; +} + +static int LoadEncodingFile(TCascStorage * hs) +{ + PFILE_ENCODING_SEGMENT pEncodingSegment; + PCASC_ENCODING_ENTRY pEncodingEntry; + LPBYTE pbStartOfSegment; + LPBYTE pbEncodingFile = NULL; + HANDLE hFile = NULL; + DWORD cbEncodingFile = 0; + DWORD dwNumberOfSegments = 0; + DWORD dwSegmentsPos = 0; + int nError = ERROR_SUCCESS; + + // Open the encoding file + if(!CascOpenFileByIndexKey((HANDLE)hs, &hs->EncodingEKey, 0, &hFile)) + nError = GetLastError(); + + // Load the encoding file to memory + if(nError == ERROR_SUCCESS) + { + // Retrieve the CASC header. We do not usually need to load + // the entire file, but we need to know how big part of it we need + cbEncodingFile = GetSizeOfEncodingFile(hFile); + + // Load the entire file to memory + pbEncodingFile = LoadCascFile(hFile, cbEncodingFile, &cbEncodingFile); + if(pbEncodingFile == NULL || cbEncodingFile <= sizeof(CASC_ENCODING_HEADER)) + nError = ERROR_FILE_CORRUPT; + + CascCloseFile(hFile); + } + + // Verify all encoding segments + if(nError == ERROR_SUCCESS) + { + // Save the encoding header + hs->pEncodingHeader = (PCASC_ENCODING_HEADER)pbEncodingFile; + + // Convert size and offset + dwNumberOfSegments = ConvertBytesToInteger_4(hs->pEncodingHeader->NumSegments); + dwSegmentsPos = ConvertBytesToInteger_4(hs->pEncodingHeader->SegmentsPos); + + // Allocate the array of encoding segments + pEncodingSegment = (PFILE_ENCODING_SEGMENT)(pbEncodingFile + sizeof(CASC_ENCODING_HEADER) + dwSegmentsPos); + pbStartOfSegment = (LPBYTE)(pEncodingSegment + dwNumberOfSegments); + + // Go through all encoding segments and verify them + for(DWORD i = 0; i < dwNumberOfSegments; i++) + { + // Check if there is enough space in the buffer + if((pbStartOfSegment + CASC_ENCODING_SEGMENT_SIZE) > (pbEncodingFile + cbEncodingFile)) + { + nError = ERROR_FILE_CORRUPT; + break; + } + + // Check the hash of the entire segment + // Note that verifying takes considerable time of the storage loading +// if(!VerifyDataBlockHash(pbStartOfSegment, CASC_ENCODING_SEGMENT_SIZE, pEncodingSegment->SegmentHash)) +// { +// nError = ERROR_FILE_CORRUPT; +// break; +// } + + // Check if the encoding key matches + pEncodingEntry = (PCASC_ENCODING_ENTRY)pbStartOfSegment; + if(memcmp(pEncodingEntry->EncodingKey, pEncodingSegment->FirstEncodingKey, MD5_HASH_SIZE)) + { + nError = ERROR_FILE_CORRUPT; + break; + } + + // Move to the next segment + pbStartOfSegment += CASC_ENCODING_SEGMENT_SIZE; + pEncodingSegment++; + } + } + + // Create the map of the encoding keys + // Note that the array of encoding keys is already sorted - no need to sort it + if(nError == ERROR_SUCCESS) + { + pEncodingSegment = (PFILE_ENCODING_SEGMENT)(pbEncodingFile + sizeof(CASC_ENCODING_HEADER) + dwSegmentsPos); + nError = CreateMapOfEncodingKeys(hs, pEncodingSegment, dwNumberOfSegments); + } + return nError; +} + +static int LoadRootFile(TCascStorage * hs, LPBYTE pbRootFile, DWORD cbRootFile) +{ + PFILE_ROOT_ENTRY pSrcEntry; + PCASC_ROOT_ENTRY pTrgEntry; + ROOT_BLOCK_INFO BlockInfo; + LPBYTE pbRootFileEnd = pbRootFile + cbRootFile; + LPBYTE pbFilePointer; + size_t nRootEntries = 0; + size_t nRootIndex = 0; + int nError = ERROR_NOT_ENOUGH_MEMORY; + + // Calculate the root entries + for(pbFilePointer = pbRootFile; pbFilePointer <= pbRootFileEnd; ) + { + // Validate the root block + pbFilePointer = VerifyLocaleBlock(&BlockInfo, pbFilePointer, pbRootFileEnd); + if(pbFilePointer == NULL) + break; + + // Add the number of entries + nRootEntries = nRootEntries + BlockInfo.pLocaleBlockHdr->NumberOfFiles; + } + + // Create a linear array of the root entries and sort it + hs->pRootEntries = pTrgEntry = CASC_ALLOC(CASC_ROOT_ENTRY, nRootEntries); + hs->ppRootEntries = CASC_ALLOC(PCASC_ROOT_ENTRY, nRootEntries); + if(hs->ppRootEntries && hs->pRootEntries) + { + // Convert each entry from FILE_ROOT_ENTRY to CASC_ROOT_ENTRY + for(pbFilePointer = pbRootFile; pbFilePointer <= pbRootFileEnd; ) + { + // Validate the root block + pbFilePointer = VerifyLocaleBlock(&BlockInfo, pbFilePointer, pbRootFileEnd); + if(pbFilePointer == NULL) + break; + + // Get the pointer to the first root entry + pSrcEntry = (PFILE_ROOT_ENTRY)BlockInfo.pRootEntries; + + // Convert all entries + for(DWORD i = 0; i < BlockInfo.pLocaleBlockHdr->NumberOfFiles; i++) + { + // Copy the root entry + CopyFileKey(pTrgEntry->EncodingKey, pSrcEntry->EncodingKey); + pTrgEntry->FileNameHash = pSrcEntry->FileNameHash; + pTrgEntry->Locales = BlockInfo.pLocaleBlockHdr->Locales; + pTrgEntry->Flags = BlockInfo.pLocaleBlockHdr->Flags; + +// if(pTrgEntry->FileNameHash == 0x5ddb88608673f698ULL) +// DebugBreak(); + + // Insert the CASC root entry to the linear array of pointers + hs->ppRootEntries[nRootIndex++] = pTrgEntry; + + // Move to the next root entry + pSrcEntry++; + pTrgEntry++; + } + } + + // Save the number of entries + assert(nRootIndex == nRootEntries); + hs->nRootEntries = nRootIndex; + + // Now sort the array + qsort_pointer_array((void **)hs->ppRootEntries, hs->nRootEntries, CompareRootEntries, NULL); + nError = ERROR_SUCCESS; + } + + return nError; + +/* + FILE * fp = fopen("E:\\root_entries.txt", "wt"); + if(fp != NULL) + { + for(size_t i = 0; i < nRootEntries; i++) + { + fprintf(fp, "%08X: %016I64lX\n", i, hs->ppRootEntries[i]->FileNameHash); + } + fclose(fp); + } +*/ +} + +static int LoadRootFile(TCascStorage * hs) +{ + PDWORD FileSignature; + HANDLE hFile = NULL; + LPBYTE pbRootFile = NULL; + DWORD cbRootFile = 0; + int nError = ERROR_SUCCESS; + + // Sanity checks + assert(hs->ppEncodingEntries != NULL); + assert(hs->pRootEntries == NULL); + assert(hs->nRootEntries == 0); + + // The root file is either MNDX file (Heroes of the Storm) + // or a file containing an array of root entries (World of Warcraft 6.0+) + // Note: The "root" key file's MD5 hash is equal to its name + // in the configuration + if(!CascOpenFileByEncodingKey((HANDLE)hs, &hs->RootKey, 0, &hFile)) + nError = GetLastError(); + + // Load ther entire root file to memory + if(nError == ERROR_SUCCESS) + { + // Load the entire root file to memory + pbRootFile = LoadCascFile(hFile, 0xFFFFFFFF, &cbRootFile); + if(pbRootFile == NULL || cbRootFile == 0) + nError = ERROR_FILE_CORRUPT; + + // Close the root file + CascCloseFile(hFile); + } + + // Check if the file is a MNDX file + if(nError == ERROR_SUCCESS) + { + FileSignature = (PDWORD)pbRootFile; + if(FileSignature[0] == CASC_MNDX_SIGNATURE) + { + nError = LoadMndxRootFile(hs, pbRootFile, cbRootFile); + } + else + { + nError = LoadRootFile(hs, pbRootFile, cbRootFile); + } + } + + // Free the root file + CASC_FREE(pbRootFile); + return nError; +} + +static TCascStorage * FreeCascStorage(TCascStorage * hs) +{ + size_t i; + + if(hs != NULL) + { + // Free the MNDX info + if(hs->pPackages != NULL) + CASC_FREE(hs->pPackages); + if(hs->pMndxInfo != NULL) + FreeMndxInfo(hs->pMndxInfo); + + // Free the pointers to file entries + if(hs->ppRootEntries != NULL) + CASC_FREE(hs->ppRootEntries); + if(hs->pRootEntries != NULL) + CASC_FREE(hs->pRootEntries); + if(hs->ppEncodingEntries != NULL) + CASC_FREE(hs->ppEncodingEntries); + if(hs->pEncodingHeader != NULL) + CASC_FREE(hs->pEncodingHeader); + if(hs->pIndexEntryMap != NULL) + Map_Free(hs->pIndexEntryMap); + + // Close all data files + for(i = 0; i < CASC_MAX_DATA_FILES; i++) + { + if(hs->DataFileArray[i] != NULL) + { + FileStream_Close(hs->DataFileArray[i]); + hs->DataFileArray[i] = NULL; + } + } + + // Close all key mappings + for(i = 0; i < CASC_INDEX_COUNT; i++) + { + if(hs->KeyMapping[i].szFileName != NULL) + CASC_FREE(hs->KeyMapping[i].szFileName); + if(hs->KeyMapping[i].pbFileData != NULL) + CASC_FREE(hs->KeyMapping[i].pbFileData); + hs->KeyMapping[i].pIndexEntries = NULL; + } + + // Free the file paths + if(hs->szRootPath != NULL) + CASC_FREE(hs->szRootPath); + if(hs->szDataPath != NULL) + CASC_FREE(hs->szDataPath); + if(hs->szIndexPath != NULL) + CASC_FREE(hs->szIndexPath); + if(hs->szUrlPath != NULL) + CASC_FREE(hs->szUrlPath); + + // Fre the blob arrays + QUERY_KEY_FreeArray(hs->pArchiveArray); + QUERY_KEY_FreeArray(hs->pPatchArchiveArray); + QUERY_KEY_FreeArray(hs->pEncodingKeys); + + // Free the blobs + QUERY_KEY_Free(&hs->CdnConfigKey); + QUERY_KEY_Free(&hs->CdnBuildKey); + QUERY_KEY_Free(&hs->ArchiveGroup); + QUERY_KEY_Free(&hs->PatchArchiveGroup); + QUERY_KEY_Free(&hs->RootKey); + QUERY_KEY_Free(&hs->PatchKey); + QUERY_KEY_Free(&hs->DownloadKey); + QUERY_KEY_Free(&hs->InstallKey); + + // Free the storage structure + hs->szClassName = NULL; + CASC_FREE(hs); + } + + return NULL; +} + +//----------------------------------------------------------------------------- +// Public functions + +bool WINAPI CascOpenStorage(const TCHAR * szDataPath, DWORD dwFlags, HANDLE * phStorage) +{ + TCascStorage * hs; + int nError = ERROR_SUCCESS; + + CASCLIB_UNUSED(dwFlags); + + // Allocate the storage structure + hs = (TCascStorage *)CASC_ALLOC(TCascStorage, 1); + if(hs == NULL) + nError = ERROR_NOT_ENOUGH_MEMORY; + + // Load the storage configuration + if(nError == ERROR_SUCCESS) + { + // Prepare the base storage parameters + memset(hs, 0, sizeof(TCascStorage)); + hs->szClassName = "TCascStorage"; + hs->dwRefCount = 1; + nError = InitializeCascDirectories(hs, szDataPath); + } + + // Now we need to load the root file so we know the config files + if(nError == ERROR_SUCCESS) + { + nError = LoadBuildConfiguration(hs); + } + + // Load the index files + if(nError == ERROR_SUCCESS) + { + nError = LoadIndexFiles(hs); + } + + // Load the index files + if(nError == ERROR_SUCCESS) + { + nError = LoadEncodingFile(hs); + } + + // Load the index files + if(nError == ERROR_SUCCESS) + { + nError = LoadRootFile(hs); + } + +#ifdef _DEBUG +// if(nError == ERROR_SUCCESS) +// { +// CascDumpStorage("E:\\casc_dump.txt", hs, _T("e:\\Ladik\\Appdir\\CascLib\\listfile\\listfile-wow6.txt")); +// CascDumpIndexEntries("E:\\casc_index.txt", hs); +// } +#endif + + // If something failed, free the storage and return + if(nError != ERROR_SUCCESS) + { + hs = FreeCascStorage(hs); + SetLastError(nError); + } + + *phStorage = (HANDLE)hs; + return (nError == ERROR_SUCCESS); +} + +bool WINAPI CascGetStorageInfo( + HANDLE hStorage, + CASC_STORAGE_INFO_CLASS InfoClass, + void * pvStorageInfo, + size_t cbStorageInfo, + size_t * pcbLengthNeeded) +{ + TCascStorage * hs; + DWORD dwCascFeatures = 0; + + // Verify the storage handle + hs = IsValidStorageHandle(hStorage); + if(hs == NULL) + { + SetLastError(ERROR_INVALID_HANDLE); + return false; + } + + // Differentiate between info classes + switch(InfoClass) + { + case CascStorageFileCount: + + // Check the buffer size + if(cbStorageInfo < sizeof(DWORD)) + { + *pcbLengthNeeded = sizeof(DWORD); + SetLastError(ERROR_INSUFFICIENT_BUFFER); + return false; + } + + // Give the number of files + *(PDWORD)pvStorageInfo = (DWORD)hs->pIndexEntryMap->ItemCount; + return true; + + case CascStorageFeatures: + + // Check the buffer size + if(cbStorageInfo < sizeof(DWORD)) + { + *pcbLengthNeeded = sizeof(DWORD); + SetLastError(ERROR_INSUFFICIENT_BUFFER); + return false; + } + + // Construct the features + if(hs->pMndxInfo != NULL) + dwCascFeatures |= CASC_FEATURE_LISTFILE; + + // Give the number of files + *(PDWORD)pvStorageInfo = dwCascFeatures; + return true; + + default: + SetLastError(ERROR_INVALID_PARAMETER); + return false; + } +} + + + +bool WINAPI CascCloseStorage(HANDLE hStorage) +{ + TCascStorage * hs; + + // Verify the storage handle + hs = IsValidStorageHandle(hStorage); + if(hs == NULL) + { + SetLastError(ERROR_INVALID_PARAMETER); + return false; + } + + // Only free the storage if the reference count reaches 0 + if(hs->dwRefCount == 1) + { + FreeCascStorage(hs); + return true; + } + + // Just decrement number of references + hs->dwRefCount--; + return true; +} + |