/***********************************************************************************************************************************
Command and Option Parse
***********************************************************************************************************************************/
#include "build.auto.h"

#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>

#include "common/debug.h"
#include "common/io/bufferRead.h"
#include "common/log.h"
#include "common/macro.h"
#include "common/memContext.h"
#include "common/regExp.h"
#include "config/common.h"
#include "config/config.intern.h"
#include "config/parse.h"
#include "version.h"

/***********************************************************************************************************************************
The maximum number of keys that an indexed option can have, e.g. pg256-path would be the maximum pg-path option
***********************************************************************************************************************************/
#define CFG_OPTION_KEY_MAX                                          256

/***********************************************************************************************************************************
Section enum - defines which sections of the config an option can appear in
***********************************************************************************************************************************/
typedef enum
{
    cfgSectionCommandLine,                                          // Command-line only
    cfgSectionGlobal,                                               // Command-line or in any config section
    cfgSectionStanza,                                               // Command-line or in any config stanza section
} ConfigSection;

/***********************************************************************************************************************************
Standard config file name and old default path and name
***********************************************************************************************************************************/
#define PGBACKREST_CONFIG_ORIG_PATH_FILE                            "/etc/" PROJECT_CONFIG_FILE
STRING_STATIC(PGBACKREST_CONFIG_ORIG_PATH_FILE_STR,                 PGBACKREST_CONFIG_ORIG_PATH_FILE);

/***********************************************************************************************************************************
In some environments this will not be externed
***********************************************************************************************************************************/
extern char **environ;

/***********************************************************************************************************************************
Mem context and local variables
***********************************************************************************************************************************/
static struct ConfigParseLocal
{
    MemContext *memContext;                                         // Mem context
    Ini *ini;                                                       // Parsed ini data
} configParseLocal;

/***********************************************************************************************************************************
Define how a command is parsed
***********************************************************************************************************************************/
typedef struct ParseRuleCommand
{
    const char *name;                                               // Name
    unsigned int commandRoleValid : CFG_COMMAND_ROLE_TOTAL;         // Valid for the command role?
    bool lockRequired : 1;                                          // Is an immediate lock required?
    bool lockRemoteRequired : 1;                                    // Is a lock required on the remote?
    unsigned int lockType : 2;                                      // Lock type required
    bool logFile : 1;                                               // Will the command log to a file?
    unsigned int logLevelDefault : 4;                               // Default log level
    bool parameterAllowed : 1;                                      // Command-line parameters are allowed
} ParseRuleCommand;

// Macros used to define parse rules in parse.auto.c.inc in a format that diffs well
#define PARSE_RULE_COMMAND(...)                                                                                                    \
    {__VA_ARGS__}

#define PARSE_RULE_COMMAND_NAME(nameParam)                                                                                         \
    .name = nameParam

#define PARSE_RULE_COMMAND_ROLE_VALID_LIST(...)                                                                                    \
    .commandRoleValid = 0 __VA_ARGS__

#define PARSE_RULE_COMMAND_ROLE(commandRoleParam)                                                                                  \
    | (1 << commandRoleParam)

#define PARSE_RULE_COMMAND_LOCK_REQUIRED(lockRequiredParam)                                                                        \
    .lockRequired = lockRequiredParam

#define PARSE_RULE_COMMAND_LOCK_REMOTE_REQUIRED(lockRemoteRequiredParam)                                                           \
    .lockRemoteRequired = lockRemoteRequiredParam

#define PARSE_RULE_COMMAND_LOCK_TYPE(lockTypeParam)                                                                                \
    .lockType = lockTypeParam

#define PARSE_RULE_COMMAND_LOG_FILE(logFileParam)                                                                                  \
    .logFile = logFileParam

#define PARSE_RULE_COMMAND_LOG_LEVEL_DEFAULT(logLevelDefaultParam)                                                                 \
    .logLevelDefault = logLevelDefaultParam

#define PARSE_RULE_COMMAND_PARAMETER_ALLOWED(parameterAllowedParam)                                                                \
    .parameterAllowed = parameterAllowedParam

/***********************************************************************************************************************************
Define how an option group is parsed
***********************************************************************************************************************************/
typedef struct ParseRuleOptionGroup
{
    const char *name;                                               // All options in the group must be prefixed with this name
} ParseRuleOptionGroup;

// Macros used to define parse rules in parse.auto.c.inc in a format that diffs well
#define PARSE_RULE_OPTION_GROUP(...)                                                                                               \
    {__VA_ARGS__}

#define PARSE_RULE_OPTION_GROUP_NAME(nameParam)                                                                                    \
    .name = nameParam

/***********************************************************************************************************************************
Define how an option is parsed and interacts with other options
***********************************************************************************************************************************/
typedef struct ParseRuleOption
{
    const char *name;                                               // Name
    unsigned int type : 4;                                          // e.g. string, int, boolean
    bool beta : 1;                                                  // Is the option a beta feature?
    bool negate : 1;                                                // Can the option be negated on the command line?
    bool reset : 1;                                                 // Can the option be reset on the command line?
    bool required : 1;                                              // Is the option required?
    unsigned int section : 2;                                       // e.g. global, stanza, cmd-line
    bool secure : 1;                                                // Needs to be redacted in logs and cmd-line?
    bool multi : 1;                                                 // Can be specified multiple times?
    bool group : 1;                                                 // In a group?
    unsigned int groupId : 1;                                       // Id if in a group
    bool deprecateMatch : 1;                                        // Does a deprecated name exactly match the option name?
    unsigned int packSize : 7;                                      // Size of optional data in pack format
    uint32_t commandRoleValid[CFG_COMMAND_ROLE_TOTAL];              // Valid for the command role?

    const unsigned char *pack;                                      // Optional data in pack format
} ParseRuleOption;

// Define additional types of data that can be associated with an option. Because these types are rare they are not given dedicated
// fields and are instead packed and read at runtime. This may seem inefficient but they are only accessed a single time during
// parse so space efficiency is more important than performance.
typedef enum
{
    parseRuleOptionalTypeValid = 1,
    parseRuleOptionalTypeAllowRange,
    parseRuleOptionalTypeAllowList,
    parseRuleOptionalTypeDefault,
    parseRuleOptionalTypeRequired,
} ParseRuleOptionalType;

// Optional rule filter types
typedef enum
{
    parseRuleFilterTypeCommand = 1,
} ParseRuleFilterType;

// Macros used to define parse rules in parse.auto.c.inc in a format that diffs well
#define PARSE_RULE_OPTION(...)                                                                                                     \
    {__VA_ARGS__}

#define PARSE_RULE_OPTION_NAME(nameParam)                                                                                          \
    .name = nameParam

#define PARSE_RULE_OPTION_TYPE(typeParam)                                                                                          \
    .type = typeParam

#define PARSE_RULE_OPTION_BETA(betaParam)                                                                                          \
    .beta = betaParam

#define PARSE_RULE_OPTION_NEGATE(negateParam)                                                                                      \
    .negate = negateParam

#define PARSE_RULE_OPTION_RESET(resetParam)                                                                                        \
    .reset = resetParam

#define PARSE_RULE_OPTION_REQUIRED(requiredParam)                                                                                  \
    .required = requiredParam

#define PARSE_RULE_OPTION_SECTION(sectionParam)                                                                                    \
    .section = sectionParam

#define PARSE_RULE_OPTION_SECURE(secureParam)                                                                                      \
    .secure = secureParam

#define PARSE_RULE_OPTION_MULTI(typeMulti)                                                                                         \
    .multi = typeMulti

#define PARSE_RULE_OPTION_GROUP_MEMBER(groupParam)                                                                                 \
    .group = groupParam

#define PARSE_RULE_OPTION_GROUP_ID(groupIdParam)                                                                                   \
    .groupId = groupIdParam

#define PARSE_RULE_OPTION_DEPRECATE_MATCH(deprecateMatchParam)                                                                     \
    .deprecateMatch = deprecateMatchParam

#define PARSE_RULE_OPTION_COMMAND_ROLE_MAIN_VALID_LIST(...)                                                                        \
    .commandRoleValid[cfgCmdRoleMain] = 0 __VA_ARGS__

#define PARSE_RULE_OPTION_COMMAND_ROLE_ASYNC_VALID_LIST(...)                                                                       \
    .commandRoleValid[cfgCmdRoleAsync] = 0 __VA_ARGS__

#define PARSE_RULE_OPTION_COMMAND_ROLE_LOCAL_VALID_LIST(...)                                                                       \
    .commandRoleValid[cfgCmdRoleLocal] = 0 __VA_ARGS__

#define PARSE_RULE_OPTION_COMMAND_ROLE_REMOTE_VALID_LIST(...)                                                                      \
    .commandRoleValid[cfgCmdRoleRemote] = 0 __VA_ARGS__

#define PARSE_RULE_OPTION_COMMAND(commandParam)                                                                                    \
    | (1 << commandParam)

#define PARSE_RULE_STRPUB(value)                                    {.buffer = (char *)value, .size = sizeof(value) - 1}

// Macros used to define optional parse rules in pack format
#define PARSE_RULE_VARINT_01(value)                                                                                                \
    value
#define PARSE_RULE_VARINT_02(value)                                                                                                \
    0x80 | (value & 0x7f), (value >> 7) & 0x7f

#define PARSE_RULE_BOOL_TRUE                                        0x28
#define PARSE_RULE_BOOL_FALSE                                       0x20
#define PARSE_RULE_U32_1(value)                                     0x88, PARSE_RULE_VARINT_01(value)
#define PARSE_RULE_U32_2(value)                                     0x88, PARSE_RULE_VARINT_02(value)

#define PARSE_RULE_PACK(...)                                        __VA_ARGS__ 0x00
#define PARSE_RULE_PACK_SIZE(...)                                                                                                  \
    0xf0, 0x02, sizeof((const unsigned char []){PARSE_RULE_PACK(__VA_ARGS__)}),                                                    \
    PARSE_RULE_PACK(__VA_ARGS__)

#define PARSE_RULE_VAL_BOOL_TRUE                                    PARSE_RULE_BOOL_TRUE
#define PARSE_RULE_VAL_BOOL_FALSE                                   PARSE_RULE_BOOL_FALSE

#define PARSE_RULE_OPTIONAL(...)                                                                                                   \
    .packSize = sizeof((const unsigned char []){PARSE_RULE_PACK(__VA_ARGS__)}),                                                    \
    .pack = (const unsigned char []){PARSE_RULE_PACK(__VA_ARGS__)}
#define PARSE_RULE_OPTIONAL_GROUP(...)                              PARSE_RULE_PACK_SIZE(__VA_ARGS__)

#define PARSE_RULE_FILTER_CMD(...)                                                                                                 \
    PARSE_RULE_PACK_SIZE(PARSE_RULE_U32_1(parseRuleFilterTypeCommand), __VA_ARGS__)

#define PARSE_RULE_OPTIONAL_DEPEND(...)                                                                                            \
    PARSE_RULE_U32_1(parseRuleOptionalTypeValid), PARSE_RULE_PACK_SIZE(__VA_ARGS__)
#define PARSE_RULE_OPTIONAL_DEPEND_DEFAULT(value)                   value
#define PARSE_RULE_OPTIONAL_ALLOW_LIST(...)                                                                                        \
    PARSE_RULE_U32_1(parseRuleOptionalTypeAllowList), PARSE_RULE_PACK_SIZE(__VA_ARGS__)
#define PARSE_RULE_OPTIONAL_ALLOW_RANGE(...)                                                                                       \
    PARSE_RULE_U32_1(parseRuleOptionalTypeAllowRange), PARSE_RULE_PACK_SIZE(__VA_ARGS__)
#define PARSE_RULE_OPTIONAL_DEFAULT(...)                                                                                           \
    PARSE_RULE_U32_1(parseRuleOptionalTypeDefault), PARSE_RULE_PACK_SIZE(__VA_ARGS__)
#define PARSE_RULE_OPTIONAL_REQUIRED(...)                                                                                          \
    PARSE_RULE_U32_1(parseRuleOptionalTypeRequired), PARSE_RULE_PACK_SIZE(__VA_ARGS__)
#define PARSE_RULE_OPTIONAL_NOT_REQUIRED(...)                       PARSE_RULE_OPTIONAL_REQUIRED(__VA_ARGS__)

