summaryrefslogtreecommitdiff
path: root/archived/projt-launcher/launcher/meta/BaseEntity.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'archived/projt-launcher/launcher/meta/BaseEntity.cpp')
-rw-r--r--archived/projt-launcher/launcher/meta/BaseEntity.cpp291
1 files changed, 291 insertions, 0 deletions
diff --git a/archived/projt-launcher/launcher/meta/BaseEntity.cpp b/archived/projt-launcher/launcher/meta/BaseEntity.cpp
new file mode 100644
index 0000000000..785653a6e4
--- /dev/null
+++ b/archived/projt-launcher/launcher/meta/BaseEntity.cpp
@@ -0,0 +1,291 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "BaseEntity.hpp"
+
+#include <QDir>
+#include <QFile>
+
+#include "Application.h"
+#include "BuildConfig.h"
+#include "Exception.h"
+#include "FileSystem.h"
+#include "Json.h"
+#include "modplatform/helpers/HashUtils.h"
+#include "net/ApiDownload.h"
+#include "net/ChecksumValidator.h"
+#include "net/HttpMetaCache.h"
+
+namespace projt::meta
+{
+
+ namespace
+ {
+
+ /**
+ * @brief Validator that parses downloaded JSON into the target entity.
+ */
+ class JsonParseValidator : public Net::Validator
+ {
+ public:
+ explicit JsonParseValidator(MetaEntity* entity) : m_entity(entity)
+ {}
+
+ bool init(QNetworkRequest&) override
+ {
+ m_buffer.clear();
+ return true;
+ }
+
+ bool write(QByteArray& chunk) override
+ {
+ m_buffer.append(chunk);
+ return true;
+ }
+
+ bool abort() override
+ {
+ m_buffer.clear();
+ return true;
+ }
+
+ bool validate(QNetworkReply&) override
+ {
+ QString filename = m_entity->cacheFilePath();
+ try
+ {
+ QJsonDocument doc = Json::requireDocument(m_buffer, filename);
+ QJsonObject root = Json::requireObject(doc, filename);
+ m_entity->loadFromJson(root);
+ return true;
+ }
+ catch (const Exception& ex)
+ {
+ qWarning() << "Failed to parse metadata:" << ex.cause();
+ return false;
+ }
+ }
+
+ private:
+ QByteArray m_buffer;
+ MetaEntity* m_entity;
+ };
+
+ } // anonymous namespace
+
+ QUrl MetaEntity::remoteUrl() const
+ {
+ auto settings = APPLICATION->settings();
+ QString override = settings->get("MetaURLOverride").toString();
+
+ QString baseUrl = override.isEmpty() ? BuildConfig.META_URL : override;
+ return QUrl(baseUrl).resolved(cacheFilePath());
+ }
+
+ Task::Ptr MetaEntity::createLoadTask(Net::Mode mode)
+ {
+ if (m_activeTask && m_activeTask->isRunning())
+ return m_activeTask;
+
+ m_activeTask = Task::Ptr(new EntityLoader(this, mode));
+ return m_activeTask;
+ }
+
+ // EntityLoader implementation
+
+ EntityLoader::EntityLoader(MetaEntity* target, Net::Mode mode) : m_target(target), m_mode(mode)
+ {}
+
+ void EntityLoader::executeTask()
+ {
+ attemptLocalLoad();
+ }
+
+ void EntityLoader::attemptLocalLoad()
+ {
+ QString cachePath = QDir("meta").absoluteFilePath(m_target->cacheFilePath());
+
+ if (!QFile::exists(cachePath))
+ {
+ // No local cache, need remote fetch
+ if (m_mode == Net::Mode::Offline)
+ {
+ emitFailed(tr("Metadata not available offline: %1").arg(m_target->cacheFilePath()));
+ return;
+ }
+ initiateRemoteFetch();
+ return;
+ }
+
+ QString cacheError;
+
+ if (m_mode == Net::Mode::Online)
+ {
+ // In online mode, always revalidate metadata against the remote cache entry on first load.
+ // Relying only on the cached file plus the last known expected checksum can leave us stuck
+ // on stale package manifests across launcher restarts.
+ if (loadCachedMetadata(cachePath, &cacheError))
+ {
+ m_target->m_state = MetaEntity::State::Cached;
+ m_canUseLocalFallback = true;
+ }
+ else
+ {
+ qDebug() << "Cache parse failed for" << cachePath << ":" << cacheError;
+ FS::deletePath(cachePath);
+ m_target->m_state = MetaEntity::State::Pending;
+ m_canUseLocalFallback = false;
+ }
+ initiateRemoteFetch();
+ return;
+ }
+
+ if (loadCachedMetadata(cachePath, &cacheError))
+ {
+ m_target->m_state = MetaEntity::State::Cached;
+ emitSucceeded();
+ return;
+ }
+
+ qDebug() << "Cache parse failed for" << cachePath << ":" << cacheError;
+ FS::deletePath(cachePath);
+ m_target->m_state = MetaEntity::State::Pending;
+
+ if (m_mode == Net::Mode::Offline)
+ {
+ emitFailed(tr("Cached metadata corrupted and offline mode active"));
+ return;
+ }
+
+ initiateRemoteFetch();
+ }
+
+ void EntityLoader::initiateRemoteFetch()
+ {
+ setStatus(tr("Downloading metadata: %1").arg(m_target->cacheFilePath()));
+
+ m_netTask = NetJob::Ptr(new NetJob(tr("Fetch %1").arg(m_target->cacheFilePath()), APPLICATION->network()));
+
+ auto cacheEntry = APPLICATION->metacache()->resolveEntry("meta", m_target->cacheFilePath());
+ cacheEntry->setStale(true);
+
+ auto download = Net::ApiDownload::makeCached(m_target->remoteUrl(), cacheEntry);
+
+ // Add checksum validator if available
+ if (!m_target->m_expectedSha256.isEmpty())
+ {
+ download->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha256, m_target->m_expectedSha256));
+ }
+
+ // Add JSON parsing validator
+ download->addValidator(new JsonParseValidator(m_target));
+
+ m_netTask->addNetAction(download);
+ m_netTask->setAskRetry(false);
+
+ // Connect signals
+ connect(m_netTask.get(), &Task::succeeded, this, [this]() { finalizeLoad(MetaEntity::State::Synchronized); });
+ connect(m_netTask.get(), &Task::failed, this, &EntityLoader::handleRemoteFailure);
+ connect(m_netTask.get(), &Task::progress, this, &Task::setProgress);
+ connect(m_netTask.get(), &Task::stepProgress, this, &EntityLoader::propagateStepProgress);
+ connect(m_netTask.get(), &Task::status, this, &Task::setStatus);
+ connect(m_netTask.get(), &Task::details, this, &Task::setDetails);
+
+ m_netTask->start();
+ }
+
+ bool EntityLoader::loadCachedMetadata(const QString& cachePath, QString* errorOut)
+ {
+ try
+ {
+ setStatus(tr("Loading cached metadata"));
+
+ QByteArray content = FS::read(cachePath);
+ m_target->m_actualSha256 = Hashing::hash(content, Hashing::Algorithm::Sha256);
+
+ if (m_target->m_state == MetaEntity::State::Pending)
+ {
+ QJsonDocument doc = Json::requireDocument(content, cachePath);
+ QJsonObject root = Json::requireObject(doc, cachePath);
+ m_target->loadFromJson(root);
+ }
+
+ return true;
+ }
+ catch (const Exception& ex)
+ {
+ if (errorOut)
+ *errorOut = ex.cause();
+ return false;
+ }
+ }
+
+ void EntityLoader::handleRemoteFailure(const QString& reason)
+ {
+ if (!m_canUseLocalFallback)
+ {
+ emitFailed(reason);
+ return;
+ }
+
+ QString cachePath = QDir("meta").absoluteFilePath(m_target->cacheFilePath());
+ QString cacheError;
+ if (QFile::exists(cachePath) && loadCachedMetadata(cachePath, &cacheError))
+ {
+ m_target->m_state = MetaEntity::State::Cached;
+ emitSucceeded();
+ qWarning() << "Remote metadata refresh failed for" << m_target->cacheFilePath()
+ << "- falling back to cached metadata:" << reason;
+ return;
+ }
+
+ if (!cacheError.isEmpty())
+ qWarning() << "Cached fallback also failed for" << cachePath << ":" << cacheError;
+
+ emitFailed(reason);
+ }
+
+ void EntityLoader::finalizeLoad(MetaEntity::State newState)
+ {
+ m_target->m_state = newState;
+ if (newState == MetaEntity::State::Synchronized)
+ {
+ m_target->m_actualSha256 = m_target->m_expectedSha256;
+ }
+ emitSucceeded();
+ }
+
+ bool EntityLoader::canAbort() const
+ {
+ return m_netTask ? m_netTask->canAbort() : false;
+ }
+
+ bool EntityLoader::abort()
+ {
+ if (m_netTask)
+ {
+ Task::abort();
+ return m_netTask->abort();
+ }
+ return Task::abort();
+ }
+
+} // namespace projt::meta