/* 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();
}