mirror of
https://github.com/LizardByte/Sunshine.git
synced 2025-08-10 00:52:16 +00:00
fix(audio-info): crash when device name contains special characters
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#define WINVER 0x0A00
|
||||
|
||||
// platform includes
|
||||
#include <windows.h>
|
||||
#include <Windows.h>
|
||||
|
||||
// standard includes
|
||||
#include <cmath>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
#include <timeapi.h>
|
||||
#include <userenv.h>
|
||||
#include <winsock2.h>
|
||||
#include <windows.h>
|
||||
#include <Windows.h>
|
||||
#include <winuser.h>
|
||||
#include <wlanapi.h>
|
||||
#include <ws2tcpip.h>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include <string_view>
|
||||
|
||||
// platform includes
|
||||
#include <windows.h>
|
||||
#include <Windows.h>
|
||||
#include <winnt.h>
|
||||
|
||||
namespace platf {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// platform includes
|
||||
// disable clang-format header reordering
|
||||
// clang-format off
|
||||
#include <windows.h>
|
||||
#include <Windows.h>
|
||||
#include <aclapi.h>
|
||||
// clang-format on
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// winsock2.h must be included before windows.h
|
||||
// clang-format off
|
||||
#include <winsock2.h>
|
||||
#include <windows.h>
|
||||
#include <Windows.h>
|
||||
// clang-format on
|
||||
#include <windns.h>
|
||||
#include <winerror.h>
|
||||
|
||||
127
src/platform/windows/tools/helper.h
Normal file
127
src/platform/windows/tools/helper.h
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* @file src/platform/windows/tools/helper.h
|
||||
* @brief Helpers for tools.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
// standard includes
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
// lib includes
|
||||
#include <boost/locale.hpp>
|
||||
|
||||
// platform includes
|
||||
#include <Windows.h>
|
||||
|
||||
/**
|
||||
* @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<char>(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<char>(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<wchar_t>(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
|
||||
424
tests/unit/platform/windows/tools/test_helper.cpp
Normal file
424
tests/unit/platform/windows/tools/test_helper.cpp
Normal file
@@ -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 <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <fcntl.h>
|
||||
#include <io.h>
|
||||
#include <src/platform/windows/tools/helper.h>
|
||||
#include <Windows.h>
|
||||
#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<ConsoleCapture>();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
capture.reset();
|
||||
}
|
||||
|
||||
std::unique_ptr<ConsoleCapture> 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
|
||||
@@ -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})
|
||||
|
||||
@@ -6,17 +6,13 @@
|
||||
|
||||
// platform includes
|
||||
#include <audioclient.h>
|
||||
#include <codecvt>
|
||||
#include <iostream>
|
||||
#include <locale>
|
||||
#include <mmdeviceapi.h>
|
||||
#include <roapi.h>
|
||||
#include <synchapi.h>
|
||||
|
||||
// lib includes
|
||||
#include <boost/locale.hpp>
|
||||
|
||||
// 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<class T>
|
||||
void co_task_free(T *p) {
|
||||
CoTaskMemFree((LPVOID) p);
|
||||
CoTaskMemFree(static_cast<LPVOID>(p));
|
||||
}
|
||||
|
||||
using device_enum_t = util::safe_ptr<IMMDeviceEnumerator, Release<IMMDeviceEnumerator>>;
|
||||
@@ -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<PWAVEFORMATEXTENSIBLE>(static_cast<void *>(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<void **>(static_cast<void *>(&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<wchar_t>(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<void **>(static_cast<void *>(&device_enum))
|
||||
);
|
||||
|
||||
if (FAILED(status)) {
|
||||
|
||||
@@ -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 <d3dcommon.h>
|
||||
#include <dxgi.h>
|
||||
#include <format>
|
||||
#include <iostream>
|
||||
|
||||
using namespace std::literals;
|
||||
@@ -20,17 +22,14 @@ namespace dxgi {
|
||||
using factory1_t = util::safe_ptr<IDXGIFactory1, Release<IDXGIFactory1>>;
|
||||
using adapter_t = util::safe_ptr<IDXGIAdapter1, Release<IDXGIAdapter1>>;
|
||||
using output_t = util::safe_ptr<IDXGIOutput, Release<IDXGIOutput>>;
|
||||
|
||||
} // 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<void **>(static_cast<void *>(&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;
|
||||
|
||||
Reference in New Issue
Block a user