From 29d3bb8e7717524caa2f4d63ac19f407fe0c8d56 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] fix(audio-info): crash when device name contains special characters --- cmake/compile_definitions/windows.cmake | 1 + src/platform/windows/input.cpp | 2 +- src/platform/windows/misc.cpp | 2 +- src/platform/windows/misc.h | 2 +- src/platform/windows/nvprefs/nvprefs_common.h | 2 +- src/platform/windows/publish.cpp | 2 +- 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 +- 11 files changed, 605 insertions(+), 57 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 60ee905b..07d647f3 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/input.cpp b/src/platform/windows/input.cpp index 50f8aab8..da552e27 100644 --- a/src/platform/windows/input.cpp +++ b/src/platform/windows/input.cpp @@ -5,7 +5,7 @@ #define WINVER 0x0A00 // platform includes -#include +#include // standard includes #include diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index c931b0ad..3128c312 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -24,7 +24,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/src/platform/windows/misc.h b/src/platform/windows/misc.h index 30d85376..ba58c877 100644 --- a/src/platform/windows/misc.h +++ b/src/platform/windows/misc.h @@ -9,7 +9,7 @@ #include // platform includes -#include +#include #include namespace platf { diff --git a/src/platform/windows/nvprefs/nvprefs_common.h b/src/platform/windows/nvprefs/nvprefs_common.h index baf4c0fb..7f605c78 100644 --- a/src/platform/windows/nvprefs/nvprefs_common.h +++ b/src/platform/windows/nvprefs/nvprefs_common.h @@ -7,7 +7,7 @@ // platform includes // disable clang-format header reordering // clang-format off -#include +#include #include // clang-format on diff --git a/src/platform/windows/publish.cpp b/src/platform/windows/publish.cpp index 4c3c6f24..eb8dadc2 100644 --- a/src/platform/windows/publish.cpp +++ b/src/platform/windows/publish.cpp @@ -6,7 +6,7 @@ // winsock2.h must be included before windows.h // clang-format off #include -#include +#include // clang-format on #include #include diff --git a/src/platform/windows/tools/helper.h b/src/platform/windows/tools/helper.h new file mode 100644 index 00000000..def2a578 --- /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 00000000..5401a96f --- /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 26cee550..19338c75 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 c5ca506f..20db1e1f 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 5ac8cac8..22660147 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;