/* **********************************************************
 * Copyright (c) 2014-2022 Google, Inc.  All rights reserved.
 * Copyright (c) 2005-2010 VMware, Inc.  All rights reserved.
 * **********************************************************/

/*
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * * Neither the name of VMware, Inc. nor the names of its contributors may be
 *   used to endorse or promote products derived from this software without
 *   specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL VMWARE, INC. OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
 * DAMAGE.
 */

/*
 *
 * config.c: general configuration interface
 *
 */

#include "share.h"
#include "config.h"
#include "processes.h"
#include "string.h"
#include "parser.h"
#include <stdio.h>

#ifndef UNIT_TEST

void
configpath_to_registry_path(WCHAR *path)
{
    UINT i = 0;

    if (NULL == path)
        return;

    for (i = 0; i < wcslen(path); i++)
        if (CONFIG_PATH_SEPARATOR == path[i])
            path[i] = L'\\';
}

DWORD
get_key_handle(HKEY *key, HKEY parent, const WCHAR *path, BOOL absolute, DWORD flags)
{
    WCHAR keyname[MAX_PATH];

    DO_ASSERT(key != NULL);

    if (path == NULL) {
        wcsncpy(keyname, CONFIGURATION_ROOT_REGISTRY_KEY, MAX_PATH);
    } else if (parent == NULL && !absolute) {
        _snwprintf(keyname, MAX_PATH, L"%s\\%s", CONFIGURATION_ROOT_REGISTRY_KEY, path);
    } else {
        wcsncpy(keyname, path, MAX_PATH);
    }

    DO_DEBUG(DL_VERB, printf("get_key_handle using %S as keyname\n", keyname););

    configpath_to_registry_path(keyname);

    return RegCreateKeyEx(parent == NULL ? DYNAMORIO_REGISTRY_HIVE : parent, keyname, 0,
                          NULL, REG_OPTION_NON_VOLATILE, platform_key_flags() | flags,
                          NULL, key, NULL);
}

ConfigGroup *
get_child(const WCHAR *name, ConfigGroup *c)
{
    ConfigGroup *cur = NULL;

    if (c == NULL)
        return NULL;

    for (cur = c->children; NULL != cur; cur = cur->next) {
        if (0 == wcsicmp(name, cur->name))
            return cur;
    }

    return NULL;
}

void
remove_child(const WCHAR *child, ConfigGroup *config)
{
    ConfigGroup *cur = NULL, *prev = NULL;

    if (NULL == config)
        return;

    for (cur = config->children; NULL != cur; cur = cur->next) {
        if (0 == wcsicmp(child, cur->name)) {
            if (NULL == prev)
                config->children = cur->next;
            else
                prev->next = cur->next;

            if (config->lastchild == cur)
                config->lastchild = prev;

            free_config_group(cur);

            return;
        }
        prev = cur;
    }
}

BOOL
count_params(ConfigGroup *c)
{
    int i = 0;
    NameValuePairNode *cur;
    for (cur = c->params; NULL != cur; cur = cur->next)
        i++;
    return i;
}

BOOL
is_param(const WCHAR *name, ConfigGroup *c)
{
    NameValuePairNode *cur;

    if (c == NULL)
        return FALSE;

    for (cur = c->params; NULL != cur; cur = cur->next) {
        if (0 == wcsicmp(name, cur->name))
            return TRUE;
    }

    return FALSE;
}

ConfigGroup *
new_config_group(const WCHAR *name)
{
    ConfigGroup *c = (ConfigGroup *)malloc(sizeof(ConfigGroup));
    c->next = NULL;
    c->children = NULL;
    c->lastchild = NULL;
    c->params = NULL;
    c->should_clear = FALSE;
    c->name = (NULL == name) ? NULL : wcsdup(name);

    return c;
}

ConfigGroup *
copy_config_group(ConfigGroup *config, BOOL deep)
{
    NameValuePairNode *tmpnvp;
    ConfigGroup *tmpcfg;

    ConfigGroup *c = new_config_group(config->name);
    c->should_clear = config->should_clear;
    tmpnvp = config->params;
    while (tmpnvp != NULL) {
        set_config_group_parameter(c, tmpnvp->name, tmpnvp->value);
        tmpnvp = tmpnvp->next;
    }

    if (deep) {
        tmpcfg = config->children;
        while (tmpcfg != NULL) {
            add_config_group(c, copy_config_group(tmpcfg, deep));
            tmpcfg = tmpcfg->next;
        }
    }

    return c;
}

void
remove_children(ConfigGroup *config)
{
    ConfigGroup *cur = config->children, *prev;

    while (NULL != cur) {
        prev = cur;
        cur = cur->next;
        free_config_group(prev);
    }

    config->children = NULL;
    config->lastchild = NULL;
}

NameValuePairNode *
new_nvp_node(const WCHAR *name)
{
    NameValuePairNode *nvp = (NameValuePairNode *)malloc(sizeof(NameValuePairNode));
    nvp->next = NULL;
    nvp->name = wcsdup(name);
    nvp->value = NULL;

    return nvp;
}

void
free_nvp(NameValuePairNode *nvpn)
{
    if (nvpn->name != NULL)
        free(nvpn->name);

    if (nvpn->value != NULL)
        free(nvpn->value);

    free(nvpn);
}

void
free_config_group(ConfigGroup *config)
{
    NameValuePairNode *nvpn = config->params, *tmp;

    remove_children(config);

    while (nvpn != NULL) {
        tmp = nvpn->next;
        free_nvp(nvpn);
        nvpn = tmp;
    }

    if (config->name != NULL)
        free(config->name);

    free(config);
}

NameValuePairNode *
get_nvp_node(ConfigGroup *config, const WCHAR *name)
{
    NameValuePairNode *nvpn = config->params;
    while (NULL != nvpn && 0 != wcsicmp(nvpn->name, name))
        nvpn = nvpn->next;
    return nvpn;
}

WCHAR *
get_config_group_parameter(ConfigGroup *config, const WCHAR *name)
{
    NameValuePairNode *nvpn = get_nvp_node(config, name);
    return (NULL == nvpn) ? NULL : nvpn->value;
}

NameValuePairNode *
add_nvp_node(ConfigGroup *config, const WCHAR *name)
{
    NameValuePairNode *nvpn = new_nvp_node(name);
    nvpn->next = config->params;
    config->params = nvpn;
    return nvpn;
}

void
set_config_group_parameter(ConfigGroup *config, const WCHAR *name, const WCHAR *value)
{
    NameValuePairNode *nvpn = get_nvp_node(config, name);

    if (NULL == nvpn) {
        nvpn = add_nvp_node(config, name);
    }

    if (NULL != nvpn->value) {
        free(nvpn->value);
        nvpn->value = NULL;
    }

    nvpn->value = (NULL == value) ? NULL : wcsdup(value);
}

void
remove_config_group_parameter(ConfigGroup *config, const WCHAR *name)
{
    NameValuePairNode *cur = NULL, *prev = NULL;

    if (NULL == config || NULL == name)
        return;

    for (cur = config->params; NULL != cur; cur = cur->next) {
        if (0 == wcsicmp(cur->name, name)) {
            if (NULL == prev)
                config->params = cur->next;
            else
                prev->next = cur->next;

            free_nvp(cur);

            return;
        }
        prev = cur;
    }
}

BOOL
get_config_group_parameter_bool(ConfigGroup *config, const WCHAR *name)
{
    WCHAR *val = get_config_group_parameter(config, name);
    return (val != NULL && 0 == wcscmp(val, L"TRUE"));
}

int
get_config_group_parameter_int(ConfigGroup *config, const WCHAR *name)
{
    WCHAR *val = get_config_group_parameter(config, name);
    return val == NULL ? 0 : _wtoi(val);
}

void
set_config_group_parameter_bool(ConfigGroup *config, const WCHAR *name, BOOL value)
{
    set_config_group_parameter(config, name, value ? L"TRUE" : L"FALSE");
}

void
set_config_group_parameter_int(ConfigGroup *config, const WCHAR *name, int value)
{
    WCHAR buf[MAX_PATH];
    _snwprintf(buf, MAX_PATH, L"%d", value);
    set_config_group_parameter(config, name, buf);
}

void
set_config_group_parameter_ascii(ConfigGroup *config, const WCHAR *name, char *value)
{
    WCHAR buf[MAX_PATH];
    _snwprintf(buf, MAX_PATH, L"%S", value);
    buf[MAX_PATH - 1] = L'\0';
    set_config_group_parameter(config, name, buf);
}

void
set_config_group_parameter_scrambled(ConfigGroup *config, const WCHAR *name,
                                     const WCHAR *value)
{
    /* this swaps high and low bytes of WCHAR. obviously not strong,
     *  just meant to be something other than plaintext. */
    WCHAR buf[MAX_PATH];
    UINT i = 0;
    while (value[i] != 0 && i < MAX_PATH - 1) {
        buf[i] = (WCHAR)(((value[i] & 0x00ff) << 8) + ((value[i] & 0xff00) >> 8));
        i++;
    }
    buf[i] = L'\0';
    set_config_group_parameter(config, name, buf);
}

