/* omfile.c
 * This is the implementation of the build-in file output module.
 *
 * NOTE: read comments in module-template.h to understand how this file
 * works!
 *
 * File begun on 2007-07-21 by RGerhards (extracted from syslogd.c, which
 * at the time of the fork from sysklogd was under BSD license)
 *
 * A large re-write of this file was done in June, 2009. The focus was
 * to introduce many more features (like zipped writing), clean up the code
 * and make it more reliable. In short, that rewrite tries to provide a new
 * solid basis for the next three to five years to come. During it, bugs
 * may have been introduced ;) -- rgerhards, 2009-06-04
 *
 * Note that as of 2010-02-28 this module does no longer handle
 * pipes. These have been moved to ompipe, to reduced the entanglement
 * between the two different functionalities. -- rgerhards
 *
 * Copyright 2007-2026 Adiscon GmbH.
 *
 * 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.
 */
#include "config.h"
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <assert.h>
#include <errno.h>
#include <ctype.h>
#include <libgen.h>
#include <unistd.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <fcntl.h>
#ifdef HAVE_ATOMIC_BUILTINS
    #include <pthread.h>
#endif

#include "rsyslog.h"
#include "conf.h"
#include "syslogd-types.h"
#include "srUtils.h"
#include "template.h"
#include "action.h"
#include "outchannel.h"
#include "omfile.h"
#include "cfsysline.h"
#include "module-template.h"
#include "errmsg.h"
#include "stream.h"
#include "unicode-helper.h"
#include "atomic.h"
#include "statsobj.h"
#include "sigprov.h"
#include "cryprov.h"
#include "parserif.h"
#include "janitor.h"
#include "rsconf.h"

MODULE_TYPE_OUTPUT;
MODULE_TYPE_NOKEEP;
MODULE_CNFNAME("omfile")

/* forward definitions */
static rsRetVal resetConfigVariables(uchar __attribute__((unused)) * pp, void __attribute__((unused)) * pVal);
static rsRetVal normalizeDynaFileCacheSize(int *const pNewVal);

typedef struct pathComponent_s {
    const char *ptr;
    size_t len;
} pathComponent_t;

/* internal structures
 */
DEF_OMOD_STATIC_DATA;
DEFobjCurrIf(strm) DEFobjCurrIf(statsobj)

/**
 * @brief Global counter/clock for file access timestamping.
 *
 * This counter is used as a "Lamport logical clock" for the LRU mechanism
 * of the dynamic file cache. It provides a monotonically increasing sequence
 * number to track the access order of files, rather than actual time.
 * It is highly unlikely to wrap within the lifetime of a process.
 */
#ifdef HAVE_ATOMIC_BUILTINS64
    static uint64 clockFileAccess = 0;
#else
    static unsigned clockFileAccess = 0;
#endif

/**
 * @brief Mutex for protecting clockFileAccess in non-atomic builtins environments.
 */
#ifndef HAVE_ATOMIC_BUILTINS
static pthread_mutex_t mutClock;
#endif

/**
 * @brief Retrieves and increments the global file access clock.
 *
 * This function provides a monotonically increasing counter used for the LRU
 * mechanism in the dynamic file cache. It is thread-safe.
 *
 * @return The incremented value of the file access clock.
 */
static uint64 getClockFileAccess(void) {
#ifdef HAVE_ATOMIC_BUILTINS64
    return ATOMIC_INC_AND_FETCH_uint64(&clockFileAccess, &mutClock);
#else
    return ATOMIC_INC_AND_FETCH_unsigned(&clockFileAccess, &mutClock);
#endif
}


/**
 * @brief Structure for a dynamic file name cache entry.
 *
 * This structure holds information about a dynamically opened file,
 * including its name, the associated stream, signature provider data,
 * and access timestamp for LRU management.
 */
struct s_dynaFileCacheEntry {
    uchar *pName; /**< name currently open, if dynamic name */
    strm_t *pStrm; /**< our output stream */
    void *sigprovFileData; /**< opaque data ptr for provider use */
    uint64 clkTickAccessed; /**< for LRU - based on clockFileAccess */
    short nInactive; /**< number of minutes not writen - for close timeout */
};
typedef struct s_dynaFileCacheEntry dynaFileCacheEntry;


#define IOBUF_DFLT_SIZE 4096 /**< default size for io buffers */
#define FLUSH_INTRVL_DFLT 1 /**< default buffer flush interval (in seconds) */
#define USE_ASYNCWRITER_DFLT 0 /**< default buffer use async writer */
#define FLUSHONTX_DFLT 1 /**< default for flush on TX end */
#define ADDLF_SIGBUF_STACKLEN 1024 /**< stack buffer size for addLF signing helper */

/**
 * @brief Instance data for the omfile module.
 *
 * This structure holds the configuration and runtime state for each
 * file output action.
 */
typedef struct _instanceData {
    pthread_mutex_t mutWrite; /**< guard against multiple instances writing to single file */
    uchar *fname; /**< file or template name (display only) */
    uchar *tplName; /**< name of assigned template */
    uchar *dynaFileBasePath; /**< normalized constant base path for dynafile templates */
    sbool bPermitDynaFilePathEscape; /**< permit dynafile paths outside the derived base */
    sbool bRestrictDynaFileTplType; /**< reject template types without an inspectable fixed prefix */
    strm_t *pStrm; /**< our output stream */
    short nInactive; /**< number of minutes not writen (STATIC files only) */
    char bDynamicName; /**< 0 - static name, 1 - dynamic name (with properties) */
    int isDevNull; /**< do we "write" to /dev/null? - if so, do nothing */
    int fCreateMode; /**< file creation mode for open() */
    int fDirCreateMode; /**< creation mode for mkdir() */
    int bCreateDirs; /**< auto-create directories? */
    int bSyncFile; /**< should the file by sync()'ed? 1- yes, 0- no */
    uint8_t iNumTpls; /**< number of tpls we use */
    uid_t fileUID; /**< IDs for creation */
    uid_t dirUID;
    gid_t fileGID;
    gid_t dirGID;
    int bFailOnChown; /**< fail creation if chown fails? */
    uchar *sigprovName; /**< signature provider */
    uchar *sigprovNameFull; /**< full internal signature provider name */
    sigprov_if_t sigprov; /**< ptr to signature provider interface */
    void *sigprovData; /**< opaque data ptr for provider use */
    void *sigprovFileData; /**< opaque data ptr for file instance */
    sbool useSigprov; /**< quicker than checkig ptr (1 vs 8 bytes!) */
    uchar *cryprovName; /**< crypto provider */
    uchar *cryprovNameFull; /**< full internal crypto provider name */
    void *cryprovData; /**< opaque data ptr for provider use */
    cryprov_if_t cryprov; /**< ptr to crypto provider interface */
    sbool useCryprov; /**< quicker than checkig ptr (1 vs 8 bytes!) */
    int iCurrElt; /**< currently active cache element (-1 = none) */
    int iCurrCacheSize; /**< currently cache size (1-based) */
    int iDynaFileCacheSize; /**< size of file handle cache */
    /**
     * The cache is implemented as an array. An empty element is indicated
     * by a NULL pointer. Memory is allocated as needed. The following
     * pointer points to the overall structure.
     */
    dynaFileCacheEntry **dynCache;
    off_t iSizeLimit; /**< file size limit, 0 = no limit */
    uchar *pszSizeLimitCmd; /**< command to carry out when size limit is reached */
    sbool bSizeLimitCmdPassFileName; /**< pass current file name to size limit command? */
    int iZipLevel; /**< zip mode to use for this selector */
    int iIOBufSize; /**< size of associated io buffer */
    int iFlushInterval; /**< how fast flush buffer on inactivity? */
    short iCloseTimeout; /**< after how many *minutes* shall the file be closed if inactive? */
    sbool bFlushOnTXEnd; /**< flush write buffers when transaction has ended? */
    sbool bUseAsyncWriter; /**< use async stream writer? */
    sbool bVeryRobustZip;
    sbool bAddLF; /**< append LF to records that are missing it? */
    int bFollowSymlinks; /**< allow final output path component to be a symlink? */
    sbool bFollowSymlinksExplicit; /**< was symlink policy configured explicitly? */
    statsobj_t *stats; /**< dynafile, primarily cache stats */
    STATSCOUNTER_DEF(ctrRequests, mutCtrRequests);
    STATSCOUNTER_DEF(ctrLevel0, mutCtrLevel0);
    STATSCOUNTER_DEF(ctrEvict, mutCtrEvict);
    STATSCOUNTER_DEF(ctrMiss, mutCtrMiss);
    STATSCOUNTER_DEF(ctrMax, mutCtrMax);
    STATSCOUNTER_DEF(ctrCloseTimeouts, mutCtrCloseTimeouts);
    char janitorID[128]; /**< holds ID for janitor calls */
} instanceData;

/**
 * @brief Worker instance data for the omfile module.
 *
 * This structure holds a pointer to the main instance data for use
 * by worker threads.
 */
typedef struct wrkrInstanceData {
    instanceData *pData;
} wrkrInstanceData_t;

static int pathIsMeaningfulBase(const uchar *const path) {
    return path != NULL && path[0] != '\0' && ustrcmp(path, (uchar *)".");
}

static int pathStartsWithParentRef(const uchar *const path) {
    return path[0] == '.' && path[1] == '.' && (path[2] == '\0' || path[2] == '/');
}

/**
 * @brief Normalize a path syntactically without touching the filesystem.
 *
 * omfile may create missing directories and files, so realpath(3) cannot be
 * used here. This helper removes duplicate separators and "." components, and
 * resolves standalone ".." path components lexically. Relative paths that still
 * escape above their starting point keep leading ".." components so callers can
 * reject them.
 *
 * @param path Path string to normalize.
 * @param ppNorm Receives a newly allocated normalized path on success.
 * @return RS_RET_OK on success, RS_RET_OUT_OF_MEMORY on allocation failure.
 */
static rsRetVal normalizePathLexically(const uchar *const path, uchar **const ppNorm) {
    pathComponent_t *components = NULL;
    uchar *norm = NULL;
    const char *pszPath;
    const char *p;
    const char *start;
    uchar *dst;
    size_t lenPath;
    size_t maxComponents;
    size_t nComponents = 0;
    size_t lenComponent;
    size_t lenNorm;
    size_t i;
    int isAbsolute;
    DEFiRet;

    assert(path != NULL);
    assert(ppNorm != NULL);

    pszPath = (const char *)path;
    lenPath = strlen(pszPath);
    maxComponents = lenPath / 2 + 2;
    isAbsolute = pszPath[0] == '/';

    CHKmalloc(components = calloc(maxComponents, sizeof(pathComponent_t)));

    p = pszPath;
    while (*p != '\0') {
        while (*p == '/') {
            ++p;
        }
        start = p;
        while (*p != '\0' && *p != '/') {
            ++p;
        }
        lenComponent = (size_t)(p - start);

        if (lenComponent == 0 || (lenComponent == 1 && start[0] == '.')) {
            continue;
        }
        if (lenComponent == 2 && start[0] == '.' && start[1] == '.') {
            if (nComponents > 0 &&
                !(components[nComponents - 1].len == 2 && components[nComponents - 1].ptr[0] == '.' &&
                  components[nComponents - 1].ptr[1] == '.')) {
                --nComponents;
            } else if (!isAbsolute) {
                components[nComponents].ptr = start;
                components[nComponents].len = lenComponent;
                ++nComponents;
            }
            continue;
        }

        components[nComponents].ptr = start;
        components[nComponents].len = lenComponent;
        ++nComponents;
    }

    if (nComponents == 0) {
        CHKmalloc(norm = (uchar *)strdup(isAbsolute ? "/" : "."));
        FINALIZE;
    }

    lenNorm = isAbsolute ? 1 : 0;
    for (i = 0; i < nComponents; ++i) {
        lenNorm += components[i].len;
        if (i > 0) {
            ++lenNorm;
        }
    }

    CHKmalloc(norm = malloc(lenNorm + 1));
    dst = norm;
    if (isAbsolute) {
        *dst++ = '/';
    }
    for (i = 0; i < nComponents; ++i) {
        if (i > 0) {
            *dst++ = '/';
        }
        memcpy(dst, components[i].ptr, components[i].len);
        dst += components[i].len;
    }
    *dst = '\0';

finalize_it:
    free(components);
    if (iRet == RS_RET_OK) {
        *ppNorm = norm;
    } else {
        free(norm);
    }
    RETiRet;
}

