From a8a92ccc92f786f56c0c306152de2b7bde53cc45 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 28 May 2015 17:24:56 +0200 Subject: + Fixed bug in SFileReadFile --- LICENSE | 42 +- src/FileStream.cpp | 5780 ++++++++++++++-------------- src/SBaseCommon.cpp | 3548 ++++++++--------- src/SFileAddFile.cpp | 2626 ++++++------- src/SFileAttributes.cpp | 1140 +++--- src/SFileCompactArchive.cpp | 1308 +++---- src/SFileFindFile.cpp | 960 +++-- src/SFileGetFileInfo.cpp | 1984 +++++----- src/SFileListFile.cpp | 1284 +++---- src/SFilePatchArchives.cpp | 1874 ++++----- src/SFileReadFile.cpp | 4 +- src/SFileVerify.cpp | 2108 +++++----- src/StormCommon.h | 744 ++-- src/StormPort.h | 580 +-- test/StormTest.cpp | 8878 +++++++++++++++++++++---------------------- test/TLogHelper.cpp | 842 ++-- 16 files changed, 16847 insertions(+), 16855 deletions(-) diff --git a/LICENSE b/LICENSE index 136cae4..2cb432d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -The MIT License (MIT) - -Copyright (c) 1999-2013 Ladislav Zezula - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +The MIT License (MIT) + +Copyright (c) 1999-2013 Ladislav Zezula + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/FileStream.cpp b/src/FileStream.cpp index a367db6..29a4d33 100644 --- a/src/FileStream.cpp +++ b/src/FileStream.cpp @@ -1,2890 +1,2890 @@ -/*****************************************************************************/ -/* FileStream.cpp Copyright (c) Ladislav Zezula 2010 */ -/*---------------------------------------------------------------------------*/ -/* File stream support for StormLib */ -/* */ -/* Windows support: Written by Ladislav Zezula */ -/* Mac support: Written by Sam Wilkins */ -/* Linux support: Written by Sam Wilkins and Ivan Komissarov */ -/* Big-endian: Written & debugged by Sam Wilkins */ -/*---------------------------------------------------------------------------*/ -/* Date Ver Who Comment */ -/* -------- ---- --- ------- */ -/* 11.06.10 1.00 Lad Derived from StormPortMac.cpp and StormPortLinux.cpp */ -/*****************************************************************************/ - -#define __STORMLIB_SELF__ -#include "StormLib.h" -#include "StormCommon.h" -#include "FileStream.h" - -#ifdef _MSC_VER -#pragma comment(lib, "wininet.lib") // Internet functions for HTTP stream -#pragma warning(disable: 4800) // 'BOOL' : forcing value to bool 'true' or 'false' (performance warning) -#endif - -//----------------------------------------------------------------------------- -// Local defines - -#ifndef INVALID_HANDLE_VALUE -#define INVALID_HANDLE_VALUE ((HANDLE)-1) -#endif - -//----------------------------------------------------------------------------- -// Local functions - platform-specific functions - -#ifndef PLATFORM_WINDOWS -static int nLastError = ERROR_SUCCESS; - -int GetLastError() -{ - return nLastError; -} - -void SetLastError(int nError) -{ - nLastError = nError; -} -#endif - -static DWORD StringToInt(const char * szString) -{ - DWORD dwValue = 0; - - while('0' <= szString[0] && szString[0] <= '9') - { - dwValue = (dwValue * 10) + (szString[0] - '9'); - szString++; - } - - return dwValue; -} - -//----------------------------------------------------------------------------- -// Dummy init function - -static void BaseNone_Init(TFileStream *) -{ - // Nothing here -} - -//----------------------------------------------------------------------------- -// Local functions - base file support - -static bool BaseFile_Create(TFileStream * pStream) -{ -#ifdef PLATFORM_WINDOWS - { - DWORD dwWriteShare = (pStream->dwFlags & STREAM_FLAG_WRITE_SHARE) ? FILE_SHARE_WRITE : 0; - - pStream->Base.File.hFile = CreateFile(pStream->szFileName, - GENERIC_READ | GENERIC_WRITE, - dwWriteShare | FILE_SHARE_READ, - NULL, - CREATE_ALWAYS, - 0, - NULL); - if(pStream->Base.File.hFile == INVALID_HANDLE_VALUE) - return false; - } -#endif - -#if defined(PLATFORM_MAC) || defined(PLATFORM_LINUX) - { - intptr_t handle; - - handle = open(pStream->szFileName, O_RDWR | O_CREAT | O_TRUNC | O_LARGEFILE, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); - if(handle == -1) - { - nLastError = errno; - return false; - } - - pStream->Base.File.hFile = (HANDLE)handle; - } -#endif - - // Reset the file size and position - pStream->Base.File.FileSize = 0; - pStream->Base.File.FilePos = 0; - return true; -} - -static bool BaseFile_Open(TFileStream * pStream, const TCHAR * szFileName, DWORD dwStreamFlags) -{ -#ifdef PLATFORM_WINDOWS - { - ULARGE_INTEGER FileSize; - DWORD dwWriteAccess = (dwStreamFlags & STREAM_FLAG_READ_ONLY) ? 0 : FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_WRITE_ATTRIBUTES; - DWORD dwWriteShare = (dwStreamFlags & STREAM_FLAG_WRITE_SHARE) ? FILE_SHARE_WRITE : 0; - - // Open the file - pStream->Base.File.hFile = CreateFile(szFileName, - FILE_READ_DATA | FILE_READ_ATTRIBUTES | dwWriteAccess, - FILE_SHARE_READ | dwWriteShare, - NULL, - OPEN_EXISTING, - 0, - NULL); - if(pStream->Base.File.hFile == INVALID_HANDLE_VALUE) - return false; - - // Query the file size - FileSize.LowPart = GetFileSize(pStream->Base.File.hFile, &FileSize.HighPart); - pStream->Base.File.FileSize = FileSize.QuadPart; - - // Query last write time - GetFileTime(pStream->Base.File.hFile, NULL, NULL, (LPFILETIME)&pStream->Base.File.FileTime); - } -#endif - -#if defined(PLATFORM_MAC) || defined(PLATFORM_LINUX) - { - struct stat64 fileinfo; - int oflag = (dwStreamFlags & STREAM_FLAG_READ_ONLY) ? O_RDONLY : O_RDWR; - intptr_t handle; - - // Open the file - handle = open(szFileName, oflag | O_LARGEFILE); - if(handle == -1) - { - nLastError = errno; - return false; - } - - // Get the file size - if(fstat64(handle, &fileinfo) == -1) - { - nLastError = errno; - return false; - } - - // time_t is number of seconds since 1.1.1970, UTC. - // 1 second = 10000000 (decimal) in FILETIME - // Set the start to 1.1.1970 00:00:00 - pStream->Base.File.FileTime = 0x019DB1DED53E8000ULL + (10000000 * fileinfo.st_mtime); - pStream->Base.File.FileSize = (ULONGLONG)fileinfo.st_size; - pStream->Base.File.hFile = (HANDLE)handle; - } -#endif - - // Reset the file position - pStream->Base.File.FilePos = 0; - return true; -} - -static bool BaseFile_Read( - TFileStream * pStream, // Pointer to an open stream - ULONGLONG * pByteOffset, // Pointer to file byte offset. If NULL, it reads from the current position - void * pvBuffer, // Pointer to data to be read - DWORD dwBytesToRead) // Number of bytes to read from the file -{ - ULONGLONG ByteOffset = (pByteOffset != NULL) ? *pByteOffset : pStream->Base.File.FilePos; - DWORD dwBytesRead = 0; // Must be set by platform-specific code - -#ifdef PLATFORM_WINDOWS - { - // Note: StormLib no longer supports Windows 9x. - // Thus, we can use the OVERLAPPED structure to specify - // file offset to read from file. This allows us to skip - // one system call to SetFilePointer - - // Update the byte offset - pStream->Base.File.FilePos = ByteOffset; - - // Read the data - if(dwBytesToRead != 0) - { - OVERLAPPED Overlapped; - - Overlapped.OffsetHigh = (DWORD)(ByteOffset >> 32); - Overlapped.Offset = (DWORD)ByteOffset; - Overlapped.hEvent = NULL; - if(!ReadFile(pStream->Base.File.hFile, pvBuffer, dwBytesToRead, &dwBytesRead, &Overlapped)) - return false; - } - } -#endif - -#if defined(PLATFORM_MAC) || defined(PLATFORM_LINUX) - { - ssize_t bytes_read; - - // If the byte offset is different from the current file position, - // we have to update the file position xxx - if(ByteOffset != pStream->Base.File.FilePos) - { - lseek64((intptr_t)pStream->Base.File.hFile, (off64_t)(ByteOffset), SEEK_SET); - pStream->Base.File.FilePos = ByteOffset; - } - - // Perform the read operation - if(dwBytesToRead != 0) - { - bytes_read = read((intptr_t)pStream->Base.File.hFile, pvBuffer, (size_t)dwBytesToRead); - if(bytes_read == -1) - { - nLastError = errno; - return false; - } - - dwBytesRead = (DWORD)(size_t)bytes_read; - } - } -#endif - - // Increment the current file position by number of bytes read - // If the number of bytes read doesn't match to required amount, return false - pStream->Base.File.FilePos = ByteOffset + dwBytesRead; - if(dwBytesRead != dwBytesToRead) - SetLastError(ERROR_HANDLE_EOF); - return (dwBytesRead == dwBytesToRead); -} - -/** - * \a pStream Pointer to an open stream - * \a pByteOffset Pointer to file byte offset. If NULL, writes to current position - * \a pvBuffer Pointer to data to be written - * \a dwBytesToWrite Number of bytes to write to the file - */ - -static bool BaseFile_Write(TFileStream * pStream, ULONGLONG * pByteOffset, const void * pvBuffer, DWORD dwBytesToWrite) -{ - ULONGLONG ByteOffset = (pByteOffset != NULL) ? *pByteOffset : pStream->Base.File.FilePos; - DWORD dwBytesWritten = 0; // Must be set by platform-specific code - -#ifdef PLATFORM_WINDOWS - { - // Note: StormLib no longer supports Windows 9x. - // Thus, we can use the OVERLAPPED structure to specify - // file offset to read from file. This allows us to skip - // one system call to SetFilePointer - - // Update the byte offset - pStream->Base.File.FilePos = ByteOffset; - - // Read the data - if(dwBytesToWrite != 0) - { - OVERLAPPED Overlapped; - - Overlapped.OffsetHigh = (DWORD)(ByteOffset >> 32); - Overlapped.Offset = (DWORD)ByteOffset; - Overlapped.hEvent = NULL; - if(!WriteFile(pStream->Base.File.hFile, pvBuffer, dwBytesToWrite, &dwBytesWritten, &Overlapped)) - return false; - } - } -#endif - -#if defined(PLATFORM_MAC) || defined(PLATFORM_LINUX) - { - ssize_t bytes_written; - - // If the byte offset is different from the current file position, - // we have to update the file position - if(ByteOffset != pStream->Base.File.FilePos) - { - lseek64((intptr_t)pStream->Base.File.hFile, (off64_t)(ByteOffset), SEEK_SET); - pStream->Base.File.FilePos = ByteOffset; - } - - // Perform the read operation - bytes_written = write((intptr_t)pStream->Base.File.hFile, pvBuffer, (size_t)dwBytesToWrite); - if(bytes_written == -1) - { - nLastError = errno; - return false; - } - - dwBytesWritten = (DWORD)(size_t)bytes_written; - } -#endif - - // Increment the current file position by number of bytes read - pStream->Base.File.FilePos = ByteOffset + dwBytesWritten; - - // Also modify the file size, if needed - if(pStream->Base.File.FilePos > pStream->Base.File.FileSize) - pStream->Base.File.FileSize = pStream->Base.File.FilePos; - - if(dwBytesWritten != dwBytesToWrite) - SetLastError(ERROR_DISK_FULL); - return (dwBytesWritten == dwBytesToWrite); -} - -/** - * \a pStream Pointer to an open stream - * \a NewFileSize New size of the file - */ -static bool BaseFile_Resize(TFileStream * pStream, ULONGLONG NewFileSize) -{ -#ifdef PLATFORM_WINDOWS - { - LONG FileSizeHi = (LONG)(NewFileSize >> 32); - LONG FileSizeLo; - DWORD dwNewPos; - bool bResult; - - // Set the position at the new file size - dwNewPos = SetFilePointer(pStream->Base.File.hFile, (LONG)NewFileSize, &FileSizeHi, FILE_BEGIN); - if(dwNewPos == INVALID_SET_FILE_POINTER && GetLastError() != ERROR_SUCCESS) - return false; - - // Set the current file pointer as the end of the file - bResult = (bool)SetEndOfFile(pStream->Base.File.hFile); - if(bResult) - pStream->Base.File.FileSize = NewFileSize; - - // Restore the file position - FileSizeHi = (LONG)(pStream->Base.File.FilePos >> 32); - FileSizeLo = (LONG)(pStream->Base.File.FilePos); - SetFilePointer(pStream->Base.File.hFile, FileSizeLo, &FileSizeHi, FILE_BEGIN); - return bResult; - } -#endif - -#if defined(PLATFORM_MAC) || defined(PLATFORM_LINUX) - { - if(ftruncate64((intptr_t)pStream->Base.File.hFile, (off64_t)NewFileSize) == -1) - { - nLastError = errno; - return false; - } - - pStream->Base.File.FileSize = NewFileSize; - return true; - } -#endif -} - -// Gives the current file size -static bool BaseFile_GetSize(TFileStream * pStream, ULONGLONG * pFileSize) -{ - // Note: Used by all thre base providers. - // Requires the TBaseData union to have the same layout for all three base providers - *pFileSize = pStream->Base.File.FileSize; - return true; -} - -// Gives the current file position -static bool BaseFile_GetPos(TFileStream * pStream, ULONGLONG * pByteOffset) -{ - // Note: Used by all thre base providers. - // Requires the TBaseData union to have the same layout for all three base providers - *pByteOffset = pStream->Base.File.FilePos; - return true; -} - -// Renames the file pointed by pStream so that it contains data from pNewStream -static bool BaseFile_Replace(TFileStream * pStream, TFileStream * pNewStream) -{ -#ifdef PLATFORM_WINDOWS - // Delete the original stream file. Don't check the result value, - // because if the file doesn't exist, it would fail - DeleteFile(pStream->szFileName); - - // Rename the new file to the old stream's file - return (bool)MoveFile(pNewStream->szFileName, pStream->szFileName); -#endif - -#if defined(PLATFORM_MAC) || defined(PLATFORM_LINUX) - // "rename" on Linux also works if the target file exists - if(rename(pNewStream->szFileName, pStream->szFileName) == -1) - { - nLastError = errno; - return false; - } - - return true; -#endif -} - -static void BaseFile_Close(TFileStream * pStream) -{ - if(pStream->Base.File.hFile != INVALID_HANDLE_VALUE) - { -#ifdef PLATFORM_WINDOWS - CloseHandle(pStream->Base.File.hFile); -#endif - -#if defined(PLATFORM_MAC) || defined(PLATFORM_LINUX) - close((intptr_t)pStream->Base.File.hFile); -#endif - } - - // Also invalidate the handle - pStream->Base.File.hFile = INVALID_HANDLE_VALUE; -} - -// Initializes base functions for the disk file -static void BaseFile_Init(TFileStream * pStream) -{ - pStream->BaseCreate = BaseFile_Create; - pStream->BaseOpen = BaseFile_Open; - pStream->BaseRead = BaseFile_Read; - pStream->BaseWrite = BaseFile_Write; - pStream->BaseResize = BaseFile_Resize; - pStream->BaseGetSize = BaseFile_GetSize; - pStream->BaseGetPos = BaseFile_GetPos; - pStream->BaseClose = BaseFile_Close; -} - -//----------------------------------------------------------------------------- -// Local functions - base memory-mapped file support - -static bool BaseMap_Open(TFileStream * pStream, const TCHAR * szFileName, DWORD dwStreamFlags) -{ -#ifdef PLATFORM_WINDOWS - - ULARGE_INTEGER FileSize; - HANDLE hFile; - HANDLE hMap; - bool bResult = false; - - // Keep compiler happy - dwStreamFlags = dwStreamFlags; - - // Open the file for read access - hFile = CreateFile(szFileName, FILE_READ_DATA, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); - if(hFile != INVALID_HANDLE_VALUE) - { - // Retrieve file size. Don't allow mapping file of a zero size. - FileSize.LowPart = GetFileSize(hFile, &FileSize.HighPart); - if(FileSize.QuadPart != 0) - { - // Now create mapping object - hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL); - if(hMap != NULL) - { - // Map the entire view into memory - // Note that this operation will fail if the file can't fit - // into usermode address space - pStream->Base.Map.pbFile = (LPBYTE)MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0); - if(pStream->Base.Map.pbFile != NULL) - { - // Retrieve file time - GetFileTime(hFile, NULL, NULL, (LPFILETIME)&pStream->Base.Map.FileTime); - - // Retrieve file size and position - pStream->Base.Map.FileSize = FileSize.QuadPart; - pStream->Base.Map.FilePos = 0; - bResult = true; - } - - // Close the map handle - CloseHandle(hMap); - } - } - - // Close the file handle - CloseHandle(hFile); - } - - // If the file is not there and is not available for random access, - // report error - if(bResult == false) - return false; -#endif - -#if defined(PLATFORM_MAC) || defined(PLATFORM_LINUX) - struct stat64 fileinfo; - intptr_t handle; - bool bResult = false; - - // Open the file - handle = open(szFileName, O_RDONLY); - if(handle != -1) - { - // Get the file size - if(fstat64(handle, &fileinfo) != -1) - { - pStream->Base.Map.pbFile = (LPBYTE)mmap(NULL, (size_t)fileinfo.st_size, PROT_READ, MAP_PRIVATE, handle, 0); - if(pStream->Base.Map.pbFile != NULL) - { - // time_t is number of seconds since 1.1.1970, UTC. - // 1 second = 10000000 (decimal) in FILETIME - // Set the start to 1.1.1970 00:00:00 - pStream->Base.Map.FileTime = 0x019DB1DED53E8000ULL + (10000000 * fileinfo.st_mtime); - pStream->Base.Map.FileSize = (ULONGLONG)fileinfo.st_size; - pStream->Base.Map.FilePos = 0; - bResult = true; - } - } - close(handle); - } - - // Did the mapping fail? - if(bResult == false) - { - nLastError = errno; - return false; - } -#endif - - return true; -} - -static bool BaseMap_Read( - TFileStream * pStream, // Pointer to an open stream - ULONGLONG * pByteOffset, // Pointer to file byte offset. If NULL, it reads from the current position - void * pvBuffer, // Pointer to data to be read - DWORD dwBytesToRead) // Number of bytes to read from the file -{ - ULONGLONG ByteOffset = (pByteOffset != NULL) ? *pByteOffset : pStream->Base.Map.FilePos; - - // Do we have to read anything at all? - if(dwBytesToRead != 0) - { - // Don't allow reading past file size - if((ByteOffset + dwBytesToRead) > pStream->Base.Map.FileSize) - return false; - - // Copy the required data - memcpy(pvBuffer, pStream->Base.Map.pbFile + (size_t)ByteOffset, dwBytesToRead); - } - - // Move the current file position - pStream->Base.Map.FilePos += dwBytesToRead; - return true; -} - -static void BaseMap_Close(TFileStream * pStream) -{ -#ifdef PLATFORM_WINDOWS - if(pStream->Base.Map.pbFile != NULL) - UnmapViewOfFile(pStream->Base.Map.pbFile); -#endif - -#if defined(PLATFORM_MAC) || defined(PLATFORM_LINUX) - if(pStream->Base.Map.pbFile != NULL) - munmap(pStream->Base.Map.pbFile, (size_t )pStream->Base.Map.FileSize); -#endif - - pStream->Base.Map.pbFile = NULL; -} - -// Initializes base functions for the mapped file -static void BaseMap_Init(TFileStream * pStream) -{ - // Supply the file stream functions - pStream->BaseOpen = BaseMap_Open; - pStream->BaseRead = BaseMap_Read; - pStream->BaseGetSize = BaseFile_GetSize; // Reuse BaseFile function - pStream->BaseGetPos = BaseFile_GetPos; // Reuse BaseFile function - pStream->BaseClose = BaseMap_Close; - - // Mapped files are read-only - pStream->dwFlags |= STREAM_FLAG_READ_ONLY; -} - -//----------------------------------------------------------------------------- -// Local functions - base HTTP file support - -static const TCHAR * BaseHttp_ExtractServerName(const TCHAR * szFileName, TCHAR * szServerName) -{ - // Check for HTTP - if(!_tcsnicmp(szFileName, _T("http://"), 7)) - szFileName += 7; - - // Cut off the server name - if(szServerName != NULL) - { - while(szFileName[0] != 0 && szFileName[0] != _T('/')) - *szServerName++ = *szFileName++; - *szServerName = 0; - } - else - { - while(szFileName[0] != 0 && szFileName[0] != _T('/')) - szFileName++; - } - - // Return the remainder - return szFileName; -} - -static bool BaseHttp_Open(TFileStream * pStream, const TCHAR * szFileName, DWORD dwStreamFlags) -{ -#ifdef PLATFORM_WINDOWS - - HINTERNET hRequest; - DWORD dwTemp = 0; - bool bFileAvailable = false; - int nError = ERROR_SUCCESS; - - // Keep compiler happy - dwStreamFlags = dwStreamFlags; - - // Don't connect to the internet - if(!InternetGetConnectedState(&dwTemp, 0)) - return false; - - // Initiate the connection to the internet - pStream->Base.Http.hInternet = InternetOpen(_T("StormLib HTTP MPQ reader"), - INTERNET_OPEN_TYPE_PRECONFIG, - NULL, - NULL, - 0); - if(pStream->Base.Http.hInternet == NULL) - return false; - - // Connect to the server - if(nError == ERROR_SUCCESS) - { - TCHAR szServerName[MAX_PATH]; - DWORD dwFlags = INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_NO_UI | INTERNET_FLAG_NO_CACHE_WRITE; - - // Initiate connection with the server - szFileName = BaseHttp_ExtractServerName(szFileName, szServerName); - pStream->Base.Http.hConnect = InternetConnect(pStream->Base.Http.hInternet, - szServerName, - INTERNET_DEFAULT_HTTP_PORT, - NULL, - NULL, - INTERNET_SERVICE_HTTP, - dwFlags, - 0); - if(pStream->Base.Http.hConnect == NULL) - { - InternetCloseHandle(pStream->Base.Http.hInternet); - return false; - } - } - - // Now try to query the file size - if(nError == ERROR_SUCCESS) - { - // Open HTTP request to the file - hRequest = HttpOpenRequest(pStream->Base.Http.hConnect, _T("GET"), szFileName, NULL, NULL, NULL, INTERNET_FLAG_NO_CACHE_WRITE, 0); - if(hRequest != NULL) - { - if(HttpSendRequest(hRequest, NULL, 0, NULL, 0)) - { - ULONGLONG FileTime = 0; - DWORD dwFileSize = 0; - DWORD dwDataSize; - DWORD dwIndex = 0; - - // Check if the MPQ has Last Modified field - dwDataSize = sizeof(ULONGLONG); - if(HttpQueryInfo(hRequest, HTTP_QUERY_LAST_MODIFIED | HTTP_QUERY_FLAG_SYSTEMTIME, &FileTime, &dwDataSize, &dwIndex)) - pStream->Base.Http.FileTime = FileTime; - - // Verify if the server supports random access - dwDataSize = sizeof(DWORD); - if(HttpQueryInfo(hRequest, HTTP_QUERY_CONTENT_LENGTH | HTTP_QUERY_FLAG_NUMBER, &dwFileSize, &dwDataSize, &dwIndex)) - { - if(dwFileSize != 0) - { - pStream->Base.Http.FileSize = dwFileSize; - pStream->Base.Http.FilePos = 0; - bFileAvailable = true; - } - } - } - InternetCloseHandle(hRequest); - } - } - - // If the file is not there and is not available for random access, - // report error - if(bFileAvailable == false) - { - pStream->BaseClose(pStream); - return false; - } - - return true; - -#else - - // Not supported - SetLastError(ERROR_NOT_SUPPORTED); - pStream = pStream; - return false; - -#endif -} - -static bool BaseHttp_Read( - TFileStream * pStream, // Pointer to an open stream - ULONGLONG * pByteOffset, // Pointer to file byte offset. If NULL, it reads from the current position - void * pvBuffer, // Pointer to data to be read - DWORD dwBytesToRead) // Number of bytes to read from the file -{ -#ifdef PLATFORM_WINDOWS - ULONGLONG ByteOffset = (pByteOffset != NULL) ? *pByteOffset : pStream->Base.Http.FilePos; - DWORD dwTotalBytesRead = 0; - - // Do we have to read anything at all? - if(dwBytesToRead != 0) - { - HINTERNET hRequest; - LPCTSTR szFileName; - LPBYTE pbBuffer = (LPBYTE)pvBuffer; - TCHAR szRangeRequest[0x80]; - DWORD dwStartOffset = (DWORD)ByteOffset; - DWORD dwEndOffset = dwStartOffset + dwBytesToRead; - - // Open HTTP request to the file - szFileName = BaseHttp_ExtractServerName(pStream->szFileName, NULL); - hRequest = HttpOpenRequest(pStream->Base.Http.hConnect, _T("GET"), szFileName, NULL, NULL, NULL, INTERNET_FLAG_NO_CACHE_WRITE, 0); - if(hRequest != NULL) - { - // Add range request to the HTTP headers - // http://www.clevercomponents.com/articles/article015/resuming.asp - _stprintf(szRangeRequest, _T("Range: bytes=%u-%u"), (unsigned int)dwStartOffset, (unsigned int)dwEndOffset); - HttpAddRequestHeaders(hRequest, szRangeRequest, 0xFFFFFFFF, HTTP_ADDREQ_FLAG_ADD_IF_NEW); - - // Send the request to the server - if(HttpSendRequest(hRequest, NULL, 0, NULL, 0)) - { - while(dwTotalBytesRead < dwBytesToRead) - { - DWORD dwBlockBytesToRead = dwBytesToRead - dwTotalBytesRead; - DWORD dwBlockBytesRead = 0; - - // Read the block from the file - if(dwBlockBytesToRead > 0x200) - dwBlockBytesToRead = 0x200; - InternetReadFile(hRequest, pbBuffer, dwBlockBytesToRead, &dwBlockBytesRead); - - // Check for end - if(dwBlockBytesRead == 0) - break; - - // Move buffers - dwTotalBytesRead += dwBlockBytesRead; - pbBuffer += dwBlockBytesRead; - } - } - InternetCloseHandle(hRequest); - } - } - - // Increment the current file position by number of bytes read - pStream->Base.Http.FilePos = ByteOffset + dwTotalBytesRead; - - // If the number of bytes read doesn't match the required amount, return false - if(dwTotalBytesRead != dwBytesToRead) - SetLastError(ERROR_HANDLE_EOF); - return (dwTotalBytesRead == dwBytesToRead); - -#else - - // Not supported - pStream = pStream; - pByteOffset = pByteOffset; - pvBuffer = pvBuffer; - dwBytesToRead = dwBytesToRead; - SetLastError(ERROR_NOT_SUPPORTED); - return false; - -#endif -} - -static void BaseHttp_Close(TFileStream * pStream) -{ -#ifdef PLATFORM_WINDOWS - if(pStream->Base.Http.hConnect != NULL) - InternetCloseHandle(pStream->Base.Http.hConnect); - pStream->Base.Http.hConnect = NULL; - - if(pStream->Base.Http.hInternet != NULL) - InternetCloseHandle(pStream->Base.Http.hInternet); - pStream->Base.Http.hInternet = NULL; -#else - pStream = pStream; -#endif -} - -// Initializes base functions for the mapped file -static void BaseHttp_Init(TFileStream * pStream) -{ - // Supply the stream functions - pStream->BaseOpen = BaseHttp_Open; - pStream->BaseRead = BaseHttp_Read; - pStream->BaseGetSize = BaseFile_GetSize; // Reuse BaseFile function - pStream->BaseGetPos = BaseFile_GetPos; // Reuse BaseFile function - pStream->BaseClose = BaseHttp_Close; - - // HTTP files are read-only - pStream->dwFlags |= STREAM_FLAG_READ_ONLY; -} - -//----------------------------------------------------------------------------- -// Local functions - base block-based support - -// Generic function that loads blocks from the file -// The function groups the block with the same availability, -// so the called BlockRead can finish the request in a single system call -static bool BlockStream_Read( - TBlockStream * pStream, // Pointer to an open stream - ULONGLONG * pByteOffset, // Pointer to file byte offset. If NULL, it reads from the current position - void * pvBuffer, // Pointer to data to be read - DWORD dwBytesToRead) // Number of bytes to read from the file -{ - ULONGLONG BlockOffset0; - ULONGLONG BlockOffset; - ULONGLONG ByteOffset; - ULONGLONG EndOffset; - LPBYTE TransferBuffer; - LPBYTE BlockBuffer; - DWORD BlockBufferOffset; // Offset of the desired data in the block buffer - DWORD BytesNeeded; // Number of bytes that really need to be read - DWORD BlockSize = pStream->BlockSize; - DWORD BlockCount; - bool bPrevBlockAvailable; - bool bCallbackCalled = false; - bool bBlockAvailable; - bool bResult = true; - - // The base block read function must be present - assert(pStream->BlockRead != NULL); - - // NOP reading of zero bytes - if(dwBytesToRead == 0) - return true; - - // Get the current position in the stream - ByteOffset = (pByteOffset != NULL) ? pByteOffset[0] : pStream->StreamPos; - EndOffset = ByteOffset + dwBytesToRead; - if(EndOffset > pStream->StreamSize) - { - SetLastError(ERROR_HANDLE_EOF); - return false; - } - - // Calculate the block parameters - BlockOffset0 = BlockOffset = ByteOffset & ~((ULONGLONG)BlockSize - 1); - BlockCount = (DWORD)(((EndOffset - BlockOffset) + (BlockSize - 1)) / BlockSize); - BytesNeeded = (DWORD)(EndOffset - BlockOffset); - - // Remember where we have our data - assert((BlockSize & (BlockSize - 1)) == 0); - BlockBufferOffset = (DWORD)(ByteOffset & (BlockSize - 1)); - - // Allocate buffer for reading blocks - TransferBuffer = BlockBuffer = STORM_ALLOC(BYTE, (BlockCount * BlockSize)); - if(TransferBuffer == NULL) - { - SetLastError(ERROR_NOT_ENOUGH_MEMORY); - return false; - } - - // If all blocks are available, just read all blocks at once - if(pStream->IsComplete == 0) - { - // Now parse the blocks and send the block read request - // to all blocks with the same availability - assert(pStream->BlockCheck != NULL); - bPrevBlockAvailable = pStream->BlockCheck(pStream, BlockOffset); - - // Loop as long as we have something to read - while(BlockOffset < EndOffset) - { - // Determine availability of the next block - bBlockAvailable = pStream->BlockCheck(pStream, BlockOffset); - - // If the availability has changed, read all blocks up to this one - if(bBlockAvailable != bPrevBlockAvailable) - { - // Call the file stream callback, if the block is not available - if(pStream->pMaster && pStream->pfnCallback && bPrevBlockAvailable == false) - { - pStream->pfnCallback(pStream->UserData, BlockOffset0, (DWORD)(BlockOffset - BlockOffset0)); - bCallbackCalled = true; - } - - // Load the continuous blocks with the same availability - assert(BlockOffset > BlockOffset0); - bResult = pStream->BlockRead(pStream, BlockOffset0, BlockOffset, BlockBuffer, BytesNeeded, bPrevBlockAvailable); - if(!bResult) - break; - - // Move the block offset - BlockBuffer += (DWORD)(BlockOffset - BlockOffset0); - BytesNeeded -= (DWORD)(BlockOffset - BlockOffset0); - bPrevBlockAvailable = bBlockAvailable; - BlockOffset0 = BlockOffset; - } - - // Move to the block offset in the stream - BlockOffset += BlockSize; - } - - // If there is a block(s) remaining to be read, do it - if(BlockOffset > BlockOffset0) - { - // Call the file stream callback, if the block is not available - if(pStream->pMaster && pStream->pfnCallback && bPrevBlockAvailable == false) - { - pStream->pfnCallback(pStream->UserData, BlockOffset0, (DWORD)(BlockOffset - BlockOffset0)); - bCallbackCalled = true; - } - - // Read the complete blocks from the file - if(BlockOffset > pStream->StreamSize) - BlockOffset = pStream->StreamSize; - bResult = pStream->BlockRead(pStream, BlockOffset0, BlockOffset, BlockBuffer, BytesNeeded, bPrevBlockAvailable); - } - } - else - { - // Read the complete blocks from the file - if(EndOffset > pStream->StreamSize) - EndOffset = pStream->StreamSize; - bResult = pStream->BlockRead(pStream, BlockOffset, EndOffset, BlockBuffer, BytesNeeded, true); - } - - // Now copy the data to the user buffer - if(bResult) - { - memcpy(pvBuffer, TransferBuffer + BlockBufferOffset, dwBytesToRead); - pStream->StreamPos = ByteOffset + dwBytesToRead; - } - else - { - // If the block read failed, set the last error - SetLastError(ERROR_FILE_INCOMPLETE); - } - - // Call the callback to indicate we are done - if(bCallbackCalled) - pStream->pfnCallback(pStream->UserData, 0, 0); - - // Free the block buffer and return - STORM_FREE(TransferBuffer); - return bResult; -} - -static bool BlockStream_GetSize(TFileStream * pStream, ULONGLONG * pFileSize) -{ - *pFileSize = pStream->StreamSize; - return true; -} - -static bool BlockStream_GetPos(TFileStream * pStream, ULONGLONG * pByteOffset) -{ - *pByteOffset = pStream->StreamPos; - return true; -} - -static void BlockStream_Close(TBlockStream * pStream) -{ - // Free the data map, if any - if(pStream->FileBitmap != NULL) - STORM_FREE(pStream->FileBitmap); - pStream->FileBitmap = NULL; - - // Call the base class for closing the stream - pStream->BaseClose(pStream); -} - -//----------------------------------------------------------------------------- -// File stream allocation function - -static STREAM_INIT StreamBaseInit[4] = -{ - BaseFile_Init, - BaseMap_Init, - BaseHttp_Init, - BaseNone_Init -}; - -// This function allocates an empty structure for the file stream -// The stream structure is created as flat block, variable length -// The file name is placed after the end of the stream structure data -static TFileStream * AllocateFileStream( - const TCHAR * szFileName, - size_t StreamSize, - DWORD dwStreamFlags) -{ - TFileStream * pMaster = NULL; - TFileStream * pStream; - const TCHAR * szNextFile = szFileName; - size_t FileNameSize; - - // Sanity check - assert(StreamSize != 0); - - // The caller can specify chain of files in the following form: - // C:\archive.MPQ*http://www.server.com/MPQs/archive-server.MPQ - // In that case, we use the part after "*" as master file name - while(szNextFile[0] != 0 && szNextFile[0] != _T('*')) - szNextFile++; - FileNameSize = (size_t)((szNextFile - szFileName) * sizeof(TCHAR)); - - // If we have a next file, we need to open it as master stream - // Note that we don't care if the master stream exists or not, - // If it doesn't, later attempts to read missing file block will fail - if(szNextFile[0] == _T('*')) - { - // Don't allow another master file in the string - if(_tcschr(szNextFile + 1, _T('*')) != NULL) - { - SetLastError(ERROR_INVALID_PARAMETER); - return NULL; - } - - // Open the master file - pMaster = FileStream_OpenFile(szNextFile + 1, STREAM_FLAG_READ_ONLY); - } - - // Allocate the stream structure for the given stream type - pStream = (TFileStream *)STORM_ALLOC(BYTE, StreamSize + FileNameSize + sizeof(TCHAR)); - if(pStream != NULL) - { - // Zero the entire structure - memset(pStream, 0, StreamSize); - pStream->pMaster = pMaster; - pStream->dwFlags = dwStreamFlags; - - // Initialize the file name - pStream->szFileName = (TCHAR *)((BYTE *)pStream + StreamSize); - memcpy(pStream->szFileName, szFileName, FileNameSize); - pStream->szFileName[FileNameSize / sizeof(TCHAR)] = 0; - - // Initialize the stream functions - StreamBaseInit[dwStreamFlags & 0x03](pStream); - } - - return pStream; -} - -//----------------------------------------------------------------------------- -// Local functions - flat stream support - -static DWORD FlatStream_CheckFile(TBlockStream * pStream) -{ - LPBYTE FileBitmap = (LPBYTE)pStream->FileBitmap; - DWORD WholeByteCount = (pStream->BlockCount / 8); - DWORD ExtraBitsCount = (pStream->BlockCount & 7); - BYTE ExpectedValue; - - // Verify the whole bytes - their value must be 0xFF - for(DWORD i = 0; i < WholeByteCount; i++) - { - if(FileBitmap[i] != 0xFF) - return 0; - } - - // If there are extra bits, calculate the mask - if(ExtraBitsCount != 0) - { - ExpectedValue = (BYTE)((1 << ExtraBitsCount) - 1); - if(FileBitmap[WholeByteCount] != ExpectedValue) - return 0; - } - - // Yes, the file is complete - return 1; -} - -static bool FlatStream_LoadBitmap(TBlockStream * pStream) -{ - FILE_BITMAP_FOOTER Footer; - ULONGLONG ByteOffset; - LPBYTE FileBitmap; - DWORD BlockCount; - DWORD BitmapSize; - - // Do not load the bitmap if we should not have to - if(!(pStream->dwFlags & STREAM_FLAG_USE_BITMAP)) - return false; - - // Only if the size is greater than size of bitmap footer - if(pStream->Base.File.FileSize > sizeof(FILE_BITMAP_FOOTER)) - { - // Load the bitmap footer - ByteOffset = pStream->Base.File.FileSize - sizeof(FILE_BITMAP_FOOTER); - if(pStream->BaseRead(pStream, &ByteOffset, &Footer, sizeof(FILE_BITMAP_FOOTER))) - { - // Make sure that the array is properly BSWAP-ed - BSWAP_ARRAY32_UNSIGNED((LPDWORD)(&Footer), sizeof(FILE_BITMAP_FOOTER)); - - // Verify if there is actually a footer - if(Footer.Signature == ID_FILE_BITMAP_FOOTER && Footer.Version == 0x03) - { - // Get the offset of the bitmap, number of blocks and size of the bitmap - ByteOffset = MAKE_OFFSET64(Footer.MapOffsetHi, Footer.MapOffsetLo); - BlockCount = (DWORD)(((ByteOffset - 1) / Footer.BlockSize) + 1); - BitmapSize = ((BlockCount + 7) / 8); - - // Check if the sizes match - if(ByteOffset + BitmapSize + sizeof(FILE_BITMAP_FOOTER) == pStream->Base.File.FileSize) - { - // Allocate space for the bitmap - FileBitmap = STORM_ALLOC(BYTE, BitmapSize); - if(FileBitmap != NULL) - { - // Load the bitmap bits - if(!pStream->BaseRead(pStream, &ByteOffset, FileBitmap, BitmapSize)) - { - STORM_FREE(FileBitmap); - return false; - } - - // Update the stream size - pStream->BuildNumber = Footer.BuildNumber; - pStream->StreamSize = ByteOffset; - - // Fill the bitmap information - pStream->FileBitmap = FileBitmap; - pStream->BitmapSize = BitmapSize; - pStream->BlockSize = Footer.BlockSize; - pStream->BlockCount = BlockCount; - pStream->IsComplete = FlatStream_CheckFile(pStream); - return true; - } - } - } - } - } - - return false; -} - -static void FlatStream_UpdateBitmap( - TBlockStream * pStream, // Pointer to an open stream - ULONGLONG StartOffset, - ULONGLONG EndOffset) -{ - LPBYTE FileBitmap = (LPBYTE)pStream->FileBitmap; - DWORD BlockIndex; - DWORD BlockSize = pStream->BlockSize; - DWORD ByteIndex; - BYTE BitMask; - - // Sanity checks - assert((StartOffset & (BlockSize - 1)) == 0); - assert(FileBitmap != NULL); - - // Calculate the index of the block - BlockIndex = (DWORD)(StartOffset / BlockSize); - ByteIndex = (BlockIndex / 0x08); - BitMask = (BYTE)(1 << (BlockIndex & 0x07)); - - // Set all bits for the specified range - while(StartOffset < EndOffset) - { - // Set the bit - FileBitmap[ByteIndex] |= BitMask; - - // Move all - StartOffset += BlockSize; - ByteIndex += (BitMask >> 0x07); - BitMask = (BitMask >> 0x07) | (BitMask << 0x01); - } - - // Increment the bitmap update count - pStream->IsModified = 1; -} - -static bool FlatStream_BlockCheck( - TBlockStream * pStream, // Pointer to an open stream - ULONGLONG BlockOffset) -{ - LPBYTE FileBitmap = (LPBYTE)pStream->FileBitmap; - DWORD BlockIndex; - BYTE BitMask; - - // Sanity checks - assert((BlockOffset & (pStream->BlockSize - 1)) == 0); - assert(FileBitmap != NULL); - - // Calculate the index of the block - BlockIndex = (DWORD)(BlockOffset / pStream->BlockSize); - BitMask = (BYTE)(1 << (BlockIndex & 0x07)); - - // Check if the bit is present - return (FileBitmap[BlockIndex / 0x08] & BitMask) ? true : false; -} - -static bool FlatStream_BlockRead( - TBlockStream * pStream, // Pointer to an open stream - ULONGLONG StartOffset, - ULONGLONG EndOffset, - LPBYTE BlockBuffer, - DWORD BytesNeeded, - bool bAvailable) -{ - DWORD BytesToRead = (DWORD)(EndOffset - StartOffset); - - // The starting offset must be aligned to size of the block - assert(pStream->FileBitmap != NULL); - assert((StartOffset & (pStream->BlockSize - 1)) == 0); - assert(StartOffset < EndOffset); - - // If the blocks are not available, we need to load them from the master - // and then save to the mirror - if(bAvailable == false) - { - // If we have no master, we cannot satisfy read request - if(pStream->pMaster == NULL) - return false; - - // Load the blocks from the master stream - // Note that we always have to read complete blocks - // so they get properly stored to the mirror stream - if(!FileStream_Read(pStream->pMaster, &StartOffset, BlockBuffer, BytesToRead)) - return false; - - // Store the loaded blocks to the mirror file. - // Note that this operation is not required to succeed - if(pStream->BaseWrite(pStream, &StartOffset, BlockBuffer, BytesToRead)) - FlatStream_UpdateBitmap(pStream, StartOffset, EndOffset); - - return true; - } - else - { - if(BytesToRead > BytesNeeded) - BytesToRead = BytesNeeded; - return pStream->BaseRead(pStream, &StartOffset, BlockBuffer, BytesToRead); - } -} - -static void FlatStream_Close(TBlockStream * pStream) -{ - FILE_BITMAP_FOOTER Footer; - - if(pStream->FileBitmap && pStream->IsModified) - { - // Write the file bitmap - pStream->BaseWrite(pStream, &pStream->StreamSize, pStream->FileBitmap, pStream->BitmapSize); - - // Prepare and write the file footer - Footer.Signature = ID_FILE_BITMAP_FOOTER; - Footer.Version = 3; - Footer.BuildNumber = pStream->BuildNumber; - Footer.MapOffsetLo = (DWORD)(pStream->StreamSize & 0xFFFFFFFF); - Footer.MapOffsetHi = (DWORD)(pStream->StreamSize >> 0x20); - Footer.BlockSize = pStream->BlockSize; - BSWAP_ARRAY32_UNSIGNED(&Footer, sizeof(FILE_BITMAP_FOOTER)); - pStream->BaseWrite(pStream, NULL, &Footer, sizeof(FILE_BITMAP_FOOTER)); - } - - // Close the base class - BlockStream_Close(pStream); -} - -static bool FlatStream_CreateMirror(TBlockStream * pStream) -{ - ULONGLONG MasterSize = 0; - ULONGLONG MirrorSize = 0; - LPBYTE FileBitmap = NULL; - DWORD dwBitmapSize; - DWORD dwBlockCount; - bool bNeedCreateMirrorStream = true; - bool bNeedResizeMirrorStream = true; - - // Do we have master function and base creation function? - if(pStream->pMaster == NULL || pStream->BaseCreate == NULL) - return false; - - // Retrieve the master file size, block count and bitmap size - FileStream_GetSize(pStream->pMaster, &MasterSize); - dwBlockCount = (DWORD)((MasterSize + DEFAULT_BLOCK_SIZE - 1) / DEFAULT_BLOCK_SIZE); - dwBitmapSize = (DWORD)((dwBlockCount + 7) / 8); - - // Setup stream size and position - pStream->BuildNumber = DEFAULT_BUILD_NUMBER; // BUGBUG: Really??? - pStream->StreamSize = MasterSize; - pStream->StreamPos = 0; - - // Open the base stream for write access - if(pStream->BaseOpen(pStream, pStream->szFileName, 0)) - { - // If the file open succeeded, check if the file size matches required size - pStream->BaseGetSize(pStream, &MirrorSize); - if(MirrorSize == MasterSize + dwBitmapSize + sizeof(FILE_BITMAP_FOOTER)) - { - // Attempt to load an existing file bitmap - if(FlatStream_LoadBitmap(pStream)) - return true; - - // We need to create new file bitmap - bNeedResizeMirrorStream = false; - } - - // We need to create mirror stream - bNeedCreateMirrorStream = false; - } - - // Create a new stream, if needed - if(bNeedCreateMirrorStream) - { - if(!pStream->BaseCreate(pStream)) - return false; - } - - // If we need to, then resize the mirror stream - if(bNeedResizeMirrorStream) - { - if(!pStream->BaseResize(pStream, MasterSize + dwBitmapSize + sizeof(FILE_BITMAP_FOOTER))) - return false; - } - - // Allocate the bitmap array - FileBitmap = STORM_ALLOC(BYTE, dwBitmapSize); - if(FileBitmap == NULL) - return false; - - // Initialize the bitmap - memset(FileBitmap, 0, dwBitmapSize); - pStream->FileBitmap = FileBitmap; - pStream->BitmapSize = dwBitmapSize; - pStream->BlockSize = DEFAULT_BLOCK_SIZE; - pStream->BlockCount = dwBlockCount; - pStream->IsComplete = 0; - pStream->IsModified = 1; - - // Note: Don't write the stream bitmap right away. - // Doing so would cause sparse file resize on NTFS, - // which would take long time on larger files. - return true; -} - -static TFileStream * FlatStream_Open(const TCHAR * szFileName, DWORD dwStreamFlags) -{ - TBlockStream * pStream; - ULONGLONG ByteOffset = 0; - - // Create new empty stream - pStream = (TBlockStream *)AllocateFileStream(szFileName, sizeof(TBlockStream), dwStreamFlags); - if(pStream == NULL) - return NULL; - - // Do we have a master stream? - if(pStream->pMaster != NULL) - { - if(!FlatStream_CreateMirror(pStream)) - { - FileStream_Close(pStream); - SetLastError(ERROR_FILE_NOT_FOUND); - return NULL; - } - } - else - { - // Attempt to open the base stream - if(!pStream->BaseOpen(pStream, pStream->szFileName, dwStreamFlags)) - { - FileStream_Close(pStream); - return NULL; - } - - // Load the bitmap, if required to - if(dwStreamFlags & STREAM_FLAG_USE_BITMAP) - FlatStream_LoadBitmap(pStream); - } - - // If we have a stream bitmap, set the reading functions - // which check presence of each file block - if(pStream->FileBitmap != NULL) - { - // Set the stream position to zero. Stream size is already set - assert(pStream->StreamSize != 0); - pStream->StreamPos = 0; - pStream->dwFlags |= STREAM_FLAG_READ_ONLY; - - // Supply the stream functions - pStream->StreamRead = (STREAM_READ)BlockStream_Read; - pStream->StreamGetSize = BlockStream_GetSize; - pStream->StreamGetPos = BlockStream_GetPos; - pStream->StreamClose = (STREAM_CLOSE)FlatStream_Close; - - // Supply the block functions - pStream->BlockCheck = (BLOCK_CHECK)FlatStream_BlockCheck; - pStream->BlockRead = (BLOCK_READ)FlatStream_BlockRead; - } - else - { - // Reset the base position to zero - pStream->BaseRead(pStream, &ByteOffset, NULL, 0); - - // Setup stream size and position - pStream->StreamSize = pStream->Base.File.FileSize; - pStream->StreamPos = 0; - - // Set the base functions - pStream->StreamRead = pStream->BaseRead; - pStream->StreamWrite = pStream->BaseWrite; - pStream->StreamResize = pStream->BaseResize; - pStream->StreamGetSize = pStream->BaseGetSize; - pStream->StreamGetPos = pStream->BaseGetPos; - pStream->StreamClose = pStream->BaseClose; - } - - return pStream; -} - -//----------------------------------------------------------------------------- -// Local functions - partial stream support - -static bool IsPartHeader(PPART_FILE_HEADER pPartHdr) -{ - // Version number must be 2 - if(pPartHdr->PartialVersion == 2) - { - // GameBuildNumber must be an ASCII number - if(isdigit(pPartHdr->GameBuildNumber[0]) && isdigit(pPartHdr->GameBuildNumber[1]) && isdigit(pPartHdr->GameBuildNumber[2])) - { - // Block size must be power of 2 - if((pPartHdr->BlockSize & (pPartHdr->BlockSize - 1)) == 0) - return true; - } - } - - return false; -} - -static DWORD PartStream_CheckFile(TBlockStream * pStream) -{ - PPART_FILE_MAP_ENTRY FileBitmap = (PPART_FILE_MAP_ENTRY)pStream->FileBitmap; - DWORD dwBlockCount; - - // Get the number of blocks - dwBlockCount = (DWORD)((pStream->StreamSize + pStream->BlockSize - 1) / pStream->BlockSize); - - // Check all blocks - for(DWORD i = 0; i < dwBlockCount; i++, FileBitmap++) - { - // Few sanity checks - assert(FileBitmap->LargeValueHi == 0); - assert(FileBitmap->LargeValueLo == 0); - assert(FileBitmap->Flags == 0 || FileBitmap->Flags == 3); - - // Check if this block is present - if(FileBitmap->Flags != 3) - return 0; - } - - // Yes, the file is complete - return 1; -} - -static bool PartStream_LoadBitmap(TBlockStream * pStream) -{ - PPART_FILE_MAP_ENTRY FileBitmap; - PART_FILE_HEADER PartHdr; - ULONGLONG ByteOffset = 0; - ULONGLONG StreamSize = 0; - DWORD BlockCount; - DWORD BitmapSize; - - // Only if the size is greater than size of the bitmap header - if(pStream->Base.File.FileSize > sizeof(PART_FILE_HEADER)) - { - // Attempt to read PART file header - if(pStream->BaseRead(pStream, &ByteOffset, &PartHdr, sizeof(PART_FILE_HEADER))) - { - // We need to swap PART file header on big-endian platforms - BSWAP_ARRAY32_UNSIGNED(&PartHdr, sizeof(PART_FILE_HEADER)); - - // Verify the PART file header - if(IsPartHeader(&PartHdr)) - { - // Get the number of blocks and size of one block - StreamSize = MAKE_OFFSET64(PartHdr.FileSizeHi, PartHdr.FileSizeLo); - ByteOffset = sizeof(PART_FILE_HEADER); - BlockCount = (DWORD)((StreamSize + PartHdr.BlockSize - 1) / PartHdr.BlockSize); - BitmapSize = BlockCount * sizeof(PART_FILE_MAP_ENTRY); - - // Check if sizes match - if((ByteOffset + BitmapSize) < pStream->Base.File.FileSize) - { - // Allocate space for the array of PART_FILE_MAP_ENTRY - FileBitmap = STORM_ALLOC(PART_FILE_MAP_ENTRY, BlockCount); - if(FileBitmap != NULL) - { - // Load the block map - if(!pStream->BaseRead(pStream, &ByteOffset, FileBitmap, BitmapSize)) - { - STORM_FREE(FileBitmap); - return false; - } - - // Make sure that the byte order is correct - BSWAP_ARRAY32_UNSIGNED(FileBitmap, BitmapSize); - - // Update the stream size - pStream->BuildNumber = StringToInt(PartHdr.GameBuildNumber); - pStream->StreamSize = StreamSize; - - // Fill the bitmap information - pStream->FileBitmap = FileBitmap; - pStream->BitmapSize = BitmapSize; - pStream->BlockSize = PartHdr.BlockSize; - pStream->BlockCount = BlockCount; - pStream->IsComplete = PartStream_CheckFile(pStream); - return true; - } - } - } - } - } - - return false; -} - -static void PartStream_UpdateBitmap( - TBlockStream * pStream, // Pointer to an open stream - ULONGLONG StartOffset, - ULONGLONG EndOffset, - ULONGLONG RealOffset) -{ - PPART_FILE_MAP_ENTRY FileBitmap; - DWORD BlockSize = pStream->BlockSize; - - // Sanity checks - assert((StartOffset & (BlockSize - 1)) == 0); - assert(pStream->FileBitmap != NULL); - - // Calculate the first entry in the block map - FileBitmap = (PPART_FILE_MAP_ENTRY)pStream->FileBitmap + (StartOffset / BlockSize); - - // Set all bits for the specified range - while(StartOffset < EndOffset) - { - // Set the bit - FileBitmap->BlockOffsHi = (DWORD)(RealOffset >> 0x20); - FileBitmap->BlockOffsLo = (DWORD)(RealOffset & 0xFFFFFFFF); - FileBitmap->Flags = 3; - - // Move all - StartOffset += BlockSize; - RealOffset += BlockSize; - FileBitmap++; - } - - // Increment the bitmap update count - pStream->IsModified = 1; -} - -static bool PartStream_BlockCheck( - TBlockStream * pStream, // Pointer to an open stream - ULONGLONG BlockOffset) -{ - PPART_FILE_MAP_ENTRY FileBitmap; - - // Sanity checks - assert((BlockOffset & (pStream->BlockSize - 1)) == 0); - assert(pStream->FileBitmap != NULL); - - // Calculate the block map entry - FileBitmap = (PPART_FILE_MAP_ENTRY)pStream->FileBitmap + (BlockOffset / pStream->BlockSize); - - // Check if the flags are present - return (FileBitmap->Flags & 0x03) ? true : false; -} - -static bool PartStream_BlockRead( - TBlockStream * pStream, - ULONGLONG StartOffset, - ULONGLONG EndOffset, - LPBYTE BlockBuffer, - DWORD BytesNeeded, - bool bAvailable) -{ - PPART_FILE_MAP_ENTRY FileBitmap; - ULONGLONG ByteOffset; - DWORD BytesToRead; - DWORD BlockIndex = (DWORD)(StartOffset / pStream->BlockSize); - - // The starting offset must be aligned to size of the block - assert(pStream->FileBitmap != NULL); - assert((StartOffset & (pStream->BlockSize - 1)) == 0); - assert(StartOffset < EndOffset); - - // If the blocks are not available, we need to load them from the master - // and then save to the mirror - if(bAvailable == false) - { - // If we have no master, we cannot satisfy read request - if(pStream->pMaster == NULL) - return false; - - // Load the blocks from the master stream - // Note that we always have to read complete blocks - // so they get properly stored to the mirror stream - BytesToRead = (DWORD)(EndOffset - StartOffset); - if(!FileStream_Read(pStream->pMaster, &StartOffset, BlockBuffer, BytesToRead)) - return false; - - // The loaded blocks are going to be stored to the end of the file - // Note that this operation is not required to succeed - if(pStream->BaseGetSize(pStream, &ByteOffset)) - { - // Store the loaded blocks to the mirror file. - if(pStream->BaseWrite(pStream, &ByteOffset, BlockBuffer, BytesToRead)) - { - PartStream_UpdateBitmap(pStream, StartOffset, EndOffset, ByteOffset); - } - } - } - else - { - // Get the file map entry - FileBitmap = (PPART_FILE_MAP_ENTRY)pStream->FileBitmap + BlockIndex; - - // Read all blocks - while(StartOffset < EndOffset) - { - // Get the number of bytes to be read - BytesToRead = (DWORD)(EndOffset - StartOffset); - if(BytesToRead > pStream->BlockSize) - BytesToRead = pStream->BlockSize; - if(BytesToRead > BytesNeeded) - BytesToRead = BytesNeeded; - - // Read the block - ByteOffset = MAKE_OFFSET64(FileBitmap->BlockOffsHi, FileBitmap->BlockOffsLo); - if(!pStream->BaseRead(pStream, &ByteOffset, BlockBuffer, BytesToRead)) - return false; - - // Move the pointers - StartOffset += pStream->BlockSize; - BlockBuffer += pStream->BlockSize; - BytesNeeded -= pStream->BlockSize; - FileBitmap++; - } - } - - return true; -} - -static void PartStream_Close(TBlockStream * pStream) -{ - PART_FILE_HEADER PartHeader; - ULONGLONG ByteOffset = 0; - - if(pStream->FileBitmap && pStream->IsModified) - { - // Prepare the part file header - memset(&PartHeader, 0, sizeof(PART_FILE_HEADER)); - PartHeader.PartialVersion = 2; - PartHeader.FileSizeHi = (DWORD)(pStream->StreamSize >> 0x20); - PartHeader.FileSizeLo = (DWORD)(pStream->StreamSize & 0xFFFFFFFF); - PartHeader.BlockSize = pStream->BlockSize; - - // Make sure that the header is properly BSWAPed - BSWAP_ARRAY32_UNSIGNED(&PartHeader, sizeof(PART_FILE_HEADER)); - sprintf(PartHeader.GameBuildNumber, "%u", (unsigned int)pStream->BuildNumber); - - // Write the part header - pStream->BaseWrite(pStream, &ByteOffset, &PartHeader, sizeof(PART_FILE_HEADER)); - - // Write the block bitmap - BSWAP_ARRAY32_UNSIGNED(pStream->FileBitmap, pStream->BitmapSize); - pStream->BaseWrite(pStream, NULL, pStream->FileBitmap, pStream->BitmapSize); - } - - // Close the base class - BlockStream_Close(pStream); -} - -static bool PartStream_CreateMirror(TBlockStream * pStream) -{ - ULONGLONG RemainingSize; - ULONGLONG MasterSize = 0; - ULONGLONG MirrorSize = 0; - LPBYTE FileBitmap = NULL; - DWORD dwBitmapSize; - DWORD dwBlockCount; - bool bNeedCreateMirrorStream = true; - bool bNeedResizeMirrorStream = true; - - // Do we have master function and base creation function? - if(pStream->pMaster == NULL || pStream->BaseCreate == NULL) - return false; - - // Retrieve the master file size, block count and bitmap size - FileStream_GetSize(pStream->pMaster, &MasterSize); - dwBlockCount = (DWORD)((MasterSize + DEFAULT_BLOCK_SIZE - 1) / DEFAULT_BLOCK_SIZE); - dwBitmapSize = (DWORD)(dwBlockCount * sizeof(PART_FILE_MAP_ENTRY)); - - // Setup stream size and position - pStream->BuildNumber = DEFAULT_BUILD_NUMBER; // BUGBUG: Really??? - pStream->StreamSize = MasterSize; - pStream->StreamPos = 0; - - // Open the base stream for write access - if(pStream->BaseOpen(pStream, pStream->szFileName, 0)) - { - // If the file open succeeded, check if the file size matches required size - pStream->BaseGetSize(pStream, &MirrorSize); - if(MirrorSize >= sizeof(PART_FILE_HEADER) + dwBitmapSize) - { - // Check if the remaining size is aligned to block - RemainingSize = MirrorSize - sizeof(PART_FILE_HEADER) - dwBitmapSize; - if((RemainingSize & (DEFAULT_BLOCK_SIZE - 1)) == 0 || RemainingSize == MasterSize) - { - // Attempt to load an existing file bitmap - if(PartStream_LoadBitmap(pStream)) - return true; - } - } - - // We need to create mirror stream - bNeedCreateMirrorStream = false; - } - - // Create a new stream, if needed - if(bNeedCreateMirrorStream) - { - if(!pStream->BaseCreate(pStream)) - return false; - } - - // If we need to, then resize the mirror stream - if(bNeedResizeMirrorStream) - { - if(!pStream->BaseResize(pStream, sizeof(PART_FILE_HEADER) + dwBitmapSize)) - return false; - } - - // Allocate the bitmap array - FileBitmap = STORM_ALLOC(BYTE, dwBitmapSize); - if(FileBitmap == NULL) - return false; - - // Initialize the bitmap - memset(FileBitmap, 0, dwBitmapSize); - pStream->FileBitmap = FileBitmap; - pStream->BitmapSize = dwBitmapSize; - pStream->BlockSize = DEFAULT_BLOCK_SIZE; - pStream->BlockCount = dwBlockCount; - pStream->IsComplete = 0; - pStream->IsModified = 1; - - // Note: Don't write the stream bitmap right away. - // Doing so would cause sparse file resize on NTFS, - // which would take long time on larger files. - return true; -} - - -static TFileStream * PartStream_Open(const TCHAR * szFileName, DWORD dwStreamFlags) -{ - TBlockStream * pStream; - - // Create new empty stream - pStream = (TBlockStream *)AllocateFileStream(szFileName, sizeof(TBlockStream), dwStreamFlags); - if(pStream == NULL) - return NULL; - - // Do we have a master stream? - if(pStream->pMaster != NULL) - { - if(!PartStream_CreateMirror(pStream)) - { - FileStream_Close(pStream); - SetLastError(ERROR_FILE_NOT_FOUND); - return NULL; - } - } - else - { - // Attempt to open the base stream - if(!pStream->BaseOpen(pStream, pStream->szFileName, dwStreamFlags)) - { - FileStream_Close(pStream); - return NULL; - } - - // Load the part stream block map - if(!PartStream_LoadBitmap(pStream)) - { - FileStream_Close(pStream); - SetLastError(ERROR_BAD_FORMAT); - return NULL; - } - } - - // Set the stream position to zero. Stream size is already set - assert(pStream->StreamSize != 0); - pStream->StreamPos = 0; - pStream->dwFlags |= STREAM_FLAG_READ_ONLY; - - // Set new function pointers - pStream->StreamRead = (STREAM_READ)BlockStream_Read; - pStream->StreamGetPos = BlockStream_GetPos; - pStream->StreamGetSize = BlockStream_GetSize; - pStream->StreamClose = (STREAM_CLOSE)PartStream_Close; - - // Supply the block functions - pStream->BlockCheck = (BLOCK_CHECK)PartStream_BlockCheck; - pStream->BlockRead = (BLOCK_READ)PartStream_BlockRead; - return pStream; -} - -//----------------------------------------------------------------------------- -// Local functions - MPQE stream support - -static const char * szKeyTemplate = "expand 32-byte k000000000000000000000000000000000000000000000000"; - -static const char * AuthCodeArray[] = -{ - // Starcraft II (Heart of the Swarm) - // Authentication code URL: http://dist.blizzard.com/mediakey/hots-authenticationcode-bgdl.txt - // -0C- -1C--08- -18--04- -14--00- -10- - "S48B6CDTN5XEQAKQDJNDLJBJ73FDFM3U", // SC2 Heart of the Swarm-all : "expand 32-byte kQAKQ0000FM3UN5XE000073FD6CDT0000LJBJS48B0000DJND" - - // Diablo III: Agent.exe (1.0.0.954) - // Address of decryption routine: 00502b00 - // Pointer to decryptor object: ECX - // Pointer to key: ECX+0x5C - // Authentication code URL: http://dist.blizzard.com/mediakey/d3-authenticationcode-enGB.txt - // -0C- -1C--08- -18--04- -14--00- -10- - "UCMXF6EJY352EFH4XFRXCFH2XC9MQRZK", // Diablo III Installer (deDE): "expand 32-byte kEFH40000QRZKY3520000XC9MF6EJ0000CFH2UCMX0000XFRX" - "MMKVHY48RP7WXP4GHYBQ7SL9J9UNPHBP", // Diablo III Installer (enGB): "expand 32-byte kXP4G0000PHBPRP7W0000J9UNHY4800007SL9MMKV0000HYBQ" - "8MXLWHQ7VGGLTZ9MQZQSFDCLJYET3CPP", // Diablo III Installer (enSG): "expand 32-byte kTZ9M00003CPPVGGL0000JYETWHQ70000FDCL8MXL0000QZQS" - "EJ2R5TM6XFE2GUNG5QDGHKQ9UAKPWZSZ", // Diablo III Installer (enUS): "expand 32-byte kGUNG0000WZSZXFE20000UAKP5TM60000HKQ9EJ2R00005QDG" - "PBGFBE42Z6LNK65UGJQ3WZVMCLP4HQQT", // Diablo III Installer (esES): "expand 32-byte kK65U0000HQQTZ6LN0000CLP4BE420000WZVMPBGF0000GJQ3" - "X7SEJJS9TSGCW5P28EBSC47AJPEY8VU2", // Diablo III Installer (esMX): "expand 32-byte kW5P200008VU2TSGC0000JPEYJJS90000C47AX7SE00008EBS" - "5KVBQA8VYE6XRY3DLGC5ZDE4XS4P7YA2", // Diablo III Installer (frFR): "expand 32-byte kRY3D00007YA2YE6X0000XS4PQA8V0000ZDE45KVB0000LGC5" - "478JD2K56EVNVVY4XX8TDWYT5B8KB254", // Diablo III Installer (itIT): "expand 32-byte kVVY40000B2546EVN00005B8KD2K50000DWYT478J0000XX8T" - "8TS4VNFQRZTN6YWHE9CHVDH9NVWD474A", // Diablo III Installer (koKR): "expand 32-byte k6YWH0000474ARZTN0000NVWDVNFQ0000VDH98TS40000E9CH" - "LJ52Z32DF4LZ4ZJJXVKK3AZQA6GABLJB", // Diablo III Installer (plPL): "expand 32-byte k4ZJJ0000BLJBF4LZ0000A6GAZ32D00003AZQLJ520000XVKK" - "K6BDHY2ECUE2545YKNLBJPVYWHE7XYAG", // Diablo III Installer (ptBR): "expand 32-byte k545Y0000XYAGCUE20000WHE7HY2E0000JPVYK6BD0000KNLB" - "NDVW8GWLAYCRPGRNY8RT7ZZUQU63VLPR", // Diablo III Installer (ruRU): "expand 32-byte kXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - "6VWCQTN8V3ZZMRUCZXV8A8CGUX2TAA8H", // Diablo III Installer (zhTW): "expand 32-byte kMRUC0000AA8HV3ZZ0000UX2TQTN80000A8CG6VWC0000ZXV8" -// "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", // Diablo III Installer (zhCN): "expand 32-byte kXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - - // Starcraft II (Wings of Liberty): Installer.exe (4.1.1.4219) - // Address of decryption routine: 0053A3D0 - // Pointer to decryptor object: ECX - // Pointer to key: ECX+0x5C - // Authentication code URL: http://dist.blizzard.com/mediakey/sc2-authenticationcode-enUS.txt - // -0C- -1C--08- -18--04- -14--00- -10- - "Y45MD3CAK4KXSSXHYD9VY64Z8EKJ4XFX", // SC2 Wings of Liberty (deDE): "expand 32-byte kSSXH00004XFXK4KX00008EKJD3CA0000Y64ZY45M0000YD9V" - "G8MN8UDG6NA2ANGY6A3DNY82HRGF29ZH", // SC2 Wings of Liberty (enGB): "expand 32-byte kANGY000029ZH6NA20000HRGF8UDG0000NY82G8MN00006A3D" - "W9RRHLB2FDU9WW5B3ECEBLRSFWZSF7HW", // SC2 Wings of Liberty (enSG): "expand 32-byte kWW5B0000F7HWFDU90000FWZSHLB20000BLRSW9RR00003ECE" - "3DH5RE5NVM5GTFD85LXGWT6FK859ETR5", // SC2 Wings of Liberty (enUS): "expand 32-byte kTFD80000ETR5VM5G0000K859RE5N0000WT6F3DH500005LXG" - "8WLKUAXE94PFQU4Y249PAZ24N4R4XKTQ", // SC2 Wings of Liberty (esES): "expand 32-byte kQU4Y0000XKTQ94PF0000N4R4UAXE0000AZ248WLK0000249P" - "A34DXX3VHGGXSQBRFE5UFFDXMF9G4G54", // SC2 Wings of Liberty (esMX): "expand 32-byte kSQBR00004G54HGGX0000MF9GXX3V0000FFDXA34D0000FE5U" - "ZG7J9K938HJEFWPQUA768MA2PFER6EAJ", // SC2 Wings of Liberty (frFR): "expand 32-byte kFWPQ00006EAJ8HJE0000PFER9K9300008MA2ZG7J0000UA76" - "NE7CUNNNTVAPXV7E3G2BSVBWGVMW8BL2", // SC2 Wings of Liberty (itIT): "expand 32-byte kXV7E00008BL2TVAP0000GVMWUNNN0000SVBWNE7C00003G2B" - "3V9E2FTMBM9QQWK7U6MAMWAZWQDB838F", // SC2 Wings of Liberty (koKR): "expand 32-byte kQWK70000838FBM9Q0000WQDB2FTM0000MWAZ3V9E0000U6MA" - "2NSFB8MELULJ83U6YHA3UP6K4MQD48L6", // SC2 Wings of Liberty (plPL): "expand 32-byte k83U6000048L6LULJ00004MQDB8ME0000UP6K2NSF0000YHA3" - "QA2TZ9EWZ4CUU8BMB5WXCTY65F9CSW4E", // SC2 Wings of Liberty (ptBR): "expand 32-byte kU8BM0000SW4EZ4CU00005F9CZ9EW0000CTY6QA2T0000B5WX" - "VHB378W64BAT9SH7D68VV9NLQDK9YEGT", // SC2 Wings of Liberty (ruRU): "expand 32-byte k9SH70000YEGT4BAT0000QDK978W60000V9NLVHB30000D68V" - "U3NFQJV4M6GC7KBN9XQJ3BRDN3PLD9NE", // SC2 Wings of Liberty (zhTW): "expand 32-byte k7KBN0000D9NEM6GC0000N3PLQJV400003BRDU3NF00009XQJ" - - NULL -}; - -static DWORD Rol32(DWORD dwValue, DWORD dwRolCount) -{ - DWORD dwShiftRight = 32 - dwRolCount; - - return (dwValue << dwRolCount) | (dwValue >> dwShiftRight); -} - -static void CreateKeyFromAuthCode( - LPBYTE pbKeyBuffer, - const char * szAuthCode) -{ - LPDWORD KeyPosition = (LPDWORD)(pbKeyBuffer + 0x10); - LPDWORD AuthCode32 = (LPDWORD)szAuthCode; - - memcpy(pbKeyBuffer, szKeyTemplate, MPQE_CHUNK_SIZE); - KeyPosition[0x00] = AuthCode32[0x03]; - KeyPosition[0x02] = AuthCode32[0x07]; - KeyPosition[0x03] = AuthCode32[0x02]; - KeyPosition[0x05] = AuthCode32[0x06]; - KeyPosition[0x06] = AuthCode32[0x01]; - KeyPosition[0x08] = AuthCode32[0x05]; - KeyPosition[0x09] = AuthCode32[0x00]; - KeyPosition[0x0B] = AuthCode32[0x04]; - BSWAP_ARRAY32_UNSIGNED(pbKeyBuffer, MPQE_CHUNK_SIZE); -} - -static void DecryptFileChunk( - DWORD * MpqData, - LPBYTE pbKey, - ULONGLONG ByteOffset, - DWORD dwLength) -{ - ULONGLONG ChunkOffset; - DWORD KeyShuffled[0x10]; - DWORD KeyMirror[0x10]; - DWORD RoundCount = 0x14; - - // Prepare the key - ChunkOffset = ByteOffset / MPQE_CHUNK_SIZE; - memcpy(KeyMirror, pbKey, MPQE_CHUNK_SIZE); - BSWAP_ARRAY32_UNSIGNED(KeyMirror, MPQE_CHUNK_SIZE); - KeyMirror[0x05] = (DWORD)(ChunkOffset >> 32); - KeyMirror[0x08] = (DWORD)(ChunkOffset); - - while(dwLength >= MPQE_CHUNK_SIZE) - { - // Shuffle the key - part 1 - KeyShuffled[0x0E] = KeyMirror[0x00]; - KeyShuffled[0x0C] = KeyMirror[0x01]; - KeyShuffled[0x05] = KeyMirror[0x02]; - KeyShuffled[0x0F] = KeyMirror[0x03]; - KeyShuffled[0x0A] = KeyMirror[0x04]; - KeyShuffled[0x07] = KeyMirror[0x05]; - KeyShuffled[0x0B] = KeyMirror[0x06]; - KeyShuffled[0x09] = KeyMirror[0x07]; - KeyShuffled[0x03] = KeyMirror[0x08]; - KeyShuffled[0x06] = KeyMirror[0x09]; - KeyShuffled[0x08] = KeyMirror[0x0A]; - KeyShuffled[0x0D] = KeyMirror[0x0B]; - KeyShuffled[0x02] = KeyMirror[0x0C]; - KeyShuffled[0x04] = KeyMirror[0x0D]; - KeyShuffled[0x01] = KeyMirror[0x0E]; - KeyShuffled[0x00] = KeyMirror[0x0F]; - - // Shuffle the key - part 2 - for(DWORD i = 0; i < RoundCount; i += 2) - { - KeyShuffled[0x0A] = KeyShuffled[0x0A] ^ Rol32((KeyShuffled[0x0E] + KeyShuffled[0x02]), 0x07); - KeyShuffled[0x03] = KeyShuffled[0x03] ^ Rol32((KeyShuffled[0x0A] + KeyShuffled[0x0E]), 0x09); - KeyShuffled[0x02] = KeyShuffled[0x02] ^ Rol32((KeyShuffled[0x03] + KeyShuffled[0x0A]), 0x0D); - KeyShuffled[0x0E] = KeyShuffled[0x0E] ^ Rol32((KeyShuffled[0x02] + KeyShuffled[0x03]), 0x12); - - KeyShuffled[0x07] = KeyShuffled[0x07] ^ Rol32((KeyShuffled[0x0C] + KeyShuffled[0x04]), 0x07); - KeyShuffled[0x06] = KeyShuffled[0x06] ^ Rol32((KeyShuffled[0x07] + KeyShuffled[0x0C]), 0x09); - KeyShuffled[0x04] = KeyShuffled[0x04] ^ Rol32((KeyShuffled[0x06] + KeyShuffled[0x07]), 0x0D); - KeyShuffled[0x0C] = KeyShuffled[0x0C] ^ Rol32((KeyShuffled[0x04] + KeyShuffled[0x06]), 0x12); - - KeyShuffled[0x0B] = KeyShuffled[0x0B] ^ Rol32((KeyShuffled[0x05] + KeyShuffled[0x01]), 0x07); - KeyShuffled[0x08] = KeyShuffled[0x08] ^ Rol32((KeyShuffled[0x0B] + KeyShuffled[0x05]), 0x09); - KeyShuffled[0x01] = KeyShuffled[0x01] ^ Rol32((KeyShuffled[0x08] + KeyShuffled[0x0B]), 0x0D); - KeyShuffled[0x05] = KeyShuffled[0x05] ^ Rol32((KeyShuffled[0x01] + KeyShuffled[0x08]), 0x12); - - KeyShuffled[0x09] = KeyShuffled[0x09] ^ Rol32((KeyShuffled[0x0F] + KeyShuffled[0x00]), 0x07); - KeyShuffled[0x0D] = KeyShuffled[0x0D] ^ Rol32((KeyShuffled[0x09] + KeyShuffled[0x0F]), 0x09); - KeyShuffled[0x00] = KeyShuffled[0x00] ^ Rol32((KeyShuffled[0x0D] + KeyShuffled[0x09]), 0x0D); - KeyShuffled[0x0F] = KeyShuffled[0x0F] ^ Rol32((KeyShuffled[0x00] + KeyShuffled[0x0D]), 0x12); - - KeyShuffled[0x04] = KeyShuffled[0x04] ^ Rol32((KeyShuffled[0x0E] + KeyShuffled[0x09]), 0x07); - KeyShuffled[0x08] = KeyShuffled[0x08] ^ Rol32((KeyShuffled[0x04] + KeyShuffled[0x0E]), 0x09); - KeyShuffled[0x09] = KeyShuffled[0x09] ^ Rol32((KeyShuffled[0x08] + KeyShuffled[0x04]), 0x0D); - KeyShuffled[0x0E] = KeyShuffled[0x0E] ^ Rol32((KeyShuffled[0x09] + KeyShuffled[0x08]), 0x12); - - KeyShuffled[0x01] = KeyShuffled[0x01] ^ Rol32((KeyShuffled[0x0C] + KeyShuffled[0x0A]), 0x07); - KeyShuffled[0x0D] = KeyShuffled[0x0D] ^ Rol32((KeyShuffled[0x01] + KeyShuffled[0x0C]), 0x09); - KeyShuffled[0x0A] = KeyShuffled[0x0A] ^ Rol32((KeyShuffled[0x0D] + KeyShuffled[0x01]), 0x0D); - KeyShuffled[0x0C] = KeyShuffled[0x0C] ^ Rol32((KeyShuffled[0x0A] + KeyShuffled[0x0D]), 0x12); - - KeyShuffled[0x00] = KeyShuffled[0x00] ^ Rol32((KeyShuffled[0x05] + KeyShuffled[0x07]), 0x07); - KeyShuffled[0x03] = KeyShuffled[0x03] ^ Rol32((KeyShuffled[0x00] + KeyShuffled[0x05]), 0x09); - KeyShuffled[0x07] = KeyShuffled[0x07] ^ Rol32((KeyShuffled[0x03] + KeyShuffled[0x00]), 0x0D); - KeyShuffled[0x05] = KeyShuffled[0x05] ^ Rol32((KeyShuffled[0x07] + KeyShuffled[0x03]), 0x12); - - KeyShuffled[0x02] = KeyShuffled[0x02] ^ Rol32((KeyShuffled[0x0F] + KeyShuffled[0x0B]), 0x07); - KeyShuffled[0x06] = KeyShuffled[0x06] ^ Rol32((KeyShuffled[0x02] + KeyShuffled[0x0F]), 0x09); - KeyShuffled[0x0B] = KeyShuffled[0x0B] ^ Rol32((KeyShuffled[0x06] + KeyShuffled[0x02]), 0x0D); - KeyShuffled[0x0F] = KeyShuffled[0x0F] ^ Rol32((KeyShuffled[0x0B] + KeyShuffled[0x06]), 0x12); - } - - // Decrypt one data chunk - BSWAP_ARRAY32_UNSIGNED(MpqData, MPQE_CHUNK_SIZE); - MpqData[0x00] = MpqData[0x00] ^ (KeyShuffled[0x0E] + KeyMirror[0x00]); - MpqData[0x01] = MpqData[0x01] ^ (KeyShuffled[0x04] + KeyMirror[0x0D]); - MpqData[0x02] = MpqData[0x02] ^ (KeyShuffled[0x08] + KeyMirror[0x0A]); - MpqData[0x03] = MpqData[0x03] ^ (KeyShuffled[0x09] + KeyMirror[0x07]); - MpqData[0x04] = MpqData[0x04] ^ (KeyShuffled[0x0A] + KeyMirror[0x04]); - MpqData[0x05] = MpqData[0x05] ^ (KeyShuffled[0x0C] + KeyMirror[0x01]); - MpqData[0x06] = MpqData[0x06] ^ (KeyShuffled[0x01] + KeyMirror[0x0E]); - MpqData[0x07] = MpqData[0x07] ^ (KeyShuffled[0x0D] + KeyMirror[0x0B]); - MpqData[0x08] = MpqData[0x08] ^ (KeyShuffled[0x03] + KeyMirror[0x08]); - MpqData[0x09] = MpqData[0x09] ^ (KeyShuffled[0x07] + KeyMirror[0x05]); - MpqData[0x0A] = MpqData[0x0A] ^ (KeyShuffled[0x05] + KeyMirror[0x02]); - MpqData[0x0B] = MpqData[0x0B] ^ (KeyShuffled[0x00] + KeyMirror[0x0F]); - MpqData[0x0C] = MpqData[0x0C] ^ (KeyShuffled[0x02] + KeyMirror[0x0C]); - MpqData[0x0D] = MpqData[0x0D] ^ (KeyShuffled[0x06] + KeyMirror[0x09]); - MpqData[0x0E] = MpqData[0x0E] ^ (KeyShuffled[0x0B] + KeyMirror[0x06]); - MpqData[0x0F] = MpqData[0x0F] ^ (KeyShuffled[0x0F] + KeyMirror[0x03]); - BSWAP_ARRAY32_UNSIGNED(MpqData, MPQE_CHUNK_SIZE); - - // Update byte offset in the key - KeyMirror[0x08]++; - if(KeyMirror[0x08] == 0) - KeyMirror[0x05]++; - - // Move pointers and decrease number of bytes to decrypt - MpqData += (MPQE_CHUNK_SIZE / sizeof(DWORD)); - dwLength -= MPQE_CHUNK_SIZE; - } -} - -static bool MpqeStream_DetectFileKey(TEncryptedStream * pStream) -{ - ULONGLONG ByteOffset = 0; - BYTE EncryptedHeader[MPQE_CHUNK_SIZE]; - BYTE FileHeader[MPQE_CHUNK_SIZE]; - - // Read the first file chunk - if(pStream->BaseRead(pStream, &ByteOffset, EncryptedHeader, sizeof(EncryptedHeader))) - { - // We just try all known keys one by one - for(int i = 0; AuthCodeArray[i] != NULL; i++) - { - // Prepare they decryption key from game serial number - CreateKeyFromAuthCode(pStream->Key, AuthCodeArray[i]); - - // Try to decrypt with the given key - memcpy(FileHeader, EncryptedHeader, MPQE_CHUNK_SIZE); - DecryptFileChunk((LPDWORD)FileHeader, pStream->Key, ByteOffset, MPQE_CHUNK_SIZE); - - // We check the decrypted data - // All known encrypted MPQs have header at the begin of the file, - // so we check for MPQ signature there. - if(FileHeader[0] == 'M' && FileHeader[1] == 'P' && FileHeader[2] == 'Q') - { - // Update the stream size - pStream->StreamSize = pStream->Base.File.FileSize; - - // Fill the block information - pStream->BlockSize = MPQE_CHUNK_SIZE; - pStream->BlockCount = (DWORD)(pStream->Base.File.FileSize + MPQE_CHUNK_SIZE - 1) / MPQE_CHUNK_SIZE; - pStream->IsComplete = 1; - return true; - } - } - } - - // Key not found, sorry - return false; -} - -static bool MpqeStream_BlockRead( - TEncryptedStream * pStream, - ULONGLONG StartOffset, - ULONGLONG EndOffset, - LPBYTE BlockBuffer, - DWORD BytesNeeded, - bool bAvailable) -{ - DWORD dwBytesToRead; - - assert((StartOffset & (pStream->BlockSize - 1)) == 0); - assert(StartOffset < EndOffset); - assert(bAvailable != false); - BytesNeeded = BytesNeeded; - bAvailable = bAvailable; - - // Read the file from the stream as-is - // Limit the reading to number of blocks really needed - dwBytesToRead = (DWORD)(EndOffset - StartOffset); - if(!pStream->BaseRead(pStream, &StartOffset, BlockBuffer, dwBytesToRead)) - return false; - - // Decrypt the data - dwBytesToRead = (dwBytesToRead + MPQE_CHUNK_SIZE - 1) & ~(MPQE_CHUNK_SIZE - 1); - DecryptFileChunk((LPDWORD)BlockBuffer, pStream->Key, StartOffset, dwBytesToRead); - return true; -} - -static TFileStream * MpqeStream_Open(const TCHAR * szFileName, DWORD dwStreamFlags) -{ - TEncryptedStream * pStream; - - // Create new empty stream - pStream = (TEncryptedStream *)AllocateFileStream(szFileName, sizeof(TEncryptedStream), dwStreamFlags); - if(pStream == NULL) - return NULL; - - // Attempt to open the base stream - assert(pStream->BaseOpen != NULL); - if(!pStream->BaseOpen(pStream, pStream->szFileName, dwStreamFlags)) - return NULL; - - // Determine the encryption key for the MPQ - if(MpqeStream_DetectFileKey(pStream)) - { - // Set the stream position and size - assert(pStream->StreamSize != 0); - pStream->StreamPos = 0; - pStream->dwFlags |= STREAM_FLAG_READ_ONLY; - - // Set new function pointers - pStream->StreamRead = (STREAM_READ)BlockStream_Read; - pStream->StreamGetPos = BlockStream_GetPos; - pStream->StreamGetSize = BlockStream_GetSize; - pStream->StreamClose = pStream->BaseClose; - - // Supply the block functions - pStream->BlockRead = (BLOCK_READ)MpqeStream_BlockRead; - return pStream; - } - - // Cleanup the stream and return - FileStream_Close(pStream); - SetLastError(ERROR_UNKNOWN_FILE_KEY); - return NULL; -} - -//----------------------------------------------------------------------------- -// Local functions - Block4 stream support - -#define BLOCK4_BLOCK_SIZE 0x4000 // Size of one block -#define BLOCK4_HASH_SIZE 0x20 // Size of MD5 hash that is after each block -#define BLOCK4_MAX_BLOCKS 0x00002000 // Maximum amount of blocks per file -#define BLOCK4_MAX_FSIZE 0x08040000 // Max size of one file - -static bool Block4Stream_BlockRead( - TBlockStream * pStream, // Pointer to an open stream - ULONGLONG StartOffset, - ULONGLONG EndOffset, - LPBYTE BlockBuffer, - DWORD BytesNeeded, - bool bAvailable) -{ - TBaseProviderData * BaseArray = (TBaseProviderData *)pStream->FileBitmap; - ULONGLONG ByteOffset; - DWORD BytesToRead; - DWORD StreamIndex; - DWORD BlockIndex; - bool bResult; - - // The starting offset must be aligned to size of the block - assert(pStream->FileBitmap != NULL); - assert((StartOffset & (pStream->BlockSize - 1)) == 0); - assert(StartOffset < EndOffset); - assert(bAvailable == true); - - // Keep compiler happy - bAvailable = bAvailable; - EndOffset = EndOffset; - - while(BytesNeeded != 0) - { - // Calculate the block index and the file index - StreamIndex = (DWORD)((StartOffset / pStream->BlockSize) / BLOCK4_MAX_BLOCKS); - BlockIndex = (DWORD)((StartOffset / pStream->BlockSize) % BLOCK4_MAX_BLOCKS); - if(StreamIndex > pStream->BitmapSize) - return false; - - // Calculate the block offset - ByteOffset = ((ULONGLONG)BlockIndex * (BLOCK4_BLOCK_SIZE + BLOCK4_HASH_SIZE)); - BytesToRead = STORMLIB_MIN(BytesNeeded, BLOCK4_BLOCK_SIZE); - - // Read from the base stream - pStream->Base = BaseArray[StreamIndex]; - bResult = pStream->BaseRead(pStream, &ByteOffset, BlockBuffer, BytesToRead); - BaseArray[StreamIndex] = pStream->Base; - - // Did the result succeed? - if(bResult == false) - return false; - - // Move pointers - StartOffset += BytesToRead; - BlockBuffer += BytesToRead; - BytesNeeded -= BytesToRead; - } - - return true; -} - - -static void Block4Stream_Close(TBlockStream * pStream) -{ - TBaseProviderData * BaseArray = (TBaseProviderData *)pStream->FileBitmap; - - // If we have a non-zero count of base streams, - // we have to close them all - if(BaseArray != NULL) - { - // Close all base streams - for(DWORD i = 0; i < pStream->BitmapSize; i++) - { - memcpy(&pStream->Base, BaseArray + i, sizeof(TBaseProviderData)); - pStream->BaseClose(pStream); - } - } - - // Free the data map, if any - if(pStream->FileBitmap != NULL) - STORM_FREE(pStream->FileBitmap); - pStream->FileBitmap = NULL; - - // Do not call the BaseClose function, - // we closed all handles already - return; -} - -static TFileStream * Block4Stream_Open(const TCHAR * szFileName, DWORD dwStreamFlags) -{ - TBaseProviderData * NewBaseArray = NULL; - ULONGLONG RemainderBlock; - ULONGLONG BlockCount; - ULONGLONG FileSize; - TBlockStream * pStream; - TCHAR * szNameBuff; - size_t nNameLength; - DWORD dwBaseFiles = 0; - DWORD dwBaseFlags; - - // Create new empty stream - pStream = (TBlockStream *)AllocateFileStream(szFileName, sizeof(TBlockStream), dwStreamFlags); - if(pStream == NULL) - return NULL; - - // Sanity check - assert(pStream->BaseOpen != NULL); - - // Get the length of the file name without numeric suffix - nNameLength = _tcslen(pStream->szFileName); - if(pStream->szFileName[nNameLength - 2] == '.' && pStream->szFileName[nNameLength - 1] == '0') - nNameLength -= 2; - pStream->szFileName[nNameLength] = 0; - - // Supply the stream functions - pStream->StreamRead = (STREAM_READ)BlockStream_Read; - pStream->StreamGetSize = BlockStream_GetSize; - pStream->StreamGetPos = BlockStream_GetPos; - pStream->StreamClose = (STREAM_CLOSE)Block4Stream_Close; - pStream->BlockRead = (BLOCK_READ)Block4Stream_BlockRead; - - // Allocate work space for numeric names - szNameBuff = STORM_ALLOC(TCHAR, nNameLength + 4); - if(szNameBuff != NULL) - { - // Set the base flags - dwBaseFlags = (dwStreamFlags & STREAM_PROVIDERS_MASK) | STREAM_FLAG_READ_ONLY; - - // Go all suffixes from 0 to 30 - for(int nSuffix = 0; nSuffix < 30; nSuffix++) - { - // Open the n-th file - _stprintf(szNameBuff, _T("%s.%u"), pStream->szFileName, nSuffix); - if(!pStream->BaseOpen(pStream, szNameBuff, dwBaseFlags)) - break; - - // If the open succeeded, we re-allocate the base provider array - NewBaseArray = STORM_ALLOC(TBaseProviderData, dwBaseFiles + 1); - if(NewBaseArray == NULL) - { - SetLastError(ERROR_NOT_ENOUGH_MEMORY); - return NULL; - } - - // Copy the old base data array to the new base data array - if(pStream->FileBitmap != NULL) - { - memcpy(NewBaseArray, pStream->FileBitmap, sizeof(TBaseProviderData) * dwBaseFiles); - STORM_FREE(pStream->FileBitmap); - } - - // Also copy the opened base array - memcpy(NewBaseArray + dwBaseFiles, &pStream->Base, sizeof(TBaseProviderData)); - pStream->FileBitmap = NewBaseArray; - dwBaseFiles++; - - // Get the size of the base stream - pStream->BaseGetSize(pStream, &FileSize); - assert(FileSize <= BLOCK4_MAX_FSIZE); - RemainderBlock = FileSize % (BLOCK4_BLOCK_SIZE + BLOCK4_HASH_SIZE); - BlockCount = FileSize / (BLOCK4_BLOCK_SIZE + BLOCK4_HASH_SIZE); - - // Increment the stream size and number of blocks - pStream->StreamSize += (BlockCount * BLOCK4_BLOCK_SIZE); - pStream->BlockCount += (DWORD)BlockCount; - - // Is this the last file? - if(FileSize < BLOCK4_MAX_FSIZE) - { - if(RemainderBlock) - { - pStream->StreamSize += (RemainderBlock - BLOCK4_HASH_SIZE); - pStream->BlockCount++; - } - break; - } - } - - // Fill the remainining block stream variables - pStream->BitmapSize = dwBaseFiles; - pStream->BlockSize = BLOCK4_BLOCK_SIZE; - pStream->IsComplete = 1; - pStream->IsModified = 0; - - // Fill the remaining stream variables - pStream->StreamPos = 0; - pStream->dwFlags |= STREAM_FLAG_READ_ONLY; - - STORM_FREE(szNameBuff); - } - - // If we opened something, return success - if(dwBaseFiles == 0) - { - FileStream_Close(pStream); - SetLastError(ERROR_FILE_NOT_FOUND); - pStream = NULL; - } - - return pStream; -} - -//----------------------------------------------------------------------------- -// Public functions - -/** - * This function creates a new file for read-write access - * - * - If the current platform supports file sharing, - * the file must be created for read sharing (i.e. another application - * can open the file for read, but not for write) - * - If the file does not exist, the function must create new one - * - If the file exists, the function must rewrite it and set to zero size - * - The parameters of the function must be validate by the caller - * - The function must initialize all stream function pointers in TFileStream - * - If the function fails from any reason, it must close all handles - * and free all memory that has been allocated in the process of stream creation, - * including the TFileStream structure itself - * - * \a szFileName Name of the file to create - */ - -TFileStream * FileStream_CreateFile( - const TCHAR * szFileName, - DWORD dwStreamFlags) -{ - TFileStream * pStream; - - // We only support creation of flat, local file - if((dwStreamFlags & (STREAM_PROVIDERS_MASK)) != (STREAM_PROVIDER_FLAT | BASE_PROVIDER_FILE)) - { - SetLastError(ERROR_NOT_SUPPORTED); - return NULL; - } - - // Allocate file stream structure for flat stream - pStream = AllocateFileStream(szFileName, sizeof(TBlockStream), dwStreamFlags); - if(pStream != NULL) - { - // Attempt to create the disk file - if(BaseFile_Create(pStream)) - { - // Fill the stream provider functions - pStream->StreamRead = pStream->BaseRead; - pStream->StreamWrite = pStream->BaseWrite; - pStream->StreamResize = pStream->BaseResize; - pStream->StreamGetSize = pStream->BaseGetSize; - pStream->StreamGetPos = pStream->BaseGetPos; - pStream->StreamClose = pStream->BaseClose; - return pStream; - } - - // File create failed, delete the stream - STORM_FREE(pStream); - pStream = NULL; - } - - // Return the stream - return pStream; -} - -/** - * This function opens an existing file for read or read-write access - * - If the current platform supports file sharing, - * the file must be open for read sharing (i.e. another application - * can open the file for read, but not for write) - * - If the file does not exist, the function must return NULL - * - If the file exists but cannot be open, then function must return NULL - * - The parameters of the function must be validate by the caller - * - The function must initialize all stream function pointers in TFileStream - * - If the function fails from any reason, it must close all handles - * and free all memory that has been allocated in the process of stream creation, - * including the TFileStream structure itself - * - * \a szFileName Name of the file to open - * \a dwStreamFlags specifies the provider and base storage type - */ - -TFileStream * FileStream_OpenFile( - const TCHAR * szFileName, - DWORD dwStreamFlags) -{ - DWORD dwProvider = dwStreamFlags & STREAM_PROVIDERS_MASK; - size_t nPrefixLength = FileStream_Prefix(szFileName, &dwProvider); - - // Re-assemble the stream flags - dwStreamFlags = (dwStreamFlags & STREAM_OPTIONS_MASK) | dwProvider; - szFileName += nPrefixLength; - - // Perform provider-specific open - switch(dwStreamFlags & STREAM_PROVIDER_MASK) - { - case STREAM_PROVIDER_FLAT: - return FlatStream_Open(szFileName, dwStreamFlags); - - case STREAM_PROVIDER_PARTIAL: - return PartStream_Open(szFileName, dwStreamFlags); - - case STREAM_PROVIDER_MPQE: - return MpqeStream_Open(szFileName, dwStreamFlags); - - case STREAM_PROVIDER_BLOCK4: - return Block4Stream_Open(szFileName, dwStreamFlags); - - default: - SetLastError(ERROR_INVALID_PARAMETER); - return NULL; - } -} - -/** - * Returns the file name of the stream - * - * \a pStream Pointer to an open stream - */ -const TCHAR * FileStream_GetFileName(TFileStream * pStream) -{ - assert(pStream != NULL); - return pStream->szFileName; -} - -/** - * Returns the length of the provider prefix. Returns zero if no prefix - * - * \a szFileName Pointer to a stream name (file, mapped file, URL) - * \a pdwStreamProvider Pointer to a DWORD variable that receives stream provider (STREAM_PROVIDER_XXX) - */ - -size_t FileStream_Prefix(const TCHAR * szFileName, DWORD * pdwProvider) -{ - size_t nPrefixLength1 = 0; - size_t nPrefixLength2 = 0; - DWORD dwProvider = 0; - - if(szFileName != NULL) - { - // - // Determine the stream provider - // - - if(!_tcsnicmp(szFileName, _T("flat-"), 5)) - { - dwProvider |= STREAM_PROVIDER_FLAT; - nPrefixLength1 = 5; - } - - else if(!_tcsnicmp(szFileName, _T("part-"), 5)) - { - dwProvider |= STREAM_PROVIDER_PARTIAL; - nPrefixLength1 = 5; - } - - else if(!_tcsnicmp(szFileName, _T("mpqe-"), 5)) - { - dwProvider |= STREAM_PROVIDER_MPQE; - nPrefixLength1 = 5; - } - - else if(!_tcsnicmp(szFileName, _T("blk4-"), 5)) - { - dwProvider |= STREAM_PROVIDER_BLOCK4; - nPrefixLength1 = 5; - } - - // - // Determine the base provider - // - - if(!_tcsnicmp(szFileName+nPrefixLength1, _T("file:"), 5)) - { - dwProvider |= BASE_PROVIDER_FILE; - nPrefixLength2 = 5; - } - - else if(!_tcsnicmp(szFileName+nPrefixLength1, _T("map:"), 4)) - { - dwProvider |= BASE_PROVIDER_MAP; - nPrefixLength2 = 4; - } - - else if(!_tcsnicmp(szFileName+nPrefixLength1, _T("http:"), 5)) - { - dwProvider |= BASE_PROVIDER_HTTP; - nPrefixLength2 = 5; - } - - // Only accept stream provider if we recognized the base provider - if(nPrefixLength2 != 0) - { - // It is also allowed to put "//" after the base provider, e.g. "file://", "http://" - if(szFileName[nPrefixLength1+nPrefixLength2] == '/' && szFileName[nPrefixLength1+nPrefixLength2+1] == '/') - nPrefixLength2 += 2; - - if(pdwProvider != NULL) - *pdwProvider = dwProvider; - return nPrefixLength1 + nPrefixLength2; - } - } - - return 0; -} - -/** - * Sets a download callback. Whenever the stream needs to download one or more blocks - * from the server, the callback is called - * - * \a pStream Pointer to an open stream - * \a pfnCallback Pointer to callback function - * \a pvUserData Arbitrary user pointer passed to the download callback - */ - -bool FileStream_SetCallback(TFileStream * pStream, SFILE_DOWNLOAD_CALLBACK pfnCallback, void * pvUserData) -{ - TBlockStream * pBlockStream = (TBlockStream *)pStream; - - if(pStream->BlockRead == NULL) - { - SetLastError(ERROR_NOT_SUPPORTED); - return false; - } - - pBlockStream->pfnCallback = pfnCallback; - pBlockStream->UserData = pvUserData; - return true; -} - -/** - * This function gives the block map. The 'pvBitmap' pointer must point to a buffer - * of at least sizeof(STREAM_BLOCK_MAP) size. It can also have size of the complete - * block map (i.e. sizeof(STREAM_BLOCK_MAP) + BitmapSize). In that case, the function - * also copies the bit-based block map. - * - * \a pStream Pointer to an open stream - * \a pvBitmap Pointer to buffer where the block map will be stored - * \a cbBitmap Length of the buffer, of the block map - * \a cbLengthNeeded Length of the bitmap, in bytes - */ - -bool FileStream_GetBitmap(TFileStream * pStream, void * pvBitmap, DWORD cbBitmap, DWORD * pcbLengthNeeded) -{ - TStreamBitmap * pBitmap = (TStreamBitmap *)pvBitmap; - TBlockStream * pBlockStream = (TBlockStream *)pStream; - ULONGLONG BlockOffset; - LPBYTE Bitmap = (LPBYTE)(pBitmap + 1); - DWORD BitmapSize; - DWORD BlockCount; - DWORD BlockSize; - bool bResult = false; - - // Retrieve the size of one block - if(pStream->BlockCheck != NULL) - { - BlockCount = pBlockStream->BlockCount; - BlockSize = pBlockStream->BlockSize; - } - else - { - BlockCount = (DWORD)((pStream->StreamSize + DEFAULT_BLOCK_SIZE - 1) / DEFAULT_BLOCK_SIZE); - BlockSize = DEFAULT_BLOCK_SIZE; - } - - // Fill-in the variables - BitmapSize = (BlockCount + 7) / 8; - - // Give the number of blocks - if(pcbLengthNeeded != NULL) - pcbLengthNeeded[0] = sizeof(TStreamBitmap) + BitmapSize; - - // If the length of the buffer is not enough - if(pvBitmap != NULL && cbBitmap != 0) - { - // Give the STREAM_BLOCK_MAP structure - if(cbBitmap >= sizeof(TStreamBitmap)) - { - pBitmap->StreamSize = pStream->StreamSize; - pBitmap->BitmapSize = BitmapSize; - pBitmap->BlockCount = BlockCount; - pBitmap->BlockSize = BlockSize; - pBitmap->IsComplete = (pStream->BlockCheck != NULL) ? pBlockStream->IsComplete : 1; - bResult = true; - } - - // Give the block bitmap, if enough space - if(cbBitmap >= sizeof(TStreamBitmap) + BitmapSize) - { - // Version with bitmap present - if(pStream->BlockCheck != NULL) - { - DWORD ByteIndex = 0; - BYTE BitMask = 0x01; - - // Initialize the map with zeros - memset(Bitmap, 0, BitmapSize); - - // Fill the map - for(BlockOffset = 0; BlockOffset < pStream->StreamSize; BlockOffset += BlockSize) - { - // Set the bit if the block is present - if(pBlockStream->BlockCheck(pStream, BlockOffset)) - Bitmap[ByteIndex] |= BitMask; - - // Move bit position - ByteIndex += (BitMask >> 0x07); - BitMask = (BitMask >> 0x07) | (BitMask << 0x01); - } - } - else - { - memset(Bitmap, 0xFF, BitmapSize); - } - } - } - - // Set last error value and return - if(bResult == false) - SetLastError(ERROR_INSUFFICIENT_BUFFER); - return bResult; -} - -/** - * Reads data from the stream - * - * - Returns true if the read operation succeeded and all bytes have been read - * - Returns false if either read failed or not all bytes have been read - * - If the pByteOffset is NULL, the function must read the data from the current file position - * - The function can be called with dwBytesToRead = 0. In that case, pvBuffer is ignored - * and the function just adjusts file pointer. - * - * \a pStream Pointer to an open stream - * \a pByteOffset Pointer to file byte offset. If NULL, it reads from the current position - * \a pvBuffer Pointer to data to be read - * \a dwBytesToRead Number of bytes to read from the file - * - * \returns - * - If the function reads the required amount of bytes, it returns true. - * - If the function reads less than required bytes, it returns false and GetLastError() returns ERROR_HANDLE_EOF - * - If the function fails, it reads false and GetLastError() returns an error code different from ERROR_HANDLE_EOF - */ -bool FileStream_Read(TFileStream * pStream, ULONGLONG * pByteOffset, void * pvBuffer, DWORD dwBytesToRead) -{ - assert(pStream->StreamRead != NULL); - return pStream->StreamRead(pStream, pByteOffset, pvBuffer, dwBytesToRead); -} - -/** - * This function writes data to the stream - * - * - Returns true if the write operation succeeded and all bytes have been written - * - Returns false if either write failed or not all bytes have been written - * - If the pByteOffset is NULL, the function must write the data to the current file position - * - * \a pStream Pointer to an open stream - * \a pByteOffset Pointer to file byte offset. If NULL, it reads from the current position - * \a pvBuffer Pointer to data to be written - * \a dwBytesToWrite Number of bytes to write to the file - */ -bool FileStream_Write(TFileStream * pStream, ULONGLONG * pByteOffset, const void * pvBuffer, DWORD dwBytesToWrite) -{ - if(pStream->dwFlags & STREAM_FLAG_READ_ONLY) - { - SetLastError(ERROR_ACCESS_DENIED); - return false; - } - - assert(pStream->StreamWrite != NULL); - return pStream->StreamWrite(pStream, pByteOffset, pvBuffer, dwBytesToWrite); -} - -/** - * Returns the size of a file - * - * \a pStream Pointer to an open stream - * \a FileSize Pointer where to store the file size - */ -bool FileStream_GetSize(TFileStream * pStream, ULONGLONG * pFileSize) -{ - assert(pStream->StreamGetSize != NULL); - return pStream->StreamGetSize(pStream, pFileSize); -} - -/** - * Sets the size of a file - * - * \a pStream Pointer to an open stream - * \a NewFileSize File size to set - */ -bool FileStream_SetSize(TFileStream * pStream, ULONGLONG NewFileSize) -{ - if(pStream->dwFlags & STREAM_FLAG_READ_ONLY) - { - SetLastError(ERROR_ACCESS_DENIED); - return false; - } - - assert(pStream->StreamResize != NULL); - return pStream->StreamResize(pStream, NewFileSize); -} - -/** - * This function returns the current file position - * \a pStream - * \a pByteOffset - */ -bool FileStream_GetPos(TFileStream * pStream, ULONGLONG * pByteOffset) -{ - assert(pStream->StreamGetPos != NULL); - return pStream->StreamGetPos(pStream, pByteOffset); -} - -/** - * Returns the last write time of a file - * - * \a pStream Pointer to an open stream - * \a pFileType Pointer where to store the file last write time - */ -bool FileStream_GetTime(TFileStream * pStream, ULONGLONG * pFileTime) -{ - // Just use the saved filetime value - *pFileTime = pStream->Base.File.FileTime; - return true; -} - -/** - * Returns the stream flags - * - * \a pStream Pointer to an open stream - * \a pdwStreamFlags Pointer where to store the stream flags - */ -bool FileStream_GetFlags(TFileStream * pStream, LPDWORD pdwStreamFlags) -{ - *pdwStreamFlags = pStream->dwFlags; - return true; -} - -/** - * Switches a stream with another. Used for final phase of archive compacting. - * Performs these steps: - * - * 1) Closes the handle to the existing MPQ - * 2) Renames the temporary MPQ to the original MPQ, overwrites existing one - * 3) Opens the MPQ stores the handle and stream position to the new stream structure - * - * \a pStream Pointer to an open stream - * \a pNewStream Temporary ("working") stream (created during archive compacting) - */ -bool FileStream_Replace(TFileStream * pStream, TFileStream * pNewStream) -{ - // Only supported on flat files - if((pStream->dwFlags & STREAM_PROVIDERS_MASK) != (STREAM_PROVIDER_FLAT | BASE_PROVIDER_FILE)) - { - SetLastError(ERROR_NOT_SUPPORTED); - return false; - } - - // Not supported on read-only streams - if(pStream->dwFlags & STREAM_FLAG_READ_ONLY) - { - SetLastError(ERROR_ACCESS_DENIED); - return false; - } - - // Close both stream's base providers - pNewStream->BaseClose(pNewStream); - pStream->BaseClose(pStream); - - // Now we have to delete the (now closed) old file and rename the new file - if(!BaseFile_Replace(pStream, pNewStream)) - return false; - - // Now open the base file again - if(!BaseFile_Open(pStream, pStream->szFileName, pStream->dwFlags)) - return false; - - // Cleanup the new stream - FileStream_Close(pNewStream); - return true; -} - -/** - * This function closes an archive file and frees any data buffers - * that have been allocated for stream management. The function must also - * support partially allocated structure, i.e. one or more buffers - * can be NULL, if there was an allocation failure during the process - * - * \a pStream Pointer to an open stream - */ -void FileStream_Close(TFileStream * pStream) -{ - // Check if the stream structure is allocated at all - if(pStream != NULL) - { - // Free the master stream, if any - if(pStream->pMaster != NULL) - FileStream_Close(pStream->pMaster); - pStream->pMaster = NULL; - - // Close the stream provider. - if(pStream->StreamClose != NULL) - pStream->StreamClose(pStream); - - // Also close base stream, if any - else if(pStream->BaseClose != NULL) - pStream->BaseClose(pStream); - - // Free the stream itself - STORM_FREE(pStream); - } -} - -//----------------------------------------------------------------------------- -// Utility functions (ANSI) - -const char * GetPlainFileName(const char * szFileName) -{ - const char * szPlainName = szFileName; - - while(*szFileName != 0) - { - if(*szFileName == '\\' || *szFileName == '/') - szPlainName = szFileName + 1; - szFileName++; - } - - return szPlainName; -} - -void CopyFileName(char * szTarget, const char * szSource, size_t cchLength) -{ - memcpy(szTarget, szSource, cchLength); - szTarget[cchLength] = 0; -} - -//----------------------------------------------------------------------------- -// Utility functions (UNICODE) only exist in the ANSI version of the library -// In ANSI builds, TCHAR = char, so we don't need these functions implemented - -#ifdef _UNICODE -const TCHAR * GetPlainFileName(const TCHAR * szFileName) -{ - const TCHAR * szPlainName = szFileName; - - while(*szFileName != 0) - { - if(*szFileName == '\\' || *szFileName == '/') - szPlainName = szFileName + 1; - szFileName++; - } - - return szPlainName; -} - -void CopyFileName(TCHAR * szTarget, const char * szSource, size_t cchLength) -{ - mbstowcs(szTarget, szSource, cchLength); - szTarget[cchLength] = 0; -} - -void CopyFileName(char * szTarget, const TCHAR * szSource, size_t cchLength) -{ - wcstombs(szTarget, szSource, cchLength); - szTarget[cchLength] = 0; -} -#endif +/*****************************************************************************/ +/* FileStream.cpp Copyright (c) Ladislav Zezula 2010 */ +/*---------------------------------------------------------------------------*/ +/* File stream support for StormLib */ +/* */ +/* Windows support: Written by Ladislav Zezula */ +/* Mac support: Written by Sam Wilkins */ +/* Linux support: Written by Sam Wilkins and Ivan Komissarov */ +/* Big-endian: Written & debugged by Sam Wilkins */ +/*---------------------------------------------------------------------------*/ +/* Date Ver Who Comment */ +/* -------- ---- --- ------- */ +/* 11.06.10 1.00 Lad Derived from StormPortMac.cpp and StormPortLinux.cpp */ +/*****************************************************************************/ + +#define __STORMLIB_SELF__ +#include "StormLib.h" +#include "StormCommon.h" +#include "FileStream.h" + +#ifdef _MSC_VER +#pragma comment(lib, "wininet.lib") // Internet functions for HTTP stream +#pragma warning(disable: 4800) // 'BOOL' : forcing value to bool 'true' or 'false' (performance warning) +#endif + +//----------------------------------------------------------------------------- +// Local defines + +#ifndef INVALID_HANDLE_VALUE +#define INVALID_HANDLE_VALUE ((HANDLE)-1) +#endif + +//----------------------------------------------------------------------------- +// Local functions - platform-specific functions + +#ifndef PLATFORM_WINDOWS +static int nLastError = ERROR_SUCCESS; + +int GetLastError() +{ + return nLastError; +} + +void SetLastError(int nError) +{ + nLastError = nError; +} +#endif + +static DWORD StringToInt(const char * szString) +{ + DWORD dwValue = 0; + + while('0' <= szString[0] && szString[0] <= '9') + { + dwValue = (dwValue * 10) + (szString[0] - '9'); + szString++; + } + + return dwValue; +} + +//----------------------------------------------------------------------------- +// Dummy init function + +static void BaseNone_Init(TFileStream *) +{ + // Nothing here +} + +//----------------------------------------------------------------------------- +// Local functions - base file support + +static bool BaseFile_Create(TFileStream * pStream) +{ +#ifdef PLATFORM_WINDOWS + { + DWORD dwWriteShare = (pStream->dwFlags & STREAM_FLAG_WRITE_SHARE) ? FILE_SHARE_WRITE : 0; + + pStream->Base.File.hFile = CreateFile(pStream->szFileName, + GENERIC_READ | GENERIC_WRITE, + dwWriteShare | FILE_SHARE_READ, + NULL, + CREATE_ALWAYS, + 0, + NULL); + if(pStream->Base.File.hFile == INVALID_HANDLE_VALUE) + return false; + } +#endif + +#if defined(PLATFORM_MAC) || defined(PLATFORM_LINUX) + { + intptr_t handle; + + handle = open(pStream->szFileName, O_RDWR | O_CREAT | O_TRUNC | O_LARGEFILE, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + if(handle == -1) + { + nLastError = errno; + return false; + } + + pStream->Base.File.hFile = (HANDLE)handle; + } +#endif + + // Reset the file size and position + pStream->Base.File.FileSize = 0; + pStream->Base.File.FilePos = 0; + return true; +} + +static bool BaseFile_Open(TFileStream * pStream, const TCHAR * szFileName, DWORD dwStreamFlags) +{ +#ifdef PLATFORM_WINDOWS + { + ULARGE_INTEGER FileSize; + DWORD dwWriteAccess = (dwStreamFlags & STREAM_FLAG_READ_ONLY) ? 0 : FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_WRITE_ATTRIBUTES; + DWORD dwWriteShare = (dwStreamFlags & STREAM_FLAG_WRITE_SHARE) ? FILE_SHARE_WRITE : 0; + + // Open the file + pStream->Base.File.hFile = CreateFile(szFileName, + FILE_READ_DATA | FILE_READ_ATTRIBUTES | dwWriteAccess, + FILE_SHARE_READ | dwWriteShare, + NULL, + OPEN_EXISTING, + 0, + NULL); + if(pStream->Base.File.hFile == INVALID_HANDLE_VALUE) + return false; + + // Query the file size + FileSize.LowPart = GetFileSize(pStream->Base.File.hFile, &FileSize.HighPart); + pStream->Base.File.FileSize = FileSize.QuadPart; + + // Query last write time + GetFileTime(pStream->Base.File.hFile, NULL, NULL, (LPFILETIME)&pStream->Base.File.FileTime); + } +#endif + +#if defined(PLATFORM_MAC) || defined(PLATFORM_LINUX) + { + struct stat64 fileinfo; + int oflag = (dwStreamFlags & STREAM_FLAG_READ_ONLY) ? O_RDONLY : O_RDWR; + intptr_t handle; + + // Open the file + handle = open(szFileName, oflag | O_LARGEFILE); + if(handle == -1) + { + nLastError = errno; + return false; + } + + // Get the file size + if(fstat64(handle, &fileinfo) == -1) + { + nLastError = errno; + return false; + } + + // time_t is number of seconds since 1.1.1970, UTC. + // 1 second = 10000000 (decimal) in FILETIME + // Set the start to 1.1.1970 00:00:00 + pStream->Base.File.FileTime = 0x019DB1DED53E8000ULL + (10000000 * fileinfo.st_mtime); + pStream->Base.File.FileSize = (ULONGLONG)fileinfo.st_size; + pStream->Base.File.hFile = (HANDLE)handle; + } +#endif + + // Reset the file position + pStream->Base.File.FilePos = 0; + return true; +} + +static bool BaseFile_Read( + TFileStream * pStream, // Pointer to an open stream + ULONGLONG * pByteOffset, // Pointer to file byte offset. If NULL, it reads from the current position + void * pvBuffer, // Pointer to data to be read + DWORD dwBytesToRead) // Number of bytes to read from the file +{ + ULONGLONG ByteOffset = (pByteOffset != NULL) ? *pByteOffset : pStream->Base.File.FilePos; + DWORD dwBytesRead = 0; // Must be set by platform-specific code + +#ifdef PLATFORM_WINDOWS + { + // Note: StormLib no longer supports Windows 9x. + // Thus, we can use the OVERLAPPED structure to specify + // file offset to read from file. This allows us to skip + // one system call to SetFilePointer + + // Update the byte offset + pStream->Base.File.FilePos = ByteOffset; + + // Read the data + if(dwBytesToRead != 0) + { + OVERLAPPED Overlapped; + + Overlapped.OffsetHigh = (DWORD)(ByteOffset >> 32); + Overlapped.Offset = (DWORD)ByteOffset; + Overlapped.hEvent = NULL; + if(!ReadFile(pStream->Base.File.hFile, pvBuffer, dwBytesToRead, &dwBytesRead, &Overlapped)) + return false; + } + } +#endif + +#if defined(PLATFORM_MAC) || defined(PLATFORM_LINUX) + { + ssize_t bytes_read; + + // If the byte offset is different from the current file position, + // we have to update the file position xxx + if(ByteOffset != pStream->Base.File.FilePos) + { + lseek64((intptr_t)pStream->Base.File.hFile, (off64_t)(ByteOffset), SEEK_SET); + pStream->Base.File.FilePos = ByteOffset; + } + + // Perform the read operation + if(dwBytesToRead != 0) + { + bytes_read = read((intptr_t)pStream->Base.File.hFile, pvBuffer, (size_t)dwBytesToRead); + if(bytes_read == -1) + { + nLastError = errno; + return false; + } + + dwBytesRead = (DWORD)(size_t)bytes_read; + } + } +#endif + + // Increment the current file position by number of bytes read + // If the number of bytes read doesn't match to required amount, return false + pStream->Base.File.FilePos = ByteOffset + dwBytesRead; + if(dwBytesRead != dwBytesToRead) + SetLastError(ERROR_HANDLE_EOF); + return (dwBytesRead == dwBytesToRead); +} + +/** + * \a pStream Pointer to an open stream + * \a pByteOffset Pointer to file byte offset. If NULL, writes to current position + * \a pvBuffer Pointer to data to be written + * \a dwBytesToWrite Number of bytes to write to the file + */ + +static bool BaseFile_Write(TFileStream * pStream, ULONGLONG * pByteOffset, const void * pvBuffer, DWORD dwBytesToWrite) +{ + ULONGLONG ByteOffset = (pByteOffset != NULL) ? *pByteOffset : pStream->Base.File.FilePos; + DWORD dwBytesWritten = 0; // Must be set by platform-specific code + +#ifdef PLATFORM_WINDOWS + { + // Note: StormLib no longer supports Windows 9x. + // Thus, we can use the OVERLAPPED structure to specify + // file offset to read from file. This allows us to skip + // one system call to SetFilePointer + + // Update the byte offset + pStream->Base.File.FilePos = ByteOffset; + + // Read the data + if(dwBytesToWrite != 0) + { + OVERLAPPED Overlapped; + + Overlapped.OffsetHigh = (DWORD)(ByteOffset >> 32); + Overlapped.Offset = (DWORD)ByteOffset; + Overlapped.hEvent = NULL; + if(!WriteFile(pStream->Base.File.hFile, pvBuffer, dwBytesToWrite, &dwBytesWritten, &Overlapped)) + return false; + } + } +#endif + +#if defined(PLATFORM_MAC) || defined(PLATFORM_LINUX) + { + ssize_t bytes_written; + + // If the byte offset is different from the current file position, + // we have to update the file position + if(ByteOffset != pStream->Base.File.FilePos) + { + lseek64((intptr_t)pStream->Base.File.hFile, (off64_t)(ByteOffset), SEEK_SET); + pStream->Base.File.FilePos = ByteOffset; + } + + // Perform the read operation + bytes_written = write((intptr_t)pStream->Base.File.hFile, pvBuffer, (size_t)dwBytesToWrite); + if(bytes_written == -1) + { + nLastError = errno; + return false; + } + + dwBytesWritten = (DWORD)(size_t)bytes_written; + } +#endif + + // Increment the current file position by number of bytes read + pStream->Base.File.FilePos = ByteOffset + dwBytesWritten; + + // Also modify the file size, if needed + if(pStream->Base.File.FilePos > pStream->Base.File.FileSize) + pStream->Base.File.FileSize = pStream->Base.File.FilePos; + + if(dwBytesWritten != dwBytesToWrite) + SetLastError(ERROR_DISK_FULL); + return (dwBytesWritten == dwBytesToWrite); +} + +/** + * \a pStream Pointer to an open stream + * \a NewFileSize New size of the file + */ +static bool BaseFile_Resize(TFileStream * pStream, ULONGLONG NewFileSize) +{ +#ifdef PLATFORM_WINDOWS + { + LONG FileSizeHi = (LONG)(NewFileSize >> 32); + LONG FileSizeLo; + DWORD dwNewPos; + bool bResult; + + // Set the position at the new file size + dwNewPos = SetFilePointer(pStream->Base.File.hFile, (LONG)NewFileSize, &FileSizeHi, FILE_BEGIN); + if(dwNewPos == INVALID_SET_FILE_POINTER && GetLastError() != ERROR_SUCCESS) + return false; + + // Set the current file pointer as the end of the file + bResult = (bool)SetEndOfFile(pStream->Base.File.hFile); + if(bResult) + pStream->Base.File.FileSize = NewFileSize; + + // Restore the file position + FileSizeHi = (LONG)(pStream->Base.File.FilePos >> 32); + FileSizeLo = (LONG)(pStream->Base.File.FilePos); + SetFilePointer(pStream->Base.File.hFile, FileSizeLo, &FileSizeHi, FILE_BEGIN); + return bResult; + } +#endif + +#if defined(PLATFORM_MAC) || defined(PLATFORM_LINUX) + { + if(ftruncate64((intptr_t)pStream->Base.File.hFile, (off64_t)NewFileSize) == -1) + { + nLastError = errno; + return false; + } + + pStream->Base.File.FileSize = NewFileSize; + return true; + } +#endif +} + +// Gives the current file size +static bool BaseFile_GetSize(TFileStream * pStream, ULONGLONG * pFileSize) +{ + // Note: Used by all thre base providers. + // Requires the TBaseData union to have the same layout for all three base providers + *pFileSize = pStream->Base.File.FileSize; + return true; +} + +// Gives the current file position +static bool BaseFile_GetPos(TFileStream * pStream, ULONGLONG * pByteOffset) +{ + // Note: Used by all thre base providers. + // Requires the TBaseData union to have the same layout for all three base providers + *pByteOffset = pStream->Base.File.FilePos; + return true; +} + +// Renames the file pointed by pStream so that it contains data from pNewStream +static bool BaseFile_Replace(TFileStream * pStream, TFileStream * pNewStream) +{ +#ifdef PLATFORM_WINDOWS + // Delete the original stream file. Don't check the result value, + // because if the file doesn't exist, it would fail + DeleteFile(pStream->szFileName); + + // Rename the new file to the old stream's file + return (bool)MoveFile(pNewStream->szFileName, pStream->szFileName); +#endif + +#if defined(PLATFORM_MAC) || defined(PLATFORM_LINUX) + // "rename" on Linux also works if the target file exists + if(rename(pNewStream->szFileName, pStream->szFileName) == -1) + { + nLastError = errno; + return false; + } + + return true; +#endif +} + +static void BaseFile_Close(TFileStream * pStream) +{ + if(pStream->Base.File.hFile != INVALID_HANDLE_VALUE) + { +#ifdef PLATFORM_WINDOWS + CloseHandle(pStream->Base.File.hFile); +#endif + +#if defined(PLATFORM_MAC) || defined(PLATFORM_LINUX) + close((intptr_t)pStream->Base.File.hFile); +#endif + } + + // Also invalidate the handle + pStream->Base.File.hFile = INVALID_HANDLE_VALUE; +} + +// Initializes base functions for the disk file +static void BaseFile_Init(TFileStream * pStream) +{ + pStream->BaseCreate = BaseFile_Create; + pStream->BaseOpen = BaseFile_Open; + pStream->BaseRead = BaseFile_Read; + pStream->BaseWrite = BaseFile_Write; + pStream->BaseResize = BaseFile_Resize; + pStream->BaseGetSize = BaseFile_GetSize; + pStream->BaseGetPos = BaseFile_GetPos; + pStream->BaseClose = BaseFile_Close; +} + +//----------------------------------------------------------------------------- +// Local functions - base memory-mapped file support + +static bool BaseMap_Open(TFileStream * pStream, const TCHAR * szFileName, DWORD dwStreamFlags) +{ +#ifdef PLATFORM_WINDOWS + + ULARGE_INTEGER FileSize; + HANDLE hFile; + HANDLE hMap; + bool bResult = false; + + // Keep compiler happy + dwStreamFlags = dwStreamFlags; + + // Open the file for read access + hFile = CreateFile(szFileName, FILE_READ_DATA, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); + if(hFile != INVALID_HANDLE_VALUE) + { + // Retrieve file size. Don't allow mapping file of a zero size. + FileSize.LowPart = GetFileSize(hFile, &FileSize.HighPart); + if(FileSize.QuadPart != 0) + { + // Now create mapping object + hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL); + if(hMap != NULL) + { + // Map the entire view into memory + // Note that this operation will fail if the file can't fit + // into usermode address space + pStream->Base.Map.pbFile = (LPBYTE)MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0); + if(pStream->Base.Map.pbFile != NULL) + { + // Retrieve file time + GetFileTime(hFile, NULL, NULL, (LPFILETIME)&pStream->Base.Map.FileTime); + + // Retrieve file size and position + pStream->Base.Map.FileSize = FileSize.QuadPart; + pStream->Base.Map.FilePos = 0; + bResult = true; + } + + // Close the map handle + CloseHandle(hMap); + } + } + + // Close the file handle + CloseHandle(hFile); + } + + // If the file is not there and is not available for random access, + // report error + if(bResult == false) + return false; +#endif + +#if defined(PLATFORM_MAC) || defined(PLATFORM_LINUX) + struct stat64 fileinfo; + intptr_t handle; + bool bResult = false; + + // Open the file + handle = open(szFileName, O_RDONLY); + if(handle != -1) + { + // Get the file size + if(fstat64(handle, &fileinfo) != -1) + { + pStream->Base.Map.pbFile = (LPBYTE)mmap(NULL, (size_t)fileinfo.st_size, PROT_READ, MAP_PRIVATE, handle, 0); + if(pStream->Base.Map.pbFile != NULL) + { + // time_t is number of seconds since 1.1.1970, UTC. + // 1 second = 10000000 (decimal) in FILETIME + // Set the start to 1.1.1970 00:00:00 + pStream->Base.Map.FileTime = 0x019DB1DED53E8000ULL + (10000000 * fileinfo.st_mtime); + pStream->Base.Map.FileSize = (ULONGLONG)fileinfo.st_size; + pStream->Base.Map.FilePos = 0; + bResult = true; + } + } + close(handle); + } + + // Did the mapping fail? + if(bResult == false) + { + nLastError = errno; + return false; + } +#endif + + return true; +} + +static bool BaseMap_Read( + TFileStream * pStream, // Pointer to an open stream + ULONGLONG * pByteOffset, // Pointer to file byte offset. If NULL, it reads from the current position + void * pvBuffer, // Pointer to data to be read + DWORD dwBytesToRead) // Number of bytes to read from the file +{ + ULONGLONG ByteOffset = (pByteOffset != NULL) ? *pByteOffset : pStream->Base.Map.FilePos; + + // Do we have to read anything at all? + if(dwBytesToRead != 0) + { + // Don't allow reading past file size + if((ByteOffset + dwBytesToRead) > pStream->Base.Map.FileSize) + return false; + + // Copy the required data + memcpy(pvBuffer, pStream->Base.Map.pbFile + (size_t)ByteOffset, dwBytesToRead); + } + + // Move the current file position + pStream->Base.Map.FilePos += dwBytesToRead; + return true; +} + +static void BaseMap_Close(TFileStream * pStream) +{ +#ifdef PLATFORM_WINDOWS + if(pStream->Base.Map.pbFile != NULL) + UnmapViewOfFile(pStream->Base.Map.pbFile); +#endif + +#if defined(PLATFORM_MAC) || defined(PLATFORM_LINUX) + if(pStream->Base.Map.pbFile != NULL) + munmap(pStream->Base.Map.pbFile, (size_t )pStream->Base.Map.FileSize); +#endif + + pStream->Base.Map.pbFile = NULL; +} + +// Initializes base functions for the mapped file +static void BaseMap_Init(TFileStream * pStream) +{ + // Supply the file stream functions + pStream->BaseOpen = BaseMap_Open; + pStream->BaseRead = BaseMap_Read; + pStream->BaseGetSize = BaseFile_GetSize; // Reuse BaseFile function + pStream->BaseGetPos = BaseFile_GetPos; // Reuse BaseFile function + pStream->BaseClose = BaseMap_Close; + + // Mapped files are read-only + pStream->dwFlags |= STREAM_FLAG_READ_ONLY; +} + +//----------------------------------------------------------------------------- +// Local functions - base HTTP file support + +static const TCHAR * BaseHttp_ExtractServerName(const TCHAR * szFileName, TCHAR * szServerName) +{ + // Check for HTTP + if(!_tcsnicmp(szFileName, _T("http://"), 7)) + szFileName += 7; + + // Cut off the server name + if(szServerName != NULL) + { + while(szFileName[0] != 0 && szFileName[0] != _T('/')) + *szServerName++ = *szFileName++; + *szServerName = 0; + } + else + { + while(szFileName[0] != 0 && szFileName[0] != _T('/')) + szFileName++; + } + + // Return the remainder + return szFileName; +} + +static bool BaseHttp_Open(TFileStream * pStream, const TCHAR * szFileName, DWORD dwStreamFlags) +{ +#ifdef PLATFORM_WINDOWS + + HINTERNET hRequest; + DWORD dwTemp = 0; + bool bFileAvailable = false; + int nError = ERROR_SUCCESS; + + // Keep compiler happy + dwStreamFlags = dwStreamFlags; + + // Don't connect to the internet + if(!InternetGetConnectedState(&dwTemp, 0)) + return false; + + // Initiate the connection to the internet + pStream->Base.Http.hInternet = InternetOpen(_T("StormLib HTTP MPQ reader"), + INTERNET_OPEN_TYPE_PRECONFIG, + NULL, + NULL, + 0); + if(pStream->Base.Http.hInternet == NULL) + return false; + + // Connect to the server + if(nError == ERROR_SUCCESS) + { + TCHAR szServerName[MAX_PATH]; + DWORD dwFlags = INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_NO_UI | INTERNET_FLAG_NO_CACHE_WRITE; + + // Initiate connection with the server + szFileName = BaseHttp_ExtractServerName(szFileName, szServerName); + pStream->Base.Http.hConnect = InternetConnect(pStream->Base.Http.hInternet, + szServerName, + INTERNET_DEFAULT_HTTP_PORT, + NULL, + NULL, + INTERNET_SERVICE_HTTP, + dwFlags, + 0); + if(pStream->Base.Http.hConnect == NULL) + { + InternetCloseHandle(pStream->Base.Http.hInternet); + return false; + } + } + + // Now try to query the file size + if(nError == ERROR_SUCCESS) + { + // Open HTTP request to the file + hRequest = HttpOpenRequest(pStream->Base.Http.hConnect, _T("GET"), szFileName, NULL, NULL, NULL, INTERNET_FLAG_NO_CACHE_WRITE, 0); + if(hRequest != NULL) + { + if(HttpSendRequest(hRequest, NULL, 0, NULL, 0)) + { + ULONGLONG FileTime = 0; + DWORD dwFileSize = 0; + DWORD dwDataSize; + DWORD dwIndex = 0; + + // Check if the MPQ has Last Modified field + dwDataSize = sizeof(ULONGLONG); + if(HttpQueryInfo(hRequest, HTTP_QUERY_LAST_MODIFIED | HTTP_QUERY_FLAG_SYSTEMTIME, &FileTime, &dwDataSize, &dwIndex)) + pStream->Base.Http.FileTime = FileTime; + + // Verify if the server supports random access + dwDataSize = sizeof(DWORD); + if(HttpQueryInfo(hRequest, HTTP_QUERY_CONTENT_LENGTH | HTTP_QUERY_FLAG_NUMBER, &dwFileSize, &dwDataSize, &dwIndex)) + { + if(dwFileSize != 0) + { + pStream->Base.Http.FileSize = dwFileSize; + pStream->Base.Http.FilePos = 0; + bFileAvailable = true; + } + } + } + InternetCloseHandle(hRequest); + } + } + + // If the file is not there and is not available for random access, + // report error + if(bFileAvailable == false) + { + pStream->BaseClose(pStream); + return false; + } + + return true; + +#else + + // Not supported + SetLastError(ERROR_NOT_SUPPORTED); + pStream = pStream; + return false; + +#endif +} + +static bool BaseHttp_Read( + TFileStream * pStream, // Pointer to an open stream + ULONGLONG * pByteOffset, // Pointer to file byte offset. If NULL, it reads from the current position + void * pvBuffer, // Pointer to data to be read + DWORD dwBytesToRead) // Number of bytes to read from the file +{ +#ifdef PLATFORM_WINDOWS + ULONGLONG ByteOffset = (pByteOffset != NULL) ? *pByteOffset : pStream->Base.Http.FilePos; + DWORD dwTotalBytesRead = 0; + + // Do we have to read anything at all? + if(dwBytesToRead != 0) + { + HINTERNET hRequest; + LPCTSTR szFileName; + LPBYTE pbBuffer = (LPBYTE)pvBuffer; + TCHAR szRangeRequest[0x80]; + DWORD dwStartOffset = (DWORD)ByteOffset; + DWORD dwEndOffset = dwStartOffset + dwBytesToRead; + + // Open HTTP request to the file + szFileName = BaseHttp_ExtractServerName(pStream->szFileName, NULL); + hRequest = HttpOpenRequest(pStream->Base.Http.hConnect, _T("GET"), szFileName, NULL, NULL, NULL, INTERNET_FLAG_NO_CACHE_WRITE, 0); + if(hRequest != NULL) + { + // Add range request to the HTTP headers + // http://www.clevercomponents.com/articles/article015/resuming.asp + _stprintf(szRangeRequest, _T("Range: bytes=%u-%u"), (unsigned int)dwStartOffset, (unsigned int)dwEndOffset); + HttpAddRequestHeaders(hRequest, szRangeRequest, 0xFFFFFFFF, HTTP_ADDREQ_FLAG_ADD_IF_NEW); + + // Send the request to the server + if(HttpSendRequest(hRequest, NULL, 0, NULL, 0)) + { + while(dwTotalBytesRead < dwBytesToRead) + { + DWORD dwBlockBytesToRead = dwBytesToRead - dwTotalBytesRead; + DWORD dwBlockBytesRead = 0; + + // Read the block from the file + if(dwBlockBytesToRead > 0x200) + dwBlockBytesToRead = 0x200; + InternetReadFile(hRequest, pbBuffer, dwBlockBytesToRead, &dwBlockBytesRead); + + // Check for end + if(dwBlockBytesRead == 0) + break; + + // Move buffers + dwTotalBytesRead += dwBlockBytesRead; + pbBuffer += dwBlockBytesRead; + } + } + InternetCloseHandle(hRequest); + } + } + + // Increment the current file position by number of bytes read + pStream->Base.Http.FilePos = ByteOffset + dwTotalBytesRead; + + // If the number of bytes read doesn't match the required amount, return false + if(dwTotalBytesRead != dwBytesToRead) + SetLastError(ERROR_HANDLE_EOF); + return (dwTotalBytesRead == dwBytesToRead); + +#else + + // Not supported + pStream = pStream; + pByteOffset = pByteOffset; + pvBuffer = pvBuffer; + dwBytesToRead = dwBytesToRead; + SetLastError(ERROR_NOT_SUPPORTED); + return false; + +#endif +} + +static void BaseHttp_Close(TFileStream * pStream) +{ +#ifdef PLATFORM_WINDOWS + if(pStream->Base.Http.hConnect != NULL) + InternetCloseHandle(pStream->Base.Http.hConnect); + pStream->Base.Http.hConnect = NULL; + + if(pStream->Base.Http.hInternet != NULL) + InternetCloseHandle(pStream->Base.Http.hInternet); + pStream->Base.Http.hInternet = NULL; +#else + pStream = pStream; +#endif +} + +// Initializes base functions for the mapped file +static void BaseHttp_Init(TFileStream * pStream) +{ + // Supply the stream functions + pStream->BaseOpen = BaseHttp_Open; + pStream->BaseRead = BaseHttp_Read; + pStream->BaseGetSize = BaseFile_GetSize; // Reuse BaseFile function + pStream->BaseGetPos = BaseFile_GetPos; // Reuse BaseFile function + pStream->BaseClose = BaseHttp_Close; + + // HTTP files are read-only + pStream->dwFlags |= STREAM_FLAG_READ_ONLY; +} + +//----------------------------------------------------------------------------- +// Local functions - base block-based support + +// Generic function that loads blocks from the file +// The function groups the block with the same availability, +// so the called BlockRead can finish the request in a single system call +static bool BlockStream_Read( + TBlockStream * pStream, // Pointer to an open stream + ULONGLONG * pByteOffset, // Pointer to file byte offset. If NULL, it reads from the current position + void * pvBuffer, // Pointer to data to be read + DWORD dwBytesToRead) // Number of bytes to read from the file +{ + ULONGLONG BlockOffset0; + ULONGLONG BlockOffset; + ULONGLONG ByteOffset; + ULONGLONG EndOffset; + LPBYTE TransferBuffer; + LPBYTE BlockBuffer; + DWORD BlockBufferOffset; // Offset of the desired data in the block buffer + DWORD BytesNeeded; // Number of bytes that really need to be read + DWORD BlockSize = pStream->BlockSize; + DWORD BlockCount; + bool bPrevBlockAvailable; + bool bCallbackCalled = false; + bool bBlockAvailable; + bool bResult = true; + + // The base block read function must be present + assert(pStream->BlockRead != NULL); + + // NOP reading of zero bytes + if(dwBytesToRead == 0) + return true; + + // Get the current position in the stream + ByteOffset = (pByteOffset != NULL) ? pByteOffset[0] : pStream->StreamPos; + EndOffset = ByteOffset + dwBytesToRead; + if(EndOffset > pStream->StreamSize) + { + SetLastError(ERROR_HANDLE_EOF); + return false; + } + + // Calculate the block parameters + BlockOffset0 = BlockOffset = ByteOffset & ~((ULONGLONG)BlockSize - 1); + BlockCount = (DWORD)(((EndOffset - BlockOffset) + (BlockSize - 1)) / BlockSize); + BytesNeeded = (DWORD)(EndOffset - BlockOffset); + + // Remember where we have our data + assert((BlockSize & (BlockSize - 1)) == 0); + BlockBufferOffset = (DWORD)(ByteOffset & (BlockSize - 1)); + + // Allocate buffer for reading blocks + TransferBuffer = BlockBuffer = STORM_ALLOC(BYTE, (BlockCount * BlockSize)); + if(TransferBuffer == NULL) + { + SetLastError(ERROR_NOT_ENOUGH_MEMORY); + return false; + } + + // If all blocks are available, just read all blocks at once + if(pStream->IsComplete == 0) + { + // Now parse the blocks and send the block read request + // to all blocks with the same availability + assert(pStream->BlockCheck != NULL); + bPrevBlockAvailable = pStream->BlockCheck(pStream, BlockOffset); + + // Loop as long as we have something to read + while(BlockOffset < EndOffset) + { + // Determine availability of the next block + bBlockAvailable = pStream->BlockCheck(pStream, BlockOffset); + + // If the availability has changed, read all blocks up to this one + if(bBlockAvailable != bPrevBlockAvailable) + { + // Call the file stream callback, if the block is not available + if(pStream->pMaster && pStream->pfnCallback && bPrevBlockAvailable == false) + { + pStream->pfnCallback(pStream->UserData, BlockOffset0, (DWORD)(BlockOffset - BlockOffset0)); + bCallbackCalled = true; + } + + // Load the continuous blocks with the same availability + assert(BlockOffset > BlockOffset0); + bResult = pStream->BlockRead(pStream, BlockOffset0, BlockOffset, BlockBuffer, BytesNeeded, bPrevBlockAvailable); + if(!bResult) + break; + + // Move the block offset + BlockBuffer += (DWORD)(BlockOffset - BlockOffset0); + BytesNeeded -= (DWORD)(BlockOffset - BlockOffset0); + bPrevBlockAvailable = bBlockAvailable; + BlockOffset0 = BlockOffset; + } + + // Move to the block offset in the stream + BlockOffset += BlockSize; + } + + // If there is a block(s) remaining to be read, do it + if(BlockOffset > BlockOffset0) + { + // Call the file stream callback, if the block is not available + if(pStream->pMaster && pStream->pfnCallback && bPrevBlockAvailable == false) + { + pStream->pfnCallback(pStream->UserData, BlockOffset0, (DWORD)(BlockOffset - BlockOffset0)); + bCallbackCalled = true; + } + + // Read the complete blocks from the file + if(BlockOffset > pStream->StreamSize) + BlockOffset = pStream->StreamSize; + bResult = pStream->BlockRead(pStream, BlockOffset0, BlockOffset, BlockBuffer, BytesNeeded, bPrevBlockAvailable); + } + } + else + { + // Read the complete blocks from the file + if(EndOffset > pStream->StreamSize) + EndOffset = pStream->StreamSize; + bResult = pStream->BlockRead(pStream, BlockOffset, EndOffset, BlockBuffer, BytesNeeded, true); + } + + // Now copy the data to the user buffer + if(bResult) + { + memcpy(pvBuffer, TransferBuffer + BlockBufferOffset, dwBytesToRead); + pStream->StreamPos = ByteOffset + dwBytesToRead; + } + else + { + // If the block read failed, set the last error + SetLastError(ERROR_FILE_INCOMPLETE); + } + + // Call the callback to indicate we are done + if(bCallbackCalled) + pStream->pfnCallback(pStream->UserData, 0, 0); + + // Free the block buffer and return + STORM_FREE(TransferBuffer); + return bResult; +} + +static bool BlockStream_GetSize(TFileStream * pStream, ULONGLONG * pFileSize) +{ + *pFileSize = pStream->StreamSize; + return true; +} + +static bool BlockStream_GetPos(TFileStream * pStream, ULONGLONG * pByteOffset) +{ + *pByteOffset = pStream->StreamPos; + return true; +} + +static void BlockStream_Close(TBlockStream * pStream) +{ + // Free the data map, if any + if(pStream->FileBitmap != NULL) + STORM_FREE(pStream->FileBitmap); + pStream->FileBitmap = NULL; + + // Call the base class for closing the stream + pStream->BaseClose(pStream); +} + +//----------------------------------------------------------------------------- +// File stream allocation function + +static STREAM_INIT StreamBaseInit[4] = +{ + BaseFile_Init, + BaseMap_Init, + BaseHttp_Init, + BaseNone_Init +}; + +// This function allocates an empty structure for the file stream +// The stream structure is created as flat block, variable length +// The file name is placed after the end of the stream structure data +static TFileStream * AllocateFileStream( + const TCHAR * szFileName, + size_t StreamSize, + DWORD dwStreamFlags) +{ + TFileStream * pMaster = NULL; + TFileStream * pStream; + const TCHAR * szNextFile = szFileName; + size_t FileNameSize; + + // Sanity check + assert(StreamSize != 0); + + // The caller can specify chain of files in the following form: + // C:\archive.MPQ*http://www.server.com/MPQs/archive-server.MPQ + // In that case, we use the part after "*" as master file name + while(szNextFile[0] != 0 && szNextFile[0] != _T('*')) + szNextFile++; + FileNameSize = (size_t)((szNextFile - szFileName) * sizeof(TCHAR)); + + // If we have a next file, we need to open it as master stream + // Note that we don't care if the master stream exists or not, + // If it doesn't, later attempts to read missing file block will fail + if(szNextFile[0] == _T('*')) + { + // Don't allow another master file in the string + if(_tcschr(szNextFile + 1, _T('*')) != NULL) + { + SetLastError(ERROR_INVALID_PARAMETER); + return NULL; + } + + // Open the master file + pMaster = FileStream_OpenFile(szNextFile + 1, STREAM_FLAG_READ_ONLY); + } + + // Allocate the stream structure for the given stream type + pStream = (TFileStream *)STORM_ALLOC(BYTE, StreamSize + FileNameSize + sizeof(TCHAR)); + if(pStream != NULL) + { + // Zero the entire structure + memset(pStream, 0, StreamSize); + pStream->pMaster = pMaster; + pStream->dwFlags = dwStreamFlags; + + // Initialize the file name + pStream->szFileName = (TCHAR *)((BYTE *)pStream + StreamSize); + memcpy(pStream->szFileName, szFileName, FileNameSize); + pStream->szFileName[FileNameSize / sizeof(TCHAR)] = 0; + + // Initialize the stream functions + StreamBaseInit[dwStreamFlags & 0x03](pStream); + } + + return pStream; +} + +//----------------------------------------------------------------------------- +// Local functions - flat stream support + +static DWORD FlatStream_CheckFile(TBlockStream * pStream) +{ + LPBYTE FileBitmap = (LPBYTE)pStream->FileBitmap; + DWORD WholeByteCount = (pStream->BlockCount / 8); + DWORD ExtraBitsCount = (pStream->BlockCount & 7); + BYTE ExpectedValue; + + // Verify the whole bytes - their value must be 0xFF + for(DWORD i = 0; i < WholeByteCount; i++) + { + if(FileBitmap[i] != 0xFF) + return 0; + } + + // If there are extra bits, calculate the mask + if(ExtraBitsCount != 0) + { + ExpectedValue = (BYTE)((1 << ExtraBitsCount) - 1); + if(FileBitmap[WholeByteCount] != ExpectedValue) + return 0; + } + + // Yes, the file is complete + return 1; +} + +static bool FlatStream_LoadBitmap(TBlockStream * pStream) +{ + FILE_BITMAP_FOOTER Footer; + ULONGLONG ByteOffset; + LPBYTE FileBitmap; + DWORD BlockCount; + DWORD BitmapSize; + + // Do not load the bitmap if we should not have to + if(!(pStream->dwFlags & STREAM_FLAG_USE_BITMAP)) + return false; + + // Only if the size is greater than size of bitmap footer + if(pStream->Base.File.FileSize > sizeof(FILE_BITMAP_FOOTER)) + { + // Load the bitmap footer + ByteOffset = pStream->Base.File.FileSize - sizeof(FILE_BITMAP_FOOTER); + if(pStream->BaseRead(pStream, &ByteOffset, &Footer, sizeof(FILE_BITMAP_FOOTER))) + { + // Make sure that the array is properly BSWAP-ed + BSWAP_ARRAY32_UNSIGNED((LPDWORD)(&Footer), sizeof(FILE_BITMAP_FOOTER)); + + // Verify if there is actually a footer + if(Footer.Signature == ID_FILE_BITMAP_FOOTER && Footer.Version == 0x03) + { + // Get the offset of the bitmap, number of blocks and size of the bitmap + ByteOffset = MAKE_OFFSET64(Footer.MapOffsetHi, Footer.MapOffsetLo); + BlockCount = (DWORD)(((ByteOffset - 1) / Footer.BlockSize) + 1); + BitmapSize = ((BlockCount + 7) / 8); + + // Check if the sizes match + if(ByteOffset + BitmapSize + sizeof(FILE_BITMAP_FOOTER) == pStream->Base.File.FileSize) + { + // Allocate space for the bitmap + FileBitmap = STORM_ALLOC(BYTE, BitmapSize); + if(FileBitmap != NULL) + { + // Load the bitmap bits + if(!pStream->BaseRead(pStream, &ByteOffset, FileBitmap, BitmapSize)) + { + STORM_FREE(FileBitmap); + return false; + } + + // Update the stream size + pStream->BuildNumber = Footer.BuildNumber; + pStream->StreamSize = ByteOffset; + + // Fill the bitmap information + pStream->FileBitmap = FileBitmap; + pStream->BitmapSize = BitmapSize; + pStream->BlockSize = Footer.BlockSize; + pStream->BlockCount = BlockCount; + pStream->IsComplete = FlatStream_CheckFile(pStream); + return true; + } + } + } + } + } + + return false; +} + +static void FlatStream_UpdateBitmap( + TBlockStream * pStream, // Pointer to an open stream + ULONGLONG StartOffset, + ULONGLONG EndOffset) +{ + LPBYTE FileBitmap = (LPBYTE)pStream->FileBitmap; + DWORD BlockIndex; + DWORD BlockSize = pStream->BlockSize; + DWORD ByteIndex; + BYTE BitMask; + + // Sanity checks + assert((StartOffset & (BlockSize - 1)) == 0); + assert(FileBitmap != NULL); + + // Calculate the index of the block + BlockIndex = (DWORD)(StartOffset / BlockSize); + ByteIndex = (BlockIndex / 0x08); + BitMask = (BYTE)(1 << (BlockIndex & 0x07)); + + // Set all bits for the specified range + while(StartOffset < EndOffset) + { + // Set the bit + FileBitmap[ByteIndex] |= BitMask; + + // Move all + StartOffset += BlockSize; + ByteIndex += (BitMask >> 0x07); + BitMask = (BitMask >> 0x07) | (BitMask << 0x01); + } + + // Increment the bitmap update count + pStream->IsModified = 1; +} + +static bool FlatStream_BlockCheck( + TBlockStream * pStream, // Pointer to an open stream + ULONGLONG BlockOffset) +{ + LPBYTE FileBitmap = (LPBYTE)pStream->FileBitmap; + DWORD BlockIndex; + BYTE BitMask; + + // Sanity checks + assert((BlockOffset & (pStream->BlockSize - 1)) == 0); + assert(FileBitmap != NULL); + + // Calculate the index of the block + BlockIndex = (DWORD)(BlockOffset / pStream->BlockSize); + BitMask = (BYTE)(1 << (BlockIndex & 0x07)); + + // Check if the bit is present + return (FileBitmap[BlockIndex / 0x08] & BitMask) ? true : false; +} + +static bool FlatStream_BlockRead( + TBlockStream * pStream, // Pointer to an open stream + ULONGLONG StartOffset, + ULONGLONG EndOffset, + LPBYTE BlockBuffer, + DWORD BytesNeeded, + bool bAvailable) +{ + DWORD BytesToRead = (DWORD)(EndOffset - StartOffset); + + // The starting offset must be aligned to size of the block + assert(pStream->FileBitmap != NULL); + assert((StartOffset & (pStream->BlockSize - 1)) == 0); + assert(StartOffset < EndOffset); + + // If the blocks are not available, we need to load them from the master + // and then save to the mirror + if(bAvailable == false) + { + // If we have no master, we cannot satisfy read request + if(pStream->pMaster == NULL) + return false; + + // Load the blocks from the master stream + // Note that we always have to read complete blocks + // so they get properly stored to the mirror stream + if(!FileStream_Read(pStream->pMaster, &StartOffset, BlockBuffer, BytesToRead)) + return false; + + // Store the loaded blocks to the mirror file. + // Note that this operation is not required to succeed + if(pStream->BaseWrite(pStream, &StartOffset, BlockBuffer, BytesToRead)) + FlatStream_UpdateBitmap(pStream, StartOffset, EndOffset); + + return true; + } + else + { + if(BytesToRead > BytesNeeded) + BytesToRead = BytesNeeded; + return pStream->BaseRead(pStream, &StartOffset, BlockBuffer, BytesToRead); + } +} + +static void FlatStream_Close(TBlockStream * pStream) +{ + FILE_BITMAP_FOOTER Footer; + + if(pStream->FileBitmap && pStream->IsModified) + { + // Write the file bitmap + pStream->BaseWrite(pStream, &pStream->StreamSize, pStream->FileBitmap, pStream->BitmapSize); + + // Prepare and write the file footer + Footer.Signature = ID_FILE_BITMAP_FOOTER; + Footer.Version = 3; + Footer.BuildNumber = pStream->BuildNumber; + Footer.MapOffsetLo = (DWORD)(pStream->StreamSize & 0xFFFFFFFF); + Footer.MapOffsetHi = (DWORD)(pStream->StreamSize >> 0x20); + Footer.BlockSize = pStream->BlockSize; + BSWAP_ARRAY32_UNSIGNED(&Footer, sizeof(FILE_BITMAP_FOOTER)); + pStream->BaseWrite(pStream, NULL, &Footer, sizeof(FILE_BITMAP_FOOTER)); + } + + // Close the base class + BlockStream_Close(pStream); +} + +static bool FlatStream_CreateMirror(TBlockStream * pStream) +{ + ULONGLONG MasterSize = 0; + ULONGLONG MirrorSize = 0; + LPBYTE FileBitmap = NULL; + DWORD dwBitmapSize; + DWORD dwBlockCount; + bool bNeedCreateMirrorStream = true; + bool bNeedResizeMirrorStream = true; + + // Do we have master function and base creation function? + if(pStream->pMaster == NULL || pStream->BaseCreate == NULL) + return false; + + // Retrieve the master file size, block count and bitmap size + FileStream_GetSize(pStream->pMaster, &MasterSize); + dwBlockCount = (DWORD)((MasterSize + DEFAULT_BLOCK_SIZE - 1) / DEFAULT_BLOCK_SIZE); + dwBitmapSize = (DWORD)((dwBlockCount + 7) / 8); + + // Setup stream size and position + pStream->BuildNumber = DEFAULT_BUILD_NUMBER; // BUGBUG: Really??? + pStream->StreamSize = MasterSize; + pStream->StreamPos = 0; + + // Open the base stream for write access + if(pStream->BaseOpen(pStream, pStream->szFileName, 0)) + { + // If the file open succeeded, check if the file size matches required size + pStream->BaseGetSize(pStream, &MirrorSize); + if(MirrorSize == MasterSize + dwBitmapSize + sizeof(FILE_BITMAP_FOOTER)) + { + // Attempt to load an existing file bitmap + if(FlatStream_LoadBitmap(pStream)) + return true; + + // We need to create new file bitmap + bNeedResizeMirrorStream = false; + } + + // We need to create mirror stream + bNeedCreateMirrorStream = false; + } + + // Create a new stream, if needed + if(bNeedCreateMirrorStream) + { + if(!pStream->BaseCreate(pStream)) + return false; + } + + // If we need to, then resize the mirror stream + if(bNeedResizeMirrorStream) + { + if(!pStream->BaseResize(pStream, MasterSize + dwBitmapSize + sizeof(FILE_BITMAP_FOOTER))) + return false; + } + + // Allocate the bitmap array + FileBitmap = STORM_ALLOC(BYTE, dwBitmapSize); + if(FileBitmap == NULL) + return false; + + // Initialize the bitmap + memset(FileBitmap, 0, dwBitmapSize); + pStream->FileBitmap = FileBitmap; + pStream->BitmapSize = dwBitmapSize; + pStream->BlockSize = DEFAULT_BLOCK_SIZE; + pStream->BlockCount = dwBlockCount; + pStream->IsComplete = 0; + pStream->IsModified = 1; + + // Note: Don't write the stream bitmap right away. + // Doing so would cause sparse file resize on NTFS, + // which would take long time on larger files. + return true; +} + +static TFileStream * FlatStream_Open(const TCHAR * szFileName, DWORD dwStreamFlags) +{ + TBlockStream * pStream; + ULONGLONG ByteOffset = 0; + + // Create new empty stream + pStream = (TBlockStream *)AllocateFileStream(szFileName, sizeof(TBlockStream), dwStreamFlags); + if(pStream == NULL) + return NULL; + + // Do we have a master stream? + if(pStream->pMaster != NULL) + { + if(!FlatStream_CreateMirror(pStream)) + { + FileStream_Close(pStream); + SetLastError(ERROR_FILE_NOT_FOUND); + return NULL; + } + } + else + { + // Attempt to open the base stream + if(!pStream->BaseOpen(pStream, pStream->szFileName, dwStreamFlags)) + { + FileStream_Close(pStream); + return NULL; + } + + // Load the bitmap, if required to + if(dwStreamFlags & STREAM_FLAG_USE_BITMAP) + FlatStream_LoadBitmap(pStream); + } + + // If we have a stream bitmap, set the reading functions + // which check presence of each file block + if(pStream->FileBitmap != NULL) + { + // Set the stream position to zero. Stream size is already set + assert(pStream->StreamSize != 0); + pStream->StreamPos = 0; + pStream->dwFlags |= STREAM_FLAG_READ_ONLY; + + // Supply the stream functions + pStream->StreamRead = (STREAM_READ)BlockStream_Read; + pStream->StreamGetSize = BlockStream_GetSize; + pStream->StreamGetPos = BlockStream_GetPos; + pStream->StreamClose = (STREAM_CLOSE)FlatStream_Close; + + // Supply the block functions + pStream->BlockCheck = (BLOCK_CHECK)FlatStream_BlockCheck; + pStream->BlockRead = (BLOCK_READ)FlatStream_BlockRead; + } + else + { + // Reset the base position to zero + pStream->BaseRead(pStream, &ByteOffset, NULL, 0); + + // Setup stream size and position + pStream->StreamSize = pStream->Base.File.FileSize; + pStream->StreamPos = 0; + + // Set the base functions + pStream->StreamRead = pStream->BaseRead; + pStream->StreamWrite = pStream->BaseWrite; + pStream->StreamResize = pStream->BaseResize; + pStream->StreamGetSize = pStream->BaseGetSize; + pStream->StreamGetPos = pStream->BaseGetPos; + pStream->StreamClose = pStream->BaseClose; + } + + return pStream; +} + +//----------------------------------------------------------------------------- +// Local functions - partial stream support + +static bool IsPartHeader(PPART_FILE_HEADER pPartHdr) +{ + // Version number must be 2 + if(pPartHdr->PartialVersion == 2) + { + // GameBuildNumber must be an ASCII number + if(isdigit(pPartHdr->GameBuildNumber[0]) && isdigit(pPartHdr->GameBuildNumber[1]) && isdigit(pPartHdr->GameBuildNumber[2])) + { + // Block size must be power of 2 + if((pPartHdr->BlockSize & (pPartHdr->BlockSize - 1)) == 0) + return true; + } + } + + return false; +} + +static DWORD PartStream_CheckFile(TBlockStream * pStream) +{ + PPART_FILE_MAP_ENTRY FileBitmap = (PPART_FILE_MAP_ENTRY)pStream->FileBitmap; + DWORD dwBlockCount; + + // Get the number of blocks + dwBlockCount = (DWORD)((pStream->StreamSize + pStream->BlockSize - 1) / pStream->BlockSize); + + // Check all blocks + for(DWORD i = 0; i < dwBlockCount; i++, FileBitmap++) + { + // Few sanity checks + assert(FileBitmap->LargeValueHi == 0); + assert(FileBitmap->LargeValueLo == 0); + assert(FileBitmap->Flags == 0 || FileBitmap->Flags == 3); + + // Check if this block is present + if(FileBitmap->Flags != 3) + return 0; + } + + // Yes, the file is complete + return 1; +} + +static bool PartStream_LoadBitmap(TBlockStream * pStream) +{ + PPART_FILE_MAP_ENTRY FileBitmap; + PART_FILE_HEADER PartHdr; + ULONGLONG ByteOffset = 0; + ULONGLONG StreamSize = 0; + DWORD BlockCount; + DWORD BitmapSize; + + // Only if the size is greater than size of the bitmap header + if(pStream->Base.File.FileSize > sizeof(PART_FILE_HEADER)) + { + // Attempt to read PART file header + if(pStream->BaseRead(pStream, &ByteOffset, &PartHdr, sizeof(PART_FILE_HEADER))) + { + // We need to swap PART file header on big-endian platforms + BSWAP_ARRAY32_UNSIGNED(&PartHdr, sizeof(PART_FILE_HEADER)); + + // Verify the PART file header + if(IsPartHeader(&PartHdr)) + { + // Get the number of blocks and size of one block + StreamSize = MAKE_OFFSET64(PartHdr.FileSizeHi, PartHdr.FileSizeLo); + ByteOffset = sizeof(PART_FILE_HEADER); + BlockCount = (DWORD)((StreamSize + PartHdr.BlockSize - 1) / PartHdr.BlockSize); + BitmapSize = BlockCount * sizeof(PART_FILE_MAP_ENTRY); + + // Check if sizes match + if((ByteOffset + BitmapSize) < pStream->Base.File.FileSize) + { + // Allocate space for the array of PART_FILE_MAP_ENTRY + FileBitmap = STORM_ALLOC(PART_FILE_MAP_ENTRY, BlockCount); + if(FileBitmap != NULL) + { + // Load the block map + if(!pStream->BaseRead(pStream, &ByteOffset, FileBitmap, BitmapSize)) + { + STORM_FREE(FileBitmap); + return false; + } + + // Make sure that the byte order is correct + BSWAP_ARRAY32_UNSIGNED(FileBitmap, BitmapSize); + + // Update the stream size + pStream->BuildNumber = StringToInt(PartHdr.GameBuildNumber); + pStream->StreamSize = StreamSize; + + // Fill the bitmap information + pStream->FileBitmap = FileBitmap; + pStream->BitmapSize = BitmapSize; + pStream->BlockSize = PartHdr.BlockSize; + pStream->BlockCount = BlockCount; + pStream->IsComplete = PartStream_CheckFile(pStream); + return true; + } + } + } + } + } + + return false; +} + +static void PartStream_UpdateBitmap( + TBlockStream * pStream, // Pointer to an open stream + ULONGLONG StartOffset, + ULONGLONG EndOffset, + ULONGLONG RealOffset) +{ + PPART_FILE_MAP_ENTRY FileBitmap; + DWORD BlockSize = pStream->BlockSize; + + // Sanity checks + assert((StartOffset & (BlockSize - 1)) == 0); + assert(pStream->FileBitmap != NULL); + + // Calculate the first entry in the block map + FileBitmap = (PPART_FILE_MAP_ENTRY)pStream->FileBitmap + (StartOffset / BlockSize); + + // Set all bits for the specified range + while(StartOffset < EndOffset) + { + // Set the bit + FileBitmap->BlockOffsHi = (DWORD)(RealOffset >> 0x20); + FileBitmap->BlockOffsLo = (DWORD)(RealOffset & 0xFFFFFFFF); + FileBitmap->Flags = 3; + + // Move all + StartOffset += BlockSize; + RealOffset += BlockSize; + FileBitmap++; + } + + // Increment the bitmap update count + pStream->IsModified = 1; +} + +static bool PartStream_BlockCheck( + TBlockStream * pStream, // Pointer to an open stream + ULONGLONG BlockOffset) +{ + PPART_FILE_MAP_ENTRY FileBitmap; + + // Sanity checks + assert((BlockOffset & (pStream->BlockSize - 1)) == 0); + assert(pStream->FileBitmap != NULL); + + // Calculate the block map entry + FileBitmap = (PPART_FILE_MAP_ENTRY)pStream->FileBitmap + (BlockOffset / pStream->BlockSize); + + // Check if the flags are present + return (FileBitmap->Flags & 0x03) ? true : false; +} + +static bool PartStream_BlockRead( + TBlockStream * pStream, + ULONGLONG StartOffset, + ULONGLONG EndOffset, + LPBYTE BlockBuffer, + DWORD BytesNeeded, + bool bAvailable) +{ + PPART_FILE_MAP_ENTRY FileBitmap; + ULONGLONG ByteOffset; + DWORD BytesToRead; + DWORD BlockIndex = (DWORD)(StartOffset / pStream->BlockSize); + + // The starting offset must be aligned to size of the block + assert(pStream->FileBitmap != NULL); + assert((StartOffset & (pStream->BlockSize - 1)) == 0); + assert(StartOffset < EndOffset); + + // If the blocks are not available, we need to load them from the master + // and then save to the mirror + if(bAvailable == false) + { + // If we have no master, we cannot satisfy read request + if(pStream->pMaster == NULL) + return false; + + // Load the blocks from the master stream + // Note that we always have to read complete blocks + // so they get properly stored to the mirror stream + BytesToRead = (DWORD)(EndOffset - StartOffset); + if(!FileStream_Read(pStream->pMaster, &StartOffset, BlockBuffer, BytesToRead)) + return false; + + // The loaded blocks are going to be stored to the end of the file + // Note that this operation is not required to succeed + if(pStream->BaseGetSize(pStream, &ByteOffset)) + { + // Store the loaded blocks to the mirror file. + if(pStream->BaseWrite(pStream, &ByteOffset, BlockBuffer, BytesToRead)) + { + PartStream_UpdateBitmap(pStream, StartOffset, EndOffset, ByteOffset); + } + } + } + else + { + // Get the file map entry + FileBitmap = (PPART_FILE_MAP_ENTRY)pStream->FileBitmap + BlockIndex; + + // Read all blocks + while(StartOffset < EndOffset) + { + // Get the number of bytes to be read + BytesToRead = (DWORD)(EndOffset - StartOffset); + if(BytesToRead > pStream->BlockSize) + BytesToRead = pStream->BlockSize; + if(BytesToRead > BytesNeeded) + BytesToRead = BytesNeeded; + + // Read the block + ByteOffset = MAKE_OFFSET64(FileBitmap->BlockOffsHi, FileBitmap->BlockOffsLo); + if(!pStream->BaseRead(pStream, &ByteOffset, BlockBuffer, BytesToRead)) + return false; + + // Move the pointers + StartOffset += pStream->BlockSize; + BlockBuffer += pStream->BlockSize; + BytesNeeded -= pStream->BlockSize; + FileBitmap++; + } + } + + return true; +} + +static void PartStream_Close(TBlockStream * pStream) +{ + PART_FILE_HEADER PartHeader; + ULONGLONG ByteOffset = 0; + + if(pStream->FileBitmap && pStream->IsModified) + { + // Prepare the part file header + memset(&PartHeader, 0, sizeof(PART_FILE_HEADER)); + PartHeader.PartialVersion = 2; + PartHeader.FileSizeHi = (DWORD)(pStream->StreamSize >> 0x20); + PartHeader.FileSizeLo = (DWORD)(pStream->StreamSize & 0xFFFFFFFF); + PartHeader.BlockSize = pStream->BlockSize; + + // Make sure that the header is properly BSWAPed + BSWAP_ARRAY32_UNSIGNED(&PartHeader, sizeof(PART_FILE_HEADER)); + sprintf(PartHeader.GameBuildNumber, "%u", (unsigned int)pStream->BuildNumber); + + // Write the part header + pStream->BaseWrite(pStream, &ByteOffset, &PartHeader, sizeof(PART_FILE_HEADER)); + + // Write the block bitmap + BSWAP_ARRAY32_UNSIGNED(pStream->FileBitmap, pStream->BitmapSize); + pStream->BaseWrite(pStream, NULL, pStream->FileBitmap, pStream->BitmapSize); + } + + // Close the base class + BlockStream_Close(pStream); +} + +static bool PartStream_CreateMirror(TBlockStream * pStream) +{ + ULONGLONG RemainingSize; + ULONGLONG MasterSize = 0; + ULONGLONG MirrorSize = 0; + LPBYTE FileBitmap = NULL; + DWORD dwBitmapSize; + DWORD dwBlockCount; + bool bNeedCreateMirrorStream = true; + bool bNeedResizeMirrorStream = true; + + // Do we have master function and base creation function? + if(pStream->pMaster == NULL || pStream->BaseCreate == NULL) + return false; + + // Retrieve the master file size, block count and bitmap size + FileStream_GetSize(pStream->pMaster, &MasterSize); + dwBlockCount = (DWORD)((MasterSize + DEFAULT_BLOCK_SIZE - 1) / DEFAULT_BLOCK_SIZE); + dwBitmapSize = (DWORD)(dwBlockCount * sizeof(PART_FILE_MAP_ENTRY)); + + // Setup stream size and position + pStream->BuildNumber = DEFAULT_BUILD_NUMBER; // BUGBUG: Really??? + pStream->StreamSize = MasterSize; + pStream->StreamPos = 0; + + // Open the base stream for write access + if(pStream->BaseOpen(pStream, pStream->szFileName, 0)) + { + // If the file open succeeded, check if the file size matches required size + pStream->BaseGetSize(pStream, &MirrorSize); + if(MirrorSize >= sizeof(PART_FILE_HEADER) + dwBitmapSize) + { + // Check if the remaining size is aligned to block + RemainingSize = MirrorSize - sizeof(PART_FILE_HEADER) - dwBitmapSize; + if((RemainingSize & (DEFAULT_BLOCK_SIZE - 1)) == 0 || RemainingSize == MasterSize) + { + // Attempt to load an existing file bitmap + if(PartStream_LoadBitmap(pStream)) + return true; + } + } + + // We need to create mirror stream + bNeedCreateMirrorStream = false; + } + + // Create a new stream, if needed + if(bNeedCreateMirrorStream) + { + if(!pStream->BaseCreate(pStream)) + return false; + } + + // If we need to, then resize the mirror stream + if(bNeedResizeMirrorStream) + { + if(!pStream->BaseResize(pStream, sizeof(PART_FILE_HEADER) + dwBitmapSize)) + return false; + } + + // Allocate the bitmap array + FileBitmap = STORM_ALLOC(BYTE, dwBitmapSize); + if(FileBitmap == NULL) + return false; + + // Initialize the bitmap + memset(FileBitmap, 0, dwBitmapSize); + pStream->FileBitmap = FileBitmap; + pStream->BitmapSize = dwBitmapSize; + pStream->BlockSize = DEFAULT_BLOCK_SIZE; + pStream->BlockCount = dwBlockCount; + pStream->IsComplete = 0; + pStream->IsModified = 1; + + // Note: Don't write the stream bitmap right away. + // Doing so would cause sparse file resize on NTFS, + // which would take long time on larger files. + return true; +} + + +static TFileStream * PartStream_Open(const TCHAR * szFileName, DWORD dwStreamFlags) +{ + TBlockStream * pStream; + + // Create new empty stream + pStream = (TBlockStream *)AllocateFileStream(szFileName, sizeof(TBlockStream), dwStreamFlags); + if(pStream == NULL) + return NULL; + + // Do we have a master stream? + if(pStream->pMaster != NULL) + { + if(!PartStream_CreateMirror(pStream)) + { + FileStream_Close(pStream); + SetLastError(ERROR_FILE_NOT_FOUND); + return NULL; + } + } + else + { + // Attempt to open the base stream + if(!pStream->BaseOpen(pStream, pStream->szFileName, dwStreamFlags)) + { + FileStream_Close(pStream); + return NULL; + } + + // Load the part stream block map + if(!PartStream_LoadBitmap(pStream)) + { + FileStream_Close(pStream); + SetLastError(ERROR_BAD_FORMAT); + return NULL; + } + } + + // Set the stream position to zero. Stream size is already set + assert(pStream->StreamSize != 0); + pStream->StreamPos = 0; + pStream->dwFlags |= STREAM_FLAG_READ_ONLY; + + // Set new function pointers + pStream->StreamRead = (STREAM_READ)BlockStream_Read; + pStream->StreamGetPos = BlockStream_GetPos; + pStream->StreamGetSize = BlockStream_GetSize; + pStream->StreamClose = (STREAM_CLOSE)PartStream_Close; + + // Supply the block functions + pStream->BlockCheck = (BLOCK_CHECK)PartStream_BlockCheck; + pStream->BlockRead = (BLOCK_READ)PartStream_BlockRead; + return pStream; +} + +//----------------------------------------------------------------------------- +// Local functions - MPQE stream support + +static const char * szKeyTemplate = "expand 32-byte k000000000000000000000000000000000000000000000000"; + +static const char * AuthCodeArray[] = +{ + // Starcraft II (Heart of the Swarm) + // Authentication code URL: http://dist.blizzard.com/mediakey/hots-authenticationcode-bgdl.txt + // -0C- -1C--08- -18--04- -14--00- -10- + "S48B6CDTN5XEQAKQDJNDLJBJ73FDFM3U", // SC2 Heart of the Swarm-all : "expand 32-byte kQAKQ0000FM3UN5XE000073FD6CDT0000LJBJS48B0000DJND" + + // Diablo III: Agent.exe (1.0.0.954) + // Address of decryption routine: 00502b00 + // Pointer to decryptor object: ECX + // Pointer to key: ECX+0x5C + // Authentication code URL: http://dist.blizzard.com/mediakey/d3-authenticationcode-enGB.txt + // -0C- -1C--08- -18--04- -14--00- -10- + "UCMXF6EJY352EFH4XFRXCFH2XC9MQRZK", // Diablo III Installer (deDE): "expand 32-byte kEFH40000QRZKY3520000XC9MF6EJ0000CFH2UCMX0000XFRX" + "MMKVHY48RP7WXP4GHYBQ7SL9J9UNPHBP", // Diablo III Installer (enGB): "expand 32-byte kXP4G0000PHBPRP7W0000J9UNHY4800007SL9MMKV0000HYBQ" + "8MXLWHQ7VGGLTZ9MQZQSFDCLJYET3CPP", // Diablo III Installer (enSG): "expand 32-byte kTZ9M00003CPPVGGL0000JYETWHQ70000FDCL8MXL0000QZQS" + "EJ2R5TM6XFE2GUNG5QDGHKQ9UAKPWZSZ", // Diablo III Installer (enUS): "expand 32-byte kGUNG0000WZSZXFE20000UAKP5TM60000HKQ9EJ2R00005QDG" + "PBGFBE42Z6LNK65UGJQ3WZVMCLP4HQQT", // Diablo III Installer (esES): "expand 32-byte kK65U0000HQQTZ6LN0000CLP4BE420000WZVMPBGF0000GJQ3" + "X7SEJJS9TSGCW5P28EBSC47AJPEY8VU2", // Diablo III Installer (esMX): "expand 32-byte kW5P200008VU2TSGC0000JPEYJJS90000C47AX7SE00008EBS" + "5KVBQA8VYE6XRY3DLGC5ZDE4XS4P7YA2", // Diablo III Installer (frFR): "expand 32-byte kRY3D00007YA2YE6X0000XS4PQA8V0000ZDE45KVB0000LGC5" + "478JD2K56EVNVVY4XX8TDWYT5B8KB254", // Diablo III Installer (itIT): "expand 32-byte kVVY40000B2546EVN00005B8KD2K50000DWYT478J0000XX8T" + "8TS4VNFQRZTN6YWHE9CHVDH9NVWD474A", // Diablo III Installer (koKR): "expand 32-byte k6YWH0000474ARZTN0000NVWDVNFQ0000VDH98TS40000E9CH" + "LJ52Z32DF4LZ4ZJJXVKK3AZQA6GABLJB", // Diablo III Installer (plPL): "expand 32-byte k4ZJJ0000BLJBF4LZ0000A6GAZ32D00003AZQLJ520000XVKK" + "K6BDHY2ECUE2545YKNLBJPVYWHE7XYAG", // Diablo III Installer (ptBR): "expand 32-byte k545Y0000XYAGCUE20000WHE7HY2E0000JPVYK6BD0000KNLB" + "NDVW8GWLAYCRPGRNY8RT7ZZUQU63VLPR", // Diablo III Installer (ruRU): "expand 32-byte kXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + "6VWCQTN8V3ZZMRUCZXV8A8CGUX2TAA8H", // Diablo III Installer (zhTW): "expand 32-byte kMRUC0000AA8HV3ZZ0000UX2TQTN80000A8CG6VWC0000ZXV8" +// "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", // Diablo III Installer (zhCN): "expand 32-byte kXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + + // Starcraft II (Wings of Liberty): Installer.exe (4.1.1.4219) + // Address of decryption routine: 0053A3D0 + // Pointer to decryptor object: ECX + // Pointer to key: ECX+0x5C + // Authentication code URL: http://dist.blizzard.com/mediakey/sc2-authenticationcode-enUS.txt + // -0C- -1C--08- -18--04- -14--00- -10- + "Y45MD3CAK4KXSSXHYD9VY64Z8EKJ4XFX", // SC2 Wings of Liberty (deDE): "expand 32-byte kSSXH00004XFXK4KX00008EKJD3CA0000Y64ZY45M0000YD9V" + "G8MN8UDG6NA2ANGY6A3DNY82HRGF29ZH", // SC2 Wings of Liberty (enGB): "expand 32-byte kANGY000029ZH6NA20000HRGF8UDG0000NY82G8MN00006A3D" + "W9RRHLB2FDU9WW5B3ECEBLRSFWZSF7HW", // SC2 Wings of Liberty (enSG): "expand 32-byte kWW5B0000F7HWFDU90000FWZSHLB20000BLRSW9RR00003ECE" + "3DH5RE5NVM5GTFD85LXGWT6FK859ETR5", // SC2 Wings of Liberty (enUS): "expand 32-byte kTFD80000ETR5VM5G0000K859RE5N0000WT6F3DH500005LXG" + "8WLKUAXE94PFQU4Y249PAZ24N4R4XKTQ", // SC2 Wings of Liberty (esES): "expand 32-byte kQU4Y0000XKTQ94PF0000N4R4UAXE0000AZ248WLK0000249P" + "A34DXX3VHGGXSQBRFE5UFFDXMF9G4G54", // SC2 Wings of Liberty (esMX): "expand 32-byte kSQBR00004G54HGGX0000MF9GXX3V0000FFDXA34D0000FE5U" + "ZG7J9K938HJEFWPQUA768MA2PFER6EAJ", // SC2 Wings of Liberty (frFR): "expand 32-byte kFWPQ00006EAJ8HJE0000PFER9K9300008MA2ZG7J0000UA76" + "NE7CUNNNTVAPXV7E3G2BSVBWGVMW8BL2", // SC2 Wings of Liberty (itIT): "expand 32-byte kXV7E00008BL2TVAP0000GVMWUNNN0000SVBWNE7C00003G2B" + "3V9E2FTMBM9QQWK7U6MAMWAZWQDB838F", // SC2 Wings of Liberty (koKR): "expand 32-byte kQWK70000838FBM9Q0000WQDB2FTM0000MWAZ3V9E0000U6MA" + "2NSFB8MELULJ83U6YHA3UP6K4MQD48L6", // SC2 Wings of Liberty (plPL): "expand 32-byte k83U6000048L6LULJ00004MQDB8ME0000UP6K2NSF0000YHA3" + "QA2TZ9EWZ4CUU8BMB5WXCTY65F9CSW4E", // SC2 Wings of Liberty (ptBR): "expand 32-byte kU8BM0000SW4EZ4CU00005F9CZ9EW0000CTY6QA2T0000B5WX" + "VHB378W64BAT9SH7D68VV9NLQDK9YEGT", // SC2 Wings of Liberty (ruRU): "expand 32-byte k9SH70000YEGT4BAT0000QDK978W60000V9NLVHB30000D68V" + "U3NFQJV4M6GC7KBN9XQJ3BRDN3PLD9NE", // SC2 Wings of Liberty (zhTW): "expand 32-byte k7KBN0000D9NEM6GC0000N3PLQJV400003BRDU3NF00009XQJ" + + NULL +}; + +static DWORD Rol32(DWORD dwValue, DWORD dwRolCount) +{ + DWORD dwShiftRight = 32 - dwRolCount; + + return (dwValue << dwRolCount) | (dwValue >> dwShiftRight); +} + +static void CreateKeyFromAuthCode( + LPBYTE pbKeyBuffer, + const char * szAuthCode) +{ + LPDWORD KeyPosition = (LPDWORD)(pbKeyBuffer + 0x10); + LPDWORD AuthCode32 = (LPDWORD)szAuthCode; + + memcpy(pbKeyBuffer, szKeyTemplate, MPQE_CHUNK_SIZE); + KeyPosition[0x00] = AuthCode32[0x03]; + KeyPosition[0x02] = AuthCode32[0x07]; + KeyPosition[0x03] = AuthCode32[0x02]; + KeyPosition[0x05] = AuthCode32[0x06]; + KeyPosition[0x06] = AuthCode32[0x01]; + KeyPosition[0x08] = AuthCode32[0x05]; + KeyPosition[0x09] = AuthCode32[0x00]; + KeyPosition[0x0B] = AuthCode32[0x04]; + BSWAP_ARRAY32_UNSIGNED(pbKeyBuffer, MPQE_CHUNK_SIZE); +} + +static void DecryptFileChunk( + DWORD * MpqData, + LPBYTE pbKey, + ULONGLONG ByteOffset, + DWORD dwLength) +{ + ULONGLONG ChunkOffset; + DWORD KeyShuffled[0x10]; + DWORD KeyMirror[0x10]; + DWORD RoundCount = 0x14; + + // Prepare the key + ChunkOffset = ByteOffset / MPQE_CHUNK_SIZE; + memcpy(KeyMirror, pbKey, MPQE_CHUNK_SIZE); + BSWAP_ARRAY32_UNSIGNED(KeyMirror, MPQE_CHUNK_SIZE); + KeyMirror[0x05] = (DWORD)(ChunkOffset >> 32); + KeyMirror[0x08] = (DWORD)(ChunkOffset); + + while(dwLength >= MPQE_CHUNK_SIZE) + { + // Shuffle the key - part 1 + KeyShuffled[0x0E] = KeyMirror[0x00]; + KeyShuffled[0x0C] = KeyMirror[0x01]; + KeyShuffled[0x05] = KeyMirror[0x02]; + KeyShuffled[0x0F] = KeyMirror[0x03]; + KeyShuffled[0x0A] = KeyMirror[0x04]; + KeyShuffled[0x07] = KeyMirror[0x05]; + KeyShuffled[0x0B] = KeyMirror[0x06]; + KeyShuffled[0x09] = KeyMirror[0x07]; + KeyShuffled[0x03] = KeyMirror[0x08]; + KeyShuffled[0x06] = KeyMirror[0x09]; + KeyShuffled[0x08] = KeyMirror[0x0A]; + KeyShuffled[0x0D] = KeyMirror[0x0B]; + KeyShuffled[0x02] = KeyMirror[0x0C]; + KeyShuffled[0x04] = KeyMirror[0x0D]; + KeyShuffled[0x01] = KeyMirror[0x0E]; + KeyShuffled[0x00] = KeyMirror[0x0F]; + + // Shuffle the key - part 2 + for(DWORD i = 0; i < RoundCount; i += 2) + { + KeyShuffled[0x0A] = KeyShuffled[0x0A] ^ Rol32((KeyShuffled[0x0E] + KeyShuffled[0x02]), 0x07); + KeyShuffled[0x03] = KeyShuffled[0x03] ^ Rol32((KeyShuffled[0x0A] + KeyShuffled[0x0E]), 0x09); + KeyShuffled[0x02] = KeyShuffled[0x02] ^ Rol32((KeyShuffled[0x03] + KeyShuffled[0x0A]), 0x0D); + KeyShuffled[0x0E] = KeyShuffled[0x0E] ^ Rol32((KeyShuffled[0x02] + KeyShuffled[0x03]), 0x12); + + KeyShuffled[0x07] = KeyShuffled[0x07] ^ Rol32((KeyShuffled[0x0C] + KeyShuffled[0x04]), 0x07); + KeyShuffled[0x06] = KeyShuffled[0x06] ^ Rol32((KeyShuffled[0x07] + KeyShuffled[0x0C]), 0x09); + KeyShuffled[0x04] = KeyShuffled[0x04] ^ Rol32((KeyShuffled[0x06] + KeyShuffled[0x07]), 0x0D); + KeyShuffled[0x0C] = KeyShuffled[0x0C] ^ Rol32((KeyShuffled[0x04] + KeyShuffled[0x06]), 0x12); + + KeyShuffled[0x0B] = KeyShuffled[0x0B] ^ Rol32((KeyShuffled[0x05] + KeyShuffled[0x01]), 0x07); + KeyShuffled[0x08] = KeyShuffled[0x08] ^ Rol32((KeyShuffled[0x0B] + KeyShuffled[0x05]), 0x09); + KeyShuffled[0x01] = KeyShuffled[0x01] ^ Rol32((KeyShuffled[0x08] + KeyShuffled[0x0B]), 0x0D); + KeyShuffled[0x05] = KeyShuffled[0x05] ^ Rol32((KeyShuffled[0x01] + KeyShuffled[0x08]), 0x12); + + KeyShuffled[0x09] = KeyShuffled[0x09] ^ Rol32((KeyShuffled[0x0F] + KeyShuffled[0x00]), 0x07); + KeyShuffled[0x0D] = KeyShuffled[0x0D] ^ Rol32((KeyShuffled[0x09] + KeyShuffled[0x0F]), 0x09); + KeyShuffled[0x00] = KeyShuffled[0x00] ^ Rol32((KeyShuffled[0x0D] + KeyShuffled[0x09]), 0x0D); + KeyShuffled[0x0F] = KeyShuffled[0x0F] ^ Rol32((KeyShuffled[0x00] + KeyShuffled[0x0D]), 0x12); + + KeyShuffled[0x04] = KeyShuffled[0x04] ^ Rol32((KeyShuffled[0x0E] + KeyShuffled[0x09]), 0x07); + KeyShuffled[0x08] = KeyShuffled[0x08] ^ Rol32((KeyShuffled[0x04] + KeyShuffled[0x0E]), 0x09); + KeyShuffled[0x09] = KeyShuffled[0x09] ^ Rol32((KeyShuffled[0x08] + KeyShuffled[0x04]), 0x0D); + KeyShuffled[0x0E] = KeyShuffled[0x0E] ^ Rol32((KeyShuffled[0x09] + KeyShuffled[0x08]), 0x12); + + KeyShuffled[0x01] = KeyShuffled[0x01] ^ Rol32((KeyShuffled[0x0C] + KeyShuffled[0x0A]), 0x07); + KeyShuffled[0x0D] = KeyShuffled[0x0D] ^ Rol32((KeyShuffled[0x01] + KeyShuffled[0x0C]), 0x09); + KeyShuffled[0x0A] = KeyShuffled[0x0A] ^ Rol32((KeyShuffled[0x0D] + KeyShuffled[0x01]), 0x0D); + KeyShuffled[0x0C] = KeyShuffled[0x0C] ^ Rol32((KeyShuffled[0x0A] + KeyShuffled[0x0D]), 0x12); + + KeyShuffled[0x00] = KeyShuffled[0x00] ^ Rol32((KeyShuffled[0x05] + KeyShuffled[0x07]), 0x07); + KeyShuffled[0x03] = KeyShuffled[0x03] ^ Rol32((KeyShuffled[0x00] + KeyShuffled[0x05]), 0x09); + KeyShuffled[0x07] = KeyShuffled[0x07] ^ Rol32((KeyShuffled[0x03] + KeyShuffled[0x00]), 0x0D); + KeyShuffled[0x05] = KeyShuffled[0x05] ^ Rol32((KeyShuffled[0x07] + KeyShuffled[0x03]), 0x12); + + KeyShuffled[0x02] = KeyShuffled[0x02] ^ Rol32((KeyShuffled[0x0F] + KeyShuffled[0x0B]), 0x07); + KeyShuffled[0x06] = KeyShuffled[0x06] ^ Rol32((KeyShuffled[0x02] + KeyShuffled[0x0F]), 0x09); + KeyShuffled[0x0B] = KeyShuffled[0x0B] ^ Rol32((KeyShuffled[0x06] + KeyShuffled[0x02]), 0x0D); + KeyShuffled[0x0F] = KeyShuffled[0x0F] ^ Rol32((KeyShuffled[0x0B] + KeyShuffled[0x06]), 0x12); + } + + // Decrypt one data chunk + BSWAP_ARRAY32_UNSIGNED(MpqData, MPQE_CHUNK_SIZE); + MpqData[0x00] = MpqData[0x00] ^ (KeyShuffled[0x0E] + KeyMirror[0x00]); + MpqData[0x01] = MpqData[0x01] ^ (KeyShuffled[0x04] + KeyMirror[0x0D]); + MpqData[0x02] = MpqData[0x02] ^ (KeyShuffled[0x08] + KeyMirror[0x0A]); + MpqData[0x03] = MpqData[0x03] ^ (KeyShuffled[0x09] + KeyMirror[0x07]); + MpqData[0x04] = MpqData[0x04] ^ (KeyShuffled[0x0A] + KeyMirror[0x04]); + MpqData[0x05] = MpqData[0x05] ^ (KeyShuffled[0x0C] + KeyMirror[0x01]); + MpqData[0x06] = MpqData[0x06] ^ (KeyShuffled[0x01] + KeyMirror[0x0E]); + MpqData[0x07] = MpqData[0x07] ^ (KeyShuffled[0x0D] + KeyMirror[0x0B]); + MpqData[0x08] = MpqData[0x08] ^ (KeyShuffled[0x03] + KeyMirror[0x08]); + MpqData[0x09] = MpqData[0x09] ^ (KeyShuffled[0x07] + KeyMirror[0x05]); + MpqData[0x0A] = MpqData[0x0A] ^ (KeyShuffled[0x05] + KeyMirror[0x02]); + MpqData[0x0B] = MpqData[0x0B] ^ (KeyShuffled[0x00] + KeyMirror[0x0F]); + MpqData[0x0C] = MpqData[0x0C] ^ (KeyShuffled[0x02] + KeyMirror[0x0C]); + MpqData[0x0D] = MpqData[0x0D] ^ (KeyShuffled[0x06] + KeyMirror[0x09]); + MpqData[0x0E] = MpqData[0x0E] ^ (KeyShuffled[0x0B] + KeyMirror[0x06]); + MpqData[0x0F] = MpqData[0x0F] ^ (KeyShuffled[0x0F] + KeyMirror[0x03]); + BSWAP_ARRAY32_UNSIGNED(MpqData, MPQE_CHUNK_SIZE); + + // Update byte offset in the key + KeyMirror[0x08]++; + if(KeyMirror[0x08] == 0) + KeyMirror[0x05]++; + + // Move pointers and decrease number of bytes to decrypt + MpqData += (MPQE_CHUNK_SIZE / sizeof(DWORD)); + dwLength -= MPQE_CHUNK_SIZE; + } +} + +static bool MpqeStream_DetectFileKey(TEncryptedStream * pStream) +{ + ULONGLONG ByteOffset = 0; + BYTE EncryptedHeader[MPQE_CHUNK_SIZE]; + BYTE FileHeader[MPQE_CHUNK_SIZE]; + + // Read the first file chunk + if(pStream->BaseRead(pStream, &ByteOffset, EncryptedHeader, sizeof(EncryptedHeader))) + { + // We just try all known keys one by one + for(int i = 0; AuthCodeArray[i] != NULL; i++) + { + // Prepare they decryption key from game serial number + CreateKeyFromAuthCode(pStream->Key, AuthCodeArray[i]); + + // Try to decrypt with the given key + memcpy(FileHeader, EncryptedHeader, MPQE_CHUNK_SIZE); + DecryptFileChunk((LPDWORD)FileHeader, pStream->Key, ByteOffset, MPQE_CHUNK_SIZE); + + // We check the decrypted data + // All known encrypted MPQs have header at the begin of the file, + // so we check for MPQ signature there. + if(FileHeader[0] == 'M' && FileHeader[1] == 'P' && FileHeader[2] == 'Q') + { + // Update the stream size + pStream->StreamSize = pStream->Base.File.FileSize; + + // Fill the block information + pStream->BlockSize = MPQE_CHUNK_SIZE; + pStream->BlockCount = (DWORD)(pStream->Base.File.FileSize + MPQE_CHUNK_SIZE - 1) / MPQE_CHUNK_SIZE; + pStream->IsComplete = 1; + return true; + } + } + } + + // Key not found, sorry + return false; +} + +static bool MpqeStream_BlockRead( + TEncryptedStream * pStream, + ULONGLONG StartOffset, + ULONGLONG EndOffset, + LPBYTE BlockBuffer, + DWORD BytesNeeded, + bool bAvailable) +{ + DWORD dwBytesToRead; + + assert((StartOffset & (pStream->BlockSize - 1)) == 0); + assert(StartOffset < EndOffset); + assert(bAvailable != false); + BytesNeeded = BytesNeeded; + bAvailable = bAvailable; + + // Read the file from the stream as-is + // Limit the reading to number of blocks really needed + dwBytesToRead = (DWORD)(EndOffset - StartOffset); + if(!pStream->BaseRead(pStream, &StartOffset, BlockBuffer, dwBytesToRead)) + return false; + + // Decrypt the data + dwBytesToRead = (dwBytesToRead + MPQE_CHUNK_SIZE - 1) & ~(MPQE_CHUNK_SIZE - 1); + DecryptFileChunk((LPDWORD)BlockBuffer, pStream->Key, StartOffset, dwBytesToRead); + return true; +} + +static TFileStream * MpqeStream_Open(const TCHAR * szFileName, DWORD dwStreamFlags) +{ + TEncryptedStream * pStream; + + // Create new empty stream + pStream = (TEncryptedStream *)AllocateFileStream(szFileName, sizeof(TEncryptedStream), dwStreamFlags); + if(pStream == NULL) + return NULL; + + // Attempt to open the base stream + assert(pStream->BaseOpen != NULL); + if(!pStream->BaseOpen(pStream, pStream->szFileName, dwStreamFlags)) + return NULL; + + // Determine the encryption key for the MPQ + if(MpqeStream_DetectFileKey(pStream)) + { + // Set the stream position and size + assert(pStream->StreamSize != 0); + pStream->StreamPos = 0; + pStream->dwFlags |= STREAM_FLAG_READ_ONLY; + + // Set new function pointers + pStream->StreamRead = (STREAM_READ)BlockStream_Read; + pStream->StreamGetPos = BlockStream_GetPos; + pStream->StreamGetSize = BlockStream_GetSize; + pStream->StreamClose = pStream->BaseClose; + + // Supply the block functions + pStream->BlockRead = (BLOCK_READ)MpqeStream_BlockRead; + return pStream; + } + + // Cleanup the stream and return + FileStream_Close(pStream); + SetLastError(ERROR_UNKNOWN_FILE_KEY); + return NULL; +} + +//----------------------------------------------------------------------------- +// Local functions - Block4 stream support + +#define BLOCK4_BLOCK_SIZE 0x4000 // Size of one block +#define BLOCK4_HASH_SIZE 0x20 // Size of MD5 hash that is after each block +#define BLOCK4_MAX_BLOCKS 0x00002000 // Maximum amount of blocks per file +#define BLOCK4_MAX_FSIZE 0x08040000 // Max size of one file + +static bool Block4Stream_BlockRead( + TBlockStream * pStream, // Pointer to an open stream + ULONGLONG StartOffset, + ULONGLONG EndOffset, + LPBYTE BlockBuffer, + DWORD BytesNeeded, + bool bAvailable) +{ + TBaseProviderData * BaseArray = (TBaseProviderData *)pStream->FileBitmap; + ULONGLONG ByteOffset; + DWORD BytesToRead; + DWORD StreamIndex; + DWORD BlockIndex; + bool bResult; + + // The starting offset must be aligned to size of the block + assert(pStream->FileBitmap != NULL); + assert((StartOffset & (pStream->BlockSize - 1)) == 0); + assert(StartOffset < EndOffset); + assert(bAvailable == true); + + // Keep compiler happy + bAvailable = bAvailable; + EndOffset = EndOffset; + + while(BytesNeeded != 0) + { + // Calculate the block index and the file index + StreamIndex = (DWORD)((StartOffset / pStream->BlockSize) / BLOCK4_MAX_BLOCKS); + BlockIndex = (DWORD)((StartOffset / pStream->BlockSize) % BLOCK4_MAX_BLOCKS); + if(StreamIndex > pStream->BitmapSize) + return false; + + // Calculate the block offset + ByteOffset = ((ULONGLONG)BlockIndex * (BLOCK4_BLOCK_SIZE + BLOCK4_HASH_SIZE)); + BytesToRead = STORMLIB_MIN(BytesNeeded, BLOCK4_BLOCK_SIZE); + + // Read from the base stream + pStream->Base = BaseArray[StreamIndex]; + bResult = pStream->BaseRead(pStream, &ByteOffset, BlockBuffer, BytesToRead); + BaseArray[StreamIndex] = pStream->Base; + + // Did the result succeed? + if(bResult == false) + return false; + + // Move pointers + StartOffset += BytesToRead; + BlockBuffer += BytesToRead; + BytesNeeded -= BytesToRead; + } + + return true; +} + + +static void Block4Stream_Close(TBlockStream * pStream) +{ + TBaseProviderData * BaseArray = (TBaseProviderData *)pStream->FileBitmap; + + // If we have a non-zero count of base streams, + // we have to close them all + if(BaseArray != NULL) + { + // Close all base streams + for(DWORD i = 0; i < pStream->BitmapSize; i++) + { + memcpy(&pStream->Base, BaseArray + i, sizeof(TBaseProviderData)); + pStream->BaseClose(pStream); + } + } + + // Free the data map, if any + if(pStream->FileBitmap != NULL) + STORM_FREE(pStream->FileBitmap); + pStream->FileBitmap = NULL; + + // Do not call the BaseClose function, + // we closed all handles already + return; +} + +static TFileStream * Block4Stream_Open(const TCHAR * szFileName, DWORD dwStreamFlags) +{ + TBaseProviderData * NewBaseArray = NULL; + ULONGLONG RemainderBlock; + ULONGLONG BlockCount; + ULONGLONG FileSize; + TBlockStream * pStream; + TCHAR * szNameBuff; + size_t nNameLength; + DWORD dwBaseFiles = 0; + DWORD dwBaseFlags; + + // Create new empty stream + pStream = (TBlockStream *)AllocateFileStream(szFileName, sizeof(TBlockStream), dwStreamFlags); + if(pStream == NULL) + return NULL; + + // Sanity check + assert(pStream->BaseOpen != NULL); + + // Get the length of the file name without numeric suffix + nNameLength = _tcslen(pStream->szFileName); + if(pStream->szFileName[nNameLength - 2] == '.' && pStream->szFileName[nNameLength - 1] == '0') + nNameLength -= 2; + pStream->szFileName[nNameLength] = 0; + + // Supply the stream functions + pStream->StreamRead = (STREAM_READ)BlockStream_Read; + pStream->StreamGetSize = BlockStream_GetSize; + pStream->StreamGetPos = BlockStream_GetPos; + pStream->StreamClose = (STREAM_CLOSE)Block4Stream_Close; + pStream->BlockRead = (BLOCK_READ)Block4Stream_BlockRead; + + // Allocate work space for numeric names + szNameBuff = STORM_ALLOC(TCHAR, nNameLength + 4); + if(szNameBuff != NULL) + { + // Set the base flags + dwBaseFlags = (dwStreamFlags & STREAM_PROVIDERS_MASK) | STREAM_FLAG_READ_ONLY; + + // Go all suffixes from 0 to 30 + for(int nSuffix = 0; nSuffix < 30; nSuffix++) + { + // Open the n-th file + _stprintf(szNameBuff, _T("%s.%u"), pStream->szFileName, nSuffix); + if(!pStream->BaseOpen(pStream, szNameBuff, dwBaseFlags)) + break; + + // If the open succeeded, we re-allocate the base provider array + NewBaseArray = STORM_ALLOC(TBaseProviderData, dwBaseFiles + 1); + if(NewBaseArray == NULL) + { + SetLastError(ERROR_NOT_ENOUGH_MEMORY); + return NULL; + } + + // Copy the old base data array to the new base data array + if(pStream->FileBitmap != NULL) + { + memcpy(NewBaseArray, pStream->FileBitmap, sizeof(TBaseProviderData) * dwBaseFiles); + STORM_FREE(pStream->FileBitmap); + } + + // Also copy the opened base array + memcpy(NewBaseArray + dwBaseFiles, &pStream->Base, sizeof(TBaseProviderData)); + pStream->FileBitmap = NewBaseArray; + dwBaseFiles++; + + // Get the size of the base stream + pStream->BaseGetSize(pStream, &FileSize); + assert(FileSize <= BLOCK4_MAX_FSIZE); + RemainderBlock = FileSize % (BLOCK4_BLOCK_SIZE + BLOCK4_HASH_SIZE); + BlockCount = FileSize / (BLOCK4_BLOCK_SIZE + BLOCK4_HASH_SIZE); + + // Increment the stream size and number of blocks + pStream->StreamSize += (BlockCount * BLOCK4_BLOCK_SIZE); + pStream->BlockCount += (DWORD)BlockCount; + + // Is this the last file? + if(FileSize < BLOCK4_MAX_FSIZE) + { + if(RemainderBlock) + { + pStream->StreamSize += (RemainderBlock - BLOCK4_HASH_SIZE); + pStream->BlockCount++; + } + break; + } + } + + // Fill the remainining block stream variables + pStream->BitmapSize = dwBaseFiles; + pStream->BlockSize = BLOCK4_BLOCK_SIZE; + pStream->IsComplete = 1; + pStream->IsModified = 0; + + // Fill the remaining stream variables + pStream->StreamPos = 0; + pStream->dwFlags |= STREAM_FLAG_READ_ONLY; + + STORM_FREE(szNameBuff); + } + + // If we opened something, return success + if(dwBaseFiles == 0) + { + FileStream_Close(pStream); + SetLastError(ERROR_FILE_NOT_FOUND); + pStream = NULL; + } + + return pStream; +} + +//----------------------------------------------------------------------------- +// Public functions + +/** + * This function creates a new file for read-write access + * + * - If the current platform supports file sharing, + * the file must be created for read sharing (i.e. another application + * can open the file for read, but not for write) + * - If the file does not exist, the function must create new one + * - If the file exists, the function must rewrite it and set to zero size + * - The parameters of the function must be validate by the caller + * - The function must initialize all stream function pointers in TFileStream + * - If the function fails from any reason, it must close all handles + * and free all memory that has been allocated in the process of stream creation, + * including the TFileStream structure itself + * + * \a szFileName Name of the file to create + */ + +TFileStream * FileStream_CreateFile( + const TCHAR * szFileName, + DWORD dwStreamFlags) +{ + TFileStream * pStream; + + // We only support creation of flat, local file + if((dwStreamFlags & (STREAM_PROVIDERS_MASK)) != (STREAM_PROVIDER_FLAT | BASE_PROVIDER_FILE)) + { + SetLastError(ERROR_NOT_SUPPORTED); + return NULL; + } + + // Allocate file stream structure for flat stream + pStream = AllocateFileStream(szFileName, sizeof(TBlockStream), dwStreamFlags); + if(pStream != NULL) + { + // Attempt to create the disk file + if(BaseFile_Create(pStream)) + { + // Fill the stream provider functions + pStream->StreamRead = pStream->BaseRead; + pStream->StreamWrite = pStream->BaseWrite; + pStream->StreamResize = pStream->BaseResize; + pStream->StreamGetSize = pStream->BaseGetSize; + pStream->StreamGetPos = pStream->BaseGetPos; + pStream->StreamClose = pStream->BaseClose; + return pStream; + } + + // File create failed, delete the stream + STORM_FREE(pStream); + pStream = NULL; + } + + // Return the stream + return pStream; +} + +/** + * This function opens an existing file for read or read-write access + * - If the current platform supports file sharing, + * the file must be open for read sharing (i.e. another application + * can open the file for read, but not for write) + * - If the file does not exist, the function must return NULL + * - If the file exists but cannot be open, then function must return NULL + * - The parameters of the function must be validate by the caller + * - The function must initialize all stream function pointers in TFileStream + * - If the function fails from any reason, it must close all handles + * and free all memory that has been allocated in the process of stream creation, + * including the TFileStream structure itself + * + * \a szFileName Name of the file to open + * \a dwStreamFlags specifies the provider and base storage type + */ + +TFileStream * FileStream_OpenFile( + const TCHAR * szFileName, + DWORD dwStreamFlags) +{ + DWORD dwProvider = dwStreamFlags & STREAM_PROVIDERS_MASK; + size_t nPrefixLength = FileStream_Prefix(szFileName, &dwProvider); + + // Re-assemble the stream flags + dwStreamFlags = (dwStreamFlags & STREAM_OPTIONS_MASK) | dwProvider; + szFileName += nPrefixLength; + + // Perform provider-specific open + switch(dwStreamFlags & STREAM_PROVIDER_MASK) + { + case STREAM_PROVIDER_FLAT: + return FlatStream_Open(szFileName, dwStreamFlags); + + case STREAM_PROVIDER_PARTIAL: + return PartStream_Open(szFileName, dwStreamFlags); + + case STREAM_PROVIDER_MPQE: + return MpqeStream_Open(szFileName, dwStreamFlags); + + case STREAM_PROVIDER_BLOCK4: + return Block4Stream_Open(szFileName, dwStreamFlags); + + default: + SetLastError(ERROR_INVALID_PARAMETER); + return NULL; + } +} + +/** + * Returns the file name of the stream + * + * \a pStream Pointer to an open stream + */ +const TCHAR * FileStream_GetFileName(TFileStream * pStream) +{ + assert(pStream != NULL); + return pStream->szFileName; +} + +/** + * Returns the length of the provider prefix. Returns zero if no prefix + * + * \a szFileName Pointer to a stream name (file, mapped file, URL) + * \a pdwStreamProvider Pointer to a DWORD variable that receives stream provider (STREAM_PROVIDER_XXX) + */ + +size_t FileStream_Prefix(const TCHAR * szFileName, DWORD * pdwProvider) +{ + size_t nPrefixLength1 = 0; + size_t nPrefixLength2 = 0; + DWORD dwProvider = 0; + + if(szFileName != NULL) + { + // + // Determine the stream provider + // + + if(!_tcsnicmp(szFileName, _T("flat-"), 5)) + { + dwProvider |= STREAM_PROVIDER_FLAT; + nPrefixLength1 = 5; + } + + else if(!_tcsnicmp(szFileName, _T("part-"), 5)) + { + dwProvider |= STREAM_PROVIDER_PARTIAL; + nPrefixLength1 = 5; + } + + else if(!_tcsnicmp(szFileName, _T("mpqe-"), 5)) + { + dwProvider |= STREAM_PROVIDER_MPQE; + nPrefixLength1 = 5; + } + + else if(!_tcsnicmp(szFileName, _T("blk4-"), 5)) + { + dwProvider |= STREAM_PROVIDER_BLOCK4; + nPrefixLength1 = 5; + } + + // + // Determine the base provider + // + + if(!_tcsnicmp(szFileName+nPrefixLength1, _T("file:"), 5)) + { + dwProvider |= BASE_PROVIDER_FILE; + nPrefixLength2 = 5; + } + + else if(!_tcsnicmp(szFileName+nPrefixLength1, _T("map:"), 4)) + { + dwProvider |= BASE_PROVIDER_MAP; + nPrefixLength2 = 4; + } + + else if(!_tcsnicmp(szFileName+nPrefixLength1, _T("http:"), 5)) + { + dwProvider |= BASE_PROVIDER_HTTP; + nPrefixLength2 = 5; + } + + // Only accept stream provider if we recognized the base provider + if(nPrefixLength2 != 0) + { + // It is also allowed to put "//" after the base provider, e.g. "file://", "http://" + if(szFileName[nPrefixLength1+nPrefixLength2] == '/' && szFileName[nPrefixLength1+nPrefixLength2+1] == '/') + nPrefixLength2 += 2; + + if(pdwProvider != NULL) + *pdwProvider = dwProvider; + return nPrefixLength1 + nPrefixLength2; + } + } + + return 0; +} + +/** + * Sets a download callback. Whenever the stream needs to download one or more blocks + * from the server, the callback is called + * + * \a pStream Pointer to an open stream + * \a pfnCallback Pointer to callback function + * \a pvUserData Arbitrary user pointer passed to the download callback + */ + +bool FileStream_SetCallback(TFileStream * pStream, SFILE_DOWNLOAD_CALLBACK pfnCallback, void * pvUserData) +{ + TBlockStream * pBlockStream = (TBlockStream *)pStream; + + if(pStream->BlockRead == NULL) + { + SetLastError(ERROR_NOT_SUPPORTED); + return false; + } + + pBlockStream->pfnCallback = pfnCallback; + pBlockStream->UserData = pvUserData; + return true; +} + +/** + * This function gives the block map. The 'pvBitmap' pointer must point to a buffer + * of at least sizeof(STREAM_BLOCK_MAP) size. It can also have size of the complete + * block map (i.e. sizeof(STREAM_BLOCK_MAP) + BitmapSize). In that case, the function + * also copies the bit-based block map. + * + * \a pStream Pointer to an open stream + * \a pvBitmap Pointer to buffer where the block map will be stored + * \a cbBitmap Length of the buffer, of the block map + * \a cbLengthNeeded Length of the bitmap, in bytes + */ + +bool FileStream_GetBitmap(TFileStream * pStream, void * pvBitmap, DWORD cbBitmap, DWORD * pcbLengthNeeded) +{ + TStreamBitmap * pBitmap = (TStreamBitmap *)pvBitmap; + TBlockStream * pBlockStream = (TBlockStream *)pStream; + ULONGLONG BlockOffset; + LPBYTE Bitmap = (LPBYTE)(pBitmap + 1); + DWORD BitmapSize; + DWORD BlockCount; + DWORD BlockSize; + bool bResult = false; + + // Retrieve the size of one block + if(pStream->BlockCheck != NULL) + { + BlockCount = pBlockStream->BlockCount; + BlockSize = pBlockStream->BlockSize; + } + else + { + BlockCount = (DWORD)((pStream->StreamSize + DEFAULT_BLOCK_SIZE - 1) / DEFAULT_BLOCK_SIZE); + BlockSize = DEFAULT_BLOCK_SIZE; + } + + // Fill-in the variables + BitmapSize = (BlockCount + 7) / 8; + + // Give the number of blocks + if(pcbLengthNeeded != NULL) + pcbLengthNeeded[0] = sizeof(TStreamBitmap) + BitmapSize; + + // If the length of the buffer is not enough + if(pvBitmap != NULL && cbBitmap != 0) + { + // Give the STREAM_BLOCK_MAP structure + if(cbBitmap >= sizeof(TStreamBitmap)) + { + pBitmap->StreamSize = pStream->StreamSize; + pBitmap->BitmapSize = BitmapSize; + pBitmap->BlockCount = BlockCount; + pBitmap->BlockSize = BlockSize; + pBitmap->IsComplete = (pStream->BlockCheck != NULL) ? pBlockStream->IsComplete : 1; + bResult = true; + } + + // Give the block bitmap, if enough space + if(cbBitmap >= sizeof(TStreamBitmap) + BitmapSize) + { + // Version with bitmap present + if(pStream->BlockCheck != NULL) + { + DWORD ByteIndex = 0; + BYTE BitMask = 0x01; + + // Initialize the map with zeros + memset(Bitmap, 0, BitmapSize); + + // Fill the map + for(BlockOffset = 0; BlockOffset < pStream->StreamSize; BlockOffset += BlockSize) + { + // Set the bit if the block is present + if(pBlockStream->BlockCheck(pStream, BlockOffset)) + Bitmap[ByteIndex] |= BitMask; + + // Move bit position + ByteIndex += (BitMask >> 0x07); + BitMask = (BitMask >> 0x07) | (BitMask << 0x01); + } + } + else + { + memset(Bitmap, 0xFF, BitmapSize); + } + } + } + + // Set last error value and return + if(bResult == false) + SetLastError(ERROR_INSUFFICIENT_BUFFER); + return bResult; +} + +/** + * Reads data from the stream + * + * - Returns true if the read operation succeeded and all bytes have been read + * - Returns false if either read failed or not all bytes have been read + * - If the pByteOffset is NULL, the function must read the data from the current file position + * - The function can be called with dwBytesToRead = 0. In that case, pvBuffer is ignored + * and the function just adjusts file pointer. + * + * \a pStream Pointer to an open stream + * \a pByteOffset Pointer to file byte offset. If NULL, it reads from the current position + * \a pvBuffer Pointer to data to be read + * \a dwBytesToRead Number of bytes to read from the file + * + * \returns + * - If the function reads the required amount of bytes, it returns true. + * - If the function reads less than required bytes, it returns false and GetLastError() returns ERROR_HANDLE_EOF + * - If the function fails, it reads false and GetLastError() returns an error code different from ERROR_HANDLE_EOF + */ +bool FileStream_Read(TFileStream * pStream, ULONGLONG * pByteOffset, void * pvBuffer, DWORD dwBytesToRead) +{ + assert(pStream->StreamRead != NULL); + return pStream->StreamRead(pStream, pByteOffset, pvBuffer, dwBytesToRead); +} + +/** + * This function writes data to the stream + * + * - Returns true if the write operation succeeded and all bytes have been written + * - Returns false if either write failed or not all bytes have been written + * - If the pByteOffset is NULL, the function must write the data to the current file position + * + * \a pStream Pointer to an open stream + * \a pByteOffset Pointer to file byte offset. If NULL, it reads from the current position + * \a pvBuffer Pointer to data to be written + * \a dwBytesToWrite Number of bytes to write to the file + */ +bool FileStream_Write(TFileStream * pStream, ULONGLONG * pByteOffset, const void * pvBuffer, DWORD dwBytesToWrite) +{ + if(pStream->dwFlags & STREAM_FLAG_READ_ONLY) + { + SetLastError(ERROR_ACCESS_DENIED); + return false; + } + + assert(pStream->StreamWrite != NULL); + return pStream->StreamWrite(pStream, pByteOffset, pvBuffer, dwBytesToWrite); +} + +/** + * Returns the size of a file + * + * \a pStream Pointer to an open stream + * \a FileSize Pointer where to store the file size + */ +bool FileStream_GetSize(TFileStream * pStream, ULONGLONG * pFileSize) +{ + assert(pStream->StreamGetSize != NULL); + return pStream->StreamGetSize(pStream, pFileSize); +} + +/** + * Sets the size of a file + * + * \a pStream Pointer to an open stream + * \a NewFileSize File size to set + */ +bool FileStream_SetSize(TFileStream * pStream, ULONGLONG NewFileSize) +{ + if(pStream->dwFlags & STREAM_FLAG_READ_ONLY) + { + SetLastError(ERROR_ACCESS_DENIED); + return false; + } + + assert(pStream->StreamResize != NULL); + return pStream->StreamResize(pStream, NewFileSize); +} + +/** + * This function returns the current file position + * \a pStream + * \a pByteOffset + */ +bool FileStream_GetPos(TFileStream * pStream, ULONGLONG * pByteOffset) +{ + assert(pStream->StreamGetPos != NULL); + return pStream->StreamGetPos(pStream, pByteOffset); +} + +/** + * Returns the last write time of a file + * + * \a pStream Pointer to an open stream + * \a pFileType Pointer where to store the file last write time + */ +bool FileStream_GetTime(TFileStream * pStream, ULONGLONG * pFileTime) +{ + // Just use the saved filetime value + *pFileTime = pStream->Base.File.FileTime; + return true; +} + +/** + * Returns the stream flags + * + * \a pStream Pointer to an open stream + * \a pdwStreamFlags Pointer where to store the stream flags + */ +bool FileStream_GetFlags(TFileStream * pStream, LPDWORD pdwStreamFlags) +{ + *pdwStreamFlags = pStream->dwFlags; + return true; +} + +/** + * Switches a stream with another. Used for final phase of archive compacting. + * Performs these steps: + * + * 1) Closes the handle to the existing MPQ + * 2) Renames the temporary MPQ to the original MPQ, overwrites existing one + * 3) Opens the MPQ stores the handle and stream position to the new stream structure + * + * \a pStream Pointer to an open stream + * \a pNewStream Temporary ("working") stream (created during archive compacting) + */ +bool FileStream_Replace(TFileStream * pStream, TFileStream * pNewStream) +{ + // Only supported on flat files + if((pStream->dwFlags & STREAM_PROVIDERS_MASK) != (STREAM_PROVIDER_FLAT | BASE_PROVIDER_FILE)) + { + SetLastError(ERROR_NOT_SUPPORTED); + return false; + } + + // Not supported on read-only streams + if(pStream->dwFlags & STREAM_FLAG_READ_ONLY) + { + SetLastError(ERROR_ACCESS_DENIED); + return false; + } + + // Close both stream's base providers + pNewStream->BaseClose(pNewStream); + pStream->BaseClose(pStream); + + // Now we have to delete the (now closed) old file and rename the new file + if(!BaseFile_Replace(pStream, pNewStream)) + return false; + + // Now open the base file again + if(!BaseFile_Open(pStream, pStream->szFileName, pStream->dwFlags)) + return false; + + // Cleanup the new stream + FileStream_Close(pNewStream); + return true; +} + +/** + * This function closes an archive file and frees any data buffers + * that have been allocated for stream management. The function must also + * support partially allocated structure, i.e. one or more buffers + * can be NULL, if there was an allocation failure during the process + * + * \a pStream Pointer to an open stream + */ +void FileStream_Close(TFileStream * pStream) +{ + // Check if the stream structure is allocated at all + if(pStream != NULL) + { + // Free the master stream, if any + if(pStream->pMaster != NULL) + FileStream_Close(pStream->pMaster); + pStream->pMaster = NULL; + + // Close the stream provider. + if(pStream->StreamClose != NULL) + pStream->StreamClose(pStream); + + // Also close base stream, if any + else if(pStream->BaseClose != NULL) + pStream->BaseClose(pStream); + + // Free the stream itself + STORM_FREE(pStream); + } +} + +//----------------------------------------------------------------------------- +// Utility functions (ANSI) + +const char * GetPlainFileName(const char * szFileName) +{ + const char * szPlainName = szFileName; + + while(*szFileName != 0) + { + if(*szFileName == '\\' || *szFileName == '/') + szPlainName = szFileName + 1; + szFileName++; + } + + return szPlainName; +} + +void CopyFileName(char * szTarget, const char * szSource, size_t cchLength) +{ + memcpy(szTarget, szSource, cchLength); + szTarget[cchLength] = 0; +} + +//----------------------------------------------------------------------------- +// Utility functions (UNICODE) only exist in the ANSI version of the library +// In ANSI builds, TCHAR = char, so we don't need these functions implemented + +#ifdef _UNICODE +const TCHAR * GetPlainFileName(const TCHAR * szFileName) +{ + const TCHAR * szPlainName = szFileName; + + while(*szFileName != 0) + { + if(*szFileName == '\\' || *szFileName == '/') + szPlainName = szFileName + 1; + szFileName++; + } + + return szPlainName; +} + +void CopyFileName(TCHAR * szTarget, const char * szSource, size_t cchLength) +{ + mbstowcs(szTarget, szSource, cchLength); + szTarget[cchLength] = 0; +} + +void CopyFileName(char * szTarget, const TCHAR * szSource, size_t cchLength) +{ + wcstombs(szTarget, szSource, cchLength); + szTarget[cchLength] = 0; +} +#endif diff --git a/src/SBaseCommon.cpp b/src/SBaseCommon.cpp index b16472e..54fc6d0 100644 --- a/src/SBaseCommon.cpp +++ b/src/SBaseCommon.cpp @@ -1,1774 +1,1774 @@ -/*****************************************************************************/ -/* SBaseCommon.cpp Copyright (c) Ladislav Zezula 2003 */ -/*---------------------------------------------------------------------------*/ -/* Common functions for StormLib, used by all SFile*** modules */ -/*---------------------------------------------------------------------------*/ -/* Date Ver Who Comment */ -/* -------- ---- --- ------- */ -/* 24.03.03 1.00 Lad The first version of SFileCommon.cpp */ -/* 19.11.03 1.01 Dan Big endian handling */ -/* 12.06.04 1.01 Lad Renamed to SCommon.cpp */ -/* 06.09.10 1.01 Lad Renamed to SBaseCommon.cpp */ -/*****************************************************************************/ - -#define __STORMLIB_SELF__ -#include "StormLib.h" -#include "StormCommon.h" - -char StormLibCopyright[] = "StormLib v " STORMLIB_VERSION_STRING " Copyright Ladislav Zezula 1998-2014"; - -//----------------------------------------------------------------------------- -// Local variables - -LCID lcFileLocale = LANG_NEUTRAL; // File locale -USHORT wPlatform = 0; // File platform - -//----------------------------------------------------------------------------- -// Conversion to uppercase/lowercase - -// Converts ASCII characters to lowercase -// Converts slash (0x2F) to backslash (0x5C) -unsigned char AsciiToLowerTable[256] = -{ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, - 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, - 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x5C, - 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, - 0x40, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, - 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, - 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, - 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F, - 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, - 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, - 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, - 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF, - 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, - 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, - 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, - 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF -}; - -// Converts ASCII characters to uppercase -// Converts slash (0x2F) to backslash (0x5C) -unsigned char AsciiToUpperTable[256] = -{ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, - 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, - 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x5C, - 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, - 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, - 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, - 0x60, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, - 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F, - 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, - 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, - 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, - 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF, - 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, - 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, - 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, - 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF -}; - -// Converts ASCII characters to uppercase -// Does NOT convert slash (0x2F) to backslash (0x5C) -unsigned char AsciiToUpperTable_Slash[256] = -{ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, - 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, - 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, - 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, - 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, - 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, - 0x60, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, - 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F, - 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, - 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, - 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, - 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF, - 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, - 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, - 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, - 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF -}; - -//----------------------------------------------------------------------------- -// Safe string functions - -void StringCopyA(char * dest, const char * src, size_t nMaxChars) -{ - size_t nLength = strlen(src); - - // Don't copy more than nMaxChars - nLength = STORMLIB_MIN(nLength, nMaxChars); - memcpy(dest, src, nLength); - dest[nLength] = 0; -} - -void StringCatA(char * dest, const char * src, size_t nMaxChars) -{ - size_t nLength1 = strlen(dest); - size_t nLength2 = strlen(src); - - // Don't copy more than nMaxChars - if(nLength1 < nMaxChars) - { - nLength2 = STORMLIB_MIN(nLength2, (nMaxChars - nLength1)); - memcpy(dest + nLength1, src, nLength2); - dest[nLength1 + nLength2] = 0; - } -} - -void StringCopyT(TCHAR * dest, const TCHAR * src, size_t nMaxChars) -{ - size_t nLength = _tcslen(src); - - // Don't copy more than nMaxChars - nLength = STORMLIB_MIN(nLength, nMaxChars); - memcpy(dest, src, (nLength * sizeof(TCHAR))); - dest[nLength] = 0; -} - -void StringCatT(TCHAR * dest, const TCHAR * src, size_t nMaxChars) -{ - size_t nLength1 = _tcslen(dest); - size_t nLength2 = _tcslen(src); - - // Don't copy more than nMaxChars - if(nLength1 < nMaxChars) - { - nLength2 = STORMLIB_MIN(nLength2, (nMaxChars - nLength1)); - memcpy(dest + nLength1, src, (nLength2 * sizeof(TCHAR))); - dest[nLength1 + nLength2] = 0; - } -} - -//----------------------------------------------------------------------------- -// Storm hashing functions - -#define STORM_BUFFER_SIZE 0x500 - -#define HASH_INDEX_MASK(ha) (ha->pHeader->dwHashTableSize ? (ha->pHeader->dwHashTableSize - 1) : 0) - -static DWORD StormBuffer[STORM_BUFFER_SIZE]; // Buffer for the decryption engine -static bool bMpqCryptographyInitialized = false; - -void InitializeMpqCryptography() -{ - DWORD dwSeed = 0x00100001; - DWORD index1 = 0; - DWORD index2 = 0; - int i; - - // Initialize the decryption buffer. - // Do nothing if already done. - if(bMpqCryptographyInitialized == false) - { - for(index1 = 0; index1 < 0x100; index1++) - { - for(index2 = index1, i = 0; i < 5; i++, index2 += 0x100) - { - DWORD temp1, temp2; - - dwSeed = (dwSeed * 125 + 3) % 0x2AAAAB; - temp1 = (dwSeed & 0xFFFF) << 0x10; - - dwSeed = (dwSeed * 125 + 3) % 0x2AAAAB; - temp2 = (dwSeed & 0xFFFF); - - StormBuffer[index2] = (temp1 | temp2); - } - } - - // Also register both MD5 and SHA1 hash algorithms - register_hash(&md5_desc); - register_hash(&sha1_desc); - - // Use LibTomMath as support math library for LibTomCrypt - ltc_mp = ltm_desc; - - // Don't do that again - bMpqCryptographyInitialized = true; - } -} - -DWORD HashString(const char * szFileName, DWORD dwHashType) -{ - LPBYTE pbKey = (BYTE *)szFileName; - DWORD dwSeed1 = 0x7FED7FED; - DWORD dwSeed2 = 0xEEEEEEEE; - DWORD ch; - - while(*pbKey != 0) - { - // Convert the input character to uppercase - // Convert slash (0x2F) to backslash (0x5C) - ch = AsciiToUpperTable[*pbKey++]; - - dwSeed1 = StormBuffer[dwHashType + ch] ^ (dwSeed1 + dwSeed2); - dwSeed2 = ch + dwSeed1 + dwSeed2 + (dwSeed2 << 5) + 3; - } - - return dwSeed1; -} - -DWORD HashStringSlash(const char * szFileName, DWORD dwHashType) -{ - LPBYTE pbKey = (BYTE *)szFileName; - DWORD dwSeed1 = 0x7FED7FED; - DWORD dwSeed2 = 0xEEEEEEEE; - DWORD ch; - - while(*pbKey != 0) - { - // Convert the input character to uppercase - // DON'T convert slash (0x2F) to backslash (0x5C) - ch = AsciiToUpperTable_Slash[*pbKey++]; - - dwSeed1 = StormBuffer[dwHashType + ch] ^ (dwSeed1 + dwSeed2); - dwSeed2 = ch + dwSeed1 + dwSeed2 + (dwSeed2 << 5) + 3; - } - - return dwSeed1; -} - -DWORD HashStringLower(const char * szFileName, DWORD dwHashType) -{ - LPBYTE pbKey = (BYTE *)szFileName; - DWORD dwSeed1 = 0x7FED7FED; - DWORD dwSeed2 = 0xEEEEEEEE; - DWORD ch; - - while(*pbKey != 0) - { - // Convert the input character to lower - // DON'T convert slash (0x2F) to backslash (0x5C) - ch = AsciiToLowerTable[*pbKey++]; - - dwSeed1 = StormBuffer[dwHashType + ch] ^ (dwSeed1 + dwSeed2); - dwSeed2 = ch + dwSeed1 + dwSeed2 + (dwSeed2 << 5) + 3; - } - - return dwSeed1; -} - -//----------------------------------------------------------------------------- -// Calculates the hash table size for a given amount of files - -DWORD GetHashTableSizeForFileCount(DWORD dwFileCount) -{ - DWORD dwPowerOfTwo = HASH_TABLE_SIZE_MIN; - - // For zero files, there is no hash table needed - if(dwFileCount == 0) - return 0; - - // Round the hash table size up to the nearest power of two - // Don't allow the hash table size go over allowed maximum - while(dwPowerOfTwo < HASH_TABLE_SIZE_MAX && dwPowerOfTwo < dwFileCount) - dwPowerOfTwo <<= 1; - return dwPowerOfTwo; -} - -//----------------------------------------------------------------------------- -// Calculates a Jenkin's Encrypting and decrypting MPQ file data - -ULONGLONG HashStringJenkins(const char * szFileName) -{ - LPBYTE pbFileName = (LPBYTE)szFileName; - char szNameBuff[0x108]; - size_t nLength = 0; - unsigned int primary_hash = 1; - unsigned int secondary_hash = 2; - - // Normalize the file name - convert to uppercase, and convert "/" to "\\". - if(pbFileName != NULL) - { - char * szNamePtr = szNameBuff; - char * szNameEnd = szNamePtr + sizeof(szNameBuff); - - // Normalize the file name. Doesn't have to be zero terminated for hashing - while(szNamePtr < szNameEnd && pbFileName[0] != 0) - *szNamePtr++ = (char)AsciiToLowerTable[*pbFileName++]; - nLength = szNamePtr - szNameBuff; - } - - // Thanks Quantam for finding out what the algorithm is. - // I am really getting old for reversing large chunks of assembly - // that does hashing :-) - hashlittle2(szNameBuff, nLength, &secondary_hash, &primary_hash); - - // Combine those 2 together - return ((ULONGLONG)primary_hash << 0x20) | (ULONGLONG)secondary_hash; -} - -//----------------------------------------------------------------------------- -// Default flags for (attributes) and (listfile) - -DWORD GetDefaultSpecialFileFlags(DWORD dwFileSize, USHORT wFormatVersion) -{ - // Fixed for format 1.0 - if(wFormatVersion == MPQ_FORMAT_VERSION_1) - return MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_FIX_KEY; - - // Size-dependent for formats 2.0-4.0 - return (dwFileSize > 0x4000) ? (MPQ_FILE_COMPRESS | MPQ_FILE_SECTOR_CRC) : (MPQ_FILE_COMPRESS | MPQ_FILE_SINGLE_UNIT); -} - - -//----------------------------------------------------------------------------- -// Encrypting/Decrypting MPQ data block - -void EncryptMpqBlock(void * pvDataBlock, DWORD dwLength, DWORD dwKey1) -{ - LPDWORD DataBlock = (LPDWORD)pvDataBlock; - DWORD dwValue32; - DWORD dwKey2 = 0xEEEEEEEE; - - // Round to DWORDs - dwLength >>= 2; - - // Encrypt the data block at array of DWORDs - for(DWORD i = 0; i < dwLength; i++) - { - // Modify the second key - dwKey2 += StormBuffer[MPQ_HASH_KEY2_MIX + (dwKey1 & 0xFF)]; - - dwValue32 = DataBlock[i]; - DataBlock[i] = DataBlock[i] ^ (dwKey1 + dwKey2); - - dwKey1 = ((~dwKey1 << 0x15) + 0x11111111) | (dwKey1 >> 0x0B); - dwKey2 = dwValue32 + dwKey2 + (dwKey2 << 5) + 3; - } -} - -void DecryptMpqBlock(void * pvDataBlock, DWORD dwLength, DWORD dwKey1) -{ - LPDWORD DataBlock = (LPDWORD)pvDataBlock; - DWORD dwValue32; - DWORD dwKey2 = 0xEEEEEEEE; - - // Round to DWORDs - dwLength >>= 2; - - // Decrypt the data block at array of DWORDs - for(DWORD i = 0; i < dwLength; i++) - { - // Modify the second key - dwKey2 += StormBuffer[MPQ_HASH_KEY2_MIX + (dwKey1 & 0xFF)]; - - DataBlock[i] = DataBlock[i] ^ (dwKey1 + dwKey2); - dwValue32 = DataBlock[i]; - - dwKey1 = ((~dwKey1 << 0x15) + 0x11111111) | (dwKey1 >> 0x0B); - dwKey2 = dwValue32 + dwKey2 + (dwKey2 << 5) + 3; - } -} - -/** - * Functions tries to get file decryption key. This comes from these facts - * - * - We know the decrypted value of the first DWORD in the encrypted data - * - We know the decrypted value of the second DWORD (at least aproximately) - * - There is only 256 variants of how the second key is modified - * - * The first iteration of dwKey1 and dwKey2 is this: - * - * dwKey2 = 0xEEEEEEEE + StormBuffer[MPQ_HASH_KEY2_MIX + (dwKey1 & 0xFF)] - * dwDecrypted0 = DataBlock[0] ^ (dwKey1 + dwKey2); - * - * This means: - * - * (dwKey1 + dwKey2) = DataBlock[0] ^ dwDecrypted0; - * - */ - -DWORD DetectFileKeyBySectorSize(LPDWORD EncryptedData, DWORD dwSectorSize, DWORD dwDecrypted0) -{ - DWORD dwDecrypted1Max = dwSectorSize + dwDecrypted0; - DWORD dwKey1PlusKey2; - DWORD DataBlock[2]; - - // We must have at least 2 DWORDs there to be able to decrypt something - if(dwSectorSize < 0x08) - return 0; - - // Get the value of the combined encryption key - dwKey1PlusKey2 = (EncryptedData[0] ^ dwDecrypted0) - 0xEEEEEEEE; - - // Try all 256 combinations of dwKey1 - for(DWORD i = 0; i < 0x100; i++) - { - DWORD dwSaveKey1; - DWORD dwKey1 = dwKey1PlusKey2 - StormBuffer[MPQ_HASH_KEY2_MIX + i]; - DWORD dwKey2 = 0xEEEEEEEE; - - // Modify the second key and decrypt the first DWORD - dwKey2 += StormBuffer[MPQ_HASH_KEY2_MIX + (dwKey1 & 0xFF)]; - DataBlock[0] = EncryptedData[0] ^ (dwKey1 + dwKey2); - - // Did we obtain the same value like dwDecrypted0? - if(DataBlock[0] == dwDecrypted0) - { - // Save this key value. Increment by one because - // we are decrypting sector offset table - dwSaveKey1 = dwKey1 + 1; - - // Rotate both keys - dwKey1 = ((~dwKey1 << 0x15) + 0x11111111) | (dwKey1 >> 0x0B); - dwKey2 = DataBlock[0] + dwKey2 + (dwKey2 << 5) + 3; - - // Modify the second key again and decrypt the second DWORD - dwKey2 += StormBuffer[MPQ_HASH_KEY2_MIX + (dwKey1 & 0xFF)]; - DataBlock[1] = EncryptedData[1] ^ (dwKey1 + dwKey2); - - // Now compare the results - if(DataBlock[1] <= dwDecrypted1Max) - return dwSaveKey1; - } - } - - // Key not found - return 0; -} - -// Function tries to detect file encryption key based on expected file content -// It is the same function like before, except that we know the value of the second DWORD -DWORD DetectFileKeyByKnownContent(void * pvEncryptedData, DWORD dwDecrypted0, DWORD dwDecrypted1) -{ - LPDWORD EncryptedData = (LPDWORD)pvEncryptedData; - DWORD dwKey1PlusKey2; - DWORD DataBlock[2]; - - // Get the value of the combined encryption key - dwKey1PlusKey2 = (EncryptedData[0] ^ dwDecrypted0) - 0xEEEEEEEE; - - // Try all 256 combinations of dwKey1 - for(DWORD i = 0; i < 0x100; i++) - { - DWORD dwSaveKey1; - DWORD dwKey1 = dwKey1PlusKey2 - StormBuffer[MPQ_HASH_KEY2_MIX + i]; - DWORD dwKey2 = 0xEEEEEEEE; - - // Modify the second key and decrypt the first DWORD - dwKey2 += StormBuffer[MPQ_HASH_KEY2_MIX + (dwKey1 & 0xFF)]; - DataBlock[0] = EncryptedData[0] ^ (dwKey1 + dwKey2); - - // Did we obtain the same value like dwDecrypted0? - if(DataBlock[0] == dwDecrypted0) - { - // Save this key value - dwSaveKey1 = dwKey1; - - // Rotate both keys - dwKey1 = ((~dwKey1 << 0x15) + 0x11111111) | (dwKey1 >> 0x0B); - dwKey2 = DataBlock[0] + dwKey2 + (dwKey2 << 5) + 3; - - // Modify the second key again and decrypt the second DWORD - dwKey2 += StormBuffer[MPQ_HASH_KEY2_MIX + (dwKey1 & 0xFF)]; - DataBlock[1] = EncryptedData[1] ^ (dwKey1 + dwKey2); - - // Now compare the results - if(DataBlock[1] == dwDecrypted1) - return dwSaveKey1; - } - } - - // Key not found - return 0; -} - -DWORD DetectFileKeyByContent(void * pvEncryptedData, DWORD dwSectorSize, DWORD dwFileSize) -{ - DWORD dwFileKey; - - // Try to break the file encryption key as if it was a WAVE file - if(dwSectorSize >= 0x0C) - { - dwFileKey = DetectFileKeyByKnownContent(pvEncryptedData, 0x46464952, dwFileSize - 8); - if(dwFileKey != 0) - return dwFileKey; - } - - // Try to break the encryption key as if it was an EXE file - if(dwSectorSize > 0x40) - { - dwFileKey = DetectFileKeyByKnownContent(pvEncryptedData, 0x00905A4D, 0x00000003); - if(dwFileKey != 0) - return dwFileKey; - } - - // Try to break the encryption key as if it was a XML file - if(dwSectorSize > 0x04) - { - dwFileKey = DetectFileKeyByKnownContent(pvEncryptedData, 0x6D783F3C, 0x6576206C); - if(dwFileKey != 0) - return dwFileKey; - } - - // Not detected, sorry - return 0; -} - -DWORD DecryptFileKey( - const char * szFileName, - ULONGLONG MpqPos, - DWORD dwFileSize, - DWORD dwFlags) -{ - DWORD dwFileKey; - DWORD dwMpqPos = (DWORD)MpqPos; - - // File key is calculated from plain name - szFileName = GetPlainFileName(szFileName); - dwFileKey = HashString(szFileName, MPQ_HASH_FILE_KEY); - - // Fix the key, if needed - if(dwFlags & MPQ_FILE_FIX_KEY) - dwFileKey = (dwFileKey + dwMpqPos) ^ dwFileSize; - - // Return the key - return dwFileKey; -} - -//----------------------------------------------------------------------------- -// Handle validation functions - -TMPQArchive * IsValidMpqHandle(HANDLE hMpq) -{ - TMPQArchive * ha = (TMPQArchive *)hMpq; - - return (ha != NULL && ha->pHeader != NULL && ha->pHeader->dwID == ID_MPQ) ? ha : NULL; -} - -TMPQFile * IsValidFileHandle(HANDLE hFile) -{ - TMPQFile * hf = (TMPQFile *)hFile; - - // Must not be NULL - if(hf != NULL && hf->dwMagic == ID_MPQ_FILE) - { - // Local file handle? - if(hf->pStream != NULL) - return hf; - - // Also verify the MPQ handle within the file handle - if(IsValidMpqHandle(hf->ha)) - return hf; - } - - return NULL; -} - -//----------------------------------------------------------------------------- -// Hash table and block table manipulation - -// Attempts to search a free hash entry, or an entry whose names and locale matches -TMPQHash * FindFreeHashEntry(TMPQArchive * ha, DWORD dwStartIndex, DWORD dwName1, DWORD dwName2, LCID lcLocale) -{ - TMPQHash * pDeletedEntry = NULL; // If a deleted entry was found in the continuous hash range - TMPQHash * pFreeEntry = NULL; // If a free entry was found in the continuous hash range - DWORD dwHashIndexMask = HASH_INDEX_MASK(ha); - DWORD dwIndex; - - // Set the initial index - dwStartIndex = dwIndex = (dwStartIndex & dwHashIndexMask); - - // Search the hash table and return the found entries in the following priority: - // 1) - // 2) - // 3) - // 4) NULL - for(;;) - { - TMPQHash * pHash = ha->pHashTable + dwIndex; - - // If we found a matching entry, return that one - if(pHash->dwName1 == dwName1 && pHash->dwName2 == dwName2 && pHash->lcLocale == lcLocale) - return pHash; - - // If we found a deleted entry, remember it but keep searching - if(pHash->dwBlockIndex == HASH_ENTRY_DELETED && pDeletedEntry == NULL) - pDeletedEntry = pHash; - - // If we found a free entry, we need to stop searching - if(pHash->dwBlockIndex == HASH_ENTRY_FREE) - { - pFreeEntry = pHash; - break; - } - - // Move to the next hash entry. - // If we reached the starting entry, it's failure. - dwIndex = (dwIndex + 1) & dwHashIndexMask; - if(dwIndex == dwStartIndex) - break; - } - - // If we found a deleted entry, return that one preferentially - return (pDeletedEntry != NULL) ? pDeletedEntry : pFreeEntry; -} - -// Retrieves the first hash entry for the given file. -// Every locale version of a file has its own hash entry -TMPQHash * GetFirstHashEntry(TMPQArchive * ha, const char * szFileName) -{ - DWORD dwHashIndexMask = HASH_INDEX_MASK(ha); - DWORD dwStartIndex = ha->pfnHashString(szFileName, MPQ_HASH_TABLE_INDEX); - DWORD dwName1 = ha->pfnHashString(szFileName, MPQ_HASH_NAME_A); - DWORD dwName2 = ha->pfnHashString(szFileName, MPQ_HASH_NAME_B); - DWORD dwIndex; - - // Set the initial index - dwStartIndex = dwIndex = (dwStartIndex & dwHashIndexMask); - - // Search the hash table - for(;;) - { - TMPQHash * pHash = ha->pHashTable + dwIndex; - - // If the entry matches, we found it. - if(pHash->dwName1 == dwName1 && pHash->dwName2 == dwName2 && pHash->dwBlockIndex < ha->dwFileTableSize) - return pHash; - - // If that hash entry is a free entry, it means we haven't found the file - if(pHash->dwBlockIndex == HASH_ENTRY_FREE) - return NULL; - - // Move to the next hash entry. Stop searching - // if we got reached the original hash entry - dwIndex = (dwIndex + 1) & dwHashIndexMask; - if(dwIndex == dwStartIndex) - return NULL; - } -} - -TMPQHash * GetNextHashEntry(TMPQArchive * ha, TMPQHash * pFirstHash, TMPQHash * pHash) -{ - DWORD dwHashIndexMask = HASH_INDEX_MASK(ha); - DWORD dwStartIndex = (DWORD)(pFirstHash - ha->pHashTable); - DWORD dwName1 = pHash->dwName1; - DWORD dwName2 = pHash->dwName2; - DWORD dwIndex = (DWORD)(pHash - ha->pHashTable); - - // Now go for any next entry that follows the pHash, - // until either free hash entry was found, or the start entry was reached - for(;;) - { - // Move to the next hash entry. Stop searching - // if we got reached the original hash entry - dwIndex = (dwIndex + 1) & dwHashIndexMask; - if(dwIndex == dwStartIndex) - return NULL; - pHash = ha->pHashTable + dwIndex; - - // If the entry matches, we found it. - if(pHash->dwName1 == dwName1 && pHash->dwName2 == dwName2 && pHash->dwBlockIndex < ha->dwFileTableSize) - return pHash; - - // If that hash entry is a free entry, it means we haven't found the file - if(pHash->dwBlockIndex == HASH_ENTRY_FREE) - return NULL; - } -} - -// Allocates an entry in the hash table -TMPQHash * AllocateHashEntry( - TMPQArchive * ha, - TFileEntry * pFileEntry, - LCID lcLocale) -{ - TMPQHash * pHash; - DWORD dwStartIndex = ha->pfnHashString(pFileEntry->szFileName, MPQ_HASH_TABLE_INDEX); - DWORD dwName1 = ha->pfnHashString(pFileEntry->szFileName, MPQ_HASH_NAME_A); - DWORD dwName2 = ha->pfnHashString(pFileEntry->szFileName, MPQ_HASH_NAME_B); - - // Attempt to find a free hash entry - pHash = FindFreeHashEntry(ha, dwStartIndex, dwName1, dwName2, lcLocale); - if(pHash != NULL) - { - // Fill the free hash entry - pHash->dwName1 = dwName1; - pHash->dwName2 = dwName2; - pHash->lcLocale = (USHORT)lcLocale; - pHash->wPlatform = 0; - pHash->dwBlockIndex = (DWORD)(pFileEntry - ha->pFileTable); - } - - return pHash; -} - -// Finds a free space in the MPQ where to store next data -// The free space begins beyond the file that is stored at the fuhrtest -// position in the MPQ. (listfile), (attributes) and (signature) are ignored, -// unless the MPQ is being flushed. -ULONGLONG FindFreeMpqSpace(TMPQArchive * ha) -{ - TMPQHeader * pHeader = ha->pHeader; - TFileEntry * pFileTableEnd = ha->pFileTable + ha->dwFileTableSize; - TFileEntry * pFileEntry; - ULONGLONG FreeSpacePos = ha->pHeader->dwHeaderSize; - DWORD dwChunkCount; - - // Parse the entire block table - for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++) - { - // Only take existing files with nonzero size - if((pFileEntry->dwFlags & MPQ_FILE_EXISTS) && (pFileEntry->dwCmpSize != 0)) - { - // If we are not saving MPQ tables, ignore internal MPQ files - if((ha->dwFlags & MPQ_FLAG_SAVING_TABLES) == 0 && IsInternalMpqFileName(pFileEntry->szFileName)) - continue; - - // If the end of the file is bigger than current MPQ table pos, update it - if((pFileEntry->ByteOffset + pFileEntry->dwCmpSize) > FreeSpacePos) - { - // Get the end of the file data - FreeSpacePos = pFileEntry->ByteOffset + pFileEntry->dwCmpSize; - - // Add the MD5 chunks, if present - if(pHeader->dwRawChunkSize != 0 && pFileEntry->dwCmpSize != 0) - { - dwChunkCount = ((pFileEntry->dwCmpSize - 1) / pHeader->dwRawChunkSize) + 1; - FreeSpacePos += dwChunkCount * MD5_DIGEST_SIZE; - } - } - } - } - - // Give the free space position to the caller - return FreeSpacePos; -} - -//----------------------------------------------------------------------------- -// Common functions - MPQ File - -TMPQFile * CreateFileHandle(TMPQArchive * ha, TFileEntry * pFileEntry) -{ - TMPQFile * hf; - - // Allocate space for TMPQFile - hf = STORM_ALLOC(TMPQFile, 1); - if(hf != NULL) - { - // Fill the file structure - memset(hf, 0, sizeof(TMPQFile)); - hf->dwMagic = ID_MPQ_FILE; - hf->pStream = NULL; - hf->ha = ha; - - // If the called entered a file entry, we also copy informations from the file entry - if(ha != NULL && pFileEntry != NULL) - { - // Set the raw position and MPQ position - hf->RawFilePos = FileOffsetFromMpqOffset(ha, pFileEntry->ByteOffset); - hf->MpqFilePos = pFileEntry->ByteOffset; - - // Set the data size - hf->dwDataSize = pFileEntry->dwFileSize; - hf->pFileEntry = pFileEntry; - } - } - - return hf; -} - -TMPQFile * CreateWritableHandle(TMPQArchive * ha, DWORD dwFileSize) -{ - ULONGLONG FreeMpqSpace; - ULONGLONG TempPos; - TMPQFile * hf; - - // We need to find the position in the MPQ where we save the file data - FreeMpqSpace = FindFreeMpqSpace(ha); - - // When format V1, the size of the archive cannot exceed 4 GB - if(ha->pHeader->wFormatVersion == MPQ_FORMAT_VERSION_1) - { - TempPos = FreeMpqSpace + - dwFileSize + - (ha->pHeader->dwHashTableSize * sizeof(TMPQHash)) + - (ha->dwFileTableSize * sizeof(TMPQBlock)); - if((TempPos >> 32) != 0) - { - SetLastError(ERROR_DISK_FULL); - return NULL; - } - } - - // Allocate the file handle - hf = CreateFileHandle(ha, NULL); - if(hf == NULL) - { - SetLastError(ERROR_NOT_ENOUGH_MEMORY); - return NULL; - } - - // We need to find the position in the MPQ where we save the file data - hf->MpqFilePos = FreeMpqSpace; - hf->bIsWriteHandle = true; - return hf; -} - -// Loads a table from MPQ. -// Can be used for hash table, block table, sector offset table or sector checksum table -void * LoadMpqTable( - TMPQArchive * ha, - ULONGLONG ByteOffset, - DWORD dwCompressedSize, - DWORD dwTableSize, - DWORD dwKey, - bool * pbTableIsCut) -{ - ULONGLONG FileSize = 0; - LPBYTE pbCompressed = NULL; - LPBYTE pbMpqTable; - LPBYTE pbToRead; - DWORD dwBytesToRead = dwCompressedSize; - int nError = ERROR_SUCCESS; - - // Allocate the MPQ table - pbMpqTable = pbToRead = STORM_ALLOC(BYTE, dwTableSize); - if(pbMpqTable != NULL) - { - // Check if the MPQ table is encrypted - if(dwCompressedSize < dwTableSize) - { - // Allocate temporary buffer for holding compressed data - pbCompressed = pbToRead = STORM_ALLOC(BYTE, dwCompressedSize); - if(pbCompressed == NULL) - { - STORM_FREE(pbMpqTable); - return NULL; - } - } - - // Get the file offset from which we will read the table - // Note: According to Storm.dll from Warcraft III (version 2002), - // if the hash table position is 0xFFFFFFFF, no SetFilePointer call is done - // and the table is loaded from the current file offset - if(ByteOffset == SFILE_INVALID_POS) - FileStream_GetPos(ha->pStream, &ByteOffset); - - // On archives v 1.0, hash table and block table can go beyond EOF. - // Storm.dll reads as much as possible, then fills the missing part with zeros. - // Abused by Spazzler map protector which sets hash table size to 0x00100000 - if(ha->pHeader->wFormatVersion == MPQ_FORMAT_VERSION_1) - { - // Cut the table size - FileStream_GetSize(ha->pStream, &FileSize); - if((ByteOffset + dwBytesToRead) > FileSize) - { - // Fill the extra data with zeros - dwBytesToRead = (DWORD)(FileSize - ByteOffset); - memset(pbMpqTable + dwBytesToRead, 0, (dwTableSize - dwBytesToRead)); - - // Give the caller information that the table was cut - if(pbTableIsCut != NULL) - pbTableIsCut[0] = true; - } - } - - // If everything succeeded, read the raw table form the MPQ - if(FileStream_Read(ha->pStream, &ByteOffset, pbToRead, dwBytesToRead)) - { - // First of all, decrypt the table - if(dwKey != 0) - { - BSWAP_ARRAY32_UNSIGNED(pbToRead, dwCompressedSize); - DecryptMpqBlock(pbToRead, dwCompressedSize, dwKey); - BSWAP_ARRAY32_UNSIGNED(pbToRead, dwCompressedSize); - } - - // If the table is compressed, decompress it - if(dwCompressedSize < dwTableSize) - { - int cbOutBuffer = (int)dwTableSize; - int cbInBuffer = (int)dwCompressedSize; - - if(!SCompDecompress2(pbMpqTable, &cbOutBuffer, pbCompressed, cbInBuffer)) - nError = GetLastError(); - } - - // Make sure that the table is properly byte-swapped - BSWAP_ARRAY32_UNSIGNED(pbMpqTable, dwTableSize); - } - else - { - nError = GetLastError(); - } - - // If read failed, free the table and return - if(nError != ERROR_SUCCESS) - { - STORM_FREE(pbMpqTable); - pbMpqTable = NULL; - } - - // Free the compression buffer, if any - if(pbCompressed != NULL) - STORM_FREE(pbCompressed); - } - - // Return the MPQ table - return pbMpqTable; -} - -unsigned char * AllocateMd5Buffer( - DWORD dwRawDataSize, - DWORD dwChunkSize, - LPDWORD pcbMd5Size) -{ - unsigned char * md5_array; - DWORD cbMd5Size; - - // Sanity check - assert(dwRawDataSize != 0); - assert(dwChunkSize != 0); - - // Calculate how many MD5's we will calculate - cbMd5Size = (((dwRawDataSize - 1) / dwChunkSize) + 1) * MD5_DIGEST_SIZE; - - // Allocate space for array or MD5s - md5_array = STORM_ALLOC(BYTE, cbMd5Size); - - // Give the size of the MD5 array - if(pcbMd5Size != NULL) - *pcbMd5Size = cbMd5Size; - return md5_array; -} - -// Allocates sector buffer and sector offset table -int AllocateSectorBuffer(TMPQFile * hf) -{ - TMPQArchive * ha = hf->ha; - - // Caller of AllocateSectorBuffer must ensure these - assert(hf->pbFileSector == NULL); - assert(hf->pFileEntry != NULL); - assert(hf->ha != NULL); - - // Don't allocate anything if the file has zero size - if(hf->pFileEntry->dwFileSize == 0 || hf->dwDataSize == 0) - return ERROR_SUCCESS; - - // Determine the file sector size and allocate buffer for it - hf->dwSectorSize = (hf->pFileEntry->dwFlags & MPQ_FILE_SINGLE_UNIT) ? hf->dwDataSize : ha->dwSectorSize; - hf->pbFileSector = STORM_ALLOC(BYTE, hf->dwSectorSize); - hf->dwSectorOffs = SFILE_INVALID_POS; - - // Return result - return (hf->pbFileSector != NULL) ? (int)ERROR_SUCCESS : (int)ERROR_NOT_ENOUGH_MEMORY; -} - -// Allocates sector offset table -int AllocatePatchInfo(TMPQFile * hf, bool bLoadFromFile) -{ - TMPQArchive * ha = hf->ha; - DWORD dwLength = sizeof(TPatchInfo); - - // The following conditions must be true - assert(hf->pFileEntry->dwFlags & MPQ_FILE_PATCH_FILE); - assert(hf->pPatchInfo == NULL); - -__AllocateAndLoadPatchInfo: - - // Allocate space for patch header. Start with default size, - // and if its size if bigger, then we reload them - hf->pPatchInfo = STORM_ALLOC(TPatchInfo, 1); - if(hf->pPatchInfo == NULL) - return ERROR_NOT_ENOUGH_MEMORY; - - // Do we have to load the patch header from the file ? - if(bLoadFromFile) - { - // Load the patch header - if(!FileStream_Read(ha->pStream, &hf->RawFilePos, hf->pPatchInfo, dwLength)) - { - // Free the patch info - STORM_FREE(hf->pPatchInfo); - hf->pPatchInfo = NULL; - return GetLastError(); - } - - // Perform necessary swapping - hf->pPatchInfo->dwLength = BSWAP_INT32_UNSIGNED(hf->pPatchInfo->dwLength); - hf->pPatchInfo->dwFlags = BSWAP_INT32_UNSIGNED(hf->pPatchInfo->dwFlags); - hf->pPatchInfo->dwDataSize = BSWAP_INT32_UNSIGNED(hf->pPatchInfo->dwDataSize); - - // Verify the size of the patch header - // If it's not default size, we have to reload them - if(hf->pPatchInfo->dwLength > dwLength) - { - // Free the patch info - dwLength = hf->pPatchInfo->dwLength; - STORM_FREE(hf->pPatchInfo); - hf->pPatchInfo = NULL; - - // If the length is out of all possible ranges, fail the operation - if(dwLength > 0x400) - return ERROR_FILE_CORRUPT; - goto __AllocateAndLoadPatchInfo; - } - - // Patch file data size according to the patch header - hf->dwDataSize = hf->pPatchInfo->dwDataSize; - } - else - { - memset(hf->pPatchInfo, 0, dwLength); - } - - // Save the final length to the patch header - hf->pPatchInfo->dwLength = dwLength; - hf->pPatchInfo->dwFlags = 0x80000000; - return ERROR_SUCCESS; -} - -// Allocates sector offset table -int AllocateSectorOffsets(TMPQFile * hf, bool bLoadFromFile) -{ - TMPQArchive * ha = hf->ha; - TFileEntry * pFileEntry = hf->pFileEntry; - DWORD dwSectorOffsLen; - bool bSectorOffsetTableCorrupt = false; - - // Caller of AllocateSectorOffsets must ensure these - assert(hf->SectorOffsets == NULL); - assert(hf->pFileEntry != NULL); - assert(hf->dwDataSize != 0); - assert(hf->ha != NULL); - - // If the file is stored as single unit, just set number of sectors to 1 - if(pFileEntry->dwFlags & MPQ_FILE_SINGLE_UNIT) - { - hf->dwSectorCount = 1; - return ERROR_SUCCESS; - } - - // Calculate the number of data sectors - // Note that this doesn't work if the file size is zero - hf->dwSectorCount = ((hf->dwDataSize - 1) / hf->dwSectorSize) + 1; - - // Calculate the number of file sectors - dwSectorOffsLen = (hf->dwSectorCount + 1) * sizeof(DWORD); - - // If MPQ_FILE_SECTOR_CRC flag is set, there will either be extra DWORD - // or an array of MD5's. Either way, we read at least 4 bytes more - // in order to save additional read from the file. - if(pFileEntry->dwFlags & MPQ_FILE_SECTOR_CRC) - dwSectorOffsLen += sizeof(DWORD); - - // Only allocate and load the table if the file is compressed - if(pFileEntry->dwFlags & MPQ_FILE_COMPRESS_MASK) - { - __LoadSectorOffsets: - - // Allocate the sector offset table - hf->SectorOffsets = STORM_ALLOC(DWORD, (dwSectorOffsLen / sizeof(DWORD))); - if(hf->SectorOffsets == NULL) - return ERROR_NOT_ENOUGH_MEMORY; - - // Only read from the file if we are supposed to do so - if(bLoadFromFile) - { - ULONGLONG RawFilePos = hf->RawFilePos; - - // Append the length of the patch info, if any - if(hf->pPatchInfo != NULL) - { - if((RawFilePos + hf->pPatchInfo->dwLength) < RawFilePos) - return ERROR_FILE_CORRUPT; - RawFilePos += hf->pPatchInfo->dwLength; - } - - // Load the sector offsets from the file - if(!FileStream_Read(ha->pStream, &RawFilePos, hf->SectorOffsets, dwSectorOffsLen)) - { - // Free the sector offsets - STORM_FREE(hf->SectorOffsets); - hf->SectorOffsets = NULL; - return GetLastError(); - } - - // Swap the sector positions - BSWAP_ARRAY32_UNSIGNED(hf->SectorOffsets, dwSectorOffsLen); - - // Decrypt loaded sector positions if necessary - if(pFileEntry->dwFlags & MPQ_FILE_ENCRYPTED) - { - // If we don't know the file key, try to find it. - if(hf->dwFileKey == 0) - { - hf->dwFileKey = DetectFileKeyBySectorSize(hf->SectorOffsets, ha->dwSectorSize, dwSectorOffsLen); - if(hf->dwFileKey == 0) - { - STORM_FREE(hf->SectorOffsets); - hf->SectorOffsets = NULL; - return ERROR_UNKNOWN_FILE_KEY; - } - } - - // Decrypt sector positions - DecryptMpqBlock(hf->SectorOffsets, dwSectorOffsLen, hf->dwFileKey - 1); - } - - // - // Validate the sector offset table - // - // Note: Some MPQ protectors put the actual file data before the sector offset table. - // In this case, the sector offsets are negative (> 0x80000000). - // - - for(DWORD i = 0; i < hf->dwSectorCount; i++) - { - DWORD dwSectorOffset1 = hf->SectorOffsets[i+1]; - DWORD dwSectorOffset0 = hf->SectorOffsets[i]; - - // Every following sector offset must be bigger than the previous one - if(dwSectorOffset1 <= dwSectorOffset0) - { - bSectorOffsetTableCorrupt = true; - break; - } - - // The sector size must not be bigger than compressed file size - // Edit: Yes, but apparently, in original Storm.dll, the compressed - // size is not checked anywhere. However, we need to do this check - // in order to sector offset table malformed by MPQ protectors - if((dwSectorOffset1 - dwSectorOffset0) > ha->dwSectorSize) - { - bSectorOffsetTableCorrupt = true; - break; - } - } - - // If data corruption detected, free the sector offset table - if(bSectorOffsetTableCorrupt) - { - STORM_FREE(hf->SectorOffsets); - hf->SectorOffsets = NULL; - return ERROR_FILE_CORRUPT; - } - - // - // There may be various extra DWORDs loaded after the sector offset table. - // They are mostly empty on WoW release MPQs, but on MPQs from PTR, - // they contain random non-zero data. Their meaning is unknown. - // - // These extra values are, however, include in the dwCmpSize in the file - // table. We cannot ignore them, because compacting archive would fail - // - - if(hf->SectorOffsets[0] > dwSectorOffsLen) - { - // MPQ protectors put some ridiculous values there. We must limit the extra bytes - if(hf->SectorOffsets[0] > (dwSectorOffsLen + 0x400)) - return ERROR_FILE_CORRUPT; - - // Free the old sector offset table - dwSectorOffsLen = hf->SectorOffsets[0]; - STORM_FREE(hf->SectorOffsets); - goto __LoadSectorOffsets; - } - } - else - { - memset(hf->SectorOffsets, 0, dwSectorOffsLen); - hf->SectorOffsets[0] = dwSectorOffsLen; - } - } - - return ERROR_SUCCESS; -} - -int AllocateSectorChecksums(TMPQFile * hf, bool bLoadFromFile) -{ - TMPQArchive * ha = hf->ha; - TFileEntry * pFileEntry = hf->pFileEntry; - ULONGLONG RawFilePos; - DWORD dwCompressedSize = 0; - DWORD dwExpectedSize; - DWORD dwCrcOffset; // Offset of the CRC table, relative to file offset in the MPQ - DWORD dwCrcSize; - - // Caller of AllocateSectorChecksums must ensure these - assert(hf->SectorChksums == NULL); - assert(hf->SectorOffsets != NULL); - assert(hf->pFileEntry != NULL); - assert(hf->ha != NULL); - - // Single unit files don't have sector checksums - if(pFileEntry->dwFlags & MPQ_FILE_SINGLE_UNIT) - return ERROR_SUCCESS; - - // Caller must ensure that we are only called when we have sector checksums - assert(pFileEntry->dwFlags & MPQ_FILE_SECTOR_CRC); - - // - // Older MPQs store an array of CRC32's after - // the raw file data in the MPQ. - // - // In newer MPQs, the (since Cataclysm BETA) the (attributes) file - // contains additional 32-bit values beyond the sector table. - // Their number depends on size of the (attributes), but their - // meaning is unknown. They are usually zeroed in retail game files, - // but contain some sort of checksum in BETA MPQs - // - - // Does the size of the file table match with the CRC32-based checksums? - dwExpectedSize = (hf->dwSectorCount + 2) * sizeof(DWORD); - if(hf->SectorOffsets[0] != 0 && hf->SectorOffsets[0] == dwExpectedSize) - { - // If we are not loading from the MPQ file, we just allocate the sector table - // In that case, do not check any sizes - if(bLoadFromFile == false) - { - hf->SectorChksums = STORM_ALLOC(DWORD, hf->dwSectorCount); - if(hf->SectorChksums == NULL) - return ERROR_NOT_ENOUGH_MEMORY; - - // Fill the checksum table with zeros - memset(hf->SectorChksums, 0, hf->dwSectorCount * sizeof(DWORD)); - return ERROR_SUCCESS; - } - else - { - // Is there valid size of the sector checksums? - if(hf->SectorOffsets[hf->dwSectorCount + 1] >= hf->SectorOffsets[hf->dwSectorCount]) - dwCompressedSize = hf->SectorOffsets[hf->dwSectorCount + 1] - hf->SectorOffsets[hf->dwSectorCount]; - - // Ignore cases when the length is too small or too big. - if(dwCompressedSize < sizeof(DWORD) || dwCompressedSize > hf->dwSectorSize) - return ERROR_SUCCESS; - - // Calculate offset of the CRC table - dwCrcSize = hf->dwSectorCount * sizeof(DWORD); - dwCrcOffset = hf->SectorOffsets[hf->dwSectorCount]; - RawFilePos = CalculateRawSectorOffset(hf, dwCrcOffset); - - // Now read the table from the MPQ - hf->SectorChksums = (DWORD *)LoadMpqTable(ha, RawFilePos, dwCompressedSize, dwCrcSize, 0, NULL); - if(hf->SectorChksums == NULL) - return ERROR_NOT_ENOUGH_MEMORY; - } - } - - // If the size doesn't match, we ignore sector checksums -// assert(false); - return ERROR_SUCCESS; -} - -int WritePatchInfo(TMPQFile * hf) -{ - TMPQArchive * ha = hf->ha; - TPatchInfo * pPatchInfo = hf->pPatchInfo; - - // The caller must make sure that this function is only called - // when the following is true. - assert(hf->pFileEntry->dwFlags & MPQ_FILE_PATCH_FILE); - assert(pPatchInfo != NULL); - - BSWAP_ARRAY32_UNSIGNED(pPatchInfo, 3 * sizeof(DWORD)); - if(!FileStream_Write(ha->pStream, &hf->RawFilePos, pPatchInfo, sizeof(TPatchInfo))) - return GetLastError(); - - return ERROR_SUCCESS; -} - -int WriteSectorOffsets(TMPQFile * hf) -{ - TMPQArchive * ha = hf->ha; - TFileEntry * pFileEntry = hf->pFileEntry; - ULONGLONG RawFilePos = hf->RawFilePos; - DWORD dwSectorOffsLen; - - // The caller must make sure that this function is only called - // when the following is true. - assert(hf->pFileEntry->dwFlags & MPQ_FILE_COMPRESS_MASK); - assert(hf->SectorOffsets != NULL); - dwSectorOffsLen = hf->SectorOffsets[0]; - - // If file is encrypted, sector positions are also encrypted - if(pFileEntry->dwFlags & MPQ_FILE_ENCRYPTED) - EncryptMpqBlock(hf->SectorOffsets, dwSectorOffsLen, hf->dwFileKey - 1); - BSWAP_ARRAY32_UNSIGNED(hf->SectorOffsets, dwSectorOffsLen); - - // Adjust sector offset table position, if we also have patch info - if(hf->pPatchInfo != NULL) - RawFilePos += hf->pPatchInfo->dwLength; - - // Write sector offsets to the archive - if(!FileStream_Write(ha->pStream, &RawFilePos, hf->SectorOffsets, dwSectorOffsLen)) - return GetLastError(); - - // Not necessary, as the sector checksums - // are going to be freed when this is done. -// BSWAP_ARRAY32_UNSIGNED(hf->SectorOffsets, dwSectorOffsLen); - return ERROR_SUCCESS; -} - - -int WriteSectorChecksums(TMPQFile * hf) -{ - TMPQArchive * ha = hf->ha; - ULONGLONG RawFilePos; - TFileEntry * pFileEntry = hf->pFileEntry; - LPBYTE pbCompressed; - DWORD dwCompressedSize = 0; - DWORD dwCrcSize; - int nOutSize; - int nError = ERROR_SUCCESS; - - // The caller must make sure that this function is only called - // when the following is true. - assert(hf->pFileEntry->dwFlags & MPQ_FILE_SECTOR_CRC); - assert(hf->SectorOffsets != NULL); - assert(hf->SectorChksums != NULL); - - // If the MPQ has MD5 of each raw data chunk, - // we leave sector offsets empty - if(ha->pHeader->dwRawChunkSize != 0) - { - hf->SectorOffsets[hf->dwSectorCount + 1] = hf->SectorOffsets[hf->dwSectorCount]; - return ERROR_SUCCESS; - } - - // Calculate size of the checksum array - dwCrcSize = hf->dwSectorCount * sizeof(DWORD); - - // Allocate buffer for compressed sector CRCs. - pbCompressed = STORM_ALLOC(BYTE, dwCrcSize); - if(pbCompressed == NULL) - return ERROR_NOT_ENOUGH_MEMORY; - - // Perform the compression - BSWAP_ARRAY32_UNSIGNED(hf->SectorChksums, dwCrcSize); - - nOutSize = (int)dwCrcSize; - SCompCompress(pbCompressed, &nOutSize, hf->SectorChksums, (int)dwCrcSize, MPQ_COMPRESSION_ZLIB, 0, 0); - dwCompressedSize = (DWORD)nOutSize; - - // Write the sector CRCs to the archive - RawFilePos = hf->RawFilePos + hf->SectorOffsets[hf->dwSectorCount]; - if(hf->pPatchInfo != NULL) - RawFilePos += hf->pPatchInfo->dwLength; - if(!FileStream_Write(ha->pStream, &RawFilePos, pbCompressed, dwCompressedSize)) - nError = GetLastError(); - - // Not necessary, as the sector checksums - // are going to be freed when this is done. -// BSWAP_ARRAY32_UNSIGNED(hf->SectorChksums, dwCrcSize); - - // Store the sector CRCs - hf->SectorOffsets[hf->dwSectorCount + 1] = hf->SectorOffsets[hf->dwSectorCount] + dwCompressedSize; - pFileEntry->dwCmpSize += dwCompressedSize; - STORM_FREE(pbCompressed); - return nError; -} - -int WriteMemDataMD5( - TFileStream * pStream, - ULONGLONG RawDataOffs, - void * pvRawData, - DWORD dwRawDataSize, - DWORD dwChunkSize, - LPDWORD pcbTotalSize) -{ - unsigned char * md5_array; - unsigned char * md5; - LPBYTE pbRawData = (LPBYTE)pvRawData; - DWORD dwBytesRemaining = dwRawDataSize; - DWORD dwMd5ArraySize = 0; - int nError = ERROR_SUCCESS; - - // Allocate buffer for array of MD5 - md5_array = md5 = AllocateMd5Buffer(dwRawDataSize, dwChunkSize, &dwMd5ArraySize); - if(md5_array == NULL) - return ERROR_NOT_ENOUGH_MEMORY; - - // For every file chunk, calculate MD5 - while(dwBytesRemaining != 0) - { - // Get the remaining number of bytes to read - dwChunkSize = STORMLIB_MIN(dwBytesRemaining, dwChunkSize); - - // Calculate MD5 - CalculateDataBlockHash(pbRawData, dwChunkSize, md5); - md5 += MD5_DIGEST_SIZE; - - // Move offset and size - dwBytesRemaining -= dwChunkSize; - pbRawData += dwChunkSize; - } - - // Write the array od MD5's to the file - RawDataOffs += dwRawDataSize; - if(!FileStream_Write(pStream, &RawDataOffs, md5_array, dwMd5ArraySize)) - nError = GetLastError(); - - // Give the caller the size of the MD5 array - if(pcbTotalSize != NULL) - *pcbTotalSize = dwRawDataSize + dwMd5ArraySize; - - // Free buffers and exit - STORM_FREE(md5_array); - return nError; -} - - -// Writes the MD5 for each chunk of the raw file data -int WriteMpqDataMD5( - TFileStream * pStream, - ULONGLONG RawDataOffs, - DWORD dwRawDataSize, - DWORD dwChunkSize) -{ - unsigned char * md5_array; - unsigned char * md5; - LPBYTE pbFileChunk; - DWORD dwMd5ArraySize = 0; - DWORD dwToRead = dwRawDataSize; - int nError = ERROR_SUCCESS; - - // Allocate buffer for array of MD5 - md5_array = md5 = AllocateMd5Buffer(dwRawDataSize, dwChunkSize, &dwMd5ArraySize); - if(md5_array == NULL) - return ERROR_NOT_ENOUGH_MEMORY; - - // Allocate space for file chunk - pbFileChunk = STORM_ALLOC(BYTE, dwChunkSize); - if(pbFileChunk == NULL) - { - STORM_FREE(md5_array); - return ERROR_NOT_ENOUGH_MEMORY; - } - - // For every file chunk, calculate MD5 - while(dwRawDataSize != 0) - { - // Get the remaining number of bytes to read - dwToRead = STORMLIB_MIN(dwRawDataSize, dwChunkSize); - - // Read the chunk - if(!FileStream_Read(pStream, &RawDataOffs, pbFileChunk, dwToRead)) - { - nError = GetLastError(); - break; - } - - // Calculate MD5 - CalculateDataBlockHash(pbFileChunk, dwToRead, md5); - md5 += MD5_DIGEST_SIZE; - - // Move offset and size - RawDataOffs += dwToRead; - dwRawDataSize -= dwToRead; - } - - // Write the array od MD5's to the file - if(nError == ERROR_SUCCESS) - { - if(!FileStream_Write(pStream, NULL, md5_array, dwMd5ArraySize)) - nError = GetLastError(); - } - - // Free buffers and exit - STORM_FREE(pbFileChunk); - STORM_FREE(md5_array); - return nError; -} - -// Frees the structure for MPQ file -void FreeFileHandle(TMPQFile *& hf) -{ - if(hf != NULL) - { - // If we have patch file attached to this one, free it first - if(hf->hfPatch != NULL) - FreeFileHandle(hf->hfPatch); - - // Then free all buffers allocated in the file structure - if(hf->pbFileData != NULL) - STORM_FREE(hf->pbFileData); - if(hf->pPatchInfo != NULL) - STORM_FREE(hf->pPatchInfo); - if(hf->SectorOffsets != NULL) - STORM_FREE(hf->SectorOffsets); - if(hf->SectorChksums != NULL) - STORM_FREE(hf->SectorChksums); - if(hf->pbFileSector != NULL) - STORM_FREE(hf->pbFileSector); - if(hf->pStream != NULL) - FileStream_Close(hf->pStream); - STORM_FREE(hf); - hf = NULL; - } -} - -// Frees the MPQ archive -void FreeArchiveHandle(TMPQArchive *& ha) -{ - if(ha != NULL) - { - // First of all, free the patch archive, if any - if(ha->haPatch != NULL) - FreeArchiveHandle(ha->haPatch); - - // Free the patch prefix, if any - if(ha->pPatchPrefix != NULL) - STORM_FREE(ha->pPatchPrefix); - - // Close the file stream - FileStream_Close(ha->pStream); - ha->pStream = NULL; - - // Free the file names from the file table - if(ha->pFileTable != NULL) - { - for(DWORD i = 0; i < ha->dwFileTableSize; i++) - { - if(ha->pFileTable[i].szFileName != NULL) - STORM_FREE(ha->pFileTable[i].szFileName); - ha->pFileTable[i].szFileName = NULL; - } - - // Then free all buffers allocated in the archive structure - STORM_FREE(ha->pFileTable); - } - - if(ha->pHashTable != NULL) - STORM_FREE(ha->pHashTable); - if(ha->pHetTable != NULL) - FreeHetTable(ha->pHetTable); - STORM_FREE(ha); - ha = NULL; - } -} - -bool IsInternalMpqFileName(const char * szFileName) -{ - if(szFileName != NULL && szFileName[0] == '(') - { - if(!_stricmp(szFileName, LISTFILE_NAME) || - !_stricmp(szFileName, ATTRIBUTES_NAME) || - !_stricmp(szFileName, SIGNATURE_NAME)) - { - return true; - } - } - - return false; -} - -// Verifies if the file name is a pseudo-name -bool IsPseudoFileName(const char * szFileName, DWORD * pdwFileIndex) -{ - DWORD dwFileIndex = 0; - - if(szFileName != NULL) - { - // Must be "File########.ext" - if(!_strnicmp(szFileName, "File", 4)) - { - // Check 8 digits - for(int i = 4; i < 4+8; i++) - { - if(szFileName[i] < '0' || szFileName[i] > '9') - return false; - dwFileIndex = (dwFileIndex * 10) + (szFileName[i] - '0'); - } - - // An extension must follow - if(szFileName[12] == '.') - { - if(pdwFileIndex != NULL) - *pdwFileIndex = dwFileIndex; - return true; - } - } - } - - // Not a pseudo-name - return false; -} - -//----------------------------------------------------------------------------- -// Functions calculating and verifying the MD5 signature - -bool IsValidMD5(LPBYTE pbMd5) -{ - LPDWORD Md5 = (LPDWORD)pbMd5; - - return (Md5[0] | Md5[1] | Md5[2] | Md5[3]) ? true : false; -} - -bool IsValidSignature(LPBYTE pbSignature) -{ - LPDWORD Signature = (LPDWORD)pbSignature; - DWORD SigValid = 0; - - for(int i = 0; i < MPQ_WEAK_SIGNATURE_SIZE / sizeof(DWORD); i++) - SigValid |= Signature[i]; - - return (SigValid != 0) ? true : false; -} - - -bool VerifyDataBlockHash(void * pvDataBlock, DWORD cbDataBlock, LPBYTE expected_md5) -{ - hash_state md5_state; - BYTE md5_digest[MD5_DIGEST_SIZE]; - - // Don't verify the block if the MD5 is not valid. - if(!IsValidMD5(expected_md5)) - return true; - - // Calculate the MD5 of the data block - md5_init(&md5_state); - md5_process(&md5_state, (unsigned char *)pvDataBlock, cbDataBlock); - md5_done(&md5_state, md5_digest); - - // Does the MD5's match? - return (memcmp(md5_digest, expected_md5, MD5_DIGEST_SIZE) == 0); -} - -void CalculateDataBlockHash(void * pvDataBlock, DWORD cbDataBlock, LPBYTE md5_hash) -{ - hash_state md5_state; - - md5_init(&md5_state); - md5_process(&md5_state, (unsigned char *)pvDataBlock, cbDataBlock); - md5_done(&md5_state, md5_hash); -} - - -//----------------------------------------------------------------------------- -// Swapping functions - -#ifndef PLATFORM_LITTLE_ENDIAN - -// -// Note that those functions are implemented for Mac operating system, -// as this is the only supported platform that uses big endian. -// - -// Swaps a signed 16-bit integer -int16_t SwapInt16(uint16_t data) -{ - return (int16_t)CFSwapInt16(data); -} - -// Swaps an unsigned 16-bit integer -uint16_t SwapUInt16(uint16_t data) -{ - return CFSwapInt16(data); -} - -// Swaps signed 32-bit integer -int32_t SwapInt32(uint32_t data) -{ - return (int32_t)CFSwapInt32(data); -} - -// Swaps an unsigned 32-bit integer -uint32_t SwapUInt32(uint32_t data) -{ - return CFSwapInt32(data); -} - -// Swaps signed 64-bit integer -int64_t SwapInt64(int64_t data) -{ - return (int64_t)CFSwapInt64(data); -} - -// Swaps an unsigned 64-bit integer -uint64_t SwapUInt64(uint64_t data) -{ - return CFSwapInt64(data); -} - -// Swaps array of unsigned 16-bit integers -void ConvertUInt16Buffer(void * ptr, size_t length) -{ - uint16_t * buffer = (uint16_t *)ptr; - uint32_t nElements = (uint32_t)(length / sizeof(uint16_t)); - - while(nElements-- > 0) - { - *buffer = SwapUInt16(*buffer); - buffer++; - } -} - -// Swaps array of unsigned 32-bit integers -void ConvertUInt32Buffer(void * ptr, size_t length) -{ - uint32_t * buffer = (uint32_t *)ptr; - uint32_t nElements = (uint32_t)(length / sizeof(uint32_t)); - - while(nElements-- > 0) - { - *buffer = SwapUInt32(*buffer); - buffer++; - } -} - -// Swaps array of unsigned 64-bit integers -void ConvertUInt64Buffer(void * ptr, size_t length) -{ - uint64_t * buffer = (uint64_t *)ptr; - uint32_t nElements = (uint32_t)(length / sizeof(uint64_t)); - - while(nElements-- > 0) - { - *buffer = SwapUInt64(*buffer); - buffer++; - } -} - -// Swaps the TMPQHeader structure -void ConvertTMPQHeader(void *header, uint16_t version) -{ - TMPQHeader * theHeader = (TMPQHeader *)header; - - // Swap header part version 1 - if(version == MPQ_FORMAT_VERSION_1) - { - theHeader->dwID = SwapUInt32(theHeader->dwID); - theHeader->dwHeaderSize = SwapUInt32(theHeader->dwHeaderSize); - theHeader->dwArchiveSize = SwapUInt32(theHeader->dwArchiveSize); - theHeader->wFormatVersion = SwapUInt16(theHeader->wFormatVersion); - theHeader->wSectorSize = SwapUInt16(theHeader->wSectorSize); - theHeader->dwHashTablePos = SwapUInt32(theHeader->dwHashTablePos); - theHeader->dwBlockTablePos = SwapUInt32(theHeader->dwBlockTablePos); - theHeader->dwHashTableSize = SwapUInt32(theHeader->dwHashTableSize); - theHeader->dwBlockTableSize = SwapUInt32(theHeader->dwBlockTableSize); - } - - if(version == MPQ_FORMAT_VERSION_2) - { - theHeader->HiBlockTablePos64 = SwapUInt64(theHeader->HiBlockTablePos64); - theHeader->wHashTablePosHi = SwapUInt16(theHeader->wHashTablePosHi); - theHeader->wBlockTablePosHi = SwapUInt16(theHeader->wBlockTablePosHi); - } - - if(version == MPQ_FORMAT_VERSION_3) - { - theHeader->ArchiveSize64 = SwapUInt64(theHeader->ArchiveSize64); - theHeader->BetTablePos64 = SwapUInt64(theHeader->BetTablePos64); - theHeader->HetTablePos64 = SwapUInt64(theHeader->HetTablePos64); - } - - if(version == MPQ_FORMAT_VERSION_4) - { - theHeader->HashTableSize64 = SwapUInt64(theHeader->HashTableSize64); - theHeader->BlockTableSize64 = SwapUInt64(theHeader->BlockTableSize64); - theHeader->HiBlockTableSize64 = SwapUInt64(theHeader->HiBlockTableSize64); - theHeader->HetTableSize64 = SwapUInt64(theHeader->HetTableSize64); - theHeader->BetTableSize64 = SwapUInt64(theHeader->BetTableSize64); - } -} - -#endif // PLATFORM_LITTLE_ENDIAN +/*****************************************************************************/ +/* SBaseCommon.cpp Copyright (c) Ladislav Zezula 2003 */ +/*---------------------------------------------------------------------------*/ +/* Common functions for StormLib, used by all SFile*** modules */ +/*---------------------------------------------------------------------------*/ +/* Date Ver Who Comment */ +/* -------- ---- --- ------- */ +/* 24.03.03 1.00 Lad The first version of SFileCommon.cpp */ +/* 19.11.03 1.01 Dan Big endian handling */ +/* 12.06.04 1.01 Lad Renamed to SCommon.cpp */ +/* 06.09.10 1.01 Lad Renamed to SBaseCommon.cpp */ +/*****************************************************************************/ + +#define __STORMLIB_SELF__ +#include "StormLib.h" +#include "StormCommon.h" + +char StormLibCopyright[] = "StormLib v " STORMLIB_VERSION_STRING " Copyright Ladislav Zezula 1998-2014"; + +//----------------------------------------------------------------------------- +// Local variables + +LCID lcFileLocale = LANG_NEUTRAL; // File locale +USHORT wPlatform = 0; // File platform + +//----------------------------------------------------------------------------- +// Conversion to uppercase/lowercase + +// Converts ASCII characters to lowercase +// Converts slash (0x2F) to backslash (0x5C) +unsigned char AsciiToLowerTable[256] = +{ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x5C, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, + 0x40, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, + 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F, + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, + 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, + 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, + 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF, + 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, + 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF +}; + +// Converts ASCII characters to uppercase +// Converts slash (0x2F) to backslash (0x5C) +unsigned char AsciiToUpperTable[256] = +{ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x5C, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, + 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, + 0x60, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, + 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F, + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, + 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, + 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, + 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF, + 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, + 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF +}; + +// Converts ASCII characters to uppercase +// Does NOT convert slash (0x2F) to backslash (0x5C) +unsigned char AsciiToUpperTable_Slash[256] = +{ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, + 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, + 0x60, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, + 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F, + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, + 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, + 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, + 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF, + 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, + 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF +}; + +//----------------------------------------------------------------------------- +// Safe string functions + +void StringCopyA(char * dest, const char * src, size_t nMaxChars) +{ + size_t nLength = strlen(src); + + // Don't copy more than nMaxChars + nLength = STORMLIB_MIN(nLength, nMaxChars); + memcpy(dest, src, nLength); + dest[nLength] = 0; +} + +void StringCatA(char * dest, const char * src, size_t nMaxChars) +{ + size_t nLength1 = strlen(dest); + size_t nLength2 = strlen(src); + + // Don't copy more than nMaxChars + if(nLength1 < nMaxChars) + { + nLength2 = STORMLIB_MIN(nLength2, (nMaxChars - nLength1)); + memcpy(dest + nLength1, src, nLength2); + dest[nLength1 + nLength2] = 0; + } +} + +void StringCopyT(TCHAR * dest, const TCHAR * src, size_t nMaxChars) +{ + size_t nLength = _tcslen(src); + + // Don't copy more than nMaxChars + nLength = STORMLIB_MIN(nLength, nMaxChars); + memcpy(dest, src, (nLength * sizeof(TCHAR))); + dest[nLength] = 0; +} + +void StringCatT(TCHAR * dest, const TCHAR * src, size_t nMaxChars) +{ + size_t nLength1 = _tcslen(dest); + size_t nLength2 = _tcslen(src); + + // Don't copy more than nMaxChars + if(nLength1 < nMaxChars) + { + nLength2 = STORMLIB_MIN(nLength2, (nMaxChars - nLength1)); + memcpy(dest + nLength1, src, (nLength2 * sizeof(TCHAR))); + dest[nLength1 + nLength2] = 0; + } +} + +//----------------------------------------------------------------------------- +// Storm hashing functions + +#define STORM_BUFFER_SIZE 0x500 + +#define HASH_INDEX_MASK(ha) (ha->pHeader->dwHashTableSize ? (ha->pHeader->dwHashTableSize - 1) : 0) + +static DWORD StormBuffer[STORM_BUFFER_SIZE]; // Buffer for the decryption engine +static bool bMpqCryptographyInitialized = false; + +void InitializeMpqCryptography() +{ + DWORD dwSeed = 0x00100001; + DWORD index1 = 0; + DWORD index2 = 0; + int i; + + // Initialize the decryption buffer. + // Do nothing if already done. + if(bMpqCryptographyInitialized == false) + { + for(index1 = 0; index1 < 0x100; index1++) + { + for(index2 = index1, i = 0; i < 5; i++, index2 += 0x100) + { + DWORD temp1, temp2; + + dwSeed = (dwSeed * 125 + 3) % 0x2AAAAB; + temp1 = (dwSeed & 0xFFFF) << 0x10; + + dwSeed = (dwSeed * 125 + 3) % 0x2AAAAB; + temp2 = (dwSeed & 0xFFFF); + + StormBuffer[index2] = (temp1 | temp2); + } + } + + // Also register both MD5 and SHA1 hash algorithms + register_hash(&md5_desc); + register_hash(&sha1_desc); + + // Use LibTomMath as support math library for LibTomCrypt + ltc_mp = ltm_desc; + + // Don't do that again + bMpqCryptographyInitialized = true; + } +} + +DWORD HashString(const char * szFileName, DWORD dwHashType) +{ + LPBYTE pbKey = (BYTE *)szFileName; + DWORD dwSeed1 = 0x7FED7FED; + DWORD dwSeed2 = 0xEEEEEEEE; + DWORD ch; + + while(*pbKey != 0) + { + // Convert the input character to uppercase + // Convert slash (0x2F) to backslash (0x5C) + ch = AsciiToUpperTable[*pbKey++]; + + dwSeed1 = StormBuffer[dwHashType + ch] ^ (dwSeed1 + dwSeed2); + dwSeed2 = ch + dwSeed1 + dwSeed2 + (dwSeed2 << 5) + 3; + } + + return dwSeed1; +} + +DWORD HashStringSlash(const char * szFileName, DWORD dwHashType) +{ + LPBYTE pbKey = (BYTE *)szFileName; + DWORD dwSeed1 = 0x7FED7FED; + DWORD dwSeed2 = 0xEEEEEEEE; + DWORD ch; + + while(*pbKey != 0) + { + // Convert the input character to uppercase + // DON'T convert slash (0x2F) to backslash (0x5C) + ch = AsciiToUpperTable_Slash[*pbKey++]; + + dwSeed1 = StormBuffer[dwHashType + ch] ^ (dwSeed1 + dwSeed2); + dwSeed2 = ch + dwSeed1 + dwSeed2 + (dwSeed2 << 5) + 3; + } + + return dwSeed1; +} + +DWORD HashStringLower(const char * szFileName, DWORD dwHashType) +{ + LPBYTE pbKey = (BYTE *)szFileName; + DWORD dwSeed1 = 0x7FED7FED; + DWORD dwSeed2 = 0xEEEEEEEE; + DWORD ch; + + while(*pbKey != 0) + { + // Convert the input character to lower + // DON'T convert slash (0x2F) to backslash (0x5C) + ch = AsciiToLowerTable[*pbKey++]; + + dwSeed1 = StormBuffer[dwHashType + ch] ^ (dwSeed1 + dwSeed2); + dwSeed2 = ch + dwSeed1 + dwSeed2 + (dwSeed2 << 5) + 3; + } + + return dwSeed1; +} + +//----------------------------------------------------------------------------- +// Calculates the hash table size for a given amount of files + +DWORD GetHashTableSizeForFileCount(DWORD dwFileCount) +{ + DWORD dwPowerOfTwo = HASH_TABLE_SIZE_MIN; + + // For zero files, there is no hash table needed + if(dwFileCount == 0) + return 0; + + // Round the hash table size up to the nearest power of two + // Don't allow the hash table size go over allowed maximum + while(dwPowerOfTwo < HASH_TABLE_SIZE_MAX && dwPowerOfTwo < dwFileCount) + dwPowerOfTwo <<= 1; + return dwPowerOfTwo; +} + +//----------------------------------------------------------------------------- +// Calculates a Jenkin's Encrypting and decrypting MPQ file data + +ULONGLONG HashStringJenkins(const char * szFileName) +{ + LPBYTE pbFileName = (LPBYTE)szFileName; + char szNameBuff[0x108]; + size_t nLength = 0; + unsigned int primary_hash = 1; + unsigned int secondary_hash = 2; + + // Normalize the file name - convert to uppercase, and convert "/" to "\\". + if(pbFileName != NULL) + { + char * szNamePtr = szNameBuff; + char * szNameEnd = szNamePtr + sizeof(szNameBuff); + + // Normalize the file name. Doesn't have to be zero terminated for hashing + while(szNamePtr < szNameEnd && pbFileName[0] != 0) + *szNamePtr++ = (char)AsciiToLowerTable[*pbFileName++]; + nLength = szNamePtr - szNameBuff; + } + + // Thanks Quantam for finding out what the algorithm is. + // I am really getting old for reversing large chunks of assembly + // that does hashing :-) + hashlittle2(szNameBuff, nLength, &secondary_hash, &primary_hash); + + // Combine those 2 together + return ((ULONGLONG)primary_hash << 0x20) | (ULONGLONG)secondary_hash; +} + +//----------------------------------------------------------------------------- +// Default flags for (attributes) and (listfile) + +DWORD GetDefaultSpecialFileFlags(DWORD dwFileSize, USHORT wFormatVersion) +{ + // Fixed for format 1.0 + if(wFormatVersion == MPQ_FORMAT_VERSION_1) + return MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_FIX_KEY; + + // Size-dependent for formats 2.0-4.0 + return (dwFileSize > 0x4000) ? (MPQ_FILE_COMPRESS | MPQ_FILE_SECTOR_CRC) : (MPQ_FILE_COMPRESS | MPQ_FILE_SINGLE_UNIT); +} + + +//----------------------------------------------------------------------------- +// Encrypting/Decrypting MPQ data block + +void EncryptMpqBlock(void * pvDataBlock, DWORD dwLength, DWORD dwKey1) +{ + LPDWORD DataBlock = (LPDWORD)pvDataBlock; + DWORD dwValue32; + DWORD dwKey2 = 0xEEEEEEEE; + + // Round to DWORDs + dwLength >>= 2; + + // Encrypt the data block at array of DWORDs + for(DWORD i = 0; i < dwLength; i++) + { + // Modify the second key + dwKey2 += StormBuffer[MPQ_HASH_KEY2_MIX + (dwKey1 & 0xFF)]; + + dwValue32 = DataBlock[i]; + DataBlock[i] = DataBlock[i] ^ (dwKey1 + dwKey2); + + dwKey1 = ((~dwKey1 << 0x15) + 0x11111111) | (dwKey1 >> 0x0B); + dwKey2 = dwValue32 + dwKey2 + (dwKey2 << 5) + 3; + } +} + +void DecryptMpqBlock(void * pvDataBlock, DWORD dwLength, DWORD dwKey1) +{ + LPDWORD DataBlock = (LPDWORD)pvDataBlock; + DWORD dwValue32; + DWORD dwKey2 = 0xEEEEEEEE; + + // Round to DWORDs + dwLength >>= 2; + + // Decrypt the data block at array of DWORDs + for(DWORD i = 0; i < dwLength; i++) + { + // Modify the second key + dwKey2 += StormBuffer[MPQ_HASH_KEY2_MIX + (dwKey1 & 0xFF)]; + + DataBlock[i] = DataBlock[i] ^ (dwKey1 + dwKey2); + dwValue32 = DataBlock[i]; + + dwKey1 = ((~dwKey1 << 0x15) + 0x11111111) | (dwKey1 >> 0x0B); + dwKey2 = dwValue32 + dwKey2 + (dwKey2 << 5) + 3; + } +} + +/** + * Functions tries to get file decryption key. This comes from these facts + * + * - We know the decrypted value of the first DWORD in the encrypted data + * - We know the decrypted value of the second DWORD (at least aproximately) + * - There is only 256 variants of how the second key is modified + * + * The first iteration of dwKey1 and dwKey2 is this: + * + * dwKey2 = 0xEEEEEEEE + StormBuffer[MPQ_HASH_KEY2_MIX + (dwKey1 & 0xFF)] + * dwDecrypted0 = DataBlock[0] ^ (dwKey1 + dwKey2); + * + * This means: + * + * (dwKey1 + dwKey2) = DataBlock[0] ^ dwDecrypted0; + * + */ + +DWORD DetectFileKeyBySectorSize(LPDWORD EncryptedData, DWORD dwSectorSize, DWORD dwDecrypted0) +{ + DWORD dwDecrypted1Max = dwSectorSize + dwDecrypted0; + DWORD dwKey1PlusKey2; + DWORD DataBlock[2]; + + // We must have at least 2 DWORDs there to be able to decrypt something + if(dwSectorSize < 0x08) + return 0; + + // Get the value of the combined encryption key + dwKey1PlusKey2 = (EncryptedData[0] ^ dwDecrypted0) - 0xEEEEEEEE; + + // Try all 256 combinations of dwKey1 + for(DWORD i = 0; i < 0x100; i++) + { + DWORD dwSaveKey1; + DWORD dwKey1 = dwKey1PlusKey2 - StormBuffer[MPQ_HASH_KEY2_MIX + i]; + DWORD dwKey2 = 0xEEEEEEEE; + + // Modify the second key and decrypt the first DWORD + dwKey2 += StormBuffer[MPQ_HASH_KEY2_MIX + (dwKey1 & 0xFF)]; + DataBlock[0] = EncryptedData[0] ^ (dwKey1 + dwKey2); + + // Did we obtain the same value like dwDecrypted0? + if(DataBlock[0] == dwDecrypted0) + { + // Save this key value. Increment by one because + // we are decrypting sector offset table + dwSaveKey1 = dwKey1 + 1; + + // Rotate both keys + dwKey1 = ((~dwKey1 << 0x15) + 0x11111111) | (dwKey1 >> 0x0B); + dwKey2 = DataBlock[0] + dwKey2 + (dwKey2 << 5) + 3; + + // Modify the second key again and decrypt the second DWORD + dwKey2 += StormBuffer[MPQ_HASH_KEY2_MIX + (dwKey1 & 0xFF)]; + DataBlock[1] = EncryptedData[1] ^ (dwKey1 + dwKey2); + + // Now compare the results + if(DataBlock[1] <= dwDecrypted1Max) + return dwSaveKey1; + } + } + + // Key not found + return 0; +} + +// Function tries to detect file encryption key based on expected file content +// It is the same function like before, except that we know the value of the second DWORD +DWORD DetectFileKeyByKnownContent(void * pvEncryptedData, DWORD dwDecrypted0, DWORD dwDecrypted1) +{ + LPDWORD EncryptedData = (LPDWORD)pvEncryptedData; + DWORD dwKey1PlusKey2; + DWORD DataBlock[2]; + + // Get the value of the combined encryption key + dwKey1PlusKey2 = (EncryptedData[0] ^ dwDecrypted0) - 0xEEEEEEEE; + + // Try all 256 combinations of dwKey1 + for(DWORD i = 0; i < 0x100; i++) + { + DWORD dwSaveKey1; + DWORD dwKey1 = dwKey1PlusKey2 - StormBuffer[MPQ_HASH_KEY2_MIX + i]; + DWORD dwKey2 = 0xEEEEEEEE; + + // Modify the second key and decrypt the first DWORD + dwKey2 += StormBuffer[MPQ_HASH_KEY2_MIX + (dwKey1 & 0xFF)]; + DataBlock[0] = EncryptedData[0] ^ (dwKey1 + dwKey2); + + // Did we obtain the same value like dwDecrypted0? + if(DataBlock[0] == dwDecrypted0) + { + // Save this key value + dwSaveKey1 = dwKey1; + + // Rotate both keys + dwKey1 = ((~dwKey1 << 0x15) + 0x11111111) | (dwKey1 >> 0x0B); + dwKey2 = DataBlock[0] + dwKey2 + (dwKey2 << 5) + 3; + + // Modify the second key again and decrypt the second DWORD + dwKey2 += StormBuffer[MPQ_HASH_KEY2_MIX + (dwKey1 & 0xFF)]; + DataBlock[1] = EncryptedData[1] ^ (dwKey1 + dwKey2); + + // Now compare the results + if(DataBlock[1] == dwDecrypted1) + return dwSaveKey1; + } + } + + // Key not found + return 0; +} + +DWORD DetectFileKeyByContent(void * pvEncryptedData, DWORD dwSectorSize, DWORD dwFileSize) +{ + DWORD dwFileKey; + + // Try to break the file encryption key as if it was a WAVE file + if(dwSectorSize >= 0x0C) + { + dwFileKey = DetectFileKeyByKnownContent(pvEncryptedData, 0x46464952, dwFileSize - 8); + if(dwFileKey != 0) + return dwFileKey; + } + + // Try to break the encryption key as if it was an EXE file + if(dwSectorSize > 0x40) + { + dwFileKey = DetectFileKeyByKnownContent(pvEncryptedData, 0x00905A4D, 0x00000003); + if(dwFileKey != 0) + return dwFileKey; + } + + // Try to break the encryption key as if it was a XML file + if(dwSectorSize > 0x04) + { + dwFileKey = DetectFileKeyByKnownContent(pvEncryptedData, 0x6D783F3C, 0x6576206C); + if(dwFileKey != 0) + return dwFileKey; + } + + // Not detected, sorry + return 0; +} + +DWORD DecryptFileKey( + const char * szFileName, + ULONGLONG MpqPos, + DWORD dwFileSize, + DWORD dwFlags) +{ + DWORD dwFileKey; + DWORD dwMpqPos = (DWORD)MpqPos; + + // File key is calculated from plain name + szFileName = GetPlainFileName(szFileName); + dwFileKey = HashString(szFileName, MPQ_HASH_FILE_KEY); + + // Fix the key, if needed + if(dwFlags & MPQ_FILE_FIX_KEY) + dwFileKey = (dwFileKey + dwMpqPos) ^ dwFileSize; + + // Return the key + return dwFileKey; +} + +//----------------------------------------------------------------------------- +// Handle validation functions + +TMPQArchive * IsValidMpqHandle(HANDLE hMpq) +{ + TMPQArchive * ha = (TMPQArchive *)hMpq; + + return (ha != NULL && ha->pHeader != NULL && ha->pHeader->dwID == ID_MPQ) ? ha : NULL; +} + +TMPQFile * IsValidFileHandle(HANDLE hFile) +{ + TMPQFile * hf = (TMPQFile *)hFile; + + // Must not be NULL + if(hf != NULL && hf->dwMagic == ID_MPQ_FILE) + { + // Local file handle? + if(hf->pStream != NULL) + return hf; + + // Also verify the MPQ handle within the file handle + if(IsValidMpqHandle(hf->ha)) + return hf; + } + + return NULL; +} + +//----------------------------------------------------------------------------- +// Hash table and block table manipulation + +// Attempts to search a free hash entry, or an entry whose names and locale matches +TMPQHash * FindFreeHashEntry(TMPQArchive * ha, DWORD dwStartIndex, DWORD dwName1, DWORD dwName2, LCID lcLocale) +{ + TMPQHash * pDeletedEntry = NULL; // If a deleted entry was found in the continuous hash range + TMPQHash * pFreeEntry = NULL; // If a free entry was found in the continuous hash range + DWORD dwHashIndexMask = HASH_INDEX_MASK(ha); + DWORD dwIndex; + + // Set the initial index + dwStartIndex = dwIndex = (dwStartIndex & dwHashIndexMask); + + // Search the hash table and return the found entries in the following priority: + // 1) + // 2) + // 3) + // 4) NULL + for(;;) + { + TMPQHash * pHash = ha->pHashTable + dwIndex; + + // If we found a matching entry, return that one + if(pHash->dwName1 == dwName1 && pHash->dwName2 == dwName2 && pHash->lcLocale == lcLocale) + return pHash; + + // If we found a deleted entry, remember it but keep searching + if(pHash->dwBlockIndex == HASH_ENTRY_DELETED && pDeletedEntry == NULL) + pDeletedEntry = pHash; + + // If we found a free entry, we need to stop searching + if(pHash->dwBlockIndex == HASH_ENTRY_FREE) + { + pFreeEntry = pHash; + break; + } + + // Move to the next hash entry. + // If we reached the starting entry, it's failure. + dwIndex = (dwIndex + 1) & dwHashIndexMask; + if(dwIndex == dwStartIndex) + break; + } + + // If we found a deleted entry, return that one preferentially + return (pDeletedEntry != NULL) ? pDeletedEntry : pFreeEntry; +} + +// Retrieves the first hash entry for the given file. +// Every locale version of a file has its own hash entry +TMPQHash * GetFirstHashEntry(TMPQArchive * ha, const char * szFileName) +{ + DWORD dwHashIndexMask = HASH_INDEX_MASK(ha); + DWORD dwStartIndex = ha->pfnHashString(szFileName, MPQ_HASH_TABLE_INDEX); + DWORD dwName1 = ha->pfnHashString(szFileName, MPQ_HASH_NAME_A); + DWORD dwName2 = ha->pfnHashString(szFileName, MPQ_HASH_NAME_B); + DWORD dwIndex; + + // Set the initial index + dwStartIndex = dwIndex = (dwStartIndex & dwHashIndexMask); + + // Search the hash table + for(;;) + { + TMPQHash * pHash = ha->pHashTable + dwIndex; + + // If the entry matches, we found it. + if(pHash->dwName1 == dwName1 && pHash->dwName2 == dwName2 && pHash->dwBlockIndex < ha->dwFileTableSize) + return pHash; + + // If that hash entry is a free entry, it means we haven't found the file + if(pHash->dwBlockIndex == HASH_ENTRY_FREE) + return NULL; + + // Move to the next hash entry. Stop searching + // if we got reached the original hash entry + dwIndex = (dwIndex + 1) & dwHashIndexMask; + if(dwIndex == dwStartIndex) + return NULL; + } +} + +TMPQHash * GetNextHashEntry(TMPQArchive * ha, TMPQHash * pFirstHash, TMPQHash * pHash) +{ + DWORD dwHashIndexMask = HASH_INDEX_MASK(ha); + DWORD dwStartIndex = (DWORD)(pFirstHash - ha->pHashTable); + DWORD dwName1 = pHash->dwName1; + DWORD dwName2 = pHash->dwName2; + DWORD dwIndex = (DWORD)(pHash - ha->pHashTable); + + // Now go for any next entry that follows the pHash, + // until either free hash entry was found, or the start entry was reached + for(;;) + { + // Move to the next hash entry. Stop searching + // if we got reached the original hash entry + dwIndex = (dwIndex + 1) & dwHashIndexMask; + if(dwIndex == dwStartIndex) + return NULL; + pHash = ha->pHashTable + dwIndex; + + // If the entry matches, we found it. + if(pHash->dwName1 == dwName1 && pHash->dwName2 == dwName2 && pHash->dwBlockIndex < ha->dwFileTableSize) + return pHash; + + // If that hash entry is a free entry, it means we haven't found the file + if(pHash->dwBlockIndex == HASH_ENTRY_FREE) + return NULL; + } +} + +// Allocates an entry in the hash table +TMPQHash * AllocateHashEntry( + TMPQArchive * ha, + TFileEntry * pFileEntry, + LCID lcLocale) +{ + TMPQHash * pHash; + DWORD dwStartIndex = ha->pfnHashString(pFileEntry->szFileName, MPQ_HASH_TABLE_INDEX); + DWORD dwName1 = ha->pfnHashString(pFileEntry->szFileName, MPQ_HASH_NAME_A); + DWORD dwName2 = ha->pfnHashString(pFileEntry->szFileName, MPQ_HASH_NAME_B); + + // Attempt to find a free hash entry + pHash = FindFreeHashEntry(ha, dwStartIndex, dwName1, dwName2, lcLocale); + if(pHash != NULL) + { + // Fill the free hash entry + pHash->dwName1 = dwName1; + pHash->dwName2 = dwName2; + pHash->lcLocale = (USHORT)lcLocale; + pHash->wPlatform = 0; + pHash->dwBlockIndex = (DWORD)(pFileEntry - ha->pFileTable); + } + + return pHash; +} + +// Finds a free space in the MPQ where to store next data +// The free space begins beyond the file that is stored at the fuhrtest +// position in the MPQ. (listfile), (attributes) and (signature) are ignored, +// unless the MPQ is being flushed. +ULONGLONG FindFreeMpqSpace(TMPQArchive * ha) +{ + TMPQHeader * pHeader = ha->pHeader; + TFileEntry * pFileTableEnd = ha->pFileTable + ha->dwFileTableSize; + TFileEntry * pFileEntry; + ULONGLONG FreeSpacePos = ha->pHeader->dwHeaderSize; + DWORD dwChunkCount; + + // Parse the entire block table + for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++) + { + // Only take existing files with nonzero size + if((pFileEntry->dwFlags & MPQ_FILE_EXISTS) && (pFileEntry->dwCmpSize != 0)) + { + // If we are not saving MPQ tables, ignore internal MPQ files + if((ha->dwFlags & MPQ_FLAG_SAVING_TABLES) == 0 && IsInternalMpqFileName(pFileEntry->szFileName)) + continue; + + // If the end of the file is bigger than current MPQ table pos, update it + if((pFileEntry->ByteOffset + pFileEntry->dwCmpSize) > FreeSpacePos) + { + // Get the end of the file data + FreeSpacePos = pFileEntry->ByteOffset + pFileEntry->dwCmpSize; + + // Add the MD5 chunks, if present + if(pHeader->dwRawChunkSize != 0 && pFileEntry->dwCmpSize != 0) + { + dwChunkCount = ((pFileEntry->dwCmpSize - 1) / pHeader->dwRawChunkSize) + 1; + FreeSpacePos += dwChunkCount * MD5_DIGEST_SIZE; + } + } + } + } + + // Give the free space position to the caller + return FreeSpacePos; +} + +//----------------------------------------------------------------------------- +// Common functions - MPQ File + +TMPQFile * CreateFileHandle(TMPQArchive * ha, TFileEntry * pFileEntry) +{ + TMPQFile * hf; + + // Allocate space for TMPQFile + hf = STORM_ALLOC(TMPQFile, 1); + if(hf != NULL) + { + // Fill the file structure + memset(hf, 0, sizeof(TMPQFile)); + hf->dwMagic = ID_MPQ_FILE; + hf->pStream = NULL; + hf->ha = ha; + + // If the called entered a file entry, we also copy informations from the file entry + if(ha != NULL && pFileEntry != NULL) + { + // Set the raw position and MPQ position + hf->RawFilePos = FileOffsetFromMpqOffset(ha, pFileEntry->ByteOffset); + hf->MpqFilePos = pFileEntry->ByteOffset; + + // Set the data size + hf->dwDataSize = pFileEntry->dwFileSize; + hf->pFileEntry = pFileEntry; + } + } + + return hf; +} + +TMPQFile * CreateWritableHandle(TMPQArchive * ha, DWORD dwFileSize) +{ + ULONGLONG FreeMpqSpace; + ULONGLONG TempPos; + TMPQFile * hf; + + // We need to find the position in the MPQ where we save the file data + FreeMpqSpace = FindFreeMpqSpace(ha); + + // When format V1, the size of the archive cannot exceed 4 GB + if(ha->pHeader->wFormatVersion == MPQ_FORMAT_VERSION_1) + { + TempPos = FreeMpqSpace + + dwFileSize + + (ha->pHeader->dwHashTableSize * sizeof(TMPQHash)) + + (ha->dwFileTableSize * sizeof(TMPQBlock)); + if((TempPos >> 32) != 0) + { + SetLastError(ERROR_DISK_FULL); + return NULL; + } + } + + // Allocate the file handle + hf = CreateFileHandle(ha, NULL); + if(hf == NULL) + { + SetLastError(ERROR_NOT_ENOUGH_MEMORY); + return NULL; + } + + // We need to find the position in the MPQ where we save the file data + hf->MpqFilePos = FreeMpqSpace; + hf->bIsWriteHandle = true; + return hf; +} + +// Loads a table from MPQ. +// Can be used for hash table, block table, sector offset table or sector checksum table +void * LoadMpqTable( + TMPQArchive * ha, + ULONGLONG ByteOffset, + DWORD dwCompressedSize, + DWORD dwTableSize, + DWORD dwKey, + bool * pbTableIsCut) +{ + ULONGLONG FileSize = 0; + LPBYTE pbCompressed = NULL; + LPBYTE pbMpqTable; + LPBYTE pbToRead; + DWORD dwBytesToRead = dwCompressedSize; + int nError = ERROR_SUCCESS; + + // Allocate the MPQ table + pbMpqTable = pbToRead = STORM_ALLOC(BYTE, dwTableSize); + if(pbMpqTable != NULL) + { + // Check if the MPQ table is encrypted + if(dwCompressedSize < dwTableSize) + { + // Allocate temporary buffer for holding compressed data + pbCompressed = pbToRead = STORM_ALLOC(BYTE, dwCompressedSize); + if(pbCompressed == NULL) + { + STORM_FREE(pbMpqTable); + return NULL; + } + } + + // Get the file offset from which we will read the table + // Note: According to Storm.dll from Warcraft III (version 2002), + // if the hash table position is 0xFFFFFFFF, no SetFilePointer call is done + // and the table is loaded from the current file offset + if(ByteOffset == SFILE_INVALID_POS) + FileStream_GetPos(ha->pStream, &ByteOffset); + + // On archives v 1.0, hash table and block table can go beyond EOF. + // Storm.dll reads as much as possible, then fills the missing part with zeros. + // Abused by Spazzler map protector which sets hash table size to 0x00100000 + if(ha->pHeader->wFormatVersion == MPQ_FORMAT_VERSION_1) + { + // Cut the table size + FileStream_GetSize(ha->pStream, &FileSize); + if((ByteOffset + dwBytesToRead) > FileSize) + { + // Fill the extra data with zeros + dwBytesToRead = (DWORD)(FileSize - ByteOffset); + memset(pbMpqTable + dwBytesToRead, 0, (dwTableSize - dwBytesToRead)); + + // Give the caller information that the table was cut + if(pbTableIsCut != NULL) + pbTableIsCut[0] = true; + } + } + + // If everything succeeded, read the raw table form the MPQ + if(FileStream_Read(ha->pStream, &ByteOffset, pbToRead, dwBytesToRead)) + { + // First of all, decrypt the table + if(dwKey != 0) + { + BSWAP_ARRAY32_UNSIGNED(pbToRead, dwCompressedSize); + DecryptMpqBlock(pbToRead, dwCompressedSize, dwKey); + BSWAP_ARRAY32_UNSIGNED(pbToRead, dwCompressedSize); + } + + // If the table is compressed, decompress it + if(dwCompressedSize < dwTableSize) + { + int cbOutBuffer = (int)dwTableSize; + int cbInBuffer = (int)dwCompressedSize; + + if(!SCompDecompress2(pbMpqTable, &cbOutBuffer, pbCompressed, cbInBuffer)) + nError = GetLastError(); + } + + // Make sure that the table is properly byte-swapped + BSWAP_ARRAY32_UNSIGNED(pbMpqTable, dwTableSize); + } + else + { + nError = GetLastError(); + } + + // If read failed, free the table and return + if(nError != ERROR_SUCCESS) + { + STORM_FREE(pbMpqTable); + pbMpqTable = NULL; + } + + // Free the compression buffer, if any + if(pbCompressed != NULL) + STORM_FREE(pbCompressed); + } + + // Return the MPQ table + return pbMpqTable; +} + +unsigned char * AllocateMd5Buffer( + DWORD dwRawDataSize, + DWORD dwChunkSize, + LPDWORD pcbMd5Size) +{ + unsigned char * md5_array; + DWORD cbMd5Size; + + // Sanity check + assert(dwRawDataSize != 0); + assert(dwChunkSize != 0); + + // Calculate how many MD5's we will calculate + cbMd5Size = (((dwRawDataSize - 1) / dwChunkSize) + 1) * MD5_DIGEST_SIZE; + + // Allocate space for array or MD5s + md5_array = STORM_ALLOC(BYTE, cbMd5Size); + + // Give the size of the MD5 array + if(pcbMd5Size != NULL) + *pcbMd5Size = cbMd5Size; + return md5_array; +} + +// Allocates sector buffer and sector offset table +int AllocateSectorBuffer(TMPQFile * hf) +{ + TMPQArchive * ha = hf->ha; + + // Caller of AllocateSectorBuffer must ensure these + assert(hf->pbFileSector == NULL); + assert(hf->pFileEntry != NULL); + assert(hf->ha != NULL); + + // Don't allocate anything if the file has zero size + if(hf->pFileEntry->dwFileSize == 0 || hf->dwDataSize == 0) + return ERROR_SUCCESS; + + // Determine the file sector size and allocate buffer for it + hf->dwSectorSize = (hf->pFileEntry->dwFlags & MPQ_FILE_SINGLE_UNIT) ? hf->dwDataSize : ha->dwSectorSize; + hf->pbFileSector = STORM_ALLOC(BYTE, hf->dwSectorSize); + hf->dwSectorOffs = SFILE_INVALID_POS; + + // Return result + return (hf->pbFileSector != NULL) ? (int)ERROR_SUCCESS : (int)ERROR_NOT_ENOUGH_MEMORY; +} + +// Allocates sector offset table +int AllocatePatchInfo(TMPQFile * hf, bool bLoadFromFile) +{ + TMPQArchive * ha = hf->ha; + DWORD dwLength = sizeof(TPatchInfo); + + // The following conditions must be true + assert(hf->pFileEntry->dwFlags & MPQ_FILE_PATCH_FILE); + assert(hf->pPatchInfo == NULL); + +__AllocateAndLoadPatchInfo: + + // Allocate space for patch header. Start with default size, + // and if its size if bigger, then we reload them + hf->pPatchInfo = STORM_ALLOC(TPatchInfo, 1); + if(hf->pPatchInfo == NULL) + return ERROR_NOT_ENOUGH_MEMORY; + + // Do we have to load the patch header from the file ? + if(bLoadFromFile) + { + // Load the patch header + if(!FileStream_Read(ha->pStream, &hf->RawFilePos, hf->pPatchInfo, dwLength)) + { + // Free the patch info + STORM_FREE(hf->pPatchInfo); + hf->pPatchInfo = NULL; + return GetLastError(); + } + + // Perform necessary swapping + hf->pPatchInfo->dwLength = BSWAP_INT32_UNSIGNED(hf->pPatchInfo->dwLength); + hf->pPatchInfo->dwFlags = BSWAP_INT32_UNSIGNED(hf->pPatchInfo->dwFlags); + hf->pPatchInfo->dwDataSize = BSWAP_INT32_UNSIGNED(hf->pPatchInfo->dwDataSize); + + // Verify the size of the patch header + // If it's not default size, we have to reload them + if(hf->pPatchInfo->dwLength > dwLength) + { + // Free the patch info + dwLength = hf->pPatchInfo->dwLength; + STORM_FREE(hf->pPatchInfo); + hf->pPatchInfo = NULL; + + // If the length is out of all possible ranges, fail the operation + if(dwLength > 0x400) + return ERROR_FILE_CORRUPT; + goto __AllocateAndLoadPatchInfo; + } + + // Patch file data size according to the patch header + hf->dwDataSize = hf->pPatchInfo->dwDataSize; + } + else + { + memset(hf->pPatchInfo, 0, dwLength); + } + + // Save the final length to the patch header + hf->pPatchInfo->dwLength = dwLength; + hf->pPatchInfo->dwFlags = 0x80000000; + return ERROR_SUCCESS; +} + +// Allocates sector offset table +int AllocateSectorOffsets(TMPQFile * hf, bool bLoadFromFile) +{ + TMPQArchive * ha = hf->ha; + TFileEntry * pFileEntry = hf->pFileEntry; + DWORD dwSectorOffsLen; + bool bSectorOffsetTableCorrupt = false; + + // Caller of AllocateSectorOffsets must ensure these + assert(hf->SectorOffsets == NULL); + assert(hf->pFileEntry != NULL); + assert(hf->dwDataSize != 0); + assert(hf->ha != NULL); + + // If the file is stored as single unit, just set number of sectors to 1 + if(pFileEntry->dwFlags & MPQ_FILE_SINGLE_UNIT) + { + hf->dwSectorCount = 1; + return ERROR_SUCCESS; + } + + // Calculate the number of data sectors + // Note that this doesn't work if the file size is zero + hf->dwSectorCount = ((hf->dwDataSize - 1) / hf->dwSectorSize) + 1; + + // Calculate the number of file sectors + dwSectorOffsLen = (hf->dwSectorCount + 1) * sizeof(DWORD); + + // If MPQ_FILE_SECTOR_CRC flag is set, there will either be extra DWORD + // or an array of MD5's. Either way, we read at least 4 bytes more + // in order to save additional read from the file. + if(pFileEntry->dwFlags & MPQ_FILE_SECTOR_CRC) + dwSectorOffsLen += sizeof(DWORD); + + // Only allocate and load the table if the file is compressed + if(pFileEntry->dwFlags & MPQ_FILE_COMPRESS_MASK) + { + __LoadSectorOffsets: + + // Allocate the sector offset table + hf->SectorOffsets = STORM_ALLOC(DWORD, (dwSectorOffsLen / sizeof(DWORD))); + if(hf->SectorOffsets == NULL) + return ERROR_NOT_ENOUGH_MEMORY; + + // Only read from the file if we are supposed to do so + if(bLoadFromFile) + { + ULONGLONG RawFilePos = hf->RawFilePos; + + // Append the length of the patch info, if any + if(hf->pPatchInfo != NULL) + { + if((RawFilePos + hf->pPatchInfo->dwLength) < RawFilePos) + return ERROR_FILE_CORRUPT; + RawFilePos += hf->pPatchInfo->dwLength; + } + + // Load the sector offsets from the file + if(!FileStream_Read(ha->pStream, &RawFilePos, hf->SectorOffsets, dwSectorOffsLen)) + { + // Free the sector offsets + STORM_FREE(hf->SectorOffsets); + hf->SectorOffsets = NULL; + return GetLastError(); + } + + // Swap the sector positions + BSWAP_ARRAY32_UNSIGNED(hf->SectorOffsets, dwSectorOffsLen); + + // Decrypt loaded sector positions if necessary + if(pFileEntry->dwFlags & MPQ_FILE_ENCRYPTED) + { + // If we don't know the file key, try to find it. + if(hf->dwFileKey == 0) + { + hf->dwFileKey = DetectFileKeyBySectorSize(hf->SectorOffsets, ha->dwSectorSize, dwSectorOffsLen); + if(hf->dwFileKey == 0) + { + STORM_FREE(hf->SectorOffsets); + hf->SectorOffsets = NULL; + return ERROR_UNKNOWN_FILE_KEY; + } + } + + // Decrypt sector positions + DecryptMpqBlock(hf->SectorOffsets, dwSectorOffsLen, hf->dwFileKey - 1); + } + + // + // Validate the sector offset table + // + // Note: Some MPQ protectors put the actual file data before the sector offset table. + // In this case, the sector offsets are negative (> 0x80000000). + // + + for(DWORD i = 0; i < hf->dwSectorCount; i++) + { + DWORD dwSectorOffset1 = hf->SectorOffsets[i+1]; + DWORD dwSectorOffset0 = hf->SectorOffsets[i]; + + // Every following sector offset must be bigger than the previous one + if(dwSectorOffset1 <= dwSectorOffset0) + { + bSectorOffsetTableCorrupt = true; + break; + } + + // The sector size must not be bigger than compressed file size + // Edit: Yes, but apparently, in original Storm.dll, the compressed + // size is not checked anywhere. However, we need to do this check + // in order to sector offset table malformed by MPQ protectors + if((dwSectorOffset1 - dwSectorOffset0) > ha->dwSectorSize) + { + bSectorOffsetTableCorrupt = true; + break; + } + } + + // If data corruption detected, free the sector offset table + if(bSectorOffsetTableCorrupt) + { + STORM_FREE(hf->SectorOffsets); + hf->SectorOffsets = NULL; + return ERROR_FILE_CORRUPT; + } + + // + // There may be various extra DWORDs loaded after the sector offset table. + // They are mostly empty on WoW release MPQs, but on MPQs from PTR, + // they contain random non-zero data. Their meaning is unknown. + // + // These extra values are, however, include in the dwCmpSize in the file + // table. We cannot ignore them, because compacting archive would fail + // + + if(hf->SectorOffsets[0] > dwSectorOffsLen) + { + // MPQ protectors put some ridiculous values there. We must limit the extra bytes + if(hf->SectorOffsets[0] > (dwSectorOffsLen + 0x400)) + return ERROR_FILE_CORRUPT; + + // Free the old sector offset table + dwSectorOffsLen = hf->SectorOffsets[0]; + STORM_FREE(hf->SectorOffsets); + goto __LoadSectorOffsets; + } + } + else + { + memset(hf->SectorOffsets, 0, dwSectorOffsLen); + hf->SectorOffsets[0] = dwSectorOffsLen; + } + } + + return ERROR_SUCCESS; +} + +int AllocateSectorChecksums(TMPQFile * hf, bool bLoadFromFile) +{ + TMPQArchive * ha = hf->ha; + TFileEntry * pFileEntry = hf->pFileEntry; + ULONGLONG RawFilePos; + DWORD dwCompressedSize = 0; + DWORD dwExpectedSize; + DWORD dwCrcOffset; // Offset of the CRC table, relative to file offset in the MPQ + DWORD dwCrcSize; + + // Caller of AllocateSectorChecksums must ensure these + assert(hf->SectorChksums == NULL); + assert(hf->SectorOffsets != NULL); + assert(hf->pFileEntry != NULL); + assert(hf->ha != NULL); + + // Single unit files don't have sector checksums + if(pFileEntry->dwFlags & MPQ_FILE_SINGLE_UNIT) + return ERROR_SUCCESS; + + // Caller must ensure that we are only called when we have sector checksums + assert(pFileEntry->dwFlags & MPQ_FILE_SECTOR_CRC); + + // + // Older MPQs store an array of CRC32's after + // the raw file data in the MPQ. + // + // In newer MPQs, the (since Cataclysm BETA) the (attributes) file + // contains additional 32-bit values beyond the sector table. + // Their number depends on size of the (attributes), but their + // meaning is unknown. They are usually zeroed in retail game files, + // but contain some sort of checksum in BETA MPQs + // + + // Does the size of the file table match with the CRC32-based checksums? + dwExpectedSize = (hf->dwSectorCount + 2) * sizeof(DWORD); + if(hf->SectorOffsets[0] != 0 && hf->SectorOffsets[0] == dwExpectedSize) + { + // If we are not loading from the MPQ file, we just allocate the sector table + // In that case, do not check any sizes + if(bLoadFromFile == false) + { + hf->SectorChksums = STORM_ALLOC(DWORD, hf->dwSectorCount); + if(hf->SectorChksums == NULL) + return ERROR_NOT_ENOUGH_MEMORY; + + // Fill the checksum table with zeros + memset(hf->SectorChksums, 0, hf->dwSectorCount * sizeof(DWORD)); + return ERROR_SUCCESS; + } + else + { + // Is there valid size of the sector checksums? + if(hf->SectorOffsets[hf->dwSectorCount + 1] >= hf->SectorOffsets[hf->dwSectorCount]) + dwCompressedSize = hf->SectorOffsets[hf->dwSectorCount + 1] - hf->SectorOffsets[hf->dwSectorCount]; + + // Ignore cases when the length is too small or too big. + if(dwCompressedSize < sizeof(DWORD) || dwCompressedSize > hf->dwSectorSize) + return ERROR_SUCCESS; + + // Calculate offset of the CRC table + dwCrcSize = hf->dwSectorCount * sizeof(DWORD); + dwCrcOffset = hf->SectorOffsets[hf->dwSectorCount]; + RawFilePos = CalculateRawSectorOffset(hf, dwCrcOffset); + + // Now read the table from the MPQ + hf->SectorChksums = (DWORD *)LoadMpqTable(ha, RawFilePos, dwCompressedSize, dwCrcSize, 0, NULL); + if(hf->SectorChksums == NULL) + return ERROR_NOT_ENOUGH_MEMORY; + } + } + + // If the size doesn't match, we ignore sector checksums +// assert(false); + return ERROR_SUCCESS; +} + +int WritePatchInfo(TMPQFile * hf) +{ + TMPQArchive * ha = hf->ha; + TPatchInfo * pPatchInfo = hf->pPatchInfo; + + // The caller must make sure that this function is only called + // when the following is true. + assert(hf->pFileEntry->dwFlags & MPQ_FILE_PATCH_FILE); + assert(pPatchInfo != NULL); + + BSWAP_ARRAY32_UNSIGNED(pPatchInfo, 3 * sizeof(DWORD)); + if(!FileStream_Write(ha->pStream, &hf->RawFilePos, pPatchInfo, sizeof(TPatchInfo))) + return GetLastError(); + + return ERROR_SUCCESS; +} + +int WriteSectorOffsets(TMPQFile * hf) +{ + TMPQArchive * ha = hf->ha; + TFileEntry * pFileEntry = hf->pFileEntry; + ULONGLONG RawFilePos = hf->RawFilePos; + DWORD dwSectorOffsLen; + + // The caller must make sure that this function is only called + // when the following is true. + assert(hf->pFileEntry->dwFlags & MPQ_FILE_COMPRESS_MASK); + assert(hf->SectorOffsets != NULL); + dwSectorOffsLen = hf->SectorOffsets[0]; + + // If file is encrypted, sector positions are also encrypted + if(pFileEntry->dwFlags & MPQ_FILE_ENCRYPTED) + EncryptMpqBlock(hf->SectorOffsets, dwSectorOffsLen, hf->dwFileKey - 1); + BSWAP_ARRAY32_UNSIGNED(hf->SectorOffsets, dwSectorOffsLen); + + // Adjust sector offset table position, if we also have patch info + if(hf->pPatchInfo != NULL) + RawFilePos += hf->pPatchInfo->dwLength; + + // Write sector offsets to the archive + if(!FileStream_Write(ha->pStream, &RawFilePos, hf->SectorOffsets, dwSectorOffsLen)) + return GetLastError(); + + // Not necessary, as the sector checksums + // are going to be freed when this is done. +// BSWAP_ARRAY32_UNSIGNED(hf->SectorOffsets, dwSectorOffsLen); + return ERROR_SUCCESS; +} + + +int WriteSectorChecksums(TMPQFile * hf) +{ + TMPQArchive * ha = hf->ha; + ULONGLONG RawFilePos; + TFileEntry * pFileEntry = hf->pFileEntry; + LPBYTE pbCompressed; + DWORD dwCompressedSize = 0; + DWORD dwCrcSize; + int nOutSize; + int nError = ERROR_SUCCESS; + + // The caller must make sure that this function is only called + // when the following is true. + assert(hf->pFileEntry->dwFlags & MPQ_FILE_SECTOR_CRC); + assert(hf->SectorOffsets != NULL); + assert(hf->SectorChksums != NULL); + + // If the MPQ has MD5 of each raw data chunk, + // we leave sector offsets empty + if(ha->pHeader->dwRawChunkSize != 0) + { + hf->SectorOffsets[hf->dwSectorCount + 1] = hf->SectorOffsets[hf->dwSectorCount]; + return ERROR_SUCCESS; + } + + // Calculate size of the checksum array + dwCrcSize = hf->dwSectorCount * sizeof(DWORD); + + // Allocate buffer for compressed sector CRCs. + pbCompressed = STORM_ALLOC(BYTE, dwCrcSize); + if(pbCompressed == NULL) + return ERROR_NOT_ENOUGH_MEMORY; + + // Perform the compression + BSWAP_ARRAY32_UNSIGNED(hf->SectorChksums, dwCrcSize); + + nOutSize = (int)dwCrcSize; + SCompCompress(pbCompressed, &nOutSize, hf->SectorChksums, (int)dwCrcSize, MPQ_COMPRESSION_ZLIB, 0, 0); + dwCompressedSize = (DWORD)nOutSize; + + // Write the sector CRCs to the archive + RawFilePos = hf->RawFilePos + hf->SectorOffsets[hf->dwSectorCount]; + if(hf->pPatchInfo != NULL) + RawFilePos += hf->pPatchInfo->dwLength; + if(!FileStream_Write(ha->pStream, &RawFilePos, pbCompressed, dwCompressedSize)) + nError = GetLastError(); + + // Not necessary, as the sector checksums + // are going to be freed when this is done. +// BSWAP_ARRAY32_UNSIGNED(hf->SectorChksums, dwCrcSize); + + // Store the sector CRCs + hf->SectorOffsets[hf->dwSectorCount + 1] = hf->SectorOffsets[hf->dwSectorCount] + dwCompressedSize; + pFileEntry->dwCmpSize += dwCompressedSize; + STORM_FREE(pbCompressed); + return nError; +} + +int WriteMemDataMD5( + TFileStream * pStream, + ULONGLONG RawDataOffs, + void * pvRawData, + DWORD dwRawDataSize, + DWORD dwChunkSize, + LPDWORD pcbTotalSize) +{ + unsigned char * md5_array; + unsigned char * md5; + LPBYTE pbRawData = (LPBYTE)pvRawData; + DWORD dwBytesRemaining = dwRawDataSize; + DWORD dwMd5ArraySize = 0; + int nError = ERROR_SUCCESS; + + // Allocate buffer for array of MD5 + md5_array = md5 = AllocateMd5Buffer(dwRawDataSize, dwChunkSize, &dwMd5ArraySize); + if(md5_array == NULL) + return ERROR_NOT_ENOUGH_MEMORY; + + // For every file chunk, calculate MD5 + while(dwBytesRemaining != 0) + { + // Get the remaining number of bytes to read + dwChunkSize = STORMLIB_MIN(dwBytesRemaining, dwChunkSize); + + // Calculate MD5 + CalculateDataBlockHash(pbRawData, dwChunkSize, md5); + md5 += MD5_DIGEST_SIZE; + + // Move offset and size + dwBytesRemaining -= dwChunkSize; + pbRawData += dwChunkSize; + } + + // Write the array od MD5's to the file + RawDataOffs += dwRawDataSize; + if(!FileStream_Write(pStream, &RawDataOffs, md5_array, dwMd5ArraySize)) + nError = GetLastError(); + + // Give the caller the size of the MD5 array + if(pcbTotalSize != NULL) + *pcbTotalSize = dwRawDataSize + dwMd5ArraySize; + + // Free buffers and exit + STORM_FREE(md5_array); + return nError; +} + + +// Writes the MD5 for each chunk of the raw file data +int WriteMpqDataMD5( + TFileStream * pStream, + ULONGLONG RawDataOffs, + DWORD dwRawDataSize, + DWORD dwChunkSize) +{ + unsigned char * md5_array; + unsigned char * md5; + LPBYTE pbFileChunk; + DWORD dwMd5ArraySize = 0; + DWORD dwToRead = dwRawDataSize; + int nError = ERROR_SUCCESS; + + // Allocate buffer for array of MD5 + md5_array = md5 = AllocateMd5Buffer(dwRawDataSize, dwChunkSize, &dwMd5ArraySize); + if(md5_array == NULL) + return ERROR_NOT_ENOUGH_MEMORY; + + // Allocate space for file chunk + pbFileChunk = STORM_ALLOC(BYTE, dwChunkSize); + if(pbFileChunk == NULL) + { + STORM_FREE(md5_array); + return ERROR_NOT_ENOUGH_MEMORY; + } + + // For every file chunk, calculate MD5 + while(dwRawDataSize != 0) + { + // Get the remaining number of bytes to read + dwToRead = STORMLIB_MIN(dwRawDataSize, dwChunkSize); + + // Read the chunk + if(!FileStream_Read(pStream, &RawDataOffs, pbFileChunk, dwToRead)) + { + nError = GetLastError(); + break; + } + + // Calculate MD5 + CalculateDataBlockHash(pbFileChunk, dwToRead, md5); + md5 += MD5_DIGEST_SIZE; + + // Move offset and size + RawDataOffs += dwToRead; + dwRawDataSize -= dwToRead; + } + + // Write the array od MD5's to the file + if(nError == ERROR_SUCCESS) + { + if(!FileStream_Write(pStream, NULL, md5_array, dwMd5ArraySize)) + nError = GetLastError(); + } + + // Free buffers and exit + STORM_FREE(pbFileChunk); + STORM_FREE(md5_array); + return nError; +} + +// Frees the structure for MPQ file +void FreeFileHandle(TMPQFile *& hf) +{ + if(hf != NULL) + { + // If we have patch file attached to this one, free it first + if(hf->hfPatch != NULL) + FreeFileHandle(hf->hfPatch); + + // Then free all buffers allocated in the file structure + if(hf->pbFileData != NULL) + STORM_FREE(hf->pbFileData); + if(hf->pPatchInfo != NULL) + STORM_FREE(hf->pPatchInfo); + if(hf->SectorOffsets != NULL) + STORM_FREE(hf->SectorOffsets); + if(hf->SectorChksums != NULL) + STORM_FREE(hf->SectorChksums); + if(hf->pbFileSector != NULL) + STORM_FREE(hf->pbFileSector); + if(hf->pStream != NULL) + FileStream_Close(hf->pStream); + STORM_FREE(hf); + hf = NULL; + } +} + +// Frees the MPQ archive +void FreeArchiveHandle(TMPQArchive *& ha) +{ + if(ha != NULL) + { + // First of all, free the patch archive, if any + if(ha->haPatch != NULL) + FreeArchiveHandle(ha->haPatch); + + // Free the patch prefix, if any + if(ha->pPatchPrefix != NULL) + STORM_FREE(ha->pPatchPrefix); + + // Close the file stream + FileStream_Close(ha->pStream); + ha->pStream = NULL; + + // Free the file names from the file table + if(ha->pFileTable != NULL) + { + for(DWORD i = 0; i < ha->dwFileTableSize; i++) + { + if(ha->pFileTable[i].szFileName != NULL) + STORM_FREE(ha->pFileTable[i].szFileName); + ha->pFileTable[i].szFileName = NULL; + } + + // Then free all buffers allocated in the archive structure + STORM_FREE(ha->pFileTable); + } + + if(ha->pHashTable != NULL) + STORM_FREE(ha->pHashTable); + if(ha->pHetTable != NULL) + FreeHetTable(ha->pHetTable); + STORM_FREE(ha); + ha = NULL; + } +} + +bool IsInternalMpqFileName(const char * szFileName) +{ + if(szFileName != NULL && szFileName[0] == '(') + { + if(!_stricmp(szFileName, LISTFILE_NAME) || + !_stricmp(szFileName, ATTRIBUTES_NAME) || + !_stricmp(szFileName, SIGNATURE_NAME)) + { + return true; + } + } + + return false; +} + +// Verifies if the file name is a pseudo-name +bool IsPseudoFileName(const char * szFileName, DWORD * pdwFileIndex) +{ + DWORD dwFileIndex = 0; + + if(szFileName != NULL) + { + // Must be "File########.ext" + if(!_strnicmp(szFileName, "File", 4)) + { + // Check 8 digits + for(int i = 4; i < 4+8; i++) + { + if(szFileName[i] < '0' || szFileName[i] > '9') + return false; + dwFileIndex = (dwFileIndex * 10) + (szFileName[i] - '0'); + } + + // An extension must follow + if(szFileName[12] == '.') + { + if(pdwFileIndex != NULL) + *pdwFileIndex = dwFileIndex; + return true; + } + } + } + + // Not a pseudo-name + return false; +} + +//----------------------------------------------------------------------------- +// Functions calculating and verifying the MD5 signature + +bool IsValidMD5(LPBYTE pbMd5) +{ + LPDWORD Md5 = (LPDWORD)pbMd5; + + return (Md5[0] | Md5[1] | Md5[2] | Md5[3]) ? true : false; +} + +bool IsValidSignature(LPBYTE pbSignature) +{ + LPDWORD Signature = (LPDWORD)pbSignature; + DWORD SigValid = 0; + + for(int i = 0; i < MPQ_WEAK_SIGNATURE_SIZE / sizeof(DWORD); i++) + SigValid |= Signature[i]; + + return (SigValid != 0) ? true : false; +} + + +bool VerifyDataBlockHash(void * pvDataBlock, DWORD cbDataBlock, LPBYTE expected_md5) +{ + hash_state md5_state; + BYTE md5_digest[MD5_DIGEST_SIZE]; + + // Don't verify the block if the MD5 is not valid. + if(!IsValidMD5(expected_md5)) + return true; + + // Calculate the MD5 of the data block + md5_init(&md5_state); + md5_process(&md5_state, (unsigned char *)pvDataBlock, cbDataBlock); + md5_done(&md5_state, md5_digest); + + // Does the MD5's match? + return (memcmp(md5_digest, expected_md5, MD5_DIGEST_SIZE) == 0); +} + +void CalculateDataBlockHash(void * pvDataBlock, DWORD cbDataBlock, LPBYTE md5_hash) +{ + hash_state md5_state; + + md5_init(&md5_state); + md5_process(&md5_state, (unsigned char *)pvDataBlock, cbDataBlock); + md5_done(&md5_state, md5_hash); +} + + +//----------------------------------------------------------------------------- +// Swapping functions + +#ifndef PLATFORM_LITTLE_ENDIAN + +// +// Note that those functions are implemented for Mac operating system, +// as this is the only supported platform that uses big endian. +// + +// Swaps a signed 16-bit integer +int16_t SwapInt16(uint16_t data) +{ + return (int16_t)CFSwapInt16(data); +} + +// Swaps an unsigned 16-bit integer +uint16_t SwapUInt16(uint16_t data) +{ + return CFSwapInt16(data); +} + +// Swaps signed 32-bit integer +int32_t SwapInt32(uint32_t data) +{ + return (int32_t)CFSwapInt32(data); +} + +// Swaps an unsigned 32-bit integer +uint32_t SwapUInt32(uint32_t data) +{ + return CFSwapInt32(data); +} + +// Swaps signed 64-bit integer +int64_t SwapInt64(int64_t data) +{ + return (int64_t)CFSwapInt64(data); +} + +// Swaps an unsigned 64-bit integer +uint64_t SwapUInt64(uint64_t data) +{ + return CFSwapInt64(data); +} + +// Swaps array of unsigned 16-bit integers +void ConvertUInt16Buffer(void * ptr, size_t length) +{ + uint16_t * buffer = (uint16_t *)ptr; + uint32_t nElements = (uint32_t)(length / sizeof(uint16_t)); + + while(nElements-- > 0) + { + *buffer = SwapUInt16(*buffer); + buffer++; + } +} + +// Swaps array of unsigned 32-bit integers +void ConvertUInt32Buffer(void * ptr, size_t length) +{ + uint32_t * buffer = (uint32_t *)ptr; + uint32_t nElements = (uint32_t)(length / sizeof(uint32_t)); + + while(nElements-- > 0) + { + *buffer = SwapUInt32(*buffer); + buffer++; + } +} + +// Swaps array of unsigned 64-bit integers +void ConvertUInt64Buffer(void * ptr, size_t length) +{ + uint64_t * buffer = (uint64_t *)ptr; + uint32_t nElements = (uint32_t)(length / sizeof(uint64_t)); + + while(nElements-- > 0) + { + *buffer = SwapUInt64(*buffer); + buffer++; + } +} + +// Swaps the TMPQHeader structure +void ConvertTMPQHeader(void *header, uint16_t version) +{ + TMPQHeader * theHeader = (TMPQHeader *)header; + + // Swap header part version 1 + if(version == MPQ_FORMAT_VERSION_1) + { + theHeader->dwID = SwapUInt32(theHeader->dwID); + theHeader->dwHeaderSize = SwapUInt32(theHeader->dwHeaderSize); + theHeader->dwArchiveSize = SwapUInt32(theHeader->dwArchiveSize); + theHeader->wFormatVersion = SwapUInt16(theHeader->wFormatVersion); + theHeader->wSectorSize = SwapUInt16(theHeader->wSectorSize); + theHeader->dwHashTablePos = SwapUInt32(theHeader->dwHashTablePos); + theHeader->dwBlockTablePos = SwapUInt32(theHeader->dwBlockTablePos); + theHeader->dwHashTableSize = SwapUInt32(theHeader->dwHashTableSize); + theHeader->dwBlockTableSize = SwapUInt32(theHeader->dwBlockTableSize); + } + + if(version == MPQ_FORMAT_VERSION_2) + { + theHeader->HiBlockTablePos64 = SwapUInt64(theHeader->HiBlockTablePos64); + theHeader->wHashTablePosHi = SwapUInt16(theHeader->wHashTablePosHi); + theHeader->wBlockTablePosHi = SwapUInt16(theHeader->wBlockTablePosHi); + } + + if(version == MPQ_FORMAT_VERSION_3) + { + theHeader->ArchiveSize64 = SwapUInt64(theHeader->ArchiveSize64); + theHeader->BetTablePos64 = SwapUInt64(theHeader->BetTablePos64); + theHeader->HetTablePos64 = SwapUInt64(theHeader->HetTablePos64); + } + + if(version == MPQ_FORMAT_VERSION_4) + { + theHeader->HashTableSize64 = SwapUInt64(theHeader->HashTableSize64); + theHeader->BlockTableSize64 = SwapUInt64(theHeader->BlockTableSize64); + theHeader->HiBlockTableSize64 = SwapUInt64(theHeader->HiBlockTableSize64); + theHeader->HetTableSize64 = SwapUInt64(theHeader->HetTableSize64); + theHeader->BetTableSize64 = SwapUInt64(theHeader->BetTableSize64); + } +} + +#endif // PLATFORM_LITTLE_ENDIAN diff --git a/src/SFileAddFile.cpp b/src/SFileAddFile.cpp index b70eeb1..0b70d35 100644 --- a/src/SFileAddFile.cpp +++ b/src/SFileAddFile.cpp @@ -1,1313 +1,1313 @@ -/*****************************************************************************/ -/* 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->nAddFileError = ERROR_SUCCESS; - - return ERROR_SUCCESS; -} - -//----------------------------------------------------------------------------- -// MPQ write data functions - -static int 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 - int nCompressionLevel; // ADPCM compression level (only used for wave files) - int nError = ERROR_SUCCESS; - - // 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(nError == 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) - { - nError = 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)) - { - nError = 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 nError; -} - -//----------------------------------------------------------------------------- -// Recrypts file data for file renaming - -static int 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; - int nError = 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 - nError = AllocateSectorBuffer(hf); - if(nError != ERROR_SUCCESS) - return nError; - - // Also allocate buffer for sector offsets - // Note: Don't load sector checksums, we don't need to recrypt them - nError = AllocateSectorOffsets(hf, true); - if(nError != ERROR_SUCCESS) - return nError; - - // 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)) - nError = 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(nError == 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)) - { - nError = 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)) - { - nError = GetLastError(); - break; - } - - // Decrement number of bytes remaining - dwBytesToRecrypt -= hf->dwSectorSize; - } - } - - return nError; -} - -//----------------------------------------------------------------------------- -// Internal support for MPQ modifications - -int 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; - int nError = 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(nError == 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 - nError = ERROR_ALREADY_EXISTS; - } - else - { - // Free all internal files - (listfile), (attributes), (signature) - InvalidateInternalFiles(ha); - - // Attempt to allocate new file entry - pFileEntry = AllocateFileEntry(ha, szFileName, lcLocale, &dwHashIndex); - if(pFileEntry == NULL) - nError = ERROR_DISK_FULL; - } - - // Set the file entry to the file structure - hf->pFileEntry = pFileEntry; - } - - // Prepare the pointer to hash table entry - if(nError == ERROR_SUCCESS && ha->pHashTable != NULL && dwHashIndex < ha->pHeader->dwHashTableSize) - { - hf->pHashEntry = ha->pHashTable + dwHashIndex; - hf->pHashEntry->lcLocale = (USHORT)lcLocale; - } - - // Prepare the file key - if(nError == ERROR_SUCCESS && (dwFlags & MPQ_FILE_ENCRYPTED)) - { - hf->dwFileKey = DecryptFileKey(szFileName, hf->MpqFilePos, dwFileSize, dwFlags); - if(hf->dwFileKey == 0) - nError = ERROR_UNKNOWN_FILE_KEY; - } - - // Fill the file entry and TMPQFile structure - if(nError == 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); - - nError = FillWritableHandle(ha, hf, FileTime, dwFileSize, dwFlags); - } - - // Free the file handle if failed - if(nError != ERROR_SUCCESS && hf != NULL) - FreeFileHandle(hf); - - // Give the handle to the caller - *phf = hf; - return nError; -} - -int 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; - int nError = ERROR_SUCCESS; - - // Allocate the TMPQFile entry for newly added file - hf = CreateWritableHandle(ha, dwFileSize); - if(hf == NULL) - nError = 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(nError == 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 - nError = ERROR_ALREADY_EXISTS; - - // Set the file entry to the file structure - hf->pFileEntry = pFileEntry; - } - - // Prepare the pointer to hash table entry - if(nError == 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(nError == ERROR_SUCCESS && (dwFlags & MPQ_FILE_ENCRYPTED)) - { - hf->dwFileKey = hfSrc->dwFileKey; - if(hf->dwFileKey == 0) - nError = ERROR_UNKNOWN_FILE_KEY; - } - - // Fill the file entry and TMPQFile structure - if(nError == ERROR_SUCCESS) - { - nError = FillWritableHandle(ha, hf, FileTime, dwFileSize, dwFlags); - } - - // Free the file handle if failed - if(nError != ERROR_SUCCESS && hf != NULL) - FreeFileHandle(hf); - - // Give the handle to the caller - *phf = hf; - return nError; -} - -int SFileAddFile_Write(TMPQFile * hf, const void * pvData, DWORD dwSize, DWORD dwCompression) -{ - TMPQArchive * ha; - TFileEntry * pFileEntry; - int nError = 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->nAddFileError = nError = AllocateSectorBuffer(hf); - if(nError != ERROR_SUCCESS) - return nError; - - // Allocate patch info, if the data is patch - if(hf->pPatchInfo == NULL && IsIncrementalPatchFile(pvData, dwSize, &hf->dwPatchedFileSize)) - { - // Set the MPQ_FILE_PATCH_FILE flag - hf->pFileEntry->dwFlags |= MPQ_FILE_PATCH_FILE; - - // Allocate the patch info - hf->nAddFileError = nError = AllocatePatchInfo(hf, false); - if(nError != ERROR_SUCCESS) - return nError; - } - - // Allocate sector offsets - if(hf->SectorOffsets == NULL) - { - hf->nAddFileError = nError = AllocateSectorOffsets(hf, false); - if(nError != ERROR_SUCCESS) - return nError; - } - - // Create array of sector checksums - if(hf->SectorChksums == NULL && (pFileEntry->dwFlags & MPQ_FILE_SECTOR_CRC)) - { - hf->nAddFileError = nError = AllocateSectorChecksums(hf, false); - if(nError != ERROR_SUCCESS) - return nError; - } - - // Pre-save the patch info, if any - if(hf->pPatchInfo != NULL) - { - if(!FileStream_Write(ha->pStream, &RawFilePos, hf->pPatchInfo, hf->pPatchInfo->dwLength)) - nError = 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])) - nError = GetLastError(); - - pFileEntry->dwCmpSize += hf->SectorOffsets[0]; - RawFilePos += hf->SectorOffsets[0]; - } - } - - // Write the MPQ data to the file - if(nError == 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 - nError = 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(nError == ERROR_SUCCESS) - { - if(hf->dwFilePos >= pFileEntry->dwFileSize) - { - // Finish calculating CRC32 - hf->pFileEntry->dwCrc32 = hf->dwCrc32; - - // Finish calculating MD5 - md5_done((hash_state *)hf->hctx, hf->pFileEntry->md5); - - // If we also have sector checksums, write them to the file - if(hf->SectorChksums != NULL) - { - nError = WriteSectorChecksums(hf); - } - - // Now write patch info - if(hf->pPatchInfo != NULL) - { - memcpy(hf->pPatchInfo->md5, hf->pFileEntry->md5, MD5_DIGEST_SIZE); - hf->pPatchInfo->dwDataSize = hf->pFileEntry->dwFileSize; - hf->pFileEntry->dwFileSize = hf->dwPatchedFileSize; - nError = WritePatchInfo(hf); - } - - // Now write sector offsets to the file - if(hf->SectorOffsets != NULL) - { - nError = WriteSectorOffsets(hf); - } - - // Write the MD5 hashes of each file chunk, if required - if(ha->pHeader->dwRawChunkSize != 0) - { - nError = WriteMpqDataMD5(ha->pStream, - ha->MpqPos + hf->pFileEntry->ByteOffset, - hf->pFileEntry->dwCmpSize, - ha->pHeader->dwRawChunkSize); - } - } - } - - // Store the error code from the Write File operation - hf->nAddFileError = nError; - return nError; -} - -int SFileAddFile_Finish(TMPQFile * hf) -{ - TMPQArchive * ha = hf->ha; - TFileEntry * pFileEntry = hf->pFileEntry; - int nError = hf->nAddFileError; - - // If all previous operations succeeded, we can update the MPQ - if(nError == ERROR_SUCCESS) - { - // Verify if the caller wrote the file properly - if(hf->pPatchInfo == NULL) - { - assert(pFileEntry != NULL); - if(hf->dwFilePos != pFileEntry->dwFileSize) - nError = ERROR_CAN_NOT_COMPLETE; - } - else - { - if(hf->dwFilePos != hf->pPatchInfo->dwDataSize) - nError = ERROR_CAN_NOT_COMPLETE; - } - } - - // Now we need to recreate the HET table, if exists - if(nError == ERROR_SUCCESS && ha->pHetTable != NULL) - { - nError = RebuildHetTable(ha); - } - - // Update the block table size - if(nError == 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 nError; -} - -//----------------------------------------------------------------------------- -// 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; - int nError = ERROR_SUCCESS; - - // Check valid parameters - if(!IsValidMpqHandle(hMpq)) - nError = ERROR_INVALID_HANDLE; - if(szArchivedName == NULL || *szArchivedName == 0) - nError = ERROR_INVALID_PARAMETER; - if(phFile == NULL) - nError = ERROR_INVALID_PARAMETER; - - // Don't allow to add file if the MPQ is open for read only - if(nError == ERROR_SUCCESS) - { - if(ha->dwFlags & MPQ_FLAG_READ_ONLY) - nError = ERROR_ACCESS_DENIED; - - // Don't allow to add a file under pseudo-file name - if(IsPseudoFileName(szArchivedName, NULL)) - nError = ERROR_INVALID_PARAMETER; - - // Don't allow to add any of the internal files - if(IsInternalMpqFileName(szArchivedName)) - nError = ERROR_INTERNAL_FILE; - } - - // Perform validity check of the MPQ flags - if(nError == ERROR_SUCCESS) - { - // Mask all unsupported flags out - dwFlags &= MPQ_FILE_VALID_FLAGS; - - // Check for valid flag combinations - if((dwFlags & (MPQ_FILE_IMPLODE | MPQ_FILE_COMPRESS)) == (MPQ_FILE_IMPLODE | MPQ_FILE_COMPRESS)) - nError = ERROR_INVALID_PARAMETER; - } - - // Initiate the add file operation - if(nError == ERROR_SUCCESS) - nError = SFileAddFile_Init(ha, szArchivedName, FileTime, dwFileSize, lcLocale, dwFlags, (TMPQFile **)phFile); - - // Deal with the errors - if(nError != ERROR_SUCCESS) - SetLastError(nError); - return (nError == ERROR_SUCCESS); -} - -bool WINAPI SFileWriteFile( - HANDLE hFile, - const void * pvData, - DWORD dwSize, - DWORD dwCompression) -{ - TMPQFile * hf = (TMPQFile *)hFile; - int nError = ERROR_SUCCESS; - - // Check the proper parameters - if(!IsValidFileHandle(hFile)) - nError = ERROR_INVALID_HANDLE; - if(hf->bIsWriteHandle == false) - nError = ERROR_INVALID_HANDLE; - - // Special checks for single unit files - if(nError == 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) -// nError = ERROR_INVALID_PARAMETER; -// -// if(dwFlags & MPQ_FILE_ENCRYPTED) -// nError = ERROR_INVALID_PARAMETER; - - // Lossy compression is not allowed on single unit files - if(dwCompression & MPQ_LOSSY_COMPRESSION_MASK) - nError = ERROR_INVALID_PARAMETER; - } - - - // Write the data to the file - if(nError == ERROR_SUCCESS) - nError = SFileAddFile_Write(hf, pvData, dwSize, dwCompression); - - // Deal with errors - if(nError != ERROR_SUCCESS) - SetLastError(nError); - return (nError == ERROR_SUCCESS); -} - -bool WINAPI SFileFinishFile(HANDLE hFile) -{ - TMPQFile * hf = (TMPQFile *)hFile; - int nError = ERROR_SUCCESS; - - // Check the proper parameters - if(!IsValidFileHandle(hFile)) - nError = ERROR_INVALID_HANDLE; - if(hf->bIsWriteHandle == false) - nError = ERROR_INVALID_HANDLE; - - // Finish the file - if(nError == ERROR_SUCCESS) - nError = SFileAddFile_Finish(hf); - - // Deal with errors - if(nError != ERROR_SUCCESS) - SetLastError(nError); - return (nError == 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; - int nError = 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) - nError = ERROR_DISK_FULL; - - // Allocate data buffer for reading from the source file - if(nError == ERROR_SUCCESS) - { - dwBytesRemaining = (DWORD)FileSize; - pbFileData = STORM_ALLOC(BYTE, dwSectorSize); - if(pbFileData == NULL) - nError = ERROR_NOT_ENOUGH_MEMORY; - } - - // Deal with various combination of compressions - if(nError == 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, lcFileLocale, dwFlags, &hMpqFile)) - nError = GetLastError(); - } - - // Write the file data to the MPQ - while(nError == 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)) - { - nError = 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)) - { - nError = GetLastError(); - break; - } - - // Set the next data compression - dwBytesRemaining -= dwBytesToRead; - dwCompression = dwCompressionNext; - } - - // Finish the file writing - if(hMpqFile != NULL) - { - if(!SFileFinishFile(hMpqFile)) - nError = GetLastError(); - } - - // Cleanup and exit - if(pbFileData != NULL) - STORM_FREE(pbFileData); - if(pStream != NULL) - FileStream_Close(pStream); - if(nError != ERROR_SUCCESS) - SetLastError(nError); - return (nError == 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; - int nError = ERROR_SUCCESS; - - // Keep compiler happy - dwSearchScope = dwSearchScope; - - // Check the parameters - if(ha == NULL) - nError = ERROR_INVALID_HANDLE; - if(szFileName == NULL || *szFileName == 0) - nError = ERROR_INVALID_PARAMETER; - if(IsInternalMpqFileName(szFileName)) - nError = ERROR_INTERNAL_FILE; - - // Do not allow to remove files from read-only or patched MPQs - if(nError == ERROR_SUCCESS) - { - if((ha->dwFlags & MPQ_FLAG_READ_ONLY) || (ha->haPatch != NULL)) - nError = ERROR_ACCESS_DENIED; - } - - // If all checks have passed, we can delete the file from the MPQ - if(nError == ERROR_SUCCESS) - { - // Open the file from the MPQ - if(SFileOpenFileEx(hMpq, szFileName, SFILE_OPEN_BASE_FILE, (HANDLE *)&hf)) - { - // Delete the file entry - nError = DeleteFileEntry(ha, hf); - FreeFileHandle(hf); - } - else - nError = GetLastError(); - } - - // If the file has been deleted, we need to invalidate - // the internal files and recreate HET table - if(nError == 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(nError != ERROR_SUCCESS) - SetLastError(nError); - return (nError == 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; - int nError = ERROR_SUCCESS; - - // Test the valid parameters - if(ha == NULL) - nError = ERROR_INVALID_HANDLE; - if(szFileName == NULL || *szFileName == 0 || szNewFileName == NULL || *szNewFileName == 0) - nError = ERROR_INVALID_PARAMETER; - if(IsInternalMpqFileName(szFileName) || IsInternalMpqFileName(szNewFileName)) - nError = ERROR_INTERNAL_FILE; - - // Do not allow to rename files in MPQ open for read only - if(nError == ERROR_SUCCESS) - { - if(ha->dwFlags & MPQ_FLAG_READ_ONLY) - nError = ERROR_ACCESS_DENIED; - } - - // Open the new file. If exists, we don't allow rename operation - if(nError == ERROR_SUCCESS) - { - if(GetFileEntryLocale(ha, szNewFileName, lcFileLocale) != NULL) - nError = ERROR_ALREADY_EXISTS; - } - - // Open the file from the MPQ - if(nError == 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 - nError = RenameFileEntry(ha, hf, szNewFileName); - - // If the file is encrypted, we have to re-crypt the file content - // with the new decryption key - if((nError == ERROR_SUCCESS) && (pFileEntry->dwFlags & MPQ_FILE_ENCRYPTED)) - { - // Recrypt the file data in the MPQ - nError = RecryptFileData(ha, hf, szFileName, szNewFileName); - - // Update the MD5 of the raw block - if(nError == 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 - { - nError = GetLastError(); - } - } - - // We also need to rebuild the HET table, if present - if(nError == ERROR_SUCCESS && ha->pHetTable != NULL) - nError = RebuildHetTable(ha); - - // Resolve error and exit - if(nError != ERROR_SUCCESS) - SetLastError(nError); - return (nError == 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; -} +/*****************************************************************************/ +/* 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->nAddFileError = ERROR_SUCCESS; + + return ERROR_SUCCESS; +} + +//----------------------------------------------------------------------------- +// MPQ write data functions + +static int 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 + int nCompressionLevel; // ADPCM compression level (only used for wave files) + int nError = ERROR_SUCCESS; + + // 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(nError == 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) + { + nError = 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)) + { + nError = 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 nError; +} + +//----------------------------------------------------------------------------- +// Recrypts file data for file renaming + +static int 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; + int nError = 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 + nError = AllocateSectorBuffer(hf); + if(nError != ERROR_SUCCESS) + return nError; + + // Also allocate buffer for sector offsets + // Note: Don't load sector checksums, we don't need to recrypt them + nError = AllocateSectorOffsets(hf, true); + if(nError != ERROR_SUCCESS) + return nError; + + // 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)) + nError = 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(nError == 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)) + { + nError = 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)) + { + nError = GetLastError(); + break; + } + + // Decrement number of bytes remaining + dwBytesToRecrypt -= hf->dwSectorSize; + } + } + + return nError; +} + +//----------------------------------------------------------------------------- +// Internal support for MPQ modifications + +int 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; + int nError = 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(nError == 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 + nError = ERROR_ALREADY_EXISTS; + } + else + { + // Free all internal files - (listfile), (attributes), (signature) + InvalidateInternalFiles(ha); + + // Attempt to allocate new file entry + pFileEntry = AllocateFileEntry(ha, szFileName, lcLocale, &dwHashIndex); + if(pFileEntry == NULL) + nError = ERROR_DISK_FULL; + } + + // Set the file entry to the file structure + hf->pFileEntry = pFileEntry; + } + + // Prepare the pointer to hash table entry + if(nError == ERROR_SUCCESS && ha->pHashTable != NULL && dwHashIndex < ha->pHeader->dwHashTableSize) + { + hf->pHashEntry = ha->pHashTable + dwHashIndex; + hf->pHashEntry->lcLocale = (USHORT)lcLocale; + } + + // Prepare the file key + if(nError == ERROR_SUCCESS && (dwFlags & MPQ_FILE_ENCRYPTED)) + { + hf->dwFileKey = DecryptFileKey(szFileName, hf->MpqFilePos, dwFileSize, dwFlags); + if(hf->dwFileKey == 0) + nError = ERROR_UNKNOWN_FILE_KEY; + } + + // Fill the file entry and TMPQFile structure + if(nError == 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); + + nError = FillWritableHandle(ha, hf, FileTime, dwFileSize, dwFlags); + } + + // Free the file handle if failed + if(nError != ERROR_SUCCESS && hf != NULL) + FreeFileHandle(hf); + + // Give the handle to the caller + *phf = hf; + return nError; +} + +int 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; + int nError = ERROR_SUCCESS; + + // Allocate the TMPQFile entry for newly added file + hf = CreateWritableHandle(ha, dwFileSize); + if(hf == NULL) + nError = 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(nError == 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 + nError = ERROR_ALREADY_EXISTS; + + // Set the file entry to the file structure + hf->pFileEntry = pFileEntry; + } + + // Prepare the pointer to hash table entry + if(nError == 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(nError == ERROR_SUCCESS && (dwFlags & MPQ_FILE_ENCRYPTED)) + { + hf->dwFileKey = hfSrc->dwFileKey; + if(hf->dwFileKey == 0) + nError = ERROR_UNKNOWN_FILE_KEY; + } + + // Fill the file entry and TMPQFile structure + if(nError == ERROR_SUCCESS) + { + nError = FillWritableHandle(ha, hf, FileTime, dwFileSize, dwFlags); + } + + // Free the file handle if failed + if(nError != ERROR_SUCCESS && hf != NULL) + FreeFileHandle(hf); + + // Give the handle to the caller + *phf = hf; + return nError; +} + +int SFileAddFile_Write(TMPQFile * hf, const void * pvData, DWORD dwSize, DWORD dwCompression) +{ + TMPQArchive * ha; + TFileEntry * pFileEntry; + int nError = 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->nAddFileError = nError = AllocateSectorBuffer(hf); + if(nError != ERROR_SUCCESS) + return nError; + + // Allocate patch info, if the data is patch + if(hf->pPatchInfo == NULL && IsIncrementalPatchFile(pvData, dwSize, &hf->dwPatchedFileSize)) + { + // Set the MPQ_FILE_PATCH_FILE flag + hf->pFileEntry->dwFlags |= MPQ_FILE_PATCH_FILE; + + // Allocate the patch info + hf->nAddFileError = nError = AllocatePatchInfo(hf, false); + if(nError != ERROR_SUCCESS) + return nError; + } + + // Allocate sector offsets + if(hf->SectorOffsets == NULL) + { + hf->nAddFileError = nError = AllocateSectorOffsets(hf, false); + if(nError != ERROR_SUCCESS) + return nError; + } + + // Create array of sector checksums + if(hf->SectorChksums == NULL && (pFileEntry->dwFlags & MPQ_FILE_SECTOR_CRC)) + { + hf->nAddFileError = nError = AllocateSectorChecksums(hf, false); + if(nError != ERROR_SUCCESS) + return nError; + } + + // Pre-save the patch info, if any + if(hf->pPatchInfo != NULL) + { + if(!FileStream_Write(ha->pStream, &RawFilePos, hf->pPatchInfo, hf->pPatchInfo->dwLength)) + nError = 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])) + nError = GetLastError(); + + pFileEntry->dwCmpSize += hf->SectorOffsets[0]; + RawFilePos += hf->SectorOffsets[0]; + } + } + + // Write the MPQ data to the file + if(nError == 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 + nError = 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(nError == ERROR_SUCCESS) + { + if(hf->dwFilePos >= pFileEntry->dwFileSize) + { + // Finish calculating CRC32 + hf->pFileEntry->dwCrc32 = hf->dwCrc32; + + // Finish calculating MD5 + md5_done((hash_state *)hf->hctx, hf->pFileEntry->md5); + + // If we also have sector checksums, write them to the file + if(hf->SectorChksums != NULL) + { + nError = WriteSectorChecksums(hf); + } + + // Now write patch info + if(hf->pPatchInfo != NULL) + { + memcpy(hf->pPatchInfo->md5, hf->pFileEntry->md5, MD5_DIGEST_SIZE); + hf->pPatchInfo->dwDataSize = hf->pFileEntry->dwFileSize; + hf->pFileEntry->dwFileSize = hf->dwPatchedFileSize; + nError = WritePatchInfo(hf); + } + + // Now write sector offsets to the file + if(hf->SectorOffsets != NULL) + { + nError = WriteSectorOffsets(hf); + } + + // Write the MD5 hashes of each file chunk, if required + if(ha->pHeader->dwRawChunkSize != 0) + { + nError = WriteMpqDataMD5(ha->pStream, + ha->MpqPos + hf->pFileEntry->ByteOffset, + hf->pFileEntry->dwCmpSize, + ha->pHeader->dwRawChunkSize); + } + } + } + + // Store the error code from the Write File operation + hf->nAddFileError = nError; + return nError; +} + +int SFileAddFile_Finish(TMPQFile * hf) +{ + TMPQArchive * ha = hf->ha; + TFileEntry * pFileEntry = hf->pFileEntry; + int nError = hf->nAddFileError; + + // If all previous operations succeeded, we can update the MPQ + if(nError == ERROR_SUCCESS) + { + // Verify if the caller wrote the file properly + if(hf->pPatchInfo == NULL) + { + assert(pFileEntry != NULL); + if(hf->dwFilePos != pFileEntry->dwFileSize) + nError = ERROR_CAN_NOT_COMPLETE; + } + else + { + if(hf->dwFilePos != hf->pPatchInfo->dwDataSize) + nError = ERROR_CAN_NOT_COMPLETE; + } + } + + // Now we need to recreate the HET table, if exists + if(nError == ERROR_SUCCESS && ha->pHetTable != NULL) + { + nError = RebuildHetTable(ha); + } + + // Update the block table size + if(nError == 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 nError; +} + +//----------------------------------------------------------------------------- +// 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; + int nError = ERROR_SUCCESS; + + // Check valid parameters + if(!IsValidMpqHandle(hMpq)) + nError = ERROR_INVALID_HANDLE; + if(szArchivedName == NULL || *szArchivedName == 0) + nError = ERROR_INVALID_PARAMETER; + if(phFile == NULL) + nError = ERROR_INVALID_PARAMETER; + + // Don't allow to add file if the MPQ is open for read only + if(nError == ERROR_SUCCESS) + { + if(ha->dwFlags & MPQ_FLAG_READ_ONLY) + nError = ERROR_ACCESS_DENIED; + + // Don't allow to add a file under pseudo-file name + if(IsPseudoFileName(szArchivedName, NULL)) + nError = ERROR_INVALID_PARAMETER; + + // Don't allow to add any of the internal files + if(IsInternalMpqFileName(szArchivedName)) + nError = ERROR_INTERNAL_FILE; + } + + // Perform validity check of the MPQ flags + if(nError == ERROR_SUCCESS) + { + // Mask all unsupported flags out + dwFlags &= MPQ_FILE_VALID_FLAGS; + + // Check for valid flag combinations + if((dwFlags & (MPQ_FILE_IMPLODE | MPQ_FILE_COMPRESS)) == (MPQ_FILE_IMPLODE | MPQ_FILE_COMPRESS)) + nError = ERROR_INVALID_PARAMETER; + } + + // Initiate the add file operation + if(nError == ERROR_SUCCESS) + nError = SFileAddFile_Init(ha, szArchivedName, FileTime, dwFileSize, lcLocale, dwFlags, (TMPQFile **)phFile); + + // Deal with the errors + if(nError != ERROR_SUCCESS) + SetLastError(nError); + return (nError == ERROR_SUCCESS); +} + +bool WINAPI SFileWriteFile( + HANDLE hFile, + const void * pvData, + DWORD dwSize, + DWORD dwCompression) +{ + TMPQFile * hf = (TMPQFile *)hFile; + int nError = ERROR_SUCCESS; + + // Check the proper parameters + if(!IsValidFileHandle(hFile)) + nError = ERROR_INVALID_HANDLE; + if(hf->bIsWriteHandle == false) + nError = ERROR_INVALID_HANDLE; + + // Special checks for single unit files + if(nError == 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) +// nError = ERROR_INVALID_PARAMETER; +// +// if(dwFlags & MPQ_FILE_ENCRYPTED) +// nError = ERROR_INVALID_PARAMETER; + + // Lossy compression is not allowed on single unit files + if(dwCompression & MPQ_LOSSY_COMPRESSION_MASK) + nError = ERROR_INVALID_PARAMETER; + } + + + // Write the data to the file + if(nError == ERROR_SUCCESS) + nError = SFileAddFile_Write(hf, pvData, dwSize, dwCompression); + + // Deal with errors + if(nError != ERROR_SUCCESS) + SetLastError(nError); + return (nError == ERROR_SUCCESS); +} + +bool WINAPI SFileFinishFile(HANDLE hFile) +{ + TMPQFile * hf = (TMPQFile *)hFile; + int nError = ERROR_SUCCESS; + + // Check the proper parameters + if(!IsValidFileHandle(hFile)) + nError = ERROR_INVALID_HANDLE; + if(hf->bIsWriteHandle == false) + nError = ERROR_INVALID_HANDLE; + + // Finish the file + if(nError == ERROR_SUCCESS) + nError = SFileAddFile_Finish(hf); + + // Deal with errors + if(nError != ERROR_SUCCESS) + SetLastError(nError); + return (nError == 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; + int nError = 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) + nError = ERROR_DISK_FULL; + + // Allocate data buffer for reading from the source file + if(nError == ERROR_SUCCESS) + { + dwBytesRemaining = (DWORD)FileSize; + pbFileData = STORM_ALLOC(BYTE, dwSectorSize); + if(pbFileData == NULL) + nError = ERROR_NOT_ENOUGH_MEMORY; + } + + // Deal with various combination of compressions + if(nError == 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, lcFileLocale, dwFlags, &hMpqFile)) + nError = GetLastError(); + } + + // Write the file data to the MPQ + while(nError == 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)) + { + nError = 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)) + { + nError = GetLastError(); + break; + } + + // Set the next data compression + dwBytesRemaining -= dwBytesToRead; + dwCompression = dwCompressionNext; + } + + // Finish the file writing + if(hMpqFile != NULL) + { + if(!SFileFinishFile(hMpqFile)) + nError = GetLastError(); + } + + // Cleanup and exit + if(pbFileData != NULL) + STORM_FREE(pbFileData); + if(pStream != NULL) + FileStream_Close(pStream); + if(nError != ERROR_SUCCESS) + SetLastError(nError); + return (nError == 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; + int nError = ERROR_SUCCESS; + + // Keep compiler happy + dwSearchScope = dwSearchScope; + + // Check the parameters + if(ha == NULL) + nError = ERROR_INVALID_HANDLE; + if(szFileName == NULL || *szFileName == 0) + nError = ERROR_INVALID_PARAMETER; + if(IsInternalMpqFileName(szFileName)) + nError = ERROR_INTERNAL_FILE; + + // Do not allow to remove files from read-only or patched MPQs + if(nError == ERROR_SUCCESS) + { + if((ha->dwFlags & MPQ_FLAG_READ_ONLY) || (ha->haPatch != NULL)) + nError = ERROR_ACCESS_DENIED; + } + + // If all checks have passed, we can delete the file from the MPQ + if(nError == ERROR_SUCCESS) + { + // Open the file from the MPQ + if(SFileOpenFileEx(hMpq, szFileName, SFILE_OPEN_BASE_FILE, (HANDLE *)&hf)) + { + // Delete the file entry + nError = DeleteFileEntry(ha, hf); + FreeFileHandle(hf); + } + else + nError = GetLastError(); + } + + // If the file has been deleted, we need to invalidate + // the internal files and recreate HET table + if(nError == 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(nError != ERROR_SUCCESS) + SetLastError(nError); + return (nError == 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; + int nError = ERROR_SUCCESS; + + // Test the valid parameters + if(ha == NULL) + nError = ERROR_INVALID_HANDLE; + if(szFileName == NULL || *szFileName == 0 || szNewFileName == NULL || *szNewFileName == 0) + nError = ERROR_INVALID_PARAMETER; + if(IsInternalMpqFileName(szFileName) || IsInternalMpqFileName(szNewFileName)) + nError = ERROR_INTERNAL_FILE; + + // Do not allow to rename files in MPQ open for read only + if(nError == ERROR_SUCCESS) + { + if(ha->dwFlags & MPQ_FLAG_READ_ONLY) + nError = ERROR_ACCESS_DENIED; + } + + // Open the new file. If exists, we don't allow rename operation + if(nError == ERROR_SUCCESS) + { + if(GetFileEntryLocale(ha, szNewFileName, lcFileLocale) != NULL) + nError = ERROR_ALREADY_EXISTS; + } + + // Open the file from the MPQ + if(nError == 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 + nError = RenameFileEntry(ha, hf, szNewFileName); + + // If the file is encrypted, we have to re-crypt the file content + // with the new decryption key + if((nError == ERROR_SUCCESS) && (pFileEntry->dwFlags & MPQ_FILE_ENCRYPTED)) + { + // Recrypt the file data in the MPQ + nError = RecryptFileData(ha, hf, szFileName, szNewFileName); + + // Update the MD5 of the raw block + if(nError == 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 + { + nError = GetLastError(); + } + } + + // We also need to rebuild the HET table, if present + if(nError == ERROR_SUCCESS && ha->pHetTable != NULL) + nError = RebuildHetTable(ha); + + // Resolve error and exit + if(nError != ERROR_SUCCESS) + SetLastError(nError); + return (nError == 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; +} diff --git a/src/SFileAttributes.cpp b/src/SFileAttributes.cpp index dc13b2c..d58426c 100644 --- a/src/SFileAttributes.cpp +++ b/src/SFileAttributes.cpp @@ -1,570 +1,570 @@ -/*****************************************************************************/ -/* SAttrFile.cpp Copyright (c) Ladislav Zezula 2007 */ -/*---------------------------------------------------------------------------*/ -/* Description: */ -/*---------------------------------------------------------------------------*/ -/* Date Ver Who Comment */ -/* -------- ---- --- ------- */ -/* 12.06.04 1.00 Lad The first version of SAttrFile.cpp */ -/*****************************************************************************/ - -#define __STORMLIB_SELF__ -#include "StormLib.h" -#include "StormCommon.h" - -//----------------------------------------------------------------------------- -// Local structures - -typedef struct _MPQ_ATTRIBUTES_HEADER -{ - DWORD dwVersion; // Version of the (attributes) file. Must be 100 (0x64) - DWORD dwFlags; // See MPQ_ATTRIBUTE_XXXX - - // Followed by an array of CRC32 - // Followed by an array of file times - // Followed by an array of MD5 - // Followed by an array of patch bits - - // Note: The MD5 in (attributes), if present, is a hash of the entire file. - // In case the file is an incremental patch, it contains MD5 of the file - // after being patched. - -} MPQ_ATTRIBUTES_HEADER, *PMPQ_ATTRIBUTES_HEADER; - -//----------------------------------------------------------------------------- -// Local functions - -static DWORD GetSizeOfAttributesFile(DWORD dwAttrFlags, DWORD dwBlockTableSize) -{ - DWORD cbAttrFile = sizeof(MPQ_ATTRIBUTES_HEADER); - - // Calculate size of the (attributes) file - if(dwAttrFlags & MPQ_ATTRIBUTE_CRC32) - cbAttrFile += dwBlockTableSize * sizeof(DWORD); - if(dwAttrFlags & MPQ_ATTRIBUTE_FILETIME) - cbAttrFile += dwBlockTableSize * sizeof(ULONGLONG); - if(dwAttrFlags & MPQ_ATTRIBUTE_MD5) - cbAttrFile += dwBlockTableSize * MD5_DIGEST_SIZE; - - // The bit array has been created without the last bit belonging to (attributes) - // When the number of files is a multiplier of 8 plus one, then the size of (attributes) - // if 1 byte less than expected. - // Example: wow-update-13164.MPQ: BlockTableSize = 0x62E1, but there's only 0xC5C bytes - if(dwAttrFlags & MPQ_ATTRIBUTE_PATCH_BIT) - cbAttrFile += (dwBlockTableSize + 6) / 8; - - return cbAttrFile; -} - -static DWORD CheckSizeOfAttributesFile(DWORD cbAttrFile, DWORD dwAttrFlags, DWORD dwBlockTableSize) -{ - DWORD cbHeaderSize = sizeof(MPQ_ATTRIBUTES_HEADER); - DWORD cbChecksumSize1 = 0; - DWORD cbChecksumSize2 = 0; - DWORD cbFileTimeSize1 = 0; - DWORD cbFileTimeSize2 = 0; - DWORD cbFileHashSize1 = 0; - DWORD cbFileHashSize2 = 0; - DWORD cbPatchBitSize1 = 0; - DWORD cbPatchBitSize2 = 0; - DWORD cbPatchBitSize3 = 0; - - // - // Various variants with the patch bit - // - // interface.MPQ.part from WoW build 10958 has - // the MPQ_ATTRIBUTE_PATCH_BIT set, but there's an array of DWORDs instead. - // The array is filled with zeros, so we don't know what it should contain - // - // Zenith.SC2MAP has the MPQ_ATTRIBUTE_PATCH_BIT set, but the bit array is missing - // - // Elimination Tournament 2.w3x's (attributes) have one entry less - // - // There may be two variants: Either the (attributes) file has full - // number of entries, or has one entry less - // - - // Get the expected size of CRC32 array - if(dwAttrFlags & MPQ_ATTRIBUTE_CRC32) - { - cbChecksumSize1 += dwBlockTableSize * sizeof(DWORD); - cbChecksumSize2 += cbChecksumSize1 - sizeof(DWORD); - } - - // Get the expected size of FILETIME array - if(dwAttrFlags & MPQ_ATTRIBUTE_FILETIME) - { - cbFileTimeSize1 += dwBlockTableSize * sizeof(ULONGLONG); - cbFileTimeSize2 += cbFileTimeSize1 - sizeof(ULONGLONG); - } - - // Get the expected size of MD5 array - if(dwAttrFlags & MPQ_ATTRIBUTE_MD5) - { - cbFileHashSize1 += dwBlockTableSize * MD5_DIGEST_SIZE; - cbFileHashSize2 += cbFileHashSize1 - MD5_DIGEST_SIZE; - } - - // Get the expected size of patch bit array - if(dwAttrFlags & MPQ_ATTRIBUTE_PATCH_BIT) - { - cbPatchBitSize1 = - cbPatchBitSize2 = ((dwBlockTableSize + 6) / 8); - cbPatchBitSize3 = dwBlockTableSize * sizeof(DWORD); - } - - // Check if the (attributes) file entry count is equal to our file table size - if(cbAttrFile == (cbHeaderSize + cbChecksumSize1 + cbFileTimeSize1 + cbFileHashSize1 + cbPatchBitSize1)) - return dwBlockTableSize; - - // Check if the (attributes) file entry count is equal to our file table size minus one - if(cbAttrFile == (cbHeaderSize + cbChecksumSize2 + cbFileTimeSize2 + cbFileHashSize2 + cbPatchBitSize2)) - return dwBlockTableSize - 1; - - // Zenith.SC2MAP has the MPQ_ATTRIBUTE_PATCH_BIT set, but the bit array is missing - if(cbAttrFile == (cbHeaderSize + cbChecksumSize1 + cbFileTimeSize1 + cbFileHashSize1)) - return dwBlockTableSize; - - // interface.MPQ.part (WoW build 10958) has the MPQ_ATTRIBUTE_PATCH_BIT set - // but there's an array of DWORDs (filled with zeros) instead of array of bits - if(cbAttrFile == (cbHeaderSize + cbChecksumSize1 + cbFileTimeSize1 + cbFileHashSize1 + cbPatchBitSize3)) - return dwBlockTableSize; - -#ifdef __STORMLIB_TEST__ - // Invalid size of the (attributes) file - // Note that many MPQs, especially Warcraft III maps have the size of (attributes) invalid. - // We only perform this check if this is the STORMLIB testprogram itself -// assert(false); -#endif - - return 0; -} - -static int LoadAttributesFile(TMPQArchive * ha, LPBYTE pbAttrFile, DWORD cbAttrFile) -{ - LPBYTE pbAttrFileEnd = pbAttrFile + cbAttrFile; - LPBYTE pbAttrPtr = pbAttrFile; - DWORD dwAttributesEntries = 0; - DWORD i; - - // Load and verify the header - if((pbAttrPtr + sizeof(MPQ_ATTRIBUTES_HEADER)) <= pbAttrFileEnd) - { - PMPQ_ATTRIBUTES_HEADER pAttrHeader = (PMPQ_ATTRIBUTES_HEADER)pbAttrPtr; - - // Verify the header version - BSWAP_ARRAY32_UNSIGNED(pAttrHeader, sizeof(MPQ_ATTRIBUTES_HEADER)); - if(pAttrHeader->dwVersion != MPQ_ATTRIBUTES_V1) - return ERROR_BAD_FORMAT; - - // Verify the flags - if(pAttrHeader->dwFlags & ~MPQ_ATTRIBUTE_ALL) - return ERROR_BAD_FORMAT; - - // Verify whether file size of (attributes) is expected - dwAttributesEntries = CheckSizeOfAttributesFile(cbAttrFile, pAttrHeader->dwFlags, ha->pHeader->dwBlockTableSize); - if(dwAttributesEntries == 0) - return ERROR_BAD_FORMAT; - - ha->dwAttrFlags = pAttrHeader->dwFlags; - pbAttrPtr = (LPBYTE)(pAttrHeader + 1); - } - - // Load the CRC32 (if present) - if(ha->dwAttrFlags & MPQ_ATTRIBUTE_CRC32) - { - LPDWORD ArrayCRC32 = (LPDWORD)pbAttrPtr; - DWORD cbArraySize = dwAttributesEntries * sizeof(DWORD); - - // Verify if there's enough data - if((pbAttrPtr + cbArraySize) > pbAttrFileEnd) - return ERROR_FILE_CORRUPT; - - BSWAP_ARRAY32_UNSIGNED(ArrayCRC32, cbCRC32Size); - for(i = 0; i < dwAttributesEntries; i++) - ha->pFileTable[i].dwCrc32 = ArrayCRC32[i]; - pbAttrPtr += cbArraySize; - } - - // Load the FILETIME (if present) - if(ha->dwAttrFlags & MPQ_ATTRIBUTE_FILETIME) - { - ULONGLONG * ArrayFileTime = (ULONGLONG *)pbAttrPtr; - DWORD cbArraySize = dwAttributesEntries * sizeof(ULONGLONG); - - // Verify if there's enough data - if((pbAttrPtr + cbArraySize) > pbAttrFileEnd) - return ERROR_FILE_CORRUPT; - - BSWAP_ARRAY64_UNSIGNED(ArrayFileTime, cbFileTimeSize); - for(i = 0; i < dwAttributesEntries; i++) - ha->pFileTable[i].FileTime = ArrayFileTime[i]; - pbAttrPtr += cbArraySize; - } - - // Load the MD5 (if present) - if(ha->dwAttrFlags & MPQ_ATTRIBUTE_MD5) - { - LPBYTE ArrayMd5 = pbAttrPtr; - DWORD cbArraySize = dwAttributesEntries * MD5_DIGEST_SIZE; - - // Verify if there's enough data - if((pbAttrPtr + cbArraySize) > pbAttrFileEnd) - return ERROR_FILE_CORRUPT; - - for(i = 0; i < dwAttributesEntries; i++) - { - memcpy(ha->pFileTable[i].md5, ArrayMd5, MD5_DIGEST_SIZE); - ArrayMd5 += MD5_DIGEST_SIZE; - } - pbAttrPtr += cbArraySize; - } - - // Read the patch bit for each file (if present) - if(ha->dwAttrFlags & MPQ_ATTRIBUTE_PATCH_BIT) - { - LPBYTE pbBitArray = pbAttrPtr; - DWORD cbArraySize = (dwAttributesEntries + 7) / 8; - DWORD dwByteIndex = 0; - DWORD dwBitMask = 0x80; - - // Verify if there's enough data - if((pbAttrPtr + cbArraySize) == pbAttrFileEnd) - { - for(i = 0; i < dwAttributesEntries; i++) - { - ha->pFileTable[i].dwFlags |= (pbBitArray[dwByteIndex] & dwBitMask) ? MPQ_FILE_PATCH_FILE : 0; - dwByteIndex += (dwBitMask & 0x01); - dwBitMask = (dwBitMask << 0x07) | (dwBitMask >> 0x01); - } - } - } - - return ERROR_SUCCESS; -} - -static LPBYTE CreateAttributesFile(TMPQArchive * ha, DWORD * pcbAttrFile) -{ - PMPQ_ATTRIBUTES_HEADER pAttrHeader; - TFileEntry * pFileTableEnd = ha->pFileTable + ha->pHeader->dwBlockTableSize; - TFileEntry * pFileEntry; - LPBYTE pbAttrFile; - LPBYTE pbAttrPtr; - size_t cbAttrFile; - - // Check if we need patch bits in the (attributes) file - for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++) - { - if(pFileEntry->dwFlags & MPQ_FILE_PATCH_FILE) - { - ha->dwAttrFlags |= MPQ_ATTRIBUTE_PATCH_BIT; - break; - } - } - - // Allocate the buffer for holding the entire (attributes) - // Allocate 1 byte more (See GetSizeOfAttributesFile for more info) - cbAttrFile = GetSizeOfAttributesFile(ha->dwAttrFlags, ha->pHeader->dwBlockTableSize); - pbAttrFile = pbAttrPtr = STORM_ALLOC(BYTE, cbAttrFile + 1); - if(pbAttrFile != NULL) - { - // Make sure it's all zeroed - memset(pbAttrFile, 0, cbAttrFile + 1); - - // Write the header of the (attributes) file - pAttrHeader = (PMPQ_ATTRIBUTES_HEADER)pbAttrPtr; - pAttrHeader->dwVersion = BSWAP_INT32_UNSIGNED(100); - pAttrHeader->dwFlags = BSWAP_INT32_UNSIGNED((ha->dwAttrFlags & MPQ_ATTRIBUTE_ALL)); - pbAttrPtr = (LPBYTE)(pAttrHeader + 1); - - // Write the array of CRC32, if present - if(ha->dwAttrFlags & MPQ_ATTRIBUTE_CRC32) - { - LPDWORD pArrayCRC32 = (LPDWORD)pbAttrPtr; - - // Copy from file table - for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++) - *pArrayCRC32++ = BSWAP_INT32_UNSIGNED(pFileEntry->dwCrc32); - - // Update pointer - pbAttrPtr = (LPBYTE)pArrayCRC32; - } - - // Write the array of file time - if(ha->dwAttrFlags & MPQ_ATTRIBUTE_FILETIME) - { - ULONGLONG * pArrayFileTime = (ULONGLONG *)pbAttrPtr; - - // Copy from file table - for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++) - *pArrayFileTime++ = BSWAP_INT64_UNSIGNED(pFileEntry->FileTime); - - // Update pointer - pbAttrPtr = (LPBYTE)pArrayFileTime; - } - - // Write the array of MD5s - if(ha->dwAttrFlags & MPQ_ATTRIBUTE_MD5) - { - LPBYTE pbArrayMD5 = pbAttrPtr; - - // Copy from file table - for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++) - { - memcpy(pbArrayMD5, pFileEntry->md5, MD5_DIGEST_SIZE); - pbArrayMD5 += MD5_DIGEST_SIZE; - } - - // Update pointer - pbAttrPtr = pbArrayMD5; - } - - // Write the array of patch bits - if(ha->dwAttrFlags & MPQ_ATTRIBUTE_PATCH_BIT) - { - LPBYTE pbBitArray = pbAttrPtr; - DWORD dwByteIndex = 0; - BYTE dwBitMask = 0x80; - - // Copy from file table - for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++) - { - // Set the bit, if needed - if(pFileEntry->dwFlags & MPQ_FILE_PATCH_FILE) - pbBitArray[dwByteIndex] |= dwBitMask; - - // Update bit index and bit mask - dwByteIndex += (dwBitMask & 0x01); - dwBitMask = (dwBitMask << 0x07) | (dwBitMask >> 0x01); - } - - // Move past the bit array - pbAttrPtr += (ha->pHeader->dwBlockTableSize + 6) / 8; - } - - // Now we expect that current position matches the estimated size - // Note that if there is 1 extra bit above the byte size, - // the table is actually 1 byte shorter in Blizzard MPQs. See GetSizeOfAttributesFile - assert((size_t)(pbAttrPtr - pbAttrFile) == cbAttrFile); - } - - // Give away the attributes file - if(pcbAttrFile != NULL) - *pcbAttrFile = (DWORD)cbAttrFile; - return pbAttrFile; -} - -//----------------------------------------------------------------------------- -// Public functions (internal use by StormLib) - -int SAttrLoadAttributes(TMPQArchive * ha) -{ - HANDLE hFile = NULL; - LPBYTE pbAttrFile; - DWORD dwBytesRead; - DWORD cbAttrFile = 0; - int nError = ERROR_FILE_CORRUPT; - - // File table must be initialized - assert(ha->pFileTable != NULL); - assert((ha->dwFlags & MPQ_FLAG_BLOCK_TABLE_CUT) == 0); - - // Don't load the attributes file from malformed Warcraft III maps - if(ha->dwFlags & MPQ_FLAG_MALFORMED) - return ERROR_FILE_CORRUPT; - - // Attempt to open the "(attributes)" file. - if(SFileOpenFileEx((HANDLE)ha, ATTRIBUTES_NAME, SFILE_OPEN_ANY_LOCALE, &hFile)) - { - // Retrieve and check size of the (attributes) file - cbAttrFile = SFileGetFileSize(hFile, NULL); - - // Integer overflow check - if((cbAttrFile + 1) > cbAttrFile) - { - // Size of the (attributes) might be 1 byte less than expected - // See GetSizeOfAttributesFile for more info - pbAttrFile = STORM_ALLOC(BYTE, cbAttrFile + 1); - if(pbAttrFile != NULL) - { - // Set the last byte to 0 in case the size should be 1 byte greater - pbAttrFile[cbAttrFile] = 0; - - // Load the entire file to memory - SFileReadFile(hFile, pbAttrFile, cbAttrFile, &dwBytesRead, NULL); - if(dwBytesRead == cbAttrFile) - nError = LoadAttributesFile(ha, pbAttrFile, cbAttrFile); - - // Free the buffer - STORM_FREE(pbAttrFile); - } - } - - // Close the attributes file - SFileCloseFile(hFile); - } - - return nError; -} - -// Saves the (attributes) to the MPQ -int SAttrFileSaveToMpq(TMPQArchive * ha) -{ - TMPQFile * hf = NULL; - LPBYTE pbAttrFile; - DWORD cbAttrFile = 0; - int nError = ERROR_SUCCESS; - - // Only save the attributes if we should do so - if(ha->dwFileFlags2 != 0) - { - // At this point, we expect to have at least one reserved entry in the file table - assert(ha->dwFlags & MPQ_FLAG_ATTRIBUTES_NEW); - assert(ha->dwReservedFiles > 0); - - // Create the raw data that is to be written to (attributes) - // Note: Blizzard MPQs have entries for (listfile) and (attributes), - // but they are filled empty - pbAttrFile = CreateAttributesFile(ha, &cbAttrFile); - if(pbAttrFile != NULL) - { - // Determine the real flags for (attributes) - if(ha->dwFileFlags2 == MPQ_FILE_EXISTS) - ha->dwFileFlags2 = GetDefaultSpecialFileFlags(cbAttrFile, ha->pHeader->wFormatVersion); - - // Create the attributes file in the MPQ - nError = SFileAddFile_Init(ha, ATTRIBUTES_NAME, - 0, - cbAttrFile, - LANG_NEUTRAL, - ha->dwFileFlags2 | MPQ_FILE_REPLACEEXISTING, - &hf); - - // Write the attributes file raw data to it - if(nError == ERROR_SUCCESS) - { - // Write the content of the attributes file to the MPQ - nError = SFileAddFile_Write(hf, pbAttrFile, cbAttrFile, MPQ_COMPRESSION_ZLIB); - SFileAddFile_Finish(hf); - } - - // Clear the number of reserved files - ha->dwFlags &= ~(MPQ_FLAG_ATTRIBUTES_NEW | MPQ_FLAG_ATTRIBUTES_NONE); - ha->dwReservedFiles--; - - // Free the attributes buffer - STORM_FREE(pbAttrFile); - } - else - { - // If the (attributes) file would be empty, its OK - nError = (cbAttrFile == 0) ? ERROR_SUCCESS : ERROR_NOT_ENOUGH_MEMORY; - } - } - - return nError; -} - -//----------------------------------------------------------------------------- -// Public functions - -DWORD WINAPI SFileGetAttributes(HANDLE hMpq) -{ - TMPQArchive * ha = (TMPQArchive *)hMpq; - - // Verify the parameters - if(!IsValidMpqHandle(hMpq)) - { - SetLastError(ERROR_INVALID_PARAMETER); - return SFILE_INVALID_ATTRIBUTES; - } - - return ha->dwAttrFlags; -} - -bool WINAPI SFileSetAttributes(HANDLE hMpq, DWORD dwFlags) -{ - TMPQArchive * ha = (TMPQArchive *)hMpq; - - // Verify the parameters - if(!IsValidMpqHandle(hMpq)) - { - SetLastError(ERROR_INVALID_PARAMETER); - return false; - } - - // Not allowed when the archive is read-only - if(ha->dwFlags & MPQ_FLAG_READ_ONLY) - { - SetLastError(ERROR_ACCESS_DENIED); - return false; - } - - // Set the attributes - InvalidateInternalFiles(ha); - ha->dwAttrFlags = (dwFlags & MPQ_ATTRIBUTE_ALL); - return true; -} - -bool WINAPI SFileUpdateFileAttributes(HANDLE hMpq, const char * szFileName) -{ - hash_state md5_state; - TMPQArchive * ha = (TMPQArchive *)hMpq; - TMPQFile * hf; - BYTE Buffer[0x1000]; - HANDLE hFile = NULL; - DWORD dwTotalBytes = 0; - DWORD dwBytesRead; - DWORD dwCrc32; - - // Verify the parameters - if(!IsValidMpqHandle(ha)) - { - SetLastError(ERROR_INVALID_PARAMETER); - return false; - } - - // Not allowed when the archive is read-only - if(ha->dwFlags & MPQ_FLAG_READ_ONLY) - { - SetLastError(ERROR_ACCESS_DENIED); - return false; - } - - // Attempt to open the file - if(!SFileOpenFileEx(hMpq, szFileName, SFILE_OPEN_BASE_FILE, &hFile)) - return false; - - // Get the file size - hf = (TMPQFile *)hFile; - dwTotalBytes = hf->pFileEntry->dwFileSize; - - // Initialize the CRC32 and MD5 contexts - md5_init(&md5_state); - dwCrc32 = crc32(0, Z_NULL, 0); - - // Go through entire file and calculate both CRC32 and MD5 - while(dwTotalBytes != 0) - { - // Read data from file - SFileReadFile(hFile, Buffer, sizeof(Buffer), &dwBytesRead, NULL); - if(dwBytesRead == 0) - break; - - // Update CRC32 and MD5 - dwCrc32 = crc32(dwCrc32, Buffer, dwBytesRead); - md5_process(&md5_state, Buffer, dwBytesRead); - - // Decrement the total size - dwTotalBytes -= dwBytesRead; - } - - // Update both CRC32 and MD5 - hf->pFileEntry->dwCrc32 = dwCrc32; - md5_done(&md5_state, hf->pFileEntry->md5); - - // Remember that we need to save the MPQ tables - InvalidateInternalFiles(ha); - SFileCloseFile(hFile); - return true; -} +/*****************************************************************************/ +/* SAttrFile.cpp Copyright (c) Ladislav Zezula 2007 */ +/*---------------------------------------------------------------------------*/ +/* Description: */ +/*---------------------------------------------------------------------------*/ +/* Date Ver Who Comment */ +/* -------- ---- --- ------- */ +/* 12.06.04 1.00 Lad The first version of SAttrFile.cpp */ +/*****************************************************************************/ + +#define __STORMLIB_SELF__ +#include "StormLib.h" +#include "StormCommon.h" + +//----------------------------------------------------------------------------- +// Local structures + +typedef struct _MPQ_ATTRIBUTES_HEADER +{ + DWORD dwVersion; // Version of the (attributes) file. Must be 100 (0x64) + DWORD dwFlags; // See MPQ_ATTRIBUTE_XXXX + + // Followed by an array of CRC32 + // Followed by an array of file times + // Followed by an array of MD5 + // Followed by an array of patch bits + + // Note: The MD5 in (attributes), if present, is a hash of the entire file. + // In case the file is an incremental patch, it contains MD5 of the file + // after being patched. + +} MPQ_ATTRIBUTES_HEADER, *PMPQ_ATTRIBUTES_HEADER; + +//----------------------------------------------------------------------------- +// Local functions + +static DWORD GetSizeOfAttributesFile(DWORD dwAttrFlags, DWORD dwBlockTableSize) +{ + DWORD cbAttrFile = sizeof(MPQ_ATTRIBUTES_HEADER); + + // Calculate size of the (attributes) file + if(dwAttrFlags & MPQ_ATTRIBUTE_CRC32) + cbAttrFile += dwBlockTableSize * sizeof(DWORD); + if(dwAttrFlags & MPQ_ATTRIBUTE_FILETIME) + cbAttrFile += dwBlockTableSize * sizeof(ULONGLONG); + if(dwAttrFlags & MPQ_ATTRIBUTE_MD5) + cbAttrFile += dwBlockTableSize * MD5_DIGEST_SIZE; + + // The bit array has been created without the last bit belonging to (attributes) + // When the number of files is a multiplier of 8 plus one, then the size of (attributes) + // if 1 byte less than expected. + // Example: wow-update-13164.MPQ: BlockTableSize = 0x62E1, but there's only 0xC5C bytes + if(dwAttrFlags & MPQ_ATTRIBUTE_PATCH_BIT) + cbAttrFile += (dwBlockTableSize + 6) / 8; + + return cbAttrFile; +} + +static DWORD CheckSizeOfAttributesFile(DWORD cbAttrFile, DWORD dwAttrFlags, DWORD dwBlockTableSize) +{ + DWORD cbHeaderSize = sizeof(MPQ_ATTRIBUTES_HEADER); + DWORD cbChecksumSize1 = 0; + DWORD cbChecksumSize2 = 0; + DWORD cbFileTimeSize1 = 0; + DWORD cbFileTimeSize2 = 0; + DWORD cbFileHashSize1 = 0; + DWORD cbFileHashSize2 = 0; + DWORD cbPatchBitSize1 = 0; + DWORD cbPatchBitSize2 = 0; + DWORD cbPatchBitSize3 = 0; + + // + // Various variants with the patch bit + // + // interface.MPQ.part from WoW build 10958 has + // the MPQ_ATTRIBUTE_PATCH_BIT set, but there's an array of DWORDs instead. + // The array is filled with zeros, so we don't know what it should contain + // + // Zenith.SC2MAP has the MPQ_ATTRIBUTE_PATCH_BIT set, but the bit array is missing + // + // Elimination Tournament 2.w3x's (attributes) have one entry less + // + // There may be two variants: Either the (attributes) file has full + // number of entries, or has one entry less + // + + // Get the expected size of CRC32 array + if(dwAttrFlags & MPQ_ATTRIBUTE_CRC32) + { + cbChecksumSize1 += dwBlockTableSize * sizeof(DWORD); + cbChecksumSize2 += cbChecksumSize1 - sizeof(DWORD); + } + + // Get the expected size of FILETIME array + if(dwAttrFlags & MPQ_ATTRIBUTE_FILETIME) + { + cbFileTimeSize1 += dwBlockTableSize * sizeof(ULONGLONG); + cbFileTimeSize2 += cbFileTimeSize1 - sizeof(ULONGLONG); + } + + // Get the expected size of MD5 array + if(dwAttrFlags & MPQ_ATTRIBUTE_MD5) + { + cbFileHashSize1 += dwBlockTableSize * MD5_DIGEST_SIZE; + cbFileHashSize2 += cbFileHashSize1 - MD5_DIGEST_SIZE; + } + + // Get the expected size of patch bit array + if(dwAttrFlags & MPQ_ATTRIBUTE_PATCH_BIT) + { + cbPatchBitSize1 = + cbPatchBitSize2 = ((dwBlockTableSize + 6) / 8); + cbPatchBitSize3 = dwBlockTableSize * sizeof(DWORD); + } + + // Check if the (attributes) file entry count is equal to our file table size + if(cbAttrFile == (cbHeaderSize + cbChecksumSize1 + cbFileTimeSize1 + cbFileHashSize1 + cbPatchBitSize1)) + return dwBlockTableSize; + + // Check if the (attributes) file entry count is equal to our file table size minus one + if(cbAttrFile == (cbHeaderSize + cbChecksumSize2 + cbFileTimeSize2 + cbFileHashSize2 + cbPatchBitSize2)) + return dwBlockTableSize - 1; + + // Zenith.SC2MAP has the MPQ_ATTRIBUTE_PATCH_BIT set, but the bit array is missing + if(cbAttrFile == (cbHeaderSize + cbChecksumSize1 + cbFileTimeSize1 + cbFileHashSize1)) + return dwBlockTableSize; + + // interface.MPQ.part (WoW build 10958) has the MPQ_ATTRIBUTE_PATCH_BIT set + // but there's an array of DWORDs (filled with zeros) instead of array of bits + if(cbAttrFile == (cbHeaderSize + cbChecksumSize1 + cbFileTimeSize1 + cbFileHashSize1 + cbPatchBitSize3)) + return dwBlockTableSize; + +#ifdef __STORMLIB_TEST__ + // Invalid size of the (attributes) file + // Note that many MPQs, especially Warcraft III maps have the size of (attributes) invalid. + // We only perform this check if this is the STORMLIB testprogram itself +// assert(false); +#endif + + return 0; +} + +static int LoadAttributesFile(TMPQArchive * ha, LPBYTE pbAttrFile, DWORD cbAttrFile) +{ + LPBYTE pbAttrFileEnd = pbAttrFile + cbAttrFile; + LPBYTE pbAttrPtr = pbAttrFile; + DWORD dwAttributesEntries = 0; + DWORD i; + + // Load and verify the header + if((pbAttrPtr + sizeof(MPQ_ATTRIBUTES_HEADER)) <= pbAttrFileEnd) + { + PMPQ_ATTRIBUTES_HEADER pAttrHeader = (PMPQ_ATTRIBUTES_HEADER)pbAttrPtr; + + // Verify the header version + BSWAP_ARRAY32_UNSIGNED(pAttrHeader, sizeof(MPQ_ATTRIBUTES_HEADER)); + if(pAttrHeader->dwVersion != MPQ_ATTRIBUTES_V1) + return ERROR_BAD_FORMAT; + + // Verify the flags + if(pAttrHeader->dwFlags & ~MPQ_ATTRIBUTE_ALL) + return ERROR_BAD_FORMAT; + + // Verify whether file size of (attributes) is expected + dwAttributesEntries = CheckSizeOfAttributesFile(cbAttrFile, pAttrHeader->dwFlags, ha->pHeader->dwBlockTableSize); + if(dwAttributesEntries == 0) + return ERROR_BAD_FORMAT; + + ha->dwAttrFlags = pAttrHeader->dwFlags; + pbAttrPtr = (LPBYTE)(pAttrHeader + 1); + } + + // Load the CRC32 (if present) + if(ha->dwAttrFlags & MPQ_ATTRIBUTE_CRC32) + { + LPDWORD ArrayCRC32 = (LPDWORD)pbAttrPtr; + DWORD cbArraySize = dwAttributesEntries * sizeof(DWORD); + + // Verify if there's enough data + if((pbAttrPtr + cbArraySize) > pbAttrFileEnd) + return ERROR_FILE_CORRUPT; + + BSWAP_ARRAY32_UNSIGNED(ArrayCRC32, cbCRC32Size); + for(i = 0; i < dwAttributesEntries; i++) + ha->pFileTable[i].dwCrc32 = ArrayCRC32[i]; + pbAttrPtr += cbArraySize; + } + + // Load the FILETIME (if present) + if(ha->dwAttrFlags & MPQ_ATTRIBUTE_FILETIME) + { + ULONGLONG * ArrayFileTime = (ULONGLONG *)pbAttrPtr; + DWORD cbArraySize = dwAttributesEntries * sizeof(ULONGLONG); + + // Verify if there's enough data + if((pbAttrPtr + cbArraySize) > pbAttrFileEnd) + return ERROR_FILE_CORRUPT; + + BSWAP_ARRAY64_UNSIGNED(ArrayFileTime, cbFileTimeSize); + for(i = 0; i < dwAttributesEntries; i++) + ha->pFileTable[i].FileTime = ArrayFileTime[i]; + pbAttrPtr += cbArraySize; + } + + // Load the MD5 (if present) + if(ha->dwAttrFlags & MPQ_ATTRIBUTE_MD5) + { + LPBYTE ArrayMd5 = pbAttrPtr; + DWORD cbArraySize = dwAttributesEntries * MD5_DIGEST_SIZE; + + // Verify if there's enough data + if((pbAttrPtr + cbArraySize) > pbAttrFileEnd) + return ERROR_FILE_CORRUPT; + + for(i = 0; i < dwAttributesEntries; i++) + { + memcpy(ha->pFileTable[i].md5, ArrayMd5, MD5_DIGEST_SIZE); + ArrayMd5 += MD5_DIGEST_SIZE; + } + pbAttrPtr += cbArraySize; + } + + // Read the patch bit for each file (if present) + if(ha->dwAttrFlags & MPQ_ATTRIBUTE_PATCH_BIT) + { + LPBYTE pbBitArray = pbAttrPtr; + DWORD cbArraySize = (dwAttributesEntries + 7) / 8; + DWORD dwByteIndex = 0; + DWORD dwBitMask = 0x80; + + // Verify if there's enough data + if((pbAttrPtr + cbArraySize) == pbAttrFileEnd) + { + for(i = 0; i < dwAttributesEntries; i++) + { + ha->pFileTable[i].dwFlags |= (pbBitArray[dwByteIndex] & dwBitMask) ? MPQ_FILE_PATCH_FILE : 0; + dwByteIndex += (dwBitMask & 0x01); + dwBitMask = (dwBitMask << 0x07) | (dwBitMask >> 0x01); + } + } + } + + return ERROR_SUCCESS; +} + +static LPBYTE CreateAttributesFile(TMPQArchive * ha, DWORD * pcbAttrFile) +{ + PMPQ_ATTRIBUTES_HEADER pAttrHeader; + TFileEntry * pFileTableEnd = ha->pFileTable + ha->pHeader->dwBlockTableSize; + TFileEntry * pFileEntry; + LPBYTE pbAttrFile; + LPBYTE pbAttrPtr; + size_t cbAttrFile; + + // Check if we need patch bits in the (attributes) file + for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++) + { + if(pFileEntry->dwFlags & MPQ_FILE_PATCH_FILE) + { + ha->dwAttrFlags |= MPQ_ATTRIBUTE_PATCH_BIT; + break; + } + } + + // Allocate the buffer for holding the entire (attributes) + // Allocate 1 byte more (See GetSizeOfAttributesFile for more info) + cbAttrFile = GetSizeOfAttributesFile(ha->dwAttrFlags, ha->pHeader->dwBlockTableSize); + pbAttrFile = pbAttrPtr = STORM_ALLOC(BYTE, cbAttrFile + 1); + if(pbAttrFile != NULL) + { + // Make sure it's all zeroed + memset(pbAttrFile, 0, cbAttrFile + 1); + + // Write the header of the (attributes) file + pAttrHeader = (PMPQ_ATTRIBUTES_HEADER)pbAttrPtr; + pAttrHeader->dwVersion = BSWAP_INT32_UNSIGNED(100); + pAttrHeader->dwFlags = BSWAP_INT32_UNSIGNED((ha->dwAttrFlags & MPQ_ATTRIBUTE_ALL)); + pbAttrPtr = (LPBYTE)(pAttrHeader + 1); + + // Write the array of CRC32, if present + if(ha->dwAttrFlags & MPQ_ATTRIBUTE_CRC32) + { + LPDWORD pArrayCRC32 = (LPDWORD)pbAttrPtr; + + // Copy from file table + for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++) + *pArrayCRC32++ = BSWAP_INT32_UNSIGNED(pFileEntry->dwCrc32); + + // Update pointer + pbAttrPtr = (LPBYTE)pArrayCRC32; + } + + // Write the array of file time + if(ha->dwAttrFlags & MPQ_ATTRIBUTE_FILETIME) + { + ULONGLONG * pArrayFileTime = (ULONGLONG *)pbAttrPtr; + + // Copy from file table + for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++) + *pArrayFileTime++ = BSWAP_INT64_UNSIGNED(pFileEntry->FileTime); + + // Update pointer + pbAttrPtr = (LPBYTE)pArrayFileTime; + } + + // Write the array of MD5s + if(ha->dwAttrFlags & MPQ_ATTRIBUTE_MD5) + { + LPBYTE pbArrayMD5 = pbAttrPtr; + + // Copy from file table + for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++) + { + memcpy(pbArrayMD5, pFileEntry->md5, MD5_DIGEST_SIZE); + pbArrayMD5 += MD5_DIGEST_SIZE; + } + + // Update pointer + pbAttrPtr = pbArrayMD5; + } + + // Write the array of patch bits + if(ha->dwAttrFlags & MPQ_ATTRIBUTE_PATCH_BIT) + { + LPBYTE pbBitArray = pbAttrPtr; + DWORD dwByteIndex = 0; + BYTE dwBitMask = 0x80; + + // Copy from file table + for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++) + { + // Set the bit, if needed + if(pFileEntry->dwFlags & MPQ_FILE_PATCH_FILE) + pbBitArray[dwByteIndex] |= dwBitMask; + + // Update bit index and bit mask + dwByteIndex += (dwBitMask & 0x01); + dwBitMask = (dwBitMask << 0x07) | (dwBitMask >> 0x01); + } + + // Move past the bit array + pbAttrPtr += (ha->pHeader->dwBlockTableSize + 6) / 8; + } + + // Now we expect that current position matches the estimated size + // Note that if there is 1 extra bit above the byte size, + // the table is actually 1 byte shorter in Blizzard MPQs. See GetSizeOfAttributesFile + assert((size_t)(pbAttrPtr - pbAttrFile) == cbAttrFile); + } + + // Give away the attributes file + if(pcbAttrFile != NULL) + *pcbAttrFile = (DWORD)cbAttrFile; + return pbAttrFile; +} + +//----------------------------------------------------------------------------- +// Public functions (internal use by StormLib) + +int SAttrLoadAttributes(TMPQArchive * ha) +{ + HANDLE hFile = NULL; + LPBYTE pbAttrFile; + DWORD dwBytesRead; + DWORD cbAttrFile = 0; + int nError = ERROR_FILE_CORRUPT; + + // File table must be initialized + assert(ha->pFileTable != NULL); + assert((ha->dwFlags & MPQ_FLAG_BLOCK_TABLE_CUT) == 0); + + // Don't load the attributes file from malformed Warcraft III maps + if(ha->dwFlags & MPQ_FLAG_MALFORMED) + return ERROR_FILE_CORRUPT; + + // Attempt to open the "(attributes)" file. + if(SFileOpenFileEx((HANDLE)ha, ATTRIBUTES_NAME, SFILE_OPEN_ANY_LOCALE, &hFile)) + { + // Retrieve and check size of the (attributes) file + cbAttrFile = SFileGetFileSize(hFile, NULL); + + // Integer overflow check + if((cbAttrFile + 1) > cbAttrFile) + { + // Size of the (attributes) might be 1 byte less than expected + // See GetSizeOfAttributesFile for more info + pbAttrFile = STORM_ALLOC(BYTE, cbAttrFile + 1); + if(pbAttrFile != NULL) + { + // Set the last byte to 0 in case the size should be 1 byte greater + pbAttrFile[cbAttrFile] = 0; + + // Load the entire file to memory + SFileReadFile(hFile, pbAttrFile, cbAttrFile, &dwBytesRead, NULL); + if(dwBytesRead == cbAttrFile) + nError = LoadAttributesFile(ha, pbAttrFile, cbAttrFile); + + // Free the buffer + STORM_FREE(pbAttrFile); + } + } + + // Close the attributes file + SFileCloseFile(hFile); + } + + return nError; +} + +// Saves the (attributes) to the MPQ +int SAttrFileSaveToMpq(TMPQArchive * ha) +{ + TMPQFile * hf = NULL; + LPBYTE pbAttrFile; + DWORD cbAttrFile = 0; + int nError = ERROR_SUCCESS; + + // Only save the attributes if we should do so + if(ha->dwFileFlags2 != 0) + { + // At this point, we expect to have at least one reserved entry in the file table + assert(ha->dwFlags & MPQ_FLAG_ATTRIBUTES_NEW); + assert(ha->dwReservedFiles > 0); + + // Create the raw data that is to be written to (attributes) + // Note: Blizzard MPQs have entries for (listfile) and (attributes), + // but they are filled empty + pbAttrFile = CreateAttributesFile(ha, &cbAttrFile); + if(pbAttrFile != NULL) + { + // Determine the real flags for (attributes) + if(ha->dwFileFlags2 == MPQ_FILE_EXISTS) + ha->dwFileFlags2 = GetDefaultSpecialFileFlags(cbAttrFile, ha->pHeader->wFormatVersion); + + // Create the attributes file in the MPQ + nError = SFileAddFile_Init(ha, ATTRIBUTES_NAME, + 0, + cbAttrFile, + LANG_NEUTRAL, + ha->dwFileFlags2 | MPQ_FILE_REPLACEEXISTING, + &hf); + + // Write the attributes file raw data to it + if(nError == ERROR_SUCCESS) + { + // Write the content of the attributes file to the MPQ + nError = SFileAddFile_Write(hf, pbAttrFile, cbAttrFile, MPQ_COMPRESSION_ZLIB); + SFileAddFile_Finish(hf); + } + + // Clear the number of reserved files + ha->dwFlags &= ~(MPQ_FLAG_ATTRIBUTES_NEW | MPQ_FLAG_ATTRIBUTES_NONE); + ha->dwReservedFiles--; + + // Free the attributes buffer + STORM_FREE(pbAttrFile); + } + else + { + // If the (attributes) file would be empty, its OK + nError = (cbAttrFile == 0) ? ERROR_SUCCESS : ERROR_NOT_ENOUGH_MEMORY; + } + } + + return nError; +} + +//----------------------------------------------------------------------------- +// Public functions + +DWORD WINAPI SFileGetAttributes(HANDLE hMpq) +{ + TMPQArchive * ha = (TMPQArchive *)hMpq; + + // Verify the parameters + if(!IsValidMpqHandle(hMpq)) + { + SetLastError(ERROR_INVALID_PARAMETER); + return SFILE_INVALID_ATTRIBUTES; + } + + return ha->dwAttrFlags; +} + +bool WINAPI SFileSetAttributes(HANDLE hMpq, DWORD dwFlags) +{ + TMPQArchive * ha = (TMPQArchive *)hMpq; + + // Verify the parameters + if(!IsValidMpqHandle(hMpq)) + { + SetLastError(ERROR_INVALID_PARAMETER); + return false; + } + + // Not allowed when the archive is read-only + if(ha->dwFlags & MPQ_FLAG_READ_ONLY) + { + SetLastError(ERROR_ACCESS_DENIED); + return false; + } + + // Set the attributes + InvalidateInternalFiles(ha); + ha->dwAttrFlags = (dwFlags & MPQ_ATTRIBUTE_ALL); + return true; +} + +bool WINAPI SFileUpdateFileAttributes(HANDLE hMpq, const char * szFileName) +{ + hash_state md5_state; + TMPQArchive * ha = (TMPQArchive *)hMpq; + TMPQFile * hf; + BYTE Buffer[0x1000]; + HANDLE hFile = NULL; + DWORD dwTotalBytes = 0; + DWORD dwBytesRead; + DWORD dwCrc32; + + // Verify the parameters + if(!IsValidMpqHandle(ha)) + { + SetLastError(ERROR_INVALID_PARAMETER); + return false; + } + + // Not allowed when the archive is read-only + if(ha->dwFlags & MPQ_FLAG_READ_ONLY) + { + SetLastError(ERROR_ACCESS_DENIED); + return false; + } + + // Attempt to open the file + if(!SFileOpenFileEx(hMpq, szFileName, SFILE_OPEN_BASE_FILE, &hFile)) + return false; + + // Get the file size + hf = (TMPQFile *)hFile; + dwTotalBytes = hf->pFileEntry->dwFileSize; + + // Initialize the CRC32 and MD5 contexts + md5_init(&md5_state); + dwCrc32 = crc32(0, Z_NULL, 0); + + // Go through entire file and calculate both CRC32 and MD5 + while(dwTotalBytes != 0) + { + // Read data from file + SFileReadFile(hFile, Buffer, sizeof(Buffer), &dwBytesRead, NULL); + if(dwBytesRead == 0) + break; + + // Update CRC32 and MD5 + dwCrc32 = crc32(dwCrc32, Buffer, dwBytesRead); + md5_process(&md5_state, Buffer, dwBytesRead); + + // Decrement the total size + dwTotalBytes -= dwBytesRead; + } + + // Update both CRC32 and MD5 + hf->pFileEntry->dwCrc32 = dwCrc32; + md5_done(&md5_state, hf->pFileEntry->md5); + + // Remember that we need to save the MPQ tables + InvalidateInternalFiles(ha); + SFileCloseFile(hFile); + return true; +} diff --git a/src/SFileCompactArchive.cpp b/src/SFileCompactArchive.cpp index 5b6cbe3..57c8839 100644 --- a/src/SFileCompactArchive.cpp +++ b/src/SFileCompactArchive.cpp @@ -1,654 +1,654 @@ -/*****************************************************************************/ -/* SFileCompactArchive.cpp Copyright (c) Ladislav Zezula 2003 */ -/*---------------------------------------------------------------------------*/ -/* Archive compacting function */ -/*---------------------------------------------------------------------------*/ -/* Date Ver Who Comment */ -/* -------- ---- --- ------- */ -/* 14.04.03 1.00 Lad Splitted from SFileCreateArchiveEx.cpp */ -/* 19.11.03 1.01 Dan Big endian handling */ -/* 21.04.13 1.02 Dea Compact callback now part of TMPQArchive */ -/*****************************************************************************/ - -#define __STORMLIB_SELF__ -#include "StormLib.h" -#include "StormCommon.h" - -/*****************************************************************************/ -/* Local functions */ -/*****************************************************************************/ - -static int CheckIfAllFilesKnown(TMPQArchive * ha) -{ - TFileEntry * pFileTableEnd = ha->pFileTable + ha->dwFileTableSize; - TFileEntry * pFileEntry; - DWORD dwBlockIndex = 0; - int nError = ERROR_SUCCESS; - - // Verify the file table - if(nError == ERROR_SUCCESS) - { - for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++, dwBlockIndex++) - { - // If there is an existing entry in the file table, check its name - if(pFileEntry->dwFlags & MPQ_FILE_EXISTS) - { - // The name must be valid and must not be a pseudo-name - if(pFileEntry->szFileName == NULL || IsPseudoFileName(pFileEntry->szFileName, NULL)) - { - nError = ERROR_UNKNOWN_FILE_NAMES; - break; - } - } - } - } - - return nError; -} - -static int CheckIfAllKeysKnown(TMPQArchive * ha, const char * szListFile, LPDWORD pFileKeys) -{ - TFileEntry * pFileTableEnd = ha->pFileTable + ha->dwFileTableSize; - TFileEntry * pFileEntry; - DWORD dwBlockIndex = 0; - int nError = ERROR_SUCCESS; - - // Add the listfile to the MPQ - if(szListFile != NULL) - { - // Notify the user - if(ha->pfnCompactCB != NULL) - ha->pfnCompactCB(ha->pvCompactUserData, CCB_CHECKING_FILES, ha->CompactBytesProcessed, ha->CompactTotalBytes); - - nError = SFileAddListFile((HANDLE)ha, szListFile); - } - - // Verify the file table - if(nError == ERROR_SUCCESS) - { - for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++, dwBlockIndex++) - { - // If the file exists and it's encrypted - if(pFileEntry->dwFlags & MPQ_FILE_EXISTS) - { - // If we know the name, we decrypt the file key from the file name - if(pFileEntry->szFileName != NULL && !IsPseudoFileName(pFileEntry->szFileName, NULL)) - { - // Give the key to the caller - pFileKeys[dwBlockIndex] = DecryptFileKey(pFileEntry->szFileName, - pFileEntry->ByteOffset, - pFileEntry->dwFileSize, - pFileEntry->dwFlags); - continue; - } - - // We don't know the encryption key of this file, - // thus we cannot compact the file - nError = ERROR_UNKNOWN_FILE_NAMES; - break; - } - } - } - - return nError; -} - -static int CopyNonMpqData( - TMPQArchive * ha, - TFileStream * pSrcStream, - TFileStream * pTrgStream, - ULONGLONG & ByteOffset, - ULONGLONG & ByteCount) -{ - ULONGLONG DataSize = ByteCount; - DWORD dwToRead; - char DataBuffer[0x1000]; - int nError = ERROR_SUCCESS; - - // Copy the data - while(DataSize > 0) - { - // Get the proper size of data - dwToRead = sizeof(DataBuffer); - if(DataSize < dwToRead) - dwToRead = (DWORD)DataSize; - - // Read from the source stream - if(!FileStream_Read(pSrcStream, &ByteOffset, DataBuffer, dwToRead)) - { - nError = GetLastError(); - break; - } - - // Write to the target stream - if(!FileStream_Write(pTrgStream, NULL, DataBuffer, dwToRead)) - { - nError = GetLastError(); - break; - } - - // Update the progress - if(ha->pfnCompactCB != NULL) - { - ha->CompactBytesProcessed += dwToRead; - ha->pfnCompactCB(ha->pvCompactUserData, CCB_COPYING_NON_MPQ_DATA, ha->CompactBytesProcessed, ha->CompactTotalBytes); - } - - // Decrement the number of data to be copied - ByteOffset += dwToRead; - DataSize -= dwToRead; - } - - return nError; -} - -// Copies all file sectors into another archive. -static int CopyMpqFileSectors( - TMPQArchive * ha, - TMPQFile * hf, - TFileStream * pNewStream, - ULONGLONG MpqFilePos) // MPQ file position in the new archive -{ - TFileEntry * pFileEntry = hf->pFileEntry; - ULONGLONG RawFilePos; // Used for calculating sector offset in the old MPQ archive - DWORD dwBytesToCopy = pFileEntry->dwCmpSize; - DWORD dwPatchSize = 0; // Size of patch header - DWORD dwFileKey1 = 0; // File key used for decryption - DWORD dwFileKey2 = 0; // File key used for encryption - DWORD dwCmpSize = 0; // Compressed file size, including patch header - int nError = ERROR_SUCCESS; - - // Resolve decryption keys. Note that the file key given - // in the TMPQFile structure also includes the key adjustment - if(nError == ERROR_SUCCESS && (pFileEntry->dwFlags & MPQ_FILE_ENCRYPTED)) - { - dwFileKey2 = dwFileKey1 = hf->dwFileKey; - if(pFileEntry->dwFlags & MPQ_FILE_FIX_KEY) - { - dwFileKey2 = (dwFileKey1 ^ pFileEntry->dwFileSize) - (DWORD)pFileEntry->ByteOffset; - dwFileKey2 = (dwFileKey2 + (DWORD)MpqFilePos) ^ pFileEntry->dwFileSize; - } - } - - // If we have to save patch header, do it - if(nError == ERROR_SUCCESS && hf->pPatchInfo != NULL) - { - BSWAP_ARRAY32_UNSIGNED(hf->pPatchInfo, sizeof(DWORD) * 3); - if(!FileStream_Write(pNewStream, NULL, hf->pPatchInfo, hf->pPatchInfo->dwLength)) - nError = GetLastError(); - - // Save the size of the patch info - dwPatchSize = hf->pPatchInfo->dwLength; - } - - // If we have to save sector offset table, do it. - if(nError == ERROR_SUCCESS && hf->SectorOffsets != NULL) - { - DWORD * SectorOffsetsCopy = STORM_ALLOC(DWORD, hf->SectorOffsets[0] / sizeof(DWORD)); - DWORD dwSectorOffsLen = hf->SectorOffsets[0]; - - assert((pFileEntry->dwFlags & MPQ_FILE_SINGLE_UNIT) == 0); - assert(pFileEntry->dwFlags & MPQ_FILE_COMPRESS_MASK); - - if(SectorOffsetsCopy == NULL) - nError = ERROR_NOT_ENOUGH_MEMORY; - - // Encrypt the secondary sector offset table and write it to the target file - if(nError == ERROR_SUCCESS) - { - memcpy(SectorOffsetsCopy, hf->SectorOffsets, dwSectorOffsLen); - if(pFileEntry->dwFlags & MPQ_FILE_ENCRYPTED) - EncryptMpqBlock(SectorOffsetsCopy, dwSectorOffsLen, dwFileKey2 - 1); - - BSWAP_ARRAY32_UNSIGNED(SectorOffsetsCopy, dwSectorOffsLen); - if(!FileStream_Write(pNewStream, NULL, SectorOffsetsCopy, dwSectorOffsLen)) - nError = GetLastError(); - - dwBytesToCopy -= dwSectorOffsLen; - dwCmpSize += dwSectorOffsLen; - } - - // Update compact progress - if(ha->pfnCompactCB != NULL) - { - ha->CompactBytesProcessed += dwSectorOffsLen; - ha->pfnCompactCB(ha->pvCompactUserData, CCB_COMPACTING_FILES, ha->CompactBytesProcessed, ha->CompactTotalBytes); - } - - STORM_FREE(SectorOffsetsCopy); - } - - // Now we have to copy all file sectors. We do it without - // recompression, because recompression is not necessary in this case - if(nError == ERROR_SUCCESS) - { - for(DWORD dwSector = 0; dwSector < hf->dwSectorCount; dwSector++) - { - DWORD dwRawDataInSector = hf->dwSectorSize; - DWORD dwRawByteOffset = dwSector * hf->dwSectorSize; - - // 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]; - } - - // Last sector: If there is not enough bytes remaining in the file, cut the raw size - if(dwRawDataInSector > dwBytesToCopy) - dwRawDataInSector = dwBytesToCopy; - - // 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)) - { - nError = 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. - if((pFileEntry->dwFlags & MPQ_FILE_ENCRYPTED) && dwFileKey1 != dwFileKey2) - { - BSWAP_ARRAY32_UNSIGNED(hf->pbFileSector, dwRawDataInSector); - DecryptMpqBlock(hf->pbFileSector, dwRawDataInSector, dwFileKey1 + dwSector); - EncryptMpqBlock(hf->pbFileSector, dwRawDataInSector, dwFileKey2 + dwSector); - BSWAP_ARRAY32_UNSIGNED(hf->pbFileSector, dwRawDataInSector); - } - - // Now write the sector back to the file - if(!FileStream_Write(pNewStream, NULL, hf->pbFileSector, dwRawDataInSector)) - { - nError = GetLastError(); - break; - } - - // Update compact progress - if(ha->pfnCompactCB != NULL) - { - ha->CompactBytesProcessed += dwRawDataInSector; - ha->pfnCompactCB(ha->pvCompactUserData, CCB_COMPACTING_FILES, ha->CompactBytesProcessed, ha->CompactTotalBytes); - } - - // Adjust byte counts - dwBytesToCopy -= dwRawDataInSector; - dwCmpSize += dwRawDataInSector; - } - } - - // Copy the sector CRCs, if any - // Sector CRCs are always compressed (not imploded) and unencrypted - if(nError == ERROR_SUCCESS && hf->SectorOffsets != NULL && hf->SectorChksums != NULL) - { - DWORD dwCrcLength; - - dwCrcLength = hf->SectorOffsets[hf->dwSectorCount + 1] - hf->SectorOffsets[hf->dwSectorCount]; - if(dwCrcLength != 0) - { - if(!FileStream_Read(ha->pStream, NULL, hf->SectorChksums, dwCrcLength)) - nError = GetLastError(); - - if(!FileStream_Write(pNewStream, NULL, hf->SectorChksums, dwCrcLength)) - nError = GetLastError(); - - // Update compact progress - if(ha->pfnCompactCB != NULL) - { - ha->CompactBytesProcessed += dwCrcLength; - ha->pfnCompactCB(ha->pvCompactUserData, CCB_COMPACTING_FILES, ha->CompactBytesProcessed, ha->CompactTotalBytes); - } - - // Size of the CRC block is also included in the compressed file size - dwBytesToCopy -= dwCrcLength; - dwCmpSize += dwCrcLength; - } - } - - // There might be extra data beyond sector checksum table - // Sometimes, these data are even part of sector offset table - // Examples: - // 2012 - WoW\15354\locale-enGB.MPQ:DBFilesClient\SpellLevels.dbc - // 2012 - WoW\15354\locale-enGB.MPQ:Interface\AddOns\Blizzard_AuctionUI\Blizzard_AuctionUI.xml - if(nError == ERROR_SUCCESS && dwBytesToCopy != 0) - { - LPBYTE pbExtraData; - - // Allocate space for the extra data - pbExtraData = STORM_ALLOC(BYTE, dwBytesToCopy); - if(pbExtraData != NULL) - { - if(!FileStream_Read(ha->pStream, NULL, pbExtraData, dwBytesToCopy)) - nError = GetLastError(); - - if(!FileStream_Write(pNewStream, NULL, pbExtraData, dwBytesToCopy)) - nError = GetLastError(); - - // Include these extra data in the compressed size - dwCmpSize += dwBytesToCopy; - STORM_FREE(pbExtraData); - } - else - nError = ERROR_NOT_ENOUGH_MEMORY; - } - - // Write the MD5's of the raw file data, if needed - if(nError == ERROR_SUCCESS && ha->pHeader->dwRawChunkSize != 0) - { - nError = WriteMpqDataMD5(pNewStream, - ha->MpqPos + MpqFilePos, - pFileEntry->dwCmpSize, - ha->pHeader->dwRawChunkSize); - } - - // Verify the number of bytes written - if(nError == ERROR_SUCCESS) - { - // At this point, number of bytes written should be exactly - // the same like the compressed file size. If it isn't, - // there's something wrong (an unknown archive version, MPQ malformation, ...) - // - // Note: Diablo savegames have very weird layout, and the file "hero" - // seems to have improper compressed size. Instead of real compressed size, - // the "dwCmpSize" member of the block table entry contains - // uncompressed size of file data + size of the sector table. - // If we compact the archive, Diablo will refuse to load the game - // - // Note: Some patch files in WOW patches don't count the patch header - // into compressed size - // - - if(!(dwCmpSize <= pFileEntry->dwCmpSize && pFileEntry->dwCmpSize <= dwCmpSize + dwPatchSize)) - { - nError = ERROR_FILE_CORRUPT; - assert(false); - } - } - - return nError; -} - -static int CopyMpqFiles(TMPQArchive * ha, LPDWORD pFileKeys, TFileStream * pNewStream) -{ - TFileEntry * pFileTableEnd = ha->pFileTable + ha->dwFileTableSize; - TFileEntry * pFileEntry; - TMPQFile * hf = NULL; - ULONGLONG MpqFilePos; - int nError = ERROR_SUCCESS; - - // Walk through all files and write them to the destination MPQ archive - for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++) - { - // Copy all the file sectors - // Only do that when the file has nonzero size - if((pFileEntry->dwFlags & MPQ_FILE_EXISTS)) - { - // Query the position where the destination file will be - FileStream_GetPos(pNewStream, &MpqFilePos); - MpqFilePos = MpqFilePos - ha->MpqPos; - - // Perform file copy ONLY if the file has nonzero size - if(pFileEntry->dwFileSize != 0) - { - // Allocate structure for the MPQ file - hf = CreateFileHandle(ha, pFileEntry); - if(hf == NULL) - return ERROR_NOT_ENOUGH_MEMORY; - - // Set the file decryption key - hf->dwFileKey = pFileKeys[pFileEntry - ha->pFileTable]; - - // If the file is a patch file, load the patch header - if(pFileEntry->dwFlags & MPQ_FILE_PATCH_FILE) - { - nError = AllocatePatchInfo(hf, true); - if(nError != ERROR_SUCCESS) - break; - } - - // Allocate buffers for file sector and sector offset table - nError = AllocateSectorBuffer(hf); - if(nError != ERROR_SUCCESS) - break; - - // Also allocate sector offset table and sector checksum table - nError = AllocateSectorOffsets(hf, true); - if(nError != ERROR_SUCCESS) - break; - - // Also load sector checksums, if any - if(pFileEntry->dwFlags & MPQ_FILE_SECTOR_CRC) - { - nError = AllocateSectorChecksums(hf, false); - if(nError != ERROR_SUCCESS) - break; - } - - // Copy all file sectors - nError = CopyMpqFileSectors(ha, hf, pNewStream, MpqFilePos); - if(nError != ERROR_SUCCESS) - break; - - // Free buffers. This also sets "hf" to NULL. - FreeFileHandle(hf); - } - - // Note: DO NOT update the compressed size in the file entry, no matter how bad it is. - pFileEntry->ByteOffset = MpqFilePos; - } - } - - // Cleanup and exit - if(hf != NULL) - FreeFileHandle(hf); - return nError; -} - -/*****************************************************************************/ -/* Public functions */ -/*****************************************************************************/ - -//----------------------------------------------------------------------------- -// Changing hash table size - -DWORD WINAPI SFileGetMaxFileCount(HANDLE hMpq) -{ - TMPQArchive * ha = (TMPQArchive *)hMpq; - - return ha->dwMaxFileCount; -} - -bool WINAPI SFileSetMaxFileCount(HANDLE hMpq, DWORD dwMaxFileCount) -{ - TMPQArchive * ha = (TMPQArchive *)hMpq; - DWORD dwNewHashTableSize = 0; - int nError = ERROR_SUCCESS; - - // Test the valid parameters - if(!IsValidMpqHandle(hMpq)) - nError = ERROR_INVALID_HANDLE; - if(ha->dwFlags & MPQ_FLAG_READ_ONLY) - nError = ERROR_ACCESS_DENIED; - if(dwMaxFileCount < ha->dwFileTableSize) - nError = ERROR_DISK_FULL; - - // ALL file names must be known in order to be able to rebuild hash table - if(nError == ERROR_SUCCESS && ha->pHashTable != NULL) - { - nError = CheckIfAllFilesKnown(ha); - if(nError == ERROR_SUCCESS) - { - // Calculate the hash table size for the new file limit - dwNewHashTableSize = GetHashTableSizeForFileCount(dwMaxFileCount); - - // Rebuild both file tables - nError = RebuildFileTable(ha, dwNewHashTableSize); - } - } - - // We always have to rebuild the (attributes) file due to file table change - if(nError == ERROR_SUCCESS) - { - // Invalidate (listfile) and (attributes) - InvalidateInternalFiles(ha); - - // Rebuild the HET table, if we have any - if(ha->pHetTable != NULL) - nError = RebuildHetTable(ha); - } - - // Return the error - if(nError != ERROR_SUCCESS) - SetLastError(nError); - return (nError == ERROR_SUCCESS); -} - -//----------------------------------------------------------------------------- -// Archive compacting - -bool WINAPI SFileSetCompactCallback(HANDLE hMpq, SFILE_COMPACT_CALLBACK pfnCompactCB, void * pvUserData) -{ - TMPQArchive * ha = (TMPQArchive *) hMpq; - - if (!IsValidMpqHandle(hMpq)) - { - SetLastError(ERROR_INVALID_HANDLE); - return false; - } - - ha->pfnCompactCB = pfnCompactCB; - ha->pvCompactUserData = pvUserData; - return true; -} - -bool WINAPI SFileCompactArchive(HANDLE hMpq, const char * szListFile, bool /* bReserved */) -{ - TFileStream * pTempStream = NULL; - TMPQArchive * ha = (TMPQArchive *)hMpq; - ULONGLONG ByteOffset; - ULONGLONG ByteCount; - LPDWORD pFileKeys = NULL; - TCHAR szTempFile[MAX_PATH+1] = _T(""); - int nError = ERROR_SUCCESS; - - // Test the valid parameters - if(!IsValidMpqHandle(hMpq)) - nError = ERROR_INVALID_HANDLE; - if(ha->dwFlags & MPQ_FLAG_READ_ONLY) - nError = ERROR_ACCESS_DENIED; - - // If the MPQ is changed at this moment, we have to flush the archive - if(nError == ERROR_SUCCESS && (ha->dwFlags & MPQ_FLAG_CHANGED)) - { - SFileFlushArchive(hMpq); - } - - // Create the table with file keys - if(nError == ERROR_SUCCESS) - { - if((pFileKeys = STORM_ALLOC(DWORD, ha->dwFileTableSize)) != NULL) - memset(pFileKeys, 0, sizeof(DWORD) * ha->dwFileTableSize); - else - nError = ERROR_NOT_ENOUGH_MEMORY; - } - - // First of all, we have to check of we are able to decrypt all files. - // If not, sorry, but the archive cannot be compacted. - if(nError == ERROR_SUCCESS) - { - // Initialize the progress variables for compact callback - FileStream_GetSize(ha->pStream, &(ha->CompactTotalBytes)); - ha->CompactBytesProcessed = 0; - nError = CheckIfAllKeysKnown(ha, szListFile, pFileKeys); - } - - // Get the temporary file name and create it - if(nError == ERROR_SUCCESS) - { - // Create temporary file name. Prevent buffer overflow - StringCopyT(szTempFile, FileStream_GetFileName(ha->pStream), MAX_PATH); - StringCatT(szTempFile, _T(".tmp"), MAX_PATH); - - // Create temporary file - pTempStream = FileStream_CreateFile(szTempFile, STREAM_PROVIDER_FLAT | BASE_PROVIDER_FILE); - if(pTempStream == NULL) - nError = GetLastError(); - } - - // Write the data before MPQ user data (if any) - if(nError == ERROR_SUCCESS && ha->UserDataPos != 0) - { - // Inform the application about the progress - if(ha->pfnCompactCB != NULL) - ha->pfnCompactCB(ha->pvCompactUserData, CCB_COPYING_NON_MPQ_DATA, ha->CompactBytesProcessed, ha->CompactTotalBytes); - - ByteOffset = 0; - ByteCount = ha->UserDataPos; - nError = CopyNonMpqData(ha, ha->pStream, pTempStream, ByteOffset, ByteCount); - } - - // Write the MPQ user data (if any) - if(nError == ERROR_SUCCESS && ha->MpqPos > ha->UserDataPos) - { - // At this point, we assume that the user data size is equal - // to pUserData->dwHeaderOffs. - // If this assumption doesn't work, then we have an unknown version of MPQ - ByteOffset = ha->UserDataPos; - ByteCount = ha->MpqPos - ha->UserDataPos; - - assert(ha->pUserData != NULL); - assert(ha->pUserData->dwHeaderOffs == ByteCount); - nError = CopyNonMpqData(ha, ha->pStream, pTempStream, ByteOffset, ByteCount); - } - - // Write the MPQ header - if(nError == ERROR_SUCCESS) - { - TMPQHeader SaveMpqHeader; - - // Write the MPQ header to the file - memcpy(&SaveMpqHeader, ha->pHeader, ha->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(pTempStream, NULL, &SaveMpqHeader, ha->pHeader->dwHeaderSize)) - nError = GetLastError(); - - // Update the progress - ha->CompactBytesProcessed += ha->pHeader->dwHeaderSize; - } - - // Now copy all files - if(nError == ERROR_SUCCESS) - nError = CopyMpqFiles(ha, pFileKeys, pTempStream); - - // If succeeded, switch the streams - if(nError == ERROR_SUCCESS) - { - ha->dwFlags |= MPQ_FLAG_CHANGED; - if(FileStream_Replace(ha->pStream, pTempStream)) - pTempStream = NULL; - else - nError = ERROR_CAN_NOT_COMPLETE; - } - - // Final user notification - if(nError == ERROR_SUCCESS && ha->pfnCompactCB != NULL) - { - ha->CompactBytesProcessed += (ha->pHeader->dwHashTableSize * sizeof(TMPQHash)); - ha->CompactBytesProcessed += (ha->dwFileTableSize * sizeof(TMPQBlock)); - ha->pfnCompactCB(ha->pvCompactUserData, CCB_CLOSING_ARCHIVE, ha->CompactBytesProcessed, ha->CompactTotalBytes); - } - - // Cleanup and return - if(pTempStream != NULL) - FileStream_Close(pTempStream); - if(pFileKeys != NULL) - STORM_FREE(pFileKeys); - if(nError != ERROR_SUCCESS) - SetLastError(nError); - return (nError == ERROR_SUCCESS); -} +/*****************************************************************************/ +/* SFileCompactArchive.cpp Copyright (c) Ladislav Zezula 2003 */ +/*---------------------------------------------------------------------------*/ +/* Archive compacting function */ +/*---------------------------------------------------------------------------*/ +/* Date Ver Who Comment */ +/* -------- ---- --- ------- */ +/* 14.04.03 1.00 Lad Splitted from SFileCreateArchiveEx.cpp */ +/* 19.11.03 1.01 Dan Big endian handling */ +/* 21.04.13 1.02 Dea Compact callback now part of TMPQArchive */ +/*****************************************************************************/ + +#define __STORMLIB_SELF__ +#include "StormLib.h" +#include "StormCommon.h" + +/*****************************************************************************/ +/* Local functions */ +/*****************************************************************************/ + +static int CheckIfAllFilesKnown(TMPQArchive * ha) +{ + TFileEntry * pFileTableEnd = ha->pFileTable + ha->dwFileTableSize; + TFileEntry * pFileEntry; + DWORD dwBlockIndex = 0; + int nError = ERROR_SUCCESS; + + // Verify the file table + if(nError == ERROR_SUCCESS) + { + for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++, dwBlockIndex++) + { + // If there is an existing entry in the file table, check its name + if(pFileEntry->dwFlags & MPQ_FILE_EXISTS) + { + // The name must be valid and must not be a pseudo-name + if(pFileEntry->szFileName == NULL || IsPseudoFileName(pFileEntry->szFileName, NULL)) + { + nError = ERROR_UNKNOWN_FILE_NAMES; + break; + } + } + } + } + + return nError; +} + +static int CheckIfAllKeysKnown(TMPQArchive * ha, const char * szListFile, LPDWORD pFileKeys) +{ + TFileEntry * pFileTableEnd = ha->pFileTable + ha->dwFileTableSize; + TFileEntry * pFileEntry; + DWORD dwBlockIndex = 0; + int nError = ERROR_SUCCESS; + + // Add the listfile to the MPQ + if(szListFile != NULL) + { + // Notify the user + if(ha->pfnCompactCB != NULL) + ha->pfnCompactCB(ha->pvCompactUserData, CCB_CHECKING_FILES, ha->CompactBytesProcessed, ha->CompactTotalBytes); + + nError = SFileAddListFile((HANDLE)ha, szListFile); + } + + // Verify the file table + if(nError == ERROR_SUCCESS) + { + for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++, dwBlockIndex++) + { + // If the file exists and it's encrypted + if(pFileEntry->dwFlags & MPQ_FILE_EXISTS) + { + // If we know the name, we decrypt the file key from the file name + if(pFileEntry->szFileName != NULL && !IsPseudoFileName(pFileEntry->szFileName, NULL)) + { + // Give the key to the caller + pFileKeys[dwBlockIndex] = DecryptFileKey(pFileEntry->szFileName, + pFileEntry->ByteOffset, + pFileEntry->dwFileSize, + pFileEntry->dwFlags); + continue; + } + + // We don't know the encryption key of this file, + // thus we cannot compact the file + nError = ERROR_UNKNOWN_FILE_NAMES; + break; + } + } + } + + return nError; +} + +static int CopyNonMpqData( + TMPQArchive * ha, + TFileStream * pSrcStream, + TFileStream * pTrgStream, + ULONGLONG & ByteOffset, + ULONGLONG & ByteCount) +{ + ULONGLONG DataSize = ByteCount; + DWORD dwToRead; + char DataBuffer[0x1000]; + int nError = ERROR_SUCCESS; + + // Copy the data + while(DataSize > 0) + { + // Get the proper size of data + dwToRead = sizeof(DataBuffer); + if(DataSize < dwToRead) + dwToRead = (DWORD)DataSize; + + // Read from the source stream + if(!FileStream_Read(pSrcStream, &ByteOffset, DataBuffer, dwToRead)) + { + nError = GetLastError(); + break; + } + + // Write to the target stream + if(!FileStream_Write(pTrgStream, NULL, DataBuffer, dwToRead)) + { + nError = GetLastError(); + break; + } + + // Update the progress + if(ha->pfnCompactCB != NULL) + { + ha->CompactBytesProcessed += dwToRead; + ha->pfnCompactCB(ha->pvCompactUserData, CCB_COPYING_NON_MPQ_DATA, ha->CompactBytesProcessed, ha->CompactTotalBytes); + } + + // Decrement the number of data to be copied + ByteOffset += dwToRead; + DataSize -= dwToRead; + } + + return nError; +} + +// Copies all file sectors into another archive. +static int CopyMpqFileSectors( + TMPQArchive * ha, + TMPQFile * hf, + TFileStream * pNewStream, + ULONGLONG MpqFilePos) // MPQ file position in the new archive +{ + TFileEntry * pFileEntry = hf->pFileEntry; + ULONGLONG RawFilePos; // Used for calculating sector offset in the old MPQ archive + DWORD dwBytesToCopy = pFileEntry->dwCmpSize; + DWORD dwPatchSize = 0; // Size of patch header + DWORD dwFileKey1 = 0; // File key used for decryption + DWORD dwFileKey2 = 0; // File key used for encryption + DWORD dwCmpSize = 0; // Compressed file size, including patch header + int nError = ERROR_SUCCESS; + + // Resolve decryption keys. Note that the file key given + // in the TMPQFile structure also includes the key adjustment + if(nError == ERROR_SUCCESS && (pFileEntry->dwFlags & MPQ_FILE_ENCRYPTED)) + { + dwFileKey2 = dwFileKey1 = hf->dwFileKey; + if(pFileEntry->dwFlags & MPQ_FILE_FIX_KEY) + { + dwFileKey2 = (dwFileKey1 ^ pFileEntry->dwFileSize) - (DWORD)pFileEntry->ByteOffset; + dwFileKey2 = (dwFileKey2 + (DWORD)MpqFilePos) ^ pFileEntry->dwFileSize; + } + } + + // If we have to save patch header, do it + if(nError == ERROR_SUCCESS && hf->pPatchInfo != NULL) + { + BSWAP_ARRAY32_UNSIGNED(hf->pPatchInfo, sizeof(DWORD) * 3); + if(!FileStream_Write(pNewStream, NULL, hf->pPatchInfo, hf->pPatchInfo->dwLength)) + nError = GetLastError(); + + // Save the size of the patch info + dwPatchSize = hf->pPatchInfo->dwLength; + } + + // If we have to save sector offset table, do it. + if(nError == ERROR_SUCCESS && hf->SectorOffsets != NULL) + { + DWORD * SectorOffsetsCopy = STORM_ALLOC(DWORD, hf->SectorOffsets[0] / sizeof(DWORD)); + DWORD dwSectorOffsLen = hf->SectorOffsets[0]; + + assert((pFileEntry->dwFlags & MPQ_FILE_SINGLE_UNIT) == 0); + assert(pFileEntry->dwFlags & MPQ_FILE_COMPRESS_MASK); + + if(SectorOffsetsCopy == NULL) + nError = ERROR_NOT_ENOUGH_MEMORY; + + // Encrypt the secondary sector offset table and write it to the target file + if(nError == ERROR_SUCCESS) + { + memcpy(SectorOffsetsCopy, hf->SectorOffsets, dwSectorOffsLen); + if(pFileEntry->dwFlags & MPQ_FILE_ENCRYPTED) + EncryptMpqBlock(SectorOffsetsCopy, dwSectorOffsLen, dwFileKey2 - 1); + + BSWAP_ARRAY32_UNSIGNED(SectorOffsetsCopy, dwSectorOffsLen); + if(!FileStream_Write(pNewStream, NULL, SectorOffsetsCopy, dwSectorOffsLen)) + nError = GetLastError(); + + dwBytesToCopy -= dwSectorOffsLen; + dwCmpSize += dwSectorOffsLen; + } + + // Update compact progress + if(ha->pfnCompactCB != NULL) + { + ha->CompactBytesProcessed += dwSectorOffsLen; + ha->pfnCompactCB(ha->pvCompactUserData, CCB_COMPACTING_FILES, ha->CompactBytesProcessed, ha->CompactTotalBytes); + } + + STORM_FREE(SectorOffsetsCopy); + } + + // Now we have to copy all file sectors. We do it without + // recompression, because recompression is not necessary in this case + if(nError == ERROR_SUCCESS) + { + for(DWORD dwSector = 0; dwSector < hf->dwSectorCount; dwSector++) + { + DWORD dwRawDataInSector = hf->dwSectorSize; + DWORD dwRawByteOffset = dwSector * hf->dwSectorSize; + + // 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]; + } + + // Last sector: If there is not enough bytes remaining in the file, cut the raw size + if(dwRawDataInSector > dwBytesToCopy) + dwRawDataInSector = dwBytesToCopy; + + // 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)) + { + nError = 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. + if((pFileEntry->dwFlags & MPQ_FILE_ENCRYPTED) && dwFileKey1 != dwFileKey2) + { + BSWAP_ARRAY32_UNSIGNED(hf->pbFileSector, dwRawDataInSector); + DecryptMpqBlock(hf->pbFileSector, dwRawDataInSector, dwFileKey1 + dwSector); + EncryptMpqBlock(hf->pbFileSector, dwRawDataInSector, dwFileKey2 + dwSector); + BSWAP_ARRAY32_UNSIGNED(hf->pbFileSector, dwRawDataInSector); + } + + // Now write the sector back to the file + if(!FileStream_Write(pNewStream, NULL, hf->pbFileSector, dwRawDataInSector)) + { + nError = GetLastError(); + break; + } + + // Update compact progress + if(ha->pfnCompactCB != NULL) + { + ha->CompactBytesProcessed += dwRawDataInSector; + ha->pfnCompactCB(ha->pvCompactUserData, CCB_COMPACTING_FILES, ha->CompactBytesProcessed, ha->CompactTotalBytes); + } + + // Adjust byte counts + dwBytesToCopy -= dwRawDataInSector; + dwCmpSize += dwRawDataInSector; + } + } + + // Copy the sector CRCs, if any + // Sector CRCs are always compressed (not imploded) and unencrypted + if(nError == ERROR_SUCCESS && hf->SectorOffsets != NULL && hf->SectorChksums != NULL) + { + DWORD dwCrcLength; + + dwCrcLength = hf->SectorOffsets[hf->dwSectorCount + 1] - hf->SectorOffsets[hf->dwSectorCount]; + if(dwCrcLength != 0) + { + if(!FileStream_Read(ha->pStream, NULL, hf->SectorChksums, dwCrcLength)) + nError = GetLastError(); + + if(!FileStream_Write(pNewStream, NULL, hf->SectorChksums, dwCrcLength)) + nError = GetLastError(); + + // Update compact progress + if(ha->pfnCompactCB != NULL) + { + ha->CompactBytesProcessed += dwCrcLength; + ha->pfnCompactCB(ha->pvCompactUserData, CCB_COMPACTING_FILES, ha->CompactBytesProcessed, ha->CompactTotalBytes); + } + + // Size of the CRC block is also included in the compressed file size + dwBytesToCopy -= dwCrcLength; + dwCmpSize += dwCrcLength; + } + } + + // There might be extra data beyond sector checksum table + // Sometimes, these data are even part of sector offset table + // Examples: + // 2012 - WoW\15354\locale-enGB.MPQ:DBFilesClient\SpellLevels.dbc + // 2012 - WoW\15354\locale-enGB.MPQ:Interface\AddOns\Blizzard_AuctionUI\Blizzard_AuctionUI.xml + if(nError == ERROR_SUCCESS && dwBytesToCopy != 0) + { + LPBYTE pbExtraData; + + // Allocate space for the extra data + pbExtraData = STORM_ALLOC(BYTE, dwBytesToCopy); + if(pbExtraData != NULL) + { + if(!FileStream_Read(ha->pStream, NULL, pbExtraData, dwBytesToCopy)) + nError = GetLastError(); + + if(!FileStream_Write(pNewStream, NULL, pbExtraData, dwBytesToCopy)) + nError = GetLastError(); + + // Include these extra data in the compressed size + dwCmpSize += dwBytesToCopy; + STORM_FREE(pbExtraData); + } + else + nError = ERROR_NOT_ENOUGH_MEMORY; + } + + // Write the MD5's of the raw file data, if needed + if(nError == ERROR_SUCCESS && ha->pHeader->dwRawChunkSize != 0) + { + nError = WriteMpqDataMD5(pNewStream, + ha->MpqPos + MpqFilePos, + pFileEntry->dwCmpSize, + ha->pHeader->dwRawChunkSize); + } + + // Verify the number of bytes written + if(nError == ERROR_SUCCESS) + { + // At this point, number of bytes written should be exactly + // the same like the compressed file size. If it isn't, + // there's something wrong (an unknown archive version, MPQ malformation, ...) + // + // Note: Diablo savegames have very weird layout, and the file "hero" + // seems to have improper compressed size. Instead of real compressed size, + // the "dwCmpSize" member of the block table entry contains + // uncompressed size of file data + size of the sector table. + // If we compact the archive, Diablo will refuse to load the game + // + // Note: Some patch files in WOW patches don't count the patch header + // into compressed size + // + + if(!(dwCmpSize <= pFileEntry->dwCmpSize && pFileEntry->dwCmpSize <= dwCmpSize + dwPatchSize)) + { + nError = ERROR_FILE_CORRUPT; + assert(false); + } + } + + return nError; +} + +static int CopyMpqFiles(TMPQArchive * ha, LPDWORD pFileKeys, TFileStream * pNewStream) +{ + TFileEntry * pFileTableEnd = ha->pFileTable + ha->dwFileTableSize; + TFileEntry * pFileEntry; + TMPQFile * hf = NULL; + ULONGLONG MpqFilePos; + int nError = ERROR_SUCCESS; + + // Walk through all files and write them to the destination MPQ archive + for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++) + { + // Copy all the file sectors + // Only do that when the file has nonzero size + if((pFileEntry->dwFlags & MPQ_FILE_EXISTS)) + { + // Query the position where the destination file will be + FileStream_GetPos(pNewStream, &MpqFilePos); + MpqFilePos = MpqFilePos - ha->MpqPos; + + // Perform file copy ONLY if the file has nonzero size + if(pFileEntry->dwFileSize != 0) + { + // Allocate structure for the MPQ file + hf = CreateFileHandle(ha, pFileEntry); + if(hf == NULL) + return ERROR_NOT_ENOUGH_MEMORY; + + // Set the file decryption key + hf->dwFileKey = pFileKeys[pFileEntry - ha->pFileTable]; + + // If the file is a patch file, load the patch header + if(pFileEntry->dwFlags & MPQ_FILE_PATCH_FILE) + { + nError = AllocatePatchInfo(hf, true); + if(nError != ERROR_SUCCESS) + break; + } + + // Allocate buffers for file sector and sector offset table + nError = AllocateSectorBuffer(hf); + if(nError != ERROR_SUCCESS) + break; + + // Also allocate sector offset table and sector checksum table + nError = AllocateSectorOffsets(hf, true); + if(nError != ERROR_SUCCESS) + break; + + // Also load sector checksums, if any + if(pFileEntry->dwFlags & MPQ_FILE_SECTOR_CRC) + { + nError = AllocateSectorChecksums(hf, false); + if(nError != ERROR_SUCCESS) + break; + } + + // Copy all file sectors + nError = CopyMpqFileSectors(ha, hf, pNewStream, MpqFilePos); + if(nError != ERROR_SUCCESS) + break; + + // Free buffers. This also sets "hf" to NULL. + FreeFileHandle(hf); + } + + // Note: DO NOT update the compressed size in the file entry, no matter how bad it is. + pFileEntry->ByteOffset = MpqFilePos; + } + } + + // Cleanup and exit + if(hf != NULL) + FreeFileHandle(hf); + return nError; +} + +/*****************************************************************************/ +/* Public functions */ +/*****************************************************************************/ + +//----------------------------------------------------------------------------- +// Changing hash table size + +DWORD WINAPI SFileGetMaxFileCount(HANDLE hMpq) +{ + TMPQArchive * ha = (TMPQArchive *)hMpq; + + return ha->dwMaxFileCount; +} + +bool WINAPI SFileSetMaxFileCount(HANDLE hMpq, DWORD dwMaxFileCount) +{ + TMPQArchive * ha = (TMPQArchive *)hMpq; + DWORD dwNewHashTableSize = 0; + int nError = ERROR_SUCCESS; + + // Test the valid parameters + if(!IsValidMpqHandle(hMpq)) + nError = ERROR_INVALID_HANDLE; + if(ha->dwFlags & MPQ_FLAG_READ_ONLY) + nError = ERROR_ACCESS_DENIED; + if(dwMaxFileCount < ha->dwFileTableSize) + nError = ERROR_DISK_FULL; + + // ALL file names must be known in order to be able to rebuild hash table + if(nError == ERROR_SUCCESS && ha->pHashTable != NULL) + { + nError = CheckIfAllFilesKnown(ha); + if(nError == ERROR_SUCCESS) + { + // Calculate the hash table size for the new file limit + dwNewHashTableSize = GetHashTableSizeForFileCount(dwMaxFileCount); + + // Rebuild both file tables + nError = RebuildFileTable(ha, dwNewHashTableSize); + } + } + + // We always have to rebuild the (attributes) file due to file table change + if(nError == ERROR_SUCCESS) + { + // Invalidate (listfile) and (attributes) + InvalidateInternalFiles(ha); + + // Rebuild the HET table, if we have any + if(ha->pHetTable != NULL) + nError = RebuildHetTable(ha); + } + + // Return the error + if(nError != ERROR_SUCCESS) + SetLastError(nError); + return (nError == ERROR_SUCCESS); +} + +//----------------------------------------------------------------------------- +// Archive compacting + +bool WINAPI SFileSetCompactCallback(HANDLE hMpq, SFILE_COMPACT_CALLBACK pfnCompactCB, void * pvUserData) +{ + TMPQArchive * ha = (TMPQArchive *) hMpq; + + if (!IsValidMpqHandle(hMpq)) + { + SetLastError(ERROR_INVALID_HANDLE); + return false; + } + + ha->pfnCompactCB = pfnCompactCB; + ha->pvCompactUserData = pvUserData; + return true; +} + +bool WINAPI SFileCompactArchive(HANDLE hMpq, const char * szListFile, bool /* bReserved */) +{ + TFileStream * pTempStream = NULL; + TMPQArchive * ha = (TMPQArchive *)hMpq; + ULONGLONG ByteOffset; + ULONGLONG ByteCount; + LPDWORD pFileKeys = NULL; + TCHAR szTempFile[MAX_PATH+1] = _T(""); + int nError = ERROR_SUCCESS; + + // Test the valid parameters + if(!IsValidMpqHandle(hMpq)) + nError = ERROR_INVALID_HANDLE; + if(ha->dwFlags & MPQ_FLAG_READ_ONLY) + nError = ERROR_ACCESS_DENIED; + + // If the MPQ is changed at this moment, we have to flush the archive + if(nError == ERROR_SUCCESS && (ha->dwFlags & MPQ_FLAG_CHANGED)) + { + SFileFlushArchive(hMpq); + } + + // Create the table with file keys + if(nError == ERROR_SUCCESS) + { + if((pFileKeys = STORM_ALLOC(DWORD, ha->dwFileTableSize)) != NULL) + memset(pFileKeys, 0, sizeof(DWORD) * ha->dwFileTableSize); + else + nError = ERROR_NOT_ENOUGH_MEMORY; + } + + // First of all, we have to check of we are able to decrypt all files. + // If not, sorry, but the archive cannot be compacted. + if(nError == ERROR_SUCCESS) + { + // Initialize the progress variables for compact callback + FileStream_GetSize(ha->pStream, &(ha->CompactTotalBytes)); + ha->CompactBytesProcessed = 0; + nError = CheckIfAllKeysKnown(ha, szListFile, pFileKeys); + } + + // Get the temporary file name and create it + if(nError == ERROR_SUCCESS) + { + // Create temporary file name. Prevent buffer overflow + StringCopyT(szTempFile, FileStream_GetFileName(ha->pStream), MAX_PATH); + StringCatT(szTempFile, _T(".tmp"), MAX_PATH); + + // Create temporary file + pTempStream = FileStream_CreateFile(szTempFile, STREAM_PROVIDER_FLAT | BASE_PROVIDER_FILE); + if(pTempStream == NULL) + nError = GetLastError(); + } + + // Write the data before MPQ user data (if any) + if(nError == ERROR_SUCCESS && ha->UserDataPos != 0) + { + // Inform the application about the progress + if(ha->pfnCompactCB != NULL) + ha->pfnCompactCB(ha->pvCompactUserData, CCB_COPYING_NON_MPQ_DATA, ha->CompactBytesProcessed, ha->CompactTotalBytes); + + ByteOffset = 0; + ByteCount = ha->UserDataPos; + nError = CopyNonMpqData(ha, ha->pStream, pTempStream, ByteOffset, ByteCount); + } + + // Write the MPQ user data (if any) + if(nError == ERROR_SUCCESS && ha->MpqPos > ha->UserDataPos) + { + // At this point, we assume that the user data size is equal + // to pUserData->dwHeaderOffs. + // If this assumption doesn't work, then we have an unknown version of MPQ + ByteOffset = ha->UserDataPos; + ByteCount = ha->MpqPos - ha->UserDataPos; + + assert(ha->pUserData != NULL); + assert(ha->pUserData->dwHeaderOffs == ByteCount); + nError = CopyNonMpqData(ha, ha->pStream, pTempStream, ByteOffset, ByteCount); + } + + // Write the MPQ header + if(nError == ERROR_SUCCESS) + { + TMPQHeader SaveMpqHeader; + + // Write the MPQ header to the file + memcpy(&SaveMpqHeader, ha->pHeader, ha->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(pTempStream, NULL, &SaveMpqHeader, ha->pHeader->dwHeaderSize)) + nError = GetLastError(); + + // Update the progress + ha->CompactBytesProcessed += ha->pHeader->dwHeaderSize; + } + + // Now copy all files + if(nError == ERROR_SUCCESS) + nError = CopyMpqFiles(ha, pFileKeys, pTempStream); + + // If succeeded, switch the streams + if(nError == ERROR_SUCCESS) + { + ha->dwFlags |= MPQ_FLAG_CHANGED; + if(FileStream_Replace(ha->pStream, pTempStream)) + pTempStream = NULL; + else + nError = ERROR_CAN_NOT_COMPLETE; + } + + // Final user notification + if(nError == ERROR_SUCCESS && ha->pfnCompactCB != NULL) + { + ha->CompactBytesProcessed += (ha->pHeader->dwHashTableSize * sizeof(TMPQHash)); + ha->CompactBytesProcessed += (ha->dwFileTableSize * sizeof(TMPQBlock)); + ha->pfnCompactCB(ha->pvCompactUserData, CCB_CLOSING_ARCHIVE, ha->CompactBytesProcessed, ha->CompactTotalBytes); + } + + // Cleanup and return + if(pTempStream != NULL) + FileStream_Close(pTempStream); + if(pFileKeys != NULL) + STORM_FREE(pFileKeys); + if(nError != ERROR_SUCCESS) + SetLastError(nError); + return (nError == ERROR_SUCCESS); +} diff --git a/src/SFileFindFile.cpp b/src/SFileFindFile.cpp index 3e6854b..aacba12 100644 --- a/src/SFileFindFile.cpp +++ b/src/SFileFindFile.cpp @@ -1,484 +1,476 @@ -/*****************************************************************************/ -/* SFileFindFile.cpp Copyright (c) Ladislav Zezula 2003 */ -/*---------------------------------------------------------------------------*/ -/* A module for file searching within MPQs */ -/*---------------------------------------------------------------------------*/ -/* Date Ver Who Comment */ -/* -------- ---- --- ------- */ -/* 25.03.03 1.00 Lad The first version of SFileFindFile.cpp */ -/*****************************************************************************/ - -#define __STORMLIB_SELF__ -#include "StormLib.h" -#include "StormCommon.h" - -//----------------------------------------------------------------------------- -// Defines - -#define LISTFILE_CACHE_SIZE 0x1000 - -//----------------------------------------------------------------------------- -// Private structure used for file search (search handle) - -struct TMPQSearch; -typedef int (*MPQSEARCH)(TMPQSearch *, SFILE_FIND_DATA *); - -// Used by searching in MPQ archives -struct TMPQSearch -{ - TMPQArchive * ha; // Handle to MPQ, where the search runs - TFileEntry ** pSearchTable; // Table for files that have been already found - DWORD dwSearchTableItems; // Number of items in the search table - DWORD dwNextIndex; // Next file index to be checked - DWORD dwFlagMask; // For checking flag mask - char szSearchMask[1]; // Search mask (variable length) -}; - -//----------------------------------------------------------------------------- -// Local functions - -static TMPQSearch * IsValidSearchHandle(HANDLE hFind) -{ - TMPQSearch * hs = (TMPQSearch *)hFind; - - if(hs != NULL && IsValidMpqHandle(hs->ha)) - return hs; - - return NULL; -} - -bool CheckWildCard(const char * szString, const char * szWildCard) -{ - const char * szWildCardPtr; - - for(;;) - { - // If there is '?' in the wildcard, we skip one char - while(szWildCard[0] == '?') - { - if(szString[0] == 0) - return false; - - szWildCard++; - szString++; - } - - // Handle '*' - szWildCardPtr = szWildCard; - if(szWildCardPtr[0] != 0) - { - if(szWildCardPtr[0] == '*') - { - szWildCardPtr++; - - if(szWildCardPtr[0] == '*') - continue; - - if(szWildCardPtr[0] == 0) - return true; - - if(AsciiToUpperTable[szWildCardPtr[0]] == AsciiToUpperTable[szString[0]]) - { - if(CheckWildCard(szString, szWildCardPtr)) - return true; - } - } - else - { - if(AsciiToUpperTable[szWildCardPtr[0]] != AsciiToUpperTable[szString[0]]) - return false; - - szWildCard = szWildCardPtr + 1; - } - - if(szString[0] == 0) - return false; - szString++; - } - else - { - return (szString[0] == 0) ? true : false; - } - } -} - -static DWORD GetSearchTableItems(TMPQArchive * ha) -{ - DWORD dwMergeItems = 0; - - // Loop over all patches - while(ha != NULL) - { - // Append the number of files - dwMergeItems += (ha->pHetTable != NULL) ? ha->pHetTable->dwEntryCount - : ha->pHeader->dwBlockTableSize; - // Move to the patched archive - ha = ha->haPatch; - } - - // Return the double size of number of items - return (dwMergeItems | 1); -} - -static bool FileWasFoundBefore( - TMPQArchive * ha, - TMPQSearch * hs, - TFileEntry * pFileEntry) -{ - TFileEntry * pEntry; - char * szRealFileName = pFileEntry->szFileName; - DWORD dwStartIndex; - DWORD dwNameHash; - DWORD dwIndex; - - if(hs->pSearchTable != NULL && szRealFileName != NULL) - { - // If we are in patch MPQ, we check if patch prefix matches - // and then trim the patch prefix - if(ha->pPatchPrefix != NULL) - { - // If the patch prefix doesn't fit, we pretend that the file - // was there before and it will be skipped - if(_strnicmp(szRealFileName, ha->pPatchPrefix->szPatchPrefix, ha->pPatchPrefix->nLength)) - return true; - - szRealFileName += ha->pPatchPrefix->nLength; - } - - // Calculate the hash to the table - dwNameHash = HashString(szRealFileName, MPQ_HASH_NAME_A); - dwStartIndex = dwIndex = (dwNameHash % hs->dwSearchTableItems); - - // The file might have been found before - // only if this is not the first MPQ being searched - if(ha->haBase != NULL) - { - // Enumerate all entries in the search table - for(;;) - { - // Get the file entry at that position - pEntry = hs->pSearchTable[dwIndex]; - if(pEntry == NULL) - break; - - if(pEntry->szFileName != NULL) - { - // Does the name match? - if(!_stricmp(pEntry->szFileName, szRealFileName)) - return true; - } - - // Move to the next entry - dwIndex = (dwIndex + 1) % hs->dwSearchTableItems; - if(dwIndex == dwStartIndex) - break; - } - } - - // Put the entry to the table for later use - hs->pSearchTable[dwIndex] = pFileEntry; - } - return false; -} - -static TFileEntry * FindPatchEntry(TMPQArchive * ha, TFileEntry * pFileEntry) -{ - TFileEntry * pPatchEntry = NULL; - TFileEntry * pTempEntry; - char szFileName[MAX_PATH+1]; - - // Go while there are patches - while(ha->haPatch != NULL) - { - // Move to the patch archive - ha = ha->haPatch; - szFileName[0] = 0; - - // Prepare the prefix for the file name - if(ha->pPatchPrefix != NULL) - StringCopyA(szFileName, ha->pPatchPrefix->szPatchPrefix, MAX_PATH); - StringCatA(szFileName, pFileEntry->szFileName, MAX_PATH); - - // Try to find the file there - pTempEntry = GetFileEntryExact(ha, szFileName, 0, NULL); - if(pTempEntry != NULL) - pPatchEntry = pTempEntry; - } - - // Return the found patch entry - return pPatchEntry; -} - -static bool DoMPQSearch_FileEntry( - TMPQSearch * hs, - SFILE_FIND_DATA * lpFindFileData, - TMPQArchive * ha, - TMPQHash * pHashEntry, - TFileEntry * pFileEntry) -{ - TFileEntry * pPatchEntry; - HANDLE hFile = NULL; - const char * szFileName; - size_t nPrefixLength = (ha->pPatchPrefix != NULL) ? ha->pPatchPrefix->nLength : 0; - DWORD dwBlockIndex; - char szNameBuff[MAX_PATH]; - - // Is it a file but not a patch file? - if((pFileEntry->dwFlags & hs->dwFlagMask) == MPQ_FILE_EXISTS) - { - // Now we have to check if this file was not enumerated before - if(!FileWasFoundBefore(ha, hs, pFileEntry)) - { -// if(pFileEntry != NULL && !_stricmp(pFileEntry->szFileName, "TriggerLibs\\NativeLib.galaxy")) -// DebugBreak(); - - // Find a patch to this file - pPatchEntry = FindPatchEntry(ha, pFileEntry); - if(pPatchEntry == NULL) - pPatchEntry = pFileEntry; - - // Prepare the block index - dwBlockIndex = (DWORD)(pFileEntry - ha->pFileTable); - - // Get the file name. If it's not known, we will create pseudo-name - szFileName = pFileEntry->szFileName; - if(szFileName == NULL) - { - // Open the file by its pseudo-name. - sprintf(szNameBuff, "File%08u.xxx", (unsigned int)dwBlockIndex); - if(SFileOpenFileEx((HANDLE)hs->ha, szNameBuff, SFILE_OPEN_BASE_FILE, &hFile)) - { - SFileGetFileName(hFile, szNameBuff); - szFileName = szNameBuff; - SFileCloseFile(hFile); - } - } - - // If the file name is still NULL, we cannot include the file to search results - if(szFileName != NULL) - { - // Check the file name against the wildcard - if(CheckWildCard(szFileName + nPrefixLength, hs->szSearchMask)) - { - // Fill the found entry. hash entry and block index are taken from the base MPQ - lpFindFileData->dwHashIndex = HASH_ENTRY_FREE; - lpFindFileData->dwBlockIndex = dwBlockIndex; - lpFindFileData->dwFileSize = pPatchEntry->dwFileSize; - lpFindFileData->dwFileFlags = pPatchEntry->dwFlags; - lpFindFileData->dwCompSize = pPatchEntry->dwCmpSize; - lpFindFileData->lcLocale = 0; // pPatchEntry->lcLocale; - - // Fill the filetime - lpFindFileData->dwFileTimeHi = (DWORD)(pPatchEntry->FileTime >> 32); - lpFindFileData->dwFileTimeLo = (DWORD)(pPatchEntry->FileTime); - - // Fill-in the entries from hash table entry, if given - if(pHashEntry != NULL) - { - lpFindFileData->dwHashIndex = (DWORD)(pHashEntry - ha->pHashTable); - lpFindFileData->lcLocale = pHashEntry->lcLocale; - } - - // Fill the file name and plain file name - StringCopyA(lpFindFileData->cFileName, szFileName + nPrefixLength, MAX_PATH-1); - lpFindFileData->szPlainName = (char *)GetPlainFileName(lpFindFileData->cFileName); - return true; - } - } - } - } - - // Either not a valid item or was found before - return false; -} - -static int DoMPQSearch_HashTable(TMPQSearch * hs, SFILE_FIND_DATA * lpFindFileData, TMPQArchive * ha) -{ - TMPQHash * pHashTableEnd = ha->pHashTable + ha->pHeader->dwHashTableSize; - TMPQHash * pHash; - - // Parse the file table - for(pHash = ha->pHashTable + hs->dwNextIndex; pHash < pHashTableEnd; pHash++) - { - // Increment the next index for subsequent search - hs->dwNextIndex++; - - // Does this hash table entry point to a proper block table entry? - if(IsValidHashEntry(ha, pHash)) - { - // Check if this file entry should be included in the search result - if(DoMPQSearch_FileEntry(hs, lpFindFileData, ha, pHash, ha->pFileTable + pHash->dwBlockIndex)) - return ERROR_SUCCESS; - } - } - - // No more files - return ERROR_NO_MORE_FILES; -} - -static int DoMPQSearch_FileTable(TMPQSearch * hs, SFILE_FIND_DATA * lpFindFileData, TMPQArchive * ha) -{ - TFileEntry * pFileTableEnd = ha->pFileTable + ha->dwFileTableSize; - TFileEntry * pFileEntry; - - // Parse the file table - for(pFileEntry = ha->pFileTable + hs->dwNextIndex; pFileEntry < pFileTableEnd; pFileEntry++) - { - // Increment the next index for subsequent search - hs->dwNextIndex++; - - // Check if this file entry should be included in the search result - if(DoMPQSearch_FileEntry(hs, lpFindFileData, ha, NULL, pFileEntry)) - return ERROR_SUCCESS; - } - - // No more files - return ERROR_NO_MORE_FILES; -} - -// Performs one MPQ search -static int DoMPQSearch(TMPQSearch * hs, SFILE_FIND_DATA * lpFindFileData) -{ - TMPQArchive * ha = hs->ha; - int nError; - - // Start searching with base MPQ - while(ha != NULL) - { - // If the archive has hash table, we need to use hash table - // in order to catch hash table index and file locale. - // Note: If multiple hash table entries, point to the same block entry, - // we need, to report them all - nError = (ha->pHashTable != NULL) ? DoMPQSearch_HashTable(hs, lpFindFileData, ha) - : DoMPQSearch_FileTable(hs, lpFindFileData, ha); - if(nError == ERROR_SUCCESS) - return nError; - - // If there is no more patches in the chain, stop it. - // This also keeps hs->ha non-NULL, which is required - // for freeing the handle later - if(ha->haPatch == NULL) - break; - - // Move to the next patch in the patch chain - hs->ha = ha = ha->haPatch; - hs->dwNextIndex = 0; - } - - // No more files found, return error - return ERROR_NO_MORE_FILES; -} - -static void FreeMPQSearch(TMPQSearch *& hs) -{ - if(hs != NULL) - { - if(hs->pSearchTable != NULL) - STORM_FREE(hs->pSearchTable); - STORM_FREE(hs); - hs = NULL; - } -} - -//----------------------------------------------------------------------------- -// Public functions - -HANDLE WINAPI SFileFindFirstFile(HANDLE hMpq, const char * szMask, SFILE_FIND_DATA * lpFindFileData, const char * szListFile) -{ - TMPQArchive * ha = (TMPQArchive *)hMpq; - TMPQSearch * hs = NULL; - size_t nSize = 0; - int nError = ERROR_SUCCESS; - - // Check for the valid parameters - if(!IsValidMpqHandle(hMpq)) - nError = ERROR_INVALID_HANDLE; - if(szMask == NULL || lpFindFileData == NULL) - nError = ERROR_INVALID_PARAMETER; - - // Include the listfile into the MPQ's internal listfile - // Note that if the listfile name is NULL, do nothing because the - // internal listfile is always included. - if(nError == ERROR_SUCCESS && szListFile != NULL && *szListFile != 0) - nError = SFileAddListFile((HANDLE)ha, szListFile); - - // Allocate the structure for MPQ search - if(nError == ERROR_SUCCESS) - { - nSize = sizeof(TMPQSearch) + strlen(szMask) + 1; - if((hs = (TMPQSearch *)STORM_ALLOC(char, nSize)) == NULL) - nError = ERROR_NOT_ENOUGH_MEMORY; - } - - // Perform the first search - if(nError == ERROR_SUCCESS) - { - memset(hs, 0, sizeof(TMPQSearch)); - strcpy(hs->szSearchMask, szMask); - hs->dwFlagMask = MPQ_FILE_EXISTS; - hs->ha = ha; - - // If the archive is patched archive, we have to create a merge table - // to prevent files being repeated - if(ha->haPatch != NULL) - { - hs->dwSearchTableItems = GetSearchTableItems(ha); - hs->pSearchTable = STORM_ALLOC(TFileEntry *, hs->dwSearchTableItems); - hs->dwFlagMask = MPQ_FILE_EXISTS | MPQ_FILE_PATCH_FILE; - if(hs->pSearchTable != NULL) - memset(hs->pSearchTable, 0, hs->dwSearchTableItems * sizeof(TFileEntry *)); - else - nError = ERROR_NOT_ENOUGH_MEMORY; - } - } - - // Perform first item searching - if(nError == ERROR_SUCCESS) - { - nError = DoMPQSearch(hs, lpFindFileData); - } - - // Cleanup - if(nError != ERROR_SUCCESS) - { - FreeMPQSearch(hs); - SetLastError(nError); - } - - // Return the result value - return (HANDLE)hs; -} - -bool WINAPI SFileFindNextFile(HANDLE hFind, SFILE_FIND_DATA * lpFindFileData) -{ - TMPQSearch * hs = IsValidSearchHandle(hFind); - int nError = ERROR_SUCCESS; - - // Check the parameters - if(hs == NULL) - nError = ERROR_INVALID_HANDLE; - if(lpFindFileData == NULL) - nError = ERROR_INVALID_PARAMETER; - - if(nError == ERROR_SUCCESS) - nError = DoMPQSearch(hs, lpFindFileData); - - if(nError != ERROR_SUCCESS) - SetLastError(nError); - return (nError == ERROR_SUCCESS); -} - -bool WINAPI SFileFindClose(HANDLE hFind) -{ - TMPQSearch * hs = IsValidSearchHandle(hFind); - - // Check the parameters - if(hs == NULL) - { - SetLastError(ERROR_INVALID_HANDLE); - return false; - } - - FreeMPQSearch(hs); - return true; -} +/*****************************************************************************/ +/* SFileFindFile.cpp Copyright (c) Ladislav Zezula 2003 */ +/*---------------------------------------------------------------------------*/ +/* A module for file searching within MPQs */ +/*---------------------------------------------------------------------------*/ +/* Date Ver Who Comment */ +/* -------- ---- --- ------- */ +/* 25.03.03 1.00 Lad The first version of SFileFindFile.cpp */ +/*****************************************************************************/ + +#define __STORMLIB_SELF__ +#include "StormLib.h" +#include "StormCommon.h" + +//----------------------------------------------------------------------------- +// Private structure used for file search (search handle) + +// Used by searching in MPQ archives +struct TMPQSearch +{ + TMPQArchive * ha; // Handle to MPQ, where the search runs + TFileEntry ** pSearchTable; // Table for files that have been already found + DWORD dwSearchTableItems; // Number of items in the search table + DWORD dwNextIndex; // Next file index to be checked + DWORD dwFlagMask; // For checking flag mask + char szSearchMask[1]; // Search mask (variable length) +}; + +//----------------------------------------------------------------------------- +// Local functions + +static TMPQSearch * IsValidSearchHandle(HANDLE hFind) +{ + TMPQSearch * hs = (TMPQSearch *)hFind; + + if(hs != NULL && IsValidMpqHandle(hs->ha)) + return hs; + + return NULL; +} + +bool CheckWildCard(const char * szString, const char * szWildCard) +{ + const char * szWildCardPtr; + + for(;;) + { + // If there is '?' in the wildcard, we skip one char + while(szWildCard[0] == '?') + { + if(szString[0] == 0) + return false; + + szWildCard++; + szString++; + } + + // Handle '*' + szWildCardPtr = szWildCard; + if(szWildCardPtr[0] != 0) + { + if(szWildCardPtr[0] == '*') + { + szWildCardPtr++; + + if(szWildCardPtr[0] == '*') + continue; + + if(szWildCardPtr[0] == 0) + return true; + + if(AsciiToUpperTable[szWildCardPtr[0]] == AsciiToUpperTable[szString[0]]) + { + if(CheckWildCard(szString, szWildCardPtr)) + return true; + } + } + else + { + if(AsciiToUpperTable[szWildCardPtr[0]] != AsciiToUpperTable[szString[0]]) + return false; + + szWildCard = szWildCardPtr + 1; + } + + if(szString[0] == 0) + return false; + szString++; + } + else + { + return (szString[0] == 0) ? true : false; + } + } +} + +static DWORD GetSearchTableItems(TMPQArchive * ha) +{ + DWORD dwMergeItems = 0; + + // Loop over all patches + while(ha != NULL) + { + // Append the number of files + dwMergeItems += (ha->pHetTable != NULL) ? ha->pHetTable->dwEntryCount + : ha->pHeader->dwBlockTableSize; + // Move to the patched archive + ha = ha->haPatch; + } + + // Return the double size of number of items + return (dwMergeItems | 1); +} + +static bool FileWasFoundBefore( + TMPQArchive * ha, + TMPQSearch * hs, + TFileEntry * pFileEntry) +{ + TFileEntry * pEntry; + char * szRealFileName = pFileEntry->szFileName; + DWORD dwStartIndex; + DWORD dwNameHash; + DWORD dwIndex; + + if(hs->pSearchTable != NULL && szRealFileName != NULL) + { + // If we are in patch MPQ, we check if patch prefix matches + // and then trim the patch prefix + if(ha->pPatchPrefix != NULL) + { + // If the patch prefix doesn't fit, we pretend that the file + // was there before and it will be skipped + if(_strnicmp(szRealFileName, ha->pPatchPrefix->szPatchPrefix, ha->pPatchPrefix->nLength)) + return true; + + szRealFileName += ha->pPatchPrefix->nLength; + } + + // Calculate the hash to the table + dwNameHash = HashString(szRealFileName, MPQ_HASH_NAME_A); + dwStartIndex = dwIndex = (dwNameHash % hs->dwSearchTableItems); + + // The file might have been found before + // only if this is not the first MPQ being searched + if(ha->haBase != NULL) + { + // Enumerate all entries in the search table + for(;;) + { + // Get the file entry at that position + pEntry = hs->pSearchTable[dwIndex]; + if(pEntry == NULL) + break; + + if(pEntry->szFileName != NULL) + { + // Does the name match? + if(!_stricmp(pEntry->szFileName, szRealFileName)) + return true; + } + + // Move to the next entry + dwIndex = (dwIndex + 1) % hs->dwSearchTableItems; + if(dwIndex == dwStartIndex) + break; + } + } + + // Put the entry to the table for later use + hs->pSearchTable[dwIndex] = pFileEntry; + } + return false; +} + +static TFileEntry * FindPatchEntry(TMPQArchive * ha, TFileEntry * pFileEntry) +{ + TFileEntry * pPatchEntry = NULL; + TFileEntry * pTempEntry; + char szFileName[MAX_PATH+1]; + + // Go while there are patches + while(ha->haPatch != NULL) + { + // Move to the patch archive + ha = ha->haPatch; + szFileName[0] = 0; + + // Prepare the prefix for the file name + if(ha->pPatchPrefix != NULL) + StringCopyA(szFileName, ha->pPatchPrefix->szPatchPrefix, MAX_PATH); + StringCatA(szFileName, pFileEntry->szFileName, MAX_PATH); + + // Try to find the file there + pTempEntry = GetFileEntryExact(ha, szFileName, 0, NULL); + if(pTempEntry != NULL) + pPatchEntry = pTempEntry; + } + + // Return the found patch entry + return pPatchEntry; +} + +static bool DoMPQSearch_FileEntry( + TMPQSearch * hs, + SFILE_FIND_DATA * lpFindFileData, + TMPQArchive * ha, + TMPQHash * pHashEntry, + TFileEntry * pFileEntry) +{ + TFileEntry * pPatchEntry; + HANDLE hFile = NULL; + const char * szFileName; + size_t nPrefixLength = (ha->pPatchPrefix != NULL) ? ha->pPatchPrefix->nLength : 0; + DWORD dwBlockIndex; + char szNameBuff[MAX_PATH]; + + // Is it a file but not a patch file? + if((pFileEntry->dwFlags & hs->dwFlagMask) == MPQ_FILE_EXISTS) + { + // Now we have to check if this file was not enumerated before + if(!FileWasFoundBefore(ha, hs, pFileEntry)) + { +// if(pFileEntry != NULL && !_stricmp(pFileEntry->szFileName, "TriggerLibs\\NativeLib.galaxy")) +// DebugBreak(); + + // Find a patch to this file + pPatchEntry = FindPatchEntry(ha, pFileEntry); + if(pPatchEntry == NULL) + pPatchEntry = pFileEntry; + + // Prepare the block index + dwBlockIndex = (DWORD)(pFileEntry - ha->pFileTable); + + // Get the file name. If it's not known, we will create pseudo-name + szFileName = pFileEntry->szFileName; + if(szFileName == NULL) + { + // Open the file by its pseudo-name. + sprintf(szNameBuff, "File%08u.xxx", (unsigned int)dwBlockIndex); + if(SFileOpenFileEx((HANDLE)hs->ha, szNameBuff, SFILE_OPEN_BASE_FILE, &hFile)) + { + SFileGetFileName(hFile, szNameBuff); + szFileName = szNameBuff; + SFileCloseFile(hFile); + } + } + + // If the file name is still NULL, we cannot include the file to search results + if(szFileName != NULL) + { + // Check the file name against the wildcard + if(CheckWildCard(szFileName + nPrefixLength, hs->szSearchMask)) + { + // Fill the found entry. hash entry and block index are taken from the base MPQ + lpFindFileData->dwHashIndex = HASH_ENTRY_FREE; + lpFindFileData->dwBlockIndex = dwBlockIndex; + lpFindFileData->dwFileSize = pPatchEntry->dwFileSize; + lpFindFileData->dwFileFlags = pPatchEntry->dwFlags; + lpFindFileData->dwCompSize = pPatchEntry->dwCmpSize; + lpFindFileData->lcLocale = 0; // pPatchEntry->lcLocale; + + // Fill the filetime + lpFindFileData->dwFileTimeHi = (DWORD)(pPatchEntry->FileTime >> 32); + lpFindFileData->dwFileTimeLo = (DWORD)(pPatchEntry->FileTime); + + // Fill-in the entries from hash table entry, if given + if(pHashEntry != NULL) + { + lpFindFileData->dwHashIndex = (DWORD)(pHashEntry - ha->pHashTable); + lpFindFileData->lcLocale = pHashEntry->lcLocale; + } + + // Fill the file name and plain file name + StringCopyA(lpFindFileData->cFileName, szFileName + nPrefixLength, MAX_PATH-1); + lpFindFileData->szPlainName = (char *)GetPlainFileName(lpFindFileData->cFileName); + return true; + } + } + } + } + + // Either not a valid item or was found before + return false; +} + +static int DoMPQSearch_HashTable(TMPQSearch * hs, SFILE_FIND_DATA * lpFindFileData, TMPQArchive * ha) +{ + TMPQHash * pHashTableEnd = ha->pHashTable + ha->pHeader->dwHashTableSize; + TMPQHash * pHash; + + // Parse the file table + for(pHash = ha->pHashTable + hs->dwNextIndex; pHash < pHashTableEnd; pHash++) + { + // Increment the next index for subsequent search + hs->dwNextIndex++; + + // Does this hash table entry point to a proper block table entry? + if(IsValidHashEntry(ha, pHash)) + { + // Check if this file entry should be included in the search result + if(DoMPQSearch_FileEntry(hs, lpFindFileData, ha, pHash, ha->pFileTable + pHash->dwBlockIndex)) + return ERROR_SUCCESS; + } + } + + // No more files + return ERROR_NO_MORE_FILES; +} + +static int DoMPQSearch_FileTable(TMPQSearch * hs, SFILE_FIND_DATA * lpFindFileData, TMPQArchive * ha) +{ + TFileEntry * pFileTableEnd = ha->pFileTable + ha->dwFileTableSize; + TFileEntry * pFileEntry; + + // Parse the file table + for(pFileEntry = ha->pFileTable + hs->dwNextIndex; pFileEntry < pFileTableEnd; pFileEntry++) + { + // Increment the next index for subsequent search + hs->dwNextIndex++; + + // Check if this file entry should be included in the search result + if(DoMPQSearch_FileEntry(hs, lpFindFileData, ha, NULL, pFileEntry)) + return ERROR_SUCCESS; + } + + // No more files + return ERROR_NO_MORE_FILES; +} + +// Performs one MPQ search +static int DoMPQSearch(TMPQSearch * hs, SFILE_FIND_DATA * lpFindFileData) +{ + TMPQArchive * ha = hs->ha; + int nError; + + // Start searching with base MPQ + while(ha != NULL) + { + // If the archive has hash table, we need to use hash table + // in order to catch hash table index and file locale. + // Note: If multiple hash table entries, point to the same block entry, + // we need, to report them all + nError = (ha->pHashTable != NULL) ? DoMPQSearch_HashTable(hs, lpFindFileData, ha) + : DoMPQSearch_FileTable(hs, lpFindFileData, ha); + if(nError == ERROR_SUCCESS) + return nError; + + // If there is no more patches in the chain, stop it. + // This also keeps hs->ha non-NULL, which is required + // for freeing the handle later + if(ha->haPatch == NULL) + break; + + // Move to the next patch in the patch chain + hs->ha = ha = ha->haPatch; + hs->dwNextIndex = 0; + } + + // No more files found, return error + return ERROR_NO_MORE_FILES; +} + +static void FreeMPQSearch(TMPQSearch *& hs) +{ + if(hs != NULL) + { + if(hs->pSearchTable != NULL) + STORM_FREE(hs->pSearchTable); + STORM_FREE(hs); + hs = NULL; + } +} + +//----------------------------------------------------------------------------- +// Public functions + +HANDLE WINAPI SFileFindFirstFile(HANDLE hMpq, const char * szMask, SFILE_FIND_DATA * lpFindFileData, const char * szListFile) +{ + TMPQArchive * ha = (TMPQArchive *)hMpq; + TMPQSearch * hs = NULL; + size_t nSize = 0; + int nError = ERROR_SUCCESS; + + // Check for the valid parameters + if(!IsValidMpqHandle(hMpq)) + nError = ERROR_INVALID_HANDLE; + if(szMask == NULL || lpFindFileData == NULL) + nError = ERROR_INVALID_PARAMETER; + + // Include the listfile into the MPQ's internal listfile + // Note that if the listfile name is NULL, do nothing because the + // internal listfile is always included. + if(nError == ERROR_SUCCESS && szListFile != NULL && *szListFile != 0) + nError = SFileAddListFile((HANDLE)ha, szListFile); + + // Allocate the structure for MPQ search + if(nError == ERROR_SUCCESS) + { + nSize = sizeof(TMPQSearch) + strlen(szMask) + 1; + if((hs = (TMPQSearch *)STORM_ALLOC(char, nSize)) == NULL) + nError = ERROR_NOT_ENOUGH_MEMORY; + } + + // Perform the first search + if(nError == ERROR_SUCCESS) + { + memset(hs, 0, sizeof(TMPQSearch)); + strcpy(hs->szSearchMask, szMask); + hs->dwFlagMask = MPQ_FILE_EXISTS; + hs->ha = ha; + + // If the archive is patched archive, we have to create a merge table + // to prevent files being repeated + if(ha->haPatch != NULL) + { + hs->dwSearchTableItems = GetSearchTableItems(ha); + hs->pSearchTable = STORM_ALLOC(TFileEntry *, hs->dwSearchTableItems); + hs->dwFlagMask = MPQ_FILE_EXISTS | MPQ_FILE_PATCH_FILE; + if(hs->pSearchTable != NULL) + memset(hs->pSearchTable, 0, hs->dwSearchTableItems * sizeof(TFileEntry *)); + else + nError = ERROR_NOT_ENOUGH_MEMORY; + } + } + + // Perform first item searching + if(nError == ERROR_SUCCESS) + { + nError = DoMPQSearch(hs, lpFindFileData); + } + + // Cleanup + if(nError != ERROR_SUCCESS) + { + FreeMPQSearch(hs); + SetLastError(nError); + } + + // Return the result value + return (HANDLE)hs; +} + +bool WINAPI SFileFindNextFile(HANDLE hFind, SFILE_FIND_DATA * lpFindFileData) +{ + TMPQSearch * hs = IsValidSearchHandle(hFind); + int nError = ERROR_SUCCESS; + + // Check the parameters + if(hs == NULL) + nError = ERROR_INVALID_HANDLE; + if(lpFindFileData == NULL) + nError = ERROR_INVALID_PARAMETER; + + if(nError == ERROR_SUCCESS) + nError = DoMPQSearch(hs, lpFindFileData); + + if(nError != ERROR_SUCCESS) + SetLastError(nError); + return (nError == ERROR_SUCCESS); +} + +bool WINAPI SFileFindClose(HANDLE hFind) +{ + TMPQSearch * hs = IsValidSearchHandle(hFind); + + // Check the parameters + if(hs == NULL) + { + SetLastError(ERROR_INVALID_HANDLE); + return false; + } + + FreeMPQSearch(hs); + return true; +} diff --git a/src/SFileGetFileInfo.cpp b/src/SFileGetFileInfo.cpp index bf6273d..9874793 100644 --- a/src/SFileGetFileInfo.cpp +++ b/src/SFileGetFileInfo.cpp @@ -1,992 +1,992 @@ -/*****************************************************************************/ -/* SFileGetFileInfo.cpp Copyright (c) Ladislav Zezula 2013 */ -/*---------------------------------------------------------------------------*/ -/* Description: */ -/*---------------------------------------------------------------------------*/ -/* Date Ver Who Comment */ -/* -------- ---- --- ------- */ -/* 30.11.13 1.00 Lad The first version of SFileGetFileInfo.cpp */ -/*****************************************************************************/ - -#define __STORMLIB_SELF__ -#include "StormLib.h" -#include "StormCommon.h" - -//----------------------------------------------------------------------------- -// Local defines - -// Information types for SFileGetFileInfo -#define SFILE_INFO_TYPE_INVALID_HANDLE 0 -#define SFILE_INFO_TYPE_NOT_FOUND 1 -#define SFILE_INFO_TYPE_DIRECT_POINTER 2 -#define SFILE_INFO_TYPE_ALLOCATED 3 -#define SFILE_INFO_TYPE_READ_FROM_FILE 4 -#define SFILE_INFO_TYPE_TABLE_POINTER 5 -#define SFILE_INFO_TYPE_FILE_ENTRY 6 - -//----------------------------------------------------------------------------- -// Local functions - -static void ConvertFileEntryToSelfRelative(TFileEntry * pFileEntry, TFileEntry * pSrcFileEntry) -{ - // Copy the file entry itself - memcpy(pFileEntry, pSrcFileEntry, sizeof(TFileEntry)); - - // If source is NULL, leave it NULL - if(pSrcFileEntry->szFileName != NULL) - { - // Set the file name pointer after the file entry - pFileEntry->szFileName = (char *)(pFileEntry + 1); - strcpy(pFileEntry->szFileName, pSrcFileEntry->szFileName); - } -} - - -static DWORD GetMpqFileCount(TMPQArchive * ha) -{ - TFileEntry * pFileTableEnd; - TFileEntry * pFileEntry; - DWORD dwFileCount = 0; - - // Go through all open MPQs, including patches - while(ha != NULL) - { - // Only count files that are not patch files - pFileTableEnd = ha->pFileTable + ha->dwFileTableSize; - for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++) - { - // If the file is patch file and this is not primary archive, skip it - // BUGBUG: This errorneously counts non-patch files that are in both - // base MPQ and in patches, and increases the number of files by cca 50% - if((pFileEntry->dwFlags & (MPQ_FILE_EXISTS | MPQ_FILE_PATCH_FILE)) == MPQ_FILE_EXISTS) - dwFileCount++; - } - - // Move to the next patch archive - ha = ha->haPatch; - } - - return dwFileCount; -} - -static bool GetFilePatchChain(TMPQFile * hf, void * pvFileInfo, DWORD cbFileInfo, DWORD * pcbLengthNeeded) -{ - TMPQFile * hfTemp; - TCHAR * szFileInfo = (TCHAR *)pvFileInfo; - size_t cchCharsNeeded = 1; - size_t cchFileInfo = (cbFileInfo / sizeof(TCHAR)); - size_t nLength; - - // Patch chain is only supported on MPQ files. - if(hf->pStream != NULL) - { - SetLastError(ERROR_INVALID_PARAMETER); - return false; - } - - // Calculate the necessary length of the multi-string - for(hfTemp = hf; hfTemp != NULL; hfTemp = hfTemp->hfPatch) - cchCharsNeeded += _tcslen(FileStream_GetFileName(hfTemp->ha->pStream)) + 1; - - // Give the caller the needed length - if(pcbLengthNeeded != NULL) - pcbLengthNeeded[0] = (DWORD)(cchCharsNeeded * sizeof(TCHAR)); - - // If the caller gave both buffer pointer and data length, - // try to copy the patch chain - if(szFileInfo != NULL && cchFileInfo != 0) - { - // If there is enough space in the buffer, copy the patch chain - if(cchCharsNeeded > cchFileInfo) - { - SetLastError(ERROR_INSUFFICIENT_BUFFER); - return false; - } - - // Copy each patch - for(hfTemp = hf; hfTemp != NULL; hfTemp = hfTemp->hfPatch) - { - // Get the file name and its length - const TCHAR * szFileName = FileStream_GetFileName(hfTemp->ha->pStream); - nLength = _tcslen(szFileName) + 1; - - // Copy the file name - memcpy(szFileInfo, szFileName, nLength * sizeof(TCHAR)); - szFileInfo += nLength; - } - - // Make it multi-string - szFileInfo[0] = 0; - } - - return true; -} - -//----------------------------------------------------------------------------- -// Retrieves an information about an archive or about a file within the archive -// -// hMpqOrFile - Handle to an MPQ archive or to a file -// InfoClass - Information to obtain -// pvFileInfo - Pointer to buffer to store the information -// cbFileInfo - Size of the buffer pointed by pvFileInfo -// pcbLengthNeeded - Receives number of bytes necessary to store the information - -bool WINAPI SFileGetFileInfo( - HANDLE hMpqOrFile, - SFileInfoClass InfoClass, - void * pvFileInfo, - DWORD cbFileInfo, - LPDWORD pcbLengthNeeded) -{ - MPQ_SIGNATURE_INFO SignatureInfo; - TMPQArchive * ha = NULL; - TFileEntry * pFileEntry = NULL; - ULONGLONG Int64Value = 0; - ULONGLONG ByteOffset = 0; - TMPQFile * hf = NULL; - void * pvSrcFileInfo = NULL; - DWORD cbSrcFileInfo = 0; - DWORD dwInt32Value = 0; - int nInfoType = SFILE_INFO_TYPE_INVALID_HANDLE; - int nError = ERROR_SUCCESS; - - switch(InfoClass) - { - case SFileMpqFileName: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - pvSrcFileInfo = (void *)FileStream_GetFileName(ha->pStream); - cbSrcFileInfo = (DWORD)(_tcslen((TCHAR *)pvSrcFileInfo) + 1) * sizeof(TCHAR); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqStreamBitmap: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - return FileStream_GetBitmap(ha->pStream, pvFileInfo, cbFileInfo, pcbLengthNeeded); - break; - - case SFileMpqUserDataOffset: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - nInfoType = SFILE_INFO_TYPE_NOT_FOUND; - if(ha->pUserData != NULL) - { - pvSrcFileInfo = &ha->UserDataPos; - cbSrcFileInfo = sizeof(ULONGLONG); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - } - break; - - case SFileMpqUserDataHeader: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - nInfoType = SFILE_INFO_TYPE_NOT_FOUND; - if(ha->pUserData != NULL) - { - ByteOffset = ha->UserDataPos; - cbSrcFileInfo = sizeof(TMPQUserData); - nInfoType = SFILE_INFO_TYPE_READ_FROM_FILE; - } - } - break; - - case SFileMpqUserData: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - nInfoType = SFILE_INFO_TYPE_NOT_FOUND; - if(ha->pUserData != NULL) - { - ByteOffset = ha->UserDataPos + sizeof(TMPQUserData); - cbSrcFileInfo = ha->pUserData->dwHeaderOffs - sizeof(TMPQUserData); - nInfoType = SFILE_INFO_TYPE_READ_FROM_FILE; - } - } - break; - - case SFileMpqHeaderOffset: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - pvSrcFileInfo = &ha->MpqPos; - cbSrcFileInfo = sizeof(ULONGLONG); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqHeaderSize: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - pvSrcFileInfo = &ha->pHeader->dwHeaderSize; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqHeader: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - ByteOffset = ha->MpqPos; - cbSrcFileInfo = ha->pHeader->dwHeaderSize; - nInfoType = SFILE_INFO_TYPE_READ_FROM_FILE; - } - break; - - case SFileMpqHetTableOffset: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - pvSrcFileInfo = &ha->pHeader->HetTablePos64; - cbSrcFileInfo = sizeof(ULONGLONG); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqHetTableSize: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - pvSrcFileInfo = &ha->pHeader->HetTableSize64; - cbSrcFileInfo = sizeof(ULONGLONG); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqHetHeader: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - nInfoType = SFILE_INFO_TYPE_NOT_FOUND; - pvSrcFileInfo = LoadExtTable(ha, ha->pHeader->HetTablePos64, (size_t)ha->pHeader->HetTableSize64, HET_TABLE_SIGNATURE, MPQ_KEY_HASH_TABLE); - if(pvSrcFileInfo != NULL) - { - cbSrcFileInfo = sizeof(TMPQHetHeader); - nInfoType = SFILE_INFO_TYPE_ALLOCATED; - } - } - break; - - case SFileMpqHetTable: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - nInfoType = SFILE_INFO_TYPE_NOT_FOUND; - pvSrcFileInfo = LoadHetTable(ha); - if(pvSrcFileInfo != NULL) - { - cbSrcFileInfo = sizeof(void *); - nInfoType = SFILE_INFO_TYPE_TABLE_POINTER; - } - } - break; - - case SFileMpqBetTableOffset: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - pvSrcFileInfo = &ha->pHeader->BetTablePos64; - cbSrcFileInfo = sizeof(ULONGLONG); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqBetTableSize: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - pvSrcFileInfo = &ha->pHeader->BetTableSize64; - cbSrcFileInfo = sizeof(ULONGLONG); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqBetHeader: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - nInfoType = SFILE_INFO_TYPE_NOT_FOUND; - pvSrcFileInfo = LoadExtTable(ha, ha->pHeader->BetTablePos64, (size_t)ha->pHeader->BetTableSize64, BET_TABLE_SIGNATURE, MPQ_KEY_BLOCK_TABLE); - if(pvSrcFileInfo != NULL) - { - // It is allowed for the caller to only require BET header. - cbSrcFileInfo = sizeof(TMPQBetHeader) + ((TMPQBetHeader *)pvSrcFileInfo)->dwFlagCount * sizeof(DWORD); - if(cbFileInfo == sizeof(TMPQBetHeader)) - cbSrcFileInfo = sizeof(TMPQBetHeader); - nInfoType = SFILE_INFO_TYPE_ALLOCATED; - } - } - break; - - case SFileMpqBetTable: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - nInfoType = SFILE_INFO_TYPE_NOT_FOUND; - pvSrcFileInfo = LoadBetTable(ha); - if(pvSrcFileInfo != NULL) - { - cbSrcFileInfo = sizeof(void *); - nInfoType = SFILE_INFO_TYPE_TABLE_POINTER; - } - } - break; - - case SFileMpqHashTableOffset: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - Int64Value = MAKE_OFFSET64(ha->pHeader->wHashTablePosHi, ha->pHeader->dwHashTablePos); - pvSrcFileInfo = &Int64Value; - cbSrcFileInfo = sizeof(ULONGLONG); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqHashTableSize64: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - pvSrcFileInfo = &ha->pHeader->HashTableSize64; - cbSrcFileInfo = sizeof(ULONGLONG); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqHashTableSize: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - pvSrcFileInfo = &ha->pHeader->dwHashTableSize; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqHashTable: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL && ha->pHashTable != NULL) - { - pvSrcFileInfo = ha->pHashTable; - cbSrcFileInfo = ha->pHeader->dwHashTableSize * sizeof(TMPQHash); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqBlockTableOffset: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - Int64Value = MAKE_OFFSET64(ha->pHeader->wBlockTablePosHi, ha->pHeader->dwBlockTablePos); - pvSrcFileInfo = &Int64Value; - cbSrcFileInfo = sizeof(ULONGLONG); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqBlockTableSize64: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - pvSrcFileInfo = &ha->pHeader->BlockTableSize64; - cbSrcFileInfo = sizeof(ULONGLONG); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqBlockTableSize: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - pvSrcFileInfo = &ha->pHeader->dwBlockTableSize; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqBlockTable: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - nInfoType = SFILE_INFO_TYPE_NOT_FOUND; - if(MAKE_OFFSET64(ha->pHeader->wBlockTablePosHi, ha->pHeader->dwBlockTablePos) != 0) - { - cbSrcFileInfo = ha->pHeader->dwBlockTableSize * sizeof(TMPQBlock); - if(cbFileInfo >= cbSrcFileInfo) - pvSrcFileInfo = LoadBlockTable(ha, true); - nInfoType = SFILE_INFO_TYPE_ALLOCATED; - } - } - break; - - case SFileMpqHiBlockTableOffset: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - pvSrcFileInfo = &ha->pHeader->HiBlockTablePos64; - cbSrcFileInfo = sizeof(ULONGLONG); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqHiBlockTableSize64: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - pvSrcFileInfo = &ha->pHeader->HiBlockTableSize64; - cbSrcFileInfo = sizeof(ULONGLONG); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqHiBlockTable: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - nInfoType = SFILE_INFO_TYPE_NOT_FOUND; - if(ha->pHeader->HiBlockTablePos64 && ha->pHeader->HiBlockTableSize64) - { - assert(false); - } - } - break; - - case SFileMpqSignatures: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL && QueryMpqSignatureInfo(ha, &SignatureInfo)) - { - pvSrcFileInfo = &SignatureInfo.SignatureTypes; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqStrongSignatureOffset: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - nInfoType = SFILE_INFO_TYPE_NOT_FOUND; - if(QueryMpqSignatureInfo(ha, &SignatureInfo) && (SignatureInfo.SignatureTypes & SIGNATURE_TYPE_STRONG)) - { - pvSrcFileInfo = &SignatureInfo.EndMpqData; - cbSrcFileInfo = sizeof(ULONGLONG); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - } - break; - - case SFileMpqStrongSignatureSize: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - nInfoType = SFILE_INFO_TYPE_NOT_FOUND; - if(QueryMpqSignatureInfo(ha, &SignatureInfo) && (SignatureInfo.SignatureTypes & SIGNATURE_TYPE_STRONG)) - { - dwInt32Value = MPQ_STRONG_SIGNATURE_SIZE + 4; - pvSrcFileInfo = &dwInt32Value; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - } - break; - - case SFileMpqStrongSignature: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - nInfoType = SFILE_INFO_TYPE_NOT_FOUND; - if(QueryMpqSignatureInfo(ha, &SignatureInfo) && (SignatureInfo.SignatureTypes & SIGNATURE_TYPE_STRONG)) - { - pvSrcFileInfo = SignatureInfo.Signature; - cbSrcFileInfo = MPQ_STRONG_SIGNATURE_SIZE + 4; - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - } - break; - - case SFileMpqArchiveSize64: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - pvSrcFileInfo = &ha->pHeader->ArchiveSize64; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqArchiveSize: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - pvSrcFileInfo = &ha->pHeader->dwArchiveSize; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqMaxFileCount: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - pvSrcFileInfo = &ha->dwMaxFileCount; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqFileTableSize: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - pvSrcFileInfo = &ha->dwFileTableSize; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqSectorSize: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - pvSrcFileInfo = &ha->dwSectorSize; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqNumberOfFiles: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - pvSrcFileInfo = &dwInt32Value; - cbSrcFileInfo = sizeof(DWORD); - dwInt32Value = GetMpqFileCount(ha); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqRawChunkSize: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - nInfoType = SFILE_INFO_TYPE_NOT_FOUND; - if(ha->pHeader->dwRawChunkSize != 0) - { - pvSrcFileInfo = &ha->pHeader->dwRawChunkSize; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - } - break; - - case SFileMpqStreamFlags: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - FileStream_GetFlags(ha->pStream, &dwInt32Value); - pvSrcFileInfo = &dwInt32Value; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileMpqFlags: - ha = IsValidMpqHandle(hMpqOrFile); - if(ha != NULL) - { - dwInt32Value = ha->dwFlags; - pvSrcFileInfo = &dwInt32Value; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileInfoPatchChain: - hf = IsValidFileHandle(hMpqOrFile); - if(hf != NULL) - return GetFilePatchChain(hf, pvFileInfo, cbFileInfo, pcbLengthNeeded); - break; - - case SFileInfoFileEntry: - hf = IsValidFileHandle(hMpqOrFile); - if(hf != NULL && hf->pFileEntry != NULL) - { - pvSrcFileInfo = pFileEntry = hf->pFileEntry; - cbSrcFileInfo = sizeof(TFileEntry); - if(pFileEntry->szFileName != NULL) - cbSrcFileInfo += (DWORD)strlen(pFileEntry->szFileName) + 1; - nInfoType = SFILE_INFO_TYPE_FILE_ENTRY; - } - break; - - case SFileInfoHashEntry: - hf = IsValidFileHandle(hMpqOrFile); - if(hf != NULL && hf->pHashEntry != NULL) - { - pvSrcFileInfo = hf->pHashEntry; - cbSrcFileInfo = sizeof(TMPQHash); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileInfoHashIndex: - hf = IsValidFileHandle(hMpqOrFile); - if(hf != NULL && hf->pHashEntry != NULL) - { - pvSrcFileInfo = &hf->dwHashIndex; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileInfoNameHash1: - hf = IsValidFileHandle(hMpqOrFile); - if(hf != NULL && hf->pHashEntry != NULL) - { - dwInt32Value = hf->pHashEntry->dwName1; - pvSrcFileInfo = &dwInt32Value; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileInfoNameHash2: - hf = IsValidFileHandle(hMpqOrFile); - if(hf != NULL && hf->pHashEntry != NULL) - { - dwInt32Value = hf->pHashEntry->dwName2; - pvSrcFileInfo = &dwInt32Value; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileInfoNameHash3: - hf = IsValidFileHandle(hMpqOrFile); - if(hf != NULL && hf->pFileEntry != NULL) - { - pvSrcFileInfo = &hf->pFileEntry->FileNameHash; - cbSrcFileInfo = sizeof(ULONGLONG); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileInfoLocale: - hf = IsValidFileHandle(hMpqOrFile); - if(hf != NULL && hf->pHashEntry != NULL) - { - dwInt32Value = hf->pHashEntry->lcLocale; - pvSrcFileInfo = &dwInt32Value; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileInfoFileIndex: - hf = IsValidFileHandle(hMpqOrFile); - if(hf != NULL && hf->ha != NULL && hf->pFileEntry != NULL) - { - dwInt32Value = (DWORD)(hf->pFileEntry - hf->ha->pFileTable); - pvSrcFileInfo = &dwInt32Value; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileInfoByteOffset: - hf = IsValidFileHandle(hMpqOrFile); - if(hf != NULL && hf->pFileEntry != NULL) - { - pvSrcFileInfo = &hf->pFileEntry->ByteOffset; - cbSrcFileInfo = sizeof(ULONGLONG); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileInfoFileTime: - hf = IsValidFileHandle(hMpqOrFile); - if(hf != NULL && hf->pFileEntry != NULL) - { - pvSrcFileInfo = &hf->pFileEntry->FileTime; - cbSrcFileInfo = sizeof(ULONGLONG); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileInfoFileSize: - hf = IsValidFileHandle(hMpqOrFile); - if(hf != NULL && hf->pFileEntry != NULL) - { - pvSrcFileInfo = &hf->pFileEntry->dwFileSize; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileInfoCompressedSize: - hf = IsValidFileHandle(hMpqOrFile); - if(hf != NULL && hf->pFileEntry != NULL) - { - pvSrcFileInfo = &hf->pFileEntry->dwCmpSize; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileInfoFlags: - hf = IsValidFileHandle(hMpqOrFile); - if(hf != NULL && hf->pFileEntry != NULL) - { - pvSrcFileInfo = &hf->pFileEntry->dwFlags; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileInfoEncryptionKey: - hf = IsValidFileHandle(hMpqOrFile); - if(hf != NULL) - { - pvSrcFileInfo = &hf->dwFileKey; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - case SFileInfoEncryptionKeyRaw: - hf = IsValidFileHandle(hMpqOrFile); - if(hf != NULL && hf->pFileEntry != NULL) - { - dwInt32Value = hf->dwFileKey; - if(hf->pFileEntry->dwFlags & MPQ_FILE_FIX_KEY) - dwInt32Value = (dwInt32Value ^ hf->pFileEntry->dwFileSize) - (DWORD)hf->MpqFilePos; - pvSrcFileInfo = &dwInt32Value; - cbSrcFileInfo = sizeof(DWORD); - nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; - } - break; - - default: // Invalid info class - SetLastError(ERROR_INVALID_PARAMETER); - return false; - } - - // If we validated the handle and info class, give as much info as possible - if(nInfoType >= SFILE_INFO_TYPE_DIRECT_POINTER) - { - // Give the length needed, if wanted - if(pcbLengthNeeded != NULL) - pcbLengthNeeded[0] = cbSrcFileInfo; - - // If the caller entered an output buffer, the output size must also be entered - if(pvFileInfo != NULL && cbFileInfo != 0) - { - // Check if there is enough space in the output buffer - if(cbSrcFileInfo <= cbFileInfo) - { - switch(nInfoType) - { - case SFILE_INFO_TYPE_DIRECT_POINTER: - case SFILE_INFO_TYPE_ALLOCATED: - assert(pvSrcFileInfo != NULL); - memcpy(pvFileInfo, pvSrcFileInfo, cbSrcFileInfo); - break; - - case SFILE_INFO_TYPE_READ_FROM_FILE: - if(!FileStream_Read(ha->pStream, &ByteOffset, pvFileInfo, cbSrcFileInfo)) - nError = GetLastError(); - break; - - case SFILE_INFO_TYPE_TABLE_POINTER: - assert(pvSrcFileInfo != NULL); - *(void **)pvFileInfo = pvSrcFileInfo; - pvSrcFileInfo = NULL; - break; - - case SFILE_INFO_TYPE_FILE_ENTRY: - assert(pFileEntry != NULL); - ConvertFileEntryToSelfRelative((TFileEntry *)pvFileInfo, pFileEntry); - break; - } - } - else - { - nError = ERROR_INSUFFICIENT_BUFFER; - } - } - - // Free the file info if needed - if(nInfoType == SFILE_INFO_TYPE_ALLOCATED && pvSrcFileInfo != NULL) - STORM_FREE(pvSrcFileInfo); - if(nInfoType == SFILE_INFO_TYPE_TABLE_POINTER && pvSrcFileInfo != NULL) - SFileFreeFileInfo(pvSrcFileInfo, InfoClass); - } - else - { - // Handle error cases - if(nInfoType == SFILE_INFO_TYPE_INVALID_HANDLE) - nError = ERROR_INVALID_HANDLE; - if(nInfoType == SFILE_INFO_TYPE_NOT_FOUND) - nError = ERROR_FILE_NOT_FOUND; - } - - // Set the last error value, if needed - if(nError != ERROR_SUCCESS) - SetLastError(nError); - return (nError == ERROR_SUCCESS); -} - -bool WINAPI SFileFreeFileInfo(void * pvFileInfo, SFileInfoClass InfoClass) -{ - switch(InfoClass) - { - case SFileMpqHetTable: - FreeHetTable((TMPQHetTable *)pvFileInfo); - return true; - - case SFileMpqBetTable: - FreeBetTable((TMPQBetTable *)pvFileInfo); - return true; - - default: - break; - } - - SetLastError(ERROR_INVALID_PARAMETER); - return false; -} - -//----------------------------------------------------------------------------- -// Tries to retrieve the file name - -struct TFileHeader2Ext -{ - DWORD dwOffset00Data; // Required data at offset 00 (32-bits) - DWORD dwOffset00Mask; // Mask for data at offset 00 (32 bits). 0 = data are ignored - DWORD dwOffset04Data; // Required data at offset 04 (32-bits) - DWORD dwOffset04Mask; // Mask for data at offset 04 (32 bits). 0 = data are ignored - const char * szExt; // Supplied extension, if the condition is true -}; - -static TFileHeader2Ext data2ext[] = -{ - {0x00005A4D, 0x0000FFFF, 0x00000000, 0x00000000, "exe"}, // EXE files - {0x00000006, 0xFFFFFFFF, 0x00000001, 0xFFFFFFFF, "dc6"}, // EXE files - {0x1A51504D, 0xFFFFFFFF, 0x00000000, 0x00000000, "mpq"}, // MPQ archive header ID ('MPQ\x1A') - {0x46464952, 0xFFFFFFFF, 0x00000000, 0x00000000, "wav"}, // WAVE header 'RIFF' - {0x324B4D53, 0xFFFFFFFF, 0x00000000, 0x00000000, "smk"}, // Old "Smacker Video" files 'SMK2' - {0x694B4942, 0xFFFFFFFF, 0x00000000, 0x00000000, "bik"}, // Bink video files (new) - {0x0801050A, 0xFFFFFFFF, 0x00000000, 0x00000000, "pcx"}, // PCX images used in Diablo I - {0x544E4F46, 0xFFFFFFFF, 0x00000000, 0x00000000, "fnt"}, // Font files used in Diablo II - {0x6D74683C, 0xFFFFFFFF, 0x00000000, 0x00000000, "html"}, // HTML 'ha->pFileTable), data2ext[i].szExt); - - // Save the pseudo-name in the file entry as well - AllocateFileName(hf->ha, pFileEntry, szPseudoName); - - // If the caller wants to copy the file name, do it - if(szFileName != NULL) - strcpy(szFileName, szPseudoName); - return ERROR_SUCCESS; - } - } - } - - return ERROR_CAN_NOT_COMPLETE; -} - -bool WINAPI SFileGetFileName(HANDLE hFile, char * szFileName) -{ - TMPQFile * hf = (TMPQFile *)hFile; // MPQ File handle - int nError = ERROR_INVALID_HANDLE; - - // Check valid parameters - if(IsValidFileHandle(hFile)) - { - TFileEntry * pFileEntry = hf->pFileEntry; - - // For MPQ files, retrieve the file name from the file entry - if(hf->pStream == NULL) - { - if(pFileEntry != NULL) - { - // If the file name is not there yet, create a pseudo name - if(pFileEntry->szFileName == NULL) - nError = CreatePseudoFileName(hFile, pFileEntry, szFileName); - - // Copy the file name to the output buffer, if any - if(pFileEntry->szFileName && szFileName) - { - strcpy(szFileName, pFileEntry->szFileName); - nError = ERROR_SUCCESS; - } - } - } - - // For local files, copy the file name from the stream - else - { - if(szFileName != NULL) - { - const TCHAR * szStreamName = FileStream_GetFileName(hf->pStream); - CopyFileName(szFileName, szStreamName, _tcslen(szStreamName)); - } - nError = ERROR_SUCCESS; - } - } - - if(nError != ERROR_SUCCESS) - SetLastError(nError); - return (nError == ERROR_SUCCESS); -} - +/*****************************************************************************/ +/* SFileGetFileInfo.cpp Copyright (c) Ladislav Zezula 2013 */ +/*---------------------------------------------------------------------------*/ +/* Description: */ +/*---------------------------------------------------------------------------*/ +/* Date Ver Who Comment */ +/* -------- ---- --- ------- */ +/* 30.11.13 1.00 Lad The first version of SFileGetFileInfo.cpp */ +/*****************************************************************************/ + +#define __STORMLIB_SELF__ +#include "StormLib.h" +#include "StormCommon.h" + +//----------------------------------------------------------------------------- +// Local defines + +// Information types for SFileGetFileInfo +#define SFILE_INFO_TYPE_INVALID_HANDLE 0 +#define SFILE_INFO_TYPE_NOT_FOUND 1 +#define SFILE_INFO_TYPE_DIRECT_POINTER 2 +#define SFILE_INFO_TYPE_ALLOCATED 3 +#define SFILE_INFO_TYPE_READ_FROM_FILE 4 +#define SFILE_INFO_TYPE_TABLE_POINTER 5 +#define SFILE_INFO_TYPE_FILE_ENTRY 6 + +//----------------------------------------------------------------------------- +// Local functions + +static void ConvertFileEntryToSelfRelative(TFileEntry * pFileEntry, TFileEntry * pSrcFileEntry) +{ + // Copy the file entry itself + memcpy(pFileEntry, pSrcFileEntry, sizeof(TFileEntry)); + + // If source is NULL, leave it NULL + if(pSrcFileEntry->szFileName != NULL) + { + // Set the file name pointer after the file entry + pFileEntry->szFileName = (char *)(pFileEntry + 1); + strcpy(pFileEntry->szFileName, pSrcFileEntry->szFileName); + } +} + + +static DWORD GetMpqFileCount(TMPQArchive * ha) +{ + TFileEntry * pFileTableEnd; + TFileEntry * pFileEntry; + DWORD dwFileCount = 0; + + // Go through all open MPQs, including patches + while(ha != NULL) + { + // Only count files that are not patch files + pFileTableEnd = ha->pFileTable + ha->dwFileTableSize; + for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++) + { + // If the file is patch file and this is not primary archive, skip it + // BUGBUG: This errorneously counts non-patch files that are in both + // base MPQ and in patches, and increases the number of files by cca 50% + if((pFileEntry->dwFlags & (MPQ_FILE_EXISTS | MPQ_FILE_PATCH_FILE)) == MPQ_FILE_EXISTS) + dwFileCount++; + } + + // Move to the next patch archive + ha = ha->haPatch; + } + + return dwFileCount; +} + +static bool GetFilePatchChain(TMPQFile * hf, void * pvFileInfo, DWORD cbFileInfo, DWORD * pcbLengthNeeded) +{ + TMPQFile * hfTemp; + TCHAR * szFileInfo = (TCHAR *)pvFileInfo; + size_t cchCharsNeeded = 1; + size_t cchFileInfo = (cbFileInfo / sizeof(TCHAR)); + size_t nLength; + + // Patch chain is only supported on MPQ files. + if(hf->pStream != NULL) + { + SetLastError(ERROR_INVALID_PARAMETER); + return false; + } + + // Calculate the necessary length of the multi-string + for(hfTemp = hf; hfTemp != NULL; hfTemp = hfTemp->hfPatch) + cchCharsNeeded += _tcslen(FileStream_GetFileName(hfTemp->ha->pStream)) + 1; + + // Give the caller the needed length + if(pcbLengthNeeded != NULL) + pcbLengthNeeded[0] = (DWORD)(cchCharsNeeded * sizeof(TCHAR)); + + // If the caller gave both buffer pointer and data length, + // try to copy the patch chain + if(szFileInfo != NULL && cchFileInfo != 0) + { + // If there is enough space in the buffer, copy the patch chain + if(cchCharsNeeded > cchFileInfo) + { + SetLastError(ERROR_INSUFFICIENT_BUFFER); + return false; + } + + // Copy each patch + for(hfTemp = hf; hfTemp != NULL; hfTemp = hfTemp->hfPatch) + { + // Get the file name and its length + const TCHAR * szFileName = FileStream_GetFileName(hfTemp->ha->pStream); + nLength = _tcslen(szFileName) + 1; + + // Copy the file name + memcpy(szFileInfo, szFileName, nLength * sizeof(TCHAR)); + szFileInfo += nLength; + } + + // Make it multi-string + szFileInfo[0] = 0; + } + + return true; +} + +//----------------------------------------------------------------------------- +// Retrieves an information about an archive or about a file within the archive +// +// hMpqOrFile - Handle to an MPQ archive or to a file +// InfoClass - Information to obtain +// pvFileInfo - Pointer to buffer to store the information +// cbFileInfo - Size of the buffer pointed by pvFileInfo +// pcbLengthNeeded - Receives number of bytes necessary to store the information + +bool WINAPI SFileGetFileInfo( + HANDLE hMpqOrFile, + SFileInfoClass InfoClass, + void * pvFileInfo, + DWORD cbFileInfo, + LPDWORD pcbLengthNeeded) +{ + MPQ_SIGNATURE_INFO SignatureInfo; + TMPQArchive * ha = NULL; + TFileEntry * pFileEntry = NULL; + ULONGLONG Int64Value = 0; + ULONGLONG ByteOffset = 0; + TMPQFile * hf = NULL; + void * pvSrcFileInfo = NULL; + DWORD cbSrcFileInfo = 0; + DWORD dwInt32Value = 0; + int nInfoType = SFILE_INFO_TYPE_INVALID_HANDLE; + int nError = ERROR_SUCCESS; + + switch(InfoClass) + { + case SFileMpqFileName: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + pvSrcFileInfo = (void *)FileStream_GetFileName(ha->pStream); + cbSrcFileInfo = (DWORD)(_tcslen((TCHAR *)pvSrcFileInfo) + 1) * sizeof(TCHAR); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqStreamBitmap: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + return FileStream_GetBitmap(ha->pStream, pvFileInfo, cbFileInfo, pcbLengthNeeded); + break; + + case SFileMpqUserDataOffset: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + nInfoType = SFILE_INFO_TYPE_NOT_FOUND; + if(ha->pUserData != NULL) + { + pvSrcFileInfo = &ha->UserDataPos; + cbSrcFileInfo = sizeof(ULONGLONG); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + } + break; + + case SFileMpqUserDataHeader: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + nInfoType = SFILE_INFO_TYPE_NOT_FOUND; + if(ha->pUserData != NULL) + { + ByteOffset = ha->UserDataPos; + cbSrcFileInfo = sizeof(TMPQUserData); + nInfoType = SFILE_INFO_TYPE_READ_FROM_FILE; + } + } + break; + + case SFileMpqUserData: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + nInfoType = SFILE_INFO_TYPE_NOT_FOUND; + if(ha->pUserData != NULL) + { + ByteOffset = ha->UserDataPos + sizeof(TMPQUserData); + cbSrcFileInfo = ha->pUserData->dwHeaderOffs - sizeof(TMPQUserData); + nInfoType = SFILE_INFO_TYPE_READ_FROM_FILE; + } + } + break; + + case SFileMpqHeaderOffset: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + pvSrcFileInfo = &ha->MpqPos; + cbSrcFileInfo = sizeof(ULONGLONG); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqHeaderSize: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + pvSrcFileInfo = &ha->pHeader->dwHeaderSize; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqHeader: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + ByteOffset = ha->MpqPos; + cbSrcFileInfo = ha->pHeader->dwHeaderSize; + nInfoType = SFILE_INFO_TYPE_READ_FROM_FILE; + } + break; + + case SFileMpqHetTableOffset: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + pvSrcFileInfo = &ha->pHeader->HetTablePos64; + cbSrcFileInfo = sizeof(ULONGLONG); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqHetTableSize: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + pvSrcFileInfo = &ha->pHeader->HetTableSize64; + cbSrcFileInfo = sizeof(ULONGLONG); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqHetHeader: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + nInfoType = SFILE_INFO_TYPE_NOT_FOUND; + pvSrcFileInfo = LoadExtTable(ha, ha->pHeader->HetTablePos64, (size_t)ha->pHeader->HetTableSize64, HET_TABLE_SIGNATURE, MPQ_KEY_HASH_TABLE); + if(pvSrcFileInfo != NULL) + { + cbSrcFileInfo = sizeof(TMPQHetHeader); + nInfoType = SFILE_INFO_TYPE_ALLOCATED; + } + } + break; + + case SFileMpqHetTable: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + nInfoType = SFILE_INFO_TYPE_NOT_FOUND; + pvSrcFileInfo = LoadHetTable(ha); + if(pvSrcFileInfo != NULL) + { + cbSrcFileInfo = sizeof(void *); + nInfoType = SFILE_INFO_TYPE_TABLE_POINTER; + } + } + break; + + case SFileMpqBetTableOffset: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + pvSrcFileInfo = &ha->pHeader->BetTablePos64; + cbSrcFileInfo = sizeof(ULONGLONG); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqBetTableSize: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + pvSrcFileInfo = &ha->pHeader->BetTableSize64; + cbSrcFileInfo = sizeof(ULONGLONG); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqBetHeader: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + nInfoType = SFILE_INFO_TYPE_NOT_FOUND; + pvSrcFileInfo = LoadExtTable(ha, ha->pHeader->BetTablePos64, (size_t)ha->pHeader->BetTableSize64, BET_TABLE_SIGNATURE, MPQ_KEY_BLOCK_TABLE); + if(pvSrcFileInfo != NULL) + { + // It is allowed for the caller to only require BET header. + cbSrcFileInfo = sizeof(TMPQBetHeader) + ((TMPQBetHeader *)pvSrcFileInfo)->dwFlagCount * sizeof(DWORD); + if(cbFileInfo == sizeof(TMPQBetHeader)) + cbSrcFileInfo = sizeof(TMPQBetHeader); + nInfoType = SFILE_INFO_TYPE_ALLOCATED; + } + } + break; + + case SFileMpqBetTable: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + nInfoType = SFILE_INFO_TYPE_NOT_FOUND; + pvSrcFileInfo = LoadBetTable(ha); + if(pvSrcFileInfo != NULL) + { + cbSrcFileInfo = sizeof(void *); + nInfoType = SFILE_INFO_TYPE_TABLE_POINTER; + } + } + break; + + case SFileMpqHashTableOffset: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + Int64Value = MAKE_OFFSET64(ha->pHeader->wHashTablePosHi, ha->pHeader->dwHashTablePos); + pvSrcFileInfo = &Int64Value; + cbSrcFileInfo = sizeof(ULONGLONG); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqHashTableSize64: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + pvSrcFileInfo = &ha->pHeader->HashTableSize64; + cbSrcFileInfo = sizeof(ULONGLONG); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqHashTableSize: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + pvSrcFileInfo = &ha->pHeader->dwHashTableSize; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqHashTable: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL && ha->pHashTable != NULL) + { + pvSrcFileInfo = ha->pHashTable; + cbSrcFileInfo = ha->pHeader->dwHashTableSize * sizeof(TMPQHash); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqBlockTableOffset: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + Int64Value = MAKE_OFFSET64(ha->pHeader->wBlockTablePosHi, ha->pHeader->dwBlockTablePos); + pvSrcFileInfo = &Int64Value; + cbSrcFileInfo = sizeof(ULONGLONG); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqBlockTableSize64: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + pvSrcFileInfo = &ha->pHeader->BlockTableSize64; + cbSrcFileInfo = sizeof(ULONGLONG); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqBlockTableSize: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + pvSrcFileInfo = &ha->pHeader->dwBlockTableSize; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqBlockTable: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + nInfoType = SFILE_INFO_TYPE_NOT_FOUND; + if(MAKE_OFFSET64(ha->pHeader->wBlockTablePosHi, ha->pHeader->dwBlockTablePos) != 0) + { + cbSrcFileInfo = ha->pHeader->dwBlockTableSize * sizeof(TMPQBlock); + if(cbFileInfo >= cbSrcFileInfo) + pvSrcFileInfo = LoadBlockTable(ha, true); + nInfoType = SFILE_INFO_TYPE_ALLOCATED; + } + } + break; + + case SFileMpqHiBlockTableOffset: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + pvSrcFileInfo = &ha->pHeader->HiBlockTablePos64; + cbSrcFileInfo = sizeof(ULONGLONG); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqHiBlockTableSize64: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + pvSrcFileInfo = &ha->pHeader->HiBlockTableSize64; + cbSrcFileInfo = sizeof(ULONGLONG); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqHiBlockTable: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + nInfoType = SFILE_INFO_TYPE_NOT_FOUND; + if(ha->pHeader->HiBlockTablePos64 && ha->pHeader->HiBlockTableSize64) + { + assert(false); + } + } + break; + + case SFileMpqSignatures: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL && QueryMpqSignatureInfo(ha, &SignatureInfo)) + { + pvSrcFileInfo = &SignatureInfo.SignatureTypes; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqStrongSignatureOffset: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + nInfoType = SFILE_INFO_TYPE_NOT_FOUND; + if(QueryMpqSignatureInfo(ha, &SignatureInfo) && (SignatureInfo.SignatureTypes & SIGNATURE_TYPE_STRONG)) + { + pvSrcFileInfo = &SignatureInfo.EndMpqData; + cbSrcFileInfo = sizeof(ULONGLONG); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + } + break; + + case SFileMpqStrongSignatureSize: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + nInfoType = SFILE_INFO_TYPE_NOT_FOUND; + if(QueryMpqSignatureInfo(ha, &SignatureInfo) && (SignatureInfo.SignatureTypes & SIGNATURE_TYPE_STRONG)) + { + dwInt32Value = MPQ_STRONG_SIGNATURE_SIZE + 4; + pvSrcFileInfo = &dwInt32Value; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + } + break; + + case SFileMpqStrongSignature: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + nInfoType = SFILE_INFO_TYPE_NOT_FOUND; + if(QueryMpqSignatureInfo(ha, &SignatureInfo) && (SignatureInfo.SignatureTypes & SIGNATURE_TYPE_STRONG)) + { + pvSrcFileInfo = SignatureInfo.Signature; + cbSrcFileInfo = MPQ_STRONG_SIGNATURE_SIZE + 4; + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + } + break; + + case SFileMpqArchiveSize64: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + pvSrcFileInfo = &ha->pHeader->ArchiveSize64; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqArchiveSize: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + pvSrcFileInfo = &ha->pHeader->dwArchiveSize; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqMaxFileCount: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + pvSrcFileInfo = &ha->dwMaxFileCount; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqFileTableSize: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + pvSrcFileInfo = &ha->dwFileTableSize; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqSectorSize: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + pvSrcFileInfo = &ha->dwSectorSize; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqNumberOfFiles: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + pvSrcFileInfo = &dwInt32Value; + cbSrcFileInfo = sizeof(DWORD); + dwInt32Value = GetMpqFileCount(ha); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqRawChunkSize: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + nInfoType = SFILE_INFO_TYPE_NOT_FOUND; + if(ha->pHeader->dwRawChunkSize != 0) + { + pvSrcFileInfo = &ha->pHeader->dwRawChunkSize; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + } + break; + + case SFileMpqStreamFlags: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + FileStream_GetFlags(ha->pStream, &dwInt32Value); + pvSrcFileInfo = &dwInt32Value; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileMpqFlags: + ha = IsValidMpqHandle(hMpqOrFile); + if(ha != NULL) + { + dwInt32Value = ha->dwFlags; + pvSrcFileInfo = &dwInt32Value; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileInfoPatchChain: + hf = IsValidFileHandle(hMpqOrFile); + if(hf != NULL) + return GetFilePatchChain(hf, pvFileInfo, cbFileInfo, pcbLengthNeeded); + break; + + case SFileInfoFileEntry: + hf = IsValidFileHandle(hMpqOrFile); + if(hf != NULL && hf->pFileEntry != NULL) + { + pvSrcFileInfo = pFileEntry = hf->pFileEntry; + cbSrcFileInfo = sizeof(TFileEntry); + if(pFileEntry->szFileName != NULL) + cbSrcFileInfo += (DWORD)strlen(pFileEntry->szFileName) + 1; + nInfoType = SFILE_INFO_TYPE_FILE_ENTRY; + } + break; + + case SFileInfoHashEntry: + hf = IsValidFileHandle(hMpqOrFile); + if(hf != NULL && hf->pHashEntry != NULL) + { + pvSrcFileInfo = hf->pHashEntry; + cbSrcFileInfo = sizeof(TMPQHash); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileInfoHashIndex: + hf = IsValidFileHandle(hMpqOrFile); + if(hf != NULL && hf->pHashEntry != NULL) + { + pvSrcFileInfo = &hf->dwHashIndex; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileInfoNameHash1: + hf = IsValidFileHandle(hMpqOrFile); + if(hf != NULL && hf->pHashEntry != NULL) + { + dwInt32Value = hf->pHashEntry->dwName1; + pvSrcFileInfo = &dwInt32Value; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileInfoNameHash2: + hf = IsValidFileHandle(hMpqOrFile); + if(hf != NULL && hf->pHashEntry != NULL) + { + dwInt32Value = hf->pHashEntry->dwName2; + pvSrcFileInfo = &dwInt32Value; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileInfoNameHash3: + hf = IsValidFileHandle(hMpqOrFile); + if(hf != NULL && hf->pFileEntry != NULL) + { + pvSrcFileInfo = &hf->pFileEntry->FileNameHash; + cbSrcFileInfo = sizeof(ULONGLONG); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileInfoLocale: + hf = IsValidFileHandle(hMpqOrFile); + if(hf != NULL && hf->pHashEntry != NULL) + { + dwInt32Value = hf->pHashEntry->lcLocale; + pvSrcFileInfo = &dwInt32Value; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileInfoFileIndex: + hf = IsValidFileHandle(hMpqOrFile); + if(hf != NULL && hf->ha != NULL && hf->pFileEntry != NULL) + { + dwInt32Value = (DWORD)(hf->pFileEntry - hf->ha->pFileTable); + pvSrcFileInfo = &dwInt32Value; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileInfoByteOffset: + hf = IsValidFileHandle(hMpqOrFile); + if(hf != NULL && hf->pFileEntry != NULL) + { + pvSrcFileInfo = &hf->pFileEntry->ByteOffset; + cbSrcFileInfo = sizeof(ULONGLONG); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileInfoFileTime: + hf = IsValidFileHandle(hMpqOrFile); + if(hf != NULL && hf->pFileEntry != NULL) + { + pvSrcFileInfo = &hf->pFileEntry->FileTime; + cbSrcFileInfo = sizeof(ULONGLONG); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileInfoFileSize: + hf = IsValidFileHandle(hMpqOrFile); + if(hf != NULL && hf->pFileEntry != NULL) + { + pvSrcFileInfo = &hf->pFileEntry->dwFileSize; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileInfoCompressedSize: + hf = IsValidFileHandle(hMpqOrFile); + if(hf != NULL && hf->pFileEntry != NULL) + { + pvSrcFileInfo = &hf->pFileEntry->dwCmpSize; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileInfoFlags: + hf = IsValidFileHandle(hMpqOrFile); + if(hf != NULL && hf->pFileEntry != NULL) + { + pvSrcFileInfo = &hf->pFileEntry->dwFlags; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileInfoEncryptionKey: + hf = IsValidFileHandle(hMpqOrFile); + if(hf != NULL) + { + pvSrcFileInfo = &hf->dwFileKey; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + case SFileInfoEncryptionKeyRaw: + hf = IsValidFileHandle(hMpqOrFile); + if(hf != NULL && hf->pFileEntry != NULL) + { + dwInt32Value = hf->dwFileKey; + if(hf->pFileEntry->dwFlags & MPQ_FILE_FIX_KEY) + dwInt32Value = (dwInt32Value ^ hf->pFileEntry->dwFileSize) - (DWORD)hf->MpqFilePos; + pvSrcFileInfo = &dwInt32Value; + cbSrcFileInfo = sizeof(DWORD); + nInfoType = SFILE_INFO_TYPE_DIRECT_POINTER; + } + break; + + default: // Invalid info class + SetLastError(ERROR_INVALID_PARAMETER); + return false; + } + + // If we validated the handle and info class, give as much info as possible + if(nInfoType >= SFILE_INFO_TYPE_DIRECT_POINTER) + { + // Give the length needed, if wanted + if(pcbLengthNeeded != NULL) + pcbLengthNeeded[0] = cbSrcFileInfo; + + // If the caller entered an output buffer, the output size must also be entered + if(pvFileInfo != NULL && cbFileInfo != 0) + { + // Check if there is enough space in the output buffer + if(cbSrcFileInfo <= cbFileInfo) + { + switch(nInfoType) + { + case SFILE_INFO_TYPE_DIRECT_POINTER: + case SFILE_INFO_TYPE_ALLOCATED: + assert(pvSrcFileInfo != NULL); + memcpy(pvFileInfo, pvSrcFileInfo, cbSrcFileInfo); + break; + + case SFILE_INFO_TYPE_READ_FROM_FILE: + if(!FileStream_Read(ha->pStream, &ByteOffset, pvFileInfo, cbSrcFileInfo)) + nError = GetLastError(); + break; + + case SFILE_INFO_TYPE_TABLE_POINTER: + assert(pvSrcFileInfo != NULL); + *(void **)pvFileInfo = pvSrcFileInfo; + pvSrcFileInfo = NULL; + break; + + case SFILE_INFO_TYPE_FILE_ENTRY: + assert(pFileEntry != NULL); + ConvertFileEntryToSelfRelative((TFileEntry *)pvFileInfo, pFileEntry); + break; + } + } + else + { + nError = ERROR_INSUFFICIENT_BUFFER; + } + } + + // Free the file info if needed + if(nInfoType == SFILE_INFO_TYPE_ALLOCATED && pvSrcFileInfo != NULL) + STORM_FREE(pvSrcFileInfo); + if(nInfoType == SFILE_INFO_TYPE_TABLE_POINTER && pvSrcFileInfo != NULL) + SFileFreeFileInfo(pvSrcFileInfo, InfoClass); + } + else + { + // Handle error cases + if(nInfoType == SFILE_INFO_TYPE_INVALID_HANDLE) + nError = ERROR_INVALID_HANDLE; + if(nInfoType == SFILE_INFO_TYPE_NOT_FOUND) + nError = ERROR_FILE_NOT_FOUND; + } + + // Set the last error value, if needed + if(nError != ERROR_SUCCESS) + SetLastError(nError); + return (nError == ERROR_SUCCESS); +} + +bool WINAPI SFileFreeFileInfo(void * pvFileInfo, SFileInfoClass InfoClass) +{ + switch(InfoClass) + { + case SFileMpqHetTable: + FreeHetTable((TMPQHetTable *)pvFileInfo); + return true; + + case SFileMpqBetTable: + FreeBetTable((TMPQBetTable *)pvFileInfo); + return true; + + default: + break; + } + + SetLastError(ERROR_INVALID_PARAMETER); + return false; +} + +//----------------------------------------------------------------------------- +// Tries to retrieve the file name + +struct TFileHeader2Ext +{ + DWORD dwOffset00Data; // Required data at offset 00 (32-bits) + DWORD dwOffset00Mask; // Mask for data at offset 00 (32 bits). 0 = data are ignored + DWORD dwOffset04Data; // Required data at offset 04 (32-bits) + DWORD dwOffset04Mask; // Mask for data at offset 04 (32 bits). 0 = data are ignored + const char * szExt; // Supplied extension, if the condition is true +}; + +static TFileHeader2Ext data2ext[] = +{ + {0x00005A4D, 0x0000FFFF, 0x00000000, 0x00000000, "exe"}, // EXE files + {0x00000006, 0xFFFFFFFF, 0x00000001, 0xFFFFFFFF, "dc6"}, // EXE files + {0x1A51504D, 0xFFFFFFFF, 0x00000000, 0x00000000, "mpq"}, // MPQ archive header ID ('MPQ\x1A') + {0x46464952, 0xFFFFFFFF, 0x00000000, 0x00000000, "wav"}, // WAVE header 'RIFF' + {0x324B4D53, 0xFFFFFFFF, 0x00000000, 0x00000000, "smk"}, // Old "Smacker Video" files 'SMK2' + {0x694B4942, 0xFFFFFFFF, 0x00000000, 0x00000000, "bik"}, // Bink video files (new) + {0x0801050A, 0xFFFFFFFF, 0x00000000, 0x00000000, "pcx"}, // PCX images used in Diablo I + {0x544E4F46, 0xFFFFFFFF, 0x00000000, 0x00000000, "fnt"}, // Font files used in Diablo II + {0x6D74683C, 0xFFFFFFFF, 0x00000000, 0x00000000, "html"}, // HTML 'ha->pFileTable), data2ext[i].szExt); + + // Save the pseudo-name in the file entry as well + AllocateFileName(hf->ha, pFileEntry, szPseudoName); + + // If the caller wants to copy the file name, do it + if(szFileName != NULL) + strcpy(szFileName, szPseudoName); + return ERROR_SUCCESS; + } + } + } + + return ERROR_CAN_NOT_COMPLETE; +} + +bool WINAPI SFileGetFileName(HANDLE hFile, char * szFileName) +{ + TMPQFile * hf = (TMPQFile *)hFile; // MPQ File handle + int nError = ERROR_INVALID_HANDLE; + + // Check valid parameters + if(IsValidFileHandle(hFile)) + { + TFileEntry * pFileEntry = hf->pFileEntry; + + // For MPQ files, retrieve the file name from the file entry + if(hf->pStream == NULL) + { + if(pFileEntry != NULL) + { + // If the file name is not there yet, create a pseudo name + if(pFileEntry->szFileName == NULL) + nError = CreatePseudoFileName(hFile, pFileEntry, szFileName); + + // Copy the file name to the output buffer, if any + if(pFileEntry->szFileName && szFileName) + { + strcpy(szFileName, pFileEntry->szFileName); + nError = ERROR_SUCCESS; + } + } + } + + // For local files, copy the file name from the stream + else + { + if(szFileName != NULL) + { + const TCHAR * szStreamName = FileStream_GetFileName(hf->pStream); + CopyFileName(szFileName, szStreamName, _tcslen(szStreamName)); + } + nError = ERROR_SUCCESS; + } + } + + if(nError != ERROR_SUCCESS) + SetLastError(nError); + return (nError == ERROR_SUCCESS); +} + diff --git a/src/SFileListFile.cpp b/src/SFileListFile.cpp index e47a9ea..62b8f11 100644 --- a/src/SFileListFile.cpp +++ b/src/SFileListFile.cpp @@ -1,642 +1,642 @@ -/*****************************************************************************/ -/* SListFile.cpp Copyright (c) Ladislav Zezula 2004 */ -/*---------------------------------------------------------------------------*/ -/* Description: */ -/*---------------------------------------------------------------------------*/ -/* Date Ver Who Comment */ -/* -------- ---- --- ------- */ -/* 12.06.04 1.00 Lad The first version of SListFile.cpp */ -/*****************************************************************************/ - -#define __STORMLIB_SELF__ -#include "StormLib.h" -#include "StormCommon.h" -#include - -//----------------------------------------------------------------------------- -// Listfile entry structure - -#define CACHE_BUFFER_SIZE 0x1000 // Size of the cache buffer -#define MAX_LISTFILE_SIZE 0x04000000 // Maximum accepted listfile size is about 68 MB - -struct TListFileCache -{ - char * szWildCard; // Self-relative pointer to file mask - LPBYTE pBegin; // The begin of the listfile cache - LPBYTE pPos; // Current position in the cache - LPBYTE pEnd; // The last character in the file cache - -// char szWildCard[wildcard_length]; // Followed by the name mask (if any) -// char szListFile[listfile_length]; // Followed by the listfile (if any) -}; - -//----------------------------------------------------------------------------- -// Local functions (cache) - -static char * CopyListLine(char * szListLine, const char * szFileName) -{ - // Copy the string - while(szFileName[0] != 0) - *szListLine++ = *szFileName++; - - // Append the end-of-line - *szListLine++ = 0x0D; - *szListLine++ = 0x0A; - return szListLine; -} - -static bool FreeListFileCache(TListFileCache * pCache) -{ - // Valid parameter check - if(pCache != NULL) - STORM_FREE(pCache); - return true; -} - -static TListFileCache * CreateListFileCache(HANDLE hListFile, const char * szWildCard) -{ - TListFileCache * pCache = NULL; - size_t cchWildCard = 0; - DWORD dwBytesRead = 0; - DWORD dwFileSize; - - // Get the amount of bytes that need to be allocated - dwFileSize = SFileGetFileSize(hListFile, NULL); - if(dwFileSize == 0 || dwFileSize > MAX_LISTFILE_SIZE) - return NULL; - - // Append buffer for name mask, if any - if(szWildCard != NULL) - cchWildCard = strlen(szWildCard) + 1; - - // Allocate cache for one file block - pCache = (TListFileCache *)STORM_ALLOC(BYTE, sizeof(TListFileCache) + cchWildCard + dwFileSize + 1); - if(pCache != NULL) - { - // Clear the entire structure - memset(pCache, 0, sizeof(TListFileCache) + cchWildCard); - - // Shall we copy the mask? - if(cchWildCard != 0) - { - pCache->szWildCard = (char *)(pCache + 1); - memcpy(pCache->szWildCard, szWildCard, cchWildCard); - } - - // Fill-in the rest of the cache pointers - pCache->pBegin = (LPBYTE)(pCache + 1) + cchWildCard; - - // Load the entire listfile to the cache - SFileReadFile(hListFile, pCache->pBegin, dwFileSize, &dwBytesRead, NULL); - if(dwBytesRead != 0) - { - // Allocate pointers - pCache->pPos = pCache->pBegin; - pCache->pEnd = pCache->pBegin + dwBytesRead; - } - else - { - FreeListFileCache(pCache); - pCache = NULL; - } - } - - // Return the cache - return pCache; -} - -#ifdef _DEBUG -/* -TMPQNameCache * CreateNameCache(HANDLE hListFile, const char * szSearchMask) -{ - TMPQNameCache * pNameCache; - char * szCachePointer; - size_t cbToAllocate; - size_t nMaskLength = 1; - DWORD dwBytesRead = 0; - DWORD dwFileSize; - - // Get the size of the listfile. Ignore zero or too long ones - dwFileSize = SFileGetFileSize(hListFile, NULL); - if(dwFileSize == 0 || dwFileSize > MAX_LISTFILE_SIZE) - return NULL; - - // Get the length of the search mask - if(szSearchMask == NULL) - szSearchMask = "*"; - nMaskLength = strlen(szSearchMask) + 1; - - // Allocate the name cache - cbToAllocate = sizeof(TMPQNameCache) + nMaskLength + dwFileSize + 1; - pNameCache = (TMPQNameCache *)STORM_ALLOC(BYTE, cbToAllocate); - if(pNameCache != NULL) - { - // Initialize the name cache - memset(pNameCache, 0, sizeof(TMPQNameCache)); - pNameCache->TotalCacheSize = (DWORD)(nMaskLength + dwFileSize + 1); - szCachePointer = (char *)(pNameCache + 1); - - // Copy the search mask, if any - memcpy(szCachePointer, szSearchMask, nMaskLength); - pNameCache->FirstNameOffset = (DWORD)nMaskLength; - pNameCache->FreeSpaceOffset = (DWORD)nMaskLength; - - // Read the listfile itself - SFileSetFilePointer(hListFile, 0, NULL, FILE_BEGIN); - SFileReadFile(hListFile, szCachePointer + nMaskLength, dwFileSize, &dwBytesRead, NULL); - - // If nothing has been read from the listfile, clear the cache - if(dwBytesRead == 0) - { - STORM_FREE(pNameCache); - return NULL; - } - - // Move the free space offset - pNameCache->FreeSpaceOffset = pNameCache->FirstNameOffset + dwBytesRead + 1; - szCachePointer[nMaskLength + dwBytesRead] = 0; - } - - return pNameCache; -} - -static void FreeNameCache(TMPQNameCache * pNameCache) -{ - if(pNameCache != NULL) - STORM_FREE(pNameCache); - pNameCache = NULL; -} -*/ -#endif // _DEBUG - -static char * ReadListFileLine(TListFileCache * pCache, size_t * PtrLength) -{ - LPBYTE pbLineBegin; - LPBYTE pbLineEnd; - LPBYTE pbExtraString = NULL; - - // Skip newlines, spaces, tabs and another non-printable stuff - while(pCache->pPos < pCache->pEnd && pCache->pPos[0] <= 0x20) - pCache->pPos++; - - // Set the line begin and end - if(pCache->pPos >= pCache->pEnd) - return NULL; - pbLineBegin = pbLineEnd = pCache->pPos; - - // Copy the remaining characters - while(pCache->pPos < pCache->pEnd && pCache->pPos[0] != 0x0A && pCache->pPos[0] != 0x0D) - { - // Blizzard listfiles can also contain information about patch: - // Pass1\Files\MacOS\unconditional\user\Background Downloader.app\Contents\Info.plist~Patch(Data#frFR#base-frFR,1326) - if(pCache->pPos[0] == '~') - pbExtraString = pCache->pPos; - - // Copy the character - pCache->pPos++; - } - - // If there was extra string after the file name, clear it - if(pbExtraString != NULL) - { - if(pbExtraString[0] == '~' && pbExtraString[1] == 'P') - { - pbLineEnd = pbExtraString; - pbLineEnd[0] = 0; - } - } - else - { - pbLineEnd = pCache->pPos++; - pbLineEnd[0] = 0; - } - - // Give the line to the caller - if(PtrLength != NULL) - PtrLength[0] = (size_t)(pbLineEnd - pbLineBegin); - return (char *)pbLineBegin; -} - -static int CompareFileNodes(const void * p1, const void * p2) -{ - char * szFileName1 = *(char **)p1; - char * szFileName2 = *(char **)p2; - - return _stricmp(szFileName1, szFileName2); -} - -static LPBYTE CreateListFile(TMPQArchive * ha, DWORD * pcbListFile) -{ - TFileEntry * pFileTableEnd = ha->pFileTable + ha->dwFileTableSize; - TFileEntry * pFileEntry; - char ** SortTable = NULL; - char * szListFile = NULL; - char * szListLine; - size_t nFileNodes = 0; - size_t cbListFile = 0; - size_t nIndex0; - size_t nIndex1; - - // Allocate the table for sorting listfile - SortTable = STORM_ALLOC(char*, ha->dwFileTableSize); - if(SortTable == NULL) - return NULL; - - // Construct the sort table - // Note: in MPQs with multiple locale versions of the same file, - // this code causes adding multiple listfile entries. - // They will get removed after the listfile sorting - for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++) - { - // Only take existing items - if((pFileEntry->dwFlags & MPQ_FILE_EXISTS) && pFileEntry->szFileName != NULL) - { - // Ignore pseudo-names and internal names - if(!IsPseudoFileName(pFileEntry->szFileName, NULL) && !IsInternalMpqFileName(pFileEntry->szFileName)) - { - SortTable[nFileNodes++] = pFileEntry->szFileName; - } - } - } - - // Remove duplicities - if(nFileNodes > 0) - { - // Sort the table - qsort(SortTable, nFileNodes, sizeof(char *), CompareFileNodes); - - // Count the 0-th item - cbListFile += strlen(SortTable[0]) + 2; - - // Walk through the items and only use the ones that are not duplicated - for(nIndex0 = 0, nIndex1 = 1; nIndex1 < nFileNodes; nIndex1++) - { - // If the next file node is different, we will include it to the result listfile - if(_stricmp(SortTable[nIndex1], SortTable[nIndex0]) != 0) - { - cbListFile += strlen(SortTable[nIndex1]) + 2; - nIndex0 = nIndex1; - } - } - - // Now allocate buffer for the entire listfile - szListFile = szListLine = STORM_ALLOC(char, cbListFile + 1); - if(szListFile != NULL) - { - // Copy the 0-th item - szListLine = CopyListLine(szListLine, SortTable[0]); - - // Walk through the items and only use the ones that are not duplicated - for(nIndex0 = 0, nIndex1 = 1; nIndex1 < nFileNodes; nIndex1++) - { - // If the next file node is different, we will include it to the result listfile - if(_stricmp(SortTable[nIndex1], SortTable[nIndex0]) != 0) - { - // Copy the listfile line - szListLine = CopyListLine(szListLine, SortTable[nIndex1]); - nIndex0 = nIndex1; - } - } - - // Sanity check - does the size match? - assert((size_t)(szListLine - szListFile) == cbListFile); - } - } - else - { - szListFile = STORM_ALLOC(char, 1); - cbListFile = 0; - } - - // Free the sort table - STORM_FREE(SortTable); - - // Give away the listfile - if(pcbListFile != NULL) - *pcbListFile = (DWORD)cbListFile; - return (LPBYTE)szListFile; -} - -//----------------------------------------------------------------------------- -// Local functions (listfile nodes) - -// Adds a name into the list of all names. For each locale in the MPQ, -// one entry will be created -// If the file name is already there, does nothing. -static int SListFileCreateNodeForAllLocales(TMPQArchive * ha, const char * szFileName) -{ - TFileEntry * pFileEntry; - TMPQHash * pFirstHash; - TMPQHash * pHash; - - // If we have HET table, use that one - if(ha->pHetTable != NULL) - { - pFileEntry = GetFileEntryLocale(ha, szFileName, 0); - if(pFileEntry != NULL) - { - // Allocate file name for the file entry - AllocateFileName(ha, pFileEntry, szFileName); - } - - return ERROR_SUCCESS; - } - - // If we have hash table, we use it - if(ha->pHashTable != NULL) - { - // Go while we found something - pFirstHash = pHash = GetFirstHashEntry(ha, szFileName); - while(pHash != NULL) - { - // Allocate file name for the file entry - AllocateFileName(ha, ha->pFileTable + pHash->dwBlockIndex, szFileName); - - // Now find the next language version of the file - pHash = GetNextHashEntry(ha, pFirstHash, pHash); - } - - return ERROR_SUCCESS; - } - - return ERROR_CAN_NOT_COMPLETE; -} - -// Saves the whole listfile to the MPQ -int SListFileSaveToMpq(TMPQArchive * ha) -{ - TMPQFile * hf = NULL; - LPBYTE pbListFile; - DWORD cbListFile = 0; - int nError = ERROR_SUCCESS; - - // Only save the listfile if we should do so - if(ha->dwFileFlags1 != 0) - { - // At this point, we expect to have at least one reserved entry in the file table - assert(ha->dwFlags & MPQ_FLAG_LISTFILE_NEW); - assert(ha->dwReservedFiles > 0); - - // Create the raw data that is to be written to (listfile) - // Note: Creating the raw data before the (listfile) has been created in the MPQ - // causes that the name of the listfile will not be included in the listfile itself. - // That is OK, because (listfile) in Blizzard MPQs does not contain it either. - pbListFile = CreateListFile(ha, &cbListFile); - if(pbListFile != NULL) - { - // Determine the real flags for (listfile) - if(ha->dwFileFlags1 == MPQ_FILE_EXISTS) - ha->dwFileFlags1 = GetDefaultSpecialFileFlags(cbListFile, ha->pHeader->wFormatVersion); - - // Create the listfile in the MPQ - nError = SFileAddFile_Init(ha, LISTFILE_NAME, - 0, - cbListFile, - LANG_NEUTRAL, - ha->dwFileFlags1 | MPQ_FILE_REPLACEEXISTING, - &hf); - - // Write the listfile raw data to it - if(nError == ERROR_SUCCESS) - { - // Write the content of the listfile to the MPQ - nError = SFileAddFile_Write(hf, pbListFile, cbListFile, MPQ_COMPRESSION_ZLIB); - SFileAddFile_Finish(hf); - } - - // Clear the listfile flags - ha->dwFlags &= ~(MPQ_FLAG_LISTFILE_NEW | MPQ_FLAG_LISTFILE_NONE); - ha->dwReservedFiles--; - - // Free the listfile buffer - STORM_FREE(pbListFile); - } - else - { - // If the (listfile) file would be empty, its OK - nError = (cbListFile == 0) ? ERROR_SUCCESS : ERROR_NOT_ENOUGH_MEMORY; - } - } - - return nError; -} - -static int SFileAddArbitraryListFile( - TMPQArchive * ha, - HANDLE hListFile) -{ - TListFileCache * pCache = NULL; - - // Create the listfile cache for that file - pCache = CreateListFileCache(hListFile, NULL); - if(pCache != NULL) - { - char * szFileName; - size_t nLength = 0; - - // Get the next line - while((szFileName = ReadListFileLine(pCache, &nLength)) != NULL) - { - // Add the line to the MPQ - if(nLength != 0) - SListFileCreateNodeForAllLocales(ha, szFileName); - } - - // Delete the cache - FreeListFileCache(pCache); - } - - return (pCache != NULL) ? ERROR_SUCCESS : ERROR_FILE_CORRUPT; -} - -static int SFileAddExternalListFile( - TMPQArchive * ha, - HANDLE hMpq, - const char * szListFile) -{ - HANDLE hListFile; - int nError = ERROR_SUCCESS; - - // Open the external list file - if(!SFileOpenFileEx(hMpq, szListFile, SFILE_OPEN_LOCAL_FILE, &hListFile)) - return GetLastError(); - - // Add the data from the listfile to MPQ - nError = SFileAddArbitraryListFile(ha, hListFile); - SFileCloseFile(hListFile); - return nError; -} - -static int SFileAddInternalListFile( - TMPQArchive * ha, - HANDLE hMpq) -{ - TMPQHash * pFirstHash; - TMPQHash * pHash; - HANDLE hListFile; - DWORD dwFileSize; - LCID lcSaveLocale = lcFileLocale; - bool bIgnoreListFile = false; - int nError = ERROR_SUCCESS; - - // If there is hash table, we need to support multiple listfiles - // with different locales (BrooDat.mpq) - if(ha->pHashTable != NULL) - { - pFirstHash = pHash = GetFirstHashEntry(ha, LISTFILE_NAME); - while(nError == ERROR_SUCCESS && pHash != NULL) - { - // Set the prefered locale to that from list file - SFileSetLocale(pHash->lcLocale); - - // Attempt to open the file with that locale - if(SFileOpenFileEx(hMpq, LISTFILE_NAME, 0, &hListFile)) - { - // If the archive is a malformed map, ignore too large listfiles - if(ha->dwFlags & MPQ_FLAG_MALFORMED) - { - dwFileSize = SFileGetFileSize(hListFile, NULL); - bIgnoreListFile = (dwFileSize > 0x40000); - } - - // Add the data from the listfile to MPQ - if(bIgnoreListFile == false) - nError = SFileAddArbitraryListFile(ha, hListFile); - SFileCloseFile(hListFile); - } - - // Restore the original locale - SFileSetLocale(lcSaveLocale); - - // Move to the next hash - pHash = GetNextHashEntry(ha, pFirstHash, pHash); - } - } - else - { - // Open the external list file - if(SFileOpenFileEx(hMpq, LISTFILE_NAME, 0, &hListFile)) - { - // Add the data from the listfile to MPQ - // The function also closes the listfile handle - nError = SFileAddArbitraryListFile(ha, hListFile); - SFileCloseFile(hListFile); - } - } - - // Return the result of the operation - return nError; -} - -static bool DoListFileSearch(TListFileCache * pCache, SFILE_FIND_DATA * lpFindFileData) -{ - // Check for the valid search handle - if(pCache != NULL) - { - char * szFileName; - size_t nLength = 0; - - // Get the next line - while((szFileName = ReadListFileLine(pCache, &nLength)) != NULL) - { - // Check search mask - if(nLength != 0 && CheckWildCard(szFileName, pCache->szWildCard)) - { - if(nLength >= sizeof(lpFindFileData->cFileName)) - nLength = sizeof(lpFindFileData->cFileName) - 1; - - memcpy(lpFindFileData->cFileName, szFileName, nLength); - lpFindFileData->cFileName[nLength] = 0; - return true; - } - } - } - - // No more files - memset(lpFindFileData, 0, sizeof(SFILE_FIND_DATA)); - SetLastError(ERROR_NO_MORE_FILES); - return false; -} - -//----------------------------------------------------------------------------- -// File functions - -// Adds a listfile into the MPQ archive. -int WINAPI SFileAddListFile(HANDLE hMpq, const char * szListFile) -{ - TMPQArchive * ha = (TMPQArchive *)hMpq; - int nError = ERROR_SUCCESS; - - // Add the listfile for each MPQ in the patch chain - while(ha != NULL) - { - if(szListFile != NULL) - nError = SFileAddExternalListFile(ha, hMpq, szListFile); - else - nError = SFileAddInternalListFile(ha, hMpq); - - // Also, add three special files to the listfile: - // (listfile) itself, (attributes) and (signature) - SListFileCreateNodeForAllLocales(ha, LISTFILE_NAME); - SListFileCreateNodeForAllLocales(ha, SIGNATURE_NAME); - SListFileCreateNodeForAllLocales(ha, ATTRIBUTES_NAME); - - // Move to the next archive in the chain - ha = ha->haPatch; - } - - return nError; -} - -//----------------------------------------------------------------------------- -// Enumerating files in listfile - -HANDLE WINAPI SListFileFindFirstFile(HANDLE hMpq, const char * szListFile, const char * szMask, SFILE_FIND_DATA * lpFindFileData) -{ - TListFileCache * pCache = NULL; - HANDLE hListFile = NULL; - DWORD dwSearchScope = SFILE_OPEN_LOCAL_FILE; - - // Initialize the structure with zeros - memset(lpFindFileData, 0, sizeof(SFILE_FIND_DATA)); - - // If the szListFile is NULL, it means we have to open internal listfile - if(szListFile == NULL) - { - // Use SFILE_OPEN_ANY_LOCALE for listfile. This will allow us to load - // the listfile even if there is only non-neutral version of the listfile in the MPQ - dwSearchScope = SFILE_OPEN_ANY_LOCALE; - szListFile = LISTFILE_NAME; - } - - // Open the local/internal listfile - if(SFileOpenFileEx(hMpq, szListFile, dwSearchScope, &hListFile)) - { - pCache = CreateListFileCache(hListFile, szMask); - SFileCloseFile(hListFile); - } - - if(!DoListFileSearch(pCache, lpFindFileData)) - { - memset(lpFindFileData, 0, sizeof(SFILE_FIND_DATA)); - SetLastError(ERROR_NO_MORE_FILES); - FreeListFileCache(pCache); - pCache = NULL; - } - - // Return the listfile cache as handle - return (HANDLE)pCache; -} - -bool WINAPI SListFileFindNextFile(HANDLE hFind, SFILE_FIND_DATA * lpFindFileData) -{ - return DoListFileSearch((TListFileCache *)hFind, lpFindFileData); -} - -bool WINAPI SListFileFindClose(HANDLE hFind) -{ - TListFileCache * pCache = (TListFileCache *)hFind; - - return FreeListFileCache(pCache); -} - +/*****************************************************************************/ +/* SListFile.cpp Copyright (c) Ladislav Zezula 2004 */ +/*---------------------------------------------------------------------------*/ +/* Description: */ +/*---------------------------------------------------------------------------*/ +/* Date Ver Who Comment */ +/* -------- ---- --- ------- */ +/* 12.06.04 1.00 Lad The first version of SListFile.cpp */ +/*****************************************************************************/ + +#define __STORMLIB_SELF__ +#include "StormLib.h" +#include "StormCommon.h" +#include + +//----------------------------------------------------------------------------- +// Listfile entry structure + +#define CACHE_BUFFER_SIZE 0x1000 // Size of the cache buffer +#define MAX_LISTFILE_SIZE 0x04000000 // Maximum accepted listfile size is about 68 MB + +struct TListFileCache +{ + char * szWildCard; // Self-relative pointer to file mask + LPBYTE pBegin; // The begin of the listfile cache + LPBYTE pPos; // Current position in the cache + LPBYTE pEnd; // The last character in the file cache + +// char szWildCard[wildcard_length]; // Followed by the name mask (if any) +// char szListFile[listfile_length]; // Followed by the listfile (if any) +}; + +//----------------------------------------------------------------------------- +// Local functions (cache) + +static char * CopyListLine(char * szListLine, const char * szFileName) +{ + // Copy the string + while(szFileName[0] != 0) + *szListLine++ = *szFileName++; + + // Append the end-of-line + *szListLine++ = 0x0D; + *szListLine++ = 0x0A; + return szListLine; +} + +static bool FreeListFileCache(TListFileCache * pCache) +{ + // Valid parameter check + if(pCache != NULL) + STORM_FREE(pCache); + return true; +} + +static TListFileCache * CreateListFileCache(HANDLE hListFile, const char * szWildCard) +{ + TListFileCache * pCache = NULL; + size_t cchWildCard = 0; + DWORD dwBytesRead = 0; + DWORD dwFileSize; + + // Get the amount of bytes that need to be allocated + dwFileSize = SFileGetFileSize(hListFile, NULL); + if(dwFileSize == 0 || dwFileSize > MAX_LISTFILE_SIZE) + return NULL; + + // Append buffer for name mask, if any + if(szWildCard != NULL) + cchWildCard = strlen(szWildCard) + 1; + + // Allocate cache for one file block + pCache = (TListFileCache *)STORM_ALLOC(BYTE, sizeof(TListFileCache) + cchWildCard + dwFileSize + 1); + if(pCache != NULL) + { + // Clear the entire structure + memset(pCache, 0, sizeof(TListFileCache) + cchWildCard); + + // Shall we copy the mask? + if(cchWildCard != 0) + { + pCache->szWildCard = (char *)(pCache + 1); + memcpy(pCache->szWildCard, szWildCard, cchWildCard); + } + + // Fill-in the rest of the cache pointers + pCache->pBegin = (LPBYTE)(pCache + 1) + cchWildCard; + + // Load the entire listfile to the cache + SFileReadFile(hListFile, pCache->pBegin, dwFileSize, &dwBytesRead, NULL); + if(dwBytesRead != 0) + { + // Allocate pointers + pCache->pPos = pCache->pBegin; + pCache->pEnd = pCache->pBegin + dwBytesRead; + } + else + { + FreeListFileCache(pCache); + pCache = NULL; + } + } + + // Return the cache + return pCache; +} + +#ifdef _DEBUG +/* +TMPQNameCache * CreateNameCache(HANDLE hListFile, const char * szSearchMask) +{ + TMPQNameCache * pNameCache; + char * szCachePointer; + size_t cbToAllocate; + size_t nMaskLength = 1; + DWORD dwBytesRead = 0; + DWORD dwFileSize; + + // Get the size of the listfile. Ignore zero or too long ones + dwFileSize = SFileGetFileSize(hListFile, NULL); + if(dwFileSize == 0 || dwFileSize > MAX_LISTFILE_SIZE) + return NULL; + + // Get the length of the search mask + if(szSearchMask == NULL) + szSearchMask = "*"; + nMaskLength = strlen(szSearchMask) + 1; + + // Allocate the name cache + cbToAllocate = sizeof(TMPQNameCache) + nMaskLength + dwFileSize + 1; + pNameCache = (TMPQNameCache *)STORM_ALLOC(BYTE, cbToAllocate); + if(pNameCache != NULL) + { + // Initialize the name cache + memset(pNameCache, 0, sizeof(TMPQNameCache)); + pNameCache->TotalCacheSize = (DWORD)(nMaskLength + dwFileSize + 1); + szCachePointer = (char *)(pNameCache + 1); + + // Copy the search mask, if any + memcpy(szCachePointer, szSearchMask, nMaskLength); + pNameCache->FirstNameOffset = (DWORD)nMaskLength; + pNameCache->FreeSpaceOffset = (DWORD)nMaskLength; + + // Read the listfile itself + SFileSetFilePointer(hListFile, 0, NULL, FILE_BEGIN); + SFileReadFile(hListFile, szCachePointer + nMaskLength, dwFileSize, &dwBytesRead, NULL); + + // If nothing has been read from the listfile, clear the cache + if(dwBytesRead == 0) + { + STORM_FREE(pNameCache); + return NULL; + } + + // Move the free space offset + pNameCache->FreeSpaceOffset = pNameCache->FirstNameOffset + dwBytesRead + 1; + szCachePointer[nMaskLength + dwBytesRead] = 0; + } + + return pNameCache; +} + +static void FreeNameCache(TMPQNameCache * pNameCache) +{ + if(pNameCache != NULL) + STORM_FREE(pNameCache); + pNameCache = NULL; +} +*/ +#endif // _DEBUG + +static char * ReadListFileLine(TListFileCache * pCache, size_t * PtrLength) +{ + LPBYTE pbLineBegin; + LPBYTE pbLineEnd; + LPBYTE pbExtraString = NULL; + + // Skip newlines, spaces, tabs and another non-printable stuff + while(pCache->pPos < pCache->pEnd && pCache->pPos[0] <= 0x20) + pCache->pPos++; + + // Set the line begin and end + if(pCache->pPos >= pCache->pEnd) + return NULL; + pbLineBegin = pbLineEnd = pCache->pPos; + + // Copy the remaining characters + while(pCache->pPos < pCache->pEnd && pCache->pPos[0] != 0x0A && pCache->pPos[0] != 0x0D) + { + // Blizzard listfiles can also contain information about patch: + // Pass1\Files\MacOS\unconditional\user\Background Downloader.app\Contents\Info.plist~Patch(Data#frFR#base-frFR,1326) + if(pCache->pPos[0] == '~') + pbExtraString = pCache->pPos; + + // Copy the character + pCache->pPos++; + } + + // If there was extra string after the file name, clear it + if(pbExtraString != NULL) + { + if(pbExtraString[0] == '~' && pbExtraString[1] == 'P') + { + pbLineEnd = pbExtraString; + pbLineEnd[0] = 0; + } + } + else + { + pbLineEnd = pCache->pPos++; + pbLineEnd[0] = 0; + } + + // Give the line to the caller + if(PtrLength != NULL) + PtrLength[0] = (size_t)(pbLineEnd - pbLineBegin); + return (char *)pbLineBegin; +} + +static int CompareFileNodes(const void * p1, const void * p2) +{ + char * szFileName1 = *(char **)p1; + char * szFileName2 = *(char **)p2; + + return _stricmp(szFileName1, szFileName2); +} + +static LPBYTE CreateListFile(TMPQArchive * ha, DWORD * pcbListFile) +{ + TFileEntry * pFileTableEnd = ha->pFileTable + ha->dwFileTableSize; + TFileEntry * pFileEntry; + char ** SortTable = NULL; + char * szListFile = NULL; + char * szListLine; + size_t nFileNodes = 0; + size_t cbListFile = 0; + size_t nIndex0; + size_t nIndex1; + + // Allocate the table for sorting listfile + SortTable = STORM_ALLOC(char*, ha->dwFileTableSize); + if(SortTable == NULL) + return NULL; + + // Construct the sort table + // Note: in MPQs with multiple locale versions of the same file, + // this code causes adding multiple listfile entries. + // They will get removed after the listfile sorting + for(pFileEntry = ha->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++) + { + // Only take existing items + if((pFileEntry->dwFlags & MPQ_FILE_EXISTS) && pFileEntry->szFileName != NULL) + { + // Ignore pseudo-names and internal names + if(!IsPseudoFileName(pFileEntry->szFileName, NULL) && !IsInternalMpqFileName(pFileEntry->szFileName)) + { + SortTable[nFileNodes++] = pFileEntry->szFileName; + } + } + } + + // Remove duplicities + if(nFileNodes > 0) + { + // Sort the table + qsort(SortTable, nFileNodes, sizeof(char *), CompareFileNodes); + + // Count the 0-th item + cbListFile += strlen(SortTable[0]) + 2; + + // Walk through the items and only use the ones that are not duplicated + for(nIndex0 = 0, nIndex1 = 1; nIndex1 < nFileNodes; nIndex1++) + { + // If the next file node is different, we will include it to the result listfile + if(_stricmp(SortTable[nIndex1], SortTable[nIndex0]) != 0) + { + cbListFile += strlen(SortTable[nIndex1]) + 2; + nIndex0 = nIndex1; + } + } + + // Now allocate buffer for the entire listfile + szListFile = szListLine = STORM_ALLOC(char, cbListFile + 1); + if(szListFile != NULL) + { + // Copy the 0-th item + szListLine = CopyListLine(szListLine, SortTable[0]); + + // Walk through the items and only use the ones that are not duplicated + for(nIndex0 = 0, nIndex1 = 1; nIndex1 < nFileNodes; nIndex1++) + { + // If the next file node is different, we will include it to the result listfile + if(_stricmp(SortTable[nIndex1], SortTable[nIndex0]) != 0) + { + // Copy the listfile line + szListLine = CopyListLine(szListLine, SortTable[nIndex1]); + nIndex0 = nIndex1; + } + } + + // Sanity check - does the size match? + assert((size_t)(szListLine - szListFile) == cbListFile); + } + } + else + { + szListFile = STORM_ALLOC(char, 1); + cbListFile = 0; + } + + // Free the sort table + STORM_FREE(SortTable); + + // Give away the listfile + if(pcbListFile != NULL) + *pcbListFile = (DWORD)cbListFile; + return (LPBYTE)szListFile; +} + +//----------------------------------------------------------------------------- +// Local functions (listfile nodes) + +// Adds a name into the list of all names. For each locale in the MPQ, +// one entry will be created +// If the file name is already there, does nothing. +static int SListFileCreateNodeForAllLocales(TMPQArchive * ha, const char * szFileName) +{ + TFileEntry * pFileEntry; + TMPQHash * pFirstHash; + TMPQHash * pHash; + + // If we have HET table, use that one + if(ha->pHetTable != NULL) + { + pFileEntry = GetFileEntryLocale(ha, szFileName, 0); + if(pFileEntry != NULL) + { + // Allocate file name for the file entry + AllocateFileName(ha, pFileEntry, szFileName); + } + + return ERROR_SUCCESS; + } + + // If we have hash table, we use it + if(ha->pHashTable != NULL) + { + // Go while we found something + pFirstHash = pHash = GetFirstHashEntry(ha, szFileName); + while(pHash != NULL) + { + // Allocate file name for the file entry + AllocateFileName(ha, ha->pFileTable + pHash->dwBlockIndex, szFileName); + + // Now find the next language version of the file + pHash = GetNextHashEntry(ha, pFirstHash, pHash); + } + + return ERROR_SUCCESS; + } + + return ERROR_CAN_NOT_COMPLETE; +} + +// Saves the whole listfile to the MPQ +int SListFileSaveToMpq(TMPQArchive * ha) +{ + TMPQFile * hf = NULL; + LPBYTE pbListFile; + DWORD cbListFile = 0; + int nError = ERROR_SUCCESS; + + // Only save the listfile if we should do so + if(ha->dwFileFlags1 != 0) + { + // At this point, we expect to have at least one reserved entry in the file table + assert(ha->dwFlags & MPQ_FLAG_LISTFILE_NEW); + assert(ha->dwReservedFiles > 0); + + // Create the raw data that is to be written to (listfile) + // Note: Creating the raw data before the (listfile) has been created in the MPQ + // causes that the name of the listfile will not be included in the listfile itself. + // That is OK, because (listfile) in Blizzard MPQs does not contain it either. + pbListFile = CreateListFile(ha, &cbListFile); + if(pbListFile != NULL) + { + // Determine the real flags for (listfile) + if(ha->dwFileFlags1 == MPQ_FILE_EXISTS) + ha->dwFileFlags1 = GetDefaultSpecialFileFlags(cbListFile, ha->pHeader->wFormatVersion); + + // Create the listfile in the MPQ + nError = SFileAddFile_Init(ha, LISTFILE_NAME, + 0, + cbListFile, + LANG_NEUTRAL, + ha->dwFileFlags1 | MPQ_FILE_REPLACEEXISTING, + &hf); + + // Write the listfile raw data to it + if(nError == ERROR_SUCCESS) + { + // Write the content of the listfile to the MPQ + nError = SFileAddFile_Write(hf, pbListFile, cbListFile, MPQ_COMPRESSION_ZLIB); + SFileAddFile_Finish(hf); + } + + // Clear the listfile flags + ha->dwFlags &= ~(MPQ_FLAG_LISTFILE_NEW | MPQ_FLAG_LISTFILE_NONE); + ha->dwReservedFiles--; + + // Free the listfile buffer + STORM_FREE(pbListFile); + } + else + { + // If the (listfile) file would be empty, its OK + nError = (cbListFile == 0) ? ERROR_SUCCESS : ERROR_NOT_ENOUGH_MEMORY; + } + } + + return nError; +} + +static int SFileAddArbitraryListFile( + TMPQArchive * ha, + HANDLE hListFile) +{ + TListFileCache * pCache = NULL; + + // Create the listfile cache for that file + pCache = CreateListFileCache(hListFile, NULL); + if(pCache != NULL) + { + char * szFileName; + size_t nLength = 0; + + // Get the next line + while((szFileName = ReadListFileLine(pCache, &nLength)) != NULL) + { + // Add the line to the MPQ + if(nLength != 0) + SListFileCreateNodeForAllLocales(ha, szFileName); + } + + // Delete the cache + FreeListFileCache(pCache); + } + + return (pCache != NULL) ? ERROR_SUCCESS : ERROR_FILE_CORRUPT; +} + +static int SFileAddExternalListFile( + TMPQArchive * ha, + HANDLE hMpq, + const char * szListFile) +{ + HANDLE hListFile; + int nError = ERROR_SUCCESS; + + // Open the external list file + if(!SFileOpenFileEx(hMpq, szListFile, SFILE_OPEN_LOCAL_FILE, &hListFile)) + return GetLastError(); + + // Add the data from the listfile to MPQ + nError = SFileAddArbitraryListFile(ha, hListFile); + SFileCloseFile(hListFile); + return nError; +} + +static int SFileAddInternalListFile( + TMPQArchive * ha, + HANDLE hMpq) +{ + TMPQHash * pFirstHash; + TMPQHash * pHash; + HANDLE hListFile; + DWORD dwFileSize; + LCID lcSaveLocale = lcFileLocale; + bool bIgnoreListFile = false; + int nError = ERROR_SUCCESS; + + // If there is hash table, we need to support multiple listfiles + // with different locales (BrooDat.mpq) + if(ha->pHashTable != NULL) + { + pFirstHash = pHash = GetFirstHashEntry(ha, LISTFILE_NAME); + while(nError == ERROR_SUCCESS && pHash != NULL) + { + // Set the prefered locale to that from list file + SFileSetLocale(pHash->lcLocale); + + // Attempt to open the file with that locale + if(SFileOpenFileEx(hMpq, LISTFILE_NAME, 0, &hListFile)) + { + // If the archive is a malformed map, ignore too large listfiles + if(ha->dwFlags & MPQ_FLAG_MALFORMED) + { + dwFileSize = SFileGetFileSize(hListFile, NULL); + bIgnoreListFile = (dwFileSize > 0x40000); + } + + // Add the data from the listfile to MPQ + if(bIgnoreListFile == false) + nError = SFileAddArbitraryListFile(ha, hListFile); + SFileCloseFile(hListFile); + } + + // Restore the original locale + SFileSetLocale(lcSaveLocale); + + // Move to the next hash + pHash = GetNextHashEntry(ha, pFirstHash, pHash); + } + } + else + { + // Open the external list file + if(SFileOpenFileEx(hMpq, LISTFILE_NAME, 0, &hListFile)) + { + // Add the data from the listfile to MPQ + // The function also closes the listfile handle + nError = SFileAddArbitraryListFile(ha, hListFile); + SFileCloseFile(hListFile); + } + } + + // Return the result of the operation + return nError; +} + +static bool DoListFileSearch(TListFileCache * pCache, SFILE_FIND_DATA * lpFindFileData) +{ + // Check for the valid search handle + if(pCache != NULL) + { + char * szFileName; + size_t nLength = 0; + + // Get the next line + while((szFileName = ReadListFileLine(pCache, &nLength)) != NULL) + { + // Check search mask + if(nLength != 0 && CheckWildCard(szFileName, pCache->szWildCard)) + { + if(nLength >= sizeof(lpFindFileData->cFileName)) + nLength = sizeof(lpFindFileData->cFileName) - 1; + + memcpy(lpFindFileData->cFileName, szFileName, nLength); + lpFindFileData->cFileName[nLength] = 0; + return true; + } + } + } + + // No more files + memset(lpFindFileData, 0, sizeof(SFILE_FIND_DATA)); + SetLastError(ERROR_NO_MORE_FILES); + return false; +} + +//----------------------------------------------------------------------------- +// File functions + +// Adds a listfile into the MPQ archive. +int WINAPI SFileAddListFile(HANDLE hMpq, const char * szListFile) +{ + TMPQArchive * ha = (TMPQArchive *)hMpq; + int nError = ERROR_SUCCESS; + + // Add the listfile for each MPQ in the patch chain + while(ha != NULL) + { + if(szListFile != NULL) + nError = SFileAddExternalListFile(ha, hMpq, szListFile); + else + nError = SFileAddInternalListFile(ha, hMpq); + + // Also, add three special files to the listfile: + // (listfile) itself, (attributes) and (signature) + SListFileCreateNodeForAllLocales(ha, LISTFILE_NAME); + SListFileCreateNodeForAllLocales(ha, SIGNATURE_NAME); + SListFileCreateNodeForAllLocales(ha, ATTRIBUTES_NAME); + + // Move to the next archive in the chain + ha = ha->haPatch; + } + + return nError; +} + +//----------------------------------------------------------------------------- +// Enumerating files in listfile + +HANDLE WINAPI SListFileFindFirstFile(HANDLE hMpq, const char * szListFile, const char * szMask, SFILE_FIND_DATA * lpFindFileData) +{ + TListFileCache * pCache = NULL; + HANDLE hListFile = NULL; + DWORD dwSearchScope = SFILE_OPEN_LOCAL_FILE; + + // Initialize the structure with zeros + memset(lpFindFileData, 0, sizeof(SFILE_FIND_DATA)); + + // If the szListFile is NULL, it means we have to open internal listfile + if(szListFile == NULL) + { + // Use SFILE_OPEN_ANY_LOCALE for listfile. This will allow us to load + // the listfile even if there is only non-neutral version of the listfile in the MPQ + dwSearchScope = SFILE_OPEN_ANY_LOCALE; + szListFile = LISTFILE_NAME; + } + + // Open the local/internal listfile + if(SFileOpenFileEx(hMpq, szListFile, dwSearchScope, &hListFile)) + { + pCache = CreateListFileCache(hListFile, szMask); + SFileCloseFile(hListFile); + } + + if(!DoListFileSearch(pCache, lpFindFileData)) + { + memset(lpFindFileData, 0, sizeof(SFILE_FIND_DATA)); + SetLastError(ERROR_NO_MORE_FILES); + FreeListFileCache(pCache); + pCache = NULL; + } + + // Return the listfile cache as handle + return (HANDLE)pCache; +} + +bool WINAPI SListFileFindNextFile(HANDLE hFind, SFILE_FIND_DATA * lpFindFileData) +{ + return DoListFileSearch((TListFileCache *)hFind, lpFindFileData); +} + +bool WINAPI SListFileFindClose(HANDLE hFind) +{ + TListFileCache * pCache = (TListFileCache *)hFind; + + return FreeListFileCache(pCache); +} + diff --git a/src/SFilePatchArchives.cpp b/src/SFilePatchArchives.cpp index 9cb6ecd..7c6515e 100644 --- a/src/SFilePatchArchives.cpp +++ b/src/SFilePatchArchives.cpp @@ -1,937 +1,937 @@ -/*****************************************************************************/ -/* SFilePatchArchives.cpp Copyright (c) Ladislav Zezula 2010 */ -/*---------------------------------------------------------------------------*/ -/* Description: */ -/*---------------------------------------------------------------------------*/ -/* Date Ver Who Comment */ -/* -------- ---- --- ------- */ -/* 18.08.10 1.00 Lad The first version of SFilePatchArchives.cpp */ -/*****************************************************************************/ - -#define __STORMLIB_SELF__ -#include "StormLib.h" -#include "StormCommon.h" - -//----------------------------------------------------------------------------- -// Local structures - -#define PATCH_SIGNATURE_HEADER 0x48435450 -#define PATCH_SIGNATURE_MD5 0x5f35444d -#define PATCH_SIGNATURE_XFRM 0x4d524658 - -#define SIZE_OF_XFRM_HEADER 0x0C - -// Header for incremental patch files -typedef struct _MPQ_PATCH_HEADER -{ - //-- PATCH header ----------------------------------- - DWORD dwSignature; // 'PTCH' - DWORD dwSizeOfPatchData; // Size of the entire patch (decompressed) - DWORD dwSizeBeforePatch; // Size of the file before patch - DWORD dwSizeAfterPatch; // Size of file after patch - - //-- MD5 block -------------------------------------- - DWORD dwMD5; // 'MD5_' - DWORD dwMd5BlockSize; // Size of the MD5 block, including the signature and size itself - BYTE md5_before_patch[0x10]; // MD5 of the original (unpached) file - BYTE md5_after_patch[0x10]; // MD5 of the patched file - - //-- XFRM block ------------------------------------- - DWORD dwXFRM; // 'XFRM' - DWORD dwXfrmBlockSize; // Size of the XFRM block, includes XFRM header and patch data - DWORD dwPatchType; // Type of patch ('BSD0' or 'COPY') - - // Followed by the patch data -} MPQ_PATCH_HEADER, *PMPQ_PATCH_HEADER; - -typedef struct _BLIZZARD_BSDIFF40_FILE -{ - ULONGLONG Signature; - ULONGLONG CtrlBlockSize; - ULONGLONG DataBlockSize; - ULONGLONG NewFileSize; -} BLIZZARD_BSDIFF40_FILE, *PBLIZZARD_BSDIFF40_FILE; - -typedef struct _BSDIFF_CTRL_BLOCK -{ - DWORD dwAddDataLength; - DWORD dwMovDataLength; - DWORD dwOldMoveLength; - -} BSDIFF_CTRL_BLOCK, *PBSDIFF_CTRL_BLOCK; - -typedef struct _LOCALIZED_MPQ_INFO -{ - const char * szNameTemplate; // Name template - size_t nLangOffset; // Offset of the language - size_t nLength; // Length of the name template -} LOCALIZED_MPQ_INFO, *PLOCALIZED_MPQ_INFO; - -//----------------------------------------------------------------------------- -// Local variables - -// 4-byte groups for all languages -static const char * LanguageList = "baseteenenUSenGBenCNenTWdeDEesESesMXfrFRitITkoKRptBRptPTruRUzhCNzhTW"; - -// List of localized MPQs for World of Warcraft -static LOCALIZED_MPQ_INFO LocaleMpqs_WoW[] = -{ - {"expansion1-locale-####", 18, 22}, - {"expansion1-speech-####", 18, 22}, - {"expansion2-locale-####", 18, 22}, - {"expansion2-speech-####", 18, 22}, - {"expansion3-locale-####", 18, 22}, - {"expansion3-speech-####", 18, 22}, - {"locale-####", 7, 11}, - {"speech-####", 7, 11}, - {NULL, 0, 0} -}; - -//----------------------------------------------------------------------------- -// Local functions - -static inline bool IsPatchMetadataFile(TFileEntry * pFileEntry) -{ - // The file must ave a name - if(pFileEntry->szFileName != NULL && (pFileEntry->dwFlags & MPQ_FILE_PATCH_FILE) == 0) - { - // The file must be small - if(0 < pFileEntry->dwFileSize && pFileEntry->dwFileSize < 0x40) - { - // Compare the plain name - return (_stricmp(GetPlainFileName(pFileEntry->szFileName), PATCH_METADATA_NAME) == 0); - } - } - - // Not a patch_metadata - return false; -} - -static void Decompress_RLE(LPBYTE pbDecompressed, DWORD cbDecompressed, LPBYTE pbCompressed, DWORD cbCompressed) -{ - LPBYTE pbDecompressedEnd = pbDecompressed + cbDecompressed; - LPBYTE pbCompressedEnd = pbCompressed + cbCompressed; - BYTE RepeatCount; - BYTE OneByte; - - // Cut the initial DWORD from the compressed chunk - pbCompressed += sizeof(DWORD); - - // Pre-fill decompressed buffer with zeros - memset(pbDecompressed, 0, cbDecompressed); - - // Unpack - while(pbCompressed < pbCompressedEnd && pbDecompressed < pbDecompressedEnd) - { - OneByte = *pbCompressed++; - - // Is it a repetition byte ? - if(OneByte & 0x80) - { - RepeatCount = (OneByte & 0x7F) + 1; - for(BYTE i = 0; i < RepeatCount; i++) - { - if(pbDecompressed == pbDecompressedEnd || pbCompressed == pbCompressedEnd) - break; - - *pbDecompressed++ = *pbCompressed++; - } - } - else - { - pbDecompressed += (OneByte + 1); - } - } -} - -static int LoadFilePatch_COPY(TMPQFile * hf, PMPQ_PATCH_HEADER pFullPatch) -{ - DWORD cbBytesToRead = pFullPatch->dwSizeOfPatchData - sizeof(MPQ_PATCH_HEADER); - DWORD cbBytesRead = 0; - - // Simply load the rest of the patch - SFileReadFile((HANDLE)hf, (pFullPatch + 1), cbBytesToRead, &cbBytesRead, NULL); - return (cbBytesRead == cbBytesToRead) ? ERROR_SUCCESS : ERROR_FILE_CORRUPT; -} - -static int LoadFilePatch_BSD0(TMPQFile * hf, PMPQ_PATCH_HEADER pFullPatch) -{ - LPBYTE pbDecompressed = (LPBYTE)(pFullPatch + 1); - LPBYTE pbCompressed = NULL; - DWORD cbDecompressed = 0; - DWORD cbCompressed = 0; - DWORD dwBytesRead = 0; - int nError = ERROR_SUCCESS; - - // Calculate the size of compressed data - cbDecompressed = pFullPatch->dwSizeOfPatchData - sizeof(MPQ_PATCH_HEADER); - cbCompressed = pFullPatch->dwXfrmBlockSize - SIZE_OF_XFRM_HEADER; - - // Is that file compressed? - if(cbCompressed < cbDecompressed) - { - pbCompressed = STORM_ALLOC(BYTE, cbCompressed); - if(pbCompressed == NULL) - nError = ERROR_NOT_ENOUGH_MEMORY; - - // Read the compressed patch data - if(nError == ERROR_SUCCESS) - { - SFileReadFile((HANDLE)hf, pbCompressed, cbCompressed, &dwBytesRead, NULL); - if(dwBytesRead != cbCompressed) - nError = ERROR_FILE_CORRUPT; - } - - // Decompress the data - if(nError == ERROR_SUCCESS) - Decompress_RLE(pbDecompressed, cbDecompressed, pbCompressed, cbCompressed); - - if(pbCompressed != NULL) - STORM_FREE(pbCompressed); - } - else - { - SFileReadFile((HANDLE)hf, pbDecompressed, cbDecompressed, &dwBytesRead, NULL); - if(dwBytesRead != cbDecompressed) - nError = ERROR_FILE_CORRUPT; - } - - return nError; -} - -static int ApplyFilePatch_COPY( - TMPQPatcher * pPatcher, - PMPQ_PATCH_HEADER pFullPatch, - LPBYTE pbTarget, - LPBYTE pbSource) -{ - // Sanity checks - assert(pPatcher->cbMaxFileData >= pPatcher->cbFileData); - pFullPatch = pFullPatch; - - // Copy the patch data as-is - memcpy(pbTarget, pbSource, pPatcher->cbFileData); - return ERROR_SUCCESS; -} - -static int ApplyFilePatch_BSD0( - TMPQPatcher * pPatcher, - PMPQ_PATCH_HEADER pFullPatch, - LPBYTE pbTarget, - LPBYTE pbSource) -{ - PBLIZZARD_BSDIFF40_FILE pBsdiff; - PBSDIFF_CTRL_BLOCK pCtrlBlock; - LPBYTE pbPatchData = (LPBYTE)(pFullPatch + 1); - LPBYTE pDataBlock; - LPBYTE pExtraBlock; - LPBYTE pbOldData = pbSource; - LPBYTE pbNewData = pbTarget; - DWORD dwCombineSize; - DWORD dwNewOffset = 0; // Current position to patch - DWORD dwOldOffset = 0; // Current source position - DWORD dwNewSize; // Patched file size - DWORD dwOldSize = pPatcher->cbFileData; // File size before patch - - // Get pointer to the patch header - // Format of BSDIFF header corresponds to original BSDIFF, which is: - // 0000 8 bytes signature "BSDIFF40" - // 0008 8 bytes size of the control block - // 0010 8 bytes size of the data block - // 0018 8 bytes new size of the patched file - pBsdiff = (PBLIZZARD_BSDIFF40_FILE)pbPatchData; - pbPatchData += sizeof(BLIZZARD_BSDIFF40_FILE); - - // Get pointer to the 32-bit BSDIFF control block - // The control block follows immediately after the BSDIFF header - // and consists of three 32-bit integers - // 0000 4 bytes Length to copy from the BSDIFF data block the new file - // 0004 4 bytes Length to copy from the BSDIFF extra block - // 0008 4 bytes Size to increment source file offset - pCtrlBlock = (PBSDIFF_CTRL_BLOCK)pbPatchData; - pbPatchData += (size_t)BSWAP_INT64_UNSIGNED(pBsdiff->CtrlBlockSize); - - // Get the pointer to the data block - pDataBlock = (LPBYTE)pbPatchData; - pbPatchData += (size_t)BSWAP_INT64_UNSIGNED(pBsdiff->DataBlockSize); - - // Get the pointer to the extra block - pExtraBlock = (LPBYTE)pbPatchData; - dwNewSize = (DWORD)BSWAP_INT64_UNSIGNED(pBsdiff->NewFileSize); - - // Now patch the file - while(dwNewOffset < dwNewSize) - { - DWORD dwAddDataLength = BSWAP_INT32_UNSIGNED(pCtrlBlock->dwAddDataLength); - DWORD dwMovDataLength = BSWAP_INT32_UNSIGNED(pCtrlBlock->dwMovDataLength); - DWORD dwOldMoveLength = BSWAP_INT32_UNSIGNED(pCtrlBlock->dwOldMoveLength); - DWORD i; - - // Sanity check - if((dwNewOffset + dwAddDataLength) > dwNewSize) - return ERROR_FILE_CORRUPT; - - // Read the diff string to the target buffer - memcpy(pbNewData + dwNewOffset, pDataBlock, dwAddDataLength); - pDataBlock += dwAddDataLength; - - // Get the longest block that we can combine - dwCombineSize = ((dwOldOffset + dwAddDataLength) >= dwOldSize) ? (dwOldSize - dwOldOffset) : dwAddDataLength; - - // Now combine the patch data with the original file - for(i = 0; i < dwCombineSize; i++) - pbNewData[dwNewOffset + i] = pbNewData[dwNewOffset + i] + pbOldData[dwOldOffset + i]; - - // Move the offsets - dwNewOffset += dwAddDataLength; - dwOldOffset += dwAddDataLength; - - // Sanity check - if((dwNewOffset + dwMovDataLength) > dwNewSize) - return ERROR_FILE_CORRUPT; - - // Copy the data from the extra block in BSDIFF patch - memcpy(pbNewData + dwNewOffset, pExtraBlock, dwMovDataLength); - pExtraBlock += dwMovDataLength; - dwNewOffset += dwMovDataLength; - - // Move the old offset - if(dwOldMoveLength & 0x80000000) - dwOldMoveLength = 0x80000000 - dwOldMoveLength; - dwOldOffset += dwOldMoveLength; - pCtrlBlock++; - } - - // The size after patch must match - if(dwNewOffset != pFullPatch->dwSizeAfterPatch) - return ERROR_FILE_CORRUPT; - - // Update the new data size - pPatcher->cbFileData = dwNewOffset; - return ERROR_SUCCESS; -} - -static PMPQ_PATCH_HEADER LoadFullFilePatch(TMPQFile * hf, MPQ_PATCH_HEADER & PatchHeader) -{ - PMPQ_PATCH_HEADER pFullPatch; - int nError = ERROR_SUCCESS; - - // BSWAP the entire header, if needed - BSWAP_ARRAY32_UNSIGNED(&PatchHeader, sizeof(DWORD) * 6); - BSWAP_ARRAY32_UNSIGNED(&PatchHeader.dwXFRM, sizeof(DWORD) * 3); - - // Verify the signatures in the patch header - if(PatchHeader.dwSignature != PATCH_SIGNATURE_HEADER || PatchHeader.dwMD5 != PATCH_SIGNATURE_MD5 || PatchHeader.dwXFRM != PATCH_SIGNATURE_XFRM) - return NULL; - - // Allocate space for patch header and compressed data - pFullPatch = (PMPQ_PATCH_HEADER)STORM_ALLOC(BYTE, PatchHeader.dwSizeOfPatchData); - if(pFullPatch != NULL) - { - // Copy the patch header - memcpy(pFullPatch, &PatchHeader, sizeof(MPQ_PATCH_HEADER)); - - // Read the patch, depending on patch type - if(nError == ERROR_SUCCESS) - { - switch(PatchHeader.dwPatchType) - { - case 0x59504f43: // 'COPY' - nError = LoadFilePatch_COPY(hf, pFullPatch); - break; - - case 0x30445342: // 'BSD0' - nError = LoadFilePatch_BSD0(hf, pFullPatch); - break; - - default: - nError = ERROR_FILE_CORRUPT; - break; - } - } - - // If something failed, free the patch buffer - if(nError != ERROR_SUCCESS) - { - STORM_FREE(pFullPatch); - pFullPatch = NULL; - } - } - - // Give the result to the caller - return pFullPatch; -} - -static int ApplyFilePatch( - TMPQPatcher * pPatcher, - PMPQ_PATCH_HEADER pFullPatch) -{ - LPBYTE pbSource = (pPatcher->nCounter & 0x1) ? pPatcher->pbFileData2 : pPatcher->pbFileData1; - LPBYTE pbTarget = (pPatcher->nCounter & 0x1) ? pPatcher->pbFileData1 : pPatcher->pbFileData2; - int nError; - - // Sanity checks - assert(pFullPatch->dwSizeAfterPatch <= pPatcher->cbMaxFileData); - - // Apply the patch according to the type - switch(pFullPatch->dwPatchType) - { - case 0x59504f43: // 'COPY' - nError = ApplyFilePatch_COPY(pPatcher, pFullPatch, pbTarget, pbSource); - break; - - case 0x30445342: // 'BSD0' - nError = ApplyFilePatch_BSD0(pPatcher, pFullPatch, pbTarget, pbSource); - break; - - default: - nError = ERROR_FILE_CORRUPT; - break; - } - - // Verify MD5 after patch - if(nError == ERROR_SUCCESS && pFullPatch->dwSizeAfterPatch != 0) - { - // Verify the patched file - if(!VerifyDataBlockHash(pbTarget, pFullPatch->dwSizeAfterPatch, pFullPatch->md5_after_patch)) - nError = ERROR_FILE_CORRUPT; - - // Copy the MD5 of the new block - memcpy(pPatcher->this_md5, pFullPatch->md5_after_patch, MD5_DIGEST_SIZE); - } - - return nError; -} - -//----------------------------------------------------------------------------- -// Local functions (patch prefix matching) - -static bool CreatePatchPrefix(TMPQArchive * ha, const char * szFileName, size_t nLength) -{ - TMPQNamePrefix * pNewPrefix; - - // If the end of the patch prefix was not entered, find it - if(szFileName != NULL && nLength == 0) - nLength = strlen(szFileName); - - // Create the patch prefix - pNewPrefix = (TMPQNamePrefix *)STORM_ALLOC(BYTE, sizeof(TMPQNamePrefix) + nLength); - if(pNewPrefix != NULL) - { - // Fill the name prefix - pNewPrefix->nLength = nLength; - pNewPrefix->szPatchPrefix[0] = 0; - - // Fill the name prefix. Also add the backslash - if(szFileName && nLength) - { - memcpy(pNewPrefix->szPatchPrefix, szFileName, nLength); - pNewPrefix->szPatchPrefix[nLength] = 0; - } - } - - ha->pPatchPrefix = pNewPrefix; - return (pNewPrefix != NULL); -} - -static bool IsMatchingPatchFile( - TMPQArchive * ha, - const char * szFileName, - LPBYTE pbFileMd5) -{ - MPQ_PATCH_HEADER PatchHeader = {0}; - HANDLE hFile = NULL; - DWORD dwTransferred = 0; - bool bResult = false; - - // Open the file and load the patch header - if(SFileOpenFileEx((HANDLE)ha, szFileName, SFILE_OPEN_BASE_FILE, &hFile)) - { - // Load the patch header - SFileReadFile(hFile, &PatchHeader, sizeof(MPQ_PATCH_HEADER), &dwTransferred, NULL); - BSWAP_ARRAY32_UNSIGNED(pPatchHeader, sizeof(DWORD) * 6); - - // If the file contains an incremental patch, - // compare the "MD5 before patching" with the base file MD5 - if(dwTransferred == sizeof(MPQ_PATCH_HEADER) && PatchHeader.dwSignature == PATCH_SIGNATURE_HEADER) - bResult = (!memcmp(PatchHeader.md5_before_patch, pbFileMd5, MD5_DIGEST_SIZE)); - - // Close the file - SFileCloseFile(hFile); - } - - return bResult; -} - -static const char * FindArchiveLanguage(TMPQArchive * ha, PLOCALIZED_MPQ_INFO pMpqInfo) -{ - TFileEntry * pFileEntry; - const char * szLanguage = LanguageList; - char szFileName[0x40]; - - // Iterate through all localized languages - while(pMpqInfo->szNameTemplate != NULL) - { - // Iterate through all languages - for(szLanguage = LanguageList; szLanguage[0] != 0; szLanguage += 4) - { - // Construct the file name - memcpy(szFileName, pMpqInfo->szNameTemplate, pMpqInfo->nLength); - szFileName[pMpqInfo->nLangOffset + 0] = szLanguage[0]; - szFileName[pMpqInfo->nLangOffset + 1] = szLanguage[1]; - szFileName[pMpqInfo->nLangOffset + 2] = szLanguage[2]; - szFileName[pMpqInfo->nLangOffset + 3] = szLanguage[3]; - - // Append the suffix - memcpy(szFileName + pMpqInfo->nLength, "-md5.lst", 9); - - // Check whether the name exists - pFileEntry = GetFileEntryLocale(ha, szFileName, 0); - if(pFileEntry != NULL) - return szLanguage; - } - - // Move to the next language name - pMpqInfo++; - } - - // Not found - return NULL; -} - -static TFileEntry * FindBaseLstFile(TMPQArchive * ha) -{ - TFileEntry * pFileEntry; - const char * szLanguage; - char szFileName[0x40]; - - // Prepare the file name tenplate - memcpy(szFileName, "####-md5.lst", 13); - - // Try all languages - for(szLanguage = LanguageList; szLanguage[0] != 0; szLanguage++) - { - // Copy the language name - szFileName[0] = szLanguage[0]; - szFileName[1] = szLanguage[1]; - szFileName[2] = szLanguage[2]; - szFileName[3] = szLanguage[3]; - - // Check whether this file exists - pFileEntry = GetFileEntryLocale(ha, szFileName, 0); - if(pFileEntry != NULL) - return pFileEntry; - } - - return NULL; -} - -static bool FindPatchPrefix_WoW_13164_13623(TMPQArchive * haBase, TMPQArchive * haPatch) -{ - const char * szPatchPrefix; - char szNamePrefix[0x08]; - - // Try to find the language of the MPQ archive - szPatchPrefix = FindArchiveLanguage(haBase, LocaleMpqs_WoW); - if(szPatchPrefix == NULL) - szPatchPrefix = "Base"; - - // Format the patch prefix - szNamePrefix[0] = szPatchPrefix[0]; - szNamePrefix[1] = szPatchPrefix[1]; - szNamePrefix[2] = szPatchPrefix[2]; - szNamePrefix[3] = szPatchPrefix[3]; - szNamePrefix[4] = '\\'; - szNamePrefix[5] = 0; - return CreatePatchPrefix(haPatch, szNamePrefix, 5); -} - -// -// Find match in Starcraft II patch MPQs -// Match a LST file in the root directory if the MPQ with any of the file in subdirectories -// -// The problem: -// Base: enGB-md5.lst -// Patch: Campaigns\Liberty.SC2Campaign\enGB.SC2Assets\enGB-md5.lst -// Campaigns\Liberty.SC2Campaign\enGB.SC2Data\enGB-md5.lst -// Campaigns\LibertyStory.SC2Campaign\enGB.SC2Data\enGB-md5.lst -// Campaigns\LibertyStory.SC2Campaign\enGB.SC2Data\enGB-md5.lst Mods\Core.SC2Mod\enGB.SC2Assets\enGB-md5.lst -// Mods\Core.SC2Mod\enGB.SC2Data\enGB-md5.lst -// Mods\Liberty.SC2Mod\enGB.SC2Assets\enGB-md5.lst -// Mods\Liberty.SC2Mod\enGB.SC2Data\enGB-md5.lst -// Mods\LibertyMulti.SC2Mod\enGB.SC2Data\enGB-md5.lst -// -// Solution: -// We need to match the file by its MD5 -// - - -static bool FindPatchPrefix_SC2(TMPQArchive * haBase, TMPQArchive * haPatch) -{ - TMPQNamePrefix * pPatchPrefix; - TFileEntry * pBaseEntry; - char * szLstFileName; - char * szPlainName; - size_t cchWorkBuffer = 0x400; - bool bResult = false; - - // First-level patches: Find the same file within the patch archive - // and verify by MD5-before-patch - if(haBase->haPatch == NULL) - { - TFileEntry * pFileTableEnd = haPatch->pFileTable + haPatch->dwFileTableSize; - TFileEntry * pFileEntry; - - // Allocate working buffer for merging LST file - szLstFileName = STORM_ALLOC(char, cchWorkBuffer); - if(szLstFileName != NULL) - { - // Find a *-md5.lst file in the base archive - pBaseEntry = FindBaseLstFile(haBase); - if(pBaseEntry == NULL) - { - STORM_FREE(szLstFileName); - return false; - } - - // Parse the entire file table - for(pFileEntry = haPatch->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++) - { - // Look for "patch_metadata" file - if(IsPatchMetadataFile(pFileEntry)) - { - // Construct the name of the MD5 file - strcpy(szLstFileName, pFileEntry->szFileName); - szPlainName = (char *)GetPlainFileName(szLstFileName); - strcpy(szPlainName, pBaseEntry->szFileName); - - // Check for matching MD5 file - if(IsMatchingPatchFile(haPatch, szLstFileName, pBaseEntry->md5)) - { - bResult = CreatePatchPrefix(haPatch, szLstFileName, (size_t)(szPlainName - szLstFileName)); - break; - } - } - } - - // Delete the merge buffer - STORM_FREE(szLstFileName); - } - } - - // For second-level patches, just take the patch prefix from the lower level patch - else - { - // There must be at least two patches in the chain - assert(haBase->haPatch->pPatchPrefix != NULL); - pPatchPrefix = haBase->haPatch->pPatchPrefix; - - // Copy the patch prefix - bResult = CreatePatchPrefix(haPatch, - pPatchPrefix->szPatchPrefix, - pPatchPrefix->nLength); - } - - return bResult; -} - -static bool FindPatchPrefix(TMPQArchive * haBase, TMPQArchive * haPatch, const char * szPatchPathPrefix) -{ - // If the patch prefix was explicitly entered, we use that one - if(szPatchPathPrefix != NULL) - return CreatePatchPrefix(haPatch, szPatchPathPrefix, 0); - - // Patches for World of Warcraft - they mostly do not use prefix. - // All patches that use patch prefix have the "base\\(patch_metadata) file present - if(GetFileEntryLocale(haPatch, "base\\" PATCH_METADATA_NAME, 0)) - return FindPatchPrefix_WoW_13164_13623(haBase, haPatch); - - // Updates for Starcraft II - // Match: LocalizedData\GameHotkeys.txt <==> Campaigns\Liberty.SC2Campaign\enGB.SC2Data\LocalizedData\GameHotkeys.txt - // All Starcraft II base archives seem to have the file "StreamingBuckets.txt" present - if(GetFileEntryLocale(haBase, "StreamingBuckets.txt", 0)) - return FindPatchPrefix_SC2(haBase, haPatch); - - // Diablo III patch MPQs don't use patch prefix - // Hearthstone MPQs don't use patch prefix - CreatePatchPrefix(haPatch, NULL, 0); - return true; -} - -//----------------------------------------------------------------------------- -// Public functions (StormLib internals) - -bool IsIncrementalPatchFile(const void * pvData, DWORD cbData, LPDWORD pdwPatchedFileSize) -{ - PMPQ_PATCH_HEADER pPatchHeader = (PMPQ_PATCH_HEADER)pvData; - BLIZZARD_BSDIFF40_FILE DiffFile; - DWORD dwPatchType; - - if(cbData >= sizeof(MPQ_PATCH_HEADER) + sizeof(BLIZZARD_BSDIFF40_FILE)) - { - dwPatchType = BSWAP_INT32_UNSIGNED(pPatchHeader->dwPatchType); - if(dwPatchType == 0x30445342) - { - // Give the caller the patch file size - if(pdwPatchedFileSize != NULL) - { - Decompress_RLE((LPBYTE)&DiffFile, sizeof(BLIZZARD_BSDIFF40_FILE), (LPBYTE)(pPatchHeader + 1), sizeof(BLIZZARD_BSDIFF40_FILE)); - DiffFile.NewFileSize = BSWAP_INT64_UNSIGNED(DiffFile.NewFileSize); - *pdwPatchedFileSize = (DWORD)DiffFile.NewFileSize; - return true; - } - } - } - - return false; -} - -int Patch_InitPatcher(TMPQPatcher * pPatcher, TMPQFile * hf) -{ - DWORD cbMaxFileData = 0; - - // Overflow check - if((sizeof(MPQ_PATCH_HEADER) + cbMaxFileData) < cbMaxFileData) - return ERROR_NOT_ENOUGH_MEMORY; - if(hf->hfPatch == NULL) - return ERROR_INVALID_PARAMETER; - - // Initialize the entire structure with zeros - memset(pPatcher, 0, sizeof(TMPQPatcher)); - - // Copy the MD5 of the current file - memcpy(pPatcher->this_md5, hf->pFileEntry->md5, MD5_DIGEST_SIZE); - - // Find out the biggest data size needed during the patching process - while(hf != NULL) - { - if(hf->pFileEntry->dwFileSize > cbMaxFileData) - cbMaxFileData = hf->pFileEntry->dwFileSize; - hf = hf->hfPatch; - } - - // Allocate primary and secondary buffer - pPatcher->pbFileData1 = STORM_ALLOC(BYTE, cbMaxFileData); - pPatcher->pbFileData2 = STORM_ALLOC(BYTE, cbMaxFileData); - if(!pPatcher->pbFileData1 || !pPatcher->pbFileData2) - return ERROR_NOT_ENOUGH_MEMORY; - - pPatcher->cbMaxFileData = cbMaxFileData; - return ERROR_SUCCESS; -} - -// -// Note: The patch may either be applied to the base file or to the previous version -// In Starcraft II, Mods\Core.SC2Mod\Base.SC2Data, file StreamingBuckets.txt: -// -// Base file MD5: 31376b0344b6df59ad009d4296125539 -// -// s2-update-base-23258: from 31376b0344b6df59ad009d4296125539 to 941a82683452e54bf024a8d491501824 -// s2-update-base-24540: from 31376b0344b6df59ad009d4296125539 to 941a82683452e54bf024a8d491501824 -// s2-update-base-26147: from 31376b0344b6df59ad009d4296125539 to d5d5253c762fac6b9761240288a0771a -// s2-update-base-28522: from 31376b0344b6df59ad009d4296125539 to 5a76c4b356920aab7afd22e0e1913d7a -// s2-update-base-30508: from 31376b0344b6df59ad009d4296125539 to 8cb0d4799893fe801cc78ae4488a3671 -// s2-update-base-32283: from 31376b0344b6df59ad009d4296125539 to 8cb0d4799893fe801cc78ae4488a3671 -// -// We don't keep all intermediate versions in memory, as it would cause massive -// memory usage during patching process. A prime example is the file -// DBFilesClient\\Item-Sparse.db2 from locale-enGB.MPQ (WoW 16965), which has -// 9 patches in a row, each requiring 70 MB memory (35 MB patch data + 35 MB work buffer) -// - -int Patch_Process(TMPQPatcher * pPatcher, TMPQFile * hf) -{ - PMPQ_PATCH_HEADER pFullPatch; - MPQ_PATCH_HEADER PatchHeader1; - MPQ_PATCH_HEADER PatchHeader2 = {0}; - TMPQFile * hfBase = hf; - DWORD cbBytesRead = 0; - int nError = ERROR_SUCCESS; - - // Move to the first patch - assert(hfBase->pbFileData == NULL); - assert(hfBase->cbFileData == 0); - hf = hf->hfPatch; - - // Read the header of the current patch - SFileReadFile((HANDLE)hf, &PatchHeader1, sizeof(MPQ_PATCH_HEADER), &cbBytesRead, NULL); - if(cbBytesRead != sizeof(MPQ_PATCH_HEADER)) - return ERROR_FILE_CORRUPT; - - // Perform the patching process - while(nError == ERROR_SUCCESS && hf != NULL) - { - // Try to read the next patch header. If the md5_before_patch - // still matches we go directly to the next one and repeat - while(hf->hfPatch != NULL) - { - // Attempt to read the patch header - SFileReadFile((HANDLE)hf->hfPatch, &PatchHeader2, sizeof(MPQ_PATCH_HEADER), &cbBytesRead, NULL); - if(cbBytesRead != sizeof(MPQ_PATCH_HEADER)) - return ERROR_FILE_CORRUPT; - - // Compare the md5_before_patch - if(memcmp(PatchHeader2.md5_before_patch, pPatcher->this_md5, MD5_DIGEST_SIZE)) - break; - - // Move one patch fuhrter - PatchHeader1 = PatchHeader2; - hf = hf->hfPatch; - } - - // Allocate memory for the patch data - pFullPatch = LoadFullFilePatch(hf, PatchHeader1); - if(pFullPatch != NULL) - { - // Apply the patch - nError = ApplyFilePatch(pPatcher, pFullPatch); - STORM_FREE(pFullPatch); - } - else - { - nError = ERROR_FILE_CORRUPT; - } - - // Move to the next patch - PatchHeader1 = PatchHeader2; - pPatcher->nCounter++; - hf = hf->hfPatch; - } - - // Put the result data to the file structure - if(nError == ERROR_SUCCESS) - { - // Swap the pointer to the file data structure - if(pPatcher->nCounter & 0x01) - { - hfBase->pbFileData = pPatcher->pbFileData2; - pPatcher->pbFileData2 = NULL; - } - else - { - hfBase->pbFileData = pPatcher->pbFileData1; - pPatcher->pbFileData1 = NULL; - } - - // Also supply the data size - hfBase->cbFileData = pPatcher->cbFileData; - } - - return ERROR_SUCCESS; -} - -void Patch_Finalize(TMPQPatcher * pPatcher) -{ - if(pPatcher != NULL) - { - if(pPatcher->pbFileData1 != NULL) - STORM_FREE(pPatcher->pbFileData1); - if(pPatcher->pbFileData2 != NULL) - STORM_FREE(pPatcher->pbFileData2); - - memset(pPatcher, 0, sizeof(TMPQPatcher)); - } -} - - -//----------------------------------------------------------------------------- -// Public functions - -// -// Patch prefix is the path subdirectory where the patched files are within MPQ. -// -// Example 1: -// Main MPQ: locale-enGB.MPQ -// Patch MPQ: wow-update-12694.MPQ -// File in main MPQ: DBFilesClient\Achievement.dbc -// File in patch MPQ: enGB\DBFilesClient\Achievement.dbc -// Path prefix: enGB -// -// Example 2: -// Main MPQ: expansion1.MPQ -// Patch MPQ: wow-update-12694.MPQ -// File in main MPQ: DBFilesClient\Achievement.dbc -// File in patch MPQ: Base\DBFilesClient\Achievement.dbc -// Path prefix: Base -// - -bool WINAPI SFileOpenPatchArchive( - HANDLE hMpq, - const TCHAR * szPatchMpqName, - const char * szPatchPathPrefix, - DWORD dwFlags) -{ - TMPQArchive * haPatch; - TMPQArchive * ha = (TMPQArchive *)hMpq; - HANDLE hPatchMpq = NULL; - int nError = ERROR_SUCCESS; - - // Keep compiler happy - dwFlags = dwFlags; - - // Verify input parameters - if(!IsValidMpqHandle(hMpq)) - nError = ERROR_INVALID_HANDLE; - if(szPatchMpqName == NULL || *szPatchMpqName == 0) - nError = ERROR_INVALID_PARAMETER; - - // - // We don't allow adding patches to archives that have been open for write - // - // Error scenario: - // - // 1) Open archive for writing - // 2) Modify or replace a file - // 3) Add patch archive to the opened MPQ - // 4) Read patched file - // 5) Now what ? - // - - if(nError == ERROR_SUCCESS) - { - if(!(ha->dwFlags & MPQ_FLAG_READ_ONLY)) - nError = ERROR_ACCESS_DENIED; - } - - // Open the archive like it is normal archive - if(nError == ERROR_SUCCESS) - { - if(!SFileOpenArchive(szPatchMpqName, 0, MPQ_OPEN_READ_ONLY | MPQ_OPEN_PATCH, &hPatchMpq)) - return false; - haPatch = (TMPQArchive *)hPatchMpq; - - // We need to remember the proper patch prefix to match names of patched files - FindPatchPrefix(ha, (TMPQArchive *)hPatchMpq, szPatchPathPrefix); - - // Now add the patch archive to the list of patches to the original MPQ - while(ha != NULL) - { - if(ha->haPatch == NULL) - { - haPatch->haBase = ha; - ha->haPatch = haPatch; - return true; - } - - // Move to the next archive - ha = ha->haPatch; - } - - // Should never happen - nError = ERROR_CAN_NOT_COMPLETE; - } - - SetLastError(nError); - return false; -} - -bool WINAPI SFileIsPatchedArchive(HANDLE hMpq) -{ - TMPQArchive * ha = (TMPQArchive *)hMpq; - - // Verify input parameters - if(!IsValidMpqHandle(hMpq)) - return false; - - return (ha->haPatch != NULL); -} +/*****************************************************************************/ +/* SFilePatchArchives.cpp Copyright (c) Ladislav Zezula 2010 */ +/*---------------------------------------------------------------------------*/ +/* Description: */ +/*---------------------------------------------------------------------------*/ +/* Date Ver Who Comment */ +/* -------- ---- --- ------- */ +/* 18.08.10 1.00 Lad The first version of SFilePatchArchives.cpp */ +/*****************************************************************************/ + +#define __STORMLIB_SELF__ +#include "StormLib.h" +#include "StormCommon.h" + +//----------------------------------------------------------------------------- +// Local structures + +#define PATCH_SIGNATURE_HEADER 0x48435450 +#define PATCH_SIGNATURE_MD5 0x5f35444d +#define PATCH_SIGNATURE_XFRM 0x4d524658 + +#define SIZE_OF_XFRM_HEADER 0x0C + +// Header for incremental patch files +typedef struct _MPQ_PATCH_HEADER +{ + //-- PATCH header ----------------------------------- + DWORD dwSignature; // 'PTCH' + DWORD dwSizeOfPatchData; // Size of the entire patch (decompressed) + DWORD dwSizeBeforePatch; // Size of the file before patch + DWORD dwSizeAfterPatch; // Size of file after patch + + //-- MD5 block -------------------------------------- + DWORD dwMD5; // 'MD5_' + DWORD dwMd5BlockSize; // Size of the MD5 block, including the signature and size itself + BYTE md5_before_patch[0x10]; // MD5 of the original (unpached) file + BYTE md5_after_patch[0x10]; // MD5 of the patched file + + //-- XFRM block ------------------------------------- + DWORD dwXFRM; // 'XFRM' + DWORD dwXfrmBlockSize; // Size of the XFRM block, includes XFRM header and patch data + DWORD dwPatchType; // Type of patch ('BSD0' or 'COPY') + + // Followed by the patch data +} MPQ_PATCH_HEADER, *PMPQ_PATCH_HEADER; + +typedef struct _BLIZZARD_BSDIFF40_FILE +{ + ULONGLONG Signature; + ULONGLONG CtrlBlockSize; + ULONGLONG DataBlockSize; + ULONGLONG NewFileSize; +} BLIZZARD_BSDIFF40_FILE, *PBLIZZARD_BSDIFF40_FILE; + +typedef struct _BSDIFF_CTRL_BLOCK +{ + DWORD dwAddDataLength; + DWORD dwMovDataLength; + DWORD dwOldMoveLength; + +} BSDIFF_CTRL_BLOCK, *PBSDIFF_CTRL_BLOCK; + +typedef struct _LOCALIZED_MPQ_INFO +{ + const char * szNameTemplate; // Name template + size_t nLangOffset; // Offset of the language + size_t nLength; // Length of the name template +} LOCALIZED_MPQ_INFO, *PLOCALIZED_MPQ_INFO; + +//----------------------------------------------------------------------------- +// Local variables + +// 4-byte groups for all languages +static const char * LanguageList = "baseteenenUSenGBenCNenTWdeDEesESesMXfrFRitITkoKRptBRptPTruRUzhCNzhTW"; + +// List of localized MPQs for World of Warcraft +static LOCALIZED_MPQ_INFO LocaleMpqs_WoW[] = +{ + {"expansion1-locale-####", 18, 22}, + {"expansion1-speech-####", 18, 22}, + {"expansion2-locale-####", 18, 22}, + {"expansion2-speech-####", 18, 22}, + {"expansion3-locale-####", 18, 22}, + {"expansion3-speech-####", 18, 22}, + {"locale-####", 7, 11}, + {"speech-####", 7, 11}, + {NULL, 0, 0} +}; + +//----------------------------------------------------------------------------- +// Local functions + +static inline bool IsPatchMetadataFile(TFileEntry * pFileEntry) +{ + // The file must ave a name + if(pFileEntry->szFileName != NULL && (pFileEntry->dwFlags & MPQ_FILE_PATCH_FILE) == 0) + { + // The file must be small + if(0 < pFileEntry->dwFileSize && pFileEntry->dwFileSize < 0x40) + { + // Compare the plain name + return (_stricmp(GetPlainFileName(pFileEntry->szFileName), PATCH_METADATA_NAME) == 0); + } + } + + // Not a patch_metadata + return false; +} + +static void Decompress_RLE(LPBYTE pbDecompressed, DWORD cbDecompressed, LPBYTE pbCompressed, DWORD cbCompressed) +{ + LPBYTE pbDecompressedEnd = pbDecompressed + cbDecompressed; + LPBYTE pbCompressedEnd = pbCompressed + cbCompressed; + BYTE RepeatCount; + BYTE OneByte; + + // Cut the initial DWORD from the compressed chunk + pbCompressed += sizeof(DWORD); + + // Pre-fill decompressed buffer with zeros + memset(pbDecompressed, 0, cbDecompressed); + + // Unpack + while(pbCompressed < pbCompressedEnd && pbDecompressed < pbDecompressedEnd) + { + OneByte = *pbCompressed++; + + // Is it a repetition byte ? + if(OneByte & 0x80) + { + RepeatCount = (OneByte & 0x7F) + 1; + for(BYTE i = 0; i < RepeatCount; i++) + { + if(pbDecompressed == pbDecompressedEnd || pbCompressed == pbCompressedEnd) + break; + + *pbDecompressed++ = *pbCompressed++; + } + } + else + { + pbDecompressed += (OneByte + 1); + } + } +} + +static int LoadFilePatch_COPY(TMPQFile * hf, PMPQ_PATCH_HEADER pFullPatch) +{ + DWORD cbBytesToRead = pFullPatch->dwSizeOfPatchData - sizeof(MPQ_PATCH_HEADER); + DWORD cbBytesRead = 0; + + // Simply load the rest of the patch + SFileReadFile((HANDLE)hf, (pFullPatch + 1), cbBytesToRead, &cbBytesRead, NULL); + return (cbBytesRead == cbBytesToRead) ? ERROR_SUCCESS : ERROR_FILE_CORRUPT; +} + +static int LoadFilePatch_BSD0(TMPQFile * hf, PMPQ_PATCH_HEADER pFullPatch) +{ + LPBYTE pbDecompressed = (LPBYTE)(pFullPatch + 1); + LPBYTE pbCompressed = NULL; + DWORD cbDecompressed = 0; + DWORD cbCompressed = 0; + DWORD dwBytesRead = 0; + int nError = ERROR_SUCCESS; + + // Calculate the size of compressed data + cbDecompressed = pFullPatch->dwSizeOfPatchData - sizeof(MPQ_PATCH_HEADER); + cbCompressed = pFullPatch->dwXfrmBlockSize - SIZE_OF_XFRM_HEADER; + + // Is that file compressed? + if(cbCompressed < cbDecompressed) + { + pbCompressed = STORM_ALLOC(BYTE, cbCompressed); + if(pbCompressed == NULL) + nError = ERROR_NOT_ENOUGH_MEMORY; + + // Read the compressed patch data + if(nError == ERROR_SUCCESS) + { + SFileReadFile((HANDLE)hf, pbCompressed, cbCompressed, &dwBytesRead, NULL); + if(dwBytesRead != cbCompressed) + nError = ERROR_FILE_CORRUPT; + } + + // Decompress the data + if(nError == ERROR_SUCCESS) + Decompress_RLE(pbDecompressed, cbDecompressed, pbCompressed, cbCompressed); + + if(pbCompressed != NULL) + STORM_FREE(pbCompressed); + } + else + { + SFileReadFile((HANDLE)hf, pbDecompressed, cbDecompressed, &dwBytesRead, NULL); + if(dwBytesRead != cbDecompressed) + nError = ERROR_FILE_CORRUPT; + } + + return nError; +} + +static int ApplyFilePatch_COPY( + TMPQPatcher * pPatcher, + PMPQ_PATCH_HEADER pFullPatch, + LPBYTE pbTarget, + LPBYTE pbSource) +{ + // Sanity checks + assert(pPatcher->cbMaxFileData >= pPatcher->cbFileData); + pFullPatch = pFullPatch; + + // Copy the patch data as-is + memcpy(pbTarget, pbSource, pPatcher->cbFileData); + return ERROR_SUCCESS; +} + +static int ApplyFilePatch_BSD0( + TMPQPatcher * pPatcher, + PMPQ_PATCH_HEADER pFullPatch, + LPBYTE pbTarget, + LPBYTE pbSource) +{ + PBLIZZARD_BSDIFF40_FILE pBsdiff; + PBSDIFF_CTRL_BLOCK pCtrlBlock; + LPBYTE pbPatchData = (LPBYTE)(pFullPatch + 1); + LPBYTE pDataBlock; + LPBYTE pExtraBlock; + LPBYTE pbOldData = pbSource; + LPBYTE pbNewData = pbTarget; + DWORD dwCombineSize; + DWORD dwNewOffset = 0; // Current position to patch + DWORD dwOldOffset = 0; // Current source position + DWORD dwNewSize; // Patched file size + DWORD dwOldSize = pPatcher->cbFileData; // File size before patch + + // Get pointer to the patch header + // Format of BSDIFF header corresponds to original BSDIFF, which is: + // 0000 8 bytes signature "BSDIFF40" + // 0008 8 bytes size of the control block + // 0010 8 bytes size of the data block + // 0018 8 bytes new size of the patched file + pBsdiff = (PBLIZZARD_BSDIFF40_FILE)pbPatchData; + pbPatchData += sizeof(BLIZZARD_BSDIFF40_FILE); + + // Get pointer to the 32-bit BSDIFF control block + // The control block follows immediately after the BSDIFF header + // and consists of three 32-bit integers + // 0000 4 bytes Length to copy from the BSDIFF data block the new file + // 0004 4 bytes Length to copy from the BSDIFF extra block + // 0008 4 bytes Size to increment source file offset + pCtrlBlock = (PBSDIFF_CTRL_BLOCK)pbPatchData; + pbPatchData += (size_t)BSWAP_INT64_UNSIGNED(pBsdiff->CtrlBlockSize); + + // Get the pointer to the data block + pDataBlock = (LPBYTE)pbPatchData; + pbPatchData += (size_t)BSWAP_INT64_UNSIGNED(pBsdiff->DataBlockSize); + + // Get the pointer to the extra block + pExtraBlock = (LPBYTE)pbPatchData; + dwNewSize = (DWORD)BSWAP_INT64_UNSIGNED(pBsdiff->NewFileSize); + + // Now patch the file + while(dwNewOffset < dwNewSize) + { + DWORD dwAddDataLength = BSWAP_INT32_UNSIGNED(pCtrlBlock->dwAddDataLength); + DWORD dwMovDataLength = BSWAP_INT32_UNSIGNED(pCtrlBlock->dwMovDataLength); + DWORD dwOldMoveLength = BSWAP_INT32_UNSIGNED(pCtrlBlock->dwOldMoveLength); + DWORD i; + + // Sanity check + if((dwNewOffset + dwAddDataLength) > dwNewSize) + return ERROR_FILE_CORRUPT; + + // Read the diff string to the target buffer + memcpy(pbNewData + dwNewOffset, pDataBlock, dwAddDataLength); + pDataBlock += dwAddDataLength; + + // Get the longest block that we can combine + dwCombineSize = ((dwOldOffset + dwAddDataLength) >= dwOldSize) ? (dwOldSize - dwOldOffset) : dwAddDataLength; + + // Now combine the patch data with the original file + for(i = 0; i < dwCombineSize; i++) + pbNewData[dwNewOffset + i] = pbNewData[dwNewOffset + i] + pbOldData[dwOldOffset + i]; + + // Move the offsets + dwNewOffset += dwAddDataLength; + dwOldOffset += dwAddDataLength; + + // Sanity check + if((dwNewOffset + dwMovDataLength) > dwNewSize) + return ERROR_FILE_CORRUPT; + + // Copy the data from the extra block in BSDIFF patch + memcpy(pbNewData + dwNewOffset, pExtraBlock, dwMovDataLength); + pExtraBlock += dwMovDataLength; + dwNewOffset += dwMovDataLength; + + // Move the old offset + if(dwOldMoveLength & 0x80000000) + dwOldMoveLength = 0x80000000 - dwOldMoveLength; + dwOldOffset += dwOldMoveLength; + pCtrlBlock++; + } + + // The size after patch must match + if(dwNewOffset != pFullPatch->dwSizeAfterPatch) + return ERROR_FILE_CORRUPT; + + // Update the new data size + pPatcher->cbFileData = dwNewOffset; + return ERROR_SUCCESS; +} + +static PMPQ_PATCH_HEADER LoadFullFilePatch(TMPQFile * hf, MPQ_PATCH_HEADER & PatchHeader) +{ + PMPQ_PATCH_HEADER pFullPatch; + int nError = ERROR_SUCCESS; + + // BSWAP the entire header, if needed + BSWAP_ARRAY32_UNSIGNED(&PatchHeader, sizeof(DWORD) * 6); + BSWAP_ARRAY32_UNSIGNED(&PatchHeader.dwXFRM, sizeof(DWORD) * 3); + + // Verify the signatures in the patch header + if(PatchHeader.dwSignature != PATCH_SIGNATURE_HEADER || PatchHeader.dwMD5 != PATCH_SIGNATURE_MD5 || PatchHeader.dwXFRM != PATCH_SIGNATURE_XFRM) + return NULL; + + // Allocate space for patch header and compressed data + pFullPatch = (PMPQ_PATCH_HEADER)STORM_ALLOC(BYTE, PatchHeader.dwSizeOfPatchData); + if(pFullPatch != NULL) + { + // Copy the patch header + memcpy(pFullPatch, &PatchHeader, sizeof(MPQ_PATCH_HEADER)); + + // Read the patch, depending on patch type + if(nError == ERROR_SUCCESS) + { + switch(PatchHeader.dwPatchType) + { + case 0x59504f43: // 'COPY' + nError = LoadFilePatch_COPY(hf, pFullPatch); + break; + + case 0x30445342: // 'BSD0' + nError = LoadFilePatch_BSD0(hf, pFullPatch); + break; + + default: + nError = ERROR_FILE_CORRUPT; + break; + } + } + + // If something failed, free the patch buffer + if(nError != ERROR_SUCCESS) + { + STORM_FREE(pFullPatch); + pFullPatch = NULL; + } + } + + // Give the result to the caller + return pFullPatch; +} + +static int ApplyFilePatch( + TMPQPatcher * pPatcher, + PMPQ_PATCH_HEADER pFullPatch) +{ + LPBYTE pbSource = (pPatcher->nCounter & 0x1) ? pPatcher->pbFileData2 : pPatcher->pbFileData1; + LPBYTE pbTarget = (pPatcher->nCounter & 0x1) ? pPatcher->pbFileData1 : pPatcher->pbFileData2; + int nError; + + // Sanity checks + assert(pFullPatch->dwSizeAfterPatch <= pPatcher->cbMaxFileData); + + // Apply the patch according to the type + switch(pFullPatch->dwPatchType) + { + case 0x59504f43: // 'COPY' + nError = ApplyFilePatch_COPY(pPatcher, pFullPatch, pbTarget, pbSource); + break; + + case 0x30445342: // 'BSD0' + nError = ApplyFilePatch_BSD0(pPatcher, pFullPatch, pbTarget, pbSource); + break; + + default: + nError = ERROR_FILE_CORRUPT; + break; + } + + // Verify MD5 after patch + if(nError == ERROR_SUCCESS && pFullPatch->dwSizeAfterPatch != 0) + { + // Verify the patched file + if(!VerifyDataBlockHash(pbTarget, pFullPatch->dwSizeAfterPatch, pFullPatch->md5_after_patch)) + nError = ERROR_FILE_CORRUPT; + + // Copy the MD5 of the new block + memcpy(pPatcher->this_md5, pFullPatch->md5_after_patch, MD5_DIGEST_SIZE); + } + + return nError; +} + +//----------------------------------------------------------------------------- +// Local functions (patch prefix matching) + +static bool CreatePatchPrefix(TMPQArchive * ha, const char * szFileName, size_t nLength) +{ + TMPQNamePrefix * pNewPrefix; + + // If the end of the patch prefix was not entered, find it + if(szFileName != NULL && nLength == 0) + nLength = strlen(szFileName); + + // Create the patch prefix + pNewPrefix = (TMPQNamePrefix *)STORM_ALLOC(BYTE, sizeof(TMPQNamePrefix) + nLength); + if(pNewPrefix != NULL) + { + // Fill the name prefix + pNewPrefix->nLength = nLength; + pNewPrefix->szPatchPrefix[0] = 0; + + // Fill the name prefix. Also add the backslash + if(szFileName && nLength) + { + memcpy(pNewPrefix->szPatchPrefix, szFileName, nLength); + pNewPrefix->szPatchPrefix[nLength] = 0; + } + } + + ha->pPatchPrefix = pNewPrefix; + return (pNewPrefix != NULL); +} + +static bool IsMatchingPatchFile( + TMPQArchive * ha, + const char * szFileName, + LPBYTE pbFileMd5) +{ + MPQ_PATCH_HEADER PatchHeader = {0}; + HANDLE hFile = NULL; + DWORD dwTransferred = 0; + bool bResult = false; + + // Open the file and load the patch header + if(SFileOpenFileEx((HANDLE)ha, szFileName, SFILE_OPEN_BASE_FILE, &hFile)) + { + // Load the patch header + SFileReadFile(hFile, &PatchHeader, sizeof(MPQ_PATCH_HEADER), &dwTransferred, NULL); + BSWAP_ARRAY32_UNSIGNED(pPatchHeader, sizeof(DWORD) * 6); + + // If the file contains an incremental patch, + // compare the "MD5 before patching" with the base file MD5 + if(dwTransferred == sizeof(MPQ_PATCH_HEADER) && PatchHeader.dwSignature == PATCH_SIGNATURE_HEADER) + bResult = (!memcmp(PatchHeader.md5_before_patch, pbFileMd5, MD5_DIGEST_SIZE)); + + // Close the file + SFileCloseFile(hFile); + } + + return bResult; +} + +static const char * FindArchiveLanguage(TMPQArchive * ha, PLOCALIZED_MPQ_INFO pMpqInfo) +{ + TFileEntry * pFileEntry; + const char * szLanguage = LanguageList; + char szFileName[0x40]; + + // Iterate through all localized languages + while(pMpqInfo->szNameTemplate != NULL) + { + // Iterate through all languages + for(szLanguage = LanguageList; szLanguage[0] != 0; szLanguage += 4) + { + // Construct the file name + memcpy(szFileName, pMpqInfo->szNameTemplate, pMpqInfo->nLength); + szFileName[pMpqInfo->nLangOffset + 0] = szLanguage[0]; + szFileName[pMpqInfo->nLangOffset + 1] = szLanguage[1]; + szFileName[pMpqInfo->nLangOffset + 2] = szLanguage[2]; + szFileName[pMpqInfo->nLangOffset + 3] = szLanguage[3]; + + // Append the suffix + memcpy(szFileName + pMpqInfo->nLength, "-md5.lst", 9); + + // Check whether the name exists + pFileEntry = GetFileEntryLocale(ha, szFileName, 0); + if(pFileEntry != NULL) + return szLanguage; + } + + // Move to the next language name + pMpqInfo++; + } + + // Not found + return NULL; +} + +static TFileEntry * FindBaseLstFile(TMPQArchive * ha) +{ + TFileEntry * pFileEntry; + const char * szLanguage; + char szFileName[0x40]; + + // Prepare the file name tenplate + memcpy(szFileName, "####-md5.lst", 13); + + // Try all languages + for(szLanguage = LanguageList; szLanguage[0] != 0; szLanguage++) + { + // Copy the language name + szFileName[0] = szLanguage[0]; + szFileName[1] = szLanguage[1]; + szFileName[2] = szLanguage[2]; + szFileName[3] = szLanguage[3]; + + // Check whether this file exists + pFileEntry = GetFileEntryLocale(ha, szFileName, 0); + if(pFileEntry != NULL) + return pFileEntry; + } + + return NULL; +} + +static bool FindPatchPrefix_WoW_13164_13623(TMPQArchive * haBase, TMPQArchive * haPatch) +{ + const char * szPatchPrefix; + char szNamePrefix[0x08]; + + // Try to find the language of the MPQ archive + szPatchPrefix = FindArchiveLanguage(haBase, LocaleMpqs_WoW); + if(szPatchPrefix == NULL) + szPatchPrefix = "Base"; + + // Format the patch prefix + szNamePrefix[0] = szPatchPrefix[0]; + szNamePrefix[1] = szPatchPrefix[1]; + szNamePrefix[2] = szPatchPrefix[2]; + szNamePrefix[3] = szPatchPrefix[3]; + szNamePrefix[4] = '\\'; + szNamePrefix[5] = 0; + return CreatePatchPrefix(haPatch, szNamePrefix, 5); +} + +// +// Find match in Starcraft II patch MPQs +// Match a LST file in the root directory if the MPQ with any of the file in subdirectories +// +// The problem: +// Base: enGB-md5.lst +// Patch: Campaigns\Liberty.SC2Campaign\enGB.SC2Assets\enGB-md5.lst +// Campaigns\Liberty.SC2Campaign\enGB.SC2Data\enGB-md5.lst +// Campaigns\LibertyStory.SC2Campaign\enGB.SC2Data\enGB-md5.lst +// Campaigns\LibertyStory.SC2Campaign\enGB.SC2Data\enGB-md5.lst Mods\Core.SC2Mod\enGB.SC2Assets\enGB-md5.lst +// Mods\Core.SC2Mod\enGB.SC2Data\enGB-md5.lst +// Mods\Liberty.SC2Mod\enGB.SC2Assets\enGB-md5.lst +// Mods\Liberty.SC2Mod\enGB.SC2Data\enGB-md5.lst +// Mods\LibertyMulti.SC2Mod\enGB.SC2Data\enGB-md5.lst +// +// Solution: +// We need to match the file by its MD5 +// + + +static bool FindPatchPrefix_SC2(TMPQArchive * haBase, TMPQArchive * haPatch) +{ + TMPQNamePrefix * pPatchPrefix; + TFileEntry * pBaseEntry; + char * szLstFileName; + char * szPlainName; + size_t cchWorkBuffer = 0x400; + bool bResult = false; + + // First-level patches: Find the same file within the patch archive + // and verify by MD5-before-patch + if(haBase->haPatch == NULL) + { + TFileEntry * pFileTableEnd = haPatch->pFileTable + haPatch->dwFileTableSize; + TFileEntry * pFileEntry; + + // Allocate working buffer for merging LST file + szLstFileName = STORM_ALLOC(char, cchWorkBuffer); + if(szLstFileName != NULL) + { + // Find a *-md5.lst file in the base archive + pBaseEntry = FindBaseLstFile(haBase); + if(pBaseEntry == NULL) + { + STORM_FREE(szLstFileName); + return false; + } + + // Parse the entire file table + for(pFileEntry = haPatch->pFileTable; pFileEntry < pFileTableEnd; pFileEntry++) + { + // Look for "patch_metadata" file + if(IsPatchMetadataFile(pFileEntry)) + { + // Construct the name of the MD5 file + strcpy(szLstFileName, pFileEntry->szFileName); + szPlainName = (char *)GetPlainFileName(szLstFileName); + strcpy(szPlainName, pBaseEntry->szFileName); + + // Check for matching MD5 file + if(IsMatchingPatchFile(haPatch, szLstFileName, pBaseEntry->md5)) + { + bResult = CreatePatchPrefix(haPatch, szLstFileName, (size_t)(szPlainName - szLstFileName)); + break; + } + } + } + + // Delete the merge buffer + STORM_FREE(szLstFileName); + } + } + + // For second-level patches, just take the patch prefix from the lower level patch + else + { + // There must be at least two patches in the chain + assert(haBase->haPatch->pPatchPrefix != NULL); + pPatchPrefix = haBase->haPatch->pPatchPrefix; + + // Copy the patch prefix + bResult = CreatePatchPrefix(haPatch, + pPatchPrefix->szPatchPrefix, + pPatchPrefix->nLength); + } + + return bResult; +} + +static bool FindPatchPrefix(TMPQArchive * haBase, TMPQArchive * haPatch, const char * szPatchPathPrefix) +{ + // If the patch prefix was explicitly entered, we use that one + if(szPatchPathPrefix != NULL) + return CreatePatchPrefix(haPatch, szPatchPathPrefix, 0); + + // Patches for World of Warcraft - they mostly do not use prefix. + // All patches that use patch prefix have the "base\\(patch_metadata) file present + if(GetFileEntryLocale(haPatch, "base\\" PATCH_METADATA_NAME, 0)) + return FindPatchPrefix_WoW_13164_13623(haBase, haPatch); + + // Updates for Starcraft II + // Match: LocalizedData\GameHotkeys.txt <==> Campaigns\Liberty.SC2Campaign\enGB.SC2Data\LocalizedData\GameHotkeys.txt + // All Starcraft II base archives seem to have the file "StreamingBuckets.txt" present + if(GetFileEntryLocale(haBase, "StreamingBuckets.txt", 0)) + return FindPatchPrefix_SC2(haBase, haPatch); + + // Diablo III patch MPQs don't use patch prefix + // Hearthstone MPQs don't use patch prefix + CreatePatchPrefix(haPatch, NULL, 0); + return true; +} + +//----------------------------------------------------------------------------- +// Public functions (StormLib internals) + +bool IsIncrementalPatchFile(const void * pvData, DWORD cbData, LPDWORD pdwPatchedFileSize) +{ + PMPQ_PATCH_HEADER pPatchHeader = (PMPQ_PATCH_HEADER)pvData; + BLIZZARD_BSDIFF40_FILE DiffFile; + DWORD dwPatchType; + + if(cbData >= sizeof(MPQ_PATCH_HEADER) + sizeof(BLIZZARD_BSDIFF40_FILE)) + { + dwPatchType = BSWAP_INT32_UNSIGNED(pPatchHeader->dwPatchType); + if(dwPatchType == 0x30445342) + { + // Give the caller the patch file size + if(pdwPatchedFileSize != NULL) + { + Decompress_RLE((LPBYTE)&DiffFile, sizeof(BLIZZARD_BSDIFF40_FILE), (LPBYTE)(pPatchHeader + 1), sizeof(BLIZZARD_BSDIFF40_FILE)); + DiffFile.NewFileSize = BSWAP_INT64_UNSIGNED(DiffFile.NewFileSize); + *pdwPatchedFileSize = (DWORD)DiffFile.NewFileSize; + return true; + } + } + } + + return false; +} + +int Patch_InitPatcher(TMPQPatcher * pPatcher, TMPQFile * hf) +{ + DWORD cbMaxFileData = 0; + + // Overflow check + if((sizeof(MPQ_PATCH_HEADER) + cbMaxFileData) < cbMaxFileData) + return ERROR_NOT_ENOUGH_MEMORY; + if(hf->hfPatch == NULL) + return ERROR_INVALID_PARAMETER; + + // Initialize the entire structure with zeros + memset(pPatcher, 0, sizeof(TMPQPatcher)); + + // Copy the MD5 of the current file + memcpy(pPatcher->this_md5, hf->pFileEntry->md5, MD5_DIGEST_SIZE); + + // Find out the biggest data size needed during the patching process + while(hf != NULL) + { + if(hf->pFileEntry->dwFileSize > cbMaxFileData) + cbMaxFileData = hf->pFileEntry->dwFileSize; + hf = hf->hfPatch; + } + + // Allocate primary and secondary buffer + pPatcher->pbFileData1 = STORM_ALLOC(BYTE, cbMaxFileData); + pPatcher->pbFileData2 = STORM_ALLOC(BYTE, cbMaxFileData); + if(!pPatcher->pbFileData1 || !pPatcher->pbFileData2) + return ERROR_NOT_ENOUGH_MEMORY; + + pPatcher->cbMaxFileData = cbMaxFileData; + return ERROR_SUCCESS; +} + +// +// Note: The patch may either be applied to the base file or to the previous version +// In Starcraft II, Mods\Core.SC2Mod\Base.SC2Data, file StreamingBuckets.txt: +// +// Base file MD5: 31376b0344b6df59ad009d4296125539 +// +// s2-update-base-23258: from 31376b0344b6df59ad009d4296125539 to 941a82683452e54bf024a8d491501824 +// s2-update-base-24540: from 31376b0344b6df59ad009d4296125539 to 941a82683452e54bf024a8d491501824 +// s2-update-base-26147: from 31376b0344b6df59ad009d4296125539 to d5d5253c762fac6b9761240288a0771a +// s2-update-base-28522: from 31376b0344b6df59ad009d4296125539 to 5a76c4b356920aab7afd22e0e1913d7a +// s2-update-base-30508: from 31376b0344b6df59ad009d4296125539 to 8cb0d4799893fe801cc78ae4488a3671 +// s2-update-base-32283: from 31376b0344b6df59ad009d4296125539 to 8cb0d4799893fe801cc78ae4488a3671 +// +// We don't keep all intermediate versions in memory, as it would cause massive +// memory usage during patching process. A prime example is the file +// DBFilesClient\\Item-Sparse.db2 from locale-enGB.MPQ (WoW 16965), which has +// 9 patches in a row, each requiring 70 MB memory (35 MB patch data + 35 MB work buffer) +// + +int Patch_Process(TMPQPatcher * pPatcher, TMPQFile * hf) +{ + PMPQ_PATCH_HEADER pFullPatch; + MPQ_PATCH_HEADER PatchHeader1; + MPQ_PATCH_HEADER PatchHeader2 = {0}; + TMPQFile * hfBase = hf; + DWORD cbBytesRead = 0; + int nError = ERROR_SUCCESS; + + // Move to the first patch + assert(hfBase->pbFileData == NULL); + assert(hfBase->cbFileData == 0); + hf = hf->hfPatch; + + // Read the header of the current patch + SFileReadFile((HANDLE)hf, &PatchHeader1, sizeof(MPQ_PATCH_HEADER), &cbBytesRead, NULL); + if(cbBytesRead != sizeof(MPQ_PATCH_HEADER)) + return ERROR_FILE_CORRUPT; + + // Perform the patching process + while(nError == ERROR_SUCCESS && hf != NULL) + { + // Try to read the next patch header. If the md5_before_patch + // still matches we go directly to the next one and repeat + while(hf->hfPatch != NULL) + { + // Attempt to read the patch header + SFileReadFile((HANDLE)hf->hfPatch, &PatchHeader2, sizeof(MPQ_PATCH_HEADER), &cbBytesRead, NULL); + if(cbBytesRead != sizeof(MPQ_PATCH_HEADER)) + return ERROR_FILE_CORRUPT; + + // Compare the md5_before_patch + if(memcmp(PatchHeader2.md5_before_patch, pPatcher->this_md5, MD5_DIGEST_SIZE)) + break; + + // Move one patch fuhrter + PatchHeader1 = PatchHeader2; + hf = hf->hfPatch; + } + + // Allocate memory for the patch data + pFullPatch = LoadFullFilePatch(hf, PatchHeader1); + if(pFullPatch != NULL) + { + // Apply the patch + nError = ApplyFilePatch(pPatcher, pFullPatch); + STORM_FREE(pFullPatch); + } + else + { + nError = ERROR_FILE_CORRUPT; + } + + // Move to the next patch + PatchHeader1 = PatchHeader2; + pPatcher->nCounter++; + hf = hf->hfPatch; + } + + // Put the result data to the file structure + if(nError == ERROR_SUCCESS) + { + // Swap the pointer to the file data structure + if(pPatcher->nCounter & 0x01) + { + hfBase->pbFileData = pPatcher->pbFileData2; + pPatcher->pbFileData2 = NULL; + } + else + { + hfBase->pbFileData = pPatcher->pbFileData1; + pPatcher->pbFileData1 = NULL; + } + + // Also supply the data size + hfBase->cbFileData = pPatcher->cbFileData; + } + + return ERROR_SUCCESS; +} + +void Patch_Finalize(TMPQPatcher * pPatcher) +{ + if(pPatcher != NULL) + { + if(pPatcher->pbFileData1 != NULL) + STORM_FREE(pPatcher->pbFileData1); + if(pPatcher->pbFileData2 != NULL) + STORM_FREE(pPatcher->pbFileData2); + + memset(pPatcher, 0, sizeof(TMPQPatcher)); + } +} + + +//----------------------------------------------------------------------------- +// Public functions + +// +// Patch prefix is the path subdirectory where the patched files are within MPQ. +// +// Example 1: +// Main MPQ: locale-enGB.MPQ +// Patch MPQ: wow-update-12694.MPQ +// File in main MPQ: DBFilesClient\Achievement.dbc +// File in patch MPQ: enGB\DBFilesClient\Achievement.dbc +// Path prefix: enGB +// +// Example 2: +// Main MPQ: expansion1.MPQ +// Patch MPQ: wow-update-12694.MPQ +// File in main MPQ: DBFilesClient\Achievement.dbc +// File in patch MPQ: Base\DBFilesClient\Achievement.dbc +// Path prefix: Base +// + +bool WINAPI SFileOpenPatchArchive( + HANDLE hMpq, + const TCHAR * szPatchMpqName, + const char * szPatchPathPrefix, + DWORD dwFlags) +{ + TMPQArchive * haPatch; + TMPQArchive * ha = (TMPQArchive *)hMpq; + HANDLE hPatchMpq = NULL; + int nError = ERROR_SUCCESS; + + // Keep compiler happy + dwFlags = dwFlags; + + // Verify input parameters + if(!IsValidMpqHandle(hMpq)) + nError = ERROR_INVALID_HANDLE; + if(szPatchMpqName == NULL || *szPatchMpqName == 0) + nError = ERROR_INVALID_PARAMETER; + + // + // We don't allow adding patches to archives that have been open for write + // + // Error scenario: + // + // 1) Open archive for writing + // 2) Modify or replace a file + // 3) Add patch archive to the opened MPQ + // 4) Read patched file + // 5) Now what ? + // + + if(nError == ERROR_SUCCESS) + { + if(!(ha->dwFlags & MPQ_FLAG_READ_ONLY)) + nError = ERROR_ACCESS_DENIED; + } + + // Open the archive like it is normal archive + if(nError == ERROR_SUCCESS) + { + if(!SFileOpenArchive(szPatchMpqName, 0, MPQ_OPEN_READ_ONLY | MPQ_OPEN_PATCH, &hPatchMpq)) + return false; + haPatch = (TMPQArchive *)hPatchMpq; + + // We need to remember the proper patch prefix to match names of patched files + FindPatchPrefix(ha, (TMPQArchive *)hPatchMpq, szPatchPathPrefix); + + // Now add the patch archive to the list of patches to the original MPQ + while(ha != NULL) + { + if(ha->haPatch == NULL) + { + haPatch->haBase = ha; + ha->haPatch = haPatch; + return true; + } + + // Move to the next archive + ha = ha->haPatch; + } + + // Should never happen + nError = ERROR_CAN_NOT_COMPLETE; + } + + SetLastError(nError); + return false; +} + +bool WINAPI SFileIsPatchedArchive(HANDLE hMpq) +{ + TMPQArchive * ha = (TMPQArchive *)hMpq; + + // Verify input parameters + if(!IsValidMpqHandle(hMpq)) + return false; + + return (ha->haPatch != NULL); +} diff --git a/src/SFileReadFile.cpp b/src/SFileReadFile.cpp index 73e6879..30dc587 100644 --- a/src/SFileReadFile.cpp +++ b/src/SFileReadFile.cpp @@ -680,14 +680,14 @@ bool WINAPI SFileReadFile(HANDLE hFile, void * pvBuffer, DWORD dwToRead, LPDWORD return false; } - if(pvBuffer == NULL || hf->pFileEntry == NULL) + if(pvBuffer == NULL) { SetLastError(ERROR_INVALID_PARAMETER); return false; } // If we didn't load the patch info yet, do it now - if((hf->pFileEntry->dwFlags & MPQ_FILE_PATCH_FILE) && hf->pPatchInfo == NULL) + if(hf->pFileEntry != NULL && (hf->pFileEntry->dwFlags & MPQ_FILE_PATCH_FILE) && hf->pPatchInfo == NULL) { nError = AllocatePatchInfo(hf, true); if(nError != ERROR_SUCCESS || hf->pPatchInfo == NULL) diff --git a/src/SFileVerify.cpp b/src/SFileVerify.cpp index 45ff58e..e68d51a 100644 --- a/src/SFileVerify.cpp +++ b/src/SFileVerify.cpp @@ -1,1054 +1,1054 @@ -/*****************************************************************************/ -/* SFileVerify.cpp Copyright (c) Ladislav Zezula 2010 */ -/*---------------------------------------------------------------------------*/ -/* MPQ files and MPQ archives verification. */ -/* */ -/* The MPQ signature verification has been written by Jean-Francois Roy */ -/* and Justin Olbrantz (Quantam). */ -/* The MPQ public keys have been created by MPQKit, using OpenSSL library. */ -/* */ -/*---------------------------------------------------------------------------*/ -/* Date Ver Who Comment */ -/* -------- ---- --- ------- */ -/* 04.05.10 1.00 Lad The first version of SFileVerify.cpp */ -/*****************************************************************************/ - -#define __STORMLIB_SELF__ -#include "StormLib.h" -#include "StormCommon.h" - -//----------------------------------------------------------------------------- -// Local defines - -#define MPQ_DIGEST_UNIT_SIZE 0x10000 - -//----------------------------------------------------------------------------- -// Known Blizzard public keys -// Created by Jean-Francois Roy using OpenSSL - -static const char * szBlizzardWeakPrivateKey = - "-----BEGIN PRIVATE KEY-----" - "MIIBOQIBAAJBAJJidwS/uILMBSO5DLGsBFknIXWWjQJe2kfdfEk3G/j66w4KkhZ1" - "V61Rt4zLaMVCYpDun7FLwRjkMDSepO1q2DcCAwEAAQJANtiztVDMJh2hE1hjPDKy" - "UmEJ9U/aN3gomuKOjbQbQ/bWWcM/WfhSVHmPqtqh/bQI2UXFr0rnXngeteZHLr/b" - "8QIhAMuWriSKGMACw18/rVVfUrThs915odKBH1Alr3vMVVzZAiEAuBHPSQkgwcb6" - "L4MWaiKuOzq08mSyNqPeN8oSy18q848CIHeMn+3s+eOmu7su1UYQl6yH7OrdBd1q" - "3UxfFNEJiAbhAiAqxdCyOxHGlbM7aS3DOg3cq5ayoN2cvtV7h1R4t8OmVwIgF+5z" - "/6vkzBUsZhd8Nwyis+MeQYH0rpFpMKdTlqmPF2Q=" - "-----END PRIVATE KEY-----"; - -static const char * szBlizzardWeakPublicKey = - "-----BEGIN PUBLIC KEY-----" - "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJJidwS/uILMBSO5DLGsBFknIXWWjQJe" - "2kfdfEk3G/j66w4KkhZ1V61Rt4zLaMVCYpDun7FLwRjkMDSepO1q2DcCAwEAAQ==" - "-----END PUBLIC KEY-----"; - -static const char * szBlizzardStrongPublicKey = - "-----BEGIN PUBLIC KEY-----" - "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsQZ+ziT2h8h+J/iMQpgd" - "tH1HaJzOBE3agjU4yMPcrixaPOZoA4t8bwfey7qczfWywocYo3pleytFF+IuD4HD" - "Fl9OXN1SFyupSgMx1EGZlgbFAomnbq9MQJyMqQtMhRAjFgg4TndS7YNb+JMSAEKp" - "kXNqY28n/EVBHD5TsMuVCL579gIenbr61dI92DDEdy790IzIG0VKWLh/KOTcTJfm" - "Ds/7HQTkGouVW+WUsfekuqNQo7ND9DBnhLjLjptxeFE2AZqYcA1ao3S9LN3GL1tW" - "lVXFIX9c7fWqaVTQlZ2oNsI/ARVApOK3grNgqvwH6YoVYVXjNJEo5sQJsPsdV/hk" - "dwIDAQAB" - "-----END PUBLIC KEY-----"; - -static const char * szWarcraft3MapPublicKey = - "-----BEGIN PUBLIC KEY-----" - "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1BwklUUQ3UvjizOBRoF5" - "yyOVc7KD+oGOQH5i6eUk1yfs0luCC70kNucNrfqhmviywVtahRse1JtXCPrx2bd3" - "iN8Dx91fbkxjYIOGTsjYoHKTp0BbaFkJih776fcHgnFSb+7mJcDuJVvJOXxEH6w0" - "1vo6VtujCqj1arqbyoal+xtAaczF3us5cOEp45sR1zAWTn1+7omN7VWV4QqJPaDS" - "gBSESc0l1grO0i1VUSumayk7yBKIkb+LBvcG6WnYZHCi7VdLmaxER5m8oZfER66b" - "heHoiSQIZf9PAY6Guw2DT5BTc54j/AaLQAKf2qcRSgQLVo5kQaddF3rCpsXoB/74" - "6QIDAQAB" - "-----END PUBLIC KEY-----"; - -static const char * szWowPatchPublicKey = - "-----BEGIN PUBLIC KEY-----" - "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwOsMV0LagAWPEtEQM6b9" - "6FHFkUyGbbyda2/Dfc9dyl21E9QvX+Yw7qKRMAKPzA2TlQQLZKvXpnKXF/YIK5xa" - "5uwg9CEHCEAYolLG4xn0FUOE0E/0PuuytI0p0ICe6rk00PifZzTr8na2wI/l/GnQ" - "bvnIVF1ck6cslATpQJ5JJVMXzoFlUABS19WESw4MXuJAS3AbMhxNWdEhVv7eO51c" - "yGjRLy9QjogZODZTY0fSEksgBqQxNCoYVJYI/sF5K2flDsGqrIp0OdJ6teJlzg1Y" - "UjYnb6bKjlidXoHEXI2TgA/mD6O3XFIt08I9s3crOCTgICq7cgX35qrZiIVWZdRv" - "TwIDAQAB" - "-----END PUBLIC KEY-----"; - -static const char * szWowSurveyPublicKey = - "-----BEGIN PUBLIC KEY-----" - "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnIt1DR6nRyyKsy2qahHe" - "MKLtacatn/KxieHcwH87wLBxKy+jZ0gycTmJ7SaTdBAEMDs/V5IPIXEtoqYnid2c" - "63TmfGDU92oc3Ph1PWUZ2PWxBhT06HYxRdbrgHw9/I29pNPi/607x+lzPORITOgU" - "BR6MR8au8HsQP4bn4vkJNgnSgojh48/XQOB/cAln7As1neP61NmVimoLR4Bwi3zt" - "zfgrZaUpyeNCUrOYJmH09YIjbBySTtXOUidoPHjFrMsCWpr6xs8xbETbs7MJFL6a" - "vcUfTT67qfIZ9RsuKfnXJTIrV0kwDSjjuNXiPTmWAehSsiHIsrUXX5RNcwsSjClr" - "nQIDAQAB" - "-----END PUBLIC KEY-----"; - -static const char * szStarcraft2MapPublicKey = - "-----BEGIN PUBLIC KEY-----" - "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmk4GT8zb+ICC25a17KZB" - "q/ygKGJ2VSO6IT5PGHJlm1KfnHBA4B6SH3xMlJ4c6eG2k7QevZv+FOhjsAHubyWq" - "2VKqWbrIFKv2ILc2RfMn8J9EDVRxvcxh6slRrVL69D0w1tfVGjMiKq2Fym5yGoRT" - "E7CRgDqbAbXP9LBsCNWHiJLwfxMGzHbk8pIl9oia5pvM7ofZamSHchxlpy6xa4GJ" - "7xKN01YCNvklTL1D7uol3wkwcHc7vrF8QwuJizuA5bSg4poEGtH62BZOYi+UL/z0" - "31YK+k9CbQyM0X0pJoJoYz1TK+Y5J7vBnXCZtfcTYQ/ZzN6UcxTa57dJaiOlCh9z" - "nQIDAQAB" - "-----END PUBLIC KEY-----"; - -//----------------------------------------------------------------------------- -// Local functions - -static void memrev(unsigned char *buf, size_t count) -{ - unsigned char *r; - - for (r = buf + count - 1; buf < r; buf++, r--) - { - *buf ^= *r; - *r ^= *buf; - *buf ^= *r; - } -} - -static bool decode_base64_key(const char * szKeyBase64, rsa_key * key) -{ - unsigned char decoded_key[0x200]; - const char * szBase64Begin; - const char * szBase64End; - unsigned long decoded_length = sizeof(decoded_key); - unsigned long length; - - // Find out the begin of the BASE64 data - szBase64Begin = szKeyBase64 + strlen("-----BEGIN PUBLIC KEY-----"); - szBase64End = szBase64Begin + strlen(szBase64Begin) - strlen("-----END PUBLIC KEY-----"); - if(szBase64End[0] != '-') - return false; - - // decode the base64 string - length = (unsigned long)(szBase64End - szBase64Begin); - if(base64_decode((unsigned char *)szBase64Begin, length, decoded_key, &decoded_length) != CRYPT_OK) - return false; - - // Create RSA key - if(rsa_import(decoded_key, decoded_length, key) != CRYPT_OK) - return false; - - return true; -} - -static void GetPlainAnsiFileName( - const TCHAR * szFileName, - char * szPlainName) -{ - const TCHAR * szPlainNameT = GetPlainFileName(szFileName); - - // Convert the plain name to ANSI - while(*szPlainNameT != 0) - *szPlainName++ = (char)*szPlainNameT++; - *szPlainName = 0; -} - -// Calculate begin and end of the MPQ archive -static void CalculateArchiveRange( - TMPQArchive * ha, - PMPQ_SIGNATURE_INFO pSI) -{ - ULONGLONG TempPos = 0; - char szMapHeader[0x200]; - - // Get the MPQ begin - pSI->BeginMpqData = ha->MpqPos; - - // Warcraft III maps are signed from the map header to the end - if(FileStream_Read(ha->pStream, &TempPos, szMapHeader, sizeof(szMapHeader))) - { - // Is it a map header ? - if(szMapHeader[0] == 'H' && szMapHeader[1] == 'M' && szMapHeader[2] == '3' && szMapHeader[3] == 'W') - { - // We will have to hash since the map header - pSI->BeginMpqData = 0; - } - } - - // Get the MPQ data end. This is stored in the MPQ header - pSI->EndMpqData = ha->MpqPos + ha->pHeader->ArchiveSize64; - - // Get the size of the entire file - FileStream_GetSize(ha->pStream, &pSI->EndOfFile); -} - -static bool CalculateMpqHashMd5( - TMPQArchive * ha, - PMPQ_SIGNATURE_INFO pSI, - LPBYTE pMd5Digest) -{ - hash_state md5_state; - ULONGLONG BeginBuffer; - ULONGLONG EndBuffer; - LPBYTE pbDigestBuffer = NULL; - - // Allocate buffer for creating the MPQ digest. - pbDigestBuffer = STORM_ALLOC(BYTE, MPQ_DIGEST_UNIT_SIZE); - if(pbDigestBuffer == NULL) - return false; - - // Initialize the MD5 hash state - md5_init(&md5_state); - - // Set the byte offset of begin of the data - BeginBuffer = pSI->BeginMpqData; - - // Create the digest - for(;;) - { - ULONGLONG BytesRemaining; - LPBYTE pbSigBegin = NULL; - LPBYTE pbSigEnd = NULL; - DWORD dwToRead = MPQ_DIGEST_UNIT_SIZE; - - // Check the number of bytes remaining - BytesRemaining = pSI->EndMpqData - BeginBuffer; - if(BytesRemaining < MPQ_DIGEST_UNIT_SIZE) - dwToRead = (DWORD)BytesRemaining; - if(dwToRead == 0) - break; - - // Read the next chunk - if(!FileStream_Read(ha->pStream, &BeginBuffer, pbDigestBuffer, dwToRead)) - { - STORM_FREE(pbDigestBuffer); - return false; - } - - // Move the current byte offset - EndBuffer = BeginBuffer + dwToRead; - - // Check if the signature is within the loaded digest - if(BeginBuffer <= pSI->BeginExclude && pSI->BeginExclude < EndBuffer) - pbSigBegin = pbDigestBuffer + (size_t)(pSI->BeginExclude - BeginBuffer); - if(BeginBuffer <= pSI->EndExclude && pSI->EndExclude < EndBuffer) - pbSigEnd = pbDigestBuffer + (size_t)(pSI->EndExclude - BeginBuffer); - - // Zero the part that belongs to the signature - if(pbSigBegin != NULL || pbSigEnd != NULL) - { - if(pbSigBegin == NULL) - pbSigBegin = pbDigestBuffer; - if(pbSigEnd == NULL) - pbSigEnd = pbDigestBuffer + dwToRead; - - memset(pbSigBegin, 0, (pbSigEnd - pbSigBegin)); - } - - // Pass the buffer to the hashing function - md5_process(&md5_state, pbDigestBuffer, dwToRead); - - // Move pointers - BeginBuffer += dwToRead; - } - - // Finalize the MD5 hash - md5_done(&md5_state, pMd5Digest); - STORM_FREE(pbDigestBuffer); - return true; -} - -static void AddTailToSha1( - hash_state * psha1_state, - const char * szTail) -{ - unsigned char * pbTail = (unsigned char *)szTail; - unsigned char szUpperCase[0x200]; - unsigned long nLength = 0; - - // Convert the tail to uppercase - // Note that we don't need to terminate the string with zero - while(*pbTail != 0) - { - szUpperCase[nLength++] = AsciiToUpperTable[*pbTail++]; - } - - // Append the tail to the SHA1 - sha1_process(psha1_state, szUpperCase, nLength); -} - -static bool CalculateMpqHashSha1( - TMPQArchive * ha, - PMPQ_SIGNATURE_INFO pSI, - unsigned char * sha1_tail0, - unsigned char * sha1_tail1, - unsigned char * sha1_tail2) -{ - ULONGLONG BeginBuffer; - hash_state sha1_state_temp; - hash_state sha1_state; - LPBYTE pbDigestBuffer = NULL; - char szPlainName[MAX_PATH]; - - // Allocate buffer for creating the MPQ digest. - pbDigestBuffer = STORM_ALLOC(BYTE, MPQ_DIGEST_UNIT_SIZE); - if(pbDigestBuffer == NULL) - return false; - - // Initialize SHA1 state structure - sha1_init(&sha1_state); - - // Calculate begin of data to be hashed - BeginBuffer = pSI->BeginMpqData; - - // Create the digest - for(;;) - { - ULONGLONG BytesRemaining; - DWORD dwToRead = MPQ_DIGEST_UNIT_SIZE; - - // Check the number of bytes remaining - BytesRemaining = pSI->EndMpqData - BeginBuffer; - if(BytesRemaining < MPQ_DIGEST_UNIT_SIZE) - dwToRead = (DWORD)BytesRemaining; - if(dwToRead == 0) - break; - - // Read the next chunk - if(!FileStream_Read(ha->pStream, &BeginBuffer, pbDigestBuffer, dwToRead)) - { - STORM_FREE(pbDigestBuffer); - return false; - } - - // Pass the buffer to the hashing function - sha1_process(&sha1_state, pbDigestBuffer, dwToRead); - - // Move pointers - BeginBuffer += dwToRead; - } - - // Add all three known tails and generate three hashes - memcpy(&sha1_state_temp, &sha1_state, sizeof(hash_state)); - sha1_done(&sha1_state_temp, sha1_tail0); - - memcpy(&sha1_state_temp, &sha1_state, sizeof(hash_state)); - GetPlainAnsiFileName(FileStream_GetFileName(ha->pStream), szPlainName); - AddTailToSha1(&sha1_state_temp, szPlainName); - sha1_done(&sha1_state_temp, sha1_tail1); - - memcpy(&sha1_state_temp, &sha1_state, sizeof(hash_state)); - AddTailToSha1(&sha1_state_temp, "ARCHIVE"); - sha1_done(&sha1_state_temp, sha1_tail2); - - // Finalize the MD5 hash - STORM_FREE(pbDigestBuffer); - return true; -} - -static int VerifyRawMpqData( - TMPQArchive * ha, - ULONGLONG ByteOffset, - DWORD dwDataSize) -{ - ULONGLONG DataOffset = ha->MpqPos + ByteOffset; - LPBYTE pbDataChunk; - LPBYTE pbMD5Array1; // Calculated MD5 array - LPBYTE pbMD5Array2; // MD5 array loaded from the MPQ - DWORD dwBytesInChunk; - DWORD dwChunkCount; - DWORD dwChunkSize = ha->pHeader->dwRawChunkSize; - DWORD dwMD5Size; - int nError = ERROR_SUCCESS; - - // Don't verify zero-sized blocks - if(dwDataSize == 0) - return ERROR_SUCCESS; - - // Get the number of data chunks to calculate MD5 - assert(dwChunkSize != 0); - dwChunkCount = ((dwDataSize - 1) / dwChunkSize) + 1; - dwMD5Size = dwChunkCount * MD5_DIGEST_SIZE; - - // Allocate space for data chunk and for the MD5 array - pbDataChunk = STORM_ALLOC(BYTE, dwChunkSize); - if(pbDataChunk == NULL) - return ERROR_NOT_ENOUGH_MEMORY; - - // Allocate space for MD5 array - pbMD5Array1 = STORM_ALLOC(BYTE, dwMD5Size); - pbMD5Array2 = STORM_ALLOC(BYTE, dwMD5Size); - if(pbMD5Array1 == NULL || pbMD5Array2 == NULL) - nError = ERROR_NOT_ENOUGH_MEMORY; - - // Calculate MD5 of each data chunk - if(nError == ERROR_SUCCESS) - { - LPBYTE pbMD5 = pbMD5Array1; - - for(DWORD i = 0; i < dwChunkCount; i++) - { - // Get the number of bytes in the chunk - dwBytesInChunk = STORMLIB_MIN(dwChunkSize, dwDataSize); - - // Read the data chunk - if(!FileStream_Read(ha->pStream, &DataOffset, pbDataChunk, dwBytesInChunk)) - { - nError = ERROR_FILE_CORRUPT; - break; - } - - // Calculate MD5 - CalculateDataBlockHash(pbDataChunk, dwBytesInChunk, pbMD5); - - // Move pointers and offsets - DataOffset += dwBytesInChunk; - dwDataSize -= dwBytesInChunk; - pbMD5 += MD5_DIGEST_SIZE; - } - } - - // Read the MD5 array - if(nError == ERROR_SUCCESS) - { - // Read the array of MD5 - if(!FileStream_Read(ha->pStream, &DataOffset, pbMD5Array2, dwMD5Size)) - nError = GetLastError(); - } - - // Compare the array of MD5 - if(nError == ERROR_SUCCESS) - { - // Compare the MD5 - if(memcmp(pbMD5Array1, pbMD5Array2, dwMD5Size)) - nError = ERROR_FILE_CORRUPT; - } - - // Free memory and return result - if(pbMD5Array2 != NULL) - STORM_FREE(pbMD5Array2); - if(pbMD5Array1 != NULL) - STORM_FREE(pbMD5Array1); - if(pbDataChunk != NULL) - STORM_FREE(pbDataChunk); - return nError; -} - -static DWORD VerifyWeakSignature( - TMPQArchive * ha, - PMPQ_SIGNATURE_INFO pSI) -{ - BYTE RevSignature[MPQ_WEAK_SIGNATURE_SIZE]; - BYTE Md5Digest[MD5_DIGEST_SIZE]; - rsa_key key; - int hash_idx = find_hash("md5"); - int result = 0; - - // The signature might be zeroed out. In that case, we ignore it - if(!IsValidSignature(pSI->Signature)) - return ERROR_WEAK_SIGNATURE_OK; - - // Calculate hash of the entire archive, skipping the (signature) file - if(!CalculateMpqHashMd5(ha, pSI, Md5Digest)) - return ERROR_VERIFY_FAILED; - - // Import the Blizzard key in OpenSSL format - if(!decode_base64_key(szBlizzardWeakPublicKey, &key)) - return ERROR_VERIFY_FAILED; - - // Verify the signature - memcpy(RevSignature, &pSI->Signature[8], MPQ_WEAK_SIGNATURE_SIZE); - memrev(RevSignature, MPQ_WEAK_SIGNATURE_SIZE); - rsa_verify_hash_ex(RevSignature, MPQ_WEAK_SIGNATURE_SIZE, Md5Digest, sizeof(Md5Digest), LTC_LTC_PKCS_1_V1_5, hash_idx, 0, &result, &key); - rsa_free(&key); - - // Return the result - return result ? ERROR_WEAK_SIGNATURE_OK : ERROR_WEAK_SIGNATURE_ERROR; -} - -static DWORD VerifyStrongSignatureWithKey( - unsigned char * reversed_signature, - unsigned char * padded_digest, - const char * szPublicKey) -{ - rsa_key key; - int result = 0; - - // Import the Blizzard key in OpenSSL format - if(!decode_base64_key(szPublicKey, &key)) - { - assert(false); - return ERROR_VERIFY_FAILED; - } - - // Verify the signature - if(rsa_verify_simple(reversed_signature, MPQ_STRONG_SIGNATURE_SIZE, padded_digest, MPQ_STRONG_SIGNATURE_SIZE, &result, &key) != CRYPT_OK) - return ERROR_VERIFY_FAILED; - - // Free the key and return result - rsa_free(&key); - return result ? ERROR_STRONG_SIGNATURE_OK : ERROR_STRONG_SIGNATURE_ERROR; -} - -static DWORD VerifyStrongSignature( - TMPQArchive * ha, - PMPQ_SIGNATURE_INFO pSI) -{ - unsigned char reversed_signature[MPQ_STRONG_SIGNATURE_SIZE]; - unsigned char Sha1Digest_tail0[SHA1_DIGEST_SIZE]; - unsigned char Sha1Digest_tail1[SHA1_DIGEST_SIZE]; - unsigned char Sha1Digest_tail2[SHA1_DIGEST_SIZE]; - unsigned char padded_digest[MPQ_STRONG_SIGNATURE_SIZE]; - DWORD dwResult; - size_t digest_offset; - - // Calculate SHA1 hash of the archive - if(!CalculateMpqHashSha1(ha, pSI, Sha1Digest_tail0, Sha1Digest_tail1, Sha1Digest_tail2)) - return ERROR_VERIFY_FAILED; - - // Prepare the signature for decryption - memcpy(reversed_signature, &pSI->Signature[4], MPQ_STRONG_SIGNATURE_SIZE); - memrev(reversed_signature, MPQ_STRONG_SIGNATURE_SIZE); - - // Prepare the padded digest for comparison - digest_offset = sizeof(padded_digest) - SHA1_DIGEST_SIZE; - memset(padded_digest, 0xbb, digest_offset); - padded_digest[0] = 0x0b; - - // Try Blizzard Strong public key with no SHA1 tail - memcpy(padded_digest + digest_offset, Sha1Digest_tail0, SHA1_DIGEST_SIZE); - memrev(padded_digest + digest_offset, SHA1_DIGEST_SIZE); - dwResult = VerifyStrongSignatureWithKey(reversed_signature, padded_digest, szBlizzardStrongPublicKey); - if(dwResult == ERROR_STRONG_SIGNATURE_OK) - return dwResult; - - // Try War 3 map public key with plain file name as SHA1 tail - memcpy(padded_digest + digest_offset, Sha1Digest_tail1, SHA1_DIGEST_SIZE); - memrev(padded_digest + digest_offset, SHA1_DIGEST_SIZE); - dwResult = VerifyStrongSignatureWithKey(reversed_signature, padded_digest, szWarcraft3MapPublicKey); - if(dwResult == ERROR_STRONG_SIGNATURE_OK) - return dwResult; - - // Try WoW-TBC public key with "ARCHIVE" as SHA1 tail - memcpy(padded_digest + digest_offset, Sha1Digest_tail2, SHA1_DIGEST_SIZE); - memrev(padded_digest + digest_offset, SHA1_DIGEST_SIZE); - dwResult = VerifyStrongSignatureWithKey(reversed_signature, padded_digest, szWowPatchPublicKey); - if(dwResult == ERROR_STRONG_SIGNATURE_OK) - return dwResult; - - // Try Survey public key with no SHA1 tail - memcpy(padded_digest + digest_offset, Sha1Digest_tail0, SHA1_DIGEST_SIZE); - memrev(padded_digest + digest_offset, SHA1_DIGEST_SIZE); - dwResult = VerifyStrongSignatureWithKey(reversed_signature, padded_digest, szWowSurveyPublicKey); - if(dwResult == ERROR_STRONG_SIGNATURE_OK) - return dwResult; - - // Try Starcraft II public key with no SHA1 tail - memcpy(padded_digest + digest_offset, Sha1Digest_tail0, SHA1_DIGEST_SIZE); - memrev(padded_digest + digest_offset, SHA1_DIGEST_SIZE); - dwResult = VerifyStrongSignatureWithKey(reversed_signature, padded_digest, szStarcraft2MapPublicKey); - if(dwResult == ERROR_STRONG_SIGNATURE_OK) - return dwResult; - - return ERROR_STRONG_SIGNATURE_ERROR; -} - -static DWORD VerifyFile( - HANDLE hMpq, - const char * szFileName, - LPDWORD pdwCrc32, - char * pMD5, - DWORD dwFlags) -{ - hash_state md5_state; - unsigned char * pFileMd5; - unsigned char md5[MD5_DIGEST_SIZE]; - TFileEntry * pFileEntry; - TMPQFile * hf; - BYTE Buffer[0x1000]; - HANDLE hFile = NULL; - DWORD dwVerifyResult = 0; - DWORD dwTotalBytes = 0; - DWORD dwCrc32 = 0; - - // - // Note: When the MPQ is patched, it will - // automatically check the patched version of the file - // - - // Make sure the md5 is initialized - memset(md5, 0, sizeof(md5)); - - // If we have to verify raw data MD5, do it before file open - if(dwFlags & SFILE_VERIFY_RAW_MD5) - { - TMPQArchive * ha = (TMPQArchive *)hMpq; - - // Parse the base MPQ and all patches - while(ha != NULL) - { - // Does the archive have support for raw MD5? - if(ha->pHeader->dwRawChunkSize != 0) - { - // The file has raw MD5 if the archive supports it - dwVerifyResult |= VERIFY_FILE_HAS_RAW_MD5; - - // Find file entry for the file - pFileEntry = GetFileEntryLocale(ha, szFileName, lcFileLocale); - if(pFileEntry != NULL) - { - // If the file's raw MD5 doesn't match, don't bother with more checks - if(VerifyRawMpqData(ha, pFileEntry->ByteOffset, pFileEntry->dwCmpSize) != ERROR_SUCCESS) - return dwVerifyResult | VERIFY_FILE_RAW_MD5_ERROR; - } - } - - // Move to the next patch - ha = ha->haPatch; - } - } - - // Attempt to open the file - if(SFileOpenFileEx(hMpq, szFileName, SFILE_OPEN_FROM_MPQ, &hFile)) - { - // Get the file size - hf = (TMPQFile *)hFile; - pFileEntry = hf->pFileEntry; - dwTotalBytes = SFileGetFileSize(hFile, NULL); - - // Initialize the CRC32 and MD5 contexts - md5_init(&md5_state); - dwCrc32 = crc32(0, Z_NULL, 0); - - // Also turn on sector checksum verification - if(dwFlags & SFILE_VERIFY_SECTOR_CRC) - hf->bCheckSectorCRCs = true; - - // Go through entire file and update both CRC32 and MD5 - for(;;) - { - DWORD dwBytesRead = 0; - - // Read data from file - SFileReadFile(hFile, Buffer, sizeof(Buffer), &dwBytesRead, NULL); - if(dwBytesRead == 0) - { - if(GetLastError() == ERROR_CHECKSUM_ERROR) - dwVerifyResult |= VERIFY_FILE_SECTOR_CRC_ERROR; - break; - } - - // Update CRC32 value - if(dwFlags & SFILE_VERIFY_FILE_CRC) - dwCrc32 = crc32(dwCrc32, Buffer, dwBytesRead); - - // Update MD5 value - if(dwFlags & SFILE_VERIFY_FILE_MD5) - md5_process(&md5_state, Buffer, dwBytesRead); - - // Decrement the total size - dwTotalBytes -= dwBytesRead; - } - - // If the file has sector checksums, indicate it in the flags - if(dwFlags & SFILE_VERIFY_SECTOR_CRC) - { - if((hf->pFileEntry->dwFlags & MPQ_FILE_SECTOR_CRC) && hf->SectorChksums != NULL && hf->SectorChksums[0] != 0) - dwVerifyResult |= VERIFY_FILE_HAS_SECTOR_CRC; - } - - // Check if the entire file has been read - // No point in checking CRC32 and MD5 if not - // Skip checksum checks if the file has patches - if(dwTotalBytes == 0) - { - // Check CRC32 and MD5 only if there is no patches - if(hf->hfPatch == NULL) - { - // Check if the CRC32 matches. - if(dwFlags & SFILE_VERIFY_FILE_CRC) - { - // Only check the CRC32 if it is valid - if(pFileEntry->dwCrc32 != 0) - { - dwVerifyResult |= VERIFY_FILE_HAS_CHECKSUM; - if(dwCrc32 != pFileEntry->dwCrc32) - dwVerifyResult |= VERIFY_FILE_CHECKSUM_ERROR; - } - } - - // Check if MD5 matches - if(dwFlags & SFILE_VERIFY_FILE_MD5) - { - // Patch files have their MD5 saved in the patch info - pFileMd5 = (hf->pPatchInfo != NULL) ? hf->pPatchInfo->md5 : pFileEntry->md5; - md5_done(&md5_state, md5); - - // Only check the MD5 if it is valid - if(IsValidMD5(pFileMd5)) - { - dwVerifyResult |= VERIFY_FILE_HAS_MD5; - if(memcmp(md5, pFileMd5, MD5_DIGEST_SIZE)) - dwVerifyResult |= VERIFY_FILE_MD5_ERROR; - } - } - } - else - { - // Patched files are MD5-checked automatically - dwVerifyResult |= VERIFY_FILE_HAS_MD5; - } - } - else - { - dwVerifyResult |= VERIFY_READ_ERROR; - } - - SFileCloseFile(hFile); - } - else - { - // Remember that the file couldn't be open - dwVerifyResult |= VERIFY_OPEN_ERROR; - } - - // If the caller required CRC32 and/or MD5, give it to him - if(pdwCrc32 != NULL) - *pdwCrc32 = dwCrc32; - if(pMD5 != NULL) - memcpy(pMD5, md5, MD5_DIGEST_SIZE); - - return dwVerifyResult; -} - -// Used in SFileGetFileInfo -bool QueryMpqSignatureInfo( - TMPQArchive * ha, - PMPQ_SIGNATURE_INFO pSI) -{ - TFileEntry * pFileEntry; - ULONGLONG ExtraBytes; - DWORD dwFileSize; - - // Make sure it's all zeroed - memset(pSI, 0, sizeof(MPQ_SIGNATURE_INFO)); - - // Calculate the range of the MPQ - CalculateArchiveRange(ha, pSI); - - // If there is "(signature)" file in the MPQ, it has a weak signature - pFileEntry = GetFileEntryLocale(ha, SIGNATURE_NAME, LANG_NEUTRAL); - if(pFileEntry != NULL) - { - // Calculate the begin and end of the signature file itself - pSI->BeginExclude = ha->MpqPos + pFileEntry->ByteOffset; - pSI->EndExclude = pSI->BeginExclude + pFileEntry->dwCmpSize; - dwFileSize = (DWORD)(pSI->EndExclude - pSI->BeginExclude); - - // Does the signature have proper size? - if(dwFileSize == MPQ_SIGNATURE_FILE_SIZE) - { - // Read the weak signature - if(!FileStream_Read(ha->pStream, &pSI->BeginExclude, pSI->Signature, dwFileSize)) - return false; - - pSI->SignatureTypes |= SIGNATURE_TYPE_WEAK; - pSI->cbSignatureSize = dwFileSize; - return true; - } - } - - // If there is extra bytes beyond the end of the archive, - // it's the strong signature - ExtraBytes = pSI->EndOfFile - pSI->EndMpqData; - if(ExtraBytes >= (MPQ_STRONG_SIGNATURE_SIZE + 4)) - { - // Read the strong signature - if(!FileStream_Read(ha->pStream, &pSI->EndMpqData, pSI->Signature, (MPQ_STRONG_SIGNATURE_SIZE + 4))) - return false; - - // Check the signature header "NGIS" - if(pSI->Signature[0] != 'N' || pSI->Signature[1] != 'G' || pSI->Signature[2] != 'I' || pSI->Signature[3] != 'S') - return false; - - pSI->SignatureTypes |= SIGNATURE_TYPE_STRONG; - return true; - } - - // Succeeded, but no known signature found - return true; -} - -//----------------------------------------------------------------------------- -// Support for weak signature - -int SSignFileCreate(TMPQArchive * ha) -{ - TMPQFile * hf = NULL; - BYTE EmptySignature[MPQ_SIGNATURE_FILE_SIZE]; - int nError = ERROR_SUCCESS; - - // Only save the signature if we should do so - if(ha->dwFileFlags3 != 0) - { - // The (signature) file must be non-encrypted and non-compressed - assert(ha->dwFlags & MPQ_FLAG_SIGNATURE_NEW); - assert(ha->dwFileFlags3 == MPQ_FILE_EXISTS); - assert(ha->dwReservedFiles > 0); - - // Create the (signature) file file in the MPQ - // Note that the file must not be compressed or encrypted - nError = SFileAddFile_Init(ha, SIGNATURE_NAME, - 0, - sizeof(EmptySignature), - LANG_NEUTRAL, - ha->dwFileFlags3 | MPQ_FILE_REPLACEEXISTING, - &hf); - - // Write the empty signature file to the archive - if(nError == ERROR_SUCCESS) - { - // Write the empty zeroed file to the MPQ - memset(EmptySignature, 0, sizeof(EmptySignature)); - nError = SFileAddFile_Write(hf, EmptySignature, (DWORD)sizeof(EmptySignature), 0); - SFileAddFile_Finish(hf); - - // Clear the invalid mark - ha->dwFlags &= ~(MPQ_FLAG_SIGNATURE_NEW | MPQ_FLAG_SIGNATURE_NONE); - ha->dwReservedFiles--; - } - } - - return nError; -} - -int SSignFileFinish(TMPQArchive * ha) -{ - MPQ_SIGNATURE_INFO si; - unsigned long signature_len = MPQ_WEAK_SIGNATURE_SIZE; - BYTE WeakSignature[MPQ_SIGNATURE_FILE_SIZE]; - BYTE Md5Digest[MD5_DIGEST_SIZE]; - rsa_key key; - int hash_idx = find_hash("md5"); - - // Sanity checks - assert((ha->dwFlags & MPQ_FLAG_CHANGED) == 0); - assert(ha->dwFileFlags3 == MPQ_FILE_EXISTS); - - // Query the weak signature info - memset(&si, 0, sizeof(MPQ_SIGNATURE_INFO)); - if(!QueryMpqSignatureInfo(ha, &si)) - return ERROR_FILE_CORRUPT; - - // There must be exactly one signature - if(si.SignatureTypes != SIGNATURE_TYPE_WEAK) - return ERROR_FILE_CORRUPT; - - // Calculate MD5 of the entire archive - if(!CalculateMpqHashMd5(ha, &si, Md5Digest)) - return ERROR_VERIFY_FAILED; - - // Decode the private key - if(!decode_base64_key(szBlizzardWeakPrivateKey, &key)) - return ERROR_VERIFY_FAILED; - - // Sign the hash - memset(WeakSignature, 0, sizeof(WeakSignature)); - rsa_sign_hash_ex(Md5Digest, sizeof(Md5Digest), WeakSignature + 8, &signature_len, LTC_LTC_PKCS_1_V1_5, 0, 0, hash_idx, 0, &key); - memrev(WeakSignature + 8, MPQ_WEAK_SIGNATURE_SIZE); - rsa_free(&key); - - // Write the signature to the MPQ. Don't use SFile* functions, but write the hash directly - if(!FileStream_Write(ha->pStream, &si.BeginExclude, WeakSignature, MPQ_SIGNATURE_FILE_SIZE)) - return GetLastError(); - - return ERROR_SUCCESS; -} - -//----------------------------------------------------------------------------- -// Public (exported) functions - -bool WINAPI SFileGetFileChecksums(HANDLE hMpq, const char * szFileName, LPDWORD pdwCrc32, char * pMD5) -{ - DWORD dwVerifyResult; - DWORD dwVerifyFlags = 0; - - if(pdwCrc32 != NULL) - dwVerifyFlags |= SFILE_VERIFY_FILE_CRC; - if(pMD5 != NULL) - dwVerifyFlags |= SFILE_VERIFY_FILE_MD5; - - dwVerifyResult = VerifyFile(hMpq, - szFileName, - pdwCrc32, - pMD5, - dwVerifyFlags); - - // If verification failed, return zero - if(dwVerifyResult & VERIFY_FILE_ERROR_MASK) - { - SetLastError(ERROR_FILE_CORRUPT); - return false; - } - - return true; -} - - -DWORD WINAPI SFileVerifyFile(HANDLE hMpq, const char * szFileName, DWORD dwFlags) -{ - return VerifyFile(hMpq, - szFileName, - NULL, - NULL, - dwFlags); -} - -// Verifies raw data of the archive Only works for MPQs version 4 or newer -int WINAPI SFileVerifyRawData(HANDLE hMpq, DWORD dwWhatToVerify, const char * szFileName) -{ - TMPQArchive * ha = (TMPQArchive *)hMpq; - TFileEntry * pFileEntry; - TMPQHeader * pHeader; - - // Verify input parameters - if(!IsValidMpqHandle(hMpq)) - return ERROR_INVALID_PARAMETER; - pHeader = ha->pHeader; - - // If the archive doesn't have raw data MD5, report it as OK - if(pHeader->dwRawChunkSize == 0) - return ERROR_SUCCESS; - - // If we have to verify MPQ header, do it - switch(dwWhatToVerify) - { - case SFILE_VERIFY_MPQ_HEADER: - - // Only if the header is of version 4 or newer - if(pHeader->dwHeaderSize >= (MPQ_HEADER_SIZE_V4 - MD5_DIGEST_SIZE)) - return VerifyRawMpqData(ha, 0, MPQ_HEADER_SIZE_V4 - MD5_DIGEST_SIZE); - return ERROR_SUCCESS; - - case SFILE_VERIFY_HET_TABLE: - - // Only if we have HET table - if(pHeader->HetTablePos64 && pHeader->HetTableSize64) - return VerifyRawMpqData(ha, pHeader->HetTablePos64, (DWORD)pHeader->HetTableSize64); - return ERROR_SUCCESS; - - case SFILE_VERIFY_BET_TABLE: - - // Only if we have BET table - if(pHeader->BetTablePos64 && pHeader->BetTableSize64) - return VerifyRawMpqData(ha, pHeader->BetTablePos64, (DWORD)pHeader->BetTableSize64); - return ERROR_SUCCESS; - - case SFILE_VERIFY_HASH_TABLE: - - // Hash table is not protected by MD5 - return ERROR_SUCCESS; - - case SFILE_VERIFY_BLOCK_TABLE: - - // Block table is not protected by MD5 - return ERROR_SUCCESS; - - case SFILE_VERIFY_HIBLOCK_TABLE: - - // It is unknown if the hi-block table is protected my MD5 or not. - return ERROR_SUCCESS; - - case SFILE_VERIFY_FILE: - - // Verify parameters - if(szFileName == NULL || *szFileName == 0) - return ERROR_INVALID_PARAMETER; - - // Get the offset of a file - pFileEntry = GetFileEntryLocale(ha, szFileName, lcFileLocale); - if(pFileEntry == NULL) - return ERROR_FILE_NOT_FOUND; - - return VerifyRawMpqData(ha, pFileEntry->ByteOffset, pFileEntry->dwCmpSize); - } - - return ERROR_INVALID_PARAMETER; -} - - -// Verifies the archive against the signature -DWORD WINAPI SFileVerifyArchive(HANDLE hMpq) -{ - MPQ_SIGNATURE_INFO si; - TMPQArchive * ha = (TMPQArchive *)hMpq; - - // Verify input parameters - if(!IsValidMpqHandle(hMpq)) - return ERROR_VERIFY_FAILED; - - // If the archive was modified, we need to flush it - if(ha->dwFlags & MPQ_FLAG_CHANGED) - SFileFlushArchive(hMpq); - - // Get the MPQ signature and signature type - memset(&si, 0, sizeof(MPQ_SIGNATURE_INFO)); - if(!QueryMpqSignatureInfo(ha, &si)) - return ERROR_VERIFY_FAILED; - - // If there is no signature - if(si.SignatureTypes == 0) - return ERROR_NO_SIGNATURE; - - // We haven't seen a MPQ with both signatures - assert(si.SignatureTypes == SIGNATURE_TYPE_WEAK || si.SignatureTypes == SIGNATURE_TYPE_STRONG); - - // Verify the strong signature, if present - if(si.SignatureTypes & SIGNATURE_TYPE_STRONG) - return VerifyStrongSignature(ha, &si); - - // Verify the weak signature, if present - if(si.SignatureTypes & SIGNATURE_TYPE_WEAK) - return VerifyWeakSignature(ha, &si); - - return ERROR_NO_SIGNATURE; -} - -// Verifies the archive against the signature -bool WINAPI SFileSignArchive(HANDLE hMpq, DWORD dwSignatureType) -{ - TMPQArchive * ha; - - // Verify the archive handle - ha = IsValidMpqHandle(hMpq); - if(ha == NULL) - { - SetLastError(ERROR_INVALID_PARAMETER); - return false; - } - - // We only support weak signature, and only for MPQs version 1.0 - if(dwSignatureType != SIGNATURE_TYPE_WEAK) - { - SetLastError(ERROR_INVALID_PARAMETER); - return false; - } - - // The archive must not be malformed and must not be read-only - if(ha->dwFlags & (MPQ_FLAG_READ_ONLY | MPQ_FLAG_MALFORMED)) - { - SetLastError(ERROR_ACCESS_DENIED); - return false; - } - - // If the signature is not there yet - if(ha->dwFileFlags3 == 0) - { - // Turn the signature on. The signature will - // be applied when the archive is closed - ha->dwFlags |= MPQ_FLAG_SIGNATURE_NEW | MPQ_FLAG_CHANGED; - ha->dwFileFlags3 = MPQ_FILE_EXISTS; - ha->dwReservedFiles++; - } - - return true; -} - +/*****************************************************************************/ +/* SFileVerify.cpp Copyright (c) Ladislav Zezula 2010 */ +/*---------------------------------------------------------------------------*/ +/* MPQ files and MPQ archives verification. */ +/* */ +/* The MPQ signature verification has been written by Jean-Francois Roy */ +/* and Justin Olbrantz (Quantam). */ +/* The MPQ public keys have been created by MPQKit, using OpenSSL library. */ +/* */ +/*---------------------------------------------------------------------------*/ +/* Date Ver Who Comment */ +/* -------- ---- --- ------- */ +/* 04.05.10 1.00 Lad The first version of SFileVerify.cpp */ +/*****************************************************************************/ + +#define __STORMLIB_SELF__ +#include "StormLib.h" +#include "StormCommon.h" + +//----------------------------------------------------------------------------- +// Local defines + +#define MPQ_DIGEST_UNIT_SIZE 0x10000 + +//----------------------------------------------------------------------------- +// Known Blizzard public keys +// Created by Jean-Francois Roy using OpenSSL + +static const char * szBlizzardWeakPrivateKey = + "-----BEGIN PRIVATE KEY-----" + "MIIBOQIBAAJBAJJidwS/uILMBSO5DLGsBFknIXWWjQJe2kfdfEk3G/j66w4KkhZ1" + "V61Rt4zLaMVCYpDun7FLwRjkMDSepO1q2DcCAwEAAQJANtiztVDMJh2hE1hjPDKy" + "UmEJ9U/aN3gomuKOjbQbQ/bWWcM/WfhSVHmPqtqh/bQI2UXFr0rnXngeteZHLr/b" + "8QIhAMuWriSKGMACw18/rVVfUrThs915odKBH1Alr3vMVVzZAiEAuBHPSQkgwcb6" + "L4MWaiKuOzq08mSyNqPeN8oSy18q848CIHeMn+3s+eOmu7su1UYQl6yH7OrdBd1q" + "3UxfFNEJiAbhAiAqxdCyOxHGlbM7aS3DOg3cq5ayoN2cvtV7h1R4t8OmVwIgF+5z" + "/6vkzBUsZhd8Nwyis+MeQYH0rpFpMKdTlqmPF2Q=" + "-----END PRIVATE KEY-----"; + +static const char * szBlizzardWeakPublicKey = + "-----BEGIN PUBLIC KEY-----" + "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJJidwS/uILMBSO5DLGsBFknIXWWjQJe" + "2kfdfEk3G/j66w4KkhZ1V61Rt4zLaMVCYpDun7FLwRjkMDSepO1q2DcCAwEAAQ==" + "-----END PUBLIC KEY-----"; + +static const char * szBlizzardStrongPublicKey = + "-----BEGIN PUBLIC KEY-----" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsQZ+ziT2h8h+J/iMQpgd" + "tH1HaJzOBE3agjU4yMPcrixaPOZoA4t8bwfey7qczfWywocYo3pleytFF+IuD4HD" + "Fl9OXN1SFyupSgMx1EGZlgbFAomnbq9MQJyMqQtMhRAjFgg4TndS7YNb+JMSAEKp" + "kXNqY28n/EVBHD5TsMuVCL579gIenbr61dI92DDEdy790IzIG0VKWLh/KOTcTJfm" + "Ds/7HQTkGouVW+WUsfekuqNQo7ND9DBnhLjLjptxeFE2AZqYcA1ao3S9LN3GL1tW" + "lVXFIX9c7fWqaVTQlZ2oNsI/ARVApOK3grNgqvwH6YoVYVXjNJEo5sQJsPsdV/hk" + "dwIDAQAB" + "-----END PUBLIC KEY-----"; + +static const char * szWarcraft3MapPublicKey = + "-----BEGIN PUBLIC KEY-----" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1BwklUUQ3UvjizOBRoF5" + "yyOVc7KD+oGOQH5i6eUk1yfs0luCC70kNucNrfqhmviywVtahRse1JtXCPrx2bd3" + "iN8Dx91fbkxjYIOGTsjYoHKTp0BbaFkJih776fcHgnFSb+7mJcDuJVvJOXxEH6w0" + "1vo6VtujCqj1arqbyoal+xtAaczF3us5cOEp45sR1zAWTn1+7omN7VWV4QqJPaDS" + "gBSESc0l1grO0i1VUSumayk7yBKIkb+LBvcG6WnYZHCi7VdLmaxER5m8oZfER66b" + "heHoiSQIZf9PAY6Guw2DT5BTc54j/AaLQAKf2qcRSgQLVo5kQaddF3rCpsXoB/74" + "6QIDAQAB" + "-----END PUBLIC KEY-----"; + +static const char * szWowPatchPublicKey = + "-----BEGIN PUBLIC KEY-----" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwOsMV0LagAWPEtEQM6b9" + "6FHFkUyGbbyda2/Dfc9dyl21E9QvX+Yw7qKRMAKPzA2TlQQLZKvXpnKXF/YIK5xa" + "5uwg9CEHCEAYolLG4xn0FUOE0E/0PuuytI0p0ICe6rk00PifZzTr8na2wI/l/GnQ" + "bvnIVF1ck6cslATpQJ5JJVMXzoFlUABS19WESw4MXuJAS3AbMhxNWdEhVv7eO51c" + "yGjRLy9QjogZODZTY0fSEksgBqQxNCoYVJYI/sF5K2flDsGqrIp0OdJ6teJlzg1Y" + "UjYnb6bKjlidXoHEXI2TgA/mD6O3XFIt08I9s3crOCTgICq7cgX35qrZiIVWZdRv" + "TwIDAQAB" + "-----END PUBLIC KEY-----"; + +static const char * szWowSurveyPublicKey = + "-----BEGIN PUBLIC KEY-----" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnIt1DR6nRyyKsy2qahHe" + "MKLtacatn/KxieHcwH87wLBxKy+jZ0gycTmJ7SaTdBAEMDs/V5IPIXEtoqYnid2c" + "63TmfGDU92oc3Ph1PWUZ2PWxBhT06HYxRdbrgHw9/I29pNPi/607x+lzPORITOgU" + "BR6MR8au8HsQP4bn4vkJNgnSgojh48/XQOB/cAln7As1neP61NmVimoLR4Bwi3zt" + "zfgrZaUpyeNCUrOYJmH09YIjbBySTtXOUidoPHjFrMsCWpr6xs8xbETbs7MJFL6a" + "vcUfTT67qfIZ9RsuKfnXJTIrV0kwDSjjuNXiPTmWAehSsiHIsrUXX5RNcwsSjClr" + "nQIDAQAB" + "-----END PUBLIC KEY-----"; + +static const char * szStarcraft2MapPublicKey = + "-----BEGIN PUBLIC KEY-----" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmk4GT8zb+ICC25a17KZB" + "q/ygKGJ2VSO6IT5PGHJlm1KfnHBA4B6SH3xMlJ4c6eG2k7QevZv+FOhjsAHubyWq" + "2VKqWbrIFKv2ILc2RfMn8J9EDVRxvcxh6slRrVL69D0w1tfVGjMiKq2Fym5yGoRT" + "E7CRgDqbAbXP9LBsCNWHiJLwfxMGzHbk8pIl9oia5pvM7ofZamSHchxlpy6xa4GJ" + "7xKN01YCNvklTL1D7uol3wkwcHc7vrF8QwuJizuA5bSg4poEGtH62BZOYi+UL/z0" + "31YK+k9CbQyM0X0pJoJoYz1TK+Y5J7vBnXCZtfcTYQ/ZzN6UcxTa57dJaiOlCh9z" + "nQIDAQAB" + "-----END PUBLIC KEY-----"; + +//----------------------------------------------------------------------------- +// Local functions + +static void memrev(unsigned char *buf, size_t count) +{ + unsigned char *r; + + for (r = buf + count - 1; buf < r; buf++, r--) + { + *buf ^= *r; + *r ^= *buf; + *buf ^= *r; + } +} + +static bool decode_base64_key(const char * szKeyBase64, rsa_key * key) +{ + unsigned char decoded_key[0x200]; + const char * szBase64Begin; + const char * szBase64End; + unsigned long decoded_length = sizeof(decoded_key); + unsigned long length; + + // Find out the begin of the BASE64 data + szBase64Begin = szKeyBase64 + strlen("-----BEGIN PUBLIC KEY-----"); + szBase64End = szBase64Begin + strlen(szBase64Begin) - strlen("-----END PUBLIC KEY-----"); + if(szBase64End[0] != '-') + return false; + + // decode the base64 string + length = (unsigned long)(szBase64End - szBase64Begin); + if(base64_decode((unsigned char *)szBase64Begin, length, decoded_key, &decoded_length) != CRYPT_OK) + return false; + + // Create RSA key + if(rsa_import(decoded_key, decoded_length, key) != CRYPT_OK) + return false; + + return true; +} + +static void GetPlainAnsiFileName( + const TCHAR * szFileName, + char * szPlainName) +{ + const TCHAR * szPlainNameT = GetPlainFileName(szFileName); + + // Convert the plain name to ANSI + while(*szPlainNameT != 0) + *szPlainName++ = (char)*szPlainNameT++; + *szPlainName = 0; +} + +// Calculate begin and end of the MPQ archive +static void CalculateArchiveRange( + TMPQArchive * ha, + PMPQ_SIGNATURE_INFO pSI) +{ + ULONGLONG TempPos = 0; + char szMapHeader[0x200]; + + // Get the MPQ begin + pSI->BeginMpqData = ha->MpqPos; + + // Warcraft III maps are signed from the map header to the end + if(FileStream_Read(ha->pStream, &TempPos, szMapHeader, sizeof(szMapHeader))) + { + // Is it a map header ? + if(szMapHeader[0] == 'H' && szMapHeader[1] == 'M' && szMapHeader[2] == '3' && szMapHeader[3] == 'W') + { + // We will have to hash since the map header + pSI->BeginMpqData = 0; + } + } + + // Get the MPQ data end. This is stored in the MPQ header + pSI->EndMpqData = ha->MpqPos + ha->pHeader->ArchiveSize64; + + // Get the size of the entire file + FileStream_GetSize(ha->pStream, &pSI->EndOfFile); +} + +static bool CalculateMpqHashMd5( + TMPQArchive * ha, + PMPQ_SIGNATURE_INFO pSI, + LPBYTE pMd5Digest) +{ + hash_state md5_state; + ULONGLONG BeginBuffer; + ULONGLONG EndBuffer; + LPBYTE pbDigestBuffer = NULL; + + // Allocate buffer for creating the MPQ digest. + pbDigestBuffer = STORM_ALLOC(BYTE, MPQ_DIGEST_UNIT_SIZE); + if(pbDigestBuffer == NULL) + return false; + + // Initialize the MD5 hash state + md5_init(&md5_state); + + // Set the byte offset of begin of the data + BeginBuffer = pSI->BeginMpqData; + + // Create the digest + for(;;) + { + ULONGLONG BytesRemaining; + LPBYTE pbSigBegin = NULL; + LPBYTE pbSigEnd = NULL; + DWORD dwToRead = MPQ_DIGEST_UNIT_SIZE; + + // Check the number of bytes remaining + BytesRemaining = pSI->EndMpqData - BeginBuffer; + if(BytesRemaining < MPQ_DIGEST_UNIT_SIZE) + dwToRead = (DWORD)BytesRemaining; + if(dwToRead == 0) + break; + + // Read the next chunk + if(!FileStream_Read(ha->pStream, &BeginBuffer, pbDigestBuffer, dwToRead)) + { + STORM_FREE(pbDigestBuffer); + return false; + } + + // Move the current byte offset + EndBuffer = BeginBuffer + dwToRead; + + // Check if the signature is within the loaded digest + if(BeginBuffer <= pSI->BeginExclude && pSI->BeginExclude < EndBuffer) + pbSigBegin = pbDigestBuffer + (size_t)(pSI->BeginExclude - BeginBuffer); + if(BeginBuffer <= pSI->EndExclude && pSI->EndExclude < EndBuffer) + pbSigEnd = pbDigestBuffer + (size_t)(pSI->EndExclude - BeginBuffer); + + // Zero the part that belongs to the signature + if(pbSigBegin != NULL || pbSigEnd != NULL) + { + if(pbSigBegin == NULL) + pbSigBegin = pbDigestBuffer; + if(pbSigEnd == NULL) + pbSigEnd = pbDigestBuffer + dwToRead; + + memset(pbSigBegin, 0, (pbSigEnd - pbSigBegin)); + } + + // Pass the buffer to the hashing function + md5_process(&md5_state, pbDigestBuffer, dwToRead); + + // Move pointers + BeginBuffer += dwToRead; + } + + // Finalize the MD5 hash + md5_done(&md5_state, pMd5Digest); + STORM_FREE(pbDigestBuffer); + return true; +} + +static void AddTailToSha1( + hash_state * psha1_state, + const char * szTail) +{ + unsigned char * pbTail = (unsigned char *)szTail; + unsigned char szUpperCase[0x200]; + unsigned long nLength = 0; + + // Convert the tail to uppercase + // Note that we don't need to terminate the string with zero + while(*pbTail != 0) + { + szUpperCase[nLength++] = AsciiToUpperTable[*pbTail++]; + } + + // Append the tail to the SHA1 + sha1_process(psha1_state, szUpperCase, nLength); +} + +static bool CalculateMpqHashSha1( + TMPQArchive * ha, + PMPQ_SIGNATURE_INFO pSI, + unsigned char * sha1_tail0, + unsigned char * sha1_tail1, + unsigned char * sha1_tail2) +{ + ULONGLONG BeginBuffer; + hash_state sha1_state_temp; + hash_state sha1_state; + LPBYTE pbDigestBuffer = NULL; + char szPlainName[MAX_PATH]; + + // Allocate buffer for creating the MPQ digest. + pbDigestBuffer = STORM_ALLOC(BYTE, MPQ_DIGEST_UNIT_SIZE); + if(pbDigestBuffer == NULL) + return false; + + // Initialize SHA1 state structure + sha1_init(&sha1_state); + + // Calculate begin of data to be hashed + BeginBuffer = pSI->BeginMpqData; + + // Create the digest + for(;;) + { + ULONGLONG BytesRemaining; + DWORD dwToRead = MPQ_DIGEST_UNIT_SIZE; + + // Check the number of bytes remaining + BytesRemaining = pSI->EndMpqData - BeginBuffer; + if(BytesRemaining < MPQ_DIGEST_UNIT_SIZE) + dwToRead = (DWORD)BytesRemaining; + if(dwToRead == 0) + break; + + // Read the next chunk + if(!FileStream_Read(ha->pStream, &BeginBuffer, pbDigestBuffer, dwToRead)) + { + STORM_FREE(pbDigestBuffer); + return false; + } + + // Pass the buffer to the hashing function + sha1_process(&sha1_state, pbDigestBuffer, dwToRead); + + // Move pointers + BeginBuffer += dwToRead; + } + + // Add all three known tails and generate three hashes + memcpy(&sha1_state_temp, &sha1_state, sizeof(hash_state)); + sha1_done(&sha1_state_temp, sha1_tail0); + + memcpy(&sha1_state_temp, &sha1_state, sizeof(hash_state)); + GetPlainAnsiFileName(FileStream_GetFileName(ha->pStream), szPlainName); + AddTailToSha1(&sha1_state_temp, szPlainName); + sha1_done(&sha1_state_temp, sha1_tail1); + + memcpy(&sha1_state_temp, &sha1_state, sizeof(hash_state)); + AddTailToSha1(&sha1_state_temp, "ARCHIVE"); + sha1_done(&sha1_state_temp, sha1_tail2); + + // Finalize the MD5 hash + STORM_FREE(pbDigestBuffer); + return true; +} + +static int VerifyRawMpqData( + TMPQArchive * ha, + ULONGLONG ByteOffset, + DWORD dwDataSize) +{ + ULONGLONG DataOffset = ha->MpqPos + ByteOffset; + LPBYTE pbDataChunk; + LPBYTE pbMD5Array1; // Calculated MD5 array + LPBYTE pbMD5Array2; // MD5 array loaded from the MPQ + DWORD dwBytesInChunk; + DWORD dwChunkCount; + DWORD dwChunkSize = ha->pHeader->dwRawChunkSize; + DWORD dwMD5Size; + int nError = ERROR_SUCCESS; + + // Don't verify zero-sized blocks + if(dwDataSize == 0) + return ERROR_SUCCESS; + + // Get the number of data chunks to calculate MD5 + assert(dwChunkSize != 0); + dwChunkCount = ((dwDataSize - 1) / dwChunkSize) + 1; + dwMD5Size = dwChunkCount * MD5_DIGEST_SIZE; + + // Allocate space for data chunk and for the MD5 array + pbDataChunk = STORM_ALLOC(BYTE, dwChunkSize); + if(pbDataChunk == NULL) + return ERROR_NOT_ENOUGH_MEMORY; + + // Allocate space for MD5 array + pbMD5Array1 = STORM_ALLOC(BYTE, dwMD5Size); + pbMD5Array2 = STORM_ALLOC(BYTE, dwMD5Size); + if(pbMD5Array1 == NULL || pbMD5Array2 == NULL) + nError = ERROR_NOT_ENOUGH_MEMORY; + + // Calculate MD5 of each data chunk + if(nError == ERROR_SUCCESS) + { + LPBYTE pbMD5 = pbMD5Array1; + + for(DWORD i = 0; i < dwChunkCount; i++) + { + // Get the number of bytes in the chunk + dwBytesInChunk = STORMLIB_MIN(dwChunkSize, dwDataSize); + + // Read the data chunk + if(!FileStream_Read(ha->pStream, &DataOffset, pbDataChunk, dwBytesInChunk)) + { + nError = ERROR_FILE_CORRUPT; + break; + } + + // Calculate MD5 + CalculateDataBlockHash(pbDataChunk, dwBytesInChunk, pbMD5); + + // Move pointers and offsets + DataOffset += dwBytesInChunk; + dwDataSize -= dwBytesInChunk; + pbMD5 += MD5_DIGEST_SIZE; + } + } + + // Read the MD5 array + if(nError == ERROR_SUCCESS) + { + // Read the array of MD5 + if(!FileStream_Read(ha->pStream, &DataOffset, pbMD5Array2, dwMD5Size)) + nError = GetLastError(); + } + + // Compare the array of MD5 + if(nError == ERROR_SUCCESS) + { + // Compare the MD5 + if(memcmp(pbMD5Array1, pbMD5Array2, dwMD5Size)) + nError = ERROR_FILE_CORRUPT; + } + + // Free memory and return result + if(pbMD5Array2 != NULL) + STORM_FREE(pbMD5Array2); + if(pbMD5Array1 != NULL) + STORM_FREE(pbMD5Array1); + if(pbDataChunk != NULL) + STORM_FREE(pbDataChunk); + return nError; +} + +static DWORD VerifyWeakSignature( + TMPQArchive * ha, + PMPQ_SIGNATURE_INFO pSI) +{ + BYTE RevSignature[MPQ_WEAK_SIGNATURE_SIZE]; + BYTE Md5Digest[MD5_DIGEST_SIZE]; + rsa_key key; + int hash_idx = find_hash("md5"); + int result = 0; + + // The signature might be zeroed out. In that case, we ignore it + if(!IsValidSignature(pSI->Signature)) + return ERROR_WEAK_SIGNATURE_OK; + + // Calculate hash of the entire archive, skipping the (signature) file + if(!CalculateMpqHashMd5(ha, pSI, Md5Digest)) + return ERROR_VERIFY_FAILED; + + // Import the Blizzard key in OpenSSL format + if(!decode_base64_key(szBlizzardWeakPublicKey, &key)) + return ERROR_VERIFY_FAILED; + + // Verify the signature + memcpy(RevSignature, &pSI->Signature[8], MPQ_WEAK_SIGNATURE_SIZE); + memrev(RevSignature, MPQ_WEAK_SIGNATURE_SIZE); + rsa_verify_hash_ex(RevSignature, MPQ_WEAK_SIGNATURE_SIZE, Md5Digest, sizeof(Md5Digest), LTC_LTC_PKCS_1_V1_5, hash_idx, 0, &result, &key); + rsa_free(&key); + + // Return the result + return result ? ERROR_WEAK_SIGNATURE_OK : ERROR_WEAK_SIGNATURE_ERROR; +} + +static DWORD VerifyStrongSignatureWithKey( + unsigned char * reversed_signature, + unsigned char * padded_digest, + const char * szPublicKey) +{ + rsa_key key; + int result = 0; + + // Import the Blizzard key in OpenSSL format + if(!decode_base64_key(szPublicKey, &key)) + { + assert(false); + return ERROR_VERIFY_FAILED; + } + + // Verify the signature + if(rsa_verify_simple(reversed_signature, MPQ_STRONG_SIGNATURE_SIZE, padded_digest, MPQ_STRONG_SIGNATURE_SIZE, &result, &key) != CRYPT_OK) + return ERROR_VERIFY_FAILED; + + // Free the key and return result + rsa_free(&key); + return result ? ERROR_STRONG_SIGNATURE_OK : ERROR_STRONG_SIGNATURE_ERROR; +} + +static DWORD VerifyStrongSignature( + TMPQArchive * ha, + PMPQ_SIGNATURE_INFO pSI) +{ + unsigned char reversed_signature[MPQ_STRONG_SIGNATURE_SIZE]; + unsigned char Sha1Digest_tail0[SHA1_DIGEST_SIZE]; + unsigned char Sha1Digest_tail1[SHA1_DIGEST_SIZE]; + unsigned char Sha1Digest_tail2[SHA1_DIGEST_SIZE]; + unsigned char padded_digest[MPQ_STRONG_SIGNATURE_SIZE]; + DWORD dwResult; + size_t digest_offset; + + // Calculate SHA1 hash of the archive + if(!CalculateMpqHashSha1(ha, pSI, Sha1Digest_tail0, Sha1Digest_tail1, Sha1Digest_tail2)) + return ERROR_VERIFY_FAILED; + + // Prepare the signature for decryption + memcpy(reversed_signature, &pSI->Signature[4], MPQ_STRONG_SIGNATURE_SIZE); + memrev(reversed_signature, MPQ_STRONG_SIGNATURE_SIZE); + + // Prepare the padded digest for comparison + digest_offset = sizeof(padded_digest) - SHA1_DIGEST_SIZE; + memset(padded_digest, 0xbb, digest_offset); + padded_digest[0] = 0x0b; + + // Try Blizzard Strong public key with no SHA1 tail + memcpy(padded_digest + digest_offset, Sha1Digest_tail0, SHA1_DIGEST_SIZE); + memrev(padded_digest + digest_offset, SHA1_DIGEST_SIZE); + dwResult = VerifyStrongSignatureWithKey(reversed_signature, padded_digest, szBlizzardStrongPublicKey); + if(dwResult == ERROR_STRONG_SIGNATURE_OK) + return dwResult; + + // Try War 3 map public key with plain file name as SHA1 tail + memcpy(padded_digest + digest_offset, Sha1Digest_tail1, SHA1_DIGEST_SIZE); + memrev(padded_digest + digest_offset, SHA1_DIGEST_SIZE); + dwResult = VerifyStrongSignatureWithKey(reversed_signature, padded_digest, szWarcraft3MapPublicKey); + if(dwResult == ERROR_STRONG_SIGNATURE_OK) + return dwResult; + + // Try WoW-TBC public key with "ARCHIVE" as SHA1 tail + memcpy(padded_digest + digest_offset, Sha1Digest_tail2, SHA1_DIGEST_SIZE); + memrev(padded_digest + digest_offset, SHA1_DIGEST_SIZE); + dwResult = VerifyStrongSignatureWithKey(reversed_signature, padded_digest, szWowPatchPublicKey); + if(dwResult == ERROR_STRONG_SIGNATURE_OK) + return dwResult; + + // Try Survey public key with no SHA1 tail + memcpy(padded_digest + digest_offset, Sha1Digest_tail0, SHA1_DIGEST_SIZE); + memrev(padded_digest + digest_offset, SHA1_DIGEST_SIZE); + dwResult = VerifyStrongSignatureWithKey(reversed_signature, padded_digest, szWowSurveyPublicKey); + if(dwResult == ERROR_STRONG_SIGNATURE_OK) + return dwResult; + + // Try Starcraft II public key with no SHA1 tail + memcpy(padded_digest + digest_offset, Sha1Digest_tail0, SHA1_DIGEST_SIZE); + memrev(padded_digest + digest_offset, SHA1_DIGEST_SIZE); + dwResult = VerifyStrongSignatureWithKey(reversed_signature, padded_digest, szStarcraft2MapPublicKey); + if(dwResult == ERROR_STRONG_SIGNATURE_OK) + return dwResult; + + return ERROR_STRONG_SIGNATURE_ERROR; +} + +static DWORD VerifyFile( + HANDLE hMpq, + const char * szFileName, + LPDWORD pdwCrc32, + char * pMD5, + DWORD dwFlags) +{ + hash_state md5_state; + unsigned char * pFileMd5; + unsigned char md5[MD5_DIGEST_SIZE]; + TFileEntry * pFileEntry; + TMPQFile * hf; + BYTE Buffer[0x1000]; + HANDLE hFile = NULL; + DWORD dwVerifyResult = 0; + DWORD dwTotalBytes = 0; + DWORD dwCrc32 = 0; + + // + // Note: When the MPQ is patched, it will + // automatically check the patched version of the file + // + + // Make sure the md5 is initialized + memset(md5, 0, sizeof(md5)); + + // If we have to verify raw data MD5, do it before file open + if(dwFlags & SFILE_VERIFY_RAW_MD5) + { + TMPQArchive * ha = (TMPQArchive *)hMpq; + + // Parse the base MPQ and all patches + while(ha != NULL) + { + // Does the archive have support for raw MD5? + if(ha->pHeader->dwRawChunkSize != 0) + { + // The file has raw MD5 if the archive supports it + dwVerifyResult |= VERIFY_FILE_HAS_RAW_MD5; + + // Find file entry for the file + pFileEntry = GetFileEntryLocale(ha, szFileName, lcFileLocale); + if(pFileEntry != NULL) + { + // If the file's raw MD5 doesn't match, don't bother with more checks + if(VerifyRawMpqData(ha, pFileEntry->ByteOffset, pFileEntry->dwCmpSize) != ERROR_SUCCESS) + return dwVerifyResult | VERIFY_FILE_RAW_MD5_ERROR; + } + } + + // Move to the next patch + ha = ha->haPatch; + } + } + + // Attempt to open the file + if(SFileOpenFileEx(hMpq, szFileName, SFILE_OPEN_FROM_MPQ, &hFile)) + { + // Get the file size + hf = (TMPQFile *)hFile; + pFileEntry = hf->pFileEntry; + dwTotalBytes = SFileGetFileSize(hFile, NULL); + + // Initialize the CRC32 and MD5 contexts + md5_init(&md5_state); + dwCrc32 = crc32(0, Z_NULL, 0); + + // Also turn on sector checksum verification + if(dwFlags & SFILE_VERIFY_SECTOR_CRC) + hf->bCheckSectorCRCs = true; + + // Go through entire file and update both CRC32 and MD5 + for(;;) + { + DWORD dwBytesRead = 0; + + // Read data from file + SFileReadFile(hFile, Buffer, sizeof(Buffer), &dwBytesRead, NULL); + if(dwBytesRead == 0) + { + if(GetLastError() == ERROR_CHECKSUM_ERROR) + dwVerifyResult |= VERIFY_FILE_SECTOR_CRC_ERROR; + break; + } + + // Update CRC32 value + if(dwFlags & SFILE_VERIFY_FILE_CRC) + dwCrc32 = crc32(dwCrc32, Buffer, dwBytesRead); + + // Update MD5 value + if(dwFlags & SFILE_VERIFY_FILE_MD5) + md5_process(&md5_state, Buffer, dwBytesRead); + + // Decrement the total size + dwTotalBytes -= dwBytesRead; + } + + // If the file has sector checksums, indicate it in the flags + if(dwFlags & SFILE_VERIFY_SECTOR_CRC) + { + if((hf->pFileEntry->dwFlags & MPQ_FILE_SECTOR_CRC) && hf->SectorChksums != NULL && hf->SectorChksums[0] != 0) + dwVerifyResult |= VERIFY_FILE_HAS_SECTOR_CRC; + } + + // Check if the entire file has been read + // No point in checking CRC32 and MD5 if not + // Skip checksum checks if the file has patches + if(dwTotalBytes == 0) + { + // Check CRC32 and MD5 only if there is no patches + if(hf->hfPatch == NULL) + { + // Check if the CRC32 matches. + if(dwFlags & SFILE_VERIFY_FILE_CRC) + { + // Only check the CRC32 if it is valid + if(pFileEntry->dwCrc32 != 0) + { + dwVerifyResult |= VERIFY_FILE_HAS_CHECKSUM; + if(dwCrc32 != pFileEntry->dwCrc32) + dwVerifyResult |= VERIFY_FILE_CHECKSUM_ERROR; + } + } + + // Check if MD5 matches + if(dwFlags & SFILE_VERIFY_FILE_MD5) + { + // Patch files have their MD5 saved in the patch info + pFileMd5 = (hf->pPatchInfo != NULL) ? hf->pPatchInfo->md5 : pFileEntry->md5; + md5_done(&md5_state, md5); + + // Only check the MD5 if it is valid + if(IsValidMD5(pFileMd5)) + { + dwVerifyResult |= VERIFY_FILE_HAS_MD5; + if(memcmp(md5, pFileMd5, MD5_DIGEST_SIZE)) + dwVerifyResult |= VERIFY_FILE_MD5_ERROR; + } + } + } + else + { + // Patched files are MD5-checked automatically + dwVerifyResult |= VERIFY_FILE_HAS_MD5; + } + } + else + { + dwVerifyResult |= VERIFY_READ_ERROR; + } + + SFileCloseFile(hFile); + } + else + { + // Remember that the file couldn't be open + dwVerifyResult |= VERIFY_OPEN_ERROR; + } + + // If the caller required CRC32 and/or MD5, give it to him + if(pdwCrc32 != NULL) + *pdwCrc32 = dwCrc32; + if(pMD5 != NULL) + memcpy(pMD5, md5, MD5_DIGEST_SIZE); + + return dwVerifyResult; +} + +// Used in SFileGetFileInfo +bool QueryMpqSignatureInfo( + TMPQArchive * ha, + PMPQ_SIGNATURE_INFO pSI) +{ + TFileEntry * pFileEntry; + ULONGLONG ExtraBytes; + DWORD dwFileSize; + + // Make sure it's all zeroed + memset(pSI, 0, sizeof(MPQ_SIGNATURE_INFO)); + + // Calculate the range of the MPQ + CalculateArchiveRange(ha, pSI); + + // If there is "(signature)" file in the MPQ, it has a weak signature + pFileEntry = GetFileEntryLocale(ha, SIGNATURE_NAME, LANG_NEUTRAL); + if(pFileEntry != NULL) + { + // Calculate the begin and end of the signature file itself + pSI->BeginExclude = ha->MpqPos + pFileEntry->ByteOffset; + pSI->EndExclude = pSI->BeginExclude + pFileEntry->dwCmpSize; + dwFileSize = (DWORD)(pSI->EndExclude - pSI->BeginExclude); + + // Does the signature have proper size? + if(dwFileSize == MPQ_SIGNATURE_FILE_SIZE) + { + // Read the weak signature + if(!FileStream_Read(ha->pStream, &pSI->BeginExclude, pSI->Signature, dwFileSize)) + return false; + + pSI->SignatureTypes |= SIGNATURE_TYPE_WEAK; + pSI->cbSignatureSize = dwFileSize; + return true; + } + } + + // If there is extra bytes beyond the end of the archive, + // it's the strong signature + ExtraBytes = pSI->EndOfFile - pSI->EndMpqData; + if(ExtraBytes >= (MPQ_STRONG_SIGNATURE_SIZE + 4)) + { + // Read the strong signature + if(!FileStream_Read(ha->pStream, &pSI->EndMpqData, pSI->Signature, (MPQ_STRONG_SIGNATURE_SIZE + 4))) + return false; + + // Check the signature header "NGIS" + if(pSI->Signature[0] != 'N' || pSI->Signature[1] != 'G' || pSI->Signature[2] != 'I' || pSI->Signature[3] != 'S') + return false; + + pSI->SignatureTypes |= SIGNATURE_TYPE_STRONG; + return true; + } + + // Succeeded, but no known signature found + return true; +} + +//----------------------------------------------------------------------------- +// Support for weak signature + +int SSignFileCreate(TMPQArchive * ha) +{ + TMPQFile * hf = NULL; + BYTE EmptySignature[MPQ_SIGNATURE_FILE_SIZE]; + int nError = ERROR_SUCCESS; + + // Only save the signature if we should do so + if(ha->dwFileFlags3 != 0) + { + // The (signature) file must be non-encrypted and non-compressed + assert(ha->dwFlags & MPQ_FLAG_SIGNATURE_NEW); + assert(ha->dwFileFlags3 == MPQ_FILE_EXISTS); + assert(ha->dwReservedFiles > 0); + + // Create the (signature) file file in the MPQ + // Note that the file must not be compressed or encrypted + nError = SFileAddFile_Init(ha, SIGNATURE_NAME, + 0, + sizeof(EmptySignature), + LANG_NEUTRAL, + ha->dwFileFlags3 | MPQ_FILE_REPLACEEXISTING, + &hf); + + // Write the empty signature file to the archive + if(nError == ERROR_SUCCESS) + { + // Write the empty zeroed file to the MPQ + memset(EmptySignature, 0, sizeof(EmptySignature)); + nError = SFileAddFile_Write(hf, EmptySignature, (DWORD)sizeof(EmptySignature), 0); + SFileAddFile_Finish(hf); + + // Clear the invalid mark + ha->dwFlags &= ~(MPQ_FLAG_SIGNATURE_NEW | MPQ_FLAG_SIGNATURE_NONE); + ha->dwReservedFiles--; + } + } + + return nError; +} + +int SSignFileFinish(TMPQArchive * ha) +{ + MPQ_SIGNATURE_INFO si; + unsigned long signature_len = MPQ_WEAK_SIGNATURE_SIZE; + BYTE WeakSignature[MPQ_SIGNATURE_FILE_SIZE]; + BYTE Md5Digest[MD5_DIGEST_SIZE]; + rsa_key key; + int hash_idx = find_hash("md5"); + + // Sanity checks + assert((ha->dwFlags & MPQ_FLAG_CHANGED) == 0); + assert(ha->dwFileFlags3 == MPQ_FILE_EXISTS); + + // Query the weak signature info + memset(&si, 0, sizeof(MPQ_SIGNATURE_INFO)); + if(!QueryMpqSignatureInfo(ha, &si)) + return ERROR_FILE_CORRUPT; + + // There must be exactly one signature + if(si.SignatureTypes != SIGNATURE_TYPE_WEAK) + return ERROR_FILE_CORRUPT; + + // Calculate MD5 of the entire archive + if(!CalculateMpqHashMd5(ha, &si, Md5Digest)) + return ERROR_VERIFY_FAILED; + + // Decode the private key + if(!decode_base64_key(szBlizzardWeakPrivateKey, &key)) + return ERROR_VERIFY_FAILED; + + // Sign the hash + memset(WeakSignature, 0, sizeof(WeakSignature)); + rsa_sign_hash_ex(Md5Digest, sizeof(Md5Digest), WeakSignature + 8, &signature_len, LTC_LTC_PKCS_1_V1_5, 0, 0, hash_idx, 0, &key); + memrev(WeakSignature + 8, MPQ_WEAK_SIGNATURE_SIZE); + rsa_free(&key); + + // Write the signature to the MPQ. Don't use SFile* functions, but write the hash directly + if(!FileStream_Write(ha->pStream, &si.BeginExclude, WeakSignature, MPQ_SIGNATURE_FILE_SIZE)) + return GetLastError(); + + return ERROR_SUCCESS; +} + +//----------------------------------------------------------------------------- +// Public (exported) functions + +bool WINAPI SFileGetFileChecksums(HANDLE hMpq, const char * szFileName, LPDWORD pdwCrc32, char * pMD5) +{ + DWORD dwVerifyResult; + DWORD dwVerifyFlags = 0; + + if(pdwCrc32 != NULL) + dwVerifyFlags |= SFILE_VERIFY_FILE_CRC; + if(pMD5 != NULL) + dwVerifyFlags |= SFILE_VERIFY_FILE_MD5; + + dwVerifyResult = VerifyFile(hMpq, + szFileName, + pdwCrc32, + pMD5, + dwVerifyFlags); + + // If verification failed, return zero + if(dwVerifyResult & VERIFY_FILE_ERROR_MASK) + { + SetLastError(ERROR_FILE_CORRUPT); + return false; + } + + return true; +} + + +DWORD WINAPI SFileVerifyFile(HANDLE hMpq, const char * szFileName, DWORD dwFlags) +{ + return VerifyFile(hMpq, + szFileName, + NULL, + NULL, + dwFlags); +} + +// Verifies raw data of the archive Only works for MPQs version 4 or newer +int WINAPI SFileVerifyRawData(HANDLE hMpq, DWORD dwWhatToVerify, const char * szFileName) +{ + TMPQArchive * ha = (TMPQArchive *)hMpq; + TFileEntry * pFileEntry; + TMPQHeader * pHeader; + + // Verify input parameters + if(!IsValidMpqHandle(hMpq)) + return ERROR_INVALID_PARAMETER; + pHeader = ha->pHeader; + + // If the archive doesn't have raw data MD5, report it as OK + if(pHeader->dwRawChunkSize == 0) + return ERROR_SUCCESS; + + // If we have to verify MPQ header, do it + switch(dwWhatToVerify) + { + case SFILE_VERIFY_MPQ_HEADER: + + // Only if the header is of version 4 or newer + if(pHeader->dwHeaderSize >= (MPQ_HEADER_SIZE_V4 - MD5_DIGEST_SIZE)) + return VerifyRawMpqData(ha, 0, MPQ_HEADER_SIZE_V4 - MD5_DIGEST_SIZE); + return ERROR_SUCCESS; + + case SFILE_VERIFY_HET_TABLE: + + // Only if we have HET table + if(pHeader->HetTablePos64 && pHeader->HetTableSize64) + return VerifyRawMpqData(ha, pHeader->HetTablePos64, (DWORD)pHeader->HetTableSize64); + return ERROR_SUCCESS; + + case SFILE_VERIFY_BET_TABLE: + + // Only if we have BET table + if(pHeader->BetTablePos64 && pHeader->BetTableSize64) + return VerifyRawMpqData(ha, pHeader->BetTablePos64, (DWORD)pHeader->BetTableSize64); + return ERROR_SUCCESS; + + case SFILE_VERIFY_HASH_TABLE: + + // Hash table is not protected by MD5 + return ERROR_SUCCESS; + + case SFILE_VERIFY_BLOCK_TABLE: + + // Block table is not protected by MD5 + return ERROR_SUCCESS; + + case SFILE_VERIFY_HIBLOCK_TABLE: + + // It is unknown if the hi-block table is protected my MD5 or not. + return ERROR_SUCCESS; + + case SFILE_VERIFY_FILE: + + // Verify parameters + if(szFileName == NULL || *szFileName == 0) + return ERROR_INVALID_PARAMETER; + + // Get the offset of a file + pFileEntry = GetFileEntryLocale(ha, szFileName, lcFileLocale); + if(pFileEntry == NULL) + return ERROR_FILE_NOT_FOUND; + + return VerifyRawMpqData(ha, pFileEntry->ByteOffset, pFileEntry->dwCmpSize); + } + + return ERROR_INVALID_PARAMETER; +} + + +// Verifies the archive against the signature +DWORD WINAPI SFileVerifyArchive(HANDLE hMpq) +{ + MPQ_SIGNATURE_INFO si; + TMPQArchive * ha = (TMPQArchive *)hMpq; + + // Verify input parameters + if(!IsValidMpqHandle(hMpq)) + return ERROR_VERIFY_FAILED; + + // If the archive was modified, we need to flush it + if(ha->dwFlags & MPQ_FLAG_CHANGED) + SFileFlushArchive(hMpq); + + // Get the MPQ signature and signature type + memset(&si, 0, sizeof(MPQ_SIGNATURE_INFO)); + if(!QueryMpqSignatureInfo(ha, &si)) + return ERROR_VERIFY_FAILED; + + // If there is no signature + if(si.SignatureTypes == 0) + return ERROR_NO_SIGNATURE; + + // We haven't seen a MPQ with both signatures + assert(si.SignatureTypes == SIGNATURE_TYPE_WEAK || si.SignatureTypes == SIGNATURE_TYPE_STRONG); + + // Verify the strong signature, if present + if(si.SignatureTypes & SIGNATURE_TYPE_STRONG) + return VerifyStrongSignature(ha, &si); + + // Verify the weak signature, if present + if(si.SignatureTypes & SIGNATURE_TYPE_WEAK) + return VerifyWeakSignature(ha, &si); + + return ERROR_NO_SIGNATURE; +} + +// Verifies the archive against the signature +bool WINAPI SFileSignArchive(HANDLE hMpq, DWORD dwSignatureType) +{ + TMPQArchive * ha; + + // Verify the archive handle + ha = IsValidMpqHandle(hMpq); + if(ha == NULL) + { + SetLastError(ERROR_INVALID_PARAMETER); + return false; + } + + // We only support weak signature, and only for MPQs version 1.0 + if(dwSignatureType != SIGNATURE_TYPE_WEAK) + { + SetLastError(ERROR_INVALID_PARAMETER); + return false; + } + + // The archive must not be malformed and must not be read-only + if(ha->dwFlags & (MPQ_FLAG_READ_ONLY | MPQ_FLAG_MALFORMED)) + { + SetLastError(ERROR_ACCESS_DENIED); + return false; + } + + // If the signature is not there yet + if(ha->dwFileFlags3 == 0) + { + // Turn the signature on. The signature will + // be applied when the archive is closed + ha->dwFlags |= MPQ_FLAG_SIGNATURE_NEW | MPQ_FLAG_CHANGED; + ha->dwFileFlags3 = MPQ_FILE_EXISTS; + ha->dwReservedFiles++; + } + + return true; +} + diff --git a/src/StormCommon.h b/src/StormCommon.h index 0b1ccb2..3c2148e 100644 --- a/src/StormCommon.h +++ b/src/StormCommon.h @@ -1,372 +1,372 @@ -/*****************************************************************************/ -/* SCommon.h Copyright (c) Ladislav Zezula 2003 */ -/*---------------------------------------------------------------------------*/ -/* Common functions for encryption/decryption from Storm.dll. Included by */ -/* SFile*** functions, do not include and do not use this file directly */ -/*---------------------------------------------------------------------------*/ -/* Date Ver Who Comment */ -/* -------- ---- --- ------- */ -/* 24.03.03 1.00 Lad The first version of SFileCommon.h */ -/* 12.06.04 1.00 Lad Renamed to SCommon.h */ -/* 06.09.10 1.00 Lad Renamed to StormCommon.h */ -/*****************************************************************************/ - -#ifndef __STORMCOMMON_H__ -#define __STORMCOMMON_H__ - -//----------------------------------------------------------------------------- -// Compression support - -// Include functions from Pkware Data Compression Library -#include "pklib/pklib.h" - -// Include functions from Huffmann compression -#include "huffman/huff.h" - -// Include functions from IMA ADPCM compression -#include "adpcm/adpcm.h" - -// Include functions from SPARSE compression -#include "sparse/sparse.h" - -// Include functions from LZMA compression -#include "lzma/C/LzmaEnc.h" -#include "lzma/C/LzmaDec.h" - -// Include functions from zlib -#ifndef __SYS_ZLIB - #include "zlib/zlib.h" -#else - #include -#endif - -// Include functions from bzlib -#ifndef __SYS_BZLIB - #include "bzip2/bzlib.h" -#else - #include -#endif - -//----------------------------------------------------------------------------- -// Cryptography support - -// Headers from LibTomCrypt -#include "libtomcrypt/src/headers/tomcrypt.h" - -// For HashStringJenkins -#include "jenkins/lookup.h" - -//----------------------------------------------------------------------------- -// StormLib private defines - -#define ID_MPQ_FILE 0x46494c45 // Used internally for checking TMPQFile ('FILE') - -// Prevent problems with CRT "min" and "max" functions, -// as they are not defined on all platforms -#define STORMLIB_MIN(a, b) ((a < b) ? a : b) -#define STORMLIB_MAX(a, b) ((a > b) ? a : b) -#define STORMLIB_UNUSED(p) ((void)(p)) - -// Macro for building 64-bit file offset from two 32-bit -#define MAKE_OFFSET64(hi, lo) (((ULONGLONG)hi << 32) | (ULONGLONG)lo) - -//----------------------------------------------------------------------------- -// MPQ signature information - -// Size of each signature type -#define MPQ_WEAK_SIGNATURE_SIZE 64 -#define MPQ_STRONG_SIGNATURE_SIZE 256 -#define MPQ_STRONG_SIGNATURE_ID 0x5349474E // ID of the strong signature ("NGIS") -#define MPQ_SIGNATURE_FILE_SIZE (MPQ_WEAK_SIGNATURE_SIZE + 8) - -// MPQ signature info -typedef struct _MPQ_SIGNATURE_INFO -{ - ULONGLONG BeginMpqData; // File offset where the hashing starts - ULONGLONG BeginExclude; // Begin of the excluded area (used for (signature) file) - ULONGLONG EndExclude; // End of the excluded area (used for (signature) file) - ULONGLONG EndMpqData; // File offset where the hashing ends - ULONGLONG EndOfFile; // Size of the entire file - BYTE Signature[MPQ_STRONG_SIGNATURE_SIZE + 0x10]; - DWORD cbSignatureSize; // Length of the signature - DWORD SignatureTypes; // See SIGNATURE_TYPE_XXX - -} MPQ_SIGNATURE_INFO, *PMPQ_SIGNATURE_INFO; - -//----------------------------------------------------------------------------- -// Memory management -// -// We use our own macros for allocating/freeing memory. If you want -// to redefine them, please keep the following rules: -// -// - The memory allocation must return NULL if not enough memory -// (i.e not to throw exception) -// - The allocating function does not need to fill the allocated buffer with zeros -// - Memory freeing function doesn't have to test the pointer to NULL -// - -//#if defined(_MSC_VER) && defined(_DEBUG) -// -//#define STORM_ALLOC(type, nitems) (type *)HeapAlloc(GetProcessHeap(), 0, ((nitems) * sizeof(type))) -//#define STORM_REALLOC(type, ptr, nitems) (type *)HeapReAlloc(GetProcessHeap(), 0, ptr, ((nitems) * sizeof(type))) -//#define STORM_FREE(ptr) HeapFree(GetProcessHeap(), 0, ptr) -// -//#else - -#define STORM_ALLOC(type, nitems) (type *)malloc((nitems) * sizeof(type)) -#define STORM_REALLOC(type, ptr, nitems) (type *)realloc(ptr, ((nitems) * sizeof(type))) -#define STORM_FREE(ptr) free(ptr) - -//#endif - -//----------------------------------------------------------------------------- -// StormLib internal global variables - -extern LCID lcFileLocale; // Preferred file locale - -//----------------------------------------------------------------------------- -// Conversion to uppercase/lowercase (and "/" to "\") - -extern unsigned char AsciiToLowerTable[256]; -extern unsigned char AsciiToUpperTable[256]; - -//----------------------------------------------------------------------------- -// Safe string functions - -void StringCopyA(char * dest, const char * src, size_t nMaxChars); -void StringCatA(char * dest, const char * src, size_t nMaxChars); - -void StringCopyT(TCHAR * dest, const TCHAR * src, size_t nMaxChars); -void StringCatT(TCHAR * dest, const TCHAR * src, size_t nMaxChars); - -//----------------------------------------------------------------------------- -// Encryption and decryption functions - -#define MPQ_HASH_TABLE_INDEX 0x000 -#define MPQ_HASH_NAME_A 0x100 -#define MPQ_HASH_NAME_B 0x200 -#define MPQ_HASH_FILE_KEY 0x300 -#define MPQ_HASH_KEY2_MIX 0x400 - -DWORD HashString(const char * szFileName, DWORD dwHashType); -DWORD HashStringSlash(const char * szFileName, DWORD dwHashType); -DWORD HashStringLower(const char * szFileName, DWORD dwHashType); - -void InitializeMpqCryptography(); - -DWORD GetHashTableSizeForFileCount(DWORD dwFileCount); - -bool IsPseudoFileName(const char * szFileName, LPDWORD pdwFileIndex); -ULONGLONG HashStringJenkins(const char * szFileName); - -DWORD GetDefaultSpecialFileFlags(DWORD dwFileSize, USHORT wFormatVersion); - -void EncryptMpqBlock(void * pvDataBlock, DWORD dwLength, DWORD dwKey); -void DecryptMpqBlock(void * pvDataBlock, DWORD dwLength, DWORD dwKey); - -DWORD DetectFileKeyBySectorSize(LPDWORD EncryptedData, DWORD dwSectorSize, DWORD dwSectorOffsLen); -DWORD DetectFileKeyByContent(void * pvEncryptedData, DWORD dwSectorSize, DWORD dwFileSize); -DWORD DecryptFileKey(const char * szFileName, ULONGLONG MpqPos, DWORD dwFileSize, DWORD dwFlags); - -bool IsValidMD5(LPBYTE pbMd5); -bool IsValidSignature(LPBYTE pbSignature); -bool VerifyDataBlockHash(void * pvDataBlock, DWORD cbDataBlock, LPBYTE expected_md5); -void CalculateDataBlockHash(void * pvDataBlock, DWORD cbDataBlock, LPBYTE md5_hash); - -//----------------------------------------------------------------------------- -// Handle validation functions - -TMPQArchive * IsValidMpqHandle(HANDLE hMpq); -TMPQFile * IsValidFileHandle(HANDLE hFile); - -//----------------------------------------------------------------------------- -// Support for MPQ file tables - -ULONGLONG FileOffsetFromMpqOffset(TMPQArchive * ha, ULONGLONG MpqOffset); -ULONGLONG CalculateRawSectorOffset(TMPQFile * hf, DWORD dwSectorOffset); - -int ConvertMpqHeaderToFormat4(TMPQArchive * ha, ULONGLONG MpqOffset, ULONGLONG FileSize, DWORD dwFlags); - -bool IsValidHashEntry(TMPQArchive * ha, TMPQHash * pHash); - -TMPQHash * FindFreeHashEntry(TMPQArchive * ha, DWORD dwStartIndex, DWORD dwName1, DWORD dwName2, LCID lcLocale); -TMPQHash * GetFirstHashEntry(TMPQArchive * ha, const char * szFileName); -TMPQHash * GetNextHashEntry(TMPQArchive * ha, TMPQHash * pFirstHash, TMPQHash * pPrevHash); -TMPQHash * AllocateHashEntry(TMPQArchive * ha, TFileEntry * pFileEntry, LCID lcLocale); - -TMPQExtHeader * LoadExtTable(TMPQArchive * ha, ULONGLONG ByteOffset, size_t Size, DWORD dwSignature, DWORD dwKey); -TMPQHetTable * LoadHetTable(TMPQArchive * ha); -TMPQBetTable * LoadBetTable(TMPQArchive * ha); - -TMPQBlock * LoadBlockTable(TMPQArchive * ha, bool bDontFixEntries = false); -TMPQBlock * TranslateBlockTable(TMPQArchive * ha, ULONGLONG * pcbTableSize, bool * pbNeedHiBlockTable); - -ULONGLONG FindFreeMpqSpace(TMPQArchive * ha); - -// Functions that load the HET and BET tables -int CreateHashTable(TMPQArchive * ha, DWORD dwHashTableSize); -int LoadAnyHashTable(TMPQArchive * ha); -int BuildFileTable(TMPQArchive * ha); -int DefragmentFileTable(TMPQArchive * ha); - -int CreateFileTable(TMPQArchive * ha, DWORD dwFileTableSize); -int RebuildHetTable(TMPQArchive * ha); -int RebuildFileTable(TMPQArchive * ha, DWORD dwNewHashTableSize); -int SaveMPQTables(TMPQArchive * ha); - -TMPQHetTable * CreateHetTable(DWORD dwEntryCount, DWORD dwTotalCount, DWORD dwHashBitSize, LPBYTE pbSrcData); -void FreeHetTable(TMPQHetTable * pHetTable); - -TMPQBetTable * CreateBetTable(DWORD dwMaxFileCount); -void FreeBetTable(TMPQBetTable * pBetTable); - -// Functions for finding files in the file table -TFileEntry * GetFileEntryLocale2(TMPQArchive * ha, const char * szFileName, LCID lcLocale, LPDWORD PtrHashIndex); -TFileEntry * GetFileEntryLocale(TMPQArchive * ha, const char * szFileName, LCID lcLocale); -TFileEntry * GetFileEntryExact(TMPQArchive * ha, const char * szFileName, LCID lcLocale, LPDWORD PtrHashIndex); - -// Allocates file name in the file entry -void AllocateFileName(TMPQArchive * ha, TFileEntry * pFileEntry, const char * szFileName); - -// Allocates new file entry in the MPQ tables. Reuses existing, if possible -TFileEntry * AllocateFileEntry(TMPQArchive * ha, const char * szFileName, LCID lcLocale, LPDWORD PtrHashIndex); -int RenameFileEntry(TMPQArchive * ha, TMPQFile * hf, const char * szNewFileName); -int DeleteFileEntry(TMPQArchive * ha, TMPQFile * hf); - -// Invalidates entries for (listfile) and (attributes) -void InvalidateInternalFiles(TMPQArchive * ha); - -// Retrieves information about the strong signature -bool QueryMpqSignatureInfo(TMPQArchive * ha, PMPQ_SIGNATURE_INFO pSignatureInfo); - -//----------------------------------------------------------------------------- -// Support for alternate file formats (SBaseSubTypes.cpp) - -int ConvertSqpHeaderToFormat4(TMPQArchive * ha, ULONGLONG FileSize, DWORD dwFlags); -TMPQHash * LoadSqpHashTable(TMPQArchive * ha); -TMPQBlock * LoadSqpBlockTable(TMPQArchive * ha); - -int ConvertMpkHeaderToFormat4(TMPQArchive * ha, ULONGLONG FileSize, DWORD dwFlags); -void DecryptMpkTable(void * pvMpkTable, size_t cbSize); -TMPQHash * LoadMpkHashTable(TMPQArchive * ha); -TMPQBlock * LoadMpkBlockTable(TMPQArchive * ha); -int SCompDecompressMpk(void * pvOutBuffer, int * pcbOutBuffer, void * pvInBuffer, int cbInBuffer); - -//----------------------------------------------------------------------------- -// Common functions - MPQ File - -TMPQFile * CreateFileHandle(TMPQArchive * ha, TFileEntry * pFileEntry); -TMPQFile * CreateWritableHandle(TMPQArchive * ha, DWORD dwFileSize); -void * LoadMpqTable(TMPQArchive * ha, ULONGLONG ByteOffset, DWORD dwCompressedSize, DWORD dwRealSize, DWORD dwKey, bool * pbTableIsCut); -int AllocateSectorBuffer(TMPQFile * hf); -int AllocatePatchInfo(TMPQFile * hf, bool bLoadFromFile); -int AllocateSectorOffsets(TMPQFile * hf, bool bLoadFromFile); -int AllocateSectorChecksums(TMPQFile * hf, bool bLoadFromFile); -int WritePatchInfo(TMPQFile * hf); -int WriteSectorOffsets(TMPQFile * hf); -int WriteSectorChecksums(TMPQFile * hf); -int WriteMemDataMD5(TFileStream * pStream, ULONGLONG RawDataOffs, void * pvRawData, DWORD dwRawDataSize, DWORD dwChunkSize, LPDWORD pcbTotalSize); -int WriteMpqDataMD5(TFileStream * pStream, ULONGLONG RawDataOffs, DWORD dwRawDataSize, DWORD dwChunkSize); -void FreeFileHandle(TMPQFile *& hf); -void FreeArchiveHandle(TMPQArchive *& ha); - -//----------------------------------------------------------------------------- -// Patch functions - -// Structure used for the patching process -typedef struct _TMPQPatcher -{ - BYTE this_md5[MD5_DIGEST_SIZE]; // MD5 of the current file state - LPBYTE pbFileData1; // Primary working buffer - LPBYTE pbFileData2; // Secondary working buffer - DWORD cbMaxFileData; // Maximum allowed size of the patch data - DWORD cbFileData; // Current size of the result data - DWORD nCounter; // Counter of the patch process - -} TMPQPatcher; - -bool IsIncrementalPatchFile(const void * pvData, DWORD cbData, LPDWORD pdwPatchedFileSize); -int Patch_InitPatcher(TMPQPatcher * pPatcher, TMPQFile * hf); -int Patch_Process(TMPQPatcher * pPatcher, TMPQFile * hf); -void Patch_Finalize(TMPQPatcher * pPatcher); - -//----------------------------------------------------------------------------- -// Utility functions - -bool CheckWildCard(const char * szString, const char * szWildCard); -bool IsInternalMpqFileName(const char * szFileName); - -const TCHAR * GetPlainFileName(const TCHAR * szFileName); -const char * GetPlainFileName(const char * szFileName); - -void CopyFileName(TCHAR * szTarget, const char * szSource, size_t cchLength); -void CopyFileName(char * szTarget, const TCHAR * szSource, size_t cchLength); - -//----------------------------------------------------------------------------- -// Internal support for MPQ modifications - -int SFileAddFile_Init( - TMPQArchive * ha, - const char * szArchivedName, - ULONGLONG ft, - DWORD dwFileSize, - LCID lcLocale, - DWORD dwFlags, - TMPQFile ** phf - ); - -int SFileAddFile_Init( - TMPQArchive * ha, - TMPQFile * hfSrc, - TMPQFile ** phf - ); - -int SFileAddFile_Write( - TMPQFile * hf, - const void * pvData, - DWORD dwSize, - DWORD dwCompression - ); - -int SFileAddFile_Finish( - TMPQFile * hf - ); - -//----------------------------------------------------------------------------- -// Attributes support - -int SAttrLoadAttributes(TMPQArchive * ha); -int SAttrFileSaveToMpq(TMPQArchive * ha); - -//----------------------------------------------------------------------------- -// Listfile functions - -int SListFileSaveToMpq(TMPQArchive * ha); - -//----------------------------------------------------------------------------- -// Weak signature support - -int SSignFileCreate(TMPQArchive * ha); -int SSignFileFinish(TMPQArchive * ha); - -//----------------------------------------------------------------------------- -// Dump data support - -#ifdef __STORMLIB_DUMP_DATA__ - -void DumpMpqHeader(TMPQHeader * pHeader); -void DumpHashTable(TMPQHash * pHashTable, DWORD dwHashTableSize); -void DumpHetAndBetTable(TMPQHetTable * pHetTable, TMPQBetTable * pBetTable); -void DumpFileTable(TFileEntry * pFileTable, DWORD dwFileTableSize); - -#else - -#define DumpMpqHeader(h) /* */ -#define DumpHashTable(t, s) /* */ -#define DumpHetAndBetTable(t, s) /* */ -#define DumpFileTable(t, s) /* */ - -#endif - -#endif // __STORMCOMMON_H__ - +/*****************************************************************************/ +/* SCommon.h Copyright (c) Ladislav Zezula 2003 */ +/*---------------------------------------------------------------------------*/ +/* Common functions for encryption/decryption from Storm.dll. Included by */ +/* SFile*** functions, do not include and do not use this file directly */ +/*---------------------------------------------------------------------------*/ +/* Date Ver Who Comment */ +/* -------- ---- --- ------- */ +/* 24.03.03 1.00 Lad The first version of SFileCommon.h */ +/* 12.06.04 1.00 Lad Renamed to SCommon.h */ +/* 06.09.10 1.00 Lad Renamed to StormCommon.h */ +/*****************************************************************************/ + +#ifndef __STORMCOMMON_H__ +#define __STORMCOMMON_H__ + +//----------------------------------------------------------------------------- +// Compression support + +// Include functions from Pkware Data Compression Library +#include "pklib/pklib.h" + +// Include functions from Huffmann compression +#include "huffman/huff.h" + +// Include functions from IMA ADPCM compression +#include "adpcm/adpcm.h" + +// Include functions from SPARSE compression +#include "sparse/sparse.h" + +// Include functions from LZMA compression +#include "lzma/C/LzmaEnc.h" +#include "lzma/C/LzmaDec.h" + +// Include functions from zlib +#ifndef __SYS_ZLIB + #include "zlib/zlib.h" +#else + #include +#endif + +// Include functions from bzlib +#ifndef __SYS_BZLIB + #include "bzip2/bzlib.h" +#else + #include +#endif + +//----------------------------------------------------------------------------- +// Cryptography support + +// Headers from LibTomCrypt +#include "libtomcrypt/src/headers/tomcrypt.h" + +// For HashStringJenkins +#include "jenkins/lookup.h" + +//----------------------------------------------------------------------------- +// StormLib private defines + +#define ID_MPQ_FILE 0x46494c45 // Used internally for checking TMPQFile ('FILE') + +// Prevent problems with CRT "min" and "max" functions, +// as they are not defined on all platforms +#define STORMLIB_MIN(a, b) ((a < b) ? a : b) +#define STORMLIB_MAX(a, b) ((a > b) ? a : b) +#define STORMLIB_UNUSED(p) ((void)(p)) + +// Macro for building 64-bit file offset from two 32-bit +#define MAKE_OFFSET64(hi, lo) (((ULONGLONG)hi << 32) | (ULONGLONG)lo) + +//----------------------------------------------------------------------------- +// MPQ signature information + +// Size of each signature type +#define MPQ_WEAK_SIGNATURE_SIZE 64 +#define MPQ_STRONG_SIGNATURE_SIZE 256 +#define MPQ_STRONG_SIGNATURE_ID 0x5349474E // ID of the strong signature ("NGIS") +#define MPQ_SIGNATURE_FILE_SIZE (MPQ_WEAK_SIGNATURE_SIZE + 8) + +// MPQ signature info +typedef struct _MPQ_SIGNATURE_INFO +{ + ULONGLONG BeginMpqData; // File offset where the hashing starts + ULONGLONG BeginExclude; // Begin of the excluded area (used for (signature) file) + ULONGLONG EndExclude; // End of the excluded area (used for (signature) file) + ULONGLONG EndMpqData; // File offset where the hashing ends + ULONGLONG EndOfFile; // Size of the entire file + BYTE Signature[MPQ_STRONG_SIGNATURE_SIZE + 0x10]; + DWORD cbSignatureSize; // Length of the signature + DWORD SignatureTypes; // See SIGNATURE_TYPE_XXX + +} MPQ_SIGNATURE_INFO, *PMPQ_SIGNATURE_INFO; + +//----------------------------------------------------------------------------- +// Memory management +// +// We use our own macros for allocating/freeing memory. If you want +// to redefine them, please keep the following rules: +// +// - The memory allocation must return NULL if not enough memory +// (i.e not to throw exception) +// - The allocating function does not need to fill the allocated buffer with zeros +// - Memory freeing function doesn't have to test the pointer to NULL +// + +//#if defined(_MSC_VER) && defined(_DEBUG) +// +//#define STORM_ALLOC(type, nitems) (type *)HeapAlloc(GetProcessHeap(), 0, ((nitems) * sizeof(type))) +//#define STORM_REALLOC(type, ptr, nitems) (type *)HeapReAlloc(GetProcessHeap(), 0, ptr, ((nitems) * sizeof(type))) +//#define STORM_FREE(ptr) HeapFree(GetProcessHeap(), 0, ptr) +// +//#else + +#define STORM_ALLOC(type, nitems) (type *)malloc((nitems) * sizeof(type)) +#define STORM_REALLOC(type, ptr, nitems) (type *)realloc(ptr, ((nitems) * sizeof(type))) +#define STORM_FREE(ptr) free(ptr) + +//#endif + +//----------------------------------------------------------------------------- +// StormLib internal global variables + +extern LCID lcFileLocale; // Preferred file locale + +//----------------------------------------------------------------------------- +// Conversion to uppercase/lowercase (and "/" to "\") + +extern unsigned char AsciiToLowerTable[256]; +extern unsigned char AsciiToUpperTable[256]; + +//----------------------------------------------------------------------------- +// Safe string functions + +void StringCopyA(char * dest, const char * src, size_t nMaxChars); +void StringCatA(char * dest, const char * src, size_t nMaxChars); + +void StringCopyT(TCHAR * dest, const TCHAR * src, size_t nMaxChars); +void StringCatT(TCHAR * dest, const TCHAR * src, size_t nMaxChars); + +//----------------------------------------------------------------------------- +// Encryption and decryption functions + +#define MPQ_HASH_TABLE_INDEX 0x000 +#define MPQ_HASH_NAME_A 0x100 +#define MPQ_HASH_NAME_B 0x200 +#define MPQ_HASH_FILE_KEY 0x300 +#define MPQ_HASH_KEY2_MIX 0x400 + +DWORD HashString(const char * szFileName, DWORD dwHashType); +DWORD HashStringSlash(const char * szFileName, DWORD dwHashType); +DWORD HashStringLower(const char * szFileName, DWORD dwHashType); + +void InitializeMpqCryptography(); + +DWORD GetHashTableSizeForFileCount(DWORD dwFileCount); + +bool IsPseudoFileName(const char * szFileName, LPDWORD pdwFileIndex); +ULONGLONG HashStringJenkins(const char * szFileName); + +DWORD GetDefaultSpecialFileFlags(DWORD dwFileSize, USHORT wFormatVersion); + +void EncryptMpqBlock(void * pvDataBlock, DWORD dwLength, DWORD dwKey); +void DecryptMpqBlock(void * pvDataBlock, DWORD dwLength, DWORD dwKey); + +DWORD DetectFileKeyBySectorSize(LPDWORD EncryptedData, DWORD dwSectorSize, DWORD dwSectorOffsLen); +DWORD DetectFileKeyByContent(void * pvEncryptedData, DWORD dwSectorSize, DWORD dwFileSize); +DWORD DecryptFileKey(const char * szFileName, ULONGLONG MpqPos, DWORD dwFileSize, DWORD dwFlags); + +bool IsValidMD5(LPBYTE pbMd5); +bool IsValidSignature(LPBYTE pbSignature); +bool VerifyDataBlockHash(void * pvDataBlock, DWORD cbDataBlock, LPBYTE expected_md5); +void CalculateDataBlockHash(void * pvDataBlock, DWORD cbDataBlock, LPBYTE md5_hash); + +//----------------------------------------------------------------------------- +// Handle validation functions + +TMPQArchive * IsValidMpqHandle(HANDLE hMpq); +TMPQFile * IsValidFileHandle(HANDLE hFile); + +//----------------------------------------------------------------------------- +// Support for MPQ file tables + +ULONGLONG FileOffsetFromMpqOffset(TMPQArchive * ha, ULONGLONG MpqOffset); +ULONGLONG CalculateRawSectorOffset(TMPQFile * hf, DWORD dwSectorOffset); + +int ConvertMpqHeaderToFormat4(TMPQArchive * ha, ULONGLONG MpqOffset, ULONGLONG FileSize, DWORD dwFlags); + +bool IsValidHashEntry(TMPQArchive * ha, TMPQHash * pHash); + +TMPQHash * FindFreeHashEntry(TMPQArchive * ha, DWORD dwStartIndex, DWORD dwName1, DWORD dwName2, LCID lcLocale); +TMPQHash * GetFirstHashEntry(TMPQArchive * ha, const char * szFileName); +TMPQHash * GetNextHashEntry(TMPQArchive * ha, TMPQHash * pFirstHash, TMPQHash * pPrevHash); +TMPQHash * AllocateHashEntry(TMPQArchive * ha, TFileEntry * pFileEntry, LCID lcLocale); + +TMPQExtHeader * LoadExtTable(TMPQArchive * ha, ULONGLONG ByteOffset, size_t Size, DWORD dwSignature, DWORD dwKey); +TMPQHetTable * LoadHetTable(TMPQArchive * ha); +TMPQBetTable * LoadBetTable(TMPQArchive * ha); + +TMPQBlock * LoadBlockTable(TMPQArchive * ha, bool bDontFixEntries = false); +TMPQBlock * TranslateBlockTable(TMPQArchive * ha, ULONGLONG * pcbTableSize, bool * pbNeedHiBlockTable); + +ULONGLONG FindFreeMpqSpace(TMPQArchive * ha); + +// Functions that load the HET and BET tables +int CreateHashTable(TMPQArchive * ha, DWORD dwHashTableSize); +int LoadAnyHashTable(TMPQArchive * ha); +int BuildFileTable(TMPQArchive * ha); +int DefragmentFileTable(TMPQArchive * ha); + +int CreateFileTable(TMPQArchive * ha, DWORD dwFileTableSize); +int RebuildHetTable(TMPQArchive * ha); +int RebuildFileTable(TMPQArchive * ha, DWORD dwNewHashTableSize); +int SaveMPQTables(TMPQArchive * ha); + +TMPQHetTable * CreateHetTable(DWORD dwEntryCount, DWORD dwTotalCount, DWORD dwHashBitSize, LPBYTE pbSrcData); +void FreeHetTable(TMPQHetTable * pHetTable); + +TMPQBetTable * CreateBetTable(DWORD dwMaxFileCount); +void FreeBetTable(TMPQBetTable * pBetTable); + +// Functions for finding files in the file table +TFileEntry * GetFileEntryLocale2(TMPQArchive * ha, const char * szFileName, LCID lcLocale, LPDWORD PtrHashIndex); +TFileEntry * GetFileEntryLocale(TMPQArchive * ha, const char * szFileName, LCID lcLocale); +TFileEntry * GetFileEntryExact(TMPQArchive * ha, const char * szFileName, LCID lcLocale, LPDWORD PtrHashIndex); + +// Allocates file name in the file entry +void AllocateFileName(TMPQArchive * ha, TFileEntry * pFileEntry, const char * szFileName); + +// Allocates new file entry in the MPQ tables. Reuses existing, if possible +TFileEntry * AllocateFileEntry(TMPQArchive * ha, const char * szFileName, LCID lcLocale, LPDWORD PtrHashIndex); +int RenameFileEntry(TMPQArchive * ha, TMPQFile * hf, const char * szNewFileName); +int DeleteFileEntry(TMPQArchive * ha, TMPQFile * hf); + +// Invalidates entries for (listfile) and (attributes) +void InvalidateInternalFiles(TMPQArchive * ha); + +// Retrieves information about the strong signature +bool QueryMpqSignatureInfo(TMPQArchive * ha, PMPQ_SIGNATURE_INFO pSignatureInfo); + +//----------------------------------------------------------------------------- +// Support for alternate file formats (SBaseSubTypes.cpp) + +int ConvertSqpHeaderToFormat4(TMPQArchive * ha, ULONGLONG FileSize, DWORD dwFlags); +TMPQHash * LoadSqpHashTable(TMPQArchive * ha); +TMPQBlock * LoadSqpBlockTable(TMPQArchive * ha); + +int ConvertMpkHeaderToFormat4(TMPQArchive * ha, ULONGLONG FileSize, DWORD dwFlags); +void DecryptMpkTable(void * pvMpkTable, size_t cbSize); +TMPQHash * LoadMpkHashTable(TMPQArchive * ha); +TMPQBlock * LoadMpkBlockTable(TMPQArchive * ha); +int SCompDecompressMpk(void * pvOutBuffer, int * pcbOutBuffer, void * pvInBuffer, int cbInBuffer); + +//----------------------------------------------------------------------------- +// Common functions - MPQ File + +TMPQFile * CreateFileHandle(TMPQArchive * ha, TFileEntry * pFileEntry); +TMPQFile * CreateWritableHandle(TMPQArchive * ha, DWORD dwFileSize); +void * LoadMpqTable(TMPQArchive * ha, ULONGLONG ByteOffset, DWORD dwCompressedSize, DWORD dwRealSize, DWORD dwKey, bool * pbTableIsCut); +int AllocateSectorBuffer(TMPQFile * hf); +int AllocatePatchInfo(TMPQFile * hf, bool bLoadFromFile); +int AllocateSectorOffsets(TMPQFile * hf, bool bLoadFromFile); +int AllocateSectorChecksums(TMPQFile * hf, bool bLoadFromFile); +int WritePatchInfo(TMPQFile * hf); +int WriteSectorOffsets(TMPQFile * hf); +int WriteSectorChecksums(TMPQFile * hf); +int WriteMemDataMD5(TFileStream * pStream, ULONGLONG RawDataOffs, void * pvRawData, DWORD dwRawDataSize, DWORD dwChunkSize, LPDWORD pcbTotalSize); +int WriteMpqDataMD5(TFileStream * pStream, ULONGLONG RawDataOffs, DWORD dwRawDataSize, DWORD dwChunkSize); +void FreeFileHandle(TMPQFile *& hf); +void FreeArchiveHandle(TMPQArchive *& ha); + +//----------------------------------------------------------------------------- +// Patch functions + +// Structure used for the patching process +typedef struct _TMPQPatcher +{ + BYTE this_md5[MD5_DIGEST_SIZE]; // MD5 of the current file state + LPBYTE pbFileData1; // Primary working buffer + LPBYTE pbFileData2; // Secondary working buffer + DWORD cbMaxFileData; // Maximum allowed size of the patch data + DWORD cbFileData; // Current size of the result data + DWORD nCounter; // Counter of the patch process + +} TMPQPatcher; + +bool IsIncrementalPatchFile(const void * pvData, DWORD cbData, LPDWORD pdwPatchedFileSize); +int Patch_InitPatcher(TMPQPatcher * pPatcher, TMPQFile * hf); +int Patch_Process(TMPQPatcher * pPatcher, TMPQFile * hf); +void Patch_Finalize(TMPQPatcher * pPatcher); + +//----------------------------------------------------------------------------- +// Utility functions + +bool CheckWildCard(const char * szString, const char * szWildCard); +bool IsInternalMpqFileName(const char * szFileName); + +const TCHAR * GetPlainFileName(const TCHAR * szFileName); +const char * GetPlainFileName(const char * szFileName); + +void CopyFileName(TCHAR * szTarget, const char * szSource, size_t cchLength); +void CopyFileName(char * szTarget, const TCHAR * szSource, size_t cchLength); + +//----------------------------------------------------------------------------- +// Internal support for MPQ modifications + +int SFileAddFile_Init( + TMPQArchive * ha, + const char * szArchivedName, + ULONGLONG ft, + DWORD dwFileSize, + LCID lcLocale, + DWORD dwFlags, + TMPQFile ** phf + ); + +int SFileAddFile_Init( + TMPQArchive * ha, + TMPQFile * hfSrc, + TMPQFile ** phf + ); + +int SFileAddFile_Write( + TMPQFile * hf, + const void * pvData, + DWORD dwSize, + DWORD dwCompression + ); + +int SFileAddFile_Finish( + TMPQFile * hf + ); + +//----------------------------------------------------------------------------- +// Attributes support + +int SAttrLoadAttributes(TMPQArchive * ha); +int SAttrFileSaveToMpq(TMPQArchive * ha); + +//----------------------------------------------------------------------------- +// Listfile functions + +int SListFileSaveToMpq(TMPQArchive * ha); + +//----------------------------------------------------------------------------- +// Weak signature support + +int SSignFileCreate(TMPQArchive * ha); +int SSignFileFinish(TMPQArchive * ha); + +//----------------------------------------------------------------------------- +// Dump data support + +#ifdef __STORMLIB_DUMP_DATA__ + +void DumpMpqHeader(TMPQHeader * pHeader); +void DumpHashTable(TMPQHash * pHashTable, DWORD dwHashTableSize); +void DumpHetAndBetTable(TMPQHetTable * pHetTable, TMPQBetTable * pBetTable); +void DumpFileTable(TFileEntry * pFileTable, DWORD dwFileTableSize); + +#else + +#define DumpMpqHeader(h) /* */ +#define DumpHashTable(t, s) /* */ +#define DumpHetAndBetTable(t, s) /* */ +#define DumpFileTable(t, s) /* */ + +#endif + +#endif // __STORMCOMMON_H__ + diff --git a/src/StormPort.h b/src/StormPort.h index 41b4307..154d573 100644 --- a/src/StormPort.h +++ b/src/StormPort.h @@ -1,290 +1,290 @@ -/*****************************************************************************/ -/* StormPort.h Copyright (c) Marko Friedemann 2001 */ -/*---------------------------------------------------------------------------*/ -/* Portability module for the StormLib library. Contains a wrapper symbols */ -/* to make the compilation under Linux work */ -/* */ -/* Author: Marko Friedemann */ -/* Created at: Mon Jan 29 18:26:01 CEST 2001 */ -/* Computer: whiplash.flachland-chemnitz.de */ -/* System: Linux 2.4.0 on i686 */ -/* */ -/* Author: Sam Wilkins */ -/* System: Mac OS X and port to big endian processor */ -/* */ -/*---------------------------------------------------------------------------*/ -/* Date Ver Who Comment */ -/* -------- ---- --- ------- */ -/* 29.01.01 1.00 Mar Created */ -/* 24.03.03 1.01 Lad Some cosmetic changes */ -/* 12.11.03 1.02 Dan Macintosh compatibility */ -/* 24.07.04 1.03 Sam Mac OS X compatibility */ -/* 22.11.06 1.04 Sam Mac OS X compatibility (for StormLib 6.0) */ -/* 31.12.06 1.05 XPinguin Full GNU/Linux compatibility */ -/* 17.10.12 1.05 Lad Moved error codes so they don't overlap with errno.h */ -/*****************************************************************************/ - -#ifndef __STORMPORT_H__ -#define __STORMPORT_H__ - -#ifndef __cplusplus - #define bool char - #define true 1 - #define false 0 -#endif - -//----------------------------------------------------------------------------- -// Defines for Windows - -#if !defined(PLATFORM_DEFINED) && defined(_WIN32) - - // In MSVC 8.0, there are some functions declared as deprecated. - #if _MSC_VER >= 1400 - #define _CRT_SECURE_NO_DEPRECATE - #define _CRT_NON_CONFORMING_SWPRINTFS - #endif - - #include - #include - #include - #include - #include - #include - #define PLATFORM_LITTLE_ENDIAN - - #ifdef _WIN64 - #define PLATFORM_64BIT - #else - #define PLATFORM_32BIT - #endif - - #define PLATFORM_WINDOWS - #define PLATFORM_DEFINED // The platform is known now - -#endif - -//----------------------------------------------------------------------------- -// Defines for Mac - -#if !defined(PLATFORM_DEFINED) && defined(__APPLE__) // Mac BSD API - - // Macintosh - #include - #include - #include - #include - #include - #include - #include - - // Support for PowerPC on Max OS X - #if (__ppc__ == 1) || (__POWERPC__ == 1) || (_ARCH_PPC == 1) - #include - #include - #endif - - #define PKEXPORT - #define __SYS_ZLIB - #define __SYS_BZLIB - - #ifndef __BIG_ENDIAN__ - #define PLATFORM_LITTLE_ENDIAN - #endif - - #define PLATFORM_MAC - #define PLATFORM_DEFINED // The platform is known now - -#endif - -//----------------------------------------------------------------------------- -// Assumption: we are not on Windows nor Macintosh, so this must be linux *grin* - -#if !defined(PLATFORM_DEFINED) - - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - - #define PLATFORM_LITTLE_ENDIAN - #define PLATFORM_LINUX - #define PLATFORM_DEFINED - -#endif - -//----------------------------------------------------------------------------- -// Definition of Windows-specific types for non-Windows platforms - -#ifndef PLATFORM_WINDOWS - #if __LP64__ - #define PLATFORM_64BIT - #else - #define PLATFORM_32BIT - #endif - - // Typedefs for ANSI C - typedef unsigned char BYTE; - typedef unsigned short USHORT; - typedef int LONG; - typedef unsigned int DWORD; - typedef unsigned long DWORD_PTR; - typedef long LONG_PTR; - typedef long INT_PTR; - typedef long long LONGLONG; - typedef unsigned long long ULONGLONG; - typedef void * HANDLE; - typedef void * LPOVERLAPPED; // Unsupported on Linux and Mac - typedef char TCHAR; - typedef unsigned int LCID; - typedef LONG * PLONG; - typedef DWORD * LPDWORD; - typedef BYTE * LPBYTE; - - #ifdef PLATFORM_32BIT - #define _LZMA_UINT32_IS_ULONG - #endif - - // Some Windows-specific defines - #ifndef MAX_PATH - #define MAX_PATH 1024 - #endif - - #define WINAPI - - #define FILE_BEGIN SEEK_SET - #define FILE_CURRENT SEEK_CUR - #define FILE_END SEEK_END - - #define _T(x) x - #define _tcslen strlen - #define _tcscpy strcpy - #define _tcscat strcat - #define _tcschr strchr - #define _tcsrchr strrchr - #define _tcsstr strstr - #define _tprintf printf - #define _stprintf sprintf - #define _tremove remove - - #define _stricmp strcasecmp - #define _strnicmp strncasecmp - #define _tcsicmp strcasecmp - #define _tcsnicmp strncasecmp - -#endif // !PLATFORM_WINDOWS - -// 64-bit calls are supplied by "normal" calls on Mac -#if defined(PLATFORM_MAC) - #define stat64 stat - #define fstat64 fstat - #define lseek64 lseek - #define ftruncate64 ftruncate - #define off64_t off_t - #define O_LARGEFILE 0 -#endif - -// Platform-specific error codes for UNIX-based platforms -#if defined(PLATFORM_MAC) || defined(PLATFORM_LINUX) - #define ERROR_SUCCESS 0 - #define ERROR_FILE_NOT_FOUND ENOENT - #define ERROR_ACCESS_DENIED EPERM - #define ERROR_INVALID_HANDLE EBADF - #define ERROR_NOT_ENOUGH_MEMORY ENOMEM - #define ERROR_NOT_SUPPORTED ENOTSUP - #define ERROR_INVALID_PARAMETER EINVAL - #define ERROR_DISK_FULL ENOSPC - #define ERROR_ALREADY_EXISTS EEXIST - #define ERROR_INSUFFICIENT_BUFFER ENOBUFS - #define ERROR_BAD_FORMAT 1000 // No such error code under Linux - #define ERROR_NO_MORE_FILES 1001 // No such error code under Linux - #define ERROR_HANDLE_EOF 1002 // No such error code under Linux - #define ERROR_CAN_NOT_COMPLETE 1003 // No such error code under Linux - #define ERROR_FILE_CORRUPT 1004 // No such error code under Linux -#endif - -//----------------------------------------------------------------------------- -// Swapping functions - -#ifdef PLATFORM_LITTLE_ENDIAN - #define BSWAP_INT16_UNSIGNED(a) (a) - #define BSWAP_INT16_SIGNED(a) (a) - #define BSWAP_INT32_UNSIGNED(a) (a) - #define BSWAP_INT32_SIGNED(a) (a) - #define BSWAP_INT64_SIGNED(a) (a) - #define BSWAP_INT64_UNSIGNED(a) (a) - #define BSWAP_ARRAY16_UNSIGNED(a,b) {} - #define BSWAP_ARRAY32_UNSIGNED(a,b) {} - #define BSWAP_ARRAY64_UNSIGNED(a,b) {} - #define BSWAP_PART_HEADER(a) {} - #define BSWAP_TMPQHEADER(a,b) {} - #define BSWAP_TMPKHEADER(a) {} -#else - -#ifdef __cplusplus - extern "C" { -#endif - int16_t SwapInt16(uint16_t); - uint16_t SwapUInt16(uint16_t); - int32_t SwapInt32(uint32_t); - uint32_t SwapUInt32(uint32_t); - int64_t SwapInt64(uint64_t); - uint64_t SwapUInt64(uint64_t); - void ConvertUInt16Buffer(void * ptr, size_t length); - void ConvertUInt32Buffer(void * ptr, size_t length); - void ConvertUInt64Buffer(void * ptr, size_t length); - void ConvertTMPQUserData(void *userData); - void ConvertTMPQHeader(void *header, uint16_t wPart); - void ConvertTMPKHeader(void *header); -#ifdef __cplusplus - } -#endif - #define BSWAP_INT16_SIGNED(a) SwapInt16((a)) - #define BSWAP_INT16_UNSIGNED(a) SwapUInt16((a)) - #define BSWAP_INT32_SIGNED(a) SwapInt32((a)) - #define BSWAP_INT32_UNSIGNED(a) SwapUInt32((a)) - #define BSWAP_INT64_SIGNED(a) SwapInt64((a)) - #define BSWAP_INT64_UNSIGNED(a) SwapUInt64((a)) - #define BSWAP_ARRAY16_UNSIGNED(a,b) ConvertUInt16Buffer((a),(b)) - #define BSWAP_ARRAY32_UNSIGNED(a,b) ConvertUInt32Buffer((a),(b)) - #define BSWAP_ARRAY64_UNSIGNED(a,b) ConvertUInt64Buffer((a),(b)) - #define BSWAP_TMPQHEADER(a,b) ConvertTMPQHeader((a),(b)) - #define BSWAP_TMPKHEADER(a) ConvertTMPKHeader((a)) -#endif - -//----------------------------------------------------------------------------- -// Macro for deprecated symbols - -/* -#ifdef _MSC_VER - #if _MSC_FULL_VER >= 140050320 - #define STORMLIB_DEPRECATED(_Text) __declspec(deprecated(_Text)) - #else - #define STORMLIB_DEPRECATED(_Text) __declspec(deprecated) - #endif -#else - #ifdef __GNUC__ - #define STORMLIB_DEPRECATED(_Text) __attribute__((deprecated)) - #else - #define STORMLIB_DEPRECATED(_Text) __attribute__((deprecated(_Text))) - #endif -#endif - -// When a flag is deprecated, use this macro -#ifndef _STORMLIB_NO_DEPRECATE - #define STORMLIB_DEPRECATED_FLAG(type, oldflag, newflag) \ - const STORMLIB_DEPRECATED(#oldflag " is deprecated. Use " #newflag ". To supress this warning, define _STORMLIB_NO_DEPRECATE") static type oldflag = (type)newflag; -#else -#define STORMLIB_DEPRECATED_FLAG(type, oldflag, newflag) static type oldflag = (type)newflag; -#endif -*/ - -#endif // __STORMPORT_H__ +/*****************************************************************************/ +/* StormPort.h Copyright (c) Marko Friedemann 2001 */ +/*---------------------------------------------------------------------------*/ +/* Portability module for the StormLib library. Contains a wrapper symbols */ +/* to make the compilation under Linux work */ +/* */ +/* Author: Marko Friedemann */ +/* Created at: Mon Jan 29 18:26:01 CEST 2001 */ +/* Computer: whiplash.flachland-chemnitz.de */ +/* System: Linux 2.4.0 on i686 */ +/* */ +/* Author: Sam Wilkins */ +/* System: Mac OS X and port to big endian processor */ +/* */ +/*---------------------------------------------------------------------------*/ +/* Date Ver Who Comment */ +/* -------- ---- --- ------- */ +/* 29.01.01 1.00 Mar Created */ +/* 24.03.03 1.01 Lad Some cosmetic changes */ +/* 12.11.03 1.02 Dan Macintosh compatibility */ +/* 24.07.04 1.03 Sam Mac OS X compatibility */ +/* 22.11.06 1.04 Sam Mac OS X compatibility (for StormLib 6.0) */ +/* 31.12.06 1.05 XPinguin Full GNU/Linux compatibility */ +/* 17.10.12 1.05 Lad Moved error codes so they don't overlap with errno.h */ +/*****************************************************************************/ + +#ifndef __STORMPORT_H__ +#define __STORMPORT_H__ + +#ifndef __cplusplus + #define bool char + #define true 1 + #define false 0 +#endif + +//----------------------------------------------------------------------------- +// Defines for Windows + +#if !defined(PLATFORM_DEFINED) && defined(_WIN32) + + // In MSVC 8.0, there are some functions declared as deprecated. + #if _MSC_VER >= 1400 + #define _CRT_SECURE_NO_DEPRECATE + #define _CRT_NON_CONFORMING_SWPRINTFS + #endif + + #include + #include + #include + #include + #include + #include + #define PLATFORM_LITTLE_ENDIAN + + #ifdef _WIN64 + #define PLATFORM_64BIT + #else + #define PLATFORM_32BIT + #endif + + #define PLATFORM_WINDOWS + #define PLATFORM_DEFINED // The platform is known now + +#endif + +//----------------------------------------------------------------------------- +// Defines for Mac + +#if !defined(PLATFORM_DEFINED) && defined(__APPLE__) // Mac BSD API + + // Macintosh + #include + #include + #include + #include + #include + #include + #include + + // Support for PowerPC on Max OS X + #if (__ppc__ == 1) || (__POWERPC__ == 1) || (_ARCH_PPC == 1) + #include + #include + #endif + + #define PKEXPORT + #define __SYS_ZLIB + #define __SYS_BZLIB + + #ifndef __BIG_ENDIAN__ + #define PLATFORM_LITTLE_ENDIAN + #endif + + #define PLATFORM_MAC + #define PLATFORM_DEFINED // The platform is known now + +#endif + +//----------------------------------------------------------------------------- +// Assumption: we are not on Windows nor Macintosh, so this must be linux *grin* + +#if !defined(PLATFORM_DEFINED) + + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + + #define PLATFORM_LITTLE_ENDIAN + #define PLATFORM_LINUX + #define PLATFORM_DEFINED + +#endif + +//----------------------------------------------------------------------------- +// Definition of Windows-specific types for non-Windows platforms + +#ifndef PLATFORM_WINDOWS + #if __LP64__ + #define PLATFORM_64BIT + #else + #define PLATFORM_32BIT + #endif + + // Typedefs for ANSI C + typedef unsigned char BYTE; + typedef unsigned short USHORT; + typedef int LONG; + typedef unsigned int DWORD; + typedef unsigned long DWORD_PTR; + typedef long LONG_PTR; + typedef long INT_PTR; + typedef long long LONGLONG; + typedef unsigned long long ULONGLONG; + typedef void * HANDLE; + typedef void * LPOVERLAPPED; // Unsupported on Linux and Mac + typedef char TCHAR; + typedef unsigned int LCID; + typedef LONG * PLONG; + typedef DWORD * LPDWORD; + typedef BYTE * LPBYTE; + + #ifdef PLATFORM_32BIT + #define _LZMA_UINT32_IS_ULONG + #endif + + // Some Windows-specific defines + #ifndef MAX_PATH + #define MAX_PATH 1024 + #endif + + #define WINAPI + + #define FILE_BEGIN SEEK_SET + #define FILE_CURRENT SEEK_CUR + #define FILE_END SEEK_END + + #define _T(x) x + #define _tcslen strlen + #define _tcscpy strcpy + #define _tcscat strcat + #define _tcschr strchr + #define _tcsrchr strrchr + #define _tcsstr strstr + #define _tprintf printf + #define _stprintf sprintf + #define _tremove remove + + #define _stricmp strcasecmp + #define _strnicmp strncasecmp + #define _tcsicmp strcasecmp + #define _tcsnicmp strncasecmp + +#endif // !PLATFORM_WINDOWS + +// 64-bit calls are supplied by "normal" calls on Mac +#if defined(PLATFORM_MAC) + #define stat64 stat + #define fstat64 fstat + #define lseek64 lseek + #define ftruncate64 ftruncate + #define off64_t off_t + #define O_LARGEFILE 0 +#endif + +// Platform-specific error codes for UNIX-based platforms +#if defined(PLATFORM_MAC) || defined(PLATFORM_LINUX) + #define ERROR_SUCCESS 0 + #define ERROR_FILE_NOT_FOUND ENOENT + #define ERROR_ACCESS_DENIED EPERM + #define ERROR_INVALID_HANDLE EBADF + #define ERROR_NOT_ENOUGH_MEMORY ENOMEM + #define ERROR_NOT_SUPPORTED ENOTSUP + #define ERROR_INVALID_PARAMETER EINVAL + #define ERROR_DISK_FULL ENOSPC + #define ERROR_ALREADY_EXISTS EEXIST + #define ERROR_INSUFFICIENT_BUFFER ENOBUFS + #define ERROR_BAD_FORMAT 1000 // No such error code under Linux + #define ERROR_NO_MORE_FILES 1001 // No such error code under Linux + #define ERROR_HANDLE_EOF 1002 // No such error code under Linux + #define ERROR_CAN_NOT_COMPLETE 1003 // No such error code under Linux + #define ERROR_FILE_CORRUPT 1004 // No such error code under Linux +#endif + +//----------------------------------------------------------------------------- +// Swapping functions + +#ifdef PLATFORM_LITTLE_ENDIAN + #define BSWAP_INT16_UNSIGNED(a) (a) + #define BSWAP_INT16_SIGNED(a) (a) + #define BSWAP_INT32_UNSIGNED(a) (a) + #define BSWAP_INT32_SIGNED(a) (a) + #define BSWAP_INT64_SIGNED(a) (a) + #define BSWAP_INT64_UNSIGNED(a) (a) + #define BSWAP_ARRAY16_UNSIGNED(a,b) {} + #define BSWAP_ARRAY32_UNSIGNED(a,b) {} + #define BSWAP_ARRAY64_UNSIGNED(a,b) {} + #define BSWAP_PART_HEADER(a) {} + #define BSWAP_TMPQHEADER(a,b) {} + #define BSWAP_TMPKHEADER(a) {} +#else + +#ifdef __cplusplus + extern "C" { +#endif + int16_t SwapInt16(uint16_t); + uint16_t SwapUInt16(uint16_t); + int32_t SwapInt32(uint32_t); + uint32_t SwapUInt32(uint32_t); + int64_t SwapInt64(uint64_t); + uint64_t SwapUInt64(uint64_t); + void ConvertUInt16Buffer(void * ptr, size_t length); + void ConvertUInt32Buffer(void * ptr, size_t length); + void ConvertUInt64Buffer(void * ptr, size_t length); + void ConvertTMPQUserData(void *userData); + void ConvertTMPQHeader(void *header, uint16_t wPart); + void ConvertTMPKHeader(void *header); +#ifdef __cplusplus + } +#endif + #define BSWAP_INT16_SIGNED(a) SwapInt16((a)) + #define BSWAP_INT16_UNSIGNED(a) SwapUInt16((a)) + #define BSWAP_INT32_SIGNED(a) SwapInt32((a)) + #define BSWAP_INT32_UNSIGNED(a) SwapUInt32((a)) + #define BSWAP_INT64_SIGNED(a) SwapInt64((a)) + #define BSWAP_INT64_UNSIGNED(a) SwapUInt64((a)) + #define BSWAP_ARRAY16_UNSIGNED(a,b) ConvertUInt16Buffer((a),(b)) + #define BSWAP_ARRAY32_UNSIGNED(a,b) ConvertUInt32Buffer((a),(b)) + #define BSWAP_ARRAY64_UNSIGNED(a,b) ConvertUInt64Buffer((a),(b)) + #define BSWAP_TMPQHEADER(a,b) ConvertTMPQHeader((a),(b)) + #define BSWAP_TMPKHEADER(a) ConvertTMPKHeader((a)) +#endif + +//----------------------------------------------------------------------------- +// Macro for deprecated symbols + +/* +#ifdef _MSC_VER + #if _MSC_FULL_VER >= 140050320 + #define STORMLIB_DEPRECATED(_Text) __declspec(deprecated(_Text)) + #else + #define STORMLIB_DEPRECATED(_Text) __declspec(deprecated) + #endif +#else + #ifdef __GNUC__ + #define STORMLIB_DEPRECATED(_Text) __attribute__((deprecated)) + #else + #define STORMLIB_DEPRECATED(_Text) __attribute__((deprecated(_Text))) + #endif +#endif + +// When a flag is deprecated, use this macro +#ifndef _STORMLIB_NO_DEPRECATE + #define STORMLIB_DEPRECATED_FLAG(type, oldflag, newflag) \ + const STORMLIB_DEPRECATED(#oldflag " is deprecated. Use " #newflag ". To supress this warning, define _STORMLIB_NO_DEPRECATE") static type oldflag = (type)newflag; +#else +#define STORMLIB_DEPRECATED_FLAG(type, oldflag, newflag) static type oldflag = (type)newflag; +#endif +*/ + +#endif // __STORMPORT_H__ diff --git a/test/StormTest.cpp b/test/StormTest.cpp index 192c5d4..932458e 100644 --- a/test/StormTest.cpp +++ b/test/StormTest.cpp @@ -1,4439 +1,4439 @@ -/*****************************************************************************/ -/* StormTest.cpp Copyright (c) Ladislav Zezula 2003 */ -/*---------------------------------------------------------------------------*/ -/* Test module for StormLib */ -/*---------------------------------------------------------------------------*/ -/* Date Ver Who Comment */ -/* -------- ---- --- ------- */ -/* 25.03.03 1.00 Lad The first version of StormTest.cpp */ -/*****************************************************************************/ - -#define _CRT_NON_CONFORMING_SWPRINTFS -#define _CRT_SECURE_NO_DEPRECATE -#define __INCLUDE_CRYPTOGRAPHY__ -#define __STORMLIB_SELF__ // Don't use StormLib.lib -#include - -#ifdef _MSC_VER -#include -#endif - -#include "../src/StormLib.h" -#include "../src/StormCommon.h" - -#include "TLogHelper.cpp" // Helper class for showing test results - -#ifdef _MSC_VER -#pragma warning(disable: 4505) // 'XXX' : unreferenced local function has been removed -#pragma comment(lib, "winmm.lib") -#endif - -#ifdef PLATFORM_LINUX -#include -#endif - -//------------------------------------------------------------------------------ -// Defines - -#ifdef PLATFORM_WINDOWS -#define WORK_PATH_ROOT "E:\\Multimedia\\MPQs" -#endif - -#ifdef PLATFORM_LINUX -#define WORK_PATH_ROOT "/home/ladik/MPQs" -#endif - -#ifdef PLATFORM_MAC -#define WORK_PATH_ROOT "/Users/sam/StormLib/test" -#endif - -// Global for the work MPQ -static const char * szMpqSubDir = "1995 - Test MPQs"; -static const char * szMpqPatchDir = "1995 - Test MPQs\\patches"; - -typedef int (*FIND_FILE_CALLBACK)(const char * szFullPath); -typedef int (*FIND_PAIR_CALLBACK)(const char * szFullPath1, const char * szFullPath2); - -#define ERROR_UNDETERMINED_RESULT 0xC000FFFF - -//----------------------------------------------------------------------------- -// Testing data - -static DWORD AddFlags[] = -{ -// Compression Encryption Fixed key Single Unit Sector CRC - 0 | 0 | 0 | 0 | 0, - 0 | MPQ_FILE_ENCRYPTED | 0 | 0 | 0, - 0 | MPQ_FILE_ENCRYPTED | MPQ_FILE_FIX_KEY | 0 | 0, - 0 | 0 | 0 | MPQ_FILE_SINGLE_UNIT | 0, - 0 | MPQ_FILE_ENCRYPTED | 0 | MPQ_FILE_SINGLE_UNIT | 0, - 0 | MPQ_FILE_ENCRYPTED | MPQ_FILE_FIX_KEY | MPQ_FILE_SINGLE_UNIT | 0, - MPQ_FILE_IMPLODE | 0 | 0 | 0 | 0, - MPQ_FILE_IMPLODE | MPQ_FILE_ENCRYPTED | 0 | 0 | 0, - MPQ_FILE_IMPLODE | MPQ_FILE_ENCRYPTED | MPQ_FILE_FIX_KEY | 0 | 0, - MPQ_FILE_IMPLODE | 0 | 0 | MPQ_FILE_SINGLE_UNIT | 0, - MPQ_FILE_IMPLODE | MPQ_FILE_ENCRYPTED | 0 | MPQ_FILE_SINGLE_UNIT | 0, - MPQ_FILE_IMPLODE | MPQ_FILE_ENCRYPTED | MPQ_FILE_FIX_KEY | MPQ_FILE_SINGLE_UNIT | 0, - MPQ_FILE_IMPLODE | 0 | 0 | 0 | MPQ_FILE_SECTOR_CRC, - MPQ_FILE_IMPLODE | MPQ_FILE_ENCRYPTED | 0 | 0 | MPQ_FILE_SECTOR_CRC, - MPQ_FILE_IMPLODE | MPQ_FILE_ENCRYPTED | MPQ_FILE_FIX_KEY | 0 | MPQ_FILE_SECTOR_CRC, - MPQ_FILE_COMPRESS | 0 | 0 | 0 | 0, - MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | 0 | 0 | 0, - MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_FIX_KEY | 0 | 0, - MPQ_FILE_COMPRESS | 0 | 0 | MPQ_FILE_SINGLE_UNIT | 0, - MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | 0 | MPQ_FILE_SINGLE_UNIT | 0, - MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_FIX_KEY | MPQ_FILE_SINGLE_UNIT | 0, - MPQ_FILE_COMPRESS | 0 | 0 | 0 | MPQ_FILE_SECTOR_CRC, - MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | 0 | 0 | MPQ_FILE_SECTOR_CRC, - MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_FIX_KEY | 0 | MPQ_FILE_SECTOR_CRC, - 0xFFFFFFFF -}; - -static DWORD WaveCompressions[] = -{ - MPQ_COMPRESSION_ADPCM_MONO | MPQ_COMPRESSION_HUFFMANN, - MPQ_COMPRESSION_ADPCM_STEREO | MPQ_COMPRESSION_HUFFMANN, - MPQ_COMPRESSION_PKWARE, - MPQ_COMPRESSION_ZLIB, - MPQ_COMPRESSION_BZIP2 -}; - -static const wchar_t szUnicodeName1[] = { // Czech - 0x010C, 0x0065, 0x0073, 0x006B, 0x00FD, _T('.'), _T('m'), _T('p'), _T('q'), 0 -}; - -static const wchar_t szUnicodeName2[] = { // Russian - 0x0420, 0x0443, 0x0441, 0x0441, 0x043A, 0x0438, 0x0439, _T('.'), _T('m'), _T('p'), _T('q'), 0 -}; - -static const wchar_t szUnicodeName3[] = { // Greek - 0x03B5, 0x03BB, 0x03BB, 0x03B7, 0x03BD, 0x03B9, 0x03BA, 0x03AC, _T('.'), _T('m'), _T('p'), _T('q'), 0 -}; - -static const wchar_t szUnicodeName4[] = { // Chinese - 0x65E5, 0x672C, 0x8A9E, _T('.'), _T('m'), _T('p'), _T('q'), 0 -}; - -static const wchar_t szUnicodeName5[] = { // Japanese - 0x7B80, 0x4F53, 0x4E2D, 0x6587, _T('.'), _T('m'), _T('p'), _T('q'), 0 -}; - -static const wchar_t szUnicodeName6[] = { // Arabic - 0x0627, 0x0644, 0x0639, 0x0639, 0x0631, 0x0628, 0x064A, 0x0629, _T('.'), _T('m'), _T('p'), _T('q'), 0 -}; - -static const char * PatchList_WoW_OldWorld13286[] = -{ - "MPQ_2012_v4_OldWorld.MPQ", - "wow-update-oldworld-13154.MPQ", - "wow-update-oldworld-13286.MPQ", - NULL -}; - -static const char * PatchList_WoW_15050[] = -{ - "MPQ_2013_v4_world.MPQ", - "wow-update-13164.MPQ", - "wow-update-13205.MPQ", - "wow-update-13287.MPQ", - "wow-update-13329.MPQ", - "wow-update-13596.MPQ", - "wow-update-13623.MPQ", - "wow-update-base-13914.MPQ", - "wow-update-base-14007.MPQ", - "wow-update-base-14333.MPQ", - "wow-update-base-14480.MPQ", - "wow-update-base-14545.MPQ", - "wow-update-base-14946.MPQ", - "wow-update-base-15005.MPQ", - "wow-update-base-15050.MPQ", - NULL -}; - -static const char * PatchList_WoW_16965[] = -{ - "MPQ_2013_v4_locale-enGB.MPQ", - "wow-update-enGB-16016.MPQ", - "wow-update-enGB-16048.MPQ", - "wow-update-enGB-16057.MPQ", - "wow-update-enGB-16309.MPQ", - "wow-update-enGB-16357.MPQ", - "wow-update-enGB-16516.MPQ", - "wow-update-enGB-16650.MPQ", - "wow-update-enGB-16844.MPQ", - "wow-update-enGB-16965.MPQ", - NULL -}; - -static const char * PatchList_SC2_32283[] = -{ - "MPQ_2013_v4_Base1.SC2Data", - "s2-update-base-23258.MPQ", - "s2-update-base-24540.MPQ", - "s2-update-base-26147.MPQ", - "s2-update-base-28522.MPQ", - "s2-update-base-30508.MPQ", - "s2-update-base-32283.MPQ", - NULL -}; - -static const char * PatchList_SC2_34644[] = -{ - "MPQ_2013_v4_Base1.SC2Data", - "s2-update-base-23258.MPQ", - "s2-update-base-24540.MPQ", - "s2-update-base-26147.MPQ", - "s2-update-base-28522.MPQ", - "s2-update-base-32384.MPQ", - "s2-update-base-34644.MPQ", - NULL -}; - -static const char * PatchList_SC2_34644_Maps[] = -{ - "MPQ_2013_v4_Base3.SC2Maps", - "s2-update-base-23258.MPQ", - "s2-update-base-24540.MPQ", - "s2-update-base-26147.MPQ", - "s2-update-base-28522.MPQ", - "s2-update-base-32384.MPQ", - "s2-update-base-34644.MPQ", - NULL -}; - -static const char * PatchList_SC2_32283_enGB[] = -{ - "MPQ_2013_v4_enGB.SC2Data", - "s2-update-enGB-23258.MPQ", - "s2-update-enGB-24540.MPQ", - "s2-update-enGB-26147.MPQ", - "s2-update-enGB-28522.MPQ", - "s2-update-enGB-30508.MPQ", - "s2-update-enGB-32283.MPQ", - NULL -}; - -static const char * PatchList_HS_6898_enGB[] = -{ - "MPQ_2014_v4_base-Win.MPQ", - "hs-0-5314-Win-final.MPQ", - "hs-5314-5435-Win-final.MPQ", - "hs-5435-5506-Win-final.MPQ", - "hs-5506-5834-Win-final.MPQ", - "hs-5834-6024-Win-final.MPQ", - "hs-6024-6141-Win-final.MPQ", - "hs-6141-6187-Win-final.MPQ", - "hs-6187-6284-Win-final.MPQ", - "hs-6284-6485-Win-final.MPQ", - "hs-6485-6898-Win-final.MPQ", - NULL -}; - -//----------------------------------------------------------------------------- -// Local file functions - -// Definition of the path separator -#ifdef PLATFORM_WINDOWS -#define PATH_SEPARATOR '\\' // Path separator for Windows platforms -#else -#define PATH_SEPARATOR '/' // Path separator for Windows platforms -#endif - -// This must be the directory where our test MPQs are stored. -// We also expect a subdirectory named -static char szMpqDirectory[MAX_PATH+1]; -size_t cchMpqDirectory = 0; - -static bool IsFullPath(const char * szFileName) -{ -#ifdef PLATFORM_WINDOWS - if(('A' <= szFileName[0] && szFileName[0] <= 'Z') || ('a' <= szFileName[0] && szFileName[0] <= 'z')) - { - return (szFileName[1] == ':' && szFileName[2] == PATH_SEPARATOR); - } -#endif - - szFileName = szFileName; - return false; -} - -static bool IsMpqExtension(const char * szFileName) -{ - const char * szExtension = strrchr(szFileName, '.'); - - if(szExtension != NULL) - { - if(!_stricmp(szExtension, ".mpq")) - return true; - if(!_stricmp(szExtension, ".w3m")) - return true; - if(!_stricmp(szExtension, ".w3x")) - return true; - if(!_stricmp(szExtension, ".mpqe")) - return true; - if(!_stricmp(szExtension, ".part")) - return true; - if(!_stricmp(szExtension, ".sv")) - return true; - if(!_stricmp(szExtension, ".s2ma")) - return true; - if(!_stricmp(szExtension, ".SC2Map")) - return true; - if(!_stricmp(szExtension, ".0")) // .MPQ.0 - return true; -// if(!_stricmp(szExtension, ".link")) -// return true; - } - - return false; -} - -static void AddStringBeforeExtension(char * szBuffer, const char * szFileName, const char * szExtraString) -{ - const char * szExtension; - size_t nLength; - - // Get the extension - szExtension = strrchr(szFileName, '.'); - if(szExtension == NULL) - szExtension = szFileName + strlen(szFileName); - nLength = (size_t)(szExtension - szFileName); - - // Copy the part before extension - memcpy(szBuffer, szFileName, nLength); - szFileName += nLength; - szBuffer += nLength; - - // Append the extra data - if(szExtraString != NULL) - strcpy(szBuffer, szExtraString); - - // Append the rest of the file name - strcat(szBuffer, szFileName); -} - -static bool CompareBlocks(LPBYTE pbBlock1, LPBYTE pbBlock2, DWORD dwLength, DWORD * pdwDifference) -{ - for(DWORD i = 0; i < dwLength; i++) - { - if(pbBlock1[i] != pbBlock2[i]) - { - pdwDifference[0] = i; - return false; - } - } - - return true; -} - -static size_t ConvertSha1ToText(const unsigned char * sha1_digest, char * szSha1Text) -{ - const char * szTable = "0123456789abcdef"; - - for(size_t i = 0; i < SHA1_DIGEST_SIZE; i++) - { - *szSha1Text++ = szTable[(sha1_digest[0] >> 0x04)]; - *szSha1Text++ = szTable[(sha1_digest[0] & 0x0F)]; - sha1_digest++; - } - - *szSha1Text = 0; - return (SHA1_DIGEST_SIZE * 2); -} - -static int GetPathSeparatorCount(const char * szPath) -{ - int nSeparatorCount = 0; - - while(szPath[0] != 0) - { - if(szPath[0] == '\\' || szPath[0] == '/') - nSeparatorCount++; - szPath++; - } - - return nSeparatorCount; -} - -static const char * FindNextPathPart(const char * szPath, size_t nPartCount) -{ - const char * szPathPart = szPath; - - while(szPath[0] != 0 && nPartCount > 0) - { - // Is there path separator? - if(szPath[0] == '\\' || szPath[0] == '/') - { - szPathPart = szPath + 1; - nPartCount--; - } - - // Move to the next letter - szPath++; - } - - return szPathPart; -} - -static const char * GetShortPlainName(const char * szFileName) -{ - const char * szPlainName = FindNextPathPart(szFileName, 1000); - const char * szPlainEnd = szFileName + strlen(szFileName); - - // If the name is still too long, cut it - if((szPlainEnd - szPlainName) > 50) - szPlainName = szPlainEnd - 50; - - return szPlainName; -} - -static void CopyPathPart(char * szBuffer, const char * szPath) -{ - while(szPath[0] != 0) - { - szBuffer[0] = (szPath[0] == '\\' || szPath[0] == '/') ? '/' : szPath[0]; - szBuffer++; - szPath++; - } - - *szBuffer = 0; -} - -static bool CopyStringAndVerifyConversion( - const TCHAR * szFoundFile, - TCHAR * szBufferT, - char * szBufferA) -{ - // Convert the TCHAR name to ANSI name - CopyFileName(szBufferA, szFoundFile, _tcslen(szFoundFile)); - CopyFileName(szBufferT, szBufferA, strlen(szBufferA)); - - // Compare both TCHAR strings - return (_tcsicmp(szBufferT, szFoundFile) == 0) ? true : false; -} - -static void CalculateRelativePath(const char * szFullPath1, const char * szFullPath2, char * szBuffer) -{ - const char * szPathPart1 = szFullPath1; - const char * szPathPart2 = szFullPath2; - const char * szNextPart1; - const char * szNextPart2; - int nEqualParts = 0; - int nStepsUp = 0; - - // Parse both paths and find all path parts that are equal - for(;;) - { - // Find the next part of the first path - szNextPart1 = FindNextPathPart(szPathPart1, 1); - if(szNextPart1 == szPathPart1) - break; - - szNextPart2 = FindNextPathPart(szPathPart2, 1); - if(szNextPart2 == szPathPart2) - break; - - // Are these equal? - if((szNextPart2 - szPathPart2) != (szNextPart1 - szPathPart1)) - break; - if(_strnicmp(szPathPart1, szPathPart2, (szNextPart1 - szPathPart1 - 1))) - break; - - // Increment the number of path parts that are equal - szPathPart1 = szNextPart1; - szPathPart2 = szNextPart2; - nEqualParts++; - } - - // If we found at least one equal part, we can create relative path - if(nEqualParts != 0) - { - // Calculate how many steps up we need to go - nStepsUp = GetPathSeparatorCount(szPathPart2); - - // Append "../" nStepsUp-times - for(int i = 0; i < nStepsUp; i++) - { - *szBuffer++ = '.'; - *szBuffer++ = '.'; - *szBuffer++ = '/'; - } - - // Append the rest of the path. Also change DOS backslashes to slashes - CopyPathPart(szBuffer, szPathPart1); - return; - } - - // Failed. Just copy the source path as it is - strcpy(szBuffer, szFullPath1); -} - -static TFileStream * FileStream_OpenFileA(const char * szFileName, DWORD dwStreamFlags) -{ - TCHAR szFileNameT[MAX_PATH]; - - CopyFileName(szFileNameT, szFileName, strlen(szFileName)); - return FileStream_OpenFile(szFileNameT, dwStreamFlags); -} - -static TFileStream * FileStream_CreateFileA(const char * szFileName, DWORD dwStreamFlags) -{ - TCHAR szFileNameT[MAX_PATH]; - - CopyFileName(szFileNameT, szFileName, strlen(szFileName)); - return FileStream_CreateFile(szFileNameT, dwStreamFlags); -} - -static size_t FileStream_PrefixA(const char * szFileName, DWORD * pdwProvider) -{ - TCHAR szFileNameT[MAX_PATH]; - size_t nPrefixLength = 0; - - if(szFileName != NULL) - { - CopyFileName(szFileNameT, szFileName, strlen(szFileName)); - nPrefixLength = FileStream_Prefix(szFileNameT, pdwProvider); - } - - return nPrefixLength; -} - -static void CreateFullPathName(char * szBuffer, const char * szSubDir, const char * szNamePart1, const char * szNamePart2 = NULL) -{ - char * szSaveBuffer = szBuffer; - size_t nPrefixLength = 0; - size_t nLength; - DWORD dwProvider = 0; - bool bIsFullPath = false; - char chSeparator = PATH_SEPARATOR; - - // Determine the path prefix - if(szNamePart1 != NULL) - { - nPrefixLength = FileStream_PrefixA(szNamePart1, &dwProvider); - if((dwProvider & BASE_PROVIDER_MASK) == BASE_PROVIDER_HTTP) - { - bIsFullPath = true; - chSeparator = '/'; - } - else - bIsFullPath = IsFullPath(szNamePart1 + nPrefixLength); - } - - // Copy the MPQ prefix, if any - if(nPrefixLength > 0) - { - memcpy(szBuffer, szNamePart1, nPrefixLength); - szSaveBuffer += nPrefixLength; - szNamePart1 += nPrefixLength; - szBuffer += nPrefixLength; - } - - // If the given name is not a full path, copy the MPQ directory - if(bIsFullPath == false) - { - // Copy the master MPQ directory - memcpy(szBuffer, szMpqDirectory, cchMpqDirectory); - szBuffer += cchMpqDirectory; - - // Append the subdirectory, if any - if(szSubDir != NULL && (nLength = strlen(szSubDir)) != 0) - { - // No leading or trailing separator are allowed - assert(szSubDir[0] != '/' && szSubDir[0] != '\\'); - assert(szSubDir[nLength - 1] != '/' && szSubDir[nLength - 1] != '\\'); - - // Append file path separator - *szBuffer++ = PATH_SEPARATOR; - - // Append the subdirectory - memcpy(szBuffer, szSubDir, nLength); - szBuffer += nLength; - } - } - - // Copy the file name, if any - if(szNamePart1 != NULL && (nLength = strlen(szNamePart1)) != 0) - { - // Path separators are not allowed in the name part - assert(szNamePart1[0] != '\\' && szNamePart1[0] != '/'); - assert(szNamePart1[nLength - 1] != '/' && szNamePart1[nLength - 1] != '\\'); - - // Append file path separator - if(bIsFullPath == false) - *szBuffer++ = PATH_SEPARATOR; - - // Copy the file name - memcpy(szBuffer, szNamePart1, nLength); - szBuffer += nLength; - } - - // Append the second part of the name - if(szNamePart2 != NULL && (nLength = strlen(szNamePart2)) != 0) - { - // Copy the file name - memcpy(szBuffer, szNamePart2, nLength); - szBuffer += nLength; - } - - // Normalize the path separators - while(szSaveBuffer < szBuffer) - { - szSaveBuffer[0] = (szSaveBuffer[0] != '/' && szSaveBuffer[0] != '\\') ? szSaveBuffer[0] : chSeparator; - szSaveBuffer++; - } - - // Terminate the buffer with zero - *szBuffer = 0; -} - -static int CalculateFileSha1(TLogHelper * pLogger, const char * szFullPath, char * szFileSha1) -{ - TFileStream * pStream; - unsigned char sha1_digest[SHA1_DIGEST_SIZE]; - const char * szShortPlainName = GetShortPlainName(szFullPath); - hash_state sha1_state; - ULONGLONG ByteOffset = 0; - ULONGLONG FileSize = 0; - BYTE * pbFileBlock; - DWORD cbBytesToRead; - DWORD cbFileBlock = 0x100000; - int nError = ERROR_SUCCESS; - - // Notify the user - pLogger->PrintProgress("Hashing file %s", szShortPlainName); - szFileSha1[0] = 0; - - // Open the file to be verified - pStream = FileStream_OpenFileA(szFullPath, STREAM_FLAG_READ_ONLY); - if(pStream != NULL) - { - // Retrieve the size of the file - FileStream_GetSize(pStream, &FileSize); - - // Allocate the buffer for loading file parts - pbFileBlock = STORM_ALLOC(BYTE, cbFileBlock); - if(pbFileBlock != NULL) - { - // Initialize SHA1 calculation - sha1_init(&sha1_state); - - // Calculate the SHA1 of the file - while(ByteOffset < FileSize) - { - // Notify the user - pLogger->PrintProgress("Hashing file %s (%I64u of %I64u)", szShortPlainName, ByteOffset, FileSize); - - // Load the file block - cbBytesToRead = ((FileSize - ByteOffset) > cbFileBlock) ? cbFileBlock : (DWORD)(FileSize - ByteOffset); - if(!FileStream_Read(pStream, &ByteOffset, pbFileBlock, cbBytesToRead)) - { - nError = GetLastError(); - break; - } - - // Add to SHA1 - sha1_process(&sha1_state, pbFileBlock, cbBytesToRead); - ByteOffset += cbBytesToRead; - } - - // Notify the user - pLogger->PrintProgress("Hashing file %s (%I64u of %I64u)", szShortPlainName, ByteOffset, FileSize); - - // Finalize SHA1 - sha1_done(&sha1_state, sha1_digest); - - // Convert the SHA1 to ANSI text - ConvertSha1ToText(sha1_digest, szFileSha1); - STORM_FREE(pbFileBlock); - } - - FileStream_Close(pStream); - } - - // If we calculated something, return OK - if(nError == ERROR_SUCCESS && szFileSha1[0] == 0) - nError = ERROR_CAN_NOT_COMPLETE; - return nError; -} - -//----------------------------------------------------------------------------- -// Directory search - -static HANDLE InitDirectorySearch(const char * szDirectory) -{ -#ifdef PLATFORM_WINDOWS - - WIN32_FIND_DATA wf; - HANDLE hFind; - TCHAR szSearchMask[MAX_PATH]; - - // Keep compilers happy - CopyFileName(szSearchMask, szDirectory, strlen(szDirectory)); - _tcscat(szSearchMask, _T("\\*")); - - // Construct the directory mask - hFind = FindFirstFile(szSearchMask, &wf); - return (hFind != INVALID_HANDLE_VALUE) ? hFind : NULL; - -#endif - -#ifdef PLATFORM_LINUX - - // Keep compilers happy - return (HANDLE)opendir(szDirectory); - -#endif -} - -static bool SearchDirectory(HANDLE hFind, char * szDirEntry, bool & IsDirectory) -{ -#ifdef PLATFORM_WINDOWS - - WIN32_FIND_DATA wf; - TCHAR szDirEntryT[MAX_PATH]; - char szDirEntryA[MAX_PATH]; - - __SearchNextEntry: - - // Search for the hnext entry. - if(FindNextFile(hFind, &wf)) - { - // Verify if the directory entry is an UNICODE name that would be destroyed - // by Unicode->ANSI->Unicode conversion - if(CopyStringAndVerifyConversion(wf.cFileName, szDirEntryT, szDirEntryA) == false) - goto __SearchNextEntry; - - IsDirectory = (wf.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? true : false; - CopyFileName(szDirEntry, wf.cFileName, _tcslen(wf.cFileName)); - return true; - } - - return false; - -#endif - -#ifdef PLATFORM_LINUX - - struct dirent * directory_entry; - - directory_entry = readdir((DIR *)hFind); - if(directory_entry != NULL) - { - IsDirectory = (directory_entry->d_type == DT_DIR) ? true : false; - strcpy(szDirEntry, directory_entry->d_name); - return true; - } - - return false; - -#endif -} - -static void FreeDirectorySearch(HANDLE hFind) -{ -#ifdef PLATFORM_WINDOWS - FindClose(hFind); -#endif - -#ifdef PLATFORM_LINUX - closedir((DIR *)hFind); -#endif -} - -static int FindFilesInternal(FIND_FILE_CALLBACK pfnTest, char * szDirectory) -{ - HANDLE hFind; - char * szPlainName; - size_t nLength; - char szDirEntry[MAX_PATH]; - bool IsDirectory = false; - int nError = ERROR_SUCCESS; - - if(szDirectory != NULL) - { - // Initiate directory search - hFind = InitDirectorySearch(szDirectory); - if(hFind != NULL) - { - // Append slash at the end of the directory name - nLength = strlen(szDirectory); - szDirectory[nLength++] = PATH_SEPARATOR; - szPlainName = szDirectory + nLength; - - // Skip the first entry, since it's always "." or ".." - while(SearchDirectory(hFind, szDirEntry, IsDirectory) && nError == ERROR_SUCCESS) - { - // Copy the directory entry name to both names - strcpy(szPlainName, szDirEntry); - - // Found a directory? - if(IsDirectory) - { - if(szDirEntry[0] != '.') - { - nError = FindFilesInternal(pfnTest, szDirectory); - } - } - else - { - if(pfnTest != NULL) - { - nError = pfnTest(szDirectory); - } - } - } - - FreeDirectorySearch(hFind); - } - } - - // Free the path buffer, if any - return nError; -} - -static int FindFiles(FIND_FILE_CALLBACK pfnFindFile, const char * szSubDirectory) -{ - char szWorkBuff[MAX_PATH]; - - CreateFullPathName(szWorkBuff, szSubDirectory, NULL); - return FindFilesInternal(pfnFindFile, szWorkBuff); -} - -static int FindFilePairsInternal( - FIND_PAIR_CALLBACK pfnFilePair, - char * szSource, - char * szTarget) -{ - char * szPlainName1; - char * szPlainName2; - int nError = ERROR_SUCCESS; - - // Setup the search masks - strcat(szSource, "\\*"); - szPlainName1 = strrchr(szSource, '*'); - strcat(szTarget, "\\*"); - szPlainName2 = strrchr(szTarget, '*'); - - // If both paths are OK, perform the search - if(szPlainName1 != NULL && szPlainName2 != NULL) - { -#ifdef PLATFORM_WINDOWS - WIN32_FIND_DATAA wf; - HANDLE hFind; - - // Search the second directory - hFind = FindFirstFileA(szTarget, &wf); - if(hFind != INVALID_HANDLE_VALUE) - { - // Skip the first entry, since it's always "." or ".." - while(FindNextFileA(hFind, &wf) && nError == ERROR_SUCCESS) - { - // Found a directory? - if(wf.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) - { - if(wf.cFileName[0] != '.') - { - strcpy(szPlainName1, wf.cFileName); - strcpy(szPlainName2, wf.cFileName); - nError = FindFilePairsInternal(pfnFilePair, szSource, szTarget); - } - } - else - { - if(pfnFilePair != NULL) - { - strcpy(szPlainName1, wf.cFileName); - strcpy(szPlainName2, wf.cFileName); - nError = pfnFilePair(szSource, szTarget); - } - } - } - - FindClose(hFind); - } -#endif - } - - return nError; -} - -static int FindFilePairs(FIND_PAIR_CALLBACK pfnFindPair, const char * szSourceSubDir, const char * szTargetSubDir) -{ - char szSource[MAX_PATH]; - char szTarget[MAX_PATH]; - - // Create the source search mask - CreateFullPathName(szSource, szSourceSubDir, NULL); - CreateFullPathName(szTarget, szTargetSubDir, NULL); - return FindFilePairsInternal(pfnFindPair, szSource, szTarget); -} - -static int InitializeMpqDirectory(char * argv[], int argc) -{ - TLogHelper Logger("InitWorkDir"); - TFileStream * pStream; - const char * szWhereFrom = NULL; - const char * szDirName; - char szFullPath[MAX_PATH]; - - // Retrieve the name of the MPQ directory - if(argc > 1 && argv[1] != NULL) - { - szWhereFrom = "command line"; - szDirName = argv[1]; - } - else - { - szWhereFrom = "default"; - szDirName = WORK_PATH_ROOT; - } - - // Copy the name of the MPQ directory. - StringCopyA(szMpqDirectory, szDirName, MAX_PATH); - cchMpqDirectory = strlen(szMpqDirectory); - - // Cut trailing slashes and/or backslashes - while((cchMpqDirectory > 0) && (szMpqDirectory[cchMpqDirectory - 1] == '/' || szMpqDirectory[cchMpqDirectory - 1] == '\\')) - cchMpqDirectory--; - szMpqDirectory[cchMpqDirectory] = 0; - - // Print the work directory info - Logger.PrintMessage("Work directory %s (%s)", szMpqDirectory, szWhereFrom); - - // Verify if the work MPQ directory is writable - CreateFullPathName(szFullPath, NULL, "TestFile.bin"); - pStream = FileStream_CreateFileA(szFullPath, 0); - if(pStream == NULL) - return Logger.PrintError("MPQ subdirectory doesn't exist or is not writable"); - - // Close the stream - FileStream_Close(pStream); - remove(szFullPath); - - // Verify if the working directory exists and if there is a subdirectory with the file name - CreateFullPathName(szFullPath, szMpqSubDir, "ListFile_Blizzard.txt"); - pStream = FileStream_OpenFileA(szFullPath, STREAM_FLAG_READ_ONLY); - if(pStream == NULL) - return Logger.PrintError("The main listfile (%s) was not found. Check your paths", GetShortPlainName(szFullPath)); - - // Close the stream - FileStream_Close(pStream); - return ERROR_SUCCESS; -} - -static int GetFilePatchCount(TLogHelper * pLogger, HANDLE hMpq, const char * szFileName) -{ - TCHAR * szPatchName; - HANDLE hFile; - TCHAR szPatchChain[0x400]; - int nPatchCount = 0; - int nError = ERROR_SUCCESS; - - // Open the MPQ file - if(SFileOpenFileEx(hMpq, szFileName, 0, &hFile)) - { - // Notify the user - pLogger->PrintProgress("Verifying patch chain for %s ...", GetShortPlainName(szFileName)); - - // Query the patch chain - if(!SFileGetFileInfo(hFile, SFileInfoPatchChain, szPatchChain, sizeof(szPatchChain), NULL)) - nError = pLogger->PrintError("Failed to retrieve the patch chain on %s", szFileName); - - // Is there anything at all in the patch chain? - if(nError == ERROR_SUCCESS && szPatchChain[0] == 0) - { - pLogger->PrintError("The patch chain for %s is empty", szFileName); - nError = ERROR_FILE_CORRUPT; - } - - // Now calculate the number of patches - if(nError == ERROR_SUCCESS) - { - // Get the pointer to the patch - szPatchName = szPatchChain; - - // Skip the base name - for(;;) - { - // Skip the current name - szPatchName = szPatchName + _tcslen(szPatchName) + 1; - if(szPatchName[0] == 0) - break; - - // Increment number of patches - nPatchCount++; - } - } - - SFileCloseFile(hFile); - } - else - { - pLogger->PrintError("Failed to open file %s", szFileName); - } - - return nPatchCount; -} - -static int VerifyFilePatchCount(TLogHelper * pLogger, HANDLE hMpq, const char * szFileName, int nExpectedPatchCount) -{ - int nPatchCount = 0; - - // Retrieve the patch count - pLogger->PrintProgress("Verifying patch count for %s ...", szFileName); - nPatchCount = GetFilePatchCount(pLogger, hMpq, szFileName); - - // Check if there are any patches at all - if(nExpectedPatchCount != 0 && nPatchCount == 0) - { - pLogger->PrintMessage("There are no patches beyond %s", szFileName); - return ERROR_FILE_CORRUPT; - } - - // Check if the number of patches fits - if(nPatchCount != nExpectedPatchCount) - { - pLogger->PrintMessage("Unexpected number of patches for %s", szFileName); - return ERROR_FILE_CORRUPT; - } - - return ERROR_SUCCESS; -} - -static int CreateEmptyFile(TLogHelper * pLogger, const char * szPlainName, ULONGLONG FileSize, char * szBuffer) -{ - TFileStream * pStream; - char szFullPath[MAX_PATH]; - - // Notify the user - pLogger->PrintProgress("Creating empty file %s ...", szPlainName); - - // Construct the full path and crete the file - CreateFullPathName(szFullPath, NULL, szPlainName); - pStream = FileStream_CreateFileA(szFullPath, STREAM_PROVIDER_FLAT | BASE_PROVIDER_FILE); - if(pStream == NULL) - return pLogger->PrintError("Failed to create file %s", szBuffer); - - // Write the required size - FileStream_SetSize(pStream, FileSize); - FileStream_Close(pStream); - - // Give the caller the full file name - if(szBuffer != NULL) - strcpy(szBuffer, szFullPath); - return ERROR_SUCCESS; -} - -static int VerifyFilePosition( - TLogHelper * pLogger, - TFileStream * pStream, - ULONGLONG ExpectedPosition) -{ - ULONGLONG ByteOffset = 0; - int nError = ERROR_SUCCESS; - - // Retrieve the file position - if(FileStream_GetPos(pStream, &ByteOffset)) - { - if(ByteOffset != ExpectedPosition) - { - pLogger->PrintMessage("The file position is different than expected (expected: " I64u_a ", current: " I64u_a, ExpectedPosition, ByteOffset); - nError = ERROR_FILE_CORRUPT; - } - } - else - { - nError = pLogger->PrintError("Failed to retrieve the file offset"); - } - - return nError; -} - -static int VerifyFileMpqHeader(TLogHelper * pLogger, TFileStream * pStream, ULONGLONG * pByteOffset) -{ - TMPQHeader Header; - int nError = ERROR_SUCCESS; - - memset(&Header, 0xFE, sizeof(TMPQHeader)); - if(FileStream_Read(pStream, pByteOffset, &Header, sizeof(TMPQHeader))) - { - if(Header.dwID != ID_MPQ) - { - pLogger->PrintMessage("Read error - the data is not a MPQ header"); - nError = ERROR_FILE_CORRUPT; - } - } - else - { - nError = pLogger->PrintError("Failed to read the MPQ header"); - } - - return nError; -} - -static int WriteMpqUserDataHeader( - TLogHelper * pLogger, - TFileStream * pStream, - ULONGLONG ByteOffset, - DWORD dwByteCount) -{ - TMPQUserData UserData; - int nError = ERROR_SUCCESS; - - // Notify the user - pLogger->PrintProgress("Writing user data header..."); - - // Fill the user data header - UserData.dwID = ID_MPQ_USERDATA; - UserData.cbUserDataSize = dwByteCount; - UserData.dwHeaderOffs = (dwByteCount + sizeof(TMPQUserData)); - UserData.cbUserDataHeader = dwByteCount / 2; - if(!FileStream_Write(pStream, &ByteOffset, &UserData, sizeof(TMPQUserData))) - nError = GetLastError(); - return nError; -} - -static int WriteFileData( - TLogHelper * pLogger, - TFileStream * pStream, - ULONGLONG ByteOffset, - ULONGLONG ByteCount) -{ - ULONGLONG SaveByteCount = ByteCount; - ULONGLONG BytesWritten = 0; - LPBYTE pbDataBuffer; - DWORD cbDataBuffer = 0x10000; - int nError = ERROR_SUCCESS; - - // Write some data - pbDataBuffer = new BYTE[cbDataBuffer]; - if(pbDataBuffer != NULL) - { - memset(pbDataBuffer, 0, cbDataBuffer); - strcpy((char *)pbDataBuffer, "This is a test data written to a file."); - - // Perform the write - while(ByteCount > 0) - { - DWORD cbToWrite = (ByteCount > cbDataBuffer) ? cbDataBuffer : (DWORD)ByteCount; - - // Notify the user - pLogger->PrintProgress("Writing file data (%I64u of %I64u) ...", BytesWritten, SaveByteCount); - - // Write the data - if(!FileStream_Write(pStream, &ByteOffset, pbDataBuffer, cbToWrite)) - { - nError = GetLastError(); - break; - } - - BytesWritten += cbToWrite; - ByteOffset += cbToWrite; - ByteCount -= cbToWrite; - } - - delete [] pbDataBuffer; - } - return nError; -} - -static int CopyFileData( - TLogHelper * pLogger, - TFileStream * pStream1, - TFileStream * pStream2, - ULONGLONG ByteOffset, - ULONGLONG ByteCount) -{ - ULONGLONG BytesCopied = 0; - ULONGLONG EndOffset = ByteOffset + ByteCount; - LPBYTE pbCopyBuffer; - DWORD BytesToRead; - DWORD BlockLength = 0x100000; - int nError = ERROR_SUCCESS; - - // Allocate copy buffer - pbCopyBuffer = STORM_ALLOC(BYTE, BlockLength); - if(pbCopyBuffer != NULL) - { - while(ByteOffset < EndOffset) - { - // Read source - BytesToRead = ((EndOffset - ByteOffset) > BlockLength) ? BlockLength : (DWORD)(EndOffset - ByteOffset); - if(!FileStream_Read(pStream1, &ByteOffset, pbCopyBuffer, BytesToRead)) - { - nError = GetLastError(); - break; - } - - // Write to the destination file - if(!FileStream_Write(pStream2, NULL, pbCopyBuffer, BytesToRead)) - { - nError = GetLastError(); - break; - } - - // Increment the byte counts - BytesCopied += BytesToRead; - ByteOffset += BytesToRead; - - // Notify the user - pLogger->PrintProgress("Copying (%I64u of %I64u complete) ...", BytesCopied, ByteCount); - } - - STORM_FREE(pbCopyBuffer); - } - - return nError; -} - -// Support function for copying file -static int CreateFileCopy( - TLogHelper * pLogger, - const char * szPlainName, - const char * szFileCopy, - char * szBuffer, - ULONGLONG PreMpqDataSize = 0, - ULONGLONG UserDataSize = 0) -{ - TFileStream * pStream1; // Source file - TFileStream * pStream2; // Target file - ULONGLONG ByteOffset = 0; - ULONGLONG FileSize = 0; - char szFileName1[MAX_PATH]; - char szFileName2[MAX_PATH]; - int nError = ERROR_SUCCESS; - - // Notify the user - szPlainName += FileStream_PrefixA(szPlainName, NULL); - pLogger->PrintProgress("Creating copy of %s ...", szPlainName); - - // Construct both file names. Check if they are not the same - CreateFullPathName(szFileName1, szMpqSubDir, szPlainName); - CreateFullPathName(szFileName2, NULL, szFileCopy + FileStream_PrefixA(szFileCopy, NULL)); - if(!_stricmp(szFileName1, szFileName2)) - { - pLogger->PrintError("Failed to create copy of MPQ (the copy name is the same like the original name)"); - return ERROR_CAN_NOT_COMPLETE; - } - - // Open the source file - pStream1 = FileStream_OpenFileA(szFileName1, STREAM_FLAG_READ_ONLY); - if(pStream1 == NULL) - { - pLogger->PrintError("Failed to open the source file %s", szFileName1); - return ERROR_CAN_NOT_COMPLETE; - } - - // Create the destination file - pStream2 = FileStream_CreateFileA(szFileName2, 0); - if(pStream2 != NULL) - { - // If we should write some pre-MPQ data to the target file, do it - if(PreMpqDataSize != 0) - { - nError = WriteFileData(pLogger, pStream2, ByteOffset, PreMpqDataSize); - ByteOffset += PreMpqDataSize; - } - - // If we should write some MPQ user data, write the header first - if(UserDataSize != 0) - { - nError = WriteMpqUserDataHeader(pLogger, pStream2, ByteOffset, (DWORD)UserDataSize); - ByteOffset += sizeof(TMPQUserData); - - nError = WriteFileData(pLogger, pStream2, ByteOffset, UserDataSize); - ByteOffset += UserDataSize; - } - - // Copy the file data from the source file to the destination file - FileStream_GetSize(pStream1, &FileSize); - if(FileSize != 0) - { - nError = CopyFileData(pLogger, pStream1, pStream2, 0, FileSize); - ByteOffset += FileSize; - } - FileStream_Close(pStream2); - } - - // Close the source file - FileStream_Close(pStream1); - - // Create the full file name of the target file, including prefix - if(szBuffer != NULL) - CreateFullPathName(szBuffer, NULL, szFileCopy); - - // Report error, if any - if(nError != ERROR_SUCCESS) - pLogger->PrintError("Failed to create copy of MPQ"); - return nError; -} - -static int CreateMasterAndMirrorPaths( - TLogHelper * pLogger, - char * szMirrorPath, - char * szMasterPath, - const char * szMirrorName, - const char * szMasterName, - bool bCopyMirrorFile) -{ - char szCopyPath[MAX_PATH]; - int nError = ERROR_SUCCESS; - - // Always delete the mirror file - CreateFullPathName(szMasterPath, szMpqSubDir, szMasterName); - CreateFullPathName(szCopyPath, NULL, szMirrorName); - remove(szCopyPath + FileStream_PrefixA(szCopyPath, NULL)); - - // Copy the mirrored file from the source to the work directory - if(bCopyMirrorFile) - nError = CreateFileCopy(pLogger, szMirrorName, szMirrorName, NULL); - - // Create the mirror*master path - if(nError == ERROR_SUCCESS) - sprintf(szMirrorPath, "%s*%s", szCopyPath, szMasterPath); - - return nError; -} - -static void WINAPI AddFileCallback(void * pvUserData, DWORD dwBytesWritten, DWORD dwTotalBytes, bool bFinalCall) -{ - TLogHelper * pLogger = (TLogHelper *)pvUserData; - - // Keep compiler happy - bFinalCall = bFinalCall; - - pLogger->PrintProgress("Adding file (%s) (%u of %u) (%u of %u) ...", pLogger->UserString, - pLogger->UserCount, - pLogger->UserTotal, - dwBytesWritten, - dwTotalBytes); -} - -static void WINAPI CompactCallback(void * pvUserData, DWORD dwWork, ULONGLONG BytesDone, ULONGLONG TotalBytes) -{ - TLogHelper * pLogger = (TLogHelper *)pvUserData; - const char * szWork = NULL; - - switch(dwWork) - { - case CCB_CHECKING_FILES: - szWork = "Checking files in archive"; - break; - - case CCB_CHECKING_HASH_TABLE: - szWork = "Checking hash table"; - break; - - case CCB_COPYING_NON_MPQ_DATA: - szWork = "Copying non-MPQ data"; - break; - - case CCB_COMPACTING_FILES: - szWork = "Compacting files"; - break; - - case CCB_CLOSING_ARCHIVE: - szWork = "Closing archive"; - break; - } - - if(szWork != NULL) - { - if(pLogger != NULL) - pLogger->PrintProgress("%s (%I64u of %I64u) ...", szWork, BytesDone, TotalBytes); - else - printf("%s (" I64u_a " of " I64u_a ") ... \r", szWork, BytesDone, TotalBytes); - } -} - -//----------------------------------------------------------------------------- -// MPQ file utilities - -#define TEST_FLAG_LOAD_FILES 0x00000001 // Test function should load all files in the MPQ -#define TEST_FLAG_HASH_FILES 0x00000002 // Test function should load all files in the MPQ -#define TEST_FLAG_PLAY_WAVES 0x00000004 // Play extracted WAVE files -#define TEST_FLAG_MOST_PATCHED 0x00000008 // Find the most patched file - -struct TFileData -{ - DWORD dwBlockIndex; - DWORD dwFileSize; - DWORD dwFlags; - DWORD dwReserved; // Alignment - BYTE FileData[1]; -}; - -static bool CheckIfFileIsPresent(TLogHelper * pLogger, HANDLE hMpq, const char * szFileName, bool bShouldExist) -{ - HANDLE hFile = NULL; - - if(SFileOpenFileEx(hMpq, szFileName, 0, &hFile)) - { - if(bShouldExist == false) - pLogger->PrintMessage("The file %s is present, but it should not be", szFileName); - SFileCloseFile(hFile); - return true; - } - else - { - if(bShouldExist) - pLogger->PrintMessage("The file %s is not present, but it should be", szFileName); - return false; - } -} - -static TFileData * LoadLocalFile(TLogHelper * pLogger, const char * szFileName, bool bMustSucceed) -{ - TFileStream * pStream; - TFileData * pFileData = NULL; - ULONGLONG FileSize = 0; - size_t nAllocateBytes; - - // Notify the user - if(pLogger != NULL) - pLogger->PrintProgress("Loading local file ..."); - - // Attempt to open the file - pStream = FileStream_OpenFileA(szFileName, STREAM_FLAG_READ_ONLY); - if(pStream == NULL) - { - if(pLogger != NULL && bMustSucceed == true) - pLogger->PrintError("Failed to open the file %s", szFileName); - return NULL; - } - - // Verify the size - FileStream_GetSize(pStream, &FileSize); - if((FileSize >> 0x20) == 0) - { - // Allocate space for the file - nAllocateBytes = sizeof(TFileData) + (size_t)FileSize; - pFileData = (TFileData *)STORM_ALLOC(BYTE, nAllocateBytes); - if(pFileData != NULL) - { - // Make sure it;s properly zeroed - memset(pFileData, 0, nAllocateBytes); - pFileData->dwFileSize = (DWORD)FileSize; - - // Load to memory - if(!FileStream_Read(pStream, NULL, pFileData->FileData, pFileData->dwFileSize)) - { - STORM_FREE(pFileData); - pFileData = NULL; - } - } - } - - FileStream_Close(pStream); - return pFileData; -} - -static int CompareTwoLocalFilesRR( - TLogHelper * pLogger, - TFileStream * pStream1, // Master file - TFileStream * pStream2, // Mirror file - int nIterations) // Number of iterations -{ - ULONGLONG RandomNumber = 0x12345678; // We need pseudo-random number that will repeat each run of the program - ULONGLONG RandomSeed; - ULONGLONG ByteOffset; - ULONGLONG FileSize1 = 1; - ULONGLONG FileSize2 = 2; - DWORD BytesToRead; - DWORD Difference; - LPBYTE pbBuffer1; - LPBYTE pbBuffer2; - DWORD cbBuffer = 0x100000; - int nError = ERROR_SUCCESS; - - // Compare file sizes - FileStream_GetSize(pStream1, &FileSize1); - FileStream_GetSize(pStream2, &FileSize2); - if(FileSize1 != FileSize2) - { - pLogger->PrintMessage("The files have different size"); - return ERROR_CAN_NOT_COMPLETE; - } - - // Allocate both buffers - pbBuffer1 = STORM_ALLOC(BYTE, cbBuffer); - pbBuffer2 = STORM_ALLOC(BYTE, cbBuffer); - if(pbBuffer1 && pbBuffer2) - { - // Perform many random reads - for(int i = 0; i < nIterations; i++) - { - // Generate psudo-random offsrt and data size - ByteOffset = (RandomNumber % FileSize1); - BytesToRead = (DWORD)(RandomNumber % cbBuffer); - - // Show the progress message - pLogger->PrintProgress("Comparing file: Offset: " I64u_a ", Length: %u", ByteOffset, BytesToRead); - - // Only perform read if the byte offset is below - if(ByteOffset < FileSize1) - { - if((ByteOffset + BytesToRead) > FileSize1) - BytesToRead = (DWORD)(FileSize1 - ByteOffset); - - memset(pbBuffer1, 0xEE, cbBuffer); - memset(pbBuffer2, 0xAA, cbBuffer); - - FileStream_Read(pStream1, &ByteOffset, pbBuffer1, BytesToRead); - FileStream_Read(pStream2, &ByteOffset, pbBuffer2, BytesToRead); - - if(!CompareBlocks(pbBuffer1, pbBuffer2, BytesToRead, &Difference)) - { - pLogger->PrintMessage("Difference at %u (Offset " I64X_a ", Length %X)", Difference, ByteOffset, BytesToRead); - nError = ERROR_FILE_CORRUPT; - break; - } - - // Shuffle the random number - memcpy(&RandomSeed, pbBuffer1, sizeof(RandomSeed)); - RandomNumber = ((RandomNumber >> 0x11) | (RandomNumber << 0x29)) ^ (RandomNumber + RandomSeed); - } - } - } - - // Free both buffers - if(pbBuffer2 != NULL) - STORM_FREE(pbBuffer2); - if(pbBuffer1 != NULL) - STORM_FREE(pbBuffer1); - return nError; -} - -static TFileData * LoadMpqFile(TLogHelper * pLogger, HANDLE hMpq, const char * szFileName) -{ - TFileData * pFileData = NULL; - HANDLE hFile; - DWORD dwFileSizeHi = 0xCCCCCCCC; - DWORD dwFileSizeLo = 0; - DWORD dwBytesRead; - int nError = ERROR_SUCCESS; - - // Notify the user that we are loading a file from MPQ - pLogger->PrintProgress("Loading file %s ...", GetShortPlainName(szFileName)); - -#if defined(_MSC_VER) && defined(_DEBUG) -// if(!_stricmp(szFileName, "manifest-cards.csv")) -// DebugBreak(); -#endif - - // Open the file from MPQ - if(!SFileOpenFileEx(hMpq, szFileName, 0, &hFile)) - nError = pLogger->PrintError("Failed to open the file %s", szFileName); - - // Get the size of the file - if(nError == ERROR_SUCCESS) - { - dwFileSizeLo = SFileGetFileSize(hFile, &dwFileSizeHi); - if(dwFileSizeLo == SFILE_INVALID_SIZE || dwFileSizeHi != 0) - nError = pLogger->PrintError("Failed to query the file size"); - } - - // Spazzler protector: Creates fake files with size of 0x7FFFE7CA - if(nError == ERROR_SUCCESS) - { - if(dwFileSizeLo > 0x1FFFFFFF) - nError = ERROR_FILE_CORRUPT; - } - - // Allocate buffer for the file content - if(nError == ERROR_SUCCESS) - { - pFileData = (TFileData *)STORM_ALLOC(BYTE, sizeof(TFileData) + dwFileSizeLo); - if(pFileData == NULL) - { - pLogger->PrintError("Failed to allocate buffer for the file content"); - nError = ERROR_NOT_ENOUGH_MEMORY; - } - } - - // get the file index of the MPQ file - if(nError == ERROR_SUCCESS) - { - // Store the file size - memset(pFileData, 0, sizeof(TFileData) + dwFileSizeLo); - pFileData->dwFileSize = dwFileSizeLo; - - // Retrieve the block index and file flags - if(!SFileGetFileInfo(hFile, SFileInfoFileIndex, &pFileData->dwBlockIndex, sizeof(DWORD), NULL)) - nError = pLogger->PrintError("Failed retrieve the file index of %s", szFileName); - if(!SFileGetFileInfo(hFile, SFileInfoFlags, &pFileData->dwFlags, sizeof(DWORD), NULL)) - nError = pLogger->PrintError("Failed retrieve the file flags of %s", szFileName); - } - - // Load the entire file - if(nError == ERROR_SUCCESS) - { - // Read the file data - SFileReadFile(hFile, pFileData->FileData, dwFileSizeLo, &dwBytesRead, NULL); - if(dwBytesRead != dwFileSizeLo) - nError = pLogger->PrintError("Failed to read the content of the file %s", szFileName); - } - - // If failed, free the buffer - if(nError != ERROR_SUCCESS) - { - STORM_FREE(pFileData); - SetLastError(nError); - pFileData = NULL; - } - - // Close the file and return what we got - if(hFile != NULL) - SFileCloseFile(hFile); - return pFileData; -} - -static bool CompareTwoFiles(TLogHelper * pLogger, TFileData * pFileData1, TFileData * pFileData2) -{ - // Compare the file size - if(pFileData1->dwFileSize != pFileData2->dwFileSize) - { - pLogger->PrintErrorVa(_T("The files have different size (%u vs %u)"), pFileData1->dwFileSize, pFileData2->dwFileSize); - SetLastError(ERROR_FILE_CORRUPT); - return false; - } - - // Compare the files - for(DWORD i = 0; i < pFileData1->dwFileSize; i++) - { - if(pFileData1->FileData[i] != pFileData2->FileData[i]) - { - pLogger->PrintErrorVa(_T("Files are different at offset %08X"), i); - SetLastError(ERROR_FILE_CORRUPT); - return false; - } - } - - // The files are identical - return true; -} - -static int SearchArchive( - TLogHelper * pLogger, - HANDLE hMpq, - DWORD dwTestFlags = 0, - DWORD * pdwFileCount = NULL, - LPBYTE pbFileHash = NULL) -{ - SFILE_FIND_DATA sf; - TFileData * pFileData; - HANDLE hFind; - DWORD dwFileCount = 0; - hash_state md5state; - char szMostPatched[MAX_PATH] = ""; - char szListFile[MAX_PATH]; - bool bFound = true; - int nMaxPatchCount = 0; - int nPatchCount = 0; - int nError = ERROR_SUCCESS; - - // Construct the full name of the listfile - CreateFullPathName(szListFile, szMpqSubDir, "ListFile_Blizzard.txt"); - - // Prepare hashing - md5_init(&md5state); - - // Initiate the MPQ search - pLogger->PrintProgress("Searching the archive ..."); - hFind = SFileFindFirstFile(hMpq, "*", &sf, szListFile); - if(hFind == NULL) - { - nError = GetLastError(); - nError = (nError == ERROR_NO_MORE_FILES) ? ERROR_SUCCESS : nError; - return nError; - } - - // Perform the search - while(bFound == true) - { - // Increment number of files - dwFileCount++; - -// if(!_stricmp(sf.cFileName, "Interface\\Glues\\CREDITS\\1024px-Blade3_final2.blp")) -// DebugBreak(); - - if(dwTestFlags & TEST_FLAG_MOST_PATCHED) - { - // Load the patch count - nPatchCount = GetFilePatchCount(pLogger, hMpq, sf.cFileName); - - // Check if it's greater than maximum - if(nPatchCount > nMaxPatchCount) - { - strcpy(szMostPatched, sf.cFileName); - nMaxPatchCount = nPatchCount; - } - } - - // Load the file to memory, if required - if(dwTestFlags & TEST_FLAG_LOAD_FILES) - { - // Load the entire file to the MPQ - pFileData = LoadMpqFile(pLogger, hMpq, sf.cFileName); - if(pFileData != NULL) - { - // Hash the file data, if needed - if((dwTestFlags & TEST_FLAG_HASH_FILES) && !IsInternalMpqFileName(sf.cFileName)) - md5_process(&md5state, pFileData->FileData, pFileData->dwFileSize); - - // Play sound files, if required - if((dwTestFlags & TEST_FLAG_PLAY_WAVES) && strstr(sf.cFileName, ".wav") != NULL) - { -#ifdef _MSC_VER - pLogger->PrintProgress("Playing sound %s", sf.cFileName); - PlaySound((LPCTSTR)pFileData->FileData, NULL, SND_MEMORY); -#endif - } - - STORM_FREE(pFileData); - } - } - - bFound = SFileFindNextFile(hFind, &sf); - } - SFileFindClose(hFind); - - // Give the file count, if required - if(pdwFileCount != NULL) - pdwFileCount[0] = dwFileCount; - - // Give the hash, if required - if(pbFileHash != NULL && (dwTestFlags & TEST_FLAG_HASH_FILES)) - md5_done(&md5state, pbFileHash); - - return nError; -} - -static int CreateNewArchive(TLogHelper * pLogger, const char * szPlainName, DWORD dwCreateFlags, DWORD dwMaxFileCount, HANDLE * phMpq) -{ - HANDLE hMpq = NULL; - TCHAR szMpqName[MAX_PATH]; - char szFullPath[MAX_PATH]; - - // Make sure that the MPQ is deleted - CreateFullPathName(szFullPath, NULL, szPlainName); - remove(szFullPath); - - // Create the new MPQ - CopyFileName(szMpqName, szFullPath, strlen(szFullPath)); - if(!SFileCreateArchive(szMpqName, dwCreateFlags, dwMaxFileCount, &hMpq)) - return pLogger->PrintError(_T("Failed to create archive %s"), szMpqName); - - // Shall we close it right away? - if(phMpq == NULL) - SFileCloseArchive(hMpq); - else - *phMpq = hMpq; - - return ERROR_SUCCESS; -} - -static int CreateNewArchive_V2(TLogHelper * pLogger, const char * szPlainName, DWORD dwCreateFlags, DWORD dwMaxFileCount, HANDLE * phMpq) -{ - SFILE_CREATE_MPQ CreateInfo; - HANDLE hMpq = NULL; - TCHAR szMpqName[MAX_PATH]; - char szFullPath[MAX_PATH]; - - // Make sure that the MPQ is deleted - CreateFullPathName(szFullPath, NULL, szPlainName); - CopyFileName(szMpqName, szFullPath, strlen(szFullPath)); - remove(szFullPath); - - // Fill the create structure - memset(&CreateInfo, 0, sizeof(SFILE_CREATE_MPQ)); - CreateInfo.cbSize = sizeof(SFILE_CREATE_MPQ); - CreateInfo.dwMpqVersion = (dwCreateFlags & MPQ_CREATE_ARCHIVE_VMASK) >> FLAGS_TO_FORMAT_SHIFT; - CreateInfo.dwStreamFlags = STREAM_PROVIDER_FLAT | BASE_PROVIDER_FILE; - CreateInfo.dwFileFlags1 = (dwCreateFlags & MPQ_CREATE_LISTFILE) ? MPQ_FILE_EXISTS : 0; - CreateInfo.dwFileFlags2 = (dwCreateFlags & MPQ_CREATE_ATTRIBUTES) ? MPQ_FILE_EXISTS : 0; - CreateInfo.dwFileFlags3 = (dwCreateFlags & MPQ_CREATE_SIGNATURE) ? MPQ_FILE_EXISTS : 0; - CreateInfo.dwAttrFlags = (dwCreateFlags & MPQ_CREATE_ATTRIBUTES) ? (MPQ_ATTRIBUTE_CRC32 | MPQ_ATTRIBUTE_FILETIME | MPQ_ATTRIBUTE_MD5) : 0; - CreateInfo.dwSectorSize = (CreateInfo.dwMpqVersion >= MPQ_FORMAT_VERSION_3) ? 0x4000 : 0x1000; - CreateInfo.dwRawChunkSize = (CreateInfo.dwMpqVersion >= MPQ_FORMAT_VERSION_4) ? 0x4000 : 0; - CreateInfo.dwMaxFileCount = dwMaxFileCount; - - // Create the new MPQ - if(!SFileCreateArchive2(szMpqName, &CreateInfo, &hMpq)) - return pLogger->PrintError(_T("Failed to create archive %s"), szMpqName); - - // Shall we close it right away? - if(phMpq == NULL) - SFileCloseArchive(hMpq); - else - *phMpq = hMpq; - - return ERROR_SUCCESS; -} - -// Creates new archive with UNICODE name. Adds prefix to the name -static int CreateNewArchiveU(TLogHelper * pLogger, const wchar_t * szPlainName, DWORD dwCreateFlags, DWORD dwMaxFileCount) -{ -#ifdef _UNICODE - HANDLE hMpq = NULL; - TCHAR szMpqName[MAX_PATH+1]; - char szFullPath[MAX_PATH]; - - // Construct the full UNICODE name - CreateFullPathName(szFullPath, NULL, "StormLibTest_"); - CopyFileName(szMpqName, szFullPath, strlen(szFullPath)); - StringCatT(szMpqName, szPlainName, MAX_PATH); - - // Make sure that the MPQ is deleted - _tremove(szMpqName); - - // Create the archive - pLogger->PrintProgress("Creating new archive with UNICODE name ..."); - if(!SFileCreateArchive(szMpqName, dwCreateFlags, dwMaxFileCount, &hMpq)) - return pLogger->PrintError(_T("Failed to create archive %s"), szMpqName); - - SFileCloseArchive(hMpq); -#else - pLogger = pLogger; - szPlainName = szPlainName; - dwCreateFlags = dwCreateFlags; - dwMaxFileCount = dwMaxFileCount; -#endif - return ERROR_SUCCESS; -} - -static int OpenExistingArchive(TLogHelper * pLogger, const char * szFullPath, DWORD dwFlags, HANDLE * phMpq) -{ - HANDLE hMpq = NULL; - TCHAR szMpqName[MAX_PATH]; -// bool bReopenResult; - int nError = ERROR_SUCCESS; - - // Is it an encrypted MPQ ? - if(strstr(szFullPath, ".MPQE") != NULL) - dwFlags |= STREAM_PROVIDER_MPQE; - if(strstr(szFullPath, ".MPQ.part") != NULL) - dwFlags |= STREAM_PROVIDER_PARTIAL; - if(strstr(szFullPath, ".mpq.part") != NULL) - dwFlags |= STREAM_PROVIDER_PARTIAL; - if(strstr(szFullPath, ".MPQ.0") != NULL) - dwFlags |= STREAM_PROVIDER_BLOCK4; - - // Open the copied archive - pLogger->PrintProgress("Opening archive %s ...", GetShortPlainName(szFullPath)); - CopyFileName(szMpqName, szFullPath, strlen(szFullPath)); - if(!SFileOpenArchive(szMpqName, 0, dwFlags, &hMpq)) - { - switch(nError = GetLastError()) - { -// case ERROR_BAD_FORMAT: // If the error is ERROR_BAD_FORMAT, try to open with MPQ_OPEN_FORCE_MPQ_V1 -// bReopenResult = SFileOpenArchive(szMpqName, 0, dwFlags | MPQ_OPEN_FORCE_MPQ_V1, &hMpq); -// nError = (bReopenResult == false) ? GetLastError() : ERROR_SUCCESS; -// break; - - case ERROR_AVI_FILE: // Ignore the error if it's an AVI file or if the file is incomplete - case ERROR_FILE_INCOMPLETE: - return nError; - } - - // Show the open error to the user - return pLogger->PrintError("Failed to open archive %s", szFullPath); - } - - // Store the archive handle or close the archive - if(phMpq == NULL) - SFileCloseArchive(hMpq); - else - *phMpq = hMpq; - return nError; -} - -static int OpenPatchArchive(TLogHelper * pLogger, HANDLE hMpq, const char * szFullPath) -{ - TCHAR szPatchName[MAX_PATH]; - int nError = ERROR_SUCCESS; - - pLogger->PrintProgress("Adding patch %s ...", GetShortPlainName(szFullPath)); - CopyFileName(szPatchName, szFullPath, strlen(szFullPath)); - if(!SFileOpenPatchArchive(hMpq, szPatchName, NULL, 0)) - nError = pLogger->PrintError("Failed to add patch %s ...", szFullPath); - - return nError; -} - -static int OpenExistingArchiveWithCopy(TLogHelper * pLogger, const char * szFileName, const char * szCopyName, HANDLE * phMpq) -{ - DWORD dwFlags = 0; - char szFullPath[MAX_PATH]; - int nError = ERROR_SUCCESS; - - // We expect MPQ directory to be already prepared by InitializeMpqDirectory - assert(szMpqDirectory[0] != 0); - - // At least one name must be entered - assert(szFileName != NULL || szCopyName != NULL); - - // If both names entered, create a copy - if(szFileName != NULL && szCopyName != NULL) - { - nError = CreateFileCopy(pLogger, szFileName, szCopyName, szFullPath); - if(nError != ERROR_SUCCESS) - return nError; - } - - // If only source name entered, open it for read-only access - else if(szFileName != NULL && szCopyName == NULL) - { - CreateFullPathName(szFullPath, szMpqSubDir, szFileName); - dwFlags |= MPQ_OPEN_READ_ONLY; - } - - // If only target name entered, open it directly - else if(szFileName == NULL && szCopyName != NULL) - { - CreateFullPathName(szFullPath, NULL, szCopyName); - } - - // Open the archive - return OpenExistingArchive(pLogger, szFullPath, dwFlags, phMpq); -} - -static int OpenPatchedArchive(TLogHelper * pLogger, HANDLE * phMpq, const char * PatchList[]) -{ - HANDLE hMpq = NULL; - char szFullPath[MAX_PATH]; - int nError = ERROR_SUCCESS; - - // The first file is expected to be valid - assert(PatchList[0] != NULL); - - // Open the primary MPQ - CreateFullPathName(szFullPath, szMpqSubDir, PatchList[0]); - nError = OpenExistingArchive(pLogger, szFullPath, MPQ_OPEN_READ_ONLY, &hMpq); - - // Add all patches - if(nError == ERROR_SUCCESS) - { - for(size_t i = 1; PatchList[i] != NULL; i++) - { - CreateFullPathName(szFullPath, szMpqPatchDir, PatchList[i]); - nError = OpenPatchArchive(pLogger, hMpq, szFullPath); - if(nError != ERROR_SUCCESS) - break; - } - } - - // Store the archive handle or close the archive - if(phMpq == NULL) - SFileCloseArchive(hMpq); - else - *phMpq = hMpq; - return nError; -} - -static int AddFileToMpq( - TLogHelper * pLogger, - HANDLE hMpq, - const char * szFileName, - const char * szFileData, - DWORD dwFlags = 0, - DWORD dwCompression = 0, - int nExpectedError = ERROR_SUCCESS) -{ - HANDLE hFile = NULL; - DWORD dwFileSize = (DWORD)strlen(szFileData); - int nError = ERROR_SUCCESS; - - // Notify the user - pLogger->PrintProgress("Adding file %s ...", szFileName); - - // Get the default flags - if(dwFlags == 0) - dwFlags = MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED; - if(dwCompression == 0) - dwCompression = MPQ_COMPRESSION_ZLIB; - - // Create the file within the MPQ - if(SFileCreateFile(hMpq, szFileName, 0, dwFileSize, 0, dwFlags, &hFile)) - { - // Write the file - if(!SFileWriteFile(hFile, szFileData, dwFileSize, dwCompression)) - nError = pLogger->PrintError("Failed to write data to the MPQ"); - SFileCloseFile(hFile); - } - else - { - nError = GetLastError(); - } - - // Check the expected error code - if(nExpectedError != ERROR_UNDETERMINED_RESULT && nError != nExpectedError) - return pLogger->PrintError("Unexpected result from SFileCreateFile(%s)", szFileName); - return nError; -} - -static int AddLocalFileToMpq( - TLogHelper * pLogger, - HANDLE hMpq, - const char * szArchivedName, - const char * szLocalFileName, - DWORD dwFlags = 0, - DWORD dwCompression = 0, - bool bMustSucceed = false) -{ - TCHAR szFileName[MAX_PATH]; - DWORD dwVerifyResult; - - // Notify the user - pLogger->PrintProgress("Adding file %s (%u of %u)...", GetShortPlainName(szLocalFileName), pLogger->UserCount, pLogger->UserTotal); - pLogger->UserString = szArchivedName; - - // Get the default flags - if(dwFlags == 0) - dwFlags = MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED; - if(dwCompression == 0) - dwCompression = MPQ_COMPRESSION_ZLIB; - - // Set the notification callback - SFileSetAddFileCallback(hMpq, AddFileCallback, pLogger); - - // Add the file to the MPQ - CopyFileName(szFileName, szLocalFileName, strlen(szLocalFileName)); - if(!SFileAddFileEx(hMpq, szFileName, szArchivedName, dwFlags, dwCompression, MPQ_COMPRESSION_NEXT_SAME)) - { - if(bMustSucceed) - return pLogger->PrintError("Failed to add the file %s", szArchivedName); - return GetLastError(); - } - - // Verify the file unless it was lossy compression - if((dwCompression & (MPQ_COMPRESSION_ADPCM_MONO | MPQ_COMPRESSION_ADPCM_STEREO)) == 0) - { - // Notify the user - pLogger->PrintProgress("Verifying file %s (%u of %u) ...", szArchivedName, pLogger->UserCount, pLogger->UserTotal); - - // Perform the verification - dwVerifyResult = SFileVerifyFile(hMpq, szArchivedName, MPQ_ATTRIBUTE_CRC32 | MPQ_ATTRIBUTE_MD5); - if(dwVerifyResult & (VERIFY_OPEN_ERROR | VERIFY_READ_ERROR | VERIFY_FILE_SECTOR_CRC_ERROR | VERIFY_FILE_CHECKSUM_ERROR | VERIFY_FILE_MD5_ERROR)) - return pLogger->PrintError("CRC error on %s", szArchivedName); - } - - return ERROR_SUCCESS; -} - -static int RenameMpqFile(TLogHelper * pLogger, HANDLE hMpq, const char * szOldFileName, const char * szNewFileName, int nExpectedError) -{ - int nError = ERROR_SUCCESS; - - // Notify the user - pLogger->PrintProgress("Renaming %s to %s ...", szOldFileName, szNewFileName); - - // Perform the deletion - if(!SFileRenameFile(hMpq, szOldFileName, szNewFileName)) - nError = GetLastError(); - - if(nError != nExpectedError) - return pLogger->PrintErrorVa("Unexpected result from SFileRenameFile(%s -> %s)", szOldFileName, szNewFileName); - return ERROR_SUCCESS; -} - -static int RemoveMpqFile(TLogHelper * pLogger, HANDLE hMpq, const char * szFileName, int nExpectedError) -{ - int nError = ERROR_SUCCESS; - - // Notify the user - pLogger->PrintProgress("Removing file %s ...", szFileName); - - // Perform the deletion - if(!SFileRemoveFile(hMpq, szFileName, 0)) - nError = GetLastError(); - - if(nError != nExpectedError) - return pLogger->PrintError("Unexpected result from SFileRemoveFile(%s)", szFileName); - return ERROR_SUCCESS; -} - -//----------------------------------------------------------------------------- -// Tests - -static void TestGetFileInfo( - TLogHelper * pLogger, - HANDLE hMpqOrFile, - SFileInfoClass InfoClass, - void * pvFileInfo, - DWORD cbFileInfo, - DWORD * pcbLengthNeeded, - bool bExpectedResult, - int nExpectedError) -{ - bool bResult; - int nError = ERROR_SUCCESS; - - // Call the get file info - bResult = SFileGetFileInfo(hMpqOrFile, InfoClass, pvFileInfo, cbFileInfo, pcbLengthNeeded); - if(!bResult) - nError = GetLastError(); - - if(bResult != bExpectedResult) - pLogger->PrintMessage("Different result of SFileGetFileInfo."); - if(nError != nExpectedError) - pLogger->PrintMessage("Different error from SFileGetFileInfo (expected %u, returned %u)", nExpectedError, nError); -} - -// StormLib is able to open local files (as well as the original Storm.dll) -// I want to keep this for occasional use -static int TestOpenLocalFile(const char * szPlainName) -{ - TLogHelper Logger("OpenLocalFile", szPlainName); - HANDLE hFile; - DWORD dwFileSizeHi = 0; - DWORD dwFileSizeLo = 0; - char szFileName1[MAX_PATH]; - char szFileName2[MAX_PATH]; - char szFileLine[0x40]; - - CreateFullPathName(szFileName1, szMpqSubDir, szPlainName); - if(SFileOpenFileEx(NULL, szFileName1, SFILE_OPEN_LOCAL_FILE, &hFile)) - { - // Retrieve the file name. It must match the name under which the file was open - SFileGetFileName(hFile, szFileName2); - if(strcmp(szFileName2, szFileName1)) - Logger.PrintMessage("The retrieved name does not match the open name"); - - // Retrieve the file size - dwFileSizeLo = SFileGetFileSize(hFile, &dwFileSizeHi); - if(dwFileSizeHi != 0 || dwFileSizeLo != 3904784) - Logger.PrintMessage("Local file size mismatch"); - - // Read the first line - memset(szFileLine, 0, sizeof(szFileLine)); - SFileReadFile(hFile, szFileLine, 18, NULL, NULL); - if(strcmp(szFileLine, "(1)Enslavers01.scm")) - Logger.PrintMessage("Content of the listfile does not match"); - - SFileCloseFile(hFile); - } - - return ERROR_SUCCESS; -} - -static int TestSearchListFile(const char * szPlainName) -{ - SFILE_FIND_DATA sf; - TLogHelper Logger("SearchListFile", szPlainName); - HANDLE hFind; - char szFullPath[MAX_PATH]; - int nFileCount = 0; - - CreateFullPathName(szFullPath, szMpqSubDir, szPlainName); - hFind = SListFileFindFirstFile(NULL, szFullPath, "*", &sf); - if(hFind != NULL) - { - for(;;) - { - Logger.PrintProgress("Found file (%04u): %s", nFileCount++, GetShortPlainName(sf.cFileName)); - if(!SListFileFindNextFile(hFind, &sf)) - break; - } - - SListFileFindClose(hFind); - } - return ERROR_SUCCESS; -} - -static void WINAPI TestReadFile_DownloadCallback( - void * UserData, - ULONGLONG ByteOffset, - DWORD DataLength) -{ - TLogHelper * pLogger = (TLogHelper *)UserData; - - if(ByteOffset != 0 && DataLength != 0) - pLogger->PrintProgress("Downloading data (offset: " I64X_a ", length: %X)", ByteOffset, DataLength); - else - pLogger->PrintProgress("Download complete."); -} - -// Open a file stream with mirroring a master file -static int TestReadFile_MasterMirror(const char * szMirrorName, const char * szMasterName, bool bCopyMirrorFile) -{ - TFileStream * pStream1; // Master file - TFileStream * pStream2; // Mirror file - TLogHelper Logger("OpenMirrorFile", szMirrorName); - DWORD dwProvider = 0; - char szMirrorPath[MAX_PATH + MAX_PATH]; - char szMasterPath[MAX_PATH]; - int nIterations = 0x10000; - int nError; - - // Retrieve the provider - FileStream_PrefixA(szMasterName, &dwProvider); - -#ifndef PLATFORM_WINDOWS - if((dwProvider & BASE_PROVIDER_MASK) == BASE_PROVIDER_HTTP) - return ERROR_SUCCESS; -#endif - - // Create copy of the file to serve as mirror, keep master there - nError = CreateMasterAndMirrorPaths(&Logger, szMirrorPath, szMasterPath, szMirrorName, szMasterName, bCopyMirrorFile); - if(nError == ERROR_SUCCESS) - { - // Open both master and mirror file - pStream1 = FileStream_OpenFileA(szMasterPath, STREAM_FLAG_READ_ONLY); - pStream2 = FileStream_OpenFileA(szMirrorPath, STREAM_FLAG_READ_ONLY | STREAM_FLAG_USE_BITMAP); - if(pStream1 && pStream2) - { - // For internet based files, we limit the number of operations - if((dwProvider & BASE_PROVIDER_MASK) == BASE_PROVIDER_HTTP) - nIterations = 0x80; - - FileStream_SetCallback(pStream2, TestReadFile_DownloadCallback, &Logger); - nError = CompareTwoLocalFilesRR(&Logger, pStream1, pStream2, nIterations); - } - - if(pStream2 != NULL) - FileStream_Close(pStream2); - if(pStream1 != NULL) - FileStream_Close(pStream1); - } - - return nError; -} - -// Test of the TFileStream object -static int TestSparseCompression() -{ - BYTE InpBuffer[0x1000]; - BYTE Compressed[0x1000]; - BYTE Decompressed[0x1000]; - int cbCompressed = sizeof(Compressed); - int cbDecompressed = sizeof(Compressed); - - // Prepare compressed buffer - memset(InpBuffer, 0, sizeof(InpBuffer)); - - // Compress and decompress - CompressSparse(Compressed, &cbCompressed, InpBuffer, sizeof(InpBuffer)); - DecompressSparse(Decompressed, &cbDecompressed, Compressed, cbCompressed); - - // Check the result of decompression - if(cbDecompressed != sizeof(InpBuffer)) - return ERROR_FILE_CORRUPT; - if(memcmp(Decompressed, InpBuffer, sizeof(InpBuffer))) - return ERROR_FILE_CORRUPT; - - return ERROR_SUCCESS; -} - -// Test of the TFileStream object -static int TestFileStreamOperations(const char * szPlainName, DWORD dwStreamFlags) -{ - TFileStream * pStream = NULL; - TLogHelper Logger("FileStreamTest", szPlainName); - ULONGLONG ByteOffset; - ULONGLONG FileSize = 0; - DWORD dwRequiredFlags = 0; - char szFullPath[MAX_PATH]; - BYTE Buffer[0x10]; - int nError = ERROR_SUCCESS; - - // Copy the file so we won't screw up - if((dwStreamFlags & STREAM_PROVIDER_MASK) == STREAM_PROVIDER_BLOCK4) - CreateFullPathName(szFullPath, szMpqSubDir, szPlainName); - else - nError = CreateFileCopy(&Logger, szPlainName, szPlainName, szFullPath); - - // Open the file stream - if(nError == ERROR_SUCCESS) - { - pStream = FileStream_OpenFileA(szFullPath, dwStreamFlags); - if(pStream == NULL) - return Logger.PrintError("Failed to open %s", szFullPath); - } - - // Get the size of the file stream - if(nError == ERROR_SUCCESS) - { - if(!FileStream_GetFlags(pStream, &dwStreamFlags)) - nError = Logger.PrintError("Failed to retrieve the stream flags"); - - if(!FileStream_GetSize(pStream, &FileSize)) - nError = Logger.PrintError("Failed to retrieve the file size"); - - // Any other stream except STREAM_PROVIDER_FLAT | BASE_PROVIDER_FILE should be read-only - if((dwStreamFlags & STREAM_PROVIDERS_MASK) != (STREAM_PROVIDER_FLAT | BASE_PROVIDER_FILE)) - dwRequiredFlags |= STREAM_FLAG_READ_ONLY; -// if(pStream->BlockPresent) -// dwRequiredFlags |= STREAM_FLAG_READ_ONLY; - - // Check the flags there - if((dwStreamFlags & dwRequiredFlags) != dwRequiredFlags) - { - Logger.PrintMessage("The stream should be read-only but it isn't"); - nError = ERROR_FILE_CORRUPT; - } - } - - // After successful open, the stream position must be zero - if(nError == ERROR_SUCCESS) - nError = VerifyFilePosition(&Logger, pStream, 0); - - // Read the MPQ header from the current file offset. - if(nError == ERROR_SUCCESS) - nError = VerifyFileMpqHeader(&Logger, pStream, NULL); - - // After successful open, the stream position must sizeof(TMPQHeader) - if(nError == ERROR_SUCCESS) - nError = VerifyFilePosition(&Logger, pStream, sizeof(TMPQHeader)); - - // Now try to read the MPQ header from the offset 0 - if(nError == ERROR_SUCCESS) - { - ByteOffset = 0; - nError = VerifyFileMpqHeader(&Logger, pStream, &ByteOffset); - } - - // After successful open, the stream position must sizeof(TMPQHeader) - if(nError == ERROR_SUCCESS) - nError = VerifyFilePosition(&Logger, pStream, sizeof(TMPQHeader)); - - // Try a write operation - if(nError == ERROR_SUCCESS) - { - bool bExpectedResult = (dwStreamFlags & STREAM_FLAG_READ_ONLY) ? false : true; - bool bResult; - - // Attempt to write to the file - ByteOffset = 0; - bResult = FileStream_Write(pStream, &ByteOffset, Buffer, sizeof(Buffer)); - - // If the result is not expected - if(bResult != bExpectedResult) - { - Logger.PrintMessage("FileStream_Write result is different than expected"); - nError = ERROR_FILE_CORRUPT; - } - } - - // Move the position 9 bytes from the end and try to read 10 bytes. - // This must fail, because stream reading functions are "all or nothing" - if(nError == ERROR_SUCCESS) - { - ByteOffset = FileSize - 9; - if(FileStream_Read(pStream, &ByteOffset, Buffer, 10)) - { - Logger.PrintMessage("FileStream_Read succeeded, but it shouldn't"); - nError = ERROR_FILE_CORRUPT; - } - } - - // Try again with 9 bytes. This must succeed, unless the file block is not available - if(nError == ERROR_SUCCESS) - { - ByteOffset = FileSize - 9; - if(!FileStream_Read(pStream, &ByteOffset, Buffer, 9)) - { - Logger.PrintMessage("FileStream_Read from the end of the file failed"); - nError = ERROR_FILE_CORRUPT; - } - } - - // Verify file position - it must be at the end of the file - if(nError == ERROR_SUCCESS) - nError = VerifyFilePosition(&Logger, pStream, FileSize); - - // Close the stream - if(pStream != NULL) - FileStream_Close(pStream); - return nError; -} - -static int TestOpenFile_OpenById(const char * szPlainName) -{ - TLogHelper Logger("OpenFileById", szPlainName); - TFileData * pFileData1 = NULL; - TFileData * pFileData2 = NULL; - HANDLE hMpq; - int nError; - - // Copy the archive so we won't fuck up the original one - nError = OpenExistingArchiveWithCopy(&Logger, szPlainName, NULL, &hMpq); - - // Now try to open a file without knowing the file name - if(nError == ERROR_SUCCESS) - { - // File00000023.xxx = music\dintro.wav - pFileData1 = LoadMpqFile(&Logger, hMpq, "File00000023.xxx"); - if(pFileData1 == NULL) - nError = Logger.PrintError("Failed to load the file %s", "File00000023.xxx"); - } - - // Now try to open the file again with its original name - if(nError == ERROR_SUCCESS) - { - // File00000023.xxx = music\dintro.wav - pFileData2 = LoadMpqFile(&Logger, hMpq, "music\\dintro.wav"); - if(pFileData2 == NULL) - nError = Logger.PrintError("Failed to load the file %s", "music\\dintro.wav"); - } - - // Now compare both files - if(nError == ERROR_SUCCESS) - { - if(!CompareTwoFiles(&Logger, pFileData1, pFileData1)) - nError = Logger.PrintError("The file has different size/content when open without name"); - } - - // Close the archive - if(pFileData2 != NULL) - STORM_FREE(pFileData2); - if(pFileData1 != NULL) - STORM_FREE(pFileData1); - if(hMpq != NULL) - SFileCloseArchive(hMpq); - return nError; -} - -static int TestOpenArchive(const char * szPlainName, const char * szListFile = NULL, bool bDontCopyArchive = false) -{ - TLogHelper Logger("OpenMpqTest", szPlainName); - TFileData * pFileData; - const char * szCopyName = (bDontCopyArchive) ? NULL : szPlainName; - HANDLE hMpq; - DWORD dwFileCount = 0; - DWORD dwTestFlags; - char szListFileBuff[MAX_PATH]; - bool bIsPartialMpq = false; - int nError; - - // If the file is a partial MPQ, don't load all files - bIsPartialMpq = (strstr(szPlainName, ".MPQ.part") != NULL); - - // Copy the archive so we won't fuck up the original one - nError = OpenExistingArchiveWithCopy(&Logger, szPlainName, szCopyName, &hMpq); - if(nError == ERROR_SUCCESS) - { - // If the listfile was given, add it to the MPQ - if(szListFile != NULL) - { - Logger.PrintProgress("Adding listfile %s ...", szListFile); - CreateFullPathName(szListFileBuff, szMpqSubDir, szListFile); - nError = SFileAddListFile(hMpq, szListFileBuff); - if(nError != ERROR_SUCCESS) - Logger.PrintMessage("Failed to add the listfile to the MPQ"); - } - - // Attempt to open the listfile and attributes - if(SFileHasFile(hMpq, LISTFILE_NAME)) - { - pFileData = LoadMpqFile(&Logger, hMpq, LISTFILE_NAME); - if(pFileData != NULL) - STORM_FREE(pFileData); - } - - // Attempt to open the listfile and attributes - if(SFileHasFile(hMpq, ATTRIBUTES_NAME)) - { - pFileData = LoadMpqFile(&Logger, hMpq, ATTRIBUTES_NAME); - if(pFileData != NULL) - STORM_FREE(pFileData); - } - - // Search the archive and load every file - dwTestFlags = bIsPartialMpq ? 0 : TEST_FLAG_LOAD_FILES; - nError = SearchArchive(&Logger, hMpq, dwTestFlags, &dwFileCount); - SFileCloseArchive(hMpq); - } - - return nError; -} - - -// Open an empty archive (found in WoW cache - it's just a header) -static int TestOpenArchive_WillFail(const char * szPlainName) -{ - TLogHelper Logger("FailMpqTest", szPlainName); - HANDLE hMpq = NULL; - TCHAR szMpqName[MAX_PATH]; - char szFullPath[MAX_PATH]; - - // Create the full path name for the archive - CreateFullPathName(szFullPath, szMpqSubDir, szPlainName); - CopyFileName(szMpqName, szFullPath, strlen(szFullPath)); - - // Try to open the archive. It is expected to fail - Logger.PrintProgress("Opening archive %s", szPlainName); - if(!SFileOpenArchive(szMpqName, 0, MPQ_OPEN_READ_ONLY, &hMpq)) - return ERROR_SUCCESS; - - Logger.PrintError("Archive %s succeeded to open, even if it should not.", szPlainName); - SFileCloseArchive(hMpq); - return ERROR_CAN_NOT_COMPLETE; -} - -static int TestOpenArchive_Corrupt(const char * szPlainName) -{ - TLogHelper Logger("OpenCorruptMpqTest", szPlainName); - HANDLE hMpq = NULL; - TCHAR szFullPathT[MAX_PATH]; - char szFullPath[MAX_PATH]; - - // Copy the archive so we won't fuck up the original one - CreateFullPathName(szFullPath, szMpqSubDir, szPlainName); - CopyFileName(szFullPathT, szFullPath, strlen(szFullPath)); - if(SFileOpenArchive(szFullPathT, 0, STREAM_FLAG_READ_ONLY, &hMpq)) - { - SFileCloseArchive(hMpq); - Logger.PrintMessage("Opening archive %s succeeded, but it shouldn't", szFullPath); - return ERROR_CAN_NOT_COMPLETE; - } - - return ERROR_SUCCESS; -} - - -// Opens a patched MPQ archive -static int TestOpenArchive_Patched(const char * PatchList[], const char * szPatchedFile, int nExpectedPatchCount) -{ - TLogHelper Logger("OpenPatchedMpqTest", PatchList[0]); - HANDLE hMpq; - HANDLE hFile; - BYTE Buffer[0x100]; - DWORD dwFileCount = 0; - DWORD BytesRead = 0; - int nError; - - // Open a patched MPQ archive - nError = OpenPatchedArchive(&Logger, &hMpq, PatchList); - if(nError == ERROR_SUCCESS) - { - // Check patch count - if(szPatchedFile != NULL) - nError = VerifyFilePatchCount(&Logger, hMpq, szPatchedFile, nExpectedPatchCount); - - // Try to open and read the file - if(nError == ERROR_SUCCESS) - { - if(SFileOpenFileEx(hMpq, szPatchedFile, 0, &hFile)) - { - SFileReadFile(hFile, Buffer, sizeof(Buffer), &BytesRead, NULL); - SFileCloseFile(hFile); - } - } - - // Search the archive and load every file - if(nError == ERROR_SUCCESS) - nError = SearchArchive(&Logger, hMpq, TEST_FLAG_LOAD_FILES, &dwFileCount); - - // Close the archive - SFileCloseArchive(hMpq); - } - - return nError; -} - -// Open an archive for read-only access -static int TestOpenArchive_ReadOnly(const char * szPlainName, bool bReadOnly) -{ - const char * szCopyName; - TLogHelper Logger("ReadOnlyTest", szPlainName); - HANDLE hMpq = NULL; - char szFullPathName[MAX_PATH]; - DWORD dwFlags = bReadOnly ? MPQ_OPEN_READ_ONLY : 0;; - int nExpectedError; - int nError; - - // Copy the fiel so we wont screw up something - szCopyName = bReadOnly ? "StormLibTest_ReadOnly.mpq" : "StormLibTest_ReadWrite.mpq"; - nError = CreateFileCopy(&Logger, szPlainName, szCopyName, szFullPathName); - - // Now open the archive for read-only access - if(nError == ERROR_SUCCESS) - nError = OpenExistingArchive(&Logger, szFullPathName, dwFlags, &hMpq); - - // Now try to add a file. This must fail if the MPQ is read only - if(nError == ERROR_SUCCESS) - { - nExpectedError = (bReadOnly) ? ERROR_ACCESS_DENIED : ERROR_SUCCESS; - AddFileToMpq(&Logger, hMpq, "AddedFile.txt", "This is an added file.", MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED, 0, nExpectedError); - } - - // Now try to rename a file in the MPQ. This must only succeed if the MPQ is not read only - if(nError == ERROR_SUCCESS) - { - nExpectedError = (bReadOnly) ? ERROR_ACCESS_DENIED : ERROR_SUCCESS; - RenameMpqFile(&Logger, hMpq, "spawn.mpq", "spawn-renamed.mpq", nExpectedError); - } - - // Now try to delete a file in the MPQ. This must only succeed if the MPQ is not read only - if(nError == ERROR_SUCCESS) - { - nExpectedError = (bReadOnly) ? ERROR_ACCESS_DENIED : ERROR_SUCCESS; - RemoveMpqFile(&Logger, hMpq, "spawn-renamed.mpq", nExpectedError); - } - - // Close the archive - if(hMpq != NULL) - SFileCloseArchive(hMpq); - return nError; -} - -static int TestOpenArchive_GetFileInfo(const char * szPlainName1, const char * szPlainName4) -{ - TLogHelper Logger("GetFileInfoTest", szPlainName1, szPlainName4); - HANDLE hFile; - HANDLE hMpq4; - HANDLE hMpq1; - DWORD cbLength; - BYTE DataBuff[0x400]; - int nError1; - int nError4; - - // Copy the archive so we won't fuck up the original one - nError1 = OpenExistingArchiveWithCopy(&Logger, szPlainName1, NULL, &hMpq1); - nError4 = OpenExistingArchiveWithCopy(&Logger, szPlainName4, NULL, &hMpq4); - if(nError1 == ERROR_SUCCESS && nError4 == ERROR_SUCCESS) - { - // Invalid handle - expected (false, ERROR_INVALID_HANDLE) - TestGetFileInfo(&Logger, NULL, SFileMpqBetHeader, NULL, 0, NULL, false, ERROR_INVALID_HANDLE); - - // Valid handle but invalid value of file info class (false, ERROR_INVALID_PARAMETER) - TestGetFileInfo(&Logger, NULL, (SFileInfoClass)0xFFF, NULL, 0, NULL, false, ERROR_INVALID_PARAMETER); - - // Valid archive handle but file info class is for file (false, ERROR_INVALID_HANDLE) - TestGetFileInfo(&Logger, NULL, SFileInfoNameHash1, NULL, 0, NULL, false, ERROR_INVALID_HANDLE); - - // Valid handle and all parameters NULL - // Returns (true, ERROR_SUCCESS), if BET table is present, otherwise (false, ERROR_CAN_NOT_COMPLETE) - TestGetFileInfo(&Logger, hMpq1, SFileMpqBetHeader, NULL, 0, NULL, false, ERROR_FILE_NOT_FOUND); - TestGetFileInfo(&Logger, hMpq4, SFileMpqBetHeader, NULL, 0, NULL, true, ERROR_SUCCESS); - - // Now try to retrieve the required size of the BET table header - TestGetFileInfo(&Logger, hMpq4, SFileMpqBetHeader, NULL, 0, &cbLength, true, ERROR_SUCCESS); - - // When we call SFileInfo with buffer = NULL and nonzero buffer size, it is ignored - TestGetFileInfo(&Logger, hMpq4, SFileMpqBetHeader, NULL, 3, &cbLength, true, ERROR_SUCCESS); - - // When we call SFileInfo with buffer != NULL and nonzero buffer size, it should return error - TestGetFileInfo(&Logger, hMpq4, SFileMpqBetHeader, DataBuff, 3, &cbLength, false, ERROR_INSUFFICIENT_BUFFER); - - // Request for bet table header should also succeed if we want header only - TestGetFileInfo(&Logger, hMpq4, SFileMpqBetHeader, DataBuff, sizeof(TMPQBetHeader), &cbLength, true, ERROR_SUCCESS); - - // Request for bet table header should also succeed if we want header+flag table only - TestGetFileInfo(&Logger, hMpq4, SFileMpqBetHeader, DataBuff, sizeof(DataBuff), &cbLength, true, ERROR_SUCCESS); - - // Try to retrieve strong signature from the MPQ - TestGetFileInfo(&Logger, hMpq1, SFileMpqStrongSignature, NULL, 0, NULL, true, ERROR_SUCCESS); - TestGetFileInfo(&Logger, hMpq4, SFileMpqStrongSignature, NULL, 0, NULL, false, ERROR_FILE_NOT_FOUND); - - // Strong signature is returned including the signature ID - TestGetFileInfo(&Logger, hMpq1, SFileMpqStrongSignature, NULL, 0, &cbLength, true, ERROR_SUCCESS); - assert(cbLength == MPQ_STRONG_SIGNATURE_SIZE + 4); - - // Retrieve the signature - TestGetFileInfo(&Logger, hMpq1, SFileMpqStrongSignature, DataBuff, sizeof(DataBuff), &cbLength, true, ERROR_SUCCESS); - assert(memcmp(DataBuff, "NGIS", 4) == 0); - - // Check SFileGetFileInfo on - if(SFileOpenFileEx(hMpq4, LISTFILE_NAME, 0, &hFile)) - { - // Valid parameters but the handle should be file handle - TestGetFileInfo(&Logger, hMpq4, SFileInfoFileTime, DataBuff, sizeof(DataBuff), &cbLength, false, ERROR_INVALID_HANDLE); - - // Valid parameters - TestGetFileInfo(&Logger, hFile, SFileInfoFileTime, DataBuff, sizeof(DataBuff), &cbLength, true, ERROR_SUCCESS); - - SFileCloseFile(hFile); - } - } - - if(hMpq4 != NULL) - SFileCloseArchive(hMpq4); - if(hMpq1 != NULL) - SFileCloseArchive(hMpq1); - return ERROR_SUCCESS; -} - -static int TestOpenArchive_MasterMirror(const char * szMirrorName, const char * szMasterName, const char * szFileToExtract, bool bCopyMirrorFile) -{ - TFileData * pFileData; - TLogHelper Logger("OpenServerMirror", szMirrorName); - HANDLE hFile = NULL; - HANDLE hMpq = NULL; - DWORD dwVerifyResult; - char szMirrorPath[MAX_PATH + MAX_PATH]; // Combined name - char szMasterPath[MAX_PATH]; // Original (server) name - int nError; - - // Create both paths - nError = CreateMasterAndMirrorPaths(&Logger, szMirrorPath, szMasterPath, szMirrorName, szMasterName, bCopyMirrorFile); - - // Now open both archives as local-server pair - if(nError == ERROR_SUCCESS) - { - nError = OpenExistingArchive(&Logger, szMirrorPath, 0, &hMpq); - } - - // The MPQ must be read-only. Writing to mirrored MPQ is not allowed - if(nError == ERROR_SUCCESS) - { - if(SFileCreateFile(hMpq, "AddedFile.bin", 0, 0x10, 0, MPQ_FILE_COMPRESS, &hFile)) - { - SFileCloseFile(hFile); - Logger.PrintMessage("The archive is writable, although it should not be"); - nError = ERROR_FILE_CORRUPT; - } - } - - // Verify the file - if(nError == ERROR_SUCCESS && szFileToExtract != NULL) - { - dwVerifyResult = SFileVerifyFile(hMpq, szFileToExtract, SFILE_VERIFY_ALL); - if(dwVerifyResult & VERIFY_FILE_ERROR_MASK) - { - Logger.PrintMessage("File verification failed"); - nError = ERROR_FILE_CORRUPT; - } - } - - // Load the file to memory - if(nError == ERROR_SUCCESS && szFileToExtract) - { - pFileData = LoadMpqFile(&Logger, hMpq, szFileToExtract); - if(pFileData != NULL) - STORM_FREE(pFileData); - } - - if(hMpq != NULL) - SFileCloseArchive(hMpq); - return nError; -} - - -static int TestOpenArchive_VerifySignature(const char * szPlainName, const char * szOriginalName) -{ - TLogHelper Logger("VerifySignatureTest", szPlainName); - HANDLE hMpq; - DWORD dwSignatures = 0; - int nVerifyError; - int nError = ERROR_SUCCESS; - - // We need original name for the signature check - nError = OpenExistingArchiveWithCopy(&Logger, szPlainName, szOriginalName, &hMpq); - if(nError == ERROR_SUCCESS) - { - // Query the signature types - Logger.PrintProgress("Retrieving signatures ..."); - TestGetFileInfo(&Logger, hMpq, SFileMpqSignatures, &dwSignatures, sizeof(DWORD), NULL, true, ERROR_SUCCESS); - - // Verify any of the present signatures - Logger.PrintProgress("Verifying archive signature ..."); - nVerifyError = SFileVerifyArchive(hMpq); - - // Verify the result - if((dwSignatures & SIGNATURE_TYPE_STRONG) && (nVerifyError != ERROR_STRONG_SIGNATURE_OK)) - { - Logger.PrintMessage("Strong signature verification error"); - nError = ERROR_FILE_CORRUPT; - } - - // Verify the result - if((dwSignatures & SIGNATURE_TYPE_WEAK) && (nVerifyError != ERROR_WEAK_SIGNATURE_OK)) - { - Logger.PrintMessage("Weak signature verification error"); - nError = ERROR_FILE_CORRUPT; - } - - SFileCloseArchive(hMpq); - } - return nError; -} - -static int TestOpenArchive_ModifySigned(const char * szPlainName, const char * szOriginalName) -{ - TLogHelper Logger("ModifySignedTest", szPlainName); - HANDLE hMpq = NULL; - int nVerifyError; - int nError = ERROR_SUCCESS; - - // We need original name for the signature check - nError = OpenExistingArchiveWithCopy(&Logger, szPlainName, szOriginalName, &hMpq); - if(nError == ERROR_SUCCESS) - { - // Verify the weak signature - Logger.PrintProgress("Verifying archive signature ..."); - nVerifyError = SFileVerifyArchive(hMpq); - - // Check the result signature - if(nVerifyError != ERROR_WEAK_SIGNATURE_OK) - { - Logger.PrintMessage("Weak signature verification error"); - nError = ERROR_FILE_CORRUPT; - } - } - - // Add a file and verify the signature again - if(nError == ERROR_SUCCESS) - { - // Verify any of the present signatures - Logger.PrintProgress("Modifying signed archive ..."); - nError = AddFileToMpq(&Logger, hMpq, "AddedFile01.txt", "This is a file added to signed MPQ", 0, 0, ERROR_SUCCESS); - } - - // Verify the signature again - if(nError == ERROR_SUCCESS) - { - // Verify the weak signature - Logger.PrintProgress("Verifying archive signature ..."); - nVerifyError = SFileVerifyArchive(hMpq); - - // Check the result signature - if(nVerifyError != ERROR_WEAK_SIGNATURE_OK) - { - Logger.PrintMessage("Weak signature verification error"); - nError = ERROR_FILE_CORRUPT; - } - } - - // Close the MPQ - if(hMpq != NULL) - SFileCloseArchive(hMpq); - return nError; -} - -static int TestOpenArchive_SignExisting(const char * szPlainName) -{ - TLogHelper Logger("SignExistingMpq", szPlainName); - HANDLE hMpq = NULL; - int nVerifyError; - int nError = ERROR_SUCCESS; - - // We need original name for the signature check - nError = OpenExistingArchiveWithCopy(&Logger, szPlainName, szPlainName, &hMpq); - if(nError == ERROR_SUCCESS) - { - // Verify the weak signature - Logger.PrintProgress("Verifying archive signature ..."); - nVerifyError = SFileVerifyArchive(hMpq); - - // Check the result signature - if(nVerifyError != ERROR_NO_SIGNATURE) - { - Logger.PrintMessage("There already is a signature in the MPQ"); - nError = ERROR_FILE_CORRUPT; - } - } - - // Add a file and verify the signature again - if(nError == ERROR_SUCCESS) - { - // Verify any of the present signatures - Logger.PrintProgress("Signing the MPQ ..."); - if(!SFileSignArchive(hMpq, SIGNATURE_TYPE_WEAK)) - { - Logger.PrintMessage("Failed to create archive signature"); - nError = ERROR_FILE_CORRUPT; - } - } - - // Verify the signature again - if(nError == ERROR_SUCCESS) - { - // Verify the weak signature - Logger.PrintProgress("Verifying archive signature ..."); - nVerifyError = SFileVerifyArchive(hMpq); - - // Check the result signature - if(nVerifyError != ERROR_WEAK_SIGNATURE_OK) - { - Logger.PrintMessage("Weak signature verification error"); - nError = ERROR_FILE_CORRUPT; - } - } - - // Close the MPQ - if(hMpq != NULL) - SFileCloseArchive(hMpq); - return nError; -} - -// Open an empty archive (found in WoW cache - it's just a header) -static int TestOpenArchive_CraftedUserData(const char * szPlainName, const char * szCopyName) -{ - TLogHelper Logger("CraftedMpqTest", szPlainName); - HANDLE hMpq; - DWORD dwFileCount1 = 0; - DWORD dwFileCount2 = 0; - BYTE FileHash1[MD5_DIGEST_SIZE]; - BYTE FileHash2[MD5_DIGEST_SIZE]; - char szFullPath[MAX_PATH]; - int nError; - - // Create copy of the archive, with interleaving some user data - nError = CreateFileCopy(&Logger, szPlainName, szCopyName, szFullPath, 0x400, 0x531); - - // Open the archive and load some files - if(nError == ERROR_SUCCESS) - { - // Open the archive - nError = OpenExistingArchive(&Logger, szFullPath, 0, &hMpq); - if(nError != ERROR_SUCCESS) - return nError; - - // Verify presence of (listfile) and (attributes) - CheckIfFileIsPresent(&Logger, hMpq, LISTFILE_NAME, true); - CheckIfFileIsPresent(&Logger, hMpq, ATTRIBUTES_NAME, true); - - // Search the archive and load every file - nError = SearchArchive(&Logger, hMpq, TEST_FLAG_LOAD_FILES | TEST_FLAG_HASH_FILES, &dwFileCount1, FileHash1); - SFileCloseArchive(hMpq); - } - - // Try to compact the MPQ - if(nError == ERROR_SUCCESS) - { - // Open the archive again - nError = OpenExistingArchive(&Logger, szFullPath, 0, &hMpq); - if(nError != ERROR_SUCCESS) - return nError; - - // Compact the archive - Logger.PrintProgress("Compacting archive %s ...", GetShortPlainName(szFullPath)); - if(!SFileSetCompactCallback(hMpq, CompactCallback, &Logger)) - nError = Logger.PrintError("Failed to compact archive %s", szFullPath); - - SFileCompactArchive(hMpq, NULL, false); - SFileCloseArchive(hMpq); - } - - // Open the archive and load some files - if(nError == ERROR_SUCCESS) - { - // Open the archive - nError = OpenExistingArchive(&Logger, szFullPath, 0, &hMpq); - if(nError != ERROR_SUCCESS) - return nError; - - // Verify presence of (listfile) and (attributes) - CheckIfFileIsPresent(&Logger, hMpq, LISTFILE_NAME, true); - CheckIfFileIsPresent(&Logger, hMpq, ATTRIBUTES_NAME, true); - - // Search the archive and load every file - nError = SearchArchive(&Logger, hMpq, TEST_FLAG_LOAD_FILES | TEST_FLAG_HASH_FILES, &dwFileCount2, FileHash2); - SFileCloseArchive(hMpq); - } - - // Compare the file counts and their hashes - if(nError == ERROR_SUCCESS) - { - if(dwFileCount2 != dwFileCount1) - Logger.PrintMessage("Different file count after compacting archive: %u vs %u", dwFileCount2, dwFileCount1); - - if(memcmp(FileHash2, FileHash1, MD5_DIGEST_SIZE)) - Logger.PrintMessage("Different file hash after compacting archive"); - } - - return nError; -} - -static int ForEachFile_VerifyFileChecksum(const char * szFullPath) -{ - const char * szShortPlainName = GetShortPlainName(szFullPath); - TFileData * pFileData; - char * szExtension; - char szShaFileName[MAX_PATH+1]; - char szSha1Text[0x40]; - int nError = ERROR_SUCCESS; - - // Try to load the file with the SHA extension - StringCopyA(szShaFileName, szFullPath, MAX_PATH); - szExtension = strrchr(szShaFileName, '.'); - if(szExtension == NULL) - return ERROR_SUCCESS; - - // Skip .SHA and .TXT files - if(!_stricmp(szExtension, ".sha") || !_stricmp(szExtension, ".txt")) - return ERROR_SUCCESS; - - // Load the local file to memory - strcpy(szExtension, ".sha"); - pFileData = LoadLocalFile(NULL, szShaFileName, false); - if(pFileData != NULL) - { - TLogHelper Logger("VerifyFileHash", szShortPlainName); - - // Calculate SHA1 of the entire file - nError = CalculateFileSha1(&Logger, szFullPath, szSha1Text); - if(nError == ERROR_SUCCESS) - { - // Compare with what we loaded from the file - if(pFileData->dwFileSize >= (SHA1_DIGEST_SIZE * 2)) - { - // Compare the SHA1 - if(_strnicmp(szSha1Text, (char *)pFileData->FileData, (SHA1_DIGEST_SIZE * 2))) - { - Logger.PrintError("File CRC check failed: %s", szFullPath); - nError = ERROR_FILE_CORRUPT; - } - } - } - - STORM_FREE(pFileData); - } - - return nError; -} - -// Opens a found archive -static int ForEachFile_OpenArchive(const char * szFullPath) -{ - HANDLE hMpq = NULL; - DWORD dwFileCount = 0; - int nError = ERROR_SUCCESS; - - // Check if it's a MPQ file type - if(IsMpqExtension(szFullPath)) - { - TLogHelper Logger("OpenEachMpqTest", GetShortPlainName(szFullPath)); - - // Open the MPQ name - nError = OpenExistingArchive(&Logger, szFullPath, 0, &hMpq); - if(nError == ERROR_AVI_FILE || nError == ERROR_FILE_CORRUPT || nError == ERROR_BAD_FORMAT) - return ERROR_SUCCESS; - - // Search the archive and load every file - if(nError == ERROR_SUCCESS) - { - nError = SearchArchive(&Logger, hMpq, 0, &dwFileCount); - SFileCloseArchive(hMpq); - } - } - - // Correct some errors - if(nError == ERROR_FILE_CORRUPT || nError == ERROR_FILE_INCOMPLETE) - return ERROR_SUCCESS; - return nError; -} - -// Adding a file to MPQ that had size of the file table equal -// or greater than hash table, but has free entries -static int TestAddFile_FullTable(const char * szFullMpqName) -{ - TLogHelper Logger("FullMpqTest", szFullMpqName); - const char * szFileName = "AddedFile001.txt"; - const char * szFileData = "0123456789ABCDEF"; - HANDLE hMpq = NULL; - int nError = ERROR_SUCCESS; - - // Copy the archive so we won't fuck up the original one - nError = OpenExistingArchiveWithCopy(&Logger, szFullMpqName, szFullMpqName, &hMpq); - if(nError == ERROR_SUCCESS) - { - // Attempt to add a file - nError = AddFileToMpq(&Logger, hMpq, szFileName, szFileData, MPQ_FILE_IMPLODE, MPQ_COMPRESSION_PKWARE, ERROR_SUCCESS); - SFileCloseArchive(hMpq); - } - - return nError; -} - -// Adding a file to MPQ that had no (listfile) and no (attributes). -// We expect that neither of these will be present after the archive is closed -static int TestAddFile_ListFileTest(const char * szSourceMpq, bool bShouldHaveListFile, bool bShouldHaveAttributes) -{ - TLogHelper Logger("ListFileTest", szSourceMpq); - TFileData * pFileData = NULL; - const char * szBackupMpq = bShouldHaveListFile ? "StormLibTest_HasListFile.mpq" : "StormLibTest_NoListFile.mpq"; - const char * szFileName = "AddedFile001.txt"; - const char * szFileData = "0123456789ABCDEF"; - HANDLE hMpq = NULL; - DWORD dwFileSize = (DWORD)strlen(szFileData); - int nError = ERROR_SUCCESS; - - // Copy the archive so we won't fuck up the original one - nError = OpenExistingArchiveWithCopy(&Logger, szSourceMpq, szBackupMpq, &hMpq); - - // Add a file - if(nError == ERROR_SUCCESS) - { - // Now add a file - nError = AddFileToMpq(&Logger, hMpq, szFileName, szFileData, MPQ_FILE_IMPLODE, MPQ_COMPRESSION_PKWARE); - SFileCloseArchive(hMpq); - hMpq = NULL; - } - - // Now reopen the archive - if(nError == ERROR_SUCCESS) - nError = OpenExistingArchiveWithCopy(&Logger, NULL, szBackupMpq, &hMpq); - - // Now the file has been written and the MPQ has been saved. - // We Reopen the MPQ and check if there is no (listfile) nor (attributes). - if(nError == ERROR_SUCCESS) - { - // Verify presence of (listfile) and (attributes) - CheckIfFileIsPresent(&Logger, hMpq, LISTFILE_NAME, bShouldHaveListFile); - CheckIfFileIsPresent(&Logger, hMpq, ATTRIBUTES_NAME, bShouldHaveAttributes); - - // Try to open the file that we recently added - pFileData = LoadMpqFile(&Logger, hMpq, szFileName); - if(pFileData != NULL) - { - // Verify if the file size matches - if(pFileData->dwFileSize == dwFileSize) - { - // Verify if the file data match - if(memcmp(pFileData->FileData, szFileData, dwFileSize)) - { - Logger.PrintError("The data of the added file does not match"); - nError = ERROR_FILE_CORRUPT; - } - } - else - { - Logger.PrintError("The size of the added file does not match"); - nError = ERROR_FILE_CORRUPT; - } - - // Delete the file data - STORM_FREE(pFileData); - } - else - { - nError = Logger.PrintError("Failed to open the file previously added"); - } - } - - // Close the MPQ archive - if(hMpq != NULL) - SFileCloseArchive(hMpq); - return nError; -} -/* -static int TestCreateArchive_Deprotect(const char * szPlainName) -{ - TLogHelper Logger("DeprotectTest", szPlainName); - HANDLE hMpq1 = NULL; - HANDLE hMpq2 = NULL; - char szMpqName1[MAX_PATH]; - char szMpqName2[MAX_PATH]; - BYTE FileHash1[MD5_DIGEST_SIZE]; - BYTE FileHash2[MD5_DIGEST_SIZE]; - DWORD dwFileCount1 = 0; - DWORD dwFileCount2 = 0; - DWORD dwTestFlags = TEST_FLAG_LOAD_FILES | TEST_FLAG_HASH_FILES; - int nError = ERROR_SUCCESS; - - // First copy: The original (untouched) file - if(nError == ERROR_SUCCESS) - { - AddStringBeforeExtension(szMpqName1, szPlainName, "_original"); - nError = OpenExistingArchiveWithCopy(&Logger, szPlainName, szMpqName1, &hMpq1); - if(nError != ERROR_SUCCESS) - Logger.PrintMessage("Failed to open %s", szMpqName1); - } - - // Second copy: Will be deprotected - if(nError == ERROR_SUCCESS) - { - AddStringBeforeExtension(szMpqName2, szPlainName, "_deprotected"); - nError = OpenExistingArchiveWithCopy(&Logger, szPlainName, szMpqName2, &hMpq2); - if(nError != ERROR_SUCCESS) - Logger.PrintMessage("Failed to open %s", szMpqName2); - } - - // Deprotect the second map - if(nError == ERROR_SUCCESS) - { - SFileSetCompactCallback(hMpq2, CompactCallback, &Logger); - if(!SFileCompactArchive(hMpq2, NULL, false)) - nError = Logger.PrintError("Failed to deprotect archive %s", szMpqName2); - } - - // Calculate number of files and compare their hash (archive 1) - if(nError == ERROR_SUCCESS) - { - memset(FileHash1, 0, sizeof(FileHash1)); - nError = SearchArchive(&Logger, hMpq1, dwTestFlags, &dwFileCount1, FileHash1); - } - - // Calculate number of files and compare their hash (archive 2) - if(nError == ERROR_SUCCESS) - { - memset(FileHash1, 0, sizeof(FileHash2)); - nError = SearchArchive(&Logger, hMpq2, dwTestFlags, &dwFileCount2, FileHash2); - } - - if(nError == ERROR_SUCCESS) - { - if(dwFileCount1 != dwFileCount2) - Logger.PrintMessage("Different file count (%u in %s; %u in %s)", dwFileCount1, szMpqName1, dwFileCount2, szMpqName2); - if(memcmp(FileHash1, FileHash2, MD5_DIGEST_SIZE)) - Logger.PrintMessage("Different file hash (%s vs %s)", szMpqName1, szMpqName2); - } - - // Close both MPQs - if(hMpq2 != NULL) - SFileCloseArchive(hMpq2); - if(hMpq1 != NULL) - SFileCloseArchive(hMpq1); - return nError; -} -*/ -static int TestCreateArchive_EmptyMpq(const char * szPlainName, DWORD dwCreateFlags) -{ - TLogHelper Logger("CreateEmptyMpq", szPlainName); - HANDLE hMpq = NULL; - DWORD dwFileCount = 0; - int nError; - - // Create the full path name - nError = CreateNewArchive(&Logger, szPlainName, dwCreateFlags, 0, &hMpq); - if(nError == ERROR_SUCCESS) - { - SearchArchive(&Logger, hMpq); - SFileCloseArchive(hMpq); - } - - // Reopen the empty MPQ - if(nError == ERROR_SUCCESS) - { - nError = OpenExistingArchiveWithCopy(&Logger, NULL, szPlainName, &hMpq); - if(nError == ERROR_SUCCESS) - { - SFileGetFileInfo(hMpq, SFileMpqNumberOfFiles, &dwFileCount, sizeof(dwFileCount), NULL); - - CheckIfFileIsPresent(&Logger, hMpq, "File00000000.xxx", false); - CheckIfFileIsPresent(&Logger, hMpq, LISTFILE_NAME, false); - SearchArchive(&Logger, hMpq); - SFileCloseArchive(hMpq); - } - } - - return nError; -} - -static int TestCreateArchive_TestGaps(const char * szPlainName) -{ - TLogHelper Logger("CreateGapsTest", szPlainName); - ULONGLONG ByteOffset1 = 0xFFFFFFFF; - ULONGLONG ByteOffset2 = 0xEEEEEEEE; - HANDLE hMpq = NULL; - HANDLE hFile = NULL; - char szFullPath[MAX_PATH]; - int nError = ERROR_SUCCESS; - - // Create new MPQ - nError = CreateNewArchive_V2(&Logger, szPlainName, MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES | MPQ_FORMAT_VERSION_4, 4000, &hMpq); - if(nError == ERROR_SUCCESS) - { - // Add one file and flush the archive - nError = AddFileToMpq(&Logger, hMpq, "AddedFile01.txt", "This is the file data.", MPQ_FILE_COMPRESS); - SFileCloseArchive(hMpq); - hMpq = NULL; - } - - // Reopen the MPQ and add another file. - // The new file must be added to the position of the (listfile) - if(nError == ERROR_SUCCESS) - { - CreateFullPathName(szFullPath, NULL, szPlainName); - nError = OpenExistingArchive(&Logger, szFullPath, 0, &hMpq); - if(nError == ERROR_SUCCESS) - { - // Retrieve the position of the (listfile) - if(SFileOpenFileEx(hMpq, LISTFILE_NAME, 0, &hFile)) - { - SFileGetFileInfo(hFile, SFileInfoByteOffset, &ByteOffset1, sizeof(ULONGLONG), NULL); - SFileCloseFile(hFile); - } - else - nError = GetLastError(); - } - } - - // Add another file and check its position. It must be at the position of the former listfile - if(nError == ERROR_SUCCESS) - { - const char * szAddedFile = "AddedFile02.txt"; - - // Add another file - nError = AddFileToMpq(&Logger, hMpq, szAddedFile, "This is the second added file.", MPQ_FILE_COMPRESS); - - // Retrieve the position of the (listfile) - if(SFileOpenFileEx(hMpq, szAddedFile, 0, &hFile)) - { - SFileGetFileInfo(hFile, SFileInfoByteOffset, &ByteOffset2, sizeof(ULONGLONG), NULL); - SFileCloseFile(hFile); - } - else - nError = GetLastError(); - } - - // Now check the positions - if(nError == ERROR_SUCCESS) - { - if(ByteOffset1 != ByteOffset2) - { - Logger.PrintError("The added file was not written to the position of (listfile)"); - nError = ERROR_FILE_CORRUPT; - } - } - - // Close the archive if needed - if(hMpq != NULL) - SFileCloseArchive(hMpq); - return nError; -} - -static int TestCreateArchive_Signed(const char * szPlainName, bool bSignAtCreate) -{ - TLogHelper Logger("CreateSignedMpq", szPlainName); - HANDLE hMpq = NULL; - DWORD dwCreateFlags = MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES | MPQ_FORMAT_VERSION_1; - DWORD dwSignatures = 0; - DWORD nVerifyError = 0; - int nError = ERROR_SUCCESS; - - // Method 1: Create the archive as signed - if(bSignAtCreate) - dwCreateFlags |= MPQ_CREATE_SIGNATURE; - - // Create new MPQ - nError = CreateNewArchive_V2(&Logger, szPlainName, dwCreateFlags, 4000, &hMpq); - if(nError == ERROR_SUCCESS) - { - // Add one file and flush the archive - nError = AddFileToMpq(&Logger, hMpq, "AddedFile01.txt", "This is the file data.", MPQ_FILE_COMPRESS); - } - - // Sign the archive with weak signature - if(nError == ERROR_SUCCESS) - { - if(!SFileSignArchive(hMpq, SIGNATURE_TYPE_WEAK)) - nError = ERROR_SUCCESS; - } - - // Reopen the MPQ and add another file. - // The new file must be added to the position of the (listfile) - if(nError == ERROR_SUCCESS) - { - // Query the signature types - Logger.PrintProgress("Retrieving signatures ..."); - TestGetFileInfo(&Logger, hMpq, SFileMpqSignatures, &dwSignatures, sizeof(DWORD), NULL, true, ERROR_SUCCESS); - - // Verify any of the present signatures - Logger.PrintProgress("Verifying archive signature ..."); - nVerifyError = SFileVerifyArchive(hMpq); - - // Verify the result - if((dwSignatures != SIGNATURE_TYPE_WEAK) && (nVerifyError != ERROR_WEAK_SIGNATURE_OK)) - { - Logger.PrintMessage("Weak signature verification error"); - nError = ERROR_FILE_CORRUPT; - } - } - - // Close the archive - if(hMpq != NULL) - SFileCloseArchive(hMpq); - return nError; -} - -static int TestCreateArchive_MpqEditor(const char * szPlainName, const char * szFileName) -{ - TLogHelper Logger("CreateMpqEditor", szPlainName); - HANDLE hMpq = NULL; - int nError = ERROR_SUCCESS; - - // Create new MPQ - nError = CreateNewArchive_V2(&Logger, szPlainName, MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES, 4000, &hMpq); - if(nError == ERROR_SUCCESS) - { - // Flush the archive first - SFileFlushArchive(hMpq); - - // Add one file - nError = AddFileToMpq(&Logger, hMpq, szFileName, "This is the file data.", MPQ_FILE_COMPRESS); - - // Flush the archive again - SFileFlushArchive(hMpq); - SFileCloseArchive(hMpq); - } - else - { - nError = GetLastError(); - } - - return nError; -} - -static int TestCreateArchive_FillArchive(const char * szPlainName, DWORD dwCreateFlags) -{ - TLogHelper Logger("CreateFullMpq", szPlainName); - const char * szFileData = "TestCreateArchive_FillArchive: Testing file data"; - char szFileName[MAX_PATH]; - HANDLE hMpq = NULL; - DWORD dwMaxFileCount = 6; - DWORD dwCompression = MPQ_COMPRESSION_ZLIB; - DWORD dwFlags = MPQ_FILE_ENCRYPTED | MPQ_FILE_COMPRESS; - int nError; - - // - // Note that StormLib will round the maxfile count - // up to hash table size (nearest power of two) - // - if((dwCreateFlags & MPQ_CREATE_LISTFILE) == 0) - dwMaxFileCount++; - if((dwCreateFlags & MPQ_CREATE_ATTRIBUTES) == 0) - dwMaxFileCount++; - - // Create the new MPQ archive - nError = CreateNewArchive_V2(&Logger, szPlainName, dwCreateFlags, dwMaxFileCount, &hMpq); - if(nError == ERROR_SUCCESS) - { - // Flush the archive first - SFileFlushArchive(hMpq); - - // Add all files - for(unsigned int i = 0; i < dwMaxFileCount; i++) - { - sprintf(szFileName, "AddedFile%03u.txt", i); - nError = AddFileToMpq(&Logger, hMpq, szFileName, szFileData, dwFlags, dwCompression); - if(nError != ERROR_SUCCESS) - break; - } - - // Flush the archive again - SFileFlushArchive(hMpq); - } - - // Now the MPQ should be full. It must not be possible to add another file - if(nError == ERROR_SUCCESS) - { - nError = AddFileToMpq(&Logger, hMpq, "ShouldNotBeHere.txt", szFileData, MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB, ERROR_DISK_FULL); - assert(nError != ERROR_SUCCESS); - nError = ERROR_SUCCESS; - } - - // Close the archive to enforce saving all tables - if(hMpq != NULL) - SFileCloseArchive(hMpq); - hMpq = NULL; - - // Reopen the archive again - if(nError == ERROR_SUCCESS) - nError = OpenExistingArchiveWithCopy(&Logger, NULL, szPlainName, &hMpq); - - // The archive should still be full - if(nError == ERROR_SUCCESS) - { - CheckIfFileIsPresent(&Logger, hMpq, LISTFILE_NAME, (dwCreateFlags & MPQ_CREATE_LISTFILE) ? true : false); - CheckIfFileIsPresent(&Logger, hMpq, ATTRIBUTES_NAME, (dwCreateFlags & MPQ_CREATE_ATTRIBUTES) ? true : false); - nError = AddFileToMpq(&Logger, hMpq, "ShouldNotBeHere.txt", szFileData, MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB, ERROR_DISK_FULL); - assert(nError != ERROR_SUCCESS); - nError = ERROR_SUCCESS; - } - - // The (listfile) and (attributes) must be present - if(nError == ERROR_SUCCESS) - { - CheckIfFileIsPresent(&Logger, hMpq, LISTFILE_NAME, (dwCreateFlags & MPQ_CREATE_LISTFILE) ? true : false); - CheckIfFileIsPresent(&Logger, hMpq, ATTRIBUTES_NAME, (dwCreateFlags & MPQ_CREATE_ATTRIBUTES) ? true : false); - nError = RemoveMpqFile(&Logger, hMpq, szFileName, ERROR_SUCCESS); - } - - // Now add the file again. This time, it should be possible OK - if(nError == ERROR_SUCCESS) - { - nError = AddFileToMpq(&Logger, hMpq, szFileName, szFileData, dwFlags, dwCompression, ERROR_SUCCESS); - assert(nError == ERROR_SUCCESS); - } - - // Now add the file again. This time, it should fail - if(nError == ERROR_SUCCESS) - { - nError = AddFileToMpq(&Logger, hMpq, szFileName, szFileData, dwFlags, dwCompression, ERROR_ALREADY_EXISTS); - assert(nError != ERROR_SUCCESS); - nError = ERROR_SUCCESS; - } - - // Now add the file again. This time, it should fail - if(nError == ERROR_SUCCESS) - { - nError = AddFileToMpq(&Logger, hMpq, "ShouldNotBeHere.txt", szFileData, dwFlags, dwCompression, ERROR_DISK_FULL); - assert(nError != ERROR_SUCCESS); - nError = ERROR_SUCCESS; - } - - // Close the archive and return - if(hMpq != NULL) - SFileCloseArchive(hMpq); - hMpq = NULL; - - // Reopen the archive for the third time to verify that both internal files are there - if(nError == ERROR_SUCCESS) - { - nError = OpenExistingArchiveWithCopy(&Logger, NULL, szPlainName, &hMpq); - if(nError == ERROR_SUCCESS) - { - CheckIfFileIsPresent(&Logger, hMpq, LISTFILE_NAME, (dwCreateFlags & MPQ_CREATE_LISTFILE) ? true : false); - CheckIfFileIsPresent(&Logger, hMpq, ATTRIBUTES_NAME, (dwCreateFlags & MPQ_CREATE_ATTRIBUTES) ? true : false); - SFileCloseArchive(hMpq); - } - } - - return nError; -} - -static int TestCreateArchive_IncMaxFileCount(const char * szPlainName) -{ - TLogHelper Logger("IncMaxFileCount", szPlainName); - const char * szFileData = "TestCreateArchive_IncMaxFileCount: Testing file data"; - char szFileName[MAX_PATH]; - HANDLE hMpq = NULL; - DWORD dwMaxFileCount = 1; - int nError; - - // Create the new MPQ - nError = CreateNewArchive(&Logger, szPlainName, MPQ_CREATE_ARCHIVE_V4 | MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES, dwMaxFileCount, &hMpq); - - // Now add exactly one file - if(nError == ERROR_SUCCESS) - { - nError = AddFileToMpq(&Logger, hMpq, "AddFile_base.txt", szFileData); - SFileFlushArchive(hMpq); - SFileCloseArchive(hMpq); - } - - // Now add 10 files. Each time we cannot add the file due to archive being full, - // we increment the max file count - if(nError == ERROR_SUCCESS) - { - for(unsigned int i = 0; i < 10; i++) - { - // Open the archive again - nError = OpenExistingArchiveWithCopy(&Logger, NULL, szPlainName, &hMpq); - if(nError != ERROR_SUCCESS) - break; - - // Add one file - sprintf(szFileName, "AddFile_%04u.txt", i); - nError = AddFileToMpq(&Logger, hMpq, szFileName, szFileData, 0, 0, ERROR_UNDETERMINED_RESULT); - if(nError != ERROR_SUCCESS) - { - // Increment the max file count by one - dwMaxFileCount = SFileGetMaxFileCount(hMpq) + 1; - Logger.PrintProgress("Increasing max file count to %u ...", dwMaxFileCount); - SFileSetMaxFileCount(hMpq, dwMaxFileCount); - - // Attempt to create the file again - nError = AddFileToMpq(&Logger, hMpq, szFileName, szFileData, 0, 0, ERROR_SUCCESS); - } - - // Compact the archive and close it - SFileSetCompactCallback(hMpq, CompactCallback, &Logger); - SFileCompactArchive(hMpq, NULL, false); - SFileCloseArchive(hMpq); - if(nError != ERROR_SUCCESS) - break; - } - } - - return nError; -} - -static int TestCreateArchive_UnicodeNames() -{ - TLogHelper Logger("MpqUnicodeName"); - DWORD dwCreateFlags = MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES; - int nError = ERROR_SUCCESS; - - nError = CreateNewArchiveU(&Logger, szUnicodeName1, dwCreateFlags | MPQ_CREATE_ARCHIVE_V1, 15); - if(nError != ERROR_SUCCESS) - return nError; - - nError = CreateNewArchiveU(&Logger, szUnicodeName2, dwCreateFlags | MPQ_CREATE_ARCHIVE_V2, 58); - if(nError != ERROR_SUCCESS) - return nError; - - nError = CreateNewArchiveU(&Logger, szUnicodeName3, dwCreateFlags | MPQ_CREATE_ARCHIVE_V3, 15874); - if(nError != ERROR_SUCCESS) - return nError; - - nError = CreateNewArchiveU(&Logger, szUnicodeName4, dwCreateFlags | MPQ_CREATE_ARCHIVE_V4, 87541); - if(nError != ERROR_SUCCESS) - return nError; - - nError = CreateNewArchiveU(&Logger, szUnicodeName5, dwCreateFlags | MPQ_CREATE_ARCHIVE_V3, 87541); - if(nError != ERROR_SUCCESS) - return nError; - - nError = CreateNewArchiveU(&Logger, szUnicodeName5, dwCreateFlags | MPQ_CREATE_ARCHIVE_V2, 87541); - if(nError != ERROR_SUCCESS) - return nError; - - return nError; -} - -static int TestCreateArchive_FileFlagTest(const char * szPlainName) -{ - TLogHelper Logger("FileFlagTest", szPlainName); - HANDLE hMpq = NULL; // Handle of created archive - char szFileName1[MAX_PATH]; - char szFileName2[MAX_PATH]; - char szFullPath[MAX_PATH]; - const char * szMiddleFile = "FileTest_10.exe"; - LCID LocaleIDs[] = {0x000, 0x405, 0x406, 0x407, 0xFFFF}; - char szArchivedName[MAX_PATH]; - DWORD dwMaxFileCount = 0; - DWORD dwFileCount = 0; - size_t i; - int nError; - - // Create paths for local file to be added - CreateFullPathName(szFileName1, szMpqSubDir, "AddFile.exe"); - CreateFullPathName(szFileName2, szMpqSubDir, "AddFile.bin"); - - // Create an empty file that will serve as holder for the MPQ - nError = CreateEmptyFile(&Logger, szPlainName, 0x100000, szFullPath); - - // Create new MPQ archive over that file - if(nError == ERROR_SUCCESS) - nError = CreateNewArchive(&Logger, szPlainName, MPQ_CREATE_ARCHIVE_V1 | MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES, 17, &hMpq); - - // Add the same file multiple times - if(nError == ERROR_SUCCESS) - { - dwMaxFileCount = SFileGetMaxFileCount(hMpq); - for(i = 0; AddFlags[i] != 0xFFFFFFFF; i++) - { - sprintf(szArchivedName, "FileTest_%02u.exe", (unsigned int)i); - nError = AddLocalFileToMpq(&Logger, hMpq, szArchivedName, szFileName1, AddFlags[i], 0); - if(nError != ERROR_SUCCESS) - break; - - dwFileCount++; - } - } - - // Delete a file in the middle of the file table - if(nError == ERROR_SUCCESS) - { - Logger.PrintProgress("Removing file %s ...", szMiddleFile); - nError = RemoveMpqFile(&Logger, hMpq, szMiddleFile, ERROR_SUCCESS); - dwFileCount--; - } - - // Add one more file - if(nError == ERROR_SUCCESS) - { - nError = AddLocalFileToMpq(&Logger, hMpq, "FileTest_xx.exe", szFileName1); - dwFileCount++; - } - - // Try to decrement max file count. This must succeed - if(nError == ERROR_SUCCESS) - { - Logger.PrintProgress("Attempting to decrement max file count ..."); - if(SFileSetMaxFileCount(hMpq, 5)) - nError = Logger.PrintError("Max file count decremented, even if it should fail"); - } - - // Add ZeroSize.txt several times under a different locale - if(nError == ERROR_SUCCESS) - { - for(i = 0; LocaleIDs[i] != 0xFFFF; i++) - { - bool bMustSucceed = ((dwFileCount + 2) < dwMaxFileCount); - - SFileSetLocale(LocaleIDs[i]); - nError = AddLocalFileToMpq(&Logger, hMpq, "ZeroSize_1.txt", szFileName2); - if(nError != ERROR_SUCCESS) - { - if(bMustSucceed == false) - nError = ERROR_SUCCESS; - break; - } - - dwFileCount++; - } - } - - // Add ZeroSize.txt again several times under a different locale - if(nError == ERROR_SUCCESS) - { - for(i = 0; LocaleIDs[i] != 0xFFFF; i++) - { - bool bMustSucceed = ((dwFileCount + 2) < dwMaxFileCount); - - SFileSetLocale(LocaleIDs[i]); - nError = AddLocalFileToMpq(&Logger, hMpq, "ZeroSize_2.txt", szFileName2, 0, 0, bMustSucceed); - if(nError != ERROR_SUCCESS) - { - if(bMustSucceed == false) - nError = ERROR_SUCCESS; - break; - } - - dwFileCount++; - } - } - - // Verify how many files did we add to the MPQ - if(nError == ERROR_SUCCESS) - { - if(dwFileCount + 2 != dwMaxFileCount) - { - Logger.PrintErrorVa("Number of files added to MPQ was unexpected (expected %u, added %u)", dwFileCount, dwMaxFileCount - 2); - nError = ERROR_FILE_CORRUPT; - } - } - - // Test rename function - if(nError == ERROR_SUCCESS) - { - Logger.PrintProgress("Testing rename files ..."); - SFileSetLocale(LANG_NEUTRAL); - if(!SFileRenameFile(hMpq, "FileTest_08.exe", "FileTest_08a.exe")) - nError = Logger.PrintError("Failed to rename the file"); - } - - if(nError == ERROR_SUCCESS) - { - if(!SFileRenameFile(hMpq, "FileTest_08a.exe", "FileTest_08.exe")) - nError = Logger.PrintError("Failed to rename the file"); - } - - if(nError == ERROR_SUCCESS) - { - if(SFileRenameFile(hMpq, "FileTest_10.exe", "FileTest_10a.exe")) - { - Logger.PrintError("Rename test succeeded even if it shouldn't"); - nError = ERROR_FILE_CORRUPT; - } - } - - if(nError == ERROR_SUCCESS) - { - if(SFileRenameFile(hMpq, "FileTest_10a.exe", "FileTest_10.exe")) - { - Logger.PrintError("Rename test succeeded even if it shouldn't"); - nError = ERROR_FILE_CORRUPT; - } - } - - // Close the archive - if(hMpq != NULL) - SFileCloseArchive(hMpq); - hMpq = NULL; - - // Try to reopen the archive - if(nError == ERROR_SUCCESS) - nError = OpenExistingArchive(&Logger, szFullPath, 0, NULL); - return nError; -} - -static int TestCreateArchive_WaveCompressionsTest(const char * szPlainName, const char * szWaveFile) -{ - TLogHelper Logger("CompressionsTest", szPlainName); - HANDLE hMpq = NULL; // Handle of created archive - char szFileName[MAX_PATH]; // Source file to be added - char szArchivedName[MAX_PATH]; - DWORD dwCmprCount = sizeof(WaveCompressions) / sizeof(DWORD); - DWORD dwAddedFiles = 0; - DWORD dwFoundFiles = 0; - int nError; - - // Create paths for local file to be added - CreateFullPathName(szFileName, szMpqSubDir, szWaveFile); - - // Create new archive - nError = CreateNewArchive(&Logger, szPlainName, MPQ_CREATE_ARCHIVE_V1 | MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES, 0x40, &hMpq); - - // Add the same file multiple times - if(nError == ERROR_SUCCESS) - { - Logger.UserTotal = dwCmprCount; - for(unsigned int i = 0; i < dwCmprCount; i++) - { - sprintf(szArchivedName, "WaveFile_%02u.wav", i + 1); - nError = AddLocalFileToMpq(&Logger, hMpq, szArchivedName, szFileName, MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_SECTOR_CRC, WaveCompressions[i]); - if(nError != ERROR_SUCCESS) - break; - - Logger.UserCount++; - dwAddedFiles++; - } - - SFileCloseArchive(hMpq); - } - - // Reopen the archive extract each WAVE file and try to play it - if(nError == ERROR_SUCCESS) - { - nError = OpenExistingArchiveWithCopy(&Logger, NULL, szPlainName, &hMpq); - if(nError == ERROR_SUCCESS) - { - SearchArchive(&Logger, hMpq, TEST_FLAG_LOAD_FILES | TEST_FLAG_PLAY_WAVES, &dwFoundFiles, NULL); - SFileCloseArchive(hMpq); - } - - // Check if the number of found files is the same like the number of added files - // DOn;t forget that there will be (listfile) and (attributes) - if(dwFoundFiles != (dwAddedFiles + 2)) - { - Logger.PrintError("Number of found files does not match number of added files."); - nError = ERROR_FILE_CORRUPT; - } - } - - return nError; -} - -static int TestCreateArchive_ListFilePos(const char * szPlainName) -{ - TFileData * pFileData; - const char * szReaddedFile = "AddedFile_##.txt"; - const char * szFileMask = "AddedFile_%02u.txt"; - TLogHelper Logger("ListFilePos", szPlainName); - HANDLE hMpq = NULL; // Handle of created archive - char szArchivedName[MAX_PATH]; - DWORD dwMaxFileCount = 0x0E; - DWORD dwFileCount = 0; - size_t i; - int nError; - - // Create a new archive with the limit of 0x20 files - nError = CreateNewArchive(&Logger, szPlainName, MPQ_CREATE_ARCHIVE_V4 | MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES, dwMaxFileCount, &hMpq); - - // Add maximum files files - if(nError == ERROR_SUCCESS) - { - for(i = 0; i < dwMaxFileCount; i++) - { - sprintf(szArchivedName, szFileMask, i); - nError = AddFileToMpq(&Logger, hMpq, szArchivedName, "This is a text data.", 0, 0, ERROR_SUCCESS); - if(nError != ERROR_SUCCESS) - break; - - dwFileCount++; - } - } - - // Delete few middle files - if(nError == ERROR_SUCCESS) - { - for(i = 0; i < (dwMaxFileCount / 2); i++) - { - sprintf(szArchivedName, szFileMask, i); - nError = RemoveMpqFile(&Logger, hMpq, szArchivedName, ERROR_SUCCESS); - if(nError != ERROR_SUCCESS) - break; - - dwFileCount--; - } - } - - // Close the archive - if(hMpq != NULL) - SFileCloseArchive(hMpq); - hMpq = NULL; - - // Reopen the archive to catch any asserts - if(nError == ERROR_SUCCESS) - nError = OpenExistingArchiveWithCopy(&Logger, NULL, szPlainName, &hMpq); - - // Check that (listfile) is at the end - if(nError == ERROR_SUCCESS) - { - pFileData = LoadMpqFile(&Logger, hMpq, LISTFILE_NAME); - if(pFileData != NULL) - { - if(pFileData->dwBlockIndex < dwFileCount) - Logger.PrintMessage("Unexpected file index of %s", LISTFILE_NAME); - STORM_FREE(pFileData); - } - - pFileData = LoadMpqFile(&Logger, hMpq, ATTRIBUTES_NAME); - if(pFileData != NULL) - { - if(pFileData->dwBlockIndex <= dwFileCount) - Logger.PrintMessage("Unexpected file index of %s", ATTRIBUTES_NAME); - STORM_FREE(pFileData); - } - - // Add new file to the archive. It should be added to the last position - nError = AddFileToMpq(&Logger, hMpq, szReaddedFile, "This is a re-added file.", 0, 0, ERROR_SUCCESS); - if(nError == ERROR_SUCCESS) - { - // Force update of the tables - SFileFlushArchive(hMpq); - - // Load the file - pFileData = LoadMpqFile(&Logger, hMpq, szReaddedFile); - if(pFileData != NULL) - { - if(pFileData->dwBlockIndex != dwFileCount) - Logger.PrintMessage("Unexpected file index of %s", szReaddedFile); - STORM_FREE(pFileData); - } - } - - SFileCloseArchive(hMpq); - } - - return nError; -} - -static int TestCreateArchive_BigArchive(const char * szPlainName) -{ - const char * szFileMask = "AddedFile_%02u.txt"; - TLogHelper Logger("BigMpqTest"); - HANDLE hMpq = NULL; // Handle of created archive - char szLocalFileName[MAX_PATH]; - char szArchivedName[MAX_PATH]; - DWORD dwMaxFileCount = 0x20; - DWORD dwAddedCount = 0; - size_t i; - int nError; - - // Create a new archive with the limit of 0x20 files - nError = CreateNewArchive(&Logger, szPlainName, MPQ_CREATE_ARCHIVE_V3 | MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES, dwMaxFileCount, &hMpq); - if(nError == ERROR_SUCCESS) - { - // Now add few really big files - CreateFullPathName(szLocalFileName, szMpqSubDir, "MPQ_1997_v1_Diablo1_DIABDAT.MPQ"); - Logger.UserTotal = (dwMaxFileCount / 2); - - for(i = 0; i < dwMaxFileCount / 2; i++) - { - sprintf(szArchivedName, szFileMask, i + 1); - nError = AddLocalFileToMpq(&Logger, hMpq, szArchivedName, szLocalFileName, 0, 0, true); - if(nError != ERROR_SUCCESS) - break; - - Logger.UserCount++; - dwAddedCount++; - } - } - - // Close the archive - if(hMpq != NULL) - SFileCloseArchive(hMpq); - hMpq = NULL; - - // Reopen the archive to catch any asserts - if(nError == ERROR_SUCCESS) - nError = OpenExistingArchiveWithCopy(&Logger, NULL, szPlainName, &hMpq); - - // Check that (listfile) is at the end - if(nError == ERROR_SUCCESS) - { - CheckIfFileIsPresent(&Logger, hMpq, LISTFILE_NAME, true); - CheckIfFileIsPresent(&Logger, hMpq, ATTRIBUTES_NAME, true); - - SFileCloseArchive(hMpq); - } - - return nError; -} - -// "MPQ_2014_v4_Heroes_Replay.MPQ", "AddFile-replay.message.events" -static int TestModifyArchive_ReplaceFile(const char * szMpqPlainName, const char * szFileName) -{ - TLogHelper Logger("ModifyTest"); - HANDLE hMpq = NULL; - const char * szArchivedName; - char szLocalFileName[MAX_PATH]; - size_t nOffset = 0; - int nError; - - // Open an existing archive - nError = OpenExistingArchiveWithCopy(&Logger, szMpqPlainName, szMpqPlainName, &hMpq); - - // Add the given file - if(nError == ERROR_SUCCESS) - { - // Get the name of archived file - if(!_strnicmp(szFileName, "AddFile-", 8)) - nOffset = 8; - szArchivedName = szFileName + nOffset; - - // Create the local file name - CreateFullPathName(szLocalFileName, szMpqSubDir, szFileName); - - // Add the file to MPQ - nError = AddLocalFileToMpq(&Logger, hMpq, - szArchivedName, - szLocalFileName, - MPQ_FILE_REPLACEEXISTING | MPQ_FILE_COMPRESS | MPQ_FILE_SINGLE_UNIT, - MPQ_COMPRESSION_ZLIB, - true); - } - - // Reopen the MPQ and compact it - if(nError == ERROR_SUCCESS) - { - // Compact the archive - Logger.PrintProgress("Compacting archive %s ...", szMpqPlainName); - if(!SFileSetCompactCallback(hMpq, CompactCallback, &Logger)) - nError = Logger.PrintError("Failed to compact archive %s", szMpqPlainName); - - if(!SFileCompactArchive(hMpq, NULL, 0)) - nError = GetLastError(); - - SFileCloseArchive(hMpq); - } - - // Try to open the archive again - if(nError == ERROR_SUCCESS) - { - CreateFullPathName(szLocalFileName, NULL, szMpqPlainName); - nError = OpenExistingArchive(&Logger, szLocalFileName, 0, &hMpq); - if(nError == ERROR_SUCCESS) - SFileCloseArchive(hMpq); - } - - return nError; -} - -//----------------------------------------------------------------------------- -// Comparing two directories, creating links - -#define LINK_COMPARE_BLOCK_SIZE 0x200 - -static int CreateArchiveLinkFile(const char * szFullPath1, const char * szFullPath2, const char * szFileHash) -{ - TFileStream * pStream; - char szLinkData[MAX_PATH + 0x80]; - char szLinkFile[MAX_PATH]; - char szLinkPath[MAX_PATH]; - int nLength; - int nError = ERROR_SUCCESS; - - // Construct the link file name - CalculateRelativePath(szFullPath1, szFullPath2, szLinkPath); - sprintf(szLinkFile, "%s.link", szFullPath2); - - // Format the content of the link file - nLength = sprintf(szLinkData, "LINK:%s\x0D\x0ASHA1:%s", szLinkPath, szFileHash); - - // Create the link file - pStream = FileStream_CreateFileA(szLinkFile, 0); - if(pStream == NULL) - return GetLastError(); - - // Write the content of the link file - if(!FileStream_Write(pStream, NULL, szLinkData, (DWORD)nLength)) - nError = GetLastError(); - - FileStream_Close(pStream); - return ERROR_SUCCESS; -} - -static int ForEachFile_CreateArchiveLink(const char * szFullPath1, const char * szFullPath2) -{ - TLogHelper Logger("CreateMpqLink", GetShortPlainName(szFullPath2)); - char szFileHash1[0x40]; - char szFileHash2[0x40]; - int nError; - - // Prevent logger from witing any result messages - Logger.bDontPrintResult = true; - - // Create SHA1 of both files - nError = CalculateFileSha1(&Logger, szFullPath1, szFileHash1); - if(nError == ERROR_SUCCESS) - { - nError = CalculateFileSha1(&Logger, szFullPath2, szFileHash2); - if(nError == ERROR_SUCCESS) - { - // If the hashes are identical, we can create link - if(!strcmp(szFileHash1, szFileHash2)) - { - nError = CreateArchiveLinkFile(szFullPath1, szFullPath2, szFileHash1); - if(nError == ERROR_SUCCESS) - { - Logger.PrintMessage("Created link to %s", szFullPath2); - } - } - } - } - - return ERROR_SUCCESS; -} - -//----------------------------------------------------------------------------- -// Main - -int main(int argc, char * argv[]) -{ - int nError = ERROR_SUCCESS; - -#if defined(_MSC_VER) && defined(_DEBUG) - _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); -#endif // defined(_MSC_VER) && defined(_DEBUG) - - // Initialize storage and mix the random number generator - printf("==== Test Suite for StormLib version %s ====\n", STORMLIB_VERSION_STRING); - nError = InitializeMpqDirectory(argv, argc); - - // Not a test, but rather a tool for creating links to duplicated files -// if(nError == ERROR_SUCCESS) -// nError = FindFilePairs(ForEachFile_CreateArchiveLink, "2004 - WoW\\06080", "2004 - WoW\\06299"); -/* - // Search all testing archives and verify their SHA1 hash - if(nError == ERROR_SUCCESS) - nError = FindFiles(ForEachFile_VerifyFileChecksum, szMpqSubDir); - - // Test sparse compression - if(nError == ERROR_SUCCESS) - nError = TestSparseCompression(); - - // Test reading linear file without bitmap - if(nError == ERROR_SUCCESS) - nError = TestFileStreamOperations("MPQ_2013_v4_alternate-original.MPQ", 0); - - // Test reading linear file without bitmap (read only) - if(nError == ERROR_SUCCESS) - nError = TestFileStreamOperations("MPQ_2013_v4_alternate-original.MPQ", STREAM_FLAG_READ_ONLY); - - // Test reading linear file with bitmap - if(nError == ERROR_SUCCESS) - nError = TestFileStreamOperations("MPQ_2013_v4_alternate-complete.MPQ", STREAM_FLAG_USE_BITMAP); - - // Test reading partial file - if(nError == ERROR_SUCCESS) - nError = TestFileStreamOperations("part-file://MPQ_2009_v2_WoW_patch.MPQ.part", 0); - - // Test reading Block4K file - if(nError == ERROR_SUCCESS) - nError = TestFileStreamOperations("blk4-file://streaming/model.MPQ.0", STREAM_PROVIDER_BLOCK4); - - // Test reading encrypted file - if(nError == ERROR_SUCCESS) - nError = TestFileStreamOperations("mpqe-file://MPQ_2011_v2_EncryptedMpq.MPQE", STREAM_PROVIDER_MPQE); - - // Open a stream, paired with local master. The mirror file is created new - if(nError == ERROR_SUCCESS) - nError = TestReadFile_MasterMirror("part-file://MPQ_2009_v1_patch-created.MPQ.part", "MPQ_2009_v1_patch-original.MPQ", false); - - // Open a stream, paired with local master. Only part of the mirror exists - if(nError == ERROR_SUCCESS) - nError = TestReadFile_MasterMirror("part-file://MPQ_2009_v1_patch-partial.MPQ.part", "MPQ_2009_v1_patch-original.MPQ", true); - - // Open a stream, paired with local master. Only part of the mirror exists - if(nError == ERROR_SUCCESS) - nError = TestReadFile_MasterMirror("part-file://MPQ_2009_v1_patch-complete.MPQ.part", "MPQ_2009_v1_patch-original.MPQ", true); - - // Open a stream, paired with local master - if(nError == ERROR_SUCCESS) - nError = TestReadFile_MasterMirror("MPQ_2013_v4_alternate-created.MPQ", "MPQ_2013_v4_alternate-original.MPQ", false); - - // Open a stream, paired with local master - if(nError == ERROR_SUCCESS) - nError = TestReadFile_MasterMirror("MPQ_2013_v4_alternate-incomplete.MPQ", "MPQ_2013_v4_alternate-incomplete.MPQ", true); - - // Open a stream, paired with local master - if(nError == ERROR_SUCCESS) - nError = TestReadFile_MasterMirror("MPQ_2013_v4_alternate-complete.MPQ", "MPQ_2013_v4_alternate-original.MPQ", true); - - // Open a stream, paired with remote master (takes hell lot of time!) -// if(nError == ERROR_SUCCESS) -// nError = TestReadFile_MasterMirror("MPQ_2013_v4_alternate-downloaded.MPQ", "http://www.zezula.net\\mpqs\\alternate.zip", false); - - // Search in listfile - if(nError == ERROR_SUCCESS) - nError = TestSearchListFile("ListFile_Blizzard.txt"); - - // Test opening local file with SFileOpenFileEx - if(nError == ERROR_SUCCESS) - nError = TestOpenLocalFile("ListFile_Blizzard.txt"); - - // Test working with an archive that has no listfile - if(nError == ERROR_SUCCESS) - nError = TestOpenFile_OpenById("MPQ_1997_v1_Diablo1_DIABDAT.MPQ"); - - // Open an empty archive (found in WoW cache - it's just a header) - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("MPQ_2012_v2_EmptyMpq.MPQ"); - - // Open an empty archive (created artificially - it's just a header) - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("MPQ_2013_v4_EmptyMpq.MPQ"); - - // Open an empty archive (found in WoW cache - it's just a header) - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("MPQ_2013_v4_patch-base-16357.MPQ"); - - // Open an empty archive (A buggy MPQ with invalid HET entry count) - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("MPQ_2011_v4_InvalidHetEntryCount.MPQ"); - - // Open a truncated archive - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("MPQ_2002_v1_BlockTableCut.MPQ"); - - // Open a MPQ that actually has user data - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("MPQ_2010_v2_HasUserData.s2ma"); - - // Open an Warcraft III map whose "(attributes)" file has (BlockTableSize-1) entries - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("MPQ_2014_v1_AttributesOneEntryLess.w3x"); - - // Open a MPQ archive v 3.0 - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("MPQ_2010_v3_expansion-locale-frFR.MPQ"); - - // Open an encrypted archive from Starcraft II installer - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("mpqe-file://MPQ_2011_v2_EncryptedMpq.MPQE"); - - // Open a MPK archive from Longwu online - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("MPx_2013_v1_LongwuOnline.mpk"); - - // Open a SQP archive from War of the Immortals - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("MPx_2013_v1_WarOfTheImmortals.sqp", "ListFile_WarOfTheImmortals.txt"); - - // Open a partial MPQ with compressed hash table - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("part-file://MPQ_2010_v2_HashTableCompressed.MPQ.part"); - - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("MPQ_2002_v1_ProtectedMap_HashTable_FakeValid.w3x"); - - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("MPQ_2002_v1_ProtectedMap_InvalidUserData.w3x"); - - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("MPQ_2002_v1_ProtectedMap_InvalidMpqFormat.w3x"); - - // Open an Warcraft III map locked by the Spazzler protector - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("MPQ_2002_v1_ProtectedMap_Spazzler.w3x"); - - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("MPQ_2014_v1_ProtectedMap_Spazzler2.w3x"); - - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("MPQ_2014_v1_ProtectedMap_Spazzler3.w3x"); - - // Open an Warcraft III map locked by the BOBA protector - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("MPQ_2002_v1_ProtectedMap_BOBA.w3m"); - - // Open an Warcraft III map locked by a protector - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("MPQ_2015_v1_ProtectedMap_KangTooJee.w3x"); - - // Open an Warcraft III map locked by a protector - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("MPQ_2015_v1_ProtectedMap_Somj2hM16.w3x"); - - // Open an Warcraft III map locked by Spazy protector - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("MPQ_2015_v1_ProtectedMap_Spazy.w3x"); - - // Open an protected map - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("MPQ_2015_v1_flem1.w3x"); - - // Open the multi-file archive with wrong prefix to see how StormLib deals with it - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_WillFail("flat-file://streaming/model.MPQ.0"); - - // Open an archive that is merged with multiple files - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive("blk4-file://streaming/model.MPQ.0", NULL, true); - - // Open every MPQ that we have in the storage - if(nError == ERROR_SUCCESS) - nError = FindFiles(ForEachFile_OpenArchive, NULL); - - // Test on an archive that has been invalidated by extending an old valid MPQ - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_Corrupt("MPQ_2013_vX_Battle.net.MPQ"); - - // Open a patched archive - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_Patched(PatchList_WoW_OldWorld13286, "OldWorld\\World\\Model.blob", 2); - - // Open a patched archive - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_Patched(PatchList_WoW_15050, "World\\Model.blob", 8); - - // Open a patched archive - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_Patched(PatchList_WoW_16965, "DBFilesClient\\BattlePetNPCTeamMember.db2", 0); - - // Open a patched archive - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_Patched(PatchList_SC2_32283, "TriggerLibs\\natives.galaxy", 6); - - // Open a patched archive - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_Patched(PatchList_SC2_34644, "TriggerLibs\\GameData\\GameData.galaxy", 2); - - // Open a patched archive with new format of BSDIFF patch - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_Patched(PatchList_SC2_34644_Maps, "Maps\\Campaign\\THorner03.SC2Map\\BankList.xml", 3); - - // Open a patched archive - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_Patched(PatchList_SC2_32283_enGB, "LocalizedData\\GameHotkeys.txt", 6); - - // Open a patched archive - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_Patched(PatchList_HS_6898_enGB, "Hearthstone_Data\\Managed\\Assembly-Csharp.dll", 10); - - // Check the opening archive for read-only - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_ReadOnly("MPQ_1997_v1_Diablo1_DIABDAT.MPQ", true); - - // Check the opening archive for read-only - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_ReadOnly("MPQ_1997_v1_Diablo1_DIABDAT.MPQ", false); - - // Check the SFileGetFileInfo function - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_GetFileInfo("MPQ_2002_v1_StrongSignature.w3m", "MPQ_2013_v4_SC2_EmptyMap.SC2Map"); - - // Downloadable MPQ archive - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_MasterMirror("part-file://MPQ_2009_v1_patch-partial.MPQ.part", "MPQ_2009_v1_patch-original.MPQ", "world\\Azeroth\\DEADMINES\\PASSIVEDOODADS\\GOBLINMELTINGPOT\\DUST2.BLP", false); - - // Downloadable MPQ archive - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_MasterMirror("MPQ_2013_v4_alternate-downloaded.MPQ", "MPQ_2013_v4_alternate-original.MPQ", "alternate\\DUNGEONS\\TEXTURES\\ICECROWN\\GATE\\jlo_IceC_Floor_Thrown.blp", false); - - // Check archive signature - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_VerifySignature("MPQ_1999_v1_WeakSignature.exe", "War2Patch_202.exe"); - - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_VerifySignature("MPQ_2003_v1_WeakSignatureEmpty.exe", "WoW-1.2.3.4211-enUS-patch.exe"); - - // Check archive signature - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_VerifySignature("MPQ_2002_v1_StrongSignature.w3m", "(10)DustwallowKeys.w3m"); - - // Compact the archive - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_CraftedUserData("MPQ_2010_v3_expansion-locale-frFR.MPQ", "StormLibTest_CraftedMpq1_v3.mpq"); - - // Open a MPQ (add custom user data to it) - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_CraftedUserData("MPQ_2013_v4_SC2_EmptyMap.SC2Map", "StormLibTest_CraftedMpq2_v4.mpq"); - - // Open a MPQ (add custom user data to it) - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_CraftedUserData("MPQ_2013_v4_expansion1.MPQ", "StormLibTest_CraftedMpq3_v4.mpq"); - - if(nError == ERROR_SUCCESS) - nError = TestAddFile_FullTable("MPQ_2014_v1_out1.w3x"); - - if(nError == ERROR_SUCCESS) - nError = TestAddFile_FullTable("MPQ_2014_v1_out2.w3x"); - - // Test modifying file with no (listfile) and no (attributes) - if(nError == ERROR_SUCCESS) - nError = TestAddFile_ListFileTest("MPQ_1997_v1_Diablo1_DIABDAT.MPQ", false, false); - - // Test modifying an archive that contains (listfile) and (attributes) - if(nError == ERROR_SUCCESS) - nError = TestAddFile_ListFileTest("MPQ_2013_v4_SC2_EmptyMap.SC2Map", true, true); - - // Create an empty archive v2 - if(nError == ERROR_SUCCESS) - nError = TestCreateArchive_EmptyMpq("StormLibTest_EmptyMpq_v2.mpq", MPQ_CREATE_ARCHIVE_V2 | MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES); - - // Create an empty archive v4 - if(nError == ERROR_SUCCESS) - nError = TestCreateArchive_EmptyMpq("StormLibTest_EmptyMpq_v4.mpq", MPQ_CREATE_ARCHIVE_V4 | MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES); - - // Test creating of an archive the same way like MPQ Editor does - if(nError == ERROR_SUCCESS) - nError = TestCreateArchive_TestGaps("StormLibTest_GapsTest.mpq"); - - // Sign an existing non-signed archive - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_SignExisting("MPQ_1998_v1_StarDat.mpq"); - - // Open a signed archive, add a file and verify the signature - if(nError == ERROR_SUCCESS) - nError = TestOpenArchive_ModifySigned("MPQ_1999_v1_WeakSignature.exe", "War2Patch_202.exe"); - - // Create new archive and sign it - if(nError == ERROR_SUCCESS) - nError = TestCreateArchive_Signed("MPQ_1999_v1_WeakSigned1.mpq", true); - - if(nError == ERROR_SUCCESS) - nError = TestCreateArchive_Signed("MPQ_1999_v1_WeakSigned2.mpq", false); - - // Test creating of an archive the same way like MPQ Editor does - if(nError == ERROR_SUCCESS) - nError = TestCreateArchive_MpqEditor("StormLibTest_MpqEditorTest.mpq", "AddedFile.exe"); - - // Create an archive and fill it with files up to the max file count - if(nError == ERROR_SUCCESS) - nError = TestCreateArchive_FillArchive("StormLibTest_FileTableFull.mpq", 0); - - // Create an archive and fill it with files up to the max file count - if(nError == ERROR_SUCCESS) - nError = TestCreateArchive_FillArchive("StormLibTest_FileTableFull.mpq", MPQ_CREATE_LISTFILE); - - // Create an archive and fill it with files up to the max file count - if(nError == ERROR_SUCCESS) - nError = TestCreateArchive_FillArchive("StormLibTest_FileTableFull.mpq", MPQ_CREATE_ATTRIBUTES); - - // Create an archive and fill it with files up to the max file count - if(nError == ERROR_SUCCESS) - nError = TestCreateArchive_FillArchive("StormLibTest_FileTableFull.mpq", MPQ_CREATE_ATTRIBUTES | MPQ_CREATE_LISTFILE); - - // Create an archive, and increment max file count several times - if(nError == ERROR_SUCCESS) - nError = TestCreateArchive_IncMaxFileCount("StormLibTest_IncMaxFileCount.mpq"); - - // Create a MPQ archive with UNICODE names - if(nError == ERROR_SUCCESS) - nError = TestCreateArchive_UnicodeNames(); - - // Create a MPQ file, add files with various flags - if(nError == ERROR_SUCCESS) - nError = TestCreateArchive_FileFlagTest("StormLibTest_FileFlagTest.mpq"); - - // Create a MPQ file, add a mono-WAVE file with various compressions - if(nError == ERROR_SUCCESS) - nError = TestCreateArchive_WaveCompressionsTest("StormLibTest_AddWaveMonoTest.mpq", "AddFile-Mono.wav"); - - // Create a MPQ file, add a mono-WAVE with 8 bits per sample file with various compressions - if(nError == ERROR_SUCCESS) - nError = TestCreateArchive_WaveCompressionsTest("StormLibTest_AddWaveMonoBadTest.mpq", "AddFile-MonoBad.wav"); - - // Create a MPQ file, add a stereo-WAVE file with various compressions - if(nError == ERROR_SUCCESS) - nError = TestCreateArchive_WaveCompressionsTest("StormLibTest_AddWaveStereoTest.mpq", "AddFile-Stereo.wav"); - - // Check if the listfile is always created at the end of the file table in the archive - if(nError == ERROR_SUCCESS) - nError = TestCreateArchive_ListFilePos("StormLibTest_ListFilePos.mpq"); - - // Open a MPQ (add custom user data to it) - if(nError == ERROR_SUCCESS) - nError = TestCreateArchive_BigArchive("StormLibTest_BigArchive_v4.mpq"); -*/ - // Test replacing a file with zero size file - if(nError == ERROR_SUCCESS) - nError = TestModifyArchive_ReplaceFile("MPQ_2014_v4_Base.StormReplay", "AddFile-replay.message.events"); - -#ifdef _MSC_VER - _CrtDumpMemoryLeaks(); -#endif // _MSC_VER - - return nError; -} +/*****************************************************************************/ +/* StormTest.cpp Copyright (c) Ladislav Zezula 2003 */ +/*---------------------------------------------------------------------------*/ +/* Test module for StormLib */ +/*---------------------------------------------------------------------------*/ +/* Date Ver Who Comment */ +/* -------- ---- --- ------- */ +/* 25.03.03 1.00 Lad The first version of StormTest.cpp */ +/*****************************************************************************/ + +#define _CRT_NON_CONFORMING_SWPRINTFS +#define _CRT_SECURE_NO_DEPRECATE +#define __INCLUDE_CRYPTOGRAPHY__ +#define __STORMLIB_SELF__ // Don't use StormLib.lib +#include + +#ifdef _MSC_VER +#include +#endif + +#include "../src/StormLib.h" +#include "../src/StormCommon.h" + +#include "TLogHelper.cpp" // Helper class for showing test results + +#ifdef _MSC_VER +#pragma warning(disable: 4505) // 'XXX' : unreferenced local function has been removed +#pragma comment(lib, "winmm.lib") +#endif + +#ifdef PLATFORM_LINUX +#include +#endif + +//------------------------------------------------------------------------------ +// Defines + +#ifdef PLATFORM_WINDOWS +#define WORK_PATH_ROOT "E:\\Multimedia\\MPQs" +#endif + +#ifdef PLATFORM_LINUX +#define WORK_PATH_ROOT "/home/ladik/MPQs" +#endif + +#ifdef PLATFORM_MAC +#define WORK_PATH_ROOT "/Users/sam/StormLib/test" +#endif + +// Global for the work MPQ +static const char * szMpqSubDir = "1995 - Test MPQs"; +static const char * szMpqPatchDir = "1995 - Test MPQs\\patches"; + +typedef int (*FIND_FILE_CALLBACK)(const char * szFullPath); +typedef int (*FIND_PAIR_CALLBACK)(const char * szFullPath1, const char * szFullPath2); + +#define ERROR_UNDETERMINED_RESULT 0xC000FFFF + +//----------------------------------------------------------------------------- +// Testing data + +static DWORD AddFlags[] = +{ +// Compression Encryption Fixed key Single Unit Sector CRC + 0 | 0 | 0 | 0 | 0, + 0 | MPQ_FILE_ENCRYPTED | 0 | 0 | 0, + 0 | MPQ_FILE_ENCRYPTED | MPQ_FILE_FIX_KEY | 0 | 0, + 0 | 0 | 0 | MPQ_FILE_SINGLE_UNIT | 0, + 0 | MPQ_FILE_ENCRYPTED | 0 | MPQ_FILE_SINGLE_UNIT | 0, + 0 | MPQ_FILE_ENCRYPTED | MPQ_FILE_FIX_KEY | MPQ_FILE_SINGLE_UNIT | 0, + MPQ_FILE_IMPLODE | 0 | 0 | 0 | 0, + MPQ_FILE_IMPLODE | MPQ_FILE_ENCRYPTED | 0 | 0 | 0, + MPQ_FILE_IMPLODE | MPQ_FILE_ENCRYPTED | MPQ_FILE_FIX_KEY | 0 | 0, + MPQ_FILE_IMPLODE | 0 | 0 | MPQ_FILE_SINGLE_UNIT | 0, + MPQ_FILE_IMPLODE | MPQ_FILE_ENCRYPTED | 0 | MPQ_FILE_SINGLE_UNIT | 0, + MPQ_FILE_IMPLODE | MPQ_FILE_ENCRYPTED | MPQ_FILE_FIX_KEY | MPQ_FILE_SINGLE_UNIT | 0, + MPQ_FILE_IMPLODE | 0 | 0 | 0 | MPQ_FILE_SECTOR_CRC, + MPQ_FILE_IMPLODE | MPQ_FILE_ENCRYPTED | 0 | 0 | MPQ_FILE_SECTOR_CRC, + MPQ_FILE_IMPLODE | MPQ_FILE_ENCRYPTED | MPQ_FILE_FIX_KEY | 0 | MPQ_FILE_SECTOR_CRC, + MPQ_FILE_COMPRESS | 0 | 0 | 0 | 0, + MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | 0 | 0 | 0, + MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_FIX_KEY | 0 | 0, + MPQ_FILE_COMPRESS | 0 | 0 | MPQ_FILE_SINGLE_UNIT | 0, + MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | 0 | MPQ_FILE_SINGLE_UNIT | 0, + MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_FIX_KEY | MPQ_FILE_SINGLE_UNIT | 0, + MPQ_FILE_COMPRESS | 0 | 0 | 0 | MPQ_FILE_SECTOR_CRC, + MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | 0 | 0 | MPQ_FILE_SECTOR_CRC, + MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_FIX_KEY | 0 | MPQ_FILE_SECTOR_CRC, + 0xFFFFFFFF +}; + +static DWORD WaveCompressions[] = +{ + MPQ_COMPRESSION_ADPCM_MONO | MPQ_COMPRESSION_HUFFMANN, + MPQ_COMPRESSION_ADPCM_STEREO | MPQ_COMPRESSION_HUFFMANN, + MPQ_COMPRESSION_PKWARE, + MPQ_COMPRESSION_ZLIB, + MPQ_COMPRESSION_BZIP2 +}; + +static const wchar_t szUnicodeName1[] = { // Czech + 0x010C, 0x0065, 0x0073, 0x006B, 0x00FD, _T('.'), _T('m'), _T('p'), _T('q'), 0 +}; + +static const wchar_t szUnicodeName2[] = { // Russian + 0x0420, 0x0443, 0x0441, 0x0441, 0x043A, 0x0438, 0x0439, _T('.'), _T('m'), _T('p'), _T('q'), 0 +}; + +static const wchar_t szUnicodeName3[] = { // Greek + 0x03B5, 0x03BB, 0x03BB, 0x03B7, 0x03BD, 0x03B9, 0x03BA, 0x03AC, _T('.'), _T('m'), _T('p'), _T('q'), 0 +}; + +static const wchar_t szUnicodeName4[] = { // Chinese + 0x65E5, 0x672C, 0x8A9E, _T('.'), _T('m'), _T('p'), _T('q'), 0 +}; + +static const wchar_t szUnicodeName5[] = { // Japanese + 0x7B80, 0x4F53, 0x4E2D, 0x6587, _T('.'), _T('m'), _T('p'), _T('q'), 0 +}; + +static const wchar_t szUnicodeName6[] = { // Arabic + 0x0627, 0x0644, 0x0639, 0x0639, 0x0631, 0x0628, 0x064A, 0x0629, _T('.'), _T('m'), _T('p'), _T('q'), 0 +}; + +static const char * PatchList_WoW_OldWorld13286[] = +{ + "MPQ_2012_v4_OldWorld.MPQ", + "wow-update-oldworld-13154.MPQ", + "wow-update-oldworld-13286.MPQ", + NULL +}; + +static const char * PatchList_WoW_15050[] = +{ + "MPQ_2013_v4_world.MPQ", + "wow-update-13164.MPQ", + "wow-update-13205.MPQ", + "wow-update-13287.MPQ", + "wow-update-13329.MPQ", + "wow-update-13596.MPQ", + "wow-update-13623.MPQ", + "wow-update-base-13914.MPQ", + "wow-update-base-14007.MPQ", + "wow-update-base-14333.MPQ", + "wow-update-base-14480.MPQ", + "wow-update-base-14545.MPQ", + "wow-update-base-14946.MPQ", + "wow-update-base-15005.MPQ", + "wow-update-base-15050.MPQ", + NULL +}; + +static const char * PatchList_WoW_16965[] = +{ + "MPQ_2013_v4_locale-enGB.MPQ", + "wow-update-enGB-16016.MPQ", + "wow-update-enGB-16048.MPQ", + "wow-update-enGB-16057.MPQ", + "wow-update-enGB-16309.MPQ", + "wow-update-enGB-16357.MPQ", + "wow-update-enGB-16516.MPQ", + "wow-update-enGB-16650.MPQ", + "wow-update-enGB-16844.MPQ", + "wow-update-enGB-16965.MPQ", + NULL +}; + +static const char * PatchList_SC2_32283[] = +{ + "MPQ_2013_v4_Base1.SC2Data", + "s2-update-base-23258.MPQ", + "s2-update-base-24540.MPQ", + "s2-update-base-26147.MPQ", + "s2-update-base-28522.MPQ", + "s2-update-base-30508.MPQ", + "s2-update-base-32283.MPQ", + NULL +}; + +static const char * PatchList_SC2_34644[] = +{ + "MPQ_2013_v4_Base1.SC2Data", + "s2-update-base-23258.MPQ", + "s2-update-base-24540.MPQ", + "s2-update-base-26147.MPQ", + "s2-update-base-28522.MPQ", + "s2-update-base-32384.MPQ", + "s2-update-base-34644.MPQ", + NULL +}; + +static const char * PatchList_SC2_34644_Maps[] = +{ + "MPQ_2013_v4_Base3.SC2Maps", + "s2-update-base-23258.MPQ", + "s2-update-base-24540.MPQ", + "s2-update-base-26147.MPQ", + "s2-update-base-28522.MPQ", + "s2-update-base-32384.MPQ", + "s2-update-base-34644.MPQ", + NULL +}; + +static const char * PatchList_SC2_32283_enGB[] = +{ + "MPQ_2013_v4_enGB.SC2Data", + "s2-update-enGB-23258.MPQ", + "s2-update-enGB-24540.MPQ", + "s2-update-enGB-26147.MPQ", + "s2-update-enGB-28522.MPQ", + "s2-update-enGB-30508.MPQ", + "s2-update-enGB-32283.MPQ", + NULL +}; + +static const char * PatchList_HS_6898_enGB[] = +{ + "MPQ_2014_v4_base-Win.MPQ", + "hs-0-5314-Win-final.MPQ", + "hs-5314-5435-Win-final.MPQ", + "hs-5435-5506-Win-final.MPQ", + "hs-5506-5834-Win-final.MPQ", + "hs-5834-6024-Win-final.MPQ", + "hs-6024-6141-Win-final.MPQ", + "hs-6141-6187-Win-final.MPQ", + "hs-6187-6284-Win-final.MPQ", + "hs-6284-6485-Win-final.MPQ", + "hs-6485-6898-Win-final.MPQ", + NULL +}; + +//----------------------------------------------------------------------------- +// Local file functions + +// Definition of the path separator +#ifdef PLATFORM_WINDOWS +#define PATH_SEPARATOR '\\' // Path separator for Windows platforms +#else +#define PATH_SEPARATOR '/' // Path separator for Windows platforms +#endif + +// This must be the directory where our test MPQs are stored. +// We also expect a subdirectory named +static char szMpqDirectory[MAX_PATH+1]; +size_t cchMpqDirectory = 0; + +static bool IsFullPath(const char * szFileName) +{ +#ifdef PLATFORM_WINDOWS + if(('A' <= szFileName[0] && szFileName[0] <= 'Z') || ('a' <= szFileName[0] && szFileName[0] <= 'z')) + { + return (szFileName[1] == ':' && szFileName[2] == PATH_SEPARATOR); + } +#endif + + szFileName = szFileName; + return false; +} + +static bool IsMpqExtension(const char * szFileName) +{ + const char * szExtension = strrchr(szFileName, '.'); + + if(szExtension != NULL) + { + if(!_stricmp(szExtension, ".mpq")) + return true; + if(!_stricmp(szExtension, ".w3m")) + return true; + if(!_stricmp(szExtension, ".w3x")) + return true; + if(!_stricmp(szExtension, ".mpqe")) + return true; + if(!_stricmp(szExtension, ".part")) + return true; + if(!_stricmp(szExtension, ".sv")) + return true; + if(!_stricmp(szExtension, ".s2ma")) + return true; + if(!_stricmp(szExtension, ".SC2Map")) + return true; + if(!_stricmp(szExtension, ".0")) // .MPQ.0 + return true; +// if(!_stricmp(szExtension, ".link")) +// return true; + } + + return false; +} + +static void AddStringBeforeExtension(char * szBuffer, const char * szFileName, const char * szExtraString) +{ + const char * szExtension; + size_t nLength; + + // Get the extension + szExtension = strrchr(szFileName, '.'); + if(szExtension == NULL) + szExtension = szFileName + strlen(szFileName); + nLength = (size_t)(szExtension - szFileName); + + // Copy the part before extension + memcpy(szBuffer, szFileName, nLength); + szFileName += nLength; + szBuffer += nLength; + + // Append the extra data + if(szExtraString != NULL) + strcpy(szBuffer, szExtraString); + + // Append the rest of the file name + strcat(szBuffer, szFileName); +} + +static bool CompareBlocks(LPBYTE pbBlock1, LPBYTE pbBlock2, DWORD dwLength, DWORD * pdwDifference) +{ + for(DWORD i = 0; i < dwLength; i++) + { + if(pbBlock1[i] != pbBlock2[i]) + { + pdwDifference[0] = i; + return false; + } + } + + return true; +} + +static size_t ConvertSha1ToText(const unsigned char * sha1_digest, char * szSha1Text) +{ + const char * szTable = "0123456789abcdef"; + + for(size_t i = 0; i < SHA1_DIGEST_SIZE; i++) + { + *szSha1Text++ = szTable[(sha1_digest[0] >> 0x04)]; + *szSha1Text++ = szTable[(sha1_digest[0] & 0x0F)]; + sha1_digest++; + } + + *szSha1Text = 0; + return (SHA1_DIGEST_SIZE * 2); +} + +static int GetPathSeparatorCount(const char * szPath) +{ + int nSeparatorCount = 0; + + while(szPath[0] != 0) + { + if(szPath[0] == '\\' || szPath[0] == '/') + nSeparatorCount++; + szPath++; + } + + return nSeparatorCount; +} + +static const char * FindNextPathPart(const char * szPath, size_t nPartCount) +{ + const char * szPathPart = szPath; + + while(szPath[0] != 0 && nPartCount > 0) + { + // Is there path separator? + if(szPath[0] == '\\' || szPath[0] == '/') + { + szPathPart = szPath + 1; + nPartCount--; + } + + // Move to the next letter + szPath++; + } + + return szPathPart; +} + +static const char * GetShortPlainName(const char * szFileName) +{ + const char * szPlainName = FindNextPathPart(szFileName, 1000); + const char * szPlainEnd = szFileName + strlen(szFileName); + + // If the name is still too long, cut it + if((szPlainEnd - szPlainName) > 50) + szPlainName = szPlainEnd - 50; + + return szPlainName; +} + +static void CopyPathPart(char * szBuffer, const char * szPath) +{ + while(szPath[0] != 0) + { + szBuffer[0] = (szPath[0] == '\\' || szPath[0] == '/') ? '/' : szPath[0]; + szBuffer++; + szPath++; + } + + *szBuffer = 0; +} + +static bool CopyStringAndVerifyConversion( + const TCHAR * szFoundFile, + TCHAR * szBufferT, + char * szBufferA) +{ + // Convert the TCHAR name to ANSI name + CopyFileName(szBufferA, szFoundFile, _tcslen(szFoundFile)); + CopyFileName(szBufferT, szBufferA, strlen(szBufferA)); + + // Compare both TCHAR strings + return (_tcsicmp(szBufferT, szFoundFile) == 0) ? true : false; +} + +static void CalculateRelativePath(const char * szFullPath1, const char * szFullPath2, char * szBuffer) +{ + const char * szPathPart1 = szFullPath1; + const char * szPathPart2 = szFullPath2; + const char * szNextPart1; + const char * szNextPart2; + int nEqualParts = 0; + int nStepsUp = 0; + + // Parse both paths and find all path parts that are equal + for(;;) + { + // Find the next part of the first path + szNextPart1 = FindNextPathPart(szPathPart1, 1); + if(szNextPart1 == szPathPart1) + break; + + szNextPart2 = FindNextPathPart(szPathPart2, 1); + if(szNextPart2 == szPathPart2) + break; + + // Are these equal? + if((szNextPart2 - szPathPart2) != (szNextPart1 - szPathPart1)) + break; + if(_strnicmp(szPathPart1, szPathPart2, (szNextPart1 - szPathPart1 - 1))) + break; + + // Increment the number of path parts that are equal + szPathPart1 = szNextPart1; + szPathPart2 = szNextPart2; + nEqualParts++; + } + + // If we found at least one equal part, we can create relative path + if(nEqualParts != 0) + { + // Calculate how many steps up we need to go + nStepsUp = GetPathSeparatorCount(szPathPart2); + + // Append "../" nStepsUp-times + for(int i = 0; i < nStepsUp; i++) + { + *szBuffer++ = '.'; + *szBuffer++ = '.'; + *szBuffer++ = '/'; + } + + // Append the rest of the path. Also change DOS backslashes to slashes + CopyPathPart(szBuffer, szPathPart1); + return; + } + + // Failed. Just copy the source path as it is + strcpy(szBuffer, szFullPath1); +} + +static TFileStream * FileStream_OpenFileA(const char * szFileName, DWORD dwStreamFlags) +{ + TCHAR szFileNameT[MAX_PATH]; + + CopyFileName(szFileNameT, szFileName, strlen(szFileName)); + return FileStream_OpenFile(szFileNameT, dwStreamFlags); +} + +static TFileStream * FileStream_CreateFileA(const char * szFileName, DWORD dwStreamFlags) +{ + TCHAR szFileNameT[MAX_PATH]; + + CopyFileName(szFileNameT, szFileName, strlen(szFileName)); + return FileStream_CreateFile(szFileNameT, dwStreamFlags); +} + +static size_t FileStream_PrefixA(const char * szFileName, DWORD * pdwProvider) +{ + TCHAR szFileNameT[MAX_PATH]; + size_t nPrefixLength = 0; + + if(szFileName != NULL) + { + CopyFileName(szFileNameT, szFileName, strlen(szFileName)); + nPrefixLength = FileStream_Prefix(szFileNameT, pdwProvider); + } + + return nPrefixLength; +} + +static void CreateFullPathName(char * szBuffer, const char * szSubDir, const char * szNamePart1, const char * szNamePart2 = NULL) +{ + char * szSaveBuffer = szBuffer; + size_t nPrefixLength = 0; + size_t nLength; + DWORD dwProvider = 0; + bool bIsFullPath = false; + char chSeparator = PATH_SEPARATOR; + + // Determine the path prefix + if(szNamePart1 != NULL) + { + nPrefixLength = FileStream_PrefixA(szNamePart1, &dwProvider); + if((dwProvider & BASE_PROVIDER_MASK) == BASE_PROVIDER_HTTP) + { + bIsFullPath = true; + chSeparator = '/'; + } + else + bIsFullPath = IsFullPath(szNamePart1 + nPrefixLength); + } + + // Copy the MPQ prefix, if any + if(nPrefixLength > 0) + { + memcpy(szBuffer, szNamePart1, nPrefixLength); + szSaveBuffer += nPrefixLength; + szNamePart1 += nPrefixLength; + szBuffer += nPrefixLength; + } + + // If the given name is not a full path, copy the MPQ directory + if(bIsFullPath == false) + { + // Copy the master MPQ directory + memcpy(szBuffer, szMpqDirectory, cchMpqDirectory); + szBuffer += cchMpqDirectory; + + // Append the subdirectory, if any + if(szSubDir != NULL && (nLength = strlen(szSubDir)) != 0) + { + // No leading or trailing separator are allowed + assert(szSubDir[0] != '/' && szSubDir[0] != '\\'); + assert(szSubDir[nLength - 1] != '/' && szSubDir[nLength - 1] != '\\'); + + // Append file path separator + *szBuffer++ = PATH_SEPARATOR; + + // Append the subdirectory + memcpy(szBuffer, szSubDir, nLength); + szBuffer += nLength; + } + } + + // Copy the file name, if any + if(szNamePart1 != NULL && (nLength = strlen(szNamePart1)) != 0) + { + // Path separators are not allowed in the name part + assert(szNamePart1[0] != '\\' && szNamePart1[0] != '/'); + assert(szNamePart1[nLength - 1] != '/' && szNamePart1[nLength - 1] != '\\'); + + // Append file path separator + if(bIsFullPath == false) + *szBuffer++ = PATH_SEPARATOR; + + // Copy the file name + memcpy(szBuffer, szNamePart1, nLength); + szBuffer += nLength; + } + + // Append the second part of the name + if(szNamePart2 != NULL && (nLength = strlen(szNamePart2)) != 0) + { + // Copy the file name + memcpy(szBuffer, szNamePart2, nLength); + szBuffer += nLength; + } + + // Normalize the path separators + while(szSaveBuffer < szBuffer) + { + szSaveBuffer[0] = (szSaveBuffer[0] != '/' && szSaveBuffer[0] != '\\') ? szSaveBuffer[0] : chSeparator; + szSaveBuffer++; + } + + // Terminate the buffer with zero + *szBuffer = 0; +} + +static int CalculateFileSha1(TLogHelper * pLogger, const char * szFullPath, char * szFileSha1) +{ + TFileStream * pStream; + unsigned char sha1_digest[SHA1_DIGEST_SIZE]; + const char * szShortPlainName = GetShortPlainName(szFullPath); + hash_state sha1_state; + ULONGLONG ByteOffset = 0; + ULONGLONG FileSize = 0; + BYTE * pbFileBlock; + DWORD cbBytesToRead; + DWORD cbFileBlock = 0x100000; + int nError = ERROR_SUCCESS; + + // Notify the user + pLogger->PrintProgress("Hashing file %s", szShortPlainName); + szFileSha1[0] = 0; + + // Open the file to be verified + pStream = FileStream_OpenFileA(szFullPath, STREAM_FLAG_READ_ONLY); + if(pStream != NULL) + { + // Retrieve the size of the file + FileStream_GetSize(pStream, &FileSize); + + // Allocate the buffer for loading file parts + pbFileBlock = STORM_ALLOC(BYTE, cbFileBlock); + if(pbFileBlock != NULL) + { + // Initialize SHA1 calculation + sha1_init(&sha1_state); + + // Calculate the SHA1 of the file + while(ByteOffset < FileSize) + { + // Notify the user + pLogger->PrintProgress("Hashing file %s (%I64u of %I64u)", szShortPlainName, ByteOffset, FileSize); + + // Load the file block + cbBytesToRead = ((FileSize - ByteOffset) > cbFileBlock) ? cbFileBlock : (DWORD)(FileSize - ByteOffset); + if(!FileStream_Read(pStream, &ByteOffset, pbFileBlock, cbBytesToRead)) + { + nError = GetLastError(); + break; + } + + // Add to SHA1 + sha1_process(&sha1_state, pbFileBlock, cbBytesToRead); + ByteOffset += cbBytesToRead; + } + + // Notify the user + pLogger->PrintProgress("Hashing file %s (%I64u of %I64u)", szShortPlainName, ByteOffset, FileSize); + + // Finalize SHA1 + sha1_done(&sha1_state, sha1_digest); + + // Convert the SHA1 to ANSI text + ConvertSha1ToText(sha1_digest, szFileSha1); + STORM_FREE(pbFileBlock); + } + + FileStream_Close(pStream); + } + + // If we calculated something, return OK + if(nError == ERROR_SUCCESS && szFileSha1[0] == 0) + nError = ERROR_CAN_NOT_COMPLETE; + return nError; +} + +//----------------------------------------------------------------------------- +// Directory search + +static HANDLE InitDirectorySearch(const char * szDirectory) +{ +#ifdef PLATFORM_WINDOWS + + WIN32_FIND_DATA wf; + HANDLE hFind; + TCHAR szSearchMask[MAX_PATH]; + + // Keep compilers happy + CopyFileName(szSearchMask, szDirectory, strlen(szDirectory)); + _tcscat(szSearchMask, _T("\\*")); + + // Construct the directory mask + hFind = FindFirstFile(szSearchMask, &wf); + return (hFind != INVALID_HANDLE_VALUE) ? hFind : NULL; + +#endif + +#ifdef PLATFORM_LINUX + + // Keep compilers happy + return (HANDLE)opendir(szDirectory); + +#endif +} + +static bool SearchDirectory(HANDLE hFind, char * szDirEntry, bool & IsDirectory) +{ +#ifdef PLATFORM_WINDOWS + + WIN32_FIND_DATA wf; + TCHAR szDirEntryT[MAX_PATH]; + char szDirEntryA[MAX_PATH]; + + __SearchNextEntry: + + // Search for the hnext entry. + if(FindNextFile(hFind, &wf)) + { + // Verify if the directory entry is an UNICODE name that would be destroyed + // by Unicode->ANSI->Unicode conversion + if(CopyStringAndVerifyConversion(wf.cFileName, szDirEntryT, szDirEntryA) == false) + goto __SearchNextEntry; + + IsDirectory = (wf.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? true : false; + CopyFileName(szDirEntry, wf.cFileName, _tcslen(wf.cFileName)); + return true; + } + + return false; + +#endif + +#ifdef PLATFORM_LINUX + + struct dirent * directory_entry; + + directory_entry = readdir((DIR *)hFind); + if(directory_entry != NULL) + { + IsDirectory = (directory_entry->d_type == DT_DIR) ? true : false; + strcpy(szDirEntry, directory_entry->d_name); + return true; + } + + return false; + +#endif +} + +static void FreeDirectorySearch(HANDLE hFind) +{ +#ifdef PLATFORM_WINDOWS + FindClose(hFind); +#endif + +#ifdef PLATFORM_LINUX + closedir((DIR *)hFind); +#endif +} + +static int FindFilesInternal(FIND_FILE_CALLBACK pfnTest, char * szDirectory) +{ + HANDLE hFind; + char * szPlainName; + size_t nLength; + char szDirEntry[MAX_PATH]; + bool IsDirectory = false; + int nError = ERROR_SUCCESS; + + if(szDirectory != NULL) + { + // Initiate directory search + hFind = InitDirectorySearch(szDirectory); + if(hFind != NULL) + { + // Append slash at the end of the directory name + nLength = strlen(szDirectory); + szDirectory[nLength++] = PATH_SEPARATOR; + szPlainName = szDirectory + nLength; + + // Skip the first entry, since it's always "." or ".." + while(SearchDirectory(hFind, szDirEntry, IsDirectory) && nError == ERROR_SUCCESS) + { + // Copy the directory entry name to both names + strcpy(szPlainName, szDirEntry); + + // Found a directory? + if(IsDirectory) + { + if(szDirEntry[0] != '.') + { + nError = FindFilesInternal(pfnTest, szDirectory); + } + } + else + { + if(pfnTest != NULL) + { + nError = pfnTest(szDirectory); + } + } + } + + FreeDirectorySearch(hFind); + } + } + + // Free the path buffer, if any + return nError; +} + +static int FindFiles(FIND_FILE_CALLBACK pfnFindFile, const char * szSubDirectory) +{ + char szWorkBuff[MAX_PATH]; + + CreateFullPathName(szWorkBuff, szSubDirectory, NULL); + return FindFilesInternal(pfnFindFile, szWorkBuff); +} + +static int FindFilePairsInternal( + FIND_PAIR_CALLBACK pfnFilePair, + char * szSource, + char * szTarget) +{ + char * szPlainName1; + char * szPlainName2; + int nError = ERROR_SUCCESS; + + // Setup the search masks + strcat(szSource, "\\*"); + szPlainName1 = strrchr(szSource, '*'); + strcat(szTarget, "\\*"); + szPlainName2 = strrchr(szTarget, '*'); + + // If both paths are OK, perform the search + if(szPlainName1 != NULL && szPlainName2 != NULL) + { +#ifdef PLATFORM_WINDOWS + WIN32_FIND_DATAA wf; + HANDLE hFind; + + // Search the second directory + hFind = FindFirstFileA(szTarget, &wf); + if(hFind != INVALID_HANDLE_VALUE) + { + // Skip the first entry, since it's always "." or ".." + while(FindNextFileA(hFind, &wf) && nError == ERROR_SUCCESS) + { + // Found a directory? + if(wf.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + if(wf.cFileName[0] != '.') + { + strcpy(szPlainName1, wf.cFileName); + strcpy(szPlainName2, wf.cFileName); + nError = FindFilePairsInternal(pfnFilePair, szSource, szTarget); + } + } + else + { + if(pfnFilePair != NULL) + { + strcpy(szPlainName1, wf.cFileName); + strcpy(szPlainName2, wf.cFileName); + nError = pfnFilePair(szSource, szTarget); + } + } + } + + FindClose(hFind); + } +#endif + } + + return nError; +} + +static int FindFilePairs(FIND_PAIR_CALLBACK pfnFindPair, const char * szSourceSubDir, const char * szTargetSubDir) +{ + char szSource[MAX_PATH]; + char szTarget[MAX_PATH]; + + // Create the source search mask + CreateFullPathName(szSource, szSourceSubDir, NULL); + CreateFullPathName(szTarget, szTargetSubDir, NULL); + return FindFilePairsInternal(pfnFindPair, szSource, szTarget); +} + +static int InitializeMpqDirectory(char * argv[], int argc) +{ + TLogHelper Logger("InitWorkDir"); + TFileStream * pStream; + const char * szWhereFrom = NULL; + const char * szDirName; + char szFullPath[MAX_PATH]; + + // Retrieve the name of the MPQ directory + if(argc > 1 && argv[1] != NULL) + { + szWhereFrom = "command line"; + szDirName = argv[1]; + } + else + { + szWhereFrom = "default"; + szDirName = WORK_PATH_ROOT; + } + + // Copy the name of the MPQ directory. + StringCopyA(szMpqDirectory, szDirName, MAX_PATH); + cchMpqDirectory = strlen(szMpqDirectory); + + // Cut trailing slashes and/or backslashes + while((cchMpqDirectory > 0) && (szMpqDirectory[cchMpqDirectory - 1] == '/' || szMpqDirectory[cchMpqDirectory - 1] == '\\')) + cchMpqDirectory--; + szMpqDirectory[cchMpqDirectory] = 0; + + // Print the work directory info + Logger.PrintMessage("Work directory %s (%s)", szMpqDirectory, szWhereFrom); + + // Verify if the work MPQ directory is writable + CreateFullPathName(szFullPath, NULL, "TestFile.bin"); + pStream = FileStream_CreateFileA(szFullPath, 0); + if(pStream == NULL) + return Logger.PrintError("MPQ subdirectory doesn't exist or is not writable"); + + // Close the stream + FileStream_Close(pStream); + remove(szFullPath); + + // Verify if the working directory exists and if there is a subdirectory with the file name + CreateFullPathName(szFullPath, szMpqSubDir, "ListFile_Blizzard.txt"); + pStream = FileStream_OpenFileA(szFullPath, STREAM_FLAG_READ_ONLY); + if(pStream == NULL) + return Logger.PrintError("The main listfile (%s) was not found. Check your paths", GetShortPlainName(szFullPath)); + + // Close the stream + FileStream_Close(pStream); + return ERROR_SUCCESS; +} + +static int GetFilePatchCount(TLogHelper * pLogger, HANDLE hMpq, const char * szFileName) +{ + TCHAR * szPatchName; + HANDLE hFile; + TCHAR szPatchChain[0x400]; + int nPatchCount = 0; + int nError = ERROR_SUCCESS; + + // Open the MPQ file + if(SFileOpenFileEx(hMpq, szFileName, 0, &hFile)) + { + // Notify the user + pLogger->PrintProgress("Verifying patch chain for %s ...", GetShortPlainName(szFileName)); + + // Query the patch chain + if(!SFileGetFileInfo(hFile, SFileInfoPatchChain, szPatchChain, sizeof(szPatchChain), NULL)) + nError = pLogger->PrintError("Failed to retrieve the patch chain on %s", szFileName); + + // Is there anything at all in the patch chain? + if(nError == ERROR_SUCCESS && szPatchChain[0] == 0) + { + pLogger->PrintError("The patch chain for %s is empty", szFileName); + nError = ERROR_FILE_CORRUPT; + } + + // Now calculate the number of patches + if(nError == ERROR_SUCCESS) + { + // Get the pointer to the patch + szPatchName = szPatchChain; + + // Skip the base name + for(;;) + { + // Skip the current name + szPatchName = szPatchName + _tcslen(szPatchName) + 1; + if(szPatchName[0] == 0) + break; + + // Increment number of patches + nPatchCount++; + } + } + + SFileCloseFile(hFile); + } + else + { + pLogger->PrintError("Failed to open file %s", szFileName); + } + + return nPatchCount; +} + +static int VerifyFilePatchCount(TLogHelper * pLogger, HANDLE hMpq, const char * szFileName, int nExpectedPatchCount) +{ + int nPatchCount = 0; + + // Retrieve the patch count + pLogger->PrintProgress("Verifying patch count for %s ...", szFileName); + nPatchCount = GetFilePatchCount(pLogger, hMpq, szFileName); + + // Check if there are any patches at all + if(nExpectedPatchCount != 0 && nPatchCount == 0) + { + pLogger->PrintMessage("There are no patches beyond %s", szFileName); + return ERROR_FILE_CORRUPT; + } + + // Check if the number of patches fits + if(nPatchCount != nExpectedPatchCount) + { + pLogger->PrintMessage("Unexpected number of patches for %s", szFileName); + return ERROR_FILE_CORRUPT; + } + + return ERROR_SUCCESS; +} + +static int CreateEmptyFile(TLogHelper * pLogger, const char * szPlainName, ULONGLONG FileSize, char * szBuffer) +{ + TFileStream * pStream; + char szFullPath[MAX_PATH]; + + // Notify the user + pLogger->PrintProgress("Creating empty file %s ...", szPlainName); + + // Construct the full path and crete the file + CreateFullPathName(szFullPath, NULL, szPlainName); + pStream = FileStream_CreateFileA(szFullPath, STREAM_PROVIDER_FLAT | BASE_PROVIDER_FILE); + if(pStream == NULL) + return pLogger->PrintError("Failed to create file %s", szBuffer); + + // Write the required size + FileStream_SetSize(pStream, FileSize); + FileStream_Close(pStream); + + // Give the caller the full file name + if(szBuffer != NULL) + strcpy(szBuffer, szFullPath); + return ERROR_SUCCESS; +} + +static int VerifyFilePosition( + TLogHelper * pLogger, + TFileStream * pStream, + ULONGLONG ExpectedPosition) +{ + ULONGLONG ByteOffset = 0; + int nError = ERROR_SUCCESS; + + // Retrieve the file position + if(FileStream_GetPos(pStream, &ByteOffset)) + { + if(ByteOffset != ExpectedPosition) + { + pLogger->PrintMessage("The file position is different than expected (expected: " I64u_a ", current: " I64u_a, ExpectedPosition, ByteOffset); + nError = ERROR_FILE_CORRUPT; + } + } + else + { + nError = pLogger->PrintError("Failed to retrieve the file offset"); + } + + return nError; +} + +static int VerifyFileMpqHeader(TLogHelper * pLogger, TFileStream * pStream, ULONGLONG * pByteOffset) +{ + TMPQHeader Header; + int nError = ERROR_SUCCESS; + + memset(&Header, 0xFE, sizeof(TMPQHeader)); + if(FileStream_Read(pStream, pByteOffset, &Header, sizeof(TMPQHeader))) + { + if(Header.dwID != ID_MPQ) + { + pLogger->PrintMessage("Read error - the data is not a MPQ header"); + nError = ERROR_FILE_CORRUPT; + } + } + else + { + nError = pLogger->PrintError("Failed to read the MPQ header"); + } + + return nError; +} + +static int WriteMpqUserDataHeader( + TLogHelper * pLogger, + TFileStream * pStream, + ULONGLONG ByteOffset, + DWORD dwByteCount) +{ + TMPQUserData UserData; + int nError = ERROR_SUCCESS; + + // Notify the user + pLogger->PrintProgress("Writing user data header..."); + + // Fill the user data header + UserData.dwID = ID_MPQ_USERDATA; + UserData.cbUserDataSize = dwByteCount; + UserData.dwHeaderOffs = (dwByteCount + sizeof(TMPQUserData)); + UserData.cbUserDataHeader = dwByteCount / 2; + if(!FileStream_Write(pStream, &ByteOffset, &UserData, sizeof(TMPQUserData))) + nError = GetLastError(); + return nError; +} + +static int WriteFileData( + TLogHelper * pLogger, + TFileStream * pStream, + ULONGLONG ByteOffset, + ULONGLONG ByteCount) +{ + ULONGLONG SaveByteCount = ByteCount; + ULONGLONG BytesWritten = 0; + LPBYTE pbDataBuffer; + DWORD cbDataBuffer = 0x10000; + int nError = ERROR_SUCCESS; + + // Write some data + pbDataBuffer = new BYTE[cbDataBuffer]; + if(pbDataBuffer != NULL) + { + memset(pbDataBuffer, 0, cbDataBuffer); + strcpy((char *)pbDataBuffer, "This is a test data written to a file."); + + // Perform the write + while(ByteCount > 0) + { + DWORD cbToWrite = (ByteCount > cbDataBuffer) ? cbDataBuffer : (DWORD)ByteCount; + + // Notify the user + pLogger->PrintProgress("Writing file data (%I64u of %I64u) ...", BytesWritten, SaveByteCount); + + // Write the data + if(!FileStream_Write(pStream, &ByteOffset, pbDataBuffer, cbToWrite)) + { + nError = GetLastError(); + break; + } + + BytesWritten += cbToWrite; + ByteOffset += cbToWrite; + ByteCount -= cbToWrite; + } + + delete [] pbDataBuffer; + } + return nError; +} + +static int CopyFileData( + TLogHelper * pLogger, + TFileStream * pStream1, + TFileStream * pStream2, + ULONGLONG ByteOffset, + ULONGLONG ByteCount) +{ + ULONGLONG BytesCopied = 0; + ULONGLONG EndOffset = ByteOffset + ByteCount; + LPBYTE pbCopyBuffer; + DWORD BytesToRead; + DWORD BlockLength = 0x100000; + int nError = ERROR_SUCCESS; + + // Allocate copy buffer + pbCopyBuffer = STORM_ALLOC(BYTE, BlockLength); + if(pbCopyBuffer != NULL) + { + while(ByteOffset < EndOffset) + { + // Read source + BytesToRead = ((EndOffset - ByteOffset) > BlockLength) ? BlockLength : (DWORD)(EndOffset - ByteOffset); + if(!FileStream_Read(pStream1, &ByteOffset, pbCopyBuffer, BytesToRead)) + { + nError = GetLastError(); + break; + } + + // Write to the destination file + if(!FileStream_Write(pStream2, NULL, pbCopyBuffer, BytesToRead)) + { + nError = GetLastError(); + break; + } + + // Increment the byte counts + BytesCopied += BytesToRead; + ByteOffset += BytesToRead; + + // Notify the user + pLogger->PrintProgress("Copying (%I64u of %I64u complete) ...", BytesCopied, ByteCount); + } + + STORM_FREE(pbCopyBuffer); + } + + return nError; +} + +// Support function for copying file +static int CreateFileCopy( + TLogHelper * pLogger, + const char * szPlainName, + const char * szFileCopy, + char * szBuffer, + ULONGLONG PreMpqDataSize = 0, + ULONGLONG UserDataSize = 0) +{ + TFileStream * pStream1; // Source file + TFileStream * pStream2; // Target file + ULONGLONG ByteOffset = 0; + ULONGLONG FileSize = 0; + char szFileName1[MAX_PATH]; + char szFileName2[MAX_PATH]; + int nError = ERROR_SUCCESS; + + // Notify the user + szPlainName += FileStream_PrefixA(szPlainName, NULL); + pLogger->PrintProgress("Creating copy of %s ...", szPlainName); + + // Construct both file names. Check if they are not the same + CreateFullPathName(szFileName1, szMpqSubDir, szPlainName); + CreateFullPathName(szFileName2, NULL, szFileCopy + FileStream_PrefixA(szFileCopy, NULL)); + if(!_stricmp(szFileName1, szFileName2)) + { + pLogger->PrintError("Failed to create copy of MPQ (the copy name is the same like the original name)"); + return ERROR_CAN_NOT_COMPLETE; + } + + // Open the source file + pStream1 = FileStream_OpenFileA(szFileName1, STREAM_FLAG_READ_ONLY); + if(pStream1 == NULL) + { + pLogger->PrintError("Failed to open the source file %s", szFileName1); + return ERROR_CAN_NOT_COMPLETE; + } + + // Create the destination file + pStream2 = FileStream_CreateFileA(szFileName2, 0); + if(pStream2 != NULL) + { + // If we should write some pre-MPQ data to the target file, do it + if(PreMpqDataSize != 0) + { + nError = WriteFileData(pLogger, pStream2, ByteOffset, PreMpqDataSize); + ByteOffset += PreMpqDataSize; + } + + // If we should write some MPQ user data, write the header first + if(UserDataSize != 0) + { + nError = WriteMpqUserDataHeader(pLogger, pStream2, ByteOffset, (DWORD)UserDataSize); + ByteOffset += sizeof(TMPQUserData); + + nError = WriteFileData(pLogger, pStream2, ByteOffset, UserDataSize); + ByteOffset += UserDataSize; + } + + // Copy the file data from the source file to the destination file + FileStream_GetSize(pStream1, &FileSize); + if(FileSize != 0) + { + nError = CopyFileData(pLogger, pStream1, pStream2, 0, FileSize); + ByteOffset += FileSize; + } + FileStream_Close(pStream2); + } + + // Close the source file + FileStream_Close(pStream1); + + // Create the full file name of the target file, including prefix + if(szBuffer != NULL) + CreateFullPathName(szBuffer, NULL, szFileCopy); + + // Report error, if any + if(nError != ERROR_SUCCESS) + pLogger->PrintError("Failed to create copy of MPQ"); + return nError; +} + +static int CreateMasterAndMirrorPaths( + TLogHelper * pLogger, + char * szMirrorPath, + char * szMasterPath, + const char * szMirrorName, + const char * szMasterName, + bool bCopyMirrorFile) +{ + char szCopyPath[MAX_PATH]; + int nError = ERROR_SUCCESS; + + // Always delete the mirror file + CreateFullPathName(szMasterPath, szMpqSubDir, szMasterName); + CreateFullPathName(szCopyPath, NULL, szMirrorName); + remove(szCopyPath + FileStream_PrefixA(szCopyPath, NULL)); + + // Copy the mirrored file from the source to the work directory + if(bCopyMirrorFile) + nError = CreateFileCopy(pLogger, szMirrorName, szMirrorName, NULL); + + // Create the mirror*master path + if(nError == ERROR_SUCCESS) + sprintf(szMirrorPath, "%s*%s", szCopyPath, szMasterPath); + + return nError; +} + +static void WINAPI AddFileCallback(void * pvUserData, DWORD dwBytesWritten, DWORD dwTotalBytes, bool bFinalCall) +{ + TLogHelper * pLogger = (TLogHelper *)pvUserData; + + // Keep compiler happy + bFinalCall = bFinalCall; + + pLogger->PrintProgress("Adding file (%s) (%u of %u) (%u of %u) ...", pLogger->UserString, + pLogger->UserCount, + pLogger->UserTotal, + dwBytesWritten, + dwTotalBytes); +} + +static void WINAPI CompactCallback(void * pvUserData, DWORD dwWork, ULONGLONG BytesDone, ULONGLONG TotalBytes) +{ + TLogHelper * pLogger = (TLogHelper *)pvUserData; + const char * szWork = NULL; + + switch(dwWork) + { + case CCB_CHECKING_FILES: + szWork = "Checking files in archive"; + break; + + case CCB_CHECKING_HASH_TABLE: + szWork = "Checking hash table"; + break; + + case CCB_COPYING_NON_MPQ_DATA: + szWork = "Copying non-MPQ data"; + break; + + case CCB_COMPACTING_FILES: + szWork = "Compacting files"; + break; + + case CCB_CLOSING_ARCHIVE: + szWork = "Closing archive"; + break; + } + + if(szWork != NULL) + { + if(pLogger != NULL) + pLogger->PrintProgress("%s (%I64u of %I64u) ...", szWork, BytesDone, TotalBytes); + else + printf("%s (" I64u_a " of " I64u_a ") ... \r", szWork, BytesDone, TotalBytes); + } +} + +//----------------------------------------------------------------------------- +// MPQ file utilities + +#define TEST_FLAG_LOAD_FILES 0x00000001 // Test function should load all files in the MPQ +#define TEST_FLAG_HASH_FILES 0x00000002 // Test function should load all files in the MPQ +#define TEST_FLAG_PLAY_WAVES 0x00000004 // Play extracted WAVE files +#define TEST_FLAG_MOST_PATCHED 0x00000008 // Find the most patched file + +struct TFileData +{ + DWORD dwBlockIndex; + DWORD dwFileSize; + DWORD dwFlags; + DWORD dwReserved; // Alignment + BYTE FileData[1]; +}; + +static bool CheckIfFileIsPresent(TLogHelper * pLogger, HANDLE hMpq, const char * szFileName, bool bShouldExist) +{ + HANDLE hFile = NULL; + + if(SFileOpenFileEx(hMpq, szFileName, 0, &hFile)) + { + if(bShouldExist == false) + pLogger->PrintMessage("The file %s is present, but it should not be", szFileName); + SFileCloseFile(hFile); + return true; + } + else + { + if(bShouldExist) + pLogger->PrintMessage("The file %s is not present, but it should be", szFileName); + return false; + } +} + +static TFileData * LoadLocalFile(TLogHelper * pLogger, const char * szFileName, bool bMustSucceed) +{ + TFileStream * pStream; + TFileData * pFileData = NULL; + ULONGLONG FileSize = 0; + size_t nAllocateBytes; + + // Notify the user + if(pLogger != NULL) + pLogger->PrintProgress("Loading local file ..."); + + // Attempt to open the file + pStream = FileStream_OpenFileA(szFileName, STREAM_FLAG_READ_ONLY); + if(pStream == NULL) + { + if(pLogger != NULL && bMustSucceed == true) + pLogger->PrintError("Failed to open the file %s", szFileName); + return NULL; + } + + // Verify the size + FileStream_GetSize(pStream, &FileSize); + if((FileSize >> 0x20) == 0) + { + // Allocate space for the file + nAllocateBytes = sizeof(TFileData) + (size_t)FileSize; + pFileData = (TFileData *)STORM_ALLOC(BYTE, nAllocateBytes); + if(pFileData != NULL) + { + // Make sure it;s properly zeroed + memset(pFileData, 0, nAllocateBytes); + pFileData->dwFileSize = (DWORD)FileSize; + + // Load to memory + if(!FileStream_Read(pStream, NULL, pFileData->FileData, pFileData->dwFileSize)) + { + STORM_FREE(pFileData); + pFileData = NULL; + } + } + } + + FileStream_Close(pStream); + return pFileData; +} + +static int CompareTwoLocalFilesRR( + TLogHelper * pLogger, + TFileStream * pStream1, // Master file + TFileStream * pStream2, // Mirror file + int nIterations) // Number of iterations +{ + ULONGLONG RandomNumber = 0x12345678; // We need pseudo-random number that will repeat each run of the program + ULONGLONG RandomSeed; + ULONGLONG ByteOffset; + ULONGLONG FileSize1 = 1; + ULONGLONG FileSize2 = 2; + DWORD BytesToRead; + DWORD Difference; + LPBYTE pbBuffer1; + LPBYTE pbBuffer2; + DWORD cbBuffer = 0x100000; + int nError = ERROR_SUCCESS; + + // Compare file sizes + FileStream_GetSize(pStream1, &FileSize1); + FileStream_GetSize(pStream2, &FileSize2); + if(FileSize1 != FileSize2) + { + pLogger->PrintMessage("The files have different size"); + return ERROR_CAN_NOT_COMPLETE; + } + + // Allocate both buffers + pbBuffer1 = STORM_ALLOC(BYTE, cbBuffer); + pbBuffer2 = STORM_ALLOC(BYTE, cbBuffer); + if(pbBuffer1 && pbBuffer2) + { + // Perform many random reads + for(int i = 0; i < nIterations; i++) + { + // Generate psudo-random offsrt and data size + ByteOffset = (RandomNumber % FileSize1); + BytesToRead = (DWORD)(RandomNumber % cbBuffer); + + // Show the progress message + pLogger->PrintProgress("Comparing file: Offset: " I64u_a ", Length: %u", ByteOffset, BytesToRead); + + // Only perform read if the byte offset is below + if(ByteOffset < FileSize1) + { + if((ByteOffset + BytesToRead) > FileSize1) + BytesToRead = (DWORD)(FileSize1 - ByteOffset); + + memset(pbBuffer1, 0xEE, cbBuffer); + memset(pbBuffer2, 0xAA, cbBuffer); + + FileStream_Read(pStream1, &ByteOffset, pbBuffer1, BytesToRead); + FileStream_Read(pStream2, &ByteOffset, pbBuffer2, BytesToRead); + + if(!CompareBlocks(pbBuffer1, pbBuffer2, BytesToRead, &Difference)) + { + pLogger->PrintMessage("Difference at %u (Offset " I64X_a ", Length %X)", Difference, ByteOffset, BytesToRead); + nError = ERROR_FILE_CORRUPT; + break; + } + + // Shuffle the random number + memcpy(&RandomSeed, pbBuffer1, sizeof(RandomSeed)); + RandomNumber = ((RandomNumber >> 0x11) | (RandomNumber << 0x29)) ^ (RandomNumber + RandomSeed); + } + } + } + + // Free both buffers + if(pbBuffer2 != NULL) + STORM_FREE(pbBuffer2); + if(pbBuffer1 != NULL) + STORM_FREE(pbBuffer1); + return nError; +} + +static TFileData * LoadMpqFile(TLogHelper * pLogger, HANDLE hMpq, const char * szFileName) +{ + TFileData * pFileData = NULL; + HANDLE hFile; + DWORD dwFileSizeHi = 0xCCCCCCCC; + DWORD dwFileSizeLo = 0; + DWORD dwBytesRead; + int nError = ERROR_SUCCESS; + + // Notify the user that we are loading a file from MPQ + pLogger->PrintProgress("Loading file %s ...", GetShortPlainName(szFileName)); + +#if defined(_MSC_VER) && defined(_DEBUG) +// if(!_stricmp(szFileName, "manifest-cards.csv")) +// DebugBreak(); +#endif + + // Open the file from MPQ + if(!SFileOpenFileEx(hMpq, szFileName, 0, &hFile)) + nError = pLogger->PrintError("Failed to open the file %s", szFileName); + + // Get the size of the file + if(nError == ERROR_SUCCESS) + { + dwFileSizeLo = SFileGetFileSize(hFile, &dwFileSizeHi); + if(dwFileSizeLo == SFILE_INVALID_SIZE || dwFileSizeHi != 0) + nError = pLogger->PrintError("Failed to query the file size"); + } + + // Spazzler protector: Creates fake files with size of 0x7FFFE7CA + if(nError == ERROR_SUCCESS) + { + if(dwFileSizeLo > 0x1FFFFFFF) + nError = ERROR_FILE_CORRUPT; + } + + // Allocate buffer for the file content + if(nError == ERROR_SUCCESS) + { + pFileData = (TFileData *)STORM_ALLOC(BYTE, sizeof(TFileData) + dwFileSizeLo); + if(pFileData == NULL) + { + pLogger->PrintError("Failed to allocate buffer for the file content"); + nError = ERROR_NOT_ENOUGH_MEMORY; + } + } + + // get the file index of the MPQ file + if(nError == ERROR_SUCCESS) + { + // Store the file size + memset(pFileData, 0, sizeof(TFileData) + dwFileSizeLo); + pFileData->dwFileSize = dwFileSizeLo; + + // Retrieve the block index and file flags + if(!SFileGetFileInfo(hFile, SFileInfoFileIndex, &pFileData->dwBlockIndex, sizeof(DWORD), NULL)) + nError = pLogger->PrintError("Failed retrieve the file index of %s", szFileName); + if(!SFileGetFileInfo(hFile, SFileInfoFlags, &pFileData->dwFlags, sizeof(DWORD), NULL)) + nError = pLogger->PrintError("Failed retrieve the file flags of %s", szFileName); + } + + // Load the entire file + if(nError == ERROR_SUCCESS) + { + // Read the file data + SFileReadFile(hFile, pFileData->FileData, dwFileSizeLo, &dwBytesRead, NULL); + if(dwBytesRead != dwFileSizeLo) + nError = pLogger->PrintError("Failed to read the content of the file %s", szFileName); + } + + // If failed, free the buffer + if(nError != ERROR_SUCCESS) + { + STORM_FREE(pFileData); + SetLastError(nError); + pFileData = NULL; + } + + // Close the file and return what we got + if(hFile != NULL) + SFileCloseFile(hFile); + return pFileData; +} + +static bool CompareTwoFiles(TLogHelper * pLogger, TFileData * pFileData1, TFileData * pFileData2) +{ + // Compare the file size + if(pFileData1->dwFileSize != pFileData2->dwFileSize) + { + pLogger->PrintErrorVa(_T("The files have different size (%u vs %u)"), pFileData1->dwFileSize, pFileData2->dwFileSize); + SetLastError(ERROR_FILE_CORRUPT); + return false; + } + + // Compare the files + for(DWORD i = 0; i < pFileData1->dwFileSize; i++) + { + if(pFileData1->FileData[i] != pFileData2->FileData[i]) + { + pLogger->PrintErrorVa(_T("Files are different at offset %08X"), i); + SetLastError(ERROR_FILE_CORRUPT); + return false; + } + } + + // The files are identical + return true; +} + +static int SearchArchive( + TLogHelper * pLogger, + HANDLE hMpq, + DWORD dwTestFlags = 0, + DWORD * pdwFileCount = NULL, + LPBYTE pbFileHash = NULL) +{ + SFILE_FIND_DATA sf; + TFileData * pFileData; + HANDLE hFind; + DWORD dwFileCount = 0; + hash_state md5state; + char szMostPatched[MAX_PATH] = ""; + char szListFile[MAX_PATH]; + bool bFound = true; + int nMaxPatchCount = 0; + int nPatchCount = 0; + int nError = ERROR_SUCCESS; + + // Construct the full name of the listfile + CreateFullPathName(szListFile, szMpqSubDir, "ListFile_Blizzard.txt"); + + // Prepare hashing + md5_init(&md5state); + + // Initiate the MPQ search + pLogger->PrintProgress("Searching the archive ..."); + hFind = SFileFindFirstFile(hMpq, "*", &sf, szListFile); + if(hFind == NULL) + { + nError = GetLastError(); + nError = (nError == ERROR_NO_MORE_FILES) ? ERROR_SUCCESS : nError; + return nError; + } + + // Perform the search + while(bFound == true) + { + // Increment number of files + dwFileCount++; + +// if(!_stricmp(sf.cFileName, "Interface\\Glues\\CREDITS\\1024px-Blade3_final2.blp")) +// DebugBreak(); + + if(dwTestFlags & TEST_FLAG_MOST_PATCHED) + { + // Load the patch count + nPatchCount = GetFilePatchCount(pLogger, hMpq, sf.cFileName); + + // Check if it's greater than maximum + if(nPatchCount > nMaxPatchCount) + { + strcpy(szMostPatched, sf.cFileName); + nMaxPatchCount = nPatchCount; + } + } + + // Load the file to memory, if required + if(dwTestFlags & TEST_FLAG_LOAD_FILES) + { + // Load the entire file to the MPQ + pFileData = LoadMpqFile(pLogger, hMpq, sf.cFileName); + if(pFileData != NULL) + { + // Hash the file data, if needed + if((dwTestFlags & TEST_FLAG_HASH_FILES) && !IsInternalMpqFileName(sf.cFileName)) + md5_process(&md5state, pFileData->FileData, pFileData->dwFileSize); + + // Play sound files, if required + if((dwTestFlags & TEST_FLAG_PLAY_WAVES) && strstr(sf.cFileName, ".wav") != NULL) + { +#ifdef _MSC_VER + pLogger->PrintProgress("Playing sound %s", sf.cFileName); + PlaySound((LPCTSTR)pFileData->FileData, NULL, SND_MEMORY); +#endif + } + + STORM_FREE(pFileData); + } + } + + bFound = SFileFindNextFile(hFind, &sf); + } + SFileFindClose(hFind); + + // Give the file count, if required + if(pdwFileCount != NULL) + pdwFileCount[0] = dwFileCount; + + // Give the hash, if required + if(pbFileHash != NULL && (dwTestFlags & TEST_FLAG_HASH_FILES)) + md5_done(&md5state, pbFileHash); + + return nError; +} + +static int CreateNewArchive(TLogHelper * pLogger, const char * szPlainName, DWORD dwCreateFlags, DWORD dwMaxFileCount, HANDLE * phMpq) +{ + HANDLE hMpq = NULL; + TCHAR szMpqName[MAX_PATH]; + char szFullPath[MAX_PATH]; + + // Make sure that the MPQ is deleted + CreateFullPathName(szFullPath, NULL, szPlainName); + remove(szFullPath); + + // Create the new MPQ + CopyFileName(szMpqName, szFullPath, strlen(szFullPath)); + if(!SFileCreateArchive(szMpqName, dwCreateFlags, dwMaxFileCount, &hMpq)) + return pLogger->PrintError(_T("Failed to create archive %s"), szMpqName); + + // Shall we close it right away? + if(phMpq == NULL) + SFileCloseArchive(hMpq); + else + *phMpq = hMpq; + + return ERROR_SUCCESS; +} + +static int CreateNewArchive_V2(TLogHelper * pLogger, const char * szPlainName, DWORD dwCreateFlags, DWORD dwMaxFileCount, HANDLE * phMpq) +{ + SFILE_CREATE_MPQ CreateInfo; + HANDLE hMpq = NULL; + TCHAR szMpqName[MAX_PATH]; + char szFullPath[MAX_PATH]; + + // Make sure that the MPQ is deleted + CreateFullPathName(szFullPath, NULL, szPlainName); + CopyFileName(szMpqName, szFullPath, strlen(szFullPath)); + remove(szFullPath); + + // Fill the create structure + memset(&CreateInfo, 0, sizeof(SFILE_CREATE_MPQ)); + CreateInfo.cbSize = sizeof(SFILE_CREATE_MPQ); + CreateInfo.dwMpqVersion = (dwCreateFlags & MPQ_CREATE_ARCHIVE_VMASK) >> FLAGS_TO_FORMAT_SHIFT; + CreateInfo.dwStreamFlags = STREAM_PROVIDER_FLAT | BASE_PROVIDER_FILE; + CreateInfo.dwFileFlags1 = (dwCreateFlags & MPQ_CREATE_LISTFILE) ? MPQ_FILE_EXISTS : 0; + CreateInfo.dwFileFlags2 = (dwCreateFlags & MPQ_CREATE_ATTRIBUTES) ? MPQ_FILE_EXISTS : 0; + CreateInfo.dwFileFlags3 = (dwCreateFlags & MPQ_CREATE_SIGNATURE) ? MPQ_FILE_EXISTS : 0; + CreateInfo.dwAttrFlags = (dwCreateFlags & MPQ_CREATE_ATTRIBUTES) ? (MPQ_ATTRIBUTE_CRC32 | MPQ_ATTRIBUTE_FILETIME | MPQ_ATTRIBUTE_MD5) : 0; + CreateInfo.dwSectorSize = (CreateInfo.dwMpqVersion >= MPQ_FORMAT_VERSION_3) ? 0x4000 : 0x1000; + CreateInfo.dwRawChunkSize = (CreateInfo.dwMpqVersion >= MPQ_FORMAT_VERSION_4) ? 0x4000 : 0; + CreateInfo.dwMaxFileCount = dwMaxFileCount; + + // Create the new MPQ + if(!SFileCreateArchive2(szMpqName, &CreateInfo, &hMpq)) + return pLogger->PrintError(_T("Failed to create archive %s"), szMpqName); + + // Shall we close it right away? + if(phMpq == NULL) + SFileCloseArchive(hMpq); + else + *phMpq = hMpq; + + return ERROR_SUCCESS; +} + +// Creates new archive with UNICODE name. Adds prefix to the name +static int CreateNewArchiveU(TLogHelper * pLogger, const wchar_t * szPlainName, DWORD dwCreateFlags, DWORD dwMaxFileCount) +{ +#ifdef _UNICODE + HANDLE hMpq = NULL; + TCHAR szMpqName[MAX_PATH+1]; + char szFullPath[MAX_PATH]; + + // Construct the full UNICODE name + CreateFullPathName(szFullPath, NULL, "StormLibTest_"); + CopyFileName(szMpqName, szFullPath, strlen(szFullPath)); + StringCatT(szMpqName, szPlainName, MAX_PATH); + + // Make sure that the MPQ is deleted + _tremove(szMpqName); + + // Create the archive + pLogger->PrintProgress("Creating new archive with UNICODE name ..."); + if(!SFileCreateArchive(szMpqName, dwCreateFlags, dwMaxFileCount, &hMpq)) + return pLogger->PrintError(_T("Failed to create archive %s"), szMpqName); + + SFileCloseArchive(hMpq); +#else + pLogger = pLogger; + szPlainName = szPlainName; + dwCreateFlags = dwCreateFlags; + dwMaxFileCount = dwMaxFileCount; +#endif + return ERROR_SUCCESS; +} + +static int OpenExistingArchive(TLogHelper * pLogger, const char * szFullPath, DWORD dwFlags, HANDLE * phMpq) +{ + HANDLE hMpq = NULL; + TCHAR szMpqName[MAX_PATH]; +// bool bReopenResult; + int nError = ERROR_SUCCESS; + + // Is it an encrypted MPQ ? + if(strstr(szFullPath, ".MPQE") != NULL) + dwFlags |= STREAM_PROVIDER_MPQE; + if(strstr(szFullPath, ".MPQ.part") != NULL) + dwFlags |= STREAM_PROVIDER_PARTIAL; + if(strstr(szFullPath, ".mpq.part") != NULL) + dwFlags |= STREAM_PROVIDER_PARTIAL; + if(strstr(szFullPath, ".MPQ.0") != NULL) + dwFlags |= STREAM_PROVIDER_BLOCK4; + + // Open the copied archive + pLogger->PrintProgress("Opening archive %s ...", GetShortPlainName(szFullPath)); + CopyFileName(szMpqName, szFullPath, strlen(szFullPath)); + if(!SFileOpenArchive(szMpqName, 0, dwFlags, &hMpq)) + { + switch(nError = GetLastError()) + { +// case ERROR_BAD_FORMAT: // If the error is ERROR_BAD_FORMAT, try to open with MPQ_OPEN_FORCE_MPQ_V1 +// bReopenResult = SFileOpenArchive(szMpqName, 0, dwFlags | MPQ_OPEN_FORCE_MPQ_V1, &hMpq); +// nError = (bReopenResult == false) ? GetLastError() : ERROR_SUCCESS; +// break; + + case ERROR_AVI_FILE: // Ignore the error if it's an AVI file or if the file is incomplete + case ERROR_FILE_INCOMPLETE: + return nError; + } + + // Show the open error to the user + return pLogger->PrintError("Failed to open archive %s", szFullPath); + } + + // Store the archive handle or close the archive + if(phMpq == NULL) + SFileCloseArchive(hMpq); + else + *phMpq = hMpq; + return nError; +} + +static int OpenPatchArchive(TLogHelper * pLogger, HANDLE hMpq, const char * szFullPath) +{ + TCHAR szPatchName[MAX_PATH]; + int nError = ERROR_SUCCESS; + + pLogger->PrintProgress("Adding patch %s ...", GetShortPlainName(szFullPath)); + CopyFileName(szPatchName, szFullPath, strlen(szFullPath)); + if(!SFileOpenPatchArchive(hMpq, szPatchName, NULL, 0)) + nError = pLogger->PrintError("Failed to add patch %s ...", szFullPath); + + return nError; +} + +static int OpenExistingArchiveWithCopy(TLogHelper * pLogger, const char * szFileName, const char * szCopyName, HANDLE * phMpq) +{ + DWORD dwFlags = 0; + char szFullPath[MAX_PATH]; + int nError = ERROR_SUCCESS; + + // We expect MPQ directory to be already prepared by InitializeMpqDirectory + assert(szMpqDirectory[0] != 0); + + // At least one name must be entered + assert(szFileName != NULL || szCopyName != NULL); + + // If both names entered, create a copy + if(szFileName != NULL && szCopyName != NULL) + { + nError = CreateFileCopy(pLogger, szFileName, szCopyName, szFullPath); + if(nError != ERROR_SUCCESS) + return nError; + } + + // If only source name entered, open it for read-only access + else if(szFileName != NULL && szCopyName == NULL) + { + CreateFullPathName(szFullPath, szMpqSubDir, szFileName); + dwFlags |= MPQ_OPEN_READ_ONLY; + } + + // If only target name entered, open it directly + else if(szFileName == NULL && szCopyName != NULL) + { + CreateFullPathName(szFullPath, NULL, szCopyName); + } + + // Open the archive + return OpenExistingArchive(pLogger, szFullPath, dwFlags, phMpq); +} + +static int OpenPatchedArchive(TLogHelper * pLogger, HANDLE * phMpq, const char * PatchList[]) +{ + HANDLE hMpq = NULL; + char szFullPath[MAX_PATH]; + int nError = ERROR_SUCCESS; + + // The first file is expected to be valid + assert(PatchList[0] != NULL); + + // Open the primary MPQ + CreateFullPathName(szFullPath, szMpqSubDir, PatchList[0]); + nError = OpenExistingArchive(pLogger, szFullPath, MPQ_OPEN_READ_ONLY, &hMpq); + + // Add all patches + if(nError == ERROR_SUCCESS) + { + for(size_t i = 1; PatchList[i] != NULL; i++) + { + CreateFullPathName(szFullPath, szMpqPatchDir, PatchList[i]); + nError = OpenPatchArchive(pLogger, hMpq, szFullPath); + if(nError != ERROR_SUCCESS) + break; + } + } + + // Store the archive handle or close the archive + if(phMpq == NULL) + SFileCloseArchive(hMpq); + else + *phMpq = hMpq; + return nError; +} + +static int AddFileToMpq( + TLogHelper * pLogger, + HANDLE hMpq, + const char * szFileName, + const char * szFileData, + DWORD dwFlags = 0, + DWORD dwCompression = 0, + int nExpectedError = ERROR_SUCCESS) +{ + HANDLE hFile = NULL; + DWORD dwFileSize = (DWORD)strlen(szFileData); + int nError = ERROR_SUCCESS; + + // Notify the user + pLogger->PrintProgress("Adding file %s ...", szFileName); + + // Get the default flags + if(dwFlags == 0) + dwFlags = MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED; + if(dwCompression == 0) + dwCompression = MPQ_COMPRESSION_ZLIB; + + // Create the file within the MPQ + if(SFileCreateFile(hMpq, szFileName, 0, dwFileSize, 0, dwFlags, &hFile)) + { + // Write the file + if(!SFileWriteFile(hFile, szFileData, dwFileSize, dwCompression)) + nError = pLogger->PrintError("Failed to write data to the MPQ"); + SFileCloseFile(hFile); + } + else + { + nError = GetLastError(); + } + + // Check the expected error code + if(nExpectedError != ERROR_UNDETERMINED_RESULT && nError != nExpectedError) + return pLogger->PrintError("Unexpected result from SFileCreateFile(%s)", szFileName); + return nError; +} + +static int AddLocalFileToMpq( + TLogHelper * pLogger, + HANDLE hMpq, + const char * szArchivedName, + const char * szLocalFileName, + DWORD dwFlags = 0, + DWORD dwCompression = 0, + bool bMustSucceed = false) +{ + TCHAR szFileName[MAX_PATH]; + DWORD dwVerifyResult; + + // Notify the user + pLogger->PrintProgress("Adding file %s (%u of %u)...", GetShortPlainName(szLocalFileName), pLogger->UserCount, pLogger->UserTotal); + pLogger->UserString = szArchivedName; + + // Get the default flags + if(dwFlags == 0) + dwFlags = MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED; + if(dwCompression == 0) + dwCompression = MPQ_COMPRESSION_ZLIB; + + // Set the notification callback + SFileSetAddFileCallback(hMpq, AddFileCallback, pLogger); + + // Add the file to the MPQ + CopyFileName(szFileName, szLocalFileName, strlen(szLocalFileName)); + if(!SFileAddFileEx(hMpq, szFileName, szArchivedName, dwFlags, dwCompression, MPQ_COMPRESSION_NEXT_SAME)) + { + if(bMustSucceed) + return pLogger->PrintError("Failed to add the file %s", szArchivedName); + return GetLastError(); + } + + // Verify the file unless it was lossy compression + if((dwCompression & (MPQ_COMPRESSION_ADPCM_MONO | MPQ_COMPRESSION_ADPCM_STEREO)) == 0) + { + // Notify the user + pLogger->PrintProgress("Verifying file %s (%u of %u) ...", szArchivedName, pLogger->UserCount, pLogger->UserTotal); + + // Perform the verification + dwVerifyResult = SFileVerifyFile(hMpq, szArchivedName, MPQ_ATTRIBUTE_CRC32 | MPQ_ATTRIBUTE_MD5); + if(dwVerifyResult & (VERIFY_OPEN_ERROR | VERIFY_READ_ERROR | VERIFY_FILE_SECTOR_CRC_ERROR | VERIFY_FILE_CHECKSUM_ERROR | VERIFY_FILE_MD5_ERROR)) + return pLogger->PrintError("CRC error on %s", szArchivedName); + } + + return ERROR_SUCCESS; +} + +static int RenameMpqFile(TLogHelper * pLogger, HANDLE hMpq, const char * szOldFileName, const char * szNewFileName, int nExpectedError) +{ + int nError = ERROR_SUCCESS; + + // Notify the user + pLogger->PrintProgress("Renaming %s to %s ...", szOldFileName, szNewFileName); + + // Perform the deletion + if(!SFileRenameFile(hMpq, szOldFileName, szNewFileName)) + nError = GetLastError(); + + if(nError != nExpectedError) + return pLogger->PrintErrorVa("Unexpected result from SFileRenameFile(%s -> %s)", szOldFileName, szNewFileName); + return ERROR_SUCCESS; +} + +static int RemoveMpqFile(TLogHelper * pLogger, HANDLE hMpq, const char * szFileName, int nExpectedError) +{ + int nError = ERROR_SUCCESS; + + // Notify the user + pLogger->PrintProgress("Removing file %s ...", szFileName); + + // Perform the deletion + if(!SFileRemoveFile(hMpq, szFileName, 0)) + nError = GetLastError(); + + if(nError != nExpectedError) + return pLogger->PrintError("Unexpected result from SFileRemoveFile(%s)", szFileName); + return ERROR_SUCCESS; +} + +//----------------------------------------------------------------------------- +// Tests + +static void TestGetFileInfo( + TLogHelper * pLogger, + HANDLE hMpqOrFile, + SFileInfoClass InfoClass, + void * pvFileInfo, + DWORD cbFileInfo, + DWORD * pcbLengthNeeded, + bool bExpectedResult, + int nExpectedError) +{ + bool bResult; + int nError = ERROR_SUCCESS; + + // Call the get file info + bResult = SFileGetFileInfo(hMpqOrFile, InfoClass, pvFileInfo, cbFileInfo, pcbLengthNeeded); + if(!bResult) + nError = GetLastError(); + + if(bResult != bExpectedResult) + pLogger->PrintMessage("Different result of SFileGetFileInfo."); + if(nError != nExpectedError) + pLogger->PrintMessage("Different error from SFileGetFileInfo (expected %u, returned %u)", nExpectedError, nError); +} + +// StormLib is able to open local files (as well as the original Storm.dll) +// I want to keep this for occasional use +static int TestOpenLocalFile(const char * szPlainName) +{ + TLogHelper Logger("OpenLocalFile", szPlainName); + HANDLE hFile; + DWORD dwFileSizeHi = 0; + DWORD dwFileSizeLo = 0; + char szFileName1[MAX_PATH]; + char szFileName2[MAX_PATH]; + char szFileLine[0x40]; + + CreateFullPathName(szFileName1, szMpqSubDir, szPlainName); + if(SFileOpenFileEx(NULL, szFileName1, SFILE_OPEN_LOCAL_FILE, &hFile)) + { + // Retrieve the file name. It must match the name under which the file was open + SFileGetFileName(hFile, szFileName2); + if(strcmp(szFileName2, szFileName1)) + Logger.PrintMessage("The retrieved name does not match the open name"); + + // Retrieve the file size + dwFileSizeLo = SFileGetFileSize(hFile, &dwFileSizeHi); + if(dwFileSizeHi != 0 || dwFileSizeLo != 3904784) + Logger.PrintMessage("Local file size mismatch"); + + // Read the first line + memset(szFileLine, 0, sizeof(szFileLine)); + SFileReadFile(hFile, szFileLine, 18, NULL, NULL); + if(strcmp(szFileLine, "(1)Enslavers01.scm")) + Logger.PrintMessage("Content of the listfile does not match"); + + SFileCloseFile(hFile); + } + + return ERROR_SUCCESS; +} + +static int TestSearchListFile(const char * szPlainName) +{ + SFILE_FIND_DATA sf; + TLogHelper Logger("SearchListFile", szPlainName); + HANDLE hFind; + char szFullPath[MAX_PATH]; + int nFileCount = 0; + + CreateFullPathName(szFullPath, szMpqSubDir, szPlainName); + hFind = SListFileFindFirstFile(NULL, szFullPath, "*", &sf); + if(hFind != NULL) + { + for(;;) + { + Logger.PrintProgress("Found file (%04u): %s", nFileCount++, GetShortPlainName(sf.cFileName)); + if(!SListFileFindNextFile(hFind, &sf)) + break; + } + + SListFileFindClose(hFind); + } + return ERROR_SUCCESS; +} + +static void WINAPI TestReadFile_DownloadCallback( + void * UserData, + ULONGLONG ByteOffset, + DWORD DataLength) +{ + TLogHelper * pLogger = (TLogHelper *)UserData; + + if(ByteOffset != 0 && DataLength != 0) + pLogger->PrintProgress("Downloading data (offset: " I64X_a ", length: %X)", ByteOffset, DataLength); + else + pLogger->PrintProgress("Download complete."); +} + +// Open a file stream with mirroring a master file +static int TestReadFile_MasterMirror(const char * szMirrorName, const char * szMasterName, bool bCopyMirrorFile) +{ + TFileStream * pStream1; // Master file + TFileStream * pStream2; // Mirror file + TLogHelper Logger("OpenMirrorFile", szMirrorName); + DWORD dwProvider = 0; + char szMirrorPath[MAX_PATH + MAX_PATH]; + char szMasterPath[MAX_PATH]; + int nIterations = 0x10000; + int nError; + + // Retrieve the provider + FileStream_PrefixA(szMasterName, &dwProvider); + +#ifndef PLATFORM_WINDOWS + if((dwProvider & BASE_PROVIDER_MASK) == BASE_PROVIDER_HTTP) + return ERROR_SUCCESS; +#endif + + // Create copy of the file to serve as mirror, keep master there + nError = CreateMasterAndMirrorPaths(&Logger, szMirrorPath, szMasterPath, szMirrorName, szMasterName, bCopyMirrorFile); + if(nError == ERROR_SUCCESS) + { + // Open both master and mirror file + pStream1 = FileStream_OpenFileA(szMasterPath, STREAM_FLAG_READ_ONLY); + pStream2 = FileStream_OpenFileA(szMirrorPath, STREAM_FLAG_READ_ONLY | STREAM_FLAG_USE_BITMAP); + if(pStream1 && pStream2) + { + // For internet based files, we limit the number of operations + if((dwProvider & BASE_PROVIDER_MASK) == BASE_PROVIDER_HTTP) + nIterations = 0x80; + + FileStream_SetCallback(pStream2, TestReadFile_DownloadCallback, &Logger); + nError = CompareTwoLocalFilesRR(&Logger, pStream1, pStream2, nIterations); + } + + if(pStream2 != NULL) + FileStream_Close(pStream2); + if(pStream1 != NULL) + FileStream_Close(pStream1); + } + + return nError; +} + +// Test of the TFileStream object +static int TestSparseCompression() +{ + BYTE InpBuffer[0x1000]; + BYTE Compressed[0x1000]; + BYTE Decompressed[0x1000]; + int cbCompressed = sizeof(Compressed); + int cbDecompressed = sizeof(Compressed); + + // Prepare compressed buffer + memset(InpBuffer, 0, sizeof(InpBuffer)); + + // Compress and decompress + CompressSparse(Compressed, &cbCompressed, InpBuffer, sizeof(InpBuffer)); + DecompressSparse(Decompressed, &cbDecompressed, Compressed, cbCompressed); + + // Check the result of decompression + if(cbDecompressed != sizeof(InpBuffer)) + return ERROR_FILE_CORRUPT; + if(memcmp(Decompressed, InpBuffer, sizeof(InpBuffer))) + return ERROR_FILE_CORRUPT; + + return ERROR_SUCCESS; +} + +// Test of the TFileStream object +static int TestFileStreamOperations(const char * szPlainName, DWORD dwStreamFlags) +{ + TFileStream * pStream = NULL; + TLogHelper Logger("FileStreamTest", szPlainName); + ULONGLONG ByteOffset; + ULONGLONG FileSize = 0; + DWORD dwRequiredFlags = 0; + char szFullPath[MAX_PATH]; + BYTE Buffer[0x10]; + int nError = ERROR_SUCCESS; + + // Copy the file so we won't screw up + if((dwStreamFlags & STREAM_PROVIDER_MASK) == STREAM_PROVIDER_BLOCK4) + CreateFullPathName(szFullPath, szMpqSubDir, szPlainName); + else + nError = CreateFileCopy(&Logger, szPlainName, szPlainName, szFullPath); + + // Open the file stream + if(nError == ERROR_SUCCESS) + { + pStream = FileStream_OpenFileA(szFullPath, dwStreamFlags); + if(pStream == NULL) + return Logger.PrintError("Failed to open %s", szFullPath); + } + + // Get the size of the file stream + if(nError == ERROR_SUCCESS) + { + if(!FileStream_GetFlags(pStream, &dwStreamFlags)) + nError = Logger.PrintError("Failed to retrieve the stream flags"); + + if(!FileStream_GetSize(pStream, &FileSize)) + nError = Logger.PrintError("Failed to retrieve the file size"); + + // Any other stream except STREAM_PROVIDER_FLAT | BASE_PROVIDER_FILE should be read-only + if((dwStreamFlags & STREAM_PROVIDERS_MASK) != (STREAM_PROVIDER_FLAT | BASE_PROVIDER_FILE)) + dwRequiredFlags |= STREAM_FLAG_READ_ONLY; +// if(pStream->BlockPresent) +// dwRequiredFlags |= STREAM_FLAG_READ_ONLY; + + // Check the flags there + if((dwStreamFlags & dwRequiredFlags) != dwRequiredFlags) + { + Logger.PrintMessage("The stream should be read-only but it isn't"); + nError = ERROR_FILE_CORRUPT; + } + } + + // After successful open, the stream position must be zero + if(nError == ERROR_SUCCESS) + nError = VerifyFilePosition(&Logger, pStream, 0); + + // Read the MPQ header from the current file offset. + if(nError == ERROR_SUCCESS) + nError = VerifyFileMpqHeader(&Logger, pStream, NULL); + + // After successful open, the stream position must sizeof(TMPQHeader) + if(nError == ERROR_SUCCESS) + nError = VerifyFilePosition(&Logger, pStream, sizeof(TMPQHeader)); + + // Now try to read the MPQ header from the offset 0 + if(nError == ERROR_SUCCESS) + { + ByteOffset = 0; + nError = VerifyFileMpqHeader(&Logger, pStream, &ByteOffset); + } + + // After successful open, the stream position must sizeof(TMPQHeader) + if(nError == ERROR_SUCCESS) + nError = VerifyFilePosition(&Logger, pStream, sizeof(TMPQHeader)); + + // Try a write operation + if(nError == ERROR_SUCCESS) + { + bool bExpectedResult = (dwStreamFlags & STREAM_FLAG_READ_ONLY) ? false : true; + bool bResult; + + // Attempt to write to the file + ByteOffset = 0; + bResult = FileStream_Write(pStream, &ByteOffset, Buffer, sizeof(Buffer)); + + // If the result is not expected + if(bResult != bExpectedResult) + { + Logger.PrintMessage("FileStream_Write result is different than expected"); + nError = ERROR_FILE_CORRUPT; + } + } + + // Move the position 9 bytes from the end and try to read 10 bytes. + // This must fail, because stream reading functions are "all or nothing" + if(nError == ERROR_SUCCESS) + { + ByteOffset = FileSize - 9; + if(FileStream_Read(pStream, &ByteOffset, Buffer, 10)) + { + Logger.PrintMessage("FileStream_Read succeeded, but it shouldn't"); + nError = ERROR_FILE_CORRUPT; + } + } + + // Try again with 9 bytes. This must succeed, unless the file block is not available + if(nError == ERROR_SUCCESS) + { + ByteOffset = FileSize - 9; + if(!FileStream_Read(pStream, &ByteOffset, Buffer, 9)) + { + Logger.PrintMessage("FileStream_Read from the end of the file failed"); + nError = ERROR_FILE_CORRUPT; + } + } + + // Verify file position - it must be at the end of the file + if(nError == ERROR_SUCCESS) + nError = VerifyFilePosition(&Logger, pStream, FileSize); + + // Close the stream + if(pStream != NULL) + FileStream_Close(pStream); + return nError; +} + +static int TestOpenFile_OpenById(const char * szPlainName) +{ + TLogHelper Logger("OpenFileById", szPlainName); + TFileData * pFileData1 = NULL; + TFileData * pFileData2 = NULL; + HANDLE hMpq; + int nError; + + // Copy the archive so we won't fuck up the original one + nError = OpenExistingArchiveWithCopy(&Logger, szPlainName, NULL, &hMpq); + + // Now try to open a file without knowing the file name + if(nError == ERROR_SUCCESS) + { + // File00000023.xxx = music\dintro.wav + pFileData1 = LoadMpqFile(&Logger, hMpq, "File00000023.xxx"); + if(pFileData1 == NULL) + nError = Logger.PrintError("Failed to load the file %s", "File00000023.xxx"); + } + + // Now try to open the file again with its original name + if(nError == ERROR_SUCCESS) + { + // File00000023.xxx = music\dintro.wav + pFileData2 = LoadMpqFile(&Logger, hMpq, "music\\dintro.wav"); + if(pFileData2 == NULL) + nError = Logger.PrintError("Failed to load the file %s", "music\\dintro.wav"); + } + + // Now compare both files + if(nError == ERROR_SUCCESS) + { + if(!CompareTwoFiles(&Logger, pFileData1, pFileData1)) + nError = Logger.PrintError("The file has different size/content when open without name"); + } + + // Close the archive + if(pFileData2 != NULL) + STORM_FREE(pFileData2); + if(pFileData1 != NULL) + STORM_FREE(pFileData1); + if(hMpq != NULL) + SFileCloseArchive(hMpq); + return nError; +} + +static int TestOpenArchive(const char * szPlainName, const char * szListFile = NULL, bool bDontCopyArchive = false) +{ + TLogHelper Logger("OpenMpqTest", szPlainName); + TFileData * pFileData; + const char * szCopyName = (bDontCopyArchive) ? NULL : szPlainName; + HANDLE hMpq; + DWORD dwFileCount = 0; + DWORD dwTestFlags; + char szListFileBuff[MAX_PATH]; + bool bIsPartialMpq = false; + int nError; + + // If the file is a partial MPQ, don't load all files + bIsPartialMpq = (strstr(szPlainName, ".MPQ.part") != NULL); + + // Copy the archive so we won't fuck up the original one + nError = OpenExistingArchiveWithCopy(&Logger, szPlainName, szCopyName, &hMpq); + if(nError == ERROR_SUCCESS) + { + // If the listfile was given, add it to the MPQ + if(szListFile != NULL) + { + Logger.PrintProgress("Adding listfile %s ...", szListFile); + CreateFullPathName(szListFileBuff, szMpqSubDir, szListFile); + nError = SFileAddListFile(hMpq, szListFileBuff); + if(nError != ERROR_SUCCESS) + Logger.PrintMessage("Failed to add the listfile to the MPQ"); + } + + // Attempt to open the listfile and attributes + if(SFileHasFile(hMpq, LISTFILE_NAME)) + { + pFileData = LoadMpqFile(&Logger, hMpq, LISTFILE_NAME); + if(pFileData != NULL) + STORM_FREE(pFileData); + } + + // Attempt to open the listfile and attributes + if(SFileHasFile(hMpq, ATTRIBUTES_NAME)) + { + pFileData = LoadMpqFile(&Logger, hMpq, ATTRIBUTES_NAME); + if(pFileData != NULL) + STORM_FREE(pFileData); + } + + // Search the archive and load every file + dwTestFlags = bIsPartialMpq ? 0 : TEST_FLAG_LOAD_FILES; + nError = SearchArchive(&Logger, hMpq, dwTestFlags, &dwFileCount); + SFileCloseArchive(hMpq); + } + + return nError; +} + + +// Open an empty archive (found in WoW cache - it's just a header) +static int TestOpenArchive_WillFail(const char * szPlainName) +{ + TLogHelper Logger("FailMpqTest", szPlainName); + HANDLE hMpq = NULL; + TCHAR szMpqName[MAX_PATH]; + char szFullPath[MAX_PATH]; + + // Create the full path name for the archive + CreateFullPathName(szFullPath, szMpqSubDir, szPlainName); + CopyFileName(szMpqName, szFullPath, strlen(szFullPath)); + + // Try to open the archive. It is expected to fail + Logger.PrintProgress("Opening archive %s", szPlainName); + if(!SFileOpenArchive(szMpqName, 0, MPQ_OPEN_READ_ONLY, &hMpq)) + return ERROR_SUCCESS; + + Logger.PrintError("Archive %s succeeded to open, even if it should not.", szPlainName); + SFileCloseArchive(hMpq); + return ERROR_CAN_NOT_COMPLETE; +} + +static int TestOpenArchive_Corrupt(const char * szPlainName) +{ + TLogHelper Logger("OpenCorruptMpqTest", szPlainName); + HANDLE hMpq = NULL; + TCHAR szFullPathT[MAX_PATH]; + char szFullPath[MAX_PATH]; + + // Copy the archive so we won't fuck up the original one + CreateFullPathName(szFullPath, szMpqSubDir, szPlainName); + CopyFileName(szFullPathT, szFullPath, strlen(szFullPath)); + if(SFileOpenArchive(szFullPathT, 0, STREAM_FLAG_READ_ONLY, &hMpq)) + { + SFileCloseArchive(hMpq); + Logger.PrintMessage("Opening archive %s succeeded, but it shouldn't", szFullPath); + return ERROR_CAN_NOT_COMPLETE; + } + + return ERROR_SUCCESS; +} + + +// Opens a patched MPQ archive +static int TestOpenArchive_Patched(const char * PatchList[], const char * szPatchedFile, int nExpectedPatchCount) +{ + TLogHelper Logger("OpenPatchedMpqTest", PatchList[0]); + HANDLE hMpq; + HANDLE hFile; + BYTE Buffer[0x100]; + DWORD dwFileCount = 0; + DWORD BytesRead = 0; + int nError; + + // Open a patched MPQ archive + nError = OpenPatchedArchive(&Logger, &hMpq, PatchList); + if(nError == ERROR_SUCCESS) + { + // Check patch count + if(szPatchedFile != NULL) + nError = VerifyFilePatchCount(&Logger, hMpq, szPatchedFile, nExpectedPatchCount); + + // Try to open and read the file + if(nError == ERROR_SUCCESS) + { + if(SFileOpenFileEx(hMpq, szPatchedFile, 0, &hFile)) + { + SFileReadFile(hFile, Buffer, sizeof(Buffer), &BytesRead, NULL); + SFileCloseFile(hFile); + } + } + + // Search the archive and load every file + if(nError == ERROR_SUCCESS) + nError = SearchArchive(&Logger, hMpq, TEST_FLAG_LOAD_FILES, &dwFileCount); + + // Close the archive + SFileCloseArchive(hMpq); + } + + return nError; +} + +// Open an archive for read-only access +static int TestOpenArchive_ReadOnly(const char * szPlainName, bool bReadOnly) +{ + const char * szCopyName; + TLogHelper Logger("ReadOnlyTest", szPlainName); + HANDLE hMpq = NULL; + char szFullPathName[MAX_PATH]; + DWORD dwFlags = bReadOnly ? MPQ_OPEN_READ_ONLY : 0;; + int nExpectedError; + int nError; + + // Copy the fiel so we wont screw up something + szCopyName = bReadOnly ? "StormLibTest_ReadOnly.mpq" : "StormLibTest_ReadWrite.mpq"; + nError = CreateFileCopy(&Logger, szPlainName, szCopyName, szFullPathName); + + // Now open the archive for read-only access + if(nError == ERROR_SUCCESS) + nError = OpenExistingArchive(&Logger, szFullPathName, dwFlags, &hMpq); + + // Now try to add a file. This must fail if the MPQ is read only + if(nError == ERROR_SUCCESS) + { + nExpectedError = (bReadOnly) ? ERROR_ACCESS_DENIED : ERROR_SUCCESS; + AddFileToMpq(&Logger, hMpq, "AddedFile.txt", "This is an added file.", MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED, 0, nExpectedError); + } + + // Now try to rename a file in the MPQ. This must only succeed if the MPQ is not read only + if(nError == ERROR_SUCCESS) + { + nExpectedError = (bReadOnly) ? ERROR_ACCESS_DENIED : ERROR_SUCCESS; + RenameMpqFile(&Logger, hMpq, "spawn.mpq", "spawn-renamed.mpq", nExpectedError); + } + + // Now try to delete a file in the MPQ. This must only succeed if the MPQ is not read only + if(nError == ERROR_SUCCESS) + { + nExpectedError = (bReadOnly) ? ERROR_ACCESS_DENIED : ERROR_SUCCESS; + RemoveMpqFile(&Logger, hMpq, "spawn-renamed.mpq", nExpectedError); + } + + // Close the archive + if(hMpq != NULL) + SFileCloseArchive(hMpq); + return nError; +} + +static int TestOpenArchive_GetFileInfo(const char * szPlainName1, const char * szPlainName4) +{ + TLogHelper Logger("GetFileInfoTest", szPlainName1, szPlainName4); + HANDLE hFile; + HANDLE hMpq4; + HANDLE hMpq1; + DWORD cbLength; + BYTE DataBuff[0x400]; + int nError1; + int nError4; + + // Copy the archive so we won't fuck up the original one + nError1 = OpenExistingArchiveWithCopy(&Logger, szPlainName1, NULL, &hMpq1); + nError4 = OpenExistingArchiveWithCopy(&Logger, szPlainName4, NULL, &hMpq4); + if(nError1 == ERROR_SUCCESS && nError4 == ERROR_SUCCESS) + { + // Invalid handle - expected (false, ERROR_INVALID_HANDLE) + TestGetFileInfo(&Logger, NULL, SFileMpqBetHeader, NULL, 0, NULL, false, ERROR_INVALID_HANDLE); + + // Valid handle but invalid value of file info class (false, ERROR_INVALID_PARAMETER) + TestGetFileInfo(&Logger, NULL, (SFileInfoClass)0xFFF, NULL, 0, NULL, false, ERROR_INVALID_PARAMETER); + + // Valid archive handle but file info class is for file (false, ERROR_INVALID_HANDLE) + TestGetFileInfo(&Logger, NULL, SFileInfoNameHash1, NULL, 0, NULL, false, ERROR_INVALID_HANDLE); + + // Valid handle and all parameters NULL + // Returns (true, ERROR_SUCCESS), if BET table is present, otherwise (false, ERROR_CAN_NOT_COMPLETE) + TestGetFileInfo(&Logger, hMpq1, SFileMpqBetHeader, NULL, 0, NULL, false, ERROR_FILE_NOT_FOUND); + TestGetFileInfo(&Logger, hMpq4, SFileMpqBetHeader, NULL, 0, NULL, true, ERROR_SUCCESS); + + // Now try to retrieve the required size of the BET table header + TestGetFileInfo(&Logger, hMpq4, SFileMpqBetHeader, NULL, 0, &cbLength, true, ERROR_SUCCESS); + + // When we call SFileInfo with buffer = NULL and nonzero buffer size, it is ignored + TestGetFileInfo(&Logger, hMpq4, SFileMpqBetHeader, NULL, 3, &cbLength, true, ERROR_SUCCESS); + + // When we call SFileInfo with buffer != NULL and nonzero buffer size, it should return error + TestGetFileInfo(&Logger, hMpq4, SFileMpqBetHeader, DataBuff, 3, &cbLength, false, ERROR_INSUFFICIENT_BUFFER); + + // Request for bet table header should also succeed if we want header only + TestGetFileInfo(&Logger, hMpq4, SFileMpqBetHeader, DataBuff, sizeof(TMPQBetHeader), &cbLength, true, ERROR_SUCCESS); + + // Request for bet table header should also succeed if we want header+flag table only + TestGetFileInfo(&Logger, hMpq4, SFileMpqBetHeader, DataBuff, sizeof(DataBuff), &cbLength, true, ERROR_SUCCESS); + + // Try to retrieve strong signature from the MPQ + TestGetFileInfo(&Logger, hMpq1, SFileMpqStrongSignature, NULL, 0, NULL, true, ERROR_SUCCESS); + TestGetFileInfo(&Logger, hMpq4, SFileMpqStrongSignature, NULL, 0, NULL, false, ERROR_FILE_NOT_FOUND); + + // Strong signature is returned including the signature ID + TestGetFileInfo(&Logger, hMpq1, SFileMpqStrongSignature, NULL, 0, &cbLength, true, ERROR_SUCCESS); + assert(cbLength == MPQ_STRONG_SIGNATURE_SIZE + 4); + + // Retrieve the signature + TestGetFileInfo(&Logger, hMpq1, SFileMpqStrongSignature, DataBuff, sizeof(DataBuff), &cbLength, true, ERROR_SUCCESS); + assert(memcmp(DataBuff, "NGIS", 4) == 0); + + // Check SFileGetFileInfo on + if(SFileOpenFileEx(hMpq4, LISTFILE_NAME, 0, &hFile)) + { + // Valid parameters but the handle should be file handle + TestGetFileInfo(&Logger, hMpq4, SFileInfoFileTime, DataBuff, sizeof(DataBuff), &cbLength, false, ERROR_INVALID_HANDLE); + + // Valid parameters + TestGetFileInfo(&Logger, hFile, SFileInfoFileTime, DataBuff, sizeof(DataBuff), &cbLength, true, ERROR_SUCCESS); + + SFileCloseFile(hFile); + } + } + + if(hMpq4 != NULL) + SFileCloseArchive(hMpq4); + if(hMpq1 != NULL) + SFileCloseArchive(hMpq1); + return ERROR_SUCCESS; +} + +static int TestOpenArchive_MasterMirror(const char * szMirrorName, const char * szMasterName, const char * szFileToExtract, bool bCopyMirrorFile) +{ + TFileData * pFileData; + TLogHelper Logger("OpenServerMirror", szMirrorName); + HANDLE hFile = NULL; + HANDLE hMpq = NULL; + DWORD dwVerifyResult; + char szMirrorPath[MAX_PATH + MAX_PATH]; // Combined name + char szMasterPath[MAX_PATH]; // Original (server) name + int nError; + + // Create both paths + nError = CreateMasterAndMirrorPaths(&Logger, szMirrorPath, szMasterPath, szMirrorName, szMasterName, bCopyMirrorFile); + + // Now open both archives as local-server pair + if(nError == ERROR_SUCCESS) + { + nError = OpenExistingArchive(&Logger, szMirrorPath, 0, &hMpq); + } + + // The MPQ must be read-only. Writing to mirrored MPQ is not allowed + if(nError == ERROR_SUCCESS) + { + if(SFileCreateFile(hMpq, "AddedFile.bin", 0, 0x10, 0, MPQ_FILE_COMPRESS, &hFile)) + { + SFileCloseFile(hFile); + Logger.PrintMessage("The archive is writable, although it should not be"); + nError = ERROR_FILE_CORRUPT; + } + } + + // Verify the file + if(nError == ERROR_SUCCESS && szFileToExtract != NULL) + { + dwVerifyResult = SFileVerifyFile(hMpq, szFileToExtract, SFILE_VERIFY_ALL); + if(dwVerifyResult & VERIFY_FILE_ERROR_MASK) + { + Logger.PrintMessage("File verification failed"); + nError = ERROR_FILE_CORRUPT; + } + } + + // Load the file to memory + if(nError == ERROR_SUCCESS && szFileToExtract) + { + pFileData = LoadMpqFile(&Logger, hMpq, szFileToExtract); + if(pFileData != NULL) + STORM_FREE(pFileData); + } + + if(hMpq != NULL) + SFileCloseArchive(hMpq); + return nError; +} + + +static int TestOpenArchive_VerifySignature(const char * szPlainName, const char * szOriginalName) +{ + TLogHelper Logger("VerifySignatureTest", szPlainName); + HANDLE hMpq; + DWORD dwSignatures = 0; + int nVerifyError; + int nError = ERROR_SUCCESS; + + // We need original name for the signature check + nError = OpenExistingArchiveWithCopy(&Logger, szPlainName, szOriginalName, &hMpq); + if(nError == ERROR_SUCCESS) + { + // Query the signature types + Logger.PrintProgress("Retrieving signatures ..."); + TestGetFileInfo(&Logger, hMpq, SFileMpqSignatures, &dwSignatures, sizeof(DWORD), NULL, true, ERROR_SUCCESS); + + // Verify any of the present signatures + Logger.PrintProgress("Verifying archive signature ..."); + nVerifyError = SFileVerifyArchive(hMpq); + + // Verify the result + if((dwSignatures & SIGNATURE_TYPE_STRONG) && (nVerifyError != ERROR_STRONG_SIGNATURE_OK)) + { + Logger.PrintMessage("Strong signature verification error"); + nError = ERROR_FILE_CORRUPT; + } + + // Verify the result + if((dwSignatures & SIGNATURE_TYPE_WEAK) && (nVerifyError != ERROR_WEAK_SIGNATURE_OK)) + { + Logger.PrintMessage("Weak signature verification error"); + nError = ERROR_FILE_CORRUPT; + } + + SFileCloseArchive(hMpq); + } + return nError; +} + +static int TestOpenArchive_ModifySigned(const char * szPlainName, const char * szOriginalName) +{ + TLogHelper Logger("ModifySignedTest", szPlainName); + HANDLE hMpq = NULL; + int nVerifyError; + int nError = ERROR_SUCCESS; + + // We need original name for the signature check + nError = OpenExistingArchiveWithCopy(&Logger, szPlainName, szOriginalName, &hMpq); + if(nError == ERROR_SUCCESS) + { + // Verify the weak signature + Logger.PrintProgress("Verifying archive signature ..."); + nVerifyError = SFileVerifyArchive(hMpq); + + // Check the result signature + if(nVerifyError != ERROR_WEAK_SIGNATURE_OK) + { + Logger.PrintMessage("Weak signature verification error"); + nError = ERROR_FILE_CORRUPT; + } + } + + // Add a file and verify the signature again + if(nError == ERROR_SUCCESS) + { + // Verify any of the present signatures + Logger.PrintProgress("Modifying signed archive ..."); + nError = AddFileToMpq(&Logger, hMpq, "AddedFile01.txt", "This is a file added to signed MPQ", 0, 0, ERROR_SUCCESS); + } + + // Verify the signature again + if(nError == ERROR_SUCCESS) + { + // Verify the weak signature + Logger.PrintProgress("Verifying archive signature ..."); + nVerifyError = SFileVerifyArchive(hMpq); + + // Check the result signature + if(nVerifyError != ERROR_WEAK_SIGNATURE_OK) + { + Logger.PrintMessage("Weak signature verification error"); + nError = ERROR_FILE_CORRUPT; + } + } + + // Close the MPQ + if(hMpq != NULL) + SFileCloseArchive(hMpq); + return nError; +} + +static int TestOpenArchive_SignExisting(const char * szPlainName) +{ + TLogHelper Logger("SignExistingMpq", szPlainName); + HANDLE hMpq = NULL; + int nVerifyError; + int nError = ERROR_SUCCESS; + + // We need original name for the signature check + nError = OpenExistingArchiveWithCopy(&Logger, szPlainName, szPlainName, &hMpq); + if(nError == ERROR_SUCCESS) + { + // Verify the weak signature + Logger.PrintProgress("Verifying archive signature ..."); + nVerifyError = SFileVerifyArchive(hMpq); + + // Check the result signature + if(nVerifyError != ERROR_NO_SIGNATURE) + { + Logger.PrintMessage("There already is a signature in the MPQ"); + nError = ERROR_FILE_CORRUPT; + } + } + + // Add a file and verify the signature again + if(nError == ERROR_SUCCESS) + { + // Verify any of the present signatures + Logger.PrintProgress("Signing the MPQ ..."); + if(!SFileSignArchive(hMpq, SIGNATURE_TYPE_WEAK)) + { + Logger.PrintMessage("Failed to create archive signature"); + nError = ERROR_FILE_CORRUPT; + } + } + + // Verify the signature again + if(nError == ERROR_SUCCESS) + { + // Verify the weak signature + Logger.PrintProgress("Verifying archive signature ..."); + nVerifyError = SFileVerifyArchive(hMpq); + + // Check the result signature + if(nVerifyError != ERROR_WEAK_SIGNATURE_OK) + { + Logger.PrintMessage("Weak signature verification error"); + nError = ERROR_FILE_CORRUPT; + } + } + + // Close the MPQ + if(hMpq != NULL) + SFileCloseArchive(hMpq); + return nError; +} + +// Open an empty archive (found in WoW cache - it's just a header) +static int TestOpenArchive_CraftedUserData(const char * szPlainName, const char * szCopyName) +{ + TLogHelper Logger("CraftedMpqTest", szPlainName); + HANDLE hMpq; + DWORD dwFileCount1 = 0; + DWORD dwFileCount2 = 0; + BYTE FileHash1[MD5_DIGEST_SIZE]; + BYTE FileHash2[MD5_DIGEST_SIZE]; + char szFullPath[MAX_PATH]; + int nError; + + // Create copy of the archive, with interleaving some user data + nError = CreateFileCopy(&Logger, szPlainName, szCopyName, szFullPath, 0x400, 0x531); + + // Open the archive and load some files + if(nError == ERROR_SUCCESS) + { + // Open the archive + nError = OpenExistingArchive(&Logger, szFullPath, 0, &hMpq); + if(nError != ERROR_SUCCESS) + return nError; + + // Verify presence of (listfile) and (attributes) + CheckIfFileIsPresent(&Logger, hMpq, LISTFILE_NAME, true); + CheckIfFileIsPresent(&Logger, hMpq, ATTRIBUTES_NAME, true); + + // Search the archive and load every file + nError = SearchArchive(&Logger, hMpq, TEST_FLAG_LOAD_FILES | TEST_FLAG_HASH_FILES, &dwFileCount1, FileHash1); + SFileCloseArchive(hMpq); + } + + // Try to compact the MPQ + if(nError == ERROR_SUCCESS) + { + // Open the archive again + nError = OpenExistingArchive(&Logger, szFullPath, 0, &hMpq); + if(nError != ERROR_SUCCESS) + return nError; + + // Compact the archive + Logger.PrintProgress("Compacting archive %s ...", GetShortPlainName(szFullPath)); + if(!SFileSetCompactCallback(hMpq, CompactCallback, &Logger)) + nError = Logger.PrintError("Failed to compact archive %s", szFullPath); + + SFileCompactArchive(hMpq, NULL, false); + SFileCloseArchive(hMpq); + } + + // Open the archive and load some files + if(nError == ERROR_SUCCESS) + { + // Open the archive + nError = OpenExistingArchive(&Logger, szFullPath, 0, &hMpq); + if(nError != ERROR_SUCCESS) + return nError; + + // Verify presence of (listfile) and (attributes) + CheckIfFileIsPresent(&Logger, hMpq, LISTFILE_NAME, true); + CheckIfFileIsPresent(&Logger, hMpq, ATTRIBUTES_NAME, true); + + // Search the archive and load every file + nError = SearchArchive(&Logger, hMpq, TEST_FLAG_LOAD_FILES | TEST_FLAG_HASH_FILES, &dwFileCount2, FileHash2); + SFileCloseArchive(hMpq); + } + + // Compare the file counts and their hashes + if(nError == ERROR_SUCCESS) + { + if(dwFileCount2 != dwFileCount1) + Logger.PrintMessage("Different file count after compacting archive: %u vs %u", dwFileCount2, dwFileCount1); + + if(memcmp(FileHash2, FileHash1, MD5_DIGEST_SIZE)) + Logger.PrintMessage("Different file hash after compacting archive"); + } + + return nError; +} + +static int ForEachFile_VerifyFileChecksum(const char * szFullPath) +{ + const char * szShortPlainName = GetShortPlainName(szFullPath); + TFileData * pFileData; + char * szExtension; + char szShaFileName[MAX_PATH+1]; + char szSha1Text[0x40]; + int nError = ERROR_SUCCESS; + + // Try to load the file with the SHA extension + StringCopyA(szShaFileName, szFullPath, MAX_PATH); + szExtension = strrchr(szShaFileName, '.'); + if(szExtension == NULL) + return ERROR_SUCCESS; + + // Skip .SHA and .TXT files + if(!_stricmp(szExtension, ".sha") || !_stricmp(szExtension, ".txt")) + return ERROR_SUCCESS; + + // Load the local file to memory + strcpy(szExtension, ".sha"); + pFileData = LoadLocalFile(NULL, szShaFileName, false); + if(pFileData != NULL) + { + TLogHelper Logger("VerifyFileHash", szShortPlainName); + + // Calculate SHA1 of the entire file + nError = CalculateFileSha1(&Logger, szFullPath, szSha1Text); + if(nError == ERROR_SUCCESS) + { + // Compare with what we loaded from the file + if(pFileData->dwFileSize >= (SHA1_DIGEST_SIZE * 2)) + { + // Compare the SHA1 + if(_strnicmp(szSha1Text, (char *)pFileData->FileData, (SHA1_DIGEST_SIZE * 2))) + { + Logger.PrintError("File CRC check failed: %s", szFullPath); + nError = ERROR_FILE_CORRUPT; + } + } + } + + STORM_FREE(pFileData); + } + + return nError; +} + +// Opens a found archive +static int ForEachFile_OpenArchive(const char * szFullPath) +{ + HANDLE hMpq = NULL; + DWORD dwFileCount = 0; + int nError = ERROR_SUCCESS; + + // Check if it's a MPQ file type + if(IsMpqExtension(szFullPath)) + { + TLogHelper Logger("OpenEachMpqTest", GetShortPlainName(szFullPath)); + + // Open the MPQ name + nError = OpenExistingArchive(&Logger, szFullPath, 0, &hMpq); + if(nError == ERROR_AVI_FILE || nError == ERROR_FILE_CORRUPT || nError == ERROR_BAD_FORMAT) + return ERROR_SUCCESS; + + // Search the archive and load every file + if(nError == ERROR_SUCCESS) + { + nError = SearchArchive(&Logger, hMpq, 0, &dwFileCount); + SFileCloseArchive(hMpq); + } + } + + // Correct some errors + if(nError == ERROR_FILE_CORRUPT || nError == ERROR_FILE_INCOMPLETE) + return ERROR_SUCCESS; + return nError; +} + +// Adding a file to MPQ that had size of the file table equal +// or greater than hash table, but has free entries +static int TestAddFile_FullTable(const char * szFullMpqName) +{ + TLogHelper Logger("FullMpqTest", szFullMpqName); + const char * szFileName = "AddedFile001.txt"; + const char * szFileData = "0123456789ABCDEF"; + HANDLE hMpq = NULL; + int nError = ERROR_SUCCESS; + + // Copy the archive so we won't fuck up the original one + nError = OpenExistingArchiveWithCopy(&Logger, szFullMpqName, szFullMpqName, &hMpq); + if(nError == ERROR_SUCCESS) + { + // Attempt to add a file + nError = AddFileToMpq(&Logger, hMpq, szFileName, szFileData, MPQ_FILE_IMPLODE, MPQ_COMPRESSION_PKWARE, ERROR_SUCCESS); + SFileCloseArchive(hMpq); + } + + return nError; +} + +// Adding a file to MPQ that had no (listfile) and no (attributes). +// We expect that neither of these will be present after the archive is closed +static int TestAddFile_ListFileTest(const char * szSourceMpq, bool bShouldHaveListFile, bool bShouldHaveAttributes) +{ + TLogHelper Logger("ListFileTest", szSourceMpq); + TFileData * pFileData = NULL; + const char * szBackupMpq = bShouldHaveListFile ? "StormLibTest_HasListFile.mpq" : "StormLibTest_NoListFile.mpq"; + const char * szFileName = "AddedFile001.txt"; + const char * szFileData = "0123456789ABCDEF"; + HANDLE hMpq = NULL; + DWORD dwFileSize = (DWORD)strlen(szFileData); + int nError = ERROR_SUCCESS; + + // Copy the archive so we won't fuck up the original one + nError = OpenExistingArchiveWithCopy(&Logger, szSourceMpq, szBackupMpq, &hMpq); + + // Add a file + if(nError == ERROR_SUCCESS) + { + // Now add a file + nError = AddFileToMpq(&Logger, hMpq, szFileName, szFileData, MPQ_FILE_IMPLODE, MPQ_COMPRESSION_PKWARE); + SFileCloseArchive(hMpq); + hMpq = NULL; + } + + // Now reopen the archive + if(nError == ERROR_SUCCESS) + nError = OpenExistingArchiveWithCopy(&Logger, NULL, szBackupMpq, &hMpq); + + // Now the file has been written and the MPQ has been saved. + // We Reopen the MPQ and check if there is no (listfile) nor (attributes). + if(nError == ERROR_SUCCESS) + { + // Verify presence of (listfile) and (attributes) + CheckIfFileIsPresent(&Logger, hMpq, LISTFILE_NAME, bShouldHaveListFile); + CheckIfFileIsPresent(&Logger, hMpq, ATTRIBUTES_NAME, bShouldHaveAttributes); + + // Try to open the file that we recently added + pFileData = LoadMpqFile(&Logger, hMpq, szFileName); + if(pFileData != NULL) + { + // Verify if the file size matches + if(pFileData->dwFileSize == dwFileSize) + { + // Verify if the file data match + if(memcmp(pFileData->FileData, szFileData, dwFileSize)) + { + Logger.PrintError("The data of the added file does not match"); + nError = ERROR_FILE_CORRUPT; + } + } + else + { + Logger.PrintError("The size of the added file does not match"); + nError = ERROR_FILE_CORRUPT; + } + + // Delete the file data + STORM_FREE(pFileData); + } + else + { + nError = Logger.PrintError("Failed to open the file previously added"); + } + } + + // Close the MPQ archive + if(hMpq != NULL) + SFileCloseArchive(hMpq); + return nError; +} +/* +static int TestCreateArchive_Deprotect(const char * szPlainName) +{ + TLogHelper Logger("DeprotectTest", szPlainName); + HANDLE hMpq1 = NULL; + HANDLE hMpq2 = NULL; + char szMpqName1[MAX_PATH]; + char szMpqName2[MAX_PATH]; + BYTE FileHash1[MD5_DIGEST_SIZE]; + BYTE FileHash2[MD5_DIGEST_SIZE]; + DWORD dwFileCount1 = 0; + DWORD dwFileCount2 = 0; + DWORD dwTestFlags = TEST_FLAG_LOAD_FILES | TEST_FLAG_HASH_FILES; + int nError = ERROR_SUCCESS; + + // First copy: The original (untouched) file + if(nError == ERROR_SUCCESS) + { + AddStringBeforeExtension(szMpqName1, szPlainName, "_original"); + nError = OpenExistingArchiveWithCopy(&Logger, szPlainName, szMpqName1, &hMpq1); + if(nError != ERROR_SUCCESS) + Logger.PrintMessage("Failed to open %s", szMpqName1); + } + + // Second copy: Will be deprotected + if(nError == ERROR_SUCCESS) + { + AddStringBeforeExtension(szMpqName2, szPlainName, "_deprotected"); + nError = OpenExistingArchiveWithCopy(&Logger, szPlainName, szMpqName2, &hMpq2); + if(nError != ERROR_SUCCESS) + Logger.PrintMessage("Failed to open %s", szMpqName2); + } + + // Deprotect the second map + if(nError == ERROR_SUCCESS) + { + SFileSetCompactCallback(hMpq2, CompactCallback, &Logger); + if(!SFileCompactArchive(hMpq2, NULL, false)) + nError = Logger.PrintError("Failed to deprotect archive %s", szMpqName2); + } + + // Calculate number of files and compare their hash (archive 1) + if(nError == ERROR_SUCCESS) + { + memset(FileHash1, 0, sizeof(FileHash1)); + nError = SearchArchive(&Logger, hMpq1, dwTestFlags, &dwFileCount1, FileHash1); + } + + // Calculate number of files and compare their hash (archive 2) + if(nError == ERROR_SUCCESS) + { + memset(FileHash1, 0, sizeof(FileHash2)); + nError = SearchArchive(&Logger, hMpq2, dwTestFlags, &dwFileCount2, FileHash2); + } + + if(nError == ERROR_SUCCESS) + { + if(dwFileCount1 != dwFileCount2) + Logger.PrintMessage("Different file count (%u in %s; %u in %s)", dwFileCount1, szMpqName1, dwFileCount2, szMpqName2); + if(memcmp(FileHash1, FileHash2, MD5_DIGEST_SIZE)) + Logger.PrintMessage("Different file hash (%s vs %s)", szMpqName1, szMpqName2); + } + + // Close both MPQs + if(hMpq2 != NULL) + SFileCloseArchive(hMpq2); + if(hMpq1 != NULL) + SFileCloseArchive(hMpq1); + return nError; +} +*/ +static int TestCreateArchive_EmptyMpq(const char * szPlainName, DWORD dwCreateFlags) +{ + TLogHelper Logger("CreateEmptyMpq", szPlainName); + HANDLE hMpq = NULL; + DWORD dwFileCount = 0; + int nError; + + // Create the full path name + nError = CreateNewArchive(&Logger, szPlainName, dwCreateFlags, 0, &hMpq); + if(nError == ERROR_SUCCESS) + { + SearchArchive(&Logger, hMpq); + SFileCloseArchive(hMpq); + } + + // Reopen the empty MPQ + if(nError == ERROR_SUCCESS) + { + nError = OpenExistingArchiveWithCopy(&Logger, NULL, szPlainName, &hMpq); + if(nError == ERROR_SUCCESS) + { + SFileGetFileInfo(hMpq, SFileMpqNumberOfFiles, &dwFileCount, sizeof(dwFileCount), NULL); + + CheckIfFileIsPresent(&Logger, hMpq, "File00000000.xxx", false); + CheckIfFileIsPresent(&Logger, hMpq, LISTFILE_NAME, false); + SearchArchive(&Logger, hMpq); + SFileCloseArchive(hMpq); + } + } + + return nError; +} + +static int TestCreateArchive_TestGaps(const char * szPlainName) +{ + TLogHelper Logger("CreateGapsTest", szPlainName); + ULONGLONG ByteOffset1 = 0xFFFFFFFF; + ULONGLONG ByteOffset2 = 0xEEEEEEEE; + HANDLE hMpq = NULL; + HANDLE hFile = NULL; + char szFullPath[MAX_PATH]; + int nError = ERROR_SUCCESS; + + // Create new MPQ + nError = CreateNewArchive_V2(&Logger, szPlainName, MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES | MPQ_FORMAT_VERSION_4, 4000, &hMpq); + if(nError == ERROR_SUCCESS) + { + // Add one file and flush the archive + nError = AddFileToMpq(&Logger, hMpq, "AddedFile01.txt", "This is the file data.", MPQ_FILE_COMPRESS); + SFileCloseArchive(hMpq); + hMpq = NULL; + } + + // Reopen the MPQ and add another file. + // The new file must be added to the position of the (listfile) + if(nError == ERROR_SUCCESS) + { + CreateFullPathName(szFullPath, NULL, szPlainName); + nError = OpenExistingArchive(&Logger, szFullPath, 0, &hMpq); + if(nError == ERROR_SUCCESS) + { + // Retrieve the position of the (listfile) + if(SFileOpenFileEx(hMpq, LISTFILE_NAME, 0, &hFile)) + { + SFileGetFileInfo(hFile, SFileInfoByteOffset, &ByteOffset1, sizeof(ULONGLONG), NULL); + SFileCloseFile(hFile); + } + else + nError = GetLastError(); + } + } + + // Add another file and check its position. It must be at the position of the former listfile + if(nError == ERROR_SUCCESS) + { + const char * szAddedFile = "AddedFile02.txt"; + + // Add another file + nError = AddFileToMpq(&Logger, hMpq, szAddedFile, "This is the second added file.", MPQ_FILE_COMPRESS); + + // Retrieve the position of the (listfile) + if(SFileOpenFileEx(hMpq, szAddedFile, 0, &hFile)) + { + SFileGetFileInfo(hFile, SFileInfoByteOffset, &ByteOffset2, sizeof(ULONGLONG), NULL); + SFileCloseFile(hFile); + } + else + nError = GetLastError(); + } + + // Now check the positions + if(nError == ERROR_SUCCESS) + { + if(ByteOffset1 != ByteOffset2) + { + Logger.PrintError("The added file was not written to the position of (listfile)"); + nError = ERROR_FILE_CORRUPT; + } + } + + // Close the archive if needed + if(hMpq != NULL) + SFileCloseArchive(hMpq); + return nError; +} + +static int TestCreateArchive_Signed(const char * szPlainName, bool bSignAtCreate) +{ + TLogHelper Logger("CreateSignedMpq", szPlainName); + HANDLE hMpq = NULL; + DWORD dwCreateFlags = MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES | MPQ_FORMAT_VERSION_1; + DWORD dwSignatures = 0; + DWORD nVerifyError = 0; + int nError = ERROR_SUCCESS; + + // Method 1: Create the archive as signed + if(bSignAtCreate) + dwCreateFlags |= MPQ_CREATE_SIGNATURE; + + // Create new MPQ + nError = CreateNewArchive_V2(&Logger, szPlainName, dwCreateFlags, 4000, &hMpq); + if(nError == ERROR_SUCCESS) + { + // Add one file and flush the archive + nError = AddFileToMpq(&Logger, hMpq, "AddedFile01.txt", "This is the file data.", MPQ_FILE_COMPRESS); + } + + // Sign the archive with weak signature + if(nError == ERROR_SUCCESS) + { + if(!SFileSignArchive(hMpq, SIGNATURE_TYPE_WEAK)) + nError = ERROR_SUCCESS; + } + + // Reopen the MPQ and add another file. + // The new file must be added to the position of the (listfile) + if(nError == ERROR_SUCCESS) + { + // Query the signature types + Logger.PrintProgress("Retrieving signatures ..."); + TestGetFileInfo(&Logger, hMpq, SFileMpqSignatures, &dwSignatures, sizeof(DWORD), NULL, true, ERROR_SUCCESS); + + // Verify any of the present signatures + Logger.PrintProgress("Verifying archive signature ..."); + nVerifyError = SFileVerifyArchive(hMpq); + + // Verify the result + if((dwSignatures != SIGNATURE_TYPE_WEAK) && (nVerifyError != ERROR_WEAK_SIGNATURE_OK)) + { + Logger.PrintMessage("Weak signature verification error"); + nError = ERROR_FILE_CORRUPT; + } + } + + // Close the archive + if(hMpq != NULL) + SFileCloseArchive(hMpq); + return nError; +} + +static int TestCreateArchive_MpqEditor(const char * szPlainName, const char * szFileName) +{ + TLogHelper Logger("CreateMpqEditor", szPlainName); + HANDLE hMpq = NULL; + int nError = ERROR_SUCCESS; + + // Create new MPQ + nError = CreateNewArchive_V2(&Logger, szPlainName, MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES, 4000, &hMpq); + if(nError == ERROR_SUCCESS) + { + // Flush the archive first + SFileFlushArchive(hMpq); + + // Add one file + nError = AddFileToMpq(&Logger, hMpq, szFileName, "This is the file data.", MPQ_FILE_COMPRESS); + + // Flush the archive again + SFileFlushArchive(hMpq); + SFileCloseArchive(hMpq); + } + else + { + nError = GetLastError(); + } + + return nError; +} + +static int TestCreateArchive_FillArchive(const char * szPlainName, DWORD dwCreateFlags) +{ + TLogHelper Logger("CreateFullMpq", szPlainName); + const char * szFileData = "TestCreateArchive_FillArchive: Testing file data"; + char szFileName[MAX_PATH]; + HANDLE hMpq = NULL; + DWORD dwMaxFileCount = 6; + DWORD dwCompression = MPQ_COMPRESSION_ZLIB; + DWORD dwFlags = MPQ_FILE_ENCRYPTED | MPQ_FILE_COMPRESS; + int nError; + + // + // Note that StormLib will round the maxfile count + // up to hash table size (nearest power of two) + // + if((dwCreateFlags & MPQ_CREATE_LISTFILE) == 0) + dwMaxFileCount++; + if((dwCreateFlags & MPQ_CREATE_ATTRIBUTES) == 0) + dwMaxFileCount++; + + // Create the new MPQ archive + nError = CreateNewArchive_V2(&Logger, szPlainName, dwCreateFlags, dwMaxFileCount, &hMpq); + if(nError == ERROR_SUCCESS) + { + // Flush the archive first + SFileFlushArchive(hMpq); + + // Add all files + for(unsigned int i = 0; i < dwMaxFileCount; i++) + { + sprintf(szFileName, "AddedFile%03u.txt", i); + nError = AddFileToMpq(&Logger, hMpq, szFileName, szFileData, dwFlags, dwCompression); + if(nError != ERROR_SUCCESS) + break; + } + + // Flush the archive again + SFileFlushArchive(hMpq); + } + + // Now the MPQ should be full. It must not be possible to add another file + if(nError == ERROR_SUCCESS) + { + nError = AddFileToMpq(&Logger, hMpq, "ShouldNotBeHere.txt", szFileData, MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB, ERROR_DISK_FULL); + assert(nError != ERROR_SUCCESS); + nError = ERROR_SUCCESS; + } + + // Close the archive to enforce saving all tables + if(hMpq != NULL) + SFileCloseArchive(hMpq); + hMpq = NULL; + + // Reopen the archive again + if(nError == ERROR_SUCCESS) + nError = OpenExistingArchiveWithCopy(&Logger, NULL, szPlainName, &hMpq); + + // The archive should still be full + if(nError == ERROR_SUCCESS) + { + CheckIfFileIsPresent(&Logger, hMpq, LISTFILE_NAME, (dwCreateFlags & MPQ_CREATE_LISTFILE) ? true : false); + CheckIfFileIsPresent(&Logger, hMpq, ATTRIBUTES_NAME, (dwCreateFlags & MPQ_CREATE_ATTRIBUTES) ? true : false); + nError = AddFileToMpq(&Logger, hMpq, "ShouldNotBeHere.txt", szFileData, MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB, ERROR_DISK_FULL); + assert(nError != ERROR_SUCCESS); + nError = ERROR_SUCCESS; + } + + // The (listfile) and (attributes) must be present + if(nError == ERROR_SUCCESS) + { + CheckIfFileIsPresent(&Logger, hMpq, LISTFILE_NAME, (dwCreateFlags & MPQ_CREATE_LISTFILE) ? true : false); + CheckIfFileIsPresent(&Logger, hMpq, ATTRIBUTES_NAME, (dwCreateFlags & MPQ_CREATE_ATTRIBUTES) ? true : false); + nError = RemoveMpqFile(&Logger, hMpq, szFileName, ERROR_SUCCESS); + } + + // Now add the file again. This time, it should be possible OK + if(nError == ERROR_SUCCESS) + { + nError = AddFileToMpq(&Logger, hMpq, szFileName, szFileData, dwFlags, dwCompression, ERROR_SUCCESS); + assert(nError == ERROR_SUCCESS); + } + + // Now add the file again. This time, it should fail + if(nError == ERROR_SUCCESS) + { + nError = AddFileToMpq(&Logger, hMpq, szFileName, szFileData, dwFlags, dwCompression, ERROR_ALREADY_EXISTS); + assert(nError != ERROR_SUCCESS); + nError = ERROR_SUCCESS; + } + + // Now add the file again. This time, it should fail + if(nError == ERROR_SUCCESS) + { + nError = AddFileToMpq(&Logger, hMpq, "ShouldNotBeHere.txt", szFileData, dwFlags, dwCompression, ERROR_DISK_FULL); + assert(nError != ERROR_SUCCESS); + nError = ERROR_SUCCESS; + } + + // Close the archive and return + if(hMpq != NULL) + SFileCloseArchive(hMpq); + hMpq = NULL; + + // Reopen the archive for the third time to verify that both internal files are there + if(nError == ERROR_SUCCESS) + { + nError = OpenExistingArchiveWithCopy(&Logger, NULL, szPlainName, &hMpq); + if(nError == ERROR_SUCCESS) + { + CheckIfFileIsPresent(&Logger, hMpq, LISTFILE_NAME, (dwCreateFlags & MPQ_CREATE_LISTFILE) ? true : false); + CheckIfFileIsPresent(&Logger, hMpq, ATTRIBUTES_NAME, (dwCreateFlags & MPQ_CREATE_ATTRIBUTES) ? true : false); + SFileCloseArchive(hMpq); + } + } + + return nError; +} + +static int TestCreateArchive_IncMaxFileCount(const char * szPlainName) +{ + TLogHelper Logger("IncMaxFileCount", szPlainName); + const char * szFileData = "TestCreateArchive_IncMaxFileCount: Testing file data"; + char szFileName[MAX_PATH]; + HANDLE hMpq = NULL; + DWORD dwMaxFileCount = 1; + int nError; + + // Create the new MPQ + nError = CreateNewArchive(&Logger, szPlainName, MPQ_CREATE_ARCHIVE_V4 | MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES, dwMaxFileCount, &hMpq); + + // Now add exactly one file + if(nError == ERROR_SUCCESS) + { + nError = AddFileToMpq(&Logger, hMpq, "AddFile_base.txt", szFileData); + SFileFlushArchive(hMpq); + SFileCloseArchive(hMpq); + } + + // Now add 10 files. Each time we cannot add the file due to archive being full, + // we increment the max file count + if(nError == ERROR_SUCCESS) + { + for(unsigned int i = 0; i < 10; i++) + { + // Open the archive again + nError = OpenExistingArchiveWithCopy(&Logger, NULL, szPlainName, &hMpq); + if(nError != ERROR_SUCCESS) + break; + + // Add one file + sprintf(szFileName, "AddFile_%04u.txt", i); + nError = AddFileToMpq(&Logger, hMpq, szFileName, szFileData, 0, 0, ERROR_UNDETERMINED_RESULT); + if(nError != ERROR_SUCCESS) + { + // Increment the max file count by one + dwMaxFileCount = SFileGetMaxFileCount(hMpq) + 1; + Logger.PrintProgress("Increasing max file count to %u ...", dwMaxFileCount); + SFileSetMaxFileCount(hMpq, dwMaxFileCount); + + // Attempt to create the file again + nError = AddFileToMpq(&Logger, hMpq, szFileName, szFileData, 0, 0, ERROR_SUCCESS); + } + + // Compact the archive and close it + SFileSetCompactCallback(hMpq, CompactCallback, &Logger); + SFileCompactArchive(hMpq, NULL, false); + SFileCloseArchive(hMpq); + if(nError != ERROR_SUCCESS) + break; + } + } + + return nError; +} + +static int TestCreateArchive_UnicodeNames() +{ + TLogHelper Logger("MpqUnicodeName"); + DWORD dwCreateFlags = MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES; + int nError = ERROR_SUCCESS; + + nError = CreateNewArchiveU(&Logger, szUnicodeName1, dwCreateFlags | MPQ_CREATE_ARCHIVE_V1, 15); + if(nError != ERROR_SUCCESS) + return nError; + + nError = CreateNewArchiveU(&Logger, szUnicodeName2, dwCreateFlags | MPQ_CREATE_ARCHIVE_V2, 58); + if(nError != ERROR_SUCCESS) + return nError; + + nError = CreateNewArchiveU(&Logger, szUnicodeName3, dwCreateFlags | MPQ_CREATE_ARCHIVE_V3, 15874); + if(nError != ERROR_SUCCESS) + return nError; + + nError = CreateNewArchiveU(&Logger, szUnicodeName4, dwCreateFlags | MPQ_CREATE_ARCHIVE_V4, 87541); + if(nError != ERROR_SUCCESS) + return nError; + + nError = CreateNewArchiveU(&Logger, szUnicodeName5, dwCreateFlags | MPQ_CREATE_ARCHIVE_V3, 87541); + if(nError != ERROR_SUCCESS) + return nError; + + nError = CreateNewArchiveU(&Logger, szUnicodeName5, dwCreateFlags | MPQ_CREATE_ARCHIVE_V2, 87541); + if(nError != ERROR_SUCCESS) + return nError; + + return nError; +} + +static int TestCreateArchive_FileFlagTest(const char * szPlainName) +{ + TLogHelper Logger("FileFlagTest", szPlainName); + HANDLE hMpq = NULL; // Handle of created archive + char szFileName1[MAX_PATH]; + char szFileName2[MAX_PATH]; + char szFullPath[MAX_PATH]; + const char * szMiddleFile = "FileTest_10.exe"; + LCID LocaleIDs[] = {0x000, 0x405, 0x406, 0x407, 0xFFFF}; + char szArchivedName[MAX_PATH]; + DWORD dwMaxFileCount = 0; + DWORD dwFileCount = 0; + size_t i; + int nError; + + // Create paths for local file to be added + CreateFullPathName(szFileName1, szMpqSubDir, "AddFile.exe"); + CreateFullPathName(szFileName2, szMpqSubDir, "AddFile.bin"); + + // Create an empty file that will serve as holder for the MPQ + nError = CreateEmptyFile(&Logger, szPlainName, 0x100000, szFullPath); + + // Create new MPQ archive over that file + if(nError == ERROR_SUCCESS) + nError = CreateNewArchive(&Logger, szPlainName, MPQ_CREATE_ARCHIVE_V1 | MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES, 17, &hMpq); + + // Add the same file multiple times + if(nError == ERROR_SUCCESS) + { + dwMaxFileCount = SFileGetMaxFileCount(hMpq); + for(i = 0; AddFlags[i] != 0xFFFFFFFF; i++) + { + sprintf(szArchivedName, "FileTest_%02u.exe", (unsigned int)i); + nError = AddLocalFileToMpq(&Logger, hMpq, szArchivedName, szFileName1, AddFlags[i], 0); + if(nError != ERROR_SUCCESS) + break; + + dwFileCount++; + } + } + + // Delete a file in the middle of the file table + if(nError == ERROR_SUCCESS) + { + Logger.PrintProgress("Removing file %s ...", szMiddleFile); + nError = RemoveMpqFile(&Logger, hMpq, szMiddleFile, ERROR_SUCCESS); + dwFileCount--; + } + + // Add one more file + if(nError == ERROR_SUCCESS) + { + nError = AddLocalFileToMpq(&Logger, hMpq, "FileTest_xx.exe", szFileName1); + dwFileCount++; + } + + // Try to decrement max file count. This must succeed + if(nError == ERROR_SUCCESS) + { + Logger.PrintProgress("Attempting to decrement max file count ..."); + if(SFileSetMaxFileCount(hMpq, 5)) + nError = Logger.PrintError("Max file count decremented, even if it should fail"); + } + + // Add ZeroSize.txt several times under a different locale + if(nError == ERROR_SUCCESS) + { + for(i = 0; LocaleIDs[i] != 0xFFFF; i++) + { + bool bMustSucceed = ((dwFileCount + 2) < dwMaxFileCount); + + SFileSetLocale(LocaleIDs[i]); + nError = AddLocalFileToMpq(&Logger, hMpq, "ZeroSize_1.txt", szFileName2); + if(nError != ERROR_SUCCESS) + { + if(bMustSucceed == false) + nError = ERROR_SUCCESS; + break; + } + + dwFileCount++; + } + } + + // Add ZeroSize.txt again several times under a different locale + if(nError == ERROR_SUCCESS) + { + for(i = 0; LocaleIDs[i] != 0xFFFF; i++) + { + bool bMustSucceed = ((dwFileCount + 2) < dwMaxFileCount); + + SFileSetLocale(LocaleIDs[i]); + nError = AddLocalFileToMpq(&Logger, hMpq, "ZeroSize_2.txt", szFileName2, 0, 0, bMustSucceed); + if(nError != ERROR_SUCCESS) + { + if(bMustSucceed == false) + nError = ERROR_SUCCESS; + break; + } + + dwFileCount++; + } + } + + // Verify how many files did we add to the MPQ + if(nError == ERROR_SUCCESS) + { + if(dwFileCount + 2 != dwMaxFileCount) + { + Logger.PrintErrorVa("Number of files added to MPQ was unexpected (expected %u, added %u)", dwFileCount, dwMaxFileCount - 2); + nError = ERROR_FILE_CORRUPT; + } + } + + // Test rename function + if(nError == ERROR_SUCCESS) + { + Logger.PrintProgress("Testing rename files ..."); + SFileSetLocale(LANG_NEUTRAL); + if(!SFileRenameFile(hMpq, "FileTest_08.exe", "FileTest_08a.exe")) + nError = Logger.PrintError("Failed to rename the file"); + } + + if(nError == ERROR_SUCCESS) + { + if(!SFileRenameFile(hMpq, "FileTest_08a.exe", "FileTest_08.exe")) + nError = Logger.PrintError("Failed to rename the file"); + } + + if(nError == ERROR_SUCCESS) + { + if(SFileRenameFile(hMpq, "FileTest_10.exe", "FileTest_10a.exe")) + { + Logger.PrintError("Rename test succeeded even if it shouldn't"); + nError = ERROR_FILE_CORRUPT; + } + } + + if(nError == ERROR_SUCCESS) + { + if(SFileRenameFile(hMpq, "FileTest_10a.exe", "FileTest_10.exe")) + { + Logger.PrintError("Rename test succeeded even if it shouldn't"); + nError = ERROR_FILE_CORRUPT; + } + } + + // Close the archive + if(hMpq != NULL) + SFileCloseArchive(hMpq); + hMpq = NULL; + + // Try to reopen the archive + if(nError == ERROR_SUCCESS) + nError = OpenExistingArchive(&Logger, szFullPath, 0, NULL); + return nError; +} + +static int TestCreateArchive_WaveCompressionsTest(const char * szPlainName, const char * szWaveFile) +{ + TLogHelper Logger("CompressionsTest", szPlainName); + HANDLE hMpq = NULL; // Handle of created archive + char szFileName[MAX_PATH]; // Source file to be added + char szArchivedName[MAX_PATH]; + DWORD dwCmprCount = sizeof(WaveCompressions) / sizeof(DWORD); + DWORD dwAddedFiles = 0; + DWORD dwFoundFiles = 0; + int nError; + + // Create paths for local file to be added + CreateFullPathName(szFileName, szMpqSubDir, szWaveFile); + + // Create new archive + nError = CreateNewArchive(&Logger, szPlainName, MPQ_CREATE_ARCHIVE_V1 | MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES, 0x40, &hMpq); + + // Add the same file multiple times + if(nError == ERROR_SUCCESS) + { + Logger.UserTotal = dwCmprCount; + for(unsigned int i = 0; i < dwCmprCount; i++) + { + sprintf(szArchivedName, "WaveFile_%02u.wav", i + 1); + nError = AddLocalFileToMpq(&Logger, hMpq, szArchivedName, szFileName, MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_SECTOR_CRC, WaveCompressions[i]); + if(nError != ERROR_SUCCESS) + break; + + Logger.UserCount++; + dwAddedFiles++; + } + + SFileCloseArchive(hMpq); + } + + // Reopen the archive extract each WAVE file and try to play it + if(nError == ERROR_SUCCESS) + { + nError = OpenExistingArchiveWithCopy(&Logger, NULL, szPlainName, &hMpq); + if(nError == ERROR_SUCCESS) + { + SearchArchive(&Logger, hMpq, TEST_FLAG_LOAD_FILES | TEST_FLAG_PLAY_WAVES, &dwFoundFiles, NULL); + SFileCloseArchive(hMpq); + } + + // Check if the number of found files is the same like the number of added files + // DOn;t forget that there will be (listfile) and (attributes) + if(dwFoundFiles != (dwAddedFiles + 2)) + { + Logger.PrintError("Number of found files does not match number of added files."); + nError = ERROR_FILE_CORRUPT; + } + } + + return nError; +} + +static int TestCreateArchive_ListFilePos(const char * szPlainName) +{ + TFileData * pFileData; + const char * szReaddedFile = "AddedFile_##.txt"; + const char * szFileMask = "AddedFile_%02u.txt"; + TLogHelper Logger("ListFilePos", szPlainName); + HANDLE hMpq = NULL; // Handle of created archive + char szArchivedName[MAX_PATH]; + DWORD dwMaxFileCount = 0x0E; + DWORD dwFileCount = 0; + size_t i; + int nError; + + // Create a new archive with the limit of 0x20 files + nError = CreateNewArchive(&Logger, szPlainName, MPQ_CREATE_ARCHIVE_V4 | MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES, dwMaxFileCount, &hMpq); + + // Add maximum files files + if(nError == ERROR_SUCCESS) + { + for(i = 0; i < dwMaxFileCount; i++) + { + sprintf(szArchivedName, szFileMask, i); + nError = AddFileToMpq(&Logger, hMpq, szArchivedName, "This is a text data.", 0, 0, ERROR_SUCCESS); + if(nError != ERROR_SUCCESS) + break; + + dwFileCount++; + } + } + + // Delete few middle files + if(nError == ERROR_SUCCESS) + { + for(i = 0; i < (dwMaxFileCount / 2); i++) + { + sprintf(szArchivedName, szFileMask, i); + nError = RemoveMpqFile(&Logger, hMpq, szArchivedName, ERROR_SUCCESS); + if(nError != ERROR_SUCCESS) + break; + + dwFileCount--; + } + } + + // Close the archive + if(hMpq != NULL) + SFileCloseArchive(hMpq); + hMpq = NULL; + + // Reopen the archive to catch any asserts + if(nError == ERROR_SUCCESS) + nError = OpenExistingArchiveWithCopy(&Logger, NULL, szPlainName, &hMpq); + + // Check that (listfile) is at the end + if(nError == ERROR_SUCCESS) + { + pFileData = LoadMpqFile(&Logger, hMpq, LISTFILE_NAME); + if(pFileData != NULL) + { + if(pFileData->dwBlockIndex < dwFileCount) + Logger.PrintMessage("Unexpected file index of %s", LISTFILE_NAME); + STORM_FREE(pFileData); + } + + pFileData = LoadMpqFile(&Logger, hMpq, ATTRIBUTES_NAME); + if(pFileData != NULL) + { + if(pFileData->dwBlockIndex <= dwFileCount) + Logger.PrintMessage("Unexpected file index of %s", ATTRIBUTES_NAME); + STORM_FREE(pFileData); + } + + // Add new file to the archive. It should be added to the last position + nError = AddFileToMpq(&Logger, hMpq, szReaddedFile, "This is a re-added file.", 0, 0, ERROR_SUCCESS); + if(nError == ERROR_SUCCESS) + { + // Force update of the tables + SFileFlushArchive(hMpq); + + // Load the file + pFileData = LoadMpqFile(&Logger, hMpq, szReaddedFile); + if(pFileData != NULL) + { + if(pFileData->dwBlockIndex != dwFileCount) + Logger.PrintMessage("Unexpected file index of %s", szReaddedFile); + STORM_FREE(pFileData); + } + } + + SFileCloseArchive(hMpq); + } + + return nError; +} + +static int TestCreateArchive_BigArchive(const char * szPlainName) +{ + const char * szFileMask = "AddedFile_%02u.txt"; + TLogHelper Logger("BigMpqTest"); + HANDLE hMpq = NULL; // Handle of created archive + char szLocalFileName[MAX_PATH]; + char szArchivedName[MAX_PATH]; + DWORD dwMaxFileCount = 0x20; + DWORD dwAddedCount = 0; + size_t i; + int nError; + + // Create a new archive with the limit of 0x20 files + nError = CreateNewArchive(&Logger, szPlainName, MPQ_CREATE_ARCHIVE_V3 | MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES, dwMaxFileCount, &hMpq); + if(nError == ERROR_SUCCESS) + { + // Now add few really big files + CreateFullPathName(szLocalFileName, szMpqSubDir, "MPQ_1997_v1_Diablo1_DIABDAT.MPQ"); + Logger.UserTotal = (dwMaxFileCount / 2); + + for(i = 0; i < dwMaxFileCount / 2; i++) + { + sprintf(szArchivedName, szFileMask, i + 1); + nError = AddLocalFileToMpq(&Logger, hMpq, szArchivedName, szLocalFileName, 0, 0, true); + if(nError != ERROR_SUCCESS) + break; + + Logger.UserCount++; + dwAddedCount++; + } + } + + // Close the archive + if(hMpq != NULL) + SFileCloseArchive(hMpq); + hMpq = NULL; + + // Reopen the archive to catch any asserts + if(nError == ERROR_SUCCESS) + nError = OpenExistingArchiveWithCopy(&Logger, NULL, szPlainName, &hMpq); + + // Check that (listfile) is at the end + if(nError == ERROR_SUCCESS) + { + CheckIfFileIsPresent(&Logger, hMpq, LISTFILE_NAME, true); + CheckIfFileIsPresent(&Logger, hMpq, ATTRIBUTES_NAME, true); + + SFileCloseArchive(hMpq); + } + + return nError; +} + +// "MPQ_2014_v4_Heroes_Replay.MPQ", "AddFile-replay.message.events" +static int TestModifyArchive_ReplaceFile(const char * szMpqPlainName, const char * szFileName) +{ + TLogHelper Logger("ModifyTest"); + HANDLE hMpq = NULL; + const char * szArchivedName; + char szLocalFileName[MAX_PATH]; + size_t nOffset = 0; + int nError; + + // Open an existing archive + nError = OpenExistingArchiveWithCopy(&Logger, szMpqPlainName, szMpqPlainName, &hMpq); + + // Add the given file + if(nError == ERROR_SUCCESS) + { + // Get the name of archived file + if(!_strnicmp(szFileName, "AddFile-", 8)) + nOffset = 8; + szArchivedName = szFileName + nOffset; + + // Create the local file name + CreateFullPathName(szLocalFileName, szMpqSubDir, szFileName); + + // Add the file to MPQ + nError = AddLocalFileToMpq(&Logger, hMpq, + szArchivedName, + szLocalFileName, + MPQ_FILE_REPLACEEXISTING | MPQ_FILE_COMPRESS | MPQ_FILE_SINGLE_UNIT, + MPQ_COMPRESSION_ZLIB, + true); + } + + // Reopen the MPQ and compact it + if(nError == ERROR_SUCCESS) + { + // Compact the archive + Logger.PrintProgress("Compacting archive %s ...", szMpqPlainName); + if(!SFileSetCompactCallback(hMpq, CompactCallback, &Logger)) + nError = Logger.PrintError("Failed to compact archive %s", szMpqPlainName); + + if(!SFileCompactArchive(hMpq, NULL, 0)) + nError = GetLastError(); + + SFileCloseArchive(hMpq); + } + + // Try to open the archive again + if(nError == ERROR_SUCCESS) + { + CreateFullPathName(szLocalFileName, NULL, szMpqPlainName); + nError = OpenExistingArchive(&Logger, szLocalFileName, 0, &hMpq); + if(nError == ERROR_SUCCESS) + SFileCloseArchive(hMpq); + } + + return nError; +} + +//----------------------------------------------------------------------------- +// Comparing two directories, creating links + +#define LINK_COMPARE_BLOCK_SIZE 0x200 + +static int CreateArchiveLinkFile(const char * szFullPath1, const char * szFullPath2, const char * szFileHash) +{ + TFileStream * pStream; + char szLinkData[MAX_PATH + 0x80]; + char szLinkFile[MAX_PATH]; + char szLinkPath[MAX_PATH]; + int nLength; + int nError = ERROR_SUCCESS; + + // Construct the link file name + CalculateRelativePath(szFullPath1, szFullPath2, szLinkPath); + sprintf(szLinkFile, "%s.link", szFullPath2); + + // Format the content of the link file + nLength = sprintf(szLinkData, "LINK:%s\x0D\x0ASHA1:%s", szLinkPath, szFileHash); + + // Create the link file + pStream = FileStream_CreateFileA(szLinkFile, 0); + if(pStream == NULL) + return GetLastError(); + + // Write the content of the link file + if(!FileStream_Write(pStream, NULL, szLinkData, (DWORD)nLength)) + nError = GetLastError(); + + FileStream_Close(pStream); + return ERROR_SUCCESS; +} + +static int ForEachFile_CreateArchiveLink(const char * szFullPath1, const char * szFullPath2) +{ + TLogHelper Logger("CreateMpqLink", GetShortPlainName(szFullPath2)); + char szFileHash1[0x40]; + char szFileHash2[0x40]; + int nError; + + // Prevent logger from witing any result messages + Logger.bDontPrintResult = true; + + // Create SHA1 of both files + nError = CalculateFileSha1(&Logger, szFullPath1, szFileHash1); + if(nError == ERROR_SUCCESS) + { + nError = CalculateFileSha1(&Logger, szFullPath2, szFileHash2); + if(nError == ERROR_SUCCESS) + { + // If the hashes are identical, we can create link + if(!strcmp(szFileHash1, szFileHash2)) + { + nError = CreateArchiveLinkFile(szFullPath1, szFullPath2, szFileHash1); + if(nError == ERROR_SUCCESS) + { + Logger.PrintMessage("Created link to %s", szFullPath2); + } + } + } + } + + return ERROR_SUCCESS; +} + +//----------------------------------------------------------------------------- +// Main + +int main(int argc, char * argv[]) +{ + int nError = ERROR_SUCCESS; + +#if defined(_MSC_VER) && defined(_DEBUG) + _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); +#endif // defined(_MSC_VER) && defined(_DEBUG) + + // Initialize storage and mix the random number generator + printf("==== Test Suite for StormLib version %s ====\n", STORMLIB_VERSION_STRING); + nError = InitializeMpqDirectory(argv, argc); + + // Not a test, but rather a tool for creating links to duplicated files +// if(nError == ERROR_SUCCESS) +// nError = FindFilePairs(ForEachFile_CreateArchiveLink, "2004 - WoW\\06080", "2004 - WoW\\06299"); +/* + // Search all testing archives and verify their SHA1 hash + if(nError == ERROR_SUCCESS) + nError = FindFiles(ForEachFile_VerifyFileChecksum, szMpqSubDir); + + // Test sparse compression + if(nError == ERROR_SUCCESS) + nError = TestSparseCompression(); + + // Test reading linear file without bitmap + if(nError == ERROR_SUCCESS) + nError = TestFileStreamOperations("MPQ_2013_v4_alternate-original.MPQ", 0); + + // Test reading linear file without bitmap (read only) + if(nError == ERROR_SUCCESS) + nError = TestFileStreamOperations("MPQ_2013_v4_alternate-original.MPQ", STREAM_FLAG_READ_ONLY); + + // Test reading linear file with bitmap + if(nError == ERROR_SUCCESS) + nError = TestFileStreamOperations("MPQ_2013_v4_alternate-complete.MPQ", STREAM_FLAG_USE_BITMAP); + + // Test reading partial file + if(nError == ERROR_SUCCESS) + nError = TestFileStreamOperations("part-file://MPQ_2009_v2_WoW_patch.MPQ.part", 0); + + // Test reading Block4K file + if(nError == ERROR_SUCCESS) + nError = TestFileStreamOperations("blk4-file://streaming/model.MPQ.0", STREAM_PROVIDER_BLOCK4); + + // Test reading encrypted file + if(nError == ERROR_SUCCESS) + nError = TestFileStreamOperations("mpqe-file://MPQ_2011_v2_EncryptedMpq.MPQE", STREAM_PROVIDER_MPQE); + + // Open a stream, paired with local master. The mirror file is created new + if(nError == ERROR_SUCCESS) + nError = TestReadFile_MasterMirror("part-file://MPQ_2009_v1_patch-created.MPQ.part", "MPQ_2009_v1_patch-original.MPQ", false); + + // Open a stream, paired with local master. Only part of the mirror exists + if(nError == ERROR_SUCCESS) + nError = TestReadFile_MasterMirror("part-file://MPQ_2009_v1_patch-partial.MPQ.part", "MPQ_2009_v1_patch-original.MPQ", true); + + // Open a stream, paired with local master. Only part of the mirror exists + if(nError == ERROR_SUCCESS) + nError = TestReadFile_MasterMirror("part-file://MPQ_2009_v1_patch-complete.MPQ.part", "MPQ_2009_v1_patch-original.MPQ", true); + + // Open a stream, paired with local master + if(nError == ERROR_SUCCESS) + nError = TestReadFile_MasterMirror("MPQ_2013_v4_alternate-created.MPQ", "MPQ_2013_v4_alternate-original.MPQ", false); + + // Open a stream, paired with local master + if(nError == ERROR_SUCCESS) + nError = TestReadFile_MasterMirror("MPQ_2013_v4_alternate-incomplete.MPQ", "MPQ_2013_v4_alternate-incomplete.MPQ", true); + + // Open a stream, paired with local master + if(nError == ERROR_SUCCESS) + nError = TestReadFile_MasterMirror("MPQ_2013_v4_alternate-complete.MPQ", "MPQ_2013_v4_alternate-original.MPQ", true); + + // Open a stream, paired with remote master (takes hell lot of time!) +// if(nError == ERROR_SUCCESS) +// nError = TestReadFile_MasterMirror("MPQ_2013_v4_alternate-downloaded.MPQ", "http://www.zezula.net\\mpqs\\alternate.zip", false); + + // Search in listfile + if(nError == ERROR_SUCCESS) + nError = TestSearchListFile("ListFile_Blizzard.txt"); + + // Test opening local file with SFileOpenFileEx + if(nError == ERROR_SUCCESS) + nError = TestOpenLocalFile("ListFile_Blizzard.txt"); + + // Test working with an archive that has no listfile + if(nError == ERROR_SUCCESS) + nError = TestOpenFile_OpenById("MPQ_1997_v1_Diablo1_DIABDAT.MPQ"); + + // Open an empty archive (found in WoW cache - it's just a header) + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("MPQ_2012_v2_EmptyMpq.MPQ"); + + // Open an empty archive (created artificially - it's just a header) + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("MPQ_2013_v4_EmptyMpq.MPQ"); + + // Open an empty archive (found in WoW cache - it's just a header) + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("MPQ_2013_v4_patch-base-16357.MPQ"); + + // Open an empty archive (A buggy MPQ with invalid HET entry count) + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("MPQ_2011_v4_InvalidHetEntryCount.MPQ"); + + // Open a truncated archive + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("MPQ_2002_v1_BlockTableCut.MPQ"); + + // Open a MPQ that actually has user data + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("MPQ_2010_v2_HasUserData.s2ma"); + + // Open an Warcraft III map whose "(attributes)" file has (BlockTableSize-1) entries + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("MPQ_2014_v1_AttributesOneEntryLess.w3x"); + + // Open a MPQ archive v 3.0 + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("MPQ_2010_v3_expansion-locale-frFR.MPQ"); + + // Open an encrypted archive from Starcraft II installer + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("mpqe-file://MPQ_2011_v2_EncryptedMpq.MPQE"); + + // Open a MPK archive from Longwu online + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("MPx_2013_v1_LongwuOnline.mpk"); + + // Open a SQP archive from War of the Immortals + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("MPx_2013_v1_WarOfTheImmortals.sqp", "ListFile_WarOfTheImmortals.txt"); + + // Open a partial MPQ with compressed hash table + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("part-file://MPQ_2010_v2_HashTableCompressed.MPQ.part"); + + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("MPQ_2002_v1_ProtectedMap_HashTable_FakeValid.w3x"); + + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("MPQ_2002_v1_ProtectedMap_InvalidUserData.w3x"); + + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("MPQ_2002_v1_ProtectedMap_InvalidMpqFormat.w3x"); + + // Open an Warcraft III map locked by the Spazzler protector + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("MPQ_2002_v1_ProtectedMap_Spazzler.w3x"); + + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("MPQ_2014_v1_ProtectedMap_Spazzler2.w3x"); + + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("MPQ_2014_v1_ProtectedMap_Spazzler3.w3x"); + + // Open an Warcraft III map locked by the BOBA protector + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("MPQ_2002_v1_ProtectedMap_BOBA.w3m"); + + // Open an Warcraft III map locked by a protector + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("MPQ_2015_v1_ProtectedMap_KangTooJee.w3x"); + + // Open an Warcraft III map locked by a protector + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("MPQ_2015_v1_ProtectedMap_Somj2hM16.w3x"); + + // Open an Warcraft III map locked by Spazy protector + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("MPQ_2015_v1_ProtectedMap_Spazy.w3x"); + + // Open an protected map + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("MPQ_2015_v1_flem1.w3x"); + + // Open the multi-file archive with wrong prefix to see how StormLib deals with it + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_WillFail("flat-file://streaming/model.MPQ.0"); + + // Open an archive that is merged with multiple files + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive("blk4-file://streaming/model.MPQ.0", NULL, true); + + // Open every MPQ that we have in the storage + if(nError == ERROR_SUCCESS) + nError = FindFiles(ForEachFile_OpenArchive, NULL); + + // Test on an archive that has been invalidated by extending an old valid MPQ + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_Corrupt("MPQ_2013_vX_Battle.net.MPQ"); + + // Open a patched archive + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_Patched(PatchList_WoW_OldWorld13286, "OldWorld\\World\\Model.blob", 2); + + // Open a patched archive + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_Patched(PatchList_WoW_15050, "World\\Model.blob", 8); + + // Open a patched archive + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_Patched(PatchList_WoW_16965, "DBFilesClient\\BattlePetNPCTeamMember.db2", 0); + + // Open a patched archive + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_Patched(PatchList_SC2_32283, "TriggerLibs\\natives.galaxy", 6); + + // Open a patched archive + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_Patched(PatchList_SC2_34644, "TriggerLibs\\GameData\\GameData.galaxy", 2); + + // Open a patched archive with new format of BSDIFF patch + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_Patched(PatchList_SC2_34644_Maps, "Maps\\Campaign\\THorner03.SC2Map\\BankList.xml", 3); + + // Open a patched archive + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_Patched(PatchList_SC2_32283_enGB, "LocalizedData\\GameHotkeys.txt", 6); + + // Open a patched archive + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_Patched(PatchList_HS_6898_enGB, "Hearthstone_Data\\Managed\\Assembly-Csharp.dll", 10); + + // Check the opening archive for read-only + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_ReadOnly("MPQ_1997_v1_Diablo1_DIABDAT.MPQ", true); + + // Check the opening archive for read-only + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_ReadOnly("MPQ_1997_v1_Diablo1_DIABDAT.MPQ", false); + + // Check the SFileGetFileInfo function + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_GetFileInfo("MPQ_2002_v1_StrongSignature.w3m", "MPQ_2013_v4_SC2_EmptyMap.SC2Map"); + + // Downloadable MPQ archive + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_MasterMirror("part-file://MPQ_2009_v1_patch-partial.MPQ.part", "MPQ_2009_v1_patch-original.MPQ", "world\\Azeroth\\DEADMINES\\PASSIVEDOODADS\\GOBLINMELTINGPOT\\DUST2.BLP", false); + + // Downloadable MPQ archive + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_MasterMirror("MPQ_2013_v4_alternate-downloaded.MPQ", "MPQ_2013_v4_alternate-original.MPQ", "alternate\\DUNGEONS\\TEXTURES\\ICECROWN\\GATE\\jlo_IceC_Floor_Thrown.blp", false); + + // Check archive signature + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_VerifySignature("MPQ_1999_v1_WeakSignature.exe", "War2Patch_202.exe"); + + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_VerifySignature("MPQ_2003_v1_WeakSignatureEmpty.exe", "WoW-1.2.3.4211-enUS-patch.exe"); + + // Check archive signature + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_VerifySignature("MPQ_2002_v1_StrongSignature.w3m", "(10)DustwallowKeys.w3m"); + + // Compact the archive + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_CraftedUserData("MPQ_2010_v3_expansion-locale-frFR.MPQ", "StormLibTest_CraftedMpq1_v3.mpq"); + + // Open a MPQ (add custom user data to it) + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_CraftedUserData("MPQ_2013_v4_SC2_EmptyMap.SC2Map", "StormLibTest_CraftedMpq2_v4.mpq"); + + // Open a MPQ (add custom user data to it) + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_CraftedUserData("MPQ_2013_v4_expansion1.MPQ", "StormLibTest_CraftedMpq3_v4.mpq"); + + if(nError == ERROR_SUCCESS) + nError = TestAddFile_FullTable("MPQ_2014_v1_out1.w3x"); + + if(nError == ERROR_SUCCESS) + nError = TestAddFile_FullTable("MPQ_2014_v1_out2.w3x"); + + // Test modifying file with no (listfile) and no (attributes) + if(nError == ERROR_SUCCESS) + nError = TestAddFile_ListFileTest("MPQ_1997_v1_Diablo1_DIABDAT.MPQ", false, false); + + // Test modifying an archive that contains (listfile) and (attributes) + if(nError == ERROR_SUCCESS) + nError = TestAddFile_ListFileTest("MPQ_2013_v4_SC2_EmptyMap.SC2Map", true, true); + + // Create an empty archive v2 + if(nError == ERROR_SUCCESS) + nError = TestCreateArchive_EmptyMpq("StormLibTest_EmptyMpq_v2.mpq", MPQ_CREATE_ARCHIVE_V2 | MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES); + + // Create an empty archive v4 + if(nError == ERROR_SUCCESS) + nError = TestCreateArchive_EmptyMpq("StormLibTest_EmptyMpq_v4.mpq", MPQ_CREATE_ARCHIVE_V4 | MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES); + + // Test creating of an archive the same way like MPQ Editor does + if(nError == ERROR_SUCCESS) + nError = TestCreateArchive_TestGaps("StormLibTest_GapsTest.mpq"); + + // Sign an existing non-signed archive + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_SignExisting("MPQ_1998_v1_StarDat.mpq"); + + // Open a signed archive, add a file and verify the signature + if(nError == ERROR_SUCCESS) + nError = TestOpenArchive_ModifySigned("MPQ_1999_v1_WeakSignature.exe", "War2Patch_202.exe"); + + // Create new archive and sign it + if(nError == ERROR_SUCCESS) + nError = TestCreateArchive_Signed("MPQ_1999_v1_WeakSigned1.mpq", true); + + if(nError == ERROR_SUCCESS) + nError = TestCreateArchive_Signed("MPQ_1999_v1_WeakSigned2.mpq", false); + + // Test creating of an archive the same way like MPQ Editor does + if(nError == ERROR_SUCCESS) + nError = TestCreateArchive_MpqEditor("StormLibTest_MpqEditorTest.mpq", "AddedFile.exe"); + + // Create an archive and fill it with files up to the max file count + if(nError == ERROR_SUCCESS) + nError = TestCreateArchive_FillArchive("StormLibTest_FileTableFull.mpq", 0); + + // Create an archive and fill it with files up to the max file count + if(nError == ERROR_SUCCESS) + nError = TestCreateArchive_FillArchive("StormLibTest_FileTableFull.mpq", MPQ_CREATE_LISTFILE); + + // Create an archive and fill it with files up to the max file count + if(nError == ERROR_SUCCESS) + nError = TestCreateArchive_FillArchive("StormLibTest_FileTableFull.mpq", MPQ_CREATE_ATTRIBUTES); + + // Create an archive and fill it with files up to the max file count + if(nError == ERROR_SUCCESS) + nError = TestCreateArchive_FillArchive("StormLibTest_FileTableFull.mpq", MPQ_CREATE_ATTRIBUTES | MPQ_CREATE_LISTFILE); + + // Create an archive, and increment max file count several times + if(nError == ERROR_SUCCESS) + nError = TestCreateArchive_IncMaxFileCount("StormLibTest_IncMaxFileCount.mpq"); + + // Create a MPQ archive with UNICODE names + if(nError == ERROR_SUCCESS) + nError = TestCreateArchive_UnicodeNames(); + + // Create a MPQ file, add files with various flags + if(nError == ERROR_SUCCESS) + nError = TestCreateArchive_FileFlagTest("StormLibTest_FileFlagTest.mpq"); + + // Create a MPQ file, add a mono-WAVE file with various compressions + if(nError == ERROR_SUCCESS) + nError = TestCreateArchive_WaveCompressionsTest("StormLibTest_AddWaveMonoTest.mpq", "AddFile-Mono.wav"); + + // Create a MPQ file, add a mono-WAVE with 8 bits per sample file with various compressions + if(nError == ERROR_SUCCESS) + nError = TestCreateArchive_WaveCompressionsTest("StormLibTest_AddWaveMonoBadTest.mpq", "AddFile-MonoBad.wav"); + + // Create a MPQ file, add a stereo-WAVE file with various compressions + if(nError == ERROR_SUCCESS) + nError = TestCreateArchive_WaveCompressionsTest("StormLibTest_AddWaveStereoTest.mpq", "AddFile-Stereo.wav"); + + // Check if the listfile is always created at the end of the file table in the archive + if(nError == ERROR_SUCCESS) + nError = TestCreateArchive_ListFilePos("StormLibTest_ListFilePos.mpq"); + + // Open a MPQ (add custom user data to it) + if(nError == ERROR_SUCCESS) + nError = TestCreateArchive_BigArchive("StormLibTest_BigArchive_v4.mpq"); +*/ + // Test replacing a file with zero size file + if(nError == ERROR_SUCCESS) + nError = TestModifyArchive_ReplaceFile("MPQ_2014_v4_Base.StormReplay", "AddFile-replay.message.events"); + +#ifdef _MSC_VER + _CrtDumpMemoryLeaks(); +#endif // _MSC_VER + + return nError; +} diff --git a/test/TLogHelper.cpp b/test/TLogHelper.cpp index 078de1d..d22b76d 100644 --- a/test/TLogHelper.cpp +++ b/test/TLogHelper.cpp @@ -1,421 +1,421 @@ -/*****************************************************************************/ -/* TLogHelper.cpp Copyright (c) Ladislav Zezula 2013 */ -/*---------------------------------------------------------------------------*/ -/* Helper class for reporting StormLib tests */ -/* This file should be included directly from StormTest.cpp using #include */ -/*---------------------------------------------------------------------------*/ -/* Date Ver Who Comment */ -/* -------- ---- --- ------- */ -/* 26.11.13 1.00 Lad The first version of TLogHelper.cpp */ -/*****************************************************************************/ - -//----------------------------------------------------------------------------- -// Definition of the TLogHelper class - -class TLogHelper -{ - public: - - TLogHelper(const char * szNewMainTitle = NULL, const char * szNewSubTitle1 = NULL, const char * szNewSubTitle2 = NULL); - ~TLogHelper(); - -#if defined(UNICODE) || defined(UNICODE) - // TCHAR-based functions. They are only needed on UNICODE builds. - // On ANSI builds is TCHAR = char, so we don't need them at all - int PrintWithClreol(const TCHAR * szFormat, va_list argList, bool bPrintPrefix, bool bPrintLastError, bool bPrintEndOfLine); - void PrintProgress(const TCHAR * szFormat, ...); - int PrintErrorVa(const TCHAR * szFormat, ...); - int PrintError(const TCHAR * szFormat, const TCHAR * szFileName = NULL); -#endif // defined(UNICODE) || defined(UNICODE) - - // ANSI functions - int PrintWithClreol(const char * szFormat, va_list argList, bool bPrintPrefix, bool bPrintLastError, bool bPrintEndOfLine); - void PrintProgress(const char * szFormat, ...); - void PrintMessage(const char * szFormat, ...); - int PrintErrorVa(const char * szFormat, ...); - int PrintError(const char * szFormat, const char * szFileName = NULL); - - const char * UserString; - unsigned int UserCount; - unsigned int UserTotal; - bool bDontPrintResult; - - protected: - -#if defined(UNICODE) || defined(UNICODE) - TCHAR * CopyFormatCharacter(TCHAR * szBuffer, const TCHAR *& szFormat); -#endif - char * CopyFormatCharacter(char * szBuffer, const char *& szFormat); - int GetConsoleWidth(); - - const char * szMainTitle; // Title of the text (usually name) - const char * szSubTitle1; // Title of the text (can be name of the tested file) - const char * szSubTitle2; // Title of the text (can be name of the tested file) - size_t nTextLength; // Length of the previous progress message - bool bMessagePrinted; -}; - -//----------------------------------------------------------------------------- -// String replacements for format strings - -#ifdef _MSC_VER -#define I64u_t _T("%I64u") -#define I64u_a "%I64u" -#define I64X_t _T("%I64X") -#define I64X_a "%I64X" -#else -#define I64u_t _T("%llu") -#define I64u_a "%llu" -#define I64X_t _T("%llX") -#define I64X_a "%llX" -#endif - -//----------------------------------------------------------------------------- -// Constructor and destructor - - -TLogHelper::TLogHelper(const char * szNewMainTitle, const char * szNewSubTitle1, const char * szNewSubTitle2) -{ - UserString = ""; - UserCount = 1; - UserTotal = 1; - - // Fill the test line structure - szMainTitle = szNewMainTitle; - szSubTitle1 = szNewSubTitle1; - szSubTitle2 = szNewSubTitle2; - nTextLength = 0; - bMessagePrinted = false; - bDontPrintResult = false; - - // Print the initial information - if(szMainTitle != NULL) - { - if(szSubTitle1 != NULL && szSubTitle2 != NULL) - printf("Running %s (%s+%s) ...", szMainTitle, szSubTitle1, szSubTitle2); - else if(szSubTitle1 != NULL) - printf("Running %s (%s) ...", szMainTitle, szSubTitle1); - else - printf("Running %s ...", szMainTitle); - } -} - -TLogHelper::~TLogHelper() -{ - const char * szSaveMainTitle = szMainTitle; - const char * szSaveSubTitle1 = szSubTitle1; - const char * szSaveSubTitle2 = szSubTitle2; - - // Set both to NULL so the won't be printed - szMainTitle = szSubTitle1 = szSubTitle2 = NULL; - - // Print the final information - if(szSaveMainTitle != NULL && bMessagePrinted == false) - { - if(bDontPrintResult == false) - { - if(szSaveSubTitle1 != NULL && szSaveSubTitle2 != NULL) - PrintMessage("%s (%s+%s) succeeded.", szSaveMainTitle, szSaveSubTitle1, szSaveSubTitle2); - else if(szSaveSubTitle1 != NULL) - PrintMessage("%s (%s) succeeded.", szSaveMainTitle, szSaveSubTitle1); - else - PrintMessage("%s succeeded.", szSaveMainTitle); - } - else - { - PrintProgress(" "); - printf("\r"); - } - } - -#if defined(_MSC_VER) && defined(_DEBUG) - if(_CrtDumpMemoryLeaks()) - { - PrintMessage("Memory leak detected after %s\n.", szSaveMainTitle); - } -#endif // _MSC_VER -} - -//----------------------------------------------------------------------------- -// TCHAR-based functions. They are only needed on UNICODE builds. -// On ANSI builds is TCHAR = char, so we don't need them at all - -#if defined(UNICODE) || defined(UNICODE) -int TLogHelper::PrintWithClreol(const TCHAR * szFormat, va_list argList, bool bPrintPrefix, bool bPrintLastError, bool bPrintEndOfLine) -{ - TCHAR szFormatBuff[0x200]; - TCHAR szMessage[0x200]; - TCHAR * szBuffer = szFormatBuff; - int nRemainingWidth; - int nConsoleWidth = GetConsoleWidth(); - int nLength = 0; - int nError = GetLastError(); - - // Always start the buffer with '\r' - *szBuffer++ = '\r'; - - // Print the prefix, if needed - if(szMainTitle != NULL && bPrintPrefix) - { - while(szMainTitle[nLength] != 0) - *szBuffer++ = szMainTitle[nLength++]; - - *szBuffer++ = ':'; - *szBuffer++ = ' '; - } - - // Copy the message format itself. Replace %s with "%s", unless it's (%s) - if(szFormat != NULL) - { - szBuffer = CopyFormatCharacter(szBuffer, szFormat); - szFormat += nLength; - } - - // Append the last error - if(bPrintLastError) - { - nLength = _stprintf(szBuffer, _T(" (error code: %u)"), nError); - szBuffer += nLength; - } - - // Create the result string - szBuffer[0] = 0; - nLength = _vstprintf(szMessage, szFormatBuff, argList); - szBuffer = szMessage + nLength; - - // Shall we pad the string? - if(nLength < nConsoleWidth) - { - // Calculate the remaining width - nRemainingWidth = nConsoleWidth - nLength - 1; - - // Pad the string with spaces to fill it up to the end of the line - for(int i = 0; i < nRemainingWidth; i++) - *szBuffer++ = 0x20; - } - - // Put the newline, if requested - *szBuffer++ = bPrintEndOfLine ? '\n' : 0; - *szBuffer = 0; - - // Remember if we printed a message - if(bPrintEndOfLine) - bMessagePrinted = true; - - // Spit out the text in one single printf - _tprintf(_T("%s"), szMessage); - return nError; -} - -void TLogHelper::PrintProgress(const TCHAR * szFormat, ...) -{ - va_list argList; - - va_start(argList, szFormat); - PrintWithClreol(szFormat, argList, true, false, false); - va_end(argList); -} - -int TLogHelper::PrintErrorVa(const TCHAR * szFormat, ...) -{ - va_list argList; - int nResult; - - va_start(argList, szFormat); - nResult = PrintWithClreol(szFormat, argList, true, true, true); - va_end(argList); - - return nResult; -} - -int TLogHelper::PrintError(const TCHAR * szFormat, const TCHAR * szFileName) -{ - return PrintErrorVa(szFormat, szFileName); -} -#endif // defined(UNICODE) || defined(UNICODE) - -//----------------------------------------------------------------------------- -// ANSI functions - -int TLogHelper::PrintWithClreol(const char * szFormat, va_list argList, bool bPrintPrefix, bool bPrintLastError, bool bPrintEndOfLine) -{ - char szFormatBuff[0x200]; - char szMessage[0x200]; - char * szBuffer = szFormatBuff; - int nRemainingWidth; - int nConsoleWidth = GetConsoleWidth(); - int nLength = 0; - int nError = GetLastError(); - - // Always start the buffer with '\r' - *szBuffer++ = '\r'; - - // Print the prefix, if needed - if(szMainTitle != NULL && bPrintPrefix) - { - while(szMainTitle[nLength] != 0) - *szBuffer++ = szMainTitle[nLength++]; - - *szBuffer++ = ':'; - *szBuffer++ = ' '; - } - - // Copy the message format itself. Replace %s with "%s", unless it's (%s) - if(szFormat != NULL) - { - while(szFormat[0] != 0) - { - szBuffer = CopyFormatCharacter(szBuffer, szFormat); - } - } - - // Append the last error - if(bPrintLastError) - { - nLength = sprintf(szBuffer, " (error code: %u)", nError); - szBuffer += nLength; - } - - // Create the result string - szBuffer[0] = 0; - nLength = vsprintf(szMessage, szFormatBuff, argList); - - // Shall we pad the string? - szBuffer = szMessage + nLength; - if(nLength < nConsoleWidth) - { - // Calculate the remaining width - nRemainingWidth = nConsoleWidth - nLength - 1; - - // Pad the string with spaces to fill it up to the end of the line - for(int i = 0; i < nRemainingWidth; i++) - *szBuffer++ = 0x20; - } - - // Put the newline, if requested - *szBuffer++ = bPrintEndOfLine ? '\n' : '\r'; - *szBuffer = 0; - - // Remember if we printed a message - if(bPrintEndOfLine) - bMessagePrinted = true; - - // Spit out the text in one single printf - printf("%s", szMessage); - return nError; -} - -void TLogHelper::PrintProgress(const char * szFormat, ...) -{ - va_list argList; - - va_start(argList, szFormat); - PrintWithClreol(szFormat, argList, true, false, false); - va_end(argList); -} - -void TLogHelper::PrintMessage(const char * szFormat, ...) -{ - va_list argList; - - va_start(argList, szFormat); - PrintWithClreol(szFormat, argList, true, false, true); - va_end(argList); -} - -int TLogHelper::PrintErrorVa(const char * szFormat, ...) -{ - va_list argList; - int nResult; - - va_start(argList, szFormat); - nResult = PrintWithClreol(szFormat, argList, true, true, true); - va_end(argList); - - return nResult; -} - -int TLogHelper::PrintError(const char * szFormat, const char * szFileName) -{ - return PrintErrorVa(szFormat, szFileName); -} - -//----------------------------------------------------------------------------- -// Protected functions - -#ifdef _UNICODE -TCHAR * TLogHelper::CopyFormatCharacter(TCHAR * szBuffer, const TCHAR *& szFormat) -{ - static const TCHAR * szStringFormat = _T("\"%s\""); - static const TCHAR * szUint64Format = I64u_t; - - // String format - if(szFormat[0] == '%') - { - if(szFormat[1] == 's' && szFormat[2] != ')') - { - _tcscpy(szBuffer, szStringFormat); - szFormat += 2; - return szBuffer + _tcslen(szStringFormat); - } - - // Replace %I64u with the proper platform-dependent suffix - if(szFormat[1] == 'I' && szFormat[2] == '6' && szFormat[3] == '4' && szFormat[4] == 'u') - { - _tcscpy(szBuffer, szUint64Format); - szFormat += 5; - return szBuffer + _tcslen(szUint64Format); - } - } - - // Copy the character as-is - *szBuffer++ = *szFormat++; - return szBuffer; -} -#endif - -char * TLogHelper::CopyFormatCharacter(char * szBuffer, const char *& szFormat) -{ - static const char * szStringFormat = "\"%s\""; - static const char * szUint64Format = I64u_a; - - // String format - if(szFormat[0] == '%') - { - if(szFormat[1] == 's' && szFormat[2] != ')') - { - strcpy(szBuffer, szStringFormat); - szFormat += 2; - return szBuffer + strlen(szStringFormat); - } - - // Replace %I64u with the proper platform-dependent suffix - if(szFormat[1] == 'I' && szFormat[2] == '6' && szFormat[3] == '4' && szFormat[4] == 'u') - { - strcpy(szBuffer, szUint64Format); - szFormat += 5; - return szBuffer + strlen(szUint64Format); - } - } - - // Copy the character as-is - *szBuffer++ = *szFormat++; - return szBuffer; -} - -int TLogHelper::GetConsoleWidth() -{ -#ifdef PLATFORM_WINDOWS - - CONSOLE_SCREEN_BUFFER_INFO ScreenInfo; - GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &ScreenInfo); - return (int)(ScreenInfo.srWindow.Right - ScreenInfo.srWindow.Left); - -#else - - // On non-Windows platforms, we assume that width of the console line - // is 80 characters - return 120; - -#endif -} +/*****************************************************************************/ +/* TLogHelper.cpp Copyright (c) Ladislav Zezula 2013 */ +/*---------------------------------------------------------------------------*/ +/* Helper class for reporting StormLib tests */ +/* This file should be included directly from StormTest.cpp using #include */ +/*---------------------------------------------------------------------------*/ +/* Date Ver Who Comment */ +/* -------- ---- --- ------- */ +/* 26.11.13 1.00 Lad The first version of TLogHelper.cpp */ +/*****************************************************************************/ + +//----------------------------------------------------------------------------- +// Definition of the TLogHelper class + +class TLogHelper +{ + public: + + TLogHelper(const char * szNewMainTitle = NULL, const char * szNewSubTitle1 = NULL, const char * szNewSubTitle2 = NULL); + ~TLogHelper(); + +#if defined(UNICODE) || defined(UNICODE) + // TCHAR-based functions. They are only needed on UNICODE builds. + // On ANSI builds is TCHAR = char, so we don't need them at all + int PrintWithClreol(const TCHAR * szFormat, va_list argList, bool bPrintPrefix, bool bPrintLastError, bool bPrintEndOfLine); + void PrintProgress(const TCHAR * szFormat, ...); + int PrintErrorVa(const TCHAR * szFormat, ...); + int PrintError(const TCHAR * szFormat, const TCHAR * szFileName = NULL); +#endif // defined(UNICODE) || defined(UNICODE) + + // ANSI functions + int PrintWithClreol(const char * szFormat, va_list argList, bool bPrintPrefix, bool bPrintLastError, bool bPrintEndOfLine); + void PrintProgress(const char * szFormat, ...); + void PrintMessage(const char * szFormat, ...); + int PrintErrorVa(const char * szFormat, ...); + int PrintError(const char * szFormat, const char * szFileName = NULL); + + const char * UserString; + unsigned int UserCount; + unsigned int UserTotal; + bool bDontPrintResult; + + protected: + +#if defined(UNICODE) || defined(UNICODE) + TCHAR * CopyFormatCharacter(TCHAR * szBuffer, const TCHAR *& szFormat); +#endif + char * CopyFormatCharacter(char * szBuffer, const char *& szFormat); + int GetConsoleWidth(); + + const char * szMainTitle; // Title of the text (usually name) + const char * szSubTitle1; // Title of the text (can be name of the tested file) + const char * szSubTitle2; // Title of the text (can be name of the tested file) + size_t nTextLength; // Length of the previous progress message + bool bMessagePrinted; +}; + +//----------------------------------------------------------------------------- +// String replacements for format strings + +#ifdef _MSC_VER +#define I64u_t _T("%I64u") +#define I64u_a "%I64u" +#define I64X_t _T("%I64X") +#define I64X_a "%I64X" +#else +#define I64u_t _T("%llu") +#define I64u_a "%llu" +#define I64X_t _T("%llX") +#define I64X_a "%llX" +#endif + +//----------------------------------------------------------------------------- +// Constructor and destructor + + +TLogHelper::TLogHelper(const char * szNewMainTitle, const char * szNewSubTitle1, const char * szNewSubTitle2) +{ + UserString = ""; + UserCount = 1; + UserTotal = 1; + + // Fill the test line structure + szMainTitle = szNewMainTitle; + szSubTitle1 = szNewSubTitle1; + szSubTitle2 = szNewSubTitle2; + nTextLength = 0; + bMessagePrinted = false; + bDontPrintResult = false; + + // Print the initial information + if(szMainTitle != NULL) + { + if(szSubTitle1 != NULL && szSubTitle2 != NULL) + printf("Running %s (%s+%s) ...", szMainTitle, szSubTitle1, szSubTitle2); + else if(szSubTitle1 != NULL) + printf("Running %s (%s) ...", szMainTitle, szSubTitle1); + else + printf("Running %s ...", szMainTitle); + } +} + +TLogHelper::~TLogHelper() +{ + const char * szSaveMainTitle = szMainTitle; + const char * szSaveSubTitle1 = szSubTitle1; + const char * szSaveSubTitle2 = szSubTitle2; + + // Set both to NULL so the won't be printed + szMainTitle = szSubTitle1 = szSubTitle2 = NULL; + + // Print the final information + if(szSaveMainTitle != NULL && bMessagePrinted == false) + { + if(bDontPrintResult == false) + { + if(szSaveSubTitle1 != NULL && szSaveSubTitle2 != NULL) + PrintMessage("%s (%s+%s) succeeded.", szSaveMainTitle, szSaveSubTitle1, szSaveSubTitle2); + else if(szSaveSubTitle1 != NULL) + PrintMessage("%s (%s) succeeded.", szSaveMainTitle, szSaveSubTitle1); + else + PrintMessage("%s succeeded.", szSaveMainTitle); + } + else + { + PrintProgress(" "); + printf("\r"); + } + } + +#if defined(_MSC_VER) && defined(_DEBUG) + if(_CrtDumpMemoryLeaks()) + { + PrintMessage("Memory leak detected after %s\n.", szSaveMainTitle); + } +#endif // _MSC_VER +} + +//----------------------------------------------------------------------------- +// TCHAR-based functions. They are only needed on UNICODE builds. +// On ANSI builds is TCHAR = char, so we don't need them at all + +#if defined(UNICODE) || defined(UNICODE) +int TLogHelper::PrintWithClreol(const TCHAR * szFormat, va_list argList, bool bPrintPrefix, bool bPrintLastError, bool bPrintEndOfLine) +{ + TCHAR szFormatBuff[0x200]; + TCHAR szMessage[0x200]; + TCHAR * szBuffer = szFormatBuff; + int nRemainingWidth; + int nConsoleWidth = GetConsoleWidth(); + int nLength = 0; + int nError = GetLastError(); + + // Always start the buffer with '\r' + *szBuffer++ = '\r'; + + // Print the prefix, if needed + if(szMainTitle != NULL && bPrintPrefix) + { + while(szMainTitle[nLength] != 0) + *szBuffer++ = szMainTitle[nLength++]; + + *szBuffer++ = ':'; + *szBuffer++ = ' '; + } + + // Copy the message format itself. Replace %s with "%s", unless it's (%s) + if(szFormat != NULL) + { + szBuffer = CopyFormatCharacter(szBuffer, szFormat); + szFormat += nLength; + } + + // Append the last error + if(bPrintLastError) + { + nLength = _stprintf(szBuffer, _T(" (error code: %u)"), nError); + szBuffer += nLength; + } + + // Create the result string + szBuffer[0] = 0; + nLength = _vstprintf(szMessage, szFormatBuff, argList); + szBuffer = szMessage + nLength; + + // Shall we pad the string? + if(nLength < nConsoleWidth) + { + // Calculate the remaining width + nRemainingWidth = nConsoleWidth - nLength - 1; + + // Pad the string with spaces to fill it up to the end of the line + for(int i = 0; i < nRemainingWidth; i++) + *szBuffer++ = 0x20; + } + + // Put the newline, if requested + *szBuffer++ = bPrintEndOfLine ? '\n' : 0; + *szBuffer = 0; + + // Remember if we printed a message + if(bPrintEndOfLine) + bMessagePrinted = true; + + // Spit out the text in one single printf + _tprintf(_T("%s"), szMessage); + return nError; +} + +void TLogHelper::PrintProgress(const TCHAR * szFormat, ...) +{ + va_list argList; + + va_start(argList, szFormat); + PrintWithClreol(szFormat, argList, true, false, false); + va_end(argList); +} + +int TLogHelper::PrintErrorVa(const TCHAR * szFormat, ...) +{ + va_list argList; + int nResult; + + va_start(argList, szFormat); + nResult = PrintWithClreol(szFormat, argList, true, true, true); + va_end(argList); + + return nResult; +} + +int TLogHelper::PrintError(const TCHAR * szFormat, const TCHAR * szFileName) +{ + return PrintErrorVa(szFormat, szFileName); +} +#endif // defined(UNICODE) || defined(UNICODE) + +//----------------------------------------------------------------------------- +// ANSI functions + +int TLogHelper::PrintWithClreol(const char * szFormat, va_list argList, bool bPrintPrefix, bool bPrintLastError, bool bPrintEndOfLine) +{ + char szFormatBuff[0x200]; + char szMessage[0x200]; + char * szBuffer = szFormatBuff; + int nRemainingWidth; + int nConsoleWidth = GetConsoleWidth(); + int nLength = 0; + int nError = GetLastError(); + + // Always start the buffer with '\r' + *szBuffer++ = '\r'; + + // Print the prefix, if needed + if(szMainTitle != NULL && bPrintPrefix) + { + while(szMainTitle[nLength] != 0) + *szBuffer++ = szMainTitle[nLength++]; + + *szBuffer++ = ':'; + *szBuffer++ = ' '; + } + + // Copy the message format itself. Replace %s with "%s", unless it's (%s) + if(szFormat != NULL) + { + while(szFormat[0] != 0) + { + szBuffer = CopyFormatCharacter(szBuffer, szFormat); + } + } + + // Append the last error + if(bPrintLastError) + { + nLength = sprintf(szBuffer, " (error code: %u)", nError); + szBuffer += nLength; + } + + // Create the result string + szBuffer[0] = 0; + nLength = vsprintf(szMessage, szFormatBuff, argList); + + // Shall we pad the string? + szBuffer = szMessage + nLength; + if(nLength < nConsoleWidth) + { + // Calculate the remaining width + nRemainingWidth = nConsoleWidth - nLength - 1; + + // Pad the string with spaces to fill it up to the end of the line + for(int i = 0; i < nRemainingWidth; i++) + *szBuffer++ = 0x20; + } + + // Put the newline, if requested + *szBuffer++ = bPrintEndOfLine ? '\n' : '\r'; + *szBuffer = 0; + + // Remember if we printed a message + if(bPrintEndOfLine) + bMessagePrinted = true; + + // Spit out the text in one single printf + printf("%s", szMessage); + return nError; +} + +void TLogHelper::PrintProgress(const char * szFormat, ...) +{ + va_list argList; + + va_start(argList, szFormat); + PrintWithClreol(szFormat, argList, true, false, false); + va_end(argList); +} + +void TLogHelper::PrintMessage(const char * szFormat, ...) +{ + va_list argList; + + va_start(argList, szFormat); + PrintWithClreol(szFormat, argList, true, false, true); + va_end(argList); +} + +int TLogHelper::PrintErrorVa(const char * szFormat, ...) +{ + va_list argList; + int nResult; + + va_start(argList, szFormat); + nResult = PrintWithClreol(szFormat, argList, true, true, true); + va_end(argList); + + return nResult; +} + +int TLogHelper::PrintError(const char * szFormat, const char * szFileName) +{ + return PrintErrorVa(szFormat, szFileName); +} + +//----------------------------------------------------------------------------- +// Protected functions + +#ifdef _UNICODE +TCHAR * TLogHelper::CopyFormatCharacter(TCHAR * szBuffer, const TCHAR *& szFormat) +{ + static const TCHAR * szStringFormat = _T("\"%s\""); + static const TCHAR * szUint64Format = I64u_t; + + // String format + if(szFormat[0] == '%') + { + if(szFormat[1] == 's' && szFormat[2] != ')') + { + _tcscpy(szBuffer, szStringFormat); + szFormat += 2; + return szBuffer + _tcslen(szStringFormat); + } + + // Replace %I64u with the proper platform-dependent suffix + if(szFormat[1] == 'I' && szFormat[2] == '6' && szFormat[3] == '4' && szFormat[4] == 'u') + { + _tcscpy(szBuffer, szUint64Format); + szFormat += 5; + return szBuffer + _tcslen(szUint64Format); + } + } + + // Copy the character as-is + *szBuffer++ = *szFormat++; + return szBuffer; +} +#endif + +char * TLogHelper::CopyFormatCharacter(char * szBuffer, const char *& szFormat) +{ + static const char * szStringFormat = "\"%s\""; + static const char * szUint64Format = I64u_a; + + // String format + if(szFormat[0] == '%') + { + if(szFormat[1] == 's' && szFormat[2] != ')') + { + strcpy(szBuffer, szStringFormat); + szFormat += 2; + return szBuffer + strlen(szStringFormat); + } + + // Replace %I64u with the proper platform-dependent suffix + if(szFormat[1] == 'I' && szFormat[2] == '6' && szFormat[3] == '4' && szFormat[4] == 'u') + { + strcpy(szBuffer, szUint64Format); + szFormat += 5; + return szBuffer + strlen(szUint64Format); + } + } + + // Copy the character as-is + *szBuffer++ = *szFormat++; + return szBuffer; +} + +int TLogHelper::GetConsoleWidth() +{ +#ifdef PLATFORM_WINDOWS + + CONSOLE_SCREEN_BUFFER_INFO ScreenInfo; + GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &ScreenInfo); + return (int)(ScreenInfo.srWindow.Right - ScreenInfo.srWindow.Left); + +#else + + // On non-Windows platforms, we assume that width of the console line + // is 80 characters + return 120; + +#endif +} -- cgit v1.2.3