Shipwright/StormLib/src/SFileAddFile.cpp

1317 lines
45 KiB
C++

/*****************************************************************************/
/* SFileAddFile.cpp Copyright (c) Ladislav Zezula 2010 */
/*---------------------------------------------------------------------------*/
/* MPQ Editing functions */
/*---------------------------------------------------------------------------*/
/* Date Ver Who Comment */
/* -------- ---- --- ------- */
/* 27.03.10 1.00 Lad Splitted from SFileCreateArchiveEx.cpp */
/* 21.04.13 1.01 Dea AddFile callback now part of TMPQArchive */
/*****************************************************************************/
#define __STORMLIB_SELF__
#include "StormLib.h"
#include "StormCommon.h"
//-----------------------------------------------------------------------------
// Local variables
// Mask for lossy compressions
#define MPQ_LOSSY_COMPRESSION_MASK (MPQ_COMPRESSION_ADPCM_MONO | MPQ_COMPRESSION_ADPCM_STEREO | MPQ_COMPRESSION_HUFFMANN)
// Data compression for SFileAddFile
// Kept here for compatibility with code that was created with StormLib version < 6.50
static DWORD DefaultDataCompression = MPQ_COMPRESSION_PKWARE;
//-----------------------------------------------------------------------------
// WAVE verification
#define FILE_SIGNATURE_RIFF 0x46464952
#define FILE_SIGNATURE_WAVE 0x45564157
#define FILE_SIGNATURE_FMT 0x20746D66
#define AUDIO_FORMAT_PCM 1
typedef struct _WAVE_FILE_HEADER
{
DWORD dwChunkId; // 0x52494646 ("RIFF")
DWORD dwChunkSize; // Size of that chunk, in bytes
DWORD dwFormat; // Must be 0x57415645 ("WAVE")
// Format sub-chunk
DWORD dwSubChunk1Id; // 0x666d7420 ("fmt ")
DWORD dwSubChunk1Size; // 0x16 for PCM
USHORT wAudioFormat; // 1 = PCM. Other value means some sort of compression
USHORT wChannels; // Number of channels
DWORD dwSampleRate; // 8000, 44100, etc.
DWORD dwBytesRate; // SampleRate * NumChannels * BitsPerSample/8
USHORT wBlockAlign; // NumChannels * BitsPerSample/8
USHORT wBitsPerSample; // 8 bits = 8, 16 bits = 16, etc.
// Followed by "data" sub-chunk (we don't care)
} WAVE_FILE_HEADER, *PWAVE_FILE_HEADER;
static bool IsWaveFile_16BitsPerAdpcmSample(
LPBYTE pbFileData,
DWORD cbFileData,
LPDWORD pdwChannels)
{
PWAVE_FILE_HEADER pWaveHdr = (PWAVE_FILE_HEADER)pbFileData;
// The amount of file data must be at least size of WAVE header
if(cbFileData > sizeof(WAVE_FILE_HEADER))
{
// Check for the RIFF header
if(pWaveHdr->dwChunkId == FILE_SIGNATURE_RIFF && pWaveHdr->dwFormat == FILE_SIGNATURE_WAVE)
{
// Check for ADPCM format
if(pWaveHdr->dwSubChunk1Id == FILE_SIGNATURE_FMT && pWaveHdr->wAudioFormat == AUDIO_FORMAT_PCM)
{
// Now the number of bits per sample must be at least 16.
// If not, the WAVE file gets corrupted by the ADPCM compression
if(pWaveHdr->wBitsPerSample >= 0x10)
{
*pdwChannels = pWaveHdr->wChannels;
return true;
}
}
}
}
return false;
}
static int FillWritableHandle(
TMPQArchive * ha,
TMPQFile * hf,
ULONGLONG FileTime,
DWORD dwFileSize,
DWORD dwFlags)
{
TFileEntry * pFileEntry = hf->pFileEntry;
// Initialize the hash entry for the file
hf->RawFilePos = ha->MpqPos + hf->MpqFilePos;
hf->dwDataSize = dwFileSize;
// Initialize the block table entry for the file
pFileEntry->ByteOffset = hf->MpqFilePos;
pFileEntry->dwFileSize = dwFileSize;
pFileEntry->dwCmpSize = 0;
pFileEntry->dwFlags = dwFlags | MPQ_FILE_EXISTS;
// Initialize the file time, CRC32 and MD5
//assert(sizeof(hf->hctx) >= sizeof(hash_state));
memset(pFileEntry->md5, 0, MD5_DIGEST_SIZE);
md5_init((hash_state *)hf->hctx);
pFileEntry->dwCrc32 = crc32(0, Z_NULL, 0);
// If the caller gave us a file time, use it.
pFileEntry->FileTime = FileTime;
// Mark the archive as modified
ha->dwFlags |= MPQ_FLAG_CHANGED;
// Call the callback, if needed
if(ha->pfnAddFileCB != NULL)
ha->pfnAddFileCB(ha->pvAddFileUserData, 0, hf->dwDataSize, false);
hf->dwAddFileError = ERROR_SUCCESS;
return ERROR_SUCCESS;
}
//-----------------------------------------------------------------------------
// MPQ write data functions
static DWORD WriteDataToMpqFile(
TMPQArchive * ha,
TMPQFile * hf,
LPBYTE pbFileData,
DWORD dwDataSize,
DWORD dwCompression)
{
TFileEntry * pFileEntry = hf->pFileEntry;
ULONGLONG ByteOffset;
LPBYTE pbCompressed = NULL; // Compressed (target) data
LPBYTE pbToWrite = hf->pbFileSector; // Data to write to the file
DWORD dwErrCode = ERROR_SUCCESS;
int nCompressionLevel; // ADPCM compression level (only used for wave files)
// Make sure that the caller won't overrun the previously initiated file size
assert(hf->dwFilePos + dwDataSize <= pFileEntry->dwFileSize);
assert(hf->dwSectorCount != 0);
assert(hf->pbFileSector != NULL);
if((hf->dwFilePos + dwDataSize) > pFileEntry->dwFileSize)
return ERROR_DISK_FULL;
// Now write all data to the file sector buffer
if(dwErrCode == ERROR_SUCCESS)
{
DWORD dwBytesInSector = hf->dwFilePos % hf->dwSectorSize;
DWORD dwSectorIndex = hf->dwFilePos / hf->dwSectorSize;
DWORD dwBytesToCopy;
// Process all data.
while(dwDataSize != 0)
{
dwBytesToCopy = dwDataSize;
// Check for sector overflow
if(dwBytesToCopy > (hf->dwSectorSize - dwBytesInSector))
dwBytesToCopy = (hf->dwSectorSize - dwBytesInSector);
// Copy the data to the file sector
memcpy(hf->pbFileSector + dwBytesInSector, pbFileData, dwBytesToCopy);
dwBytesInSector += dwBytesToCopy;
pbFileData += dwBytesToCopy;
dwDataSize -= dwBytesToCopy;
// Update the file position
hf->dwFilePos += dwBytesToCopy;
// If the current sector is full, or if the file is already full,
// then write the data to the MPQ
if(dwBytesInSector >= hf->dwSectorSize || hf->dwFilePos >= pFileEntry->dwFileSize)
{
// Set the position in the file
ByteOffset = hf->RawFilePos + pFileEntry->dwCmpSize;
// Update CRC32 and MD5 of the file
md5_process((hash_state *)hf->hctx, hf->pbFileSector, dwBytesInSector);
hf->dwCrc32 = crc32(hf->dwCrc32, hf->pbFileSector, dwBytesInSector);
// Compress the file sector, if needed
if(pFileEntry->dwFlags & MPQ_FILE_COMPRESS_MASK)
{
int nOutBuffer = (int)dwBytesInSector;
int nInBuffer = (int)dwBytesInSector;
// If the file is compressed, allocate buffer for the compressed data.
// Note that we allocate buffer that is a bit longer than sector size,
// for case if the compression method performs a buffer overrun
if(pbCompressed == NULL)
{
pbToWrite = pbCompressed = STORM_ALLOC(BYTE, hf->dwSectorSize + 0x100);
if(pbCompressed == NULL)
{
dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
break;
}
}
//
// Note that both SCompImplode and SCompCompress copy data as-is,
// if they are unable to compress the data.
//
if(pFileEntry->dwFlags & MPQ_FILE_IMPLODE)
{
SCompImplode(pbCompressed, &nOutBuffer, hf->pbFileSector, nInBuffer);
}
if(pFileEntry->dwFlags & MPQ_FILE_COMPRESS)
{
// If this is the first sector, we need to override the given compression
// by the first sector compression. This is because the entire sector must
// be compressed by the same compression.
//
// Test case:
//
// WRITE_FILE(hFile, pvBuffer, 0x10, MPQ_COMPRESSION_PKWARE) // Write 0x10 bytes (sector 0)
// WRITE_FILE(hFile, pvBuffer, 0x10, MPQ_COMPRESSION_ADPCM_MONO) // Write 0x10 bytes (still sector 0)
// WRITE_FILE(hFile, pvBuffer, 0x10, MPQ_COMPRESSION_ADPCM_MONO) // Write 0x10 bytes (still sector 0)
// WRITE_FILE(hFile, pvBuffer, 0x10, MPQ_COMPRESSION_ADPCM_MONO) // Write 0x10 bytes (still sector 0)
dwCompression = (dwSectorIndex == 0) ? hf->dwCompression0 : dwCompression;
// If the caller wants ADPCM compression, we will set wave compression level to 4,
// which corresponds to medium quality
nCompressionLevel = (dwCompression & MPQ_LOSSY_COMPRESSION_MASK) ? 4 : -1;
SCompCompress(pbCompressed, &nOutBuffer, hf->pbFileSector, nInBuffer, (unsigned)dwCompression, 0, nCompressionLevel);
}
// Update sector positions
dwBytesInSector = nOutBuffer;
if(hf->SectorOffsets != NULL)
hf->SectorOffsets[dwSectorIndex+1] = hf->SectorOffsets[dwSectorIndex] + dwBytesInSector;
// We have to calculate sector CRC, if enabled
if(hf->SectorChksums != NULL)
hf->SectorChksums[dwSectorIndex] = adler32(0, pbCompressed, nOutBuffer);
}
// Encrypt the sector, if necessary
if(pFileEntry->dwFlags & MPQ_FILE_ENCRYPTED)
{
BSWAP_ARRAY32_UNSIGNED(pbToWrite, dwBytesInSector);
EncryptMpqBlock(pbToWrite, dwBytesInSector, hf->dwFileKey + dwSectorIndex);
BSWAP_ARRAY32_UNSIGNED(pbToWrite, dwBytesInSector);
}
// Write the file sector
if(!FileStream_Write(ha->pStream, &ByteOffset, pbToWrite, dwBytesInSector))
{
dwErrCode = GetLastError();
break;
}
// Call the compact callback, if any
if(ha->pfnAddFileCB != NULL)
ha->pfnAddFileCB(ha->pvAddFileUserData, hf->dwFilePos, hf->dwDataSize, false);
// Update the compressed file size
pFileEntry->dwCmpSize += dwBytesInSector;
dwBytesInSector = 0;
dwSectorIndex++;
}
}
}
// Cleanup
if(pbCompressed != NULL)
STORM_FREE(pbCompressed);
return dwErrCode;
}
//-----------------------------------------------------------------------------
// Recrypts file data for file renaming
static DWORD RecryptFileData(
TMPQArchive * ha,
TMPQFile * hf,
const char * szFileName,
const char * szNewFileName)
{
ULONGLONG RawFilePos;
TFileEntry * pFileEntry = hf->pFileEntry;
DWORD dwBytesToRecrypt = pFileEntry->dwCmpSize;
DWORD dwOldKey;
DWORD dwNewKey;
DWORD dwErrCode = ERROR_SUCCESS;
// The file must be encrypted
assert(pFileEntry->dwFlags & MPQ_FILE_ENCRYPTED);
// File decryption key is calculated from the plain name
szNewFileName = GetPlainFileName(szNewFileName);
szFileName = GetPlainFileName(szFileName);
// Calculate both file keys
dwOldKey = DecryptFileKey(szFileName, pFileEntry->ByteOffset, pFileEntry->dwFileSize, pFileEntry->dwFlags);
dwNewKey = DecryptFileKey(szNewFileName, pFileEntry->ByteOffset, pFileEntry->dwFileSize, pFileEntry->dwFlags);
// Incase the keys are equal, don't recrypt the file
if(dwNewKey == dwOldKey)
return ERROR_SUCCESS;
hf->dwFileKey = dwOldKey;
// Calculate the raw position of the file in the archive
hf->MpqFilePos = pFileEntry->ByteOffset;
hf->RawFilePos = ha->MpqPos + hf->MpqFilePos;
// Allocate buffer for file transfer
dwErrCode = AllocateSectorBuffer(hf);
if(dwErrCode != ERROR_SUCCESS)
return dwErrCode;
// Also allocate buffer for sector offsets
// Note: Don't load sector checksums, we don't need to recrypt them
dwErrCode = AllocateSectorOffsets(hf, true);
if(dwErrCode != ERROR_SUCCESS)
return dwErrCode;
// If we have sector offsets, recrypt these as well
if(hf->SectorOffsets != NULL)
{
// Allocate secondary buffer for sectors copy
DWORD * SectorOffsetsCopy = STORM_ALLOC(DWORD, hf->SectorOffsets[0] / sizeof(DWORD));
DWORD dwSectorOffsLen = hf->SectorOffsets[0];
if(SectorOffsetsCopy == NULL)
return ERROR_NOT_ENOUGH_MEMORY;
// Recrypt the array of sector offsets
memcpy(SectorOffsetsCopy, hf->SectorOffsets, dwSectorOffsLen);
EncryptMpqBlock(SectorOffsetsCopy, dwSectorOffsLen, dwNewKey - 1);
BSWAP_ARRAY32_UNSIGNED(SectorOffsetsCopy, dwSectorOffsLen);
// Write the recrypted array back
if(!FileStream_Write(ha->pStream, &hf->RawFilePos, SectorOffsetsCopy, dwSectorOffsLen))
dwErrCode = GetLastError();
STORM_FREE(SectorOffsetsCopy);
}
// Now we have to recrypt all file sectors. We do it without
// recompression, because recompression is not necessary in this case
if(dwErrCode == ERROR_SUCCESS)
{
for(DWORD dwSector = 0; dwSector < hf->dwSectorCount; dwSector++)
{
DWORD dwRawDataInSector = hf->dwSectorSize;
DWORD dwRawByteOffset = dwSector * hf->dwSectorSize;
// Last sector: If there is not enough bytes remaining in the file, cut the raw size
if(dwRawDataInSector > dwBytesToRecrypt)
dwRawDataInSector = dwBytesToRecrypt;
// Fix the raw data length if the file is compressed
if(hf->SectorOffsets != NULL)
{
dwRawDataInSector = hf->SectorOffsets[dwSector+1] - hf->SectorOffsets[dwSector];
dwRawByteOffset = hf->SectorOffsets[dwSector];
}
// Calculate the raw file offset of the file sector
RawFilePos = CalculateRawSectorOffset(hf, dwRawByteOffset);
// Read the file sector
if(!FileStream_Read(ha->pStream, &RawFilePos, hf->pbFileSector, dwRawDataInSector))
{
dwErrCode = GetLastError();
break;
}
// If necessary, re-encrypt the sector
// Note: Recompression is not necessary here. Unlike encryption,
// the compression does not depend on the position of the file in MPQ.
BSWAP_ARRAY32_UNSIGNED(hf->pbFileSector, dwRawDataInSector);
DecryptMpqBlock(hf->pbFileSector, dwRawDataInSector, dwOldKey + dwSector);
EncryptMpqBlock(hf->pbFileSector, dwRawDataInSector, dwNewKey + dwSector);
BSWAP_ARRAY32_UNSIGNED(hf->pbFileSector, dwRawDataInSector);
// Write the sector back
if(!FileStream_Write(ha->pStream, &RawFilePos, hf->pbFileSector, dwRawDataInSector))
{
dwErrCode = GetLastError();
break;
}
// Decrement number of bytes remaining
dwBytesToRecrypt -= hf->dwSectorSize;
}
}
return dwErrCode;
}
//-----------------------------------------------------------------------------
// Internal support for MPQ modifications
DWORD SFileAddFile_Init(
TMPQArchive * ha,
const char * szFileName,
ULONGLONG FileTime,
DWORD dwFileSize,
LCID lcLocale,
DWORD dwFlags,
TMPQFile ** phf)
{
TFileEntry * pFileEntry = NULL;
TMPQFile * hf = NULL; // File structure for newly added file
DWORD dwHashIndex = HASH_ENTRY_FREE;
DWORD dwErrCode = ERROR_SUCCESS;
//
// Note: This is an internal function so no validity checks are done.
// It is the caller's responsibility to make sure that no invalid
// flags get to this point
//
// Sestor CRC is not allowed with single unit files
if(dwFlags & MPQ_FILE_SINGLE_UNIT)
dwFlags &= ~MPQ_FILE_SECTOR_CRC;
// Sector CRC is not allowed if the file is not compressed
if(!(dwFlags & MPQ_FILE_COMPRESS_MASK))
dwFlags &= ~MPQ_FILE_SECTOR_CRC;
// Fix Key is not allowed if the file is not enrypted
if(!(dwFlags & MPQ_FILE_ENCRYPTED))
dwFlags &= ~MPQ_FILE_FIX_KEY;
// If the MPQ is of version 3.0 or higher, we ignore file locale.
// This is because HET and BET tables have no known support for it
if(ha->pHeader->wFormatVersion >= MPQ_FORMAT_VERSION_3)
lcLocale = 0;
// Allocate the TMPQFile entry for newly added file
hf = CreateWritableHandle(ha, dwFileSize);
if(hf == NULL)
return false;
// Allocate file entry in the MPQ
if(dwErrCode == ERROR_SUCCESS)
{
// Check if the file already exists in the archive
pFileEntry = GetFileEntryExact(ha, szFileName, lcLocale, &dwHashIndex);
if(pFileEntry != NULL)
{
if(dwFlags & MPQ_FILE_REPLACEEXISTING)
InvalidateInternalFiles(ha);
else
dwErrCode = ERROR_ALREADY_EXISTS;
}
else
{
// Attempt to allocate new file entry
pFileEntry = AllocateFileEntry(ha, szFileName, lcLocale, &dwHashIndex);
if(pFileEntry != NULL)
InvalidateInternalFiles(ha);
else
dwErrCode = ERROR_DISK_FULL;
}
// Set the file entry to the file structure
hf->pFileEntry = pFileEntry;
}
// Prepare the pointer to hash table entry
if(dwErrCode == ERROR_SUCCESS && ha->pHashTable != NULL && dwHashIndex < ha->pHeader->dwHashTableSize)
{
hf->pHashEntry = ha->pHashTable + dwHashIndex;
hf->pHashEntry->lcLocale = (USHORT)lcLocale;
}
// Prepare the file key
if(dwErrCode == ERROR_SUCCESS && (dwFlags & MPQ_FILE_ENCRYPTED))
{
hf->dwFileKey = DecryptFileKey(szFileName, hf->MpqFilePos, dwFileSize, dwFlags);
if(hf->dwFileKey == 0)
dwErrCode = ERROR_UNKNOWN_FILE_KEY;
}
// Fill the file entry and TMPQFile structure
if(dwErrCode == ERROR_SUCCESS)
{
// At this point, the file name in the file entry must be set
assert(pFileEntry->szFileName != NULL);
assert(_stricmp(pFileEntry->szFileName, szFileName) == 0);
dwErrCode = FillWritableHandle(ha, hf, FileTime, dwFileSize, dwFlags);
}
// Free the file handle if failed
if(dwErrCode != ERROR_SUCCESS && hf != NULL)
FreeFileHandle(hf);
// Give the handle to the caller
*phf = hf;
return dwErrCode;
}
DWORD SFileAddFile_Init(
TMPQArchive * ha,
TMPQFile * hfSrc,
TMPQFile ** phf)
{
TFileEntry * pFileEntry = NULL;
TMPQFile * hf = NULL; // File structure for newly added file
ULONGLONG FileTime = hfSrc->pFileEntry->FileTime;
DWORD dwFileSize = hfSrc->pFileEntry->dwFileSize;
DWORD dwFlags = hfSrc->pFileEntry->dwFlags;
DWORD dwErrCode = ERROR_SUCCESS;
// Allocate the TMPQFile entry for newly added file
hf = CreateWritableHandle(ha, dwFileSize);
if(hf == NULL)
dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
// We need to keep the file entry index the same like in the source archive
// This is because multiple hash table entries can point to the same file entry
if(dwErrCode == ERROR_SUCCESS)
{
// Retrieve the file entry for the target file
pFileEntry = ha->pFileTable + (hfSrc->pFileEntry - hfSrc->ha->pFileTable);
// Copy all variables except file name
if((pFileEntry->dwFlags & MPQ_FILE_EXISTS) == 0)
{
pFileEntry[0] = hfSrc->pFileEntry[0];
pFileEntry->szFileName = NULL;
}
else
dwErrCode = ERROR_ALREADY_EXISTS;
// Set the file entry to the file structure
hf->pFileEntry = pFileEntry;
}
// Prepare the pointer to hash table entry
if(dwErrCode == ERROR_SUCCESS && ha->pHashTable != NULL && hfSrc->pHashEntry != NULL)
{
hf->dwHashIndex = (DWORD)(hfSrc->pHashEntry - hfSrc->ha->pHashTable);
hf->pHashEntry = ha->pHashTable + hf->dwHashIndex;
}
// Prepare the file key (copy from source file)
if(dwErrCode == ERROR_SUCCESS && (dwFlags & MPQ_FILE_ENCRYPTED))
{
hf->dwFileKey = hfSrc->dwFileKey;
if(hf->dwFileKey == 0)
dwErrCode = ERROR_UNKNOWN_FILE_KEY;
}
// Fill the file entry and TMPQFile structure
if(dwErrCode == ERROR_SUCCESS)
{
dwErrCode = FillWritableHandle(ha, hf, FileTime, dwFileSize, dwFlags);
}
// Free the file handle if failed
if(dwErrCode != ERROR_SUCCESS && hf != NULL)
FreeFileHandle(hf);
// Give the handle to the caller
*phf = hf;
return dwErrCode;
}
DWORD SFileAddFile_Write(TMPQFile * hf, const void * pvData, DWORD dwSize, DWORD dwCompression)
{
TMPQArchive * ha;
TFileEntry * pFileEntry;
DWORD dwErrCode = ERROR_SUCCESS;
// Don't bother if the caller gave us zero size
if(pvData == NULL || dwSize == 0)
return ERROR_SUCCESS;
// Get pointer to the MPQ archive
pFileEntry = hf->pFileEntry;
ha = hf->ha;
// Allocate file buffers
if(hf->pbFileSector == NULL)
{
ULONGLONG RawFilePos = hf->RawFilePos;
// Allocate buffer for file sector
hf->dwAddFileError = dwErrCode = AllocateSectorBuffer(hf);
if(dwErrCode != ERROR_SUCCESS)
return dwErrCode;
// Allocate patch info, if the data is patch
if(hf->pPatchInfo == NULL && IsIncrementalPatchFile(pvData, dwSize, &hf->dwPatchedFileSize))
{
// Set the MPQ_FILE_PATCH_FILE flag
pFileEntry->dwFlags |= MPQ_FILE_PATCH_FILE;
// Allocate the patch info
hf->dwAddFileError = dwErrCode = AllocatePatchInfo(hf, false);
if(dwErrCode != ERROR_SUCCESS)
return dwErrCode;
}
// Allocate sector offsets
if(hf->SectorOffsets == NULL)
{
hf->dwAddFileError = dwErrCode = AllocateSectorOffsets(hf, false);
if(dwErrCode != ERROR_SUCCESS)
return dwErrCode;
}
// Create array of sector checksums
if(hf->SectorChksums == NULL && (pFileEntry->dwFlags & MPQ_FILE_SECTOR_CRC))
{
hf->dwAddFileError = dwErrCode = AllocateSectorChecksums(hf, false);
if(dwErrCode != ERROR_SUCCESS)
return dwErrCode;
}
// Pre-save the patch info, if any
if(hf->pPatchInfo != NULL)
{
if(!FileStream_Write(ha->pStream, &RawFilePos, hf->pPatchInfo, hf->pPatchInfo->dwLength))
dwErrCode = GetLastError();
pFileEntry->dwCmpSize += hf->pPatchInfo->dwLength;
RawFilePos += hf->pPatchInfo->dwLength;
}
// Pre-save the sector offset table, just to reserve space in the file.
// Note that we dont need to swap the sector positions, nor encrypt the table
// at the moment, as it will be written again after writing all file sectors.
if(hf->SectorOffsets != NULL)
{
if(!FileStream_Write(ha->pStream, &RawFilePos, hf->SectorOffsets, hf->SectorOffsets[0]))
dwErrCode = GetLastError();
pFileEntry->dwCmpSize += hf->SectorOffsets[0];
RawFilePos += hf->SectorOffsets[0];
}
}
// Write the MPQ data to the file
if(dwErrCode == ERROR_SUCCESS)
{
// Save the first sector compression to the file structure
// Note that the entire first file sector will be compressed
// by compression that was passed to the first call of SFileAddFile_Write
if(hf->dwFilePos == 0)
hf->dwCompression0 = dwCompression;
// Write the data to the MPQ
dwErrCode = WriteDataToMpqFile(ha, hf, (LPBYTE)pvData, dwSize, dwCompression);
}
// If it succeeded and we wrote all the file data,
// we need to re-save sector offset table
if(dwErrCode == ERROR_SUCCESS)
{
if(hf->dwFilePos >= pFileEntry->dwFileSize)
{
// Finish calculating CRC32
pFileEntry->dwCrc32 = hf->dwCrc32;
// Finish calculating MD5
md5_done((hash_state *)hf->hctx, pFileEntry->md5);
// If we also have sector checksums, write them to the file
if(hf->SectorChksums != NULL)
{
dwErrCode = WriteSectorChecksums(hf);
}
// Now write patch info
if(hf->pPatchInfo != NULL)
{
memcpy(hf->pPatchInfo->md5, pFileEntry->md5, MD5_DIGEST_SIZE);
hf->pPatchInfo->dwDataSize = pFileEntry->dwFileSize;
pFileEntry->dwFileSize = hf->dwPatchedFileSize;
dwErrCode = WritePatchInfo(hf);
}
// Now write sector offsets to the file
if(hf->SectorOffsets != NULL)
{
dwErrCode = WriteSectorOffsets(hf);
}
// Write the MD5 hashes of each file chunk, if required
if(ha->pHeader->dwRawChunkSize != 0)
{
dwErrCode = WriteMpqDataMD5(ha->pStream,
ha->MpqPos + pFileEntry->ByteOffset,
hf->pFileEntry->dwCmpSize,
ha->pHeader->dwRawChunkSize);
}
}
}
// Update the archive size
if((ha->MpqPos + pFileEntry->ByteOffset + pFileEntry->dwCmpSize) > ha->FileSize)
ha->FileSize = ha->MpqPos + pFileEntry->ByteOffset + pFileEntry->dwCmpSize;
// Store the error code from the Write File operation
hf->dwAddFileError = dwErrCode;
return dwErrCode;
}
DWORD SFileAddFile_Finish(TMPQFile * hf)
{
TMPQArchive * ha = hf->ha;
TFileEntry * pFileEntry = hf->pFileEntry;
DWORD dwErrCode = hf->dwAddFileError;
// If all previous operations succeeded, we can update the MPQ
if(dwErrCode == ERROR_SUCCESS)
{
// Verify if the caller wrote the file properly
if(hf->pPatchInfo == NULL)
{
assert(pFileEntry != NULL);
if(hf->dwFilePos != pFileEntry->dwFileSize)
dwErrCode = ERROR_CAN_NOT_COMPLETE;
}
else
{
if(hf->dwFilePos != hf->pPatchInfo->dwDataSize)
dwErrCode = ERROR_CAN_NOT_COMPLETE;
}
}
// Now we need to recreate the HET table, if exists
if(dwErrCode == ERROR_SUCCESS && ha->pHetTable != NULL)
{
dwErrCode = RebuildHetTable(ha);
}
// Update the block table size
if(dwErrCode == ERROR_SUCCESS)
{
// Call the user callback, if any
if(ha->pfnAddFileCB != NULL)
ha->pfnAddFileCB(ha->pvAddFileUserData, hf->dwDataSize, hf->dwDataSize, true);
}
else
{
// Free the file entry in MPQ tables
if(pFileEntry != NULL)
DeleteFileEntry(ha, hf);
}
// Clear the add file callback
FreeFileHandle(hf);
return dwErrCode;
}
//-----------------------------------------------------------------------------
// Adds data as file to the archive
bool WINAPI SFileCreateFile(
HANDLE hMpq,
const char * szArchivedName,
ULONGLONG FileTime,
DWORD dwFileSize,
LCID lcLocale,
DWORD dwFlags,
HANDLE * phFile)
{
TMPQArchive * ha = (TMPQArchive *)hMpq;
DWORD dwErrCode = ERROR_SUCCESS;
// Check valid parameters
if(!IsValidMpqHandle(hMpq))
dwErrCode = ERROR_INVALID_HANDLE;
if(szArchivedName == NULL || *szArchivedName == 0)
dwErrCode = ERROR_INVALID_PARAMETER;
if(phFile == NULL)
dwErrCode = ERROR_INVALID_PARAMETER;
// Don't allow to add file if the MPQ is open for read only
if(dwErrCode == ERROR_SUCCESS)
{
if(ha->dwFlags & MPQ_FLAG_READ_ONLY)
dwErrCode = ERROR_ACCESS_DENIED;
// Don't allow to add a file under pseudo-file name
if(IsPseudoFileName(szArchivedName, NULL))
dwErrCode = ERROR_INVALID_PARAMETER;
// Don't allow to add any of the internal files
if(IsInternalMpqFileName(szArchivedName))
dwErrCode = ERROR_INTERNAL_FILE;
}
// Perform validity check of the MPQ flags
if(dwErrCode == ERROR_SUCCESS)
{
// Mask all unsupported flags out
dwFlags &= (ha->dwFlags & MPQ_FLAG_WAR3_MAP) ? MPQ_FILE_VALID_FLAGS_W3X : MPQ_FILE_VALID_FLAGS;
// Check for valid flag combinations
if((dwFlags & (MPQ_FILE_IMPLODE | MPQ_FILE_COMPRESS)) == (MPQ_FILE_IMPLODE | MPQ_FILE_COMPRESS))
dwErrCode = ERROR_INVALID_PARAMETER;
}
// Initiate the add file operation
if(dwErrCode == ERROR_SUCCESS)
dwErrCode = SFileAddFile_Init(ha, szArchivedName, FileTime, dwFileSize, lcLocale, dwFlags, (TMPQFile **)phFile);
// Deal with the errors
if(dwErrCode != ERROR_SUCCESS)
SetLastError(dwErrCode);
return (dwErrCode == ERROR_SUCCESS);
}
bool WINAPI SFileWriteFile(
HANDLE hFile,
const void * pvData,
DWORD dwSize,
DWORD dwCompression)
{
TMPQFile * hf = (TMPQFile *)hFile;
DWORD dwErrCode = ERROR_SUCCESS;
// Check the proper parameters
if(!IsValidFileHandle(hFile))
dwErrCode = ERROR_INVALID_HANDLE;
if(hf->bIsWriteHandle == false)
dwErrCode = ERROR_INVALID_HANDLE;
// Special checks for single unit files
if(dwErrCode == ERROR_SUCCESS && (hf->pFileEntry->dwFlags & MPQ_FILE_SINGLE_UNIT))
{
//
// Note: Blizzard doesn't support single unit files
// that are stored as encrypted or imploded. We will allow them here,
// the calling application must ensure that such flag combination doesn't get here
//
// if(dwFlags & MPQ_FILE_IMPLODE)
// dwErrCode = ERROR_INVALID_PARAMETER;
//
// if(dwFlags & MPQ_FILE_ENCRYPTED)
// dwErrCode = ERROR_INVALID_PARAMETER;
// Lossy compression is not allowed on single unit files
if(dwCompression & MPQ_LOSSY_COMPRESSION_MASK)
dwErrCode = ERROR_INVALID_PARAMETER;
}
// Write the data to the file
if(dwErrCode == ERROR_SUCCESS)
dwErrCode = SFileAddFile_Write(hf, pvData, dwSize, dwCompression);
// Deal with errors
if(dwErrCode != ERROR_SUCCESS)
SetLastError(dwErrCode);
return (dwErrCode == ERROR_SUCCESS);
}
bool WINAPI SFileFinishFile(HANDLE hFile)
{
TMPQFile * hf = (TMPQFile *)hFile;
DWORD dwErrCode = ERROR_SUCCESS;
// Check the proper parameters
if(!IsValidFileHandle(hFile))
dwErrCode = ERROR_INVALID_HANDLE;
if(hf->bIsWriteHandle == false)
dwErrCode = ERROR_INVALID_HANDLE;
// Finish the file
if(dwErrCode == ERROR_SUCCESS)
dwErrCode = SFileAddFile_Finish(hf);
// Deal with errors
if(dwErrCode != ERROR_SUCCESS)
SetLastError(dwErrCode);
return (dwErrCode == ERROR_SUCCESS);
}
//-----------------------------------------------------------------------------
// Adds a file to the archive
bool WINAPI SFileAddFileEx(
HANDLE hMpq,
const TCHAR * szFileName,
const char * szArchivedName,
DWORD dwFlags,
DWORD dwCompression, // Compression of the first sector
DWORD dwCompressionNext) // Compression of next sectors
{
ULONGLONG FileSize = 0;
ULONGLONG FileTime = 0;
TFileStream * pStream = NULL;
HANDLE hMpqFile = NULL;
LPBYTE pbFileData = NULL;
DWORD dwBytesRemaining = 0;
DWORD dwBytesToRead;
DWORD dwSectorSize = 0x1000;
DWORD dwChannels = 0;
bool bIsAdpcmCompression = false;
bool bIsFirstSector = true;
DWORD dwErrCode = ERROR_SUCCESS;
// Check parameters
if(hMpq == NULL || szFileName == NULL || *szFileName == 0)
{
SetLastError(ERROR_INVALID_PARAMETER);
return false;
}
// Open added file
pStream = FileStream_OpenFile(szFileName, STREAM_FLAG_READ_ONLY | STREAM_PROVIDER_FLAT | BASE_PROVIDER_FILE);
if(pStream == NULL)
return false;
// Files bigger than 4GB cannot be added to MPQ
FileStream_GetTime(pStream, &FileTime);
FileStream_GetSize(pStream, &FileSize);
if(FileSize >> 32)
dwErrCode = ERROR_DISK_FULL;
// Allocate data buffer for reading from the source file
if(dwErrCode == ERROR_SUCCESS)
{
dwBytesRemaining = (DWORD)FileSize;
pbFileData = STORM_ALLOC(BYTE, dwSectorSize);
if(pbFileData == NULL)
dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
}
// Deal with various combination of compressions
if(dwErrCode == ERROR_SUCCESS)
{
// When the compression for next blocks is set to default,
// we will copy the compression for the first sector
if(dwCompressionNext == MPQ_COMPRESSION_NEXT_SAME)
dwCompressionNext = dwCompression;
// If the caller wants ADPCM compression, we make sure
// that the first sector is not compressed with lossy compression
if(dwCompressionNext & (MPQ_COMPRESSION_ADPCM_MONO | MPQ_COMPRESSION_ADPCM_STEREO))
{
// The compression of the first file sector must not be ADPCM
// in order not to corrupt the headers
if(dwCompression & (MPQ_COMPRESSION_ADPCM_MONO | MPQ_COMPRESSION_ADPCM_STEREO))
dwCompression = MPQ_COMPRESSION_PKWARE;
// Remove both flag mono and stereo flags.
// They will be re-added according to WAVE type
dwCompressionNext &= ~(MPQ_COMPRESSION_ADPCM_MONO | MPQ_COMPRESSION_ADPCM_STEREO);
bIsAdpcmCompression = true;
}
// Initiate adding file to the MPQ
if(!SFileCreateFile(hMpq, szArchivedName, FileTime, (DWORD)FileSize, g_lcFileLocale, dwFlags, &hMpqFile))
dwErrCode = GetLastError();
}
// Write the file data to the MPQ
while(dwErrCode == ERROR_SUCCESS && dwBytesRemaining != 0)
{
// Get the number of bytes remaining in the source file
dwBytesToRead = dwBytesRemaining;
if(dwBytesToRead > dwSectorSize)
dwBytesToRead = dwSectorSize;
// Read data from the local file
if(!FileStream_Read(pStream, NULL, pbFileData, dwBytesToRead))
{
dwErrCode = GetLastError();
break;
}
// If the file being added is a WAVE file, we check number of channels
if(bIsFirstSector && bIsAdpcmCompression)
{
// The file must really be a WAVE file with at least 16 bits per sample,
// otherwise the ADPCM compression will corrupt it
if(IsWaveFile_16BitsPerAdpcmSample(pbFileData, dwBytesToRead, &dwChannels))
{
// Setup the compression of next sectors according to number of channels
dwCompressionNext |= (dwChannels == 1) ? MPQ_COMPRESSION_ADPCM_MONO : MPQ_COMPRESSION_ADPCM_STEREO;
}
else
{
// Setup the compression of next sectors to a lossless compression
dwCompressionNext = (dwCompression & MPQ_LOSSY_COMPRESSION_MASK) ? MPQ_COMPRESSION_PKWARE : dwCompression;
}
bIsFirstSector = false;
}
// Add the file sectors to the MPQ
if(!SFileWriteFile(hMpqFile, pbFileData, dwBytesToRead, dwCompression))
{
dwErrCode = GetLastError();
break;
}
// Set the next data compression
dwBytesRemaining -= dwBytesToRead;
dwCompression = dwCompressionNext;
}
// Finish the file writing
if(hMpqFile != NULL)
{
if(!SFileFinishFile(hMpqFile))
dwErrCode = GetLastError();
}
// Cleanup and exit
if(pbFileData != NULL)
STORM_FREE(pbFileData);
if(pStream != NULL)
FileStream_Close(pStream);
if(dwErrCode != ERROR_SUCCESS)
SetLastError(dwErrCode);
return (dwErrCode == ERROR_SUCCESS);
}
// Adds a data file into the archive
bool WINAPI SFileAddFile(HANDLE hMpq, const TCHAR * szFileName, const char * szArchivedName, DWORD dwFlags)
{
return SFileAddFileEx(hMpq,
szFileName,
szArchivedName,
dwFlags,
DefaultDataCompression,
DefaultDataCompression);
}
// Adds a WAVE file into the archive
bool WINAPI SFileAddWave(HANDLE hMpq, const TCHAR * szFileName, const char * szArchivedName, DWORD dwFlags, DWORD dwQuality)
{
DWORD dwCompression = 0;
//
// Note to wave compression level:
// The following conversion table applied:
// High quality: WaveCompressionLevel = -1
// Medium quality: WaveCompressionLevel = 4
// Low quality: WaveCompressionLevel = 2
//
// Starcraft files are packed as Mono (0x41) on medium quality.
// Because this compression is not used anymore, our compression functions
// will default to WaveCompressionLevel = 4 when using ADPCM compression
//
// Convert quality to data compression
switch(dwQuality)
{
case MPQ_WAVE_QUALITY_HIGH:
// WaveCompressionLevel = -1;
dwCompression = MPQ_COMPRESSION_PKWARE;
break;
case MPQ_WAVE_QUALITY_MEDIUM:
// WaveCompressionLevel = 4;
dwCompression = MPQ_COMPRESSION_ADPCM_STEREO | MPQ_COMPRESSION_HUFFMANN;
break;
case MPQ_WAVE_QUALITY_LOW:
// WaveCompressionLevel = 2;
dwCompression = MPQ_COMPRESSION_ADPCM_STEREO | MPQ_COMPRESSION_HUFFMANN;
break;
}
return SFileAddFileEx(hMpq,
szFileName,
szArchivedName,
dwFlags,
MPQ_COMPRESSION_PKWARE, // First sector should be compressed as data
dwCompression); // Next sectors should be compressed as WAVE
}
//-----------------------------------------------------------------------------
// bool SFileRemoveFile(HANDLE hMpq, char * szFileName)
//
// This function removes a file from the archive.
//
bool WINAPI SFileRemoveFile(HANDLE hMpq, const char * szFileName, DWORD dwSearchScope)
{
TMPQArchive * ha = IsValidMpqHandle(hMpq);
TMPQFile * hf = NULL;
DWORD dwErrCode = ERROR_SUCCESS;
// Keep compiler happy
dwSearchScope = dwSearchScope;
// Check the parameters
if(ha == NULL)
dwErrCode = ERROR_INVALID_HANDLE;
if(szFileName == NULL || *szFileName == 0)
dwErrCode = ERROR_INVALID_PARAMETER;
if(IsInternalMpqFileName(szFileName))
dwErrCode = ERROR_INTERNAL_FILE;
// Do not allow to remove files from read-only or patched MPQs
if(dwErrCode == ERROR_SUCCESS)
{
if((ha->dwFlags & MPQ_FLAG_READ_ONLY) || (ha->haPatch != NULL))
dwErrCode = ERROR_ACCESS_DENIED;
}
// If all checks have passed, we can delete the file from the MPQ
if(dwErrCode == ERROR_SUCCESS)
{
// Open the file from the MPQ
if(SFileOpenFileEx(hMpq, szFileName, SFILE_OPEN_BASE_FILE, (HANDLE *)&hf))
{
// Delete the file entry
dwErrCode = DeleteFileEntry(ha, hf);
FreeFileHandle(hf);
}
else
dwErrCode = GetLastError();
}
// If the file has been deleted, we need to invalidate
// the internal files and recreate HET table
if(dwErrCode == ERROR_SUCCESS)
{
// Invalidate the entries for internal files
// After we are done with MPQ changes, we need to re-create them anyway
InvalidateInternalFiles(ha);
//
// Don't rebuild HET table now; the file's flags indicate
// that it's been deleted, which is enough
//
}
// Resolve error and exit
if(dwErrCode != ERROR_SUCCESS)
SetLastError(dwErrCode);
return (dwErrCode == ERROR_SUCCESS);
}
// Renames the file within the archive.
bool WINAPI SFileRenameFile(HANDLE hMpq, const char * szFileName, const char * szNewFileName)
{
TMPQArchive * ha = IsValidMpqHandle(hMpq);
TMPQFile * hf;
DWORD dwErrCode = ERROR_SUCCESS;
// Test the valid parameters
if(ha == NULL)
dwErrCode = ERROR_INVALID_HANDLE;
if(szFileName == NULL || *szFileName == 0 || szNewFileName == NULL || *szNewFileName == 0)
dwErrCode = ERROR_INVALID_PARAMETER;
if(IsInternalMpqFileName(szFileName) || IsInternalMpqFileName(szNewFileName))
dwErrCode = ERROR_INTERNAL_FILE;
// Do not allow to rename files in MPQ open for read only
if(dwErrCode == ERROR_SUCCESS)
{
if(ha->dwFlags & MPQ_FLAG_READ_ONLY)
dwErrCode = ERROR_ACCESS_DENIED;
}
// Open the new file. If exists, we don't allow rename operation
if(dwErrCode == ERROR_SUCCESS)
{
if(GetFileEntryLocale(ha, szNewFileName, g_lcFileLocale) != NULL)
dwErrCode = ERROR_ALREADY_EXISTS;
}
// Open the file from the MPQ
if(dwErrCode == ERROR_SUCCESS)
{
// Attempt to open the file
if(SFileOpenFileEx(hMpq, szFileName, SFILE_OPEN_BASE_FILE, (HANDLE *)&hf))
{
ULONGLONG RawDataOffs;
TFileEntry * pFileEntry = hf->pFileEntry;
// Invalidate the entries for internal files
InvalidateInternalFiles(ha);
// Rename the file entry in the table
dwErrCode = RenameFileEntry(ha, hf, szNewFileName);
// If the file is encrypted, we have to re-crypt the file content
// with the new decryption key
if((dwErrCode == ERROR_SUCCESS) && (pFileEntry->dwFlags & MPQ_FILE_ENCRYPTED))
{
// Recrypt the file data in the MPQ
dwErrCode = RecryptFileData(ha, hf, szFileName, szNewFileName);
// Update the MD5 of the raw block
if(dwErrCode == ERROR_SUCCESS && ha->pHeader->dwRawChunkSize != 0)
{
RawDataOffs = ha->MpqPos + pFileEntry->ByteOffset;
WriteMpqDataMD5(ha->pStream,
RawDataOffs,
pFileEntry->dwCmpSize,
ha->pHeader->dwRawChunkSize);
}
}
// Free the file handle
FreeFileHandle(hf);
}
else
{
dwErrCode = GetLastError();
}
}
// We also need to rebuild the HET table, if present
if(dwErrCode == ERROR_SUCCESS && ha->pHetTable != NULL)
dwErrCode = RebuildHetTable(ha);
// Resolve error and exit
if(dwErrCode != ERROR_SUCCESS)
SetLastError(dwErrCode);
return (dwErrCode == ERROR_SUCCESS);
}
//-----------------------------------------------------------------------------
// Sets default data compression for SFileAddFile
bool WINAPI SFileSetDataCompression(DWORD DataCompression)
{
unsigned int uValidMask = (MPQ_COMPRESSION_ZLIB | MPQ_COMPRESSION_PKWARE | MPQ_COMPRESSION_BZIP2 | MPQ_COMPRESSION_SPARSE);
if((DataCompression & uValidMask) != DataCompression)
{
SetLastError(ERROR_INVALID_PARAMETER);
return false;
}
DefaultDataCompression = DataCompression;
return true;
}
//-----------------------------------------------------------------------------
// Changes locale ID of a file
bool WINAPI SFileSetFileLocale(HANDLE hFile, LCID lcNewLocale)
{
TMPQArchive * ha;
TFileEntry * pFileEntry;
TMPQFile * hf = IsValidFileHandle(hFile);
// Invalid handle => do nothing
if(hf == NULL)
{
SetLastError(ERROR_INVALID_HANDLE);
return false;
}
// Do not allow to rename files in MPQ open for read only
ha = hf->ha;
if(ha->dwFlags & MPQ_FLAG_READ_ONLY)
{
SetLastError(ERROR_ACCESS_DENIED);
return false;
}
// Do not allow unnamed access
if(hf->pFileEntry->szFileName == NULL)
{
SetLastError(ERROR_CAN_NOT_COMPLETE);
return false;
}
// Do not allow to change locale of any internal file
if(IsInternalMpqFileName(hf->pFileEntry->szFileName))
{
SetLastError(ERROR_INTERNAL_FILE);
return false;
}
// Do not allow changing file locales if there is no hash table
if(hf->pHashEntry == NULL)
{
SetLastError(ERROR_NOT_SUPPORTED);
return false;
}
// We have to check if the file+locale is not already there
pFileEntry = GetFileEntryExact(ha, hf->pFileEntry->szFileName, lcNewLocale, NULL);
if(pFileEntry != NULL)
{
SetLastError(ERROR_ALREADY_EXISTS);
return false;
}
// Update the locale in the hash table entry
hf->pHashEntry->lcLocale = (USHORT)lcNewLocale;
ha->dwFlags |= MPQ_FLAG_CHANGED;
return true;
}
//-----------------------------------------------------------------------------
// Sets add file callback
bool WINAPI SFileSetAddFileCallback(HANDLE hMpq, SFILE_ADDFILE_CALLBACK AddFileCB, void * pvUserData)
{
TMPQArchive * ha = (TMPQArchive *) hMpq;
if(!IsValidMpqHandle(hMpq))
{
SetLastError(ERROR_INVALID_HANDLE);
return false;
}
ha->pvAddFileUserData = pvUserData;
ha->pfnAddFileCB = AddFileCB;
return true;
}