Shipwright/StormLib/src/SBaseFileTable.cpp

3102 lines
109 KiB
C++

/*****************************************************************************/
/* SBaseFileTable.cpp Copyright (c) Ladislav Zezula 2010 */
/*---------------------------------------------------------------------------*/
/* Description: Common handler for classic and new hash&block tables */
/*---------------------------------------------------------------------------*/
/* Date Ver Who Comment */
/* -------- ---- --- ------- */
/* 06.09.10 1.00 Lad The first version of SBaseFileTable.cpp */
/*****************************************************************************/
#define __STORMLIB_SELF__
#include "StormLib.h"
#include "StormCommon.h"
//-----------------------------------------------------------------------------
// Local defines
#define INVALID_FLAG_VALUE 0xCCCCCCCC
#define MAX_FLAG_INDEX 512
//-----------------------------------------------------------------------------
// Support for calculating bit sizes
static void InitFileFlagArray(LPDWORD FlagArray)
{
memset(FlagArray, 0xCC, MAX_FLAG_INDEX * sizeof(DWORD));
}
static DWORD GetFileFlagIndex(LPDWORD FlagArray, DWORD dwFlags)
{
// Find free or equal entry in the flag array
for(DWORD dwFlagIndex = 0; dwFlagIndex < MAX_FLAG_INDEX; dwFlagIndex++)
{
if(FlagArray[dwFlagIndex] == INVALID_FLAG_VALUE || FlagArray[dwFlagIndex] == dwFlags)
{
FlagArray[dwFlagIndex] = dwFlags;
return dwFlagIndex;
}
}
// This should never happen
assert(false);
return 0xFFFFFFFF;
}
static DWORD GetNecessaryBitCount(ULONGLONG MaxValue)
{
DWORD dwBitCount = 0;
while(MaxValue > 0)
{
MaxValue >>= 1;
dwBitCount++;
}
return dwBitCount;
}
//-----------------------------------------------------------------------------
// Implementation of the TMPQBits struct
struct TMPQBits
{
static TMPQBits * Create(DWORD NumberOfBits, BYTE FillValue);
void GetBits(unsigned int nBitPosition, unsigned int nBitLength, void * pvBuffer, int nResultSize);
void SetBits(unsigned int nBitPosition, unsigned int nBitLength, void * pvBuffer, int nResultSize);
static const USHORT SetBitsMask[]; // Bit mask for each number of bits (0-8)
DWORD NumberOfBytes; // Total number of bytes in "Elements"
DWORD NumberOfBits; // Total number of bits that are available
BYTE Elements[1]; // Array of elements (variable length)
};
const USHORT TMPQBits::SetBitsMask[] = {0x00, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF};
TMPQBits * TMPQBits::Create(
DWORD NumberOfBits,
BYTE FillValue)
{
TMPQBits * pBitArray;
size_t nSize = sizeof(TMPQBits) + (NumberOfBits + 7) / 8;
// Allocate the bit array
pBitArray = (TMPQBits *)STORM_ALLOC(BYTE, nSize);
if(pBitArray != NULL)
{
memset(pBitArray, FillValue, nSize);
pBitArray->NumberOfBytes = (NumberOfBits + 7) / 8;
pBitArray->NumberOfBits = NumberOfBits;
}
return pBitArray;
}
void TMPQBits::GetBits(
unsigned int nBitPosition,
unsigned int nBitLength,
void * pvBuffer,
int nResultByteSize)
{
unsigned char * pbBuffer = (unsigned char *)pvBuffer;
unsigned int nBytePosition0 = (nBitPosition / 8);
unsigned int nBytePosition1 = nBytePosition0 + 1;
unsigned int nByteLength = (nBitLength / 8);
unsigned int nBitOffset = (nBitPosition & 0x07);
unsigned char BitBuffer;
// Keep compiler happy for platforms where nResultByteSize is not used
nResultByteSize = nResultByteSize;
#ifdef _DEBUG
// Check if the target is properly zeroed
for(int i = 0; i < nResultByteSize; i++)
assert(pbBuffer[i] == 0);
#endif
#ifndef STORMLIB_LITTLE_ENDIAN
// Adjust the buffer pointer for big endian platforms
pbBuffer += (nResultByteSize - 1);
#endif
// Copy whole bytes, if any
while(nByteLength > 0)
{
// Is the current position in the Elements byte-aligned?
if(nBitOffset != 0)
{
BitBuffer = (unsigned char)((Elements[nBytePosition0] >> nBitOffset) | (Elements[nBytePosition1] << (0x08 - nBitOffset)));
}
else
{
BitBuffer = Elements[nBytePosition0];
}
#ifdef STORMLIB_LITTLE_ENDIAN
*pbBuffer++ = BitBuffer;
#else
*pbBuffer-- = BitBuffer;
#endif
// Move byte positions and lengths
nBytePosition1++;
nBytePosition0++;
nByteLength--;
}
// Get the rest of the bits
nBitLength = (nBitLength & 0x07);
if(nBitLength != 0)
{
*pbBuffer = (unsigned char)(Elements[nBytePosition0] >> nBitOffset);
if(nBitLength > (8 - nBitOffset))
*pbBuffer = (unsigned char)((Elements[nBytePosition1] << (8 - nBitOffset)) | (Elements[nBytePosition0] >> nBitOffset));
*pbBuffer &= (0x01 << nBitLength) - 1;
}
}
void TMPQBits::SetBits(
unsigned int nBitPosition,
unsigned int nBitLength,
void * pvBuffer,
int nResultByteSize)
{
unsigned char * pbBuffer = (unsigned char *)pvBuffer;
unsigned int nBytePosition = (nBitPosition / 8);
unsigned int nBitOffset = (nBitPosition & 0x07);
unsigned short BitBuffer = 0;
unsigned short AndMask = 0;
unsigned short OneByte = 0;
// Keep compiler happy for platforms where nResultByteSize is not used
nResultByteSize = nResultByteSize;
#ifndef STORMLIB_LITTLE_ENDIAN
// Adjust the buffer pointer for big endian platforms
pbBuffer += (nResultByteSize - 1);
#endif
// Copy whole bytes, if any
while(nBitLength > 8)
{
// Reload the bit buffer
#ifdef STORMLIB_LITTLE_ENDIAN
OneByte = *pbBuffer++;
#else
OneByte = *pbBuffer--;
#endif
// Update the BitBuffer and AndMask for the bit array
BitBuffer = (BitBuffer >> 0x08) | (OneByte << nBitOffset);
AndMask = (AndMask >> 0x08) | (0x00FF << nBitOffset);
// Update the byte in the array
Elements[nBytePosition] = (BYTE)((Elements[nBytePosition] & ~AndMask) | BitBuffer);
// Move byte positions and lengths
nBytePosition++;
nBitLength -= 0x08;
}
if(nBitLength != 0)
{
// Reload the bit buffer
OneByte = *pbBuffer;
// Update the AND mask for the last bit
BitBuffer = (BitBuffer >> 0x08) | (OneByte << nBitOffset);
AndMask = (AndMask >> 0x08) | (SetBitsMask[nBitLength] << nBitOffset);
// Update the byte in the array
Elements[nBytePosition] = (BYTE)((Elements[nBytePosition] & ~AndMask) | BitBuffer);
// Update the next byte, if needed
if(AndMask & 0xFF00)
{
nBytePosition++;
BitBuffer >>= 0x08;
AndMask >>= 0x08;
Elements[nBytePosition] = (BYTE)((Elements[nBytePosition] & ~AndMask) | BitBuffer);
}
}
}
void GetMPQBits(TMPQBits * pBits, unsigned int nBitPosition, unsigned int nBitLength, void * pvBuffer, int nResultByteSize)
{
pBits->GetBits(nBitPosition, nBitLength, pvBuffer, nResultByteSize);
}
//-----------------------------------------------------------------------------
// Support for MPQ header
static bool VerifyTablePosition64(
ULONGLONG MpqOffset, // Position of the MPQ header
ULONGLONG TableOffset, // Position of the MPQ table, relative to MPQ header
ULONGLONG TableSize, // Size of the MPQ table, in bytes
ULONGLONG FileSize) // Size of the entire file, in bytes
{
if(TableOffset != 0)
{
// Verify overflows
if((MpqOffset + TableOffset) < MpqOffset)
return false;
if((MpqOffset + TableOffset + TableSize) < MpqOffset)
return false;
// Verify sizes
if(TableOffset >= FileSize || TableSize >= FileSize)
return false;
if((MpqOffset + TableOffset) >= FileSize)
return false;
if((MpqOffset + TableOffset + TableSize) >= FileSize)
return false;
}
return true;
}
static bool VerifyTableTandemPositions(
ULONGLONG MpqOffset, // Position of the MPQ header
ULONGLONG TableOffset1, // 1st table: Position, relative to MPQ header
ULONGLONG TableSize1, // 1st table: Size in bytes
ULONGLONG TableOffset2, // 2nd table: Position, relative to MPQ header
ULONGLONG TableSize2, // 2nd table: Size in bytes
ULONGLONG FileSize) // Size of the entire file, in bytes
{
return VerifyTablePosition64(MpqOffset, TableOffset1, TableSize1, FileSize) &&
VerifyTablePosition64(MpqOffset, TableOffset2, TableSize2, FileSize);
}
static ULONGLONG DetermineArchiveSize_V1(
TMPQArchive * ha,
TMPQHeader * pHeader,
ULONGLONG MpqOffset,
ULONGLONG FileSize)
{
ULONGLONG ByteOffset;
ULONGLONG EndOfMpq = FileSize;
DWORD SignatureHeader = 0;
DWORD dwArchiveSize32;
// This could only be called for MPQs version 1.0
assert(pHeader->wFormatVersion == MPQ_FORMAT_VERSION_1);
// Check if we can rely on the archive size in the header
if(pHeader->dwBlockTablePos < pHeader->dwArchiveSize)
{
// The block table cannot be compressed, so the sizes must match
if((pHeader->dwArchiveSize - pHeader->dwBlockTablePos) == (pHeader->dwBlockTableSize * sizeof(TMPQBlock)))
return pHeader->dwArchiveSize;
// If the archive size in the header is less than real file size
dwArchiveSize32 = (DWORD)(FileSize - MpqOffset);
if(pHeader->dwArchiveSize == dwArchiveSize32)
return pHeader->dwArchiveSize;
}
// Check if there is a signature header
if((EndOfMpq - MpqOffset) > (MPQ_STRONG_SIGNATURE_SIZE + 4))
{
ByteOffset = EndOfMpq - MPQ_STRONG_SIGNATURE_SIZE - 4;
if(FileStream_Read(ha->pStream, &ByteOffset, &SignatureHeader, sizeof(DWORD)))
{
if(BSWAP_INT32_UNSIGNED(SignatureHeader) == MPQ_STRONG_SIGNATURE_ID)
EndOfMpq = EndOfMpq - MPQ_STRONG_SIGNATURE_SIZE - 4;
}
}
// Return the returned archive size
return (EndOfMpq - MpqOffset);
}
static ULONGLONG DetermineArchiveSize_V2(
TMPQHeader * pHeader,
ULONGLONG MpqOffset,
ULONGLONG FileSize)
{
ULONGLONG EndOfMpq = FileSize;
DWORD dwArchiveSize32;
// This could only be called for MPQs version 2.0
assert(pHeader->wFormatVersion == MPQ_FORMAT_VERSION_2);
// Check if we can rely on the archive size in the header
if((FileSize >> 0x20) == 0)
{
if(pHeader->dwBlockTablePos < pHeader->dwArchiveSize)
{
if((pHeader->dwArchiveSize - pHeader->dwBlockTablePos) <= (pHeader->dwBlockTableSize * sizeof(TMPQBlock)))
return pHeader->dwArchiveSize;
// If the archive size in the header is less than real file size
dwArchiveSize32 = (DWORD)(FileSize - MpqOffset);
if(pHeader->dwArchiveSize <= dwArchiveSize32)
return pHeader->dwArchiveSize;
}
}
// Return the calculated archive size
return (EndOfMpq - MpqOffset);
}
static ULONGLONG DetermineArchiveSize_V4(
TMPQHeader * pHeader,
ULONGLONG /* MpqOffset */,
ULONGLONG /* FileSize */)
{
ULONGLONG ArchiveSize = 0;
ULONGLONG EndOfTable;
// This could only be called for MPQs version 4
assert(pHeader->wFormatVersion == MPQ_FORMAT_VERSION_4);
// Check position of BET table, if correct
if((pHeader->BetTablePos64 >> 0x20) == 0 && (pHeader->BetTableSize64 >> 0x20) == 0)
{
EndOfTable = pHeader->BetTablePos64 + pHeader->BetTableSize64;
if(EndOfTable > ArchiveSize)
ArchiveSize = EndOfTable;
}
// Check position of HET table, if correct
if((pHeader->HetTablePos64 >> 0x20) == 0 && (pHeader->HetTableSize64 >> 0x20) == 0)
{
EndOfTable = pHeader->HetTablePos64 + pHeader->HetTableSize64;
if(EndOfTable > ArchiveSize)
ArchiveSize = EndOfTable;
}
EndOfTable = pHeader->dwHashTablePos + pHeader->dwHashTableSize * sizeof(TMPQHash);
if(EndOfTable > ArchiveSize)
ArchiveSize = EndOfTable;
EndOfTable = pHeader->dwBlockTablePos + pHeader->dwBlockTableSize * sizeof(TMPQBlock);
if(EndOfTable > ArchiveSize)
ArchiveSize = EndOfTable;
// Return the calculated archive size
return ArchiveSize;
}
ULONGLONG FileOffsetFromMpqOffset(TMPQArchive * ha, ULONGLONG MpqOffset)
{
if(ha->pHeader->wFormatVersion == MPQ_FORMAT_VERSION_1)
{
// For MPQ archive v1, any file offset is only 32-bit
return (ULONGLONG)((DWORD)ha->MpqPos + (DWORD)MpqOffset);
}
else
{
// For MPQ archive v2+, file offsets are full 64-bit
return ha->MpqPos + MpqOffset;
}
}
ULONGLONG CalculateRawSectorOffset(
TMPQFile * hf,
DWORD dwSectorOffset)
{
ULONGLONG RawFilePos;
// Must be used for files within a MPQ
assert(hf->ha != NULL);
assert(hf->ha->pHeader != NULL);
//
// Some MPQ protectors place the sector offset table after the actual file data.
// Sector offsets in the sector offset table are negative. When added
// to MPQ file offset from the block table entry, the result is a correct
// position of the file data in the MPQ.
//
// For MPQs version 1.0, the offset is purely 32-bit
//
RawFilePos = hf->RawFilePos + dwSectorOffset;
if(hf->ha->pHeader->wFormatVersion == MPQ_FORMAT_VERSION_1)
RawFilePos = (DWORD)hf->ha->MpqPos + (DWORD)hf->pFileEntry->ByteOffset + dwSectorOffset;
// We also have to add patch header size, if patch header is present
if(hf->pPatchInfo != NULL)
RawFilePos += hf->pPatchInfo->dwLength;
// Return the result offset
return RawFilePos;
}
// This function converts the MPQ header so it always looks like version 4
DWORD ConvertMpqHeaderToFormat4(
TMPQArchive * ha,
ULONGLONG ByteOffset,
ULONGLONG FileSize,
DWORD dwFlags,
MTYPE MapType)
{
TMPQHeader * pHeader = (TMPQHeader *)ha->HeaderData;
ULONGLONG BlockTablePos64 = 0;
ULONGLONG HashTablePos64 = 0;
ULONGLONG BlockTableMask = (ULONGLONG)-1;
ULONGLONG MaxOffset;
USHORT wFormatVersion = BSWAP_INT16_UNSIGNED(pHeader->wFormatVersion);
bool bHashBlockOffsetOK = false;
bool bHetBetOffsetOK = false;
DWORD dwErrCode = ERROR_SUCCESS;
// If version 1.0 is forced, then the format version is forced to be 1.0
// Reason: Storm.dll in Warcraft III ignores format version value
if((MapType == MapTypeWarcraft3) || (dwFlags & MPQ_OPEN_FORCE_MPQ_V1))
wFormatVersion = MPQ_FORMAT_VERSION_1;
// Don't accept format 3 for Starcraft II maps
if((MapType == MapTypeStarcraft2) && (pHeader->wFormatVersion > MPQ_FORMAT_VERSION_2))
wFormatVersion = MPQ_FORMAT_VERSION_4;
// Format-specific fixes
switch(wFormatVersion)
{
case MPQ_FORMAT_VERSION_1:
// Check for malformed MPQ header version 1.0
BSWAP_TMPQHEADER(pHeader, MPQ_FORMAT_VERSION_1);
if(pHeader->wFormatVersion != MPQ_FORMAT_VERSION_1 || pHeader->dwHeaderSize != MPQ_HEADER_SIZE_V1)
{
pHeader->wFormatVersion = MPQ_FORMAT_VERSION_1;
pHeader->dwHeaderSize = MPQ_HEADER_SIZE_V1;
ha->dwFlags |= MPQ_FLAG_MALFORMED;
}
//
// Note: The value of "dwArchiveSize" member in the MPQ header
// is ignored by Storm.dll and can contain garbage value
// ("w3xmaster" protector).
//
Label_ArchiveVersion1:
if(pHeader->dwBlockTableSize > 1) // Prevent empty MPQs being marked as malformed
{
if(pHeader->dwHashTablePos <= pHeader->dwHeaderSize || (pHeader->dwHashTablePos & 0x80000000))
ha->dwFlags |= MPQ_FLAG_MALFORMED;
if(pHeader->dwBlockTablePos <= pHeader->dwHeaderSize || (pHeader->dwBlockTablePos & 0x80000000))
ha->dwFlags |= MPQ_FLAG_MALFORMED;
}
// Only low byte of sector size is really used
if(pHeader->wSectorSize & 0xFF00)
ha->dwFlags |= MPQ_FLAG_MALFORMED;
pHeader->wSectorSize = pHeader->wSectorSize & 0xFF;
// Fill the rest of the header
memset((LPBYTE)pHeader + MPQ_HEADER_SIZE_V1, 0, sizeof(TMPQHeader) - MPQ_HEADER_SIZE_V1);
pHeader->BlockTableSize64 = pHeader->dwBlockTableSize * sizeof(TMPQBlock);
pHeader->HashTableSize64 = pHeader->dwHashTableSize * sizeof(TMPQHash);
pHeader->ArchiveSize64 = pHeader->dwArchiveSize;
// Block table position must be calculated as 32-bit value
// Note: BOBA protector puts block table before the MPQ header, so it is negative
BlockTablePos64 = (ULONGLONG)((DWORD)ByteOffset + pHeader->dwBlockTablePos);
BlockTableMask = 0xFFFFFFF0;
// Determine the archive size on malformed MPQs
if(ha->dwFlags & MPQ_FLAG_MALFORMED)
{
// Calculate the archive size
pHeader->ArchiveSize64 = DetermineArchiveSize_V1(ha, pHeader, ByteOffset, FileSize);
pHeader->dwArchiveSize = (DWORD)pHeader->ArchiveSize64;
}
// EWIX_v8_7.w3x: TMPQHeader::dwBlockTableSize = 0x00319601
// Size of TFileTable goes to ~200MB, so we artificially cut it
if(BlockTablePos64 + (pHeader->dwBlockTableSize * sizeof(TMPQBlock)) > FileSize)
{
pHeader->dwBlockTableSize = (DWORD)((FileSize - BlockTablePos64) / sizeof(TMPQBlock));
pHeader->BlockTableSize64 = pHeader->dwBlockTableSize * sizeof(TMPQBlock);
}
break;
case MPQ_FORMAT_VERSION_2:
// Check for malformed MPQ header version 1.0
BSWAP_TMPQHEADER(pHeader, MPQ_FORMAT_VERSION_2);
if(pHeader->wFormatVersion != MPQ_FORMAT_VERSION_2 || pHeader->dwHeaderSize != MPQ_HEADER_SIZE_V2)
{
pHeader->wFormatVersion = MPQ_FORMAT_VERSION_1;
pHeader->dwHeaderSize = MPQ_HEADER_SIZE_V1;
ha->dwFlags |= MPQ_FLAG_MALFORMED;
goto Label_ArchiveVersion1;
}
// Fill the rest of the header with zeros
memset((LPBYTE)pHeader + MPQ_HEADER_SIZE_V2, 0, sizeof(TMPQHeader) - MPQ_HEADER_SIZE_V2);
// Calculate the expected hash table size
pHeader->HashTableSize64 = (pHeader->dwHashTableSize * sizeof(TMPQHash));
HashTablePos64 = MAKE_OFFSET64(pHeader->wHashTablePosHi, pHeader->dwHashTablePos);
// Calculate the expected block table size
pHeader->BlockTableSize64 = (pHeader->dwBlockTableSize * sizeof(TMPQBlock));
BlockTablePos64 = MAKE_OFFSET64(pHeader->wBlockTablePosHi, pHeader->dwBlockTablePos);
// We require the block table to follow hash table
if(BlockTablePos64 >= HashTablePos64)
{
// HashTableSize64 may be less than TblSize * sizeof(TMPQHash).
// That means that the hash table is compressed.
pHeader->HashTableSize64 = BlockTablePos64 - HashTablePos64;
// Calculate the compressed block table size
if(pHeader->HiBlockTablePos64 != 0)
{
// BlockTableSize64 may be less than TblSize * sizeof(TMPQBlock).
// That means that the block table is compressed.
pHeader->BlockTableSize64 = pHeader->HiBlockTablePos64 - BlockTablePos64;
assert(pHeader->BlockTableSize64 <= (pHeader->dwBlockTableSize * sizeof(TMPQBlock)));
// Determine real archive size
pHeader->ArchiveSize64 = DetermineArchiveSize_V2(pHeader, ByteOffset, FileSize);
// Calculate the size of the hi-block table
pHeader->HiBlockTableSize64 = pHeader->ArchiveSize64 - pHeader->HiBlockTablePos64;
assert(pHeader->HiBlockTableSize64 == (pHeader->dwBlockTableSize * sizeof(USHORT)));
}
else
{
// Determine real archive size
pHeader->ArchiveSize64 = DetermineArchiveSize_V2(pHeader, ByteOffset, FileSize);
// Calculate size of the block table
pHeader->BlockTableSize64 = pHeader->ArchiveSize64 - BlockTablePos64;
assert(pHeader->BlockTableSize64 <= (pHeader->dwBlockTableSize * sizeof(TMPQBlock)));
}
}
else
{
pHeader->ArchiveSize64 = pHeader->dwArchiveSize;
ha->dwFlags |= MPQ_FLAG_MALFORMED;
}
// Add the MPQ Offset
BlockTablePos64 += ByteOffset;
break;
case MPQ_FORMAT_VERSION_3:
// In MPQ format 3.0, the entire header is optional
// and the size of the header can actually be identical
// to size of header 2.0
BSWAP_TMPQHEADER(pHeader, MPQ_FORMAT_VERSION_3);
if(pHeader->dwHeaderSize < MPQ_HEADER_SIZE_V3)
{
pHeader->ArchiveSize64 = pHeader->dwArchiveSize;
pHeader->HetTablePos64 = 0;
pHeader->BetTablePos64 = 0;
}
//
// We need to calculate the compressed size of each table. We assume the following order:
// 1) HET table
// 2) BET table
// 3) Classic hash table
// 4) Classic block table
// 5) Hi-block table
//
// Fill the rest of the header with zeros
memset((LPBYTE)pHeader + MPQ_HEADER_SIZE_V3, 0, sizeof(TMPQHeader) - MPQ_HEADER_SIZE_V3);
BlockTablePos64 = MAKE_OFFSET64(pHeader->wBlockTablePosHi, pHeader->dwBlockTablePos);
HashTablePos64 = MAKE_OFFSET64(pHeader->wHashTablePosHi, pHeader->dwHashTablePos);
MaxOffset = pHeader->ArchiveSize64;
// Size of the hi-block table
if(pHeader->HiBlockTablePos64)
{
pHeader->HiBlockTableSize64 = MaxOffset - pHeader->HiBlockTablePos64;
MaxOffset = pHeader->HiBlockTablePos64;
}
// Size of the block table
if(BlockTablePos64)
{
pHeader->BlockTableSize64 = MaxOffset - BlockTablePos64;
MaxOffset = BlockTablePos64;
}
// Size of the hash table
if(HashTablePos64)
{
pHeader->HashTableSize64 = MaxOffset - HashTablePos64;
MaxOffset = HashTablePos64;
}
// Size of the BET table
if(pHeader->BetTablePos64)
{
pHeader->BetTableSize64 = MaxOffset - pHeader->BetTablePos64;
MaxOffset = pHeader->BetTablePos64;
}
// Size of the HET table
if(pHeader->HetTablePos64)
{
pHeader->HetTableSize64 = MaxOffset - pHeader->HetTablePos64;
// MaxOffset = pHeader->HetTablePos64;
}
// Add the MPQ Offset
BlockTablePos64 += ByteOffset;
break;
case MPQ_FORMAT_VERSION_4:
// Verify header MD5. Header MD5 is calculated from the MPQ header since the 'MPQ\x1A'
// signature until the position of header MD5 at offset 0xC0
BSWAP_TMPQHEADER(pHeader, MPQ_FORMAT_VERSION_4);
// Apparently, Starcraft II only accepts MPQ headers where the MPQ header hash matches
// If MD5 doesn't match, we ignore this offset. We also ignore it if there's no MD5 at all
if(!IsValidMD5(pHeader->MD5_MpqHeader))
return ERROR_FAKE_MPQ_HEADER;
if(!VerifyDataBlockHash(pHeader, MPQ_HEADER_SIZE_V4 - MD5_DIGEST_SIZE, pHeader->MD5_MpqHeader))
return ERROR_FAKE_MPQ_HEADER;
// HiBlockTable must be 0 for archives under 4GB
if((pHeader->ArchiveSize64 >> 0x20) == 0 && pHeader->HiBlockTablePos64 != 0)
return ERROR_FAKE_MPQ_HEADER;
// Is the "HET&BET" table tandem OK?
bHetBetOffsetOK = VerifyTableTandemPositions(ByteOffset,
pHeader->HetTablePos64, pHeader->HetTableSize64,
pHeader->BetTablePos64, pHeader->BetTableSize64,
FileSize);
// Is the "Hash&Block" table tandem OK?
bHashBlockOffsetOK = VerifyTableTandemPositions(ByteOffset,
pHeader->dwHashTablePos, pHeader->HashTableSize64,
pHeader->dwBlockTablePos, pHeader->BlockTableSize64,
FileSize);
// At least one pair must be OK
if(bHetBetOffsetOK == false && bHashBlockOffsetOK == false)
return ERROR_FAKE_MPQ_HEADER;
// Check for malformed MPQs
if(pHeader->wFormatVersion != MPQ_FORMAT_VERSION_4 || (ByteOffset + pHeader->ArchiveSize64) != FileSize || (ByteOffset + pHeader->HiBlockTablePos64) >= FileSize)
{
pHeader->wFormatVersion = MPQ_FORMAT_VERSION_4;
pHeader->dwHeaderSize = MPQ_HEADER_SIZE_V4;
ha->dwFlags |= MPQ_FLAG_MALFORMED;
}
// Recalculate archive size
if(ha->dwFlags & MPQ_FLAG_MALFORMED)
{
// Calculate the archive size
pHeader->ArchiveSize64 = DetermineArchiveSize_V4(pHeader, ByteOffset, FileSize);
pHeader->dwArchiveSize = (DWORD)pHeader->ArchiveSize64;
}
// Calculate the block table position
BlockTablePos64 = ByteOffset + MAKE_OFFSET64(pHeader->wBlockTablePosHi, pHeader->dwBlockTablePos);
break;
default:
// Check if it's a War of the Immortal data file (SQP)
// If not, we treat it as malformed MPQ version 1.0
if(ConvertSqpHeaderToFormat4(ha, FileSize, dwFlags) != ERROR_SUCCESS)
{
pHeader->wFormatVersion = MPQ_FORMAT_VERSION_1;
pHeader->dwHeaderSize = MPQ_HEADER_SIZE_V1;
ha->dwFlags |= MPQ_FLAG_MALFORMED;
goto Label_ArchiveVersion1;
}
// Calculate the block table position
BlockTablePos64 = ByteOffset + MAKE_OFFSET64(pHeader->wBlockTablePosHi, pHeader->dwBlockTablePos);
break;
}
// Handle case when block table is placed before the MPQ header
// Used by BOBA protector
if(BlockTablePos64 < ByteOffset)
ha->dwFlags |= MPQ_FLAG_MALFORMED;
return dwErrCode;
}
//-----------------------------------------------------------------------------
// Support for hash table
// Hash entry verification when the file table does not exist yet
bool IsValidHashEntry(TMPQArchive * ha, TMPQHash * pHash)
{
TFileEntry * pFileEntry = ha->pFileTable + MPQ_BLOCK_INDEX(pHash);
return ((MPQ_BLOCK_INDEX(pHash) < ha->dwFileTableSize) && (pFileEntry->dwFlags & MPQ_FILE_EXISTS)) ? true : false;
}
// Hash entry verification when the file table does not exist yet
static bool IsValidHashEntry1(TMPQArchive * ha, TMPQHash * pHash, TMPQBlock * pBlockTable)
{
ULONGLONG ByteOffset;
TMPQBlock * pBlock;
// The block index is considered valid if it's less than block table size
if(MPQ_BLOCK_INDEX(pHash) < ha->pHeader->dwBlockTableSize)
{
// Calculate the block table position
pBlock = pBlockTable + MPQ_BLOCK_INDEX(pHash);
// Check whether this is an existing file
// Also we do not allow to be file size greater than 2GB
if((pBlock->dwFlags & MPQ_FILE_EXISTS) && (pBlock->dwFSize & 0x80000000) == 0)
{
// The begin of the file must be within the archive
ByteOffset = FileOffsetFromMpqOffset(ha, pBlock->dwFilePos);
return (ByteOffset < ha->FileSize);
}
}
return false;
}
// Returns a hash table entry in the following order:
// 1) A hash table entry with the preferred locale and platform
// 2) A hash table entry with the neutral|matching locale and neutral|matching platform
// 3) NULL
// Storm_2016.dll: 15020940
static TMPQHash * GetHashEntryLocale(TMPQArchive * ha, const char * szFileName, LCID lcLocale, BYTE Platform)
{
TMPQHash * pFirstHash = GetFirstHashEntry(ha, szFileName);
TMPQHash * pBestEntry = NULL;
TMPQHash * pHash = pFirstHash;
// Parse the found hashes
while(pHash != NULL)
{
// Storm_2016.dll: 150209CB
// If the hash entry matches both locale and platform, return it immediately
// Note: We only succeed this check if the locale is non-neutral, because
// some Warcraft III maps have several items with neutral locale&platform, which leads
// to wrong item being returned
if((lcLocale || Platform) && pHash->lcLocale == lcLocale && pHash->Platform == Platform)
return pHash;
// Storm_2016.dll: 150209D9
// If (locale matches or is neutral) OR (platform matches or is neutral)
// remember this as the best entry
if(pHash->lcLocale == 0 || pHash->lcLocale == lcLocale)
{
if(pHash->Platform == 0 || pHash->Platform == Platform)
pBestEntry = pHash;
}
// Get the next hash entry for that file
pHash = GetNextHashEntry(ha, pFirstHash, pHash);
}
// At the end, return neutral hash (if found), otherwise NULL
return pBestEntry;
}
// Returns a hash table entry in the following order:
// 1) A hash table entry with the preferred locale
// 2) NULL
static TMPQHash * GetHashEntryExact(TMPQArchive * ha, const char * szFileName, LCID lcLocale)
{
TMPQHash * pFirstHash = GetFirstHashEntry(ha, szFileName);
TMPQHash * pHash = pFirstHash;
// Parse the found hashes
while(pHash != NULL)
{
// If the locales match, return it
if(pHash->lcLocale == lcLocale)
return pHash;
// Get the next hash entry for that file
pHash = GetNextHashEntry(ha, pFirstHash, pHash);
}
// Not found
return NULL;
}
// Defragment the file table so it does not contain any gaps
// Note: As long as all values of all TMPQHash::dwBlockIndex
// are not HASH_ENTRY_FREE, the startup search index does not matter.
// Hash table is circular, so as long as there is no terminator,
// all entries will be found.
static TMPQHash * DefragmentHashTable(
TMPQArchive * ha,
TMPQHash * pHashTable,
TMPQBlock * pBlockTable)
{
TMPQHeader * pHeader = ha->pHeader;
TMPQHash * pHashTableEnd = pHashTable + pHeader->dwHashTableSize;
TMPQHash * pSource = pHashTable;
TMPQHash * pTarget = pHashTable;
DWORD dwFirstFreeEntry;
DWORD dwNewTableSize;
// Sanity checks
assert(pHeader->wFormatVersion == MPQ_FORMAT_VERSION_1);
assert(pHeader->HiBlockTablePos64 == 0);
// Parse the hash table and move the entries to the begin of it
for(pSource = pHashTable; pSource < pHashTableEnd; pSource++)
{
// Check whether this is a valid hash table entry
if(IsValidHashEntry1(ha, pSource, pBlockTable))
{
// Copy the hash table entry back
if(pSource > pTarget)
pTarget[0] = pSource[0];
// Move the target
pTarget++;
}
}
// Calculate how many entries in the hash table we really need
dwFirstFreeEntry = (DWORD)(pTarget - pHashTable);
dwNewTableSize = GetNearestPowerOfTwo(dwFirstFreeEntry);
// Fill the rest with entries that look like deleted
pHashTableEnd = pHashTable + dwNewTableSize;
pSource = pHashTable + dwFirstFreeEntry;
memset(pSource, 0xFF, (dwNewTableSize - dwFirstFreeEntry) * sizeof(TMPQHash));
// Mark the block indexes as deleted
for(; pSource < pHashTableEnd; pSource++)
pSource->dwBlockIndex = HASH_ENTRY_DELETED;
// Free some of the space occupied by the hash table
if(dwNewTableSize < pHeader->dwHashTableSize)
{
pHashTable = STORM_REALLOC(TMPQHash, pHashTable, dwNewTableSize);
ha->pHeader->BlockTableSize64 = dwNewTableSize * sizeof(TMPQHash);
ha->pHeader->dwHashTableSize = dwNewTableSize;
}
return pHashTable;
}
static DWORD BuildFileTableFromBlockTable(
TMPQArchive * ha,
TMPQBlock * pBlockTable)
{
TFileEntry * pFileEntry;
TMPQHeader * pHeader = ha->pHeader;
TMPQBlock * pBlock;
TMPQHash * pHashTableEnd;
TMPQHash * pHash;
LPDWORD DefragmentTable = NULL;
DWORD dwItemCount = 0;
DWORD dwFlagMask;
// Sanity checks
assert(ha->pFileTable != NULL);
assert(ha->dwFileTableSize >= ha->dwMaxFileCount);
// MPQs for Warcraft III doesn't know some flags, namely MPQ_FILE_SINGLE_UNIT and MPQ_FILE_PATCH_FILE
dwFlagMask = (ha->dwFlags & MPQ_FLAG_WAR3_MAP) ? MPQ_FILE_VALID_FLAGS_W3X : MPQ_FILE_VALID_FLAGS;
// Defragment the hash table, if needed
if(ha->dwFlags & MPQ_FLAG_HASH_TABLE_CUT)
{
ha->pHashTable = DefragmentHashTable(ha, ha->pHashTable, pBlockTable);
ha->dwMaxFileCount = pHeader->dwHashTableSize;
}
// If the hash table or block table is cut,
// we will defragment the block table
if(ha->dwFlags & (MPQ_FLAG_HASH_TABLE_CUT | MPQ_FLAG_BLOCK_TABLE_CUT))
{
// Sanity checks
assert(pHeader->wFormatVersion == MPQ_FORMAT_VERSION_1);
assert(pHeader->HiBlockTablePos64 == 0);
// Allocate the translation table
DefragmentTable = STORM_ALLOC(DWORD, pHeader->dwBlockTableSize);
if(DefragmentTable == NULL)
return ERROR_NOT_ENOUGH_MEMORY;
// Fill the translation table
memset(DefragmentTable, 0xFF, pHeader->dwBlockTableSize * sizeof(DWORD));
}
// Parse the entire hash table
pHashTableEnd = ha->pHashTable + pHeader->dwHashTableSize;
for(pHash = ha->pHashTable; pHash < pHashTableEnd; pHash++)
{
//
// We need to properly handle these cases:
// - Multiple hash entries (same file name) point to the same block entry
// - Multiple hash entries (different file name) point to the same block entry
//
// Ignore all hash table entries where:
// - Block Index >= BlockTableSize
// - Flags of the appropriate block table entry
//
if(IsValidHashEntry1(ha, pHash, pBlockTable))
{
DWORD dwOldIndex = MPQ_BLOCK_INDEX(pHash);
DWORD dwNewIndex = MPQ_BLOCK_INDEX(pHash);
// Determine the new block index
if(DefragmentTable != NULL)
{
// Need to handle case when multiple hash
// entries point to the same block entry
if(DefragmentTable[dwOldIndex] == HASH_ENTRY_FREE)
{
DefragmentTable[dwOldIndex] = dwItemCount;
dwNewIndex = dwItemCount++;
}
else
{
dwNewIndex = DefragmentTable[dwOldIndex];
}
// Fix the pointer in the hash entry
pHash->dwBlockIndex = dwNewIndex;
// Dump the relocation entry
// printf("Relocating hash entry %08X-%08X: %08X -> %08X\n", pHash->dwName1, pHash->dwName2, dwBlockIndex, dwNewIndex);
}
// Get the pointer to the file entry and the block entry
pFileEntry = ha->pFileTable + dwNewIndex;
pBlock = pBlockTable + dwOldIndex;
// ByteOffset is only valid if file size is not zero
pFileEntry->ByteOffset = pBlock->dwFilePos;
if(pFileEntry->ByteOffset == 0 && pBlock->dwFSize == 0)
pFileEntry->ByteOffset = ha->pHeader->dwHeaderSize;
// Fill the rest of the file entry
pFileEntry->dwFileSize = pBlock->dwFSize;
pFileEntry->dwCmpSize = pBlock->dwCSize;
pFileEntry->dwFlags = pBlock->dwFlags & dwFlagMask;
}
}
// Free the translation table
if(DefragmentTable != NULL)
{
// If we defragmented the block table in the process,
// free some memory by shrinking the file table
if(ha->dwFileTableSize > ha->dwMaxFileCount)
{
ha->pFileTable = STORM_REALLOC(TFileEntry, ha->pFileTable, ha->dwMaxFileCount);
ha->pHeader->BlockTableSize64 = ha->dwMaxFileCount * sizeof(TMPQBlock);
ha->pHeader->dwBlockTableSize = ha->dwMaxFileCount;
ha->dwFileTableSize = ha->dwMaxFileCount;
}
// DumpFileTable(ha->pFileTable, ha->dwFileTableSize);
// Free the translation table
STORM_FREE(DefragmentTable);
}
return ERROR_SUCCESS;
}
static TMPQHash * TranslateHashTable(
TMPQArchive * ha,
ULONGLONG * pcbTableSize)
{
TMPQHash * pHashTable;
size_t HashTableSize;
// Allocate copy of the hash table
pHashTable = STORM_ALLOC(TMPQHash, ha->pHeader->dwHashTableSize);
if(pHashTable != NULL)
{
// Copy the hash table
HashTableSize = sizeof(TMPQHash) * ha->pHeader->dwHashTableSize;
memcpy(pHashTable, ha->pHashTable, HashTableSize);
// Give the size to the caller
if(pcbTableSize != NULL)
{
*pcbTableSize = (ULONGLONG)HashTableSize;
}
}
return pHashTable;
}
// Also used in SFileGetFileInfo
TMPQBlock * TranslateBlockTable(
TMPQArchive * ha,
ULONGLONG * pcbTableSize,
bool * pbNeedHiBlockTable)
{
TFileEntry * pFileEntry = ha->pFileTable;
TMPQBlock * pBlockTable;
TMPQBlock * pBlock;
DWORD NeedHiBlockTable = 0;
DWORD dwBlockTableSize = ha->pHeader->dwBlockTableSize;
// Allocate copy of the hash table
pBlockTable = pBlock = STORM_ALLOC(TMPQBlock, dwBlockTableSize);
if(pBlockTable != NULL)
{
// Convert the block table
for(DWORD i = 0; i < dwBlockTableSize; i++)
{
NeedHiBlockTable |= (DWORD)(pFileEntry->ByteOffset >> 32);
pBlock->dwFilePos = (DWORD)pFileEntry->ByteOffset;
pBlock->dwFSize = pFileEntry->dwFileSize;
pBlock->dwCSize = pFileEntry->dwCmpSize;
pBlock->dwFlags = pFileEntry->dwFlags;
pFileEntry++;
pBlock++;
}
// Give the size to the caller
if(pcbTableSize != NULL)
*pcbTableSize = (ULONGLONG)dwBlockTableSize * sizeof(TMPQBlock);
if(pbNeedHiBlockTable != NULL)
*pbNeedHiBlockTable = NeedHiBlockTable ? true : false;
}
return pBlockTable;
}
static USHORT * TranslateHiBlockTable(
TMPQArchive * ha,
ULONGLONG * pcbTableSize)
{
TFileEntry * pFileEntry = ha->pFileTable;
USHORT * pHiBlockTable;
USHORT * pHiBlock;
DWORD dwBlockTableSize = ha->pHeader->dwBlockTableSize;
// Allocate copy of the hash table
pHiBlockTable = pHiBlock = STORM_ALLOC(USHORT, dwBlockTableSize);
if(pHiBlockTable != NULL)
{
// Copy the block table
for(DWORD i = 0; i < dwBlockTableSize; i++)
pHiBlock[i] = (USHORT)(pFileEntry[i].ByteOffset >> 0x20);
// Give the size to the caller
if(pcbTableSize != NULL)
*pcbTableSize = (ULONGLONG)dwBlockTableSize * sizeof(USHORT);
}
return pHiBlockTable;
}
//-----------------------------------------------------------------------------
// General EXT table functions
TMPQExtHeader * LoadExtTable(
TMPQArchive * ha,
ULONGLONG ByteOffset,
size_t Size,
DWORD dwSignature,
DWORD dwKey)
{
TMPQExtHeader * pCompressed = NULL; // Compressed table
TMPQExtHeader * pExtTable = NULL; // Uncompressed table
// Do nothing if the size is zero
if(ByteOffset != 0 && Size != 0)
{
// Allocate size for the compressed table
pExtTable = (TMPQExtHeader *)STORM_ALLOC(BYTE, Size);
if(pExtTable != NULL)
{
// Load the table from the MPQ
ByteOffset += ha->MpqPos;
if(!FileStream_Read(ha->pStream, &ByteOffset, pExtTable, (DWORD)Size))
{
STORM_FREE(pExtTable);
return NULL;
}
// Swap the ext table header
BSWAP_ARRAY32_UNSIGNED(pExtTable, sizeof(TMPQExtHeader));
if(pExtTable->dwSignature != dwSignature)
{
STORM_FREE(pExtTable);
return NULL;
}
// Decrypt the block
BSWAP_ARRAY32_UNSIGNED(pExtTable + 1, pExtTable->dwDataSize);
DecryptMpqBlock(pExtTable + 1, (DWORD)(Size - sizeof(TMPQExtHeader)), dwKey);
BSWAP_ARRAY32_UNSIGNED(pExtTable + 1, pExtTable->dwDataSize);
// If the table is compressed, decompress it
if((pExtTable->dwDataSize + sizeof(TMPQExtHeader)) > Size)
{
pCompressed = pExtTable;
pExtTable = (TMPQExtHeader *)STORM_ALLOC(BYTE, sizeof(TMPQExtHeader) + pCompressed->dwDataSize);
if(pExtTable != NULL)
{
int cbOutBuffer = (int)pCompressed->dwDataSize;
int cbInBuffer = (int)Size;
// Decompress the extended table
pExtTable->dwSignature = pCompressed->dwSignature;
pExtTable->dwVersion = pCompressed->dwVersion;
pExtTable->dwDataSize = pCompressed->dwDataSize;
if(!SCompDecompress2(pExtTable + 1, &cbOutBuffer, pCompressed + 1, cbInBuffer))
{
STORM_FREE(pExtTable);
pExtTable = NULL;
}
}
// Free the compressed block
STORM_FREE(pCompressed);
}
}
}
// Return the decompressed table to the caller
return pExtTable;
}
static DWORD SaveMpqTable(
TMPQArchive * ha,
void * pMpqTable,
ULONGLONG ByteOffset,
size_t Size,
unsigned char * md5,
DWORD dwKey,
bool bCompress)
{
ULONGLONG FileOffset;
void * pCompressed = NULL;
DWORD dwErrCode = ERROR_SUCCESS;
// Do we have to compress the table?
if(bCompress)
{
int cbOutBuffer = (int)Size;
int cbInBuffer = (int)Size;
// Allocate extra space for compressed table
pCompressed = STORM_ALLOC(BYTE, Size);
if(pCompressed == NULL)
return ERROR_NOT_ENOUGH_MEMORY;
// Compress the table
SCompCompress(pCompressed, &cbOutBuffer, pMpqTable, cbInBuffer, MPQ_COMPRESSION_ZLIB, 0, 0);
// If the compression failed, revert it. Otherwise, swap the tables
if(cbOutBuffer >= cbInBuffer)
{
STORM_FREE(pCompressed);
pCompressed = NULL;
}
else
{
pMpqTable = pCompressed;
}
}
// Encrypt the table
if(dwKey != 0)
{
BSWAP_ARRAY32_UNSIGNED(pMpqTable, Size);
EncryptMpqBlock(pMpqTable, (DWORD)Size, dwKey);
BSWAP_ARRAY32_UNSIGNED(pMpqTable, Size);
}
// Calculate the MD5
if(md5 != NULL)
{
CalculateDataBlockHash(pMpqTable, (DWORD)Size, md5);
}
// Save the table to the MPQ
BSWAP_ARRAY32_UNSIGNED(pMpqTable, Size);
FileOffset = ha->MpqPos + ByteOffset;
if(!FileStream_Write(ha->pStream, &FileOffset, pMpqTable, (DWORD)Size))
dwErrCode = GetLastError();
// Free the compressed table, if any
if(pCompressed != NULL)
STORM_FREE(pCompressed);
return dwErrCode;
}
static DWORD SaveExtTable(
TMPQArchive * ha,
TMPQExtHeader * pExtTable,
ULONGLONG ByteOffset,
DWORD dwTableSize,
unsigned char * md5,
DWORD dwKey,
bool bCompress,
LPDWORD pcbTotalSize)
{
ULONGLONG FileOffset;
TMPQExtHeader * pCompressed = NULL;
DWORD cbTotalSize = 0;
DWORD dwErrCode = ERROR_SUCCESS;
// Do we have to compress the table?
if(bCompress)
{
int cbOutBuffer = (int)dwTableSize;
int cbInBuffer = (int)dwTableSize;
// Allocate extra space for compressed table
pCompressed = (TMPQExtHeader *)STORM_ALLOC(BYTE, dwTableSize);
if(pCompressed == NULL)
return ERROR_NOT_ENOUGH_MEMORY;
// Compress the table
pCompressed->dwSignature = pExtTable->dwSignature;
pCompressed->dwVersion = pExtTable->dwVersion;
pCompressed->dwDataSize = pExtTable->dwDataSize;
SCompCompress((pCompressed + 1), &cbOutBuffer, (pExtTable + 1), cbInBuffer, MPQ_COMPRESSION_ZLIB, 0, 0);
// If the compression failed, revert it. Otherwise, swap the tables
if(cbOutBuffer >= cbInBuffer)
{
STORM_FREE(pCompressed);
pCompressed = NULL;
}
else
{
pExtTable = pCompressed;
}
}
// Encrypt the table
if(dwKey != 0)
{
BSWAP_ARRAY32_UNSIGNED(pExtTable + 1, pExtTable->dwDataSize);
EncryptMpqBlock(pExtTable + 1, (DWORD)(dwTableSize - sizeof(TMPQExtHeader)), dwKey);
BSWAP_ARRAY32_UNSIGNED(pExtTable + 1, pExtTable->dwDataSize);
}
// Calculate the MD5 of the table after
if(md5 != NULL)
{
CalculateDataBlockHash(pExtTable, dwTableSize, md5);
}
// Save the table to the MPQ
FileOffset = ha->MpqPos + ByteOffset;
if(FileStream_Write(ha->pStream, &FileOffset, pExtTable, dwTableSize))
cbTotalSize += dwTableSize;
else
dwErrCode = GetLastError();
// We have to write raw data MD5
if(dwErrCode == ERROR_SUCCESS && ha->pHeader->dwRawChunkSize != 0)
{
dwErrCode = WriteMemDataMD5(ha->pStream,
FileOffset,
pExtTable,
dwTableSize,
ha->pHeader->dwRawChunkSize,
&cbTotalSize);
}
// Give the total written size, if needed
if(pcbTotalSize != NULL)
*pcbTotalSize = cbTotalSize;
// Free the compressed table, if any
if(pCompressed != NULL)
STORM_FREE(pCompressed);
return dwErrCode;
}
//-----------------------------------------------------------------------------
// Support for HET table
static void CreateHetHeader(
TMPQHetTable * pHetTable,
TMPQHetHeader * pHetHeader)
{
// Fill the common header
pHetHeader->ExtHdr.dwSignature = HET_TABLE_SIGNATURE;
pHetHeader->ExtHdr.dwVersion = 1;
pHetHeader->ExtHdr.dwDataSize = 0;
// Fill the HET header
pHetHeader->dwEntryCount = pHetTable->dwEntryCount;
pHetHeader->dwTotalCount = pHetTable->dwTotalCount;
pHetHeader->dwNameHashBitSize = pHetTable->dwNameHashBitSize;
pHetHeader->dwIndexSizeTotal = pHetTable->dwIndexSizeTotal;
pHetHeader->dwIndexSizeExtra = pHetTable->dwIndexSizeExtra;
pHetHeader->dwIndexSize = pHetTable->dwIndexSize;
pHetHeader->dwIndexTableSize = ((pHetHeader->dwIndexSizeTotal * pHetTable->dwTotalCount) + 7) / 8;
// Calculate the total size needed for holding HET table
pHetHeader->ExtHdr.dwDataSize =
pHetHeader->dwTableSize = sizeof(TMPQHetHeader) - sizeof(TMPQExtHeader) +
pHetHeader->dwTotalCount +
pHetHeader->dwIndexTableSize;
}
TMPQHetTable * CreateHetTable(DWORD dwEntryCount, DWORD dwTotalCount, DWORD dwNameHashBitSize, LPBYTE pbSrcData)
{
TMPQHetTable * pHetTable;
pHetTable = STORM_ALLOC(TMPQHetTable, 1);
if(pHetTable != NULL)
{
// Zero the HET table
memset(pHetTable, 0, sizeof(TMPQHetTable));
// Hash sizes less than 0x40 bits are not tested
assert(dwNameHashBitSize == 0x40);
// Calculate masks
pHetTable->AndMask64 = ((dwNameHashBitSize != 0x40) ? ((ULONGLONG)1 << dwNameHashBitSize) : 0) - 1;
pHetTable->OrMask64 = (ULONGLONG)1 << (dwNameHashBitSize - 1);
// If the total count is not entered, use default
if(dwTotalCount == 0)
dwTotalCount = (dwEntryCount * 4) / 3;
// Store the HET table parameters
pHetTable->dwEntryCount = dwEntryCount;
pHetTable->dwTotalCount = dwTotalCount;
pHetTable->dwNameHashBitSize = dwNameHashBitSize;
pHetTable->dwIndexSizeTotal = GetNecessaryBitCount(dwEntryCount);
pHetTable->dwIndexSizeExtra = 0;
pHetTable->dwIndexSize = pHetTable->dwIndexSizeTotal;
// Allocate array of hashes
pHetTable->pNameHashes = STORM_ALLOC(BYTE, dwTotalCount);
if(pHetTable->pNameHashes != NULL)
{
// Make sure the data are initialized
memset(pHetTable->pNameHashes, 0, dwTotalCount);
// Allocate the bit array for file indexes
pHetTable->pBetIndexes = TMPQBits::Create(dwTotalCount * pHetTable->dwIndexSizeTotal, 0xFF);
if(pHetTable->pBetIndexes != NULL)
{
// Initialize the HET table from the source data (if given)
if(pbSrcData != NULL)
{
// Copy the name hashes
memcpy(pHetTable->pNameHashes, pbSrcData, dwTotalCount);
// Copy the file indexes
memcpy(pHetTable->pBetIndexes->Elements, pbSrcData + dwTotalCount, pHetTable->pBetIndexes->NumberOfBytes);
}
// Return the result HET table
return pHetTable;
}
// Free the name hashes
STORM_FREE(pHetTable->pNameHashes);
}
STORM_FREE(pHetTable);
}
// Failed
return NULL;
}
static DWORD InsertHetEntry(TMPQHetTable * pHetTable, ULONGLONG FileNameHash, DWORD dwFileIndex)
{
DWORD StartIndex;
DWORD Index;
BYTE NameHash1;
// Get the start index and the high 8 bits of the name hash
StartIndex = Index = (DWORD)(FileNameHash % pHetTable->dwTotalCount);
NameHash1 = (BYTE)(FileNameHash >> (pHetTable->dwNameHashBitSize - 8));
// Find a place where to put it
for(;;)
{
// Did we find a free HET entry?
if(pHetTable->pNameHashes[Index] == HET_ENTRY_FREE)
{
// Set the entry in the name hash table
pHetTable->pNameHashes[Index] = NameHash1;
// Set the entry in the file index table
pHetTable->pBetIndexes->SetBits(pHetTable->dwIndexSizeTotal * Index,
pHetTable->dwIndexSize,
&dwFileIndex,
4);
return ERROR_SUCCESS;
}
// Move to the next entry in the HET table
// If we came to the start index again, we are done
Index = (Index + 1) % pHetTable->dwTotalCount;
if(Index == StartIndex)
break;
}
// No space in the HET table. Should never happen,
// because the HET table is created according to the number of files
assert(false);
return ERROR_DISK_FULL;
}
static TMPQHetTable * TranslateHetTable(TMPQHetHeader * pHetHeader)
{
TMPQHetTable * pHetTable = NULL;
LPBYTE pbSrcData = (LPBYTE)(pHetHeader + 1);
// Sanity check
assert(pHetHeader->ExtHdr.dwSignature == HET_TABLE_SIGNATURE);
assert(pHetHeader->ExtHdr.dwVersion == 1);
// Verify size of the HET table
if(pHetHeader->ExtHdr.dwDataSize >= (sizeof(TMPQHetHeader) - sizeof(TMPQExtHeader)))
{
// Verify the size of the table in the header
if(pHetHeader->ExtHdr.dwDataSize >= pHetHeader->dwTableSize)
{
// The size of the HET table must be sum of header, hash and index table size
assert((sizeof(TMPQHetHeader) - sizeof(TMPQExtHeader) + pHetHeader->dwTotalCount + pHetHeader->dwIndexTableSize) == pHetHeader->dwTableSize);
// So far, all MPQs with HET Table have had total number of entries equal to 4/3 of file count
// Exception: "2010 - Starcraft II\!maps\Tya's Zerg Defense (unprotected).SC2Map"
// assert(((pHetHeader->dwEntryCount * 4) / 3) == pHetHeader->dwTotalCount);
// The size of one index is predictable as well
assert(GetNecessaryBitCount(pHetHeader->dwEntryCount) == pHetHeader->dwIndexSizeTotal);
// The size of index table (in entries) is expected
// to be the same like the hash table size (in bytes)
assert(((pHetHeader->dwTotalCount * pHetHeader->dwIndexSizeTotal) + 7) / 8 == pHetHeader->dwIndexTableSize);
// Create translated table
pHetTable = CreateHetTable(pHetHeader->dwEntryCount, pHetHeader->dwTotalCount, pHetHeader->dwNameHashBitSize, pbSrcData);
if(pHetTable != NULL)
{
// Now the sizes in the hash table should be already set
assert(pHetTable->dwEntryCount == pHetHeader->dwEntryCount);
assert(pHetTable->dwTotalCount == pHetHeader->dwTotalCount);
assert(pHetTable->dwIndexSizeTotal == pHetHeader->dwIndexSizeTotal);
// Copy the missing variables
pHetTable->dwIndexSizeExtra = pHetHeader->dwIndexSizeExtra;
pHetTable->dwIndexSize = pHetHeader->dwIndexSize;
}
}
}
return pHetTable;
}
static TMPQExtHeader * TranslateHetTable(TMPQHetTable * pHetTable, ULONGLONG * pcbHetTable)
{
TMPQHetHeader * pHetHeader = NULL;
TMPQHetHeader HetHeader;
LPBYTE pbLinearTable = NULL;
LPBYTE pbTrgData;
// Prepare header of the HET table
CreateHetHeader(pHetTable, &HetHeader);
// Allocate space for the linear table
pbLinearTable = STORM_ALLOC(BYTE, sizeof(TMPQExtHeader) + HetHeader.dwTableSize);
if(pbLinearTable != NULL)
{
// Copy the table header
pHetHeader = (TMPQHetHeader *)pbLinearTable;
memcpy(pHetHeader, &HetHeader, sizeof(TMPQHetHeader));
pbTrgData = (LPBYTE)(pHetHeader + 1);
// Copy the array of name hashes
memcpy(pbTrgData, pHetTable->pNameHashes, pHetTable->dwTotalCount);
pbTrgData += pHetTable->dwTotalCount;
// Copy the bit array of BET indexes
memcpy(pbTrgData, pHetTable->pBetIndexes->Elements, HetHeader.dwIndexTableSize);
// Calculate the total size of the table, including the TMPQExtHeader
if(pcbHetTable != NULL)
{
*pcbHetTable = (ULONGLONG)(sizeof(TMPQExtHeader) + HetHeader.dwTableSize);
}
}
// Keep Coverity happy
assert((TMPQExtHeader *)&pHetHeader->ExtHdr == (TMPQExtHeader *)pbLinearTable);
return (TMPQExtHeader *)pbLinearTable;
}
static DWORD GetFileIndex_Het(TMPQArchive * ha, const char * szFileName)
{
TMPQHetTable * pHetTable = ha->pHetTable;
ULONGLONG FileNameHash;
DWORD StartIndex;
DWORD Index;
BYTE NameHash1; // Upper 8 bits of the masked file name hash
// If there are no entries in the HET table, do nothing
if(pHetTable->dwEntryCount == 0)
return HASH_ENTRY_FREE;
// Do nothing if the MPQ has no HET table
assert(ha->pHetTable != NULL);
// Calculate 64-bit hash of the file name
FileNameHash = (HashStringJenkins(szFileName) & pHetTable->AndMask64) | pHetTable->OrMask64;
// Split the file name hash into two parts:
// NameHash1: The highest 8 bits of the name hash
// NameHash2: File name hash limited to hash size
// Note: Our file table contains full name hash, no need to cut the high 8 bits before comparison
NameHash1 = (BYTE)(FileNameHash >> (pHetTable->dwNameHashBitSize - 8));
// Calculate the starting index to the hash table
StartIndex = Index = (DWORD)(FileNameHash % pHetTable->dwTotalCount);
// Go through HET table until we find a terminator
while(pHetTable->pNameHashes[Index] != HET_ENTRY_FREE)
{
// Did we find a match ?
if(pHetTable->pNameHashes[Index] == NameHash1)
{
DWORD dwFileIndex = 0;
// Get the file index
pHetTable->pBetIndexes->GetBits(pHetTable->dwIndexSizeTotal * Index,
pHetTable->dwIndexSize,
&dwFileIndex,
sizeof(DWORD));
// Verify the FileNameHash against the entry in the table of name hashes
if(dwFileIndex <= ha->dwFileTableSize && ha->pFileTable[dwFileIndex].FileNameHash == FileNameHash)
{
return dwFileIndex;
}
}
// Move to the next entry in the HET table
// If we came to the start index again, we are done
Index = (Index + 1) % pHetTable->dwTotalCount;
if(Index == StartIndex)
break;
}
// File not found
return HASH_ENTRY_FREE;
}
void FreeHetTable(TMPQHetTable * pHetTable)
{
if(pHetTable != NULL)
{
if(pHetTable->pNameHashes != NULL)
STORM_FREE(pHetTable->pNameHashes);
if(pHetTable->pBetIndexes != NULL)
STORM_FREE(pHetTable->pBetIndexes);
STORM_FREE(pHetTable);
}
}
//-----------------------------------------------------------------------------
// Support for BET table
static void CreateBetHeader(
TMPQArchive * ha,
TMPQBetHeader * pBetHeader)
{
TFileEntry * pFileTableEnd = ha->pFileTable + ha->dwFileTableSize;
TFileEntry * pFileEntry;
ULONGLONG MaxByteOffset = 0;
DWORD FlagArray[MAX_FLAG_INDEX];
DWORD dwMaxFlagIndex = 0;
DWORD dwMaxFileSize = 0;
DWORD dwMaxCmpSize = 0;
DWORD dwFlagIndex;
// Initialize array of flag combinations
InitFileFlagArray(FlagArray);
// Fill the common header
pBetHeader->ExtHdr.dwSignature = BET_TABLE_SIGNATURE;
pBetHeader->ExtHdr.dwVersion = 1;
pBetHeader->ExtHdr.dwDataSize = 0;
// Get the maximum values for the BET table
pFileTableEnd = ha->pFileTable + ha->pHeader->dwBlockTableSize;
for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++)
{
//
// Note: Deleted files must be counted as well
//
// Highest file position in the MPQ
if(pFileEntry->ByteOffset > MaxByteOffset)
MaxByteOffset = pFileEntry->ByteOffset;
// Biggest file size
if(pFileEntry->dwFileSize > dwMaxFileSize)
dwMaxFileSize = pFileEntry->dwFileSize;
// Biggest compressed size
if(pFileEntry->dwCmpSize > dwMaxCmpSize)
dwMaxCmpSize = pFileEntry->dwCmpSize;
// Check if this flag was there before
dwFlagIndex = GetFileFlagIndex(FlagArray, pFileEntry->dwFlags);
if(dwFlagIndex > dwMaxFlagIndex)
dwMaxFlagIndex = dwFlagIndex;
}
// Now save bit count for every piece of file information
pBetHeader->dwBitIndex_FilePos = 0;
pBetHeader->dwBitCount_FilePos = GetNecessaryBitCount(MaxByteOffset);
pBetHeader->dwBitIndex_FileSize = pBetHeader->dwBitIndex_FilePos + pBetHeader->dwBitCount_FilePos;
pBetHeader->dwBitCount_FileSize = GetNecessaryBitCount(dwMaxFileSize);
pBetHeader->dwBitIndex_CmpSize = pBetHeader->dwBitIndex_FileSize + pBetHeader->dwBitCount_FileSize;
pBetHeader->dwBitCount_CmpSize = GetNecessaryBitCount(dwMaxCmpSize);
pBetHeader->dwBitIndex_FlagIndex = pBetHeader->dwBitIndex_CmpSize + pBetHeader->dwBitCount_CmpSize;
pBetHeader->dwBitCount_FlagIndex = GetNecessaryBitCount(dwMaxFlagIndex + 1);
pBetHeader->dwBitIndex_Unknown = pBetHeader->dwBitIndex_FlagIndex + pBetHeader->dwBitCount_FlagIndex;
pBetHeader->dwBitCount_Unknown = 0;
// Calculate the total size of one entry
pBetHeader->dwTableEntrySize = pBetHeader->dwBitCount_FilePos +
pBetHeader->dwBitCount_FileSize +
pBetHeader->dwBitCount_CmpSize +
pBetHeader->dwBitCount_FlagIndex +
pBetHeader->dwBitCount_Unknown;
// Save the file count and flag count
pBetHeader->dwEntryCount = ha->pHeader->dwBlockTableSize;
pBetHeader->dwFlagCount = dwMaxFlagIndex + 1;
pBetHeader->dwUnknown08 = 0x10;
// Save the total size of the BET hash
pBetHeader->dwBitTotal_NameHash2 = ha->pHetTable->dwNameHashBitSize - 0x08;
pBetHeader->dwBitExtra_NameHash2 = 0;
pBetHeader->dwBitCount_NameHash2 = pBetHeader->dwBitTotal_NameHash2;
pBetHeader->dwNameHashArraySize = ((pBetHeader->dwBitTotal_NameHash2 * pBetHeader->dwEntryCount) + 7) / 8;
// Save the total table size
pBetHeader->ExtHdr.dwDataSize =
pBetHeader->dwTableSize = sizeof(TMPQBetHeader) - sizeof(TMPQExtHeader) +
pBetHeader->dwFlagCount * sizeof(DWORD) +
((pBetHeader->dwTableEntrySize * pBetHeader->dwEntryCount) + 7) / 8 +
pBetHeader->dwNameHashArraySize;
}
TMPQBetTable * CreateBetTable(DWORD dwEntryCount)
{
TMPQBetTable * pBetTable;
// Allocate BET table
pBetTable = STORM_ALLOC(TMPQBetTable, 1);
if(pBetTable != NULL)
{
memset(pBetTable, 0, sizeof(TMPQBetTable));
pBetTable->dwEntryCount = dwEntryCount;
}
return pBetTable;
}
static TMPQBetTable * TranslateBetTable(
TMPQArchive * ha,
TMPQBetHeader * pBetHeader)
{
TMPQBetTable * pBetTable = NULL;
LPBYTE pbSrcData = (LPBYTE)(pBetHeader + 1);
DWORD LengthInBytes = 0;
// Sanity check
assert(pBetHeader->ExtHdr.dwSignature == BET_TABLE_SIGNATURE);
assert(pBetHeader->ExtHdr.dwVersion == 1);
assert(ha->pHetTable != NULL);
ha = ha;
// Verify size of the HET table
if(pBetHeader->ExtHdr.dwDataSize >= (sizeof(TMPQBetHeader) - sizeof(TMPQExtHeader)))
{
// Verify the size of the table in the header
if(pBetHeader->ExtHdr.dwDataSize >= pBetHeader->dwTableSize)
{
// The number of entries in the BET table must be the same like number of entries in the block table
// Note: Ignored if there is no block table
//assert(pBetHeader->dwEntryCount == ha->pHeader->dwBlockTableSize);
assert(pBetHeader->dwEntryCount <= ha->dwMaxFileCount);
// The number of entries in the BET table must be the same like number of entries in the HET table
// Note that if it's not, it is not a problem
//assert(pBetHeader->dwEntryCount == ha->pHetTable->dwEntryCount);
// Create translated table
pBetTable = CreateBetTable(pBetHeader->dwEntryCount);
if(pBetTable != NULL)
{
// Copy the variables from the header to the BetTable
pBetTable->dwTableEntrySize = pBetHeader->dwTableEntrySize;
pBetTable->dwBitIndex_FilePos = pBetHeader->dwBitIndex_FilePos;
pBetTable->dwBitIndex_FileSize = pBetHeader->dwBitIndex_FileSize;
pBetTable->dwBitIndex_CmpSize = pBetHeader->dwBitIndex_CmpSize;
pBetTable->dwBitIndex_FlagIndex = pBetHeader->dwBitIndex_FlagIndex;
pBetTable->dwBitIndex_Unknown = pBetHeader->dwBitIndex_Unknown;
pBetTable->dwBitCount_FilePos = pBetHeader->dwBitCount_FilePos;
pBetTable->dwBitCount_FileSize = pBetHeader->dwBitCount_FileSize;
pBetTable->dwBitCount_CmpSize = pBetHeader->dwBitCount_CmpSize;
pBetTable->dwBitCount_FlagIndex = pBetHeader->dwBitCount_FlagIndex;
pBetTable->dwBitCount_Unknown = pBetHeader->dwBitCount_Unknown;
// Since we don't know what the "unknown" is, we'll assert when it's zero
assert(pBetTable->dwBitCount_Unknown == 0);
// Allocate array for flags
if(pBetHeader->dwFlagCount != 0)
{
// Allocate array for file flags and load it
pBetTable->pFileFlags = STORM_ALLOC(DWORD, pBetHeader->dwFlagCount);
if(pBetTable->pFileFlags != NULL)
{
LengthInBytes = pBetHeader->dwFlagCount * sizeof(DWORD);
memcpy(pBetTable->pFileFlags, pbSrcData, LengthInBytes);
BSWAP_ARRAY32_UNSIGNED(pBetTable->pFileFlags, LengthInBytes);
pbSrcData += LengthInBytes;
}
// Save the number of flags
pBetTable->dwFlagCount = pBetHeader->dwFlagCount;
}
// Load the bit-based file table
pBetTable->pFileTable = TMPQBits::Create(pBetTable->dwTableEntrySize * pBetHeader->dwEntryCount, 0);
if(pBetTable->pFileTable != NULL)
{
LengthInBytes = (pBetTable->pFileTable->NumberOfBits + 7) / 8;
memcpy(pBetTable->pFileTable->Elements, pbSrcData, LengthInBytes);
pbSrcData += LengthInBytes;
}
// Fill the sizes of BET hash
pBetTable->dwBitTotal_NameHash2 = pBetHeader->dwBitTotal_NameHash2;
pBetTable->dwBitExtra_NameHash2 = pBetHeader->dwBitExtra_NameHash2;
pBetTable->dwBitCount_NameHash2 = pBetHeader->dwBitCount_NameHash2;
// Create and load the array of BET hashes
pBetTable->pNameHashes = TMPQBits::Create(pBetTable->dwBitTotal_NameHash2 * pBetHeader->dwEntryCount, 0);
if(pBetTable->pNameHashes != NULL)
{
LengthInBytes = (pBetTable->pNameHashes->NumberOfBits + 7) / 8;
memcpy(pBetTable->pNameHashes->Elements, pbSrcData, LengthInBytes);
// pbSrcData += LengthInBytes;
}
// Dump both tables
// DumpHetAndBetTable(ha->pHetTable, pBetTable);
}
}
}
return pBetTable;
}
TMPQExtHeader * TranslateBetTable(
TMPQArchive * ha,
ULONGLONG * pcbBetTable)
{
TMPQBetHeader * pBetHeader = NULL;
TMPQBetHeader BetHeader;
TFileEntry * pFileTableEnd = ha->pFileTable + ha->dwFileTableSize;
TFileEntry * pFileEntry;
TMPQBits * pBitArray = NULL;
LPBYTE pbLinearTable = NULL;
LPBYTE pbTrgData;
DWORD LengthInBytes;
DWORD FlagArray[MAX_FLAG_INDEX];
// Calculate the bit sizes of various entries
InitFileFlagArray(FlagArray);
CreateBetHeader(ha, &BetHeader);
// Allocate space
pbLinearTable = STORM_ALLOC(BYTE, sizeof(TMPQExtHeader) + BetHeader.dwTableSize);
if(pbLinearTable != NULL)
{
// Copy the BET header to the linear buffer
pBetHeader = (TMPQBetHeader *)pbLinearTable;
memcpy(pBetHeader, &BetHeader, sizeof(TMPQBetHeader));
pbTrgData = (LPBYTE)(pBetHeader + 1);
// Save the bit-based block table
pBitArray = TMPQBits::Create(BetHeader.dwEntryCount * BetHeader.dwTableEntrySize, 0);
if(pBitArray != NULL)
{
DWORD dwFlagIndex = 0;
DWORD nBitOffset = 0;
// Construct the bit-based file table
pFileTableEnd = ha->pFileTable + BetHeader.dwEntryCount;
for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++)
{
//
// Note: Missing files must be included as well
//
// Save the byte offset
pBitArray->SetBits(nBitOffset + BetHeader.dwBitIndex_FilePos,
BetHeader.dwBitCount_FilePos,
&pFileEntry->ByteOffset,
8);
pBitArray->SetBits(nBitOffset + BetHeader.dwBitIndex_FileSize,
BetHeader.dwBitCount_FileSize,
&pFileEntry->dwFileSize,
4);
pBitArray->SetBits(nBitOffset + BetHeader.dwBitIndex_CmpSize,
BetHeader.dwBitCount_CmpSize,
&pFileEntry->dwCmpSize,
4);
// Save the flag index
dwFlagIndex = GetFileFlagIndex(FlagArray, pFileEntry->dwFlags);
pBitArray->SetBits(nBitOffset + BetHeader.dwBitIndex_FlagIndex,
BetHeader.dwBitCount_FlagIndex,
&dwFlagIndex,
4);
// Move the bit offset
nBitOffset += BetHeader.dwTableEntrySize;
}
// Write the array of flags
LengthInBytes = BetHeader.dwFlagCount * sizeof(DWORD);
memcpy(pbTrgData, FlagArray, LengthInBytes);
BSWAP_ARRAY32_UNSIGNED(pbTrgData, LengthInBytes);
pbTrgData += LengthInBytes;
// Write the bit-based block table
LengthInBytes = (pBitArray->NumberOfBits + 7) / 8;
memcpy(pbTrgData, pBitArray->Elements, LengthInBytes);
pbTrgData += LengthInBytes;
// Free the bit array
STORM_FREE(pBitArray);
}
// Create bit array for name hashes
pBitArray = TMPQBits::Create(BetHeader.dwBitTotal_NameHash2 * BetHeader.dwEntryCount, 0);
if(pBitArray != NULL)
{
DWORD dwFileIndex = 0;
for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++)
{
// Insert the name hash to the bit array
pBitArray->SetBits(BetHeader.dwBitTotal_NameHash2 * dwFileIndex,
BetHeader.dwBitCount_NameHash2,
&pFileEntry->FileNameHash,
8);
assert(dwFileIndex < BetHeader.dwEntryCount);
dwFileIndex++;
}
// Write the array of BET hashes
LengthInBytes = (pBitArray->NumberOfBits + 7) / 8;
memcpy(pbTrgData, pBitArray->Elements, LengthInBytes);
// pbTrgData += LengthInBytes;
// Free the bit array
STORM_FREE(pBitArray);
}
// Write the size of the BET table in the MPQ
if(pcbBetTable != NULL)
{
*pcbBetTable = (ULONGLONG)(sizeof(TMPQExtHeader) + BetHeader.dwTableSize);
}
}
// Keep Coverity happy
assert((TMPQExtHeader *)&pBetHeader->ExtHdr == (TMPQExtHeader *)pbLinearTable);
return (TMPQExtHeader *)pbLinearTable;
}
void FreeBetTable(TMPQBetTable * pBetTable)
{
if(pBetTable != NULL)
{
if(pBetTable->pFileTable != NULL)
STORM_FREE(pBetTable->pFileTable);
if(pBetTable->pFileFlags != NULL)
STORM_FREE(pBetTable->pFileFlags);
if(pBetTable->pNameHashes != NULL)
STORM_FREE(pBetTable->pNameHashes);
STORM_FREE(pBetTable);
}
}
//-----------------------------------------------------------------------------
// Support for file table
TFileEntry * GetFileEntryLocale2(TMPQArchive * ha, const char * szFileName, LCID lcLocale, LPDWORD PtrHashIndex)
{
TMPQHash * pHash;
DWORD dwFileIndex;
// First, we have to search the classic hash table
// This is because on renaming, deleting, or changing locale,
// we will need the pointer to hash table entry
if(ha->pHashTable != NULL)
{
pHash = GetHashEntryLocale(ha, szFileName, lcLocale, 0);
if(pHash != NULL && MPQ_BLOCK_INDEX(pHash) < ha->dwFileTableSize)
{
if(PtrHashIndex != NULL)
PtrHashIndex[0] = (DWORD)(pHash - ha->pHashTable);
return ha->pFileTable + MPQ_BLOCK_INDEX(pHash);
}
}
// If we have HET table in the MPQ, try to find the file in HET table
if(ha->pHetTable != NULL)
{
dwFileIndex = GetFileIndex_Het(ha, szFileName);
if(dwFileIndex != HASH_ENTRY_FREE)
return ha->pFileTable + dwFileIndex;
}
// Not found
return NULL;
}
TFileEntry * GetFileEntryLocale(TMPQArchive * ha, const char * szFileName, LCID lcLocale)
{
return GetFileEntryLocale2(ha, szFileName, lcLocale, NULL);
}
TFileEntry * GetFileEntryExact(TMPQArchive * ha, const char * szFileName, LCID lcLocale, LPDWORD PtrHashIndex)
{
TMPQHash * pHash;
DWORD dwFileIndex;
// If the hash table is present, find the entry from hash table
if(ha->pHashTable != NULL)
{
pHash = GetHashEntryExact(ha, szFileName, lcLocale);
if(pHash != NULL && MPQ_BLOCK_INDEX(pHash) < ha->dwFileTableSize)
{
if(PtrHashIndex != NULL)
PtrHashIndex[0] = (DWORD)(pHash - ha->pHashTable);
return ha->pFileTable + MPQ_BLOCK_INDEX(pHash);
}
}
// If we have HET table in the MPQ, try to find the file in HET table
if(ha->pHetTable != NULL)
{
dwFileIndex = GetFileIndex_Het(ha, szFileName);
if(dwFileIndex != HASH_ENTRY_FREE)
{
if(PtrHashIndex != NULL)
PtrHashIndex[0] = HASH_ENTRY_FREE;
return ha->pFileTable + dwFileIndex;
}
}
// Not found
return NULL;
}
void AllocateFileName(TMPQArchive * ha, TFileEntry * pFileEntry, const char * szFileName)
{
// Sanity check
assert(pFileEntry != NULL);
// If the file name is pseudo file name, free it at this point
if(IsPseudoFileName(pFileEntry->szFileName, NULL))
{
if(pFileEntry->szFileName != NULL)
STORM_FREE(pFileEntry->szFileName);
pFileEntry->szFileName = NULL;
}
// Only allocate new file name if it's not there yet
if(pFileEntry->szFileName == NULL)
{
pFileEntry->szFileName = STORM_ALLOC(char, strlen(szFileName) + 1);
if(pFileEntry->szFileName != NULL)
strcpy(pFileEntry->szFileName, szFileName);
}
// We also need to create the file name hash
if(ha->pHetTable != NULL)
{
ULONGLONG AndMask64 = ha->pHetTable->AndMask64;
ULONGLONG OrMask64 = ha->pHetTable->OrMask64;
pFileEntry->FileNameHash = (HashStringJenkins(szFileName) & AndMask64) | OrMask64;
}
}
TFileEntry * AllocateFileEntry(TMPQArchive * ha, const char * szFileName, LCID lcLocale, LPDWORD PtrHashIndex)
{
TFileEntry * pFileTableEnd = ha->pFileTable + ha->dwFileTableSize;
TFileEntry * pFreeEntry = NULL;
TFileEntry * pFileEntry;
TMPQHash * pHash = NULL;
DWORD dwReservedFiles = ha->dwReservedFiles;
DWORD dwFreeCount = 0;
// Sanity check: File table size must be greater or equal to max file count
assert(ha->dwFileTableSize >= ha->dwMaxFileCount);
// If we are saving MPQ tables, we don't tale number of reserved files into account
dwReservedFiles = (ha->dwFlags & MPQ_FLAG_SAVING_TABLES) ? 0 : ha->dwReservedFiles;
// Now find a free entry in the file table.
// Note that in the case when free entries are in the middle,
// we need to use these
for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++)
{
if((pFileEntry->dwFlags & MPQ_FILE_EXISTS) == 0)
{
// Remember the first free entry
if(pFreeEntry == NULL)
pFreeEntry = pFileEntry;
dwFreeCount++;
// If the number of free items is greater than number
// of reserved items, We can add the file
if(dwFreeCount > dwReservedFiles)
break;
}
}
// If the total number of free entries is less than number of reserved files,
// we cannot add the file to the archive
if(pFreeEntry == NULL || dwFreeCount <= dwReservedFiles)
return NULL;
// Initialize the file entry and set its file name
memset(pFreeEntry, 0, sizeof(TFileEntry));
AllocateFileName(ha, pFreeEntry, szFileName);
// If the archive has a hash table, we need to first free entry there
if(ha->pHashTable != NULL)
{
// Make sure that the entry is not there yet
assert(GetHashEntryExact(ha, szFileName, lcLocale) == NULL);
// Find a free hash table entry for the name
pHash = AllocateHashEntry(ha, pFreeEntry, lcLocale);
if(pHash == NULL)
return NULL;
// Set the file index to the hash table
pHash->dwBlockIndex = (DWORD)(pFreeEntry - ha->pFileTable);
PtrHashIndex[0] = (DWORD)(pHash - ha->pHashTable);
}
// If the archive has a HET table, just do some checks
// Note: Don't bother modifying the HET table. It will be rebuilt from scratch after, anyway
if(ha->pHetTable != NULL)
{
assert(GetFileIndex_Het(ha, szFileName) == HASH_ENTRY_FREE);
}
// Return the free table entry
return pFreeEntry;
}
DWORD RenameFileEntry(
TMPQArchive * ha,
TMPQFile * hf,
const char * szNewFileName)
{
TFileEntry * pFileEntry = hf->pFileEntry;
TMPQHash * pHashEntry = hf->pHashEntry;
LCID lcLocale = 0;
// If the archive hash hash table, we need to free the hash table entry
if(ha->pHashTable != NULL)
{
// The file must have hash table entry assigned
// Will exit if there are multiple HASH entries pointing to the same file entry
if(pHashEntry == NULL)
return ERROR_NOT_SUPPORTED;
// Save the locale
lcLocale = pHashEntry->lcLocale;
// Mark the hash table entry as deleted
pHashEntry->dwName1 = 0xFFFFFFFF;
pHashEntry->dwName2 = 0xFFFFFFFF;
pHashEntry->lcLocale = 0xFFFF;
pHashEntry->Platform = 0xFF;
pHashEntry->Reserved = 0xFF;
pHashEntry->dwBlockIndex = HASH_ENTRY_DELETED;
}
// Free the old file name
if(pFileEntry->szFileName != NULL)
STORM_FREE(pFileEntry->szFileName);
pFileEntry->szFileName = NULL;
// Allocate new file name
AllocateFileName(ha, pFileEntry, szNewFileName);
// Allocate new hash entry
if(ha->pHashTable != NULL)
{
// Since we freed one hash entry before, this must succeed
hf->pHashEntry = AllocateHashEntry(ha, pFileEntry, lcLocale);
assert(hf->pHashEntry != NULL);
}
return ERROR_SUCCESS;
}
DWORD DeleteFileEntry(TMPQArchive * ha, TMPQFile * hf)
{
TFileEntry * pFileEntry = hf->pFileEntry;
TMPQHash * pHashEntry = hf->pHashEntry;
// If the archive hash hash table, we need to free the hash table entry
if(ha->pHashTable != NULL)
{
// The file must have hash table entry assigned
// Will exit if there are multiple HASH entries pointing to the same file entry
if(pHashEntry == NULL)
return ERROR_NOT_SUPPORTED;
// Mark the hash table entry as deleted
pHashEntry->dwName1 = 0xFFFFFFFF;
pHashEntry->dwName2 = 0xFFFFFFFF;
pHashEntry->lcLocale = 0xFFFF;
pHashEntry->Platform = 0xFF;
pHashEntry->Reserved = 0xFF;
pHashEntry->dwBlockIndex = HASH_ENTRY_DELETED;
}
// Free the file name, and set the file entry as deleted
if(pFileEntry->szFileName != NULL)
STORM_FREE(pFileEntry->szFileName);
pFileEntry->szFileName = NULL;
//
// Don't modify the HET table, because it gets recreated by the caller
// Don't decrement the number of entries in the file table
// Keep Byte Offset, file size, compressed size, CRC32 and MD5
// Clear the file name hash and the MPQ_FILE_EXISTS bit
//
pFileEntry->dwFlags &= ~MPQ_FILE_EXISTS;
pFileEntry->FileNameHash = 0;
return ERROR_SUCCESS;
}
DWORD InvalidateInternalFile(TMPQArchive * ha, const char * szFileName, DWORD dwFlagNone, DWORD dwFlagNew, DWORD dwForceAddTheFile = 0)
{
TMPQFile * hf = NULL;
DWORD dwFileFlags = MPQ_FILE_DEFAULT_INTERNAL;
DWORD dwErrCode = ERROR_FILE_NOT_FOUND;
// Open the file from the MPQ
if(SFileOpenFileEx((HANDLE)ha, szFileName, SFILE_OPEN_BASE_FILE, (HANDLE *)&hf))
{
// Remember the file flags
dwFileFlags = hf->pFileEntry->dwFlags;
// Delete the file entry
dwErrCode = DeleteFileEntry(ha, hf);
if(dwErrCode == ERROR_SUCCESS)
dwForceAddTheFile = 1;
// Close the file
FreeFileHandle(hf);
}
// Are we going to add the file?
if(dwForceAddTheFile)
{
ha->dwFlags |= dwFlagNew;
ha->dwReservedFiles++;
}
else
{
ha->dwFlags |= dwFlagNone;
dwFileFlags = 0;
}
// Return the intended file flags
return dwFileFlags;
}
void InvalidateInternalFiles(TMPQArchive * ha)
{
// Do nothing if we are in the middle of saving internal files
if(!(ha->dwFlags & MPQ_FLAG_SAVING_TABLES))
{
//
// We clear the file entries for (listfile), (attributes) and (signature)
// For each internal file cleared, we increment the number
// of reserved entries in the file table.
//
// Invalidate the (listfile), if not done yet
if((ha->dwFlags & (MPQ_FLAG_LISTFILE_NONE | MPQ_FLAG_LISTFILE_NEW)) == 0)
{
ha->dwFileFlags1 = InvalidateInternalFile(ha, LISTFILE_NAME, MPQ_FLAG_LISTFILE_NONE, MPQ_FLAG_LISTFILE_NEW, (ha->dwFlags & MPQ_FLAG_LISTFILE_FORCE));
}
// Invalidate the (attributes), if not done yet
if((ha->dwFlags & (MPQ_FLAG_ATTRIBUTES_NONE | MPQ_FLAG_ATTRIBUTES_NEW)) == 0)
{
ha->dwFileFlags2 = InvalidateInternalFile(ha, ATTRIBUTES_NAME, MPQ_FLAG_ATTRIBUTES_NONE, MPQ_FLAG_ATTRIBUTES_NEW);
}
// Invalidate the (signature), if not done yet
if((ha->dwFlags & (MPQ_FLAG_SIGNATURE_NONE | MPQ_FLAG_SIGNATURE_NEW)) == 0)
{
ha->dwFileFlags3 = InvalidateInternalFile(ha, SIGNATURE_NAME, MPQ_FLAG_SIGNATURE_NONE, MPQ_FLAG_SIGNATURE_NEW);
}
// Remember that the MPQ has been changed
ha->dwFlags |= MPQ_FLAG_CHANGED;
}
}
//-----------------------------------------------------------------------------
// Support for file tables - hash table, block table, hi-block table
DWORD CreateHashTable(TMPQArchive * ha, DWORD dwHashTableSize)
{
TMPQHash * pHashTable;
// Sanity checks
assert((dwHashTableSize & (dwHashTableSize - 1)) == 0);
assert(ha->pHashTable == NULL);
// If the required hash table size is zero, don't create anything
if(dwHashTableSize == 0)
dwHashTableSize = HASH_TABLE_SIZE_DEFAULT;
// Create the hash table
pHashTable = STORM_ALLOC(TMPQHash, dwHashTableSize);
if(pHashTable == NULL)
return ERROR_NOT_ENOUGH_MEMORY;
// Fill it
memset(pHashTable, 0xFF, dwHashTableSize * sizeof(TMPQHash));
ha->pHeader->dwHashTableSize = dwHashTableSize;
ha->dwMaxFileCount = dwHashTableSize;
ha->pHashTable = pHashTable;
return ERROR_SUCCESS;
}
static TMPQHash * LoadHashTable(TMPQArchive * ha)
{
TMPQHeader * pHeader = ha->pHeader;
ULONGLONG ByteOffset;
TMPQHash * pHashTable = NULL;
DWORD dwTableSize;
DWORD dwCmpSize;
bool bHashTableIsCut = false;
// Note: It is allowed to load hash table if it is at offset 0.
// Example: MPQ_2016_v1_ProtectedMap_HashOffsIsZero.w3x
// if(pHeader->dwHashTablePos == 0 && pHeader->wHashTablePosHi == 0)
// return NULL;
// If the hash table size is zero, do nothing
if(pHeader->dwHashTableSize == 0)
return NULL;
// Load the hash table for MPQ variations
switch(ha->dwSubType)
{
case MPQ_SUBTYPE_MPQ:
// Calculate the position and size of the hash table
ByteOffset = FileOffsetFromMpqOffset(ha, MAKE_OFFSET64(pHeader->wHashTablePosHi, pHeader->dwHashTablePos));
dwTableSize = pHeader->dwHashTableSize * sizeof(TMPQHash);
dwCmpSize = (DWORD)pHeader->HashTableSize64;
// Read, decrypt and uncompress the hash table
pHashTable = (TMPQHash *)LoadMpqTable(ha, ByteOffset, pHeader->MD5_HashTable, dwCmpSize, dwTableSize, g_dwHashTableKey, &bHashTableIsCut);
// DumpHashTable(pHashTable, pHeader->dwHashTableSize);
// If the hash table was cut, we can/have to defragment it
if(pHashTable != NULL && bHashTableIsCut)
ha->dwFlags |= (MPQ_FLAG_MALFORMED | MPQ_FLAG_HASH_TABLE_CUT);
break;
case MPQ_SUBTYPE_SQP:
pHashTable = LoadSqpHashTable(ha);
break;
case MPQ_SUBTYPE_MPK:
pHashTable = LoadMpkHashTable(ha);
break;
}
// Return the loaded hash table
return pHashTable;
}
DWORD CreateFileTable(TMPQArchive * ha, DWORD dwFileTableSize)
{
ha->pFileTable = STORM_ALLOC(TFileEntry, dwFileTableSize);
if(ha->pFileTable == NULL)
return ERROR_NOT_ENOUGH_MEMORY;
memset(ha->pFileTable, 0x00, sizeof(TFileEntry) * dwFileTableSize);
ha->dwFileTableSize = dwFileTableSize;
return ERROR_SUCCESS;
}
TMPQBlock * LoadBlockTable(TMPQArchive * ha, bool /* bDontFixEntries */)
{
TMPQHeader * pHeader = ha->pHeader;
TMPQBlock * pBlockTable = NULL;
ULONGLONG ByteOffset;
DWORD dwTableSize;
DWORD dwCmpSize;
bool bBlockTableIsCut = false;
// Note: It is possible that the block table starts at offset 0
// Example: MPQ_2016_v1_ProtectedMap_HashOffsIsZero.w3x
// if(pHeader->dwBlockTablePos == 0 && pHeader->wBlockTablePosHi == 0)
// return NULL;
// Do nothing if the block table size is zero
if(pHeader->dwBlockTableSize == 0)
return NULL;
// Load the block table for MPQ variations
switch(ha->dwSubType)
{
case MPQ_SUBTYPE_MPQ:
// Calculate byte position of the block table
ByteOffset = FileOffsetFromMpqOffset(ha, MAKE_OFFSET64(pHeader->wBlockTablePosHi, pHeader->dwBlockTablePos));
dwTableSize = pHeader->dwBlockTableSize * sizeof(TMPQBlock);
dwCmpSize = (DWORD)pHeader->BlockTableSize64;
// Read, decrypt and uncompress the block table
pBlockTable = (TMPQBlock * )LoadMpqTable(ha, ByteOffset, NULL, dwCmpSize, dwTableSize, g_dwBlockTableKey, &bBlockTableIsCut);
// If the block table was cut, we need to remember it
if(pBlockTable != NULL && bBlockTableIsCut)
ha->dwFlags |= (MPQ_FLAG_MALFORMED | MPQ_FLAG_BLOCK_TABLE_CUT);
break;
case MPQ_SUBTYPE_SQP:
pBlockTable = LoadSqpBlockTable(ha);
break;
case MPQ_SUBTYPE_MPK:
pBlockTable = LoadMpkBlockTable(ha);
break;
}
return pBlockTable;
}
TMPQHetTable * LoadHetTable(TMPQArchive * ha)
{
TMPQExtHeader * pExtTable;
TMPQHetTable * pHetTable = NULL;
TMPQHeader * pHeader = ha->pHeader;
// If the HET table position is not 0, we expect the table to be present
if(pHeader->HetTablePos64 && pHeader->HetTableSize64)
{
// Attempt to load the HET table (Hash Extended Table)
pExtTable = LoadExtTable(ha, pHeader->HetTablePos64, (size_t)pHeader->HetTableSize64, HET_TABLE_SIGNATURE, MPQ_KEY_HASH_TABLE);
if(pExtTable != NULL)
{
// If loading HET table fails, we ignore the result.
pHetTable = TranslateHetTable((TMPQHetHeader *)pExtTable);
STORM_FREE(pExtTable);
}
}
return pHetTable;
}
TMPQBetTable * LoadBetTable(TMPQArchive * ha)
{
TMPQExtHeader * pExtTable;
TMPQBetTable * pBetTable = NULL;
TMPQHeader * pHeader = ha->pHeader;
// If the BET table position is not 0, we expect the table to be present
if(pHeader->BetTablePos64 && pHeader->BetTableSize64)
{
// Attempt to load the HET table (Hash Extended Table)
pExtTable = LoadExtTable(ha, pHeader->BetTablePos64, (size_t)pHeader->BetTableSize64, BET_TABLE_SIGNATURE, MPQ_KEY_BLOCK_TABLE);
if(pExtTable != NULL)
{
// If succeeded, we translate the BET table
// to more readable form
pBetTable = TranslateBetTable(ha, (TMPQBetHeader *)pExtTable);
STORM_FREE(pExtTable);
}
}
return pBetTable;
}
DWORD LoadAnyHashTable(TMPQArchive * ha)
{
TMPQHeader * pHeader = ha->pHeader;
// If the MPQ archive is empty, don't bother trying to load anything
if(pHeader->dwHashTableSize == 0 && pHeader->HetTableSize64 == 0)
return CreateHashTable(ha, HASH_TABLE_SIZE_DEFAULT);
// Try to load HET table
if(pHeader->HetTablePos64 != 0)
ha->pHetTable = LoadHetTable(ha);
// Try to load classic hash table
// Note that we load the classic hash table even when HET table exists,
// because if the MPQ gets modified and saved, hash table must be there
if(pHeader->dwHashTableSize)
ha->pHashTable = LoadHashTable(ha);
// At least one of the tables must be present
if(ha->pHetTable == NULL && ha->pHashTable == NULL)
return ERROR_FILE_CORRUPT;
// Set the maximum file count to the size of the hash table.
// Note: We don't care about HET table limits, because HET table is rebuilt
// after each file add/rename/delete.
ha->dwMaxFileCount = (ha->pHashTable != NULL) ? pHeader->dwHashTableSize : HASH_TABLE_SIZE_MAX;
return ERROR_SUCCESS;
}
static DWORD BuildFileTable_Classic(TMPQArchive * ha)
{
TMPQHeader * pHeader = ha->pHeader;
TMPQBlock * pBlockTable;
DWORD dwErrCode = ERROR_SUCCESS;
// Sanity checks
assert(ha->pHashTable != NULL);
assert(ha->pFileTable != NULL);
// If the MPQ has no block table, do nothing
if(pHeader->dwBlockTableSize == 0)
return ERROR_SUCCESS;
assert(ha->dwFileTableSize >= pHeader->dwBlockTableSize);
// Load the block table
// WARNING! ha->pFileTable can change in the process!!
pBlockTable = (TMPQBlock *)LoadBlockTable(ha);
if(pBlockTable != NULL)
{
dwErrCode = BuildFileTableFromBlockTable(ha, pBlockTable);
STORM_FREE(pBlockTable);
}
else
{
dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
}
// Load the hi-block table
if(dwErrCode == ERROR_SUCCESS && pHeader->HiBlockTablePos64 != 0)
{
ULONGLONG ByteOffset;
USHORT * pHiBlockTable = NULL;
DWORD dwTableSize = pHeader->dwBlockTableSize * sizeof(USHORT);
// Allocate space for the hi-block table
// Note: pHeader->dwBlockTableSize can be zero !!!
pHiBlockTable = STORM_ALLOC(USHORT, pHeader->dwBlockTableSize + 1);
if(pHiBlockTable != NULL)
{
// Load the hi-block table. It is not encrypted, nor compressed
ByteOffset = ha->MpqPos + pHeader->HiBlockTablePos64;
if(!FileStream_Read(ha->pStream, &ByteOffset, pHiBlockTable, dwTableSize))
dwErrCode = GetLastError();
// Now merge the hi-block table to the file table
if(dwErrCode == ERROR_SUCCESS)
{
TFileEntry * pFileEntry = ha->pFileTable;
// Swap the hi-block table
BSWAP_ARRAY16_UNSIGNED(pHiBlockTable, dwTableSize);
// Add the high file offset to the base file offset.
for(DWORD i = 0; i < pHeader->dwBlockTableSize; i++, pFileEntry++)
pFileEntry->ByteOffset = MAKE_OFFSET64(pHiBlockTable[i], pFileEntry->ByteOffset);
}
// Free the hi-block table
STORM_FREE(pHiBlockTable);
}
else
{
dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
}
}
return dwErrCode;
}
static DWORD BuildFileTable_HetBet(TMPQArchive * ha)
{
TMPQHetTable * pHetTable = ha->pHetTable;
TMPQBetTable * pBetTable;
TFileEntry * pFileEntry = ha->pFileTable;
TMPQBits * pBitArray;
DWORD dwBitPosition = 0;
DWORD i;
DWORD dwErrCode = ERROR_FILE_CORRUPT;
// Load the BET table from the MPQ
pBetTable = LoadBetTable(ha);
if(pBetTable != NULL)
{
// Verify the size of NameHash2 in the BET table.
// It has to be 8 bits less than the information in HET table
if((pBetTable->dwBitCount_NameHash2 + 8) != pHetTable->dwNameHashBitSize)
{
FreeBetTable(pBetTable);
return ERROR_FILE_CORRUPT;
}
// Step one: Fill the name indexes
for(i = 0; i < pHetTable->dwTotalCount; i++)
{
DWORD dwFileIndex = 0;
// Is the entry in the HET table occupied?
if(pHetTable->pNameHashes[i] != HET_ENTRY_FREE)
{
// Load the index to the BET table
pHetTable->pBetIndexes->GetBits(pHetTable->dwIndexSizeTotal * i,
pHetTable->dwIndexSize,
&dwFileIndex,
4);
// Overflow test
if(dwFileIndex < pBetTable->dwEntryCount)
{
ULONGLONG NameHash1 = pHetTable->pNameHashes[i];
ULONGLONG NameHash2 = 0;
// Load the BET hash
pBetTable->pNameHashes->GetBits(pBetTable->dwBitTotal_NameHash2 * dwFileIndex,
pBetTable->dwBitCount_NameHash2,
&NameHash2,
8);
// Combine both part of the name hash and put it to the file table
pFileEntry = ha->pFileTable + dwFileIndex;
pFileEntry->FileNameHash = (NameHash1 << pBetTable->dwBitCount_NameHash2) | NameHash2;
}
}
}
// Go through the entire BET table and convert it to the file table.
pFileEntry = ha->pFileTable;
pBitArray = pBetTable->pFileTable;
for(i = 0; i < pBetTable->dwEntryCount; i++)
{
DWORD dwFlagIndex = 0;
// Read the file position
pBitArray->GetBits(dwBitPosition + pBetTable->dwBitIndex_FilePos,
pBetTable->dwBitCount_FilePos,
&pFileEntry->ByteOffset,
8);
// Read the file size
pBitArray->GetBits(dwBitPosition + pBetTable->dwBitIndex_FileSize,
pBetTable->dwBitCount_FileSize,
&pFileEntry->dwFileSize,
4);
// Read the compressed size
pBitArray->GetBits(dwBitPosition + pBetTable->dwBitIndex_CmpSize,
pBetTable->dwBitCount_CmpSize,
&pFileEntry->dwCmpSize,
4);
// Read the flag index
if(pBetTable->dwFlagCount != 0)
{
pBitArray->GetBits(dwBitPosition + pBetTable->dwBitIndex_FlagIndex,
pBetTable->dwBitCount_FlagIndex,
&dwFlagIndex,
4);
pFileEntry->dwFlags = pBetTable->pFileFlags[dwFlagIndex];
}
//
// TODO: Locale (?)
//
// Move the current bit position
dwBitPosition += pBetTable->dwTableEntrySize;
pFileEntry++;
}
// Set the current size of the file table
FreeBetTable(pBetTable);
dwErrCode = ERROR_SUCCESS;
}
else
{
dwErrCode = ERROR_FILE_CORRUPT;
}
return dwErrCode;
}
DWORD BuildFileTable(TMPQArchive * ha)
{
DWORD dwFileTableSize;
bool bFileTableCreated = false;
// Sanity checks
assert(ha->pFileTable == NULL);
assert(ha->dwFileTableSize == 0);
assert(ha->dwMaxFileCount != 0);
// Determine the allocation size for the file table
dwFileTableSize = STORMLIB_MAX(ha->pHeader->dwBlockTableSize, ha->dwMaxFileCount);
// Allocate the file table with size determined before
ha->pFileTable = STORM_ALLOC(TFileEntry, dwFileTableSize);
if(ha->pFileTable == NULL)
return ERROR_NOT_ENOUGH_MEMORY;
// Fill the table with zeros
memset(ha->pFileTable, 0, dwFileTableSize * sizeof(TFileEntry));
ha->dwFileTableSize = dwFileTableSize;
// If we have HET table, we load file table from the BET table
// Note: If BET table is corrupt or missing, we set the archive as read only
if(ha->pHetTable != NULL)
{
if(BuildFileTable_HetBet(ha) != ERROR_SUCCESS)
ha->dwFlags |= MPQ_FLAG_READ_ONLY;
else
bFileTableCreated = true;
}
// If we have hash table, we load the file table from the block table
// Note: If block table is corrupt or missing, we set the archive as read only
if(ha->pHashTable != NULL)
{
if(BuildFileTable_Classic(ha) != ERROR_SUCCESS)
ha->dwFlags |= MPQ_FLAG_READ_ONLY;
else
bFileTableCreated = true;
}
// Return result
return bFileTableCreated ? ERROR_SUCCESS : ERROR_FILE_CORRUPT;
}
/*
void UpdateBlockTableSize(TMPQArchive * ha)
{
TFileEntry * pFileTableEnd = ha->pFileTable + ha->dwFileTableSize;
TFileEntry * pFileEntry;
DWORD dwBlockTableSize = 0;
// Calculate the number of files
for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++)
{
// If the source table entry is valid,
if(pFileEntry->dwFlags & MPQ_FILE_EXISTS)
dwBlockTableSize = (DWORD)(pFileEntry - ha->pFileTable) + 1;
}
// Save the block table size to the MPQ header
ha->pHeader->dwBlockTableSize = ha->dwReservedFiles + dwBlockTableSize;
}
*/
// Defragment the file table so it does not contain any gaps
DWORD DefragmentFileTable(TMPQArchive * ha)
{
TFileEntry * pFileTableEnd = ha->pFileTable + ha->dwFileTableSize;
TFileEntry * pSource = ha->pFileTable;
TFileEntry * pTarget = ha->pFileTable;
LPDWORD DefragmentTable;
DWORD dwBlockTableSize = 0;
DWORD dwSrcIndex;
DWORD dwTrgIndex;
// Allocate brand new file table
DefragmentTable = STORM_ALLOC(DWORD, ha->dwFileTableSize);
if(DefragmentTable != NULL)
{
// Clear the file table
memset(DefragmentTable, 0xFF, sizeof(DWORD) * ha->dwFileTableSize);
// Parse the entire file table and defragment it
for(; pSource < pFileTableEnd; pSource++)
{
// If the source table entry is valid,
if(pSource->dwFlags & MPQ_FILE_EXISTS)
{
// Remember the index conversion
dwSrcIndex = (DWORD)(pSource - ha->pFileTable);
dwTrgIndex = (DWORD)(pTarget - ha->pFileTable);
DefragmentTable[dwSrcIndex] = dwTrgIndex;
// Move the entry, if needed
if(pTarget != pSource)
pTarget[0] = pSource[0];
pTarget++;
// Update the block table size
dwBlockTableSize = (DWORD)(pTarget - ha->pFileTable);
}
else
{
// If there is file name left, free it
if(pSource->szFileName != NULL)
STORM_FREE(pSource->szFileName);
pSource->szFileName = NULL;
}
}
// Did we defragment something?
if(pTarget < pFileTableEnd)
{
// Clear the remaining file entries
memset(pTarget, 0, (pFileTableEnd - pTarget) * sizeof(TFileEntry));
// Go through the hash table and relocate the block indexes
if(ha->pHashTable != NULL)
{
TMPQHash * pHashTableEnd = ha->pHashTable + ha->pHeader->dwHashTableSize;
TMPQHash * pHash;
DWORD dwNewBlockIndex;
for(pHash = ha->pHashTable; pHash < pHashTableEnd; pHash++)
{
if(MPQ_BLOCK_INDEX(pHash) < ha->dwFileTableSize)
{
// If that block entry is there, set it to the hash entry
// If not, set it as DELETED
dwNewBlockIndex = DefragmentTable[MPQ_BLOCK_INDEX(pHash)];
pHash->dwBlockIndex = (dwNewBlockIndex != HASH_ENTRY_FREE) ? dwNewBlockIndex : HASH_ENTRY_DELETED;
}
}
}
}
// Save the block table size
ha->pHeader->dwBlockTableSize = ha->dwReservedFiles + dwBlockTableSize;
// Free the defragment table
STORM_FREE(DefragmentTable);
}
return ERROR_SUCCESS;
}
// Rebuilds the HET table from scratch based on the file table
// Used after a modifying operation (add, rename, delete)
DWORD RebuildHetTable(TMPQArchive * ha)
{
TMPQHetTable * pOldHetTable = ha->pHetTable;
TFileEntry * pFileTableEnd;
TFileEntry * pFileEntry;
DWORD dwBlockTableSize = ha->dwFileTableSize;
DWORD dwErrCode = ERROR_SUCCESS;
// If we are in the state of saving MPQ tables, the real size of block table
// must already have been calculated. Use that value instead
if(ha->dwFlags & MPQ_FLAG_SAVING_TABLES)
{
assert(ha->pHeader->dwBlockTableSize != 0);
dwBlockTableSize = ha->pHeader->dwBlockTableSize;
}
// Create new HET table based on the total number of entries in the file table
// Note that if we fail to create it, we just stop using HET table
ha->pHetTable = CreateHetTable(dwBlockTableSize, 0, 0x40, NULL);
if(ha->pHetTable != NULL)
{
// Go through the file table again and insert all existing files
pFileTableEnd = ha->pFileTable + dwBlockTableSize;
for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++)
{
if(pFileEntry->dwFlags & MPQ_FILE_EXISTS)
{
// Get the high
dwErrCode = InsertHetEntry(ha->pHetTable, pFileEntry->FileNameHash, (DWORD)(pFileEntry - ha->pFileTable));
if(dwErrCode != ERROR_SUCCESS)
break;
}
}
}
// Free the old HET table
FreeHetTable(pOldHetTable);
return dwErrCode;
}
// Rebuilds the file table, removing all deleted file entries.
// Used when compacting the archive
DWORD RebuildFileTable(TMPQArchive * ha, DWORD dwNewHashTableSize)
{
TFileEntry * pFileEntry;
TMPQHash * pHashTableEnd = ha->pHashTable + ha->pHeader->dwHashTableSize;
TMPQHash * pOldHashTable = ha->pHashTable;
TMPQHash * pHashTable = NULL;
TMPQHash * pHash;
DWORD dwErrCode = ERROR_SUCCESS;
// The new hash table size must be greater or equal to the current hash table size
assert(dwNewHashTableSize >= ha->pHeader->dwHashTableSize);
assert(dwNewHashTableSize >= ha->dwMaxFileCount);
assert((dwNewHashTableSize & (dwNewHashTableSize - 1)) == 0);
assert(ha->pHashTable != NULL);
// Reallocate the new file table, if needed
if(dwNewHashTableSize > ha->dwFileTableSize)
{
ha->pFileTable = STORM_REALLOC(TFileEntry, ha->pFileTable, dwNewHashTableSize);
if(ha->pFileTable == NULL)
return ERROR_NOT_ENOUGH_MEMORY;
memset(ha->pFileTable + ha->dwFileTableSize, 0, (dwNewHashTableSize - ha->dwFileTableSize) * sizeof(TFileEntry));
}
// Allocate new hash table
if(dwErrCode == ERROR_SUCCESS)
{
pHashTable = STORM_ALLOC(TMPQHash, dwNewHashTableSize);
if(pHashTable == NULL)
dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
}
// If both succeeded, we need to rebuild the file table
if(dwErrCode == ERROR_SUCCESS)
{
// Make sure that the hash table is properly filled
memset(pHashTable, 0xFF, sizeof(TMPQHash) * dwNewHashTableSize);
ha->pHashTable = pHashTable;
// Set the new limits to the MPQ archive
ha->pHeader->dwHashTableSize = dwNewHashTableSize;
// Parse the old hash table and copy all entries to the new table
for(pHash = pOldHashTable; pHash < pHashTableEnd; pHash++)
{
if(IsValidHashEntry(ha, pHash))
{
pFileEntry = ha->pFileTable + MPQ_BLOCK_INDEX(pHash);
AllocateHashEntry(ha, pFileEntry, pHash->lcLocale);
}
}
// Increment the max file count for the file
ha->dwFileTableSize = dwNewHashTableSize;
ha->dwMaxFileCount = dwNewHashTableSize;
ha->dwFlags |= MPQ_FLAG_CHANGED;
}
// Now free the remaining entries
if(pOldHashTable != NULL)
STORM_FREE(pOldHashTable);
return dwErrCode;
}
// Saves MPQ header, hash table, block table and hi-block table.
DWORD SaveMPQTables(TMPQArchive * ha)
{
TMPQExtHeader * pHetTable = NULL;
TMPQExtHeader * pBetTable = NULL;
TMPQHeader * pHeader = ha->pHeader;
TMPQBlock * pBlockTable = NULL;
TMPQHash * pHashTable = NULL;
ULONGLONG HetTableSize64 = 0;
ULONGLONG BetTableSize64 = 0;
ULONGLONG HashTableSize64 = 0;
ULONGLONG BlockTableSize64 = 0;
ULONGLONG HiBlockTableSize64 = 0;
ULONGLONG TablePos = 0; // A table position, relative to the begin of the MPQ
USHORT * pHiBlockTable = NULL;
DWORD cbTotalSize;
bool bNeedHiBlockTable = false;
DWORD dwErrCode = ERROR_SUCCESS;
// We expect this function to be called only when tables have been changed
assert(ha->dwFlags & MPQ_FLAG_CHANGED);
// Find the space where the MPQ tables will be saved
TablePos = FindFreeMpqSpace(ha);
// If the MPQ has HET table, we prepare a ready-to-save version
if(dwErrCode == ERROR_SUCCESS && ha->pHetTable != NULL)
{
pHetTable = TranslateHetTable(ha->pHetTable, &HetTableSize64);
if(pHetTable == NULL)
dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
}
// If the MPQ has HET table, we also must create BET table to be saved
if(dwErrCode == ERROR_SUCCESS && ha->pHetTable != NULL)
{
pBetTable = TranslateBetTable(ha, &BetTableSize64);
if(pBetTable == NULL)
dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
}
// Now create hash table
if(dwErrCode == ERROR_SUCCESS && ha->pHashTable != NULL)
{
pHashTable = TranslateHashTable(ha, &HashTableSize64);
if(pHashTable == NULL)
dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
}
// Create block table
if(dwErrCode == ERROR_SUCCESS && ha->pFileTable != NULL)
{
pBlockTable = TranslateBlockTable(ha, &BlockTableSize64, &bNeedHiBlockTable);
if(pBlockTable == NULL)
dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
}
// Create hi-block table, if needed
if(dwErrCode == ERROR_SUCCESS && bNeedHiBlockTable)
{
pHiBlockTable = TranslateHiBlockTable(ha, &HiBlockTableSize64);
if(pHiBlockTable == NULL)
dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
}
// Write the HET table, if any
if(dwErrCode == ERROR_SUCCESS && pHetTable != NULL)
{
pHeader->HetTableSize64 = HetTableSize64;
pHeader->HetTablePos64 = TablePos;
dwErrCode = SaveExtTable(ha, pHetTable, TablePos, (DWORD)HetTableSize64, pHeader->MD5_HetTable, MPQ_KEY_HASH_TABLE, false, &cbTotalSize);
TablePos += cbTotalSize;
}
// Write the BET table, if any
if(dwErrCode == ERROR_SUCCESS && pBetTable != NULL)
{
pHeader->BetTableSize64 = BetTableSize64;
pHeader->BetTablePos64 = TablePos;
dwErrCode = SaveExtTable(ha, pBetTable, TablePos, (DWORD)BetTableSize64, pHeader->MD5_BetTable, MPQ_KEY_BLOCK_TABLE, false, &cbTotalSize);
TablePos += cbTotalSize;
}
// Write the hash table, if we have any
if(dwErrCode == ERROR_SUCCESS && pHashTable != NULL)
{
pHeader->HashTableSize64 = HashTableSize64;
pHeader->wHashTablePosHi = (USHORT)(TablePos >> 32);
pHeader->dwHashTableSize = (DWORD)(HashTableSize64 / sizeof(TMPQHash));
pHeader->dwHashTablePos = (DWORD)TablePos;
dwErrCode = SaveMpqTable(ha, pHashTable, TablePos, (size_t)HashTableSize64, pHeader->MD5_HashTable, MPQ_KEY_HASH_TABLE, false);
TablePos += HashTableSize64;
}
// Write the block table, if we have any
if(dwErrCode == ERROR_SUCCESS && pBlockTable != NULL)
{
pHeader->BlockTableSize64 = BlockTableSize64;
pHeader->wBlockTablePosHi = (USHORT)(TablePos >> 32);
pHeader->dwBlockTableSize = (DWORD)(BlockTableSize64 / sizeof(TMPQBlock));
pHeader->dwBlockTablePos = (DWORD)TablePos;
dwErrCode = SaveMpqTable(ha, pBlockTable, TablePos, (size_t)BlockTableSize64, pHeader->MD5_BlockTable, MPQ_KEY_BLOCK_TABLE, false);
TablePos += BlockTableSize64;
}
// Write the hi-block table, if we have any
if(dwErrCode == ERROR_SUCCESS && pHiBlockTable != NULL)
{
ULONGLONG ByteOffset = ha->MpqPos + TablePos;
pHeader->HiBlockTableSize64 = HiBlockTableSize64;
pHeader->HiBlockTablePos64 = TablePos;
BSWAP_ARRAY16_UNSIGNED(pHiBlockTable, HiBlockTableSize64);
if(!FileStream_Write(ha->pStream, &ByteOffset, pHiBlockTable, (DWORD)HiBlockTableSize64))
dwErrCode = GetLastError();
TablePos += HiBlockTableSize64;
}
// Cut the MPQ
if(dwErrCode == ERROR_SUCCESS)
{
ULONGLONG FileSize = ha->MpqPos + TablePos;
if(!FileStream_SetSize(ha->pStream, FileSize))
dwErrCode = GetLastError();
}
// Write the MPQ header
if(dwErrCode == ERROR_SUCCESS)
{
TMPQHeader SaveMpqHeader;
// Update the size of the archive
pHeader->ArchiveSize64 = TablePos;
pHeader->dwArchiveSize = (DWORD)TablePos;
// Update the MD5 of the archive header
CalculateDataBlockHash(pHeader, MPQ_HEADER_SIZE_V4 - MD5_DIGEST_SIZE, pHeader->MD5_MpqHeader);
// Write the MPQ header to the file
memcpy(&SaveMpqHeader, pHeader, pHeader->dwHeaderSize);
BSWAP_TMPQHEADER(&SaveMpqHeader, MPQ_FORMAT_VERSION_1);
BSWAP_TMPQHEADER(&SaveMpqHeader, MPQ_FORMAT_VERSION_2);
BSWAP_TMPQHEADER(&SaveMpqHeader, MPQ_FORMAT_VERSION_3);
BSWAP_TMPQHEADER(&SaveMpqHeader, MPQ_FORMAT_VERSION_4);
if(!FileStream_Write(ha->pStream, &ha->MpqPos, &SaveMpqHeader, pHeader->dwHeaderSize))
dwErrCode = GetLastError();
}
// Clear the changed flag
if(dwErrCode == ERROR_SUCCESS)
ha->dwFlags &= ~MPQ_FLAG_CHANGED;
// Cleanup and exit
if(pHetTable != NULL)
STORM_FREE(pHetTable);
if(pBetTable != NULL)
STORM_FREE(pBetTable);
if(pHashTable != NULL)
STORM_FREE(pHashTable);
if(pBlockTable != NULL)
STORM_FREE(pBlockTable);
if(pHiBlockTable != NULL)
STORM_FREE(pHiBlockTable);
return dwErrCode;
}