// 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"


// internal function declarations

static HRESULT DirectorySearchExists(
    __in BURN_SEARCH* pSearch,
    __in BURN_VARIABLES* pVariables
    );
static HRESULT DirectorySearchPath(
    __in BURN_SEARCH* pSearch,
    __in BURN_VARIABLES* pVariables
    );
static HRESULT FileSearchExists(
    __in BURN_SEARCH* pSearch,
    __in BURN_VARIABLES* pVariables
    );
static HRESULT FileSearchVersion(
    __in BURN_SEARCH* pSearch,
    __in BURN_VARIABLES* pVariables
    );
static HRESULT FileSearchPath(
    __in BURN_SEARCH* pSearch,
    __in BURN_VARIABLES* pVariables
    );
static HRESULT RegistrySearchExists(
    __in BURN_SEARCH* pSearch,
    __in BURN_VARIABLES* pVariables
    );
static HRESULT RegistrySearchValue(
    __in BURN_SEARCH* pSearch,
    __in BURN_VARIABLES* pVariables
    );
static HRESULT MsiComponentSearch(
    __in BURN_SEARCH* pSearch,
    __in BURN_VARIABLES* pVariables
    );
static HRESULT MsiProductSearch(
    __in BURN_SEARCH* pSearch,
    __in BURN_VARIABLES* pVariables
    );
static HRESULT PerformExtensionSearch(
    __in BURN_SEARCH* pSearch
    );
static HRESULT PerformSetVariable(
    __in BURN_SEARCH* pSearch,
    __in BURN_VARIABLES* pVariables
);


// function definitions

