0946c7c138
Not integrated yet, but the logic has tests and shouldn't be too shaky. Integration comes next.
367 lines
10 KiB
C++
367 lines
10 KiB
C++
#include "PackageManifest.h"
|
|
#include <Json.h>
|
|
#include <QDir>
|
|
#include <QDirIterator>
|
|
#include <QCryptographicHash>
|
|
#include <QDebug>
|
|
|
|
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, "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>{
|
|
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;
|
|
}
|
|
|
|
}
|