/***********************************************************************************************************************************
Define option deprecations
***********************************************************************************************************************************/
typedef struct ParseRuleOptionDeprecate
{
    const char *name;                                               // Deprecated name
    ConfigOption id;                                                // Option Id
    bool indexed;                                                   // Can the deprecation be indexed?
    bool unindexed;                                                 // Can the deprecation be unindexed?
} ParseRuleOptionDeprecate;

/***********************************************************************************************************************************
Include automatically generated parse data
***********************************************************************************************************************************/
#include "config/parse.auto.c.inc"

/***********************************************************************************************************************************
Struct to hold options parsed from the command line
***********************************************************************************************************************************/
typedef struct ParseOptionValue
{
    bool found : 1;                                                 // Was the option found?
    bool negate : 1;                                                // Was the option negated on the command line?
    bool reset : 1;                                                 // Was the option reset on the command line?
    unsigned int source : 2;                                        // Where was the option found?
    StringList *valueList;                                          // List of values found
} ParseOptionValue;

typedef struct ParseOption
{
    unsigned int indexListTotal;                                    // Total options in indexed list
    ParseOptionValue *indexList;                                    // List of indexed option values
} ParseOption;

#define FUNCTION_LOG_PARSE_OPTION_FORMAT(value, buffer, bufferSize)                                                                \
    typeToLog("ParseOption", buffer, bufferSize)

/***********************************************************************************************************************************
Get the indexed value, creating the array to contain it if needed
***********************************************************************************************************************************/
static ParseOptionValue *
parseOptionIdxValue(ParseOption *optionList, unsigned int optionId, unsigned int optionKeyIdx)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM_P(PARSE_OPTION, optionList);            // Structure containing all options being parsed
        FUNCTION_TEST_PARAM(UINT, optionId);                        // Unique ID which also identifies the option in the parse list
        FUNCTION_TEST_PARAM(UINT, optionKeyIdx);                    // Zero-based key index (e.g. pg3-path => 2), 0 for non-indexed
    FUNCTION_TEST_END();

    // If the requested index is beyond what has already been allocated
    if (optionKeyIdx >= optionList[optionId].indexListTotal)
    {
        // If the option is in a group
        if (parseRuleOption[optionId].group)
        {
            unsigned int optionOffset = 0;

            // Allocate enough memory to include the requested indexed or a fixed amount to avoid too many allocations
            if (optionList[optionId].indexListTotal == 0)
            {
                optionList[optionId].indexListTotal =
                    optionKeyIdx >= (LIST_INITIAL_SIZE / 2) ? optionKeyIdx + 1 : (LIST_INITIAL_SIZE / 2);
                optionList[optionId].indexList = memNew(sizeof(ParseOptionValue) * optionList[optionId].indexListTotal);
            }
            // Allocate more memory when needed. This could be more efficient but the limited number of indexes currently allowed
            // makes it difficult to get coverage on a better implementation.
            else
            {
                optionOffset = optionList[optionId].indexListTotal;
                optionList[optionId].indexListTotal = optionKeyIdx + 1;
                optionList[optionId].indexList = memResize(
                    optionList[optionId].indexList, sizeof(ParseOptionValue) * optionList[optionId].indexListTotal);
            }

            // Initialize the newly allocated memory
            for (unsigned int optKeyIdx = optionOffset; optKeyIdx < optionList[optionId].indexListTotal; optKeyIdx++)
                optionList[optionId].indexList[optKeyIdx] = (ParseOptionValue){0};
        }
        // Else the option is not in a group so there can only be one value
        else
        {
            optionList[optionId].indexList = memNew(sizeof(ParseOptionValue));
            optionList[optionId].indexListTotal = 1;
            optionList[optionId].indexList[0] = (ParseOptionValue){0};
        }
    }

    // Return the indexed value
    FUNCTION_TEST_RETURN_TYPE_P(ParseOptionValue, &optionList[optionId].indexList[optionKeyIdx]);
}

/***********************************************************************************************************************************
Check that --beta is set if a beta option is used
***********************************************************************************************************************************/
static void
parseOptionBeta(
    const unsigned int optionId, const unsigned int optionKeyIdx, const bool beta, const ParseOption *const parseOptionBeta)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(UINT, optionId);
        FUNCTION_TEST_PARAM(UINT, optionKeyIdx);
        FUNCTION_TEST_PARAM(BOOL, beta);
        FUNCTION_TEST_PARAM_P(PARSE_OPTION, parseOptionBeta);
    FUNCTION_TEST_END();

    if (beta && optionId != cfgOptBeta && (parseOptionBeta->indexListTotal == 0 || parseOptionBeta->indexList[0].negate))
    {
        THROW_FMT(
            OptionInvalidError,
            "option '%s' is not valid without option '" CFGOPT_BETA "'\n"
            "HINT: beta features require the --" CFGOPT_BETA " option to prevent accidental usage.",
            cfgParseOptionKeyIdxName(optionId, optionKeyIdx));
    }

    FUNCTION_TEST_RETURN_VOID();
}

/***********************************************************************************************************************************
Get command id by name
***********************************************************************************************************************************/
static ConfigCommand
cfgParseCommandId(const char *const commandName)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(STRINGZ, commandName);
    FUNCTION_TEST_END();

    ASSERT(commandName != NULL);

    ConfigCommand commandId;

    for (commandId = 0; commandId < CFG_COMMAND_TOTAL; commandId++)
    {
        if (strcmp(commandName, parseRuleCommand[commandId].name) == 0)
            break;
    }

    FUNCTION_TEST_RETURN(ENUM, commandId);
}

/**********************************************************************************************************************************/
FN_EXTERN const char *
cfgParseCommandName(const ConfigCommand commandId)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(ENUM, commandId);
    FUNCTION_TEST_END();

    ASSERT(commandId < cfgCmdNone);

    FUNCTION_TEST_RETURN_CONST(STRINGZ, parseRuleCommand[commandId].name);
}

/***********************************************************************************************************************************
Convert command role from String to enum and vice versa
***********************************************************************************************************************************/
STRING_STATIC(CONFIG_COMMAND_ROLE_ASYNC_STR,                        CONFIG_COMMAND_ROLE_ASYNC);
STRING_STATIC(CONFIG_COMMAND_ROLE_LOCAL_STR,                        CONFIG_COMMAND_ROLE_LOCAL);
STRING_STATIC(CONFIG_COMMAND_ROLE_REMOTE_STR,                       CONFIG_COMMAND_ROLE_REMOTE);

static ConfigCommandRole
cfgParseCommandRoleEnum(const String *const commandRole)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(STRING, commandRole);
    FUNCTION_TEST_END();

    if (commandRole == NULL)
        FUNCTION_TEST_RETURN(ENUM, cfgCmdRoleMain);
    else if (strEq(commandRole, CONFIG_COMMAND_ROLE_ASYNC_STR))
        FUNCTION_TEST_RETURN(ENUM, cfgCmdRoleAsync);
    else if (strEq(commandRole, CONFIG_COMMAND_ROLE_LOCAL_STR))
        FUNCTION_TEST_RETURN(ENUM, cfgCmdRoleLocal);
    else if (strEq(commandRole, CONFIG_COMMAND_ROLE_REMOTE_STR))
        FUNCTION_TEST_RETURN(ENUM, cfgCmdRoleRemote);

    THROW_FMT(CommandInvalidError, "invalid command role '%s'", strZ(commandRole));
}

FN_EXTERN const String *
cfgParseCommandRoleStr(const ConfigCommandRole commandRole)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(ENUM, commandRole);
    FUNCTION_TEST_END();

    const String *result = NULL;

    switch (commandRole)
    {
        case cfgCmdRoleMain:
            break;

        case cfgCmdRoleAsync:
            result = CONFIG_COMMAND_ROLE_ASYNC_STR;
            break;

        case cfgCmdRoleLocal:
            result = CONFIG_COMMAND_ROLE_LOCAL_STR;
            break;

        case cfgCmdRoleRemote:
            result = CONFIG_COMMAND_ROLE_REMOTE_STR;
            break;
    }

    FUNCTION_TEST_RETURN_CONST(STRING, result);
}

/**********************************************************************************************************************************/
FN_EXTERN String *
cfgParseCommandRoleName(const ConfigCommand commandId, const ConfigCommandRole commandRoleId)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(ENUM, commandId);
        FUNCTION_TEST_PARAM(ENUM, commandRoleId);
    FUNCTION_TEST_END();

    String *const result = strCatZ(strNew(), cfgParseCommandName(commandId));

    if (commandRoleId != cfgCmdRoleMain)
        strCatFmt(result, ":%s", strZ(cfgParseCommandRoleStr(commandRoleId)));

    FUNCTION_TEST_RETURN(STRING, result);
}

/**********************************************************************************************************************************/
FN_EXTERN const Ini *
cfgParseIni(void)
{
    FUNCTION_TEST_VOID();
    FUNCTION_TEST_RETURN(INI, configParseLocal.ini);
}

/**********************************************************************************************************************************/
FN_EXTERN StringList *
cfgParseStanzaList(void)
{
    FUNCTION_TEST_VOID();

    StringList *const result = strLstNew();

    if (configParseLocal.ini != NULL)
    {
        MEM_CONTEXT_TEMP_BEGIN()
        {
            const StringList *const sectionList = strLstSort(iniSectionList(configParseLocal.ini), sortOrderAsc);

            for (unsigned int sectionIdx = 0; sectionIdx < strLstSize(sectionList); sectionIdx++)
            {
                const String *const section = strLstGet(sectionList, sectionIdx);

                // Skip global sections
                if (strEqZ(section, CFGDEF_SECTION_GLOBAL) || strBeginsWithZ(section, CFGDEF_SECTION_GLOBAL ":"))
                    continue;

                // Extract stanza
                const StringList *const sectionPart = strLstNewSplitZ(section, ":");
                ASSERT(strLstSize(sectionPart) <= 2);

                strLstAddIfMissing(result, strLstGet(sectionPart, 0));
            }
        }
        MEM_CONTEXT_TEMP_END();
    }

    strLstSort(result, sortOrderAsc);

    FUNCTION_TEST_RETURN(STRING_LIST, result);
}

/***********************************************************************************************************************************
Find an option by name in the option list
***********************************************************************************************************************************/
#define OPTION_PREFIX_NEGATE                                        "no-"
#define OPTION_PREFIX_RESET                                         "reset-"
#define OPTION_NAME_SIZE_MAX                                        64

