From fcfeef64f99b93213e045ab44a6276932ee7e73a Mon Sep 17 00:00:00 2001 From: Andrey Kryuchenko Date: Sat, 18 Apr 2026 20:24:08 +0200 Subject: [PATCH 1/3] refactor: extract CalculatePathInfo into path_info module Moves the path/env collection helpers into path_info.{hpp,cpp} under a namespace (keeps Windows.h macros like GetWindowsDirectory from rewriting the member function names). While moving, fix the follow-on issues the static analysis flagged: - GetModuleFileNameW / GetCurrentDirectoryW / GetTempPathW no longer truncate silently at MAX_PATH; buffers are sized from each API's own return value (growing retry for GetModuleFileNameW). - C-style casts on GetProcAddress results replaced with reinterpret_cast. - exeDir = exeDir.substr(0, lastSlash) replaced with in-place erase. cli_args_debugger.cpp: 1964 -> 1846 lines (-118). CalculatePathInfo drops from CCN 14 / 99 NLOC to a 3-line delegate. --- .github/workflows/build.yml | 2 +- CMakeLists.txt | 1 + README.md | 2 +- cli_args_debugger.cpp | 126 +----------------------- path_info.cpp | 185 ++++++++++++++++++++++++++++++++++++ path_info.hpp | 32 +++++++ tests/CMakeLists.txt | 1 + validate.sh | 2 +- 8 files changed, 226 insertions(+), 125 deletions(-) create mode 100644 path_info.cpp create mode 100644 path_info.hpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 102d367..92e6d23 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: shell: cmd run: | cl /EHsc /std:c++20 /permissive- /I. /DUNICODE /D_UNICODE /GS /sdl ^ - cli_args_debugger.cpp log_manager.cpp seh_wrapper.cpp qrcodegen.cpp ^ + cli_args_debugger.cpp log_manager.cpp path_info.cpp seh_wrapper.cpp qrcodegen.cpp ^ /Fe:build\cloud-streaming-args-debugger.exe ^ /Fo:obj\ ^ /link d3d11.lib d3dcompiler.lib d2d1.lib dwrite.lib ole32.lib avrt.lib user32.lib shell32.lib gdi32.lib propsys.lib winmm.lib psapi.lib diff --git a/CMakeLists.txt b/CMakeLists.txt index c2f6b4e..efcfb3a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,7 @@ add_executable(cloud-streaming-args-debugger WIN32 # Specify that the application uses WinMain instead of main cli_args_debugger.cpp log_manager.cpp + path_info.cpp seh_wrapper.cpp qrcodegen.cpp # Include QR code generator ) diff --git a/README.md b/README.md index 203aea5..d6efcce 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Check out a preview of the application: # Compile with MSVC cl /EHsc /std:c++20 /permissive- /I. /DUNICODE /D_UNICODE ^ - cli_args_debugger.cpp log_manager.cpp seh_wrapper.cpp qrcodegen.cpp ^ + cli_args_debugger.cpp log_manager.cpp path_info.cpp seh_wrapper.cpp qrcodegen.cpp ^ /Fe:build/ArgumentDebugger.exe ^ /Fo:build/ ^ /link d3d11.lib d3dcompiler.lib d2d1.lib dwrite.lib ole32.lib avrt.lib user32.lib shell32.lib gdi32.lib propsys.lib diff --git a/cli_args_debugger.cpp b/cli_args_debugger.cpp index f4a15a5..44ebd9f 100644 --- a/cli_args_debugger.cpp +++ b/cli_args_debugger.cpp @@ -93,6 +93,9 @@ using qrcodegen::QrCode; // seh_wrapper) can link against the same instance. #include "log_manager.hpp" +// Path/env inspection (executable path, OS version, Wine/Proton, etc.) +#include "path_info.hpp" + // Use Microsoft::WRL::ComPtr for COM object management using Microsoft::WRL::ComPtr; @@ -1738,128 +1741,7 @@ void ArgumentDebuggerWindow::ReadData() // Helper method to calculate and cache path information once void ArgumentDebuggerWindow::CalculatePathInfo() { - cached_path_items_.clear(); - - // Display full and relative path where the application is running - wchar_t exePath[MAX_PATH] = L"\0"; - GetModuleFileNameW(nullptr, exePath, MAX_PATH); - std::wstring fullPath = exePath; - - // Get current working directory for relative path - wchar_t currentDir[MAX_PATH] = L"\0"; - GetCurrentDirectoryW(MAX_PATH, currentDir); - std::wstring currentDirStr = currentDir; - - // Extract just the executable name - std::wstring exeName = fullPath; - size_t lastSlash = exeName.find_last_of(L"\\"); - if (lastSlash != std::wstring::npos) - { - exeName = exeName.substr(lastSlash + 1); - } - - // Extract directory from full path - std::wstring exeDir = fullPath; - if (lastSlash != std::wstring::npos) - { - exeDir = exeDir.substr(0, lastSlash); - } - - // Get TEMP directory - wchar_t tempPath[MAX_PATH] = L"\0"; - GetTempPathW(MAX_PATH, tempPath); - std::wstring tempDir = tempPath; - if (!tempDir.empty() && tempDir.back() == L'\\') - { - tempDir.pop_back(); // Remove trailing slash - } - - // Get Windows directory - wchar_t winPath[MAX_PATH] = L"\0"; - GetWindowsDirectoryW(winPath, MAX_PATH); - std::wstring winDir = winPath; - - // Get System directory - wchar_t sysPath[MAX_PATH] = L"\0"; - GetSystemDirectoryW(sysPath, MAX_PATH); - std::wstring sysDir = sysPath; - - // Get command line - std::wstring cmdLine = GetCommandLineW(); - - // Get OS version - std::wstring osVersion = L"Unknown"; - typedef NTSTATUS(WINAPI * RtlGetVersionPtr)(PRTL_OSVERSIONINFOW); - HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll"); - if (hNtdll) - { - RtlGetVersionPtr RtlGetVersion = (RtlGetVersionPtr)GetProcAddress(hNtdll, "RtlGetVersion"); - if (RtlGetVersion) - { - RTL_OSVERSIONINFOW osvi = {0}; - osvi.dwOSVersionInfoSize = sizeof(osvi); - if (RtlGetVersion(&osvi) == 0) - { - osVersion = L"Windows " + std::to_wstring(osvi.dwMajorVersion) + L"." + - std::to_wstring(osvi.dwMinorVersion) + L" (Build " + std::to_wstring(osvi.dwBuildNumber) + - L")"; - } - } - } - - // Check for Wine/Proton (simplified version) - std::wstring wineVersion = L"Not detected"; - HMODULE hNtdllCheck = GetModuleHandleW(L"ntdll.dll"); - if (hNtdllCheck && GetProcAddress(hNtdllCheck, "wine_get_version")) - { - typedef const char* (*wine_get_version_func)(void); - wine_get_version_func wine_get_version = - (wine_get_version_func)GetProcAddress(hNtdllCheck, "wine_get_version"); - if (wine_get_version) - { - const char* version = wine_get_version(); - if (version) - { - int size_needed = MultiByteToWideChar(CP_UTF8, 0, version, -1, NULL, 0); - std::wstring wversion(size_needed - 1, 0); - MultiByteToWideChar(CP_UTF8, 0, version, -1, &wversion[0], size_needed); - wineVersion = L"Wine " + wversion; - - // Quick Proton check - wchar_t envBuf[1024] = {0}; - if (GetEnvironmentVariableW(L"PROTON_VERSION", envBuf, 1024) > 0) - { - wineVersion = L"Proton " + std::wstring(envBuf) + L" (Wine " + wversion + L")"; - } - } - } - } - - // Get save path - std::wstring savePath = L"Not available"; - PWSTR appdata_path = nullptr; - HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &appdata_path); - if (SUCCEEDED(hr)) - { - savePath = appdata_path; - CoTaskMemFree(appdata_path); - savePath += L"\\CloudStreamingArgsDebugger\\saved_data.txt"; - } - - // Store all path information - cached_path_items_ = { - {L"OS Version: ", osVersion}, - {L"Wine/Proton: ", wineVersion}, - {L"Executable name: ", exeName}, - {L"Full path: ", fullPath}, - {L"Executable directory: ", exeDir}, - {L"Current directory: ", currentDirStr}, - {L"Command line: ", cmdLine}, - {L"Save file path: ", savePath}, - {L"TEMP directory: ", tempDir}, - {L"Windows directory: ", winDir}, - {L"System directory: ", sysDir} - }; + cached_path_items_ = path_info::Collect(); } void ArgumentDebuggerWindow::PlayTelephoneBeeps() // Name kept for compatibility diff --git a/path_info.cpp b/path_info.cpp new file mode 100644 index 0000000..d0c1802 --- /dev/null +++ b/path_info.cpp @@ -0,0 +1,185 @@ +#ifndef UNICODE +#define UNICODE +#define _UNICODE +#endif + +#include +#include +#include +#include + +#include "path_info.hpp" + +#pragma comment(lib, "shell32") +#pragma comment(lib, "ole32") + +namespace +{ + +// Generic retry pattern for Win32 "fill-a-buffer" APIs that either write +// `len < size` on success or report `ERROR_INSUFFICIENT_BUFFER` when the +// buffer is too small. Avoids MAX_PATH truncation silently. +template std::wstring FillWithGrowingBuffer(Fill fill) +{ + std::wstring buf; + buf.resize(MAX_PATH); + for (int attempt = 0; attempt < 6; ++attempt) + { + SetLastError(ERROR_SUCCESS); + DWORD len = fill(buf.data(), static_cast(buf.size())); + if (len == 0) + return {}; + if (len < buf.size()) + { + buf.resize(len); + return buf; + } + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) + { + buf.resize(len); + return buf; + } + buf.resize(buf.size() * 2); + } + return {}; +} + +// For APIs whose first form takes (size=0, buf=nullptr) and returns the +// required character count including the terminator. +template std::wstring FillWithQueriedSize(Query query) +{ + DWORD needed = query(0, nullptr); + if (!needed) + return {}; + std::wstring buf(needed, L'\0'); + DWORD got = query(needed, buf.data()); + if (got == 0 || got > needed) + return {}; + buf.resize(got); + return buf; +} + +} // namespace + +namespace path_info +{ + +std::wstring ExecutablePath() +{ + return FillWithGrowingBuffer([](wchar_t* p, DWORD n) { return GetModuleFileNameW(nullptr, p, n); }); +} + +std::wstring CurrentWorkingDirectory() +{ + return FillWithQueriedSize([](DWORD n, wchar_t* p) { return GetCurrentDirectoryW(n, p); }); +} + +std::wstring TempDirectory() +{ + std::wstring path = FillWithQueriedSize([](DWORD n, wchar_t* p) { return GetTempPathW(n, p); }); + if (!path.empty() && path.back() == L'\\') + path.pop_back(); + return path; +} + +std::wstring WindowsDirectory() +{ + return FillWithQueriedSize([](DWORD n, wchar_t* p) { return ::GetWindowsDirectoryW(p, n); }); +} + +std::wstring SystemDirectory() +{ + return FillWithQueriedSize([](DWORD n, wchar_t* p) { return ::GetSystemDirectoryW(p, n); }); +} + +std::wstring OsVersionString() +{ + using RtlGetVersionPtr = NTSTATUS(WINAPI*)(PRTL_OSVERSIONINFOW); + // GetModuleHandleW returns a cached handle — do not call FreeLibrary on it. + HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll"); + if (!hNtdll) + return L"Unknown"; + + auto rtlGetVersion = reinterpret_cast(GetProcAddress(hNtdll, "RtlGetVersion")); + if (!rtlGetVersion) + return L"Unknown"; + + RTL_OSVERSIONINFOW osvi{}; + osvi.dwOSVersionInfoSize = sizeof(osvi); + if (rtlGetVersion(&osvi) != 0) + return L"Unknown"; + + return L"Windows " + std::to_wstring(osvi.dwMajorVersion) + L"." + std::to_wstring(osvi.dwMinorVersion) + + L" (Build " + std::to_wstring(osvi.dwBuildNumber) + L")"; +} + +std::wstring WineOrProtonVersion() +{ + using wine_get_version_func = const char* (*)(void); + HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll"); + if (!hNtdll) + return L"Not detected"; + + auto wineGetVersion = reinterpret_cast(GetProcAddress(hNtdll, "wine_get_version")); + if (!wineGetVersion) + return L"Not detected"; + + const char* version = wineGetVersion(); + if (!version) + return L"Not detected"; + + int size_needed = MultiByteToWideChar(CP_UTF8, 0, version, -1, nullptr, 0); + if (size_needed <= 1) + return L"Not detected"; + + std::wstring wversion(size_needed - 1, L'\0'); + MultiByteToWideChar(CP_UTF8, 0, version, -1, wversion.data(), size_needed); + + wchar_t protonBuf[1024]{}; + if (GetEnvironmentVariableW(L"PROTON_VERSION", protonBuf, 1024) > 0) + return L"Proton " + std::wstring(protonBuf) + L" (Wine " + wversion + L")"; + return L"Wine " + wversion; +} + +std::wstring SaveFilePath() +{ + PWSTR appdata_path = nullptr; + HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &appdata_path); + if (FAILED(hr)) + return L"Not available"; + + std::wstring path = appdata_path; + CoTaskMemFree(appdata_path); + path += L"\\CloudStreamingArgsDebugger\\saved_data.txt"; + return path; +} + +std::vector Collect() +{ + const std::wstring fullPath = ExecutablePath(); + + std::wstring exeName = fullPath; + std::wstring exeDir = fullPath; + const size_t lastSlash = fullPath.find_last_of(L'\\'); + if (lastSlash != std::wstring::npos) + { + exeName.erase(0, lastSlash + 1); + exeDir.erase(lastSlash); + } + + return { + {L"OS Version: ", OsVersionString()}, + {L"Wine/Proton: ", WineOrProtonVersion()}, + {L"Executable name: ", exeName}, + {L"Full path: ", fullPath}, + {L"Executable directory: ", exeDir}, + {L"Current directory: ", CurrentWorkingDirectory()}, + {L"Command line: ", GetCommandLineW() ? std::wstring(GetCommandLineW()) : std::wstring()}, + {L"Save file path: ", SaveFilePath()}, + {L"TEMP directory: ", TempDirectory()}, + {L"Windows directory: ", WindowsDirectory()}, + {L"System directory: ", SystemDirectory()}, + }; +} + +} // namespace path_info diff --git a/path_info.hpp b/path_info.hpp new file mode 100644 index 0000000..235185b --- /dev/null +++ b/path_info.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +// `label` keeps a trailing space so the UI can draw the value immediately +// after it. Using a plain pair keeps this module drop-in compatible with the +// previous `cached_path_items_` field, which already had this type. +using PathItem = std::pair; + +// The namespace guards against Windows.h macros such as `GetWindowsDirectory` +// and `GetSystemDirectory` rewriting these names into their W/A variants. +namespace path_info +{ + +// Collect executable path, working dir, OS version, Wine/Proton marker, TEMP, +// Windows, System and save-file paths. All Win32 calls are sized from their +// own return values, so paths longer than MAX_PATH are not silently truncated. +std::vector Collect(); + +// Helpers exposed for unit tests and for callers that need only one value. +std::wstring ExecutablePath(); +std::wstring CurrentWorkingDirectory(); +std::wstring TempDirectory(); +std::wstring WindowsDirectory(); +std::wstring SystemDirectory(); +std::wstring OsVersionString(); +std::wstring WineOrProtonVersion(); +std::wstring SaveFilePath(); + +} // namespace path_info diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 72c8de9..127332b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -36,6 +36,7 @@ set(PARENT_SOURCES ../qrcodegen.cpp ../cli_args_debugger.cpp ../log_manager.cpp + ../path_info.cpp ../seh_wrapper.cpp ) diff --git a/validate.sh b/validate.sh index b6dbf3e..10b9bdd 100755 --- a/validate.sh +++ b/validate.sh @@ -19,7 +19,7 @@ echo "🔍 Checking for common C++ syntax issues..." # Check for missing semicolons, unmatched braces, etc. syntax_errors=0 -for file in cli_args_debugger.cpp seh_wrapper.cpp log_manager.cpp; do +for file in cli_args_debugger.cpp seh_wrapper.cpp log_manager.cpp path_info.cpp; do if [ -f "$file" ]; then echo "Checking $file..." From b59f4aed11ff4a4325c80d1bc30748e371d7094e Mon Sep 17 00:00:00 2001 From: Andrey Kryuchenko Date: Sat, 18 Apr 2026 20:26:46 +0200 Subject: [PATCH 2/3] fix(path_info): include before uses EXTERN_C and GUID, both of which come from . cli_args_debugger.cpp happened to pull in transitively via , so the old sort order worked there; path_info.cpp has no such trigger and MSVC bailed with C2146 / C4430 on every FOLDERID_* definition. Wrap the Windows header block in clang-format off/on so the required order survives future auto-sorts. --- path_info.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/path_info.cpp b/path_info.cpp index d0c1802..27db226 100644 --- a/path_info.cpp +++ b/path_info.cpp @@ -3,10 +3,14 @@ #define _UNICODE #endif +// clang-format off +// must come first; and rely on +// EXTERN_C / GUID being already defined. +#include #include #include -#include #include +// clang-format on #include "path_info.hpp" From 14b0c4ad373c9dbbd4eddcb05a45f6e6bd38b6c8 Mon Sep 17 00:00:00 2001 From: Andrey Kryuchenko Date: Sat, 18 Apr 2026 20:37:54 +0200 Subject: [PATCH 3/3] fix(path_info): use &buf[0] instead of wstring::data() for C++17 tests tests/CMakeLists.txt compiles the test binary with CMAKE_CXX_STANDARD 17, whereas the main executable is built with /std:c++20. std::wstring::data() is const-qualified in C++17 and only returns a writable wchar_t* starting in C++20, so passing it to Win32 APIs that take LPWSTR breaks the test build with C2664 while the main build stays happy. Switch the three call sites in path_info.cpp to &buf[0], which yields a non-const wchar_t* in both standards. Buffers are always pre-sized with resize()/the sized constructor before the call, so &buf[0] is valid. --- path_info.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/path_info.cpp b/path_info.cpp index 27db226..037f6af 100644 --- a/path_info.cpp +++ b/path_info.cpp @@ -30,7 +30,9 @@ template std::wstring FillWithGrowingBuffer(Fill fill) for (int attempt = 0; attempt < 6; ++attempt) { SetLastError(ERROR_SUCCESS); - DWORD len = fill(buf.data(), static_cast(buf.size())); + // `&buf[0]` yields `wchar_t*` in both C++17 and C++20; `buf.data()` + // would only do so in C++20. Tests compile with C++17. + DWORD len = fill(&buf[0], static_cast(buf.size())); if (len == 0) return {}; if (len < buf.size()) @@ -56,7 +58,7 @@ template std::wstring FillWithQueriedSize(Query query) if (!needed) return {}; std::wstring buf(needed, L'\0'); - DWORD got = query(needed, buf.data()); + DWORD got = query(needed, &buf[0]); if (got == 0 || got > needed) return {}; buf.resize(got); @@ -137,7 +139,7 @@ std::wstring WineOrProtonVersion() return L"Not detected"; std::wstring wversion(size_needed - 1, L'\0'); - MultiByteToWideChar(CP_UTF8, 0, version, -1, wversion.data(), size_needed); + MultiByteToWideChar(CP_UTF8, 0, version, -1, &wversion[0], size_needed); wchar_t protonBuf[1024]{}; if (GetEnvironmentVariableW(L"PROTON_VERSION", protonBuf, 1024) > 0)