diff --git a/launcher/Version.cpp b/launcher/Version.cpp index bffe5d58a..fdfd2721b 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -1,111 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2026 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ #include "Version.h" #include #include #include - -Version::Version(QString str) : m_string(std::move(str)) -{ - parse(); -} - -#define VERSION_OPERATOR(return_on_different) \ - bool exclude_our_sections = false; \ - bool exclude_their_sections = false; \ - \ - const auto size = qMax(m_sections.size(), other.m_sections.size()); \ - for (int i = 0; i < size; ++i) { \ - Section sec1 = (i >= m_sections.size()) ? Section() : m_sections.at(i); \ - Section sec2 = (i >= other.m_sections.size()) ? Section() : other.m_sections.at(i); \ - \ - { /* Don't include appendixes in the comparison */ \ - if (sec1.isAppendix()) \ - exclude_our_sections = true; \ - if (sec2.isAppendix()) \ - exclude_their_sections = true; \ - \ - if (exclude_our_sections) { \ - sec1 = Section(); \ - if (sec2.m_isNull) \ - break; \ - } \ - \ - if (exclude_their_sections) { \ - sec2 = Section(); \ - if (sec1.m_isNull) \ - break; \ - } \ - } \ - \ - if (sec1 != sec2) \ - return return_on_different; \ - } - -bool Version::operator<(const Version& other) const -{ - VERSION_OPERATOR(sec1 < sec2) - - return false; -} -bool Version::operator==(const Version& other) const -{ - VERSION_OPERATOR(false) - - return true; -} -bool Version::operator!=(const Version& other) const -{ - return !operator==(other); -} -bool Version::operator<=(const Version& other) const -{ - return *this < other || *this == other; -} -bool Version::operator>(const Version& other) const -{ - return !(*this <= other); -} -bool Version::operator>=(const Version& other) const -{ - return !(*this < other); -} - -void Version::parse() -{ - m_sections.clear(); - QString currentSection; - - if (m_string.isEmpty()) - return; - - auto classChange = [¤tSection](QChar lastChar, QChar currentChar) { - if (lastChar.isNull()) - return false; - if (lastChar.isDigit() != currentChar.isDigit()) - return true; - - const QList s_separators{ '.', '-', '+' }; - if (s_separators.contains(currentChar) && currentSection.at(0) != currentChar) - return true; - - return false; - }; - - currentSection += m_string.at(0); - for (int i = 1; i < m_string.size(); ++i) { - const auto& current_char = m_string.at(i); - if (classChange(m_string.at(i - 1), current_char)) { - if (!currentSection.isEmpty()) - m_sections.append(Section(currentSection)); - currentSection = ""; - } - - currentSection += current_char; - } - - if (!currentSection.isEmpty()) - m_sections.append(Section(currentSection)); -} +#include /// qDebug print support for the Version class QDebug operator<<(QDebug debug, const Version& v) @@ -115,10 +32,11 @@ QDebug operator<<(QDebug debug, const Version& v) debug.nospace() << "Version{ string: " << v.toString() << ", sections: [ "; bool first = true; - for (auto s : v.m_sections) { - if (!first) + for (const auto& s : v.m_sections) { + if (!first) { debug.nospace() << ", "; - debug.nospace() << s.m_fullString; + } + debug.nospace() << s.value; first = false; } @@ -126,3 +44,111 @@ QDebug operator<<(QDebug debug, const Version& v) return debug; } + +std::strong_ordering Version::Section::operator<=>(const Section& other) const +{ + // If both components are numeric, compare numerically (codepoint-wise) + if (this->t == Type::Numeric && other.t == Type::Numeric) { + auto aLen = this->value.size(); + if (aLen != other.value.size()) { + // Lengths differ; compare by length + return aLen <=> other.value.size(); + } + // Compare by digits + auto cmp = QString::compare(this->value, other.value); + if (cmp < 0) { + return std::strong_ordering::less; + } + if (cmp > 0) { + return std::strong_ordering::greater; + } + return std::strong_ordering::equal; + } + // One or both are null + if (this->t == Type::Null) { + if (other.t == Type::PreRelease) { + return std::strong_ordering::greater; + } + return std::strong_ordering::less; + } + if (other.t == Type::Null) { + if (this->t == Type::PreRelease) { + return std::strong_ordering::less; + } + return std::strong_ordering::greater; + } + // Textual comparison (differing type, or both textual/pre-release) + auto minLen = qMin(this->value.size(), other.value.size()); + for (int i = 0; i < minLen; i++) { + auto a = this->value.at(i); + auto b = other.value.at(i); + if (a != b) { + // Compare by rune + return a.unicode() <=> b.unicode(); + } + } + // Compare by length + return this->value.size() <=> other.value.size(); +} + +namespace { +void removeLeadingZeros(QString& s) +{ + s.remove(0, std::distance(s.begin(), std::ranges::find_if_not(s, [](QChar c) { return c == '0'; }))); +} +} // namespace + +void Version::parse() +{ + auto len = m_string.size(); + for (int i = 0; i < len;) { + Section cur(Section::Type::Textual); + auto c = m_string.at(i); + if (c == '+') { + break; // Ignore appendices + } + if (c == '-') { + // Add dash to component + cur.value += '-'; + i++; + // If the next rune is non-digit, mark as pre-release (requires >= 1 non-digit after dash so the component has length > 1) + if (i < len && !m_string.at(i).isDigit()) { + cur.t = Section::Type::PreRelease; + } + } else if (c.isDigit()) { + // Mark as numeric + cur.t = Section::Type::Numeric; + } + for (; i < len; i++) { + auto r = m_string.at(i); + if ((r.isDigit() != (cur.t == Section::Type::Numeric)) || + (r == '-' && cur.t != Section::Type::PreRelease) // "---" is a valid pre-release component + || r == '+') { + // Run completed (do not consume this rune) + break; + } + // Add rune to current run + cur.value += r; + } + if (!cur.value.isEmpty()) { + if (cur.t == Section::Type::Numeric) { + removeLeadingZeros(cur.value); + } + m_sections.append(cur); + } + } +} + +std::strong_ordering Version::operator<=>(const Version& other) const +{ + const auto size = qMax(m_sections.size(), other.m_sections.size()); + for (int i = 0; i < size; ++i) { + auto sec1 = (i >= m_sections.size()) ? Section() : m_sections.at(i); + auto sec2 = (i >= other.m_sections.size()) ? Section() : other.m_sections.at(i); + + if (auto cmp = sec1 <=> sec2; cmp != std::strong_ordering::equal) { + return cmp; + } + } + return std::strong_ordering::equal; +} diff --git a/launcher/Version.h b/launcher/Version.h index 9933fecc3..d123f8348 100644 --- a/launcher/Version.h +++ b/launcher/Version.h @@ -3,6 +3,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 flowln * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2026 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,23 +16,6 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. */ #pragma once @@ -41,115 +25,36 @@ #include #include -class QUrl; - +// this implements the FlexVer +// https://git.sleeping.town/exa/FlexVer class Version { public: - Version(QString str); + Version(QString str) : m_string(std::move(str)) { parse(); } Version() = default; - bool operator<(const Version& other) const; - bool operator<=(const Version& other) const; - bool operator>(const Version& other) const; - bool operator>=(const Version& other) const; - bool operator==(const Version& other) const; - bool operator!=(const Version& other) const; + private: + struct Section { + enum class Type : std::uint8_t { Null, Textual, Numeric, PreRelease }; + explicit Section(Type t = Type::Null, QString value = "") : t(t), value(std::move(value)) {} + Type t; + QString value; + bool operator==(const Section& other) const = default; + std::strong_ordering operator<=>(const Section& other) const; + }; + private: + void parse(); + + public: QString toString() const { return m_string; } bool isEmpty() const { return m_string.isEmpty(); } friend QDebug operator<<(QDebug debug, const Version& v); - private: - struct Section { - explicit Section(QString fullString) : m_fullString(std::move(fullString)) - { - qsizetype cutoff = m_fullString.size(); - for (int i = 0; i < m_fullString.size(); i++) { - if (!m_fullString[i].isDigit()) { - cutoff = i; - break; - } - } - - auto numPart = QStringView{ m_fullString }.left(cutoff); - - if (!numPart.isEmpty()) { - m_isNull = false; - m_numPart = numPart.toInt(); - } - - auto stringPart = QStringView{ m_fullString }.mid(cutoff); - - if (!stringPart.isEmpty()) { - m_isNull = false; - m_stringPart = stringPart.toString(); - } - } - - explicit Section() = default; - - bool m_isNull = true; - - int m_numPart = 0; - QString m_stringPart; - - QString m_fullString; - - inline bool isAppendix() const { return m_stringPart.startsWith('+'); } - inline bool isPreRelease() const { return m_stringPart.startsWith('-') && m_stringPart.length() > 1; } - - inline bool operator==(const Section& other) const - { - if (m_isNull && !other.m_isNull) - return false; - if (!m_isNull && other.m_isNull) - return false; - - if (!m_isNull && !other.m_isNull) { - return (m_numPart == other.m_numPart) && (m_stringPart == other.m_stringPart); - } - - return true; - } - - inline bool operator<(const Section& other) const - { - static auto unequal_is_less = [](const Section& non_null) -> bool { - if (non_null.m_stringPart.isEmpty()) - return non_null.m_numPart == 0; - return (non_null.m_stringPart != QLatin1Char('.')) && non_null.isPreRelease(); - }; - - if (!m_isNull && other.m_isNull) - return unequal_is_less(*this); - if (m_isNull && !other.m_isNull) - return !unequal_is_less(other); - - if (!m_isNull && !other.m_isNull) { - if (m_numPart < other.m_numPart) - return true; - if (m_numPart == other.m_numPart && m_stringPart < other.m_stringPart) - return true; - - if (!m_stringPart.isEmpty() && other.m_stringPart.isEmpty()) - return false; - if (m_stringPart.isEmpty() && !other.m_stringPart.isEmpty()) - return true; - - return false; - } - - return m_fullString < other.m_fullString; - } - - inline bool operator!=(const Section& other) const { return !(*this == other); } - inline bool operator>(const Section& other) const { return !(*this < other || *this == other); } - }; + bool operator==(const Version& other) const { return (*this <=> other) == std::strong_ordering::equal; } + std::strong_ordering operator<=>(const Version& other) const; private: QString m_string; QList
m_sections; - - void parse(); -}; +}; \ No newline at end of file diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 4b3d75169..aa1f01601 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -22,6 +22,8 @@ #include #include #include +#include +#include #include #include #include @@ -91,6 +93,15 @@ auto intEntry(toml::table table, QString entry_name) -> int return node.value_or(0); } +bool sortMCVersions(const QString& a, const QString& b) +{ + auto cmp = Version(a) <=> Version(b); + if (cmp == std::strong_ordering::equal) { + return a < b; + } + return cmp == std::strong_ordering::less; +}; + auto V1::createModFormat([[maybe_unused]] const QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod @@ -117,8 +128,8 @@ auto V1::createModFormat([[maybe_unused]] const QDir& index_dir, mod.side = mod_version.side == ModPlatform::Side::NoSide ? mod_pack.side : mod_version.side; mod.loaders = mod_version.loaders; mod.mcVersions = mod_version.mcVersion; - std::sort(mod.mcVersions.begin(), mod.mcVersions.end(), - [](QString a, QString b) { return Version(std::move(a)) < Version(std::move(b)); }); + mod.mcVersions.removeDuplicates(); + std::ranges::sort(mod.mcVersions, sortMCVersions); mod.releaseType = mod_version.version_type; mod.version_number = mod_version.version_number; @@ -304,8 +315,8 @@ auto V1::getIndexForMod(const QDir& index_dir, QString slug) -> Mod } } } - std::sort(mod.mcVersions.begin(), mod.mcVersions.end(), - [](QString a, QString b) { return Version(std::move(a)) < Version(std::move(b)); }); + mod.mcVersions.removeDuplicates(); + std::ranges::sort(mod.mcVersions, sortMCVersions); } } mod.version_number = table["x-prismlauncher-version-number"].value_or(""); diff --git a/tests/testdata/Version/test_vectors.txt b/tests/testdata/Version/test_vectors.txt index e6c6507cf..971f23daf 100644 --- a/tests/testdata/Version/test_vectors.txt +++ b/tests/testdata/Version/test_vectors.txt @@ -1,5 +1,5 @@ # Test vector from: -# https://github.com/unascribed/FlexVer/blob/704e12759b6e59220ff888f8bf2ec15b8f8fd969/test/test_vectors.txt +# https://git.sleeping.town/exa/FlexVer/src/branch/trunk/test/test_vectors.txt # # This test file is formatted as " ", seperated by the space character # Implementations should ignore lines starting with "#" and lines that have a length of 0 @@ -61,3 +61,10 @@ a1.1.2 < a1.1.2_01 13w02a < c0.3.0_01 0.6.0-1.18.x < 0.9.beta-1.18.x +# removeLeadingZeroes (#17) +0000.0.0 = 0.0.0 +0000.00.0 = 0.00.0 +0.0.0 = 0.00.0000 +# General leading zeroes +1.0.01 = 1.0.1 +1.0.0001 = 1.0.01