pissircd/src/conf_preprocessor.c

521 lines
12 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);
static inline int ValidVarCharacter(char x)
{
if (isupper(x) || isdigit(x) || strchr("_", x))
return 1;
return 0;
}
PreprocessorItem evaluate_preprocessor_if(char *statement, const char *filename, int linenumber, ConditionalConfig **cc_out)
{
char *p=statement, *name;
int negative = 0;
ConditionalConfig *cc;
/* Currently we support only 4 things:
* $XYZ == "something"
* $XYZ != "something"
* module-loaded("something")
* !module-loaded("something")
* defined($XYZ)
* !defined($XYZ)
* 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);
}
/* Now comes the keyword or a variable name */
if (!strncmp(p, "module-loaded", 13))
{
p += 13;
skip_whitespace(&p);
if (*p != '(')
{
config_error("%s:%i: expected '(' for module-loaded(...",
filename, linenumber);
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 = IF_MODULE;
cc->negative = negative;
safe_strdup(cc->name, name);
*cc_out = cc;
return PREPROCESSOR_IF;
} else
if (!strncmp(p, "defined", 7))
{
p += 7;
skip_whitespace(&p);
if (*p != '(')
{
config_error("%s:%i: expected '(' for defined(...",
filename, linenumber);
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 = IF_DEFINED;
cc->negative = negative;
safe_strdup(cc->name, name);
*cc_out = cc;
return PREPROCESSOR_IF;
} else
{
char *name_terminate, *name2;
// Should be one of:
// $XYZ == "something"
// $XYZ != "something"
// Anything else is an error.
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;
}
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;
skip_whitespace(&p);
if (!strncmp(p, "==", 2))
{
negative = 0;
} else
if (!strncmp(p, "!=", 2))
{
negative = 1;
} else
{
*name_terminate = '\0';
config_error("%s:%i: @if: expected == or != after '%s'",
filename, linenumber, name);
return PREPROCESSOR_ERROR;
}
p += 2;
*name_terminate = '\0';
skip_whitespace(&p);
if (*p != '"')
{
config_error("%s:%i: @if: expected double quotes, missing \" perhaps?",
filename, linenumber);
return PREPROCESSOR_ERROR;
}
p++;
name2 = p;
read_until(&p, "\"");
if (!*p)
{
config_error("%s:%i: invalid @if statement, missing \" at end perhaps?",
filename, linenumber);
return PREPROCESSOR_ERROR;
}
*p = '\0';
cc = safe_alloc(sizeof(ConditionalConfig));
cc->condition = IF_VALUE;
cc->negative = negative;
safe_strdup(cc->name, name);
safe_strdup(cc->opt, name2);
*cc_out = cc;
return PREPROCESSOR_IF;
}
config_error("%s:%i: Error while evaluating '@if' statement '%s'",
filename, linenumber, statement);
return PREPROCESSOR_ERROR;
}
PreprocessorItem evaluate_preprocessor_define(char *statement, const char *filename, int linenumber)
{
char *p = statement;
char *name, *name_terminator;
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);
if (*p != '"')
{
config_error("%s:%i: @define: expected double quotes, missing \" perhaps?",
filename, linenumber);
return PREPROCESSOR_ERROR;
}
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';
*name_terminator = '\0';
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;
}
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, "@endif", 6))
return PREPROCESSOR_ENDIF;
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;
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 = 0;
if (!cc)
return 1;
if (cc->condition == IF_MODULE)
{
if (phase == PREPROCESSOR_PHASE_INITIAL)
{
/* We cannot handle @if module-loaded() yet.. */
return 1;
}
if (phase == PREPROCESSOR_PHASE_SECONDARY)
{
/* We can only handle blacklisted modules at this point, so: */
if (is_blacklisted_module(cc->name))
return 0;
return 1;
}
if (is_module_loaded(cc->name))
{
result = 1;
}
} else
if (cc->condition == IF_DEFINED)
{
NameValuePrioList *d = find_config_define(cc->name);
if (d)
{
result = 1;
}
} else
if (cc->condition == IF_VALUE)
{
NameValuePrioList *d = find_config_define(cc->name);
if (d && !strcasecmp(d->value, cc->opt))
{
result = 1;
}
} else
{
config_status("[BUG] unhandled @if type!!");
}
if (cc->negative)
result = result ? 0 : 1;
return result;
}
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 add the build-in ones */
void free_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);
}
/** 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);
}