view th_config.c @ 771:c17eadc60c3d

Rename th_ioctx struct to th_ioctx_t, for consistency. Breaks API.
author Matti Hamalainen <ccr@tnsp.org>
date Mon, 20 Feb 2023 23:33:45 +0200
parents 31bc1ed07cf5
children 4744e64ffa3a
line wrap: on
line source

/*
 * Very simple configuration handling functions
 * Programmed and designed by Matti 'ccr' Hamalainen
 * (C) Copyright 2004-2022 Tecnic Software productions (TNSP)
 *
 * Please read file 'COPYING' for information on license and distribution.
 */
#include "th_util.h"
#include "th_config.h"
#include "th_string.h"
#include <stdio.h>
#include <stdarg.h>


#define SET_MAX_BUF     (8192)


/* Deallocate a given configuration. Notice that the values are NOT freed,
 * unless a deallocator function is specified to free them.
 */
void th_cfg_free(th_cfgitem_t *cfg, void (*freefunc)(th_cfgitem_t *))
{
    th_cfgitem_t *node = cfg;

    while (node != NULL)
    {
        th_cfgitem_t *next = (th_cfgitem_t *) node->node.next;

        if (node->type == ITEM_SECTION)
            th_cfg_free((th_cfgitem_t *) node->v.data, freefunc);
        else
        if (freefunc != NULL)
            freefunc(node);

        th_free(node->name);
        th_free(node);
        node = next;
    }
}


/* Allocate and add new item to configuration
 */
static th_cfgitem_t *th_cfg_add(th_cfgitem_t **cfg, const char *name,
                             const int type, void *data)
{
    th_cfgitem_t *node;

    if (cfg == NULL)
        return NULL;

    // Allocate new item
    if ((node = (th_cfgitem_t *) th_malloc0(sizeof(th_cfgitem_t))) == NULL)
        return NULL;

    // Set values
    node->type = type;
    node->v.data = data;
    node->name = th_strdup(name);

    // Insert into linked list
    th_llist_append_node((th_llist_t **) cfg, (th_llist_t *) node);

    return node;
}


/* Add integer type setting into give configuration
 */
int th_cfg_add_int(th_cfgitem_t **cfg, const char *name, int *itemData, int defValue)
{
    th_cfgitem_t *node = th_cfg_add(cfg, name, ITEM_INT, (void *) itemData);
    if (node == NULL)
        return THERR_MALLOC;

    *itemData = defValue;

    return THERR_OK;
}


int th_cfg_add_hexvalue(th_cfgitem_t **cfg, const char *name,
                        unsigned int *itemData, unsigned int defValue)
{
    th_cfgitem_t *node = th_cfg_add(cfg, name, ITEM_HEX_TRIPLET, (void *) itemData);
    if (node == NULL)
        return THERR_MALLOC;

    *itemData = defValue;

    return THERR_OK;
}


/* Add unsigned integer type setting into give configuration
 */
int th_cfg_add_uint(th_cfgitem_t **cfg, const char *name,
                    unsigned int *itemData, unsigned int defValue)
{
    th_cfgitem_t *node = th_cfg_add(cfg, name, ITEM_UINT, (void *) itemData);
    if (node == NULL)
        return THERR_MALLOC;

    *itemData = defValue;

    return THERR_OK;
}


/* Add strint type setting into given configuration
 */
int th_cfg_add_string(th_cfgitem_t **cfg, const char *name,
                      char **itemData, char *defValue)
{
    th_cfgitem_t *node = th_cfg_add(cfg, name, ITEM_STRING, (void *) itemData);
    if (node == NULL)
        return THERR_MALLOC;

    *itemData = th_strdup(defValue);

    return THERR_OK;
}


/* Add boolean type setting into given configuration
 */
int th_cfg_add_bool(th_cfgitem_t **cfg, const char *name,
                    bool *itemData, bool defValue)
{
    th_cfgitem_t *node = th_cfg_add(cfg, name, ITEM_bool, (void *) itemData);
    if (node == NULL)
        return THERR_MALLOC;

    *itemData = defValue;

    return THERR_OK;
}


/* Add implicit comment
 */
int th_cfg_add_comment(th_cfgitem_t **cfg, const char *comment)
{
    th_cfgitem_t *node = th_cfg_add(cfg, comment, ITEM_COMMENT, NULL);
    if (node == NULL)
        return THERR_MALLOC;

    return THERR_OK;
}


