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:
lumberyard-employee-dm
2021-09-10 10:02:17 -05:00
committed by GitHub
parent 0505f200e5
commit e638f27572
13 changed files with 1132 additions and 881 deletions
+20 -5
View File
@@ -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 }
)
);