extern "C" HRESULT SearchesParseFromXml(
    __in BURN_SEARCHES* pSearches,
    __in BURN_EXTENSIONS* pBurnExtensions,
    __in IXMLDOMNode* pixnBundle
    )
{
    HRESULT hr = S_OK;
    IXMLDOMNodeList* pixnNodes = NULL;
    IXMLDOMNode* pixnNode = NULL;
    DWORD cNodes = 0;
    BSTR bstrNodeName = NULL;
    BOOL fXmlFound = FALSE;
    LPWSTR scz = NULL;

    // select search nodes
    hr = XmlSelectNodes(pixnBundle, L"DirectorySearch|FileSearch|RegistrySearch|MsiComponentSearch|MsiProductSearch|MsiFeatureSearch|ExtensionSearch|SetVariable", &pixnNodes);
    ExitOnFailure(hr, "Failed to select search nodes.");

    // get search node count
    hr = pixnNodes->get_length((long*)&cNodes);
    ExitOnRootFailure(hr, "Failed to get search node count.");

    if (!cNodes)
    {
        ExitFunction();
    }

    // allocate memory for searches
    pSearches->rgSearches = (BURN_SEARCH*)MemAlloc(sizeof(BURN_SEARCH) * cNodes, TRUE);
    ExitOnNull(pSearches->rgSearches, hr, E_OUTOFMEMORY, "Failed to allocate memory for search structs.");

    pSearches->cSearches = cNodes;

    // parse search elements
    for (DWORD i = 0; i < cNodes; ++i)
    {
        BURN_SEARCH* pSearch = &pSearches->rgSearches[i];

        hr = XmlNextElement(pixnNodes, &pixnNode, &bstrNodeName);
        ExitOnFailure(hr, "Failed to get next node.");

        // @Id
        hr = XmlGetAttributeEx(pixnNode, L"Id", &pSearch->sczKey);
        ExitOnRequiredXmlQueryFailure(hr, "Failed to get @Id.");

        // @Variable
        hr = XmlGetAttributeEx(pixnNode, L"Variable", &pSearch->sczVariable);
        ExitOnRequiredXmlQueryFailure(hr, "Failed to get @Variable.");

        // @Condition
        hr = XmlGetAttributeEx(pixnNode, L"Condition", &pSearch->sczCondition);
        ExitOnOptionalXmlQueryFailure(hr, fXmlFound, "Failed to get @Condition.");

        // read type specific attributes
        if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, bstrNodeName, -1, L"DirectorySearch", -1))
        {
            pSearch->Type = BURN_SEARCH_TYPE_DIRECTORY;

            // @Path
            hr = XmlGetAttributeEx(pixnNode, L"Path", &pSearch->DirectorySearch.sczPath);
            ExitOnRequiredXmlQueryFailure(hr, "Failed to get @Path.");

            // @Type
            hr = XmlGetAttributeEx(pixnNode, L"Type", &scz);
            ExitOnRequiredXmlQueryFailure(hr, "Failed to get @Type.");

            if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"exists", -1))
            {
                pSearch->DirectorySearch.Type = BURN_DIRECTORY_SEARCH_TYPE_EXISTS;
            }
            else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"path", -1))
            {
                pSearch->DirectorySearch.Type = BURN_DIRECTORY_SEARCH_TYPE_PATH;
            }
            else
            {
                ExitWithRootFailure(hr, E_INVALIDARG, "Invalid value for @Type: %ls", scz);
            }
        }
        else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, bstrNodeName, -1, L"FileSearch", -1))
        {
            pSearch->Type = BURN_SEARCH_TYPE_FILE;

            // @Path
            hr = XmlGetAttributeEx(pixnNode, L"Path", &pSearch->FileSearch.sczPath);
            ExitOnRequiredXmlQueryFailure(hr, "Failed to get @Path.");

            // @DisableFileRedirection
            hr = XmlGetYesNoAttribute(pixnNode, L"DisableFileRedirection", &pSearch->FileSearch.fDisableFileRedirection);
            ExitOnOptionalXmlQueryFailure(hr, fXmlFound, "Failed to get DisableFileRedirection attribute.");

            // @Type
            hr = XmlGetAttributeEx(pixnNode, L"Type", &scz);
            ExitOnRequiredXmlQueryFailure(hr, "Failed to get @Type.");

            if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"exists", -1))
            {
                pSearch->FileSearch.Type = BURN_FILE_SEARCH_TYPE_EXISTS;
            }
            else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"version", -1))
            {
                pSearch->FileSearch.Type = BURN_FILE_SEARCH_TYPE_VERSION;
            }
            else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"path", -1))
            {
                pSearch->FileSearch.Type = BURN_FILE_SEARCH_TYPE_PATH;
            }
            else
            {
                ExitWithRootFailure(hr, E_INVALIDARG, "Invalid value for @Type: %ls", scz);
            }
        }
        else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, bstrNodeName, -1, L"RegistrySearch", -1))
        {
            pSearch->Type = BURN_SEARCH_TYPE_REGISTRY;

            // @Root
            hr = XmlGetAttributeEx(pixnNode, L"Root", &scz);
            ExitOnRequiredXmlQueryFailure(hr, "Failed to get @Root.");

            if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"HKCR", -1))
            {
                pSearch->RegistrySearch.hRoot = HKEY_CLASSES_ROOT;
            }
            else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"HKCU", -1))
            {
                pSearch->RegistrySearch.hRoot = HKEY_CURRENT_USER;
            }
            else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"HKLM", -1))
            {
                pSearch->RegistrySearch.hRoot = HKEY_LOCAL_MACHINE;
            }
            else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"HKU", -1))
            {
                pSearch->RegistrySearch.hRoot = HKEY_USERS;
            }
            else
            {
                ExitWithRootFailure(hr, E_INVALIDARG, "Invalid value for @Root: %ls", scz);
            }

            // @Key
            hr = XmlGetAttributeEx(pixnNode, L"Key", &pSearch->RegistrySearch.sczKey);
            ExitOnRequiredXmlQueryFailure(hr, "Failed to get Key attribute.");

            // @Value
            hr = XmlGetAttributeEx(pixnNode, L"Value", &pSearch->RegistrySearch.sczValue);
            ExitOnOptionalXmlQueryFailure(hr, fXmlFound, "Failed to get Value attribute.");

            // @Type
            hr = XmlGetAttributeEx(pixnNode, L"Type", &scz);
            ExitOnRequiredXmlQueryFailure(hr, "Failed to get @Type.");

            hr = XmlGetYesNoAttribute(pixnNode, L"Win64", &pSearch->RegistrySearch.fWin64);
            ExitOnOptionalXmlQueryFailure(hr, fXmlFound, "Failed to get Win64 attribute.");

            if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"exists", -1))
            {
                pSearch->RegistrySearch.Type = BURN_REGISTRY_SEARCH_TYPE_EXISTS;
            }
            else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"value", -1))
            {
                pSearch->RegistrySearch.Type = BURN_REGISTRY_SEARCH_TYPE_VALUE;

                // @ExpandEnvironment
                hr = XmlGetYesNoAttribute(pixnNode, L"ExpandEnvironment", &pSearch->RegistrySearch.fExpandEnvironment);
                ExitOnOptionalXmlQueryFailure(hr, fXmlFound, "Failed to get @ExpandEnvironment.");

                // @VariableType
                hr = XmlGetAttributeEx(pixnNode, L"VariableType", &scz);
                ExitOnRequiredXmlQueryFailure(hr, "Failed to get @VariableType.");

                if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"formatted", -1))
                {
                    pSearch->RegistrySearch.VariableType = BURN_VARIANT_TYPE_FORMATTED;
                }
                else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"numeric", -1))
                {
                    pSearch->RegistrySearch.VariableType = BURN_VARIANT_TYPE_NUMERIC;
                }
                else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"string", -1))
                {
                    pSearch->RegistrySearch.VariableType = BURN_VARIANT_TYPE_STRING;
                }
                else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"version", -1))
                {
                    pSearch->RegistrySearch.VariableType = BURN_VARIANT_TYPE_VERSION;
                }
                else
                {
                    ExitWithRootFailure(hr, E_INVALIDARG, "Invalid value for @VariableType: %ls", scz);
                }
            }
            else
            {
                ExitWithRootFailure(hr, E_INVALIDARG, "Invalid value for @Type: %ls", scz);
            }
        }
        else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, bstrNodeName, -1, L"MsiComponentSearch", -1))
        {
            pSearch->Type = BURN_SEARCH_TYPE_MSI_COMPONENT;

            // @ProductCode
            hr = XmlGetAttributeEx(pixnNode, L"ProductCode", &pSearch->MsiComponentSearch.sczProductCode);
            ExitOnOptionalXmlQueryFailure(hr, fXmlFound, "Failed to get @ProductCode.");

            // @ComponentId
            hr = XmlGetAttributeEx(pixnNode, L"ComponentId", &pSearch->MsiComponentSearch.sczComponentId);
            ExitOnRequiredXmlQueryFailure(hr, "Failed to get @ComponentId.");

            // @Type
            hr = XmlGetAttributeEx(pixnNode, L"Type", &scz);
            ExitOnRequiredXmlQueryFailure(hr, "Failed to get @Type.");

            if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"keyPath", -1))
            {
                pSearch->MsiComponentSearch.Type = BURN_MSI_COMPONENT_SEARCH_TYPE_KEYPATH;
            }
            else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"state", -1))
            {
                pSearch->MsiComponentSearch.Type = BURN_MSI_COMPONENT_SEARCH_TYPE_STATE;
            }
            else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"directory", -1))
            {
                pSearch->MsiComponentSearch.Type = BURN_MSI_COMPONENT_SEARCH_TYPE_DIRECTORY;
            }
            else
            {
                ExitWithRootFailure(hr, E_INVALIDARG, "Invalid value for @Type: %ls", scz);
            }
        }
        else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, bstrNodeName, -1, L"MsiProductSearch", -1))
        {
            pSearch->Type = BURN_SEARCH_TYPE_MSI_PRODUCT;
            pSearch->MsiProductSearch.GuidType = BURN_MSI_PRODUCT_SEARCH_GUID_TYPE_NONE;

            // @ProductCode (if we don't find a product code then look for an upgrade code)
            hr = XmlGetAttributeEx(pixnNode, L"ProductCode", &pSearch->MsiProductSearch.sczGuid);
            ExitOnOptionalXmlQueryFailure(hr, fXmlFound, "Failed to get @ProductCode.");

            if (fXmlFound)
            {
                pSearch->MsiProductSearch.GuidType = BURN_MSI_PRODUCT_SEARCH_GUID_TYPE_PRODUCTCODE;
            }
            else
            {
                // @UpgradeCode
                hr = XmlGetAttributeEx(pixnNode, L"UpgradeCode", &pSearch->MsiProductSearch.sczGuid);
                ExitOnOptionalXmlQueryFailure(hr, fXmlFound, "Failed to get @UpgradeCode.");

                if (fXmlFound)
                {
                    pSearch->MsiProductSearch.GuidType = BURN_MSI_PRODUCT_SEARCH_GUID_TYPE_UPGRADECODE;
                }
            }

            // make sure we found either a product or upgrade code
            if (BURN_MSI_PRODUCT_SEARCH_GUID_TYPE_NONE == pSearch->MsiProductSearch.GuidType)
            {
                ExitWithRootFailure(hr, E_NOTFOUND, "Failed to get @ProductCode or @UpgradeCode.");
            }

            // @Type
            hr = XmlGetAttributeEx(pixnNode, L"Type", &scz);
            ExitOnRequiredXmlQueryFailure(hr, "Failed to get @Type.");

            if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"version", -1))
            {
                pSearch->MsiProductSearch.Type = BURN_MSI_PRODUCT_SEARCH_TYPE_VERSION;
            }
            else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"language", -1))
            {
                pSearch->MsiProductSearch.Type = BURN_MSI_PRODUCT_SEARCH_TYPE_LANGUAGE;
            }
            else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"state", -1))
            {
                pSearch->MsiProductSearch.Type = BURN_MSI_PRODUCT_SEARCH_TYPE_STATE;
            }
            else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"assignment", -1))
            {
                pSearch->MsiProductSearch.Type = BURN_MSI_PRODUCT_SEARCH_TYPE_ASSIGNMENT;
            }
            else
            {
                ExitWithRootFailure(hr, E_INVALIDARG, "Invalid value for @Type: %ls", scz);
            }
        }
        else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, bstrNodeName, -1, L"ExtensionSearch", -1))
        {
            pSearch->Type = BURN_SEARCH_TYPE_EXTENSION;

            // @ExtensionId
            hr = XmlGetAttributeEx(pixnNode, L"ExtensionId", &scz);
            ExitOnRequiredXmlQueryFailure(hr, "Failed to get @ExtensionId.");

            hr = BurnExtensionFindById(pBurnExtensions, scz, &pSearch->ExtensionSearch.pExtension);
            ExitOnRootFailure(hr, "Failed to find extension '%ls' for search '%ls'", scz, pSearch->sczKey);
        }
        else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, bstrNodeName, -1, L"SetVariable", -1))
        {
            pSearch->Type = BURN_SEARCH_TYPE_SET_VARIABLE;

            // @Value
            hr = XmlGetAttributeEx(pixnNode, L"Value", &scz);
            ExitOnOptionalXmlQueryFailure(hr, fXmlFound, "Failed to get @Value.");

            if (fXmlFound)
            {
                pSearch->SetVariable.sczValue = scz;
                scz = NULL;

                // @Type
                hr = XmlGetAttributeEx(pixnNode, L"Type", &scz);
                ExitOnRequiredXmlQueryFailure(hr, "Failed to get @Type.");

                if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"formatted", -1))
                {
                    pSearch->SetVariable.targetType = BURN_VARIANT_TYPE_FORMATTED;
                }
                else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"numeric", -1))
                {
                    pSearch->SetVariable.targetType = BURN_VARIANT_TYPE_NUMERIC;
                }
                else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"string", -1))
                {
                    pSearch->SetVariable.targetType = BURN_VARIANT_TYPE_STRING;
                }
                else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"version", -1))
                {
                    pSearch->SetVariable.targetType = BURN_VARIANT_TYPE_VERSION;
                }
                else
                {
                    ExitWithRootFailure(hr, E_INVALIDARG, "Invalid value for @Type: %ls", scz);
                }
            }
            else
            {
                pSearch->SetVariable.targetType = BURN_VARIANT_TYPE_NONE;
            }
        }
        else
        {
            ExitWithRootFailure(hr, E_UNEXPECTED, "Unexpected element name: %ls", bstrNodeName);
        }

        // prepare next iteration
        ReleaseNullObject(pixnNode);
        ReleaseNullBSTR(bstrNodeName);
    }

    hr = S_OK;

