From befcd209d62a25020f46a688002b259c59e4dc3b Mon Sep 17 00:00:00 2001
From: Sean Hall <r.sean.hall@gmail.com>
Date: Mon, 28 Feb 2022 18:42:51 -0600
Subject: Refactor related bundle enumeration into butil.

Related to #3693
---
 src/burn/engine/precomp.h                          |   1 +
 src/burn/engine/relatedbundle.cpp                  | 390 +++------------------
 src/burn/test/BurnUnitTest/BurnTestException.h     |  11 +-
 src/burn/test/BurnUnitTest/BurnUnitTest.vcxproj    |   1 +
 .../test/BurnUnitTest/BurnUnitTest.vcxproj.filters |   3 +
 src/burn/test/BurnUnitTest/RelatedBundleTest.cpp   | 199 +++++++++++
 src/burn/test/BurnUnitTest/precomp.h               |   1 +
 7 files changed, 261 insertions(+), 345 deletions(-)
 create mode 100644 src/burn/test/BurnUnitTest/RelatedBundleTest.cpp

(limited to 'src/burn')

diff --git a/src/burn/engine/precomp.h b/src/burn/engine/precomp.h
index 26adf44c..c83c1e74 100644
--- a/src/burn/engine/precomp.h
+++ b/src/burn/engine/precomp.h
@@ -55,6 +55,7 @@
 #include <atomutil.h>
 #include <apuputil.h>
 #include <dpiutil.h>
+#include <butil.h>
 
 #include "BootstrapperEngine.h"
 #include "BootstrapperApplication.h"
diff --git a/src/burn/engine/relatedbundle.cpp b/src/burn/engine/relatedbundle.cpp
index 3e0bc799..e6633131 100644
--- a/src/burn/engine/relatedbundle.cpp
+++ b/src/burn/engine/relatedbundle.cpp
@@ -2,6 +2,12 @@
 
 #include "precomp.h"
 
