diff options
| author | Nicholas Estelami <NEstelami@users.noreply.github.com> | 2022-06-01 16:05:05 -0400 |
|---|---|---|
| committer | Nicholas Estelami <NEstelami@users.noreply.github.com> | 2022-06-16 20:35:52 -0400 |
| commit | b3e299dbdee829c12ada556a1c3aa1f109a0e05b (patch) | |
| tree | a6e647da673ba7df9389cb1f382a11fc8350bc23 /StormLib/src/SFileAddFile.cpp | |
| parent | 941c19f2c33a0d1af75a10a64fe0905387d1e440 (diff) | |
Replaced StormLib .lib files with source code.
Some modifications to handle backslashes and forward slashes, along with some optimizations to speed up OTR generation.
Diffstat (limited to 'StormLib/src/SFileAddFile.cpp')
| -rw-r--r-- | StormLib/src/SFileAddFile.cpp | 1316 |
1 files changed, 1316 insertions, 0 deletions
diff --git a/StormLib/src/SFileAddFile.cpp b/StormLib/src/SFileAddFile.cpp new file mode 100644 index 000000000..1f7156e99 --- /dev/null +++ b/StormLib/src/SFileAddFile.cpp @@ -0,0 +1,1316 @@ +/*****************************************************************************/ +/* 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; +} |