LExit:
    ReleaseObject(pixnNodes);
    ReleaseObject(pixnNode);
    ReleaseBSTR(bstrNodeName);
    ReleaseStr(scz);
    return hr;
}

extern "C" HRESULT SearchesExecute(
    __in BURN_SEARCHES* pSearches,
    __in BURN_VARIABLES* pVariables
    )
{
    HRESULT hr = S_OK;
    BOOL f = FALSE;

    for (DWORD i = 0; i < pSearches->cSearches; ++i)
    {
        BURN_SEARCH* pSearch = &pSearches->rgSearches[i];

        // evaluate condition
        if (pSearch->sczCondition && *pSearch->sczCondition)
        {
            hr = ConditionEvaluate(pVariables, pSearch->sczCondition, &f);
            if (E_INVALIDDATA == hr)
            {
                TraceError(hr, "Failed to parse search condition. Id = '%ls', Condition = '%ls'", pSearch->sczKey, pSearch->sczCondition);
                hr = S_OK;
                continue;
            }
            ExitOnFailure(hr, "Failed to evaluate search condition. Id = '%ls', Condition = '%ls'", pSearch->sczKey, pSearch->sczCondition);

            if (!f)
            {
                continue; // condition evaluated to false, skip
            }
        }

        switch (pSearch->Type)
        {
        case BURN_SEARCH_TYPE_DIRECTORY:
            switch (pSearch->DirectorySearch.Type)
            {
            case BURN_DIRECTORY_SEARCH_TYPE_EXISTS:
                hr = DirectorySearchExists(pSearch, pVariables);
                break;
            case BURN_DIRECTORY_SEARCH_TYPE_PATH:
                hr = DirectorySearchPath(pSearch, pVariables);
                break;
            default:
                hr = E_UNEXPECTED;
            }
            break;
        case BURN_SEARCH_TYPE_FILE:
            switch (pSearch->FileSearch.Type)
            {
            case BURN_FILE_SEARCH_TYPE_EXISTS:
                hr = FileSearchExists(pSearch, pVariables);
                break;
            case BURN_FILE_SEARCH_TYPE_VERSION:
                hr = FileSearchVersion(pSearch, pVariables);
                break;
            case BURN_FILE_SEARCH_TYPE_PATH:
                hr = FileSearchPath(pSearch, pVariables);
                break;
            default:
                hr = E_UNEXPECTED;
            }
            break;
        case BURN_SEARCH_TYPE_REGISTRY:
            switch (pSearch->RegistrySearch.Type)
            {
            case BURN_REGISTRY_SEARCH_TYPE_EXISTS:
                hr = RegistrySearchExists(pSearch, pVariables);
                break;
            case BURN_REGISTRY_SEARCH_TYPE_VALUE:
                hr = RegistrySearchValue(pSearch, pVariables);
                break;
            default:
                hr = E_UNEXPECTED;
            }
            break;
        case BURN_SEARCH_TYPE_MSI_COMPONENT:
            hr = MsiComponentSearch(pSearch, pVariables);
            break;
        case BURN_SEARCH_TYPE_MSI_PRODUCT:
            hr = MsiProductSearch(pSearch, pVariables);
            break;
        case BURN_SEARCH_TYPE_EXTENSION:
            hr = PerformExtensionSearch(pSearch);
            break;
        case BURN_SEARCH_TYPE_SET_VARIABLE:
            hr = PerformSetVariable(pSearch, pVariables);
            break;
        default:
            hr = E_UNEXPECTED;
        }

        if (FAILED(hr))
        {
            TraceError(hr, "Search failed. Id = '%ls'", pSearch->sczKey);
            continue;
        }
    }

    hr = S_OK;

LExit:
    return hr;
}

