calog/libs/calogRegex.c

398 lines
15 KiB
C

// calogRegex.c -- calog regex library (see calogRegex.h), over PCRE2 (8-bit, interpreted, no JIT).
// A pattern is compiled per call (no cache in v1); subjects/results are binary-safe. Natives inline.
#include "calogRegex.h"
#include "calogInternal.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#define PCRE2_CODE_UNIT_WIDTH 8
#include <pcre2.h>
static int32_t regexCompile(const CalogValueT *patternArg, const char *flags, int64_t flagsLen, bool *global, pcre2_code **out, CalogValueT *result);
static int32_t regexGroups(const char *subject, PCRE2_SIZE *ovector, uint32_t count, bool includeFull, CalogAggT **out);
static int32_t regexMatch(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static uint32_t regexOptions(const char *flags, int64_t flagsLen, bool *global);
static int32_t regexReplace(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t regexSearch(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t regexSplit(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
int32_t calogRegexRegister(CalogT *calog) {
int32_t status;
status = calogRegisterInline(calog, "regexMatch", regexMatch, NULL);
if (status == calogOkE) {
status = calogRegisterInline(calog, "regexSearch", regexSearch, NULL);
}
if (status == calogOkE) {
status = calogRegisterInline(calog, "regexReplace", regexReplace, NULL);
}
if (status == calogOkE) {
status = calogRegisterInline(calog, "regexSplit", regexSplit, NULL);
}
return status;
}
static uint32_t regexOptions(const char *flags, int64_t flagsLen, bool *global) {
uint32_t options;
int64_t i;
options = 0;
*global = false;
for (i = 0; i < flagsLen; i++) {
switch (flags[i]) {
case 'i': options |= PCRE2_CASELESS; break;
case 'm': options |= PCRE2_MULTILINE; break;
case 's': options |= PCRE2_DOTALL; break;
case 'x': options |= PCRE2_EXTENDED; break;
case 'g': *global = true; break;
default: break;
}
}
return options;
}
// Compile args[argIndex 0] as the pattern with the parsed flags; on error set result and return it.
static int32_t regexCompile(const CalogValueT *patternArg, const char *flags, int64_t flagsLen, bool *global, pcre2_code **out, CalogValueT *result) {
pcre2_code *code;
uint32_t options;
int errorCode;
PCRE2_SIZE errorOffset;
*out = NULL;
options = regexOptions(flags, flagsLen, global);
code = pcre2_compile((PCRE2_SPTR)patternArg->as.s.bytes, (PCRE2_SIZE)patternArg->as.s.length, options, &errorCode, &errorOffset, NULL);
if (code == NULL) {
PCRE2_UCHAR message[256];
pcre2_get_error_message(errorCode, message, sizeof(message));
return calogFail(result, calogErrArgE, (const char *)message);
}
*out = code;
return calogOkE;
}
// Build a list of the captured groups from the ovector (group 0 = whole match included only when
// includeFull). A non-participating group (offset PCRE2_UNSET) becomes nil.
static int32_t regexGroups(const char *subject, PCRE2_SIZE *ovector, uint32_t count, bool includeFull, CalogAggT **out) {
CalogAggT *list;
uint32_t start;
uint32_t i;
int32_t status;
*out = NULL;
status = calogAggCreate(&list, calogListE);
if (status != calogOkE) {
return status;
}
start = includeFull ? 0u : 1u;
for (i = start; i < count; i++) {
CalogValueT value;
if (ovector[2 * i] == PCRE2_UNSET) {
calogValueNil(&value);
} else {
PCRE2_SIZE from;
PCRE2_SIZE to;
from = ovector[2 * i];
to = ovector[2 * i + 1];
status = calogValueString(&value, subject + from, (int64_t)(to - from));
if (status != calogOkE) {
calogAggFree(list);
return status;
}
}
status = calogAggPush(list, &value);
if (status != calogOkE) {
calogValueFree(&value);
calogAggFree(list);
return status;
}
}
*out = list;
return calogOkE;
}
static int32_t regexMatch(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
pcre2_code *code;
pcre2_match_data *matchData;
CalogAggT *groups;
const char *flags;
int64_t flagsLen;
bool global;
int rc;
int32_t status;
(void)userData;
calogValueNil(result);
if (argCount < 2 || argCount > 3 || args[0].type != calogStringE || args[1].type != calogStringE) {
return calogFail(result, calogErrArgE, "regexMatch expects (pattern, subject [, flags])");
}
if (argCount == 3 && args[2].type != calogStringE) {
return calogFail(result, calogErrArgE, "regexMatch: flags must be a string");
}
flags = argCount == 3 ? args[2].as.s.bytes : "";
flagsLen = argCount == 3 ? args[2].as.s.length : 0;
status = regexCompile(&args[0], flags, flagsLen, &global, &code, result);
if (status != calogOkE) {
return status;
}
matchData = pcre2_match_data_create_from_pattern(code, NULL);
if (matchData == NULL) {
pcre2_code_free(code);
return calogFail(result, calogErrOomE, "regexMatch: out of memory");
}
rc = pcre2_match(code, (PCRE2_SPTR)args[1].as.s.bytes, (PCRE2_SIZE)args[1].as.s.length, 0, 0, matchData, NULL);
if (rc < 0) {
pcre2_match_data_free(matchData);
pcre2_code_free(code);
return calogOkE; // PCRE2_ERROR_NOMATCH (and any other failure) -> nil
}
status = regexGroups(args[1].as.s.bytes, pcre2_get_ovector_pointer(matchData), (uint32_t)rc, true, &groups);
pcre2_match_data_free(matchData);
pcre2_code_free(code);
if (status != calogOkE) {
return calogFail(result, status, "regexMatch: out of memory");
}
calogValueAgg(result, groups);
return calogOkE;
}
static int32_t regexReplace(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
pcre2_code *code;
PCRE2_UCHAR *output;
PCRE2_SIZE outLen;
const char *flags;
int64_t flagsLen;
bool global;
uint32_t options;
int rc;
int32_t status;
(void)userData;
calogValueNil(result);
if (argCount < 3 || argCount > 4 || args[0].type != calogStringE || args[1].type != calogStringE || args[2].type != calogStringE) {
return calogFail(result, calogErrArgE, "regexReplace expects (pattern, subject, replacement [, flags])");
}
if (argCount == 4 && args[3].type != calogStringE) {
return calogFail(result, calogErrArgE, "regexReplace: flags must be a string");
}
flags = argCount == 4 ? args[3].as.s.bytes : "";
flagsLen = argCount == 4 ? args[3].as.s.length : 0;
status = regexCompile(&args[0], flags, flagsLen, &global, &code, result);
if (status != calogOkE) {
return status;
}
options = PCRE2_SUBSTITUTE_EXTENDED | PCRE2_SUBSTITUTE_OVERFLOW_LENGTH;
if (global) {
options |= PCRE2_SUBSTITUTE_GLOBAL;
}
// First pass: OVERFLOW_LENGTH makes PCRE2 report the needed length in outLen if the buffer is
// too small (a guess start avoids a second call for the common case).
outLen = (PCRE2_SIZE)args[1].as.s.length + 256;
output = (PCRE2_UCHAR *)malloc(outLen);
if (output == NULL) {
pcre2_code_free(code);
return calogFail(result, calogErrOomE, "regexReplace: out of memory");
}
rc = pcre2_substitute(code, (PCRE2_SPTR)args[1].as.s.bytes, (PCRE2_SIZE)args[1].as.s.length, 0, options, NULL, NULL,
(PCRE2_SPTR)args[2].as.s.bytes, (PCRE2_SIZE)args[2].as.s.length, output, &outLen);
if (rc == PCRE2_ERROR_NOMEMORY) {
PCRE2_UCHAR *grown;
grown = (PCRE2_UCHAR *)realloc(output, outLen);
if (grown == NULL) {
free(output);
pcre2_code_free(code);
return calogFail(result, calogErrOomE, "regexReplace: out of memory");
}
output = grown;
rc = pcre2_substitute(code, (PCRE2_SPTR)args[1].as.s.bytes, (PCRE2_SIZE)args[1].as.s.length, 0, options, NULL, NULL,
(PCRE2_SPTR)args[2].as.s.bytes, (PCRE2_SIZE)args[2].as.s.length, output, &outLen);
}
pcre2_code_free(code);
if (rc < 0) {
PCRE2_UCHAR message[256];
pcre2_get_error_message(rc, message, sizeof(message));
free(output);
return calogFail(result, calogErrArgE, (const char *)message);
}
status = calogValueString(result, (const char *)output, (int64_t)outLen);
free(output);
return status;
}
static int32_t regexSearch(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
pcre2_code *code;
pcre2_match_data *matchData;
CalogAggT *map;
CalogAggT *groups;
PCRE2_SIZE *ovector;
const char *flags;
int64_t flagsLen;
bool global;
int rc;
int32_t status;
(void)userData;
calogValueNil(result);
if (argCount < 2 || argCount > 3 || args[0].type != calogStringE || args[1].type != calogStringE) {
return calogFail(result, calogErrArgE, "regexSearch expects (pattern, subject [, flags])");
}
if (argCount == 3 && args[2].type != calogStringE) {
return calogFail(result, calogErrArgE, "regexSearch: flags must be a string");
}
flags = argCount == 3 ? args[2].as.s.bytes : "";
flagsLen = argCount == 3 ? args[2].as.s.length : 0;
status = regexCompile(&args[0], flags, flagsLen, &global, &code, result);
if (status != calogOkE) {
return status;
}
matchData = pcre2_match_data_create_from_pattern(code, NULL);
if (matchData == NULL) {
pcre2_code_free(code);
return calogFail(result, calogErrOomE, "regexSearch: out of memory");
}
rc = pcre2_match(code, (PCRE2_SPTR)args[1].as.s.bytes, (PCRE2_SIZE)args[1].as.s.length, 0, 0, matchData, NULL);
if (rc < 0) {
pcre2_match_data_free(matchData);
pcre2_code_free(code);
return calogOkE; // no match -> nil
}
ovector = pcre2_get_ovector_pointer(matchData);
status = regexGroups(args[1].as.s.bytes, ovector, (uint32_t)rc, false, &groups);
if (status != calogOkE) {
pcre2_match_data_free(matchData);
pcre2_code_free(code);
return calogFail(result, status, "regexSearch: out of memory");
}
status = calogAggCreate(&map, calogMapE);
if (status == calogOkE) {
status = calogMapSetStr(map, "match", args[1].as.s.bytes + ovector[0], (int64_t)(ovector[1] - ovector[0]));
}
if (status == calogOkE) {
status = calogMapSetInt(map, "start", (int64_t)ovector[0]);
}
if (status == calogOkE) {
status = calogMapSetInt(map, "end", (int64_t)ovector[1]);
}
if (status == calogOkE) {
CalogValueT groupsKey;
CalogValueT groupsValue;
calogValueAgg(&groupsValue, groups);
if (calogValueString(&groupsKey, "groups", 6) == calogOkE) {
status = calogAggSet(map, &groupsKey, &groupsValue); // takes ownership of key + value
} else {
calogValueFree(&groupsValue);
status = calogErrOomE;
}
} else {
calogAggFree(groups);
}
pcre2_match_data_free(matchData);
pcre2_code_free(code);
if (status != calogOkE) {
calogAggFree(map);
return calogFail(result, status, "regexSearch: failed to build the result");
}
calogValueAgg(result, map);
return calogOkE;
}
static int32_t regexSplit(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
pcre2_code *code;
pcre2_match_data *matchData;
CalogAggT *pieces;
const char *subject;
const char *flags;
int64_t subjectLen;
int64_t flagsLen;
PCRE2_SIZE offset;
bool global;
int32_t status;
(void)userData;
calogValueNil(result);
if (argCount < 2 || argCount > 3 || args[0].type != calogStringE || args[1].type != calogStringE) {
return calogFail(result, calogErrArgE, "regexSplit expects (pattern, subject [, flags])");
}
if (argCount == 3 && args[2].type != calogStringE) {
return calogFail(result, calogErrArgE, "regexSplit: flags must be a string");
}
flags = argCount == 3 ? args[2].as.s.bytes : "";
flagsLen = argCount == 3 ? args[2].as.s.length : 0;
status = regexCompile(&args[0], flags, flagsLen, &global, &code, result);
if (status != calogOkE) {
return status;
}
matchData = pcre2_match_data_create_from_pattern(code, NULL);
if (matchData == NULL) {
pcre2_code_free(code);
return calogFail(result, calogErrOomE, "regexSplit: out of memory");
}
status = calogAggCreate(&pieces, calogListE);
if (status != calogOkE) {
pcre2_match_data_free(matchData);
pcre2_code_free(code);
return calogFail(result, status, "regexSplit: out of memory");
}
subject = args[1].as.s.bytes;
subjectLen = args[1].as.s.length;
offset = 0;
while (status == calogOkE) {
PCRE2_SIZE *ovector;
PCRE2_SIZE matchStart;
PCRE2_SIZE matchEnd;
CalogValueT piece;
int rc;
rc = pcre2_match(code, (PCRE2_SPTR)subject, (PCRE2_SIZE)subjectLen, offset, 0, matchData, NULL);
if (rc < 0) {
break;
}
ovector = pcre2_get_ovector_pointer(matchData);
matchStart = ovector[0];
matchEnd = ovector[1];
if (matchEnd == matchStart) {
// Empty match: advance one byte so the split terminates rather than looping.
if (matchStart >= (PCRE2_SIZE)subjectLen) {
break;
}
offset = matchStart + 1;
continue;
}
status = calogValueString(&piece, subject + offset, (int64_t)(matchStart - offset));
if (status == calogOkE) {
status = calogAggPush(pieces, &piece);
if (status != calogOkE) {
calogValueFree(&piece);
}
}
offset = matchEnd;
}
if (status == calogOkE) {
CalogValueT tail;
status = calogValueString(&tail, subject + offset, (int64_t)(subjectLen - (int64_t)offset));
if (status == calogOkE) {
status = calogAggPush(pieces, &tail);
if (status != calogOkE) {
calogValueFree(&tail);
}
}
}
pcre2_match_data_free(matchData);
pcre2_code_free(code);
if (status != calogOkE) {
calogAggFree(pieces);
return calogFail(result, status, "regexSplit: out of memory");
}
calogValueAgg(result, pieces);
return calogOkE;
}