/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ // Original file Copyright Crytek GMBH or its affiliates, used under license. #include "stdafx.h" #include <smartptr.h> #include "Util.h" #include "ZipFileFormat.h" #include "zipdirstructures.h" #include "ZipDirTree.h" #include "ZipDirList.h" #include "ZipDirCache.h" #include "ZipDirCacheRW.h" #include "ZipDirCacheFactory.h" #include "ZipDirFindRW.h" #include "ThreadUtils.h" #include <zlib.h> // declaration of Z_OK for ZipRawDecompress #include <AzCore/std/parallel/mutex.h> #include <AzCore/std/smart_ptr/unique_ptr.h> #include <AzFramework/IO/LocalFileIO.h> #include <AzFramework/StringFunc/StringFunc.h> #include "Codec.h" #include <zstd.h> #include <lz4.h> #include <chrono> #include <ratio> enum PackFileStatus { PACKFILE_COMPRESSED, PACKFILE_ADDED, PACKFILE_UPTODATE, PACKFILE_SKIPPED, PACKFILE_MISSING, PACKFILE_FAILED }; enum PackFileCompressionPolicy { PACKFILE_USE_REQUESTED_COMPRESSOR, PACKFILE_USE_FASTEST_DECOMPRESSING_CODEC }; class PackFilePool; struct PackFileBatch { PackFilePool* pool; int zipMaxSize; int sourceMinSize; int sourceMaxSize; int compressionMethod; int compressionLevel; PackFileBatch() : pool(0) , sourceMinSize(0) , sourceMaxSize(0) , zipMaxSize(0) , compressionMethod(0) , compressionLevel(0) { } }; class PackFilePool; struct PackFileJob { int index; int key; PackFileBatch* batch; const char* relativePathSrc; const char* realFilename; unsigned int existingCRC; void* compressedData; unsigned long compressedSize; unsigned long compressedSizePreviously; void* uncompressedData; unsigned long uncompressedSize; unsigned long uncompressedSizePreviously; int64 modTime; ZipDir::ErrorEnum zdError; PackFileStatus status; PackFileCompressionPolicy compressionPolicy; PackFileJob() : index(0) , key(0) , batch(0) , realFilename(0) , relativePathSrc(0) , existingCRC(0) , compressedData(0) , compressedSize(0) , compressedSizePreviously(0) , uncompressedData(0) , uncompressedSize(0) , uncompressedSizePreviously(0) , modTime(0) , zdError(ZipDir::ZD_ERROR_NOT_IMPLEMENTED) , status(PACKFILE_FAILED) , compressionPolicy(PACKFILE_USE_REQUESTED_COMPRESSOR) { } void DetachUncompressedData() { if (uncompressedData && uncompressedData == compressedData) { compressedData = 0; compressedSize = 0; } uncompressedData = 0; uncompressedSize = 0; } ~PackFileJob() { if (compressedData && compressedData != uncompressedData) { azfree(compressedData); compressedData = 0; } if (uncompressedData) { azfree(uncompressedData); uncompressedData = 0; } } }; // --------------------------------------------------------------------------- static void PackFileFromDisc(PackFileJob* job); class PackFilePool { public: PackFilePool(int numFiles, size_t memoryLimit) : m_pool(false) , m_skip(false) , m_awaitedFile(0) , m_memoryLimit(memoryLimit) , m_allocatedMemory(0) { m_files.reserve(numFiles); } ~PackFilePool() { } void Submit(int key, const PackFileJob& job) { PackFileJob* newJob = new PackFileJob(job); // index in queue, and custom key for identification newJob->index = int(m_files.size()); newJob->key = key; m_files.push_back(newJob); } PackFileJob* WaitForFile(int index) { while (true) { { AZStd::lock_guard<AZStd::mutex> lock(m_filesLock); m_awaitedFile = index; if (size_t(index) >= m_files.size()) { return 0; } if (m_files[index]) { return m_files[index]; } } Sleep(0); } assert(0); return 0; } void Start(unsigned numExtraThreads) { if (numExtraThreads == 0) { for (PackFileJob* job : m_files) { PackFileFromDisc(job); } } else { for (size_t i = 0; i < m_files.size(); ++i) { PackFileJob* job = m_files[i]; m_files[i] = 0; m_pool.Submit(&ProcessFile, job); } m_pool.Start(numExtraThreads); } } size_t GetJobCount() const { return m_files.size(); } void SkipPendingFiles() { m_skip = true; } void ReleaseFile(int index) { assert(m_files[index] != 0); if (m_files[index]) { if (m_memoryLimit != 0) { AZStd::lock_guard<AZStd::mutex> lock(m_filesLock); m_allocatedMemory -= m_files[index]->uncompressedSize; m_allocatedMemory -= m_files[index]->compressedSize; } delete m_files[index]; m_files[index] = 0; } } private: // called from non-main thread static void ProcessFile(PackFileJob* job) { PackFilePool* self = job->batch->pool; if (!self->m_skip) { if (self->m_memoryLimit != 0) { while (true) { size_t allocatedMemory = 0; int awaitedFile = 0; { AZStd::lock_guard<AZStd::mutex> lock(self->m_filesLock); allocatedMemory = self->m_allocatedMemory; awaitedFile = self->m_awaitedFile; } if (allocatedMemory > self->m_memoryLimit && job->index > awaitedFile + 1) { Sleep(10); // give time to main thread to write data to file } else { break; } } } PackFileFromDisc(job); } self->FileCompleted(job); } // called from non-main thread void FileCompleted(PackFileJob* job) { AZStd::lock_guard<AZStd::mutex> lock(m_filesLock); assert(job); assert(job->index < m_files.size()); assert(m_files[job->index] == 0); m_files[job->index] = job; if (m_memoryLimit != 0) { m_allocatedMemory += job->uncompressedSize; m_allocatedMemory += job->compressedSize; } } size_t m_memoryLimit; AZStd::mutex m_filesLock; std::vector<PackFileJob*> m_files; int m_awaitedFile; size_t m_allocatedMemory; bool m_skip; ThreadUtils::SimpleThreadPool m_pool; }; ////////////////////////////////////////////////////////////////////////// static size_t AlignTo(size_t offset, size_t alignment) { const size_t remainder = offset % alignment; return remainder ? offset + alignment - remainder : offset; } ////////////////////////////////////////////////////////////////////////// // Calculates new offset of the header to make sure that following data are // aligned properly static size_t CalculateAlignedHeaderOffset(const char* fileName, size_t currentOffset, size_t alignment) { // Since file should start from header if (currentOffset == 0) { return 0; } // Local header is followed by filename const size_t totalHeaderSize = sizeof(ZipFile::LocalFileHeader) + strlen(fileName); // Align end of the header const size_t dataOffset = AlignTo(currentOffset + totalHeaderSize, alignment); return dataOffset - totalHeaderSize; } ////////////////////////////////////////////////////////////////////////// ZipDir::CacheRW::CacheRW(bool encryptHeaders, const EncryptionKey& encryptionKey) : m_pFile (NULL) , m_nFlags (0) , m_lCDROffset (0) , m_fileAlignment (1) , m_bEncryptedHeaders(encryptHeaders) , m_bHeadersEncryptedOnClose(encryptHeaders) , m_encryptionKey(encryptionKey) { m_nRefCount = 0; } ////////////////////////////////////////////////////////////////////////// ZipDir::CacheRW::~CacheRW() { Close(); } ////////////////////////////////////////////////////////////////////////// void ZipDir::CacheRW::AddRef() { ++m_nRefCount; } ////////////////////////////////////////////////////////////////////////// void ZipDir::CacheRW::Release() { if (--m_nRefCount <= 0) { delete this; } } void ZipDir::CacheRW::Close() { if (m_pFile) { if (!(m_nFlags & FLAGS_READ_ONLY)) { if ((m_nFlags & FLAGS_UNCOMPACTED) && !(m_nFlags & FLAGS_DONT_COMPACT)) { if (!RelinkZip()) { WriteCDR(); } } else if (m_nFlags & FLAGS_CDR_DIRTY) { WriteCDR(); } } if (m_pFile) // RelinkZip() might have closed the file { fclose (m_pFile); } m_pFile = NULL; } m_treeDir.Clear(); } ////////////////////////////////////////////////////////////////////////// char* ZipDir::CacheRW::UnifyPath(char* const str, const char* pPath) { assert(str); const char* src = pPath; char* trg = str; while (*src) { if (*src != '/') { *trg++ = ::tolower(*src++); } else { *trg++ = '\\'; src++; } } *trg = 0; return str; } ////////////////////////////////////////////////////////////////////////// char* ZipDir::CacheRW::ToUnixPath(char* const str, const char* pPath) { assert(str); const char* src = pPath; char* trg = str; while (*src) { if (*src != '/') { *trg++ = *src++; } else { *trg++ = '\\'; src++; } } *trg = 0; return str; } ////////////////////////////////////////////////////////////////////////// char* ZipDir::CacheRW::AllocPath(const char* pPath) { char str[_MAX_PATH]; char* temp = ToUnixPath(str, pPath); temp = m_tempStringPool.Append(temp, strlen(temp)); return temp; } static bool UseZlibForFileType(const char* filename) { AZStd::string f(filename); //some files types are forced to use zlib bool found = AzFramework::StringFunc::Path::IsExtension(filename, ".dds") || f.find("cover.ctc") != string::npos || AzFramework::StringFunc::Path::IsExtension(filename, ".uicanvas"); return found; } static const char* CodecAsString(CompressionCodec::Codec codec) { switch (codec) { case CompressionCodec::Codec::ZLIB: return "ZLIB"; case CompressionCodec::Codec::ZSTD: return "ZSTD"; case CompressionCodec::Codec::LZ4: return "LZ4"; } return "ERROR"; } static bool CompressData(PackFileJob *job) { bool bUseZlib = UseZlibForFileType(job->relativePathSrc) || (job->compressionPolicy == PACKFILE_USE_REQUESTED_COMPRESSOR); bool compressionSuccessful = true; if (bUseZlib) { job->compressedSize = ZipDir::GetCompressedSizeEstimate(job->uncompressedSize,CompressionCodec::Codec::ZLIB); job->compressedData = azmalloc(job->compressedSize); int error = ZipDir::ZipRawCompress(job->uncompressedData, &job->compressedSize, job->compressedData, job->uncompressedSize, job->batch->compressionLevel); if (error == Z_OK) { job->status = PACKFILE_COMPRESSED; job->zdError = ZipDir::ZD_ERROR_SUCCESS; } else { compressionSuccessful = false; } } else { unsigned long compressedSize[static_cast<int>(CompressionCodec::Codec::NUM_CODECS)]; void* compressedData[static_cast<int>(CompressionCodec::Codec::NUM_CODECS)]; std::chrono::milliseconds decompressionTime[static_cast<int>(CompressionCodec::Codec::NUM_CODECS)]; bool compressionCodecWasSuccessful[static_cast<int>(CompressionCodec::Codec::NUM_CODECS)]; std::chrono::time_point<std::chrono::steady_clock> start; //do compression for (CompressionCodec::Codec codec : CompressionCodec::s_AllCodecs) { unsigned int index = static_cast<int>(codec); compressedSize[index] = ZipDir::GetCompressedSizeEstimate(job->uncompressedSize, codec); compressedData[index] = azmalloc(compressedSize[index]); AZStd::unique_ptr<char[]> tempBuffer; unsigned long tempSize = 0; //some files decompress so fast they are beyond our ability to measure so we need to do it a few times to get a reading int numTimesToDecompress = 1 + ZipDir::TARGET_MIN_TEST_COMPRESS_BYTES / job->uncompressedSize; auto testDecompressionTime = [&tempSize, job, &tempBuffer, &start, &compressionCodecWasSuccessful, index, numTimesToDecompress, &compressedData, &compressedSize, &decompressionTime]() { tempSize = job->uncompressedSize; tempBuffer = AZStd::make_unique<char[]>(tempSize); start = std::chrono::high_resolution_clock::now(); //start by assuming the decompression test is never going to result in an error compressionCodecWasSuccessful[index] = true; for (int i = 0; i < numTimesToDecompress; i++) { int zerror = ZipDir::ZipRawUncompress(tempBuffer.get(), &tempSize, compressedData[index], compressedSize[index]); if (zerror != Z_OK) { compressionCodecWasSuccessful[index] = false; break; } } decompressionTime[index] = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - start); }; switch (codec) { case CompressionCodec::Codec::ZLIB: if (ZipDir::ZipRawCompress(job->uncompressedData, &compressedSize[index], compressedData[index], job->uncompressedSize, job->batch->compressionLevel) == Z_OK) { testDecompressionTime(); } else { compressionCodecWasSuccessful[index] = false; } break; case CompressionCodec::Codec::ZSTD: if (ZipDir::ZipRawCompressZSTD(job->uncompressedData, &compressedSize[index], compressedData[index], job->uncompressedSize, 1) == Z_OK) { testDecompressionTime(); } else { compressionCodecWasSuccessful[index] = false; } break; case CompressionCodec::Codec::LZ4: if (ZipDir::ZipRawCompressLZ4(job->uncompressedData, &compressedSize[index], compressedData[index], job->uncompressedSize, job->batch->compressionLevel) == Z_OK) { testDecompressionTime(); } else { compressionCodecWasSuccessful[index] = false; } break; default: break; } } //check decompression speed int bestTimeIndex = -1; int numberOfSuccessfulCodecs = 0; for (CompressionCodec::Codec codec : CompressionCodec::s_AllCodecs) { int index = static_cast<int>(codec); if (compressionCodecWasSuccessful[index]) { numberOfSuccessfulCodecs++; if (bestTimeIndex == -1) { bestTimeIndex = index; continue; } if ((decompressionTime[index] < decompressionTime[bestTimeIndex])) { bestTimeIndex = index; } } } if (!numberOfSuccessfulCodecs) { AZ_Error("ZipDirCacheRW", false, "None of the available codecs were able to compress the file: %s", job->relativePathSrc); compressionSuccessful = false; } else { #ifdef AZ_DEBUG_BUILD AZ_Printf("ZipDirCacheRW", "Winner for %s is %s with: %d ms ", job->realFilename, CodecAsString(static_cast<CompressionCodec::Codec>(bestTimeIndex)), decompressionTime[bestTimeIndex]); #endif } //get rid of losing data for (CompressionCodec::Codec codec : CompressionCodec::s_AllCodecs) { int index = static_cast<int>(codec); if (index != bestTimeIndex) { azfree(compressedData[index]); compressedData[index] = nullptr; } } if (compressionSuccessful) { job->compressedSize = compressedSize[bestTimeIndex]; job->compressedData = compressedData[bestTimeIndex]; } } //if there was a problem with the compression so just store the file if (!compressionSuccessful) { azfree(job->compressedData); job->compressedData = job->uncompressedData; job->compressedSize = job->uncompressedSize; } job->status = PACKFILE_COMPRESSED; job->zdError = ZipDir::ZD_ERROR_SUCCESS; return true; } static void PackFileFromMemory(PackFileJob* job) { if (job->existingCRC != 0) { unsigned int crcCode = (unsigned int)crc32(0, (unsigned char*)job->uncompressedData, job->uncompressedSize); if (crcCode == job->existingCRC) { job->compressedData = 0; job->compressedSize = 0; job->status = PACKFILE_UPTODATE; job->zdError = ZipDir::ZD_ERROR_SUCCESS; // This file with same data already in pak, skip it. return; } } switch (job->batch->compressionMethod) { case ZipFile::METHOD_DEFLATE_AND_ENCRYPT: case ZipFile::METHOD_DEFLATE: { // allocate memory for compression. Min is nSize * 1.001 + 12 if (job->uncompressedSize > 0) { CompressData(job); } else { job->status = PACKFILE_COMPRESSED; job->zdError = ZipDir::ZD_ERROR_SUCCESS; job->compressedSize = 0; job->compressedData = 0; } break; } case ZipFile::METHOD_STORE: job->compressedData = job->uncompressedData; job->compressedSize = job->uncompressedSize; job->status = PACKFILE_COMPRESSED; job->zdError = ZipDir::ZD_ERROR_SUCCESS; break; default: job->status = PACKFILE_FAILED; job->zdError = ZipDir::ZD_ERROR_UNSUPPORTED; break; } } bool ZipDir::CacheRW::WriteCompressedData(const char* data, size_t size, bool encrypt, FILE* file) { if (size <= 0) { return true; } std::vector<char> buffer; if (encrypt) { buffer.resize(size); memcpy(&buffer[0], data, size); ZipDir::Encrypt(&buffer[0], size, m_encryptionKey); data = &buffer[0]; } // Danny - writing a single large chunk (more than 6MB?) causes // Windows fwrite to (silently?!) fail. So we're writing data // in small chunks. while (size > 0) { const size_t sizeToWrite = Util::getMin(size, size_t(1024 * 1024)); if (fwrite(data, sizeToWrite, 1, file) != 1) { return false; } data += sizeToWrite; size -= sizeToWrite; } return true; } static bool WriteRandomData(FILE* file, size_t size) { if (size <= 0) { return true; } const size_t bufferSize = Util::getMin(size, size_t(1024 * 1024)); std::vector<char> buffer(bufferSize); while (size > 0) { const size_t sizeToWrite = Util::getMin(size, bufferSize); for (size_t i = 0; i < sizeToWrite; ++i) { buffer[i] = rand() & 0xff; } if (fwrite(&buffer[0], sizeToWrite, 1, file) != 1) { return false; } size -= sizeToWrite; } return true; } bool ZipDir::CacheRW::WriteNullData(size_t size) { if (size <= 0) { return true; } const size_t bufferSize = Util::getMin(size, size_t(1024 * 1024)); std::vector<char> buffer(bufferSize, 0); while (size > 0) { const size_t sizeToWrite = Util::getMin(size, bufferSize); if (fwrite(&buffer[0], sizeToWrite, 1, m_pFile) != 1) { return false; } size -= sizeToWrite; } return true; } void ZipDir::CacheRW::StorePackedFile(PackFileJob* job) { if (job->batch->zipMaxSize > 0 && GetTotalFileSize() > job->batch->zipMaxSize) { job->status = PACKFILE_SKIPPED; job->zdError = ZipDir::ZD_ERROR_SUCCESS; return; } job->status = PACKFILE_FAILED; char str[_MAX_PATH]; char* relativePath = UnifyPath(str, job->relativePathSrc); // create or find the file entry.. this object will rollback (delete the object // if the operation fails) if needed. FileEntryTransactionAdd pFileEntry(this, AllocPath(job->relativePathSrc), AllocPath(relativePath)); if (!pFileEntry) { job->zdError = ZipDir::ZD_ERROR_INVALID_PATH; return; } pFileEntry->OnNewFileData(job->uncompressedData, job->uncompressedSize, job->compressedSize, job->batch->compressionMethod, false); pFileEntry->SetFromFileTimeNTFS(job->modTime); // since we changed the time, we'll have to update CDR m_nFlags |= FLAGS_CDR_DIRTY; // the new CDR position, if the operation completes successfully unsigned lNewCDROffset = m_lCDROffset; if (pFileEntry->IsInitialized()) { // this file entry is already allocated in CDR // check if the new compressed data fits into the old place unsigned nFreeSpace = pFileEntry->nEOFOffset - pFileEntry->nFileHeaderOffset - (unsigned)sizeof(ZipFile::LocalFileHeader) - (unsigned)strlen(relativePath); if (nFreeSpace != job->compressedSize) { m_nFlags |= FLAGS_UNCOMPACTED; } if (nFreeSpace >= job->compressedSize) { // and we can just override the compressed data in the file ErrorEnum e = WriteLocalHeader(m_pFile, pFileEntry, job->relativePathSrc, m_bEncryptedHeaders); if (e != ZipDir::ZD_ERROR_SUCCESS) { job->zdError = e; return; } } else { // we need to write the file anew - in place of current CDR pFileEntry->nFileHeaderOffset = CalculateAlignedHeaderOffset(job->relativePathSrc, m_lCDROffset, m_fileAlignment); ErrorEnum e = WriteLocalHeader(m_pFile, pFileEntry, job->relativePathSrc, m_bEncryptedHeaders); lNewCDROffset = pFileEntry->nEOFOffset; if (e != ZipDir::ZD_ERROR_SUCCESS) { job->zdError = e; return; } } } else { pFileEntry->nFileHeaderOffset = CalculateAlignedHeaderOffset(job->relativePathSrc, m_lCDROffset, m_fileAlignment); ErrorEnum e = WriteLocalHeader(m_pFile, pFileEntry, job->relativePathSrc, m_bEncryptedHeaders); if (e != ZipDir::ZD_ERROR_SUCCESS) { job->zdError = e; return; } lNewCDROffset = pFileEntry->nFileDataOffset + job->compressedSize; m_nFlags |= FLAGS_CDR_DIRTY; } // now we have the fresh local header and data offset #ifdef WIN32 if (_fseeki64 (m_pFile, (__int64)pFileEntry->nFileDataOffset, SEEK_SET) != 0) #else if (fseek (m_pFile, pFileEntry->nFileDataOffset, SEEK_SET) != 0) #endif { job->zdError = ZD_ERROR_IO_FAILED; return; } const bool encrypt = pFileEntry->nMethod == ZipFile::METHOD_DEFLATE_AND_ENCRYPT; if (!WriteCompressedData((char*)job->compressedData, job->compressedSize, encrypt, m_pFile)) { job->zdError = ZD_ERROR_IO_FAILED; return; } // since we wrote the file successfully, update the new CDR position m_lCDROffset = lNewCDROffset; pFileEntry.Commit(); job->status = PACKFILE_ADDED; job->zdError = ZD_ERROR_SUCCESS; } // Adds a new file to the zip or update an existing one // adds a directory (creates several nested directories if needed) ZipDir::ErrorEnum ZipDir::CacheRW::UpdateFile (const char* szRelativePathSrc, void* pUncompressed, unsigned nSize, unsigned nCompressionMethod, int nCompressionLevel, int64 modTime) { char str[_MAX_PATH]; char* szRelativePath = UnifyPath(str, szRelativePathSrc); PackFileBatch batch; batch.compressionMethod = nCompressionMethod; batch.compressionLevel = nCompressionLevel; PackFileJob job; job.relativePathSrc = szRelativePathSrc; job.modTime = modTime; job.uncompressedData = pUncompressed; job.uncompressedSize = nSize; job.batch = &batch; // crc will be used to check if this file need to be updated at all ZipDir::FileEntry* entry = FindFile(szRelativePath); if (entry) { job.existingCRC = entry->desc.lCRC32; } PackFileFromMemory(&job); switch (job.status) { case PACKFILE_SKIPPED: case PACKFILE_MISSING: case PACKFILE_FAILED: return ZD_ERROR_IO_FAILED; } StorePackedFile(&job); job.DetachUncompressedData(); return job.zdError; } static FILETIME GetFileWriteTimeAndSize(uint64* fileSize, const char* filename) { // Warning: FindFirstFile on NTFS may report file size that // is not up-to-date with the actual file content. // http://blogs.msdn.com/b/oldnewthing/archive/2011/12/26/10251026.aspx FILETIME fileTime; #if defined(AZ_PLATFORM_WINDOWS) WIN32_FIND_DATAA FindFileData; HANDLE hFind = FindFirstFileA(filename, &FindFileData); if (hFind == INVALID_HANDLE_VALUE) { fileTime.dwLowDateTime = 0; fileTime.dwHighDateTime = 0; if (fileSize) { *fileSize = 0; } } else { fileTime.dwLowDateTime = FindFileData.ftLastWriteTime.dwLowDateTime; fileTime.dwHighDateTime = FindFileData.ftLastWriteTime.dwHighDateTime; if (fileSize) { *fileSize = (uint64(FindFileData.nFileSizeHigh) << 32) + FindFileData.nFileSizeLow; } FindClose(hFind); } #elif AZ_TRAIT_OS_PLATFORM_APPLE || defined(AZ_PLATFORM_LINUX) //We cant use this implmentation for the windows version because ModificationTime //returns the time filename was changed(ChangeTime) not last written into(LastWriteTime). //If LocalFileIO ever adds support for LastWriteTime we can have a common implementation. AZ::IO::LocalFileIO localFileIO; AZ::u64 modTime = 0; modTime = localFileIO.ModificationTime(filename); if(modTime != 0) { fileTime.dwHighDateTime = modTime >> 32; fileTime.dwLowDateTime = modTime & 0xFFFFFFFF; if (fileSize) { localFileIO.Size(filename, *fileSize); } } #else #error Needs implmentation! #endif return fileTime; } static void PackFileFromDisc(PackFileJob* job) { const FILETIME ft = GetFileWriteTimeAndSize(0, job->realFilename); LARGE_INTEGER lt; lt.HighPart = ft.dwHighDateTime; lt.LowPart = ft.dwLowDateTime; job->modTime = lt.QuadPart; FILE* f = nullptr; azfopen(&f, job->realFilename, "rb"); if (!f) { job->status = PACKFILE_FAILED; job->zdError = ZipDir::ZD_ERROR_FILE_NOT_FOUND; return; } fseek(f, 0, SEEK_END); size_t fileSize = (size_t)ftell(f); if ((fileSize < job->batch->sourceMinSize) || (job->batch->sourceMaxSize > 0 && fileSize > job->batch->sourceMaxSize)) { fclose(f); job->status = PACKFILE_SKIPPED; job->zdError = ZipDir::ZD_ERROR_SUCCESS; return; } if (!fileSize) { //Allow 0-Bytes long files. job->uncompressedData = nullptr; } else { job->uncompressedData = azmalloc(fileSize); fseek(f, 0, SEEK_SET); if (fread(job->uncompressedData, 1, fileSize, f) != fileSize) { azfree(job->uncompressedData); job->uncompressedData = 0; fclose(f); job->status = PACKFILE_FAILED; job->zdError = ZipDir::ZD_ERROR_IO_FAILED; return; } } fclose(f); job->uncompressedSize = fileSize; PackFileFromMemory(job); } bool ZipDir::CacheRW::UpdateMultipleFiles(const char** realFilenames, const char** filenamesInZip, size_t fileCount, int compressionLevel, bool encryptContent, size_t zipMaxSize, int sourceMinSize, int sourceMaxSize, unsigned numExtraThreads, ZipDir::IReporter* reporter, ZipDir::ISplitter* splitter, bool useFastestDecompressionCodec) { int compressionMethod = ZipFile::METHOD_DEFLATE; if (encryptContent) { compressionMethod = ZipFile::METHOD_DEFLATE_AND_ENCRYPT; } else if (compressionLevel == 0) { compressionMethod = ZipFile::METHOD_STORE; } uint64 totalSize = 0; clock_t startTime = clock(); PackFileBatch batch; batch.compressionLevel = compressionLevel; batch.compressionMethod = compressionMethod; batch.sourceMinSize = sourceMinSize; batch.sourceMaxSize = sourceMaxSize; batch.zipMaxSize = zipMaxSize; const size_t memoryLimit = 1024 * 1024 * 1024; // prevents threads from generating more than 1GB of data PackFilePool pool(fileCount, memoryLimit); batch.pool = &pool; for (int i = 0; i < fileCount; ++i) { const char* realFilename = realFilenames[i]; const char* filenameInZip = filenamesInZip[i]; PackFileJob job; job.relativePathSrc = filenameInZip; job.realFilename = realFilename; job.batch = &batch; job.compressionPolicy = useFastestDecompressionCodec ? PACKFILE_USE_FASTEST_DECOMPRESSING_CODEC : PACKFILE_USE_REQUESTED_COMPRESSOR; { // crc will be used to check if this file need to be updated at all ZipDir::FileEntry* entry = FindFile(filenameInZip); if (entry) { uint64 fileSize = 0; const FILETIME ft = GetFileWriteTimeAndSize(&fileSize, realFilename); LARGE_INTEGER lt; lt.HighPart = ft.dwHighDateTime; lt.LowPart = ft.dwLowDateTime; job.modTime = lt.QuadPart; job.existingCRC = entry->desc.lCRC32; job.compressedSizePreviously = entry->desc.lSizeCompressed; job.uncompressedSizePreviously = entry->desc.lSizeUncompressed; // Check if file with the same name, timestamp and size already exists in pak. if (entry->CompareFileTimeNTFS(job.modTime) && fileSize == entry->desc.lSizeUncompressed) { if (reporter) { reporter->ReportUpToDate(filenameInZip); } continue; } } } pool.Submit(i, job); } // Get the number of submitted jobs, which is at most // as large as the largest successfully submitted file-index. // Any number of files can be skipped for submission. const int jobCount = pool.GetJobCount(); if (jobCount == 0) { return true; } pool.Start(numExtraThreads); for (int i = 0; i < jobCount; ++i) { PackFileJob* job = pool.WaitForFile(i); if (!job) { assert(job); continue; } if (job->status == PACKFILE_COMPRESSED) { if (splitter) { size_t dsk = GetTotalFileSizeOnDiskSoFar(); size_t bse = 0; size_t add = 0; size_t sub = 0; bse += sizeof(ZipFile::CDRFileHeader) + strlen(job->relativePathSrc); bse += sizeof(ZipFile::LocalFileHeader) + strlen(job->relativePathSrc); if (job->compressedSize) { add += bse + job->compressedSize; } if (job->compressedSizePreviously) { sub += bse + job->compressedSizePreviously; } if (splitter->CheckWriteLimit(dsk, add, sub)) { splitter->SetLastFile(dsk, add, sub, job->key - 1); // deplete the pool before leaving the loop pool.SkipPendingFiles(); for (; i < jobCount; ++i) { pool.WaitForFile(i); pool.ReleaseFile(i); } break; } } StorePackedFile(job); } switch (job->status) { case PACKFILE_ADDED: if (reporter) { reporter->ReportAdded(job->relativePathSrc); } totalSize += job->uncompressedSize; break; case PACKFILE_MISSING: if (reporter) { reporter->ReportMissing(job->realFilename); } break; case PACKFILE_UPTODATE: if (reporter) { reporter->ReportUpToDate(job->realFilename); } break; case PACKFILE_SKIPPED: if (reporter) { reporter->ReportSkipped(job->realFilename); } break; default: if (reporter) { reporter->ReportFailed(job->realFilename, ""); // TODO reason } break; } pool.ReleaseFile(i); } clock_t endTime = clock(); double timeSeconds = double(endTime - startTime) / CLOCKS_PER_SEC; double speed = (endTime - startTime) == 0 ? 0.0 : double(totalSize) / timeSeconds; if (reporter) { reporter->ReportSpeed(speed); } return true; } // Adds a new file to the zip or update an existing one if it is not compressed - just stored - start a big file ZipDir::ErrorEnum ZipDir::CacheRW::StartContinuousFileUpdate(const char* szRelativePathSrc, unsigned nSize) { char str[_MAX_PATH]; char* szRelativePath = UnifyPath(str, szRelativePathSrc); SmartPtr pBufferDestroyer; // create or find the file entry.. this object will rollback (delete the object // if the operation fails) if needed. FileEntryTransactionAdd pFileEntry(this, AllocPath(szRelativePathSrc), AllocPath(szRelativePath)); if (!pFileEntry) { return ZD_ERROR_INVALID_PATH; } pFileEntry->OnNewFileData (NULL, nSize, nSize, ZipFile::METHOD_STORE, false); // since we changed the time, we'll have to update CDR m_nFlags |= FLAGS_CDR_DIRTY; // the new CDR position, if the operation completes successfully unsigned lNewCDROffset = m_lCDROffset; if (pFileEntry->IsInitialized()) { // check if the new compressed data fits into the old place unsigned nFreeSpace = pFileEntry->nEOFOffset - pFileEntry->nFileHeaderOffset - (unsigned)sizeof(ZipFile::LocalFileHeader) - (unsigned)strlen(szRelativePath); if (nFreeSpace != nSize) { m_nFlags |= FLAGS_UNCOMPACTED; } if (nFreeSpace >= nSize) { // and we can just override the compressed data in the file ErrorEnum e = WriteLocalHeader(m_pFile, pFileEntry, szRelativePathSrc, m_bEncryptedHeaders); if (e != ZD_ERROR_SUCCESS) { return e; } } else { // we need to write the file anew - in place of current CDR pFileEntry->nFileHeaderOffset = CalculateAlignedHeaderOffset(szRelativePathSrc, m_lCDROffset, m_fileAlignment); ErrorEnum e = WriteLocalHeader(m_pFile, pFileEntry, szRelativePathSrc, m_bEncryptedHeaders); lNewCDROffset = pFileEntry->nEOFOffset; if (e != ZD_ERROR_SUCCESS) { return e; } } } else { pFileEntry->nFileHeaderOffset = CalculateAlignedHeaderOffset(szRelativePathSrc, m_lCDROffset, m_fileAlignment); ErrorEnum e = WriteLocalHeader(m_pFile, pFileEntry, szRelativePathSrc, m_bEncryptedHeaders); if (e != ZD_ERROR_SUCCESS) { return e; } lNewCDROffset = pFileEntry->nFileDataOffset + nSize; m_nFlags |= FLAGS_CDR_DIRTY; } #ifdef WIN32 if (_fseeki64 (m_pFile, (__int64)pFileEntry->nFileDataOffset, SEEK_SET) != 0) #else if (fseek (m_pFile, pFileEntry->nFileDataOffset, SEEK_SET) != 0) #endif { return ZD_ERROR_IO_FAILED; } if (!WriteNullData(nSize)) { return ZD_ERROR_IO_FAILED; } pFileEntry->nEOFOffset = pFileEntry->nFileDataOffset; // since we wrote the file successfully, update the new CDR position m_lCDROffset = lNewCDROffset; pFileEntry.Commit(); return ZD_ERROR_SUCCESS; } // Adds a new file to the zip or update an existing's segment if it is not compressed - just stored // adds a directory (creates several nested directories if needed) ZipDir::ErrorEnum ZipDir::CacheRW::UpdateFileContinuousSegment (const char* szRelativePathSrc, unsigned nSize, void* pUncompressed, unsigned nSegmentSize, unsigned nOverwriteSeekPos) { char str[_MAX_PATH]; char* szRelativePath = UnifyPath(str, szRelativePathSrc); SmartPtr pBufferDestroyer; // create or find the file entry.. this object will rollback (delete the object // if the operation fails) if needed. FileEntryTransactionAdd pFileEntry(this, AllocPath(szRelativePathSrc), AllocPath(szRelativePath)); if (!pFileEntry) { return ZD_ERROR_INVALID_PATH; } pFileEntry->OnNewFileData (pUncompressed, nSegmentSize, nSegmentSize, ZipFile::METHOD_STORE, true); // since we changed the time, we'll have to update CDR m_nFlags |= FLAGS_CDR_DIRTY; // this file entry is already allocated in CDR unsigned lSegmentOffset = pFileEntry->nEOFOffset; #ifdef WIN32 if (_fseeki64 (m_pFile, (__int64)pFileEntry->nFileHeaderOffset, SEEK_SET) != 0) #else if (fseek (m_pFile, pFileEntry->nFileHeaderOffset, SEEK_SET) != 0) #endif { return ZD_ERROR_IO_FAILED; } // and we can just override the compressed data in the file ErrorEnum e = WriteLocalHeader(m_pFile, pFileEntry, szRelativePath, m_bEncryptedHeaders); if (e != ZD_ERROR_SUCCESS) { return e; } if (nOverwriteSeekPos != 0xffffffff) { lSegmentOffset = pFileEntry->nFileDataOffset + nOverwriteSeekPos; } // now we have the fresh local header and data offset #ifdef WIN32 if (_fseeki64 (m_pFile, (__int64)lSegmentOffset, SEEK_SET) != 0) #else if (fseek (m_pFile, lSegmentOffset, SEEK_SET) != 0) #endif { return ZD_ERROR_IO_FAILED; } const bool encrypt = false; // encryption is not supported for continous updates if (!WriteCompressedData((char*)pUncompressed, nSegmentSize, encrypt, m_pFile)) { return ZD_ERROR_IO_FAILED; } if (nOverwriteSeekPos == 0xffffffff) { pFileEntry->nEOFOffset = lSegmentOffset + nSegmentSize; } // since we wrote the file successfully, update CDR pFileEntry.Commit(); return ZD_ERROR_SUCCESS; } ZipDir::ErrorEnum ZipDir::CacheRW::UpdateFileCRC (const char* szRelativePathSrc, unsigned dwCRC32) { char str[_MAX_PATH]; char* szRelativePath = UnifyPath(str, szRelativePathSrc); SmartPtr pBufferDestroyer; // create or find the file entry.. this object will rollback (delete the object // if the operation fails) if needed. FileEntryTransactionAdd pFileEntry(this, AllocPath(szRelativePathSrc), AllocPath(szRelativePath)); if (!pFileEntry) { return ZD_ERROR_INVALID_PATH; } // since we changed the time, we'll have to update CDR m_nFlags |= FLAGS_CDR_DIRTY; pFileEntry->desc.lCRC32 = dwCRC32; #ifdef WIN32 if (_fseeki64 (m_pFile, (__int64)pFileEntry->nFileHeaderOffset, SEEK_SET) != 0) #else if (fseek (m_pFile, pFileEntry->nFileHeaderOffset, SEEK_SET) != 0) #endif { return ZD_ERROR_IO_FAILED; } // and we can just override the compressed data in the file ErrorEnum e = WriteLocalHeader(m_pFile, pFileEntry, szRelativePath, m_bEncryptedHeaders); if (e != ZD_ERROR_SUCCESS) { return e; } // since we wrote the file successfully, update pFileEntry.Commit(); return ZD_ERROR_SUCCESS; } // deletes the file from the archive ZipDir::ErrorEnum ZipDir::CacheRW::RemoveFile (const char* szRelativePathSrc) { char str[_MAX_PATH]; char* szRelativePath = UnifyPath(str, szRelativePathSrc); // find the last slash in the path const char* pSlash = (std::max)(strrchr(szRelativePath, '/'), strrchr(szRelativePath, '\\')); const char* pFileName; // the name of the file to delete FileEntryTree* pDir; // the dir from which the subdir will be deleted if (pSlash) { FindDirRW fd (GetRoot()); // the directory to remove pDir = fd.FindExact(string (szRelativePath, pSlash - szRelativePath).c_str()); if (!pDir) { return ZD_ERROR_DIR_NOT_FOUND;// there is no such directory } pFileName = pSlash + 1; } else { pDir = GetRoot(); pFileName = szRelativePath; } ErrorEnum e = pDir->RemoveFile (pFileName); if (e == ZD_ERROR_SUCCESS) { m_nFlags |= FLAGS_UNCOMPACTED | FLAGS_CDR_DIRTY; } return e; } // deletes the directory, with all its descendants (files and subdirs) ZipDir::ErrorEnum ZipDir::CacheRW::RemoveDir (const char* szRelativePathSrc) { char str[_MAX_PATH]; char* szRelativePath = UnifyPath(str, szRelativePathSrc); // find the last slash in the path const char* pSlash = (std::max)(strrchr(szRelativePath, '/'), strrchr(szRelativePath, '\\')); const char* pDirName; // the name of the dir to delete FileEntryTree* pDir; // the dir from which the subdir will be deleted if (pSlash) { FindDirRW fd (GetRoot()); // the directory to remove pDir = fd.FindExact(string (szRelativePath, pSlash - szRelativePath).c_str()); if (!pDir) { return ZD_ERROR_DIR_NOT_FOUND;// there is no such directory } pDirName = pSlash + 1; } else { pDir = GetRoot(); pDirName = szRelativePath; } ErrorEnum e = pDir->RemoveDir (pDirName); if (e == ZD_ERROR_SUCCESS) { m_nFlags |= FLAGS_UNCOMPACTED | FLAGS_CDR_DIRTY; } return e; } // deletes all files and directories in this archive ZipDir::ErrorEnum ZipDir::CacheRW::RemoveAll() { ErrorEnum e = m_treeDir.RemoveAll(); if (e == ZD_ERROR_SUCCESS) { m_nFlags |= FLAGS_UNCOMPACTED | FLAGS_CDR_DIRTY; } return e; } ZipDir::ErrorEnum ZipDir::CacheRW::ReadFile (FileEntry* pFileEntry, void* pCompressed, void* pUncompressed) { if (!pFileEntry) { return ZD_ERROR_INVALID_CALL; } if (pFileEntry->desc.lSizeUncompressed == 0) { assert (pFileEntry->desc.lSizeCompressed == 0); return ZD_ERROR_SUCCESS; } assert (pFileEntry->desc.lSizeCompressed > 0); ErrorEnum nError = Refresh(pFileEntry); if (nError != ZD_ERROR_SUCCESS) { return nError; } #ifdef WIN32 if (_fseeki64 (m_pFile, (__int64)pFileEntry->nFileDataOffset, SEEK_SET)) #else if (fseek (m_pFile, pFileEntry->nFileDataOffset, SEEK_SET)) #endif { return ZD_ERROR_IO_FAILED; } SmartPtr pBufferDestroyer; void* pBuffer = pCompressed; // the buffer where the compressed data will go if (pFileEntry->nMethod == 0 && pUncompressed) { // we can directly read into the uncompress buffer pBuffer = pUncompressed; } if (!pBuffer) { if (!pUncompressed) { // what's the sense of it - no buffers at all? return ZD_ERROR_INVALID_CALL; } pBuffer = azmalloc(pFileEntry->desc.lSizeCompressed); pBufferDestroyer.Attach(pBuffer); // we want it auto-freed once we return } if (fread((char*)pBuffer, pFileEntry->desc.lSizeCompressed, 1, m_pFile) != 1) { return ZD_ERROR_IO_FAILED; } if (pFileEntry->nMethod == ZipFile::METHOD_DEFLATE_AND_ENCRYPT) { ZipDir::Decrypt((char*)pBuffer, pFileEntry->desc.lSizeCompressed, m_encryptionKey); } // if there's a buffer for uncompressed data, uncompress it to that buffer if (pUncompressed) { if (pFileEntry->nMethod == 0) { assert (pBuffer == pUncompressed); //assert (pFileEntry->nSizeCompressed == pFileEntry->nSizeUncompressed); //memcpy (pUncompressed, pBuffer, pFileEntry->nSizeCompressed); } else { unsigned long nSizeUncompressed = pFileEntry->desc.lSizeUncompressed; if (nSizeUncompressed > 0) { if (Z_OK != ZipRawUncompress(pUncompressed, &nSizeUncompressed, pBuffer, pFileEntry->desc.lSizeCompressed)) { return ZD_ERROR_CORRUPTED_DATA; } } } } return ZD_ERROR_SUCCESS; } ////////////////////////////////////////////////////////////////////////// // finds the file by exact path ZipDir::FileEntry* ZipDir::CacheRW::FindFile (const char* szPathSrc, bool bFullInfo) { char str[_MAX_PATH]; char* szPath = UnifyPath(str, szPathSrc); ZipDir::FindFileRW fd (GetRoot()); if (!fd.FindExact(szPath)) { assert (!fd.GetFileEntry()); return NULL; } assert (fd.GetFileEntry()); return fd.GetFileEntry(); } // returns the size of memory occupied by the instance referred to by this cache size_t ZipDir::CacheRW::GetSize() const { return sizeof(*this) + m_strFilePath.capacity() + m_treeDir.GetSize() - sizeof(m_treeDir); } // returns the compressed size of all the entries size_t ZipDir::CacheRW::GetCompressedSize() const { return m_treeDir.GetCompressedFileSize(); } // returns the total size of memory occupied by the instance of this cache and all the compressed files size_t ZipDir::CacheRW::GetTotalFileSize() const { return GetSize() + GetCompressedSize(); } // returns the total size of space occupied on disk by the instance of this cache and all the compressed files size_t ZipDir::CacheRW::GetTotalFileSizeOnDiskSoFar() { FileRecordList arrFiles(GetRoot()); FileRecordList::ZipStats statFiles = arrFiles.GetStats(); return m_lCDROffset + statFiles.nSizeCDR; } // refreshes information about the given file entry into this file entry ZipDir::ErrorEnum ZipDir::CacheRW::Refresh (FileEntry* pFileEntry) { if (!pFileEntry) { return ZD_ERROR_INVALID_CALL; } if (pFileEntry->nFileDataOffset != pFileEntry->INVALID_DATA_OFFSET) { return ZD_ERROR_SUCCESS; // the data offset has been successfully read.. } return ZipDir::Refresh(m_pFile, pFileEntry, m_bEncryptedHeaders); } // writes the CDR to the disk bool ZipDir::CacheRW::WriteCDR(FILE* fTarget, bool encryptCDR) { if (!fTarget) { return false; } #ifdef WIN32 if (_fseeki64(fTarget, (__int64)m_lCDROffset, SEEK_SET)) #else if (fseek(fTarget, m_lCDROffset, SEEK_SET)) #endif { return false; } FileRecordList arrFiles(GetRoot()); //arrFiles.SortByFileOffset(); size_t nSizeCDR = arrFiles.GetStats().nSizeCDR; void* pCDR = malloc(nSizeCDR); size_t nSizeCDRSerialized = arrFiles.MakeZipCDR(m_lCDROffset, pCDR, encryptCDR); assert (nSizeCDRSerialized == nSizeCDR); if (encryptCDR) { // We do not encrypt CDREnd, so we could find it by signature ZipDir::Encrypt((char*)pCDR, nSizeCDR - sizeof(ZipFile::CDREnd), m_encryptionKey); } size_t nWriteRes = fwrite (pCDR, nSizeCDR, 1, fTarget); free(pCDR); return nWriteRes == 1; } // generates random file name string ZipDir::CacheRW::GetRandomName(int nAttempt) { if (nAttempt) { char szBuf[8]; int i; for (i = 0; i < sizeof(szBuf) - 1; ++i) { int r = rand() % (10 + 'z' - 'a' + 1); szBuf[i] = r > 9 ? (r - 10) + 'a' : '0' + r; } szBuf[i] = '\0'; return szBuf; } else { return string(); } } bool ZipDir::CacheRW::RelinkZip() { AZ::IO::LocalFileIO localFileIO; for (int nAttempt = 0; nAttempt < 32; ++nAttempt) { string strNewFilePath = m_strFilePath + "$" + GetRandomName(nAttempt); FILE* f = nullptr; azfopen(&f, strNewFilePath.c_str(), "wb"); if (f) { bool bOk = RelinkZip(f); fclose (f); // we don't need the temporary file handle anyway if (!bOk) { // we don't need the temporary file localFileIO.Remove(strNewFilePath.c_str()); return false; } // we successfully relinked, now copy the temporary file to the original file fclose (m_pFile); m_pFile = NULL; localFileIO.Remove(m_strFilePath.c_str()); if (localFileIO.Rename(strNewFilePath.c_str(), m_strFilePath.c_str()) == 0) { // successfully renamed - reopen m_pFile = nullptr; azfopen(&m_pFile, m_strFilePath.c_str(), "r+b"); return m_pFile == NULL; } else { // could not rename //m_pFile = fopen (strNewFilePath.c_str(), "r+b"); return false; } } } // couldn't open temp file return false; } bool ZipDir::CacheRW::RelinkZip(FILE* fTmp) { FileRecordList arrFiles(GetRoot()); arrFiles.SortByFileOffset(); FileRecordList::ZipStats Stats = arrFiles.GetStats(); // we back up our file entries, because we'll need to restore them // in case the operation fails std::vector<FileEntry> arrFileEntryBackup; arrFiles.Backup (arrFileEntryBackup); // this is the set of files that are to be written out - compressed data and the file record iterator std::vector<FileDataRecordPtr> queFiles; queFiles.reserve (g_nMaxItemsRelinkBuffer); // the total size of data in the queue unsigned nQueueSize = 0; for (FileRecordList::iterator it = arrFiles.begin(); it != arrFiles.end(); ++it) { FileEntry* entry = it->pFileEntry; // find the file data offset if (ZD_ERROR_SUCCESS != Refresh(entry)) { return false; } // go to the file data #ifdef WIN32 if (_fseeki64 (m_pFile, (__int64)entry->nFileDataOffset, SEEK_SET) != 0) #else if (fseek (m_pFile, entry->nFileDataOffset, SEEK_SET) != 0) #endif { return false; } // allocate memory for the file compressed data FileDataRecordPtr pFile = FileDataRecord::New (*it); if (!pFile) { return false; } // read the compressed data if (entry->desc.lSizeCompressed && fread (pFile->GetData(), entry->desc.lSizeCompressed, 1, m_pFile) != 1) { return false; } if (entry->nMethod == ZipFile::METHOD_DEFLATE_AND_ENCRYPT) { ZipDir::Decrypt((char*)pFile->GetData(), entry->desc.lSizeCompressed, m_encryptionKey); } // put the file into the queue for copying (writing) queFiles.push_back(pFile); nQueueSize += entry->desc.lSizeCompressed; // if the queue is big enough, write it out if (nQueueSize > g_nSizeRelinkBuffer || queFiles.size() >= g_nMaxItemsRelinkBuffer) { nQueueSize = 0; if (!WriteZipFiles(queFiles, fTmp)) { return false; } } } if (!WriteZipFiles(queFiles, fTmp)) { return false; } ZipFile::ulong lOldCDROffset = m_lCDROffset; // the file data has now been written out. Now write the CDR #ifdef WIN32 m_lCDROffset = (ZipFile::ulong)_ftelli64(fTmp); #else m_lCDROffset = ftell(fTmp); #endif if (m_lCDROffset >= 0 && WriteCDR(fTmp, m_bHeadersEncryptedOnClose) && 0 == fflush (fTmp)) { // the new file positions are already there - just discard the backup and return return true; } // recover from backup arrFiles.Restore (arrFileEntryBackup); m_lCDROffset = lOldCDROffset; m_bEncryptedHeaders = m_bHeadersEncryptedOnClose; return false; } // writes out the file data in the queue into the given file. Empties the queue bool ZipDir::CacheRW::WriteZipFiles(std::vector<FileDataRecordPtr>& queFiles, FILE* fTmp) { for (std::vector<FileDataRecordPtr>::iterator it = queFiles.begin(); it != queFiles.end(); ++it) { // set the new header offset to the file entry - we won't need it #ifdef WIN32 const unsigned long currentPos = (unsigned long)_ftelli64 (fTmp); #else const unsigned long currentPos = ftell (fTmp); #endif (*it)->pFileEntry->nFileHeaderOffset = CalculateAlignedHeaderOffset((*it)->strPath.c_str(), currentPos, m_fileAlignment); // while writing the local header, the data offset will also be calculated if (ZD_ERROR_SUCCESS != WriteLocalHeader(fTmp, (*it)->pFileEntry, (*it)->strPath.c_str(), m_bHeadersEncryptedOnClose)) { return false; } ; // write the compressed file data const bool encrypt = (*it)->pFileEntry->nMethod == ZipFile::METHOD_DEFLATE_AND_ENCRYPT; if (!WriteCompressedData((char*)(*it)->GetData(), (*it)->pFileEntry->desc.lSizeCompressed, encrypt, fTmp)) { return false; } #ifdef WIN32 assert ((*it)->pFileEntry->nEOFOffset == (unsigned long)_ftelli64 (fTmp)); #else assert ((*it)->pFileEntry->nEOFOffset == ftell (fTmp)); #endif } queFiles.clear(); queFiles.reserve (g_nMaxItemsRelinkBuffer); return true; } void TruncateFile(FILE* file, size_t newLength) { #if defined(AZ_PLATFORM_WINDOWS) int filedes = _fileno(file); _chsize_s(filedes, newLength); #elif AZ_TRAIT_OS_PLATFORM_APPLE || defined(AZ_PLATFORM_LINUX) ftruncate(fileno(file), newLength); #else #error Not implemented! #endif } bool ZipDir::CacheRW::EncryptArchive(EncryptionChange change, IEncryptPredicate* encryptContentPredicate, int* numChanged, int* numSkipped) { FileRecordList arrFiles(GetRoot()); arrFiles.SortByFileOffset(); // the total size of data in the queue unsigned nQueueSize = 0; size_t unusedSpace = 0; size_t lastDataEnd = 0; for (FileRecordList::iterator it = arrFiles.begin(); it != arrFiles.end(); ++it) { FileEntry* entry = it->pFileEntry; if (entry->nFileHeaderOffset > lastDataEnd) { fseek(m_pFile, lastDataEnd, SEEK_SET); size_t gapLength = entry->nFileHeaderOffset - lastDataEnd; unusedSpace += gapLength; if (change == ENCRYPT) { if (!WriteRandomData(m_pFile, gapLength)) { return false; } } else { if (!WriteNullData(gapLength)) { return false; } } } lastDataEnd = entry->nEOFOffset; if (numSkipped) { ++(*numSkipped); } // find the file data offset if (ZD_ERROR_SUCCESS != Refresh (entry)) { return false; } bool methodChanged = false; ZipFile::ushort oldMethod = entry->nMethod; ZipFile::ushort newMethod = oldMethod; if (change == ENCRYPT) { if (entry->nMethod == ZipFile::METHOD_DEFLATE) { newMethod = ZipFile::METHOD_DEFLATE_AND_ENCRYPT; } } else { if (entry->nMethod == ZipFile::METHOD_DEFLATE_AND_ENCRYPT) { newMethod = ZipFile::METHOD_DEFLATE; } } // allow encryption only for matching files if (newMethod == ZipFile::METHOD_DEFLATE_AND_ENCRYPT && (!encryptContentPredicate || !encryptContentPredicate->Match(it->strPath.c_str()))) { newMethod = ZipFile::METHOD_DEFLATE; } entry->nMethod = newMethod; const bool encryptHeaders = change == ENCRYPT; // encryption is toggled or compression method changed... if (newMethod != oldMethod || encryptHeaders != m_bEncryptedHeaders) { // ... update header if (ZipDir::WriteLocalHeader(m_pFile, entry, it->strPath.c_str(), encryptHeaders) != ZD_ERROR_SUCCESS) { return false; } } if (newMethod == oldMethod) { // no need to update file content continue; } // go to the file data #ifdef WIN32 if (_fseeki64 (m_pFile, (__int64)entry->nFileDataOffset, SEEK_SET) != 0) #else if (fseek (m_pFile, entry->nFileDataOffset, SEEK_SET) != 0) #endif { return false; } // allocate memory for the file compressed data FileDataRecordPtr pFile = FileDataRecord::New(*it); if (!pFile) { return false; } // read the compressed data if (entry->desc.lSizeCompressed && fread (pFile->GetData(), entry->desc.lSizeCompressed, 1, m_pFile) != 1) { return false; } if (oldMethod == ZipFile::METHOD_DEFLATE_AND_ENCRYPT) { ZipDir::Decrypt((char*)pFile->GetData(), entry->desc.lSizeCompressed, m_encryptionKey); } #ifdef WIN32 if (_fseeki64 (m_pFile, (__int64)entry->nFileDataOffset, SEEK_SET) != 0) #else if (fseek (m_pFile, entry->nFileDataOffset, SEEK_SET) != 0) #endif { return false; } const bool encryptContent = newMethod == ZipFile::METHOD_DEFLATE_AND_ENCRYPT; if (!WriteCompressedData((const char*)pFile->GetData(), entry->desc.lSizeCompressed, encryptContent, m_pFile)) { return false; } if (numSkipped) { --(*numSkipped); } if (numChanged) { ++(*numChanged); } } m_bEncryptedHeaders = change == ENCRYPT; m_bHeadersEncryptedOnClose = m_bEncryptedHeaders; if (!WriteCDR(m_pFile, m_bEncryptedHeaders)) { return false; } if (fflush (m_pFile) != 0) { return false; } size_t endOfCDR = (size_t)ftell(m_pFile); fseek(m_pFile, 0, SEEK_END); size_t fileSize = (size_t)ftell(m_pFile); if (fileSize != endOfCDR) { TruncateFile(m_pFile, endOfCDR); } fclose(m_pFile); m_pFile = 0; m_treeDir.Clear(); return true; }