mirror of
https://github.com/TrinityCore/TrinityCore.git
synced 2026-01-19 08:55:32 +01:00
1564 lines
51 KiB
C++
1564 lines
51 KiB
C++
/*****************************************************************************/
|
|
/* CascFiles.cpp Copyright (c) Ladislav Zezula 2014 */
|
|
/*---------------------------------------------------------------------------*/
|
|
/* Various text file parsers */
|
|
/*---------------------------------------------------------------------------*/
|
|
/* Date Ver Who Comment */
|
|
/* -------- ---- --- ------- */
|
|
/* 29.04.14 1.00 Lad The first version of CascBuildCfg.cpp */
|
|
/* 30.10.15 1.00 Lad Renamed to CascFiles.cpp */
|
|
/*****************************************************************************/
|
|
|
|
#define __CASCLIB_SELF__
|
|
#include "CascLib.h"
|
|
#include "CascCommon.h"
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Local defines
|
|
|
|
typedef DWORD (*PARSECSVFILE)(TCascStorage * hs, CASC_CSV & Csv);
|
|
typedef DWORD (*PARSETEXTFILE)(TCascStorage * hs, void * pvListFile);
|
|
typedef DWORD (*PARSE_VARIABLE)(TCascStorage * hs, const char * szVariableName, const char * szDataBegin, const char * szDataEnd, void * pvParam);
|
|
|
|
#define MAX_VAR_NAME 80
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Local structures
|
|
|
|
struct TBuildFileInfo
|
|
{
|
|
LPCTSTR szFileName;
|
|
CBLD_TYPE BuildFileType;
|
|
};
|
|
|
|
struct TGameLocaleString
|
|
{
|
|
const char * szLocale;
|
|
DWORD dwLocale;
|
|
};
|
|
|
|
static const TBuildFileInfo BuildTypes[] =
|
|
{
|
|
{_T(".build.info"), CascBuildInfo}, // Since HOTS build 30027, the game uses .build.info file for storage info
|
|
{_T(".build.db"), CascBuildDb}, // Older CASC storages
|
|
{_T("versions"), CascVersionsDb}, // Older CASC storages
|
|
{NULL, CascBuildNone}
|
|
};
|
|
|
|
static LPCTSTR DataDirs[] =
|
|
{
|
|
_T("data") _T(PATH_SEP_STRING) _T("casc"), // Overwatch
|
|
_T("data"), // TACT casc (for Linux systems)
|
|
_T("Data"), // World of Warcraft, Diablo
|
|
_T("SC2Data"), // Starcraft II (Legacy of the Void) build 38749
|
|
_T("HeroesData"), // Heroes of the Storm
|
|
_T("BNTData"), // Heroes of the Storm, until build 30414
|
|
NULL,
|
|
};
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Local functions
|
|
|
|
static bool inline IsWhiteSpace(const char * szVarValue)
|
|
{
|
|
return (0 <= szVarValue[0] && szVarValue[0] <= 0x20);
|
|
}
|
|
|
|
static bool inline IsValueSeparator(const char * szVarValue)
|
|
{
|
|
return (IsWhiteSpace(szVarValue) || (szVarValue[0] == '|'));
|
|
}
|
|
|
|
static bool IsCharDigit(BYTE OneByte)
|
|
{
|
|
return ('0' <= OneByte && OneByte <= '9');
|
|
}
|
|
|
|
static bool StringEndsWith(LPCSTR szString, size_t nLength, LPCSTR szSuffix, size_t nSuffixLength)
|
|
{
|
|
return ((nLength > nSuffixLength) && !strcmp(szString + nLength - nSuffixLength, szSuffix));
|
|
}
|
|
|
|
static const char * CaptureDecimalInteger(const char * szDataPtr, const char * szDataEnd, PDWORD PtrValue)
|
|
{
|
|
const char * szSaveDataPtr = szDataPtr;
|
|
DWORD TotalValue = 0;
|
|
DWORD AddValue = 0;
|
|
|
|
// Skip all spaces
|
|
while (szDataPtr < szDataEnd && szDataPtr[0] == ' ')
|
|
szDataPtr++;
|
|
|
|
// Load the number
|
|
while (szDataPtr < szDataEnd && szDataPtr[0] != ' ')
|
|
{
|
|
// Must only contain decimal digits ('0' - '9')
|
|
if (!IsCharDigit(szDataPtr[0]))
|
|
break;
|
|
|
|
// Get the next value and verify overflow
|
|
AddValue = szDataPtr[0] - '0';
|
|
if ((TotalValue + AddValue) < TotalValue)
|
|
return NULL;
|
|
|
|
TotalValue = (TotalValue * 10) + AddValue;
|
|
szDataPtr++;
|
|
}
|
|
|
|
// If there were no decimal digits, we consider it failure
|
|
if(szDataPtr == szSaveDataPtr)
|
|
return NULL;
|
|
|
|
// Give the result
|
|
PtrValue[0] = TotalValue;
|
|
return szDataPtr;
|
|
}
|
|
|
|
static const char * CaptureSingleString(const char * szDataPtr, const char * szDataEnd, char * szBuffer, size_t ccBuffer)
|
|
{
|
|
char * szBufferEnd = szBuffer + ccBuffer - 1;
|
|
|
|
// Skip all whitespaces
|
|
while (szDataPtr < szDataEnd && IsWhiteSpace(szDataPtr))
|
|
szDataPtr++;
|
|
|
|
// Copy the string
|
|
while (szDataPtr < szDataEnd && szBuffer < szBufferEnd && !IsWhiteSpace(szDataPtr) && szDataPtr[0] != '=')
|
|
*szBuffer++ = *szDataPtr++;
|
|
|
|
szBuffer[0] = 0;
|
|
return szDataPtr;
|
|
}
|
|
|
|
static const char * CaptureSingleHash(const char * szDataPtr, const char * szDataEnd, LPBYTE HashValue, size_t HashLength)
|
|
{
|
|
const char * szHashString;
|
|
size_t HashStringLength = HashLength * 2;
|
|
|
|
// Skip all whitespaces
|
|
while (szDataPtr < szDataEnd && IsWhiteSpace(szDataPtr))
|
|
szDataPtr++;
|
|
szHashString = szDataPtr;
|
|
|
|
// Count all hash characters
|
|
for (size_t i = 0; i < HashStringLength; i++)
|
|
{
|
|
if (szDataPtr >= szDataEnd || isxdigit(szDataPtr[0]) == 0)
|
|
return NULL;
|
|
szDataPtr++;
|
|
}
|
|
|
|
// There must be a separator or end-of-string
|
|
if (szDataPtr > szDataEnd || IsWhiteSpace(szDataPtr) == false)
|
|
return NULL;
|
|
|
|
// Give the values
|
|
ConvertStringToBinary(szHashString, HashStringLength, HashValue);
|
|
return szDataPtr;
|
|
}
|
|
|
|
static const char * CaptureHashCount(const char * szDataPtr, const char * szDataEnd, size_t * PtrHashCount)
|
|
{
|
|
BYTE HashValue[MD5_HASH_SIZE];
|
|
size_t HashCount = 0;
|
|
|
|
// Capculate the hash count
|
|
while (szDataPtr < szDataEnd)
|
|
{
|
|
// Check one hash
|
|
szDataPtr = CaptureSingleHash(szDataPtr, szDataEnd, HashValue, MD5_HASH_SIZE);
|
|
if (szDataPtr == NULL)
|
|
return NULL;
|
|
|
|
// Skip all whitespaces
|
|
while (szDataPtr < szDataEnd && IsWhiteSpace(szDataPtr))
|
|
szDataPtr++;
|
|
|
|
HashCount++;
|
|
}
|
|
|
|
// Give results
|
|
PtrHashCount[0] = HashCount;
|
|
return szDataPtr;
|
|
}
|
|
|
|
static DWORD GetLocaleValue(LPCSTR szTag)
|
|
{
|
|
DWORD Language = 0;
|
|
|
|
// Convert the string language to integer
|
|
Language = (Language << 0x08) | szTag[0];
|
|
Language = (Language << 0x08) | szTag[1];
|
|
Language = (Language << 0x08) | szTag[2];
|
|
Language = (Language << 0x08) | szTag[3];
|
|
|
|
// Language-specific action
|
|
switch(Language)
|
|
{
|
|
case 0x656e5553: return CASC_LOCALE_ENUS;
|
|
case 0x656e4742: return CASC_LOCALE_ENGB;
|
|
case 0x656e434e: return CASC_LOCALE_ENCN;
|
|
case 0x656e5457: return CASC_LOCALE_ENTW;
|
|
case 0x65734553: return CASC_LOCALE_ESES;
|
|
case 0x65734d58: return CASC_LOCALE_ESMX;
|
|
case 0x70744252: return CASC_LOCALE_PTBR;
|
|
case 0x70745054: return CASC_LOCALE_PTPT;
|
|
case 0x7a68434e: return CASC_LOCALE_ZHCN;
|
|
case 0x7a685457: return CASC_LOCALE_ZHTW;
|
|
case 0x6b6f4b52: return CASC_LOCALE_KOKR;
|
|
case 0x66724652: return CASC_LOCALE_FRFR;
|
|
case 0x64654445: return CASC_LOCALE_DEDE;
|
|
case 0x72755255: return CASC_LOCALE_RURU;
|
|
case 0x69744954: return CASC_LOCALE_ITIT;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static bool CheckConfigFileVariable(
|
|
TCascStorage * hs, // Pointer to storage structure
|
|
const char * szLinePtr, // Pointer to the begin of the line
|
|
const char * szLineEnd, // Pointer to the end of the line
|
|
const char * szVarName, // Pointer to the variable to check
|
|
PARSE_VARIABLE PfnParseProc, // Pointer to the parsing function
|
|
void * pvParseParam) // Pointer to the parameter passed to parsing function
|
|
{
|
|
char szVariableName[MAX_VAR_NAME];
|
|
|
|
// Capture the variable from the line
|
|
szLinePtr = CaptureSingleString(szLinePtr, szLineEnd, szVariableName, sizeof(szVariableName));
|
|
if (szLinePtr == NULL)
|
|
return false;
|
|
|
|
// Verify whether this is the variable
|
|
if (!CascCheckWildCard(szVariableName, szVarName))
|
|
return false;
|
|
|
|
// Skip the spaces and '='
|
|
while (szLinePtr < szLineEnd && (IsWhiteSpace(szLinePtr) || szLinePtr[0] == '='))
|
|
szLinePtr++;
|
|
|
|
// Call the parsing function only if there is some data
|
|
if (szLinePtr >= szLineEnd)
|
|
return false;
|
|
|
|
return (PfnParseProc(hs, szVariableName, szLinePtr, szLineEnd, pvParseParam) == ERROR_SUCCESS);
|
|
}
|
|
|
|
static DWORD LoadHashArray(
|
|
PQUERY_KEY pBlob,
|
|
const char * szLinePtr,
|
|
const char * szLineEnd,
|
|
size_t HashCount)
|
|
{
|
|
DWORD dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
|
|
|
|
// Allocate the blob buffer
|
|
pBlob->cbData = (DWORD)(HashCount * MD5_HASH_SIZE);
|
|
pBlob->pbData = CASC_ALLOC<BYTE>(pBlob->cbData);
|
|
if (pBlob->pbData != NULL)
|
|
{
|
|
LPBYTE pbBuffer = pBlob->pbData;
|
|
|
|
for (size_t i = 0; i < HashCount; i++)
|
|
{
|
|
// Capture the hash value
|
|
szLinePtr = CaptureSingleHash(szLinePtr, szLineEnd, pbBuffer, MD5_HASH_SIZE);
|
|
if (szLinePtr == NULL)
|
|
return ERROR_BAD_FORMAT;
|
|
|
|
// Move buffer
|
|
pbBuffer += MD5_HASH_SIZE;
|
|
}
|
|
|
|
dwErrCode = ERROR_SUCCESS;
|
|
}
|
|
|
|
return dwErrCode;
|
|
}
|
|
|
|
static DWORD LoadMultipleHashes(PQUERY_KEY pBlob, const char * szLineBegin, const char * szLineEnd)
|
|
{
|
|
size_t HashCount = 0;
|
|
DWORD dwErrCode = ERROR_SUCCESS;
|
|
|
|
// Retrieve the hash count
|
|
if (CaptureHashCount(szLineBegin, szLineEnd, &HashCount) == NULL)
|
|
return ERROR_BAD_FORMAT;
|
|
|
|
// Do nothing if there is no data
|
|
if(HashCount != 0)
|
|
{
|
|
dwErrCode = LoadHashArray(pBlob, szLineBegin, szLineEnd, HashCount);
|
|
}
|
|
|
|
return dwErrCode;
|
|
}
|
|
|
|
// Loads a query key from the text form
|
|
// A QueryKey is an array of "ContentKey EncodedKey1 ... EncodedKeyN"
|
|
static DWORD LoadQueryKey(TCascStorage * /* hs */, const char * /* szVariableName */, const char * szDataBegin, const char * szDataEnd, void * pvParam)
|
|
{
|
|
return LoadMultipleHashes((PQUERY_KEY)pvParam, szDataBegin, szDataEnd);
|
|
}
|
|
|
|
static DWORD LoadCKeyEntry(TCascStorage * hs, const char * szVariableName, const char * szDataPtr, const char * szDataEnd, void * pvParam)
|
|
{
|
|
PCASC_CKEY_ENTRY pCKeyEntry = (PCASC_CKEY_ENTRY)pvParam;
|
|
size_t nLength = strlen(szVariableName);
|
|
size_t HashCount = 0;
|
|
|
|
// Ignore "xxx-config" items
|
|
if(StringEndsWith(szVariableName, nLength, "-config", 7))
|
|
return ERROR_SUCCESS;
|
|
|
|
// If the variable ends at "-size", it means we need to capture the size
|
|
if(StringEndsWith(szVariableName, nLength, "-size", 5))
|
|
{
|
|
DWORD ContentSize = CASC_INVALID_SIZE;
|
|
DWORD EncodedSize = CASC_INVALID_SIZE;
|
|
|
|
// Load the content size
|
|
szDataPtr = CaptureDecimalInteger(szDataPtr, szDataEnd, &ContentSize);
|
|
if(szDataPtr == NULL)
|
|
return ERROR_BAD_FORMAT;
|
|
|
|
CaptureDecimalInteger(szDataPtr, szDataEnd, &EncodedSize);
|
|
pCKeyEntry->ContentSize = ContentSize;
|
|
pCKeyEntry->EncodedSize = EncodedSize;
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
// If the string doesn't end with "-config", assume it's the CKey/EKey
|
|
else
|
|
{
|
|
// Get the number of hashes. It is expected to be 1 or 2
|
|
if(CaptureHashCount(szDataPtr, szDataEnd, &HashCount) != NULL)
|
|
{
|
|
// Capture the CKey
|
|
if(HashCount >= 1)
|
|
{
|
|
// Load the CKey. This should alway be there
|
|
szDataPtr = CaptureSingleHash(szDataPtr, szDataEnd, pCKeyEntry->CKey, MD5_HASH_SIZE);
|
|
if(szDataPtr == NULL)
|
|
return ERROR_BAD_FORMAT;
|
|
pCKeyEntry->Flags |= CASC_CE_HAS_CKEY;
|
|
}
|
|
|
|
// Capture EKey, if any
|
|
if(HashCount == 2)
|
|
{
|
|
// Load the EKey into the structure
|
|
szDataPtr = CaptureSingleHash(szDataPtr, szDataEnd, pCKeyEntry->EKey, MD5_HASH_SIZE);
|
|
if(szDataPtr == NULL)
|
|
return ERROR_BAD_FORMAT;
|
|
pCKeyEntry->Flags |= CASC_CE_HAS_EKEY;
|
|
|
|
// Increment the number of EKey entries loaded from text build file
|
|
hs->EKeyEntries++;
|
|
}
|
|
|
|
return (HashCount == 1 || HashCount == 2) ? ERROR_SUCCESS : ERROR_BAD_FORMAT;
|
|
}
|
|
}
|
|
|
|
// Unrecognized
|
|
return ERROR_BAD_FORMAT;
|
|
}
|
|
|
|
// For files like PATCH, which only contain EKey in the build file
|
|
static DWORD LoadEKeyEntry(TCascStorage * hs, const char * szVariableName, const char * szDataPtr, const char * szDataEnd, void * pvParam)
|
|
{
|
|
PCASC_CKEY_ENTRY pCKeyEntry = (PCASC_CKEY_ENTRY)pvParam;
|
|
DWORD dwErrCode;
|
|
|
|
// Load the entry as if it was a CKey. Then move CKey into EKey and adjust flags
|
|
if((dwErrCode = LoadCKeyEntry(hs, szVariableName, szDataPtr, szDataEnd, pvParam)) == ERROR_SUCCESS)
|
|
{
|
|
if((pCKeyEntry->Flags & (CASC_CE_HAS_CKEY | CASC_CE_HAS_EKEY | CASC_CE_HAS_EKEY_PARTIAL)) == CASC_CE_HAS_CKEY)
|
|
{
|
|
// Move the CKey into EKey
|
|
CopyMemory16(pCKeyEntry->EKey, pCKeyEntry->CKey);
|
|
ZeroMemory16(pCKeyEntry->CKey);
|
|
|
|
// Adjust flags
|
|
pCKeyEntry->Flags = (pCKeyEntry->Flags & ~CASC_CE_HAS_CKEY) | CASC_CE_HAS_EKEY;
|
|
}
|
|
}
|
|
|
|
return dwErrCode;
|
|
}
|
|
|
|
static DWORD LoadVfsRootEntry(TCascStorage * hs, const char * szVariableName, const char * szDataPtr, const char * szDataEnd, void * pvParam)
|
|
{
|
|
PCASC_CKEY_ENTRY pCKeyEntry;
|
|
CASC_ARRAY * pArray = (CASC_ARRAY *)pvParam;
|
|
const char * szVarPtr = szVariableName;
|
|
const char * szVarEnd = szVarPtr + strlen(szVarPtr);
|
|
DWORD VfsRootIndex = CASC_INVALID_INDEX;
|
|
|
|
// Skip the "vfs-" part
|
|
if (!strncmp(szVariableName, "vfs-", 4))
|
|
{
|
|
// Then, there must be a decimal number as index
|
|
if ((szVarPtr = CaptureDecimalInteger(szVarPtr + 4, szVarEnd, &VfsRootIndex)) != NULL)
|
|
{
|
|
// We expect the array to be initialized
|
|
assert(pArray->IsInitialized());
|
|
|
|
// The index of the key must not be NULL
|
|
if(VfsRootIndex != 0)
|
|
{
|
|
// Make sure that he entry is in the array
|
|
pCKeyEntry = (PCASC_CKEY_ENTRY)pArray->ItemAt(VfsRootIndex - 1);
|
|
if(pCKeyEntry == NULL)
|
|
{
|
|
// Insert a new entry
|
|
pCKeyEntry = (PCASC_CKEY_ENTRY)pArray->InsertAt(VfsRootIndex - 1);
|
|
if(pCKeyEntry == NULL)
|
|
return ERROR_NOT_ENOUGH_MEMORY;
|
|
|
|
// Initialize the new entry
|
|
pCKeyEntry->Init();
|
|
}
|
|
|
|
// Call just a normal parse function
|
|
return LoadCKeyEntry(hs, szVariableName, szDataPtr, szDataEnd, pCKeyEntry);
|
|
}
|
|
}
|
|
}
|
|
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
static DWORD LoadBuildProductId(TCascStorage * hs, const char * /* szVariableName */, const char * szDataBegin, const char * szDataEnd, void * /* pvParam */)
|
|
{
|
|
size_t nLength = (szDataEnd - szDataBegin);
|
|
|
|
if(hs->szCodeName == NULL)
|
|
{
|
|
if((hs->szCodeName = CASC_ALLOC<TCHAR>(nLength + 1)) != NULL)
|
|
{
|
|
CascStrCopy(hs->szCodeName, nLength + 1, szDataBegin, nLength);
|
|
}
|
|
}
|
|
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
// "B29049"
|
|
// "WOW-18125patch6.0.1"
|
|
// "30013_Win32_2_2_0_Ptr_ptr"
|
|
// "prometheus-0_8_0_0-24919"
|
|
static DWORD LoadBuildNumber(TCascStorage * hs, const char * /* szVariableName */, const char * szDataBegin, const char * szDataEnd, void * /* pvParam */)
|
|
{
|
|
DWORD dwBuildNumber = 0;
|
|
DWORD dwMaxValue = 0;
|
|
|
|
// Parse the string and take the largest decimal numeric value
|
|
// "build-name = 1.21.5.4037-retail"
|
|
while(szDataBegin < szDataEnd)
|
|
{
|
|
// There must be at least three digits (build 99 anyone?)
|
|
if(IsCharDigit(szDataBegin[0]))
|
|
{
|
|
dwBuildNumber = (dwBuildNumber * 10) + (szDataBegin[0] - '0');
|
|
dwMaxValue = CASCLIB_MAX(dwBuildNumber, dwMaxValue);
|
|
}
|
|
else
|
|
{
|
|
// Reset build number when we find non-digit char
|
|
dwBuildNumber = 0;
|
|
}
|
|
|
|
// Move to the next
|
|
szDataBegin++;
|
|
}
|
|
|
|
// If not there, just take value from build file
|
|
if((hs->dwBuildNumber = dwMaxValue) == 0)
|
|
hs->dwBuildNumber = hs->CdnBuildKey.pbData[0] % 100;
|
|
return ERROR_BAD_FORMAT;
|
|
}
|
|
|
|
static int LoadQueryKey(const CASC_CSV_COLUMN & Column, QUERY_KEY & Key)
|
|
{
|
|
// Check the input data
|
|
if (Column.szValue == NULL)
|
|
return ERROR_BUFFER_OVERFLOW;
|
|
if (Column.nLength != MD5_STRING_SIZE)
|
|
return ERROR_BAD_FORMAT;
|
|
|
|
return LoadHashArray(&Key, Column.szValue, Column.szValue + Column.nLength, 1);
|
|
}
|
|
|
|
static void SetProductCodeName(TCascStorage * hs, LPCSTR szCodeName)
|
|
{
|
|
if(hs->szCodeName == NULL && szCodeName != NULL)
|
|
{
|
|
hs->szCodeName = CascNewStrA2T(szCodeName);
|
|
}
|
|
}
|
|
|
|
static DWORD GetDefaultCdnPath(TCascStorage * hs, const CASC_CSV_COLUMN & Column)
|
|
{
|
|
if(hs->szCdnPath == NULL && Column.nLength != 0)
|
|
{
|
|
hs->szCdnPath = CascNewStrA2T(Column.szValue);
|
|
}
|
|
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
static DWORD GetDefaultLocaleMask(TCascStorage * hs, const CASC_CSV_COLUMN & Column)
|
|
{
|
|
LPCSTR szTagEnd = Column.szValue + Column.nLength - 4;
|
|
LPCSTR szTagPtr = Column.szValue;
|
|
DWORD dwLocaleMask = 0;
|
|
|
|
while(szTagPtr < szTagEnd)
|
|
{
|
|
DWORD dwLocaleValue = GetLocaleValue(szTagPtr);
|
|
|
|
if(dwLocaleValue != 0)
|
|
{
|
|
dwLocaleMask = dwLocaleMask | GetLocaleValue(szTagPtr);
|
|
szTagPtr += 4;
|
|
}
|
|
else
|
|
{
|
|
szTagPtr++;
|
|
}
|
|
}
|
|
|
|
hs->dwDefaultLocale = dwLocaleMask;
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
static DWORD ParseFile_CDNS(TCascStorage * hs, CASC_CSV & Csv)
|
|
{
|
|
const char * szWantedRegion = hs->szRegion;
|
|
size_t nLineCount;
|
|
|
|
// Fix the region
|
|
if(szWantedRegion == NULL || !_stricmp(szWantedRegion, "beta") || !_stricmp(szWantedRegion, "xx"))
|
|
szWantedRegion = "us";
|
|
|
|
// Determine the row count
|
|
nLineCount = Csv.GetLineCount();
|
|
|
|
// Find the active config
|
|
for (size_t i = 0; i < nLineCount; i++)
|
|
{
|
|
// Is it the version we are looking for?
|
|
if(!strcmp(Csv[i]["Name!STRING:0"].szValue, szWantedRegion))
|
|
{
|
|
// Save the list of CDN servers
|
|
hs->szCdnServers = CascNewStrA2T(Csv[i]["Hosts!STRING:0"].szValue);
|
|
|
|
// Save the CDN subpath
|
|
hs->szCdnPath = CascNewStrA2T(Csv[i]["Path!STRING:0"].szValue);
|
|
|
|
// Check and return result
|
|
return (hs->szCdnServers && hs->szCdnPath) ? ERROR_SUCCESS : ERROR_BAD_FORMAT;
|
|
}
|
|
}
|
|
|
|
return ERROR_FILE_NOT_FOUND;
|
|
}
|
|
|
|
static DWORD ParseFile_BuildInfo(TCascStorage * hs, CASC_CSV & Csv)
|
|
{
|
|
PFNPRODUCTCALLBACK PfnProductCallback = hs->pArgs->PfnProductCallback;
|
|
LPCSTR szProductColumn = "Product!STRING:0";
|
|
LPCSTR szCodeName;
|
|
void * PtrProductParam = hs->pArgs->PtrProductParam;
|
|
size_t nProductColumnIndex = Csv.GetColumnIndex(szProductColumn);
|
|
size_t nLineCount = Csv.GetLineCount();
|
|
size_t nSelected = CASC_INVALID_INDEX;
|
|
DWORD dwErrCode;
|
|
|
|
//
|
|
// Find the configuration that we're gonna load.
|
|
//
|
|
|
|
// If the product is not specified and there is product callback, we use the callback to specify the product
|
|
if(hs->szCodeName == NULL && nProductColumnIndex != CASC_INVALID_INDEX && PfnProductCallback != NULL)
|
|
{
|
|
LPCSTR ProductsList[0x40] = {NULL};
|
|
size_t nProductCount = 0;
|
|
size_t nChoiceIndex = CASC_INVALID_INDEX;
|
|
size_t nDefault = CASC_INVALID_INDEX;
|
|
|
|
// Load all products to the array
|
|
for(size_t i = 0; i < nLineCount; i++)
|
|
{
|
|
// Ignore anything that is not marked "active"
|
|
if(!strcmp(Csv[i]["Active!DEC:1"].szValue, "1"))
|
|
{
|
|
ProductsList[nProductCount] = Csv[i]["Product!STRING:0"].szValue;
|
|
nProductCount++;
|
|
nDefault = i;
|
|
}
|
|
}
|
|
|
|
// Only if there is more than one active products
|
|
if(nProductCount > 1)
|
|
{
|
|
// Ask the callback to choose the product
|
|
if(!PfnProductCallback(PtrProductParam, ProductsList, nProductCount, &nChoiceIndex) || (nChoiceIndex >= nProductCount))
|
|
return ERROR_CANCELLED;
|
|
|
|
// We now have preferred product to open
|
|
SetProductCodeName(hs, ProductsList[nChoiceIndex]);
|
|
}
|
|
else if(nProductCount == 1)
|
|
{
|
|
// We now have preferred product to open
|
|
SetProductCodeName(hs, ProductsList[nDefault]);
|
|
}
|
|
else
|
|
{
|
|
return ERROR_FILE_NOT_FOUND;
|
|
}
|
|
}
|
|
|
|
// Parse all lines in the CSV file. Either take the first that is active
|
|
// or take the one that has been selected by the callback
|
|
for(size_t i = 0; i < nLineCount; i++)
|
|
{
|
|
// Ignore anything that is not marked "active"
|
|
if(!strcmp(Csv[i]["Active!DEC:1"].szValue, "1"))
|
|
{
|
|
// If the product code is specified and exists in the build file, we need to check it
|
|
if(hs->szCodeName != NULL && (szCodeName = Csv[i]["Product!STRING:0"].szValue) != NULL)
|
|
{
|
|
TCHAR szBuffer[0x20];
|
|
|
|
// Skip if they are not equal
|
|
CascStrCopy(szBuffer, _countof(szBuffer), szCodeName);
|
|
if(_tcsicmp(hs->szCodeName, szBuffer))
|
|
continue;
|
|
|
|
// Save the code name of the selected product
|
|
SetProductCodeName(hs, Csv[i]["Product!STRING:0"].szValue);
|
|
nSelected = i;
|
|
break;
|
|
}
|
|
|
|
// If the caller didn't specify the product code name, pick the first active
|
|
nSelected = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Load the selected product
|
|
if(nSelected < nLineCount)
|
|
{
|
|
// Extract the CDN build key
|
|
dwErrCode = LoadQueryKey(Csv[nSelected]["Build Key!HEX:16"], hs->CdnBuildKey);
|
|
if (dwErrCode != ERROR_SUCCESS)
|
|
return dwErrCode;
|
|
|
|
// Extract the CDN config key
|
|
dwErrCode = LoadQueryKey(Csv[nSelected]["CDN Key!HEX:16"], hs->CdnConfigKey);
|
|
if (dwErrCode != ERROR_SUCCESS)
|
|
return dwErrCode;
|
|
|
|
// Get the CDN path
|
|
GetDefaultCdnPath(hs, Csv[nSelected]["CDN Path!STRING:0"]);
|
|
|
|
// If we found tags, we can extract language build from it
|
|
GetDefaultLocaleMask(hs, Csv[nSelected]["Tags!STRING:0"]);
|
|
|
|
// If we found version, extract a build number
|
|
const CASC_CSV_COLUMN & VerColumn = Csv[nSelected]["Version!STRING:0"];
|
|
if(VerColumn.szValue && VerColumn.nLength)
|
|
{
|
|
LoadBuildNumber(hs, NULL, VerColumn.szValue, VerColumn.szValue + VerColumn.nLength, NULL);
|
|
}
|
|
|
|
// Verify all variables
|
|
return (hs->CdnBuildKey.pbData != NULL && hs->CdnConfigKey.pbData != NULL) ? ERROR_SUCCESS : ERROR_BAD_FORMAT;
|
|
}
|
|
|
|
return ERROR_FILE_NOT_FOUND;
|
|
}
|
|
|
|
static DWORD ParseFile_VersionsDb(TCascStorage * hs, CASC_CSV & Csv)
|
|
{
|
|
size_t nLineCount = Csv.GetLineCount();
|
|
DWORD dwErrCode = ERROR_SUCCESS;
|
|
|
|
// Find the active config
|
|
for (size_t i = 0; i < nLineCount; i++)
|
|
{
|
|
// Either take the version required or take the first one
|
|
if (hs->szRegion == NULL || !strcmp(Csv[i]["Region!STRING:0"].szValue, hs->szRegion))
|
|
{
|
|
// Extract the CDN build key
|
|
dwErrCode = LoadQueryKey(Csv[i]["BuildConfig!HEX:16"], hs->CdnBuildKey);
|
|
if (dwErrCode != ERROR_SUCCESS)
|
|
return dwErrCode;
|
|
|
|
// Extract the CDN config key
|
|
dwErrCode = LoadQueryKey(Csv[i]["CDNConfig!HEX:16"], hs->CdnConfigKey);
|
|
if (dwErrCode != ERROR_SUCCESS)
|
|
return dwErrCode;
|
|
|
|
const CASC_CSV_COLUMN & VerColumn = Csv[i]["VersionsName!String:0"];
|
|
if (VerColumn.szValue && VerColumn.nLength)
|
|
{
|
|
LoadBuildNumber(hs, NULL, VerColumn.szValue, VerColumn.szValue + VerColumn.nLength, NULL);
|
|
}
|
|
|
|
// Verify all variables
|
|
if(hs->CdnBuildKey.pbData != NULL && hs->CdnConfigKey.pbData != NULL)
|
|
{
|
|
// If we have manually given build key, override the value
|
|
if(hs->szBuildKey != NULL)
|
|
dwErrCode = ConvertStringToBinary(hs->szBuildKey, MD5_STRING_SIZE, hs->CdnBuildKey.pbData);
|
|
return dwErrCode;
|
|
}
|
|
|
|
return ERROR_BAD_FORMAT;
|
|
}
|
|
}
|
|
|
|
return ERROR_FILE_NOT_FOUND;
|
|
}
|
|
|
|
static DWORD ParseFile_BuildDb(TCascStorage * hs, CASC_CSV & Csv)
|
|
{
|
|
DWORD dwErrCode;
|
|
|
|
// Extract the CDN build key
|
|
dwErrCode = LoadQueryKey(Csv[CSV_ZERO][CSV_ZERO], hs->CdnBuildKey);
|
|
if(dwErrCode != ERROR_SUCCESS)
|
|
return dwErrCode;
|
|
|
|
// Extract the CDN config key
|
|
dwErrCode = LoadQueryKey(Csv[CSV_ZERO][1], hs->CdnConfigKey);
|
|
if (dwErrCode != ERROR_SUCCESS)
|
|
return dwErrCode;
|
|
|
|
// Verify all variables
|
|
return (hs->CdnBuildKey.pbData != NULL && hs->CdnConfigKey.pbData != NULL) ? ERROR_SUCCESS : ERROR_BAD_FORMAT;
|
|
}
|
|
|
|
static DWORD ParseFile_CdnConfig(TCascStorage * hs, void * pvListFile)
|
|
{
|
|
const char * szLineBegin;
|
|
const char * szLineEnd;
|
|
DWORD dwErrCode = ERROR_SUCCESS;
|
|
|
|
// Keep parsing the listfile while there is something in there
|
|
for(;;)
|
|
{
|
|
// Get the next line
|
|
if(!ListFile_GetNextLine(pvListFile, &szLineBegin, &szLineEnd))
|
|
break;
|
|
|
|
// CDN key of ARCHIVE-GROUP. Archive-group is actually a very special '.index' file.
|
|
// It is essentially a merger of all .index files, with a structure change
|
|
// When ".index" added after the ARCHIVE-GROUP, we get file name in "indices" folder
|
|
if(CheckConfigFileVariable(hs, szLineBegin, szLineEnd, "archive-group", LoadQueryKey, &hs->ArchiveGroup))
|
|
continue;
|
|
|
|
// CDN key of all archives. When ".index" added to the string, we get file name in "indices" folder
|
|
if(CheckConfigFileVariable(hs, szLineBegin, szLineEnd, "archives", LoadQueryKey, &hs->ArchivesKey))
|
|
continue;
|
|
|
|
// CDN keys of patch archives (needs research)
|
|
if(CheckConfigFileVariable(hs, szLineBegin, szLineEnd, "patch-archives", LoadQueryKey, &hs->PatchArchivesKey))
|
|
continue;
|
|
|
|
// CDN key of probably the combined patch index file (needs research)
|
|
if(CheckConfigFileVariable(hs, szLineBegin, szLineEnd, "patch-archive-group", LoadQueryKey, &hs->PatchArchivesGroup))
|
|
continue;
|
|
|
|
// List of build configs this config supports (optional)
|
|
// Points to file: "data\config\%02X\%02X\%s
|
|
if(CheckConfigFileVariable(hs, szLineBegin, szLineEnd, "builds", LoadQueryKey, &hs->BuildFiles))
|
|
continue;
|
|
}
|
|
|
|
// Check if all required fields are present
|
|
if(hs->ArchivesKey.pbData == NULL || hs->ArchivesKey.cbData == 0)
|
|
return ERROR_BAD_FORMAT;
|
|
|
|
return dwErrCode;
|
|
}
|
|
|
|
static DWORD ParseFile_CdnBuild(TCascStorage * hs, void * pvListFile)
|
|
{
|
|
const char * szLineBegin;
|
|
const char * szLineEnd = NULL;
|
|
DWORD dwErrCode;
|
|
|
|
// Initialize the empty VFS array
|
|
dwErrCode = hs->VfsRootList.Create<CASC_CKEY_ENTRY>(0x10);
|
|
if (dwErrCode != ERROR_SUCCESS)
|
|
return dwErrCode;
|
|
|
|
// Parse all variables
|
|
while(ListFile_GetNextLine(pvListFile, &szLineBegin, &szLineEnd) != 0)
|
|
{
|
|
// Product name and build name
|
|
if(CheckConfigFileVariable(hs, szLineBegin, szLineEnd, "build-uid", LoadBuildProductId, NULL))
|
|
continue;
|
|
if(CheckConfigFileVariable(hs, szLineBegin, szLineEnd, "build-name", LoadBuildNumber, NULL))
|
|
continue;
|
|
|
|
// Content key of the ROOT file. Look this up in ENCODING to get the encoded key
|
|
if(CheckConfigFileVariable(hs, szLineBegin, szLineEnd, "root*", LoadCKeyEntry, &hs->RootFile))
|
|
continue;
|
|
|
|
// Content key [+ encoded key] of the INSTALL file
|
|
// If EKey is absent, you need to query the ENCODING file for it
|
|
if(CheckConfigFileVariable(hs, szLineBegin, szLineEnd, "install*", LoadCKeyEntry, &hs->InstallCKey))
|
|
continue;
|
|
|
|
// Content key [+ encoded key] of the DOWNLOAD file
|
|
// If EKey is absent, you need to query the ENCODING file for it
|
|
if(CheckConfigFileVariable(hs, szLineBegin, szLineEnd, "download*", LoadCKeyEntry, &hs->DownloadCKey))
|
|
continue;
|
|
|
|
// Content key + encoded key of the ENCODING file. Contains CKey+EKey
|
|
// If either none or 1 is found, the game (at least Wow) switches to plain-data(?). Seen in build 20173
|
|
if(CheckConfigFileVariable(hs, szLineBegin, szLineEnd, "encoding*", LoadCKeyEntry, &hs->EncodingCKey))
|
|
continue;
|
|
|
|
// PATCH file
|
|
if(CheckConfigFileVariable(hs, szLineBegin, szLineEnd, "patch*", LoadEKeyEntry, &hs->PatchFile))
|
|
continue;
|
|
|
|
// SIZE file
|
|
if(CheckConfigFileVariable(hs, szLineBegin, szLineEnd, "size*", LoadCKeyEntry, &hs->SizeFile))
|
|
continue;
|
|
|
|
// VFS root file (the root file of the storage VFS)
|
|
if(CheckConfigFileVariable(hs, szLineBegin, szLineEnd, "vfs-root*", LoadCKeyEntry, &hs->VfsRoot))
|
|
continue;
|
|
|
|
// Load a directory entry to the VFS
|
|
if(CheckConfigFileVariable(hs, szLineBegin, szLineEnd, "vfs-*", LoadVfsRootEntry, &hs->VfsRootList))
|
|
continue;
|
|
}
|
|
|
|
// Special case: Some builds of WoW (22267) don't have the variable in the build file
|
|
if(hs->szCodeName == NULL && hs->szCdnPath != NULL)
|
|
{
|
|
hs->szCodeName = CascNewStr(GetPlainFileName(hs->szCdnPath));
|
|
}
|
|
|
|
// Both CKey and EKey of ENCODING file is required
|
|
if((hs->EncodingCKey.Flags & (CASC_CE_HAS_CKEY | CASC_CE_HAS_EKEY)) != (CASC_CE_HAS_CKEY | CASC_CE_HAS_EKEY))
|
|
dwErrCode = ERROR_BAD_FORMAT;
|
|
return dwErrCode;
|
|
}
|
|
|
|
static DWORD CheckDataDirectory(TCascStorage * hs, LPTSTR szDirectory)
|
|
{
|
|
LPTSTR szDataPath;
|
|
DWORD dwErrCode = ERROR_FILE_NOT_FOUND;
|
|
|
|
// Try all known subdirectories
|
|
for(size_t i = 0; DataDirs[i] != NULL; i++)
|
|
{
|
|
// Create the eventual data path
|
|
szDataPath = CombinePath(szDirectory, DataDirs[i]);
|
|
if(szDataPath != NULL)
|
|
{
|
|
// Does that directory exist?
|
|
if(DirectoryExists(szDataPath))
|
|
{
|
|
hs->szRootPath = CascNewStr(szDirectory);
|
|
hs->szDataPath = szDataPath;
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
// Free the data path
|
|
CASC_FREE(szDataPath);
|
|
}
|
|
}
|
|
|
|
return dwErrCode;
|
|
}
|
|
|
|
static DWORD LoadCsvFile(TCascStorage * hs, LPCTSTR szFileName, PARSECSVFILE PfnParseProc, bool bHasHeader)
|
|
{
|
|
CASC_CSV Csv(0x40, bHasHeader);
|
|
DWORD dwErrCode;
|
|
|
|
// Load the external file to memory
|
|
if ((dwErrCode = Csv.Load(szFileName)) == ERROR_SUCCESS)
|
|
dwErrCode = PfnParseProc(hs, Csv);
|
|
return dwErrCode;
|
|
}
|
|
|
|
static LPCTSTR ExtractCdnServerName(LPTSTR szServerName, size_t cchServerName, LPCTSTR szCdnServers)
|
|
{
|
|
LPCTSTR szSeparator;
|
|
|
|
if(szCdnServers[0] != 0)
|
|
{
|
|
szSeparator = _tcschr(szCdnServers, _T(' '));
|
|
if(szSeparator != NULL)
|
|
{
|
|
// Copy one server name
|
|
CascStrCopy(szServerName, cchServerName, szCdnServers, (szSeparator - szCdnServers));
|
|
|
|
// Skip all separators
|
|
while(szSeparator[0] == ' ' || szSeparator[0] == 0x09)
|
|
szSeparator++;
|
|
return szSeparator;
|
|
}
|
|
else
|
|
{
|
|
CascStrCopy(szServerName, MAX_PATH, szCdnServers);
|
|
return szCdnServers + _tcslen(szCdnServers);
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void CreateRemoteAndLocalPath(TCascStorage * hs, CASC_CDN_DOWNLOAD & CdnsInfo, CASC_PATH<TCHAR> & RemotePath, CASC_PATH<TCHAR> & LocalPath)
|
|
{
|
|
PCASC_EKEY_ENTRY pEKeyEntry;
|
|
ULONGLONG ByteMask = 1;
|
|
DWORD ArchiveIndex;
|
|
|
|
// Append the CDN host / root folder
|
|
RemotePath.SetPathRoot(CdnsInfo.szCdnsHost);
|
|
RemotePath.AppendString(CdnsInfo.szCdnsPath, true);
|
|
LocalPath.SetPathRoot(hs->szRootPath);
|
|
|
|
// If there is an EKey, take EKey
|
|
if(CdnsInfo.pbEKey != NULL)
|
|
{
|
|
// The file is given by EKey. It's either a loose file, or it's stored in an archive.
|
|
// We check that using the EKey map
|
|
if ((pEKeyEntry = (PCASC_EKEY_ENTRY)hs->IndexMap.FindObject(CdnsInfo.pbEKey)) != NULL)
|
|
{
|
|
// Change the path type to "data"
|
|
RemotePath.AppendString(_T("data"), true);
|
|
LocalPath.AppendString(_T("data"), true);
|
|
|
|
// Append the EKey of the archive instead of the file itself
|
|
ArchiveIndex = (DWORD)(pEKeyEntry->StorageOffset >> hs->FileOffsetBits);
|
|
CdnsInfo.pbArchiveKey = hs->ArchivesKey.pbData + (MD5_HASH_SIZE * ArchiveIndex);
|
|
RemotePath.AppendEKey(CdnsInfo.pbArchiveKey);
|
|
LocalPath.AppendEKey(CdnsInfo.pbArchiveKey);
|
|
|
|
// Get the archive index and archive offset
|
|
CdnsInfo.ArchiveIndex = ArchiveIndex;
|
|
CdnsInfo.ArchiveOffs = pEKeyEntry->StorageOffset & ((ByteMask << hs->FileOffsetBits) - 1);
|
|
CdnsInfo.EncodedSize = pEKeyEntry->EncodedSize;
|
|
}
|
|
else
|
|
{
|
|
// Append the path type
|
|
RemotePath.AppendString(CdnsInfo.szPathType, true);
|
|
LocalPath.AppendString((CdnsInfo.szLoPaType != NULL) ? CdnsInfo.szLoPaType : CdnsInfo.szPathType, true);
|
|
|
|
// Append the EKey
|
|
RemotePath.AppendEKey(CdnsInfo.pbEKey);
|
|
LocalPath.AppendEKey(CdnsInfo.pbEKey);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
assert(CdnsInfo.szFileName != NULL);
|
|
RemotePath.AppendString(CdnsInfo.szFileName, true);
|
|
LocalPath.AppendString(CdnsInfo.szFileName, true);
|
|
}
|
|
|
|
// Append extension
|
|
RemotePath.AppendString(CdnsInfo.szExtension, false);
|
|
LocalPath.AppendString(CdnsInfo.szExtension, false);
|
|
}
|
|
|
|
static DWORD ForcePathExist(LPCTSTR szFileName, bool bIsFileName)
|
|
{
|
|
LPTSTR szLocalPath;
|
|
size_t nIndex;
|
|
bool bFirstSeparator = false;
|
|
DWORD dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
|
|
|
|
// Sanity checks
|
|
if(szFileName && szFileName[0])
|
|
{
|
|
szLocalPath = CascNewStr(szFileName);
|
|
if(szLocalPath != NULL)
|
|
{
|
|
// Get the end of search
|
|
if(bIsFileName)
|
|
CutLastPathPart(szLocalPath);
|
|
|
|
// Check the entire path
|
|
if(_taccess(szLocalPath, 0) != 0)
|
|
{
|
|
// Searth the entire path
|
|
for(nIndex = 0; szLocalPath[nIndex] != 0; nIndex++)
|
|
{
|
|
if(szLocalPath[nIndex] == '\\' || szLocalPath[nIndex] == '/')
|
|
{
|
|
// Cut the path and verify whether the folder/file exists
|
|
szLocalPath[nIndex] = 0;
|
|
|
|
// Skip the very first separator
|
|
if(bFirstSeparator == true)
|
|
{
|
|
// Is it there?
|
|
if(DirectoryExists(szLocalPath) == false && MakeDirectory(szLocalPath) == false)
|
|
{
|
|
dwErrCode = ERROR_PATH_NOT_FOUND;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Restore the character
|
|
szLocalPath[nIndex] = PATH_SEP_CHAR;
|
|
bFirstSeparator = true;
|
|
dwErrCode = ERROR_SUCCESS;
|
|
}
|
|
}
|
|
|
|
// Now check the final path
|
|
if(DirectoryExists(szLocalPath) || MakeDirectory(szLocalPath))
|
|
{
|
|
dwErrCode = ERROR_SUCCESS;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = ERROR_SUCCESS;
|
|
}
|
|
|
|
CASC_FREE(szLocalPath);
|
|
}
|
|
}
|
|
|
|
return dwErrCode;
|
|
}
|
|
|
|
static bool FileAlreadyExists(LPCTSTR szFileName)
|
|
{
|
|
TFileStream * pStream;
|
|
ULONGLONG FileSize = 0;
|
|
|
|
// The file open must succeed and also must be of non-zero size
|
|
if((pStream = FileStream_OpenFile(szFileName, 0)) != NULL)
|
|
{
|
|
FileStream_GetSize(pStream, &FileSize);
|
|
FileStream_Close(pStream);
|
|
}
|
|
|
|
return (FileSize != 0);
|
|
}
|
|
|
|
static DWORD DownloadFile(
|
|
LPCTSTR szRemoteName,
|
|
LPCTSTR szLocalName,
|
|
PULONGLONG PtrByteOffset,
|
|
DWORD cbReadSize,
|
|
DWORD dwPortFlags)
|
|
{
|
|
TFileStream * pRemStream;
|
|
TFileStream * pLocStream;
|
|
LPBYTE pbFileData;
|
|
DWORD dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
|
|
|
|
// Open the remote stream
|
|
pRemStream = FileStream_OpenFile(szRemoteName, BASE_PROVIDER_HTTP | STREAM_PROVIDER_FLAT | dwPortFlags);
|
|
if (pRemStream != NULL)
|
|
{
|
|
// Will we download the entire file or just a part of it?
|
|
if (PtrByteOffset == NULL)
|
|
{
|
|
ULONGLONG FileSize = 0;
|
|
|
|
// Retrieve the file size, but not longer than 1 GB
|
|
if (FileStream_GetSize(pRemStream, &FileSize) && 0 < FileSize && FileSize < 0x40000000)
|
|
{
|
|
// Cut the file size down to 32 bits
|
|
cbReadSize = (DWORD)FileSize;
|
|
}
|
|
}
|
|
|
|
// Shall we read something?
|
|
if ((cbReadSize != 0) && (pbFileData = CASC_ALLOC<BYTE>(cbReadSize)) != NULL)
|
|
{
|
|
// Read all required data from the remote file
|
|
if (FileStream_Read(pRemStream, PtrByteOffset, pbFileData, cbReadSize))
|
|
{
|
|
pLocStream = FileStream_CreateFile(szLocalName, BASE_PROVIDER_FILE | STREAM_PROVIDER_FLAT);
|
|
if (pLocStream != NULL)
|
|
{
|
|
if (FileStream_Write(pLocStream, NULL, pbFileData, cbReadSize))
|
|
dwErrCode = ERROR_SUCCESS;
|
|
|
|
FileStream_Close(pLocStream);
|
|
}
|
|
}
|
|
|
|
// Free the data buffer
|
|
CASC_FREE(pbFileData);
|
|
}
|
|
|
|
// Close the remote stream
|
|
FileStream_Close(pRemStream);
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = GetLastError();
|
|
}
|
|
|
|
return dwErrCode;
|
|
}
|
|
|
|
static DWORD DownloadFileFromCDN2(TCascStorage * hs, CASC_CDN_DOWNLOAD & CdnsInfo)
|
|
{
|
|
CASC_PATH<TCHAR> RemotePath(URL_SEP_CHAR);
|
|
CASC_PATH<TCHAR> LocalPath(PATH_SEP_CHAR);
|
|
DWORD dwPortFlags = (CdnsInfo.Flags & CASC_CDN_FLAG_PORT1119) ? STREAM_FLAG_USE_PORT_1119 : 0;
|
|
DWORD dwErrCode;
|
|
|
|
// Assemble both the remote and local path
|
|
assert(CdnsInfo.szCdnsHost != NULL && CdnsInfo.szCdnsHost[0] != 0);
|
|
CreateRemoteAndLocalPath(hs, CdnsInfo, RemotePath, LocalPath);
|
|
|
|
// Check whether the local file exists
|
|
if((CdnsInfo.Flags & CASC_CDN_FORCE_DOWNLOAD) || !FileAlreadyExists(LocalPath))
|
|
{
|
|
// Make sure that the path exists
|
|
dwErrCode = ForcePathExist(LocalPath, true);
|
|
if(dwErrCode != ERROR_SUCCESS)
|
|
return dwErrCode;
|
|
|
|
// Attempt to download the file
|
|
dwErrCode = DownloadFile(RemotePath, LocalPath, NULL, 0, dwPortFlags);
|
|
if(dwErrCode != ERROR_SUCCESS)
|
|
return dwErrCode;
|
|
}
|
|
|
|
// Give the path to the caller, if any
|
|
LocalPath.Copy(CdnsInfo.szLocalPath, CdnsInfo.ccLocalPath);
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
DWORD DownloadFileFromCDN(TCascStorage * hs, CASC_CDN_DOWNLOAD & CdnsInfo)
|
|
{
|
|
LPCTSTR szCdnServers = hs->szCdnServers;
|
|
TCHAR szCdnHost[MAX_PATH] = _T("");
|
|
DWORD dwErrCode = ERROR_CAN_NOT_COMPLETE;
|
|
|
|
// If we have a given CDN server, use it. If not, try all CDNs
|
|
// from the storage's configuration
|
|
if(CdnsInfo.szCdnsHost == NULL)
|
|
{
|
|
// Try all download servers
|
|
while((szCdnServers = ExtractCdnServerName(szCdnHost, _countof(szCdnHost), szCdnServers)) != NULL)
|
|
{
|
|
CdnsInfo.szCdnsHost = szCdnHost;
|
|
if((dwErrCode = DownloadFileFromCDN2(hs, CdnsInfo)) == ERROR_SUCCESS)
|
|
return ERROR_SUCCESS;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = DownloadFileFromCDN2(hs, CdnsInfo);
|
|
}
|
|
|
|
return dwErrCode;
|
|
}
|
|
|
|
static DWORD FetchAndLoadConfigFile(TCascStorage * hs, PQUERY_KEY pFileKey, PARSETEXTFILE PfnParseProc)
|
|
{
|
|
LPCTSTR szPathType = _T("config");
|
|
TCHAR szLocalPath[MAX_PATH];
|
|
TCHAR szSubDir[0x80] = _T("");
|
|
void * pvListFile = NULL;
|
|
DWORD dwErrCode = ERROR_CAN_NOT_COMPLETE;
|
|
|
|
// If online storage, we download the file. Otherwise, create a local path
|
|
if(hs->dwFeatures & CASC_FEATURE_ONLINE)
|
|
{
|
|
CASC_CDN_DOWNLOAD CdnsInfo = {0};
|
|
|
|
// Prepare the download structure for "%CDNS_HOST%/%CDNS_PATH%/##/##/EKey" file
|
|
CdnsInfo.szCdnsPath = hs->szCdnPath;
|
|
CdnsInfo.szPathType = szPathType;
|
|
CdnsInfo.pbEKey = pFileKey->pbData;
|
|
CdnsInfo.szLocalPath = szLocalPath;
|
|
CdnsInfo.ccLocalPath = _countof(szLocalPath);
|
|
|
|
// Download the config file
|
|
dwErrCode = DownloadFileFromCDN(hs, CdnsInfo);
|
|
if(dwErrCode != ERROR_SUCCESS)
|
|
return dwErrCode;
|
|
}
|
|
else
|
|
{
|
|
CASC_PATH<TCHAR> Path;
|
|
|
|
Path.AppendString(hs->szDataPath, false);
|
|
Path.AppendString(szSubDir, true);
|
|
Path.AppendString(szPathType, true);
|
|
Path.AppendEKey(pFileKey->pbData);
|
|
Path.Copy(szLocalPath, _countof(szLocalPath));
|
|
}
|
|
|
|
// Load and verify the external listfile
|
|
pvListFile = ListFile_OpenExternal(szLocalPath);
|
|
if (pvListFile != NULL)
|
|
{
|
|
if (ListFile_VerifyMD5(pvListFile, pFileKey->pbData))
|
|
{
|
|
dwErrCode = PfnParseProc(hs, pvListFile);
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = ERROR_FILE_CORRUPT;
|
|
}
|
|
CASC_FREE(pvListFile);
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = ERROR_FILE_NOT_FOUND;
|
|
}
|
|
|
|
return dwErrCode;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Public functions
|
|
|
|
bool InvokeProgressCallback(TCascStorage * hs, LPCSTR szMessage, LPCSTR szObject, DWORD CurrentValue, DWORD TotalValue)
|
|
{
|
|
PCASC_OPEN_STORAGE_ARGS pArgs = hs->pArgs;
|
|
bool bResult = false;
|
|
|
|
if(pArgs && pArgs->PfnProgressCallback)
|
|
bResult = pArgs->PfnProgressCallback(pArgs->PtrProgressParam, szMessage, szObject, CurrentValue, TotalValue);
|
|
return bResult;
|
|
}
|
|
|
|
DWORD GetFileSpanInfo(PCASC_CKEY_ENTRY pCKeyEntry, PULONGLONG PtrContentSize, PULONGLONG PtrEncodedSize)
|
|
{
|
|
ULONGLONG ContentSize = 0;
|
|
ULONGLONG EncodedSize = 0;
|
|
DWORD dwSpanCount = pCKeyEntry->SpanCount;
|
|
bool bContentSizeError = false;
|
|
bool bEncodedSizeError = false;
|
|
|
|
// Sanity check
|
|
assert(pCKeyEntry->SpanCount != 0);
|
|
|
|
// Sum the file size over all file spans
|
|
// Note: The first file span, if referenced by the ROOT folder, gets the same size
|
|
// like the entire file (example: zone\base.xpak, zone\base.xpak_1)
|
|
for(DWORD i = 0; i < dwSpanCount; i++, pCKeyEntry++)
|
|
{
|
|
if(pCKeyEntry->ContentSize == CASC_INVALID_SIZE)
|
|
bContentSizeError = true;
|
|
if(pCKeyEntry->EncodedSize == CASC_INVALID_SIZE)
|
|
bEncodedSizeError = true;
|
|
|
|
ContentSize = ContentSize + pCKeyEntry->ContentSize;
|
|
EncodedSize = EncodedSize + pCKeyEntry->EncodedSize;
|
|
}
|
|
|
|
// Reset the sizes if there was an error
|
|
PtrContentSize[0] = (bContentSizeError == false) ? ContentSize : CASC_INVALID_SIZE64;
|
|
PtrEncodedSize[0] = (bEncodedSizeError == false) ? EncodedSize : CASC_INVALID_SIZE64;
|
|
return dwSpanCount;
|
|
}
|
|
|
|
// Checks whether there is a ".build.info" or ".build.db".
|
|
// If yes, the function sets "szDataPath" and "szIndexPath"
|
|
// in the storage structure and returns ERROR_SUCCESS
|
|
DWORD CheckGameDirectory(TCascStorage * hs, LPTSTR szDirectory)
|
|
{
|
|
TFileStream * pStream;
|
|
LPTSTR szBuildFile;
|
|
DWORD dwErrCode = ERROR_FILE_NOT_FOUND;
|
|
|
|
// Try to find any of the root files used in the history
|
|
for (size_t i = 0; BuildTypes[i].szFileName != NULL; i++)
|
|
{
|
|
// Create the full name of the .agent.db file
|
|
szBuildFile = CombinePath(szDirectory, BuildTypes[i].szFileName);
|
|
if (szBuildFile != NULL)
|
|
{
|
|
// Attempt to open the file
|
|
pStream = FileStream_OpenFile(szBuildFile, STREAM_FLAG_READ_ONLY);
|
|
if (pStream != NULL)
|
|
{
|
|
// Free the stream
|
|
FileStream_Close(pStream);
|
|
|
|
// Check for the data directory
|
|
dwErrCode = CheckDataDirectory(hs, szDirectory);
|
|
if (dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
hs->szBuildFile = szBuildFile;
|
|
hs->BuildFileType = BuildTypes[i].BuildFileType;
|
|
return ERROR_SUCCESS;
|
|
}
|
|
}
|
|
|
|
CASC_FREE(szBuildFile);
|
|
}
|
|
}
|
|
|
|
return dwErrCode;
|
|
}
|
|
|
|
DWORD LoadBuildInfo(TCascStorage * hs)
|
|
{
|
|
PARSECSVFILE PfnParseProc = NULL;
|
|
DWORD dwErrCode;
|
|
bool bHasHeader = true;
|
|
|
|
// If the storage is online storage, we need to download "versions"
|
|
if(hs->dwFeatures & CASC_FEATURE_ONLINE)
|
|
{
|
|
CASC_CDN_DOWNLOAD CdnsInfo = {0};
|
|
TCHAR szLocalFile[MAX_PATH];
|
|
|
|
// Inform the user about loading the build.info/build.db/versions
|
|
if(InvokeProgressCallback(hs, "Downloading the \"versions\" file", NULL, 0, 0))
|
|
return ERROR_CANCELLED;
|
|
|
|
// Prepare the download structure for "us.patch.battle.net/%CODENAME%/versions" file
|
|
CdnsInfo.szCdnsHost = _T("us.patch.battle.net");
|
|
CdnsInfo.szCdnsPath = hs->szCodeName;
|
|
CdnsInfo.szPathType = _T("");
|
|
CdnsInfo.szFileName = _T("versions");
|
|
CdnsInfo.szLocalPath = szLocalFile;
|
|
CdnsInfo.ccLocalPath = _countof(szLocalFile);
|
|
CdnsInfo.Flags = CASC_CDN_FLAG_PORT1119 | CASC_CDN_FORCE_DOWNLOAD;
|
|
|
|
// Attempt to download the "versions" file
|
|
dwErrCode = DownloadFileFromCDN(hs, CdnsInfo);
|
|
if(dwErrCode != ERROR_SUCCESS)
|
|
return dwErrCode;
|
|
|
|
// Retrieve the name of the "versions" file
|
|
if((hs->szBuildFile = CascNewStr(szLocalFile)) == NULL)
|
|
return ERROR_NOT_ENOUGH_MEMORY;
|
|
}
|
|
|
|
// We support ".build.info", ".build.db" or "versions"
|
|
switch (hs->BuildFileType)
|
|
{
|
|
case CascBuildInfo:
|
|
PfnParseProc = ParseFile_BuildInfo;
|
|
break;
|
|
|
|
case CascVersionsDb:
|
|
PfnParseProc = ParseFile_VersionsDb;
|
|
break;
|
|
|
|
case CascBuildDb:
|
|
PfnParseProc = ParseFile_BuildDb;
|
|
bHasHeader = false;
|
|
break;
|
|
|
|
default:
|
|
return ERROR_NOT_SUPPORTED;
|
|
}
|
|
|
|
assert(hs->szBuildFile != NULL);
|
|
return LoadCsvFile(hs, hs->szBuildFile, PfnParseProc, bHasHeader);
|
|
}
|
|
|
|
DWORD LoadCdnsFile(TCascStorage * hs)
|
|
{
|
|
CASC_CDN_DOWNLOAD CdnsInfo = {0};
|
|
TCHAR szLocalPath[MAX_PATH];
|
|
DWORD dwErrCode = ERROR_SUCCESS;
|
|
|
|
// Sanity checks
|
|
assert(hs->dwFeatures & CASC_FEATURE_ONLINE);
|
|
|
|
// Inform the user about what we are doing
|
|
if(InvokeProgressCallback(hs, "Downloading the \"cdns\" file", NULL, 0, 0))
|
|
return ERROR_CANCELLED;
|
|
|
|
// Prepare the download structure
|
|
CdnsInfo.szCdnsHost = _T("us.patch.battle.net");
|
|
CdnsInfo.szCdnsPath = hs->szCodeName;
|
|
CdnsInfo.szPathType = _T("");
|
|
CdnsInfo.szFileName = _T("cdns");
|
|
CdnsInfo.szLocalPath = szLocalPath;
|
|
CdnsInfo.ccLocalPath = _countof(szLocalPath);
|
|
CdnsInfo.Flags = CASC_CDN_FLAG_PORT1119 | CASC_CDN_FORCE_DOWNLOAD;
|
|
|
|
// Download file and parse it
|
|
if((dwErrCode = DownloadFileFromCDN(hs, CdnsInfo)) == ERROR_SUCCESS)
|
|
{
|
|
dwErrCode = LoadCsvFile(hs, szLocalPath, ParseFile_CDNS, true);
|
|
}
|
|
|
|
return dwErrCode;
|
|
}
|
|
|
|
DWORD LoadCdnConfigFile(TCascStorage * hs)
|
|
{
|
|
// The CKey for the CDN config should have been loaded already
|
|
assert(hs->CdnConfigKey.pbData != NULL && hs->CdnConfigKey.cbData == MD5_HASH_SIZE);
|
|
|
|
// Inform the user about what we are doing
|
|
if(InvokeProgressCallback(hs, "Loading CDN config file", NULL, 0, 0))
|
|
return ERROR_CANCELLED;
|
|
|
|
// Load the CDN config file
|
|
return FetchAndLoadConfigFile(hs, &hs->CdnConfigKey, ParseFile_CdnConfig);
|
|
}
|
|
|
|
DWORD LoadCdnBuildFile(TCascStorage * hs)
|
|
{
|
|
// The CKey for the CDN config should have been loaded already
|
|
assert(hs->CdnBuildKey.pbData != NULL && hs->CdnBuildKey.cbData == MD5_HASH_SIZE);
|
|
|
|
// Inform the user about what we are doing
|
|
if(InvokeProgressCallback(hs, "Loading CDN build file", NULL, 0, 0))
|
|
return ERROR_CANCELLED;
|
|
|
|
// Load the CDN config file. Note that we don't
|
|
// need it for anything, really, so we don't care if it fails
|
|
return FetchAndLoadConfigFile(hs, &hs->CdnBuildKey, ParseFile_CdnBuild);
|
|
}
|
|
|
|
LPBYTE LoadInternalFileToMemory(TCascStorage * hs, PCASC_CKEY_ENTRY pCKeyEntry, DWORD * pcbFileData)
|
|
{
|
|
LPBYTE pbFileData = NULL;
|
|
HANDLE hFile = NULL;
|
|
DWORD dwFileSizeHi = 0;
|
|
DWORD cbFileData = 0;
|
|
DWORD dwBytesRead = 0;
|
|
DWORD dwErrCode = ERROR_SUCCESS;
|
|
|
|
// Open the file either by CKey or by EKey
|
|
if(OpenFileByCKeyEntry(hs, pCKeyEntry, CASC_STRICT_DATA_CHECK, &hFile))
|
|
{
|
|
// Make the file not cached. We always load the entire file to memory,
|
|
// so no need to cache (and needlessly copy from one buffer to another)
|
|
SetCacheStrategy(hFile, CascCacheNothing);
|
|
|
|
// Retrieve the size of the file. Note that the caller might specify
|
|
// the real size of the file, in case the file size is not retrievable
|
|
// or if the size is wrong. Example: ENCODING file has size specified in BUILD
|
|
if(pCKeyEntry->ContentSize == CASC_INVALID_SIZE)
|
|
{
|
|
cbFileData = CascGetFileSize(hFile, &dwFileSizeHi);
|
|
if(cbFileData == CASC_INVALID_SIZE || dwFileSizeHi != 0)
|
|
dwErrCode = ERROR_FILE_CORRUPT;
|
|
}
|
|
else
|
|
{
|
|
cbFileData = pCKeyEntry->ContentSize;
|
|
}
|
|
|
|
// Load the entire file to memory
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
// Allocate space for the ENCODING file
|
|
pbFileData = CASC_ALLOC<BYTE>(cbFileData);
|
|
if(pbFileData != NULL)
|
|
{
|
|
// Read the entire file to memory
|
|
CascReadFile(hFile, pbFileData, cbFileData, &dwBytesRead);
|
|
if(dwBytesRead != cbFileData)
|
|
{
|
|
dwErrCode = ERROR_FILE_CORRUPT;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
|
|
}
|
|
}
|
|
|
|
// Close the file
|
|
CascCloseFile(hFile);
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = GetLastError();
|
|
}
|
|
|
|
// Handle errors
|
|
if(dwErrCode != ERROR_SUCCESS)
|
|
{
|
|
// Free the file data
|
|
CASC_FREE(pbFileData);
|
|
cbFileData = 0;
|
|
|
|
// Set the last error
|
|
SetLastError(dwErrCode);
|
|
}
|
|
|
|
// Give the loaded file length
|
|
if(pcbFileData != NULL)
|
|
*pcbFileData = cbFileData;
|
|
return pbFileData;
|
|
}
|
|
|
|
LPBYTE LoadFileToMemory(LPCTSTR szFileName, DWORD * pcbFileData)
|
|
{
|
|
TFileStream * pStream;
|
|
ULONGLONG FileSize = 0;
|
|
LPBYTE pbFileData = NULL;
|
|
DWORD cbFileData = 0;
|
|
|
|
// Open the stream for read-only access and read the file
|
|
pStream = FileStream_OpenFile(szFileName, STREAM_FLAG_READ_ONLY | STREAM_FLAG_WRITE_SHARE | STREAM_PROVIDER_FLAT | BASE_PROVIDER_FILE);
|
|
if(pStream != NULL)
|
|
{
|
|
// Retrieve the file size
|
|
FileStream_GetSize(pStream, &FileSize);
|
|
cbFileData = (DWORD)FileSize;
|
|
|
|
// Do not load zero files or too larget files
|
|
if(0 < FileSize && FileSize <= 0x2000000)
|
|
{
|
|
// Allocate file data buffer. Make it 1 byte longer
|
|
// so string functions can put '\0' there
|
|
pbFileData = CASC_ALLOC<BYTE>(cbFileData + 1);
|
|
if(pbFileData != NULL)
|
|
{
|
|
if(!FileStream_Read(pStream, NULL, pbFileData, cbFileData))
|
|
{
|
|
CASC_FREE(pbFileData);
|
|
cbFileData = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SetLastError(ERROR_NOT_ENOUGH_MEMORY);
|
|
cbFileData = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SetLastError(ERROR_BAD_FORMAT);
|
|
cbFileData = 0;
|
|
assert(false);
|
|
}
|
|
|
|
// Close the file stream
|
|
FileStream_Close(pStream);
|
|
}
|
|
|
|
// Give out values
|
|
if(pcbFileData != NULL)
|
|
pcbFileData[0] = cbFileData;
|
|
return pbFileData;
|
|
}
|
|
|