Files
TrinityCore/dep/CascLib/src/CascOpenStorage.cpp

1567 lines
53 KiB
C++

/*****************************************************************************/
/* 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 Created */
/*****************************************************************************/
#define __CASCLIB_SELF__
#include "CascLib.h"
#include "CascCommon.h"
#ifdef INTERLOCKED_NOT_SUPPORTED
#pragma error Interlocked operations are not supported on this architecture. Multi-threaded access to CASC storages will not work properly.
#endif
//-----------------------------------------------------------------------------
// Local defines
// Limit for "additional" items in CKey table
#define CASC_MAX_EXTRA_ITEMS 0x40
//-----------------------------------------------------------------------------
// DEBUG functions
#define CHECKED_KEY {0x00, 0x00, 0x0F, 0x84}
#if defined(CASCLIB_DEBUG) && defined(CHECKED_KEY)
inline bool CheckForXKey(LPBYTE XKey)
{
BYTE CheckedKey[] = CHECKED_KEY;
for(size_t i = 0; i < _countof(CheckedKey); i++)
{
if(XKey[i] != CheckedKey[i])
return false;
}
return true;
}
#define BREAK_ON_WATCHED(XKey) if(CheckForXKey((LPBYTE)XKey)) { __debugbreak(); }
#else
#define BREAK_ON_WATCHED(XKey) { /* NOTHING */ }
#endif
//-----------------------------------------------------------------------------
// TCascStorage class functions
TCascStorage::TCascStorage()
{
// Prepare the base storage parameters
ClassName = CASC_MAGIC_STORAGE;
pRootHandler = NULL;
dwRefCount = 1;
szRootPath = szDataPath = szIndexPath = szFilesPath = szConfigPath = szMainFile = NULL;
szCdnHostUrl = szCdnServers = szCdnPath = szCodeName = NULL;
szIndexFormat = NULL;
szRegion = NULL;
szBuildKey = NULL;
memset(DataFiles, 0, sizeof(DataFiles));
memset(IndexFiles, 0, sizeof(IndexFiles));
CascInitLock(StorageLock);
dwDefaultLocale = 0;
dwBuildNumber = 0;
dwFeatures = 0;
BuildFileType = CascBuildNone;
LastFailKeyName = 0;
LocalFiles = TotalFiles = EKeyEntries = EKeyLength = FileOffsetBits = 0;
pArgs = NULL;
}
TCascStorage::~TCascStorage()
{
// Free the root handler
if(pRootHandler != NULL)
delete pRootHandler;
pRootHandler = NULL;
// Close all data files
for(size_t i = 0; i < CASC_MAX_DATA_FILES; i++)
{
FileStream_Close(DataFiles[i]);
DataFiles[i] = NULL;
}
// Cleanup space occupied by index files
FreeIndexFiles(this);
// Cleanup the lock
CascFreeLock(StorageLock);
// Free the file paths
CASC_FREE(szRootPath);
CASC_FREE(szDataPath);
CASC_FREE(szIndexPath);
CASC_FREE(szFilesPath);
CASC_FREE(szConfigPath);
CASC_FREE(szMainFile);
CASC_FREE(szCdnHostUrl);
CASC_FREE(szCdnServers);
CASC_FREE(szCdnPath);
CASC_FREE(szCodeName);
CASC_FREE(szRegion);
CASC_FREE(szBuildKey);
// Free the blobs
FreeCascBlob(&CdnConfigKey);
FreeCascBlob(&CdnBuildKey);
FreeCascBlob(&ArchiveGroup);
FreeCascBlob(&ArchivesKey);
FreeCascBlob(&PatchArchivesKey);
FreeCascBlob(&PatchArchivesGroup);
FreeCascBlob(&BuildFiles);
ClassName = 0;
}
TCascStorage * TCascStorage::AddRef()
{
// Need this to be atomic to make multi-threaded file opens work
CascInterlockedIncrement(&dwRefCount);
return this;
}
TCascStorage * TCascStorage::Release()
{
// If the reference count reached zero, we close the archive
// Need this to be atomic to make multi-threaded file opens work
if(CascInterlockedDecrement(&dwRefCount) == 0)
{
// Release all references in the socket cache
if(dwFeatures & CASC_FEATURE_ONLINE)
sockets_set_caching(false);
// Delete the object and return NULL
delete this;
return NULL;
}
return this;
}
//-----------------------------------------------------------------------------
// Local functions
void * ProbeOutputBuffer(void * pvBuffer, size_t cbLength, size_t cbMinLength, size_t * pcbLengthNeeded)
{
// Verify the output length
if(cbLength < cbMinLength)
{
SetCascError(ERROR_INSUFFICIENT_BUFFER);
pvBuffer = NULL;
}
// Give the output length and return result
if(pcbLengthNeeded != NULL)
pcbLengthNeeded[0] = cbMinLength;
return pvBuffer;
}
// Inserts an entry from the text build file
static PCASC_CKEY_ENTRY InsertCKeyEntry(TCascStorage * hs, CASC_CKEY_ENTRY & CKeyEntry)
{
PCASC_CKEY_ENTRY pCKeyEntry = NULL;
// Stop on file-of-interest
BREAK_ON_WATCHED(CKeyEntry.EKey);
// Skip entries without any key
if(CKeyEntry.Flags & (CASC_CE_HAS_CKEY | CASC_CE_HAS_EKEY))
{
// Check if there is an existing entry
if((pCKeyEntry = FindCKeyEntry_CKey(hs, CKeyEntry.CKey)) == NULL)
{
// Insert a new entry to the array. DO NOT ALLOW enlarge array here
pCKeyEntry = (PCASC_CKEY_ENTRY)hs->CKeyArray.Insert(1, false);
if(pCKeyEntry == NULL)
return NULL;
// Fill in the item
memcpy(pCKeyEntry, &CKeyEntry, sizeof(CASC_CKEY_ENTRY));
// If we have CKey present, insert it to the CKey map
if(CKeyEntry.Flags & CASC_CE_HAS_CKEY)
hs->CKeyMap.InsertObject(pCKeyEntry, pCKeyEntry->CKey);
// If we have EKey present, insert it to the EKey map
if(CKeyEntry.Flags & CASC_CE_HAS_EKEY)
hs->EKeyMap.InsertObject(pCKeyEntry, pCKeyEntry->EKey);
}
else
{
if(pCKeyEntry->ContentSize == CASC_INVALID_SIZE)
pCKeyEntry->ContentSize = CKeyEntry.ContentSize;
if(pCKeyEntry->EncodedSize == CASC_INVALID_SIZE)
pCKeyEntry->EncodedSize = CKeyEntry.EncodedSize;
}
}
return pCKeyEntry;
}
// Inserts an entry from ENCODING
static PCASC_CKEY_ENTRY InsertCKeyEntry(TCascStorage * hs, PFILE_CKEY_ENTRY pFileEntry)
{
PCASC_CKEY_ENTRY pCKeyEntry;
// Stop on file-of-interest
BREAK_ON_WATCHED(pFileEntry->EKey);
// Insert a new entry to the array. DO NOT ALLOW enlarge array here
pCKeyEntry = (PCASC_CKEY_ENTRY)hs->CKeyArray.Insert(1, false);
if(pCKeyEntry != NULL)
{
// Initialize the entry
CopyMemory16(pCKeyEntry->CKey, pFileEntry->CKey);
CopyMemory16(pCKeyEntry->EKey, pFileEntry->EKey);
pCKeyEntry->StorageOffset = CASC_INVALID_OFFS64;
pCKeyEntry->TagBitMask = 0;
pCKeyEntry->ContentSize = ConvertBytesToInteger_4(pFileEntry->ContentSize);
pCKeyEntry->EncodedSize = CASC_INVALID_SIZE;
pCKeyEntry->Flags = CASC_CE_HAS_CKEY | CASC_CE_HAS_EKEY | CASC_CE_IN_ENCODING;
pCKeyEntry->RefCount = 0;
pCKeyEntry->SpanCount = 1;
// Copy the information from index files to the CKey entry
CopyEKeyEntry(hs, pCKeyEntry);
// Insert the item into both maps
hs->CKeyMap.InsertObject(pCKeyEntry, pCKeyEntry->CKey);
hs->EKeyMap.InsertObject(pCKeyEntry, pCKeyEntry->EKey);
}
else
{
assert(false);
}
return pCKeyEntry;
}
// Inserts an entry from DOWNLOAD
static PCASC_CKEY_ENTRY InsertCKeyEntry(TCascStorage * hs, CASC_DOWNLOAD_ENTRY & DlEntry)
{
PCASC_CKEY_ENTRY pCKeyEntry;
// Stop on file-of-interest
BREAK_ON_WATCHED(DlEntry.EKey);
// Check whether the entry is already there
if((pCKeyEntry = FindCKeyEntry_EKey(hs, DlEntry.EKey)) == NULL)
{
// Insert dummy CKey entry to the array. DO NOT allow to enlarge the array
pCKeyEntry = (PCASC_CKEY_ENTRY)hs->CKeyArray.Insert(1, false);
if(pCKeyEntry == NULL)
{
assert(false);
return NULL;
}
// Copy the entry
ZeroMemory16(pCKeyEntry->CKey);
CopyMemory16(pCKeyEntry->EKey, DlEntry.EKey);
pCKeyEntry->StorageOffset = CASC_INVALID_OFFS64;
pCKeyEntry->TagBitMask = 0;
pCKeyEntry->ContentSize = CASC_INVALID_SIZE;
pCKeyEntry->EncodedSize = (DWORD)DlEntry.EncodedSize;
pCKeyEntry->Flags = CASC_CE_HAS_EKEY | CASC_CE_IN_DOWNLOAD;
pCKeyEntry->RefCount = 0;
pCKeyEntry->SpanCount = 1;
// Copy the information from index files to the CKey entry
CopyEKeyEntry(hs, pCKeyEntry);
// Insert the entry to the map. Only insert it to the EKey map, as there is no CKey present
hs->EKeyMap.InsertObject(pCKeyEntry, pCKeyEntry->EKey);
}
else
{
// Copy the EKey if we only have partial one
if(pCKeyEntry->Flags & CASC_CE_HAS_EKEY_PARTIAL)
CopyMemory16(pCKeyEntry->EKey, DlEntry.EKey);
// Supply the encoded size, if unknown yet
if(pCKeyEntry->EncodedSize == CASC_INVALID_SIZE)
pCKeyEntry->EncodedSize = (DWORD)DlEntry.EncodedSize;
pCKeyEntry->Flags = (pCKeyEntry->Flags & ~CASC_CE_HAS_EKEY_PARTIAL) | CASC_CE_IN_DOWNLOAD;
}
// Save the rest and return the entry
pCKeyEntry->Priority = DlEntry.Priority;
return pCKeyEntry;
}
static DWORD CopyBuildFileItemsToCKeyArray(TCascStorage * hs)
{
// Insert the well-known files
// InsertCKeyEntry(hs, hs->EncodingCKey);
InsertCKeyEntry(hs, hs->DownloadCKey);
InsertCKeyEntry(hs, hs->InstallCKey);
InsertCKeyEntry(hs, hs->PatchFile);
InsertCKeyEntry(hs, hs->RootFile);
InsertCKeyEntry(hs, hs->SizeFile);
InsertCKeyEntry(hs, hs->VfsRoot);
// Insert all VFS roots
for(size_t i = 0; i < hs->VfsRootList.ItemCount(); i++)
{
PCASC_CKEY_ENTRY pCKeyEntry = (PCASC_CKEY_ENTRY)hs->VfsRootList.ItemAt(i);
InsertCKeyEntry(hs, *pCKeyEntry);
}
return ERROR_SUCCESS;
}
// Estimate the total number of files, so we won't have to re-allocate arrays and maps
// and thus speed-up storage loading. In theory, we could guess the file count by
// measuring size of ENCODING or DOWNLOAD manifests.
static size_t GetEstimatedNumberOfFiles(TCascStorage * hs)
{
size_t nNumberOfFiles1 = 0;
size_t nNumberOfFiles2 = 0;
// If we know the size of DOWNLOAD at this point, we estimate number of files from it.
// Size of one entry in DOWNLOAD is at least 22 bytes. This is the most reliable method.
// However, for some online storages ("agent"), this is a very small value
if(hs->DownloadCKey.ContentSize != CASC_INVALID_SIZE)
nNumberOfFiles1 = (hs->DownloadCKey.ContentSize / sizeof(FILE_DOWNLOAD_ENTRY)) + CASC_MAX_EXTRA_ITEMS;
// If we know the size of ENCODING at this point, we estimate number of files from it.
// Size of one entry in ENCODING is at least 38 bytes. This method fails on storages
// with TVFS file system, as ENCODING only contains a small subset of file.
// Fortunately, all known TVFS-based storages have "download-size" present
if(hs->EncodingCKey.ContentSize != CASC_INVALID_SIZE)
nNumberOfFiles2 = (hs->EncodingCKey.ContentSize / sizeof(FILE_CKEY_ENTRY)) + CASC_MAX_EXTRA_ITEMS;
// Do we know any of them?
if(nNumberOfFiles1 || nNumberOfFiles2)
return CASCLIB_MAX(nNumberOfFiles1, nNumberOfFiles2);
// Older storages (HOTS before 39445, WoW before 19116) don't state sizes of ENCODING
// and DOWNLOAD in the Build Config files. Solution: Assume there is max 1M of files
return 1000000;
}
static DWORD InitCKeyArray(TCascStorage * hs)
{
size_t nNumberOfFiles = GetEstimatedNumberOfFiles(hs);
DWORD dwErrCode;
//
// Allocate array and map of CKey entries
//
// Create the array of CKey items
dwErrCode = hs->CKeyArray.Create(sizeof(CASC_CKEY_ENTRY), nNumberOfFiles);
if(dwErrCode != ERROR_SUCCESS)
return dwErrCode;
// Create the map CKey -> CASC_CKEY_ENTRY
dwErrCode = hs->CKeyMap.Create(nNumberOfFiles, MD5_HASH_SIZE, FIELD_OFFSET(CASC_CKEY_ENTRY, CKey));
if(dwErrCode != ERROR_SUCCESS)
return dwErrCode;
// Create the map CKey -> CASC_CKEY_ENTRY. Note that TVFS root references files
// using 9-byte EKey, so cut the search EKey length to 9 bytes
dwErrCode = hs->EKeyMap.Create(nNumberOfFiles, CASC_EKEY_SIZE, FIELD_OFFSET(CASC_CKEY_ENTRY, EKey));
if(dwErrCode != ERROR_SUCCESS)
return dwErrCode;
return ERROR_SUCCESS;
}
int CaptureEncodingHeader(CASC_ENCODING_HEADER & EnHeader, LPBYTE pbFileData, size_t cbFileData)
{
PFILE_ENCODING_HEADER pFileHeader = (PFILE_ENCODING_HEADER)pbFileData;
// Check the signature ('EN') and version
if(cbFileData < sizeof(FILE_ENCODING_HEADER) || pFileHeader->Magic != FILE_MAGIC_ENCODING || pFileHeader->Version != 0x01)
return ERROR_BAD_FORMAT;
// Note that we don't support CKey and EKey sizes other than 0x10 in the ENCODING file
if(pFileHeader->CKeyLength != MD5_HASH_SIZE || pFileHeader->EKeyLength != MD5_HASH_SIZE)
return ERROR_BAD_FORMAT;
EnHeader.Magic = pFileHeader->Magic;
EnHeader.Version = pFileHeader->Version;
EnHeader.CKeyLength = pFileHeader->CKeyLength;
EnHeader.EKeyLength = pFileHeader->EKeyLength;
EnHeader.CKeyPageCount = ConvertBytesToInteger_4(pFileHeader->CKeyPageCount);
EnHeader.CKeyPageSize = ConvertBytesToInteger_2(pFileHeader->CKeyPageSize) * 1024;
EnHeader.EKeyPageCount = ConvertBytesToInteger_4(pFileHeader->EKeyPageCount);
EnHeader.EKeyPageSize = ConvertBytesToInteger_2(pFileHeader->EKeyPageSize) * 1024;
EnHeader.ESpecBlockSize = ConvertBytesToInteger_4(pFileHeader->ESpecBlockSize);
return ERROR_SUCCESS;
}
static DWORD LoadEncodingCKeyPage(TCascStorage * hs, CASC_ENCODING_HEADER & EnHeader, LPBYTE pbPageBegin, LPBYTE pbEndOfPage)
{
PFILE_CKEY_ENTRY pFileEntry;
LPBYTE pbFileEntry = pbPageBegin;
// Sanity checks
assert(hs->CKeyMap.IsInitialized());
assert(hs->EKeyMap.IsInitialized());
// Parse all encoding entries
while(pbFileEntry < pbEndOfPage)
{
// Get pointer to the encoding entry
pFileEntry = (PFILE_CKEY_ENTRY)pbFileEntry;
if(pFileEntry->EKeyCount == 0)
break;
// Example of a file entry with multiple EKeys:
// Overwatch build 24919, CKey: 0e 90 94 fa d2 cb 85 ac d0 7c ea 09 f9 c5 ba 00
// BREAKIF(pFileEntry->EKeyCount > 1);
// BREAK_ON_XKEY3(pFileEntry->CKey, 0x34, 0x82, 0x1f);
// Insert the entry to the central CKey table
InsertCKeyEntry(hs, pFileEntry);
// Move to the next encoding entry
pbFileEntry = pbFileEntry + 2 + 4 + EnHeader.CKeyLength + (pFileEntry->EKeyCount * EnHeader.EKeyLength);
}
return ERROR_SUCCESS;
}
static DWORD LoadEncodingManifest(TCascStorage * hs)
{
CASC_CKEY_ENTRY & CKeyEntry = hs->EncodingCKey;
CASC_BLOB EncodingFile;
DWORD dwErrCode = ERROR_SUCCESS;
// Inform the user about what we are doing
if(InvokeProgressCallback(hs, CascProgressLoadingManifest, "ENCODING", 0, 0))
return ERROR_CANCELLED;
// Fill-in the information from the index entry and insert it to the file tree
if(!CopyEKeyEntry(hs, &CKeyEntry))
return ERROR_FILE_NOT_FOUND;
InsertCKeyEntry(hs, CKeyEntry);
// Load the entire encoding file to memory
dwErrCode = LoadInternalFileToMemory(hs, &hs->EncodingCKey, EncodingFile);
if(dwErrCode == ERROR_SUCCESS && EncodingFile.cbData != 0)
{
CASC_ENCODING_HEADER EnHeader;
// Capture the header of the ENCODING file
dwErrCode = CaptureEncodingHeader(EnHeader, EncodingFile.pbData, EncodingFile.cbData);
if(dwErrCode == ERROR_SUCCESS)
{
// Get the CKey page header and the first page
PFILE_CKEY_PAGE pPageHeader = (PFILE_CKEY_PAGE)(EncodingFile.pbData + sizeof(FILE_ENCODING_HEADER) + EnHeader.ESpecBlockSize);
LPBYTE pbEncodingEnd = EncodingFile.pbData + EncodingFile.cbData;
LPBYTE pbCKeyPage = (LPBYTE)(pPageHeader + EnHeader.CKeyPageCount);
// Go through all CKey pages and verify them
for(DWORD i = 0; i < EnHeader.CKeyPageCount; i++)
{
// Check if there is enough space in the buffer
if((pbCKeyPage + EnHeader.CKeyPageSize) > pbEncodingEnd)
{
dwErrCode = ERROR_FILE_CORRUPT;
break;
}
// Check the hash of the entire segment
// Note that verifying takes considerable time of the storage loading
// if(!VerifyDataBlockHash(pbCKeyPage, EnHeader.CKeyPageSize, pEncodingSegment->SegmentHash))
// {
// dwErrCode = ERROR_FILE_CORRUPT;
// break;
// }
// Check if the CKey matches with the expected first value
if(memcmp(((PFILE_CKEY_ENTRY)pbCKeyPage)->CKey, pPageHeader[i].FirstKey, MD5_HASH_SIZE))
{
dwErrCode = ERROR_FILE_CORRUPT;
break;
}
// Load the entire page of CKey entries.
// This operation will never fail, because all memory is already pre-allocated
dwErrCode = LoadEncodingCKeyPage(hs, EnHeader, pbCKeyPage, pbCKeyPage + EnHeader.CKeyPageSize);
if(dwErrCode != ERROR_SUCCESS)
break;
// Move to the next CKey page
pbCKeyPage += EnHeader.CKeyPageSize;
}
}
// All CKey->EKey entries from the text build files need to be copied to the CKey array
if(dwErrCode == ERROR_SUCCESS)
{
dwErrCode = CopyBuildFileItemsToCKeyArray(hs);
}
}
else
{
dwErrCode = GetCascError();
}
return dwErrCode;
}
size_t GetTagBitmapLength(LPBYTE pbFilePtr, LPBYTE pbFileEnd, DWORD EntryCount)
{
size_t nBitmapLength;
nBitmapLength = (EntryCount / 8) + ((EntryCount & 0x07) ? 1 : 0);
if((pbFilePtr + nBitmapLength) > pbFileEnd)
nBitmapLength = (pbFileEnd - pbFilePtr);
return nBitmapLength;
}
int CaptureDownloadHeader(CASC_DOWNLOAD_HEADER & DlHeader, LPBYTE pbFileData, size_t cbFileData)
{
PFILE_DOWNLOAD_HEADER pFileHeader = (PFILE_DOWNLOAD_HEADER)pbFileData;
// Check the signature ('DL') and version
if(cbFileData < sizeof(FILE_DOWNLOAD_HEADER) || pFileHeader->Magic != FILE_MAGIC_DOWNLOAD || pFileHeader->Version > 3)
return ERROR_BAD_FORMAT;
// Note that we don't support CKey sizes greater than 0x10 in the DOWNLOAD file
if(pFileHeader->EKeyLength > MD5_HASH_SIZE)
return ERROR_BAD_FORMAT;
// Capture the header version 1
memset(&DlHeader, 0, sizeof(CASC_DOWNLOAD_HEADER));
DlHeader.Magic = pFileHeader->Magic;
DlHeader.Version = pFileHeader->Version;
DlHeader.EKeyLength = pFileHeader->EKeyLength;
DlHeader.EntryHasChecksum = pFileHeader->EntryHasChecksum;
DlHeader.EntryCount = ConvertBytesToInteger_4(pFileHeader->EntryCount);
DlHeader.TagCount = ConvertBytesToInteger_2(pFileHeader->TagCount);
DlHeader.HeaderLength = FIELD_OFFSET(FILE_DOWNLOAD_HEADER, FlagByteSize);
DlHeader.EntryLength = DlHeader.EKeyLength + 5 + 1 + (DlHeader.EntryHasChecksum ? 4 : 0);
// Capture header version 2
if(pFileHeader->Version >= 2)
{
DlHeader.FlagByteSize = pFileHeader->FlagByteSize;
DlHeader.HeaderLength = FIELD_OFFSET(FILE_DOWNLOAD_HEADER, BasePriority);
DlHeader.EntryLength += DlHeader.FlagByteSize;
// Capture header version 3
if(pFileHeader->Version >= 3)
{
DlHeader.BasePriority = pFileHeader->BasePriority;
DlHeader.HeaderLength = sizeof(FILE_DOWNLOAD_HEADER);
}
}
return ERROR_SUCCESS;
}
int CaptureDownloadEntry(CASC_DOWNLOAD_HEADER & DlHeader, CASC_DOWNLOAD_ENTRY & DlEntry, LPBYTE pbFilePtr, LPBYTE pbFileEnd)
{
// Check the range
if((pbFilePtr + DlHeader.EntryLength) >= pbFileEnd)
return ERROR_BAD_FORMAT;
memset(&DlEntry, 0, sizeof(CASC_DOWNLOAD_ENTRY));
// Copy the EKey
memcpy(DlEntry.EKey, pbFilePtr, DlHeader.EKeyLength);
pbFilePtr += DlHeader.EKeyLength;
// Convert the file size
DlEntry.EncodedSize = ConvertBytesToInteger_5(pbFilePtr);
pbFilePtr += 5;
// Copy the file priority
DlEntry.Priority = pbFilePtr[0];
pbFilePtr++;
// Copy the checksum
if(DlHeader.EntryHasChecksum)
{
DlEntry.Checksum = ConvertBytesToInteger_4(pbFilePtr);
pbFilePtr += 4;
}
// Copy the flags
DlEntry.Flags = ConvertBytesToInteger_X(pbFilePtr, DlHeader.FlagByteSize);
return ERROR_SUCCESS;
}
int CaptureDownloadTag(CASC_DOWNLOAD_HEADER & DlHeader, CASC_TAG_ENTRY1 & DlTag, LPBYTE pbFilePtr, LPBYTE pbFileEnd)
{
LPBYTE pbSaveFilePtr = pbFilePtr;
// Prepare the tag structure
memset(&DlTag, 0, sizeof(CASC_TAG_ENTRY1));
DlTag.szTagName = (const char *)pbFilePtr;
// Skip the tag string
while(pbFilePtr < pbFileEnd && pbFilePtr[0] != 0)
pbFilePtr++;
if(pbFilePtr >= pbFileEnd)
return ERROR_BAD_FORMAT;
// Save the length of the tag name
DlTag.NameLength = (pbFilePtr - pbSaveFilePtr);
pbFilePtr++;
// Get the tag value
if((pbFilePtr + sizeof(DWORD)) > pbFileEnd)
return ERROR_BAD_FORMAT;
DlTag.TagValue = ConvertBytesToInteger_2(pbFilePtr);
pbFilePtr += 2;
// Get the bitmap
DlTag.Bitmap = pbFilePtr;
// Get the bitmap length.
// If the bitmap is last in the list and it's shorter than declared, we make it shorter
DlTag.BitmapLength = GetTagBitmapLength(pbFilePtr, pbFileEnd, DlHeader.EntryCount);
// Get the entry length
DlTag.TagLength = (pbFilePtr - pbSaveFilePtr) + DlTag.BitmapLength;
return ERROR_SUCCESS;
}
static int LoadDownloadManifest(TCascStorage * hs, CASC_DOWNLOAD_HEADER & DlHeader, LPBYTE pbFileData, LPBYTE pbFileEnd)
{
PCASC_TAG_ENTRY1 TagArray = NULL;
LPBYTE pbEntries = pbFileData + DlHeader.HeaderLength;
LPBYTE pbEntry = pbEntries;
LPBYTE pbTags = pbEntries + DlHeader.EntryLength * DlHeader.EntryCount;
LPBYTE pbTag = pbTags;
size_t nMaxNameLength = 0;
size_t nTagEntryLengh = 0;
DWORD dwErrCode = ERROR_SUCCESS;
// Does the storage support tags?
if(DlHeader.TagCount != 0)
{
// Remember that we support tags
hs->dwFeatures |= CASC_FEATURE_TAGS;
// Allocate space for the tag array
TagArray = CASC_ALLOC<CASC_TAG_ENTRY1>(DlHeader.TagCount);
if(TagArray != NULL)
{
// Get the longest tag name
for(DWORD i = 0; i < DlHeader.TagCount; i++)
{
if(CaptureDownloadTag(DlHeader, TagArray[i], pbTag, pbFileEnd) == ERROR_SUCCESS)
nMaxNameLength = CASCLIB_MAX(nMaxNameLength, TagArray[i].NameLength);
pbTag = pbTag + TagArray[i].TagLength;
}
// Determine the tag entry length
nTagEntryLengh = FIELD_OFFSET(CASC_TAG_ENTRY2, szTagName) + nMaxNameLength;
nTagEntryLengh = ALIGN_TO_SIZE(nTagEntryLengh, 8);
// Load the tags into array in the storage structure
dwErrCode = hs->TagsArray.Create(nTagEntryLengh, DlHeader.TagCount);
if(dwErrCode == ERROR_SUCCESS)
{
// Convert the array of CASC_DOWNLOAD_TAG1 to array of CASC_DOWNLOAD_TAG2
for(DWORD i = 0; i < DlHeader.TagCount; i++)
{
PCASC_TAG_ENTRY1 pSourceTag = &TagArray[i];
PCASC_TAG_ENTRY2 pTargetTag;
// Insert the tag to the array
pTargetTag = (PCASC_TAG_ENTRY2)hs->TagsArray.Insert(1);
if(pTargetTag == NULL)
{
dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
break;
}
// Copy the tag structure
memset(pTargetTag, 0, nTagEntryLengh);
memcpy(pTargetTag->szTagName, pSourceTag->szTagName, pSourceTag->NameLength);
pTargetTag->NameLength = pSourceTag->NameLength;
pTargetTag->TagValue = pSourceTag->TagValue;
}
}
}
else
{
dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
}
}
// Now parse all entries. For each entry, mark the corresponding tag bit in the EKey table
for(DWORD i = 0; i < DlHeader.EntryCount; i++)
{
CASC_DOWNLOAD_ENTRY DlEntry;
PCASC_CKEY_ENTRY pCKeyEntry;
ULONGLONG TagBit = 1;
size_t BitMaskOffset = (i / 8);
size_t TagItemCount = hs->TagsArray.ItemCount();
BYTE BitMaskBit = 0x80 >> (i % 8);
// Capture the download entry
if(CaptureDownloadEntry(DlHeader, DlEntry, pbEntry, pbFileEnd) != ERROR_SUCCESS)
break;
// COD4: zone/base.xpak
//BREAK_ON_XKEY3(DlEntry.EKey, 0xa5, 0x00, 0x16);
// Insert the entry to the central CKey table
if((pCKeyEntry = InsertCKeyEntry(hs, DlEntry)) != NULL)
{
if(TagArray != NULL)
{
// Supply the tag bits
for(size_t j = 0; j < TagItemCount; j++)
{
// Set the bit in the entry, if the tag for it is present
if((BitMaskOffset < TagArray[j].BitmapLength) && (TagArray[j].Bitmap[BitMaskOffset] & BitMaskBit))
pCKeyEntry->TagBitMask |= TagBit;
// Move to the next bit
TagBit <<= 1;
}
}
}
// Move to the next entry
pbEntry += DlHeader.EntryLength;
}
// Free the tag array, if any
CASC_FREE(TagArray);
// Remember the total file count
hs->TotalFiles = hs->CKeyArray.ItemCount();
return dwErrCode;
}
static int LoadDownloadManifest(TCascStorage * hs)
{
PCASC_CKEY_ENTRY pCKeyEntry = FindCKeyEntry_CKey(hs, hs->DownloadCKey.CKey);
CASC_BLOB DownloadFile;
DWORD dwErrCode = ERROR_SUCCESS;
// Inform the user about what we are doing
if(InvokeProgressCallback(hs, CascProgressLoadingManifest, "DOWNLOAD", 0, 0))
return ERROR_CANCELLED;
// Load the entire DOWNLOAD file to memory
dwErrCode = LoadInternalFileToMemory(hs, pCKeyEntry, DownloadFile);
if(dwErrCode == ERROR_SUCCESS && DownloadFile.cbData != 0)
{
CASC_DOWNLOAD_HEADER DlHeader;
// Capture the header of the DOWNLOAD file
dwErrCode = CaptureDownloadHeader(DlHeader, DownloadFile.pbData, DownloadFile.cbData);
if(dwErrCode == ERROR_SUCCESS)
{
// Parse the entire download manifest
dwErrCode = LoadDownloadManifest(hs, DlHeader, DownloadFile.pbData, DownloadFile.pbData + DownloadFile.cbData);
}
}
// If the DOWNLOAD manifest is not present, we won't abort the downloading process.
return dwErrCode;
}
//-----------------------------------------------------------------------------
// INSTALL manifest. This is a replacement for ROOT, if loading ROOT fails
// https://wowdev.wiki/TACT#Install_manifest
static int LoadInstallManifest(TCascStorage * hs)
{
PCASC_CKEY_ENTRY pCKeyEntry = FindCKeyEntry_CKey(hs, hs->InstallCKey.CKey);
CASC_BLOB InstallFile;
DWORD dwErrCode = ERROR_SUCCESS;
// Inform the user about what we are doing
if(InvokeProgressCallback(hs, CascProgressLoadingManifest, "INSTALL", 0, 0))
return ERROR_CANCELLED;
// Load the entire DOWNLOAD file to memory
dwErrCode = LoadInternalFileToMemory(hs, pCKeyEntry, InstallFile);
if(dwErrCode == ERROR_SUCCESS && InstallFile.cbData != 0)
{
dwErrCode = RootHandler_CreateInstall(hs, InstallFile);
}
else
{
dwErrCode = GetCascError();
}
return dwErrCode;
}
static bool InsertWellKnownFile(TCascStorage * hs, const char * szFileName, CASC_CKEY_ENTRY & FakeCKeyEntry, DWORD dwFlags = 0)
{
PCASC_CKEY_ENTRY pCKeyEntry = NULL;
// We need to find the CKey entry in the central array
if(FakeCKeyEntry.Flags & CASC_CE_HAS_CKEY)
{
// Did we find anything?
pCKeyEntry = FindCKeyEntry_CKey(hs, FakeCKeyEntry.CKey);
if(pCKeyEntry != NULL)
{
// Insert the key to the root handler. Note that the file can already be referenced
// ("index" vs "vfs-root" in Warcraft III storages)
hs->pRootHandler->Insert(szFileName, pCKeyEntry);
// Copy some flags
pCKeyEntry->Flags |= (dwFlags | CASC_CE_IN_BUILD);
return true;
}
}
// Special case: the PATCH file is usually not in any indices.
// It's also never locally available
if((dwFlags & CASC_CE_FILE_PATCH) && (hs->dwFeatures & CASC_FEATURE_ONLINE))
{
// Get or insert the PATCH entry
pCKeyEntry = InsertCKeyEntry(hs, FakeCKeyEntry);
if(pCKeyEntry != NULL)
{
hs->pRootHandler->Insert(szFileName, pCKeyEntry);
pCKeyEntry->Flags |= (dwFlags | CASC_CE_IN_BUILD);
return true;
}
}
return false;
}
static int LoadBuildManifest(TCascStorage * hs, DWORD dwLocaleMask)
{
PCASC_CKEY_ENTRY pCKeyEntry = &hs->RootFile;
TRootHandler * pOldRootHandler = NULL;
CASC_BLOB RootFile;
PDWORD FileSignature;
DWORD dwErrCode = ERROR_BAD_FORMAT;
// Sanity checks
assert(hs->CKeyMap.IsInitialized() == true);
assert(hs->pRootHandler == NULL);
// Inform the user about what we are doing
if(InvokeProgressCallback(hs, CascProgressLoadingManifest, "ROOT", 0, 0))
return ERROR_CANCELLED;
// Locale: The default parameter is 0 - in that case, we load all locales
dwLocaleMask = (dwLocaleMask != 0) ? dwLocaleMask : 0xFFFFFFFF;
// Prioritize the VFS root over legacy ROOT file, unless it's WoW
if(hs->VfsRoot.ContentSize != CASC_INVALID_SIZE)
pCKeyEntry = &hs->VfsRoot;
__LoadRootFile:
// Load the entire ROOT file to memory
pCKeyEntry = FindCKeyEntry_CKey(hs, pCKeyEntry->CKey);
dwErrCode = LoadInternalFileToMemory(hs, pCKeyEntry, RootFile);
if(dwErrCode == ERROR_SUCCESS)
{
// Ignore ROOT files that contain just a MD5 hash
if(RootFile.cbData > MD5_STRING_SIZE)
{
// Check the type of the ROOT file
FileSignature = (PDWORD)(RootFile.pbData);
switch(FileSignature[0])
{
case CASC_MNDX_ROOT_SIGNATURE:
dwErrCode = RootHandler_CreateMNDX(hs, RootFile);
break;
case CASC_DIABLO3_ROOT_SIGNATURE:
dwErrCode = RootHandler_CreateDiablo3(hs, RootFile);
break;
case CASC_TVFS_ROOT_SIGNATURE:
dwErrCode = RootHandler_CreateTVFS(hs, RootFile);
break;
case CASC_WOW_ROOT_SIGNATURE:
dwErrCode = RootHandler_CreateWoW(hs, RootFile, dwLocaleMask);
break;
default:
//
// Each of these handler creators must verify their format first.
// If the format was not recognized, they need to return ERROR_BAD_FORMAT
//
dwErrCode = RootHandler_CreateOverwatch(hs, RootFile);
if(dwErrCode == ERROR_BAD_FORMAT)
{
dwErrCode = RootHandler_CreateStarcraft1(hs, RootFile);
if(dwErrCode == ERROR_BAD_FORMAT)
{
dwErrCode = RootHandler_CreateWoW(hs, RootFile, dwLocaleMask);
}
}
break;
}
}
else
{
dwErrCode = ERROR_BAD_FORMAT;
}
}
else
{
dwErrCode = GetCascError();
}
// Handle reparsing of the root file
if(dwErrCode == ERROR_REPARSE_ROOT && pCKeyEntry != &hs->RootFile)
{
if(InvokeProgressCallback(hs, CascProgressLoadingManifest, "ROOT (reparsed)", 0, 0))
return ERROR_CANCELLED;
// Replace the root handler
pOldRootHandler = hs->pRootHandler;
hs->pRootHandler = NULL;
// Replace the CKey entry for the ROOT file
pCKeyEntry = &hs->RootFile;
goto __LoadRootFile;
}
// If we reparsed the ROOT file and we have the old one, we need to copy all items to the new one
if(hs->pRootHandler && pOldRootHandler)
{
hs->pRootHandler->Copy(pOldRootHandler);
delete pOldRootHandler;
}
return dwErrCode;
}
static DWORD GetStorageTotalFileCount(TCascStorage * hs)
{
PCASC_CKEY_ENTRY pCKeyEntry;
size_t nItemCount = hs->CKeyArray.ItemCount();
DWORD TotalFileCount = 0;
for(size_t i = 0; i < nItemCount; i++)
{
if((pCKeyEntry = (PCASC_CKEY_ENTRY)hs->CKeyArray.ItemAt(i)) != NULL)
{
if(pCKeyEntry->IsFile())
{
// If there is zero or one file name reference, we count the item as one file.
// If there is more than 1 name reference, we count the file as many times as number of references
DWORD RefCount = (pCKeyEntry->RefCount) ? pCKeyEntry->RefCount : 1;
// Add the number of references to the total file count
TotalFileCount += RefCount;
}
}
}
return TotalFileCount;
}
static bool GetStorageProduct(TCascStorage * hs, void * pvStorageInfo, size_t cbStorageInfo, size_t * pcbLengthNeeded)
{
PCASC_STORAGE_PRODUCT pProductInfo;
// Verify whether we have enough space in the buffer
pProductInfo = (PCASC_STORAGE_PRODUCT)ProbeOutputBuffer(pvStorageInfo, cbStorageInfo, sizeof(CASC_STORAGE_PRODUCT), pcbLengthNeeded);
if(pProductInfo != NULL)
{
// Clear the entire structure
memset(pProductInfo, 0, sizeof(CASC_STORAGE_PRODUCT));
// Copy the product code name and build number
if(hs->szCodeName != NULL)
CascStrCopy(pProductInfo->szCodeName, _countof(pProductInfo->szCodeName), hs->szCodeName);
pProductInfo->BuildNumber = hs->dwBuildNumber;
}
return (pProductInfo != NULL);
}
static bool GetStorageTags(TCascStorage * hs, void * pvStorageInfo, size_t cbStorageInfo, size_t * pcbLengthNeeded)
{
PCASC_STORAGE_TAGS pTags;
PCASC_TAG_ENTRY2 pTag;
char * szNameBuffer;
size_t cbMinLength;
// Does the storage support tags?
if(hs->TagsArray.IsInitialized() == false)
{
SetCascError(ERROR_NOT_SUPPORTED);
return false;
}
// Calculate the length of the tags
cbMinLength = FIELD_OFFSET(CASC_STORAGE_TAGS, Tags) + hs->TagsArray.ItemCount() * sizeof(CASC_STORAGE_TAG);
szNameBuffer = (char *)pvStorageInfo + cbMinLength;
// Also include the tag length
for(size_t i = 0; i < hs->TagsArray.ItemCount(); i++)
{
pTag = (PCASC_TAG_ENTRY2)hs->TagsArray.ItemAt(i);
cbMinLength = cbMinLength + pTag->NameLength + 1;
}
// Verify whether we have enough space in the buffer
pTags = (PCASC_STORAGE_TAGS)ProbeOutputBuffer(pvStorageInfo, cbStorageInfo, cbMinLength, pcbLengthNeeded);
if(pTags != NULL)
{
// Fill the output structure
pTags->TagCount = hs->TagsArray.ItemCount();
pTags->Reserved = 0;
// Copy the tags
for(size_t i = 0; i < hs->TagsArray.ItemCount(); i++)
{
// Get the source tag
pTag = (PCASC_TAG_ENTRY2)hs->TagsArray.ItemAt(i);
// Fill the target tag
pTags->Tags[i].szTagName = szNameBuffer;
pTags->Tags[i].TagNameLength = (DWORD)pTag->NameLength;
pTags->Tags[i].TagValue = pTag->TagValue;
// Copy the tag name
memcpy(szNameBuffer, pTag->szTagName, pTag->NameLength);
szNameBuffer[pTag->NameLength] = 0;
szNameBuffer = szNameBuffer + pTag->NameLength + 1;
}
}
return (pTags != NULL);
}
static bool GetStoragePathProduct(TCascStorage * hs, void * pvStorageInfo, size_t cbStorageInfo, size_t * pcbLengthNeeded)
{
LPTSTR szBuffer = (LPTSTR)pvStorageInfo;
size_t nMaxChars = cbStorageInfo / sizeof(TCHAR);
size_t nLength;
// Calculate the length needed
nLength = _tcslen(hs->szRootPath);
if(hs->szCodeName != NULL)
nLength = nLength + 1 + _tcslen(hs->szCodeName);
if(hs->szRegion != NULL)
nLength = nLength + 1 + strlen(hs->szRegion);
nLength++;
// Verify whether we have enough space in the buffer
szBuffer = (LPTSTR)ProbeOutputBuffer(pvStorageInfo, cbStorageInfo, (nLength * sizeof(TCHAR)), pcbLengthNeeded);
if(szBuffer != NULL)
{
LPTSTR szBufferEnd = szBuffer + nMaxChars;
// Copy the storage path
CascStrCopy(szBuffer, (szBufferEnd - szBuffer), hs->szRootPath);
szBuffer += _tcslen(hs->szRootPath);
// Append the product code name, if any
if(hs->szCodeName != NULL)
{
*szBuffer++ = _T(CASC_PARAM_SEPARATOR);
CascStrCopy(szBuffer, (szBufferEnd - szBuffer), hs->szCodeName);
szBuffer += _tcslen(hs->szCodeName);
}
// Append the product region, if any
if(hs->szRegion != NULL)
{
*szBuffer++ = _T(CASC_PARAM_SEPARATOR);
CascStrCopy(szBuffer, (szBufferEnd - szBuffer), hs->szRegion);
}
}
return (szBuffer != NULL);
}
static DWORD LoadCascStorage(TCascStorage * hs, PCASC_OPEN_STORAGE_ARGS pArgs, LPCTSTR szMainFile, CBLD_TYPE BuildFileType, DWORD dwFeatures)
{
LPCTSTR szCdnHostUrl = NULL;
LPCTSTR szCodeName = NULL;
LPCTSTR szRegion = NULL;
LPCTSTR szBuildKey = NULL;
DWORD dwLocaleMask = 0;
DWORD dwErrCode = ERROR_SUCCESS;
// Pass the argument array to the storage
hs->pArgs = pArgs;
// Extract optional arguments
ExtractVersionedArgument(pArgs, FIELD_OFFSET(CASC_OPEN_STORAGE_ARGS, dwLocaleMask), &dwLocaleMask);
// Extract the CDN host URL
if(ExtractVersionedArgument(pArgs, FIELD_OFFSET(CASC_OPEN_STORAGE_ARGS, szCdnHostUrl), &szCdnHostUrl) && szCdnHostUrl != NULL)
hs->szCdnHostUrl = CascNewStr(szCdnHostUrl);
// Extract the product code name
if(ExtractVersionedArgument(pArgs, FIELD_OFFSET(CASC_OPEN_STORAGE_ARGS, szCodeName), &szCodeName) && szCodeName != NULL)
hs->szCodeName = CascNewStr(szCodeName);
// Extract the region (optional)
if(ExtractVersionedArgument(pArgs, FIELD_OFFSET(CASC_OPEN_STORAGE_ARGS, szRegion), &szRegion) && szRegion != NULL)
hs->szRegion = CascNewStrT2A(szRegion);
// Extract the build key (optional)
if(ExtractVersionedArgument(pArgs, FIELD_OFFSET(CASC_OPEN_STORAGE_ARGS, szBuildKey), &szBuildKey) && szBuildKey != NULL)
hs->szBuildKey = CascNewStrT2A(szBuildKey);
// Merge features
hs->dwFeatures |= (dwFeatures & (CASC_FEATURE_DATA_ARCHIVES | CASC_FEATURE_DATA_FILES | CASC_FEATURE_ONLINE));
hs->dwFeatures |= (pArgs->dwFlags & CASC_FEATURE_FORCE_DOWNLOAD);
hs->dwFeatures |= (BuildFileType == CascVersions) ? CASC_FEATURE_ONLINE : 0;
hs->BuildFileType = BuildFileType;
// Copy the name of the build file
hs->szMainFile = CascNewStr(szMainFile);
// Construct the root path from the name of the build file
CASC_PATH<TCHAR> RootPath(szMainFile, NULL);
hs->szRootPath = RootPath.New(true);
// If either of the root path or build file is known, it's an error
if(hs->szRootPath == NULL || hs->szMainFile == NULL)
{
dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
}
// Initialize variables for local CASC storages
if(dwErrCode == ERROR_SUCCESS)
{
// For local (game) storages, we need the data and indices subdirectory
if(hs->dwFeatures & CASC_FEATURE_DATA_ARCHIVES)
{
if(CheckArchiveFilesDirectories(hs) != ERROR_SUCCESS)
hs->dwFeatures &= ~CASC_FEATURE_DATA_ARCHIVES;
}
// For data files storage, we need that folder
if(hs->dwFeatures & CASC_FEATURE_DATA_FILES)
{
if(CheckDataFilesDirectory(hs) != ERROR_SUCCESS)
hs->dwFeatures &= ~CASC_FEATURE_DATA_FILES;
}
// Enable caching of the sockets. This will add references
// to all existing and all future sockets
if(hs->dwFeatures & CASC_FEATURE_ONLINE)
sockets_set_caching(true);
// Now, load the main storage file (".build.info", ".build.db" or "versions")
dwErrCode = LoadMainFile(hs);
}
// Proceed with loading the CDN config file
if(dwErrCode == ERROR_SUCCESS)
{
dwErrCode = LoadCdnConfigFile(hs);
if(dwErrCode != ERROR_SUCCESS && (hs->dwFeatures & CASC_FEATURE_ONLINE) == 0)
dwErrCode = ERROR_SUCCESS;
}
// Proceed with loading the CDN build file
if(dwErrCode == ERROR_SUCCESS)
{
dwErrCode = LoadCdnBuildFile(hs);
}
// Make sure we have a build number. If we don't, we assign a build number
// that is derived from the first beta TVFS build number
if(hs->dwBuildNumber == 0)
{
hs->dwBuildNumber = 21742 + hs->InstallCKey.ContentSize;
}
// Make sure we have a code name. Not a case of WoW build 22267
if(hs->szCodeName == NULL && hs->dwBuildNumber == 22267)
{
hs->SetProductCodeName("wow", 3);
}
// Create the array of CKey entries. Each entry represents a file in the storage
if(dwErrCode == ERROR_SUCCESS)
{
dwErrCode = InitCKeyArray(hs);
}
// Pre-load the local index files
if(dwErrCode == ERROR_SUCCESS)
{
dwErrCode = LoadIndexFiles(hs);
}
// Load the ENCODING manifest
if(dwErrCode == ERROR_SUCCESS)
{
dwErrCode = LoadEncodingManifest(hs);
}
// We need to load the DOWNLOAD manifest
if(dwErrCode == ERROR_SUCCESS)
{
dwErrCode = LoadDownloadManifest(hs);
}
// Load the build manifest ("ROOT" file)
if(dwErrCode == ERROR_SUCCESS)
{
// For WoW storages, multiple files are present in the storage (same name, same file data ID, different locale).
// Failing to select storage on them will lead to the first-in-order file in the list being loaded.
// Example: WoW build 32144, file: DBFilesClient\Achievement.db2, file data ID: 1260179
// Locales: koKR frFR deDE zhCN esES zhTW enUS&enGB esMX ruRU itIT ptBT&ptPT (in order of appearance in the build manifest)
dwLocaleMask = (dwLocaleMask != 0) ? dwLocaleMask : hs->dwDefaultLocale;
// Continue loading the manifest
dwErrCode = LoadBuildManifest(hs, dwLocaleMask);
// If we fail to load the ROOT file, we take the file names from the INSTALL manifest
// Beware on low memory condition - in that case, we cannot guarantee a consistent state of the root file
if(dwErrCode != ERROR_SUCCESS && dwErrCode != ERROR_NOT_ENOUGH_MEMORY)
{
dwErrCode = LoadInstallManifest(hs);
}
}
// Insert entries for files with well-known names. Their CKeys are in the BUILD file
// See https://wowdev.wiki/TACT#Encoding_table for their list
if(dwErrCode == ERROR_SUCCESS)
{
InsertWellKnownFile(hs, "ENCODING", hs->EncodingCKey);
InsertWellKnownFile(hs, "DOWNLOAD", hs->DownloadCKey);
InsertWellKnownFile(hs, "INSTALL", hs->InstallCKey);
InsertWellKnownFile(hs, "PATCH", hs->PatchFile, CASC_CE_FILE_PATCH);
InsertWellKnownFile(hs, "ROOT", hs->RootFile);
InsertWellKnownFile(hs, "SIZE", hs->SizeFile);
// Also reset the total file count. CascGetStorageInfo will update it on next call
hs->TotalFiles = 0;
}
// Load the encryption keys
if(dwErrCode == ERROR_SUCCESS)
{
dwErrCode = CascLoadEncryptionKeys(hs);
}
// Cleanup and exit
FreeIndexFiles(hs);
hs->pArgs = NULL;
return dwErrCode;
}
// Check for URL pattern. Note that the string may be terminated by CASC_PARAM_SEPARATOR instead of '\0'
static bool IsUrl(LPCTSTR szString)
{
while(szString[0] != 0 && szString[0] != CASC_PARAM_SEPARATOR)
{
// Check for "://"
if(!_tcsncmp(szString, _T("://"), 3))
return true;
// Dot or slash both indicate an URL
if(szString[0] == '.' || szString[0] == '/')
return true;
szString++;
}
return false;
}
static LPTSTR GetNextParam(LPTSTR szParamsPtr, bool bMustBeUrl = false)
{
LPTSTR szSeparator = NULL;
// The 'szParamsPtr' must be valid
if(szParamsPtr != NULL)
{
// Find the separator ("*") or end of string
if((szSeparator = _tcschr(szParamsPtr, _T(CASC_PARAM_SEPARATOR))) != NULL)
{
// Check for URL pattern, if needed
if(bMustBeUrl && IsUrl(szSeparator + 1) == false)
return NULL;
// Put the EOS there
*szSeparator++ = 0;
}
}
return szSeparator;
}
static DWORD ParseOpenParams(LPTSTR szParams, PCASC_OPEN_STORAGE_ARGS pArgs)
{
LPTSTR szParamsPtr = szParams;
LPTSTR szParamsTmp;
//
// Format of the params:
//
// Local: local_path*code_name ("C:\\Games\\World of Warcraft*wowt")
// Online: local_cache_path[*cdn_url]*code_name*region" ("C:\\Cache*wowt*us)
//
// If the caller supplied the local_path/local_cache_path
// both in szParams and pArgs, it's a conflict
if(pArgs->szLocalPath && pArgs->szLocalPath[0])
return ERROR_INVALID_PARAMETER;
pArgs->szLocalPath = szParams;
// Extract the optional CDN path. If present, then we put it
// into CASC_OPEN_STORAGE_ARGS::szCdnHostUrl
if((szParamsTmp = GetNextParam(szParamsPtr, true)) != NULL)
{
if(pArgs->szCdnHostUrl && pArgs->szCdnHostUrl[0])
return ERROR_INVALID_PARAMETER;
pArgs->szCdnHostUrl = szParamsTmp;
szParamsPtr = szParamsTmp;
}
// The next must be the code name of the product
if((szParamsPtr = GetNextParam(szParamsPtr)) != NULL)
{
if(pArgs->szCodeName && pArgs->szCodeName[0])
return ERROR_INVALID_PARAMETER;
pArgs->szCodeName = szParamsPtr;
}
// There could be region appended at the end
if((szParamsPtr = GetNextParam(szParamsPtr)) != NULL)
{
if(pArgs->szRegion && pArgs->szRegion[0])
return ERROR_INVALID_PARAMETER;
pArgs->szRegion = szParamsPtr;
}
// There could be region appended at the end
if((szParamsPtr = GetNextParam(szParamsPtr)) != NULL)
{
if(pArgs->szBuildKey && pArgs->szBuildKey[0])
return ERROR_INVALID_PARAMETER;
pArgs->szBuildKey = szParamsPtr;
}
return ERROR_SUCCESS;
}
//-----------------------------------------------------------------------------
// Public functions
bool WINAPI CascOpenStorageEx(LPCTSTR szParams, PCASC_OPEN_STORAGE_ARGS pArgs, bool bOnlineStorage, HANDLE * phStorage)
{
CASC_OPEN_STORAGE_ARGS LocalArgs = {sizeof(CASC_OPEN_STORAGE_ARGS)};
TCascStorage * hs = NULL;
LPTSTR szParamsCopy = NULL;
DWORD dwErrCode = ERROR_SUCCESS;
// Supply the local args if not given by the caller
pArgs = (pArgs != NULL) ? pArgs : &LocalArgs;
//
// Parse the parameter string and put the parts into CASC_OPEN_STORAGE_ARGS
//
// Note that the parameter string is optional - is it possible
// to enter all params purely in CASC_OPEN_STORAGE_ARGS structure.
//
if(szParams != NULL)
{
// Make a copy of the parameters so we can tamper with them
if((szParamsCopy = CascNewStr(szParams)) == NULL)
{
SetCascError(ERROR_NOT_ENOUGH_MEMORY);
return false;
}
// Parse the parameter string and put the corresponding parts
// into the CASC_OPEN_STORAGE_ARGS structure.
dwErrCode = ParseOpenParams(szParamsCopy, pArgs);
}
// Verify the minimum arguments
if(dwErrCode == ERROR_SUCCESS)
{
if(pArgs->szLocalPath == NULL || pArgs->szLocalPath[0] == 0)
{
dwErrCode = ERROR_INVALID_PARAMETER;
}
}
// Now we need to get the CASC main file, which is either
// [*] .build.info - for current local storages
// [*] .build.db - for older local storages
// [*] versions - for cached online storages
// If there is none of these and `bOnlineStorage` is specified,
// CascLib will download it, as long as the product code was specified
if(dwErrCode == ERROR_SUCCESS)
{
if((hs = new TCascStorage()) != NULL)
{
CASC_BUILD_FILE BuildFile = {NULL};
// Check for one of the supported main files (.build.info, .build.db, versions)
if((dwErrCode = CheckCascBuildFileExact(BuildFile, pArgs->szLocalPath)) == ERROR_SUCCESS)
{
dwErrCode = LoadCascStorage(hs, pArgs, BuildFile.szFullPath, BuildFile.BuildFileType, CASC_FEATURE_DATA_ARCHIVES | CASC_FEATURE_DATA_FILES);
}
// Search the folder and upper folders for the build file
else if((dwErrCode = CheckCascBuildFileDirs(BuildFile, pArgs->szLocalPath)) == ERROR_SUCCESS)
{
dwErrCode = LoadCascStorage(hs, pArgs, BuildFile.szFullPath, BuildFile.BuildFileType, CASC_FEATURE_DATA_ARCHIVES | CASC_FEATURE_DATA_FILES);
}
// If the caller requested an online storage, we must have the code name
else if((dwErrCode = CheckOnlineStorage(pArgs, BuildFile, bOnlineStorage)) == ERROR_SUCCESS)
{
dwErrCode = LoadCascStorage(hs, pArgs, BuildFile.szFullPath, BuildFile.BuildFileType, CASC_FEATURE_DATA_FILES);
}
}
}
// Delete the storage on error
if(dwErrCode != ERROR_SUCCESS)
hs = hs->Release();
CASC_FREE(szParamsCopy);
// Give the output parameter to the caller
if(phStorage != NULL)
phStorage[0] = (HANDLE)hs;
if(dwErrCode != ERROR_SUCCESS)
SetCascError(dwErrCode);
return (dwErrCode == ERROR_SUCCESS);
}
//
// Opens a local CASC storage
//
// szParams: "local_path:code_name", like "C:\\Games\\World of Warcraft:wowt"
//
// local_path Local folder, where the online file will be cached.
//
// code_name: Product code name, e.g. "agent" for Battle.net Agent.
// More info: https://wowdev.wiki/TACT#Products
//
bool WINAPI CascOpenStorage(LPCTSTR szParams, DWORD dwLocaleMask, HANDLE * phStorage)
{
CASC_OPEN_STORAGE_ARGS OpenArgs = {sizeof(CASC_OPEN_STORAGE_ARGS)};
OpenArgs.dwLocaleMask = dwLocaleMask;
return CascOpenStorageEx(szParams, &OpenArgs, false, phStorage);
}
//
// Opens an online CDN storage
//
// szParams: "local_cache_path[:cdn_url]:code_name:region", e.g. "C:\\Cache:wowt:us"
//
// local_cache_path Local folder, where the online file will be cached.
// cdn_url URL of the custom CDN server. Can also contain port.
// This parameter is optional. Example: http://eu.custom-wow-cdn.com:8000
// code_name Product code name, e.g. "agent" for Battle.net Agent.
// More info: https://wowdev.wiki/TACT#Products
// region The region (or subvariant) of the product.
// Corresponds to the first column of the "versions" file.
//
bool WINAPI CascOpenOnlineStorage(LPCTSTR szParams, DWORD dwLocaleMask, HANDLE * phStorage)
{
CASC_OPEN_STORAGE_ARGS OpenArgs = {sizeof(CASC_OPEN_STORAGE_ARGS)};
OpenArgs.dwLocaleMask = dwLocaleMask;
return CascOpenStorageEx(szParams, &OpenArgs, true, phStorage);
}
bool WINAPI CascGetStorageInfo(
HANDLE hStorage,
CASC_STORAGE_INFO_CLASS InfoClass,
void * pvStorageInfo,
size_t cbStorageInfo,
size_t * pcbLengthNeeded)
{
TCascStorage * hs;
PDWORD PtrOutputValue;
DWORD dwInfoValue = 0;
// Verify the storage handle
hs = TCascStorage::IsValid(hStorage);
if(hs == NULL)
{
SetCascError(ERROR_INVALID_HANDLE);
return false;
}
// Differentiate between info classes
switch(InfoClass)
{
case CascStorageLocalFileCount:
dwInfoValue = (DWORD)hs->LocalFiles;
break;
case CascStorageTotalFileCount:
if(hs->TotalFiles == 0)
hs->TotalFiles = GetStorageTotalFileCount(hs);
dwInfoValue = (DWORD)hs->TotalFiles;
break;
case CascStorageFeatures:
dwInfoValue = hs->dwFeatures | hs->pRootHandler->GetFeatures();
break;
case CascStorageInstalledLocales:
dwInfoValue = hs->dwDefaultLocale;
break;
case CascStorageProduct:
return GetStorageProduct(hs, pvStorageInfo, cbStorageInfo, pcbLengthNeeded);
case CascStorageTags:
return GetStorageTags(hs, pvStorageInfo, cbStorageInfo, pcbLengthNeeded);
case CascStoragePathProduct:
return GetStoragePathProduct(hs, pvStorageInfo, cbStorageInfo, pcbLengthNeeded);
default:
SetCascError(ERROR_INVALID_PARAMETER);
return false;
}
//
// Default: return a 32-bit unsigned value
//
PtrOutputValue = (PDWORD)ProbeOutputBuffer(pvStorageInfo, cbStorageInfo, sizeof(DWORD), pcbLengthNeeded);
if(PtrOutputValue != NULL)
PtrOutputValue[0] = dwInfoValue;
return (PtrOutputValue != NULL);
}
bool WINAPI CascCloseStorage(HANDLE hStorage)
{
TCascStorage * hs;
// Verify the storage handle
hs = TCascStorage::IsValid(hStorage);
if(hs == NULL)
{
SetCascError(ERROR_INVALID_PARAMETER);
return false;
}
// Only free the storage if the reference count reaches 0
hs->Release();
return true;
}