extern "C" void SearchesUninitialize(
    __in BURN_SEARCHES* pSearches
    )
{
    if (pSearches->rgSearches)
    {
        for (DWORD i = 0; i < pSearches->cSearches; ++i)
        {
            BURN_SEARCH* pSearch = &pSearches->rgSearches[i];

            ReleaseStr(pSearch->sczKey);
            ReleaseStr(pSearch->sczVariable);
            ReleaseStr(pSearch->sczCondition);

            switch (pSearch->Type)
            {
            case BURN_SEARCH_TYPE_DIRECTORY:
                ReleaseStr(pSearch->DirectorySearch.sczPath);
                break;
            case BURN_SEARCH_TYPE_FILE:
                ReleaseStr(pSearch->FileSearch.sczPath);
                break;
            case BURN_SEARCH_TYPE_REGISTRY:
                ReleaseStr(pSearch->RegistrySearch.sczKey);
                ReleaseStr(pSearch->RegistrySearch.sczValue);
                break;
            case BURN_SEARCH_TYPE_MSI_COMPONENT:
                ReleaseStr(pSearch->MsiComponentSearch.sczProductCode);
                ReleaseStr(pSearch->MsiComponentSearch.sczComponentId);
                break;
            case BURN_SEARCH_TYPE_MSI_PRODUCT:
                ReleaseStr(pSearch->MsiProductSearch.sczGuid);
                break;
            case BURN_SEARCH_TYPE_SET_VARIABLE:
                ReleaseStr(pSearch->SetVariable.sczValue);
                break;
            }
        }
        MemFree(pSearches->rgSearches);
    }
}


// internal function definitions

#if !defined(_WIN64)

typedef struct _BURN_FILE_SEARCH
{
    BURN_SEARCH* pSearch;
    PROC_FILESYSTEMREDIRECTION pfsr;
} BURN_FILE_SEARCH;

static HRESULT FileSystemSearchStart(
    __in BURN_FILE_SEARCH* pFileSearch
    )
{
    HRESULT hr = S_OK;

    if (pFileSearch->pSearch->FileSearch.fDisableFileRedirection)
    {
        hr = ProcDisableWowFileSystemRedirection(&pFileSearch->pfsr);
        if (hr == E_NOTIMPL)
        {
            hr = S_FALSE;
        }
        ExitOnFailure(hr, "Failed to disable file system redirection.");
    }

LExit:
    return hr;
}

static void FileSystemSearchEnd(
    __in BURN_FILE_SEARCH* pFileSearch
    )
{
    HRESULT hr = S_OK;

    hr = ProcRevertWowFileSystemRedirection(&pFileSearch->pfsr);
    ExitOnFailure(hr, "Failed to revert file system redirection.");

LExit:
    return;
}

#endif

static HRESULT DirectorySearchExists(
    __in BURN_SEARCH* pSearch,
    __in BURN_VARIABLES* pVariables
    )
{
    HRESULT hr = S_OK;
    DWORD er = ERROR_SUCCESS;
    LPWSTR sczPath = NULL;
    BOOL fExists = FALSE;

#if !defined(_WIN64)
    BURN_FILE_SEARCH bfs = { };

    bfs.pSearch = pSearch;

    hr = FileSystemSearchStart(&bfs);
    ExitOnFailure(hr, "Failed to initialize file search.");
#endif

    // format path
    hr = VariableFormatString(pVariables, pSearch->DirectorySearch.sczPath, &sczPath, NULL);
    ExitOnFailure(hr, "Failed to format variable string.");

    DWORD dwAttributes = ::GetFileAttributesW(sczPath);
    if (INVALID_FILE_ATTRIBUTES == dwAttributes)
    {
        er = ::GetLastError();
        if (ERROR_FILE_NOT_FOUND == er || ERROR_PATH_NOT_FOUND == er)
        {
            LogStringLine(REPORT_STANDARD, "Directory search: %ls, did not find path: %ls", pSearch->sczKey, pSearch->DirectorySearch.sczPath);
        }
        else
        {
            ExitOnWin32Error(er, hr, "Directory search: %ls, failed get to directory attributes. '%ls'", pSearch->sczKey, pSearch->DirectorySearch.sczPath);
        }
    }
    else if (FILE_ATTRIBUTE_DIRECTORY != (dwAttributes & FILE_ATTRIBUTE_DIRECTORY))
    {
        LogStringLine(REPORT_STANDARD, "Directory search: %ls, found file at path: %ls", pSearch->sczKey, pSearch->DirectorySearch.sczPath);
    }
    else
    {
        fExists = TRUE;
    }

    // set variable
    hr = VariableSetNumeric(pVariables, pSearch->sczVariable, fExists, FALSE);
    ExitOnFailure(hr, "Failed to set variable.");

LExit:
#if !defined(_WIN64)
    FileSystemSearchEnd(&bfs);
#endif

    StrSecureZeroFreeString(sczPath);

    return hr;
}

