/* This is the template processing code of rsyslog.
 * begun 2004-11-17 rgerhards
 *
 * Copyright 2004-2019 Rainer Gerhards and Adiscon
 *
 * This file is part of rsyslog.
 *
 * 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.
 *
 * Note: there is a tiny bit of code left where I could not get any response
 * from the author if this code can be placed under ASL2.0. I have guarded this
 * with #ifdef STRICT_GPLV3. Only if that macro is defined, the code will be
 * compiled. Otherwise this feature is not present. The plan is to do a
 * different implementation in the future to get rid of this problem.
 * rgerhards, 2012-08-25
 */
#include "config.h"

#include "rsyslog.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <assert.h>
#include <limits.h>
#include <json.h>
#include "stringbuf.h"
#include "syslogd-types.h"
#include "template.h"
#include "msg.h"
#include "dirty.h"
#include "obj.h"
#include "errmsg.h"
#include "strgen.h"
#include "rsconf.h"
#include "msg.h"
#include "parserif.h"
#include "unicode-helper.h"

/* states for lazily built JSON tree used in list templates with jsonf mode */
#define TPL_JSON_TREE_NOT_BUILT 0
#define TPL_JSON_TREE_BUILT 1
#define TPL_JSON_TREE_UNSUPPORTED 2

enum tplJsonNodeType { tplJsonNodeObject = 0, tplJsonNodeValue = 1 };

struct tplJsonNode {
    enum tplJsonNodeType type;
    uchar *name;
    size_t nameLen;
    struct templateEntry *pTpe; /* only valid for value nodes */
    struct tplJsonNode *parent;
    struct tplJsonNode *firstChild;
    struct tplJsonNode *lastChild;
    struct tplJsonNode *nextSibling;
};

static struct tplJsonNode *tplJsonNodeNew(enum tplJsonNodeType type, const uchar *name, size_t nameLen);
static void tplJsonNodeAddChild(struct tplJsonNode *parent, struct tplJsonNode *child);
static void tplJsonNodeFree(struct tplJsonNode *node);
static rsRetVal tplJsonEnsureContainer(
    struct tplJsonNode *parent, const uchar *name, size_t nameLen, struct tplJsonNode **ppContainer, int *pUnsupported);
static rsRetVal tplJsonAddValueNode(
    struct tplJsonNode *parent, struct templateEntry *pTpe, const uchar *name, size_t nameLen, int *pUnsupported);
static rsRetVal tplJsonBuildTree(struct template *pTpl);
static rsRetVal tplJsonRender(struct template *pTpl, smsg_t *pMsg, actWrkrIParams_t *iparam, struct syslogTime *ttNow);
static rsRetVal tplJsonRenderChildren(
    const struct tplJsonNode *parent, smsg_t *pMsg, struct syslogTime *ttNow, es_str_t **ppDst, int *pNeedComma);
static rsRetVal tplJsonRenderNode(
    const struct tplJsonNode *node, smsg_t *pMsg, struct syslogTime *ttNow, es_str_t **ppOut, int *pHasOutput);
static rsRetVal tplJsonRenderValue(
    const struct tplJsonNode *node, smsg_t *pMsg, struct syslogTime *ttNow, es_str_t **ppOut, int *pHasOutput);
static rsRetVal tplJsonRenderObject(
    const struct tplJsonNode *node, smsg_t *pMsg, struct syslogTime *ttNow, es_str_t **ppOut, int *pHasOutput);
static void tplWarnDuplicateJsonKeys(struct template *pTpl);

/**
 * \brief Allocate a fresh JSON tree node for dotted jsonf rendering.
 *
 * The node optionally owns a copy of \p name so descendants can be
 * addressed without depending on the template entry backing storage.
 *
 * \param type     Node role (object container or scalar value).
 * \param name     Optional name for the node.
 * \param nameLen  Length of \p name in bytes (without terminator).
 * \return Newly allocated node or NULL on allocation failure.
 */
static struct tplJsonNode *tplJsonNodeNew(enum tplJsonNodeType type, const uchar *name, size_t nameLen) {
    struct tplJsonNode *node = calloc(1, sizeof(struct tplJsonNode));
    if (node == NULL) return NULL;
    node->type = type;
    node->pTpe = NULL;
    if (name != NULL && nameLen > 0) {
        node->name = malloc(nameLen + 1);
        if (node->name == NULL) {
            free(node);
            return NULL;
        }
        memcpy(node->name, name, nameLen);
        node->name[nameLen] = '\0';
        node->nameLen = nameLen;
    }
    return node;
}

/**
 * \brief Link a child node to its parent while maintaining sibling order.
 *
 * The helper records both first and last child so breadth-first iteration
 * is cheap when walking the tree later during rendering.
 *
 * \param parent Parent object that will receive \p child.
 * \param child  Node to link; ownership is transferred to \p parent.
 */
static void tplJsonNodeAddChild(struct tplJsonNode *parent, struct tplJsonNode *child) {
    if (parent == NULL || child == NULL) return;
    child->parent = parent;
    child->nextSibling = NULL;
    if (parent->firstChild == NULL) {
        parent->firstChild = parent->lastChild = child;
    } else {
        parent->lastChild->nextSibling = child;
        parent->lastChild = child;
    }
}

/**
 * \brief Recursively dispose a JSON node subtree.
 *
 * Frees all descendants, the node name buffer, and the node itself.
 * Safe to call with NULL.
 *
 * \param node Root of the subtree to release.
 */
static void tplJsonNodeFree(struct tplJsonNode *node) {
    if (node == NULL) return;
    struct tplJsonNode *child = node->firstChild;
    while (child != NULL) {
        struct tplJsonNode *next = child->nextSibling;
        tplJsonNodeFree(child);
        child = next;
    }
    free(node->name);
    free(node);
}

/**
 * \brief Ensure an object child exists for the given dotted segment.
 *
 * Creates an intermediate container when necessary and flags the template
 * as unsupported if the segment collides with an existing value node.
 *
 * \param parent        Current node that should hold the container.
 * \param name          Segment name to ensure.
 * \param nameLen       Length of \p name.
 * \param ppContainer   Out parameter receiving the container when found.
 * \param pUnsupported  Set to 1 when the template cannot be expressed.
 * \retval RS_RET_OK         Container exists or was created.
 * \retval RS_RET_OUT_OF_MEMORY  Allocation failed.
 */
static rsRetVal tplJsonEnsureContainer(struct tplJsonNode *parent,
                                       const uchar *name,
                                       size_t nameLen,
                                       struct tplJsonNode **ppContainer,
                                       int *pUnsupported) {
    struct tplJsonNode *child;
    if (ppContainer != NULL) *ppContainer = NULL;
    if (pUnsupported != NULL) *pUnsupported = 0;

    if (parent == NULL) return RS_RET_ERR;

    for (child = parent->firstChild; child != NULL; child = child->nextSibling) {
        if (child->nameLen == nameLen && memcmp(child->name, name, nameLen) == 0) {
            if (child->type != tplJsonNodeObject) {
                if (pUnsupported != NULL) *pUnsupported = 1;
                return RS_RET_OK;
            }
            if (ppContainer != NULL) *ppContainer = child;
            return RS_RET_OK;
        }
    }

    child = tplJsonNodeNew(tplJsonNodeObject, name, nameLen);
    if (child == NULL) return RS_RET_OUT_OF_MEMORY;
    tplJsonNodeAddChild(parent, child);
    if (ppContainer != NULL) *ppContainer = child;
    return RS_RET_OK;
}

/**
 * \brief Attach a value node for a template entry at the current tree level.
 *
 * Rejects collisions where an existing object node already occupies the
 * desired name and propagates an unsupported flag to the caller.
 *
 * \param parent        Object node receiving the value.
 * \param pTpe          Template entry that supplies the value.
 * \param name          Final path segment for the value.
 * \param nameLen       Length of \p name.
 * \param pUnsupported  Output flag marked when a collision is detected.
 * \retval RS_RET_OK            Value node linked or collision recorded.
 * \retval RS_RET_OUT_OF_MEMORY Allocation failed.
 */
static rsRetVal tplJsonAddValueNode(
    struct tplJsonNode *parent, struct templateEntry *pTpe, const uchar *name, size_t nameLen, int *pUnsupported) {
    struct tplJsonNode *child;
    if (pUnsupported != NULL) *pUnsupported = 0;

    if (parent == NULL) return RS_RET_ERR;

    for (child = parent->firstChild; child != NULL; child = child->nextSibling) {
        if (child->nameLen == nameLen && memcmp(child->name, name, nameLen) == 0) {
            if (child->type == tplJsonNodeObject) {
                if (pUnsupported != NULL) *pUnsupported = 1;
                return RS_RET_OK;
            }
            child->pTpe = pTpe;
            return RS_RET_OK;
        }
    }

    child = tplJsonNodeNew(tplJsonNodeValue, name, nameLen);
    if (child == NULL) return RS_RET_OUT_OF_MEMORY;
    child->pTpe = pTpe;
    tplJsonNodeAddChild(parent, child);
    return RS_RET_OK;
}

/**
 * \brief Build the JSON tree representation for a jsonf list template.
 *
 * Walks dotted outname segments, creating containers as required and
 * recording which templates cannot be represented as structured JSON.
 *
 * \param pTpl Template to analyse; its tree fields are updated in place.
 * \retval RS_RET_OK on success or when jsonf is unsupported for the template.
 * \retval RS_RET_OUT_OF_MEMORY on allocation failure.
 */
static rsRetVal tplJsonBuildTree(struct template *pTpl) {
    struct tplJsonNode *root = NULL;
    struct templateEntry *pTpe;
    int unsupported = 0;
    DEFiRet;

    if (pTpl->bJsonTreeBuilt != TPL_JSON_TREE_NOT_BUILT) RETiRet;

    CHKmalloc(root = tplJsonNodeNew(tplJsonNodeObject, NULL, 0));

    for (pTpe = pTpl->pEntryRoot; pTpe != NULL; pTpe = pTpe->pNext) {
        if (pTpe->fieldName == NULL || pTpe->lenFieldName == 0) {
            unsupported = 1;
            break;
        }

        if (pTpe->eEntryType == CONSTANT) {
            if (!pTpe->data.constant.bJSONf) {
                unsupported = 1;
                break;
            }
        } else if (pTpe->eEntryType == FIELD) {
            if (!pTpe->data.field.options.bJSONf && !pTpe->data.field.options.bJSONfr) {
                unsupported = 1;
                break;
            }
        } else {
            unsupported = 1;
            break;
        }

        struct tplJsonNode *parent = root;
        const uchar *segStart = pTpe->fieldName;
        const uchar *ptr = pTpe->fieldName;
        const uchar *end = pTpe->fieldName + pTpe->lenFieldName;

        while (ptr < end) {
            if (*ptr == '.') {
                if (ptr == segStart) {
                    unsupported = 1;
                    break;
                }
                struct tplJsonNode *container = NULL;
                const size_t segLen = (size_t)(ptr - segStart);
                CHKiRet(tplJsonEnsureContainer(parent, segStart, segLen, &container, &unsupported));
                if (unsupported) break;
                parent = container;
                ++ptr;
                segStart = ptr;
                continue;
            }
            ++ptr;
        }
        if (unsupported) break;

        const size_t segLen = (size_t)(end - segStart);
        if (segLen == 0) {
            unsupported = 1;
            break;
        }

        CHKiRet(tplJsonAddValueNode(parent, pTpe, segStart, segLen, &unsupported));
        if (unsupported) break;
    }

    if (unsupported) {
        tplJsonNodeFree(root);
        pTpl->pJsonRoot = NULL;
        pTpl->bJsonTreeBuilt = TPL_JSON_TREE_UNSUPPORTED;
        LogError(0, NO_ERRCODE,
                 "template '%s' with option jsonftree has conflicting keys (e.g. 'a' and 'a.b'); "
                 "falling back to flat JSON output.",
                 pTpl->pszName != NULL ? pTpl->pszName : "<unnamed>");
    } else {
        pTpl->pJsonRoot = root;
        pTpl->bJsonTreeBuilt = TPL_JSON_TREE_BUILT;
    }

finalize_it:
    if (iRet != RS_RET_OK) {
        tplJsonNodeFree(root);
    }
    RETiRet;
}

/**
 * \brief Render a list template with jsonf option via the pre-built tree.
 *
 * Serialises the lazily cached tree into the caller-provided work buffer
 * producing canonical JSON with nested objects.
 *
 * \param pTpl    Template to render; must already have a built tree.
 * \param pMsg    Message supplying field data.
 * \param iparam  Worker output buffer descriptor.
 * \param ttNow   Timestamp context for field formatting.
 * \retval RS_RET_OK on success or an error code from subordinate lookups.
 */
static rsRetVal tplJsonRender(struct template *pTpl, smsg_t *pMsg, actWrkrIParams_t *iparam, struct syslogTime *ttNow) {
    es_str_t *out = NULL;
    char *rendered = NULL;
    int needComma = 0;
    DEFiRet;

    if (pTpl->pJsonRoot == NULL) RETiRet;

    CHKmalloc(out = es_newStr(128));
    es_addChar(&out, '{');
    CHKiRet(tplJsonRenderChildren(pTpl->pJsonRoot, pMsg, ttNow, &out, &needComma));
    es_addBufConstcstr(&out, "}\n");

    const int len = es_strlen(out);
    if ((size_t)len + 1 > (size_t)iparam->lenBuf) {
        CHKiRet(ExtendBuf(iparam, (size_t)len + 1));
    }
    rendered = es_str2cstr(out, NULL);
    CHKmalloc(rendered);
    memcpy(iparam->param, rendered, (size_t)len);
    iparam->param[len] = '\0';
    iparam->lenStr = len;

finalize_it:
    if (rendered != NULL) free(rendered);
    if (out != NULL) es_deleteStr(out);
    RETiRet;
}

/**
 * \brief Render all children of a JSON node, inserting commas as needed.
 *
 * Recursively descends into the tree and appends emitted fragments to the
 * destination string buffer.
 *
 * \param parent     Node whose children should be rendered.
 * \param pMsg       Message that provides runtime values.
 * \param ttNow      Timestamp context for property formatting.
 * \param ppDst      Destination string buffer reference.
 * \param pNeedComma Tracks whether a comma should precede the next child.
 */
static rsRetVal tplJsonRenderChildren(
    const struct tplJsonNode *parent, smsg_t *pMsg, struct syslogTime *ttNow, es_str_t **ppDst, int *pNeedComma) {
    const struct tplJsonNode *child = parent->firstChild;
    es_str_t *childStr = NULL;
    int childHasOutput = 0;
    DEFiRet;

    while (child != NULL) {
        childStr = NULL;
        childHasOutput = 0;
        CHKiRet(tplJsonRenderNode(child, pMsg, ttNow, &childStr, &childHasOutput));
        if (childHasOutput) {
            if (*pNeedComma) es_addBufConstcstr(ppDst, ", ");
            es_addStr(ppDst, childStr);
            *pNeedComma = 1;
        }
        if (childStr != NULL) {
            es_deleteStr(childStr);
            childStr = NULL;
        }
        child = child->nextSibling;
    }

finalize_it:
    if (childStr != NULL) es_deleteStr(childStr);
    RETiRet;
}