void
get_config_group_parameter_scrambled(ConfigGroup *config, const WCHAR *name,
                                     WCHAR *buffer, UINT maxchars)
{
    WCHAR *value = get_config_group_parameter(config, name);
    UINT i = 0;

    if (value == NULL) {
        buffer[0] = L'\0';
        return;
    }

    while (value[i] != 0 && i < maxchars - 1) {
        buffer[i] = (WCHAR)(((value[i] & 0x00ff) << 8) + ((value[i] & 0xff00) >> 8));
        i++;
    }
    buffer[i] = L'\0';
}

void
add_config_group(ConfigGroup *parent, ConfigGroup *new_child)
{
    if (NULL != get_child(new_child->name, parent)) {
        DO_DEBUG(DL_WARN, printf("adding multiple child: %S\n", new_child->name););
    }

    if (parent->children == NULL)
        parent->children = new_child;

    if (parent->lastchild != NULL)
        parent->lastchild->next = new_child;

    parent->lastchild = new_child;
    new_child->next = NULL;
}

typedef DWORD (*custom_config_read_handler)(ConfigGroup *config, const WCHAR *name,
                                            const WCHAR *value);

#    define CURRENT_MODES_VERSION 42000
#    define MAX_MODES_FILE_SIZE (64 * 1024)

DWORD
custom_hotp_modes_read_handler(ConfigGroup *config, const WCHAR *name, const WCHAR *value)
{
    char modes_file[MAX_MODES_FILE_SIZE];
    WCHAR modes_path[MAX_PATH];
    WCHAR parambuf[MAX_PATH];
    WCHAR valbuf[MAX_PATH];
    DWORD res;
    SIZE_T needed = 0;
    BOOL done = FALSE;
    char *modes_line;
    ConfigGroup *hotp_config;

    if (name == NULL || value == NULL)
        return ERROR_SUCCESS;

    /* read in modes file */
    _snwprintf(modes_path, MAX_PATH, L"%s\\%d\\%S", value, CURRENT_MODES_VERSION,
               HOTP_MODES_FILENAME);
    modes_path[MAX_PATH - 1] = L'\0';

    res = read_file_contents(modes_path, modes_file, MAX_MODES_FILE_SIZE, &needed);

    /* if the modes file isn't there, no worries; assume no modes */
    if (ERROR_FILE_NOT_FOUND == res)
        return ERROR_SUCCESS;

    if (ERROR_SUCCESS != res)
        return res;

    if (needed > MAX_MODES_FILE_SIZE - 2)
        return ERROR_INSUFFICIENT_BUFFER;

    /* create child config group */
    hotp_config = new_config_group(L_DYNAMORIO_VAR_HOT_PATCH_MODES);

    /* modes_file has contents, now sscanf and set all nvp. */
    modes_line = parse_line_sep(modes_file, ':', &done, parambuf, valbuf, MAX_PATH);
    DO_DEBUG(DL_VERB, printf("hotp modes first line %S:%S\n", parambuf, valbuf););

    /* expect num lines first */
    if (valbuf[0] != '\0')
        return ERROR_PARSE_ERROR;

    while (!done) {
        modes_line = parse_line_sep(modes_line, ':', &done, parambuf, valbuf, MAX_PATH);
        if (done)
            break;
        set_config_group_parameter(hotp_config, parambuf, valbuf);
    }

    add_config_group(config, hotp_config);

    return ERROR_SUCCESS;
}

custom_config_read_handler
get_custom_config_read_handler(const WCHAR *name)
{
    if (name == NULL)
        return NULL;

    if (0 == wcscmp(name, L_DYNAMORIO_VAR_HOT_PATCH_MODES)) {
        return (&custom_hotp_modes_read_handler);
    }

    return NULL;
}

static DWORD
read_config_group_from_registry(HKEY parent, ConfigGroup **configptr, const WCHAR *name,
                                BOOL recursive)
{
    HKEY config_key = NULL;
    ConfigGroup *config = NULL;
    WCHAR keyname[MAX_PATH];
    WCHAR keyvalue[MAX_PARAM_LEN];
    DWORD idx, keySz, valSz, res = ERROR_SUCCESS;
    FILETIME writetime;
    custom_config_read_handler handler;

    if (name == NULL) {
        config_key = parent;
    } else {
        WCHAR translated_name[MAX_PATH];
        wcsncpy(translated_name, name, MAX_PATH);
        configpath_to_registry_path(translated_name);
        res = RegOpenKeyEx(parent, translated_name, 0, platform_key_flags() | KEY_READ,
                           &config_key);
        if (res != ERROR_SUCCESS)
            goto read_config_out;
    }

    config = new_config_group(name);

    /* first read in all values */
    for (idx = 0; /* TRUE */; idx++) {
        keySz = MAX_PATH;
        valSz = sizeof(keyvalue);
        res = RegEnumValue(config_key, idx, keyname, &keySz, 0, NULL, (LPBYTE)keyvalue,
                           &valSz);
        if (res == ERROR_NO_MORE_ITEMS) {
            res = ERROR_SUCCESS;
            break;
        } else if (res != ERROR_SUCCESS) {
            goto read_config_out;
        }

        if (NULL != (handler = get_custom_config_read_handler(keyname))) {
            res = handler(config, keyname, keyvalue);
            if (res != ERROR_SUCCESS)
                goto read_config_out;
        } else {
            set_config_group_parameter(config, keyname, keyvalue);
        }
    }

    /* and read in all children (if desired) */
    for (idx = 0; recursive; idx++) {
        ConfigGroup *child_config;

        keySz = MAX_PATH;
        res = RegEnumKeyEx(config_key, idx, keyname, &keySz, 0, NULL, NULL, &writetime);
        if (res == ERROR_NO_MORE_ITEMS) {
            res = ERROR_SUCCESS;
            break;
        } else if (res != ERROR_SUCCESS) {
            goto read_config_out;
        }

        res = read_config_group_from_registry(config_key, &child_config, keyname,
                                              recursive);
        if (res != ERROR_SUCCESS)
            return res;

        add_config_group(config, child_config);
    }

    *configptr = config;

read_config_out:

    if (NULL != config_key && NULL != name)
        RegCloseKey(config_key);

    return res;
}

DWORD
read_config_group(ConfigGroup **configptr, const WCHAR *name,
                  BOOL read_children_recursively)
{
    HKEY rootkey;
    DWORD res = ERROR_SUCCESS;

    DO_ASSERT(configptr != NULL);
    DO_ASSERT(name != NULL);
    *configptr = NULL;

    res = get_key_handle(&rootkey, NULL, NULL, FALSE, KEY_READ);
    if (res != ERROR_SUCCESS)
        return res;

    res = read_config_group_from_registry(rootkey, configptr, name,
                                          read_children_recursively);

    RegCloseKey(rootkey);

    return res;
}

typedef DWORD (*custom_config_write_handler)(HKEY parent, ConfigGroup *config,
                                             ConfigGroup *config_parent);

DWORD
custom_hotp_modes_write_handler(HKEY parent, ConfigGroup *config,
                                ConfigGroup *config_parent)
{
    /* need to write modes file path to key, and the modes file
     * contents. */
    DWORD res;
    WCHAR modes_file[MAX_PATH];
    WCHAR modes_key[MAX_PATH];
    char modes_file_contents[MAX_MODES_FILE_SIZE];
    char modes_file_line[MAX_PATH];
    BOOL changed = FALSE;
    NameValuePairNode *nvpn;

    /* the modes file name is based on the parent ConfigGroup; if the
     *  parent config is the root, then the modes file goes in the
     *  top-level config dir, otherwise it goes in an app-specific
     *  config dir. */
    if (0 == wcscmp(L_PRODUCT_NAME, config_parent->name)) {
        _snwprintf(modes_key, MAX_PATH, L"%s\\config", get_dynamorio_home());
    } else {
        _snwprintf(modes_key, MAX_PATH, L"%s\\config\\%s", get_dynamorio_home(),
                   config_parent->name);
    }
    modes_key[MAX_PATH - 1] = L'\0';

    /* now write modes file content; name=patch ID, value=mode */
    _snprintf(modes_file_contents, MAX_MODES_FILE_SIZE, "%d\n", count_params(config));
    modes_file_contents[MAX_MODES_FILE_SIZE - 1] = '\0';
    for (nvpn = config->params; NULL != nvpn; nvpn = nvpn->next) {
        _snprintf(modes_file_line, MAX_PATH, "%S:%S\n", nvpn->name, nvpn->value);
        modes_file_line[MAX_PATH - 1] = '\0';
        strncat(modes_file_contents, modes_file_line,
                MAX_MODES_FILE_SIZE - strlen(modes_file_contents));
        modes_file_contents[MAX_MODES_FILE_SIZE - 1] = '\0';
    }

    if (strlen(modes_file_contents) > MAX_MODES_FILE_SIZE - 2) {
        return ERROR_INSUFFICIENT_BUFFER;
    }

    /* first, mkdir -p */
    _snwprintf(modes_file, MAX_PATH, L"%s\\%d\\%S", modes_key, CURRENT_MODES_VERSION,
               HOTP_MODES_FILENAME);
    modes_file[MAX_PATH - 1] = L'\0';
    ensure_directory_exists_for_file(modes_file);

    /* and then write file */
    res = write_file_contents_if_different(modes_file, modes_file_contents, &changed);
    if (res != ERROR_SUCCESS)
        return res;

    /* finally, write the filename to the modes key */
    return write_reg_string(parent, L_DYNAMORIO_VAR_HOT_PATCH_MODES, modes_key);
}

