diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a269c2..821f720 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,7 @@ include(ECMGenerateHeaders) include(ECMPoQmTools) include(KDEGitCommitHooks) include(KDEClangFormat) +include(ECMAddTests) ecm_setup_version(${PROJECT_VERSION} VARIABLE_PREFIX ASTRA @@ -46,7 +47,8 @@ find_package(Qt6 ${QT_MIN_VERSION} REQUIRED COMPONENTS Network QuickControls2 WebView - Concurrent) + Concurrent + Test) find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS Kirigami2 I18n Config CoreAddons) find_package(PkgConfig REQUIRED) find_package(QCoro6 REQUIRED COMPONENTS Core Network Qml) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index b698747..e907fef 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -1,6 +1,15 @@ # SPDX-FileCopyrightText: 2023 Joshua Goins # SPDX-License-Identifier: CC0-1.0 +add_library(astra_static STATIC) + +target_sources(astra_static PRIVATE + include/patchlist.h + src/patchlist.cpp) +target_include_directories(astra_static PUBLIC include) +target_link_libraries(astra_static PUBLIC + Qt6::Core) + add_executable(astra) qt_add_qml_module(astra @@ -75,6 +84,7 @@ qt_target_qml_sources(astra kconfig_add_kcfg_files(astra GENERATE_MOC config.kcfgc accountconfig.kcfgc profileconfig.kcfgc) target_include_directories(astra PRIVATE include ${CMAKE_BINARY_DIR}) target_link_libraries(astra PRIVATE + astra_static physis cotp QuaZip::QuaZip @@ -142,3 +152,7 @@ if (WIN32) COMMAND "${WINDEPLOYQT_ENV_SETUP}" && "${WINDEPLOYQT_EXECUTABLE}" \"$\" ) endif () + +if (BUILD_TESTING) + add_subdirectory(autotests) +endif() diff --git a/launcher/autotests/CMakeLists.txt b/launcher/autotests/CMakeLists.txt new file mode 100644 index 0000000..19a1fc3 --- /dev/null +++ b/launcher/autotests/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2023 Joshua Goins +# SPDX-License-Identifier: CC0-1.0 + +ecm_add_test(patchlisttest.cpp + TEST_NAME patchlisttest + LINK_LIBRARIES astra_static Qt::Test + NAME_PREFIX "astra-" +) \ No newline at end of file diff --git a/launcher/autotests/patchlisttest.cpp b/launcher/autotests/patchlisttest.cpp new file mode 100644 index 0000000..7c2e7fb --- /dev/null +++ b/launcher/autotests/patchlisttest.cpp @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: 2023 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "patchlist.h" + +class PatchListTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testBootPatchList() + { + const QString testCase{ + QStringLiteral("--477D80B1_38BC_41d4_8B48_5273ADB89CAC\r\nContent-Type: application/octet-stream\r\nContent-Location: " + "ffxivpatch/2b5cbc63/metainfo/D2023.04.28.0000.0001.http\r\nX-Patch-Length: " + "22221335\r\n\r\n22221335\t69674819\t19\t18\t2023.09.14.0000.0001\thttp://patch-dl.ffxiv.com/boot/2b5cbc63/" + "D2023.09.14.0000.0001.patch\r\n--477D80B1_38BC_41d4_8B48_5273ADB89CAC--\r\n")}; + + const PatchList patchList{testCase}; + + QCOMPARE(patchList.patches().size(), 1); + QCOMPARE(patchList.patches()[0].version, QStringLiteral("2023.09.14.0000.0001")); + QCOMPARE(patchList.patches()[0].url, QStringLiteral("http://patch-dl.ffxiv.com/boot/2b5cbc63/D2023.09.14.0000.0001.patch")); + } + + void testGamePatchList() + { + const QString testCase{QStringLiteral( + "--477D80B1_38BC_41d4_8B48_5273ADB89CAC\r\nContent-Type: application/octet-stream\r\nContent-Location: " + "ffxivpatch/4e9a232b/metainfo/2023.07.26.0000.0000.http\r\nX-Patch-Length: " + "1664916486\r\n\r\n1479062470\t44145529682\t71\t11\t2023.09.15.0000.0000\tsha1\t50000000\t1c66becde2a8cf26a99d0fc7c06f15f8bab2d87c," + "950725418366c965d824228bf20f0496f81e0b9a,cabef48f7bf00fbf18b72843bdae2f61582ad264,53608de567b52f5fdb43fdb8b623156317e26704," + "f0bc06cabf9ff6490f36114b25f62619d594dbe8,3c5e4b962cd8445bd9ee29011ecdb331d108abd8,88e1a2a322f09de3dc28173d4130a2829950d4e0," + "1040667917dc99b9215dfccff0e458c2e8a724a8,149c7e20e9e3e376377a130e0526b35fd7f43df2,1bb4e33807355cdf46af93ce828b6e145a9a8795," + "a79daff43db488f087da8e22bb4c21fd3a390f3c,6b04fadb656d467fb8318eba1c7f5ee8f030d967,a6641e1c894db961a49b70fda2b0d6d87be487a7," + "edf419de49f42ef19bd6814f8184b35a25e9e977,c1525c4df6001b66b575e2891db0284dc3a16566,01b7628095b07fa3c9c1aed2d66d32d118020321," + "991b137ea0ebb11bd668f82149bc2392a4cbcf52,ad3f74d4fca143a6cf507fc859544a4bcd501d85,936a0f1711e273519cae6b2da0d8b435fe6aa020," + "023f19d8d8b3ecaaf865e3170e8243dd437a384c,2d9e934de152956961a849e81912ca8d848265ca,8e32f9aa76c95c60a9dbe0967aee5792b812d5ec," + "dee052b9aa1cc8863efd61afc63ac3c2d56f9acc,fa81225aea53fa13a9bae1e8e02dea07de6d7052,59b24693b1b62ea1660bc6f96a61f7d41b3f7878," + "349b691db1853f6c0120a8e66093c763ba6e3671,4561eb6f954d80cdb1ece3cc4d58cbd864bf2b50,de94175c4db39a11d5334aefc7a99434eea8e4f9," + "55dd7215f24441d6e47d1f9b32cebdb041f2157f,2ca09db645cfeefa41a04251dfcb13587418347a\thttp://patch-dl.ffxiv.com/game/4e9a232b/" + "D2023.09.15.0000.0000.patch\r\n61259063\t44145955874\t71\t11\t2023.09.21.0000.0001\tsha1\t50000000\t88c9bbfe2af4eea7b56384baeeafd59afb47ddeb," + "095c26e87b4d25505845515c389dd22dd429ea7e\thttp://patch-dl.ffxiv.com/game/4e9a232b/" + "D2023.09.21.0000.0001.patch\r\n63776300\t44146911186\t71\t11\t2023.09.23.0000.0001\tsha1\t50000000\tc8fc6910be12d10b39e4e6ae980d4c219cfe56a1," + "5c0199b7147a47f620a2b50654a87c9b0cbcf43b\thttp://patch-dl.ffxiv.com/game/4e9a232b/" + "D2023.09.23.0000.0001.patch\r\n32384649\t44146977234\t71\t11\t2023.09.26.0000." + "0000\tsha1\t50000000\t519a5e46edb67ba6edb9871df5eb3991276da254\thttp://patch-dl.ffxiv.com/game/4e9a232b/" + "D2023.09.26.0000.0000.patch\r\n28434004\t44147154898\t71\t11\t2023.09.28.0000." + "0000\tsha1\t50000000\ta08e8a071b615b0babc45a09979ab6bc70affe14\thttp://patch-dl.ffxiv.com/game/4e9a232b/" + "D2023.09.28.0000.0000.patch\r\n82378953\t5854598228\t30\t4\t2023.07.26.0000.0001\tsha1\t50000000\t07d9fecb3975028fdf81166aa5a4cca48bc5a4b0," + "c10677985f809df93a739ed7b244d90d37456353\thttp://patch-dl.ffxiv.com/game/ex1/6b936f08/" + "D2023.07.26.0000.0001.patch\r\n29384945\t5855426508\t30\t4\t2023.09.23.0000.0001\tsha1\t50000000\tcf4970957e846e6cacfdad521252de18afc7e29b\thttp:/" + "/patch-dl.ffxiv.com/game/ex1/6b936f08/" + "D2023.09.23.0000.0001.patch\r\n136864\t5855426508\t30\t4\t2023.09.26.0000.0000\tsha1\t50000000\t3ca5e0160e1cedb7a2801048408b247095f432ea\thttp://" + "patch-dl.ffxiv.com/game/ex1/6b936f08/" + "D2023.09.26.0000.0000.patch\r\n126288\t5855426508\t30\t4\t2023.09.28.0000.0000\tsha1\t50000000\t88d201defb32366004c88b236d03278f95d9b442\thttp://" + "patch-dl.ffxiv.com/game/ex1/6b936f08/" + "D2023.09.28.0000.0000.patch\r\n49352444\t7620831756\t24\t4\t2023.09.23.0000.0001\tsha1\t50000000\t2a05a452281d119241f222f4eae43266a22560fe\thttp:/" + "/patch-dl.ffxiv.com/game/ex2/f29a3eb2/" + "D2023.09.23.0000.0001.patch\r\n301600\t7620831756\t24\t4\t2023.09.26.0000.0000\tsha1\t50000000\t215de572fe51bca45f83e19d719f52220818bc39\thttp://" + "patch-dl.ffxiv.com/game/ex2/f29a3eb2/" + "D2023.09.26.0000.0000.patch\r\n385096\t7620831756\t24\t4\t2023.09.28.0000.0000\tsha1\t50000000\t427c3fd61f2ca79698ecd6dc34e3457f6d8c01cd\thttp://" + "patch-dl.ffxiv.com/game/ex2/f29a3eb2/" + "D2023.09.28.0000.0000.patch\r\n60799419\t9737248724\t26\t4\t2023.09.23.0000.0001\tsha1\t50000000\tab064df7aec0526e8306a78e51adbbba8b129c3f," + "4e1bb58a987b3157c16f59821bc67b1464a301e5\thttp://patch-dl.ffxiv.com/game/ex3/859d0e24/" + "D2023.09.23.0000.0001.patch\r\n555712\t9737248724\t26\t4\t2023.09.26.0000.0000\tsha1\t50000000\tdb6b1f34b0b58ca0d0621ff6cebcc63e7eb692c5\thttp://" + "patch-dl.ffxiv.com/game/ex3/859d0e24/" + "D2023.09.26.0000.0000.patch\r\n579560\t9737248724\t26\t4\t2023.09.28.0000.0000\tsha1\t50000000\t67b5d62ee8202fe045c763deea38c136c5324195\thttp://" + "patch-dl.ffxiv.com/game/ex3/859d0e24/" + "D2023.09.28.0000.0000.patch\r\n867821466\t12469514712\t40\t4\t2023.09.15.0000.0001\tsha1\t50000000\t08f6164685b4363d719d09a8d0ef0910b48ec4a1," + "05819ea5182885b59f0dfb981ecab159f46d7343,6ab6106ce4153cac5edb26ad0aabc6ba718ee500,3c552f54cc3c101d9f1786156c1cbd9880a7c02f," + "e0d425f6032da1ceb60ff9ca14a10e5e89f23218,245402087bf6d535cb08bbc189647e8f56301722,a3a0630bb4ddd36b532be0e0a8982dbce1bb9053," + "eca8a1394db1e84b9ec11a99bd970e6335326da5,40546e6d37cc5ea21d26c2e00a11f46a13d99a77,f41f312ad72ee65dc97645c8f7426c35970187ca," + "e5a9966528ecab5a51059de3d5cd83a921c73a1c,c03855127d135d22c65e34e615eddbe6f38769e9,befab30a77c14743f53b20910b564bb6a97dfe86," + "dcce8ea707f03606b583d51d947a4cf99b52635e,c4a33a8c51a047706b65887bed804ec6c2c29016,a17bc8bd8709c2a0c5725c134101132d3536e67d," + "d2b277de55a65697d80cfe8ee46199a8d7482c30,6b97cc2862c6f8f5d279be5f28cc13ed011763e5\thttp://patch-dl.ffxiv.com/game/ex4/1bf99b87/" + "D2023.09.15.0000.0001.patch\r\n17717567\t12471821656\t40\t4\t2023.09.23.0000.0001\tsha1\t50000000\tda144d5c1c173ef1d98e4e7b558414ae53bcd392\thttp:" + "//patch-dl.ffxiv.com/game/ex4/1bf99b87/" + "D2023.09.23.0000.0001.patch\r\n3117253\t12471859944\t40\t4\t2023.09.26.0000.0000\tsha1\t50000000\t7acdab61e99d69ffa53e9136f65a1a1d3b33732b\thttp:/" + "/patch-dl.ffxiv.com/game/ex4/1bf99b87/" + "D2023.09.26.0000.0000.patch\r\n4676853\t12471859944\t40\t4\t2023.09.28.0000.0000\tsha1\t50000000\tce26ccb2115af612ccd4c42c1a27ef2ec925c81e\thttp:/" + "/patch-dl.ffxiv.com/game/ex4/1bf99b87/D2023.09.28.0000.0000.patch\r\n--477D80B1_38BC_41d4_8B48_5273ADB89CAC--\r\n")}; + + const PatchList patchList{testCase}; + QCOMPARE(patchList.patches().size(), 19); + QCOMPARE(patchList.patches()[5].version, QStringLiteral("2023.07.26.0000.0001")); + QCOMPARE(patchList.patches()[5].url, QStringLiteral("http://patch-dl.ffxiv.com/game/ex1/6b936f08/D2023.07.26.0000.0001.patch")); + } +}; + +QTEST_MAIN(PatchListTest) +#include "patchlisttest.moc" diff --git a/launcher/include/patcher.h b/launcher/include/patcher.h index 8002159..22a5caa 100644 --- a/launcher/include/patcher.h +++ b/launcher/include/patcher.h @@ -3,6 +3,7 @@ #pragma once +#include "patchlist.h" #include #include #include @@ -21,7 +22,7 @@ public: Patcher(LauncherCore &launcher, const QString &baseDirectory, GameData &gameData, QObject *parent = nullptr); Patcher(LauncherCore &launcher, const QString &baseDirectory, BootData &bootData, QObject *parent = nullptr); - QCoro::Task patch(const QString &patchList); + QCoro::Task patch(const PatchList &patchList); private: void setupDirectories(); diff --git a/launcher/include/patchlist.h b/launcher/include/patchlist.h new file mode 100644 index 0000000..a5c5f77 --- /dev/null +++ b/launcher/include/patchlist.h @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2023 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +class Patch +{ +public: + QString name, url, repository, version; + QVector hashes; + long hashBlockSize = 0; + long length = 0; +}; + +class PatchList +{ +public: + explicit PatchList(const QString &patchList); + + [[nodiscard]] QVector patches() const; + + [[nodiscard]] bool isEmpty() const; + +private: + QVector m_patches; +}; \ No newline at end of file diff --git a/launcher/src/patcher.cpp b/launcher/src/patcher.cpp index e2faa1d..9f30a02 100644 --- a/launcher/src/patcher.cpp +++ b/launcher/src/patcher.cpp @@ -14,6 +14,7 @@ #include #include "launchercore.h" +#include "patchlist.h" Patcher::Patcher(LauncherCore &launcher, const QString &baseDirectory, BootData &bootData, QObject *parent) : QObject(parent) @@ -37,7 +38,7 @@ Patcher::Patcher(LauncherCore &launcher, const QString &baseDirectory, GameData Q_EMIT m_launcher.stageChanged(i18n("Checking %1 version.", getBaseString())); } -QCoro::Task Patcher::patch(const QString &patchList) +QCoro::Task Patcher::patch(const PatchList &patchList) { if (patchList.isEmpty()) { co_return false; @@ -46,48 +47,42 @@ QCoro::Task Patcher::patch(const QString &patchList) Q_EMIT m_launcher.stageIndeterminate(); Q_EMIT m_launcher.stageChanged(i18n("Checking %1 version.", getBaseString())); - const QStringList parts = patchList.split("\r\n"); - - m_remainingPatches = parts.size() - 7; + m_remainingPatches = patchList.patches().size(); m_patchQueue.resize(m_remainingPatches); QFutureSynchronizer synchronizer; int patchIndex = 0; - for (int i = 5; i < parts.size() - 2; i++) { - const QStringList patchParts = parts[i].split(QLatin1Char('\t')); - - const int length = patchParts[0].toInt(); + for (auto &patch : patchList.patches()) { const int ourIndex = patchIndex++; - const QString &version = patchParts[4]; - const long hashBlockSize = patchParts.size() == 9 ? patchParts[6].toLong() : 0; - - const QString &name = version; - const QStringList hashes = patchParts.size() == 9 ? (patchParts[7].split(QLatin1Char(','))) : QStringList(); - const QString &url = patchParts[patchParts.size() == 9 ? 8 : 5]; - const QString filename = QStringLiteral("%1.patch").arg(name); - - auto url_parts = url.split(QLatin1Char('/')); - const QString repository = url_parts[url_parts.size() - 3]; - - const QDir repositoryDir = m_patchesDir.absoluteFilePath(repository); + const QString filename = QStringLiteral("%1.patch").arg(patch.name); + const QDir repositoryDir = m_patchesDir.absoluteFilePath(patch.repository); if (!QDir().exists(repositoryDir.absolutePath())) QDir().mkpath(repositoryDir.absolutePath()); const QString patchPath = repositoryDir.absoluteFilePath(filename); - const QueuedPatch patch{name, repository, version, patchPath, hashes, hashBlockSize, length, isBoot()}; + const QueuedPatch queuedPatch{patch.name, patch.repository, patch.version, patchPath, patch.hashes, patch.hashBlockSize, patch.length, isBoot()}; - m_patchQueue[ourIndex] = patch; + qDebug() << "Adding a queued patch:"; + qDebug() << "- Name:" << patch.name; + qDebug() << "- Repository or is boot:" << (isBoot() ? QStringLiteral("boot") : patch.repository); + qDebug() << "- Version:" << patch.version; + qDebug() << "- Downloaded Path:" << patchPath; + qDebug() << "- Hashes:" << patch.hashes; + qDebug() << "- Hash Block Size:" << patch.hashBlockSize; + qDebug() << "- Length:" << patch.length; + + m_patchQueue[ourIndex] = queuedPatch; if (!QFile::exists(patchPath)) { - auto patchReply = m_launcher.mgr->get(QNetworkRequest(url)); + auto patchReply = m_launcher.mgr->get(QNetworkRequest(patch.url)); - connect(patchReply, &QNetworkReply::downloadProgress, this, [this, patch](int received, int total) { - Q_EMIT m_launcher.stageChanged(i18n("Updating %1.\nDownloading %2", getBaseString(), patch.getVersion())); + connect(patchReply, &QNetworkReply::downloadProgress, this, [this, queuedPatch](int received, int total) { + Q_EMIT m_launcher.stageChanged(i18n("Updating %1.\nDownloading %2", getBaseString(), queuedPatch.getVersion())); Q_EMIT m_launcher.stageDeterminate(0, total, received); }); @@ -98,7 +93,7 @@ QCoro::Task Patcher::patch(const QString &patchList) file.close(); })); } else { - qDebug() << "Found existing patch: " << name; + qDebug() << "Found existing patch: " << patch.name; } } @@ -152,6 +147,8 @@ void Patcher::processPatch(const QueuedPatch &patch) physis_gamedata_apply_patch(m_gameData, patch.path.toStdString().c_str()); } + qDebug() << "Installed" << patch.path << "to" << (isBoot() ? QStringLiteral("boot") : patch.repository); + QString verFilePath; if (isBoot()) { verFilePath = m_baseDirectory + QStringLiteral("/ffxivboot.ver"); diff --git a/launcher/src/patchlist.cpp b/launcher/src/patchlist.cpp new file mode 100644 index 0000000..0be7b45 --- /dev/null +++ b/launcher/src/patchlist.cpp @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "patchlist.h" + +#include + +PatchList::PatchList(const QString &patchList) +{ + const QStringList parts = patchList.split("\r\n"); + + for (int i = 5; i < parts.size() - 2; i++) { + const QStringList patchParts = parts[i].split(QLatin1Char('\t')); + + const int length = patchParts[0].toInt(); + + const QString &version = patchParts[4]; + const long hashBlockSize = patchParts.size() == 9 ? patchParts[6].toLong() : 0; + + const QString &name = version; + const QStringList hashes = patchParts.size() == 9 ? (patchParts[7].split(QLatin1Char(','))) : QStringList(); + const QString &url = patchParts[patchParts.size() == 9 ? 8 : 5]; + + auto url_parts = url.split(QLatin1Char('/')); + const QString repository = url_parts[url_parts.size() - 3]; + + m_patches.push_back( + {.name = name, .url = url, .repository = repository, .version = version, .hashes = hashes, .hashBlockSize = hashBlockSize, .length = length}); + } + + Q_ASSERT_X(m_patches.isEmpty() ? patchList.isEmpty() : true, + "PatchList", + "Patch parsing failed, we were given an non-empty patchlist body but nothing were parsed."); + + qDebug() << "Finished parsing patch list. Num patches:" << m_patches.size(); +} + +QVector PatchList::patches() const +{ + return m_patches; +} + +bool PatchList::isEmpty() const +{ + return m_patches.isEmpty(); +} diff --git a/launcher/src/squareboot.cpp b/launcher/src/squareboot.cpp index e4ca018..a8b9fc5 100644 --- a/launcher/src/squareboot.cpp +++ b/launcher/src/squareboot.cpp @@ -50,7 +50,7 @@ QCoro::Task<> SquareBoot::bootCheck(const LoginInformation &info) const QString patchList = reply->readAll(); if (!patchList.isEmpty()) { patcher = new Patcher(window, info.profile->gamePath() + QStringLiteral("/boot"), *info.profile->bootData(), this); - const bool hasPatched = co_await patcher->patch(reply->readAll()); + const bool hasPatched = co_await patcher->patch(PatchList(patchList)); if (hasPatched) { // update game version information info.profile->readGameVersion(); diff --git a/launcher/src/squarelauncher.cpp b/launcher/src/squarelauncher.cpp index 1b319e5..bf6bfc1 100644 --- a/launcher/src/squarelauncher.cpp +++ b/launcher/src/squarelauncher.cpp @@ -191,7 +191,7 @@ QCoro::Task<> SquareLauncher::registerSession(const LoginInformation &info) if (!body.isEmpty()) { patcher = new Patcher(window, info.profile->gamePath() + QStringLiteral("/game"), *info.profile->gameData(), this); - const bool hasPatched = co_await patcher->patch(body); + const bool hasPatched = co_await patcher->patch(PatchList(body)); if (hasPatched) { // re-read game version if it has updated info.profile->readGameVersion();