diff --git a/CMakeLists.txt b/CMakeLists.txt
index 00085f4721..d1ec133da4 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -42,6 +42,7 @@ add_subdirectory(pcsx2)
if (QT_BUILD)
add_subdirectory(pcsx2-qt)
+ add_subdirectory(updater)
endif()
# tests
diff --git a/PCSX2_qt.sln b/PCSX2_qt.sln
index c980ab1867..bdeb0d63a6 100644
--- a/PCSX2_qt.sln
+++ b/PCSX2_qt.sln
@@ -64,6 +64,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "d3d12memalloc", "3rdparty\d
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "lzma", "3rdparty\lzma\lzma.vcxproj", "{A4323327-3F2B-4271-83D9-7F9A3C66B6B2}"
EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "updater", "updater\updater.vcxproj", "{90BBDC04-CC44-4006-B893-06A4FEA8ED47}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug AVX2|x64 = Debug AVX2|x64
@@ -398,6 +400,18 @@ Global
{A4323327-3F2B-4271-83D9-7F9A3C66B6B2}.Release AVX2|x64.Build.0 = Release|x64
{A4323327-3F2B-4271-83D9-7F9A3C66B6B2}.Release|x64.ActiveCfg = Release|x64
{A4323327-3F2B-4271-83D9-7F9A3C66B6B2}.Release|x64.Build.0 = Release|x64
+ {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Debug AVX2|x64.ActiveCfg = Debug|x64
+ {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Debug AVX2|x64.Build.0 = Debug|x64
+ {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Debug|x64.ActiveCfg = Debug|x64
+ {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Debug|x64.Build.0 = Debug|x64
+ {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Devel AVX2|x64.ActiveCfg = Devel|x64
+ {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Devel AVX2|x64.Build.0 = Devel|x64
+ {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Devel|x64.ActiveCfg = Devel|x64
+ {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Devel|x64.Build.0 = Devel|x64
+ {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Release AVX2|x64.ActiveCfg = Release|x64
+ {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Release AVX2|x64.Build.0 = Release|x64
+ {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Release|x64.ActiveCfg = Release|x64
+ {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/updater/CMakeLists.txt b/updater/CMakeLists.txt
new file mode 100644
index 0000000000..5711bbcc2c
--- /dev/null
+++ b/updater/CMakeLists.txt
@@ -0,0 +1,12 @@
+add_executable(updater
+ Updater.cpp
+ Updater.h
+)
+
+target_link_libraries(updater PRIVATE common fmt::fmt lzma)
+
+if(WIN32)
+ target_sources(updater PRIVATE
+ Win32Update.cpp
+ )
+endif()
diff --git a/updater/SZErrors.h b/updater/SZErrors.h
new file mode 100644
index 0000000000..3910a1a760
--- /dev/null
+++ b/updater/SZErrors.h
@@ -0,0 +1,43 @@
+/* PCSX2 - PS2 Emulator for PCs
+ * Copyright (C) 2002-2022 PCSX2 Dev Team
+ *
+ * PCSX2 is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU Lesser General Public License as published by the Free Software Found-
+ * ation, either version 3 of the License, or (at your option) any later version.
+ *
+ * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with PCSX2.
+ * If not, see .
+ */
+
+#pragma once
+
+#include "7zTypes.h"
+
+static inline const char* SZErrorToString(SRes res)
+{
+ // clang-format off
+ switch (res)
+ {
+ case SZ_OK: return "SZ_OK";
+ case SZ_ERROR_DATA: return "SZ_ERROR_DATA";
+ case SZ_ERROR_MEM: return "SZ_ERROR_MEM";
+ case SZ_ERROR_CRC: return "SZ_ERROR_CRC";
+ case SZ_ERROR_UNSUPPORTED: return "SZ_ERROR_UNSUPPORTED";
+ case SZ_ERROR_PARAM: return "SZ_ERROR_PARAM";
+ case SZ_ERROR_INPUT_EOF: return "SZ_ERROR_INPUT_EOF";
+ case SZ_ERROR_OUTPUT_EOF: return "SZ_ERROR_OUTPUT_EOF";
+ case SZ_ERROR_READ: return "SZ_ERROR_READ";
+ case SZ_ERROR_WRITE: return "SZ_ERROR_WRITE";
+ case SZ_ERROR_PROGRESS: return "SZ_ERROR_PROGRESS";
+ case SZ_ERROR_FAIL: return "SZ_ERROR_FAIL";
+ case SZ_ERROR_THREAD: return "SZ_ERROR_THREAD";
+ case SZ_ERROR_ARCHIVE: return "SZ_ERROR_ARCHIVE";
+ case SZ_ERROR_NO_ARCHIVE: return "SZ_ERROR_NO_ARCHIVE";
+ default: return "SZ_UNKNOWN";
+ }
+ // clang-format on
+}
diff --git a/updater/Updater.cpp b/updater/Updater.cpp
new file mode 100644
index 0000000000..087f98120e
--- /dev/null
+++ b/updater/Updater.cpp
@@ -0,0 +1,392 @@
+/* PCSX2 - PS2 Emulator for PCs
+ * Copyright (C) 2002-2022 PCSX2 Dev Team
+ *
+ * PCSX2 is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU Lesser General Public License as published by the Free Software Found-
+ * ation, either version 3 of the License, or (at your option) any later version.
+ *
+ * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with PCSX2.
+ * If not, see .
+ */
+
+#include "Updater.h"
+#include "SZErrors.h"
+
+#include "common/Console.h"
+#include "common/FileSystem.h"
+#include "common/Path.h"
+#include "common/ScopedGuard.h"
+#include "common/StringUtil.h"
+
+#include "7zAlloc.h"
+#include "7zCrc.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifdef _WIN32
+#include
+#endif
+
+static constexpr size_t kInputBufSize = ((size_t)1 << 18);
+static constexpr ISzAlloc g_Alloc = {SzAlloc, SzFree};
+
+static std::FILE* s_file_console_stream;
+static constexpr IConsoleWriter s_file_console_writer = {
+ [](const wxString& fmt) { // WriteRaw
+ auto buf = fmt.ToUTF8();
+ std::fwrite(buf.data(), buf.length(), 1, s_file_console_stream);
+ std::fflush(s_file_console_stream);
+ },
+ [](const wxString& fmt) { // DoWriteLn
+ auto buf = fmt.ToUTF8();
+ std::fwrite(buf.data(), buf.length(), 1, s_file_console_stream);
+ std::fputc('\n', s_file_console_stream);
+ std::fflush(s_file_console_stream);
+ },
+ [](ConsoleColors) { // DoSetColor
+ },
+ [](const wxString& fmt) { // DoWriteFromStdout
+ auto buf = fmt.ToUTF8();
+ std::fwrite(buf.data(), buf.length(), 1, s_file_console_stream);
+ std::fflush(s_file_console_stream);
+ },
+ []() { // Newline
+ std::fputc('\n', s_file_console_stream);
+ std::fflush(s_file_console_stream);
+ },
+ [](const wxString&) { // SetTitle
+ }};
+
+static void CloseConsoleFile()
+{
+ if (s_file_console_stream)
+ std::fclose(s_file_console_stream);
+}
+
+Updater::Updater(ProgressCallback* progress)
+ : m_progress(progress)
+{
+ progress->SetTitle("PCSX2 Update Installer");
+}
+
+Updater::~Updater()
+{
+ if (m_archive_opened)
+ SzArEx_Free(&m_archive, &g_Alloc);
+
+ ISzAlloc_Free(&g_Alloc, m_look_stream.buf);
+
+ if (m_file_opened)
+ File_Close(&m_archive_stream.file);
+}
+
+void Updater::SetupLogging(ProgressCallback* progress, const std::string& destination_directory)
+{
+ const std::string log_path(Path::CombineStdString(destination_directory, "updater.log"));
+ s_file_console_stream = FileSystem::OpenCFile(log_path.c_str(), "w");
+ if (!s_file_console_stream)
+ {
+ progress->DisplayFormattedModalError("Failed to open log file '%s'", log_path.c_str());
+ return;
+ }
+
+ Console_SetActiveHandler(s_file_console_writer);
+ std::atexit(CloseConsoleFile);
+}
+
+bool Updater::Initialize(std::string destination_directory)
+{
+ m_destination_directory = std::move(destination_directory);
+ m_staging_directory = StringUtil::StdStringFromFormat("%s" FS_OSPATH_SEPARATOR_STR "%s",
+ m_destination_directory.c_str(), "UPDATE_STAGING");
+ m_progress->DisplayFormattedInformation("Destination directory: '%s'", m_destination_directory.c_str());
+ m_progress->DisplayFormattedInformation("Staging directory: '%s'", m_staging_directory.c_str());
+ return true;
+}
+
+bool Updater::OpenUpdateZip(const char* path)
+{
+ FileInStream_CreateVTable(&m_archive_stream);
+ LookToRead2_CreateVTable(&m_look_stream, False);
+ CrcGenerateTable();
+
+ m_look_stream.buf = (Byte*)ISzAlloc_Alloc(&g_Alloc, kInputBufSize);
+ if (!m_look_stream.buf)
+ {
+ m_progress->DisplayFormattedError("Failed to allocate input buffer?!");
+ return false;
+ }
+
+ m_look_stream.bufSize = kInputBufSize;
+ m_look_stream.realStream = &m_archive_stream.vt;
+ LookToRead2_Init(&m_look_stream);
+
+#ifdef _WIN32
+ WRes wres = InFile_OpenW(&m_archive_stream.file, StringUtil::UTF8StringToWideString(path).c_str());
+#else
+ WRes wres = InFile_Open(&m_archive_stream.file, path);
+#endif
+ if (wres != 0)
+ {
+ m_progress->DisplayFormattedModalError("Failed to open '%s': %d", path, wres);
+ return false;
+ }
+
+ m_file_opened = true;
+ SzArEx_Init(&m_archive);
+
+ SRes res = SzArEx_Open(&m_archive, &m_look_stream.vt, &g_Alloc, &g_Alloc);
+ if (res != SZ_OK)
+ {
+ m_progress->DisplayFormattedModalError("SzArEx_Open() failed: %s [%d]", SZErrorToString(res), res);
+ return false;
+ }
+
+ m_archive_opened = true;
+ m_progress->SetStatusText("Parsing update zip...");
+ return ParseZip();
+}
+
+bool Updater::RecursiveDeleteDirectory(const char* path)
+{
+#ifdef _WIN32
+ // making this safer on Win32...
+ std::wstring wpath(StringUtil::UTF8StringToWideString(path));
+ wpath += L'\0';
+
+ SHFILEOPSTRUCTW op = {};
+ op.wFunc = FO_DELETE;
+ op.pFrom = wpath.c_str();
+ op.fFlags = FOF_NOCONFIRMATION;
+
+ return (SHFileOperationW(&op) == 0 && !op.fAnyOperationsAborted);
+#else
+ return FileSystem::DeleteDirectory(path, true);
+#endif
+}
+
+bool Updater::ParseZip()
+{
+ std::vector filename_buffer;
+
+ for (u32 file_index = 0; file_index < m_archive.NumFiles; file_index++)
+ {
+ // skip directories, we handle them ourselves
+ if (SzArEx_IsDir(&m_archive, file_index))
+ continue;
+
+ size_t filename_len = SzArEx_GetFileNameUtf16(&m_archive, file_index, nullptr);
+ if (filename_len <= 1)
+ continue;
+
+ filename_buffer.resize(filename_len);
+ SzArEx_GetFileNameUtf16(&m_archive, file_index, filename_buffer.data());
+
+ // TODO: This won't work on Linux (4-byte wchar_t).
+ FileToUpdate entry;
+ entry.file_index = file_index;
+ entry.destination_filename = StringUtil::WideStringToUTF8String(reinterpret_cast(filename_buffer.data()));
+ if (entry.destination_filename.empty())
+ continue;
+
+ // replace forward slashes with backslashes
+ for (size_t i = 0; i < entry.destination_filename.length(); i++)
+ {
+ if (entry.destination_filename[i] == '/' || entry.destination_filename[i] == '\\')
+ entry.destination_filename[i] = FS_OSPATH_SEPARATOR_CHARACTER;
+ }
+
+ // should never have a leading slash. just in case.
+ while (entry.destination_filename[0] == FS_OSPATH_SEPARATOR_CHARACTER)
+ entry.destination_filename.erase(0, 1);
+
+ // skip directories (we sort them out later)
+ if (!entry.destination_filename.empty() && entry.destination_filename.back() != FS_OSPATH_SEPARATOR_CHARACTER)
+ {
+ // skip updater itself, since it was already pre-extracted.
+ if (StringUtil::Strcasecmp(entry.destination_filename.c_str(), "updater.exe") != 0)
+ {
+ m_progress->DisplayFormattedInformation("Found file in zip: '%s'", entry.destination_filename.c_str());
+ m_update_paths.push_back(std::move(entry));
+ }
+ }
+ }
+
+ if (m_update_paths.empty())
+ {
+ m_progress->ModalError("No files found in update zip.");
+ return false;
+ }
+
+ for (const FileToUpdate& ftu : m_update_paths)
+ {
+ const size_t len = ftu.destination_filename.length();
+ for (size_t i = 0; i < len; i++)
+ {
+ if (ftu.destination_filename[i] == FS_OSPATH_SEPARATOR_CHARACTER)
+ {
+ std::string dir(ftu.destination_filename.begin(), ftu.destination_filename.begin() + i);
+ while (!dir.empty() && dir[dir.length() - 1] == FS_OSPATH_SEPARATOR_CHARACTER)
+ dir.erase(dir.length() - 1);
+
+ if (std::find(m_update_directories.begin(), m_update_directories.end(), dir) == m_update_directories.end())
+ m_update_directories.push_back(std::move(dir));
+ }
+ }
+ }
+
+ std::sort(m_update_directories.begin(), m_update_directories.end());
+ for (const std::string& dir : m_update_directories)
+ m_progress->DisplayFormattedDebugMessage("Directory: %s", dir.c_str());
+
+ return true;
+}
+
+bool Updater::PrepareStagingDirectory()
+{
+ if (FileSystem::DirectoryExists(m_staging_directory.c_str()))
+ {
+ m_progress->DisplayFormattedWarning("Update staging directory already exists, removing");
+ if (!RecursiveDeleteDirectory(m_staging_directory.c_str()) ||
+ FileSystem::DirectoryExists(m_staging_directory.c_str()))
+ {
+ m_progress->ModalError("Failed to remove old staging directory");
+ return false;
+ }
+ }
+ if (!FileSystem::CreateDirectoryPath(m_staging_directory.c_str(), false))
+ {
+ m_progress->DisplayFormattedModalError("Failed to create staging directory %s", m_staging_directory.c_str());
+ return false;
+ }
+
+ // create subdirectories in staging directory
+ for (const std::string& subdir : m_update_directories)
+ {
+ m_progress->DisplayFormattedInformation("Creating subdirectory in staging: %s", subdir.c_str());
+
+ const std::string staging_subdir =
+ StringUtil::StdStringFromFormat("%s" FS_OSPATH_SEPARATOR_STR "%s", m_staging_directory.c_str(), subdir.c_str());
+ if (!FileSystem::CreateDirectoryPath(staging_subdir.c_str(), false))
+ {
+ m_progress->DisplayFormattedModalError("Failed to create staging subdirectory %s", staging_subdir.c_str());
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool Updater::StageUpdate()
+{
+ m_progress->SetProgressRange(static_cast(m_update_paths.size()));
+ m_progress->SetProgressValue(0);
+
+ UInt32 block_index = 0xFFFFFFFF; /* it can have any value before first call (if outBuffer = 0) */
+ Byte* out_buffer = 0; /* it must be 0 before first call for each new archive. */
+ size_t out_buffer_size = 0; /* it can have any value before first call (if outBuffer = 0) */
+ ScopedGuard out_buffer_guard([&out_buffer]() {
+ if (out_buffer)
+ ISzAlloc_Free(&g_Alloc, out_buffer);
+ });
+
+ for (const FileToUpdate& ftu : m_update_paths)
+ {
+ m_progress->SetFormattedStatusText("Extracting '%s'...", ftu.destination_filename.c_str());
+ m_progress->DisplayFormattedInformation("Decompressing '%s'...", ftu.destination_filename.c_str());
+
+ size_t out_offset = 0;
+ size_t extracted_size = 0;
+ SRes res = SzArEx_Extract(&m_archive, &m_look_stream.vt, ftu.file_index,
+ &block_index, &out_buffer, &out_buffer_size, &out_offset, &extracted_size, &g_Alloc, &g_Alloc);
+ if (res != SZ_OK)
+ {
+ m_progress->DisplayFormattedModalError("Failed to decompress file '%s' from 7z (file index=%u, error=%s)",
+ ftu.destination_filename.c_str(), ftu.file_index, SZErrorToString(res));
+ return false;
+ }
+
+ m_progress->DisplayFormattedInformation("Writing '%s' to staging (%zu bytes)...", ftu.destination_filename.c_str(), extracted_size);
+
+ const std::string destination_file = StringUtil::StdStringFromFormat(
+ "%s" FS_OSPATH_SEPARATOR_STR "%s", m_staging_directory.c_str(), ftu.destination_filename.c_str());
+ std::FILE* fp = FileSystem::OpenCFile(destination_file.c_str(), "wb");
+ if (!fp)
+ {
+ m_progress->DisplayFormattedModalError("Failed to open staging output file '%s'", destination_file.c_str());
+ return false;
+ }
+
+ const bool wrote_completely = std::fwrite(out_buffer + out_offset, extracted_size, 1, fp) == 1 && std::fflush(fp) == 0;
+ if (std::fclose(fp) != 0 || !wrote_completely)
+ {
+ m_progress->DisplayFormattedModalError("Failed to write output file '%s'", destination_file.c_str());
+ FileSystem::DeleteFilePath(destination_file.c_str());
+ return false;
+ }
+
+ m_progress->IncrementProgressValue();
+ }
+
+ return true;
+}
+
+bool Updater::CommitUpdate()
+{
+ m_progress->SetStatusText("Committing update...");
+
+ // create directories in target
+ for (const std::string& subdir : m_update_directories)
+ {
+ const std::string dest_subdir = StringUtil::StdStringFromFormat("%s" FS_OSPATH_SEPARATOR_STR "%s",
+ m_destination_directory.c_str(), subdir.c_str());
+
+ if (!FileSystem::DirectoryExists(dest_subdir.c_str()) && !FileSystem::CreateDirectoryPath(dest_subdir.c_str(), false))
+ {
+ m_progress->DisplayFormattedModalError("Failed to create target directory '%s'", dest_subdir.c_str());
+ return false;
+ }
+ }
+
+ // move files to target
+ for (const FileToUpdate& ftu : m_update_paths)
+ {
+ const std::string staging_file_name = StringUtil::StdStringFromFormat(
+ "%s" FS_OSPATH_SEPARATOR_STR "%s", m_staging_directory.c_str(), ftu.destination_filename.c_str());
+ const std::string dest_file_name = StringUtil::StdStringFromFormat(
+ "%s" FS_OSPATH_SEPARATOR_STR "%s", m_destination_directory.c_str(), ftu.destination_filename.c_str());
+ m_progress->DisplayFormattedInformation("Moving '%s' to '%s'", staging_file_name.c_str(), dest_file_name.c_str());
+#ifdef _WIN32
+ const bool result =
+ MoveFileExW(StringUtil::UTF8StringToWideString(staging_file_name).c_str(),
+ StringUtil::UTF8StringToWideString(dest_file_name).c_str(), MOVEFILE_REPLACE_EXISTING);
+#else
+ const bool result = (rename(staging_file_name.c_str(), dest_file_name.c_str()) == 0);
+#endif
+ if (!result)
+ {
+ m_progress->DisplayFormattedModalError("Failed to rename '%s' to '%s'", staging_file_name.c_str(),
+ dest_file_name.c_str());
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void Updater::CleanupStagingDirectory()
+{
+ // remove staging directory itself
+ if (!RecursiveDeleteDirectory(m_staging_directory.c_str()))
+ m_progress->DisplayFormattedError("Failed to remove staging directory '%s'", m_staging_directory.c_str());
+}
diff --git a/updater/Updater.h b/updater/Updater.h
new file mode 100644
index 0000000000..b697091c85
--- /dev/null
+++ b/updater/Updater.h
@@ -0,0 +1,66 @@
+/* PCSX2 - PS2 Emulator for PCs
+ * Copyright (C) 2002-2022 PCSX2 Dev Team
+ *
+ * PCSX2 is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU Lesser General Public License as published by the Free Software Found-
+ * ation, either version 3 of the License, or (at your option) any later version.
+ *
+ * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with PCSX2.
+ * If not, see .
+ */
+
+#pragma once
+
+#include "common/ProgressCallback.h"
+
+#include "7z.h"
+#include "7zFile.h"
+
+#include
+#include
+
+class Updater
+{
+public:
+ Updater(ProgressCallback* progress);
+ ~Updater();
+
+ static void SetupLogging(ProgressCallback* progress, const std::string& destination_directory);
+
+ bool Initialize(std::string destination_directory);
+
+ bool OpenUpdateZip(const char* path);
+ bool PrepareStagingDirectory();
+ bool StageUpdate();
+ bool CommitUpdate();
+ void CleanupStagingDirectory();
+
+private:
+ static bool RecursiveDeleteDirectory(const char* path);
+
+ struct FileToUpdate
+ {
+ u32 file_index;
+ std::string destination_filename;
+ };
+
+ bool ParseZip();
+
+ std::string m_destination_directory;
+ std::string m_staging_directory;
+
+ std::vector m_update_paths;
+ std::vector m_update_directories;
+
+ ProgressCallback* m_progress;
+ CFileInStream m_archive_stream = {};
+ CLookToRead2 m_look_stream = {};
+ CSzArEx m_archive = {};
+
+ bool m_file_opened = false;
+ bool m_archive_opened = false;
+};
diff --git a/updater/UpdaterExtractor.h b/updater/UpdaterExtractor.h
new file mode 100644
index 0000000000..608e861c15
--- /dev/null
+++ b/updater/UpdaterExtractor.h
@@ -0,0 +1,166 @@
+/* PCSX2 - PS2 Emulator for PCs
+ * Copyright (C) 2002-2022 PCSX2 Dev Team
+ *
+ * PCSX2 is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU Lesser General Public License as published by the Free Software Found-
+ * ation, either version 3 of the License, or (at your option) any later version.
+ *
+ * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with PCSX2.
+ * If not, see .
+ */
+
+#pragma once
+
+#include "SZErrors.h"
+
+#include "common/FileSystem.h"
+#include "common/ScopedGuard.h"
+#include "common/StringUtil.h"
+
+#include "fmt/core.h"
+
+#if defined(_WIN32)
+#include "7z.h"
+#include "7zAlloc.h"
+#include "7zCrc.h"
+#include "7zFile.h"
+#endif
+
+#include
+#include
+#include
+
+#ifdef _WIN32
+static constexpr char UPDATER_EXECUTABLE[] = "updater.exe";
+static constexpr char UPDATER_ARCHIVE_NAME[] = "update.7z";
+#endif
+
+static inline bool ExtractUpdater(const char* archive_path, const char* destination_path, std::string* error)
+{
+#if defined(_WIN32)
+ static constexpr size_t kInputBufSize = ((size_t)1 << 18);
+ static constexpr ISzAlloc g_Alloc = {SzAlloc, SzFree};
+
+ CFileInStream instream = {};
+ CLookToRead2 lookstream = {};
+ CSzArEx archive = {};
+
+ FileInStream_CreateVTable(&instream);
+ LookToRead2_CreateVTable(&lookstream, False);
+ CrcGenerateTable();
+
+ lookstream.buf = (Byte*)ISzAlloc_Alloc(&g_Alloc, kInputBufSize);
+ if (!lookstream.buf)
+ {
+ *error = "Failed to allocate input buffer?!";
+ return false;
+ }
+
+ lookstream.bufSize = kInputBufSize;
+ lookstream.realStream = &instream.vt;
+ LookToRead2_Init(&lookstream);
+ ScopedGuard buffer_guard([&lookstream]() {
+ ISzAlloc_Free(&g_Alloc, lookstream.buf);
+ });
+
+#ifdef _WIN32
+ WRes wres = InFile_OpenW(&instream.file, StringUtil::UTF8StringToWideString(archive_path).c_str());
+#else
+ WRes wres = InFile_Open(&instream.file, archive_path);
+#endif
+ if (wres != 0)
+ {
+ *error = fmt::format("Failed to open '{0}': {1}", archive_path, wres);
+ return false;
+ }
+
+ ScopedGuard file_guard([&instream]() {
+ File_Close(&instream.file);
+ });
+
+ SzArEx_Init(&archive);
+
+ SRes res = SzArEx_Open(&archive, &lookstream.vt, &g_Alloc, &g_Alloc);
+ if (res != SZ_OK)
+ {
+ *error = fmt::format("SzArEx_Open() failed: {0} [{1}]", SZErrorToString(res), res);
+ return false;
+ }
+ ScopedGuard archive_guard([&archive]() {
+ SzArEx_Free(&archive, &g_Alloc);
+ });
+
+ std::vector filename_buffer;
+ u32 updater_file_index = archive.NumFiles;
+ for (u32 file_index = 0; file_index < archive.NumFiles; file_index++)
+ {
+ if (SzArEx_IsDir(&archive, file_index))
+ continue;
+
+ size_t filename_len = SzArEx_GetFileNameUtf16(&archive, file_index, nullptr);
+ if (filename_len <= 1)
+ continue;
+
+ filename_buffer.resize(filename_len);
+ filename_len = SzArEx_GetFileNameUtf16(&archive, file_index, filename_buffer.data());
+
+ // TODO: This won't work on Linux (4-byte wchar_t).
+ const std::string filename(StringUtil::WideStringToUTF8String(reinterpret_cast(filename_buffer.data())));
+ if (filename != UPDATER_EXECUTABLE)
+ continue;
+
+ updater_file_index = file_index;
+ break;
+ }
+
+ if (updater_file_index == archive.NumFiles)
+ {
+ *error = fmt::format("Updater executable ({}) not found in archive.", UPDATER_EXECUTABLE);
+ return false;
+ }
+
+ UInt32 block_index = 0xFFFFFFFF; /* it can have any value before first call (if outBuffer = 0) */
+ Byte* out_buffer = 0; /* it must be 0 before first call for each new archive. */
+ size_t out_buffer_size = 0; /* it can have any value before first call (if outBuffer = 0) */
+ ScopedGuard out_buffer_guard([&out_buffer]() {
+ if (out_buffer)
+ ISzAlloc_Free(&g_Alloc, out_buffer);
+ });
+
+ size_t out_offset = 0;
+ size_t extracted_size = 0;
+ res = SzArEx_Extract(&archive, &lookstream.vt, updater_file_index,
+ &block_index, &out_buffer, &out_buffer_size, &out_offset, &extracted_size, &g_Alloc, &g_Alloc);
+ if (res != SZ_OK)
+ {
+ *error = fmt::format("Failed to decompress {0} from 7z (file index=%u, error=%s)",
+ UPDATER_EXECUTABLE, updater_file_index, SZErrorToString(res));
+ return false;
+ }
+
+ std::FILE* fp = FileSystem::OpenCFile(destination_path, "wb");
+ if (!fp)
+ {
+ *error = fmt::format("Failed to open '{0}' for writing.", destination_path);
+ return false;
+ }
+
+ const bool wrote_completely = std::fwrite(out_buffer + out_offset, extracted_size, 1, fp) == 1 && std::fflush(fp) == 0;
+ if (std::fclose(fp) != 0 || !wrote_completely)
+ {
+ *error = fmt::format("Failed to write output file '{}'", destination_path);
+ FileSystem::DeleteFilePath(destination_path);
+ return false;
+ }
+
+ error->clear();
+ return true;
+#else
+ *error = "Not supported on this platform";
+ return false;
+#endif
+}
diff --git a/updater/Windows/WindowsUpdater.cpp b/updater/Windows/WindowsUpdater.cpp
new file mode 100644
index 0000000000..dbaa22b0a6
--- /dev/null
+++ b/updater/Windows/WindowsUpdater.cpp
@@ -0,0 +1,395 @@
+/* PCSX2 - PS2 Emulator for PCs
+ * Copyright (C) 2002-2022 PCSX2 Dev Team
+ *
+ * PCSX2 is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU Lesser General Public License as published by the Free Software Found-
+ * ation, either version 3 of the License, or (at your option) any later version.
+ *
+ * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with PCSX2.
+ * If not, see .
+ */
+
+#include "Updater.h"
+#include "Windows/resource.h"
+
+#include "common/FileSystem.h"
+#include "common/Console.h"
+#include "common/StringUtil.h"
+#include "common/ProgressCallback.h"
+#include "common/RedtapeWindows.h"
+
+#include
+#include
+
+class Win32ProgressCallback final : public BaseProgressCallback
+{
+public:
+ Win32ProgressCallback();
+
+ void PushState() override;
+ void PopState() override;
+
+ void SetCancellable(bool cancellable) override;
+ void SetTitle(const char* title) override;
+ void SetStatusText(const char* text) override;
+ void SetProgressRange(u32 range) override;
+ void SetProgressValue(u32 value) override;
+
+ void DisplayError(const char* message) override;
+ void DisplayWarning(const char* message) override;
+ void DisplayInformation(const char* message) override;
+ void DisplayDebugMessage(const char* message) override;
+
+ void ModalError(const char* message) override;
+ bool ModalConfirmation(const char* message) override;
+ void ModalInformation(const char* message) override;
+
+private:
+ enum : int
+ {
+ WINDOW_WIDTH = 600,
+ WINDOW_HEIGHT = 300,
+ WINDOW_MARGIN = 10,
+ SUBWINDOW_WIDTH = WINDOW_WIDTH - 20 - WINDOW_MARGIN - WINDOW_MARGIN,
+ };
+
+ bool Create();
+ void Destroy();
+ void Redraw(bool force);
+ void PumpMessages();
+
+ static LRESULT CALLBACK WndProcThunk(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
+ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
+
+ HWND m_window_hwnd{};
+ HWND m_text_hwnd{};
+ HWND m_progress_hwnd{};
+ HWND m_list_box_hwnd{};
+
+ int m_last_progress_percent = -1;
+};
+
+Win32ProgressCallback::Win32ProgressCallback()
+ : BaseProgressCallback()
+{
+ Create();
+}
+
+void Win32ProgressCallback::PushState()
+{
+ BaseProgressCallback::PushState();
+}
+
+void Win32ProgressCallback::PopState()
+{
+ BaseProgressCallback::PopState();
+ Redraw(true);
+}
+
+void Win32ProgressCallback::SetCancellable(bool cancellable)
+{
+ BaseProgressCallback::SetCancellable(cancellable);
+ Redraw(true);
+}
+
+void Win32ProgressCallback::SetTitle(const char* title)
+{
+ SetWindowTextW(m_window_hwnd, StringUtil::UTF8StringToWideString(title).c_str());
+}
+
+void Win32ProgressCallback::SetStatusText(const char* text)
+{
+ BaseProgressCallback::SetStatusText(text);
+ Redraw(true);
+}
+
+void Win32ProgressCallback::SetProgressRange(u32 range)
+{
+ BaseProgressCallback::SetProgressRange(range);
+ Redraw(false);
+}
+
+void Win32ProgressCallback::SetProgressValue(u32 value)
+{
+ BaseProgressCallback::SetProgressValue(value);
+ Redraw(false);
+}
+
+bool Win32ProgressCallback::Create()
+{
+ static const wchar_t* CLASS_NAME = L"PCSX2Win32ProgressCallbackWindow";
+ static bool class_registered = false;
+
+ if (!class_registered)
+ {
+ InitCommonControls();
+
+ WNDCLASSEX wc = {};
+ wc.cbSize = sizeof(WNDCLASSEX);
+ wc.lpfnWndProc = WndProcThunk;
+ wc.hInstance = GetModuleHandle(nullptr);
+ wc.hIcon = LoadIcon(wc.hInstance, MAKEINTRESOURCE(IDI_ICON1));
+ wc.hIconSm = LoadIcon(wc.hInstance, MAKEINTRESOURCE(IDI_ICON1));
+ wc.hCursor = LoadCursor(NULL, IDC_WAIT);
+ wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
+ wc.lpszClassName = CLASS_NAME;
+ if (!RegisterClassExW(&wc))
+ {
+ MessageBoxW(nullptr, L"Failed to register window class", L"Error", MB_OK);
+ return false;
+ }
+
+ class_registered = true;
+ }
+
+ m_window_hwnd =
+ CreateWindowExW(WS_EX_CLIENTEDGE, CLASS_NAME, L"Win32ProgressCallback", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,
+ CW_USEDEFAULT, WINDOW_WIDTH, WINDOW_HEIGHT, nullptr, nullptr, GetModuleHandle(nullptr), this);
+ if (!m_window_hwnd)
+ {
+ MessageBoxW(nullptr, L"Failed to create window", L"Error", MB_OK);
+ return false;
+ }
+
+ SetWindowLongPtr(m_window_hwnd, GWLP_USERDATA, reinterpret_cast(this));
+ ShowWindow(m_window_hwnd, SW_SHOW);
+ PumpMessages();
+ return true;
+}
+
+void Win32ProgressCallback::Destroy()
+{
+ if (!m_window_hwnd)
+ return;
+
+ DestroyWindow(m_window_hwnd);
+ m_window_hwnd = {};
+ m_text_hwnd = {};
+ m_progress_hwnd = {};
+}
+
+void Win32ProgressCallback::PumpMessages()
+{
+ MSG msg;
+ while (PeekMessageW(&msg, m_window_hwnd, 0, 0, PM_REMOVE))
+ {
+ TranslateMessage(&msg);
+ DispatchMessageW(&msg);
+ }
+}
+
+void Win32ProgressCallback::Redraw(bool force)
+{
+ const int percent =
+ static_cast((static_cast(m_progress_value) / static_cast(m_progress_range)) * 100.0f);
+ if (percent == m_last_progress_percent && !force)
+ {
+ PumpMessages();
+ return;
+ }
+
+ m_last_progress_percent = percent;
+
+ SendMessageW(m_progress_hwnd, PBM_SETRANGE, 0, MAKELPARAM(0, m_progress_range));
+ SendMessageW(m_progress_hwnd, PBM_SETPOS, static_cast(m_progress_value), 0);
+ SetWindowTextW(m_text_hwnd, StringUtil::UTF8StringToWideString(m_status_text).c_str());
+ RedrawWindow(m_text_hwnd, nullptr, nullptr, RDW_INVALIDATE);
+ PumpMessages();
+}
+
+LRESULT CALLBACK Win32ProgressCallback::WndProcThunk(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
+{
+ Win32ProgressCallback* cb;
+ if (msg == WM_CREATE)
+ {
+ const CREATESTRUCTW* cs = reinterpret_cast(lparam);
+ cb = static_cast(cs->lpCreateParams);
+ }
+ else
+ {
+ cb = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
+ }
+
+ return cb->WndProc(hwnd, msg, wparam, lparam);
+}
+
+LRESULT CALLBACK Win32ProgressCallback::WndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
+{
+ switch (msg)
+ {
+ case WM_CREATE:
+ {
+ const CREATESTRUCTA* cs = reinterpret_cast(lparam);
+ HFONT default_font = reinterpret_cast(GetStockObject(ANSI_VAR_FONT));
+ SendMessageW(hwnd, WM_SETFONT, WPARAM(default_font), TRUE);
+
+ int y = WINDOW_MARGIN;
+
+ m_text_hwnd = CreateWindowExW(0, L"Static", nullptr, WS_VISIBLE | WS_CHILD, WINDOW_MARGIN, y, SUBWINDOW_WIDTH, 16,
+ hwnd, nullptr, cs->hInstance, nullptr);
+ SendMessageW(m_text_hwnd, WM_SETFONT, WPARAM(default_font), TRUE);
+ y += 16 + WINDOW_MARGIN;
+
+ m_progress_hwnd = CreateWindowExW(0, PROGRESS_CLASSW, nullptr, WS_VISIBLE | WS_CHILD, WINDOW_MARGIN, y,
+ SUBWINDOW_WIDTH, 32, hwnd, nullptr, cs->hInstance, nullptr);
+ y += 32 + WINDOW_MARGIN;
+
+ m_list_box_hwnd =
+ CreateWindowExW(0, L"LISTBOX", nullptr, WS_VISIBLE | WS_CHILD | WS_VSCROLL | WS_HSCROLL | WS_BORDER | LBS_NOSEL,
+ WINDOW_MARGIN, y, SUBWINDOW_WIDTH, 170, hwnd, nullptr, cs->hInstance, nullptr);
+ SendMessageW(m_list_box_hwnd, WM_SETFONT, WPARAM(default_font), TRUE);
+ y += 170;
+ }
+ break;
+
+ default:
+ return DefWindowProcW(hwnd, msg, wparam, lparam);
+ }
+
+ return 0;
+}
+
+void Win32ProgressCallback::DisplayError(const char* message)
+{
+ Console.Error(message);
+ SendMessageW(m_list_box_hwnd, LB_ADDSTRING, 0, reinterpret_cast(StringUtil::UTF8StringToWideString(message).c_str()));
+ SendMessageW(m_list_box_hwnd, WM_VSCROLL, SB_BOTTOM, 0);
+ PumpMessages();
+}
+
+void Win32ProgressCallback::DisplayWarning(const char* message)
+{
+ Console.Warning(message);
+ SendMessageW(m_list_box_hwnd, LB_ADDSTRING, 0, reinterpret_cast(StringUtil::UTF8StringToWideString(message).c_str()));
+ SendMessageW(m_list_box_hwnd, WM_VSCROLL, SB_BOTTOM, 0);
+ PumpMessages();
+}
+
+void Win32ProgressCallback::DisplayInformation(const char* message)
+{
+ Console.WriteLn(message);
+ SendMessageW(m_list_box_hwnd, LB_ADDSTRING, 0, reinterpret_cast(StringUtil::UTF8StringToWideString(message).c_str()));
+ SendMessageW(m_list_box_hwnd, WM_VSCROLL, SB_BOTTOM, 0);
+ PumpMessages();
+}
+
+void Win32ProgressCallback::DisplayDebugMessage(const char* message)
+{
+ Console.WriteLn(message);
+}
+
+void Win32ProgressCallback::ModalError(const char* message)
+{
+ PumpMessages();
+ MessageBoxW(m_window_hwnd, StringUtil::UTF8StringToWideString(message).c_str(), L"Error", MB_ICONERROR | MB_OK);
+ PumpMessages();
+}
+
+bool Win32ProgressCallback::ModalConfirmation(const char* message)
+{
+ PumpMessages();
+ bool result = MessageBoxW(m_window_hwnd, StringUtil::UTF8StringToWideString(message).c_str(), L"Confirmation", MB_ICONQUESTION | MB_YESNO) == IDYES;
+ PumpMessages();
+ return result;
+}
+
+void Win32ProgressCallback::ModalInformation(const char* message)
+{
+ MessageBoxW(m_window_hwnd, StringUtil::UTF8StringToWideString(message).c_str(), L"Information", MB_ICONINFORMATION | MB_OK);
+}
+
+
+static void WaitForProcessToExit(int process_id)
+{
+ HANDLE hProcess = OpenProcess(SYNCHRONIZE, FALSE, process_id);
+ if (!hProcess)
+ return;
+
+ WaitForSingleObject(hProcess, INFINITE);
+ CloseHandle(hProcess);
+}
+
+#include "UpdaterExtractor.h"
+
+int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nShowCmd)
+{
+ Win32ProgressCallback progress;
+
+ int argc = 0;
+ LPWSTR* argv = CommandLineToArgvW(lpCmdLine, &argc);
+ if (!argv || argc <= 0)
+ {
+ progress.ModalError("Failed to parse command line.");
+ return 1;
+ }
+ if (argc != 4)
+ {
+ progress.ModalError("Expected 4 arguments: parent process id, output directory, update zip, program to "
+ "launch.\n\nThis program is not intended to be run manually, please use the Qt frontend and "
+ "click Help->Check for Updates.");
+ LocalFree(argv);
+ return 1;
+ }
+
+ const int parent_process_id = StringUtil::FromChars(StringUtil::WideStringToUTF8String(argv[0])).value_or(0);
+ const std::string destination_directory = StringUtil::WideStringToUTF8String(argv[1]);
+ const std::string zip_path = StringUtil::WideStringToUTF8String(argv[2]);
+ const std::wstring program_to_launch(argv[3]);
+ LocalFree(argv);
+
+ if (parent_process_id <= 0 || destination_directory.empty() || zip_path.empty() || program_to_launch.empty())
+ {
+ progress.ModalError("One or more parameters is empty.");
+ return 1;
+ }
+
+ Updater::SetupLogging(&progress, destination_directory);
+
+ progress.SetFormattedStatusText("Waiting for parent process %d to exit...", parent_process_id);
+ WaitForProcessToExit(parent_process_id);
+
+ Updater updater(&progress);
+ if (!updater.Initialize(destination_directory))
+ {
+ progress.ModalError("Failed to initialize updater.");
+ return 1;
+ }
+
+ if (!updater.OpenUpdateZip(zip_path.c_str()))
+ {
+ progress.DisplayFormattedModalError("Could not open update zip '%s'. Update not installed.", zip_path.c_str());
+ return 1;
+ }
+
+ if (!updater.PrepareStagingDirectory())
+ {
+ progress.ModalError("Failed to prepare staging directory. Update not installed.");
+ return 1;
+ }
+
+ if (!updater.StageUpdate())
+ {
+ progress.ModalError("Failed to stage update. Update not installed.");
+ return 1;
+ }
+
+ if (!updater.CommitUpdate())
+ {
+ progress.ModalError(
+ "Failed to commit update. Your installation may be corrupted, please re-download a fresh version from GitHub.");
+ return 1;
+ }
+
+ updater.CleanupStagingDirectory();
+
+ progress.ModalInformation("Update complete.");
+
+ progress.DisplayFormattedInformation("Launching '%s'...",
+ StringUtil::WideStringToUTF8String(program_to_launch).c_str());
+ ShellExecuteW(nullptr, L"open", program_to_launch.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
+ return 0;
+}
diff --git a/updater/Windows/resource.h b/updater/Windows/resource.h
new file mode 100644
index 0000000000..312731b487
--- /dev/null
+++ b/updater/Windows/resource.h
@@ -0,0 +1,16 @@
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by duckstation-qt.rc
+//
+#define IDI_ICON1 102
+
+// Next default values for new objects
+//
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NEXT_RESOURCE_VALUE 103
+#define _APS_NEXT_COMMAND_VALUE 40001
+#define _APS_NEXT_CONTROL_VALUE 1001
+#define _APS_NEXT_SYMED_VALUE 101
+#endif
+#endif
diff --git a/updater/Windows/updater.ico b/updater/Windows/updater.ico
new file mode 100644
index 0000000000..5a7a7a5f80
Binary files /dev/null and b/updater/Windows/updater.ico differ
diff --git a/updater/Windows/updater.manifest b/updater/Windows/updater.manifest
new file mode 100644
index 0000000000..4258255b62
--- /dev/null
+++ b/updater/Windows/updater.manifest
@@ -0,0 +1,22 @@
+
+
+
+PCSX2 Updater
+
+
+
+
+
+
\ No newline at end of file
diff --git a/updater/Windows/updater.rc b/updater/Windows/updater.rc
new file mode 100644
index 0000000000..1cd896f3ef
--- /dev/null
+++ b/updater/Windows/updater.rc
@@ -0,0 +1,110 @@
+// Microsoft Visual C++ generated resource script.
+//
+#include "resource.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "winres.h"
+
+/////////////////////////////////////////////////////////////////////////////
+#undef APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// English (Australia) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENA)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_AUS
+#pragma code_page(1252)
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE
+BEGIN
+ "resource.h\0"
+END
+
+2 TEXTINCLUDE
+BEGIN
+ "#include ""winres.h""\r\n"
+ "\0"
+END
+
+3 TEXTINCLUDE
+BEGIN
+ "\r\n"
+ "\0"
+END
+
+#endif // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Version
+//
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION 1,0,0,1
+ PRODUCTVERSION 1,0,0,1
+ FILEFLAGSMASK 0x3fL
+#ifdef _DEBUG
+ FILEFLAGS 0x1L
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS 0x40004L
+ FILETYPE 0x1L
+ FILESUBTYPE 0x0L
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "0c0904b0"
+ BEGIN
+ VALUE "CompanyName", "PCSX2"
+ VALUE "FileDescription", "PCSX2"
+ VALUE "FileVersion", "2.0"
+ VALUE "InternalName", "updater.exe"
+ VALUE "LegalCopyright", "Copyright (C) 2022 PCSX2 Dev Team"
+ VALUE "OriginalFilename", "updater.exe"
+ VALUE "ProductName", "PCSX2 Update Installer"
+ VALUE "ProductVersion", "2.0"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0xc09, 1200
+ END
+END
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Icon
+//
+
+// Icon with lowest ID value placed first to ensure application icon
+// remains consistent on all systems.
+IDI_ICON1 ICON "updater.ico"
+
+#endif // English (Australia) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif // not APSTUDIO_INVOKED
+
diff --git a/updater/updater.vcxproj b/updater/updater.vcxproj
new file mode 100644
index 0000000000..6b2ce5ba39
--- /dev/null
+++ b/updater/updater.vcxproj
@@ -0,0 +1,84 @@
+
+
+
+
+
+ {90BBDC04-CC44-4006-B893-06A4FEA8ED47}
+
+
+
+ Application
+ Unicode
+ $(DefaultPlatformToolset)
+ true
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AllRules.ruleset
+ updater$(BuildString)
+
+
+
+ $(SolutionDir)3rdparty\lzma\include;%(AdditionalIncludeDirectories)
+ $(ProjectDir);%(AdditionalIncludeDirectories)
+ Async
+ NotUsing
+ NoExtensions
+ WIN32_LEAN_AND_MEAN;%(PreprocessorDefinitions)
+ NotSet
+ false
+ true
+ true
+ /Zc:__cplusplus /Zo /utf-8%(AdditionalOptions)
+
+
+ Windows
+ Yes
+
+
+
+
+ {449ad25e-424a-4714-babc-68706cdcc33b}
+
+
+ {a4323327-3f2b-4271-83d9-7f9a3c66b6b2}
+
+
+ {4639972e-424e-4e13-8b07-ca403c481346}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/updater/updater.vcxproj.filters b/updater/updater.vcxproj.filters
new file mode 100644
index 0000000000..a8d3b2125d
--- /dev/null
+++ b/updater/updater.vcxproj.filters
@@ -0,0 +1,37 @@
+
+
+
+
+
+ Windows
+
+
+
+
+
+
+ Windows
+
+
+
+
+
+ {bdeccfd9-a573-4076-b112-e013516c30c8}
+
+
+
+
+ Windows
+
+
+
+
+ Windows
+
+
+
+
+ Windows
+
+
+
\ No newline at end of file