1 parent
ebf8ff7869
commit
41599fa82f
67 files changed
+8775
-4
No files matched your search
@@ -0,0 +1,236 @@
|
||||
#include "testenv.h"
|
||||
#include "fixtures.h"
|
||||
|
||||
#include "buildarchiveworker.h"
|
||||
#include "localization.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QSignalSpy>
|
||||
|
||||
class TestBuildArchiveWorker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QString m_game;
|
||||
QString m_work;
|
||||
|
||||
void populateGame()
|
||||
{
|
||||
Fixtures::write(m_game + "/options.txt", "fov:90");
|
||||
Fixtures::write(m_game + "/mods/a.jar", "mod-a");
|
||||
Fixtures::write(m_game + "/mods/sub/b.jar", "mod-b");
|
||||
Fixtures::write(m_game + "/config/x/y.toml", "y=1");
|
||||
Fixtures::write(m_game + "/saves/World/level.dat", "level"); // общее — исключается
|
||||
Fixtures::write(m_game + "/versions/1.20.1/1.20.1.json", "{}"); // общее — исключается
|
||||
}
|
||||
|
||||
// Последний вызов finished(): (ok, error)
|
||||
static QPair<bool, QString> result(const QSignalSpy &spy)
|
||||
{
|
||||
if (spy.isEmpty())
|
||||
return {false, QStringLiteral("finished не пришёл")};
|
||||
return {spy.constLast().at(0).toBool(), spy.constLast().at(1).toString()};
|
||||
}
|
||||
|
||||
private slots:
|
||||
void init()
|
||||
{
|
||||
m_game = TestEnv::freshDir(QStringLiteral("game"));
|
||||
m_work = TestEnv::freshDir(QStringLiteral("archives"));
|
||||
}
|
||||
|
||||
void archiveClearRestoreRoundTrip()
|
||||
{
|
||||
populateGame();
|
||||
const QStringList shared{"saves", "versions"};
|
||||
const QString zip = m_work + "/build.zip";
|
||||
|
||||
BuildArchiveWorker worker;
|
||||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||||
QSignalSpy progress(&worker, &BuildArchiveWorker::progress);
|
||||
|
||||
worker.archive(m_game, zip, shared);
|
||||
QCOMPARE(result(finished), qMakePair(true, QString()));
|
||||
QCOMPARE(Fixtures::zipEntries(zip),
|
||||
QStringList({"config/x/y.toml", "mods/a.jar", "mods/sub/b.jar", "options.txt"}));
|
||||
// progress: (0,total) в начале и (total,total) в конце.
|
||||
QCOMPARE(progress.constFirst().at(0).toInt(), 0);
|
||||
QCOMPARE(progress.constLast().at(0).toInt(), 4);
|
||||
QCOMPARE(progress.constLast().at(1).toInt(), 4);
|
||||
|
||||
worker.clear(m_game, shared);
|
||||
QCOMPARE(result(finished), qMakePair(true, QString()));
|
||||
QCOMPARE(Fixtures::listTree(m_game),
|
||||
QStringList({"saves/World/level.dat", "versions/1.20.1/1.20.1.json"}));
|
||||
|
||||
worker.restore(zip, m_game);
|
||||
QCOMPARE(result(finished), qMakePair(true, QString()));
|
||||
QCOMPARE(Fixtures::listTree(m_game),
|
||||
QStringList({"config/x/y.toml", "mods/a.jar", "mods/sub/b.jar",
|
||||
"options.txt", "saves/World/level.dat", "versions/1.20.1/1.20.1.json"}));
|
||||
QCOMPARE(Fixtures::readFile(m_game + "/mods/sub/b.jar"), QByteArray("mod-b"));
|
||||
}
|
||||
|
||||
void dotFilesSurviveRoundTrip()
|
||||
{
|
||||
// В .minecraft живут .fabric/, .mixin.out/, на macOS ещё .DS_Store.
|
||||
Fixtures::write(m_game + "/.fabric/remappedJars/x.jar", "cache");
|
||||
Fixtures::write(m_game + "/mods/a.jar", "a");
|
||||
const QString zip = m_work + "/dots.zip";
|
||||
|
||||
BuildArchiveWorker worker;
|
||||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||||
worker.archive(m_game, zip, {});
|
||||
QVERIFY(result(finished).first);
|
||||
worker.clear(m_game, {});
|
||||
worker.restore(zip, m_game);
|
||||
QVERIFY(result(finished).first);
|
||||
QVERIFY(QFile::exists(m_game + "/mods/a.jar"));
|
||||
|
||||
// QZipReader при чтении срезает ведущие «.» и «/» в именах записей, поэтому
|
||||
// после переключения сборок .fabric/ возвращается как fabric/.
|
||||
QEXPECT_FAIL("", "QZipReader срезает ведущую точку: .fabric/ восстанавливается как fabric/", Continue);
|
||||
QVERIFY(QFile::exists(m_game + "/.fabric/remappedJars/x.jar"));
|
||||
}
|
||||
|
||||
void archiveMissingGameDir()
|
||||
{
|
||||
BuildArchiveWorker worker;
|
||||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||||
const QString missing = m_game + "/nope";
|
||||
worker.archive(missing, m_work + "/x.zip", {});
|
||||
QCOMPARE(result(finished),
|
||||
qMakePair(false, Loc::text("switch.error.gameDirMissing").arg(QDir::toNativeSeparators(missing))));
|
||||
}
|
||||
|
||||
void archiveCancelRemovesTempZip()
|
||||
{
|
||||
populateGame();
|
||||
BuildArchiveWorker worker;
|
||||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||||
worker.requestCancel();
|
||||
worker.archive(m_game, m_work + "/cancel.zip", {});
|
||||
QCOMPARE(result(finished), qMakePair(false, Loc::text("switch.status.archivingCancelled")));
|
||||
QVERIFY(!QFile::exists(m_work + "/cancel.zip"));
|
||||
|
||||
worker.clearCancel();
|
||||
worker.archive(m_game, m_work + "/ok.zip", {});
|
||||
QVERIFY(result(finished).first);
|
||||
}
|
||||
|
||||
void clearMissingDirIsOk()
|
||||
{
|
||||
BuildArchiveWorker worker;
|
||||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||||
worker.clear(m_game + "/nope", {});
|
||||
QVERIFY(result(finished).first);
|
||||
}
|
||||
|
||||
void restoreRejectsUnreadableAndUnsafeArchives()
|
||||
{
|
||||
BuildArchiveWorker worker;
|
||||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||||
|
||||
const QString missing = m_work + "/missing.zip";
|
||||
worker.restore(missing, m_game);
|
||||
QCOMPARE(result(finished),
|
||||
qMakePair(false, Loc::text("switch.error.archiveOpenFailed").arg(QDir::toNativeSeparators(missing))));
|
||||
|
||||
// Zip-slip: запись с «..» не должна вылезти из папки игры.
|
||||
const QString evil = m_work + "/evil.zip";
|
||||
// Ведущие «../» QZipReader срезает сам, а «..» в середине пути — нет.
|
||||
QVERIFY(Fixtures::makeZip(evil, {{"ok.txt", "ok"}, {"mods/../../escaped.txt", "pwned"}}));
|
||||
QVERIFY(Fixtures::zipEntries(evil).contains("mods/../../escaped.txt"));
|
||||
worker.restore(evil, m_game);
|
||||
QCOMPARE(result(finished),
|
||||
qMakePair(false, Loc::text("switch.error.archiveBadPath").arg("mods/../../escaped.txt")));
|
||||
QVERIFY(!QFile::exists(QFileInfo(m_game).absolutePath() + "/escaped.txt"));
|
||||
}
|
||||
|
||||
void restoreCancel()
|
||||
{
|
||||
const QString zip = m_work + "/r.zip";
|
||||
QVERIFY(Fixtures::makeZip(zip, {{"a.txt", "a"}}));
|
||||
BuildArchiveWorker worker;
|
||||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||||
worker.requestCancel();
|
||||
worker.restore(zip, m_game);
|
||||
QCOMPARE(result(finished), qMakePair(false, Loc::text("switch.status.extractCancelled")));
|
||||
}
|
||||
|
||||
void applyPackRemovesThenWrites()
|
||||
{
|
||||
populateGame();
|
||||
Fixtures::write(m_game + "/mods/old/legacy.jar", "old");
|
||||
Fixtures::write(m_game + "/mods/keep.jar", "keep");
|
||||
|
||||
const QString pack = m_work + "/pack.zip";
|
||||
QVERIFY(Fixtures::makeZip(pack, {{"mods/new.jar", "new"},
|
||||
{"config/x/y.toml", "y=2"},
|
||||
{"options.txt", "fov:70"}}));
|
||||
|
||||
BuildArchiveWorker worker;
|
||||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||||
QSignalSpy entries(&worker, &BuildArchiveWorker::packEntries);
|
||||
worker.applyPack(pack, m_game,
|
||||
{"mods/old/legacy.jar", "mods/missing.jar",
|
||||
"../outside.txt", "versions/1.20.1/1.20.1.json"},
|
||||
{"versions", "saves"});
|
||||
QCOMPARE(result(finished), qMakePair(true, QString()));
|
||||
|
||||
QVERIFY(!QFile::exists(m_game + "/mods/old/legacy.jar"));
|
||||
QVERIFY(!QDir(m_game + "/mods/old").exists()); // опустевшая папка убрана
|
||||
QVERIFY(QFile::exists(m_game + "/mods/keep.jar"));
|
||||
// Удаление в запретной зоне молча пропускается.
|
||||
QVERIFY(QFile::exists(m_game + "/versions/1.20.1/1.20.1.json"));
|
||||
QCOMPARE(Fixtures::readFile(m_game + "/mods/new.jar"), QByteArray("new"));
|
||||
QCOMPARE(Fixtures::readFile(m_game + "/config/x/y.toml"), QByteArray("y=2"));
|
||||
QCOMPARE(Fixtures::readFile(m_game + "/options.txt"), QByteArray("fov:70"));
|
||||
|
||||
QCOMPARE(entries.count(), 1);
|
||||
QStringList applied = entries.constFirst().constFirst().toStringList();
|
||||
applied.sort();
|
||||
QCOMPARE(applied, QStringList({"config/x/y.toml", "mods/new.jar", "options.txt"}));
|
||||
}
|
||||
|
||||
void applyPackRefusesLauncherFolders()
|
||||
{
|
||||
populateGame();
|
||||
const QString pack = m_work + "/bad.zip";
|
||||
QVERIFY(Fixtures::makeZip(pack, {{"mods/new.jar", "new"}, {"versions/hack/hack.json", "{}"}}));
|
||||
|
||||
BuildArchiveWorker worker;
|
||||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||||
worker.applyPack(pack, m_game, {"mods/a.jar"}, {"versions"});
|
||||
QCOMPARE(result(finished),
|
||||
qMakePair(false, Loc::text("switch.error.packTouchesLauncher").arg("versions/hack/hack.json")));
|
||||
// Проверка до любых изменений: ничего не удалено и не записано.
|
||||
QVERIFY(QFile::exists(m_game + "/mods/a.jar"));
|
||||
QVERIFY(!QFile::exists(m_game + "/mods/new.jar"));
|
||||
}
|
||||
|
||||
void applyPackRefusesUnsafePaths()
|
||||
{
|
||||
const QString pack = m_work + "/slip.zip";
|
||||
QVERIFY(Fixtures::makeZip(pack, {{"mods/../../../etc/x", "x"}}));
|
||||
BuildArchiveWorker worker;
|
||||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||||
worker.applyPack(pack, m_game, {}, {});
|
||||
QCOMPARE(result(finished), qMakePair(false, Loc::text("switch.error.packBadPath").arg("mods/../../../etc/x")));
|
||||
}
|
||||
|
||||
void applyPackCancel()
|
||||
{
|
||||
const QString pack = m_work + "/c.zip";
|
||||
QVERIFY(Fixtures::makeZip(pack, {{"a.txt", "a"}}));
|
||||
BuildArchiveWorker worker;
|
||||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||||
worker.requestCancel();
|
||||
worker.applyPack(pack, m_game, {}, {});
|
||||
QCOMPARE(result(finished), qMakePair(false, Loc::text("switch.status.installCancelled")));
|
||||
QVERIFY(!QFile::exists(m_game + "/a.txt"));
|
||||
}
|
||||
};
|
||||
|
||||
BILAUNCHER_TEST_MAIN(TestBuildArchiveWorker)
|
||||
#include "tst_buildarchiveworker.moc"
|
||||
Reference in new issue
Block a user