FN_EXTERN CfgParseOptionResult
cfgParseOption(const String *const optionCandidate, const CfgParseOptionParam param)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(STRING, optionCandidate);
        FUNCTION_TEST_PARAM(BOOL, param.prefixMatch);
        FUNCTION_TEST_PARAM(BOOL, param.ignoreMissingIndex);
    FUNCTION_TEST_END();

    ASSERT(optionCandidate != NULL);

    CfgParseOptionResult result = {0};

    // Copy the option to a buffer so it can be efficiently manipulated
    char optionName[OPTION_NAME_SIZE_MAX + 1];
    size_t optionNameSize = strSize(optionCandidate);

    if (optionNameSize > sizeof(optionName) - 1)
    {
        THROW_FMT(
            OptionInvalidError, "option '%s' exceeds maximum size of " STRINGIFY(OPTION_NAME_SIZE_MAX), strZ(optionCandidate));
    }

    strncpy(optionName, strZ(optionCandidate), sizeof(optionName) - 1);
    optionName[OPTION_NAME_SIZE_MAX] = '\0';

    // If this looks like negate
    if (strncmp(optionName, OPTION_PREFIX_NEGATE, sizeof(OPTION_PREFIX_NEGATE) - 1) == 0)
    {
        result.negate = true;

        // Strip the negate prefix
        optionNameSize -= sizeof(OPTION_PREFIX_NEGATE) - 1;
        memmove(optionName, optionName + (sizeof(OPTION_PREFIX_NEGATE) - 1), optionNameSize + 1);
    }
    // Else if looks like reset
    else if (strncmp(optionName, OPTION_PREFIX_RESET, sizeof(OPTION_PREFIX_RESET) - 1) == 0)
    {
        result.reset = true;

        // Strip the reset prefix
        optionNameSize -= sizeof(OPTION_PREFIX_RESET) - 1;
        memmove(optionName, optionName + (sizeof(OPTION_PREFIX_RESET) - 1), optionNameSize + 1);
    }

    // Indexed options must have at least one dash
    char *const dashPtr = strchr(optionName, '-');
    bool indexed = false;

    if (dashPtr != NULL)
    {
        if (dashPtr == optionName)
            THROW_FMT(OptionInvalidError, "option '%s' cannot begin with a dash", strZ(optionCandidate));

        // Check if the first dash is preceeded by a numeric key and keep a tally of the key
        char *numberPtr = dashPtr;
        unsigned int multiplier = 1;

        while (numberPtr > optionName && isdigit(*(numberPtr - 1)))
        {
            numberPtr--;

            result.keyIdx += (unsigned int)(*numberPtr - '0') * multiplier;
            multiplier *= 10;
        }

        if (numberPtr == optionName)
            THROW_FMT(OptionInvalidError, "option '%s' cannot begin with a number", strZ(optionCandidate));

        // If there was a number then the option is indexed
        if (numberPtr != dashPtr)
        {
            indexed = true;

            // Strip the key to get the base option name
            optionNameSize -= (size_t)(dashPtr - numberPtr);
            memmove(numberPtr, dashPtr, optionNameSize + 1);

            // Check that the index does not exceed the maximum
            if (result.keyIdx > CFG_OPTION_KEY_MAX)
            {
                THROW_FMT(
                    OptionInvalidError, "option '%s' key exceeds maximum of " STRINGIFY(CFG_OPTION_KEY_MAX), strZ(optionCandidate));
            }

            // Subtract one to represent a key index
            result.keyIdx--;
        }
    }

    // Search for an exact match. A copy of the option name must be made because bsearch() requires a reference.
    const char *const optionNamePtr = optionName;

    const ParseRuleOption *optionFound = bsearch(
        &optionNamePtr, parseRuleOption, CFG_OPTION_TOTAL, sizeof(ParseRuleOption), lstComparatorZ);

    // If the option was not found
    if (optionFound == NULL)
    {
        // Search for a single partial match (if requested)
        if (param.prefixMatch)
        {
            unsigned int findPartialIdx = 0;
            unsigned int findPartialTotal = 0;

            for (unsigned int findIdx = 0; findIdx < CFG_OPTION_TOTAL; findIdx++)
            {
                if (strncmp(parseRuleOption[findIdx].name, optionName, optionNameSize) == 0)
                {
                    findPartialIdx = findIdx;
                    findPartialTotal++;

                    if (findPartialTotal > 1)
                        break;
                }
            }

            // If a single partial match was found
            if (findPartialTotal == 1)
                optionFound = &parseRuleOption[findPartialIdx];
        }

        // If the option was not found search deprecations
        if (optionFound == NULL)
        {
            // Search deprecations for an exact match
            const ParseRuleOptionDeprecate *deprecate = bsearch(
                &optionNamePtr, parseRuleOptionDeprecate, CFG_OPTION_DEPRECATE_TOTAL, sizeof(ParseRuleOptionDeprecate),
                lstComparatorZ);

            // If the option was not found then search deprecations for a single partial match (if requested)
            if (deprecate == NULL && param.prefixMatch)
            {
                unsigned int findPartialIdx = 0;
                unsigned int findPartialTotal = 0;

                for (unsigned int deprecateIdx = 0; deprecateIdx < CFG_OPTION_DEPRECATE_TOTAL; deprecateIdx++)
                {
                    if (strncmp(parseRuleOptionDeprecate[deprecateIdx].name, optionName, optionNameSize) == 0)
                    {
                        findPartialIdx = deprecateIdx;
                        findPartialTotal++;

                        if (findPartialTotal > 1)
                            break;
                    }
                }

                // If a single partial match was found
                if (findPartialTotal == 1)
                    deprecate = &parseRuleOptionDeprecate[findPartialIdx];
            }

            // Deprecation was found
            if (deprecate != NULL)
            {
                // Error if the option is indexed but the deprecation is not
                if (indexed && !deprecate->indexed)
                    THROW_FMT(OptionInvalidError, "deprecated option '%s' cannot have an index", strZ(optionCandidate));

                // Error if the option is unindexed but the deprecation is not
                if (!indexed && !deprecate->unindexed && !param.ignoreMissingIndex)
                {
                    ASSERT(dashPtr != NULL);

                    const char *const groupName = parseRuleOptionGroup[parseRuleOption[deprecate->id].groupId].name;

                    THROW_FMT(
                        OptionInvalidError, "deprecated option '%s' requires an index\n"
                        "HINT: add the required index, e.g. %.*s1%s.\n"
                        "HINT: consider using the non-deprecated name, e.g. %s1%s.",
                        strZ(optionCandidate), (int)(dashPtr - optionName), optionName, dashPtr, groupName,
                        parseRuleOption[deprecate->id].name + strlen(groupName));
                }

                result.deprecated = true;
                optionFound = &parseRuleOption[deprecate->id];
            }
        }
    }

    // Option was found
    if (optionFound != NULL)
    {
        result.found = true;
        result.id = (unsigned int)(optionFound - parseRuleOption);

        // Error if negate is not allowed
        if (result.negate && !optionFound->negate)
            THROW_FMT(OptionInvalidError, "option '%s' cannot be negated", strZ(optionCandidate));

        // Error if reset is not allowed
        if (result.reset && !optionFound->reset)
            THROW_FMT(OptionInvalidError, "option '%s' cannot be reset", strZ(optionCandidate));

        // It is possible for an unindexed deprecation to match an indexed option name (without the index) exactly. For example, the
        // deprecated repo-path option now maps to repo-path index 0, which will yield an exact match. In this case we still need to
        // mark the option as deprecated.
        if (indexed == false && optionFound->deprecateMatch)
            result.deprecated = true;

        // If not deprecated make sure indexing matches. Deprecation indexing has already been checked because the rules are per
        // deprecation.
        if (!result.deprecated)
        {
            // Error if the option is indexed but should not be
            if (indexed && !optionFound->group)
                THROW_FMT(OptionInvalidError, "option '%s' cannot have an index", strZ(optionCandidate));

            // Error if the option is unindexed but an index is required
            if (!indexed && optionFound->group && !param.ignoreMissingIndex)
            {
                const char *const groupName = parseRuleOptionGroup[optionFound->groupId].name;

                THROW_FMT(
                    OptionInvalidError,
                    "option '%s' requires an index\n"
                    "HINT: add the required index, e.g. %s1%s.",
                    strZ(optionCandidate), groupName, optionFound->name + strlen(groupName));
            }
        }

        // Set flags
        result.beta = optionFound->beta;
        result.multi = optionFound->multi;

        FUNCTION_TEST_RETURN_TYPE(CfgParseOptionResult, result);
    }

    FUNCTION_TEST_RETURN_TYPE(CfgParseOptionResult, (CfgParseOptionResult){0});
}

/**********************************************************************************************************************************/
FN_EXTERN ConfigOptionDataType
cfgParseOptionDataType(const ConfigOption optionId)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(ENUM, optionId);
    FUNCTION_TEST_END();

    ASSERT(optionId < CFG_OPTION_TOTAL);

    switch (parseRuleOption[optionId].type)
    {
        case cfgOptTypeBoolean:
            FUNCTION_TEST_RETURN(ENUM, cfgOptDataTypeBoolean);

        case cfgOptTypeHash:
            FUNCTION_TEST_RETURN(ENUM, cfgOptDataTypeHash);

        case cfgOptTypeInteger:
        case cfgOptTypeSize:
        case cfgOptTypeTime:
            FUNCTION_TEST_RETURN(ENUM, cfgOptDataTypeInteger);

        case cfgOptTypeList:
            FUNCTION_TEST_RETURN(ENUM, cfgOptDataTypeList);

        case cfgOptTypeStringId:
            FUNCTION_TEST_RETURN(ENUM, cfgOptDataTypeStringId);

        default:
            break;
    }

    ASSERT(parseRuleOption[optionId].type == cfgOptTypePath || parseRuleOption[optionId].type == cfgOptTypeString);

    FUNCTION_TEST_RETURN(ENUM, cfgOptDataTypeString);
}

/***********************************************************************************************************************************
Find an optional rule
***********************************************************************************************************************************/
typedef struct CfgParseOptionalRuleState
{
    PackRead *pack;
    unsigned int typeNext;
    bool done;

    // Valid
    const unsigned char *valid;
    size_t validSize;

    // Allow range
    int64_t allowRangeMin;
    int64_t allowRangeMax;

    // Allow list
    const unsigned char *allowList;
    size_t allowListSize;

    // Default
    const String *defaultRaw;
    ConfigOptionValueType defaultValue;

    // Required
    bool required;
} CfgParseOptionalRuleState;

static bool
cfgParseOptionalRule(
    CfgParseOptionalRuleState *optionalRules, ParseRuleOptionalType optionalRuleType, ConfigCommand commandId,
    const ConfigOption optionId)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM_P(VOID, optionalRules);
        FUNCTION_TEST_PARAM(ENUM, optionalRuleType);
        FUNCTION_TEST_PARAM(ENUM, commandId);
        FUNCTION_TEST_PARAM(ENUM, optionId);
    FUNCTION_TEST_END();

    FUNCTION_AUDIT_HELPER();

    ASSERT(optionalRuleType != 0);
    ASSERT(commandId < CFG_COMMAND_TOTAL);
    ASSERT(optionId < CFG_OPTION_TOTAL);

    bool result = false;

    // Check for optional rules
    if (!optionalRules->done && parseRuleOption[optionId].pack != NULL)
    {
        // Initialize optional rules
        if (optionalRules->pack == NULL)
        {
            MEM_CONTEXT_TEMP_BEGIN()
            {
                PackRead *const groupList = pckReadNewC(parseRuleOption[optionId].pack, parseRuleOption[optionId].packSize);

                // Seach for a matching group
                do
                {
                    // Get the group pack
                    PackRead *group = pckReadPackReadConstP(groupList);

                    // Process filters if any
                    pckReadNext(group);

                    if (pckReadType(group) == pckTypePack)
                    {
                        // Check for filter match
                        bool match = false;
                        PackRead *const filter = pckReadPackReadConstP(group);
                        const ParseRuleFilterType filterType = (ParseRuleFilterType)pckReadU32P(filter);

                        switch (filterType)
                        {
                            default:
                            {
                                ASSERT(filterType == parseRuleFilterTypeCommand);

                                while (pckReadNext(filter))
                                {
                                    if ((ConfigCommand)pckReadU32P(filter) == commandId)
                                    {
                                        match = true;
                                        break;
                                    }
                                }

                                break;
                            }
                        }

                        // Filter did not match
                        if (!match)
                            group = NULL;
                    }

                    // If the group matched
                    if (group != NULL)
                    {
                        // Get first optional rule type. This is needed since pckReadNext() has already been called and it cannot
                        // be called again until data is read.
                        optionalRules->typeNext = pckReadU32P(group);
                        ASSERT(optionalRules->typeNext != 0);

                        // Move group to prior context and stop searching
                        optionalRules->pack = pckReadMove(group, memContextPrior());
                        break;
                    }
                }
                while (pckReadNext(groupList));
            }
            MEM_CONTEXT_TEMP_END();

            // If no group matched then done
            if (optionalRules->pack == NULL)
            {
                optionalRules->done = true;
                FUNCTION_TEST_RETURN(BOOL, false);
            }
        }

        // Search for the specified optional rule
        do
        {
            // Read the next optional rule type if it has not already been read
            if (optionalRules->typeNext == 0)
            {
                // If there are no more rules then done
                if (!pckReadNext(optionalRules->pack))
                {
                    optionalRules->done = true;
                    FUNCTION_TEST_RETURN(BOOL, false);
                }

                optionalRules->typeNext = pckReadU32P(optionalRules->pack);
                ASSERT(optionalRules->typeNext != 0);
            }

            // If this is the requested optional rule
            if (optionalRules->typeNext == optionalRuleType)
            {
                // Optional rule was found
                result = true;

                // Process optional rule
                switch (optionalRuleType)
                {
                    case parseRuleOptionalTypeValid:
                    {
                        pckReadNext(optionalRules->pack);

                        optionalRules->valid = pckReadBufPtr(optionalRules->pack);
                        optionalRules->validSize = pckReadSize(optionalRules->pack);

                        pckReadConsume(optionalRules->pack);
                        break;
                    }

                    case parseRuleOptionalTypeAllowList:
                    {
                        pckReadNext(optionalRules->pack);

                        optionalRules->allowList = pckReadBufPtr(optionalRules->pack);
                        optionalRules->allowListSize = pckReadSize(optionalRules->pack);

                        pckReadConsume(optionalRules->pack);
                        break;
                    }

                    case parseRuleOptionalTypeAllowRange:
                    {
                        PackRead *const ruleData = pckReadPackReadConstP(optionalRules->pack);

                        optionalRules->allowRangeMin = parseRuleValueInt[pckReadU32P(ruleData)];
                        optionalRules->allowRangeMax = parseRuleValueInt[pckReadU32P(ruleData)];

                        break;
                    }

                    case parseRuleOptionalTypeDefault:
                    {
                        PackRead *const ruleData = pckReadPackReadConstP(optionalRules->pack);
                        pckReadNext(ruleData);

                        switch (pckReadType(ruleData))
                        {
                            case pckTypeBool:
                                optionalRules->defaultValue.boolean = pckReadBoolP(ruleData);
                                optionalRules->defaultRaw = optionalRules->defaultValue.boolean ? Y_STR : N_STR;
                                break;

                            default:
                            {
                                switch (parseRuleOption[optionId].type)
                                {
                                    case cfgOptTypeInteger:
                                    case cfgOptTypeTime:
                                    case cfgOptTypeSize:
                                    {
                                        optionalRules->defaultValue.integer = parseRuleValueInt[pckReadU32P(ruleData)];
                                        optionalRules->defaultRaw = (const String *)&parseRuleValueStr[pckReadU32P(ruleData)];

                                        break;
                                    }

                                    case cfgOptTypePath:
                                    case cfgOptTypeString:
                                    {
                                        optionalRules->defaultRaw = (const String *)&parseRuleValueStr[pckReadU32P(ruleData)];
                                        optionalRules->defaultValue.string = optionalRules->defaultRaw;

                                        break;
                                    }

                                    case cfgOptTypeStringId:
                                        optionalRules->defaultValue.stringId = parseRuleValueStrId[pckReadU32P(ruleData)];
                                        optionalRules->defaultRaw = (const String *)&parseRuleValueStr[pckReadU32P(ruleData)];
                                        break;
                                }
                            }
                        }

                        break;
                    }

                    default:
                    {
                        ASSERT(optionalRuleType == parseRuleOptionalTypeRequired);

                        optionalRules->required = !parseRuleOption[optionId].required;
                    }
                }
            }
            // Else not the requested optional rule
            else
            {
                // If the optional rule type is greater than requested then return. The optional rule may be requested later.
                if (optionalRules->typeNext > optionalRuleType)
                    FUNCTION_TEST_RETURN(BOOL, false);

                // Consume the unused optional rule
                pckReadConsume(optionalRules->pack);
            }

            // Type will need to be read again on next iteration
            optionalRules->typeNext = 0;
        }
        while (!result);
    }

    FUNCTION_TEST_RETURN(BOOL, result);
}