static HRESULT DirectorySearchPath(
    __in BURN_SEARCH* pSearch,
    __in BURN_VARIABLES* pVariables
    )
{
    HRESULT hr = S_OK;
    LPWSTR sczPath = NULL;

#if !defined(_WIN64)
    BURN_FILE_SEARCH bfs = { };

    bfs.pSearch = pSearch;

    hr = FileSystemSearchStart(&bfs);
    ExitOnFailure(hr, "Failed to initialize file search.");
#endif

    // format path
    hr = VariableFormatString(pVariables, pSearch->DirectorySearch.sczPath, &sczPath, NULL);
    ExitOnFailure(hr, "Failed to format variable string.");

    DWORD dwAttributes = ::GetFileAttributesW(sczPath);
    if (INVALID_FILE_ATTRIBUTES == dwAttributes)
    {
        hr = HRESULT_FROM_WIN32(::GetLastError());
    }
    else if (dwAttributes & FILE_ATTRIBUTE_DIRECTORY)
    {
        hr = VariableSetString(pVariables, pSearch->sczVariable, sczPath, FALSE, FALSE);
        ExitOnFailure(hr, "Failed to set directory search path variable.");
    }
    else // must have found a file.
    {
        hr = E_PATHNOTFOUND;
    }

    if (E_FILENOTFOUND == hr || E_PATHNOTFOUND == hr)
    {
        LogStringLine(REPORT_STANDARD, "Directory search: %ls, did not find path: %ls, reason: 0x%x", pSearch->sczKey, pSearch->DirectorySearch.sczPath, hr);
        ExitFunction1(hr = S_OK);
    }
    ExitOnFailure(hr, "Failed while searching directory search: %ls, for path: %ls", pSearch->sczKey, pSearch->DirectorySearch.sczPath);

LExit:
#if !defined(_WIN64)
    FileSystemSearchEnd(&bfs);
#endif

    StrSecureZeroFreeString(sczPath);

    return hr;
}

static HRESULT FileSearchExists(
    __in BURN_SEARCH* pSearch,
    __in BURN_VARIABLES* pVariables
    )
{
    HRESULT hr = S_OK;
    DWORD er = ERROR_SUCCESS;
    LPWSTR sczPath = NULL;
    BOOL fExists = FALSE;

#if !defined(_WIN64)
    BURN_FILE_SEARCH bfs = { };

    bfs.pSearch = pSearch;

    hr = FileSystemSearchStart(&bfs);
    ExitOnFailure(hr, "Failed to initialize file search.");
#endif

    // format path
    hr = VariableFormatString(pVariables, pSearch->FileSearch.sczPath, &sczPath, NULL);
    ExitOnFailure(hr, "Failed to format variable string.");

    // find file
    DWORD dwAttributes = ::GetFileAttributesW(sczPath);
    if (INVALID_FILE_ATTRIBUTES == dwAttributes)
    {
        er = ::GetLastError();
        if (ERROR_FILE_NOT_FOUND == er || ERROR_PATH_NOT_FOUND == er)
        {
            LogStringLine(REPORT_STANDARD, "File search: %ls, did not find path: %ls", pSearch->sczKey, pSearch->FileSearch.sczPath);
        }
        else
        {
            ExitOnWin32Error(er, hr, "File search: %ls, failed get to file attributes. '%ls'", pSearch->sczKey, pSearch->FileSearch.sczPath);
        }
    }
    else if (FILE_ATTRIBUTE_DIRECTORY == (dwAttributes & FILE_ATTRIBUTE_DIRECTORY))
    {
        LogStringLine(REPORT_STANDARD, "File search: %ls, found directory at path: %ls", pSearch->sczKey, pSearch->FileSearch.sczPath);
    }
    else
    {
        fExists = TRUE;
    }

    // set variable
    hr = VariableSetNumeric(pVariables, pSearch->sczVariable, fExists, FALSE);
    ExitOnFailure(hr, "Failed to set variable.");

LExit:
#if !defined(_WIN64)
    FileSystemSearchEnd(&bfs);
#endif

    StrSecureZeroFreeString(sczPath);
    return hr;
}

static HRESULT FileSearchVersion(
    __in BURN_SEARCH* pSearch,
    __in BURN_VARIABLES* pVariables
    )
{
    HRESULT hr = S_OK;
    ULARGE_INTEGER uliVersion = { };
    LPWSTR sczPath = NULL;
    VERUTIL_VERSION* pVersion = NULL;

#if !defined(_WIN64)
    BURN_FILE_SEARCH bfs = { };

    bfs.pSearch = pSearch;

    hr = FileSystemSearchStart(&bfs);
    ExitOnFailure(hr, "Failed to initialize file search.");
#endif

    // format path
    hr = VariableFormatString(pVariables, pSearch->FileSearch.sczPath, &sczPath, NULL);
    ExitOnFailure(hr, "Failed to format path string.");

    // get file version
    hr = FileVersion(sczPath, &uliVersion.HighPart, &uliVersion.LowPart);
    if (E_FILENOTFOUND == hr || E_PATHNOTFOUND == hr)
    {
        LogStringLine(REPORT_STANDARD, "File search: %ls, did not find path: %ls", pSearch->sczKey, pSearch->FileSearch.sczPath);
        ExitFunction1(hr = S_OK);
    }
    ExitOnFailure(hr, "Failed to get file version.");

    hr = VerVersionFromQword(uliVersion.QuadPart, &pVersion);
    ExitOnFailure(hr, "Failed to create version from file version.");

    // set variable
    hr = VariableSetVersion(pVariables, pSearch->sczVariable, pVersion, FALSE);
    ExitOnFailure(hr, "Failed to set variable.");

LExit:
#if !defined(_WIN64)
    FileSystemSearchEnd(&bfs);
#endif

    StrSecureZeroFreeString(sczPath);
    ReleaseVerutilVersion(pVersion);
    return hr;
}

