mirror of
https://github.com/cemu-project/Cemu.git
synced 2026-10-30 19:24:26 +01:00
UI: Refactor auto-updater and make it more robust
This commit is contained in:
1 parent
7945f102dc
commit
a1cd67ca11
17 files changed
+879
-447
No files matched your search
@@ -142,10 +142,8 @@ if (MACOS_BUNDLE)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(UPDATE_SH_PATH "${CMAKE_SOURCE_DIR}/src/resource/update.sh")
|
||||
set(APP_BUNDLE_DIR "$<TARGET_BUNDLE_DIR:CemuBin>")
|
||||
set(FRAMEWORKS_DIR "${APP_BUNDLE_DIR}/Contents/Frameworks")
|
||||
set(RESOURCES_DIR "${APP_BUNDLE_DIR}/Contents/Resources")
|
||||
|
||||
if (ENABLE_VULKAN)
|
||||
add_custom_command(TARGET CemuBin POST_BUILD
|
||||
@@ -155,14 +153,6 @@ if (MACOS_BUNDLE)
|
||||
)
|
||||
endif()
|
||||
|
||||
add_custom_command(TARGET CemuBin POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
"${UPDATE_SH_PATH}"
|
||||
"${RESOURCES_DIR}/update.sh"
|
||||
COMMAND chmod 755
|
||||
"${RESOURCES_DIR}/update.sh"
|
||||
)
|
||||
|
||||
if (ENABLE_LIBUSB)
|
||||
add_custom_command(TARGET CemuBin POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
|
||||
@@ -242,3 +242,58 @@ void FileStream::SyncReadWriteSeek(bool nextOpIsWrite)
|
||||
|
||||
m_prevOperationWasWrite = nextOpIsWrite;
|
||||
}
|
||||
|
||||
bool FileStream::WriteFileAtomic(const fs::path& path, std::span<uint8> fileData, bool allowTargetFileRename)
|
||||
{
|
||||
std::error_code ec;
|
||||
fs::path altPath = path;
|
||||
altPath.replace_extension( _utf8ToPath(_pathToUtf8(altPath.extension()).append("_tmp")));
|
||||
FileStream* fsAlt = FileStream::createFile2(altPath);
|
||||
if (!fsAlt)
|
||||
return false;
|
||||
if (fsAlt->writeData(fileData.data(), fileData.size()) != fileData.size())
|
||||
{
|
||||
delete fsAlt;
|
||||
fs::remove(altPath, ec);
|
||||
return false;
|
||||
}
|
||||
fsAlt->Flush();
|
||||
delete fsAlt;
|
||||
fs::rename(altPath, path, ec);
|
||||
if (ec)
|
||||
{
|
||||
// the target file might be in use, try to rename it if allowed
|
||||
// this isn't strictly atomic
|
||||
if (allowTargetFileRename)
|
||||
{
|
||||
fs::path backupPath = path;
|
||||
backupPath.replace_extension( _utf8ToPath(_pathToUtf8(backupPath.extension()).append(".backup")));
|
||||
std::error_code ec;
|
||||
fs::rename(path, backupPath, ec);
|
||||
if (ec)
|
||||
{
|
||||
cemuLog_log(LogType::Force, "Failed to atomically replace file {}, error {}", _pathToUtf8(path), ec.message());
|
||||
fs::remove(altPath, ec);
|
||||
return false;
|
||||
}
|
||||
// target file renamed, try to replace it again
|
||||
fs::rename(altPath, path, ec);
|
||||
if (ec)
|
||||
{
|
||||
cemuLog_log(LogType::Force, "Failed to atomically replace file {}, error {}", _pathToUtf8(path), ec.message());
|
||||
fs::rename(backupPath, path, ec); // try to undo rename
|
||||
if (ec)
|
||||
cemuLog_log(LogType::Force, "Failed to restore original file {}, error {}", _pathToUtf8(path), ec.message());
|
||||
fs::remove(altPath, ec);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (ec)
|
||||
{
|
||||
cemuLog_log(LogType::Force, "Failed to atomically replace file {}, error {}", _pathToUtf8(path), ec.message());
|
||||
fs::remove(altPath, ec);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -47,6 +47,9 @@ class FileStream
|
||||
~FileStream();
|
||||
FileStream() {};
|
||||
|
||||
// atomic helper
|
||||
static bool WriteFileAtomic(const fs::path& path, std::span<uint8> fileData, bool allowTargetFileRename = false);
|
||||
|
||||
private:
|
||||
void SyncReadWriteSeek(bool nextOpIsWrite);
|
||||
FileStream(const fs::path& path, bool isOpen, bool isWriteable);
|
||||
|
||||
@@ -185,3 +185,58 @@ FileStream::FileStream(HANDLE hFile)
|
||||
m_hFile = hFile;
|
||||
m_isValid = true;
|
||||
}
|
||||
|
||||
bool FileStream::WriteFileAtomic(const fs::path& path, std::span<uint8> fileData, bool allowTargetFileRename)
|
||||
{
|
||||
std::error_code ec;
|
||||
fs::path altPath = path;
|
||||
altPath.replace_extension( _utf8ToPath(_pathToUtf8(altPath.extension()).append("_tmp")));
|
||||
FileStream* fsAlt = FileStream::createFile2(altPath);
|
||||
if (!fsAlt)
|
||||
return false;
|
||||
if (fsAlt->writeData(fileData.data(), fileData.size()) != fileData.size())
|
||||
{
|
||||
delete fsAlt;
|
||||
fs::remove(altPath, ec);
|
||||
return false;
|
||||
}
|
||||
// only on unix: fsAlt->Flush();
|
||||
delete fsAlt;
|
||||
fs::rename(altPath, path, ec);
|
||||
if (ec)
|
||||
{
|
||||
// the target file might be in use, try to rename it if allowed
|
||||
// this isn't strictly atomic
|
||||
if (allowTargetFileRename)
|
||||
{
|
||||
fs::path backupPath = path;
|
||||
backupPath.replace_extension( _utf8ToPath(_pathToUtf8(backupPath.extension()).append(".backup")));
|
||||
std::error_code ec;
|
||||
fs::rename(path, backupPath, ec);
|
||||
if (ec)
|
||||
{
|
||||
cemuLog_log(LogType::Force, "Failed to atomically replace file {}, error {}", _pathToUtf8(path), ec.message());
|
||||
fs::remove(altPath, ec);
|
||||
return false;
|
||||
}
|
||||
// target file renamed, try to replace it again
|
||||
fs::rename(altPath, path, ec);
|
||||
if (ec)
|
||||
{
|
||||
cemuLog_log(LogType::Force, "Failed to atomically replace file {}, error {}", _pathToUtf8(path), ec.message());
|
||||
fs::rename(backupPath, path, ec); // try to undo rename
|
||||
if (ec)
|
||||
cemuLog_log(LogType::Force, "Failed to restore original file {}, error {}", _pathToUtf8(path), ec.message());
|
||||
fs::remove(altPath, ec);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (ec)
|
||||
{
|
||||
cemuLog_log(LogType::Force, "Failed to atomically replace file {}, error {}", _pathToUtf8(path), ec.message());
|
||||
fs::remove(altPath, ec);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -45,6 +45,9 @@ class FileStream
|
||||
~FileStream();
|
||||
FileStream() = default;
|
||||
|
||||
// atomic helper
|
||||
static bool WriteFileAtomic(const fs::path& path, std::span<uint8> fileData, bool allowTargetFileRename = false);
|
||||
|
||||
private:
|
||||
FileStream(HANDLE hFile);
|
||||
|
||||
|
||||
@@ -408,6 +408,25 @@ int CemuApp::OnExit()
|
||||
#if BOOST_OS_MACOS
|
||||
SDLControllerProvider::ShutdownSDL();
|
||||
#endif
|
||||
// handle restart if requested
|
||||
if (m_restartExecutable.has_value() && !m_restartExecutable->empty() && fs::exists(*m_restartExecutable))
|
||||
{
|
||||
fs::path restartPath = *m_restartExecutable;
|
||||
#if BOOST_OS_WINDOWS
|
||||
PROCESS_INFORMATION pi{};
|
||||
STARTUPINFOW si{};
|
||||
si.cb = sizeof(si);
|
||||
std::wstring cmdline;
|
||||
cmdline = L"\"" + boost::nowide::widen(_pathToUtf8(restartPath)) + L"\"";
|
||||
CreateProcessW(nullptr, (wchar_t*)cmdline.c_str(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi);
|
||||
#elif BOOST_OS_LINUX
|
||||
std::string appPath = _pathToUtf8(restartPath);
|
||||
execlp(appPath.c_str(), appPath.c_str(), (char *)NULL);
|
||||
#elif BOOST_OS_MACOS
|
||||
std::string appPath = _pathToUtf8(restartPath);
|
||||
execlp(appPath.c_str(), appPath.c_str(), (char *)NULL);
|
||||
#endif
|
||||
}
|
||||
#if BOOST_OS_WINDOWS
|
||||
ExitProcess(retValue);
|
||||
#else
|
||||
@@ -647,3 +666,13 @@ void CemuApp::ActivateApp(wxActivateEvent& event)
|
||||
g_window_info.app_active = event.GetActive();
|
||||
event.Skip();
|
||||
}
|
||||
|
||||
void CemuApp::RequestRestart(fs::path executablePath)
|
||||
{
|
||||
m_restartExecutable = executablePath;
|
||||
CallAfter([this, executablePath = std::move(executablePath)]() mutable
|
||||
{
|
||||
if (m_mainFrame && !m_mainFrame->IsBeingDeleted())
|
||||
m_mainFrame->Close(true);
|
||||
});
|
||||
}
|
||||
@@ -23,6 +23,8 @@ public:
|
||||
|
||||
static void InitializeNewMLCOrFail(fs::path mlc);
|
||||
static void InitializeExistingMLCOrFail(fs::path mlc);
|
||||
|
||||
void RequestRestart(fs::path executablePath);
|
||||
private:
|
||||
void LocalizeUI(wxLanguage languageToUse);
|
||||
|
||||
@@ -39,6 +41,7 @@ private:
|
||||
|
||||
wxLocale m_locale;
|
||||
std::vector<const wxLanguageInfo*> m_availableTranslations;
|
||||
std::optional<fs::path> m_restartExecutable;
|
||||
};
|
||||
|
||||
wxDECLARE_APP(CemuApp);
|
||||
+540
-384
File diff suppressed because it is too large.
Load diff
@@ -30,16 +30,17 @@ private:
|
||||
void OnGaugeUpdate(wxCommandEvent& event);
|
||||
|
||||
static size_t WriteStringCallback(char* ptr, size_t size, size_t nmemb, void* userdata);
|
||||
static bool QueryUpdateInfo(std::string& downloadUrlOut, std::string& changelogUrlOut);
|
||||
static bool QueryUpdateInfo(std::string& downloadUrlOut, std::string& changelogUrlOut, std::array<uint8, 32>& fileSha256Out);
|
||||
static bool CheckVersion();
|
||||
|
||||
static int ProgressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow);
|
||||
bool DownloadCemuZip(const std::string& url, const fs::path& filename);
|
||||
bool ExtractUpdate(const fs::path& zipname, const fs::path& targetpath, std::string& cemuFolderName);
|
||||
bool DownloadCemuUpdateFile(const std::string& url, const fs::path& filename);
|
||||
bool ExtractZipUpdate(const fs::path& zipname, const fs::path& targetpath);
|
||||
|
||||
enum class WorkerOrder
|
||||
{
|
||||
Idle,
|
||||
InProgress, // work command received
|
||||
Exit,
|
||||
CheckVersion,
|
||||
UpdateVersion,
|
||||
@@ -57,10 +58,24 @@ private:
|
||||
};
|
||||
std::mutex m_mutex;
|
||||
std::condition_variable m_condition;
|
||||
WorkerOrder m_order = WorkerOrder::CheckVersion;
|
||||
std::atomic<WorkerOrder> m_order = WorkerOrder::CheckVersion;
|
||||
void WorkerThread();
|
||||
|
||||
void SubmitWorkerResult(Result newWorkerState);
|
||||
|
||||
#if BOOST_OS_WINDOWS
|
||||
bool WorkerThread_Windows();
|
||||
#endif
|
||||
#if BOOST_OS_LINUX
|
||||
bool WorkerThread_AppImage();
|
||||
#endif
|
||||
#if BOOST_OS_MACOS
|
||||
bool WorkerThread_MacBundle();
|
||||
#endif
|
||||
|
||||
|
||||
std::string m_downloadUrl, m_changelogUrl;
|
||||
std::array<uint8, 32> m_fileSHA256;
|
||||
int m_gaugeMaxValue = 0;
|
||||
|
||||
std::thread m_thread;
|
||||
|
||||
@@ -200,15 +200,16 @@ void ChecksumTool::LoadOnlineData() const
|
||||
|
||||
// check latest version
|
||||
/*
|
||||
https://api.github.com/repos/teamcemu/title-checksums/branches/master
|
||||
https://api.github.com/repos/teamcemu/title-checksums/commits?per_page=1
|
||||
https://api.github.com/repos/cemu-project/title-checksums/branches/master
|
||||
https://api.github.com/repos/cemu-project/title-checksums/commits?per_page=1
|
||||
*/
|
||||
std::string data;
|
||||
auto* curl = curl_easy_init();
|
||||
curl_easy_setopt(curl, CURLOPT_URL, "https://api.github.com/repos/teamcemu/title-checksums/commits/master");
|
||||
curl_easy_setopt(curl, CURLOPT_URL, "https://api.github.com/repos/cemu-project/title-checksums/commits/master");
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, BUILD_VERSION_WITH_NAME_STRING);
|
||||
|
||||
curl_easy_perform(curl);
|
||||
@@ -240,10 +241,11 @@ void ChecksumTool::LoadOnlineData() const
|
||||
{
|
||||
std::string data;
|
||||
auto* curl = curl_easy_init();
|
||||
curl_easy_setopt(curl, CURLOPT_URL, "https://github.com/TeamCemu/title-checksums/archive/master.zip");
|
||||
curl_easy_setopt(curl, CURLOPT_URL, "https://github.com/cemu-project/title-checksums/archive/master.zip");
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, BUILD_VERSION_WITH_NAME_STRING);
|
||||
|
||||
@@ -258,7 +260,7 @@ void ChecksumTool::LoadOnlineData() const
|
||||
zip_error_t error;
|
||||
zip_error_init(&error);
|
||||
zip_source_t* src;
|
||||
if ((src = zip_source_buffer_create(data.data(), data.size(), 1, &error)) == nullptr)
|
||||
if ((src = zip_source_buffer_create(data.data(), data.size(), 0, &error)) == nullptr)
|
||||
{
|
||||
zip_error_fini(&error);
|
||||
return;
|
||||
@@ -267,6 +269,8 @@ void ChecksumTool::LoadOnlineData() const
|
||||
auto* za = zip_open_from_source(src, ZIP_RDONLY, &error);
|
||||
if (!za)
|
||||
{
|
||||
zip_source_free(src);
|
||||
zip_error_fini(&error);
|
||||
wxQueueEvent(m_verify_online, new wxCommandEvent(wxEVT_ENABLE));
|
||||
return;
|
||||
}
|
||||
@@ -324,6 +328,9 @@ void ChecksumTool::LoadOnlineData() const
|
||||
zip_fclose(zipFile);
|
||||
}
|
||||
|
||||
zip_discard(za);
|
||||
zip_error_fini(&error);
|
||||
|
||||
std::ofstream file(checksum_path / "commit.txt");
|
||||
if (file.is_open())
|
||||
{
|
||||
|
||||
@@ -51,8 +51,9 @@ bool DownloadGraphicPacksWindow::curlDownloadFile(const char *url, curlDownloadF
|
||||
curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, downloadState);
|
||||
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, BUILD_VERSION_WITH_NAME_STRING);
|
||||
downloadState->fileData.resize(0);
|
||||
@@ -120,12 +121,12 @@ void DownloadGraphicPacksWindow::UpdateThread()
|
||||
queryUrl.append("&");
|
||||
sprintf(temp, "t=%u", (uint32)std::chrono::seconds(std::time(NULL)).count()); // add a dynamic part to the url to bypass overly aggressive caching (like some proxies do)
|
||||
queryUrl.append(temp);
|
||||
if (curlDownloadFile(queryUrl.c_str(), &tempDownloadState) && boost::starts_with((const char*)tempDownloadState.fileData.data(), "http"))
|
||||
if (curlDownloadFile(queryUrl.c_str(), &tempDownloadState))
|
||||
{
|
||||
// convert downloaded data to url string
|
||||
githubAPIUrl.assign(tempDownloadState.fileData.cbegin(), tempDownloadState.fileData.cend());
|
||||
}
|
||||
else
|
||||
if (githubAPIUrl.empty())
|
||||
{
|
||||
// cemu api request failed, use hardcoded github url
|
||||
cemuLog_log(LogType::Force, "Graphic pack update request failed or returned invalid URL. Using default repository URL instead");
|
||||
@@ -214,7 +215,7 @@ void DownloadGraphicPacksWindow::UpdateThread()
|
||||
|
||||
// init zip source
|
||||
zip_error_init(&error);
|
||||
if ((src = zip_source_buffer_create(m_downloadState->fileData.data(), m_downloadState->fileData.size(), 1, &error)) == NULL)
|
||||
if ((src = zip_source_buffer_create(m_downloadState->fileData.data(), m_downloadState->fileData.size(), 0, &error)) == NULL)
|
||||
{
|
||||
zip_error_fini(&error);
|
||||
m_threadState = ThreadError;
|
||||
@@ -285,6 +286,7 @@ void DownloadGraphicPacksWindow::UpdateThread()
|
||||
zip_fclose(zipFile);
|
||||
}
|
||||
|
||||
zip_discard(za);
|
||||
zip_error_fini(&error);
|
||||
createGraphicPackDownloadedVersionFile(assetName);
|
||||
m_threadState = ThreadFinished;
|
||||
|
||||
@@ -242,7 +242,6 @@ EVT_COMMAND(wxID_ANY, wxEVT_GAMELIST_BEGIN_UPDATE, MainWindow::OnGameListBeginUp
|
||||
EVT_COMMAND(wxID_ANY, wxEVT_GAMELIST_END_UPDATE, MainWindow::OnGameListEndUpdate)
|
||||
EVT_COMMAND(wxID_ANY, wxEVT_ACCOUNTLIST_REFRESH, MainWindow::OnAccountListRefresh)
|
||||
EVT_COMMAND(wxID_ANY, wxEVT_SET_WINDOW_TITLE, MainWindow::OnSetWindowTitle)
|
||||
|
||||
EVT_COMMAND(wxID_ANY, wxEVT_REQUEST_RECREATE_CANVAS, MainWindow::OnRequestRecreateCanvas)
|
||||
EVT_COMMAND(wxID_ANY, wxEVT_REQUEST_GAME_EXIT, MainWindow::OnRequestGameExit)
|
||||
|
||||
|
||||
@@ -160,6 +160,7 @@ private:
|
||||
void CafeRecreateCanvas() override;
|
||||
void CafePPCProcessExit() override;
|
||||
|
||||
void OnRequestRestart(wxCommandEvent& event);
|
||||
void OnRequestRecreateCanvas(wxCommandEvent& event);
|
||||
void OnRequestGameExit(wxCommandEvent& event);
|
||||
|
||||
|
||||
+11
-28
@@ -198,35 +198,18 @@ void requireConsole()
|
||||
|
||||
void HandlePostUpdate()
|
||||
{
|
||||
// finalize update process
|
||||
// delete update cemu.exe.backup if available
|
||||
const auto filename = ActiveSettings::GetExecutablePath().replace_extension("exe.backup");
|
||||
if (fs::exists(filename))
|
||||
auto exeBackupPath = ActiveSettings::GetExecutablePath();
|
||||
exeBackupPath.replace_extension( _utf8ToPath(_pathToUtf8(exeBackupPath.extension()).append(".backup")));
|
||||
std::error_code ec;
|
||||
if (!fs::exists(exeBackupPath, ec))
|
||||
return;
|
||||
// try to delete update residue, but give up quickly as to not cause a permanent hang
|
||||
// it may succeed on next turn
|
||||
for (sint32 i=0; i<3; i++)
|
||||
{
|
||||
#if BOOST_OS_WINDOWS
|
||||
HANDLE lock;
|
||||
do
|
||||
{
|
||||
lock = CreateMutexW(nullptr, TRUE, L"Global\\cemu_update_lock");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
} while (lock == nullptr);
|
||||
const DWORD wait_result = WaitForSingleObject(lock, 2000);
|
||||
CloseHandle(lock);
|
||||
|
||||
if (wait_result == WAIT_OBJECT_0)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
std::error_code ec;
|
||||
fs::remove(filename, ec);
|
||||
}
|
||||
#else
|
||||
while (fs::exists(filename))
|
||||
{
|
||||
std::error_code ec;
|
||||
fs::remove(filename, ec);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||
}
|
||||
#endif
|
||||
if (fs::remove(exeBackupPath, ec))
|
||||
break;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
APP=$(cd "$(dirname "0")"/;pwd)
|
||||
hdiutil attach $TMPDIR/cemu_update/cemu.dmg
|
||||
cp -rf /Volumes/Cemu/Cemu.app "$APP"
|
||||
hdiutil detach /Volumes/Cemu/
|
||||
|
||||
open -n -a "$APP/Cemu.app"
|
||||
@@ -38,6 +38,7 @@ add_library(CemuUtil
|
||||
helpers/StringParser.h
|
||||
helpers/SystemException.h
|
||||
helpers/TempState.h
|
||||
helpers/ZipArchive.h
|
||||
highresolutiontimer/HighResolutionTimer.cpp
|
||||
highresolutiontimer/HighResolutionTimer.h
|
||||
ImageWriter/bmp.h
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
#pragma once
|
||||
|
||||
#include <zip.h>
|
||||
|
||||
// more secure and robust wrapper over libzip API. Also follows our utf8 conventions for std::string(_view). Read-only access for now
|
||||
class ZipArchive
|
||||
{
|
||||
public:
|
||||
struct ZipFileEntry
|
||||
{
|
||||
std::string_view fullPath;
|
||||
std::string_view filename;
|
||||
};
|
||||
|
||||
ZipArchive(const fs::path& path)
|
||||
{
|
||||
m_zipPath = _pathToUtf8(path);
|
||||
int zeError{0};
|
||||
m_zip = zip_open(m_zipPath.c_str(), ZIP_RDONLY, &zeError);
|
||||
if (!m_zip)
|
||||
cemuLog_log(LogType::Force, "Opening zip \"{}\" failed with error code {}", m_zipPath, zeError);
|
||||
}
|
||||
|
||||
~ZipArchive()
|
||||
{
|
||||
if (m_zip)
|
||||
zip_close(m_zip);
|
||||
}
|
||||
|
||||
bool IsValid()
|
||||
{
|
||||
return m_zip;
|
||||
}
|
||||
|
||||
size_t GetNumEntries()
|
||||
{
|
||||
cemu_assert(m_zip); // zip must be valid to call this
|
||||
zip_int64_t num = zip_get_num_entries(m_zip, 0);
|
||||
if (num < 0)
|
||||
return 0;
|
||||
return static_cast<size_t>(num);
|
||||
}
|
||||
|
||||
bool GetFileEntryByIndex(size_t index, ZipFileEntry& entry)
|
||||
{
|
||||
zip_stat_t st{};
|
||||
if (zip_stat_index(m_zip, (zip_uint64_t)index, 0, &st) != 0)
|
||||
{
|
||||
cemuLog_log(LogType::Force, "Zip has corrupted entry at index {} in zip {}", index, m_zipPath);
|
||||
return false;
|
||||
}
|
||||
// we only return entries with a valid name
|
||||
if (!(st.valid & ZIP_STAT_NAME))
|
||||
return false;
|
||||
size_t nameLen = strlen(st.name);
|
||||
if (nameLen == 0)
|
||||
return false;
|
||||
// reject "." and ".." path components
|
||||
{
|
||||
std::string_view path = st.name;
|
||||
size_t start = 0;
|
||||
while (start <= path.size())
|
||||
{
|
||||
size_t end = path.find('/', start);
|
||||
if (end == std::string_view::npos)
|
||||
end = path.size();
|
||||
const auto component = path.substr(start, end - start);
|
||||
if (component == "." || component == "..")
|
||||
return false;
|
||||
if (end == path.size())
|
||||
break;
|
||||
start = end + 1;
|
||||
}
|
||||
}
|
||||
// reject backward slash in path, its technically allowed on Linux and macOS, but for Windows this is a path delimiter
|
||||
// this also rejects UNC paths like \\server\share\file
|
||||
if (strchr(st.name, '\\'))
|
||||
return false;
|
||||
// reject absolute Unix paths
|
||||
if (st.name[0] == '/')
|
||||
return false;
|
||||
// reject Windows drive paths, e.g. C:/foo
|
||||
if (nameLen >= 2 &&
|
||||
((st.name[0] >= 'A' && st.name[0] <= 'Z') || (st.name[0] >= 'a' && st.name[0] <= 'z')) &&
|
||||
st.name[1] == ':')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// an entry may also be an explicit directory, indicated by a slash at the end
|
||||
// we do not return those as files either
|
||||
bool isDir = st.name[nameLen-1] == '/';
|
||||
if (isDir)
|
||||
return false;
|
||||
entry = {};
|
||||
entry.fullPath = st.name;
|
||||
// extract filename from full path
|
||||
auto pos = entry.fullPath.find_last_of('/');
|
||||
if (pos != std::string_view::npos)
|
||||
entry.filename = entry.fullPath.substr(pos+1);
|
||||
else
|
||||
entry.filename = entry.fullPath;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ExtractIntoMemoryByIndex(size_t index, std::vector<uint8>& data)
|
||||
{
|
||||
zip_file_t* zf = zip_fopen_index(m_zip, index, 0);
|
||||
if (!zf)
|
||||
{
|
||||
cemuLog_log(LogType::Force, "Failed to open file {} in zip {}", index, m_zipPath);
|
||||
cemu_assert_suspicious();
|
||||
return false;
|
||||
}
|
||||
data.clear();
|
||||
uint8 tmpBuffer[1024*4];
|
||||
while (true)
|
||||
{
|
||||
// keep calling zip_fread until end of file is reached
|
||||
zip_int64_t r = zip_fread(zf, tmpBuffer, 1024*4);
|
||||
if (r == 0)
|
||||
break;
|
||||
if (r < 0)
|
||||
{
|
||||
cemuLog_log(LogType::Force, "Read error while reading file at index {} in zip {}", index, m_zipPath);
|
||||
cemu_assert_suspicious();
|
||||
zip_fclose(zf);
|
||||
return false;
|
||||
}
|
||||
data.insert(data.end(), tmpBuffer, tmpBuffer + r);
|
||||
}
|
||||
zip_fclose(zf);
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
zip_t* m_zip{};
|
||||
std::string m_zipPath;
|
||||
};
|
||||
Reference in new issue
Block a user