/* Add new section
 */
int th_cfg_add_section(th_cfgitem_t **cfg, const char *name, th_cfgitem_t *sect)
{
    th_cfgitem_t *node = th_cfg_add(cfg, name, ITEM_SECTION, (void *) sect);
    if (node == NULL)
        return THERR_MALLOC;

    return THERR_OK;
}


int th_cfg_add_string_list(th_cfgitem_t **cfg, const char *name, th_llist_t **data)
{
    th_cfgitem_t *node;

    if (data == NULL)
        return THERR_NULLPTR;

    if ((node = th_cfg_add(cfg, name, ITEM_STRING_LIST, (void *) data)) == NULL)
        return THERR_MALLOC;

    return THERR_OK;
}


/* Read a given file into configuration structure and variables
 */
/// @cond
enum
{
    PM_EOF,
    PM_ERROR,
    PM_IDLE,
    PM_COMMENT,
    PM_NEXT,
    PM_KEYNAME,
    PM_KEYSET,
    PM_STRING,
    PM_NUMERIC,
    PM_bool,
    PM_SECTION,
    PM_LIST,

    PM_LAST
};


typedef struct
{
    int ch, strDelim,
        prevMode, nextMode, parseMode;
} th_cfgparserctx_t;

/// @endcond


static void th_cfg_set_parsemode(th_cfgparserctx_t *ctx, const int mode)
{
    ctx->prevMode = ctx->parseMode;
    ctx->parseMode = mode;
}


static void th_cfg_set_next_parsemode(th_cfgparserctx_t *ctx, const int mode)
{
    th_cfg_set_parsemode(ctx, PM_NEXT);
    ctx->nextMode = mode;
}


#define VADDCH(ch) if (strPos < SET_MAX_BUF) { tmpStr[strPos++] = ch; }


static bool th_cfg_is_end(const int ch)
{
    return
        ch == '\r' ||
        ch == '\n' ||
        ch == ';' ||
        ch == '#' ||
        th_isspace(ch);
}


static int th_cfg_get_parsemode(const int type)
{
    switch (type)
    {
        case ITEM_HEX_TRIPLET:
        case ITEM_STRING:
            return PM_STRING;

        case ITEM_INT:
        case ITEM_UINT:
        case ITEM_FLOAT:
            return PM_NUMERIC;

        case ITEM_bool:
            return PM_bool;

        case ITEM_SECTION:
            return PM_SECTION;

        case ITEM_STRING_LIST:
            return PM_LIST;

        default:
            return PM_ERROR;
    }
}


static int th_cfg_set_item(th_cfgparserctx_t *ctx, th_cfgitem_t *item, const char *str)
{
    bool res = true;

    switch (item->type)
    {
        case ITEM_HEX_TRIPLET:
            res  = th_get_hex_triplet(str, item->v.val_uint);
            break;

        case ITEM_STRING:
            th_pstr_cpy(item->v.val_str, str);
            break;

        case ITEM_STRING_LIST:
            {
                char *tmp;
                if ((tmp = th_strdup(str)) != NULL)
                {
                    th_llist_append(item->v.list, tmp);
                    th_cfg_set_next_parsemode(ctx, PM_LIST);

                    // Early exit as we set the parsemode here
                    return THERR_OK;
                }
                else
                    res = false;
            }
            break;

        case ITEM_INT:
            *(item->v.val_int) = atoi(str);
            break;

        case ITEM_UINT:
            *(item->v.val_uint) = atol(str);
            break;

        case ITEM_FLOAT:
            *(item->v.val_float) = atof(str);
            break;

        case ITEM_bool:
            res = th_get_boolean(str, item->v.val_bool);
            break;

        default:
            res = false;
            break;
    }

    th_cfg_set_parsemode(ctx, PM_IDLE);

    return res ? THERR_OK : THERR_INVALID_DATA;
}