static int normalizedPathIsBelowBase(const uchar *const normPath, const uchar *const normBase) {
    const size_t lenBase = ustrlen(normBase);

    if (!ustrcmp(normBase, (uchar *)"/")) {
        return normPath[0] == '/';
    }
    if (!ustrcmp(normPath, normBase)) {
        return 1;
    }
    return !strncmp((const char *)normPath, (const char *)normBase, lenBase) && normPath[lenBase] == '/';
}

/**
 * @brief Validate a rendered dynamic file name before it can be opened.
 *
 * If the dynafile template had a meaningful static directory prefix, the
 * rendered path must normalize to that directory or a child of it. For templates
 * where no useful base can be derived, absolute paths and leading parent
 * traversal are rejected as a fallback. Installations that intentionally rely
 * on those legacy forms can opt in with dangerousPermitPathEscape.
 *
 * @param pData omfile action instance data.
 * @param newFileName Rendered dynafile path.
 * @return RS_RET_OK if the path is acceptable, RS_RET_DISCARDMSG if it is blocked.
 */
static rsRetVal validateDynaFilePath(instanceData *const pData, const uchar *const newFileName) {
    uchar *normPath = NULL;
    DEFiRet;

    assert(pData != NULL);
    assert(newFileName != NULL);

    CHKiRet(normalizePathLexically(newFileName, &normPath));
    if (pathIsMeaningfulBase(pData->dynaFileBasePath)) {
        if (!normalizedPathIsBelowBase(normPath, pData->dynaFileBasePath)) {
            LogError(0, RS_RET_ERR, "omfile: dynafile path traversal blocked: '%s' resolves outside base '%s'",
                     newFileName, pData->dynaFileBasePath);
            ABORT_FINALIZE(RS_RET_DISCARDMSG);
        }
    } else if (normPath[0] == '/' || pathStartsWithParentRef(normPath)) {
        LogError(0, RS_RET_ERR, "omfile: dynafile path traversal blocked: '%s'", newFileName);
        ABORT_FINALIZE(RS_RET_DISCARDMSG);
    }

finalize_it:
    free(normPath);
    RETiRet;
}

/**
 * @brief Derive the immutable base directory from a dynafile template.
 *
 * The trusted part of a dynafile template is the contiguous run of constant
 * template entries before the first property field. The last slash in that
 * trusted prefix defines the directory base that rendered file names may not
 * escape. For example, `/var/log/%HOSTNAME%/%APP-NAME%.log` derives `/var/log`.
 *
 * Templates backed by string generators or subtree output are intentionally not
 * inspected; in those cases runtime validation falls back to rejecting absolute
 * paths and leading parent traversal.
 *
 * @param pData omfile action instance data to receive the normalized base.
 * @param pTpl Resolved dynafile template.
 * @return RS_RET_OK on success, RS_RET_OUT_OF_MEMORY on allocation failure.
 */
static rsRetVal deriveDynaFileBasePath(instanceData *const pData, const struct template *const pTpl) {
    const struct templateEntry *entry;
    uchar *prefix = NULL;
    uchar *base = NULL;
    uchar *normBase = NULL;
    uchar *lastSlash;
    size_t prefixLen = 0;
    size_t baseLen;
    uchar *dst;
    DEFiRet;

    assert(pData != NULL);

    free(pData->dynaFileBasePath);
    pData->dynaFileBasePath = NULL;

    if (pTpl == NULL || pTpl->pStrgen != NULL || pTpl->bHaveSubtree) {
        FINALIZE;
    }

    /* Only constants before the first field are trusted as admin-authored path. */
    for (entry = pTpl->pEntryRoot; entry != NULL && entry->eEntryType == CONSTANT; entry = entry->pNext) {
        prefixLen += (size_t)entry->data.constant.iLenConstant;
    }
    if (prefixLen == 0) {
        FINALIZE;
    }

    CHKmalloc(prefix = malloc(prefixLen + 1));
    dst = prefix;
    for (entry = pTpl->pEntryRoot; entry != NULL && entry->eEntryType == CONSTANT; entry = entry->pNext) {
        memcpy(dst, entry->data.constant.pConstant, (size_t)entry->data.constant.iLenConstant);
        dst += entry->data.constant.iLenConstant;
    }
    *dst = '\0';

    /* The base is the deepest complete directory in the trusted prefix. */
    lastSlash = (uchar *)strrchr((const char *)prefix, '/');
    if (lastSlash == NULL) {
        FINALIZE;
    }

    baseLen = (size_t)(lastSlash - prefix);
    if (baseLen == 0) {
        baseLen = 1;
    }
    CHKmalloc(base = malloc(baseLen + 1));
    memcpy(base, prefix, baseLen);
    base[baseLen] = '\0';

    CHKiRet(normalizePathLexically(base, &normBase));
    if (pathIsMeaningfulBase(normBase)) {
        pData->dynaFileBasePath = normBase;
        normBase = NULL;
    }

finalize_it:
    free(prefix);
    free(base);
    free(normBase);
    RETiRet;
}

/**
 * @brief Reject dynafile templates whose fixed prefix cannot be inspected.
 *
 * Dynafile path hardening depends on inspecting the static template entries
 * before the first message-derived field. Rsyslog string and list templates
 * both use the template entry list and can be checked this way. Subtree and
 * plugin/string-generator templates bypass that representation, so omfile
 * cannot derive a trusted base directory from their configured form.
 *
 * @param pData omfile action instance data.
 * @param pTpl Resolved dynafile template.
 * @return RS_RET_OK if the template type is accepted, RS_RET_ERR otherwise.
 */
static rsRetVal checkDynaFileTemplateType(instanceData *const pData, const struct template *const pTpl) {
    DEFiRet;

    assert(pData != NULL);

    if (pData->bRestrictDynaFileTplType && (pTpl == NULL || pTpl->pStrgen != NULL || pTpl->bHaveSubtree)) {
        LogError(0, RS_RET_ERR,
                 "omfile: dynafile template '%s' uses a template type that cannot be "
                 "safely inspected for path traversal; use type=\"string\" or "
                 "type=\"list\", or set module parameter "
                 "dynafile.restrictTemplateType=\"off\" to use the legacy "
                 "template subject to the fallback path guards",
                 pData->fname);
        ABORT_FINALIZE(RS_RET_ERR);
    }

finalize_it:
    RETiRet;
}

static void warnDynaFilePathEscapeEnabled(const char *const scope) {
    parser_warnmsg(
        "omfile: %s dynafile.dangerousPermitPathEscape permits dynafile "
        "paths to escape the configured base directory. This is dangerous "
        "with untrusted message properties and can overwrite any file "
        "permitted by OS permissions.",
        scope);
}

/**
 * @brief Module-global configuration settings.
 *
 * This structure holds the configuration settings that apply globally
 * to the omfile module.
 */
typedef struct configSettings_s {
    int iDynaFileCacheSize; /**< max cache for dynamic files */
    int fCreateMode; /**< mode to use when creating files */
    int fDirCreateMode; /**< mode to use when creating files */
    int bFailOnChown; /**< fail if chown fails? */
    uid_t fileUID; /**< UID to be used for newly created files */
    uid_t fileGID; /**< GID to be used for newly created files */
    uid_t dirUID; /**< UID to be used for newly created directories */
    uid_t dirGID; /**< GID to be used for newly created directories */
    int bCreateDirs; /**< auto-create directories for dynaFiles: 0 - no, 1 - yes */
    int bEnableSync; /**< enable syncing of files (no dash in front of pathname in conf): 0 - no, 1 - yes */
    int iZipLevel; /**< zip compression mode (0..9 as usual) */
    sbool bFlushOnTXEnd; /**< flush write buffers when transaction has ended? */
    int64 iIOBufSize; /**< size of an io buffer */
    int iFlushInterval; /**< how often flush the output buffer on inactivity? */
    int bUseAsyncWriter; /**< should we enable asynchronous writing? */
    sbool bAddLF; /**< append LF to records that are missing it? */
    EMPTY_STRUCT
} configSettings_t;
static configSettings_t cs;
uchar *pszFileDfltTplName; /**< name of the default template to use */

/**
 * @brief Module configuration data for the v6 config system.
 */
struct modConfData_s {
    rsconf_t *pConf; /**< our overall config object */
    uchar *tplName; /**< default template */
    int fCreateMode; /**< default mode to use when creating files */
    int fDirCreateMode; /**< default mode to use when creating files */
    uid_t fileUID; /**< default IDs for creation */
    uid_t dirUID;
    gid_t fileGID;
    gid_t dirGID;
    int bDynafileDoNotSuspend;
    int bFollowSymlinks;
    sbool bFollowSymlinksExplicit;
    sbool bPermitDynaFilePathEscape;
    sbool bRestrictDynaFileTplType;
    strm_compressionDriver_t compressionDriver;
    int compressionDriver_workers;
    sbool bAddLF; /**< default setting for addLF action parameter */
};

static modConfData_t *loadModConf = NULL; /**< modConf ptr to use for the current load process */
static modConfData_t *runModConf = NULL; /**< modConf ptr to use for the current exec process */

/* tables for interfacing with the v6 config system */
/* module-global parameters */
static struct cnfparamdescr modpdescr[] = {
    {"template", eCmdHdlrGetWord, 0},
    {"addlf", eCmdHdlrBinary, 0},
    {"compression.driver", eCmdHdlrGetWord, 0},
    {"compression.zstd.workers", eCmdHdlrPositiveInt, 0},
    {"dircreatemode", eCmdHdlrFileCreateMode, 0},
    {"filecreatemode", eCmdHdlrFileCreateMode, 0},
    {"dirowner", eCmdHdlrUID, 0},
    {"dirownernum", eCmdHdlrInt, 0},
    {"dirgroup", eCmdHdlrGID, 0},
    {"dirgroupnum", eCmdHdlrInt, 0},
    {"fileowner", eCmdHdlrUID, 0},
    {"fileownernum", eCmdHdlrInt, 0},
    {"filegroup", eCmdHdlrGID, 0},
    {"dynafile.donotsuspend", eCmdHdlrBinary, 0},
    {"followsymlinks", eCmdHdlrBinary, 0},
    {"dynafile.dangerouspermitpathescape", eCmdHdlrBinary, 0},
    {"dynafile.restricttemplatetype", eCmdHdlrBinary, 0},
    {"filegroupnum", eCmdHdlrInt, 0},
};
static struct cnfparamblk modpblk = {CNFPARAMBLK_VERSION, sizeof(modpdescr) / sizeof(struct cnfparamdescr), modpdescr};

