428 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			428 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
#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 {
 | 
						|
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") {
 | 
						|
            FileSource bestSource;
 | 
						|
            File file;
 | 
						|
            file.executable = Json::ensureBoolean(fileObject, QString("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;
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
#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. that is useless. 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;
 | 
						|
    std::string temp(size, '\0');
 | 
						|
    // because we don't realiably know how long the damn thing actually is, we loop and expand. POSIX is naff
 | 
						|
    do
 | 
						|
    {
 | 
						|
        auto link_length = ::readlink(filepath_cstr, &temp[0], temp.size());
 | 
						|
        if(link_length == -1)
 | 
						|
        {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        if(std::string::size_type(link_length) < temp.size())
 | 
						|
        {
 | 
						|
            // buffer was long enough and we managed to read the link target. RETURN here.
 | 
						|
            temp.resize(link_length);
 | 
						|
            out = Path(QString::fromUtf8(temp.c_str()));
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
        temp.resize(temp.size() * 2);
 | 
						|
    } while (true);
 | 
						|
}
 | 
						|
}
 | 
						|
#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 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>{
 | 
						|
                    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>{
 | 
						|
                    path,
 | 
						|
                    FileDownload(to.sources.at(iter->second.hash), iter->second.executable)
 | 
						|
                }
 | 
						|
            );
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // Folders
 | 
						|
    std::set<Path, deep_first_sort> remove_folders;
 | 
						|
    std::set<Path, shallow_first_sort> 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;
 | 
						|
}
 | 
						|
 | 
						|
}
 |