carb/extras/Path.h

File members: carb/extras/Path.h

// Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved.
//
// NVIDIA CORPORATION and its licensors retain all intellectual property
// and proprietary rights in and to this software, related documentation
// and any modifications thereto. Any use, reproduction, disclosure or
// distribution of this software and related documentation without an express
// license agreement from NVIDIA CORPORATION is strictly prohibited.
//

#pragma once

#include "../Defines.h"

#include "../logging/Log.h"
#include "../../omni/String.h"

#include <functional>
#include <iostream>
#include <string>
#include <utility>
#include <vector>

namespace carb
{
namespace extras
{

// Forward declarations
class Path;
inline Path operator/(const Path& left, const Path& right);
inline Path operator+(const Path& left, const Path& right);
inline Path operator+(const Path& left, const char* right);
inline Path operator+(const Path& left, const std::string& right);
inline Path operator+(const Path& left, const omni::string& right);
inline Path operator+(const char* left, const Path& right);
inline Path operator+(const std::string& left, const Path& right);
inline Path operator+(const omni::string& left, const Path& right);

class Path
{
public:
    static constexpr size_t npos = std::string::npos;
    static_assert(npos == size_t(-1), "Invalid assumption");

    //--------------------------------------------------------------------------------------
    // Constructors/destructor and assignment operations

    Path() = default;

    Path(const char* path, size_t pathLen)
    {
        if (path && pathLen)
        {
            m_pathString.assign(path, pathLen);
            _sanitizePath();
        }
    }

    Path(const char* path)
    {
        if (path)
        {
            m_pathString = path;
        }
        _sanitizePath();
    }

    Path(std::string path) : m_pathString(std::move(path))
    {
        _sanitizePath();
    }

    Path(const omni::string& path) : m_pathString(path.c_str(), path.length())
    {
        _sanitizePath();
    }

    Path(const Path& other) : m_pathString(other.m_pathString)
    {
    }

    Path(Path&& other) noexcept : m_pathString(std::move(other.m_pathString))
    {
    }

    Path& operator=(const Path& other)
    {
        m_pathString = other.m_pathString;
        return *this;
    }

    Path& operator=(Path&& other) noexcept
    {
        m_pathString = std::move(other.m_pathString);
        return *this;
    }

    ~Path() = default;

    //--------------------------------------------------------------------------------------
    // Getting a string representation of the internal data

    std::string getString() const
    {
        return m_pathString;
    }

    operator std::string() const
    {
        return m_pathString;
    }

    const char* c_str() const noexcept
    {
        return m_pathString.c_str();
    }

    const char* getStringBuffer() const noexcept
    {
        return m_pathString.c_str();
    }

    CARB_NO_DOC(friend) // Sphinx 3.5.4 complains that this is an invalid C++ declaration, so ignore `friend` for docs.
    std::ostream& operator<<(std::ostream& os, const Path& path)
    {
        os << path.m_pathString;
        return os;
    }

    explicit operator const char*() const noexcept
    {
        return getStringBuffer();
    }

    //--------------------------------------------------------------------------------------
    // Path operations
    // Compare operations

    bool operator==(const Path& other) const noexcept
    {
        return m_pathString == other.m_pathString;
    }

    bool operator==(const std::string& other) const noexcept
    {
        return m_pathString == other;
    }

    bool operator==(const char* other) const noexcept
    {
        if (other == nullptr)
        {
            return false;
        }
        return m_pathString == other;
    }

    bool operator!=(const Path& other) const noexcept
    {
        return !(*this == other);
    }

    bool operator!=(const std::string& other) const noexcept
    {
        return !(*this == other);
    }

    bool operator!=(const char* other) const noexcept
    {
        return !(*this == other);
    }

    size_t getLength() const noexcept
    {
        return m_pathString.size();
    }

    size_t length() const noexcept
    {
        return m_pathString.size();
    }

    size_t size() const noexcept
    {
        return m_pathString.size();
    }

    Path& clear()
    {
        m_pathString.clear();
        return *this;
    }

    bool isEmpty() const noexcept
    {
        return m_pathString.empty();
    }

    bool empty() const noexcept
    {
        return m_pathString.empty();
    }

    Path getFilename() const
    {
        size_t offset = _getFilenameOffset();
        if (offset == npos)
            return {};

        return Path(Sanitized, m_pathString.substr(offset));
    }