/* action (instance) parameters */
static struct cnfparamdescr actpdescr[] = {{"dynafilecachesize", eCmdHdlrInt, 0}, /* legacy: dynafilecachesize */
                                           {"ziplevel", eCmdHdlrInt, 0}, /* legacy: omfileziplevel */
                                           {"flushinterval", eCmdHdlrInt, 0}, /* legacy: omfileflushinterval */
                                           {"asyncwriting", eCmdHdlrBinary, 0}, /* legacy: omfileasyncwriting */
                                           {"veryrobustzip", eCmdHdlrBinary, 0},
                                           {"flushontxend", eCmdHdlrBinary, 0}, /* legacy: omfileflushontxend */
                                           {"iobuffersize", eCmdHdlrSize, 0}, /* legacy: omfileiobuffersize */
                                           {"dirowner", eCmdHdlrUID, 0}, /* legacy: dirowner */
                                           {"dirownernum", eCmdHdlrInt, 0}, /* legacy: dirownernum */
                                           {"dirgroup", eCmdHdlrGID, 0}, /* legacy: dirgroup */
                                           {"dirgroupnum", eCmdHdlrInt, 0}, /* legacy: dirgroupnum */
                                           {"fileowner", eCmdHdlrUID, 0}, /* legacy: fileowner */
                                           {"fileownernum", eCmdHdlrInt, 0}, /* legacy: fileownernum */
                                           {"filegroup", eCmdHdlrGID, 0}, /* legacy: filegroup */
                                           {"filegroupnum", eCmdHdlrInt, 0}, /* legacy: filegroupnum */
                                           {"dircreatemode", eCmdHdlrFileCreateMode, 0}, /* legacy: dircreatemode */
                                           {"filecreatemode", eCmdHdlrFileCreateMode, 0}, /* legacy: filecreatemode */
                                           {"failonchownfailure", eCmdHdlrBinary, 0}, /* legacy: failonchownfailure */
                                           {"createdirs", eCmdHdlrBinary, 0}, /* legacy: createdirs */
                                           {"sync", eCmdHdlrBinary, 0}, /* legacy: actionfileenablesync */
                                           {"file", eCmdHdlrString, 0}, /* either "file" or ... */
                                           {"dynafile", eCmdHdlrString, 0}, /* "dynafile" MUST be present */
                                           {"followsymlinks", eCmdHdlrBinary, 0},
                                           {"sig.provider", eCmdHdlrGetWord, 0},
                                           {"cry.provider", eCmdHdlrGetWord, 0},
                                           {"closetimeout", eCmdHdlrPositiveInt, 0},
                                           {"rotation.sizelimit", eCmdHdlrSize, 0},
                                           {"rotation.sizelimitcommand", eCmdHdlrString, 0},
                                           {"rotation.sizelimitcommandpassfilename", eCmdHdlrBinary, 0},
                                           {"template", eCmdHdlrGetWord, 0},
                                           {"dynafile.dangerouspermitpathescape", eCmdHdlrBinary, 0},
                                           {"addlf", eCmdHdlrBinary, 0}};
static struct cnfparamblk actpblk = {CNFPARAMBLK_VERSION, sizeof(actpdescr) / sizeof(struct cnfparamdescr), actpdescr};


/**
 * @brief Gets the default template name to be used.
 *
 * This function coordinates the default template name between old-style
 * and new-style configuration parts.
 *
 * @return A pointer to the default template name string.
 */
static uchar *getDfltTpl(void) {
    if (loadModConf != NULL && loadModConf->tplName != NULL)
        return loadModConf->tplName;
    else if (pszFileDfltTplName == NULL)
        return (uchar *)"RSYSLOG_FileFormat";
    else
        return pszFileDfltTplName;
}


BEGINinitConfVars /* (re)set config variables to default values */
    CODESTARTinitConfVars;
    pszFileDfltTplName = NULL; /* make sure this can be free'ed! */
    iRet = resetConfigVariables(NULL, NULL); /* params are dummies */
ENDinitConfVars

BEGINisCompatibleWithFeature
    CODESTARTisCompatibleWithFeature;
    if (eFeat == sFEATURERepeatedMsgReduction) iRet = RS_RET_OK;
ENDisCompatibleWithFeature


BEGINdbgPrintInstInfo
    CODESTARTdbgPrintInstInfo;
    if (pData->bDynamicName) {
        dbgprintf("[dynamic]\n");
    } else { /* regular file */
        dbgprintf("%s%s\n", pData->fname, (pData->pStrm == NULL) ? " (closed)" : "");
    }

    dbgprintf("\ttemplate='%s'\n", pData->fname);
    dbgprintf("\tuse async writer=%d\n", pData->bUseAsyncWriter);
    dbgprintf("\tflush on TX end=%d\n", pData->bFlushOnTXEnd);
    dbgprintf("\tflush interval=%d\n", pData->iFlushInterval);
    dbgprintf("\tappend LF=%d\n", pData->bAddLF);
    dbgprintf("\tfile cache size=%d\n", pData->iDynaFileCacheSize);
    dbgprintf("\tcreate directories: %s\n", pData->bCreateDirs ? "on" : "off");
    dbgprintf("\tvery robust zip: %s\n", pData->bCreateDirs ? "on" : "off");
    dbgprintf("\tfile owner %d, group %d\n", (int)pData->fileUID, (int)pData->fileGID);
    dbgprintf("\tdirectory owner %d, group %d\n", (int)pData->dirUID, (int)pData->dirGID);
    dbgprintf("\tdir create mode 0%3.3o, file create mode 0%3.3o\n", pData->fDirCreateMode, pData->fCreateMode);
    dbgprintf("\tfail if owner/group can not be set: %s\n", pData->bFailOnChown ? "yes" : "no");
ENDdbgPrintInstInfo


/**
 * @brief Sets the default template to be used for output files (legacy method).
 *
 * This is a module-global parameter and needs special handling to coordinate
 * with values set via the v2 config system (rsyslog v6+). This directive is
 * not permitted after the v2 config system has been used to set the parameter.
 *
 * @param pVal Unused pointer, kept for compatibility with command handler signature.
 * @param newVal The new default template name. Ownership is transferred to this function.
 * @return RS_RET_OK on success, RS_RET_ERR if already set by new config system.
 */
static rsRetVal setLegacyDfltTpl(void __attribute__((unused)) * pVal, uchar *newVal) {
    DEFiRet;

    if (loadModConf != NULL && loadModConf->tplName != NULL) {
        free(newVal);
        parser_errmsg(
            "omfile: default template already set via module "
            "global parameter - can no longer be changed");
        ABORT_FINALIZE(RS_RET_ERR);
    }
    free(pszFileDfltTplName);
    pszFileDfltTplName = newVal;
finalize_it:
    RETiRet;
}


/**
 * @brief Sets the dynamic file cache size.
 *
 * Performs limit checking to ensure the new value is valid and logs warnings
 * for unusually large values.
 *
 * @param pVal Unused pointer, kept for compatibility with command handler signature.
 * @param iNewVal The new size for the dynamic file cache.
 * @return RS_RET_OK on success, RS_RET_VAL_OUT_OF_RANGE if the value was invalid
 * (the value will be adjusted to a valid range in this case).
 */
static rsRetVal setDynaFileCacheSize(void __attribute__((unused)) * pVal, int iNewVal) {
    DEFiRet;
    iRet = normalizeDynaFileCacheSize(&iNewVal);

    cs.iDynaFileCacheSize = iNewVal;
    DBGPRINTF("DynaFileCacheSize changed to %d.\n", iNewVal);
    RETiRet;
}

static rsRetVal normalizeDynaFileCacheSize(int *const pNewVal) {
    DEFiRet;
    assert(pNewVal != NULL);

    if (*pNewVal < 1) {
        errno = 0;
        parser_errmsg("DynaFileCacheSize must be greater 0 (%d given), changed to 1.", *pNewVal);
        iRet = RS_RET_VAL_OUT_OF_RANGE;
        *pNewVal = 1;
    } else if (*pNewVal > 25000) {
        errno = 0;
        parser_warnmsg(
            "DynaFileCacheSize is larger than 25,000 (%d given) - this looks very "
            "large. Is it intended?",
            *pNewVal);
    }

    RETiRet;
}


/**
 * @brief Parses an output channel name and associates it with the action.
 *
 * This is a helper function to `cfline()`. It parses an output channel name
 * from the configuration line, looks up the channel, and extracts its
 * associated file template and size limit settings, storing them in the
 * provided `instanceData` structure.
 *
 * @param pData Pointer to the instance data structure to populate.
 * @param p Pointer to the current position in the configuration line.
 * @param pOMSR Pointer to the OMOD String Request structure.
 * @param iEntry Index for the OMOD String Request.
 * @param iTplOpts Template options.
 * @return RS_RET_OK on success, or an error code if the output channel
 * is not found or has no valid file name template.
 */
static rsRetVal cflineParseOutchannel(
    instanceData *pData, uchar *p, omodStringRequest_t *pOMSR, int iEntry, int iTplOpts) {
    DEFiRet;
    size_t i;
    struct outchannel *pOch;
    char szBuf[128]; /* should be more than sufficient */

    ++p; /* skip '$' */
    i = 0;
    /* get outchannel name */
    while (*p && *p != ';' && *p != ' ' && i < (sizeof(szBuf) - 1)) {
        szBuf[i++] = *p++;
    }
    szBuf[i] = '\0';

    /* got the name, now look up the channel... */
    pOch = ochFind(szBuf, i);

    if (pOch == NULL) {
        parser_errmsg("outchannel '%s' not found - ignoring action line", szBuf);
        ABORT_FINALIZE(RS_RET_NOT_FOUND);
    }

    /* check if there is a file name in the outchannel... */
    if (pOch->pszFileTemplate == NULL) {
        parser_errmsg("outchannel '%s' has no file name template - ignoring action line", szBuf);
        ABORT_FINALIZE(RS_RET_ERR);
    }

    /* OK, we finally got a correct template. So let's use it... */
    pData->fname = ustrdup(pOch->pszFileTemplate);
    pData->iSizeLimit = pOch->uSizeLimit;
    if (pOch->cmdOnSizeLimit != NULL) pData->pszSizeLimitCmd = ustrdup(pOch->cmdOnSizeLimit);
    /* legacy outchannel: preserve historic behavior (no filename argument) */
    pData->bSizeLimitCmdPassFileName = 0;

    iRet = cflineParseTemplateName(&p, pOMSR, iEntry, iTplOpts, getDfltTpl());

finalize_it:
    RETiRet;
}


/**
 * @brief Deletes an entry from the dynamic file name cache.
 *
 * This function closes the associated file stream, frees memory for the
 * file name, and optionally frees the cache entry structure itself.
 *
 * @param pData Pointer to the instance data containing the dynamic file cache.
 * @param iEntry The index of the entry to be deleted in the cache array.
 * @param bFreeEntry If 1, the cache entry structure itself is free()ed;
 * if 0, only its contents are freed (e.g., if it's being
 * reused for a new entry).
 * @return RS_RET_OK on success.
 */
static rsRetVal dynaFileDelCacheEntry(instanceData *__restrict__ const pData, const int iEntry, const int bFreeEntry) {
    dynaFileCacheEntry **pCache = pData->dynCache;
    DEFiRet;
    assert(pCache != NULL);

    if (pCache[iEntry] == NULL) FINALIZE;

    DBGPRINTF("Removing entry %d for file '%s' from dynaCache.\n", iEntry,
              pCache[iEntry]->pName == NULL ? UCHAR_CONSTANT("[OPEN FAILED]") : pCache[iEntry]->pName);

    if (pCache[iEntry]->pName != NULL) {
        free(pCache[iEntry]->pName);
        pCache[iEntry]->pName = NULL;
    }

    if (pCache[iEntry]->pStrm != NULL) {
        if (iEntry == pData->iCurrElt) {
            pData->iCurrElt = -1;
            pData->pStrm = NULL;
        }
        strm.Destruct(&pCache[iEntry]->pStrm);
        if (pData->useSigprov) {
            pData->sigprov.OnFileClose(pCache[iEntry]->sigprovFileData);
            pCache[iEntry]->sigprovFileData = NULL;
        }
    }

    if (bFreeEntry) {
        free(pCache[iEntry]);
        pCache[iEntry] = NULL;
    }

finalize_it:
    RETiRet;
}


/**
 * @brief Frees all dynamic file name cache entries and closes relevant files.
 *
 * This function is typically called during module shutdown or HUP processing
 * to release all resources held by the dynamic file cache.
 *
 * @param pData Pointer to the instance data containing the dynamic file cache.
 */
static void dynaFileFreeCacheEntries(instanceData *__restrict__ const pData) {
    register int i;
    assert(pData != NULL);

    for (i = 0; i < pData->iCurrCacheSize; ++i) {
        dynaFileDelCacheEntry(pData, i, 1);
    }
    /* invalidate current element */
    pData->iCurrElt = -1;
    pData->pStrm = NULL;
}


/**
 * @brief Frees the dynamic file name cache structure.
 *
 * This function first frees all entries within the cache and then
 * deallocates the cache array itself.
 *
 * @param pData Pointer to the instance data containing the dynamic file cache.
 */
static void dynaFileFreeCache(instanceData *__restrict__ const pData) {
    assert(pData != NULL);

    dynaFileFreeCacheEntries(pData);
    if (pData->dynCache != NULL) free(pData->dynCache);
}


