From 0946c7c138d73a6e11835dc95da764ff1c0db5ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sun, 11 Oct 2020 23:20:35 +0200 Subject: [PATCH] NOISSUE basic code for downloading JREs from Mojang Not integrated yet, but the logic has tests and shouldn't be too shaky. Integration comes next. --- api/logic/CMakeLists.txt | 19 + api/logic/mojang/PackageManifest.cpp | 366 +++++++++++++++++++ api/logic/mojang/PackageManifest.h | 169 +++++++++ api/logic/mojang/PackageManifest_test.cpp | 333 +++++++++++++++++ api/logic/mojang/testdata/1.8.0_202-x64.json | Bin 0 -> 125581 bytes api/logic/mojang/testdata/inspect/a/b.txt | Bin api/logic/mojang/testdata/inspect/a/b/b.txt | 1 + 7 files changed, 888 insertions(+) create mode 100644 api/logic/mojang/PackageManifest.cpp create mode 100644 api/logic/mojang/PackageManifest.h create mode 100644 api/logic/mojang/PackageManifest_test.cpp create mode 100644 api/logic/mojang/testdata/1.8.0_202-x64.json create mode 100755 api/logic/mojang/testdata/inspect/a/b.txt create mode 120000 api/logic/mojang/testdata/inspect/a/b/b.txt diff --git a/api/logic/CMakeLists.txt b/api/logic/CMakeLists.txt index 740cd886..6e9aec08 100644 --- a/api/logic/CMakeLists.txt +++ b/api/logic/CMakeLists.txt @@ -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 diff --git a/api/logic/mojang/PackageManifest.cpp b/api/logic/mojang/PackageManifest.cpp new file mode 100644 index 00000000..42a66442 --- /dev/null +++ b/api/logic/mojang/PackageManifest.cpp @@ -0,0 +1,366 @@ +#include "PackageManifest.h" +#include +#include +#include +#include +#include + +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 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, "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; + } +} + +// 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()); + if(fileInfo.isSymLink()) { + out.addLink(relPath, fileInfo.symLinkTarget()); + } + else 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 ¤t_hash = iter->second.hash; + const auto ¤t_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(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(to.sources.at(iter->second.hash), iter->second.executable) + } + ); + } + } + + // Folders + std::set remove_folders; + std::set 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 ¤t_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; +} + +} diff --git a/api/logic/mojang/PackageManifest.h b/api/logic/mojang/PackageManifest.h new file mode 100644 index 00000000..893d4c50 --- /dev/null +++ b/api/logic/mojang/PackageManifest.h @@ -0,0 +1,169 @@ +#pragma once + +#include +#include +#include +#include +#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) { + parts = QStringList(start, end); + } + 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 sources; + bool valid = true; + std::set folders; + std::map files; + std::map symlinks; +}; + +struct MULTIMC_LOGIC_EXPORT FileDownload : FileSource +{ + FileDownload(const FileSource& source, bool executable) { + static_cast (*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 deletes; + std::vector rmdirs; + std::vector mkdirs; + std::map downloads; + std::map mklinks; + std::map executable_fixes; +}; + +} diff --git a/api/logic/mojang/PackageManifest_test.cpp b/api/logic/mojang/PackageManifest_test.cpp new file mode 100644 index 00000000..08628973 --- /dev/null +++ b/api/logic/mojang/PackageManifest_test.cpp @@ -0,0 +1,333 @@ +#include +#include +#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(); + void test_diff(); + + 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); + QCOMPARE(manifest.valid, true); + QCOMPARE(manifest.files.size(), 1); + QCOMPARE(manifest.files.count(Path("a/b.txt")), 1); + auto &file = manifest.files[Path("a/b.txt")]; + QCOMPARE(file.executable, true); + QCOMPARE(file.hash, "da39a3ee5e6b4b0d3255bfef95601890afd80709"); + QCOMPARE(file.size, 0); + QCOMPARE(manifest.folders.size(), 4); + QCOMPARE(manifest.folders.count(Path(".")), 1); + QCOMPARE(manifest.folders.count(Path("a")), 1); + QCOMPARE(manifest.folders.count(Path("a/b")), 1); + QCOMPARE(manifest.folders.count(Path("a/b/c")), 1); + QCOMPARE(manifest.symlinks.size(), 1); + auto symlinkPath = Path("a/b/c.txt"); + QCOMPARE(manifest.symlinks.count(symlinkPath), 1); + auto &symlink = manifest.symlinks[symlinkPath]; + QCOMPARE(symlink, Path("../b.txt")); + QCOMPARE(manifest.sources.size(), 1); +} + +void PackageManifestTest::test_parse_file() { + auto path = QFINDTESTDATA("testdata/1.8.0_202-x64.json"); + auto manifest = Package::fromManifestFile(path); + QCOMPARE(manifest.valid, true); +} + +void PackageManifestTest::test_inspect() { + auto path = QFINDTESTDATA("testdata/inspect/"); + auto manifest = Package::fromInspectedFolder(path); + QCOMPARE(manifest.valid, true); + QCOMPARE(manifest.files.size(), 1); + QCOMPARE(manifest.files.count(Path("a/b.txt")), 1); + auto &file = manifest.files[Path("a/b.txt")]; + QCOMPARE(file.executable, true); + QCOMPARE(file.hash, "da39a3ee5e6b4b0d3255bfef95601890afd80709"); + QCOMPARE(file.size, 0); + QCOMPARE(manifest.folders.size(), 3); + QCOMPARE(manifest.folders.count(Path(".")), 1); + QCOMPARE(manifest.folders.count(Path("a")), 1); + QCOMPARE(manifest.folders.count(Path("a/b")), 1); + QCOMPARE(manifest.symlinks.size(), 1); +} + +void PackageManifestTest::test_diff() { + auto path = QFINDTESTDATA("testdata/inspect/"); + auto from = Package::fromInspectedFolder(path); + auto to = Package::fromManifestContents(basic_manifest); + auto operations = UpdateOperations::resolve(from, to); + QCOMPARE(operations.valid, true); + QCOMPARE(operations.mkdirs.size(), 1); + QCOMPARE(operations.mkdirs[0], Path("a/b/c")); + + QCOMPARE(operations.rmdirs.size(), 0); + QCOMPARE(operations.deletes.size(), 1); + QCOMPARE(operations.deletes[0], Path("a/b/b.txt")); + QCOMPARE(operations.downloads.size(), 0); + QCOMPARE(operations.mklinks.size(), 1); + QCOMPARE(operations.mklinks.count(Path("a/b/c.txt")), 1); + QCOMPARE(operations.mklinks[Path("a/b/c.txt")], Path("../b.txt")); +} + +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); + QCOMPARE(operations.deletes.size(), 0); + QCOMPARE(operations.rmdirs.size(), 0); + + QCOMPARE(operations.mkdirs.size(), 6); + QCOMPARE(operations.mkdirs[0], Path(".")); + QCOMPARE(operations.mkdirs[1], Path("a")); + QCOMPARE(operations.mkdirs[2], Path("a/b")); + QCOMPARE(operations.mkdirs[3], Path("a/b/c")); + QCOMPARE(operations.mkdirs[4], Path("a/b/c/d")); + QCOMPARE(operations.mkdirs[5], Path("a/b/c/d/e")); + + QCOMPARE(operations.downloads.size(), 0); + QCOMPARE(operations.mklinks.size(), 0); + QCOMPARE(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); + QCOMPARE(operations.deletes.size(), 0); + + QCOMPARE(operations.rmdirs.size(), 6); + QCOMPARE(operations.rmdirs[0], Path("a/b/c/d/e")); + QCOMPARE(operations.rmdirs[1], Path("a/b/c/d")); + QCOMPARE(operations.rmdirs[2], Path("a/b/c")); + QCOMPARE(operations.rmdirs[3], Path("a/b")); + QCOMPARE(operations.rmdirs[4], Path("a")); + QCOMPARE(operations.rmdirs[5], Path(".")); + + QCOMPARE(operations.mkdirs.size(), 0); + QCOMPARE(operations.downloads.size(), 0); + QCOMPARE(operations.mklinks.size(), 0); + QCOMPARE(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); + QCOMPARE(operations.deletes.size(), 0); + QCOMPARE(operations.rmdirs.size(), 0); + QCOMPARE(operations.mkdirs.size(), 0); + QCOMPARE(operations.downloads.size(), 0); + QCOMPARE(operations.mklinks.size(), 0); + QCOMPARE(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); + QCOMPARE(operations.deletes.size(), 1); + QCOMPARE(operations.deletes[0], Path("a/b/c/d/file")); + QCOMPARE(operations.rmdirs.size(), 0); + QCOMPARE(operations.mkdirs.size(), 0); + QCOMPARE(operations.downloads.size(), 1); + QCOMPARE(operations.mklinks.size(), 0); + QCOMPARE(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); + QCOMPARE(operations.deletes.size(), 0); + QCOMPARE(operations.rmdirs.size(), 0); + QCOMPARE(operations.mkdirs.size(), 0); + QCOMPARE(operations.downloads.size(), 1); + QCOMPARE(operations.mklinks.size(), 0); + QCOMPARE(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); + QCOMPARE(operations.deletes.size(), 1); + QCOMPARE(operations.deletes[0], Path("a/b/c/d/file")); + QCOMPARE(operations.rmdirs.size(), 0); + QCOMPARE(operations.mkdirs.size(), 0); + QCOMPARE(operations.downloads.size(), 0); + QCOMPARE(operations.mklinks.size(), 0); + QCOMPARE(operations.executable_fixes.size(), 0); +} + +QTEST_GUILESS_MAIN(PackageManifestTest) + +#include "PackageManifest_test.moc" + diff --git a/api/logic/mojang/testdata/1.8.0_202-x64.json b/api/logic/mojang/testdata/1.8.0_202-x64.json new file mode 100644 index 0000000000000000000000000000000000000000..3d99d719b39a3717e36df435f176a6a7e7461307 GIT binary patch literal 125581 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 literal 0 HcmV?d00001 diff --git a/api/logic/mojang/testdata/inspect/a/b.txt b/api/logic/mojang/testdata/inspect/a/b.txt new file mode 100755 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/api/logic/mojang/testdata/inspect/a/b/b.txt b/api/logic/mojang/testdata/inspect/a/b/b.txt new file mode 120000 index 00000000..4e19a044 --- /dev/null +++ b/api/logic/mojang/testdata/inspect/a/b/b.txt @@ -0,0 +1 @@ +../b.txt \ No newline at end of file