mirror of
https://github.com/TrinityCore/TrinityCore.git
synced 2026-01-16 07:30:42 +01:00
1429 lines
48 KiB
C++
1429 lines
48 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 The first version of CascOpenStorage.cpp */
|
|
/*****************************************************************************/
|
|
|
|
#define __CASCLIB_SELF__
|
|
#include "CascLib.h"
|
|
#include "CascCommon.h"
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Local defines
|
|
|
|
// Limit for "orphaned" items - those that are in index files, but are not in ENCODING manifest
|
|
#define CASC_MAX_ORPHANED_ITEMS 0x100
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// TCascStorage service functions
|
|
|
|
TCascStorage::TCascStorage()
|
|
{
|
|
// Prepare the base storage parameters
|
|
szClassName = "TCascStorage";
|
|
pRootHandler = NULL;
|
|
dwDefaultLocale = CASC_LOCALE_ENUS | CASC_LOCALE_ENGB;
|
|
dwRefCount = 1;
|
|
|
|
szRootPath = szDataPath = szIndexPath = szBuildFile = szCdnServers = szCdnPath = szCodeName = NULL;
|
|
szProductName = NULL;
|
|
szIndexFormat = NULL;
|
|
szRegion = NULL;
|
|
|
|
memset(DataFiles, 0, sizeof(DataFiles));
|
|
Product = UnknownProduct;
|
|
dwBuildNumber = 0;
|
|
dwFeatures = 0;
|
|
bAllowOrphans = false;
|
|
BuildFileType = CascBuildNone;
|
|
|
|
LocalFiles = TotalFiles = EKeyEntries = OrphanItems = SkippedItems = EKeyLength = FileOffsetBits = 0;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// Free the file paths
|
|
CASC_FREE(szDataPath);
|
|
CASC_FREE(szRootPath);
|
|
CASC_FREE(szBuildFile);
|
|
CASC_FREE(szIndexPath);
|
|
CASC_FREE(szCdnServers);
|
|
CASC_FREE(szCdnPath);
|
|
CASC_FREE(szCodeName);
|
|
CASC_FREE(szRegion);
|
|
|
|
// Free the blobs
|
|
FreeCascBlob(&CdnConfigKey);
|
|
FreeCascBlob(&CdnBuildKey);
|
|
|
|
FreeCascBlob(&ArchiveGroup);
|
|
FreeCascBlob(&ArchivesKey);
|
|
FreeCascBlob(&PatchArchivesKey);
|
|
FreeCascBlob(&PatchArchivesGroup);
|
|
FreeCascBlob(&BuildFiles);
|
|
szClassName = NULL;
|
|
}
|
|
|
|
TCascStorage * TCascStorage::AddRef()
|
|
{
|
|
dwRefCount++;
|
|
return this;
|
|
}
|
|
|
|
TCascStorage * TCascStorage::Release()
|
|
{
|
|
if (dwRefCount == 1)
|
|
{
|
|
delete this;
|
|
return NULL;
|
|
}
|
|
|
|
dwRefCount--;
|
|
return NULL;
|
|
}
|
|
|
|
TCascStorage * TCascStorage::IsValid(HANDLE hStorage)
|
|
{
|
|
TCascStorage * hs = (TCascStorage *)hStorage;
|
|
|
|
return (hs != NULL && hs->szClassName != NULL && !strcmp(hs->szClassName, "TCascStorage")) ? hs : NULL;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Local functions
|
|
|
|
void * ProbeOutputBuffer(void * pvBuffer, size_t cbLength, size_t cbMinLength, size_t * pcbLengthNeeded)
|
|
{
|
|
// Verify the output length
|
|
if(cbLength < cbMinLength)
|
|
{
|
|
SetLastError(ERROR_INSUFFICIENT_BUFFER);
|
|
pvBuffer = NULL;
|
|
}
|
|
|
|
// Give the output length and return result
|
|
if(pcbLengthNeeded != NULL)
|
|
pcbLengthNeeded[0] = cbMinLength;
|
|
return pvBuffer;
|
|
}
|
|
|
|
static TCHAR * CheckForIndexDirectory(TCascStorage * hs, const TCHAR * szSubDir)
|
|
{
|
|
TCHAR * szIndexPath;
|
|
|
|
// Combine the index path
|
|
szIndexPath = CombinePath(hs->szDataPath, szSubDir);
|
|
if (!DirectoryExists(szIndexPath))
|
|
{
|
|
CASC_FREE(szIndexPath);
|
|
}
|
|
|
|
return szIndexPath;
|
|
}
|
|
|
|
static int CreateCKeyMaps(TCascStorage * hs, CASC_ENCODING_HEADER & EnHeader)
|
|
{
|
|
size_t nEstimatedEntries = (EnHeader.CKeyPageCount * EnHeader.CKeyPageSize) / sizeof(FILE_CKEY_ENTRY);
|
|
size_t nIxEntries = hs->IndexArray.ItemCount();
|
|
int nError;
|
|
|
|
// Orphaned items: These are present in INDEX files (by EKey), but missing in the ENCODING manifest.
|
|
// Probably a bug in generator of "2018 - New CASC\00001", but we want to open the storage nontheless.
|
|
if(nEstimatedEntries < 0x100)
|
|
{
|
|
nEstimatedEntries = nEstimatedEntries + nIxEntries;
|
|
hs->bAllowOrphans = true;
|
|
}
|
|
|
|
// Allow some room for extra entries
|
|
nEstimatedEntries += CASC_MAX_ORPHANED_ITEMS;
|
|
|
|
// Create the array of CKey items
|
|
nError = hs->CKeyArray.Create(sizeof(CASC_CKEY_ENTRY), nEstimatedEntries);
|
|
if(nError != ERROR_SUCCESS)
|
|
return nError;
|
|
|
|
// Create the map CKey -> CASC_CKEY_ENTRY
|
|
nError = hs->CKeyMap.Create(nEstimatedEntries, EnHeader.CKeyLength, FIELD_OFFSET(CASC_CKEY_ENTRY, CKey));
|
|
if(nError != ERROR_SUCCESS)
|
|
return nError;
|
|
|
|
// Create the map EKey -> CASC_CKEY_ENTRY
|
|
nError = hs->EKeyMap.Create(nEstimatedEntries, hs->EKeyLength, FIELD_OFFSET(CASC_CKEY_ENTRY, EKey));
|
|
if(nError != ERROR_SUCCESS)
|
|
return nError;
|
|
|
|
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 int LoadEncodingCKeyPage(TCascStorage * hs, CASC_ENCODING_HEADER & EnHeader, LPBYTE pbPageBegin, LPBYTE pbEndOfPage)
|
|
{
|
|
PCASC_CKEY_ENTRY pCKeyEntry;
|
|
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);
|
|
|
|
// Insert the CKey entry into the array
|
|
pCKeyEntry = (PCASC_CKEY_ENTRY)hs->CKeyArray.Insert(1);
|
|
if(pCKeyEntry != NULL)
|
|
{
|
|
// Supply both CKey and EKey. Rewrite EKey regardless, because ENCODING manifest contains a full one
|
|
CopyMemory16(pCKeyEntry->CKey, pFileEntry->CKey);
|
|
CopyMemory16(pCKeyEntry->EKey, pFileEntry->EKey);
|
|
pCKeyEntry->StorageOffset = CASC_INVALID_OFFS64;
|
|
pCKeyEntry->TagBitMask = 0;
|
|
pCKeyEntry->EncodedSize = CASC_INVALID_SIZE;
|
|
pCKeyEntry->ContentSize = ConvertBytesToInteger_4(pFileEntry->ContentSize);
|
|
pCKeyEntry->RefCount = 0;
|
|
pCKeyEntry->Priority = 0;
|
|
pCKeyEntry->Flags = (CASC_CE_HAS_CKEY | CASC_CE_HAS_EKEY | CASC_CE_IN_ENCODING);
|
|
|
|
// Insert the item into both maps
|
|
hs->CKeyMap.InsertObject(pCKeyEntry, pCKeyEntry->CKey);
|
|
hs->EKeyMap.InsertObject(pCKeyEntry, pCKeyEntry->EKey);
|
|
}
|
|
|
|
// Move to the next encoding entry
|
|
pbFileEntry = pbFileEntry + 2 + 4 + EnHeader.CKeyLength + (pFileEntry->EKeyCount * EnHeader.EKeyLength);
|
|
}
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
static PCASC_CKEY_ENTRY InsertCKeyEntry(TCascStorage * hs, PCASC_CKEY_ENTRY pSourceEntry, bool bAllowOrphans, bool * pbAllocatedNewEntry)
|
|
{
|
|
PCASC_CKEY_ENTRY pCKeyEntry = NULL;
|
|
bool bAllocatedNewEntry = false;
|
|
|
|
if(pSourceEntry->Flags & CASC_CE_HAS_EKEY)
|
|
{
|
|
// If there is that item already, reuse it
|
|
pCKeyEntry = FindCKeyEntry_EKey(hs, pSourceEntry->EKey);
|
|
if(pCKeyEntry == NULL)
|
|
{
|
|
// Increment number of orphaned index entries
|
|
hs->OrphanItems++;
|
|
|
|
// Insert the orphan item only of they are allowed and if we won't overflow the array
|
|
if(bAllowOrphans && (hs->CKeyArray.ItemCount() + 1) < hs->CKeyArray.ItemCountMax())
|
|
{
|
|
// Insert a new entry to the array
|
|
pCKeyEntry = (PCASC_CKEY_ENTRY)hs->CKeyArray.Insert(1);
|
|
if(pCKeyEntry != NULL)
|
|
{
|
|
// Copy CKey, EKey and some flags
|
|
if(pSourceEntry->Flags & CASC_CE_HAS_CKEY)
|
|
CopyMemory16(pCKeyEntry->CKey, pSourceEntry->CKey);
|
|
|
|
if(pSourceEntry->Flags & CASC_CE_HAS_EKEY)
|
|
CopyMemory16(pCKeyEntry->EKey, pSourceEntry->EKey);
|
|
|
|
pCKeyEntry->StorageOffset = CASC_INVALID_OFFS64;
|
|
pCKeyEntry->TagBitMask = 0;
|
|
pCKeyEntry->RefCount = 0;
|
|
pCKeyEntry->Priority = 0;
|
|
|
|
pCKeyEntry->EncodedSize = CASC_INVALID_SIZE;
|
|
pCKeyEntry->ContentSize = CASC_INVALID_SIZE;
|
|
pCKeyEntry->Flags = (pSourceEntry->Flags & (CASC_CE_HAS_CKEY | CASC_CE_HAS_EKEY | CASC_CE_HAS_EKEY_PARTIAL));
|
|
bAllocatedNewEntry = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
hs->SkippedItems++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(pbAllocatedNewEntry != NULL)
|
|
pbAllocatedNewEntry[0] = bAllocatedNewEntry;
|
|
return pCKeyEntry;
|
|
}
|
|
|
|
static PCASC_CKEY_ENTRY CopyBuildFileItemToCKeyArray(TCascStorage * hs, PCASC_CKEY_ENTRY pSourceEntry)
|
|
{
|
|
PCASC_CKEY_ENTRY pCKeyEntry = NULL;
|
|
bool bAllocatedNewEntry = false;
|
|
|
|
pCKeyEntry = InsertCKeyEntry(hs, pSourceEntry, true, &bAllocatedNewEntry);
|
|
if(pCKeyEntry != NULL)
|
|
{
|
|
// Fill the values that might be known
|
|
if(pCKeyEntry->EncodedSize == CASC_INVALID_SIZE)
|
|
pCKeyEntry->EncodedSize = pSourceEntry->EncodedSize;
|
|
if(pCKeyEntry->ContentSize == CASC_INVALID_SIZE)
|
|
pCKeyEntry->ContentSize = pSourceEntry->ContentSize;
|
|
|
|
// If this is a new entry, we need to insert it to the maps
|
|
if(bAllocatedNewEntry)
|
|
{
|
|
if(pCKeyEntry->Flags & CASC_CE_HAS_CKEY)
|
|
hs->CKeyMap.InsertObject(pCKeyEntry, pCKeyEntry->CKey);
|
|
if(pCKeyEntry->Flags & CASC_CE_HAS_EKEY)
|
|
hs->EKeyMap.InsertObject(pCKeyEntry, pCKeyEntry->EKey);
|
|
}
|
|
}
|
|
|
|
return pCKeyEntry;
|
|
}
|
|
|
|
static int CopyBuildFileItemsToCKeyArray(TCascStorage * hs)
|
|
{
|
|
// Insert the well-known files
|
|
CopyBuildFileItemToCKeyArray(hs, &hs->EncodingCKey);
|
|
CopyBuildFileItemToCKeyArray(hs, &hs->DownloadCKey);
|
|
CopyBuildFileItemToCKeyArray(hs, &hs->InstallCKey);
|
|
CopyBuildFileItemToCKeyArray(hs, &hs->PatchFile);
|
|
CopyBuildFileItemToCKeyArray(hs, &hs->RootFile);
|
|
CopyBuildFileItemToCKeyArray(hs, &hs->SizeFile);
|
|
CopyBuildFileItemToCKeyArray(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);
|
|
CopyBuildFileItemToCKeyArray(hs, pCKeyEntry);
|
|
}
|
|
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
static int CopyIndexItemsToCKeyArray(TCascStorage * hs)
|
|
{
|
|
PCASC_CKEY_ENTRY pIndexEntry;
|
|
PCASC_CKEY_ENTRY pCKeyEntry;
|
|
size_t nItemCount = hs->IndexArray.ItemCount();
|
|
bool bAllocatedNewEntry = false;
|
|
|
|
// Iterate over all index items
|
|
for(size_t i = 0; i < nItemCount; i++)
|
|
{
|
|
// Get the n-th index entry
|
|
pIndexEntry = (PCASC_CKEY_ENTRY)hs->IndexArray.ItemAt(i);
|
|
|
|
// Sometimes, there are multiple items with the same EKey in the index files
|
|
// Example: "2018 - New CASC\00001", EKey 37 89 16 5b 2d cc 71 c1 25 00 00 00 00 00 00 00
|
|
// Positions: 0x2D, 0x2E, 0x2F
|
|
//BREAK_ON_XKEY3(pIndexEntry->EKey, 0x37, 0x89, 0x16);
|
|
|
|
// Copy the index entry to the central storage
|
|
if((pCKeyEntry = InsertCKeyEntry(hs, pIndexEntry, hs->bAllowOrphans, &bAllocatedNewEntry)) != NULL)
|
|
{
|
|
// Make sure that the CKey is zeroed when not present
|
|
if((pCKeyEntry->Flags & CASC_CE_HAS_CKEY) == 0)
|
|
ZeroMemory16(pCKeyEntry->CKey);
|
|
|
|
// Only copy the storage offset and sizes if not available yet
|
|
if(pCKeyEntry->StorageOffset == CASC_INVALID_OFFS64)
|
|
{
|
|
pCKeyEntry->StorageOffset = pIndexEntry->StorageOffset;
|
|
pCKeyEntry->EncodedSize = pIndexEntry->EncodedSize;
|
|
}
|
|
|
|
if(bAllocatedNewEntry)
|
|
{
|
|
if(pCKeyEntry->Flags & CASC_CE_HAS_CKEY)
|
|
hs->CKeyMap.InsertObject(pCKeyEntry, pCKeyEntry->CKey);
|
|
if(pCKeyEntry->Flags & CASC_CE_HAS_EKEY)
|
|
hs->EKeyMap.InsertObject(pCKeyEntry, pCKeyEntry->EKey);
|
|
}
|
|
|
|
// Mark the file as available locally
|
|
pCKeyEntry->Flags |= CASC_CE_FILE_IS_LOCAL;
|
|
}
|
|
}
|
|
|
|
// We free the index array at this point
|
|
hs->IndexArray.Free();
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
static int LoadEncodingManifest(TCascStorage * hs)
|
|
{
|
|
LPBYTE pbEncodingFile;
|
|
DWORD cbEncodingFile = 0;
|
|
int nError = ERROR_SUCCESS;
|
|
|
|
// Inform the user about what we are doing
|
|
if(InvokeProgressCallback(hs, "Loading ENCODING manifest", NULL, 0, 0))
|
|
return ERROR_CANCELLED;
|
|
|
|
// Load the entire encoding file to memory
|
|
pbEncodingFile = LoadInternalFileToMemory(hs, &hs->EncodingCKey, &cbEncodingFile);
|
|
if(pbEncodingFile != NULL && cbEncodingFile != 0)
|
|
{
|
|
CASC_ENCODING_HEADER EnHeader;
|
|
|
|
// Capture the header of the ENCODING file
|
|
nError = CaptureEncodingHeader(EnHeader, pbEncodingFile, cbEncodingFile);
|
|
if(nError == ERROR_SUCCESS)
|
|
{
|
|
// Get the CKey page header and the first page
|
|
PFILE_CKEY_PAGE pPageHeader = (PFILE_CKEY_PAGE)(pbEncodingFile + sizeof(FILE_ENCODING_HEADER) + EnHeader.ESpecBlockSize);
|
|
LPBYTE pbCKeyPage = (LPBYTE)(pPageHeader + EnHeader.CKeyPageCount);
|
|
|
|
// Since ENCODING contains the full list of all files (even those not downloaded),
|
|
// we can now make a fair estimate about how large maps shall we create.
|
|
// So, we can build the maps CKey and EKey map.
|
|
if((nError = CreateCKeyMaps(hs, EnHeader)) == ERROR_SUCCESS)
|
|
{
|
|
// Go through all CKey pages and verify them
|
|
for(DWORD i = 0; i < EnHeader.CKeyPageCount; i++)
|
|
{
|
|
PFILE_CKEY_ENTRY pCKeyEntry = (PFILE_CKEY_ENTRY)pbCKeyPage;
|
|
|
|
// Check if there is enough space in the buffer
|
|
if((pbCKeyPage + EnHeader.CKeyPageSize) > (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(pbCKeyPage, EnHeader.CKeyPageSize, pEncodingSegment->SegmentHash))
|
|
// {
|
|
// nError = ERROR_FILE_CORRUPT;
|
|
// break;
|
|
// }
|
|
|
|
// Check if the CKey matches with the expected first value
|
|
if(memcmp(pCKeyEntry->CKey, pPageHeader[i].FirstKey, CASC_CKEY_SIZE))
|
|
{
|
|
nError = ERROR_FILE_CORRUPT;
|
|
break;
|
|
}
|
|
|
|
// Load the entire page of CKey entries.
|
|
// This operation will never fail, because all memory is already pre-allocated
|
|
nError = LoadEncodingCKeyPage(hs, EnHeader, pbCKeyPage, pbCKeyPage + EnHeader.CKeyPageSize);
|
|
if(nError != 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
|
|
// This also includes the ENCODING file itself, which is vital for later loading
|
|
if(nError == ERROR_SUCCESS)
|
|
{
|
|
nError = CopyBuildFileItemsToCKeyArray(hs);
|
|
}
|
|
|
|
// Now supply all the entries from the index files
|
|
if(nError == ERROR_SUCCESS)
|
|
{
|
|
nError = CopyIndexItemsToCKeyArray(hs);
|
|
}
|
|
|
|
// Free the loaded ENCODING file
|
|
CASC_FREE(pbEncodingFile);
|
|
}
|
|
else
|
|
{
|
|
nError = GetLastError();
|
|
}
|
|
|
|
return nError;
|
|
}
|
|
|
|
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;
|
|
int nError = 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
|
|
nError = hs->TagsArray.Create(nTagEntryLengh, DlHeader.TagCount);
|
|
if(nError == 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)
|
|
{
|
|
nError = 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
|
|
{
|
|
nError = 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;
|
|
size_t BitMaskOffset = (i / 8);
|
|
BYTE BitMaskBit = 0x80 >> (i % 8);
|
|
|
|
// Capture the download entry
|
|
if(CaptureDownloadEntry(DlHeader, DlEntry, pbEntry, pbFileEnd) != ERROR_SUCCESS)
|
|
break;
|
|
|
|
// Make sure we have the entry in CKey table
|
|
pCKeyEntry = FindCKeyEntry_EKey(hs, DlEntry.EKey);
|
|
if(pCKeyEntry != NULL)
|
|
{
|
|
ULONGLONG TagBit = 1;
|
|
size_t TagItemCount = hs->TagsArray.ItemCount();
|
|
|
|
// 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;
|
|
}
|
|
|
|
// If the EKey has partial EKey only, fix that
|
|
if(pCKeyEntry->Flags & CASC_CE_HAS_EKEY_PARTIAL)
|
|
{
|
|
CopyMemory16(pCKeyEntry->EKey, DlEntry.EKey);
|
|
pCKeyEntry->Flags &= ~CASC_CE_HAS_EKEY_PARTIAL;
|
|
}
|
|
|
|
// Supply the priority
|
|
pCKeyEntry->Priority = DlEntry.Priority;
|
|
pCKeyEntry->Flags |= CASC_CE_IN_DOWNLOAD;
|
|
}
|
|
|
|
// 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 nError;
|
|
}
|
|
|
|
static int LoadDownloadManifest(TCascStorage * hs)
|
|
{
|
|
PCASC_CKEY_ENTRY pCKeyEntry = FindCKeyEntry_CKey(hs, hs->DownloadCKey.CKey);
|
|
LPBYTE pbDownloadFile = NULL;
|
|
DWORD cbDownloadFile = 0;
|
|
int nError = ERROR_SUCCESS;
|
|
|
|
// Inform the user about what we are doing
|
|
if(InvokeProgressCallback(hs, "Loading DOWNLOAD manifest", NULL, 0, 0))
|
|
return ERROR_CANCELLED;
|
|
|
|
// Load the entire DOWNLOAD file to memory
|
|
pbDownloadFile = LoadInternalFileToMemory(hs, pCKeyEntry, &cbDownloadFile);
|
|
if(pbDownloadFile != NULL && cbDownloadFile != 0)
|
|
{
|
|
CASC_DOWNLOAD_HEADER DlHeader;
|
|
|
|
// Capture the header of the DOWNLOAD file
|
|
nError = CaptureDownloadHeader(DlHeader, pbDownloadFile, cbDownloadFile);
|
|
if(nError == ERROR_SUCCESS)
|
|
{
|
|
// Parse the entire download manifest
|
|
nError = LoadDownloadManifest(hs, DlHeader, pbDownloadFile, pbDownloadFile + cbDownloadFile);
|
|
}
|
|
|
|
// Free the loaded manifest
|
|
CASC_FREE(pbDownloadFile);
|
|
}
|
|
|
|
// If the DOWNLOAD manifest is not present, we won't abort the downloading process.
|
|
return nError;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// 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);
|
|
LPBYTE pbInstallFile = NULL;
|
|
DWORD cbInstallFile = 0;
|
|
int nError = ERROR_SUCCESS;
|
|
|
|
// Inform the user about what we are doing
|
|
if(InvokeProgressCallback(hs, "Loading INSTALL manifest", NULL, 0, 0))
|
|
return ERROR_CANCELLED;
|
|
|
|
// Load the entire DOWNLOAD file to memory
|
|
pbInstallFile = LoadInternalFileToMemory(hs, pCKeyEntry, &cbInstallFile);
|
|
if (pbInstallFile != NULL && cbInstallFile != 0)
|
|
{
|
|
nError = RootHandler_CreateInstall(hs, pbInstallFile, cbInstallFile);
|
|
CASC_FREE(pbInstallFile);
|
|
}
|
|
else
|
|
{
|
|
nError = GetLastError();
|
|
}
|
|
|
|
return nError;
|
|
}
|
|
|
|
static bool InsertWellKnownFile(TCascStorage * hs, const char * szFileName, CASC_CKEY_ENTRY & FakeCKeyEntry)
|
|
{
|
|
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, unless it's already referenced by a name
|
|
if(pCKeyEntry->RefCount == 0)
|
|
hs->pRootHandler->Insert(szFileName, pCKeyEntry);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static int LoadBuildManifest(TCascStorage * hs, DWORD dwLocaleMask)
|
|
{
|
|
PCASC_CKEY_ENTRY pCKeyEntry;
|
|
PDWORD FileSignature;
|
|
LPBYTE pbRootFile = NULL;
|
|
DWORD cbRootFile = 0;
|
|
int nError = ERROR_BAD_FORMAT;
|
|
|
|
// Sanity checks
|
|
assert(hs->CKeyMap.IsInitialized() == true);
|
|
assert(hs->pRootHandler == NULL);
|
|
|
|
// Locale: The default parameter is 0 - in that case,
|
|
// we assign the default locale, loaded from the .build.info file
|
|
if(dwLocaleMask == 0)
|
|
dwLocaleMask = hs->dwDefaultLocale;
|
|
|
|
// Prioritize the VFS root over legacy ROOT file
|
|
pCKeyEntry = (hs->VfsRoot.ContentSize != CASC_INVALID_SIZE) ? &hs->VfsRoot : &hs->RootFile;
|
|
pCKeyEntry = FindCKeyEntry_CKey(hs, pCKeyEntry->CKey);
|
|
|
|
// Inform the user about what we are doing
|
|
if(InvokeProgressCallback(hs, "Loading ROOT manifest", NULL, 0, 0))
|
|
return ERROR_CANCELLED;
|
|
|
|
// Load the entire ROOT file to memory
|
|
pbRootFile = LoadInternalFileToMemory(hs, pCKeyEntry, &cbRootFile);
|
|
if(pbRootFile != NULL)
|
|
{
|
|
// Ignore ROOT files that contain just a MD5 hash
|
|
if(cbRootFile > MD5_STRING_SIZE)
|
|
{
|
|
// Check the type of the ROOT file
|
|
FileSignature = (PDWORD)pbRootFile;
|
|
switch(FileSignature[0])
|
|
{
|
|
case CASC_MNDX_ROOT_SIGNATURE:
|
|
nError = RootHandler_CreateMNDX(hs, pbRootFile, cbRootFile);
|
|
break;
|
|
|
|
case CASC_DIABLO3_ROOT_SIGNATURE:
|
|
nError = RootHandler_CreateDiablo3(hs, pbRootFile, cbRootFile);
|
|
break;
|
|
|
|
case CASC_TVFS_ROOT_SIGNATURE:
|
|
nError = RootHandler_CreateTVFS(hs, pbRootFile, cbRootFile);
|
|
break;
|
|
|
|
case CASC_WOW82_ROOT_SIGNATURE:
|
|
nError = RootHandler_CreateWoW(hs, pbRootFile, cbRootFile, 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
|
|
//
|
|
|
|
nError = RootHandler_CreateOverwatch(hs, pbRootFile, cbRootFile);
|
|
if(nError == ERROR_BAD_FORMAT)
|
|
{
|
|
nError = RootHandler_CreateStarcraft1(hs, pbRootFile, cbRootFile);
|
|
if(nError == ERROR_BAD_FORMAT)
|
|
{
|
|
nError = RootHandler_CreateWoW(hs, pbRootFile, cbRootFile, dwLocaleMask);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Free the root file
|
|
CASC_FREE(pbRootFile);
|
|
}
|
|
else
|
|
{
|
|
nError = GetLastError();
|
|
}
|
|
|
|
return nError;
|
|
}
|
|
|
|
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->Flags & CASC_CE_FOLDER_ENTRY) == 0)
|
|
{
|
|
// 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 > 0) ? 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)
|
|
{
|
|
pProductInfo->szProductName = hs->szProductName;
|
|
pProductInfo->dwBuildNumber = hs->dwBuildNumber;
|
|
pProductInfo->Product = hs->Product;
|
|
}
|
|
|
|
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)
|
|
{
|
|
SetLastError(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(':');
|
|
CascStrCopy(szBuffer, (szBufferEnd - szBuffer), hs->szCodeName);
|
|
szBuffer += _tcslen(hs->szCodeName);
|
|
}
|
|
|
|
// Append the product region, if any
|
|
if(hs->szRegion != NULL)
|
|
{
|
|
*szBuffer++ = _T(':');
|
|
CascStrCopy(szBuffer, (szBufferEnd - szBuffer), hs->szRegion);
|
|
}
|
|
}
|
|
|
|
return (szBuffer != NULL);
|
|
}
|
|
|
|
static DWORD InitializeLocalDirectories(TCascStorage * hs, PCASC_OPEN_STORAGE_ARGS pArgs)
|
|
{
|
|
TCHAR * szWorkPath;
|
|
DWORD dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
|
|
|
|
// Find the root directory of the storage. The root directory
|
|
// is the one with ".build.info" or ".build.db".
|
|
szWorkPath = CascNewStr(pArgs->szLocalPath);
|
|
if(szWorkPath != NULL)
|
|
{
|
|
// Get the length and go up until we find the ".build.info" or ".build.db"
|
|
for(;;)
|
|
{
|
|
// Is this a game directory?
|
|
dwErrCode = CheckGameDirectory(hs, szWorkPath);
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
dwErrCode = ERROR_SUCCESS;
|
|
break;
|
|
}
|
|
|
|
// Cut one path part
|
|
if(!CutLastPathPart(szWorkPath))
|
|
{
|
|
dwErrCode = ERROR_FILE_NOT_FOUND;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Find the index directory
|
|
if (dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
// First, check for more common "data" subdirectory
|
|
if ((hs->szIndexPath = CheckForIndexDirectory(hs, _T("data"))) != NULL)
|
|
dwErrCode = ERROR_SUCCESS;
|
|
|
|
// Second, try the "darch" subdirectory (older builds of HOTS - Alpha)
|
|
else if ((hs->szIndexPath = CheckForIndexDirectory(hs, _T("darch"))) != NULL)
|
|
dwErrCode = ERROR_SUCCESS;
|
|
|
|
else
|
|
dwErrCode = ERROR_FILE_NOT_FOUND;
|
|
}
|
|
|
|
// Free the work path buffer
|
|
CASC_FREE(szWorkPath);
|
|
}
|
|
|
|
return dwErrCode;
|
|
}
|
|
|
|
static DWORD InitializeOnlineDirectories(TCascStorage * hs, PCASC_OPEN_STORAGE_ARGS pArgs)
|
|
{
|
|
LPCTSTR szLocalCache = pArgs->szLocalPath;
|
|
LPCTSTR szCodeName = pArgs->szCodeName;
|
|
|
|
// Create the root path
|
|
hs->szRootPath = CombinePath(szLocalCache, szCodeName);
|
|
if (hs->szRootPath != NULL)
|
|
{
|
|
// Create the name of the build file
|
|
hs->szBuildFile = CombinePath(hs->szRootPath, _T("versions"));
|
|
if(hs->szBuildFile != NULL)
|
|
{
|
|
hs->BuildFileType = CascVersionsDb;
|
|
hs->dwFeatures |= CASC_FEATURE_ONLINE;
|
|
return ERROR_SUCCESS;
|
|
}
|
|
}
|
|
|
|
return ERROR_NOT_ENOUGH_MEMORY;
|
|
}
|
|
|
|
static DWORD LoadCascStorage(TCascStorage * hs, PCASC_OPEN_STORAGE_ARGS pArgs)
|
|
{
|
|
LPCTSTR szCodeName = NULL;
|
|
LPCTSTR szRegion = NULL;
|
|
char szRegionA[0x40];
|
|
DWORD dwLocaleMask = 0;
|
|
DWORD dwErrCode = ERROR_SUCCESS;
|
|
|
|
// Pass the argument array to the storage
|
|
hs->pArgs = pArgs;
|
|
|
|
// Extract optional arguments
|
|
ExtractVersionedArgument(pArgs, offsetof(CASC_OPEN_STORAGE_ARGS, dwLocaleMask), &dwLocaleMask);
|
|
|
|
// Extract the product code name
|
|
if(ExtractVersionedArgument(pArgs, offsetof(CASC_OPEN_STORAGE_ARGS, szCodeName), &szCodeName) && szCodeName != NULL)
|
|
hs->szCodeName = CascNewStr(szCodeName);
|
|
|
|
// Extract the region (optional)
|
|
if(ExtractVersionedArgument(pArgs, offsetof(CASC_OPEN_STORAGE_ARGS, szRegion), &szRegion) && szRegion != NULL)
|
|
{
|
|
CascStrCopy(szRegionA, _countof(szRegionA), szRegion);
|
|
hs->szRegion = CascNewStr(szRegionA);
|
|
}
|
|
|
|
// For online storages, we need to load CDN servers
|
|
if ((dwErrCode == ERROR_SUCCESS) && (hs->dwFeatures & CASC_FEATURE_ONLINE))
|
|
{
|
|
dwErrCode = LoadCdnsInfo(hs);
|
|
}
|
|
|
|
// Now, load the main storage file ".build.info" (or ".build.db" in old storages)
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
dwErrCode = LoadBuildInfo(hs);
|
|
}
|
|
|
|
// If the .build.info OR .build.db file has been loaded,
|
|
// proceed with loading the CDN config file
|
|
if (dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
dwErrCode = LoadCdnConfigFile(hs);
|
|
}
|
|
|
|
// Proceed with loading the CDN build file
|
|
if (dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
dwErrCode = LoadCdnBuildFile(hs);
|
|
}
|
|
|
|
// Load the index files. Store information from the index files to the CKeyArray.
|
|
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. This will give us the information about
|
|
// how many physical files are in the storage, so we can start building file tables
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
dwErrCode = LoadDownloadManifest(hs);
|
|
}
|
|
|
|
// Load the build manifest ("ROOT" file)
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
// If we fail to load the ROOT file, we take the file names from the INSTALL manifest
|
|
dwErrCode = LoadBuildManifest(hs, dwLocaleMask);
|
|
if (dwErrCode != ERROR_SUCCESS)
|
|
{
|
|
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);
|
|
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);
|
|
}
|
|
|
|
// Clear the arg structure
|
|
hs->pArgs = pArgs;
|
|
return dwErrCode;
|
|
}
|
|
|
|
static LPTSTR ParseOpenParams(LPCTSTR szParams, PCASC_OPEN_STORAGE_ARGS pArgs)
|
|
{
|
|
LPTSTR szParamsCopy;
|
|
|
|
// The 'szParams' must not be empty
|
|
if(szParams == NULL || pArgs == NULL || szParams[0] == 0)
|
|
{
|
|
SetLastError(ERROR_INVALID_PARAMETER);
|
|
return NULL;
|
|
}
|
|
|
|
// The 'pArgs' must be valid but must not contain 'szLocalPath', 'szCodeName' or 'szRegion'
|
|
if(pArgs->szLocalPath != NULL || pArgs->szCodeName != NULL || pArgs->szRegion != NULL)
|
|
{
|
|
SetLastError(ERROR_INVALID_PARAMETER);
|
|
return NULL;
|
|
}
|
|
|
|
// Make a copy of the parameters so we can temper with them
|
|
if((szParamsCopy = CascNewStr(szParams)) != NULL)
|
|
{
|
|
LPTSTR szPlainName = (LPTSTR)GetPlainFileName(szParamsCopy);
|
|
LPTSTR szSeparator;
|
|
|
|
// The local path is always set
|
|
pArgs->szLocalPath = szParamsCopy;
|
|
pArgs->szCodeName = NULL;
|
|
pArgs->szRegion = NULL;
|
|
|
|
// Find the first ":". This will indicate the end of local path and also begin of product code
|
|
if((szSeparator = _tcschr(szPlainName, _T(':'))) != NULL)
|
|
{
|
|
// The found string is a product code name
|
|
pArgs->szCodeName = szSeparator + 1;
|
|
szSeparator[0] = 0;
|
|
|
|
// Try again. If found, it is a product region
|
|
if((szSeparator = _tcschr(szSeparator + 1, _T(':'))) != NULL)
|
|
{
|
|
pArgs->szRegion = szSeparator + 1;
|
|
szSeparator[0] = 0;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SetLastError(ERROR_NOT_ENOUGH_MEMORY);
|
|
}
|
|
|
|
return szParamsCopy;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// 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)};
|
|
DWORD (*PfnInitDirs)(TCascStorage * hs, PCASC_OPEN_STORAGE_ARGS pArgs);
|
|
TCascStorage * hs;
|
|
LPTSTR szParamsCopy = NULL;
|
|
DWORD dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
|
|
|
|
// The storage path[+product[+region]] must either be passed in szParams or in pArgs. Not both.
|
|
// It is allowed to pass NULL as pArgs if the szParams is not NULL
|
|
if(szParams != NULL)
|
|
{
|
|
if(pArgs == NULL)
|
|
pArgs = &LocalArgs;
|
|
|
|
szParamsCopy = ParseOpenParams(szParams, pArgs);
|
|
if(szParamsCopy == NULL)
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
// The arguments and the local path must be entered
|
|
if(pArgs == NULL || pArgs->szLocalPath == NULL || pArgs->szLocalPath[0] == 0)
|
|
{
|
|
SetLastError(ERROR_INVALID_PARAMETER);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Allocate the storage structure
|
|
if((hs = new TCascStorage()) != NULL)
|
|
{
|
|
// Setup the directories
|
|
PfnInitDirs = (bOnlineStorage) ? InitializeOnlineDirectories : InitializeLocalDirectories;
|
|
dwErrCode = PfnInitDirs(hs, pArgs);
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
// Perform the entire storage loading
|
|
dwErrCode = LoadCascStorage(hs, pArgs);
|
|
}
|
|
|
|
// Free the storage structure on fail
|
|
if(dwErrCode != ERROR_SUCCESS)
|
|
{
|
|
hs = hs->Release();
|
|
}
|
|
}
|
|
|
|
// Give the output parameter to the caller
|
|
CASC_FREE(szParamsCopy);
|
|
*phStorage = (HANDLE)hs;
|
|
|
|
// Return the result
|
|
if(dwErrCode != ERROR_SUCCESS)
|
|
SetLastError(dwErrCode);
|
|
return (dwErrCode == ERROR_SUCCESS);
|
|
}
|
|
|
|
// szParams: "LocalPath:CodeName", e.g. "C:\\Games\\World of Warcraft:wowt"
|
|
// * LocalPath: Local folder, where the online file will be cached.
|
|
// * CodeName: 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);
|
|
}
|
|
|
|
// Allows to browse an online CDN storage
|
|
// szParams: "CachePath:CodeName:Region", e.g. "C:\\Cache:wowt:us"
|
|
// * CachePath: Local folder, where the online file will be cached.
|
|
// * CodeName: 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)
|
|
{
|
|
SetLastError(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:
|
|
SetLastError(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)
|
|
{
|
|
SetLastError(ERROR_INVALID_PARAMETER);
|
|
return false;
|
|
}
|
|
|
|
// Only free the storage if the reference count reaches 0
|
|
hs->Release();
|
|
return true;
|
|
}
|