/**
 * @brief Closes the currently open file stream for a given instance.
 *
 * If a signature provider is in use, its `OnFileClose` method is also called.
 *
 * @param pData Pointer to the instance data whose file stream is to be closed.
 * @return RS_RET_OK on success.
 */
static rsRetVal closeFile(instanceData *__restrict__ const pData) {
    DEFiRet;
    if (pData->useSigprov) {
        pData->sigprov.OnFileClose(pData->sigprovFileData);
        pData->sigprovFileData = NULL;
    }
    strm.Destruct(&pData->pStrm);
    RETiRet;
}


/**
 * @brief Prepares the signature provider for processing a file.
 *
 * This function calls the `OnFileOpen` method of the configured signature
 * provider, passing the file name and obtaining an opaque data pointer for
 * file-specific signature operations.
 *
 * @param pData Pointer to the instance data containing the signature provider.
 * @param fn The name of the file to be processed.
 * @return RS_RET_OK on success.
 */
static rsRetVal sigprovPrepare(instanceData *__restrict__ const pData, uchar *__restrict__ const fn) {
    DEFiRet;
    pData->sigprov.OnFileOpen(pData->sigprovData, fn, &pData->sigprovFileData);
    RETiRet;
}

static int getCompatDefaultFollowSymlinks(const rsconf_t *const cnf) {
    return cnf == NULL || cnf->globals.compatDefaultsSecure != COMPAT_DEFAULTS_SECURE_STRICT;
}

static void warnIfFollowingSymlink(const instanceData *__restrict__ const pData,
                                   const uchar *__restrict__ const fileName) {
    struct stat st;

    if (!pData->bFollowSymlinks || pData->bFollowSymlinksExplicit || runModConf == NULL || runModConf->pConf == NULL ||
        runModConf->pConf->globals.compatDefaultsSecure != COMPAT_DEFAULTS_SECURE_WARN) {
        return;
    }
    if (lstat((const char *)fileName, &st) == 0 && S_ISLNK(st.st_mode)) {
        LogMsg(0, RS_RET_OK, LOG_WARNING,
               "omfile: following symbolic link output file '%s'; "
               "set action(type=\"omfile\" followSymlinks=\"off\") or "
               "global(compatibility.defaults.secure=\"strict\") to reject final-component symlinks",
               fileName);
    }
}

/**
 * @brief Prepares file access for a given file name.
 *
 * This function handles the creation of parent directories (if configured),
 * creates the file itself (if it doesn't exist), sets its ownership and
 * permissions, and then constructs and finalizes the rsyslog stream object
 * for writing to the file. It also initializes signature and crypto providers
 * if enabled.
 *
 * @param pData Pointer to the instance data for the file output action.
 * @param newFileName The name of the file to prepare access for.
 * @return RS_RET_OK on success, or an error code if file creation,
 * directory creation, or stream construction fails.
 */
static rsRetVal prepareFile(instanceData *__restrict__ const pData,
                            const uchar *__restrict__ const newFileName,
                            const int bFailOnOpenErr) {
    int fd;
    char errStr[1024]; /* buffer for strerr() */
    DEFiRet;

    pData->pStrm = NULL;
    if (access((char *)newFileName, F_OK) != 0) {
        /* file does not exist, create it (and eventually parent directories */
        if (pData->bCreateDirs) {
            /* We first need to create parent dirs if they are missing.
             * We do not report any errors here ourselfs but let the code
             * fall through to error handler below.
             */
            if (makeFileParentDirs(newFileName, ustrlen(newFileName), pData->fDirCreateMode, pData->dirUID,
                                   pData->dirGID, pData->bFailOnChown) != 0) {
                rs_strerror_r(errno, errStr, sizeof(errStr));
                parser_errmsg(
                    "omfile: creating parent "
                    "directories for file  '%s' failed: %s",
                    newFileName, errStr);
                ABORT_FINALIZE(RS_RET_ERR); /* we give up */
            }
        }
        /* no matter if we needed to create directories or not, we now try to create
         * the file. -- rgerhards, 2008-12-18 (based on patch from William Tisater)
         */
        int openFlags = O_WRONLY | O_APPEND | O_CREAT | O_NOCTTY | O_CLOEXEC;
#ifdef O_NOFOLLOW
        if (!pData->bFollowSymlinks) {
            openFlags |= O_NOFOLLOW;
        }
#endif
        fd = open((char *)newFileName, openFlags, pData->fCreateMode);
        if (fd != -1) {
            /* check and set uid/gid */
            if (pData->fileUID != (uid_t)-1 || pData->fileGID != (gid_t)-1) {
                /* we need to set owner/group */
                if (fchown(fd, pData->fileUID, pData->fileGID) != 0) {
                    rs_strerror_r(errno, errStr, sizeof(errStr));
                    parser_errmsg("omfile: chown for file '%s' failed: %s", newFileName, errStr);
                    if (pData->bFailOnChown) {
                        close(fd);
                        ABORT_FINALIZE(RS_RET_ERR); /* we give up */
                    }
                    /* we will silently ignore the chown() failure
                     * if configured to do so.
                     */
                }
            }
            close(fd); /* close again, as we need a stream further on */
        } else if (bFailOnOpenErr) {
            const int openErrno = errno;
            rs_strerror_r(openErrno, errStr, sizeof(errStr));
            parser_errmsg("omfile: open file '%s' failed: %s", newFileName, errStr);
            ABORT_FINALIZE((openErrno == ENOENT) ? RS_RET_FILE_NOT_FOUND : RS_RET_FILE_OPEN_ERROR);
        }
    }
    warnIfFollowingSymlink(pData, newFileName);

    /* the copies below are clumpsy, but there is no way around given the
     * anomalies in dirname() and basename() [they MODIFY the provided buffer...]
     */
    uchar szNameBuf[MAXFNAME + 1];
    uchar szDirName[MAXFNAME + 1];
    uchar szBaseName[MAXFNAME + 1];
    ustrncpy(szNameBuf, newFileName, MAXFNAME);
    szNameBuf[MAXFNAME] = '\0';
    ustrncpy(szDirName, (uchar *)dirname((char *)szNameBuf), MAXFNAME);
    szDirName[MAXFNAME] = '\0';
    ustrncpy(szNameBuf, newFileName, MAXFNAME);
    szNameBuf[MAXFNAME] = '\0';
    ustrncpy(szBaseName, (uchar *)basename((char *)szNameBuf), MAXFNAME);
    szBaseName[MAXFNAME] = '\0';

    CHKiRet(strm.Construct(&pData->pStrm));
    CHKiRet(strm.SetFName(pData->pStrm, szBaseName, ustrlen(szBaseName)));
    CHKiRet(strm.SetDir(pData->pStrm, szDirName, ustrlen(szDirName)));
    CHKiRet(strm.SetiZipLevel(pData->pStrm, pData->iZipLevel));
    CHKiRet(strm.SetbVeryReliableZip(pData->pStrm, pData->bVeryRobustZip));
    CHKiRet(strm.SetsIOBufSize(pData->pStrm, (size_t)pData->iIOBufSize));
    CHKiRet(strm.SettOperationsMode(pData->pStrm, STREAMMODE_WRITE_APPEND));
    CHKiRet(strm.SettOpenMode(pData->pStrm, cs.fCreateMode));
    CHKiRet(strm.SetbNoFollowFinal(pData->pStrm, !pData->bFollowSymlinks));
    CHKiRet(strm.SetcompressionDriver(pData->pStrm, runModConf->compressionDriver));
    CHKiRet(strm.SetCompressionWorkers(pData->pStrm, runModConf->compressionDriver_workers));
    CHKiRet(strm.SetbSync(pData->pStrm, pData->bSyncFile));
    CHKiRet(strm.SetsType(pData->pStrm, STREAMTYPE_FILE_SINGLE));
    CHKiRet(strm.SetiSizeLimit(pData->pStrm, pData->iSizeLimit));
    CHKiRet(strm.SetbSizeLimitCmdPassFileName(pData->pStrm, pData->bSizeLimitCmdPassFileName));
    if (pData->useCryprov) {
        CHKiRet(strm.Setcryprov(pData->pStrm, &pData->cryprov));
        CHKiRet(strm.SetcryprovData(pData->pStrm, pData->cryprovData));
    }
    /* set the flush interval only if we actually use it - otherwise it will activate
     * async processing, which is a real performance waste if we do not do buffered
     * writes! -- rgerhards, 2009-07-06
     */
    if (pData->bUseAsyncWriter) CHKiRet(strm.SetiFlushInterval(pData->pStrm, pData->iFlushInterval));
    if (pData->pszSizeLimitCmd != NULL) CHKiRet(strm.SetpszSizeLimitCmd(pData->pStrm, ustrdup(pData->pszSizeLimitCmd)));
    CHKiRet(strm.ConstructFinalize(pData->pStrm));

    if (pData->useSigprov) sigprovPrepare(pData, szNameBuf);

finalize_it:
    if (iRet != RS_RET_OK) {
        if (pData->pStrm != NULL) {
            closeFile(pData);
        }
    }
    RETiRet;
}


/**
 * @brief Handles dynamic file names, ensuring the correct file is open for writing.
 *
 * This function checks if the requested dynamic file name is already present
 * in the cache. If so, it reuses the existing file handle. If not, it attempts
 * to open the new file, potentially evicting an older entry from the LRU cache
 * if the cache is full.
 *
 * @param pData Pointer to the instance data for the file output action.
 * @param newFileName The new dynamic file name to prepare.
 * @return RS_RET_OK on success, or an error code if the file cannot be opened
 * or memory allocation fails.
 */