+typedef struct _BUNDLE_QUERY_CONTEXT
+{
+    BURN_REGISTRATION* pRegistration;
+    BURN_RELATED_BUNDLES* pRelatedBundles;
+} BUNDLE_QUERY_CONTEXT;
+
 // internal function declarations
 
 static __callback int __cdecl CompareRelatedBundles(
@@ -9,25 +15,15 @@ static __callback int __cdecl CompareRelatedBundles(
     __in const void* pvLeft,
     __in const void* pvRight
 );
-static HRESULT InitializeForScopeAndBitness(
-    __in BOOL fPerMachine,
-    __in REG_KEY_BITNESS regBitness,
-    __in BURN_REGISTRATION* pRegistration,
-    __in BURN_RELATED_BUNDLES* pRelatedBundles
+static BUNDLE_QUERY_CALLBACK_RESULT CALLBACK QueryRelatedBundlesCallback(
+    __in const BUNDLE_QUERY_RELATED_BUNDLE_RESULT* pBundle,
+    __in_opt LPVOID pvContext
     );
 static HRESULT LoadIfRelatedBundle(
-    __in BOOL fPerMachine,
-    __in REG_KEY_BITNESS regBitness,
-    __in HKEY hkUninstallKey,
-    __in_z LPCWSTR sczRelatedBundleId,
+    __in const BUNDLE_QUERY_RELATED_BUNDLE_RESULT* pBundle,
     __in BURN_REGISTRATION* pRegistration,
     __in BURN_RELATED_BUNDLES* pRelatedBundles
     );
-static HRESULT DetermineRelationType(
-    __in HKEY hkBundleId,
-    __in BURN_REGISTRATION* pRegistration,
-    __out BOOTSTRAPPER_RELATION_TYPE* pRelationType
-    );
 static HRESULT LoadRelatedBundleFromKey(
     __in_z LPCWSTR wzRelatedBundleId,
     __in HKEY hkBundleId,
@@ -46,12 +42,25 @@ extern "C" HRESULT RelatedBundlesInitializeForScope(
     )
 {
     HRESULT hr = S_OK;
-
-    hr = InitializeForScopeAndBitness(fPerMachine, REG_KEY_32BIT, pRegistration, pRelatedBundles);
-    ExitOnFailure(hr, "Failed to open 32-bit uninstall registry key.");
-
-    hr = InitializeForScopeAndBitness(fPerMachine, REG_KEY_64BIT, pRegistration, pRelatedBundles);
-    ExitOnFailure(hr, "Failed to open 64-bit uninstall registry key.");
+    BUNDLE_INSTALL_CONTEXT installContext = fPerMachine ? BUNDLE_INSTALL_CONTEXT_MACHINE : BUNDLE_INSTALL_CONTEXT_USER;
+    BUNDLE_QUERY_CONTEXT queryContext = { };
+
+    queryContext.pRegistration = pRegistration;
+    queryContext.pRelatedBundles = pRelatedBundles;
+
+    hr = BundleQueryRelatedBundles(
+        installContext,
+        const_cast<LPCWSTR*>(pRegistration->rgsczDetectCodes),
+        pRegistration->cDetectCodes,
+        const_cast<LPCWSTR*>(pRegistration->rgsczUpgradeCodes),
+        pRegistration->cUpgradeCodes,
+        const_cast<LPCWSTR*>(pRegistration->rgsczAddonCodes),
+        pRegistration->cAddonCodes,
+        const_cast<LPCWSTR*>(pRegistration->rgsczPatchCodes),
+        pRegistration->cPatchCodes,
+        QueryRelatedBundlesCallback,
+        &queryContext);
+    ExitOnFailure(hr, "Failed to initialize related bundles for scope.");
 
 LExit:
     return hr;
@@ -166,346 +175,53 @@ static __callback int __cdecl CompareRelatedBundles(
     return ret;
 }
 
-static HRESULT InitializeForScopeAndBitness(
-    __in BOOL fPerMachine,
-    __in REG_KEY_BITNESS regBitness,
-    __in BURN_REGISTRATION * pRegistration,
-    __in BURN_RELATED_BUNDLES * pRelatedBundles
-)
+static BUNDLE_QUERY_CALLBACK_RESULT CALLBACK QueryRelatedBundlesCallback(
+    __in const BUNDLE_QUERY_RELATED_BUNDLE_RESULT* pBundle,
+    __in_opt LPVOID pvContext
+    )
 {
     HRESULT hr = S_OK;
-    HKEY hkRoot = fPerMachine ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER;
-    HKEY hkUninstallKey = NULL;
-    LPWSTR sczRelatedBundleId = NULL;
+    BUNDLE_QUERY_CALLBACK_RESULT result = BUNDLE_QUERY_CALLBACK_RESULT_CONTINUE;
+    BUNDLE_QUERY_CONTEXT* pContext = reinterpret_cast<BUNDLE_QUERY_CONTEXT*>(pvContext);
 
-    hr = RegOpenEx(hkRoot, BURN_REGISTRATION_REGISTRY_UNINSTALL_KEY, KEY_READ, regBitness, &hkUninstallKey);
-    if (HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND) == hr || HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) == hr)
-    {
-        ExitFunction1(hr = S_OK);
-    }
-    ExitOnFailure(hr, "Failed to open uninstall registry key.");
-
-    for (DWORD dwIndex = 0; /* exit via break below */; ++dwIndex)
-    {
-        hr = RegKeyEnum(hkUninstallKey, dwIndex, &sczRelatedBundleId);
-        if (E_NOMOREITEMS == hr)
-        {
-            hr = S_OK;
-            break;
-        }
-        ExitOnFailure(hr, "Failed to enumerate uninstall key for related bundles.");
-
-        // If we did not find our bundle id, try to load the subkey as a related bundle.
-        if (CSTR_EQUAL != ::CompareStringW(LOCALE_NEUTRAL, NORM_IGNORECASE, sczRelatedBundleId, -1, pRegistration->sczId, -1))
-        {
-            // Ignore failures here since we'll often find products that aren't actually
-            // related bundles (or even bundles at all).
-            HRESULT hrRelatedBundle = LoadIfRelatedBundle(fPerMachine, regBitness, hkUninstallKey, sczRelatedBundleId, pRegistration, pRelatedBundles);
-            UNREFERENCED_PARAMETER(hrRelatedBundle);
-        }
-    }
+    hr = LoadIfRelatedBundle(pBundle, pContext->pRegistration, pContext->pRelatedBundles);
+    ExitOnFailure(hr, "Failed to load related bundle: %ls", pBundle->wzBundleId);
 
 LExit:
-    ReleaseStr(sczRelatedBundleId);
-    ReleaseRegKey(hkUninstallKey);
-
-    return hr;
+    return result;
 }
 
 static HRESULT LoadIfRelatedBundle(
-    __in BOOL fPerMachine,
-    __in REG_KEY_BITNESS regBitness,
-    __in HKEY hkUninstallKey,
-    __in_z LPCWSTR sczRelatedBundleId,
+    __in const BUNDLE_QUERY_RELATED_BUNDLE_RESULT* pBundle,
     __in BURN_REGISTRATION* pRegistration,
     __in BURN_RELATED_BUNDLES* pRelatedBundles
     )
 {
     HRESULT hr = S_OK;
-    HKEY hkBundleId = NULL;
-    BOOTSTRAPPER_RELATION_TYPE relationType = BOOTSTRAPPER_RELATION_NONE;
-
-    hr = RegOpenEx(hkUninstallKey, sczRelatedBundleId, KEY_READ, regBitness, &hkBundleId);
-    ExitOnFailure(hr, "Failed to open uninstall key for potential related bundle: %ls", sczRelatedBundleId);
-
-    hr = DetermineRelationType(hkBundleId, pRegistration, &relationType);
-    if (FAILED(hr) || BOOTSTRAPPER_RELATION_NONE == relationType)
-    {
-        // Must not be a related bundle.
-        hr = E_NOTFOUND;
-    }
-    else // load the related bundle.
-    {
-        hr = MemEnsureArraySize(reinterpret_cast<LPVOID*>(&pRelatedBundles->rgRelatedBundles), pRelatedBundles->cRelatedBundles + 1, sizeof(BURN_RELATED_BUNDLE), 5);
-        ExitOnFailure(hr, "Failed to ensure there is space for related bundles.");
-
-        BURN_RELATED_BUNDLE* pRelatedBundle = pRelatedBundles->rgRelatedBundles + pRelatedBundles->cRelatedBundles;
-
-        hr = LoadRelatedBundleFromKey(sczRelatedBundleId, hkBundleId, fPerMachine, relationType, pRelatedBundle);
-        ExitOnFailure(hr, "Failed to initialize package from related bundle id: %ls", sczRelatedBundleId);
-
-        hr = DependencyDetectRelatedBundle(pRelatedBundle, pRegistration);
-        ExitOnFailure(hr, "Failed to detect dependencies for related bundle.");
-
-        ++pRelatedBundles->cRelatedBundles;
-    }
-
-LExit:
-    ReleaseRegKey(hkBundleId);
-
-    return hr;
-}
-
-static HRESULT DetermineRelationType(
-    __in HKEY hkBundleId,
-    __in BURN_REGISTRATION* pRegistration,
-    __out BOOTSTRAPPER_RELATION_TYPE* pRelationType
-    )
-{
-    HRESULT hr = S_OK;
-    LPWSTR* rgsczUpgradeCodes = NULL;
-    DWORD cUpgradeCodes = 0;
-    STRINGDICT_HANDLE sdUpgradeCodes = NULL;
-    LPWSTR* rgsczAddonCodes = NULL;
-    DWORD cAddonCodes = 0;
-    STRINGDICT_HANDLE sdAddonCodes = NULL;
-    LPWSTR* rgsczDetectCodes = NULL;
-    DWORD cDetectCodes = 0;
-    STRINGDICT_HANDLE sdDetectCodes = NULL;
-    LPWSTR* rgsczPatchCodes = NULL;
-    DWORD cPatchCodes = 0;
-    STRINGDICT_HANDLE sdPatchCodes = NULL;
-
-    *pRelationType = BOOTSTRAPPER_RELATION_NONE;
-
-    // All remaining operations should treat all related bundles as non-vital.
-    hr = RegReadStringArray(hkBundleId, BURN_REGISTRATION_REGISTRY_BUNDLE_UPGRADE_CODE, &rgsczUpgradeCodes, &cUpgradeCodes);
-    if (HRESULT_FROM_WIN32(ERROR_INVALID_DATATYPE) == hr)
-    {
-        TraceError(hr, "Failed to read upgrade codes as REG_MULTI_SZ. Trying again as REG_SZ in case of older bundles.");
-
-        rgsczUpgradeCodes = reinterpret_cast<LPWSTR*>(MemAlloc(sizeof(LPWSTR), TRUE));
-        ExitOnNull(rgsczUpgradeCodes, hr, E_OUTOFMEMORY, "Failed to allocate list for a single upgrade code from older bundle.");
-
-        hr = RegReadString(hkBundleId, BURN_REGISTRATION_REGISTRY_BUNDLE_UPGRADE_CODE, &rgsczUpgradeCodes[0]);
-        if (SUCCEEDED(hr))
-        {
-            cUpgradeCodes = 1;
-        }
-    }
-
-    // Compare upgrade codes.
-    if (SUCCEEDED(hr))
-    {
-        hr = DictCreateStringListFromArray(&sdUpgradeCodes, rgsczUpgradeCodes, cUpgradeCodes, DICT_FLAG_CASEINSENSITIVE);
-        ExitOnFailure(hr, "Failed to create string dictionary for %hs.", "upgrade codes");
-
-        // Upgrade relationship: when their upgrade codes match our upgrade codes.
-        hr = DictCompareStringListToArray(sdUpgradeCodes, const_cast<LPCWSTR*>(pRegistration->rgsczUpgradeCodes), pRegistration->cUpgradeCodes);
-        if (HRESULT_FROM_WIN32(ERROR_NO_MATCH) == hr)
-        {
-            hr = S_OK;
-        }
-        else
-        {
-            ExitOnFailure(hr, "Failed to do array search for upgrade code match.");
-
-            *pRelationType = BOOTSTRAPPER_RELATION_UPGRADE;
-            ExitFunction();
-        }
-
-        // Detect relationship: when their upgrade codes match our detect codes.
-        hr = DictCompareStringListToArray(sdUpgradeCodes, const_cast<LPCWSTR*>(pRegistration->rgsczDetectCodes), pRegistration->cDetectCodes);
-        if (HRESULT_FROM_WIN32(ERROR_NO_MATCH) == hr)
-        {
-            hr = S_OK;
-        }
-        else
-        {
-            ExitOnFailure(hr, "Failed to do array search for detect code match.");
-
-            *pRelationType = BOOTSTRAPPER_RELATION_DETECT;
-            ExitFunction();
-        }
-
-        // Dependent relationship: when their upgrade codes match our addon codes.
-        hr = DictCompareStringListToArray(sdUpgradeCodes, const_cast<LPCWSTR*>(pRegistration->rgsczAddonCodes), pRegistration->cAddonCodes);
-        if (HRESULT_FROM_WIN32(ERROR_NO_MATCH) == hr)
-        {
-            hr = S_OK;
-        }
-        else
-        {
-            ExitOnFailure(hr, "Failed to do array search for addon code match.");
-
-            *pRelationType = BOOTSTRAPPER_RELATION_DEPENDENT;
-            ExitFunction();
-        }
-
-        // Dependent relationship: when their upgrade codes match our patch codes.
-        hr = DictCompareStringListToArray(sdUpgradeCodes, const_cast<LPCWSTR*>(pRegistration->rgsczPatchCodes), pRegistration->cPatchCodes);
-        if (HRESULT_FROM_WIN32(ERROR_NO_MATCH) == hr)
-        {
-            hr = S_OK;
-        }
-        else
-        {
-            ExitOnFailure(hr, "Failed to do array search for addon code match.");
-
-            *pRelationType = BOOTSTRAPPER_RELATION_DEPENDENT;
-            ExitFunction();
-        }
-
-        ReleaseNullDict(sdUpgradeCodes);
-        ReleaseNullStrArray(rgsczUpgradeCodes, cUpgradeCodes);
-    }
-
-    // Compare addon codes.
-    hr = RegReadStringArray(hkBundleId, BURN_REGISTRATION_REGISTRY_BUNDLE_ADDON_CODE, &rgsczAddonCodes, &cAddonCodes);
-    if (SUCCEEDED(hr))
-    {
-        hr = DictCreateStringListFromArray(&sdAddonCodes, rgsczAddonCodes, cAddonCodes, DICT_FLAG_CASEINSENSITIVE);
-        ExitOnFailure(hr, "Failed to create string dictionary for %hs.", "addon codes");
-
-        // Addon relationship: when their addon codes match our detect codes.
-        hr = DictCompareStringListToArray(sdAddonCodes, const_cast<LPCWSTR*>(pRegistration->rgsczDetectCodes), pRegistration->cDetectCodes);
-        if (HRESULT_FROM_WIN32(ERROR_NO_MATCH) == hr)
-        {
-            hr = S_OK;
-        }
-        else
-        {
-            ExitOnFailure(hr, "Failed to do array search for addon code match.");
-
-            *pRelationType = BOOTSTRAPPER_RELATION_ADDON;
-            ExitFunction();
-        }
-
-        // Addon relationship: when their addon codes match our upgrade codes.
-        hr = DictCompareStringListToArray(sdAddonCodes, const_cast<LPCWSTR*>(pRegistration->rgsczUpgradeCodes), pRegistration->cUpgradeCodes);
-        if (HRESULT_FROM_WIN32(ERROR_NO_MATCH) == hr)
-        {
-            hr = S_OK;
-        }
-        else
-        {
-            ExitOnFailure(hr, "Failed to do array search for addon code match.");
-
-            *pRelationType = BOOTSTRAPPER_RELATION_ADDON;
-            ExitFunction();
-        }
-
-        ReleaseNullDict(sdAddonCodes);
-        ReleaseNullStrArray(rgsczAddonCodes, cAddonCodes);
-    }
+    BOOL fPerMachine = BUNDLE_INSTALL_CONTEXT_MACHINE == pBundle->installContext;
+    BOOTSTRAPPER_RELATION_TYPE relationType = (BOOTSTRAPPER_RELATION_TYPE)pBundle->relationType;
+    BURN_RELATED_BUNDLE* pRelatedBundle = NULL;
 
-    // Compare patch codes.
-    hr = RegReadStringArray(hkBundleId, BURN_REGISTRATION_REGISTRY_BUNDLE_PATCH_CODE, &rgsczPatchCodes, &cPatchCodes);
-    if (SUCCEEDED(hr))
+    // If we found our bundle id, it's not a related bundle.
+    if (CSTR_EQUAL == ::CompareStringW(LOCALE_NEUTRAL, NORM_IGNORECASE, pBundle->wzBundleId, -1, pRegistration->sczId, -1))
     {
-        hr = DictCreateStringListFromArray(&sdPatchCodes, rgsczPatchCodes, cPatchCodes, DICT_FLAG_CASEINSENSITIVE);
-        ExitOnFailure(hr, "Failed to create string dictionary for %hs.", "patch codes");
-
-        // Patch relationship: when their patch codes match our detect codes.
-        hr = DictCompareStringListToArray(sdPatchCodes, const_cast<LPCWSTR*>(pRegistration->rgsczDetectCodes), pRegistration->cDetectCodes);
-        if (HRESULT_FROM_WIN32(ERROR_NO_MATCH) == hr)
-        {
-            hr = S_OK;
-        }
-        else
-        {
-            ExitOnFailure(hr, "Failed to do array search for patch code match.");
-
-            *pRelationType = BOOTSTRAPPER_RELATION_PATCH;
-            ExitFunction();
-        }
-
-        // Patch relationship: when their patch codes match our upgrade codes.
-        hr = DictCompareStringListToArray(sdPatchCodes, const_cast<LPCWSTR*>(pRegistration->rgsczUpgradeCodes), pRegistration->cUpgradeCodes);
-        if (HRESULT_FROM_WIN32(ERROR_NO_MATCH) == hr)
-        {
-            hr = S_OK;
-        }
-        else
-        {
-            ExitOnFailure(hr, "Failed to do array search for patch code match.");
-
-            *pRelationType = BOOTSTRAPPER_RELATION_PATCH;
-            ExitFunction();
-        }
-
-        ReleaseNullDict(sdPatchCodes);
-        ReleaseNullStrArray(rgsczPatchCodes, cPatchCodes);
+        ExitFunction1(hr = S_FALSE);
     }
 
-    // Compare detect codes.
-    hr = RegReadStringArray(hkBundleId, BURN_REGISTRATION_REGISTRY_BUNDLE_DETECT_CODE, &rgsczDetectCodes, &cDetectCodes);
-    if (SUCCEEDED(hr))
-    {
-        hr = DictCreateStringListFromArray(&sdDetectCodes, rgsczDetectCodes, cDetectCodes, DICT_FLAG_CASEINSENSITIVE);
-        ExitOnFailure(hr, "Failed to create string dictionary for %hs.", "detect codes");
+    hr = MemEnsureArraySize(reinterpret_cast<LPVOID*>(&pRelatedBundles->rgRelatedBundles), pRelatedBundles->cRelatedBundles + 1, sizeof(BURN_RELATED_BUNDLE), 5);
+    ExitOnFailure(hr, "Failed to ensure there is space for related bundles.");
 
-        // Detect relationship: when their detect codes match our detect codes.
-        hr = DictCompareStringListToArray(sdDetectCodes, const_cast<LPCWSTR*>(pRegistration->rgsczDetectCodes), pRegistration->cDetectCodes);
-        if (HRESULT_FROM_WIN32(ERROR_NO_MATCH) == hr)
-        {
-            hr = S_OK;
-        }
-        else
-        {
-            ExitOnFailure(hr, "Failed to do array search for detect code match.");
+    pRelatedBundle = pRelatedBundles->rgRelatedBundles + pRelatedBundles->cRelatedBundles;
 
-            *pRelationType = BOOTSTRAPPER_RELATION_DETECT;
-            ExitFunction();
-        }
+    hr = LoadRelatedBundleFromKey(pBundle->wzBundleId, pBundle->hkBundle, fPerMachine, relationType, pRelatedBundle);
+    ExitOnFailure(hr, "Failed to initialize package from related bundle id: %ls", pBundle->wzBundleId);
 
-        // Dependent relationship: when their detect codes match our addon codes.
-        hr = DictCompareStringListToArray(sdDetectCodes, const_cast<LPCWSTR*>(pRegistration->rgsczAddonCodes), pRegistration->cAddonCodes);
-        if (HRESULT_FROM_WIN32(ERROR_NO_MATCH) == hr)
-        {
-            hr = S_OK;
-        }
-        else
-        {
-            ExitOnFailure(hr, "Failed to do array search for addon code match.");
-
-            *pRelationType = BOOTSTRAPPER_RELATION_DEPENDENT;
-            ExitFunction();
-        }
-
-        // Dependent relationship: when their detect codes match our patch codes.
-        hr = DictCompareStringListToArray(sdDetectCodes, const_cast<LPCWSTR*>(pRegistration->rgsczPatchCodes), pRegistration->cPatchCodes);
-        if (HRESULT_FROM_WIN32(ERROR_NO_MATCH) == hr)
-        {
-            hr = S_OK;
-        }
-        else
-        {
-            ExitOnFailure(hr, "Failed to do array search for addon code match.");
+    hr = DependencyDetectRelatedBundle(pRelatedBundle, pRegistration);
+    ExitOnFailure(hr, "Failed to detect dependencies for related bundle.");
 
-            *pRelationType = BOOTSTRAPPER_RELATION_DEPENDENT;
-            ExitFunction();
-        }
-
-        ReleaseNullDict(sdDetectCodes);
-        ReleaseNullStrArray(rgsczDetectCodes, cDetectCodes);
-    }
+    ++pRelatedBundles->cRelatedBundles;
 
 LExit:
-    if (SUCCEEDED(hr) && BOOTSTRAPPER_RELATION_NONE == *pRelationType)
-    {
-        hr = E_NOTFOUND;
-    }
-
-    ReleaseDict(sdUpgradeCodes);
-    ReleaseStrArray(rgsczUpgradeCodes, cUpgradeCodes);
-    ReleaseDict(sdAddonCodes);
-    ReleaseStrArray(rgsczAddonCodes, cAddonCodes);
-    ReleaseDict(sdDetectCodes);
-    ReleaseStrArray(rgsczDetectCodes, cDetectCodes);
-    ReleaseDict(sdPatchCodes);
-    ReleaseStrArray(rgsczPatchCodes, cPatchCodes);
-
     return hr;
 }
 
diff --git a/src/burn/test/BurnUnitTest/BurnTestException.h b/src/burn/test/BurnUnitTest/BurnTestException.h
index bd94b4fc..e813f95c 100644
--- a/src/burn/test/BurnUnitTest/BurnTestException.h
+++ b/src/burn/test/BurnUnitTest/BurnTestException.h
@@ -13,19 +13,14 @@ namespace Test
 namespace Bootstrapper
 {
     using namespace System;
+    using namespace WixBuildTools::TestSupport;
 
-    public ref struct BurnTestException : public System::Exception
+    public ref struct BurnTestException : public SucceededException
     {
     public:
-        BurnTestException(HRESULT error)
-        {
-            this->HResult = error;
-        }
-
         BurnTestException(HRESULT error, String^ message)
-            : Exception(message)
+            : SucceededException(error, message)
         {
-            this->HResult = error;
         }
 
         property Int32 ErrorCode
diff --git a/src/burn/test/BurnUnitTest/BurnUnitTest.vcxproj b/src/burn/test/BurnUnitTest/BurnUnitTest.vcxproj
index 36903239..f4f95b7f 100644
--- a/src/burn/test/BurnUnitTest/BurnUnitTest.vcxproj
+++ b/src/burn/test/BurnUnitTest/BurnUnitTest.vcxproj
@@ -56,6 +56,7 @@
       <DisableSpecificWarnings>4564;4691</DisableSpecificWarnings>
     </ClCompile>
     <ClCompile Include="RegistrationTest.cpp" />
+    <ClCompile Include="RelatedBundleTest.cpp" />
     <ClCompile Include="SearchTest.cpp" />
     <ClCompile Include="TestRegistryFixture.cpp" />
     <ClCompile Include="VariableHelpers.cpp" />
diff --git a/src/burn/test/BurnUnitTest/BurnUnitTest.vcxproj.filters b/src/burn/test/BurnUnitTest/BurnUnitTest.vcxproj.filters
index 96563fc6..90290f52 100644
--- a/src/burn/test/BurnUnitTest/BurnUnitTest.vcxproj.filters
+++ b/src/burn/test/BurnUnitTest/BurnUnitTest.vcxproj.filters
@@ -39,6 +39,9 @@
     <ClCompile Include="RegistrationTest.cpp">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="RelatedBundleTest.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
     <ClCompile Include="SearchTest.cpp">
       <Filter>Source Files</Filter>
     </ClCompile>
diff --git a/src/burn/test/BurnUnitTest/RelatedBundleTest.cpp b/src/burn/test/BurnUnitTest/RelatedBundleTest.cpp
new file mode 100644
index 00000000..3d1964c3
--- /dev/null
+++ b/src/burn/test/BurnUnitTest/RelatedBundleTest.cpp
@@ -0,0 +1,199 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+#include "precomp.h"
+
+
+namespace Microsoft
+{
+namespace Tools
+{
+namespace WindowsInstallerXml
+{
+namespace Test
+{
+namespace Bootstrapper
+{
+    using namespace System;
+    using namespace System::IO;
+    using namespace Xunit;
+    using namespace WixBuildTools::TestSupport;
+
+    public ref class RelatedBundleTest : BurnUnitTest, IClassFixture<TestRegistryFixture^>
+    {
+    private:
+        TestRegistryFixture^ testRegistry;
+    public:
+        RelatedBundleTest(BurnTestFixture^ fixture, TestRegistryFixture^ registryFixture) : BurnUnitTest(fixture)
+        {
+            this->testRegistry = registryFixture;
+        }
+
+        [Fact]
+        void RelatedBundleDetectPerMachineTest()
+        {
+            HRESULT hr = S_OK;
+            IXMLDOMElement* pixeBundle = NULL;
+            BURN_REGISTRATION registration = { };
+            BURN_RELATED_BUNDLES relatedBundles = { };
+            BURN_CACHE cache = { };
+            BURN_ENGINE_COMMAND internalCommand = { };
+
+            try
+            {
+                this->testRegistry->SetUp();
+                this->RegisterFakeBundles();
+
+                LPCWSTR wzDocument =
+                    L"<Bundle>"
+                    L"    <UX>"
+                    L"        <Payload Id='ux.dll' FilePath='ux.dll' Packaging='embedded' SourcePath='ux.dll' />"
+                    L"    </UX>"
+                    L"    <RelatedBundle Id='{89FDAE1F-8CC1-48B9-B930-3945E0D3E7F0}' Action='Upgrade' />"
+                    L"    <Registration Id='{D54F896D-1952-43E6-9C67-B5652240618C}' Tag='foo' ProviderKey='foo' Version='1.0.0.0' ExecutableName='setup.exe' PerMachine='yes'>"
+                    L"        <Arp Register='yes' Publisher='WiX Toolset' DisplayName='RegisterBasicTest' DisplayVersion='1.0.0.0' />"
+                    L"    </Registration>"
+                    L"</Bundle>";
+
+                // load XML document
+                LoadBundleXmlHelper(wzDocument, &pixeBundle);
+
+                hr = CacheInitialize(&cache, &internalCommand);
+                TestThrowOnFailure(hr, L"Failed initialize cache.");
+
+                hr = RegistrationParseFromXml(&registration, &cache, pixeBundle);
+                TestThrowOnFailure(hr, L"Failed to parse registration from XML.");
+
+                RelatedBundlesInitializeForScope(registration.fPerMachine, &registration, &relatedBundles);
+
+                Assert::Equal(1lu, relatedBundles.cRelatedBundles);
+
+                BURN_RELATED_BUNDLE* pRelatedBundle = relatedBundles.rgRelatedBundles + 0;
+                NativeAssert::StringEqual(L"{AD75BE46-B5D7-4208-BC8B-918553C72D83}", pRelatedBundle->package.sczId);
+                //{E2355133-384C-4332-9B62-1FA950D707B7} should be missing because it causes an error while processing it. It's important that this doesn't cause initialization to fail.
+            }
+            finally
+            {
+                ReleaseObject(pixeBundle);
+                RegistrationUninitialize(&registration);
+
+                this->testRegistry->TearDown();
+            }
+        }
+
+        [Fact]
+        void RelatedBundleDetectPerUserTest()
+        {
+            HRESULT hr = S_OK;
+            IXMLDOMElement* pixeBundle = NULL;
+            BURN_REGISTRATION registration = { };
+            BURN_RELATED_BUNDLES relatedBundles = { };
+            BURN_CACHE cache = { };
+            BURN_ENGINE_COMMAND internalCommand = { };
+
+            try
+            {
+                this->testRegistry->SetUp();
+                this->RegisterFakeBundles();
+
+                LPCWSTR wzDocument =
+                    L"<Bundle>"
+                    L"    <UX>"
+                    L"        <Payload Id='ux.dll' FilePath='ux.dll' Packaging='embedded' SourcePath='ux.dll' />"
+                    L"    </UX>"
+                    L"    <RelatedBundle Id='{89FDAE1F-8CC1-48B9-B930-3945E0D3E7F0}' Action='Upgrade' />"
+                    L"    <Registration Id='{3DB49D3D-1FB8-4147-A465-BBE8BFD0DAD0}' Tag='foo' ProviderKey='foo' Version='4.0.0.0' ExecutableName='setup.exe' PerMachine='no'>"
+                    L"        <Arp Register='yes' Publisher='WiX Toolset' DisplayName='RegisterBasicTest' DisplayVersion='4.0.0.0' />"
+                    L"    </Registration>"
+                    L"</Bundle>";
+
+                // load XML document
+                LoadBundleXmlHelper(wzDocument, &pixeBundle);
+
+                hr = CacheInitialize(&cache, &internalCommand);
+                TestThrowOnFailure(hr, L"Failed initialize cache.");
+
+                hr = RegistrationParseFromXml(&registration, &cache, pixeBundle);
+                TestThrowOnFailure(hr, L"Failed to parse registration from XML.");
+
+                RelatedBundlesInitializeForScope(registration.fPerMachine, &registration, &relatedBundles);
+
+                Assert::Equal(1lu, relatedBundles.cRelatedBundles);
+
+                BURN_RELATED_BUNDLE* pRelatedBundle = relatedBundles.rgRelatedBundles + 0;
+                NativeAssert::StringEqual(L"{6DB5D48C-CD7D-40D2-BCBC-AF630E136761}", pRelatedBundle->package.sczId);
+                //{42D16EBE-8B6B-4A9A-9AE9-5300F30011AA} should be missing because it causes an error while processing it. It's important that this doesn't cause initialization to fail.
+            }
+            finally
+            {
+                ReleaseObject(pixeBundle);
+                RegistrationUninitialize(&registration);
+
+                this->testRegistry->TearDown();
+            }
+        }
+
+        void RegisterFakeBundles()
+        {
+            this->RegisterFakeBundle(L"{D54F896D-1952-43E6-9C67-B5652240618C}", L"{89FDAE1F-8CC1-48B9-B930-3945E0D3E7F0}", NULL, L"1.0.0.0", TRUE);
+            this->RegisterFakeBundle(L"{E2355133-384C-4332-9B62-1FA950D707B7}", L"{89FDAE1F-8CC1-48B9-B930-3945E0D3E7F0}", L"", L"1.1.0.0", TRUE);
+            this->RegisterFakeBundle(L"{AD75BE46-B5D7-4208-BC8B-918553C72D83}", L"{89FDAE1F-8CC1-48B9-B930-3945E0D3E7F0}", NULL, L"2.0.0.0", TRUE);
+            this->RegisterFakeBundle(L"{6DB5D48C-CD7D-40D2-BCBC-AF630E136761}", L"{89FDAE1F-8CC1-48B9-B930-3945E0D3E7F0}", NULL, L"3.0.0.0", FALSE);
+            this->RegisterFakeBundle(L"{42D16EBE-8B6B-4A9A-9AE9-5300F30011AA}", L"{89FDAE1F-8CC1-48B9-B930-3945E0D3E7F0}", L"", L"3.1.0.0", FALSE);
+            this->RegisterFakeBundle(L"{3DB49D3D-1FB8-4147-A465-BBE8BFD0DAD0}", L"{89FDAE1F-8CC1-48B9-B930-3945E0D3E7F0}", NULL, L"4.0.0.0", FALSE);
+        }
+
+        void RegisterFakeBundle(LPCWSTR wzBundleId, LPCWSTR wzUpgradeCodes, LPCWSTR wzCachePath, LPCWSTR wzVersion, BOOL fPerMachine)
+        {
+            HRESULT hr = S_OK;
+            LPWSTR* rgsczUpgradeCodes = NULL;
+            DWORD cUpgradeCodes = 0;
+            LPWSTR sczRegistrationKey = NULL;
+            LPWSTR sczCachePath = NULL;
+            HKEY hkRegistration = NULL;
+            HKEY hkRoot = fPerMachine ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER;
+
+            try
+            {
+                hr = StrSplitAllocArray(&rgsczUpgradeCodes, reinterpret_cast<UINT*>(&cUpgradeCodes), wzUpgradeCodes, L";");
+                NativeAssert::Succeeded(hr, "Failed to split upgrade codes.");
+
+                hr = StrAllocFormatted(&sczRegistrationKey, L"%s\\%s", BURN_REGISTRATION_REGISTRY_UNINSTALL_KEY, wzBundleId);
+                NativeAssert::Succeeded(hr, "Failed to build uninstall registry key path.");
+
+                if (!wzCachePath)
+                {
+                    hr = StrAllocFormatted(&sczCachePath, L"%ls.exe", wzBundleId);
+                    NativeAssert::Succeeded(hr, "Failed to build cache path.");
+
+                    wzCachePath = sczCachePath;
+                }
+
+                hr = RegCreate(hkRoot, sczRegistrationKey, KEY_WRITE, &hkRegistration);
+                NativeAssert::Succeeded(hr, "Failed to create registration key.");
+
+                hr = RegWriteStringArray(hkRegistration, BURN_REGISTRATION_REGISTRY_BUNDLE_UPGRADE_CODE, rgsczUpgradeCodes, cUpgradeCodes);
+                NativeAssert::Succeeded(hr, "Failed to write %ls value.", BURN_REGISTRATION_REGISTRY_BUNDLE_UPGRADE_CODE);
+
+                if (wzCachePath && *wzCachePath)
+                {
+                    hr = RegWriteString(hkRegistration, BURN_REGISTRATION_REGISTRY_BUNDLE_CACHE_PATH, wzCachePath);
+                    NativeAssert::Succeeded(hr, "Failed to write %ls value.", BURN_REGISTRATION_REGISTRY_BUNDLE_CACHE_PATH);
+                }
+
+                hr = RegWriteString(hkRegistration, BURN_REGISTRATION_REGISTRY_BUNDLE_VERSION, wzVersion);
+                NativeAssert::Succeeded(hr, "Failed to write %ls value.", BURN_REGISTRATION_REGISTRY_BUNDLE_VERSION);
+            }
+            finally
+            {
+                ReleaseStrArray(rgsczUpgradeCodes, cUpgradeCodes);
+                ReleaseStr(sczRegistrationKey);
+                ReleaseStr(sczCachePath);
+                ReleaseRegKey(hkRegistration);
+            }
+        }
+    };
+}
+}
+}
+}
+}
diff --git a/src/burn/test/BurnUnitTest/precomp.h b/src/burn/test/BurnUnitTest/precomp.h
index ecab3494..ded9fc2d 100644
--- a/src/burn/test/BurnUnitTest/precomp.h
+++ b/src/burn/test/BurnUnitTest/precomp.h
@@ -53,6 +53,7 @@
 #include "update.h"
 #include "pseudobundle.h"
 #include "registration.h"
+#include "relatedbundle.h"
 #include "plan.h"
 #include "pipe.h"
 #include "logging.h"
-- 
cgit v1.2.3-55-g6feb