/*****************************************************************************/ /* CascRootFile_TVFS.cpp Copyright (c) Ladislav Zezula 2018 */ /*---------------------------------------------------------------------------*/ /* ROOT handler for TACT VFS manifest format (root) */ /* Note: TACT = Trusted Application Content Transfer */ /*---------------------------------------------------------------------------*/ /* Date Ver Who Comment */ /* -------- ---- --- ------- */ /* 24.05.18 1.00 Lad The first version of CascRootFile_TVFS.cpp */ /*****************************************************************************/ #define __CASCLIB_SELF__ #include "CascLib.h" #include "CascCommon.h" //----------------------------------------------------------------------------- // Local defines #define TVFS_FLAG_INCLUDE_CKEY 0x0001 // Include C-key in content file record #define TVFS_FLAG_WRITE_SUPPORT 0x0002 // Write support. Include a table of encoding specifiers. This is required for writing files to the underlying storage. This bit is implied by the patch-support bit #define TVFS_FLAG_PATCH_SUPPORT 0x0004 // Patch support. Include patch records in the content file records. #define TVFS_FLAG_LOWERCASE_MANIFEST 0x0008 // Lowercase manifest. All paths in the path table have been converted to ASCII lowercase (i.e. [A-Z] converted to [a-z]) #define TVFS_PTE_PATH_SEPARATOR_PRE 0x0001 // There is path separator before the name #define TVFS_PTE_PATH_SEPARATOR_POST 0x0002 // There is path separator after the name #define TVFS_PTE_NODE_VALUE 0x0004 // The NodeValue in path table entry is valid #define TVFS_FOLDER_NODE 0x80000000 // Highest bit is set if a file node is a folder #define TVFS_FOLDER_SIZE_MASK 0x7FFFFFFF // Mask to get length of the folder // Uncomment this to parse TVFS root files for World of Warcraft // Note that this is signigicantly slower than using the legacy ROOT file //#define TVFS_PARSE_WOW_ROOT //----------------------------------------------------------------------------- // Local structures // In-memory layout of the TVFS file header struct TVFS_DIRECTORY_HEADER { TVFS_DIRECTORY_HEADER() { memset(this, 0, sizeof(TVFS_DIRECTORY_HEADER) - FIELD_OFFSET(TVFS_DIRECTORY_HEADER, Data)); } LPBYTE DataAt(DWORD dwOffset) { return Data.pbData + dwOffset; } DWORD Signature; // Must be CASC_TVFS_ROOT_SIGNATURE BYTE FormatVersion; // Version of the format. Should be 1. BYTE HeaderSize; // Size of the header, in bytes BYTE EKeySize; // Size of an E-Key. TACT uses 9-byte E-keys BYTE PatchKeySize; // Size of a patch key. TACT uses 9-byte P-keys DWORD Flags; // Flags. See TVFS_FLAG_XXX // Followed by the offset table (variable length) DWORD PathTableOffset; // Offset of the path table DWORD PathTableSize; // Size of the path table DWORD VfsTableOffset; // Offset of the VFS table DWORD VfsTableSize; // Size of the VFS table DWORD CftTableOffset; // Offset of the container file table DWORD CftTableSize; // Size of the container file table USHORT MaxDepth; // The maximum depth of the path prefix tree stored in the path table DWORD EstTableOffset; // The offset of the encoding specifier table. Only if the write-support bit is set in the header flag DWORD EstTableSize; // The size of the encoding specifier table. Only if the write-support bit is set in the header flag DWORD CftOffsSize; // Byte length of the offset in the Content File Table entry DWORD EstOffsSize; // Byte length of the offset in the Encoding Specifier Table entry CASC_BLOB Data; // The complete directory data // LPBYTE pbPathFileTable; // Begin and end of the path table // LPBYTE pbPathTableEnd; // LPBYTE pbVfsFileTable; // Begin and end of the VFS file table // LPBYTE pbVfsTableEnd; // LPBYTE pbCftFileTable; // Begin and end of the content file table // LPBYTE pbCftTableEnd; }; /* // Minimum size of a valid path table entry. 1 byte + 1-byte name + 1 byte + DWORD #define TVFS_HEADER_LENGTH FIELD_OFFSET(TVFS_DIRECTORY_HEADER, CftOffsSize) // Minimum size of a valid path table entry. 1 byte + 1-byte name + 1 byte + DWORD #define TVFS_MIN_PATH_ENTRY (1 + 1 + 1 + sizeof(DWORD)) // Minimum size of the VFS entry (SpanCount + FileOffset + SpanLength + CftOffset) #define TVFS_MIN_VFS_ENTRY (1 + sizeof(DWORD) + sizeof(DWORD) + 1) // Minimum size of the Content File Table entry (CASC_EKEY_SIZE + EncodedSize + ContentSize) #define TVFS_MIN_CFT_ENTRY (CASC_EKEY_SIZE + sizeof(DWORD) + sizeof(DWORD)) // Minimum size of the TVFS folder data #define TVFS_MIN_FILE_SIZE (TVFS_HEADER_LENGTH + TVFS_MIN_PATH_ENTRY + TVFS_MIN_VFS_ENTRY + TVFS_MIN_CFT_ENTRY) // Maximum estimated file table. Empirically set to 8 MB, increase if needed. #define TVFS_MAX_FILE_SIZE 0x00800000 */ // In-memory layout of the path table entry typedef struct _TVFS_PATH_TABLE_ENTRY { char * m_pNamePtr; // Pointer to the begin of the node name char * m_pNameEnd; // Pointer to the end of the file name DWORD NodeFlags; // TVFS_PTE_XXX DWORD NodeValue; // Node value } TVFS_PATH_TABLE_ENTRY, *PTVFS_PATH_TABLE_ENTRY; typedef struct _TVFS_WOW_ENTRY { DWORD LocaleFlags; USHORT ContentFlags; DWORD FileDataId; BYTE ContentKey[MD5_HASH_SIZE]; } TVFS_WOW_ENTRY, *PTVFS_WOW_ENTRY; //----------------------------------------------------------------------------- // Handler definition for TVFS root file // Structure for the root handler struct TRootHandler_TVFS : public TFileTreeRoot { public: TRootHandler_TVFS() : TFileTreeRoot(0) { // TVFS supports file names, but DOESN'T support CKeys. dwFeatures |= CASC_FEATURE_FILE_NAMES; } // Returns size of "container file table offset" field in the VFS. // - If the container file table is larger than 0xffffff bytes, it's 4 bytes // - If the container file table is larger than 0xffff bytes, it's 3 bytes // - If the container file table is larger than 0xff bytes, it's 2 bytes // - If the container file table is smaller than 0xff bytes, it's 1 byte static DWORD GetOffsetFieldSize(DWORD dwTableSize) { if(dwTableSize > 0xffffff) return 4; if(dwTableSize > 0xffff) return 3; if(dwTableSize > 0xff) return 2; return 1; } bool PathBuffer_AppendNode(CASC_PATH & PathBuffer, TVFS_PATH_TABLE_ENTRY & PathEntry) { // Append the prefix separator, if needed if(PathEntry.NodeFlags & TVFS_PTE_PATH_SEPARATOR_PRE) PathBuffer.AppendChar('/'); // Append the name fragment, if any if(PathEntry.m_pNameEnd > PathEntry.m_pNamePtr) PathBuffer.AppendStringN(PathEntry.m_pNamePtr, (PathEntry.m_pNameEnd - PathEntry.m_pNamePtr), false); // Append the postfix separator, if needed if(PathEntry.NodeFlags & TVFS_PTE_PATH_SEPARATOR_POST) PathBuffer.AppendChar('/'); return true; } static DWORD CaptureDirectoryHeader(TVFS_DIRECTORY_HEADER & DirHeader, CASC_BLOB & Data) { LPBYTE pbDataPtr = NULL; LPBYTE pbDataEnd = NULL; // Extract the data out of the buffer DirHeader.Data.MoveFrom(Data); pbDataPtr = DirHeader.Data.pbData; pbDataEnd = DirHeader.Data.End(); // Capture the signature pbDataPtr = CaptureInteger32(pbDataPtr, pbDataEnd, &DirHeader.Signature); if(pbDataPtr == NULL || DirHeader.Signature != CASC_TVFS_ROOT_SIGNATURE) return ERROR_BAD_FORMAT; // Capture the other four integers pbDataPtr = CaptureByteArray(pbDataPtr, pbDataEnd, 4, &DirHeader.FormatVersion); if(pbDataPtr == NULL || DirHeader.FormatVersion != 1 || DirHeader.EKeySize != 9 || DirHeader.PatchKeySize != 9 || DirHeader.HeaderSize < 8) return ERROR_BAD_FORMAT; // Capture the rest pbDataPtr = CaptureByteArray(pbDataPtr, pbDataEnd, DirHeader.HeaderSize - FIELD_OFFSET(TVFS_DIRECTORY_HEADER, Flags), (LPBYTE)(&DirHeader.Flags)); if(pbDataPtr == NULL) return ERROR_BAD_FORMAT; // Swap the header values DirHeader.Flags = ConvertBytesToInteger_4_LE((LPBYTE)(&DirHeader.Flags)); // Swap the offset table values DirHeader.PathTableOffset = ConvertBytesToInteger_4((LPBYTE)(&DirHeader.PathTableOffset)); DirHeader.PathTableSize = ConvertBytesToInteger_4((LPBYTE)(&DirHeader.PathTableSize)); DirHeader.VfsTableOffset = ConvertBytesToInteger_4((LPBYTE)(&DirHeader.VfsTableOffset)); DirHeader.VfsTableSize = ConvertBytesToInteger_4((LPBYTE)(&DirHeader.VfsTableSize)); DirHeader.CftTableOffset = ConvertBytesToInteger_4((LPBYTE)(&DirHeader.CftTableOffset)); DirHeader.CftTableSize = ConvertBytesToInteger_4((LPBYTE)(&DirHeader.CftTableSize)); DirHeader.MaxDepth = ConvertBytesToInteger_2((LPBYTE)(&DirHeader.MaxDepth)); DirHeader.EstTableOffset = ConvertBytesToInteger_4((LPBYTE)(&DirHeader.EstTableOffset)); DirHeader.EstTableSize = ConvertBytesToInteger_4((LPBYTE)(&DirHeader.EstTableSize)); // Determine size of file table offsets DirHeader.CftOffsSize = GetOffsetFieldSize(DirHeader.CftTableSize); DirHeader.EstOffsSize = GetOffsetFieldSize(DirHeader.EstTableSize); // Capture the path table // DirHeader.pbPathFileTable = pbDirectory + DirHeader.PathTableOffset; // DirHeader.pbPathTableEnd = pbDirectory + DirHeader.PathTableOffset + DirHeader.PathTableSize; // if(DirHeader.pbPathTableEnd > pbDataEnd) // return ERROR_BAD_FORMAT; // Capture the VFS file table // DirHeader.pbVfsFileTable = pbDirectory + DirHeader.VfsTableOffset; // DirHeader.pbVfsTableEnd = pbDirectory + DirHeader.VfsTableOffset + DirHeader.VfsTableSize; // if(DirHeader.pbVfsTableEnd > pbDataEnd) // return ERROR_BAD_FORMAT; // Capture the container file table // DirHeader.pbCftFileTable = pbDirectory + DirHeader.CftTableOffset; // DirHeader.pbCftTableEnd = pbDirectory + DirHeader.CftTableOffset + DirHeader.CftTableSize; // if(DirHeader.pbCftTableEnd > pbDataEnd) // return ERROR_BAD_FORMAT; return ERROR_SUCCESS; } LPBYTE CaptureVfsSpanCount(TVFS_DIRECTORY_HEADER & DirHeader, DWORD dwVfsOffset, DWORD & SpanCount) { LPBYTE pbVfsFileTable = DirHeader.DataAt(DirHeader.VfsTableOffset); LPBYTE pbVfsFileEntry = pbVfsFileTable + dwVfsOffset; LPBYTE pbVfsFileEnd = pbVfsFileTable + DirHeader.VfsTableSize; // Get the number of span entries if(!(pbVfsFileTable <= pbVfsFileEntry && pbVfsFileEntry < pbVfsFileEnd)) return NULL; SpanCount = *pbVfsFileEntry++; // 1 - 224 = valid file, 225-254 = other file, 255 = deleted file // We will ignore all files with unsupported span count return (1 <= SpanCount && SpanCount <= 224) ? pbVfsFileEntry : NULL; } LPBYTE CaptureVfsSpanEntries(TVFS_DIRECTORY_HEADER & DirHeader, LPBYTE pbVfsSpanEntry, PCASC_CKEY_ENTRY PtrSpanEntry, size_t SpanCount) { LPBYTE pbCftFileTable; LPBYTE pbCftFileEntry; LPBYTE pbCftFileEnd; LPBYTE pbVfsFileTable = DirHeader.DataAt(DirHeader.VfsTableOffset); LPBYTE pbVfsFileEnd = pbVfsFileTable + DirHeader.VfsTableSize; size_t ItemSize = sizeof(DWORD) + sizeof(DWORD) + DirHeader.CftOffsSize; // Check whether all spans are included in the valid range if(pbVfsSpanEntry < pbVfsFileTable || (pbVfsSpanEntry + (ItemSize * SpanCount)) > pbVfsFileEnd) return NULL; // Convert all spans for(size_t i = 0; i < SpanCount; i++) { DWORD dwCftOffset = ConvertBytesToInteger_X(pbVfsSpanEntry + sizeof(DWORD) + sizeof(DWORD), DirHeader.CftOffsSize); // // Structure of the span entry: // (4bytes): Offset into the referenced file (big endian) // (4bytes): Size of the span (big endian) // (?bytes): Offset into Container File Table. Length depends on container file table size // // Resolve the Container File Table entry pbCftFileTable = DirHeader.DataAt(DirHeader.CftTableOffset); pbCftFileEntry = pbCftFileTable + dwCftOffset; pbCftFileEnd = pbCftFileTable + DirHeader.CftTableSize; // Capture the EKey and the file size if((pbCftFileEntry + DirHeader.EKeySize + sizeof(DWORD)) > pbCftFileEnd) return NULL; // Copy the EKey and content size CaptureEncodedKey(PtrSpanEntry->EKey, pbCftFileEntry, DirHeader.EKeySize); PtrSpanEntry->ContentSize = ConvertBytesToInteger_4(pbVfsSpanEntry + sizeof(DWORD)); // Move to the next entry pbVfsSpanEntry += ItemSize; PtrSpanEntry++; } return pbVfsSpanEntry; } // // Structure of the path table entry: // (1byte) 0x00 (optional) - means that there will be prefix path separator // (1byte) File name length // (?byte) File name // (1byte) 0x00 (optional) - means that there will be postfix path separator // (1byte) 0xFF (optional) - node value identifier // (4byte) - node value // // Note: The path "data\archive\maps\file.bmp" could be cut into nodes like: // data\0 (or data with subdirectory) // arc // hive\0 // maps\0 (or folder data) // file.bmp // LPBYTE CapturePathEntry(TVFS_PATH_TABLE_ENTRY & PathEntry, LPBYTE pbPathTablePtr, LPBYTE pbPathTableEnd) { // Reset the path entry structure PathEntry.m_pNamePtr = (char *)(pbPathTablePtr); PathEntry.m_pNameEnd = (char *)(pbPathTablePtr); PathEntry.NodeFlags = 0; PathEntry.NodeValue = 0; // Zero before the name means prefix path separator if(pbPathTablePtr < pbPathTableEnd && pbPathTablePtr[0] == 0) { PathEntry.NodeFlags |= TVFS_PTE_PATH_SEPARATOR_PRE; pbPathTablePtr++; } // Capture the length of the name fragment if(pbPathTablePtr < pbPathTableEnd && pbPathTablePtr[0] != 0xFF) { // Capture length of the name fragment size_t nLength = *pbPathTablePtr++; if((pbPathTablePtr + nLength) > pbPathTableEnd) return NULL; PathEntry.m_pNamePtr = (char *)(pbPathTablePtr); PathEntry.m_pNameEnd = (char *)(pbPathTablePtr + nLength); pbPathTablePtr += nLength; } // Zero after the name means postfix path separator if(pbPathTablePtr < pbPathTableEnd && pbPathTablePtr[0] == 0) { PathEntry.NodeFlags |= TVFS_PTE_PATH_SEPARATOR_POST; pbPathTablePtr++; } if(pbPathTablePtr < pbPathTableEnd) { // Check for node value if(pbPathTablePtr[0] == 0xFF) { if((pbPathTablePtr + 1 + sizeof(DWORD)) > pbPathTableEnd) return NULL; PathEntry.NodeValue = ConvertBytesToInteger_4(pbPathTablePtr + 1); PathEntry.NodeFlags |= TVFS_PTE_NODE_VALUE; pbPathTablePtr = pbPathTablePtr + 1 + sizeof(DWORD); } // Non-0xFF after the name means path separator after else { PathEntry.NodeFlags |= TVFS_PTE_PATH_SEPARATOR_POST; assert(pbPathTablePtr[0] != 0); } } return pbPathTablePtr; } bool IsVfsFileEKey(TCascStorage * hs, LPBYTE EKey, size_t EKeyLength) { PCASC_CKEY_ENTRY pCKeyEntry; size_t ItemCount = hs->VfsRootList.ItemCount(); // Search the array for (size_t i = 0; i < ItemCount; i++) { pCKeyEntry = (PCASC_CKEY_ENTRY)hs->VfsRootList.ItemAt(i); if(pCKeyEntry != NULL) { if(!memcmp(pCKeyEntry->EKey, EKey, EKeyLength)) return true; } } // Not found in the VFS list return false; } // This function verifies whether a file is actually a sub-directory. // If yes, it contains just another "TVFS" virtual file system, just like the ROOT file. DWORD IsVfsSubDirectory(TCascStorage * hs, TVFS_DIRECTORY_HEADER & DirHeader, TVFS_DIRECTORY_HEADER & SubHeader, LPBYTE EKey, DWORD dwFileSize) { PCASC_CKEY_ENTRY pCKeyEntry; CASC_BLOB VfsData; DWORD dwErrCode = ERROR_BAD_FORMAT; // Keep compiler happy CASCLIB_UNUSED(dwFileSize); // Verify whether the EKey is in the list of VFS root files if(IsVfsFileEKey(hs, EKey, DirHeader.EKeySize)) { // Locate the CKey entry if((pCKeyEntry = FindCKeyEntry_EKey(hs, EKey)) != NULL) { // Load the entire file into memory dwErrCode = LoadInternalFileToMemory(hs, pCKeyEntry, VfsData); if(dwErrCode == ERROR_SUCCESS && VfsData.cbData) { // Capture the file folder. This also serves as test dwErrCode = CaptureDirectoryHeader(SubHeader, VfsData); if(dwErrCode == ERROR_SUCCESS) return dwErrCode; // Clear the captured header memset(&SubHeader, 0, sizeof(TVFS_DIRECTORY_HEADER)); } } } return dwErrCode; } PCASC_CKEY_ENTRY InsertUnknownCKeyEntry(TCascStorage * hs, LPBYTE pbEKey, size_t cbEKey, DWORD ContentSize) { PCASC_CKEY_ENTRY pCKeyEntry; // Insert a new entry to the array. DO NOT ALLOW enlarge array here pCKeyEntry = (PCASC_CKEY_ENTRY)hs->CKeyArray.Insert(1, false); if(pCKeyEntry != NULL) { memset(pCKeyEntry, 0, sizeof(CASC_CKEY_ENTRY)); memcpy(pCKeyEntry->EKey, pbEKey, cbEKey); pCKeyEntry->StorageOffset = CASC_INVALID_OFFS64; pCKeyEntry->ContentSize = ContentSize; pCKeyEntry->EncodedSize = CASC_INVALID_SIZE; pCKeyEntry->Flags = CASC_CE_HAS_EKEY | CASC_CE_HAS_EKEY_PARTIAL; pCKeyEntry->SpanCount = 1; // Copy the information from index files to the CKey entry CopyEKeyEntry(hs, pCKeyEntry); // Insert the item into EKey map hs->EKeyMap.InsertObject(pCKeyEntry, pCKeyEntry->EKey); } return pCKeyEntry; } void InsertRootVfsEntry(TCascStorage * hs, LPBYTE pbCKey, const char * szFormat, size_t nIndex) { PCASC_CKEY_ENTRY pCKeyEntry; char szFileName[0x20]; // The CKey entry must exist if((pCKeyEntry = FindCKeyEntry_CKey(hs, pbCKey)) != NULL) { CascStrPrintf(szFileName, _countof(szFileName), szFormat, nIndex); Insert(szFileName, pCKeyEntry); } } DWORD ParsePathFileTable(TCascStorage * hs, TVFS_DIRECTORY_HEADER & DirHeader, CASC_PATH & PathBuffer, LPBYTE pbPathTablePtr, LPBYTE pbPathTableEnd) { TVFS_DIRECTORY_HEADER SubHeader; TVFS_PATH_TABLE_ENTRY PathEntry; PCASC_CKEY_ENTRY pCKeyEntry; LPBYTE pbVfsSpanEntry; size_t nSavePos = PathBuffer.Save(); DWORD dwSpanCount; DWORD dwErrCode; // Sanity check assert(SpanArray.IsInitialized()); // Parse the file table while(pbPathTablePtr < pbPathTableEnd) { // Capture the single path table entry pbPathTablePtr = CapturePathEntry(PathEntry, pbPathTablePtr, pbPathTableEnd); if(pbPathTablePtr == NULL) return ERROR_BAD_FORMAT; // Append the node name to the total path. Also add backslash, if it's a folder PathBuffer_AppendNode(PathBuffer, PathEntry); // Folder component if(PathEntry.NodeFlags & TVFS_PTE_NODE_VALUE) { // If the TVFS_FOLDER_NODE is set, then the path node is a directory, // with its data immediately following the path node. Lower 31 bits of NodeValue // contain the length of the directory (including the NodeValue!) if(PathEntry.NodeValue & TVFS_FOLDER_NODE) { LPBYTE pbDirectoryEnd = pbPathTablePtr + (PathEntry.NodeValue & TVFS_FOLDER_SIZE_MASK) - sizeof(DWORD); // Check the available data assert((PathEntry.NodeValue & TVFS_FOLDER_SIZE_MASK) >= sizeof(DWORD)); // Recursively call the folder parser on the same file dwErrCode = ParsePathFileTable(hs, DirHeader, PathBuffer, pbPathTablePtr, pbDirectoryEnd); if(dwErrCode != ERROR_SUCCESS) return dwErrCode; // Skip the directory data pbPathTablePtr = pbDirectoryEnd; } else { // Capture the number of VFS spans pbVfsSpanEntry = CaptureVfsSpanCount(DirHeader, PathEntry.NodeValue, dwSpanCount); if(pbVfsSpanEntry == NULL) return ERROR_BAD_FORMAT; // If it's one span, it's either a subdirectory or an entire file if(dwSpanCount == 1) { CASC_CKEY_ENTRY SpanEntry; // Capture the single span entry pbVfsSpanEntry = CaptureVfsSpanEntries(DirHeader, pbVfsSpanEntry, &SpanEntry, 1); if(pbVfsSpanEntry == NULL) return ERROR_FILE_CORRUPT; // Find the CKey entry pCKeyEntry = FindCKeyEntry_EKey(hs, SpanEntry.EKey); if(pCKeyEntry == NULL) { // Some files are in the ROOT manifest even if they are not in ENCODING and DOWNLOAD. // Example: "2018 - New CASC\00001", file "DivideAndConquer.w3m:war3mapMap.blp" pCKeyEntry = InsertUnknownCKeyEntry(hs, SpanEntry.EKey, DirHeader.EKeySize, SpanEntry.ContentSize); if(pCKeyEntry == NULL) { return ERROR_NOT_ENOUGH_MEMORY; } } //BREAKIF(strcmp((const char *)PathBuffer, "Base") == 0); //BREAKIF(strcmp((const char *)PathBuffer, "base") == 0); //BREAKIF(strcmp((const char *)PathBuffer, "base:ComplexTypeDescriptorSizes.dat") == 0); //BREAKIF(strcmp((const char *)PathBuffer, "DivideAndConquer.w3m:war3map.doo") == 0); // We need to check whether this is another TVFS directory file if(IsVfsSubDirectory(hs, DirHeader, SubHeader, SpanEntry.EKey, SpanEntry.ContentSize) == ERROR_SUCCESS) { // Add colon (':') PathBuffer.AppendChar(':'); // The file content size should already be there assert(pCKeyEntry->ContentSize == SpanEntry.ContentSize); FileTree.InsertByName(pCKeyEntry, PathBuffer); // Parse the subdir. On error, stop the parsing dwErrCode = ParseDirectoryData(hs, SubHeader, PathBuffer); if(dwErrCode != ERROR_SUCCESS) return dwErrCode; } else { TVFS_WOW_ENTRY WowEntry; // If the content content size is not there, supply it now if(pCKeyEntry->ContentSize == CASC_INVALID_SIZE) pCKeyEntry->ContentSize = SpanEntry.ContentSize; // Detect generic file names from World of Warcraft (since build 45779) switch(dwErrCode = CheckWoWGenericName(PathBuffer, WowEntry)) { case ERROR_SUCCESS: // The entry was recognized and has the right format FileTree.InsertByName(pCKeyEntry, PathBuffer, WowEntry.FileDataId, WowEntry.LocaleFlags, WowEntry.ContentFlags); break; case ERROR_BAD_FORMAT: // The entry was not recognized as TVFS WoW name FileTree.InsertByName(pCKeyEntry, PathBuffer); break; default: // The entry has a bad format - use classic ROOT file assert(dwErrCode == ERROR_REPARSE_ROOT); return dwErrCode; } // If not a generic name, insert to the tree //printf("%s\n", (const char *)PathBuffer); } } else { PCASC_CKEY_ENTRY pSpanEntries; PCASC_FILE_NODE pFileNode; DWORD RefCount; bool bFilePresent = true; // // Need to support multi-span files, possibly lager than 4 GB // Example: CoD: Black Ops 4, file "zone/base.xpak" 0x16 spans, over 15 GB size // // Allocate buffer for all span entries pSpanEntries = (PCASC_CKEY_ENTRY)SpanArray.Insert(dwSpanCount); if(pSpanEntries == NULL) return ERROR_NOT_ENOUGH_MEMORY; // Capture all span entries pbVfsSpanEntry = CaptureVfsSpanEntries(DirHeader, pbVfsSpanEntry, pSpanEntries, dwSpanCount); if(pbVfsSpanEntry == NULL) return ERROR_FILE_CORRUPT; // Parse all span entries for(DWORD dwSpanIndex = 0; dwSpanIndex < dwSpanCount; dwSpanIndex++) { PCASC_CKEY_ENTRY pSpanEntry = pSpanEntries + dwSpanIndex; // Find the CKey entry pCKeyEntry = FindCKeyEntry_EKey(hs, pSpanEntries[dwSpanIndex].EKey); if(pCKeyEntry == NULL) { bFilePresent = false; break; } // Supply the content size if(pCKeyEntry->ContentSize == CASC_INVALID_SIZE) pCKeyEntry->ContentSize = pSpanEntry->ContentSize; assert(pCKeyEntry->ContentSize == pSpanEntry->ContentSize); // Fill-in the span entry if(dwSpanIndex == 0) { pCKeyEntry->SpanCount = (BYTE)(dwSpanCount); pCKeyEntry->RefCount++; } else { // Mark the CKey entry as a file span. Note that a CKey entry // can actually be both a file span and a standalone file: // * zone/zm_red.xpak - { zone/zm_red.xpak_1, zone/zm_red.xpak_2, ..., zone/zm_red.xpak_6 } pCKeyEntry->Flags |= CASC_CE_FILE_SPAN; } // Copy all from the existing CKey entry memcpy(pSpanEntry, pCKeyEntry, sizeof(CASC_CKEY_ENTRY)); } // Do nothing if the file is not present locally if(bFilePresent) { // Insert a new file node that will contain pointer to the span entries RefCount = pSpanEntries->RefCount; pFileNode = FileTree.InsertByName(pSpanEntries, PathBuffer); pSpanEntries->RefCount = RefCount; if(pFileNode == NULL) return ERROR_NOT_ENOUGH_MEMORY; } } } // Reset the position of the path buffer PathBuffer.Restore(nSavePos); } } // Return the total number of entries return ERROR_SUCCESS; } DWORD ParseDirectoryData(TCascStorage * hs, TVFS_DIRECTORY_HEADER & DirHeader, CASC_PATH & PathBuffer) { LPBYTE pbRootDirectory = DirHeader.DataAt(DirHeader.PathTableOffset); LPBYTE pbRootDirPtr = pbRootDirectory; LPBYTE pbRootDirEnd = pbRootDirPtr + DirHeader.PathTableSize; DWORD dwNodeValue = 0; // Most usually, there is a root directory in the folder if((pbRootDirPtr + 1 + sizeof(DWORD)) < pbRootDirEnd) { // // The structure of the root directory // ----------------------------------- // 1byte 0xFF // 4bytes NodeValue (BigEndian). The most significant bit is set // - Lower 31 bits contain length of the directory data, including NodeValue // if(pbRootDirPtr[0] == 0xFF) { // Get the NodeValue and check its highest bit if(CaptureInteger32_BE(pbRootDirPtr + 1, pbRootDirEnd, &dwNodeValue) == NULL || (dwNodeValue & TVFS_FOLDER_NODE) == 0) return ERROR_BAD_FORMAT; // Get the range of the root directory pbRootDirEnd = pbRootDirPtr + 1 + (dwNodeValue & TVFS_FOLDER_SIZE_MASK); pbRootDirPtr = pbRootDirPtr + 1 + sizeof(DWORD); // Check the directory if(pbRootDirEnd > (pbRootDirectory + DirHeader.PathTableSize)) return ERROR_BAD_FORMAT; } } // Now go parse the path file table return ParsePathFileTable(hs, DirHeader, PathBuffer, pbRootDirPtr, pbRootDirEnd); } DWORD Load(TCascStorage * hs, TVFS_DIRECTORY_HEADER & RootHeader) { CASC_PATH PathBuffer; DWORD dwErrCode; // Save the length of the key FileTree.SetKeyLength(RootHeader.EKeySize); // Initialize the array of span entries dwErrCode = SpanArray.Create(sizeof(CASC_CKEY_ENTRY), 0x100); if(dwErrCode != ERROR_SUCCESS) return dwErrCode; // Insert the main VFS root file as named entry InsertRootVfsEntry(hs, hs->VfsRoot.CKey, "vfs-root", 0); // Insert all VFS roots folders as files //for(size_t i = 0; i < hs->VfsRootList.ItemCount(); i++) //{ // pCKeyEntry = (PCASC_CKEY_ENTRY)hs->VfsRootList.ItemAt(i); // InsertRootVfsEntry(hs, pCKeyEntry->CKey, "vfs-%u", i+1); //} // Parse the entire directory data return ParseDirectoryData(hs, RootHeader, PathBuffer); } DWORD CheckWoWGenericName(const CASC_PATH & PathBuffer, TVFS_WOW_ENTRY & WowEntry) { size_t nPathLength = PathBuffer.Length(); BYTE BinaryBuffer[4+2+4+16]; // // WoW Build 45779: 000000020000:000C472F02BA924C604A670B253AA02DBCD9441 (Bug: Missing last digit of the CKey) // WoW Build 46144: 000000020000:000C472F02BA924C604A670B253AA02DBCD9441C // LLLLLLLLCCCC IIIIIIIIKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK // // L = Locale flags, C = Content flags, I = File Data ID, K = CKey // if(nPathLength == 52 || nPathLength == 53) { if(PathBuffer[12] == ':') { // Check the first part of the TVFS name if(BinaryFromString(&PathBuffer[00], 12, (LPBYTE)(&BinaryBuffer[0])) != ERROR_SUCCESS) return ERROR_REPARSE_ROOT; // Check the second part of the file name if(BinaryFromString(&PathBuffer[13], 40, (LPBYTE)(&BinaryBuffer[6])) != ERROR_SUCCESS) return ERROR_REPARSE_ROOT; #ifdef TVFS_PARSE_WOW_ROOT // We accept strings with length 53 chars if(nPathLength == 53) { WowEntry.LocaleFlags = ConvertBytesToInteger_4(BinaryBuffer + 0x00); WowEntry.ContentFlags = ConvertBytesToInteger_2(BinaryBuffer + 0x04); WowEntry.FileDataId = ConvertBytesToInteger_4(BinaryBuffer + 0x06); memcpy(WowEntry.ContentKey, BinaryBuffer + 0x0A, MD5_HASH_SIZE); return ERROR_SUCCESS; } #endif // TVFS_PARSE_WOW_ROOT // An invalid entry - reparse tot he normal root CASCLIB_UNUSED(WowEntry); return ERROR_REPARSE_ROOT; } } return ERROR_BAD_FORMAT; } CASC_ARRAY SpanArray; // Array of CASC_SPAN_ENTRY for all multi-span files }; //----------------------------------------------------------------------------- // Public functions - TVFS root DWORD RootHandler_CreateTVFS(TCascStorage * hs, CASC_BLOB & RootFile) { TRootHandler_TVFS * pRootHandler = NULL; TVFS_DIRECTORY_HEADER RootHeader; DWORD dwErrCode; // Capture the entire root directory dwErrCode = TRootHandler_TVFS::CaptureDirectoryHeader(RootHeader, RootFile); if(dwErrCode == ERROR_SUCCESS) { // Allocate the root handler object pRootHandler = new TRootHandler_TVFS(); if(pRootHandler != NULL) { // Load the root directory. If load failed, we free the object dwErrCode = pRootHandler->Load(hs, RootHeader); if(dwErrCode != ERROR_SUCCESS && dwErrCode != ERROR_REPARSE_ROOT) { delete pRootHandler; pRootHandler = NULL; } } } // Assign the root directory (or NULL) and return error hs->pRootHandler = pRootHandler; return dwErrCode; }