static rsRetVal ATTR_NONNULL()
    prepareDynFile(instanceData *__restrict__ const pData, const uchar *__restrict__ const newFileName) {
    uint64 ctOldest; /* "timestamp" of oldest element */
    int iOldest;
    int i;
    int iFirstFree;
    int bNewCacheSlot;
    rsRetVal localRet;
    dynaFileCacheEntry **pCache;
    DEFiRet;

    assert(pData != NULL);
    assert(newFileName != NULL);

    if (pData->iDynaFileCacheSize < 1 || pData->dynCache == NULL) {
        parser_errmsg("omfile: invalid dynafile cache state for '%s' (size %d) - discarding message",
                      (char *)pData->fname, pData->iDynaFileCacheSize);
        ABORT_FINALIZE(RS_RET_ERR);
    }

    pCache = pData->dynCache;

    /* first check, if we still have the current file */
    if ((pData->iCurrElt != -1) && (pCache[pData->iCurrElt] != NULL) && (pCache[pData->iCurrElt]->pName != NULL) &&
        !ustrcmp(newFileName, pCache[pData->iCurrElt]->pName)) {
        /* great, we are all set */
        pCache[pData->iCurrElt]->clkTickAccessed = getClockFileAccess();
        STATSCOUNTER_INC(pData->ctrLevel0, pData->mutCtrLevel0);
        /* LRU needs only a strictly monotonically increasing counter, so such a one could do */
        FINALIZE;
    }

    /* Cached names were checked before insertion. Validate only paths that
     * may need a cache lookup or a new file, preserving the level-0 fast path. */
    if (!pData->bPermitDynaFilePathEscape) {
        CHKiRet(validateDynaFilePath(pData, newFileName));
    }

    /* ok, no luck - current file cannot be re-used */

    /* if we need to flush (at least) on TXEnd, we need to flush now - because
     * we do not know if we will otherwise come back to this file to flush it
     * at end of TX. see https://github.com/rsyslog/rsyslog/issues/2502
     */
    if (((runModConf->pConf->globals.glblDevOptions & DEV_OPTION_8_1905_HANG_TEST) == 0) && pData->bFlushOnTXEnd &&
        pData->pStrm != NULL) {
        CHKiRet(strm.Flush(pData->pStrm));
    }

    /* Now let's search the table if we find a matching spot.
     * While doing so, we also prepare for creation of a new one.
     */
    pData->iCurrElt = -1; /* invalid current element pointer */
    iFirstFree = -1; /* not yet found */
    bNewCacheSlot = 0;
    iOldest = 0; /* we assume the first element to be the oldest - that will change as we loop */
    ctOldest = getClockFileAccess(); /* there must always be an older one */
    for (i = 0; i < pData->iCurrCacheSize; ++i) {
        if (pCache[i] == NULL || pCache[i]->pName == NULL) {
            if (iFirstFree == -1) iFirstFree = i;
        } else { /* got an element, let's see if it matches */
            if (!ustrcmp(newFileName, pCache[i]->pName)) {
                /* we found our element! */
                pData->pStrm = pCache[i]->pStrm;
                if (pData->useSigprov) pData->sigprovFileData = pCache[i]->sigprovFileData;
                pData->iCurrElt = i;
                pCache[i]->clkTickAccessed = getClockFileAccess(); /* update "timestamp" for LRU */
                FINALIZE;
            }
            /* did not find it - so lets keep track of the counters for LRU */
            if (pCache[i]->clkTickAccessed < ctOldest) {
                ctOldest = pCache[i]->clkTickAccessed;
                iOldest = i;
            }
        }
    }

    /* we have not found an entry */
    STATSCOUNTER_INC(pData->ctrMiss, pData->mutCtrMiss);

    /* similarly, we need to set the current pStrm to NULL, because otherwise, if prepareFile() fails,
     * we may end up using an old stream. This bug depends on how exactly prepareFile fails,
     * but it could be triggered in the common case of a failed open() system call.
     * rgerhards, 2010-03-22
     */
    pData->pStrm = NULL, pData->sigprovFileData = NULL;

    if (iFirstFree == -1 && (pData->iCurrCacheSize < pData->iDynaFileCacheSize)) {
        /* there is space left, so set it to that index */
        iFirstFree = pData->iCurrCacheSize;
        bNewCacheSlot = 1;
    }

    /* Note that the following code sequence does not work with the cache entry itself,
     * but rather with pData->pStrm, the (sole) stream pointer in the non-dynafile case.
     * The cache array is only updated after the open was successful. -- rgerhards, 2010-03-21
     */
    if (iFirstFree == -1) {
        dynaFileDelCacheEntry(pData, iOldest, 0);
        STATSCOUNTER_INC(pData->ctrEvict, pData->mutCtrEvict);
        iFirstFree = iOldest; /* this one *is* now free ;) */
    }
    if (pCache[iFirstFree] == NULL) {
        /* we need to allocate memory for the cache structure */
        CHKmalloc(pCache[iFirstFree] = (dynaFileCacheEntry *)calloc(1, sizeof(dynaFileCacheEntry)));
    }

    /* Ok, we finally can open the file */
    localRet = prepareFile(pData, newFileName, 1);

    /* check if we had an error */
    if (localRet != RS_RET_OK) {
        dynaFileDelCacheEntry(pData, iFirstFree, 1);
        pData->iCurrElt = -1;
        pData->pStrm = NULL;
        pData->sigprovFileData = NULL;
        /* We do no longer care about internal messages. The errmsg rate limiter
         * will take care of too-frequent error messages.
         */
        parser_errmsg(
            "Could not open dynamic file '%s' [state %d] - discarding "
            "message",
            newFileName, localRet);
        ABORT_FINALIZE(localRet);
    }

    if ((pCache[iFirstFree]->pName = ustrdup(newFileName)) == NULL) {
        closeFile(pData); /* need to free failed entry! */
        dynaFileDelCacheEntry(pData, iFirstFree, 1);
        pData->iCurrElt = -1;
        pData->pStrm = NULL;
        pData->sigprovFileData = NULL;
        ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
    }
    pCache[iFirstFree]->pStrm = pData->pStrm;
    if (pData->useSigprov) pCache[iFirstFree]->sigprovFileData = pData->sigprovFileData;
    pCache[iFirstFree]->clkTickAccessed = getClockFileAccess();
    pData->iCurrElt = iFirstFree;
    if (bNewCacheSlot) {
        pData->iCurrCacheSize++;
        STATSCOUNTER_SETMAX_NOMUT(pData->ctrMax, (unsigned)pData->iCurrCacheSize);
    }
    DBGPRINTF("Added new entry %d for file cache, file '%s'.\n", iFirstFree, newFileName);

finalize_it:
    if (iRet == RS_RET_OK) pCache[pData->iCurrElt]->nInactive = 0;
    RETiRet;
}


/**
 * @brief Performs the actual buffered write operation to the file stream.
 *
 * This function writes the provided buffer to the stream associated with
 * the instance data. It also calls the signature provider's `OnRecordWrite`
 * method if a signature provider is in use.
 *
 * @param pData Pointer to the instance data containing the stream and provider info.
 * @param pszBuf Pointer to the buffer containing the data to write.
 * @param lenBuf The length of the data in the buffer.
 * @return RS_RET_OK on success, or an error code from the stream write operation.
 */
static rsRetVal doWrite(instanceData *__restrict__ const pData, uchar *__restrict__ const pszBuf, const int lenBuf) {
    DEFiRet;
    assert(pData != NULL);
    assert(pszBuf != NULL);

    const int needsLF = pData->bAddLF && (lenBuf == 0 || pszBuf[lenBuf - 1] != '\n');
    uchar addlfBuf[ADDLF_SIGBUF_STACKLEN];
    uchar *sigBuf = NULL;
    uchar *sigWriteBuf = pszBuf;
    size_t sigLen = 0;
    rs_size_t sigWriteLen = (rs_size_t)lenBuf;
    int freeSigBuf = 0;

    DBGPRINTF("omfile: write to stream, pData->pStrm %p, lenBuf %d, needsLF %d, strt data %.128s\n", pData->pStrm,
              lenBuf, needsLF, pszBuf);
    if (pData->pStrm != NULL) {
        if (lenBuf > 0) {
            CHKiRet(strm.Write(pData->pStrm, pszBuf, lenBuf));
        }
        if (needsLF) {
            CHKiRet(strm.WriteChar(pData->pStrm, '\n'));
        }
    }

    if (pData->useSigprov) {
        if (needsLF) {
            sigLen = (size_t)lenBuf + 1;
            if (sigLen <= sizeof(addlfBuf)) {
                sigBuf = addlfBuf;
            } else {
                sigBuf = (uchar *)malloc(sigLen);
                if (sigBuf == NULL) {
                    ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
                }
                freeSigBuf = 1;
            }
            if (lenBuf > 0) {
                memcpy(sigBuf, pszBuf, (size_t)lenBuf);
            }
            sigBuf[lenBuf] = '\n';
            sigWriteBuf = sigBuf;
            sigWriteLen = (rs_size_t)sigLen;
        }
        CHKiRet(pData->sigprov.OnRecordWrite(pData->sigprovFileData, sigWriteBuf, sigWriteLen));
    }

finalize_it:
    if (freeSigBuf) {
        free(sigBuf);
    }
    RETiRet;
}


/**
 * @brief Writes a message to the configured file.
 *
 * This function determines whether the output is to a static file or a
 * dynamic file. If dynamic, it calls `prepareDynFile` to ensure the correct
 * file is open. It then calls `doWrite` to perform the actual write operation.
 *
 * @param pData Pointer to the instance data for the file output action.
 * @param pParam Pointer to the action worker instance parameters.
 * @param iMsg The index of the message within the batch to process.
 * @return RS_RET_OK on success, or an error code if file preparation
 * or writing fails.
 */
static rsRetVal writeFile(instanceData *__restrict__ const pData,
                          const actWrkrIParams_t *__restrict__ const pParam,
                          const int iMsg) {
    DEFiRet;

    STATSCOUNTER_INC(pData->ctrRequests, pData->mutCtrRequests);
    /* first check if we have a dynamic file name and, if so,
     * check if it still is ok or a new file needs to be created
     */
    if (pData->bDynamicName) {
        DBGPRINTF("omfile: file to log to: %s\n", actParam(pParam, pData->iNumTpls, iMsg, 1).param);
        iRet = prepareDynFile(pData, actParam(pParam, pData->iNumTpls, iMsg, 1).param);
        if (iRet == RS_RET_DISCARDMSG) {
            /* A blocked message was not written; keep processing this batch so
             * successfully written records are not retried by the action core. */
            iRet = RS_RET_OK;
            FINALIZE;
        }
        CHKiRet(iRet);
    } else { /* "regular", non-dynafile */
        if (pData->pStrm == NULL) {
            CHKiRet(prepareFile(pData, pData->fname, 0));
            if (pData->pStrm == NULL) {
                parser_errmsg("Could not open output file '%s'", pData->fname);
            }
        }
        pData->nInactive = 0;
    }

    iRet = doWrite(pData, actParam(pParam, pData->iNumTpls, iMsg, 0).param,
                   actParam(pParam, pData->iNumTpls, iMsg, 0).lenStr);

finalize_it:
    RETiRet;
}


BEGINbeginCnfLoad
    CODESTARTbeginCnfLoad;
    loadModConf = pModConf;
    pModConf->pConf = pConf;
    pModConf->tplName = NULL;
    pModConf->fCreateMode = 0644;
    pModConf->fDirCreateMode = 0700;
    pModConf->fileUID = -1;
    pModConf->dirUID = -1;
    pModConf->fileGID = -1;
    pModConf->dirGID = -1;
    pModConf->bDynafileDoNotSuspend = 1;
    pModConf->bPermitDynaFilePathEscape = 0;
    pModConf->bRestrictDynaFileTplType = 0;
    pModConf->bAddLF = 1;
    pModConf->bFollowSymlinks = -1;
    pModConf->bFollowSymlinksExplicit = 0;
ENDbeginCnfLoad