static int th_cfg_read_sect(th_ioctx_t *fh, th_cfgitem_t *sect, int nesting)
{
    th_cfgparserctx_t ctx;
    th_cfgitem_t *item = NULL;
    char *tmpStr = NULL;
    size_t strPos;
    bool isEscaped, isStart, fpSet;
    int ret = THERR_OK;

    // Initialize values
    memset(&ctx, 0, sizeof(ctx));
    ctx.ch = -1;
    ctx.nextMode = ctx.prevMode = ctx.parseMode = PM_IDLE;
    isEscaped = fpSet = isStart = false;
    strPos = 0;

    if ((tmpStr = th_malloc(SET_MAX_BUF + 1)) == NULL)
        goto out;

    // Parse the configuration
    while (ctx.parseMode != PM_EOF && ctx.parseMode != PM_ERROR)
    {
        if (ctx.ch == -1)
        {
            // Get next character
            switch (ctx.ch = thfgetc(fh))
            {
            case EOF:
                if (ctx.parseMode != PM_IDLE)
                {
                    ret = th_io_error(fh, THERR_OUT_OF_DATA,
                        "Unexpected end of file.");
                    goto out;
                }

                ctx.parseMode = PM_EOF;
                break;

            case '\n':
                fh->line++;
            }
        }

        switch (ctx.parseMode)
        {
        case PM_COMMENT:
            // Comment parsing mode
            if (ctx.ch == '\n')
            {
                // End of line, end of comment
                th_cfg_set_parsemode(&ctx, ctx.prevMode);
            }
            ctx.ch = -1;
            break;

        case PM_IDLE:
            // Normal parsing mode
            if (ctx.ch == '#')
            {
                th_cfg_set_parsemode(&ctx, PM_COMMENT);
                ctx.ch = -1;
            }
            else
            if (th_cfg_is_end(ctx.ch))
            {
                ctx.ch = -1;
            }
            else
            if (ctx.ch == '}')
            {
                if (nesting > 0)
                {
                    // Check for validation errors
                    goto out;
                }
                else
                {
                    ret = th_io_error(fh, THERR_INVALID_DATA,
                        "Invalid nesting sequence encountered.");
                    goto out;
                }
            }
            else
            if (th_isalpha(ctx.ch))
            {
                // Start of key name found
                th_cfg_set_parsemode(&ctx, PM_KEYNAME);
                strPos = 0;
            }
            else
            {
                // Error! Invalid character found
                ret = th_io_error(fh, THERR_INVALID_DATA,
                    "Unexpected character '%c'.", ctx.ch);
                goto out;
            }
            break;

        case PM_KEYNAME:
            // Configuration KEY name parsing mode
            if (ctx.ch == '#')
            {
                // Start of comment
                th_cfg_set_parsemode(&ctx, PM_COMMENT);
                ctx.ch = -1;
            }
            else
            if (th_iscrlf(ctx.ch) || th_isspace(ctx.ch) || ctx.ch == '=')
            {
                // End of key name
                th_cfg_set_next_parsemode(&ctx, PM_KEYSET);
            }
            else
            if (th_isalnum(ctx.ch) || ctx.ch == '_' || ctx.ch == '-')
            {
                // Add to key name string
                VADDCH(ctx.ch)
                else
                {
                    // Error! Key name string too long!
                    ret = th_io_error(fh, THERR_INVALID_DATA,
                        "Config key name too long!");
                    goto out;
                }
                ctx.ch = -1;
                tmpStr[strPos] = 0;
            }
            else
            {
                // Error! Invalid character found
                tmpStr[strPos] = 0;
                ret = th_io_error(fh, THERR_INVALID_DATA,
                    "Unexpected character '%c' in key name '%s'.",
                    ctx.ch, tmpStr);
                goto out;
            }
            break;

        case PM_KEYSET:
            if (ctx.ch == '=')
            {
                // Find key from configuration
                bool found;
                tmpStr[strPos] = 0;

                for (item = sect, found = false; item != NULL && !found; )
                {
                    if (item->type != ITEM_COMMENT &&
                        item->name != NULL &&
                        strcmp(item->name, tmpStr) == 0)
                        found = true;
                    else
                        item = (th_cfgitem_t *) item->node.next;
                }

                // Check if key was found
                if (found)
                {
                    // Okay, set next mode
                    th_cfg_set_next_parsemode(&ctx, th_cfg_get_parsemode(item->type));
                    isStart = true;
                    fpSet = false;
                    strPos = 0;
                }
                else
                {
                    // Error! No configuration key by this name found
                    ret = th_io_error(fh, THERR_INVALID_DATA,
                        "No such configuration setting: '%s'.",
                        tmpStr);
                    goto out;
                }

                ctx.ch = -1;
            }
            else
            {
                // Error! '=' expected!
                ret = th_io_error(fh, THERR_INVALID_DATA,
                    "Unexpected character '%c', assignation '=' was expected.",
                    ctx.ch);
                goto out;
            }
            break;

        case PM_NEXT:
            // Search next item parsing mode
            if (ctx.ch == '#')
            {
                // Start of comment
                th_cfg_set_parsemode(&ctx, PM_COMMENT);
            }
            else
            if (th_isspace(ctx.ch) || th_iscrlf(ctx.ch))
            {
                // Ignore whitespaces and linechanges
                ctx.ch = -1;
            }
            else
            {
                // Next item found
                th_cfg_set_parsemode(&ctx, ctx.nextMode);
            }
            break;

        case PM_LIST:
            if (isStart)
            {
                th_cfg_set_parsemode(&ctx, PM_STRING);
            }
            else
            if (ctx.ch == ',')
            {
                th_cfg_set_next_parsemode(&ctx, PM_STRING);
                ctx.ch = -1;
                isStart = true;
            }
            else
            {
                th_cfg_set_parsemode(&ctx, PM_IDLE);
            }
            break;

        case PM_SECTION:
            // Section parsing mode
            if (ctx.ch != '{')
            {
                // Error! Section start '{' expected!
                ret = th_io_error(fh, THERR_INVALID_DATA,
                    "Unexpected character '%c', section start '{' was expected.",
                    ctx.ch);
                goto out;
            }
            else
            {
                if ((ret = th_cfg_read_sect(fh, item->v.section, nesting + 1)) != THERR_OK)
                    goto out;

                th_cfg_set_parsemode(&ctx, PM_IDLE);
                ctx.ch = -1;
            }
            break;

        case PM_STRING:
            // String parsing mode
            if (isStart)
            {
                // Start of string, get delimiter
                ctx.strDelim = ctx.ch;
                isStart = false;
                strPos = 0;
                isEscaped = false;
            }
            else
            if (!isEscaped && ctx.ch == ctx.strDelim)
            {
                // End of string, set the value
                tmpStr[strPos] = 0;
                if ((ret = th_cfg_set_item(&ctx, item, tmpStr)) != THERR_OK)
                    goto out;
            }
            else
            if (!isEscaped && ctx.ch == '\\')
            {
                // Escape sequence
                isEscaped = true;
            }
            else
            {
                // Add character to string
                VADDCH(ctx.ch)
                else
                {
                    // Error! String too long!
                    ret = th_io_error(fh, THERR_INVALID_DATA,
                        "String too long! Maximum is %d characters.",
                        SET_MAX_BUF);
                    goto out;
                }
                isEscaped = false;
            }

            ctx.ch = -1;
            break;

        case PM_NUMERIC:
            // Integer parsing mode
            if (isStart && item->type == ITEM_UINT && ctx.ch == '-')
            {
                // Error! Negative values not allowed for unsigned ints
                ret = th_io_error(fh, THERR_INVALID_DATA,
                    "Negative value specified for %s, unsigned value expected.",
                    item->name);
                goto out;
            }
            else
            if (isStart && (ctx.ch == '-' || ctx.ch == '+'))
            {
                VADDCH(ctx.ch)
                else
                ret = THERR_INVALID_DATA;
            }
            else
            if (isStart && item->type == ITEM_FLOAT && ctx.ch == '.')
            {
                fpSet = true;
                VADDCH('0')
                else
                ret = THERR_INVALID_DATA;

                VADDCH(ctx.ch)
                else
                ret = THERR_INVALID_DATA;
            }
            else
            if (item->type == ITEM_FLOAT && ctx.ch == '.' && !fpSet)
            {
                fpSet = true;
                VADDCH(ctx.ch)
                else
                ret = THERR_INVALID_DATA;
            }
            else
            if (th_isdigit(ctx.ch))
            {
                VADDCH(ctx.ch)
                else
                ret = THERR_INVALID_DATA;
            }
            else
            if (th_cfg_is_end(ctx.ch))
            {
                // End of integer parsing mode
                tmpStr[strPos] = 0;

                if ((ret = th_cfg_set_item(&ctx, item, tmpStr)) != THERR_OK)
                    goto out;

                th_cfg_set_parsemode(&ctx, PM_IDLE);
            }
            else
            {
                // Error! Unexpected character.
                ret = th_io_error(fh, THERR_INVALID_DATA,
                    "Unexpected character '%c' for numeric setting '%s'.",
                    ctx.ch, item->name);
                goto out;
            }

            if (ret != THERR_OK)
            {
                // Error! String too long!
                ret = th_io_error(fh, ret,
                    "String too long! Maximum is %d characters.",
                    SET_MAX_BUF);
                goto out;
            }

            isStart = false;
            ctx.ch = -1;
            break;

        case PM_bool:
            // Boolean parsing mode
            if (isStart)
            {
                isStart = false;
                strPos = 0;
            }

            if (th_isalnum(ctx.ch))
            {
                VADDCH(ctx.ch)
                else
                ret = THERR_INVALID_DATA;
            }
            else
            if (th_cfg_is_end(ctx.ch))
            {
                tmpStr[strPos] = 0;
                ret = th_cfg_set_item(&ctx, item, tmpStr);
            }

            if (ret != THERR_OK)
            {
                ret = th_io_error(fh, ret,
                    "Invalid boolean value for '%s'.",
                    item->name);
                goto out;
            }
            ctx.ch = -1;
            break;
        }
    }

out:
    th_free(tmpStr);

    // Return result
    if (ret == THERR_OK && ctx.parseMode == PM_ERROR)
        ret = THERR_INVALID_DATA;

    return ret;
}


