From 98ac5fc25044b5e33b30c3ef85f94be32bfd9912 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Mon, 20 Apr 2026 14:43:58 -0700 Subject: [PATCH 1/3] Add set_value_multistring_nothrow for REG_MULTI_SZ writes Adds set_value_multistring_nothrow() - the missing nothrow counterpart to the existing set_value_multistring(). Two overloads matching the existing pattern. Uses try/CATCH_RETURN() since get_multistring_from_wstrings allocates internally. Gated behind WIL_USE_STL. Fixes #479 --- include/wil/registry.h | 37 ++++++++++++++ tests/RegistryTests.cpp | 105 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) diff --git a/include/wil/registry.h b/include/wil/registry.h index 5aa89d017..96879737a 100644 --- a/include/wil/registry.h +++ b/include/wil/registry.h @@ -884,6 +884,43 @@ namespace reg return ::wil::reg::set_value_expanded_string_nothrow(key, nullptr, value_name, data); } +#if (WIL_USE_STL && defined(WIL_ENABLE_EXCEPTIONS)) || defined(WIL_DOXYGEN) + /** + * @brief Writes a REG_MULTI_SZ value from a std::vector + * @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 A std::vector to write to the specified registry value. + * Each string will be marshaled to a contiguous null-terminator-delimited multi-sz string + * @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, const ::std::vector<::std::wstring>& data) WI_NOEXCEPT + try + { + const auto multiStringWcharVector(reg_view_details::get_multistring_from_wstrings(::std::begin(data), ::std::end(data))); + const reg_view_details::reg_view_nothrow regview{key}; + return regview.set_value(subkey, value_name, multiStringWcharVector, REG_MULTI_SZ); + } + CATCH_RETURN(); + + /** + * @brief Writes a REG_MULTI_SZ value from a std::vector + * @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 A std::vector to write to the specified registry value. + * Each string will be marshaled to a contiguous null-terminator-delimited multi-sz string. + * @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, const ::std::vector<::std::wstring>& data) WI_NOEXCEPT + { + return ::wil::reg::set_value_multistring_nothrow(key, nullptr, value_name, data); + } +#endif // (WIL_USE_STL && defined(WIL_ENABLE_EXCEPTIONS)) + #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/tests/RegistryTests.cpp b/tests/RegistryTests.cpp index 2374d778e..3961a64c6 100644 --- a/tests/RegistryTests.cpp +++ b/tests/RegistryTests.cpp @@ -3028,6 +3028,111 @@ TEST_CASE("BasicRegistryTests::multi-strings", "[registry]") } #endif #endif + +#if WIL_USE_STL && defined(WIL_ENABLE_EXCEPTIONS) + 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) + { + REQUIRE_SUCCEEDED(wil::reg::set_value_multistring_nothrow(hkey.get(), stringValueName, value)); + 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"}; + REQUIRE_SUCCEEDED(wil::reg::set_value_multistring_nothrow(hkey.get(), nullptr, testValue)); + 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) + { + REQUIRE_SUCCEEDED(wil::reg::set_value_multistring_nothrow(HKEY_CURRENT_USER, testSubkey, stringValueName, value)); + 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"}; + REQUIRE_SUCCEEDED(wil::reg::set_value_multistring_nothrow(HKEY_CURRENT_USER, testSubkey, nullptr, testValue)); + 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, test_multistring_empty)); + 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, test_multistring_empty)); + 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, test_multistring_empty)); + 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, test_multistring_empty)); + 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 std::vector testValue{L"test"}; + auto hr = wil::reg::set_value_multistring_nothrow(readOnlyKey.get(), stringValueName, 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 std::vector testValue{L"alpha", L"bravo", L"charlie"}; + REQUIRE_SUCCEEDED(wil::reg::set_value_multistring_nothrow(hkey.get(), stringValueName, 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_) From 76f3cc519e4e06c119b3f9d0c340c1f311af9dbc Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Wed, 22 Apr 2026 13:56:12 -0700 Subject: [PATCH 2/3] Rewrite set_value_multistring_nothrow without internal exceptions Replace try/CATCH_RETURN() with a truly nothrow implementation that builds the multi-sz buffer using HeapAlloc + memcpy, matching the idiomatic wil pattern for nothrow functions. - Pre-calculate total buffer size with overflow checks - Guard against size_t-to-DWORD truncation before calling RegSetKeyValueW - Use HeapAlloc for the temporary buffer (no COM dependency needed) - Relax preprocessor guard from WIL_USE_STL+WIL_ENABLE_EXCEPTIONS to just WIL_USE_STL since exceptions are no longer used internally Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- include/wil/registry.h | 15 +++++----- include/wil/registry_helpers.h | 51 ++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/include/wil/registry.h b/include/wil/registry.h index 96879737a..1a9c5bf39 100644 --- a/include/wil/registry.h +++ b/include/wil/registry.h @@ -884,7 +884,7 @@ namespace reg return ::wil::reg::set_value_expanded_string_nothrow(key, nullptr, value_name, data); } -#if (WIL_USE_STL && defined(WIL_ENABLE_EXCEPTIONS)) || defined(WIL_DOXYGEN) +#if WIL_USE_STL || defined(WIL_DOXYGEN) /** * @brief Writes a REG_MULTI_SZ value from a std::vector * @param key An open or well-known registry key @@ -898,13 +898,14 @@ namespace reg */ inline HRESULT set_value_multistring_nothrow( HKEY key, _In_opt_ PCWSTR subkey, _In_opt_ PCWSTR value_name, const ::std::vector<::std::wstring>& data) WI_NOEXCEPT - try { - const auto multiStringWcharVector(reg_view_details::get_multistring_from_wstrings(::std::begin(data), ::std::end(data))); - const reg_view_details::reg_view_nothrow regview{key}; - return regview.set_value(subkey, value_name, multiStringWcharVector, REG_MULTI_SZ); + wchar_t* buffer = nullptr; + DWORD bufferSizeBytes = 0; + RETURN_IF_FAILED(reg_view_details::get_multistring_from_wstrings_nothrow(data, &buffer, &bufferSizeBytes)); + const auto hr = HRESULT_FROM_WIN32(::RegSetKeyValueW(key, subkey, value_name, REG_MULTI_SZ, buffer, bufferSizeBytes)); + ::HeapFree(::GetProcessHeap(), 0, buffer); + return hr; } - CATCH_RETURN(); /** * @brief Writes a REG_MULTI_SZ value from a std::vector @@ -919,7 +920,7 @@ namespace reg { return ::wil::reg::set_value_multistring_nothrow(key, nullptr, value_name, data); } -#endif // (WIL_USE_STL && defined(WIL_ENABLE_EXCEPTIONS)) +#endif // WIL_USE_STL #if defined(__WIL_OBJBASE_H_) || defined(WIL_DOXYGEN) /** diff --git a/include/wil/registry_helpers.h b/include/wil/registry_helpers.h index 964462609..9cf55c393 100644 --- a/include/wil/registry_helpers.h +++ b/include/wil/registry_helpers.h @@ -227,6 +227,57 @@ namespace reg } #endif +#if WIL_USE_STL + inline HRESULT get_multistring_from_wstrings_nothrow( + const ::std::vector<::std::wstring>& data, _Out_ wchar_t** buffer, _Out_ DWORD* bufferSizeBytes) WI_NOEXCEPT + { + *buffer = nullptr; + *bufferSizeBytes = 0; + + size_t total_size_bytes = sizeof(wchar_t); // final double-null terminator + for (const auto& str : data) + { + const size_t entry_size = (str.size() + 1) * sizeof(wchar_t); + if (total_size_bytes + entry_size < total_size_bytes || total_size_bytes + entry_size > MAXDWORD) + { + return E_INVALIDARG; + } + total_size_bytes += entry_size; + } + + if (data.empty()) + { + total_size_bytes = 2 * sizeof(wchar_t); + } + + auto result = static_cast(::HeapAlloc(::GetProcessHeap(), 0, total_size_bytes)); + if (!result) + { + return E_OUTOFMEMORY; + } + + size_t offset = 0; + if (data.empty()) + { + result[offset++] = L'\0'; + } + else + { + for (const auto& str : data) + { + memcpy(result + offset, str.c_str(), str.size() * sizeof(wchar_t)); + offset += str.size(); + result[offset++] = L'\0'; + } + } + result[offset] = L'\0'; + + *buffer = result; + *bufferSizeBytes = static_cast(total_size_bytes); + return S_OK; + } +#endif + #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 From aa93024c1ebd1b520bb877a77091a3898058e70c Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Fri, 1 May 2026 15:08:29 -0700 Subject: [PATCH 3/3] Address review feedback: use unique_process_heap_ptr, simplify memcpy, add WI_ASSERT - Use unique_process_heap_ptr instead of raw HeapAlloc/HeapFree - Simplify memcpy to copy null terminator with c_str() (size + 1) - Add WI_ASSERT to verify offset matches expected buffer size Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- include/wil/registry.h | 5 ++--- include/wil/registry_helpers.h | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/include/wil/registry.h b/include/wil/registry.h index 1a9c5bf39..7176ea6e2 100644 --- a/include/wil/registry.h +++ b/include/wil/registry.h @@ -902,9 +902,8 @@ namespace reg wchar_t* buffer = nullptr; DWORD bufferSizeBytes = 0; RETURN_IF_FAILED(reg_view_details::get_multistring_from_wstrings_nothrow(data, &buffer, &bufferSizeBytes)); - const auto hr = HRESULT_FROM_WIN32(::RegSetKeyValueW(key, subkey, value_name, REG_MULTI_SZ, buffer, bufferSizeBytes)); - ::HeapFree(::GetProcessHeap(), 0, buffer); - return hr; + ::wil::unique_process_heap_ptr bufferOwner(buffer); + return HRESULT_FROM_WIN32(::RegSetKeyValueW(key, subkey, value_name, REG_MULTI_SZ, buffer, bufferSizeBytes)); } /** diff --git a/include/wil/registry_helpers.h b/include/wil/registry_helpers.h index 9cf55c393..56ef6a49c 100644 --- a/include/wil/registry_helpers.h +++ b/include/wil/registry_helpers.h @@ -265,12 +265,12 @@ namespace reg { for (const auto& str : data) { - memcpy(result + offset, str.c_str(), str.size() * sizeof(wchar_t)); - offset += str.size(); - result[offset++] = L'\0'; + memcpy(result + offset, str.c_str(), (str.size() + 1) * sizeof(wchar_t)); + offset += str.size() + 1; } } result[offset] = L'\0'; + WI_ASSERT(offset == (total_size_bytes / sizeof(wchar_t)) - 1); *buffer = result; *bufferSizeBytes = static_cast(total_size_bytes);