mirror of
https://github.com/TrinityCore/TrinityCore.git
synced 2026-01-22 10:05:32 +01:00
1887 lines
62 KiB
C++
1887 lines
62 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"
|
|
|
|
#ifdef _MSC_VER
|
|
#pragma comment(lib, "ws2_32.lib") // Internet functions for HTTP stream
|
|
#endif
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Local defines
|
|
|
|
#define MAX_VAR_NAME 80
|
|
|
|
typedef DWORD (*PARSE_CSV_FILE)(TCascStorage * hs, CASC_CSV & Csv);
|
|
typedef DWORD (*PARSE_TEXT_FILE)(TCascStorage * hs, void * pvListFile);
|
|
typedef DWORD (*PARSE_VARIABLE)(TCascStorage * hs, const char * szVariableName, const char * szDataBegin, const char * szDataEnd, void * pvParam);
|
|
typedef DWORD (*PARSE_REGION_LINE)(TCascStorage * hs, CASC_CSV & Csv, size_t nLine);
|
|
|
|
static DWORD RibbitDownloadFile(LPCTSTR szCdnHostUrl, LPCTSTR szProduct, LPCTSTR szFileName, CASC_PATH<TCHAR> & LocalPath, CASC_BLOB & FileData);
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Local structures
|
|
|
|
struct TBuildFileInfo
|
|
{
|
|
LPCTSTR szFileName;
|
|
size_t nLength;
|
|
CBLD_TYPE BuildFileType;
|
|
};
|
|
|
|
struct TGameLocaleString
|
|
{
|
|
const char * szLocale;
|
|
DWORD dwLocale;
|
|
};
|
|
|
|
static const TBuildFileInfo BuildTypes[] =
|
|
{
|
|
{_T(".build.info"), 11, CascBuildInfo}, // Since HOTS build 30027, the game uses .build.info file for storage info
|
|
{_T(".build.db"), 9, CascBuildDb}, // Older CASC storages
|
|
{_T("versions"), 8, CascVersions}, // Online/cached CASC storages
|
|
};
|
|
|
|
static LPCTSTR DataDirs[] =
|
|
{
|
|
_T("data") _T(PATH_SEP_STRING) _T("casc"), // Overwatch. This item must be the first in the list
|
|
_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,
|
|
};
|
|
|
|
static const LPCTSTR szDefaultCDN = _T("ribbit://us.version.battle.net/v1/products");
|
|
static const ULONGLONG ValueOne64 = 1;
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// 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 LPCTSTR GetSubFolder(CPATH_TYPE PathType)
|
|
{
|
|
switch(PathType)
|
|
{
|
|
case PathTypeConfig: return _T("config");
|
|
case PathTypeData: return _T("data");
|
|
case PathTypePatch: return _T("patch");
|
|
|
|
default:
|
|
assert(false);
|
|
return _T("");
|
|
}
|
|
}
|
|
|
|
static bool CheckForTwoDigitFolder(LPCTSTR szPathName, void * pvContext)
|
|
{
|
|
BYTE Binary[8];
|
|
|
|
// Must be a two-digit hexa string (the first two digits of the CKey)
|
|
if(_tcslen(szPathName) == 2)
|
|
{
|
|
if(BinaryFromString(szPathName, 2, Binary) == ERROR_SUCCESS)
|
|
{
|
|
*(bool *)pvContext = true;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Keep searching
|
|
return true;
|
|
}
|
|
|
|
static bool ReplaceVersionsWithCdns(LPTSTR szBuffer, size_t ccBuffer, LPCTSTR szVersions)
|
|
{
|
|
LPCTSTR szVersions0 = _T("versions");
|
|
LPCTSTR szCdns0 = _T("cdns");
|
|
size_t nLength;
|
|
|
|
// Copy the existing file name into new buffer
|
|
CascStrCopy(szBuffer, ccBuffer, GetPlainFileName(szVersions));
|
|
|
|
// Find the ending "versions" string
|
|
if((nLength = _tcslen(szBuffer)) >= 8)
|
|
{
|
|
szBuffer = szBuffer + nLength - 8;
|
|
if(!_tcsicmp(szBuffer, szVersions0))
|
|
{
|
|
CascStrCopy(szBuffer, 5, szCdns0);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
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
|
|
BinaryFromString(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;
|
|
default: return CASC_LOCALE_NONE;
|
|
}
|
|
}
|
|
|
|
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(
|
|
PCASC_BLOB 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(PCASC_BLOB 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((PCASC_BLOB)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 */)
|
|
{
|
|
hs->SetProductCodeName(szDataBegin, (szDataEnd - szDataBegin));
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
// "B29049"
|
|
// "WOW-18125patch6.0.1"
|
|
// "WOW-45779patch10.0.2_Beta"
|
|
// "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)
|
|
{
|
|
// If the character is a digit, we include it into the built number
|
|
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 we don't have a build number yet, take the max value, if any
|
|
if(hs->dwBuildNumber == 0 && dwMaxValue >= 100)
|
|
{
|
|
hs->dwBuildNumber = dwMaxValue;
|
|
return ERROR_SUCCESS;
|
|
}
|
|
return ERROR_BAD_FORMAT;
|
|
}
|
|
|
|
static int LoadQueryKey(const CASC_CSV_COLUMN & Column, CASC_BLOB & 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 DWORD GetDefaultCdnServers(TCascStorage * hs, const CASC_CSV_COLUMN & Column)
|
|
{
|
|
if(hs->szCdnServers == NULL && Column.nLength != 0)
|
|
hs->szCdnServers = CascNewStrA2T(Column.szValue);
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
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 GetDefaultLocaleByRegion(LPCSTR szRegion)
|
|
{
|
|
#define CASC_REGION_INT(hi, lo) (((DWORD)(hi) << 0x08) | lo)
|
|
|
|
// Setup the default locale
|
|
switch(CASC_REGION_INT(szRegion[0], szRegion[1]))
|
|
{
|
|
case CASC_REGION_INT('u', 's'): return CASC_LOCALE_ENUS;
|
|
case CASC_REGION_INT('e', 'u'): return CASC_LOCALE_ENGB;
|
|
case CASC_REGION_INT('c', 'n'): return CASC_LOCALE_ZHCN;
|
|
case CASC_REGION_INT('k', 'r'): return CASC_LOCALE_KOKR;
|
|
case CASC_REGION_INT('t', 'w'): return CASC_LOCALE_ZHTW;
|
|
// case CASC_REGION_INT('s', 'g'): return CASC_LOCALE_????;
|
|
default: return CASC_LOCALE_ENUS;
|
|
}
|
|
}
|
|
|
|
static DWORD GetDefaultLocaleMask(const CASC_CSV_COLUMN & Column)
|
|
{
|
|
LPCSTR szTagEnd = Column.szValue + Column.nLength - 4;
|
|
LPCSTR szTagPtr;
|
|
DWORD dwLocaleValue;
|
|
DWORD dwLocaleMask = 0;
|
|
|
|
// Go through the whole tag string
|
|
for(szTagPtr = Column.szValue; szTagPtr <= szTagEnd; szTagPtr++)
|
|
{
|
|
// Try to recognize the 4-char locale code
|
|
if((dwLocaleValue = GetLocaleValue(szTagPtr)) != CASC_LOCALE_NONE)
|
|
{
|
|
dwLocaleMask |= dwLocaleValue;
|
|
szTagPtr += 3; // Will be moved by 1 more at the end of the loop
|
|
}
|
|
}
|
|
return dwLocaleMask;
|
|
}
|
|
|
|
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
|
|
hs->SetProductCodeName(ProductsList[nChoiceIndex]);
|
|
}
|
|
else if(nProductCount == 1)
|
|
{
|
|
// We now have preferred product to open
|
|
hs->SetProductCodeName(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
|
|
hs->SetProductCodeName(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;
|
|
|
|
// If we found tags, we can extract language build from it
|
|
hs->dwDefaultLocale = GetDefaultLocaleMask(Csv[nSelected]["Tags!STRING:0"]);
|
|
|
|
// Get the CDN servers and hosts
|
|
if(hs->dwFeatures & CASC_FEATURE_ONLINE)
|
|
{
|
|
GetDefaultCdnServers(hs, Csv[nSelected]["CDN Hosts!STRING:0"]);
|
|
GetDefaultCdnPath(hs, Csv[nSelected]["CDN Path!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 ParseRegionLine_Versions(TCascStorage * hs, CASC_CSV & Csv, size_t nLine)
|
|
{
|
|
DWORD dwErrCode;
|
|
|
|
// If the region line is not there yet, supply default one
|
|
if(hs->szRegion == NULL)
|
|
hs->szRegion = CascNewStr(Csv[nLine]["Region!STRING:0"].szValue);
|
|
|
|
// Get the default locale mask based on the region
|
|
hs->dwDefaultLocale = GetDefaultLocaleByRegion(hs->szRegion);
|
|
|
|
// Extract the CDN build key
|
|
dwErrCode = LoadQueryKey(Csv[nLine]["BuildConfig!HEX:16"], hs->CdnBuildKey);
|
|
if(dwErrCode != ERROR_SUCCESS)
|
|
return dwErrCode;
|
|
|
|
// Extract the CDN config key
|
|
dwErrCode = LoadQueryKey(Csv[nLine]["CDNConfig!HEX:16"], hs->CdnConfigKey);
|
|
if(dwErrCode != ERROR_SUCCESS)
|
|
return dwErrCode;
|
|
|
|
const CASC_CSV_COLUMN & VerColumn = Csv[nLine]["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 = BinaryFromString(hs->szBuildKey, MD5_STRING_SIZE, hs->CdnBuildKey.pbData);
|
|
return dwErrCode;
|
|
}
|
|
|
|
return ERROR_BAD_FORMAT;
|
|
}
|
|
|
|
static DWORD ParseRegionLine_Cdns(TCascStorage * hs, CASC_CSV & Csv, size_t nLine)
|
|
{
|
|
hs->szCdnServers = CascNewStrA2T(Csv[nLine]["Hosts!STRING:0"].szValue);
|
|
hs->szCdnPath = CascNewStrA2T(Csv[nLine]["Path!STRING:0"].szValue);
|
|
return (hs->szCdnServers && hs->szCdnPath) ? ERROR_SUCCESS : 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;
|
|
|
|
// Extract tags
|
|
hs->dwDefaultLocale = GetDefaultLocaleMask(Csv[CSV_ZERO][2]);
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Loads a local CSV file
|
|
static DWORD LoadCsvFile(TCascStorage * hs, LPCTSTR szFileName, PARSE_CSV_FILE 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;
|
|
}
|
|
|
|
// Loading an online file (VERSIONS or CDNS)
|
|
static DWORD LoadCsvFile(TCascStorage * hs, PARSE_REGION_LINE PfnParseRegionLine, LPCTSTR szFileName, LPCSTR szColumnName, bool bForceDownload)
|
|
{
|
|
CASC_PATH<TCHAR> LocalPath;
|
|
CASC_BLOB FileData;
|
|
DWORD dwErrCode = ERROR_SUCCESS;
|
|
char szFileNameA[0x20];
|
|
|
|
// Prepare the loading / downloading
|
|
LocalPath.SetPathRoot(hs->szRootPath);
|
|
LocalPath.AppendString(szFileName, true);
|
|
LocalPath.SetLocalCaching(bForceDownload == false);
|
|
|
|
// If the storage is defined as online, we can download the file, if it doesn't exist
|
|
if(hs->dwFeatures & CASC_FEATURE_ONLINE)
|
|
{
|
|
// Inform the user that we are downloading something
|
|
CascStrCopy(szFileNameA, _countof(szFileNameA), szFileName);
|
|
if(InvokeProgressCallback(hs, CascProgressDownloadingFile, szFileNameA, 0, 0))
|
|
return ERROR_CANCELLED;
|
|
|
|
// Download the file using Ribbit/HTTP protocol
|
|
dwErrCode = RibbitDownloadFile(hs->szCdnHostUrl, hs->szCodeName, szFileName, LocalPath, FileData);
|
|
}
|
|
else
|
|
{
|
|
// Load the local file
|
|
dwErrCode = LoadFileToMemory(LocalPath, FileData);
|
|
}
|
|
|
|
// Load the VERSIONS file
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
CASC_CSV Csv(0x40, true);
|
|
|
|
// Load the external file to memory
|
|
if((dwErrCode = Csv.Load(FileData.pbData, FileData.cbData)) == ERROR_SUCCESS)
|
|
{
|
|
size_t nLineCount = Csv.GetLineCount();
|
|
size_t nRegionXX = CASC_INVALID_SIZE_T;
|
|
size_t nRegionUS = CASC_INVALID_SIZE_T;
|
|
size_t nRegionEU = CASC_INVALID_SIZE_T;
|
|
|
|
// Find a matching config
|
|
for(size_t i = 0; i < nLineCount; i++)
|
|
{
|
|
const char * szRegion = Csv[i][szColumnName].szValue;
|
|
|
|
if(hs->szRegion && szRegion && !strcmp(hs->szRegion, szRegion))
|
|
return PfnParseRegionLine(hs, Csv, i);
|
|
|
|
if(szRegion != NULL)
|
|
{
|
|
if(!strcmp(szRegion, "xx"))
|
|
{
|
|
nRegionXX = i;
|
|
continue;
|
|
}
|
|
|
|
if(!strcmp(szRegion, "us"))
|
|
{
|
|
nRegionUS = i;
|
|
continue;
|
|
}
|
|
|
|
if(!strcmp(szRegion, "eu"))
|
|
{
|
|
nRegionEU = i;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now load the regions in this order:
|
|
// 1) US region
|
|
// 2. EU region
|
|
// 3. XX region
|
|
// 4. The first line
|
|
if(nRegionUS != CASC_INVALID_SIZE_T)
|
|
return PfnParseRegionLine(hs, Csv, nRegionUS);
|
|
if(nRegionEU != CASC_INVALID_SIZE_T)
|
|
return PfnParseRegionLine(hs, Csv, nRegionEU);
|
|
if(nRegionXX != CASC_INVALID_SIZE_T)
|
|
return PfnParseRegionLine(hs, Csv, nRegionXX);
|
|
if(nLineCount != 0)
|
|
return PfnParseRegionLine(hs, Csv, 0);
|
|
dwErrCode = ERROR_FILE_NOT_FOUND;
|
|
}
|
|
}
|
|
return dwErrCode;
|
|
}
|
|
|
|
static DWORD ForcePathExist(LPCTSTR szFileName, bool bIsFileName)
|
|
{
|
|
DWORD dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
|
|
bool bFirstSeparator = false;
|
|
|
|
// Sanity checks
|
|
if(szFileName && szFileName[0])
|
|
{
|
|
CASC_PATH<TCHAR> LocalPath(szFileName, NULL);
|
|
|
|
// Get the end of search
|
|
if(bIsFileName)
|
|
LocalPath.CutLastPart();
|
|
|
|
// Check whether the path exists
|
|
if(_taccess(LocalPath, 0) != 0)
|
|
{
|
|
LPTSTR szLocalPath = (LPTSTR)(LPCTSTR)LocalPath;
|
|
|
|
// Search the entire path
|
|
for(size_t nIndex = 0; nIndex < LocalPath.Length(); 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;
|
|
}
|
|
}
|
|
|
|
return dwErrCode;
|
|
}
|
|
|
|
static DWORD SaveLocalFile(LPCTSTR szLocalName, LPBYTE pbFileData, size_t cbFileData)
|
|
{
|
|
TFileStream * pLocStream;
|
|
DWORD dwErrCode = ERROR_DISK_FULL;
|
|
|
|
// Make sure that the path exists
|
|
ForcePathExist(szLocalName, true);
|
|
|
|
// Create local file
|
|
pLocStream = FileStream_CreateFile(szLocalName, BASE_PROVIDER_FILE | STREAM_PROVIDER_FLAT);
|
|
if(pLocStream != NULL)
|
|
{
|
|
if(FileStream_Write(pLocStream, NULL, pbFileData, (DWORD)(cbFileData)))
|
|
dwErrCode = ERROR_SUCCESS;
|
|
|
|
FileStream_Close(pLocStream);
|
|
}
|
|
else
|
|
dwErrCode = GetCascError();
|
|
|
|
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 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 RibbitDownloadFile(LPCTSTR szCdnHostUrl, LPCTSTR szProduct, LPCTSTR szFileName, CASC_PATH<TCHAR> & LocalPath, CASC_BLOB & FileData)
|
|
{
|
|
CASC_PATH<TCHAR> LocalFile;
|
|
TFileStream * pStream;
|
|
ULONGLONG FileSize = 0;
|
|
TCHAR szRemoteUrl[256];
|
|
DWORD dwErrCode = ERROR_CAN_NOT_COMPLETE;
|
|
|
|
// If required, try to load the local name first
|
|
if(LocalPath.Length() && LocalPath.LocalCaching())
|
|
{
|
|
// Load the local file into memory
|
|
if((dwErrCode = LoadFileToMemory(LocalPath, FileData)) == ERROR_SUCCESS)
|
|
{
|
|
// Pass the file data to the caller
|
|
return ERROR_SUCCESS;
|
|
}
|
|
}
|
|
|
|
// Supply the default CDN URL, if not present
|
|
if(szCdnHostUrl == NULL || szCdnHostUrl[0] == 0)
|
|
szCdnHostUrl = szDefaultCDN;
|
|
|
|
// Construct the full URL (https://wowdev.wiki/Ribbit)
|
|
// Old (HTTP) download: wget http://us.patch.battle.net:1119/wow_classic/cdns
|
|
CascStrPrintf(szRemoteUrl, _countof(szRemoteUrl), _T("%s/%s/%s"), szCdnHostUrl, szProduct, szFileName);
|
|
|
|
// Open the file stream
|
|
if((pStream = FileStream_OpenFile(szRemoteUrl, 0)) != NULL)
|
|
{
|
|
if(FileStream_GetSize(pStream, &FileSize) && FileSize <= 0x04000000)
|
|
{
|
|
// Fill-in the file pointer and size
|
|
if((dwErrCode = FileData.SetSize((size_t)FileSize)) == ERROR_SUCCESS)
|
|
{
|
|
if(FileStream_Read(pStream, NULL, FileData.pbData, (DWORD)FileSize))
|
|
{
|
|
dwErrCode = ERROR_SUCCESS;
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = GetCascError();
|
|
FileData.Free();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = GetCascError();
|
|
}
|
|
|
|
// Close the remote stream
|
|
FileStream_Close(pStream);
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = GetCascError();
|
|
}
|
|
|
|
// Save the file to the local cache
|
|
if(LocalPath.Length() && FileData.pbData && FileData.cbData && dwErrCode == ERROR_SUCCESS)
|
|
SaveLocalFile(LocalPath, FileData.pbData, FileData.cbData);
|
|
return dwErrCode;
|
|
}
|
|
|
|
static DWORD HttpDownloadFile(
|
|
LPCTSTR szRemoteName,
|
|
LPCTSTR szLocalName,
|
|
PULONGLONG PtrByteOffset,
|
|
DWORD cbReadSize,
|
|
DWORD dwPortFlags)
|
|
{
|
|
TFileStream * pRemStream;
|
|
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))
|
|
{
|
|
// Verify valid size
|
|
if(0 < FileSize && FileSize < CASC_MAX_ONLINE_FILE_SIZE)
|
|
{
|
|
cbReadSize = (DWORD)FileSize;
|
|
dwErrCode = ERROR_SUCCESS;
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = ERROR_BAD_FORMAT;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = GetCascError();
|
|
}
|
|
}
|
|
|
|
// Shall we read something?
|
|
if((dwErrCode == ERROR_SUCCESS) && (cbReadSize != 0) && (pbFileData = CASC_ALLOC<BYTE>(cbReadSize)) != NULL)
|
|
{
|
|
// Read all required data from the remote file
|
|
if(FileStream_Read(pRemStream, PtrByteOffset, pbFileData, cbReadSize))
|
|
dwErrCode = SaveLocalFile(szLocalName, pbFileData, cbReadSize);
|
|
|
|
// Free the data buffer
|
|
CASC_FREE(pbFileData);
|
|
}
|
|
|
|
// Close the remote stream
|
|
FileStream_Close(pRemStream);
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = GetCascError();
|
|
}
|
|
|
|
return dwErrCode;
|
|
}
|
|
|
|
DWORD SetProductCodeName(TCascStorage * hs, LPCSTR szCodeName, size_t nLength)
|
|
{
|
|
if(hs->szCodeName == NULL && szCodeName != NULL)
|
|
{
|
|
// Make sure we have the length
|
|
if(nLength == 0)
|
|
{
|
|
nLength = strlen(szCodeName);
|
|
}
|
|
|
|
if((hs->szCodeName = CASC_ALLOC<TCHAR>(nLength + 1)) == NULL)
|
|
return ERROR_NOT_ENOUGH_MEMORY;
|
|
CascStrCopy(hs->szCodeName, nLength + 1, szCodeName, nLength);
|
|
}
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
DWORD FetchCascFile(
|
|
TCascStorage * hs,
|
|
LPCTSTR szRootPath,
|
|
CPATH_TYPE PathType,
|
|
LPBYTE pbEKey,
|
|
LPCTSTR szExtension,
|
|
CASC_PATH<TCHAR> & LocalPath)
|
|
{
|
|
LPCTSTR szCdnServers = hs->szCdnServers;
|
|
DWORD dwErrCode = ERROR_SUCCESS;
|
|
TCHAR szCdnServer[MAX_PATH] = _T("");
|
|
|
|
// First, construct the local path
|
|
LocalPath.Create(szRootPath, GetSubFolder(PathType), NULL);
|
|
LocalPath.AppendEKey(pbEKey);
|
|
LocalPath.AppendString(szExtension, false);
|
|
|
|
// Check whether the file already exists
|
|
if(!FileAlreadyExists(LocalPath))
|
|
{
|
|
// If this is not an online version, do nothing and return error
|
|
if(!(hs->dwFeatures & CASC_FEATURE_ONLINE))
|
|
return ERROR_FILE_NOT_FOUND;
|
|
|
|
// Force-create the local path
|
|
if((dwErrCode = ForcePathExist(LocalPath, true)) != ERROR_SUCCESS)
|
|
return dwErrCode;
|
|
|
|
// Try all download servers
|
|
while((szCdnServers = ExtractCdnServerName(szCdnServer, _countof(szCdnServer), szCdnServers)) != NULL)
|
|
{
|
|
CASC_PATH<TCHAR> RemotePath(URL_SEP_CHAR);
|
|
|
|
// Construct the full remote URL path
|
|
RemotePath.Create(szCdnServer, hs->szCdnPath, GetSubFolder(PathType), NULL);
|
|
RemotePath.AppendEKey(pbEKey);
|
|
RemotePath.AppendString(szExtension, false);
|
|
|
|
// Attempt to download the file
|
|
dwErrCode = HttpDownloadFile(RemotePath, LocalPath, NULL, 0, 0);
|
|
|
|
// Stop on low memory condition, as it will most likely
|
|
// end up with low memory on next download
|
|
if(dwErrCode == ERROR_SUCCESS || dwErrCode == ERROR_NOT_ENOUGH_MEMORY)
|
|
return dwErrCode;
|
|
}
|
|
|
|
// Sorry, the file was not found
|
|
dwErrCode = ERROR_FILE_NOT_FOUND;
|
|
}
|
|
return dwErrCode;
|
|
}
|
|
|
|
DWORD FetchCascFile(TCascStorage * hs, CPATH_TYPE PathType, LPBYTE pbEKey, LPCTSTR szExtension, CASC_PATH<TCHAR> & LocalPath, PCASC_ARCHIVE_INFO pArchiveInfo)
|
|
{
|
|
PCASC_EKEY_ENTRY pEKeyEntry;
|
|
LPBYTE pbArchiveKey;
|
|
DWORD dwErrCode = ERROR_SUCCESS;
|
|
|
|
// Data files may be stored in archives, therefore we need to check
|
|
if((pbEKey != NULL) && (pEKeyEntry = (PCASC_EKEY_ENTRY)hs->IndexMap.FindObject(pbEKey)) != NULL)
|
|
{
|
|
// Can't complete if the caller doesn't know the archive info
|
|
if(pArchiveInfo != NULL)
|
|
{
|
|
// Fill-in the archive info
|
|
pArchiveInfo->ArchiveIndex = (DWORD)(pEKeyEntry->StorageOffset >> hs->FileOffsetBits);
|
|
pArchiveInfo->ArchiveOffs = (DWORD)(pEKeyEntry->StorageOffset & ((ValueOne64 << hs->FileOffsetBits) - 1));
|
|
pArchiveInfo->EncodedSize = pEKeyEntry->EncodedSize;
|
|
|
|
// Fill-in the archive key
|
|
pbArchiveKey = pbEKey = hs->ArchivesKey.pbData + (MD5_HASH_SIZE * pArchiveInfo->ArchiveIndex);
|
|
memcpy(pArchiveInfo->ArchiveKey, pbArchiveKey, MD5_HASH_SIZE);
|
|
|
|
// Remap the path type to "data"
|
|
PathType = PathTypeData;
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = ERROR_CAN_NOT_COMPLETE;
|
|
}
|
|
}
|
|
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
// Try the local archives
|
|
if(hs->dwFeatures & CASC_FEATURE_DATA_ARCHIVES)
|
|
{
|
|
dwErrCode = FetchCascFile(hs, hs->szDataPath, PathType, pbEKey, szExtension, LocalPath);
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
// Try to download the file into the "data/<type>" path
|
|
if(hs->szDataPath != NULL)
|
|
{
|
|
dwErrCode = FetchCascFile(hs, hs->szDataPath, PathType, pbEKey, szExtension, LocalPath);
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
// Try to download the file into the "<type>" path
|
|
if(hs->szRootPath != NULL)
|
|
{
|
|
dwErrCode = FetchCascFile(hs, hs->szRootPath, PathType, pbEKey, szExtension, LocalPath);
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
return ERROR_SUCCESS;
|
|
}
|
|
}
|
|
return dwErrCode;
|
|
}
|
|
|
|
static DWORD FetchAndLoadConfigFile(TCascStorage * hs, PCASC_BLOB pFileKey, PARSE_TEXT_FILE PfnParseProc)
|
|
{
|
|
CASC_PATH<TCHAR> LocalPath;
|
|
void * pvListFile = NULL;
|
|
DWORD dwErrCode;
|
|
|
|
// Make sure there is a local copy of the file
|
|
dwErrCode = FetchCascFile(hs, PathTypeConfig, pFileKey->pbData, NULL, LocalPath);
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
// Load and verify the external listfile
|
|
pvListFile = ListFile_OpenExternal(LocalPath);
|
|
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;
|
|
}
|
|
|
|
static LPTSTR CheckForDirectory(LPCTSTR szParentFolder, LPCTSTR szSubFolder)
|
|
{
|
|
CASC_PATH<TCHAR> LocalPath(szParentFolder, szSubFolder, NULL);
|
|
LPTSTR szLocalPath = NULL;
|
|
|
|
// Check whether the path exists
|
|
if(DirectoryExists(LocalPath))
|
|
szLocalPath = LocalPath.New();
|
|
return szLocalPath;
|
|
}
|
|
|
|
static LPTSTR CheckForDirectories(LPCTSTR szParentFolder, ...)
|
|
{
|
|
LPCTSTR szSubDir;
|
|
va_list argList;
|
|
LPTSTR szFolder = NULL;
|
|
|
|
va_start(argList, szParentFolder);
|
|
while((szSubDir = va_arg(argList, LPCTSTR)) != NULL)
|
|
{
|
|
if((szFolder = CheckForDirectory(szParentFolder, szSubDir)) != NULL)
|
|
break;
|
|
}
|
|
va_end(argList);
|
|
|
|
return szFolder;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Public functions
|
|
|
|
bool InvokeProgressCallback(TCascStorage * hs, CASC_PROGRESS_MSG Message, 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, Message, 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;
|
|
}
|
|
|
|
DWORD CheckCascBuildFileExact(CASC_BUILD_FILE & BuildFile, LPCTSTR szLocalPath)
|
|
{
|
|
// Calculate the length of the local path
|
|
TFileStream * pStream;
|
|
LPCTSTR szFileType;
|
|
size_t nLength = _tcslen(szLocalPath);
|
|
|
|
// Clear the build file structure
|
|
memset(&BuildFile, 0, sizeof(CASC_BUILD_FILE));
|
|
|
|
// Check every type of the build file
|
|
for(size_t i = 0; i < _countof(BuildTypes); i++)
|
|
{
|
|
// We support any file name with the appropriate ending,
|
|
// for example wow-19342.build.info or wow-47186.versions
|
|
szFileType = szLocalPath + nLength - BuildTypes[i].nLength;
|
|
if(!_tcsicmp(BuildTypes[i].szFileName, szFileType))
|
|
{
|
|
// We also try to open the file
|
|
if((pStream = FileStream_OpenFile(szLocalPath, 0)) != NULL)
|
|
{
|
|
CascStrCopy(BuildFile.szFullPath, _countof(BuildFile.szFullPath), szLocalPath);
|
|
BuildFile.szPlainName = GetPlainFileName(BuildFile.szFullPath);
|
|
BuildFile.BuildFileType = BuildTypes[i].BuildFileType;
|
|
FileStream_Close(pStream);
|
|
return ERROR_SUCCESS;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Unrecognized file name
|
|
return ERROR_FILE_NOT_FOUND;
|
|
}
|
|
|
|
DWORD CheckCascBuildFileDirs(CASC_BUILD_FILE & BuildFile, LPCTSTR szLocalPath)
|
|
{
|
|
CASC_PATH<TCHAR> WorkPath(szLocalPath, NULL);
|
|
DWORD dwLevelCount = 0;
|
|
|
|
// Clear the build file structure
|
|
memset(&BuildFile, 0, sizeof(CASC_BUILD_FILE));
|
|
|
|
for(;;)
|
|
{
|
|
// Try all supported build file types
|
|
for(size_t i = 0; i < _countof(BuildTypes); i++)
|
|
{
|
|
CASC_PATH<TCHAR> FilePath(WorkPath, BuildTypes[i].szFileName, NULL);
|
|
|
|
// If the file exists there, we found it
|
|
if(CheckCascBuildFileExact(BuildFile, FilePath) == ERROR_SUCCESS)
|
|
{
|
|
return ERROR_SUCCESS;
|
|
}
|
|
}
|
|
|
|
// Try to cut off one path path. Don't go indefinitely.
|
|
if((dwLevelCount > 5) || !WorkPath.CutLastPart())
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// None of the supported file names was found
|
|
return ERROR_FILE_NOT_FOUND;
|
|
}
|
|
|
|
DWORD CheckOnlineStorage(PCASC_OPEN_STORAGE_ARGS pArgs, CASC_BUILD_FILE & BuildFile, bool bOnlineStorage)
|
|
{
|
|
// If the online storage is required, we try to extract the product code
|
|
if((bOnlineStorage) && (pArgs->szCodeName != NULL))
|
|
{
|
|
CASC_PATH<TCHAR> FilePath(pArgs->szLocalPath, _T("versions"), NULL);
|
|
|
|
FilePath.CopyTo(BuildFile.szFullPath, _countof(BuildFile.szFullPath));
|
|
BuildFile.szPlainName = GetPlainFileName(BuildFile.szFullPath);
|
|
BuildFile.BuildFileType = CascVersions;
|
|
return ERROR_SUCCESS;
|
|
}
|
|
return ERROR_FILE_NOT_FOUND;
|
|
}
|
|
|
|
DWORD CheckArchiveFilesDirectories(TCascStorage * hs)
|
|
{
|
|
// Sanity checks
|
|
assert((hs->dwFeatures & CASC_FEATURE_DATA_ARCHIVES) != 0);
|
|
assert(hs->szRootPath != NULL);
|
|
assert(hs->szDataPath == NULL);
|
|
assert(hs->szIndexPath == NULL);
|
|
|
|
LPTSTR szDataPath = NULL;
|
|
LPTSTR szConfigPath = NULL;
|
|
LPTSTR szIndexPath = NULL;
|
|
|
|
// Try all known subdirectories for the game data sub dirs
|
|
for(size_t i = 0; i < _countof(DataDirs); i++)
|
|
{
|
|
if((szDataPath = CheckForDirectory(hs->szRootPath, DataDirs[i])) != NULL)
|
|
{
|
|
// Check the config folder
|
|
if((szConfigPath = CheckForDirectory(szDataPath, _T("config"))) != NULL)
|
|
{
|
|
// First, check for more common "data" subdirectory
|
|
// Second, try the "darch" subdirectory (older builds of HOTS - Alpha)
|
|
if((szIndexPath = CheckForDirectories(szDataPath, _T("data"), _T("darch"), NULL)) != NULL)
|
|
{
|
|
hs->szDataPath = szDataPath;
|
|
hs->szConfigPath = szConfigPath;
|
|
hs->szIndexPath = szIndexPath;
|
|
return ERROR_SUCCESS;
|
|
}
|
|
CASC_FREE(szConfigPath);
|
|
}
|
|
CASC_FREE(szDataPath);
|
|
}
|
|
}
|
|
|
|
// One of the paths was not found
|
|
return ERROR_FILE_NOT_FOUND;
|
|
}
|
|
|
|
DWORD CheckDataFilesDirectory(TCascStorage * hs)
|
|
{
|
|
CASC_PATH<TCHAR> DataPath(hs->szRootPath, _T("data"), NULL);
|
|
DWORD dwErrCode;
|
|
bool bTwoDigitFolderFound = false;
|
|
|
|
// When CASC_FEATURE_ONLINE is not set, then the folder must exist
|
|
if((hs->dwFeatures & CASC_FEATURE_ONLINE) == 0)
|
|
{
|
|
// Check if there are subfolders at all. If not, do not bother
|
|
// the file system with open requests into data files folder
|
|
if((dwErrCode = ScanDirectory(DataPath, CheckForTwoDigitFolder, NULL, &bTwoDigitFolderFound)) != ERROR_SUCCESS)
|
|
return dwErrCode;
|
|
|
|
if(bTwoDigitFolderFound == false)
|
|
return ERROR_PATH_NOT_FOUND;
|
|
}
|
|
|
|
// Create the path for raw files
|
|
return ((hs->szFilesPath = DataPath.New()) == NULL) ? ERROR_NOT_ENOUGH_MEMORY : ERROR_SUCCESS;
|
|
}
|
|
|
|
DWORD LoadBuildFile_Versions_Cdns(TCascStorage * hs)
|
|
{
|
|
LPCTSTR szVersions = _T("versions");
|
|
DWORD dwErrCode;
|
|
TCHAR szBuffer[MAX_PATH];
|
|
bool bForceDownload = (hs->dwFeatures & CASC_FEATURE_FORCE_DOWNLOAD) ? true : false;
|
|
|
|
// The default name of the build file is "versions". However, the caller may select different file
|
|
if(hs->szMainFile && hs->szMainFile[0])
|
|
szVersions = GetPlainFileName(hs->szMainFile);
|
|
dwErrCode = LoadCsvFile(hs, ParseRegionLine_Versions, szVersions, "Region!STRING:0", bForceDownload);
|
|
|
|
// We also need to load the "cdns" file
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
// The default CDNS file is "cdns". However, if the build file is different, we change "cdns" as well
|
|
// Example: If the build file name is "hearthstone-25.0.3.160183.159202.versions", then we want "hearthstone-25.0.3.160183.159202.cdns"
|
|
if(hs->szMainFile && hs->szMainFile[0])
|
|
{
|
|
if(ReplaceVersionsWithCdns(szBuffer, _countof(szBuffer), hs->szMainFile))
|
|
{
|
|
dwErrCode = LoadCsvFile(hs, ParseRegionLine_Cdns, szBuffer, "Name!STRING:0", bForceDownload);
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
return dwErrCode;
|
|
}
|
|
}
|
|
|
|
// Fall back to the default "cdns" file
|
|
dwErrCode = LoadCsvFile(hs, ParseRegionLine_Cdns, _T("cdns"), "Name!STRING:0", bForceDownload);
|
|
}
|
|
return dwErrCode;
|
|
}
|
|
|
|
DWORD LoadMainFile(TCascStorage * hs)
|
|
{
|
|
DWORD dwErrCode = ERROR_NOT_SUPPORTED;
|
|
|
|
// The build file must be known at this point, even for online storage
|
|
// that are to bo created
|
|
assert(hs->szMainFile != NULL);
|
|
|
|
// Perform support for each build file type
|
|
switch(hs->BuildFileType)
|
|
{
|
|
case CascBuildDb: // Older storages have "build.db"
|
|
dwErrCode = LoadCsvFile(hs, hs->szMainFile, ParseFile_BuildDb, false);
|
|
break;
|
|
|
|
case CascBuildInfo: // Current storages have "build.db"
|
|
dwErrCode = LoadCsvFile(hs, hs->szMainFile, ParseFile_BuildInfo, true);
|
|
break;
|
|
|
|
case CascVersions: // Online storages have "versions+cdns"
|
|
dwErrCode = LoadBuildFile_Versions_Cdns(hs);
|
|
break;
|
|
|
|
default:
|
|
assert(false);
|
|
break;
|
|
}
|
|
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, CascProgressLoadingFile, "CDN config", 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, CascProgressLoadingFile, "CDN build", 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);
|
|
}
|
|
|
|
DWORD LoadInternalFileToMemory(TCascStorage * hs, PCASC_CKEY_ENTRY pCKeyEntry, CASC_BLOB & FileData)
|
|
{
|
|
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
|
|
if((dwErrCode = FileData.SetSize(cbFileData)) == ERROR_SUCCESS)
|
|
{
|
|
// Read the entire file to memory
|
|
CascReadFile(hFile, FileData.pbData, cbFileData, &dwBytesRead);
|
|
if(dwBytesRead != cbFileData)
|
|
{
|
|
dwErrCode = ERROR_FILE_CORRUPT;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
|
|
}
|
|
}
|
|
|
|
// Close the file
|
|
CascCloseFile(hFile);
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = GetCascError();
|
|
}
|
|
|
|
// Handle errors
|
|
if(dwErrCode != ERROR_SUCCESS)
|
|
FileData.Free();
|
|
return dwErrCode;
|
|
}
|
|
|
|
DWORD LoadFileToMemory(LPCTSTR szFileName, CASC_BLOB & FileData)
|
|
{
|
|
TFileStream * pStream;
|
|
ULONGLONG FileSize = 0;
|
|
DWORD dwErrCode = ERROR_SUCCESS;
|
|
|
|
// 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);
|
|
|
|
// Do not load zero files or too large files
|
|
if(0 < FileSize && FileSize <= 0x2000000)
|
|
{
|
|
// Allocate file data buffer. Make it 1 byte longer
|
|
// so string functions can put '\0' there
|
|
dwErrCode = FileData.SetSize((size_t)(FileSize));
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
if(FileStream_Read(pStream, NULL, FileData.pbData, (DWORD)(FileData.cbData)))
|
|
{
|
|
// Terminate the data with zero so various string-based functions can process it
|
|
FileData.pbData[FileData.cbData] = 0;
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = GetCascError();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = ERROR_BAD_FORMAT;
|
|
}
|
|
|
|
// Close the file stream
|
|
FileStream_Close(pStream);
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = GetCascError();
|
|
}
|
|
|
|
return dwErrCode;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Public CDN functions
|
|
|
|
LPCTSTR WINAPI CascCdnGetDefault()
|
|
{
|
|
return szDefaultCDN;
|
|
}
|
|
|
|
LPBYTE WINAPI CascCdnDownload(LPCTSTR szCdnHostUrl, LPCTSTR szProduct, LPCTSTR szFileName, DWORD * PtrSize)
|
|
{
|
|
CASC_PATH<TCHAR> LocalPath;
|
|
CASC_BLOB FileData;
|
|
LPBYTE pbFileData = NULL;
|
|
size_t cbFileData = 0;
|
|
DWORD dwErrCode;
|
|
|
|
// Download the file
|
|
dwErrCode = RibbitDownloadFile(szCdnHostUrl, szProduct, szFileName, LocalPath, FileData);
|
|
if(dwErrCode == ERROR_SUCCESS)
|
|
{
|
|
// Create copy of the buffer
|
|
if((pbFileData = CASC_ALLOC<BYTE>(FileData.cbData + 1)) != NULL)
|
|
{
|
|
memcpy(pbFileData, FileData.pbData, FileData.cbData);
|
|
pbFileData[FileData.cbData] = 0;
|
|
cbFileData = FileData.cbData;
|
|
}
|
|
else
|
|
{
|
|
dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
|
|
}
|
|
}
|
|
|
|
// Give the results
|
|
if(dwErrCode != ERROR_SUCCESS)
|
|
SetCascError(dwErrCode);
|
|
if(PtrSize != NULL)
|
|
PtrSize[0] = (DWORD)cbFileData;
|
|
return pbFileData;
|
|
}
|
|
|
|
void WINAPI CascCdnFree(void * buffer)
|
|
{
|
|
CASC_FREE(buffer);
|
|
}
|
|
|