int th_cfg_read(th_ioctx_t *fh, th_cfgitem_t *cfg)
{
    if (fh == NULL || cfg == NULL)
        return THERR_NULLPTR;

    return th_cfg_read_sect(fh, cfg, 0);
}


/* Write a configuration into file
 */
static bool th_print_indent_a(th_ioctx_t *fh, const int nesting, const char *fmt, va_list ap)
{
    for (int i = 0; i < nesting * 4; i++)
    {
        if (thfputc(' ', fh) == EOF)
            return false;
    }

    return thvfprintf(fh, fmt, ap) >= 0;
}


static bool th_print_indent(th_ioctx_t *fh, const int nesting, const char *fmt, ...)
{
    bool ret;
    va_list ap;
    va_start(ap, fmt);
    ret = th_print_indent_a(fh, nesting, fmt, ap);
    va_end(ap);
    return ret;
}


static bool th_cfg_is_item_valid(const th_cfgitem_t *item)
{
    switch (item->type)
    {
        case ITEM_STRING:
            return (*(item->v.val_str) != NULL);

        case ITEM_STRING_LIST:
            return (*(item->v.list) != NULL);

        case ITEM_SECTION:
            return true;

        case ITEM_INT:
        case ITEM_UINT:
        case ITEM_FLOAT:
        case ITEM_bool:
        case ITEM_HEX_TRIPLET:
            return true;

        default:
            return false;
    }
}


