diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:45:07 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:45:07 +0300 |
| commit | 31b9a8949ed0a288143e23bf739f2eb64fdc63be (patch) | |
| tree | 8a984fa143c38fccad461a77792d6864f3e82cd3 /meshmc/launcher/mojang/PackageManifest.cpp | |
| parent | 934382c8a1ce738589dee9ee0f14e1cec812770e (diff) | |
| parent | fad6a1066616b69d7f5fef01178efdf014c59537 (diff) | |
| download | Project-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.tar.gz Project-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.zip | |
Add 'meshmc/' from commit 'fad6a1066616b69d7f5fef01178efdf014c59537'
git-subtree-dir: meshmc
git-subtree-mainline: 934382c8a1ce738589dee9ee0f14e1cec812770e
git-subtree-split: fad6a1066616b69d7f5fef01178efdf014c59537
Diffstat (limited to 'meshmc/launcher/mojang/PackageManifest.cpp')
| -rw-r--r-- | meshmc/launcher/mojang/PackageManifest.cpp | 467 |
1 files changed, 467 insertions, 0 deletions
diff --git a/meshmc/launcher/mojang/PackageManifest.cpp b/meshmc/launcher/mojang/PackageManifest.cpp new file mode 100644 index 0000000000..bc8fe7178f --- /dev/null +++ b/meshmc/launcher/mojang/PackageManifest.cpp @@ -0,0 +1,467 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "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 + { + + FileSource parseFileDownloads(const QJsonObject& fileObject, File& file) + { + FileSource bestSource; + 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); + } + return bestSource; + } + + 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") { + File file; + file.executable = Json::ensureBoolean( + fileObject, QString("executable"), false); + auto bestSource = parseFileDownloads(fileObject, file); + 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()); + } + } // namespace + + 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. + // 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; + QByteArray temp(size, '\0'); + // because we don't reliably know how long the link target is, + // we loop and expand. POSIX is naff + do { + auto link_length = + ::readlink(filepath_cstr, temp.data(), temp.size()); + if (link_length == -1) { + return false; + } + if (link_length < temp.size()) { + // buffer was long enough + out = + Path(QString::fromUtf8(temp.constData(), link_length)); + return true; + } + temp.resize(temp.size() * 2); + } while (true); + } + } // namespace +#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 ShallowFirstSort { + 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 DeepFirstSort { + 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; + } + }; + } // namespace + + static void resolveFiles(const Package& from, const Package& to, + UpdateOperations& out) + { + 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()) { + 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); + auto sourceIt = to.sources.find(iter2->second.hash); + if (sourceIt == to.sources.end()) { + continue; + } + out.downloads.emplace(std::pair<Path, FileDownload>{ + path, + FileDownload(sourceIt->second, 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)) { + auto sourceIt = to.sources.find(iter->second.hash); + if (sourceIt == to.sources.end()) { + continue; + } + out.downloads.emplace(std::pair<Path, FileDownload>{ + path, + FileDownload(sourceIt->second, iter->second.executable)}); + } + } + } + + static void resolveFolders(const Package& from, const Package& to, + UpdateOperations& out) + { + std::set<Path, DeepFirstSort> remove_folders; + std::set<Path, ShallowFirstSort> make_folders; + for (auto from_path : from.folders) { + if (to.folders.find(from_path) == to.folders.end()) { + remove_folders.insert(from_path); + } + } + for (auto& rmdir : remove_folders) { + out.rmdirs.push_back(rmdir); + } + for (auto to_path : to.folders) { + if (from.folders.find(to_path) == from.folders.end()) { + make_folders.insert(to_path); + } + } + for (auto& mkdir : make_folders) { + out.mkdirs.push_back(mkdir); + } + } + + static void resolveSymlinks(const Package& from, const Package& to, + UpdateOperations& out) + { + 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()) { + out.deletes.push_back(path); + continue; + } + if (current_target != iter2->second) { + out.deletes.push_back(path); + out.mklinks[path] = iter2->second; + } + } + for (auto iter = to.symlinks.begin(); iter != to.symlinks.end(); + iter++) { + if (!from.symlinks.count(iter->first)) { + out.mklinks[iter->first] = iter->second; + } + } + } + + UpdateOperations UpdateOperations::resolve(const Package& from, + const Package& to) + { + UpdateOperations out; + + if (!from.valid || !to.valid) { + out.valid = false; + return out; + } + + resolveFiles(from, to, out); + resolveFolders(from, to, out); + resolveSymlinks(from, to, out); + + out.valid = true; + return out; + } + +} // namespace mojang_files |
