/*****************************************************************************/ /* 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 { const TCHAR * 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 const TCHAR * 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(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(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) { TCHAR szCodeNameT[0x40]; if(hs->szCodeName == NULL && szCodeName != NULL) { CascStrCopy(szCodeNameT, _countof(szCodeNameT), szCodeName); hs->szCodeName = CascNewStr(szCodeNameT); } } static int 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; TCHAR szCdnServers[MAX_PATH]; TCHAR szCdnPath[MAX_PATH]; 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 CascStrCopy(szCdnServers, _countof(szCdnServers), Csv[i]["Hosts!STRING:0"].szValue); hs->szCdnServers = CascNewStr(szCdnServers); // Save the CDN subpath CascStrCopy(szCdnPath, _countof(szCdnPath), Csv[i]["Path!STRING:0"].szValue); hs->szCdnPath = CascNewStr(szCdnPath); // 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; // 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; // 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 return (hs->CdnBuildKey.pbData != NULL && hs->CdnConfigKey.pbData != NULL) ? ERROR_SUCCESS : 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(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, TCHAR * szDirectory) { TCHAR * 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, const TCHAR * 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 const TCHAR * ExtractCdnServerName(TCHAR * szServerName, size_t cchServerName, const TCHAR * szCdnServers) { const TCHAR * 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 & RemotePath, CASC_PATH & 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(const TCHAR * szFileName, bool bIsFileName) { TCHAR * 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 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(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 RemotePath(URL_SEP_CHAR); CASC_PATH 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) || (_taccess(LocalPath, 0) == -1)) { // 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 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 all span size 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, TCHAR * szDirectory) { TFileStream * pStream; TCHAR * 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; } // Retrieve the size of the ENCODING file if(dwErrCode == ERROR_SUCCESS) { // Allocate space for the ENCODING file pbFileData = CASC_ALLOC(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 // Note that this fails when the game is running (sharing violation). pStream = FileStream_OpenFile(szFileName, STREAM_FLAG_READ_ONLY | 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(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; }