view th_regex.c @ 643:a2bf1ea05b05

Cleanups.
author Matti Hamalainen <ccr@tnsp.org>
date Fri, 24 Jan 2020 12:06:43 +0200
parents 3a35db5c1873
children b897995101b7
line wrap: on
line source

/*
 * Simple regular expression matching functionality
 * Programmed and designed by Matti 'ccr' Hamalainen
 * (C) Copyright 2020 Tecnic Software productions (TNSP)
 *
 * Please read file 'COPYING' for information on license and distribution.
 */
#include "th_util.h"
#include "th_regex.h"


#ifdef TH_EXPERIMENTAL_REGEX_DEBUG
#    define DBG_RE_COMPILE(...) do { if (th_dbg_re_flags & TH_DBG_RE_COMPILE) fprintf(stdout, __VA_ARGS__); } while (0)
#    define DBG_RE_FREE(...)    do { if (th_dbg_re_flags & TH_DBG_RE_FREE) fprintf(stdout, __VA_ARGS__); } while (0)
#    define DBG_RE_MATCH(...)   do { if (th_dbg_re_flags & TH_DBG_RE_MATCH) fprintf(stdout, __VA_ARGS__); } while (0)

int th_dbg_re_flags = 0;

static const char *re_match_modes[] =
{
    "ONCE",
    "COUNT",
    "ANCHOR START",
    "ANCHOR END",
};

static const char *re_match_types[] =
{
    "CHAR",
    "ANY",
    "LIST",
    "LIST REVERSE",
    "SUBEXPR",
};

#else
#    define DBG_RE_COMPILE(...)
#    define DBG_RE_FREE(...)
#    define DBG_RE_MATCH(...)
#endif


#ifdef TH_EXPERIMENTAL_REGEX

typedef struct
{
    int type;
    th_regex_char_t start, end;

    size_t nchars;
    th_regex_char_t *chars;
} th_regex_list_item_t;


typedef struct
{
    size_t nitems, itemssize;
    th_regex_list_item_t *items;
} th_regex_list_t;


typedef struct
{
    int mode, type;
    ssize_t repeatMin, repeatMax;

    struct {
        th_regex_char_t chr;
        th_regex_list_t list;
        struct th_regex_t *expr;
    } match;
} th_regex_node_t;


struct th_regex_t
{
    size_t nnodes, nodessize;
    th_regex_node_t *nodes;
};


enum
{
    TH_RE_MATCH_ONCE,
    TH_RE_MATCH_COUNT,

    TH_RE_MATCH_ANCHOR_START,
    TH_RE_MATCH_ANCHOR_END,
};


enum
{
    TH_RE_TYPE_CHAR,
    TH_RE_TYPE_ANY_CHAR,
    TH_RE_TYPE_LIST,
    TH_RE_TYPE_LIST_REVERSE,
    TH_RE_TYPE_SUBEXPR,
};


typedef struct
{
    const th_regex_char_t *pattern;
    size_t offs;

    th_regex_t *data;

    size_t nstack, stacksize;
    th_regex_t **stack;
} th_regex_parse_ctx_t;


static void th_regex_node_init(th_regex_node_t *node)
{
    memset(node, 0, sizeof(th_regex_node_t));
    node->mode = TH_RE_MATCH_ONCE;
}


static int th_regex_strndup(th_regex_char_t **pdst,
    const th_regex_char_t *src, const size_t len)
{
    if (pdst == NULL)
        return THERR_NULLPTR;

    if (UINTPTR_MAX / sizeof(th_regex_char_t) < len + 1)
        return THERR_BOUNDS;

    if ((*pdst = (th_regex_char_t *)
        th_malloc((len + 1) * sizeof(th_regex_char_t))) == NULL)
        return THERR_MALLOC;

    memcpy(*pdst, src, len * sizeof(th_regex_char_t));
    (*pdst)[len] = 0;

    return THERR_OK;
}