custom_config_write_handler
get_custom_config_write_handler(ConfigGroup *config)
{
    if (config == NULL || config->name == NULL)
        return NULL;

    if (0 == wcscmp(config->name, L_DYNAMORIO_VAR_HOT_PATCH_MODES)) {
        return (&custom_hotp_modes_write_handler);
    }

    return NULL;
}

DWORD
write_config_group_to_registry(HKEY parent, ConfigGroup *config,
                               ConfigGroup *parent_config)
{
    HKEY config_key = NULL;
    NameValuePairNode *nvpn;
    DWORD res = ERROR_SUCCESS;
    custom_config_write_handler handler = NULL;

    if (NULL == config->name) {
        config_key = parent;
    } else if (NULL != (handler = get_custom_config_write_handler(config))) {
        res = handler(parent, config, parent_config);
        if (res != ERROR_SUCCESS)
            goto write_config_out;
        /* only continue down the chain if custom-handled */
        if (NULL != config->next) {
            res = write_config_group_to_registry(parent, config->next, parent_config);
        }
        goto write_config_out;
    } else {
        /* open or create */
        res = get_key_handle(&config_key, parent, config->name, FALSE,
                             KEY_WRITE | KEY_ENUMERATE_SUB_KEYS);
        if (res != ERROR_SUCCESS)
            goto write_config_out;
    }

    /* write the NVP's */
    for (nvpn = config->params; NULL != nvpn; nvpn = nvpn->next) {
        /* hook for forced deletion of a key */
        if (0 == wcscmp(nvpn->value, L_DELETE_PARAMETER_KEY)) {
            RegDeleteValue(config_key, nvpn->name);
            res = ERROR_SUCCESS;
        } else {
            res = write_reg_string(config_key, nvpn->name, nvpn->value);
        }
        if (res != ERROR_SUCCESS)
            goto write_config_out;
    }

    /* write the children */
    if (NULL != config->children) {
        res = write_config_group_to_registry(config_key, config->children, config);
        if (res != ERROR_SUCCESS)
            goto write_config_out;
    }

    /* continue down the chain */
    if (NULL != config->next) {
        res = write_config_group_to_registry(parent, config->next, parent_config);
        if (res != ERROR_SUCCESS)
            goto write_config_out;
    }

write_config_out:

    if (NULL != config_key && NULL != config->name)
        RegCloseKey(config_key);

    return res;
}

/*
 * this enforces that the key tree matches the config group exactly,
 * e.g., all keys/values are deleted that do not appear in 'filter'
 *
 * this complexity is necessary in order to allow
 * write_config_group_to_registry to be atomic, in the sense that
 * config information is updated on a key-by-key atomic basis.
 */
DWORD
recursive_delete_key(HKEY parent, const WCHAR *keyname, ConfigGroup *filter)
{
    HKEY subkey = NULL;
    WCHAR subkeyname[MAX_PATH];
    FILETIME writetime;
    DWORD res, keySz, idx;
    ConfigGroup *child;

    res = get_key_handle(&subkey, parent, keyname, TRUE, KEY_WRITE | KEY_READ);
    if (res != ERROR_SUCCESS)
        goto recursive_delete_out;

    /* and read in all children (if desired) */
    for (idx = 0;;) {
        /* note that since deleting, we always pass index 0 */
        keySz = MAX_PATH;
        res = RegEnumKeyEx(subkey, idx, subkeyname, &keySz, 0, NULL, NULL, &writetime);
        if (res == ERROR_NO_MORE_ITEMS) {
            res = ERROR_SUCCESS;
            break;
        } else if (res != ERROR_SUCCESS) {
            goto recursive_delete_out;
        }

        /* note that recursive_delete_key will only delete the entire
         *  subkey if it's not a child (eg, if the 'filter' parameter
         *  is NULL) */
        child = get_child(subkeyname, filter);
        res = recursive_delete_key(subkey, subkeyname, child);
        if (res != ERROR_SUCCESS)
            goto recursive_delete_out;

        /* increment the index if we're not deleting the key */
        if (NULL != child)
            idx++;
    }

    if (NULL != filter) {
        /* prune values too */
        for (idx = 0;;) {
            keySz = MAX_PATH;
            res = RegEnumValue(subkey, idx, subkeyname, &keySz, 0, NULL, NULL, NULL);
            if (res == ERROR_NO_MORE_ITEMS) {
                res = ERROR_SUCCESS;
                break;
            } else if (res != ERROR_SUCCESS) {
                goto recursive_delete_out;
            }

            /* don't delete if the param exists, OR if this is a custom
             * handled name and the child exists. */
            if (is_param(subkeyname, filter) ||
                (get_child(subkeyname, filter) &&
                 get_custom_config_read_handler(subkeyname))) {
                /* if we're not deleting, increment the index */
                idx++;
            } else {
                res = RegDeleteValue(subkey, subkeyname);
                if (res != ERROR_SUCCESS)
                    goto recursive_delete_out;
            }
        }
    }

    RegCloseKey(subkey);
    subkey = NULL;

    /* only delete the key itself if it's not being filtered out. */
    if (filter == NULL) {
        /* PR 244206: we assume we're only going to recursively delete keys in
         * HKLM\Software\Company
         */
        res = delete_product_key(parent, keyname);
    }

recursive_delete_out:

    if (subkey != NULL)
        RegCloseKey(subkey);

    return res;
}

DWORD
write_config_group(ConfigGroup *config)
{
    HKEY rootkey;
    DWORD res;

    res = get_key_handle(&rootkey, NULL, NULL, FALSE, KEY_WRITE | KEY_ENUMERATE_SUB_KEYS);
    if (res != ERROR_SUCCESS)
        return res;

    res = write_config_group_to_registry(rootkey, config, NULL);

    /* prune if necessary */
    if (ERROR_SUCCESS == res && config->should_clear) {
        res = recursive_delete_key(rootkey, config->name, config);
    }

    RegCloseKey(rootkey);

    return res;
}

/* single parameter config functions */

DWORD
set_config_parameter(const WCHAR *path, BOOL absolute, const WCHAR *name,
                     const WCHAR *value)
{
    HKEY rootkey;
    DWORD res;

    res = get_key_handle(&rootkey, NULL, path, absolute, KEY_WRITE);
    if (res != ERROR_SUCCESS)
        return res;

    res = write_reg_string(rootkey, name, value);

    RegCloseKey(rootkey);

    return res;
}

DWORD
get_config_parameter(const WCHAR *path, BOOL absolute, const WCHAR *name, WCHAR *value,
                     int maxlen)
{
    HKEY rootkey;
    DWORD res;

    res = get_key_handle(&rootkey, NULL, path, absolute, KEY_READ);
    if (res != ERROR_SUCCESS)
        return res;

    res = read_reg_string(rootkey, name, value, maxlen);

    RegCloseKey(rootkey);

    return res;
}

DWORD
read_reg_string(HKEY subkey, const WCHAR *keyname, WCHAR *value, int valchars)
{
    DWORD len = valchars * sizeof(WCHAR);
    return RegQueryValueEx(subkey, keyname, 0, NULL, (LPBYTE)value, &len);
}

/* if value is NULL, the key will be deleted */
DWORD
write_reg_string(HKEY subkey, const WCHAR *keyname, const WCHAR *value)
{
    if (value)
        return RegSetValueEx(subkey, keyname, 0, REG_SZ, (LPBYTE)value,
                             (DWORD)(wcslen(value) + 1) * sizeof(WCHAR));
    else
        return RegDeleteValue(subkey, keyname);
}

/* process identification routines */

/* tries both with and without no_strip */
ConfigGroup *
get_qualified_config_group(ConfigGroup *config, const WCHAR *exename,
                           const WCHAR *cmdline)
{
    WCHAR qname[MAX_PATH];
    ConfigGroup *c;

    /* need to try both with and without NO_STRIP! */
    _snwprintf(qname, MAX_PATH, L"%s-", exename);
    if (get_commandline_qualifier(cmdline, qname + wcslen(qname),
                                  (UINT)(MAX_PATH - wcslen(qname)), FALSE)) {
        c = get_child(qname, config);
        if (NULL != c)
            return c;
    }

    _snwprintf(qname, MAX_PATH, L"%s-", exename);
    if (get_commandline_qualifier(cmdline, qname + wcslen(qname),
                                  (UINT)(MAX_PATH - wcslen(qname)), TRUE)) {
        c = get_child(qname, config);
        if (NULL != c)
            return c;
    }

    return NULL;
}

BOOL
is_parent_of_qualified_config_group(ConfigGroup *config)
{
    WCHAR *run_under = NULL;

    if (config == NULL)
        return FALSE;
    run_under = get_config_group_parameter(config, L_DYNAMORIO_VAR_RUNUNDER);
    if (run_under == NULL)
        return FALSE;

    return TEST(RUNUNDER_COMMANDLINE_DISPATCH, _wtoi(run_under));
}