    Path getExtension() const
    {
        size_t ext = _getExtensionOffset();
        if (ext == npos)
            return {};

        return Path(Sanitized, m_pathString.substr(ext));
    }

    Path getParent() const;

    Path getStem() const
    {
        size_t offset = _getFilenameOffset();
        if (offset == npos)
            return {};

        size_t ext = _getExtensionOffset();
        CARB_ASSERT(ext == npos || ext >= offset);
        // If ext is npos, `ext - offset` results in the full filename since there is no extension
        return Path(Sanitized, m_pathString.substr(offset, ext - offset));
    }

    Path getRootName() const
    {
        size_t rootNameEnd = _getRootNameEndOffset();
        if (rootNameEnd == npos)
        {
            return {};
        }

        return Path(Sanitized, m_pathString.substr(0, rootNameEnd));
    }

    Path getRelativePart() const
    {
        size_t relativePart = _getRelativePartOffset();
        if (relativePart == npos)
        {
            return {};
        }

        return Path(Sanitized, m_pathString.substr(relativePart));
    }

    Path getRootDirectory() const
    {
        constexpr static auto kForwardSlashChar_ = kForwardSlashChar; // CC-1110
        return hasRootDirectory() ? Path(Sanitized, std::string(&kForwardSlashChar_, 1)) : Path();
    }

    bool hasRootDirectory() const noexcept
    {
        size_t rootDirectoryEnd = _getRootDirectoryEndOffset();
        if (rootDirectoryEnd == npos)
        {
            return false;
        }

        size_t rootNameEnd = _getRootNameEndOffset();
        if (rootNameEnd == rootDirectoryEnd)
        {
            return false;
        }

        return true;
    }

    Path getRoot() const
    {
        size_t rootDirectoryEnd = _getRootDirectoryEndOffset();
        if (rootDirectoryEnd == npos)
        {
            return {};
        }

        return Path(Sanitized, m_pathString.substr(0, rootDirectoryEnd));
    }

    Path concat(const Path& toAppend) const
    {
        if (isEmpty())
        {
            return toAppend;
        }
        if (toAppend.isEmpty())
        {
            return *this;
        }

        std::string total;
        total.reserve(m_pathString.length() + toAppend.m_pathString.length());

        total.append(m_pathString);
        total.append(toAppend.m_pathString);

        return Path(Sanitized, std::move(total));
    }

    Path join(const Path& toJoin) const;

    Path& operator/=(const Path& path)
    {
        return *this = *this / path;
    }

    Path& operator+=(const Path& path)
    {
        return *this = *this + path;
    }

    Path& replaceExtension(const Path& newExtension);

    Path getAbsolute(const Path& root = Path()) const
    {
        if (isAbsolute() || root.isEmpty())
        {
            return this->getNormalized();
        }
        return root.join(*this).getNormalized();
    }

    Path getNormalized() const;

    Path& normalize()
    {
        return *this = getNormalized();
    }

    bool isAbsolute() const noexcept;

    bool isRelative() const
    {
        return !isAbsolute();
    }

    Path getRelative(const Path& base) const noexcept;

private:
    static constexpr char kDotChar = '.';
    static constexpr char kForwardSlashChar = '/';
    static constexpr char kColonChar = ':';

    struct Sanitized_t
    {
    };
    constexpr static Sanitized_t Sanitized{};

    Path(Sanitized_t, std::string s) : m_pathString(std::move(s))
    {
        // Pre-sanitized, so no need to do it again
    }

    enum class PathTokenType
    {
        Slash,
        RootName,
        Dot,
        DotDot,
        Name
    };