static int th_regex_parse_ctx_get_prev_node(
    th_regex_parse_ctx_t *ctx, th_regex_node_t **pnode)
{
    if (ctx->data != NULL && ctx->data->nnodes > 0)
    {
        *pnode = &ctx->data->nodes[ctx->data->nnodes - 1];
        return THERR_OK;
    }
    else
        return THERR_INVALID_DATA;
}


static int th_regex_parse_ctx_push(th_regex_parse_ctx_t *ctx)
{
    if (ctx->stack == NULL || ctx->nstack + 1 >= ctx->stacksize)
    {
        ctx->stacksize += 16;

        if ((ctx->stack = th_realloc(ctx->stack,
            ctx->stacksize * sizeof(th_regex_node_t *))) == NULL)
            return THERR_MALLOC;
    }

    ctx->stack[ctx->nstack] = ctx->data;
    ctx->nstack++;
    ctx->data = NULL;

    return THERR_OK;
}


static int th_regex_parse_ctx_pop(th_regex_parse_ctx_t *ctx, th_regex_t **data)
{
    if (ctx->nstack > 0)
    {
        *data = ctx->data;
        ctx->nstack--;
        ctx->data = ctx->stack[ctx->nstack];
        return THERR_OK;
    }
    else
        return THERR_INVALID_DATA;
}


static int th_regex_parse_ctx_node_commit(th_regex_parse_ctx_t *ctx, th_regex_node_t *node)
{
    th_regex_t *data = ctx->data;

    if (data == NULL)
    {
        if ((data = ctx->data = th_malloc0(sizeof(th_regex_t))) == NULL)
            return THERR_MALLOC;
    }

    if (data->nodes == NULL || data->nnodes + 1 >= data->nodessize)
    {
        data->nodessize += 16;
        if ((data->nodes = th_realloc(data->nodes,
            data->nodessize * sizeof(th_regex_node_t))) == NULL)
            return THERR_MALLOC;
    }

    memcpy(&data->nodes[data->nnodes], node, sizeof(th_regex_node_t));
    data->nnodes++;

    DBG_RE_COMPILE(
        "node [%" PRIu_SIZE_T " / %" PRIu_SIZE_T "]: "
        "mode=%d, type=%d, rmin=%" PRId_SSIZE_T ", rmax=%" PRId_SSIZE_T "\n",
        data->nnodes, data->nodessize,
        node->mode, node->type,
        node->repeatMin, node->repeatMax);

    return THERR_OK;
}


static BOOL th_regex_find_next(const th_regex_char_t *str,
    const size_t start, size_t *offs,
    const th_regex_char_t delim)
{
    for (*offs = start; str[*offs] != 0; (*offs)++)
    {
        if (str[*offs] == delim)
            return TRUE;
    }
    return FALSE;
}


static BOOL th_regex_parse_ssize_t(const th_regex_char_t *str,
    ssize_t *value)
{
    th_regex_char_t ch;
    BOOL neg;

    if (*str == '-')
    {
        str++;
        neg = TRUE;
    }
    else
        neg = FALSE;

    // Is the value negative?
    while ((ch = *str++))
    {
        if (ch >= '0' && ch <= '9')
        {
            *value *= 10;
            *value += ch - '0';
        }
        else
            return FALSE;
    }

    if (neg)
        *value = -(*value);

    return TRUE;
}


static void th_regex_list_item_init(th_regex_list_item_t *item)
{
    memset(item, 0, sizeof(th_regex_list_item_t));
}


static int th_regex_list_add_item(th_regex_list_t *list, th_regex_list_item_t *item)
{
    if (list->items == NULL || list->nitems + 1 >= list->itemssize)
    {
        list->itemssize += 16;

        if ((list->items = th_realloc(list->items,
            list->itemssize * sizeof(th_regex_list_item_t))) == NULL)
            return THERR_MALLOC;
    }

    memcpy(&list->items[list->nitems], item, sizeof(th_regex_list_item_t));
    list->nitems++;

    return THERR_OK;
}


static void th_regex_list_free(th_regex_list_t *list)
{
    if (list != NULL)
    {
        for (size_t n = 0; n < list->nitems; n++)
        {
            th_free(list->items[n].chars);
        }
        th_free(list->items);
    }
}