/***********************************************************************************************************************************
Resolve an option dependency
***********************************************************************************************************************************/
typedef struct CfgParseOptionalFilterDependResult
{
    bool valid;
    bool defaultExists;
    bool defaultValue;
} CfgParseOptionalFilterDependResult;

static CfgParseOptionalFilterDependResult
cfgParseOptionalFilterDepend(PackRead *const filter, const Config *const config, const unsigned int optionListIdx)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(PACK_READ, filter);
        FUNCTION_TEST_PARAM_P(VOID, config);
        FUNCTION_TEST_PARAM(UINT, optionListIdx);
    FUNCTION_TEST_END();

    CfgParseOptionalFilterDependResult result = {.valid = false};

    // Default when the dependency is not resolved, if it exists
    pckReadNext(filter);

    if (pckReadType(filter) == pckTypeBool)
    {
        result.defaultExists = true;
        result.defaultValue = pckReadBoolP(filter);
    }

    // Get the depend option value
    const ConfigOption dependId = (ConfigOption)pckReadU32P(filter);
    ASSERT(config->option[dependId].index != NULL);
    const ConfigOptionValue *const dependValue =
        &config->option[dependId].index[parseRuleOption[dependId].group ? optionListIdx : 0];

    // Is the dependency resolved?
    if (dependValue->set)
    {
        // If a depend list exists, make sure the value is in the list
        if (pckReadNext(filter))
        {
            do
            {
                switch (cfgParseOptionDataType(dependId))
                {
                    case cfgOptDataTypeBoolean:
                        result.valid = pckReadBoolP(filter) == dependValue->value.boolean;
                        break;

                    default:
                    {
                        ASSERT(cfgParseOptionDataType(dependId) == cfgOptDataTypeStringId);

                        if (parseRuleValueStrId[pckReadU32P(filter)] == dependValue->value.stringId)
                            result.valid = true;
                        break;
                    }
                }
            }
            while (pckReadNext(filter));
        }
        else
            result.valid = true;
    }

    FUNCTION_TEST_RETURN_TYPE(CfgParseOptionalFilterDependResult, result);
}

/**********************************************************************************************************************************/
FN_EXTERN const String *
cfgParseOptionDefault(ConfigCommand commandId, ConfigOption optionId)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(ENUM, commandId);
        FUNCTION_TEST_PARAM(ENUM, optionId);
    FUNCTION_TEST_END();

    ASSERT(commandId < CFG_COMMAND_TOTAL);
    ASSERT(optionId < CFG_OPTION_TOTAL);

    const String *result = NULL;

    MEM_CONTEXT_TEMP_BEGIN()
    {
        CfgParseOptionalRuleState optionalRules = {0};

        if (cfgParseOptionalRule(&optionalRules, parseRuleOptionalTypeDefault, commandId, optionId))
            result = optionalRules.defaultRaw;
    }
    MEM_CONTEXT_TEMP_END();

    FUNCTION_TEST_RETURN_CONST(STRING, result);
}

/**********************************************************************************************************************************/
FN_EXTERN const char *
cfgParseOptionName(ConfigOption optionId)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(ENUM, optionId);
    FUNCTION_TEST_END();

    ASSERT(optionId < CFG_OPTION_TOTAL);

    FUNCTION_TEST_RETURN_CONST(STRINGZ, parseRuleOption[optionId].name);
}

/**********************************************************************************************************************************/
FN_EXTERN const char *
cfgParseOptionKeyIdxName(ConfigOption optionId, unsigned int keyIdx)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(ENUM, optionId);
        FUNCTION_TEST_PARAM(UINT, keyIdx);
    FUNCTION_TEST_END();

    ASSERT(optionId < CFG_OPTION_TOTAL);
    ASSERT((!parseRuleOption[optionId].group && keyIdx == 0) || parseRuleOption[optionId].group);

    // If the option is in a group then construct the name
    if (parseRuleOption[optionId].group)
    {
        FUNCTION_TEST_RETURN_CONST(
            STRINGZ,
            zNewFmt(
                "%s%u%s", parseRuleOptionGroup[parseRuleOption[optionId].groupId].name, keyIdx + 1,
                parseRuleOption[optionId].name + strlen(parseRuleOptionGroup[parseRuleOption[optionId].groupId].name)));
    }

    // Else return the stored name
    FUNCTION_TEST_RETURN_CONST(STRINGZ, parseRuleOption[optionId].name);
}

/**********************************************************************************************************************************/
FN_EXTERN bool
cfgParseOptionRequired(ConfigCommand commandId, ConfigOption optionId)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(ENUM, commandId);
        FUNCTION_TEST_PARAM(ENUM, optionId);
    FUNCTION_TEST_END();

    ASSERT(commandId < CFG_COMMAND_TOTAL);
    ASSERT(optionId < CFG_OPTION_TOTAL);

    bool found = false;
    bool result = false;

    MEM_CONTEXT_TEMP_BEGIN()
    {
        CfgParseOptionalRuleState optionalRules = {0};

        if (cfgParseOptionalRule(&optionalRules, parseRuleOptionalTypeRequired, commandId, optionId))
        {
            found = true;
            result = optionalRules.required;
        }
    }
    MEM_CONTEXT_TEMP_END();

    if (found)
        FUNCTION_TEST_RETURN(BOOL, result);

    FUNCTION_TEST_RETURN(BOOL, parseRuleOption[optionId].required);
}

/**********************************************************************************************************************************/
FN_EXTERN bool
cfgParseOptionSecure(ConfigOption optionId)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(ENUM, optionId);
    FUNCTION_TEST_END();

    ASSERT(optionId < CFG_OPTION_TOTAL);

    FUNCTION_TEST_RETURN(BOOL, parseRuleOption[optionId].secure);
}

/**********************************************************************************************************************************/
FN_EXTERN ConfigOptionType
cfgParseOptionType(ConfigOption optionId)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(ENUM, optionId);
    FUNCTION_TEST_END();

    ASSERT(optionId < CFG_OPTION_TOTAL);

    FUNCTION_TEST_RETURN(ENUM, parseRuleOption[optionId].type);
}

/**********************************************************************************************************************************/
FN_EXTERN bool
cfgParseOptionValid(ConfigCommand commandId, ConfigCommandRole commandRoleId, ConfigOption optionId)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(ENUM, commandId);
        FUNCTION_TEST_PARAM(ENUM, commandRoleId);
        FUNCTION_TEST_PARAM(ENUM, optionId);
    FUNCTION_TEST_END();

    ASSERT(commandId < CFG_COMMAND_TOTAL);
    ASSERT(optionId < CFG_OPTION_TOTAL);

    FUNCTION_TEST_RETURN(BOOL, parseRuleOption[optionId].commandRoleValid[commandRoleId] & ((uint32_t)1 << commandId));
}

/***********************************************************************************************************************************
Load the configuration file(s)

The parent mem context is used. Defaults are passed to make testing easier.

Rules:
- config and config-include-path are default. In this case, the config file will be loaded, if it exists, and *.conf files in the
  config-include-path will be appended, if they exist. A missing/empty dir will be ignored except that the original default
  for the config file will be attempted to be loaded if the current default is not found.
- config only is specified. Only the specified config file will be loaded and is required. The default config-include-path will be
  ignored.
- config and config-path are specified. The specified config file will be loaded and is required. The overridden default of the
  config-include-path (<config-path>/conf.d) will be loaded if exists but is not required.
- config-include-path only is specified. *.conf files in the config-include-path will be loaded and the path is required to exist.
  The default config will be be loaded if it exists.
- config-include-path and config-path are specified. The *.conf files in the config-include-path will be loaded and the directory
  passed must exist. The overridden default of the config file path (<config-path>/pgbackrest.conf) will be loaded if exists but is
  not required.
- If the config and config-include-path are specified. The config file will be loaded and is expected to exist and *.conf files in
  the config-include-path will be appended and at least one is expected to exist.
- If --no-config is specified and --config-include-path is specified then only *.conf files in the config-include-path will be
  loaded; the directory is required.
- If --no-config is specified and --config-path is specified then only *.conf files in the overridden default config-include-path
  (<config-path>/conf.d) will be loaded if exist but not required.
- If --no-config is specified and neither --config-include-path nor --config-path are specified then no configs will be loaded.
- If --config-path only, the defaults for config and config-include-path will be changed to use that as a base path but the files
  will not be required to exist since this is a default override.
***********************************************************************************************************************************/
static void
cfgFileLoadPart(String **config, const Buffer *configPart)
{
    FUNCTION_LOG_BEGIN(logLevelTrace);
        FUNCTION_LOG_PARAM_P(STRING, config);
        FUNCTION_LOG_PARAM(BUFFER, configPart);
    FUNCTION_LOG_END();

    FUNCTION_AUDIT_HELPER();

    if (configPart != NULL)
    {
        // Validate the file by parsing it as an Ini object. If the file is not properly formed, an error will occur.
        if (bufUsed(configPart) > 0)
        {
            iniValid(iniNewP(ioBufferReadNew(configPart)));

            // Create the result config file
            if (*config == NULL)
            {
                *config = strNew();
            }
            // Else add an LF in case the previous file did not end with one
            else
                strCat(*config, LF_STR);

            // Add the config part to the result config file
            strCat(*config, strNewBuf(configPart));
        }
    }

    FUNCTION_LOG_RETURN_VOID();
}