BEGINsetModCnf
    struct cnfparamvals *pvals = NULL;
    int i;
    CODESTARTsetModCnf;
    pvals = nvlstGetParams(lst, &modpblk, NULL);
    if (pvals == NULL) {
        parser_errmsg(
            "error processing module "
            "config parameters [module(...)]");
        ABORT_FINALIZE(RS_RET_MISSING_CNFPARAMS);
    }

    if (Debug) {
        dbgprintf("module (global) param blk for omfile:\n");
        cnfparamsPrint(&modpblk, pvals);
    }

    for (i = 0; i < modpblk.nParams; ++i) {
        if (!pvals[i].bUsed) {
            continue;
        }

        if (!strcmp(modpblk.descr[i].name, "template")) {
            CHKmalloc(loadModConf->tplName = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
            if (pszFileDfltTplName != NULL) {
                parser_errmsg(
                    "omfile: warning: default template was already "
                    "set via legacy directive - may lead to inconsistent "
                    "results.");
            }
        } else if (!strcmp(modpblk.descr[i].name, "addlf")) {
            loadModConf->bAddLF = pvals[i].val.d.n;
        } else if (!strcmp(modpblk.descr[i].name, "compression.driver")) {
            if (!es_strcasebufcmp(pvals[i].val.d.estr, (const unsigned char *)"zlib", 4)) {
                loadModConf->compressionDriver = STRM_COMPRESS_ZIP;
            } else if (!es_strcasebufcmp(pvals[i].val.d.estr, (const unsigned char *)"zstd", 4)) {
                loadModConf->compressionDriver = STRM_COMPRESS_ZSTD;
            } else {
                parser_errmsg(
                    "omfile: error: invalid compression.driver driver "
                    "name - noch applying setting. Valid drivers: 'zlib' and "
                    "'zstd'.");
            }
        } else if (!strcmp(modpblk.descr[i].name, "compression.zstd.workers")) {
            loadModConf->compressionDriver_workers = (int)pvals[i].val.d.n;
        } else if (!strcmp(modpblk.descr[i].name, "dircreatemode")) {
            loadModConf->fDirCreateMode = (int)pvals[i].val.d.n;
        } else if (!strcmp(modpblk.descr[i].name, "filecreatemode")) {
            loadModConf->fCreateMode = (int)pvals[i].val.d.n;
        } else if (!strcmp(modpblk.descr[i].name, "dirowner")) {
            loadModConf->dirUID = (int)pvals[i].val.d.n;
        } else if (!strcmp(modpblk.descr[i].name, "dirownernum")) {
            loadModConf->dirUID = (int)pvals[i].val.d.n;
        } else if (!strcmp(modpblk.descr[i].name, "dirgroup")) {
            loadModConf->dirGID = (int)pvals[i].val.d.n;
        } else if (!strcmp(modpblk.descr[i].name, "dirgroupnum")) {
            loadModConf->dirGID = (int)pvals[i].val.d.n;
        } else if (!strcmp(modpblk.descr[i].name, "fileowner")) {
            loadModConf->fileUID = (int)pvals[i].val.d.n;
        } else if (!strcmp(modpblk.descr[i].name, "fileownernum")) {
            loadModConf->fileUID = (int)pvals[i].val.d.n;
        } else if (!strcmp(modpblk.descr[i].name, "filegroup")) {
            loadModConf->fileGID = (int)pvals[i].val.d.n;
        } else if (!strcmp(modpblk.descr[i].name, "filegroupnum")) {
            loadModConf->fileGID = (int)pvals[i].val.d.n;
        } else if (!strcmp(modpblk.descr[i].name, "dynafile.donotsuspend")) {
            loadModConf->bDynafileDoNotSuspend = (int)pvals[i].val.d.n;
        } else if (!strcmp(modpblk.descr[i].name, "followsymlinks")) {
            loadModConf->bFollowSymlinks = (int)pvals[i].val.d.n;
            loadModConf->bFollowSymlinksExplicit = 1;
        } else if (!strcmp(modpblk.descr[i].name, "dynafile.dangerouspermitpathescape")) {
            loadModConf->bPermitDynaFilePathEscape = pvals[i].val.d.n;
            if (loadModConf->bPermitDynaFilePathEscape) {
                warnDynaFilePathEscapeEnabled("module parameter");
            }
        } else if (!strcmp(modpblk.descr[i].name, "dynafile.restricttemplatetype")) {
            loadModConf->bRestrictDynaFileTplType = pvals[i].val.d.n;
        } else {
            dbgprintf(
                "omfile: program error, non-handled "
                "param '%s' in beginCnfLoad\n",
                modpblk.descr[i].name);
        }
    }
finalize_it:
    if (pvals != NULL) cnfparamvalsDestruct(pvals, &modpblk);
ENDsetModCnf

/**
 * @brief Checks the dynamic file cache for entries exceeding their close timeout.
 *
 * This function iterates through the dynamic file cache and closes/deletes
 * entries that have been inactive for longer than their configured
 * `iCloseTimeout`. It is typically called by the janitor.
 *
 * @param pData Pointer to the instance data containing the dynamic file cache.
 */
static void janitorChkDynaFiles(instanceData *__restrict__ const pData) {
    int i;
    dynaFileCacheEntry **pCache = pData->dynCache;

    for (i = 0; i < pData->iCurrCacheSize; ++i) {
        if (pCache[i] == NULL) continue;
        DBGPRINTF("omfile janitor: checking dynafile %d:%s, inactive since %d\n", i,
                  pCache[i]->pName == NULL ? UCHAR_CONSTANT("[OPEN FAILED]") : pCache[i]->pName,
                  (int)pCache[i]->nInactive);
        if (pCache[i]->nInactive >= pData->iCloseTimeout) {
            STATSCOUNTER_INC(pData->ctrCloseTimeouts, pData->mutCtrCloseTimeouts);
            dynaFileDelCacheEntry(pData, i, 1);
            if (pData->iCurrElt == i) pData->iCurrElt = -1; /* no longer available! */
        } else {
            pCache[i]->nInactive += runModConf->pConf->globals.janitorInterval;
        }
    }
}

/**
 * @brief Callback function for the janitor.
 *
 * This function is called periodically by the rsyslog janitor to clean out
 * inactive files (both static and dynamic) based on their configured
 * `iCloseTimeout`. It acquires a mutex to protect access to the file data.
 *
 * @param pUsr A pointer to the `instanceData` structure for the file action.
 */
static void janitorCB(void *pUsr) {
    instanceData *__restrict__ const pData = (instanceData *)pUsr;
    pthread_mutex_lock(&pData->mutWrite);
    if (pData->bDynamicName) {
        janitorChkDynaFiles(pData);
    } else {
        if (pData->pStrm != NULL) {
            DBGPRINTF("omfile janitor: checking file %s, inactive since %d\n", pData->fname, pData->nInactive);
            if (pData->nInactive >= pData->iCloseTimeout) {
                STATSCOUNTER_INC(pData->ctrCloseTimeouts, pData->mutCtrCloseTimeouts);
                closeFile(pData);
            } else {
                pData->nInactive += runModConf->pConf->globals.janitorInterval;
            }
        }
    }
    pthread_mutex_unlock(&pData->mutWrite);
}


BEGINendCnfLoad
    CODESTARTendCnfLoad;
    loadModConf = NULL; /* done loading */
    /* free legacy config vars */
    free(pszFileDfltTplName);
    pszFileDfltTplName = NULL;
ENDendCnfLoad

BEGINcheckCnf
    CODESTARTcheckCnf;
ENDcheckCnf

BEGINactivateCnf
    CODESTARTactivateCnf;
    runModConf = pModConf;
ENDactivateCnf

BEGINfreeCnf
    CODESTARTfreeCnf;
    free(pModConf->tplName);
ENDfreeCnf


BEGINcreateInstance
    CODESTARTcreateInstance;
    pData->pStrm = NULL;
    pData->bAddLF = 1;
    pthread_mutex_init(&pData->mutWrite, NULL);
ENDcreateInstance


BEGINcreateWrkrInstance
    CODESTARTcreateWrkrInstance;
ENDcreateWrkrInstance


BEGINfreeInstance
    CODESTARTfreeInstance;
    free(pData->tplName);
    free(pData->fname);
    free(pData->dynaFileBasePath);
    free(pData->pszSizeLimitCmd);
    if (pData->iCloseTimeout > 0) janitorDelEtry(pData->janitorID);
    if (pData->bDynamicName) {
        dynaFileFreeCache(pData);
    } else if (pData->pStrm != NULL)
        closeFile(pData);
    if (pData->stats != NULL) statsobj.Destruct(&(pData->stats));
    if (pData->useSigprov) {
        pData->sigprov.Destruct(&pData->sigprovData);
        obj.ReleaseObj(__FILE__, pData->sigprovNameFull + 2, pData->sigprovNameFull, (void *)&pData->sigprov);
        free(pData->sigprovName);
        free(pData->sigprovNameFull);
    }
    if (pData->useCryprov) {
        pData->cryprov.Destruct(&pData->cryprovData);
        obj.ReleaseObj(__FILE__, pData->cryprovNameFull + 2, pData->cryprovNameFull, (void *)&pData->cryprov);
        free(pData->cryprovName);
        free(pData->cryprovNameFull);
    }
    pthread_mutex_destroy(&pData->mutWrite);
ENDfreeInstance


BEGINfreeWrkrInstance
    CODESTARTfreeWrkrInstance;
ENDfreeWrkrInstance


BEGINsetActionInfo
    CODESTARTsetActionInfo;
    if (pData->bDynamicName && pAction->iNumTpls > 1) {
        iRet = checkDynaFileTemplateType(pData, pAction->ppTpl[1]);
        if (iRet != RS_RET_OK) {
            RETiRet;
        }
        iRet = deriveDynaFileBasePath(pData, pAction->ppTpl[1]);
    }
ENDsetActionInfo


BEGINtryResume
    CODESTARTtryResume;
ENDtryResume

BEGINbeginTransaction
    CODESTARTbeginTransaction;
    /* we have nothing to do to begin a transaction */
ENDbeginTransaction


BEGINcommitTransaction
    instanceData *__restrict__ const pData = pWrkrData->pData;
    rsRetVal localRet;
    unsigned i;
    CODESTARTcommitTransaction;

    if (pData->isDevNull) {
        goto terminate;
    }

    pthread_mutex_lock(&pData->mutWrite);

    for (i = 0; i < nParams; ++i) {
        localRet = writeFile(pData, pParams, i);
        if (localRet != RS_RET_OK && iRet == RS_RET_OK) {
            iRet = localRet;
        }
    }
    /* Note: pStrm may be NULL if there was an error opening the stream */
    /* if bFlushOnTXEnd is set, we need to flush on transaction end - in
     * any case. It is not relevant if this is using background writes
     * (which then become pretty slow) or not. And, similarly, no flush
     * happens when it is not set. Please see
     * https://github.com/rsyslog/rsyslog/issues/1297
     * for a discussion of why we actually need this.
     * rgerhards, 2017-01-13
     */
    if (pData->bFlushOnTXEnd && pData->pStrm != NULL) {
        localRet = strm.Flush(pData->pStrm);
        if (localRet != RS_RET_OK && iRet == RS_RET_OK) {
            CHKiRet(localRet);
        }
    }

finalize_it:
    pthread_mutex_unlock(&pData->mutWrite);
    if (iRet == RS_RET_FILE_OPEN_ERROR || iRet == RS_RET_FILE_NOT_FOUND) {
        iRet = (pData->bDynamicName && runModConf->bDynafileDoNotSuspend) ? RS_RET_OK : RS_RET_SUSPENDED;
    }

terminate:
ENDcommitTransaction

/**
 * @brief Sets default parameter values for a new omfile action instance.
 *
 * This function initializes various fields of the `instanceData` structure
 * with their default values, some of which are derived from module-global
 * configuration settings.
 *
 * @param pData Pointer to the `instanceData` structure to be initialized.
 */
static void setInstParamDefaults(instanceData *__restrict__ const pData) {
    pData->fname = NULL;
    pData->tplName = NULL;
    pData->dynaFileBasePath = NULL;
    pData->bPermitDynaFilePathEscape = loadModConf->bPermitDynaFilePathEscape;
    pData->bRestrictDynaFileTplType = loadModConf->bRestrictDynaFileTplType;
    pData->fileUID = loadModConf->fileUID;
    pData->fileGID = loadModConf->fileGID;
    pData->dirUID = loadModConf->dirUID;
    pData->dirGID = loadModConf->dirGID;
    pData->bFailOnChown = 1;
    pData->iDynaFileCacheSize = 10;
    pData->fCreateMode = loadModConf->fCreateMode;
    pData->fDirCreateMode = loadModConf->fDirCreateMode;
    pData->bCreateDirs = 1;
    pData->bSyncFile = 0;
    pData->iZipLevel = 0;
    pData->bVeryRobustZip = 0;
    pData->bFlushOnTXEnd = FLUSHONTX_DFLT;
    pData->iIOBufSize = IOBUF_DFLT_SIZE;
    pData->iFlushInterval = FLUSH_INTRVL_DFLT;
    pData->bUseAsyncWriter = USE_ASYNCWRITER_DFLT;
    pData->bAddLF = loadModConf->bAddLF;
    pData->bFollowSymlinks = loadModConf->bFollowSymlinks;
    pData->bFollowSymlinksExplicit = loadModConf->bFollowSymlinksExplicit;
    if (pData->bFollowSymlinks == -1) {
        pData->bFollowSymlinks = getCompatDefaultFollowSymlinks(loadConf);
    }
    pData->sigprovName = NULL;
    pData->cryprovName = NULL;
    pData->useSigprov = 0;
    pData->useCryprov = 0;
    pData->iCloseTimeout = -1;
    pData->iSizeLimit = 0;
    pData->isDevNull = 0;
    pData->pszSizeLimitCmd = NULL;
    pData->bSizeLimitCmdPassFileName = 1;
}

/**
 * @brief Sets up statistics counters for a new omfile instance.
 *
 * This function initializes and registers statistics counters for dynamic
 * file actions, including requests, cache hits/misses, evictions, and
 * close timeouts.
 *
 * @param pData Pointer to the `instanceData` structure for which to set up stats.
 * @return RS_RET_OK on success, or an error code if statsobj construction fails.
 */
static rsRetVal setupInstStatsCtrs(instanceData *__restrict__ const pData) {
    uchar ctrName[512];
    DEFiRet;

    if (!pData->bDynamicName) {
        FINALIZE;
    }

    /* support statistics gathering */
    snprintf((char *)ctrName, sizeof(ctrName), "dynafile cache %s", pData->fname);
    ctrName[sizeof(ctrName) - 1] = '\0'; /* be on the save side */
    CHKiRet(statsobj.Construct(&(pData->stats)));
    CHKiRet(statsobj.SetName(pData->stats, ctrName));
    CHKiRet(statsobj.SetOrigin(pData->stats, (uchar *)"omfile"));
    STATSCOUNTER_INIT(pData->ctrRequests, pData->mutCtrRequests);
    CHKiRet(statsobj.AddCounter(pData->stats, UCHAR_CONSTANT("requests"), ctrType_IntCtr, CTR_FLAG_RESETTABLE,
                                &(pData->ctrRequests)));
    STATSCOUNTER_INIT(pData->ctrLevel0, pData->mutCtrLevel0);
    CHKiRet(statsobj.AddCounter(pData->stats, UCHAR_CONSTANT("level0"), ctrType_IntCtr, CTR_FLAG_RESETTABLE,
                                &(pData->ctrLevel0)));
    STATSCOUNTER_INIT(pData->ctrMiss, pData->mutCtrMiss);
    CHKiRet(statsobj.AddCounter(pData->stats, UCHAR_CONSTANT("missed"), ctrType_IntCtr, CTR_FLAG_RESETTABLE,
                                &(pData->ctrMiss)));
    STATSCOUNTER_INIT(pData->ctrEvict, pData->mutCtrEvict);
    CHKiRet(statsobj.AddCounter(pData->stats, UCHAR_CONSTANT("evicted"), ctrType_IntCtr, CTR_FLAG_RESETTABLE,
                                &(pData->ctrEvict)));
    STATSCOUNTER_INIT(pData->ctrMax, pData->mutCtrMax);
    CHKiRet(statsobj.AddCounter(pData->stats, UCHAR_CONSTANT("maxused"), ctrType_IntCtr, CTR_FLAG_RESETTABLE,
                                &(pData->ctrMax)));
    STATSCOUNTER_INIT(pData->ctrCloseTimeouts, pData->mutCtrCloseTimeouts);
    CHKiRet(statsobj.AddCounter(pData->stats, UCHAR_CONSTANT("closetimeouts"), ctrType_IntCtr, CTR_FLAG_RESETTABLE,
                                &(pData->ctrCloseTimeouts)));
    CHKiRet(statsobj.ConstructFinalize(pData->stats));

finalize_it:
    RETiRet;
}

/**
 * @brief Initializes the signature provider for an omfile instance.
 *
 * This function loads the specified signature provider module, constructs
 * its data instance, and sets its configuration parameters. If any step
 * fails, signatures are disabled for this instance.
 *
 * @param pData Pointer to the `instanceData` structure.
 * @param lst List of configuration parameters for the signature provider.
 */
static void initSigprov(instanceData *__restrict__ const pData, struct nvlst *lst) {
    uchar szDrvrName[1024];

    if (snprintf((char *)szDrvrName, sizeof(szDrvrName), "lmsig_%s", pData->sigprovName) == sizeof(szDrvrName)) {
        parser_errmsg(
            "omfile: signature provider "
            "name is too long: '%s' - signatures disabled",
            pData->sigprovName);
        goto done;
    }
    pData->sigprovNameFull = ustrdup(szDrvrName);

    pData->sigprov.ifVersion = sigprovCURR_IF_VERSION;
    /* The pDrvrName+2 below is a hack to obtain the object name. It
     * safes us to have yet another variable with the name without "lm" in
     * front of it. If we change the module load interface, we may re-think
     * about this hack, but for the time being it is efficient and clean enough.
     */
    if (obj.UseObj(__FILE__, szDrvrName, szDrvrName, (void *)&pData->sigprov) != RS_RET_OK) {
        parser_errmsg(
            "omfile: could not load "
            "signature provider '%s' - signatures disabled",
            szDrvrName);
        goto done;
    }

    if (pData->sigprov.Construct(&pData->sigprovData) != RS_RET_OK) {
        parser_errmsg(
            "omfile: error constructing "
            "signature provider %s dataset - signatures disabled",
            szDrvrName);
        goto done;
    }
    pData->sigprov.SetCnfParam(pData->sigprovData, lst);

    dbgprintf("loaded signature provider %s, data instance at %p\n", szDrvrName, pData->sigprovData);
    pData->useSigprov = 1;
done:
    return;
}

/**
 * @brief Initializes the crypto provider for an omfile instance.
 *
 * This function loads the specified crypto provider module, constructs
 * its data instance, and sets its configuration parameters. If any step
 * fails, encryption is disabled for this instance.
 *
 * @param pData Pointer to the `instanceData` structure.
 * @param lst List of configuration parameters for the crypto provider.
 * @return RS_RET_OK on success, or an error code if loading or construction fails.
 */
static rsRetVal initCryprov(instanceData *__restrict__ const pData, struct nvlst *lst) {
    uchar szDrvrName[1024];
    DEFiRet;

    if (snprintf((char *)szDrvrName, sizeof(szDrvrName), "lmcry_%s", pData->cryprovName) == sizeof(szDrvrName)) {
        parser_errmsg(
            "omfile: crypto provider "
            "name is too long: '%s' - encryption disabled",
            pData->cryprovName);
        ABORT_FINALIZE(RS_RET_ERR);
    }
    pData->cryprovNameFull = ustrdup(szDrvrName);

    pData->cryprov.ifVersion = cryprovCURR_IF_VERSION;
    /* The pDrvrName+2 below is a hack to obtain the object name. It
     * safes us to have yet another variable with the name without "lm" in
     * front of it. If we change the module load interface, we may re-think
     * about this hack, but for the time being it is efficient and clean enough.
     */
    if (obj.UseObj(__FILE__, szDrvrName, szDrvrName, (void *)&pData->cryprov) != RS_RET_OK) {
        parser_errmsg(
            "omfile: could not load "
            "crypto provider '%s' - encryption disabled",
            szDrvrName);
        ABORT_FINALIZE(RS_RET_CRYPROV_ERR);
    }

    if (pData->cryprov.Construct(&pData->cryprovData) != RS_RET_OK) {
        parser_errmsg(
            "omfile: error constructing "
            "crypto provider %s dataset - encryption disabled",
            szDrvrName);
        ABORT_FINALIZE(RS_RET_CRYPROV_ERR);
    }
    CHKiRet(pData->cryprov.SetCnfParam(pData->cryprovData, lst, CRYPROV_PARAMTYPE_REGULAR));

    dbgprintf("loaded crypto provider %s, data instance at %p\n", szDrvrName, pData->cryprovData);
    pData->useCryprov = 1;
finalize_it:
    RETiRet;
}

BEGINnewActInst
    struct cnfparamvals *pvals;
    uchar *tplToUse;
    int i;
    CODESTARTnewActInst;
    DBGPRINTF("newActInst (omfile)\n");

    pvals = nvlstGetParams(lst, &actpblk, NULL);
    if (pvals == NULL) {
        parser_errmsg(
            "omfile: either the \"file\" or "
            "\"dynafile\" parameter must be given");
        ABORT_FINALIZE(RS_RET_MISSING_CNFPARAMS);
    }

    if (Debug) {
        dbgprintf("action param blk in omfile:\n");
        cnfparamsPrint(&actpblk, pvals);
    }

    CHKiRet(createInstance(&pData));
    setInstParamDefaults(pData);

    for (i = 0; i < actpblk.nParams; ++i) {
        if (!pvals[i].bUsed) continue;
        if (!strcmp(actpblk.descr[i].name, "dynafilecachesize")) {
            pData->iDynaFileCacheSize = (int)pvals[i].val.d.n;
            const rsRetVal localRet = normalizeDynaFileCacheSize(&pData->iDynaFileCacheSize);
            if (localRet != RS_RET_OK && localRet != RS_RET_VAL_OUT_OF_RANGE) {
                ABORT_FINALIZE(localRet);
            }
        } else if (!strcmp(actpblk.descr[i].name, "ziplevel")) {
            pData->iZipLevel = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "flushinterval")) {
            pData->iFlushInterval = pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "veryrobustzip")) {
            pData->bVeryRobustZip = pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "asyncwriting")) {
            pData->bUseAsyncWriter = pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "flushontxend")) {
            pData->bFlushOnTXEnd = pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "iobuffersize")) {
            pData->iIOBufSize = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "dirowner")) {
            pData->dirUID = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "dirownernum")) {
            pData->dirUID = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "dirgroup")) {
            pData->dirGID = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "dirgroupnum")) {
            pData->dirGID = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "fileowner")) {
            pData->fileUID = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "fileownernum")) {
            pData->fileUID = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "filegroup")) {
            pData->fileGID = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "filegroupnum")) {
            pData->fileGID = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "dircreatemode")) {
            pData->fDirCreateMode = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "filecreatemode")) {
            pData->fCreateMode = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "failonchownfailure")) {
            pData->bFailOnChown = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "sync")) {
            pData->bSyncFile = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "createdirs")) {
            pData->bCreateDirs = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "addlf")) {
            pData->bAddLF = pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "file")) {
            CHKmalloc(pData->fname = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
            CODE_STD_STRING_REQUESTnewActInst(1);
            pData->bDynamicName = 0;
        } else if (!strcmp(actpblk.descr[i].name, "dynafile")) {
            if (pData->fname != NULL) {
                parser_errmsg("omfile: both \"file\" and \"dynafile\" set, will use dynafile");
            }
            CHKmalloc(pData->fname = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
            CODE_STD_STRING_REQUESTnewActInst(2);
            pData->bDynamicName = 1;
        } else if (!strcmp(actpblk.descr[i].name, "followsymlinks")) {
            pData->bFollowSymlinks = (int)pvals[i].val.d.n;
            pData->bFollowSymlinksExplicit = 1;
        } else if (!strcmp(actpblk.descr[i].name, "template")) {
            CHKmalloc(pData->tplName = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "dynafile.dangerouspermitpathescape")) {
            pData->bPermitDynaFilePathEscape = pvals[i].val.d.n;
            if (pData->bPermitDynaFilePathEscape) {
                warnDynaFilePathEscapeEnabled("action parameter");
            }
        } else if (!strcmp(actpblk.descr[i].name, "sig.provider")) {
            CHKmalloc(pData->sigprovName = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "cry.provider")) {
            CHKmalloc(pData->cryprovName = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "closetimeout")) {
            pData->iCloseTimeout = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "rotation.sizelimit")) {
            pData->iSizeLimit = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "rotation.sizelimitcommand")) {
            CHKmalloc(pData->pszSizeLimitCmd = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "rotation.sizelimitcommandpassfilename")) {
            pData->bSizeLimitCmdPassFileName = (int)pvals[i].val.d.n;
        } else {
            dbgprintf(
                "omfile: program error, non-handled "
                "param '%s'\n",
                actpblk.descr[i].name);
        }
    }

    if (pData->fname == NULL || *pData->fname == '\0') {
        parser_errmsg(
            "omfile: either the \"file\" or "
            "\"dynafile\" parameter must be given");
        ABORT_FINALIZE(RS_RET_MISSING_CNFPARAMS);
    }

    int allWhiteSpace = 1;
    for (const char *p = (const char *)pData->fname; *p; ++p) {
        if (!isspace((unsigned char)*p)) {
            allWhiteSpace = 0;
            break;
        }
    }
    if (allWhiteSpace) {
        parser_errmsg(
            "omfile: \"file\" or \"dynafile\" parameter "
            "consist only of whitespace - this is not permitted");
        ABORT_FINALIZE(RS_RET_MISSING_CNFPARAMS);
    }

    if (!strcmp((const char *)pData->fname, "/dev/null")) {
        pData->isDevNull = 1;
    }

    if (pData->sigprovName != NULL) {
        initSigprov(pData, lst);
    }

    if (pData->cryprovName != NULL) {
        CHKiRet(initCryprov(pData, lst));
    }

    tplToUse = ustrdup((pData->tplName == NULL) ? getDfltTpl() : pData->tplName);
    CHKiRet(OMSRsetEntry(*ppOMSR, 0, tplToUse, OMSR_NO_RQD_TPL_OPTS));
    pData->iNumTpls = 1;

    if (pData->bDynamicName) {
        /* "filename" is actually a template name, we need this as string 1. So let's add it
         * to the pOMSR. -- rgerhards, 2007-07-27
         */
        CHKiRet(OMSRsetEntry(*ppOMSR, 1, ustrdup(pData->fname), OMSR_TPL_AS_DYNAFILE));
        pData->iNumTpls = 2;
        // TODO: create unified code for this (legacy+v6 system)
        /* we now allocate the cache table */
        CHKmalloc(pData->dynCache =
                      (dynaFileCacheEntry **)calloc(pData->iDynaFileCacheSize, sizeof(dynaFileCacheEntry *)));
        pData->iCurrElt = -1; /* no current element */
    }
    // TODO: add	pData->iSizeLimit = 0; /* default value, use outchannels to configure! */
    setupInstStatsCtrs(pData);

    if (pData->iCloseTimeout == -1) { /* unset? */
        pData->iCloseTimeout = (pData->bDynamicName) ? 10 : 0;
    }

    snprintf(pData->janitorID, sizeof(pData->janitorID), "omfile:%sfile:%s:%p", (pData->bDynamicName) ? "dyna" : "",
             pData->fname, pData);
    pData->janitorID[sizeof(pData->janitorID) - 1] = '\0'; /* just in case... */

    if (pData->iCloseTimeout > 0) janitorAddEtry(janitorCB, pData->janitorID, pData);

    CODE_STD_FINALIZERnewActInst;
    cnfparamvalsDestruct(pvals, &actpblk);
