/*
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* snapkitty-regex
* Copyright (C) 2026 SnapKitty Collective
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
/*
* snapkitty-regex core: Thompson NFA construction + parallel simulation.
*
* Follows Russ Cox's "Regular Expression Matching Can Be Simple And Fast"
* (https://swtch.com/~rsc/regexp/regexp1.html).
*
* re2post : infix -> postfix (explicit '.' concatenation operator)
* post2nfa: postfix -> NFA via Thompson's construction
* match : parallel NFA simulation over the input
*
* Worst-case matching time is O(m*n) for pattern length m and text
* length n. There are no pathological inputs.
*/
#include "regex.h"
#include
#include
enum {
Split = 256, /* epsilon choice state */
Match = 257 /* accepting state */
};
typedef struct State State;
typedef struct Frag Frag;
typedef struct Ptrlist Ptrlist;
typedef struct List List;
struct State {
int c; /* character, Split, or Match */
State *out;
State *out1;
int lastlist; /* dedup generation, see addstate */
};
struct Frag {
State *start;
Ptrlist *out; /* dangling arrows */
};
struct Ptrlist {
Ptrlist *next;
State **s; /* pointer to a State* field to patch */
};
struct List {
State **s;
int n;
};
/* Opaque compiled pattern. */
struct Regex {
State *start;
State *matchstate;
State **states; /* every allocated state, for teardown */
int nstates;
int listid; /* dedup generation counter */
};
/* Compile-time context: tracks allocations so failures unwind cleanly. */
typedef struct Compiler {
State **states;
int nstates;
int capstates;
} Compiler;
static State *
state(Compiler *cp, int c, State *out, State *out1)
{
State *s;
State **ns;
int ncap;
if (cp->nstates == cp->capstates) {
ncap = cp->capstates ? cp->capstates * 2 : 64;
ns = realloc(cp->states, (size_t)ncap * sizeof *ns);
if (ns == NULL)
return NULL;
cp->states = ns;
cp->capstates = ncap;
}
s = malloc(sizeof *s);
if (s == NULL)
return NULL;
s->c = c;
s->out = out;
s->out1 = out1;
s->lastlist = 0;
cp->states[cp->nstates++] = s;
return s;
}
static Ptrlist *
list1(State **outp)
{
Ptrlist *l = malloc(sizeof *l);
if (l == NULL)
return NULL;
l->next = NULL;
l->s = outp;
return l;
}
static Ptrlist *
append(Ptrlist *l1, Ptrlist *l2)
{
Ptrlist *oldl = l1;
if (l1 == NULL)
return l2;
while (l1->next)
l1 = l1->next;
l1->next = l2;
return oldl;
}
/* Connect every dangling arrow in l to s, freeing the list nodes. */
static void
patch(Ptrlist *l, State *s)
{
Ptrlist *next;
for (; l; l = next) {
next = l->next;
*l->s = s;
free(l);
}
}
static void
ptrlist_free(Ptrlist *l)
{
Ptrlist *next;
for (; l; l = next) {
next = l->next;
free(l);
}
}
static Frag
frag(State *start, Ptrlist *out)
{
Frag f;
f.start = start;
f.out = out;
return f;
}
/*
* Rewrite an infix pattern to postfix with '.' as the explicit
* concatenation operator. Backslash escapes the next character into a
* literal (emitted as '\' + char in the postfix stream).
* Returns a malloc'd string, or NULL on malformed pattern / OOM.
*/
static char *
re2post(const char *re)
{
size_t len = strlen(re);
char *buf = malloc(6 * len + 8);
char *dst;
int nalt, natom;
struct { int nalt; int natom; } *paren, *p;
if (buf == NULL)
return NULL;
paren = malloc((len + 1) * sizeof *paren);
if (paren == NULL) {
free(buf);
return NULL;
}
dst = buf;
p = paren;
nalt = 0;
natom = 0;
for (; *re; re++) {
switch (*re) {
case '(':
if (natom > 1) {
--natom;
*dst++ = '.';
}
p->nalt = nalt;
p->natom = natom;
p++;
nalt = 0;
natom = 0;
break;
case '|':
if (natom == 0)
goto fail;
while (--natom > 0)
*dst++ = '.';
nalt++;
break;
case ')':
if (p == paren || natom == 0)
goto fail;
while (--natom > 0)
*dst++ = '.';
for (; nalt > 0; nalt--)
*dst++ = '|';
--p;
nalt = p->nalt;
natom = p->natom;
natom++;
break;
case '*':
case '+':
case '?':
if (natom == 0)
goto fail;
*dst++ = *re;
break;
case '\\':
if (re[1] == '\0')
goto fail;
if (natom > 1) {
--natom;
*dst++ = '.';
}
*dst++ = '\\';
*dst++ = *++re;
natom++;
break;
default:
if (natom > 1) {
--natom;
*dst++ = '.';
}
/* '.' is the postfix concatenation operator: a literal
* dot must go through as an escaped literal. */
if (*re == '.')
*dst++ = '\\';
*dst++ = *re;
natom++;
break;
}
}
if (p != paren)
goto fail;
while (--natom > 0)
*dst++ = '.';
for (; nalt > 0; nalt--)
*dst++ = '|';
*dst = '\0';
free(paren);
return buf;
fail:
free(paren);
free(buf);
return NULL;
}
/*
* Thompson's construction: build an NFA fragment per postfix token on a
* fragment stack. Returns the start state, or NULL on failure.
*/
static State *
post2nfa(Compiler *cp, const char *postfix, State *matchstate)
{
Frag *stack, *stackp, e1, e2, e;
State *s;
Ptrlist *l;
size_t n = strlen(postfix);
const char *p;
#define push(f) (*stackp++ = (f))
#define pop() (*--stackp)
if (n == 0)
return NULL;
stack = malloc((n + 1) * sizeof *stack);
if (stack == NULL)
return NULL;
stackp = stack;
for (p = postfix; *p; p++) {
switch (*p) {
default: /* literal character */
s = state(cp, (unsigned char)*p, NULL, NULL);
if (s == NULL)
goto fail;
l = list1(&s->out);
if (l == NULL)
goto fail;
push(frag(s, l));
break;
case '\\': /* escaped literal */
if (p[1] == '\0')
goto fail;
s = state(cp, (unsigned char)*++p, NULL, NULL);
if (s == NULL)
goto fail;
l = list1(&s->out);
if (l == NULL)
goto fail;
push(frag(s, l));
break;
case '.': /* concatenation */
if (stackp - stack < 2)
goto fail;
e2 = pop();
e1 = pop();
patch(e1.out, e2.start);
push(frag(e1.start, e2.out));
break;
case '|': /* alternation */
if (stackp - stack < 2)
goto fail;
e2 = pop();
e1 = pop();
s = state(cp, Split, e1.start, e2.start);
if (s == NULL)
goto fail;
push(frag(s, append(e1.out, e2.out)));
break;
case '?': /* zero or one */
if (stackp - stack < 1)
goto fail;
e = pop();
s = state(cp, Split, e.start, NULL);
if (s == NULL)
goto fail;
l = list1(&s->out1);
if (l == NULL)
goto fail;
push(frag(s, append(e.out, l)));
break;
case '*': /* zero or more */
if (stackp - stack < 1)
goto fail;
e = pop();
s = state(cp, Split, e.start, NULL);
if (s == NULL)
goto fail;
patch(e.out, s);
l = list1(&s->out1);
if (l == NULL)
goto fail;
push(frag(s, l));
break;
case '+': /* one or more */
if (stackp - stack < 1)
goto fail;
e = pop();
s = state(cp, Split, e.start, NULL);
if (s == NULL)
goto fail;
patch(e.out, s);
l = list1(&s->out1);
if (l == NULL)
goto fail;
push(frag(e.start, l));
break;
}
}
if (stackp != stack + 1)
goto fail;
e = pop();
patch(e.out, matchstate);
free(stack);
return e.start;
fail:
while (stackp > stack) {
e = pop();
ptrlist_free(e.out);
}
free(stack);
return NULL;
#undef push
#undef pop
}
/*
* Add s to list l, following unlabeled (Split) arrows. The per-Regex
* listid generation counter dedups in O(1) without clearing the list.
*/
static void
addstate(Regex *re, List *l, State *s)
{
if (s == NULL || s->lastlist == re->listid)
return;
s->lastlist = re->listid;
if (s->c == Split) {
addstate(re, l, s->out);
addstate(re, l, s->out1);
return;
}
l->s[l->n++] = s;
}
static List *
startlist(Regex *re, State *s, List *l)
{
re->listid++;
l->n = 0;
addstate(re, l, s);
return l;
}
/* Advance the NFA past one input character. */
static void
step(Regex *re, List *clist, int c, List *nlist)
{
int i;
State *s;
re->listid++;
nlist->n = 0;
for (i = 0; i < clist->n; i++) {
s = clist->s[i];
if (s->c == c)
addstate(re, nlist, s->out);
}
}
static int
ismatch(Regex *re, List *l)
{
int i;
for (i = 0; i < l->n; i++)
if (l->s[i] == re->matchstate)
return 1;
return 0;
}
/* Parallel NFA simulation over the input. With prefix != 0, report a
* match as soon as the accept state is reachable (pattern matches a
* prefix of s); otherwise require it after the last character (full
* match). */
static int
pump(Regex *re, const char *s, int prefix)
{
State **buf1 = malloc((size_t)re->nstates * sizeof *buf1);
State **buf2 = malloc((size_t)re->nstates * sizeof *buf2);
List l1, l2, *clist, *nlist, *t;
int m;
if (buf1 == NULL || buf2 == NULL) {
free(buf1);
free(buf2);
return 0;
}
l1.s = buf1;
l1.n = 0;
l2.s = buf2;
l2.n = 0;
clist = startlist(re, re->start, &l1);
if (prefix && ismatch(re, clist)) {
m = 1;
goto done;
}
nlist = &l2;
for (; *s; s++) {
step(re, clist, (unsigned char)*s, nlist);
t = clist;
clist = nlist;
nlist = t;
if (prefix && ismatch(re, clist)) {
m = 1;
goto done;
}
}
m = ismatch(re, clist);
done:
free(buf1);
free(buf2);
return m;
}
static int
match(Regex *re, const char *s)
{
return pump(re, s, 0);
}
Regex *
re_compile(const char *pattern)
{
char *post;
Compiler cp = { NULL, 0, 0 };
Regex *re;
State *start;
int i;
if (pattern == NULL || *pattern == '\0')
return NULL;
post = re2post(pattern);
if (post == NULL)
return NULL;
re = calloc(1, sizeof *re);
if (re == NULL) {
free(post);
return NULL;
}
re->matchstate = state(&cp, Match, NULL, NULL);
start = re->matchstate ? post2nfa(&cp, post, re->matchstate) : NULL;
free(post);
if (start == NULL) {
for (i = 0; i < cp.nstates; i++)
free(cp.states[i]);
free(cp.states);
free(re);
return NULL;
}
re->start = start;
re->states = cp.states;
re->nstates = cp.nstates;
re->listid = 0;
return re;
}
int
re_fullmatch(Regex *re, const char *s)
{
if (re == NULL || s == NULL)
return 0;
return match(re, s);
}
int
re_match(Regex *re, const char *s)
{
if (re == NULL || s == NULL)
return 0;
return pump(re, s, 1);
}
int
re_search(Regex *re, const char *s)
{
const char *p;
if (re == NULL || s == NULL)
return 0;
for (p = s; ; p++) {
if (re_match(re, p))
return 1;
if (*p == '\0')
break;
}
return 0;
}
void
re_free(Regex *re)
{
int i;
if (re == NULL)
return;
for (i = 0; i < re->nstates; i++)
free(re->states[i]);
free(re->states);
free(re);
}