static String *
cfgFileLoad(
    const Storage *storage,                                         // Storage to load configs
    const ParseOption *optionList,                                  // All options and their current settings
    // NOTE: Passing defaults to enable more complete test coverage
    const String *optConfigDefault,                                 // Current default for --config option
    const String *optConfigIncludePathDefault,                      // Current default for --config-include-path option
    const String *origConfigDefault)                                // Original --config option default (/etc/pgbackrest.conf)
{
    FUNCTION_LOG_BEGIN(logLevelTrace);
        FUNCTION_LOG_PARAM(STORAGE, storage);
        FUNCTION_LOG_PARAM_P(PARSE_OPTION, optionList);
        FUNCTION_LOG_PARAM(STRING, optConfigDefault);
        FUNCTION_LOG_PARAM(STRING, optConfigIncludePathDefault);
        FUNCTION_LOG_PARAM(STRING, origConfigDefault);
    FUNCTION_LOG_END();

    FUNCTION_AUDIT_HELPER();

    ASSERT(optionList != NULL);
    ASSERT(optConfigDefault != NULL);
    ASSERT(optConfigIncludePathDefault != NULL);
    ASSERT(origConfigDefault != NULL);

    bool loadConfig = true;
    bool loadConfigInclude = true;

    // If the option is specified on the command line, then found will be true meaning the file is required to exist,
    // else it is optional
    bool configFound = optionList[cfgOptConfig].indexList != NULL && optionList[cfgOptConfig].indexList[0].found;
    bool configRequired = configFound;
    bool configPathRequired = optionList[cfgOptConfigPath].indexList != NULL && optionList[cfgOptConfigPath].indexList[0].found;
    bool configIncludeRequired =
        optionList[cfgOptConfigIncludePath].indexList != NULL && optionList[cfgOptConfigIncludePath].indexList[0].found;

    // Save default for later determining if must check old original default config path
    const String *optConfigDefaultCurrent = optConfigDefault;

    // If the config-path option is found on the command line, then its value will override the base path defaults for config and
    // config-include-path
    if (configPathRequired)
    {
        optConfigDefault = strNewFmt(
            "%s/%s", strZ(strLstGet(optionList[cfgOptConfigPath].indexList[0].valueList, 0)), strBaseZ(optConfigDefault));
        optConfigIncludePathDefault = strNewFmt(
            "%s/%s", strZ(strLstGet(optionList[cfgOptConfigPath].indexList[0].valueList, 0)), PROJECT_CONFIG_INCLUDE_PATH);
    }

    // If the --no-config option was passed then do not load the config file
    if (optionList[cfgOptConfig].indexList != NULL && optionList[cfgOptConfig].indexList[0].negate)
    {
        loadConfig = false;
        configRequired = false;
    }

    // If --config option is specified on the command line but neither the --config-include-path nor the config-path are passed,
    // then do not attempt to load the include files
    if (configFound && !(configPathRequired || configIncludeRequired))
    {
        loadConfigInclude = false;
        configIncludeRequired = false;
    }

    String *result = NULL;

    // Load the main config file
    if (loadConfig)
    {
        const String *configFileName = NULL;

        // Get the config file name from the command-line if it exists else default
        if (configRequired)
            configFileName = strLstGet(optionList[cfgOptConfig].indexList[0].valueList, 0);
        else
            configFileName = optConfigDefault;

        // Load the config file
        Buffer *buffer = storageGetP(storageNewReadP(storage, configFileName, .ignoreMissing = !configRequired));

        // Convert the contents of the file buffer to the config string object
        if (buffer != NULL)
            result = strCatBuf(strNew(), buffer);
        else if (strEq(configFileName, optConfigDefaultCurrent))
        {
            // If config is current default and it was not found, attempt to load the config file from the old default location
            buffer = storageGetP(storageNewReadP(storage, origConfigDefault, .ignoreMissing = !configRequired));

            if (buffer != NULL)
                result = strCatBuf(strNew(), buffer);
        }
    }

    // Load *.conf files from the include directory
    if (loadConfigInclude)
    {
        // Validate the file by parsing it as an Ini object. If the file is not properly formed, an error will occur.
        if (result != NULL)
            iniValid(iniNewP(ioBufferReadNew(BUFSTR(result))));

        const String *configIncludePath = NULL;

        // Get the config include path from the command-line if it exists else default
        if (configIncludeRequired)
            configIncludePath = strLstGet(optionList[cfgOptConfigIncludePath].indexList[0].valueList, 0);
        else
            configIncludePath = optConfigIncludePathDefault;

        // Get a list of conf files from the specified path -error on missing directory if the option was passed on the command line
        StringList *list = storageListP(
            storage, configIncludePath, .expression = STRDEF(".+\\.conf$"), .errorOnMissing = configIncludeRequired,
            .nullOnMissing = !configIncludeRequired);

        // If conf files are found, then add them to the config string
        if (list != NULL && !strLstEmpty(list))
        {
            // Sort the list for reproducibility only -- order does not matter
            strLstSort(list, sortOrderAsc);

            for (unsigned int listIdx = 0; listIdx < strLstSize(list); listIdx++)
            {
                cfgFileLoadPart(
                    &result,
                    storageGetP(
                        storageNewReadP(
                            storage, strNewFmt("%s/%s", strZ(configIncludePath), strZ(strLstGet(list, listIdx))),
                            .ignoreMissing = true)));
            }
        }
    }

    FUNCTION_LOG_RETURN(STRING, result);
}

/***********************************************************************************************************************************
??? Add validation of section names and check all sections for invalid options in the check command. It's too expensive to add the
logic to this critical path code.
***********************************************************************************************************************************/
// Helper to check that option values are valid for conditional builds. This is a bit tricky since the distros used for unit testing
// have all possible features enabled, so this is split out to allow it to be tested independently. The loop variable is
// intentionally integrated into this function to make it obvious if it is omitted from the caller.
FN_EXTERN bool
cfgParseOptionValueCondition(
    bool more, PackRead *const allowList, const bool allowListFound, const unsigned int optionId, const unsigned int optionKeyIdx,
    const String *const valueAllow)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(BOOL, more);
        FUNCTION_TEST_PARAM(PACK_READ, allowList);
        FUNCTION_TEST_PARAM(BOOL, allowListFound);
        FUNCTION_TEST_PARAM(UINT, optionId);
        FUNCTION_TEST_PARAM(UINT, optionKeyIdx);
        FUNCTION_TEST_PARAM(STRING, valueAllow);
    FUNCTION_TEST_END();

    ASSERT(allowList != NULL);
    ASSERT(valueAllow != NULL);

    if (more && pckReadType(allowList) == pckTypeBool)
    {
        pckReadBoolP(allowList);

        if (allowListFound)
        {
            THROW_FMT(
                OptionInvalidValueError,
                PROJECT_NAME " not built with '%s=%s' support\n"
                "HINT: if " PROJECT_NAME " was installed from a package, does the package support this feature?\n"
                "HINT: if " PROJECT_NAME " was built from source, were the required development packages installed?",
                cfgParseOptionKeyIdxName(optionId, optionKeyIdx), strZ(valueAllow));
        }

        more = pckReadNext(allowList);
    }

    FUNCTION_TEST_RETURN(BOOL, more);
}