/**
 * \brief Dispatch renderer for a node depending on its type.
 *
 * Value nodes are expanded into name/value pairs, while object nodes yield
 * nested JSON objects.
 *
 * \param node       Node to render.
 * \param pMsg       Message providing data.
 * \param ttNow      Timestamp context.
 * \param ppOut      Output string produced for this node.
 * \param pHasOutput Flag indicating whether a value was emitted.
 */
static rsRetVal tplJsonRenderNode(
    const struct tplJsonNode *node, smsg_t *pMsg, struct syslogTime *ttNow, es_str_t **ppOut, int *pHasOutput) {
    if (node->type == tplJsonNodeValue) {
        return tplJsonRenderValue(node, pMsg, ttNow, ppOut, pHasOutput);
    }
    return tplJsonRenderObject(node, pMsg, ttNow, ppOut, pHasOutput);
}

/**
 * \brief Render a scalar template entry into a JSON name/value fragment.
 *
 * Handles both constant and property-backed entries, including trimming any
 * legacy jsonf field prefixes and skipping empty expansions.
 *
 * \param node       Value node describing the template entry.
 * \param pMsg       Message context for property lookups.
 * \param ttNow      Timestamp context.
 * \param ppOut      Newly allocated string with the rendered fragment.
 * \param pHasOutput Set to 1 if the fragment contains data.
 */
static rsRetVal tplJsonRenderValue(
    const struct tplJsonNode *node, smsg_t *pMsg, struct syslogTime *ttNow, es_str_t **ppOut, int *pHasOutput) {
    uchar *pVal = NULL;
    rs_size_t lenVal = 0;
    unsigned short bMustBeFreed = 0;
    es_str_t *out = NULL;
    const uchar *valuePtr;
    size_t valueLen;
    const uchar *colon;
    DEFiRet;

    *ppOut = NULL;
    *pHasOutput = 0;

    if (node->pTpe == NULL || node->name == NULL || node->nameLen == 0) RETiRet;

    if (node->pTpe->eEntryType == CONSTANT) {
        pVal = node->pTpe->data.constant.pConstant;
        lenVal = node->pTpe->data.constant.iLenConstant;
    } else if (node->pTpe->eEntryType == FIELD) {
        pVal = MsgGetProp(pMsg, node->pTpe, &node->pTpe->data.field.msgProp, &lenVal, &bMustBeFreed, ttNow);
    } else {
        RETiRet;
    }

    if (pVal == NULL || lenVal <= 0) goto finalize_it;

    valuePtr = pVal;
    valueLen = (size_t)lenVal;
    colon = memchr(pVal, ':', (size_t)lenVal);
    if (colon != NULL) {
        const uchar *keyStart = pVal;
        const uchar *keyEnd = colon;
        while (keyStart < colon && isspace((int)*keyStart)) ++keyStart;
        while (keyEnd > keyStart && isspace((int)*(keyEnd - 1))) --keyEnd;
        const size_t prefixLen = (size_t)(keyEnd - keyStart);
        int matchesKey = 0;
        if (prefixLen >= 2 && keyStart[0] == '"' && keyEnd[-1] == '"' && node->pTpe->fieldName != NULL) {
            if ((size_t)node->pTpe->lenFieldName == prefixLen - 2 &&
                memcmp(keyStart + 1, node->pTpe->fieldName, node->pTpe->lenFieldName) == 0) {
                matchesKey = 1;
            }
        }
        if (matchesKey) {
            size_t offset = (size_t)((colon + 1) - pVal);
            if (offset >= (size_t)lenVal) goto finalize_it;
            valuePtr = pVal + offset;
            valueLen = (size_t)lenVal - offset;
        }
    }

    while (valueLen > 0 && isspace((int)*valuePtr)) {
        ++valuePtr;
        --valueLen;
    }
    while (valueLen > 0 && isspace((int)valuePtr[valueLen - 1])) --valueLen;
    if (valueLen == 0) goto finalize_it;
    const size_t requiredLen = node->nameLen + valueLen + 4;
    if (requiredLen > (size_t)INT_MAX) {
        parser_errmsg("error: jsonf value to be rendered is too large");
        ABORT_FINALIZE(RS_RET_ERR);
    }
    CHKmalloc(out = es_newStr((int)requiredLen));
    es_addChar(&out, '"');
    es_addBuf(&out, (const char *)node->name, node->nameLen);
    es_addBufConstcstr(&out, "\":");
    es_addBuf(&out, (const char *)valuePtr, valueLen);

    *ppOut = out;
    out = NULL;
    *pHasOutput = 1;

finalize_it:
    if (bMustBeFreed && pVal != NULL) free(pVal);
    if (out != NULL) es_deleteStr(out);
    RETiRet;
}

/**
 * \brief Render an object node and all nested children into JSON.
 *
 * Skips empty objects to preserve legacy behaviour while ensuring commas
 * and braces are placed consistently.
 *
 * \param node       Object node to render.
 * \param pMsg       Message supplying data for descendants.
 * \param ttNow      Timestamp context.
 * \param ppOut      Output string containing the rendered object.
 * \param pHasOutput Flag set when the object emitted any fields.
 */
static rsRetVal tplJsonRenderObject(
    const struct tplJsonNode *node, smsg_t *pMsg, struct syslogTime *ttNow, es_str_t **ppOut, int *pHasOutput) {
    es_str_t *out = NULL;
    es_str_t *childStr = NULL;
    int needComma = 0;
    const struct tplJsonNode *child;
    DEFiRet;

    *ppOut = NULL;
    *pHasOutput = 0;

    if (node->name == NULL || node->nameLen == 0) RETiRet;

    const size_t requiredLen = node->nameLen + 5;
    if (requiredLen > (size_t)INT_MAX) {
        parser_errmsg("error: jsonf object key to be rendered is too large");
        ABORT_FINALIZE(RS_RET_ERR);
    }
    CHKmalloc(out = es_newStr((int)requiredLen));
    es_addChar(&out, '"');
    es_addBuf(&out, (const char *)node->name, node->nameLen);
    es_addBufConstcstr(&out, "\": {");

    child = node->firstChild;
    while (child != NULL) {
        childStr = NULL;
        int childHasOutput = 0;
        CHKiRet(tplJsonRenderNode(child, pMsg, ttNow, &childStr, &childHasOutput));
        if (childHasOutput) {
            if (needComma) es_addBufConstcstr(&out, ", ");
            es_addStr(&out, childStr);
            needComma = 1;
        }
        if (childStr != NULL) {
            es_deleteStr(childStr);
            childStr = NULL;
        }
        child = child->nextSibling;
    }

    if (!needComma) goto finalize_it;

    es_addChar(&out, '}');
    *ppOut = out;
    out = NULL;
    *pHasOutput = 1;

finalize_it:
    if (childStr != NULL) es_deleteStr(childStr);
    if (out != NULL) es_deleteStr(out);
    RETiRet;
}

static void tplWarnDuplicateJsonKeys(struct template *pTpl) {
    struct templateEntry *curr;
    if (pTpl == NULL) return;

    for (curr = pTpl->pEntryRoot; curr != NULL; curr = curr->pNext) {
        struct templateEntry *scan;
        int isJsonField = 0;
        if (curr->fieldName == NULL || curr->lenFieldName <= 0) continue;

        if (curr->eEntryType == CONSTANT) {
            isJsonField = curr->data.constant.bJSONf;
        } else if (curr->eEntryType == FIELD) {
            isJsonField = curr->data.field.options.bJSONf || curr->data.field.options.bJSONfr;
        }
        if (!isJsonField) continue;

        for (scan = pTpl->pEntryRoot; scan != curr; scan = scan->pNext) {
            int scanJsonField = 0;
            if (scan->fieldName == NULL || scan->lenFieldName <= 0) continue;
            if (scan->eEntryType == CONSTANT) {
                scanJsonField = scan->data.constant.bJSONf;
            } else if (scan->eEntryType == FIELD) {
                scanJsonField = scan->data.field.options.bJSONf || scan->data.field.options.bJSONfr;
            }
            if (!scanJsonField) continue;
            if (scan->lenFieldName == curr->lenFieldName &&
                memcmp(scan->fieldName, curr->fieldName, curr->lenFieldName) == 0) {
                LogError(0, NO_ERRCODE,
                         "template '%s': duplicate JSON key '%.*s' encountered; later definition takes precedence",
                         pTpl->pszName != NULL ? pTpl->pszName : "<unnamed>", curr->lenFieldName, curr->fieldName);
                break;
            }
        }
    }
}

PRAGMA_IGNORE_Wswitch_enum
    /* static data */
    DEFobjCurrIf(obj) DEFobjCurrIf(strgen)

    /* tables for interfacing with the v6 config system */
    static struct cnfparamdescr cnfparamdescr[] = {{"name", eCmdHdlrString, 1},
                                                   {"type", eCmdHdlrString, 1},
                                                   {"string", eCmdHdlrString, 0},
                                                   {"plugin", eCmdHdlrString, 0},
                                                   {"subtree", eCmdHdlrString, 0},
                                                   {"option.stdsql", eCmdHdlrBinary, 0},
                                                   {"option.sql", eCmdHdlrBinary, 0},
                                                   {"option.json", eCmdHdlrBinary, 0},
                                                   {"option.jsonf", eCmdHdlrBinary, 0},
                                                   {"option.jsonftree", eCmdHdlrBinary, 0},
                                                   {"option.casesensitive", eCmdHdlrBinary, 0}};
static struct cnfparamblk pblk = {CNFPARAMBLK_VERSION, sizeof(cnfparamdescr) / sizeof(struct cnfparamdescr),
                                  cnfparamdescr};

static struct cnfparamdescr cnfparamdescrProperty[] = {{"name", eCmdHdlrString, 1},
                                                       {"outname", eCmdHdlrString, 0},
                                                       {"dateformat", eCmdHdlrString, 0},
                                                       {"date.inutc", eCmdHdlrBinary, 0},
                                                       {"compressspace", eCmdHdlrBinary, 0},
                                                       {"caseconversion", eCmdHdlrString, 0},
                                                       {"controlcharacters", eCmdHdlrString, 0},
                                                       {"securepath", eCmdHdlrString, 0},
                                                       {"format", eCmdHdlrString, 0},
                                                       {"position.from", eCmdHdlrInt, 0},
                                                       {"position.to", eCmdHdlrInt, 0},
                                                       {"position.relativetoend", eCmdHdlrBinary, 0},
                                                       {"field.number", eCmdHdlrInt, 0},
                                                       {"field.delimiter", eCmdHdlrInt, 0},
                                                       {"regex.expression", eCmdHdlrString, 0},
                                                       {"regex.type", eCmdHdlrString, 0},
                                                       {"regex.nomatchmode", eCmdHdlrString, 0},
                                                       {"regex.match", eCmdHdlrInt, 0},
                                                       {"regex.submatch", eCmdHdlrInt, 0},
                                                       {"droplastlf", eCmdHdlrBinary, 0},
                                                       {"fixedwidth", eCmdHdlrBinary, 0},
                                                       {"datatype", eCmdHdlrString, 0},
                                                       {"onempty", eCmdHdlrString, 0},
                                                       {"mandatory", eCmdHdlrBinary, 0},
                                                       {"spifno1stsp", eCmdHdlrBinary, 0}};
static struct cnfparamblk pblkProperty = {
    CNFPARAMBLK_VERSION, sizeof(cnfparamdescrProperty) / sizeof(struct cnfparamdescr), cnfparamdescrProperty};

static struct cnfparamdescr cnfparamdescrConstant[] = {
    {"value", eCmdHdlrString, 1}, {"format", eCmdHdlrString, 0}, {"outname", eCmdHdlrString, 0}};
static struct cnfparamblk pblkConstant = {
    CNFPARAMBLK_VERSION, sizeof(cnfparamdescrConstant) / sizeof(struct cnfparamdescr), cnfparamdescrConstant};


#ifdef FEATURE_REGEXP
DEFobjCurrIf(regexp) static int bFirstRegexpErrmsg = 1; /**< did we already do a "can't load regexp" error message? */
#endif

/* helper to tplToString and strgen's, extends buffer */
#define ALLOC_INC 128
rsRetVal ExtendBuf(actWrkrIParams_t *__restrict__ const iparam, const size_t iMinSize) {
    uchar *pNewBuf;
    size_t iNewSize;
    DEFiRet;

    iNewSize = (iMinSize / ALLOC_INC + 1) * ALLOC_INC;
    CHKmalloc(pNewBuf = (uchar *)realloc(iparam->param, iNewSize));
    iparam->param = pNewBuf;
    iparam->lenBuf = iNewSize;

finalize_it:
    RETiRet;
}


/* This functions converts a template into a string.
 *
 * The function takes a pointer to a template and a pointer to a msg object
 * as well as a pointer to an output buffer and its size. Note that the output
 * buffer pointer may be NULL, size 0, in which case a new one is allocated.
 * The output buffer is grown as required. It is the caller's duty to free the
 * buffer when it is done. Note that it is advisable to reuse memory, as this
 * offers big performance improvements.
 * rewritten 2009-06-19 rgerhards
 */
