Fixed PathView MakeRelativeTo and Append functions path segment comparisons (#3628)
* Fixed PathView `MakeRelativeTo` and `Append` functions path segment comparisons when using the Windows path separator of '\' The PathSegment comparisons were case-sensitive in both those functions and now use `Internal::ComparePathSegments` function to perform the appropriate case comparison based on the path separator value of the Path class Reverted the LocalFileIO::CheckInvalidWrite function back to not lowercasing the assets alias and input path before invoking `PathView::IsRelativeTo` Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> * Simplified the LocalFileIO::ConvertToAliasBuffer logic Fix for the ArchiveTest `IResourceList_Add_AbsolutePath_RemovesAndReplacesWithAlias` and `TestArchiveViaFileIO` test Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> * Added a PathIterable structure stores a non-heap container of normalized path segments of an input path. Moved the PathParser logic to a PathParser.inl file Removed dependency of the PathView::IsRelativeTo logic on FixedMaxPath There is no longer a 1024 character limit when determining if a path is relative to a base Added a GetNormalPathParts and AppendNormalPathParts to function and removed LexicallyNormalInplace to share the logic for creating a normalized path between IsRelativeTo and LexicallyNormal Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> * Clang PathIterable.inl build fix Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> * Fixed Normalize and Relative Path functions initialize the result paths With the correct path separator for the paths being transformed Ported over the Custom Path Root Separator logic to the PathParser.inl Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> * Updated he Shader Preprocessor include path gather. It now uses AZ::IO::Path for the path operations and checks if the path exist before adding it to the list of include paths. Finally the set logic has been removed for a simpler find_if check to see if the include path already since in the project include paths This fixes the Asset Processing issues with shader includes due to the Path.inl changes Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> * Fixed tail recursion call to AppendNormalPathParts to supply a PathView with the same path separator as the parent call Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> * Adding reference qualifier overloads to the Path class Native function Removed the conversion operators from the Path class for converting to a string_type&/const string_type& Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
0505f200e5
commit
e638f27572
@@ -256,8 +256,22 @@ namespace AZ::IO
|
||||
|
||||
template <typename PathResultType>
|
||||
static constexpr void MakeRelativeTo(PathResultType& pathResult, const AZ::IO::PathView& path, const AZ::IO::PathView& base);
|
||||
template <typename PathResultType>
|
||||
static constexpr void LexicallyNormalInplace(PathResultType& pathResult, const AZ::IO::PathView& path);
|
||||
|
||||
struct PathIterable;
|
||||
//! Returns a structure that provides a view of the path parts which can be used for iteration
|
||||
//! Only the path parts that correspond to creating an normalized path is returned
|
||||
//! This function is useful for returning a "view" into a normalized path without the need
|
||||
//! to allocate memory for the heap
|
||||
static constexpr PathIterable GetNormalPathParts(const AZ::IO::PathView& path) noexcept;
|
||||
// joins the input path to the Path Iterable structure using similiar logic to Path::Append
|
||||
// If the input path is absolute it will replace the current PathIterable otherwise
|
||||
// the input path will be appended to the Path Iterable structure
|
||||
// For example a PathIterable with parts = ['C:', '/', 'foo']
|
||||
// If the path input = 'bar', then the new PathIterable parts = [C:', '/', 'foo', 'bar']
|
||||
// If the path input = 'C:/bar', then the new PathIterable parts = [C:', '/', 'bar']
|
||||
// If the path input = 'C:bar', then the new PathIterable parts = [C:', '/', 'foo', 'bar' ]
|
||||
// If the path input = 'D:bar', then the new PathIterable parts = [D:, 'bar' ]
|
||||
static constexpr void AppendNormalPathParts(PathIterable& pathIterableResult, const AZ::IO::PathView& path) noexcept;
|
||||
|
||||
constexpr int compare_string_view(AZStd::string_view other) const;
|
||||
constexpr AZStd::string_view root_name_view() const;
|
||||
@@ -442,14 +456,15 @@ namespace AZ::IO
|
||||
constexpr void swap(BasicPath& rhs) noexcept;
|
||||
|
||||
// native format observers
|
||||
constexpr const string_type& Native() const noexcept;
|
||||
constexpr const string_type& Native() const& noexcept;
|
||||
constexpr const string_type&& Native() const&& noexcept;
|
||||
constexpr const value_type* c_str() const noexcept;
|
||||
constexpr explicit operator string_type() const;
|
||||
|
||||
// Adds support for retrieving a modifiable copy of the underlying string
|
||||
// Any modifications to the string invalidates existing PathIterators
|
||||
constexpr string_type& Native() noexcept;
|
||||
constexpr explicit operator string_type&() noexcept;
|
||||
constexpr string_type& Native() & noexcept;
|
||||
constexpr string_type&& Native() && noexcept;
|
||||
|
||||
//! The string and wstring functions cannot be constexpr until AZStd::basic_string is made constexpr.
|
||||
//! This cannot occur until C++20 as operator new/delete cannot be used within constexpr functions
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AzCore/std/containers/array.h>
|
||||
#include <AzCore/IO/Path/PathParser.inl>
|
||||
|
||||
namespace AZ::IO
|
||||
{
|
||||
struct PathView::PathIterable
|
||||
{
|
||||
inline static constexpr size_t MaxPathParts = 64;
|
||||
using PartKindPair = AZStd::pair<AZStd::string_view, AZ::IO::parser::PathPartKind>;
|
||||
using PartKindArray = AZStd::array<PartKindPair, MaxPathParts>;
|
||||
constexpr PathIterable() = default;
|
||||
|
||||
[[nodiscard]] constexpr bool empty() const noexcept;
|
||||
constexpr auto size() const noexcept-> size_t;
|
||||
constexpr auto begin() noexcept-> PartKindArray::iterator;
|
||||
constexpr auto begin() const noexcept -> PartKindArray::const_iterator;
|
||||
constexpr auto cbegin() const noexcept -> PartKindArray::const_iterator;
|
||||
constexpr auto end() noexcept -> PartKindArray::iterator;
|
||||
constexpr auto end() const noexcept -> PartKindArray::const_iterator;
|
||||
constexpr auto cend() const noexcept -> PartKindArray::const_iterator;
|
||||
constexpr auto rbegin() noexcept -> PartKindArray::reverse_iterator;
|
||||
constexpr auto rbegin() const noexcept -> PartKindArray::const_reverse_iterator;
|
||||
constexpr auto crbegin() const noexcept -> PartKindArray::const_reverse_iterator;
|
||||
constexpr auto rend() noexcept -> PartKindArray::reverse_iterator;
|
||||
constexpr auto rend() const noexcept -> PartKindArray::const_reverse_iterator;
|
||||
constexpr auto crend() const noexcept -> PartKindArray::const_reverse_iterator;
|
||||
|
||||
[[nodiscard]] constexpr bool IsAbsolute() const noexcept;
|
||||
|
||||
private:
|
||||
template <typename... Args>
|
||||
constexpr PartKindPair& emplace_back(Args&&... args) noexcept;
|
||||
constexpr void pop_back() noexcept;
|
||||
constexpr const PartKindPair& back() const noexcept;
|
||||
constexpr PartKindPair& back() noexcept;
|
||||
|
||||
constexpr const PartKindPair& front() const noexcept;
|
||||
constexpr PartKindPair& front() noexcept;
|
||||
|
||||
constexpr void clear() noexcept;
|
||||
|
||||
friend constexpr auto PathView::GetNormalPathParts(const AZ::IO::PathView&) noexcept -> PathIterable;
|
||||
friend constexpr auto PathView::AppendNormalPathParts(PathIterable& pathIterable, const AZ::IO::PathView&) noexcept -> void;
|
||||
PartKindArray m_parts{};
|
||||
size_t m_size{};
|
||||
};
|
||||
|
||||
// public
|
||||
[[nodiscard]] constexpr auto PathView::PathIterable::empty() const noexcept -> bool
|
||||
{
|
||||
return m_size == 0;
|
||||
}
|
||||
constexpr auto PathView::PathIterable::size() const noexcept -> size_t
|
||||
{
|
||||
return m_size;
|
||||
}
|
||||
|
||||
constexpr auto PathView::PathIterable::begin() noexcept -> PartKindArray::iterator
|
||||
{
|
||||
return m_parts.begin();
|
||||
}
|
||||
constexpr auto PathView::PathIterable::begin() const noexcept -> PartKindArray::const_iterator
|
||||
{
|
||||
return m_parts.begin();
|
||||
}
|
||||
constexpr auto PathView::PathIterable::cbegin() const noexcept -> PartKindArray::const_iterator
|
||||
{
|
||||
return begin();
|
||||
}
|
||||
constexpr auto PathView::PathIterable::end() noexcept -> PartKindArray::iterator
|
||||
{
|
||||
return begin() + size();
|
||||
}
|
||||
constexpr auto PathView::PathIterable::end() const noexcept -> PartKindArray::const_iterator
|
||||
{
|
||||
return begin() + size();
|
||||
}
|
||||
constexpr auto PathView::PathIterable::cend() const noexcept -> PartKindArray::const_iterator
|
||||
{
|
||||
return end();
|
||||
}
|
||||
constexpr auto PathView::PathIterable::rbegin() noexcept -> PartKindArray::reverse_iterator
|
||||
{
|
||||
return PartKindArray::reverse_iterator(begin() + size());
|
||||
}
|
||||
constexpr auto PathView::PathIterable::rbegin() const noexcept -> PartKindArray::const_reverse_iterator
|
||||
{
|
||||
return PartKindArray::const_reverse_iterator(begin() + size());
|
||||
}
|
||||
constexpr auto PathView::PathIterable::crbegin() const noexcept -> PartKindArray::const_reverse_iterator
|
||||
{
|
||||
return rbegin();
|
||||
}
|
||||
constexpr auto PathView::PathIterable::rend() noexcept -> PartKindArray::reverse_iterator
|
||||
{
|
||||
return PartKindArray::reverse_iterator(begin());
|
||||
}
|
||||
constexpr auto PathView::PathIterable::rend() const noexcept -> PartKindArray::const_reverse_iterator
|
||||
{
|
||||
return PartKindArray::const_reverse_iterator(begin());
|
||||
}
|
||||
constexpr auto PathView::PathIterable::crend() const noexcept -> PartKindArray::const_reverse_iterator
|
||||
{
|
||||
return rend();
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr auto PathView::PathIterable::IsAbsolute() const noexcept -> bool
|
||||
{
|
||||
return !empty() && (front().second == parser::PathPartKind::PK_RootSep
|
||||
|| (size() > 1 && front().second == parser::PathPartKind::PK_RootName && m_parts[1].second == parser::PathPartKind::PK_RootSep));
|
||||
}
|
||||
|
||||
// private
|
||||
template <typename... Args>
|
||||
constexpr auto PathView::PathIterable::emplace_back(Args&&... args) noexcept -> PartKindPair&
|
||||
{
|
||||
AZ_Assert(m_size < MaxPathParts, "PathIterable cannot be made out of a path with more than %zu parts", MaxPathParts);
|
||||
m_parts[m_size++] = PartKindPair{ AZStd::forward<Args>(args)... };
|
||||
return back();
|
||||
}
|
||||
constexpr auto PathView::PathIterable::pop_back() noexcept -> void
|
||||
{
|
||||
AZ_Assert(m_size > 0, "Cannot pop_back() from a PathIterable with 0 parts");
|
||||
--m_size;
|
||||
}
|
||||
constexpr auto PathView::PathIterable::back() const noexcept -> const PartKindPair&
|
||||
{
|
||||
AZ_Assert(!empty(), "back() was invoked on PathIterable with 0 parts");
|
||||
return m_parts[m_size - 1];
|
||||
}
|
||||
constexpr auto PathView::PathIterable::back() noexcept -> PartKindPair&
|
||||
{
|
||||
AZ_Assert(!empty(), "back() was invoked on PathIterable with 0 parts");
|
||||
return m_parts[m_size - 1];
|
||||
}
|
||||
|
||||
constexpr auto PathView::PathIterable::front() const noexcept -> const PartKindPair&
|
||||
{
|
||||
AZ_Assert(!empty(), "front() was invoked on PathIterable with 0 parts");
|
||||
return m_parts[0];
|
||||
}
|
||||
constexpr auto PathView::PathIterable::front() noexcept -> PartKindPair&
|
||||
{
|
||||
AZ_Assert(!empty(), "front() was invoked on PathIterable with 0 parts");
|
||||
return m_parts[0];
|
||||
}
|
||||
|
||||
constexpr auto PathView::PathIterable::clear() noexcept -> void
|
||||
{
|
||||
m_size = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,706 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AzCore/AzCore_Traits_Platform.h>
|
||||
#include <AzCore/Casting/numeric_cast.h>
|
||||
|
||||
namespace AZ::IO::Internal
|
||||
{
|
||||
constexpr bool IsSeparator(const char elem)
|
||||
{
|
||||
return elem == '/' || elem == '\\';
|
||||
}
|
||||
template <typename InputIt, typename EndIt, typename = AZStd::enable_if_t<AZStd::Internal::is_input_iterator_v<InputIt>>>
|
||||
static constexpr bool HasDrivePrefix(InputIt first, EndIt last)
|
||||
{
|
||||
size_t prefixSize = AZStd::distance(first, last);
|
||||
if (prefixSize < 2 || *AZStd::next(first, 1) != ':')
|
||||
{
|
||||
// Drive prefix must be at least two characters and have a colon for the second character
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr size_t ValidDrivePrefixRange = 26;
|
||||
// Uppercase the drive letter by bitwise and'ing out the the 2^5 bit
|
||||
unsigned char driveLetter = static_cast<unsigned char>(*first);
|
||||
|
||||
driveLetter &= 0b1101'1111;
|
||||
// normalize the character value in the range of A-Z -> 0-25
|
||||
driveLetter -= 'A';
|
||||
return driveLetter < ValidDrivePrefixRange;
|
||||
}
|
||||
|
||||
static constexpr bool HasDrivePrefix(AZStd::string_view prefix)
|
||||
{
|
||||
return HasDrivePrefix(prefix.begin(), prefix.end());
|
||||
}
|
||||
|
||||
//! Returns an iterator past the end of the consumed root name
|
||||
//! Windows root names can have include drive letter within them
|
||||
template <typename InputIt>
|
||||
constexpr auto ConsumeRootName(InputIt entryBeginIter, InputIt entryEndIter, const char preferredSeparator)
|
||||
-> AZStd::enable_if_t<AZStd::Internal::is_forward_iterator_v<InputIt>, InputIt>
|
||||
{
|
||||
if (preferredSeparator == PosixPathSeparator)
|
||||
{
|
||||
// If the preferred separator is forward slash the parser is in posix path
|
||||
// parsing mode, which doesn't have a root name,
|
||||
// unless we're on a posix platform that uses a custom path root separator
|
||||
#if defined(AZ_TRAIT_CUSTOM_PATH_ROOT_SEPARATOR)
|
||||
const AZStd::string_view path{ entryBeginIter, entryEndIter };
|
||||
const auto positionOfPathSeparator = path.find(AZ_TRAIT_CUSTOM_PATH_ROOT_SEPARATOR);
|
||||
if (positionOfPathSeparator != AZStd::string_view::npos)
|
||||
{
|
||||
return AZStd::next(entryBeginIter, positionOfPathSeparator + 1);
|
||||
}
|
||||
#endif
|
||||
return entryBeginIter;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Information for GetRootName has been gathered from Microsoft <filesystem> header
|
||||
// Below are examples of paths and what there root-name will return
|
||||
// "/" - returns ""
|
||||
// "foo/" - returns ""
|
||||
// "C:DriveRelative" - returns "C:"
|
||||
// "C:\\DriveAbsolute" - returns "C:"
|
||||
// "C://DriveAbsolute" - returns "C:"
|
||||
// "\\server\share" - returns "\\server"
|
||||
// The following paths are based on the UNC specification to work with paths longer than the 260 character path limit
|
||||
// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#maximum-path-length-limitation
|
||||
// \\?\device - returns "\\?"
|
||||
// \??\device - returns "\??"
|
||||
// \\.\device - returns "\\."
|
||||
|
||||
|
||||
AZStd::string_view path{ entryBeginIter, entryEndIter };
|
||||
|
||||
if (path.size() < 2)
|
||||
{
|
||||
// A root name is either <drive letter><colon> or a network path
|
||||
// therefore it has a least two characters
|
||||
return entryBeginIter;
|
||||
}
|
||||
|
||||
if (HasDrivePrefix(path))
|
||||
{
|
||||
// If the path has a drive prefix, then it has a root name of <driver letter><colon>
|
||||
return AZStd::next(entryBeginIter, 2);
|
||||
}
|
||||
|
||||
if (!Internal::IsSeparator(path[0]))
|
||||
{
|
||||
// At this point all other root names start with a path separator
|
||||
return entryBeginIter;
|
||||
}
|
||||
|
||||
// Check if the path has the form of "\\?\, "\??\" or "\\.\"
|
||||
const bool pathInUncForm = path.size() >= 4 && Internal::IsSeparator(path[3])
|
||||
&& (path.size() == 4 || !Internal::IsSeparator(path[4]));
|
||||
if (pathInUncForm)
|
||||
{
|
||||
// \\?\<0 or more> or \\.\$<zero or more>
|
||||
const bool slashQuestionMark = Internal::IsSeparator(path[1]) && (path[2] == '?' || path[2] == '.');
|
||||
// \??\<0 or more>
|
||||
const bool questionMarkTwice = path[1] == '?' && path[2] == '?';
|
||||
if (slashQuestionMark || questionMarkTwice)
|
||||
{
|
||||
// Return the root value root slash - i.e "\\?"
|
||||
return AZStd::next(entryBeginIter, 3);
|
||||
}
|
||||
}
|
||||
|
||||
if (path.size() >= 3 && Internal::IsSeparator(path[1]) && !Internal::IsSeparator(path[2]))
|
||||
{
|
||||
// Find the next path separator for network paths that have the form of \\server\share
|
||||
constexpr AZStd::string_view PathSeparators = { "/\\" };
|
||||
size_t nextPathSeparatorOffset = path.find_first_of(PathSeparators, 3);
|
||||
return AZStd::next(entryBeginIter, nextPathSeparatorOffset != AZStd::string_view::npos ? nextPathSeparatorOffset : path.size());
|
||||
}
|
||||
|
||||
return entryBeginIter;
|
||||
}
|
||||
}
|
||||
|
||||
//! Returns an iterator past the end of the consumed path separator(s)
|
||||
template <typename InputIt>
|
||||
constexpr InputIt ConsumeSeparator(InputIt entryBeginIter, InputIt entryEndIter) noexcept
|
||||
{
|
||||
return AZStd::find_if_not(entryBeginIter, entryEndIter, [](const char elem) { return Internal::IsSeparator(elem); });
|
||||
}
|
||||
|
||||
//! Returns an iterator past the end of the consumed filename
|
||||
template <typename InputIt>
|
||||
constexpr InputIt ConsumeName(InputIt entryBeginIter, InputIt entryEndIter) noexcept
|
||||
{
|
||||
return AZStd::find_if(entryBeginIter, entryEndIter, [](const char elem) { return Internal::IsSeparator(elem); });
|
||||
}
|
||||
|
||||
//! Check if a path is absolute on a OS basis
|
||||
//! If the preferred separator is '/' just checks if the path starts with a '/
|
||||
//! Otherwise a check for a Windows absolute path occurs
|
||||
//! Windows absolute paths can include a RootName
|
||||
template <typename InputIt, typename EndIt, typename = AZStd::enable_if_t<AZStd::Internal::is_input_iterator_v<InputIt>>>
|
||||
static constexpr bool IsAbsolute(InputIt first, EndIt last, const char preferredSeparator)
|
||||
{
|
||||
size_t pathSize = AZStd::distance(first, last);
|
||||
|
||||
// If the preferred separator is a forward slash
|
||||
// than an absolute path is simply one that starts with a forward slash,
|
||||
// unless we're on a posix platform that uses a custom path root separator
|
||||
if (preferredSeparator == PosixPathSeparator)
|
||||
{
|
||||
#if defined(AZ_TRAIT_CUSTOM_PATH_ROOT_SEPARATOR)
|
||||
const AZStd::string_view path{ first, last };
|
||||
return path.find(AZ_TRAIT_CUSTOM_PATH_ROOT_SEPARATOR) != AZStd::string_view::npos;
|
||||
#else
|
||||
return pathSize > 0 && IsSeparator(*first);
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Internal::HasDrivePrefix(first, last))
|
||||
{
|
||||
// If a windows path ends starts with C:foo it is a root relative path
|
||||
// A path is absolute root absolute on windows if it starts with <drive_letter><colon><path_separator>
|
||||
return pathSize > 2 && Internal::IsSeparator(*AZStd::next(first, 2));
|
||||
}
|
||||
|
||||
return first != ConsumeRootName(first, last, preferredSeparator);
|
||||
}
|
||||
}
|
||||
static constexpr bool IsAbsolute(AZStd::string_view pathView, const char preferredSeparator)
|
||||
{
|
||||
// Uses the template preferred to branch on the absolute path check
|
||||
// logic
|
||||
return IsAbsolute(pathView.begin(), pathView.end(), preferredSeparator);
|
||||
}
|
||||
|
||||
// Compares path segments using either Posix or Windows path rules based on the path separator in use
|
||||
// Posix paths perform a case-sensitive comparison, while Windows paths perform a case-insensitive comparison
|
||||
static int ComparePathSegment(AZStd::string_view left, AZStd::string_view right, char pathSeparator)
|
||||
{
|
||||
const size_t maxCharsToCompare = (AZStd::min)(left.size(), right.size());
|
||||
|
||||
int charCompareResult = pathSeparator == PosixPathSeparator
|
||||
? maxCharsToCompare ? strncmp(left.data(), right.data(), maxCharsToCompare) : 0
|
||||
: maxCharsToCompare ? azstrnicmp(left.data(), right.data(), maxCharsToCompare) : 0;
|
||||
return charCompareResult == 0
|
||||
? static_cast<int>(aznumeric_cast<ptrdiff_t>(left.size()) - aznumeric_cast<ptrdiff_t>(right.size()))
|
||||
: charCompareResult;
|
||||
}
|
||||
}
|
||||
|
||||
//! PathParser implementation
|
||||
//! For internal use only
|
||||
namespace AZ::IO::parser
|
||||
{
|
||||
using parser_path_type = PathView;
|
||||
using string_view_pair = AZStd::pair<AZStd::string_view, AZStd::string_view>;
|
||||
using PosPtr = const typename parser_path_type::value_type*;
|
||||
|
||||
enum ParserState : uint8_t
|
||||
{
|
||||
// Zero is a special sentinel value used by default constructed iterators.
|
||||
PS_BeforeBegin = PathIterator<PathView>::BeforeBegin,
|
||||
PS_InRootName = PathIterator<PathView>::InRootName,
|
||||
PS_InRootDir = PathIterator<PathView>::InRootDir,
|
||||
PS_InFilenames = PathIterator<PathView>::InFilenames,
|
||||
PS_AtEnd = PathIterator<PathView>::AtEnd
|
||||
};
|
||||
|
||||
struct PathParser
|
||||
{
|
||||
AZStd::string_view m_path_view;
|
||||
AZStd::string_view m_path_raw_entry;
|
||||
ParserState m_parser_state{};
|
||||
const char m_preferred_separator{ AZ_TRAIT_OS_PATH_SEPARATOR };
|
||||
|
||||
constexpr PathParser(AZStd::string_view path, ParserState state, const char preferredSeparator) noexcept
|
||||
: m_path_view(path)
|
||||
, m_parser_state(state)
|
||||
, m_preferred_separator(preferredSeparator)
|
||||
{
|
||||
}
|
||||
|
||||
constexpr PathParser(AZStd::string_view path, AZStd::string_view entry, ParserState state, const char preferredSeparator) noexcept
|
||||
: m_path_view(path)
|
||||
, m_path_raw_entry(entry)
|
||||
, m_parser_state(static_cast<ParserState>(state))
|
||||
, m_preferred_separator(preferredSeparator)
|
||||
{
|
||||
}
|
||||
|
||||
constexpr static PathParser CreateBegin(AZStd::string_view path, const char preferredSeparator) noexcept
|
||||
{
|
||||
PathParser pathParser(path, PS_BeforeBegin, preferredSeparator);
|
||||
pathParser.Increment();
|
||||
return pathParser;
|
||||
}
|
||||
|
||||
constexpr static PathParser CreateEnd(AZStd::string_view path, const char preferredSeparator) noexcept
|
||||
{
|
||||
PathParser pathParser(path, PS_AtEnd, preferredSeparator);
|
||||
return pathParser;
|
||||
}
|
||||
|
||||
constexpr PosPtr Peek() const noexcept
|
||||
{
|
||||
auto tokenEnd = getNextTokenStartPos();
|
||||
auto End = m_path_view.end();
|
||||
return tokenEnd == End ? nullptr : tokenEnd;
|
||||
}
|
||||
|
||||
constexpr void Increment() noexcept
|
||||
{
|
||||
const PosPtr pathEnd = m_path_view.end();
|
||||
const PosPtr currentPathEntry = getNextTokenStartPos();
|
||||
if (currentPathEntry == pathEnd)
|
||||
{
|
||||
return MakeState(PS_AtEnd);
|
||||
}
|
||||
|
||||
switch (m_parser_state)
|
||||
{
|
||||
case PS_BeforeBegin:
|
||||
{
|
||||
/*
|
||||
* First the determine if the path contains only a root-name such as "C:" or is a filename such as "foo"
|
||||
* root-relative path(Windows only) - C:foo
|
||||
* root-absolute path - C:\foo
|
||||
* root-absolute path - /foo
|
||||
* relative path - foo
|
||||
*
|
||||
* Try to consume the root-name then the root directory to determine if path entry
|
||||
* being parsed is a root-name or filename
|
||||
* The State transitions from BeforeBegin are
|
||||
* "C:", "\\server\", "\\?\", "\??\", "\\.\" -> Root Name
|
||||
* "/", "\" -> Root Directory
|
||||
* "path/foo", "foo" -> Filename
|
||||
*/
|
||||
auto rootNameEnd = Internal::ConsumeRootName(currentPathEntry, pathEnd, m_preferred_separator);
|
||||
if (currentPathEntry != rootNameEnd)
|
||||
{
|
||||
// Transition to the Root Name state
|
||||
return MakeState(PS_InRootName, currentPathEntry, rootNameEnd);
|
||||
}
|
||||
[[fallthrough]];
|
||||
}
|
||||
case PS_InRootName:
|
||||
{
|
||||
auto rootDirEnd = Internal::ConsumeSeparator(currentPathEntry, pathEnd);
|
||||
if (currentPathEntry != rootDirEnd)
|
||||
{
|
||||
// Transition to Root Directory state
|
||||
return MakeState(PS_InRootDir, currentPathEntry, rootDirEnd);
|
||||
}
|
||||
[[fallthrough]];
|
||||
}
|
||||
case PS_InRootDir:
|
||||
{
|
||||
auto filenameEnd = Internal::ConsumeName(currentPathEntry, pathEnd);
|
||||
if (currentPathEntry != filenameEnd)
|
||||
{
|
||||
return MakeState(PS_InFilenames, currentPathEntry, filenameEnd);
|
||||
}
|
||||
[[fallthrough]];
|
||||
}
|
||||
case PS_InFilenames:
|
||||
{
|
||||
auto separatorEnd = Internal::ConsumeSeparator(currentPathEntry, pathEnd);
|
||||
if (separatorEnd != pathEnd)
|
||||
{
|
||||
// find the end of the current filename entry
|
||||
auto filenameEnd = Internal::ConsumeName(separatorEnd, pathEnd);
|
||||
return MakeState(PS_InFilenames, separatorEnd, filenameEnd);
|
||||
}
|
||||
// If after consuming the separator that path entry is at the end iterator
|
||||
// move the path state to AtEnd
|
||||
return MakeState(PS_AtEnd);
|
||||
}
|
||||
case PS_AtEnd:
|
||||
AZ_Assert(false, "Path Parser cannot be incremented when it is in the AtEnd state");
|
||||
}
|
||||
}
|
||||
|
||||
constexpr void Decrement() noexcept
|
||||
{
|
||||
auto pathStart = m_path_view.begin();
|
||||
auto currentPathEntry = getCurrentTokenStartPos();
|
||||
|
||||
if (currentPathEntry == pathStart)
|
||||
{
|
||||
// we're decrementing the begin
|
||||
return MakeState(PS_BeforeBegin);
|
||||
}
|
||||
switch (m_parser_state)
|
||||
{
|
||||
case PS_AtEnd:
|
||||
{
|
||||
/*
|
||||
* First the determine if the path contains only a root-name such as "C:" or is a filename such as "foo"
|
||||
* root-relative path(Windows only) - C:foo
|
||||
* root-absolute path - C:\foo
|
||||
* root-absolute path - /foo
|
||||
* relative path - foo
|
||||
* Try to consume the root-name then the root directory to determine if path entry
|
||||
* being parsed is a root-name or filename
|
||||
* The State transitions from AtEnd are
|
||||
* "/path/foo/", "foo/", "C:foo\", "C:\foo\" -> Trailing Separator
|
||||
* "/path/foo", "foo", "C:foo", "C:\foo" -> Filename
|
||||
* "/", "C:\" or "\\server\" -> Root Directory
|
||||
* "C:", "\\server", "\\?", "\??", "\\." -> Root Name
|
||||
*/
|
||||
auto rootNameEnd = Internal::ConsumeRootName(pathStart, currentPathEntry, m_preferred_separator);
|
||||
if (pathStart != rootNameEnd && currentPathEntry == rootNameEnd)
|
||||
{
|
||||
// Transition to the Root Name state
|
||||
return MakeState(PS_InRootName, pathStart, currentPathEntry);
|
||||
}
|
||||
|
||||
auto rootDirEnd = Internal::ConsumeSeparator(rootNameEnd, currentPathEntry);
|
||||
if (rootNameEnd != rootDirEnd && currentPathEntry == rootDirEnd)
|
||||
{
|
||||
// Transition to Root Directory state
|
||||
return MakeState(PS_InRootDir, rootNameEnd, currentPathEntry);
|
||||
}
|
||||
|
||||
auto filenameEnd = currentPathEntry;
|
||||
if (Internal::IsSeparator(*(filenameEnd - 1)))
|
||||
{
|
||||
// The last character a path separator that isn't root directory
|
||||
// consume all the preceding path separators
|
||||
filenameEnd = Internal::ConsumeSeparator(AZStd::make_reverse_iterator(filenameEnd),
|
||||
AZStd::make_reverse_iterator(rootDirEnd)).base();
|
||||
}
|
||||
|
||||
// The previous state will be Filename, so the beginning of the filename is searched found
|
||||
auto filenameBegin = Internal::ConsumeName(AZStd::make_reverse_iterator(filenameEnd),
|
||||
AZStd::make_reverse_iterator(rootDirEnd)).base();
|
||||
return MakeState(PS_InFilenames, filenameBegin, filenameEnd);
|
||||
}
|
||||
case PS_InFilenames:
|
||||
{
|
||||
/* The State transitions from Filename are
|
||||
* "/path/foo" -> Filename
|
||||
* ^
|
||||
* "C:\foo" -> Root Directory
|
||||
* ^
|
||||
* "C:foo" -> Root Name
|
||||
* ^
|
||||
* "foo" -> This case has been taken care of by the current path entry != path start check
|
||||
* ^
|
||||
*/
|
||||
auto rootNameEnd = Internal::ConsumeRootName(pathStart, currentPathEntry, m_preferred_separator);
|
||||
if (pathStart != rootNameEnd && currentPathEntry == rootNameEnd)
|
||||
{
|
||||
// Transition to the Root Name state
|
||||
return MakeState(PS_InRootName, pathStart, rootNameEnd);
|
||||
}
|
||||
|
||||
auto rootDirEnd = Internal::ConsumeSeparator(rootNameEnd, currentPathEntry);
|
||||
if (rootNameEnd != rootDirEnd && currentPathEntry == rootDirEnd)
|
||||
{
|
||||
// Transition to Root Directory state
|
||||
return MakeState(PS_InRootDir, rootNameEnd, rootDirEnd);
|
||||
}
|
||||
// The previous state will be Filename again, so first the end of that filename is found
|
||||
// proceeded by finding the beginning of that filename
|
||||
auto filenameEnd = Internal::ConsumeSeparator(AZStd::make_reverse_iterator(currentPathEntry),
|
||||
AZStd::make_reverse_iterator(rootDirEnd)).base();
|
||||
auto filenameBegin = Internal::ConsumeName(AZStd::make_reverse_iterator(filenameEnd),
|
||||
AZStd::make_reverse_iterator(rootDirEnd)).base();
|
||||
return MakeState(PS_InFilenames, filenameBegin, filenameEnd);
|
||||
}
|
||||
case PS_InRootDir:
|
||||
{
|
||||
/* The State transitions from Root Directory are
|
||||
* "C:\" "\\server\", "\\?\", "\??\", "\\.\" -> Root Name
|
||||
* ^ ^ ^ ^ ^
|
||||
* "/" -> This case has been taken care of by the current path entry != path start check
|
||||
* ^
|
||||
*/
|
||||
return MakeState(PS_InRootName, pathStart, currentPathEntry);
|
||||
}
|
||||
case PS_InRootName:
|
||||
// The only valid state transition from Root Name is BeforeBegin
|
||||
return MakeState(PS_BeforeBegin);
|
||||
case PS_BeforeBegin:
|
||||
AZ_Assert(false, "Path Parser cannot be decremented when it is in the BeforeBegin State");
|
||||
}
|
||||
}
|
||||
|
||||
//! Return a view of the current element in the path processor state
|
||||
constexpr AZStd::string_view operator*() const noexcept
|
||||
{
|
||||
switch (m_parser_state)
|
||||
{
|
||||
case PS_BeforeBegin:
|
||||
[[fallthrough]];
|
||||
case PS_AtEnd:
|
||||
[[fallthrough]];
|
||||
case PS_InRootDir:
|
||||
return m_preferred_separator == '/' ? "/" : "\\";
|
||||
case PS_InRootName:
|
||||
case PS_InFilenames:
|
||||
return m_path_raw_entry;
|
||||
default:
|
||||
AZ_Assert(false, "Path Parser is in an invalid state");
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
constexpr explicit operator bool() const noexcept
|
||||
{
|
||||
return m_parser_state != PS_BeforeBegin && m_parser_state != PS_AtEnd;
|
||||
}
|
||||
|
||||
constexpr PathParser& operator++() noexcept
|
||||
{
|
||||
Increment();
|
||||
return *this;
|
||||
}
|
||||
|
||||
constexpr PathParser& operator--() noexcept
|
||||
{
|
||||
Decrement();
|
||||
return *this;
|
||||
}
|
||||
|
||||
constexpr bool AtEnd() const noexcept
|
||||
{
|
||||
return m_parser_state == PS_AtEnd;
|
||||
}
|
||||
|
||||
constexpr bool InRootDir() const noexcept
|
||||
{
|
||||
return m_parser_state == PS_InRootDir;
|
||||
}
|
||||
|
||||
constexpr bool InRootName() const noexcept
|
||||
{
|
||||
return m_parser_state == PS_InRootName;
|
||||
}
|
||||
|
||||
constexpr bool InRootPath() const noexcept
|
||||
{
|
||||
return InRootName() || InRootDir();
|
||||
}
|
||||
|
||||
private:
|
||||
constexpr void MakeState(ParserState newState, typename AZStd::string_view::iterator start, typename AZStd::string_view::iterator end) noexcept
|
||||
{
|
||||
m_parser_state = newState;
|
||||
m_path_raw_entry = AZStd::string_view(start, end);
|
||||
}
|
||||
constexpr void MakeState(ParserState newState) noexcept
|
||||
{
|
||||
m_parser_state = newState;
|
||||
m_path_raw_entry = {};
|
||||
}
|
||||
|
||||
//! Return a pointer to the first character after the currently lexed element.
|
||||
constexpr typename AZStd::string_view::iterator getNextTokenStartPos() const noexcept
|
||||
{
|
||||
switch (m_parser_state)
|
||||
{
|
||||
case PS_BeforeBegin:
|
||||
return m_path_view.begin();
|
||||
case PS_InRootName:
|
||||
case PS_InRootDir:
|
||||
case PS_InFilenames:
|
||||
return m_path_raw_entry.end();
|
||||
case PS_AtEnd:
|
||||
return m_path_view.end();
|
||||
default:
|
||||
AZ_Assert(false, "Path Parser is in an invalid state");
|
||||
}
|
||||
return m_path_view.end();
|
||||
}
|
||||
|
||||
//! Return a pointer to the first character in the currently lexed element.
|
||||
constexpr typename AZStd::string_view::iterator getCurrentTokenStartPos() const noexcept
|
||||
{
|
||||
switch (m_parser_state)
|
||||
{
|
||||
case PS_BeforeBegin:
|
||||
case PS_InRootName:
|
||||
return m_path_view.begin();
|
||||
case PS_InRootDir:
|
||||
case PS_InFilenames:
|
||||
return m_path_raw_entry.begin();
|
||||
case PS_AtEnd:
|
||||
return m_path_view.end();
|
||||
default:
|
||||
AZ_Assert(false, "Path Parser is in an invalid state");
|
||||
}
|
||||
return m_path_view.end();
|
||||
}
|
||||
};
|
||||
|
||||
constexpr string_view_pair SeparateFilename(const AZStd::string_view& srcView)
|
||||
{
|
||||
if (srcView == "." || srcView == ".." || srcView.empty())
|
||||
{
|
||||
return string_view_pair{ srcView, "" };
|
||||
}
|
||||
auto pos = srcView.find_last_of('.');
|
||||
if (pos == AZStd::string_view::npos || pos == 0)
|
||||
{
|
||||
return string_view_pair{ srcView, AZStd::string_view{} };
|
||||
}
|
||||
return string_view_pair{ srcView.substr(0, pos), srcView.substr(pos) };
|
||||
}
|
||||
|
||||
|
||||
// path part consumption
|
||||
constexpr bool ConsumeRootName(PathParser* pathParser)
|
||||
{
|
||||
static_assert(PS_BeforeBegin == 1 && PS_InRootName == 2,
|
||||
"PathParser must be in state before begin or in the root name in order to consume the root name");
|
||||
while (pathParser->m_parser_state <= PS_InRootName)
|
||||
{
|
||||
++(*pathParser);
|
||||
}
|
||||
return pathParser->m_parser_state == PS_AtEnd;
|
||||
}
|
||||
constexpr bool ConsumeRootDir(PathParser* pathParser)
|
||||
{
|
||||
static_assert(PS_BeforeBegin == 1 && PS_InRootName == 2 && PS_InRootDir == 3,
|
||||
"PathParser must be in state before begin, in the root name or in the root directory in order to consume the root directory");
|
||||
while (pathParser->m_parser_state <= PS_InRootDir)
|
||||
{
|
||||
++(*pathParser);
|
||||
}
|
||||
return pathParser->m_parser_state == PS_AtEnd;
|
||||
}
|
||||
|
||||
// path.comparisons
|
||||
constexpr int CompareRootName(PathParser* lhsPathParser, PathParser* rhsPathParser)
|
||||
{
|
||||
if (!lhsPathParser->InRootName() && !rhsPathParser->InRootName())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto GetRootName = [](PathParser* pathParser) constexpr -> AZStd::string_view
|
||||
{
|
||||
return pathParser->InRootName() ? **pathParser : "";
|
||||
};
|
||||
int res = Internal::ComparePathSegment(GetRootName(lhsPathParser), GetRootName(rhsPathParser), lhsPathParser->m_preferred_separator);
|
||||
ConsumeRootName(lhsPathParser);
|
||||
ConsumeRootName(rhsPathParser);
|
||||
return res;
|
||||
}
|
||||
constexpr int CompareRootDir(PathParser* lhsPathParser, PathParser* rhsPathParser)
|
||||
{
|
||||
if (!lhsPathParser->InRootDir() && rhsPathParser->InRootDir())
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else if (lhsPathParser->InRootDir() && !rhsPathParser->InRootDir())
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
ConsumeRootDir(lhsPathParser);
|
||||
ConsumeRootDir(rhsPathParser);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
constexpr int CompareRelative(PathParser* lhsPathParserPtr, PathParser* rhsPathParserPtr)
|
||||
{
|
||||
auto& lhsPathParser = *lhsPathParserPtr;
|
||||
auto& rhsPathParser = *rhsPathParserPtr;
|
||||
|
||||
while (lhsPathParser && rhsPathParser)
|
||||
{
|
||||
if (int res = Internal::ComparePathSegment(*lhsPathParser, *rhsPathParser, lhsPathParser.m_preferred_separator);
|
||||
res != 0)
|
||||
{
|
||||
return res;
|
||||
}
|
||||
++lhsPathParser;
|
||||
++rhsPathParser;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
constexpr int CompareEndState(PathParser* lhsPathParser, PathParser* rhsPathParser)
|
||||
{
|
||||
if (lhsPathParser->AtEnd() && !rhsPathParser->AtEnd())
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else if (!lhsPathParser->AtEnd() && rhsPathParser->AtEnd())
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
constexpr int DetermineLexicalElementCount(PathParser pathParser)
|
||||
{
|
||||
int count = 0;
|
||||
for (; pathParser; ++pathParser)
|
||||
{
|
||||
auto pathElement = *pathParser;
|
||||
if (pathElement == "..")
|
||||
{
|
||||
--count;
|
||||
}
|
||||
else if (pathElement != "." && pathElement != "")
|
||||
{
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
enum class PathPartKind : uint8_t
|
||||
{
|
||||
PK_None,
|
||||
PK_RootName,
|
||||
PK_RootSep,
|
||||
PK_Filename,
|
||||
PK_Dot,
|
||||
PK_DotDot,
|
||||
};
|
||||
|
||||
constexpr PathPartKind ClassifyPathPart(const PathParser& parser)
|
||||
{
|
||||
// Check each parser state to determine the PathPartKind
|
||||
if (parser.m_parser_state == PS_InRootDir)
|
||||
{
|
||||
return PathPartKind::PK_RootSep;
|
||||
}
|
||||
if (parser.m_parser_state == PS_InRootName)
|
||||
{
|
||||
return PathPartKind::PK_RootName;
|
||||
}
|
||||
|
||||
// Fallback to checking parser pathEntry view value
|
||||
// to determine if the special "." or ".." values are being used
|
||||
AZStd::string_view pathPart = *parser;
|
||||
if (pathPart == ".")
|
||||
{
|
||||
return PathPartKind::PK_Dot;
|
||||
}
|
||||
if (pathPart == "..")
|
||||
{
|
||||
return PathPartKind::PK_DotDot;
|
||||
}
|
||||
|
||||
// Return PathPartKind of PK_ilename if the parser state doesn't match
|
||||
// the states of InRootDir or InRootName and the filename
|
||||
// isn't made up of the special directory values of "." and ".."
|
||||
return PathPartKind::PK_Filename;
|
||||
}
|
||||
}
|
||||
@@ -176,6 +176,8 @@ set(FILES
|
||||
IO/Path/Path.cpp
|
||||
IO/Path/Path.h
|
||||
IO/Path/Path.inl
|
||||
IO/Path/PathIterable.inl
|
||||
IO/Path/PathParser.inl
|
||||
IO/Path/Path_fwd.h
|
||||
IO/SystemFile.cpp
|
||||
IO/SystemFile.h
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace UnitTest
|
||||
}
|
||||
|
||||
|
||||
// filesystem::path::is_absolute test
|
||||
// PathView::IsAbsolute test
|
||||
TEST_F(PathFixture, IsAbsolute_ReturnsTrue)
|
||||
{
|
||||
using fixed_max_path = AZ::IO::FixedMaxPath;
|
||||
@@ -88,7 +88,7 @@ namespace UnitTest
|
||||
static_assert(IsAbsolute());
|
||||
}
|
||||
|
||||
// filesystem::path::is_relative test
|
||||
// PathView::isRelative test
|
||||
TEST_F(PathFixture, IsRelative_ReturnsTrue)
|
||||
{
|
||||
using fixed_max_path = AZ::IO::FixedMaxPath;
|
||||
@@ -573,7 +573,16 @@ namespace UnitTest
|
||||
PathLexicallyNormalParams{ '/', "foo/./bar/..", "foo" },
|
||||
PathLexicallyNormalParams{ '/', "foo/.///bar/../", "foo" },
|
||||
PathLexicallyNormalParams{ '/', R"(/foo\./bar\..\)", "/foo" },
|
||||
PathLexicallyNormalParams{ '\\', R"(C:/O3DE/dev/Cache\game/../pc)", R"(C:\O3DE\dev\Cache\pc)" }
|
||||
PathLexicallyNormalParams{ '/', R"(/..)", "/" },
|
||||
PathLexicallyNormalParams{ '\\', R"(C:/O3DE/dev/Cache\game/../pc)", R"(C:\O3DE\dev\Cache\pc)" },
|
||||
PathLexicallyNormalParams{ '\\', R"(C:/foo/C:bar)", R"(C:\foo\bar)" },
|
||||
PathLexicallyNormalParams{ '\\', R"(C:foo/C:bar)", R"(C:foo\bar)" },
|
||||
PathLexicallyNormalParams{ '\\', R"(C:/foo/C:/bar)", R"(C:\bar)" },
|
||||
PathLexicallyNormalParams{ '\\', R"(C:/foo/C:)", R"(C:\foo)" },
|
||||
PathLexicallyNormalParams{ '\\', R"(C:/foo/C:/)", R"(C:\)" },
|
||||
PathLexicallyNormalParams{ '\\', R"(C:/foo/D:bar)", R"(D:bar)" },
|
||||
PathLexicallyNormalParams{ '\\', R"(..)", R"(..)" },
|
||||
PathLexicallyNormalParams{ '\\', R"(foo/../../bar)", R"(..\bar)" }
|
||||
)
|
||||
);
|
||||
|
||||
@@ -641,7 +650,12 @@ namespace UnitTest
|
||||
PathViewLexicallyProximateParams{ '\\', "C:\\a\\b", "C:\\a\\d\\c", "..\\..\\b", false },
|
||||
PathViewLexicallyProximateParams{ '\\', "C:a\\b", "C:\\a\\b", "C:a\\b", false },
|
||||
PathViewLexicallyProximateParams{ '\\', "C:\\a\\b", "C:a\\b", "C:\\a\\b", false },
|
||||
PathViewLexicallyProximateParams{ '\\', "E:\\a\\b", "F:\\a\\b", "E:\\a\\b", false }
|
||||
PathViewLexicallyProximateParams{ '\\', "E:\\a\\b", "F:\\a\\b", "E:\\a\\b", false },
|
||||
PathViewLexicallyProximateParams{ '\\', "D:/o3de/proJECT/cache/asset.txt", "d:\\o3de\\Project\\Cache", "asset.txt", true },
|
||||
PathViewLexicallyProximateParams{ '\\', "D:/o3de/proJECT/cache/pc/..", "d:\\o3de\\Project\\Cache", "pc\\..", true },
|
||||
PathViewLexicallyProximateParams{ '\\', "D:/o3de/proJECT/cache/pc/asset.txt/..", "d:\\o3de\\Project\\Cache\\", "pc\\asset.txt\\..", true },
|
||||
PathViewLexicallyProximateParams{ '\\', "D:/o3de/proJECT/cache\\", "D:\\o3de\\Project\\Cache/", ".", true },
|
||||
PathViewLexicallyProximateParams{ '\\', "D:/o3de/proJECT/cache/../foo", "D:\\o3de\\Project\\Cache", "..\\foo", false }
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -1912,7 +1912,7 @@ namespace AZ::IO
|
||||
return m_pFileEntry->nFileDataOffset;
|
||||
}
|
||||
|
||||
bool Archive::MakeDir(AZStd::string_view szPathIn, [[maybe_unused]] bool bGamePathMapping)
|
||||
bool Archive::MakeDir(AZStd::string_view szPathIn)
|
||||
{
|
||||
AZ::IO::StackString pathStr{ szPathIn };
|
||||
// Determine if there is a period ('.') after the last slash to determine if the path contains a file.
|
||||
@@ -2327,7 +2327,9 @@ namespace AZ::IO
|
||||
// we only want to record ASSET access
|
||||
// assets are identified as things which start with no alias, or with the @assets@ alias
|
||||
auto assetPath = AZ::IO::FileIOBase::GetInstance()->ConvertToAlias(szFilename);
|
||||
if (assetPath && assetPath->Native().starts_with("@assets@"))
|
||||
if (assetPath && (assetPath->Native().starts_with("@assets@")
|
||||
|| assetPath->Native().starts_with("@root@")
|
||||
|| assetPath->Native().starts_with("@projectplatformcache@")))
|
||||
{
|
||||
IResourceList* pList = GetResourceList(m_eRecordFileOpenList);
|
||||
|
||||
|
||||
@@ -249,7 +249,7 @@ namespace AZ::IO
|
||||
IArchive::SignedFileSize GetFileSizeOnDisk(AZStd::string_view filename) override;
|
||||
|
||||
// creates a directory
|
||||
bool MakeDir(AZStd::string_view szPath, bool bGamePathMapping = false) override;
|
||||
bool MakeDir(AZStd::string_view szPath) override;
|
||||
|
||||
// compresses the raw data into raw data. The buffer for compressed data itself with the heap passed. Uses method 8 (deflate)
|
||||
// returns one of the Z_* errors (Z_OK upon success)
|
||||
|
||||
@@ -335,7 +335,7 @@ namespace AZ::IO
|
||||
virtual IArchive::SignedFileSize GetFileSizeOnDisk(AZStd::string_view filename) = 0;
|
||||
|
||||
// creates a directory
|
||||
virtual bool MakeDir(AZStd::string_view szPath, bool bGamePathMapping = false) = 0;
|
||||
virtual bool MakeDir(AZStd::string_view szPath) = 0;
|
||||
|
||||
// open the physical archive file - creates if it doesn't exist
|
||||
// returns NULL if it's invalid or can't open the file
|
||||
|
||||
@@ -287,11 +287,8 @@ namespace AZ
|
||||
const char* assetAliasPath = GetAlias("@assets@");
|
||||
if (path && assetAliasPath)
|
||||
{
|
||||
AZStd::string assetsAlias(assetAliasPath);
|
||||
AZStd::string pathString = path;
|
||||
AZStd::to_lower(assetsAlias.begin(), assetsAlias.end());
|
||||
AZStd::to_lower(pathString.begin(), pathString.end());
|
||||
if (AZ::IO::PathView(pathString.c_str()).IsRelativeTo(assetsAlias.c_str()))
|
||||
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);
|
||||
@@ -587,26 +584,11 @@ namespace AZ
|
||||
// strings that are shorter than the alias's mapped path without checking.
|
||||
if ((longestMatch == 0) || (resolvedAlias.size() > longestMatch) && (resolvedAlias.size() <= bufStringLength))
|
||||
{
|
||||
// custom strcmp that ignores slash directions
|
||||
constexpr AZStd::string_view pathSeparators{ "/\\" };
|
||||
bool allMatch = AZStd::equal(resolvedAlias.begin(), resolvedAlias.end(), inBuffer.begin(),
|
||||
[&pathSeparators](const char lhs, const char rhs)
|
||||
// Check if the input path is relative to the alias value
|
||||
if (AZ::IO::PathView(inBuffer).IsRelativeTo(AZ::IO::PathView(resolvedAlias)))
|
||||
{
|
||||
const bool lhsIsSeparator = pathSeparators.find_first_of(lhs) != AZStd::string_view::npos;
|
||||
const bool rhsIsSeparator = pathSeparators.find_first_of(lhs) != AZStd::string_view::npos;
|
||||
return (lhsIsSeparator && rhsIsSeparator) || tolower(lhs) == tolower(rhs);
|
||||
});
|
||||
|
||||
if (allMatch)
|
||||
{
|
||||
// Either the resolvedAlias path must match the path exactly or the path must have a path separator character
|
||||
// right after the resolved alias
|
||||
if (const size_t matchLen = resolvedAlias.size();
|
||||
matchLen == bufStringLength || (pathSeparators.find_first_of(inBuffer[matchLen]) != AZStd::string_view::npos))
|
||||
{
|
||||
longestMatch = matchLen;
|
||||
longestAlias = alias;
|
||||
}
|
||||
longestMatch = resolvedAlias.size();
|
||||
longestAlias = alias;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -712,6 +694,7 @@ namespace AZ
|
||||
const char* assetAliasPath = GetAlias("@assets@");
|
||||
const char* rootAliasPath = GetAlias("@root@");
|
||||
const char* projectPlatformCacheAliasPath = GetAlias("@projectplatformcache@");
|
||||
|
||||
const bool lowercasePath = (assetAliasPath != nullptr && AZ::StringFunc::StartsWith(resolvedPath, assetAliasPath)) ||
|
||||
(rootAliasPath != nullptr && AZ::StringFunc::StartsWith(resolvedPath, rootAliasPath)) ||
|
||||
(projectPlatformCacheAliasPath != nullptr && AZ::StringFunc::StartsWith(resolvedPath, projectPlatformCacheAliasPath));
|
||||
|
||||
@@ -608,6 +608,9 @@ namespace UnitTest
|
||||
archive->ClosePack(genericArchiveFileName);
|
||||
cpfio.Remove(genericArchiveFileName);
|
||||
|
||||
// create the asset alias directory
|
||||
cpfio.CreatePath("@assets@");
|
||||
|
||||
// create generic file
|
||||
|
||||
HandleType normalFileHandle;
|
||||
@@ -848,13 +851,17 @@ namespace UnitTest
|
||||
const char *assetsPath = ioBase->GetAlias("@assets@");
|
||||
ASSERT_NE(nullptr, assetsPath);
|
||||
|
||||
AZStd::string stringToAdd = AZStd::string::format("%s/textures/test.dds", assetsPath);
|
||||
auto stringToAdd = AZ::IO::Path(assetsPath) / "textures" / "test.dds";
|
||||
|
||||
reslist->Clear();
|
||||
reslist->Add(stringToAdd.c_str());
|
||||
reslist->Add(stringToAdd.Native());
|
||||
|
||||
// it normalizes the string, so the slashes flip and everything is lowercased.
|
||||
EXPECT_STREQ(reslist->GetFirst(), "@assets@/textures/test.dds");
|
||||
AZ::IO::FixedMaxPath resolvedAddedPath;
|
||||
AZ::IO::FixedMaxPath resolvedResourcePath;
|
||||
EXPECT_TRUE(ioBase->ReplaceAlias(resolvedAddedPath, "@assets@/textures/test.dds"));
|
||||
EXPECT_TRUE(ioBase->ReplaceAlias(resolvedResourcePath, reslist->GetFirst()));
|
||||
EXPECT_EQ(resolvedAddedPath, resolvedResourcePath);
|
||||
reslist->Clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ struct CryPakMock
|
||||
MOCK_METHOD2(IsFileExist, bool(AZStd::string_view sFilename, EFileSearchLocation));
|
||||
MOCK_METHOD1(IsFolder, bool(AZStd::string_view sPath));
|
||||
MOCK_METHOD1(GetFileSizeOnDisk, AZ::IO::IArchive::SignedFileSize(AZStd::string_view filename));
|
||||
MOCK_METHOD2(MakeDir, bool(AZStd::string_view szPath, bool bGamePathMapping));
|
||||
MOCK_METHOD1(MakeDir, bool(AZStd::string_view szPath));
|
||||
MOCK_METHOD4(OpenArchive, AZStd::intrusive_ptr<AZ::IO::INestedArchive> (AZStd::string_view szPath, AZStd::string_view bindRoot, uint32_t nFlags, AZStd::intrusive_ptr<AZ::IO::MemoryBlock> pData));
|
||||
MOCK_METHOD1(GetFileArchivePath, const char* (AZ::IO::HandleType f));
|
||||
MOCK_METHOD5(RawCompress, int(const void* pUncompressed, size_t* pDestSize, void* pCompressed, size_t nSrcSize, int nLevel));
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
#include <AzCore/std/string/regex.h>
|
||||
#include <AzCore/std/containers/vector.h>
|
||||
#include <AzCore/std/smart_ptr/unique_ptr.h>
|
||||
#include <AzCore/StringFunc/StringFunc.h>
|
||||
#include <AzCore/Casting/numeric_cast.h>
|
||||
#include <AzCore/IO/SystemFile.h>
|
||||
#include <AzCore/Utils/Utils.h>
|
||||
|
||||
#include <AzFramework/StringFunc/StringFunc.h>
|
||||
#include <AzFramework/API/ApplicationAPI.h>
|
||||
|
||||
#include <AzToolsFramework/API/EditorAssetSystemAPI.h>
|
||||
@@ -56,7 +56,7 @@ namespace AZ
|
||||
m_predefinedMacros.begin(), m_predefinedMacros.end(),
|
||||
[&](const AZStd::string& predefinedMacro) {
|
||||
// Haystack, needle, bCaseSensitive
|
||||
if (!AzFramework::StringFunc::StartsWith(predefinedMacro, macroName, true))
|
||||
if (!AZ::StringFunc::StartsWith(predefinedMacro, macroName, true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -117,11 +117,11 @@ namespace AZ
|
||||
char localBuffer[DefaultFprintfBufferSize];
|
||||
|
||||
va_list args;
|
||||
|
||||
|
||||
va_start(args, format);
|
||||
int count = azvsnprintf(localBuffer, DefaultFprintfBufferSize, format, args);
|
||||
va_end(args);
|
||||
|
||||
|
||||
char* result = localBuffer;
|
||||
|
||||
// @result will be bound to @biggerData in case @localBuffer is not big enough.
|
||||
@@ -134,7 +134,7 @@ namespace AZ
|
||||
count++; // vsnprintf returns a size that doesn't include the null character.
|
||||
biggerData.reset(new char[count]);
|
||||
result = &biggerData[0];
|
||||
|
||||
|
||||
// Remark: for MacOS & Linux it is important to call va_start again before
|
||||
// each call to azvsnprintf. Not required for Windows.
|
||||
va_start(args, format);
|
||||
@@ -149,7 +149,7 @@ namespace AZ
|
||||
// https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/vsnprintf-vsnprintf-vsnprintf-l-vsnwprintf-vsnwprintf-l?view=msvc-160
|
||||
// In particular: "If the number of characters to write is greater than count,
|
||||
// these functions return -1 indicating that output has been truncated."
|
||||
|
||||
|
||||
// There wasn't enough space in the local store.
|
||||
// Remark: for MacOS & Linux it is important to call va_start again before
|
||||
// each call to azvsnprintf. Not required for Windows.
|
||||
@@ -157,10 +157,10 @@ namespace AZ
|
||||
count = azvscprintf(format, args);
|
||||
count += 1; // vscprintf returns a size that doesn't include the null character.
|
||||
va_end(args);
|
||||
|
||||
|
||||
biggerData.reset(new char[count]);
|
||||
result = &biggerData[0];
|
||||
|
||||
|
||||
va_start(args, format);
|
||||
count = azvsnprintf(result, count, format, args);
|
||||
va_end(args);
|
||||
@@ -191,7 +191,7 @@ namespace AZ
|
||||
|
||||
// definitions for the linker
|
||||
AZStd::mutex McppBinder::s_mcppExclusiveProtection;
|
||||
McppBinder* McppBinder::s_currentInstance = nullptr;
|
||||
McppBinder* McppBinder::s_currentInstance = nullptr;
|
||||
|
||||
// McppBinder ends
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
@@ -204,7 +204,7 @@ namespace AZ
|
||||
|
||||
// create the argc/argv
|
||||
const char* processName = "builder";
|
||||
const char* inputPath = fullPath.c_str();
|
||||
const char* inputPath = fullPath.c_str();
|
||||
// let's create the equivalent of that expression but in dynamic form:
|
||||
//const char* argv[] = { processName, szInPath, "-C", "-+", "-D macro1"..., "-I path"..., NULL };
|
||||
AZStd::vector< const char* > argv;
|
||||
@@ -230,7 +230,7 @@ namespace AZ
|
||||
argv.push_back(nullptr); // usual argv terminator
|
||||
// output the command line:
|
||||
AZStd::string stringifiedCommandLine;
|
||||
AzFramework::StringFunc::Join(stringifiedCommandLine, argv.begin(), argv.end() - 1, " ");
|
||||
AZ::StringFunc::Join(stringifiedCommandLine, argv.begin(), argv.end() - 1, " ");
|
||||
AZ_TracePrintf("Preprocessor", "%s", stringifiedCommandLine.c_str());
|
||||
// when we don't specify an -o outfile, mcpp uses stdout.
|
||||
// the trick is that since we hijacked putc & puts, stdout will not be written.
|
||||
@@ -238,17 +238,12 @@ namespace AZ
|
||||
return result;
|
||||
}
|
||||
|
||||
static void VerifySameFolder(const AZStd::string& path1, const AZStd::string& path2)
|
||||
static void VerifySameFolder([[maybe_unused]] AZStd::string_view path1, [[maybe_unused]] AZStd::string_view path2)
|
||||
{
|
||||
AZStd::string folder1, folder2;
|
||||
AzFramework::StringFunc::Path::GetFolderPath(path1.c_str(), folder1);
|
||||
AzFramework::StringFunc::Path::GetFolderPath(path2.c_str(), folder2);
|
||||
AzFramework::StringFunc::Path::Normalize(folder1);
|
||||
AzFramework::StringFunc::Path::Normalize(folder2);
|
||||
AZ_Warning("Preprocessing",
|
||||
folder1 == folder2,
|
||||
"The preprocessed file %s is in a different folder than its origin %s. Watch for #include problems with relative paths.",
|
||||
path1.c_str(), path2.c_str()
|
||||
AZ::IO::PathView(path1).ParentPath().LexicallyNormal() == AZ::IO::PathView(path2).ParentPath().LexicallyNormal(),
|
||||
"The preprocessed file %.*s is in a different folder than its origin %.*s. Watch for #include problems with relative paths.",
|
||||
AZ_STRING_ARG(path1), AZ_STRING_ARG(path2)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -260,7 +255,7 @@ namespace AZ
|
||||
// containing file names, to match the ORIGINAL source, and not the actual source in use by azslc.
|
||||
// That gymnastic is better for error messages anyway, so instead of making the SRG layout builder more intelligent,
|
||||
// we'll fake the origin of the file, by setting the original source as a filename
|
||||
// note that it is not possible to build a file in a different folder and fake it to a file eslewhere because relative includes will fail.
|
||||
// note that it is not possible to build a file in a different folder and fake it to a file elsewhere because relative includes will fail.
|
||||
void MutateLineDirectivesFileOrigin(
|
||||
AZStd::string& sourceCode,
|
||||
AZStd::string newFileOrigin)
|
||||
@@ -272,11 +267,11 @@ namespace AZ
|
||||
// we will use that as the information of the source path to mutate.
|
||||
if (sourceCode.starts_with("#line"))
|
||||
{
|
||||
auto firstQuote = sourceCode.find('"');
|
||||
auto secondQuote = sourceCode.find('"', firstQuote + 1);
|
||||
auto firstQuote = sourceCode.find('"');
|
||||
auto secondQuote = firstQuote != AZStd::string::npos ? sourceCode.find('"', firstQuote + 1) : AZStd::string::npos;
|
||||
auto originalFile = sourceCode.substr(firstQuote + 1, secondQuote - firstQuote - 1); // start +1, count -1 because we don't want the quotes included.
|
||||
VerifySameFolder(originalFile, newFileOrigin);
|
||||
[[maybe_unused]] bool didReplace = AzFramework::StringFunc::Replace(sourceCode, originalFile.c_str(), newFileOrigin.c_str(), true /*case sensitive*/);
|
||||
[[maybe_unused]] bool didReplace = AZ::StringFunc::Replace(sourceCode, originalFile.c_str(), newFileOrigin.c_str(), true /*case sensitive*/);
|
||||
AZ_Assert(didReplace, "Failed to replace %s for %s in preprocessed source.", originalFile.c_str(), newFileOrigin.c_str());
|
||||
}
|
||||
else
|
||||
@@ -285,26 +280,6 @@ namespace AZ
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
template< typename Container1, typename Container2 >
|
||||
void TransferContent(Container1& destination, Container2&& source)
|
||||
{
|
||||
destination.insert(AZStd::end(destination),
|
||||
AZStd::make_move_iterator(AZStd::begin(source)),
|
||||
AZStd::make_move_iterator(AZStd::end(source)));
|
||||
}
|
||||
|
||||
void DeleteFromSet(const AZStd::string& string, AZStd::set<AZStd::string>& set)
|
||||
{
|
||||
auto iter = set.find(string);
|
||||
if (iter != set.end())
|
||||
{
|
||||
set.erase(iter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// populate options with scan folders and contents of parsing shader_global_build_options.json
|
||||
void InitializePreprocessorOptions(
|
||||
PreprocessorOptions& options, [[maybe_unused]] const char* builderName, const char* optionalIncludeFolder)
|
||||
@@ -315,44 +290,61 @@ namespace AZ
|
||||
bool success = true;
|
||||
AZStd::vector<AZStd::string> scanFoldersVector;
|
||||
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(success,
|
||||
&AzToolsFramework::AssetSystemRequestBus::Events::GetScanFolders,
|
||||
scanFoldersVector);
|
||||
&AzToolsFramework::AssetSystemRequestBus::Events::GetScanFolders,
|
||||
scanFoldersVector);
|
||||
AZ_Warning(builderName, success, "Preprocessor option: Could not acquire a list of scan folders from the database.");
|
||||
|
||||
// we transfer to a set, to order the folders, uniquify them, and ensure deterministic build behavior
|
||||
AZStd::set<AZStd::string> scanFoldersSet;
|
||||
// Add the project path to list of include paths
|
||||
AZ::IO::FixedMaxPathString projectPath = AZ::Utils::GetProjectPath();
|
||||
scanFoldersSet.emplace(projectPath.c_str(), projectPath.size());
|
||||
AZ::IO::FixedMaxPath projectPath = AZ::Utils::GetProjectPath();
|
||||
auto FindPath = [](AZ::IO::PathView searchPath)
|
||||
{
|
||||
return [searchPath](AZStd::string_view includePathView)
|
||||
{
|
||||
return searchPath == AZ::IO::PathView(includePathView);
|
||||
};
|
||||
};
|
||||
if (auto it = AZStd::find_if(options.m_projectIncludePaths.begin(), options.m_projectIncludePaths.end(), FindPath(projectPath));
|
||||
it == options.m_projectIncludePaths.end())
|
||||
{
|
||||
options.m_projectIncludePaths.emplace_back(projectPath.c_str(), projectPath.Native().size());
|
||||
}
|
||||
if (optionalIncludeFolder)
|
||||
{
|
||||
scanFoldersSet.emplace(optionalIncludeFolder, strnlen(optionalIncludeFolder, AZ::IO::MaxPathLength));
|
||||
if (auto it = AZStd::find_if(options.m_projectIncludePaths.begin(), options.m_projectIncludePaths.end(), FindPath(optionalIncludeFolder));
|
||||
it == options.m_projectIncludePaths.end())
|
||||
{
|
||||
if (AZ::IO::SystemFile::Exists(optionalIncludeFolder))
|
||||
{
|
||||
options.m_projectIncludePaths.emplace_back(AZStd::move(AZ::IO::Path(optionalIncludeFolder).LexicallyNormal().Native()));
|
||||
}
|
||||
}
|
||||
}
|
||||
// but while we transfer to the set, we're going to keep only folders where +/ShaderLib exists
|
||||
for (AZStd::string folder : scanFoldersVector)
|
||||
for (AZ::IO::Path shaderScanFolder : scanFoldersVector)
|
||||
{
|
||||
AzFramework::StringFunc::Path::Join(folder.c_str(), "ShaderLib", folder);
|
||||
if (AZ::IO::SystemFile::Exists(folder.c_str()))
|
||||
shaderScanFolder /= "ShaderLib";
|
||||
if (auto it = AZStd::find_if(options.m_projectIncludePaths.begin(), options.m_projectIncludePaths.end(), FindPath(shaderScanFolder));
|
||||
it == options.m_projectIncludePaths.end())
|
||||
{
|
||||
scanFoldersSet.emplace(std::move(folder));
|
||||
// the folders constructed this fashion constitute the base of automatic include search paths
|
||||
if (AZ::IO::SystemFile::Exists(shaderScanFolder.c_str()))
|
||||
{
|
||||
options.m_projectIncludePaths.emplace_back(AZStd::move(shaderScanFolder.LexicallyNormal().Native()));
|
||||
}
|
||||
}
|
||||
} // the folders constructed this fashion constitute the base of automatic include search paths
|
||||
|
||||
// get the engine root:
|
||||
AZ::IO::FixedMaxPath engineRoot = AZ::Utils::GetEnginePath();
|
||||
|
||||
// add optional additional options
|
||||
for (AZStd::string& path : options.m_projectIncludePaths)
|
||||
{
|
||||
path = (engineRoot / path).String();
|
||||
DeleteFromSet(path, scanFoldersSet); // no need to add a path two times.
|
||||
}
|
||||
// back-insert the default paths (after the config-read paths we just read)
|
||||
TransferContent(/*to:*/options.m_projectIncludePaths, /*from:*/scanFoldersSet);
|
||||
|
||||
// finally the <engineroot>/Gems fallback
|
||||
AZStd::string gemsFolder;
|
||||
AzFramework::StringFunc::Path::Join(engineRoot.c_str(), "Gems", gemsFolder);
|
||||
options.m_projectIncludePaths.push_back(gemsFolder);
|
||||
AZ::IO::Path engineGemsFolder(AZStd::string_view{ AZ::Utils::GetEnginePath() });
|
||||
engineGemsFolder /= "Gems";
|
||||
if (auto it = AZStd::find_if(options.m_projectIncludePaths.begin(), options.m_projectIncludePaths.end(), FindPath(engineGemsFolder));
|
||||
it == options.m_projectIncludePaths.end())
|
||||
{
|
||||
if (AZ::IO::SystemFile::Exists(engineGemsFolder.c_str()))
|
||||
{
|
||||
options.m_projectIncludePaths.emplace_back(AZStd::move(engineGemsFolder.Native()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ShaderBuilder
|
||||
|
||||
Reference in New Issue
Block a user