FN_EXTERN void
cfgParse(const Storage *const storage, const unsigned int argListSize, const char *argList[], const CfgParseParam param)
{
    FUNCTION_LOG_BEGIN(logLevelTrace);
        FUNCTION_LOG_PARAM(STORAGE, storage);
        FUNCTION_LOG_PARAM(UINT, argListSize);
        FUNCTION_LOG_PARAM(CHARPY, argList);
        FUNCTION_LOG_PARAM(BOOL, param.noResetLogLevel);
        FUNCTION_LOG_PARAM(BOOL, param.noConfigLoad);
    FUNCTION_LOG_END();

    // Initialize local mem context
    if (configParseLocal.memContext == NULL)
    {
        MEM_CONTEXT_BEGIN(memContextTop())
        {
            MEM_CONTEXT_NEW_BEGIN(ConfigParse, .childQty = MEM_CONTEXT_QTY_MAX)
            {
                configParseLocal.memContext = MEM_CONTEXT_NEW();
            }
            MEM_CONTEXT_NEW_END();
        }
        MEM_CONTEXT_END();
    }

    MEM_CONTEXT_TEMP_BEGIN()
    {
        // Create the config struct
        Config *config;

        OBJ_NEW_BASE_BEGIN(Config, .childQty = MEM_CONTEXT_QTY_MAX, .allocQty = MEM_CONTEXT_QTY_MAX)
        {
            config = OBJ_NEW_ALLOC();

            *config = (Config)
            {
                .memContext = MEM_CONTEXT_NEW(),
                .command = cfgCmdNone,
                .exe = strNewZ(argList[0]),
            };
        }
        OBJ_NEW_END();

        // Phase 1: parse command line parameters
        // -------------------------------------------------------------------------------------------------------------------------
        // List of parsed options
        ParseOption parseOptionList[CFG_OPTION_TOTAL] = {{0}};

        // Only the first non-option parameter should be treated as a command so track if the command has been set
        bool commandSet = false;
        unsigned int argIgnore = 0;
        bool help = false;
        bool version = false;

        for (unsigned int argListIdx = 1; argListIdx < argListSize; argListIdx++)
        {
            const char *arg = argList[argListIdx];

            // If an option
            if (arg[0] == '-')
            {
                // Options must start with --
                if (arg[1] != '-')
                    THROW_FMT(OptionInvalidError, "option '%s' must begin with --", arg);

                // Consume --
                arg += 2;

                // Get the option name and the value when separated by =
                const String *optionName = NULL;
                const String *optionArg = NULL;

                const char *const equalPtr = strchr(arg, '=');

                if (equalPtr)
                {
                    optionName = strNewZN(arg, (size_t)(equalPtr - arg));
                    optionArg = strNewZ(equalPtr + 1);
                }
                else
                    optionName = strNewZ(arg);

                // Lookup the option name
                CfgParseOptionResult option = cfgParseOptionP(optionName, .prefixMatch = true);

                if (!option.found)
                {
                    // Check for --help shortcut to the help command
                    if (strcmp(arg, CFGCMD_HELP) == 0)
                    {
                        help = true;
                        argIgnore++;
                        continue;
                    }
                    // Else check for --version shortcut to the version command
                    else if (strcmp(arg, CFGCMD_VERSION) == 0)
                    {
                        version = true;
                        argIgnore++;
                        continue;
                    }
                    // Else invalid option
                    else
                        THROW_FMT(OptionInvalidError, "invalid option '--%s'", arg);
                }

                // If the option may have an argument (arguments are optional for boolean options)
                if (!option.negate && !option.reset)
                {
                    // Handle boolean (only y/n allowed as argument)
                    if (parseRuleOption[option.id].type == cfgOptTypeBoolean)
                    {
                        // Validate argument/set negate when argument present
                        if (optionArg != NULL)
                        {
                            if (strEqZ(optionArg, "n"))
                                option.negate = true;
                            else if (!strEqZ(optionArg, "y"))
                            {
                                THROW_FMT(
                                    OptionInvalidValueError, "boolean option '--%s' argument must be 'y' or 'n'", strZ(optionName));
                            }
                        }
                    }
                    // If no argument was found with the option then try the next argument
                    else if (optionArg == NULL)
                    {
                        // Error if there are no more arguments in the list
                        if (argListIdx == argListSize - 1)
                            THROW_FMT(OptionInvalidError, "option '--%s' requires an argument", strZ(optionName));

                        optionArg = strNewZ(argList[++argListIdx]);
                    }
                }
                // Else error if an argument was found with the option
                else if (optionArg != NULL)
                    THROW_FMT(OptionInvalidError, "option '%s' does not allow an argument", strZ(optionName));

                // Error if this option is secure and cannot be passed on the command line
                if (cfgParseOptionSecure(option.id))
                {
                    THROW_FMT(
                        OptionInvalidError,
                        "option '%s' is not allowed on the command-line\n"
                        "HINT: this option could expose secrets in the process list.\n"
                        "HINT: specify the option in a configuration file or an environment variable instead.",
                        cfgParseOptionKeyIdxName(option.id, option.keyIdx));
                }

                // If the option has not been found yet then set it
                ParseOptionValue *optionValue = parseOptionIdxValue(parseOptionList, option.id, option.keyIdx);

                if (!optionValue->found)
                {
                    *optionValue = (ParseOptionValue)
                    {
                        .found = true,
                        .negate = option.negate,
                        .reset = option.reset,
                        .source = cfgSourceParam,
                    };

                    // Only set the argument if the option has one
                    if (optionArg != NULL)
                    {
                        optionValue->valueList = strLstNew();
                        strLstAdd(optionValue->valueList, optionArg);
                    }
                }
                else
                {
                    // Make sure option is not negated more than once. It probably wouldn't hurt anything to accept this case but
                    // there's no point in allowing the user to be sloppy.
                    if (optionValue->negate && option.negate)
                    {
                        THROW_FMT(
                            OptionInvalidError, "option '%s' is negated multiple times",
                            cfgParseOptionKeyIdxName(option.id, option.keyIdx));
                    }

                    // Make sure option is not reset more than once. Same justification as negate.
                    if (optionValue->reset && option.reset)
                    {
                        THROW_FMT(
                            OptionInvalidError, "option '%s' is reset multiple times",
                            cfgParseOptionKeyIdxName(option.id, option.keyIdx));
                    }

                    // Don't allow an option to be both negated and reset
                    if ((optionValue->reset && option.negate) || (optionValue->negate && option.reset))
                    {
                        THROW_FMT(
                            OptionInvalidError, "option '%s' cannot be negated and reset",
                            cfgParseOptionKeyIdxName(option.id, option.keyIdx));
                    }

                    // Don't allow an option to be both set and negated
                    if (optionValue->negate != option.negate)
                    {
                        THROW_FMT(
                            OptionInvalidError, "option '%s' cannot be set and negated",
                            cfgParseOptionKeyIdxName(option.id, option.keyIdx));
                    }

                    // Don't allow an option to be both set and reset
                    if (optionValue->reset != option.reset)
                    {
                        THROW_FMT(
                            OptionInvalidError, "option '%s' cannot be set and reset",
                            cfgParseOptionKeyIdxName(option.id, option.keyIdx));
                    }

                    // Add the argument
                    if (optionArg != NULL && option.multi)
                    {
                        strLstAdd(optionValue->valueList, optionArg);
                    }
                    // Error if the option does not accept multiple arguments
                    else
                    {
                        THROW_FMT(
                            OptionInvalidError, "option '%s' cannot be set multiple times",
                            cfgParseOptionKeyIdxName(option.id, option.keyIdx));
                    }
                }
            }
            // Else command or parameter
            else
            {
                // The first argument should be the command
                if (!commandSet)
                {
                    // Try getting the command from the valid command list
                    config->command = cfgParseCommandId(arg);
                    config->commandRole = cfgCmdRoleMain;

                    // If not successful then a command role may be appended
                    if (config->command == cfgCmdNone)
                    {
                        const StringList *commandPart = strLstNewSplitZ(STR(arg), ":");

                        if (strLstSize(commandPart) == 2)
                        {
                            // Get command id
                            config->command = cfgParseCommandId(strZ(strLstGet(commandPart, 0)));

                            // If command id is valid then get command role id
                            if (config->command != cfgCmdNone)
                                config->commandRole = cfgParseCommandRoleEnum(strLstGet(commandPart, 1));
                        }
                    }

                    // Error when command does not exist
                    if (config->command == cfgCmdNone)
                        THROW_FMT(CommandInvalidError, "invalid command '%s'", arg);

                    // Error when role is not valid for the command
                    if (!(parseRuleCommand[config->command].commandRoleValid & ((unsigned int)1 << config->commandRole)))
                        THROW_FMT(CommandInvalidError, "invalid command/role combination '%s'", arg);

                    if (config->command == cfgCmdHelp && !config->help)
                    {
                        config->command = cfgCmdNone;
                        config->help = true;
                    }
                    else
                        commandSet = true;
                }
                // Additional arguments are command arguments
                else
                {
                    if (config->paramList == NULL)
                    {
                        MEM_CONTEXT_BEGIN(config->memContext)
                        {
                            config->paramList = strLstNew();
                        }
                        MEM_CONTEXT_END();
                    }

                    strLstAddZ(config->paramList, arg);
                }
            }
        }

        // Handle command not found
        if (!commandSet && !config->help)
        {
            // If there are args then error
            if (argListSize - argIgnore > 1)
                THROW_FMT(CommandRequiredError, "no command found");

            // Output help if --help specified or --version not specified
            if (help || !version)
            {
                config->help = true;
            }
            // Else output version
            else
                config->command = cfgCmdVersion;
        }

        // Set command options
        if (config->command != cfgCmdNone)
        {
            config->lockRequired = parseRuleCommand[config->command].lockRequired;
            config->lockRemoteRequired = parseRuleCommand[config->command].lockRemoteRequired;
            config->lockType = (LockType)parseRuleCommand[config->command].lockType;
            config->logFile = parseRuleCommand[config->command].logFile;
            config->logLevelDefault = (LogLevel)parseRuleCommand[config->command].logLevelDefault;
        }

        // Error when parameters found but the command does not allow parameters
        if (config->paramList != NULL && !config->help && !parseRuleCommand[config->command].parameterAllowed)
            THROW(ParamInvalidError, "command does not allow parameters");

        // Enable logging for main role so config file warnings will be output
        if (!param.noResetLogLevel && config->commandRole == cfgCmdRoleMain)
            logInit(logLevelWarn, logLevelWarn, logLevelOff, false, 0, 1, false);

        // Only continue if command options need to be validated, i.e. a real command is running or we are getting help for a
        // specific command and would like to display actual option values in the help.
        if (config->command != cfgCmdNone && config->command != cfgCmdVersion && config->command != cfgCmdHelp)
        {
            // Error if --help or --version passed to command
            if (help || version)
                THROW_FMT(OptionInvalidError, "invalid option '--%s'", help ? CFGCMD_HELP : CFGCMD_VERSION);

            // Phase 2: parse environment variables
            // ---------------------------------------------------------------------------------------------------------------------
            unsigned int environIdx = 0;

            // Loop through all environment variables and look for our env vars by matching the prefix
            while (environ[environIdx] != NULL)
            {
                const char *keyValue = environ[environIdx];
                environIdx++;

                if (strstr(keyValue, PGBACKREST_ENV) == keyValue)
                {
                    // Find the first = char
                    const char *equalPtr = strchr(keyValue, '=');
                    ASSERT(equalPtr != NULL);

                    // Get key and value
                    const String *key = strReplaceChr(
                        strLower(strNewZN(keyValue + PGBACKREST_ENV_SIZE, (size_t)(equalPtr - (keyValue + PGBACKREST_ENV_SIZE)))),
                        '_', '-');
                    const String *value = STR(equalPtr + 1);

                    // Find the option
                    CfgParseOptionResult option = cfgParseOptionP(key);

                    // Warn if the option not found
                    if (!option.found)
                    {
                        LOG_WARN_FMT("environment contains invalid option '%s'", strZ(key));
                        continue;
                    }
                    // Warn if negate option found in env
                    else if (option.negate)
                    {
                        LOG_WARN_FMT("environment contains invalid negate option '%s'", strZ(key));
                        continue;
                    }
                    // Warn if reset option found in env
                    else if (option.reset)
                    {
                        LOG_WARN_FMT("environment contains invalid reset option '%s'", strZ(key));
                        continue;
                    }

                    // Continue if the option is not valid for this command
                    if (!cfgParseOptionValid(config->command, config->commandRole, option.id))
                        continue;

                    if (strSize(value) == 0)
                        THROW_FMT(OptionInvalidValueError, "environment variable '%s' must have a value", strZ(key));

                    // Continue if the option has already been specified on the command line
                    ParseOptionValue *optionValue = parseOptionIdxValue(parseOptionList, option.id, option.keyIdx);

                    if (optionValue->found)
                        continue;

                    optionValue->found = true;
                    optionValue->source = cfgSourceConfig;

                    // Convert boolean to string
                    if (cfgParseOptionType(option.id) == cfgOptTypeBoolean)
                    {
                        if (strEqZ(value, "n"))
                            optionValue->negate = true;
                        else if (!strEqZ(value, "y"))
                            THROW_FMT(OptionInvalidValueError, "environment boolean option '%s' must be 'y' or 'n'", strZ(key));
                    }
                    // Else split list/hash into separate values
                    else if (option.multi)
                    {
                        optionValue->valueList = strLstNewSplitZ(value, ":");
                    }
                    // Else add the string value
                    else
                    {
                        optionValue->valueList = strLstNew();
                        strLstAdd(optionValue->valueList, value);
                    }
                }
            }

            // Phase 3: parse config file unless --no-config passed
            // ---------------------------------------------------------------------------------------------------------------------
            // Load the configuration file(s)
            if (!param.noConfigLoad)
            {
                const String *const configString = cfgFileLoad(
                    storage, parseOptionList,
                    (const String *)&parseRuleValueStr[parseRuleValStrCFGOPTDEF_CONFIG_PATH_SP_QT_FS_QT_SP_PROJECT_CONFIG_FILE],
                    (const String *)&parseRuleValueStr[
                        parseRuleValStrCFGOPTDEF_CONFIG_PATH_SP_QT_FS_QT_SP_PROJECT_CONFIG_INCLUDE_PATH],
                    PGBACKREST_CONFIG_ORIG_PATH_FILE_STR);

                iniFree(configParseLocal.ini);
                configParseLocal.ini = NULL;

                if (configString != NULL)
                {
                    MEM_CONTEXT_BEGIN(configParseLocal.memContext)
                    {
                        configParseLocal.ini = iniNewP(ioBufferReadNew(BUFSTR(configString)), .store = true);
                    }
                    MEM_CONTEXT_END();
                }
            }

            if (configParseLocal.ini != NULL)
            {
                // Get the stanza name
                String *stanza = NULL;

                if (parseOptionList[cfgOptStanza].indexList != NULL)
                    stanza = strLstGet(parseOptionList[cfgOptStanza].indexList[0].valueList, 0);

                // Build list of sections to search for options
                StringList *sectionList = strLstNew();

                if (stanza != NULL)
                {
                    strLstAddFmt(sectionList, "%s:%s", strZ(stanza), cfgParseCommandName(config->command));
                    strLstAdd(sectionList, stanza);
                }

                strLstAddFmt(sectionList, CFGDEF_SECTION_GLOBAL ":%s", cfgParseCommandName(config->command));
                strLstAddZ(sectionList, CFGDEF_SECTION_GLOBAL);

                // Loop through sections to search for options
                for (unsigned int sectionIdx = 0; sectionIdx < strLstSize(sectionList); sectionIdx++)
                {
                    String *section = strLstGet(sectionList, sectionIdx);
                    const StringList *const keyList = iniSectionKeyList(configParseLocal.ini, section);
                    KeyValue *optionFound = kvNew();

                    // Loop through keys to search for options
                    for (unsigned int keyIdx = 0; keyIdx < strLstSize(keyList); keyIdx++)
                    {
                        String *key = strLstGet(keyList, keyIdx);

                        // Find the optionName in the main list
                        CfgParseOptionResult option = cfgParseOptionP(key);

                        // Warn if the option not found
                        if (!option.found)
                        {
                            LOG_WARN_FMT("configuration file contains invalid option '%s'", strZ(key));
                            continue;
                        }
                        // Warn if negate option found in config
                        else if (option.negate)
                        {
                            LOG_WARN_FMT("configuration file contains negate option '%s'", strZ(key));
                            continue;
                        }
                        // Warn if reset option found in config
                        else if (option.reset)
                        {
                            LOG_WARN_FMT("configuration file contains reset option '%s'", strZ(key));
                            continue;
                        }

                        // Warn if this option should be command-line only
                        if (parseRuleOption[option.id].section == cfgSectionCommandLine)
                        {
                            LOG_WARN_FMT("configuration file contains command-line only option '%s'", strZ(key));
                            continue;
                        }

                        // Make sure this option does not appear in the same section with an alternate name
                        const Variant *optionFoundKey = VARUINT64(option.id * CFG_OPTION_KEY_MAX + option.keyIdx);
                        const Variant *optionFoundName = kvGet(optionFound, optionFoundKey);

                        if (optionFoundName != NULL)
                        {
                            THROW_FMT(
                                OptionInvalidError, "configuration file contains duplicate options ('%s', '%s') in section '[%s]'",
                                strZ(key), strZ(varStr(optionFoundName)), strZ(section));
                        }
                        else
                            kvPut(optionFound, optionFoundKey, VARSTR(key));

                        // Continue if the option is not valid for this command
                        if (!cfgParseOptionValid(config->command, config->commandRole, option.id))
                        {
                            // Warn if it is in a command section
                            if (sectionIdx % 2 == 0)
                            {
                                LOG_WARN_FMT(
                                    "configuration file contains option '%s' invalid for section '%s'", strZ(key),
                                    strZ(section));
                                continue;
                            }

                            continue;
                        }

                        // Continue if stanza option is in a global section
                        if (parseRuleOption[option.id].section == cfgSectionStanza &&
                            (strEqZ(section, CFGDEF_SECTION_GLOBAL) || strBeginsWithZ(section, CFGDEF_SECTION_GLOBAL ":")))
                        {
                            LOG_WARN_FMT(
                                "configuration file contains stanza-only option '%s' in global section '%s'", strZ(key),
                                strZ(section));
                            continue;
                        }

                        // Continue if this option has already been found in another section or command-line/environment
                        ParseOptionValue *optionValue = parseOptionIdxValue(parseOptionList, option.id, option.keyIdx);

                        if (optionValue->found)
                            continue;

                        optionValue->found = true;
                        optionValue->source = cfgSourceConfig;

                        // Process list
                        if (iniSectionKeyIsList(configParseLocal.ini, section, key))
                        {
                            // Error if the option cannot be specified multiple times
                            if (!option.multi)
                            {
                                THROW_FMT(
                                    OptionInvalidError, "option '%s' cannot be set multiple times",
                                    cfgParseOptionKeyIdxName(option.id, option.keyIdx));
                            }

                            optionValue->valueList = iniGetList(configParseLocal.ini, section, key);
                        }
                        else
                        {
                            // Get the option value
                            const String *value = iniGet(configParseLocal.ini, section, key);

                            if (strSize(value) == 0)
                            {
                                THROW_FMT(
                                    OptionInvalidValueError, "section '%s', key '%s' must have a value", strZ(section),
                                    strZ(key));
                            }

                            if (cfgParseOptionType(option.id) == cfgOptTypeBoolean)
                            {
                                if (strEqZ(value, "n"))
                                    optionValue->negate = true;
                                else if (!strEqZ(value, "y"))
                                    THROW_FMT(OptionInvalidValueError, "boolean option '%s' must be 'y' or 'n'", strZ(key));
                            }
                            // Else add the string value
                            else
                            {
                                optionValue->valueList = strLstNew();
                                strLstAdd(optionValue->valueList, value);
                            }
                        }
                    }
                }
            }

            // Phase 4: create the config and resolve indexed options for each group
            // ---------------------------------------------------------------------------------------------------------------------
            // Determine how many indexes are used in each group
            bool groupIdxMap[CFG_OPTION_GROUP_TOTAL][CFG_OPTION_KEY_MAX] = {{0}};

            for (unsigned int optionId = 0; optionId < CFG_OPTION_TOTAL; optionId++)
            {
                // Always assign name since it may be needed for error messages
                config->option[optionId].name = parseRuleOption[optionId].name;

                // Is the option valid for this command?
                if (cfgParseOptionValid(config->command, config->commandRole, optionId))
                {
                    config->option[optionId].valid = true;
                    config->option[optionId].dataType = cfgParseOptionDataType(optionId);
                    config->option[optionId].group = parseRuleOption[optionId].group;
                    config->option[optionId].groupId = parseRuleOption[optionId].groupId;
                }
                else
                {
                    // Error if the invalid option was explicitly set on the command-line
                    if (parseOptionList[optionId].indexList != NULL)
                    {
                        THROW_FMT(
                            OptionInvalidError, "option '%s' not valid for command '%s'", cfgParseOptionName(optionId),
                            cfgParseCommandName(config->command));
                    }

                    // Continue to the next option
                    continue;
                }

                // If the option is in a group
                if (parseRuleOption[optionId].group)
                {
                    unsigned int groupId = parseRuleOption[optionId].groupId;

                    config->optionGroup[groupId].valid = true;

                    // Scan the option values to determine which indexes are in use. Store them in a map that will later be scanned
                    // to create a list of just the used indexes.
                    for (unsigned int optionKeyIdx = 0; optionKeyIdx < parseOptionList[optionId].indexListTotal; optionKeyIdx++)
                    {
                        if (parseOptionList[optionId].indexList[optionKeyIdx].found &&
                            !parseOptionList[optionId].indexList[optionKeyIdx].reset)
                        {
                            if (!groupIdxMap[groupId][optionKeyIdx])
                            {
                                config->optionGroup[groupId].indexTotal++;
                                groupIdxMap[groupId][optionKeyIdx] = true;
                            }
                        }
                    }
                }
            }

            // Write the indexes into the group in order
            for (unsigned int groupId = 0; groupId < CFG_OPTION_GROUP_TOTAL; groupId++)
            {
                // Set group name
                config->optionGroup[groupId].name = parseRuleOptionGroup[groupId].name;

                // Skip the group if it is not valid
                if (!config->optionGroup[groupId].valid)
                    continue;

                // Allocate memory for the index to key index map
                MEM_CONTEXT_BEGIN(config->memContext)
                {
                    config->optionGroup[groupId].indexMap = memNew(
                        sizeof(unsigned int) *
                        (config->optionGroup[groupId].indexTotal == 0 ? 1 : config->optionGroup[groupId].indexTotal));
                }
                MEM_CONTEXT_END();

                // If no values were found in any index then use index 0 since all valid groups must have at least one index. This
                // may lead to an error unless all options in the group have defaults but that will be resolved later.
                if (config->optionGroup[groupId].indexTotal == 0)
                {
                    config->optionGroup[groupId].indexTotal = 1;
                    config->optionGroup[groupId].indexMap[0] = 0;
                }
                // Else write the key to index map for the group. This allows translation from keys to indexes and vice versa.
                else
                {
                    unsigned int optionIdxMax = 0;
                    unsigned int optionKeyIdx = 0;

                    // ??? For the pg group, key 1 is required to maintain compatibility with older versions. Before removing this
                    // constraint the pg group remap to key 1 for remotes will need to be dealt with in the protocol/helper module.
                    if (groupId == cfgOptGrpPg)
                    {
                        optionKeyIdx = 1;
                        optionIdxMax = 1;

                        config->optionGroup[groupId].indexMap[0] = 0;
                    }

                    // Write keys into the index map
                    for (; optionKeyIdx < CFG_OPTION_KEY_MAX; optionKeyIdx++)
                    {
                        if (groupIdxMap[groupId][optionKeyIdx])
                        {
                            config->optionGroup[groupId].indexMap[optionIdxMax] = optionKeyIdx;
                            optionIdxMax++;
                        }
                    }
                }
            }

            // Phase 5: validate option definitions and load into configuration
            // ---------------------------------------------------------------------------------------------------------------------
            // Determine whether a group index will be kept based on non-default values
            bool optionGroupIndexKeep[CFG_OPTION_GROUP_TOTAL][CFG_OPTION_KEY_MAX] = {{false}};

            for (unsigned int optionOrderIdx = 0; optionOrderIdx < CFG_OPTION_TOTAL; optionOrderIdx++)
            {
                // Validate options based on the option resolve order. This allows resolving all options in a single pass.
                ConfigOption optionId = optionResolveOrder[optionOrderIdx];

                // Skip this option if it is not valid
                if (!config->option[optionId].valid)
                    continue;

                // Determine the option index total. For options that are not indexed the index total is 1.
                bool optionGroup = parseRuleOption[optionId].group;
                unsigned int optionGroupId = optionGroup ? parseRuleOption[optionId].groupId : UINT_MAX;
                unsigned int optionListIndexTotal = optionGroup ? config->optionGroup[optionGroupId].indexTotal : 1;

                MEM_CONTEXT_BEGIN(config->memContext)
                {
                    config->option[optionId].index = memNew(sizeof(ConfigOptionValue) * optionListIndexTotal);
                }
                MEM_CONTEXT_END();

                // Loop through the option indexes
                ConfigOptionType optionType = cfgParseOptionType(optionId);

                for (unsigned int optionListIdx = 0; optionListIdx < optionListIndexTotal; optionListIdx++)
                {
                    // Get the key index by looking it up in the group or by defaulting to 0 for ungrouped options
                    unsigned optionKeyIdx = optionGroup ? config->optionGroup[optionGroupId].indexMap[optionListIdx] : 0;

                    // Get the parsed value using the key index. Provide a default structure when the value was not found.
                    ParseOptionValue *parseOptionValue =
                        optionKeyIdx < parseOptionList[optionId].indexListTotal ?
                            &parseOptionList[optionId].indexList[optionKeyIdx] : &(ParseOptionValue){0};

                    // Get the location where the value will be stored in the configuration
                    ConfigOptionValue *configOptionValue = &config->option[optionId].index[optionListIdx];

                    // Is the value set for this option?
                    bool optionSet =
                        parseOptionValue->found && (optionType == cfgOptTypeBoolean || !parseOptionValue->negate) &&
                        !parseOptionValue->reset;

                    // Initialize option value and set negate and reset flag
                    *configOptionValue = (ConfigOptionValue){.negate = parseOptionValue->negate, .reset = parseOptionValue->reset};

                    // Is the option valid?
                    CfgParseOptionalRuleState optionalRules = {0};
                    CfgParseOptionalFilterDependResult dependResult = {.valid = true};

                    if (cfgParseOptionalRule(&optionalRules, parseRuleOptionalTypeValid, config->command, optionId))
                    {
                        PackRead *filter = pckReadNewC(optionalRules.valid, optionalRules.validSize);
                        dependResult = cfgParseOptionalFilterDepend(filter, config, optionListIdx);

                        // If depend not resolved and option value is set on the command-line then error. It is OK to have
                        // unresolved options in the config file because they may be there for another command. For instance,
                        // spool-path is only loaded for the archive-push command when archive-async=y, and the presence of
                        // spool-path in the config file should not cause an error here, it will just end up null.
                        if (!dependResult.valid && optionSet && parseOptionValue->source == cfgSourceParam)
                        {
                            PackRead *filter = pckReadNewC(optionalRules.valid, optionalRules.validSize);

                            // If there is a boolean default value just consume it since it is not needed here
                            pckReadNext(filter);

                            if (pckReadType(filter) == pckTypeBool)
                                pckReadBoolP(filter);

                            // Get depend option id and name
                            ConfigOption dependId = pckReadU32P(filter);
                            const String *dependOptionName = STR(
                                cfgParseOptionKeyIdxName(dependId, parseRuleOption[dependId].group ? optionKeyIdx : 0));

                            // If depend value is not set
                            ASSERT(config->option[dependId].index != NULL);

                            if (!config->option[dependId].index[optionListIdx].set)
                            {
                                THROW_FMT(
                                    OptionInvalidError, "option '%s' not valid without option '%s'",
                                    cfgParseOptionKeyIdxName(optionId, optionKeyIdx), strZ(dependOptionName));
                            }

                            // Build type dependent error data
                            const String *errorValue = EMPTY_STR;

                            switch (cfgParseOptionDataType(dependId))
                            {
                                case cfgOptDataTypeBoolean:
                                {
                                    if (!pckReadBoolP(filter))
                                        dependOptionName = strNewFmt("no-%s", strZ(dependOptionName));

                                    break;
                                }

                                default:
                                {
                                    ASSERT(cfgParseOptionDataType(dependId) == cfgOptDataTypeStringId);

                                    String *const errorList = strNew();
                                    unsigned int validSize = 0;

                                    while (pckReadNext(filter))
                                    {
                                        strCatFmt(
                                            errorList, "%s'%s'", validSize != 0 ? ", " : "",
                                            strZ(strIdToStr(parseRuleValueStrId[pckReadU32P(filter)])));

                                        validSize++;
                                    }

                                    ASSERT(validSize > 0);

                                    if (validSize == 1)
                                        errorValue = strNewFmt(" = %s", strZ(errorList));
                                    else
                                        errorValue = strNewFmt(" in (%s)", strZ(errorList));
                                }
                            }

                            THROW_FMT(
                                OptionInvalidError, "option '%s' not valid without option '%s'%s",
                                cfgParseOptionKeyIdxName(optionId, optionKeyIdx), strZ(dependOptionName), strZ(errorValue));
                        }

                        pckReadFree(filter);
                    }

                    if (dependResult.valid)
                    {
                        // Is the option set?
                        if (optionSet)
                        {
                            configOptionValue->set = true;
                            configOptionValue->source = parseOptionValue->source;

                            // Check beta status
                            parseOptionBeta(optionId, optionKeyIdx, parseRuleOption[optionId].beta, &parseOptionList[cfgOptBeta]);

                            if (optionType == cfgOptTypeBoolean)
                            {
                                configOptionValue->value.boolean = !parseOptionValue->negate;
                            }
                            else if (optionType == cfgOptTypeHash)
                            {
                                KeyValue *value = NULL;

                                MEM_CONTEXT_BEGIN(config->memContext)
                                {
                                    value = kvNew();
                                }
                                MEM_CONTEXT_END();

                                for (unsigned int listIdx = 0; listIdx < strLstSize(parseOptionValue->valueList); listIdx++)
                                {
                                    const char *pair = strZ(strLstGet(parseOptionValue->valueList, listIdx));
                                    const char *equal = strchr(pair, '=');

                                    if (equal == NULL)
                                    {
                                        THROW_FMT(
                                            OptionInvalidError, "key/value '%s' not valid for '%s' option",
                                            strZ(strLstGet(parseOptionValue->valueList, listIdx)),
                                            cfgParseOptionKeyIdxName(optionId, optionKeyIdx));
                                    }

                                    kvPut(value, VARSTR(strNewZN(pair, (size_t)(equal - pair))), VARSTRZ(equal + 1));
                                }

                                configOptionValue->value.keyValue = value;
                            }
                            else if (optionType == cfgOptTypeList)
                            {
                                MEM_CONTEXT_BEGIN(config->memContext)
                                {
                                    configOptionValue->value.list = varLstNewStrLst(parseOptionValue->valueList);
                                }
                                MEM_CONTEXT_END();
                            }
                            else
                            {
                                String *value = strLstGet(parseOptionValue->valueList, 0);
                                const String *valueAllow = value;

                                // Preserve original value to display
                                MEM_CONTEXT_BEGIN(config->memContext)
                                {
                                    configOptionValue->display = strDup(value);
                                }
                                MEM_CONTEXT_END();

                                // If a numeric type check that the value is valid
                                if (optionType == cfgOptTypeInteger || optionType == cfgOptTypeSize ||
                                    optionType == cfgOptTypeTime)
                                {
                                    // Check that the value can be converted
                                    TRY_BEGIN()
                                    {
                                        switch (optionType)
                                        {
                                            case cfgOptTypeInteger:
                                                configOptionValue->value.integer = cvtZToInt64(strZ(value));
                                                break;

                                            case cfgOptTypeSize:
                                                configOptionValue->value.integer = cfgParseSize(value);
                                                break;

                                            default:
                                            {
                                                ASSERT(optionType == cfgOptTypeTime);

                                                configOptionValue->value.integer = cfgParseTime(value);
                                                break;
                                            }
                                        }
                                    }
                                    CATCH_ANY()
                                    {
                                        THROW_FMT(
                                            OptionInvalidValueError, "'%s' is not valid for '%s' option", strZ(value),
                                            cfgParseOptionKeyIdxName(optionId, optionKeyIdx));
                                    }
                                    TRY_END();

                                    if (cfgParseOptionalRule(
                                            &optionalRules, parseRuleOptionalTypeAllowRange, config->command, optionId) &&
                                        (configOptionValue->value.integer < optionalRules.allowRangeMin ||
                                         configOptionValue->value.integer > optionalRules.allowRangeMax))
                                    {
                                        THROW_FMT(
                                            OptionInvalidValueError, "'%s' is out of range for '%s' option", strZ(value),
                                            cfgParseOptionKeyIdxName(optionId, optionKeyIdx));
                                    }
                                }
                                // Else if StringId
                                else if (optionType == cfgOptTypeStringId)
                                {
                                    configOptionValue->value.stringId = strIdFromZN(strZ(valueAllow), strSize(valueAllow), false);
                                }
                                // Else if string make sure it is valid
                                else
                                {
                                    ASSERT(optionType == cfgOptTypePath || optionType == cfgOptTypeString);

                                    // Set string value to display value
                                    configOptionValue->value.string = configOptionValue->display;

                                    // Empty strings are not valid
                                    if (strSize(value) == 0)
                                    {
                                        THROW_FMT(
                                            OptionInvalidValueError, "'%s' must be >= 1 character for '%s' option", strZ(value),
                                            cfgParseOptionKeyIdxName(optionId, optionKeyIdx));
                                    }

                                    // If path make sure it is valid
                                    if (optionType == cfgOptTypePath)
                                    {
                                        // Make sure it starts with /
                                        if (!strBeginsWithZ(value, "/"))
                                        {
                                            THROW_FMT(
                                                OptionInvalidValueError, "'%s' must begin with / for '%s' option", strZ(value),
                                                cfgParseOptionKeyIdxName(optionId, optionKeyIdx));
                                        }

                                        // Make sure there are no occurrences of //
                                        if (strstr(strZ(value), "//") != NULL)
                                        {
                                            THROW_FMT(
                                                OptionInvalidValueError, "'%s' cannot contain // for '%s' option", strZ(value),
                                                cfgParseOptionKeyIdxName(optionId, optionKeyIdx));
                                        }

                                        // If the path ends with a / we'll strip it off (unless the value is just /)
                                        if (strEndsWithZ(value, "/") && strSize(value) != 1)
                                        {
                                            strTruncIdx(value, (int)strSize(value) - 1);

                                            // Reset string value since it was modified
                                            MEM_CONTEXT_BEGIN(config->memContext)
                                            {
                                                configOptionValue->value.string = strDup(value);
                                            }
                                            MEM_CONTEXT_END();
                                        }
                                    }
                                }

                                // If the option has an allow list then check it
                                if (cfgParseOptionalRule(
                                        &optionalRules, parseRuleOptionalTypeAllowList, config->command, optionId))
                                {
                                    PackRead *const allowList = pckReadNewC(optionalRules.allowList, optionalRules.allowListSize);
                                    bool allowListFound = false;

                                    pckReadNext(allowList);

                                    while (true)
                                    {
                                        // Compare based on option type
                                        const unsigned int valueIdx = pckReadU32P(allowList);

                                        switch (parseRuleOption[optionId].type)
                                        {
                                            case cfgOptTypeStringId:
                                                allowListFound = parseRuleValueStrId[valueIdx] == configOptionValue->value.stringId;
                                                break;

                                            default:
                                            {
                                                ASSERT(parseRuleOption[optionId].type == cfgOptTypeSize);

                                                allowListFound = parseRuleValueInt[valueIdx] == configOptionValue->value.integer;
                                                break;
                                            }
                                        }

                                        // Stop when allow list is exhausted or value is found
                                        if (!cfgParseOptionValueCondition(
                                                pckReadNext(allowList), allowList, allowListFound, optionId, optionKeyIdx,
                                                valueAllow) ||
                                            allowListFound)
                                        {
                                            break;
                                        }
                                    }

                                    pckReadFree(allowList);

                                    if (!allowListFound)
                                    {
                                        THROW_FMT(
                                            OptionInvalidValueError, "'%s' is not allowed for '%s' option", strZ(valueAllow),
                                            cfgParseOptionKeyIdxName(optionId, optionKeyIdx));
                                    }
                                }
                            }
                        }
                        else if (parseOptionValue->negate)
                            configOptionValue->source = parseOptionValue->source;
                        // Else try to set a default
                        else
                        {
                            bool found = false;

                            MEM_CONTEXT_BEGIN(config->memContext)
                            {
                                found = cfgParseOptionalRule(
                                    &optionalRules, parseRuleOptionalTypeDefault, config->command, optionId);
                            }
                            MEM_CONTEXT_END();

                            // If the option has a default
                            if (found)
                            {
                                configOptionValue->set = true;
                                configOptionValue->value = optionalRules.defaultValue;
                            }
                            // Else error if option is required and help was not requested
                            else
                            {
                                const bool required =
                                    cfgParseOptionalRule(
                                        &optionalRules, parseRuleOptionalTypeRequired, config->command, optionId) ?
                                        optionalRules.required : parseRuleOption[optionId].required;

                                if (required && !config->help)
                                {
                                    THROW_FMT(
                                        OptionRequiredError, "%s command requires option: %s%s",
                                        cfgParseCommandName(config->command),
                                        cfgParseOptionKeyIdxName(optionId, optionKeyIdx),
                                        parseRuleOption[optionId].section == cfgSectionStanza ?
                                            "\nHINT: does this stanza exist?" : "");
                                }
                            }
                        }

                        // If a non-default group option, keep the group index
                        if (optionGroup && configOptionValue->source != cfgSourceDefault)
                            optionGroupIndexKeep[optionGroupId][optionListIdx] = true;
                    }
                    // Else apply the default for the unresolved dependency, if it exists
                    else if (dependResult.defaultExists)
                    {
                        configOptionValue->set = true;
                        configOptionValue->value.boolean = dependResult.defaultValue;
                    }

                    pckReadFree(optionalRules.pack);
                }
            }

            // Phase 6: Remove any group indexes that have all default values (unless there is only one)
            //
            // It is possible that a group index was created for an option that was later found to not meet dependencies. In this
            // case all values will be default leading to a phantom group, which can be quite confusing. Remove all group indexes
            // that are all default (except the final one) and make sure the key for the final all default group index is 1.
            // ---------------------------------------------------------------------------------------------------------------------
            for (unsigned int optionGroupIdx = 0; optionGroupIdx < CFG_OPTION_GROUP_TOTAL; optionGroupIdx++)
            {
                ConfigOptionGroupData *const optionGroup = &config->optionGroup[optionGroupIdx];

                // Iterate group indexes
                for (unsigned int keyIdx = optionGroup->indexTotal - 1; keyIdx + 1 > 0; keyIdx--)
                {
                    // Break if there is only one index since each group must have at least one
                    if (optionGroup->indexTotal == 1)
                        break;

                    // If the group index does not have a non-default value
                    if (!optionGroupIndexKeep[optionGroupIdx][keyIdx])
                    {
                        // Remove the value if it is not last
                        if (keyIdx < optionGroup->indexTotal - 1)
                        {
                            // Remove index key
                            memmove(
                                optionGroup->indexMap + keyIdx, optionGroup->indexMap + (keyIdx + 1),
                                sizeof(unsigned int) * (optionGroup->indexTotal - keyIdx - 1));

                            // Iterate all options
                            for (unsigned int optionIdx = 0; optionIdx < CFG_OPTION_TOTAL; optionIdx++)
                            {
                                ConfigOptionData *const option = &config->option[optionIdx];

                                // Remove the value if in the correct group
                                if (option->group && option->groupId == optionGroupIdx)
                                {
                                    memmove(
                                        option->index + keyIdx, option->index + (keyIdx + 1),
                                        sizeof(ConfigOptionValue) * (optionGroup->indexTotal - keyIdx - 1));
                                }
                            }
                        }

                        // Decrement index total
                        optionGroup->indexTotal--;
                    }
                }

                // If the remaining index contains all default values and is not key 1 then make it key 1. This prevents the key
                // from being determined by the key of an unused option.
                if (optionGroup->indexTotal == 1 && !optionGroupIndexKeep[optionGroupIdx][0] && optionGroup->indexMap[0] != 0)
                    optionGroup->indexMap[0] = 0;
            }
        }

        // Initialize config
        cfgInit(config);

        // Set option group default index. The first index in the group is automatically set unless the group option, e.g. pg, is
        // set. For now the group default options are hard-coded but they could be dynamic. An assert has been added to make sure
        // the code breaks if a new group is added.
        for (unsigned int groupId = 0; groupId < CFG_OPTION_GROUP_TOTAL; groupId++)
        {
            ASSERT(groupId == cfgOptGrpPg || groupId == cfgOptGrpRepo);

            // Get the group default option
            unsigned int defaultOptionId = groupId == cfgOptGrpPg ? cfgOptPg : cfgOptRepo;

            // Does a default always exist?
            config->optionGroup[groupId].indexDefaultExists =
                // A default always exists for the pg group
                groupId == cfgOptGrpPg ||
                // The repo group allows a default when the repo option is valid, i.e. either repo1 is the only key set or a repo
                // is specified
                cfgOptionValid(cfgOptRepo);

            // Does the group default option exist?
            if (cfgOptionTest(defaultOptionId))
            {
                // Search for the key
                unsigned int optionKeyIdx = cfgOptionUInt(defaultOptionId) - 1;
                unsigned int index = 0;

                for (; index < cfgOptionGroupIdxTotal(groupId); index++)
                {
                    if (config->optionGroup[groupId].indexMap[index] == optionKeyIdx)
                        break;
                }

                // Error if the key was not found
                if (index == cfgOptionGroupIdxTotal(groupId))
                {
                    THROW_FMT(
                        OptionInvalidValueError, "key '%s' is not valid for '%s' option", strZ(cfgOptionDisplay(defaultOptionId)),
                        cfgOptionName(defaultOptionId));
                }

                // Set the default
                config->optionGroup[groupId].indexDefault = index;
                config->optionGroup[groupId].indexDefaultExists = true;
            }
        }
    }
    MEM_CONTEXT_TEMP_END();

    FUNCTION_LOG_RETURN_VOID();
}
