From 24de43b8a22f116922c05b12e5538f7045370130 Mon Sep 17 00:00:00 2001 From: shadow <81448108+shdwmtr@users.noreply.github.com> Date: Mon, 30 Dec 2024 23:38:45 -0400 Subject: [PATCH] feat: Add embedded updater --- src/api/executor.cc | 6 +- src/core/co_initialize/co_stub.cc | 5 +- src/core/ffi/ffi.h | 18 ++-- src/core/ffi/javascript.cc | 2 +- src/core/hooks/csp_bypass.h | 2 +- src/git/vcs.h | 91 ----------------- src/main.cc | 38 ++----- win32/CMakeLists.txt | 11 +++ win32/http.h | 82 ++++++++++++++++ win32/main.cc | 114 ++++++++++++++++++--- win32/steam.h | 21 ++++ win32/unzip.h | 158 ++++++++++++++++++++++++++++++ 12 files changed, 397 insertions(+), 151 deletions(-) delete mode 100644 src/git/vcs.h create mode 100644 win32/http.h create mode 100644 win32/steam.h create mode 100644 win32/unzip.h diff --git a/src/api/executor.cc b/src/api/executor.cc index 310f011f..1109374f 100644 --- a/src/api/executor.cc +++ b/src/api/executor.cc @@ -117,11 +117,11 @@ PyObject* CallFrontendMethod(PyObject* self, PyObject* args, PyObject* kwargs) return JavaScript::EvaluateFromSocket( // Check the the frontend code is actually loaded aside from SteamUI fmt::format( - "if (typeof MillenniumFrontEndError === 'undefined') {{ class MillenniumFrontEndError extends Error {{ constructor(message) {{ super(message); this.name = 'MillenniumFrontEndError'; }} }} }}" - "if (typeof PLUGIN_LIST === 'undefined' || !PLUGIN_LIST?.['{}']) throw new MillenniumFrontEndError('frontend not loaded yet!');\n\n{}", + "if (typeof window !== 'undefined' && typeof window.MillenniumFrontEndError === 'undefined') {{ window.MillenniumFrontEndError = class MillenniumFrontEndError extends Error {{ constructor(message) {{ super(message); this.name = 'MillenniumFrontEndError'; }} }} }}" + "if (typeof PLUGIN_LIST === 'undefined' || !PLUGIN_LIST?.['{}']) throw new window.MillenniumFrontEndError('frontend not loaded yet!');\n\n{}", pluginName, script - ) + ) ); } diff --git a/src/core/co_initialize/co_stub.cc b/src/core/co_initialize/co_stub.cc index df697368..8935533e 100644 --- a/src/core/co_initialize/co_stub.cc +++ b/src/core/co_initialize/co_stub.cc @@ -274,8 +274,7 @@ void OnBackendLoad(uint16_t ftpPort, uint16_t ipcPort) auto socketEmitterThread = std::thread([&] { - JavaScript::SharedJSMessageEmitter::InstanceRef().OnMessage("msg", - [&] (const nlohmann::json& eventMessage, int listenerId) + JavaScript::SharedJSMessageEmitter::InstanceRef().OnMessage("msg", "OnBackendLoad", [&](const nlohmann::json& eventMessage, std::string listenerId) { std::unique_lock lock(mtx); @@ -349,7 +348,7 @@ const void CoInitializer::InjectFrontendShims(uint16_t ftpPort, uint16_t ipcPort Logger.Log("Preparing to inject frontend shims..."); - JavaScript::SharedJSMessageEmitter::InstanceRef().OnMessage("msg", [&](const nlohmann::json& eventMessage, int listenerId) + JavaScript::SharedJSMessageEmitter::InstanceRef().OnMessage("msg", "InjectFrontendShims", [&](const nlohmann::json& eventMessage, std::string listenerId) { std::lock_guard lock(mtx); diff --git a/src/core/ffi/ffi.h b/src/core/ffi/ffi.h index 8b640044..867b8476 100644 --- a/src/core/ffi/ffi.h +++ b/src/core/ffi/ffi.h @@ -58,15 +58,14 @@ namespace JavaScript { Types type; }; - using EventHandler = std::function; + using EventHandler = std::function; class SharedJSMessageEmitter { private: SharedJSMessageEmitter() {} - std::unordered_map>> events; + std::unordered_map>> events; std::unordered_map> missedMessages; // New data structure for missed messages - int nextListenerId = 0; public: SharedJSMessageEmitter(const SharedJSMessageEmitter&) = delete; @@ -77,22 +76,21 @@ namespace JavaScript { return InstanceRef; } - int OnMessage(const std::string& event, EventHandler handler) { - int listenerId = nextListenerId++; - events[event].push_back(std::make_pair(listenerId, handler)); + std::string OnMessage(const std::string& event, const std::string name, EventHandler handler) { + events[event].push_back(std::make_pair(name, handler)); // Deliver any missed messages auto it = missedMessages.find(event); if (it != missedMessages.end()) { for (const auto message : it->second) { - handler(message, listenerId); + handler(message, name); } missedMessages.erase(it); // Clear missed messages once delivered } - return listenerId; + return name; } - void RemoveListener(const std::string& event, int listenerId) { + void RemoveListener(const std::string& event, std::string listenerId) { auto it = events.find(event); if (it != events.end()) { auto& handlers = it->second; @@ -113,7 +111,7 @@ namespace JavaScript { try { handler.second(data, handler.first); } catch (const std::bad_function_call& e) { - Logger.Warn("Failed to emit message. exception: {}", e.what()); + Logger.Warn("Failed to emit message on {}. exception: {}", handler.first, e.what()); } } } else { diff --git a/src/core/ffi/javascript.cc b/src/core/ffi/javascript.cc index c091395c..0b9c55fd 100644 --- a/src/core/ffi/javascript.cc +++ b/src/core/ffi/javascript.cc @@ -34,7 +34,7 @@ const EvalResult ExecuteOnSharedJsContext(std::string javaScriptEval) throw std::runtime_error("couldn't send message to socket"); } - int listenerId = JavaScript::SharedJSMessageEmitter::InstanceRef().OnMessage("msg", [&](const nlohmann::json& eventMessage, int listenerId) + std::string listenerId = JavaScript::SharedJSMessageEmitter::InstanceRef().OnMessage("msg", "ExecuteOnSharedJsContext", [&](const nlohmann::json& eventMessage, std::string listenerId) { std::lock_guard lock(mtx); // Lock mutex for safe access diff --git a/src/core/hooks/csp_bypass.h b/src/core/hooks/csp_bypass.h index 088cd636..9b77c3f8 100644 --- a/src/core/hooks/csp_bypass.h +++ b/src/core/hooks/csp_bypass.h @@ -8,7 +8,7 @@ const void BypassCSP(void) // The CSP is implemented by the web server and enforced by the web browser. // The CSP is a set of rules that the web server sends to the web browser to tell it what content is allowed to be loaded - JavaScript::SharedJSMessageEmitter::InstanceRef().OnMessage("msg", [&] (const nlohmann::json& message, int listenerId) + JavaScript::SharedJSMessageEmitter::InstanceRef().OnMessage("msg", "BypassCSP", [&](const nlohmann::json& message, std::string listenerId) { try { diff --git a/src/git/vcs.h b/src/git/vcs.h deleted file mode 100644 index 6f07c1f6..00000000 --- a/src/git/vcs.h +++ /dev/null @@ -1,91 +0,0 @@ -#pragma once -#include -#include - -static constexpr const char* releaseUrl = "https://api.github.com/repos/shdwmtr/millennium/releases"; - -const bool UpdatesEnabled() -{ - std::unique_ptr settingsStore = std::make_unique(); - return settingsStore->GetSetting("check_for_updates", "yes") == "yes"; -} - -const std::string GetLatestVersion() -{ - try - { - nlohmann::json releaseData = nlohmann::json::parse(Http::Get(releaseUrl, false)); - - // find latest non-pre-release version - for (const auto& release : releaseData) - { - if (release.contains("prerelease") && !release["prerelease"].get()) - { - return release["tag_name"].get(); - } - } - } - catch (const nlohmann::json::exception& exception) - { - LOG_ERROR("An error occurred while getting the latest version of Millennium: {}", exception.what()); - } - catch (const std::exception& exception) - { - LOG_ERROR("An error occurred while getting the latest version of Millennium: {}", exception.what()); - } - - return MILLENNIUM_VERSION; -} - -#ifdef _WIN32 -bool RunPowershellCommand(const std::wstring& command) -{ - STARTUPINFO si; - PROCESS_INFORMATION pi; - ZeroMemory(&si, sizeof(si)); - si.cb = sizeof(si); - si.dwFlags = STARTF_USESHOWWINDOW; - si.wShowWindow = SW_HIDE; - ZeroMemory(&pi, sizeof(pi)); - - std::wstring cmdLine = L"cmd.exe /c powershell.exe -NoProfile -ExecutionPolicy Bypass \"" + command + L"\""; - - if (!CreateProcess(NULL, (LPWSTR)cmdLine.c_str(), NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) { - std::wcout << L"CreateProcess failed (" << GetLastError() << L").\n"; - return false; - } - - WaitForSingleObject(pi.hProcess, INFINITE); - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); - - return true; -} -#endif - -const void CheckForUpdates() -{ - if (UpdatesEnabled()) - { - // Check for updates - const std::string latestVersion = GetLatestVersion(); - - if (latestVersion != MILLENNIUM_VERSION) - { - #ifdef _WIN32 - { - Logger.Warn("Upgrading Millennium@{} -> Millennium@{}", MILLENNIUM_VERSION, latestVersion); - RunPowershellCommand(L"iwr -useb https://steambrew.app/update.ps1 | iex"); - } - #elif __linux__ - { - Logger.Warn("An update is available for Millennium. {} -> {}\nRun 'millennium update' to update to the latest version", MILLENNIUM_VERSION, latestVersion); - } - #endif - } - else - { - Logger.Log("Millennium@{} is up to date.", MILLENNIUM_VERSION); - } - } -} \ No newline at end of file diff --git a/src/main.cc b/src/main.cc index c46c2d1b..c484dcb4 100644 --- a/src/main.cc +++ b/src/main.cc @@ -15,38 +15,21 @@ #include #include #include -#include #include -class Preload +const static void VerifyEnvironment() { -public: - Preload() - { - this->VerifyEnvironment(); - } - - ~Preload() { } + const auto filePath = SystemIO::GetSteamPath() / ".cef-enable-remote-debugging"; - const void VerifyEnvironment() + // Steam's CEF Remote Debugger isn't exposed to port 8080 + if (!std::filesystem::exists(filePath)) { - const auto filePath = SystemIO::GetSteamPath() / ".cef-enable-remote-debugging"; - - // Steam's CEF Remote Debugger isn't exposed to port 8080 - if (!std::filesystem::exists(filePath)) - { - std::ofstream(filePath).close(); + std::ofstream(filePath).close(); - Logger.Log("Successfully enabled CEF remote debugging, you can now restart Steam..."); - std::exit(1); - } + Logger.Log("Successfully enabled CEF remote debugging, you can now restart Steam..."); + std::exit(1); } - - const void Start() - { - CheckForUpdates(); - } -}; +} void OnTerminate() { @@ -100,10 +83,7 @@ const static void EntryMain() uint16_t ftpPort = Crow::CreateAsyncServer(); const auto startTime = std::chrono::system_clock::now(); - { - std::unique_ptr preload = std::make_unique(); - preload->Start(); - } + VerifyEnvironment(); std::shared_ptr loader = std::make_shared(startTime, ftpPort); SetPluginLoader(loader); diff --git a/win32/CMakeLists.txt b/win32/CMakeLists.txt index 78eaf421..8803341d 100644 --- a/win32/CMakeLists.txt +++ b/win32/CMakeLists.txt @@ -1,5 +1,7 @@ cmake_minimum_required(VERSION 3.10) +set(BUILD_SHARED_LIBS OFF) + # Set the project name project(ShimDll) @@ -16,9 +18,18 @@ elseif(UNIX AND NOT GITHUB_ACTION_BUILD) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "$ENV{HOME}/.millennium/") endif() +find_package(CURL REQUIRED) # used for web requests. +find_package(unofficial-minizip CONFIG REQUIRED) # used for extracting zip files + +add_compile_definitions( + CURL_STATICLIB +) + # Add the executable add_library(ShimDll SHARED main.cc) +target_link_libraries(ShimDll CURL::libcurl unofficial::minizip::minizip) + set_target_properties(ShimDll PROPERTIES OUTPUT_NAME "user32") set_target_properties(ShimDll PROPERTIES PREFIX "") set_target_properties(ShimDll PROPERTIES NO_EXPORT TRUE) diff --git a/win32/http.h b/win32/http.h new file mode 100644 index 00000000..13583767 --- /dev/null +++ b/win32/http.h @@ -0,0 +1,82 @@ +#include +#include +#include +#include + +size_t write_file_callback(void* contents, size_t size, size_t nmemb, void* userp) { + std::ofstream* file = static_cast(userp); + size_t totalSize = size * nmemb; + file->write(static_cast(contents), totalSize); + return totalSize; +} + +size_t write_callback(char* ptr, size_t size, size_t nmemb, std::string* data) { + data->append(ptr, size * nmemb); + return size * nmemb; +} + +bool download_file(const std::string& url, const std::string& outputFilePath) { + CURL* curl; + CURLcode res; + std::ofstream outputFile; + + curl = curl_easy_init(); + if (!curl) { + std::cerr << "Failed to initialize CURL." << std::endl; + return false; + } + + outputFile.open(outputFilePath, std::ios::binary); + if (!outputFile.is_open()) { + std::cerr << "Failed to open file: " << outputFilePath << std::endl; + curl_easy_cleanup(curl); + return false; + } + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_file_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &outputFile); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "Millennium/1.0"); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + std::cerr << "CURL error: " << curl_easy_strerror(res) << std::endl; + outputFile.close(); + curl_easy_cleanup(curl); + return false; + } + + // Clean up + outputFile.close(); + curl_easy_cleanup(curl); + return true; +} + +const std::string get(const char* url, bool retry = true) { + CURL* curl; + CURLcode res; + std::string response; + curl = curl_easy_init(); + + if (curl) { + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "Millennium/1.0"); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); // Follow redirects + + while (true) { + res = curl_easy_perform(curl); + + if (!retry || res == CURLE_OK) { + break; + } + + std::cerr << "res: " << res << std::endl; + std::this_thread::sleep_for(std::chrono::milliseconds(3)); + } + curl_easy_cleanup(curl); + } + return response; +} \ No newline at end of file diff --git a/win32/main.cc b/win32/main.cc index 9497cdbb..510d1de1 100644 --- a/win32/main.cc +++ b/win32/main.cc @@ -1,28 +1,116 @@ #include +#include +#include +#include +#include +#include "http.h" +#include "unzip.h" +#include "steam.h" -const void ShutdownShim(HINSTANCE hinstDLL) -{ +const void shutdown_shim(HINSTANCE hinstDLL) { FreeLibraryAndExitThread(hinstDLL, 0); } -const void LoadMillennium(HINSTANCE hinstDLL) -{ +std::string get_platform_module(nlohmann::basic_json<> latest_release, const std::string &latest_version) { + for (const auto &asset : latest_release["assets"]) { + if (asset["name"].get() == "millennium-" + latest_version + "-windows-x86_64.zip") { + return asset["browser_download_url"].get(); + } + } + return {}; +} + +void download_latest(nlohmann::basic_json<> latest_release, const std::string &latest_version, std::string steam_path) { + + printf("updating from %s to %s\n", MILLENNIUM_VERSION, latest_version.c_str()); + const std::string download_url = get_platform_module(latest_release, latest_version); + printf("downloading asset: %s\n", download_url.c_str()); + + const std::string download_path = steam_path + "/millennium.zip"; + + if (download_file(download_url, download_path.c_str())) { + printf("successfully downloaded asset...\n"); + + extract_zip(download_path.c_str(), "C:/Program Files (x86)/Steam"); + remove(download_path.c_str()); + + printf("updated to %s\n", latest_version.c_str()); + } + else { + printf("failed to download asset: %s\n", download_url); + } +} + +const void check_for_updates() { + + const auto start = std::chrono::high_resolution_clock::now(); + + printf("checking for updates...\n"); + std::string steam_path = get_steam_path(); + printf("steam path: %s\n", steam_path.c_str()); + + try { + nlohmann::json latest_release; + + printf("fetching releases..."); + std::string releases_str = get("https://api.github.com/repos/shdwmtr/millennium/releases"); + printf(" ok\n"); + printf("parsing releases..."); + const nlohmann::json releases = nlohmann::json::parse(releases_str); + printf(" ok\n"); + + printf("finding latest release..."); + for (const auto &release : releases) { + if (!release["prerelease"].get()) { + latest_release = release; + printf(" ok\n"); + break; + } + } + + const std::string latest_version = latest_release["tag_name"].get(); + printf("latest version: %s\n", latest_version.c_str()); + + if ((!latest_version.empty() && latest_version != MILLENNIUM_VERSION) || !std::filesystem::exists("millennium.dll")) { + download_latest(latest_release, latest_version, steam_path); + } + else { + printf("no updates found\n"); + } + } + catch (const nlohmann::json::exception &e) { + printf("Error parsing JSON: %s\n", e.what()); + } + + const auto end = std::chrono::high_resolution_clock::now(); + const std::chrono::duration elapsed = end - start; + printf("elapsed time: %fs\n", elapsed.count()); +} + +const void load_millennium(HINSTANCE hinstDLL) { + FILE *stream; + AllocConsole(); + freopen_s(&stream, "CONOUT$", "w", stdout); + freopen_s(&stream, "CONOUT$", "w", stderr); + + // check_for_updates(); + printf("finished checking for updates\n"); + HMODULE hMillennium = LoadLibrary(TEXT("millennium.dll")); - if (hMillennium == nullptr) - { + if (hMillennium == nullptr) { MessageBoxA(nullptr, "Failed to load millennium.dll", "Error", MB_ICONERROR); return; } + else { + printf("loaded millennium...\n"); + } - CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)ShutdownShim, hinstDLL, 0, nullptr); + CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)shutdown_shim, hinstDLL, 0, nullptr); } -BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) -{ - if (fdwReason == DLL_PROCESS_ATTACH) - { - LoadMillennium(hinstDLL); +BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { + if (fdwReason == DLL_PROCESS_ATTACH) { + load_millennium(hinstDLL); } - return true; } \ No newline at end of file diff --git a/win32/steam.h b/win32/steam.h new file mode 100644 index 00000000..8f4446e0 --- /dev/null +++ b/win32/steam.h @@ -0,0 +1,21 @@ +#include +#include + +std::string get_steam_path() { + // get the steam path from registry + HKEY hKey; + LONG lRes = RegOpenKeyExA(HKEY_CURRENT_USER, "Software\\Valve\\Steam", 0, KEY_READ, &hKey); + if (lRes != ERROR_SUCCESS) { + return "C:/Program Files (x86)/Steam"; + } + + char buffer[MAX_PATH]; + DWORD bufferSize = sizeof(buffer); + lRes = RegQueryValueExA(hKey, "SteamPath", NULL, NULL, (LPBYTE)buffer, &bufferSize); + if (lRes != ERROR_SUCCESS) { + return "C:/Program Files (x86)/Steam"; + } + + RegCloseKey(hKey); + return std::string(buffer, bufferSize); +} \ No newline at end of file diff --git a/win32/unzip.h b/win32/unzip.h new file mode 100644 index 00000000..230e6bc1 --- /dev/null +++ b/win32/unzip.h @@ -0,0 +1,158 @@ +#include +#include +#include +#include +#include +#include + +#define WRITE_BUFFER_SIZE 8192 + +int is_symlink(const char *path) { +#ifdef _WIN32 + DWORD attributes = GetFileAttributesA(path); + if (attributes == INVALID_FILE_ATTRIBUTES) { + return 0; + } + return (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; +#else + struct stat path_stat; + if (lstat(path, &path_stat) == -1) { + return 0; + } + return S_ISLNK(path_stat.st_mode); +#endif +} + +int is_any_parent_symlink(const char *path) { + char tmp[512]; + snprintf(tmp, sizeof(tmp), "%s", path); + + char *slash = strrchr(tmp, '/'); + while (slash) { + *slash = '\0'; + if (is_symlink(tmp)) { + return 1; + } + slash = strrchr(tmp, '/'); + } + + return 0; +} + +void create_directories(const char *path) { + char tmp[512]; + snprintf(tmp, sizeof(tmp), "%s", path); + size_t len = strlen(tmp); + + if (tmp[len - 1] == '/' || tmp[len - 1] == '\\') { + tmp[len - 1] = '\0'; + } + + for (char *p = tmp + 1; *p; p++) { + if (*p == '/' || *p == '\\') { + *p = '\0'; + _mkdir(tmp); + *p = '/'; + } + } + _mkdir(tmp); +} + +void extract_file(unzFile zipfile, const char *filename) { + if (is_symlink(filename)) { + printf("Skipping symlink: %s\n", filename); + return; + } + + if (strcmp(filename, "user32.dll") == 0) { + printf("Skipping file: %s\n", filename); + return; + } + + char buffer[WRITE_BUFFER_SIZE]; + FILE *outfile = fopen(filename, "wb"); + if (!outfile) { + fprintf(stderr, "Error opening file %s for writing\n", filename); + return; + } + + int bytes_read; + do { + bytes_read = unzReadCurrentFile(zipfile, buffer, WRITE_BUFFER_SIZE); + if (bytes_read < 0) { + fprintf(stderr, "Error reading file %s from zip archive\n", filename); + fclose(outfile); + return; + } + if (bytes_read > 0) { + fwrite(buffer, 1, bytes_read, outfile); + } + } + while (bytes_read > 0); + fclose(outfile); +} + +void extract_zip(const char *zip_filename, const char *output_dir) { + unzFile zipfile = unzOpen(zip_filename); + if (!zipfile) { + fprintf(stderr, "Error: Cannot open zip file %s\n", zip_filename); + return; + } + + if (unzGoToFirstFile(zipfile) != UNZ_OK) { + fprintf(stderr, "Error: Cannot find the first file in %s\n", zip_filename); + unzClose(zipfile); + return; + } + + do { + char filename[256]; + unz_file_info file_info; + + if (unzGetCurrentFileInfo(zipfile, &file_info, filename, sizeof(filename), NULL, 0, NULL, 0) != UNZ_OK) { + fprintf(stderr, "Error reading file info in zip archive\n"); + break; + } + + char output_path[512]; + snprintf(output_path, sizeof(output_path), "%s/%s", output_dir, filename); + + if (strcmp(filename, "user32.dll") == 0) { + // rename it user32.queue.dll + snprintf(output_path, sizeof(output_path), "%s/%s", output_dir, "user32.queue.dll"); + snprintf(filename, sizeof(filename), "%s", "user32.queue.dll"); + } + + if (is_any_parent_symlink(output_path)) { + // printf("Skipping file due to symlink in parent directory: %s\n", output_path); + continue; + } + + if (filename[strlen(filename) - 1] == '/' || filename[strlen(filename) - 1] == '\\') { + create_directories(output_path); + continue; + } + + char *slash = strrchr(output_path, '/'); + if (slash) { + *slash = '\0'; + create_directories(output_path); + *slash = '/'; + } + + if (is_symlink(output_path)) { + printf("Skipping symlink: %s\n", output_path); + continue; + } + + if (unzOpenCurrentFile(zipfile) != UNZ_OK) { + fprintf(stderr, "Error opening file %s in zip archive\n", filename); + break; + } + + extract_file(zipfile, output_path); + unzCloseCurrentFile(zipfile); + } + while (unzGoToNextFile(zipfile) == UNZ_OK); + unzClose(zipfile); +} \ No newline at end of file