ConfigGroup *
get_process_config_group(ConfigGroup *config, process_id_t pid)
{
    WCHAR buf[MAX_PATH];
    DWORD res;
    ConfigGroup *c;

    res = get_process_name(pid, buf, MAX_PATH);

    if (res != ERROR_SUCCESS)
        return NULL;

    wcstolower(buf);
    c = get_child(buf, config);

    if (c == NULL || is_parent_of_qualified_config_group(c)) {
        WCHAR cmdlbuf[MAX_PATH];
        ConfigGroup *qualified;
        get_process_cmdline(pid, cmdlbuf, MAX_PATH);
        qualified = get_qualified_config_group(config, buf, cmdlbuf);
        if (NULL != qualified)
            return qualified;
    }

    return c;
}

/******************
 * list functions *
 ******************/

/*
 * given a ;-separated list and a filename, return a pointer to
 *  the filename in the list, if it appears. comparisons are
 *  case insensitive and independent of path; eg,
 *     get_entry_location("c:\\foo\\bar.dll;blah;...", "D:\\Bar.DLL")
 *  would return a pointer to the beginning of the list.
 */
WCHAR *
get_entry_location(const WCHAR *list, const WCHAR *filename, WCHAR separator)
{
    WCHAR *lowerlist, *lowername, *entry;

    wcstolower(lowerlist = wcsdup(list));
    wcstolower(lowername = wcsdup(get_exename_from_path(filename)));

    entry = wcsstr(lowerlist, lowername);

    while (NULL != entry) {
        WCHAR last = *(entry + wcslen(lowername));

        /* make sure it's not just a substring */
        if ((last == separator || last == L'\0') &&
            (entry == lowerlist || *(entry - 1) == separator || *(entry - 1) == L'\\' ||
             *(entry - 1) == L'/')) {
            /* everything's cool; now put the path back in and translate */
            WCHAR *cur = lowerlist;
            while (NULL != wcschr(cur, separator) && wcschr(cur, separator) < entry)
                cur = wcschr(cur, separator) + 1;
            entry = ((WCHAR *)list) + (cur - lowerlist);
            break;
        } else {
            entry = wcsstr(entry + 1, lowername);
        }
    }

    free(lowername);
    free(lowerlist);

    return entry;
}

BOOL
is_in_file_list(const WCHAR *list, const WCHAR *filename, WCHAR separator)
{
    return NULL != get_entry_location(list, filename, separator);
}

/* returns a new list which needs to be freed, and frees old list */
WCHAR *
add_to_file_list(WCHAR *list, const WCHAR *filename, BOOL check_for_duplicates,
                 BOOL add_to_front, BOOL overwrite_existing, WCHAR separator)
{
    WCHAR *new_list;
    SIZE_T new_list_size;

    if (NULL == list || 0 == wcslen(list))
        return wcsdup(filename);

    if (overwrite_existing)
        remove_from_file_list(list, filename, separator);

    if (check_for_duplicates && is_in_file_list(list, filename, separator))
        return list;

    new_list_size = wcslen(list) + wcslen(filename) + 2;

    new_list = (WCHAR *)malloc(sizeof(WCHAR) * new_list_size);

    if (wcslen(list) == 0) {
        wcsncpy(new_list, filename, new_list_size);
    } else {
        _snwprintf(new_list, new_list_size, L"%s%c%s", add_to_front ? filename : list,
                   separator, add_to_front ? list : filename);
    }

    free(list);

    return new_list;
}

/* provide these helpers to avoid multiple libc problems */
WCHAR *
new_file_list(SIZE_T initial_chars)
{
    WCHAR *list = (WCHAR *)malloc(sizeof(WCHAR) * (1 + initial_chars));
    *list = L'\0';
    return list;
}

void
free_file_list(WCHAR *list)
{
    free(list);
}

/* removes all occurrences */
void
remove_from_file_list(WCHAR *list, const WCHAR *filename, WCHAR separator)
{
    WCHAR *entry, *end_of_entry;

    entry = get_entry_location(list, filename, separator);
    if (NULL == entry)
        return;

    end_of_entry = wcschr(entry, separator);

    if (NULL == end_of_entry) {
        if (entry == list)
            *entry = L'\0';
        else
            *(entry - 1) = L'\0';
    } else {
        /* note the whole wcslen(end_of_entry) to get the null
         *  terminator (since we're doing end_of_entry + 1)
         *
         * note that memmove handles appropriately the case
         *  where the src and dest regions overlap. */
        memmove(entry, end_of_entry + 1, (wcslen(end_of_entry)) * sizeof(WCHAR));
    }

    /* recurse for multiple occurrences */
    remove_from_file_list(list, filename, separator);
}

#    define BLACK 0
#    define WHITE 1

BOOL
filter(WCHAR *list, const WCHAR *filter, DWORD black_or_white, BOOL check_only,
       WCHAR separator)
{
    WCHAR *working_list, *cur, *next;
    BOOL satisfied = TRUE, remove_entry;

    if (list == NULL || wcslen(list) == 0)
        return TRUE;

    next = cur = working_list = wcsdup(list);

    do {

        if (NULL != next)
            next = wcschr(cur, separator);

        if (NULL != next)
            *next = L'\0';

        cur = get_exename_from_path(cur);
        remove_entry =
            !(is_in_file_list(filter, cur, separator) ^ (BLACK == black_or_white));

        if (remove_entry) {
            if (check_only)
                return FALSE;
            else
                remove_from_file_list(list, cur, separator);

            satisfied = FALSE;
        }

        cur = next + 1;

    } while (NULL != next);

    free(working_list);

    return satisfied;
}

BOOL
blocklist_filter(WCHAR *list, const WCHAR *blocklist, BOOL check_only, WCHAR separator)
{
    return filter(list, blocklist, BLACK, check_only, separator);
}

BOOL
allowlist_filter(WCHAR *list, const WCHAR *allowlist, BOOL check_only, WCHAR separator)
{
    return filter(list, allowlist, WHITE, check_only, separator);
}

/* appinit key */