static bool th_cfg_write_string_escaped(th_ioctx_t *fh, const char *str, const char delim)
{
    for (const char *ptr = str; *ptr; ptr++)
    {
        if (*ptr == delim)
        {
            if (thfputc('\\', fh) == EOF ||
                thfputc(*ptr, fh) == EOF)
                return false;
        }
        else
        {
            if (thfputc(*ptr, fh) == EOF)
                return false;
        }
    }

    return true;
}


static int th_cfg_write_item(th_ioctx_t *fh, const th_cfgitem_t *item)
{
    switch (item->type)
    {
        case ITEM_STRING:
            if (thfputc('"', fh) != EOF &&
                th_cfg_write_string_escaped(fh, *(item->v.val_str), '"') &&
                thfputc('"', fh) != EOF)
                return 1;
            else
                return -1;

        case ITEM_INT:
            return thfprintf(fh, "%d", *(item->v.val_int));

        case ITEM_UINT:
            return thfprintf(fh, "%u", *(item->v.val_uint));

        case ITEM_FLOAT:
            return thfprintf(fh, "%1.5f", *(item->v.val_float));

        case ITEM_bool:
            return thfprintf(fh, "%s", *(item->v.val_bool) ? "yes" : "no");

        case ITEM_HEX_TRIPLET:
            return thfprintf(fh, "\"%06x\"", *(item->v.val_int));

        default:
            return -1;
    }
}