ENDnewActInst


BEGINparseSelectorAct
    uchar fname[MAXFNAME];
    CODESTARTparseSelectorAct;
    /* Note: the indicator sequence permits us to use '$' to signify
     * outchannel, what otherwise is not possible due to truely
     * unresolvable grammar conflicts (*this time no way around*).
     * rgerhards, 2011-07-09
     */
    if (!strncmp((char *)p, ":omfile:", sizeof(":omfile:") - 1)) {
        p += sizeof(":omfile:") - 1;
    }
    if (!(*p == '$' || *p == '?' || *p == '/' || *p == '.' || *p == '-')) ABORT_FINALIZE(RS_RET_CONFLINE_UNPROCESSED);

    CHKiRet(createInstance(&pData));

    /* Legacy selector actions bypass newActInst(), so copy the dynafile
     * module defaults explicitly when a v6 module block was provided. */
    pData->dynaFileBasePath = NULL;
    pData->bPermitDynaFilePathEscape = 0;
    pData->bRestrictDynaFileTplType = 0;
    if (loadModConf != NULL) {
        pData->bPermitDynaFilePathEscape = loadModConf->bPermitDynaFilePathEscape;
        pData->bRestrictDynaFileTplType = loadModConf->bRestrictDynaFileTplType;
    }

    if (*p == '-') {
        pData->bSyncFile = 0;
        p++;
    } else {
        pData->bSyncFile = cs.bEnableSync;
    }
    pData->iSizeLimit = 0; /* default value, use outchannels to configure! */

    switch (*p) {
        case '$':
            CODE_STD_STRING_REQUESTparseSelectorAct(1) pData->iNumTpls = 1;
            /* rgerhards 2005-06-21: this is a special setting for output-channel
             * definitions. In the long term, this setting will probably replace
             * anything else, but for the time being we must co-exist with the
             * traditional mode lines.
             * rgerhards, 2007-07-24: output-channels will go away. We keep them
             * for compatibility reasons, but seems to have been a bad idea.
             */
            CHKiRet(cflineParseOutchannel(pData, p, *ppOMSR, 0, OMSR_NO_RQD_TPL_OPTS));
            pData->bDynamicName = 0;
            break;

        case '?': /* This is much like a regular file handle, but we need to obtain
                   * a template name. rgerhards, 2007-07-03
                   */
            CODE_STD_STRING_REQUESTparseSelectorAct(2) pData->iNumTpls = 2;
            ++p; /* eat '?' */
            CHKiRet(cflineParseFileName(p, fname, *ppOMSR, 0, OMSR_NO_RQD_TPL_OPTS, getDfltTpl()));
            pData->fname = ustrdup(fname);
            pData->bDynamicName = 1;
            pData->iCurrElt = -1; /* no current element */
            /* "filename" is actually a template name, we need this as string 1. So let's add it
             * to the pOMSR. -- rgerhards, 2007-07-27
             */
            CHKiRet(OMSRsetEntry(*ppOMSR, 1, ustrdup(pData->fname), OMSR_TPL_AS_DYNAFILE));
            /* we now allocate the cache table */
            CHKmalloc(pData->dynCache =
                          (dynaFileCacheEntry **)calloc(cs.iDynaFileCacheSize, sizeof(dynaFileCacheEntry *)));
            break;

        case '/':
        case '.':
            CODE_STD_STRING_REQUESTparseSelectorAct(1) pData->iNumTpls = 1;
            CHKiRet(cflineParseFileName(p, fname, *ppOMSR, 0, OMSR_NO_RQD_TPL_OPTS, getDfltTpl()));
            pData->fname = ustrdup(fname);
            pData->bDynamicName = 0;
            break;
        default:
            ABORT_FINALIZE(RS_RET_CONFLINE_UNPROCESSED);
    }

    /* freeze current paremeters for this action */
    pData->iDynaFileCacheSize = cs.iDynaFileCacheSize;
    pData->fCreateMode = cs.fCreateMode;
    pData->fDirCreateMode = cs.fDirCreateMode;
    pData->bCreateDirs = cs.bCreateDirs;
    pData->bFailOnChown = cs.bFailOnChown;
    pData->fileUID = cs.fileUID;
    pData->fileGID = cs.fileGID;
    pData->dirUID = cs.dirUID;
    pData->dirGID = cs.dirGID;
    pData->iZipLevel = cs.iZipLevel;
    pData->bFlushOnTXEnd = cs.bFlushOnTXEnd;
    pData->iIOBufSize = (int)cs.iIOBufSize;
    pData->iFlushInterval = cs.iFlushInterval;
    pData->bUseAsyncWriter = cs.bUseAsyncWriter;
    pData->bVeryRobustZip = 0; /* cannot be specified via legacy conf */
    pData->iCloseTimeout = 0; /* cannot be specified via legacy conf */
    setupInstStatsCtrs(pData);
    CODE_STD_FINALIZERparseSelectorAct
