aboutsummaryrefslogtreecommitdiff
path: root/libs/uibase/src/versioninfo.cpp
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
commit7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch)
tree27fb39be241fdb5ac2734c574de678977d1856d0 /libs/uibase/src/versioninfo.cpp
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/uibase/src/versioninfo.cpp')
-rw-r--r--libs/uibase/src/versioninfo.cpp432
1 files changed, 432 insertions, 0 deletions
diff --git a/libs/uibase/src/versioninfo.cpp b/libs/uibase/src/versioninfo.cpp
new file mode 100644
index 0000000..6dec18e
--- /dev/null
+++ b/libs/uibase/src/versioninfo.cpp
@@ -0,0 +1,432 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 3 of the License, or (at your option) any later version.
+
+This library 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
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#include <uibase/versioninfo.h>
+#include <QDate>
+#include <QLocale>
+#include <QRegularExpression>
+#include <QVersionNumber>
+
+namespace
+{
+const QRegularExpression VERSION_REGEX("^(\\d+)(\\.(\\d+))?(\\.(\\d+))?(\\.(\\d+))?");
+}
+
+namespace MOBase
+{
+
+VersionInfo::VersionInfo()
+ : m_Scheme(SCHEME_REGULAR), m_Valid(false), m_ReleaseType(RELEASE_FINAL),
+ m_Major(0), m_Minor(0), m_SubMinor(0), m_SubSubMinor(0), m_DecimalPositions(0),
+ m_Rest()
+{}
+
+VersionInfo::VersionInfo(int major, int minor, int subminor, int subsubminor,
+ ReleaseType releaseType)
+ : m_Scheme(SCHEME_REGULAR), m_Valid(true), m_ReleaseType(releaseType),
+ m_Major(major), m_Minor(minor), m_SubMinor(subminor), m_SubSubMinor(subsubminor),
+ m_DecimalPositions(0), m_Rest()
+{}
+
+VersionInfo::VersionInfo(int major, int minor, int subminor, ReleaseType releaseType)
+ : m_Scheme(SCHEME_REGULAR), m_Valid(true), m_ReleaseType(releaseType),
+ m_Major(major), m_Minor(minor), m_SubMinor(subminor), m_SubSubMinor(0),
+ m_DecimalPositions(0), m_Rest()
+{}
+
+VersionInfo::VersionInfo(const QString& versionString, VersionScheme scheme)
+ : m_Valid(true), m_ReleaseType(RELEASE_FINAL), m_Major(0), m_Minor(0),
+ m_SubMinor(0), m_SubSubMinor(0), m_DecimalPositions(0), m_Rest()
+{
+ parse(versionString, scheme);
+}
+
+VersionInfo::VersionInfo(const QString& versionString,
+ VersionInfo::VersionScheme scheme, bool manualInput)
+ : m_Valid(true), m_ReleaseType(RELEASE_FINAL), m_Major(0), m_Minor(0),
+ m_SubMinor(0), m_SubSubMinor(0), m_DecimalPositions(0), m_Rest()
+{
+ parse(versionString, scheme, manualInput);
+}
+
+void VersionInfo::clear()
+{
+ m_Scheme = SCHEME_REGULAR;
+ m_Valid = false;
+ m_ReleaseType = RELEASE_FINAL;
+ m_Major = m_Minor = m_SubMinor = m_SubSubMinor = m_DecimalPositions = 0;
+ m_Rest.clear();
+}
+
+QString VersionInfo::canonicalString() const
+{
+ if (!isValid()) {
+ return QString();
+ }
+
+ QString result;
+ if (m_Scheme == SCHEME_REGULAR) {
+ result = QString("%1.%2.%3.%4")
+ .arg(m_Major)
+ .arg(m_Minor)
+ .arg(m_SubMinor)
+ .arg(m_SubSubMinor);
+ } else if (m_Scheme == SCHEME_DECIMALMARK) {
+ result = QString("f%1.%2").arg(m_Major).arg(
+ QString("%1").arg(m_Minor).rightJustified(m_DecimalPositions, '0'));
+ } else if (m_Scheme == SCHEME_NUMBERSANDLETTERS) {
+ result = QString("n%1.%2.%3.%4")
+ .arg(m_Major)
+ .arg(m_Minor)
+ .arg(m_SubMinor)
+ .arg(m_SubSubMinor);
+ } else if (m_Scheme == SCHEME_DATE) {
+ // year.month.day was stored in the version fields
+ result = QString("d%1.%2.%3.%4")
+ .arg(m_Major)
+ .arg(m_Minor)
+ .arg(m_SubMinor)
+ .arg(m_SubSubMinor);
+ }
+ switch (m_ReleaseType) {
+ case RELEASE_PREALPHA: {
+ result.append(" pre-alpha");
+ } break;
+ case RELEASE_ALPHA: {
+ result.append("a");
+ } break;
+ case RELEASE_BETA: {
+ result.append("b");
+ } break;
+ case RELEASE_CANDIDATE: {
+ result.append("rc");
+ } break;
+ case RELEASE_FINAL: // fall-through
+ default: {
+ // nop
+ } break;
+ }
+
+ if (!m_Rest.isEmpty()) {
+ result.append(QString("%1").arg(m_Rest));
+ }
+
+ return result;
+}
+
+QString VersionInfo::displayString(int forcedVersionSegments) const
+{
+ if (!isValid()) {
+ return QString();
+ }
+
+ QString result;
+ if (m_Scheme == SCHEME_REGULAR) {
+ if (forcedVersionSegments >= 4 || m_SubSubMinor != 0) {
+ result = QString("%1.%2.%3.%4")
+ .arg(m_Major)
+ .arg(m_Minor)
+ .arg(m_SubMinor)
+ .arg(m_SubSubMinor);
+ } else if (forcedVersionSegments == 3 || m_SubMinor != 0) {
+ result = QString("%1.%2.%3").arg(m_Major).arg(m_Minor).arg(m_SubMinor);
+ } else {
+ result = QString("%1.%2").arg(m_Major).arg(m_Minor);
+ }
+ } else if (m_Scheme == SCHEME_DECIMALMARK) {
+ result = QString("%1.%2").arg(m_Major).arg(
+ QString("%1").arg(m_Minor).rightJustified(m_DecimalPositions, '0'));
+ } else if (m_Scheme == SCHEME_NUMBERSANDLETTERS) {
+ result = QString("%1.%2.%3.%4")
+ .arg(m_Major)
+ .arg(m_Minor)
+ .arg(m_SubMinor)
+ .arg(m_SubSubMinor);
+ } else if (m_Scheme == SCHEME_DATE) {
+ // year.month.day was stored in the version fields
+ const auto year = m_Major;
+ const auto month = m_Minor;
+ const auto day = m_SubMinor;
+
+ return QLocale::system().toString(QDate(year, month, day),
+ QLocale::FormatType::ShortFormat);
+ }
+ switch (m_ReleaseType) {
+ case RELEASE_PREALPHA: {
+ result.append(" pre-alpha");
+ } break;
+ case RELEASE_ALPHA: {
+ result.append("alpha");
+ } break;
+ case RELEASE_BETA: {
+ result.append("beta");
+ } break;
+ case RELEASE_CANDIDATE: {
+ result.append("rc");
+ } break;
+ case RELEASE_FINAL: // fall-through
+ default: {
+ // nop
+ } break;
+ }
+
+ if (!m_Rest.isEmpty()) {
+ result.append(QString("%1").arg(m_Rest));
+ }
+
+ return result;
+}
+
+QVersionNumber VersionInfo::asQVersionNumber() const
+{
+ return QVersionNumber::fromString(displayString()).normalized();
+}
+
+QString VersionInfo::parseReleaseType(QString versionString)
+{
+ // release types are often followed by a number (i.e. "beta4"). This needs to be
+ // extracted now, otherwise the outer parser will think it's the subminor version and
+ // then 1.0.0rc1 would be interpreted as newer than 1.0.0
+
+ static std::map<QString, ReleaseType> typeStrings = {{"prealpha", RELEASE_PREALPHA},
+ {"alpha", RELEASE_ALPHA},
+ {"beta", RELEASE_BETA},
+ {"rc", RELEASE_CANDIDATE}};
+
+ m_ReleaseType = RELEASE_FINAL;
+
+ auto typeIter = typeStrings.begin();
+ int offset = -1;
+
+ for (; (typeIter != typeStrings.end()) && (offset == -1); ++typeIter) {
+ offset = (int)versionString.indexOf(typeIter->first, Qt::CaseInsensitive);
+ if (offset != -1) {
+ m_ReleaseType = typeIter->second;
+ break;
+ }
+ }
+
+ int length = 0;
+
+ if (typeIter != typeStrings.end()) {
+ length = (int)typeIter->first.length();
+ }
+
+ if (m_Scheme == SCHEME_REGULAR) {
+ // also interpret the a/b letters, but only if they follow immediately on the
+ // version number, otherwise the margin for error is too big
+ if ((offset == -1) && (versionString.length() > 0)) {
+ if (versionString.at(0) == 'a') {
+ m_ReleaseType = RELEASE_ALPHA;
+ offset = 0;
+ length = 1;
+ } else if (versionString.at(0) == 'b') {
+ m_ReleaseType = RELEASE_BETA;
+ offset = 0;
+ length = 1;
+ }
+ }
+ }
+
+ if (offset != -1) {
+ versionString.remove(offset, length);
+ }
+ return versionString.trimmed();
+}
+
+void VersionInfo::parse(const QString& versionString, VersionScheme scheme,
+ bool manualInput)
+{
+ m_Valid = false;
+ m_Scheme = scheme == SCHEME_LITERAL ? SCHEME_REGULAR
+ : scheme != SCHEME_DISCOVER ? scheme
+ : SCHEME_REGULAR;
+ m_ReleaseType = RELEASE_FINAL;
+ m_Major = m_Minor = m_SubMinor = m_SubSubMinor = 0;
+ m_Rest.clear();
+ if (versionString.length() == 0) {
+ return;
+ }
+
+ if (QString::compare(versionString, "final", Qt::CaseInsensitive) == 0) {
+ m_Major = 1;
+ m_Valid = true;
+ return;
+ }
+
+ QString temp = versionString;
+ // first, determine the versioning scheme if there is a hint
+ VersionScheme newScheme = m_Scheme;
+ if (!manualInput) {
+ if (temp.startsWith('f')) {
+ newScheme = SCHEME_DECIMALMARK;
+ temp.remove(0, 1);
+ } else if (temp.startsWith('n')) {
+ newScheme = SCHEME_NUMBERSANDLETTERS;
+ temp.remove(0, 1);
+ } else if (temp.startsWith('d')) {
+ newScheme = SCHEME_DATE;
+ temp.remove(0, 1);
+ }
+ }
+
+ if (scheme == SCHEME_DISCOVER) {
+ m_Scheme = newScheme;
+ }
+
+ if (temp.startsWith('v', Qt::CaseInsensitive)) {
+ // v is often prepended to versions
+ temp.remove(0, 1);
+ }
+
+ auto match = VERSION_REGEX.match(temp);
+ if (match.hasMatch()) {
+ m_Major = match.captured(1).toInt();
+ m_Minor = match.captured(3).toInt();
+ QString subMinor = match.captured(5);
+ QString subSubMinor = match.captured(7);
+ if (!subMinor.isEmpty() && (m_Scheme == SCHEME_DECIMALMARK)) {
+ // nooooope, if there are two dots it can't be a decimal mark
+ m_Scheme = SCHEME_REGULAR;
+ }
+ if (m_Scheme != SCHEME_DECIMALMARK) {
+ m_SubMinor = subMinor.toInt();
+ m_SubSubMinor = subSubMinor.toInt();
+ }
+ if (subMinor.isEmpty() && (match.captured(3).size() > 1) &&
+ match.captured(3).startsWith('0')) {
+ // this indicates a decimal scheme
+ m_Scheme = SCHEME_DECIMALMARK;
+ m_DecimalPositions = (int)match.captured(3).size();
+ }
+ temp.remove(VERSION_REGEX);
+ } else {
+ m_Scheme = SCHEME_LITERAL;
+ }
+
+ if (m_Scheme == SCHEME_REGULAR) {
+ temp = parseReleaseType(temp);
+ }
+
+ if ((m_Scheme == SCHEME_DATE) && (m_Major < 1900)) {
+ m_Scheme = SCHEME_REGULAR;
+ }
+ m_Rest = temp.trimmed();
+ m_Valid = true;
+}
+
+QDLLEXPORT bool operator<(const VersionInfo& LHS, const VersionInfo& RHS)
+{
+ if (!LHS.isValid() && RHS.isValid())
+ return true;
+ if (!RHS.isValid() && LHS.isValid())
+ return false;
+
+ // date-releases are lower than regular versions
+ if ((LHS.m_Scheme == VersionInfo::SCHEME_DATE) &&
+ (RHS.m_Scheme != VersionInfo::SCHEME_DATE)) {
+ return true;
+ } else if ((LHS.m_Scheme != VersionInfo::SCHEME_DATE) &&
+ (RHS.m_Scheme == VersionInfo::SCHEME_DATE)) {
+ return false;
+ } else if ((LHS.m_Scheme == VersionInfo::SCHEME_DECIMALMARK) ||
+ (RHS.m_Scheme == VersionInfo::SCHEME_DECIMALMARK)) {
+ // use decimal versioning if either version is a decimal. The parser interprets
+ // versions as regular if in doubt so if the scheme is "decimal" it is definitively
+ // a decimal version number whereas SCHEME_REGULAR means "probably regular"
+
+ float leftVal = QString("%1.%2")
+ .arg(LHS.m_Major)
+ .arg(QString("%1")
+ .arg(LHS.m_Minor)
+ .rightJustified(LHS.m_DecimalPositions, '0'))
+ .toFloat();
+ float rightVal = QString("%1.%2")
+ .arg(RHS.m_Major)
+ .arg(QString("%1")
+ .arg(RHS.m_Minor)
+ .rightJustified(RHS.m_DecimalPositions, '0'))
+ .toFloat();
+ if (fabs(leftVal - rightVal) > 0.001f) {
+ return leftVal < rightVal;
+ }
+ } else {
+ // if in doubt, use the sane choice. regular and numbers+letters can be treated the
+ // same way
+ if (LHS.m_Major != RHS.m_Major)
+ return LHS.m_Major < RHS.m_Major;
+ if (LHS.m_Minor != RHS.m_Minor)
+ return LHS.m_Minor < RHS.m_Minor;
+ if (LHS.m_SubMinor != RHS.m_SubMinor)
+ return LHS.m_SubMinor < RHS.m_SubMinor;
+ if (LHS.m_SubSubMinor != RHS.m_SubSubMinor)
+ return LHS.m_SubSubMinor < RHS.m_SubSubMinor;
+ }
+
+ // subminor, release-type and rest are treated the same for all versioning schemes,
+ // but on parsing they may still differ, i.e. a b-suffix is only interpreted to mean
+ // "beta" in the regular scheme
+ if (LHS.m_ReleaseType != RHS.m_ReleaseType)
+ return LHS.m_ReleaseType < RHS.m_ReleaseType;
+
+ // if the rest contains only integers, compare them numerically
+ bool LHS_ok, RHS_ok;
+ int LHS_int, RHS_int;
+ LHS_int = LHS.m_Rest.toInt(&LHS_ok);
+ RHS_int = RHS.m_Rest.toInt(&RHS_ok);
+ if (LHS_ok && RHS_ok) {
+ return LHS_int < RHS_int;
+ }
+
+ // give up and compare lexically
+ return LHS.m_Rest < RHS.m_Rest;
+}
+
+QDLLEXPORT bool operator>(const VersionInfo& LHS, const VersionInfo& RHS)
+{
+ return !(LHS <= RHS);
+}
+
+QDLLEXPORT bool operator<=(const VersionInfo& LHS, const VersionInfo& RHS)
+{
+ // TODO not exactly optimized...
+ if (LHS < RHS) {
+ return true;
+ } else {
+ return !(RHS < LHS);
+ }
+}
+
+QDLLEXPORT bool operator>=(const VersionInfo& LHS, const VersionInfo& RHS)
+{
+ return RHS <= LHS;
+}
+
+QDLLEXPORT bool operator!=(const VersionInfo& LHS, const VersionInfo& RHS)
+{
+ return (LHS < RHS) || (RHS < LHS);
+}
+
+QDLLEXPORT bool operator==(const VersionInfo& LHS, const VersionInfo& RHS)
+{
+ return !(LHS < RHS) && !(RHS < LHS);
+}
+
+} // namespace MOBase