Replace QuaZip (and other archiving operations) with libarchive (#3959)

This commit is contained in:
Seth Flynn
2025-11-27 14:53:59 -05:00
committed by GitHub
48 changed files with 1414 additions and 1464 deletions

View File

@@ -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

View File

@@ -81,7 +81,7 @@ runs:
cmark:p
qrencode:p
tomlplusplus:p
quazip-qt6:p
libarchive:p
- name: List pacman packages (MinGW)
if: ${{ inputs.msystem != '' }}

3
.gitmodules vendored
View File

@@ -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

View File

@@ -1,2 +1 @@
libraries/nbtplusplus
libraries/quazip

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -291,6 +291,8 @@ bool move(const QString& source, const QString& dest);
*/
bool deletePath(QString path);
bool removeFiles(QStringList listFile);
/**
* Trash a folder / file
*/

View File

@@ -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 <QFileInfo>
#include <QtConcurrentRun>
#include <algorithm>
#include <memory>
#include <quazip/quazipdir.h>
InstanceImportTask::InstanceImportTask(const QUrl& sourceUrl, QWidget* parent, QMap<QString, QString>&& 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<QuaZip>(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<MMCZip::ExtractZipTask>(packZip, extractDir, root);
auto zipTask = makeShared<MMCZip::ExtractZipTask>(m_archivePath, extractDir, root);
auto progressStep = std::make_shared<TaskStepProgress>();
connect(zipTask.get(), &Task::finished, this, [this, progressStep] {

View File

@@ -40,8 +40,6 @@
#include <QUrl>
#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();

View File

@@ -35,66 +35,46 @@
*/
#include "MMCZip.h"
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
#include <quazip/quazipfile.h>
#include <archive.h>
#include "FileSystem.h"
#include "archive/ArchiveReader.h"
#include "archive/ArchiveWriter.h"
#include <QCoreApplication>
#include <QDebug>
#include <QFileInfo>
#include <QUrl>
#if defined(LAUNCHER_APPLICATION)
#include <QtConcurrentRun>
#endif
#include <memory>
namespace MMCZip {
// ours
bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet<QString>& contained, const Filter& filter)
using FilterFunction = std::function<bool(const QString&)>;
#if defined(LAUNCHER_APPLICATION)
bool mergeZipFiles(ArchiveWriter& into, QFileInfo from, QSet<QString>& 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<Mod*>& 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 QList<M
if (!mod->enabled())
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 QList<M
} else if (mod->type() == 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 QList<M
files.removeAll(e);
}
if (!compressDirFiles(&zipOut, parent_dir, files)) {
if (!compressDirFiles(zipOut, parent_dir, files)) {
zipOut.close();
FS::deletePath(targetJarPath);
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
@@ -209,7 +159,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
}
}
if (!mergeZipFiles(&zipOut, QFileInfo(sourceJarPath), addedFiles, [](const QString key) { return !key.contains("META-INF"); })) {
if (!mergeZipFiles(zipOut, QFileInfo(sourceJarPath), addedFiles, [](const QString key) { return !key.contains("META-INF"); })) {
zipOut.close();
FS::deletePath(targetJarPath);
qCritical() << "Failed to insert minecraft.jar contents.";
@@ -217,8 +167,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
}
// Recompress the jar
zipOut.close();
if (zipOut.getZipError() != 0) {
if (!zipOut.close()) {
FS::deletePath(targetJarPath);
qCritical() << "Failed to finalize minecraft.jar!";
return false;
@@ -228,194 +177,121 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
#endif
// ours
QString findFolderOfFileInZip(QuaZip* zip, const QString& what, const QStringList& ignore_paths, const QString& root)
{
QuaZipDir rootDir(zip, root);
for (auto&& fileName : rootDir.entryList(QDir::Files)) {
if (fileName == what)
return root;
QCoreApplication::processEvents();
}
// Recurse the search to non-ignored subfolders
for (auto&& fileName : rootDir.entryList(QDir::Dirs)) {
if (ignore_paths.contains(fileName))
continue;
QString result = findFolderOfFileInZip(zip, what, ignore_paths, root + fileName);
if (!result.isEmpty())
return result;
}
return {};
}
// ours
bool findFilesInZip(QuaZip* zip, const QString& what, QStringList& result, const QString& root)
{
QuaZipDir rootDir(zip, root);
for (auto fileName : rootDir.entryList(QDir::Files)) {
if (fileName == what) {
result.append(root);
return true;
}
}
for (auto fileName : rootDir.entryList(QDir::Dirs)) {
findFilesInZip(zip, what, result, root + fileName);
}
return !result.isEmpty();
}
// ours
std::optional<QStringList> extractSubDir(QuaZip* zip, const QString& subdir, const QString& target)
std::optional<QStringList> 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<QStringList> 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<QStringList> 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<ZipResult>::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<ZipResult>::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

View File

@@ -36,8 +36,6 @@
#pragma once
#include <quazip.h>
#include <quazip/JlCompress.h>
#include <QDir>
#include <QFileInfo>
#include <QFuture>
@@ -46,72 +44,27 @@
#include <QSet>
#include <QString>
#include <functional>
#include <memory>
#include <optional>
#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<bool(const QFileInfo&)>;
/**
* Merge two zip files, using a filter function
*/
bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet<QString>& 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<Mod*>& 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<QStringList> extractSubDir(QuaZip* zip, const QString& subdir, const QString& target);
bool extractRelFile(QuaZip* zip, const QString& file, const QString& target);
std::optional<QStringList> 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<QString>;
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<QString, QByteArray> m_extra_files;
QFuture<ZipResult> m_build_zip_future;
QFutureWatcher<ZipResult> m_build_zip_watcher;
};
class ExtractZipTask : public Task {
Q_OBJECT
public:
ExtractZipTask(QString input, QDir outputDir, QString subdirectory = "")
: ExtractZipTask(std::make_shared<QuaZip>(input), outputDir, subdirectory)
{}
ExtractZipTask(std::shared_ptr<QuaZip> input, QDir outputDir, QString subdirectory = "")
: m_input(input), m_output_dir(outputDir), m_subdirectory(subdirectory)
{}
virtual ~ExtractZipTask() = default;
using ZipResult = std::optional<QString>;
protected:
virtual void executeTask() override;
bool abort() override;
ZipResult extractZip();
void finish();
private:
std::shared_ptr<QuaZip> m_input;
QDir m_output_dir;
QString m_subdirectory;
QFuture<ZipResult> m_zip_future;
QFutureWatcher<ZipResult> m_zip_watcher;
};
#endif
} // namespace MMCZip

View File

@@ -1,260 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023-2024 Trial97 <alexandru.tripon97@gmail.com>
*
* 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 <https://www.gnu.org/licenses/>.
*
* 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 <quagzipfile.h>
#include <QByteArray>
#include <QFileInfo>
#include <QIODevice>
#include <QString>
#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);
}

View File

@@ -1,46 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023-2024 Trial97 <alexandru.tripon97@gmail.com>
*
* 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 <https://www.gnu.org/licenses/>.
*
* 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 <QIODevice>
// 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);
}

View File

@@ -0,0 +1,230 @@
// SPDX-License-Identifier: GPL-3.0-only AND LicenseRef-PublicDomain
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2025 Trial97 <alexandru.tripon97@gmail.com>
*
* 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 <https://www.gnu.org/licenses/>.
*
* Additional note: Portions of this file are released into the public domain
* under LicenseRef-PublicDomain.
*/
#include "ArchiveReader.h"
#include <archive.h>
#include <archive_entry.h>
#include <QDir>
#include <QFileInfo>
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<const char*>(buff), static_cast<qsizetype>(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<File>
{
auto f = std::make_unique<File>();
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<bool(File*, bool&)> doStuff)
{
auto f = std::make_unique<File>();
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<bool(File*)> 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

View File

@@ -0,0 +1,72 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2025 Trial97 <alexandru.tripon97@gmail.com>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QByteArray>
#include <QDateTime>
#include <QStringList>
#include <memory>
struct archive;
struct archive_entry;
namespace MMCZip {
class ArchiveReader {
public:
using ArchivePtr = std::unique_ptr<struct archive, int (*)(struct archive*)>;
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<File> goToFile(QString filename);
bool parse(std::function<bool(File*)>);
bool parse(std::function<bool(File*, bool&)>);
private:
QString m_archivePath;
size_t m_blockSize = 10240;
QStringList m_fileNames = {};
};
} // namespace MMCZip

View File

@@ -0,0 +1,213 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2025 Trial97 <alexandru.tripon97@gmail.com>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "ArchiveWriter.h"
#include <archive.h>
#include <archive_entry.h>
#include <sys/stat.h>
#include <QFile>
#include <QFileInfo>
#include <memory>
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<archive_entry, void (*)(archive_entry*)> 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<archive_entry, void (*)(archive_entry*)> 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<archive, void (*)(archive*)> 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<archive, void (*)(archive*)> 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

View File

@@ -0,0 +1,46 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2025 Trial97 <alexandru.tripon97@gmail.com>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QByteArray>
#include <QFileDevice>
#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<archive, void (*)(archive*)> createDiskWriter();
private:
struct archive* m_archive = nullptr;
QString m_filename;
QString m_format = "zip";
};
} // namespace MMCZip

View File

@@ -0,0 +1,100 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2025 Trial97 <alexandru.tripon97@gmail.com>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "ExportToZipTask.h"
#include <QtConcurrent>
#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<ZipResult>::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

View File

@@ -0,0 +1,72 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2025 Trial97 <alexandru.tripon97@gmail.com>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QDir>
#include <QFileInfoList>
#include <QFuture>
#include <QFutureWatcher>
#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<QString>;
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<QString, QByteArray> m_extra_files;
QFuture<ZipResult> m_build_zip_future;
QFutureWatcher<ZipResult> m_build_zip_watcher;
};
} // namespace MMCZip

View File

@@ -0,0 +1,135 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2025 Trial97 <alexandru.tripon97@gmail.com>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "ExtractZipTask.h"
#include <QtConcurrent>
#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<ZipResult>::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

View File

@@ -0,0 +1,53 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2025 Trial97 <alexandru.tripon97@gmail.com>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QDir>
#include <QFuture>
#include <QFutureWatcher>
#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<QString>;
protected:
virtual void executeTask() override;
bool abort() override;
ZipResult extractZip();
void finish();
private:
ArchiveReader m_input;
QDir m_output_dir;
QString m_subdirectory;
QFuture<ZipResult> m_zip_future;
QFutureWatcher<ZipResult> m_zip_watcher;
};
} // namespace MMCZip

View File

@@ -16,12 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "java/download/ArchiveDownloadTask.h"
#include <quazip.h>
#include <memory>
#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<QuaZip>(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<MMCZip::ExtractZipTask>(zip, m_final_path, files[0]);
auto progressStep = std::make_shared<TaskStepProgress>();
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<MMCZip::ExtractZipTask>(input, m_final_path, firstFolderParts.value(0));
emitFailed(tr("Could not determine archive type!"));
auto progressStep = std::make_shared<TaskStepProgress>();
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()

View File

@@ -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);

View File

@@ -43,9 +43,6 @@
#include <FileSystem.h>
#include <MMCZip.h>
#include <io/stream_reader.h>
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
#include <quazip/quazipfile.h>
#include <tag_primitive.h>
#include <tag_string.h>
#include <sstream>
@@ -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<QString> read_string(nbt::value& parent, const char* name)
return nullopt;
}
auto& tag_str = namedValue.as<nbt::tag_string>();
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.";

View File

@@ -17,11 +17,10 @@
#include <launch/LaunchTask.h>
#include <minecraft/MinecraftInstance.h>
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
#include <QDir>
#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()

View File

@@ -44,8 +44,6 @@
#include <QPixmap>
#include <QPixmapCache>
#include <optional>
#include "ModDetails.h"
#include "Resource.h"

View File

@@ -35,14 +35,10 @@
#pragma once
#include <memory>
#include <QString>
#include <QStringList>
#include <QUrl>
#include "minecraft/mod/MetadataHandler.h"
struct ModLicense {
QString name = {};
QString id = {};

View File

@@ -23,12 +23,9 @@
#include "FileSystem.h"
#include "Json.h"
#include "archive/ArchiveReader.h"
#include "minecraft/mod/ResourcePack.h"
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
#include <quazip/quazipfile.h>
#include <QCryptographicHash>
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
}

View File

@@ -1,8 +1,6 @@
#include "LocalModParseTask.h"
#include <qdcss.h>
#include <quazip/quazip.h>
#include <quazip/quazipfile.h>
#include <toml++/toml.h>
#include <QJsonArray>
#include <QJsonDocument>
@@ -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
}

View File

@@ -22,10 +22,7 @@
#include "LocalShaderPackParseTask.h"
#include "FileSystem.h"
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
#include <quazip/quazipfile.h>
#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;
}

View File

@@ -20,9 +20,7 @@
#include "LocalTexturePackParseTask.h"
#include "FileSystem.h"
#include <quazip/quazip.h>
#include <quazip/quazipfile.h>
#include "archive/ArchiveReader.h"
#include <QCryptographicHash>
@@ -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!";

View File

@@ -23,13 +23,11 @@
#include "LocalWorldSaveParseTask.h"
#include "FileSystem.h"
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
#include <quazip/quazipfile.h>
#include "archive/ArchiveReader.h"
#include <QDir>
#include <QFileInfo>
#include <tuple>
namespace WorldSaveUtils {
@@ -105,22 +103,40 @@ bool processFolder(WorldSave& save, ProcessingLevel level)
/// QString <name of folder containing level.dat>,
/// bool <saves folder found>
/// )
static std::tuple<bool, QString, bool> contains_level_dat(QuaZip& zip)
static std::tuple<bool, QString, bool> 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;
}

View File

@@ -39,8 +39,6 @@
#include <QtConcurrent>
#include <algorithm>
#include <quazip/quazip.h>
#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<QString, QString>::of(MMCZip::extractDir), archivePath,
extractDir.absolutePath() + "/minecraft");
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, [this]() { downloadMods(); });

View File

@@ -30,7 +30,6 @@
#include <memory>
#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 = "<li><a href=\"{url}\">{name}{authors}</a></li>\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<MMCZip::ExportToZipTask>(m_options.output, m_gameRoot, files, "overrides/", true, false);
auto zipTask = makeShared<MMCZip::ExportToZipTask>(m_options.output, m_gameRoot, files, "overrides/", true);
zipTask->addExtraFile("manifest.json", generateIndex());
zipTask->addExtraFile("modlist.html", generateHTML());

View File

@@ -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<QString, QString>::of(MMCZip::extractDir), archivePath,
extractDir.absolutePath() + "/unzip");
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onUnzipFinished);

View File

@@ -1,6 +1,4 @@
#pragma once
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
#include "InstanceTask.h"
#include "PackHelpers.h"
#include "meta/Index.h"
@@ -39,7 +37,6 @@ class PackInstallTask : public InstanceTask {
private: /* data */
shared_qobject_ptr<QNetworkAccessManager> m_network;
bool abortable = false;
std::unique_ptr<QuaZip> m_packZip;
QFuture<std::optional<QStringList>> m_extractFuture;
QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
NetJob::Ptr netJobContainer;

View File

@@ -25,6 +25,7 @@
#include <QtConcurrentRun>
#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<MMCZip::ExportToZipTask>(output, gameRoot, files, "overrides/", true, true);
auto zipTask = makeShared<MMCZip::ExportToZipTask>(output, gameRoot, files, "overrides/", true);
zipTask->addExtraFile("modrinth.index.json", generateIndex());
zipTask->setExcludeFiles(resolvedFiles.keys());

View File

@@ -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<QStringList>::finished, this, &Technic::SingleZipPackInstallTask::extractFinished);

View File

@@ -16,10 +16,9 @@
#pragma once
#include "InstanceTask.h"
#include "archive/ArchiveReader.h"
#include "net/NetJob.h"
#include <quazip/quazip.h>
#include <QFutureWatcher>
#include <QStringList>
#include <QUrl>
@@ -54,7 +53,7 @@ class SingleZipPackInstallTask : public InstanceTask {
QString m_minecraftVersion;
QString m_archivePath;
NetJob::Ptr m_filesNetJob;
std::unique_ptr<QuaZip> m_packZip;
std::unique_ptr<MMCZip::ArchiveReader> m_packZip;
QFuture<std::optional<QStringList>> m_extractFuture;
QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
};

View File

@@ -19,12 +19,10 @@
#include <Json.h>
#include <minecraft/MinecraftInstance.h>
#include <minecraft/PackProfile.h>
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
#include <quazip/quazipfile.h>
#include <settings/INISettingsObject.h>
#include <memory>
#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;

View File

@@ -43,6 +43,7 @@
#include <QMessageBox>
#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<MMCZip::ExportToZipTask>(output, m_instance->instanceRoot(), files, "", true, true);
auto task = makeShared<MMCZip::ExportToZipTask>(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(); });

View File

@@ -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

Submodule libraries/quazip deleted from 3fd3b299b8

View File

@@ -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
]

View File

@@ -1,7 +1,7 @@
{
"default-registry": {
"kind": "git",
"baseline": "1fddddc280dfed63956e15ef74f4321bc6a219c9",
"baseline": "2d6a6cf3ac9a7cc93942c3d289a2f9c661a6f4a7",
"repository": "https://github.com/microsoft/vcpkg"
},
"registries": [

View File

@@ -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"
]