static int th_regex_parse_list(const th_regex_char_t *str,
    const size_t slen, th_regex_list_t *list)
{
    th_regex_char_t *tmp = NULL;
    th_regex_list_item_t item;
    int res;

    if ((res = th_regex_strndup(&tmp, str, slen)) != THERR_OK)
        goto out;

    // Handle ranges like [A-Z]
    for (size_t offs = 0; offs < slen; offs++)
    {
        th_regex_char_t
            *prev = (offs > 0) ? tmp + offs - 1 : NULL,
            *curr = tmp + offs,
            *next = (offs + 1 < slen) ? tmp + offs + 1 : NULL;

        if (*curr == '-')
        {
            if (prev != NULL && next != NULL)
            {
                // Range
                th_regex_list_item_init(&item);
                item.type = 1;
                item.start = *prev;
                item.end = *next;

                if (item.start <= item.end)
                {
                    res = THERR_INVALID_DATA;
                    goto out;
                }

                *curr = *prev = *next = 0;

                if ((res = th_regex_list_add_item(list, &item)) != THERR_OK)
                    goto out;
            }
            else
            if (next != NULL)
            {
                res = THERR_INVALID_DATA;
                goto out;
            }
        }
    }

    // Count number of remaining characters
    th_regex_list_item_init(&item);
    item.type = 0;
    item.nchars = 0;

    for (size_t offs = 0; offs < slen; offs++)
    {
        th_regex_char_t curr = tmp[offs];
        if (curr != 0)
            item.nchars++;
    }

    if (item.nchars > 0)
    {
        if ((item.chars = th_malloc(sizeof(th_regex_char_t) * item.nchars)) == NULL)
        {
            res = THERR_MALLOC;
            goto out;
        }

        for (size_t offs = 0, n = 0; offs < slen; offs++)
        {
            th_regex_char_t curr = tmp[offs];
            if (curr != 0)
            {
                item.chars[n] = curr;
                n++;
            }
        }

        if ((res = th_regex_list_add_item(list, &item)) != THERR_OK)
        {
            th_free(item.chars);
            goto out;
        }
    }

out:
    th_free(tmp);
    return res;
}