ENDparseSelectorAct


/**
 * @brief Resets configuration variables for the omfile module to default values.
 *
 * This function is used to revert module-global configuration settings
 * to their initial states, typically during `resetconfigvariables` processing.
 *
 * @param pp Unused pointer, kept for compatibility.
 * @param pVal Unused pointer, kept for compatibility.
 * @return RS_RET_OK always.
 */
static rsRetVal resetConfigVariables(uchar __attribute__((unused)) * pp, void __attribute__((unused)) * pVal) {
    cs.fileUID = -1;
    cs.fileGID = -1;
    cs.dirUID = -1;
    cs.dirGID = -1;
    cs.bFailOnChown = 1;
    cs.iDynaFileCacheSize = 10;
    cs.fCreateMode = 0644;
    cs.fDirCreateMode = 0700;
    cs.bCreateDirs = 1;
    cs.bEnableSync = 0;
    cs.iZipLevel = 0;
    cs.bFlushOnTXEnd = FLUSHONTX_DFLT;
    cs.iIOBufSize = IOBUF_DFLT_SIZE;
    cs.iFlushInterval = FLUSH_INTRVL_DFLT;
    cs.bUseAsyncWriter = USE_ASYNCWRITER_DFLT;
    free(pszFileDfltTplName);
    pszFileDfltTplName = NULL;
    return RS_RET_OK;
}


BEGINdoHUP
    CODESTARTdoHUP;
    pthread_mutex_lock(&pData->mutWrite);
    if (pData->bDynamicName) {
        dynaFileFreeCacheEntries(pData);
    } else {
        if (pData->pStrm != NULL) {
            closeFile(pData);
        }
    }
    pthread_mutex_unlock(&pData->mutWrite);
ENDdoHUP


BEGINmodExit
    CODESTARTmodExit;
    objRelease(strm, CORE_COMPONENT);
    objRelease(statsobj, CORE_COMPONENT);
    DESTROY_ATOMIC_HELPER_MUT(mutClock);
ENDmodExit


BEGINqueryEtryPt
    CODESTARTqueryEtryPt;
    CODEqueryEtryPt_STD_OMODTX_QUERIES;
    CODEqueryEtryPt_STD_OMOD8_QUERIES;
    CODEqueryEtryPt_STD_CONF2_QUERIES;
    CODEqueryEtryPt_STD_CONF2_setModCnf_QUERIES;
    CODEqueryEtryPt_STD_CONF2_OMOD_QUERIES;
    CODEqueryEtryPt_SetActionInfo_IF_OMOD_QUERIES;
    CODEqueryEtryPt_doHUP
ENDqueryEtryPt


BEGINmodInitNoPredecl(File)
    CODESTARTmodInit;
    *ipIFVersProvided = CURR_MOD_IF_VERSION; /* we only support the current interface specification */
    CODEmodInit_QueryRegCFSLineHdlr INITLegCnfVars;
    CHKiRet(objUse(strm, CORE_COMPONENT));
    CHKiRet(objUse(statsobj, CORE_COMPONENT));

    INIT_ATOMIC_HELPER_MUT(mutClock);

    INITChkCoreFeature(bCoreSupportsBatching, CORE_FEATURE_BATCHING);
    DBGPRINTF("omfile: %susing transactional output interface.\n", bCoreSupportsBatching ? "" : "not ");
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"dynafilecachesize", 0, eCmdHdlrInt, setDynaFileCacheSize, NULL,
                               STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"omfileziplevel", 0, eCmdHdlrInt, NULL, &cs.iZipLevel, STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"omfileflushinterval", 0, eCmdHdlrInt, NULL, &cs.iFlushInterval,
                               STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"omfileasyncwriting", 0, eCmdHdlrBinary, NULL, &cs.bUseAsyncWriter,
                               STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"omfileflushontxend", 0, eCmdHdlrBinary, NULL, &cs.bFlushOnTXEnd,
                               STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"omfileiobuffersize", 0, eCmdHdlrSize, NULL, &cs.iIOBufSize,
                               STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"dirowner", 0, eCmdHdlrUID, NULL, &cs.dirUID, STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"dirownernum", 0, eCmdHdlrInt, NULL, &cs.dirUID, STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"dirgroup", 0, eCmdHdlrGID, NULL, &cs.dirGID, STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"dirgroupnum", 0, eCmdHdlrInt, NULL, &cs.dirGID, STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"fileowner", 0, eCmdHdlrUID, NULL, &cs.fileUID, STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"fileownernum", 0, eCmdHdlrInt, NULL, &cs.fileUID, STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"filegroup", 0, eCmdHdlrGID, NULL, &cs.fileGID, STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"filegroupnum", 0, eCmdHdlrInt, NULL, &cs.fileGID, STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"dircreatemode", 0, eCmdHdlrFileCreateMode, NULL, &cs.fDirCreateMode,
                               STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"filecreatemode", 0, eCmdHdlrFileCreateMode, NULL, &cs.fCreateMode,
                               STD_LOADABLE_MODULE_ID));
    CHKiRet(
        omsdRegCFSLineHdlr((uchar *)"createdirs", 0, eCmdHdlrBinary, NULL, &cs.bCreateDirs, STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"failonchownfailure", 0, eCmdHdlrBinary, NULL, &cs.bFailOnChown,
                               STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"omfileforcechown", 0, eCmdHdlrGoneAway, NULL, NULL, STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"actionfileenablesync", 0, eCmdHdlrBinary, NULL, &cs.bEnableSync,
                               STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"actionfiledefaulttemplate", 0, eCmdHdlrGetWord, setLegacyDfltTpl, NULL,
                               STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"resetconfigvariables", 1, eCmdHdlrCustomHandler, resetConfigVariables, NULL,
                               STD_LOADABLE_MODULE_ID));
ENDmodInit
