398 lines
15 KiB
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;
|
|
}
|