rsRetVal tplToString(struct template *__restrict__ const pTpl,
                     smsg_t *__restrict__ const pMsg,
                     actWrkrIParams_t *__restrict__ const iparam,
                     struct syslogTime *const ttNow) {
    DEFiRet;
    struct templateEntry *__restrict__ pTpe;
    size_t iBuf;
    unsigned short bMustBeFreed = 0;
    uchar *pVal;
    rs_size_t iLenVal = 0;
    int need_comma = 0;

    if (pTpl->pStrgen != NULL) {
        CHKiRet(pTpl->pStrgen(pMsg, iparam));
        FINALIZE;
    }

    if (pTpl->bHaveSubtree) {
        /* only a single CEE subtree must be provided */
        /* note: we could optimize the code below, however, this is
         * not worth the effort, as this passing mode is not expected
         * in subtree mode and so most probably only used for debug & test.
         */
        getJSONPropVal(pMsg, &pTpl->subtree, &pVal, &iLenVal, &bMustBeFreed);
        if (iLenVal >= (rs_size_t)iparam->lenBuf) /* we reserve one char for the final \0! */
            CHKiRet(ExtendBuf(iparam, iLenVal + 1));
        memcpy(iparam->param, pVal, iLenVal + 1);
        iparam->lenStr = iLenVal;
        FINALIZE;
    }

    /* we have a "regular" template with template entries */

    if (pTpl->optFormatEscape == JSONF && pTpl->bJsonTreeEnabled) {
        if (pTpl->bJsonTreeBuilt == TPL_JSON_TREE_NOT_BUILT) {
            CHKiRet(tplJsonBuildTree(pTpl));
        }
        if (pTpl->bJsonTreeBuilt == TPL_JSON_TREE_BUILT && pTpl->pJsonRoot != NULL) {
            CHKiRet(tplJsonRender(pTpl, pMsg, iparam, ttNow));
            FINALIZE;
        }
    }

    /* loop through the template. We obtain one value
     * and copy it over to our dynamic string buffer. Then, we
     * free the obtained value (if requested). We continue this
     * loop until we got hold of all values.
     */
    pTpe = pTpl->pEntryRoot;
    iBuf = 0;
    const int isJsonFlat = (pTpl->optFormatEscape == JSONF && !pTpl->bJsonTreeEnabled);
    if (isJsonFlat) {
        if (iparam->lenBuf < 2) /* we reserve one char for the final \0! */
            CHKiRet(ExtendBuf(iparam, 2));
        iBuf = 1;
        *iparam->param = '{';
    }
    while (pTpe != NULL) {
        if (pTpe->eEntryType == CONSTANT) {
            pVal = (uchar *)pTpe->data.constant.pConstant;
            iLenVal = pTpe->data.constant.iLenConstant;
            bMustBeFreed = 0;
        } else if (pTpe->eEntryType == FIELD) {
            pVal = (uchar *)MsgGetProp(pMsg, pTpe, &pTpe->data.field.msgProp, &iLenVal, &bMustBeFreed, ttNow);
            /* we now need to check if we should use SQL option. In this case,
             * we must go over the generated string and escape '\'' characters.
             * rgerhards, 2005-09-22: the option values below look somewhat misplaced,
             * but they are handled in this way because of legacy (don't break any
             * existing thing).
             */
            if (pTpl->optFormatEscape == SQL_ESCAPE)
                doEscape(&pVal, &iLenVal, &bMustBeFreed, SQL_ESCAPE);
            else if (pTpl->optFormatEscape == JSON_ESCAPE)
                doEscape(&pVal, &iLenVal, &bMustBeFreed, JSON_ESCAPE);
            else if (pTpl->optFormatEscape == STDSQL_ESCAPE)
                doEscape(&pVal, &iLenVal, &bMustBeFreed, STDSQL_ESCAPE);
        } else {
            DBGPRINTF("TplToString: invalid entry type %d\n", pTpe->eEntryType);
            pVal = (uchar *)"*LOGIC ERROR*";
            iLenVal = sizeof("*LOGIC ERROR*") - 1;
            bMustBeFreed = 0;
        }
        /* got source, now copy over */
        const int isLastEntry = (pTpe->pNext == NULL);
        const size_t closingLen = (isJsonFlat && isLastEntry) ? 2 : 0;
        size_t requiredLen = closingLen;
        if (iLenVal > 0) {
            if (need_comma) requiredLen += 2;
            requiredLen += (size_t)iLenVal;
        }
        if (requiredLen > 0 && (iBuf + requiredLen) >= iparam->lenBuf)
            CHKiRet(ExtendBuf(iparam, iBuf + requiredLen + 1));

        if (iLenVal > 0) { /* may be zero depending on property */
            if (need_comma) {
                memcpy(iparam->param + iBuf, ", ", 2);
                iBuf += 2;
            }

            memcpy(iparam->param + iBuf, pVal, iLenVal);
            iBuf += iLenVal;
            if (isJsonFlat) {
                need_comma = 1;
            }
        }

        if (closingLen > 0) {
            memcpy(iparam->param + iBuf, "}\n", 2);
            iBuf += 2;
        }

        if (bMustBeFreed) {
            free(pVal);
            bMustBeFreed = 0;
        }

        pTpe = pTpe->pNext;
    }

    if (iBuf == iparam->lenBuf) {
        /* in the weired case of an *empty* template, this can happen.
         * it is debatable if we should really fix it here or simply
         * forbid that case. However, performance toll is minimal, so
         * I tend to permit it. -- 2010-11-05 rgerhards
         */
        CHKiRet(ExtendBuf(iparam, iBuf + 1));
    }
    iparam->param[iBuf] = '\0';
    iparam->lenStr = iBuf;

finalize_it:
    if (bMustBeFreed) {
        free(pVal);
        bMustBeFreed = 0;
    }

    RETiRet;
}


/* This functions converts a template into a json object.
 * For further general details, see the very similar funtion
 * tpltoString().
 * rgerhards, 2012-08-29
 */
rsRetVal tplToJSON(struct template *pTpl, smsg_t *pMsg, struct json_object **pjson, struct syslogTime *ttNow) {
    struct templateEntry *pTpe;
    rs_size_t propLen;
    unsigned short bMustBeFreed;
    uchar *pVal;
    struct json_object *json, *jsonf;
    rsRetVal localRet;
    DEFiRet;

    if (pTpl->bHaveSubtree) {
        if (jsonFind(pMsg, &pTpl->subtree, pjson) != RS_RET_OK) *pjson = NULL;
        if (*pjson == NULL) {
            /* we need to have a root object! */
            *pjson = json_object_new_object();
        } else {
            json_object_get(*pjson); /* inc refcount */
        }
        FINALIZE;
    }

    json = json_object_new_object();
    for (pTpe = pTpl->pEntryRoot; pTpe != NULL; pTpe = pTpe->pNext) {
        if (pTpe->eEntryType == CONSTANT) {
            if (pTpe->fieldName == NULL) continue;
            jsonf = json_object_new_string((char *)pTpe->data.constant.pConstant);
            json_object_object_add(json, (char *)pTpe->fieldName, jsonf);
        } else if (pTpe->eEntryType == FIELD) {
            if (pTpe->data.field.msgProp.id == PROP_CEE || pTpe->data.field.msgProp.id == PROP_LOCAL_VAR ||
                pTpe->data.field.msgProp.id == PROP_GLOBAL_VAR) {
                localRet = msgGetJSONPropJSON(pMsg, &pTpe->data.field.msgProp, &jsonf);
                if (localRet == RS_RET_OK) {
                    json_object_object_add(json, (char *)pTpe->fieldName, json_object_get(jsonf));
                } else {
                    DBGPRINTF("tplToJSON: error %d looking up property %s\n", localRet, pTpe->fieldName);
                    if (pTpe->data.field.options.bMandatory) {
                        json_object_object_add(json, (char *)pTpe->fieldName, NULL);
                    }
                }
            } else {
                pVal = (uchar *)MsgGetProp(pMsg, pTpe, &pTpe->data.field.msgProp, &propLen, &bMustBeFreed, ttNow);
                if (pTpe->data.field.options.bMandatory || propLen > 0) {
                    jsonf = json_object_new_string_len((char *)pVal, propLen + 1);
                    json_object_object_add(json, (char *)pTpe->fieldName, jsonf);
                }
                if (bMustBeFreed) { /* json-c makes its own private copy! */
                    free(pVal);
                }
            }
        }
    }
    assert(iRet == RS_RET_OK);
    *pjson = json;

finalize_it:
    RETiRet;
}


/* Helper to doEscape. This is called if doEscape
 * runs out of memory allocating the escaped string.
 * Then we are in trouble. We can
 * NOT simply return the unmodified string because this
 * may cause SQL injection. But we also can not simply
 * abort the run, this would be a DoS. I think an appropriate
 * measure is to remove the dangerous \' characters (SQL). We
 * replace them by \", which will break the message and
 * signatures eventually present - but this is the
 * best thing we can do now (or does anybody
 * have a better idea?). rgerhards 2004-11-23
 * added support for escape mode (see doEscape for details).
 * if mode = SQL_ESCAPE, then backslashes are changed to slashes.
 * rgerhards 2005-09-22
 */
static void doEmergencyEscape(register uchar *p, int mode) {
    while (*p) {
        if ((mode == SQL_ESCAPE || mode == STDSQL_ESCAPE) && *p == '\'') {
            *p = '"';
        } else if (mode == JSON_ESCAPE) {
            if (*p == '"') {
                *p = '\'';
            } else if (*p == '\\') {
                *p = '/';
            }
        } else if ((mode == SQL_ESCAPE) && *p == '\\') {
            *p = '/';
        }
        ++p;
    }
}


/* SQL-Escape a string. Single quotes are found and
 * replaced by two of them. A new buffer is allocated
 * for the provided string and the provided buffer is
 * freed. The length is updated. Parameter pbMustBeFreed
 * is set to 1 if a new buffer is allocated. Otherwise,
 * it is left untouched.
 * --
 * We just discovered a security issue. MySQL is so
 * "smart" to not only support the standard SQL mechanism
 * for escaping quotes, but to also provide its own (using
 * c-type syntax with backslashes). As such, it is actually
 * possible to do sql injection via rsyslogd. The cure is now
 * to escape backslashes, too. As we have found on the web, some
 * other databases seem to be similar "smart" (why do we have standards
 * at all if they are violated without any need???). Even better, MySQL's
 * smartness depends on config settings. So we add a new option to this
 * function that allows the caller to select if they want to standard or
 * "smart" encoding ;)
 * --
 * Parameter "mode" is STDSQL_ESCAPE, SQL_ESCAPE "smart" SQL engines, or
 * JSON_ESCAPE for everyone requiring escaped JSON (e.g. ElasticSearch).
 * 2005-09-22 rgerhards
 */
rsRetVal doEscape(uchar **pp, rs_size_t *pLen, unsigned short *pbMustBeFreed, int mode) {
    DEFiRet;
    uchar *p = NULL;
    int iLen;
    cstr_t *pStrB = NULL;
    uchar *pszGenerated;

    assert(pp != NULL);
    assert(*pp != NULL);
    assert(pLen != NULL);
    assert(pbMustBeFreed != NULL);

    /* first check if we need to do anything at all... */
    if (mode == STDSQL_ESCAPE)
        for (p = *pp; *p && *p != '\''; ++p);
    else if (mode == SQL_ESCAPE)
        for (p = *pp; *p && *p != '\'' && *p != '\\'; ++p);
    else if (mode == JSON_ESCAPE)
        for (p = *pp; *p && (*p == '"' || *p == '\\'); ++p);
    /* when we get out of the loop, we are either at the
     * string terminator or the first character to escape */
    if (p && *p == '\0') FINALIZE; /* nothing to do in this case! */

    p = *pp;
    iLen = *pLen;
    CHKiRet(cstrConstruct(&pStrB));

    while (*p) {
        if ((mode == SQL_ESCAPE || mode == STDSQL_ESCAPE) && *p == '\'') {
            CHKiRet(cstrAppendChar(pStrB, (mode == STDSQL_ESCAPE) ? '\'' : '\\'));
            iLen++; /* reflect the extra character */
        } else if ((mode == SQL_ESCAPE) && *p == '\\') {
            CHKiRet(cstrAppendChar(pStrB, '\\'));
            iLen++; /* reflect the extra character */
        } else if ((mode == JSON_ESCAPE) && (*p == '"' || *p == '\\')) {
            CHKiRet(cstrAppendChar(pStrB, '\\'));
            iLen++; /* reflect the extra character */
        }
        CHKiRet(cstrAppendChar(pStrB, *p));
        ++p;
    }
    cstrFinalize(pStrB);
    CHKiRet(cstrConvSzStrAndDestruct(&pStrB, &pszGenerated, 0));

    if (*pbMustBeFreed) free(*pp); /* discard previous value */

    *pp = pszGenerated;
    *pLen = iLen;
    *pbMustBeFreed = 1;

finalize_it:
    if (iRet != RS_RET_OK) {
        doEmergencyEscape(*pp, mode);
        if (pStrB != NULL) cstrDestruct(&pStrB);
    }

    RETiRet;
}


/* Constructs a template entry object. Returns pointer to it
 * or NULL (if it fails). Pointer to associated template list entry
 * must be provided.
 */
static struct templateEntry *tpeConstruct(struct template *pTpl) {
    struct templateEntry *pTpe;

    assert(pTpl != NULL);

    if ((pTpe = calloc(1, sizeof(struct templateEntry))) == NULL) return NULL;

    /* basic initialization is done via calloc() - need to
     * initialize only values != 0. */

    if (pTpl->pEntryLast == NULL) {
        /* we are the first element! */
        pTpl->pEntryRoot = pTpl->pEntryLast = pTpe;
    } else {
        pTpl->pEntryLast->pNext = pTpe;
        pTpl->pEntryLast = pTpe;
    }
    pTpl->tpenElements++;

    return (pTpe);
}


/* Helper function to apply case-sensitivity to templates.
 */
static void apply_case_sensitivity(struct template *pTpl) {
    if (pTpl->optCaseSensitive) return;

    struct templateEntry *pTpe;

    for (pTpe = pTpl->pEntryRoot; pTpe != NULL; pTpe = pTpe->pNext) {
        if (pTpe->eEntryType == FIELD) {
            if (pTpe->data.field.msgProp.id == PROP_CEE || pTpe->data.field.msgProp.id == PROP_LOCAL_VAR ||
                pTpe->data.field.msgProp.id == PROP_GLOBAL_VAR) {
                uchar *p;
                p = pTpe->fieldName;
                for (; *p; ++p) *p = tolower(*p);
                p = pTpe->data.field.msgProp.name;
                for (; *p; ++p) *p = tolower(*p);
            }
        }
    }
}


/* Constructs a template list object. Returns pointer to it
 * or NULL (if it fails).
 */
static struct template *tplConstruct(rsconf_t *conf) {
    struct template *pTpl;
    if ((pTpl = calloc(1, sizeof(struct template))) == NULL) return NULL;

    /* basic initialisation is done via calloc() - need to
     * initialize only values != 0. */

    if (conf->templates.last == NULL) {
        /* we are the first element! */
        conf->templates.root = conf->templates.last = pTpl;
    } else {
        conf->templates.last->pNext = pTpl;
        conf->templates.last = pTpl;
    }

    return (pTpl);
}


/* helper to tplAddLine. Parses a constant and generates
 * the necessary structure.
 * Parameter "bDoEscapes" is to support legacy vs. v6+ config system. In
 * legacy, we must do escapes ourselves, whereas v6+ passes in already
 * escaped strings (which we are NOT permitted to further escape, this would
 * cause invalid result strings!). Note: if escapes are not permitted,
 * quotes (") are just a regular character and do NOT terminate the constant!
 */
static rsRetVal do_Constant(unsigned char **pp, struct template *pTpl, int bDoEscapes) {
    register unsigned char *p;
    cstr_t *pStrB;
    struct templateEntry *pTpe;
    int i;
    DEFiRet;

    assert(pp != NULL);
    assert(*pp != NULL);
    assert(pTpl != NULL);

    p = *pp;

    CHKiRet(cstrConstruct(&pStrB));
    /* process the message and expand escapes
     * (additional escapes can be added here if needed)
     */
    while (*p && *p != '%' && !(bDoEscapes && *p == '\"')) {
        if (bDoEscapes && *p == '\\') {
            switch (*++p) {
                case '\0':
                    /* the best we can do - it's invalid anyhow... */
                    cstrAppendChar(pStrB, *p);
                    break;
                case 'n':
                    cstrAppendChar(pStrB, '\n');
                    ++p;
                    break;
                case 'r':
                    cstrAppendChar(pStrB, '\r');
                    ++p;
                    break;
                case '\\':
                    cstrAppendChar(pStrB, '\\');
                    ++p;
                    break;
                case '%':
                    cstrAppendChar(pStrB, '%');
                    ++p;
                    break;
                case '0': /* numerical escape sequence */
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                    i = 0;
                    while (*p && isdigit((int)*p)) {
                        i = i * 10 + *p++ - '0';
                    }
                    cstrAppendChar(pStrB, i);
                    break;
                default:
                    cstrAppendChar(pStrB, *p++);
                    break;
            }
        } else
            cstrAppendChar(pStrB, *p++);
    }

    if ((pTpe = tpeConstruct(pTpl)) == NULL) {
        rsCStrDestruct(&pStrB);
        ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
    }
    pTpe->eEntryType = CONSTANT;
    cstrFinalize(pStrB);
    /* We obtain the length from the counted string object
     * (before we delete it). Later we might take additional
     * benefit from the counted string object.
     * 2005-09-09 rgerhards
     */
    pTpe->data.constant.iLenConstant = rsCStrLen(pStrB);
    CHKiRet(cstrConvSzStrAndDestruct(&pStrB, &pTpe->data.constant.pConstant, 0));

    *pp = p;

finalize_it:
    RETiRet;
}

