mirror of
https://github.com/TrinityCore/TrinityCore.git
synced 2026-01-16 07:30:42 +01:00
1415 lines
48 KiB
C++
1415 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 "additional" items in CKey table
|
|
#define CASC_MAX_EXTRA_ITEMS 0x40
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// TCascStorage class functions
|
|
|
|
TCascStorage::TCascStorage()
|
|
{
|
|
// Prepare the base storage parameters
|
|
ClassName = CASC_MAGIC_STORAGE;
|
|
pRootHandler = NULL;
|
|
dwRefCount = 1;
|
|
|
|
szRootPath = szDataPath = szIndexPath = szBuildFile = szCdnServers = szCdnPath = szCodeName = NULL;
|
|
szIndexFormat = NULL;
|
|
szRegion = NULL;
|
|
|
|
memset(DataFiles, 0, sizeof(DataFiles));
|
|
dwBuildNumber = 0;
|
|
dwFeatures = 0;
|
|
BuildFileType = CascBuildNone;
|
|
|
|
LocalFiles = TotalFiles = EKeyEntries = 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);
|
|
ClassName = 0;
|
|
}
|
|
|
|
TCascStorage * TCascStorage::AddRef()
|
|
{
|
|
CascInterlockedIncrement(&dwRefCount);
|
|
return this;
|
|
}
|
|
|
|
TCascStorage * TCascStorage::Release()
|
|
{
|
|
if(CascInterlockedDecrement(&dwRefCount) == 0)
|
|
{
|
|
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)
|
|
{
|
|
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;
|
|
}
|
|
|
|
// Inserts an entry from the text build file
|
|
static PCASC_CKEY_ENTRY InsertCKeyEntry(TCascStorage * hs, CASC_CKEY_ENTRY & CKeyEntry)
|
|
{
|
|
PCASC_CKEY_ENTRY pCKeyEntry = NULL;
|
|
|
|
// 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;
|
|
|
|
// Check whether the entry is already there
|
|
if((pCKeyEntry = FindCKeyEntry_EKey(hs, pFileEntry->EKey)) == 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;
|
|
|
|
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;
|
|
pCKeyEntry->Priority = 0;
|
|
|
|
// Insert the item into both maps
|
|
hs->CKeyMap.InsertObject(pCKeyEntry, pCKeyEntry->CKey);
|
|
hs->EKeyMap.InsertObject(pCKeyEntry, pCKeyEntry->EKey);
|
|
}
|
|
else
|
|
{
|
|
// 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);
|
|
|
|
// Supply the content size
|
|
if(pCKeyEntry->ContentSize == CASC_INVALID_SIZE)
|
|
pCKeyEntry->ContentSize = ConvertBytesToInteger_4(pFileEntry->ContentSize);
|
|
pCKeyEntry->Flags |= CASC_CE_HAS_CKEY | CASC_CE_HAS_EKEY | CASC_CE_IN_ENCODING;
|
|
pCKeyEntry->Flags &= ~CASC_CE_HAS_EKEY_PARTIAL;
|
|
|
|
// Insert the item into CKey map
|
|
hs->CKeyMap.InsertObject(pCKeyEntry, pCKeyEntry->CKey);
|
|
}
|
|
|
|
return pCKeyEntry;
|
|
}
|
|
|
|
// Inserts an entry from DOWNLOAD
|
|
static PCASC_CKEY_ENTRY InsertCKeyEntry(TCascStorage * hs, CASC_DOWNLOAD_ENTRY & DlEntry)
|
|
{
|
|
PCASC_CKEY_ENTRY pCKeyEntry;
|
|
|
|
// 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)
|
|
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;
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Supply the rest
|
|
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 the array
|
|
static size_t GetEstimatedNumberOfFiles(TCascStorage * hs)
|
|
{
|
|
// 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 26 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)
|
|
return (hs->DownloadCKey.ContentSize / 26) + 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)
|
|
return (hs->EncodingCKey.ContentSize / 26) + CASC_MAX_EXTRA_ITEMS;
|
|
|
|
// By default, it's gonna be half a million, which is the maximum observed number of files
|
|
// for all older storages (HOTS before 39445, WoW before 19116)
|
|
return 500000;
|
|
}
|
|
|
|
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;
|
|
|
|
// Insert the entry of ENCODING file. This is vital for its opening and loading
|
|
InsertCKeyEntry(hs, hs->EncodingCKey);
|
|
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)
|
|
{
|
|
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->EKey, 0x09, 0xF3, 0xCD);
|
|
|
|
// 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 int LoadEncodingManifest(TCascStorage * hs)
|
|
{
|
|
PCASC_CKEY_ENTRY pCKeyEntry = FindCKeyEntry_CKey(hs, hs->EncodingCKey.CKey);
|
|
LPBYTE pbEncodingFile;
|
|
DWORD cbEncodingFile = 0;
|
|
DWORD dwErrCode = 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, pCKeyEntry, &cbEncodingFile);
|
|
if(pbEncodingFile != NULL && cbEncodingFile != 0)
|
|
{
|
|
CASC_ENCODING_HEADER EnHeader;
|
|
|
|
// Capture the header of the ENCODING file
|
|
dwErrCode = CaptureEncodingHeader(EnHeader, pbEncodingFile, cbEncodingFile);
|
|
if(dwErrCode == 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);
|
|
|
|
// 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) > (pbEncodingFile + cbEncodingFile))
|
|
{
|
|
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);
|
|
}
|
|
|
|
// Now supply all the entries from the index files
|
|
//if(dwErrCode == ERROR_SUCCESS)
|
|
//{
|
|
// dwErrCode = CopyIndexItemsToCKeyArray(hs);
|
|
//}
|
|
|
|
// Free the loaded ENCODING file
|
|
CASC_FREE(pbEncodingFile);
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = GetLastError();
|
|
}
|
|
|
|
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)
|
|
{
|
|
// 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);
|
|
LPBYTE pbDownloadFile = NULL;
|
|
DWORD cbDownloadFile = 0;
|
|
DWORD dwErrCode = 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
|
|
dwErrCode = CaptureDownloadHeader(DlHeader, pbDownloadFile, cbDownloadFile);
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
// Parse the entire download manifest
|
|
dwErrCode = 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 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);
|
|
LPBYTE pbInstallFile = NULL;
|
|
DWORD cbInstallFile = 0;
|
|
DWORD dwErrCode = 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)
|
|
{
|
|
dwErrCode = RootHandler_CreateInstall(hs, pbInstallFile, cbInstallFile);
|
|
CASC_FREE(pbInstallFile);
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = GetLastError();
|
|
}
|
|
|
|
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);
|
|
pCKeyEntry->Flags |= (CASC_CE_IN_BUILD | dwFlags);
|
|
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 |= (CASC_CE_IN_BUILD | dwFlags);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static int LoadBuildManifest(TCascStorage * hs, DWORD dwLocaleMask)
|
|
{
|
|
PCASC_CKEY_ENTRY pCKeyEntry;
|
|
PDWORD FileSignature;
|
|
LPBYTE pbRootFile = NULL;
|
|
DWORD cbRootFile = 0;
|
|
DWORD dwErrCode = ERROR_BAD_FORMAT;
|
|
|
|
// Sanity checks
|
|
assert(hs->CKeyMap.IsInitialized() == true);
|
|
assert(hs->pRootHandler == NULL);
|
|
|
|
// 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
|
|
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:
|
|
dwErrCode = RootHandler_CreateMNDX(hs, pbRootFile, cbRootFile);
|
|
break;
|
|
|
|
case CASC_DIABLO3_ROOT_SIGNATURE:
|
|
dwErrCode = RootHandler_CreateDiablo3(hs, pbRootFile, cbRootFile);
|
|
break;
|
|
|
|
case CASC_TVFS_ROOT_SIGNATURE:
|
|
dwErrCode = RootHandler_CreateTVFS(hs, pbRootFile, cbRootFile);
|
|
break;
|
|
|
|
case CASC_WOW82_ROOT_SIGNATURE:
|
|
dwErrCode = 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
|
|
//
|
|
|
|
dwErrCode = RootHandler_CreateOverwatch(hs, pbRootFile, cbRootFile);
|
|
if(dwErrCode == ERROR_BAD_FORMAT)
|
|
{
|
|
dwErrCode = RootHandler_CreateStarcraft1(hs, pbRootFile, cbRootFile);
|
|
if(dwErrCode == ERROR_BAD_FORMAT)
|
|
{
|
|
dwErrCode = RootHandler_CreateWoW(hs, pbRootFile, cbRootFile, dwLocaleMask);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Free the root file
|
|
CASC_FREE(pbRootFile);
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = GetLastError();
|
|
}
|
|
|
|
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 > 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)
|
|
{
|
|
// 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)
|
|
{
|
|
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)
|
|
{
|
|
// Create the root path
|
|
hs->szRootPath = CascNewStr(pArgs->szLocalPath);
|
|
if (hs->szRootPath != 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 = LoadCdnsFile(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);
|
|
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);
|
|
}
|
|
|
|
// Create the central file array
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
dwErrCode = InitCKeyArray(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, 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);
|
|
}
|
|
|
|
// 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)};
|
|
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
|
|
dwErrCode = (bOnlineStorage) ? InitializeOnlineDirectories(hs, pArgs) : InitializeLocalDirectories(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;
|
|
}
|