static int th_cfg_write_sect(th_ioctx_t *fh, const th_cfgitem_t *item, const int nesting)
{
    while (item != NULL)
    {
        if (item->name == NULL)
            return THERR_NULLPTR;

        if (item->type == ITEM_COMMENT)
        {
            bool lineStart = true, lineFeed = false;

            for (const char *ptr = item->name; *ptr; ptr++)
            switch (*ptr)
            {
                case '\r':
                case '\n':
                    lineStart = lineFeed = true;
                    break;

                default:
                    if (lineFeed)
                    {
                        thfprintf(fh, "\n");
                        lineFeed = false;
                    }
                    if (lineStart)
                    {
                        if (!th_print_indent(fh, nesting, "# "))
                            return THERR_FWRITE;

                        lineStart = false;
                    }

                    if (thfputc(*ptr, fh) == EOF)
                        return THERR_FWRITE;
            }

            if (!lineFeed)
                thfprintf(fh, "\n");
        }
        else
        if (item->type == ITEM_SECTION)
        {
            int res;

            if (!th_print_indent(fh, nesting, "%s = {\n", item->name))
                return THERR_FWRITE;

            if ((res = th_cfg_write_sect(fh, item->v.section, nesting + 1)) != 0)
                return res;

            if (!th_print_indent(fh, nesting, "}\n\n"))
                return THERR_FWRITE;
        }
        else
        if (item->type == ITEM_STRING_LIST)
        {
            if (!th_cfg_is_item_valid(item))
            {
                if (!th_print_indent(fh, nesting, "#%s = \"\", \"\"", item->name))
                    return THERR_FWRITE;
            }
            else
            {
                th_llist_t *node = *(item->v.list);
                size_t n = th_llist_length(node);

                if (!th_print_indent(fh, nesting, "%s = \n", item->name))
                    return THERR_FWRITE;

                for (; node != NULL; node = node->next)
                if (node->data != NULL)
                {
                    if (!th_print_indent(fh, nesting, "\"") ||
                        !th_cfg_write_string_escaped(fh, (char *) node->data, '"') ||
                        thfprintf(fh, "\"%s\n", (--n > 0) ? "," : "") < 0)
                        return THERR_FWRITE;
                }

                if (!th_print_indent(fh, nesting, "\n"))
                    return THERR_FWRITE;
            }
        }
        else
        {
            if (!th_print_indent(fh, nesting, "%s%s = ",
                    th_cfg_is_item_valid(item) ? "" : "#",
                    item->name) ||
                th_cfg_write_item(fh, item) < 0 ||
                thfprintf(fh, "\n") < 0)
                return THERR_FWRITE;
        }

        item = (th_cfgitem_t *) item->node.next;
    }

    return THERR_OK;
}


int th_cfg_write(th_ioctx_t *fh, const th_cfgitem_t *cfg)
{
    if (fh == NULL || cfg == NULL)
        return THERR_NULLPTR;

    thfprintf(fh, "# Configuration written by %s %s\n\n",
            th_prog_desc, th_prog_version);

    return th_cfg_write_sect(fh, cfg, 0);
}


/* Find a configuration item based on section and/or name (and/or) type.
 * The first matching item will be returned.
 */
static th_cfgitem_t *th_cfg_find_do(th_cfgitem_t *item, const char *name, const int type)
{
    while (item != NULL)
    {
        bool match = true;

        // Has type check been set, and does it match?
        if (type != -1 && item->type != type)
            match = false;

        // Check item name
        if (name != NULL && item->name != NULL &&
            strcmp(name, item->name) != 0)
            match = false;

        // Recurse to section
        if (!match && item->type == ITEM_SECTION)
        {
            th_cfgitem_t *tmp = th_cfg_find_do(item->v.section, name, type);
            if (tmp != NULL)
                return tmp;
        }

        // Do we have a match?
        if (match)
            return item;

        item = (th_cfgitem_t *) item->node.next;
    }

    return NULL;
}


th_cfgitem_t *th_cfg_find(th_cfgitem_t *cfg, const char *section, const char *name, const int type)
{
    th_cfgitem_t *node;

    if (section != NULL)
        node = th_cfg_find_do(cfg, section, ITEM_SECTION);
    else
        node = cfg;

    return th_cfg_find_do(node, name, type);
}