diff --git a/Source/Core/CMakeLists.txt b/Source/Core/CMakeLists.txt index 522f3e3e37..b1a19522bf 100644 --- a/Source/Core/CMakeLists.txt +++ b/Source/Core/CMakeLists.txt @@ -16,5 +16,6 @@ if(ENABLE_QT) endif() if (APPLE) + add_subdirectory(UpdaterCommon) add_subdirectory(MacUpdater) endif() diff --git a/Source/Core/MacUpdater/AppDelegate.mm b/Source/Core/MacUpdater/AppDelegate.mm index 79251571ba..e8bfc5c016 100644 --- a/Source/Core/MacUpdater/AppDelegate.mm +++ b/Source/Core/MacUpdater/AppDelegate.mm @@ -4,54 +4,19 @@ #import "AppDelegate.h" -#include "UICommon/AutoUpdate.h" +#include "Common/FileUtil.h" + +#include "UpdaterCommon/UI.h" +#include "UpdaterCommon/UpdaterCommon.h" #include #include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include #include #include -#include - -#include -#include - -#include "Common/CommonPaths.h" -#include "Common/CommonTypes.h" -#include "Common/FileUtil.h" -#include "Common/HttpRequest.h" -#include "Common/StringUtil.h" - -#include "MacUpdater/UI.h" namespace { -// Public key used to verify update manifests. -const u8 UPDATE_PUB_KEY[] = {0x2a, 0xb3, 0xd1, 0xdc, 0x6e, 0xf5, 0x07, 0xf6, 0xa0, 0x6c, 0x7c, - 0x54, 0xdf, 0x54, 0xf4, 0x42, 0x80, 0xa6, 0x28, 0x8b, 0x6d, 0x70, - 0x14, 0xb5, 0x4c, 0x34, 0x95, 0x20, 0x4d, 0xd4, 0xd3, 0x5d}; - -const char UPDATE_TEMP_DIR[] = "TempUpdate"; - -// Where to log updater output. -FILE* log_fp = stderr; - -void FlushLog() -{ - fflush(log_fp); - fclose(log_fp); -} - -// Internal representation of options passed on the command-line. struct Options { std::string this_manifest_url; @@ -131,545 +96,6 @@ std::optional ParseCommandLine(std::vector& args) return opts; } - -std::optional GzipInflate(const std::string& data) -{ - z_stream zstrm; - zstrm.zalloc = nullptr; - zstrm.zfree = nullptr; - zstrm.opaque = nullptr; - zstrm.avail_in = static_cast(data.size()); - zstrm.next_in = reinterpret_cast(const_cast(data.data())); - - // 16 + MAX_WBITS means gzip. Don't ask me. - inflateInit2(&zstrm, 16 + MAX_WBITS); - - std::string out; - char buffer[4096]; - int ret; - - do - { - zstrm.avail_out = sizeof(buffer); - zstrm.next_out = reinterpret_cast(buffer); - - ret = inflate(&zstrm, 0); - out.append(buffer, sizeof(buffer) - zstrm.avail_out); - } while (ret == Z_OK); - - inflateEnd(&zstrm); - - if (ret != Z_STREAM_END) - { - fprintf(log_fp, "Could not read the data as gzip: error %d.\n", ret); - return {}; - } - - return out; -} - -bool VerifySignature(const std::string& data, const std::string& b64_signature) -{ - u8 signature[64]; // ed25519 sig size. - size_t sig_size; - - if (mbedtls_base64_decode(signature, sizeof(signature), &sig_size, - reinterpret_cast(b64_signature.data()), - b64_signature.size()) || - sig_size != sizeof(signature)) - { - fprintf(log_fp, "Invalid base64: %s\n", b64_signature.c_str()); - return false; - } - - return ed25519_verify(signature, reinterpret_cast(data.data()), data.size(), - UPDATE_PUB_KEY); -} - -struct Manifest -{ - using Filename = std::string; - using Hash = std::array; - std::map entries; -}; - -bool HexDecode(const std::string& hex, u8* buffer, size_t size) -{ - if (hex.size() != size * 2) - return false; - - auto DecodeNibble = [](char c) -> std::optional { - if (c >= '0' && c <= '9') - return static_cast(c - '0'); - else if (c >= 'a' && c <= 'f') - return static_cast(c - 'a' + 10); - else if (c >= 'A' && c <= 'F') - return static_cast(c - 'A' + 10); - else - return {}; - }; - for (size_t i = 0; i < size; ++i) - { - std::optional high = DecodeNibble(hex[2 * i]); - std::optional low = DecodeNibble(hex[2 * i + 1]); - - if (!high || !low) - return false; - - buffer[i] = (*high << 4) | *low; - } - - return true; -} - -std::string HexEncode(const u8* buffer, size_t size) -{ - std::string out(size * 2, '\0'); - - for (size_t i = 0; i < size; ++i) - { - out[2 * i] = "0123456789abcdef"[buffer[i] >> 4]; - out[2 * i + 1] = "0123456789abcdef"[buffer[i] & 0xF]; - } - - return out; -} - -std::optional ParseManifest(const std::string& manifest) -{ - Manifest parsed; - size_t pos = 0; - - while (pos < manifest.size()) - { - size_t filename_end_pos = manifest.find('\t', pos); - if (filename_end_pos == std::string::npos) - { - fprintf(log_fp, "Manifest entry %zu: could not find filename end.\n", parsed.entries.size()); - return {}; - } - size_t hash_end_pos = manifest.find('\n', filename_end_pos); - if (hash_end_pos == std::string::npos) - { - fprintf(log_fp, "Manifest entry %zu: could not find hash end.\n", parsed.entries.size()); - return {}; - } - - std::string filename = manifest.substr(pos, filename_end_pos - pos); - std::string hash = manifest.substr(filename_end_pos + 1, hash_end_pos - filename_end_pos - 1); - if (hash.size() != 32) - { - fprintf(log_fp, "Manifest entry %zu: invalid hash: \"%s\".\n", parsed.entries.size(), - hash.c_str()); - return {}; - } - - Manifest::Hash decoded_hash; - if (!HexDecode(hash, decoded_hash.data(), decoded_hash.size())) - { - fprintf(log_fp, "Manifest entry %zu: invalid hash: \"%s\".\n", parsed.entries.size(), - hash.c_str()); - return {}; - } - - parsed.entries[filename] = decoded_hash; - pos = hash_end_pos + 1; - } - - return parsed; -} - -// Not showing a progress bar here because this part is just too quick -std::optional FetchAndParseManifest(const std::string& url) -{ - Common::HttpRequest http; - - Common::HttpRequest::Response resp = http.Get(url); - if (!resp) - { - fprintf(log_fp, "Manifest download failed.\n"); - return {}; - } - - std::string contents(reinterpret_cast(resp->data()), resp->size()); - std::optional maybe_decompressed = GzipInflate(contents); - if (!maybe_decompressed) - return {}; - std::string decompressed = std::move(*maybe_decompressed); - - // Split into manifest and signature. - size_t boundary = decompressed.rfind("\n\n"); - if (boundary == std::string::npos) - { - fprintf(log_fp, "No signature was found in manifest.\n"); - return {}; - } - - std::string signature_block = decompressed.substr(boundary + 2); // 2 for "\n\n". - decompressed.resize(boundary + 1); // 1 to keep the final "\n". - - std::vector signatures = SplitString(signature_block, '\n'); - bool found_valid_signature = false; - for (const auto& signature : signatures) - { - if (VerifySignature(decompressed, signature)) - { - found_valid_signature = true; - break; - } - } - if (!found_valid_signature) - { - fprintf(log_fp, "Could not verify signature of the manifest.\n"); - return {}; - } - - return ParseManifest(decompressed); -} - -// Represent the operations to be performed by the updater. -struct TodoList -{ - struct DownloadOp - { - Manifest::Filename filename; - Manifest::Hash hash; - }; - std::vector to_download; - - struct UpdateOp - { - Manifest::Filename filename; - std::optional old_hash; - Manifest::Hash new_hash; - }; - std::vector to_update; - - struct DeleteOp - { - Manifest::Filename filename; - Manifest::Hash old_hash; - }; - std::vector to_delete; - - void Log() const - { - if (to_update.size()) - { - fprintf(log_fp, "Updating:\n"); - for (const auto& op : to_update) - { - std::string old_desc = - op.old_hash ? HexEncode(op.old_hash->data(), op.old_hash->size()) : "(new)"; - fprintf(log_fp, " - %s: %s -> %s\n", op.filename.c_str(), old_desc.c_str(), - HexEncode(op.new_hash.data(), op.new_hash.size()).c_str()); - } - } - if (to_delete.size()) - { - fprintf(log_fp, "Deleting:\n"); - for (const auto& op : to_delete) - { - fprintf(log_fp, " - %s (%s)\n", op.filename.c_str(), - HexEncode(op.old_hash.data(), op.old_hash.size()).c_str()); - } - } - } -}; - -TodoList ComputeActionsToDo(Manifest this_manifest, Manifest next_manifest) -{ - TodoList todo; - - // Delete if present in this manifest but not in next manifest. - for (const auto& entry : this_manifest.entries) - { - if (next_manifest.entries.find(entry.first) == next_manifest.entries.end()) - { - TodoList::DeleteOp del; - del.filename = entry.first; - del.old_hash = entry.second; - todo.to_delete.push_back(std::move(del)); - } - } - - // Download and update if present in next manifest with different hash from this manifest. - for (const auto& entry : next_manifest.entries) - { - std::optional old_hash; - - const auto& old_entry = this_manifest.entries.find(entry.first); - if (old_entry != this_manifest.entries.end()) - old_hash = old_entry->second; - - if (!old_hash || *old_hash != entry.second) - { - TodoList::DownloadOp download; - download.filename = entry.first; - download.hash = entry.second; - - todo.to_download.push_back(std::move(download)); - - TodoList::UpdateOp update; - update.filename = entry.first; - update.old_hash = old_hash; - update.new_hash = entry.second; - todo.to_update.push_back(std::move(update)); - } - } - - return todo; -} - -std::optional FindOrCreateTempDir(const std::string& base_path) -{ - std::string temp_path = base_path + DIR_SEP + UPDATE_TEMP_DIR; - int counter = 0; - - do - { - if (!File::Exists(temp_path)) - { - if (File::CreateDir(temp_path)) - { - return temp_path; - } - else - { - fprintf(log_fp, "Couldn't create temp directory.\n"); - return {}; - } - } - else if (File::IsDirectory(temp_path)) - { - return temp_path; - } - else - { - // Try again with a counter appended to the path. - std::string suffix = UPDATE_TEMP_DIR + std::to_string(counter); - temp_path = base_path + DIR_SEP + suffix; - } - } while (counter++ < 10); - - fprintf(log_fp, "Could not find an appropriate temp directory name. Giving up.\n"); - return {}; -} - -void CleanUpTempDir(const std::string& temp_dir, const TodoList& todo) -{ - // This is best-effort cleanup, we ignore most errors. - for (const auto& download : todo.to_download) - File::Delete(temp_dir + DIR_SEP + HexEncode(download.hash.data(), download.hash.size())); - File::DeleteDir(temp_dir); -} - -Manifest::Hash ComputeHash(const std::string& contents) -{ - std::array full; - mbedtls_sha256(reinterpret_cast(contents.data()), contents.size(), full.data(), false); - - Manifest::Hash out; - std::copy(full.begin(), full.begin() + 16, out.begin()); - return out; -} - -bool ProgressCallback(double total, double now, double, double) -{ - UI::SetCurrentProgress(static_cast(now), static_cast(total)); - return true; -} - -bool DownloadContent(const std::vector& to_download, - const std::string& content_base_url, const std::string& temp_path) -{ - Common::HttpRequest req(std::chrono::seconds(30), ProgressCallback); - - UI::SetTotalMarquee(false); - - for (size_t i = 0; i < to_download.size(); i++) - { - UI::SetTotalProgress(static_cast(i + 1), static_cast(to_download.size())); - - auto& download = to_download[i]; - - std::string hash_filename = HexEncode(download.hash.data(), download.hash.size()); - UI::SetDescription("Downloading " + download.filename + "... (File " + std::to_string(i + 1) + - " of " + std::to_string(to_download.size()) + ")"); - UI::SetCurrentMarquee(false); - - // Add slashes where needed. - std::string content_store_path = hash_filename; - content_store_path.insert(4, "/"); - content_store_path.insert(2, "/"); - - std::string url = content_base_url + content_store_path; - fprintf(log_fp, "Downloading %s ...\n", url.c_str()); - - auto resp = req.Get(url); - if (!resp) - return false; - - UI::SetCurrentMarquee(true); - UI::SetDescription("Verifying " + download.filename + "..."); - - std::string contents(reinterpret_cast(resp->data()), resp->size()); - std::optional maybe_decompressed = GzipInflate(contents); - if (!maybe_decompressed) - return false; - std::string decompressed = std::move(*maybe_decompressed); - - // Check that the downloaded contents have the right hash. - Manifest::Hash contents_hash = ComputeHash(decompressed); - if (contents_hash != download.hash) - { - fprintf(log_fp, "Wrong hash on downloaded content %s.\n", url.c_str()); - return false; - } - - std::string out = temp_path + DIR_SEP + hash_filename; - if (!File::WriteStringToFile(decompressed, out)) - { - fprintf(log_fp, "Could not write cache file %s.\n", out.c_str()); - return false; - } - } - return true; -} - -bool BackupFile(const std::string& path) -{ - std::string backup_path = path + ".bak"; - fprintf(log_fp, "Backing up unknown pre-existing %s to .bak.\n", path.c_str()); - if (!File::Rename(path, backup_path)) - { - fprintf(log_fp, "Cound not rename %s to %s for backup.\n", path.c_str(), backup_path.c_str()); - return false; - } - return true; -} - -bool UpdateFiles(const std::vector& to_update, - const std::string& install_base_path, const std::string& temp_path) -{ - for (const auto& op : to_update) - { - std::string path = install_base_path + DIR_SEP + op.filename; - if (!File::CreateFullPath(path)) - { - fprintf(log_fp, "Could not create directory structure for %s.\n", op.filename.c_str()); - return false; - } - - // TODO: A new updater protocol version is required to properly mark executable files. For - // now, copy executable bits from existing files. This will break for newly added executables. - std::optional permission; - - if (File::Exists(path)) - { - struct stat file_stats; - - if (stat(path.c_str(), &file_stats) != 0) - return false; - - permission = file_stats.st_mode; - - std::string contents; - if (!File::ReadFileToString(path, contents)) - { - fprintf(log_fp, "Could not read existing file %s.\n", op.filename.c_str()); - return false; - } - Manifest::Hash contents_hash = ComputeHash(contents); - if (contents_hash == op.new_hash) - { - fprintf(log_fp, "File %s was already up to date. Partial update?\n", op.filename.c_str()); - continue; - } - else if (!op.old_hash || contents_hash != *op.old_hash) - { - if (!BackupFile(path)) - return false; - } - } - - // Now we can safely move the new contents to the location. - std::string content_filename = HexEncode(op.new_hash.data(), op.new_hash.size()); - fprintf(log_fp, "Updating file %s from content %s...\n", op.filename.c_str(), - content_filename.c_str()); - if (!File::Copy(temp_path + DIR_SEP + content_filename, path)) - { - fprintf(log_fp, "Could not update file %s.\n", op.filename.c_str()); - - return false; - } - - if (permission.has_value() && chmod(path.c_str(), permission.value()) != 0) - return false; - } - return true; -} - -bool DeleteObsoleteFiles(const std::vector& to_delete, - const std::string& install_base_path) -{ - for (const auto& op : to_delete) - { - std::string path = install_base_path + DIR_SEP + op.filename; - - if (!File::Exists(path)) - { - fprintf(log_fp, "File %s is already missing.\n", op.filename.c_str()); - continue; - } - else - { - std::string contents; - if (!File::ReadFileToString(path, contents)) - { - fprintf(log_fp, "Could not read file planned for deletion: %s.\n", op.filename.c_str()); - return false; - } - Manifest::Hash contents_hash = ComputeHash(contents); - if (contents_hash != op.old_hash) - { - if (!BackupFile(path)) - return false; - } - - File::Delete(path); - } - } - return true; -} - -bool PerformUpdate(const TodoList& todo, const std::string& install_base_path, - const std::string& content_base_url, const std::string& temp_path) -{ - fprintf(log_fp, "Starting download step...\n"); - if (!DownloadContent(todo.to_download, content_base_url, temp_path)) - return false; - fprintf(log_fp, "Download step completed.\n"); - - fprintf(log_fp, "Starting update step...\n"); - if (!UpdateFiles(todo.to_update, install_base_path, temp_path)) - return false; - fprintf(log_fp, "Update step completed.\n"); - - fprintf(log_fp, "Starting deletion step...\n"); - if (!DeleteObsoleteFiles(todo.to_delete, install_base_path)) - return false; - fprintf(log_fp, "Deletion step completed.\n"); - - return true; -} - -void FatalError(const std::string& message) -{ - fprintf(log_fp, "%s\n", message.c_str()); - - UI::Error(message); -} } // namespace @interface AppDelegate () diff --git a/Source/Core/MacUpdater/CMakeLists.txt b/Source/Core/MacUpdater/CMakeLists.txt index 61d3489bfc..725917e030 100644 --- a/Source/Core/MacUpdater/CMakeLists.txt +++ b/Source/Core/MacUpdater/CMakeLists.txt @@ -6,8 +6,7 @@ set(SOURCES AppDelegate.mm ViewController.h ViewController.m - UI.h - UI.mm + MacUI.mm ${STORYBOARDS} ) @@ -31,10 +30,8 @@ target_link_libraries(MacUpdater PRIVATE "-framework AppKit" "-framework CoreData" "-framework Foundation" - uicommon - mbedtls - z - ed25519 + uicommon + updatercommon ) # Compile storyboards (Adapted from https://gitlab.kitware.com/cmake/community/wikis/doc/tutorials/OSX-InterfaceBuilderFiles) diff --git a/Source/Core/MacUpdater/UI.mm b/Source/Core/MacUpdater/MacUI.mm similarity index 97% rename from Source/Core/MacUpdater/UI.mm rename to Source/Core/MacUpdater/MacUI.mm index 0deb85db93..aab655749d 100644 --- a/Source/Core/MacUpdater/UI.mm +++ b/Source/Core/MacUpdater/MacUI.mm @@ -2,9 +2,10 @@ // Licensed under GPLv2+ // Refer to the license.txt file included. -#include "MacUpdater/UI.h" #include "MacUpdater/ViewController.h" +#include "UpdaterCommon/UI.h" + #include #include @@ -105,3 +106,7 @@ void UI::SetTotalProgress(int current, int total) { run_on_main([&] { [GetView() SetTotalProgress:(double)current total:(double)total]; }); } + +void UI::Stop() +{ +} diff --git a/Source/Core/MacUpdater/UI.h b/Source/Core/MacUpdater/UI.h deleted file mode 100644 index 9a963c91e7..0000000000 --- a/Source/Core/MacUpdater/UI.h +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2019 Dolphin Emulator Project -// Licensed under GPLv2+ -// Refer to the license.txt file included. - -#pragma once - -#include - -namespace UI -{ -void Error(const std::string& text); - -void SetVisible(bool visible); - -void SetDescription(const std::string& text); - -void SetTotalMarquee(bool marquee); -void ResetTotalProgress(); -void SetTotalProgress(int current, int total); - -void SetCurrentMarquee(bool marquee); -void ResetCurrentProgress(); -void SetCurrentProgress(int current, int total); -} // namespace UI diff --git a/Source/Core/Updater/Main.cpp b/Source/Core/Updater/Main.cpp index 9acd3ece5c..35e868a7f1 100644 --- a/Source/Core/Updater/Main.cpp +++ b/Source/Core/Updater/Main.cpp @@ -6,45 +6,22 @@ #include #include -#include #include -#include -#include -#include -#include -#include #include #include +#include #include #include -#include #include "Common/CommonPaths.h" #include "Common/CommonTypes.h" #include "Common/FileUtil.h" -#include "Common/HttpRequest.h" -#include "Common/StringUtil.h" -#include "Updater/UI.h" +#include "UpdaterCommon/UI.h" +#include "UpdaterCommon/UpdaterCommon.h" namespace { -// Public key used to verify update manifests. -const u8 UPDATE_PUB_KEY[] = {0x2a, 0xb3, 0xd1, 0xdc, 0x6e, 0xf5, 0x07, 0xf6, 0xa0, 0x6c, 0x7c, - 0x54, 0xdf, 0x54, 0xf4, 0x42, 0x80, 0xa6, 0x28, 0x8b, 0x6d, 0x70, - 0x14, 0xb5, 0x4c, 0x34, 0x95, 0x20, 0x4d, 0xd4, 0xd3, 0x5d}; - -const char UPDATE_TEMP_DIR[] = "TempUpdate"; - -// Where to log updater output. -FILE* log_fp = stderr; - -void FlushLog() -{ - fflush(log_fp); - fclose(log_fp); -} - // Internal representation of options passed on the command-line. struct Options { @@ -142,530 +119,7 @@ std::optional ParseCommandLine(PCWSTR command_line) return opts; } - -std::optional GzipInflate(const std::string& data) -{ - z_stream zstrm; - zstrm.zalloc = nullptr; - zstrm.zfree = nullptr; - zstrm.opaque = nullptr; - zstrm.avail_in = static_cast(data.size()); - zstrm.next_in = reinterpret_cast(const_cast(data.data())); - - // 16 + MAX_WBITS means gzip. Don't ask me. - inflateInit2(&zstrm, 16 + MAX_WBITS); - - std::string out; - char buffer[4096]; - int ret; - - do - { - zstrm.avail_out = sizeof(buffer); - zstrm.next_out = reinterpret_cast(buffer); - - ret = inflate(&zstrm, 0); - out.append(buffer, sizeof(buffer) - zstrm.avail_out); - } while (ret == Z_OK); - - inflateEnd(&zstrm); - - if (ret != Z_STREAM_END) - { - fprintf(log_fp, "Could not read the data as gzip: error %d.\n", ret); - return {}; - } - - return out; -} - -bool VerifySignature(const std::string& data, const std::string& b64_signature) -{ - u8 signature[64]; // ed25519 sig size. - size_t sig_size; - - if (mbedtls_base64_decode(signature, sizeof(signature), &sig_size, - reinterpret_cast(b64_signature.data()), - b64_signature.size()) || - sig_size != sizeof(signature)) - { - fprintf(log_fp, "Invalid base64: %s\n", b64_signature.c_str()); - return false; - } - - return ed25519_verify(signature, reinterpret_cast(data.data()), data.size(), - UPDATE_PUB_KEY); -} - -struct Manifest -{ - using Filename = std::string; - using Hash = std::array; - std::map entries; -}; - -bool HexDecode(const std::string& hex, u8* buffer, size_t size) -{ - if (hex.size() != size * 2) - return false; - - auto DecodeNibble = [](char c) -> std::optional { - if (c >= '0' && c <= '9') - return static_cast(c - '0'); - else if (c >= 'a' && c <= 'f') - return static_cast(c - 'a' + 10); - else if (c >= 'A' && c <= 'F') - return static_cast(c - 'A' + 10); - else - return {}; - }; - for (size_t i = 0; i < size; ++i) - { - std::optional high = DecodeNibble(hex[2 * i]); - std::optional low = DecodeNibble(hex[2 * i + 1]); - - if (!high || !low) - return false; - - buffer[i] = (*high << 4) | *low; - } - - return true; -} - -std::string HexEncode(const u8* buffer, size_t size) -{ - std::string out(size * 2, '\0'); - - for (size_t i = 0; i < size; ++i) - { - out[2 * i] = "0123456789abcdef"[buffer[i] >> 4]; - out[2 * i + 1] = "0123456789abcdef"[buffer[i] & 0xF]; - } - - return out; -} - -std::optional ParseManifest(const std::string& manifest) -{ - Manifest parsed; - size_t pos = 0; - - while (pos < manifest.size()) - { - size_t filename_end_pos = manifest.find('\t', pos); - if (filename_end_pos == std::string::npos) - { - fprintf(log_fp, "Manifest entry %zu: could not find filename end.\n", parsed.entries.size()); - return {}; - } - size_t hash_end_pos = manifest.find('\n', filename_end_pos); - if (hash_end_pos == std::string::npos) - { - fprintf(log_fp, "Manifest entry %zu: could not find hash end.\n", parsed.entries.size()); - return {}; - } - - std::string filename = manifest.substr(pos, filename_end_pos - pos); - std::string hash = manifest.substr(filename_end_pos + 1, hash_end_pos - filename_end_pos - 1); - if (hash.size() != 32) - { - fprintf(log_fp, "Manifest entry %zu: invalid hash: \"%s\".\n", parsed.entries.size(), - hash.c_str()); - return {}; - } - - Manifest::Hash decoded_hash; - if (!HexDecode(hash, decoded_hash.data(), decoded_hash.size())) - { - fprintf(log_fp, "Manifest entry %zu: invalid hash: \"%s\".\n", parsed.entries.size(), - hash.c_str()); - return {}; - } - - parsed.entries[filename] = decoded_hash; - pos = hash_end_pos + 1; - } - - return parsed; -} - -// Not showing a progress bar here because this part is just too quick -std::optional FetchAndParseManifest(const std::string& url) -{ - Common::HttpRequest http; - - Common::HttpRequest::Response resp = http.Get(url); - if (!resp) - { - fprintf(log_fp, "Manifest download failed.\n"); - return {}; - } - - std::string contents(reinterpret_cast(resp->data()), resp->size()); - std::optional maybe_decompressed = GzipInflate(contents); - if (!maybe_decompressed) - return {}; - std::string decompressed = std::move(*maybe_decompressed); - - // Split into manifest and signature. - size_t boundary = decompressed.rfind("\n\n"); - if (boundary == std::string::npos) - { - fprintf(log_fp, "No signature was found in manifest.\n"); - return {}; - } - - std::string signature_block = decompressed.substr(boundary + 2); // 2 for "\n\n". - decompressed.resize(boundary + 1); // 1 to keep the final "\n". - - std::vector signatures = SplitString(signature_block, '\n'); - bool found_valid_signature = false; - for (const auto& signature : signatures) - { - if (VerifySignature(decompressed, signature)) - { - found_valid_signature = true; - break; - } - } - if (!found_valid_signature) - { - fprintf(log_fp, "Could not verify signature of the manifest.\n"); - return {}; - } - - return ParseManifest(decompressed); -} - -// Represent the operations to be performed by the updater. -struct TodoList -{ - struct DownloadOp - { - Manifest::Filename filename; - Manifest::Hash hash; - }; - std::vector to_download; - - struct UpdateOp - { - Manifest::Filename filename; - std::optional old_hash; - Manifest::Hash new_hash; - }; - std::vector to_update; - - struct DeleteOp - { - Manifest::Filename filename; - Manifest::Hash old_hash; - }; - std::vector to_delete; - - void Log() const - { - if (to_update.size()) - { - fprintf(log_fp, "Updating:\n"); - for (const auto& op : to_update) - { - std::string old_desc = - op.old_hash ? HexEncode(op.old_hash->data(), op.old_hash->size()) : "(new)"; - fprintf(log_fp, " - %s: %s -> %s\n", op.filename.c_str(), old_desc.c_str(), - HexEncode(op.new_hash.data(), op.new_hash.size()).c_str()); - } - } - if (to_delete.size()) - { - fprintf(log_fp, "Deleting:\n"); - for (const auto& op : to_delete) - { - fprintf(log_fp, " - %s (%s)\n", op.filename.c_str(), - HexEncode(op.old_hash.data(), op.old_hash.size()).c_str()); - } - } - } -}; - -TodoList ComputeActionsToDo(Manifest this_manifest, Manifest next_manifest) -{ - TodoList todo; - - // Delete if present in this manifest but not in next manifest. - for (const auto& entry : this_manifest.entries) - { - if (next_manifest.entries.find(entry.first) == next_manifest.entries.end()) - { - TodoList::DeleteOp del; - del.filename = entry.first; - del.old_hash = entry.second; - todo.to_delete.push_back(std::move(del)); - } - } - - // Download and update if present in next manifest with different hash from this manifest. - for (const auto& entry : next_manifest.entries) - { - std::optional old_hash; - - const auto& old_entry = this_manifest.entries.find(entry.first); - if (old_entry != this_manifest.entries.end()) - old_hash = old_entry->second; - - if (!old_hash || *old_hash != entry.second) - { - TodoList::DownloadOp download; - download.filename = entry.first; - download.hash = entry.second; - - todo.to_download.push_back(std::move(download)); - - TodoList::UpdateOp update; - update.filename = entry.first; - update.old_hash = old_hash; - update.new_hash = entry.second; - todo.to_update.push_back(std::move(update)); - } - } - - return todo; -} - -std::optional FindOrCreateTempDir(const std::string& base_path) -{ - std::string temp_path = base_path + DIR_SEP + UPDATE_TEMP_DIR; - int counter = 0; - - do - { - if (!File::Exists(temp_path)) - { - if (File::CreateDir(temp_path)) - return temp_path; - else - { - fprintf(log_fp, "Couldn't create temp directory.\n"); - return {}; - } - } - else if (File::IsDirectory(temp_path)) - { - return temp_path; - } - else - { - // Try again with a counter appended to the path. - std::string suffix = UPDATE_TEMP_DIR + std::to_string(counter); - temp_path = base_path + DIR_SEP + suffix; - } - } while (counter++ < 10); - - fprintf(log_fp, "Could not find an appropriate temp directory name. Giving up.\n"); - return {}; -} - -void CleanUpTempDir(const std::string& temp_dir, const TodoList& todo) -{ - // This is best-effort cleanup, we ignore most errors. - for (const auto& download : todo.to_download) - File::Delete(temp_dir + DIR_SEP + HexEncode(download.hash.data(), download.hash.size())); - File::DeleteDir(temp_dir); -} - -Manifest::Hash ComputeHash(const std::string& contents) -{ - std::array full; - mbedtls_sha256(reinterpret_cast(contents.data()), contents.size(), full.data(), false); - - Manifest::Hash out; - std::copy(full.begin(), full.begin() + 16, out.begin()); - return out; -} - -bool ProgressCallback(double total, double now, double, double) -{ - UI::SetCurrentProgress(static_cast(now), static_cast(total)); - return true; -} - -bool DownloadContent(const std::vector& to_download, - const std::string& content_base_url, const std::string& temp_path) -{ - Common::HttpRequest req(std::chrono::seconds(30), ProgressCallback); - - UI::SetTotalMarquee(false); - - for (size_t i = 0; i < to_download.size(); i++) - { - UI::SetTotalProgress(static_cast(i + 1), static_cast(to_download.size())); - - auto& download = to_download[i]; - - std::string hash_filename = HexEncode(download.hash.data(), download.hash.size()); - UI::SetDescription("Downloading " + download.filename + "... (File " + std::to_string(i + 1) + - " of " + std::to_string(to_download.size()) + ")"); - UI::SetCurrentMarquee(false); - - // Add slashes where needed. - std::string content_store_path = hash_filename; - content_store_path.insert(4, "/"); - content_store_path.insert(2, "/"); - - std::string url = content_base_url + content_store_path; - fprintf(log_fp, "Downloading %s ...\n", url.c_str()); - - auto resp = req.Get(url); - if (!resp) - return false; - - UI::SetCurrentMarquee(true); - UI::SetDescription("Verifying " + download.filename + "..."); - - std::string contents(reinterpret_cast(resp->data()), resp->size()); - std::optional maybe_decompressed = GzipInflate(contents); - if (!maybe_decompressed) - return false; - std::string decompressed = std::move(*maybe_decompressed); - - // Check that the downloaded contents have the right hash. - Manifest::Hash contents_hash = ComputeHash(decompressed); - if (contents_hash != download.hash) - { - fprintf(log_fp, "Wrong hash on downloaded content %s.\n", url.c_str()); - return false; - } - - std::string out = temp_path + DIR_SEP + hash_filename; - if (!File::WriteStringToFile(decompressed, out)) - { - fprintf(log_fp, "Could not write cache file %s.\n", out.c_str()); - return false; - } - } - return true; -} - -bool BackupFile(const std::string& path) -{ - std::string backup_path = path + ".bak"; - fprintf(log_fp, "Backing up unknown pre-existing %s to .bak.\n", path.c_str()); - if (!File::Rename(path, backup_path)) - { - fprintf(log_fp, "Cound not rename %s to %s for backup.\n", path.c_str(), backup_path.c_str()); - return false; - } - return true; -} - -bool UpdateFiles(const std::vector& to_update, - const std::string& install_base_path, const std::string& temp_path) -{ - for (const auto& op : to_update) - { - std::string path = install_base_path + DIR_SEP + op.filename; - if (!File::CreateFullPath(path)) - { - fprintf(log_fp, "Could not create directory structure for %s.\n", op.filename.c_str()); - return false; - } - - if (File::Exists(path)) - { - std::string contents; - if (!File::ReadFileToString(path, contents)) - { - fprintf(log_fp, "Could not read existing file %s.\n", op.filename.c_str()); - return false; - } - Manifest::Hash contents_hash = ComputeHash(contents); - if (contents_hash == op.new_hash) - { - fprintf(log_fp, "File %s was already up to date. Partial update?\n", op.filename.c_str()); - continue; - } - else if (!op.old_hash || contents_hash != *op.old_hash) - { - if (!BackupFile(path)) - return false; - } - } - - // Now we can safely move the new contents to the location. - std::string content_filename = HexEncode(op.new_hash.data(), op.new_hash.size()); - fprintf(log_fp, "Updating file %s from content %s...\n", op.filename.c_str(), - content_filename.c_str()); - if (!File::Copy(temp_path + DIR_SEP + content_filename, path)) - { - fprintf(log_fp, "Could not update file %s.\n", op.filename.c_str()); - return false; - } - } - return true; -} - -bool DeleteObsoleteFiles(const std::vector& to_delete, - const std::string& install_base_path) -{ - for (const auto& op : to_delete) - { - std::string path = install_base_path + DIR_SEP + op.filename; - - if (!File::Exists(path)) - { - fprintf(log_fp, "File %s is already missing.\n", op.filename.c_str()); - continue; - } - else - { - std::string contents; - if (!File::ReadFileToString(path, contents)) - { - fprintf(log_fp, "Could not read file planned for deletion: %s.\n", op.filename.c_str()); - return false; - } - Manifest::Hash contents_hash = ComputeHash(contents); - if (contents_hash != op.old_hash) - { - if (!BackupFile(path)) - return false; - } - - File::Delete(path); - } - } - return true; -} - -bool PerformUpdate(const TodoList& todo, const std::string& install_base_path, - const std::string& content_base_url, const std::string& temp_path) -{ - fprintf(log_fp, "Starting download step...\n"); - if (!DownloadContent(todo.to_download, content_base_url, temp_path)) - return false; - fprintf(log_fp, "Download step completed.\n"); - - fprintf(log_fp, "Starting update step...\n"); - if (!UpdateFiles(todo.to_update, install_base_path, temp_path)) - return false; - fprintf(log_fp, "Update step completed.\n"); - - fprintf(log_fp, "Starting deletion step...\n"); - if (!DeleteObsoleteFiles(todo.to_delete, install_base_path)) - return false; - fprintf(log_fp, "Deletion step completed.\n"); - - return true; -} - -void FatalError(const std::string& message) -{ - fprintf(log_fp, "%s\n", message.c_str()); - - UI::Error(message); - UI::Stop(); -} -} // namespace +}; // namespace int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) { diff --git a/Source/Core/Updater/Updater.vcxproj b/Source/Core/Updater/Updater.vcxproj index 3877d8d6f6..cb6085f967 100644 --- a/Source/Core/Updater/Updater.vcxproj +++ b/Source/Core/Updater/Updater.vcxproj @@ -45,25 +45,16 @@ {c636d9d1-82fe-42b5-9987-63b7d4836341} - - {bb00605c-125f-4a21-b33b-7bf418322dcb} - - - {5bdf4b91-1491-4fb0-bc27-78e9a8e97dc3} - - - {bdb6578b-0691-4e80-a46c-df21639fd3b8} - - - {ff213b23-2c26-4214-9f88-85271e557e87} - {2e6c348c-c75c-4d94-8d1e-9c1fcbf3efe4} + + + {B001D13E-7EAB-4689-842D-801E5ACFFAC5} - + @@ -72,9 +63,6 @@ - - - @@ -82,4 +70,4 @@ - \ No newline at end of file + diff --git a/Source/Core/Updater/Updater.vcxproj.filters b/Source/Core/Updater/Updater.vcxproj.filters index f39ac62aae..a430cf5a09 100644 --- a/Source/Core/Updater/Updater.vcxproj.filters +++ b/Source/Core/Updater/Updater.vcxproj.filters @@ -2,12 +2,9 @@ - - - - + - \ No newline at end of file + diff --git a/Source/Core/Updater/UI.cpp b/Source/Core/Updater/WinUI.cpp similarity index 98% rename from Source/Core/Updater/UI.cpp rename to Source/Core/Updater/WinUI.cpp index dc90758036..8d73236c29 100644 --- a/Source/Core/Updater/UI.cpp +++ b/Source/Core/Updater/WinUI.cpp @@ -2,13 +2,14 @@ // Licensed under GPLv2+ // Refer to the license.txt file included. -#include "Updater/UI.h" - -#include -#include +#include "UpdaterCommon/UI.h" #include +#include +#include +#include + #include "Common/Flag.h" #include "Common/StringUtil.h" diff --git a/Source/Core/UpdaterCommon/CMakeLists.txt b/Source/Core/UpdaterCommon/CMakeLists.txt new file mode 100644 index 0000000000..c789681ef4 --- /dev/null +++ b/Source/Core/UpdaterCommon/CMakeLists.txt @@ -0,0 +1,8 @@ +add_library(updatercommon + UpdaterCommon.cpp) + +target_link_libraries(updatercommon PRIVATE + uicommon + mbedtls + z + ed25519) \ No newline at end of file diff --git a/Source/Core/Updater/UI.h b/Source/Core/UpdaterCommon/UI.h similarity index 91% rename from Source/Core/Updater/UI.h rename to Source/Core/UpdaterCommon/UI.h index f0c1a6401a..f542d02da0 100644 --- a/Source/Core/Updater/UI.h +++ b/Source/Core/UpdaterCommon/UI.h @@ -4,7 +4,7 @@ #pragma once -#include +#include namespace UI { @@ -21,4 +21,6 @@ void SetTotalProgress(int current, int total); void SetCurrentMarquee(bool marquee); void ResetCurrentProgress(); void SetCurrentProgress(int current, int total); + +void SetVisible(bool visible); } // namespace UI diff --git a/Source/Core/UpdaterCommon/UpdaterCommon.cpp b/Source/Core/UpdaterCommon/UpdaterCommon.cpp new file mode 100644 index 0000000000..3a65829cfd --- /dev/null +++ b/Source/Core/UpdaterCommon/UpdaterCommon.cpp @@ -0,0 +1,549 @@ +// Copyright 2019 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "UpdaterCommon/UpdaterCommon.h" + +#include +#include +#include +#include +#include + +#include "Common/CommonPaths.h" +#include "Common/FileUtil.h" +#include "Common/HttpRequest.h" +#include "Common/StringUtil.h" +#include "UpdaterCommon/UI.h" + +#ifndef _WIN32 +#include +#include +#endif + +// Where to log updater output. +FILE* log_fp = stderr; + +// Public key used to verify update manifests. +const std::array UPDATE_PUB_KEY = { + 0x2a, 0xb3, 0xd1, 0xdc, 0x6e, 0xf5, 0x07, 0xf6, 0xa0, 0x6c, 0x7c, 0x54, 0xdf, 0x54, 0xf4, 0x42, + 0x80, 0xa6, 0x28, 0x8b, 0x6d, 0x70, 0x14, 0xb5, 0x4c, 0x34, 0x95, 0x20, 0x4d, 0xd4, 0xd3, 0x5d}; + +const char UPDATE_TEMP_DIR[] = "TempUpdate"; + +static bool ProgressCallback(double total, double now, double, double) +{ + UI::SetCurrentProgress(static_cast(now), static_cast(total)); + return true; +} + +static std::string HexEncode(const u8* buffer, size_t size) +{ + std::string out(size * 2, '\0'); + + for (size_t i = 0; i < size; ++i) + { + out[2 * i] = "0123456789abcdef"[buffer[i] >> 4]; + out[2 * i + 1] = "0123456789abcdef"[buffer[i] & 0xF]; + } + + return out; +} + +static bool HexDecode(const std::string& hex, u8* buffer, size_t size) +{ + if (hex.size() != size * 2) + return false; + + auto DecodeNibble = [](char c) -> std::optional { + if (c >= '0' && c <= '9') + return static_cast(c - '0'); + else if (c >= 'a' && c <= 'f') + return static_cast(c - 'a' + 10); + else if (c >= 'A' && c <= 'F') + return static_cast(c - 'A' + 10); + else + return {}; + }; + for (size_t i = 0; i < size; ++i) + { + std::optional high = DecodeNibble(hex[2 * i]); + std::optional low = DecodeNibble(hex[2 * i + 1]); + + if (!high || !low) + return false; + + buffer[i] = (*high << 4) | *low; + } + + return true; +} + +static std::optional GzipInflate(const std::string& data) +{ + z_stream zstrm; + zstrm.zalloc = nullptr; + zstrm.zfree = nullptr; + zstrm.opaque = nullptr; + zstrm.avail_in = static_cast(data.size()); + zstrm.next_in = reinterpret_cast(const_cast(data.data())); + + // 16 + MAX_WBITS means gzip. Don't ask me. + inflateInit2(&zstrm, 16 + MAX_WBITS); + + std::string out; + char buffer[4096]; + int ret; + + do + { + zstrm.avail_out = sizeof(buffer); + zstrm.next_out = reinterpret_cast(buffer); + + ret = inflate(&zstrm, 0); + out.append(buffer, sizeof(buffer) - zstrm.avail_out); + } while (ret == Z_OK); + + inflateEnd(&zstrm); + + if (ret != Z_STREAM_END) + { + fprintf(log_fp, "Could not read the data as gzip: error %d.\n", ret); + return {}; + } + + return out; +} + +static Manifest::Hash ComputeHash(const std::string& contents) +{ + std::array full; + mbedtls_sha256(reinterpret_cast(contents.data()), contents.size(), full.data(), false); + + Manifest::Hash out; + std::copy(full.begin(), full.begin() + 16, out.begin()); + return out; +} + +static bool VerifySignature(const std::string& data, const std::string& b64_signature) +{ + u8 signature[64]; // ed25519 sig size. + size_t sig_size; + + if (mbedtls_base64_decode(signature, sizeof(signature), &sig_size, + reinterpret_cast(b64_signature.data()), + b64_signature.size()) || + sig_size != sizeof(signature)) + { + fprintf(log_fp, "Invalid base64: %s\n", b64_signature.c_str()); + return false; + } + + return ed25519_verify(signature, reinterpret_cast(data.data()), data.size(), + UPDATE_PUB_KEY.data()); +} + +void FlushLog() +{ + fflush(log_fp); + fclose(log_fp); +} + +void TodoList::Log() const +{ + if (to_update.size()) + { + fprintf(log_fp, "Updating:\n"); + for (const auto& op : to_update) + { + std::string old_desc = + op.old_hash ? HexEncode(op.old_hash->data(), op.old_hash->size()) : "(new)"; + fprintf(log_fp, " - %s: %s -> %s\n", op.filename.c_str(), old_desc.c_str(), + HexEncode(op.new_hash.data(), op.new_hash.size()).c_str()); + } + } + if (to_delete.size()) + { + fprintf(log_fp, "Deleting:\n"); + for (const auto& op : to_delete) + { + fprintf(log_fp, " - %s (%s)\n", op.filename.c_str(), + HexEncode(op.old_hash.data(), op.old_hash.size()).c_str()); + } + } +} + +static bool DownloadContent(const std::vector& to_download, + const std::string& content_base_url, const std::string& temp_path) +{ + Common::HttpRequest req(std::chrono::seconds(30), ProgressCallback); + + UI::SetTotalMarquee(false); + + for (size_t i = 0; i < to_download.size(); i++) + { + UI::SetTotalProgress(static_cast(i + 1), static_cast(to_download.size())); + + auto& download = to_download[i]; + + std::string hash_filename = HexEncode(download.hash.data(), download.hash.size()); + UI::SetDescription("Downloading " + download.filename + "... (File " + std::to_string(i + 1) + + " of " + std::to_string(to_download.size()) + ")"); + UI::SetCurrentMarquee(false); + + // Add slashes where needed. + std::string content_store_path = hash_filename; + content_store_path.insert(4, "/"); + content_store_path.insert(2, "/"); + + std::string url = content_base_url + content_store_path; + fprintf(log_fp, "Downloading %s ...\n", url.c_str()); + + auto resp = req.Get(url); + if (!resp) + return false; + + UI::SetCurrentMarquee(true); + UI::SetDescription("Verifying " + download.filename + "..."); + + std::string contents(reinterpret_cast(resp->data()), resp->size()); + std::optional maybe_decompressed = GzipInflate(contents); + if (!maybe_decompressed) + return false; + std::string decompressed = std::move(*maybe_decompressed); + + // Check that the downloaded contents have the right hash. + Manifest::Hash contents_hash = ComputeHash(decompressed); + if (contents_hash != download.hash) + { + fprintf(log_fp, "Wrong hash on downloaded content %s.\n", url.c_str()); + return false; + } + + std::string out = temp_path + DIR_SEP + hash_filename; + if (!File::WriteStringToFile(decompressed, out)) + { + fprintf(log_fp, "Could not write cache file %s.\n", out.c_str()); + return false; + } + } + return true; +} + +TodoList ComputeActionsToDo(Manifest this_manifest, Manifest next_manifest) +{ + TodoList todo; + + // Delete if present in this manifest but not in next manifest. + for (const auto& entry : this_manifest.entries) + { + if (next_manifest.entries.find(entry.first) == next_manifest.entries.end()) + { + TodoList::DeleteOp del; + del.filename = entry.first; + del.old_hash = entry.second; + todo.to_delete.push_back(std::move(del)); + } + } + + // Download and update if present in next manifest with different hash from this manifest. + for (const auto& entry : next_manifest.entries) + { + std::optional old_hash; + + const auto& old_entry = this_manifest.entries.find(entry.first); + if (old_entry != this_manifest.entries.end()) + old_hash = old_entry->second; + + if (!old_hash || *old_hash != entry.second) + { + TodoList::DownloadOp download; + download.filename = entry.first; + download.hash = entry.second; + + todo.to_download.push_back(std::move(download)); + + TodoList::UpdateOp update; + update.filename = entry.first; + update.old_hash = old_hash; + update.new_hash = entry.second; + todo.to_update.push_back(std::move(update)); + } + } + + return todo; +} + +std::optional FindOrCreateTempDir(const std::string& base_path) +{ + std::string temp_path = base_path + DIR_SEP + UPDATE_TEMP_DIR; + int counter = 0; + + do + { + if (!File::Exists(temp_path)) + { + if (File::CreateDir(temp_path)) + { + return temp_path; + } + else + { + fprintf(log_fp, "Couldn't create temp directory.\n"); + return {}; + } + } + else if (File::IsDirectory(temp_path)) + { + return temp_path; + } + else + { + // Try again with a counter appended to the path. + std::string suffix = UPDATE_TEMP_DIR + std::to_string(counter); + temp_path = base_path + DIR_SEP + suffix; + } + } while (counter++ < 10); + + fprintf(log_fp, "Could not find an appropriate temp directory name. Giving up.\n"); + return {}; +} + +void CleanUpTempDir(const std::string& temp_dir, const TodoList& todo) +{ + // This is best-effort cleanup, we ignore most errors. + for (const auto& download : todo.to_download) + File::Delete(temp_dir + DIR_SEP + HexEncode(download.hash.data(), download.hash.size())); + File::DeleteDir(temp_dir); +} + +static bool BackupFile(const std::string& path) +{ + std::string backup_path = path + ".bak"; + fprintf(log_fp, "Backing up unknown pre-existing %s to .bak.\n", path.c_str()); + if (!File::Rename(path, backup_path)) + { + fprintf(log_fp, "Cound not rename %s to %s for backup.\n", path.c_str(), backup_path.c_str()); + return false; + } + return true; +} + +static bool DeleteObsoleteFiles(const std::vector& to_delete, + const std::string& install_base_path) +{ + for (const auto& op : to_delete) + { + std::string path = install_base_path + DIR_SEP + op.filename; + + if (!File::Exists(path)) + { + fprintf(log_fp, "File %s is already missing.\n", op.filename.c_str()); + continue; + } + else + { + std::string contents; + if (!File::ReadFileToString(path, contents)) + { + fprintf(log_fp, "Could not read file planned for deletion: %s.\n", op.filename.c_str()); + return false; + } + Manifest::Hash contents_hash = ComputeHash(contents); + if (contents_hash != op.old_hash) + { + if (!BackupFile(path)) + return false; + } + + File::Delete(path); + } + } + return true; +} + +static bool UpdateFiles(const std::vector& to_update, + const std::string& install_base_path, const std::string& temp_path) +{ + for (const auto& op : to_update) + { + std::string path = install_base_path + DIR_SEP + op.filename; + if (!File::CreateFullPath(path)) + { + fprintf(log_fp, "Could not create directory structure for %s.\n", op.filename.c_str()); + return false; + } + +#ifndef _WIN32 + // TODO: A new updater protocol version is required to properly mark executable files. For + // now, copy executable bits from existing files. This will break for newly added executables. + std::optional permission; +#endif + + if (File::Exists(path)) + { +#ifndef _WIN32 + struct stat file_stats; + + if (stat(path.c_str(), &file_stats) != 0) + return false; + + permission = file_stats.st_mode; +#endif + std::string contents; + if (!File::ReadFileToString(path, contents)) + { + fprintf(log_fp, "Could not read existing file %s.\n", op.filename.c_str()); + return false; + } + Manifest::Hash contents_hash = ComputeHash(contents); + if (contents_hash == op.new_hash) + { + fprintf(log_fp, "File %s was already up to date. Partial update?\n", op.filename.c_str()); + continue; + } + else if (!op.old_hash || contents_hash != *op.old_hash) + { + if (!BackupFile(path)) + return false; + } + } + + // Now we can safely move the new contents to the location. + std::string content_filename = HexEncode(op.new_hash.data(), op.new_hash.size()); + fprintf(log_fp, "Updating file %s from content %s...\n", op.filename.c_str(), + content_filename.c_str()); + if (!File::Copy(temp_path + DIR_SEP + content_filename, path)) + { + fprintf(log_fp, "Could not update file %s.\n", op.filename.c_str()); + return false; + } + +#ifndef _WIN32 + if (permission.has_value() && chmod(path.c_str(), permission.value()) != 0) + return false; +#endif + } + return true; +} + +bool PerformUpdate(const TodoList& todo, const std::string& install_base_path, + const std::string& content_base_url, const std::string& temp_path) +{ + fprintf(log_fp, "Starting download step...\n"); + if (!DownloadContent(todo.to_download, content_base_url, temp_path)) + return false; + fprintf(log_fp, "Download step completed.\n"); + + fprintf(log_fp, "Starting update step...\n"); + if (!UpdateFiles(todo.to_update, install_base_path, temp_path)) + return false; + fprintf(log_fp, "Update step completed.\n"); + + fprintf(log_fp, "Starting deletion step...\n"); + if (!DeleteObsoleteFiles(todo.to_delete, install_base_path)) + return false; + fprintf(log_fp, "Deletion step completed.\n"); + + return true; +} + +void FatalError(const std::string& message) +{ + fprintf(log_fp, "%s\n", message.c_str()); + + UI::Error(message); + UI::Stop(); +} + +static std::optional ParseManifest(const std::string& manifest) +{ + Manifest parsed; + size_t pos = 0; + + while (pos < manifest.size()) + { + size_t filename_end_pos = manifest.find('\t', pos); + if (filename_end_pos == std::string::npos) + { + fprintf(log_fp, "Manifest entry %zu: could not find filename end.\n", parsed.entries.size()); + return {}; + } + size_t hash_end_pos = manifest.find('\n', filename_end_pos); + if (hash_end_pos == std::string::npos) + { + fprintf(log_fp, "Manifest entry %zu: could not find hash end.\n", parsed.entries.size()); + return {}; + } + + std::string filename = manifest.substr(pos, filename_end_pos - pos); + std::string hash = manifest.substr(filename_end_pos + 1, hash_end_pos - filename_end_pos - 1); + if (hash.size() != 32) + { + fprintf(log_fp, "Manifest entry %zu: invalid hash: \"%s\".\n", parsed.entries.size(), + hash.c_str()); + return {}; + } + + Manifest::Hash decoded_hash; + if (!HexDecode(hash, decoded_hash.data(), decoded_hash.size())) + { + fprintf(log_fp, "Manifest entry %zu: invalid hash: \"%s\".\n", parsed.entries.size(), + hash.c_str()); + return {}; + } + + parsed.entries[filename] = decoded_hash; + pos = hash_end_pos + 1; + } + + return parsed; +} + +// Not showing a progress bar here because this part is just too quick +std::optional FetchAndParseManifest(const std::string& url) +{ + Common::HttpRequest http; + + Common::HttpRequest::Response resp = http.Get(url); + if (!resp) + { + fprintf(log_fp, "Manifest download failed.\n"); + return {}; + } + + std::string contents(reinterpret_cast(resp->data()), resp->size()); + std::optional maybe_decompressed = GzipInflate(contents); + if (!maybe_decompressed) + return {}; + std::string decompressed = std::move(*maybe_decompressed); + + // Split into manifest and signature. + size_t boundary = decompressed.rfind("\n\n"); + if (boundary == std::string::npos) + { + fprintf(log_fp, "No signature was found in manifest.\n"); + return {}; + } + + std::string signature_block = decompressed.substr(boundary + 2); // 2 for "\n\n". + decompressed.resize(boundary + 1); // 1 to keep the final "\n". + + std::vector signatures = SplitString(signature_block, '\n'); + bool found_valid_signature = false; + for (const auto& signature : signatures) + { + if (VerifySignature(decompressed, signature)) + { + found_valid_signature = true; + break; + } + } + if (!found_valid_signature) + { + fprintf(log_fp, "Could not verify signature of the manifest.\n"); + return {}; + } + + return ParseManifest(decompressed); +} diff --git a/Source/Core/UpdaterCommon/UpdaterCommon.h b/Source/Core/UpdaterCommon/UpdaterCommon.h new file mode 100644 index 0000000000..fb63994d80 --- /dev/null +++ b/Source/Core/UpdaterCommon/UpdaterCommon.h @@ -0,0 +1,60 @@ +// Copyright 2019 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "Common/CommonTypes.h" + +extern FILE* log_fp; + +struct Manifest +{ + using Filename = std::string; + using Hash = std::array; + std::map entries; +}; + +// Represent the operations to be performed by the updater. +struct TodoList +{ + struct DownloadOp + { + Manifest::Filename filename; + Manifest::Hash hash; + }; + std::vector to_download; + + struct UpdateOp + { + Manifest::Filename filename; + std::optional old_hash; + Manifest::Hash new_hash; + }; + std::vector to_update; + + struct DeleteOp + { + Manifest::Filename filename; + Manifest::Hash old_hash; + }; + std::vector to_delete; + + void Log() const; +}; + +void FatalError(const std::string& message); +std::optional FetchAndParseManifest(const std::string& url); +TodoList ComputeActionsToDo(Manifest this_manifest, Manifest next_manifest); +std::optional FindOrCreateTempDir(const std::string& base_path); +void CleanUpTempDir(const std::string& temp_dir, const TodoList& todo); +bool PerformUpdate(const TodoList& todo, const std::string& install_base_path, + const std::string& content_base_url, const std::string& temp_path); +void FlushLog(); diff --git a/Source/Core/UpdaterCommon/UpdaterCommon.vcxproj b/Source/Core/UpdaterCommon/UpdaterCommon.vcxproj new file mode 100644 index 0000000000..191098f15f --- /dev/null +++ b/Source/Core/UpdaterCommon/UpdaterCommon.vcxproj @@ -0,0 +1,65 @@ + + + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {B001D13E-7EAB-4689-842D-801E5ACFFAC5} + UpdaterCommon + 10.0.17134.0 + + + + StaticLibrary + v141 + Unicode + + + true + + + false + + + + + + + + + + + + + + + {2e6c348c-c75c-4d94-8d1e-9c1fcbf3efe4} + + + {bb00605c-125f-4a21-b33b-7bf418322dcb} + + + {5bdf4b91-1491-4fb0-bc27-78e9a8e97dc3} + + + {bdb6578b-0691-4e80-a46c-df21639fd3b8} + + + {ff213b23-2c26-4214-9f88-85271e557e87} + + + + + + + + + \ No newline at end of file diff --git a/Source/dolphin-emu.sln b/Source/dolphin-emu.sln index daf796987d..20b4d9d146 100644 --- a/Source/dolphin-emu.sln +++ b/Source/dolphin-emu.sln @@ -91,180 +91,268 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "minizip", "..\Externals\min EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "imgui", "..\Externals\imgui\imgui.vcxproj", "{4C3B2264-EA73-4A7B-9CFE-65B0FD635EBB}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "UpdaterCommon", "Core\UpdaterCommon\UpdaterCommon.vcxproj", "{B001D13E-7EAB-4689-842D-801E5ACFFAC5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {FA3FA62B-6F58-4B86-9453-4D149940A066}.Debug|x64.ActiveCfg = Debug|x64 {FA3FA62B-6F58-4B86-9453-4D149940A066}.Debug|x64.Build.0 = Debug|x64 + {FA3FA62B-6F58-4B86-9453-4D149940A066}.Debug|x86.ActiveCfg = Debug|x64 {FA3FA62B-6F58-4B86-9453-4D149940A066}.Release|x64.ActiveCfg = Release|x64 {FA3FA62B-6F58-4B86-9453-4D149940A066}.Release|x64.Build.0 = Release|x64 - {47411FDB-1BF2-48D0-AB4E-C7C41160F898}.Debug|x64.ActiveCfg = Debug|x64 - {47411FDB-1BF2-48D0-AB4E-C7C41160F898}.Debug|x64.Build.0 = Debug|x64 - {47411FDB-1BF2-48D0-AB4E-C7C41160F898}.Release|x64.ActiveCfg = Release|x64 - {47411FDB-1BF2-48D0-AB4E-C7C41160F898}.Release|x64.Build.0 = Release|x64 + {FA3FA62B-6F58-4B86-9453-4D149940A066}.Release|x86.ActiveCfg = Release|x64 {E54CF649-140E-4255-81A5-30A673C1FB36}.Debug|x64.ActiveCfg = Debug|x64 {E54CF649-140E-4255-81A5-30A673C1FB36}.Debug|x64.Build.0 = Debug|x64 + {E54CF649-140E-4255-81A5-30A673C1FB36}.Debug|x86.ActiveCfg = Debug|x64 {E54CF649-140E-4255-81A5-30A673C1FB36}.Release|x64.ActiveCfg = Release|x64 {E54CF649-140E-4255-81A5-30A673C1FB36}.Release|x64.Build.0 = Release|x64 + {E54CF649-140E-4255-81A5-30A673C1FB36}.Release|x86.ActiveCfg = Release|x64 {54AA7840-5BEB-4A0C-9452-74BA4CC7FD44}.Debug|x64.ActiveCfg = Debug|x64 {54AA7840-5BEB-4A0C-9452-74BA4CC7FD44}.Debug|x64.Build.0 = Debug|x64 + {54AA7840-5BEB-4A0C-9452-74BA4CC7FD44}.Debug|x86.ActiveCfg = Debug|x64 {54AA7840-5BEB-4A0C-9452-74BA4CC7FD44}.Release|x64.ActiveCfg = Release|x64 {54AA7840-5BEB-4A0C-9452-74BA4CC7FD44}.Release|x64.Build.0 = Release|x64 + {54AA7840-5BEB-4A0C-9452-74BA4CC7FD44}.Release|x86.ActiveCfg = Release|x64 {2E6C348C-C75C-4D94-8D1E-9C1FCBF3EFE4}.Debug|x64.ActiveCfg = Debug|x64 {2E6C348C-C75C-4D94-8D1E-9C1FCBF3EFE4}.Debug|x64.Build.0 = Debug|x64 + {2E6C348C-C75C-4D94-8D1E-9C1FCBF3EFE4}.Debug|x86.ActiveCfg = Debug|x64 {2E6C348C-C75C-4D94-8D1E-9C1FCBF3EFE4}.Release|x64.ActiveCfg = Release|x64 {2E6C348C-C75C-4D94-8D1E-9C1FCBF3EFE4}.Release|x64.Build.0 = Release|x64 + {2E6C348C-C75C-4D94-8D1E-9C1FCBF3EFE4}.Release|x86.ActiveCfg = Release|x64 {160BDC25-5626-4B0D-BDD8-2953D9777FB5}.Debug|x64.ActiveCfg = Debug|x64 {160BDC25-5626-4B0D-BDD8-2953D9777FB5}.Debug|x64.Build.0 = Debug|x64 + {160BDC25-5626-4B0D-BDD8-2953D9777FB5}.Debug|x86.ActiveCfg = Debug|x64 {160BDC25-5626-4B0D-BDD8-2953D9777FB5}.Release|x64.ActiveCfg = Release|x64 {160BDC25-5626-4B0D-BDD8-2953D9777FB5}.Release|x64.Build.0 = Release|x64 + {160BDC25-5626-4B0D-BDD8-2953D9777FB5}.Release|x86.ActiveCfg = Release|x64 {6BBD47CF-91FD-4077-B676-8B76980178A9}.Debug|x64.ActiveCfg = Debug|x64 {6BBD47CF-91FD-4077-B676-8B76980178A9}.Debug|x64.Build.0 = Debug|x64 + {6BBD47CF-91FD-4077-B676-8B76980178A9}.Debug|x86.ActiveCfg = Debug|x64 {6BBD47CF-91FD-4077-B676-8B76980178A9}.Release|x64.ActiveCfg = Release|x64 {6BBD47CF-91FD-4077-B676-8B76980178A9}.Release|x64.Build.0 = Release|x64 + {6BBD47CF-91FD-4077-B676-8B76980178A9}.Release|x86.ActiveCfg = Release|x64 {604C8368-F34A-4D55-82C8-CC92A0C13254}.Debug|x64.ActiveCfg = Debug|x64 {604C8368-F34A-4D55-82C8-CC92A0C13254}.Debug|x64.Build.0 = Debug|x64 + {604C8368-F34A-4D55-82C8-CC92A0C13254}.Debug|x86.ActiveCfg = Debug|x64 {604C8368-F34A-4D55-82C8-CC92A0C13254}.Release|x64.ActiveCfg = Release|x64 {604C8368-F34A-4D55-82C8-CC92A0C13254}.Release|x64.Build.0 = Release|x64 + {604C8368-F34A-4D55-82C8-CC92A0C13254}.Release|x86.ActiveCfg = Release|x64 {3DE9EE35-3E91-4F27-A014-2866AD8C3FE3}.Debug|x64.ActiveCfg = Debug|x64 {3DE9EE35-3E91-4F27-A014-2866AD8C3FE3}.Debug|x64.Build.0 = Debug|x64 + {3DE9EE35-3E91-4F27-A014-2866AD8C3FE3}.Debug|x86.ActiveCfg = Debug|x64 {3DE9EE35-3E91-4F27-A014-2866AD8C3FE3}.Release|x64.ActiveCfg = Release|x64 {3DE9EE35-3E91-4F27-A014-2866AD8C3FE3}.Release|x64.Build.0 = Release|x64 + {3DE9EE35-3E91-4F27-A014-2866AD8C3FE3}.Release|x86.ActiveCfg = Release|x64 {8ADA04D7-6DB1-4DA4-AB55-64FB12A0997B}.Debug|x64.ActiveCfg = Debug|x64 {8ADA04D7-6DB1-4DA4-AB55-64FB12A0997B}.Debug|x64.Build.0 = Debug|x64 + {8ADA04D7-6DB1-4DA4-AB55-64FB12A0997B}.Debug|x86.ActiveCfg = Debug|x64 {8ADA04D7-6DB1-4DA4-AB55-64FB12A0997B}.Release|x64.ActiveCfg = Release|x64 {8ADA04D7-6DB1-4DA4-AB55-64FB12A0997B}.Release|x64.Build.0 = Release|x64 + {8ADA04D7-6DB1-4DA4-AB55-64FB12A0997B}.Release|x86.ActiveCfg = Release|x64 {0E033BE3-2E08-428E-9AE9-BC673EFA12B5}.Debug|x64.ActiveCfg = Debug|x64 {0E033BE3-2E08-428E-9AE9-BC673EFA12B5}.Debug|x64.Build.0 = Debug|x64 + {0E033BE3-2E08-428E-9AE9-BC673EFA12B5}.Debug|x86.ActiveCfg = Debug|x64 {0E033BE3-2E08-428E-9AE9-BC673EFA12B5}.Release|x64.ActiveCfg = Release|x64 {0E033BE3-2E08-428E-9AE9-BC673EFA12B5}.Release|x64.Build.0 = Release|x64 + {0E033BE3-2E08-428E-9AE9-BC673EFA12B5}.Release|x86.ActiveCfg = Release|x64 {AB993F38-C31D-4897-B139-A620C42BC565}.Debug|x64.ActiveCfg = Debug|x64 {AB993F38-C31D-4897-B139-A620C42BC565}.Debug|x64.Build.0 = Debug|x64 + {AB993F38-C31D-4897-B139-A620C42BC565}.Debug|x86.ActiveCfg = Debug|x64 {AB993F38-C31D-4897-B139-A620C42BC565}.Release|x64.ActiveCfg = Release|x64 {AB993F38-C31D-4897-B139-A620C42BC565}.Release|x64.Build.0 = Release|x64 + {AB993F38-C31D-4897-B139-A620C42BC565}.Release|x86.ActiveCfg = Release|x64 {31643FDB-1BB8-4965-9DE7-000FC88D35AE}.Debug|x64.ActiveCfg = Debug|x64 {31643FDB-1BB8-4965-9DE7-000FC88D35AE}.Debug|x64.Build.0 = Debug|x64 + {31643FDB-1BB8-4965-9DE7-000FC88D35AE}.Debug|x86.ActiveCfg = Debug|x64 {31643FDB-1BB8-4965-9DE7-000FC88D35AE}.Release|x64.ActiveCfg = Release|x64 {31643FDB-1BB8-4965-9DE7-000FC88D35AE}.Release|x64.Build.0 = Release|x64 + {31643FDB-1BB8-4965-9DE7-000FC88D35AE}.Release|x86.ActiveCfg = Release|x64 {4C9F135B-A85E-430C-BAD4-4C67EF5FC12C}.Debug|x64.ActiveCfg = Debug|x64 {4C9F135B-A85E-430C-BAD4-4C67EF5FC12C}.Debug|x64.Build.0 = Debug|x64 + {4C9F135B-A85E-430C-BAD4-4C67EF5FC12C}.Debug|x86.ActiveCfg = Debug|x64 {4C9F135B-A85E-430C-BAD4-4C67EF5FC12C}.Release|x64.ActiveCfg = Release|x64 {4C9F135B-A85E-430C-BAD4-4C67EF5FC12C}.Release|x64.Build.0 = Release|x64 + {4C9F135B-A85E-430C-BAD4-4C67EF5FC12C}.Release|x86.ActiveCfg = Release|x64 {677EA016-1182-440C-9345-DC88D1E98C0C}.Debug|x64.ActiveCfg = Debug|x64 {677EA016-1182-440C-9345-DC88D1E98C0C}.Debug|x64.Build.0 = Debug|x64 + {677EA016-1182-440C-9345-DC88D1E98C0C}.Debug|x86.ActiveCfg = Debug|x64 {677EA016-1182-440C-9345-DC88D1E98C0C}.Release|x64.ActiveCfg = Release|x64 {677EA016-1182-440C-9345-DC88D1E98C0C}.Release|x64.Build.0 = Release|x64 + {677EA016-1182-440C-9345-DC88D1E98C0C}.Release|x86.ActiveCfg = Release|x64 {FF213B23-2C26-4214-9F88-85271E557E87}.Debug|x64.ActiveCfg = Debug|x64 {FF213B23-2C26-4214-9F88-85271E557E87}.Debug|x64.Build.0 = Debug|x64 + {FF213B23-2C26-4214-9F88-85271E557E87}.Debug|x86.ActiveCfg = Debug|x64 {FF213B23-2C26-4214-9F88-85271E557E87}.Release|x64.ActiveCfg = Release|x64 {FF213B23-2C26-4214-9F88-85271E557E87}.Release|x64.Build.0 = Release|x64 + {FF213B23-2C26-4214-9F88-85271E557E87}.Release|x86.ActiveCfg = Release|x64 {EC082900-B4D8-42E9-9663-77F02F6936AE}.Debug|x64.ActiveCfg = Debug|x64 {EC082900-B4D8-42E9-9663-77F02F6936AE}.Debug|x64.Build.0 = Debug|x64 + {EC082900-B4D8-42E9-9663-77F02F6936AE}.Debug|x86.ActiveCfg = Debug|x64 {EC082900-B4D8-42E9-9663-77F02F6936AE}.Release|x64.ActiveCfg = Release|x64 {EC082900-B4D8-42E9-9663-77F02F6936AE}.Release|x64.Build.0 = Release|x64 + {EC082900-B4D8-42E9-9663-77F02F6936AE}.Release|x86.ActiveCfg = Release|x64 {BDB6578B-0691-4E80-A46C-DF21639FD3B8}.Debug|x64.ActiveCfg = Debug|x64 {BDB6578B-0691-4E80-A46C-DF21639FD3B8}.Debug|x64.Build.0 = Debug|x64 + {BDB6578B-0691-4E80-A46C-DF21639FD3B8}.Debug|x86.ActiveCfg = Debug|x64 {BDB6578B-0691-4E80-A46C-DF21639FD3B8}.Release|x64.ActiveCfg = Release|x64 {BDB6578B-0691-4E80-A46C-DF21639FD3B8}.Release|x64.Build.0 = Release|x64 + {BDB6578B-0691-4E80-A46C-DF21639FD3B8}.Release|x86.ActiveCfg = Release|x64 {41279555-F94F-4EBC-99DE-AF863C10C5C4}.Debug|x64.ActiveCfg = Debug|x64 {41279555-F94F-4EBC-99DE-AF863C10C5C4}.Debug|x64.Build.0 = Debug|x64 + {41279555-F94F-4EBC-99DE-AF863C10C5C4}.Debug|x86.ActiveCfg = Debug|x64 {41279555-F94F-4EBC-99DE-AF863C10C5C4}.Release|x64.ActiveCfg = Release|x64 {41279555-F94F-4EBC-99DE-AF863C10C5C4}.Release|x64.Build.0 = Release|x64 + {41279555-F94F-4EBC-99DE-AF863C10C5C4}.Release|x86.ActiveCfg = Release|x64 {93D73454-2512-424E-9CDA-4BB357FE13DD}.Debug|x64.ActiveCfg = Debug|x64 {93D73454-2512-424E-9CDA-4BB357FE13DD}.Debug|x64.Build.0 = Debug|x64 + {93D73454-2512-424E-9CDA-4BB357FE13DD}.Debug|x86.ActiveCfg = Debug|x64 {93D73454-2512-424E-9CDA-4BB357FE13DD}.Release|x64.ActiveCfg = Release|x64 {93D73454-2512-424E-9CDA-4BB357FE13DD}.Release|x64.Build.0 = Release|x64 + {93D73454-2512-424E-9CDA-4BB357FE13DD}.Release|x86.ActiveCfg = Release|x64 {349EE8F9-7D25-4909-AAF5-FF3FADE72187}.Debug|x64.ActiveCfg = Debug|x64 {349EE8F9-7D25-4909-AAF5-FF3FADE72187}.Debug|x64.Build.0 = Debug|x64 + {349EE8F9-7D25-4909-AAF5-FF3FADE72187}.Debug|x86.ActiveCfg = Debug|x64 {349EE8F9-7D25-4909-AAF5-FF3FADE72187}.Release|x64.ActiveCfg = Release|x64 {349EE8F9-7D25-4909-AAF5-FF3FADE72187}.Release|x64.Build.0 = Release|x64 + {349EE8F9-7D25-4909-AAF5-FF3FADE72187}.Release|x86.ActiveCfg = Release|x64 {1970D175-3DE8-4738-942A-4D98D1CDBF64}.Debug|x64.ActiveCfg = Debug|x64 {1970D175-3DE8-4738-942A-4D98D1CDBF64}.Debug|x64.Build.0 = Debug|x64 + {1970D175-3DE8-4738-942A-4D98D1CDBF64}.Debug|x86.ActiveCfg = Debug|x64 {1970D175-3DE8-4738-942A-4D98D1CDBF64}.Release|x64.ActiveCfg = Release|x64 {1970D175-3DE8-4738-942A-4D98D1CDBF64}.Release|x64.Build.0 = Release|x64 + {1970D175-3DE8-4738-942A-4D98D1CDBF64}.Release|x86.ActiveCfg = Release|x64 {96020103-4BA5-4FD2-B4AA-5B6D24492D4E}.Debug|x64.ActiveCfg = Debug|x64 {96020103-4BA5-4FD2-B4AA-5B6D24492D4E}.Debug|x64.Build.0 = Debug|x64 + {96020103-4BA5-4FD2-B4AA-5B6D24492D4E}.Debug|x86.ActiveCfg = Debug|x64 {96020103-4BA5-4FD2-B4AA-5B6D24492D4E}.Release|x64.ActiveCfg = Release|x64 {96020103-4BA5-4FD2-B4AA-5B6D24492D4E}.Release|x64.Build.0 = Release|x64 + {96020103-4BA5-4FD2-B4AA-5B6D24492D4E}.Release|x86.ActiveCfg = Release|x64 {EC1A314C-5588-4506-9C1E-2E58E5817F75}.Debug|x64.ActiveCfg = Debug|x64 {EC1A314C-5588-4506-9C1E-2E58E5817F75}.Debug|x64.Build.0 = Debug|x64 + {EC1A314C-5588-4506-9C1E-2E58E5817F75}.Debug|x86.ActiveCfg = Debug|x64 {EC1A314C-5588-4506-9C1E-2E58E5817F75}.Release|x64.ActiveCfg = Release|x64 {EC1A314C-5588-4506-9C1E-2E58E5817F75}.Release|x64.Build.0 = Release|x64 + {EC1A314C-5588-4506-9C1E-2E58E5817F75}.Release|x86.ActiveCfg = Release|x64 {A4C423AA-F57C-46C7-A172-D1A777017D29}.Debug|x64.ActiveCfg = Debug|x64 {A4C423AA-F57C-46C7-A172-D1A777017D29}.Debug|x64.Build.0 = Debug|x64 + {A4C423AA-F57C-46C7-A172-D1A777017D29}.Debug|x86.ActiveCfg = Debug|x64 {A4C423AA-F57C-46C7-A172-D1A777017D29}.Release|x64.ActiveCfg = Release|x64 {A4C423AA-F57C-46C7-A172-D1A777017D29}.Release|x64.Build.0 = Release|x64 + {A4C423AA-F57C-46C7-A172-D1A777017D29}.Release|x86.ActiveCfg = Release|x64 {53A5391B-737E-49A8-BC8F-312ADA00736F}.Debug|x64.ActiveCfg = Debug|x64 {53A5391B-737E-49A8-BC8F-312ADA00736F}.Debug|x64.Build.0 = Debug|x64 + {53A5391B-737E-49A8-BC8F-312ADA00736F}.Debug|x86.ActiveCfg = Debug|x64 {53A5391B-737E-49A8-BC8F-312ADA00736F}.Release|x64.ActiveCfg = Release|x64 {53A5391B-737E-49A8-BC8F-312ADA00736F}.Release|x64.Build.0 = Release|x64 + {53A5391B-737E-49A8-BC8F-312ADA00736F}.Release|x86.ActiveCfg = Release|x64 {29F29A19-F141-45AD-9679-5A2923B49DA3}.Debug|x64.ActiveCfg = Debug|x64 {29F29A19-F141-45AD-9679-5A2923B49DA3}.Debug|x64.Build.0 = Debug|x64 + {29F29A19-F141-45AD-9679-5A2923B49DA3}.Debug|x86.ActiveCfg = Debug|x64 {29F29A19-F141-45AD-9679-5A2923B49DA3}.Release|x64.ActiveCfg = Release|x64 {29F29A19-F141-45AD-9679-5A2923B49DA3}.Release|x64.Build.0 = Release|x64 + {29F29A19-F141-45AD-9679-5A2923B49DA3}.Release|x86.ActiveCfg = Release|x64 {76563A7F-1011-4EAD-B667-7BB18D09568E}.Debug|x64.ActiveCfg = Debug|x64 {76563A7F-1011-4EAD-B667-7BB18D09568E}.Debug|x64.Build.0 = Debug|x64 + {76563A7F-1011-4EAD-B667-7BB18D09568E}.Debug|x86.ActiveCfg = Debug|x64 {76563A7F-1011-4EAD-B667-7BB18D09568E}.Release|x64.ActiveCfg = Release|x64 {76563A7F-1011-4EAD-B667-7BB18D09568E}.Release|x64.Build.0 = Release|x64 + {76563A7F-1011-4EAD-B667-7BB18D09568E}.Release|x86.ActiveCfg = Release|x64 {474661E7-C73A-43A6-AFEE-EE1EC433D49E}.Debug|x64.ActiveCfg = Debug|x64 {474661E7-C73A-43A6-AFEE-EE1EC433D49E}.Debug|x64.Build.0 = Debug|x64 + {474661E7-C73A-43A6-AFEE-EE1EC433D49E}.Debug|x86.ActiveCfg = Debug|x64 {474661E7-C73A-43A6-AFEE-EE1EC433D49E}.Release|x64.ActiveCfg = Release|x64 {474661E7-C73A-43A6-AFEE-EE1EC433D49E}.Release|x64.Build.0 = Release|x64 + {474661E7-C73A-43A6-AFEE-EE1EC433D49E}.Release|x86.ActiveCfg = Release|x64 {CBC76802-C128-4B17-BF6C-23B08C313E5E}.Debug|x64.ActiveCfg = Debug|x64 {CBC76802-C128-4B17-BF6C-23B08C313E5E}.Debug|x64.Build.0 = Debug|x64 + {CBC76802-C128-4B17-BF6C-23B08C313E5E}.Debug|x86.ActiveCfg = Debug|x64 {CBC76802-C128-4B17-BF6C-23B08C313E5E}.Release|x64.ActiveCfg = Release|x64 {CBC76802-C128-4B17-BF6C-23B08C313E5E}.Release|x64.Build.0 = Release|x64 + {CBC76802-C128-4B17-BF6C-23B08C313E5E}.Release|x86.ActiveCfg = Release|x64 {BB00605C-125F-4A21-B33B-7BF418322DCB}.Debug|x64.ActiveCfg = Debug|x64 {BB00605C-125F-4A21-B33B-7BF418322DCB}.Debug|x64.Build.0 = Debug|x64 + {BB00605C-125F-4A21-B33B-7BF418322DCB}.Debug|x86.ActiveCfg = Debug|x64 {BB00605C-125F-4A21-B33B-7BF418322DCB}.Release|x64.ActiveCfg = Release|x64 {BB00605C-125F-4A21-B33B-7BF418322DCB}.Release|x64.Build.0 = Release|x64 + {BB00605C-125F-4A21-B33B-7BF418322DCB}.Release|x86.ActiveCfg = Release|x64 {D178061B-84D3-44F9-BEED-EFD18D9033F0}.Debug|x64.ActiveCfg = Debug|x64 {D178061B-84D3-44F9-BEED-EFD18D9033F0}.Debug|x64.Build.0 = Debug|x64 + {D178061B-84D3-44F9-BEED-EFD18D9033F0}.Debug|x86.ActiveCfg = Debug|x64 {D178061B-84D3-44F9-BEED-EFD18D9033F0}.Release|x64.ActiveCfg = Release|x64 {D178061B-84D3-44F9-BEED-EFD18D9033F0}.Release|x64.Build.0 = Release|x64 + {D178061B-84D3-44F9-BEED-EFD18D9033F0}.Release|x86.ActiveCfg = Release|x64 {C636D9D1-82FE-42B5-9987-63B7D4836341}.Debug|x64.ActiveCfg = Debug|x64 {C636D9D1-82FE-42B5-9987-63B7D4836341}.Debug|x64.Build.0 = Debug|x64 + {C636D9D1-82FE-42B5-9987-63B7D4836341}.Debug|x86.ActiveCfg = Debug|x64 {C636D9D1-82FE-42B5-9987-63B7D4836341}.Release|x64.ActiveCfg = Release|x64 {C636D9D1-82FE-42B5-9987-63B7D4836341}.Release|x64.Build.0 = Release|x64 + {C636D9D1-82FE-42B5-9987-63B7D4836341}.Release|x86.ActiveCfg = Release|x64 {8EA11166-6512-44FC-B7A5-A4D1ECC81170}.Debug|x64.ActiveCfg = Debug|x64 {8EA11166-6512-44FC-B7A5-A4D1ECC81170}.Debug|x64.Build.0 = Debug|x64 + {8EA11166-6512-44FC-B7A5-A4D1ECC81170}.Debug|x86.ActiveCfg = Debug|x64 {8EA11166-6512-44FC-B7A5-A4D1ECC81170}.Release|x64.ActiveCfg = Release|x64 {8EA11166-6512-44FC-B7A5-A4D1ECC81170}.Release|x64.Build.0 = Release|x64 + {8EA11166-6512-44FC-B7A5-A4D1ECC81170}.Release|x86.ActiveCfg = Release|x64 {38FEE76F-F347-484B-949C-B4649381CFFB}.Debug|x64.ActiveCfg = Debug|x64 {38FEE76F-F347-484B-949C-B4649381CFFB}.Debug|x64.Build.0 = Debug|x64 + {38FEE76F-F347-484B-949C-B4649381CFFB}.Debug|x86.ActiveCfg = Debug|x64 {38FEE76F-F347-484B-949C-B4649381CFFB}.Release|x64.ActiveCfg = Release|x64 {38FEE76F-F347-484B-949C-B4649381CFFB}.Release|x64.Build.0 = Release|x64 + {38FEE76F-F347-484B-949C-B4649381CFFB}.Release|x86.ActiveCfg = Release|x64 {2C0D058E-DE35-4471-AD99-E68A2CAF9E18}.Debug|x64.ActiveCfg = Debug|x64 {2C0D058E-DE35-4471-AD99-E68A2CAF9E18}.Debug|x64.Build.0 = Debug|x64 + {2C0D058E-DE35-4471-AD99-E68A2CAF9E18}.Debug|x86.ActiveCfg = Debug|x64 {2C0D058E-DE35-4471-AD99-E68A2CAF9E18}.Release|x64.ActiveCfg = Release|x64 {2C0D058E-DE35-4471-AD99-E68A2CAF9E18}.Release|x64.Build.0 = Release|x64 + {2C0D058E-DE35-4471-AD99-E68A2CAF9E18}.Release|x86.ActiveCfg = Release|x64 {5BDF4B91-1491-4FB0-BC27-78E9A8E97DC3}.Debug|x64.ActiveCfg = Debug|x64 {5BDF4B91-1491-4FB0-BC27-78E9A8E97DC3}.Debug|x64.Build.0 = Debug|x64 + {5BDF4B91-1491-4FB0-BC27-78E9A8E97DC3}.Debug|x86.ActiveCfg = Debug|x64 {5BDF4B91-1491-4FB0-BC27-78E9A8E97DC3}.Release|x64.ActiveCfg = Release|x64 {5BDF4B91-1491-4FB0-BC27-78E9A8E97DC3}.Release|x64.Build.0 = Release|x64 + {5BDF4B91-1491-4FB0-BC27-78E9A8E97DC3}.Release|x86.ActiveCfg = Release|x64 {E4BECBAB-9C6E-41AB-BB56-F9D70AB6BE03}.Debug|x64.ActiveCfg = Debug|x64 {E4BECBAB-9C6E-41AB-BB56-F9D70AB6BE03}.Debug|x64.Build.0 = Debug|x64 + {E4BECBAB-9C6E-41AB-BB56-F9D70AB6BE03}.Debug|x86.ActiveCfg = Debug|x64 {E4BECBAB-9C6E-41AB-BB56-F9D70AB6BE03}.Release|x64.ActiveCfg = Release|x64 {E4BECBAB-9C6E-41AB-BB56-F9D70AB6BE03}.Release|x64.Build.0 = Release|x64 + {E4BECBAB-9C6E-41AB-BB56-F9D70AB6BE03}.Release|x86.ActiveCfg = Release|x64 {8498F2FA-5CA6-4169-9971-DE5B1FE6132C}.Debug|x64.ActiveCfg = Debug|x64 {8498F2FA-5CA6-4169-9971-DE5B1FE6132C}.Debug|x64.Build.0 = Debug|x64 + {8498F2FA-5CA6-4169-9971-DE5B1FE6132C}.Debug|x86.ActiveCfg = Debug|x64 {8498F2FA-5CA6-4169-9971-DE5B1FE6132C}.Release|x64.ActiveCfg = Release|x64 {8498F2FA-5CA6-4169-9971-DE5B1FE6132C}.Release|x64.Build.0 = Release|x64 + {8498F2FA-5CA6-4169-9971-DE5B1FE6132C}.Release|x86.ActiveCfg = Release|x64 {4482FD2A-EC43-3FFB-AC20-2E5C54B05EAD}.Debug|x64.ActiveCfg = Debug|x64 {4482FD2A-EC43-3FFB-AC20-2E5C54B05EAD}.Debug|x64.Build.0 = Debug|x64 + {4482FD2A-EC43-3FFB-AC20-2E5C54B05EAD}.Debug|x86.ActiveCfg = Debug|x64 {4482FD2A-EC43-3FFB-AC20-2E5C54B05EAD}.Release|x64.ActiveCfg = Release|x64 {4482FD2A-EC43-3FFB-AC20-2E5C54B05EAD}.Release|x64.Build.0 = Release|x64 + {4482FD2A-EC43-3FFB-AC20-2E5C54B05EAD}.Release|x86.ActiveCfg = Release|x64 {23114507-079A-4418-9707-CFA81A03CA99}.Debug|x64.ActiveCfg = Debug|x64 {23114507-079A-4418-9707-CFA81A03CA99}.Debug|x64.Build.0 = Debug|x64 + {23114507-079A-4418-9707-CFA81A03CA99}.Debug|x86.ActiveCfg = Debug|x64 {23114507-079A-4418-9707-CFA81A03CA99}.Release|x64.ActiveCfg = Release|x64 {23114507-079A-4418-9707-CFA81A03CA99}.Release|x64.Build.0 = Release|x64 + {23114507-079A-4418-9707-CFA81A03CA99}.Release|x86.ActiveCfg = Release|x64 {4C3B2264-EA73-4A7B-9CFE-65B0FD635EBB}.Debug|x64.ActiveCfg = Debug|x64 {4C3B2264-EA73-4A7B-9CFE-65B0FD635EBB}.Debug|x64.Build.0 = Debug|x64 + {4C3B2264-EA73-4A7B-9CFE-65B0FD635EBB}.Debug|x86.ActiveCfg = Debug|x64 {4C3B2264-EA73-4A7B-9CFE-65B0FD635EBB}.Release|x64.ActiveCfg = Release|x64 {4C3B2264-EA73-4A7B-9CFE-65B0FD635EBB}.Release|x64.Build.0 = Release|x64 + {4C3B2264-EA73-4A7B-9CFE-65B0FD635EBB}.Release|x86.ActiveCfg = Release|x64 + {B001D13E-7EAB-4689-842D-801E5ACFFAC5}.Debug|x64.ActiveCfg = Debug|x64 + {B001D13E-7EAB-4689-842D-801E5ACFFAC5}.Debug|x64.Build.0 = Debug|x64 + {B001D13E-7EAB-4689-842D-801E5ACFFAC5}.Debug|x86.ActiveCfg = Debug|x64 + {B001D13E-7EAB-4689-842D-801E5ACFFAC5}.Release|x64.ActiveCfg = Release|x64 + {B001D13E-7EAB-4689-842D-801E5ACFFAC5}.Release|x64.Build.0 = Release|x64 + {B001D13E-7EAB-4689-842D-801E5ACFFAC5}.Release|x86.ActiveCfg = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -307,4 +395,7 @@ Global {23114507-079A-4418-9707-CFA81A03CA99} = {87ADDFF9-5768-4DA2-A33B-2477593D6677} {4C3B2264-EA73-4A7B-9CFE-65B0FD635EBB} = {87ADDFF9-5768-4DA2-A33B-2477593D6677} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {710976F2-1BC7-4F2A-B32D-5DD2BBCB44E1} + EndGlobalSection EndGlobal