static HRESULT FileSearchPath(
    __in BURN_SEARCH* pSearch,
    __in BURN_VARIABLES* pVariables
    )
{
    HRESULT hr = S_OK;
    LPWSTR sczPath = NULL;

#if !defined(_WIN64)
    BURN_FILE_SEARCH bfs = { };

    bfs.pSearch = pSearch;

    hr = FileSystemSearchStart(&bfs);
    ExitOnFailure(hr, "Failed to initialize file search.");
#endif

    // format path
    hr = VariableFormatString(pVariables, pSearch->FileSearch.sczPath, &sczPath, NULL);
    ExitOnFailure(hr, "Failed to format variable string.");

    DWORD dwAttributes = ::GetFileAttributesW(sczPath);
    if (INVALID_FILE_ATTRIBUTES == dwAttributes)
    {
        hr = HRESULT_FROM_WIN32(::GetLastError());
    }
    else if (dwAttributes & FILE_ATTRIBUTE_DIRECTORY) // found a directory.
    {
        hr = E_FILENOTFOUND;
    }
    else // found our file.
    {
        hr = VariableSetString(pVariables, pSearch->sczVariable, sczPath, FALSE, FALSE);
        ExitOnFailure(hr, "Failed to set variable to file search path.");
    }

    if (E_FILENOTFOUND == hr || E_PATHNOTFOUND == hr)
    {
        LogStringLine(REPORT_STANDARD, "File search: %ls, did not find path: %ls", pSearch->sczKey, pSearch->FileSearch.sczPath);
        ExitFunction1(hr = S_OK);
    }
    ExitOnFailure(hr, "Failed while searching file search: %ls, for path: %ls", pSearch->sczKey, pSearch->FileSearch.sczPath);

LExit:
#if !defined(_WIN64)
    FileSystemSearchEnd(&bfs);
#endif

    StrSecureZeroFreeString(sczPath);

    return hr;
}

static HRESULT RegistrySearchExists(
    __in BURN_SEARCH* pSearch,
    __in BURN_VARIABLES* pVariables
    )
{
    HRESULT hr = S_OK;
    DWORD er = ERROR_SUCCESS;
    LPWSTR sczKey = NULL;
    LPWSTR sczValue = NULL;
    HKEY hKey = NULL;
    DWORD dwType = 0;
    BOOL fExists = FALSE;

    // format key string
    hr = VariableFormatString(pVariables, pSearch->RegistrySearch.sczKey, &sczKey, NULL);
    ExitOnFailure(hr, "Failed to format key string.");

    // open key
    hr = RegOpenEx(pSearch->RegistrySearch.hRoot, sczKey, KEY_QUERY_VALUE, pSearch->RegistrySearch.fWin64 ? REG_KEY_64BIT : REG_KEY_32BIT, &hKey);
    ExitOnPathFailure(hr, fExists, "Failed to open registry key. Key = '%ls'", pSearch->RegistrySearch.sczKey);

    if (!fExists)
    {
        LogStringLine(REPORT_STANDARD, "Registry key not found. Key = '%ls'", pSearch->RegistrySearch.sczKey);
    }
    else if (pSearch->RegistrySearch.sczValue)
    {
        // format value string
        hr = VariableFormatString(pVariables, pSearch->RegistrySearch.sczValue, &sczValue, NULL);
        ExitOnFailure(hr, "Failed to format value string.");

        // query value
        er = ::RegQueryValueExW(hKey, sczValue, NULL, &dwType, NULL, NULL);
        switch (er)
        {
        case ERROR_SUCCESS:
            fExists = TRUE;
            break;
        case ERROR_FILE_NOT_FOUND:
            LogStringLine(REPORT_STANDARD, "Registry value not found. Key = '%ls', Value = '%ls'", pSearch->RegistrySearch.sczKey, pSearch->RegistrySearch.sczValue);
            fExists = FALSE;
            break;
        default:
            ExitOnWin32Error(er, hr, "Failed to query registry key value.");
        }
    }

    // set variable
    hr = VariableSetNumeric(pVariables, pSearch->sczVariable, fExists, FALSE);
    ExitOnFailure(hr, "Failed to set variable.");

LExit:
    if (FAILED(hr))
    {
        LogStringLine(REPORT_STANDARD, "RegistrySearchExists failed: ID '%ls', HRESULT 0x%x", pSearch->sczKey, hr);
    }

    StrSecureZeroFreeString(sczKey);
    StrSecureZeroFreeString(sczValue);
    ReleaseRegKey(hKey);

    return hr;
}

static HRESULT RegistrySearchValue(
    __in BURN_SEARCH* pSearch,
    __in BURN_VARIABLES* pVariables
    )
{
    HRESULT hr = S_OK;
    LPWSTR sczKey = NULL;
    LPWSTR sczValue = NULL;
    HKEY hKey = NULL;
    BOOL fExists = FALSE;
    DWORD dwType = 0;
    SIZE_T cbData = 0;
    LPBYTE pData = NULL;
    BURN_VARIANT value = { };
    DWORD dwValue = 0;
    LONGLONG llValue = 0;

    // format key string
    hr = VariableFormatString(pVariables, pSearch->RegistrySearch.sczKey, &sczKey, NULL);
    ExitOnFailure(hr, "Failed to format key string.");

    // format value string
    if (pSearch->RegistrySearch.sczValue)
    {
        hr = VariableFormatString(pVariables, pSearch->RegistrySearch.sczValue, &sczValue, NULL);
        ExitOnFailure(hr, "Failed to format value string.");
    }

    // open key
    hr = RegOpenEx(pSearch->RegistrySearch.hRoot, sczKey, KEY_QUERY_VALUE, pSearch->RegistrySearch.fWin64 ? REG_KEY_64BIT : REG_KEY_32BIT, &hKey);
    ExitOnPathFailure(hr, fExists, "Failed to open registry key.");

    if (!fExists)
    {
        LogStringLine(REPORT_STANDARD, "Registry key not found. Key = '%ls'", pSearch->RegistrySearch.sczKey);

        ExitFunction();
    }

    // get value
    hr = RegReadValue(hKey, sczValue, pSearch->RegistrySearch.fExpandEnvironment, &pData, &cbData, &dwType);
    if (E_FILENOTFOUND == hr)
    {
        LogStringLine(REPORT_STANDARD, "Registry value not found. Key = '%ls', Value = '%ls'", pSearch->RegistrySearch.sczKey, pSearch->RegistrySearch.sczValue);

        ExitFunction1(hr = S_OK);
    }
    ExitOnFailure(hr, "Failed to query registry key value.");

    switch (dwType)
    {
    case REG_DWORD:
        if (memcpy_s(&dwValue, sizeof(DWORD), pData, cbData))
        {
            ExitFunction1(hr = E_UNEXPECTED);
        }
        hr = BVariantSetNumeric(&value, dwValue);
        break;
    case REG_QWORD:
        if (memcpy_s(&llValue, sizeof(LONGLONG), pData, cbData))
        {
            ExitFunction1(hr = E_UNEXPECTED);
        }
        hr = BVariantSetNumeric(&value, llValue);
        break;
    case REG_EXPAND_SZ: __fallthrough;
    case REG_SZ:
        hr = BVariantSetString(&value, (LPCWSTR)pData, 0, FALSE);
        break;
    default:
        ExitWithRootFailure(hr, E_NOTIMPL, "Unsupported registry key value type. Type = '%u'", dwType);
    }
    ExitOnFailure(hr, "Failed to read registry value.");

    // change value to requested type
    hr = BVariantChangeType(&value, pSearch->RegistrySearch.VariableType);
    ExitOnFailure(hr, "Failed to change value type.");

    // Set variable.
    hr = VariableSetVariant(pVariables, pSearch->sczVariable, &value);
    ExitOnFailure(hr, "Failed to set variable.");

LExit:
    if (FAILED(hr))
    {
        LogStringLine(REPORT_STANDARD, "RegistrySearchValue failed: ID '%ls', HRESULT 0x%x", pSearch->sczKey, hr);
    }

    StrSecureZeroFreeString(sczKey);
    StrSecureZeroFreeString(sczValue);
    ReleaseRegKey(hKey);
    ReleaseMem(pData);
    BVariantUninitialize(&value);

    return hr;
}

