rsyslog/runtime/yamlconf.c
Rainer Gerhards b963aadf8f runtime: add config translation mode
Why: provide a migration path between supported rsyslog config
formats without requiring external conversion tooling.

Impact: rsyslogd can now emit canonical YAML or RainerScript
from supported configs during -N1 validation, with warning comments
for legacy or potentially misunderstood constructs.

Before/After: config validation could inspect syntax only; it can
now also write a translated canonical config file.

Technical Overview:
Add a retained translation document that captures config objects and
ruleset script during config load, before transient parser data is
consumed or destroyed.

Wire rsyslogd to accept a translation target format and output path,
then emit canonical YAML or modern RainerScript from the retained
representation.

Preserve module, input, queue, ruleset, and other shared config
objects, normalize top-level script into an explicit default
ruleset, and add in-output warning comments for legacy or lossy
translations.

Guard syntax cloning so translation-only capture does not add normal
runtime overhead, and report item-scoped fatal translation warnings
through the standard error path.

Add focused testbench coverage for RainerScript-to-YAML,
YAML-to-RainerScript, and legacy-warning output.

With the help of AI-Agents: Codex
2026-04-02 19:36:34 +02:00

2060 lines
82 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** @file yamlconf.c
* @brief YAML configuration file loader for rsyslog.
*
* Parses a .yaml/.yml rsyslog config file and drives the same processing
* pipeline that the RainerScript lex/bison parser uses:
* - builds struct cnfobj + struct nvlst for each declarative block
* - calls cnfDoObj() to let rsconf.c dispatch to the right processor
* - feeds ruleset script: strings back into the existing parser via
* cnfAddConfigBuffer() so RainerScript logic is fully supported
*
* The YAML schema mirrors the RainerScript object model:
*
* global: { key: value, ... }
* modules: [ { load: name, param: val, ... }, ... ]
* inputs: [ { type: name, param: val, ... }, ... ]
* templates: [ { name: n, type: t, string: s, ... }, ... ]
* rulesets: [ { name: n, script: |rainerscript..., ... }, ... ]
* or [ { name: n, statements: [{if:...,action:...}, ...] }, ... ]
* mainqueue: { type: linkedlist, size: 100000, ... }
* include: [ { path: glob, optional: on }, ... ]
* parser: [ { name: n, type: t, ... }, ... ]
* lookup_table: [ { name: n, file: f, ... }, ... ]
* dyn_stats: [ { name: n, ... }, ... ]
* timezone: [ { id: z, offset: o }, ... ]
* ratelimit: [ { name: n, interval: i, burst: b }, ... ]
*
* Parameter names within each block are identical to their RainerScript
* counterparts; all type conversion and validation happens downstream in
* nvlstGetParams() as usual.
*
* Concurrency & Locking: this code runs only during config load, which is
* single-threaded. No synchronisation primitives are needed.
*
* Copyright 2025 Rainer Gerhards and Adiscon GmbH.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* -or-
* see COPYING.ASL20 in the source distribution
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "config.h"
#ifdef HAVE_LIBYAML
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <yaml.h>
#include <libestr.h>
#include "rsyslog.h"
#include "errmsg.h"
#include "yamlconf.h"
#include "grammar/rainerscript.h"
#include "grammar/parserif.h"
/* Maximum nesting depth tracked during YAML node skipping.
* rsyslog config objects are inherently shallow (2-3 levels); 16 guards
* against malformed or adversarial YAML without needing a full DFS. */
/** @brief Maximum YAML nesting depth allowed when skipping unknown nodes. */
#define YAMLCONF_MAX_DEPTH 16
/* --------------------------------------------------------------------------
* Forward declarations
* -------------------------------------------------------------------------- */
static rsRetVal process_top_level(yaml_parser_t *parser, const char *key, const char *fname);
static rsRetVal parse_singleton_obj(yaml_parser_t *parser, enum cnfobjType objType, const char *fname);
static rsRetVal parse_obj_sequence(yaml_parser_t *parser, enum cnfobjType objType, const char *fname);
static rsRetVal parse_template_sequence(yaml_parser_t *parser, const char *fname);
static rsRetVal parse_elements_sequence(yaml_parser_t *parser, struct objlst **out, const char *fname);
static rsRetVal parse_ruleset_sequence(yaml_parser_t *parser, const char *fname);
static rsRetVal parse_actions_sequence(yaml_parser_t *parser, struct cnfstmt **head_out, const char *fname);
static rsRetVal parse_include_sequence(yaml_parser_t *parser, const char *fname);
static rsRetVal build_nvlst_from_mapping(yaml_parser_t *parser, struct nvlst **out, const char *fname);
static rsRetVal build_array_from_sequence(yaml_parser_t *parser, struct cnfarray **out, const char *fname);
static rsRetVal yaml_value_to_nvlst_node(
yaml_parser_t *parser, const char *fname, yaml_event_t *ev, const char *key, struct nvlst **out);
static rsRetVal build_stmts_rs(yaml_parser_t *parser, es_str_t **s, const char *fname);
static rsRetVal expect_event(
yaml_parser_t *parser, yaml_event_t *ev, yaml_event_type_t expected, const char *ctx, const char *fname);
static void skip_node(yaml_parser_t *parser);
/* --------------------------------------------------------------------------
* Helpers
* -------------------------------------------------------------------------- */
/* Convert a YAML scalar value (C string) into a heap-allocated es_str_t.
* The caller owns the returned pointer. */
static es_str_t *scalar_to_estr(const char *val) {
return es_newStrFromCStr(val, strlen(val));
}
/* Append a C string to an es_str_t, growing as needed.
* Returns 0 on success, -1 on error. */
static int estr_append_cstr(es_str_t **s, const char *buf, size_t len) {
if (len > (size_t)INT_MAX) {
LogError(0, RS_RET_OUT_OF_MEMORY, "yamlconf: string length %zu exceeds maximum supported size", len);
return -1;
}
return es_addBuf(s, buf, (es_size_t)len);
}
/* Append a single character to an es_str_t. */
static int estr_append_char(es_str_t **s, char c) {
return es_addChar(s, (unsigned char)c);
}
/* Append a string value enclosed in double-quotes, escaping backslash and
* double-quote characters with a preceding backslash. This produces valid
* RainerScript string literals. */
static int estr_append_quoted(es_str_t **s, const char *val, size_t len) {
if (estr_append_char(s, '"') != 0) return -1;
for (size_t i = 0; i < len; i++) {
char c = val[i];
if (c == '"' || c == '\\') {
if (estr_append_char(s, '\\') != 0) return -1;
}
if (estr_append_char(s, c) != 0) return -1;
}
return estr_append_char(s, '"');
}
/* Synthesise a complete RainerScript ruleset block:
*
* ruleset(name="<n>" [key="val" ...]) {
* <script_str>
* }
*
* All entries from 'lst' (the nvlst collected from the YAML mapping) are
* serialised as key="value" pairs inside the parentheses. String values are
* double-quoted and escaped. Array values use ["v1","v2"] syntax.
*
* The resulting es_str_t has two trailing NUL bytes appended so it is safe
* to pass directly to cnfAddConfigBuffer() / yy_scan_buffer().
*
* Returns RS_RET_OK on success, RS_RET_OUT_OF_MEMORY on allocation failure. */
static rsRetVal build_ruleset_rainerscript(struct nvlst *lst, const char *script_str, es_str_t **out) {
es_str_t *s = es_newStr(256);
DEFiRet;
if (s == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
if (estr_append_cstr(&s, "ruleset(", 8) != 0) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
for (struct nvlst *n = lst; n != NULL; n = n->next) {
if (n != lst) {
if (estr_append_char(&s, ' ') != 0) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
}
/* key name */
size_t klen = es_strlen(n->name);
if (estr_append_cstr(&s, (char *)es_getBufAddr(n->name), klen) != 0) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
if (estr_append_char(&s, '=') != 0) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
if (n->val.datatype == 'S') {
/* String value → "escaped" */
size_t vlen = es_strlen(n->val.d.estr);
if (estr_append_quoted(&s, (char *)es_getBufAddr(n->val.d.estr), vlen) != 0)
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
} else if (n->val.datatype == 'A') {
/* Array value → ["v1","v2",...] */
struct cnfarray *ar = n->val.d.ar;
if (estr_append_char(&s, '[') != 0) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
for (int i = 0; i < ar->nmemb; i++) {
if (i > 0 && estr_append_char(&s, ',') != 0) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
size_t elen = es_strlen(ar->arr[i]);
if (estr_append_quoted(&s, (char *)es_getBufAddr(ar->arr[i]), elen) != 0)
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
}
if (estr_append_char(&s, ']') != 0) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
} else {
/* Unexpected type; skip with a warning (should not happen for rulesets) */
LogError(0, RS_RET_CONF_PARSE_ERROR,
"yamlconf: unexpected nvlst value datatype '%c' in ruleset "
"parameter serialisation, skipping",
n->val.datatype);
if (estr_append_cstr(&s, "\"\"", 2) != 0) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
}
}
/* Close parameter list, open script body */
if (estr_append_cstr(&s, ") {\n", 4) != 0) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
/* Script body */
size_t slen = strlen(script_str);
if (estr_append_cstr(&s, script_str, slen) != 0) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
/* Ensure the script ends with a newline before the closing brace */
if (slen == 0 || script_str[slen - 1] != '\n') {
if (estr_append_char(&s, '\n') != 0) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
}
if (estr_append_cstr(&s, "}\n", 2) != 0) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
/* Two trailing NUL bytes required by yy_scan_buffer() */
if (estr_append_char(&s, '\0') != 0) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
if (estr_append_char(&s, '\0') != 0) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
*out = s;
s = NULL; /* caller owns it */
iRet = RS_RET_OK;
finalize_it:
if (s != NULL) es_deleteStr(s);
RETiRet;
}
/* Emit a parse-error message that includes the YAML source location. */
static void yaml_errmsg(yaml_parser_t *parser, const char *fname, const char *extra) {
if (parser->problem != NULL) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: %s (line %lu col %lu)%s%s", fname, parser->problem,
(unsigned long)parser->problem_mark.line + 1, (unsigned long)parser->problem_mark.column + 1,
extra ? " " : "", extra ? extra : "");
} else {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: parse error%s%s", fname, extra ? " " : "",
extra ? extra : "");
}
}
/* Pull the next event from the parser; on error log and return RS_RET_CONF_PARSE_ERROR. */
static rsRetVal next_event(yaml_parser_t *parser, yaml_event_t *ev, const char *fname) {
DEFiRet;
if (!yaml_parser_parse(parser, ev)) {
yaml_errmsg(parser, fname, NULL);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
finalize_it:
RETiRet;
}
/* Pull the next event and assert it has the expected type. */
static rsRetVal expect_event(
yaml_parser_t *parser, yaml_event_t *ev, yaml_event_type_t expected, const char *ctx, const char *fname) {
DEFiRet;
CHKiRet(next_event(parser, ev, fname));
if (ev->type != expected) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: expected event %d got %d in %s", fname, (int)expected,
(int)ev->type, ctx);
yaml_event_delete(ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
finalize_it:
RETiRet;
}
/* Skip over the current node (scalar, sequence, or mapping) and all its
* children. Assumes the opening event for this node has already been consumed
* (i.e. the caller has just read MAPPING_START or SEQUENCE_START; for a
* scalar the caller passes the scalar event and we do nothing). */
static void skip_node(yaml_parser_t *parser) {
yaml_event_t ev;
int depth = 1;
while (depth > 0) {
if (!yaml_parser_parse(parser, &ev)) break;
PRAGMA_DIAGNOSTIC_PUSH
PRAGMA_IGNORE_Wswitch_enum switch (ev.type) {
case YAML_MAPPING_START_EVENT:
case YAML_SEQUENCE_START_EVENT:
depth++;
if (depth > YAMLCONF_MAX_DEPTH) {
yaml_event_delete(&ev);
LogError(0, RS_RET_CONF_PARSE_ERROR,
"yamlconf: YAML nesting exceeds maximum depth %d; "
"skipping remainder",
YAMLCONF_MAX_DEPTH);
return;
}
break;
case YAML_MAPPING_END_EVENT:
case YAML_SEQUENCE_END_EVENT:
depth--;
break;
default:
break;
}
PRAGMA_DIAGNOSTIC_POP
yaml_event_delete(&ev);
}
}
/* --------------------------------------------------------------------------
* nvlst / cnfarray builders
* -------------------------------------------------------------------------- */
/* Build a struct cnfarray* from the remaining items of a YAML sequence.
* The SEQUENCE_START event must already have been consumed by the caller.
* Consumes up to and including the matching SEQUENCE_END event. */
static rsRetVal build_array_from_sequence(yaml_parser_t *parser, struct cnfarray **out, const char *fname) {
yaml_event_t ev;
struct cnfarray *arr = NULL;
es_str_t *estr;
DEFiRet;
*out = NULL;
while (1) {
CHKiRet(next_event(parser, &ev, fname));
if (ev.type == YAML_SEQUENCE_END_EVENT) {
yaml_event_delete(&ev);
break;
}
if (ev.type != YAML_SCALAR_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: array elements must be scalars", fname);
yaml_event_delete(&ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
estr = scalar_to_estr((char *)ev.data.scalar.value);
yaml_event_delete(&ev);
if (estr == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
if (arr == NULL)
arr = cnfarrayNew(estr);
else
arr = cnfarrayAdd(arr, estr);
if (arr == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
}
*out = arr;
finalize_it:
if (iRet != RS_RET_OK && arr != NULL) {
cnfarrayContentDestruct(arr);
free(arr);
}
RETiRet;
}
/**
* @brief Converts one YAML value event into a named nvlst node.
*
* The event @p ev must already have been pulled from the parser by the
* caller (the value event following a key scalar). The event is always
* consumed before returning, regardless of success or failure.
*
* Callers that need to handle YAML_MAPPING_START_EVENT specially (e.g. to
* skip nested mappings with a warning) should check ev->type before calling
* and only invoke this function for SCALAR and SEQUENCE_START events.
*
* @param parser libyaml parser; read-from only when ev is SEQUENCE_START.
* @param fname Config file name used in diagnostic messages.
* @param ev Pre-read value event; always consumed.
* @param key Parameter key (C string); copied into the node's name field.
* @param out Set to the new nvlst node on success; NULL on any error.
* @return RS_RET_OK, RS_RET_OUT_OF_MEMORY, or RS_RET_CONF_PARSE_ERROR.
*/
static rsRetVal yaml_value_to_nvlst_node(
yaml_parser_t *parser, const char *fname, yaml_event_t *ev, const char *key, struct nvlst **out) {
es_str_t *keyestr = NULL;
struct nvlst *node = NULL;
DEFiRet;
*out = NULL;
keyestr = scalar_to_estr(key);
if (keyestr == NULL) {
yaml_event_delete(ev);
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
}
if (ev->type == YAML_SCALAR_EVENT) {
es_str_t *val = scalar_to_estr((char *)ev->data.scalar.value);
yaml_event_delete(ev);
if (val == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
node = nvlstNewStr(val);
if (node == NULL) {
es_deleteStr(val);
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
}
} else if (ev->type == YAML_SEQUENCE_START_EVENT) {
struct cnfarray *arr = NULL;
yaml_event_delete(ev);
CHKiRet(build_array_from_sequence(parser, &arr, fname));
node = nvlstNewArray(arr);
if (node == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
} else {
yaml_event_delete(ev);
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: unsupported value type for parameter '%s'", fname, key);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
nvlstSetName(node, keyestr);
keyestr = NULL; /* owned by node */
*out = node;
node = NULL;
finalize_it:
es_deleteStr(keyestr);
if (node != NULL) nvlstDestruct(node);
RETiRet;
}
/* Build a struct nvlst* from a YAML mapping.
* The MAPPING_START event must already have been consumed by the caller.
* Consumes up to and including the matching MAPPING_END event.
*
* Each key/value pair becomes one nvlst node:
* scalar value → nvlstNewStr(estr)
* sequence → nvlstNewArray(cnfarray)
* nested mapping → logged as unsupported and skipped (rsyslog params are flat)
*/
static rsRetVal build_nvlst_from_mapping(yaml_parser_t *parser, struct nvlst **out, const char *fname) {
yaml_event_t ev;
struct nvlst *lst = NULL;
struct nvlst *node;
char *key = NULL;
DEFiRet;
*out = NULL;
while (1) {
/* Expect: key scalar or mapping end */
CHKiRet(next_event(parser, &ev, fname));
if (ev.type == YAML_MAPPING_END_EVENT) {
yaml_event_delete(&ev);
break;
}
if (ev.type != YAML_SCALAR_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: expected scalar key in mapping", fname);
yaml_event_delete(&ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
free(key);
key = strdup((char *)ev.data.scalar.value);
yaml_event_delete(&ev);
if (key == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
/* Now read the value */
CHKiRet(next_event(parser, &ev, fname));
if (ev.type == YAML_MAPPING_START_EVENT) {
/* Nested mappings are not part of the rsyslog parameter model.
* Skip and warn so the user knows the key was ignored. */
yaml_event_delete(&ev);
LogError(0, RS_RET_CONF_PARSE_ERROR,
"yamlconf: %s: nested mapping for key '%s' is not supported ignoring", fname, key);
skip_node(parser);
continue;
}
CHKiRet(yaml_value_to_nvlst_node(parser, fname, &ev, key, &node));
/* Prepend; cnfDoObj() does not depend on order */
node->next = lst;
lst = node;
}
*out = lst;
finalize_it:
free(key);
if (iRet != RS_RET_OK && lst != NULL) {
nvlstDestruct(lst);
}
RETiRet;
}
/* --------------------------------------------------------------------------
* Object section parsers
* -------------------------------------------------------------------------- */
/* Parse a singleton mapping section (e.g. global:, mainqueue:) and emit one
* cnfobj of the given type.
* The MAPPING_START event for the section body must already be consumed. */
static rsRetVal parse_singleton_obj(yaml_parser_t *parser, enum cnfobjType objType, const char *fname) {
struct nvlst *lst = NULL;
struct cnfobj *obj = NULL;
DEFiRet;
CHKiRet(build_nvlst_from_mapping(parser, &lst, fname));
obj = cnfobjNew(objType, lst);
if (obj == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
lst = NULL; /* owned by obj now */
cnfDoObj(obj);
finalize_it:
if (lst != NULL) nvlstDestruct(lst);
RETiRet;
}
/* Parse a sequence of mapping items, each becoming one cnfobj.
* The SEQUENCE_START event for the list must already be consumed.
*
* Used for: modules, inputs, templates, parser, lookup_table, dyn_stats,
* ratelimit, timezone. */
static rsRetVal parse_obj_sequence(yaml_parser_t *parser, enum cnfobjType objType, const char *fname) {
yaml_event_t ev;
DEFiRet;
while (1) {
CHKiRet(next_event(parser, &ev, fname));
if (ev.type == YAML_SEQUENCE_END_EVENT) {
yaml_event_delete(&ev);
break;
}
if (ev.type != YAML_MAPPING_START_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: sequence item must be a mapping", fname);
yaml_event_delete(&ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
yaml_event_delete(&ev);
struct nvlst *lst = NULL;
CHKiRet(build_nvlst_from_mapping(parser, &lst, fname));
struct cnfobj *obj = cnfobjNew(objType, lst);
if (obj == NULL) {
nvlstDestruct(lst);
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
}
cnfDoObj(obj);
}
finalize_it:
RETiRet;
}
/* Parse the elements: sequence of a list template.
*
* Each item is a mapping with exactly one key, "property" or "constant",
* whose value is a mapping of parameters:
*
* elements:
* - property:
* name: msg
* - constant:
* value: "\n"
*
* Builds a struct objlst of CNFOBJ_PROPERTY / CNFOBJ_CONSTANT nodes that
* mirrors what the bison grammar constructs for:
* template(name="t" type="list") { property(name="msg") constant(value="\n") }
*
* The SEQUENCE_START event must already have been consumed by the caller.
* Ownership of the returned objlst passes to the caller. */
static rsRetVal parse_elements_sequence(yaml_parser_t *parser, struct objlst **out, const char *fname) {
yaml_event_t ev;
struct objlst *subobjs = NULL;
char *elemtype = NULL;
DEFiRet;
*out = NULL;
while (1) {
CHKiRet(next_event(parser, &ev, fname));
if (ev.type == YAML_SEQUENCE_END_EVENT) {
yaml_event_delete(&ev);
break;
}
if (ev.type != YAML_MAPPING_START_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR,
"yamlconf: %s: each elements: item must be a mapping with a "
"'property' or 'constant' key",
fname);
yaml_event_delete(&ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
yaml_event_delete(&ev);
/* Read the element type key: "property" or "constant" */
CHKiRet(next_event(parser, &ev, fname));
if (ev.type != YAML_SCALAR_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: elements: item key must be 'property' or 'constant'",
fname);
yaml_event_delete(&ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
free(elemtype);
elemtype = strdup((char *)ev.data.scalar.value);
yaml_event_delete(&ev);
if (elemtype == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
enum cnfobjType etype;
if (!strcmp(elemtype, "property")) {
etype = CNFOBJ_PROPERTY;
} else if (!strcmp(elemtype, "constant")) {
etype = CNFOBJ_CONSTANT;
} else {
LogError(0, RS_RET_CONF_PARSE_ERROR,
"yamlconf: %s: elements: item key must be 'property' or 'constant', got '%s'", fname, elemtype);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
/* Read MAPPING_START for the element parameters */
CHKiRet(expect_event(parser, &ev, YAML_MAPPING_START_EVENT, "element params mapping", fname));
yaml_event_delete(&ev);
struct nvlst *elst = NULL;
CHKiRet(build_nvlst_from_mapping(parser, &elst, fname));
struct cnfobj *eobj = cnfobjNew(etype, elst);
elst = NULL; /* owned by eobj */
if (eobj == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
subobjs = objlstAdd(subobjs, eobj);
/* Consume the MAPPING_END of the outer element item mapping */
CHKiRet(expect_event(parser, &ev, YAML_MAPPING_END_EVENT, "element item end", fname));
yaml_event_delete(&ev);
}
*out = subobjs;
subobjs = NULL;
finalize_it:
free(elemtype);
if (subobjs != NULL) objlstDestruct(subobjs);
RETiRet;
}
/* Parse the templates: sequence.
*
* Simple templates (type=string/subtree/plugin) use the flat nvlst path
* identical to other object sequences.
*
* List templates (type=list) need sub-objects: the "elements:" key takes
* a sequence of property:/constant: items that are built as
* CNFOBJ_PROPERTY/CNFOBJ_CONSTANT nodes and attached to the template
* cnfobj->subobjs — exactly what the bison grammar does for:
* template(name="t" type="list") { property(...) constant(...) }
*
* YAML schema summary:
* # string template
* - name: t1
* type: string
* string: "%msg%\n"
*
* # subtree template
* - name: t2
* type: subtree
* subtree: "$!all-json"
*
* # list template
* - name: t3
* type: list
* elements:
* - property:
* name: msg
* - constant:
* value: "\n"
*
* SEQUENCE_START must already be consumed by the caller. */
static rsRetVal parse_template_sequence(yaml_parser_t *parser, const char *fname) {
yaml_event_t ev;
struct nvlst *lst = NULL;
struct objlst *subobjs = NULL;
char *kname = NULL;
DEFiRet;
while (1) {
CHKiRet(next_event(parser, &ev, fname));
if (ev.type == YAML_SEQUENCE_END_EVENT) {
yaml_event_delete(&ev);
break;
}
if (ev.type != YAML_MAPPING_START_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: templates sequence item must be a mapping", fname);
yaml_event_delete(&ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
yaml_event_delete(&ev);
/* Reset per-template state */
lst = NULL;
subobjs = NULL;
/* Walk the template mapping manually to intercept "elements:" */
while (1) {
CHKiRet(next_event(parser, &ev, fname));
if (ev.type == YAML_MAPPING_END_EVENT) {
yaml_event_delete(&ev);
break;
}
if (ev.type != YAML_SCALAR_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: expected scalar key in template mapping", fname);
yaml_event_delete(&ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
free(kname);
kname = strdup((char *)ev.data.scalar.value);
yaml_event_delete(&ev);
if (kname == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
CHKiRet(next_event(parser, &ev, fname));
if (!strcmp(kname, "elements")) {
/* elements: [...] sub-objects for list template */
if (ev.type != YAML_SEQUENCE_START_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR,
"yamlconf: %s: 'elements' value must be a sequence of "
"property/constant items",
fname);
yaml_event_delete(&ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
yaml_event_delete(&ev);
if (subobjs != NULL) objlstDestruct(subobjs);
subobjs = NULL;
CHKiRet(parse_elements_sequence(parser, &subobjs, fname));
} else {
/* Regular template param: add to nvlst */
struct nvlst *node = NULL;
CHKiRet(yaml_value_to_nvlst_node(parser, fname, &ev, kname, &node));
node->next = lst;
lst = node;
}
} /* end per-template key loop */
struct cnfobj *obj = cnfobjNew(CNFOBJ_TPL, lst);
lst = NULL; /* owned by obj */
if (obj == NULL) {
if (subobjs != NULL) objlstDestruct(subobjs);
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
}
obj->subobjs = subobjs; /* NULL for non-list templates */
subobjs = NULL;
cnfDoObj(obj);
} /* end template sequence loop */
finalize_it:
free(kname);
nvlstDestruct(lst);
if (subobjs != NULL) objlstDestruct(subobjs);
RETiRet;
}
/* Parse the actions: sequence.
*
* Each item is a YAML mapping that mirrors an action() object: the "type"
* key names the output module and all other keys are passed as parameters.
* We build an nvlst for each item and call cnfstmtNewAct() directly, then
* chain the resulting cnfstmt nodes via ->next.
*
* The SEQUENCE_START event must already have been consumed by the caller.
* On success *head_out points to the first node of the chain (possibly NULL
* if the sequence was empty). Ownership passes to the caller. */
static rsRetVal parse_actions_sequence(yaml_parser_t *parser, struct cnfstmt **head_out, const char *fname) {
yaml_event_t ev;
struct cnfstmt *head = NULL, *tail = NULL;
DEFiRet;
*head_out = NULL;
while (1) {
CHKiRet(next_event(parser, &ev, fname));
if (ev.type == YAML_SEQUENCE_END_EVENT) {
yaml_event_delete(&ev);
break;
}
if (ev.type != YAML_MAPPING_START_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: actions: item must be a mapping", fname);
yaml_event_delete(&ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
yaml_event_delete(&ev);
struct nvlst *alst = NULL;
CHKiRet(build_nvlst_from_mapping(parser, &alst, fname));
/* cnfstmtNewAct() takes ownership of alst and frees it internally. */
struct cnfstmt *act = cnfstmtNewAct(alst);
if (act == NULL) {
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
}
if (head == NULL) {
head = tail = act;
} else {
tail->next = act;
tail = act;
}
}
*head_out = head;
head = NULL;
finalize_it:
if (head != NULL) cnfstmtDestructLst(head);
RETiRet;
}
/* --------------------------------------------------------------------------
* YAML-native statements: block
*
* Converts a YAML statements: sequence into a RainerScript body string that
* is then injected via cnfAddConfigBuffer(), following the same proven path
* as the raw script: key.
*
* Supported statement types in a statements: sequence item:
*
* Unconditional action (same schema as actions: items):
* - type: omfile
* file: /path
* template: outfmt
*
* Conditional (single-action shorthand):
* - if: '$msg contains "msgnum:"'
* action:
* type: omfile
* file: /path
* else: # optional
* - stop: true
*
* Conditional (multi-statement then/else):
* - if: '$msg contains "msgnum:"'
* then:
* - type: omfile
* file: /path
* else: # optional
* - stop: true
*
* Control flow:
* - stop: true # or stop:
* - continue: true # or continue:
* - call: rulesetname
*
* Variable assignment:
* - set:
* var: "$.nbr"
* expr: 'field($msg, 58, 2)'
* - unset: "$.var"
*
* foreach loop:
* - foreach:
* var: "$.item"
* in: "$!array"
* do:
* - type: omfile
* file: /tmp/out
*
* Indirect ruleset call:
* - call_indirect: "$!rulesetname"
*
* Reload a lookup table (triggers an asynchronous reload of a previously
* loaded lookup table). Unlike action() parameters, reload_lookup_table
* takes positional string arguments in RainerScript, so the YAML keys
* "table" and "stub_value" are extracted and emitted as quoted literals:
* - reload_lookup_table:
* table: myLookupTable
* stub_value: unknown # optional; returned on reload failure
*
* -------------------------------------------------------------------------- */
/* Append all entries of 'lst' as key="val" pairs to the RainerScript
* string *s. Used to serialise action() parameter lists.
* Reuses the same quoting logic as build_ruleset_rainerscript().
* Returns 0 on success, -1 on allocation failure (int, not rsRetVal). */
static int append_nvlst_as_rs_params(es_str_t **s, struct nvlst *lst) {
for (struct nvlst *n = lst; n != NULL; n = n->next) {
if (n != lst) {
if (estr_append_char(s, ' ') != 0) return -1;
}
size_t klen = es_strlen(n->name);
if (estr_append_cstr(s, (char *)es_getBufAddr(n->name), klen) != 0) return -1;
if (estr_append_char(s, '=') != 0) return -1;
if (n->val.datatype == 'S') {
size_t vlen = es_strlen(n->val.d.estr);
if (estr_append_quoted(s, (char *)es_getBufAddr(n->val.d.estr), vlen) != 0) return -1;
} else if (n->val.datatype == 'A') {
struct cnfarray *ar = n->val.d.ar;
if (estr_append_char(s, '[') != 0) return -1;
for (int i = 0; i < ar->nmemb; i++) {
if (i > 0 && estr_append_char(s, ',') != 0) return -1;
size_t elen = es_strlen(ar->arr[i]);
if (estr_append_quoted(s, (char *)es_getBufAddr(ar->arr[i]), elen) != 0) return -1;
}
if (estr_append_char(s, ']') != 0) return -1;
} else {
if (estr_append_cstr(s, "\"\"", 2) != 0) return -1;
}
}
return 0;
}
/* Parse the sub-mapping for a foreach: statement.
* MAPPING_START already consumed. Fills *p_var, *p_in, *p_body (all
* owned by caller; freed via the finalize_it: of build_one_stmt_rs). */
static rsRetVal parse_foreach_mapping(
yaml_parser_t *parser, const char *fname, char **p_var, char **p_in, es_str_t **p_body) {
yaml_event_t ev;
DEFiRet;
while (1) {
CHKiRet(next_event(parser, &ev, fname));
if (ev.type == YAML_MAPPING_END_EVENT) {
yaml_event_delete(&ev);
break;
}
if (ev.type != YAML_SCALAR_EVENT) {
yaml_event_delete(&ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
char *fk = strdup((char *)ev.data.scalar.value);
yaml_event_delete(&ev);
if (fk == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
if (!strcmp(fk, "var") || !strcmp(fk, "in")) {
yaml_event_t fv;
rsRetVal fret = next_event(parser, &fv, fname);
if (fret != RS_RET_OK) {
free(fk);
ABORT_FINALIZE(fret);
}
if (fv.type == YAML_SCALAR_EVENT) {
char **dst = !strcmp(fk, "var") ? p_var : p_in;
free(*dst);
*dst = strdup((char *)fv.data.scalar.value);
if (*dst == NULL) {
yaml_event_delete(&fv);
free(fk);
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
}
}
yaml_event_delete(&fv);
} else if (!strcmp(fk, "do")) {
yaml_event_t sev;
rsRetVal sret = next_event(parser, &sev, fname);
if (sret != RS_RET_OK) {
free(fk);
ABORT_FINALIZE(sret);
}
if (sev.type != YAML_SEQUENCE_START_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: foreach 'do' must be a sequence of statements",
fname);
yaml_event_delete(&sev);
free(fk);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
yaml_event_delete(&sev);
es_deleteStr(*p_body);
*p_body = es_newStr(64);
if (*p_body == NULL) {
free(fk);
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
}
rsRetVal bret = build_stmts_rs(parser, p_body, fname);
if (bret != RS_RET_OK) {
free(fk);
ABORT_FINALIZE(bret);
}
} else {
/* skip unknown foreach sub-key */
yaml_event_t fv;
rsRetVal fret = next_event(parser, &fv, fname);
if (fret == RS_RET_OK) yaml_event_delete(&fv);
}
free(fk);
}
finalize_it:
RETiRet;
}
/* Parse the sub-mapping for a set: statement.
* MAPPING_START already consumed. Fills *p_var and *p_expr. */
static rsRetVal parse_set_subkeys(yaml_parser_t *parser, const char *fname, char **p_var, char **p_expr) {
yaml_event_t ev;
DEFiRet;
while (1) {
CHKiRet(next_event(parser, &ev, fname));
if (ev.type == YAML_MAPPING_END_EVENT) {
yaml_event_delete(&ev);
break;
}
if (ev.type != YAML_SCALAR_EVENT) {
yaml_event_delete(&ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
char *sk = strdup((char *)ev.data.scalar.value);
yaml_event_delete(&ev);
if (sk == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
yaml_event_t sv;
rsRetVal sret = next_event(parser, &sv, fname);
if (sret != RS_RET_OK) {
free(sk);
ABORT_FINALIZE(sret);
}
if (sv.type == YAML_SCALAR_EVENT) {
char **dst = !strcmp(sk, "var") ? p_var : (!strcmp(sk, "expr") ? p_expr : NULL);
if (dst != NULL) {
free(*dst);
*dst = strdup((char *)sv.data.scalar.value);
if (*dst == NULL) {
yaml_event_delete(&sv);
free(sk);
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
}
}
}
yaml_event_delete(&sv);
free(sk);
}
finalize_it:
RETiRet;
}
/* Build RainerScript for one statement mapping. MAPPING_START already consumed.
* Appends to *s.
*
* Design note: this function intentionally uses a two-phase collect-then-emit
* pattern rather than being split further. The statement type cannot be
* determined until all keys in the YAML mapping have been read (e.g. an
* item with only 'type:' is a flat action, but one with 'if:' and 'action:'
* is an if/action shorthand). Splitting collect from emit would require a
* ~15-field context struct with its own cleanup, adding boilerplate without
* clarity. The foreach and set sub-mapping parsers are extracted into
* parse_foreach_mapping() and parse_set_subkeys() to keep nesting shallow. */
static rsRetVal build_one_stmt_rs(yaml_parser_t *parser, es_str_t **s, const char *fname) {
yaml_event_t ev;
DEFiRet;
/* Collect all top-level keys from this mapping so we can determine the
* statement type. Some values (then:/else:/action:/set:) are nested
* sequences or mappings that we handle inline. */
char *if_expr = NULL; /* value of if: */
char *call_name = NULL; /* value of call: */
char *call_indirect_expr = NULL; /* value of call_indirect: */
char *set_var = NULL; /* value of set.var */
char *set_expr = NULL; /* value of set.expr */
char *unset_var = NULL; /* value of unset: */
char *foreach_var = NULL; /* value of foreach.var */
char *foreach_in = NULL; /* value of foreach.in */
es_str_t *foreach_body = NULL; /* compiled body of foreach.do */
es_str_t *then_rs = NULL;
es_str_t *else_rs = NULL;
es_str_t *action_rs = NULL; /* action: nested mapping → serialised RS */
struct nvlst *act_lst = NULL; /* for flat action (type: key at top level) */
int has_stop = 0;
int has_continue = 0;
int has_if = 0;
int has_call = 0;
int has_call_indirect = 0;
int has_set = 0;
int has_unset = 0;
int has_foreach = 0;
int has_type = 0; /* flat action form */
/* reload_lookup_table: uses positional args: ("tableName"[, "stubValue"]) */
char *rlt_table = NULL;
char *rlt_stub = NULL;
int has_rlt = 0;
while (1) {
CHKiRet(next_event(parser, &ev, fname));
if (ev.type == YAML_MAPPING_END_EVENT) {
yaml_event_delete(&ev);
break;
}
if (ev.type != YAML_SCALAR_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: expected scalar key in statements item", fname);
yaml_event_delete(&ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
char *kname = strdup((char *)ev.data.scalar.value);
yaml_event_delete(&ev);
if (kname == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
if (!strcmp(kname, "if")) {
/* value: scalar expression string */
yaml_event_t vev;
CHKiRet_Hdlr(next_event(parser, &vev, fname)) {
free(kname);
FINALIZE;
}
if (vev.type != YAML_SCALAR_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: 'if' value must be a scalar expression", fname);
yaml_event_delete(&vev);
free(kname);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
free(if_expr);
if_expr = strdup((char *)vev.data.scalar.value);
yaml_event_delete(&vev);
has_if = 1;
free(kname);
if (if_expr == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
} else if (!strcmp(kname, "then") || !strcmp(kname, "else")) {
/* value: sequence of statement items */
int is_else = (kname[0] == 'e');
free(kname);
yaml_event_t sev;
CHKiRet(next_event(parser, &sev, fname));
if (sev.type != YAML_SEQUENCE_START_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR,
"yamlconf: %s: 'then'/'else' value must be a sequence of statements", fname);
yaml_event_delete(&sev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
yaml_event_delete(&sev);
es_str_t *branch_rs = es_newStr(64);
if (branch_rs == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
rsRetVal bret = build_stmts_rs(parser, &branch_rs, fname);
if (bret != RS_RET_OK) {
es_deleteStr(branch_rs);
ABORT_FINALIZE(bret);
}
if (is_else) {
es_deleteStr(else_rs);
else_rs = branch_rs;
} else {
es_deleteStr(then_rs);
then_rs = branch_rs;
}
} else if (!strcmp(kname, "action")) {
/* value: mapping with action params (type: + others) */
free(kname);
yaml_event_t mev;
CHKiRet(next_event(parser, &mev, fname));
if (mev.type != YAML_MAPPING_START_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR,
"yamlconf: %s: 'action' value must be a mapping of action parameters", fname);
yaml_event_delete(&mev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
yaml_event_delete(&mev);
struct nvlst *alst = NULL;
rsRetVal aret = build_nvlst_from_mapping(parser, &alst, fname);
if (aret != RS_RET_OK) ABORT_FINALIZE(aret);
/* serialise to action(...) string */
es_deleteStr(action_rs);
action_rs = es_newStr(128);
if (action_rs == NULL) {
nvlstDestruct(alst);
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
}
if (estr_append_cstr(&action_rs, "action(", 7) != 0 || append_nvlst_as_rs_params(&action_rs, alst) != 0 ||
estr_append_cstr(&action_rs, ")\n", 2) != 0) {
nvlstDestruct(alst);
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
}
nvlstDestruct(alst);
} else if (!strcmp(kname, "stop")) {
free(kname);
/* Only emit stop if value is truthy. Canonical YAML falsy values
* (false/no/off/0) must NOT trigger the statement — stop: false is
* a valid way to conditionally disable a stop. Non-scalar values
* are a config error. */
yaml_event_t vev;
CHKiRet(next_event(parser, &vev, fname));
if (vev.type != YAML_SCALAR_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: 'stop' value must be a scalar (true/false)", fname);
yaml_event_delete(&vev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
if (strcasecmp((char *)vev.data.scalar.value, "false") != 0 &&
strcasecmp((char *)vev.data.scalar.value, "no") != 0 &&
strcasecmp((char *)vev.data.scalar.value, "off") != 0 &&
strcmp((char *)vev.data.scalar.value, "0") != 0) {
has_stop = 1;
}
yaml_event_delete(&vev);
} else if (!strcmp(kname, "continue")) {
free(kname);
yaml_event_t vev;
CHKiRet(next_event(parser, &vev, fname));
if (vev.type != YAML_SCALAR_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: 'continue' value must be a scalar (true/false)",
fname);
yaml_event_delete(&vev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
if (strcasecmp((char *)vev.data.scalar.value, "false") != 0 &&
strcasecmp((char *)vev.data.scalar.value, "no") != 0 &&
strcasecmp((char *)vev.data.scalar.value, "off") != 0 &&
strcmp((char *)vev.data.scalar.value, "0") != 0) {
has_continue = 1;
}
yaml_event_delete(&vev);
} else if (!strcmp(kname, "call")) {
free(kname);
yaml_event_t vev;
CHKiRet(next_event(parser, &vev, fname));
if (vev.type != YAML_SCALAR_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: 'call' value must be a ruleset name", fname);
yaml_event_delete(&vev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
free(call_name);
call_name = strdup((char *)vev.data.scalar.value);
yaml_event_delete(&vev);
has_call = 1;
if (call_name == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
} else if (!strcmp(kname, "call_indirect")) {
free(kname);
yaml_event_t vev;
CHKiRet(next_event(parser, &vev, fname));
if (vev.type != YAML_SCALAR_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR,
"yamlconf: %s: 'call_indirect' value must be a variable expression", fname);
yaml_event_delete(&vev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
free(call_indirect_expr);
call_indirect_expr = strdup((char *)vev.data.scalar.value);
yaml_event_delete(&vev);
has_call_indirect = 1;
if (call_indirect_expr == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
} else if (!strcmp(kname, "unset")) {
free(kname);
yaml_event_t vev;
CHKiRet(next_event(parser, &vev, fname));
if (vev.type != YAML_SCALAR_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: 'unset' value must be a variable name", fname);
yaml_event_delete(&vev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
free(unset_var);
unset_var = strdup((char *)vev.data.scalar.value);
yaml_event_delete(&vev);
has_unset = 1;
if (unset_var == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
} else if (!strcmp(kname, "foreach")) {
free(kname);
has_foreach = 1;
yaml_event_t mev;
CHKiRet(next_event(parser, &mev, fname));
if (mev.type != YAML_MAPPING_START_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR,
"yamlconf: %s: 'foreach' value must be a mapping with var:, in:, and do: keys", fname);
yaml_event_delete(&mev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
yaml_event_delete(&mev);
CHKiRet(parse_foreach_mapping(parser, fname, &foreach_var, &foreach_in, &foreach_body));
} else if (!strcmp(kname, "set")) {
free(kname);
has_set = 1;
yaml_event_t mev;
CHKiRet(next_event(parser, &mev, fname));
if (mev.type != YAML_MAPPING_START_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR,
"yamlconf: %s: 'set' value must be a mapping with var: and expr: keys", fname);
yaml_event_delete(&mev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
yaml_event_delete(&mev);
CHKiRet(parse_set_subkeys(parser, fname, &set_var, &set_expr));
} else if (!strcmp(kname, "reload_lookup_table")) {
free(kname);
has_rlt = 1;
yaml_event_t mev;
CHKiRet(next_event(parser, &mev, fname));
if (mev.type != YAML_MAPPING_START_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR,
"yamlconf: %s: 'reload_lookup_table' value must be a mapping with at "
"least a 'table' key",
fname);
yaml_event_delete(&mev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
yaml_event_delete(&mev);
/* Extract positional args: table (required) and stub_value (optional).
* The RainerScript syntax is reload_lookup_table("name"[, "stub"])
* so we cannot use append_nvlst_as_rs_params(). */
struct nvlst *rlt_nl = NULL;
rsRetVal rlt_r = build_nvlst_from_mapping(parser, &rlt_nl, fname);
if (rlt_r != RS_RET_OK) ABORT_FINALIZE(rlt_r);
for (struct nvlst *n = rlt_nl; n != NULL; n = n->next) {
if (n->val.datatype != 'S') continue;
/* es_getBufAddr() is NOT null-terminated; use es_str2cstr() */
char *nkey = es_str2cstr(n->name, NULL);
char *nval = es_str2cstr(n->val.d.estr, NULL);
if (nkey != NULL && nval != NULL) {
if (!strcmp(nkey, "table")) {
free(rlt_table);
rlt_table = strdup(nval);
} else if (!strcmp(nkey, "stub_value")) {
free(rlt_stub);
rlt_stub = strdup(nval);
}
}
free(nkey);
free(nval);
}
nvlstDestruct(rlt_nl);
if (rlt_table == NULL && has_rlt) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: reload_lookup_table: 'table' key is required",
fname);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
} else {
/* Unknown key at statement level: treat as action param (flat action form).
* Collect into act_lst so we can emit action(...) at the end. */
has_type = has_type || (!strcmp(kname, "type") ? 1 : 0);
yaml_event_t vev;
rsRetVal next_ret = next_event(parser, &vev, fname);
if (next_ret != RS_RET_OK) {
free(kname);
ABORT_FINALIZE(next_ret);
}
struct nvlst *node = NULL;
rsRetVal nret = yaml_value_to_nvlst_node(parser, fname, &vev, kname, &node);
free(kname);
kname = NULL;
if (nret != RS_RET_OK) ABORT_FINALIZE(nret);
node->next = act_lst;
act_lst = node;
}
} /* end mapping loop */
/* ---- Generate RainerScript from collected fields ---- */
if (has_if) {
/* if (<expr>) then { ... } [else { ... }] */
if (estr_append_cstr(s, "if (", 4) != 0 || estr_append_cstr(s, if_expr, strlen(if_expr)) != 0 ||
estr_append_cstr(s, ") then {\n", 9) != 0)
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
/* then branch: either action_rs (from action: key) or then_rs (from then: key) */
if (action_rs != NULL) {
if (es_addStr(s, action_rs) != 0) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
} else if (then_rs != NULL) {
if (es_addStr(s, then_rs) != 0) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
}
if (estr_append_char(s, '}') != 0) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
if (else_rs != NULL) {
if (estr_append_cstr(s, " else {\n", 8) != 0 || es_addStr(s, else_rs) != 0 || estr_append_char(s, '}') != 0)
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
}
if (estr_append_char(s, '\n') != 0) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
} else if (has_stop) {
if (estr_append_cstr(s, "stop\n", 5) != 0) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
} else if (has_continue) {
if (estr_append_cstr(s, "continue\n", 9) != 0) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
} else if (has_call) {
if (estr_append_cstr(s, "call ", 5) != 0 || estr_append_cstr(s, call_name, strlen(call_name)) != 0 ||
estr_append_char(s, '\n') != 0)
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
} else if (has_call_indirect) {
if (estr_append_cstr(s, "call_indirect ", 14) != 0 ||
estr_append_cstr(s, call_indirect_expr, strlen(call_indirect_expr)) != 0 ||
estr_append_cstr(s, ";\n", 2) != 0)
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
} else if (has_unset) {
if (estr_append_cstr(s, "unset ", 6) != 0 || estr_append_cstr(s, unset_var, strlen(unset_var)) != 0 ||
estr_append_cstr(s, ";\n", 2) != 0)
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
} else if (has_foreach) {
if (foreach_var == NULL || foreach_in == NULL || foreach_body == NULL) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: foreach: requires 'var', 'in', and 'do' keys", fname);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
if (estr_append_cstr(s, "foreach (", 9) != 0 || estr_append_cstr(s, foreach_var, strlen(foreach_var)) != 0 ||
estr_append_cstr(s, " in ", 4) != 0 || estr_append_cstr(s, foreach_in, strlen(foreach_in)) != 0 ||
estr_append_cstr(s, ") do {\n", 7) != 0 || es_addStr(s, foreach_body) != 0 ||
estr_append_cstr(s, "}\n", 2) != 0)
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
} else if (has_set) {
if (set_var == NULL || set_expr == NULL) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: set: requires both 'var' and 'expr' keys", fname);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
if (estr_append_cstr(s, "set ", 4) != 0 || estr_append_cstr(s, set_var, strlen(set_var)) != 0 ||
estr_append_cstr(s, " = ", 3) != 0 || estr_append_cstr(s, set_expr, strlen(set_expr)) != 0 ||
estr_append_cstr(s, ";\n", 2) != 0)
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
} else if (has_type || act_lst != NULL) {
/* Flat action: item with type: key directly at statement level */
if (estr_append_cstr(s, "action(", 7) != 0 || append_nvlst_as_rs_params(s, act_lst) != 0 ||
estr_append_cstr(s, ")\n", 2) != 0)
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
} else if (has_rlt) {
/* Emit: reload_lookup_table("tableName"[, "stubValue"]) */
if (estr_append_cstr(s, "reload_lookup_table(", 20) != 0 ||
estr_append_quoted(s, rlt_table, strlen(rlt_table)) != 0)
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
if (rlt_stub != NULL) {
if (estr_append_cstr(s, ", ", 2) != 0 || estr_append_quoted(s, rlt_stub, strlen(rlt_stub)) != 0)
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
}
if (estr_append_cstr(s, ")\n", 2) != 0) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
} else {
LogError(0, RS_RET_CONF_PARSE_ERROR,
"yamlconf: %s: unrecognised statement item in statements: "
"(expected if/type/stop/continue/call/call_indirect/unset/set/foreach/reload_lookup_table)",
fname);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
finalize_it:
free(if_expr);
free(call_name);
free(call_indirect_expr);
free(unset_var);
free(foreach_var);
free(foreach_in);
free(set_var);
free(set_expr);
es_deleteStr(then_rs);
es_deleteStr(else_rs);
es_deleteStr(action_rs);
es_deleteStr(foreach_body);
nvlstDestruct(act_lst);
free(rlt_table);
free(rlt_stub);
RETiRet;
}
/* Parse a statements: sequence and append the equivalent RainerScript to *s.
* SEQUENCE_START must already be consumed by the caller.
*
* This function is RECURSIVE: build_one_stmt_rs() calls build_stmts_rs() for
* then:/else: branch bodies, and parse_foreach_mapping() calls it for do:
* bodies. Recursion depth is bounded by YAML nesting depth which is shallow
* in practice (foreach inside if is the deepest realistic case). */
static rsRetVal build_stmts_rs(yaml_parser_t *parser, es_str_t **s, const char *fname) {
yaml_event_t ev;
DEFiRet;
while (1) {
CHKiRet(next_event(parser, &ev, fname));
if (ev.type == YAML_SEQUENCE_END_EVENT) {
yaml_event_delete(&ev);
break;
}
if (ev.type != YAML_MAPPING_START_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: each item in a statements: sequence must be a mapping",
fname);
yaml_event_delete(&ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
yaml_event_delete(&ev);
CHKiRet(build_one_stmt_rs(parser, s, fname));
}
finalize_it:
RETiRet;
}
/* Parse the rulesets: sequence.
*
* Each item is a mapping with at least a "name" key. The special "script"
* key causes a complete ruleset(params) { script } RainerScript block to
* be synthesised and pushed onto the flex buffer stack via
* cnfAddConfigBuffer(); the ongoing yyparse() then processes it naturally.
*
* The "statements" key is the YAML-native alternative to "script". It takes
* a sequence of statement mappings (if/action/stop/continue/call/set) that are
* translated to an equivalent RainerScript body and then processed via the
* same cnfAddConfigBuffer() path. Only the filter expression in "if:" remains
* as a RainerScript expression string; action parameters and control-flow are
* expressed as YAML keys. script:/statements: and filter:/actions: are all
* mutually exclusive.
*
* ORDERING NOTE: The synthesised ruleset buffer is pushed AFTER any .conf
* files that were included via the include: section (which are also pushed
* via cnfAddConfigBuffer / cnfSetLexFile). Because the flex buffer stack is
* LIFO, the ruleset script is processed BEFORE those included .conf files.
* Consequently, any templates or objects that a ruleset script references
* must be defined in the YAML file's own templates:/global:/etc. sections
* (processed immediately via cnfDoObj()), not in included .conf fragments.
*
* Structured filter shortcut (Phase 2):
* filter: "<filter-string>" # optional; "*.info" (PRI) or ":prop,op,val" (property)
* actions: # mutually exclusive with script:
* - type: omfile
* file: /var/log/messages
* When filter: + actions: are used, cnfstmt nodes are built directly via
* cnfstmtNewPRIFILT / cnfstmtNewPROPFILT / cnfstmtNewAct and set on the
* cnfobj->script field before calling cnfDoObj() — no text buffer needed. */
static rsRetVal parse_ruleset_sequence(yaml_parser_t *parser, const char *fname) {
yaml_event_t ev;
/* Hoisted to function scope so finalize_it: can clean up on error */
struct nvlst *lst = NULL;
char *script_str = NULL;
char *filter_str = NULL;
struct cnfstmt *actions = NULL;
char *kname = NULL;
DEFiRet;
while (1) {
CHKiRet(next_event(parser, &ev, fname));
if (ev.type == YAML_SEQUENCE_END_EVENT) {
yaml_event_delete(&ev);
break;
}
if (ev.type != YAML_MAPPING_START_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: rulesets sequence item must be a mapping", fname);
yaml_event_delete(&ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
yaml_event_delete(&ev);
/* Reset per-ruleset state (all NULL at function entry or after prior iteration) */
lst = NULL;
script_str = NULL;
filter_str = NULL;
actions = NULL;
/* We parse the mapping manually here so we can intercept special keys */
while (1) {
CHKiRet(next_event(parser, &ev, fname));
if (ev.type == YAML_MAPPING_END_EVENT) {
yaml_event_delete(&ev);
break;
}
if (ev.type != YAML_SCALAR_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: expected scalar key in ruleset mapping", fname);
yaml_event_delete(&ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
free(kname);
kname = strdup((char *)ev.data.scalar.value);
yaml_event_delete(&ev);
if (kname == NULL) {
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
}
CHKiRet(next_event(parser, &ev, fname));
if (!strcmp(kname, "script")) {
/* script: must be a scalar (RainerScript text) */
if (ev.type != YAML_SCALAR_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR,
"yamlconf: %s: 'script' value must be a scalar"
" (use a YAML block scalar '|' for multi-line)",
fname);
yaml_event_delete(&ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
free(script_str);
script_str = strdup((char *)ev.data.scalar.value);
yaml_event_delete(&ev);
if (script_str == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
} else if (!strcmp(kname, "statements")) {
/* statements: sequence — YAML-native scripting.
* Translates to RainerScript and stored in script_str. */
if (ev.type != YAML_SEQUENCE_START_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: 'statements' value must be a sequence", fname);
yaml_event_delete(&ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
yaml_event_delete(&ev);
es_str_t *stmts_estr = es_newStr(256);
if (stmts_estr == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
rsRetVal sret = build_stmts_rs(parser, &stmts_estr, fname);
if (sret != RS_RET_OK) {
es_deleteStr(stmts_estr);
ABORT_FINALIZE(sret);
}
free(script_str);
script_str = es_str2cstr(stmts_estr, NULL);
es_deleteStr(stmts_estr);
if (script_str == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
} else if (!strcmp(kname, "filter")) {
/* filter: "<pri-filter>" or ":<property-filter>" */
if (ev.type != YAML_SCALAR_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: 'filter' value must be a scalar string", fname);
yaml_event_delete(&ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
free(filter_str);
filter_str = strdup((char *)ev.data.scalar.value);
yaml_event_delete(&ev);
if (filter_str == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
} else if (!strcmp(kname, "actions")) {
/* actions: [ {type: ..., ...}, ... ] */
if (ev.type != YAML_SEQUENCE_START_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: 'actions' value must be a sequence", fname);
yaml_event_delete(&ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
yaml_event_delete(&ev);
if (actions) {
cnfstmtDestructLst(actions);
actions = NULL;
}
CHKiRet(parse_actions_sequence(parser, &actions, fname));
} else {
/* All other keys go into the header nvlst */
struct nvlst *node = NULL;
CHKiRet(yaml_value_to_nvlst_node(parser, fname, &ev, kname, &node));
node->next = lst;
lst = node;
}
} /* end per-ruleset key loop */
/* Sanity: script:/statements: and actions: are mutually exclusive */
if (script_str != NULL && actions != NULL) {
LogError(0, RS_RET_CONF_PARSE_ERROR,
"yamlconf: %s: ruleset cannot have both 'script'/'statements' and 'actions';"
" use one or the other",
fname);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
if (script_str != NULL && *script_str != '\0') {
/* Synthesise a complete RainerScript ruleset(...) { script }
* block and push it onto the flex buffer stack. The bison parser
* will call cnfDoObj() itself after parsing the whole block,
* setting cnfobj->script correctly via rulesetProcessCnf(). */
free(filter_str);
filter_str = NULL;
es_str_t *estr = NULL;
rsRetVal br = build_ruleset_rainerscript(lst, script_str, &estr);
nvlstDestruct(lst);
lst = NULL;
free(script_str);
script_str = NULL;
if (br != RS_RET_OK) ABORT_FINALIZE(br);
/* cnfAddConfigBuffer() takes ownership of estr */
cnfAddConfigBuffer(estr, fname);
} else if (actions != NULL) {
/* Phase 2 structured shortcut: build cnfstmt chain directly.
* If filter: was given, wrap the action chain in a filter node.
* Otherwise the actions run unconditionally (equivalent to *.*). */
struct cnfstmt *body = actions;
actions = NULL;
if (filter_str != NULL && *filter_str != '\0') {
struct cnfstmt *filt;
/* Property filters start with ':', PRI filters do not */
if (filter_str[0] == ':') {
filt = cnfstmtNewPROPFILT(filter_str, body);
} else {
filt = cnfstmtNewPRIFILT(filter_str, body);
}
if (filt == NULL) {
cnfstmtDestructLst(body);
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
}
/* filter_str ownership taken by cnfstmt (printable field) */
filter_str = NULL;
body = filt;
}
free(filter_str);
filter_str = NULL;
struct cnfobj *obj = cnfobjNew(CNFOBJ_RULESET, lst);
lst = NULL;
if (obj == NULL) {
cnfstmtDestructLst(body);
ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
}
obj->script = body;
cnfDoObj(obj);
} else {
/* No script or actions — emit just the ruleset header. */
free(script_str);
script_str = NULL;
free(filter_str);
filter_str = NULL;
struct cnfobj *obj = cnfobjNew(CNFOBJ_RULESET, lst);
lst = NULL; /* cnfobjNew takes ownership */
if (obj == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
cnfDoObj(obj);
}
} /* end ruleset sequence loop */
finalize_it:
free(kname);
free(script_str);
free(filter_str);
if (actions != NULL) cnfstmtDestructLst(actions);
nvlstDestruct(lst);
RETiRet;
}
/* Parse the include: sequence and trigger file inclusion.
* Each item is a mapping with "path" (required) and "optional" (optional).
*
* Processing order matters because the flex buffer stack is LIFO:
* - .yaml/.yml paths: cnfDoInclude() → yamlconf_load() immediately and
* recursively, so they must be processed in forward (document) order.
* - .conf / other paths: cnfDoInclude() → cnfSetLexFile() which pushes
* onto the LIFO flex buffer stack. To ensure the bison parser later
* sees them in document order (A then B then C), we must push them in
* REVERSE order (C, B, A → stack top to bottom A, B, C → pop A first).
*
* We therefore collect all items first, then do a forward pass for YAML
* files and a reverse pass for non-YAML files.
*
* ORDERING LIMITATION: when the include: list mixes .yaml and non-YAML files
* (e.g. [a.conf, b.yaml, c.conf]), YAML files are always processed before
* non-YAML files, regardless of their relative position in the document.
* This is a fundamental architectural constraint — the LIFO flex buffer stack
* cannot interleave with synchronous YAML loads. A warning is emitted when
* such a mixed list is detected. If strict order is required, use separate
* include: blocks or consolidate to a single file type. */
static rsRetVal parse_include_sequence(yaml_parser_t *parser, const char *fname) {
yaml_event_t ev;
char *k = NULL;
char *path = NULL; /* current item path; function-scope so finalize_it can free it */
/* Dynamic array of collected items */
struct inc_item {
char *path;
int optional;
} *items = NULL;
int nitems = 0, cap = 0;
DEFiRet;
/* ---- Phase 1: collect all (path, optional) pairs ---- */
while (1) {
free(path); /* free any leftover from previous iteration */
path = NULL;
CHKiRet(next_event(parser, &ev, fname));
if (ev.type == YAML_SEQUENCE_END_EVENT) {
yaml_event_delete(&ev);
break;
}
if (ev.type != YAML_MAPPING_START_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: include sequence item must be a mapping", fname);
yaml_event_delete(&ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
yaml_event_delete(&ev);
int optional = 0;
while (1) {
CHKiRet(next_event(parser, &ev, fname));
if (ev.type == YAML_MAPPING_END_EVENT) {
yaml_event_delete(&ev);
break;
}
if (ev.type != YAML_SCALAR_EVENT) {
yaml_event_delete(&ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
k = strdup((char *)ev.data.scalar.value);
yaml_event_delete(&ev);
if (k == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
yaml_event_t vev;
CHKiRet(next_event(parser, &vev, fname));
if (vev.type == YAML_SCALAR_EVENT) {
const char *v = (char *)vev.data.scalar.value;
if (!strcmp(k, "path")) {
free(path);
path = strdup(v);
} else if (!strcmp(k, "optional")) {
optional = (!strcasecmp(v, "on") || !strcasecmp(v, "yes") || !strcmp(v, "1")) ? 1 : 0;
}
yaml_event_delete(&vev);
} else {
/* Non-scalar value for an include key is a config error.
* Consume the nested structure so the parser stays in sync. */
LogError(0, RS_RET_CONF_PARSE_ERROR,
"yamlconf: %s: include item key '%s' must have a scalar value, skipping", fname, k);
if (vev.type == YAML_MAPPING_START_EVENT || vev.type == YAML_SEQUENCE_START_EVENT) {
yaml_event_delete(&vev);
skip_node(parser);
} else {
yaml_event_delete(&vev);
}
}
free(k);
k = NULL;
}
if (path != NULL) {
if (nitems >= cap) {
int newcap = cap == 0 ? 8 : cap * 2;
struct inc_item *tmp = realloc(items, newcap * sizeof(*items));
if (tmp == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
items = tmp;
cap = newcap;
}
items[nitems].path = path;
path = NULL; /* ownership transferred to items[]; finalize_it must not free */
items[nitems].optional = optional;
nitems++;
} else {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: include item missing required 'path' key", fname);
/* non-fatal: skip this item */
}
}
/* ---- Phase 2: warn on mixed list, then forward pass for YAML files ---- */
/* Detect mixed .yaml / non-YAML to warn about ordering limitation. */
int has_yaml_item = 0, has_conf_item = 0;
for (int i = 0; i < nitems; i++) {
const char *ext = strrchr(items[i].path, '.');
if (ext != NULL && (!strcmp(ext, ".yaml") || !strcmp(ext, ".yml")))
has_yaml_item = 1;
else
has_conf_item = 1;
}
if (has_yaml_item && has_conf_item) {
LogError(0, RS_RET_OK,
"yamlconf: %s: include: list mixes .yaml and non-YAML files; "
"YAML files are loaded first regardless of document order — "
"use separate include: blocks if strict ordering is required",
fname);
}
for (int i = 0; i < nitems; i++) {
const char *ext = strrchr(items[i].path, '.');
int is_yaml = (ext != NULL && (!strcmp(ext, ".yaml") || !strcmp(ext, ".yml")));
if (!is_yaml) continue;
int iret = cnfDoInclude(items[i].path, items[i].optional);
if (iret != 0 && !items[i].optional) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: include of '%s' failed (non-optional)", fname,
items[i].path);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
}
/* ---- Phase 3: reverse pass — non-YAML files → LIFO stack ---- */
/* Pushing in reverse order ensures the bison parser processes them in
* the same forward (document) order as they appear in the YAML list. */
for (int i = nitems - 1; i >= 0; i--) {
const char *ext = strrchr(items[i].path, '.');
int is_yaml = (ext != NULL && (!strcmp(ext, ".yaml") || !strcmp(ext, ".yml")));
if (is_yaml) continue;
int iret = cnfDoInclude(items[i].path, items[i].optional);
if (iret != 0 && !items[i].optional) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: include of '%s' failed (non-optional)", fname,
items[i].path);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
}
finalize_it:
free(k);
free(path); /* NULL-safe; only non-NULL if ABORT_FINALIZE before item was stored */
for (int i = 0; i < nitems; i++) free(items[i].path);
free(items);
RETiRet;
}
/* --------------------------------------------------------------------------
* Top-level dispatcher
* -------------------------------------------------------------------------- */
/* Map a top-level YAML key to its cnfobjType and call the right parser.
* The opening event for the section value (MAPPING_START or SEQUENCE_START)
* has already been consumed when this function is called. */
static rsRetVal process_top_level(yaml_parser_t *parser, const char *key, const char *fname) {
DEFiRet;
if (!strcmp(key, "global")) {
CHKiRet(parse_singleton_obj(parser, CNFOBJ_GLOBAL, fname));
} else if (!strcmp(key, "mainqueue") || !strcmp(key, "main_queue")) {
CHKiRet(parse_singleton_obj(parser, CNFOBJ_MAINQ, fname));
} else if (!strcmp(key, "modules") || !strcmp(key, "testbench_modules")) {
CHKiRet(parse_obj_sequence(parser, CNFOBJ_MODULE, fname));
} else if (!strcmp(key, "inputs")) {
CHKiRet(parse_obj_sequence(parser, CNFOBJ_INPUT, fname));
} else if (!strcmp(key, "templates")) {
CHKiRet(parse_template_sequence(parser, fname));
} else if (!strcmp(key, "rulesets")) {
CHKiRet(parse_ruleset_sequence(parser, fname));
} else if (!strcmp(key, "parsers")) {
CHKiRet(parse_obj_sequence(parser, CNFOBJ_PARSER, fname));
} else if (!strcmp(key, "lookup_tables")) {
CHKiRet(parse_obj_sequence(parser, CNFOBJ_LOOKUP_TABLE, fname));
} else if (!strcmp(key, "dyn_stats")) {
CHKiRet(parse_obj_sequence(parser, CNFOBJ_DYN_STATS, fname));
} else if (!strcmp(key, "perctile_stats")) {
CHKiRet(parse_obj_sequence(parser, CNFOBJ_PERCTILE_STATS, fname));
} else if (!strcmp(key, "ratelimits")) {
CHKiRet(parse_obj_sequence(parser, CNFOBJ_RATELIMIT, fname));
} else if (!strcmp(key, "timezones")) {
CHKiRet(parse_obj_sequence(parser, CNFOBJ_TIMEZONE, fname));
} else if (!strcmp(key, "include")) {
CHKiRet(parse_include_sequence(parser, fname));
} else if (!strcmp(key, "version")) {
/* Informational; skip the scalar value */
yaml_event_t ev;
if (next_event(parser, &ev, fname) == RS_RET_OK) yaml_event_delete(&ev);
} else {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: unknown top-level key '%s' ignoring", fname, key);
/* Skip the value node so the parser stays in sync */
yaml_event_t ev;
if (next_event(parser, &ev, fname) == RS_RET_OK) {
if (ev.type == YAML_MAPPING_START_EVENT || ev.type == YAML_SEQUENCE_START_EVENT) {
yaml_event_delete(&ev);
skip_node(parser);
} else {
yaml_event_delete(&ev);
}
}
}
finalize_it:
RETiRet;
}
/* --------------------------------------------------------------------------
* Public entry point
* -------------------------------------------------------------------------- */
/**
* @brief Load and process a YAML rsyslog configuration file.
*
* Opens @p fname, drives the libyaml event parser over the top-level
* mapping, and dispatches each section to the appropriate handler.
* All resulting config objects are committed to rsconf via cnfDoObj() or
* cnfAddConfigBuffer() before this function returns.
*
* @param fname Absolute or relative path to the .yaml/.yml config file.
* @return RS_RET_OK on success; RS_RET_FILE_NOT_FOUND if @p fname cannot
* be opened; RS_RET_CONF_PARSE_ERROR on any YAML or semantic error.
*/
rsRetVal yamlconf_load(const char *fname) {
yaml_parser_t parser;
yaml_event_t ev;
int parserInit = 0;
FILE *fh = NULL;
#define YAMLCONF_MAX_TOPKEYS 32
char *seen_keys[YAMLCONF_MAX_TOPKEYS];
int seen_count = 0;
DEFiRet;
char *prev_cnfcurrfn = cnfcurrfn;
cnfcurrfn = (char *)fname;
fh = fopen(fname, "r");
if (fh == NULL) {
LogError(errno, RS_RET_CONF_FILE_NOT_FOUND, "yamlconf: cannot open config file '%s'", fname);
ABORT_FINALIZE(RS_RET_CONF_FILE_NOT_FOUND);
}
if (!yaml_parser_initialize(&parser)) {
LogError(0, RS_RET_INTERNAL_ERROR, "yamlconf: failed to initialize YAML parser");
ABORT_FINALIZE(RS_RET_INTERNAL_ERROR);
}
parserInit = 1;
yaml_parser_set_input_file(&parser, fh);
/* Consume stream/document start */
CHKiRet(expect_event(&parser, &ev, YAML_STREAM_START_EVENT, "stream start", fname));
yaml_event_delete(&ev);
CHKiRet(expect_event(&parser, &ev, YAML_DOCUMENT_START_EVENT, "document start", fname));
yaml_event_delete(&ev);
/* The top-level node must be a mapping */
CHKiRet(expect_event(&parser, &ev, YAML_MAPPING_START_EVENT, "top-level mapping", fname));
yaml_event_delete(&ev);
/* Track seen top-level keys to warn on duplicates. Duplicate keys are
* undefined behaviour in the YAML spec and unsupported by rsyslog; use
* sequences for multiple items or include: for file-level composition. */
/* Walk the top-level key/value pairs */
while (1) {
CHKiRet(next_event(&parser, &ev, fname));
if (ev.type == YAML_MAPPING_END_EVENT) {
yaml_event_delete(&ev);
break;
}
if (ev.type != YAML_SCALAR_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: expected scalar key at top level", fname);
yaml_event_delete(&ev);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
char *topkey = strdup((char *)ev.data.scalar.value);
yaml_event_delete(&ev);
if (topkey == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
/* Warn on duplicate top-level key. */
for (int ki = 0; ki < seen_count; ++ki) {
if (!strcmp(seen_keys[ki], topkey)) {
LogError(0, RS_RET_CONF_PARSE_ERROR,
"yamlconf: %s: duplicate top-level key '%s' - "
"this is unsupported; use sequences for multiple items "
"or include: for file-level composition",
fname, topkey);
break;
}
}
int topkey_owned = 0; /* 1 if seen_keys owns topkey, 0 if we must free it */
if (seen_count < YAMLCONF_MAX_TOPKEYS) {
seen_keys[seen_count++] = topkey; /* ownership transferred to seen_keys */
topkey_owned = 1;
}
/* else: array full (>32 distinct top-level keys — highly unusual);
* duplicate detection skipped for this key; topkey freed below. */
/* Consume the opening event of the value node before dispatching */
yaml_event_t vev;
rsRetVal vret = next_event(&parser, &vev, fname);
if (vret != RS_RET_OK) {
if (!topkey_owned) free(topkey);
ABORT_FINALIZE(vret);
}
/* For sections that expect a sequence we need SEQUENCE_START;
* for singleton sections we need MAPPING_START.
* process_top_level() takes responsibility from here, but we already
* consumed the opening event so verify it makes sense. */
if (vev.type != YAML_MAPPING_START_EVENT && vev.type != YAML_SEQUENCE_START_EVENT &&
vev.type != YAML_SCALAR_EVENT) {
LogError(0, RS_RET_CONF_PARSE_ERROR, "yamlconf: %s: unexpected value type for key '%s'", fname, topkey);
yaml_event_delete(&vev);
if (!topkey_owned) free(topkey);
ABORT_FINALIZE(RS_RET_CONF_PARSE_ERROR);
}
/* For scalar values (e.g. version: 2) put the event back via a small
* detour: handle scalars inline here before calling process_top_level. */
if (vev.type == YAML_SCALAR_EVENT) {
if (!strcmp(topkey, "version")) {
/* silently accept */
} else {
LogError(0, RS_RET_CONF_PARSE_ERROR,
"yamlconf: %s: key '%s' has a scalar value but expects"
" a mapping or sequence",
fname, topkey);
/* non-fatal */
}
yaml_event_delete(&vev);
if (!topkey_owned) free(topkey);
continue;
}
yaml_event_delete(&vev);
/* process_top_level() will now call the right parser which will consume
* the rest of the value node (up to and including its END event). */
rsRetVal pret = process_top_level(&parser, topkey, fname);
if (!topkey_owned) free(topkey);
if (pret != RS_RET_OK) ABORT_FINALIZE(pret);
}
finalize_it:
for (int ki = 0; ki < seen_count; ++ki) free(seen_keys[ki]);
if (parserInit) yaml_parser_delete(&parser);
if (fh) fclose(fh);
cnfcurrfn = prev_cnfcurrfn;
RETiRet;
}
#endif /* HAVE_LIBYAML */