#pragma once #include #include #include #include #include #include #include #include #include namespace version_compare { using prerelease_identifier_t = std::variant; struct semver_t { int major {0}; int minor {0}; int patch {0}; std::vector prerelease; }; inline std::string ascii_lower(std::string value) { std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) { return static_cast(std::tolower(ch)); }); return value; } inline bool is_numeric_identifier(const std::string &identifier) { return !identifier.empty() && std::all_of(identifier.begin(), identifier.end(), [](unsigned char ch) { return std::isdigit(ch) != 0; }); } inline bool parse_nonnegative_integer(std::string_view value, int &result) { if (value.empty()) { return false; } const auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), result); return error == std::errc {} && end == value.data() + value.size() && result >= 0; } inline semver_t parse_semver(std::string_view version) { semver_t out; if (version.empty()) { return out; } std::string value(version); if (!value.empty() && (value[0] == 'v' || value[0] == 'V')) { value.erase(0, 1); } if (auto plus = value.find('+'); plus != std::string::npos) { value.resize(plus); } std::string core = value; if (auto dash = value.find('-'); dash != std::string::npos) { core = value.substr(0, dash); std::stringstream prerelease_stream(value.substr(dash + 1)); std::string identifier; while (std::getline(prerelease_stream, identifier, '.')) { if (identifier.empty()) { continue; } if (is_numeric_identifier(identifier)) { int numeric_identifier = 0; if (parse_nonnegative_integer(identifier, numeric_identifier)) { out.prerelease.emplace_back(numeric_identifier); continue; } } out.prerelease.emplace_back(identifier); } } std::stringstream core_stream(core); std::string part; if (std::getline(core_stream, part, '.') && !parse_nonnegative_integer(part, out.major)) { return semver_t {}; } if (std::getline(core_stream, part, '.') && !parse_nonnegative_integer(part, out.minor)) { return semver_t {}; } if (std::getline(core_stream, part, '.') && !parse_nonnegative_integer(part, out.patch)) { return semver_t {}; } return out; } inline bool is_stable_channel(const semver_t &version) { if (version.prerelease.empty() || !std::holds_alternative(version.prerelease.front())) { return false; } return ascii_lower(std::get(version.prerelease.front())) == "stable"; } inline bool is_prerelease_channel(std::string_view version) { const auto parsed = parse_semver(version); return !parsed.prerelease.empty() && !is_stable_channel(parsed); } inline int compare_identifier_lists( const std::vector &lhs, const std::vector &rhs, size_t start_index = 0 ) { const size_t len = std::max(lhs.size(), rhs.size()); for (size_t i = start_index; i < len; ++i) { if (i >= lhs.size()) { return -1; } if (i >= rhs.size()) { return 1; } const auto &left = lhs[i]; const auto &right = rhs[i]; const bool left_is_num = std::holds_alternative(left); const bool right_is_num = std::holds_alternative(right); if (left_is_num && right_is_num) { const int left_value = std::get(left); const int right_value = std::get(right); if (left_value != right_value) { return left_value < right_value ? -1 : 1; } continue; } if (left_is_num != right_is_num) { return left_is_num ? -1 : 1; } const auto &left_value = std::get(left); const auto &right_value = std::get(right); if (left_value != right_value) { return left_value < right_value ? -1 : 1; } } return 0; } inline int compare_semver(std::string_view lhs, std::string_view rhs) { const auto left = parse_semver(lhs); const auto right = parse_semver(rhs); if (left.major != right.major) { return left.major < right.major ? -1 : 1; } if (left.minor != right.minor) { return left.minor < right.minor ? -1 : 1; } if (left.patch != right.patch) { return left.patch < right.patch ? -1 : 1; } const bool left_stable = is_stable_channel(left); const bool right_stable = is_stable_channel(right); // ArtLight Server uses `-stable.N` for post-release respins, so those need to sort // above the naked release while keeping normal prerelease semantics elsewhere. if (left_stable && right_stable) { return compare_identifier_lists(left.prerelease, right.prerelease, 1); } if (left_stable != right_stable) { return left_stable ? 1 : -1; } if (left.prerelease.empty() && right.prerelease.empty()) { return 0; } if (left.prerelease.empty()) { return 1; } if (right.prerelease.empty()) { return -1; } return compare_identifier_lists(left.prerelease, right.prerelease); } } // namespace version_compare