int th_regex_compile(th_regex_t **pexpr, const th_regex_char_t *pattern)
{
    int res = THERR_OK;
    th_regex_parse_ctx_t ctx;
    th_regex_node_t node, *pnode;
    th_regex_char_t *tmp = NULL;
    size_t start;

    if (pexpr == NULL || pattern == NULL)
    {
        res = THERR_NULLPTR;
        goto exit;
    }

    memset(&ctx, 0, sizeof(ctx));
    ctx.pattern = pattern;

    for (; ctx.pattern[ctx.offs] != 0; ctx.offs++)
    {
        th_regex_char_t cch = ctx.pattern[ctx.offs];
        DBG_RE_COMPILE("[%" PRIu_SIZE_T "] '%c'\n", ctx.offs, cch);
        switch (cch)
        {
            case '?':
            case '*':
            case '+':
                if ((res = th_regex_parse_ctx_get_prev_node(&ctx, &pnode)) != THERR_OK)
                    goto exit;

                if (cch == '?')
                {
                    // Previous token is optional (repeat 0-1 times) (non-greedy matching)
                    pnode->mode = TH_RE_MATCH_COUNT;
                    pnode->repeatMin = 0;
                    pnode->repeatMax = 1;
                }
                else
                {
                    // Check if previous was a count ("**", "*+", etc.)
                    if (pnode->mode == TH_RE_MATCH_COUNT)
                    {
                        res = THERR_INVALID_DATA;
                        goto exit;
                    }

                    pnode->mode = TH_RE_MATCH_COUNT;

                    if (cch == '*')
                    {
                        // Previous token can repeat 0 or more times
                        pnode->repeatMin = 0;
                        pnode->repeatMax = -1;
                    }
                    else
                    {
                        // Previous token must repeat 1 or more times
                        pnode->repeatMin = 1;
                        pnode->repeatMax = -1;
                    }
                }
                break;

            case '{':
                // {n} | {min,max}
                start = ctx.offs + 1;
                if (!th_regex_find_next(ctx.pattern, start, &ctx.offs, '}'))
                {
                    // End not found
                    res = THERR_INVALID_DATA;
                    goto exit;
                }

                th_free(tmp);

                if ((res = th_regex_parse_ctx_get_prev_node(&ctx, &pnode)) != THERR_OK ||
                    (res = th_regex_strndup(&tmp, ctx.pattern + start,
                    ctx.offs - start)) != THERR_OK)
                    goto exit;

                pnode->mode = TH_RE_MATCH_COUNT;

                if (th_regex_find_next(tmp, 0, &start, ','))
                {
                    tmp[start] = 0;
                    if (!th_regex_parse_ssize_t(tmp, &pnode->repeatMin) ||
                        !th_regex_parse_ssize_t(tmp + start + 1, &pnode->repeatMax))
                    {
                        res = THERR_INVALID_DATA;
                        goto exit;
                    }
                }
                else
                {
                    if (!th_regex_parse_ssize_t(tmp, &pnode->repeatMin))
                    {
                        res = THERR_INVALID_DATA;
                        goto exit;
                    }
                    pnode->repeatMax = pnode->repeatMin;
                }

                if (pnode->repeatMin < 0 || pnode->repeatMax < 1 ||
                    pnode->repeatMax < pnode->repeatMin)
                {
                    // Invalid repeat counts
                    res = THERR_INVALID_DATA;
                    goto exit;
                }
                break;

            case '(':
                // Start of subpattern
                if ((res = th_regex_parse_ctx_push(&ctx)) != THERR_OK)
                    goto exit;
                break;

            case ')':
                // End of subpattern
                th_regex_node_init(&node);
                node.type = TH_RE_TYPE_SUBEXPR;

                if ((res = th_regex_parse_ctx_pop(&ctx, &node.match.expr)) != THERR_OK ||
                    (res = th_regex_parse_ctx_node_commit(&ctx, &node)) != THERR_OK)
                    goto exit;
                break;

            case '^':
                // Start of line anchor
                th_regex_node_init(&node);
                node.mode = TH_RE_MATCH_ANCHOR_START;

                if ((res = th_regex_parse_ctx_node_commit(&ctx, &node)) != THERR_OK)
                    goto exit;
                break;

            case '$':
                // End of line anchor
                th_regex_node_init(&node);
                node.mode = TH_RE_MATCH_ANCHOR_END;

                if ((res = th_regex_parse_ctx_node_commit(&ctx, &node)) != THERR_OK)
                    goto exit;
                break;

            case '[':
                // Start of char list
                start = ctx.offs + 1;
                if (!th_regex_find_next(ctx.pattern, start, &ctx.offs, ']') ||
                    ctx.offs == start)
                {
                    res = THERR_INVALID_DATA;
                    goto exit;
                }

                th_regex_node_init(&node);
                if (ctx.pattern[start] == '^')
                {
                    node.type = TH_RE_TYPE_LIST_REVERSE;
                    start++;
                }
                else
                    node.type = TH_RE_TYPE_LIST;

                if ((res = th_regex_parse_list(ctx.pattern + start,
                    ctx.offs - start, &node.match.list) != THERR_OK) ||
                    (res = th_regex_parse_ctx_node_commit(&ctx, &node)) != THERR_OK)
                    goto exit;
                break;

            case '.':
                // Any single character matches
                th_regex_node_init(&node);
                node.type = TH_RE_TYPE_ANY_CHAR;

                if ((res = th_regex_parse_ctx_node_commit(&ctx, &node)) != THERR_OK)
                    goto exit;
                break;

            case '\\':
                // Literal escape
                ctx.offs++;
                if (ctx.pattern[ctx.offs] == 0)
                {
                    // End of pattern, error
                    res = THERR_INVALID_DATA;
                    goto exit;
                }
                // fall-through

            default:
                // Given character must match
                th_regex_node_init(&node);
                node.type = TH_RE_TYPE_CHAR;
                node.match.chr = ctx.pattern[ctx.offs];

                if ((res = th_regex_parse_ctx_node_commit(&ctx, &node)) != THERR_OK)
                    goto exit;
                break;
        }
    }

exit:
    *pexpr = ctx.data;
    th_free(tmp);
    return res;
}


