diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 3a9009b2d..140a68e68 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -32,8 +32,8 @@ set(CORE_SOURCES archive/ArchiveWriter.h archive/ExportToZipTask.cpp archive/ExportToZipTask.h - Untar.h - Untar.cpp + archive/ExtractZipTask.cpp + archive/ExtractZipTask.h StringUtils.h StringUtils.cpp QVariantUtils.h diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 308f8620e..ca1225e4a 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -1701,4 +1701,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 0e573a09e..ed2d2715a 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..92cc89510 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,8 +55,8 @@ #include "net/ApiDownload.h" +#include #include -#include #include #include @@ -109,38 +110,26 @@ 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)) { + 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 fileInfo.path(); } - if (fileName == "manifest.json") { + if (fileInfo.fileName() == "manifest.json") { qDebug() << "Flame:" << true; m_modpackType = ModpackType::Flame; - return root; + return 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 +140,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 +153,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 +174,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..c3c833926 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -58,7 +58,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 8d496fbb9..6a051ba63 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -265,7 +265,7 @@ std::optional extractSubDir(ArchiveReader* zip, const QString& subd return true; })) { qWarning() << "Failed to parse file" << zip->getZipName(); - JlCompress::removeFile(extracted); + FS::removeFiles(extracted); return std::nullopt; } @@ -363,140 +363,4 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q } return true; } - -#if defined(LAUNCHER_APPLICATION) - -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 e18795ba4..ab0f2d660 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -108,36 +108,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 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 index 5bb8054d4..e48d168ae 100644 --- a/launcher/archive/ArchiveReader.cpp +++ b/launcher/archive/ArchiveReader.cpp @@ -1,5 +1,8 @@ #include "ArchiveReader.h" #include +#include +#include + namespace MMCZip { QStringList ArchiveReader::getFiles() { @@ -167,4 +170,32 @@ 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; +} + } // namespace MMCZip diff --git a/launcher/archive/ArchiveReader.h b/launcher/archive/ArchiveReader.h index 9831c2cb7..5cbf75308 100644 --- a/launcher/archive/ArchiveReader.h +++ b/launcher/archive/ArchiveReader.h @@ -18,6 +18,7 @@ class ArchiveReader { QStringList getFiles(); QString getZipName(); bool collectFiles(bool onlyFiles = true); + bool exists(const QString& filePath) const; class File { public: diff --git a/launcher/archive/ExtractZipTask.cpp b/launcher/archive/ExtractZipTask.cpp new file mode 100644 index 000000000..69069d550 --- /dev/null +++ b/launcher/archive/ExtractZipTask.cpp @@ -0,0 +1,130 @@ +#include "ExtractZipTask.h" +#include +#include "FileSystem.h" +#include "archive/ArchiveReader.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(); + } + + int flags; + + /* Select which attributes we want to restore. */ + flags = ARCHIVE_EXTRACT_TIME; + flags |= ARCHIVE_EXTRACT_PERM; + flags |= ARCHIVE_EXTRACT_ACL; + flags |= ARCHIVE_EXTRACT_FFLAGS; + + std::unique_ptr extPtr(archive_write_disk_new(), [](archive* a) { + archive_write_close(a); + archive_write_free(a); + }); + auto ext = extPtr.get(); + archive_write_disk_set_options(ext, flags); + archive_write_disk_set_standard_lookup(ext); + + 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..4d9a4ccc4 --- /dev/null +++ b/launcher/archive/ExtractZipTask.h @@ -0,0 +1,36 @@ +#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..e632225b0 100644 --- a/launcher/java/download/ArchiveDownloadTask.cpp +++ b/launcher/java/download/ArchiveDownloadTask.cpp @@ -18,10 +18,10 @@ #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 +67,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/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index ba3a25aa3..a3f045d02 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -675,13 +675,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/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp index 2b9bd127a..046817714 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 42808a1a2..9d236decc 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.h +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.h @@ -41,7 +41,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;