Files
o3de/Code/Framework/AzFramework/AzFramework/IO/LocalFileIO.cpp
T
bosnichd 403e2ff1e3 Fix bug in LocalFileIO::ConvertToAliasBuffer when a resolved alias ends in a path separator. (#5136)
* Fix bug in LocalFileIO::ConvertToAliasBuffer when a resolved alias ends in a path separator, in which case we do not want to consume it when replacing it with the alias.

eg. If the @products@ alias resolves to "C:\" and we call ConvertToAliasBuffer with "C:\some_folder\some_file.txt", the current behaviour results in "@products@some_folder\some_file.txt", but it needs to be "@products@\some_folder\some_file.txt"

Signed-off-by: bosnichd <bosnichd@amazon.com>

* Update based on review feedback.

Signed-off-by: bosnichd <bosnichd@amazon.com>
2021-11-02 10:31:50 -06:00

891 lines
34 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 <AzFramework/IO/LocalFileIO.h>
#include <sys/stat.h>
#include <AzCore/IO/SystemFile.h>
#include <AzCore/IO/IOUtils.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/Casting/numeric_cast.h>
#include <AzCore/Casting/lossy_cast.h>
#include <AzCore/std/containers/fixed_unordered_set.h>
#include <AzCore/std/functional.h>
#include <AzCore/std/string/conversions.h>
#include <AzCore/std/string/string_view.h>
#include <AzCore/StringFunc/StringFunc.h>
#include <AzCore/Utils/Utils.h>
#include <cctype>
namespace AZ
{
namespace IO
{
const HandleType LocalHandleStartValue = 1000000; //start the local file io handles at 1 million
LocalFileIO::LocalFileIO()
{
m_nextHandle = LocalHandleStartValue;
}
LocalFileIO::~LocalFileIO()
{
AZStd::lock_guard<AZStd::recursive_mutex> lock(m_openFileGuard);
while (!m_openFiles.empty())
{
const auto& handlePair = *m_openFiles.begin();
Close(handlePair.first);
}
AZ_Assert(m_openFiles.empty(), "Trying to shutdown filing system with files still open");
}
Result LocalFileIO::Open(const char* filePath, OpenMode mode, HandleType& fileHandle)
{
char resolvedPath[AZ_MAX_PATH_LEN];
ResolvePath(filePath, resolvedPath, AZ_MAX_PATH_LEN);
AZ::IO::UpdateOpenModeForReading(mode);
// Generate open modes for SystemFile
int systemFileMode = TranslateOpenModeToSystemFileMode(resolvedPath, mode);
bool write = AnyFlag(mode & (OpenMode::ModeWrite | OpenMode::ModeUpdate | OpenMode::ModeAppend));
if (write)
{
CheckInvalidWrite(resolvedPath);
}
{
AZStd::lock_guard<AZStd::recursive_mutex> lock(m_openFileGuard);
fileHandle = GetNextHandle();
// Construct a new SystemFile in the map (SystemFiles don't copy/move very well).
auto newPair = m_openFiles.emplace(fileHandle);
// Check for successful insert
if (!newPair.second)
{
fileHandle = InvalidHandle;
return ResultCode::Error;
}
// Attempt to open the newly created file
if (newPair.first->second.Open(resolvedPath, systemFileMode, 0))
{
return ResultCode::Success;
}
else
{
// Remove file, it's not actually open
m_openFiles.erase(fileHandle);
// On failure, ensure the fileHandle returned is invalid
// some code does not check return but handle value (equivalent to checking for nullptr FILE*)
fileHandle = InvalidHandle;
return ResultCode::Error;
}
}
}
Result LocalFileIO::Close(HandleType fileHandle)
{
auto filePointer = GetFilePointerFromHandle(fileHandle);
if (!filePointer)
{
return ResultCode::Error_HandleInvalid;
}
filePointer->Close();
{
AZStd::lock_guard<AZStd::recursive_mutex> lock(m_openFileGuard);
m_openFiles.erase(fileHandle);
}
return ResultCode::Success;
}
Result LocalFileIO::Read(HandleType fileHandle, void* buffer, AZ::u64 size, bool failOnFewerThanSizeBytesRead, AZ::u64* bytesRead)
{
auto filePointer = GetFilePointerFromHandle(fileHandle);
if (!filePointer)
{
return ResultCode::Error_HandleInvalid;
}
SystemFile::SizeType readResult = filePointer->Read(size, buffer);
if (bytesRead)
{
*bytesRead = aznumeric_cast<AZ::u64>(readResult);
}
if (static_cast<AZ::u64>(readResult) != size)
{
if (failOnFewerThanSizeBytesRead)
{
return ResultCode::Error;
}
// Reading less than desired is valid if ferror is not set
AZ_Assert(Eof(fileHandle), "End of file unexpectedly reached before all data was read");
}
return ResultCode::Success;
}
Result LocalFileIO::Write(HandleType fileHandle, const void* buffer, AZ::u64 size, AZ::u64* bytesWritten)
{
auto filePointer = GetFilePointerFromHandle(fileHandle);
if (!filePointer)
{
return ResultCode::Error_HandleInvalid;
}
SystemFile::SizeType writeResult = filePointer->Write(buffer, size);
if (bytesWritten)
{
*bytesWritten = writeResult;
}
if (static_cast<AZ::u64>(writeResult) != size)
{
return ResultCode::Error;
}
return ResultCode::Success;
}
Result LocalFileIO::Flush(HandleType fileHandle)
{
auto filePointer = GetFilePointerFromHandle(fileHandle);
if (!filePointer)
{
return ResultCode::Error_HandleInvalid;
}
filePointer->Flush();
return ResultCode::Success;
}
Result LocalFileIO::Tell(HandleType fileHandle, AZ::u64& offset)
{
auto filePointer = GetFilePointerFromHandle(fileHandle);
if (!filePointer)
{
return ResultCode::Error_HandleInvalid;
}
SystemFile::SizeType resultValue = filePointer->Tell();
if (resultValue == -1)
{
return ResultCode::Error;
}
offset = static_cast<AZ::u64>(resultValue);
return ResultCode::Success;
}
Result LocalFileIO::Seek(HandleType fileHandle, AZ::s64 offset, SeekType type)
{
auto filePointer = GetFilePointerFromHandle(fileHandle);
if (!filePointer)
{
return ResultCode::Error_HandleInvalid;
}
SystemFile::SeekMode mode = SystemFile::SF_SEEK_BEGIN;
if (type == SeekType::SeekFromCurrent)
{
mode = SystemFile::SF_SEEK_CURRENT;
}
else if (type == SeekType::SeekFromEnd)
{
mode = SystemFile::SF_SEEK_END;
}
filePointer->Seek(offset, mode);
return ResultCode::Success;
}
Result LocalFileIO::Size(HandleType fileHandle, AZ::u64& size)
{
auto filePointer = GetFilePointerFromHandle(fileHandle);
if (!filePointer)
{
return ResultCode::Error_HandleInvalid;
}
size = filePointer->Length();
return ResultCode::Success;
}
Result LocalFileIO::Size(const char* filePath, AZ::u64& size)
{
char resolvedPath[AZ_MAX_PATH_LEN];
ResolvePath(filePath, resolvedPath, AZ_MAX_PATH_LEN);
size = SystemFile::Length(resolvedPath);
if (!size)
{
return SystemFile::Exists(resolvedPath) ? ResultCode::Success : ResultCode::Error;
}
return ResultCode::Success;
}
bool LocalFileIO::IsReadOnly(const char* filePath)
{
char resolvedPath[AZ_MAX_PATH_LEN];
ResolvePath(filePath, resolvedPath, AZ_MAX_PATH_LEN);
return !SystemFile::IsWritable(resolvedPath);
}
bool LocalFileIO::Eof(HandleType fileHandle)
{
auto filePointer = GetFilePointerFromHandle(fileHandle);
if (!filePointer)
{
return false;
}
return filePointer->Eof();
}
AZ::u64 LocalFileIO::ModificationTime(const char* filePath)
{
char resolvedPath[AZ_MAX_PATH_LEN];
ResolvePath(filePath, resolvedPath, AZ_MAX_PATH_LEN);
return SystemFile::ModificationTime(resolvedPath);
}
AZ::u64 LocalFileIO::ModificationTime(HandleType fileHandle)
{
auto filePointer = GetFilePointerFromHandle(fileHandle);
if (!filePointer)
{
return 0;
}
return filePointer->ModificationTime();
}
bool LocalFileIO::Exists(const char* filePath)
{
char resolvedPath[AZ_MAX_PATH_LEN];
ResolvePath(filePath, resolvedPath, AZ_MAX_PATH_LEN);
return SystemFile::Exists(resolvedPath);
}
bool LocalFileIO::IsDirectory(const char* filePath)
{
char resolvedPath[AZ_MAX_PATH_LEN];
ResolvePath(filePath, resolvedPath, AZ_MAX_PATH_LEN);
return SystemFile::IsDirectory(resolvedPath);
}
void LocalFileIO::CheckInvalidWrite([[maybe_unused]] const char* path)
{
#if defined(AZ_ENABLE_TRACING)
const char* assetAliasPath = GetAlias("@products@");
if (path && assetAliasPath)
{
const AZ::IO::PathView pathView(path);
if (pathView.IsRelativeTo(assetAliasPath))
{
AZ_Error("FileIO", false, "You may not alter data inside the asset cache. Please check the call stack and consider writing into the source asset folder instead.\n"
"Attempted write location: %s", path);
}
}
#endif
}
Result LocalFileIO::Remove(const char* filePath)
{
char resolvedPath[AZ_MAX_PATH_LEN];
ResolvePath(filePath, resolvedPath, AZ_MAX_PATH_LEN);
CheckInvalidWrite(resolvedPath);
if (IsDirectory(resolvedPath))
{
return ResultCode::Error;
}
return SystemFile::Delete(resolvedPath) ? ResultCode::Success : ResultCode::Error;
}
Result LocalFileIO::Rename(const char* originalFilePath, const char* newFilePath)
{
char resolvedOldPath[AZ_MAX_PATH_LEN];
char resolvedNewPath[AZ_MAX_PATH_LEN];
ResolvePath(originalFilePath, resolvedOldPath, AZ_MAX_PATH_LEN);
ResolvePath(newFilePath, resolvedNewPath, AZ_MAX_PATH_LEN);
CheckInvalidWrite(resolvedNewPath);
if (!SystemFile::Rename(resolvedOldPath, resolvedNewPath))
{
return ResultCode::Error;
}
return ResultCode::Success;
}
SystemFile* LocalFileIO::GetFilePointerFromHandle(HandleType fileHandle)
{
AZStd::lock_guard<AZStd::recursive_mutex> lock(m_openFileGuard);
auto openFileIterator = m_openFiles.find(fileHandle);
if (openFileIterator != m_openFiles.end())
{
SystemFile& file = openFileIterator->second;
return &file;
}
return nullptr;
}
HandleType LocalFileIO::GetNextHandle()
{
return m_nextHandle++;
}
static ResultCode DestroyPath_Recurse(LocalFileIO* fileIO, const char* filePath)
{
// this is a deltree command. It needs to eat everything. Even files.
ResultCode res = ResultCode::Success;
fileIO->FindFiles(filePath, "*", [&](const char* iterPath) -> bool
{
// depth first recurse into directories!
// note: findFiles returns full path names.
if (fileIO->IsDirectory(iterPath))
{
// recurse.
if (DestroyPath_Recurse(fileIO, iterPath) != ResultCode::Success)
{
res = ResultCode::Error;
return false; // stop the find files.
}
}
else
{
// if its a file, remove it
if (fileIO->Remove(iterPath) != ResultCode::Success)
{
res = ResultCode::Error;
return false; // stop the find files.
}
}
return true; // continue the find files
});
if (res != ResultCode::Success)
{
return res;
}
// now that we've finished recursing, rmdir on the folder itself
return AZ::IO::SystemFile::DeleteDir(filePath) ? ResultCode::Success : ResultCode::Error;
}
Result LocalFileIO::DestroyPath(const char* filePath)
{
char resolvedPath[AZ_MAX_PATH_LEN];
ResolvePath(filePath, resolvedPath, AZ_MAX_PATH_LEN);
bool pathExists = Exists(resolvedPath);
if (!pathExists)
{
return ResultCode::Success;
}
if (pathExists && (!IsDirectory(resolvedPath)))
{
return ResultCode::Error;
}
return DestroyPath_Recurse(this, resolvedPath);
}
static void ToUnixSlashes(char* path, AZ::u64 size)
{
auto PrevAndCurrentCharIsPathSeparator = [](const char prev, const char next) -> bool
{
constexpr AZStd::string_view pathSeparator = "/";
const bool prevIsPathSeparator = pathSeparator.find_first_of(prev) != AZStd::string_view::npos;
const bool nextIsPathSeparator = pathSeparator.find_first_of(next) != AZStd::string_view::npos;
return prevIsPathSeparator && nextIsPathSeparator;
};
size_t copyOffset = 0;
for (size_t i = 0; i < size && path[i] != '\0'; i++)
{
if (path[i] == '\\')
{
path[i] = '/';
}
// Replace runs of path separators with one path separator
#if AZ_TRAIT_USE_WINDOWS_FILE_API
// Network file systems for Windows based APIs start with consecutive path separators
// so skip over the first character in this case
constexpr size_t duplicateSeparatorStartOffet = 1;
#else
constexpr size_t duplicateSeparatorStartOffet = 0;
#endif
if (i > duplicateSeparatorStartOffet)
{
if (PrevAndCurrentCharIsPathSeparator(path[i - 1], path[i]))
{
continue;
}
}
// If the copy offset is different than the iteration index of the path, then copy over it over
if (copyOffset != i)
{
path[copyOffset] = path[i];
}
++copyOffset;
}
// Null-terminate the path again in case duplicate slashes were collapsed
path[copyOffset] = '\0';
}
bool LocalFileIO::ResolvePath(const char* path, char* resolvedPath, AZ::u64 resolvedPathSize) const
{
if (resolvedPath == nullptr || resolvedPathSize == 0)
{
return false;
}
resolvedPath[0] = '\0';
if (path == nullptr)
{
return false;
}
if (AZ::IO::PathView(path).HasRootPath())
{
size_t pathLen = strlen(path);
if (pathLen + 1 < resolvedPathSize)
{
azstrncpy(resolvedPath, resolvedPathSize, path, pathLen + 1);
//see if the absolute path matches the resolved value of @products@, if it does lowercase the relative part
LowerIfBeginsWith(resolvedPath, resolvedPathSize, GetAlias("@products@"));
ToUnixSlashes(resolvedPath, resolvedPathSize);
return true;
}
else
{
return false;
}
}
constexpr AZStd::string_view productAssetAlias = "@products@";
// Add plus one for the path separator: <alias>/<path>
constexpr size_t MaxPathSizeWithProductAssetAlias = AZ::IO::MaxPathLength + productAssetAlias.size() + 1;
using RootedPathString = AZStd::fixed_string<MaxPathSizeWithProductAssetAlias>;
RootedPathString rootedPathBuffer;
const char* rootedPath = path;
// if the path does not begin with an alias, then it is assumed to begin with @products@
if (path[0] != '@')
{
if (GetAlias("@products@"))
{
if (const size_t requiredSize = productAssetAlias.size() + strlen(path) + 1;
requiredSize > rootedPathBuffer.capacity())
{
AZ_Error("FileIO", false, "Prepending the %.*s alias to the input path results in a path longer than the"
" AZ::IO::MaxPathLength + the alias size of %zu. The size of the potential failed path is %zu",
AZ_STRING_ARG(productAssetAlias), rootedPathBuffer.capacity(), requiredSize)
}
else
{
rootedPathBuffer = RootedPathString::format("%.*s/%s", AZ_STRING_ARG(productAssetAlias), path);
}
}
else
{
if (ConvertToAbsolutePath(path, rootedPathBuffer.data(), rootedPathBuffer.capacity()))
{
// Recalculate the internal string length
rootedPathBuffer.resize_no_construct(AZStd::char_traits<char>::length(rootedPathBuffer.data()));
}
}
rootedPath = rootedPathBuffer.c_str();
}
if (ResolveAliases(rootedPath, resolvedPath, resolvedPathSize))
{
ToUnixSlashes(resolvedPath, resolvedPathSize);
return true;
}
return false;
}
bool LocalFileIO::ResolvePath(AZ::IO::FixedMaxPath& resolvedPath, const AZ::IO::PathView& path) const
{
if (AZ::IO::FixedMaxPathString fixedPath{ path.Native() };
ResolvePath(fixedPath.c_str(), resolvedPath.Native().data(), resolvedPath.Native().capacity()))
{
// Update the null-terminator offset
resolvedPath.Native().resize_no_construct(AZStd::char_traits<char>::length(resolvedPath.Native().data()));
return true;
}
return false;
}
void LocalFileIO::SetAlias(const char* key, const char* path)
{
char fullPath[AZ_MAX_PATH_LEN];
ConvertToAbsolutePath(path, fullPath, AZ_MAX_PATH_LEN);
m_aliases[key] = fullPath;
}
const char* LocalFileIO::GetAlias(const char* key) const
{
if (const auto it = m_aliases.find(key); it != m_aliases.end())
{
return it->second.c_str();
}
else if (const auto deprecatedIt = m_deprecatedAliases.find(key);
deprecatedIt != m_deprecatedAliases.end())
{
AZ_Error("FileIO", false, R"(Alias "%s" is deprecated. Please use alias "%s" instead)",
key, deprecatedIt->second.c_str());
AZStd::string_view aliasValue = deprecatedIt->second;
// Contains the list of aliases resolved so far
// If max_size is hit, than an error is logged and nullptr is returned
using VisitedAliasSet = AZStd::fixed_unordered_set<AZStd::string_view, 8, 8>;
VisitedAliasSet visitedAliasSet;
while (aliasValue.starts_with("@"))
{
if (visitedAliasSet.contains(aliasValue))
{
AZ_Error("FileIO", false, "Cycle found with for alias %.*s when trying to resolve deprecated alias %s",
AZ_STRING_ARG(aliasValue), key);
return nullptr;
}
if(visitedAliasSet.size() == visitedAliasSet.max_size())
{
AZ_Error("FileIO", false, "Unable to resolve path to deprecated alias %s within %zu steps",
key, visitedAliasSet.max_size());
return nullptr;
}
// Add the current alias value to the visited set
visitedAliasSet.emplace(aliasValue);
// Check if the alias value corresponds to another alias
if (auto resolvedIter = m_aliases.find(aliasValue); resolvedIter != m_aliases.end())
{
aliasValue = resolvedIter->second;
}
else if (resolvedIter = m_deprecatedAliases.find(aliasValue);
resolvedIter != m_deprecatedAliases.end())
{
aliasValue = resolvedIter->second;
}
else
{
return nullptr;
}
}
return aliasValue.data();
}
return nullptr;
}
void LocalFileIO::ClearAlias(const char* key)
{
m_aliases.erase(key);
}
void LocalFileIO::SetDeprecatedAlias(AZStd::string_view oldAlias, AZStd::string_view newAlias)
{
m_deprecatedAliases[oldAlias] = newAlias;
}
AZStd::optional<AZ::u64> LocalFileIO::ConvertToAliasBuffer(char* outBuffer, AZ::u64 outBufferLength, AZStd::string_view inBuffer) const
{
size_t longestMatch = 0;
size_t bufStringLength = inBuffer.size();
AZStd::string_view longestAlias;
AZStd::string_view longestResolvedAlias;
for (const auto& [alias, resolvedAlias] : m_aliases)
{
// here we are making sure that the buffer being passed in has enough space to include the alias in it.
// we are trying to find the LONGEST match, meaning of the following two examples, the second should 'win'
// File: g:/O3DE/dev/files/morefiles/blah.xml
// Alias1 links to 'g:/O3DE/dev/'
// Alias2 links to 'g:/O3DE/dev/files/morefiles'
// so returning Alias2 is preferred as it is more specific, even though alias1 includes it.
// note that its not possible for this to be matched if the string is shorter than the length of the alias itself so we skip
// strings that are shorter than the alias's mapped path without checking.
if ((longestMatch == 0) || (resolvedAlias.size() > longestMatch) && (resolvedAlias.size() <= bufStringLength))
{
// Check if the input path is relative to the alias value
if (AZ::IO::PathView(inBuffer).IsRelativeTo(AZ::IO::PathView(resolvedAlias)))
{
longestMatch = resolvedAlias.size();
longestAlias = alias;
longestResolvedAlias = resolvedAlias;
}
}
}
if (!longestAlias.empty())
{
// rearrange the buffer to have
// [alias][old path]
size_t aliasSize = longestAlias.size();
// If the resolved alias ends in a path separator, do not consume it.
const bool resolvedAliasEndsInPathSeparator = (longestResolvedAlias.ends_with(AZ::IO::PosixPathSeparator) ||
longestResolvedAlias.ends_with(AZ::IO::WindowsPathSeparator));
const size_t charsToAbsorb = resolvedAliasEndsInPathSeparator ? longestMatch - 1 : longestMatch;
size_t remainingData = bufStringLength - charsToAbsorb;
size_t finalStringSize = aliasSize + remainingData;
if (finalStringSize >= outBufferLength)
{
AZ_Error("FileIO", false, "Alias %.*s cannot fit in output buffer. The output buffer is too small (max len %lu, actual len %zu)",
aznumeric_cast<int>(longestAlias.size()), longestAlias.data(), outBufferLength, finalStringSize);
// avoid buffer overflow, return original untouched
return AZStd::nullopt;
}
// move the remaining piece of the string:
memmove(outBuffer + aliasSize, inBuffer.data() + charsToAbsorb, remainingData);
// copy the alias
memcpy(outBuffer, longestAlias.data(), aliasSize);
/// add a null
outBuffer[finalStringSize] = 0;
return finalStringSize;
}
// If the input and output buffer are different, copy over the input buffer to the output buffer
if (outBuffer != inBuffer.data())
{
if (inBuffer.size() >= outBufferLength)
{
AZ_Error("FileIO", false, R"(Path "%.*s" cannot fit in output buffer. The output buffer is too small (max len %llu, actual len %zu))",
aznumeric_cast<int>(inBuffer.size()), inBuffer.data(), outBufferLength, inBuffer.size());
return AZStd::nullopt;
}
size_t finalStringSize = inBuffer.copy(outBuffer, outBufferLength);
outBuffer[finalStringSize] = 0;
}
return bufStringLength;
}
AZStd::optional<AZ::u64> LocalFileIO::ConvertToAlias(char* inOutBuffer, AZ::u64 bufferLength) const
{
return ConvertToAliasBuffer(inOutBuffer, bufferLength, inOutBuffer);
}
bool LocalFileIO::ConvertToAlias(AZ::IO::FixedMaxPath& convertedPath, const AZ::IO::PathView& pathView) const
{
AZStd::optional<AZ::u64> convertedPathSize =
ConvertToAliasBuffer(convertedPath.Native().data(), convertedPath.Native().capacity(), pathView.Native());
if (convertedPathSize)
{
// Force update of AZStd::fixed_string m_size member to set correct null-terminator offset
convertedPath.Native().resize_no_construct(*convertedPathSize);
return true;
}
return false;
}
bool LocalFileIO::ResolveAliases(const char* path, char* resolvedPath, AZ::u64 resolvedPathSize) const
{
AZ_Assert(path && path[0] != '%', "%% is deprecated, @ is the only valid alias token");
AZStd::string_view pathView(path);
const auto found = AZStd::find_if(m_aliases.begin(), m_aliases.end(),
[pathView](const auto& alias)
{
return pathView.starts_with(alias.first);
});
using string_view_pair = AZStd::pair<AZStd::string_view, AZStd::string_view>;
auto [aliasKey, aliasValue] = (found != m_aliases.end()) ? string_view_pair(*found)
: string_view_pair{};
size_t requiredResolvedPathSize = pathView.size() - aliasKey.size() + aliasValue.size() + 1;
AZ_Assert(path != resolvedPath, "ResolveAliases does not support inplace update of the path");
AZ_Assert(resolvedPathSize >= requiredResolvedPathSize, "Resolved path size %llu not large enough. It needs to be %zu",
resolvedPathSize, requiredResolvedPathSize);
// we assert above, but we also need to properly handle the case when the resolvedPath buffer size
// is too small to copy the source into.
if (path == resolvedPath || (resolvedPathSize < requiredResolvedPathSize))
{
return false;
}
// Skip past the alias key in the pathView
// must ensure that we are replacing the entire folder name, not a partial (e.g. @GAME01@/ vs @GAME0@/)
if (AZStd::string_view postAliasView = pathView.substr(aliasKey.size());
!aliasKey.empty() && (postAliasView.empty() || postAliasView.starts_with('/') || postAliasView.starts_with('\\')))
{
// Copy over resolved alias path first
size_t resolvedPathLen = 0;
aliasValue.copy(resolvedPath, aliasValue.size());
resolvedPathLen += aliasValue.size();
// Append the post alias path next
postAliasView.copy(resolvedPath + resolvedPathLen, postAliasView.size());
resolvedPathLen += postAliasView.size();
// Null-Terminated the resolved path
resolvedPath[resolvedPathLen] = '\0';
// If the path started with one of the "asset cache" path aliases, lowercase the path
const char* projectPlatformCacheAliasPath = GetAlias("@products@");
const bool lowercasePath = projectPlatformCacheAliasPath != nullptr && AZ::StringFunc::StartsWith(resolvedPath, projectPlatformCacheAliasPath);
if (lowercasePath)
{
// Lowercase only the relative part after the replaced alias.
AZStd::to_lower(resolvedPath + aliasValue.size(), resolvedPath + resolvedPathLen);
}
// Replace any backslashes with posix slashes
AZStd::replace(resolvedPath, resolvedPath + resolvedPathLen, AZ::IO::WindowsPathSeparator, AZ::IO::PosixPathSeparator);
return true;
}
else
{
// The input path doesn't start with an available, copy it directly to the resolved path
pathView.copy(resolvedPath, pathView.size());
// Null-Terminated the resolved path
resolvedPath[pathView.size()] = '\0';
}
// warn on failing to resolve an alias
AZ_Warning("LocalFileIO::ResolveAlias", path && path[0] != '@', "Failed to resolve an alias: %s", path ? path : "(null)");
return false;
}
bool LocalFileIO::ReplaceAlias(AZ::IO::FixedMaxPath& replacedAliasPath, const AZ::IO::PathView& path) const
{
if (path.empty())
{
replacedAliasPath = path;
return true;
}
AZStd::string_view pathStrView = path.Native();
for (const auto& [aliasKey, aliasValue] : m_aliases)
{
if (AZ::StringFunc::StartsWith(pathStrView, aliasKey))
{
// Add to the size of result path by the resolved alias length - the alias key length
AZStd::string_view postAliasView = pathStrView.substr(aliasKey.size());
size_t requiredFixedMaxPathSize = postAliasView.size();
requiredFixedMaxPathSize += aliasValue.size();
// The replaced alias path is greater than 1024 characters, return false
if (requiredFixedMaxPathSize > replacedAliasPath.Native().max_size())
{
return false;
}
replacedAliasPath.Native() = AZ::IO::FixedMaxPathString(AZStd::string_view{ aliasValue });
replacedAliasPath.Native() += postAliasView;
return true;
}
}
if (pathStrView.size() > replacedAliasPath.Native().max_size())
{
return false;
}
replacedAliasPath = path;
return true;
}
bool LocalFileIO::GetFilename(HandleType fileHandle, char* filename, AZ::u64 filenameSize) const
{
AZStd::lock_guard<AZStd::recursive_mutex> lock(m_openFileGuard);
auto fileIt = m_openFiles.find(fileHandle);
if (fileIt != m_openFiles.end())
{
azstrncpy(filename, filenameSize, fileIt->second.Name(), filenameSize);
return true;
}
return false;
}
bool LocalFileIO::LowerIfBeginsWith(char* inOutBuffer, AZ::u64 bufferLen, const char* alias) const
{
if (alias)
{
AZ::u64 aliasLen = azlossy_caster(strlen(alias));
if (azstrnicmp(inOutBuffer, alias, aliasLen) == 0)
{
for (AZ::u64 i = aliasLen; i < bufferLen && inOutBuffer[i] != '\0'; ++i)
{
inOutBuffer[i] = static_cast<char>(std::tolower(static_cast<int>(inOutBuffer[i])));
}
return true;
}
}
return false;
}
AZStd::string LocalFileIO::RemoveTrailingSlash(const AZStd::string& pathStr)
{
if (pathStr.empty() || (pathStr[pathStr.length() - 1] != '/' && pathStr[pathStr.length() - 1] != '\\'))
{
return pathStr;
}
return pathStr.substr(0, pathStr.length() - 1);
}
AZStd::string LocalFileIO::CheckForTrailingSlash(const AZStd::string& pathStr)
{
if (pathStr.empty() || pathStr[pathStr.length() - 1] == '/')
{
return pathStr;
}
if (pathStr[pathStr.length() - 1] == '\\')
{
return pathStr.substr(0, pathStr.length() - 1) + "/";
}
return pathStr + "/";
}
bool LocalFileIO::ConvertToAbsolutePath(const char* path, char* absolutePath, AZ::u64 maxLength) const
{
return AZ::Utils::ConvertToAbsolutePath(path, absolutePath, maxLength);
}
} // namespace IO
} // namespace AZ