mirror of
https://github.com/PrismLauncher/PrismLauncher.git
synced 2026-06-29 01:54:20 +03:00
fix heap overflow with unstable version comparation (#5252)
This commit is contained in:
commit
16bd9c2743
6 changed files with 230 additions and 252 deletions
|
|
@ -1,124 +1,42 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
/*
|
||||||
|
* Prism Launcher - Minecraft Launcher
|
||||||
|
* Copyright (C) 2023 flowln <flowlnlnln@gmail.com>
|
||||||
|
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||||
|
* Copyright (c) 2026 Trial97 <alexandru.tripon97@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
#include "Version.h"
|
#include "Version.h"
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QRegularExpressionMatch>
|
#include <QRegularExpressionMatch>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
#include <compare>
|
||||||
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<QChar> 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));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// qDebug print support for the Version class
|
/// qDebug print support for the Version class
|
||||||
QDebug operator<<(QDebug debug, const Version& v)
|
QDebug operator<<(QDebug debug, const Version& v)
|
||||||
{
|
{
|
||||||
QDebugStateSaver saver(debug);
|
const QDebugStateSaver saver(debug);
|
||||||
|
|
||||||
debug.nospace() << "Version{ string: " << v.toString() << ", sections: [ ";
|
debug.nospace() << "Version{ string: " << v.toString() << ", sections: [ ";
|
||||||
|
|
||||||
bool first = true;
|
bool first = true;
|
||||||
for (auto s : v.m_sections) {
|
for (const auto& s : v.m_sections) {
|
||||||
if (!first)
|
if (!first) {
|
||||||
debug.nospace() << ", ";
|
debug.nospace() << ", ";
|
||||||
debug.nospace() << s.m_fullString;
|
}
|
||||||
|
debug.nospace() << s.value;
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,3 +44,114 @@ QDebug operator<<(QDebug debug, const Version& v)
|
||||||
|
|
||||||
return debug;
|
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
|
||||||
|
}
|
||||||
|
// custom: the space is special to handle the strings like "1.20 Pre-Release 1"
|
||||||
|
// this is needed to support Modrinth versions
|
||||||
|
if (c == '-' || c == ' ') {
|
||||||
|
// Add dash to component
|
||||||
|
cur.value += c;
|
||||||
|
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)) // starts a new section
|
||||||
|
|| (r == ' ' && cur.t == Section::Type::Numeric) // custom: numeric section then a space is a pre-release
|
||||||
|
|| (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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
* Prism Launcher - Minecraft Launcher
|
* Prism Launcher - Minecraft Launcher
|
||||||
* Copyright (C) 2023 flowln <flowlnlnln@gmail.com>
|
* Copyright (C) 2023 flowln <flowlnlnln@gmail.com>
|
||||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||||
|
* Copyright (c) 2026 Trial97 <alexandru.tripon97@gmail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* 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
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*
|
|
||||||
* 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
|
#pragma once
|
||||||
|
|
@ -41,115 +25,36 @@
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringView>
|
#include <QStringView>
|
||||||
|
|
||||||
class QUrl;
|
// this implements the FlexVer
|
||||||
|
// https://git.sleeping.town/exa/FlexVer
|
||||||
class Version {
|
class Version {
|
||||||
public:
|
public:
|
||||||
Version(QString str);
|
Version(QString str) : m_string(std::move(str)) { parse(); } // NOLINT(hicpp-explicit-conversions)
|
||||||
Version() = default;
|
Version() = default;
|
||||||
|
|
||||||
bool operator<(const Version& other) const;
|
private:
|
||||||
bool operator<=(const Version& other) const;
|
struct Section {
|
||||||
bool operator>(const Version& other) const;
|
enum class Type : std::uint8_t { Null, Textual, Numeric, PreRelease };
|
||||||
bool operator>=(const Version& other) const;
|
explicit Section(Type t = Type::Null, QString value = "") : t(t), value(std::move(value)) {}
|
||||||
bool operator==(const Version& other) const;
|
Type t;
|
||||||
bool operator!=(const Version& other) const;
|
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; }
|
QString toString() const { return m_string; }
|
||||||
bool isEmpty() const { return m_string.isEmpty(); }
|
bool isEmpty() const { return m_string.isEmpty(); }
|
||||||
|
|
||||||
friend QDebug operator<<(QDebug debug, const Version& v);
|
friend QDebug operator<<(QDebug debug, const Version& v);
|
||||||
|
|
||||||
private:
|
bool operator==(const Version& other) const { return (*this <=> other) == std::strong_ordering::equal; }
|
||||||
struct Section {
|
std::strong_ordering operator<=>(const Version& other) const;
|
||||||
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); }
|
|
||||||
};
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString m_string;
|
QString m_string;
|
||||||
QList<Section> m_sections;
|
QList<Section> m_sections;
|
||||||
|
};
|
||||||
void parse();
|
|
||||||
};
|
|
||||||
|
|
@ -22,6 +22,8 @@
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <compare>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
@ -36,61 +38,73 @@
|
||||||
|
|
||||||
namespace Packwiz {
|
namespace Packwiz {
|
||||||
|
|
||||||
auto getRealIndexName(const QDir& index_dir, QString normalized_fname, bool should_find_match) -> QString
|
namespace {
|
||||||
|
auto getRealIndexName(const QDir& indexDir, const QString& normalizedFname, bool shouldFindMatch = false) -> QString
|
||||||
{
|
{
|
||||||
QFile index_file(index_dir.absoluteFilePath(normalized_fname));
|
const QFile indexFile(indexDir.absoluteFilePath(normalizedFname));
|
||||||
|
|
||||||
QString real_fname = normalized_fname;
|
QString realFname = normalizedFname;
|
||||||
if (!index_file.exists()) {
|
if (!indexFile.exists()) {
|
||||||
// Tries to get similar entries
|
// Tries to get similar entries
|
||||||
for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) {
|
for (auto& fileName : indexDir.entryList(QDir::Filter::Files)) {
|
||||||
if (!QString::compare(normalized_fname, file_name, Qt::CaseInsensitive)) {
|
if (QString::compare(normalizedFname, fileName, Qt::CaseInsensitive) == 0) {
|
||||||
real_fname = file_name;
|
realFname = fileName;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (should_find_match && !QString::compare(normalized_fname, real_fname, Qt::CaseSensitive)) {
|
if (shouldFindMatch && (QString::compare(normalizedFname, realFname, Qt::CaseSensitive) == 0)) {
|
||||||
qCritical() << "Could not find a match for a valid metadata file!";
|
qCritical() << "Could not find a match for a valid metadata file!";
|
||||||
qCritical() << "File:" << normalized_fname;
|
qCritical() << "File:" << normalizedFname;
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return real_fname;
|
return realFname;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
static inline auto indexFileName(const QString& mod_slug) -> QString
|
auto indexFileName(const QString& modSlug) -> QString
|
||||||
{
|
{
|
||||||
if (mod_slug.endsWith(".pw.toml"))
|
if (modSlug.endsWith(".pw.toml")) {
|
||||||
return mod_slug;
|
return modSlug;
|
||||||
return QString("%1.pw.toml").arg(mod_slug);
|
}
|
||||||
|
return QString("%1.pw.toml").arg(modSlug);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions for extracting data from the TOML file
|
// Helper functions for extracting data from the TOML file
|
||||||
auto stringEntry(toml::table table, QString entry_name) -> QString
|
auto stringEntry(toml::table table, const QString& entryName) -> QString
|
||||||
{
|
{
|
||||||
auto node = table[StringUtils::toStdString(entry_name)];
|
auto* node = table.get(StringUtils::toStdString(entryName));
|
||||||
if (!node) {
|
if (!node) {
|
||||||
qDebug() << "Failed to read str property '" + entry_name + "' in mod metadata.";
|
qDebug() << "Failed to read str property '" + entryName + "' in mod metadata.";
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return node.value_or("");
|
return node->value_or("");
|
||||||
}
|
}
|
||||||
|
|
||||||
auto intEntry(toml::table table, QString entry_name) -> int
|
auto intEntry(toml::table table, const QString& entryName) -> int
|
||||||
{
|
{
|
||||||
auto node = table[StringUtils::toStdString(entry_name)];
|
auto* node = table.get(StringUtils::toStdString(entryName));
|
||||||
if (!node) {
|
if (!node) {
|
||||||
qDebug() << "Failed to read int property '" + entry_name + "' in mod metadata.";
|
qDebug() << "Failed to read int property '" + entryName + "' in mod metadata.";
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return node.value_or(0);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
auto V1::createModFormat([[maybe_unused]] const QDir& index_dir,
|
auto V1::createModFormat([[maybe_unused]] const QDir& index_dir,
|
||||||
ModPlatform::IndexedPack& mod_pack,
|
ModPlatform::IndexedPack& mod_pack,
|
||||||
ModPlatform::IndexedVersion& mod_version) -> Mod
|
ModPlatform::IndexedVersion& mod_version) -> Mod
|
||||||
|
|
@ -117,8 +131,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.side = mod_version.side == ModPlatform::Side::NoSide ? mod_pack.side : mod_version.side;
|
||||||
mod.loaders = mod_version.loaders;
|
mod.loaders = mod_version.loaders;
|
||||||
mod.mcVersions = mod_version.mcVersion;
|
mod.mcVersions = mod_version.mcVersion;
|
||||||
std::sort(mod.mcVersions.begin(), mod.mcVersions.end(),
|
mod.mcVersions.removeDuplicates();
|
||||||
[](QString a, QString b) { return Version(std::move(a)) < Version(std::move(b)); });
|
std::ranges::sort(mod.mcVersions, sortMCVersions);
|
||||||
mod.releaseType = mod_version.version_type;
|
mod.releaseType = mod_version.version_type;
|
||||||
|
|
||||||
mod.version_number = mod_version.version_number;
|
mod.version_number = mod_version.version_number;
|
||||||
|
|
@ -304,8 +318,8 @@ auto V1::getIndexForMod(const QDir& index_dir, QString slug) -> Mod
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::sort(mod.mcVersions.begin(), mod.mcVersions.end(),
|
mod.mcVersions.removeDuplicates();
|
||||||
[](QString a, QString b) { return Version(std::move(a)) < Version(std::move(b)); });
|
std::ranges::sort(mod.mcVersions, sortMCVersions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mod.version_number = table["x-prismlauncher-version-number"].value_or("");
|
mod.version_number = table["x-prismlauncher-version-number"].value_or("");
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,6 @@ class QDir;
|
||||||
|
|
||||||
namespace Packwiz {
|
namespace Packwiz {
|
||||||
|
|
||||||
auto getRealIndexName(const QDir& index_dir, QString normalized_index_name, bool should_match = false) -> QString;
|
|
||||||
|
|
||||||
class V1 {
|
class V1 {
|
||||||
public:
|
public:
|
||||||
// can also represent other resources beside loader mods - but this is what packwiz calls it
|
// can also represent other resources beside loader mods - but this is what packwiz calls it
|
||||||
|
|
|
||||||
|
|
@ -181,8 +181,33 @@ class VersionTest : public QObject {
|
||||||
QCOMPARE(v1 > v2, !lessThan && !equal);
|
QCOMPARE(v1 > v2, !lessThan && !equal);
|
||||||
QCOMPARE(v1 == v2, equal);
|
QCOMPARE(v1 == v2, equal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_strict_weak_order()
|
||||||
|
{
|
||||||
|
// this tests the strict_weak_order
|
||||||
|
// https://en.cppreference.com/w/cpp/concepts/strict_weak_order.html
|
||||||
|
const Version a("1.10 Pre-Release 1"); // this is a pre-relese is before b because ' ' is lower than '-'
|
||||||
|
const Version b("1.10-pre1"); // this is a pre-release is before c that is an actual release
|
||||||
|
const Version c("1.10");
|
||||||
|
|
||||||
|
auto r = [](const Version& a, const Version& b) { return a < b; };
|
||||||
|
auto e = [&r](const Version& a, const Version& b) { return !r(a, b) && !r(b, a); };
|
||||||
|
|
||||||
|
qCritical() << a << b << c;
|
||||||
|
|
||||||
|
// irreflexive
|
||||||
|
QCOMPARE(r(a, a), false);
|
||||||
|
QCOMPARE(r(b, b), false);
|
||||||
|
QCOMPARE(r(c, c), false);
|
||||||
|
// transitive
|
||||||
|
QCOMPARE(r(a, b), true);
|
||||||
|
QCOMPARE(r(b, c), true);
|
||||||
|
QCOMPARE(r(a, c), true);
|
||||||
|
// transitive equivalence
|
||||||
|
QCOMPARE(e(a, b) && e(b, c), e(a, c));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
QTEST_GUILESS_MAIN(VersionTest)
|
QTEST_GUILESS_MAIN(VersionTest)
|
||||||
|
|
||||||
#include "Version_test.moc"
|
#include "Version_test.moc"
|
||||||
9
tests/testdata/Version/test_vectors.txt
vendored
9
tests/testdata/Version/test_vectors.txt
vendored
|
|
@ -1,5 +1,5 @@
|
||||||
# Test vector from:
|
# 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 "<lefthand> <operator> <righthand>", seperated by the space character
|
# This test file is formatted as "<lefthand> <operator> <righthand>", seperated by the space character
|
||||||
# Implementations should ignore lines starting with "#" and lines that have a length of 0
|
# 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
|
13w02a < c0.3.0_01
|
||||||
0.6.0-1.18.x < 0.9.beta-1.18.x
|
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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue