Merge pull request #5 from MultiMC/develop

Some updates
This commit is contained in:
Zakhar Afonin
2020-11-19 22:59:15 +03:00
committed by GitHub
34 changed files with 2420 additions and 13 deletions

View File

@@ -306,6 +306,9 @@ set(MINECRAFT_SOURCES
# Skin upload utilities
minecraft/SkinUpload.cpp
minecraft/SkinUpload.h
mojang/PackageManifest.h
mojang/PackageManifest.cpp
)
add_unit_test(GradleSpecifier
@@ -313,6 +316,22 @@ add_unit_test(GradleSpecifier
LIBS MultiMC_logic
)
add_executable(PackageManifest
mojang/PackageManifest_test.cpp
)
target_link_libraries(PackageManifest
MultiMC_logic
Qt5::Test
)
target_include_directories(PackageManifest
PRIVATE ../../cmake/UnitTest/
)
add_test(
NAME PackageManifest
COMMAND PackageManifest
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
add_unit_test(MojangVersionFormat
SOURCES minecraft/MojangVersionFormat_test.cpp
LIBS MultiMC_logic
@@ -458,6 +477,15 @@ set(MODPACKSCH_SOURCES
modplatform/modpacksch/FTBPackManifest.cpp
)
set(TECHNIC_SOURCES
modplatform/technic/SingleZipPackInstallTask.h
modplatform/technic/SingleZipPackInstallTask.cpp
modplatform/technic/SolderPackInstallTask.h
modplatform/technic/SolderPackInstallTask.cpp
modplatform/technic/TechnicPackProcessor.h
modplatform/technic/TechnicPackProcessor.cpp
)
add_unit_test(Index
SOURCES meta/Index_test.cpp
LIBS MultiMC_logic
@@ -489,6 +517,7 @@ set(LOGIC_SOURCES
${FTB_SOURCES}
${FLAME_SOURCES}
${MODPACKSCH_SOURCES}
${TECHNIC_SOURCES}
)
add_library(MultiMC_logic SHARED ${LOGIC_SOURCES})

View File

@@ -98,6 +98,7 @@ void Env::initHttpMetaCache()
m_metacache->addBase("general", QDir("cache").absolutePath());
m_metacache->addBase("FTBPacks", QDir("cache/FTBPacks").absolutePath());
m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath());
m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath());
m_metacache->addBase("TwitchPacks", QDir("cache/TwitchPacks").absolutePath());
m_metacache->addBase("skins", QDir("accounts/skins").absolutePath());
m_metacache->addBase("root", QDir::currentPath());

View File

@@ -1,3 +1,18 @@
/* Copyright 2013-2020 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 "InstanceImportTask.h"
#include "BaseInstance.h"
#include "FileSystem.h"
@@ -15,6 +30,8 @@
#include "modplatform/flame/FileResolvingTask.h"
#include "modplatform/flame/PackManifest.h"
#include "Json.h"
#include <quazipdir.h>
#include "modplatform/technic/TechnicPackProcessor.h"
InstanceImportTask::InstanceImportTask(const QUrl sourceUrl)
{
@@ -23,8 +40,6 @@ InstanceImportTask::InstanceImportTask(const QUrl sourceUrl)
void InstanceImportTask::executeTask()
{
InstancePtr newInstance;
if (m_sourceUrl.isLocalFile())
{
m_archivePath = m_sourceUrl.toLocalFile();
@@ -82,6 +97,7 @@ void InstanceImportTask::processZipPack()
QStringList blacklist = {"instance.cfg", "manifest.json"};
QString mmcFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg");
bool technicFound = QuaZipDir(m_packZip.get()).exists("/bin/modpack.jar") || QuaZipDir(m_packZip.get()).exists("/bin/version.json");
QString flameFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json");
QString root;
if(!mmcFound.isNull())
@@ -91,6 +107,14 @@ void InstanceImportTask::processZipPack()
root = mmcFound;
m_modpackType = ModpackType::MultiMC;
}
else if (technicFound)
{
// process as Technic pack
qDebug() << "Technic:" << technicFound;
extractDir.mkpath(".minecraft");
extractDir.cd(".minecraft");
m_modpackType = ModpackType::Technic;
}
else if(!flameFound.isNull())
{
// process as Flame pack
@@ -98,7 +122,6 @@ void InstanceImportTask::processZipPack()
root = flameFound;
m_modpackType = ModpackType::Flame;
}
if(m_modpackType == ModpackType::Unknown)
{
emitFailed(tr("Archive does not contain a recognized modpack type."));
@@ -161,6 +184,9 @@ void InstanceImportTask::extractFinished()
case ModpackType::MultiMC:
processMultiMC();
return;
case ModpackType::Technic:
processTechnic();
return;
case ModpackType::Unknown:
emitFailed(tr("Archive does not contain a recognized modpack type."));
return;
@@ -371,6 +397,14 @@ void InstanceImportTask::processFlame()
m_modIdResolver->start();
}
void InstanceImportTask::processTechnic()
{
shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &InstanceImportTask::emitSucceeded);
connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &InstanceImportTask::emitFailed);
packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath);
}
void InstanceImportTask::processMultiMC()
{
// FIXME: copy from FolderInstanceProvider!!! FIX IT!!!

View File

@@ -1,3 +1,18 @@
/* Copyright 2013-2020 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 "InstanceTask.h"
@@ -29,6 +44,7 @@ private:
void processZipPack();
void processMultiMC();
void processFlame();
void processTechnic();
private slots:
void downloadSucceeded();
@@ -49,6 +65,7 @@ private: /* data */
enum class ModpackType{
Unknown,
MultiMC,
Flame
Flame,
Technic
} m_modpackType = ModpackType::Unknown;
};

View File

@@ -1,4 +1,4 @@
/* Copyright 2013-2019 MultiMC Contributors
/* Copyright 2013-2020 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,4 +1,4 @@
/* Copyright 2013-2019 MultiMC Contributors
/* Copyright 2013-2020 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -67,5 +67,4 @@ namespace MMCZip
* \return The list of the full paths of the files extracted, empty on failure.
*/
QStringList MULTIMC_LOGIC_EXPORT extractDir(QString fileCompressed, QString dir);
}

View File

@@ -123,16 +123,20 @@ void PackInstallTask::install()
auto dl = Net::Download::makeFile(file.url, path);
jobPtr->addNetAction(dl);
}
connect(jobPtr.get(), &NetJob::succeeded, this, [&]()
{
jobPtr.reset();
emitSucceeded();
});
connect(jobPtr.get(), &NetJob::failed, [&](QString reason)
{
jobPtr.reset();
emitFailed(reason);
// FIXME: Temporarily ignore file download failures (matching FTB's installer),
// while FTB's data is fucked.
qWarning() << "Failed to download files for modpack: " + reason;
emitSucceeded();
});
connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total)
{

View File

@@ -0,0 +1,129 @@
/* Copyright 2013-2020 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 "SingleZipPackInstallTask.h"
#include "Env.h"
#include "MMCZip.h"
#include "TechnicPackProcessor.h"
#include <QtConcurrent>
#include <FileSystem.h>
Technic::SingleZipPackInstallTask::SingleZipPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion)
{
m_sourceUrl = sourceUrl;
m_minecraftVersion = minecraftVersion;
}
void Technic::SingleZipPackInstallTask::executeTask()
{
setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString()));
const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path();
auto entry = ENV.metacache()->resolveEntry("general", path);
entry->setStale(true);
m_filesNetJob.reset(new NetJob(tr("Modpack download")));
m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry));
m_archivePath = entry->getFullPath();
auto job = m_filesNetJob.get();
connect(job, &NetJob::succeeded, this, &Technic::SingleZipPackInstallTask::downloadSucceeded);
connect(job, &NetJob::progress, this, &Technic::SingleZipPackInstallTask::downloadProgressChanged);
connect(job, &NetJob::failed, this, &Technic::SingleZipPackInstallTask::downloadFailed);
m_filesNetJob->start();
}
void Technic::SingleZipPackInstallTask::downloadSucceeded()
{
setStatus(tr("Extracting modpack"));
QDir extractDir(FS::PathCombine(m_stagingPath, ".minecraft"));
qDebug() << "Attempting to create instance from" << m_archivePath;
// open the zip and find relevant files in it
m_packZip.reset(new QuaZip(m_archivePath));
if (!m_packZip->open(QuaZip::mdUnzip))
{
emitFailed(tr("Unable to open supplied modpack zip file."));
return;
}
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), QString(""), extractDir.absolutePath());
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &Technic::SingleZipPackInstallTask::extractFinished);
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &Technic::SingleZipPackInstallTask::extractAborted);
m_extractFutureWatcher.setFuture(m_extractFuture);
m_filesNetJob.reset();
}
void Technic::SingleZipPackInstallTask::downloadFailed(QString reason)
{
emitFailed(reason);
m_filesNetJob.reset();
}
void Technic::SingleZipPackInstallTask::downloadProgressChanged(qint64 current, qint64 total)
{
setProgress(current / 2, total);
}
void Technic::SingleZipPackInstallTask::extractFinished()
{
m_packZip.reset();
if (m_extractFuture.result().isEmpty())
{
emitFailed(tr("Failed to extract modpack"));
return;
}
QDir extractDir(m_stagingPath);
qDebug() << "Fixing permissions for extracted pack files...";
QDirIterator it(extractDir, QDirIterator::Subdirectories);
while (it.hasNext())
{
auto filepath = it.next();
QFileInfo file(filepath);
auto permissions = QFile::permissions(filepath);
auto origPermissions = permissions;
if (file.isDir())
{
// Folder +rwx for current user
permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser;
}
else
{
// File +rw for current user
permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser;
}
if (origPermissions != permissions)
{
if (!QFile::setPermissions(filepath, permissions))
{
logWarning(tr("Could not fix permissions for %1").arg(filepath));
}
else
{
qDebug() << "Fixed" << filepath;
}
}
}
shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SingleZipPackInstallTask::emitSucceeded);
connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SingleZipPackInstallTask::emitFailed);
packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath, m_minecraftVersion);
}
void Technic::SingleZipPackInstallTask::extractAborted()
{
emitFailed(tr("Instance import has been aborted."));
}

View File

@@ -0,0 +1,58 @@
/* Copyright 2013-2020 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 "InstanceTask.h"
#include "net/NetJob.h"
#include "multimc_logic_export.h"
#include "quazip.h"
#include <QFutureWatcher>
#include <QStringList>
#include <QUrl>
namespace Technic {
class MULTIMC_LOGIC_EXPORT SingleZipPackInstallTask : public InstanceTask
{
Q_OBJECT
public:
SingleZipPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion);
protected:
void executeTask() override;
private slots:
void downloadSucceeded();
void downloadFailed(QString reason);
void downloadProgressChanged(qint64 current, qint64 total);
void extractFinished();
void extractAborted();
private:
QUrl m_sourceUrl;
QString m_minecraftVersion;
QString m_archivePath;
NetJobPtr m_filesNetJob;
std::unique_ptr<QuaZip> m_packZip;
QFuture<QStringList> m_extractFuture;
QFutureWatcher<QStringList> m_extractFutureWatcher;
};
} // namespace Technic

View File

@@ -0,0 +1,195 @@
/* Copyright 2013-2020 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 "SolderPackInstallTask.h"
#include <FileSystem.h>
#include <Json.h>
#include <QtConcurrentRun>
#include <MMCZip.h>
#include "TechnicPackProcessor.h"
Technic::SolderPackInstallTask::SolderPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion)
{
m_sourceUrl = sourceUrl;
m_minecraftVersion = minecraftVersion;
}
void Technic::SolderPackInstallTask::executeTask()
{
setStatus(tr("Finding recommended version:\n%1").arg(m_sourceUrl.toString()));
m_filesNetJob.reset(new NetJob(tr("Finding recommended version")));
m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response));
auto job = m_filesNetJob.get();
connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::versionSucceeded);
connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
m_filesNetJob->start();
}
void Technic::SolderPackInstallTask::versionSucceeded()
{
try
{
QJsonDocument doc = Json::requireDocument(m_response);
QJsonObject obj = Json::requireObject(doc);
QString version = Json::requireString(obj, "recommended", "__placeholder__");
m_sourceUrl = m_sourceUrl.toString() + '/' + version;
}
catch (const JSONValidationError &e)
{
emitFailed(e.cause());
m_filesNetJob.reset();
return;
}
setStatus(tr("Resolving modpack files:\n%1").arg(m_sourceUrl.toString()));
m_filesNetJob.reset(new NetJob(tr("Resolving modpack files")));
m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response));
auto job = m_filesNetJob.get();
connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded);
connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
m_filesNetJob->start();
}
void Technic::SolderPackInstallTask::fileListSucceeded()
{
setStatus(tr("Downloading modpack:"));
QStringList modUrls;
try
{
QJsonDocument doc = Json::requireDocument(m_response);
QJsonObject obj = Json::requireObject(doc);
QString minecraftVersion = Json::ensureString(obj, "minecraft", QString(), "__placeholder__");
if (!minecraftVersion.isEmpty())
m_minecraftVersion = minecraftVersion;
QJsonArray mods = Json::requireArray(obj, "mods", "'mods'");
for (auto mod: mods)
{
QJsonObject modObject = Json::requireObject(mod);
modUrls.append(Json::requireString(modObject, "url", "'url'"));
}
}
catch (const JSONValidationError &e)
{
emitFailed(e.cause());
m_filesNetJob.reset();
return;
}
m_filesNetJob.reset(new NetJob(tr("Downloading modpack")));
int i = 0;
for (auto &modUrl: modUrls)
{
auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i));
m_filesNetJob->addNetAction(Net::Download::makeFile(modUrl, path));
i++;
}
m_modCount = modUrls.size();
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &Technic::SolderPackInstallTask::downloadSucceeded);
connect(m_filesNetJob.get(), &NetJob::progress, this, &Technic::SolderPackInstallTask::downloadProgressChanged);
connect(m_filesNetJob.get(), &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
m_filesNetJob->start();
}
void Technic::SolderPackInstallTask::downloadSucceeded()
{
setStatus(tr("Extracting modpack"));
m_filesNetJob.reset();
m_extractFuture = QtConcurrent::run([this]()
{
int i = 0;
QString extractDir = FS::PathCombine(m_stagingPath, ".minecraft");
FS::ensureFolderPathExists(extractDir);
while (m_modCount > i)
{
auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i));
if (MMCZip::extractDir(path, extractDir).isEmpty())
{
return false;
}
i++;
}
return true;
});
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &Technic::SolderPackInstallTask::extractFinished);
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &Technic::SolderPackInstallTask::extractAborted);
m_extractFutureWatcher.setFuture(m_extractFuture);
}
void Technic::SolderPackInstallTask::downloadFailed(QString reason)
{
emitFailed(reason);
m_filesNetJob.reset();
}
void Technic::SolderPackInstallTask::downloadProgressChanged(qint64 current, qint64 total)
{
setProgress(current / 2, total);
}
void Technic::SolderPackInstallTask::extractFinished()
{
if (!m_extractFuture.result())
{
emitFailed(tr("Failed to extract modpack"));
return;
}
QDir extractDir(m_stagingPath);
qDebug() << "Fixing permissions for extracted pack files...";
QDirIterator it(extractDir, QDirIterator::Subdirectories);
while (it.hasNext())
{
auto filepath = it.next();
QFileInfo file(filepath);
auto permissions = QFile::permissions(filepath);
auto origPermissions = permissions;
if(file.isDir())
{
// Folder +rwx for current user
permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser;
}
else
{
// File +rw for current user
permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser;
}
if(origPermissions != permissions)
{
if(!QFile::setPermissions(filepath, permissions))
{
logWarning(tr("Could not fix permissions for %1").arg(filepath));
}
else
{
qDebug() << "Fixed" << filepath;
}
}
}
shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SolderPackInstallTask::emitSucceeded);
connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SolderPackInstallTask::emitFailed);
packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath, m_minecraftVersion, true);
}
void Technic::SolderPackInstallTask::extractAborted()
{
emitFailed(tr("Instance import has been aborted."));
return;
}

View File

@@ -0,0 +1,55 @@
/* Copyright 2013-2020 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 <InstanceTask.h>
#include <net/NetJob.h>
#include <tasks/Task.h>
#include <QUrl>
namespace Technic
{
class MULTIMC_LOGIC_EXPORT SolderPackInstallTask : public InstanceTask
{
Q_OBJECT
public:
explicit SolderPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion);
protected:
//! Entry point for tasks.
virtual void executeTask() override;
private slots:
void versionSucceeded();
void fileListSucceeded();
void downloadSucceeded();
void downloadFailed(QString reason);
void downloadProgressChanged(qint64 current, qint64 total);
void extractFinished();
void extractAborted();
private:
NetJobPtr m_filesNetJob;
QUrl m_sourceUrl;
QString m_minecraftVersion;
QByteArray m_response;
QTemporaryDir m_outputDir;
int m_modCount;
QFuture<bool> m_extractFuture;
QFutureWatcher<bool> m_extractFutureWatcher;
};
}

View File

@@ -0,0 +1,199 @@
/* Copyright 2020 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 "TechnicPackProcessor.h"
#include <FileSystem.h>
#include <Json.h>
#include <minecraft/MinecraftInstance.h>
#include <minecraft/PackProfile.h>
#include <quazip.h>
#include <quazipdir.h>
#include <quazipfile.h>
#include <settings/INISettingsObject.h>
#include <memory>
void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const QString &instName, const QString &instIcon, const QString &stagingPath, const QString &minecraftVersion, const bool isSolder)
{
QString minecraftPath = FS::PathCombine(stagingPath, ".minecraft");
QString configPath = FS::PathCombine(stagingPath, "instance.cfg");
auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
instanceSettings->registerSetting("InstanceType", "Legacy");
instanceSettings->set("InstanceType", "OneSix");
MinecraftInstance instance(globalSettings, instanceSettings, stagingPath);
instance.setName(instName);
if (instIcon != "default")
{
instance.setIconKey(instIcon);
}
auto components = instance.getPackProfile();
components->buildingFromScratch();
QByteArray data;
QString modpackJar = FS::PathCombine(minecraftPath, "bin", "modpack.jar");
QString versionJson = FS::PathCombine(minecraftPath, "bin", "version.json");
QString fmlMinecraftVersion;
if (QFile::exists(modpackJar))
{
QuaZip zipFile(modpackJar);
if (!zipFile.open(QuaZip::mdUnzip))
{
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))
{
emit failed(tr("Unable to open \"fmlversion.properties\"!"));
return;
}
QByteArray fmlVersionData = file.readAll();
file.close();
INIFile iniFile;
iniFile.loadFile(fmlVersionData);
// If not present, this evaluates to a null string
fmlMinecraftVersion = iniFile["fmlbuild.mcversion"].toString();
}
zipFile.setCurrentFile("version.json", QuaZip::csSensitive);
QuaZipFile file(&zipFile);
if (!file.open(QIODevice::ReadOnly))
{
emit failed(tr("Unable to open \"version.json\"!"));
return;
}
data = file.readAll();
file.close();
}
else
{
if (minecraftVersion.isEmpty())
emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but minecraft version is unknown"));
components->setComponentVersion("net.minecraft", minecraftVersion, true);
components->installJarMods({modpackJar});
// 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))
{
// Really shouldn't happen, but error handling shall not be forgotten
emit failed(tr("Unable to open \"forgeversion.properties\""));
return;
}
QByteArray forgeVersionData = file.readAll();
file.close();
INIFile iniFile;
iniFile.loadFile(forgeVersionData);
QString major, minor, revision, build;
major = iniFile["forge.major.number"].toString();
minor = iniFile["forge.minor.number"].toString();
revision = iniFile["forge.revision.number"].toString();
build = iniFile["forge.build.number"].toString();
if (major.isEmpty() || minor.isEmpty() || revision.isEmpty() || build.isEmpty())
{
emit failed(tr("Invalid \"forgeversion.properties\"!"));
return;
}
components->setComponentVersion("net.minecraftforge", major + '.' + minor + '.' + revision + '.' + build);
}
components->saveNow();
emit succeeded();
return;
}
}
else if (QFile::exists(versionJson))
{
QFile file(versionJson);
if (!file.open(QIODevice::ReadOnly))
{
emit failed(tr("Unable to open \"version.json\"!"));
return;
}
data = file.readAll();
file.close();
}
else
{
// This is the "Vanilla" modpack, excluded by the search code
emit failed(tr("Unable to find a \"version.json\"!"));
return;
}
try
{
QJsonDocument doc = Json::requireDocument(data);
QJsonObject root = Json::requireObject(doc, "version.json");
QString minecraftVersion = Json::ensureString(root, "inheritsFrom", QString(), "");
if (minecraftVersion.isEmpty())
{
if (fmlMinecraftVersion.isEmpty())
{
emit failed(tr("Could not understand \"version.json\":\ninheritsFrom is missing"));
return;
}
minecraftVersion = fmlMinecraftVersion;
}
components->setComponentVersion("net.minecraft", minecraftVersion, true);
for (auto library: Json::ensureArray(root, "libraries", {}))
{
if (!library.isObject())
{
continue;
}
auto libraryObject = Json::ensureObject(library, {}, "");
auto libraryName = Json::ensureString(libraryObject, "name", "", "");
if (libraryName.startsWith("net.minecraftforge:forge:") && libraryName.contains('-'))
{
components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1));
}
else if (libraryName.startsWith("net.minecraftforge:minecraftforge:"))
{
components->setComponentVersion("net.minecraftforge", libraryName.section(':', 2));
}
else if (libraryName.startsWith("net.fabricmc:fabric-loader:"))
{
components->setComponentVersion("net.fabricmc.fabric-loader", libraryName.section(':', 2));
}
}
}
catch (const JSONValidationError &e)
{
emit failed(tr("Could not understand \"version.json\":\n") + e.cause());
return;
}
components->saveNow();
emit succeeded();
}

View File

@@ -0,0 +1,35 @@
/* Copyright 2020 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 <QString>
#include "settings/SettingsObject.h"
namespace Technic
{
// not exporting it, only used in SingleZipPackInstallTask, InstanceImportTask and SolderPackInstallTask
class TechnicPackProcessor : public QObject
{
Q_OBJECT
signals:
void succeeded();
void failed(QString reason);
public:
void run(SettingsObjectPtr globalSettings, const QString &instName, const QString &instIcon, const QString &stagingPath, const QString &minecraftVersion=QString(), const bool isSolder = false);
};
}

View File

@@ -0,0 +1,427 @@
#include "PackageManifest.h"
#include <Json.h>
#include <QDir>
#include <QDirIterator>
#include <QCryptographicHash>
#include <QDebug>
#ifndef Q_OS_WIN32
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#endif
namespace mojang_files {
const Hash hash_of_empty_string = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
int Path::compare(const Path& rhs) const
{
auto left_cursor = begin();
auto left_end = end();
auto right_cursor = rhs.begin();
auto right_end = rhs.end();
while (left_cursor != left_end && right_cursor != right_end)
{
if(*left_cursor < *right_cursor)
{
return -1;
}
else if(*left_cursor > *right_cursor)
{
return 1;
}
left_cursor++;
right_cursor++;
}
if(left_cursor == left_end)
{
if(right_cursor == right_end)
{
return 0;
}
return -1;
}
return 1;
}
void Package::addFile(const Path& path, const File& file) {
addFolder(path.parent_path());
files[path] = file;
}
void Package::addFolder(Path folder) {
if(!folder.has_parent_path()) {
return;
}
do {
folders.insert(folder);
folder = folder.parent_path();
} while(folder.has_parent_path());
}
void Package::addLink(const Path& path, const Path& target) {
addFolder(path.parent_path());
symlinks[path] = target;
}
void Package::addSource(const FileSource& source) {
sources[source.hash] = source;
}
namespace {
void fromJson(QJsonDocument & doc, Package & out) {
std::set<Path> seen_paths;
if (!doc.isObject())
{
throw JSONValidationError("file manifest is not an object");
}
QJsonObject root = doc.object();
auto filesObj = Json::ensureObject(root, "files");
auto iter = filesObj.begin();
while (iter != filesObj.end())
{
Path objectPath = Path(iter.key());
auto value = iter.value();
iter++;
if(seen_paths.count(objectPath)) {
throw JSONValidationError("duplicate path inside manifest, the manifest is invalid");
}
if (!value.isObject())
{
throw JSONValidationError("file entry inside manifest is not an an object");
}
seen_paths.insert(objectPath);
auto fileObject = value.toObject();
auto type = Json::requireString(fileObject, "type");
if(type == "directory") {
out.addFolder(objectPath);
continue;
}
else if(type == "file") {
FileSource bestSource;
File file;
file.executable = Json::ensureBoolean(fileObject, QString("executable"), false);
auto downloads = Json::requireObject(fileObject, "downloads");
for(auto iter2 = downloads.begin(); iter2 != downloads.end(); iter2++) {
FileSource source;
auto downloadObject = Json::requireObject(iter2.value());
source.hash = Json::requireString(downloadObject, "sha1");
source.size = Json::requireInteger(downloadObject, "size");
source.url = Json::requireString(downloadObject, "url");
auto compression = iter2.key();
if(compression == "raw") {
file.hash = source.hash;
file.size = source.size;
source.compression = Compression::Raw;
}
else if (compression == "lzma") {
source.compression = Compression::Lzma;
}
else {
continue;
}
bestSource.upgrade(source);
}
if(bestSource.isBad()) {
throw JSONValidationError("No valid compression method for file " + iter.key());
}
out.addFile(objectPath, file);
out.addSource(bestSource);
}
else if(type == "link") {
auto target = Json::requireString(fileObject, "target");
out.symlinks[objectPath] = target;
out.addLink(objectPath, target);
}
else {
throw JSONValidationError("Invalid item type in manifest: " + type);
}
}
// make sure the containing folder exists
out.folders.insert(Path());
}
}
Package Package::fromManifestContents(const QByteArray& contents)
{
Package out;
try
{
auto doc = Json::requireDocument(contents, "Manifest");
fromJson(doc, out);
return out;
}
catch (const Exception &e)
{
qDebug() << QString("Unable to parse manifest: %1").arg(e.cause());
out.valid = false;
return out;
}
}
Package Package::fromManifestFile(const QString & filename) {
Package out;
try
{
auto doc = Json::requireDocument(filename, filename);
fromJson(doc, out);
return out;
}
catch (const Exception &e)
{
qDebug() << QString("Unable to parse manifest file %1: %2").arg(filename, e.cause());
out.valid = false;
return out;
}
}
#ifndef Q_OS_WIN32
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
namespace {
// FIXME: Qt obscures symlink targets by making them absolute. that is useless. this is the workaround - we do it ourselves
bool actually_read_symlink_target(const QString & filepath, Path & out)
{
struct ::stat st;
// FIXME: here, we assume the native filesystem encoding. May the Gods have mercy upon our Souls.
QByteArray nativePath = filepath.toUtf8();
const char * filepath_cstr = nativePath.data();
if (lstat(filepath_cstr, &st) != 0)
{
return false;
}
auto size = st.st_size ? st.st_size + 1 : PATH_MAX;
std::string temp(size, '\0');
// because we don't realiably know how long the damn thing actually is, we loop and expand. POSIX is naff
do
{
auto link_length = ::readlink(filepath_cstr, &temp[0], temp.size());
if(link_length == -1)
{
return false;
}
if(std::string::size_type(link_length) < temp.size())
{
// buffer was long enough and we managed to read the link target. RETURN here.
temp.resize(link_length);
out = Path(QString::fromUtf8(temp.c_str()));
return true;
}
temp.resize(temp.size() * 2);
} while (true);
}
}
#endif
// FIXME: Qt filesystem abstraction is bad, but ... let's hope it doesn't break too much?
// FIXME: The error handling is just DEFICIENT
Package Package::fromInspectedFolder(const QString& folderPath)
{
QDir root(folderPath);
Package out;
QDirIterator iterator(folderPath, QDir::NoDotAndDotDot | QDir::AllEntries | QDir::System | QDir::Hidden, QDirIterator::Subdirectories);
while(iterator.hasNext()) {
iterator.next();
auto fileInfo = iterator.fileInfo();
auto relPath = root.relativeFilePath(fileInfo.filePath());
// FIXME: this is probably completely busted on Windows anyway, so just disable it.
// Qt makes shit up and doesn't understand the platform details
// TODO: Actually use a filesystem library that isn't terrible and has decen license.
// I only know one, and I wrote it. Sadly, currently proprietary. PAIN.
#ifndef Q_OS_WIN32
if(fileInfo.isSymLink()) {
Path targetPath;
if(!actually_read_symlink_target(fileInfo.filePath(), targetPath)) {
qCritical() << "Folder inspection: Unknown filesystem object:" << fileInfo.absoluteFilePath();
out.valid = false;
}
out.addLink(relPath, targetPath);
}
else
#endif
if(fileInfo.isDir()) {
out.addFolder(relPath);
}
else if(fileInfo.isFile()) {
File f;
f.executable = fileInfo.isExecutable();
f.size = fileInfo.size();
// FIXME: async / optimize the hashing
QFile input(fileInfo.absoluteFilePath());
if(!input.open(QIODevice::ReadOnly)) {
qCritical() << "Folder inspection: Failed to open file:" << fileInfo.absoluteFilePath();
out.valid = false;
break;
}
f.hash = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Sha1).toHex().constData();
out.addFile(relPath, f);
}
else {
// Something else... oh my
qCritical() << "Folder inspection: Unknown filesystem object:" << fileInfo.absoluteFilePath();
out.valid = false;
break;
}
}
out.folders.insert(Path("."));
out.valid = true;
return out;
}
namespace {
struct shallow_first_sort
{
bool operator()(const Path &lhs, const Path &rhs) const
{
auto lhs_depth = lhs.length();
auto rhs_depth = rhs.length();
if(lhs_depth < rhs_depth)
{
return true;
}
else if(lhs_depth == rhs_depth)
{
if(lhs < rhs)
{
return true;
}
}
return false;
}
};
struct deep_first_sort
{
bool operator()(const Path &lhs, const Path &rhs) const
{
auto lhs_depth = lhs.length();
auto rhs_depth = rhs.length();
if(lhs_depth > rhs_depth)
{
return true;
}
else if(lhs_depth == rhs_depth)
{
if(lhs < rhs)
{
return true;
}
}
return false;
}
};
}
UpdateOperations UpdateOperations::resolve(const Package& from, const Package& to)
{
UpdateOperations out;
if(!from.valid || !to.valid) {
out.valid = false;
return out;
}
// Files
for(auto iter = from.files.begin(); iter != from.files.end(); iter++) {
const auto &current_hash = iter->second.hash;
const auto &current_executable = iter->second.executable;
const auto &path = iter->first;
auto iter2 = to.files.find(path);
if(iter2 == to.files.end()) {
// removed
out.deletes.push_back(path);
continue;
}
auto new_hash = iter2->second.hash;
auto new_executable = iter2->second.executable;
if (current_hash != new_hash) {
out.deletes.push_back(path);
out.downloads.emplace(
std::pair<Path, FileDownload>{
path,
FileDownload(to.sources.at(iter2->second.hash), iter2->second.executable)
}
);
}
else if (current_executable != new_executable) {
out.executable_fixes[path] = new_executable;
}
}
for(auto iter = to.files.begin(); iter != to.files.end(); iter++) {
auto path = iter->first;
if(!from.files.count(path)) {
out.downloads.emplace(
std::pair<Path, FileDownload>{
path,
FileDownload(to.sources.at(iter->second.hash), iter->second.executable)
}
);
}
}
// Folders
std::set<Path, deep_first_sort> remove_folders;
std::set<Path, shallow_first_sort> make_folders;
for(auto from_path: from.folders) {
auto iter = to.folders.find(from_path);
if(iter == to.folders.end()) {
remove_folders.insert(from_path);
}
}
for(auto & rmdir: remove_folders) {
out.rmdirs.push_back(rmdir);
}
for(auto to_path: to.folders) {
auto iter = from.folders.find(to_path);
if(iter == from.folders.end()) {
make_folders.insert(to_path);
}
}
for(auto & mkdir: make_folders) {
out.mkdirs.push_back(mkdir);
}
// Symlinks
for(auto iter = from.symlinks.begin(); iter != from.symlinks.end(); iter++) {
const auto &current_target = iter->second;
const auto &path = iter->first;
auto iter2 = to.symlinks.find(path);
if(iter2 == to.symlinks.end()) {
// removed
out.deletes.push_back(path);
continue;
}
const auto &new_target = iter2->second;
if (current_target != new_target) {
out.deletes.push_back(path);
out.mklinks[path] = iter2->second;
}
}
for(auto iter = to.symlinks.begin(); iter != to.symlinks.end(); iter++) {
auto path = iter->first;
if(!from.symlinks.count(path)) {
out.mklinks[path] = iter->second;
}
}
out.valid = true;
return out;
}
}

View File

@@ -0,0 +1,173 @@
#pragma once
#include <QString>
#include <map>
#include <set>
#include <QStringList>
#include "tasks/Task.h"
#include "multimc_logic_export.h"
namespace mojang_files {
using Hash = QString;
extern const Hash empty_hash;
// simple-ish path implementation. assumes always relative and does not allow '..' entries
class MULTIMC_LOGIC_EXPORT Path
{
public:
using parts_type = QStringList;
Path() = default;
Path(QString string) {
auto parts_in = string.split('/');
for(auto & part: parts_in) {
if(part.isEmpty() || part == ".") {
continue;
}
if(part == "..") {
if(parts.size()) {
parts.pop_back();
}
continue;
}
parts.push_back(part);
}
}
bool has_parent_path() const
{
return parts.size() > 0;
}
Path parent_path() const
{
if (parts.empty())
return Path();
return Path(parts.begin(), std::prev(parts.end()));
}
bool empty() const
{
return parts.empty();
}
int length() const
{
return parts.length();
}
bool operator==(const Path & rhs) const {
return parts == rhs.parts;
}
bool operator!=(const Path & rhs) const {
return parts != rhs.parts;
}
inline bool operator<(const Path& rhs) const
{
return compare(rhs) < 0;
}
parts_type::const_iterator begin() const
{
return parts.begin();
}
parts_type::const_iterator end() const
{
return parts.end();
}
QString toString() const {
return parts.join("/");
}
private:
Path(const parts_type::const_iterator & start, const parts_type::const_iterator & end) {
auto cursor = start;
while(cursor != end) {
parts.push_back(*cursor);
cursor++;
}
}
int compare(const Path& p) const;
parts_type parts;
};
enum class Compression {
Raw,
Lzma,
Unknown
};
struct MULTIMC_LOGIC_EXPORT FileSource
{
Compression compression = Compression::Unknown;
Hash hash;
QString url;
std::size_t size = 0;
void upgrade(const FileSource & other) {
if(compression == Compression::Unknown || other.size < size) {
*this = other;
}
}
bool isBad() const {
return compression == Compression::Unknown;
}
};
struct MULTIMC_LOGIC_EXPORT File
{
Hash hash;
bool executable;
std::uint64_t size = 0;
};
struct MULTIMC_LOGIC_EXPORT Package {
static Package fromInspectedFolder(const QString &folderPath);
static Package fromManifestFile(const QString &path);
static Package fromManifestContents(const QByteArray& contents);
explicit operator bool() const
{
return valid;
}
void addFolder(Path folder);
void addFile(const Path & path, const File & file);
void addLink(const Path & path, const Path & target);
void addSource(const FileSource & source);
std::map<Hash, FileSource> sources;
bool valid = true;
std::set<Path> folders;
std::map<Path, File> files;
std::map<Path, Path> symlinks;
};
struct MULTIMC_LOGIC_EXPORT FileDownload : FileSource
{
FileDownload(const FileSource& source, bool executable) {
static_cast<FileSource &> (*this) = source;
this->executable = executable;
}
bool executable = false;
};
struct MULTIMC_LOGIC_EXPORT UpdateOperations {
static UpdateOperations resolve(const Package & from, const Package & to);
bool valid = false;
std::vector<Path> deletes;
std::vector<Path> rmdirs;
std::vector<Path> mkdirs;
std::map<Path, FileDownload> downloads;
std::map<Path, Path> mklinks;
std::map<Path, bool> executable_fixes;
};
}

View File

@@ -0,0 +1,344 @@
#include <QTest>
#include <QDebug>
#include "TestUtil.h"
#include "mojang/PackageManifest.h"
using namespace mojang_files;
QDebug operator<<(QDebug debug, const Path &path)
{
debug << path.toString();
return debug;
}
class PackageManifestTest : public QObject
{
Q_OBJECT
private slots:
void test_parse();
void test_parse_file();
void test_inspect();
#ifndef Q_OS_WIN32
void test_inspect_symlinks();
#endif
void mkdir_deep();
void rmdir_deep();
void identical_file();
void changed_file();
void added_file();
void removed_file();
};
namespace {
QByteArray basic_manifest = R"END(
{
"files": {
"a/b.txt": {
"type": "file",
"downloads": {
"raw": {
"url": "http://dethware.org/b.txt",
"sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
"size": 0
}
},
"executable": true
},
"a/b/c": {
"type": "directory"
},
"a/b/c.txt": {
"type": "link",
"target": "../b.txt"
}
}
}
)END";
}
void PackageManifestTest::test_parse()
{
auto manifest = Package::fromManifestContents(basic_manifest);
QVERIFY(manifest.valid == true);
QVERIFY(manifest.files.size() == 1);
QVERIFY(manifest.files.count(Path("a/b.txt")));
auto &file = manifest.files[Path("a/b.txt")];
QVERIFY(file.executable == true);
QVERIFY(file.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709");
QVERIFY(file.size == 0);
QVERIFY(manifest.folders.size() == 4);
QVERIFY(manifest.folders.count(Path(".")));
QVERIFY(manifest.folders.count(Path("a")));
QVERIFY(manifest.folders.count(Path("a/b")));
QVERIFY(manifest.folders.count(Path("a/b/c")));
QVERIFY(manifest.symlinks.size() == 1);
auto symlinkPath = Path("a/b/c.txt");
QVERIFY(manifest.symlinks.count(symlinkPath));
auto &symlink = manifest.symlinks[symlinkPath];
QVERIFY(symlink == Path("../b.txt"));
QVERIFY(manifest.sources.size() == 1);
}
void PackageManifestTest::test_parse_file() {
auto path = QFINDTESTDATA("testdata/1.8.0_202-x64.json");
auto manifest = Package::fromManifestFile(path);
QVERIFY(manifest.valid == true);
}
void PackageManifestTest::test_inspect() {
auto path = QFINDTESTDATA("testdata/inspect_win/");
auto manifest = Package::fromInspectedFolder(path);
QVERIFY(manifest.valid == true);
QVERIFY(manifest.files.size() == 2);
QVERIFY(manifest.files.count(Path("a/b.txt")));
auto &file1 = manifest.files[Path("a/b.txt")];
QVERIFY(file1.executable == false);
QVERIFY(file1.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709");
QVERIFY(file1.size == 0);
QVERIFY(manifest.files.count(Path("a/b/b.txt")));
auto &file2 = manifest.files[Path("a/b/b.txt")];
QVERIFY(file2.executable == false);
QVERIFY(file2.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709");
QVERIFY(file2.size == 0);
QVERIFY(manifest.folders.size() == 3);
QVERIFY(manifest.folders.count(Path(".")));
QVERIFY(manifest.folders.count(Path("a")));
QVERIFY(manifest.folders.count(Path("a/b")));
QVERIFY(manifest.symlinks.size() == 0);
}
#ifndef Q_OS_WIN32
void PackageManifestTest::test_inspect_symlinks() {
auto path = QFINDTESTDATA("testdata/inspect/");
auto manifest = Package::fromInspectedFolder(path);
QVERIFY(manifest.valid == true);
QVERIFY(manifest.files.size() == 1);
QVERIFY(manifest.files.count(Path("a/b.txt")));
auto &file = manifest.files[Path("a/b.txt")];
QVERIFY(file.executable == true);
QVERIFY(file.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709");
QVERIFY(file.size == 0);
QVERIFY(manifest.folders.size() == 3);
QVERIFY(manifest.folders.count(Path(".")));
QVERIFY(manifest.folders.count(Path("a")));
QVERIFY(manifest.folders.count(Path("a/b")));
QVERIFY(manifest.symlinks.size() == 1);
QVERIFY(manifest.symlinks.count(Path("a/b/b.txt")));
qDebug() << manifest.symlinks[Path("a/b/b.txt")];
QVERIFY(manifest.symlinks[Path("a/b/b.txt")] == Path("../b.txt"));
}
#endif
void PackageManifestTest::mkdir_deep() {
Package from;
auto to = Package::fromManifestContents(R"END(
{
"files": {
"a/b/c/d/e": {
"type": "directory"
}
}
}
)END");
auto operations = UpdateOperations::resolve(from, to);
QVERIFY(operations.deletes.size() == 0);
QVERIFY(operations.rmdirs.size() == 0);
QVERIFY(operations.mkdirs.size() == 6);
QVERIFY(operations.mkdirs[0] == Path("."));
QVERIFY(operations.mkdirs[1] == Path("a"));
QVERIFY(operations.mkdirs[2] == Path("a/b"));
QVERIFY(operations.mkdirs[3] == Path("a/b/c"));
QVERIFY(operations.mkdirs[4] == Path("a/b/c/d"));
QVERIFY(operations.mkdirs[5] == Path("a/b/c/d/e"));
QVERIFY(operations.downloads.size() == 0);
QVERIFY(operations.mklinks.size() == 0);
QVERIFY(operations.executable_fixes.size() == 0);
}
void PackageManifestTest::rmdir_deep() {
Package to;
auto from = Package::fromManifestContents(R"END(
{
"files": {
"a/b/c/d/e": {
"type": "directory"
}
}
}
)END");
auto operations = UpdateOperations::resolve(from, to);
QVERIFY(operations.deletes.size() == 0);
QVERIFY(operations.rmdirs.size() == 6);
QVERIFY(operations.rmdirs[0] == Path("a/b/c/d/e"));
QVERIFY(operations.rmdirs[1] == Path("a/b/c/d"));
QVERIFY(operations.rmdirs[2] == Path("a/b/c"));
QVERIFY(operations.rmdirs[3] == Path("a/b"));
QVERIFY(operations.rmdirs[4] == Path("a"));
QVERIFY(operations.rmdirs[5] == Path("."));
QVERIFY(operations.mkdirs.size() == 0);
QVERIFY(operations.downloads.size() == 0);
QVERIFY(operations.mklinks.size() == 0);
QVERIFY(operations.executable_fixes.size() == 0);
}
void PackageManifestTest::identical_file() {
QByteArray manifest = R"END(
{
"files": {
"a/b/c/d/empty.txt": {
"type": "file",
"downloads": {
"raw": {
"url": "http://dethware.org/empty.txt",
"sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
"size": 0
}
},
"executable": false
}
}
}
)END";
auto from = Package::fromManifestContents(manifest);
auto to = Package::fromManifestContents(manifest);
auto operations = UpdateOperations::resolve(from, to);
QVERIFY(operations.deletes.size() == 0);
QVERIFY(operations.rmdirs.size() == 0);
QVERIFY(operations.mkdirs.size() == 0);
QVERIFY(operations.downloads.size() == 0);
QVERIFY(operations.mklinks.size() == 0);
QVERIFY(operations.executable_fixes.size() == 0);
}
void PackageManifestTest::changed_file() {
auto from = Package::fromManifestContents(R"END(
{
"files": {
"a/b/c/d/file": {
"type": "file",
"downloads": {
"raw": {
"url": "http://dethware.org/empty.txt",
"sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
"size": 0
}
},
"executable": false
}
}
}
)END");
auto to = Package::fromManifestContents(R"END(
{
"files": {
"a/b/c/d/file": {
"type": "file",
"downloads": {
"raw": {
"url": "http://dethware.org/space.txt",
"sha1": "dd122581c8cd44d0227f9c305581ffcb4b6f1b46",
"size": 1
}
},
"executable": false
}
}
}
)END");
auto operations = UpdateOperations::resolve(from, to);
QVERIFY(operations.deletes.size() == 1);
QCOMPARE(operations.deletes[0], Path("a/b/c/d/file"));
QVERIFY(operations.rmdirs.size() == 0);
QVERIFY(operations.mkdirs.size() == 0);
QVERIFY(operations.downloads.size() == 1);
QVERIFY(operations.mklinks.size() == 0);
QVERIFY(operations.executable_fixes.size() == 0);
}
void PackageManifestTest::added_file() {
auto from = Package::fromManifestContents(R"END(
{
"files": {
"a/b/c/d": {
"type": "directory"
}
}
}
)END");
auto to = Package::fromManifestContents(R"END(
{
"files": {
"a/b/c/d/file": {
"type": "file",
"downloads": {
"raw": {
"url": "http://dethware.org/space.txt",
"sha1": "dd122581c8cd44d0227f9c305581ffcb4b6f1b46",
"size": 1
}
},
"executable": false
}
}
}
)END");
auto operations = UpdateOperations::resolve(from, to);
QVERIFY(operations.deletes.size() == 0);
QVERIFY(operations.rmdirs.size() == 0);
QVERIFY(operations.mkdirs.size() == 0);
QVERIFY(operations.downloads.size() == 1);
QVERIFY(operations.mklinks.size() == 0);
QVERIFY(operations.executable_fixes.size() == 0);
}
void PackageManifestTest::removed_file() {
auto from = Package::fromManifestContents(R"END(
{
"files": {
"a/b/c/d/file": {
"type": "file",
"downloads": {
"raw": {
"url": "http://dethware.org/space.txt",
"sha1": "dd122581c8cd44d0227f9c305581ffcb4b6f1b46",
"size": 1
}
},
"executable": false
}
}
}
)END");
auto to = Package::fromManifestContents(R"END(
{
"files": {
"a/b/c/d": {
"type": "directory"
}
}
}
)END");
auto operations = UpdateOperations::resolve(from, to);
QVERIFY(operations.deletes.size() == 1);
QCOMPARE(operations.deletes[0], Path("a/b/c/d/file"));
QVERIFY(operations.rmdirs.size() == 0);
QVERIFY(operations.mkdirs.size() == 0);
QVERIFY(operations.downloads.size() == 0);
QVERIFY(operations.mklinks.size() == 0);
QVERIFY(operations.executable_fixes.size() == 0);
}
QTEST_GUILESS_MAIN(PackageManifestTest)
#include "PackageManifest_test.moc"

File diff suppressed because one or more lines are too long

0
api/logic/mojang/testdata/inspect/a/b.txt vendored Executable file
View File

View File

@@ -0,0 +1 @@
../b.txt

View File

View File

View File

@@ -214,3 +214,5 @@ bool NetJob::addNetAction(NetActionPtr action)
}
return true;
}
NetJob::~NetJob() = default;

View File

@@ -34,7 +34,7 @@ public:
{
setObjectName(job_name);
}
virtual ~NetJob() {}
virtual ~NetJob();
bool addNetAction(NetActionPtr action);