/* Helper that checks to see if a property already has a format
 * type defined
 */
static int hasFormat(struct templateEntry *pTpe) {
    return (pTpe->data.field.options.bCSV || pTpe->data.field.options.bJSON || pTpe->data.field.options.bJSONf ||
            pTpe->data.field.options.bJSONr);
}

/* Helper to do_Parameter(). This parses the formatting options
 * specified in a template variable. It returns the passed-in pointer
 * updated to the next processed character.
 */
static void doOptions(unsigned char **pp, struct templateEntry *pTpe) {
    register unsigned char *p;
    unsigned char Buf[64];
    size_t i;

    assert(pp != NULL);
    assert(*pp != NULL);
    assert(pTpe != NULL);

    p = *pp;

    while (*p && *p != '%' && *p != ':') {
        /* outer loop - until end of options */
        memset(Buf, 0, sizeof(Buf)); /* silence valgrind warnings */
        i = 0;
        while ((i < sizeof(Buf) - 1) && *p && *p != '%' && *p != ':' && *p != ',') {
            /* inner loop - until end of ONE option */
            Buf[i++] = tolower((int)*p);
            ++p;
        }
        Buf[i] = '\0'; /* terminate */
        /* check if we need to skip oversize option */
        while (*p && *p != '%' && *p != ':' && *p != ',') ++p; /* just skip */
        if (*p == ',') ++p; /* eat ',' */
        /* OK, we got the option, so now lets look what
         * it tells us...
         */
        if (!strcmp((char *)Buf, "date-mysql")) {
            pTpe->data.field.eDateFormat = tplFmtMySQLDate;
        } else if (!strcmp((char *)Buf, "date-pgsql")) {
            pTpe->data.field.eDateFormat = tplFmtPgSQLDate;
        } else if (!strcmp((char *)Buf, "date-rfc3164")) {
            pTpe->data.field.eDateFormat = tplFmtRFC3164Date;
        } else if (!strcmp((char *)Buf, "date-rfc3164-buggyday")) {
            pTpe->data.field.eDateFormat = tplFmtRFC3164BuggyDate;
        } else if (!strcmp((char *)Buf, "date-rfc3339")) {
            pTpe->data.field.eDateFormat = tplFmtRFC3339Date;
        } else if (!strcmp((char *)Buf, "date-unixtimestamp")) {
            pTpe->data.field.eDateFormat = tplFmtUnixDate;
        } else if (!strcmp((char *)Buf, "date-subseconds")) {
            pTpe->data.field.eDateFormat = tplFmtSecFrac;
        } else if (!strcmp((char *)Buf, "date-wdayname")) {
            pTpe->data.field.eDateFormat = tplFmtWDayName;
        } else if (!strcmp((char *)Buf, "date-wday")) {
            pTpe->data.field.eDateFormat = tplFmtWDay;
        } else if (!strcmp((char *)Buf, "date-year")) {
            pTpe->data.field.eDateFormat = tplFmtYear;
        } else if (!strcmp((char *)Buf, "date-month")) {
            pTpe->data.field.eDateFormat = tplFmtMonth;
        } else if (!strcmp((char *)Buf, "date-day")) {
            pTpe->data.field.eDateFormat = tplFmtDay;
        } else if (!strcmp((char *)Buf, "date-hour")) {
            pTpe->data.field.eDateFormat = tplFmtHour;
        } else if (!strcmp((char *)Buf, "date-minute")) {
            pTpe->data.field.eDateFormat = tplFmtMinute;
        } else if (!strcmp((char *)Buf, "date-second")) {
            pTpe->data.field.eDateFormat = tplFmtSecond;
        } else if (!strcmp((char *)Buf, "date-tzoffshour")) {
            pTpe->data.field.eDateFormat = tplFmtTZOffsHour;
        } else if (!strcmp((char *)Buf, "date-tzoffsmin")) {
            pTpe->data.field.eDateFormat = tplFmtTZOffsMin;
        } else if (!strcmp((char *)Buf, "date-tzoffsdirection")) {
            pTpe->data.field.eDateFormat = tplFmtTZOffsDirection;
        } else if (!strcmp((char *)Buf, "date-ordinal")) {
            pTpe->data.field.eDateFormat = tplFmtOrdinal;
        } else if (!strcmp((char *)Buf, "date-week")) {
            pTpe->data.field.eDateFormat = tplFmtWeek;
        } else if (!strcmp((char *)Buf, "date-iso-week")) {
            pTpe->data.field.eDateFormat = tplFmtISOWeek;
        } else if (!strcmp((char *)Buf, "date-iso-week-year")) {
            pTpe->data.field.eDateFormat = tplFmtISOWeekYear;
        } else if (!strcmp((char *)Buf, "date-utc")) {
            pTpe->data.field.options.bDateInUTC = 1;
        } else if (!strcmp((char *)Buf, "lowercase")) {
            pTpe->data.field.eCaseConv = tplCaseConvLower;
        } else if (!strcmp((char *)Buf, "uppercase")) {
            pTpe->data.field.eCaseConv = tplCaseConvUpper;
        } else if (!strcmp((char *)Buf, "sp-if-no-1st-sp")) {
            pTpe->data.field.options.bSPIffNo1stSP = 1;
        } else if (!strcmp((char *)Buf, "compressspace")) {
            pTpe->data.field.options.bCompressSP = 1;
        } else if (!strcmp((char *)Buf, "escape-cc")) {
            pTpe->data.field.options.bEscapeCC = 1;
        } else if (!strcmp((char *)Buf, "drop-cc")) {
            pTpe->data.field.options.bDropCC = 1;
        } else if (!strcmp((char *)Buf, "space-cc")) {
            pTpe->data.field.options.bSpaceCC = 1;
        } else if (!strcmp((char *)Buf, "drop-last-lf")) {
            pTpe->data.field.options.bDropLastLF = 1;
        } else if (!strcmp((char *)Buf, "secpath-drop")) {
            pTpe->data.field.options.bSecPathDrop = 1;
        } else if (!strcmp((char *)Buf, "secpath-replace")) {
            pTpe->data.field.options.bSecPathReplace = 1;
        } else if (!strcmp((char *)Buf, "pos-end-relative")) {
            pTpe->data.field.options.bFromPosEndRelative = 1;
        } else if (!strcmp((char *)Buf, "fixed-width")) {
            pTpe->data.field.options.bFixedWidth = 1;
        } else if (!strcmp((char *)Buf, "csv")) {
            if (hasFormat(pTpe)) {
                LogError(0, NO_ERRCODE,
                         "error: can only specify "
                         "one option out of (json, jsonf, jsonr, jsonfr, csv) - csv ignored");
            } else {
                pTpe->data.field.options.bCSV = 1;
            }
        } else if (!strcmp((char *)Buf, "json")) {
            if (hasFormat(pTpe)) {
                LogError(0, NO_ERRCODE,
                         "error: can only specify "
                         "one option out of (json, jsonf, jsonr, jsonfr, csv) - json ignored");
            } else {
                pTpe->data.field.options.bJSON = 1;
            }
        } else if (!strcmp((char *)Buf, "jsonf")) {
            if (hasFormat(pTpe)) {
                LogError(0, NO_ERRCODE,
                         "error: can only specify "
                         "one option out of (json, jsonf, jsonr, jsonfr, csv) - jsonf ignored");
            } else {
                pTpe->data.field.options.bJSONf = 1;
            }
        } else if (!strcmp((char *)Buf, "jsonr")) {
            if (hasFormat(pTpe)) {
                LogError(0, NO_ERRCODE,
                         "error: can only specify "
                         "one option out of (json, jsonf, jsonr, jsonfr, csv) - jsonr ignored");
            } else {
                pTpe->data.field.options.bJSONr = 1;
            }
        } else if (!strcmp((char *)Buf, "jsonfr")) {
            if (hasFormat(pTpe)) {
                LogError(0, NO_ERRCODE,
                         "error: can only specify "
                         "one option out of (json, jsonf, jsonr, jsonfr, csv) - jsonfr ignored");
            } else {
                pTpe->data.field.options.bJSONfr = 1;
            }
        } else if (!strcmp((char *)Buf, "mandatory-field")) {
            pTpe->data.field.options.bMandatory = 1;
        } else {
            LogError(0, NO_ERRCODE,
                     "template error: invalid field option '%s' "
                     "specified - ignored",
                     Buf);
        }
    }

    *pp = p;
}

/* helper to tplAddLine. Parses a parameter and generates
 * the necessary structure.
 */
