aboutsummaryrefslogtreecommitdiffstats
path: root/src/m_ver_check/ReleaseCheck.cpp
diff options
context:
space:
mode:
authorsaturneric <[email protected]>2026-09-24 22:59:10 +0200
committersaturneric <[email protected]>2026-09-24 22:59:10 +0200
commit4249f0192baf88e33bfb4e47828279ae2b569a8d (patch)
tree5eea66cc8f58c6770c9777215a58c7be8f375e1b /src/m_ver_check/ReleaseCheck.cpp
parentdocs(README): document instant messaging module and UI actions (diff)
downloadmodules-4249f0192baf88e33bfb4e47828279ae2b569a8d.tar.gz
modules-4249f0192baf88e33bfb4e47828279ae2b569a8d.zip
feat(update): add robust GitHub update checking
- replace legacy provider tasks with an injectable update checker - track release, tag, and commit lookup results independently - preserve last known good results across failed refreshes - persist schema-versioned update state and reject stale formats - add update presentation logic and refresh the update dialog UI - remove provider selection in favor of GitHub Releases - add comprehensive release-checking and presentation tests
Diffstat (limited to 'src/m_ver_check/ReleaseCheck.cpp')
-rw-r--r--src/m_ver_check/ReleaseCheck.cpp181
1 files changed, 181 insertions, 0 deletions
diff --git a/src/m_ver_check/ReleaseCheck.cpp b/src/m_ver_check/ReleaseCheck.cpp
new file mode 100644
index 0000000..ed9e799
--- /dev/null
+++ b/src/m_ver_check/ReleaseCheck.cpp
@@ -0,0 +1,181 @@
+/**
+ * Copyright (C) 2021-2024 Saturneric <[email protected]>
+ *
+ * This file is part of GpgFrontend.
+ *
+ * GpgFrontend 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GpgFrontend 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 GpgFrontend. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * The initial version of the source code is inherited from
+ * the gpg4usb project, which is under GPL-3.0-or-later.
+ *
+ * All the source code of GpgFrontend was modified and released by
+ * Saturneric <[email protected]> starting on May 12, 2021.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#include "ReleaseCheck.h"
+
+#include <GFSDKApp.h>
+
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QRegularExpression>
+
+namespace {
+
+auto Compare(const QString& a, const QString& b) -> int {
+ return GFCompareSoftwareVersion(a.toUtf8().constData(),
+ b.toUtf8().constData());
+}
+
+auto ParseObject(const QByteArray& body) -> std::optional<QJsonObject> {
+ const auto doc = QJsonDocument::fromJson(body);
+ if (!doc.isObject()) return std::nullopt;
+ return doc.object();
+}
+
+} // namespace
+
+auto ExtractVersionFromRawTag(const QString& raw_tag) -> QString {
+ static const QRegularExpression kVersionRe(
+ R"(^[vV](\d+\.)?(\d+\.)?(\*|\d+))");
+ auto match = kVersionRe.match(raw_tag);
+ return match.hasMatch() ? match.captured(0) : QString{};
+}
+
+auto IsPrereleaseVersion(const QString& version) -> bool {
+ static const QRegularExpression kSuffixRe(R"(^[vV]?\d+(\.\d+)*[-+~_a-zA-Z])");
+ return kSuffixRe.match(version).hasMatch();
+}
+
+auto ParseReleaseList(int http_status, const QByteArray& body,
+ const QString& current) -> ListResult {
+ ListResult result;
+ if (http_status != 200) return result;
+
+ const auto doc = QJsonDocument::fromJson(body);
+ if (!doc.isArray()) return result;
+
+ const bool accept_prerelease = IsPrereleaseVersion(current);
+ result.outcome = ListOutcome::kNoMatch;
+
+ for (const auto& value : doc.array()) {
+ if (!value.isObject()) continue;
+ const auto release = value.toObject();
+
+ if (release["draft"].toBool()) continue;
+ if (release["prerelease"].toBool() && !accept_prerelease) continue;
+
+ const auto version =
+ ExtractVersionFromRawTag(release["tag_name"].toString());
+ if (version.isEmpty()) continue;
+ if (!SoftwareVersion::SameSeries(version, current)) continue;
+
+ if (result.outcome == ListOutcome::kFound &&
+ Compare(version, result.latest.version) <= 0) {
+ continue;
+ }
+
+ result.outcome = ListOutcome::kFound;
+ result.latest.version = version;
+ result.latest.published_at = release["published_at"].toString();
+ result.latest.notes = release["body"].toString();
+ result.latest.html_url = release["html_url"].toString();
+ }
+
+ return result;
+}
+
+auto ClassifyLookup(int http_status, bool body_valid) -> RemoteFact {
+ if (http_status == 200) {
+ return body_valid ? RemoteFact::kConfirmed : RemoteFact::kUnknown;
+ }
+ if (http_status == 404) return RemoteFact::kNotFound;
+ return RemoteFact::kUnknown;
+}
+
+auto ParseTagLookup(int http_status, const QByteArray& body) -> RemoteFact {
+ const auto obj = http_status == 200 ? ParseObject(body) : std::nullopt;
+ const bool valid = obj && !obj->value("tag_name").toString().isEmpty();
+ return ClassifyLookup(http_status, valid);
+}
+
+auto ParseCommitLookup(int http_status, const QByteArray& body,
+ const QString& local_commit) -> RemoteFact {
+ const auto commit = local_commit.trimmed();
+ if (commit.isEmpty()) return RemoteFact::kUnknown;
+
+ if (http_status == 422) {
+ const auto obj = ParseObject(body);
+ const bool no_commit =
+ obj && obj->value("message").toString().startsWith("No commit found");
+ return no_commit ? RemoteFact::kNotFound : RemoteFact::kUnknown;
+ }
+
+ // GitHub resolves a short SHA to the full one, so the answer must start
+ // with ours; any other SHA answers a different question.
+ const auto obj = http_status == 200 ? ParseObject(body) : std::nullopt;
+ const bool valid = obj && obj->value("sha").toString().startsWith(
+ commit, Qt::CaseInsensitive);
+ return ClassifyLookup(http_status, valid);
+}
+
+auto IsUsable(const SoftwareVersion& r) -> bool {
+ return r.list.outcome != ListOutcome::kFailed;
+}
+
+auto IsComplete(const SoftwareVersion& fresh) -> bool {
+ return IsUsable(fresh) && fresh.tag_fact != RemoteFact::kUnknown &&
+ fresh.commit_fact != RemoteFact::kUnknown;
+}
+
+auto IsAuthoritative(const SoftwareVersion& r) -> bool {
+ return IsUsable(r) && r.complete;
+}
+
+auto Merge(const std::optional<SoftwareVersion>& prev,
+ const SoftwareVersion& fresh) -> std::optional<SoftwareVersion> {
+ if (!IsUsable(fresh)) return prev;
+
+ auto merged = fresh;
+ if (prev && prev->SameBuild(fresh.current_version, fresh.local_commit_hash)) {
+ if (merged.tag_fact == RemoteFact::kUnknown) {
+ merged.tag_fact = prev->tag_fact;
+ }
+ if (merged.commit_fact == RemoteFact::kUnknown) {
+ merged.commit_fact = prev->commit_fact;
+ }
+ }
+ return merged;
+}
+
+auto Decide(const SoftwareVersion& r) -> Verdict {
+ if (!IsUsable(r)) return Verdict::kUnknown;
+ if (r.list.outcome == ListOutcome::kFound &&
+ Compare(r.current_version, r.list.latest.version) < 0) {
+ return Verdict::kUpdateAvailable;
+ }
+ if (r.tag_fact == RemoteFact::kNotFound) {
+ return Verdict::kWithdrawnOrUnreleased;
+ }
+ if (r.commit_fact == RemoteFact::kNotFound) return Verdict::kUnofficialBuild;
+ return Verdict::kUpToDate;
+}
+
+auto ShouldPromptAtStartup(Verdict v) -> bool {
+ return v == Verdict::kUpdateAvailable;
+}