mirror of
https://github.com/unrealircd/unrealircd.git
synced 2026-06-29 03:36:37 +02:00
896 lines
24 KiB
C
896 lines
24 KiB
C
/* UnrealIRCd configuration preprocessor
|
|
* (C) Copyright 2019 Bram Matthys ("Syzop") and the UnrealIRCd team
|
|
* License: GPLv2 or later
|
|
*
|
|
* Technically this isn't a 100% true preprocessor, but to the end user
|
|
* it will certainly look like it, hence the name.
|
|
*/
|
|
|
|
#include "unrealircd.h"
|
|
|
|
extern ConfigFile *conf;
|
|
|
|
NameValuePrioList *config_defines = NULL; /**< List of @defines, only valid during configuration reading */
|
|
|
|
/* Forward declarations */
|
|
NameValuePrioList *find_config_define(const char *name);
|
|
void preprocessor_cc_free_entry(ConditionalConfig *cc);
|
|
|
|
/** Find a module by its relative path, using the same filtering
|
|
* logic as is_module_loaded(): skip modules being unloaded (MODFLAG_DELAYED),
|
|
* and during config_posttest skip old modules (MODFLAG_LOADED) so we
|
|
* find the new version during a REHASH.
|
|
* @param name Module relative path (e.g., "map", "third/something")
|
|
* @returns Module pointer, or NULL if not found.
|
|
*/
|
|
static Module *find_testing_module(const char *name)
|
|
{
|
|
Module *mod;
|
|
for (mod = Modules; mod; mod = mod->next)
|
|
{
|
|
if (mod->flags & MODFLAG_DELAYED)
|
|
continue;
|
|
if ((loop.config_status < CONFIG_STATUS_LOAD) &&
|
|
(loop.config_status >= CONFIG_STATUS_POSTTEST) &&
|
|
(mod->flags == MODFLAG_LOADED))
|
|
{
|
|
continue;
|
|
}
|
|
if (!strcasecmp(mod->relpath, name))
|
|
return mod;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
typedef struct {
|
|
const char *keyword;
|
|
ConfigIfCondition condition;
|
|
int returns_value; /**< 1 if function returns a string for comparison, 0 if boolean */
|
|
} IfFunction;
|
|
|
|
/** Table of function-style @if conditions.
|
|
* NOTE: If two keywords share a prefix, put the longer one first,
|
|
* since we use strncmp for matching.
|
|
*/
|
|
static IfFunction if_functions[] = {
|
|
{ "module-loaded", IF_MODULE_LOADED, 0 },
|
|
{ "module-exists", IF_MODULE_EXISTS, 0 },
|
|
{ "minimum-version", IF_MINIMUM_VERSION, 0 },
|
|
{ "file-exists", IF_FILE_EXISTS, 0 },
|
|
{ "module-version", IF_MODULE_VERSION, 1 },
|
|
{ "environment", IF_ENVIRONMENT, 1 },
|
|
{ "defined", IF_DEFINED, 0 },
|
|
};
|
|
|
|
static inline int ValidVarCharacter(char x)
|
|
{
|
|
if (isupper(x) || isdigit(x) || strchr("_", x))
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
/** Parse a comparison operator and value: op "value"
|
|
* @param p Current position (should point at or before operator)
|
|
* @param statement Full statement (for error messages)
|
|
* @param filename Config filename (for error messages)
|
|
* @param linenumber Line number (for error messages)
|
|
* @param compare_op_out Parsed operator stored here
|
|
* @param value_out Parsed value string stored here
|
|
* @returns 1 on success, 0 on error
|
|
*/
|
|
static int parse_compare_op_and_value(char **p, char *statement,
|
|
const char *filename, int linenumber,
|
|
CompareOp *compare_op_out, char **value_out)
|
|
{
|
|
int op_len;
|
|
|
|
skip_whitespace(p);
|
|
if (!strncmp(*p, "==", 2))
|
|
{
|
|
*compare_op_out = COMPARE_EQ;
|
|
op_len = 2;
|
|
} else
|
|
if (!strncmp(*p, "!=", 2))
|
|
{
|
|
*compare_op_out = COMPARE_NE;
|
|
op_len = 2;
|
|
} else
|
|
if (!strncmp(*p, ">=", 2))
|
|
{
|
|
*compare_op_out = COMPARE_GE;
|
|
op_len = 2;
|
|
} else
|
|
if (!strncmp(*p, "<=", 2))
|
|
{
|
|
*compare_op_out = COMPARE_LE;
|
|
op_len = 2;
|
|
} else
|
|
if (**p == '>')
|
|
{
|
|
*compare_op_out = COMPARE_GT;
|
|
op_len = 1;
|
|
} else
|
|
if (**p == '<')
|
|
{
|
|
*compare_op_out = COMPARE_LT;
|
|
op_len = 1;
|
|
} else
|
|
{
|
|
config_error("%s:%i: @if: expected comparison operator (==, !=, >, >=, <, <=): %s",
|
|
filename, linenumber, statement);
|
|
return 0;
|
|
}
|
|
*p += op_len;
|
|
skip_whitespace(p);
|
|
if (**p == '"')
|
|
{
|
|
(*p)++;
|
|
*value_out = *p;
|
|
read_until(p, "\"");
|
|
if (!**p)
|
|
{
|
|
config_error("%s:%i: invalid @if statement, missing \" at end perhaps?",
|
|
filename, linenumber);
|
|
return 0;
|
|
}
|
|
**p = '\0';
|
|
} else
|
|
{
|
|
*value_out = *p;
|
|
read_until(p, " \t");
|
|
if (**p)
|
|
**p = '\0';
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/** Parse a function-style @if condition: funcname("argument")
|
|
* For value-returning functions, also parses: funcname("argument") op "value"
|
|
* @param p Position right after the keyword
|
|
* @param funcname Function name (for error messages only)
|
|
* @param condition The ConfigIfCondition enum value to set
|
|
* @param negative Whether the condition was negated with !
|
|
* @param returns_value Whether this function returns a value for comparison
|
|
* @param statement Full statement string (for error messages)
|
|
* @param filename Config filename (for error messages)
|
|
* @param linenumber Line number (for error messages)
|
|
* @param cc_out Result stored here on success
|
|
* @returns PREPROCESSOR_IF on success, PREPROCESSOR_ERROR on failure
|
|
*/
|
|
static PreprocessorItem parse_if_function(char *p, const char *funcname, ConfigIfCondition condition,
|
|
int negative, int returns_value, char *statement,
|
|
const char *filename, int linenumber,
|
|
ConditionalConfig **cc_out)
|
|
{
|
|
char *name;
|
|
ConditionalConfig *cc;
|
|
|
|
skip_whitespace(&p);
|
|
if (*p != '(')
|
|
{
|
|
config_error("%s:%i: expected '(' for %s(...",
|
|
filename, linenumber, funcname);
|
|
return PREPROCESSOR_ERROR;
|
|
}
|
|
p++;
|
|
skip_whitespace(&p);
|
|
if (*p == '"')
|
|
p++;
|
|
name = p;
|
|
read_until(&p, ")\"");
|
|
if (!*p)
|
|
{
|
|
config_error("%s:%i: invalid if statement (termination error): %s",
|
|
filename, linenumber, statement);
|
|
return PREPROCESSOR_ERROR;
|
|
}
|
|
*p = '\0';
|
|
cc = safe_alloc(sizeof(ConditionalConfig));
|
|
cc->condition = condition;
|
|
cc->negative = negative;
|
|
safe_strdup(cc->name, name);
|
|
|
|
if (returns_value)
|
|
{
|
|
p++;
|
|
if (*p == ')')
|
|
p++;
|
|
skip_whitespace(&p);
|
|
if (*p)
|
|
{
|
|
/* Value-returning function with comparison: func("arg") op "value" */
|
|
char *value = NULL;
|
|
|
|
if (negative)
|
|
{
|
|
config_error("%s:%i: @if: the '!' prefix cannot be used with %s() comparisons, use != instead: %s",
|
|
filename, linenumber, funcname, statement);
|
|
preprocessor_cc_free_entry(cc);
|
|
return PREPROCESSOR_ERROR;
|
|
}
|
|
if (!parse_compare_op_and_value(&p, statement, filename, linenumber, &cc->compare_op, &value))
|
|
{
|
|
preprocessor_cc_free_entry(cc);
|
|
return PREPROCESSOR_ERROR;
|
|
}
|
|
safe_strdup(cc->opt, value);
|
|
}
|
|
/* else: no operator after ), treat as boolean (is it set?) */
|
|
}
|
|
|
|
*cc_out = cc;
|
|
return PREPROCESSOR_IF;
|
|
}
|
|
|
|
PreprocessorItem evaluate_preprocessor_if(char *statement, const char *filename, int linenumber, ConditionalConfig **cc_out)
|
|
{
|
|
char *p=statement;
|
|
int i, negative = 0;
|
|
ConditionalConfig *cc;
|
|
|
|
/* Currently we support:
|
|
* $XYZ == "something"
|
|
* $XYZ != "something"
|
|
* $XYZ > "something"
|
|
* $XYZ >= "something"
|
|
* $XYZ < "something"
|
|
* $XYZ <= "something"
|
|
* module-loaded("something")
|
|
* !module-loaded("something")
|
|
* module-exists("something")
|
|
* !module-exists("something")
|
|
* minimum-version("6.2.0")
|
|
* !minimum-version("6.2.0")
|
|
* file-exists("something")
|
|
* !file-exists("something")
|
|
* environment("VARNAME")
|
|
* environment("VARNAME") == "value"
|
|
* module-version("name") >= "2.0"
|
|
* defined($XYZ)
|
|
* !defined($XYZ)
|
|
* @else is also supported (handled in conf.c, not here).
|
|
* We do not support && or || or anything else at this time.
|
|
*/
|
|
skip_whitespace(&p);
|
|
if (*p == '@')
|
|
p++;
|
|
if (*p == '!')
|
|
{
|
|
negative = 1;
|
|
p++;
|
|
skip_whitespace(&p);
|
|
}
|
|
|
|
/* Check function-style conditions from the table */
|
|
for (i = 0; i < ARRAY_SIZEOF(if_functions); i++)
|
|
{
|
|
int len = strlen(if_functions[i].keyword);
|
|
if (!strncmp(p, if_functions[i].keyword, len))
|
|
return parse_if_function(p + len, if_functions[i].keyword, if_functions[i].condition, negative, if_functions[i].returns_value, statement, filename, linenumber, cc_out);
|
|
}
|
|
|
|
/* Otherwise it should be a $VARIABLE comparison */
|
|
{
|
|
char *name, *name_terminate, *value = NULL;
|
|
CompareOp compare_op;
|
|
|
|
if (*p != '$')
|
|
{
|
|
config_error("%s:%i: invalid @if statement. Either an unknown function, or did you mean $VARNAME?: %s",
|
|
filename, linenumber, statement);
|
|
return PREPROCESSOR_ERROR;
|
|
}
|
|
if (negative)
|
|
{
|
|
config_error("%s:%i: @if: the '!' prefix cannot be used with variable comparisons, use != instead: %s",
|
|
filename, linenumber, statement);
|
|
return PREPROCESSOR_ERROR;
|
|
}
|
|
p++;
|
|
/* variable name starts now */
|
|
name = p;
|
|
read_until(&p, " \t=!<>");
|
|
if (!*p)
|
|
{
|
|
config_error("%s:%i: invalid if statement (termination error): %s",
|
|
filename, linenumber, statement);
|
|
return PREPROCESSOR_ERROR;
|
|
}
|
|
name_terminate = p;
|
|
if (!parse_compare_op_and_value(&p, statement, filename, linenumber, &compare_op, &value))
|
|
return PREPROCESSOR_ERROR;
|
|
*name_terminate = '\0';
|
|
cc = safe_alloc(sizeof(ConditionalConfig));
|
|
cc->condition = IF_VALUE;
|
|
cc->compare_op = compare_op;
|
|
safe_strdup(cc->name, name);
|
|
safe_strdup(cc->opt, value);
|
|
*cc_out = cc;
|
|
return PREPROCESSOR_IF;
|
|
}
|
|
|
|
config_error("%s:%i: Error while evaluating '@if' statement '%s'",
|
|
filename, linenumber, statement);
|
|
return PREPROCESSOR_ERROR;
|
|
}
|
|
|
|
/** Resolve a value-returning function for use in @define.
|
|
* Matches against if_functions[] entries that have returns_value=1.
|
|
* @param p Position at the function name
|
|
* @param statement Full statement (for error messages)
|
|
* @param filename Config filename (for error messages)
|
|
* @param linenumber Line number (for error messages)
|
|
* @returns The resolved string value, or NULL on error.
|
|
*/
|
|
static const char *resolve_define_function(char *p, char *statement,
|
|
const char *filename, int linenumber)
|
|
{
|
|
int i;
|
|
char *arg;
|
|
|
|
for (i = 0; i < ARRAY_SIZEOF(if_functions); i++)
|
|
{
|
|
int len = strlen(if_functions[i].keyword);
|
|
if (strncmp(p, if_functions[i].keyword, len))
|
|
continue;
|
|
|
|
if (!if_functions[i].returns_value)
|
|
{
|
|
config_error("%s:%i: @define: function '%s' does not return a value",
|
|
filename, linenumber, if_functions[i].keyword);
|
|
return NULL;
|
|
}
|
|
|
|
/* Parse ("argument") */
|
|
p += len;
|
|
skip_whitespace(&p);
|
|
if (*p != '(')
|
|
{
|
|
config_error("%s:%i: @define: expected '(' for %s(...",
|
|
filename, linenumber, if_functions[i].keyword);
|
|
return NULL;
|
|
}
|
|
p++;
|
|
skip_whitespace(&p);
|
|
if (*p == '"')
|
|
p++;
|
|
arg = p;
|
|
read_until(&p, ")\"");
|
|
if (!*p)
|
|
{
|
|
config_error("%s:%i: @define: invalid function call (termination error): %s",
|
|
filename, linenumber, statement);
|
|
return NULL;
|
|
}
|
|
*p = '\0';
|
|
|
|
/* Resolve based on condition type */
|
|
if (if_functions[i].condition == IF_ENVIRONMENT)
|
|
{
|
|
const char *env = getenv(arg);
|
|
if (!env)
|
|
{
|
|
config_error("%s:%i: @define: environment variable '%s' is not set",
|
|
filename, linenumber, arg);
|
|
return NULL;
|
|
}
|
|
return env;
|
|
}
|
|
|
|
if (if_functions[i].condition == IF_MODULE_VERSION)
|
|
{
|
|
Module *mod = find_testing_module(arg);
|
|
if (!mod)
|
|
{
|
|
config_error("%s:%i: @define: module '%s' is not loaded",
|
|
filename, linenumber, arg);
|
|
return NULL;
|
|
}
|
|
if (!mod->header->version)
|
|
{
|
|
config_error("%s:%i: @define: module '%s' has no version information",
|
|
filename, linenumber, arg);
|
|
return NULL;
|
|
}
|
|
return mod->header->version;
|
|
}
|
|
|
|
/* Future value-returning functions go here */
|
|
config_error("%s:%i: @define: [BUG] unhandled value-returning function '%s'",
|
|
filename, linenumber, if_functions[i].keyword);
|
|
return NULL;
|
|
}
|
|
|
|
config_error("%s:%i: @define: expected a quoted string or a value-returning function like environment(): %s",
|
|
filename, linenumber, statement);
|
|
return NULL;
|
|
}
|
|
|
|
PreprocessorItem evaluate_preprocessor_define(char *statement, const char *filename, int linenumber)
|
|
{
|
|
char *p = statement;
|
|
char *name, *name_terminator;
|
|
const char *value;
|
|
|
|
skip_whitespace(&p);
|
|
name = p;
|
|
read_until(&p, " \t");
|
|
if (!*p)
|
|
{
|
|
config_error("%s:%i: invalid @define statement",
|
|
filename, linenumber);
|
|
return PREPROCESSOR_ERROR;
|
|
}
|
|
name_terminator = p;
|
|
skip_whitespace(&p);
|
|
*name_terminator = '\0';
|
|
if (*p == '"')
|
|
{
|
|
/* Literal string: @define $VAR "value" */
|
|
p++;
|
|
value = p;
|
|
read_until(&p, "\"");
|
|
if (!*p)
|
|
{
|
|
config_error("%s:%i: invalid @define statement, missing \" at end perhaps?",
|
|
filename, linenumber);
|
|
return PREPROCESSOR_ERROR;
|
|
}
|
|
*p = '\0';
|
|
} else
|
|
{
|
|
/* Value-returning function: @define $VAR environment("VARNAME") */
|
|
value = resolve_define_function(p, statement, filename, linenumber);
|
|
if (!value)
|
|
return PREPROCESSOR_ERROR;
|
|
}
|
|
|
|
if (*name != '$')
|
|
{
|
|
config_error("%s:%i: the defined variable should start with a dollar sign ($), "
|
|
"so: @define $something \"123\" and not @define something \"123\"",
|
|
filename, linenumber);
|
|
return PREPROCESSOR_ERROR;
|
|
}
|
|
/* Skip dollar sign */
|
|
name++;
|
|
for (p = name; *p; p++)
|
|
{
|
|
if (!ValidVarCharacter(*p))
|
|
{
|
|
config_error("%s:%i: A $VARIABLE name may only contain UPPERcase characters, "
|
|
"digits, and the _ character. Illegal character: '%c'",
|
|
filename, linenumber, *p);
|
|
return PREPROCESSOR_ERROR;
|
|
}
|
|
}
|
|
|
|
if (strlen(value) > 512)
|
|
{
|
|
config_error("%s:%i: Value of defined variable is extremely large (%ld characters)!",
|
|
filename, linenumber, (long)strlen(value));
|
|
return PREPROCESSOR_ERROR;
|
|
}
|
|
|
|
add_nvplist(&config_defines, 0, name, value);
|
|
return PREPROCESSOR_DEFINE;
|
|
}
|
|
|
|
/** Evaluate an @error or @warning directive.
|
|
* @param statement Text after "@error " or "@warning "
|
|
* @param is_error 1 for @error, 0 for @warning
|
|
* @param filename Config filename (for error messages)
|
|
* @param linenumber Line number (for error messages)
|
|
* @returns PREPROCESSOR_USER_ERROR or PREPROCESSOR_USER_WARNING
|
|
*/
|
|
static PreprocessorItem evaluate_preprocessor_error_or_warning(char *statement,
|
|
int is_error, const char *filename, int linenumber)
|
|
{
|
|
char *p = statement;
|
|
|
|
skip_whitespace(&p);
|
|
if (*p == '"')
|
|
{
|
|
p++;
|
|
char *msg = p;
|
|
read_until(&p, "\"");
|
|
if (*p)
|
|
*p = '\0';
|
|
p = msg;
|
|
}
|
|
|
|
if (is_error)
|
|
{
|
|
config_error("%s:%i: %s", filename, linenumber, p);
|
|
return PREPROCESSOR_USER_ERROR;
|
|
}
|
|
config_warn("%s:%i: %s", filename, linenumber, p);
|
|
return PREPROCESSOR_USER_WARNING;
|
|
}
|
|
|
|
PreprocessorItem parse_preprocessor_item(char *start, char *end, const char *filename, int linenumber, ConditionalConfig **cc)
|
|
{
|
|
char buf[512];
|
|
int max;
|
|
|
|
*cc = NULL;
|
|
|
|
max = end - start + 1;
|
|
if (max > sizeof(buf))
|
|
max = sizeof(buf);
|
|
strlcpy(buf, start, max);
|
|
|
|
if (!strncmp(buf, "@define", 7))
|
|
return evaluate_preprocessor_define(buf+7, filename, linenumber);
|
|
else if (!strncmp(buf, "@if ", 4))
|
|
return evaluate_preprocessor_if(buf+4, filename, linenumber, cc);
|
|
else if (!strncmp(buf, "@else", 5))
|
|
return PREPROCESSOR_ELSE;
|
|
else if (!strncmp(buf, "@endif", 6))
|
|
return PREPROCESSOR_ENDIF;
|
|
else if (!strncmp(buf, "@error ", 7))
|
|
return evaluate_preprocessor_error_or_warning(buf+7, 1, filename, linenumber);
|
|
else if (!strncmp(buf, "@warning ", 9))
|
|
return evaluate_preprocessor_error_or_warning(buf+9, 0, filename, linenumber);
|
|
|
|
config_error("%s:%i: Unknown preprocessor directive: %s", filename, linenumber, buf);
|
|
return PREPROCESSOR_ERROR; /* ??? */
|
|
}
|
|
|
|
/** Free a ConditionalConfig entry.
|
|
* NOTE: be sure to do a DelListItem() before calling this, if necessary.
|
|
*/
|
|
void preprocessor_cc_free_entry(ConditionalConfig *cc)
|
|
{
|
|
safe_free(cc->name);
|
|
safe_free(cc->opt);
|
|
safe_free(cc);
|
|
}
|
|
|
|
/** Free ConditionalConfig entries in a linked list that
|
|
* are equal or above 'level'. This happens during an @endif.
|
|
*/
|
|
void preprocessor_cc_free_level(ConditionalConfig **cc_list, int level)
|
|
{
|
|
ConditionalConfig *cc, *cc_next;
|
|
for (cc = *cc_list; cc; cc = cc_next)
|
|
{
|
|
cc_next = cc->next;
|
|
if (cc->priority >= level)
|
|
{
|
|
DelListItem(cc, *cc_list);
|
|
preprocessor_cc_free_entry(cc);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Duplicates a linked list of ConditionalConfig entries */
|
|
void preprocessor_cc_duplicate_list(ConditionalConfig *r, ConditionalConfig **out)
|
|
{
|
|
ConditionalConfig *cc;
|
|
|
|
*out = NULL;
|
|
for (; r; r = r->next)
|
|
{
|
|
cc = safe_alloc(sizeof(ConditionalConfig));
|
|
safe_strdup(cc->name, r->name);
|
|
safe_strdup(cc->opt, r->opt);
|
|
cc->priority = r->priority;
|
|
cc->condition = r->condition;
|
|
cc->negative = r->negative;
|
|
cc->compare_op = r->compare_op;
|
|
cc->had_else = r->had_else;
|
|
AddListItem(cc, *out);
|
|
}
|
|
}
|
|
|
|
void preprocessor_cc_free_list(ConditionalConfig *cc)
|
|
{
|
|
ConditionalConfig *cc_next;
|
|
|
|
for (; cc; cc = cc_next)
|
|
{
|
|
cc_next = cc->next;
|
|
safe_free(cc->name);
|
|
safe_free(cc->opt);
|
|
safe_free(cc);
|
|
}
|
|
}
|
|
|
|
/** Resolve a preprocessor condition to true (=default) or false */
|
|
int preprocessor_resolve_if(ConditionalConfig *cc, PreprocessorPhase phase)
|
|
{
|
|
int result;
|
|
|
|
for (; cc; cc = cc->next)
|
|
{
|
|
result = 0;
|
|
|
|
if (cc->condition == IF_MODULE_LOADED)
|
|
{
|
|
if (phase == PREPROCESSOR_PHASE_INITIAL)
|
|
{
|
|
/* We cannot handle @if module-loaded() yet.. */
|
|
result = 1;
|
|
} else
|
|
if (phase == PREPROCESSOR_PHASE_SECONDARY)
|
|
{
|
|
/* We can only handle blacklisted modules at this point, so: */
|
|
if (!is_blacklisted_module(cc->name))
|
|
result = 1;
|
|
} else
|
|
if (is_module_loaded(cc->name))
|
|
{
|
|
result = 1;
|
|
}
|
|
} else
|
|
if (cc->condition == IF_MODULE_VERSION)
|
|
{
|
|
if (phase == PREPROCESSOR_PHASE_INITIAL || phase == PREPROCESSOR_PHASE_SECONDARY)
|
|
{
|
|
/* Modules are not loaded yet, default to true */
|
|
result = 1;
|
|
} else
|
|
{
|
|
Module *mod = find_testing_module(cc->name);
|
|
if (mod && mod->header->version)
|
|
{
|
|
if (cc->opt)
|
|
{
|
|
int cmp = strnatcasecmp(mod->header->version, cc->opt);
|
|
switch (cc->compare_op)
|
|
{
|
|
case COMPARE_EQ: result = (cmp == 0); break;
|
|
case COMPARE_NE: result = (cmp != 0); break;
|
|
case COMPARE_GT: result = (cmp > 0); break;
|
|
case COMPARE_GE: result = (cmp >= 0); break;
|
|
case COMPARE_LT: result = (cmp < 0); break;
|
|
case COMPARE_LE: result = (cmp <= 0); break;
|
|
}
|
|
} else
|
|
{
|
|
/* Boolean: module is loaded (has a version) */
|
|
result = 1;
|
|
}
|
|
}
|
|
}
|
|
} else
|
|
if (cc->condition == IF_MODULE_EXISTS)
|
|
{
|
|
const char *fullpath = Module_TransformPath(cc->name);
|
|
if (file_exists(fullpath))
|
|
result = 1;
|
|
} else
|
|
if (cc->condition == IF_MINIMUM_VERSION)
|
|
{
|
|
if (strnatcasecmp(VERSIONONLY, cc->name) >= 0)
|
|
result = 1;
|
|
} else
|
|
if (cc->condition == IF_FILE_EXISTS)
|
|
{
|
|
char *fullpath = convert_to_absolute_path_duplicate(cc->name, CONFDIR);
|
|
if (file_exists(fullpath))
|
|
result = 1;
|
|
safe_free(fullpath);
|
|
} else
|
|
if (cc->condition == IF_DEFINED)
|
|
{
|
|
NameValuePrioList *d = find_config_define(cc->name);
|
|
if (d)
|
|
{
|
|
result = 1;
|
|
}
|
|
} else
|
|
if (cc->condition == IF_ENVIRONMENT)
|
|
{
|
|
const char *env = getenv(cc->name);
|
|
if (env)
|
|
{
|
|
if (cc->opt)
|
|
{
|
|
int cmp = strnatcasecmp(env, cc->opt);
|
|
switch (cc->compare_op)
|
|
{
|
|
case COMPARE_EQ: result = (cmp == 0); break;
|
|
case COMPARE_NE: result = (cmp != 0); break;
|
|
case COMPARE_GT: result = (cmp > 0); break;
|
|
case COMPARE_GE: result = (cmp >= 0); break;
|
|
case COMPARE_LT: result = (cmp < 0); break;
|
|
case COMPARE_LE: result = (cmp <= 0); break;
|
|
}
|
|
} else
|
|
{
|
|
/* Boolean: environment variable is set */
|
|
result = 1;
|
|
}
|
|
}
|
|
} else
|
|
if (cc->condition == IF_VALUE)
|
|
{
|
|
NameValuePrioList *d = find_config_define(cc->name);
|
|
if (d)
|
|
{
|
|
int cmp = strnatcasecmp(d->value, cc->opt);
|
|
switch (cc->compare_op)
|
|
{
|
|
case COMPARE_EQ: result = (cmp == 0); break;
|
|
case COMPARE_NE: result = (cmp != 0); break;
|
|
case COMPARE_GT: result = (cmp > 0); break;
|
|
case COMPARE_GE: result = (cmp >= 0); break;
|
|
case COMPARE_LT: result = (cmp < 0); break;
|
|
case COMPARE_LE: result = (cmp <= 0); break;
|
|
}
|
|
}
|
|
} else
|
|
{
|
|
config_status("[BUG] unhandled @if type!!");
|
|
}
|
|
|
|
if (cc->negative)
|
|
result = result ? 0 : 1;
|
|
|
|
/* All conditions must be true (logical AND for nested @if) */
|
|
if (!result)
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
void preprocessor_resolve_conditionals_ce(ConfigEntry **ce_list, PreprocessorPhase phase)
|
|
{
|
|
ConfigEntry *ce, *next, *ce_prev;
|
|
ConfigEntry *cep, *cep_next, *cep_prev;
|
|
|
|
ce_prev = NULL;
|
|
for (ce = *ce_list; ce; ce = next)
|
|
{
|
|
next = ce->next;
|
|
/* This is for an @if before a block start */
|
|
if (!preprocessor_resolve_if(ce->conditional_config, phase))
|
|
{
|
|
/* Delete this entry */
|
|
if (ce == *ce_list)
|
|
{
|
|
/* we are head, so new head */
|
|
*ce_list = ce->next; /* can be NULL now */
|
|
} else {
|
|
/* non-head */
|
|
ce_prev->next = ce->next; /* can be NULL now */
|
|
}
|
|
config_entry_free(ce);
|
|
continue;
|
|
}
|
|
preprocessor_resolve_conditionals_ce(&ce->items, phase);
|
|
ce_prev = ce;
|
|
}
|
|
}
|
|
|
|
void preprocessor_resolve_conditionals_all(PreprocessorPhase phase)
|
|
{
|
|
ConfigFile *cfptr;
|
|
|
|
for (cfptr = conf; cfptr; cfptr = cfptr->next)
|
|
preprocessor_resolve_conditionals_ce(&cfptr->items, phase);
|
|
}
|
|
|
|
/** Frees the list of config_defines, so all @defines and then initialize the
|
|
* build-in ones.
|
|
*/
|
|
void init_config_defines(void)
|
|
{
|
|
safe_free_nvplist(config_defines);
|
|
add_nvplist(&config_defines, 0, "UNREALIRCD_VERSION", VERSIONONLY);
|
|
add_nvplist(&config_defines, 0, "UNREALIRCD_VERSION_GENERATION", PATCH1);
|
|
add_nvplist(&config_defines, 0, "UNREALIRCD_VERSION_MAJOR", PATCH2);
|
|
add_nvplist(&config_defines, 0, "UNREALIRCD_VERSION_MINOR", PATCH3);
|
|
add_nvplist(&config_defines, 0, "UNREALIRCD_VERSION_SUFFIX", PATCH4);
|
|
#ifdef GEOIP_ENGINE
|
|
add_nvplist(&config_defines, 0, "GEOIP_ENGINE", GEOIP_ENGINE);
|
|
#else
|
|
add_nvplist(&config_defines, 0, "GEOIP_ENGINE", "none");
|
|
#endif
|
|
/* Directory paths */
|
|
add_nvplist(&config_defines, 0, "CONFDIR", CONFDIR);
|
|
add_nvplist(&config_defines, 0, "DATADIR", PERMDATADIR);
|
|
add_nvplist(&config_defines, 0, "LOGDIR", LOGDIR);
|
|
add_nvplist(&config_defines, 0, "TMPDIR", TMPDIR);
|
|
#ifndef _WIN32
|
|
add_nvplist(&config_defines, 0, "DOCDIR", DOCDIR);
|
|
#endif
|
|
add_nvplist(&config_defines, 0, "MODULESDIR", MODULESDIR);
|
|
/* Max connections */
|
|
add_nvplist(&config_defines, 0, "MAXCONNECTIONS", macro_to_str(MAXCONNECTIONS));
|
|
}
|
|
|
|
/** Return the complete struct for a defined value */
|
|
NameValuePrioList *find_config_define(const char *name)
|
|
{
|
|
return find_nvplist(config_defines, name);
|
|
}
|
|
|
|
/** Return value of defined value */
|
|
char *get_config_define(char *name)
|
|
{
|
|
NameValuePrioList *e = find_nvplist(config_defines, name);
|
|
return e ? e->value : NULL;
|
|
}
|
|
|
|
void preprocessor_replace_defines(char **item, ConfigEntry *ce)
|
|
{
|
|
static char buf[4096];
|
|
char varname[512];
|
|
const char *i, *varstart, *varend;
|
|
char *o;
|
|
int n = sizeof(buf)-2;
|
|
int limit;
|
|
char *value;
|
|
|
|
if (!strchr(*item, '$'))
|
|
return; /* quick return in 99% of the cases */
|
|
|
|
o = buf;
|
|
for (i = *item; *i; i++)
|
|
{
|
|
if (*i != '$')
|
|
{
|
|
*o++ = *i;
|
|
if (--n == 0)
|
|
break;
|
|
continue;
|
|
}
|
|
|
|
/* $ encountered: */
|
|
varstart = i;
|
|
i++;
|
|
for (; *i && ValidVarCharacter(*i); i++);
|
|
varend = i;
|
|
i--;
|
|
limit = varend - varstart + 1;
|
|
if (limit > sizeof(varname))
|
|
limit = sizeof(varname);
|
|
strlcpy(varname, varstart, limit);
|
|
if (!strncmp(varstart, "$$", 2))
|
|
{
|
|
/* If we get here then we encountered "$$"
|
|
* and varname is just "$".
|
|
* This means we are hitting a $ escape sequence,
|
|
* the $$ is used as a literal $.
|
|
* Would be very rare if you need it, but.. we support it.
|
|
*/
|
|
value = varname; /* bit confusing, but no +1 here */
|
|
i++; /* skip extra $ in input */
|
|
} else
|
|
{
|
|
value = get_config_define(varname+1);
|
|
if (!value)
|
|
{
|
|
#if 0
|
|
/* Complain about $VARS if they are not defined, but don't bother
|
|
* for cases where it's clearly not a macro, eg. contains illegal
|
|
* variable characters.
|
|
*/
|
|
if ((limit > 2) && ((*varend == '\0') || strchr("\t ,.", *varend)))
|
|
{
|
|
config_warn("%s:%d: Variable %s used here but there's no @define for it earlier.",
|
|
ce->file->filename, ce->line_number, varname);
|
|
}
|
|
#endif
|
|
value = varname; /* not found? then use varname, including the '$' */
|
|
}
|
|
}
|
|
limit = strlen(value) + 1;
|
|
if (limit > n)
|
|
limit = n;
|
|
strlcpy(o, value, limit);
|
|
o += limit - 1;
|
|
n -= limit;
|
|
if (n == 0)
|
|
break; /* no output buffer left */
|
|
if (*varend == 0)
|
|
break; /* no input buffer left */
|
|
}
|
|
*o = '\0';
|
|
safe_strdup(*item, buf);
|
|
}
|