DWORD
set_autoinjection_ex(BOOL inject, DWORD flags, const WCHAR *blocklist,
                     const WCHAR *allowlist,
                     /* OUT */ DWORD *list_error, const WCHAR *custom_preinject_name,
                     /* OUT */ WCHAR *current_list, SIZE_T maxchars)
{
    WCHAR curlist[MAX_PARAM_LEN];
    WCHAR preinject_name[MAX_PATH];
    WCHAR *list;
    DWORD res;
    BOOL list_ok_ = TRUE;
    BOOL using_system32 = using_system32_for_preinject(custom_preinject_name);

    res = get_config_parameter(INJECT_ALL_KEY_L, TRUE, INJECT_ALL_SUBKEY_L, curlist,
                               MAX_PARAM_LEN);
    if (res != ERROR_SUCCESS) {
        /* if it's not there, then we create it */
        curlist[0] = L'\0';
    }

    list = new_file_list(wcslen(curlist) + 1);
    wcsncpy(list, curlist, wcslen(curlist) + 1);

    if (NULL != current_list)
        wcsncpy(current_list, curlist, maxchars);

    if (using_system32 && TEST(APPINIT_SYS32_CLEAR_OTHERS, flags)) {
        /* if we're using system32, and the clear flag is set, clear it */
        curlist[0] = L'\0';
    }

    if (NULL == custom_preinject_name) {
        res = get_preinject_name(preinject_name, MAX_PATH);
        if (res != ERROR_SUCCESS)
            return res;
    } else {
        wcsncpy(preinject_name, custom_preinject_name, MAX_PATH);
    }

    /* if using system32, make sure to copy the DLL there from
     *  the standard place (or remove it if we're turning off) */
    if (using_system32) {
        WCHAR src_path[MAX_PATH], dst_path[MAX_PATH];

        if (NULL == custom_preinject_name) {
            res = get_preinject_path(src_path, MAX_PATH, TRUE, TRUE);
            if (res != ERROR_SUCCESS)
                return res;
            wcsncat(src_path, L"\\" L_EXPAND_LEVEL(INJECT_DLL_NAME),
                    MAX_PATH - wcslen(src_path));
        } else {
            wcsncpy(src_path, custom_preinject_name, MAX_PATH);
        }

        get_preinject_path(dst_path, MAX_PATH, FALSE, TRUE);
        if (res != ERROR_SUCCESS)
            return res;
        wcsncat(dst_path, L"\\" L_EXPAND_LEVEL(INJECT_DLL_NAME),
                MAX_PATH - wcslen(dst_path));

        if (inject) {
            /* We used to only copy if (!file_exists(dst_path))
             * but it seems that we should clobber to avoid
             * upgrade issues, at risk of messing up another product
             * w/ a dll of the same name.
             */
            CopyFile(src_path, dst_path, FALSE /*don't fail if exists*/);
        } else {
            /* FIXME: do we want to use delete_file_rename_in_use?
             * this should only be called at uninstall or by
             *  tools users, so there shouldn't be any problem
             *  leaving one .tmp file around...
             * alternatively, we could try renaming to a path in
             *  our installation directory, which would be
             *  wiped out on installation. however, it gets
             *  complicated if our installation folder is on
             *  a different volume.
             * another option is to rename to %SYSTEM32%\..\TEMP,
             *  which should always exist i think...
             */
            DeleteFile(dst_path);
        }

        /* now we want just the name, since in system32 */
        if (get_exename_from_path(preinject_name) != NULL) {
            wcsncpy(preinject_name, get_exename_from_path(preinject_name),
                    BUFFER_SIZE_ELEMENTS(preinject_name));
        }
    }

    if (inject) {
        BOOL force_overwrite;
        WCHAR *old = get_entry_location(list, preinject_name, APPINIT_SEPARATOR_CHAR);

        /* first, if there's something there, make sure it exists. if
         *  not, remove it before proceeding (to ensure overwrite)
         * cf case 4053 */
        if (NULL != old) {
            if (using_system32) {
                /* PR 232765: we want to replace any full path w/ filename */
                force_overwrite = true;
            } else {
                WCHAR *firstsep, tmpbuf[MAX_PATH];
                wcsncpy(tmpbuf, old, MAX_PATH);
                firstsep = wcschr(tmpbuf, APPINIT_SEPARATOR_CHAR);
                if (NULL != firstsep)
                    *firstsep = L'\0';
                force_overwrite = !file_exists(tmpbuf);
            }
        } else {
            /* force overwrite if someone cared enough to set one of these */
            force_overwrite =
                TEST((APPINIT_FORCE_TO_FRONT | APPINIT_FORCE_TO_BACK), flags);
        }
        /* always overwrite if asked to */
        force_overwrite = force_overwrite | TEST(APPINIT_OVERWRITE, flags);

        /* add !TEST(APPINIT_FORCE_TO_BACK, flags) so that we favor adding
         *  to the front if neither are set. */
        list = add_to_file_list(list, preinject_name, TRUE,
                                TEST(APPINIT_FORCE_TO_FRONT, flags) ||
                                    !TEST(APPINIT_FORCE_TO_BACK, flags),
                                force_overwrite, APPINIT_SEPARATOR_CHAR);
    } else {
        remove_from_file_list(list, preinject_name, APPINIT_SEPARATOR_CHAR);
    }

    if (TEST(APPINIT_USE_ALLOWLIST, flags)) {
        if (NULL == allowlist)
            res = ERROR_INVALID_PARAMETER;
        else
            list_ok_ =
                allowlist_filter(list, allowlist, TEST(APPINIT_CHECK_LISTS_ONLY, flags),
                                 APPINIT_SEPARATOR_CHAR);
    } else if (TEST(APPINIT_USE_BLOCKLIST, flags)) {
        /* else if since allowlist subsumes blocklist */
        if (NULL == blocklist)
            res = ERROR_INVALID_PARAMETER;
        else
            list_ok_ =
                blocklist_filter(list, blocklist, TEST(APPINIT_CHECK_LISTS_ONLY, flags),
                                 APPINIT_SEPARATOR_CHAR);
    }

    if (list_error != NULL && !list_ok_)
        *list_error = ERROR_LIST_VIOLATION;

    if (res == ERROR_INVALID_PARAMETER)
        return res;

    if (!list_ok_ && TEST(APPINIT_BAIL_ON_LIST_VIOLATION, flags))
        remove_from_file_list(list, preinject_name, APPINIT_SEPARATOR_CHAR);

    /* now, system32 flag checks */
    if (using_system32) {

        /* not yet supported */
        if (TEST(APPINIT_SYS32_USE_LENGTH_WORKAROUND, flags))
            return ERROR_INVALID_PARAMETER;

        if (wcslen(list) > APPINIT_SYSTEM32_LENGTH_LIMIT) {

            if (list_error != NULL)
                *list_error = ERROR_LENGTH_VIOLATION;

            if (TEST(APPINIT_SYS32_FAIL_ON_LENGTH_ERROR, flags)) {
                remove_from_file_list(list, preinject_name, APPINIT_SEPARATOR_CHAR);
            } else {
                /* truncate, if the flags specify it. */
                if (TEST(APPINIT_SYS32_TRUNCATE, flags)) {
                    list[APPINIT_SYSTEM32_LENGTH_LIMIT] = L'\0';
                }
            }
        }
    }

    /* only write if it's changed */
    if (0 != wcscmp(list, curlist)) {
        res = set_config_parameter(INJECT_ALL_KEY_L, TRUE, INJECT_ALL_SUBKEY_L, list);
        if (res != ERROR_SUCCESS)
            return res;
    }

    free_file_list(list);

    return ERROR_SUCCESS;
}

DWORD
set_custom_autoinjection(const WCHAR *preinject, DWORD flags)
{
    return set_autoinjection_ex(TRUE, flags, NULL, NULL, NULL, preinject, NULL, 0);
}

DWORD
set_autoinjection()
{
    return set_autoinjection_ex(TRUE, 0, NULL, NULL, NULL, NULL, NULL, 0);
}

DWORD
unset_custom_autoinjection(const WCHAR *preinject, DWORD flags)
{
    return set_autoinjection_ex(FALSE, flags, NULL, NULL, NULL, preinject, NULL, 0);
}

DWORD
unset_autoinjection()
{
    return set_autoinjection_ex(FALSE, 0, NULL, NULL, NULL, NULL, NULL, 0);
}

/* NOTE: that this returns the current status -- which on NT
 *  is not necessarily the actual status, which is cached by the
 *  os at boot time.
 * FIXME: add a helper method for determining the status of
 *  appinit that is being used for the current boot session. */
BOOL
is_autoinjection_set()
{
    WCHAR list[MAX_PARAM_LEN], preinject[MAX_PATH];
    DWORD res;

    res = get_config_parameter(INJECT_ALL_KEY_L, TRUE, INJECT_ALL_SUBKEY_L, list,
                               MAX_PARAM_LEN);
    if (res != ERROR_SUCCESS)
        return FALSE;

    res = get_preinject_name(preinject, MAX_PATH);
    if (res != ERROR_SUCCESS)
        return FALSE;

    return is_in_file_list(list, preinject, APPINIT_SEPARATOR_CHAR);
}

BOOL
is_custom_autoinjection_set(const WCHAR *preinject)
{
    WCHAR list[MAX_PARAM_LEN];
    DWORD res;

    res = get_config_parameter(INJECT_ALL_KEY_L, TRUE, INJECT_ALL_SUBKEY_L, list,
                               MAX_PARAM_LEN);
    if (res != ERROR_SUCCESS)
        return FALSE;
    return is_in_file_list(list, preinject, APPINIT_SEPARATOR_CHAR);
}

/* Returns true for Vista or later, including Windows 7 */
BOOL
is_vista()
{
    DWORD platform = 0;
    get_platform(&platform);
    return (platform >= PLATFORM_VISTA);
}

BOOL
is_win7()
{
    DWORD platform = 0;
    get_platform(&platform);
    return (platform >= PLATFORM_WIN_7);
}

/* Also disables reqt for signature on lib for win7+ */
DWORD
set_loadappinit_value(DWORD value)
{
    HKEY rootkey;
    DWORD res;

    if (!is_vista())
        return ERROR_UNSUPPORTED_OS;

    res = get_key_handle(&rootkey, INJECT_ALL_HIVE, INJECT_ALL_KEY_L, TRUE, KEY_WRITE);
    if (res != ERROR_SUCCESS)
        return res;

    res = RegSetValueEx(rootkey, INJECT_ALL_LOAD_SUBKEY_L, 0, REG_DWORD, (LPBYTE)&value,
                        sizeof(value));
    if (res != ERROR_SUCCESS)
        return res;

    if (is_win7()) {
        /* Disable the reqt for a signature.
         * FIXME i#323: better to sign drpreinject so we don't have to
         * relax security!
         */
        DWORD disable = 0;
        res = RegSetValueEx(rootkey, INJECT_ALL_SIGN_SUBKEY_L, 0, REG_DWORD,
                            (LPBYTE)&disable, sizeof(disable));
    }
    return res;
}

DWORD
set_loadappinit()
{
    return set_loadappinit_value(1);
}

DWORD
unset_loadappinit()
{
    return set_loadappinit_value(0);
}

BOOL
is_loadappinit_set()
{
    HKEY rootkey;
    DWORD value;
    DWORD size = sizeof(value);
    DWORD res;

    res = get_key_handle(&rootkey, INJECT_ALL_HIVE, INJECT_ALL_KEY_L, TRUE, KEY_READ);
    if (res != ERROR_SUCCESS)
        return res;

    res = RegQueryValueEx(rootkey, INJECT_ALL_LOAD_SUBKEY_L, NULL, NULL, (LPBYTE)&value,
                          &size);

    return (res == ERROR_SUCCESS && value != 0);
}

/* Deletes the product eventlog.  If no one else (nodemgr/pe) is using the eventlog also
 * deletes the base key and the eventlog file. */