static rsRetVal do_Parameter(uchar **pp, struct template *pTpl) {
    uchar *p;
    cstr_t *pStrProp = NULL;
    cstr_t *pStrField = NULL;
    struct templateEntry *pTpe;
    int iNum; /* to compute numbers */
#ifdef FEATURE_REGEXP
    /* APR: variables for regex */
    rsRetVal iRetLocal;
    int longitud;
    unsigned char *regex_char;
    unsigned char *regex_end;
#endif
    DEFiRet;

    assert(pp != NULL);
    assert(*pp != NULL);
    assert(pTpl != NULL);

    p = (uchar *)*pp;
    CHKiRet(cstrConstruct(&pStrProp));
    CHKmalloc(pTpe = tpeConstruct(pTpl));
    pTpe->eEntryType = FIELD;

    while (*p && *p != '%' && *p != ':') {
        cstrAppendChar(pStrProp, *p);
        ++p;
    }

    /* got the name */
    cstrFinalize(pStrProp);

    CHKiRet(msgPropDescrFill(&pTpe->data.field.msgProp, cstrGetSzStrNoNULL(pStrProp), cstrLen(pStrProp)));

    /* Check frompos, if it has an R, then topos should be a regex */
    if (*p == ':') {
        pTpe->bComplexProcessing = 1;
        ++p; /* eat ':' */
#ifdef FEATURE_REGEXP
        if (*p == 'R') {
            /* APR: R found! regex alarm ! :) */
            ++p; /* eat ':' */

            /* first come the regex type */
            if (*p == ',') {
                ++p; /* eat ',' */
                if (p[0] == 'B' && p[1] == 'R' && p[2] == 'E' && (p[3] == ',' || p[3] == ':')) {
                    pTpe->data.field.typeRegex = TPL_REGEX_BRE;
                    p += 3; /* eat indicator sequence */
                } else if (p[0] == 'E' && p[1] == 'R' && p[2] == 'E' && (p[3] == ',' || p[3] == ':')) {
                    pTpe->data.field.typeRegex = TPL_REGEX_ERE;
                    p += 3; /* eat indicator sequence */
                } else {
                    LogError(0, NO_ERRCODE,
                             "error: invalid regular expression "
                             "type, rest of line %s",
                             (char *)p);
                }
            }

            /* now check for submatch ID */
            pTpe->data.field.iSubMatchToUse = 0;
            if (*p == ',') {
                /* in this case a number follows, which indicates which match
                 * shall be used. This must be a single digit.
                 */
                ++p; /* eat ',' */
                if (isdigit((int)*p)) {
                    pTpe->data.field.iSubMatchToUse = *p - '0';
                    ++p; /* eat digit */
                }
            }

            /* now pull what to do if we do not find a match */
            if (*p == ',') {
                ++p; /* eat ',' */
                if (p[0] == 'D' && p[1] == 'F' && p[2] == 'L' && p[3] == 'T' && (p[4] == ',' || p[4] == ':')) {
                    pTpe->data.field.nomatchAction = TPL_REGEX_NOMATCH_USE_DFLTSTR;
                    p += 4; /* eat indicator sequence */
                } else if (p[0] == 'B' && p[1] == 'L' && p[2] == 'A' && p[3] == 'N' && p[4] == 'K' &&
                           (p[5] == ',' || p[5] == ':')) {
                    pTpe->data.field.nomatchAction = TPL_REGEX_NOMATCH_USE_BLANK;
                    p += 5; /* eat indicator sequence */
                } else if (p[0] == 'F' && p[1] == 'I' && p[2] == 'E' && p[3] == 'L' && p[4] == 'D' &&
                           (p[5] == ',' || p[5] == ':')) {
                    pTpe->data.field.nomatchAction = TPL_REGEX_NOMATCH_USE_WHOLE_FIELD;
                    p += 5; /* eat indicator sequence */
                } else if (p[0] == 'Z' && p[1] == 'E' && p[2] == 'R' && p[3] == 'O' && (p[4] == ',' || p[4] == ':')) {
                    pTpe->data.field.nomatchAction = TPL_REGEX_NOMATCH_USE_ZERO;
                    p += 4; /* eat indicator sequence */
                } else if (p[0] == ',') { /* empty, use default */
                    pTpe->data.field.nomatchAction = TPL_REGEX_NOMATCH_USE_DFLTSTR;
                    /* do NOT eat indicator sequence, as this was already eaten - the
                     * comma itself is already part of the next field.
                     */
                } else {
                    LogError(0, NO_ERRCODE,
                             "template %s error: invalid regular "
                             "expression type, rest of line %s",
                             pTpl->pszName, (char *)p);
                }
            }

            /* now check for match ID */
            pTpe->data.field.iMatchToUse = 0;
            if (*p == ',') {
                /* in this case a number follows, which indicates which match
                 * shall be used. This must be a single digit.
                 */
                ++p; /* eat ',' */
                if (isdigit((int)*p)) {
                    pTpe->data.field.iMatchToUse = *p - '0';
                    ++p; /* eat digit */
                }
            }

            if (*p != ':') {
                /* There is something more than an R , this is invalid ! */
                /* Complain on extra characters */
                LogError(0, NO_ERRCODE,
                         "error: invalid character in frompos "
                         "after \"R\", property: '%%%s'",
                         (char *)*pp);
            } else {
                pTpe->data.field.has_regex = 1;
                dbgprintf("we have a regexp and use match #%d, submatch #%d\n", pTpe->data.field.iMatchToUse,
                          pTpe->data.field.iSubMatchToUse);
            }
        } else {
            /* now we fall through the "regular" FromPos code */
#endif /* #ifdef FEATURE_REGEXP */
            if (*p == 'F') {
#ifdef STRICT_GPLV3
                pTpe->data.field.field_expand = 0;
#endif
                /* we have a field counter, so indicate it in the template */
                ++p; /* eat 'F' */
                if (*p == ':') {
                    /* no delimiter specified, so use the default (HT) */
                    pTpe->data.field.has_fields = 1;
                    pTpe->data.field.field_delim = 9;
                } else if (*p == ',') {
                    ++p; /* eat ',' */
                    /* configured delimiter follows, so we need to obtain
                     * it. Important: the following number must be the
                     * **DECIMAL** ASCII value of the delimiter character.
                     */
                    pTpe->data.field.has_fields = 1;
                    if (!isdigit((int)*p)) {
                        /* complain and use default */
                        LogError(0, NO_ERRCODE,
                                 "error: invalid character in "
                                 "frompos after \"F,\", property: '%%%s' - using 9 (HT) as field delimiter",
                                 (char *)*pp);
                        pTpe->data.field.field_delim = 9;
                    } else {
                        iNum = 0;
                        while (isdigit((int)*p)) iNum = iNum * 10 + *p++ - '0';
                        if (iNum < 0 || iNum > 255) {
                            LogError(0, NO_ERRCODE,
                                     "error: non-USASCII delimiter "
                                     "character value %d in template - using 9 (HT) as substitute",
                                     iNum);
                            pTpe->data.field.field_delim = 9;
                        } else {
                            pTpe->data.field.field_delim = iNum;
#ifdef STRICT_GPLV3
                            if (*p == '+') {
                                pTpe->data.field.field_expand = 1;
                                p++;
                            }
#endif
                            if (*p == ',') { /* real fromPos? */
                                ++p;
                                iNum = 0;
                                while (isdigit((int)*p)) iNum = iNum * 10 + *p++ - '0';
                                pTpe->data.field.iFromPos = iNum;
                            } else if (*p != ':') {
                                parser_errmsg(
                                    "error: invalid character "
                                    "'%c' in frompos after \"F,\", property: '%s' "
                                    "be sure to use DECIMAL character "
                                    "codes!",
                                    *p, (char *)*pp);
                                ABORT_FINALIZE(RS_RET_SYNTAX_ERROR);
                            }
                        }
                    }
                } else {
                    /* invalid character after F, so we need to reject
                     * this.
                     */
                    LogError(0, NO_ERRCODE,
                             "error: invalid character in frompos "
                             "after \"F\", property: '%%%s'",
                             (char *)*pp);
                }
            } else {
                /* we now have a simple offset in frompos (the previously "normal" case) */
                iNum = 0;
                while (isdigit((int)*p)) iNum = iNum * 10 + *p++ - '0';
                pTpe->data.field.iFromPos = iNum;
                /* skip to next known good */
                while (*p && *p != '%' && *p != ':') {
                    /* TODO: complain on extra characters */
                    dbgprintf("error: extra character in frompos: '%s'\n", p);
                    ++p;
                }
            }
#ifdef FEATURE_REGEXP
        }
#endif /* #ifdef FEATURE_REGEXP */
    }
    /* check topos  (holds an regex if FromPos is "R"*/
    if (*p == ':') {
        ++p; /* eat ':' */

#ifdef FEATURE_REGEXP
        if (pTpe->data.field.has_regex) {
            dbgprintf("debug: has regex \n");
            /* APR 2005-09 I need the string that represent the regex */
            /* The regex end is: "--end" */
            /* TODO : this is hardcoded and cant be escaped, please change */
            regex_end = (unsigned char *)strstr((char *)p, "--end");
            if (regex_end == NULL) {
                dbgprintf("error: can not find regex end in: '%s'\n", p);
                pTpe->data.field.has_regex = 0;
            } else {
                /* We get here ONLY if the regex end was found */
                longitud = regex_end - p;
                /* Malloc for the regex string */
                regex_char = (unsigned char *)malloc(longitud + 1);
                if (regex_char == NULL) {
                    dbgprintf("Could not allocate memory for template parameter!\n");
                    pTpe->data.field.has_regex = 0;
                    ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
                }

                /* Get the regex string for compiling later */
                memcpy(regex_char, p, longitud);
                regex_char[longitud] = '\0';
                dbgprintf("debug: regex detected: '%s'\n", regex_char);
                /* Now i compile the regex */
                /* Remember that the re is an attribute of the Template entry */
                if ((iRetLocal = objUse(regexp, LM_REGEXP_FILENAME)) == RS_RET_OK) {
                    int iOptions;
                    iOptions = (pTpe->data.field.typeRegex == TPL_REGEX_ERE) ? REG_EXTENDED : 0;
                    int errcode;
                    if ((errcode = regexp.regcomp(&(pTpe->data.field.re), (char *)regex_char, iOptions) != 0)) {
                        char errbuff[512];
                        regexp.regerror(errcode, &(pTpe->data.field.re), errbuff, sizeof(errbuff));
                        DBGPRINTF("Template.c: Error in regular expression: %s\n", errbuff);
                        pTpe->data.field.has_regex = 2;
                    }
                } else {
                    /* regexp object could not be loaded */
                    dbgprintf(
                        "error %d trying to load regexp library - this may be desired "
                        "and thus OK",
                        iRetLocal);
                    if (bFirstRegexpErrmsg) {
                        /* prevent flood of messages, maybe even an endless loop! */
                        bFirstRegexpErrmsg = 0;
                        LogError(0, NO_ERRCODE,
                                 "regexp library could not be loaded "
                                 "(error %d), regexp ignored",
                                 iRetLocal);
                    }
                    pTpe->data.field.has_regex = 2;
                }

                /* Finally we move the pointer to the end of the regex
                 * so it aint parsed twice or something weird */
                p = regex_end + 5 /*strlen("--end")*/;
                free(regex_char);
            }
        } else if (*p == '$') {
            /* shortcut for "end of message */
            p++; /* eat '$' */
            /* in this case, we do a quick, somewhat dirty but totally
             * legitimate trick: we simply use a topos that is higher than
             * potentially ever can happen. The code below checks that no copy
             * will occur after the end of string, so this is perfectly legal.
             * rgerhards, 2006-10-17
             */
            pTpe->data.field.iToPos = 9999999;
        } else {
            /* fallthrough to "regular" ToPos code */
#endif /* #ifdef FEATURE_REGEXP */

            if (pTpe->data.field.has_fields == 1) {
                iNum = 0;
                while (isdigit((int)*p)) iNum = iNum * 10 + *p++ - '0';
                pTpe->data.field.iFieldNr = iNum;
                if (*p == ',') { /* get real toPos? */
                    ++p;
                    iNum = 0;
                    while (isdigit((int)*p)) iNum = iNum * 10 + *p++ - '0';
                    pTpe->data.field.iToPos = iNum;
                }
            } else {
                iNum = 0;
                while (isdigit((int)*p)) iNum = iNum * 10 + *p++ - '0';
                pTpe->data.field.iToPos = iNum;
            }
            /* skip to next known good */
            while (*p && *p != '%' && *p != ':') {
                /* TODO: complain on extra characters */
                dbgprintf("error: extra character in frompos: '%s'\n", p);
                ++p;
            }
#ifdef FEATURE_REGEXP
        }
#endif /* #ifdef FEATURE_REGEXP */
    }

    /* check options */
    if (*p == ':') {
        ++p; /* eat ':' */
        doOptions(&p, pTpe);
    }

    if (pTpe->data.field.options.bFromPosEndRelative) {
        if (pTpe->data.field.iToPos > pTpe->data.field.iFromPos) {
            iNum = pTpe->data.field.iToPos;
            pTpe->data.field.iToPos = pTpe->data.field.iFromPos;
            pTpe->data.field.iFromPos = iNum;
        }
    } else {
        if (pTpe->data.field.iToPos < pTpe->data.field.iFromPos) {
            iNum = pTpe->data.field.iToPos;
            pTpe->data.field.iToPos = pTpe->data.field.iFromPos;
            pTpe->data.field.iFromPos = iNum;
        }
    }


    /* check field name */
    if (*p == ':') {
        ++p; /* eat ':' */
        CHKiRet(cstrConstruct(&pStrField));
        while (*p != ':' && *p != '%' && *p != '\0') {
            cstrAppendChar(pStrField, *p);
            ++p;
        }
        cstrFinalize(pStrField);
    }

    /* save field name - if none was given, use the property name instead */
    if (pStrField == NULL) {
        if (pTpe->data.field.msgProp.id == PROP_CEE || pTpe->data.field.msgProp.id == PROP_LOCAL_VAR) {
            /* in CEE case, we remove "$!"/"$." from the fieldname - it's just our indicator */
            pTpe->fieldName = ustrdup(cstrGetSzStrNoNULL(pStrProp) + 2);
            pTpe->lenFieldName = cstrLen(pStrProp) - 2;
        } else {
            pTpe->fieldName = ustrdup(cstrGetSzStrNoNULL(pStrProp));
            pTpe->lenFieldName = cstrLen(pStrProp);
        }
    } else {
        pTpe->fieldName = ustrdup(cstrGetSzStrNoNULL(pStrField));
        pTpe->lenFieldName = ustrlen(pTpe->fieldName);
        cstrDestruct(&pStrField);
    }
    if (pTpe->fieldName == NULL) {
        DBGPRINTF("template/do_Parameter: fieldName is NULL!\n");
        ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
    }
    if (*p) ++p; /* eat '%' */
    *pp = p;
finalize_it:
    if (pStrProp != NULL) cstrDestruct(&pStrProp);
    RETiRet;
}


/* Add a new entry for a template module.
 * returns pointer to new object if it succeeds, NULL otherwise.
 * rgerhards, 2010-05-31
 */
static rsRetVal tplAddTplMod(struct template *pTpl, uchar **ppRestOfConfLine) {
    uchar *pSrc;
    uchar szMod[2048];
    unsigned lenMod;
    strgen_t *pStrgen;
    DEFiRet;

    pSrc = *ppRestOfConfLine;
    lenMod = 0;
    while (*pSrc && !isspace(*pSrc) && lenMod < sizeof(szMod) - 1) {
        szMod[lenMod] = *pSrc++;
        lenMod++;
    }
    szMod[lenMod] = '\0';
    *ppRestOfConfLine = pSrc;
    CHKiRet(strgen.FindStrgen(&pStrgen, szMod));
    pTpl->pStrgen = pStrgen->pModule->mod.sm.strgen;
    DBGPRINTF("template bound to strgen '%s'\n", szMod);
    /* check if the name potentially contains some well-known options
     * Note: we have opted to let the name contain all options. This sounds
     * useful, because the strgen MUST actually implement a specific set
     * of options. Doing this via the name looks to the enduser as if the
     * regular syntax were used, and it make sure the strgen postively
     * acknowledged implementing the option. -- rgerhards, 2011-03-21
     */
    if (lenMod > 6 && !strcasecmp((char *)szMod + lenMod - 7, ",stdsql")) {
        pTpl->optFormatEscape = STDSQL_ESCAPE;
        DBGPRINTF("strgen supports the stdsql option\n");
    } else if (lenMod > 3 && !strcasecmp((char *)szMod + lenMod - 4, ",sql")) {
        pTpl->optFormatEscape = SQL_ESCAPE;
        DBGPRINTF("strgen supports the sql option\n");
    } else if (lenMod > 4 && !strcasecmp((char *)szMod + lenMod - 4, ",json")) {
        pTpl->optFormatEscape = JSON_ESCAPE;
        DBGPRINTF("strgen supports the json option\n");
    }

finalize_it:
    RETiRet;
}


/* Add a new template line
 * returns pointer to new object if it succeeds, NULL otherwise.
 */
struct template *tplAddLine(rsconf_t *conf, const char *pName, uchar **ppRestOfConfLine) {
    struct template *pTpl;
    unsigned char *p;
    int bDone;
    size_t i;
    rsRetVal localRet;

    assert(pName != NULL);
    assert(ppRestOfConfLine != NULL);
    if ((pTpl = tplConstruct(conf)) == NULL) return NULL;

    DBGPRINTF("tplAddLine processing template '%s'\n", pName);
    pTpl->iLenName = strlen(pName);
    pTpl->pszName = (char *)malloc(pTpl->iLenName + 1);
    if (pTpl->pszName == NULL) {
        dbgprintf("tplAddLine could not alloc memory for template name!");
        pTpl->iLenName = 0;
        return NULL;
        /* I know - we create a memory leak here - but I deem
         * it acceptable as it is a) a very small leak b) very
         * unlikely to happen. rgerhards 2004-11-17
         */
    }
    memcpy(pTpl->pszName, pName, pTpl->iLenName + 1);

    /* now actually parse the line */
    p = *ppRestOfConfLine;
    assert(p != NULL);

    while (isspace((int)*p)) /* skip whitespace */
        ++p;

    switch (*p) {
        case '"': /* just continue */
            break;
        case '=':
            *ppRestOfConfLine = p + 1;
            localRet = tplAddTplMod(pTpl, ppRestOfConfLine);
            if (localRet != RS_RET_OK) {
                LogError(0, localRet, "Template '%s': error %d defining template via strgen module", pTpl->pszName,
                         localRet);
                /* we simply make the template defunct in this case by setting
                 * its name to a zero-string. We do not free it, as this would
                 * require additional code and causes only a very small memory
                 * consumption. Memory is freed, however, in normal operation
                 * and most importantly by HUPing syslogd.
                 */
                *pTpl->pszName = '\0';
            }
            return NULL;
        default:
            dbgprintf("Template '%s' invalid, does not start with '\"'!\n", pTpl->pszName);
            /* we simply make the template defunct in this case by setting
             * its name to a zero-string. We do not free it, as this would
             * require additional code and causes only a very small memory
             * consumption.
             */
            *pTpl->pszName = '\0';
            return NULL;
    }
    ++p;

    /* we finally go to the actual template string - so let's have some fun... */
    bDone = *p ? 0 : 1;
    while (!bDone) {
        switch (*p) {
            case '\0':
                bDone = 1;
                break;
            case '%': /* parameter */
                ++p; /* eat '%' */
                if (do_Parameter(&p, pTpl) != RS_RET_OK) {
                    dbgprintf("tplAddLine error: parameter invalid");
                    return NULL;
                };
                break;
            default: /* constant */
                do_Constant(&p, pTpl, 1);
                break;
        }
        if (*p == '"') { /* end of template string? */
            ++p; /* eat it! */
            bDone = 1;
        }
    }