static HRESULT MsiComponentSearch(
    __in BURN_SEARCH* pSearch,
    __in BURN_VARIABLES* pVariables
    )
{
    HRESULT hr = S_OK;
    INSTALLSTATE is = INSTALLSTATE_BROKEN;
    LPWSTR sczComponentId = NULL;
    LPWSTR sczProductCode = NULL;
    LPWSTR sczPath = NULL;

    // format component id string
    hr = VariableFormatString(pVariables, pSearch->MsiComponentSearch.sczComponentId, &sczComponentId, NULL);
    ExitOnFailure(hr, "Failed to format component id string.");

    if (pSearch->MsiComponentSearch.sczProductCode)
    {
        // format product code string
        hr = VariableFormatString(pVariables, pSearch->MsiComponentSearch.sczProductCode, &sczProductCode, NULL);
        ExitOnFailure(hr, "Failed to format product code string.");
    }

    if (sczProductCode)
    {
        hr = WiuGetComponentPath(sczProductCode, sczComponentId, &is, &sczPath);
    }
    else
    {
        hr = WiuLocateComponent(sczComponentId, &is, &sczPath);
    }

    if (INSTALLSTATE_SOURCEABSENT == is)
    {
        is = INSTALLSTATE_SOURCE;
    }
    else if (INSTALLSTATE_UNKNOWN == is || INSTALLSTATE_NOTUSED == is)
    {
        is = INSTALLSTATE_ABSENT;
    }
    else if (INSTALLSTATE_ABSENT != is && INSTALLSTATE_LOCAL != is && INSTALLSTATE_SOURCE != is)
    {
        hr = E_INVALIDARG;
        ExitOnFailure(hr, "Failed to get component path: %d", is);
    }

    // set variable
    switch (pSearch->MsiComponentSearch.Type)
    {
    case BURN_MSI_COMPONENT_SEARCH_TYPE_KEYPATH:
        if (INSTALLSTATE_ABSENT == is || INSTALLSTATE_LOCAL == is || INSTALLSTATE_SOURCE == is)
        {
            hr = VariableSetString(pVariables, pSearch->sczVariable, sczPath, FALSE, FALSE);
        }
        break;
    case BURN_MSI_COMPONENT_SEARCH_TYPE_STATE:
        hr = VariableSetNumeric(pVariables, pSearch->sczVariable, is, FALSE);
        break;
    case BURN_MSI_COMPONENT_SEARCH_TYPE_DIRECTORY:
        if (INSTALLSTATE_ABSENT == is || INSTALLSTATE_LOCAL == is || INSTALLSTATE_SOURCE == is)
        {
            // remove file part from path, if any
            LPWSTR wz = wcsrchr(sczPath, L'\\');
            if (wz)
            {
                wz[1] = L'\0';
            }

            hr = VariableSetString(pVariables, pSearch->sczVariable, sczPath, FALSE, FALSE);
        }
        break;
    }
    ExitOnFailure(hr, "Failed to set variable.");

LExit:
    if (FAILED(hr))
    {
        LogStringLine(REPORT_STANDARD, "MsiComponentSearch failed: ID '%ls', HRESULT 0x%x", pSearch->sczKey, hr);
    }

    StrSecureZeroFreeString(sczComponentId);
    StrSecureZeroFreeString(sczProductCode);
    ReleaseStr(sczPath);
    return hr;
}