void th_regex_free(th_regex_t *expr)
{
    if (expr != NULL)
    {
        for (size_t nnode = 0; nnode < expr->nnodes; nnode++)
        {
            th_regex_node_t *node = &expr->nodes[nnode];

            DBG_RE_FREE(
                "node [%" PRIu_SIZE_T " / %" PRIu_SIZE_T "]: "
                "mode=%s, type=%s, rmin=%" PRId_SSIZE_T ", rmax=%" PRId_SSIZE_T "\n",
                nnode, expr->nnodes,
                re_match_modes[node->mode],
                re_match_types[node->type],
                node->repeatMin, node->repeatMax);

            th_regex_free(node->match.expr);
            th_regex_list_free(&node->match.list);
        }

        th_free(expr->nodes);
    }
}


static BOOL th_regex_match_list(const th_regex_list_t *list, const th_regex_char_t cch)
{
    // Could be optimized, perhaps .. sort match.chars, binary search etc?
    for (size_t nitem = 0; nitem < list->nitems; nitem++)
    {
        const th_regex_list_item_t *item = &list->items[nitem];

        if (item->type == 0)
        {
            for (size_t n = 0; n < item->nchars; n++)
            if (item->chars[n] == cch)
                return TRUE;
        }
        else
        {
            if (cch >= item->start && cch <= item->end)
                return TRUE;
        }
    }

    return FALSE;
}


static BOOL th_regex_match_expr(
    const th_regex_char_t *haystack,
    size_t *poffs,
    const th_regex_t *expr,
    const size_t startnode,
    const int flags
    );


static BOOL th_regex_match_one(
    const th_regex_char_t *haystack,
    size_t *poffs,
    const th_regex_node_t *node,
    const int flags
    )
{
    th_regex_char_t cch;
    BOOL res = FALSE;

    switch (node->type)
    {
        case TH_RE_TYPE_SUBEXPR:
            // push current context in stack
            // new context for subexpr
            res = th_regex_match_expr(haystack, poffs, node->match.expr, 0, flags);
            break;

        case TH_RE_TYPE_LIST:
        case TH_RE_TYPE_LIST_REVERSE:
            if ((cch = haystack[*poffs]) == 0)
                res = FALSE;
            else
            {
                res = th_regex_match_list(&node->match.list, cch);

                if (node->type == TH_RE_TYPE_LIST_REVERSE)
                    res = !res;

                (*poffs)++;
            }
            break;

        case TH_RE_TYPE_ANY_CHAR:
            if ((cch = haystack[*poffs]) == 0)
                res = FALSE;
            else
            {
                res = TRUE;
                (*poffs)++;
            }
            break;

        case TH_RE_TYPE_CHAR:
            if ((cch = haystack[*poffs]) == 0)
                res = FALSE;
            else
            {
                res = (cch == node->match.chr);
                (*poffs)++;
            }
            break;
    }

    return res;
}


