diff options
37 files changed, 1045 insertions, 503 deletions
diff --git a/.github/actions/setup-dependencies/linux/action.yml b/.github/actions/setup-dependencies/linux/action.yml index 1f89c85b07..53b6284d68 100644 --- a/.github/actions/setup-dependencies/linux/action.yml +++ b/.github/actions/setup-dependencies/linux/action.yml @@ -18,7 +18,7 @@ runs: dpkg-dev \ ninja-build extra-cmake-modules pkg-config scdoc \ cmark gamemode-dev libarchive-dev libcmark-dev libqrencode-dev zlib1g-dev \ - libxcb-cursor-dev libtomlplusplus-dev libvulkan-dev libquazip1-qt6-dev + libxcb-cursor-dev libtomlplusplus-dev libvulkan-dev libarchive - name: Setup AppImage tooling shell: bash diff --git a/.github/actions/setup-dependencies/macos/action.yml b/.github/actions/setup-dependencies/macos/action.yml index f37f131411..3eb5b37fa6 100644 --- a/.github/actions/setup-dependencies/macos/action.yml +++ b/.github/actions/setup-dependencies/macos/action.yml @@ -17,7 +17,7 @@ runs: shell: bash run: | brew update - brew install ninja extra-cmake-modules temurin@17 mono quazip autoconf + brew install ninja extra-cmake-modules temurin@17 mono autoconf libarchive - name: Set JAVA_HOME shell: bash diff --git a/.github/actions/setup-dependencies/windows/action.yml b/.github/actions/setup-dependencies/windows/action.yml index 9434d7929e..a06b38d389 100644 --- a/.github/actions/setup-dependencies/windows/action.yml +++ b/.github/actions/setup-dependencies/windows/action.yml @@ -83,7 +83,7 @@ runs: qt6-svg:p qt6-imageformats:p qt6-networkauth:p - quazip-qt6:p + qt6-5compat:p cmark:p qrencode:p tomlplusplus:p diff --git a/CMakeLists.txt b/CMakeLists.txt index 442756bf8c..21a7378f59 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -113,7 +113,7 @@ find_package(Qt6 REQUIRED COMPONENTS Xml ) -find_package(QuaZip-Qt6 REQUIRED) +find_package(LibArchive REQUIRED) set(CMAKE_POSITION_INDEPENDENT_CODE ON) ####################################### Branding ####################################### @@ -143,6 +143,7 @@ set(MeshMC_Git "https://github.com/Project-Tick/MeshMC") set(MeshMC_SVGFileName "${MeshMC_AppID}.svg") set(MeshMC_Branding_ICNS "branding/${MeshMC_AppID}.icns") +set(MeshMC_Branding_ICO "${MeshMC_AppID}.ico") set(MeshMC_Branding_WindowsRC "branding/${MeshMC_AppBinaryName}.rc") set(MeshMC_Branding_LogoQRC "branding/${MeshMC_AppBinaryName}.qrc") diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 9159fa8e37..c762f8211c 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -807,9 +807,28 @@ void Application::initSettings() m_settings->registerSetting({"ProxyUser", "ProxyUsername"}, ""); m_settings->registerSetting({"ProxyPass", "ProxyPassword"}, ""); - // Memory - m_settings->registerSetting({"MinMemAlloc", "MinMemoryAlloc"}, 512); - m_settings->registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, 1024); + // Memory — compute reasonable defaults based on system RAM + int defaultMinMem = 512; + int defaultMaxMem = 1024; + { + uint64_t systemRamMiB = Sys::getSystemRam() / Sys::mebibyte; + if (systemRamMiB >= 32768) { // 32+ GB + defaultMinMem = 1024; + defaultMaxMem = 8192; + } else if (systemRamMiB >= 16384) { // 16-32 GB + defaultMinMem = 1024; + defaultMaxMem = 6144; + } else if (systemRamMiB >= 8192) { // 8-16 GB + defaultMinMem = 512; + defaultMaxMem = 4096; + } else if (systemRamMiB >= 4096) { // 4-8 GB + defaultMinMem = 512; + defaultMaxMem = 2048; + } + // <4 GB: keep 512/1024 defaults + } + m_settings->registerSetting({"MinMemAlloc", "MinMemoryAlloc"}, defaultMinMem); + m_settings->registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, defaultMaxMem); m_settings->registerSetting("PermGen", 128); // Java Settings diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 026fc34ae9..99b61b9ea5 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -773,6 +773,8 @@ SET(MESHMC_SOURCES # GUI - dialogs ui/dialogs/AboutDialog.cpp ui/dialogs/AboutDialog.h + ui/dialogs/BlockedModsDialog.cpp + ui/dialogs/BlockedModsDialog.h ui/dialogs/ProfileSelectDialog.cpp ui/dialogs/ProfileSelectDialog.h ui/dialogs/ProfileSetupDialog.cpp @@ -958,11 +960,10 @@ target_link_libraries(MeshMC_logic Qt6::NetworkAuth Qt6::Concurrent Qt6::Gui - QuaZip::QuaZip + LibArchive::LibArchive ) target_link_libraries(MeshMC_logic MeshMC_iconfix - ${QUAZIP_LIBRARIES} hoedown MeshMC_rainbow LocalPeer diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index bcf205f6dd..72d38d1a51 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -54,11 +54,14 @@ #include "modplatform/flame/PackManifest.h" #include "modplatform/modrinth/ModrinthPackManifest.h" #include "Json.h" -#include <quazipdir.h> #include "modplatform/technic/TechnicPackProcessor.h" #include "icons/IconList.h" #include "Application.h" +#include "ui/dialogs/BlockedModsDialog.h" + +#include <QDir> +#include <QStandardPaths> InstanceImportTask::InstanceImportTask(const QUrl sourceUrl) { @@ -114,20 +117,13 @@ void InstanceImportTask::processZipPack() QDir extractDir(m_stagingPath); qDebug() << "Attempting to create instance from" << m_archivePath; - // open the zip and find relevant files in it - m_packZip.reset(new QuaZip(m_archivePath)); - if (!m_packZip->open(QuaZip::mdUnzip)) - { - emitFailed(tr("Unable to open supplied modpack zip file.")); - return; - } - + // find relevant files in the zip QStringList blacklist = {"instance.cfg", "manifest.json", "modrinth.index.json"}; - QString mmcFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg"); - bool technicFound = QuaZipDir(m_packZip.get()).exists("/bin/modpack.jar") || - QuaZipDir(m_packZip.get()).exists("/bin/version.json"); - QString flameFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json"); - QString modrinthFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "modrinth.index.json"); + QString mmcFound = MMCZip::findFolderOfFileInZip(m_archivePath, "instance.cfg"); + bool technicFound = MMCZip::entryExists(m_archivePath, "bin/modpack.jar") || + MMCZip::entryExists(m_archivePath, "bin/version.json"); + QString flameFound = MMCZip::findFolderOfFileInZip(m_archivePath, "manifest.json"); + QString modrinthFound = MMCZip::findFolderOfFileInZip(m_archivePath, "modrinth.index.json"); QString root; if(!mmcFound.isNull()) { @@ -165,9 +161,10 @@ void InstanceImportTask::processZipPack() } // make sure we extract just the pack + QString archivePath = m_archivePath; m_extractFuture = QtConcurrent::run( QThreadPool::globalInstance(), MMCZip::extractSubDir, - m_packZip.get(), root, extractDir.absolutePath()); + archivePath, root, extractDir.absolutePath()); connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &InstanceImportTask::extractFinished); @@ -179,7 +176,6 @@ void InstanceImportTask::processZipPack() void InstanceImportTask::extractFinished() { - m_packZip.reset(); if (!m_extractFuture.result()) { emitFailed(tr("Failed to extract modpack")); @@ -419,6 +415,10 @@ void InstanceImportTask::onFlameFileResolutionSucceeded() { auto results = m_modIdResolver->getResults(); m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network()); + + // Collect restricted mods that need browser download + QList<BlockedMod> blockedMods; + for(auto result: results.files) { QString filename = result.fileName; @@ -441,9 +441,16 @@ void InstanceImportTask::onFlameFileResolutionSucceeded() [[fallthrough]]; case Flame::File::Type::Mod: { - if(!result.resolved || !result.url.isValid() || result.url.isEmpty()) + bool isBlocked = !result.resolved || !result.url.isValid() || result.url.isEmpty(); + if (isBlocked && !result.fileName.isEmpty()) + { + blockedMods.append({result.projectId, result.fileId, result.fileName, path, false}); + break; + } + if (isBlocked) { - logWarning(tr("Skipping %1 - no download URL available (mod may have restricted downloads)").arg(result.fileName)); + logWarning(tr("Skipping mod %1 (project %2) - no download URL and no filename available") + .arg(result.fileId).arg(result.projectId)); break; } qDebug() << "Will download" << result.url << "to" << path; @@ -463,17 +470,56 @@ void InstanceImportTask::onFlameFileResolutionSucceeded() break; } } + + // Handle restricted mods via dialog + if (!blockedMods.isEmpty()) + { + BlockedModsDialog dlg(nullptr, + tr("Restricted Mods"), + tr("The following mods have restricted downloads and are not available through the API.\n" + "Click the Download button next to each mod to open its download page in your browser.\n" + "Once all files appear in your Downloads folder, click Continue."), + blockedMods); + + if (dlg.exec() == QDialog::Accepted) + { + QString downloadDir = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); + for (const auto &mod : blockedMods) + { + if (mod.found) + { + QString srcPath = FS::PathCombine(downloadDir, mod.fileName); + QFileInfo targetInfo(mod.targetPath); + QDir().mkpath(targetInfo.absolutePath()); + + if (QFile::copy(srcPath, mod.targetPath)) + { + qDebug() << "Copied restricted mod:" << mod.fileName; + } + else + { + logWarning(tr("Failed to copy %1 from downloads folder").arg(mod.fileName)); + } + } + } + } + else + { + logWarning(tr("User cancelled restricted mod downloads - %1 mod(s) will be missing").arg(blockedMods.size())); + } + } + m_modIdResolver.reset(); connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() { - m_filesNetJob.reset(); emitSucceeded(); + m_filesNetJob.reset(); } ); connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) { - m_filesNetJob.reset(); emitFailed(reason); + m_filesNetJob.reset(); }); connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) { @@ -610,13 +656,13 @@ void InstanceImportTask::processModrinth() connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() { - m_filesNetJob.reset(); emitSucceeded(); + m_filesNetJob.reset(); }); connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) { - m_filesNetJob.reset(); emitFailed(reason); + m_filesNetJob.reset(); }); connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) { diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index 9748c6dbea..052070ea2c 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -48,7 +48,6 @@ #include <nonstd/optional> -class QuaZip; namespace Flame { class FileResolvingTask; @@ -87,7 +86,6 @@ private: /* data */ QUrl m_sourceUrl; QString m_archivePath; bool m_downloadRequired = false; - std::unique_ptr<QuaZip> m_packZip; QFuture<nonstd::optional<QStringList>> m_extractFuture; QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher; enum class ModpackType{ diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index 7870386518..b5d19ba177 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -36,29 +36,121 @@ * limitations under the License. */ -#include <quazip.h> -#include <quazipdir.h> -#include <quazipfile.h> -#include <JlCompress.h> #include "MMCZip.h" #include "FileSystem.h" +#include <archive.h> +#include <archive_entry.h> + #include <QDebug> +#include <QDir> #include <QDirIterator> +#include <QFile> -bool MMCZip::compressDir(QString zipFile, QString dir, FilterFunction excludeFilter) +#include <memory> + +// RAII helpers for libarchive +struct ArchiveReadDeleter { + void operator()(struct archive *a) const { archive_read_free(a); } // NOLINT(readability-identifier-naming) +}; +struct ArchiveWriteDeleter { + void operator()(struct archive *a) const { archive_write_free(a); } // NOLINT(readability-identifier-naming) +}; +using ArchiveReadPtr = std::unique_ptr<struct archive, ArchiveReadDeleter>; // NOLINT(readability-identifier-naming) +using ArchiveWritePtr = std::unique_ptr<struct archive, ArchiveWriteDeleter>; // NOLINT(readability-identifier-naming) + +static ArchiveReadPtr openZipForReading(const QString &path) { - QuaZip zip(zipFile); - if (!zip.open(QuaZip::mdCreate)) - { + ArchiveReadPtr a(archive_read_new()); + archive_read_support_format_zip(a.get()); + if (archive_read_open_filename(a.get(), path.toUtf8().constData(), 10240) != ARCHIVE_OK) { + qWarning() << "Could not open archive:" << path << archive_error_string(a.get()); + return nullptr; + } + return a; +} + +static ArchiveWritePtr createZipForWriting(const QString &path) +{ + ArchiveWritePtr a(archive_write_new()); + archive_write_set_format_zip(a.get()); + if (archive_write_open_filename(a.get(), path.toUtf8().constData()) != ARCHIVE_OK) { + qWarning() << "Could not create archive:" << path << archive_error_string(a.get()); + return nullptr; + } + return a; +} + +static bool copyArchiveData(struct archive *ar, struct archive *aw) // NOLINT(readability-identifier-naming) +{ + const void *buff; + size_t size; + la_int64_t offset; + int r; + while ((r = archive_read_data_block(ar, &buff, &size, &offset)) == ARCHIVE_OK) { + if (archive_write_data_block(aw, buff, size, offset) != ARCHIVE_OK) + return false; + } + return r == ARCHIVE_EOF; +} + +static bool writeFileToArchive(struct archive *aw, const QString &entryName, // NOLINT(readability-identifier-naming) + const QByteArray &data) +{ + struct archive_entry *entry = archive_entry_new(); + archive_entry_set_pathname(entry, entryName.toUtf8().constData()); + archive_entry_set_size(entry, data.size()); + archive_entry_set_filetype(entry, AE_IFREG); + archive_entry_set_perm(entry, 0644); + + if (archive_write_header(aw, entry) != ARCHIVE_OK) { + archive_entry_free(entry); return false; } + if (data.size() > 0) { + archive_write_data(aw, data.constData(), data.size()); + } + archive_entry_free(entry); + return true; +} + +static bool writeDiskEntry(struct archive *ar, const QString &absFilePath) +{ + // Ensure parent directory exists + QFileInfo fi(absFilePath); + QDir().mkpath(fi.absolutePath()); + + if (absFilePath.endsWith('/')) { + // Directory entry + QDir().mkpath(absFilePath); + return true; + } + + QFile outFile(absFilePath); + if (!outFile.open(QIODevice::WriteOnly)) { + qWarning() << "Failed to open for writing:" << absFilePath; + return false; + } + + const void *buff; + size_t size; + la_int64_t offset; + while (archive_read_data_block(ar, &buff, &size, &offset) == ARCHIVE_OK) { + outFile.write(static_cast<const char*>(buff), size); + } + outFile.close(); + return true; +} + +bool MMCZip::compressDir(QString zipFile, QString dir, FilterFunction excludeFilter) +{ + auto aw = createZipForWriting(zipFile); + if (!aw) + return false; QDir directory(dir); if (!directory.exists()) - { return false; - } QDirIterator it(dir, QDir::Files | QDir::Hidden, QDirIterator::Subdirectories); bool success = true; @@ -67,309 +159,490 @@ bool MMCZip::compressDir(QString zipFile, QString dir, FilterFunction excludeFil it.next(); QString relPath = directory.relativeFilePath(it.filePath()); if (excludeFilter && excludeFilter(relPath)) - { continue; + + QFile file(it.filePath()); + if (!file.open(QIODevice::ReadOnly)) { + success = false; + break; } - if (!JlCompress::compressFile(&zip, it.filePath(), relPath)) - { + QByteArray data = file.readAll(); + file.close(); + + if (!writeFileToArchive(aw.get(), relPath, data)) { success = false; break; } } - zip.close(); - if (zip.getZipError() != 0) - { - return false; - } + archive_write_close(aw.get()); return success; } -// ours -bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &contained, const FilterFunction filter) +bool MMCZip::mergeZipFiles(const QString &intoPath, QFileInfo from, + QSet<QString> &contained, const FilterFunction filter) { - QuaZip modZip(from.filePath()); - modZip.open(QuaZip::mdUnzip); + // Read all entries from source zip + auto ar = openZipForReading(from.filePath()); + if (!ar) + return false; - QuaZipFile fileInsideMod(&modZip); - QuaZipFile zipOutFile(into); - for (bool more = modZip.goToFirstFile(); more; more = modZip.goToNextFile()) - { - QString filename = modZip.getCurrentFileName(); - if (filter && !filter(filename)) - { - qDebug() << "Skipping file " << filename << " from " - << from.fileName() << " - filtered"; + // Read existing entries from target if it exists, then append new ones + // We build a list of entries to write + struct EntryData { + QString name; + QByteArray data; + }; + QList<EntryData> newEntries; + + struct archive_entry *entry; + while (archive_read_next_header(ar.get(), &entry) == ARCHIVE_OK) { + QString filename = QString::fromUtf8(archive_entry_pathname(entry)); + if (filter && !filter(filename)) { + qDebug() << "Skipping file " << filename << " from " << from.fileName() << " - filtered"; + archive_read_data_skip(ar.get()); continue; } - if (contained.contains(filename)) - { - qDebug() << "Skipping already contained file " << filename << " from " - << from.fileName(); + if (contained.contains(filename)) { + qDebug() << "Skipping already contained file " << filename << " from " << from.fileName(); + archive_read_data_skip(ar.get()); continue; } contained.insert(filename); - if (!fileInsideMod.open(QIODevice::ReadOnly)) - { - qCritical() << "Failed to open " << filename << " from " << from.fileName(); - return false; + // Read data + la_int64_t entrySize = archive_entry_size(entry); + QByteArray data; + if (entrySize > 0) { + data.resize(entrySize); + la_ssize_t bytesRead = archive_read_data(ar.get(), data.data(), entrySize); + if (bytesRead < 0) { + qCritical() << "Failed to read " << filename << " from " << from.fileName(); + return false; + } + data.resize(bytesRead); } + newEntries.append({filename, data}); + } - QuaZipNewInfo info_out(fileInsideMod.getActualFileName()); + // Now append to existing zip (or create new one) + // libarchive doesn't support append, so we need to read existing + write all + QList<EntryData> existingEntries; + if (QFile::exists(intoPath)) { + auto existingAr = openZipForReading(intoPath); + if (existingAr) { + while (archive_read_next_header(existingAr.get(), &entry) == ARCHIVE_OK) { + QString name = QString::fromUtf8(archive_entry_pathname(entry)); + la_int64_t sz = archive_entry_size(entry); + QByteArray data; + if (sz > 0) { + data.resize(sz); + archive_read_data(existingAr.get(), data.data(), sz); + } + existingEntries.append({name, data}); + } + } + } - if (!zipOutFile.open(QIODevice::WriteOnly, info_out)) - { - qCritical() << "Failed to open " << filename << " in the jar"; - fileInsideMod.close(); + auto aw = createZipForWriting(intoPath); + if (!aw) + return false; + + // Write existing entries + for (const auto &e : existingEntries) { + if (!writeFileToArchive(aw.get(), e.name, e.data)) { + qCritical() << "Failed to write existing entry " << e.name; return false; } - if (!JlCompress::copyData(fileInsideMod, zipOutFile)) - { - zipOutFile.close(); - fileInsideMod.close(); - qCritical() << "Failed to copy data of " << filename << " into the jar"; + } + + // Write new entries + for (const auto &e : newEntries) { + if (!writeFileToArchive(aw.get(), e.name, e.data)) { + qCritical() << "Failed to write " << e.name << " into the jar"; return false; } - zipOutFile.close(); - fileInsideMod.close(); } + + archive_write_close(aw.get()); return true; } -// ours bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod>& mods) { - QuaZip zipOut(targetJarPath); - if (!zipOut.open(QuaZip::mdCreate)) - { - QFile::remove(targetJarPath); - qCritical() << "Failed to open the minecraft.jar for modding"; - return false; - } - // Files already added to the jar. - // These files will be skipped. + // Files already added to the jar. These files will be skipped. QSet<QString> addedFiles; - // Modify the jar + // We collect all entries first, then write them all at once. + struct EntryData { + QString name; + QByteArray data; + }; + QList<EntryData> allEntries; + + // Modify the jar - process mods in reverse order QListIterator<Mod> i(mods); i.toBack(); while (i.hasPrevious()) { const Mod &mod = i.previous(); - // do not merge disabled mods. if (!mod.enabled()) continue; + if (mod.type() == Mod::MOD_ZIPFILE) { - if (!mergeZipFiles(&zipOut, mod.filename(), addedFiles)) - { - zipOut.close(); + auto ar = openZipForReading(mod.filename().absoluteFilePath()); + if (!ar) { QFile::remove(targetJarPath); qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar."; return false; } + struct archive_entry *entry; + while (archive_read_next_header(ar.get(), &entry) == ARCHIVE_OK) { + QString filename = QString::fromUtf8(archive_entry_pathname(entry)); + if (addedFiles.contains(filename)) { + archive_read_data_skip(ar.get()); + continue; + } + addedFiles.insert(filename); + la_int64_t sz = archive_entry_size(entry); + QByteArray data; + if (sz > 0) { + data.resize(sz); + archive_read_data(ar.get(), data.data(), sz); + } + allEntries.append({filename, data}); + } } else if (mod.type() == Mod::MOD_SINGLEFILE) { - // FIXME: buggy - does not work with addedFiles auto filename = mod.filename(); - if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) - { - zipOut.close(); + QFile file(filename.absoluteFilePath()); + if (!file.open(QIODevice::ReadOnly)) { QFile::remove(targetJarPath); - qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar."; + qCritical() << "Failed to add" << filename.fileName() << "to the jar."; return false; } + allEntries.append({filename.fileName(), file.readAll()}); addedFiles.insert(filename.fileName()); } else if (mod.type() == Mod::MOD_FOLDER) { - // FIXME: buggy - does not work with addedFiles auto filename = mod.filename(); QString what_to_zip = filename.absoluteFilePath(); QDir dir(what_to_zip); dir.cdUp(); QString parent_dir = dir.absolutePath(); - if (!JlCompress::compressSubDir(&zipOut, what_to_zip, parent_dir, true, QDir::NoFilter)) - { - zipOut.close(); - QFile::remove(targetJarPath); - qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar."; - return false; + QDirIterator it(what_to_zip, QDir::Files | QDir::Hidden, QDirIterator::Subdirectories); + while (it.hasNext()) { + it.next(); + QString relPath = QDir(parent_dir).relativeFilePath(it.filePath()); + QFile f(it.filePath()); + if (f.open(QIODevice::ReadOnly)) { + allEntries.append({relPath, f.readAll()}); + } } - qDebug() << "Adding folder " << filename.fileName() << " from " - << filename.absoluteFilePath(); + qDebug() << "Adding folder " << filename.fileName() << " from " << filename.absoluteFilePath(); } else { - // Make sure we do not continue launching when something is missing or undefined... - zipOut.close(); QFile::remove(targetJarPath); qCritical() << "Failed to add unknown mod type" << mod.filename().fileName() << "to the jar."; return false; } } - if (!mergeZipFiles(&zipOut, QFileInfo(sourceJarPath), addedFiles, [](const QString key){return !key.contains("META-INF");})) - { - zipOut.close(); + // Add source jar contents (skip META-INF and already added files) + auto ar = openZipForReading(sourceJarPath); + if (!ar) { QFile::remove(targetJarPath); qCritical() << "Failed to insert minecraft.jar contents."; return false; } + struct archive_entry *entry; + while (archive_read_next_header(ar.get(), &entry) == ARCHIVE_OK) { + QString filename = QString::fromUtf8(archive_entry_pathname(entry)); + if (filename.contains("META-INF")) { + archive_read_data_skip(ar.get()); + continue; + } + if (addedFiles.contains(filename)) { + archive_read_data_skip(ar.get()); + continue; + } + la_int64_t sz = archive_entry_size(entry); + QByteArray data; + if (sz > 0) { + data.resize(sz); + archive_read_data(ar.get(), data.data(), sz); + } + allEntries.append({filename, data}); + } - // Recompress the jar - zipOut.close(); - if (zipOut.getZipError() != 0) - { + // Write the final jar + auto aw = createZipForWriting(targetJarPath); + if (!aw) { QFile::remove(targetJarPath); - qCritical() << "Failed to finalize minecraft.jar!"; + qCritical() << "Failed to open the minecraft.jar for modding"; return false; } + for (const auto &e : allEntries) { + if (!writeFileToArchive(aw.get(), e.name, e.data)) { + archive_write_close(aw.get()); + (void)QFile::remove(targetJarPath); + qCritical() << "Failed to finalize minecraft.jar!"; + return false; + } + } + archive_write_close(aw.get()); return true; } -// ours -QString MMCZip::findFolderOfFileInZip(QuaZip * zip, const QString & what, const QString &root) +QString MMCZip::findFolderOfFileInZip(const QString &zipPath, const QString & what, const QString &root) { - QuaZipDir rootDir(zip, root); - for(auto fileName: rootDir.entryList(QDir::Files)) - { - if(fileName == what) + auto entries = listEntries(zipPath, root, QDir::Files); + for (const auto &fileName : entries) { + if (fileName == what) return root; } - for(auto fileName: rootDir.entryList(QDir::Dirs)) - { - QString result = findFolderOfFileInZip(zip, what, root + fileName); - if(!result.isEmpty()) - { + auto dirs = listEntries(zipPath, root, QDir::Dirs); + for (const auto &dirName : dirs) { + QString result = findFolderOfFileInZip(zipPath, what, root + dirName); + if (!result.isEmpty()) return result; - } } return QString(); } -// ours -bool MMCZip::findFilesInZip(QuaZip * zip, const QString & what, QStringList & result, const QString &root) +bool MMCZip::findFilesInZip(const QString &zipPath, const QString & what, QStringList & result, const QString &root) { - QuaZipDir rootDir(zip, root); - for(auto fileName: rootDir.entryList(QDir::Files)) - { - if(fileName == what) - { + auto entries = listEntries(zipPath, root, QDir::Files); + for (const auto &fileName : entries) { + if (fileName == what) { result.append(root); return true; } } - for(auto fileName: rootDir.entryList(QDir::Dirs)) - { - findFilesInZip(zip, what, result, root + fileName); + auto dirs = listEntries(zipPath, root, QDir::Dirs); + for (const auto &dirName : dirs) { + (void)findFilesInZip(zipPath, what, result, root + dirName); } return !result.isEmpty(); } - -// ours -nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target) +nonstd::optional<QStringList> MMCZip::extractSubDir(const QString &zipPath, const QString & subdir, const QString &target) { QDir directory(target); QStringList extracted; - qDebug() << "Extracting subdir" << subdir << "from" << zip->getZipName() << "to" << target; - auto numEntries = zip->getEntriesCount(); - if(numEntries < 0) { - qWarning() << "Failed to enumerate files in archive"; - return nonstd::nullopt; - } - else if(numEntries == 0) { - qDebug() << "Extracting empty archives seems odd..."; - return extracted; - } - else if (!zip->goToFirstFile()) - { - qWarning() << "Failed to seek to first file in zip"; + qDebug() << "Extracting subdir" << subdir << "from" << zipPath << "to" << target; + + auto ar = openZipForReading(zipPath); + if (!ar) { + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(zipPath); + if (fileInfo.size() == 22) { + return extracted; + } + qWarning() << "Failed to open archive:" << zipPath; return nonstd::nullopt; } - do - { - QString name = zip->getCurrentFileName(); - if(!name.startsWith(subdir)) - { + struct archive_entry *entry; + bool hasEntries = false; + while (archive_read_next_header(ar.get(), &entry) == ARCHIVE_OK) { + hasEntries = true; + QString name = QString::fromUtf8(archive_entry_pathname(entry)); + if (!name.startsWith(subdir)) { + archive_read_data_skip(ar.get()); continue; } - name.remove(0, subdir.size()); - QString absFilePath = directory.absoluteFilePath(name); - if(name.isEmpty()) - { + QString relName = name.mid(subdir.size()); + QString absFilePath = directory.absoluteFilePath(relName); + if (relName.isEmpty()) { absFilePath += "/"; } - if (!JlCompress::extractFile(zip, "", absFilePath)) - { + + if (!writeDiskEntry(ar.get(), absFilePath)) { qWarning() << "Failed to extract file" << name << "to" << absFilePath; - JlCompress::removeFile(extracted); + // Clean up extracted files + for (const auto &f : extracted) + (void)QFile::remove(f); return nonstd::nullopt; } extracted.append(absFilePath); - qDebug() << "Extracted file" << name; - } while (zip->goToNextFile()); + qDebug() << "Extracted file" << relName; + } + + if (!hasEntries) { + qDebug() << "Extracting empty archives seems odd..."; + } return extracted; } -// ours -bool MMCZip::extractRelFile(QuaZip *zip, const QString &file, const QString &target) +bool MMCZip::extractRelFile(const QString &zipPath, const QString &file, const QString &target) { - return JlCompress::extractFile(zip, file, target); + auto ar = openZipForReading(zipPath); + if (!ar) + return false; + + struct archive_entry *entry; + while (archive_read_next_header(ar.get(), &entry) == ARCHIVE_OK) { + QString name = QString::fromUtf8(archive_entry_pathname(entry)); + if (name == file) { + return writeDiskEntry(ar.get(), target); + } + archive_read_data_skip(ar.get()); + } + return false; } -// ours nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString dir) { - QuaZip zip(fileCompressed); - if (!zip.open(QuaZip::mdUnzip)) - { - // check if this is a minimum size empty zip file... - QFileInfo fileInfo(fileCompressed); - if(fileInfo.size() == 22) { - return QStringList(); - } - qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();; - return nonstd::nullopt; - } - return MMCZip::extractSubDir(&zip, "", dir); + return MMCZip::extractSubDir(fileCompressed, "", dir); } -// ours nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir) { - QuaZip zip(fileCompressed); - if (!zip.open(QuaZip::mdUnzip)) - { - // check if this is a minimum size empty zip file... - QFileInfo fileInfo(fileCompressed); - if(fileInfo.size() == 22) { - return QStringList(); + return MMCZip::extractSubDir(fileCompressed, subdir, dir); +} + +bool MMCZip::extractFile(QString fileCompressed, QString file, QString target) +{ + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if (fileInfo.size() == 22) { + return true; + } + return MMCZip::extractRelFile(fileCompressed, file, target); +} + +QByteArray MMCZip::readFileFromZip(const QString &zipPath, const QString &entryName) +{ + auto ar = openZipForReading(zipPath); + if (!ar) + return {}; + + struct archive_entry *entry; + while (archive_read_next_header(ar.get(), &entry) == ARCHIVE_OK) { + QString name = QString::fromUtf8(archive_entry_pathname(entry)); + if (name == entryName) { + la_int64_t sz = archive_entry_size(entry); + if (sz <= 0) { + // Size may be unknown; read in chunks + QByteArray result; + char buf[8192]; + la_ssize_t r; + while ((r = archive_read_data(ar.get(), buf, sizeof(buf))) > 0) + result.append(buf, r); + return result; + } + QByteArray data(sz, Qt::Uninitialized); + archive_read_data(ar.get(), data.data(), sz); + return data; } - qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();; - return nonstd::nullopt; + archive_read_data_skip(ar.get()); } - return MMCZip::extractSubDir(&zip, subdir, dir); + return {}; } -// ours -bool MMCZip::extractFile(QString fileCompressed, QString file, QString target) +bool MMCZip::entryExists(const QString &zipPath, const QString &entryName) { - QuaZip zip(fileCompressed); - if (!zip.open(QuaZip::mdUnzip)) - { - // check if this is a minimum size empty zip file... - QFileInfo fileInfo(fileCompressed); - if(fileInfo.size() == 22) { + auto ar = openZipForReading(zipPath); + if (!ar) + return false; + + struct archive_entry *entry; + while (archive_read_next_header(ar.get(), &entry) == ARCHIVE_OK) { + QString name = QString::fromUtf8(archive_entry_pathname(entry)); + if (name == entryName || name == entryName + "/") return true; + archive_read_data_skip(ar.get()); + } + return false; +} + +QStringList MMCZip::listEntries(const QString &zipPath) +{ + QStringList result; + auto ar = openZipForReading(zipPath); + if (!ar) + return result; + + struct archive_entry *entry; + while (archive_read_next_header(ar.get(), &entry) == ARCHIVE_OK) { + result.append(QString::fromUtf8(archive_entry_pathname(entry))); + archive_read_data_skip(ar.get()); + } + return result; +} + +QStringList MMCZip::listEntries(const QString &zipPath, const QString &dirPath, QDir::Filters type) +{ + QStringList result; + auto ar = openZipForReading(zipPath); + if (!ar) + return result; + + QSet<QString> seen; + struct archive_entry *entry; + while (archive_read_next_header(ar.get(), &entry) == ARCHIVE_OK) { + QString name = QString::fromUtf8(archive_entry_pathname(entry)); + + // Must start with dirPath + if (!name.startsWith(dirPath)) { + archive_read_data_skip(ar.get()); + continue; } - qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError(); - return false; + + QString relative = name.mid(dirPath.size()); + // Remove leading slashes + while (relative.startsWith('/')) + relative = relative.mid(1); + + if (relative.isEmpty()) { + archive_read_data_skip(ar.get()); + continue; + } + + int slashIdx = relative.indexOf('/'); + if (slashIdx < 0) { + // It's a file directly in dirPath + if (type & QDir::Files) { + if (!seen.contains(relative)) { + seen.insert(relative); + result.append(relative); + } + } + } else { + // It's inside a subdirectory + if (type & QDir::Dirs) { + QString dirName = relative.left(slashIdx + 1); // include trailing / + if (!seen.contains(dirName)) { + seen.insert(dirName); + result.append(dirName); + } + } + } + archive_read_data_skip(ar.get()); + } + return result; +} + +QDateTime MMCZip::getEntryModTime(const QString &zipPath, const QString &entryName) +{ + auto ar = openZipForReading(zipPath); + if (!ar) + return {}; + + struct archive_entry *entry; + while (archive_read_next_header(ar.get(), &entry) == ARCHIVE_OK) { + QString name = QString::fromUtf8(archive_entry_pathname(entry)); + if (name == entryName) { + time_t mtime = archive_entry_mtime(entry); + return QDateTime::fromSecsSinceEpoch(mtime); + } + archive_read_data_skip(ar.get()); } - return MMCZip::extractRelFile(&zip, file, target); + return {}; } diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h index d29cac13c9..a490793073 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -40,11 +40,12 @@ #include <QString> #include <QFileInfo> +#include <QDir> #include <QSet> +#include <QDateTime> #include "minecraft/mod/Mod.h" #include <functional> -#include <JlCompress.h> #include <nonstd/optional> namespace MMCZip @@ -52,43 +53,46 @@ namespace MMCZip using FilterFunction = std::function<bool(const QString &)>; /** - * Merge two zip files, using a filter function + * Merge two zip files, using a filter function. + * Reads entries from 'from' and writes them into the zip at 'intoPath'. + * 'contained' tracks already-added filenames to avoid duplicates. */ - bool mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &contained, - const FilterFunction filter = nullptr); + bool mergeZipFiles(const QString &intoPath, QFileInfo from, QSet<QString> &contained, + const FilterFunction filter = nullptr); /** - * take a source jar, add mods to it, resulting in target jar + * Take a source jar, add mods to it, resulting in target jar. */ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod>& mods); /** - * Find a single file in archive by file name (not path) - * + * Find a single file in archive by file name (not path). * \return the path prefix where the file is */ - QString findFolderOfFileInZip(QuaZip * zip, const QString & what, const QString &root = QString("")); + QString findFolderOfFileInZip(const QString &zipPath, const QString & what, const QString &root = QString("")); /** - * Find a multiple files of the same name in archive by file name - * If a file is found in a path, no deeper paths are searched - * + * Find multiple files of the same name in archive by file name. + * If a file is found in a path, no deeper paths are searched. * \return true if anything was found */ - bool findFilesInZip(QuaZip * zip, const QString & what, QStringList & result, const QString &root = QString()); + bool findFilesInZip(const QString &zipPath, const QString &what, + QStringList &result, const QString &root = QString()); /** - * Extract a subdirectory from an archive + * Compress a directory into a zip, using a filter function to exclude entries. */ + bool compressDir(QString zipFile, QString dir, FilterFunction excludeFilter); /** - * Compress a directory, using a filter function to exclude entries + * Extract a subdirectory from an archive. */ - bool compressDir(QString zipFile, QString dir, FilterFunction excludeFilter); - - nonstd::optional<QStringList> extractSubDir(QuaZip *zip, const QString & subdir, const QString &target); + nonstd::optional<QStringList> extractSubDir(const QString &zipPath, const QString & subdir, const QString &target); - bool extractRelFile(QuaZip *zip, const QString & file, const QString &target); + /** + * Extract a single file relative to the zip root. + */ + bool extractRelFile(const QString &zipPath, const QString & file, const QString &target); /** * Extract a whole archive. @@ -100,7 +104,7 @@ namespace MMCZip nonstd::optional<QStringList> extractDir(QString fileCompressed, QString dir); /** - * Extract a subdirectory from an archive + * Extract a subdirectory from an archive. * * \param fileCompressed The name of the archive. * \param subdir The directory within the archive to extract @@ -110,7 +114,7 @@ namespace MMCZip nonstd::optional<QStringList> extractDir(QString fileCompressed, QString subdir, QString dir); /** - * Extract a single file from an archive into a directory + * Extract a single file from an archive into a directory. * * \param fileCompressed The name of the archive. * \param file The file within the archive to extract @@ -119,4 +123,31 @@ namespace MMCZip */ bool extractFile(QString fileCompressed, QString file, QString dir); + /** + * Read a file's contents from inside a zip archive. + * \return the file data, or empty QByteArray on failure + */ + QByteArray readFileFromZip(const QString &zipPath, const QString &entryName); + + /** + * Check if a given entry path exists in a zip archive. + */ + bool entryExists(const QString &zipPath, const QString &entryName); + + /** + * List all entry names in a zip archive. + */ + QStringList listEntries(const QString &zipPath); + + /** + * List entries under a specific directory in a zip archive. + * \param type QDir::Files, QDir::Dirs, or both + */ + QStringList listEntries(const QString &zipPath, const QString &dirPath, + QDir::Filters type = QDir::Files | QDir::Dirs); + + /** + * Get the modification time of a specific entry in a zip archive. + */ + QDateTime getEntryModTime(const QString &zipPath, const QString &entryName); } diff --git a/launcher/launch/steps/Update.cpp b/launcher/launch/steps/Update.cpp index c9b9e3cee6..aa544c6c16 100644 --- a/launcher/launch/steps/Update.cpp +++ b/launcher/launch/steps/Update.cpp @@ -67,15 +67,15 @@ void Update::updateFinished() { if(m_updateTask->wasSuccessful()) { - m_updateTask.reset(); emitSucceeded(); + m_updateTask.reset(); } else { QString reason = tr("Instance update failed because: %1\n\n").arg(m_updateTask->failReason()); - m_updateTask.reset(); emit logLine(reason, MessageLevel::Fatal); emitFailed(reason); + m_updateTask.reset(); } } diff --git a/launcher/minecraft/MinecraftLoadAndCheck.h b/launcher/minecraft/MinecraftLoadAndCheck.h index 8a6db9963e..281dc16f08 100644 --- a/launcher/minecraft/MinecraftLoadAndCheck.h +++ b/launcher/minecraft/MinecraftLoadAndCheck.h @@ -43,7 +43,6 @@ #include <QUrl> #include "tasks/Task.h" -#include <quazip.h> #include "QObjectPtr.h" diff --git a/launcher/minecraft/MinecraftUpdate.h b/launcher/minecraft/MinecraftUpdate.h index 5ff5593db7..24b5c0957d 100644 --- a/launcher/minecraft/MinecraftUpdate.h +++ b/launcher/minecraft/MinecraftUpdate.h @@ -45,7 +45,6 @@ #include "net/NetJob.h" #include "tasks/Task.h" #include "minecraft/VersionFilterData.h" -#include <quazip.h> class MinecraftVersion; class MinecraftInstance; diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index a6e84325a8..104fba5cf7 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -49,9 +49,6 @@ #include <io/stream_reader.h> #include <tag_string.h> #include <tag_primitive.h> -#include <quazip.h> -#include <quazipfile.h> -#include <quazipdir.h> #include <QCoreApplication> @@ -260,41 +257,22 @@ void World::readFromFS(const QFileInfo &file) void World::readFromZip(const QFileInfo &file) { - QuaZip zip(file.absoluteFilePath()); - is_valid = zip.open(QuaZip::mdUnzip); - if (!is_valid) - { - return; - } - auto location = MMCZip::findFolderOfFileInZip(&zip, "level.dat"); + QString zipPath = file.absoluteFilePath(); + auto location = MMCZip::findFolderOfFileInZip(zipPath, "level.dat"); is_valid = !location.isEmpty(); if (!is_valid) { return; } m_containerOffsetPath = location; - QuaZipFile zippedFile(&zip); - // read the install profile - is_valid = zip.setCurrentFile(location + "level.dat"); - if (!is_valid) - { - return; - } - is_valid = zippedFile.open(QIODevice::ReadOnly); - QuaZipFileInfo64 levelDatInfo; - zippedFile.getFileInfo(&levelDatInfo); - auto modTime = levelDatInfo.getNTFSmTime(); - if(!modTime.isValid()) - { - modTime = levelDatInfo.dateTime; - } - levelDatTime = modTime; + QByteArray levelDatData = MMCZip::readFileFromZip(zipPath, location + "level.dat"); + is_valid = !levelDatData.isEmpty(); if (!is_valid) { return; } - loadFromLevelDat(zippedFile.readAll()); - zippedFile.close(); + levelDatTime = MMCZip::getEntryModTime(zipPath, location + "level.dat"); + loadFromLevelDat(levelDatData); } bool World::install(const QString &to, const QString &name) @@ -307,12 +285,8 @@ bool World::install(const QString &to, const QString &name) bool ok = false; if(m_containerFile.isFile()) { - QuaZip zip(m_containerFile.absoluteFilePath()); - if (!zip.open(QuaZip::mdUnzip)) - { - return false; - } - ok = !MMCZip::extractSubDir(&zip, m_containerOffsetPath, finalPath); + auto result = MMCZip::extractSubDir(m_containerFile.absoluteFilePath(), m_containerOffsetPath, finalPath); + ok = result.has_value(); } else if(m_containerFile.isDir()) { diff --git a/launcher/minecraft/launch/ExtractNatives.cpp b/launcher/minecraft/launch/ExtractNatives.cpp index b72b7f0484..7a74ebd34f 100644 --- a/launcher/minecraft/launch/ExtractNatives.cpp +++ b/launcher/minecraft/launch/ExtractNatives.cpp @@ -40,8 +40,6 @@ #include <minecraft/MinecraftInstance.h> #include <launch/LaunchTask.h> -#include <quazip.h> -#include <quazipdir.h> #include "MMCZip.h" #include "FileSystem.h" #include <QDir> @@ -65,19 +63,14 @@ static QString replaceSuffix (QString target, const QString &suffix, const QStri static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibHack, bool nativeOpenAL, bool nativeGLFW) { - QuaZip zip(source); - if(!zip.open(QuaZip::mdUnzip)) - { - return false; - } QDir directory(targetFolder); - if (!zip.goToFirstFile()) + QStringList entries = MMCZip::listEntries(source); + if (entries.isEmpty()) { return false; } - do + for (const auto &name : entries) { - QString name = zip.getCurrentFileName(); auto lowercase = name.toLower(); if (nativeGLFW && name.contains("glfw")) { continue; @@ -85,20 +78,20 @@ static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibH if (nativeOpenAL && name.contains("openal")) { continue; } + // Skip directories + if (name.endsWith('/')) + continue; + + QString outName = name; if(applyJnilibHack) { - name = replaceSuffix(name, ".jnilib", ".dylib"); + outName = replaceSuffix(outName, ".jnilib", ".dylib"); } - QString absFilePath = directory.absoluteFilePath(name); - if (!JlCompress::extractFile(&zip, "", absFilePath)) + QString absFilePath = directory.absoluteFilePath(outName); + if (!MMCZip::extractRelFile(source, name, absFilePath)) { return false; } - } while (zip.goToNextFile()); - zip.close(); - if(zip.getZipError()!=0) - { - return false; } return true; } diff --git a/launcher/minecraft/launch/VerifyJavaInstall.cpp b/launcher/minecraft/launch/VerifyJavaInstall.cpp index b1c47e206d..3368bfa8c1 100644 --- a/launcher/minecraft/launch/VerifyJavaInstall.cpp +++ b/launcher/minecraft/launch/VerifyJavaInstall.cpp @@ -35,6 +35,7 @@ #include "FileSystem.h" #include "Json.h" +#include "java/JavaUtils.h" #ifndef MeshMC_DISABLE_JAVA_DOWNLOADER #include "Application.h" @@ -74,48 +75,74 @@ QString VerifyJavaInstall::javaInstallDir() const QString VerifyJavaInstall::findInstalledJava(int requiredMajor) const { - QString javaBaseDir = javaInstallDir(); - QDir baseDir(javaBaseDir); - if (!baseDir.exists()) - return {}; - #if defined(Q_OS_WIN) QString binaryName = "javaw.exe"; #else QString binaryName = "java"; #endif - // Scan java/{vendor}/{version}/bin/java - QDirIterator vendorIt(javaBaseDir, QDir::Dirs | QDir::NoDotAndDotDot); - while (vendorIt.hasNext()) { - vendorIt.next(); - QString vendorPath = vendorIt.filePath(); - - QDirIterator versionIt(vendorPath, QDir::Dirs | QDir::NoDotAndDotDot); - while (versionIt.hasNext()) { - versionIt.next(); - QString versionPath = versionIt.filePath(); - - QDirIterator binIt(versionPath, QStringList() << binaryName, - QDir::Files, QDirIterator::Subdirectories); - while (binIt.hasNext()) { - binIt.next(); - QString javaPath = binIt.filePath(); - if (javaPath.contains("/bin/")) { - // Check if the directory name contains the required major version - // Version dirs are named like "zulu25.30.31-ca-jdk25.0.0-linux_x64" - // or "jdk-25+36" etc. We look for the major version number. + // Pattern matching Java major version in directory names + // Matches: "java-8-openjdk", "jdk-17", "java-21-openjdk-amd64", "zulu25.30.31-ca-jdk25.0.0-linux_x64" + QRegularExpression re(QString("(?:jdk|java|jre)[-.]?%1(?:[^0-9]|$)").arg(requiredMajor)); + + // 1. Scan MeshMC's managed java/{vendor}/{version}/bin/java directory + QString javaBaseDir = javaInstallDir(); + QDir baseDir(javaBaseDir); + if (baseDir.exists()) { + QDirIterator vendorIt(javaBaseDir, QDir::Dirs | QDir::NoDotAndDotDot); + while (vendorIt.hasNext()) { + vendorIt.next(); + QString vendorPath = vendorIt.filePath(); + + QDirIterator versionIt(vendorPath, QDir::Dirs | QDir::NoDotAndDotDot); + while (versionIt.hasNext()) { + versionIt.next(); + QString versionPath = versionIt.filePath(); + + QDirIterator binIt(versionPath, QStringList() << binaryName, + QDir::Files, QDirIterator::Subdirectories); + while (binIt.hasNext()) { + binIt.next(); + QString javaPath = binIt.filePath(); + if (!javaPath.contains("/bin/")) + continue; QString dirName = versionIt.fileName().toLower(); - // Match patterns like "jdk25", "java25", "-25.", "-25+", "-25-" - QRegularExpression re(QString("(?:jdk|java|jre)[-.]?%1(?:[^0-9]|$)").arg(requiredMajor)); - if (re.match(dirName).hasMatch()) { + if (re.match(dirName).hasMatch()) return javaPath; - } } } } } + // 2. Scan system-installed Java paths + // Use JavaUtils to discover all Java installations on the system, then + // check if any match the required major version by parsing the path. + JavaUtils javaUtils; + QList<QString> systemJavas = javaUtils.FindJavaPaths(); + for (const QString &javaPath : systemJavas) { + // Resolve to absolute path and verify it exists + QString resolved = FS::ResolveExecutable(javaPath); + if (resolved.isEmpty()) + continue; + + // Extract the parent directory components to check for version info + // Typical paths: + // /usr/lib/jvm/java-8-openjdk/bin/java + // /usr/lib/jvm/java-8-openjdk/jre/bin/java + // /usr/lib64/jvm/java-17-openjdk/bin/java + // /opt/jdk/jdk-21/bin/java + QFileInfo fi(resolved); + QString fullPath = fi.absoluteFilePath().toLower(); + + // Check if any component in the path matches the required Java version + QStringList parts = fullPath.split('/'); + for (const QString &part : parts) { + if (re.match(part).hasMatch()) { + return resolved; + } + } + } + return {}; } @@ -210,9 +237,9 @@ void VerifyJavaInstall::fetchVersionList(int requiredMajor) }); connect(m_fetchJob.get(), &NetJob::failed, this, [this, requiredMajor](QString reason) { - m_fetchJob.reset(); emitFailed(tr("Failed to fetch Java version list: %1. Please install Java %2 manually.") .arg(reason).arg(requiredMajor)); + m_fetchJob.reset(); }); m_fetchJob->start(); @@ -229,7 +256,7 @@ void VerifyJavaInstall::fetchRuntimes(const QString &versionId, int requiredMajo m_fetchJob->addNetAction(dl); connect(m_fetchJob.get(), &NetJob::succeeded, this, [this, requiredMajor]() { - m_fetchJob.reset(); + auto fetchJob = std::move(m_fetchJob); QJsonDocument doc; try { @@ -261,8 +288,8 @@ void VerifyJavaInstall::fetchRuntimes(const QString &versionId, int requiredMajo }); connect(m_fetchJob.get(), &NetJob::failed, this, [this](QString reason) { - m_fetchJob.reset(); emitFailed(tr("Failed to fetch Java runtime details: %1").arg(reason)); + m_fetchJob.reset(); }); m_fetchJob->start(); @@ -277,7 +304,6 @@ void VerifyJavaInstall::startDownload(const JavaDownload::RuntimeEntry &runtime, connect(m_downloadTask.get(), &Task::succeeded, this, [this]() { QString javaPath = m_downloadTask->installedJavaPath(); - m_downloadTask.reset(); if (javaPath.isEmpty()) { emitFailed(tr("Java was downloaded but the binary could not be found.")); @@ -289,8 +315,8 @@ void VerifyJavaInstall::startDownload(const JavaDownload::RuntimeEntry &runtime, }); connect(m_downloadTask.get(), &Task::failed, this, [this, requiredMajor](const QString &reason) { - m_downloadTask.reset(); emitFailed(tr("Failed to download Java %1: %2").arg(requiredMajor).arg(reason)); + m_downloadTask.reset(); }); connect(m_downloadTask.get(), &Task::status, this, [this](const QString &status) { diff --git a/launcher/minecraft/mod/LocalModParseTask.cpp b/launcher/minecraft/mod/LocalModParseTask.cpp index 39daaf4700..d68deb0cc6 100644 --- a/launcher/minecraft/mod/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/LocalModParseTask.cpp @@ -25,10 +25,10 @@ #include <QJsonObject> #include <QJsonArray> #include <QJsonValue> -#include <quazip.h> -#include <quazipfile.h> #include <toml.h> +#include "MMCZip.h" + #include "settings/INIFile.h" #include "FileSystem.h" @@ -343,36 +343,21 @@ LocalModParseTask::LocalModParseTask(int token, Mod::ModType type, const QFileIn void LocalModParseTask::processAsZip() { - QuaZip zip(m_modFile.filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return; - - QuaZipFile file(&zip); + QString zipPath = m_modFile.filePath(); - if (zip.setCurrentFile("META-INF/mods.toml")) + QByteArray modsToml = MMCZip::readFileFromZip(zipPath, "META-INF/mods.toml"); + if (!modsToml.isEmpty()) { - if (!file.open(QIODevice::ReadOnly)) - { - zip.close(); - return; - } - - m_result->details = ReadMCModTOML(file.readAll()); - file.close(); + m_result->details = ReadMCModTOML(modsToml); // to replace ${file.jarVersion} with the actual version, as needed if (m_result->details && m_result->details->version == "${file.jarVersion}") { - if (zip.setCurrentFile("META-INF/MANIFEST.MF")) + QByteArray manifestData = MMCZip::readFileFromZip(zipPath, "META-INF/MANIFEST.MF"); + if (!manifestData.isEmpty()) { - if (!file.open(QIODevice::ReadOnly)) - { - zip.close(); - return; - } - // quick and dirty line-by-line parser - auto manifestLines = file.readAll().split('\n'); + auto manifestLines = manifestData.split('\n'); QString manifestVersion = ""; for (auto &line : manifestLines) { @@ -391,55 +376,31 @@ void LocalModParseTask::processAsZip() } m_result->details->version = manifestVersion; - - file.close(); } } - - zip.close(); return; } - else if (zip.setCurrentFile("mcmod.info")) - { - if (!file.open(QIODevice::ReadOnly)) - { - zip.close(); - return; - } - m_result->details = ReadMCModInfo(file.readAll()); - file.close(); - zip.close(); + QByteArray mcmodInfo = MMCZip::readFileFromZip(zipPath, "mcmod.info"); + if (!mcmodInfo.isEmpty()) + { + m_result->details = ReadMCModInfo(mcmodInfo); return; } - else if (zip.setCurrentFile("fabric.mod.json")) - { - if (!file.open(QIODevice::ReadOnly)) - { - zip.close(); - return; - } - m_result->details = ReadFabricModInfo(file.readAll()); - file.close(); - zip.close(); + QByteArray fabricModJson = MMCZip::readFileFromZip(zipPath, "fabric.mod.json"); + if (!fabricModJson.isEmpty()) + { + m_result->details = ReadFabricModInfo(fabricModJson); return; } - else if (zip.setCurrentFile("forgeversion.properties")) - { - if (!file.open(QIODevice::ReadOnly)) - { - zip.close(); - return; - } - m_result->details = ReadForgeInfo(file.readAll()); - file.close(); - zip.close(); + QByteArray forgeVersionProps = MMCZip::readFileFromZip(zipPath, "forgeversion.properties"); + if (!forgeVersionProps.isEmpty()) + { + m_result->details = ReadForgeInfo(forgeVersionProps); return; } - - zip.close(); } void LocalModParseTask::processAsFolder() @@ -459,24 +420,11 @@ void LocalModParseTask::processAsFolder() void LocalModParseTask::processAsLitemod() { - QuaZip zip(m_modFile.filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return; - - QuaZipFile file(&zip); - - if (zip.setCurrentFile("litemod.json")) + QByteArray litemodJson = MMCZip::readFileFromZip(m_modFile.filePath(), "litemod.json"); + if (!litemodJson.isEmpty()) { - if (!file.open(QIODevice::ReadOnly)) - { - zip.close(); - return; - } - - m_result->details = ReadLiteModInfo(file.readAll()); - file.close(); + m_result->details = ReadLiteModInfo(litemodJson); } - zip.close(); } void LocalModParseTask::run() diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 38d99a186b..10c9bdcd7d 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -42,8 +42,6 @@ #include <QtConcurrent/QtConcurrent> #include <QRegularExpression> -#include <quazip.h> - #include "MMCZip.h" #include "minecraft/OneSixVersionFormat.h" #include "Version.h" @@ -145,8 +143,8 @@ void PackInstallTask::onDownloadSucceeded() void PackInstallTask::onDownloadFailed(QString reason) { qDebug() << "PackInstallTask::onDownloadFailed: " << QThread::currentThreadId(); - jobPtr.reset(); emitFailed(reason); + jobPtr.reset(); } QString PackInstallTask::getDirForModType(ModType type, QString raw) @@ -466,14 +464,14 @@ void PackInstallTask::installConfigs() connect(jobPtr.get(), &NetJob::succeeded, this, [&]() { abortable = false; - jobPtr.reset(); extractConfigs(); + jobPtr.reset(); }); connect(jobPtr.get(), &NetJob::failed, [&](QString reason) { abortable = false; - jobPtr.reset(); emitFailed(reason); + jobPtr.reset(); }); connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total) { @@ -491,13 +489,6 @@ void PackInstallTask::extractConfigs() QDir extractDir(m_stagingPath); - QuaZip packZip(archivePath); - if(!packZip.open(QuaZip::mdUnzip)) - { - emitFailed(tr("Failed to open pack configs %1!").arg(archivePath)); - return; - } - QString extractPath = extractDir.absolutePath() + "/minecraft"; QString archivePathCopy = archivePath; m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), [archivePathCopy, extractPath]() { @@ -631,8 +622,8 @@ void PackInstallTask::downloadMods() connect(jobPtr.get(), &NetJob::failed, [&](QString reason) { abortable = false; - jobPtr.reset(); emitFailed(reason); + jobPtr.reset(); }); connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total) { @@ -647,7 +638,6 @@ void PackInstallTask::onModsDownloaded() { abortable = false; qDebug() << "PackInstallTask::onModsDownloaded: " << QThread::currentThreadId(); - jobPtr.reset(); if(!modsToExtract.empty() || !modsToDecomp.empty() || !modsToCopy.empty()) { auto modsToExtractCopy = modsToExtract; diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp index 645c011152..f802a6409d 100644 --- a/launcher/modplatform/flame/FlamePackIndex.cpp +++ b/launcher/modplatform/flame/FlamePackIndex.cpp @@ -133,6 +133,7 @@ void Flame::loadIndexedPackVersions(Flame::IndexedPack & pack, QJsonArray & arr) file.mcVersion = versionArray[0].toString(); file.version = Json::requireString(version, "displayName"); file.downloadUrl = Json::ensureString(version, "downloadUrl", ""); + file.fileName = Json::ensureString(version, "fileName", ""); unsortedVersions.append(file); } diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h index 965df57c93..087db078b5 100644 --- a/launcher/modplatform/flame/FlamePackIndex.h +++ b/launcher/modplatform/flame/FlamePackIndex.h @@ -39,6 +39,7 @@ struct IndexedVersion { QString version; QString mcVersion; QString downloadUrl; + QString fileName; }; struct IndexedPack diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp index 1565868806..e03bf77c77 100644 --- a/launcher/modplatform/flame/PackManifest.cpp +++ b/launcher/modplatform/flame/PackManifest.cpp @@ -115,8 +115,12 @@ bool Flame::File::parseFromBytes(const QByteArray& bytes) } if(rawUrl.isEmpty()) { - qCritical() << "Resolving of" << projectId << fileId << "failed: no download URL provided (mod may have disabled third-party downloads)"; - return false; + // Mod has disabled third-party downloads — will be handled via browser download + qWarning() << "Mod" << projectId << fileId + << "(" << fileName << ") has no download URL (restricted)." + << "Will require browser download."; + resolved = false; + return true; } url = QUrl(rawUrl, QUrl::TolerantMode); if(!url.isValid()) diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp index 5f3d8d219b..d5aec541b1 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -102,13 +102,6 @@ void PackInstallTask::unzip() setStatus(tr("Extracting modpack")); QDir extractDir(m_stagingPath); - m_packZip.reset(new QuaZip(archivePath)); - if(!m_packZip->open(QuaZip::mdUnzip)) - { - emitFailed(tr("Failed to open modpack file %1!").arg(archivePath)); - return; - } - QString extractPath = extractDir.absolutePath() + "/unzip"; QString archivePathCopy = archivePath; m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), [archivePathCopy, extractPath]() { diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.h b/launcher/modplatform/legacy_ftb/PackInstallTask.h index 5d473af867..1ce3f41d27 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.h +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.h @@ -22,8 +22,6 @@ #pragma once #include "InstanceTask.h" #include "net/NetJob.h" -#include "quazip.h" -#include "quazipdir.h" #include "meta/Index.h" #include "meta/Version.h" #include "meta/VersionList.h" @@ -66,7 +64,6 @@ private slots: private: /* data */ shared_qobject_ptr<QNetworkAccessManager> m_network; bool abortable = false; - std::unique_ptr<QuaZip> m_packZip; QFuture<nonstd::optional<QStringList>> m_extractFuture; QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher; NetJob::Ptr netJobContainer; diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index 3d4758a336..a2c95b5e5e 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -97,8 +97,6 @@ void PackInstallTask::executeTask() void PackInstallTask::onDownloadSucceeded() { - jobPtr.reset(); - QJsonParseError parse_error; QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if(parse_error.error != QJsonParseError::NoError) { @@ -117,6 +115,7 @@ void PackInstallTask::onDownloadSucceeded() catch (const JSONValidationError &e) { emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); + jobPtr.reset(); return; } m_version = version; @@ -126,8 +125,8 @@ void PackInstallTask::onDownloadSucceeded() void PackInstallTask::onDownloadFailed(QString reason) { - jobPtr.reset(); emitFailed(reason); + jobPtr.reset(); } void PackInstallTask::downloadPack() @@ -169,14 +168,14 @@ void PackInstallTask::downloadPack() connect(jobPtr.get(), &NetJob::succeeded, this, [&]() { abortable = false; - jobPtr.reset(); install(); + jobPtr.reset(); }); connect(jobPtr.get(), &NetJob::failed, [&](QString reason) { abortable = false; - jobPtr.reset(); emitFailed(reason); + jobPtr.reset(); }); connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total) { diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp index 24e73d6a6a..eda32e34fd 100644 --- a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp +++ b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp @@ -85,14 +85,11 @@ void Technic::SingleZipPackInstallTask::downloadSucceeded() QDir extractDir(FS::PathCombine(m_stagingPath, ".minecraft")); qDebug() << "Attempting to create instance from" << m_archivePath; - // open the zip and find relevant files in it - m_packZip.reset(new QuaZip(m_archivePath)); - if (!m_packZip->open(QuaZip::mdUnzip)) - { - emitFailed(tr("Unable to open supplied modpack zip file.")); - return; - } - m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), QString(""), extractDir.absolutePath()); + QString archivePath = m_archivePath; + QString extractPath = extractDir.absolutePath(); + m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), [archivePath, extractPath]() { + return MMCZip::extractSubDir(archivePath, QString(""), extractPath); + }); connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &Technic::SingleZipPackInstallTask::extractFinished); connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &Technic::SingleZipPackInstallTask::extractAborted); m_extractFutureWatcher.setFuture(m_extractFuture); @@ -114,7 +111,6 @@ void Technic::SingleZipPackInstallTask::downloadProgressChanged(qint64 current, void Technic::SingleZipPackInstallTask::extractFinished() { - m_packZip.reset(); if (!m_extractFuture.result()) { emitFailed(tr("Failed to extract modpack")); diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.h b/launcher/modplatform/technic/SingleZipPackInstallTask.h index 67fe6a92be..07ef793012 100644 --- a/launcher/modplatform/technic/SingleZipPackInstallTask.h +++ b/launcher/modplatform/technic/SingleZipPackInstallTask.h @@ -41,8 +41,6 @@ #include "InstanceTask.h" #include "net/NetJob.h" -#include "quazip.h" - #include <QFutureWatcher> #include <QStringList> #include <QUrl> @@ -79,7 +77,6 @@ private: QString m_minecraftVersion; QString m_archivePath; NetJob::Ptr m_filesNetJob; - std::unique_ptr<QuaZip> m_packZip; QFuture<nonstd::optional<QStringList>> m_extractFuture; QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher; }; diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp index 94d161de0d..ac1ef4e90e 100644 --- a/launcher/modplatform/technic/TechnicPackProcessor.cpp +++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp @@ -42,9 +42,7 @@ #include <Json.h> #include <minecraft/MinecraftInstance.h> #include <minecraft/PackProfile.h> -#include <quazip.h> -#include <quazipdir.h> -#include <quazipfile.h> +#include <MMCZip.h> #include <settings/INISettingsObject.h> #include <memory> @@ -75,40 +73,27 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const QString fmlMinecraftVersion; if (QFile::exists(modpackJar)) { - QuaZip zipFile(modpackJar); - if (!zipFile.open(QuaZip::mdUnzip)) + if (MMCZip::entryExists(modpackJar, "version.json")) { - emit failed(tr("Unable to open \"bin/modpack.jar\" file!")); - return; - } - QuaZipDir zipFileRoot(&zipFile, "/"); - if (zipFileRoot.exists("/version.json")) - { - if (zipFileRoot.exists("/fmlversion.properties")) + if (MMCZip::entryExists(modpackJar, "fmlversion.properties")) { - zipFile.setCurrentFile("fmlversion.properties"); - QuaZipFile file(&zipFile); - if (!file.open(QIODevice::ReadOnly)) + QByteArray fmlVersionData = MMCZip::readFileFromZip(modpackJar, "fmlversion.properties"); + if (fmlVersionData.isEmpty()) { emit failed(tr("Unable to open \"fmlversion.properties\"!")); return; } - QByteArray fmlVersionData = file.readAll(); - file.close(); INIFile iniFile; iniFile.loadFile(fmlVersionData); // If not present, this evaluates to a null string fmlMinecraftVersion = iniFile["fmlbuild.mcversion"].toString(); } - zipFile.setCurrentFile("version.json", QuaZip::csSensitive); - QuaZipFile file(&zipFile); - if (!file.open(QIODevice::ReadOnly)) + data = MMCZip::readFileFromZip(modpackJar, "version.json"); + if (data.isEmpty()) { emit failed(tr("Unable to open \"version.json\"!")); return; } - data = file.readAll(); - file.close(); } else { @@ -120,18 +105,15 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const // Forge for 1.4.7 and for 1.5.2 require extra libraries. // Figure out the forge version and add it as a component // (the code still comes from the jar mod installed above) - if (zipFileRoot.exists("/forgeversion.properties")) + if (MMCZip::entryExists(modpackJar, "forgeversion.properties")) { - zipFile.setCurrentFile("forgeversion.properties", QuaZip::csSensitive); - QuaZipFile file(&zipFile); - if (!file.open(QIODevice::ReadOnly)) + QByteArray forgeVersionData = MMCZip::readFileFromZip(modpackJar, "forgeversion.properties"); + if (forgeVersionData.isEmpty()) { // Really shouldn't happen, but error handling shall not be forgotten emit failed(tr("Unable to open \"forgeversion.properties\"")); return; } - QByteArray forgeVersionData = file.readAll(); - file.close(); INIFile iniFile; iniFile.loadFile(forgeVersionData); QString major, minor, revision, build; diff --git a/launcher/tasks/Task.cpp b/launcher/tasks/Task.cpp index 627b3d5079..e7fb0cdd8e 100644 --- a/launcher/tasks/Task.cpp +++ b/launcher/tasks/Task.cpp @@ -39,6 +39,7 @@ #include "Task.h" #include <QDebug> +#include <QPointer> Task::Task(QObject *parent) : QObject(parent) { @@ -107,8 +108,10 @@ void Task::emitFailed(QString reason) m_state = State::Failed; m_failReason = reason; qCritical() << "Task" << describe() << "failed: " << reason; + QPointer<Task> guard(this); emit failed(reason); - emit finished(); + if (guard) + emit finished(); } void Task::emitAborted() @@ -122,8 +125,10 @@ void Task::emitAborted() m_state = State::AbortedByUser; m_failReason = "Aborted."; qDebug() << "Task" << describe() << "aborted."; + QPointer<Task> guard(this); emit failed(m_failReason); - emit finished(); + if (guard) + emit finished(); } void Task::emitSucceeded() @@ -136,8 +141,10 @@ void Task::emitSucceeded() } m_state = State::Succeeded; qDebug() << "Task" << describe() << "succeeded"; + QPointer<Task> guard(this); emit succeeded(); - emit finished(); + if (guard) + emit finished(); } QString Task::describe() diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp new file mode 100644 index 0000000000..9ea56b1383 --- /dev/null +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -0,0 +1,171 @@ +/* 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 <https://www.gnu.org/licenses/>. + */ + +#include "BlockedModsDialog.h" + +#include <QDesktopServices> +#include <QDir> +#include <QFont> +#include <QGridLayout> +#include <QScrollArea> +#include <QStandardPaths> +#include <QUrl> + +BlockedModsDialog::BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, + QList<BlockedMod> &mods) + : QDialog(parent), m_mods(mods) +{ + setWindowTitle(title); + setMinimumSize(550, 300); + resize(620, 420); + setWindowModality(Qt::WindowModal); + + auto *mainLayout = new QVBoxLayout(this); + + // Description label at top + auto *descLabel = new QLabel(text, this); + descLabel->setWordWrap(true); + mainLayout->addWidget(descLabel); + + // Scrollable area for mod list + auto *scrollArea = new QScrollArea(this); + scrollArea->setWidgetResizable(true); + + auto *scrollWidget = new QWidget(); + auto *grid = new QGridLayout(scrollWidget); + grid->setColumnStretch(0, 3); // mod name + grid->setColumnStretch(1, 1); // status + grid->setColumnStretch(2, 0); // button + + // Header row + auto *headerName = new QLabel(tr("<b>Mod</b>"), scrollWidget); + auto *headerStatus = new QLabel(tr("<b>Status</b>"), scrollWidget); + grid->addWidget(headerName, 0, 0); + grid->addWidget(headerStatus, 0, 1); + + for (int i = 0; i < m_mods.size(); i++) { + int row = i + 1; + + auto *nameLabel = new QLabel(m_mods[i].fileName, scrollWidget); + nameLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + + auto *statusLabel = new QLabel(tr("Missing"), scrollWidget); + statusLabel->setStyleSheet("color: #cc3333; font-weight: bold;"); + + auto *downloadBtn = new QPushButton(tr("Download"), scrollWidget); + connect(downloadBtn, &QPushButton::clicked, this, [this, i]() { + openModDownload(i); + }); + + grid->addWidget(nameLabel, row, 0); + grid->addWidget(statusLabel, row, 1); + grid->addWidget(downloadBtn, row, 2); + + m_rows.append({nameLabel, statusLabel, downloadBtn}); + } + + // Add stretch at bottom of grid + grid->setRowStretch(m_mods.size() + 1, 1); + + scrollWidget->setLayout(grid); + scrollArea->setWidget(scrollWidget); + mainLayout->addWidget(scrollArea, 1); + + // Button box at bottom + m_buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + m_buttons->button(QDialogButtonBox::Ok)->setText(tr("Continue")); + m_buttons->button(QDialogButtonBox::Ok)->setEnabled(false); + connect(m_buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(m_buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + mainLayout->addWidget(m_buttons); + + setLayout(mainLayout); + + // Set up Downloads folder watching + setupWatch(); + + // Initial scan + scanDownloadsFolder(); +} + +void BlockedModsDialog::setupWatch() +{ + m_downloadDir = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); + if (!m_downloadDir.isEmpty() && QDir(m_downloadDir).exists()) { + m_watcher.addPath(m_downloadDir); + connect(&m_watcher, &QFileSystemWatcher::directoryChanged, + this, &BlockedModsDialog::onDownloadDirChanged); + } +} + +void BlockedModsDialog::onDownloadDirChanged(const QString &path) +{ + Q_UNUSED(path); + scanDownloadsFolder(); +} + +void BlockedModsDialog::scanDownloadsFolder() +{ + if (m_downloadDir.isEmpty()) + return; + + QDir dir(m_downloadDir); + QStringList files = dir.entryList(QDir::Files); + + for (int i = 0; i < m_mods.size(); i++) { + if (!m_mods[i].found && files.contains(m_mods[i].fileName)) { + m_mods[i].found = true; + } + } + + updateModStatus(); +} + +void BlockedModsDialog::updateModStatus() +{ + bool allFound = true; + + for (int i = 0; i < m_mods.size(); i++) { + if (m_mods[i].found) { + m_rows[i].statusLabel->setText(QString::fromUtf8("\u2714 ") + tr("Found")); + m_rows[i].statusLabel->setStyleSheet("color: #33aa33; font-weight: bold;"); + m_rows[i].downloadButton->setEnabled(false); + } else { + m_rows[i].statusLabel->setText(tr("Missing")); + m_rows[i].statusLabel->setStyleSheet("color: #cc3333; font-weight: bold;"); + m_rows[i].downloadButton->setEnabled(true); + allFound = false; + } + } + + m_buttons->button(QDialogButtonBox::Ok)->setEnabled(allFound); +} + +void BlockedModsDialog::openModDownload(int index) +{ + if (index < 0 || index >= m_mods.size()) + return; + + const auto &mod = m_mods[index]; + QString url = QString("https://www.curseforge.com/api/v1/mods/%1/files/%2/download") + .arg(mod.projectId).arg(mod.fileId); + QDesktopServices::openUrl(QUrl(url)); +} diff --git a/launcher/ui/dialogs/BlockedModsDialog.h b/launcher/ui/dialogs/BlockedModsDialog.h new file mode 100644 index 0000000000..31cf4d169c --- /dev/null +++ b/launcher/ui/dialogs/BlockedModsDialog.h @@ -0,0 +1,70 @@ +/* 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 <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <QDialog> +#include <QFileSystemWatcher> +#include <QLabel> +#include <QPushButton> +#include <QDialogButtonBox> +#include <QVBoxLayout> + +struct BlockedMod { + int projectId; + int fileId; + QString fileName; + QString targetPath; + bool found = false; +}; + +class BlockedModsDialog : public QDialog { + Q_OBJECT + +public: + explicit BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, + QList<BlockedMod> &mods); + + /// Returns the list of mods with updated `found` status + QList<BlockedMod> &resultMods() { return m_mods; } + +private slots: + void onDownloadDirChanged(const QString &path); + void openModDownload(int index); + +private: + void scanDownloadsFolder(); + void updateModStatus(); + void setupWatch(); + + QList<BlockedMod> &m_mods; + QString m_downloadDir; + QFileSystemWatcher m_watcher; + + struct ModRow { + QLabel *nameLabel; + QLabel *statusLabel; + QPushButton *downloadButton; + }; + QList<ModRow> m_rows; + + QDialogButtonBox *m_buttons; +}; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index cd2a9112f4..fc9a190845 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -190,13 +190,29 @@ void FlamePage::suggestCurrent() return; } - if (selectedVersion.isEmpty()) + if (selectedVersionIndex < 0 || selectedVersionIndex >= current.versions.size()) { dialog->setSuggestedPack(); return; } - dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion)); + auto &version = current.versions[selectedVersionIndex]; + + if (!version.downloadUrl.isEmpty()) + { + // Normal download — direct URL available + dialog->setSuggestedPack(current.name, new InstanceImportTask(version.downloadUrl)); + } + else + { + // Restricted download — construct CurseForge browser download URL + // This URL triggers a browser download when opened, respecting ToS + QString browserUrl = QString("https://www.curseforge.com/api/v1/mods/%1/files/%2/download") + .arg(version.addonId).arg(version.fileId); + dialog->setSuggestedPack(current.name, new InstanceImportTask(browserUrl)); + qDebug() << "Pack has no API download URL, using browser download URL:" << browserUrl; + } + QString editedLogoName; editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0); listModel->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo) @@ -210,8 +226,10 @@ void FlamePage::onVersionSelectionChanged(QString data) if(data.isNull() || data.isEmpty()) { selectedVersion = ""; + selectedVersionIndex = -1; return; } selectedVersion = ui->versionSelectionBox->currentData().toString(); + selectedVersionIndex = ui->versionSelectionBox->currentIndex(); suggestCurrent(); } diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index 55648e6dd7..09afbcba12 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -100,4 +100,5 @@ private: Flame::IndexedPack current; QString selectedVersion; + int selectedVersionIndex = -1; }; diff --git a/libraries/classparser/CMakeLists.txt b/libraries/classparser/CMakeLists.txt index 400f7dea02..f4776902cf 100644 --- a/libraries/classparser/CMakeLists.txt +++ b/libraries/classparser/CMakeLists.txt @@ -38,4 +38,4 @@ add_definitions(-DCLASSPARSER_LIBRARY) add_library(MeshMC_classparser STATIC ${CLASSPARSER_SOURCES} ${CLASSPARSER_HEADERS}) target_include_directories(MeshMC_classparser PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") -target_link_libraries(MeshMC_classparser PRIVATE QuaZip::QuaZip Qt6::Core) +target_link_libraries(MeshMC_classparser PRIVATE LibArchive::LibArchive Qt6::Core) diff --git a/libraries/classparser/src/classparser.cpp b/libraries/classparser/src/classparser.cpp index fdfcfef9b1..4c7f0fe482 100644 --- a/libraries/classparser/src/classparser.cpp +++ b/libraries/classparser/src/classparser.cpp @@ -39,7 +39,8 @@ #include "classparser.h" #include <QFile> -#include <quazip/quazipfile.h> +#include <archive.h> +#include <archive_entry.h> #include <QDebug> namespace classparser @@ -54,26 +55,44 @@ QString GetMinecraftJarVersion(QString jarName) if (!jar.exists()) return version; - // open minecraft.jar - QuaZip zip(&jar); - if (!zip.open(QuaZip::mdUnzip)) + // open jar with libarchive + struct archive *a = archive_read_new(); + archive_read_support_format_zip(a); + if (archive_read_open_filename(a, jarName.toUtf8().constData(), 10240) != ARCHIVE_OK) { + archive_read_free(a); return version; + } - // open Minecraft.class - zip.setCurrentFile("net/minecraft/client/Minecraft.class", QuaZip::csSensitive); - QuaZipFile Minecraft(&zip); - if (!Minecraft.open(QuaZipFile::ReadOnly)) - return version; + // find and read Minecraft.class + QByteArray classData; + struct archive_entry *entry; + while (archive_read_next_header(a, &entry) == ARCHIVE_OK) { + QString name = QString::fromUtf8(archive_entry_pathname(entry)); + if (name == "net/minecraft/client/Minecraft.class") { + la_int64_t sz = archive_entry_size(entry); + if (sz > 0) { + classData.resize(sz); + archive_read_data(a, classData.data(), sz); + } else { + char buf[8192]; + la_ssize_t r; + while ((r = archive_read_data(a, buf, sizeof(buf))) > 0) + classData.append(buf, r); + } + break; + } + archive_read_data_skip(a); + } + archive_read_free(a); - // read Minecraft.class - qint64 size = Minecraft.size(); - char *classfile = new char[size]; - Minecraft.read(classfile, size); + if (classData.isEmpty()) + return version; // parse Minecraft.class try { - char *temp = classfile; + char *temp = classData.data(); + qint64 size = classData.size(); java::classfile MinecraftClass(temp, size); java::constant_pool constants = MinecraftClass.constants; for (java::constant_pool::container_type::const_iterator iter = constants.begin(); @@ -93,12 +112,6 @@ QString GetMinecraftJarVersion(QString jarName) } catch (const java::classfile_exception &) { } - // clean up - delete[] classfile; - Minecraft.close(); - zip.close(); - jar.close(); - return version; } } diff --git a/nix/unwrapped.nix b/nix/unwrapped.nix index 0a8b03898a..0ca752ea89 100644 --- a/nix/unwrapped.nix +++ b/nix/unwrapped.nix @@ -94,7 +94,6 @@ stdenv.mkDerivation { kdePackages.qtbase kdePackages.qtnetworkauth kdePackages.qt5compat - kdePackages.quazip qrencode libarchive tomlplusplus diff --git a/scripts/checkpatch.pl b/scripts/checkpatch.pl index 3555120a43..43098ca2e6 100755 --- a/scripts/checkpatch.pl +++ b/scripts/checkpatch.pl @@ -313,10 +313,9 @@ sub main { } # Exit code based on results + # Warnings alone do not cause a non-zero exit if ($g_error_count > 0) { exit(1); - } elsif ($g_warn_count > 0) { - exit(2); } exit(0); } diff --git a/vcpkg.json b/vcpkg.json index cb5b5e3d3d..502bd10088 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -29,10 +29,6 @@ "zstd" ] }, - { - "name": "quazip", - "platform": "!osx" - }, "tomlplusplus", "zlib", "vulkan-headers" |
