Files
o3de/Code/Framework/AzToolsFramework/AzToolsFramework/Archive/ArchiveComponent.cpp
T
Steve Pham 8fd603cd50 Fix failed archive tests on linux and remove trait
- Change the test Archive Folder name, Extract Folder Name, and File List file to be all lowercase to prevent the 'ResolvePath' function in LocalFileIO when executed to cause issues on case sensitive file systems (#6277)
- Fix memory corruption in profile builds on Linux when using AZ::IO::PathView by using AZ::IO::FixedMaxPath instead

Signed-off-by: Steve Pham <82231385+spham-amzn@users.noreply.github.com>
2021-12-09 10:18:11 -08:00

601 lines
22 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 "ArchiveComponent.h"
#include <AzCore/Component/TickBus.h>
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzFramework/Archive/INestedArchive.h>
#include <AzFramework/Archive/ZipDirStructures.h>
#include <AzFramework/Process/ProcessCommunicator.h>
#include <AzFramework/Process/ProcessWatcher.h>
#include <AzFramework/FileFunc/FileFunc.h>
namespace AzToolsFramework
{
[[maybe_unused]] constexpr const char s_traceName[] = "ArchiveComponent";
constexpr AZ::u32 s_compressionMethod = AZ::IO::INestedArchive::METHOD_DEFLATE;
constexpr AZ::s32 s_compressionLevel = AZ::IO::INestedArchive::LEVEL_NORMAL;
constexpr CompressionCodec::Codec s_compressionCodec = CompressionCodec::Codec::ZLIB;
namespace ArchiveUtils
{
// Read a file's contents into a provided buffer.
// Does not add a zero byte at the end of the buffer.
// returns true if read was successful, false otherwise.
bool ReadFile(const AZ::IO::Path& filePath, AZ::IO::OpenMode openMode, AZStd::vector<char>& outBuffer)
{
auto fileIO = AZ::IO::FileIOBase::GetDirectInstance();
if (!fileIO)
{
return false;
}
bool success = false;
AZ::IO::HandleType fileHandle = AZ::IO::InvalidHandle;
if (fileIO->Open(filePath.c_str(), openMode, fileHandle))
{
AZ::u64 fileSize = 0;
if (fileIO->Size(fileHandle, fileSize) && fileSize != 0)
{
outBuffer.resize_no_construct(fileSize);
AZ::u64 bytesRead = 0;
if (fileIO->Read(fileHandle, outBuffer.data(), fileSize, true, &bytesRead))
{
success = (fileSize == bytesRead);
}
}
fileIO->Close(fileHandle);
}
return success;
}
// Reads a text file that contains a list of file paths.
// Tokenize the file by lines.
// Calls the lineVisitor function for each line of the file.
void ProcessFileList(const AZ::IO::Path& filePath, AZStd::function<void(AZStd::string_view line)> lineVisitor)
{
AZStd::vector<char> fileBuffer;
if (ReadFile(filePath, AZ::IO::OpenMode::ModeText | AZ::IO::OpenMode::ModeRead, fileBuffer))
{
AZ::StringFunc::TokenizeVisitor(AZStd::string_view{ fileBuffer.data(), fileBuffer.size() }, lineVisitor, "\n");
}
}
} // namespace ArchiveUtils
void ArchiveComponent::Activate()
{
m_fileIO = AZ::IO::FileIOBase::GetDirectInstance();
if (m_fileIO == nullptr)
{
AZ_Error(s_traceName, false, "Failed to create a LocalFileIO instance!");
}
m_archive = AZ::Interface<AZ::IO::IArchive>::Get();
if (m_archive == nullptr)
{
AZ_Error(s_traceName, false, "Failed to get IArchive interface!");
}
ArchiveCommandsBus::Handler::BusConnect();
}
void ArchiveComponent::Deactivate()
{
ArchiveCommandsBus::Handler::BusDisconnect();
m_fileIO = nullptr;
m_archive = nullptr;
for (AZStd::thread& t : m_threads)
{
t.join();
}
m_threads = {};
}
void ArchiveComponent::Reflect(AZ::ReflectContext * context)
{
if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<ArchiveComponent, AZ::Component>()
->Version(2)
->Attribute(AZ::Edit::Attributes::SystemComponentTags, AZStd::vector<AZ::Crc32>({ AZ_CRC_CE("AssetBuilder") }))
;
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
{
editContext->Class<ArchiveComponent>(
"Archive", "Handles creation and extraction of zip archives.")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Category, "Editor")
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("System"))
;
}
}
}
std::future<bool> ArchiveComponent::CreateArchive(
const AZStd::string& archivePath,
const AZStd::string& dirToArchive)
{
if (!CheckParamsForCreate(archivePath, dirToArchive))
{
std::promise<bool> p;
p.set_value(false);
return p.get_future();
}
auto FnCreateArchive = [this, archivePath, dirToArchive](std::promise<bool>&& p) -> void
{
auto archive = m_archive->OpenArchive(archivePath, {}, AZ::IO::INestedArchive::FLAGS_CREATE_NEW);
if (!archive)
{
AZ_Error(s_traceName, false, "Failed to create archive file '%s'", archivePath.c_str());
p.set_value(false);
return;
}
auto foundFiles = AzFramework::FileFunc::FindFilesInPath(dirToArchive, "*", true);
if (!foundFiles.IsSuccess())
{
AZ_Error(s_traceName, false, "Failed to find file listing under directory '%d'", dirToArchive.c_str());
p.set_value(false);
return;
}
bool success = true;
AZStd::vector<char> fileBuffer;
const AZ::IO::FixedMaxPath workingPath{ dirToArchive };
for (const auto& fileName : foundFiles.GetValue())
{
bool thisSuccess = false;
AZ::IO::FixedMaxPath relativePath = AZ::IO::FixedMaxPath{ fileName }.LexicallyRelative(workingPath);
AZ::IO::FixedMaxPath fullPath = (workingPath / relativePath);
if (ArchiveUtils::ReadFile(static_cast<AZ::IO::PathView>(fullPath), AZ::IO::OpenMode::ModeRead, fileBuffer))
{
int result = archive->UpdateFile(
relativePath.Native(), fileBuffer.data(), fileBuffer.size(), s_compressionMethod,
s_compressionLevel, s_compressionCodec);
thisSuccess = (result == AZ::IO::ZipDir::ZD_ERROR_SUCCESS);
AZ_Error(
s_traceName, thisSuccess, "Error %d encountered while adding '%s' to archive '%.*s'", result, fileName.c_str(),
AZ_STRING_ARG(archive->GetFullPath().Native()));
}
else
{
AZ_Error(
s_traceName, false, "Error encountered while reading '%s' to add to archive '%.*s'", fileName.c_str(),
AZ_STRING_ARG(archive->GetFullPath().Native()));
}
success = (success && thisSuccess);
}
archive.reset();
p.set_value(success);
};
// Async task...
std::promise<bool> p;
std::future<bool> f = p.get_future();
AZStd::thread_desc threadDesc;
threadDesc.m_name = "Archive Task (Create)";
m_threads.emplace_back(threadDesc, FnCreateArchive, AZStd::move(p));
return f;
}
std::future<bool> ArchiveComponent::ExtractArchive(
const AZStd::string& archivePath,
const AZStd::string& destinationPath)
{
if (!CheckParamsForExtract(archivePath, destinationPath))
{
std::promise<bool> p;
p.set_value(false);
return p.get_future();
}
auto FnExtractArchive = [this, archivePath, destinationPath](std::promise<bool>&& p) -> void
{
auto archive = m_archive->OpenArchive(archivePath, {}, AZ::IO::INestedArchive::FLAGS_READ_ONLY);
if (!archive)
{
AZ_Error(s_traceName, false, "Failed to open archive file '%s'", archivePath.c_str());
p.set_value(false);
return;
}
AZStd::vector<AZ::IO::Path> filesInArchive;
if (int result = archive->ListAllFiles(filesInArchive); result != AZ::IO::ZipDir::ZD_ERROR_SUCCESS)
{
AZ_Error(s_traceName, false, "Failed to get list of files in archive '%s'", archivePath.c_str());
p.set_value(false);
return;
}
AZStd::vector<AZ::u8> fileBuffer;
AZ::IO::Path destination{ destinationPath };
AZ::u64 fileSize = 0;
AZ::u64 numFilesWritten = 0;
AZ::u64 bytesWritten = 0;
AZ::IO::INestedArchive::Handle srcHandle{};
AZ::IO::HandleType dstHandle = AZ::IO::InvalidHandle;
constexpr AZ::IO::OpenMode openMode =
(AZ::IO::OpenMode::ModeCreatePath | AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeUpdate);
for (const auto& filePath : filesInArchive)
{
srcHandle = archive->FindFile(filePath.Native());
AZ_Assert(srcHandle != nullptr, "File '%s' does not exist inside archive '%s'", filePath.c_str(), archivePath.c_str());
fileSize = (srcHandle != nullptr) ? archive->GetFileSize(srcHandle) : 0;
fileBuffer.resize_no_construct(fileSize);
if (auto result = archive->ReadFile(srcHandle, fileBuffer.data()); result != AZ::IO::ZipDir::ZD_ERROR_SUCCESS)
{
AZ_Error(
s_traceName, false, "Failed to read file '%s' in archive '%s' with error %d", filePath.c_str(), archivePath.c_str(),
result);
continue;
}
AZ::IO::Path destinationFile = destination / filePath;
if (!m_fileIO->Open(destinationFile.c_str(), openMode, dstHandle))
{
AZ_Error(s_traceName, false, "Failed to open '%s' for writing", destinationFile.c_str());
continue;
}
if (!m_fileIO->Write(dstHandle, fileBuffer.data(), fileSize, &bytesWritten))
{
AZ_Error(s_traceName, false, "Failed to write destination file '%s'", destinationFile.c_str());
}
else if (bytesWritten == fileSize)
{
++numFilesWritten;
}
m_fileIO->Close(dstHandle);
}
p.set_value(numFilesWritten == filesInArchive.size());
};
// Async task...
std::promise<bool> p;
std::future<bool> f = p.get_future();
AZStd::thread_desc threadDesc;
threadDesc.m_name = "Archive Task (Extract)";
m_threads.emplace_back(threadDesc, FnExtractArchive, AZStd::move(p));
return f;
}
std::future<bool> ArchiveComponent::ExtractFile(
const AZStd::string& archivePath,
const AZStd::string& fileInArchive,
const AZStd::string& destinationPath)
{
if (!CheckParamsForExtract(archivePath, destinationPath))
{
std::promise<bool> p;
p.set_value(false);
return p.get_future();
}
auto FnExtractFile = [this, archivePath, fileInArchive, destinationPath](std::promise<bool>&& p) -> void
{
auto archive = m_archive->OpenArchive(archivePath, {}, AZ::IO::INestedArchive::FLAGS_READ_ONLY);
if (!archive)
{
AZ_Error(s_traceName, false, "Failed to open archive file '%s'", archivePath.c_str());
p.set_value(false);
return;
}
AZ::IO::INestedArchive::Handle fileHandle = archive->FindFile(fileInArchive);
if (!fileHandle)
{
AZ_Error(s_traceName, false, "File '%s' does not exist inside archive '%s'", fileInArchive.c_str(), archivePath.c_str());
p.set_value(false);
return;
}
AZ::u64 fileSize = archive->GetFileSize(fileHandle);
AZStd::vector<AZ::u8> fileBuffer;
fileBuffer.resize_no_construct(fileSize);
if (auto result = archive->ReadFile(fileHandle, fileBuffer.data()); result != AZ::IO::ZipDir::ZD_ERROR_SUCCESS)
{
AZ_Error(
s_traceName, false, "Failed to read file '%s' in archive '%s' with error %d", fileInArchive.c_str(),
archivePath.c_str(), result);
p.set_value(false);
return;
}
AZ::IO::HandleType destFileHandle = AZ::IO::InvalidHandle;
AZ::IO::Path destinationFile{ destinationPath };
destinationFile /= fileInArchive;
AZ::IO::OpenMode openMode = (AZ::IO::OpenMode::ModeCreatePath | AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeUpdate);
if (!m_fileIO->Open(destinationFile.c_str(), openMode, destFileHandle))
{
AZ_Error(s_traceName, false, "Failed to open destination file '%s' for writing", destinationFile.c_str());
p.set_value(false);
return;
}
AZ::u64 bytesWritten = 0;
if (!m_fileIO->Write(destFileHandle, fileBuffer.data(), fileSize, &bytesWritten))
{
AZ_Error(s_traceName, false, "Failed to write destination file '%s'", destinationFile.c_str());
}
m_fileIO->Close(destFileHandle);
p.set_value(bytesWritten == fileSize);
};
// Async task...
std::promise<bool> p;
std::future<bool> f = p.get_future();
AZStd::thread_desc threadDesc;
threadDesc.m_name = "Archive Task (Extract Single)";
m_threads.emplace_back(threadDesc, FnExtractFile, AZStd::move(p));
return f;
}
bool ArchiveComponent::ListFilesInArchive(const AZStd::string& archivePath, AZStd::vector<AZStd::string>& outFileEntries)
{
if (!m_fileIO || !m_archive)
{
return false;
}
if (!m_fileIO->Exists(archivePath.c_str()))
{
AZ_Error(s_traceName, false, "Archive '%s' does not exist!", archivePath.c_str());
return false;
}
auto archive = m_archive->OpenArchive(archivePath, {}, AZ::IO::INestedArchive::FLAGS_READ_ONLY);
if (!archive)
{
AZ_Error(s_traceName, false, "Failed to open archive file '%s'", archivePath.c_str());
return false;
}
AZStd::vector<AZ::IO::Path> fileEntries;
int result = archive->ListAllFiles(fileEntries);
outFileEntries.clear();
for (const auto& path : fileEntries)
{
outFileEntries.emplace_back(path.String());
}
return (result == AZ::IO::ZipDir::ZD_ERROR_SUCCESS);
}
std::future<bool> ArchiveComponent::AddFileToArchive(
const AZStd::string& archivePath,
const AZStd::string& workingDirectory,
const AZStd::string& fileToAdd)
{
if (!CheckParamsForAdd(workingDirectory, fileToAdd))
{
std::promise<bool> p;
p.set_value(false);
return p.get_future();
}
auto FnAddFileToArchive = [this, archivePath, workingDirectory, fileToAdd](std::promise<bool>&& p) -> void
{
auto archive = m_archive->OpenArchive(archivePath);
if (!archive)
{
AZ_Error(s_traceName, false, "Failed to open archive file '%s'", archivePath.c_str());
p.set_value(false);
return;
}
AZ::IO::Path workingPath{ workingDirectory };
AZ::IO::Path fullPath = workingPath / fileToAdd;
AZ::IO::PathView relativePath = AZ::IO::PathView{ fullPath }.LexicallyRelative(workingPath);
AZStd::vector<char> fileBuffer;
bool success = false;
if (ArchiveUtils::ReadFile(fullPath, AZ::IO::OpenMode::ModeRead, fileBuffer))
{
int result = archive->UpdateFile(
relativePath.Native(), fileBuffer.data(), fileBuffer.size(), s_compressionMethod,
s_compressionLevel, s_compressionCodec);
success = (result == AZ::IO::ZipDir::ZD_ERROR_SUCCESS);
AZ_Error(
s_traceName, success, "Error %d encountered while adding '%s' to archive '%.*s'", result, fileToAdd.c_str(),
AZ_STRING_ARG(archive->GetFullPath().Native()));
}
else
{
AZ_Error(
s_traceName, false, "Error encountered while reading '%s' to add to archive '%.*s'", fileToAdd.c_str(),
AZ_STRING_ARG(archive->GetFullPath().Native()));
}
archive.reset();
p.set_value(success);
};
// Async task...
std::promise<bool> p;
std::future<bool> f = p.get_future();
AZStd::thread_desc threadDesc;
threadDesc.m_name = "Archive Task (Add Single)";
m_threads.emplace_back(threadDesc, FnAddFileToArchive, AZStd::move(p));
return f;
}
std::future<bool> ArchiveComponent::AddFilesToArchive(
const AZStd::string& archivePath,
const AZStd::string& workingDirectory,
const AZStd::string& listFilePath)
{
if (!CheckParamsForAdd(workingDirectory, listFilePath))
{
std::promise<bool> p;
p.set_value(false);
return p.get_future();
}
auto FnAddFilesToArchive = [this, archivePath, workingDirectory, listFilePath](std::promise<bool>&& p) -> void
{
auto archive = m_archive->OpenArchive(archivePath);
if (!archive)
{
AZ_Error(s_traceName, false, "Failed to open archive file '%s'", archivePath.c_str());
p.set_value(false);
return;
}
bool success = true; // starts true and turns false when any error is encountered.
AZ::IO::Path basePath{ workingDirectory };
auto PerLineCallback = [&success, &basePath, &archive](AZStd::string_view filePathLine) -> void
{
AZStd::vector<char> fileBuffer;
AZ::IO::Path fullPath = (basePath / filePathLine);
if (ArchiveUtils::ReadFile(fullPath, AZ::IO::OpenMode::ModeRead, fileBuffer))
{
int result = archive->UpdateFile(
filePathLine, fileBuffer.data(), fileBuffer.size(), s_compressionMethod,
s_compressionLevel, s_compressionCodec);
bool thisSuccess = (result == AZ::IO::ZipDir::ZD_ERROR_SUCCESS);
success = (success && thisSuccess);
AZ_Error(
s_traceName, thisSuccess, "Error %d encountered while adding '%.*s' to archive '%.*s'", result,
AZ_STRING_ARG(filePathLine), AZ_STRING_ARG(archive->GetFullPath().Native()));
}
else
{
AZ_Error(
s_traceName, false, "Error encountered while reading '%.*s' to add to archive '%.*s'", AZ_STRING_ARG(filePathLine),
AZ_STRING_ARG(archive->GetFullPath().Native()));
}
};
ArchiveUtils::ProcessFileList(listFilePath, PerLineCallback);
archive.reset();
p.set_value(success);
};
// Async task...
std::promise<bool> p;
std::future<bool> f = p.get_future();
AZStd::thread_desc threadDesc;
threadDesc.m_name = "Archive Task (Add)";
m_threads.emplace_back(threadDesc, FnAddFilesToArchive, AZStd::move(p));
return f;
}
bool ArchiveComponent::CheckParamsForAdd(const AZStd::string& directory, const AZStd::string& file)
{
if (!m_fileIO || !m_archive)
{
return false;
}
if (!m_fileIO->IsDirectory(directory.c_str()))
{
AZ_Error(
s_traceName, false, "Working directory '%s' is not a directory or doesn't exist!", directory.c_str());
return false;
}
if (!file.empty())
{
auto filePath = AZ::IO::Path{ directory } / file;
if (!m_fileIO->Exists(filePath.c_str()) || m_fileIO->IsDirectory(filePath.c_str()))
{
AZ_Error(s_traceName, false, "File list '%s' is a directory or doesn't exist!", filePath.c_str());
return false;
}
}
return true;
}
bool ArchiveComponent::CheckParamsForExtract(const AZStd::string& archive, const AZStd::string& directory)
{
if (!m_fileIO || !m_archive)
{
return false;
}
if (!m_fileIO->Exists(archive.c_str()))
{
AZ_Error(s_traceName, false, "Archive '%s' does not exist!", archive.c_str());
return false;
}
if (!m_fileIO->Exists(directory.c_str()))
{
if (!m_fileIO->CreatePath(directory.c_str()))
{
AZ_Error(s_traceName, false, "Failed to create destination directory '%s'", directory.c_str());
return false;
}
}
return true;
}
bool ArchiveComponent::CheckParamsForCreate(const AZStd::string& archive, const AZStd::string& directory)
{
if (!m_fileIO || !m_archive)
{
return false;
}
if (m_fileIO->Exists(archive.c_str()))
{
AZ_Error(s_traceName, false, "Archive file '%s' already exists, cannot create a new archive there!");
return false;
}
if (!m_fileIO->IsDirectory(directory.c_str()))
{
AZ_Error(s_traceName, false, "Directory '%s' is not a directory or doesn't exist!", directory.c_str());
return false;
}
return true;
}
} // namespace AzToolsFramework