DWORD
destroy_eventlog()
{
    DWORD res, res2;
    /* PR 244206: we don't need the wow64 flag since not HKLM\Software
     * (if we do want it, need to add on create/open as well, including
     * elm.c, which requires linking w/ utils.c)
     */
    res = RegDeleteKey(EVENTLOG_HIVE, L_EVENT_SOURCE_SUBKEY);
    res2 = RegDeleteKey(EVENTLOG_HIVE, L_EVENT_LOG_SUBKEY);
    if (res2 == ERROR_SUCCESS) {
        /* we deleted the top level key (which means it had no subkeys left) which means
         * no one else is using our eventlog, we go ahead and free the file. */
        WCHAR *file = is_vista() ? L_EVENT_FILE_NAME_VISTA : L_EVENT_FILE_NAME_PRE_VISTA;
        WCHAR *truncate_start;
        WCHAR file_exp[MAX_PATH];
        ExpandEnvironmentStrings(file, file_exp, BUFFER_SIZE_ELEMENTS(file_exp));
        NULL_TERMINATE_BUFFER(file_exp);
        if (file_exists(file_exp)) {
            /* If in use by the eventlog can't delete or re-name */
            if (!DeleteFile(file_exp))
                delete_file_on_boot(file_exp);
        }

        /* It appears the generated file is usually truncated to 8.3 by the eventlog
         * program (though apparently not always since some of my machines also have
         * the full name).  Will try to delete the truncated to 8.3 versions. */
        truncate_start = wcsrchr(file_exp, L'\\');
        if (truncate_start != NULL) {
            WCHAR *type = wcsrchr(file_exp, L'.');
            truncate_start += 8;
            if (type != NULL && truncate_start < type) {
                WCHAR tmp_buf[MAX_PATH];
                wcsncpy(tmp_buf, type, BUFFER_SIZE_ELEMENTS(tmp_buf));
                NULL_TERMINATE_BUFFER(tmp_buf);
                wcsncpy(truncate_start, tmp_buf,
                        BUFFER_SIZE_ELEMENTS(file_exp) - (truncate_start - file_exp));
                NULL_TERMINATE_BUFFER(file_exp);
                /* If in use by the eventlog can't delete or re-name */
                if (!DeleteFile(file_exp))
                    delete_file_on_boot(file_exp);
            }
        }
    }
    return res;
}

DWORD
create_eventlog(const WCHAR *dll_path)
{
    HKEY eventlog_key;
    HKEY eventsrc_key;
    WCHAR *wvalue;
    DWORD dvalue;
    DWORD res;

    res =
        get_key_handle(&eventlog_key, EVENTLOG_HIVE, L_EVENT_LOG_SUBKEY, TRUE, KEY_WRITE);
    if (res != ERROR_SUCCESS)
        return res;

    if (is_vista()) {
        wvalue = L_EVENT_FILE_NAME_VISTA;
    } else {
        wvalue = L_EVENT_FILE_NAME_PRE_VISTA;
    }
    /* REG_EXPAND_SZ since we use %systemroot% in the path which needs
     * to be expanded */
    res = RegSetValueEx(eventlog_key, L_EVENT_FILE_VALUE_NAME, 0, REG_EXPAND_SZ,
                        (LPBYTE)wvalue, (DWORD)(wcslen(wvalue) + 1) * sizeof(WCHAR));
    if (res != ERROR_SUCCESS)
        return res;

    dvalue = EVENT_MAX_SIZE;
    res = RegSetValueEx(eventlog_key, L_EVENT_MAX_SIZE_NAME, 0, REG_DWORD,
                        (LPBYTE)&dvalue, sizeof(dvalue));
    if (res != ERROR_SUCCESS)
        return res;

    dvalue = EVENT_RETENTION;
    res = RegSetValueEx(eventlog_key, L_EVENT_RETENTION_NAME, 0, REG_DWORD,
                        (LPBYTE)&dvalue, sizeof(dvalue));
    if (res != ERROR_SUCCESS)
        return res;

    res = get_key_handle(&eventsrc_key, EVENTLOG_HIVE, L_EVENT_SOURCE_SUBKEY, TRUE,
                         KEY_WRITE);
    if (res != ERROR_SUCCESS)
        return res;

    dvalue = EVENT_TYPES_SUPPORTED;
    res = RegSetValueEx(eventsrc_key, L_EVENT_TYPES_SUPPORTED_NAME, 0, REG_DWORD,
                        (LPBYTE)&dvalue, sizeof(dvalue));
    if (res != ERROR_SUCCESS)
        return res;

    dvalue = EVENT_CATEGORY_COUNT;
    res = RegSetValueEx(eventsrc_key, L_EVENT_CATEGORY_COUNT_NAME, 0, REG_DWORD,
                        (LPBYTE)&dvalue, sizeof(dvalue));
    if (res != ERROR_SUCCESS)
        return res;

    res = RegSetValueEx(eventsrc_key, L_EVENT_CATEGORY_FILE_NAME, 0, REG_SZ,
                        (LPBYTE)dll_path, (DWORD)(wcslen(dll_path) + 1) * sizeof(WCHAR));
    if (res != ERROR_SUCCESS)
        return res;

    res = RegSetValueEx(eventsrc_key, L_EVENT_MESSAGE_FILE, 0, REG_SZ, (LPBYTE)dll_path,
                        (DWORD)(wcslen(dll_path) + 1) * sizeof(WCHAR));
    if (res != ERROR_SUCCESS)
        return res;

    return ERROR_SUCCESS;
}

/* Copies the two drearlyhelper?.dlls located in "dir" to system32.
 * They are only needed on win2k, but it's up to the caller to check
 * for that.
 */
DWORD
copy_earlyhelper_dlls(const WCHAR *dir)
{
    WCHAR src_path[MAX_PATH], dst_path[MAX_PATH];
    DWORD len;

    /* We copy drearlyhelp2.dll first, to be on the safe side.
     * drearlyhelp1.dll has a dependency on drearlyhelp2.dll, so if #1 exists
     * and number 2 doesn't the loader will raise an error (and we're pre-image
     * entry point so it becomes a "process failed to initialize" error).
     */
    _snwprintf(src_path, BUFFER_SIZE_ELEMENTS(src_path), L"%s\\%s", dir,
               L_EXPAND_LEVEL(INJECT_HELPER_DLL2_NAME));
    NULL_TERMINATE_BUFFER(src_path);

    /* Get system32 path */
    len = GetSystemDirectory(dst_path, MAX_PATH);
    if (len == 0)
        return GetLastError();
    wcsncat(dst_path, L"\\" L_EXPAND_LEVEL(INJECT_HELPER_DLL2_NAME),
            BUFFER_SIZE_ELEMENTS(dst_path) - wcslen(dst_path));

    /* We could check file_exists(dst_path) but better to just
     * clobber so we can upgrade nicely (at risk of clobbering
     * some other product's same-name dll)
     */
    if (!CopyFile(src_path, dst_path, FALSE /*don't fail if exists*/))
        return GetLastError();

    _snwprintf(src_path, BUFFER_SIZE_ELEMENTS(src_path), L"%s\\%s", dir,
               L_EXPAND_LEVEL(INJECT_HELPER_DLL1_NAME));
    NULL_TERMINATE_BUFFER(src_path);

    /* Get system32 path, again: we could cache it */
    len = GetSystemDirectory(dst_path, MAX_PATH);
    if (len == 0)
        return GetLastError();
    wcsncat(dst_path, L"\\" L_EXPAND_LEVEL(INJECT_HELPER_DLL1_NAME),
            BUFFER_SIZE_ELEMENTS(dst_path) - wcslen(dst_path));

    if (!CopyFile(src_path, dst_path, FALSE /*don't fail if exists*/))
        return GetLastError();

    /* FIXME PR 232738: add a param for removing the files */

    return ERROR_SUCCESS;
}

void
dump_nvp(NameValuePairNode *nvpn)
{
    printf("%S=%S", nvpn->name, nvpn->value == NULL ? L"<null>" : nvpn->value);
}

void
dump_config_group(char *prefix, char *incr, ConfigGroup *c, BOOL traverse)
{
    NameValuePairNode *nvpn = NULL;

    printf("%sConfig Group: %S\n", prefix, c->name == NULL ? L"<null>" : c->name);
    printf("%sshould_clear: %d\n", prefix, c->should_clear);
    printf("%sparams:\n", prefix);
    for (nvpn = c->params; NULL != nvpn; nvpn = nvpn->next) {
        printf("%s%s%s", prefix, incr, incr);
        dump_nvp(nvpn);
        printf("\n");
    }
    if (c->children != NULL) {
        char p2[MAX_PATH];
        _snprintf(p2, MAX_PATH, "%s%s", prefix, incr);
        dump_config_group(p2, incr, c->children, TRUE);
    }
    if (traverse && c->next != NULL) {
        dump_config_group(prefix, incr, c->next, TRUE);
    }
}

#else // ifdef UNIT_TEST