    // Helper function to parse next path token from `[bufferStart, bufferEnd)` (bufferEnd is an off-the-end pointer).
    // On success returns pointer immediately after the token data and returns token type in the
    // resultType. On failure returns nullptr and `resultType` is unchanged.
    // Note: it doesn't determine if a Name is a RootName. (RootName is added to enum for convenience)
    static const char* _getTokenEnd(const char* bufferBegin, const char* bufferEnd, PathTokenType& resultType)
    {
        if (bufferBegin == nullptr || bufferEnd == nullptr || bufferEnd <= bufferBegin)
        {
            return nullptr;
        }

        // Trying to find the next slash
        constexpr static auto kForwardSlashChar_ = kForwardSlashChar; // CC-1110
        const char* tokenEnd = std::find(bufferBegin, bufferEnd, kForwardSlashChar_);
        // If found a slash as the first symbol then return pointer to the data after it
        if (tokenEnd == bufferBegin)
        {
            resultType = PathTokenType::Slash;
            return tokenEnd + 1;
        }

        // If no slash found we consider all passed data as a single token
        if (!tokenEnd)
        {
            tokenEnd = bufferEnd;
        }

        const size_t tokenSize = tokenEnd - bufferBegin;
        if (tokenSize == 1 && *bufferBegin == kDotChar)
        {
            resultType = PathTokenType::Dot;
        }
        else if (tokenSize == 2 && bufferBegin[0] == kDotChar && bufferBegin[1] == kDotChar)
        {
            resultType = PathTokenType::DotDot;
        }
        else
        {
            resultType = PathTokenType::Name;
        }
        return tokenEnd;
    }

    size_t _getFilenameOffset() const noexcept
    {
        if (isEmpty())
        {
            return npos;
        }

        // Find the last slash
        size_t offset = m_pathString.rfind(kForwardSlashChar);
        if (offset == npos)
        {
            // Not empty, so no slash means only filename
            return 0;
        }

        // If the slash is the end, no filename
        if ((offset + 1) == m_pathString.length())
        {
            return npos;
        }

        return offset + 1;
    }

    size_t _getExtensionOffset() const noexcept
    {
        size_t filename = _getFilenameOffset();
        if (filename == npos)
        {
            return npos;
        }

        size_t dot = m_pathString.rfind(kDotChar);

        // No extension if:
        // - No dot in filename portion (dot not found or last dot proceeds filename)
        // - filename ends with a dot
        if (dot == npos || dot < filename || dot == (m_pathString.length() - 1))
        {
            return npos;
        }

        // If the only dot found starts the filename, we don't consider that an extension
        return dot == filename ? npos : dot;
    }

    size_t _getRootNameEndOffset() const noexcept
    {
        if (isEmpty())
        {
            return npos;
        }

        if (m_pathString.length() < 2)
        {
            return 0;
        }

#if CARB_PLATFORM_WINDOWS
        // Check if the path starts with a drive letter and colon (e.g. "C:/...")
        if (m_pathString.at(1) == kColonChar && std::isalpha(m_pathString.at(0)))
        {
            return 2;
        }
#endif

        // Check if the path is a UNC path (e.g. "//server/location/...")
        // This simple check is two slashes and not a slash
        if (m_pathString.length() >= 3 && m_pathString.at(0) == kForwardSlashChar &&
            m_pathString.at(1) == kForwardSlashChar && m_pathString.at(2) != kForwardSlashChar)
        {
            // Silence GCC 11 with C++20 warnings about string bounds (gcc bug 97185):
            // `__builtin_memchr` specified bound 18446744073709551612 exceeds maximum object size 9223372036854775807
            CARB_GNUC_ONLY(if (long(m_pathString.size()) < 0) __builtin_unreachable();)
            // Find the next slash
            size_t offset = m_pathString.find(kForwardSlashChar, 3);
            return offset == npos ? m_pathString.length() : offset;
        }

        return 0;
    }

    size_t _getRelativePartOffset(size_t rootNameEnd = npos) const noexcept
    {
        size_t offset = rootNameEnd != npos ? rootNameEnd : _getRootNameEndOffset();
        if (offset == npos)
            return npos;

        // Find the first non-slash character
        constexpr static auto kForwardSlashChar_ = kForwardSlashChar; // CC-1110
        return m_pathString.find_first_not_of(&kForwardSlashChar_, offset, 1);
    }

    size_t _getRootDirectoryEndOffset() const noexcept
    {
        size_t rootNameEnd = _getRootNameEndOffset();
        size_t relativePart = _getRelativePartOffset(rootNameEnd);

        if (relativePart != rootNameEnd)
        {
            if (rootNameEnd >= m_pathString.length())
            {
                return rootNameEnd;
            }
            return rootNameEnd + 1;
        }
        return rootNameEnd;
    }

    // Patching paths in the constructors (using external string data) if needed
    void _sanitizePath()
    {
#if CARB_PLATFORM_WINDOWS
        constexpr char kBackwardSlashChar = '\\';

        // changing the backward slashes for Windows to forward ones
        for (char& c : m_pathString)
        {
            if (c == kBackwardSlashChar)
            {
                c = kForwardSlashChar;
            }
        }
#endif
    }

