Files
o3de/Code/Framework/AzFramework/AzFramework/Archive/ZipDirCache.cpp
T
amzn-phist fec77632ef Archive Component - Rewrite and additional work on Archive and Asset Bundler (#4332)
* Fix issues with seedlist for AutomatedTesting

Fixes error reporting so it will show the file hint in the tool.
Removes any missing assets from the .seed file.
Remove an unnecessary dependency from AutomatedTesting dependencies
file.

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

* Make ArchiveComponent use AZ::IO::IArchive

Initial changes that will get the sychronous calls in ArchiveComponent
to use IArchive interface rather than external zip/7z tools.

Some of the asynchronous api are still in place, anything that wasn't
being used has been removed for now.  This may change later if we move
towards all the api being asynchronous.  Until then, we can't remove the
reliance upon the external archive tools completely.

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

* Updates AZStd::thread constructors

Adds a variadic constructor which forwards args to the functor.
Because of our thread_desc extension, there was confusion on the
arugments, so the args were reordered to take the thread_desc first,
before the functor and args.
Also the thread_desc is taken as reference rather than by pointer.
Update callsites to account for this change.

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

* Async operation of ArchiveComponent api

This sets up the ArchiveComponent to operate asynchronously.  It uses
promise/future to transfer results to caller.

This is still broken, there's a few things that need to get fixed up,
but this is a good checkpoint for the work as it solidifies the api,
cleans up a bunch of unused code, and compiles.

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

* Removes the platform-specific ArchiveComponen

These are no longer needed, as they control the direct interaction with
host OS tools like 7za.exe or /bin/zip.

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

* Removes the platform-specific files from cmake

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

* Removes the 7za.exe (and legal notice)

This tool is no longer needed in the repo.

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

* Fixes usage of IArchive::GetFullPath()

This changed to return a PathView, updated to reflect that.

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

* Fix promises and threads

Make sure promises are only set exactly once.  This meant reworking some
of the initial error checking.
Detach threads when created.  Adds [[nodiscard]] to the functions that
return a future.  Since threads are detached, the future is the main way
to get communication from the thread.
Clean up interface, add comments.

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

* More edits to thread creation

Changes to thread construction to account for parameter change.

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

* Fix some remaining issues with ArchiveComponent

Put created threads inside a container, then join them at Deactivate.
Fix asset bundler case when injecting a file with no working directory.
Fix thread constructor that applies args to a function.
Fix lambdas to take string args by value rather than reference.

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

* Fixes some remaining bugs in ArchiveComponent

Open archive as read-only during extract & list operations.
Fixes paths issues.

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

* Fix initialize of opaque thread handle in thread_UnixLike.h

Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>

* Removed unused variable in AssetBundleComponent.cpp to fix compiler warning

Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>

* Fix some issues with archives

File paths in the CDR and the local headers need to match, but there
were issues with path separators and case that made it possible to get
invalid headers errors in some archives.

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

* Adds some new ArchiveComponent unit tests

Adds new tests for extraction of archive and adding files from a file
list to an archive.

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

* Fix file data offset issues when opening archives

When opening an INestedArchive it would run through the CDR headers to
create file entries in the zip cache.  The offsets to the compressed
data were being calculated incorrectly because they were using the CDR
headers rather than jumping to the local file headers and getting
offsets from those sizes.

Removed and refactored some archive validation flow and zip cache
factory init methods to either init default or init w/ additional
validation checks.

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

* Addresses PR feedback

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

* Address more points of feedback in PR

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

* Address additional PR feedback

 Fixes up some error checks and uses of strings vs paths.
 Enable archive component tests on Linux so we can see if they will
 pass.

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

* Address PR feedback

Change the INestedArchive interface to list files as AZ::IO::Path.

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

* Disabling the ArchiveComponent tests on Linux

They failed so we will revisit them to attempt a fix.

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

* Rename a member variable to be more accurate

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

* Address feedback on PR

Bump version of Archive Components for serialize context.
Improve error messages during archive open and validation.

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

* Revert recent changes

Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>

Co-authored-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>
2021-10-01 10:15:16 -05:00

988 lines
35 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/Console/Console.h>
#include <AzCore/IO/FileIO.h>
#include <AzCore/std/string/conversions.h>
#include <AzFramework/Archive/ZipFileFormat.h>
#include <AzFramework/Archive/ZipDirStructures.h>
#include <AzFramework/Archive/ZipDirTree.h>
#include <AzFramework/Archive/ZipDirList.h>
#include <AzFramework/Archive/ZipDirCache.h>
#include <AzFramework/Archive/ZipDirCacheFactory.h>
#include <AzFramework/Archive/ZipDirFind.h>
#include <AzFramework/IO/FileOperations.h>
#include <locale>
#include <random>
#include <cinttypes>
#include <lz4frame.h>
#include <zstd.h>
#include <zlib.h>
namespace AZ::IO::ZipDir
{
AZ_CVAR(int32_t, az_archive_zip_directory_cache_verbosity, 0, nullptr, AZ::ConsoleFunctorFlags::Null,
"Sets the verbosity level for zip directory cache operations\n"
">=1 - Turns on verbose logging of all operations");
namespace ZipDirCacheInternal
{
static AZStd::intrusive_ptr<AZ::IO::MemoryBlock> CreateMemoryBlock(size_t size, const char* usage)
{
if (!AZ::AllocatorInstance<AZ::OSAllocator>::IsReady())
{
AZ_Error("Archive", false, "OSAllocator is not ready. It cannot be used to allocate a MemoryBlock");
return {};
}
AZ::IAllocatorAllocate* allocator = &AZ::AllocatorInstance<AZ::OSAllocator>::Get();
AZStd::intrusive_ptr<AZ::IO::MemoryBlock> memoryBlock{ new (allocator->Allocate(sizeof(AZ::IO::MemoryBlock), alignof(AZ::IO::MemoryBlock))) AZ::IO::MemoryBlock{AZ::IO::MemoryBlockDeleter{ &AZ::AllocatorInstance<AZ::OSAllocator>::Get() }} };
auto CreateFunc = [](size_t byteSize, size_t byteAlignment, const char* name)
{
return reinterpret_cast<uint8_t*>(AZ::AllocatorInstance<AZ::OSAllocator>::Get().Allocate(byteSize, byteAlignment, 0, name));
};
auto DeleterFunc = [](uint8_t* ptrArray)
{
if (ptrArray)
{
AZ::AllocatorInstance<AZ::OSAllocator>::Get().DeAllocate(ptrArray);
}
};
memoryBlock->m_address = AZ::IO::MemoryBlock::AddressPtr{ CreateFunc(size, alignof(uint8_t), usage), AZ::IO::MemoryBlock::AddressDeleter{DeleterFunc} };
memoryBlock->m_size = size;
return memoryBlock;
}
// generates random file name
static AZStd::fixed_string<8> GetRandomName(int nAttempt)
{
if (nAttempt)
{
std::random_device rd; //Will be used to obtain a seed for the random number engine
std::mt19937 gen(rd()); //Standard mersenne_twister_engine seeded with rd()
std::uniform_int_distribution<> distrib(0, 10 + 'z' - 'a');
char szBuf[8];
int i;
for (i = 0; i < AZ_ARRAY_SIZE(szBuf) - 1; ++i)
{
int r = distrib(gen);
szBuf[i] = static_cast<char>(r > 9 ? (r - 10) + 'a' : '0' + r);
}
szBuf[i] = '\0';
return szBuf;
}
else
{
return {};
}
}
}
// creates and if needed automatically destroys the file entry
class FileEntryTransactionAdd
{
public:
operator FileEntry* ()
{
return m_pFileEntry;
}
explicit operator bool() const
{
return m_pFileEntry != nullptr;
}
FileEntry* operator -> () { return m_pFileEntry; }
FileEntryTransactionAdd(Cache* pCache, AZStd::string_view szRelativePath)
: m_pCache(pCache)
, m_szRelativePath(AZ::IO::PosixPathSeparator)
, m_bCommitted(false)
{
// Update the cache string pool with the relative path to the file
auto pathIt = m_pCache->m_relativePathPool.emplace(AZ::IO::PathView(szRelativePath, AZ::IO::PosixPathSeparator).LexicallyNormal());
m_szRelativePath = *pathIt.first;
// this is the name of the directory - create it or find it
m_pFileEntry = m_pCache->GetRoot()->Add(m_szRelativePath.Native());
if (m_pFileEntry && az_archive_zip_directory_cache_verbosity)
{
AZ_TracePrintf("Archive", R"(File "%s" has been added to archive at root "%s")", pathIt.first->c_str(), pCache->GetFilePath());
}
}
~FileEntryTransactionAdd()
{
if (m_pFileEntry && !m_bCommitted)
{
m_pCache->RemoveFile(m_szRelativePath.Native());
m_pCache->m_relativePathPool.erase(m_szRelativePath);
}
}
void Commit()
{
m_bCommitted = true;
}
AZStd::string_view GetRelativePath() const
{
return m_szRelativePath.Native();
}
private:
Cache* m_pCache;
AZ::IO::PathView m_szRelativePath;
FileEntry* m_pFileEntry;
bool m_bCommitted;
};
Cache::Cache()
: Cache{ !AZ::AllocatorInstance<AZ::OSAllocator>::IsReady() ? &AZ::AllocatorInstance<AZ::OSAllocator>::Get() : nullptr }
{
}
Cache::Cache(AZ::IAllocatorAllocate* allocator)
: m_fileHandle(AZ::IO::InvalidHandle)
, m_nFlags(0)
, m_lCDROffset(0)
, m_encryptedHeaders(ZipFile::HEADERS_NOT_ENCRYPTED)
, m_allocator{ allocator }
{
AZ_Assert(allocator, "IAllocatorAllocate object is required in order to allocated memory for the ZipDir Cache operations");
}
void Cache::Close()
{
if (m_fileHandle != AZ::IO::InvalidHandle)
{
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_fileHandle != AZ::IO::InvalidHandle) // RelinkZip() might have closed the file
{
AZ::IO::FileIOBase::GetDirectInstance()->Close(m_fileHandle);
m_fileHandle = AZ::IO::InvalidHandle;
}
}
m_allocator = nullptr;
m_treeDir.Clear();
}
bool Cache::WriteCompressedData(uint8_t* data, size_t size, bool)
{
if (size == 0)
{
return true;
}
// Danny - changed this because writing a single large chunk (more than 6MB?) causes
// Windows fwrite to (silently?!) fail. If it's big then break up the write into
// chunks
const size_t maxChunk = 1 << 20;
size_t sizeLeft = size;
size_t sizeToWrite;
char* ptr = (char*)data;
while ((sizeToWrite = (AZStd::min)(sizeLeft, maxChunk)) != 0)
{
if (!AZ::IO::FileIOBase::GetDirectInstance()->Write(m_fileHandle, ptr, sizeToWrite))
{
return false;
}
ptr += sizeToWrite;
sizeLeft -= sizeToWrite;
}
return true;
}
bool Cache::WriteNullData(size_t size)
{
const size_t maxChunk = 1 << 20;
size_t sizeLeft = size;
size_t sizeToWrite;
AZStd::intrusive_ptr<AZ::IO::MemoryBlock> memoryBlock = ZipDirCacheInternal::CreateMemoryBlock(maxChunk, "ZipDir::Cache::WriteNullData");
if (memoryBlock)
{
return ZD_ERROR_IO_FAILED;
}
memset(memoryBlock->m_address.get(), 0, sizeof(char) * maxChunk);
while ((sizeToWrite = (AZStd::min)(sizeLeft, maxChunk)) != 0)
{
if (!AZ::IO::FileIOBase::GetDirectInstance()->Write(m_fileHandle, memoryBlock->m_address.get(), sizeToWrite))
{
return false;
}
sizeLeft -= sizeToWrite;
}
return true;
}
size_t Cache::GetCompressedSizeEstimate(size_t uncompressedSize, CompressionCodec::Codec codec)
{
switch (codec)
{
case CompressionCodec::Codec::ZLIB:
return (uncompressedSize + (uncompressedSize >> 3) + 32);
case CompressionCodec::Codec::ZSTD:
return ZSTD_compressBound(uncompressedSize);
case CompressionCodec::Codec::LZ4:
return LZ4F_compressFrameBound(uncompressedSize, nullptr);
default:
AZ_Assert(false, "Unknown codec passed in for size estimate");
break;
}
return 0;
}
// Adds a new file to the zip or update an existing one
// adds a directory (creates several nested directories if needed)
ErrorEnum Cache::UpdateFile(AZStd::string_view szRelativePathSrc, const void* pUncompressed, uint64_t nSize, uint32_t nCompressionMethod, int nCompressionLevel, CompressionCodec::Codec codec)
{
AZStd::intrusive_ptr<AZ::IO::MemoryBlock> memoryBlock;
// we'll need the compressed data
void* pCompressed = nullptr;
const void* dataBuffer{};
size_t nSizeCompressed;
int nError = Z_ERRNO;
if (nSize == 0)
{
nCompressionMethod = ZipFile::METHOD_STORE;
}
switch (nCompressionMethod)
{
case ZipFile::METHOD_DEFLATE:
nSizeCompressed = GetCompressedSizeEstimate(nSize, codec);
memoryBlock = ZipDirCacheInternal::CreateMemoryBlock(nSizeCompressed, "Cache::UpdateFile");
pCompressed = memoryBlock->m_address.get();
dataBuffer = pCompressed;
switch (codec)
{
case CompressionCodec::Codec::ZSTD:
nError = ZipRawCompressZSTD(pUncompressed, &nSizeCompressed, pCompressed, nSize, nCompressionLevel);
break;
case CompressionCodec::Codec::ZLIB:
nError = ZipRawCompress(pUncompressed, &nSizeCompressed, pCompressed, nSize, nCompressionLevel);
break;
case CompressionCodec::Codec::LZ4:
nError = ZipRawCompressLZ4(pUncompressed, &nSizeCompressed, pCompressed, nSize, nCompressionLevel);
break;
}
if (Z_OK != nError)
{
return ZD_ERROR_ZLIB_FAILED;
}
break;
case ZipFile::METHOD_STORE:
dataBuffer = pUncompressed;
nSizeCompressed = nSize;
break;
default:
return ZD_ERROR_UNSUPPORTED;
}
// create or find the file entry.. this object will rollback (delete the object
// if the operation fails) if needed.
FileEntryTransactionAdd pFileEntry(this, szRelativePathSrc);
if (!pFileEntry)
{
return ZD_ERROR_INVALID_PATH;
}
pFileEntry->OnNewFileData(pUncompressed, nSize, aznumeric_cast<uint32_t>(nSizeCompressed), nCompressionMethod, 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
uint32_t 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
size_t nFreeSpace = pFileEntry->nEOFOffset - pFileEntry->nFileHeaderOffset - sizeof(ZipFile::LocalFileHeader) - pFileEntry.GetRelativePath().size();
if (nFreeSpace != nSizeCompressed)
{
m_nFlags |= FLAGS_UNCOMPACTED;
}
if (nFreeSpace >= nSizeCompressed)
{
// and we can just override the compressed data in the file
ErrorEnum e = WriteLocalHeader(m_fileHandle, pFileEntry, pFileEntry.GetRelativePath());
if (e != ZD_ERROR_SUCCESS)
{
return e;
}
}
else
{
// we need to write the file anew - in place of current CDR
pFileEntry->nFileHeaderOffset = m_lCDROffset;
ErrorEnum e = WriteLocalHeader(m_fileHandle, pFileEntry, pFileEntry.GetRelativePath());
lNewCDROffset = pFileEntry->nEOFOffset;
if (e != ZD_ERROR_SUCCESS)
{
return e;
}
}
}
else
{
pFileEntry->nFileHeaderOffset = m_lCDROffset;
ErrorEnum e = WriteLocalHeader(m_fileHandle, pFileEntry, pFileEntry.GetRelativePath());
if (e != ZD_ERROR_SUCCESS)
{
return e;
}
lNewCDROffset = aznumeric_cast<uint32_t>(pFileEntry->nFileDataOffset + nSizeCompressed);
m_nFlags |= FLAGS_CDR_DIRTY;
}
// now we have the fresh local header and data offset
if (!AZ::IO::FileIOBase::GetDirectInstance()->Seek(m_fileHandle, pFileEntry->nFileDataOffset, AZ::IO::SeekType::SeekFromStart))
{
return ZD_ERROR_IO_FAILED;
}
const size_t maxChunk = 1 << 20;
size_t sizeLeft = nSizeCompressed;
size_t sizeToWrite;
auto ptr = reinterpret_cast<const uint8_t*>(dataBuffer);
while ((sizeToWrite = (AZStd::min)(sizeLeft, maxChunk)) != 0)
{
if (!AZ::IO::FileIOBase::GetDirectInstance()->Write(m_fileHandle, ptr, sizeToWrite))
{
char error[1024];
azstrerror_s(error, AZ_ARRAY_SIZE(error), errno);
AZ_Warning("Archive", false, "Cannot write to zip file!! error = (%d): %s", errno, error);
return ZD_ERROR_IO_FAILED;
}
ptr += sizeToWrite;
sizeLeft -= sizeToWrite;
}
// 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 one if it is not compressed - just stored - start a big file
ErrorEnum Cache::StartContinuousFileUpdate(AZStd::string_view szRelativePathSrc, uint64_t nSize)
{
AZ::IO::MemoryBlock memoryBlock;
// create or find the file entry.. this object will rollback (delete the object
// if the operation fails) if needed.
FileEntryTransactionAdd pFileEntry(this, szRelativePathSrc);
if (!pFileEntry)
{
return ZD_ERROR_INVALID_PATH;
}
pFileEntry->OnNewFileData(nullptr, 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
size_t lNewCDROffset = m_lCDROffset;
if (pFileEntry->IsInitialized())
{
// check if the new compressed data fits into the old place
size_t nFreeSpace = pFileEntry->nEOFOffset - pFileEntry->nFileHeaderOffset - sizeof(ZipFile::LocalFileHeader) - pFileEntry.GetRelativePath().size();
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_fileHandle, pFileEntry, pFileEntry.GetRelativePath());
if (e != ZD_ERROR_SUCCESS)
{
return e;
}
}
else
{
// we need to write the file anew - in place of current CDR
pFileEntry->nFileHeaderOffset = m_lCDROffset;
ErrorEnum e = WriteLocalHeader(m_fileHandle, pFileEntry, pFileEntry.GetRelativePath());
lNewCDROffset = pFileEntry->nEOFOffset;
if (e != ZD_ERROR_SUCCESS)
{
return e;
}
}
}
else
{
pFileEntry->nFileHeaderOffset = m_lCDROffset;
ErrorEnum e = WriteLocalHeader(m_fileHandle, pFileEntry, pFileEntry.GetRelativePath());
if (e != ZD_ERROR_SUCCESS)
{
return e;
}
lNewCDROffset = pFileEntry->nFileDataOffset + nSize;
m_nFlags |= FLAGS_CDR_DIRTY;
}
if (!AZ::IO::FileIOBase::GetDirectInstance()->Seek(m_fileHandle, pFileEntry->nFileDataOffset, AZ::IO::SeekType::SeekFromStart))
{
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 = aznumeric_cast<uint32_t>(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)
ErrorEnum Cache::UpdateFileContinuousSegment(AZStd::string_view szRelativePathSrc, [[maybe_unused]] uint64_t nSize, const void* pUncompressed, uint64_t nSegmentSize, uint64_t nOverwriteSeekPos)
{
const bool shouldOverwriteSeekOffset = nOverwriteSeekPos != (std::numeric_limits<uint64_t>::max)();
AZ::IO::MemoryBlock memoryBlock;
// create or find the file entry.. this object will rollback (delete the object
// if the operation fails) if needed.
FileEntryTransactionAdd pFileEntry(this, szRelativePathSrc);
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
uint64_t lSegmentOffset = pFileEntry->nEOFOffset;
if (!AZ::IO::FileIOBase::GetDirectInstance()->Seek(m_fileHandle, pFileEntry->nFileHeaderOffset, AZ::IO::SeekType::SeekFromStart))
{
return ZD_ERROR_IO_FAILED;
}
// and we can just override the compressed data in the file
ErrorEnum e = WriteLocalHeader(m_fileHandle, pFileEntry, pFileEntry.GetRelativePath());
if (e != ZD_ERROR_SUCCESS)
{
return e;
}
if (shouldOverwriteSeekOffset)
{
lSegmentOffset = pFileEntry->nFileDataOffset + nOverwriteSeekPos;
}
if (!AZ::IO::FileIOBase::GetDirectInstance()->Seek(m_fileHandle, lSegmentOffset, AZ::IO::SeekType::SeekFromStart))
{
return ZD_ERROR_IO_FAILED;
}
constexpr bool encrypt = false; // we do not support encryption for continuous file update
if (!WriteCompressedData((uint8_t*)pUncompressed, nSegmentSize, encrypt))
{
char error[1024];
azstrerror_s(error, AZ_ARRAY_SIZE(error), errno);
AZ_Warning("Archive", false, "Cannot write to zip file!! error = (%d): %s", errno, error);
return ZD_ERROR_IO_FAILED;
}
if (!shouldOverwriteSeekOffset)
{
pFileEntry->nEOFOffset = aznumeric_cast<uint32_t>(lSegmentOffset + nSegmentSize);
}
// since we wrote the file successfully, update CDR
pFileEntry.Commit();
return ZD_ERROR_SUCCESS;
}
ErrorEnum Cache::UpdateFileCRC(AZStd::string_view szRelativePathSrc, AZ::Crc32 dwCRC32)
{
// create or find the file entry.. this object will rollback (delete the object
// if the operation fails) if needed.
FileEntryTransactionAdd pFileEntry(this, szRelativePathSrc);
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;
if (!AZ::IO::FileIOBase::GetDirectInstance()->Seek(m_fileHandle, pFileEntry->nFileHeaderOffset, AZ::IO::SeekType::SeekFromStart))
{
return ZD_ERROR_IO_FAILED;
}
// and we can just override the compressed data in the file
ErrorEnum e = WriteLocalHeader(m_fileHandle, pFileEntry, pFileEntry.GetRelativePath());
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
ErrorEnum Cache::RemoveFile(AZStd::string_view szRelativePathSrc)
{
AZ::IO::PathView szRelativePath{ szRelativePathSrc };
AZStd::string_view fileName; // the name of the file to delete
FileEntryTree* pDir; // the dir from which the subdir will be deleted
if (szRelativePath.HasParentPath())
{
FindDir fd(GetRoot());
// the directory to remove
pDir = fd.FindExact(szRelativePath.ParentPath());
if (!pDir)
{
return ZD_ERROR_DIR_NOT_FOUND;// there is no such directory
}
fileName = szRelativePath.Filename().Native();
}
else
{
pDir = GetRoot();
fileName = szRelativePath.Native();
}
ErrorEnum e = pDir->RemoveFile(fileName);
if (e == ZD_ERROR_SUCCESS)
{
m_nFlags |= FLAGS_UNCOMPACTED | FLAGS_CDR_DIRTY;
if (az_archive_zip_directory_cache_verbosity)
{
AZ_TracePrintf("Archive", R"(File "%.*s" has been remove from archive at root "%s")",
AZ_STRING_ARG(szRelativePath.Native()), GetFilePath());
}
}
return e;
}
// deletes the directory, with all its descendants (files and subdirs)
ErrorEnum Cache::RemoveDir(AZStd::string_view szRelativePathSrc)
{
AZ::IO::PathView szRelativePath{ szRelativePathSrc };
AZStd::string_view dirName; // the name of the dir to delete
FileEntryTree* pDir; // the dir from which the subdir will be deleted
if (szRelativePath.HasParentPath())
{
FindDir fd(GetRoot());
// the directory to remove
pDir = fd.FindExact(szRelativePath.ParentPath());
if (!pDir)
{
return ZD_ERROR_DIR_NOT_FOUND;// there is no such directory
}
dirName = szRelativePath.Filename().Native();
}
else
{
pDir = GetRoot();
dirName = szRelativePath.Native();
}
ErrorEnum e = pDir->RemoveDir(dirName);
if (e == ZD_ERROR_SUCCESS)
{
m_nFlags |= FLAGS_UNCOMPACTED | FLAGS_CDR_DIRTY;
if (az_archive_zip_directory_cache_verbosity)
{
AZ_TracePrintf("Archive", R"(Directory "%.*s" has been remove from archive at root "%s")",
AZ_STRING_ARG(szRelativePath.Native()), GetFilePath());
}
}
return e;
}
// deletes all files and directories in this archive
ErrorEnum Cache::RemoveAll()
{
ErrorEnum e = m_treeDir.RemoveAll();
if (e == ZD_ERROR_SUCCESS)
{
m_nFlags |= FLAGS_UNCOMPACTED | FLAGS_CDR_DIRTY;
}
return e;
}
ErrorEnum Cache::ReadFile(FileEntry* pFileEntry, void* pCompressed, void* pUncompressed)
{
if (!pFileEntry)
{
return ZD_ERROR_INVALID_CALL;
}
if (pFileEntry->desc.lSizeUncompressed == 0)
{
// note that even in a compressed zip file, 0 bytes MUST be stored using "STORE" compression and thus will remain 0
AZ_Assert(pFileEntry->desc.lSizeCompressed == 0, "Compressed file has uncompressed size of 0, but compressed size of %" PRIu32, pFileEntry->desc.lSizeCompressed);
return ZD_ERROR_SUCCESS;
}
AZ_Assert(pFileEntry->desc.lSizeCompressed > 0, "Compressed file has compressed size of 0. It cannot be read");
ErrorEnum nError = Refresh(pFileEntry);
if (nError != ZD_ERROR_SUCCESS)
{
return nError;
}
if (!AZ::IO::FileIOBase::GetDirectInstance()->Seek(m_fileHandle, pFileEntry->nFileDataOffset, AZ::IO::SeekType::SeekFromStart))
{
return ZD_ERROR_IO_FAILED;
}
AZStd::intrusive_ptr<AZ::IO::MemoryBlock> memoryBlock;
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;
}
memoryBlock = ZipDirCacheInternal::CreateMemoryBlock(pFileEntry->desc.lSizeCompressed, "Cache::ReadFile");
pBuffer = memoryBlock->m_address.get();
}
if (!AZ::IO::FileIOBase::GetDirectInstance()->Read(m_fileHandle, pBuffer, pFileEntry->desc.lSizeCompressed, true))
{
return ZD_ERROR_IO_FAILED;
}
// if there's a buffer for uncompressed data, uncompress it to that buffer
if (pUncompressed)
{
if (pFileEntry->nMethod == 0)
{
AZ_Assert(pBuffer == pUncompressed, "When the file entry is uncompressed the buffer should point to uncompressed buffer");
}
else
{
size_t nSizeUncompressed = pFileEntry->desc.lSizeUncompressed;
if (Z_OK != ZipRawUncompress(pUncompressed, &nSizeUncompressed, pBuffer, pFileEntry->desc.lSizeCompressed))
{
return ZD_ERROR_CORRUPTED_DATA;
}
if (pFileEntry->bCheckCRCNextRead)
{
pFileEntry->bCheckCRCNextRead = false;
uLong uCRC32 = AZ::Crc32((Bytef*)pUncompressed, nSizeUncompressed);
if (uCRC32 != pFileEntry->desc.lCRC32)
{
AZ_Warning("Archive", false, "ZD_ERROR_CRC32_CHECK: Uncompressed stream CRC32 check failed");
return ZD_ERROR_CRC32_CHECK;
}
}
}
}
return ZD_ERROR_SUCCESS;
}
//////////////////////////////////////////////////////////////////////////
// finds the file by exact path
FileEntry* Cache::FindFile(AZStd::string_view szPathSrc, [[maybe_unused]] bool bFullInfo)
{
AZ::IO::PathView szPath{ szPathSrc };
ZipDir::FindFile fd(GetRoot());
FileEntry* fileEntry = fd.FindExact(szPath);
if (!fileEntry)
{
if (az_archive_zip_directory_cache_verbosity)
{
AZ_TracePrintf("Archive", "FindExact failed to find file %.*s at root %s", AZ_STRING_ARG(szPath.Native()), GetFilePath());
}
return {};
}
return fileEntry;
}
// refreshes information about the given file entry into this file entry
ErrorEnum Cache::Refresh(FileEntryBase* pFileEntry)
{
if (!pFileEntry)
{
return ZD_ERROR_INVALID_CALL;
}
if (pFileEntry->nFileDataOffset != FileEntryBase::INVALID_DATA_OFFSET)
{
return ZD_ERROR_SUCCESS; // the data offset has been successfully read..
}
CZipFile tmp;
tmp.m_fileHandle = m_fileHandle;
return ZipDir::Refresh(&tmp, pFileEntry);
}
// writes the CDR to the disk
bool Cache::WriteCDR(AZ::IO::HandleType fTarget)
{
if (fTarget == AZ::IO::InvalidHandle)
{
return false;
}
if (!AZ::IO::FileIOBase::GetDirectInstance()->Seek(fTarget, m_lCDROffset, AZ::IO::SeekType::SeekFromStart))
{
return false;
}
FileRecordList arrFiles(GetRoot());
//arrFiles.SortByFileOffset();
size_t nSizeCDR = arrFiles.GetStats().nSizeCDR;
void* pCDR = m_allocator->Allocate(nSizeCDR, alignof(uint8_t), 0, "Cache::WriteCDR");
[[maybe_unused]] size_t nSizeCDRSerialized = arrFiles.MakeZipCDR(m_lCDROffset, pCDR);
AZ_Assert(nSizeCDRSerialized == nSizeCDR, "Serialized CDR size %zu does not match size in memory %zu", nSizeCDRSerialized, nSizeCDR);
if (m_encryptedHeaders == ZipFile::HEADERS_ENCRYPTED_TEA)
{
AZ_Warning("Archive", false, "Attempt to use XTEA pak encryption when it is disabled.");
Free(pCDR);
return false;
}
bool success = AZ::IO::FileIOBase::GetDirectInstance()->Write(fTarget, pCDR, nSizeCDR);
Free(pCDR);
return success;
}
bool Cache::RelinkZip()
{
AZ::IO::FileIOBase* fileSystem = AZ::IO::FileIOBase::GetDirectInstance();
for (int nAttempt = 0; nAttempt < 32; ++nAttempt)
{
auto strNewFilePath = AZ::IO::PathString::format("%s$%s", m_strFilePath.c_str(), ZipDirCacheInternal::GetRandomName(nAttempt).c_str());
AZ::IO::HandleType fileHandle;
if (fileSystem->Open(strNewFilePath.c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeBinary, fileHandle))
{
bool bOk = RelinkZip(fileHandle);
fileSystem->Close(fileHandle);
if (!bOk)
{
// we don't need the temporary file
fileSystem->Remove(strNewFilePath.c_str());
return false;
}
// we successfully relinked, now copy the temporary file to the original file
fileSystem->Close(m_fileHandle);
m_fileHandle = AZ::IO::InvalidHandle;
fileSystem->Remove(m_strFilePath.c_str());
if (AZ::IO::Move(strNewFilePath.c_str(), m_strFilePath.c_str()))
{
// successfully renamed - reopen
return fileSystem->Open(m_strFilePath.c_str(), AZ::IO::OpenMode::ModeRead | AZ::IO::OpenMode::ModeUpdate | AZ::IO::OpenMode::ModeBinary, m_fileHandle);
}
else
{
// could not rename
return false;
}
}
}
// couldn't open temp file
return false;
}
bool Cache::RelinkZip(AZ::IO::HandleType fTmp)
{
FileRecordList arrFiles(GetRoot());
arrFiles.SortByFileOffset();
// we back up our file entries, because we'll need to restore them
// in case the operation fails
AZStd::vector<FileEntryBase> arrFileEntryBackup;
arrFiles.Backup(arrFileEntryBackup);
// this is the set of files that are to be written out - compressed data and the file record iterator
AZStd::vector<FileDataRecordPtr> queFiles;
queFiles.reserve(g_nMaxItemsRelinkBuffer);
AZ::IO::FileIOBase* fileSystem = AZ::IO::FileIOBase::GetDirectInstance();
// the total size of data in the queue
uint32_t nQueueSize = 0;
for (FileRecordList::iterator it = arrFiles.begin(); it != arrFiles.end(); ++it)
{
// find the file data offset
if (ZD_ERROR_SUCCESS != Refresh(it->pFileEntryBase))
{
return false;
}
// go to the file data
if (!fileSystem->Seek(m_fileHandle, it->pFileEntryBase->nFileDataOffset, AZ::IO::SeekType::SeekFromStart))
{
return false;
}
// allocate memory for the file compressed data
FileDataRecordPtr pFile = FileDataRecord::New(*it, m_allocator);
if (!pFile)
{
return false;
}
// read the compressed data
if (it->pFileEntryBase->desc.lSizeCompressed && !fileSystem->Read(m_fileHandle, pFile->GetData(), it->pFileEntryBase->desc.lSizeCompressed, true))
{
return false;
}
// put the file into the queue for copying (writing)
queFiles.push_back(pFile);
nQueueSize += it->pFileEntryBase->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;
}
uint32_t lOldCDROffset = m_lCDROffset;
// the file data has now been written out. Now write the CDR
AZ::u64 tellOffset = 0;
fileSystem->Tell(fTmp, tellOffset);
m_lCDROffset = static_cast<uint32_t>(tellOffset);
if (WriteCDR(fTmp) && AZ::IO::FileIOBase::GetDirectInstance()->Flush(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;
return false;
}
// writes out the file data in the queue into the given file. Empties the queue
bool Cache::WriteZipFiles(AZStd::vector<FileDataRecordPtr>& queFiles, AZ::IO::HandleType fTmp)
{
AZ::IO::FileIOBase* fileSystem = AZ::IO::FileIOBase::GetDirectInstance();
for (auto it = queFiles.begin(); it != queFiles.end(); ++it)
{
// set the new header offset to the file entry - we won't need it
AZ::u64 tellOffset = 0;
fileSystem->Tell(fTmp, tellOffset);
(*it)->pFileEntryBase->nFileHeaderOffset = static_cast<uint32_t>(tellOffset);
// while writing the local header, the data offset will also be calculated
if (ZD_ERROR_SUCCESS != WriteLocalHeader(fTmp, (*it)->pFileEntryBase, (*it)->strPath.c_str()))
{
return false;
}
// write the compressed file data
if ((*it)->pFileEntryBase->desc.lSizeCompressed && !fileSystem->Write(fTmp, (*it)->GetData(), (*it)->pFileEntryBase->desc.lSizeCompressed))
{
return false;
}
tellOffset = 0;
fileSystem->Tell(fTmp, tellOffset);
AZ_Assert((*it)->pFileEntryBase->nEOFOffset == tellOffset, "File offset %" PRIx64 " does not match file entry EOF offset of %" PRIx32,
tellOffset, (*it)->pFileEntryBase->nEOFOffset);
}
queFiles.clear();
queFiles.reserve(g_nMaxItemsRelinkBuffer);
return true;
}
}