omni/extras/Version.h

File members: omni/extras/Version.h

// Copyright (c) 2019-2023, 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 "StringHelpers.h"
#include "../../carb/RString.h"

#include <string>

namespace omni
{
namespace extras
{

struct SemanticVersion
{
    int32_t major{ -1 };
    int32_t minor{ -1 };
    int32_t patch{ -1 };
    std::string prerelease;
    std::string build;

    SemanticVersion() = default;

    std::string toString(bool includeBuildMetadata = true) const
    {
        if (this->major < 0 && this->minor < 0 && this->patch < 0)
            return "";
        else
        {
            std::ostringstream ss;
            if (this->major >= 0)
                ss << this->major;
            if (this->minor >= 0)
                ss << "." << this->minor;
            if (this->patch >= 0)
                ss << "." << this->patch;
            if (!std::string(this->prerelease).empty())
                ss << "-" << this->prerelease;
            if (includeBuildMetadata && !std::string(this->build).empty())
                ss << "+" << this->build;
            return ss.str();
        }
    }

    void replaceNegativesWithZeros() noexcept
    {
        if (this->major < 0)
            this->major = 0;
        if (this->minor < 0)
            this->minor = 0;
        if (this->patch < 0)
            this->patch = 0;
    }

    bool isAllZeroes() const noexcept
    {
        return this->major == 0 && this->minor == 0 && this->patch == 0;
    }

    static SemanticVersion getDefault() noexcept
    {
        return {};
    }
};

inline bool isAnyVersion(const SemanticVersion& v)
{
    return v.major < 0 && v.minor < 0 && v.patch < 0;
}

inline int prereleaseCmp(const char* x, const char* y)
{
    // SemVer prerelease part comparison according to: https://semver.org/#spec-item-11

    const size_t xLen = strlen(x);
    const size_t yLen = strlen(y);

    //  both empty
    if (xLen == 0 && yLen == 0)
        return 0;

    // ("abc" < "") => -1
    if (yLen == 0)
        return -1;

    // ("" < "abc") => 1
    if (xLen == 0)
        return 1;

    const char* xPartFrom = x;
    const char* yPartFrom = y;
    while (1)
    {
        // Find next '.'. Chunks are: [xPartFrom, xPartTo] and [yPartFrom, yPartTo]
        const char* xPartTo = strchr(xPartFrom, '.');
        xPartTo = xPartTo ? xPartTo : x + xLen;
        const char* yPartTo = strchr(yPartFrom, '.');
        yPartTo = yPartTo ? yPartTo : y + yLen;
        const size_t xPartLen = xPartTo - xPartFrom;
        const size_t yPartLen = yPartTo - yPartFrom;

        // Try to convert to integer until next '.'.
        char* end;
        const long xNum = strtol(xPartFrom, &end, 10);
        const bool xIsNum = (end == xPartTo);
        const long yNum = strtol(yPartFrom, &end, 10);
        const bool yIsNum = (end == yPartTo);

        // "123" < "abc"
        if (xIsNum && !yIsNum)
            return -1;
        if (!xIsNum && yIsNum)
            return 1;

        if (xIsNum && yIsNum)
        {
            // numerical comparison
            if (xNum != yNum)
                return xNum < yNum ? -1 : 1;
        }
        else
        {
            // string comparison until next nearest '.'
            const int res = strncmp(xPartFrom, yPartFrom, carb_min(xPartLen, yPartLen));

            // if different "zzz" < "abc", return:
            if (res != 0)
                return res < 0 ? -1 : 1;

            // they are the same, but one longer? "abc" < "abcdef"
            if (xPartLen != yPartLen)
                return xPartLen < yPartLen ? -1 : 1;
        }

        // Go to the next `.` part
        xPartFrom = xPartTo + 1;
        yPartFrom = yPartTo + 1;

        // Reached the end of both? => they are equal
        if (xPartFrom == x + xLen + 1 && yPartFrom == y + yLen + 1)
            break;

        // x ended first? "abc.def" < "abc.def.xyz"
        if (xPartFrom == x + xLen + 1)
            return -1;

        // y ended first?
        if (yPartFrom == y + yLen + 1)
            return 1;
    }

    return 0;
}

inline int prereleaseCmp(const std::string& x, const std::string& y)
{
    return prereleaseCmp(x.c_str(), y.c_str());
}

template <class VersionT>
inline int versionsCmp(const VersionT& lhs, const VersionT& rhs)
{
    if (lhs.major != rhs.major)
        return lhs.major < rhs.major ? -1 : 1;

    if (lhs.minor != rhs.minor)
        return lhs.minor < rhs.minor ? -1 : 1;

    if (lhs.patch != rhs.patch)
        return lhs.patch < rhs.patch ? -1 : 1;

    return prereleaseCmp(lhs.prerelease, rhs.prerelease);
}

inline bool operator<(const SemanticVersion& lhs, const SemanticVersion& rhs)
{
    return versionsCmp(lhs, rhs) < 0;
}

inline bool operator==(const SemanticVersion& lhs, const SemanticVersion& rhs)
{
    // Notice that "build metadata" is ignored in version comparison
    return lhs.major == rhs.major && lhs.minor == rhs.minor && lhs.patch == rhs.patch && lhs.prerelease == rhs.prerelease;
}

inline bool stringToVersion(const std::string& str, SemanticVersion& outVersion)
{
    outVersion = SemanticVersion::getDefault();

    // [major].[minor].[patch]-[prerelease]+[build]
    auto versionAndBuild = extras::split(str, '+');

    // Only one `+` allowed in semver
    if (versionAndBuild.size() > 2)
        return false;

    auto versionParts = extras::split(versionAndBuild[0], '-', 2);

    // parse first part: [major].[minor].[patch]
    {
        auto parts = extras::split(versionParts[0], '.');

        if (parts.empty())
            return false;

        // 1.2.3.4 is not semver
        if (parts.size() > 3)
            return false;

        if (!extras::stringToInteger(parts[0], outVersion.major))
            return false;

        if (parts.size() > 1)
        {
            if (!extras::stringToInteger(parts[1], outVersion.minor))
                return false;
        }
        if (parts.size() > 2)
        {
            if (!extras::stringToInteger(parts[2], outVersion.patch))
                return false;
        }
    }

    // parse second part: [prerelease]+[build]
    if (versionParts.size() > 1)
        outVersion.prerelease = versionParts[1];

    if (versionAndBuild.size() > 1)
        outVersion.build = versionAndBuild[1];

    return true;
}

inline SemanticVersion stringToVersionOrDefault(const std::string& str)
{
    SemanticVersion v;
    if (!stringToVersion(str, v))
        v = SemanticVersion::getDefault();
    return v;
}

inline bool gitHashFromOmniverseVersion(const extras::SemanticVersion& version, std::string& outHash)
{
    // Omniverse Flow format: "{version}+{gitbranch}.{number}.{githash}.{build_location}"
    auto parts = extras::split(version.build, '.');
    outHash = "00000000";
    if (parts.size() > 2)
    {
        outHash = parts[2];
        return true;
    }
    return false;
}

} // namespace extras
} // namespace omni