/* SPDX-FileCopyrightText: 2026 Project Tick * SPDX-FileContributor: Project Tick * SPDX-License-Identifier: GPL-3.0-or-later * * MeshMC - A Custom Launcher for Minecraft * 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, either version 3 of the License, or * (at your option) any later version. * * 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 "JavaDownloadTask.h" #include #include #include #include #include #include "Application.h" #include "FileSystem.h" #include "Json.h" #include "net/Download.h" #include "net/ChecksumValidator.h" #include "MMCZip.h" JavaDownloadTask::JavaDownloadTask(const JavaDownload::RuntimeEntry& runtime, const QString& targetDir, QObject* parent) : Task(parent), m_runtime(runtime), m_targetDir(targetDir) { } void JavaDownloadTask::executeTask() { if (m_runtime.downloadType == "manifest") { downloadManifest(); } else { downloadArchive(); } } void JavaDownloadTask::downloadArchive() { setStatus(tr("Downloading %1...").arg(m_runtime.name)); // Determine archive extension and path QUrl url(m_runtime.url); QString filename = url.fileName(); m_archivePath = FS::PathCombine(m_targetDir, filename); // Create target directory if (!FS::ensureFolderPathExists(m_targetDir)) { emitFailed( tr("Failed to create target directory: %1").arg(m_targetDir)); return; } m_downloadJob = new NetJob(tr("Java download"), APPLICATION->network()); auto dl = Net::Download::makeFile(m_runtime.url, m_archivePath); // Add checksum validation if (!m_runtime.checksumHash.isEmpty()) { QCryptographicHash::Algorithm algo = QCryptographicHash::Sha256; if (m_runtime.checksumType == "sha1") algo = QCryptographicHash::Sha1; auto validator = new Net::ChecksumValidator( algo, QByteArray::fromHex(m_runtime.checksumHash.toLatin1())); dl->addValidator(validator); } m_downloadJob->addNetAction(dl); connect(m_downloadJob.get(), &NetJob::succeeded, this, &JavaDownloadTask::downloadFinished); connect(m_downloadJob.get(), &NetJob::failed, this, &JavaDownloadTask::downloadFailed); connect(m_downloadJob.get(), &NetJob::progress, this, &Task::setProgress); m_downloadJob->start(); } void JavaDownloadTask::downloadFinished() { m_downloadJob.reset(); extractArchive(); } void JavaDownloadTask::downloadFailed(QString reason) { m_downloadJob.reset(); // Clean up partial download if (!m_archivePath.isEmpty() && !QFile::remove(m_archivePath)) qWarning() << "Failed to remove partial download:" << m_archivePath; emitFailed(tr("Failed to download Java: %1").arg(reason)); } void JavaDownloadTask::extractArchive() { setStatus(tr("Extracting %1...").arg(m_runtime.name)); bool success = false; if (m_archivePath.endsWith(".zip")) { // Use QuaZip for zip files auto result = MMCZip::extractDir(m_archivePath, m_targetDir); success = result.has_value(); } else if (m_archivePath.endsWith(".tar.gz") || m_archivePath.endsWith(".tgz")) { // Use system tar for tar.gz files QProcess tarProcess; tarProcess.setWorkingDirectory(m_targetDir); tarProcess.start("tar", QStringList() << "xzf" << m_archivePath); tarProcess.waitForFinished(300000); // 5 minute timeout success = (tarProcess.exitCode() == 0 && tarProcess.exitStatus() == QProcess::NormalExit); if (!success) { qWarning() << "tar extraction failed:" << tarProcess.readAllStandardError(); } } else { if (!QFile::remove(m_archivePath)) qWarning() << "Failed to remove archive:" << m_archivePath; emitFailed(tr("Unsupported archive format: %1").arg(m_archivePath)); return; } // Clean up archive file if (!QFile::remove(m_archivePath)) qWarning() << "Failed to remove archive:" << m_archivePath; if (!success) { emitFailed(tr("Failed to extract Java archive.")); return; } // Fix permissions on extracted files QDirIterator it(m_targetDir, QDirIterator::Subdirectories); while (it.hasNext()) { auto filepath = it.next(); QFileInfo file(filepath); auto permissions = QFile::permissions(filepath); if (file.isDir()) { permissions |= QFileDevice::ReadUser | QFileDevice::WriteUser | QFileDevice::ExeUser; } else { permissions |= QFileDevice::ReadUser | QFileDevice::WriteUser; } QFile::setPermissions(filepath, permissions); } // Find the java binary m_installedJavaPath = findJavaBinary(m_targetDir); if (m_installedJavaPath.isEmpty()) { emitFailed(tr("Could not find java binary in extracted archive.")); return; } // Make java binary executable auto perms = QFile::permissions(m_installedJavaPath) | QFileDevice::ExeUser | QFileDevice::ExeGroup | QFileDevice::ExeOther; QFile::setPermissions(m_installedJavaPath, perms); qDebug() << "Java installed successfully at:" << m_installedJavaPath; emitSucceeded(); } void JavaDownloadTask::downloadManifest() { setStatus(tr("Downloading manifest for %1...").arg(m_runtime.name)); if (!FS::ensureFolderPathExists(m_targetDir)) { emitFailed( tr("Failed to create target directory: %1").arg(m_targetDir)); return; } m_downloadJob = new NetJob(tr("Java manifest download"), APPLICATION->network()); auto dl = Net::Download::makeByteArray(QUrl(m_runtime.url), &m_manifestData); if (m_runtime.checksumType == "sha1" && !m_runtime.checksumHash.isEmpty()) { dl->addValidator(new Net::ChecksumValidator( QCryptographicHash::Sha1, QByteArray::fromHex(m_runtime.checksumHash.toLatin1()))); } m_downloadJob->addNetAction(dl); connect(m_downloadJob.get(), &NetJob::succeeded, this, &JavaDownloadTask::manifestDownloaded); connect(m_downloadJob.get(), &NetJob::failed, this, &JavaDownloadTask::downloadFailed); m_downloadJob->start(); } void JavaDownloadTask::manifestDownloaded() { m_downloadJob.reset(); QJsonDocument doc; try { doc = Json::requireDocument(m_manifestData); } catch (const Exception& e) { m_manifestData.clear(); emitFailed(tr("Failed to parse Java manifest: %1").arg(e.cause())); return; } m_manifestData.clear(); if (!doc.isObject()) { emitFailed(tr("Failed to parse Java manifest.")); return; } auto files = doc.object()["files"].toObject(); m_executableFiles.clear(); m_linkEntries.clear(); // Create directories first for (auto it = files.begin(); it != files.end(); ++it) { auto entry = it.value().toObject(); if (entry["type"].toString() == "directory") { QDir().mkpath(FS::PathCombine(m_targetDir, it.key())); } } // Queue file downloads setStatus(tr("Downloading %1 files...").arg(m_runtime.name)); m_downloadJob = new NetJob(tr("Java runtime files"), APPLICATION->network()); for (auto it = files.begin(); it != files.end(); ++it) { auto entry = it.value().toObject(); if (entry["type"].toString() == "file") { auto downloads = entry["downloads"].toObject(); auto raw = downloads["raw"].toObject(); QString url = raw["url"].toString(); QString sha1 = raw["sha1"].toString(); QString filePath = FS::PathCombine(m_targetDir, it.key()); // Ensure parent directory exists QFileInfo fi(filePath); QDir().mkpath(fi.absolutePath()); auto dl = Net::Download::makeFile(QUrl(url), filePath); if (!sha1.isEmpty()) { dl->addValidator(new Net::ChecksumValidator( QCryptographicHash::Sha1, QByteArray::fromHex(sha1.toLatin1()))); } m_downloadJob->addNetAction(dl); if (entry["executable"].toBool()) { m_executableFiles.append(filePath); } } else if (entry["type"].toString() == "link") { m_linkEntries.append({it.key(), entry["target"].toString()}); } } connect(m_downloadJob.get(), &NetJob::succeeded, this, &JavaDownloadTask::manifestFilesDownloaded); connect(m_downloadJob.get(), &NetJob::failed, this, &JavaDownloadTask::downloadFailed); connect(m_downloadJob.get(), &NetJob::progress, this, &Task::setProgress); m_downloadJob->start(); } void JavaDownloadTask::manifestFilesDownloaded() { m_downloadJob.reset(); // Create symlinks for (const auto& link : m_linkEntries) { QString linkPath = FS::PathCombine(m_targetDir, link.first); QFileInfo fi(linkPath); QDir().mkpath(fi.absolutePath()); QFile::link(link.second, linkPath); } // Set executable permissions for (const auto& path : m_executableFiles) { QFile::setPermissions( path, QFile::permissions(path) | QFileDevice::ExeUser | QFileDevice::ExeGroup | QFileDevice::ExeOther); } // Find java binary m_installedJavaPath = findJavaBinary(m_targetDir); if (m_installedJavaPath.isEmpty()) { emitFailed(tr("Could not find java binary in downloaded runtime.")); return; } qDebug() << "Java installed successfully at:" << m_installedJavaPath; emitSucceeded(); } QString JavaDownloadTask::findJavaBinary(const QString& dir) const { #if defined(Q_OS_WIN) QString binaryName = "javaw.exe"; #else QString binaryName = "java"; #endif // Search for java binary in bin/ subdirectories QDirIterator it(dir, QStringList() << binaryName, QDir::Files, QDirIterator::Subdirectories); while (it.hasNext()) { it.next(); QString path = it.filePath(); if (path.contains("/bin/")) { return path; } } // Fallback: any match QDirIterator it2(dir, QStringList() << binaryName, QDir::Files, QDirIterator::Subdirectories); if (it2.hasNext()) { it2.next(); return it2.filePath(); } return QString(); }