diff --git a/CMakeLists.txt b/CMakeLists.txt index f4d6b073..b0ea8564 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,7 @@ endif() add_subdirectory(core) add_subdirectory(api) + if(NOT HEADLESS) add_subdirectory(ui) endif() @@ -48,3 +49,8 @@ endif() if (NOT DEMO) add_subdirectory(cli) endif() + +# WinDbg installer CLI (standalone, spawned by debuggercore API) +if(WIN32) + add_subdirectory(installer) +endif() diff --git a/api/debuggerapi.h b/api/debuggerapi.h index ebfdde5a..67d653fa 100644 --- a/api/debuggerapi.h +++ b/api/debuggerapi.h @@ -20,6 +20,7 @@ limitations under the License. #include "ffi.h" #include "../vendor/intx/intx.hpp" #include +#include using namespace BinaryNinja; @@ -842,4 +843,22 @@ namespace BinaryNinjaDebuggerAPI { bool CanConnect(Ref data); static std::vector GetAvailableAdapters(Ref data); }; + + + // WinDbg Installer API (Windows only, stubs on other platforms) + struct InstallResult + { + bool success; + std::string errorMessage; // Empty if success, otherwise describes the error + + InstallResult() : success(false) {} + InstallResult(bool s, const std::string& err = "") : success(s), errorMessage(err) {} + }; + + InstallResult InstallWinDbg(const std::string& installPath = "", bool isUpdate = false); + bool IsWinDbgInstalled(const std::string& installPath = ""); + std::string GetWinDbgInstallerPath(); + std::string GetWinDbgInstalledVersion(const std::string& installPath = ""); + std::string GetWinDbgLatestVersion(); + }; // namespace BinaryNinjaDebuggerAPI diff --git a/api/ffi.h b/api/ffi.h index ebc23813..22c74bc0 100644 --- a/api/ffi.h +++ b/api/ffi.h @@ -727,6 +727,20 @@ extern "C" DEBUGGER_FFI_API bool BNDebuggerFunctionExistsInOldView(BNDebuggerController* controller, uint64_t address); + // WinDbg Installer (Windows only) + typedef struct BNDebuggerInstallResult + { + bool success; + char* errorMessage; // NULL if success, otherwise error description (caller must free) + } BNDebuggerInstallResult; + + DEBUGGER_FFI_API BNDebuggerInstallResult BNDebuggerInstallWinDbg(const char* installPath, bool isUpdate); + DEBUGGER_FFI_API void BNDebuggerFreeInstallResult(BNDebuggerInstallResult* result); + DEBUGGER_FFI_API bool BNDebuggerIsWinDbgInstalled(const char* installPath); + DEBUGGER_FFI_API char* BNDebuggerGetWinDbgInstallerPath(void); + DEBUGGER_FFI_API char* BNDebuggerGetWinDbgInstalledVersion(const char* installPath); + DEBUGGER_FFI_API char* BNDebuggerGetWinDbgLatestVersion(void); + #ifdef __cplusplus } #endif diff --git a/api/windbginstaller.cpp b/api/windbginstaller.cpp new file mode 100644 index 00000000..2a8b5a6a --- /dev/null +++ b/api/windbginstaller.cpp @@ -0,0 +1,68 @@ +/* +Copyright 2020-2026 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "debuggerapi.h" + +using namespace BinaryNinjaDebuggerAPI; + + +InstallResult BinaryNinjaDebuggerAPI::InstallWinDbg(const std::string& installPath, bool isUpdate) +{ + BNDebuggerInstallResult ffiResult = BNDebuggerInstallWinDbg(installPath.empty() ? nullptr : installPath.c_str(), isUpdate); + + InstallResult result; + result.success = ffiResult.success; + if (ffiResult.errorMessage) + { + result.errorMessage = ffiResult.errorMessage; + } + + BNDebuggerFreeInstallResult(&ffiResult); + return result; +} + + +bool BinaryNinjaDebuggerAPI::IsWinDbgInstalled(const std::string& installPath) +{ + return BNDebuggerIsWinDbgInstalled(installPath.empty() ? nullptr : installPath.c_str()); +} + + +std::string BinaryNinjaDebuggerAPI::GetWinDbgInstallerPath() +{ + char* path = BNDebuggerGetWinDbgInstallerPath(); + std::string result = path ? path : ""; + BNDebuggerFreeString(path); + return result; +} + + +std::string BinaryNinjaDebuggerAPI::GetWinDbgInstalledVersion(const std::string& installPath) +{ + char* version = BNDebuggerGetWinDbgInstalledVersion(installPath.empty() ? nullptr : installPath.c_str()); + std::string result = version ? version : ""; + BNDebuggerFreeString(version); + return result; +} + + +std::string BinaryNinjaDebuggerAPI::GetWinDbgLatestVersion() +{ + char* version = BNDebuggerGetWinDbgLatestVersion(); + std::string result = version ? version : ""; + BNDebuggerFreeString(version); + return result; +} diff --git a/core/ffi.cpp b/core/ffi.cpp index 5f51c8cb..43db6e8c 100644 --- a/core/ffi.cpp +++ b/core/ffi.cpp @@ -1690,3 +1690,114 @@ bool BNDebuggerFunctionExistsInOldView(BNDebuggerController* controller, uint64_ { return controller->object->FunctionExistsInOldView(address); } + + +// WinDbg Installer FFI implementations (Windows only) +#ifdef WIN32 +#include "windbginstaller.h" + +BNDebuggerInstallResult BNDebuggerInstallWinDbg(const char* installPath, bool isUpdate) +{ + std::string path = installPath ? installPath : ""; + BinaryNinjaDebugger::InstallResult result = InstallWinDbg(path, isUpdate); + + BNDebuggerInstallResult ffiResult; + ffiResult.success = result.success; + if (!result.errorMessage.empty()) + { + ffiResult.errorMessage = BNAllocString(result.errorMessage.c_str()); + } + else + { + ffiResult.errorMessage = nullptr; + } + return ffiResult; +} + +void BNDebuggerFreeInstallResult(BNDebuggerInstallResult* result) +{ + if (result && result->errorMessage) + { + BNFreeString(result->errorMessage); + result->errorMessage = nullptr; + } +} + + +bool BNDebuggerIsWinDbgInstalled(const char* installPath) +{ + std::string path = installPath ? installPath : ""; + return IsWinDbgInstalled(path); +} + + +char* BNDebuggerGetWinDbgInstallerPath(void) +{ + std::string path = GetInstallerPath(); + return BNAllocString(path.c_str()); +} + + +char* BNDebuggerGetWinDbgInstalledVersion(const char* installPath) +{ + std::string path = installPath ? installPath : ""; + std::string version = GetInstalledVersion(path); + return BNAllocString(version.c_str()); +} + + +char* BNDebuggerGetWinDbgLatestVersion(void) +{ + std::string version = GetLatestVersion(); + return BNAllocString(version.c_str()); +} + +#else // !WIN32 + +// Stub implementations for non-Windows platforms +BNDebuggerInstallResult BNDebuggerInstallWinDbg(const char* installPath, bool isUpdate) +{ + (void)installPath; + (void)isUpdate; + BNDebuggerInstallResult result; + result.success = false; + result.errorMessage = BNAllocString("WinDbg installation is only supported on Windows"); + return result; +} + +void BNDebuggerFreeInstallResult(BNDebuggerInstallResult* result) +{ + if (result && result->errorMessage) + { + BNFreeString(result->errorMessage); + result->errorMessage = nullptr; + } +} + + +bool BNDebuggerIsWinDbgInstalled(const char* installPath) +{ + (void)installPath; + return false; +} + + +char* BNDebuggerGetWinDbgInstallerPath(void) +{ + return BNAllocString(""); +} + + +char* BNDebuggerGetWinDbgInstalledVersion(const char* installPath) +{ + (void)installPath; + return BNAllocString(""); +} + + +char* BNDebuggerGetWinDbgLatestVersion(void) +{ + return BNAllocString(""); +} + +#endif // WIN32 diff --git a/core/windbginstaller.cpp b/core/windbginstaller.cpp new file mode 100644 index 00000000..36120685 --- /dev/null +++ b/core/windbginstaller.cpp @@ -0,0 +1,288 @@ +/* +Copyright 2020-2026 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifdef WIN32 + +#include "windbginstaller.h" +#include +#include +#include +#include +#include + +using namespace BinaryNinja; +namespace fs = std::filesystem; + +namespace BinaryNinjaDebugger { + +std::string GetInstallerPath() { + std::string pluginRoot; + if (getenv("BN_STANDALONE_DEBUGGER") != nullptr) + pluginRoot = GetUserPluginDirectory(); + else + pluginRoot = GetBundledPluginDirectory(); + + if (!pluginRoot.empty()) { + fs::path path = fs::path(pluginRoot) / "windbg-installer.exe"; + if (fs::exists(path)) { + return fs::canonical(path).string(); + } + } + + return ""; +} + +bool IsWinDbgInstalled(const std::string& installPath) { + std::string path = installPath; + if (path.empty()) { + /* Use default path */ + char appData[MAX_PATH]; + if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_APPDATA, nullptr, 0, appData))) { + path = std::string(appData) + "\\Binary Ninja\\windbg"; + } + } + + if (path.empty()) { + return false; + } + + /* Check for required DLLs */ + return fs::exists(path + "\\amd64\\dbgeng.dll") && + fs::exists(path + "\\amd64\\dbghelp.dll"); +} + +InstallResult InstallWinDbg(const std::string& installPath, bool isUpdate) { + std::string installerPath = GetInstallerPath(); + if (installerPath.empty()) { + LogError("Could not find windbg-installer.exe"); + return InstallResult(false, "Could not find windbg-installer.exe"); + } + + /* Determine install path for result file */ + std::string targetPath = installPath; + if (targetPath.empty()) { + char appData[MAX_PATH]; + if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_APPDATA, nullptr, 0, appData))) { + targetPath = std::string(appData) + "\\Binary Ninja\\windbg"; + } + } + + /* Build command line */ + std::string cmdLine = "\"" + installerPath + "\" install"; + if (isUpdate) { + cmdLine += " --update"; + } + if (!installPath.empty()) { + cmdLine += " --path \"" + installPath + "\""; + } + + LogInfo("Running: %s", cmdLine.c_str()); + + /* Create process with visible console window */ + STARTUPINFOA si = {}; + si.cb = sizeof(si); + + PROCESS_INFORMATION pi = {}; + + if (!CreateProcessA( + nullptr, + const_cast(cmdLine.c_str()), + nullptr, + nullptr, + FALSE, + CREATE_NEW_CONSOLE, + nullptr, + nullptr, + &si, + &pi)) { + DWORD error = GetLastError(); + char errorMsg[256]; + sprintf_s(errorMsg, sizeof(errorMsg), "Failed to start installer process (error code: %lu)", error); + LogError("%s", errorMsg); + return InstallResult(false, errorMsg); + } + + /* Wait for process to finish */ + WaitForSingleObject(pi.hProcess, INFINITE); + + DWORD exitCode = 0; + GetExitCodeProcess(pi.hProcess, &exitCode); + + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + + if (exitCode == 0) { + return InstallResult(true); + } else { + /* Read error message from result file written by installer CLI */ + std::string errorMessage = "Installation failed"; + + std::string resultPath = targetPath + "\\install_result.json"; + std::ifstream resultFile(resultPath); + if (resultFile.is_open()) { + std::string line; + std::string json; + while (std::getline(resultFile, line)) { + json += line; + } + resultFile.close(); + + /* Parse JSON to extract error message */ + /* Look for "error":"message" */ + size_t errorPos = json.find("\"error\":\""); + if (errorPos != std::string::npos) { + errorPos += 9; /* Skip past "error":" */ + size_t errorEnd = json.find("\"", errorPos); + if (errorEnd != std::string::npos) { + errorMessage = json.substr(errorPos, errorEnd - errorPos); + } + } + + /* Clean up result file */ + fs::remove(resultPath); + } + + LogError("Installation failed: %s", errorMessage.c_str()); + return InstallResult(false, errorMessage); + } +} + +/* Helper function to run installer CLI and capture JSON output */ +static std::string RunInstallerCommand(const std::string& command, const std::string& extraArgs = "") { + std::string installerPath = GetInstallerPath(); + if (installerPath.empty()) { + return ""; + } + + std::string cmdLine = "\"" + installerPath + "\" " + command + " --json"; + if (!extraArgs.empty()) { + cmdLine += " " + extraArgs; + } + + /* Create pipes for stdout */ + SECURITY_ATTRIBUTES sa = {}; + sa.nLength = sizeof(sa); + sa.bInheritHandle = TRUE; + + HANDLE hReadPipe, hWritePipe; + if (!CreatePipe(&hReadPipe, &hWritePipe, &sa, 0)) { + return ""; + } + + /* Ensure read handle is not inherited */ + SetHandleInformation(hReadPipe, HANDLE_FLAG_INHERIT, 0); + + STARTUPINFOA si = {}; + si.cb = sizeof(si); + si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; + si.hStdOutput = hWritePipe; + si.hStdError = hWritePipe; + si.wShowWindow = SW_HIDE; + + PROCESS_INFORMATION pi = {}; + + if (!CreateProcessA( + nullptr, + const_cast(cmdLine.c_str()), + nullptr, + nullptr, + TRUE, /* Inherit handles */ + CREATE_NO_WINDOW, + nullptr, + nullptr, + &si, + &pi)) { + CloseHandle(hReadPipe); + CloseHandle(hWritePipe); + return ""; + } + + /* Close write end in parent */ + CloseHandle(hWritePipe); + + /* Read output */ + std::string output; + char buffer[4096]; + DWORD bytesRead; + while (ReadFile(hReadPipe, buffer, sizeof(buffer) - 1, &bytesRead, nullptr) && bytesRead > 0) { + buffer[bytesRead] = '\0'; + output += buffer; + } + + CloseHandle(hReadPipe); + + WaitForSingleObject(pi.hProcess, INFINITE); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + + return output; +} + +/* Simple JSON value extractor - finds "key":"value" pattern */ +static std::string ExtractJsonValue(const std::string& json, const std::string& key) { + std::string searchKey = "\"" + key + "\":\""; + size_t pos = json.find(searchKey); + if (pos == std::string::npos) { + return ""; + } + pos += searchKey.length(); + size_t endPos = json.find("\"", pos); + if (endPos == std::string::npos) { + return ""; + } + return json.substr(pos, endPos - pos); +} + +std::string GetInstalledVersion(const std::string& installPath) { + /* Read version directly from marker file (fast, no CLI call needed) */ + std::string path = installPath; + if (path.empty()) { + /* Use default path */ + char appData[MAX_PATH]; + if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_APPDATA, nullptr, 0, appData))) { + path = std::string(appData) + "\\Binary Ninja\\windbg"; + } + } + + if (path.empty()) { + return ""; + } + + /* Read from version marker file */ + std::string versionFilePath = path + "\\installed_version.txt"; + std::ifstream versionFile(versionFilePath); + if (versionFile.is_open()) { + std::string version; + std::getline(versionFile, version); + versionFile.close(); + if (!version.empty()) { + return version; + } + } + + /* Version file not found - installation may be from older version or corrupted */ + /* Return empty string rather than calling CLI to keep UI responsive */ + return ""; +} + +std::string GetLatestVersion() { + std::string output = RunInstallerCommand("check-update"); + return ExtractJsonValue(output, "latest"); +} + +} // namespace BinaryNinjaDebugger + +#endif // WIN32 diff --git a/core/windbginstaller.h b/core/windbginstaller.h new file mode 100644 index 00000000..bfed6377 --- /dev/null +++ b/core/windbginstaller.h @@ -0,0 +1,79 @@ +/* +Copyright 2020-2026 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#pragma once + +#ifdef WIN32 + +#include + +namespace BinaryNinjaDebugger { + +/* + * Result of WinDbg installation + */ +struct InstallResult { + bool success; + std::string errorMessage; // Empty if success, otherwise describes the error + + InstallResult() : success(false) {} + InstallResult(bool s, const std::string& err = "") : success(s), errorMessage(err) {} +}; + +/* + * Install or update WinDbg/TTD by spawning the installer CLI + * The installer CLI displays its own progress in a console window + * + * @param installPath Custom install path (empty = default) + * @param isUpdate If true, CLI will wait for Binary Ninja to exit first + * (use when WinDbg DLLs may be loaded) + * @return InstallResult with success status and error message if failed + */ +InstallResult InstallWinDbg(const std::string& installPath = "", bool isUpdate = false); + +/* + * Check if WinDbg is installed at the given path + * + * @param installPath Path to check (empty = default) + * @return true if WinDbg is installed + */ +bool IsWinDbgInstalled(const std::string& installPath = ""); + +/* + * Get the path to the installer CLI executable + * + * @return Path to windbg-installer.exe, or empty if not found + */ +std::string GetInstallerPath(); + +/* + * Get the version of installed WinDbg + * + * @param installPath Path to check (empty = default) + * @return Version string (e.g., "1.2404.24002.0"), or empty if not installed + */ +std::string GetInstalledVersion(const std::string& installPath = ""); + +/* + * Get the latest available WinDbg version from Microsoft + * + * @return Version string, or empty on error + */ +std::string GetLatestVersion(); + +} // namespace BinaryNinjaDebugger + +#endif // WIN32 diff --git a/installer/CMakeLists.txt b/installer/CMakeLists.txt new file mode 100644 index 00000000..d817c287 --- /dev/null +++ b/installer/CMakeLists.txt @@ -0,0 +1,108 @@ +cmake_minimum_required(VERSION 3.13 FATAL_ERROR) + +project(windbg-installer) + +# Only build on Windows +if(NOT WIN32) + message(STATUS "WinDbg installer is Windows-only, skipping") + return() +endif() + +# ============================================================================ +# minizip-ng library (minimal ZIP support) +# ============================================================================ + +set(MINIZIP_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/../vendor/minizip-ng/mz_inflate.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../vendor/minizip-ng/mz_zip.cpp +) + +set(MINIZIP_HEADERS + ${CMAKE_CURRENT_SOURCE_DIR}/../vendor/minizip-ng/mz.h + ${CMAKE_CURRENT_SOURCE_DIR}/../vendor/minizip-ng/mz_inflate.h + ${CMAKE_CURRENT_SOURCE_DIR}/../vendor/minizip-ng/mz_zip.h +) + +add_library(minizip-ng STATIC ${MINIZIP_SOURCES} ${MINIZIP_HEADERS}) + +target_include_directories(minizip-ng PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/../vendor/minizip-ng +) + +# Prevent Windows min/max macros from conflicting with std::min/std::max +target_compile_definitions(minizip-ng PRIVATE NOMINMAX) + +set_target_properties(minizip-ng PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON +) + +# ============================================================================ +# WinDbg installer library (used by standalone installer CLI) +# ============================================================================ + +set(INSTALLER_LIB_SOURCES + http_downloader.cpp + zip_extractor.cpp + windbg_installer.cpp +) + +set(INSTALLER_LIB_HEADERS + http_downloader.h + zip_extractor.h + windbg_installer.h +) + +# Add pugixml source +list(APPEND INSTALLER_LIB_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/../vendor/pugixml/pugixml.cpp +) + +add_library(windbg-installer-lib STATIC ${INSTALLER_LIB_SOURCES} ${INSTALLER_LIB_HEADERS}) + +target_include_directories(windbg-installer-lib PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../vendor/pugixml + ${CMAKE_CURRENT_SOURCE_DIR}/../vendor/minizip-ng +) + +target_link_libraries(windbg-installer-lib + PRIVATE minizip-ng + PRIVATE winhttp.lib + PRIVATE ole32.lib + PRIVATE shell32.lib +) + +set_target_properties(windbg-installer-lib PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON +) + +# ============================================================================ +# Standalone installer executable +# ============================================================================ + +add_executable(windbg-installer main.cpp) + +target_link_libraries(windbg-installer + PRIVATE windbg-installer-lib +) + +set_target_properties(windbg-installer PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON +) + +# Output to plugins directory (same as debuggercore) +if(BN_INTERNAL_BUILD) + set_target_properties(windbg-installer PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${BN_CORE_PLUGIN_DIR} + ) +else() + set_target_properties(windbg-installer PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/out/plugins + ) +endif() + +# Note: The installer is intentionally standalone and does not link with binaryninjaapi. +# Settings configuration is handled by the Binary Ninja UI after installation completes. diff --git a/installer/http_downloader.cpp b/installer/http_downloader.cpp new file mode 100644 index 00000000..27c178af --- /dev/null +++ b/installer/http_downloader.cpp @@ -0,0 +1,286 @@ +/* + * HTTP Downloader with progress callback using WinHTTP + * + * Copyright 2020-2026 Vector 35 Inc. + * Licensed under the Apache License, Version 2.0 + */ + +#ifdef _WIN32 + +#include "http_downloader.h" +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "winhttp.lib") + +namespace WinDbgInstaller { + +namespace { + +/* Log levels */ +enum LogLevel { + LOG_DEBUG = 0, + LOG_INFO = 1, + LOG_WARN = 2, + LOG_ERROR = 3 +}; + +void Log(LogCallback logCallback, int level, const std::string& message) { + if (logCallback) { + logCallback(level, message); + } +} + +/* Parse URL into components */ +struct UrlComponents { + std::wstring host; + std::wstring path; + INTERNET_PORT port; + bool isHttps; +}; + +bool ParseUrl(const std::string& url, UrlComponents& components) { + /* Convert to wide string */ + std::wstring wUrl(url.begin(), url.end()); + + URL_COMPONENTS urlComp = {0}; + urlComp.dwStructSize = sizeof(urlComp); + + wchar_t hostName[256] = {0}; + wchar_t urlPath[2048] = {0}; + + urlComp.lpszHostName = hostName; + urlComp.dwHostNameLength = sizeof(hostName) / sizeof(wchar_t); + urlComp.lpszUrlPath = urlPath; + urlComp.dwUrlPathLength = sizeof(urlPath) / sizeof(wchar_t); + + if (!WinHttpCrackUrl(wUrl.c_str(), (DWORD)wUrl.length(), 0, &urlComp)) { + return false; + } + + components.host = hostName; + components.path = urlPath; + components.port = urlComp.nPort; + components.isHttps = (urlComp.nScheme == INTERNET_SCHEME_HTTPS); + + return true; +} + +/* Convert wide string to narrow string */ +std::string WideToNarrow(const std::wstring& wide) { + if (wide.empty()) return ""; + int size = WideCharToMultiByte(CP_UTF8, 0, wide.c_str(), (int)wide.length(), nullptr, 0, nullptr, nullptr); + std::string result(size, 0); + WideCharToMultiByte(CP_UTF8, 0, wide.c_str(), (int)wide.length(), &result[0], size, nullptr, nullptr); + return result; +} + +} // anonymous namespace + +bool DownloadFileWithProgress( + const std::string& url, + const std::string& localPath, + DownloadProgressCallback progressCallback, + LogCallback logCallback) +{ + Log(logCallback, LOG_INFO, "Downloading from: " + url); + + UrlComponents urlComp; + if (!ParseUrl(url, urlComp)) { + Log(logCallback, LOG_ERROR, "Failed to parse URL: " + url); + return false; + } + + /* Open WinHTTP session */ + HINTERNET hSession = WinHttpOpen( + L"BinaryNinja-Debugger/1.0", + WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, + WINHTTP_NO_PROXY_NAME, + WINHTTP_NO_PROXY_BYPASS, + 0); + + if (!hSession) { + Log(logCallback, LOG_ERROR, "WinHttpOpen failed: " + std::to_string(GetLastError())); + return false; + } + + /* Connect to server */ + HINTERNET hConnect = WinHttpConnect( + hSession, + urlComp.host.c_str(), + urlComp.port, + 0); + + if (!hConnect) { + Log(logCallback, LOG_ERROR, "WinHttpConnect failed: " + std::to_string(GetLastError())); + WinHttpCloseHandle(hSession); + return false; + } + + /* Create request */ + DWORD flags = urlComp.isHttps ? WINHTTP_FLAG_SECURE : 0; + HINTERNET hRequest = WinHttpOpenRequest( + hConnect, + L"GET", + urlComp.path.c_str(), + nullptr, + WINHTTP_NO_REFERER, + WINHTTP_DEFAULT_ACCEPT_TYPES, + flags); + + if (!hRequest) { + Log(logCallback, LOG_ERROR, "WinHttpOpenRequest failed: " + std::to_string(GetLastError())); + WinHttpCloseHandle(hConnect); + WinHttpCloseHandle(hSession); + return false; + } + + /* Send request */ + if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0)) { + Log(logCallback, LOG_ERROR, "WinHttpSendRequest failed: " + std::to_string(GetLastError())); + WinHttpCloseHandle(hRequest); + WinHttpCloseHandle(hConnect); + WinHttpCloseHandle(hSession); + return false; + } + + /* Receive response */ + if (!WinHttpReceiveResponse(hRequest, nullptr)) { + Log(logCallback, LOG_ERROR, "WinHttpReceiveResponse failed: " + std::to_string(GetLastError())); + WinHttpCloseHandle(hRequest); + WinHttpCloseHandle(hConnect); + WinHttpCloseHandle(hSession); + return false; + } + + /* Check for redirect */ + DWORD statusCode = 0; + DWORD statusCodeSize = sizeof(statusCode); + WinHttpQueryHeaders(hRequest, + WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, + &statusCode, + &statusCodeSize, + WINHTTP_NO_HEADER_INDEX); + + if (statusCode >= 300 && statusCode < 400) { + /* Handle redirect - get Location header */ + wchar_t redirectUrl[2048] = {0}; + DWORD redirectUrlSize = sizeof(redirectUrl); + if (WinHttpQueryHeaders(hRequest, + WINHTTP_QUERY_LOCATION, + WINHTTP_HEADER_NAME_BY_INDEX, + redirectUrl, + &redirectUrlSize, + WINHTTP_NO_HEADER_INDEX)) { + + WinHttpCloseHandle(hRequest); + WinHttpCloseHandle(hConnect); + WinHttpCloseHandle(hSession); + + std::string newUrl = WideToNarrow(redirectUrl); + Log(logCallback, LOG_INFO, "Redirecting to: " + newUrl); + return DownloadFileWithProgress(newUrl, localPath, progressCallback, logCallback); + } + } + + if (statusCode != 200) { + Log(logCallback, LOG_ERROR, "HTTP error: " + std::to_string(statusCode)); + WinHttpCloseHandle(hRequest); + WinHttpCloseHandle(hConnect); + WinHttpCloseHandle(hSession); + return false; + } + + /* Get content length */ + int64_t contentLength = -1; + wchar_t contentLengthStr[32] = {0}; + DWORD contentLengthStrSize = sizeof(contentLengthStr); + if (WinHttpQueryHeaders(hRequest, + WINHTTP_QUERY_CONTENT_LENGTH, + WINHTTP_HEADER_NAME_BY_INDEX, + contentLengthStr, + &contentLengthStrSize, + WINHTTP_NO_HEADER_INDEX)) { + contentLength = _wtoi64(contentLengthStr); + } + + if (contentLength > 0) { + Log(logCallback, LOG_INFO, "Content length: " + std::to_string(contentLength) + " bytes"); + } + + /* Open output file */ + std::ofstream outFile(localPath, std::ios::binary); + if (!outFile) { + Log(logCallback, LOG_ERROR, "Failed to create output file: " + localPath); + WinHttpCloseHandle(hRequest); + WinHttpCloseHandle(hConnect); + WinHttpCloseHandle(hSession); + return false; + } + + /* Read data with progress */ + std::vector buffer(8192); + DWORD bytesRead = 0; + int64_t totalRead = 0; + + /* Speed calculation */ + auto startTime = std::chrono::steady_clock::now(); + auto lastSpeedUpdate = startTime; + int64_t bytesAtLastUpdate = 0; + double currentSpeed = 0.0; + + while (true) { + if (!WinHttpReadData(hRequest, buffer.data(), (DWORD)buffer.size(), &bytesRead)) { + Log(logCallback, LOG_ERROR, "WinHttpReadData failed: " + std::to_string(GetLastError())); + outFile.close(); + WinHttpCloseHandle(hRequest); + WinHttpCloseHandle(hConnect); + WinHttpCloseHandle(hSession); + return false; + } + + if (bytesRead == 0) { + break; /* End of data */ + } + + outFile.write(buffer.data(), bytesRead); + totalRead += bytesRead; + + if (progressCallback) { + /* Update speed calculation every 500ms for smoother display */ + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - lastSpeedUpdate).count(); + if (elapsed >= 500) { + int64_t bytesDelta = totalRead - bytesAtLastUpdate; + currentSpeed = (bytesDelta * 1000.0) / elapsed; + bytesAtLastUpdate = totalRead; + lastSpeedUpdate = now; + } + + DownloadProgress progress; + progress.bytesDownloaded = totalRead; + progress.totalBytes = contentLength; + progress.bytesPerSecond = currentSpeed; + progressCallback(progress); + } + } + + outFile.close(); + + WinHttpCloseHandle(hRequest); + WinHttpCloseHandle(hConnect); + WinHttpCloseHandle(hSession); + + Log(logCallback, LOG_INFO, "Downloaded " + std::to_string(totalRead) + " bytes to: " + localPath); + + return true; +} + +} // namespace WinDbgInstaller + +#endif // _WIN32 diff --git a/installer/http_downloader.h b/installer/http_downloader.h new file mode 100644 index 00000000..65c87a2a --- /dev/null +++ b/installer/http_downloader.h @@ -0,0 +1,49 @@ +/* + * HTTP Downloader with progress callback using WinHTTP + * + * Copyright 2020-2026 Vector 35 Inc. + * Licensed under the Apache License, Version 2.0 + */ + +#pragma once + +#ifdef _WIN32 + +#include +#include +#include + +namespace WinDbgInstaller { + +/* Download progress information */ +struct DownloadProgress { + int64_t bytesDownloaded; /* Bytes downloaded so far */ + int64_t totalBytes; /* Total bytes to download, -1 if unknown */ + double bytesPerSecond; /* Current download speed in bytes/second */ +}; + +/* Callback function type for download progress */ +using DownloadProgressCallback = std::function; + +/* Callback function type for logging */ +using LogCallback = std::function; + +/* + * Download a file from URL with progress callback + * + * @param url URL to download from (http:// or https://) + * @param localPath Path where to save the downloaded file + * @param progressCallback Optional callback for progress updates + * @param logCallback Optional callback for log messages + * @return true if download was successful, false otherwise + */ +bool DownloadFileWithProgress( + const std::string& url, + const std::string& localPath, + DownloadProgressCallback progressCallback = nullptr, + LogCallback logCallback = nullptr +); + +} // namespace WinDbgInstaller + +#endif // _WIN32 diff --git a/installer/main.cpp b/installer/main.cpp new file mode 100644 index 00000000..3def9ae9 --- /dev/null +++ b/installer/main.cpp @@ -0,0 +1,570 @@ +/* + * WinDbg/TTD Installer CLI + * + * A command-line utility for installing and updating WinDbg/TTD. + * Can be invoked directly by users or spawned by Binary Ninja API. + * + * Usage: + * windbg-installer install [--path ] [--quiet] [--json] + * windbg-installer check-update [--path ] [--json] + * windbg-installer version [--path ] [--json] + * windbg-installer --help + * + * Copyright 2020-2026 Vector 35 Inc. + * Licensed under the Apache License, Version 2.0 + */ + +#ifdef _WIN32 + +#include "windbg_installer.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace WinDbgInstaller; + +namespace { + +/* Output mode */ +enum class OutputMode { + Human, /* Human-readable with progress bar */ + Quiet, /* Minimal output */ + Json /* Machine-readable JSON */ +}; + +/* Console colors */ +enum ConsoleColor { + COLOR_DEFAULT = 7, + COLOR_GREEN = 10, + COLOR_YELLOW = 14, + COLOR_RED = 12, + COLOR_CYAN = 11 +}; + +void SetConsoleColor(ConsoleColor color) { + HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); + SetConsoleTextAttribute(hConsole, color); +} + +void ResetConsoleColor() { + SetConsoleColor(COLOR_DEFAULT); +} + +/* Find all processes with a given name */ +std::vector FindProcessesByName(const std::string& processName) { + std::vector pids; + + HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (hSnapshot == INVALID_HANDLE_VALUE) { + return pids; + } + + PROCESSENTRY32 pe32; + pe32.dwSize = sizeof(pe32); + + if (Process32First(hSnapshot, &pe32)) { + do { + if (_stricmp(pe32.szExeFile, processName.c_str()) == 0) { + pids.push_back(pe32.th32ProcessID); + } + } while (Process32Next(hSnapshot, &pe32)); + } + + CloseHandle(hSnapshot); + return pids; +} + +/* Wait for all instances of a process to exit */ +bool WaitForProcessesToExit(const std::string& processName, OutputMode mode) { + auto pids = FindProcessesByName(processName); + if (pids.empty()) { + return true; + } + + if (mode == OutputMode::Human) { + SetConsoleColor(COLOR_YELLOW); + std::cout << " Waiting for " << processName << " to exit...\n"; + ResetConsoleColor(); + std::cout << " Found " << pids.size() << " instance(s) running.\n"; + } else if (mode == OutputMode::Json) { + std::cout << "{\"type\":\"status\",\"message\":\"Waiting for " << processName << " to exit\",\"count\":" << pids.size() << "}" << std::endl; + } + + /* Open handles to all processes */ + std::vector handles; + for (DWORD pid : pids) { + HANDLE hProcess = OpenProcess(SYNCHRONIZE, FALSE, pid); + if (hProcess != nullptr) { + handles.push_back(hProcess); + } + } + + if (handles.empty()) { + return true; + } + + /* Wait for all processes to exit (timeout: 5 minutes) */ + DWORD result = WaitForMultipleObjects((DWORD)handles.size(), handles.data(), TRUE, 300000); + + /* Close all handles */ + for (HANDLE h : handles) { + CloseHandle(h); + } + + if (result == WAIT_TIMEOUT) { + if (mode == OutputMode::Human) { + SetConsoleColor(COLOR_RED); + std::cout << " Timeout waiting for " << processName << " to exit.\n"; + ResetConsoleColor(); + } + return false; + } + + if (mode == OutputMode::Human) { + SetConsoleColor(COLOR_GREEN); + std::cout << " " << processName << " has exited.\n\n"; + ResetConsoleColor(); + } + + return true; +} + +/* Format seconds as human-readable time (e.g., "1m 23s" or "45s") */ +std::string FormatETA(int64_t seconds) { + if (seconds < 0) return ""; + if (seconds < 60) { + return std::to_string(seconds) + "s"; + } else if (seconds < 3600) { + int mins = (int)(seconds / 60); + int secs = (int)(seconds % 60); + return std::to_string(mins) + "m " + std::to_string(secs) + "s"; + } else { + int hours = (int)(seconds / 3600); + int mins = (int)((seconds % 3600) / 60); + return std::to_string(hours) + "h " + std::to_string(mins) + "m"; + } +} + +/* Print a progress bar (human mode) */ +void PrintProgressBar(int percent, int64_t downloaded = -1, int64_t total = -1, double bytesPerSecond = 0.0) { + const int barWidth = 40; + int pos = barWidth * percent / 100; + + std::cout << "\r ["; + SetConsoleColor(COLOR_GREEN); + for (int i = 0; i < barWidth; i++) { + if (i < pos) std::cout << "="; + else if (i == pos) std::cout << ">"; + else std::cout << " "; + } + ResetConsoleColor(); + std::cout << "] " << std::setw(3) << percent << "%"; + + if (downloaded >= 0 && total > 0) { + double downloadedMB = downloaded / (1024.0 * 1024.0); + double totalMB = total / (1024.0 * 1024.0); + std::cout << " (" << std::fixed << std::setprecision(1) + << downloadedMB << "/" << totalMB << " MB"; + if (bytesPerSecond > 0) { + double speedMBps = bytesPerSecond / (1024.0 * 1024.0); + std::cout << ", " << std::setprecision(1) << speedMBps << " MB/s"; + /* Calculate and show ETA */ + int64_t remaining = total - downloaded; + int64_t etaSeconds = (int64_t)(remaining / bytesPerSecond); + std::cout << ", ETA " << FormatETA(etaSeconds); + } + std::cout << ")"; + } else if (downloaded >= 0) { + double downloadedMB = downloaded / (1024.0 * 1024.0); + std::cout << " (" << std::fixed << std::setprecision(1) + << downloadedMB << " MB"; + if (bytesPerSecond > 0) { + double speedMBps = bytesPerSecond / (1024.0 * 1024.0); + std::cout << ", " << std::setprecision(1) << speedMBps << " MB/s"; + } + std::cout << ")"; + } + + std::cout << " " << std::flush; +} + +/* Print JSON progress (json mode) */ +void PrintJsonProgress(const std::string& step, int percent, int64_t downloaded = -1, int64_t total = -1, double bytesPerSecond = 0.0) { + std::cout << "{\"type\":\"progress\",\"step\":\"" << step + << "\",\"percent\":" << percent; + if (downloaded >= 0) { + std::cout << ",\"bytesDownloaded\":" << downloaded; + } + if (total >= 0) { + std::cout << ",\"totalBytes\":" << total; + } + if (bytesPerSecond > 0) { + std::cout << ",\"bytesPerSecond\":" << std::fixed << std::setprecision(1) << bytesPerSecond; + } + std::cout << "}" << std::endl; +} + +/* Print JSON result */ +void PrintJsonResult(bool success, const std::string& message, const std::string& extra = "") { + std::cout << "{\"type\":\"result\",\"success\":" << (success ? "true" : "false") + << ",\"message\":\"" << message << "\""; + if (!extra.empty()) { + std::cout << "," << extra; + } + std::cout << "}" << std::endl; +} + +/* Print JSON version info */ +void PrintJsonVersion(const std::string& installed, const std::string& latest, bool updateAvailable) { + std::cout << "{\"type\":\"version\",\"installed\":\"" << installed + << "\",\"latest\":\"" << latest + << "\",\"updateAvailable\":" << (updateAvailable ? "true" : "false") + << "}" << std::endl; +} + +void PrintUsage(const char* programName) { + std::cout << "WinDbg/TTD Installer for Binary Ninja Debugger\n" + << "\n" + << "Usage:\n" + << " " << programName << " [options]\n" + << "\n" + << "Commands:\n" + << " install Install or update WinDbg/TTD\n" + << " version Show installed version (local only, no network)\n" + << " check-update Check for updates (compares local vs latest online)\n" + << "\n" + << "Options:\n" + << " --path Specify installation directory\n" + << " (default: %APPDATA%\\Binary Ninja\\windbg)\n" + << " --update Update mode: wait for Binary Ninja to exit first\n" + << " (use this when WinDbg DLLs may be loaded)\n" + << " --quiet Suppress progress output (exit code only)\n" + << " --json Output in JSON format (for API integration)\n" + << " --help Show this help message\n" + << "\n" + << "Examples:\n" + << " " << programName << " version\n" + << " " << programName << " check-update\n" + << " " << programName << " install\n" + << " " << programName << " install --update\n" + << " " << programName << " install --path C:\\Tools\\WinDbg\n" + << "\n" + << "Exit codes:\n" + << " 0 Success / up to date\n" + << " 1 Not installed / error\n" + << " 2 Update available (for check-update)\n" + << "\n"; +} + +void PrintBanner() { + SetConsoleColor(COLOR_CYAN); + std::cout << "\n"; + std::cout << " ============================================\n"; + std::cout << " Binary Ninja Debugger - WinDbg/TTD Installer\n"; + std::cout << " ============================================\n"; + ResetConsoleColor(); + std::cout << "\n"; +} + +/* Command: install */ +int CmdInstall(const std::string& installPath, OutputMode mode, bool isUpdate) { + /* Determine and print install path */ + std::string targetPath = installPath.empty() ? GetDefaultInstallPath() : installPath; + + if (mode == OutputMode::Human) { + std::cout << " Install path: " << targetPath << "\n\n"; + } else if (mode == OutputMode::Json) { + std::cout << "{\"type\":\"info\",\"installPath\":\"" << targetPath << "\"}" << std::endl; + } + + /* Only wait for Binary Ninja to exit when updating */ + /* Fresh installs don't need to wait since DLLs aren't loaded yet */ + if (isUpdate) { + if (mode == OutputMode::Human) { + std::cout << " Update mode: checking for running Binary Ninja instances...\n"; + } + if (!WaitForProcessesToExit("binaryninja.exe", mode)) { + if (mode == OutputMode::Json) { + std::cout << "{\"type\":\"error\",\"message\":\"Timeout waiting for Binary Ninja to exit\"}" << std::endl; + } + return 1; + } + } + + InstallConfig config; + config.installPath = targetPath; + config.updateSettings = true; + + std::string lastStep; + + /* Setup callbacks based on output mode */ + if (mode == OutputMode::Human) { + config.onProgress = [&lastStep](const ProgressInfo& info) { + if (info.step != lastStep) { + if (!lastStep.empty()) { + std::cout << "\n"; + } + SetConsoleColor(COLOR_CYAN); + std::cout << " " << info.step; + ResetConsoleColor(); + /* Only print newline for non-download steps */ + if (info.totalBytes <= 0) { + std::cout << "\n"; + } + lastStep = info.step; + } + /* Only show progress bar for the TTD download (when we have byte info) */ + if (info.totalBytes > 0) { + PrintProgressBar(info.overallPercent, info.bytesDownloaded, info.totalBytes, info.bytesPerSecond); + } + }; + + config.onLog = [](int level, const std::string& message) { + if (level >= LOG_WARN) { + ConsoleColor color = (level == LOG_ERROR) ? COLOR_RED : COLOR_YELLOW; + SetConsoleColor(color); + std::cout << "\n " << message; + ResetConsoleColor(); + } + }; + } else if (mode == OutputMode::Json) { + config.onProgress = [](const ProgressInfo& info) { + PrintJsonProgress(info.step, info.overallPercent, info.bytesDownloaded, info.totalBytes, info.bytesPerSecond); + }; + + config.onLog = [](int level, const std::string& message) { + const char* levelStr = "debug"; + if (level == LOG_INFO) levelStr = "info"; + else if (level == LOG_WARN) levelStr = "warn"; + else if (level == LOG_ERROR) levelStr = "error"; + std::cout << "{\"type\":\"log\",\"level\":\"" << levelStr + << "\",\"message\":\"" << message << "\"}" << std::endl; + }; + } + /* Quiet mode: no callbacks */ + + InstallResult result = Install(config); + + /* Write result to a file so UI can read it */ + { + std::string resultPath = targetPath + "\\install_result.json"; + std::ofstream resultFile(resultPath); + if (resultFile.is_open()) { + resultFile << "{\"success\":" << (result.success ? "true" : "false"); + if (!result.errorMessage.empty()) { + resultFile << ",\"error\":\"" << result.errorMessage << "\""; + } + resultFile << "}" << std::endl; + resultFile.close(); + } + } + + if (mode == OutputMode::Human) { + std::cout << "\n\n"; + if (result.success) { + SetConsoleColor(COLOR_GREEN); + std::cout << " Installation completed successfully!\n"; + ResetConsoleColor(); + std::cout << " Please restart Binary Ninja to use WinDbg/TTD.\n"; + } else { + SetConsoleColor(COLOR_RED); + std::cout << " Installation failed"; + if (!result.errorMessage.empty()) { + std::cout << ": " << result.errorMessage; + } + std::cout << "\n"; + ResetConsoleColor(); + } + std::cout << "\n"; + } else if (mode == OutputMode::Json) { + std::string message = result.success ? "Installation completed successfully" : result.errorMessage; + if (!result.success && result.errorMessage.empty()) { + message = "Installation failed"; + } + PrintJsonResult(result.success, message); + } + + return result.success ? 0 : 1; +} + +/* Command: version (local only, no network) */ +int CmdVersion(const std::string& installPath, OutputMode mode) { + std::string path = installPath.empty() ? GetDefaultInstallPath() : installPath; + + VersionInfo installed = GetInstalledVersion(path); + + if (mode == OutputMode::Json) { + std::cout << "{\"type\":\"version\",\"isInstalled\":" << (installed.isInstalled ? "true" : "false") + << ",\"installed\":\"" << installed.version + << "\",\"installPath\":\"" << path << "\"}" << std::endl; + } else if (mode == OutputMode::Human) { + std::cout << "\n"; + std::cout << " Install path: " << path << "\n"; + std::cout << " Installed: "; + if (!installed.isInstalled) { + SetConsoleColor(COLOR_YELLOW); + std::cout << "(not installed)"; + } else if (installed.version.empty()) { + SetConsoleColor(COLOR_YELLOW); + std::cout << "(installed, version unknown)"; + } else { + std::cout << installed.version; + } + ResetConsoleColor(); + std::cout << "\n\n"; + } + + return installed.isInstalled ? 0 : 1; +} + +/* Command: check-update */ +int CmdCheckUpdate(const std::string& installPath, OutputMode mode) { + std::string path = installPath.empty() ? GetDefaultInstallPath() : installPath; + + /* Get installed version first (local, fast) */ + VersionInfo installed = GetInstalledVersion(path); + + if (mode == OutputMode::Human) { + std::cout << "Install path: " << path << "\n"; + std::cout << "Installed: "; + if (!installed.isInstalled) { + SetConsoleColor(COLOR_YELLOW); + std::cout << "(not installed)"; + ResetConsoleColor(); + std::cout << "\n"; + return 1; /* Exit early, no need to check latest */ + } else if (installed.version.empty()) { + SetConsoleColor(COLOR_YELLOW); + std::cout << "(installed, version unknown)"; + ResetConsoleColor(); + std::cout << "\n"; + } else { + std::cout << installed.version << "\n"; + } + + std::cout << "Latest: "; + std::cout << std::flush; /* Flush before network request */ + } + + /* Fetch latest version (network request, may take time) */ + VersionInfo latest = GetLatestVersion(nullptr); + bool updateAvailable = !IsVersionUpToDate(installed, latest); + + if (mode == OutputMode::Json) { + /* Include path and isInstalled in JSON output */ + std::cout << "{\"type\":\"version\",\"isInstalled\":" << (installed.isInstalled ? "true" : "false") + << ",\"installed\":\"" << installed.version + << "\",\"latest\":\"" << latest.version + << "\",\"updateAvailable\":" << (updateAvailable ? "true" : "false") + << ",\"installPath\":\"" << path << "\"}" << std::endl; + } else if (mode == OutputMode::Human) { + if (latest.version.empty()) { + SetConsoleColor(COLOR_YELLOW); + std::cout << "(unable to check)"; + } else { + std::cout << latest.version; + } + ResetConsoleColor(); + std::cout << "\n"; + + if (installed.version.empty()) { + std::cout << "Recommend reinstalling with 'install --update' for version tracking.\n"; + } else if (updateAvailable) { + SetConsoleColor(COLOR_GREEN); + std::cout << "Update available!\n"; + ResetConsoleColor(); + } else { + std::cout << "No update available.\n"; + } + } + + /* Exit code 2 means update available, 1 means not installed */ + if (!installed.isInstalled) { + return 1; + } + return updateAvailable ? 2 : 0; +} + +} // anonymous namespace + +int main(int argc, char* argv[]) { + /* Parse command line arguments */ + std::string command; + std::string installPath; + OutputMode mode = OutputMode::Human; + bool isUpdate = false; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { + PrintUsage(argv[0]); + return 0; + } else if (strcmp(argv[i], "--path") == 0) { + if (i + 1 < argc) { + installPath = argv[++i]; + } else { + std::cerr << "Error: --path requires a directory argument\n"; + return 1; + } + } else if (strcmp(argv[i], "--quiet") == 0 || strcmp(argv[i], "-q") == 0) { + mode = OutputMode::Quiet; + } else if (strcmp(argv[i], "--json") == 0) { + mode = OutputMode::Json; + } else if (strcmp(argv[i], "--update") == 0) { + isUpdate = true; + } else if (argv[i][0] != '-') { + if (command.empty()) { + command = argv[i]; + } else { + std::cerr << "Error: Unexpected argument: " << argv[i] << "\n"; + return 1; + } + } else { + std::cerr << "Error: Unknown option: " << argv[i] << "\n"; + PrintUsage(argv[0]); + return 1; + } + } + + /* No command = show help */ + if (command.empty()) { + PrintUsage(argv[0]); + return 0; + } + + /* Print banner for human mode */ + if (mode == OutputMode::Human) { + PrintBanner(); + } + + /* Execute command */ + if (command == "install") { + return CmdInstall(installPath, mode, isUpdate); + } else if (command == "version") { + return CmdVersion(installPath, mode); + } else if (command == "check-update") { + return CmdCheckUpdate(installPath, mode); + } else { + std::cerr << "Error: Unknown command: " << command << "\n"; + PrintUsage(argv[0]); + return 1; + } +} + +#else + +/* Non-Windows stub */ +#include + +int main() { + std::cerr << "This installer is only available on Windows.\n"; + return 1; +} + +#endif // _WIN32 diff --git a/installer/windbg_installer.cpp b/installer/windbg_installer.cpp new file mode 100644 index 00000000..3b2bad39 --- /dev/null +++ b/installer/windbg_installer.cpp @@ -0,0 +1,472 @@ +/* + * WinDbg/TTD Installer Library Implementation + * + * Copyright 2020-2026 Vector 35 Inc. + * Licensed under the Apache License, Version 2.0 + */ + +#ifdef _WIN32 + +#include "windbg_installer.h" +#include "http_downloader.h" +#include "zip_extractor.h" +#include "../vendor/pugixml/pugixml.hpp" +#include +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "version.lib") + +namespace fs = std::filesystem; + +namespace WinDbgInstaller { + +namespace { + +/* URL for WinDbg appinstaller file */ +const char* kWinDbgDownloadUrl = "https://aka.ms/windbg/download"; + +/* Files required for valid installation */ +const std::vector kRequiredFiles = { + "amd64\\dbgeng.dll", + "amd64\\dbghelp.dll", + "amd64\\dbgmodel.dll", + "amd64\\dbgcore.dll", + "amd64\\ttd\\TTD.exe", + "amd64\\ttd\\TTDRecord.dll" +}; + +/* Inner MSIX file to extract from bundle */ +const char* kInnerMsixName = "windbg_win-x64.msix"; + +void Log(LogCallback logCallback, int level, const std::string& message) { + if (logCallback) { + logCallback(level, message); + } +} + +void ReportProgress(ProgressCallback progressCallback, const std::string& step, int percent, + int64_t bytesDownloaded = 0, int64_t totalBytes = -1, double bytesPerSecond = 0.0) { + if (progressCallback) { + ProgressInfo info; + info.step = step; + info.overallPercent = percent; + info.bytesDownloaded = bytesDownloaded; + info.totalBytes = totalBytes; + info.bytesPerSecond = bytesPerSecond; + progressCallback(info); + } +} + +/* Get Binary Ninja user directory */ +std::string GetUserDirectory() { + char path[MAX_PATH]; + if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_APPDATA, nullptr, 0, path))) { + return std::string(path) + "\\Binary Ninja"; + } + return ""; +} + +/* Generate a unique temporary file path */ +std::string GetTempFilePath(const std::string& extension) { + char tempPath[MAX_PATH]; + GetTempPathA(MAX_PATH, tempPath); + + /* Generate a unique filename using GUID */ + GUID guid; + CoCreateGuid(&guid); + char guidStr[40]; + sprintf_s(guidStr, sizeof(guidStr), "{%08lX-%04hX-%04hX-%02hhX%02hhX-%02hhX%02hhX%02hhX%02hhX%02hhX%02hhX}", + guid.Data1, guid.Data2, guid.Data3, + guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], + guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]); + + return std::string(tempPath) + "windbg_" + guidStr + extension; +} + +/* Parse appinstaller XML to get MSIX bundle URL */ +std::string ParseAppInstallerXml(const std::string& appInstallerPath, LogCallback logCallback) { + Log(logCallback, LOG_INFO, "Parsing appinstaller XML: " + appInstallerPath); + + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file(appInstallerPath.c_str()); + + if (!result) { + Log(logCallback, LOG_ERROR, "Failed to parse XML: " + std::string(result.description())); + return ""; + } + + /* Look for MainBundle element with Uri attribute */ + pugi::xml_node mainBundle = doc.child("AppInstaller").child("MainBundle"); + if (!mainBundle) { + Log(logCallback, LOG_ERROR, "MainBundle element not found in XML"); + return ""; + } + + pugi::xml_attribute uriAttr = mainBundle.attribute("Uri"); + if (!uriAttr) { + Log(logCallback, LOG_ERROR, "Uri attribute not found in MainBundle element"); + return ""; + } + + std::string msixUrl = uriAttr.value(); + Log(logCallback, LOG_INFO, "Found MSIX bundle URL: " + msixUrl); + return msixUrl; +} + +/* Print info about Binary Ninja settings (settings are configured by UI after install) */ +void PrintSettingsInfo(const std::string& dbgEngPath, LogCallback logCallback) { + Log(logCallback, LOG_INFO, "DbgEng path: " + dbgEngPath); + Log(logCallback, LOG_INFO, "Binary Ninja will configure settings automatically when launched."); +} + +/* Cleanup temporary files */ +void CleanupTempFiles(const std::vector& files, LogCallback logCallback) { + for (const auto& file : files) { + std::error_code ec; + if (fs::is_directory(file)) { + fs::remove_all(file, ec); + } else { + fs::remove(file, ec); + } + if (!ec) { + Log(logCallback, LOG_DEBUG, "Cleaned up: " + file); + } + } +} + +} // anonymous namespace + +std::string GetDefaultInstallPath() { + std::string userDir = GetUserDirectory(); + if (userDir.empty()) { + return ""; + } + return userDir + "\\windbg"; +} + +bool CheckInstallation(const std::string& path) { + for (const auto& file : kRequiredFiles) { + fs::path fullPath = fs::path(path) / file; + if (!fs::exists(fullPath)) { + return false; + } + } + return true; +} + +bool CheckInstallOk(const std::string& path) { + return CheckInstallation(path); +} + +InstallResult Install(const InstallConfig& config) { + LogCallback logCallback = config.onLog; + ProgressCallback progressCallback = config.onProgress; + std::vector tempFiles; + + try { + Log(logCallback, LOG_INFO, "Starting WinDbg/TTD installation"); + + ReportProgress(progressCallback, "Initializing installation...", 0); + + /* Determine install path */ + std::string installTarget = config.installPath; + if (installTarget.empty()) { + installTarget = GetDefaultInstallPath(); + if (installTarget.empty()) { + std::string error = "Could not determine installation path"; + Log(logCallback, LOG_ERROR, error); + return InstallResult(false, error); + } + } + Log(logCallback, LOG_INFO, "Installation target: " + installTarget); + + /* Step 1: Download appinstaller file (small, no progress needed) */ + ReportProgress(progressCallback, "Downloading WinDbg package information from:", 0); + ReportProgress(progressCallback, std::string(kWinDbgDownloadUrl), 0); + + std::string appInstallerPath = GetTempFilePath(".appinstaller"); + tempFiles.push_back(appInstallerPath); + + if (!DownloadFileWithProgress(kWinDbgDownloadUrl, appInstallerPath, nullptr, logCallback)) { + std::string error = "Failed to download appinstaller file"; + Log(logCallback, LOG_ERROR, error); + CleanupTempFiles(tempFiles, logCallback); + return InstallResult(false, error); + } + + /* Step 2: Parse XML to get MSIX bundle URL */ + ReportProgress(progressCallback, "Parsing package information...", 0); + + std::string msixUrl = ParseAppInstallerXml(appInstallerPath, logCallback); + if (msixUrl.empty()) { + std::string error = "Failed to parse appinstaller XML"; + Log(logCallback, LOG_ERROR, error); + CleanupTempFiles(tempFiles, logCallback); + return InstallResult(false, error); + } + + /* Step 3: Download MSIX bundle (this is the main download that shows progress) */ + ReportProgress(progressCallback, "Downloading WinDbg/TTD package from:", 0); + ReportProgress(progressCallback, msixUrl, 0); + + std::string msixPath = GetTempFilePath(".msixbundle.zip"); + tempFiles.push_back(msixPath); + + auto msixDownloadProgressCb = [&](const DownloadProgress& dp) { + /* Report download percentage (0-100%) directly - this is the only step that needs progress display */ + int percent = 0; + if (dp.totalBytes > 0) { + percent = (int)(100 * dp.bytesDownloaded / dp.totalBytes); + } + ReportProgress(progressCallback, "Downloading...", percent, + dp.bytesDownloaded, dp.totalBytes, dp.bytesPerSecond); + }; + + if (!DownloadFileWithProgress(msixUrl, msixPath, msixDownloadProgressCb, logCallback)) { + std::string error = "Failed to download MSIX bundle"; + Log(logCallback, LOG_ERROR, error); + CleanupTempFiles(tempFiles, logCallback); + return InstallResult(false, error); + } + + /* Step 4: Extract inner MSIX file from bundle */ + ReportProgress(progressCallback, "Extracting package contents...", 0); + + std::string tempExtractDir = GetTempFilePath("_extract"); + tempFiles.push_back(tempExtractDir); + + std::string innerMsixPath = ExtractFileFromZipArchive(msixPath, kInnerMsixName, tempExtractDir, logCallback); + if (innerMsixPath.empty()) { + std::string error = "Failed to extract inner MSIX file"; + Log(logCallback, LOG_ERROR, error); + CleanupTempFiles(tempFiles, logCallback); + return InstallResult(false, error); + } + + /* Step 5: Extract WinDbg contents to installation directory */ + ReportProgress(progressCallback, "Installing WinDbg/TTD files...", 0); + + if (!ExtractZipArchive(innerMsixPath, installTarget, nullptr, logCallback)) { + std::string error = "Failed to extract WinDbg contents"; + Log(logCallback, LOG_ERROR, error); + CleanupTempFiles(tempFiles, logCallback); + return InstallResult(false, error); + } + + /* Step 6: Verify installation */ + ReportProgress(progressCallback, "Verifying installation...", 0); + + if (!CheckInstallation(installTarget)) { + std::string error = "Installation verification failed - required files missing"; + Log(logCallback, LOG_ERROR, error); + CleanupTempFiles(tempFiles, logCallback); + return InstallResult(false, error); + } + + Log(logCallback, LOG_INFO, "WinDbg/TTD installed to: " + installTarget); + + /* Step 6b: Write version marker file */ + /* Re-parse appinstaller to get version (file is still on disk) */ + std::string installedVersion; + { + pugi::xml_document doc; + if (doc.load_file(appInstallerPath.c_str())) { + pugi::xml_node appInstaller = doc.child("AppInstaller"); + if (appInstaller) { + pugi::xml_attribute versionAttr = appInstaller.attribute("Version"); + if (versionAttr) { + installedVersion = versionAttr.value(); + } + } + } + } + + if (!installedVersion.empty()) { + std::string versionFilePath = installTarget + "\\installed_version.txt"; + std::ofstream versionFile(versionFilePath); + if (versionFile.is_open()) { + versionFile << installedVersion; + versionFile.close(); + Log(logCallback, LOG_INFO, "Wrote version marker: " + installedVersion); + } else { + Log(logCallback, LOG_WARN, "Could not write version marker file"); + } + } + + /* Step 7: Print settings info (actual settings configuration is done by UI) */ + if (config.updateSettings) { + std::string x64dbgEngPath = installTarget + "\\amd64"; + PrintSettingsInfo(x64dbgEngPath, logCallback); + } + + /* Cleanup */ + CleanupTempFiles(tempFiles, logCallback); + + ReportProgress(progressCallback, "Installation completed successfully!", 0); + Log(logCallback, LOG_INFO, "Please restart Binary Ninja to use WinDbg/TTD."); + + return InstallResult(true); + } + catch (const std::exception& e) { + std::string error = "Exception during installation: " + std::string(e.what()); + Log(logCallback, LOG_ERROR, error); + CleanupTempFiles(tempFiles, logCallback); + return InstallResult(false, error); + } +} + +bool InstallWinDbg(LegacyProgressCallback progressCallback) { + InstallConfig config; + config.updateSettings = true; + + /* Wrap legacy callback */ + if (progressCallback) { + config.onProgress = [progressCallback](const ProgressInfo& info) { + progressCallback(info.step, info.overallPercent); + }; + } + + InstallResult result = Install(config); + return result.success; +} + +/* ============================================================================ + * Version Functions + * ============================================================================ */ + +VersionInfo GetInstalledVersion(const std::string& installPath) { + VersionInfo info; + + std::string path = installPath.empty() ? GetDefaultInstallPath() : installPath; + if (path.empty()) { + return info; /* isInstalled = false, version = "" */ + } + + /* Store the path for reference */ + info.installPath = path; + + /* Check if installation exists */ + std::string dllPath = path + "\\amd64\\dbgeng.dll"; + if (!fs::exists(dllPath)) { + return info; /* isInstalled = false, version = "" */ + } + + /* Installation exists */ + info.isInstalled = true; + + /* Read version from marker file (written during installation) */ + std::string versionFilePath = path + "\\installed_version.txt"; + std::ifstream versionFile(versionFilePath); + if (versionFile.is_open()) { + std::getline(versionFile, info.version); + versionFile.close(); + if (!info.version.empty()) { + info.displayName = "WinDbg " + info.version; + } + } + + /* If no version file, installation is from older version - mark as unknown */ + if (info.version.empty()) { + info.displayName = "WinDbg (unknown version)"; + } + + return info; +} + +VersionInfo GetLatestVersion(LogCallback logCallback) { + VersionInfo info; + + /* Download appinstaller file to temp location */ + std::string tempPath = GetTempFilePath(".appinstaller"); + + if (!DownloadFileWithProgress(kWinDbgDownloadUrl, tempPath, nullptr, logCallback)) { + Log(logCallback, LOG_ERROR, "Failed to download appinstaller for version check"); + return info; + } + + /* Parse XML to get version */ + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file(tempPath.c_str()); + + if (!result) { + Log(logCallback, LOG_ERROR, "Failed to parse appinstaller XML: " + std::string(result.description())); + fs::remove(tempPath); + return info; + } + + /* Get version from AppInstaller element */ + pugi::xml_node appInstaller = doc.child("AppInstaller"); + if (appInstaller) { + pugi::xml_attribute versionAttr = appInstaller.attribute("Version"); + if (versionAttr) { + info.version = versionAttr.value(); + info.displayName = "WinDbg " + info.version; + } + + /* Get download URL from MainBundle */ + pugi::xml_node mainBundle = appInstaller.child("MainBundle"); + if (mainBundle) { + pugi::xml_attribute uriAttr = mainBundle.attribute("Uri"); + if (uriAttr) { + info.downloadUrl = uriAttr.value(); + } + } + } + + /* Cleanup */ + fs::remove(tempPath); + + return info; +} + +int CompareVersions(const std::string& v1, const std::string& v2) { + /* Parse version strings like "1.2404.24002.0" */ + auto parseVersion = [](const std::string& v) -> std::vector { + std::vector parts; + std::istringstream iss(v); + std::string part; + while (std::getline(iss, part, '.')) { + try { + parts.push_back(std::stoi(part)); + } catch (...) { + parts.push_back(0); + } + } + return parts; + }; + + std::vector parts1 = parseVersion(v1); + std::vector parts2 = parseVersion(v2); + + /* Pad with zeros to make them equal length */ + size_t maxLen = (std::max)(parts1.size(), parts2.size()); + parts1.resize(maxLen, 0); + parts2.resize(maxLen, 0); + + /* Compare part by part */ + for (size_t i = 0; i < maxLen; i++) { + if (parts1[i] < parts2[i]) return -1; + if (parts1[i] > parts2[i]) return 1; + } + + return 0; +} + +bool IsVersionUpToDate(const VersionInfo& installed, const VersionInfo& latest) { + /* If either version is invalid, assume up to date (can't determine) */ + if (!installed.IsValid() || !latest.IsValid()) { + return true; + } + + /* Installed >= Latest means up to date */ + return CompareVersions(installed.version, latest.version) >= 0; +} + +} // namespace WinDbgInstaller + +#endif // _WIN32 diff --git a/installer/windbg_installer.h b/installer/windbg_installer.h new file mode 100644 index 00000000..bb6a0bfe --- /dev/null +++ b/installer/windbg_installer.h @@ -0,0 +1,160 @@ +/* + * WinDbg/TTD Installer Library + * + * Copyright 2020-2026 Vector 35 Inc. + * Licensed under the Apache License, Version 2.0 + */ + +#pragma once + +#ifdef _WIN32 + +#include +#include +#include + +namespace WinDbgInstaller { + +/* Progress information */ +struct ProgressInfo { + std::string step; /* Current step description */ + int overallPercent; /* Overall progress 0-100 */ + int64_t bytesDownloaded; /* Bytes downloaded so far (for download steps) */ + int64_t totalBytes; /* Total bytes to download (-1 if unknown) */ + double bytesPerSecond; /* Current download speed in bytes/second */ +}; + +/* Callback function types */ +using ProgressCallback = std::function; +using LogCallback = std::function; + +/* Log levels */ +enum LogLevel { + LOG_DEBUG = 0, + LOG_INFO = 1, + LOG_WARN = 2, + LOG_ERROR = 3 +}; + +/* Installation configuration */ +struct InstallConfig { + std::string installPath; /* Override default install path (empty = use default) */ + bool updateSettings; /* Whether to update Binary Ninja settings (default: true) */ + ProgressCallback onProgress; /* Progress callback */ + LogCallback onLog; /* Logging callback */ + + InstallConfig() : updateSettings(true) {} +}; + +/* Installation result */ +struct InstallResult { + bool success; + std::string errorMessage; // Empty if success, otherwise describes error + + InstallResult() : success(false) {} + InstallResult(bool s, const std::string& err = "") : success(s), errorMessage(err) {} +}; + +/* + * Install WinDbg/TTD + * + * @param config Installation configuration + * @return InstallResult with success status and error message if failed + */ +InstallResult Install(const InstallConfig& config); + +/* + * Check if installation is valid at path + * + * @param path Path to check + * @return true if all required files are present + */ +bool CheckInstallation(const std::string& path); + +/* + * Get default installation path + * + * @return Default path where WinDbg will be installed + */ +std::string GetDefaultInstallPath(); + +/* ============================================================================ + * Version Information + * ============================================================================ */ + +/* Version information structure */ +struct VersionInfo { + std::string version; /* Version string (e.g., "1.2404.24002.0"), empty if unknown */ + std::string displayName; /* Display name (e.g., "WinDbg 1.2404.24002.0") */ + std::string downloadUrl; /* Download URL for this version */ + std::string installPath; /* Path where this version is installed */ + bool isInstalled; /* True if WinDbg is installed (even if version unknown) */ + + VersionInfo() : isInstalled(false) {} + + /* Returns true if version string is known */ + bool IsValid() const { return !version.empty(); } +}; + +/* + * Get version of installed WinDbg + * + * @param installPath Path to WinDbg installation (empty = use default) + * @return Version info, or empty VersionInfo if not installed + */ +VersionInfo GetInstalledVersion(const std::string& installPath = ""); + +/* + * Get latest available version from Microsoft + * + * @param logCallback Optional callback for log messages + * @return Version info, or empty VersionInfo on error + */ +VersionInfo GetLatestVersion(LogCallback logCallback = nullptr); + +/* + * Check if installed version is up to date + * + * @param installed Installed version info + * @param latest Latest version info + * @return true if installed version >= latest version (or if comparison fails) + */ +bool IsVersionUpToDate(const VersionInfo& installed, const VersionInfo& latest); + +/* + * Compare two version strings + * + * @param v1 First version string + * @param v2 Second version string + * @return -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2 + */ +int CompareVersions(const std::string& v1, const std::string& v2); + +/* ============================================================================ + * Legacy API for backward compatibility with existing UI code + * ============================================================================ */ + +/* Legacy progress callback type (from ui/install_windbg.h) */ +using LegacyProgressCallback = std::function; + +/* + * Install WinDbg/TTD (legacy API) + * + * This function maintains backward compatibility with the existing UI code. + * + * @param progressCallback Optional legacy progress callback + * @return true if installation was successful, false otherwise + */ +bool InstallWinDbg(LegacyProgressCallback progressCallback = nullptr); + +/* + * Check installation (legacy API) + * + * @param path Path to check + * @return true if all required files are present + */ +bool CheckInstallOk(const std::string& path); + +} // namespace WinDbgInstaller + +#endif // _WIN32 diff --git a/installer/zip_extractor.cpp b/installer/zip_extractor.cpp new file mode 100644 index 00000000..3b55b249 --- /dev/null +++ b/installer/zip_extractor.cpp @@ -0,0 +1,149 @@ +/* + * ZIP Extractor using minizip-ng library + * + * Copyright 2020-2026 Vector 35 Inc. + * Licensed under the Apache License, Version 2.0 + */ + +#ifdef _WIN32 + +#include "zip_extractor.h" +#include "../vendor/minizip-ng/mz_zip.h" +#include + +namespace fs = std::filesystem; + +namespace WinDbgInstaller { + +namespace { + +/* Log levels */ +enum LogLevel { + LOG_DEBUG = 0, + LOG_INFO = 1, + LOG_WARN = 2, + LOG_ERROR = 3 +}; + +void Log(LogCallback logCallback, int level, const std::string& message) { + if (logCallback) { + logCallback(level, message); + } +} + +} // anonymous namespace + +bool ExtractZipArchive( + const std::string& zipPath, + const std::string& extractPath, + ExtractionProgressCallback progressCallback, + LogCallback logCallback) +{ + Log(logCallback, LOG_INFO, "Extracting " + zipPath + " to " + extractPath); + + mz::ZipReader reader; + int result = reader.Open(zipPath); + if (result != MZ_OK) { + Log(logCallback, LOG_ERROR, "Failed to open ZIP file: " + zipPath + " (error: " + std::to_string(result) + ")"); + return false; + } + + /* Create destination directory */ + std::error_code ec; + fs::create_directories(extractPath, ec); + if (ec) { + Log(logCallback, LOG_ERROR, "Failed to create extract directory: " + extractPath); + reader.Close(); + return false; + } + + /* Wrapper callback to convert from mz:: format to our format */ + auto mzProgressCallback = [&](const std::string& filename, int filesExtracted, int totalFiles) { + if (progressCallback) { + ExtractionProgress progress; + progress.currentFile = filename; + progress.filesExtracted = filesExtracted; + progress.totalFiles = totalFiles; + progressCallback(progress); + } + if (!filename.empty()) { + Log(logCallback, LOG_DEBUG, "Extracting: " + filename); + } + }; + + result = reader.ExtractAll(extractPath, mzProgressCallback); + reader.Close(); + + if (result != MZ_OK) { + Log(logCallback, LOG_ERROR, "Failed to extract ZIP file (error: " + std::to_string(result) + ")"); + return false; + } + + Log(logCallback, LOG_INFO, "Successfully extracted " + std::to_string(reader.GetEntryCount()) + " entries"); + return true; +} + +std::string ExtractFileFromZipArchive( + const std::string& zipPath, + const std::string& fileName, + const std::string& extractDir, + LogCallback logCallback) +{ + Log(logCallback, LOG_INFO, "Extracting " + fileName + " from " + zipPath); + + mz::ZipReader reader; + int result = reader.Open(zipPath); + if (result != MZ_OK) { + Log(logCallback, LOG_ERROR, "Failed to open ZIP file: " + zipPath + " (error: " + std::to_string(result) + ")"); + return ""; + } + + /* Find the entry */ + const mz::ZipEntry* entry = reader.FindEntry(fileName); + if (!entry) { + Log(logCallback, LOG_ERROR, "File not found in ZIP: " + fileName); + + /* List available files for debugging */ + Log(logCallback, LOG_DEBUG, "Available files in ZIP:"); + for (const auto& e : reader.GetEntries()) { + Log(logCallback, LOG_DEBUG, " " + e.filename); + } + + reader.Close(); + return ""; + } + + /* Create destination directory */ + std::error_code ec; + fs::create_directories(extractDir, ec); + if (ec) { + Log(logCallback, LOG_ERROR, "Failed to create extract directory: " + extractDir); + reader.Close(); + return ""; + } + + /* Build output path using the entry's actual filename */ + fs::path outputPath = fs::path(extractDir) / entry->filename; + + /* Extract the file */ + result = reader.ExtractFileTo(entry->filename, outputPath.string()); + reader.Close(); + + if (result != MZ_OK) { + Log(logCallback, LOG_ERROR, "Failed to extract file (error: " + std::to_string(result) + ")"); + return ""; + } + + /* Verify the file exists */ + if (!fs::exists(outputPath)) { + Log(logCallback, LOG_ERROR, "Extracted file does not exist: " + outputPath.string()); + return ""; + } + + Log(logCallback, LOG_INFO, "Successfully extracted to: " + outputPath.string()); + return outputPath.string(); +} + +} // namespace WinDbgInstaller + +#endif // _WIN32 diff --git a/installer/zip_extractor.h b/installer/zip_extractor.h new file mode 100644 index 00000000..cf6a2b26 --- /dev/null +++ b/installer/zip_extractor.h @@ -0,0 +1,64 @@ +/* + * ZIP Extractor using minizip-ng library + * + * Copyright 2020-2026 Vector 35 Inc. + * Licensed under the Apache License, Version 2.0 + */ + +#pragma once + +#ifdef _WIN32 + +#include +#include + +namespace WinDbgInstaller { + +/* Extraction progress information */ +struct ExtractionProgress { + std::string currentFile; /* File currently being extracted */ + int filesExtracted; /* Number of files extracted so far */ + int totalFiles; /* Total number of files to extract */ +}; + +/* Callback function type for extraction progress */ +using ExtractionProgressCallback = std::function; + +/* Callback function type for logging */ +using LogCallback = std::function; + +/* + * Extract all files from a ZIP archive + * + * @param zipPath Path to the ZIP file + * @param extractPath Directory where to extract contents + * @param progressCallback Optional callback for progress updates + * @param logCallback Optional callback for log messages + * @return true if extraction was successful, false otherwise + */ +bool ExtractZipArchive( + const std::string& zipPath, + const std::string& extractPath, + ExtractionProgressCallback progressCallback = nullptr, + LogCallback logCallback = nullptr +); + +/* + * Extract a single file from a ZIP archive + * + * @param zipPath Path to the ZIP file + * @param fileName Name of file to extract (case-insensitive) + * @param extractDir Directory where to extract the file + * @param logCallback Optional callback for log messages + * @return Path to extracted file, or empty string if extraction failed + */ +std::string ExtractFileFromZipArchive( + const std::string& zipPath, + const std::string& fileName, + const std::string& extractDir, + LogCallback logCallback = nullptr +); + +} // namespace WinDbgInstaller + +#endif // _WIN32 diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt index 21723831..154404d4 100644 --- a/ui/CMakeLists.txt +++ b/ui/CMakeLists.txt @@ -12,11 +12,14 @@ list(FILTER SOURCES EXCLUDE REGEX qrc_.*) if (NOT WIN32) list(REMOVE_ITEM SOURCES ${PROJECT_SOURCE_DIR}/ttdrecord.h) list(REMOVE_ITEM SOURCES ${PROJECT_SOURCE_DIR}/ttdrecord.cpp) -else() - list(APPEND SOURCES ${PROJECT_SOURCE_DIR}/../vendor/pugixml/pugixml.cpp) - list(APPEND SOURCES ${PROJECT_SOURCE_DIR}/../vendor/pugixml/pugixml.hpp) + list(REMOVE_ITEM SOURCES ${PROJECT_SOURCE_DIR}/windbgupdatedialog.h) + list(REMOVE_ITEM SOURCES ${PROJECT_SOURCE_DIR}/windbgupdatedialog.cpp) endif () +# install_windbg wrapper is no longer needed - UI uses debuggerapi directly +list(REMOVE_ITEM SOURCES ${PROJECT_SOURCE_DIR}/install_windbg.h) +list(REMOVE_ITEM SOURCES ${PROJECT_SOURCE_DIR}/install_windbg.cpp) + if(DEMO) add_library(debuggerui STATIC ${SOURCES}) else() @@ -46,5 +49,6 @@ set_target_properties(debuggerui PROPERTIES target_link_libraries(debuggerui debuggerapi binaryninjaui Qt6::Core Qt6::Gui Qt6::Widgets) if(WIN32) - target_link_libraries(debuggerui urlmon.lib shell32.lib ole32.lib) + # shell32 and ole32 for UI functionality + target_link_libraries(debuggerui shell32.lib ole32.lib) endif() diff --git a/ui/install_windbg.cpp b/ui/install_windbg.cpp deleted file mode 100644 index fa437d87..00000000 --- a/ui/install_windbg.cpp +++ /dev/null @@ -1,590 +0,0 @@ -/* -Copyright 2020-2026 Vector 35 Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -#ifdef WIN32 - -#include "install_windbg.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "../vendor/pugixml/pugixml.hpp" -#include - -#pragma comment(lib, "urlmon.lib") -#pragma comment(lib, "shell32.lib") -#pragma comment(lib, "ole32.lib") -#pragma comment(lib, "oleaut32.lib") - -using namespace BinaryNinja; -using namespace std; -namespace fs = std::filesystem; - -namespace BinaryNinjaDebugger -{ - namespace - { - /// Download a file from URL to a temporary location - /// @param url URL to download from - /// @param localPath Path where to save the downloaded file - /// @return true if download was successful, false otherwise - bool DownloadFile(const std::string& url, const std::string& localPath) - { - LogInfo("Downloading from: %s", url.c_str()); - - // Remove existing file if it exists - std::error_code ec; - fs::remove(localPath, ec); - - HRESULT hr = URLDownloadToFileA(nullptr, url.c_str(), localPath.c_str(), 0, nullptr); - if (SUCCEEDED(hr)) - { - // Verify the file was actually downloaded - if (fs::exists(localPath) && fs::file_size(localPath, ec) > 0) - { - LogInfo("Successfully downloaded to: %s", localPath.c_str()); - return true; - } - else - { - LogError("Downloaded file is empty or doesn't exist: %s", localPath.c_str()); - return false; - } - } - else - { - LogError("Failed to download from %s (HRESULT: 0x%08x)", url.c_str(), hr); - return false; - } - } - - /// Extract a ZIP archive using Windows Shell COM interface (secure) - /// @param zipPath Path to the ZIP file - /// @param extractPath Directory where to extract contents - /// @return true if extraction was successful, false otherwise - bool ExtractZip(const std::string& zipPath, const std::string& extractPath) - { - LogInfo("Extracting %s to %s", zipPath.c_str(), extractPath.c_str()); - - // Create destination directory if it doesn't exist - std::error_code ec; - fs::create_directories(extractPath, ec); - if (ec) - { - LogError("Failed to create extract directory: %s", ec.message().c_str()); - return false; - } - - // Initialize COM - HRESULT hr = CoInitialize(nullptr); - if (FAILED(hr)) - { - LogError("Failed to initialize COM: 0x%08x", hr); - return false; - } - - bool success = false; - try - { - // Create Shell Application object - IShellDispatch* pShellApp = nullptr; - hr = CoCreateInstance(CLSID_Shell, nullptr, CLSCTX_INPROC_SERVER, IID_IShellDispatch, (void**)&pShellApp); - if (FAILED(hr)) - { - LogError("Failed to create Shell Application: 0x%08x", hr); - CoUninitialize(); - return false; - } - - // Convert paths to BSTRs and then to VARIANTs - _bstr_t bstrZipPath(zipPath.c_str()); - _bstr_t bstrExtractPath(extractPath.c_str()); - - VARIANT vZipPath, vExtractPath; - vZipPath.vt = VT_BSTR; - vZipPath.bstrVal = bstrZipPath.Detach(); - vExtractPath.vt = VT_BSTR; - vExtractPath.bstrVal = bstrExtractPath.Detach(); - - // Get folder objects - Folder* pZipFolder = nullptr; - Folder* pDestFolder = nullptr; - - hr = pShellApp->NameSpace(vZipPath, &pZipFolder); - if (SUCCEEDED(hr) && pZipFolder) - { - hr = pShellApp->NameSpace(vExtractPath, &pDestFolder); - if (SUCCEEDED(hr) && pDestFolder) - { - // Get items from zip folder - FolderItems* pItems = nullptr; - hr = pZipFolder->Items(&pItems); - if (SUCCEEDED(hr) && pItems) - { - // Copy items with no progress dialog and overwrite existing - VARIANT vOptions; - vOptions.vt = VT_I4; - vOptions.lVal = 0x14; // FOF_NOCONFIRMATION | FOF_NOERRORUI - - hr = pDestFolder->CopyHere(_variant_t(pItems), vOptions); - if (SUCCEEDED(hr)) - { - LogInfo("Successfully extracted ZIP archive using Shell API"); - success = true; - } - else - { - LogError("Shell CopyHere failed: 0x%08x. If you see 'file in use' errors, please close Binary Ninja and try again.", hr); - } - - pItems->Release(); - } - else - { - LogError("Failed to get items from zip folder: 0x%08x", hr); - } - - pDestFolder->Release(); - } - else - { - LogError("Failed to get destination folder: 0x%08x", hr); - } - - pZipFolder->Release(); - } - else - { - LogError("Failed to open zip file as folder: 0x%08x", hr); - } - - // Clean up VARIANTs - VariantClear(&vZipPath); - VariantClear(&vExtractPath); - - pShellApp->Release(); - } - catch (...) - { - LogError("Exception during ZIP extraction"); - } - - CoUninitialize(); - return success; - } - - /// Extract a specific file from a ZIP archive using Windows Shell COM interface (secure) - /// @param zipPath Path to the ZIP file - /// @param fileName Name of file to extract - /// @param extractDir Directory where to extract the file - /// @return Path to extracted file, or empty string if extraction failed - std::string ExtractFileFromZip(const std::string& zipPath, const std::string& fileName, const std::string& extractDir) - { - LogInfo("Extracting %s from %s", fileName.c_str(), zipPath.c_str()); - - // Create destination directory if it doesn't exist - std::error_code ec; - fs::create_directories(extractDir, ec); - if (ec) - { - LogError("Failed to create extract directory: %s", ec.message().c_str()); - return ""; - } - - // Initialize COM - HRESULT hr = CoInitialize(nullptr); - if (FAILED(hr)) - { - LogError("Failed to initialize COM: 0x%08x", hr); - return ""; - } - - std::string outputPath; - try - { - // Create Shell Application object - IShellDispatch* pShellApp = nullptr; - hr = CoCreateInstance(CLSID_Shell, nullptr, CLSCTX_INPROC_SERVER, IID_IShellDispatch, (void**)&pShellApp); - if (FAILED(hr)) - { - LogError("Failed to create Shell Application: 0x%08x", hr); - CoUninitialize(); - return ""; - } - - // Convert paths to BSTRs and then to VARIANTs - _bstr_t bstrZipPath(zipPath.c_str()); - _bstr_t bstrExtractPath(extractDir.c_str()); - - VARIANT vZipPath, vExtractPath; - vZipPath.vt = VT_BSTR; - vZipPath.bstrVal = bstrZipPath.Detach(); - vExtractPath.vt = VT_BSTR; - vExtractPath.bstrVal = bstrExtractPath.Detach(); - - // Get folder objects - Folder* pZipFolder = nullptr; - Folder* pDestFolder = nullptr; - - hr = pShellApp->NameSpace(vZipPath, &pZipFolder); - if (SUCCEEDED(hr) && pZipFolder) - { - hr = pShellApp->NameSpace(vExtractPath, &pDestFolder); - if (SUCCEEDED(hr) && pDestFolder) - { - // Get items from zip folder - FolderItems* pItems = nullptr; - hr = pZipFolder->Items(&pItems); - if (SUCCEEDED(hr) && pItems) - { - // Look for specific file - long itemCount = 0; - pItems->get_Count(&itemCount); - - for (long i = 0; i < itemCount; i++) - { - VARIANT vIndex; - vIndex.vt = VT_I4; - vIndex.lVal = i; - - FolderItem* pItem = nullptr; - hr = pItems->Item(vIndex, &pItem); - if (SUCCEEDED(hr) && pItem) - { - BSTR bstrName = nullptr; - hr = pItem->get_Name(&bstrName); - if (SUCCEEDED(hr) && bstrName) - { - _bstr_t itemName(bstrName, false); // Don't copy, take ownership - LogInfo("Found item in ZIP archive: %s", (const char*)itemName); - - // Extract base name without extension for comparison - // This handles the case where Windows "Hide extensions for known file types" is enabled - std::string fileNameWithoutExt = fileName; - size_t lastDot = fileNameWithoutExt.find_last_of('.'); - if (lastDot != std::string::npos) - { - fileNameWithoutExt = fileNameWithoutExt.substr(0, lastDot); - } - - // Match either the full filename or the filename without extension - bool matches = (_stricmp(itemName, fileName.c_str()) == 0) || - (_stricmp(itemName, fileNameWithoutExt.c_str()) == 0); - - if (matches) - { - // Found the file, extract it - VARIANT vOptions; - vOptions.vt = VT_I4; - vOptions.lVal = 0x14; // FOF_NOCONFIRMATION | FOF_NOERRORUI - - hr = pDestFolder->CopyHere(_variant_t(pItem), vOptions); - if (SUCCEEDED(hr)) - { - outputPath = extractDir + "\\" + fileName; - LogInfo("Successfully extracted %s", fileName.c_str()); - } - else - { - LogError("Failed to extract file: 0x%08x", hr); - } - - pItem->Release(); - break; - } - } - - pItem->Release(); - } - } - - if (outputPath.empty()) - { - LogError("File %s not found in ZIP archive. This may indicate that Microsoft has changed the WinDbg package structure. Please report this issue to the Binary Ninja team.", fileName.c_str()); - } - - pItems->Release(); - } - else - { - LogError("Failed to get items from zip folder: 0x%08x", hr); - } - - pDestFolder->Release(); - } - else - { - LogError("Failed to get destination folder: 0x%08x", hr); - } - - pZipFolder->Release(); - } - else - { - LogError("Failed to open zip file as folder: 0x%08x", hr); - } - - // Clean up VARIANTs - VariantClear(&vZipPath); - VariantClear(&vExtractPath); - - pShellApp->Release(); - } - catch (...) - { - LogError("Exception during file extraction"); - } - - CoUninitialize(); - - // Verify the file exists before returning - if (!outputPath.empty() && fs::exists(outputPath)) - { - return outputPath; - } - - return ""; - } - - /// Get a temporary file path - /// @param extension File extension (with dot) - /// @return Path to temporary file - std::string GetTempFilePath(const std::string& extension) - { - char tempPath[MAX_PATH]; - - GetTempPathA(MAX_PATH, tempPath); - - // Generate a unique filename - GUID guid; - CoCreateGuid(&guid); - char guidStr[40]; - sprintf_s(guidStr, sizeof(guidStr), "{%08lX-%04hX-%04hX-%02hhX%02hhX-%02hhX%02hhX%02hhX%02hhX%02hhX%02hhX}", - guid.Data1, guid.Data2, guid.Data3, - guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], - guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]); - - std::string result = std::string(tempPath) + "windbg_" + guidStr + extension; - - return result; - } - - /// Parse XML to extract MSIX bundle URL - /// @param appInstallerPath Path to the appinstaller XML file - /// @return MSIX bundle URL, or empty string if parsing failed - std::string ParseAppInstallerXml(const std::string& appInstallerPath) - { - LogInfo("Parsing appinstaller XML: %s", appInstallerPath.c_str()); - - pugi::xml_document doc; - pugi::xml_parse_result result = doc.load_file(appInstallerPath.c_str()); - - if (!result) - { - LogError("Failed to parse XML: %s", result.description()); - return ""; - } - - // Look for MainBundle element with Uri attribute - pugi::xml_node mainBundle = doc.child("AppInstaller").child("MainBundle"); - if (!mainBundle) - { - LogError("MainBundle element not found in XML"); - return ""; - } - - pugi::xml_attribute uriAttr = mainBundle.attribute("Uri"); - if (!uriAttr) - { - LogError("Uri attribute not found in MainBundle element"); - return ""; - } - - std::string msixUrl = uriAttr.value(); - LogInfo("Found MSIX bundle URL: %s", msixUrl.c_str()); - return msixUrl; - } - } - - bool CheckInstallOk(const std::string& path) - { - // Check for required WinDbg/TTD files - std::vector requiredFiles = { - "amd64\\dbgeng.dll", - "amd64\\dbghelp.dll", - "amd64\\dbgmodel.dll", - "amd64\\dbgcore.dll", - "amd64\\ttd\\TTD.exe", - "amd64\\ttd\\TTDRecord.dll" - }; - - for (const auto& file : requiredFiles) - { - fs::path fullPath = fs::path(path) / file; - if (!fs::exists(fullPath)) - { - LogWarn("Required file not found: %s", fullPath.string().c_str()); - return false; - } - } - - return true; - } - - bool InstallWinDbg(InstallProgressCallback progressCallback) - { - try - { - LogInfo("Starting WinDbg/TTD installation"); - - if (progressCallback) - progressCallback("Initializing installation...", 0); - - // Step 1: Download appinstaller file - if (progressCallback) - progressCallback("Downloading WinDbg package information...", 10); - - const std::string ttdUrl = "https://aka.ms/windbg/download"; - std::string appInstallerPath = GetTempFilePath(".appinstaller"); - - if (!DownloadFile(ttdUrl, appInstallerPath)) - { - LogError("Failed to download appinstaller file"); - return false; - } - - // Step 2: Parse XML to get MSIX bundle URL - if (progressCallback) - progressCallback("Parsing package information...", 20); - - std::string msixUrl = ParseAppInstallerXml(appInstallerPath); - if (msixUrl.empty()) - { - LogError("Failed to parse appinstaller XML"); - return false; - } - - // Step 3: Download MSIX bundle - if (progressCallback) - progressCallback("Downloading WinDbg/TTD package...", 30); - - std::string msixPath = GetTempFilePath(".msixbundle.zip"); // Use .zip extension for COM Shell compatibility - if (!DownloadFile(msixUrl, msixPath)) - { - LogError("Failed to download MSIX bundle"); - return false; - } - - // Step 4: Extract inner MSIX file from bundle - if (progressCallback) - progressCallback("Extracting package contents...", 60); - - std::string tempExtractDir = GetTempFilePath("_extract"); - std::string innerMsixPath = ExtractFileFromZip(msixPath, "windbg_win-x64.msix", tempExtractDir); - if (innerMsixPath.empty()) - { - LogError("Failed to extract inner MSIX file"); - return false; - } - - // Rename the extracted MSIX file to have .zip extension for COM Shell compatibility - std::string innerZipPath = GetTempFilePath(".zip"); - std::error_code ec; - fs::rename(innerMsixPath, innerZipPath, ec); - if (ec) - { - LogError("Failed to rename inner MSIX file to .zip: %s", ec.message().c_str()); - return false; - } - LogInfo("Renamed %s to %s for COM Shell compatibility", innerMsixPath.c_str(), innerZipPath.c_str()); - - // Step 5: Extract WinDbg contents to installation directory - if (progressCallback) - progressCallback("Installing WinDbg/TTD files...", 80); - - std::string userDir = GetUserDirectory(); - std::string installTarget = (fs::path(userDir) / "windbg").string(); - - if (!ExtractZip(innerZipPath, installTarget)) - { - LogError("Failed to extract WinDbg contents"); - return false; - } - - // Step 6: Verify installation - if (progressCallback) - progressCallback("Verifying installation...", 90); - - if (!CheckInstallOk(installTarget)) - { - LogError("WinDbg/TTD installation appears successful, but important files are missing from %s", installTarget.c_str()); - return false; - } - - LogInfo("WinDbg/TTD installed to %s!", installTarget.c_str()); - - // Step 7: Update settings - if (progressCallback) - progressCallback("Configuring Binary Ninja settings...", 95); - - std::string x64dbgEngPath = (fs::path(installTarget) / "amd64").string(); - if (Settings::Instance()->Set("debugger.x64dbgEngPath", x64dbgEngPath)) - { - LogInfo("Updated debugger.x64dbgEngPath setting to: %s", x64dbgEngPath.c_str()); - LogInfo("Please restart Binary Ninja to make the changes take effect!"); - } - else - { - LogError("Failed to set debugger.x64dbgEngPath to %s", x64dbgEngPath.c_str()); - return false; - } - - // Cleanup temporary files - try - { - fs::remove(appInstallerPath); - fs::remove(msixPath); - fs::remove(innerZipPath); - fs::remove_all(tempExtractDir); - } - catch (...) - { - // Ignore cleanup errors - } - - if (progressCallback) - progressCallback("Installation completed successfully!", 100); - - return true; - } - catch (const std::exception& e) - { - LogError("Exception during WinDbg installation: %s", e.what()); - return false; - } - } -} - -#endif // WIN32 \ No newline at end of file diff --git a/ui/install_windbg.h b/ui/install_windbg.h deleted file mode 100644 index 464f20ae..00000000 --- a/ui/install_windbg.h +++ /dev/null @@ -1,42 +0,0 @@ -/* -Copyright 2020-2026 Vector 35 Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -#pragma once - -#ifdef WIN32 - -#include -#include - -namespace BinaryNinjaDebugger -{ - /// Progress callback function type for installation progress updates - /// @param step Current step description (e.g., "Downloading...", "Extracting...") - /// @param progress Progress percentage (0-100), or -1 for indeterminate - using InstallProgressCallback = std::function; - - /// Install WinDbg/TTD by downloading and extracting the MSIX package - /// @param progressCallback Optional callback for progress updates - /// @return true if installation was successful, false otherwise - bool InstallWinDbg(InstallProgressCallback progressCallback = nullptr); - - /// Check if WinDbg/TTD installation is valid at the given path - /// @param path Path to check for required WinDbg/TTD files - /// @return true if all required files are present, false otherwise - bool CheckInstallOk(const std::string& path); -} - -#endif // WIN32 \ No newline at end of file diff --git a/ui/ui.cpp b/ui/ui.cpp index 244ef530..5091570e 100644 --- a/ui/ui.cpp +++ b/ui/ui.cpp @@ -25,8 +25,11 @@ limitations under the License. #include "QPainter" #include #include +#include +#include #include #include +#include #include "fmt/format.h" #include "threadframes.h" #include "syncgroup.h" @@ -53,7 +56,7 @@ limitations under the License. #ifdef WIN32 #include "ttdrecord.h" #include "scriptingconsole.h" - #include "install_windbg.h" + #include "windbgupdatedialog.h" #endif @@ -1385,77 +1388,108 @@ void GlobalDebuggerUI::SetupMenu(UIContext* context) #ifdef WIN32 void GlobalDebuggerUI::installTTD(const UIActionContext& ctxt) { - // Check if WinDbg is already installed + // Determine install path std::string userDir = BinaryNinja::GetUserDirectory(); std::filesystem::path installTarget = std::filesystem::path(userDir) / "windbg"; - LogDebug("installTarget: %s", installTarget.string().c_str()); + std::string installPath = installTarget.string(); + LogDebug("installTarget: %s", installPath.c_str()); - if (std::filesystem::exists(installTarget) && BinaryNinjaDebugger::CheckInstallOk(installTarget.string())) + // Check if WinDbg is already installed + if (std::filesystem::exists(installTarget) && IsWinDbgInstalled(installPath)) { - QMessageBox::StandardButton reply = QMessageBox::information( - ctxt.context->mainWindow(), - "WinDbg Already Installed", - "WinDbg/TTD is already installed. Do you want to reinstall/update it?\n\n" - "IMPORTANT: Reinstallation will fail if Binary Ninja is currently running because the DbgEng DLLs are in use.\n\n" - "To reinstall/update:\n" - "1. Close Binary Ninja completely\n" - "2. Manually delete the folder: " + QString::fromStdString(installTarget.string()) + "\n" - "3. Restart Binary Ninja\n" - "4. Run this installation again\n\n", - QMessageBox::Ok - ); + // Get installed version + std::string installedVersion = GetWinDbgInstalledVersion(installPath); + if (installedVersion.empty()) { + installedVersion = "(unknown)"; + } + + // Show update dialog + WinDbgUpdateDialog dialog(ctxt.context->mainWindow(), installPath, installedVersion); + dialog.exec(); return; } - // Create and show progress dialog with actual progress range - QProgressDialog* progress = new QProgressDialog("Initializing installation...", nullptr, 0, 100, ctxt.context->mainWindow()); - progress->setWindowModality(Qt::WindowModal); - progress->setMinimumDuration(0); - progress->setCancelButton(nullptr); // No cancel button since we can't safely cancel mid-installation - progress->show(); - QCoreApplication::processEvents(); - - // Use QTimer to run installation asynchronously - QTimer::singleShot(100, [progress]() { - bool success = false; - try - { - // Create progress callback to update the dialog - auto progressCallback = [progress](const std::string& step, int progressPercent) { - QMetaObject::invokeMethod(progress, [progress, step, progressPercent]() { - progress->setLabelText(QString::fromStdString(step)); - if (progressPercent >= 0 && progressPercent <= 100) - { - progress->setValue(progressPercent); - } - QCoreApplication::processEvents(); - }, Qt::QueuedConnection); - }; + // Not installed - proceed with fresh installation + QWidget* mainWindow = ctxt.context->mainWindow(); + + // Show confirmation dialog first + QMessageBox::StandardButton reply = QMessageBox::question( + mainWindow, + "Install WinDbg/TTD", + "The WinDbg/TTD installer will be launched in a separate window.\n\n" + "You can continue using Binary Ninja while the installation proceeds.\n" + "You will be notified when the installation completes.\n\n" + "Do you want to continue?", + QMessageBox::Yes | QMessageBox::No, + QMessageBox::Yes + ); + + if (reply != QMessageBox::Yes) { + return; + } - success = BinaryNinjaDebugger::InstallWinDbg(progressCallback); - } - catch (...) - { - success = false; - } + // Create and start background installation task + class InstallWorker : public QThread { + public: + InstallWorker(const std::string& path, QObject* parent = nullptr) + : QThread(parent), m_installPath(path) {} - progress->close(); - progress->deleteLater(); - - if (success) - { - QMessageBox::information(nullptr, "Installation Complete", - "WinDbg/TTD has been successfully installed!\n\n" - "Please restart Binary Ninja to make the changes take effect."); + void run() override { + m_result = InstallWinDbg(m_installPath); } - else - { - QMessageBox::warning(nullptr, "Installation Failed", - "Failed to install WinDbg/TTD. Please check the log for details.\n\n" - "You can also install WinDbg manually by following the documentation:\n" - "https://docs.binary.ninja/guide/debugger/dbgeng-ttd.html#install-windbg-manually"); + + const InstallResult& result() const { return m_result; } + const std::string& installPath() const { return m_installPath; } + + private: + std::string m_installPath; + InstallResult m_result; + }; + + InstallWorker* worker = new InstallWorker(installPath, mainWindow); + + // When installation completes, show result dialog and configure settings + QObject::connect(worker, &QThread::finished, mainWindow, [worker, installPath, mainWindow]() { + const InstallResult& result = worker->result(); + worker->deleteLater(); + + if (result.success && IsWinDbgInstalled(installPath)) { + // Configure debugger settings + std::string dbgEngPath = installPath + "\\amd64"; + BinaryNinja::Settings::Instance()->Set("debugger.x64dbgEngPath", dbgEngPath); + LogInfo("Configured debugger.x64dbgEngPath: %s", dbgEngPath.c_str()); + + // Offer to restart Binary Ninja + QMessageBox msgBox(mainWindow); + msgBox.setWindowTitle("Installation Successful"); + msgBox.setText("WinDbg/TTD has been installed successfully!"); + msgBox.setInformativeText("The debugger settings have been configured automatically.\n\n" + "Would you like to restart Binary Ninja now?"); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + msgBox.setDefaultButton(QMessageBox::No); + msgBox.button(QMessageBox::Yes)->setText("Restart Now"); + msgBox.button(QMessageBox::No)->setText("Restart Later"); + + if (msgBox.exec() == QMessageBox::Yes) { + // Restart Binary Ninja by spawning a new instance before quitting + QStringList args = QCoreApplication::arguments(); + QString program = args.takeFirst(); + QProcess::startDetached(program, args); + QApplication::quit(); + } + } else { + // Show error message with specific failure reason + QString errorMsg = "WinDbg/TTD installation failed."; + if (!result.errorMessage.empty()) { + errorMsg += "\n\nError: " + QString::fromStdString(result.errorMessage); + } else { + errorMsg += "\n\nPlease check the installer console window for error details."; + } + QMessageBox::critical(mainWindow, "Installation Failed", errorMsg); } }); + + worker->start(); } #endif diff --git a/ui/windbgupdatedialog.cpp b/ui/windbgupdatedialog.cpp new file mode 100644 index 00000000..af07b6d2 --- /dev/null +++ b/ui/windbgupdatedialog.cpp @@ -0,0 +1,198 @@ +/* +Copyright 2020-2026 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifdef WIN32 + +#include "windbgupdatedialog.h" +#include "debuggerapi.h" +#include "progresstask.h" +#include +#include +#include +#include + +using namespace BinaryNinjaDebuggerAPI; + +WinDbgUpdateDialog::WinDbgUpdateDialog(QWidget* parent, const std::string& installPath, const std::string& installedVersion) + : QDialog(parent), m_installPath(installPath), m_installedVersion(installedVersion) +{ + setWindowTitle("WinDbg/TTD Update"); + setMinimumWidth(450); + + QVBoxLayout* mainLayout = new QVBoxLayout(this); + + /* Version information group */ + QGroupBox* versionGroup = new QGroupBox("Version Information", this); + QVBoxLayout* versionLayout = new QVBoxLayout(versionGroup); + + QHBoxLayout* installedLayout = new QHBoxLayout(); + installedLayout->addWidget(new QLabel("Installed version:", this)); + if (m_installedVersion.empty()) { + m_installedVersionLabel = new QLabel("Unknown", this); + m_installedVersionLabel->setStyleSheet("font-weight: bold; color: gray;"); + } else { + m_installedVersionLabel = new QLabel(QString::fromStdString(m_installedVersion), this); + m_installedVersionLabel->setStyleSheet("font-weight: bold;"); + } + installedLayout->addWidget(m_installedVersionLabel); + installedLayout->addStretch(); + versionLayout->addLayout(installedLayout); + + QHBoxLayout* latestLayout = new QHBoxLayout(); + latestLayout->addWidget(new QLabel("Latest version:", this)); + m_latestVersionLabel = new QLabel("Checking...", this); + m_latestVersionLabel->setStyleSheet("font-weight: bold; color: gray;"); + latestLayout->addWidget(m_latestVersionLabel); + latestLayout->addStretch(); + versionLayout->addLayout(latestLayout); + + mainLayout->addWidget(versionGroup); + + /* Status/explanation label */ + m_statusLabel = new QLabel(this); + m_statusLabel->setWordWrap(true); + m_statusLabel->setText( + "To update or reinstall WinDbg/TTD, Binary Ninja must be closed first.\n\n" + "Clicking 'Update' will:\n" + "1. Close Binary Ninja\n" + "2. Launch the installer to download and install the latest version\n" + "3. You can restart Binary Ninja after the installation completes" + ); + mainLayout->addWidget(m_statusLabel); + + mainLayout->addStretch(); + + /* Buttons */ + QHBoxLayout* buttonLayout = new QHBoxLayout(); + buttonLayout->addStretch(); + + m_cancelButton = new QPushButton("Cancel", this); + connect(m_cancelButton, &QPushButton::clicked, this, &WinDbgUpdateDialog::onCancelClicked); + buttonLayout->addWidget(m_cancelButton); + + m_updateButton = new QPushButton("Update", this); + m_updateButton->setDefault(true); + connect(m_updateButton, &QPushButton::clicked, this, &WinDbgUpdateDialog::onUpdateClicked); + buttonLayout->addWidget(m_updateButton); + + mainLayout->addLayout(buttonLayout); + + /* Connect signal for thread-safe UI update */ + connect(this, &WinDbgUpdateDialog::latestVersionReceived, + this, &WinDbgUpdateDialog::onLatestVersionReceived); + + /* Start fetching latest version in background */ + fetchLatestVersion(); +} + +void WinDbgUpdateDialog::fetchLatestVersion() +{ + /* Fetch in background thread */ + std::thread([this]() { + std::string version = GetWinDbgLatestVersion(); + emit latestVersionReceived(QString::fromStdString(version)); + }).detach(); +} + +void WinDbgUpdateDialog::onLatestVersionReceived(const QString& version) +{ + m_latestVersion = version.toStdString(); + updateUI(); +} + +void WinDbgUpdateDialog::updateUI() +{ + if (m_latestVersion.empty()) { + m_latestVersionLabel->setText("Unable to check"); + m_latestVersionLabel->setStyleSheet("font-weight: bold; color: red;"); + /* Can still reinstall even if we can't check latest version */ + m_statusLabel->setText( + "Unable to check for the latest version.\n\n" + "You can still reinstall the current version by clicking 'Reinstall'. " + "Binary Ninja will be closed and the installer will run." + ); + m_updateButton->setText("Reinstall"); + } else { + m_latestVersionLabel->setText(QString::fromStdString(m_latestVersion)); + + /* Handle case where installed version is unknown */ + if (m_installedVersion.empty()) { + m_latestVersionLabel->setStyleSheet("font-weight: bold; color: orange;"); + m_statusLabel->setText( + "Unable to determine installed version.\n\n" + "Click 'Reinstall' to install the latest version. " + "Binary Ninja will be closed and the installer will run." + ); + m_updateButton->setText("Reinstall"); + } else if (m_latestVersion == m_installedVersion) { + m_latestVersionLabel->setStyleSheet("font-weight: bold; color: green;"); + m_statusLabel->setText( + "You already have the latest version installed.\n\n" + "If you want to reinstall anyway, click 'Reinstall'. " + "Binary Ninja will be closed and the installer will run." + ); + m_updateButton->setText("Reinstall"); + } else { + m_latestVersionLabel->setStyleSheet("font-weight: bold; color: orange;"); + m_statusLabel->setText( + "A newer version is available!\n\n" + "Clicking 'Update' will:\n" + "1. Close Binary Ninja\n" + "2. Launch the installer to download and install the latest version\n" + "3. You can restart Binary Ninja after the installation completes" + ); + m_updateButton->setText("Update"); + } + } +} + +void WinDbgUpdateDialog::onUpdateClicked() +{ + /* Confirm with user */ + QMessageBox::StandardButton reply = QMessageBox::question( + this, + "Confirm Update", + "Binary Ninja will now close and the WinDbg/TTD installer will start.\n\n" + "Do you want to continue?", + QMessageBox::Yes | QMessageBox::No, + QMessageBox::Yes + ); + + if (reply != QMessageBox::Yes) { + return; + } + + /* Launch installer with --wait-for-binja flag (CLI will wait for binja to exit) */ + std::string installPath = m_installPath; + + /* Start installer in background - it will wait for Binary Ninja to exit */ + std::thread([installPath]() { + (void)InstallWinDbg(installPath, true /* isUpdate */); + }).detach(); + + /* Accept dialog and signal to close Binary Ninja */ + accept(); + + /* Close Binary Ninja */ + QApplication::quit(); +} + +void WinDbgUpdateDialog::onCancelClicked() +{ + reject(); +} + +#endif // WIN32 diff --git a/ui/windbgupdatedialog.h b/ui/windbgupdatedialog.h new file mode 100644 index 00000000..0ec49f36 --- /dev/null +++ b/ui/windbgupdatedialog.h @@ -0,0 +1,58 @@ +/* +Copyright 2020-2026 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#pragma once + +#ifdef WIN32 + +#include +#include +#include +#include +#include +#include + +class WinDbgUpdateDialog : public QDialog +{ + Q_OBJECT + +private: + std::string m_installPath; + std::string m_installedVersion; + std::string m_latestVersion; + + QLabel* m_installedVersionLabel; + QLabel* m_latestVersionLabel; + QLabel* m_statusLabel; + QPushButton* m_updateButton; + QPushButton* m_cancelButton; + + void fetchLatestVersion(); + void updateUI(); + +public: + WinDbgUpdateDialog(QWidget* parent, const std::string& installPath, const std::string& installedVersion); + +public Q_SLOTS: + void onLatestVersionReceived(const QString& version); + void onUpdateClicked(); + void onCancelClicked(); + +Q_SIGNALS: + void latestVersionReceived(const QString& version); +}; + +#endif // WIN32 diff --git a/vendor/minizip-ng/mz.h b/vendor/minizip-ng/mz.h new file mode 100644 index 00000000..4e8b8cac --- /dev/null +++ b/vendor/minizip-ng/mz.h @@ -0,0 +1,44 @@ +/* + * Minimal ZIP library for WinDbg installer + * Based on ZIP file format specification + * + * Copyright 2020-2026 Vector 35 Inc. + * Licensed under the Apache License, Version 2.0 + */ + +#ifndef MZ_H +#define MZ_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Error codes */ +#define MZ_OK 0 +#define MZ_STREAM_ERROR (-1) +#define MZ_DATA_ERROR (-2) +#define MZ_MEM_ERROR (-3) +#define MZ_END_ERROR (-4) +#define MZ_OPEN_ERROR (-5) +#define MZ_CLOSE_ERROR (-6) +#define MZ_SEEK_ERROR (-7) +#define MZ_EXIST_ERROR (-8) +#define MZ_PARAM_ERROR (-9) + +/* Compression methods */ +#define MZ_COMPRESS_METHOD_STORE 0 +#define MZ_COMPRESS_METHOD_DEFLATE 8 + +/* ZIP signatures */ +#define MZ_ZIP_SIGNATURE_LOCALHEADER 0x04034b50 +#define MZ_ZIP_SIGNATURE_DATADESCRIPTOR 0x08074b50 +#define MZ_ZIP_SIGNATURE_CENTRALDIR 0x02014b50 +#define MZ_ZIP_SIGNATURE_ENDOFCENTRALDIR 0x06054b50 + +#ifdef __cplusplus +} +#endif + +#endif /* MZ_H */ diff --git a/vendor/minizip-ng/mz_inflate.cpp b/vendor/minizip-ng/mz_inflate.cpp new file mode 100644 index 00000000..f19f4083 --- /dev/null +++ b/vendor/minizip-ng/mz_inflate.cpp @@ -0,0 +1,356 @@ +/* + * Minimal DEFLATE decompressor implementation + * Based on RFC 1951 + * + * Copyright 2020-2026 Vector 35 Inc. + * Licensed under the Apache License, Version 2.0 + */ + +#include "mz_inflate.h" +#include "mz.h" +#include +#include + +namespace mz { + +namespace { + +/* Bit reader for reading variable-length bit fields */ +class BitReader { +public: + BitReader(const uint8_t* data, size_t size) + : m_data(data), m_size(size), m_pos(0), m_bitBuf(0), m_bitCount(0) {} + + bool HasBits(int count) const { + return m_pos < m_size || m_bitCount >= count; + } + + uint32_t ReadBits(int count) { + while (m_bitCount < count && m_pos < m_size) { + m_bitBuf |= (uint32_t)m_data[m_pos++] << m_bitCount; + m_bitCount += 8; + } + uint32_t value = m_bitBuf & ((1u << count) - 1); + m_bitBuf >>= count; + m_bitCount -= count; + return value; + } + + uint32_t PeekBits(int count) { + while (m_bitCount < count && m_pos < m_size) { + m_bitBuf |= (uint32_t)m_data[m_pos++] << m_bitCount; + m_bitCount += 8; + } + return m_bitBuf & ((1u << count) - 1); + } + + void DropBits(int count) { + m_bitBuf >>= count; + m_bitCount -= count; + } + + void AlignToByte() { + m_bitBuf >>= (m_bitCount & 7); + m_bitCount &= ~7; + } + + size_t BytePos() const { return m_pos; } + const uint8_t* Data() const { return m_data; } + size_t Size() const { return m_size; } + +private: + const uint8_t* m_data; + size_t m_size; + size_t m_pos; + uint32_t m_bitBuf; + int m_bitCount; +}; + +/* Huffman decoder */ +class HuffmanDecoder { +public: + static constexpr int MAX_BITS = 15; + static constexpr int MAX_SYMBOLS = 288; + + HuffmanDecoder() : m_maxCode(0) { + std::memset(m_counts, 0, sizeof(m_counts)); + std::memset(m_symbols, 0, sizeof(m_symbols)); + std::memset(m_firstCode, 0, sizeof(m_firstCode)); + std::memset(m_firstSymIdx, 0, sizeof(m_firstSymIdx)); + } + + bool Build(const uint8_t* lengths, int numSymbols) { + std::memset(m_counts, 0, sizeof(m_counts)); + + /* Count code lengths */ + for (int i = 0; i < numSymbols; i++) { + if (lengths[i] > MAX_BITS) return false; + m_counts[lengths[i]]++; + } + m_counts[0] = 0; + + /* Find max code length */ + m_maxCode = MAX_BITS; + while (m_maxCode > 0 && m_counts[m_maxCode] == 0) m_maxCode--; + + /* Calculate first code for each length */ + uint32_t code = 0; + int symIdx = 0; + for (int len = 1; len <= m_maxCode; len++) { + m_firstCode[len] = code; + m_firstSymIdx[len] = symIdx; + code += m_counts[len]; + symIdx += m_counts[len]; + code <<= 1; + } + + /* Build symbol table sorted by code */ + std::memset(m_symbols, 0, sizeof(m_symbols)); + int nextIdx[MAX_BITS + 1]; + for (int i = 0; i <= MAX_BITS; i++) { + nextIdx[i] = m_firstSymIdx[i]; + } + + for (int sym = 0; sym < numSymbols; sym++) { + int len = lengths[sym]; + if (len > 0) { + m_symbols[nextIdx[len]++] = sym; + } + } + + return true; + } + + int Decode(BitReader& br) const { + uint32_t code = 0; + for (int len = 1; len <= m_maxCode; len++) { + if (!br.HasBits(1)) return -1; + code = (code << 1) | br.ReadBits(1); + int count = m_counts[len]; + if (count > 0) { + uint32_t first = m_firstCode[len]; + if (code >= first && code < first + count) { + return m_symbols[m_firstSymIdx[len] + (code - first)]; + } + } + } + return -1; + } + +private: + int m_counts[MAX_BITS + 1]; + int m_symbols[MAX_SYMBOLS]; + uint32_t m_firstCode[MAX_BITS + 1]; + int m_firstSymIdx[MAX_BITS + 1]; + int m_maxCode; +}; + +/* Fixed Huffman tables for literals/lengths */ +static const uint8_t kFixedLitLenLengths[288] = { + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8 +}; + +/* Fixed Huffman tables for distances */ +static const uint8_t kFixedDistLengths[32] = { + 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5 +}; + +/* Length code extra bits and base values */ +static const uint8_t kLengthExtraBits[29] = { + 0,0,0,0,0,0,0,0, 1,1,1,1, 2,2,2,2, 3,3,3,3, 4,4,4,4, 5,5,5,5, 0 +}; +static const uint16_t kLengthBase[29] = { + 3,4,5,6,7,8,9,10, 11,13,15,17, 19,23,27,31, 35,43,51,59, 67,83,99,115, 131,163,195,227, 258 +}; + +/* Distance code extra bits and base values */ +static const uint8_t kDistExtraBits[30] = { + 0,0,0,0, 1,1,2,2, 3,3,4,4, 5,5,6,6, 7,7,8,8, 9,9,10,10, 11,11,12,12, 13,13 +}; +static const uint16_t kDistBase[30] = { + 1,2,3,4, 5,7,9,13, 17,25,33,49, 65,97,129,193, 257,385,513,769, + 1025,1537,2049,3073, 4097,6145,8193,12289, 16385,24577 +}; + +/* Code length alphabet order */ +static const uint8_t kCodeLengthOrder[19] = { + 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 +}; + +int InflateBlock(BitReader& br, uint8_t* output, size_t outputSize, size_t& outPos, + const HuffmanDecoder& litLenDecoder, const HuffmanDecoder& distDecoder) { + while (true) { + int sym = litLenDecoder.Decode(br); + if (sym < 0) return MZ_DATA_ERROR; + + if (sym < 256) { + /* Literal byte */ + if (outPos >= outputSize) return MZ_DATA_ERROR; + output[outPos++] = (uint8_t)sym; + } else if (sym == 256) { + /* End of block */ + return MZ_OK; + } else { + /* Length/distance pair */ + int lenIdx = sym - 257; + if (lenIdx >= 29) return MZ_DATA_ERROR; + + int length = kLengthBase[lenIdx]; + int extraBits = kLengthExtraBits[lenIdx]; + if (extraBits > 0) { + if (!br.HasBits(extraBits)) return MZ_DATA_ERROR; + length += br.ReadBits(extraBits); + } + + int distSym = distDecoder.Decode(br); + if (distSym < 0 || distSym >= 30) return MZ_DATA_ERROR; + + int distance = kDistBase[distSym]; + extraBits = kDistExtraBits[distSym]; + if (extraBits > 0) { + if (!br.HasBits(extraBits)) return MZ_DATA_ERROR; + distance += br.ReadBits(extraBits); + } + + /* Copy from back reference */ + if ((size_t)distance > outPos) return MZ_DATA_ERROR; + if (outPos + length > outputSize) return MZ_DATA_ERROR; + + size_t srcPos = outPos - distance; + for (int i = 0; i < length; i++) { + output[outPos++] = output[srcPos++]; + } + } + } +} + +} // anonymous namespace + +int Inflate(const uint8_t* input, size_t inputSize, + uint8_t* output, size_t outputSize, + size_t* bytesWritten) { + if (!input || !output || inputSize == 0 || outputSize == 0) { + return MZ_PARAM_ERROR; + } + + BitReader br(input, inputSize); + size_t outPos = 0; + bool finalBlock = false; + + while (!finalBlock) { + if (!br.HasBits(3)) return MZ_DATA_ERROR; + + finalBlock = br.ReadBits(1) != 0; + int blockType = br.ReadBits(2); + + if (blockType == 0) { + /* Stored block (no compression) */ + br.AlignToByte(); + + if (!br.HasBits(32)) return MZ_DATA_ERROR; + uint16_t len = br.ReadBits(16); + uint16_t nlen = br.ReadBits(16); + + if ((len ^ nlen) != 0xFFFF) return MZ_DATA_ERROR; + + size_t pos = br.BytePos(); + if (pos + len > br.Size()) return MZ_DATA_ERROR; + if (outPos + len > outputSize) return MZ_DATA_ERROR; + + std::memcpy(output + outPos, br.Data() + pos, len); + outPos += len; + + /* Skip past the stored data in bit reader */ + for (uint16_t i = 0; i < len; i++) { + br.ReadBits(8); + } + } else if (blockType == 1) { + /* Fixed Huffman codes */ + HuffmanDecoder litLenDecoder, distDecoder; + + if (!litLenDecoder.Build(kFixedLitLenLengths, 288)) return MZ_DATA_ERROR; + if (!distDecoder.Build(kFixedDistLengths, 32)) return MZ_DATA_ERROR; + + int result = InflateBlock(br, output, outputSize, outPos, litLenDecoder, distDecoder); + if (result != MZ_OK) return result; + } else if (blockType == 2) { + /* Dynamic Huffman codes */ + if (!br.HasBits(14)) return MZ_DATA_ERROR; + + int hlit = br.ReadBits(5) + 257; + int hdist = br.ReadBits(5) + 1; + int hclen = br.ReadBits(4) + 4; + + if (hlit > 286 || hdist > 30) return MZ_DATA_ERROR; + + /* Read code length code lengths */ + uint8_t codeLengthLengths[19] = {0}; + for (int i = 0; i < hclen; i++) { + if (!br.HasBits(3)) return MZ_DATA_ERROR; + codeLengthLengths[kCodeLengthOrder[i]] = br.ReadBits(3); + } + + HuffmanDecoder codeLengthDecoder; + if (!codeLengthDecoder.Build(codeLengthLengths, 19)) return MZ_DATA_ERROR; + + /* Read literal/length and distance code lengths */ + uint8_t lengths[286 + 30]; + int totalCodes = hlit + hdist; + int i = 0; + + while (i < totalCodes) { + int sym = codeLengthDecoder.Decode(br); + if (sym < 0) return MZ_DATA_ERROR; + + if (sym < 16) { + lengths[i++] = sym; + } else if (sym == 16) { + if (i == 0) return MZ_DATA_ERROR; + if (!br.HasBits(2)) return MZ_DATA_ERROR; + int repeat = br.ReadBits(2) + 3; + uint8_t prevLen = lengths[i - 1]; + while (repeat-- > 0 && i < totalCodes) { + lengths[i++] = prevLen; + } + } else if (sym == 17) { + if (!br.HasBits(3)) return MZ_DATA_ERROR; + int repeat = br.ReadBits(3) + 3; + while (repeat-- > 0 && i < totalCodes) { + lengths[i++] = 0; + } + } else if (sym == 18) { + if (!br.HasBits(7)) return MZ_DATA_ERROR; + int repeat = br.ReadBits(7) + 11; + while (repeat-- > 0 && i < totalCodes) { + lengths[i++] = 0; + } + } else { + return MZ_DATA_ERROR; + } + } + + HuffmanDecoder litLenDecoder, distDecoder; + if (!litLenDecoder.Build(lengths, hlit)) return MZ_DATA_ERROR; + if (!distDecoder.Build(lengths + hlit, hdist)) return MZ_DATA_ERROR; + + int result = InflateBlock(br, output, outputSize, outPos, litLenDecoder, distDecoder); + if (result != MZ_OK) return result; + } else { + /* Reserved block type */ + return MZ_DATA_ERROR; + } + } + + if (bytesWritten) *bytesWritten = outPos; + return MZ_OK; +} + +} // namespace mz diff --git a/vendor/minizip-ng/mz_inflate.h b/vendor/minizip-ng/mz_inflate.h new file mode 100644 index 00000000..6fa9b7e8 --- /dev/null +++ b/vendor/minizip-ng/mz_inflate.h @@ -0,0 +1,33 @@ +/* + * Minimal DEFLATE decompressor + * Based on RFC 1951 + * + * Copyright 2020-2026 Vector 35 Inc. + * Licensed under the Apache License, Version 2.0 + */ + +#ifndef MZ_INFLATE_H +#define MZ_INFLATE_H + +#include +#include + +namespace mz { + +/* + * Decompress DEFLATE compressed data + * + * @param input Pointer to compressed data + * @param inputSize Size of compressed data in bytes + * @param output Pointer to output buffer + * @param outputSize Size of output buffer in bytes + * @param bytesWritten Pointer to receive actual bytes written (optional) + * @return 0 on success, negative error code on failure + */ +int Inflate(const uint8_t* input, size_t inputSize, + uint8_t* output, size_t outputSize, + size_t* bytesWritten = nullptr); + +} // namespace mz + +#endif /* MZ_INFLATE_H */ diff --git a/vendor/minizip-ng/mz_zip.cpp b/vendor/minizip-ng/mz_zip.cpp new file mode 100644 index 00000000..667960c4 --- /dev/null +++ b/vendor/minizip-ng/mz_zip.cpp @@ -0,0 +1,471 @@ +/* + * Minimal ZIP library - ZIP file handling implementation + * + * Copyright 2020-2026 Vector 35 Inc. + * Licensed under the Apache License, Version 2.0 + */ + +#include "mz_zip.h" +#include "mz_inflate.h" +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +namespace fs = std::filesystem; + +namespace mz { + +namespace { + +#pragma pack(push, 1) +struct LocalFileHeader { + uint32_t signature; + uint16_t versionNeeded; + uint16_t flags; + uint16_t compressionMethod; + uint16_t lastModTime; + uint16_t lastModDate; + uint32_t crc32; + uint32_t compressedSize; + uint32_t uncompressedSize; + uint16_t filenameLength; + uint16_t extraFieldLength; +}; + +struct CentralDirHeader { + uint32_t signature; + uint16_t versionMade; + uint16_t versionNeeded; + uint16_t flags; + uint16_t compressionMethod; + uint16_t lastModTime; + uint16_t lastModDate; + uint32_t crc32; + uint32_t compressedSize; + uint32_t uncompressedSize; + uint16_t filenameLength; + uint16_t extraFieldLength; + uint16_t commentLength; + uint16_t diskStart; + uint16_t internalAttr; + uint32_t externalAttr; + uint32_t localHeaderOffset; +}; + +struct EndOfCentralDir { + uint32_t signature; + uint16_t diskNumber; + uint16_t diskWithCentralDir; + uint16_t entriesOnDisk; + uint16_t totalEntries; + uint32_t centralDirSize; + uint32_t centralDirOffset; + uint16_t commentLength; +}; + +struct Zip64EndOfCentralDir { + uint32_t signature; + uint64_t recordSize; + uint16_t versionMade; + uint16_t versionNeeded; + uint32_t diskNumber; + uint32_t diskWithCentralDir; + uint64_t entriesOnDisk; + uint64_t totalEntries; + uint64_t centralDirSize; + uint64_t centralDirOffset; +}; + +struct Zip64Locator { + uint32_t signature; + uint32_t diskWithZip64End; + uint64_t zip64EndOffset; + uint32_t totalDisks; +}; +#pragma pack(pop) + +bool CaseInsensitiveCompare(const std::string& a, const std::string& b) { +#ifdef _WIN32 + return _stricmp(a.c_str(), b.c_str()) == 0; +#else + if (a.size() != b.size()) return false; + for (size_t i = 0; i < a.size(); i++) { + if (tolower(a[i]) != tolower(b[i])) return false; + } + return true; +#endif +} + +} // anonymous namespace + +ZipReader::ZipReader() + : m_file(nullptr), m_isOpen(false) { +} + +ZipReader::~ZipReader() { + Close(); +} + +int ZipReader::Open(const std::string& path) { + if (m_isOpen) { + Close(); + } + + FILE* fp = nullptr; +#ifdef _WIN32 + fopen_s(&fp, path.c_str(), "rb"); +#else + fp = fopen(path.c_str(), "rb"); +#endif + + if (!fp) { + return MZ_OPEN_ERROR; + } + + m_file = fp; + m_path = path; + m_isOpen = true; + + int result = ReadCentralDirectory(); + if (result != MZ_OK) { + Close(); + return result; + } + + return MZ_OK; +} + +int ZipReader::Close() { + if (m_file) { + fclose((FILE*)m_file); + m_file = nullptr; + } + m_entries.clear(); + m_path.clear(); + m_isOpen = false; + return MZ_OK; +} + +const std::vector& ZipReader::GetEntries() const { + return m_entries; +} + +size_t ZipReader::GetEntryCount() const { + return m_entries.size(); +} + +const ZipEntry* ZipReader::FindEntry(const std::string& filename) const { + for (const auto& entry : m_entries) { + if (CaseInsensitiveCompare(entry.filename, filename)) { + return &entry; + } + /* Also try without extension for Windows "hide extensions" compatibility */ + size_t dotPos = filename.rfind('.'); + if (dotPos != std::string::npos) { + std::string nameWithoutExt = filename.substr(0, dotPos); + if (CaseInsensitiveCompare(entry.filename, nameWithoutExt)) { + return &entry; + } + } + } + return nullptr; +} + +int ZipReader::ReadCentralDirectory() { + FILE* fp = (FILE*)m_file; + + /* Find end of central directory */ + fseek(fp, 0, SEEK_END); + long fileSize = ftell(fp); + + if (fileSize < (long)sizeof(EndOfCentralDir)) { + return MZ_DATA_ERROR; + } + + /* Search for EOCD signature from end of file */ + int searchLen = (int)std::min((long)65535 + 22, fileSize); + std::vector buffer(searchLen); + + fseek(fp, fileSize - searchLen, SEEK_SET); + if (fread(buffer.data(), 1, searchLen, fp) != (size_t)searchLen) { + return MZ_DATA_ERROR; + } + + int eocdOffset = -1; + for (int i = searchLen - (int)sizeof(EndOfCentralDir); i >= 0; i--) { + if (*(uint32_t*)(buffer.data() + i) == MZ_ZIP_SIGNATURE_ENDOFCENTRALDIR) { + eocdOffset = fileSize - searchLen + i; + break; + } + } + + if (eocdOffset < 0) { + return MZ_DATA_ERROR; + } + + /* Read EOCD */ + fseek(fp, eocdOffset, SEEK_SET); + EndOfCentralDir eocd; + if (fread(&eocd, sizeof(eocd), 1, fp) != 1) { + return MZ_DATA_ERROR; + } + + uint64_t centralDirOffset = eocd.centralDirOffset; + uint64_t totalEntries = eocd.totalEntries; + + /* Check for ZIP64 */ + if (eocd.centralDirOffset == 0xFFFFFFFF || eocd.totalEntries == 0xFFFF) { + /* Look for ZIP64 locator */ + if (eocdOffset >= (int)sizeof(Zip64Locator)) { + fseek(fp, eocdOffset - sizeof(Zip64Locator), SEEK_SET); + Zip64Locator locator; + if (fread(&locator, sizeof(locator), 1, fp) == 1 && + locator.signature == 0x07064b50) { + /* Read ZIP64 EOCD */ + fseek(fp, (long)locator.zip64EndOffset, SEEK_SET); + Zip64EndOfCentralDir zip64Eocd; + if (fread(&zip64Eocd, sizeof(zip64Eocd), 1, fp) == 1 && + zip64Eocd.signature == 0x06064b50) { + centralDirOffset = zip64Eocd.centralDirOffset; + totalEntries = zip64Eocd.totalEntries; + } + } + } + } + + /* Read central directory entries */ + fseek(fp, (long)centralDirOffset, SEEK_SET); + m_entries.reserve((size_t)totalEntries); + + for (uint64_t i = 0; i < totalEntries; i++) { + CentralDirHeader header; + if (fread(&header, sizeof(header), 1, fp) != 1) { + return MZ_DATA_ERROR; + } + + if (header.signature != MZ_ZIP_SIGNATURE_CENTRALDIR) { + return MZ_DATA_ERROR; + } + + /* Read filename */ + std::string filename(header.filenameLength, '\0'); + if (fread(&filename[0], 1, header.filenameLength, fp) != header.filenameLength) { + return MZ_DATA_ERROR; + } + + /* Skip extra field and comment */ + fseek(fp, header.extraFieldLength + header.commentLength, SEEK_CUR); + + /* Handle ZIP64 extended info if needed */ + uint64_t uncompressedSize = header.uncompressedSize; + uint64_t compressedSize = header.compressedSize; + uint64_t localHeaderOffset = header.localHeaderOffset; + + if (header.uncompressedSize == 0xFFFFFFFF || + header.compressedSize == 0xFFFFFFFF || + header.localHeaderOffset == 0xFFFFFFFF) { + /* Need to read ZIP64 extra field - for now use 32-bit values */ + /* TODO: Properly parse ZIP64 extra field */ + } + + ZipEntry entry; + entry.filename = filename; + entry.compressedSize = compressedSize; + entry.uncompressedSize = uncompressedSize; + entry.crc32 = header.crc32; + entry.compressionMethod = header.compressionMethod; + entry.localHeaderOffset = localHeaderOffset; + entry.isDirectory = !filename.empty() && (filename.back() == '/' || filename.back() == '\\'); + + m_entries.push_back(entry); + } + + return MZ_OK; +} + +int ZipReader::ReadLocalFileHeader(uint64_t offset, uint16_t& headerSize) { + FILE* fp = (FILE*)m_file; + + fseek(fp, (long)offset, SEEK_SET); + LocalFileHeader header; + if (fread(&header, sizeof(header), 1, fp) != 1) { + return MZ_DATA_ERROR; + } + + if (header.signature != MZ_ZIP_SIGNATURE_LOCALHEADER) { + return MZ_DATA_ERROR; + } + + headerSize = sizeof(LocalFileHeader) + header.filenameLength + header.extraFieldLength; + return MZ_OK; +} + +int ZipReader::DecompressStore(const uint8_t* input, uint64_t inputSize, + uint8_t* output, uint64_t outputSize) { + if (inputSize != outputSize) { + return MZ_DATA_ERROR; + } + memcpy(output, input, (size_t)inputSize); + return MZ_OK; +} + +int ZipReader::DecompressDeflate(const uint8_t* input, uint64_t inputSize, + uint8_t* output, uint64_t outputSize) { + size_t bytesWritten = 0; + int result = Inflate(input, (size_t)inputSize, output, (size_t)outputSize, &bytesWritten); + if (result != MZ_OK) { + return result; + } + if (bytesWritten != outputSize) { + return MZ_DATA_ERROR; + } + return MZ_OK; +} + +int ZipReader::ExtractAll(const std::string& destPath, ExtractProgressCallback progressCallback) { + if (!m_isOpen) { + return MZ_OPEN_ERROR; + } + + /* Create destination directory */ + std::error_code ec; + fs::create_directories(destPath, ec); + if (ec) { + return MZ_OPEN_ERROR; + } + + int totalFiles = (int)m_entries.size(); + int filesExtracted = 0; + + for (const auto& entry : m_entries) { + if (progressCallback) { + progressCallback(entry.filename, filesExtracted, totalFiles); + } + + if (entry.isDirectory) { + /* Create directory */ + fs::path dirPath = fs::path(destPath) / entry.filename; + fs::create_directories(dirPath, ec); + } else { + /* Extract file */ + fs::path filePath = fs::path(destPath) / entry.filename; + + /* Create parent directories */ + fs::create_directories(filePath.parent_path(), ec); + + int result = ExtractFileTo(entry.filename, filePath.string()); + if (result != MZ_OK) { + return result; + } + } + + filesExtracted++; + } + + if (progressCallback) { + progressCallback("", filesExtracted, totalFiles); + } + + return MZ_OK; +} + +int ZipReader::ExtractFile(const std::string& filename, const std::string& destPath) { + const ZipEntry* entry = FindEntry(filename); + if (!entry) { + return MZ_EXIST_ERROR; + } + + fs::path outputPath = fs::path(destPath) / entry->filename; + + /* Create parent directories */ + std::error_code ec; + fs::create_directories(outputPath.parent_path(), ec); + + return ExtractFileTo(filename, outputPath.string()); +} + +int ZipReader::ExtractFileTo(const std::string& filename, const std::string& outputPath) { + if (!m_isOpen) { + return MZ_OPEN_ERROR; + } + + const ZipEntry* entry = FindEntry(filename); + if (!entry) { + return MZ_EXIST_ERROR; + } + + if (entry->isDirectory) { + /* Just create the directory */ + std::error_code ec; + fs::create_directories(outputPath, ec); + return MZ_OK; + } + + FILE* fp = (FILE*)m_file; + + /* Read local file header to get actual data offset */ + uint16_t headerSize = 0; + int result = ReadLocalFileHeader(entry->localHeaderOffset, headerSize); + if (result != MZ_OK) { + return result; + } + + /* Seek to compressed data */ + uint64_t dataOffset = entry->localHeaderOffset + headerSize; + fseek(fp, (long)dataOffset, SEEK_SET); + + /* Read compressed data */ + std::vector compressedData((size_t)entry->compressedSize); + if (entry->compressedSize > 0) { + if (fread(compressedData.data(), 1, (size_t)entry->compressedSize, fp) != (size_t)entry->compressedSize) { + return MZ_DATA_ERROR; + } + } + + /* Decompress */ + std::vector uncompressedData((size_t)entry->uncompressedSize); + if (entry->uncompressedSize > 0) { + if (entry->compressionMethod == MZ_COMPRESS_METHOD_STORE) { + result = DecompressStore(compressedData.data(), entry->compressedSize, + uncompressedData.data(), entry->uncompressedSize); + } else if (entry->compressionMethod == MZ_COMPRESS_METHOD_DEFLATE) { + result = DecompressDeflate(compressedData.data(), entry->compressedSize, + uncompressedData.data(), entry->uncompressedSize); + } else { + return MZ_DATA_ERROR; /* Unsupported compression method */ + } + + if (result != MZ_OK) { + return result; + } + } + + /* Write to output file */ + FILE* outFp = nullptr; +#ifdef _WIN32 + fopen_s(&outFp, outputPath.c_str(), "wb"); +#else + outFp = fopen(outputPath.c_str(), "wb"); +#endif + + if (!outFp) { + return MZ_OPEN_ERROR; + } + + if (entry->uncompressedSize > 0) { + if (fwrite(uncompressedData.data(), 1, (size_t)entry->uncompressedSize, outFp) != (size_t)entry->uncompressedSize) { + fclose(outFp); + return MZ_DATA_ERROR; + } + } + + fclose(outFp); + return MZ_OK; +} + +} // namespace mz diff --git a/vendor/minizip-ng/mz_zip.h b/vendor/minizip-ng/mz_zip.h new file mode 100644 index 00000000..642f087e --- /dev/null +++ b/vendor/minizip-ng/mz_zip.h @@ -0,0 +1,77 @@ +/* + * Minimal ZIP library - ZIP file handling + * + * Copyright 2020-2026 Vector 35 Inc. + * Licensed under the Apache License, Version 2.0 + */ + +#ifndef MZ_ZIP_H +#define MZ_ZIP_H + +#include "mz.h" +#include +#include +#include +#include + +namespace mz { + +/* ZIP entry information */ +struct ZipEntry { + std::string filename; + uint64_t compressedSize; + uint64_t uncompressedSize; + uint32_t crc32; + uint16_t compressionMethod; + uint64_t localHeaderOffset; + bool isDirectory; +}; + +/* Progress callback for extraction */ +using ExtractProgressCallback = std::function; + +/* ZIP reader class */ +class ZipReader { +public: + ZipReader(); + ~ZipReader(); + + /* Open a ZIP file for reading */ + int Open(const std::string& path); + + /* Close the ZIP file */ + int Close(); + + /* Get list of entries in the ZIP */ + const std::vector& GetEntries() const; + + /* Get number of entries */ + size_t GetEntryCount() const; + + /* Find entry by filename (case-insensitive) */ + const ZipEntry* FindEntry(const std::string& filename) const; + + /* Extract all files to a directory */ + int ExtractAll(const std::string& destPath, ExtractProgressCallback progressCallback = nullptr); + + /* Extract a single file to a directory */ + int ExtractFile(const std::string& filename, const std::string& destPath); + + /* Extract a single file to a specific output path */ + int ExtractFileTo(const std::string& filename, const std::string& outputPath); + +private: + int ReadCentralDirectory(); + int ReadLocalFileHeader(uint64_t offset, uint16_t& headerSize); + int DecompressStore(const uint8_t* input, uint64_t inputSize, uint8_t* output, uint64_t outputSize); + int DecompressDeflate(const uint8_t* input, uint64_t inputSize, uint8_t* output, uint64_t outputSize); + + void* m_file; + std::string m_path; + std::vector m_entries; + bool m_isOpen; +}; + +} // namespace mz + +#endif /* MZ_ZIP_H */