    std::string m_pathString;
};

inline Path operator+(const Path& left, const Path& right)
{
    return left.concat(right);
}

inline Path operator+(const Path& left, const char* right)
{
    return left.concat(right);
}

inline Path operator+(const Path& left, const std::string& right)
{
    return left.concat(right);
}

inline Path operator+(const Path& left, const omni::string& right)
{
    return left.concat(right);
}

inline Path operator+(const char* left, const Path& right)
{
    return Path(left).concat(right);
}

inline Path operator+(const std::string& left, const Path& right)
{
    return Path(left).concat(right);
}

inline Path operator+(const omni::string& left, const Path& right)
{
    return Path(left).concat(right);
}

inline Path operator/(const Path& left, const Path& right)
{
    return left.join(right);
}

inline Path getPathParent(std::string path)
{
    return Path(std::move(path)).getParent();
}

inline Path getPathExtension(std::string path)
{
    return Path(std::move(path)).getExtension();
}

inline Path getPathStem(std::string path)
{
    return Path(std::move(path)).getStem();
}

inline Path getPathRelative(std::string path, std::string base)
{
    return Path(std::move(path)).getRelative(Path(std::move(base)));
}

inline bool operator==(const std::string& left, const Path& right)
{
    return right == left;
}

inline bool operator==(const char* left, const Path& right)
{
    return right == left;
}

inline bool operator!=(const std::string& left, const Path& right)
{
    return right != left;
}

inline bool operator!=(const char* left, const Path& right)
{
    return right != left;
}

// Implementations of large public functions

inline Path Path::getParent() const
{
    size_t parentPathEnd = _getFilenameOffset();
    if (parentPathEnd == npos)
        parentPathEnd = m_pathString.length();

    size_t slashesDataStart = 0;
    if (hasRootDirectory())
    {
        slashesDataStart += _getRootDirectoryEndOffset();
    }

    while (parentPathEnd > slashesDataStart && m_pathString.at(parentPathEnd - 1) == kForwardSlashChar)
    {
        --parentPathEnd;
    }

    if (parentPathEnd == 0)
    {
        return {};
    }

    return Path(Sanitized, m_pathString.substr(0, parentPathEnd));
}

inline Path Path::join(const Path& joinedPart) const
{
    if (isEmpty())
    {
        return joinedPart;
    }
    if (joinedPart.isEmpty())
    {
        return *this;
    }

    const bool needSeparator =
        (m_pathString.back() != kForwardSlashChar) && (joinedPart.m_pathString.front() != kForwardSlashChar);

    std::string joined;
    joined.reserve(m_pathString.length() + joinedPart.m_pathString.length() + (needSeparator ? 1 : 0));

    joined.assign(m_pathString);

    if (needSeparator)
        joined.push_back(kForwardSlashChar);

    joined.append(joinedPart.m_pathString);

    return Path(Sanitized, std::move(joined));
}

inline Path& Path::replaceExtension(const Path& newExtension)
{
    CARB_ASSERT(std::addressof(newExtension) != this);

    // Erase the current extension
    size_t ext = _getExtensionOffset();
    if (ext != npos)
    {
        m_pathString.erase(ext);
    }

    // If the new extension is empty, we're done
    if (newExtension.isEmpty())
    {
        return *this;
    }

    // Append a dot if there isn't one in the new extension
    if (newExtension.m_pathString.front() != kDotChar)
    {
        m_pathString.push_back(kDotChar);
    }

    // Append new extension
    m_pathString.append(newExtension.m_pathString);
    return *this;
}

inline Path Path::getNormalized() const
{
    if (isEmpty())
    {
        return {};
    }

    enum class NormalizePartType
    {
        Slash,
        RootName,
        RootSlash,
        Dot,
        DotDot,
        Name,
        Error
    };

    struct ParsedPathPartDescription
    {
        NormalizePartType type;
        const char* data;
        size_t size;

        ParsedPathPartDescription(const char* partData, size_t partSize, PathTokenType partType)
            : data(partData), size(partSize)
        {
            switch (partType)
            {
                case PathTokenType::Slash:
                    type = NormalizePartType::Slash;
                    break;
                case PathTokenType::RootName:
                    type = NormalizePartType::RootName;
                    break;
                case PathTokenType::Dot:
                    type = NormalizePartType::Dot;
                    break;
                case PathTokenType::DotDot:
                    type = NormalizePartType::DotDot;
                    break;
                case PathTokenType::Name:
                    type = NormalizePartType::Name;
                    break;

                default:
                    type = NormalizePartType::Error;
                    CARB_LOG_ERROR("Invalid internal token state while normalizing a path");
                    CARB_ASSERT(false);
                    break;
            }
        }

        ParsedPathPartDescription(const char* partData, size_t partSize, NormalizePartType partType)
            : type(partType), data(partData), size(partSize)
        {
        }
    };

    std::vector<ParsedPathPartDescription> resultPathTokens;

    size_t prevTokenEndOffset = _getRootDirectoryEndOffset();
    const char* prevTokenEnd = prevTokenEndOffset == npos ? nullptr : m_pathString.data() + prevTokenEndOffset;
    const char* pathDataStart = m_pathString.data();
    const size_t pathDataLength = getLength();
    if (prevTokenEnd && prevTokenEnd > pathDataStart)
    {
        // Adding the root name and the root directory as different elements
        const char* possibleSlashPos = prevTokenEnd - 1;
        if (*possibleSlashPos == kForwardSlashChar)
        {
            if (possibleSlashPos > pathDataStart)
            {
                resultPathTokens.emplace_back(
                    pathDataStart, static_cast<size_t>(possibleSlashPos - pathDataStart), PathTokenType::RootName);
            }
            constexpr static auto kForwardSlashChar_ = kForwardSlashChar; // CC-1110
            resultPathTokens.emplace_back(&kForwardSlashChar_, 1, NormalizePartType::RootSlash);
        }
        else
        {
            resultPathTokens.emplace_back(
                pathDataStart, static_cast<size_t>(prevTokenEnd - pathDataStart), PathTokenType::RootName);
        }
    }
    else
    {
        prevTokenEnd = pathDataStart;
    }

    bool alreadyNormalized = true;
    const char* bufferEnd = pathDataStart + pathDataLength;
    PathTokenType curTokenType = PathTokenType::Name;
    for (const char* curTokenEnd = _getTokenEnd(prevTokenEnd, bufferEnd, curTokenType); curTokenEnd != nullptr;
         prevTokenEnd = curTokenEnd, curTokenEnd = _getTokenEnd(prevTokenEnd, bufferEnd, curTokenType))
    {
        switch (curTokenType)
        {
            case PathTokenType::Slash:
                if (resultPathTokens.empty() || resultPathTokens.back().type == NormalizePartType::Slash ||
                    resultPathTokens.back().type == NormalizePartType::RootSlash)
                {
                    // Skip if we already have a slash at the end
                    alreadyNormalized = false;
                    continue;
                }
                break;

            case PathTokenType::Dot:
                // Just skip it
                alreadyNormalized = false;
                continue;

            case PathTokenType::DotDot:
                if (resultPathTokens.empty())
                {
                    break;
                }
                // Check if the previous element is a part of the root name (even without a slash) and skip dot-dot in
                // such case
                if (resultPathTokens.back().type == NormalizePartType::RootName ||
                    resultPathTokens.back().type == NormalizePartType::RootSlash)
                {
                    alreadyNormalized = false;
                    continue;
                }

                if (resultPathTokens.size() > 1)
                {
                    CARB_ASSERT(resultPathTokens.back().type == NormalizePartType::Slash);

                    const NormalizePartType tokenTypeBeforeSlash = resultPathTokens[resultPathTokens.size() - 2].type;

                    // Remove <name>/<dot-dot> pattern
                    if (tokenTypeBeforeSlash == NormalizePartType::Name)
                    {
                        resultPathTokens.pop_back(); // remove the last slash
                        resultPathTokens.pop_back(); // remove the last named token
                        alreadyNormalized = false;
                        continue; // and we skip the addition of the dot-dot
                    }
                }

                break;

            case PathTokenType::Name:
                // No special processing needed
                break;

            default:
                CARB_LOG_ERROR("Invalid internal state while normalizing the path {%s}", getStringBuffer());
                CARB_ASSERT(false);
                alreadyNormalized = false;
                continue;
        }

        resultPathTokens.emplace_back(prevTokenEnd, static_cast<size_t>(curTokenEnd - prevTokenEnd), curTokenType);
    }

    if (resultPathTokens.empty())
    {
        constexpr static auto kDotChar_ = kDotChar; // CC-1110
        return Path(Sanitized, std::string(&kDotChar_, 1));
    }
    else if (resultPathTokens.back().type == NormalizePartType::Slash && resultPathTokens.size() > 1)
    {
        // Removing the trailing slash for special cases like "./" and "../"
        const size_t indexOfTokenBeforeSlash = resultPathTokens.size() - 2;
        const NormalizePartType typeOfTokenBeforeSlash = resultPathTokens[indexOfTokenBeforeSlash].type;

        if (typeOfTokenBeforeSlash == NormalizePartType::Dot || typeOfTokenBeforeSlash == NormalizePartType::DotDot)
        {
            resultPathTokens.pop_back();
            alreadyNormalized = false;
        }
    }

    if (alreadyNormalized)
    {
        return *this;
    }

    size_t totalSize = 0;
    for (auto& token : resultPathTokens)
        totalSize += token.size;

    std::string joined;
    joined.reserve(totalSize);

    for (auto& token : resultPathTokens)
        joined.append(token.data, token.size);

    return Path(Sanitized, std::move(joined));
}

inline bool Path::isAbsolute() const noexcept
{
#if CARB_POSIX
    return !isEmpty() && m_pathString[0] == kForwardSlashChar;
#elif CARB_PLATFORM_WINDOWS
    // Drive root (D:/abc) case. This is the only position where : is allowed on windows. Checking for separator is
    // important, because D:temp.txt is a relative path on windows.
    const char* pathDataStart = m_pathString.data();
    const size_t pathDataLength = getLength();
    if (pathDataLength > 2 && pathDataStart[1] == kColonChar && pathDataStart[2] == kForwardSlashChar)
        return true;
    // Drive letter (D:) case
    if (pathDataLength == 2 && pathDataStart[1] == kColonChar)
        return true;

    // extended drive letter path (ie: prefixed with "//./D:").
    if (pathDataLength > 4 && pathDataStart[0] == kForwardSlashChar && pathDataStart[1] == kForwardSlashChar &&
        pathDataStart[2] == kDotChar && pathDataStart[3] == kForwardSlashChar)
    {
        // at least a drive name was specified.
        if (pathDataLength > 6 && pathDataStart[5] == kColonChar)
        {
            // a drive plus an absolute path was specified (ie: "//./d:/abc") => succeed.
            if (pathDataStart[6] == kForwardSlashChar)
                return true;

            // a drive and relative path was specified (ie: "//./d:temp.txt") => fail.  We need to
            //   specifically fail here because this path would also get picked up by the generic
            //   special path check below and report success erroneously.
            else
                return false;
        }

        // requesting the full drive volume (ie: "//./d:") => report absolute to match behavior
        //   in the "d:" case above.
        if (pathDataLength == 6 && pathDataStart[5] == kColonChar)
            return true;
    }

    // check for special paths.  This includes all windows paths that begin with "\\" (converted
    // to Unix path separators for our purposes).  This class of paths includes extended path
    // names (ie: prefixed with "\\?\"), device names (ie: prefixed with "\\.\"), physical drive
    // paths (ie: prefixed with "\\.\PhysicalDrive<n>"), removable media access (ie: "\\.\X:")
    // COM ports (ie: "\\.\COM*"), and UNC paths (ie: prefixed with "\\servername\sharename\").
    //
    // Note that it is not necessarily sufficient to get absolute vs relative based solely on
    // the "//" prefix here without needing to dig further into the specific name used and what
    // it actually represents.  For now, we'll just assume that device, drive, volume, and
    // port names will not be used here and treat it as a UNC path.  Since all extended paths
    // and UNC paths must always be absolute, this should hold up at least for those.  If a
    // path for a drive, volume, or device is actually passed in here, it will still be treated
    // as though it were an absolute path.  The results of using such a path further may be
    // undefined however.
    if (pathDataLength > 2 && pathDataStart[0] == kForwardSlashChar && pathDataStart[1] == kForwardSlashChar &&
        pathDataStart[2] != kForwardSlashChar)
        return true;
    return false;
#else
    CARB_UNSUPPORTED_PLATFORM();
#endif
}

inline Path Path::getRelative(const Path& base) const noexcept
{
    // checking if the operation is possible
    if (isAbsolute() != base.isAbsolute() || (!hasRootDirectory() && base.hasRootDirectory()) ||
        getRootName() != base.getRootName())
    {
        return {};
    }

    PathTokenType curPathTokenType = PathTokenType::RootName;
    size_t curPathTokenEndOffset = _getRootDirectoryEndOffset();
    const char* curPathTokenEnd = curPathTokenEndOffset == npos ? nullptr : m_pathString.data() + curPathTokenEndOffset;
    const char* curPathTokenStart = curPathTokenEnd;
    const char* curPathEnd = m_pathString.data() + m_pathString.length();

    PathTokenType basePathTokenType = PathTokenType::RootName;
    size_t basePathTokenEndOffset = base._getRootDirectoryEndOffset();
    const char* basePathTokenEnd =
        basePathTokenEndOffset == npos ? nullptr : base.m_pathString.data() + basePathTokenEndOffset;
    const char* basePathEnd = base.m_pathString.data() + base.m_pathString.length();

    // finding the first mismatch
    for (;;)
    {
        curPathTokenStart = curPathTokenEnd;
        curPathTokenEnd = _getTokenEnd(curPathTokenEnd, curPathEnd, curPathTokenType);

        const char* baseTokenStart = basePathTokenEnd;
        basePathTokenEnd = _getTokenEnd(basePathTokenEnd, basePathEnd, basePathTokenType);

        if (!curPathTokenEnd || !basePathTokenEnd)
        {
            // Checking if both are null
            if (curPathTokenEnd == basePathTokenEnd)
            {
                constexpr static auto kDotChar_ = kDotChar; // CC-1110
                return Path(Sanitized, std::string(&kDotChar_, 1));
            }
            break;
        }

        if (curPathTokenType != basePathTokenType ||
            !std::equal(curPathTokenStart, curPathTokenEnd, baseTokenStart, basePathTokenEnd))
        {
            break;
        }
    }
    int requiredDotDotCount = 0;
    while (basePathTokenEnd)
    {
        if (basePathTokenType == PathTokenType::DotDot)
        {
            --requiredDotDotCount;
        }
        else if (basePathTokenType == PathTokenType::Name)
        {
            ++requiredDotDotCount;
        }

        basePathTokenEnd = _getTokenEnd(basePathTokenEnd, basePathEnd, basePathTokenType);
    }

    if (requiredDotDotCount < 0)
    {
        return {};
    }

    if (requiredDotDotCount == 0 && !curPathTokenEnd)
    {
        constexpr static auto kDotChar_ = kDotChar; // CC-1110
        return Path(Sanitized, std::string(&kDotChar_, 1));
    }

    const size_t leftoverCurPathSymbols = curPathTokenEnd != nullptr ? curPathEnd - curPathTokenStart : 0;
    constexpr static char kDotDotString[] = "..";
    constexpr static size_t kDotDotStrlen = CARB_COUNTOF(kDotDotString) - 1;
    const size_t requiredResultSize = ((1 /* '/' */) + (kDotDotStrlen)) * requiredDotDotCount + leftoverCurPathSymbols;

    Path result;
    result.m_pathString.reserve(requiredResultSize);

    if (requiredDotDotCount > 0)
    {
        result.m_pathString.append(kDotDotString, kDotDotStrlen);
        --requiredDotDotCount;

        for (int i = 0; i < requiredDotDotCount; ++i)
        {
            result.m_pathString.push_back(kForwardSlashChar);
            result.m_pathString.append(kDotDotString, kDotDotStrlen);
        }
    }

    bool needsSeparator = !result.m_pathString.empty();
    while (curPathTokenEnd)
    {
        if (curPathTokenType != PathTokenType::Slash)
        {
            if (CARB_LIKELY(needsSeparator))
            {
                result.m_pathString += kForwardSlashChar;
            }
            else
            {
                needsSeparator = true;
            }
            result.m_pathString.append(curPathTokenStart, curPathTokenEnd - curPathTokenStart);
        }

        curPathTokenStart = curPathTokenEnd;
        curPathTokenEnd = _getTokenEnd(curPathTokenEnd, curPathEnd, curPathTokenType);
    }

    return result;
}
} // namespace extras
} // namespace carb