    /* we now have the template - let's look at the options (if any)
     * we process options until we reach the end of the string or
     * an error occurs - whichever is first.
     */
    while (*p) {
        while (isspace((int)*p)) /* skip whitespace */
            ++p;

        if (*p != ',') break;
        ++p; /* eat ',' */

        while (isspace((int)*p)) /* skip whitespace */
            ++p;

        /* read option word */
        char optBuf[128] = {'\0'}; /* buffer for options - should be more than enough... */
        i = 0;
        while ((i < (sizeof(optBuf) - 1)) && *p && *p != '=' && *p != ',' && *p != '\n') {
            optBuf[i++] = tolower((int)*p);
            ++p;
        }
        optBuf[i] = '\0';

        if (*p == '\n') ++p;

        /* as of now, the no form is nonsense... but I do include
         * it anyhow... ;) rgerhards 2004-11-22
         */
        if (!strcmp(optBuf, "stdsql")) {
            pTpl->optFormatEscape = STDSQL_ESCAPE;
        } else if (!strcmp(optBuf, "json")) {
            pTpl->optFormatEscape = JSON_ESCAPE;
        } else if (!strcmp(optBuf, "sql")) {
            pTpl->optFormatEscape = SQL_ESCAPE;
        } else if (!strcmp(optBuf, "nosql")) {
            pTpl->optFormatEscape = NO_ESCAPE;
        } else if (!strcmp(optBuf, "casesensitive")) {
            pTpl->optCaseSensitive = 1;
        } else if (!strcmp(optBuf, "jsonf")) {
            pTpl->optFormatEscape = JSONF;
            pTpl->bJsonTreeEnabled = 0;
        } else if (!strcmp(optBuf, "jsonftree")) {
            pTpl->optFormatEscape = JSONF;
            pTpl->bJsonTreeEnabled = 1;
        } else {
            dbgprintf("Invalid option '%s' ignored.\n", optBuf);
        }
    }

    *ppRestOfConfLine = p;
    apply_case_sensitivity(pTpl);
    if (pTpl->optFormatEscape == JSONF) tplWarnDuplicateJsonKeys(pTpl);

    return (pTpl);
}

static rsRetVal createConstantTpe(struct template *pTpl, struct cnfobj *o) {
    struct templateEntry *pTpe;
    es_str_t *value = NULL; /* init just to keep compiler happy - mandatory parameter */
    int i;
    int is_jsonf = 0;
    struct cnfparamvals *pvals = NULL;
    struct json_object *json = NULL;
    struct json_object *jval = NULL;
    uchar *outname = NULL;
    DEFiRet;

    /* pull params */
    pvals = nvlstGetParams(o->nvlst, &pblkConstant, NULL);
    if (pvals == NULL) {
        parser_errmsg("error processing template parameters");
        ABORT_FINALIZE(RS_RET_MISSING_CNFPARAMS);
    }
    cnfparamsPrint(&pblkConstant, pvals);

    for (i = 0; i < pblkConstant.nParams; ++i) {
        if (!pvals[i].bUsed) continue;
        if (!strcmp(pblkConstant.descr[i].name, "value")) {
            value = pvals[i].val.d.estr;
        } else if (!strcmp(pblkConstant.descr[i].name, "outname")) {
            outname = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL);
        } else if (!strcmp(pblkConstant.descr[i].name, "format")) {
            if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"jsonf", sizeof("jsonf") - 1)) {
                is_jsonf = 1;
            } else {
                uchar *typeStr = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL);
                LogError(0, RS_RET_ERR, "invalid format type '%s' for constant", typeStr);
                free(typeStr);
                ABORT_FINALIZE(RS_RET_ERR);
            }
        } else {
            LogError(0, RS_RET_INTERNAL_ERROR,
                     "template:constantTpe: program error, non-handled "
                     "param '%s'\n",
                     pblkConstant.descr[i].name);
        }
    }

    if (is_jsonf && outname == NULL) {
        parser_errmsg("constant set to format jsonf, but outname not specified - aborting");
        ABORT_FINALIZE(RS_RET_ERR);
    }

    /* just double-check */
    assert(value != NULL);

    /* apply */
    CHKmalloc(pTpe = tpeConstruct(pTpl));
    es_unescapeStr(value);
    pTpe->eEntryType = CONSTANT;
    pTpe->fieldName = outname;
    if (outname != NULL) pTpe->lenFieldName = ustrlen(outname);
    pTpe->data.constant.bJSONf = is_jsonf;
    if (is_jsonf) {
        json = json_object_new_object();
        CHKmalloc(json);
        const char *sz = es_str2cstr(value, NULL);
        CHKmalloc(sz);
        jval = json_object_new_string(sz);
        if (jval == NULL) {
            free((void *)sz);
            ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
        }
        free((void *)sz);
        json_object_object_add(json, (char *)outname, jval);
        const char *jsonStr = json_object_get_string(json);
        CHKmalloc(jsonStr);
        const size_t jsonLen = strlen(jsonStr);
        if (jsonLen < 2) {
            parser_errmsg("error: failed to serialize jsonf constant");
            ABORT_FINALIZE(RS_RET_ERR);
        }
        const size_t fragmentLen = jsonLen - 2;
        uchar *fragment = (uchar *)strndup(jsonStr + 1, fragmentLen);
        CHKmalloc(fragment);
        size_t fragLen = ustrlen(fragment);
        while (fragLen > 0 && isspace((int)fragment[fragLen - 1])) {
            --fragLen;
        }
        fragment[fragLen] = '\0';
        size_t lead = 0;
        while (lead < fragLen && isspace((int)fragment[lead])) ++lead;
        if (lead > 0) {
            memmove(fragment, fragment + lead, fragLen - lead + 1);
            fragLen -= lead;
        }
        pTpe->data.constant.pConstant = fragment;
        pTpe->data.constant.iLenConstant = fragLen;
        json_object_put(json);
        json = NULL;
    } else {
        pTpe->data.constant.iLenConstant = es_strlen(value);
        pTpe->data.constant.pConstant = (uchar *)es_str2cstr(value, NULL);
    }

finalize_it:
    if (json != NULL) json_object_put(json);
    if (pvals != NULL) cnfparamvalsDestruct(pvals, &pblkConstant);
    RETiRet;
}

static rsRetVal createPropertyTpe(struct template *pTpl, struct cnfobj *o) {
    struct templateEntry *pTpe;
    uchar *name = NULL;
    uchar *outname = NULL;
    int i;
    int droplastlf = 0;
    int spifno1stsp = 0;
    int mandatory = 0;
    int frompos = -1;
    int topos = 0;
    int topos_set = 0;
    int fieldnum = -1;
    int fielddelim = 9; /* default is HT (USACSII 9) */
    int fixedwidth = 0;
    int re_matchToUse = 0;
    int re_submatchToUse = 0;
    int bComplexProcessing = 0;
    int bPosRelativeToEnd = 0;
    int bDateInUTC = 0;
    int bCompressSP = 0;
    unsigned dataType = TPE_DATATYPE_STRING;
    unsigned onEmpty = TPE_DATAEMPTY_KEEP;
    char *re_expr = NULL;
    struct cnfparamvals *pvals = NULL;
    enum { F_NONE, F_CSV, F_JSON, F_JSONF, F_JSONR, F_JSONFR } formatType = F_NONE;
    enum { CC_NONE, CC_ESCAPE, CC_SPACE, CC_DROP } controlchr = CC_NONE;
    enum { SP_NONE, SP_DROP, SP_REPLACE } secpath = SP_NONE;
    enum tplFormatCaseConvTypes caseconv = tplCaseConvNo;
    enum tplFormatTypes datefmt = tplFmtDefault;
    enum tplRegexType re_type = TPL_REGEX_BRE;
    enum tlpRegexNoMatchType re_nomatchType = TPL_REGEX_NOMATCH_USE_DFLTSTR;
    DEFiRet;

    /* pull params */
    pvals = nvlstGetParams(o->nvlst, &pblkProperty, NULL);
    if (pvals == NULL) {
        parser_errmsg("error processing template entry config parameters");
        ABORT_FINALIZE(RS_RET_MISSING_CNFPARAMS);
    }
    cnfparamsPrint(&pblkProperty, pvals);

    for (i = 0; i < pblkProperty.nParams; ++i) {
        if (!pvals[i].bUsed) continue;
        if (!strcmp(pblkProperty.descr[i].name, "name")) {
            name = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL);
        } else if (!strcmp(pblkProperty.descr[i].name, "datatype")) {
            if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"string", sizeof("string") - 1)) {
                dataType = TPE_DATATYPE_STRING;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"number", sizeof("number") - 1)) {
                dataType = TPE_DATATYPE_NUMBER;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"bool", sizeof("bool") - 1)) {
                dataType = TPE_DATATYPE_BOOL;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"auto", sizeof("auto") - 1)) {
                dataType = TPE_DATATYPE_AUTO;
            } else {
                uchar *typeStr = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL);
                LogError(0, RS_RET_ERR, "invalid dataType '%s' for property", typeStr);
                free(typeStr);
                ABORT_FINALIZE(RS_RET_ERR);
            }
        } else if (!strcmp(pblkProperty.descr[i].name, "onempty")) {
            if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"keep", sizeof("keep") - 1)) {
                onEmpty = TPE_DATAEMPTY_KEEP;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"skip", sizeof("skip") - 1)) {
                onEmpty = TPE_DATAEMPTY_SKIP;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"null", sizeof("null") - 1)) {
                onEmpty = TPE_DATAEMPTY_NULL;
            } else {
                uchar *typeStr = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL);
                LogError(0, RS_RET_ERR, "invalid onEmpty value '%s' for property", typeStr);
                free(typeStr);
                ABORT_FINALIZE(RS_RET_ERR);
            }
        } else if (!strcmp(pblkProperty.descr[i].name, "droplastlf")) {
            droplastlf = pvals[i].val.d.n;
            bComplexProcessing = 1;
        } else if (!strcmp(pblkProperty.descr[i].name, "fixedwidth")) {
            fixedwidth = pvals[i].val.d.n;
            bComplexProcessing = 1;
        } else if (!strcmp(pblkProperty.descr[i].name, "mandatory")) {
            mandatory = pvals[i].val.d.n;
        } else if (!strcmp(pblkProperty.descr[i].name, "spifno1stsp")) {
            spifno1stsp = pvals[i].val.d.n;
            bComplexProcessing = 1;
        } else if (!strcmp(pblkProperty.descr[i].name, "outname")) {
            outname = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL);
        } else if (!strcmp(pblkProperty.descr[i].name, "position.from")) {
            frompos = pvals[i].val.d.n;
            bComplexProcessing = 1;
        } else if (!strcmp(pblkProperty.descr[i].name, "position.to")) {
            topos = pvals[i].val.d.n;
            topos_set = 1;
            bComplexProcessing = 1;
        } else if (!strcmp(pblkProperty.descr[i].name, "position.relativetoend")) {
            bPosRelativeToEnd = pvals[i].val.d.n;
        } else if (!strcmp(pblkProperty.descr[i].name, "field.number")) {
            fieldnum = pvals[i].val.d.n;
            bComplexProcessing = 1;
        } else if (!strcmp(pblkProperty.descr[i].name, "field.delimiter")) {
            fielddelim = pvals[i].val.d.n;
            bComplexProcessing = 1;
        } else if (!strcmp(pblkProperty.descr[i].name, "regex.expression")) {
            re_expr = es_str2cstr(pvals[i].val.d.estr, NULL);
            bComplexProcessing = 1;
        } else if (!strcmp(pblkProperty.descr[i].name, "regex.type")) {
            bComplexProcessing = 1;
            if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"BRE", sizeof("BRE") - 1)) {
                re_type = TPL_REGEX_BRE;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"ERE", sizeof("ERE") - 1)) {
                re_type = TPL_REGEX_ERE;
            } else {
                uchar *typeStr = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL);
                LogError(0, RS_RET_ERR, "invalid regex.type '%s' for property", typeStr);
                free(typeStr);
                ABORT_FINALIZE(RS_RET_ERR);
            }
        } else if (!strcmp(pblkProperty.descr[i].name, "regex.nomatchmode")) {
            bComplexProcessing = 1;
            if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"DFLT", sizeof("DFLT") - 1)) {
                re_nomatchType = TPL_REGEX_NOMATCH_USE_DFLTSTR;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"BLANK", sizeof("BLANK") - 1)) {
                re_nomatchType = TPL_REGEX_NOMATCH_USE_BLANK;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"FIELD", sizeof("FIELD") - 1)) {
                re_nomatchType = TPL_REGEX_NOMATCH_USE_WHOLE_FIELD;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"ZERO", sizeof("ZERO") - 1)) {
                re_nomatchType = TPL_REGEX_NOMATCH_USE_ZERO;
            } else {
                uchar *typeStr = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL);
                LogError(0, RS_RET_ERR, "invalid format type '%s' for property", typeStr);
                free(typeStr);
                ABORT_FINALIZE(RS_RET_ERR);
            }
        } else if (!strcmp(pblkProperty.descr[i].name, "regex.match")) {
            bComplexProcessing = 1;
            re_matchToUse = pvals[i].val.d.n;
        } else if (!strcmp(pblkProperty.descr[i].name, "regex.submatch")) {
            bComplexProcessing = 1;
            re_submatchToUse = pvals[i].val.d.n;
        } else if (!strcmp(pblkProperty.descr[i].name, "format")) {
            bComplexProcessing = 1;
            if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"csv", sizeof("csv") - 1)) {
                formatType = F_CSV;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"json", sizeof("json") - 1)) {
                formatType = F_JSON;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"jsonf", sizeof("jsonf") - 1)) {
                formatType = F_JSONF;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"jsonr", sizeof("jsonr") - 1)) {
                formatType = F_JSONR;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"jsonfr", sizeof("jsonfr") - 1)) {
                formatType = F_JSONFR;
            } else {
                uchar *typeStr = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL);
                LogError(0, RS_RET_ERR, "invalid format type '%s' for property", typeStr);
                free(typeStr);
                ABORT_FINALIZE(RS_RET_ERR);
            }
        } else if (!strcmp(pblkProperty.descr[i].name, "controlcharacters")) {
            bComplexProcessing = 1;
            if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"escape", sizeof("escape") - 1)) {
                controlchr = CC_ESCAPE;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"space", sizeof("space") - 1)) {
                controlchr = CC_SPACE;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"drop", sizeof("drop") - 1)) {
                controlchr = CC_DROP;
            } else {
                uchar *typeStr = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL);
                LogError(0, RS_RET_ERR, "invalid controlcharacter mode '%s' for property", typeStr);
                free(typeStr);
                ABORT_FINALIZE(RS_RET_ERR);
            }
        } else if (!strcmp(pblkProperty.descr[i].name, "securepath")) {
            bComplexProcessing = 1;
            if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"drop", sizeof("drop") - 1)) {
                secpath = SP_DROP;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"replace", sizeof("replace") - 1)) {
                secpath = SP_REPLACE;
            } else {
                uchar *typeStr = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL);
                LogError(0, RS_RET_ERR, "invalid securepath mode '%s' for property", typeStr);
                free(typeStr);
                ABORT_FINALIZE(RS_RET_ERR);
            }
        } else if (!strcmp(pblkProperty.descr[i].name, "caseconversion")) {
            bComplexProcessing = 1;
            if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"lower", sizeof("lower") - 1)) {
                caseconv = tplCaseConvLower;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"upper", sizeof("upper") - 1)) {
                caseconv = tplCaseConvUpper;
            } else {
                uchar *typeStr = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL);
                LogError(0, RS_RET_ERR, "invalid caseconversion type '%s' for property", typeStr);
                free(typeStr);
                ABORT_FINALIZE(RS_RET_ERR);
            }
        } else if (!strcmp(pblkProperty.descr[i].name, "compressspace")) {
            bComplexProcessing = 1;
            bCompressSP = pvals[i].val.d.n;
        } else if (!strcmp(pblkProperty.descr[i].name, "date.inutc")) {
            bDateInUTC = pvals[i].val.d.n;
        } else if (!strcmp(pblkProperty.descr[i].name, "dateformat")) {
            if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"mysql", sizeof("mysql") - 1)) {
                datefmt = tplFmtMySQLDate;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"pgsql", sizeof("pgsql") - 1)) {
                datefmt = tplFmtPgSQLDate;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"rfc3164", sizeof("rfc3164") - 1)) {
                datefmt = tplFmtRFC3164Date;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"rfc3164-buggyday",
                                     sizeof("rfc3164-buggyday") - 1)) {
                datefmt = tplFmtRFC3164BuggyDate;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"rfc3339", sizeof("rfc3339") - 1)) {
                datefmt = tplFmtRFC3339Date;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"unixtimestamp", sizeof("unixtimestamp") - 1)) {
                datefmt = tplFmtUnixDate;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"subseconds", sizeof("subseconds") - 1)) {
                datefmt = tplFmtSecFrac;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"wdayname", sizeof("wdayname") - 1)) {
                datefmt = tplFmtWDayName;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"wday", sizeof("wday") - 1)) {
                datefmt = tplFmtWDay;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"year", sizeof("year") - 1)) {
                datefmt = tplFmtYear;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"month", sizeof("month") - 1)) {
                datefmt = tplFmtMonth;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"day", sizeof("day") - 1)) {
                datefmt = tplFmtDay;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"hour", sizeof("hour") - 1)) {
                datefmt = tplFmtHour;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"minute", sizeof("minute") - 1)) {
                datefmt = tplFmtMinute;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"second", sizeof("second") - 1)) {
                datefmt = tplFmtSecond;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"tzoffshour", sizeof("tzoffshour") - 1)) {
                datefmt = tplFmtTZOffsHour;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"tzoffsmin", sizeof("tzoffsmin") - 1)) {
                datefmt = tplFmtTZOffsMin;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"tzoffsdirection", sizeof("tzoffsdirection") - 1)) {
                datefmt = tplFmtTZOffsDirection;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"ordinal", sizeof("ordinal") - 1)) {
                datefmt = tplFmtOrdinal;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"week", sizeof("week") - 1)) {
                datefmt = tplFmtWeek;
            } else {
                uchar *typeStr = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL);
                LogError(0, RS_RET_ERR, "invalid date format '%s' for property", typeStr);
                free(typeStr);
                ABORT_FINALIZE(RS_RET_ERR);
            }
        } else {
            dbgprintf(
                "template:propertyTpe: program error, non-handled "
                "param '%s'\n",
                pblkProperty.descr[i].name);
        }
    }
    if (name == NULL) {
        CHKmalloc(name = (uchar *)strdup(""));
    }
    if (outname == NULL) {
        /* we need to drop "$!" prefix, if present */
        if (ustrlen(name) >= 2 && !strncmp((char *)name, "$!", 2))
            outname = ustrdup(name + 2);
        else
            outname = ustrdup(name);
    }

    /* sanity check */
    if (topos_set == 0 && frompos != -1) topos = 2000000000; /* large enough ;) */
    if (frompos == -1 && topos_set != 0) frompos = 0;
    if (bPosRelativeToEnd) {
        if (topos > frompos) {
            LogError(0, RS_RET_ERR,
                     "position.to=%d is higher than postion.from=%d "
                     "in 'relativeToEnd' mode\n",
                     topos, frompos);
            ABORT_FINALIZE(RS_RET_ERR);
        }
    } else {
        if ((topos >= 0) && (topos < frompos)) {
            LogError(0, RS_RET_ERR, "position.to=%d is lower than postion.from=%d\n", topos, frompos);
            ABORT_FINALIZE(RS_RET_ERR);
        }
    }
    if (fieldnum != -1 && re_expr != NULL) {
        LogError(0, RS_RET_ERR,
                 "both field extraction and regex extraction "
                 "specified - this is not possible, remove one");
        ABORT_FINALIZE(RS_RET_ERR);
    }

    /* apply */
    CHKmalloc(pTpe = tpeConstruct(pTpl));
    pTpe->eEntryType = FIELD;
    CHKiRet(msgPropDescrFill(&pTpe->data.field.msgProp, name, strlen((char *)name)));
    pTpe->data.field.options.bDropLastLF = droplastlf;
    pTpe->data.field.options.bSPIffNo1stSP = spifno1stsp;
    pTpe->data.field.options.bMandatory = mandatory;
    pTpe->data.field.options.bFixedWidth = fixedwidth;
    pTpe->data.field.options.dataType = dataType;
    pTpe->data.field.options.onEmpty = onEmpty;
    pTpe->data.field.eCaseConv = caseconv;
    switch (formatType) {
        case F_NONE:
            /* all set ;) */
            break;
        case F_CSV:
            pTpe->data.field.options.bCSV = 1;
            break;
        case F_JSON:
            pTpe->data.field.options.bJSON = 1;
            break;
        case F_JSONF:
            pTpe->data.field.options.bJSONf = 1;
            break;
        case F_JSONR:
            pTpe->data.field.options.bJSONr = 1;
            break;
        case F_JSONFR:
            pTpe->data.field.options.bJSONfr = 1;
            break;
        default:
            // No action needed for other cases
            break;
    }
    switch (controlchr) {
        case CC_NONE:
            /* all set ;) */
            break;
        case CC_ESCAPE:
            pTpe->data.field.options.bEscapeCC = 1;
            break;
        case CC_SPACE:
            pTpe->data.field.options.bSpaceCC = 1;
            break;
        case CC_DROP:
            pTpe->data.field.options.bDropCC = 1;
            break;
        default:
            // No action needed for other cases
            break;
    }
    switch (secpath) {
        case SP_NONE:
            /* all set ;) */
            break;
        case SP_DROP:
            pTpe->data.field.options.bSecPathDrop = 1;
            break;
        case SP_REPLACE:
            pTpe->data.field.options.bSecPathReplace = 1;
            break;
        default:
            // No action needed for other cases
            break;
    }
    pTpe->fieldName = outname;
    if (outname != NULL) pTpe->lenFieldName = ustrlen(outname);
    outname = NULL;
    pTpe->bComplexProcessing = bComplexProcessing;
    pTpe->data.field.eDateFormat = datefmt;
    pTpe->data.field.options.bDateInUTC = bDateInUTC;
    pTpe->data.field.options.bCompressSP = bCompressSP;
    if (fieldnum != -1) {
        pTpe->data.field.has_fields = 1;
        pTpe->data.field.iFieldNr = fieldnum;
        pTpe->data.field.field_delim = fielddelim;
    }
    if (frompos != -1) {
        pTpe->data.field.iFromPos = frompos;
        pTpe->data.field.iToPos = topos;
        pTpe->data.field.options.bFromPosEndRelative = bPosRelativeToEnd;
    }
    if (re_expr != NULL) {
        rsRetVal iRetLocal;
        pTpe->data.field.typeRegex = re_type;
        pTpe->data.field.nomatchAction = re_nomatchType;
        pTpe->data.field.iMatchToUse = re_matchToUse;
        pTpe->data.field.iSubMatchToUse = re_submatchToUse;
        pTpe->data.field.has_regex = 1;
        if ((iRetLocal = objUse(regexp, LM_REGEXP_FILENAME)) == RS_RET_OK) {
            int iOptions;
            iOptions = (pTpe->data.field.typeRegex == TPL_REGEX_ERE) ? REG_EXTENDED : 0;
            if (regexp.regcomp(&(pTpe->data.field.re), (char *)re_expr, iOptions) != 0) {
                LogError(0, NO_ERRCODE, "error compiling regex '%s'", re_expr);
                pTpe->data.field.has_regex = 2;
                ABORT_FINALIZE(RS_RET_ERR);
            }
        } else {
            /* regexp object could not be loaded */
            if (bFirstRegexpErrmsg) { /* prevent flood of messages, maybe even an endless loop! */
                bFirstRegexpErrmsg = 0;
                LogError(0, NO_ERRCODE,
                         "regexp library could not be loaded (error %d), "
                         "regexp ignored",
                         iRetLocal);
            }
            pTpe->data.field.has_regex = 2;
            ABORT_FINALIZE(RS_RET_ERR);
        }
    }

