/* 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 . */ #include "PackageManifest.h" #include #include #include #include #include #ifndef Q_OS_WIN32 #include #include #include #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 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 #include #include 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(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(sourceIt->second, iter->second.executable)}); } } } static void resolveFolders(const Package& from, const Package& to, UpdateOperations& out) { std::set remove_folders; std::set 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