Mercurial > hg > th-libs
view th_regex.c @ 640:9e1f9e1d1487
Aaand some more work. Still just a broken concept.
author | Matti Hamalainen <ccr@tnsp.org> |
---|---|
date | Fri, 24 Jan 2020 09:13:24 +0200 |
parents | 8c957ad9d4c3 |
children | 9a1ed82abefd |
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 GREEDY", "COUNT NONGREEDY", "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_GREEDY, TH_RE_MATCH_COUNT_NONGREEDY, 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 == '?') { // Check if previous was a count pnode->mode = (pnode->mode == TH_RE_MATCH_COUNT_GREEDY) ? TH_RE_MATCH_COUNT_NONGREEDY : TH_RE_MATCH_COUNT_GREEDY; // Previous token is optional (repeat 0-1 times) pnode->repeatMin = 0; pnode->repeatMax = 1; } else { // Check if previous was a count if (pnode->mode == TH_RE_MATCH_COUNT_GREEDY || pnode->mode == TH_RE_MATCH_COUNT_NONGREEDY) { res = THERR_INVALID_DATA; goto exit; } pnode->mode = TH_RE_MATCH_COUNT_GREEDY; 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_GREEDY; 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, expr->nnodes, re_match_modes[node->mode], re_match_types[node->type], haystack + *poffs); switch (node->mode) { case TH_RE_MATCH_ONCE: res = th_regex_match_one(haystack, poffs, node, flags); break; case TH_RE_MATCH_COUNT_GREEDY: case TH_RE_MATCH_COUNT_NONGREEDY: { 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 = *poffs, tnode = nnode; do { match = th_regex_match_one(haystack, &toffs, node, flags); if (match && haystack[toffs] != 0) { for (tnode = nnode + 1; match && tnode < expr->nnodes && haystack[toffs] != 0; tnode++) { size_t noffs = toffs; match = th_regex_match_one(haystack, &toffs, &expr->nodes[tnode], flags); DBG_RE_MATCH(" '%s': %d\n", haystack + noffs, match); } } } while (!match && haystack[toffs] != 0); if (match) { // Node matched count++; *poffs = soffs; 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 = (*poffs == 0); break; case TH_RE_MATCH_ANCHOR_END: res = (haystack[*poffs] == 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