static HRESULT MsiProductSearch(
    __in BURN_SEARCH* pSearch,
    __in BURN_VARIABLES* pVariables
    )
{
    HRESULT hr = S_OK;
    LPWSTR sczGuid = NULL;
    LPCWSTR wzProperty = NULL;
    LPWSTR *rgsczRelatedProductCodes = NULL;
    DWORD dwRelatedProducts = 0;
    BURN_VARIANT_TYPE type = BURN_VARIANT_TYPE_NONE;
    BURN_VARIANT value = { };

    switch (pSearch->MsiProductSearch.Type)
    {
    case BURN_MSI_PRODUCT_SEARCH_TYPE_VERSION:
        wzProperty = INSTALLPROPERTY_VERSIONSTRING;
        break;
    case BURN_MSI_PRODUCT_SEARCH_TYPE_LANGUAGE:
        wzProperty = INSTALLPROPERTY_LANGUAGE;
        break;
    case BURN_MSI_PRODUCT_SEARCH_TYPE_STATE:
        wzProperty = INSTALLPROPERTY_PRODUCTSTATE;
        break;
    case BURN_MSI_PRODUCT_SEARCH_TYPE_ASSIGNMENT:
        wzProperty = INSTALLPROPERTY_ASSIGNMENTTYPE;
        break;
    default:
        ExitOnFailure(hr = E_NOTIMPL, "Unsupported product search type: %u", pSearch->MsiProductSearch.Type);
    }

    // format guid string
    hr = VariableFormatString(pVariables, pSearch->MsiProductSearch.sczGuid, &sczGuid, NULL);
    ExitOnFailure(hr, "Failed to format GUID string.");

    // get product info
    value.Type = BURN_VARIANT_TYPE_STRING;

    // if this is an upgrade code then get the product code of the highest versioned related product
    if (BURN_MSI_PRODUCT_SEARCH_GUID_TYPE_UPGRADECODE == pSearch->MsiProductSearch.GuidType)
    {
        // WiuEnumRelatedProductCodes will log sczGuid on errors, what if there's a hidden variable in there?
        hr = WiuEnumRelatedProductCodes(sczGuid, &rgsczRelatedProductCodes, &dwRelatedProducts, TRUE);
        ExitOnFailure(hr, "Failed to enumerate related products for upgrade code.");

        // if we actually found a related product then use its upgrade code for the rest of the search
        if (1 == dwRelatedProducts)
        {
            hr = StrAllocStringSecure(&sczGuid, rgsczRelatedProductCodes[0], 0);
            ExitOnFailure(hr, "Failed to copy upgrade code.");
        }
        else
        {
            // set this here so we have a way of knowing that we don't need to bother
            // querying for the product information below
            hr = HRESULT_FROM_WIN32(ERROR_UNKNOWN_PRODUCT);
        }
    }

    if (HRESULT_FROM_WIN32(ERROR_UNKNOWN_PRODUCT) != hr)
    {
        hr = WiuGetProductInfo(sczGuid, wzProperty, &value.sczValue);
        if (HRESULT_FROM_WIN32(ERROR_UNKNOWN_PROPERTY) == hr)
        {
            // product state is available only through MsiGetProductInfoEx
            // What if there is a hidden variable in sczGuid?
            LogStringLine(REPORT_VERBOSE, "Trying per-machine extended info for property '%ls' for product: %ls", wzProperty, sczGuid);
            hr = WiuGetProductInfoEx(sczGuid, NULL, MSIINSTALLCONTEXT_MACHINE, wzProperty, &value.sczValue);

            // if not in per-machine context, try per-user (unmanaged)
            if (HRESULT_FROM_WIN32(ERROR_UNKNOWN_PRODUCT) == hr)
            {
                // What if there is a hidden variable in sczGuid?
                LogStringLine(REPORT_STANDARD, "Trying per-user extended info for property '%ls' for product: %ls", wzProperty, sczGuid);
                hr = WiuGetProductInfoEx(sczGuid, NULL, MSIINSTALLCONTEXT_USERUNMANAGED, wzProperty, &value.sczValue);
            }
        }
    }

    if (HRESULT_FROM_WIN32(ERROR_UNKNOWN_PRODUCT) == hr)
    {
        // What if there is a hidden variable in sczGuid?
        LogStringLine(REPORT_STANDARD, "Product or related product not found: %ls", sczGuid);

        // set value to indicate absent
        switch (pSearch->MsiProductSearch.Type)
        {
        case BURN_MSI_PRODUCT_SEARCH_TYPE_ASSIGNMENT: __fallthrough;
        case BURN_MSI_PRODUCT_SEARCH_TYPE_VERSION:
            value.Type = BURN_VARIANT_TYPE_NUMERIC;
            value.llValue = 0;
            break;
        case BURN_MSI_PRODUCT_SEARCH_TYPE_LANGUAGE:
            // is supposed to remain empty
            break;
        case BURN_MSI_PRODUCT_SEARCH_TYPE_STATE:
            value.Type = BURN_VARIANT_TYPE_NUMERIC;
            value.llValue = INSTALLSTATE_ABSENT;
            break;
        }

        hr = S_OK;
    }
    ExitOnFailure(hr, "Failed to get product info.");

    // change value type
    switch (pSearch->MsiProductSearch.Type)
    {
    case BURN_MSI_PRODUCT_SEARCH_TYPE_VERSION:
        type = BURN_VARIANT_TYPE_VERSION;
        break;
    case BURN_MSI_PRODUCT_SEARCH_TYPE_LANGUAGE:
        type = BURN_VARIANT_TYPE_STRING;
        break;
    case BURN_MSI_PRODUCT_SEARCH_TYPE_STATE: __fallthrough;
    case BURN_MSI_PRODUCT_SEARCH_TYPE_ASSIGNMENT:
        type = BURN_VARIANT_TYPE_NUMERIC;
        break;
    }
    hr = BVariantChangeType(&value, type);
    ExitOnFailure(hr, "Failed to change value type.");

    // Set variable.
    hr = VariableSetVariant(pVariables, pSearch->sczVariable, &value);
    ExitOnFailure(hr, "Failed to set variable.");

LExit:
    if (FAILED(hr))
    {
        LogStringLine(REPORT_STANDARD, "MsiProductSearch failed: ID '%ls', HRESULT 0x%x", pSearch->sczKey, hr);
    }

    StrSecureZeroFreeString(sczGuid);
    ReleaseStrArray(rgsczRelatedProductCodes, dwRelatedProducts);
    BVariantUninitialize(&value);

    return hr;
}

static HRESULT PerformExtensionSearch(
    __in BURN_SEARCH* pSearch
    )
{
    HRESULT hr = S_OK;

    hr = BurnExtensionPerformSearch(pSearch->ExtensionSearch.pExtension, pSearch->sczKey, pSearch->sczVariable);

    return hr;
}

static HRESULT PerformSetVariable(
    __in BURN_SEARCH* pSearch,
    __in BURN_VARIABLES* pVariables
    )
{
    HRESULT hr = S_OK;
    BURN_VARIANT newValue = { };
    LPWSTR sczFormattedValue = NULL;
    SIZE_T cchOut = 0;

    if (BURN_VARIANT_TYPE_NONE == pSearch->SetVariable.targetType)
    {
        BVariantUninitialize(&newValue);
    }
    else
    {
        hr = VariableFormatString(pVariables, pSearch->SetVariable.sczValue, &sczFormattedValue, &cchOut);
        ExitOnFailure(hr, "Failed to format search value.");

        hr = BVariantSetString(&newValue, sczFormattedValue, 0, FALSE);
        ExitOnFailure(hr, "Failed to set variant value.");

        // change value variant to correct type
        hr = BVariantChangeType(&newValue, pSearch->SetVariable.targetType);
        ExitOnFailure(hr, "Failed to change variant type.");
    }

    hr = VariableSetVariant(pVariables, pSearch->sczVariable, &newValue);
    ExitOnFailure(hr, "Failed to set variable: %ls", pSearch->sczVariable);

LExit:
    BVariantUninitialize(&newValue);

    return hr;
}