diff --git a/Code/Framework/AzCore/AzCore/IO/Path/Path.h b/Code/Framework/AzCore/AzCore/IO/Path/Path.h index 36640e821d..c0c4b1c974 100644 --- a/Code/Framework/AzCore/AzCore/IO/Path/Path.h +++ b/Code/Framework/AzCore/AzCore/IO/Path/Path.h @@ -256,8 +256,22 @@ namespace AZ::IO template static constexpr void MakeRelativeTo(PathResultType& pathResult, const AZ::IO::PathView& path, const AZ::IO::PathView& base); - template - 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 diff --git a/Code/Framework/AzCore/AzCore/IO/Path/Path.inl b/Code/Framework/AzCore/AzCore/IO/Path/Path.inl index 6354324136..40cbf6f46b 100644 --- a/Code/Framework/AzCore/AzCore/IO/Path/Path.inl +++ b/Code/Framework/AzCore/AzCore/IO/Path/Path.inl @@ -8,10 +8,10 @@ #pragma once -#include #include #include -#include + +#include // extern instantiations of Path templates to prevent implicit instantiations namespace AZ::IO @@ -56,702 +56,6 @@ namespace AZ::IO const PathIterator& rhs); } -namespace AZ::IO::Internal -{ - constexpr bool IsSeparator(const char elem) - { - return elem == '/' || elem == '\\'; - } - template >> - 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(*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 - constexpr auto ConsumeRootName(InputIt entryBeginIter, InputIt entryEndIter, const char preferredSeparator) - -> AZStd::enable_if_t, 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 entryBeginIter; - } - const AZStd::string_view rootName{ path.substr(0, positionOfPathSeparator + 1) }; - return AZStd::next(entryBeginIter, rootName.size()); - #else - return entryBeginIter; - #endif - } - else - { - // Information for GetRootName has been gathered from Microsoft 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 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 - 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 \\.\$ - 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 - 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 - 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 >> - static constexpr bool IsAbsolute(InputIt first, EndIt last, const char preferredSeparator) - { - // 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 - const size_t pathSize = AZStd::distance(first, last); - 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 - const size_t pathSize = AZStd::distance(first, last); - 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 - ? strncmp(left.data(), right.data(), maxCharsToCompare) - : azstrnicmp(left.data(), right.data(), maxCharsToCompare); - return charCompareResult == 0 - ? static_cast(aznumeric_cast(left.size()) - aznumeric_cast(right.size())) - : charCompareResult; - } -} - -//! PathParser implementation -//! For internal use only -namespace AZ::IO::parser -{ - using parser_path_type = PathView; - using string_view_pair = AZStd::pair; - 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::BeforeBegin, - PS_InRootName = PathIterator::InRootName, - PS_InRootDir = PathIterator::InRootDir, - PS_InFilenames = PathIterator::InFilenames, - PS_AtEnd = PathIterator::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(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; - } - - 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 Filename 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; - } - - constexpr int DetermineLexicalElementCount(PathParser pathParser) - { - int count = 0; - for (; pathParser; ++pathParser) - { - auto pathElement = *pathParser; - if (pathElement == "..") - { - --count; - } - else if (pathElement != "." && pathElement != "") - { - ++count; - } - } - return count; - } -} //! PathView implementation namespace AZ::IO @@ -1183,7 +487,8 @@ namespace AZ::IO }; if (pathParser.InRootName() && pathParserBase.InRootName()) { - if (*pathParser != *pathParserBase) + if (int res = Internal::ComparePathSegment(*pathParser, *pathParserBase, pathParser.m_preferred_separator); + res != 0) { pathResult.m_path = AZStd::string_view{}; return; @@ -1213,7 +518,8 @@ namespace AZ::IO // Find the first mismatching element auto pathParser = parser::PathParser::CreateBegin(path.m_path, path.m_preferred_separator); auto pathParserBase = parser::PathParser::CreateBegin(base.m_path, base.m_preferred_separator); - while (pathParser && pathParserBase && pathParser.m_parser_state == pathParserBase.m_parser_state && *pathParser == *pathParserBase) + while (pathParser && pathParserBase && pathParser.m_parser_state == pathParserBase.m_parser_state && + Internal::ComparePathSegment(*pathParser, *pathParserBase, pathParser.m_preferred_separator) == 0) { ++pathParser; ++pathParserBase; @@ -1255,66 +561,92 @@ namespace AZ::IO } } - template - constexpr void PathView::LexicallyNormalInplace(PathResultType& pathResult, const AZ::IO::PathView& path) + constexpr auto PathView::AppendNormalPathParts(PathIterable& pathIterable, const AZ::IO::PathView& path) noexcept -> void { if (path.m_path.empty()) { - pathResult = path; return; } - using PartKindPair = AZStd::pair; - // Max number of path parts supported when normalizing a path - constexpr size_t MaxPathParts = 64; - AZStd::array pathParts{}; - size_t currentPartSize = 0; - - // Track the total size of the parts as we collect them. This allows the - // resulting path to reserve the correct amount of memory. - size_t newPathSize = 0; - auto AddPart = [&newPathSize, &pathParts, ¤tPartSize](parser::PathPartKind pathKind, AZStd::string_view parserPathPart) constexpr - { - newPathSize += parserPathPart.size(); - pathParts[currentPartSize++] = { parserPathPart, pathKind }; - }; - auto LastPartKind = [&pathParts, ¤tPartSize]() constexpr - { - if (currentPartSize == 0) - { - return parser::PathPartKind::PK_None; - } - return pathParts[currentPartSize - 1].second; - }; - // Build a stack containing the remaining elements of the path, popping off // elements which occur before a '..' entry. for (auto pathParser = parser::PathParser::CreateBegin(path.m_path, path.m_preferred_separator); pathParser; ++pathParser) { - parser::PathPartKind Kind = parser::ClassifyPathPart(pathParser); - switch (Kind) + switch (const parser::PathPartKind Kind = parser::ClassifyPathPart(pathParser); Kind) { case parser::PathPartKind::PK_RootName: - case parser::PathPartKind::PK_Filename: - [[fallthrough]]; + { + // Root Name normalization is a bit tricky. + // A path of C:/foo/C:bar = C:/foo/bar and a path of C:foo/C:bar = C:foo/bar + // A path of C:/foo/C: = C:/foo + // A path of C:/foo/C:/bar = C:/bar + // Also a path of C:/foo/C: = C:/foo, but C:/foo/C:/ = C:/ + // A path of C:foo/D:bar = D:bar + // The pathIterable only stores the Root Name at the front + if (const auto [firstPartView, firstPartKind] = !pathIterable.empty() ? pathIterable.front() : PathIterable::PartKindPair{}; + firstPartKind != parser::PathPartKind::PK_RootName || firstPartView != *pathParser) + { + // The root name has changed or this is the first time a root name has been seen, + // discard the accumulated path parts + pathIterable.clear(); + pathIterable.emplace_back(*pathParser, Kind); + } + break; + } case parser::PathPartKind::PK_RootSep: { - // Add all non-dot and non-dot-dot elements to the stack of elements. - AddPart(Kind, *pathParser); + // If a root directory has been found, discard the accumulated path parts so far + // but not before storing of the first path part in case it is a Root Name + const auto [firstPartView, firstPartKind] = !pathIterable.empty() ? pathIterable.front() : PathIterable::PartKindPair{}; + pathIterable.clear(); + + if (firstPartKind == parser::PathPartKind::PK_RootName) + { + pathIterable.emplace_back(firstPartView, firstPartKind); + } + pathIterable.emplace_back(*pathParser, Kind); + break; + } + case parser::PathPartKind::PK_Filename: + { + // Special Case: The "filename" starts with a root name + // i.e D:/foo/C:/baz + // ^ + // The result should be C:/baz + // In this case restart path parsing at this element into the same PathIterable + // using tail recursion + AZStd::string_view filenameView{ *pathParser }; + if (auto filenameParser = parser::PathParser::CreateBegin(filenameView, pathParser.m_preferred_separator); + filenameParser && parser::ClassifyPathPart(filenameParser) == parser::PathPartKind::PK_RootName) + { + AZ::IO::PathView fileNamePath{ AZStd::string_view{ filenameView.begin(), path.m_path.end() }, + pathParser.m_preferred_separator }; + AppendNormalPathParts(pathIterable, fileNamePath); + return; + } + else + { + // Normal Case: The "filename" does not start with a root name + // Add all non-dot and non-dot-dot elements to the stack of elements. + pathIterable.emplace_back(*pathParser, Kind); + } break; } case parser::PathPartKind::PK_DotDot: { // Only push a ".." element if there are no elements preceding the "..", // or if the preceding element is itself "..". - auto lastPartKind = LastPartKind(); - if (lastPartKind == parser::PathPartKind::PK_Filename) + + if (const auto lastPartKind = !pathIterable.empty() ? pathIterable.back().second : parser::PathPartKind::PK_None; + lastPartKind == parser::PathPartKind::PK_Filename) { - newPathSize -= pathParts[--currentPartSize].first.size(); + // Due to the previous path part being a filename, the and the ".." cancels each other + // So remove the filename from the normalized path + pathIterable.pop_back(); } else if (lastPartKind != parser::PathPartKind::PK_RootSep) { - AddPart(parser::PathPartKind::PK_DotDot, ".."); + pathIterable.emplace_back("..", parser::PathPartKind::PK_DotDot); } break; } @@ -1324,21 +656,13 @@ namespace AZ::IO AZ_Assert(false, "Path Parser is in an invalid state"); } } - //! If the path is empty, add a dot. - if (currentPartSize == 0) - { - pathResult.m_path = AZStd::string_view{ "." }; - return; - } - - pathResult = PathResultType(path.m_preferred_separator); - pathResult.m_path.reserve(currentPartSize + newPathSize); - for (size_t partIndex = 0; partIndex < currentPartSize; ++partIndex) - { - auto& pathPart = pathParts[partIndex]; - pathResult /= pathPart.first; - } + } + constexpr auto PathView::GetNormalPathParts(const AZ::IO::PathView& path) noexcept -> PathIterable + { + PathIterable pathIterable; + AppendNormalPathParts(pathIterable, path); + return pathIterable; } } @@ -1573,12 +897,22 @@ namespace AZ::IO // Check if the other path has a root name and // that the root name doesn't match the current path root name // The scenario where this would occur was if the current path object had a path of - // "C:"foo and the other path object had a path "F:bar". + // "C:foo" and the other path object had a path "F:bar". // As the root names are different the other path replaces current path in it's entirety auto postRootNameIter = Internal::ConsumeRootName(m_path.begin(), m_path.end(), m_preferred_separator); auto otherPostRootNameIter = Internal::ConsumeRootName(first, last, m_preferred_separator); AZStd::string_view rootNameView{ m_path.begin(), postRootNameIter }; - if (first != otherPostRootNameIter && !AZStd::equal(rootNameView.begin(), rootNameView.end(), first, otherPostRootNameIter)) + + // The RootName can only ever be two characters long which is ":" + auto ToLower = [](const char element) constexpr -> char + { + return element >= 'A' && element <= 'Z' ? (element - 'A') + 'a' : element; + }; + auto compareRootName = [ToLower = AZStd::move(ToLower), path_separator = m_preferred_separator](const char lhs, const char rhs) constexpr + { + return path_separator == PosixPathSeparator ? lhs == rhs : ToLower(lhs) == ToLower(rhs); + }; + if (first != otherPostRootNameIter && !AZStd::equal(rootNameView.begin(), rootNameView.end(), first, otherPostRootNameIter, compareRootName)) { m_path.assign(first, last); return *this; @@ -1708,15 +1042,26 @@ namespace AZ::IO // native format observers template - constexpr auto BasicPath::Native() const noexcept -> const string_type& + constexpr auto BasicPath::Native() const & noexcept -> const string_type& + { + return m_path; + } + template + constexpr auto BasicPath::Native() const && noexcept -> const string_type&& + { + return AZStd::move(m_path); + } + + template + constexpr auto BasicPath::Native() & noexcept -> string_type& { return m_path; } template - constexpr auto BasicPath::Native() noexcept -> string_type& + constexpr auto BasicPath::Native() && noexcept -> string_type&& { - return m_path; + return AZStd::move(m_path); } template @@ -1726,13 +1071,7 @@ namespace AZ::IO } template - constexpr BasicPath::operator string_type() const - { - return m_path; - } - - template - constexpr BasicPath::operator string_type&() noexcept + constexpr BasicPath::operator string_type() const { return m_path; } @@ -1887,15 +1226,19 @@ namespace AZ::IO template constexpr auto BasicPath::LexicallyNormal() const -> BasicPath { - BasicPath pathResult; - static_cast(*this).LexicallyNormalInplace(pathResult, *this); + BasicPath pathResult(m_preferred_separator); + PathView::PathIterable pathIterable = PathView::GetNormalPathParts(*this); + for ([[maybe_unused]] auto [pathPartView, pathPartKind] : pathIterable) + { + pathResult /= pathPartView; + } return pathResult; } template constexpr auto BasicPath::LexicallyRelative(const PathView& base) const -> BasicPath { - BasicPath pathResult; + BasicPath pathResult(m_preferred_separator); static_cast(*this).MakeRelativeTo(pathResult, *this, base); return pathResult; } @@ -1984,20 +1327,45 @@ namespace AZ::IO { [[nodiscard]] constexpr bool PathView::IsRelativeTo(const PathView& base) const { - auto relativePath = LexicallyRelative(base); - return !relativePath.empty() && !relativePath.Native().starts_with(".."); + // PathView::LexicallyRelative is not being used as it returns a FixedMaxPath + // which has a limitation that it requires the relative path to fit within + // an AZ::IO::MaxPathLength buffer + auto ComparePathPart = [pathSeparator = m_preferred_separator]( + const PathIterable::PartKindPair& left, const PathIterable::PartKindPair& right) -> bool + { + return Internal::ComparePathSegment(left.first, right.first, pathSeparator) == 0; + }; + + const PathIterable thisPathParts = GetNormalPathParts(*this); + const PathIterable basePathParts = GetNormalPathParts(base); + [[maybe_unused]] auto [thisPathIter, basePathIter] = AZStd::mismatch(thisPathParts.begin(), thisPathParts.end(), + basePathParts.begin(), basePathParts.end(), ComparePathPart); + // Check if the entire base path has been consumed. If not, *this path cannot be relative to it + if (basePathIter != basePathParts.end()) + { + return false; + } + + // If the base path isn't empty and has been fully consumed, then *this path is relative + // Also if the base path is empty, then any relative path is relative to an empty path('.') + return !basePathParts.empty() || !thisPathParts.IsAbsolute(); } constexpr FixedMaxPath PathView::LexicallyNormal() const { - FixedMaxPath pathResult; - LexicallyNormalInplace(pathResult, *this); + FixedMaxPath pathResult(m_preferred_separator); + PathIterable pathIterable = GetNormalPathParts(*this); + for ([[maybe_unused]] auto [pathPartView, pathPartKind] : pathIterable) + { + pathResult /= pathPartView; + } + return pathResult; } constexpr FixedMaxPath PathView::LexicallyRelative(const PathView& base) const { - FixedMaxPath pathResult; + FixedMaxPath pathResult(m_preferred_separator); MakeRelativeTo(pathResult, *this, base); return pathResult; } diff --git a/Code/Framework/AzCore/AzCore/IO/Path/PathIterable.inl b/Code/Framework/AzCore/AzCore/IO/Path/PathIterable.inl new file mode 100644 index 0000000000..a1faa29b31 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/IO/Path/PathIterable.inl @@ -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 +#include + +namespace AZ::IO +{ + struct PathView::PathIterable + { + inline static constexpr size_t MaxPathParts = 64; + using PartKindPair = AZStd::pair; + using PartKindArray = AZStd::array; + 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 + 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 + 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)... }; + 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; + } +} diff --git a/Code/Framework/AzCore/AzCore/IO/Path/PathParser.inl b/Code/Framework/AzCore/AzCore/IO/Path/PathParser.inl new file mode 100644 index 0000000000..3ab2c4376c --- /dev/null +++ b/Code/Framework/AzCore/AzCore/IO/Path/PathParser.inl @@ -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 +#include + +namespace AZ::IO::Internal +{ + constexpr bool IsSeparator(const char elem) + { + return elem == '/' || elem == '\\'; + } + template >> + 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(*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 + constexpr auto ConsumeRootName(InputIt entryBeginIter, InputIt entryEndIter, const char preferredSeparator) + -> AZStd::enable_if_t, 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 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 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 + 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 \\.\$ + 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 + 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 + 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 >> + 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 + 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(aznumeric_cast(left.size()) - aznumeric_cast(right.size())) + : charCompareResult; + } +} + +//! PathParser implementation +//! For internal use only +namespace AZ::IO::parser +{ + using parser_path_type = PathView; + using string_view_pair = AZStd::pair; + 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::BeforeBegin, + PS_InRootName = PathIterator::InRootName, + PS_InRootDir = PathIterator::InRootDir, + PS_InFilenames = PathIterator::InFilenames, + PS_AtEnd = PathIterator::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(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; + } +} diff --git a/Code/Framework/AzCore/AzCore/azcore_files.cmake b/Code/Framework/AzCore/AzCore/azcore_files.cmake index b2ef586053..a41107e539 100644 --- a/Code/Framework/AzCore/AzCore/azcore_files.cmake +++ b/Code/Framework/AzCore/AzCore/azcore_files.cmake @@ -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 diff --git a/Code/Framework/AzCore/Tests/IO/Path/PathTests.cpp b/Code/Framework/AzCore/Tests/IO/Path/PathTests.cpp index 2fca03bc59..dbdb4fee78 100644 --- a/Code/Framework/AzCore/Tests/IO/Path/PathTests.cpp +++ b/Code/Framework/AzCore/Tests/IO/Path/PathTests.cpp @@ -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 } ) ); diff --git a/Code/Framework/AzFramework/AzFramework/Archive/Archive.cpp b/Code/Framework/AzFramework/AzFramework/Archive/Archive.cpp index e125cc1df6..012d713cf5 100644 --- a/Code/Framework/AzFramework/AzFramework/Archive/Archive.cpp +++ b/Code/Framework/AzFramework/AzFramework/Archive/Archive.cpp @@ -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); diff --git a/Code/Framework/AzFramework/AzFramework/Archive/Archive.h b/Code/Framework/AzFramework/AzFramework/Archive/Archive.h index bd10387d17..ec964f7fa3 100644 --- a/Code/Framework/AzFramework/AzFramework/Archive/Archive.h +++ b/Code/Framework/AzFramework/AzFramework/Archive/Archive.h @@ -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) diff --git a/Code/Framework/AzFramework/AzFramework/Archive/IArchive.h b/Code/Framework/AzFramework/AzFramework/Archive/IArchive.h index 14a810e2cf..5ac417564a 100644 --- a/Code/Framework/AzFramework/AzFramework/Archive/IArchive.h +++ b/Code/Framework/AzFramework/AzFramework/Archive/IArchive.h @@ -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 diff --git a/Code/Framework/AzFramework/AzFramework/IO/LocalFileIO.cpp b/Code/Framework/AzFramework/AzFramework/IO/LocalFileIO.cpp index 55e4f06db0..50190ac7d5 100644 --- a/Code/Framework/AzFramework/AzFramework/IO/LocalFileIO.cpp +++ b/Code/Framework/AzFramework/AzFramework/IO/LocalFileIO.cpp @@ -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)); diff --git a/Code/Framework/AzFramework/Tests/ArchiveTests.cpp b/Code/Framework/AzFramework/Tests/ArchiveTests.cpp index 6a359e714c..f3764a5e96 100644 --- a/Code/Framework/AzFramework/Tests/ArchiveTests.cpp +++ b/Code/Framework/AzFramework/Tests/ArchiveTests.cpp @@ -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(); } diff --git a/Code/Legacy/CryCommon/Mocks/ICryPakMock.h b/Code/Legacy/CryCommon/Mocks/ICryPakMock.h index 94ffbf6f6d..ab3b3875af 100644 --- a/Code/Legacy/CryCommon/Mocks/ICryPakMock.h +++ b/Code/Legacy/CryCommon/Mocks/ICryPakMock.h @@ -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 (AZStd::string_view szPath, AZStd::string_view bindRoot, uint32_t nFlags, AZStd::intrusive_ptr 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)); diff --git a/Gems/Atom/Asset/Shader/Code/Source/Editor/CommonFiles/Preprocessor.cpp b/Gems/Atom/Asset/Shader/Code/Source/Editor/CommonFiles/Preprocessor.cpp index fe1fd99f4e..1a97032864 100644 --- a/Gems/Atom/Asset/Shader/Code/Source/Editor/CommonFiles/Preprocessor.cpp +++ b/Gems/Atom/Asset/Shader/Code/Source/Editor/CommonFiles/Preprocessor.cpp @@ -14,11 +14,11 @@ #include #include #include +#include #include #include #include -#include #include #include @@ -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& 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 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 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 /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