int
main()
{
    ConfigGroup *c, *c2, *c3, *c4, *cp;
    DWORD res;
    WCHAR sep = LIST_SEPARATOR_CHAR;

    WCHAR *list1, *list2, *tmplist;

    WCHAR buf[MAX_PARAM_LEN];

    set_debuglevel(DL_INFO);
    set_abortlevel(DL_WARN);

    list1 = new_file_list(16);

    /* basic list operations */
    {
        DO_ASSERT_WSTR_EQ(L"", list1);

        list1 = add_to_file_list(list1, L"c:\\foo.dll", TRUE, TRUE, FALSE, sep);
        DO_ASSERT_WSTR_EQ(L"c:\\foo.dll", list1);

        list1 = add_to_file_list(list1, L"C:\\bar.dll", TRUE, TRUE, FALSE, sep);
        DO_ASSERT_WSTR_EQ(L"C:\\bar.dll;c:\\foo.dll", list1);

        list1 = add_to_file_list(list1, L"foo.dll", TRUE, TRUE, FALSE, sep);
        DO_ASSERT_WSTR_EQ(L"C:\\bar.dll;c:\\foo.dll", list1);

        list1 = add_to_file_list(list1, L"C:\\shr\\Foo.dll", TRUE, TRUE, FALSE, sep);
        DO_ASSERT_WSTR_EQ(L"C:\\bar.dll;c:\\foo.dll", list1);

        list1 = add_to_file_list(list1, L"C:\\shr\\Foo.dll", FALSE, TRUE, FALSE, sep);
        DO_ASSERT_WSTR_EQ(L"C:\\shr\\Foo.dll;C:\\bar.dll;c:\\foo.dll", list1);

        list1 = add_to_file_list(list1, L"d:\\gee.dll", TRUE, FALSE, FALSE, sep);
        DO_ASSERT_WSTR_EQ(L"C:\\shr\\Foo.dll;C:\\bar.dll;c:\\foo.dll;d:\\gee.dll", list1);

        list1 = add_to_file_list(list1, L"ar.dll", TRUE, FALSE, FALSE, sep);
        DO_ASSERT_WSTR_EQ(L"C:\\shr\\Foo.dll;C:\\bar.dll;c:\\foo.dll;d:\\gee.dll;ar.dll",
                          list1);
    }

    /* used in tests below */
    DO_ASSERT_WSTR_EQ(L"C:\\shr\\Foo.dll;C:\\bar.dll;c:\\foo.dll;d:\\gee.dll;ar.dll",
                      list1);

    /* remove */
    {
        tmplist = wcsdup(list1);
        remove_from_file_list(tmplist, L"ar.dll", sep);
        DO_ASSERT_WSTR_EQ(L"C:\\shr\\Foo.dll;C:\\bar.dll;c:\\foo.dll;d:\\gee.dll",
                          tmplist);

        remove_from_file_list(tmplist, L"foo.dll", sep);
        DO_ASSERT_WSTR_EQ(L"C:\\bar.dll;d:\\gee.dll", tmplist);

        free_file_list(tmplist);

        tmplist = wcsdup(list1);
        remove_from_file_list(tmplist, L"Ar.dll", sep);
        DO_ASSERT_WSTR_EQ(L"C:\\shr\\Foo.dll;C:\\bar.dll;c:\\foo.dll;d:\\gee.dll",
                          tmplist);

        remove_from_file_list(tmplist, L"Foo.DLL", sep);
        DO_ASSERT_WSTR_EQ(L"C:\\bar.dll;d:\\gee.dll", tmplist);

        free_file_list(tmplist);
    }

    /* path checking */
    {
        tmplist = wcsdup(list1);
        tmplist = add_to_file_list(tmplist, L"C:\\Program Files\\blah\\blah\\foo.dll",
                                   TRUE, TRUE, TRUE, sep);
        DO_ASSERT_WSTR_EQ(
            L"C:\\Program Files\\blah\\blah\\foo.dll;C:\\bar.dll;d:\\gee.dll;ar.dll",
            tmplist);

        free_file_list(tmplist);
    }

    /* list filters */
    {
        list2 = new_file_list(1);
        list2 = add_to_file_list(list2, L"foo.dll", TRUE, TRUE, FALSE, sep);
        list2 = add_to_file_list(list2, L"gee.dll", TRUE, TRUE, FALSE, sep);

        tmplist = wcsdup(list1);
        DO_ASSERT(!blocklist_filter(tmplist, list2, TRUE, sep));
        DO_ASSERT_WSTR_EQ(L"C:\\shr\\Foo.dll;C:\\bar.dll;c:\\foo.dll;d:\\gee.dll;ar.dll",
                          tmplist);

        DO_ASSERT(!blocklist_filter(tmplist, list2, FALSE, sep));
        DO_ASSERT_WSTR_EQ(L"C:\\bar.dll;ar.dll", tmplist);

        DO_ASSERT(blocklist_filter(tmplist, list2, TRUE, sep));
        free_file_list(tmplist);

        tmplist = wcsdup(list1);
        DO_ASSERT(!allowlist_filter(tmplist, list2, FALSE, sep));
        DO_ASSERT_WSTR_EQ(L"C:\\shr\\Foo.dll;c:\\foo.dll;d:\\gee.dll", tmplist);

        DO_ASSERT(allowlist_filter(tmplist, list2, TRUE, sep));

        free_file_list(tmplist);

        free_file_list(list2);
    }

    free_file_list(list1);

    /* force to front */
    {
        list1 = L"home.dll;C:\\PROGRA~1\\DETERM~1\\SECURE~1\\lib\\DRPREI~1.DLL;foo.dll;"
                L"bar.dll";
        list2 = new_file_list(wcslen(list1) + 1);
        wcsncpy(list2, list1, wcslen(list1) + 1);
        list2 = add_to_file_list(list2,
                                 L"C:\\PROGRA~1\\DETERM~1\\SECURE~1\\lib\\DRPREI~1.DLL",
                                 TRUE, TRUE, TRUE, sep);
        DO_ASSERT_WSTR_EQ(L"C:\\PROGRA~1\\DETERM~1\\SECURE~1\\lib\\DRPREI~1.DLL;home.dll;"
                          L"foo.dll;bar.dll",
                          list2);
        DO_ASSERT(wcslen(list1) == wcslen(list2));
    }

    /* autoinject (FIXME: needs more..) */
    {
        res = get_config_parameter(INJECT_ALL_KEY_L, TRUE, INJECT_ALL_SUBKEY_L, buf,
                                   MAX_PARAM_LEN);
        DO_ASSERT(res == ERROR_SUCCESS);

        res = set_config_parameter(INJECT_ALL_KEY_L, TRUE, INJECT_ALL_SUBKEY_L,
                                   L"foo.dll;bar.dll;home.dll");
        DO_ASSERT(res == ERROR_SUCCESS);

        /* FIXME: should all full automated tests of all
         *  APPINIT_FLAGS etc. */
        // res = set_autoinjection_ex(TRUE, );
        // DO_ASSERT(res == ERROR_SUCCESS);

        res = set_config_parameter(INJECT_ALL_KEY_L, TRUE, INJECT_ALL_SUBKEY_L, buf);
        DO_ASSERT(res == ERROR_SUCCESS);
    }

    /* config group */
    {
        c = new_config_group(L"foo");
        DO_ASSERT_WSTR_EQ(c->name, L"foo");

        set_config_group_parameter(c, L"bar", L"wise");

        c2 = new_config_group(L"foo2");
        set_config_group_parameter(c2, L"bar2", L"dumb");
        set_config_group_parameter(c2, L"bar3", L"dumber");
        add_config_group(c, c2);

        c3 = new_config_group(L"foo4");
        set_config_group_parameter(c3, L"bar4", L"eloquent");
        add_config_group(c, c3);

        c4 = new_config_group(L"foo5");
        set_config_group_parameter(c4, L"bar5", L"dull");
        add_config_group(c3, c4);
        c->should_clear = TRUE;

        DO_DEBUG(DL_VERB, dump_config_group("", "  ", c, FALSE););

        res = write_config_group(c);
        DO_ASSERT(res == ERROR_SUCCESS);
        DO_ASSERT_WSTR_EQ(c->name, L"foo");
    }

    /* copy config group */
    {
        ConfigGroup *cc, *copy = copy_config_group(c, TRUE);
        DO_ASSERT_WSTR_EQ(copy->name, L"foo");

        cc = get_child(L"foo4", copy);
        DO_ASSERT(NULL != cc);
        DO_ASSERT_WSTR_EQ(L"eloquent", get_config_group_parameter(cc, L"bar4"));

        cc = get_child(L"foo5", cc);
        DO_ASSERT(NULL != cc);
        DO_ASSERT_WSTR_EQ(L"dull", get_config_group_parameter(cc, L"bar5"));

        free_config_group(copy);

        copy = copy_config_group(c, FALSE);
        DO_ASSERT_WSTR_EQ(copy->name, L"foo");
        DO_ASSERT_WSTR_EQ(L"wise", get_config_group_parameter(copy, L"bar"));

        cc = get_child(L"foo4", copy);
        DO_ASSERT(NULL == cc);

        free_config_group(copy);
    }

    /* remove child */
    {
        DO_ASSERT(c2 == get_child(L"foo2", c));
        remove_child(L"foo2", c);
        DO_ASSERT(NULL == get_child(L"foo2", c));

        DO_ASSERT(c3 == get_child(L"foo4", c));
        DO_ASSERT(c4 == get_child(L"foo5", c3));

        DO_ASSERT(c3 == get_child(L"Foo4", c));
        DO_ASSERT(c3 == get_child(L"FOO4", c));

        /* make sure we don't die */
        remove_child(L"foo2", c);
    }

    /* remove parameter */
    {
        set_config_group_parameter(c, L"testremove", L"sucks");
        DO_ASSERT_WSTR_EQ(get_config_group_parameter(c, L"testremove"), L"sucks");
        DO_ASSERT_WSTR_EQ(get_config_group_parameter(c, L"bar"), L"wise");
        remove_config_group_parameter(c, L"testremove");
        DO_ASSERT(NULL == get_config_group_parameter(c, L"testremove"));
        DO_ASSERT_WSTR_EQ(get_config_group_parameter(c, L"bar"), L"wise");

        free_config_group(c);
    }

    /* read config */
    {
        res = read_config_group(&c, L"foo", TRUE);
        DO_ASSERT_HANDLE(res == ERROR_SUCCESS, printf("res=%d\n", res); return -1;);
        DO_ASSERT_WSTR_EQ(c->name, L"foo");
        DO_ASSERT(NULL != get_child(L"foo2", c));
        DO_ASSERT(NULL != get_child(L"foo4", c));
        DO_ASSERT(NULL != get_child(L"foo5", get_child(L"foo4", c)));

        free_config_group(c);
    }

    /* read deep config */
    {
        res = read_config_group(&c, L"foo:foo2", TRUE);
        DO_ASSERT(res == ERROR_SUCCESS);
        DO_DEBUG(DL_VERB, printf("c->name: >%S<\n", c->name););
        DO_ASSERT_WSTR_EQ(c->name, L"foo:foo2");
        free_config_group(c);
    }

    /* read deeper config */
    {
        res = read_config_group(&c, L"foo:foo4:foo5", TRUE);
        DO_ASSERT(res == ERROR_SUCCESS);
        DO_ASSERT_WSTR_EQ(c->name, L"foo:foo4:foo5");
        DO_ASSERT_WSTR_EQ(get_config_group_parameter(c, L"bar5"), L"dull");
        free_config_group(c);
    }

    /* test write */
    {
        res = read_config_group(&c, L"foo:foo4:foo5", TRUE);
        DO_ASSERT(res == ERROR_SUCCESS);
        set_config_group_parameter(c, L"testwrite", L"rocks");
        DO_ASSERT(get_config_group_parameter(c, L"testwrite") != NULL);
        DO_ASSERT_WSTR_EQ(get_config_group_parameter(c, L"testwrite"), L"rocks");
        res = write_config_group(c);
        DO_ASSERT(res == ERROR_SUCCESS);
        free_config_group(c);

        res = read_config_group(&c, L"foo:foo4:foo5", TRUE);
        DO_ASSERT(res == ERROR_SUCCESS);
        DO_ASSERT_WSTR_EQ(get_config_group_parameter(c, L"testwrite"), L"rocks");
        DO_ASSERT_WSTR_EQ(get_config_group_parameter(c, L"bar5"), L"dull");
        free_config_group(c);
    }

    /* test write patches */
    {
        ConfigGroup *hp = new_config_group(L_DYNAMORIO_VAR_HOT_PATCH_MODES);

        c = new_config_group(L"testapp.exe");
        DO_ASSERT(c != NULL);
        set_config_group_parameter(hp, L"TEST.0001", L"0");
        set_config_group_parameter(hp, L"TEST.0002", L"1");
        set_config_group_parameter(hp, L"MS06.019A", L"2");
        add_config_group(c, hp);
        hp = get_child(L_DYNAMORIO_VAR_HOT_PATCH_MODES, c);
        DO_ASSERT(hp != NULL);
        DO_ASSERT(get_config_group_parameter(hp, L"TEST.0001") != NULL);
        DO_ASSERT_WSTR_EQ(get_config_group_parameter(hp, L"TEST.0001"), L"0");
        res = write_config_group(c);
        DO_ASSERT(res == ERROR_SUCCESS);
        free_config_group(c);

        res = read_config_group(&c, L"testapp.exe", TRUE);
        DO_ASSERT(res == ERROR_SUCCESS);
        hp = get_child(L_DYNAMORIO_VAR_HOT_PATCH_MODES, c);
        DO_ASSERT(hp != NULL);
        DO_ASSERT(get_config_group_parameter(hp, L"TEST.0001") != NULL);
        DO_ASSERT_WSTR_EQ(get_config_group_parameter(hp, L"TEST.0001"), L"0");
        DO_DEBUG(DL_VERB, dump_config_group("", "  ", c, FALSE););
        free_config_group(c);
    }

    /* case insensitive read */
    {
        res = read_config_group(&c, L"FOO:FOO4:FOO5", TRUE);
        DO_ASSERT(res == ERROR_SUCCESS);
        DO_ASSERT_WSTR_EQ(get_config_group_parameter(c, L"bar5"), L"dull");

        free_config_group(c);
    }

    /* write back with foo2 removed */
    {
        res = read_config_group(&c, L"foo", TRUE);
        DO_ASSERT(res == ERROR_SUCCESS);
        DO_ASSERT_WSTR_EQ(c->name, L"foo");
        c->should_clear = TRUE;

        DO_ASSERT(NULL != get_child(L"foo2", c));
        remove_child(L"foo2", c);
        DO_ASSERT(NULL == get_child(L"foo2", c));

        res = write_config_group(c);
        DO_ASSERT(res == ERROR_SUCCESS);
        DO_ASSERT_WSTR_EQ(c->name, L"foo");

        free_config_group(c);

        /* read back and make sure foo2 is still gone */
        res = read_config_group(&c, L"foo", TRUE);
        DO_ASSERT(res == ERROR_SUCCESS);
        DO_ASSERT_WSTR_EQ(c->name, L"foo");

        DO_ASSERT(NULL == get_child(L"foo2", c));
        DO_ASSERT(NULL != get_child(L"foo4", c));
        DO_ASSERT(NULL != get_child(L"foo5", get_child(L"foo4", c)));

        free_config_group(c);
    }

    /* absolute config */
    {
        WCHAR buf[MAX_PATH];
        res = set_config_parameter(L"Software\\Microsoft", TRUE, L"foo", L"bar");
        DO_ASSERT(res == ERROR_SUCCESS);

        res = get_config_parameter(L"Software\\Microsoft", TRUE, L"foo", buf, MAX_PATH);
        DO_ASSERT(res == ERROR_SUCCESS);
        DO_ASSERT_WSTR_EQ(buf, L"bar");

        res = set_config_parameter(L"Software\\Microsoft", TRUE, L"foo", L"rebar");
        DO_ASSERT(res == ERROR_SUCCESS);

        res = get_config_parameter(L"Software\\Microsoft", TRUE, L"foo", buf, MAX_PATH);
        DO_ASSERT(res == ERROR_SUCCESS);
        DO_ASSERT_WSTR_EQ(buf, L"rebar");

        res = set_config_parameter(L"Software\\Microsoft", TRUE, L"foo", NULL);
        DO_ASSERT(res == ERROR_SUCCESS);

        res = get_config_parameter(L"Software\\Microsoft", TRUE, L"foo", buf, MAX_PATH);
        DO_ASSERT(res != ERROR_SUCCESS);
    }

    /* qnames */
    {
        c = new_config_group(L"apps");
        c2 = new_config_group(L"bar.exe");
        c3 = new_config_group(L"bar.exe-bar");
        c4 = new_config_group(L"foo.exe");

        add_config_group(c, c2);
        add_config_group(c, c3);
        add_config_group(c, c4);

        set_config_group_parameter(c2, L"DYNAMORIO_RUNUNDER", L"48");
        set_config_group_parameter(c3, L"DYNAMORIO_RUNUNDER", L"49");

        cp = get_qualified_config_group(c, L"bar.exe", L"bar.exe /bar");
        DO_ASSERT(cp != NULL);

        cp = get_qualified_config_group(c, L"bar.exe", L"bar.exe /b /a /r");
        DO_ASSERT(cp != NULL);

        cp = get_qualified_config_group(c, L"bar.exe", L"bar.exe /c bar");
        DO_ASSERT(cp != NULL);

        cp = get_qualified_config_group(c, L"bar.exe", L"bar.exe /c Bar");
        DO_ASSERT(cp != NULL);

        cp = get_qualified_config_group(c, L"bar.exe", L"Bar.exe /c bar");
        DO_ASSERT(cp != NULL);

        DO_ASSERT(is_parent_of_qualified_config_group(c2));
        DO_ASSERT(is_parent_of_qualified_config_group(c3));
        DO_ASSERT(!is_parent_of_qualified_config_group(c4));

        set_config_group_parameter(c4, L"DYNAMORIO_RUNUNDER", L"5");
        DO_ASSERT(!is_parent_of_qualified_config_group(c4));
    }

    /* autoinject tests */
    {
        res = unset_autoinjection();
        DO_ASSERT(res == ERROR_SUCCESS);

        res = set_autoinjection();
        DO_ASSERT(res == ERROR_SUCCESS);

        res = unset_autoinjection();
        DO_ASSERT(res == ERROR_SUCCESS);
    }

    printf("All Test Passed\n");

    return 0;
}

#endif