Add AnnounceUpgrade test to the module boinccas module tests.#6635
Add AnnounceUpgrade test to the module boinccas module tests.#6635
Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR adds unit tests for the BOINC Custom Action (boinccas) installer component, specifically testing the CAAnnounceUpgrade function. The tests use a helper library to load the DLL dynamically and mock MSI database operations.
- Adds unit test infrastructure for testing boinccas installer custom actions
- Integrates Windows Installer Library (WIL) for RAII resource management
- Configures conditional compilation to enable boinccas tests in CI
Reviewed Changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| win_build/unittests.vcxproj | Adds boinccas test files, MSI-related library dependencies, and conditional preprocessor definition |
| win_build/installer.vcxproj | Refactors dependency organization by consolidating common dependencies |
| win_build/boinc.sln | Adds installer project as dependency for unittests and reorders dependencies |
| tests/unit-tests/boinccas/test_boinccas_CAAnnounceUpgrade.cpp | Implements unit test for AnnounceUpgrade custom action |
| tests/unit-tests/boinccas/boinccas_helper.h | Declares helper functions and classes for MSI and registry operations |
| tests/unit-tests/boinccas/boinccas_helper.cpp | Implements MSI database and registry manipulation helpers |
| 3rdParty/vcpkg_ports/configs/msbuild/vcpkg.json | Adds WIL dependency for Windows resource management |
| .github/workflows/windows.yml | Enables BOINCCAS_TEST preprocessor flag in CI builds |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| void MsiHelper::createTable(const std::string_view& sql_create) { | ||
| MSIHANDLE hView; | ||
| auto result = MsiDatabaseOpenView(hMsi, sql_create.data(), &hView); |
There was a problem hiding this comment.
The MSIHANDLE hView is not being closed on all error paths. If MsiDatabaseOpenView succeeds but MsiViewExecute fails (lines 52-56), or if MsiViewExecute succeeds but MsiViewClose fails (lines 57-61), the handle will leak. Wrap hView with a RAII wrapper or use a unique_ptr with a custom deleter, or ensure MsiCloseHandle(hView) is called before throwing exceptions.
| MSIHANDLE hView; | ||
| auto result = MsiDatabaseOpenView(hMsi, sql_insert.data(), &hView); |
There was a problem hiding this comment.
The MSIHANDLE hView is not being closed on error paths in the loop (lines 82-108). If any operation within the loop throws an exception, hView will leak. Wrap hView with a RAII wrapper or ensure it's closed in all error paths.
| std::to_string(result)); | ||
| } | ||
| for (const auto& record : properties) { | ||
| const auto hRecord = MsiCreateRecord(static_cast<UINT>(2)); |
There was a problem hiding this comment.
[nitpick] The explicit cast static_cast<UINT>(2) is unnecessary since 2 can be directly passed as a UINT literal (2u) or the literal 2 will be implicitly converted. Consider using MsiCreateRecord(2) for cleaner code.
| const auto hRecord = MsiCreateRecord(static_cast<UINT>(2)); | |
| const auto hRecord = MsiCreateRecord(2); |
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| RegDeleteKey(HKEY_LOCAL_MACHINE, registryKey); | ||
| RegCloseKey(hKey); |
There was a problem hiding this comment.
The RegDeleteKey function is being called while a handle to the key is still open. The key handle should be closed before attempting to delete the key. Additionally, RegDeleteKey only deletes empty keys; use RegDeleteTree to delete a key with subkeys, or ensure the key is empty before deletion.
| RegDeleteKey(HKEY_LOCAL_MACHINE, registryKey); | |
| RegCloseKey(hKey); | |
| RegCloseKey(hKey); | |
| RegDeleteTree(HKEY_LOCAL_MACHINE, registryKey); |
clientsetup/win/boinccas.cpp
Outdated
| NULL | ||
| ); | ||
| if (lReturnValue != ERROR_SUCCESS) return ERROR_INSTALL_FAILURE; | ||
| if (lReturnValue != ERROR_SUCCESS) return ERROR_INSTALL_FAILURE + 3; |
There was a problem hiding this comment.
Using magic numbers (3, 4) added to ERROR_INSTALL_FAILURE reduces code readability. Consider defining named constants like ERROR_REGISTRY_CREATE_FAILED = ERROR_INSTALL_FAILURE + 3 to make the error codes self-documenting.
clientsetup/win/boinccas.cpp
Outdated
|
|
||
| RegCloseKey(hkSetupHive); | ||
| if (lReturnValue != ERROR_SUCCESS) return ERROR_INSTALL_FAILURE; | ||
| if (lReturnValue != ERROR_SUCCESS) return ERROR_INSTALL_FAILURE + 4; |
There was a problem hiding this comment.
Using magic numbers (3, 4) added to ERROR_INSTALL_FAILURE reduces code readability. Consider defining named constants like ERROR_REGISTRY_SET_FAILED = ERROR_INSTALL_FAILURE + 4 to make the error codes self-documenting.
clientsetup/win/boinccas.cpp
Outdated
| strMessage.c_str() | ||
| ); | ||
| return ERROR_INSTALL_FAILURE; | ||
| return ERROR_INSTALL_FAILURE + 1; |
There was a problem hiding this comment.
Using magic numbers (1, 2) added to ERROR_INSTALL_FAILURE reduces code readability. Consider defining named constants like ERROR_PROPERTY_GET_FAILED = ERROR_INSTALL_FAILURE + 1 to make the error codes self-documenting.
clientsetup/win/boinccas.cpp
Outdated
| ); | ||
| if ( lpszBuffer ) free( lpszBuffer ); | ||
| return ERROR_INSTALL_FAILURE; | ||
| return ERROR_INSTALL_FAILURE + 2; |
There was a problem hiding this comment.
Using magic numbers (1, 2) added to ERROR_INSTALL_FAILURE reduces code readability. Consider defining named constants like ERROR_PROPERTY_BUFFER_FAILED = ERROR_INSTALL_FAILURE + 2 to make the error codes self-documenting.
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| constexpr auto registryKey = | ||
| "SOFTWARE\\Space Sciences Laboratory, U.C. Berkeley\\BOINC Setup"; |
There was a problem hiding this comment.
The registry key string literal is split across two lines, which may cause compilation issues. Consider placing the entire string on line 124 or using proper line continuation syntax.
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
tests/unit-tests/boinccas/boinccas_helper.cpp:1
- The
createPropertiesTable()method is called ininit()at line 40, butcreatePropertiesTableis declared as a public method in the header. Sinceinit()is now being used and creates the Properties table during initialization, the test at line 55 of test_boinccas_CAAnnounceUpgrade.cpp should not callcreatePropertiesTable()again as it will fail trying to create an already-existing table.
// This file is part of BOINC.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (hMsi != 0) { | ||
| MsiCloseHandle(hMsi); | ||
| } |
There was a problem hiding this comment.
This MSI handle cleanup at line 55-57 duplicates the cleanup logic already present in the destructor (lines 35-37). The handle will be closed again in the destructor after this test completes, resulting in a double-close. Remove this redundant cleanup block or set hMsi to 0 after closing it.
| const std::vector<std::pair<std::string, std::string>>& properties); | ||
| std::string getMsiHandle() const { |
There was a problem hiding this comment.
Missing include directive for <vector>. The method signature uses std::vector but the required header is not included.
| } | ||
| result = MsiSummaryInfoSetProperty(hSummaryInfo, 9, VT_LPSTR, 0, nullptr, | ||
| "{2C4296B7-9E88-4CD8-9FC6-26CE7B053ED1}"); | ||
| if (result != ERROR_SUCCESS) { |
There was a problem hiding this comment.
The return value of RegDeleteKey is not checked. If the key deletion fails, the function silently continues. Consider checking the result for proper error handling or logging.
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (result != ERROR_SUCCESS) { | ||
| throw std::runtime_error("Error closing view: " + | ||
| std::to_string(result)); | ||
| } |
There was a problem hiding this comment.
Resource leak: hView handle is not closed with MsiCloseHandle() after MsiViewClose(). According to MSI documentation, both MsiViewClose() and MsiCloseHandle() must be called on view handles. Add MsiCloseHandle(hView) after line 78.
| } | |
| } | |
| MsiCloseHandle(hView); |
| MSIHANDLE hView; | ||
| auto result = MsiDatabaseOpenView(hMsi, sql_insert.data(), &hView); | ||
| if (result != ERROR_SUCCESS) { | ||
| throw std::runtime_error("Error creating view: " + | ||
| std::to_string(result)); | ||
| } |
There was a problem hiding this comment.
Resource leak: hView handle is not closed with MsiCloseHandle() if an exception is thrown in the loop (lines 104-130) or after MsiViewClose() succeeds. Add MsiCloseHandle(hView) after line 136, similar to how hRecord is handled with MsiCloseHandle at line 125.
| EXPECT_NE(expectedVersion, getRegistryValue("UpgradingTo")); | ||
|
|
||
| if (hMsi != 0) { | ||
| MsiCloseHandle(hMsi); |
There was a problem hiding this comment.
The hMsi handle should be set to 0 after closing to prevent double-close in the destructor. Add 'hMsi = 0;' after line 56.
| MsiCloseHandle(hMsi); | |
| MsiCloseHandle(hMsi); | |
| hMsi = 0; |
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| std::to_string(result)); | ||
| } | ||
| result = MsiViewExecute(hView, 0); | ||
| if (result != ERROR_SUCCESS) { |
There was a problem hiding this comment.
Resource leak: hView is not closed if MsiViewExecute fails. The handle created by MsiDatabaseOpenView at line 64 should be closed with MsiCloseHandle(hView) before throwing the exception at line 71.
| if (result != ERROR_SUCCESS) { | |
| if (result != ERROR_SUCCESS) { | |
| MsiCloseHandle(hView); |
| MSIHANDLE hView; | ||
| auto result = MsiDatabaseOpenView(hMsi, sql_insert.data(), &hView); | ||
| if (result != ERROR_SUCCESS) { | ||
| throw std::runtime_error("Error creating view: " + | ||
| std::to_string(result)); | ||
| } | ||
| for (const auto& record : properties) { | ||
| const auto hRecord = MsiCreateRecord(2); | ||
| if (hRecord == 0) { | ||
| throw std::runtime_error("Failed to create record"); | ||
| } | ||
| result = MsiRecordSetString(hRecord, 1, record.first.c_str()); | ||
| if (result != ERROR_SUCCESS) { | ||
| throw std::runtime_error("Failed to set record, errorcode: " + | ||
| std::to_string(result)); | ||
| } | ||
| result = MsiRecordSetString(hRecord, 2, record.second.c_str()); | ||
| if (result != ERROR_SUCCESS) { | ||
| throw std::runtime_error("Failed to set record, errorcode: " + | ||
| std::to_string(result)); | ||
| } |
There was a problem hiding this comment.
Resource leak: hView is not closed if an exception is thrown within the loop (lines 107, 112, 117). Consider using RAII wrappers or adding proper cleanup in exception paths before throwing.
| if (result != ERROR_SUCCESS) { | ||
| throw std::runtime_error("Failed to set record, errorcode: " + | ||
| std::to_string(result)); | ||
| } | ||
| result = MsiRecordSetString(hRecord, 2, record.second.c_str()); | ||
| if (result != ERROR_SUCCESS) { |
There was a problem hiding this comment.
Resource leak: hRecord is not closed if MsiRecordSetString fails at line 110. The handle created at line 105 should be closed with MsiCloseHandle(hRecord) before throwing the exception at line 112.
| if (result != ERROR_SUCCESS) { | |
| throw std::runtime_error("Failed to set record, errorcode: " + | |
| std::to_string(result)); | |
| } | |
| result = MsiRecordSetString(hRecord, 2, record.second.c_str()); | |
| if (result != ERROR_SUCCESS) { | |
| if (result != ERROR_SUCCESS) { | |
| MsiCloseHandle(hRecord); | |
| throw std::runtime_error("Failed to set record, errorcode: " + | |
| std::to_string(result)); | |
| } | |
| result = MsiRecordSetString(hRecord, 2, record.second.c_str()); | |
| if (result != ERROR_SUCCESS) { | |
| MsiCloseHandle(hRecord); |
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
| } | ||
| AnnounceUpgradeFn hFunc = nullptr; | ||
| MSIHANDLE hMsi = 0; |
There was a problem hiding this comment.
The msiHelper member variable is instantiated before hDll and hFunc are initialized in the constructor. Since MsiHelper creates an MSI database during construction and the test fixture's constructor then calls load_function_from_boinccas which may require the DLL to be present, this ordering could lead to issues. Consider moving msiHelper instantiation to after the DLL loading, or use a smart pointer to delay its construction.
| HKEY hKey = nullptr; | ||
| const auto openResult = RegOpenKeyEx(HKEY_LOCAL_MACHINE, registryKey, 0, | ||
| KEY_WRITE, &hKey); | ||
| if (openResult != ERROR_SUCCESS) { | ||
| return; | ||
| } | ||
| RegDeleteKey(HKEY_LOCAL_MACHINE, registryKey); | ||
| RegCloseKey(hKey); |
There was a problem hiding this comment.
The order of operations is incorrect. RegDeleteKey is called while hKey is still open, and then RegCloseKey is called on a potentially invalid handle. The key should be closed before attempting to delete it. Additionally, RegDeleteKey should use hKey as the first parameter, not HKEY_LOCAL_MACHINE, since the key is already opened.
| HKEY hKey = nullptr; | |
| const auto openResult = RegOpenKeyEx(HKEY_LOCAL_MACHINE, registryKey, 0, | |
| KEY_WRITE, &hKey); | |
| if (openResult != ERROR_SUCCESS) { | |
| return; | |
| } | |
| RegDeleteKey(HKEY_LOCAL_MACHINE, registryKey); | |
| RegCloseKey(hKey); | |
| // To delete a registry key, open its parent and call RegDeleteKey with the subkey name. | |
| constexpr auto parentKeyPath = "SOFTWARE\\Space Sciences Laboratory, U.C. Berkeley"; | |
| constexpr auto subKeyName = "BOINC Setup"; | |
| HKEY hParentKey = nullptr; | |
| const auto openResult = RegOpenKeyEx(HKEY_LOCAL_MACHINE, parentKeyPath, 0, | |
| KEY_WRITE, &hParentKey); | |
| if (openResult != ERROR_SUCCESS) { | |
| return; | |
| } | |
| RegCloseKey(hParentKey); // Close before deleting | |
| // Reopen parent for deletion (RegDeleteKey requires open parent) | |
| if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, parentKeyPath, 0, KEY_WRITE, &hParentKey) == ERROR_SUCCESS) { | |
| RegDeleteKey(hParentKey, subKeyName); | |
| RegCloseKey(hParentKey); | |
| } |
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
tests/unit-tests/boinccas/test_boinccas_CAAnnounceUpgrade.cpp:1
- The preprocessor directive should be
#ifdef BOINCCAS_TESTinstead of#ifndef BOINCCAS_TEST. With#ifndef, tests are excluded when BOINCCAS_TEST is defined, which is opposite to the intended behavior. The build sets-p:BOINCCAS_TEST=trueto enable these tests.
// This file is part of BOINC.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| std::filesystem::remove(std::filesystem::current_path() / msiName); | ||
| } | ||
| catch (const std::exception& ex) { | ||
| throw std::runtime_error( "Failed to remove existing MSI file: " + |
There was a problem hiding this comment.
Extra space after opening parenthesis. Should be std::runtime_error( without the space.
| throw std::runtime_error( "Failed to remove existing MSI file: " + | |
| throw std::runtime_error("Failed to remove existing MSI file: " + |
| try { | ||
| std::filesystem::remove(std::filesystem::current_path() / msiName); | ||
| } | ||
| catch (const std::exception& ex) { | ||
| throw std::runtime_error( "Failed to remove existing MSI file: " + | ||
| std::string(ex.what())); | ||
| } |
There was a problem hiding this comment.
Rethrowing exceptions in cleanup code can mask the original issue. The cleanup() method is called from the destructor (line 30), where exceptions should not be thrown. Consider using std::filesystem::remove with the std::error_code overload instead of the throwing version, or catch and log the error without rethrowing.
| try { | |
| std::filesystem::remove(std::filesystem::current_path() / msiName); | |
| } | |
| catch (const std::exception& ex) { | |
| throw std::runtime_error( "Failed to remove existing MSI file: " + | |
| std::string(ex.what())); | |
| } | |
| std::error_code ec; | |
| std::filesystem::remove(std::filesystem::current_path() / msiName, ec); | |
| if (ec) { | |
| // Optionally log the error, but do not throw | |
| OutputDebugStringA(("Failed to remove existing MSI file: " + ec.message()).c_str()); | |
| } |
clientsetup/win/CAAnnounceUpgrade.h
Outdated
| CAAnnounceUpgrade(MSIHANDLE hMSIHandle); | ||
| ~CAAnnounceUpgrade(); | ||
| virtual UINT OnExecution(); | ||
| UINT OnExecution() override final; |
There was a problem hiding this comment.
[nitpick] Using both override and final is redundant for documentation purposes. While technically correct, final implies override, so using both is unnecessary. Consider using only final or keeping just override if the method might be overridden in derived classes (though that seems unlikely here).
| UINT OnExecution() override final; | |
| UINT OnExecution() final; |
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| std::filesystem::remove(std::filesystem::current_path() / msiName); | ||
| } | ||
| catch (const std::exception& ex) { | ||
| throw std::runtime_error( "Failed to remove existing MSI file: " + |
There was a problem hiding this comment.
Extra space after opening parenthesis in throw statement. Should be 'throw std::runtime_error("Failed' without the space.
| TEST_F(test_boinccas_CACleanupOldBinaries, | ||
| CleanupOldBinaries_NonExistent_INSTALLDIR_Directory) { | ||
| const auto dir = | ||
| std::filesystem::current_path() /= "non_existent_directory"; |
There was a problem hiding this comment.
Using compound assignment operator '/=' modifies the current_path() temporary, but the result is not what's intended. This should use the '/' operator instead: 'std::filesystem::current_path() / "non_existent_directory"' without the '=' to create a new path without modifying anything.
| std::filesystem::current_path() /= "non_existent_directory"; | |
| std::filesystem::current_path() / "non_existent_directory"; |
|
|
||
| TEST_F(test_boinccas_CACleanupOldBinaries, | ||
| CleanupOldBinaries_Empty_INSTALLDIR_Directory) { | ||
| testDir = std::filesystem::current_path() /= "empty"; |
There was a problem hiding this comment.
Using compound assignment operator '/=' modifies the current_path() temporary. This should use the '/' operator instead: 'std::filesystem::current_path() / "empty"' without the '='.
| testDir = std::filesystem::current_path() /= "empty"; | |
| testDir = std::filesystem::current_path() / "empty"; |
|
|
||
| TEST_F(test_boinccas_CACleanupOldBinaries, | ||
| CleanupOldBinaries_NonEmpty_INSTALLDIR_Directory_RemoveAllListedFiles) { | ||
| testDir = std::filesystem::current_path() /= "non_empty"; |
There was a problem hiding this comment.
Using compound assignment operator '/=' modifies the current_path() temporary. This should use the '/' operator instead: 'std::filesystem::current_path() / "non_empty"' without the '='.
|
|
||
| TEST_F(test_boinccas_CACleanupOldBinaries, | ||
| CleanupOldBinaries_NonEmpty_INSTALLDIR_Directory_KeepUnListedFiles) { | ||
| testDir = std::filesystem::current_path() /= "non_empty"; |
There was a problem hiding this comment.
Using compound assignment operator '/=' modifies the current_path() temporary. This should use the '/' operator instead: 'std::filesystem::current_path() / "non_empty"' without the '='.
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| HKEY hKey = nullptr; | ||
| const auto openResult = RegOpenKeyEx(HKEY_LOCAL_MACHINE, registryKey, 0, | ||
| KEY_WRITE, &hKey); | ||
| if (openResult != ERROR_SUCCESS) { | ||
| return; | ||
| } | ||
| RegDeleteKey(HKEY_LOCAL_MACHINE, registryKey); | ||
| RegCloseKey(hKey); |
There was a problem hiding this comment.
The key handle is closed after attempting to delete the key, but RegDeleteKey should be called with the parent key handle (HKEY_LOCAL_MACHINE), not the opened subkey handle. Also, the opened key handle should be closed before attempting deletion. The current code may leak the handle if RegDeleteKey succeeds.
| HKEY hKey = nullptr; | |
| const auto openResult = RegOpenKeyEx(HKEY_LOCAL_MACHINE, registryKey, 0, | |
| KEY_WRITE, &hKey); | |
| if (openResult != ERROR_SUCCESS) { | |
| return; | |
| } | |
| RegDeleteKey(HKEY_LOCAL_MACHINE, registryKey); | |
| RegCloseKey(hKey); | |
| RegDeleteKey(HKEY_LOCAL_MACHINE, registryKey); |
| //TEST_F(test_boinccas_CACreateAcctMgrLoginFile, | ||
| // Empty_DATADIR_Directory_And_ACCTMGR_LOGIN_Property_Set) { | ||
| // PMSIHANDLE hMsi; | ||
| // const auto dir = std::filesystem::current_path() /= "test_data"; | ||
| // msiHelper.insertProperties({ | ||
| // {"DATADIR", dir.string().c_str()}, | ||
| // {"ACCTMGR_LOGIN", "testuser"} | ||
| // }); | ||
| // const auto result = MsiOpenPackage(msiHelper.getMsiHandle().c_str(), | ||
| // &hMsi); | ||
| // ASSERT_EQ(0u, result); | ||
|
|
||
| // EXPECT_EQ(0u, hFunc(hMsi)); | ||
| //} |
There was a problem hiding this comment.
This large commented-out test case should either be uncommented and fixed (if intended for future use), or removed entirely to reduce code clutter.
| //TEST_F(test_boinccas_CACreateAcctMgrLoginFile, | |
| // Empty_DATADIR_Directory_And_ACCTMGR_LOGIN_Property_Set) { | |
| // PMSIHANDLE hMsi; | |
| // const auto dir = std::filesystem::current_path() /= "test_data"; | |
| // msiHelper.insertProperties({ | |
| // {"DATADIR", dir.string().c_str()}, | |
| // {"ACCTMGR_LOGIN", "testuser"} | |
| // }); | |
| // const auto result = MsiOpenPackage(msiHelper.getMsiHandle().c_str(), | |
| // &hMsi); | |
| // ASSERT_EQ(0u, result); | |
| // EXPECT_EQ(0u, hFunc(hMsi)); | |
| //} |
| &hMsi); | ||
| ASSERT_EQ(0u, result); | ||
| EXPECT_EQ(0u, hFunc(hMsi)); | ||
| // verify that the directory is now empty |
There was a problem hiding this comment.
Comment on line 150 is incorrect. The test expects the directory to NOT be empty (EXPECT_FALSE), so the comment should say 'verify that the directory is not empty' or 'verify that unlisted files remain'.
| // verify that the directory is now empty | |
| // verify that the directory is not empty (unlisted files remain) |
| <PreprocessorDefinitions>_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions> | ||
| <PreprocessorDefinitions Condition="'$(BOINCCAS_TEST)' == 'true'">BOINCCAS_TEST;%(PreprocessorDefinitions)</PreprocessorDefinitions> |
There was a problem hiding this comment.
Having two separate PreprocessorDefinitions elements can lead to the second one overriding the first. Consider consolidating them into a single element or ensuring the conditional definition properly appends to the base definitions.
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| _T(" <password_hash>%s</password_hash>\n") | ||
| _T("</acct_mgr_login>\n"), | ||
| strAcctMgrLogin.c_str(), | ||
| strAcctMgrPasswordHash.c_str() |
There was a problem hiding this comment.
Potential null pointer dereference when strAcctMgrPasswordHash is empty. The previous implementation explicitly checked if the string was empty and used an empty string literal. This change directly uses .c_str() which, while safe for empty strings in practice, removes the explicit semantic that an empty password hash should result in an empty XML element value. Consider restoring the original conditional logic: !strAcctMgrPasswordHash.empty() ? strAcctMgrPasswordHash.c_str() : _T(\"\")
| strAcctMgrPasswordHash.c_str() | |
| !strAcctMgrPasswordHash.empty() ? strAcctMgrPasswordHash.c_str() : _T("") |
| return uiReturnValue; | ||
| } | ||
|
|
||
| // The project_init.xml file is stored in the data directory. |
There was a problem hiding this comment.
Comment incorrectly references 'project_init.xml' when the code actually creates 'acct_mgr_login.xml'. Update comment to: 'The acct_mgr_login.xml file is stored in the data directory.'
| // The project_init.xml file is stored in the data directory. | |
| // The acct_mgr_login.xml file is stored in the data directory. |
| throw std::runtime_error( "Failed to remove existing MSI file: " + | ||
| std::string(ex.what())); |
There was a problem hiding this comment.
The exception is thrown in cleanup() which is called from the destructor. Throwing exceptions from destructors can lead to program termination if another exception is already in flight. Consider logging the error instead of throwing, or silently catch and ignore the exception since this is a cleanup operation.
Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 64 out of 64 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| std::string domainName(domainSize, '\0'); | ||
| std::vector<BYTE> sidBuf(sidSize); | ||
| domainSize = 256; | ||
| if (!LookupAccountName(nullptr, username.c_str(), sidBuf.data(), | ||
| &sidSize, domainName.data(), &domainSize, &use)) { | ||
| return nullptr; |
There was a problem hiding this comment.
getUserSid() sets domainSize = 256 after allocating domainName(domainSize, '\0'), which can make LookupAccountName write past the allocated buffer (buffer overflow). Keep domainSize consistent with the allocated buffer size (or reallocate to the new required size if the second call reports ERROR_INSUFFICIENT_BUFFER).
| SID_IDENTIFIER_AUTHORITY sia = SECURITY_NT_AUTHORITY; | ||
| PSID pUsersSid = nullptr; | ||
| if (!AllocateAndInitializeSid(&sia, 2, SECURITY_BUILTIN_DOMAIN_RID, | ||
| DOMAIN_ALIAS_RID_USERS, 0, 0, 0, 0, 0, 0, &pUsersSid)) { | ||
| if (pUsersSid) { | ||
| FreeSid(pUsersSid); | ||
| pUsersSid = nullptr; | ||
| } | ||
| } | ||
| wil::unique_sid pUsersSIDDeleter(pUsersSid); | ||
| auto nameSize = 0ul; | ||
| auto domainSize = 0ul; | ||
| SID_NAME_USE snu; | ||
| LookupAccountSid(nullptr, pUsersSid, nullptr, &nameSize, | ||
| nullptr, &domainSize, &snu); |
There was a problem hiding this comment.
If AllocateAndInitializeSid() fails, pUsersSid remains null but the code continues and calls LookupAccountSid with a null SID. This should return an error immediately (and log it), or at least guard against a null SID before using it.
| pBOINCProjectSID = nullptr; | ||
| } | ||
| LogMessage(INSTALLMESSAGE_ERROR, 0, 0, 0, GetLastError(), | ||
| _T("GetAccountSid Error for 'boinc_master' user account")); |
There was a problem hiding this comment.
This error message is emitted when resolving the 'boinc_project' SID, but it says "'boinc_master' user account". Please fix the message so failures clearly indicate which account lookup failed.
| _T("GetAccountSid Error for 'boinc_master' user account")); | |
| _T("GetAccountSid Error for 'boinc_project' user account")); |
| DWORD size = 0; | ||
| auto result = | ||
| MsiGetProperty(hMsiHandle, propertyName.c_str(), "", &size); | ||
| if (result == ERROR_MORE_DATA) { |
There was a problem hiding this comment.
MsiHelper::getProperty() uses a string literal as the output buffer for the initial MsiGetProperty call (""), which is not a writable buffer and can break with strict string literal settings. Use a writable 1-char buffer (or a std::array<char,1>) and size=0 to query required size.
Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 76 out of 76 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (5)
win_build/unittests.vcxproj:1
- The conditional entry will override the unconditional one when BOINCCAS_TEST=true, which drops
_CONSOLE(and any other values set earlier in the same item definition chain). To avoid losing defines, include_CONSOLEin the conditional value (or append BOINCCAS_TEST without replacing the prior property), e.g.,_CONSOLE;BOINCCAS_TEST;%(PreprocessorDefinitions).
tests/windows_installer_integration_tests.py:1 - Dropping
.strip()makes this assertion sensitive to XML pretty-printing/whitespace (newline/indentation often becomes part of.text). This can cause intermittent failures across different XML writers/parsers. Consider restoring.strip()(or normalizing whitespace) before comparing.
tests/unit-tests/boinccas/user_group_helper.cpp:1 - This implementation has two concrete issues: (1)
LookupAccountNameis a TCHAR-macro API; when building with UNICODE it resolves toLookupAccountNameWandusername.c_str()(char*) is the wrong type (won’t compile or will call the wrong variant if forced). Convertusernameto wide and call the explicitLookupAccountNameW. (2)domainSize = 256;can exceeddomainName’s allocated size (created from the first call’sdomainSize), which can lead to buffer overflow. Pass the actual buffer length (e.g., the string’s size) and avoid resettingdomainSizeto an arbitrary number.
tests/unit-tests/boinccas/msi_helper.cpp:1 cleanup()is called from~MsiHelper(), and this implementation can throw from the destructor path, which will callstd::terminateif another exception is in flight (and is generally unsafe). Make the destructor/cleanup path non-throwing (swallow/log filesystem errors or store the error for later), and keep throwing behavior only in constructor/init paths if needed.
tests/unit-tests/boinccas/test_boinccas_CAAnnounceUpgrade.cpp:1- These tests are compiled only when
BOINCCAS_TESTis NOT defined, but the workflow enables-p:BOINCCAS_TEST=true(which definesBOINCCAS_TESTin the unit test project). As a result, the AnnounceUpgrade tests won’t run in CI. Flip this guard to#ifdef BOINCCAS_TEST(or remove the guard) so the tests actually execute under the CI configuration.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| PROJECT_INIT pi; | ||
| strncpy(pi.url, boinc_wide_to_ascii(strProjectInitUrl).c_str(), | ||
| sizeof(pi.url) - 1); | ||
| if (!project_name.empty()) { | ||
| strncpy(pi.name, boinc_wide_to_ascii(project_name).c_str(), | ||
| sizeof(pi.name) - 1); | ||
| } | ||
| else { | ||
| strncpy(pi.name, | ||
| boinc_wide_to_ascii(strProjectInitUrl).c_str(), | ||
| sizeof(pi.name) - 1); | ||
| } | ||
| strncpy(pi.account_key, | ||
| boinc_wide_to_ascii(strProjectInitAuthenticator).c_str(), | ||
| sizeof(pi.account_key) - 1); | ||
| pi.embedded = false; | ||
| pi.write(); | ||
| } |
There was a problem hiding this comment.
The previous version called pi.init() before populating fields; here PROJECT_INIT pi; is created without initialization and then write() is called, which can write garbage/uninitialized fields depending on PROJECT_INIT’s definition. Also, strncpy(..., sizeof(x)-1) does not guarantee a null terminator. Initialize the struct (e.g., call pi.init() or zero-initialize) and explicitly ensure url/name/account_key are null-terminated after copying.
Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 79 out of 79 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| #ifndef BOINCCAS_TEST | ||
| TEST_F(test_boinccas_CAAnnounceUpgrade, | ||
| Empty_ProductVersion) { | ||
| const auto result = openMsi(); | ||
| ASSERT_EQ(0u, result); | ||
|
|
||
| EXPECT_NE(0u, executeAction()); | ||
| EXPECT_NE(expectedVersion, getRegistryValue("UpgradingTo")); | ||
| } | ||
|
|
||
| TEST_F(test_boinccas_CAAnnounceUpgrade, | ||
| With_ProductVersion) { | ||
| insertMsiProperties({ | ||
| {"ProductVersion", expectedVersion} | ||
| }); | ||
| const auto result = openMsi(); | ||
| ASSERT_EQ(0u, result); | ||
|
|
||
| EXPECT_EQ(0u, executeAction()); | ||
| EXPECT_EQ(expectedVersion, getRegistryValue("UpgradingTo")); | ||
| } | ||
| #endif |
There was a problem hiding this comment.
The BOINCCAS_TEST guard is inverted here: using #ifndef BOINCCAS_TEST means these tests are excluded from CI builds where BOINCCAS_TEST is set (see workflow passing -p:BOINCCAS_TEST=true). Flip this to #ifdef BOINCCAS_TEST (or remove the guard) so the AnnounceUpgrade tests actually compile/run under the intended configuration.
Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 85 out of 85 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (9)
tests/unit-tests/boinccas/registry_helper.h:1
RegOpenKeyEx/RegQueryValueExresolve to theAorWvariant based onUNICODE. WithkeyName/valueNameasstd::string, this will not compile (or will call the ANSI APIs unintentionally) in a Unicode build. Use explicitRegOpenKeyExA/RegQueryValueExA(and keepstd::string), or switch these helpers tostd::wstring+ use theWAPIs (recommended since the rest of the Windows codebase useststring/_T). Also consider validatingtypevsT(e.g.,REG_SZvs binary) and returning empty on mismatch to avoid mis-parsing.
tests/unit-tests/boinccas/registry_helper.h:1RegOpenKeyEx/RegQueryValueExresolve to theAorWvariant based onUNICODE. WithkeyName/valueNameasstd::string, this will not compile (or will call the ANSI APIs unintentionally) in a Unicode build. Use explicitRegOpenKeyExA/RegQueryValueExA(and keepstd::string), or switch these helpers tostd::wstring+ use theWAPIs (recommended since the rest of the Windows codebase useststring/_T). Also consider validatingtypevsT(e.g.,REG_SZvs binary) and returning empty on mismatch to avoid mis-parsing.
tests/unit-tests/boinccas/registry_helper.h:1RegOpenKeyEx/RegQueryValueExresolve to theAorWvariant based onUNICODE. WithkeyName/valueNameasstd::string, this will not compile (or will call the ANSI APIs unintentionally) in a Unicode build. Use explicitRegOpenKeyExA/RegQueryValueExA(and keepstd::string), or switch these helpers tostd::wstring+ use theWAPIs (recommended since the rest of the Windows codebase useststring/_T). Also consider validatingtypevsT(e.g.,REG_SZvs binary) and returning empty on mismatch to avoid mis-parsing.
tests/unit-tests/boinccas/user_group_helper.cpp:1- This code has two concrete issues: (1)
LookupAccountNameis alsoA/Wbased onUNICODE, butusernameisstd::stringand is passed directly; this will break in Unicode builds (or call the ANSI API unexpectedly). (2)domainSizeis overwritten with256before the second call, which can cause buffer size mismatch (overflow risk) becausedomainNamewas allocated using the size returned by the first call. KeepdomainSizealigned with the allocated buffer size (and ideally use astd::wstringbuffer withLookupAccountNameW).
tests/unit-tests/boinccas/user_group_helper.h:1 rightsToApplyis reused for the “add rights” phase without being cleared, so the second phase appends to the previous “remove rights” entries and will pass an incorrect list/count toLsaAddAccountRights. AddrightsToApply.clear()(and thenreserve) before populating it the second time. Also,for (const auto right : rights)should beconst auto& rightto avoid copying strings (minor).
tests/unit-tests/boinccas/msi_helper.cpp:1MsiHelper::~MsiHelper()callscleanup(), butcleanup()can throw. Throwing from a destructor can terminate the process (especially during stack unwinding) and will make unit test runs flaky/hard to diagnose. Makecleanup()non-throwing (e.g., usestd::filesystem::remove(path, std::error_code&)and ignore/report failures), or catch all exceptions inside the destructor and convert them to test failures via logging instead of throwing.
tests/unit-tests/boinccas/msi_helper.cpp:1MsiHelper::~MsiHelper()callscleanup(), butcleanup()can throw. Throwing from a destructor can terminate the process (especially during stack unwinding) and will make unit test runs flaky/hard to diagnose. Makecleanup()non-throwing (e.g., usestd::filesystem::remove(path, std::error_code&)and ignore/report failures), or catch all exceptions inside the destructor and convert them to test failures via logging instead of throwing.
tests/unit-tests/boinccas/user_group_helper.cpp:1- On lookup failure this returns
"Users"unconditionally, which is incorrect for callers that are looking up other well-known SIDs (notablygetLocalizedAdministratorsGroupName()andaddUserToTheBuiltinAdministratorsGroup()). Returning"Users"here can cause actions/tests to target the wrong group. Prefer returning an empty string (and let callers apply their own fallback), or accept a fallback name as a parameter (e.g.,"Administrators"vs"Users"depending on the caller).
tests/unit-tests/boinccas/test_boinccas_CAGrantBOINCAdminsVirtualBoxRights.cpp:1 - This test attempts to delete
HKCR\\APPID\\{819B4D85-...}(a real VirtualBox COM AppID key on machines with VirtualBox installed). Running unit tests on developer/test machines could damage local COM registrations. Please isolate registry writes to a test-only location (e.g.,HKCU\\Software\\Classes\\AppID\\{...}) or use a test-only GUID/key name, and avoid deleting system-owned keys. If you must clean up recursively, also note thatRegDeleteKeywon’t delete non-empty keys; use a safe recursive delete on the test-only hive.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| LogMessage(INSTALLMESSAGE_INFO, 0, 0, 0, nasReturnValue, | ||
| _T("NetUserDel retval")); | ||
| LogMessage(INSTALLMESSAGE_ERROR, 0, 0, 0, nasReturnValue, | ||
| _T("Failed to delete the 'boinc_master' account.")); |
There was a problem hiding this comment.
These error messages are now potentially misleading because the deleted account name comes from MSI properties (BOINC_MASTER_USERNAME / BOINC_PROJECT_USERNAME) and is not necessarily the literal 'boinc_master' / 'boinc_project'. Include the actual username value in the error message so failures are actionable (e.g., “Failed to delete account ''”).
| LogMessage(INSTALLMESSAGE_INFO, 0, 0, 0, nasReturnValue, | ||
| _T("NetUserDel retval")); | ||
| LogMessage(INSTALLMESSAGE_ERROR, 0, 0, 0, nasReturnValue, | ||
| _T("Failed to delete the 'boinc_project' account.")); |
There was a problem hiding this comment.
These error messages are now potentially misleading because the deleted account name comes from MSI properties (BOINC_MASTER_USERNAME / BOINC_PROJECT_USERNAME) and is not necessarily the literal 'boinc_master' / 'boinc_project'. Include the actual username value in the error message so failures are actionable (e.g., “Failed to delete account ''”).
Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 91 out of 91 changed files in this pull request and generated 9 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| std::vector<LSA_UNICODE_STRING> rightsToApply; | ||
| rightsToApply.reserve(wRights.size()); | ||
| for (const auto& right : wRights) { | ||
| rightsToApply.emplace_back(toLsaUnicodeString(right)); | ||
| } | ||
| if (!rightsToApply.empty()) { | ||
| const auto result = | ||
| LsaRemoveAccountRights(policyHandle, sid.get(), FALSE, | ||
| rightsToApply.data(), static_cast<ULONG>(rightsToApply.size())); | ||
| if (result != STATUS_SUCCESS && | ||
| LsaNtStatusToWinError(result) != ERROR_NO_SUCH_PRIVILEGE) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| wRights.clear(); | ||
| for (const auto right : rights) { | ||
| if (std::find(existingRights.cbegin(), existingRights.cend(), right) == | ||
| existingRights.cend()) { | ||
| wRights.emplace_back(boinc_ascii_to_wide(right)); | ||
| } | ||
| } | ||
| rightsToApply.reserve(wRights.size()); | ||
| for (const auto& right : wRights) { | ||
| rightsToApply.emplace_back(toLsaUnicodeString(right)); | ||
| } | ||
| if (!rightsToApply.empty()) { | ||
| const auto result = LsaAddAccountRights(policyHandle, sid.get(), | ||
| rightsToApply.data(), static_cast<ULONG>(rightsToApply.size())); | ||
| if (result != STATUS_SUCCESS && | ||
| LsaNtStatusToWinError(result) != ERROR_NO_SUCH_PRIVILEGE) { | ||
| return false; | ||
| } | ||
| } |
There was a problem hiding this comment.
setAccountRights() appends to rightsToApply twice without clearing it. As a result, the subsequent LsaAddAccountRights() call can re-add the same rights that were just removed (and will also pass an incorrect count). Clear rightsToApply (and ideally rebuild it from scratch) before populating it for the add step.
| void MsiHelper::cleanup() { | ||
| if (hMsi != 0) { | ||
| MsiCloseHandle(hMsi); | ||
| hMsi = 0; | ||
| } | ||
| try { | ||
| std::filesystem::remove(std::filesystem::current_path() / msiName); | ||
| } | ||
| catch (const std::exception& ex) { | ||
| throw std::runtime_error("Failed to remove existing MSI file: " + | ||
| std::string(ex.what())); | ||
| } |
There was a problem hiding this comment.
MsiHelper::~MsiHelper() calls cleanup(), but cleanup() can throw if std::filesystem::remove(...) raises. Throwing from a destructor can terminate the process (especially during stack unwinding). Make cleanup() non-throwing (e.g., use std::error_code overloads, log/ignore failures) or ensure the destructor catches and suppresses exceptions.
| #ifndef _WIN32 | ||
| #include <string> | ||
| #include <ios> | ||
| #include "gtest/gtest.h" | ||
| #endif | ||
| #include "common_defs.h" | ||
| #include "url.h" | ||
| #include <string> | ||
| #include <ios> | ||
|
|
There was a problem hiding this comment.
This file now conditionally excludes gtest and standard headers on Windows (#ifndef _WIN32), but still declares/uses gtest test fixtures and macros unconditionally. This makes the translation unit dependent on an external forced-include PCH and will break other build systems (e.g., CMake on Windows). Include gtest (and required headers) unconditionally or wrap the test definitions in the same platform guard.
| #ifndef _WIN32 | ||
| #include <string> | ||
| #include <ios> | ||
| #include "gtest/gtest.h" | ||
| #endif | ||
|
|
||
| #include "common_defs.h" | ||
| #include "str_util.h" | ||
| #include "error_numbers.h" | ||
|
|
There was a problem hiding this comment.
Same issue as other lib unit tests: gtest/standard includes are now skipped on Windows, but the test fixture and TEST_F usages are not guarded. This relies on MSBuild’s forced PCH include and will break CMake builds on Windows. Prefer self-contained includes or guard the test code itself.
| #ifndef _WIN32 | ||
| #include <string> | ||
| #include <ios> | ||
| #include "gtest/gtest.h" | ||
| #endif | ||
| #include "common_defs.h" | ||
| #include "url.h" | ||
| #include "parse.h" | ||
|
|
There was a problem hiding this comment.
This file now skips including gtest/standard headers on Windows, but still uses gtest test fixtures/macros unconditionally. That makes the file dependent on an external forced-include PCH and can break non-MSBuild builds on Windows (e.g., CMake). Include gtest unconditionally or wrap the test definitions with the same platform guard.
| for (decltype(aclSizeInfo.AceCount) i = 0; | ||
| i < aclSizeInfo.AceCount; ++i) { | ||
| LPVOID ace = nullptr; | ||
| if (!GetAce(dacl, i, &ace)) { | ||
| continue; | ||
| } | ||
| auto aceHeader = reinterpret_cast<ACE_HEADER*>(ace); | ||
| if (aceHeader->AceType == ACCESS_ALLOWED_ACE_TYPE) { | ||
| auto allowedAce = | ||
| reinterpret_cast<ACCESS_ALLOWED_ACE*>(ace); | ||
| wil::unique_sid sid(static_cast<PSID>( | ||
| LocalAlloc(LMEM_FIXED, SECURITY_MAX_SID_SIZE))); | ||
| CopySid(SECURITY_MAX_SID_SIZE, sid.get(), | ||
| reinterpret_cast<PSID>(&allowedAce->SidStart)); | ||
| auto sidString = getSidStringFromSid(std::move(sid)); | ||
| if (!boincAdminsSidString.empty() && | ||
| sidString == boincAdminsSidString) { | ||
| DeleteAce(dacl, i); | ||
| continue; | ||
| } | ||
| if (!systemSidString.empty() && | ||
| sidString == systemSidString) { | ||
| DeleteAce(dacl, i); | ||
| continue; | ||
| } | ||
| if (!interactiveSidString.empty() && | ||
| sidString == interactiveSidString) { | ||
| DeleteAce(dacl, i); | ||
| continue; | ||
| } |
There was a problem hiding this comment.
In TearDown(), you delete ACEs from the DACL while iterating forward by index. When DeleteAce(dacl, i) succeeds, subsequent ACEs shift down and the loop increments i, which can skip entries and leave unwanted ACEs behind (and can also make the loop bounds stale). Iterate from the end toward 0, or adjust i after deletion / re-query ACE count.
| #ifndef _WIN32 | ||
| #include <filesystem> | ||
|
|
||
| #include <fstream> | ||
| #include "gtest/gtest.h" | ||
| #include "boinc_zip.h" | ||
| #endif | ||
|
|
||
| #include "util.h" | ||
| #include "boinc_zip.h" |
There was a problem hiding this comment.
These unit test sources now exclude the gtest/gtest.h include on Windows (#ifndef _WIN32), but the file still defines ::testing::Test/TEST_F unconditionally. This works in the MSBuild unittests project only because pch.h force-includes gtest, but it will break builds that compile these tests without that PCH (e.g., the tests/unit-tests/lib/CMakeLists.txt target on Windows). Include gtest (and other required standard headers like <filesystem>/<fstream>) unconditionally, or wrap the actual test code in the same platform guard if the intent is to disable these tests on Windows.
| #ifndef _WIN32 | ||
| #include "gtest/gtest.h" | ||
| #endif | ||
| #include "util.h" | ||
| #include <math.h> |
There was a problem hiding this comment.
This file now conditionally excludes gtest/gtest.h on Windows, but still uses ::testing::Test/TEST_F unconditionally. It will fail to compile in build setups that don’t force-include gtest via a PCH (e.g., the CMake test_lib target on Windows). Include gtest unconditionally or guard the tests themselves.
| for (decltype(aclSizeInfo.AceCount) i = 0; | ||
| i < aclSizeInfo.AceCount; ++i) { | ||
| LPVOID ace = nullptr; | ||
| if (!GetAce(dacl, i, &ace)) { | ||
| continue; | ||
| } | ||
| auto aceHeader = reinterpret_cast<ACE_HEADER*>(ace); | ||
| if (aceHeader->AceType == ACCESS_ALLOWED_ACE_TYPE) { | ||
| auto allowedAce = | ||
| reinterpret_cast<ACCESS_ALLOWED_ACE*>(ace); | ||
| wil::unique_sid sid(static_cast<PSID>( | ||
| LocalAlloc(LMEM_FIXED, SECURITY_MAX_SID_SIZE))); | ||
| CopySid(SECURITY_MAX_SID_SIZE, sid.get(), | ||
| reinterpret_cast<PSID>(&allowedAce->SidStart)); | ||
| auto sidString = getSidStringFromSid(std::move(sid)); | ||
| if (!boincAdminsSidString.empty() && | ||
| sidString == boincAdminsSidString) { | ||
| DeleteAce(dacl, i); | ||
| continue; | ||
| } | ||
| if (!systemSidString.empty() && | ||
| sidString == systemSidString) { | ||
| DeleteAce(dacl, i); | ||
| continue; | ||
| } | ||
| if (!interactiveSidString.empty() && | ||
| sidString == interactiveSidString) { | ||
| DeleteAce(dacl, i); | ||
| continue; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
In TearDown(), ACEs are removed from the DACL while iterating forward by index. Deleting an ACE shifts later entries, so incrementing i can skip over entries (and the original AceCount may no longer match). Iterate backwards or decrement i after DeleteAce() to ensure all targeted ACEs are removed reliably.
Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 97 out of 97 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| DWORD size = 0; | ||
| auto result = | ||
| MsiGetProperty(hMsiHandle, propertyName.c_str(), "", &size); | ||
| if (result == ERROR_MORE_DATA) { |
There was a problem hiding this comment.
The initial MsiGetProperty call passes a string literal (""") as the output buffer. MsiGetProperty expects a writable buffer (LPSTR/LPWSTR), so this is ill-formed in C++ and/or risks writing to read-only memory. Use a local writable 1-char buffer (or nullptr with the documented pattern) to query the required size, then allocate and call again.
| wRights.clear(); | ||
| for (const auto right : rights) { | ||
| if (std::find(existingRights.cbegin(), existingRights.cend(), right) == | ||
| existingRights.cend()) { | ||
| wRights.emplace_back(boinc_ascii_to_wide(right)); | ||
| } | ||
| } | ||
| rightsToApply.reserve(wRights.size()); | ||
| for (const auto& right : wRights) { | ||
| rightsToApply.emplace_back(toLsaUnicodeString(right)); | ||
| } | ||
| if (!rightsToApply.empty()) { | ||
| const auto result = LsaAddAccountRights(policyHandle, sid.get(), | ||
| rightsToApply.data(), static_cast<ULONG>(rightsToApply.size())); |
There was a problem hiding this comment.
rightsToApply is reused for both the LsaRemoveAccountRights and LsaAddAccountRights calls, but it is never cleared between the two phases. As written, the vector will contain the removal rights plus the addition rights, and the subsequent LsaAddAccountRights call will add back rights that were just removed. Clear rightsToApply (and reserve again) before populating it for the add phase.
| TEST_F(test_boinccas_CAGrantBOINCAdminsVirtualBoxRights, | ||
| BoincAdminsGroupExists_PermissionsOwerwrittenSuccess) { | ||
| const auto result = openMsi(); |
There was a problem hiding this comment.
Typo in the test name: "PermissionsOwerwrittenSuccess" should be "PermissionsOverwrittenSuccess" to avoid propagating misspellings into test output/filters.
| TEST_F(test_boinccas_CAGrantBOINCProjectsVirtualBoxRights, | ||
| BoincProjectsGroupExists_PermissionsOwerwrittenSuccess) { | ||
| const auto result = openMsi(); |
There was a problem hiding this comment.
Typo in the test name: "PermissionsOwerwrittenSuccess" should be "PermissionsOverwrittenSuccess" to avoid propagating misspellings into test output/filters.
Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 103 out of 103 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (10)
tests/unit-tests/boinccas/user_group_helper.h:1
rightsToApplyis reused after the earlier removal phase without being cleared, but it storesLSA_UNICODE_STRINGvalues that reference the (now-cleared)wRightsbuffer viatoLsaUnicodeString. This can pass dangling pointers toLsaAddAccountRightsand apply incorrect rights. ClearrightsToApply(and rebuild it) before the add phase, or use a fresh vector for each LSA call while keeping the backingstd::wstringstorage alive until after the corresponding Lsa* call returns.
tests/unit-tests/boinccas/msi_helper.cpp:1- The MSI SQL
CREATE TABLEstatement appears malformed:PRIMARY KEYis missing a separating comma before it (and MSI SQL typically expects..., PRIMARY KEY \Property`)or thePRIMARY KEYmarker in the column definition). As written, table creation is likely to fail at runtime. Adjust the statement to valid MSI SQL (e.g., add the comma beforePRIMARY KEYor movePRIMARY KEYinto theProperty` column definition per MSI table syntax).
tests/unit-tests/boinccas/msi_helper.cpp:1 - The third parameter to
MsiGetPropertymust be a writable buffer (LPTSTR). Passing a string literal (\"\") is not writable and is also type-incompatible in Unicode builds. Use a writable buffer (ornullptr) for the size-probe call, and ensure you consistently call theA/WMSI APIs matching yourstd::string/std::wstringusage (e.g.,MsiGetPropertyA+ narrow strings, or switchpropertyName/buffers to wide strings and useMsiGetPropertyW).
tests/unit-tests/boinccas/msi_helper.cpp:1 MsiSetPropertyis a TCHAR-mapped API. In a Unicode build, passingstd::stringbuffers (char*) to the macro-mapped wide API will not compile or will behave incorrectly. UseMsiSetPropertyAexplicitly (keeping the helper narrow everywhere) or switch this helper tostd::wstringand callMsiSetPropertyW.
tests/unit-tests/boinccas/registry_helper.h:1- These registry helpers use narrow
std::stringnames and returnstd::stringforREG_SZ, but the production Windows code appears to be TCHAR-based (_T(...)) and is likely Unicode. Reading/writing UTF-16REG_SZdata viastd::stringwill truncate at the first embedded NUL byte and break tests (e.g., returning only the first character). Consider switching the helpers tostd::wstring+Reg*WAPIs, or explicitly useReg*ExA/RegSetValueExAconsistently and ensure the code under test also writes ANSI data.
tests/unit-tests/boinccas/test_boinccas_CAGrantBOINCAdminsVirtualBoxRights.cpp:1 - These tests modify and then delete
HKCR\\APPID\\{819B4D85-...}inTearDown(). On machines where VirtualBox is installed, this could remove real system configuration and break the environment. A safer approach is to virtualize the registry for the test process (e.g.,RegOverridePredefKeyto a temporary hive) or to snapshot+restore only the specific values you changed, rather than deleting the entire key.
tests/unit-tests/boinccas/test_boinccas_CAGrantBOINCProjectsVirtualBoxRights.cpp:1 - This loop deletes ACEs while iterating forward by index. After
DeleteAce(dacl, i), subsequent ACEs shift left, so incrementingican skip the next ACE and leave matching entries behind. Iterate fromAceCount - 1down to0(or adjustiafter deletion) to ensure all matching ACEs are removed.
tests/unit-tests/boinccas/test_boinccas_CAGrantBOINCProjectsVirtualBoxRights.cpp:1 - This loop deletes ACEs while iterating forward by index. After
DeleteAce(dacl, i), subsequent ACEs shift left, so incrementingican skip the next ACE and leave matching entries behind. Iterate fromAceCount - 1down to0(or adjustiafter deletion) to ensure all matching ACEs are removed.
tests/windows_installer_integration_tests.py:1 - Dropping
.strip()makes this assertion sensitive to formatting whitespace/newlines in the XML text node (which can vary depending on how the file is written). To keep the test robust, consider restoring.strip()(or normalizing whitespace) before comparing.
installer/include/InstallExecuteSequence.json:1 - The PR title focuses on adding an
AnnounceUpgradetest, but this PR also removes multiple installer custom actions (e.g.,CAGetAdministratorsGroupName/CAGetUsersGroupName) and deletes legacy implementation files. Please update the PR description/title to reflect these broader installer behavior changes, or split the refactor/removals into a separate PR to keep scope clear.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| PROJECT_INIT pi; | ||
| strncpy(pi.url, boinc_wide_to_ascii(strProjectInitUrl).c_str(), | ||
| sizeof(pi.url) - 1); |
Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 112 out of 112 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (5)
win_build/unittests.vcxproj:1
- When
BOINCCAS_TESTis true, the conditionalPreprocessorDefinitionselement can override the earlier_CONSOLE;...definition depending on MSBuild property evaluation, potentially dropping_CONSOLE(and any other definitions set earlier in the same scope). Make the conditional definition explicitly append to the existing value (e.g., include_CONSOLE;and any required inherited macros, or use a conditional property append pattern) so enablingBOINCCAS_TESTdoesn't change unrelated preprocessor flags.
tests/unit-tests/boinccas/user_group_helper.h:1 rightsToApplyis reused for the add-rights pass without being cleared, so the second call may include entries from the remove-rights pass as well. This can cause incorrect rights to be added/removed or make the call fail unexpectedly. ClearrightsToApply(and ideally rebuild it from scratch) before the secondemplace_backloop; also consider iteratingfor (const auto& right : rights)to avoid copying each element.
tests/unit-tests/boinccas/msi_helper.cpp:1- The first
MsiGetPropertycall passes a string literal (\"\") as the output buffer, which is not a writable buffer and is also type-incompatible in Unicode builds. Additionally, after the second callvalueis not resized to the returnedsize, so the returnedstd::stringcan contain trailing NULs and break equality checks in tests. Use a writable buffer for the sizing call, and after the read call, resizevaluetosizebefore returning it.
tests/unit-tests/boinccas/msi_helper.cpp:1 cleanup()throws on filesystem failures, but it is called from~MsiHelper()(directly or indirectly). Throwing exceptions from destructors can terminate the process during stack unwinding and will also make test failures much harder to diagnose. Make cleanup non-throwing (e.g., swallow/log errors or convert to a status return) and keep the destructornoexcept.
installer/include/InstallExecuteSequence.json:1- The PR title indicates adding an
AnnounceUpgradetest, but the diff also removesCAGetAdministratorsGroupName/CAGetUsersGroupNamefrom the installer sequence and refactors/removes multiple legacy custom action implementations/exports. This is a significant installer behavior change; please update the PR title/description to reflect the broader scope or split the installer refactor/removals into a separate PR.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (!strBOINCMasterAccountUsername.empty()) { | ||
| const auto nasReturnValue = NetUserDel(nullptr, | ||
| strBOINCMasterAccountUsername.c_str()); | ||
| if (nasReturnValue != NERR_Success) { | ||
| LogMessage(INSTALLMESSAGE_INFO, 0, 0, 0, nasReturnValue, | ||
| _T("NetUserDel retval")); | ||
| LogMessage(INSTALLMESSAGE_ERROR, 0, 0, 0, nasReturnValue, | ||
| _T("Failed to delete the 'boinc_master' account.")); | ||
| return ERROR_INSTALL_FAILURE; | ||
| } | ||
| } |
Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
No description provided.