finalize_it:
    if (pvals != NULL) cnfparamvalsDestruct(pvals, &pblkProperty);
    free(name);
    free(outname);
    RETiRet;
}

/* create a template in list mode, is build from sub-objects */
static rsRetVal createListTpl(struct template *pTpl, struct cnfobj *o) {
    struct objlst *lst;
    DEFiRet;

    dbgprintf("create template from subobjs\n");
    objlstPrint(o->subobjs);

    for (lst = o->subobjs; lst != NULL; lst = lst->next) {
        switch (lst->obj->objType) {
            case CNFOBJ_PROPERTY:
                CHKiRet(createPropertyTpe(pTpl, lst->obj));
                break;
            case CNFOBJ_CONSTANT:
                CHKiRet(createConstantTpe(pTpl, lst->obj));
                break;
            default:
                dbgprintf(
                    "program error: invalid object type %d "
                    "in createLstTpl\n",
                    lst->obj->objType);
                break;
        }
        nvlstChkUnused(lst->obj->nvlst);
    }
finalize_it:
    RETiRet;
}

/* Add a new template via the v6 config system.  */
rsRetVal ATTR_NONNULL() tplProcessCnf(struct cnfobj *o) {
    struct template *pTpl = NULL;
    struct cnfparamvals *pvals = NULL;
    int lenName = 0; /* init just to keep compiler happy: mandatory parameter */
    char *name = NULL;
    uchar *tplStr = NULL;
    uchar *plugin = NULL;
    uchar *p;
    msgPropDescr_t subtree;
    sbool bHaveSubtree = 0;
    enum {
        T_STRING,
        T_PLUGIN,
        T_LIST,
        T_SUBTREE
    } tplType = T_STRING; /* init just to keep compiler happy: mandatory parameter */
    int i;
    int o_sql = 0, o_stdsql = 0, o_jsonf = 0, o_jsonftree = 0, o_json = 0, o_casesensitive = 0; /* options */
    int numopts;
    rsRetVal localRet;
    DEFiRet;

    pvals = nvlstGetParams(o->nvlst, &pblk, NULL);
    if (pvals == NULL) {
        parser_errmsg("error processing template config parameters");
        ABORT_FINALIZE(RS_RET_MISSING_CNFPARAMS);
    }
    cnfparamsPrint(&pblk, pvals);

    for (i = 0; i < pblk.nParams; ++i) {
        if (!pvals[i].bUsed) continue;
        if (!strcmp(pblk.descr[i].name, "name")) {
            lenName = es_strlen(pvals[i].val.d.estr);
            name = es_str2cstr(pvals[i].val.d.estr, NULL);
        } else if (!strcmp(pblk.descr[i].name, "type")) {
            if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"string", sizeof("string") - 1)) {
                tplType = T_STRING;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"plugin", sizeof("plugin") - 1)) {
                tplType = T_PLUGIN;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"list", sizeof("list") - 1)) {
                tplType = T_LIST;
            } else if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"subtree", sizeof("subtree") - 1)) {
                tplType = T_SUBTREE;
            } else {
                uchar *typeStr = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL);
                LogError(0, RS_RET_ERR, "invalid template type '%s'", typeStr);
                free(typeStr);
                ABORT_FINALIZE(RS_RET_ERR);
            }
        } else if (!strcmp(pblk.descr[i].name, "string")) {
            tplStr = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL);
        } else if (!strcmp(pblk.descr[i].name, "subtree")) {
            uchar *st_str = es_getBufAddr(pvals[i].val.d.estr);
            if (st_str[0] != '$' || st_str[1] != '!') {
                char *cstr = es_str2cstr(pvals[i].val.d.estr, NULL);
                LogError(0, RS_RET_ERR,
                         "invalid subtree "
                         "parameter, variable must start with '$!' but "
                         "var name is '%s'",
                         cstr);
                free(cstr);
                free(name); /* overall assigned */
                ABORT_FINALIZE(RS_RET_ERR);
            } else {
                uchar *cstr;
                cstr = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL);
                CHKiRet(msgPropDescrFill(&subtree, cstr, ustrlen(cstr)));
                free(cstr);
                bHaveSubtree = 1;
            }
        } else if (!strcmp(pblk.descr[i].name, "plugin")) {
            plugin = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL);
        } else if (!strcmp(pblk.descr[i].name, "option.stdsql")) {
            o_stdsql = pvals[i].val.d.n;
        } else if (!strcmp(pblk.descr[i].name, "option.sql")) {
            o_sql = pvals[i].val.d.n;
        } else if (!strcmp(pblk.descr[i].name, "option.json")) {
            o_json = pvals[i].val.d.n;
        } else if (!strcmp(pblk.descr[i].name, "option.jsonf")) {
            o_jsonf = pvals[i].val.d.n;
        } else if (!strcmp(pblk.descr[i].name, "option.jsonftree")) {
            o_jsonftree = pvals[i].val.d.n;
        } else if (!strcmp(pblk.descr[i].name, "option.casesensitive")) {
            o_casesensitive = pvals[i].val.d.n;
        } else {
            dbgprintf(
                "template: program error, non-handled "
                "param '%s'\n",
                pblk.descr[i].name);
        }
    }

    /* the following check is just for clang static anaylzer: this condition
     * cannot occur if all is setup well, because "name" is a required parameter
     * inside the param block and so the code should err out above.
     */
    if (name == NULL) {
        DBGPRINTF("template/tplProcessConf: logic error name == NULL - pblk wrong?\n");
        ABORT_FINALIZE(RS_RET_ERR);
    }

    /* do config sanity checks */
    if (tplStr == NULL) {
        if (tplType == T_STRING) {
            LogError(0, RS_RET_ERR,
                     "template '%s' of type string needs "
                     "string parameter",
                     name);
            ABORT_FINALIZE(RS_RET_ERR);
        }
    } else {
        if (tplType != T_STRING) {
            LogError(0, RS_RET_ERR,
                     "template '%s' is not a string "
                     "template but has a string specified - ignored",
                     name);
        }
    }

    if (plugin == NULL) {
        if (tplType == T_PLUGIN) {
            LogError(0, RS_RET_ERR,
                     "template '%s' of type plugin needs "
                     "plugin parameter",
                     name);
            ABORT_FINALIZE(RS_RET_ERR);
        }
    } else {
        if (tplType != T_PLUGIN) {
            LogError(0, RS_RET_ERR,
                     "template '%s' is not a plugin "
                     "template but has a plugin specified - ignored",
                     name);
        }
    }

    if (!bHaveSubtree) {
        if (tplType == T_SUBTREE) {
            LogError(0, RS_RET_ERR,
                     "template '%s' of type subtree needs "
                     "subtree parameter",
                     name);
            ABORT_FINALIZE(RS_RET_ERR);
        }
    } else {
        if (tplType != T_SUBTREE) {
            LogError(0, RS_RET_ERR,
                     "template '%s' is not a subtree "
                     "template but has a subtree specified - ignored",
                     name);
        }
    }

    if (o->subobjs == NULL) {
        if (tplType == T_LIST) {
            LogError(0, RS_RET_ERR,
                     "template '%s' of type list has "
                     "no parameters specified",
                     name);
            ABORT_FINALIZE(RS_RET_ERR);
        }
    } else {
        if (tplType != T_LIST) {
            LogError(0, RS_RET_ERR,
                     "template '%s' is not a list "
                     "template but has parameters specified - ignored",
                     name);
        }
    }

    numopts = 0;
    if (o_sql) ++numopts;
    if (o_stdsql) ++numopts;
    if (o_json) ++numopts;
    if (o_jsonf) ++numopts;
    if (o_jsonftree) ++numopts;
    if (numopts > 1) {
        LogError(0, RS_RET_ERR,
                 "template '%s' has multiple incompatible "
                 "options of sql, stdsql, json, jsonf or jsonftree specified",
                 name);
        ABORT_FINALIZE(RS_RET_ERR);
    }

    /* config ok */
    if ((pTpl = tplConstruct(loadConf)) == NULL) {
        DBGPRINTF("template.c: tplConstruct failed!\n");
        ABORT_FINALIZE(RS_RET_ERR);
    }
    pTpl->pszName = name;
    pTpl->iLenName = lenName;

    switch (tplType) {
        case T_STRING:
            p = tplStr;
            while (*p) {
                switch (*p) {
                    case '%': /* parameter */
                        ++p; /* eat '%' */
                        CHKiRet(do_Parameter(&p, pTpl));
                        break;
                    default: /* constant */
                        do_Constant(&p, pTpl, 0);
                        break;
                }
            }
            break;
        case T_PLUGIN:
            p = plugin;
            /* TODO: the use of tplAddTplMod() can be improved! */
            localRet = tplAddTplMod(pTpl, &p);
            if (localRet != RS_RET_OK) {
                LogError(0, localRet,
                         "template '%s': error %d "
                         "defining template via plugin (strgen) module",
                         pTpl->pszName, localRet);
                ABORT_FINALIZE(localRet);
            }
            break;
        case T_LIST:
            createListTpl(pTpl, o);
            break;
        case T_SUBTREE:
            memcpy(&pTpl->subtree, &subtree, sizeof(msgPropDescr_t));
            pTpl->bHaveSubtree = 1;
            break;
        default:
            // No action needed for other cases
            break;
    }

    pTpl->optFormatEscape = NO_ESCAPE;
    if (o_stdsql)
        pTpl->optFormatEscape = STDSQL_ESCAPE;
    else if (o_sql)
        pTpl->optFormatEscape = SQL_ESCAPE;
    else if (o_json)
        pTpl->optFormatEscape = JSON_ESCAPE;
    else if (o_jsonf || o_jsonftree)
        pTpl->optFormatEscape = JSONF;

    if (o_jsonftree) pTpl->bJsonTreeEnabled = 1;

    if (o_casesensitive) pTpl->optCaseSensitive = 1;
    apply_case_sensitivity(pTpl);
    if (pTpl->optFormatEscape == JSONF) tplWarnDuplicateJsonKeys(pTpl);
