// 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 #include #include #define PCRE2_CODE_UNIT_WIDTH 8 #include 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; }