From a0edeece48e242d89dfa66d1d965946bf89b535d Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 17 Jul 2025 20:08:05 -0400 Subject: [PATCH 1/5] fix(audio-info): crash when device name contains special characters --- cmake/compile_definitions/windows.cmake | 1 + src/platform/windows/tools/helper.h | 127 ++++++ .../platform/windows/tools/test_helper.cpp | 424 ++++++++++++++++++ tools/CMakeLists.txt | 1 + tools/audio.cpp | 55 ++- tools/dxgi.cpp | 44 +- 6 files changed, 600 insertions(+), 52 deletions(-) create mode 100644 src/platform/windows/tools/helper.h create mode 100644 tests/unit/platform/windows/tools/test_helper.cpp diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake index 60ee905b773..07d647f3536 100644 --- a/cmake/compile_definitions/windows.cmake +++ b/cmake/compile_definitions/windows.cmake @@ -59,6 +59,7 @@ set(PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/windows/display_ram.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/display_wgc.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/audio.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/tools/helper.h" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/src/ViGEmClient.cpp" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Client.h" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Common.h" diff --git a/src/platform/windows/tools/helper.h b/src/platform/windows/tools/helper.h new file mode 100644 index 00000000000..def2a57857e --- /dev/null +++ b/src/platform/windows/tools/helper.h @@ -0,0 +1,127 @@ +/** + * @file src/platform/windows/tools/helper.h + * @brief Helpers for tools. + */ +#pragma once + +// standard includes +#include +#include + +// lib includes +#include + +// platform includes +#include + +/** + * @brief Safe console output utilities for Windows + * These functions prevent crashes when outputting strings with special characters. + * This is only used in tools/audio-info and tools/dxgi-info. + */ +namespace output { + // ASCII character range constants for safe output, https://www.ascii-code.com/ + static constexpr int ASCII_PRINTABLE_START = 32; + static constexpr int ASCII_PRINTABLE_END = 127; + + /** + * @brief Return a non-null wide string, defaulting to "Unknown" if null + * @param str The wide string to check + * @return A non-null wide string + */ + inline const wchar_t *no_null(const wchar_t *str) { + return str ? str : L"Unknown"; + } + + /** + * @brief Safely convert a wide string to console output using Windows API + * @param wstr The wide string to output + */ + inline void safe_wcout(const std::wstring &wstr) { + if (wstr.empty()) { + return; + } + + // Try to use the Windows console API for proper Unicode output + if (const HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); hConsole != INVALID_HANDLE_VALUE) { + DWORD written; + if (WriteConsoleW(hConsole, wstr.c_str(), wstr.length(), &written, nullptr)) { + return; // Success with WriteConsoleW + } + } + + // Fallback: convert to narrow string and output to std::cout + try { + const std::string narrow_str = boost::locale::conv::utf_to_utf(wstr); + std::cout << narrow_str; + } catch (const boost::locale::conv::conversion_error &) { + // Final fallback: output character by character, replacing non-ASCII + for (const wchar_t wc : wstr) { + if (wc >= ASCII_PRINTABLE_START && wc < ASCII_PRINTABLE_END) { // Printable ASCII + std::cout << static_cast(wc); + } else { + std::cout << '?'; + } + } + } + } + + /** + * @brief Safely convert a wide string literal to console encoding and output it + * @param wstr The wide string literal to output + */ + inline void safe_wcout(const wchar_t *wstr) { + if (wstr) { + safe_wcout(std::wstring(wstr)); + } else { + std::cout << "Unknown"; + } + } + + /** + * @brief Safely convert a string to wide string and then to console output + * @param str The string to output + */ + inline void safe_cout(const std::string &str) { + if (str.empty()) { + return; + } + + try { + // Convert string to wide string first, then to console output + const std::wstring wstr = boost::locale::conv::utf_to_utf(str); + safe_wcout(wstr); + } catch (const boost::locale::conv::conversion_error &) { + // Fallback: output string directly, replacing problematic characters + for (const char c : str) { + if (c >= ASCII_PRINTABLE_START && c < ASCII_PRINTABLE_END) { // Printable ASCII + std::cout << c; + } else { + std::cout << '?'; + } + } + } + } + + /** + * @brief Output a label and value pair safely + * @param label The label to output + * @param value The wide string value to output + */ + inline void output_field(const std::string &label, const wchar_t *value) { + std::cout << label << " : "; + safe_wcout(value ? value : L"Unknown"); + std::cout << std::endl; + } + + /** + * @brief Output a label and string value pair + * @param label The label to output + * @param value The string value to output + */ + inline void output_field(const std::string &label, const std::string &value) { + std::cout << label << " : "; + safe_cout(value); + std::cout << std::endl; + } +} // namespace output diff --git a/tests/unit/platform/windows/tools/test_helper.cpp b/tests/unit/platform/windows/tools/test_helper.cpp new file mode 100644 index 00000000000..5401a96f409 --- /dev/null +++ b/tests/unit/platform/windows/tools/test_helper.cpp @@ -0,0 +1,424 @@ +/** + * @file tests/unit/platform/windows/tools/test_helper.cpp + * @brief Test src/platform/windows/tools/helper.cpp output functions. + */ +#include "../../../../tests_common.h" + +#include +#include +#include + +#ifdef _WIN32 + #include + #include + #include + #include +#endif + +namespace { + /** + * @brief Helper class to capture console output for testing + */ + class ConsoleCapture { + public: + ConsoleCapture() { + // Save original cout buffer + original_cout_buffer = std::cout.rdbuf(); + // Redirect cout to our stringstream + std::cout.rdbuf(captured_output.rdbuf()); + } + + ~ConsoleCapture() { + try { + // Restore original cout buffer + std::cout.rdbuf(original_cout_buffer); + } catch (std::exception &e) { + std::cerr << "Error restoring cout buffer: " << e.what() << std::endl; + } + } + + std::string get_output() const { + return captured_output.str(); + } + + void clear() { + captured_output.str(""); + captured_output.clear(); + } + + private: + std::streambuf *original_cout_buffer; + std::stringstream captured_output; + }; +} // namespace + +#ifdef _WIN32 +/** + * @brief Test fixture for output namespace functions + */ +class UtilityOutputTest: public testing::Test { // NOSONAR +protected: + void SetUp() override { + capture = std::make_unique(); + } + + void TearDown() override { + capture.reset(); + } + + std::unique_ptr capture; +}; + +TEST_F(UtilityOutputTest, NoNullWithValidString) { + const wchar_t *test_string = L"Valid String"; + const wchar_t *result = output::no_null(test_string); + + EXPECT_EQ(result, test_string) << "Expected no change for valid string"; + EXPECT_STREQ(result, L"Valid String") << "Expected exact match for valid string"; +} + +TEST_F(UtilityOutputTest, NoNullWithNullString) { + const wchar_t *null_string = nullptr; + const wchar_t *result = output::no_null(null_string); + + EXPECT_NE(result, nullptr) << "Expected non-null result for null input"; + EXPECT_STREQ(result, L"Unknown") << "Expected 'Unknown' for null input"; +} + +TEST_F(UtilityOutputTest, SafeWcoutWithValidWideString) { + std::wstring test_string = L"Hello World"; + + capture->clear(); + output::safe_wcout(test_string); + const std::string output = capture->get_output(); + + // In test environment, WriteConsoleW will likely fail, so it should fall back to boost::locale conversion + EXPECT_EQ(output, "Hello World") << "Expected exact string output from safe_wcout"; +} + +TEST_F(UtilityOutputTest, SafeWcoutWithEmptyWideString) { + const std::wstring empty_string = L""; + + capture->clear(); + output::safe_wcout(empty_string); + const std::string output = capture->get_output(); + + // Empty string should return early without output + EXPECT_TRUE(output.empty()) << "Empty wide string should produce no output"; +} + +TEST_F(UtilityOutputTest, SafeWcoutWithValidWideStringPointer) { + const wchar_t *test_string = L"Test String"; + + capture->clear(); + output::safe_wcout(test_string); + const std::string output = capture->get_output(); + + EXPECT_EQ(output, "Test String") << "Expected exact string output from safe_wcout with pointer"; +} + +TEST_F(UtilityOutputTest, SafeWcoutWithNullWideStringPointer) { + const wchar_t *null_string = nullptr; + + capture->clear(); + output::safe_wcout(null_string); + const std::string output = capture->get_output(); + + EXPECT_EQ(output, "Unknown") << "Expected 'Unknown' output from safe_wcout with null pointer"; +} + +TEST_F(UtilityOutputTest, SafeCoutWithValidString) { + const std::string test_string = "Hello World"; + + capture->clear(); + output::safe_cout(test_string); + const std::string output = capture->get_output(); + + EXPECT_EQ(output, "Hello World") << "Expected exact string output from safe_cout"; +} + +TEST_F(UtilityOutputTest, SafeCoutWithEmptyString) { + std::string empty_string = ""; + + capture->clear(); + output::safe_cout(empty_string); + const std::string output = capture->get_output(); + + // Empty string should return early + EXPECT_TRUE(output.empty()) << "Empty string should produce no output"; +} + +TEST_F(UtilityOutputTest, SafeCoutWithSpecialCharacters) { + const std::string special_string = "Test\x{01}\x{02}\x{03}String"; + + capture->clear(); + output::safe_cout(special_string); + const std::string output = capture->get_output(); + + // Should handle special characters without crashing + EXPECT_FALSE(output.empty()) << "Expected some output from safe_cout with special chars"; + + // The function should either succeed with boost::locale conversion or fall back to character replacement + // In the fallback case, non-printable characters (\x{01}, \x{02}, \x{03}) should be replaced with '?' + // So we expect either the original string or "Test???String" + EXPECT_TRUE(output == "Test\x{01}\x{02}\x{03}String" || output == "Test???String") + << "Expected either original string or fallback with '?' replacements, got: '" << output << "'"; +} + +TEST_F(UtilityOutputTest, OutputFieldWithWideStringPointer) { + const wchar_t *test_value = L"Test Value"; + const std::string label = "Test Label"; + + capture->clear(); + output::output_field(label, test_value); + const std::string output = capture->get_output(); + + EXPECT_TRUE(output.find("Test Label : ") != std::string::npos) << "Expected label in output"; + EXPECT_TRUE(output.find("\n") != std::string::npos) << "Expected newline at the end of output"; +} + +TEST_F(UtilityOutputTest, OutputFieldWithNullWideStringPointer) { + const wchar_t *null_value = nullptr; + const std::string label = "Test Label"; + + capture->clear(); + output::output_field(label, null_value); + const std::string output = capture->get_output(); + + EXPECT_TRUE(output.find("Test Label : ") != std::string::npos) << "Expected label in output"; + EXPECT_TRUE(output.find("Unknown") != std::string::npos) << "Expected 'Unknown' for null value"; + EXPECT_TRUE(output.find("\n") != std::string::npos) << "Expected newline at the end of output"; +} + +TEST_F(UtilityOutputTest, OutputFieldWithRegularString) { + const std::string test_value = "Test Value"; + const std::string label = "Test Label"; + + capture->clear(); + output::output_field(label, test_value); + const std::string output = capture->get_output(); + + EXPECT_TRUE(output.find("Test Label : ") != std::string::npos) << "Expected label in output"; + EXPECT_TRUE(output.find("\n") != std::string::npos) << "Expected newline at the end of output"; +} + +TEST_F(UtilityOutputTest, OutputFieldWithEmptyString) { + const std::string empty_value = ""; + const std::string label = "Empty Label"; + + capture->clear(); + output::output_field(label, empty_value); + const std::string output = capture->get_output(); + + EXPECT_TRUE(output.find("Empty Label : ") != std::string::npos) << "Expected label in output"; + EXPECT_TRUE(output.find("\n") != std::string::npos) << "Expected newline at the end of output"; +} + +TEST_F(UtilityOutputTest, OutputFieldWithSpecialCharactersInString) { + const std::string special_value = "Value\x{01}\x{02}\x{03}With\x{7F}Special"; + const std::string label = "Special Label"; + + capture->clear(); + output::output_field(label, special_value); + const std::string output = capture->get_output(); + + EXPECT_TRUE(output.find("Special Label : ") != std::string::npos) << "Expected label in output"; + EXPECT_TRUE(output.find("\n") != std::string::npos) << "Expected newline at the end of output"; +} + +TEST_F(UtilityOutputTest, OutputFieldLabelFormatting) { + const std::string test_value = "Value"; + const std::string label = "My Label"; + + capture->clear(); + output::output_field(label, test_value); + const std::string output = capture->get_output(); + + // Check that the format is "Label : Value\n" + EXPECT_TRUE(output.find("My Label : ") == 0) << "Expected output to start with 'My Label : '"; + EXPECT_TRUE(output.back() == '\n') << "Expected output to end with newline character"; +} + +// Test case for multiple consecutive calls +TEST_F(UtilityOutputTest, MultipleOutputFieldCalls) { + capture->clear(); + + output::output_field("Label1", "Value1"); + output::output_field("Label2", L"Value2"); + output::output_field("Label3", std::string("Value3")); + + const std::string output = capture->get_output(); + + EXPECT_TRUE(output.find("Label1 : ") != std::string::npos) << "Expected 'Label1' in output"; + EXPECT_TRUE(output.find("Label2 : ") != std::string::npos) << "Expected 'Label2' in output"; + EXPECT_TRUE(output.find("Label3 : ") != std::string::npos) << "Expected 'Label3' in output"; + + // Count newlines - should have 3 + const size_t newline_count = std::ranges::count(output, '\n'); + EXPECT_EQ(newline_count, 3); +} + +// Test cases for actual Unicode symbols and special characters +TEST_F(UtilityOutputTest, OutputFieldWithQuotationMarks) { + capture->clear(); + + // Test with various quotation marks + output::output_field("Single Quote", "Device 'Audio' Output"); + output::output_field("Double Quote", "Device \"Audio\" Output"); + output::output_field("Left Quote", "Device 'Audio' Output"); + output::output_field("Right Quote", "Device 'Audio' Output"); + output::output_field("Left Double Quote", "Device \u{201C}Audio\u{201D} Output"); + output::output_field("Right Double Quote", "Device \u{201C}Audio\u{201D} Output"); + + const std::string output = capture->get_output(); + + EXPECT_TRUE(output.find("Single Quote : ") != std::string::npos) << "Expected 'Single Quote' in output"; + EXPECT_TRUE(output.find("Double Quote : ") != std::string::npos) << "Expected 'Double Quote' in output"; + EXPECT_TRUE(output.find("Left Quote : ") != std::string::npos) << "Expected 'Left Quote' in output"; + EXPECT_TRUE(output.find("Right Quote : ") != std::string::npos) << "Expected 'Right Quote' in output"; + EXPECT_TRUE(output.find("Left Double Quote : ") != std::string::npos) << "Expected 'Left Double Quote' in output"; + EXPECT_TRUE(output.find("Right Double Quote : ") != std::string::npos) << "Expected 'Right Double Quote' in output"; +} + +TEST_F(UtilityOutputTest, OutputFieldWithTrademarkSymbols) { + capture->clear(); + + // Test with trademark and copyright symbols + output::output_field("Trademark", "Audio Device™"); + output::output_field("Registered", "Audio Device®"); + output::output_field("Copyright", "Audio Device©"); + output::output_field("Combined", "Realtek® Audio™"); + + const std::string output = capture->get_output(); + + EXPECT_TRUE(output.find("Trademark : ") != std::string::npos) << "Expected 'Trademark' in output"; + EXPECT_TRUE(output.find("Registered : ") != std::string::npos) << "Expected 'Registered' in output"; + EXPECT_TRUE(output.find("Copyright : ") != std::string::npos) << "Expected 'Copyright' in output"; + EXPECT_TRUE(output.find("Combined : ") != std::string::npos) << "Expected 'Combined' in output"; +} + +TEST_F(UtilityOutputTest, OutputFieldWithAccentedCharacters) { + capture->clear(); + + // Test with accented characters that might appear in device names + output::output_field("French Accents", "Haut-parleur à haute qualité"); + output::output_field("Spanish Accents", "Altavoz ñáéíóú"); + output::output_field("German Accents", "Lautsprecher äöü"); + output::output_field("Mixed Accents", "àáâãäåæçèéêë"); + + const std::string output = capture->get_output(); + + EXPECT_TRUE(output.find("French Accents : ") != std::string::npos) << "Expected 'French Accents' in output"; + EXPECT_TRUE(output.find("Spanish Accents : ") != std::string::npos) << "Expected 'Spanish Accents' in output"; + EXPECT_TRUE(output.find("German Accents : ") != std::string::npos) << "Expected 'German Accents' in output"; + EXPECT_TRUE(output.find("Mixed Accents : ") != std::string::npos) << "Expected 'Mixed Accents' in output"; +} + +TEST_F(UtilityOutputTest, OutputFieldWithSpecialSymbols) { + capture->clear(); + + // Test with various special symbols + output::output_field("Math Symbols", "Audio @ 44.1kHz ± 0.1%"); + output::output_field("Punctuation", "Audio Device #1 & #2"); + output::output_field("Programming", "Device $%^&*()"); + output::output_field("Mixed Symbols", "Audio™ @#$%^&*()"); + + const std::string output = capture->get_output(); + + EXPECT_TRUE(output.find("Math Symbols : ") != std::string::npos) << "Expected 'Math Symbols' in output"; + EXPECT_TRUE(output.find("Punctuation : ") != std::string::npos) << "Expected 'Punctuation' in output"; + EXPECT_TRUE(output.find("Programming : ") != std::string::npos) << "Expected 'Programming' in output"; + EXPECT_TRUE(output.find("Mixed Symbols : ") != std::string::npos) << "Expected 'Mixed Symbols' in output"; +} + +TEST_F(UtilityOutputTest, OutputFieldWithWideCharacterSymbols) { + capture->clear(); + + // Test with wide character symbols + const wchar_t *device_with_quotes = L"Device 'Audio' Output"; + const wchar_t *device_with_trademark = L"Realtek® Audio™"; + const wchar_t *device_with_accents = L"Haut-parleur àáâãäåæçèéêë"; + const wchar_t *device_with_symbols = L"Audio ñáéíóú & symbols @#$%^&*()"; + + output::output_field("Wide Quotes", device_with_quotes); + output::output_field("Wide Trademark", device_with_trademark); + output::output_field("Wide Accents", device_with_accents); + output::output_field("Wide Symbols", device_with_symbols); + + const std::string output = capture->get_output(); + + EXPECT_TRUE(output.find("Wide Quotes : ") != std::string::npos) << "Expected 'Wide Quotes' in output"; + EXPECT_TRUE(output.find("Wide Trademark : ") != std::string::npos) << "Expected 'Wide Trademark' in output"; + EXPECT_TRUE(output.find("Wide Accents : ") != std::string::npos) << "Expected 'Wide Accents' in output"; + EXPECT_TRUE(output.find("Wide Symbols : ") != std::string::npos) << "Expected 'Wide Symbols' in output"; +} + +TEST_F(UtilityOutputTest, OutputFieldWithRealAudioDeviceNames) { + capture->clear(); + + // Test with realistic audio device names that might contain special characters + output::output_field("Realtek Device", "Realtek® High Definition Audio"); + output::output_field("Creative Device", "Creative Sound Blaster™ X-Fi"); + output::output_field("Logitech Device", "Logitech G533 Gaming Headset"); + output::output_field("Bluetooth Device", "Sony WH-1000XM4 'Wireless' Headphones"); + output::output_field("USB Device", "USB Audio Device @ 48kHz"); + + const std::string output = capture->get_output(); + + EXPECT_TRUE(output.find("Realtek Device : ") != std::string::npos) << "Expected 'Realtek Device' in output"; + EXPECT_TRUE(output.find("Creative Device : ") != std::string::npos) << "Expected 'Creative Device' in output"; + EXPECT_TRUE(output.find("Logitech Device : ") != std::string::npos) << "Expected 'Logitech Device' in output"; + EXPECT_TRUE(output.find("Bluetooth Device : ") != std::string::npos) << "Expected 'Bluetooth Device' in output"; + EXPECT_TRUE(output.find("USB Device : ") != std::string::npos) << "Expected 'USB Device' in output"; +} + +TEST_F(UtilityOutputTest, OutputFieldWithNullAndSpecialCharacters) { + capture->clear(); + + // Test null wide string with special characters in label + const wchar_t *null_value = nullptr; + output::output_field("Device™ with 'quotes'", null_value); + output::output_field("Device àáâãäåæçèéêë", null_value); + output::output_field("Device @#$%^&*()", null_value); + + const std::string output = capture->get_output(); + + EXPECT_TRUE(output.find("Device™ with 'quotes' : ") != std::string::npos) << "Expected 'Device™ with quotes' in output"; + EXPECT_TRUE(output.find("Device àáâãäåæçèéêë : ") != std::string::npos) << "Expected 'Device àáâãäåæçèéêë' in output"; + EXPECT_TRUE(output.find("Device @#$%^&*() : ") != std::string::npos) << "Expected 'Device @#$%^&*()' in output"; + + // Should contain "Unknown" for null values + size_t unknown_count = 0; + size_t pos = 0; + while ((pos = output.find("Unknown", pos)) != std::string::npos) { + unknown_count++; + pos += 7; // length of "Unknown" + } + EXPECT_EQ(unknown_count, 3) << "Expected 'Unknown' to appear 3 times for null values"; +} + +TEST_F(UtilityOutputTest, OutputFieldWithEmptyAndSpecialCharacters) { + capture->clear(); + + // Test empty values with special character labels + output::output_field("Empty Device™", ""); + output::output_field("Empty 'Quotes'", ""); + output::output_field("Empty àáâãäåæçèéêë", ""); + + const std::string output = capture->get_output(); + + EXPECT_TRUE(output.find("Empty Device™ : ") != std::string::npos) << "Expected 'Empty Device™' in output"; + EXPECT_TRUE(output.find("Empty 'Quotes' : ") != std::string::npos) << "Expected 'Empty Quotes' in output"; + EXPECT_TRUE(output.find("Empty àáâãäåæçèéêë : ") != std::string::npos) << "Expected 'Empty àáâãäåæçèéêë' in output"; + + // Count newlines - should have 3 + const size_t newline_count = std::ranges::count(output, '\n'); + EXPECT_EQ(newline_count, 3) << "Expected 3 newlines for 3 output fields with empty values"; +} + +#else +// For non-Windows platforms, the output namespace doesn't exist +TEST(UtilityOutputTest, OutputNamespaceNotAvailableOnNonWindows) { + GTEST_SKIP() << "output namespace is Windows-specific"; +} +#endif diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 26cee550e3c..19338c757fd 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -7,6 +7,7 @@ include_directories("${CMAKE_SOURCE_DIR}") add_executable(dxgi-info dxgi.cpp) set_target_properties(dxgi-info PROPERTIES CXX_STANDARD 23) target_link_libraries(dxgi-info + ${Boost_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} dxgi ${PLATFORM_LIBRARIES}) diff --git a/tools/audio.cpp b/tools/audio.cpp index e14e90536d7..450176b636b 100644 --- a/tools/audio.cpp +++ b/tools/audio.cpp @@ -6,17 +6,13 @@ // platform includes #include -#include #include #include #include #include -#include - -// lib includes -#include // local includes +#include "src/platform/windows/tools/helper.h" #include "src/utility.h" DEFINE_PROPERTYKEY(PKEY_Device_DeviceDesc, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 2); // DEVPROP_TYPE_STRING @@ -35,7 +31,7 @@ namespace audio { template void co_task_free(T *p) { - CoTaskMemFree((LPVOID) p); + CoTaskMemFree(static_cast(p)); } using device_enum_t = util::safe_ptr>; @@ -63,10 +59,6 @@ namespace audio { PROPVARIANT prop; }; - const wchar_t *no_null(const wchar_t *str) { - return str ? str : L"Unknown"; - } - struct format_t { std::string_view name; int channels; @@ -118,7 +110,11 @@ namespace audio { wave_format->nAvgBytesPerSec = wave_format->nSamplesPerSec * wave_format->nBlockAlign; if (wave_format->wFormatTag == WAVE_FORMAT_EXTENSIBLE) { - ((PWAVEFORMATEXTENSIBLE) wave_format.get())->dwChannelMask = format.channel_mask; + // Access the extended format through proper offsetting + // WAVEFORMATEXTENSIBLE has WAVEFORMATEX as first member, so this is safe + const auto ext_format = + static_cast(static_cast(wave_format.get())); + ext_format->dwChannelMask = format.channel_mask; } } @@ -128,7 +124,7 @@ namespace audio { IID_IAudioClient, CLSCTX_ALL, nullptr, - (void **) &audio_client + static_cast(static_cast(&audio_client)) ); if (FAILED(status)) { @@ -186,7 +182,7 @@ namespace audio { return; } - std::wstring device_state_string = L"Unknown"s; + std::wstring device_state_string; switch (device_state) { case DEVICE_STATE_ACTIVE: device_state_string = L"Active"s; @@ -200,28 +196,29 @@ namespace audio { case DEVICE_STATE_NOTPRESENT: device_state_string = L"Not present"s; break; + default: + device_state_string = L"Unknown"s; + break; } - std::wstring current_format = L"Unknown"s; + std::string current_format = "Unknown"; for (const auto &format : formats) { // This will fail for any format that's not the mix format for this device, // so we can take the first match as the current format to display. - auto audio_client = make_audio_client(device, format); - if (audio_client) { - current_format = boost::locale::conv::utf_to_utf(format.name.data()); + if (auto audio_client = make_audio_client(device, format)) { + current_format = std::string(format.name); break; } } - std::wcout - << L"===== Device ====="sv << std::endl - << L"Device ID : "sv << wstring.get() << std::endl - << L"Device name : "sv << no_null((LPWSTR) device_friendly_name.prop.pszVal) << std::endl - << L"Adapter name : "sv << no_null((LPWSTR) adapter_friendly_name.prop.pszVal) << std::endl - << L"Device description : "sv << no_null((LPWSTR) device_desc.prop.pszVal) << std::endl - << L"Device state : "sv << device_state_string << std::endl - << L"Current format : "sv << current_format << std::endl - << std::endl; + std::cout << "===== Device =====" << std::endl; + output::output_field("Device ID ", wstring.get()); + output::output_field("Device name ", output::no_null(device_friendly_name.prop.pwszVal)); + output::output_field("Adapter name ", output::no_null(adapter_friendly_name.prop.pwszVal)); + output::output_field("Device description ", output::no_null(device_desc.prop.pwszVal)); + output::output_field("Device state ", device_state_string.c_str()); + output::output_field("Current format ", current_format); + std::cout << std::endl; } } // namespace audio @@ -268,15 +265,13 @@ int main(int argc, char *argv[]) { } } - HRESULT status; - audio::device_enum_t device_enum; - status = CoCreateInstance( + HRESULT status = CoCreateInstance( CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL, IID_IMMDeviceEnumerator, - (void **) &device_enum + static_cast(static_cast(&device_enum)) ); if (FAILED(status)) { diff --git a/tools/dxgi.cpp b/tools/dxgi.cpp index 5ac8cac8dea..226601475ef 100644 --- a/tools/dxgi.cpp +++ b/tools/dxgi.cpp @@ -3,10 +3,12 @@ * @brief Displays information about connected displays and GPUs */ #define WINVER 0x0A00 +#include "src/platform/windows/tools/helper.h" #include "src/utility.h" #include #include +#include #include using namespace std::literals; @@ -20,17 +22,14 @@ namespace dxgi { using factory1_t = util::safe_ptr>; using adapter_t = util::safe_ptr>; using output_t = util::safe_ptr>; - } // namespace dxgi int main(int argc, char *argv[]) { - HRESULT status; - // Set ourselves as per-monitor DPI aware for accurate resolution values on High DPI systems SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); dxgi::factory1_t::pointer factory_p {}; - status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory_p); + const HRESULT status = CreateDXGIFactory1(IID_IDXGIFactory1, static_cast(static_cast(&factory_p))); dxgi::factory1_t factory {factory_p}; if (FAILED(status)) { std::cout << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']' << std::endl; @@ -44,21 +43,24 @@ int main(int argc, char *argv[]) { DXGI_ADAPTER_DESC1 adapter_desc; adapter->GetDesc1(&adapter_desc); - std::cout - << "====== ADAPTER ====="sv << std::endl; - std::wcout - << L"Device Name : "sv << adapter_desc.Description << std::endl; - std::cout - << "Device Vendor ID : 0x"sv << util::hex(adapter_desc.VendorId).to_string_view() << std::endl - << "Device Device ID : 0x"sv << util::hex(adapter_desc.DeviceId).to_string_view() << std::endl - << "Device Video Mem : "sv << adapter_desc.DedicatedVideoMemory / 1048576 << " MiB"sv << std::endl - << "Device Sys Mem : "sv << adapter_desc.DedicatedSystemMemory / 1048576 << " MiB"sv << std::endl - << "Share Sys Mem : "sv << adapter_desc.SharedSystemMemory / 1048576 << " MiB"sv << std::endl - << std::endl - << " ====== OUTPUT ======"sv << std::endl; + std::cout << "====== ADAPTER =====" << std::endl; + output::output_field("Device Name ", adapter_desc.Description); + output::output_field("Device Vendor ID ", "0x" + util::hex(adapter_desc.VendorId).to_string()); + output::output_field("Device Device ID ", "0x" + util::hex(adapter_desc.DeviceId).to_string()); + output::output_field("Device Video Mem ", std::format("{} MiB", adapter_desc.DedicatedVideoMemory / 1048576)); + output::output_field("Device Sys Mem ", std::format("{} MiB", adapter_desc.DedicatedSystemMemory / 1048576)); + output::output_field("Share Sys Mem ", std::format("{} MiB", adapter_desc.SharedSystemMemory / 1048576)); dxgi::output_t::pointer output_p {}; + bool has_outputs = false; for (int y = 0; adapter->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) { + // Print the header only when we find the first output + if (!has_outputs) { + std::cout << std::endl + << " ====== OUTPUT ======" << std::endl; + has_outputs = true; + } + dxgi::output_t output {output_p}; DXGI_OUTPUT_DESC desc; @@ -67,13 +69,11 @@ int main(int argc, char *argv[]) { auto width = desc.DesktopCoordinates.right - desc.DesktopCoordinates.left; auto height = desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top; - std::wcout - << L" Output Name : "sv << desc.DeviceName << std::endl; - std::cout - << " AttachedToDesktop : "sv << (desc.AttachedToDesktop ? "yes"sv : "no"sv) << std::endl - << " Resolution : "sv << width << 'x' << height << std::endl - << std::endl; + output::output_field(" Output Name ", desc.DeviceName); + output::output_field(" AttachedToDesktop ", desc.AttachedToDesktop ? "yes" : "no"); + output::output_field(" Resolution ", std::format("{}x{}", width, height)); } + std::cout << std::endl; } return 0; From 1d1288cfe81880b8cc42eb16b0cab115edda4a9b Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 19 Jul 2025 15:03:43 -0400 Subject: [PATCH 2/5] refactor: re-use windows platform utf-8 functions --- cmake/compile_definitions/windows.cmake | 3 +- src/platform/windows/audio.cpp | 14 +- src/platform/windows/display_base.cpp | 13 +- src/platform/windows/display_vram.cpp | 3 +- src/platform/windows/misc.cpp | 79 +--- src/platform/windows/misc.h | 14 - src/platform/windows/publish.cpp | 5 +- src/platform/windows/tools/helper.h | 127 ------ src/platform/windows/utf_utils.cpp | 66 +++ src/platform/windows/utf_utils.h | 23 + src/process.cpp | 4 +- .../unit/platform/windows/test_utf_utils.cpp | 262 +++++++++++ .../platform/windows/tools/test_helper.cpp | 424 ------------------ tools/CMakeLists.txt | 24 +- tools/audio.cpp | 21 +- tools/dxgi.cpp | 20 +- 16 files changed, 432 insertions(+), 670 deletions(-) delete mode 100644 src/platform/windows/tools/helper.h create mode 100644 src/platform/windows/utf_utils.cpp create mode 100644 src/platform/windows/utf_utils.h create mode 100644 tests/unit/platform/windows/test_utf_utils.cpp delete mode 100644 tests/unit/platform/windows/tools/test_helper.cpp diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake index 07d647f3536..4cabe8ab099 100644 --- a/cmake/compile_definitions/windows.cmake +++ b/cmake/compile_definitions/windows.cmake @@ -59,7 +59,8 @@ set(PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/windows/display_ram.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/display_wgc.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/audio.cpp" - "${CMAKE_SOURCE_DIR}/src/platform/windows/tools/helper.h" + "${CMAKE_SOURCE_DIR}/src/platform/windows/utf_utils.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/utf_utils.h" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/src/ViGEmClient.cpp" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Client.h" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Common.h" diff --git a/src/platform/windows/audio.cpp b/src/platform/windows/audio.cpp index 98af1e75bc8..d7d4680edfd 100644 --- a/src/platform/windows/audio.cpp +++ b/src/platform/windows/audio.cpp @@ -16,10 +16,10 @@ #include // local includes -#include "misc.h" #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" +#include "utf_utils.h" // Must be the last included file // clang-format off @@ -703,7 +703,7 @@ namespace platf::audio { audio::wstring_t id; device->GetId(&id); - sink.host = to_utf8(id.get()); + sink.host = utf_utils::to_utf8(id.get()); } // Prepare to search for the device_id of the virtual audio sink device, @@ -713,14 +713,14 @@ namespace platf::audio { if (config::audio.virtual_sink.empty()) { match_list = match_steam_speakers(); } else { - match_list = match_all_fields(from_utf8(config::audio.virtual_sink)); + match_list = match_all_fields(utf_utils::from_utf8(config::audio.virtual_sink)); } // Search for the virtual audio sink device currently present in the system. auto matched = find_device_id(match_list); if (matched) { // Prepare to fill virtual audio sink names with device_id. - auto device_id = to_utf8(matched->second); + auto device_id = utf_utils::to_utf8(matched->second); // Also prepend format name (basically channel layout at the moment) // because we don't want to extend the platform interface. sink.null = std::make_optional(sink_t::null_t { @@ -736,7 +736,7 @@ namespace platf::audio { } bool is_sink_available(const std::string &sink) override { - const auto match_list = match_all_fields(from_utf8(sink)); + const auto match_list = match_all_fields(utf_utils::from_utf8(sink)); const auto matched = find_device_id(match_list); return static_cast(matched); } @@ -758,7 +758,7 @@ namespace platf::audio { for (const auto &format : formats) { auto &name = format.name; if (current.find(name) == 0) { - auto device_id = from_utf8(current.substr(name.size(), current.size() - name.size())); + auto device_id = utf_utils::from_utf8(current.substr(name.size(), current.size() - name.size())); return std::make_pair(device_id, std::reference_wrapper(format)); } } @@ -805,7 +805,7 @@ namespace platf::audio { // Sink name does not begin with virtual-(format name), hence it's not a virtual sink // and we don't want to change playback format of the corresponding device. // Also need to perform matching, sink name is not necessarily device_id in this case. - auto matched = find_device_id(match_all_fields(from_utf8(sink))); + auto matched = find_device_id(match_all_fields(utf_utils::from_utf8(sink))); if (matched) { return matched->second; } else { diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index 54f748d744a..e42bb8efa4b 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -14,6 +14,9 @@ #include #include +// local includes +#include "utf_utils.h" + // We have to include boost/process/v1.hpp before display.h due to WinSock.h, // but that prevents the definition of NTSTATUS so we must define it ourself. typedef long NTSTATUS; @@ -474,8 +477,8 @@ namespace platf::dxgi { return -1; } - auto adapter_name = from_utf8(config::video.adapter_name); - auto output_name = from_utf8(display_name); + auto adapter_name = utf_utils::from_utf8(config::video.adapter_name); + auto output_name = utf_utils::from_utf8(display_name); adapter_t::pointer adapter_p; for (int tries = 0; tries < 2; ++tries) { @@ -589,7 +592,7 @@ namespace platf::dxgi { DXGI_ADAPTER_DESC adapter_desc; adapter->GetDesc(&adapter_desc); - auto description = to_utf8(adapter_desc.Description); + auto description = utf_utils::to_utf8(adapter_desc.Description); BOOST_LOG(info) << std::endl << "Device Description : " << description << std::endl @@ -1068,7 +1071,7 @@ namespace platf { BOOST_LOG(debug) << std::endl << "====== ADAPTER ====="sv << std::endl - << "Device Name : "sv << to_utf8(adapter_desc.Description) << std::endl + << "Device Name : "sv << utf_utils::to_utf8(adapter_desc.Description) << std::endl << "Device Vendor ID : 0x"sv << util::hex(adapter_desc.VendorId).to_string_view() << std::endl << "Device Device ID : 0x"sv << util::hex(adapter_desc.DeviceId).to_string_view() << std::endl << "Device Video Mem : "sv << adapter_desc.DedicatedVideoMemory / 1048576 << " MiB"sv << std::endl @@ -1084,7 +1087,7 @@ namespace platf { DXGI_OUTPUT_DESC desc; output->GetDesc(&desc); - auto device_name = to_utf8(desc.DeviceName); + auto device_name = utf_utils::to_utf8(desc.DeviceName); auto width = desc.DesktopCoordinates.right - desc.DesktopCoordinates.left; auto height = desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top; diff --git a/src/platform/windows/display_vram.cpp b/src/platform/windows/display_vram.cpp index cf6b4c43d5c..c6b694f0a35 100644 --- a/src/platform/windows/display_vram.cpp +++ b/src/platform/windows/display_vram.cpp @@ -28,6 +28,7 @@ extern "C" { #include "src/nvenc/nvenc_d3d11_on_cuda.h" #include "src/nvenc/nvenc_utils.h" #include "src/video.h" +#include "utf_utils.h" #if !defined(SUNSHINE_SHADERS_DIR) // for testing this needs to be defined in cmake as we don't do an install #define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/directx" @@ -359,7 +360,7 @@ namespace platf::dxgi { flags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; #endif - auto wFile = from_utf8(file); + auto wFile = utf_utils::from_utf8(file); auto status = D3DCompileFromFile(wFile.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, entrypoint, shader_model, flags, 0, &compiled_p, &msg_p); if (msg_p) { diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index cb276034512..bd4f4166da5 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -45,6 +45,7 @@ #include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" +#include "utf_utils.h" // UDP_SEND_MSG_SIZE was added in the Windows 10 20H1 SDK #ifndef UDP_SEND_MSG_SIZE @@ -314,7 +315,7 @@ namespace platf { // Parse the environment block and populate env for (auto c = (PWCHAR) env_block; *c != UNICODE_NULL; c += wcslen(c) + 1) { // Environment variable entries end with a null-terminator, so std::wstring() will get an entire entry. - std::string env_tuple = to_utf8(std::wstring {c}); + std::string env_tuple = utf_utils::to_utf8(std::wstring {c}); std::string env_name = env_tuple.substr(0, env_tuple.find('=')); std::string env_val = env_tuple.substr(env_tuple.find('=') + 1); @@ -384,7 +385,7 @@ namespace platf { for (const auto &entry : env) { auto name = entry.get_name(); auto value = entry.to_string(); - size += from_utf8(name).length() + 1 /* L'=' */ + from_utf8(value).length() + 1 /* L'\0' */; + size += utf_utils::from_utf8(name).length() + 1 /* L'=' */ + utf_utils::from_utf8(value).length() + 1 /* L'\0' */; } size += 1 /* L'\0' */; @@ -396,9 +397,9 @@ namespace platf { auto value = entry.to_string(); // Construct the NAME=VAL\0 string - append_string_to_environment_block(env_block, offset, from_utf8(name)); + append_string_to_environment_block(env_block, offset, utf_utils::from_utf8(name)); env_block[offset++] = L'='; - append_string_to_environment_block(env_block, offset, from_utf8(value)); + append_string_to_environment_block(env_block, offset, utf_utils::from_utf8(value)); env_block[offset++] = L'\0'; } @@ -676,14 +677,14 @@ namespace platf { * @return A command string suitable for use by CreateProcess(). */ std::wstring resolve_command_string(const std::string &raw_cmd, const std::wstring &working_dir, HANDLE token, DWORD &creation_flags) { - std::wstring raw_cmd_w = from_utf8(raw_cmd); + std::wstring raw_cmd_w = utf_utils::from_utf8(raw_cmd); // First, convert the given command into parts so we can get the executable/file/URL without parameters auto raw_cmd_parts = boost::program_options::split_winmain(raw_cmd_w); if (raw_cmd_parts.empty()) { // This is highly unexpected, but we'll just return the raw string and hope for the best. BOOST_LOG(warning) << "Failed to split command string: "sv << raw_cmd; - return from_utf8(raw_cmd); + return utf_utils::from_utf8(raw_cmd); } auto raw_target = raw_cmd_parts.at(0); @@ -697,7 +698,7 @@ namespace platf { res = UrlGetPartW(raw_target.c_str(), scheme.data(), &out_len, URL_PART_SCHEME, 0); if (res != S_OK) { BOOST_LOG(warning) << "Failed to extract URL scheme from URL: "sv << raw_target << " ["sv << util::hex(res).to_string_view() << ']'; - return from_utf8(raw_cmd); + return utf_utils::from_utf8(raw_cmd); } // If the target is a URL, the class is found using the URL scheme (prior to and not including the ':') @@ -708,13 +709,13 @@ namespace platf { if (extension == nullptr || *extension == 0) { // If the file has no extension, assume it's a command and allow CreateProcess() // to try to find it via PATH - return from_utf8(raw_cmd); + return utf_utils::from_utf8(raw_cmd); } else if (boost::iequals(extension, L".exe")) { // If the file has an .exe extension, we will bypass the resolution here and // directly pass the unmodified command string to CreateProcess(). The argument // escaping rules are subtly different between CreateProcess() and ShellExecute(), // and we want to preserve backwards compatibility with older configs. - return from_utf8(raw_cmd); + return utf_utils::from_utf8(raw_cmd); } // For regular files, the class is found using the file extension (including the dot) @@ -731,7 +732,7 @@ namespace platf { // Override HKEY_CLASSES_ROOT and HKEY_CURRENT_USER to ensure we query the correct class info if (!override_per_user_predefined_keys(token)) { - return from_utf8(raw_cmd); + return utf_utils::from_utf8(raw_cmd); } // Find the command string for the specified class @@ -762,7 +763,7 @@ namespace platf { if (res != S_OK) { BOOST_LOG(warning) << "Failed to query command string for raw command: "sv << raw_cmd << " ["sv << util::hex(res).to_string_view() << ']'; - return from_utf8(raw_cmd); + return utf_utils::from_utf8(raw_cmd); } // Finally, construct the real command string that will be passed into CreateProcess(). @@ -896,7 +897,7 @@ namespace platf { * @return A `bp::child` object representing the new process, or an empty `bp::child` object if the launch fails. */ bp::child run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { - std::wstring start_dir = from_utf8(working_dir.string()); + std::wstring start_dir = utf_utils::from_utf8(working_dir.string()); HANDLE job = group ? group->native_handle() : nullptr; STARTUPINFOEXW startup_info = create_startup_info(file, job ? &job : nullptr, ec); PROCESS_INFORMATION process_info; @@ -1690,65 +1691,13 @@ namespace platf { return {}; } - std::wstring from_utf8(const std::string &string) { - // No conversion needed if the string is empty - if (string.empty()) { - return {}; - } - - // Get the output size required to store the string - auto output_size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, string.data(), string.size(), nullptr, 0); - if (output_size == 0) { - auto winerr = GetLastError(); - BOOST_LOG(error) << "Failed to get UTF-16 buffer size: "sv << winerr; - return {}; - } - - // Perform the conversion - std::wstring output(output_size, L'\0'); - output_size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, string.data(), string.size(), output.data(), output.size()); - if (output_size == 0) { - auto winerr = GetLastError(); - BOOST_LOG(error) << "Failed to convert string to UTF-16: "sv << winerr; - return {}; - } - - return output; - } - - std::string to_utf8(const std::wstring &string) { - // No conversion needed if the string is empty - if (string.empty()) { - return {}; - } - - // Get the output size required to store the string - auto output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string.data(), string.size(), nullptr, 0, nullptr, nullptr); - if (output_size == 0) { - auto winerr = GetLastError(); - BOOST_LOG(error) << "Failed to get UTF-8 buffer size: "sv << winerr; - return {}; - } - - // Perform the conversion - std::string output(output_size, '\0'); - output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string.data(), string.size(), output.data(), output.size(), nullptr, nullptr); - if (output_size == 0) { - auto winerr = GetLastError(); - BOOST_LOG(error) << "Failed to convert string to UTF-8: "sv << winerr; - return {}; - } - - return output; - } - std::string get_host_name() { WCHAR hostname[256]; if (GetHostNameW(hostname, ARRAYSIZE(hostname)) == SOCKET_ERROR) { BOOST_LOG(error) << "GetHostNameW() failed: "sv << WSAGetLastError(); return "Sunshine"s; } - return to_utf8(hostname); + return utf_utils::to_utf8(hostname); } class win32_high_precision_timer: public high_precision_timer { diff --git a/src/platform/windows/misc.h b/src/platform/windows/misc.h index ba58c877052..11e7af4a6e7 100644 --- a/src/platform/windows/misc.h +++ b/src/platform/windows/misc.h @@ -19,18 +19,4 @@ namespace platf { int64_t qpc_counter(); std::chrono::nanoseconds qpc_time_difference(int64_t performance_counter1, int64_t performance_counter2); - - /** - * @brief Convert a UTF-8 string into a UTF-16 wide string. - * @param string The UTF-8 string. - * @return The converted UTF-16 wide string. - */ - std::wstring from_utf8(const std::string &string); - - /** - * @brief Convert a UTF-16 wide string into a UTF-8 string. - * @param string The UTF-16 wide string. - * @return The converted UTF-8 string. - */ - std::string to_utf8(const std::wstring &string); } // namespace platf diff --git a/src/platform/windows/publish.cpp b/src/platform/windows/publish.cpp index e93001a4fc7..a96e14ca8e4 100644 --- a/src/platform/windows/publish.cpp +++ b/src/platform/windows/publish.cpp @@ -19,6 +19,7 @@ #include "src/nvhttp.h" #include "src/platform/common.h" #include "src/thread_safe.h" +#include "utf_utils.h" #define _FN(x, ret, args) \ typedef ret(*x##_fn) args; \ @@ -109,8 +110,8 @@ namespace platf::publish { std::wstring domain {SERVICE_TYPE_DOMAIN.data(), SERVICE_TYPE_DOMAIN.size()}; auto hostname = platf::get_host_name(); - auto name = from_utf8(net::mdns_instance_name(hostname) + '.') + domain; - auto host = from_utf8(hostname + ".local"); + auto name = utf_utils::from_utf8(net::mdns_instance_name(hostname) + '.') + domain; + auto host = utf_utils::from_utf8(hostname + ".local"); DNS_SERVICE_INSTANCE instance {}; instance.pszInstanceName = name.data(); diff --git a/src/platform/windows/tools/helper.h b/src/platform/windows/tools/helper.h deleted file mode 100644 index def2a57857e..00000000000 --- a/src/platform/windows/tools/helper.h +++ /dev/null @@ -1,127 +0,0 @@ -/** - * @file src/platform/windows/tools/helper.h - * @brief Helpers for tools. - */ -#pragma once - -// standard includes -#include -#include - -// lib includes -#include - -// platform includes -#include - -/** - * @brief Safe console output utilities for Windows - * These functions prevent crashes when outputting strings with special characters. - * This is only used in tools/audio-info and tools/dxgi-info. - */ -namespace output { - // ASCII character range constants for safe output, https://www.ascii-code.com/ - static constexpr int ASCII_PRINTABLE_START = 32; - static constexpr int ASCII_PRINTABLE_END = 127; - - /** - * @brief Return a non-null wide string, defaulting to "Unknown" if null - * @param str The wide string to check - * @return A non-null wide string - */ - inline const wchar_t *no_null(const wchar_t *str) { - return str ? str : L"Unknown"; - } - - /** - * @brief Safely convert a wide string to console output using Windows API - * @param wstr The wide string to output - */ - inline void safe_wcout(const std::wstring &wstr) { - if (wstr.empty()) { - return; - } - - // Try to use the Windows console API for proper Unicode output - if (const HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); hConsole != INVALID_HANDLE_VALUE) { - DWORD written; - if (WriteConsoleW(hConsole, wstr.c_str(), wstr.length(), &written, nullptr)) { - return; // Success with WriteConsoleW - } - } - - // Fallback: convert to narrow string and output to std::cout - try { - const std::string narrow_str = boost::locale::conv::utf_to_utf(wstr); - std::cout << narrow_str; - } catch (const boost::locale::conv::conversion_error &) { - // Final fallback: output character by character, replacing non-ASCII - for (const wchar_t wc : wstr) { - if (wc >= ASCII_PRINTABLE_START && wc < ASCII_PRINTABLE_END) { // Printable ASCII - std::cout << static_cast(wc); - } else { - std::cout << '?'; - } - } - } - } - - /** - * @brief Safely convert a wide string literal to console encoding and output it - * @param wstr The wide string literal to output - */ - inline void safe_wcout(const wchar_t *wstr) { - if (wstr) { - safe_wcout(std::wstring(wstr)); - } else { - std::cout << "Unknown"; - } - } - - /** - * @brief Safely convert a string to wide string and then to console output - * @param str The string to output - */ - inline void safe_cout(const std::string &str) { - if (str.empty()) { - return; - } - - try { - // Convert string to wide string first, then to console output - const std::wstring wstr = boost::locale::conv::utf_to_utf(str); - safe_wcout(wstr); - } catch (const boost::locale::conv::conversion_error &) { - // Fallback: output string directly, replacing problematic characters - for (const char c : str) { - if (c >= ASCII_PRINTABLE_START && c < ASCII_PRINTABLE_END) { // Printable ASCII - std::cout << c; - } else { - std::cout << '?'; - } - } - } - } - - /** - * @brief Output a label and value pair safely - * @param label The label to output - * @param value The wide string value to output - */ - inline void output_field(const std::string &label, const wchar_t *value) { - std::cout << label << " : "; - safe_wcout(value ? value : L"Unknown"); - std::cout << std::endl; - } - - /** - * @brief Output a label and string value pair - * @param label The label to output - * @param value The string value to output - */ - inline void output_field(const std::string &label, const std::string &value) { - std::cout << label << " : "; - safe_cout(value); - std::cout << std::endl; - } -} // namespace output diff --git a/src/platform/windows/utf_utils.cpp b/src/platform/windows/utf_utils.cpp new file mode 100644 index 00000000000..45635ad0772 --- /dev/null +++ b/src/platform/windows/utf_utils.cpp @@ -0,0 +1,66 @@ +/** + * @file src/platform/windows/utf_utils.cpp + * @brief Minimal UTF conversion utilities for Windows tools + */ +#include "utf_utils.h" + +#include "src/logging.h" + +#include +#include + +using namespace std::literals; + +namespace utf_utils { + std::wstring from_utf8(const std::string &string) { + // No conversion needed if the string is empty + if (string.empty()) { + return {}; + } + + // Get the output size required to store the string + auto output_size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, string.data(), string.size(), nullptr, 0); + if (output_size == 0) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "Failed to get UTF-16 buffer size: "sv << winerr; + return {}; + } + + // Perform the conversion + std::wstring output(output_size, L'\0'); + output_size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, string.data(), string.size(), output.data(), output.size()); + if (output_size == 0) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "Failed to convert string to UTF-16: "sv << winerr; + return {}; + } + + return output; + } + + std::string to_utf8(const std::wstring &string) { + // No conversion needed if the string is empty + if (string.empty()) { + return {}; + } + + // Get the output size required to store the string + auto output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string.data(), string.size(), nullptr, 0, nullptr, nullptr); + if (output_size == 0) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "Failed to get UTF-8 buffer size: "sv << winerr; + return {}; + } + + // Perform the conversion + std::string output(output_size, '\0'); + output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string.data(), string.size(), output.data(), output.size(), nullptr, nullptr); + if (output_size == 0) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "Failed to convert string to UTF-8: "sv << winerr; + return {}; + } + + return output; + } +} // namespace utf_utils diff --git a/src/platform/windows/utf_utils.h b/src/platform/windows/utf_utils.h new file mode 100644 index 00000000000..8759572664f --- /dev/null +++ b/src/platform/windows/utf_utils.h @@ -0,0 +1,23 @@ +/** + * @file src/platform/windows/utf_utils.h + * @brief Minimal UTF conversion utilities for Windows tools + */ +#pragma once + +#include + +namespace utf_utils { + /** + * @brief Convert a UTF-8 string into a UTF-16 wide string. + * @param string The UTF-8 string. + * @return The converted UTF-16 wide string. + */ + std::wstring from_utf8(const std::string &string); + + /** + * @brief Convert a UTF-16 wide string into a UTF-8 string. + * @param string The UTF-16 wide string. + * @return The converted UTF-8 string. + */ + std::string to_utf8(const std::wstring &string); +} // namespace utf_utils diff --git a/src/process.cpp b/src/process.cpp index 1bf41c594f9..84c1a6cf0ab 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -33,7 +33,7 @@ #ifdef _WIN32 // from_utf8() string conversion function - #include "platform/windows/misc.h" + #include "platform/windows/utf_utils.h" // _SH constants for _wfsopen() #include @@ -183,7 +183,7 @@ namespace proc { #ifdef _WIN32 // fopen() interprets the filename as an ANSI string on Windows, so we must convert it // to UTF-16 and use the wchar_t variants for proper Unicode log file path support. - auto woutput = platf::from_utf8(_app.output); + auto woutput = utf_utils::from_utf8(_app.output); // Use _SH_DENYNO to allow us to open this log file again for writing even if it is // still open from a previous execution. This is required to handle the case of a diff --git a/tests/unit/platform/windows/test_utf_utils.cpp b/tests/unit/platform/windows/test_utf_utils.cpp new file mode 100644 index 00000000000..d7afc41b134 --- /dev/null +++ b/tests/unit/platform/windows/test_utf_utils.cpp @@ -0,0 +1,262 @@ +/** + * @file tests/unit/platform/windows/test_utf_utils.cpp + * @brief Test src/platform/windows/utf_utils.cpp UTF conversion functions. + */ +#include "../../../tests_common.h" + +#include +#include + +#ifdef _WIN32 + #include + #include +#endif + +#ifdef _WIN32 +/** + * @brief Test fixture for utf_utils namespace functions + */ +class UtfUtilsTest: public testing::Test {}; + +TEST_F(UtfUtilsTest, FromUtf8WithEmptyString) { + const std::string empty_string = ""; + const std::wstring result = utf_utils::from_utf8(empty_string); + + EXPECT_TRUE(result.empty()) << "Empty UTF-8 string should produce empty wide string"; +} + +TEST_F(UtfUtilsTest, ToUtf8WithEmptyWideString) { + const std::wstring empty_wstring = L""; + const std::string result = utf_utils::to_utf8(empty_wstring); + + EXPECT_TRUE(result.empty()) << "Empty wide string should produce empty UTF-8 string"; +} + +TEST_F(UtfUtilsTest, FromUtf8WithBasicString) { + const std::string test_string = "Hello World"; + const std::wstring result = utf_utils::from_utf8(test_string); + + EXPECT_EQ(result, L"Hello World") << "Basic ASCII string should convert correctly"; +} + +TEST_F(UtfUtilsTest, ToUtf8WithBasicWideString) { + const std::wstring test_wstring = L"Hello World"; + const std::string result = utf_utils::to_utf8(test_wstring); + + EXPECT_EQ(result, "Hello World") << "Basic wide string should convert correctly"; +} + +TEST_F(UtfUtilsTest, RoundTripConversionBasic) { + const std::string original = "Test String"; + const std::wstring wide = utf_utils::from_utf8(original); + const std::string converted_back = utf_utils::to_utf8(wide); + + EXPECT_EQ(original, converted_back) << "Round trip conversion should preserve basic string"; +} + +TEST_F(UtfUtilsTest, FromUtf8WithQuotationMarks) { + // Test various quotation marks that might appear in device names + const std::string single_quote = "Device 'Audio' Output"; + const std::string double_quote = "Device \"Audio\" Output"; + const std::string left_quote = "Device \u{2018}Audio\u{2019} Output"; // Unicode left/right single quotes + const std::string right_quote = "Device \u{2019}Audio\u{2018} Output"; // Unicode right/left single quotes + const std::string left_double = "Device \u{201C}Audio\u{201D} Output"; // Unicode left/right double quotes + const std::string right_double = "Device \u{201D}Audio\u{201C} Output"; // Unicode right/left double quotes + + const std::wstring result1 = utf_utils::from_utf8(single_quote); + const std::wstring result2 = utf_utils::from_utf8(double_quote); + const std::wstring result3 = utf_utils::from_utf8(left_quote); + const std::wstring result4 = utf_utils::from_utf8(right_quote); + const std::wstring result5 = utf_utils::from_utf8(left_double); + const std::wstring result6 = utf_utils::from_utf8(right_double); + + EXPECT_EQ(result1, L"Device 'Audio' Output") << "Single quote conversion failed"; + EXPECT_EQ(result2, L"Device \"Audio\" Output") << "Double quote conversion failed"; + EXPECT_EQ(result3, L"Device \u{2018}Audio\u{2019} Output") << "Left quote conversion failed"; + EXPECT_EQ(result4, L"Device \u{2019}Audio\u{2018} Output") << "Right quote conversion failed"; + EXPECT_EQ(result5, L"Device \u{201C}Audio\u{201D} Output") << "Left double quote conversion failed"; + EXPECT_EQ(result6, L"Device \u{201D}Audio\u{201C} Output") << "Right double quote conversion failed"; +} + +TEST_F(UtfUtilsTest, FromUtf8WithTrademarkSymbols) { + // Test trademark and copyright symbols + const std::string trademark = "Audio Device™"; + const std::string registered = "Audio Device®"; + const std::string copyright = "Audio Device©"; + const std::string combined = "Realtek® Audio™"; + + const std::wstring result1 = utf_utils::from_utf8(trademark); + const std::wstring result2 = utf_utils::from_utf8(registered); + const std::wstring result3 = utf_utils::from_utf8(copyright); + const std::wstring result4 = utf_utils::from_utf8(combined); + + EXPECT_EQ(result1, L"Audio Device™") << "Trademark symbol conversion failed"; + EXPECT_EQ(result2, L"Audio Device®") << "Registered symbol conversion failed"; + EXPECT_EQ(result3, L"Audio Device©") << "Copyright symbol conversion failed"; + EXPECT_EQ(result4, L"Realtek® Audio™") << "Combined symbols conversion failed"; +} + +TEST_F(UtfUtilsTest, FromUtf8WithAccentedCharacters) { + // Test accented characters that might appear in international device names + const std::string french = "Haut-parleur à haute qualité"; + const std::string spanish = "Altavoz ñáéíóú"; + const std::string german = "Lautsprecher äöü"; + const std::string mixed = "àáâãäåæçèéêë"; + + const std::wstring result1 = utf_utils::from_utf8(french); + const std::wstring result2 = utf_utils::from_utf8(spanish); + const std::wstring result3 = utf_utils::from_utf8(german); + const std::wstring result4 = utf_utils::from_utf8(mixed); + + EXPECT_EQ(result1, L"Haut-parleur à haute qualité") << "French accents conversion failed"; + EXPECT_EQ(result2, L"Altavoz ñáéíóú") << "Spanish accents conversion failed"; + EXPECT_EQ(result3, L"Lautsprecher äöü") << "German accents conversion failed"; + EXPECT_EQ(result4, L"àáâãäåæçèéêë") << "Mixed accents conversion failed"; +} + +TEST_F(UtfUtilsTest, FromUtf8WithSpecialSymbols) { + // Test various special symbols + const std::string math_symbols = "Audio @ 44.1kHz ± 0.1%"; + const std::string punctuation = "Audio Device #1 & #2"; + const std::string programming = "Device $%^&*()"; + const std::string mixed_symbols = "Audio™ @#$%^&*()"; + + const std::wstring result1 = utf_utils::from_utf8(math_symbols); + const std::wstring result2 = utf_utils::from_utf8(punctuation); + const std::wstring result3 = utf_utils::from_utf8(programming); + const std::wstring result4 = utf_utils::from_utf8(mixed_symbols); + + EXPECT_EQ(result1, L"Audio @ 44.1kHz ± 0.1%") << "Math symbols conversion failed"; + EXPECT_EQ(result2, L"Audio Device #1 & #2") << "Punctuation conversion failed"; + EXPECT_EQ(result3, L"Device $%^&*()") << "Programming symbols conversion failed"; + EXPECT_EQ(result4, L"Audio™ @#$%^&*()") << "Mixed symbols conversion failed"; +} + +TEST_F(UtfUtilsTest, ToUtf8WithQuotationMarks) { + // Test various quotation marks conversion from wide to UTF-8 + const std::wstring single_quote = L"Device 'Audio' Output"; + const std::wstring double_quote = L"Device \"Audio\" Output"; + const std::wstring left_quote = L"Device \u{2018}Audio\u{2019} Output"; // Unicode left/right single quotes + const std::wstring right_quote = L"Device \u{2019}Audio\u{2018} Output"; // Unicode right/left single quotes + const std::wstring left_double = L"Device \u{201C}Audio\u{201D} Output"; // Unicode left/right double quotes + const std::wstring right_double = L"Device \u{201D}Audio\u{201C} Output"; // Unicode right/left double quotes + + const std::string result1 = utf_utils::to_utf8(single_quote); + const std::string result2 = utf_utils::to_utf8(double_quote); + const std::string result3 = utf_utils::to_utf8(left_quote); + const std::string result4 = utf_utils::to_utf8(right_quote); + const std::string result5 = utf_utils::to_utf8(left_double); + const std::string result6 = utf_utils::to_utf8(right_double); + + EXPECT_EQ(result1, "Device 'Audio' Output") << "Single quote to UTF-8 conversion failed"; + EXPECT_EQ(result2, "Device \"Audio\" Output") << "Double quote to UTF-8 conversion failed"; + EXPECT_EQ(result3, "Device \u{2018}Audio\u{2019} Output") << "Left quote to UTF-8 conversion failed"; + EXPECT_EQ(result4, "Device \u{2019}Audio\u{2018} Output") << "Right quote to UTF-8 conversion failed"; + EXPECT_EQ(result5, "Device \u{201C}Audio\u{201D} Output") << "Left double quote to UTF-8 conversion failed"; + EXPECT_EQ(result6, "Device \u{201D}Audio\u{201C} Output") << "Right double quote to UTF-8 conversion failed"; +} + +TEST_F(UtfUtilsTest, ToUtf8WithTrademarkSymbols) { + // Test trademark and copyright symbols conversion from wide to UTF-8 + const std::wstring trademark = L"Audio Device™"; + const std::wstring registered = L"Audio Device®"; + const std::wstring copyright = L"Audio Device©"; + const std::wstring combined = L"Realtek® Audio™"; + + const std::string result1 = utf_utils::to_utf8(trademark); + const std::string result2 = utf_utils::to_utf8(registered); + const std::string result3 = utf_utils::to_utf8(copyright); + const std::string result4 = utf_utils::to_utf8(combined); + + EXPECT_EQ(result1, "Audio Device™") << "Trademark symbol to UTF-8 conversion failed"; + EXPECT_EQ(result2, "Audio Device®") << "Registered symbol to UTF-8 conversion failed"; + EXPECT_EQ(result3, "Audio Device©") << "Copyright symbol to UTF-8 conversion failed"; + EXPECT_EQ(result4, "Realtek® Audio™") << "Combined symbols to UTF-8 conversion failed"; +} + +TEST_F(UtfUtilsTest, RoundTripConversionWithSpecialCharacters) { + // Test round trip conversion with various special characters + const std::string quotes = "Device 'Audio' with \u{201C}Special\u{201D} Characters"; + const std::string symbols = "Realtek® Audio™ @ 44.1kHz ± 0.1%"; + const std::string accents = "Haut-parleur àáâãäåæçèéêë"; + const std::string mixed = "Audio™ 'Device' @#$%^&*() ñáéíóú"; + + // Convert to wide and back + const std::wstring wide1 = utf_utils::from_utf8(quotes); + const std::wstring wide2 = utf_utils::from_utf8(symbols); + const std::wstring wide3 = utf_utils::from_utf8(accents); + const std::wstring wide4 = utf_utils::from_utf8(mixed); + + const std::string back1 = utf_utils::to_utf8(wide1); + const std::string back2 = utf_utils::to_utf8(wide2); + const std::string back3 = utf_utils::to_utf8(wide3); + const std::string back4 = utf_utils::to_utf8(wide4); + + EXPECT_EQ(quotes, back1) << "Round trip failed for quotes"; + EXPECT_EQ(symbols, back2) << "Round trip failed for symbols"; + EXPECT_EQ(accents, back3) << "Round trip failed for accents"; + EXPECT_EQ(mixed, back4) << "Round trip failed for mixed characters"; +} + +TEST_F(UtfUtilsTest, RealAudioDeviceNames) { + // Test with realistic audio device names that contain special characters + const std::string realtek = "Realtek® High Definition Audio"; + const std::string creative = "Creative Sound Blaster™ X-Fi"; + const std::string logitech = "Logitech G533 Gaming Headset"; + const std::string bluetooth = "Sony WH-1000XM4 'Wireless' Headphones"; + const std::string usb = "USB Audio Device @ 48kHz"; + + // Test conversion to wide + const std::wstring wide_realtek = utf_utils::from_utf8(realtek); + const std::wstring wide_creative = utf_utils::from_utf8(creative); + const std::wstring wide_logitech = utf_utils::from_utf8(logitech); + const std::wstring wide_bluetooth = utf_utils::from_utf8(bluetooth); + const std::wstring wide_usb = utf_utils::from_utf8(usb); + + EXPECT_FALSE(wide_realtek.empty()) << "Realtek device name conversion failed"; + EXPECT_FALSE(wide_creative.empty()) << "Creative device name conversion failed"; + EXPECT_FALSE(wide_logitech.empty()) << "Logitech device name conversion failed"; + EXPECT_FALSE(wide_bluetooth.empty()) << "Bluetooth device name conversion failed"; + EXPECT_FALSE(wide_usb.empty()) << "USB device name conversion failed"; + + // Test round trip + EXPECT_EQ(realtek, utf_utils::to_utf8(wide_realtek)) << "Realtek round trip failed"; + EXPECT_EQ(creative, utf_utils::to_utf8(wide_creative)) << "Creative round trip failed"; + EXPECT_EQ(logitech, utf_utils::to_utf8(wide_logitech)) << "Logitech round trip failed"; + EXPECT_EQ(bluetooth, utf_utils::to_utf8(wide_bluetooth)) << "Bluetooth round trip failed"; + EXPECT_EQ(usb, utf_utils::to_utf8(wide_usb)) << "USB round trip failed"; +} + +TEST_F(UtfUtilsTest, InvalidUtf8Sequences) { + // Test with invalid UTF-8 sequences - should return empty string + const std::string invalid1 = "Test\x{FF}\x{FE}\x{FD}"; // Invalid UTF-8 bytes + const std::string invalid2 = "Test\x{80}\x{81}\x{82}"; // Invalid continuation bytes + + const std::wstring result1 = utf_utils::from_utf8(invalid1); + const std::wstring result2 = utf_utils::from_utf8(invalid2); + + // The function should return empty string for invalid UTF-8 sequences + EXPECT_TRUE(result1.empty()) << "Invalid UTF-8 sequence should return empty string"; + EXPECT_TRUE(result2.empty()) << "Invalid UTF-8 sequence should return empty string"; +} + +TEST_F(UtfUtilsTest, LongStringsWithSpecialCharacters) { + // Test with longer strings containing many special characters + std::string long_special = "Device™ with 'special' characters: àáâãäåæçèéêë ñáéíóú äöü "; + for (int i = 0; i < 10; ++i) { + long_special += "Audio® Device™ @#$%^&*() "; + } + + const std::wstring wide_result = utf_utils::from_utf8(long_special); + const std::string back_result = utf_utils::to_utf8(wide_result); + + EXPECT_FALSE(wide_result.empty()) << "Long string conversion should not be empty"; + EXPECT_EQ(long_special, back_result) << "Long string round trip should preserve content"; +} + +#else +// For non-Windows platforms, the utf_utils namespace doesn't exist +TEST(UtfUtilsTest, UtfUtilsNotAvailableOnNonWindows) { + GTEST_SKIP() << "utf_utils namespace is Windows-specific"; +} +#endif diff --git a/tests/unit/platform/windows/tools/test_helper.cpp b/tests/unit/platform/windows/tools/test_helper.cpp deleted file mode 100644 index 5401a96f409..00000000000 --- a/tests/unit/platform/windows/tools/test_helper.cpp +++ /dev/null @@ -1,424 +0,0 @@ -/** - * @file tests/unit/platform/windows/tools/test_helper.cpp - * @brief Test src/platform/windows/tools/helper.cpp output functions. - */ -#include "../../../../tests_common.h" - -#include -#include -#include - -#ifdef _WIN32 - #include - #include - #include - #include -#endif - -namespace { - /** - * @brief Helper class to capture console output for testing - */ - class ConsoleCapture { - public: - ConsoleCapture() { - // Save original cout buffer - original_cout_buffer = std::cout.rdbuf(); - // Redirect cout to our stringstream - std::cout.rdbuf(captured_output.rdbuf()); - } - - ~ConsoleCapture() { - try { - // Restore original cout buffer - std::cout.rdbuf(original_cout_buffer); - } catch (std::exception &e) { - std::cerr << "Error restoring cout buffer: " << e.what() << std::endl; - } - } - - std::string get_output() const { - return captured_output.str(); - } - - void clear() { - captured_output.str(""); - captured_output.clear(); - } - - private: - std::streambuf *original_cout_buffer; - std::stringstream captured_output; - }; -} // namespace - -#ifdef _WIN32 -/** - * @brief Test fixture for output namespace functions - */ -class UtilityOutputTest: public testing::Test { // NOSONAR -protected: - void SetUp() override { - capture = std::make_unique(); - } - - void TearDown() override { - capture.reset(); - } - - std::unique_ptr capture; -}; - -TEST_F(UtilityOutputTest, NoNullWithValidString) { - const wchar_t *test_string = L"Valid String"; - const wchar_t *result = output::no_null(test_string); - - EXPECT_EQ(result, test_string) << "Expected no change for valid string"; - EXPECT_STREQ(result, L"Valid String") << "Expected exact match for valid string"; -} - -TEST_F(UtilityOutputTest, NoNullWithNullString) { - const wchar_t *null_string = nullptr; - const wchar_t *result = output::no_null(null_string); - - EXPECT_NE(result, nullptr) << "Expected non-null result for null input"; - EXPECT_STREQ(result, L"Unknown") << "Expected 'Unknown' for null input"; -} - -TEST_F(UtilityOutputTest, SafeWcoutWithValidWideString) { - std::wstring test_string = L"Hello World"; - - capture->clear(); - output::safe_wcout(test_string); - const std::string output = capture->get_output(); - - // In test environment, WriteConsoleW will likely fail, so it should fall back to boost::locale conversion - EXPECT_EQ(output, "Hello World") << "Expected exact string output from safe_wcout"; -} - -TEST_F(UtilityOutputTest, SafeWcoutWithEmptyWideString) { - const std::wstring empty_string = L""; - - capture->clear(); - output::safe_wcout(empty_string); - const std::string output = capture->get_output(); - - // Empty string should return early without output - EXPECT_TRUE(output.empty()) << "Empty wide string should produce no output"; -} - -TEST_F(UtilityOutputTest, SafeWcoutWithValidWideStringPointer) { - const wchar_t *test_string = L"Test String"; - - capture->clear(); - output::safe_wcout(test_string); - const std::string output = capture->get_output(); - - EXPECT_EQ(output, "Test String") << "Expected exact string output from safe_wcout with pointer"; -} - -TEST_F(UtilityOutputTest, SafeWcoutWithNullWideStringPointer) { - const wchar_t *null_string = nullptr; - - capture->clear(); - output::safe_wcout(null_string); - const std::string output = capture->get_output(); - - EXPECT_EQ(output, "Unknown") << "Expected 'Unknown' output from safe_wcout with null pointer"; -} - -TEST_F(UtilityOutputTest, SafeCoutWithValidString) { - const std::string test_string = "Hello World"; - - capture->clear(); - output::safe_cout(test_string); - const std::string output = capture->get_output(); - - EXPECT_EQ(output, "Hello World") << "Expected exact string output from safe_cout"; -} - -TEST_F(UtilityOutputTest, SafeCoutWithEmptyString) { - std::string empty_string = ""; - - capture->clear(); - output::safe_cout(empty_string); - const std::string output = capture->get_output(); - - // Empty string should return early - EXPECT_TRUE(output.empty()) << "Empty string should produce no output"; -} - -TEST_F(UtilityOutputTest, SafeCoutWithSpecialCharacters) { - const std::string special_string = "Test\x{01}\x{02}\x{03}String"; - - capture->clear(); - output::safe_cout(special_string); - const std::string output = capture->get_output(); - - // Should handle special characters without crashing - EXPECT_FALSE(output.empty()) << "Expected some output from safe_cout with special chars"; - - // The function should either succeed with boost::locale conversion or fall back to character replacement - // In the fallback case, non-printable characters (\x{01}, \x{02}, \x{03}) should be replaced with '?' - // So we expect either the original string or "Test???String" - EXPECT_TRUE(output == "Test\x{01}\x{02}\x{03}String" || output == "Test???String") - << "Expected either original string or fallback with '?' replacements, got: '" << output << "'"; -} - -TEST_F(UtilityOutputTest, OutputFieldWithWideStringPointer) { - const wchar_t *test_value = L"Test Value"; - const std::string label = "Test Label"; - - capture->clear(); - output::output_field(label, test_value); - const std::string output = capture->get_output(); - - EXPECT_TRUE(output.find("Test Label : ") != std::string::npos) << "Expected label in output"; - EXPECT_TRUE(output.find("\n") != std::string::npos) << "Expected newline at the end of output"; -} - -TEST_F(UtilityOutputTest, OutputFieldWithNullWideStringPointer) { - const wchar_t *null_value = nullptr; - const std::string label = "Test Label"; - - capture->clear(); - output::output_field(label, null_value); - const std::string output = capture->get_output(); - - EXPECT_TRUE(output.find("Test Label : ") != std::string::npos) << "Expected label in output"; - EXPECT_TRUE(output.find("Unknown") != std::string::npos) << "Expected 'Unknown' for null value"; - EXPECT_TRUE(output.find("\n") != std::string::npos) << "Expected newline at the end of output"; -} - -TEST_F(UtilityOutputTest, OutputFieldWithRegularString) { - const std::string test_value = "Test Value"; - const std::string label = "Test Label"; - - capture->clear(); - output::output_field(label, test_value); - const std::string output = capture->get_output(); - - EXPECT_TRUE(output.find("Test Label : ") != std::string::npos) << "Expected label in output"; - EXPECT_TRUE(output.find("\n") != std::string::npos) << "Expected newline at the end of output"; -} - -TEST_F(UtilityOutputTest, OutputFieldWithEmptyString) { - const std::string empty_value = ""; - const std::string label = "Empty Label"; - - capture->clear(); - output::output_field(label, empty_value); - const std::string output = capture->get_output(); - - EXPECT_TRUE(output.find("Empty Label : ") != std::string::npos) << "Expected label in output"; - EXPECT_TRUE(output.find("\n") != std::string::npos) << "Expected newline at the end of output"; -} - -TEST_F(UtilityOutputTest, OutputFieldWithSpecialCharactersInString) { - const std::string special_value = "Value\x{01}\x{02}\x{03}With\x{7F}Special"; - const std::string label = "Special Label"; - - capture->clear(); - output::output_field(label, special_value); - const std::string output = capture->get_output(); - - EXPECT_TRUE(output.find("Special Label : ") != std::string::npos) << "Expected label in output"; - EXPECT_TRUE(output.find("\n") != std::string::npos) << "Expected newline at the end of output"; -} - -TEST_F(UtilityOutputTest, OutputFieldLabelFormatting) { - const std::string test_value = "Value"; - const std::string label = "My Label"; - - capture->clear(); - output::output_field(label, test_value); - const std::string output = capture->get_output(); - - // Check that the format is "Label : Value\n" - EXPECT_TRUE(output.find("My Label : ") == 0) << "Expected output to start with 'My Label : '"; - EXPECT_TRUE(output.back() == '\n') << "Expected output to end with newline character"; -} - -// Test case for multiple consecutive calls -TEST_F(UtilityOutputTest, MultipleOutputFieldCalls) { - capture->clear(); - - output::output_field("Label1", "Value1"); - output::output_field("Label2", L"Value2"); - output::output_field("Label3", std::string("Value3")); - - const std::string output = capture->get_output(); - - EXPECT_TRUE(output.find("Label1 : ") != std::string::npos) << "Expected 'Label1' in output"; - EXPECT_TRUE(output.find("Label2 : ") != std::string::npos) << "Expected 'Label2' in output"; - EXPECT_TRUE(output.find("Label3 : ") != std::string::npos) << "Expected 'Label3' in output"; - - // Count newlines - should have 3 - const size_t newline_count = std::ranges::count(output, '\n'); - EXPECT_EQ(newline_count, 3); -} - -// Test cases for actual Unicode symbols and special characters -TEST_F(UtilityOutputTest, OutputFieldWithQuotationMarks) { - capture->clear(); - - // Test with various quotation marks - output::output_field("Single Quote", "Device 'Audio' Output"); - output::output_field("Double Quote", "Device \"Audio\" Output"); - output::output_field("Left Quote", "Device 'Audio' Output"); - output::output_field("Right Quote", "Device 'Audio' Output"); - output::output_field("Left Double Quote", "Device \u{201C}Audio\u{201D} Output"); - output::output_field("Right Double Quote", "Device \u{201C}Audio\u{201D} Output"); - - const std::string output = capture->get_output(); - - EXPECT_TRUE(output.find("Single Quote : ") != std::string::npos) << "Expected 'Single Quote' in output"; - EXPECT_TRUE(output.find("Double Quote : ") != std::string::npos) << "Expected 'Double Quote' in output"; - EXPECT_TRUE(output.find("Left Quote : ") != std::string::npos) << "Expected 'Left Quote' in output"; - EXPECT_TRUE(output.find("Right Quote : ") != std::string::npos) << "Expected 'Right Quote' in output"; - EXPECT_TRUE(output.find("Left Double Quote : ") != std::string::npos) << "Expected 'Left Double Quote' in output"; - EXPECT_TRUE(output.find("Right Double Quote : ") != std::string::npos) << "Expected 'Right Double Quote' in output"; -} - -TEST_F(UtilityOutputTest, OutputFieldWithTrademarkSymbols) { - capture->clear(); - - // Test with trademark and copyright symbols - output::output_field("Trademark", "Audio Device™"); - output::output_field("Registered", "Audio Device®"); - output::output_field("Copyright", "Audio Device©"); - output::output_field("Combined", "Realtek® Audio™"); - - const std::string output = capture->get_output(); - - EXPECT_TRUE(output.find("Trademark : ") != std::string::npos) << "Expected 'Trademark' in output"; - EXPECT_TRUE(output.find("Registered : ") != std::string::npos) << "Expected 'Registered' in output"; - EXPECT_TRUE(output.find("Copyright : ") != std::string::npos) << "Expected 'Copyright' in output"; - EXPECT_TRUE(output.find("Combined : ") != std::string::npos) << "Expected 'Combined' in output"; -} - -TEST_F(UtilityOutputTest, OutputFieldWithAccentedCharacters) { - capture->clear(); - - // Test with accented characters that might appear in device names - output::output_field("French Accents", "Haut-parleur à haute qualité"); - output::output_field("Spanish Accents", "Altavoz ñáéíóú"); - output::output_field("German Accents", "Lautsprecher äöü"); - output::output_field("Mixed Accents", "àáâãäåæçèéêë"); - - const std::string output = capture->get_output(); - - EXPECT_TRUE(output.find("French Accents : ") != std::string::npos) << "Expected 'French Accents' in output"; - EXPECT_TRUE(output.find("Spanish Accents : ") != std::string::npos) << "Expected 'Spanish Accents' in output"; - EXPECT_TRUE(output.find("German Accents : ") != std::string::npos) << "Expected 'German Accents' in output"; - EXPECT_TRUE(output.find("Mixed Accents : ") != std::string::npos) << "Expected 'Mixed Accents' in output"; -} - -TEST_F(UtilityOutputTest, OutputFieldWithSpecialSymbols) { - capture->clear(); - - // Test with various special symbols - output::output_field("Math Symbols", "Audio @ 44.1kHz ± 0.1%"); - output::output_field("Punctuation", "Audio Device #1 & #2"); - output::output_field("Programming", "Device $%^&*()"); - output::output_field("Mixed Symbols", "Audio™ @#$%^&*()"); - - const std::string output = capture->get_output(); - - EXPECT_TRUE(output.find("Math Symbols : ") != std::string::npos) << "Expected 'Math Symbols' in output"; - EXPECT_TRUE(output.find("Punctuation : ") != std::string::npos) << "Expected 'Punctuation' in output"; - EXPECT_TRUE(output.find("Programming : ") != std::string::npos) << "Expected 'Programming' in output"; - EXPECT_TRUE(output.find("Mixed Symbols : ") != std::string::npos) << "Expected 'Mixed Symbols' in output"; -} - -TEST_F(UtilityOutputTest, OutputFieldWithWideCharacterSymbols) { - capture->clear(); - - // Test with wide character symbols - const wchar_t *device_with_quotes = L"Device 'Audio' Output"; - const wchar_t *device_with_trademark = L"Realtek® Audio™"; - const wchar_t *device_with_accents = L"Haut-parleur àáâãäåæçèéêë"; - const wchar_t *device_with_symbols = L"Audio ñáéíóú & symbols @#$%^&*()"; - - output::output_field("Wide Quotes", device_with_quotes); - output::output_field("Wide Trademark", device_with_trademark); - output::output_field("Wide Accents", device_with_accents); - output::output_field("Wide Symbols", device_with_symbols); - - const std::string output = capture->get_output(); - - EXPECT_TRUE(output.find("Wide Quotes : ") != std::string::npos) << "Expected 'Wide Quotes' in output"; - EXPECT_TRUE(output.find("Wide Trademark : ") != std::string::npos) << "Expected 'Wide Trademark' in output"; - EXPECT_TRUE(output.find("Wide Accents : ") != std::string::npos) << "Expected 'Wide Accents' in output"; - EXPECT_TRUE(output.find("Wide Symbols : ") != std::string::npos) << "Expected 'Wide Symbols' in output"; -} - -TEST_F(UtilityOutputTest, OutputFieldWithRealAudioDeviceNames) { - capture->clear(); - - // Test with realistic audio device names that might contain special characters - output::output_field("Realtek Device", "Realtek® High Definition Audio"); - output::output_field("Creative Device", "Creative Sound Blaster™ X-Fi"); - output::output_field("Logitech Device", "Logitech G533 Gaming Headset"); - output::output_field("Bluetooth Device", "Sony WH-1000XM4 'Wireless' Headphones"); - output::output_field("USB Device", "USB Audio Device @ 48kHz"); - - const std::string output = capture->get_output(); - - EXPECT_TRUE(output.find("Realtek Device : ") != std::string::npos) << "Expected 'Realtek Device' in output"; - EXPECT_TRUE(output.find("Creative Device : ") != std::string::npos) << "Expected 'Creative Device' in output"; - EXPECT_TRUE(output.find("Logitech Device : ") != std::string::npos) << "Expected 'Logitech Device' in output"; - EXPECT_TRUE(output.find("Bluetooth Device : ") != std::string::npos) << "Expected 'Bluetooth Device' in output"; - EXPECT_TRUE(output.find("USB Device : ") != std::string::npos) << "Expected 'USB Device' in output"; -} - -TEST_F(UtilityOutputTest, OutputFieldWithNullAndSpecialCharacters) { - capture->clear(); - - // Test null wide string with special characters in label - const wchar_t *null_value = nullptr; - output::output_field("Device™ with 'quotes'", null_value); - output::output_field("Device àáâãäåæçèéêë", null_value); - output::output_field("Device @#$%^&*()", null_value); - - const std::string output = capture->get_output(); - - EXPECT_TRUE(output.find("Device™ with 'quotes' : ") != std::string::npos) << "Expected 'Device™ with quotes' in output"; - EXPECT_TRUE(output.find("Device àáâãäåæçèéêë : ") != std::string::npos) << "Expected 'Device àáâãäåæçèéêë' in output"; - EXPECT_TRUE(output.find("Device @#$%^&*() : ") != std::string::npos) << "Expected 'Device @#$%^&*()' in output"; - - // Should contain "Unknown" for null values - size_t unknown_count = 0; - size_t pos = 0; - while ((pos = output.find("Unknown", pos)) != std::string::npos) { - unknown_count++; - pos += 7; // length of "Unknown" - } - EXPECT_EQ(unknown_count, 3) << "Expected 'Unknown' to appear 3 times for null values"; -} - -TEST_F(UtilityOutputTest, OutputFieldWithEmptyAndSpecialCharacters) { - capture->clear(); - - // Test empty values with special character labels - output::output_field("Empty Device™", ""); - output::output_field("Empty 'Quotes'", ""); - output::output_field("Empty àáâãäåæçèéêë", ""); - - const std::string output = capture->get_output(); - - EXPECT_TRUE(output.find("Empty Device™ : ") != std::string::npos) << "Expected 'Empty Device™' in output"; - EXPECT_TRUE(output.find("Empty 'Quotes' : ") != std::string::npos) << "Expected 'Empty Quotes' in output"; - EXPECT_TRUE(output.find("Empty àáâãäåæçèéêë : ") != std::string::npos) << "Expected 'Empty àáâãäåæçèéêë' in output"; - - // Count newlines - should have 3 - const size_t newline_count = std::ranges::count(output, '\n'); - EXPECT_EQ(newline_count, 3) << "Expected 3 newlines for 3 output fields with empty values"; -} - -#else -// For non-Windows platforms, the output namespace doesn't exist -TEST(UtilityOutputTest, OutputNamespaceNotAvailableOnNonWindows) { - GTEST_SKIP() << "output namespace is Windows-specific"; -} -#endif diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 19338c757fd..037160e9f15 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -2,24 +2,38 @@ cmake_minimum_required(VERSION 3.20) project(sunshine_tools) -include_directories("${CMAKE_SOURCE_DIR}") +include_directories( + "${CMAKE_SOURCE_DIR}" + "${FFMPEG_INCLUDE_DIRS}" # this is included only for logging +) -add_executable(dxgi-info dxgi.cpp) +set(TOOL_SOURCES + "${CMAKE_SOURCE_DIR}/src/logging.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/utf_utils.cpp" +) + +add_executable(dxgi-info dxgi.cpp ${TOOL_SOURCES}) set_target_properties(dxgi-info PROPERTIES CXX_STANDARD 23) target_link_libraries(dxgi-info ${Boost_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} + ${FFMPEG_LIBRARIES} # this is included only for logging dxgi - ${PLATFORM_LIBRARIES}) + libdisplaydevice::display_device # this is included only for logging + ${PLATFORM_LIBRARIES} +) target_compile_options(dxgi-info PRIVATE ${SUNSHINE_COMPILE_OPTIONS}) -add_executable(audio-info audio.cpp) +add_executable(audio-info audio.cpp ${TOOL_SOURCES}) set_target_properties(audio-info PROPERTIES CXX_STANDARD 23) target_link_libraries(audio-info ${Boost_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} + ${FFMPEG_LIBRARIES} # this is included only for logging + libdisplaydevice::display_device # this is included only for logging ksuser - ${PLATFORM_LIBRARIES}) + ${PLATFORM_LIBRARIES} +) target_compile_options(audio-info PRIVATE ${SUNSHINE_COMPILE_OPTIONS}) add_executable(sunshinesvc sunshinesvc.cpp) diff --git a/tools/audio.cpp b/tools/audio.cpp index 450176b636b..3678a308771 100644 --- a/tools/audio.cpp +++ b/tools/audio.cpp @@ -12,7 +12,7 @@ #include // local includes -#include "src/platform/windows/tools/helper.h" +#include "src/platform/windows/utf_utils.h" #include "src/utility.h" DEFINE_PROPERTYKEY(PKEY_Device_DeviceDesc, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 2); // DEVPROP_TYPE_STRING @@ -211,13 +211,20 @@ namespace audio { } } + auto safe_wstring_output = [](const wchar_t *wstr) -> std::string { + if (!wstr) { + return "Unknown"; + } + return utf_utils::to_utf8(std::wstring(wstr)); + }; + std::cout << "===== Device =====" << std::endl; - output::output_field("Device ID ", wstring.get()); - output::output_field("Device name ", output::no_null(device_friendly_name.prop.pwszVal)); - output::output_field("Adapter name ", output::no_null(adapter_friendly_name.prop.pwszVal)); - output::output_field("Device description ", output::no_null(device_desc.prop.pwszVal)); - output::output_field("Device state ", device_state_string.c_str()); - output::output_field("Current format ", current_format); + std::cout << "Device ID : " << utf_utils::to_utf8(std::wstring(wstring.get())) << std::endl; + std::cout << "Device name : " << safe_wstring_output(device_friendly_name.prop.pwszVal) << std::endl; + std::cout << "Adapter name : " << safe_wstring_output(adapter_friendly_name.prop.pwszVal) << std::endl; + std::cout << "Device description : " << safe_wstring_output(device_desc.prop.pwszVal) << std::endl; + std::cout << "Device state : " << utf_utils::to_utf8(device_state_string) << std::endl; + std::cout << "Current format : " << current_format << std::endl; std::cout << std::endl; } } // namespace audio diff --git a/tools/dxgi.cpp b/tools/dxgi.cpp index 226601475ef..f1f63e1c118 100644 --- a/tools/dxgi.cpp +++ b/tools/dxgi.cpp @@ -3,7 +3,7 @@ * @brief Displays information about connected displays and GPUs */ #define WINVER 0x0A00 -#include "src/platform/windows/tools/helper.h" +#include "src/platform/windows/utf_utils.h" #include "src/utility.h" #include @@ -44,12 +44,12 @@ int main(int argc, char *argv[]) { adapter->GetDesc1(&adapter_desc); std::cout << "====== ADAPTER =====" << std::endl; - output::output_field("Device Name ", adapter_desc.Description); - output::output_field("Device Vendor ID ", "0x" + util::hex(adapter_desc.VendorId).to_string()); - output::output_field("Device Device ID ", "0x" + util::hex(adapter_desc.DeviceId).to_string()); - output::output_field("Device Video Mem ", std::format("{} MiB", adapter_desc.DedicatedVideoMemory / 1048576)); - output::output_field("Device Sys Mem ", std::format("{} MiB", adapter_desc.DedicatedSystemMemory / 1048576)); - output::output_field("Share Sys Mem ", std::format("{} MiB", adapter_desc.SharedSystemMemory / 1048576)); + std::cout << "Device Name : " << utf_utils::to_utf8(std::wstring(adapter_desc.Description)) << std::endl; + std::cout << "Device Vendor ID : " << "0x" << util::hex(adapter_desc.VendorId).to_string() << std::endl; + std::cout << "Device Device ID : " << "0x" << util::hex(adapter_desc.DeviceId).to_string() << std::endl; + std::cout << "Device Video Mem : " << std::format("{} MiB", adapter_desc.DedicatedVideoMemory / 1048576) << std::endl; + std::cout << "Device Sys Mem : " << std::format("{} MiB", adapter_desc.DedicatedSystemMemory / 1048576) << std::endl; + std::cout << "Share Sys Mem : " << std::format("{} MiB", adapter_desc.SharedSystemMemory / 1048576) << std::endl; dxgi::output_t::pointer output_p {}; bool has_outputs = false; @@ -69,9 +69,9 @@ int main(int argc, char *argv[]) { auto width = desc.DesktopCoordinates.right - desc.DesktopCoordinates.left; auto height = desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top; - output::output_field(" Output Name ", desc.DeviceName); - output::output_field(" AttachedToDesktop ", desc.AttachedToDesktop ? "yes" : "no"); - output::output_field(" Resolution ", std::format("{}x{}", width, height)); + std::cout << " Output Name : " << utf_utils::to_utf8(std::wstring(desc.DeviceName)) << std::endl; + std::cout << " AttachedToDesktop : " << (desc.AttachedToDesktop ? "yes" : "no") << std::endl; + std::cout << " Resolution : " << std::format("{}x{}", width, height) << std::endl; } std::cout << std::endl; } From c7f3b21a16b49e15b614e555a893ef968ef4b323 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 16 Jan 2026 20:06:52 -0500 Subject: [PATCH 3/5] Refactor environment block to use std::vector Replaces the fixed-size wchar_t array with a std::vector for constructing the environment block. This improves safety and flexibility by avoiding stack allocation for large environment blocks and using dynamic memory management. --- src/platform/windows/misc.cpp | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index bd4f4166da5..44557b25313 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -390,23 +390,26 @@ namespace platf { size += 1 /* L'\0' */; - wchar_t env_block[size]; + std::vector env_block(size); int offset = 0; for (const auto &entry : env) { auto name = entry.get_name(); auto value = entry.to_string(); // Construct the NAME=VAL\0 string - append_string_to_environment_block(env_block, offset, utf_utils::from_utf8(name)); - env_block[offset++] = L'='; - append_string_to_environment_block(env_block, offset, utf_utils::from_utf8(value)); - env_block[offset++] = L'\0'; + append_string_to_environment_block(env_block.data(), offset, utf_utils::from_utf8(name)); + env_block[offset] = L'='; + offset++; + append_string_to_environment_block(env_block.data(), offset, utf_utils::from_utf8(value)); + env_block[offset] = L'\0'; + offset++; } // Append a final null terminator - env_block[offset++] = L'\0'; + env_block[offset] = L'\0'; + offset++; - return std::wstring(env_block, offset); + return std::wstring(env_block.data(), offset); } LPPROC_THREAD_ATTRIBUTE_LIST allocate_proc_thread_attr_list(DWORD attribute_count) { From 0f35d4b6dc806541b4694368fb72b8fc46d7fb3c Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 16 Jan 2026 21:35:37 -0500 Subject: [PATCH 4/5] Make create_environment_block parameter const The create_environment_block function now takes a const reference to bp::environment, ensuring the environment is not modified and allowing the function to accept const arguments. --- src/platform/windows/misc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index 44557b25313..33fce6e1786 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -380,7 +380,7 @@ namespace platf { offset += wstr.length(); } - std::wstring create_environment_block(bp::environment &env) { + std::wstring create_environment_block(const bp::environment &env) { int size = 0; for (const auto &entry : env) { auto name = entry.get_name(); From 17e549fb920f7527846e27bb0de827c7e7927b8e Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 16 Jan 2026 21:35:50 -0500 Subject: [PATCH 5/5] Refactor variable scope in environment variable search Moves the declaration of the iterator inside the if statement for clarity and restricts its scope to where it is used in the case-insensitive environment variable search. --- src/platform/windows/misc.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index 33fce6e1786..18b68f7a69d 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -320,10 +320,10 @@ namespace platf { std::string env_val = env_tuple.substr(env_tuple.find('=') + 1); // Perform a case-insensitive search to see if this variable name already exists - auto itr = std::find_if(env.cbegin(), env.cend(), [&](const auto &e) { - return boost::iequals(e.get_name(), env_name); - }); - if (itr != env.cend()) { + if (auto itr = std::find_if(env.begin(), env.end(), [&](const auto &e) { + return boost::iequals(e.get_name(), env_name); + }); + itr != env.end()) { // Use this existing name if it is already present to ensure we merge properly env_name = itr->get_name(); }