diff --git a/.github/actions/setup-dependencies/linux/action.yml b/.github/actions/setup-dependencies/linux/action.yml index 21ba30dc7..610126442 100644 --- a/.github/actions/setup-dependencies/linux/action.yml +++ b/.github/actions/setup-dependencies/linux/action.yml @@ -9,7 +9,7 @@ runs: run: | sudo apt-get -y update sudo apt-get -y install \ - dpkg-dev \ + dpkg-dev libarchive-dev \ ninja-build extra-cmake-modules scdoc \ libqrencode-dev \ appstream libxcb-cursor-dev diff --git a/.github/actions/setup-dependencies/windows/action.yml b/.github/actions/setup-dependencies/windows/action.yml index c6f23a9ca..f2985fd1b 100644 --- a/.github/actions/setup-dependencies/windows/action.yml +++ b/.github/actions/setup-dependencies/windows/action.yml @@ -81,7 +81,7 @@ runs: cmark:p qrencode:p tomlplusplus:p - quazip-qt6:p + libarchive:p - name: List pacman packages (MinGW) if: ${{ inputs.msystem != '' }} diff --git a/.gitmodules b/.gitmodules index 7ad40becb..246d9d791 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "libraries/quazip"] - path = libraries/quazip - url = https://github.com/stachenov/quazip.git [submodule "libraries/tomlplusplus"] path = libraries/tomlplusplus url = https://github.com/marzer/tomlplusplus.git diff --git a/.markdownlintignore b/.markdownlintignore index a8669d01d..96f627ad9 100644 --- a/.markdownlintignore +++ b/.markdownlintignore @@ -1,2 +1 @@ libraries/nbtplusplus -libraries/quazip diff --git a/CMakeLists.txt b/CMakeLists.txt index 0ea2d4880..27d5108ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -327,14 +327,6 @@ if(Launcher_QT_VERSION_MAJOR EQUAL 6) find_package(Qt6 COMPONENTS DBus) list(APPEND Launcher_QT_DBUS Qt6::DBus) list(APPEND Launcher_QT_LIBS Qt6::Core5Compat) - - if(NOT Launcher_FORCE_BUNDLED_LIBS) - find_package(QuaZip-Qt6 1.3 QUIET) - endif() - if (NOT QuaZip-Qt6_FOUND) - set(QUAZIP_QT_MAJOR_VERSION ${QT_VERSION_MAJOR} CACHE STRING "Qt version to use (4, 5 or 6), defaults to ${QT_VERSION_MAJOR}" FORCE) - set(FORCE_BUNDLED_QUAZIP 1) - endif() else() message(FATAL_ERROR "Qt version ${Launcher_QT_VERSION_MAJOR} is not supported") endif() @@ -357,6 +349,14 @@ else() pkg_check_modules(libqrencode REQUIRED IMPORTED_TARGET libqrencode) endif() +# Find libarchive through CMake packages, mainly for vcpkg +find_package(LibArchive QUIET) +# CMake packages aren't available in most distributions of libarchive, so fallback to pkg-config +if(NOT LibArchive_FOUND) + find_package(PkgConfig REQUIRED) + pkg_check_modules(libarchive REQUIRED IMPORTED_TARGET libarchive) +endif() + if(NOT Launcher_FORCE_BUNDLED_LIBS) # Find toml++ find_package(tomlplusplus 3.2.0 QUIET) @@ -505,14 +505,6 @@ if(FORCE_BUNDLED_ZLIB) else() message(STATUS "Using system zlib") endif() -if (FORCE_BUNDLED_QUAZIP) - message(STATUS "Using bundled QuaZip") - set(BUILD_SHARED_LIBS 0) # link statically to avoid conflicts. - set(QUAZIP_INSTALL 0) - add_subdirectory(libraries/quazip) # zip manipulation library -else() - message(STATUS "Using system QuaZip") -endif() add_subdirectory(libraries/rainbow) # Qt extension for colors add_subdirectory(libraries/LocalPeer) # fork of a library from Qt solutions if(NOT tomlplusplus_FOUND) diff --git a/COPYING.md b/COPYING.md index 8588c8951..fb33844f7 100644 --- a/COPYING.md +++ b/COPYING.md @@ -212,30 +212,6 @@ This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL -## Quazip - - Copyright (C) 2005-2021 Sergey A. Tachenov - - The QuaZip library is licensed under the GNU Lesser General Public - License V2.1 plus a static linking exception. - - The original ZIP/UNZIP package (MiniZip) is copyrighted by Gilles - Vollant and contributors, see quazip/(un)zip.h files for details. - Basically it's the zlib license. - - STATIC LINKING EXCEPTION - - The copyright holders give you permission to link this library with - independent modules to produce an executable, regardless of the license - terms of these independent modules, and to copy and distribute the - resulting executable under terms of your choice, provided that you also - meet, for each linked independent module, the terms and conditions of - the license of that module. An independent module is a module which is - not derived from or based on this library. If you modify this library, - you must extend this exception to your version of the library. - - See COPYING file for the full LGPL text. - ## launcher (`libraries/launcher`) PolyMC - Minecraft Launcher diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 67ffa4dfb..37e1ebbdb 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -26,8 +26,14 @@ set(CORE_SOURCES NullInstance.h MMCZip.h MMCZip.cpp - Untar.h - Untar.cpp + archive/ArchiveReader.cpp + archive/ArchiveReader.h + archive/ArchiveWriter.cpp + archive/ArchiveWriter.h + archive/ExportToZipTask.cpp + archive/ExportToZipTask.h + archive/ExtractZipTask.cpp + archive/ExtractZipTask.h StringUtils.h StringUtils.cpp QVariantUtils.h @@ -615,6 +621,10 @@ set(PRISMUPDATER_SOURCES # Zip MMCZip.h MMCZip.cpp + archive/ArchiveReader.cpp + archive/ArchiveReader.h + archive/ArchiveWriter.cpp + archive/ArchiveWriter.h # Time MMCTime.h @@ -1320,6 +1330,11 @@ if(TARGET PkgConfig::tomlplusplus) else() target_link_libraries(Launcher_logic tomlplusplus::tomlplusplus) endif() +if(TARGET PkgConfig::libarchive) + target_link_libraries(Launcher_logic PkgConfig::libarchive) +else() + target_link_libraries(Launcher_logic LibArchive::LibArchive) +endif() if (UNIX AND NOT CYGWIN AND NOT APPLE) target_link_libraries(Launcher_logic @@ -1340,7 +1355,6 @@ target_link_libraries(Launcher_logic ${Launcher_QT_LIBS} ) target_link_libraries(Launcher_logic - QuaZip::QuaZip cmark::cmark LocalPeer Launcher_rainbow @@ -1407,7 +1421,6 @@ if(Launcher_BUILD_UPDATER) add_library(prism_updater_logic STATIC ${PRISMUPDATER_SOURCES} ${TASKS_SOURCES} ${PRISMUPDATER_UI}) target_include_directories(prism_updater_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(prism_updater_logic - QuaZip::QuaZip ${ZLIB_LIBRARIES} systeminfo BuildConfig @@ -1417,6 +1430,11 @@ if(Launcher_BUILD_UPDATER) ${Launcher_QT_LIBS} cmark::cmark ) + if(TARGET PkgConfig::libarchive) + target_link_libraries(prism_updater_logic PkgConfig::libarchive) + else() + target_link_libraries(prism_updater_logic LibArchive::LibArchive) + endif() add_executable("${Launcher_Name}_updater" WIN32 updater/prismupdater/updater_main.cpp) target_sources("${Launcher_Name}_updater" PRIVATE updater/prismupdater/updater.exe.manifest) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 5b44dbd48..30d0a9c4c 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -1702,4 +1702,14 @@ QString getUniqueResourceName(const QString& filePath) return newFileName; } +bool removeFiles(QStringList listFile) +{ + bool ret = true; + // For each file + for (int i = 0; i < listFile.count(); i++) { + // Remove + ret = ret && QFile::remove(listFile.at(i)); + } + return ret; +} } // namespace FS diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index b0d9ae2e8..f2676b147 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -291,6 +291,8 @@ bool move(const QString& source, const QString& dest); */ bool deletePath(QString path); +bool removeFiles(QStringList listFile); + /** * Trash a folder / file */ diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 77298e2ce..d3cc3bd02 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -38,10 +38,11 @@ #include "Application.h" #include "FileSystem.h" -#include "MMCZip.h" #include "NullInstance.h" #include "QObjectPtr.h" +#include "archive/ArchiveReader.h" +#include "archive/ExtractZipTask.h" #include "icons/IconList.h" #include "icons/IconUtils.h" @@ -54,12 +55,10 @@ #include "net/ApiDownload.h" +#include #include -#include #include -#include - InstanceImportTask::InstanceImportTask(const QUrl& sourceUrl, QWidget* parent, QMap&& extra_info) : m_sourceUrl(sourceUrl), m_extra_info(extra_info), m_parent(parent) {} @@ -109,38 +108,34 @@ void InstanceImportTask::downloadFromUrl() filesNetJob->start(); } -QString InstanceImportTask::getRootFromZip(QuaZip* zip, const QString& root) +QString InstanceImportTask::getRootFromZip(QStringList files) { if (!isRunning()) { return {}; } - QuaZipDir rootDir(zip, root); - for (auto&& fileName : rootDir.entryList(QDir::Files)) { + auto cleanPath = [](QString path) { + if (path == ".") + return QString(); + QString result = path; + if (result.startsWith("./")) + result = result.mid(2); + return result; + }; + for (auto&& fileName : files) { setDetails(fileName); - if (fileName == "instance.cfg") { + QFileInfo fileInfo(fileName); + if (fileInfo.fileName() == "instance.cfg") { qDebug() << "MultiMC:" << true; m_modpackType = ModpackType::MultiMC; - return root; + return cleanPath(fileInfo.path()); } - if (fileName == "manifest.json") { + if (fileInfo.fileName() == "manifest.json") { qDebug() << "Flame:" << true; m_modpackType = ModpackType::Flame; - return root; + return cleanPath(fileInfo.path()); } - QCoreApplication::processEvents(); } - - // Recurse the search to non-ignored subfolders - for (auto&& fileName : rootDir.entryList(QDir::Dirs)) { - if ("overrides/" == fileName) - continue; - - QString result = getRootFromZip(zip, root + fileName); - if (!result.isEmpty()) - return result; - } - return {}; } @@ -151,13 +146,12 @@ void InstanceImportTask::processZipPack() qDebug() << "Attempting to create instance from" << m_archivePath; // open the zip and find relevant files in it - auto packZip = std::make_shared(m_archivePath); - if (!packZip->open(QuaZip::mdUnzip)) { + MMCZip::ArchiveReader packZip(m_archivePath); + if (!packZip.collectFiles()) { emitFailed(tr("Unable to open supplied modpack zip file.")); return; } - QuaZipDir packZipDir(packZip.get()); qDebug() << "Attempting to determine instance type"; QString root; @@ -165,18 +159,18 @@ void InstanceImportTask::processZipPack() // NOTE: Prioritize modpack platforms that aren't searched for recursively. // Especially Flame has a very common filename for its manifest, which may appear inside overrides for example // https://docs.modrinth.com/docs/modpacks/format_definition/#storage - if (packZipDir.exists("/modrinth.index.json")) { + if (packZip.exists("/modrinth.index.json")) { // process as Modrinth pack qDebug() << "Modrinth:" << true; m_modpackType = ModpackType::Modrinth; - } else if (packZipDir.exists("/bin/modpack.jar") || packZipDir.exists("/bin/version.json")) { + } else if (packZip.exists("/bin/modpack.jar") || packZip.exists("/bin/version.json")) { // process as Technic pack qDebug() << "Technic:" << true; extractDir.mkpath("minecraft"); extractDir.cd("minecraft"); m_modpackType = ModpackType::Technic; } else { - root = getRootFromZip(packZip.get()); + root = getRootFromZip(packZip.getFiles()); setDetails(""); } if (m_modpackType == ModpackType::Unknown) { @@ -186,7 +180,7 @@ void InstanceImportTask::processZipPack() setStatus(tr("Extracting modpack")); // make sure we extract just the pack - auto zipTask = makeShared(packZip, extractDir, root); + auto zipTask = makeShared(m_archivePath, extractDir, root); auto progressStep = std::make_shared(); connect(zipTask.get(), &Task::finished, this, [this, progressStep] { diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index 8884e0801..4c9e6feb5 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -40,8 +40,6 @@ #include #include "InstanceTask.h" -class QuaZip; - class InstanceImportTask : public InstanceTask { Q_OBJECT public: @@ -58,7 +56,7 @@ class InstanceImportTask : public InstanceTask { void processTechnic(); void processFlame(); void processModrinth(); - QString getRootFromZip(QuaZip* zip, const QString& root = ""); + QString getRootFromZip(QStringList files); private slots: void processZipPack(); diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index dfe397930..8e4e433ed 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -35,66 +35,46 @@ */ #include "MMCZip.h" -#include -#include -#include +#include #include "FileSystem.h" +#include "archive/ArchiveReader.h" +#include "archive/ArchiveWriter.h" #include #include #include #include - -#if defined(LAUNCHER_APPLICATION) -#include -#endif +#include namespace MMCZip { // ours -bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet& contained, const Filter& filter) +using FilterFunction = std::function; +#if defined(LAUNCHER_APPLICATION) +bool mergeZipFiles(ArchiveWriter& into, QFileInfo from, QSet& contained, const FilterFunction& filter = nullptr) { - QuaZip modZip(from.filePath()); - modZip.open(QuaZip::mdUnzip); - - QuaZipFile fileInsideMod(&modZip); - QuaZipFile zipOutFile(into); - for (bool more = modZip.goToFirstFile(); more; more = modZip.goToNextFile()) { - QString filename = modZip.getCurrentFileName(); + ArchiveReader r(from.absoluteFilePath()); + return r.parse([&into, &contained, &filter, from](ArchiveReader::File* f) { + auto filename = f->filename(); if (filter && !filter(filename)) { qDebug() << "Skipping file " << filename << " from " << from.fileName() << " - filtered"; - continue; + f->skip(); + return true; } if (contained.contains(filename)) { qDebug() << "Skipping already contained file " << filename << " from " << from.fileName(); - continue; + f->skip(); + return true; } contained.insert(filename); - - if (!fileInsideMod.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open " << filename << " from " << from.fileName(); - return false; - } - - QuaZipNewInfo info_out(fileInsideMod.getActualFileName()); - - if (!zipOutFile.open(QIODevice::WriteOnly, info_out)) { - qCritical() << "Failed to open " << filename << " in the jar"; - fileInsideMod.close(); - return false; - } - if (!JlCompress::copyData(fileInsideMod, zipOutFile)) { - zipOutFile.close(); - fileInsideMod.close(); + if (!into.addFile(f)) { qCritical() << "Failed to copy data of " << filename << " into the jar"; return false; } - zipOutFile.close(); - fileInsideMod.close(); - } - return true; + return true; + }); } -bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool followSymlinks) +bool compressDirFiles(ArchiveWriter& zip, QString dir, QFileInfoList files) { QDir directory(dir); if (!directory.exists()) @@ -103,48 +83,18 @@ bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool follow for (auto e : files) { auto filePath = directory.relativeFilePath(e.absoluteFilePath()); auto srcPath = e.absoluteFilePath(); - if (followSymlinks) { - if (e.isSymLink()) { - srcPath = e.symLinkTarget(); - } else { - srcPath = e.canonicalFilePath(); - } - } - if (!JlCompress::compressFile(zip, srcPath, filePath)) + if (!zip.addFile(srcPath, filePath)) return false; } return true; } -bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks) -{ - QuaZip zip(fileCompressed); - zip.setUtf8Enabled(true); - QDir().mkpath(QFileInfo(fileCompressed).absolutePath()); - if (!zip.open(QuaZip::mdCreate)) { - FS::deletePath(fileCompressed); - return false; - } - - auto result = compressDirFiles(&zip, dir, files, followSymlinks); - - zip.close(); - if (zip.getZipError() != 0) { - FS::deletePath(fileCompressed); - return false; - } - - return result; -} - -#if defined(LAUNCHER_APPLICATION) // ours bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods) { - QuaZip zipOut(targetJarPath); - zipOut.setUtf8Enabled(true); - if (!zipOut.open(QuaZip::mdCreate)) { + ArchiveWriter zipOut(targetJarPath); + if (!zipOut.open()) { FS::deletePath(targetJarPath); qCritical() << "Failed to open the minecraft.jar for modding"; return false; @@ -161,7 +111,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QListenabled()) continue; if (mod->type() == ResourceType::ZIPFILE) { - if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles)) { + if (!mergeZipFiles(zipOut, mod->fileinfo(), addedFiles)) { zipOut.close(); FS::deletePath(targetJarPath); qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; @@ -170,7 +120,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QListtype() == ResourceType::SINGLEFILE) { // FIXME: buggy - does not work with addedFiles auto filename = mod->fileinfo(); - if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) { + if (!zipOut.addFile(filename.absoluteFilePath(), filename.fileName())) { zipOut.close(); FS::deletePath(targetJarPath); qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; @@ -193,7 +143,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QListfileinfo().fileName() << "to the jar."; @@ -209,7 +159,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList extractSubDir(QuaZip* zip, const QString& subdir, const QString& target) +std::optional extractSubDir(ArchiveReader* zip, const QString& subdir, const QString& target) { auto target_top_dir = QUrl::fromLocalFile(target); QStringList extracted; qDebug() << "Extracting subdir" << subdir << "from" << zip->getZipName() << "to" << target; - auto numEntries = zip->getEntriesCount(); - if (numEntries < 0) { + if (!zip->collectFiles()) { qWarning() << "Failed to enumerate files in archive"; return std::nullopt; - } else if (numEntries == 0) { + } + if (zip->getFiles().isEmpty()) { qDebug() << "Extracting empty archives seems odd..."; return extracted; - } else if (!zip->goToFirstFile()) { - qWarning() << "Failed to seek to first file in zip"; - return std::nullopt; } - do { - QString file_name = zip->getCurrentFileName(); - file_name = FS::RemoveInvalidPathChars(file_name); - if (!file_name.startsWith(subdir)) - continue; + auto extPtr = ArchiveWriter::createDiskWriter(); + auto ext = extPtr.get(); - auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(subdir.size())); - auto original_name = relative_file_name; - - // Fix subdirs/files ending with a / getting transformed into absolute paths - if (relative_file_name.startsWith('/')) - relative_file_name = relative_file_name.mid(1); - - // Fix weird "folders with a single file get squashed" thing - QString sub_path; - if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) { - sub_path = relative_file_name.section('/', 0, -2) + '/'; - FS::ensureFolderPathExists(FS::PathCombine(target, sub_path)); - - relative_file_name = relative_file_name.split('/').last(); - } - - QString target_file_path; - if (relative_file_name.isEmpty()) { - target_file_path = target + '/'; - } else { - target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name); - if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/')) - target_file_path += '/'; - } - - if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) { - qWarning() << "Extracting" << relative_file_name << "was cancelled, because it was effectively outside of the target path" - << target; - return std::nullopt; - } - - if (!JlCompress::extractFile(zip, "", target_file_path)) { - qWarning() << "Failed to extract file" << original_name << "to" << target_file_path; - JlCompress::removeFile(extracted); - return std::nullopt; - } - - extracted.append(target_file_path); - auto fileInfo = QFileInfo(target_file_path); - if (fileInfo.isFile()) { - auto permissions = fileInfo.permissions(); - auto maxPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser | - QFileDevice::Permission::ReadGroup | QFileDevice::Permission::ReadOther; - auto minPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; - - auto newPermisions = (permissions & maxPermisions) | minPermisions; - if (newPermisions != permissions) { - if (!QFile::setPermissions(target_file_path, newPermisions)) { - qWarning() << (QObject::tr("Could not fix permissions for %1").arg(target_file_path)); - } + if (!zip->parse([&subdir, &target, &target_top_dir, ext, &extracted](ArchiveReader::File* f) { + QString file_name = f->filename(); + file_name = FS::RemoveInvalidPathChars(file_name); + if (!file_name.startsWith(subdir)) { + f->skip(); + return true; } - } else if (fileInfo.isDir()) { - // Ensure the folder has the minimal required permissions - QFile::Permissions minimalPermissions = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner | QFile::ReadGroup | - QFile::ExeGroup | QFile::ReadOther | QFile::ExeOther; - QFile::Permissions currentPermissions = fileInfo.permissions(); - if ((currentPermissions & minimalPermissions) != minimalPermissions) { - if (!QFile::setPermissions(target_file_path, minimalPermissions)) { - qWarning() << (QObject::tr("Could not fix permissions for %1").arg(target_file_path)); - } + auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(subdir.size())); + auto original_name = relative_file_name; + + // Fix subdirs/files ending with a / getting transformed into absolute paths + if (relative_file_name.startsWith('/')) + relative_file_name = relative_file_name.mid(1); + + // Fix weird "folders with a single file get squashed" thing + QString sub_path; + if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) { + sub_path = relative_file_name.section('/', 0, -2) + '/'; + FS::ensureFolderPathExists(FS::PathCombine(target, sub_path)); + + relative_file_name = relative_file_name.split('/').last(); } - } - qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; - } while (zip->goToNextFile()); + QString target_file_path; + if (relative_file_name.isEmpty()) { + target_file_path = target + '/'; + } else { + target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name); + if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/')) + target_file_path += '/'; + } + + if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) { + qWarning() << "Extracting" << relative_file_name << "was cancelled, because it was effectively outside of the target path" + << target; + return false; + } + if (!f->writeFile(ext, target_file_path)) { + qWarning() << "Failed to extract file" << original_name << "to" << target_file_path; + return false; + } + + extracted.append(target_file_path); + + qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; + return true; + })) { + qWarning() << "Failed to parse file" << zip->getZipName(); + FS::removeFiles(extracted); + return std::nullopt; + } return extracted; } -// ours -bool extractRelFile(QuaZip* zip, const QString& file, const QString& target) -{ - return JlCompress::extractFile(zip, file, target); -} - // ours std::optional 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 unpacking:" << fileCompressed << "Error:" << zip.getZipError(); - ; - return std::nullopt; + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if (fileInfo.size() == 22) { + return QStringList(); } + ArchiveReader zip(fileCompressed); return extractSubDir(&zip, "", dir); } // ours std::optional 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(); - } - qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError(); - ; - return std::nullopt; + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if (fileInfo.size() == 22) { + return QStringList(); } + ArchiveReader zip(fileCompressed); return extractSubDir(&zip, subdir, dir); } // ours bool extractFile(QString fileCompressed, QString file, QString target) { - 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 true; - } - qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError(); + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if (fileInfo.size() == 22) { + return true; + } + ArchiveReader zip(fileCompressed); + auto f = zip.goToFile(file); + if (!f) { return false; } - return extractRelFile(&zip, file, target); + auto extPtr = ArchiveWriter::createDiskWriter(); + auto ext = extPtr.get(); + + return f->writeFile(ext, target); } bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFileFunction excludeFilter) @@ -453,218 +329,4 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q } return true; } - -#if defined(LAUNCHER_APPLICATION) -void ExportToZipTask::executeTask() -{ - setStatus("Adding files..."); - setProgress(0, m_files.length()); - m_build_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return exportZip(); }); - connect(&m_build_zip_watcher, &QFutureWatcher::finished, this, &ExportToZipTask::finish); - m_build_zip_watcher.setFuture(m_build_zip_future); -} - -auto ExportToZipTask::exportZip() -> ZipResult -{ - if (!m_dir.exists()) { - return ZipResult(tr("Folder doesn't exist")); - } - if (!m_output.isOpen() && !m_output.open(QuaZip::mdCreate)) { - return ZipResult(tr("Could not create file")); - } - - for (auto fileName : m_extra_files.keys()) { - if (m_build_zip_future.isCanceled()) - return ZipResult(); - QuaZipFile indexFile(&m_output); - if (!indexFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileName))) { - return ZipResult(tr("Could not create:") + fileName); - } - indexFile.write(m_extra_files[fileName]); - } - - for (const QFileInfo& file : m_files) { - if (m_build_zip_future.isCanceled()) - return ZipResult(); - - auto absolute = file.absoluteFilePath(); - auto relative = m_dir.relativeFilePath(absolute); - setStatus("Compressing: " + relative); - setProgress(m_progress + 1, m_progressTotal); - if (m_follow_symlinks) { - if (file.isSymLink()) - absolute = file.symLinkTarget(); - else - absolute = file.canonicalFilePath(); - } - - if (!m_exclude_files.contains(relative) && !JlCompress::compressFile(&m_output, absolute, m_destination_prefix + relative)) { - return ZipResult(tr("Could not read and compress %1").arg(relative)); - } - } - - m_output.close(); - if (m_output.getZipError() != 0) { - return ZipResult(tr("A zip error occurred")); - } - return ZipResult(); -} - -void ExportToZipTask::finish() -{ - if (m_build_zip_future.isCanceled()) { - FS::deletePath(m_output_path); - emitAborted(); - } else if (auto result = m_build_zip_future.result(); result.has_value()) { - FS::deletePath(m_output_path); - emitFailed(result.value()); - } else { - emitSucceeded(); - } -} - -bool ExportToZipTask::abort() -{ - if (m_build_zip_future.isRunning()) { - m_build_zip_future.cancel(); - // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur - // immediately. - return true; - } - return false; -} - -void ExtractZipTask::executeTask() -{ - if (!m_input->isOpen() && !m_input->open(QuaZip::mdUnzip)) { - emitFailed(tr("Unable to open supplied zip file.")); - return; - } - m_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return extractZip(); }); - connect(&m_zip_watcher, &QFutureWatcher::finished, this, &ExtractZipTask::finish); - m_zip_watcher.setFuture(m_zip_future); -} - -auto ExtractZipTask::extractZip() -> ZipResult -{ - auto target = m_output_dir.absolutePath(); - auto target_top_dir = QUrl::fromLocalFile(target); - - QStringList extracted; - - qDebug() << "Extracting subdir" << m_subdirectory << "from" << m_input->getZipName() << "to" << target; - auto numEntries = m_input->getEntriesCount(); - if (numEntries < 0) { - return ZipResult(tr("Failed to enumerate files in archive")); - } - if (numEntries == 0) { - logWarning(tr("Extracting empty archives seems odd...")); - return ZipResult(); - } - if (!m_input->goToFirstFile()) { - return ZipResult(tr("Failed to seek to first file in zip")); - } - - setStatus("Extracting files..."); - setProgress(0, numEntries); - do { - if (m_zip_future.isCanceled()) - return ZipResult(); - setProgress(m_progress + 1, m_progressTotal); - QString file_name = m_input->getCurrentFileName(); - if (!file_name.startsWith(m_subdirectory)) - continue; - - auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(m_subdirectory.size())); - auto original_name = relative_file_name; - setStatus("Unpacking: " + relative_file_name); - - // Fix subdirs/files ending with a / getting transformed into absolute paths - if (relative_file_name.startsWith('/')) - relative_file_name = relative_file_name.mid(1); - - // Fix weird "folders with a single file get squashed" thing - QString sub_path; - if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) { - sub_path = relative_file_name.section('/', 0, -2) + '/'; - FS::ensureFolderPathExists(FS::PathCombine(target, sub_path)); - - relative_file_name = relative_file_name.split('/').last(); - } - - QString target_file_path; - if (relative_file_name.isEmpty()) { - target_file_path = target + '/'; - } else { - target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name); - if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/')) - target_file_path += '/'; - } - - if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) { - return ZipResult(tr("Extracting %1 was cancelled, because it was effectively outside of the target path %2") - .arg(relative_file_name, target)); - } - - if (!JlCompress::extractFile(m_input.get(), "", target_file_path)) { - JlCompress::removeFile(extracted); - return ZipResult(tr("Failed to extract file %1 to %2").arg(original_name, target_file_path)); - } - - extracted.append(target_file_path); - auto fileInfo = QFileInfo(target_file_path); - if (fileInfo.isFile()) { - auto permissions = fileInfo.permissions(); - auto maxPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser | - QFileDevice::Permission::ReadGroup | QFileDevice::Permission::ReadOther; - auto minPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; - - auto newPermisions = (permissions & maxPermisions) | minPermisions; - if (newPermisions != permissions) { - if (!QFile::setPermissions(target_file_path, newPermisions)) { - logWarning(tr("Could not fix permissions for %1").arg(target_file_path)); - } - } - } else if (fileInfo.isDir()) { - // Ensure the folder has the minimal required permissions - QFile::Permissions minimalPermissions = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner | QFile::ReadGroup | - QFile::ExeGroup | QFile::ReadOther | QFile::ExeOther; - - QFile::Permissions currentPermissions = fileInfo.permissions(); - if ((currentPermissions & minimalPermissions) != minimalPermissions) { - if (!QFile::setPermissions(target_file_path, minimalPermissions)) { - logWarning(tr("Could not fix permissions for %1").arg(target_file_path)); - } - } - } - - qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; - } while (m_input->goToNextFile()); - - return ZipResult(); -} - -void ExtractZipTask::finish() -{ - if (m_zip_future.isCanceled()) { - emitAborted(); - } else if (auto result = m_zip_future.result(); result.has_value()) { - emitFailed(result.value()); - } else { - emitSucceeded(); - } -} - -bool ExtractZipTask::abort() -{ - if (m_zip_future.isRunning()) { - m_zip_future.cancel(); - // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur - // immediately. - return true; - } - return false; -} - -#endif } // namespace MMCZip diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h index e23d29d65..04fe90379 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -36,8 +36,6 @@ #pragma once -#include -#include #include #include #include @@ -46,72 +44,27 @@ #include #include #include -#include #include +#include "archive/ArchiveReader.h" #if defined(LAUNCHER_APPLICATION) #include "minecraft/mod/Mod.h" #endif -#include "Filter.h" -#include "tasks/Task.h" namespace MMCZip { using FilterFileFunction = std::function; -/** - * Merge two zip files, using a filter function - */ -bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet& contained, const Filter& filter = nullptr); - -/** - * Compress directory, by providing a list of files to compress - * \param zip target archive - * \param dir directory that will be compressed (to compress with relative paths) - * \param files list of files to compress - * \param followSymlinks should follow symlinks when compressing file data - * \return true for success or false for failure - */ -bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool followSymlinks = false); - -/** - * Compress directory, by providing a list of files to compress - * \param fileCompressed target archive file - * \param dir directory that will be compressed (to compress with relative paths) - * \param files list of files to compress - * \param followSymlinks should follow symlinks when compressing file data - * \return true for success or false for failure - */ -bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks = false); - #if defined(LAUNCHER_APPLICATION) /** * take a source jar, add mods to it, resulting in target jar */ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods); #endif -/** - * Find a single file in archive by file name (not path) - * - * \param ignore_paths paths to skip when recursing the search - * - * \return the path prefix where the file is - */ -QString findFolderOfFileInZip(QuaZip* zip, const QString& what, const QStringList& ignore_paths = {}, 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 - * - * \return true if anything was found - */ -bool findFilesInZip(QuaZip* zip, const QString& what, QStringList& result, const QString& root = QString()); /** * Extract a subdirectory from an archive */ -std::optional extractSubDir(QuaZip* zip, const QString& subdir, const QString& target); - -bool extractRelFile(QuaZip* zip, const QString& file, const QString& target); +std::optional extractSubDir(ArchiveReader* zip, const QString& subdir, const QString& target); /** * Extract a whole archive. @@ -151,90 +104,4 @@ bool extractFile(QString fileCompressed, QString file, QString dir); * \return true for success or false for failure */ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFileFunction excludeFilter); - -#if defined(LAUNCHER_APPLICATION) -class ExportToZipTask : public Task { - Q_OBJECT - public: - ExportToZipTask(QString outputPath, - QDir dir, - QFileInfoList files, - QString destinationPrefix = "", - bool followSymlinks = false, - bool utf8Enabled = false) - : m_output_path(outputPath) - , m_output(outputPath) - , m_dir(dir) - , m_files(files) - , m_destination_prefix(destinationPrefix) - , m_follow_symlinks(followSymlinks) - { - setAbortable(true); - m_output.setUtf8Enabled(utf8Enabled); - }; - ExportToZipTask(QString outputPath, - QString dir, - QFileInfoList files, - QString destinationPrefix = "", - bool followSymlinks = false, - bool utf8Enabled = false) - : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks, utf8Enabled) {}; - - virtual ~ExportToZipTask() = default; - - void setExcludeFiles(QStringList excludeFiles) { m_exclude_files = excludeFiles; } - void addExtraFile(QString fileName, QByteArray data) { m_extra_files.insert(fileName, data); } - - using ZipResult = std::optional; - - protected: - virtual void executeTask() override; - bool abort() override; - - ZipResult exportZip(); - void finish(); - - private: - QString m_output_path; - QuaZip m_output; - QDir m_dir; - QFileInfoList m_files; - QString m_destination_prefix; - bool m_follow_symlinks; - QStringList m_exclude_files; - QHash m_extra_files; - - QFuture m_build_zip_future; - QFutureWatcher m_build_zip_watcher; -}; - -class ExtractZipTask : public Task { - Q_OBJECT - public: - ExtractZipTask(QString input, QDir outputDir, QString subdirectory = "") - : ExtractZipTask(std::make_shared(input), outputDir, subdirectory) - {} - ExtractZipTask(std::shared_ptr input, QDir outputDir, QString subdirectory = "") - : m_input(input), m_output_dir(outputDir), m_subdirectory(subdirectory) - {} - virtual ~ExtractZipTask() = default; - - using ZipResult = std::optional; - - protected: - virtual void executeTask() override; - bool abort() override; - - ZipResult extractZip(); - void finish(); - - private: - std::shared_ptr m_input; - QDir m_output_dir; - QString m_subdirectory; - - QFuture m_zip_future; - QFutureWatcher m_zip_watcher; -}; -#endif } // namespace MMCZip diff --git a/launcher/Untar.cpp b/launcher/Untar.cpp deleted file mode 100644 index f1963e7aa..000000000 --- a/launcher/Untar.cpp +++ /dev/null @@ -1,260 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2023-2024 Trial97 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#include "Untar.h" -#include -#include -#include -#include -#include -#include "FileSystem.h" - -// adaptation of the: -// - https://github.com/madler/zlib/blob/develop/contrib/untgz/untgz.c -// - https://en.wikipedia.org/wiki/Tar_(computing) -// - https://github.com/euroelessar/cutereader/blob/master/karchive/src/ktar.cpp - -#define BLOCKSIZE 512 -#define SHORTNAMESIZE 100 - -enum class TypeFlag : char { - Regular = '0', // regular file - ARegular = 0, // regular file - Link = '1', // link - Symlink = '2', // reserved - Character = '3', // character special - Block = '4', // block special - Directory = '5', // directory - FIFO = '6', // FIFO special - Contiguous = '7', // reserved - // Posix stuff - GlobalPosixHeader = 'g', - ExtendedPosixHeader = 'x', - // 'A'– 'Z' Vendor specific extensions(POSIX .1 - 1988) - // GNU - GNULongLink = 'K', /* long link name */ - GNULongName = 'L', /* long file name */ -}; - -// struct Header { /* byte offset */ -// char name[100]; /* 0 */ -// char mode[8]; /* 100 */ -// char uid[8]; /* 108 */ -// char gid[8]; /* 116 */ -// char size[12]; /* 124 */ -// char mtime[12]; /* 136 */ -// char chksum[8]; /* 148 */ -// TypeFlag typeflag; /* 156 */ -// char linkname[100]; /* 157 */ -// char magic[6]; /* 257 */ -// char version[2]; /* 263 */ -// char uname[32]; /* 265 */ -// char gname[32]; /* 297 */ -// char devmajor[8]; /* 329 */ -// char devminor[8]; /* 337 */ -// char prefix[155]; /* 345 */ -// /* 500 */ -// }; - -bool readLonglink(QIODevice* in, qint64 size, QByteArray& longlink) -{ - qint64 n = 0; - size--; // ignore trailing null - if (size < 0) { - qCritical() << "The filename size is negative"; - return false; - } - longlink.resize(size + (BLOCKSIZE - size % BLOCKSIZE)); // make the size divisible by BLOCKSIZE - for (qint64 offset = 0; offset < longlink.size(); offset += BLOCKSIZE) { - n = in->read(longlink.data() + offset, BLOCKSIZE); - if (n != BLOCKSIZE) { - qCritical() << "The expected blocksize was not respected for the name"; - return false; - } - } - longlink.truncate(qstrlen(longlink.constData())); - return true; -} - -int getOctal(char* buffer, int maxlenght, bool* ok) -{ - return QByteArray(buffer, qstrnlen(buffer, maxlenght)).toInt(ok, 8); -} - -QString decodeName(char* name) -{ - return QFile::decodeName(QByteArray(name, qstrnlen(name, 100))); -} -bool Tar::extract(QIODevice* in, QString dst) -{ - char buffer[BLOCKSIZE]; - QString name, symlink, firstFolderName; - bool doNotReset = false, ok; - while (true) { - auto n = in->read(buffer, BLOCKSIZE); - if (n != BLOCKSIZE) { // allways expect complete blocks - qCritical() << "The expected blocksize was not respected"; - return false; - } - if (buffer[0] == 0) { // end of archive - return true; - } - int mode = getOctal(buffer + 100, 8, &ok) | QFile::ReadUser | QFile::WriteUser; // hack to ensure write and read permisions - if (!ok) { - qCritical() << "The file mode can't be read"; - return false; - } - // there are names that are exactly 100 bytes long - // and neither longlink nor \0 terminated (bug:101472) - - if (name.isEmpty()) { - name = decodeName(buffer); - if (!firstFolderName.isEmpty() && name.startsWith(firstFolderName)) { - name = name.mid(firstFolderName.size()); - } - } - if (symlink.isEmpty()) - symlink = decodeName(buffer); - qint64 size = getOctal(buffer + 124, 12, &ok); - if (!ok) { - qCritical() << "The file size can't be read"; - return false; - } - switch (TypeFlag(buffer[156])) { - case TypeFlag::Regular: - /* fallthrough */ - case TypeFlag::ARegular: { - auto fileName = FS::PathCombine(dst, name); - if (!FS::ensureFilePathExists(fileName)) { - qCritical() << "Can't ensure the file path to exist: " << fileName; - return false; - } - QFile out(fileName); - if (!out.open(QFile::WriteOnly)) { - qCritical() << "Can't open file:" << fileName; - return false; - } - out.setPermissions(QFile::Permissions(mode)); - while (size > 0) { - QByteArray tmp(BLOCKSIZE, 0); - n = in->read(tmp.data(), BLOCKSIZE); - if (n != BLOCKSIZE) { - qCritical() << "The expected blocksize was not respected when reading file"; - return false; - } - tmp.truncate(qMin(qint64(BLOCKSIZE), size)); - out.write(tmp); - size -= BLOCKSIZE; - } - break; - } - case TypeFlag::Directory: { - if (firstFolderName.isEmpty()) { - firstFolderName = name; - break; - } - auto folderPath = FS::PathCombine(dst, name); - if (!FS::ensureFolderPathExists(folderPath)) { - qCritical() << "Can't ensure that folder exists: " << folderPath; - return false; - } - break; - } - case TypeFlag::GNULongLink: { - doNotReset = true; - QByteArray longlink; - if (readLonglink(in, size, longlink)) { - symlink = QFile::decodeName(longlink.constData()); - } else { - qCritical() << "Failed to read long link"; - return false; - } - break; - } - case TypeFlag::GNULongName: { - doNotReset = true; - QByteArray longlink; - if (readLonglink(in, size, longlink)) { - name = QFile::decodeName(longlink.constData()); - } else { - qCritical() << "Failed to read long name"; - return false; - } - break; - } - case TypeFlag::Link: - /* fallthrough */ - case TypeFlag::Symlink: { - auto fileName = FS::PathCombine(dst, name); - if (!FS::create_link(FS::PathCombine(QFileInfo(fileName).path(), symlink), fileName)()) { // do not use symlinks - qCritical() << "Can't create link for:" << fileName << " to:" << FS::PathCombine(QFileInfo(fileName).path(), symlink); - return false; - } - FS::ensureFilePathExists(fileName); - QFile::setPermissions(fileName, QFile::Permissions(mode)); - break; - } - case TypeFlag::Character: - /* fallthrough */ - case TypeFlag::Block: - /* fallthrough */ - case TypeFlag::FIFO: - /* fallthrough */ - case TypeFlag::Contiguous: - /* fallthrough */ - case TypeFlag::GlobalPosixHeader: - /* fallthrough */ - case TypeFlag::ExtendedPosixHeader: - /* fallthrough */ - default: - break; - } - if (!doNotReset) { - name.truncate(0); - symlink.truncate(0); - } - doNotReset = false; - } - return true; -} - -bool GZTar::extract(QString src, QString dst) -{ - QuaGzipFile a(src); - if (!a.open(QIODevice::ReadOnly)) { - qCritical() << "Can't open tar file:" << src; - return false; - } - return Tar::extract(&a, dst); -} \ No newline at end of file diff --git a/launcher/Untar.h b/launcher/Untar.h deleted file mode 100644 index 50e3a16e3..000000000 --- a/launcher/Untar.h +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2023-2024 Trial97 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#pragma once -#include - -// this is a hack used for the java downloader (feel free to remove it in favor of a library) -// both extract functions will extract the first folder inside dest(disregarding the prefix) -namespace Tar { -bool extract(QIODevice* in, QString dst); -} - -namespace GZTar { -bool extract(QString src, QString dst); -} \ No newline at end of file diff --git a/launcher/archive/ArchiveReader.cpp b/launcher/archive/ArchiveReader.cpp new file mode 100644 index 000000000..a866b49c5 --- /dev/null +++ b/launcher/archive/ArchiveReader.cpp @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: GPL-3.0-only AND LicenseRef-PublicDomain +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Additional note: Portions of this file are released into the public domain + * under LicenseRef-PublicDomain. + */ +#include "ArchiveReader.h" +#include +#include +#include +#include + +namespace MMCZip { +QStringList ArchiveReader::getFiles() +{ + return m_fileNames; +} + +bool ArchiveReader::collectFiles(bool onlyFiles) +{ + return parse([this, onlyFiles](File* f) { + if (!onlyFiles || f->isFile()) + m_fileNames << f->filename(); + return f->skip(); + }); +} + +QString ArchiveReader::File::filename() +{ + return QString::fromUtf8(archive_entry_pathname(m_entry)); +} + +QByteArray ArchiveReader::File::readAll(int* outStatus) +{ + QByteArray data; + const void* buff; + size_t size; + la_int64_t offset; + + int status; + while ((status = archive_read_data_block(m_archive.get(), &buff, &size, &offset)) == ARCHIVE_OK) { + data.append(static_cast(buff), static_cast(size)); + } + if (status != ARCHIVE_EOF && status != ARCHIVE_OK) { + qWarning() << "libarchive read error: " << archive_error_string(m_archive.get()); + } + if (outStatus) { + *outStatus = status; + } + return data; +} + +QDateTime ArchiveReader::File::dateTime() +{ + auto mtime = archive_entry_mtime(m_entry); + auto mtime_nsec = archive_entry_mtime_nsec(m_entry); + auto dt = QDateTime::fromSecsSinceEpoch(mtime); + return dt.addMSecs(mtime_nsec / 1e6); +} + +int ArchiveReader::File::readNextHeader() +{ + return archive_read_next_header(m_archive.get(), &m_entry); +} + +auto ArchiveReader::goToFile(QString filename) -> std::unique_ptr +{ + auto f = std::make_unique(); + auto a = f->m_archive.get(); + archive_read_support_format_all(a); + archive_read_support_filter_all(a); + auto fileName = m_archivePath.toUtf8(); + if (archive_read_open_filename(a, fileName.constData(), m_blockSize) != ARCHIVE_OK) { + qCritical() << "Failed to open archive file:" << m_archivePath << "-" << archive_error_string(a); + return nullptr; + } + + while (f->readNextHeader() == ARCHIVE_OK) { + if (f->filename() == filename) { + return f; + } + f->skip(); + } + + archive_read_close(a); + return nullptr; +} + +static int copy_data(struct archive* ar, struct archive* aw, bool notBlock = false) +{ + int r; + const void* buff; + size_t size; + la_int64_t offset; + + for (;;) { + r = archive_read_data_block(ar, &buff, &size, &offset); + if (r == ARCHIVE_EOF) + return (ARCHIVE_OK); + if (r < ARCHIVE_OK) { + qCritical() << "Failed reading data block:" << archive_error_string(ar); + return (r); + } + if (notBlock) { + r = archive_write_data(aw, buff, size); + } else { + r = archive_write_data_block(aw, buff, size, offset); + } + if (r < ARCHIVE_OK) { + qCritical() << "Failed writing data block:" << archive_error_string(aw); + return (r); + } + } +} + +bool ArchiveReader::File::writeFile(archive* out, QString targetFileName, bool notBlock) +{ + auto entry = m_entry; + if (!targetFileName.isEmpty()) { + entry = archive_entry_clone(m_entry); + auto nameUtf8 = targetFileName.toUtf8(); + archive_entry_set_pathname(entry, nameUtf8.constData()); + } + if (archive_write_header(out, entry) < ARCHIVE_OK) { + qCritical() << "Failed to write header to entry:" << filename() << "-" << archive_error_string(out); + return false; + } else if (archive_entry_size(m_entry) > 0) { + auto r = copy_data(m_archive.get(), out, notBlock); + if (r < ARCHIVE_OK) + qCritical() << "Failed reading data block:" << archive_error_string(out); + if (r < ARCHIVE_WARN) + return false; + } + auto r = archive_write_finish_entry(out); + if (r < ARCHIVE_OK) + qCritical() << "Failed to finish writing entry:" << archive_error_string(out); + return (r > ARCHIVE_WARN); +} + +bool ArchiveReader::parse(std::function doStuff) +{ + auto f = std::make_unique(); + auto a = f->m_archive.get(); + archive_read_support_format_all(a); + archive_read_support_filter_all(a); + auto fileName = m_archivePath.toUtf8(); + if (archive_read_open_filename(a, fileName.constData(), m_blockSize) != ARCHIVE_OK) { + qCritical() << "Failed to open archive file:" << m_archivePath << "-" << f->error(); + return false; + } + + bool breakControl = false; + while (f->readNextHeader() == ARCHIVE_OK) { + if (!doStuff(f.get(), breakControl)) { + qCritical() << "Failed to parse file:" << f->filename() << "-" << f->error(); + return false; + } + if (breakControl) { + break; + } + } + + archive_read_close(a); + return true; +} +bool ArchiveReader::parse(std::function doStuff) +{ + return parse([doStuff](File* f, bool&) { return doStuff(f); }); +} + +bool ArchiveReader::File::isFile() +{ + return (archive_entry_filetype(m_entry) & AE_IFMT) == AE_IFREG; +} +bool ArchiveReader::File::skip() +{ + return archive_read_data_skip(m_archive.get()) == ARCHIVE_OK; +} +const char* ArchiveReader::File::error() +{ + return archive_error_string(m_archive.get()); +} +QString ArchiveReader::getZipName() +{ + return m_archivePath; +} + +bool ArchiveReader::exists(const QString& filePath) const +{ + if (filePath == QLatin1String("/") || filePath.isEmpty()) + return true; + // Normalize input path (remove trailing slash, if any) + QString normalizedPath = QDir::cleanPath(filePath); + if (normalizedPath.startsWith('/')) + normalizedPath.remove(0, 1); + if (normalizedPath == QLatin1String(".")) + return true; + if (normalizedPath == QLatin1String("..")) + return false; // root only + + // Check for exact file match + if (m_fileNames.contains(normalizedPath, Qt::CaseInsensitive)) + return true; + + // Check for directory existence by seeing if any file starts with that path + QString dirPath = normalizedPath + QLatin1Char('/'); + for (const QString& f : m_fileNames) { + if (f.startsWith(dirPath, Qt::CaseInsensitive)) + return true; + } + + return false; +} + +ArchiveReader::File::File() : m_archive(ArchivePtr(archive_read_new(), archive_read_free)) {} +} // namespace MMCZip diff --git a/launcher/archive/ArchiveReader.h b/launcher/archive/ArchiveReader.h new file mode 100644 index 000000000..379006278 --- /dev/null +++ b/launcher/archive/ArchiveReader.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include +#include +#include +#include + +struct archive; +struct archive_entry; +namespace MMCZip { +class ArchiveReader { + public: + using ArchivePtr = std::unique_ptr; + ArchiveReader(QString fileName) : m_archivePath(fileName) {} + virtual ~ArchiveReader() = default; + + QStringList getFiles(); + QString getZipName(); + bool collectFiles(bool onlyFiles = true); + bool exists(const QString& filePath) const; + + class File { + public: + File(); + virtual ~File() = default; + + QString filename(); + bool isFile(); + QDateTime dateTime(); + const char* error(); + + QByteArray readAll(int* outStatus = nullptr); + bool skip(); + bool writeFile(archive* out, QString targetFileName = "", bool notBlock = false); + + private: + int readNextHeader(); + + private: + friend ArchiveReader; + ArchivePtr m_archive; + archive_entry* m_entry; + }; + + std::unique_ptr goToFile(QString filename); + bool parse(std::function); + bool parse(std::function); + + private: + QString m_archivePath; + size_t m_blockSize = 10240; + + QStringList m_fileNames = {}; +}; +} // namespace MMCZip \ No newline at end of file diff --git a/launcher/archive/ArchiveWriter.cpp b/launcher/archive/ArchiveWriter.cpp new file mode 100644 index 000000000..87cead69c --- /dev/null +++ b/launcher/archive/ArchiveWriter.cpp @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "ArchiveWriter.h" +#include +#include +#include + +#include +#include + +#include + +namespace MMCZip { + +ArchiveWriter::ArchiveWriter(const QString& archiveName) : m_filename(archiveName) {} + +ArchiveWriter::~ArchiveWriter() +{ + close(); +} + +bool ArchiveWriter::open() +{ + if (m_filename.isEmpty()) { + qCritical() << "Archive m_filename not set."; + return false; + } + + m_archive = archive_write_new(); + if (!m_archive) { + qCritical() << "Archive not initialized."; + return false; + } + + auto format = m_format.toUtf8(); + archive_write_set_format_by_name(m_archive, format.constData()); + + if (archive_write_set_options(m_archive, "hdrcharset=UTF-8") != ARCHIVE_OK) { + qCritical() << "Failed to open archive file:" << m_filename << "-" << archive_error_string(m_archive); + return false; + } + + auto archiveNameUtf8 = m_filename.toUtf8(); + if (archive_write_open_filename(m_archive, archiveNameUtf8.constData()) != ARCHIVE_OK) { + qCritical() << "Failed to open archive file:" << m_filename << "-" << archive_error_string(m_archive); + return false; + } + + return true; +} + +bool ArchiveWriter::close() +{ + bool success = true; + if (m_archive) { + if (archive_write_close(m_archive) != ARCHIVE_OK) { + qCritical() << "Failed to close archive" << m_filename << "-" << archive_error_string(m_archive); + success = false; + } + if (archive_write_free(m_archive) != ARCHIVE_OK) { + qCritical() << "Failed to free archive" << m_filename << "-" << archive_error_string(m_archive); + success = false; + } + m_archive = nullptr; + } + return success; +} + +bool ArchiveWriter::addFile(const QString& fileName, const QString& fileDest) +{ + QFileInfo fileInfo(fileName); + if (!fileInfo.exists()) { + qCritical() << "File does not exist:" << fileInfo.filePath(); + return false; + } + + std::unique_ptr entry_ptr(archive_entry_new(), archive_entry_free); + auto entry = entry_ptr.get(); + if (!entry) { + qCritical() << "Failed to create archive entry"; + return false; + } + + auto fileDestUtf8 = fileDest.toUtf8(); + archive_entry_set_pathname(entry, fileDestUtf8.constData()); + + QByteArray utf8 = fileInfo.absoluteFilePath().toUtf8(); + const char* cpath = utf8.constData(); + struct stat st; + if (stat(cpath, &st) != 0) { + qCritical() << "Failed to stat file:" << fileInfo.filePath(); + } + // This should handle the copying of most attributes + archive_entry_copy_stat(entry, &st); + + // However: + // "The [filetype] constants used by stat(2) may have different numeric values from the corresponding [libarchive constants]." + // - `archive_entry_stat(3)` + if (fileInfo.isSymLink()) { + archive_entry_set_filetype(entry, AE_IFLNK); + + // We also need to manually copy some attributes from the link itself, as `stat` above operates on its target + auto target = fileInfo.symLinkTarget().toUtf8(); + archive_entry_set_symlink(entry, target.constData()); + archive_entry_set_size(entry, 0); + archive_entry_set_perm(entry, fileInfo.permissions()); + } else if (fileInfo.isFile()) { + archive_entry_set_filetype(entry, AE_IFREG); + } else { + qCritical() << "Unsupported file type:" << fileInfo.filePath(); + return false; + } + + if (archive_write_header(m_archive, entry) != ARCHIVE_OK) { + qCritical() << "Failed to write header for: " << fileDest << "-" << archive_error_string(m_archive); + return false; + } + + if (fileInfo.isFile() && !fileInfo.isSymLink()) { + QFile file(fileInfo.absoluteFilePath()); + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open file: " << fileInfo.filePath(); + return false; + } + + constexpr qint64 chunkSize = 8192; + QByteArray buffer; + buffer.resize(chunkSize); + + while (!file.atEnd()) { + auto bytesRead = file.read(buffer.data(), chunkSize); + if (bytesRead < 0) { + qCritical() << "Read error in file: " << fileInfo.filePath(); + return false; + } + + if (archive_write_data(m_archive, buffer.constData(), bytesRead) < 0) { + qCritical() << "Write error in archive for: " << fileDest; + return false; + } + } + } + + return true; +} + +bool ArchiveWriter::addFile(const QString& fileDest, const QByteArray& data) +{ + std::unique_ptr entry_ptr(archive_entry_new(), archive_entry_free); + auto entry = entry_ptr.get(); + if (!entry) { + qCritical() << "Failed to create archive entry"; + return false; + } + + auto fileDestUtf8 = fileDest.toUtf8(); + archive_entry_set_pathname(entry, fileDestUtf8.constData()); + archive_entry_set_perm(entry, 0644); + + archive_entry_set_filetype(entry, AE_IFREG); + archive_entry_set_size(entry, data.size()); + + if (archive_write_header(m_archive, entry) != ARCHIVE_OK) { + qCritical() << "Failed to write header for: " << fileDest << "-" << archive_error_string(m_archive); + return false; + } + + if (archive_write_data(m_archive, data.constData(), data.size()) < 0) { + qCritical() << "Write error in archive for: " << fileDest << "-" << archive_error_string(m_archive); + return false; + } + return true; +} + +bool ArchiveWriter::addFile(ArchiveReader::File* f) +{ + return f->writeFile(m_archive, "", true); +} + +std::unique_ptr ArchiveWriter::createDiskWriter() +{ + int flags = ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_ACL | ARCHIVE_EXTRACT_FFLAGS | + ARCHIVE_EXTRACT_SECURE_NODOTDOT | ARCHIVE_EXTRACT_SECURE_SYMLINKS; + + std::unique_ptr extPtr(archive_write_disk_new(), [](archive* a) { + if (a) { + archive_write_close(a); + archive_write_free(a); + } + }); + + archive* ext = extPtr.get(); + archive_write_disk_set_options(ext, flags); + archive_write_disk_set_standard_lookup(ext); + + return extPtr; +} +} // namespace MMCZip \ No newline at end of file diff --git a/launcher/archive/ArchiveWriter.h b/launcher/archive/ArchiveWriter.h new file mode 100644 index 000000000..807bb297c --- /dev/null +++ b/launcher/archive/ArchiveWriter.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include +#include +#include "archive/ArchiveReader.h" + +struct archive; +namespace MMCZip { + +class ArchiveWriter { + public: + ArchiveWriter(const QString& archiveName); + virtual ~ArchiveWriter(); + + bool open(); + bool close(); + + bool addFile(const QString& fileName, const QString& fileDest); + bool addFile(const QString& fileDest, const QByteArray& data); + bool addFile(ArchiveReader::File* f); + + static std::unique_ptr createDiskWriter(); + + private: + struct archive* m_archive = nullptr; + QString m_filename; + QString m_format = "zip"; +}; +} // namespace MMCZip \ No newline at end of file diff --git a/launcher/archive/ExportToZipTask.cpp b/launcher/archive/ExportToZipTask.cpp new file mode 100644 index 000000000..4886dc963 --- /dev/null +++ b/launcher/archive/ExportToZipTask.cpp @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "ExportToZipTask.h" + +#include + +#include "FileSystem.h" + +namespace MMCZip { +void ExportToZipTask::executeTask() +{ + setStatus("Adding files..."); + setProgress(0, m_files.length()); + m_build_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return exportZip(); }); + connect(&m_build_zip_watcher, &QFutureWatcher::finished, this, &ExportToZipTask::finish); + m_build_zip_watcher.setFuture(m_build_zip_future); +} + +auto ExportToZipTask::exportZip() -> ZipResult +{ + if (!m_dir.exists()) { + return ZipResult(tr("Folder doesn't exist")); + } + if (!m_output.open()) { + return ZipResult(tr("Could not create file")); + } + + for (auto fileName : m_extra_files.keys()) { + if (m_build_zip_future.isCanceled()) + return ZipResult(); + if (!m_output.addFile(fileName, m_extra_files[fileName])) { + return ZipResult(tr("Could not add:") + fileName); + } + } + + for (const QFileInfo& file : m_files) { + if (m_build_zip_future.isCanceled()) + return ZipResult(); + + auto absolute = file.absoluteFilePath(); + auto relative = m_dir.relativeFilePath(absolute); + setStatus("Compressing: " + relative); + setProgress(m_progress + 1, m_progressTotal); + if (m_follow_symlinks) { + if (file.isSymLink()) + absolute = file.symLinkTarget(); + else + absolute = file.canonicalFilePath(); + } + + if (!m_exclude_files.contains(relative) && !m_output.addFile(absolute, m_destination_prefix + relative)) { + return ZipResult(tr("Could not read and compress %1").arg(relative)); + } + } + + if (!m_output.close()) { + return ZipResult(tr("A zip error occurred")); + } + return ZipResult(); +} + +void ExportToZipTask::finish() +{ + if (m_build_zip_future.isCanceled()) { + FS::deletePath(m_output_path); + emitAborted(); + } else if (auto result = m_build_zip_future.result(); result.has_value()) { + FS::deletePath(m_output_path); + emitFailed(result.value()); + } else { + emitSucceeded(); + } +} + +bool ExportToZipTask::abort() +{ + if (m_build_zip_future.isRunning()) { + m_build_zip_future.cancel(); + // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur + // immediately. + return true; + } + return false; +} +} // namespace MMCZip \ No newline at end of file diff --git a/launcher/archive/ExportToZipTask.h b/launcher/archive/ExportToZipTask.h new file mode 100644 index 000000000..2e0a8273e --- /dev/null +++ b/launcher/archive/ExportToZipTask.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include +#include +#include +#include + +#include "archive/ArchiveWriter.h" +#include "tasks/Task.h" + +namespace MMCZip { +class ExportToZipTask : public Task { + Q_OBJECT + public: + ExportToZipTask(QString outputPath, QDir dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false) + : m_output_path(outputPath) + , m_output(outputPath) + , m_dir(dir) + , m_files(files) + , m_destination_prefix(destinationPrefix) + , m_follow_symlinks(followSymlinks) + { + setAbortable(true); + }; + ExportToZipTask(QString outputPath, QString dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false) + : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks) {}; + + virtual ~ExportToZipTask() = default; + + void setExcludeFiles(QStringList excludeFiles) { m_exclude_files = excludeFiles; } + void addExtraFile(QString fileName, QByteArray data) { m_extra_files.insert(fileName, data); } + + using ZipResult = std::optional; + + protected: + virtual void executeTask() override; + bool abort() override; + + ZipResult exportZip(); + void finish(); + + private: + QString m_output_path; + ArchiveWriter m_output; + QDir m_dir; + QFileInfoList m_files; + QString m_destination_prefix; + bool m_follow_symlinks; + QStringList m_exclude_files; + QHash m_extra_files; + + QFuture m_build_zip_future; + QFutureWatcher m_build_zip_watcher; +}; +} // namespace MMCZip \ No newline at end of file diff --git a/launcher/archive/ExtractZipTask.cpp b/launcher/archive/ExtractZipTask.cpp new file mode 100644 index 000000000..7dd002017 --- /dev/null +++ b/launcher/archive/ExtractZipTask.cpp @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "ExtractZipTask.h" +#include +#include "FileSystem.h" +#include "archive/ArchiveReader.h" +#include "archive/ArchiveWriter.h" + +namespace MMCZip { + +void ExtractZipTask::executeTask() +{ + m_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return extractZip(); }); + connect(&m_zip_watcher, &QFutureWatcher::finished, this, &ExtractZipTask::finish); + m_zip_watcher.setFuture(m_zip_future); +} + +auto ExtractZipTask::extractZip() -> ZipResult +{ + auto target = m_output_dir.absolutePath(); + auto target_top_dir = QUrl::fromLocalFile(target); + + QStringList extracted; + + qDebug() << "Extracting subdir" << m_subdirectory << "from" << m_input.getZipName() << "to" << target; + if (!m_input.collectFiles()) { + return ZipResult(tr("Failed to enumerate files in archive")); + } + if (m_input.getFiles().isEmpty()) { + logWarning(tr("Extracting empty archives seems odd...")); + return ZipResult(); + } + + auto extPtr = ArchiveWriter::createDiskWriter(); + auto ext = extPtr.get(); + + setStatus("Extracting files..."); + setProgress(0, m_input.getFiles().count()); + ZipResult result; + if (!m_input.parse([this, &result, &target, &target_top_dir, ext, &extracted](ArchiveReader::File* f) { + if (m_zip_future.isCanceled()) + return false; + setProgress(m_progress + 1, m_progressTotal); + QString file_name = f->filename(); + if (!file_name.startsWith(m_subdirectory)) { + f->skip(); + return true; + } + + auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(m_subdirectory.size())); + auto original_name = relative_file_name; + setStatus("Unpacking: " + relative_file_name); + + // Fix subdirs/files ending with a / getting transformed into absolute paths + if (relative_file_name.startsWith('/')) + relative_file_name = relative_file_name.mid(1); + + // Fix weird "folders with a single file get squashed" thing + QString sub_path; + if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) { + sub_path = relative_file_name.section('/', 0, -2) + '/'; + FS::ensureFolderPathExists(FS::PathCombine(target, sub_path)); + + relative_file_name = relative_file_name.split('/').last(); + } + + QString target_file_path; + if (relative_file_name.isEmpty()) { + target_file_path = target + '/'; + } else { + target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name); + if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/')) + target_file_path += '/'; + } + + if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) { + result = ZipResult(tr("Extracting %1 was cancelled, because it was effectively outside of the target path %2") + .arg(relative_file_name, target)); + return false; + } + + if (!f->writeFile(ext, target_file_path)) { + result = ZipResult(tr("Failed to extract file %1 to %2").arg(original_name, target_file_path)); + return false; + } + extracted.append(target_file_path); + + qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; + return true; + })) { + FS::removeFiles(extracted); + return result.has_value() || m_zip_future.isCanceled() ? result + : ZipResult(tr("Failed to parse file %1").arg(m_input.getZipName())); + } + return ZipResult(); +} + +void ExtractZipTask::finish() +{ + if (m_zip_future.isCanceled()) { + emitAborted(); + } else if (auto result = m_zip_future.result(); result.has_value()) { + emitFailed(result.value()); + } else { + emitSucceeded(); + } +} + +bool ExtractZipTask::abort() +{ + if (m_zip_future.isRunning()) { + m_zip_future.cancel(); + // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur + // immediately. + return true; + } + return false; +} + +} // namespace MMCZip \ No newline at end of file diff --git a/launcher/archive/ExtractZipTask.h b/launcher/archive/ExtractZipTask.h new file mode 100644 index 000000000..a9ad0a548 --- /dev/null +++ b/launcher/archive/ExtractZipTask.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include +#include +#include +#include "archive/ArchiveReader.h" +#include "tasks/Task.h" + +namespace MMCZip { + +class ExtractZipTask : public Task { + Q_OBJECT + public: + ExtractZipTask(QString input, QDir outputDir, QString subdirectory = "") + : m_input(input), m_output_dir(outputDir), m_subdirectory(subdirectory) + {} + virtual ~ExtractZipTask() = default; + + using ZipResult = std::optional; + + protected: + virtual void executeTask() override; + bool abort() override; + + ZipResult extractZip(); + void finish(); + + private: + ArchiveReader m_input; + QDir m_output_dir; + QString m_subdirectory; + + QFuture m_zip_future; + QFutureWatcher m_zip_watcher; +}; +} // namespace MMCZip \ No newline at end of file diff --git a/launcher/java/download/ArchiveDownloadTask.cpp b/launcher/java/download/ArchiveDownloadTask.cpp index bb31ca1e2..92357930b 100644 --- a/launcher/java/download/ArchiveDownloadTask.cpp +++ b/launcher/java/download/ArchiveDownloadTask.cpp @@ -16,12 +16,11 @@ * along with this program. If not, see . */ #include "java/download/ArchiveDownloadTask.h" -#include #include -#include "MMCZip.h" #include "Application.h" -#include "Untar.h" +#include "archive/ArchiveReader.h" +#include "archive/ExtractZipTask.h" #include "net/ChecksumValidator.h" #include "net/NetJob.h" #include "tasks/Task.h" @@ -67,68 +66,45 @@ void ArchiveDownloadTask::executeTask() void ArchiveDownloadTask::extractJava(QString input) { setStatus(tr("Extracting Java")); - if (input.endsWith("tar")) { - setStatus(tr("Extracting Java (Progress is not reported for tar archives)")); - QFile in(input); - if (!in.open(QFile::ReadOnly)) { - emitFailed(tr("Unable to open supplied tar file.")); - return; - } - if (!Tar::extract(&in, QDir(m_final_path).absolutePath())) { - emitFailed(tr("Unable to extract supplied tar file.")); - return; - } - emitSucceeded(); - return; - } else if (input.endsWith("tar.gz") || input.endsWith("taz") || input.endsWith("tgz")) { - setStatus(tr("Extracting Java (Progress is not reported for tar archives)")); - if (!GZTar::extract(input, QDir(m_final_path).absolutePath())) { - emitFailed(tr("Unable to extract supplied tar file.")); - return; - } - emitSucceeded(); - return; - } else if (input.endsWith("zip")) { - auto zip = std::make_shared(input); - if (!zip->open(QuaZip::mdUnzip)) { - emitFailed(tr("Unable to open supplied zip file.")); - return; - } - auto files = zip->getFileNameList(); - if (files.isEmpty()) { - emitFailed(tr("No files were found in the supplied zip file.")); - return; - } - m_task = makeShared(zip, m_final_path, files[0]); - auto progressStep = std::make_shared(); - connect(m_task.get(), &Task::finished, this, [this, progressStep] { - progressStep->state = TaskStepState::Succeeded; - stepProgress(*progressStep); - }); - - connect(m_task.get(), &Task::succeeded, this, &ArchiveDownloadTask::emitSucceeded); - connect(m_task.get(), &Task::aborted, this, &ArchiveDownloadTask::emitAborted); - connect(m_task.get(), &Task::failed, this, [this, progressStep](QString reason) { - progressStep->state = TaskStepState::Failed; - stepProgress(*progressStep); - emitFailed(reason); - }); - connect(m_task.get(), &Task::stepProgress, this, &ArchiveDownloadTask::propagateStepProgress); - - connect(m_task.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) { - progressStep->update(current, total); - stepProgress(*progressStep); - }); - connect(m_task.get(), &Task::status, this, [this, progressStep](QString status) { - progressStep->status = status; - stepProgress(*progressStep); - }); - m_task->start(); + MMCZip::ArchiveReader zip(input); + if (!zip.collectFiles()) { + emitFailed(tr("Unable to open supplied zip file.")); return; } + auto files = zip.getFiles(); + if (files.isEmpty()) { + emitFailed(tr("No files were found in the supplied zip file.")); + return; + } + auto firstFolderParts = files[0].split('/', Qt::SkipEmptyParts); + m_task = makeShared(input, m_final_path, firstFolderParts.value(0)); - emitFailed(tr("Could not determine archive type!")); + auto progressStep = std::make_shared(); + connect(m_task.get(), &Task::finished, this, [this, progressStep] { + progressStep->state = TaskStepState::Succeeded; + stepProgress(*progressStep); + }); + + connect(m_task.get(), &Task::succeeded, this, &ArchiveDownloadTask::emitSucceeded); + connect(m_task.get(), &Task::aborted, this, &ArchiveDownloadTask::emitAborted); + connect(m_task.get(), &Task::failed, this, [this, progressStep](QString reason) { + progressStep->state = TaskStepState::Failed; + stepProgress(*progressStep); + emitFailed(reason); + }); + connect(m_task.get(), &Task::stepProgress, this, &ArchiveDownloadTask::propagateStepProgress); + + connect(m_task.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) { + progressStep->update(current, total); + stepProgress(*progressStep); + }); + connect(m_task.get(), &Task::status, this, [this, progressStep](QString status) { + progressStep->status = status; + stepProgress(*progressStep); + }); + m_task->start(); + return; } bool ArchiveDownloadTask::abort() diff --git a/launcher/main.cpp b/launcher/main.cpp index 2bce655d2..46368e72e 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -37,6 +37,13 @@ int main(int argc, char* argv[]) { + // try to set the utf-8 locale for the libarchive + for (auto name : { ".UTF-8", "en_US.UTF-8", "C.UTF-8" }) { + if (std::setlocale(LC_CTYPE, name)) { + break; + } + } + // initialize Qt Application app(argc, argv); diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index 8ae097bad..bdbe721e3 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -43,9 +43,6 @@ #include #include #include -#include -#include -#include #include #include #include @@ -57,6 +54,7 @@ #include "FileSystem.h" #include "PSaveFile.h" +#include "archive/ArchiveReader.h" using std::nullopt; using std::optional; @@ -244,36 +242,25 @@ void World::readFromFS(const QFileInfo& file) void World::readFromZip(const QFileInfo& file) { - QuaZip zip(file.absoluteFilePath()); - m_isValid = zip.open(QuaZip::mdUnzip); - if (!m_isValid) { - return; - } - auto location = MMCZip::findFolderOfFileInZip(&zip, "level.dat"); - m_isValid = !location.isEmpty(); - if (!m_isValid) { - return; - } - m_containerOffsetPath = location; - QuaZipFile zippedFile(&zip); - // read the install profile - m_isValid = zip.setCurrentFile(location + "level.dat"); - if (!m_isValid) { - return; - } - m_isValid = zippedFile.open(QIODevice::ReadOnly); - QuaZipFileInfo64 levelDatInfo; - zippedFile.getFileInfo(&levelDatInfo); - auto modTime = levelDatInfo.getNTFSmTime(); - if (!modTime.isValid()) { - modTime = levelDatInfo.dateTime; - } - m_levelDatTime = modTime; - if (!m_isValid) { - return; - } - loadFromLevelDat(zippedFile.readAll()); - zippedFile.close(); + MMCZip::ArchiveReader r(file.absoluteFilePath()); + + m_isValid = false; + r.parse([this](MMCZip::ArchiveReader::File* file, bool& stop) { + const QString levelDat = "level.dat"; + auto filePath = file->filename(); + QFileInfo fi(filePath); + if (fi.fileName().compare(levelDat, Qt::CaseInsensitive) == 0) { + m_containerOffsetPath = filePath.chopped(levelDat.length()); + if (!m_containerOffsetPath.isEmpty()) { + return false; + } + m_levelDatTime = file->dateTime(); + loadFromLevelDat(file->readAll()); + m_isValid = true; + stop = true; + } + return true; + }); } bool World::install(const QString& to, const QString& name) @@ -284,10 +271,7 @@ 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; - } + MMCZip::ArchiveReader zip(m_containerFile.absoluteFilePath()); ok = !MMCZip::extractSubDir(&zip, m_containerOffsetPath, finalPath); } else if (m_containerFile.isDir()) { QString from = m_containerFile.filePath(); @@ -350,7 +334,7 @@ optional read_string(nbt::value& parent, const char* name) return nullopt; } auto& tag_str = namedValue.as(); - return QString::fromStdString(tag_str.get()); + return QString::fromUtf8(tag_str.get()); } catch ([[maybe_unused]] const std::out_of_range& e) { // fallback for old world formats qWarning() << "String NBT tag" << name << "could not be found."; diff --git a/launcher/minecraft/launch/ExtractNatives.cpp b/launcher/minecraft/launch/ExtractNatives.cpp index afe091758..3174e0e2a 100644 --- a/launcher/minecraft/launch/ExtractNatives.cpp +++ b/launcher/minecraft/launch/ExtractNatives.cpp @@ -17,11 +17,10 @@ #include #include -#include -#include #include #include "FileSystem.h" -#include "MMCZip.h" +#include "archive/ArchiveReader.h" +#include "archive/ArchiveWriter.h" #ifdef major #undef major @@ -41,30 +40,21 @@ static QString replaceSuffix(QString target, const QString& suffix, const QStrin static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibHack) { - QuaZip zip(source); - if (!zip.open(QuaZip::mdUnzip)) { - return false; - } + MMCZip::ArchiveReader zip(source); QDir directory(targetFolder); - if (!zip.goToFirstFile()) { - return false; - } - do { - QString name = zip.getCurrentFileName(); + + auto extPtr = MMCZip::ArchiveWriter::createDiskWriter(); + auto ext = extPtr.get(); + + return zip.parse([applyJnilibHack, directory, ext](MMCZip::ArchiveReader::File* f) { + QString name = f->filename(); auto lowercase = name.toLower(); if (applyJnilibHack) { name = replaceSuffix(name, ".jnilib", ".dylib"); } QString absFilePath = directory.absoluteFilePath(name); - if (!JlCompress::extractFile(&zip, "", absFilePath)) { - return false; - } - } while (zip.goToNextFile()); - zip.close(); - if (zip.getZipError() != 0) { - return false; - } - return true; + return f->writeFile(ext, absFilePath); + }); } void ExtractNatives::executeTask() diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index eceb8c256..c548f5350 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -44,8 +44,6 @@ #include #include -#include - #include "ModDetails.h" #include "Resource.h" diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h index 9195c0368..9b81f561f 100644 --- a/launcher/minecraft/mod/ModDetails.h +++ b/launcher/minecraft/mod/ModDetails.h @@ -35,14 +35,10 @@ #pragma once -#include - #include #include #include -#include "minecraft/mod/MetadataHandler.h" - struct ModLicense { QString name = {}; QString id = {}; diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp index 73e676dbd..9b4bb4a50 100644 --- a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp @@ -23,12 +23,9 @@ #include "FileSystem.h" #include "Json.h" +#include "archive/ArchiveReader.h" #include "minecraft/mod/ResourcePack.h" -#include -#include -#include - #include namespace DataPackUtils { @@ -106,67 +103,62 @@ bool processZIP(DataPack* pack, ProcessingLevel level) { Q_ASSERT(pack->type() == ResourceType::ZIPFILE); - QuaZip zip(pack->fileinfo().filePath()); - if (!zip.open(QuaZip::mdUnzip)) + MMCZip::ArchiveReader zip(pack->fileinfo().filePath()); + + bool metaParsed = false; + bool iconParsed = false; + bool mcmeta_result = false; + bool pack_png_result = false; + if (!zip.parse( + [&metaParsed, &iconParsed, &mcmeta_result, &pack_png_result, pack, level](MMCZip::ArchiveReader::File* f, bool& breakControl) { + bool skip = true; + if (!metaParsed && f->filename() == "pack.mcmeta") { + metaParsed = true; + skip = false; + auto data = f->readAll(); + + mcmeta_result = DataPackUtils::processMCMeta(pack, std::move(data)); + + if (!mcmeta_result) { + breakControl = true; + return true; // mcmeta invalid + } + } + if (!iconParsed && level != ProcessingLevel::BasicInfoOnly && f->filename() == "pack.png") { + iconParsed = true; + skip = false; + auto data = f->readAll(); + + pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); + if (!pack_png_result) { + breakControl = true; + return true; // pack.png invalid + } + } + if (skip) { + f->skip(); + } + if (metaParsed && (level == ProcessingLevel::BasicInfoOnly || iconParsed)) { + breakControl = true; + } + + return true; + })) { return false; // can't open zip file - - QuaZipFile file(&zip); - - auto mcmeta_invalid = [&pack]() { + } + if (!mcmeta_result) { qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.mcmeta"; return false; // the mcmeta is not optional - }; - - if (zip.setCurrentFile("pack.mcmeta")) { - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file in zip."; - zip.close(); - return mcmeta_invalid(); - } - - auto data = file.readAll(); - - bool mcmeta_result = DataPackUtils::processMCMeta(pack, std::move(data)); - - file.close(); - if (!mcmeta_result) { - return mcmeta_invalid(); // mcmeta invalid - } - } else { - return mcmeta_invalid(); // could not set pack.mcmeta as current file. } if (level == ProcessingLevel::BasicInfoOnly) { - zip.close(); return true; // only need basic info already checked } - auto png_invalid = [&pack]() { + if (!pack_png_result) { qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; return true; // the png is optional - }; - - if (zip.setCurrentFile("pack.png")) { - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file in zip."; - zip.close(); - return png_invalid(); - } - - auto data = file.readAll(); - - bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); - - file.close(); - zip.close(); - if (!pack_png_result) { - return png_invalid(); // pack.png invalid - } - } else { - zip.close(); - return png_invalid(); // could not set pack.mcmeta as current file. } - zip.close(); return true; } @@ -311,28 +303,17 @@ bool processPackPNG(const DataPack* pack) return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740 } case ResourceType::ZIPFILE: { - QuaZip zip(pack->fileinfo().filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return false; // can't open zip file + MMCZip::ArchiveReader zip(pack->fileinfo().filePath()); + auto f = zip.goToFile("pack.png"); + if (!f) { + return png_invalid(); + } + auto data = f->readAll(); - QuaZipFile file(&zip); - if (zip.setCurrentFile("pack.png")) { - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file in zip."; - zip.close(); - return png_invalid(); - } + bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); - auto data = file.readAll(); - - bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); - - file.close(); - if (!pack_png_result) { - return png_invalid(); // pack.png invalid - } - } else { - return png_invalid(); // could not set pack.mcmeta as current file. + if (!pack_png_result) { + return png_invalid(); // pack.png invalid } return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740 } diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index 38280f5af..59d3876b3 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -1,8 +1,6 @@ #include "LocalModParseTask.h" #include -#include -#include #include #include #include @@ -13,6 +11,7 @@ #include "FileSystem.h" #include "Json.h" +#include "archive/ArchiveReader.h" #include "minecraft/mod/ModDetails.h" #include "settings/INIFile.h" @@ -470,32 +469,33 @@ bool processZIP(Mod& mod, [[maybe_unused]] ProcessingLevel level) { ModDetails details; - QuaZip zip(mod.fileinfo().filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return false; + MMCZip::ArchiveReader zip(mod.fileinfo().filePath()); - QuaZipFile file(&zip); + bool baseForgePopulated = false; + bool isNilMod = false; + bool isValid = false; + QString manifestVersion = {}; + QByteArray nilData = {}; + QString nilFilePath = {}; - if (zip.setCurrentFile("META-INF/mods.toml") || zip.setCurrentFile("META-INF/neoforge.mods.toml")) { - if (!file.open(QIODevice::ReadOnly)) { - zip.close(); - return false; - } + if (!zip.parse([&details, &baseForgePopulated, &manifestVersion, &isValid, &nilData, &isNilMod, &nilFilePath]( + MMCZip::ArchiveReader::File* file, bool& stop) { + auto filePath = file->filename(); - details = ReadMCModTOML(file.readAll()); - file.close(); - - // to replace ${file.jarVersion} with the actual version, as needed - if (details.version == "${file.jarVersion}") { - if (zip.setCurrentFile("META-INF/MANIFEST.MF")) { - if (!file.open(QIODevice::ReadOnly)) { - zip.close(); - return false; + if (filePath == "META-INF/mods.toml" || filePath == "META-INF/neoforge.mods.toml") { + details = ReadMCModTOML(file->readAll()); + isValid = true; + if (details.version == "${file.jarVersion}" && !manifestVersion.isEmpty()) { + details.version = manifestVersion; } - + stop = details.version != "${file.jarVersion}"; + baseForgePopulated = true; + return true; + } + if (filePath == "META-INF/MANIFEST.MF") { // quick and dirty line-by-line parser - auto manifestLines = QString(file.readAll()).split(s_newlineRegex); - QString manifestVersion = ""; + auto manifestLines = QString(file->readAll()).split(s_newlineRegex); + manifestVersion = ""; for (auto& line : manifestLines) { if (line.startsWith("Implementation-Version: ", Qt::CaseInsensitive)) { manifestVersion = line.remove("Implementation-Version: ", Qt::CaseInsensitive); @@ -508,94 +508,64 @@ bool processZIP(Mod& mod, [[maybe_unused]] ProcessingLevel level) if (manifestVersion.contains("task ':jar' property 'archiveVersion'") || manifestVersion == "") { manifestVersion = "NONE"; } - - details.version = manifestVersion; - - file.close(); + if (baseForgePopulated) { + details.version = manifestVersion; + stop = true; + } + return true; + } + if (filePath == "mcmod.info") { + details = ReadMCModInfo(file->readAll()); + isValid = true; + stop = true; + return true; + } + if (filePath == "quilt.mod.json") { + details = ReadQuiltModInfo(file->readAll()); + isValid = true; + stop = true; + return true; + } + if (filePath == "fabric.mod.json") { + details = ReadFabricModInfo(file->readAll()); + isValid = true; + stop = true; + return true; + } + if (filePath == "forgeversion.properties") { + details = ReadForgeInfo(file->readAll()); + isValid = true; + stop = true; + return true; + } + if (filePath == "META-INF/nil/mappings.json") { + // nilloader uses the filename of the metadata file for the modid, so we can't know the exact filename + // thankfully, there is a good file to use as a canary so we don't look for nil meta all the time + isNilMod = true; + stop = !nilFilePath.isEmpty(); + file->skip(); + return true; } - } - - zip.close(); - mod.setDetails(details); - - return true; - } else if (zip.setCurrentFile("mcmod.info")) { - if (!file.open(QIODevice::ReadOnly)) { - zip.close(); - return false; - } - - details = ReadMCModInfo(file.readAll()); - file.close(); - zip.close(); - - mod.setDetails(details); - return true; - } else if (zip.setCurrentFile("quilt.mod.json")) { - if (!file.open(QIODevice::ReadOnly)) { - zip.close(); - return false; - } - - details = ReadQuiltModInfo(file.readAll()); - file.close(); - zip.close(); - - mod.setDetails(details); - return true; - } else if (zip.setCurrentFile("fabric.mod.json")) { - if (!file.open(QIODevice::ReadOnly)) { - zip.close(); - return false; - } - - details = ReadFabricModInfo(file.readAll()); - file.close(); - zip.close(); - - mod.setDetails(details); - return true; - } else if (zip.setCurrentFile("forgeversion.properties")) { - if (!file.open(QIODevice::ReadOnly)) { - zip.close(); - return false; - } - - details = ReadForgeInfo(file.readAll()); - file.close(); - zip.close(); - - mod.setDetails(details); - return true; - } else if (zip.setCurrentFile("META-INF/nil/mappings.json")) { - // nilloader uses the filename of the metadata file for the modid, so we can't know the exact filename - // thankfully, there is a good file to use as a canary so we don't look for nil meta all the time - - QString foundNilMeta; - for (auto& fname : zip.getFileNameList()) { // nilmods can shade nilloader to be able to run as a standalone agent - which includes nilloader's own meta file - if (fname.endsWith(".nilmod.css") && fname != "nilloader.nilmod.css") { - foundNilMeta = fname; - break; + if (filePath.endsWith(".nilmod.css") && filePath != "nilloader.nilmod.css") { + nilData = file->readAll(); + nilFilePath = filePath; + stop = isNilMod; + return true; } - } - - if (zip.setCurrentFile(foundNilMeta)) { - if (!file.open(QIODevice::ReadOnly)) { - zip.close(); - return false; - } - - details = ReadNilModInfo(file.readAll(), foundNilMeta); - file.close(); - zip.close(); - - mod.setDetails(details); + file->skip(); return true; - } + })) { + return false; + } + if (isNilMod) { + details = ReadNilModInfo(nilData, nilFilePath); + isValid = true; + } + if (isValid) { + mod.setDetails(details); + return true; } - - zip.close(); return false; // no valid mod found in archive } @@ -624,25 +594,14 @@ bool processLitemod(Mod& mod, [[maybe_unused]] ProcessingLevel level) { ModDetails details; - QuaZip zip(mod.fileinfo().filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return false; + MMCZip::ArchiveReader zip(mod.fileinfo().filePath()); - QuaZipFile file(&zip); - - if (zip.setCurrentFile("litemod.json")) { - if (!file.open(QIODevice::ReadOnly)) { - zip.close(); - return false; - } - - details = ReadLiteModInfo(file.readAll()); - file.close(); + if (auto file = zip.goToFile("litemod.json"); file) { + details = ReadLiteModInfo(file->readAll()); mod.setDetails(details); return true; } - zip.close(); return false; // no valid litemod.json found in archive } @@ -700,24 +659,13 @@ bool loadIconFile(const Mod& mod, QPixmap* pixmap) return png_invalid("file '" + icon_info.filePath() + "' does not exists or is not a file"); } case ResourceType::ZIPFILE: { - QuaZip zip(mod.fileinfo().filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return png_invalid("failed to open '" + mod.fileinfo().filePath() + "' as a zip archive"); - - QuaZipFile file(&zip); - - if (zip.setCurrentFile(mod.iconPath())) { - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file in zip."; - zip.close(); - return png_invalid("Failed to open '" + mod.iconPath() + "' in zip archive"); - } - - auto data = file.readAll(); + MMCZip::ArchiveReader zip(mod.fileinfo().filePath()); + auto file = zip.goToFile(mod.iconPath()); + if (file) { + auto data = file->readAll(); bool icon_result = ModUtils::processIconPNG(mod, std::move(data), pixmap); - file.close(); if (!icon_result) { return png_invalid("invalid png image"); // icon png invalid } diff --git a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp index a6ecc5353..3443966d5 100644 --- a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp @@ -22,10 +22,7 @@ #include "LocalShaderPackParseTask.h" #include "FileSystem.h" - -#include -#include -#include +#include "archive/ArchiveReader.h" namespace ShaderPackUtils { @@ -63,25 +60,19 @@ bool processZIP(ShaderPack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::ZIPFILE); - QuaZip zip(pack.fileinfo().filePath()); - if (!zip.open(QuaZip::mdUnzip)) + MMCZip::ArchiveReader zip(pack.fileinfo().filePath()); + if (!zip.collectFiles()) return false; // can't open zip file - QuaZipFile file(&zip); - - QuaZipDir zipDir(&zip); - if (!zipDir.exists("/shaders")) { + if (!zip.exists("/shaders")) { return false; // assets dir does not exists at zip root } pack.setPackFormat(ShaderPackFormat::VALID); if (level == ProcessingLevel::BasicInfoOnly) { - zip.close(); return true; // only need basic info already checked } - zip.close(); - return true; } diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp index 18020808a..106d7c323 100644 --- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp @@ -20,9 +20,7 @@ #include "LocalTexturePackParseTask.h" #include "FileSystem.h" - -#include -#include +#include "archive/ArchiveReader.h" #include @@ -91,55 +89,26 @@ bool processZIP(TexturePack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::ZIPFILE); - QuaZip zip(pack.fileinfo().filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return false; + MMCZip::ArchiveReader zip(pack.fileinfo().filePath()); + bool packProcessed = false; + bool iconProcessed = false; - QuaZipFile file(&zip); - - if (zip.setCurrentFile("pack.txt")) { - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file in zip."; - zip.close(); - return false; + return zip.parse([&packProcessed, &iconProcessed, &pack, level](MMCZip::ArchiveReader::File* file, bool& stop) { + if (!packProcessed && file->filename() == "pack.txt") { + packProcessed = true; + auto data = file->readAll(); + stop = packProcessed && (iconProcessed || level == ProcessingLevel::BasicInfoOnly); + return TexturePackUtils::processPackTXT(pack, std::move(data)); } - - auto data = file.readAll(); - - bool packTXT_result = TexturePackUtils::processPackTXT(pack, std::move(data)); - - file.close(); - if (!packTXT_result) { - return false; + if (!iconProcessed && file->filename() == "pack.png") { + iconProcessed = true; + auto data = file->readAll(); + stop = packProcessed && iconProcessed; + return TexturePackUtils::processPackPNG(pack, std::move(data)); } - } - - if (level == ProcessingLevel::BasicInfoOnly) { - zip.close(); + file->skip(); return true; - } - - if (zip.setCurrentFile("pack.png")) { - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file in zip."; - zip.close(); - return false; - } - - auto data = file.readAll(); - - bool packPNG_result = TexturePackUtils::processPackPNG(pack, std::move(data)); - - file.close(); - zip.close(); - if (!packPNG_result) { - return false; - } - } - - zip.close(); - - return true; + }); } bool processPackTXT(TexturePack& pack, QByteArray&& raw_data) @@ -189,32 +158,19 @@ bool processPackPNG(const TexturePack& pack) return false; } case ResourceType::ZIPFILE: { - QuaZip zip(pack.fileinfo().filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return false; // can't open zip file + MMCZip::ArchiveReader zip(pack.fileinfo().filePath()); - QuaZipFile file(&zip); - if (zip.setCurrentFile("pack.png")) { - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file in zip."; - zip.close(); - return png_invalid(); - } - - auto data = file.readAll(); + auto file = zip.goToFile("pack.png"); + if (file) { + auto data = file->readAll(); bool pack_png_result = TexturePackUtils::processPackPNG(pack, std::move(data)); - file.close(); if (!pack_png_result) { - zip.close(); return png_invalid(); // pack.png invalid } - } else { - zip.close(); - return png_invalid(); // could not set pack.mcmeta as current file. } - return false; + return png_invalid(); // could not set pack.mcmeta as current file. } default: qWarning() << "Invalid type for resource pack parse task!"; diff --git a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp index d45f537fa..50f7bbfa1 100644 --- a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp @@ -23,13 +23,11 @@ #include "LocalWorldSaveParseTask.h" #include "FileSystem.h" - -#include -#include -#include +#include "archive/ArchiveReader.h" #include #include +#include namespace WorldSaveUtils { @@ -105,22 +103,40 @@ bool processFolder(WorldSave& save, ProcessingLevel level) /// QString , /// bool /// ) -static std::tuple contains_level_dat(QuaZip& zip) +static std::tuple contains_level_dat(QString fileName) { + MMCZip::ArchiveReader zip(fileName); + if (!zip.collectFiles()) { + return std::make_tuple(false, "", false); + } bool saves = false; - QuaZipDir zipDir(&zip); - if (zipDir.exists("/saves")) { + if (zip.exists("/saves")) { saves = true; - zipDir.cd("/saves"); } - for (auto const& entry : zipDir.entryList()) { - zipDir.cd(entry); - if (zipDir.exists("level.dat")) { - return std::make_tuple(true, entry, saves); + for (auto file : zip.getFiles()) { + QString relativePath = file; + if (saves) { + if (!relativePath.startsWith("saves/", Qt::CaseInsensitive)) + continue; + relativePath = relativePath.mid(QString("saves/").length()); + } + if (!relativePath.endsWith("/level.dat", Qt::CaseInsensitive)) + continue; + + int slashIndex = relativePath.indexOf('/'); + if (slashIndex == -1) + continue; // malformed: no slash between saves/ and level.dat + + QString worldName = relativePath.left(slashIndex); + QString remaining = relativePath.mid(slashIndex + 1); + + // Check that there's nothing between worldName/ and level.dat + if (remaining == "level.dat") { + return std::make_tuple(true, worldName, saves); } - zipDir.cd(".."); } + return std::make_tuple(false, "", saves); } @@ -128,19 +144,14 @@ bool processZIP(WorldSave& save, ProcessingLevel level) { Q_ASSERT(save.type() == ResourceType::ZIPFILE); - QuaZip zip(save.fileinfo().filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return false; // can't open zip file - - auto [found, save_dir_name, found_saves_dir] = contains_level_dat(zip); - - if (save_dir_name.endsWith("/")) { - save_dir_name.chop(1); - } + auto [found, save_dir_name, found_saves_dir] = contains_level_dat(save.fileinfo().filePath()); if (!found) { return false; } + if (save_dir_name.endsWith("/")) { + save_dir_name.chop(1); + } save.setSaveDirName(save_dir_name); @@ -151,14 +162,11 @@ bool processZIP(WorldSave& save, ProcessingLevel level) } if (level == ProcessingLevel::BasicInfoOnly) { - zip.close(); return true; // only need basic info already checked } // reserved for more intensive processing - zip.close(); - return true; } diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index ba3a25aa3..f107e4700 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -39,8 +39,6 @@ #include #include -#include - #include "FileSystem.h" #include "Json.h" #include "MMCZip.h" @@ -675,13 +673,6 @@ void PackInstallTask::extractConfigs() setStatus(tr("Extracting configs...")); QDir extractDir(m_stagingPath); - - QuaZip packZip(archivePath); - if (!packZip.open(QuaZip::mdUnzip)) { - emitFailed(tr("Failed to open pack configs %1!").arg(archivePath)); - return; - } - m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload::of(MMCZip::extractDir), archivePath, extractDir.absolutePath() + "/minecraft"); connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, [this]() { downloadMods(); }); diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp index 98a61d0d1..d63eb709f 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.cpp +++ b/launcher/modplatform/flame/FlamePackExportTask.cpp @@ -30,7 +30,6 @@ #include #include "Application.h" #include "Json.h" -#include "MMCZip.h" #include "minecraft/PackProfile.h" #include "minecraft/mod/ModFolderModel.h" #include "modplatform/ModIndex.h" @@ -38,6 +37,8 @@ #include "modplatform/helpers/HashUtils.h" #include "tasks/Task.h" +#include "archive/ExportToZipTask.h" + const QString FlamePackExportTask::TEMPLATE = "
  • {name}{authors}
  • \n"; const QStringList FlamePackExportTask::FILE_EXTENSIONS({ "jar", "zip" }); @@ -318,7 +319,7 @@ void FlamePackExportTask::buildZip() setStatus(tr("Adding files...")); setProgress(4, 5); - auto zipTask = makeShared(m_options.output, m_gameRoot, files, "overrides/", true, false); + auto zipTask = makeShared(m_options.output, m_gameRoot, files, "overrides/", true); zipTask->addExtraFile("manifest.json", generateIndex()); zipTask->addExtraFile("modlist.html", generateHTML()); diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp index 034ee3eae..33c0c38b6 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -102,12 +102,6 @@ void PackInstallTask::unzip() 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; - } - m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload::of(MMCZip::extractDir), archivePath, extractDir.absolutePath() + "/unzip"); connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::onUnzipFinished); diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.h b/launcher/modplatform/legacy_ftb/PackInstallTask.h index 3459ee902..6db6cb712 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.h +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.h @@ -1,6 +1,4 @@ #pragma once -#include -#include #include "InstanceTask.h" #include "PackHelpers.h" #include "meta/Index.h" @@ -39,7 +37,6 @@ class PackInstallTask : public InstanceTask { private: /* data */ shared_qobject_ptr m_network; bool abortable = false; - std::unique_ptr m_packZip; QFuture> m_extractFuture; QFutureWatcher> m_extractFutureWatcher; NetJob::Ptr netJobContainer; diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp index 9ee4101e6..c638e8db0 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp @@ -25,6 +25,7 @@ #include #include "Json.h" #include "MMCZip.h" +#include "archive/ExportToZipTask.h" #include "minecraft/PackProfile.h" #include "minecraft/mod/MetadataHandler.h" #include "minecraft/mod/ModFolderModel.h" @@ -200,7 +201,7 @@ void ModrinthPackExportTask::buildZip() { setStatus(tr("Adding files...")); - auto zipTask = makeShared(output, gameRoot, files, "overrides/", true, true); + auto zipTask = makeShared(output, gameRoot, files, "overrides/", true); zipTask->addExtraFile("modrinth.index.json", generateIndex()); zipTask->setExcludeFiles(resolvedFiles.keys()); diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp index cc9ced10b..09428c31d 100644 --- a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp +++ b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp @@ -66,11 +66,7 @@ void Technic::SingleZipPackInstallTask::downloadSucceeded() 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_packZip.reset(new MMCZip::ArchiveReader(m_archivePath)); m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), QString(""), extractDir.absolutePath()); connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &Technic::SingleZipPackInstallTask::extractFinished); diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.h b/launcher/modplatform/technic/SingleZipPackInstallTask.h index d49d008b9..9dd54458d 100644 --- a/launcher/modplatform/technic/SingleZipPackInstallTask.h +++ b/launcher/modplatform/technic/SingleZipPackInstallTask.h @@ -16,10 +16,9 @@ #pragma once #include "InstanceTask.h" +#include "archive/ArchiveReader.h" #include "net/NetJob.h" -#include - #include #include #include @@ -54,7 +53,7 @@ class SingleZipPackInstallTask : public InstanceTask { QString m_minecraftVersion; QString m_archivePath; NetJob::Ptr m_filesNetJob; - std::unique_ptr m_packZip; + std::unique_ptr m_packZip; QFuture> m_extractFuture; QFutureWatcher> m_extractFutureWatcher; }; diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp index b762e8882..4c40ddf73 100644 --- a/launcher/modplatform/technic/TechnicPackProcessor.cpp +++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp @@ -19,12 +19,10 @@ #include #include #include -#include -#include -#include #include #include +#include "archive/ArchiveReader.h" void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const QString& instName, @@ -53,35 +51,30 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, QString versionJson = FS::PathCombine(minecraftPath, "bin", "version.json"); QString fmlMinecraftVersion; if (QFile::exists(modpackJar)) { - QuaZip zipFile(modpackJar); - if (!zipFile.open(QuaZip::mdUnzip)) { + MMCZip::ArchiveReader zipFile(modpackJar); + if (!zipFile.collectFiles()) { emit failed(tr("Unable to open \"bin/modpack.jar\" file!")); return; } - QuaZipDir zipFileRoot(&zipFile, "/"); - if (zipFileRoot.exists("/version.json")) { - if (zipFileRoot.exists("/fmlversion.properties")) { - zipFile.setCurrentFile("fmlversion.properties"); - QuaZipFile file(&zipFile); - if (!file.open(QIODevice::ReadOnly)) { + if (zipFile.exists("/version.json")) { + if (zipFile.exists("/fmlversion.properties")) { + auto file = zipFile.goToFile("fmlversion.properties"); + if (!file) { emit failed(tr("Unable to open \"fmlversion.properties\"!")); return; } - QByteArray fmlVersionData = file.readAll(); - file.close(); + QByteArray fmlVersionData = file->readAll(); 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)) { + auto file = zipFile.goToFile("version.json"); + if (!file) { emit failed(tr("Unable to open \"version.json\"!")); return; } - data = file.readAll(); - file.close(); + data = file->readAll(); } else { if (minecraftVersion.isEmpty()) { emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but Minecraft version is unknown")); @@ -93,16 +86,14 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, // 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")) { - zipFile.setCurrentFile("forgeversion.properties", QuaZip::csSensitive); - QuaZipFile file(&zipFile); - if (!file.open(QIODevice::ReadOnly)) { + if (zipFile.exists("/forgeversion.properties")) { + auto file = zipFile.goToFile("forgeversion.properties"); + if (!file) { // 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(); + auto forgeVersionData = file->readAll(); INIFile iniFile; iniFile.loadFile(forgeVersionData); QString major, minor, revision, build; diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp index 8d98b0513..21c16f01a 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.cpp +++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -43,6 +43,7 @@ #include #include "FileIgnoreProxy.h" #include "QObjectPtr.h" +#include "archive/ExportToZipTask.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "ui_ExportInstanceDialog.h" @@ -150,7 +151,7 @@ void ExportInstanceDialog::doExport() return; } - auto task = makeShared(output, m_instance->instanceRoot(), files, "", true, true); + auto task = makeShared(output, m_instance->instanceRoot(), files, "", true); connect(task.get(), &Task::failed, this, [this, output](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); diff --git a/libraries/README.md b/libraries/README.md index be41e549f..2ebb80515 100644 --- a/libraries/README.md +++ b/libraries/README.md @@ -107,11 +107,13 @@ See [github repo](https://github.com/nayuki/QR-Code-generator). MIT -## quazip +## libarchive -A zip manipulation library. +Multi-format archive and compression library. -LGPL 2.1 with linking exception. +See [github repo](https://github.com/libarchive/libarchive). + +BSD 2-Clause license with some exception. ## rainbow diff --git a/libraries/quazip b/libraries/quazip deleted file mode 160000 index 3fd3b299b..000000000 --- a/libraries/quazip +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3fd3b299b875fbd2beac4894b8a870d80022cad7 diff --git a/nix/unwrapped.nix b/nix/unwrapped.nix index 54e880818..7d461a072 100644 --- a/nix/unwrapped.nix +++ b/nix/unwrapped.nix @@ -16,6 +16,7 @@ zlib, msaClientID ? null, gamemodeSupport ? stdenv.hostPlatform.isLinux, + libarchive, }: assert lib.assertMsg ( gamemodeSupport -> stdenv.hostPlatform.isLinux @@ -76,8 +77,9 @@ stdenv.mkDerivation { cmark kdePackages.qtbase kdePackages.qtnetworkauth - kdePackages.quazip + kdePackages.qt5compat qrencode + libarchive tomlplusplus zlib ] diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index b1b0996e2..20811632c 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -1,7 +1,7 @@ { "default-registry": { "kind": "git", - "baseline": "1fddddc280dfed63956e15ef74f4321bc6a219c9", + "baseline": "2d6a6cf3ac9a7cc93942c3d289a2f9c661a6f4a7", "repository": "https://github.com/microsoft/vcpkg" }, "registries": [ diff --git a/vcpkg.json b/vcpkg.json index 0399cdf27..942e6d9e4 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,7 +1,5 @@ { "dependencies": [ - "bzip2", - "cmark", { "name": "ecm", "host": true @@ -14,6 +12,19 @@ "name": "pkgconf", "host": true }, + + "cmark", + { + "name": "libarchive", + "default-features": false, + "features": [ + "bzip2", + "lz4", + "lzma", + "lzo", + "zstd" + ] + }, "tomlplusplus", "zlib" ]