diff --git a/include/wil/registry.h b/include/wil/registry.h index 94728a22..95afb756 100644 --- a/include/wil/registry.h +++ b/include/wil/registry.h @@ -970,6 +970,42 @@ namespace reg return ::wil::reg::set_value_expanded_string_nothrow(key, nullptr, value_name, data); } + /** + * @brief Writes a REG_MULTI_SZ value from an array of null-terminated strings + * @param key An open or well-known registry key + * @param subkey The name of the subkey to append to `key`. + * If `nullptr`, then `key` is used without modification. + * @param value_name The name of the registry value whose data is to be updated. + * Can be nullptr to write to the unnamed default registry value. + * @param data An array of `count` null-terminated strings to write to the specified registry value. + * Each string is marshaled into a contiguous null-terminator-delimited multi-sz string. + * @param count The number of strings in `data` + * @return HRESULT error code indicating success or failure (does not throw C++ exceptions) + */ + inline HRESULT set_value_multistring_nothrow( + HKEY key, _In_opt_ PCWSTR subkey, _In_opt_ PCWSTR value_name, _In_reads_(count) const PCWSTR* data, size_t count) WI_NOEXCEPT + { + ::wil::unique_process_heap_ptr buffer; + DWORD bufferSizeBytes = 0; + RETURN_IF_FAILED(reg_view_details::get_multistring_from_strings_nothrow(data, count, buffer, &bufferSizeBytes)); + return HRESULT_FROM_WIN32(::RegSetKeyValueW(key, subkey, value_name, REG_MULTI_SZ, buffer.get(), bufferSizeBytes)); + } + + /** + * @brief Writes a REG_MULTI_SZ value from an array of null-terminated strings + * @param key An open or well-known registry key + * @param value_name The name of the registry value whose data is to be updated. + * Can be nullptr to write to the unnamed default registry value. + * @param data An array of `count` null-terminated strings to write to the specified registry value. + * Each string is marshaled into a contiguous null-terminator-delimited multi-sz string. + * @param count The number of strings in `data` + * @return HRESULT error code indicating success or failure (does not throw C++ exceptions) + */ + inline HRESULT set_value_multistring_nothrow(HKEY key, _In_opt_ PCWSTR value_name, _In_reads_(count) const PCWSTR* data, size_t count) WI_NOEXCEPT + { + return ::wil::reg::set_value_multistring_nothrow(key, nullptr, value_name, data, count); + } + #if defined(__WIL_OBJBASE_H_) || defined(WIL_DOXYGEN) /** * @brief Writes raw bytes into a registry value under a specified key of the specified type diff --git a/include/wil/registry_helpers.h b/include/wil/registry_helpers.h index bc3c6ddb..1c76dfbf 100644 --- a/include/wil/registry_helpers.h +++ b/include/wil/registry_helpers.h @@ -237,6 +237,70 @@ namespace reg } #endif + /** + * @brief A nothrow translation function marshaling an array of null-terminated strings into a contiguous + * null-terminator-delimited REG_MULTI_SZ buffer allocated on the process heap. + * @param data An array of `count` null-terminated strings to marshal into a multi-sz buffer + * @param count The number of strings in `data` + * @param buffer Receives ownership of the process-heap-allocated wchar_t buffer on success + * @param bufferSizeBytes Receives the size, in bytes, of the marshaled buffer (including terminators) + * @return HRESULT error code indicating success or failure (does not throw C++ exceptions) + */ + inline HRESULT get_multistring_from_strings_nothrow( + _In_reads_(count) const PCWSTR* data, size_t count, ::wil::unique_process_heap_ptr& buffer, _Out_ DWORD* bufferSizeBytes) WI_NOEXCEPT + { + buffer.reset(); + *bufferSizeBytes = 0; + + size_t total_size_chars = 1; // final terminator + for (size_t i = 0; i < count; ++i) + { + const size_t entry_chars = ::wcslen(data[i]) + 1; + if (total_size_chars + entry_chars < total_size_chars) + { + return E_INVALIDARG; // integer overflow + } + total_size_chars += entry_chars; + } + + if (count == 0) + { + // An empty multi-string still requires a leading null plus the final terminator. + total_size_chars = 2; + } + + if (total_size_chars > (MAXDWORD / sizeof(wchar_t))) + { + return E_INVALIDARG; + } + const size_t total_size_bytes = total_size_chars * sizeof(wchar_t); + + ::wil::unique_process_heap_ptr result{static_cast(::HeapAlloc(::GetProcessHeap(), 0, total_size_bytes))}; + RETURN_IF_NULL_ALLOC(result.get()); + + size_t offset = 0; + for (size_t i = 0; i < count; ++i) + { + // Each string is null-terminated and it is valid to read that null character, so copy length + 1 + // characters to include the terminator in a single memcpy. + const size_t entry_chars = ::wcslen(data[i]) + 1; + memcpy(result.get() + offset, data[i], entry_chars * sizeof(wchar_t)); + offset += entry_chars; + } + + if (count == 0) + { + result.get()[offset++] = L'\0'; // leading null for an empty multi-string + } + + result.get()[offset++] = L'\0'; // final terminator + WI_ASSERT(offset == total_size_chars); + + buffer = wistd::move(result); + *bufferSizeBytes = static_cast(total_size_bytes); + return S_OK; + } + #if defined(__WIL_OBJBASE_H_) template void get_multistring_bytearray_from_strings_nothrow(const PCWSTR data[C], ::wil::unique_cotaskmem_array_ptr& multistring) WI_NOEXCEPT diff --git a/tests/RegistryTests.cpp b/tests/RegistryTests.cpp index a075d2e0..56629204 100644 --- a/tests/RegistryTests.cpp +++ b/tests/RegistryTests.cpp @@ -3181,6 +3181,127 @@ TEST_CASE("BasicRegistryTests::multi-strings", "[registry]") } #endif #endif + +#if WIL_USE_STL && defined(WIL_ENABLE_EXCEPTIONS) + const auto to_ptrs = [](const std::vector& strings) { + std::vector ptrs; + ptrs.reserve(strings.size()); + for (const auto& s : strings) + { + ptrs.push_back(s.c_str()); + } + return ptrs; + }; + + SECTION("set_value_multistring_nothrow/get_value_multistring_nothrow: with open key") + { + wil::unique_hkey hkey; + REQUIRE_SUCCEEDED(wil::reg::create_unique_key_nothrow(HKEY_CURRENT_USER, testSubkey, hkey, wil::reg::key_access::readwrite)); + + for (const auto& value : multiStringTestVector) + { + const auto ptrs = to_ptrs(value); + REQUIRE_SUCCEEDED(wil::reg::set_value_multistring_nothrow(hkey.get(), stringValueName, ptrs.data(), ptrs.size())); + auto result = wil::reg::get_value_multistring(hkey.get(), stringValueName); + // set_value_multistring_nothrow should produce the same result as set_value_multistring + wil::reg::set_value_multistring(hkey.get(), multiStringValueName, value); + auto expected = wil::reg::get_value_multistring(hkey.get(), multiStringValueName); + REQUIRE(result == expected); + } + + // and verify default value name + const std::vector testValue{L"hello", L"world"}; + const auto testPtrs = to_ptrs(testValue); + REQUIRE_SUCCEEDED(wil::reg::set_value_multistring_nothrow(hkey.get(), nullptr, testPtrs.data(), testPtrs.size())); + auto result = wil::reg::get_value_multistring(hkey.get(), nullptr); + REQUIRE(result == testValue); + } + + SECTION("set_value_multistring_nothrow/get_value_multistring_nothrow: with string key") + { + for (const auto& value : multiStringTestVector) + { + const auto ptrs = to_ptrs(value); + REQUIRE_SUCCEEDED( + wil::reg::set_value_multistring_nothrow(HKEY_CURRENT_USER, testSubkey, stringValueName, ptrs.data(), ptrs.size())); + auto result = wil::reg::get_value_multistring(HKEY_CURRENT_USER, testSubkey, stringValueName); + // set_value_multistring_nothrow should produce the same result as set_value_multistring + wil::reg::set_value_multistring(HKEY_CURRENT_USER, testSubkey, multiStringValueName, value); + auto expected = wil::reg::get_value_multistring(HKEY_CURRENT_USER, testSubkey, multiStringValueName); + REQUIRE(result == expected); + } + + // and verify default value name + const std::vector testValue{L"hello", L"world"}; + const auto testPtrs = to_ptrs(testValue); + REQUIRE_SUCCEEDED( + wil::reg::set_value_multistring_nothrow(HKEY_CURRENT_USER, testSubkey, nullptr, testPtrs.data(), testPtrs.size())); + auto result = wil::reg::get_value_multistring(HKEY_CURRENT_USER, testSubkey, nullptr); + REQUIRE(result == testValue); + } + + SECTION("set_value_multistring_nothrow: empty array with open key") + { + wil::unique_hkey hkey; + REQUIRE_SUCCEEDED(wil::reg::create_unique_key_nothrow(HKEY_CURRENT_USER, testSubkey, hkey, wil::reg::key_access::readwrite)); + + // When passed an empty array, set_value_multistring_nothrow writes 2 null-terminators + // (i.e. a single empty string), matching the behavior of set_value_multistring + const std::vector arrayOfOne{L""}; + REQUIRE_SUCCEEDED(wil::reg::set_value_multistring_nothrow(hkey.get(), stringValueName, nullptr, 0)); + auto result = wil::reg::get_value_multistring(hkey.get(), stringValueName); + REQUIRE(result == arrayOfOne); + + // and verify default value name + REQUIRE_SUCCEEDED(wil::reg::set_value_multistring_nothrow(hkey.get(), nullptr, nullptr, 0)); + result = wil::reg::get_value_multistring(hkey.get(), nullptr); + REQUIRE(result == arrayOfOne); + } + + SECTION("set_value_multistring_nothrow: empty array with string key") + { + const std::vector arrayOfOne{L""}; + REQUIRE_SUCCEEDED(wil::reg::set_value_multistring_nothrow(HKEY_CURRENT_USER, testSubkey, stringValueName, nullptr, 0)); + auto result = wil::reg::get_value_multistring(HKEY_CURRENT_USER, testSubkey, stringValueName); + REQUIRE(result == arrayOfOne); + + // and verify default value name + REQUIRE_SUCCEEDED(wil::reg::set_value_multistring_nothrow(HKEY_CURRENT_USER, testSubkey, nullptr, nullptr, 0)); + result = wil::reg::get_value_multistring(HKEY_CURRENT_USER, testSubkey, nullptr); + REQUIRE(result == arrayOfOne); + } + + SECTION("set_value_multistring_nothrow: fails with E_ACCESSDENIED on read-only key") + { + wil::unique_hkey hkey; + REQUIRE_SUCCEEDED(wil::reg::create_unique_key_nothrow(HKEY_CURRENT_USER, testSubkey, hkey, wil::reg::key_access::readwrite)); + + wil::unique_hkey readOnlyKey; + REQUIRE_SUCCEEDED(wil::reg::open_unique_key_nothrow(HKEY_CURRENT_USER, testSubkey, readOnlyKey, wil::reg::key_access::read)); + + const PCWSTR testValue[]{L"test"}; + auto hr = wil::reg::set_value_multistring_nothrow(readOnlyKey.get(), stringValueName, testValue, ARRAYSIZE(testValue)); + REQUIRE(hr == E_ACCESSDENIED); + } + + SECTION("set_value_multistring_nothrow: round-trip with nothrow get via cotaskmem") + { + wil::unique_hkey hkey; + REQUIRE_SUCCEEDED(wil::reg::create_unique_key_nothrow(HKEY_CURRENT_USER, testSubkey, hkey, wil::reg::key_access::readwrite)); + + const PCWSTR testValue[]{L"alpha", L"bravo", L"charlie"}; + REQUIRE_SUCCEEDED(wil::reg::set_value_multistring_nothrow(hkey.get(), stringValueName, testValue, ARRAYSIZE(testValue))); + +#if defined(__WIL_OBJBASE_H_) + wil::unique_cotaskmem_array_ptr result{}; + REQUIRE_SUCCEEDED(wil::reg::get_value_multistring_nothrow(hkey.get(), stringValueName, result)); + REQUIRE(result.size() == 3); + REQUIRE(std::wstring_view(result[0]) == L"alpha"); + REQUIRE(std::wstring_view(result[1]) == L"bravo"); + REQUIRE(std::wstring_view(result[2]) == L"charlie"); +#endif // defined(__WIL_OBJBASE_H_) + } +#endif // WIL_USE_STL && defined(WIL_ENABLE_EXCEPTIONS) } #if defined(__WIL_OBJBASE_H_)