diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake index 60ee905b773..4cabe8ab099 100644 --- a/cmake/compile_definitions/windows.cmake +++ b/cmake/compile_definitions/windows.cmake @@ -59,6 +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/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 964a6afb1f0..ff19dd215c8 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 @@ -697,7 +697,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, @@ -707,14 +707,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 { @@ -730,7 +730,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); } @@ -752,7 +752,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)); } } @@ -799,7 +799,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 2538a690bf4..904b174a428 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; @@ -464,8 +467,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) { @@ -579,7 +582,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 @@ -1051,7 +1054,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 @@ -1067,7 +1070,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 b537e6e6a27..4fe18793b64 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 a921c3cc144..698a066cac5 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; @@ -1687,65 +1688,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/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 fb123470afe..a36466178d2 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -32,7 +32,7 @@ #ifdef _WIN32 // from_utf8() string conversion function - #include "platform/windows/misc.h" + #include "platform/windows/utf_utils.h" // _SH constants for _wfsopen() #include @@ -176,7 +176,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/tools/CMakeLists.txt b/tools/CMakeLists.txt index 26cee550e3c..037160e9f15 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -2,23 +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 e14e90536d7..3678a308771 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/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 @@ -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,36 @@ 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; + 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; + 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 @@ -268,15 +272,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..f1f63e1c118 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/utf_utils.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; + 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; 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; + 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; } return 0;