finalize_it:
    free(tplStr);
    free(plugin);
    if (pvals != NULL) cnfparamvalsDestruct(pvals, &pblk);
    if (iRet != RS_RET_OK) {
        if (pTpl != NULL) {
            /* we simply make the template defunct in this case by setting
             * its name to a zero-string. We do not free it, as this would
             * require additional code and causes only a very small memory
             * consumption. TODO: maybe in next iteration...
             */
            *pTpl->pszName = '\0';
        }
    }

    RETiRet;
}


/* Find a template object based on name. Search
 * currently is case-sensitive (should we change?).
 * returns pointer to template object if found and
 * NULL otherwise.
 * rgerhards 2004-11-17
 */
struct template *tplFind(rsconf_t *conf, char *pName, int iLenName) {
    struct template *pTpl;

    assert(pName != NULL);

    pTpl = conf->templates.root;
    while (pTpl != NULL && !(pTpl->iLenName == iLenName && !strcmp(pTpl->pszName, pName))) {
        pTpl = pTpl->pNext;
    }
    return (pTpl);
}

/* Destroy the template structure. This is for de-initialization
 * at program end. Everything is deleted.
 * rgerhards 2005-02-22
 * I have commented out dbgprintfs, because they are not needed for
 * "normal" debugging. Uncomment them, if they are needed.
 * rgerhards, 2007-07-05
 */
void tplDeleteAll(rsconf_t *conf) {
    struct template *pTpl, *pTplDel;
    struct templateEntry *pTpe, *pTpeDel;

    pTpl = conf->templates.root;
    while (pTpl != NULL) {
        pTpe = pTpl->pEntryRoot;
        while (pTpe != NULL) {
            pTpeDel = pTpe;
            pTpe = pTpe->pNext;
            switch (pTpeDel->eEntryType) {
                case UNDEFINED:
                    break;
                case CONSTANT:
                    free(pTpeDel->data.constant.pConstant);
                    break;
                case FIELD:
                    /* check if we have a regexp and, if so, delete it */
#ifdef FEATURE_REGEXP
                    if (pTpeDel->data.field.has_regex != 0) {
                        if (objUse(regexp, LM_REGEXP_FILENAME) == RS_RET_OK) {
                            regexp.regfree(&(pTpeDel->data.field.re));
                        }
                    }
#endif
                    msgPropDescrDestruct(&pTpeDel->data.field.msgProp);
                    break;
                default:
                    // No action needed for other cases
                    break;
            }
            free(pTpeDel->fieldName);
            free(pTpeDel);
        }
        pTplDel = pTpl;
        pTpl = pTpl->pNext;
        free(pTplDel->pszName);
        if (pTplDel->bHaveSubtree) msgPropDescrDestruct(&pTplDel->subtree);
        tplJsonNodeFree(pTplDel->pJsonRoot);
        free(pTplDel);
    }
}


/* Destroy all templates obtained from conf file
 * preserving hardcoded ones. This is called from init().
 */
void tplDeleteNew(rsconf_t *conf) {
    struct template *pTpl, *pTplDel;
    struct templateEntry *pTpe, *pTpeDel;


    if (conf->templates.root == NULL || conf->templates.lastStatic == NULL) return;

    pTpl = conf->templates.lastStatic->pNext;
    conf->templates.lastStatic->pNext = NULL;
    conf->templates.last = conf->templates.lastStatic;
    while (pTpl != NULL) {
        pTpe = pTpl->pEntryRoot;
        while (pTpe != NULL) {
            pTpeDel = pTpe;
            pTpe = pTpe->pNext;
            switch (pTpeDel->eEntryType) {
                case UNDEFINED:
                    break;
                case CONSTANT:
                    free(pTpeDel->data.constant.pConstant);
                    break;
                case FIELD:
#ifdef FEATURE_REGEXP
                    /* check if we have a regexp and, if so, delete it */
                    if (pTpeDel->data.field.has_regex != 0) {
                        if (objUse(regexp, LM_REGEXP_FILENAME) == RS_RET_OK) {
                            regexp.regfree(&(pTpeDel->data.field.re));
                        }
                    }
#endif
                    msgPropDescrDestruct(&pTpeDel->data.field.msgProp);
                    break;
                default:
                    // No action needed for other cases
                    break;
            }
            free(pTpeDel);
        }
        pTplDel = pTpl;
        pTpl = pTpl->pNext;
        free(pTplDel->pszName);
        if (pTplDel->bHaveSubtree) msgPropDescrDestruct(&pTplDel->subtree);
        tplJsonNodeFree(pTplDel->pJsonRoot);
        free(pTplDel);
    }
}

/* Store the pointer to the last hardcoded template */
void tplLastStaticInit(rsconf_t *conf, struct template *tpl) {
    conf->templates.lastStatic = tpl;
}

/* Print the template structure. This is more or less a
 * debug or test aid, but anyhow I think it's worth it...
 */
void tplPrintList(rsconf_t *conf) {
    struct template *pTpl;
    struct templateEntry *pTpe;

    pTpl = conf->templates.root;
    while (pTpl != NULL) {
        dbgprintf("Template: Name='%s' ", pTpl->pszName == NULL ? "NULL" : pTpl->pszName);
        if (pTpl->optFormatEscape == SQL_ESCAPE)
            dbgprintf("[SQL-Format (MySQL)] ");
        else if (pTpl->optFormatEscape == JSON_ESCAPE)
            dbgprintf("[JSON-Escaped Format] ");
        else if (pTpl->optFormatEscape == STDSQL_ESCAPE)
            dbgprintf("[SQL-Format (standard SQL)] ");
        else if (pTpl->optFormatEscape == JSONF)
            dbgprintf("[JSON fields] ");
        if (pTpl->optCaseSensitive) dbgprintf("[Case Sensitive Vars] ");
        dbgprintf("\n");
        pTpe = pTpl->pEntryRoot;
        while (pTpe != NULL) {
            dbgprintf("\tEntry(%lx): type %d, ", (unsigned long)pTpe, pTpe->eEntryType);
            switch (pTpe->eEntryType) {
                case UNDEFINED:
                    dbgprintf("(UNDEFINED)");
                    break;
                case CONSTANT:
                    dbgprintf("(CONSTANT), value: '%s'", pTpe->data.constant.pConstant);
                    break;
                case FIELD:
                    dbgprintf("(FIELD), value: '%d' ", pTpe->data.field.msgProp.id);
                    if (pTpe->data.field.msgProp.id == PROP_CEE) {
                        dbgprintf("[EE-Property: '%s'] ", pTpe->data.field.msgProp.name);
                    } else if (pTpe->data.field.msgProp.id == PROP_LOCAL_VAR) {
                        dbgprintf("[Local Var: '%s'] ", pTpe->data.field.msgProp.name);
                    }
                    switch (pTpe->data.field.eDateFormat) {
                        case tplFmtDefault:
                            break;
                        case tplFmtMySQLDate:
                            dbgprintf("[Format as MySQL-Date] ");
                            break;
                        case tplFmtPgSQLDate:
                            dbgprintf("[Format as PgSQL-Date] ");
                            break;
                        case tplFmtRFC3164Date:
                            dbgprintf("[Format as RFC3164-Date] ");
                            break;
                        case tplFmtRFC3339Date:
                            dbgprintf("[Format as RFC3339-Date] ");
                            break;
                        case tplFmtUnixDate:
                            dbgprintf("[Format as Unix timestamp] ");
                            break;
                        case tplFmtSecFrac:
                            dbgprintf("[fractional seconds, only] ");
                            break;
                        case tplFmtRFC3164BuggyDate:
                            dbgprintf("[Format as buggy RFC3164-Date] ");
                            break;
                        case tplFmtWDayName:
                            dbgprintf("[Format as weekday name] ");
                            break;
                        case tplFmtYear:
                            dbgprintf("[Format as year] ");
                            break;
                        case tplFmtMonth:
                            dbgprintf("[Format as month] ");
                            break;
                        case tplFmtDay:
                            dbgprintf("[Format as day] ");
                            break;
                        case tplFmtHour:
                            dbgprintf("[Format as hour] ");
                            break;
                        case tplFmtMinute:
                            dbgprintf("[Format as minute] ");
                            break;
                        case tplFmtSecond:
                            dbgprintf("[Format as second] ");
                            break;
                        case tplFmtTZOffsHour:
                            dbgprintf("[Format as offset hour] ");
                            break;
                        case tplFmtTZOffsMin:
                            dbgprintf("[Format as offset minute] ");
                            break;
                        case tplFmtTZOffsDirection:
                            dbgprintf("[Format as offset direction] ");
                            break;
                        case tplFmtWDay:
                            dbgprintf("[Format as weekday] ");
                            break;
                        case tplFmtOrdinal:
                            dbgprintf("[Format as ordinal] ");
                            break;
                        case tplFmtWeek:
                            dbgprintf("[Format as week] ");
                            break;
                        default:
                            dbgprintf("[UNKNOWN eDateFormat %d] ", pTpe->data.field.eDateFormat);
                    }
                    switch (pTpe->data.field.eCaseConv) {
                        case tplCaseConvNo:
                            break;
                        case tplCaseConvLower:
                            dbgprintf("[Converted to Lower Case] ");
                            break;
                        case tplCaseConvUpper:
                            dbgprintf("[Converted to Upper Case] ");
                            break;
                        default:
                            // No action needed for other cases
                            break;
                    }
                    if (pTpe->data.field.options.bEscapeCC) {
                        dbgprintf("[escape control-characters] ");
                    }
                    if (pTpe->data.field.options.bDropCC) {
                        dbgprintf("[drop control-characters] ");
                    }
                    if (pTpe->data.field.options.bSpaceCC) {
                        dbgprintf("[replace control-characters with space] ");
                    }
                    if (pTpe->data.field.options.bSecPathDrop) {
                        dbgprintf("[slashes are dropped] ");
                    }
                    if (pTpe->data.field.options.bSecPathReplace) {
                        dbgprintf("[slashes are replaced by '_'] ");
                    }
                    if (pTpe->data.field.options.bSPIffNo1stSP) {
                        dbgprintf("[SP iff no first SP] ");
                    }
                    if (pTpe->data.field.options.bCSV) {
                        dbgprintf("[format as CSV (RFC4180)]");
                    }
                    if (pTpe->data.field.options.bJSON) {
                        dbgprintf("[format as JSON] ");
                    }
                    if (pTpe->data.field.options.bJSONf) {
                        dbgprintf("[format as JSON field] ");
                    }
                    if (pTpe->data.field.options.bJSONr) {
                        dbgprintf("[format as JSON without re-escaping] ");
                    }
                    if (pTpe->data.field.options.bJSONfr) {
                        dbgprintf("[format as JSON field without re-escaping] ");
                    }
                    if (pTpe->data.field.options.bMandatory) {
                        dbgprintf("[mandatory field] ");
                    }
                    if (pTpe->data.field.options.bDropLastLF) {
                        dbgprintf("[drop last LF in msg] ");
                    }
                    if (pTpe->data.field.has_fields == 1) {
                        dbgprintf("[substring, field #%d only (delimiter %d)] ", pTpe->data.field.iFieldNr,
                                  pTpe->data.field.field_delim);
                    }
                    if (pTpe->data.field.iFromPos != 0 || pTpe->data.field.iToPos != 0) {
                        dbgprintf("[substring, from character %d to %d] ", pTpe->data.field.iFromPos,
                                  pTpe->data.field.iToPos);
                    }
                    break;
                default:
                    // No action needed for other cases
                    break;
            }
            if (pTpe->bComplexProcessing) dbgprintf("[COMPLEX]");
            dbgprintf("\n");
            pTpe = pTpe->pNext;
        }
        pTpl = pTpl->pNext; /* done, go next */
    }
}

int tplGetEntryCount(struct template *pTpl) {
    assert(pTpl != NULL);
    return (pTpl->tpenElements);
}

rsRetVal templateInit(void) {
    DEFiRet;
    CHKiRet(objGetObjInterface(&obj));
    CHKiRet(objUse(strgen, CORE_COMPONENT));

finalize_it:
    RETiRet;
}
