1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2026-05-22 10:15:16 +02:00
Files
pgbackrest/src/config/parse.c
T
David Steele 0aaa0772f5 Fix help not displaying help for the help command.
'pgbackrest help help' just displayed the help overview, rather than display help for the help command.

Fix this by making sure the command is set and routing correctly in the help command.
2024-01-22 12:33:40 -03:00

2712 lines
124 KiB
C

/***********************************************************************************************************************************
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();
}