static BOOL th_regex_match_expr(
    const th_regex_char_t *haystack,
    size_t *poffs,
    const th_regex_t *expr,
    const size_t startnode,
    const int flags
    )
{
    BOOL res = TRUE;
    size_t soffs = *poffs;

    for (size_t nnode = startnode; res && nnode < expr->nnodes; nnode++)
    {
        const th_regex_node_t *node = &expr->nodes[nnode];

        DBG_RE_MATCH("  expr [%" PRIu_SIZE_T "/%" PRIu_SIZE_T "]: %s %s '%s'\n",
            nnode + 1, expr->nnodes,
            re_match_modes[node->mode], re_match_types[node->type],
            haystack + soffs);

        switch (node->mode)
        {
            case TH_RE_MATCH_ONCE:
                res = th_regex_match_one(haystack, &soffs, node, flags);
                break;

            case TH_RE_MATCH_COUNT:
                {
                    ssize_t count = 0;

                    DBG_RE_MATCH("    COUNT min=%" PRId_SSIZE_T ", max=%" PRId_SSIZE_T "\n",
                        node->repeatMin, node->repeatMax);

                    do
                    {
                        BOOL match;
                        size_t toffs = soffs, tnode;

                        DBG_RE_MATCH("    START '%s'\n", haystack + toffs);
                        do {
                            match = TRUE;
                            for (tnode = nnode; match && tnode < expr->nnodes && haystack[toffs] != 0; )
                            {
                                match = th_regex_match_one(haystack, &toffs, &expr->nodes[tnode], flags);
                                if (match)
                                    tnode++;
                            }
                            DBG_RE_MATCH("    '%s': %d (tnode=%" PRIu_SIZE_T ")\n", haystack + toffs, match, tnode);
                            if (node->repeatMin >= 0 && match)
                                break;
                        } while (!match && haystack[toffs] != 0);

                        DBG_RE_MATCH("    END '%s': %d (tnode=%" PRIu_SIZE_T ")\n", haystack + toffs, match, tnode);

                        if (match)
                        {
                            // Node matched
                            count++;
                            soffs = toffs;
                            nnode = tnode;
                            res = (node->repeatMax > 0 && count >= node->repeatMax);
                        }
                        else
                        {
                            // Node did not match, check if we got the minimum if set
                            res = (node->repeatMin >= 0 && count >= node->repeatMin);
                        }

                    } while (!res);

                    DBG_RE_MATCH("    RESULT: count=%" PRId_SSIZE_T ", done=%s\n",
                        count, res ? "YES" : "NO");
                }
                break;

            case TH_RE_MATCH_ANCHOR_START:
                res = (soffs == 0);
                break;

            case TH_RE_MATCH_ANCHOR_END:
                res = (haystack[soffs] == 0);
                break;
        }
    }

    if (res)
        *poffs = soffs;

    return res;
}


int th_regex_match(const th_regex_t *expr, const th_regex_char_t *haystack,
    size_t *pnmatches, th_regex_match_t **pmatches, const size_t maxmatches,
    const int flags)
{
    size_t nmatches = 0;
    (void) flags;

    if (pnmatches != NULL)
        *pnmatches = 0;

    // Check given pattern and string
    if (expr == NULL || haystack == NULL)
        return THERR_NULLPTR;

    // Start matching
    // XXX NOTE .. lots to think about and to take into account:
    // - anchored and unanchored expressions
    // - how to check if the expression has consumed all possibilities?
    // ..
    for (size_t soffs = 0; haystack[soffs] != 0; )
    {
        size_t coffs = soffs;

        DBG_RE_MATCH("\nTRY_MATCH @ startoffs=%" PRIu_SIZE_T ": '%s'\n",
            soffs, haystack + soffs);

        if (th_regex_match_expr(haystack, &coffs, expr, 0, flags))
        {
            // A match was found, increase count
            nmatches++;

            // Deliver to caller if required
            if (pnmatches != NULL)
                *pnmatches = nmatches;

            if (pmatches != NULL)
            {
                th_regex_match_t *match = th_malloc0(sizeof(th_regex_match_t));
                if (match == NULL)
                    return THERR_MALLOC;

                match->start = soffs;
                match->len   = coffs - soffs;

                th_llist_append_node((th_llist_t **) pmatches, (th_llist_t *) match);
            }

            // Check match count limit, if set
            if (maxmatches > 0 && nmatches >= maxmatches)
                break;

            // If offset was not advanced, increase by one
            // otherwise use end of match offset as new start
            if (soffs == coffs)
                soffs++;
            else
                soffs = coffs;
        }
        else
        {
            soffs++;
        }
    }

    return THERR_OK;
}


static void th_regex_free_match(th_regex_match_t *node)
{
    (void) node;
    // Nothing to do here at the moment
}


void th_regex_free_matches(th_regex_match_t *matches)
{
    th_llist_free_func_node((th_llist_t *) matches,
        (void (*)(th_llist_t *)) th_regex_free_match);
}

#endif // TH_EXPERIMENTAL_REGEX