feat: publish Regex Tools 0.2.0

This commit is contained in:
2026-07-27 01:06:57 +02:00
parent 873b9b218d
commit 4951c966d4
180 changed files with 35344 additions and 503 deletions

View File

@@ -0,0 +1,71 @@
# SPDX-License-Identifier: GPL-3.0-or-later
cmake_minimum_required(VERSION 3.25)
project(regex_pcre2_bridge LANGUAGES C)
if(NOT EMSCRIPTEN)
message(FATAL_ERROR "The staged PCRE2 pack must be built with Emscripten.")
endif()
if(NOT DEFINED PCRE2_SOURCE_DIR OR NOT IS_DIRECTORY "${PCRE2_SOURCE_DIR}")
message(FATAL_ERROR "PCRE2_SOURCE_DIR must identify the verified PCRE2 source checkout.")
endif()
if(NOT DEFINED REGEX_PCRE2_OUTPUT_DIR)
message(FATAL_ERROR "REGEX_PCRE2_OUTPUT_DIR must identify the private staging directory.")
endif()
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(BUILD_STATIC_LIBS ON CACHE BOOL "" FORCE)
set(PCRE2_BUILD_PCRE2_8 ON CACHE BOOL "" FORCE)
set(PCRE2_BUILD_PCRE2_16 OFF CACHE BOOL "" FORCE)
set(PCRE2_BUILD_PCRE2_32 OFF CACHE BOOL "" FORCE)
set(PCRE2_BUILD_PCRE2GREP OFF CACHE BOOL "" FORCE)
set(PCRE2_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(PCRE2_DEBUG OFF CACHE STRING "" FORCE)
set(PCRE2_REBUILD_CHARTABLES OFF CACHE BOOL "" FORCE)
set(PCRE2_SHOW_REPORT OFF CACHE BOOL "" FORCE)
set(PCRE2_STATIC_PIC OFF CACHE BOOL "" FORCE)
set(PCRE2_SUPPORT_JIT OFF CACHE BOOL "" FORCE)
set(PCRE2_SUPPORT_UNICODE ON CACHE BOOL "" FORCE)
add_subdirectory("${PCRE2_SOURCE_DIR}" pcre2-source EXCLUDE_FROM_ALL)
add_executable(regex-pcre2-pack pcre2_bridge.c)
target_compile_features(regex-pcre2-pack PRIVATE c_std_11)
target_compile_definitions(regex-pcre2-pack PRIVATE PCRE2_CODE_UNIT_WIDTH=8)
target_compile_options(
regex-pcre2-pack
PRIVATE
-O3
-fno-ident
-fvisibility=hidden
)
target_link_libraries(regex-pcre2-pack PRIVATE pcre2-8-static)
target_link_options(
regex-pcre2-pack
PRIVATE
-O3
--no-entry
"SHELL:-s ALLOW_MEMORY_GROWTH=1"
"SHELL:-s ASSERTIONS=0"
"SHELL:-s ENVIRONMENT=web,worker,node"
"SHELL:-s EXPORT_ES6=1"
"SHELL:-s EXPORT_NAME=createRegexPcre2"
"SHELL:-s EXPORTED_FUNCTIONS=['_regex_pcre2_bridge_abi_version','_regex_pcre2_compile_probe','_regex_pcre2_config_flags','_regex_pcre2_error_message','_regex_pcre2_execute','_regex_pcre2_self_test','_regex_pcre2_substitute','_regex_pcre2_trace','_regex_pcre2_version','_malloc','_free']"
"SHELL:-s EXPORTED_RUNTIME_METHODS=['HEAPU8','UTF8ToString']"
"SHELL:-s FILESYSTEM=0"
"SHELL:-s INITIAL_MEMORY=16777216"
"SHELL:-s MALLOC=emmalloc"
"SHELL:-s MAXIMUM_MEMORY=268435456"
"SHELL:-s MODULARIZE=1"
"SHELL:-s NO_EXIT_RUNTIME=1"
"SHELL:-s STRICT=1"
)
set_target_properties(
regex-pcre2-pack
PROPERTIES
OUTPUT_NAME pcre2
RUNTIME_OUTPUT_DIRECTORY "${REGEX_PCRE2_OUTPUT_DIR}"
SUFFIX ".mjs"
)

27
engines/pcre2/README.md Normal file
View File

@@ -0,0 +1,27 @@
# PCRE2 engine bridge
This directory contains the GPL-3.0-or-later application bridge and
deterministic build description for the bundled PCRE2 flavour. It deliberately
does not vendor the upstream PCRE2 source tree or generated binaries.
ABI version 3 exposes:
- exact engine identity and configuration;
- compile diagnostics with UTF-8 byte offsets;
- bounded non-overlapping matching with copied group-zero/capture records and
copied capture-name metadata;
- bounded native `pcre2_substitute()` semantics over the same retained match
sequence;
- bounded automatic-callout tracing from one native `pcre2_match()` invocation,
with copied event and mark data and an immediate native stop at either trace
cap;
- match-step, depth, heap, match-count, capture-row and output limits;
- no native pointer or compiled-code handle across a call boundary.
All PCRE2 allocations are released before an ABI call returns. JavaScript owns
and frees every input/result buffer in `finally`, and the containing worker is
terminated on timeout, crash, cancellation or supersession.
The explicit offline source/toolchain identity, build, verification and local
asset-install workflow is documented in
[`docs/ENGINE_BUILDS.md`](../../docs/ENGINE_BUILDS.md).

View File

@@ -0,0 +1,910 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#define PCRE2_CODE_UNIT_WIDTH 8
#include "pcre2_bridge.h"
#include <pcre2.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#define REGEX_PCRE2_COMPILE_FLAGS \
(REGEX_PCRE2_CASELESS | REGEX_PCRE2_MULTILINE | REGEX_PCRE2_DOTALL | \
REGEX_PCRE2_EXTENDED | REGEX_PCRE2_UNGREEDY | REGEX_PCRE2_UTF | \
REGEX_PCRE2_UCP | REGEX_PCRE2_DUPNAMES)
#define REGEX_PCRE2_ALL_FLAGS \
(REGEX_PCRE2_COMPILE_FLAGS | REGEX_PCRE2_GLOBAL)
_Static_assert(sizeof(regex_pcre2_compile_result) == 20,
"Compile result is part of ABI version 3.");
_Static_assert(sizeof(regex_pcre2_limits) == 20,
"Limits record is part of ABI version 3.");
_Static_assert(sizeof(regex_pcre2_match_record) == 16,
"Match record is part of ABI version 3.");
_Static_assert(sizeof(regex_pcre2_name_record) == 12,
"Name record is part of ABI version 3.");
_Static_assert(sizeof(regex_pcre2_run_result) == 60,
"Run result is part of ABI version 3.");
_Static_assert(sizeof(regex_pcre2_trace_limits) == 20,
"Trace limits are part of ABI version 3.");
_Static_assert(sizeof(regex_pcre2_trace_event) == 32,
"Trace event is part of ABI version 3.");
_Static_assert(sizeof(regex_pcre2_trace_result) == 52,
"Trace result is part of ABI version 3.");
typedef struct regex_pcre2_run_buffers {
regex_pcre2_match_record *records;
uint32_t record_capacity;
regex_pcre2_name_record *name_records;
uint32_t name_record_capacity;
uint8_t *name_bytes;
uint32_t name_bytes_capacity;
uint8_t *output;
uint32_t output_capacity;
} regex_pcre2_run_buffers;
typedef struct regex_pcre2_trace_state {
regex_pcre2_trace_event *events;
uint32_t event_capacity;
uint8_t *mark_bytes;
uint32_t mark_bytes_capacity;
uint32_t maximum_events;
uint32_t maximum_trace_bytes;
uint32_t event_count;
uint32_t total_event_count;
uint32_t mark_bytes_length;
uint32_t events_truncated;
uint32_t marks_truncated;
} regex_pcre2_trace_state;
static const uint8_t empty_bytes[1] = {0};
static uint32_t to_pcre2_options(uint32_t application_flags) {
uint32_t options = 0;
if ((application_flags & REGEX_PCRE2_CASELESS) != 0) {
options |= PCRE2_CASELESS;
}
if ((application_flags & REGEX_PCRE2_MULTILINE) != 0) {
options |= PCRE2_MULTILINE;
}
if ((application_flags & REGEX_PCRE2_DOTALL) != 0) {
options |= PCRE2_DOTALL;
}
if ((application_flags & REGEX_PCRE2_EXTENDED) != 0) {
options |= PCRE2_EXTENDED;
}
if ((application_flags & REGEX_PCRE2_UNGREEDY) != 0) {
options |= PCRE2_UNGREEDY;
}
if ((application_flags & REGEX_PCRE2_UTF) != 0) {
options |= PCRE2_UTF;
}
if ((application_flags & REGEX_PCRE2_UCP) != 0) {
options |= PCRE2_UCP;
}
if ((application_flags & REGEX_PCRE2_DUPNAMES) != 0) {
options |= PCRE2_DUPNAMES;
}
return options;
}
static int32_t fail_compile(regex_pcre2_compile_result *result,
int32_t status) {
if (result != NULL) {
memset(result, 0, sizeof(*result));
result->status = status;
}
return status;
}
static int32_t fail_run(regex_pcre2_run_result *result,
int32_t status,
uint32_t phase,
uint32_t offset) {
if (result != NULL) {
memset(result, 0, sizeof(*result));
result->status = status;
result->error_phase = phase;
result->error_offset = offset;
}
return status;
}
static int32_t fail_trace(regex_pcre2_trace_result *result,
int32_t status,
uint32_t phase,
uint32_t offset) {
if (result != NULL) {
memset(result, 0, sizeof(*result));
result->status = status;
result->error_phase = phase;
result->error_offset = offset;
result->last_complete_event = UINT32_MAX;
}
return status;
}
static int input_pointer_valid(const uint8_t *value, uint32_t length) {
return value != NULL || length == 0;
}
static const uint8_t *nonnull_input(const uint8_t *value) {
return value == NULL ? empty_bytes : value;
}
static int limits_valid(const regex_pcre2_limits *limits) {
return limits != NULL && limits->maximum_matches > 0 &&
limits->maximum_matches <= REGEX_PCRE2_MAX_MATCHES &&
limits->maximum_capture_rows > 0 &&
limits->maximum_capture_rows <= REGEX_PCRE2_MAX_CAPTURE_ROWS &&
limits->match_limit > 0 &&
limits->match_limit <= REGEX_PCRE2_MAX_MATCH_LIMIT &&
limits->depth_limit > 0 &&
limits->depth_limit <= REGEX_PCRE2_MAX_DEPTH_LIMIT &&
limits->heap_limit_kib > 0 &&
limits->heap_limit_kib <= REGEX_PCRE2_MAX_HEAP_LIMIT_KIB;
}
static int buffers_valid(const regex_pcre2_run_buffers *buffers,
int substitution) {
if (buffers == NULL ||
(buffers->records == NULL && buffers->record_capacity != 0) ||
(buffers->name_records == NULL &&
buffers->name_record_capacity != 0) ||
(buffers->name_bytes == NULL && buffers->name_bytes_capacity != 0)) {
return 0;
}
/*
* PCRE2 writes a trailing NUL even when the semantic output capacity is
* zero, so the bridge contract always requires the advertised +1 byte.
*/
if (substitution && buffers->output == NULL) {
return 0;
}
return 1;
}
static uint32_t bounded_offset(PCRE2_SIZE value) {
return value > UINT32_MAX ? UINT32_MAX : (uint32_t)value;
}
static int32_t copy_names(const pcre2_code *code,
regex_pcre2_run_buffers *buffers,
regex_pcre2_run_result *result) {
uint32_t entry_size = 0;
PCRE2_SPTR table = NULL;
uint32_t index = 0;
uint32_t name_offset = 0;
if (result->name_count == 0) {
return 0;
}
if (pcre2_pattern_info(code, PCRE2_INFO_NAMEENTRYSIZE, &entry_size) != 0 ||
pcre2_pattern_info(code, PCRE2_INFO_NAMETABLE, &table) != 0 ||
entry_size < 3 || table == NULL) {
return REGEX_PCRE2_ERROR_CONFIG;
}
for (index = 0; index < result->name_count; index += 1) {
const uint8_t *entry = table + (index * entry_size);
uint32_t group_number =
((uint32_t)entry[0] << 8) | (uint32_t)entry[1];
uint32_t maximum_name = entry_size - 2;
uint32_t name_length = 0;
while (name_length < maximum_name && entry[2 + name_length] != 0) {
name_length += 1;
}
if (result->name_record_count >= buffers->name_record_capacity ||
name_length > buffers->name_bytes_capacity - name_offset) {
result->names_truncated = 1;
break;
}
buffers->name_records[result->name_record_count].group_number =
group_number;
buffers->name_records[result->name_record_count].name_offset =
name_offset;
buffers->name_records[result->name_record_count].name_length =
name_length;
if (name_length > 0) {
memcpy(buffers->name_bytes + name_offset, entry + 2, name_length);
}
result->name_record_count += 1;
name_offset += name_length;
}
result->name_bytes_length = name_offset;
return 0;
}
static int append_utf8_prefix(regex_pcre2_run_buffers *buffers,
regex_pcre2_run_result *result,
const uint8_t *source,
uint32_t length) {
uint32_t remaining = buffers->output_capacity - result->output_length;
uint32_t copied = length < remaining ? length : remaining;
if (copied < length) {
while (copied > 0 && (source[copied] & 0xc0u) == 0x80u) {
copied -= 1;
}
result->output_truncated = 1;
}
if (copied > 0) {
memcpy(buffers->output + result->output_length, source, copied);
result->output_length += copied;
}
return copied == length;
}
static int32_t configure_match_context_values(pcre2_match_context *context,
uint32_t match_limit,
uint32_t depth_limit,
uint32_t heap_limit_kib) {
if (pcre2_set_match_limit(context, match_limit) != 0 ||
pcre2_set_depth_limit(context, depth_limit) != 0 ||
pcre2_set_heap_limit(context, heap_limit_kib) != 0) {
return REGEX_PCRE2_ERROR_CONFIG;
}
return 0;
}
static int32_t configure_match_context(pcre2_match_context *context,
const regex_pcre2_limits *limits) {
return configure_match_context_values(
context, limits->match_limit, limits->depth_limit,
limits->heap_limit_kib);
}
static int trace_limits_valid(const regex_pcre2_trace_limits *limits) {
return limits != NULL && limits->maximum_events > 0 &&
limits->maximum_events <= REGEX_PCRE2_MAX_TRACE_EVENTS &&
limits->maximum_trace_bytes >= sizeof(regex_pcre2_trace_event) &&
limits->maximum_trace_bytes <= REGEX_PCRE2_MAX_TRACE_BYTES &&
limits->match_limit > 0 &&
limits->match_limit <= REGEX_PCRE2_MAX_MATCH_LIMIT &&
limits->depth_limit > 0 &&
limits->depth_limit <= REGEX_PCRE2_MAX_DEPTH_LIMIT &&
limits->heap_limit_kib > 0 &&
limits->heap_limit_kib <= REGEX_PCRE2_MAX_HEAP_LIMIT_KIB;
}
static uint32_t bounded_mark_length(PCRE2_SPTR mark, uint32_t *truncated) {
uint32_t length = 0;
if (mark == NULL) {
return 0;
}
while (length < REGEX_PCRE2_MAX_TRACE_MARK_BYTES && mark[length] != 0) {
length += 1;
}
if (length == REGEX_PCRE2_MAX_TRACE_MARK_BYTES && mark[length] != 0) {
*truncated = 1;
while (length > 0 && (mark[length] & 0xc0u) == 0x80u) {
length -= 1;
}
}
return length;
}
static int trace_callout(pcre2_callout_block *block, void *data) {
regex_pcre2_trace_state *state = (regex_pcre2_trace_state *)data;
regex_pcre2_trace_event *event = NULL;
uint32_t mark_length = 0;
uint32_t mark_was_truncated = 0;
uint64_t required_trace_bytes = 0;
if (block == NULL || state == NULL) {
return PCRE2_ERROR_CALLOUT;
}
if (state->total_event_count < UINT32_MAX) {
state->total_event_count += 1;
}
mark_length = bounded_mark_length(block->mark, &mark_was_truncated);
required_trace_bytes =
((uint64_t)state->event_count + 1u) *
sizeof(regex_pcre2_trace_event) +
state->mark_bytes_length + mark_length;
if (state->event_count >= state->maximum_events ||
state->event_count >= state->event_capacity ||
required_trace_bytes > state->maximum_trace_bytes ||
mark_length > state->mark_bytes_capacity - state->mark_bytes_length) {
state->events_truncated = 1;
return PCRE2_ERROR_CALLOUT;
}
event = &state->events[state->event_count];
event->callout_number = block->callout_number;
event->pattern_position_byte = bounded_offset(block->pattern_position);
event->next_item_length_byte = bounded_offset(block->next_item_length);
event->subject_position_byte = bounded_offset(block->current_position);
event->capture_top = block->capture_top;
event->capture_last = block->capture_last;
event->mark_offset = state->mark_bytes_length;
event->mark_length = mark_length;
if (mark_length > 0) {
memcpy(state->mark_bytes + state->mark_bytes_length, block->mark,
mark_length);
state->mark_bytes_length += mark_length;
}
state->marks_truncated |= mark_was_truncated;
state->event_count += 1;
return 0;
}
static int32_t run_bounded(const uint8_t *pattern,
uint32_t pattern_length,
const uint8_t *subject,
uint32_t subject_length,
const uint8_t *replacement,
uint32_t replacement_length,
uint32_t application_flags,
const regex_pcre2_limits *limits,
regex_pcre2_run_buffers *buffers,
int substitution,
regex_pcre2_run_result *result) {
pcre2_code *code = NULL;
pcre2_match_data *match_data = NULL;
pcre2_match_context *match_context = NULL;
int compile_error = 0;
PCRE2_SIZE compile_offset = 0;
uint32_t capture_count = 0;
uint32_t name_count = 0;
uint32_t options = 0;
uint32_t search_offset = 0;
uint32_t match_options = 0;
uint32_t copied_until = 0;
uint32_t capture_rows = 0;
int32_t status = 0;
const uint8_t *safe_pattern = nonnull_input(pattern);
const uint8_t *safe_subject = nonnull_input(subject);
const uint8_t *safe_replacement = nonnull_input(replacement);
if (result == NULL || !input_pointer_valid(pattern, pattern_length) ||
!input_pointer_valid(subject, subject_length) ||
(substitution &&
!input_pointer_valid(replacement, replacement_length)) ||
!buffers_valid(buffers, substitution)) {
return fail_run(result, REGEX_PCRE2_ERROR_INVALID_ARGUMENT,
REGEX_PCRE2_PHASE_BRIDGE, 0);
}
memset(result, 0, sizeof(*result));
if ((application_flags & ~REGEX_PCRE2_ALL_FLAGS) != 0) {
return fail_run(result, REGEX_PCRE2_ERROR_UNSUPPORTED_FLAGS,
REGEX_PCRE2_PHASE_BRIDGE, 0);
}
if (pattern_length > REGEX_PCRE2_MAX_PATTERN_BYTES) {
return fail_run(result, REGEX_PCRE2_ERROR_PATTERN_TOO_LARGE,
REGEX_PCRE2_PHASE_BRIDGE, 0);
}
if (subject_length > REGEX_PCRE2_MAX_SUBJECT_BYTES) {
return fail_run(result, REGEX_PCRE2_ERROR_SUBJECT_TOO_LARGE,
REGEX_PCRE2_PHASE_BRIDGE, 0);
}
if (substitution &&
replacement_length > REGEX_PCRE2_MAX_REPLACEMENT_BYTES) {
return fail_run(result, REGEX_PCRE2_ERROR_REPLACEMENT_TOO_LARGE,
REGEX_PCRE2_PHASE_BRIDGE, 0);
}
if (!limits_valid(limits)) {
return fail_run(result, REGEX_PCRE2_ERROR_LIMIT_OUT_OF_RANGE,
REGEX_PCRE2_PHASE_BRIDGE, 0);
}
options = to_pcre2_options(application_flags);
result->effective_options = options;
code = pcre2_compile(safe_pattern, pattern_length, options, &compile_error,
&compile_offset, NULL);
if (code == NULL) {
status = compile_error;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_COMPILE;
result->error_offset = bounded_offset(compile_offset);
goto cleanup;
}
if (pcre2_pattern_info(code, PCRE2_INFO_CAPTURECOUNT, &capture_count) != 0 ||
pcre2_pattern_info(code, PCRE2_INFO_NAMECOUNT, &name_count) != 0) {
status = REGEX_PCRE2_ERROR_CONFIG;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_BRIDGE;
goto cleanup;
}
result->capture_count = capture_count;
result->name_count = name_count;
if (capture_count > REGEX_PCRE2_MAX_CAPTURE_GROUPS) {
status = REGEX_PCRE2_ERROR_CAPTURE_LIMIT;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_BRIDGE;
goto cleanup;
}
status = copy_names(code, buffers, result);
if (status != 0) {
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_BRIDGE;
goto cleanup;
}
match_data = pcre2_match_data_create_from_pattern(code, NULL);
match_context = pcre2_match_context_create(NULL);
if (match_data == NULL || match_context == NULL) {
status = PCRE2_ERROR_NOMEMORY;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_BRIDGE;
goto cleanup;
}
status = configure_match_context(match_context, limits);
if (status != 0) {
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_BRIDGE;
goto cleanup;
}
while (search_offset <= subject_length) {
int match_status =
pcre2_match(code, safe_subject, subject_length, search_offset,
match_options, match_data, match_context);
PCRE2_SIZE *ovector = NULL;
uint32_t required_records = capture_count + 1;
uint32_t required_capture_rows = capture_count == 0 ? 1 : capture_count;
uint32_t group = 0;
uint32_t start = 0;
uint32_t end = 0;
if (match_status == PCRE2_ERROR_NOMATCH) {
break;
}
if (match_status < 0) {
status = match_status;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_MATCH;
result->error_offset = search_offset;
goto cleanup;
}
if (match_status == 0) {
status = REGEX_PCRE2_ERROR_CONFIG;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_BRIDGE;
goto cleanup;
}
ovector = pcre2_get_ovector_pointer(match_data);
start = bounded_offset(ovector[0]);
end = bounded_offset(ovector[1]);
if (start > end || end > subject_length) {
status = REGEX_PCRE2_ERROR_CONFIG;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_BRIDGE;
goto cleanup;
}
if (result->match_count >= limits->maximum_matches ||
capture_rows + required_capture_rows >
limits->maximum_capture_rows ||
required_records > buffers->record_capacity - result->record_count) {
result->results_truncated = 1;
break;
}
for (group = 0; group <= capture_count; group += 1) {
regex_pcre2_match_record *record =
&buffers->records[result->record_count];
PCRE2_SIZE native_start = ovector[group * 2];
PCRE2_SIZE native_end = ovector[group * 2 + 1];
record->match_number = result->match_count + 1;
record->group_number = group;
if (native_start == PCRE2_UNSET || native_end == PCRE2_UNSET) {
record->start_byte = REGEX_PCRE2_UNSET_OFFSET;
record->end_byte = REGEX_PCRE2_UNSET_OFFSET;
} else {
record->start_byte = bounded_offset(native_start);
record->end_byte = bounded_offset(native_end);
}
result->record_count += 1;
}
result->match_count += 1;
capture_rows += required_capture_rows;
if (substitution && result->output_truncated == 0) {
PCRE2_SIZE replacement_output_length =
buffers->output_capacity - result->output_length + 1;
int substitute_status = 0;
if (!append_utf8_prefix(buffers, result, safe_subject + copied_until,
start - copied_until)) {
break;
}
replacement_output_length =
buffers->output_capacity - result->output_length + 1;
substitute_status = pcre2_substitute(
code, safe_subject, subject_length, search_offset,
match_options | PCRE2_SUBSTITUTE_MATCHED |
PCRE2_SUBSTITUTE_REPLACEMENT_ONLY |
PCRE2_SUBSTITUTE_UNSET_EMPTY,
match_data, match_context, safe_replacement, replacement_length,
buffers->output + result->output_length, &replacement_output_length);
if (substitute_status == PCRE2_ERROR_NOMEMORY) {
result->output_truncated = 1;
break;
}
if (substitute_status < 0) {
status = substitute_status;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_SUBSTITUTE;
result->error_offset = bounded_offset(replacement_output_length);
goto cleanup;
}
result->output_length += bounded_offset(replacement_output_length);
result->substitution_count += (uint32_t)substitute_status;
copied_until = end;
}
if ((application_flags & REGEX_PCRE2_GLOBAL) == 0) {
break;
}
{
PCRE2_SIZE next_offset = search_offset;
uint32_t next_options = 0;
if (!pcre2_next_match(match_data, &next_offset, &next_options)) {
break;
}
search_offset = bounded_offset(next_offset);
match_options = next_options;
}
}
if (substitution && result->output_truncated == 0) {
(void)append_utf8_prefix(buffers, result, safe_subject + copied_until,
subject_length - copied_until);
}
cleanup:
if (match_context != NULL) {
pcre2_match_context_free(match_context);
}
if (match_data != NULL) {
pcre2_match_data_free(match_data);
}
if (code != NULL) {
pcre2_code_free(code);
}
return status;
}
uint32_t regex_pcre2_bridge_abi_version(void) {
return REGEX_PCRE2_BRIDGE_ABI_VERSION;
}
uint32_t regex_pcre2_config_flags(void) {
uint32_t unicode = 0;
uint32_t jit = 0;
uint32_t flags = 0;
if (pcre2_config(PCRE2_CONFIG_UNICODE, &unicode) != 0 ||
pcre2_config(PCRE2_CONFIG_JIT, &jit) != 0) {
return 0;
}
if (unicode != 0) {
flags |= REGEX_PCRE2_CONFIG_UNICODE;
}
if (jit != 0) {
flags |= REGEX_PCRE2_CONFIG_JIT;
}
return flags;
}
const uint8_t *regex_pcre2_version(void) {
static uint8_t version[64];
if (pcre2_config(PCRE2_CONFIG_VERSION, version) <= 0) {
version[0] = '\0';
}
return version;
}
int32_t regex_pcre2_error_message(int32_t error_code,
uint8_t *output,
uint32_t output_capacity) {
int status = 0;
if (output == NULL || output_capacity == 0) {
return REGEX_PCRE2_ERROR_INVALID_ARGUMENT;
}
status = pcre2_get_error_message(error_code, output, output_capacity);
if (status < 0) {
output[0] = '\0';
}
return status;
}
int32_t regex_pcre2_compile_probe(const uint8_t *pattern,
uint32_t pattern_length,
uint32_t application_flags,
regex_pcre2_compile_result *result) {
pcre2_code *code = NULL;
int error_code = 0;
PCRE2_SIZE error_offset = 0;
uint32_t capture_count = 0;
uint32_t name_count = 0;
uint32_t options = 0;
if (result == NULL || !input_pointer_valid(pattern, pattern_length)) {
return fail_compile(result, REGEX_PCRE2_ERROR_INVALID_ARGUMENT);
}
memset(result, 0, sizeof(*result));
if ((application_flags & ~REGEX_PCRE2_COMPILE_FLAGS) != 0) {
return fail_compile(result, REGEX_PCRE2_ERROR_UNSUPPORTED_FLAGS);
}
if (pattern_length > REGEX_PCRE2_MAX_PATTERN_BYTES) {
return fail_compile(result, REGEX_PCRE2_ERROR_PATTERN_TOO_LARGE);
}
options = to_pcre2_options(application_flags);
result->effective_options = options;
code = pcre2_compile(nonnull_input(pattern), pattern_length, options,
&error_code, &error_offset, NULL);
if (code == NULL) {
result->status = error_code;
result->error_offset = bounded_offset(error_offset);
return error_code;
}
if (pcre2_pattern_info(code, PCRE2_INFO_CAPTURECOUNT, &capture_count) != 0 ||
pcre2_pattern_info(code, PCRE2_INFO_NAMECOUNT, &name_count) != 0) {
pcre2_code_free(code);
return fail_compile(result, REGEX_PCRE2_ERROR_CONFIG);
}
result->capture_count = capture_count;
result->name_count = name_count;
pcre2_code_free(code);
return 0;
}
int32_t regex_pcre2_execute(
const uint8_t *pattern,
uint32_t pattern_length,
const uint8_t *subject,
uint32_t subject_length,
uint32_t application_flags,
const regex_pcre2_limits *limits,
regex_pcre2_match_record *records,
uint32_t record_capacity,
regex_pcre2_name_record *name_records,
uint32_t name_record_capacity,
uint8_t *name_bytes,
uint32_t name_bytes_capacity,
regex_pcre2_run_result *result) {
regex_pcre2_run_buffers buffers = {
records, record_capacity, name_records, name_record_capacity,
name_bytes, name_bytes_capacity, NULL, 0};
return run_bounded(pattern, pattern_length, subject, subject_length, NULL, 0,
application_flags, limits, &buffers, 0, result);
}
int32_t regex_pcre2_substitute(
const uint8_t *pattern,
uint32_t pattern_length,
const uint8_t *subject,
uint32_t subject_length,
const uint8_t *replacement,
uint32_t replacement_length,
uint32_t application_flags,
const regex_pcre2_limits *limits,
regex_pcre2_match_record *records,
uint32_t record_capacity,
regex_pcre2_name_record *name_records,
uint32_t name_record_capacity,
uint8_t *name_bytes,
uint32_t name_bytes_capacity,
uint8_t *output,
uint32_t output_capacity,
regex_pcre2_run_result *result) {
regex_pcre2_run_buffers buffers = {
records, record_capacity, name_records, name_record_capacity,
name_bytes, name_bytes_capacity, output, output_capacity};
return run_bounded(pattern, pattern_length, subject, subject_length,
replacement, replacement_length, application_flags,
limits, &buffers, 1, result);
}
int32_t regex_pcre2_trace(
const uint8_t *pattern,
uint32_t pattern_length,
const uint8_t *subject,
uint32_t subject_length,
uint32_t application_flags,
const regex_pcre2_trace_limits *limits,
regex_pcre2_trace_event *events,
uint32_t event_capacity,
uint8_t *mark_bytes,
uint32_t mark_bytes_capacity,
regex_pcre2_trace_result *result) {
pcre2_code *code = NULL;
pcre2_match_data *match_data = NULL;
pcre2_match_context *match_context = NULL;
regex_pcre2_trace_state state;
int compile_error = 0;
PCRE2_SIZE compile_offset = 0;
uint32_t capture_count = 0;
uint32_t options = 0;
int32_t match_status = 0;
int32_t status = 0;
if (result == NULL || !input_pointer_valid(pattern, pattern_length) ||
!input_pointer_valid(subject, subject_length) || events == NULL ||
event_capacity == 0 ||
(mark_bytes == NULL && mark_bytes_capacity != 0)) {
return fail_trace(result, REGEX_PCRE2_ERROR_INVALID_ARGUMENT,
REGEX_PCRE2_PHASE_BRIDGE, 0);
}
memset(result, 0, sizeof(*result));
result->last_complete_event = UINT32_MAX;
if ((application_flags & ~REGEX_PCRE2_ALL_FLAGS) != 0) {
return fail_trace(result, REGEX_PCRE2_ERROR_UNSUPPORTED_FLAGS,
REGEX_PCRE2_PHASE_BRIDGE, 0);
}
if (pattern_length > REGEX_PCRE2_MAX_PATTERN_BYTES) {
return fail_trace(result, REGEX_PCRE2_ERROR_PATTERN_TOO_LARGE,
REGEX_PCRE2_PHASE_BRIDGE, 0);
}
if (subject_length > REGEX_PCRE2_MAX_SUBJECT_BYTES) {
return fail_trace(result, REGEX_PCRE2_ERROR_SUBJECT_TOO_LARGE,
REGEX_PCRE2_PHASE_BRIDGE, 0);
}
if (!trace_limits_valid(limits) ||
event_capacity < limits->maximum_events ||
event_capacity > REGEX_PCRE2_MAX_TRACE_EVENTS ||
mark_bytes_capacity > REGEX_PCRE2_MAX_TRACE_BYTES) {
return fail_trace(result, REGEX_PCRE2_ERROR_LIMIT_OUT_OF_RANGE,
REGEX_PCRE2_PHASE_BRIDGE, 0);
}
options = to_pcre2_options(application_flags) | PCRE2_AUTO_CALLOUT;
result->effective_options = options;
code = pcre2_compile(nonnull_input(pattern), pattern_length, options,
&compile_error, &compile_offset, NULL);
if (code == NULL) {
status = compile_error;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_COMPILE;
result->error_offset = bounded_offset(compile_offset);
goto cleanup;
}
if (pcre2_pattern_info(code, PCRE2_INFO_CAPTURECOUNT, &capture_count) != 0) {
status = REGEX_PCRE2_ERROR_CONFIG;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_BRIDGE;
goto cleanup;
}
if (capture_count > REGEX_PCRE2_MAX_CAPTURE_GROUPS) {
status = REGEX_PCRE2_ERROR_CAPTURE_LIMIT;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_BRIDGE;
goto cleanup;
}
match_data = pcre2_match_data_create_from_pattern(code, NULL);
match_context = pcre2_match_context_create(NULL);
if (match_data == NULL || match_context == NULL) {
status = PCRE2_ERROR_NOMEMORY;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_BRIDGE;
goto cleanup;
}
memset(&state, 0, sizeof(state));
state.events = events;
state.event_capacity = event_capacity;
state.mark_bytes = mark_bytes;
state.mark_bytes_capacity = mark_bytes_capacity;
state.maximum_events = limits->maximum_events;
state.maximum_trace_bytes = limits->maximum_trace_bytes;
status = configure_match_context_values(
match_context, limits->match_limit, limits->depth_limit,
limits->heap_limit_kib);
if (status != 0 ||
pcre2_set_callout(match_context, trace_callout, &state) != 0) {
status = REGEX_PCRE2_ERROR_CONFIG;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_BRIDGE;
goto cleanup;
}
/*
* The global flag is intentionally application-level for normal execution.
* A trace represents one exact pcre2_match() invocation.
*/
match_status = pcre2_match(code, nonnull_input(subject), subject_length, 0, 0,
match_data, match_context);
result->native_match_status = match_status;
result->event_count = state.event_count;
result->total_event_count = state.total_event_count;
result->mark_bytes_length = state.mark_bytes_length;
result->trace_bytes_length =
state.event_count * (uint32_t)sizeof(regex_pcre2_trace_event) +
state.mark_bytes_length;
result->events_truncated = state.events_truncated;
result->marks_truncated = state.marks_truncated;
result->last_complete_event =
state.event_count == 0 ? UINT32_MAX : state.event_count - 1;
if (match_status >= 0) {
result->matched = 1;
} else if (match_status == PCRE2_ERROR_NOMATCH ||
(match_status == PCRE2_ERROR_CALLOUT &&
state.events_truncated != 0)) {
status = 0;
} else {
status = match_status;
result->status = status;
result->error_phase = REGEX_PCRE2_PHASE_MATCH;
if (state.event_count > 0) {
result->error_offset =
events[state.event_count - 1].subject_position_byte;
}
}
cleanup:
if (match_context != NULL) {
pcre2_match_context_free(match_context);
}
if (match_data != NULL) {
pcre2_match_data_free(match_data);
}
if (code != NULL) {
pcre2_code_free(code);
}
return status;
}
int32_t regex_pcre2_self_test(void) {
static const uint8_t pattern[] = "(?<word>\\p{L}+)-(?<number>\\d+)";
static const uint8_t subject[] = "x Grüße-42 y";
static const uint8_t replacement[] = "$<number>:$<word>";
static const uint8_t trace_pattern[] = "a+b";
static const uint8_t trace_subject[] = "aaab";
regex_pcre2_compile_result compile_result;
regex_pcre2_run_result run_result;
regex_pcre2_trace_result trace_result;
regex_pcre2_limits limits = {10, 100, 1000000, 1000, 32768};
regex_pcre2_trace_limits trace_limits = {100, 4096, 1000000, 1000,
32768};
regex_pcre2_match_record records[3];
regex_pcre2_name_record names[2];
regex_pcre2_trace_event trace_events[100];
uint8_t name_bytes[64];
uint8_t trace_marks[64];
uint8_t output[65];
int32_t status = regex_pcre2_compile_probe(
pattern, (uint32_t)(sizeof(pattern) - 1),
REGEX_PCRE2_UTF | REGEX_PCRE2_UCP, &compile_result);
if (status != 0 || compile_result.capture_count != 2 ||
compile_result.name_count != 2) {
return 1;
}
status = regex_pcre2_substitute(
pattern, (uint32_t)(sizeof(pattern) - 1), subject,
(uint32_t)(sizeof(subject) - 1), replacement,
(uint32_t)(sizeof(replacement) - 1),
REGEX_PCRE2_UTF | REGEX_PCRE2_UCP, &limits, records, 3, names, 2,
name_bytes, sizeof(name_bytes), output, sizeof(output) - 1, &run_result);
if (status != 0 || run_result.match_count != 1 ||
run_result.record_count != 3 || run_result.substitution_count != 1 ||
run_result.output_truncated != 0 ||
run_result.output_length != sizeof("x 42:Grüße y") - 1 ||
memcmp(output, "x 42:Grüße y", sizeof("x 42:Grüße y") - 1) != 0) {
return 2;
}
status = regex_pcre2_trace(
trace_pattern, (uint32_t)(sizeof(trace_pattern) - 1), trace_subject,
(uint32_t)(sizeof(trace_subject) - 1),
REGEX_PCRE2_UTF | REGEX_PCRE2_UCP, &trace_limits, trace_events, 100,
trace_marks, sizeof(trace_marks), &trace_result);
if (status != 0 || trace_result.status != 0 ||
trace_result.native_match_status <= 0 || trace_result.matched != 1 ||
trace_result.event_count == 0 || trace_result.events_truncated != 0 ||
trace_result.last_complete_event != trace_result.event_count - 1) {
return 3;
}
if (regex_pcre2_bridge_abi_version() != REGEX_PCRE2_BRIDGE_ABI_VERSION ||
regex_pcre2_config_flags() != REGEX_PCRE2_CONFIG_UNICODE) {
return 4;
}
if (strcmp((const char *)regex_pcre2_version(), "10.47 2025-10-21") != 0) {
return 5;
}
return 0;
}

View File

@@ -0,0 +1,236 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef REGEX_TOOLS_PCRE2_BRIDGE_H
#define REGEX_TOOLS_PCRE2_BRIDGE_H
#include <stdint.h>
#define REGEX_PCRE2_BRIDGE_ABI_VERSION 3u
#define REGEX_PCRE2_MAX_PATTERN_BYTES 1048576u
#define REGEX_PCRE2_MAX_SUBJECT_BYTES 16777216u
#define REGEX_PCRE2_MAX_REPLACEMENT_BYTES 262144u
#define REGEX_PCRE2_MAX_MATCHES 10000u
#define REGEX_PCRE2_MAX_CAPTURE_ROWS 100000u
#define REGEX_PCRE2_MAX_CAPTURE_GROUPS 1000u
#define REGEX_PCRE2_MAX_MATCH_LIMIT 100000000u
#define REGEX_PCRE2_MAX_DEPTH_LIMIT 100000u
#define REGEX_PCRE2_MAX_HEAP_LIMIT_KIB 131072u
#define REGEX_PCRE2_MAX_TRACE_EVENTS 50000u
#define REGEX_PCRE2_MAX_TRACE_BYTES 10485760u
#define REGEX_PCRE2_MAX_TRACE_MARK_BYTES 1024u
#define REGEX_PCRE2_UNSET_OFFSET UINT32_MAX
enum regex_pcre2_flag {
REGEX_PCRE2_CASELESS = 1u << 0,
REGEX_PCRE2_MULTILINE = 1u << 1,
REGEX_PCRE2_DOTALL = 1u << 2,
REGEX_PCRE2_EXTENDED = 1u << 3,
REGEX_PCRE2_UNGREEDY = 1u << 4,
REGEX_PCRE2_UTF = 1u << 5,
REGEX_PCRE2_UCP = 1u << 6,
REGEX_PCRE2_DUPNAMES = 1u << 7,
REGEX_PCRE2_GLOBAL = 1u << 8
};
enum regex_pcre2_config_flag {
REGEX_PCRE2_CONFIG_UNICODE = 1u << 0,
REGEX_PCRE2_CONFIG_JIT = 1u << 1
};
enum regex_pcre2_bridge_error {
REGEX_PCRE2_ERROR_INVALID_ARGUMENT = -10001,
REGEX_PCRE2_ERROR_UNSUPPORTED_FLAGS = -10002,
REGEX_PCRE2_ERROR_PATTERN_TOO_LARGE = -10003,
REGEX_PCRE2_ERROR_CONFIG = -10004,
REGEX_PCRE2_ERROR_SUBJECT_TOO_LARGE = -10005,
REGEX_PCRE2_ERROR_REPLACEMENT_TOO_LARGE = -10006,
REGEX_PCRE2_ERROR_LIMIT_OUT_OF_RANGE = -10007,
REGEX_PCRE2_ERROR_CAPTURE_LIMIT = -10008,
REGEX_PCRE2_ERROR_OUTPUT_BUFFER = -10009
};
enum regex_pcre2_error_phase {
REGEX_PCRE2_PHASE_NONE = 0,
REGEX_PCRE2_PHASE_BRIDGE = 1,
REGEX_PCRE2_PHASE_COMPILE = 2,
REGEX_PCRE2_PHASE_MATCH = 3,
REGEX_PCRE2_PHASE_SUBSTITUTE = 4
};
/*
* This 20-byte, five-u32 record is retained for deterministic compile-only
* build verification. Runtime callers should use the bounded APIs below.
*/
typedef struct regex_pcre2_compile_result {
int32_t status;
uint32_t error_offset;
uint32_t capture_count;
uint32_t name_count;
uint32_t effective_options;
} regex_pcre2_compile_result;
/* All values are required and validated against the bridge hard maxima. */
typedef struct regex_pcre2_limits {
uint32_t maximum_matches;
uint32_t maximum_capture_rows;
uint32_t match_limit;
uint32_t depth_limit;
uint32_t heap_limit_kib;
} regex_pcre2_limits;
/*
* One record is emitted for group zero and every capture group of each
* retained match. Unset captures use REGEX_PCRE2_UNSET_OFFSET for both bounds.
*/
typedef struct regex_pcre2_match_record {
uint32_t match_number;
uint32_t group_number;
uint32_t start_byte;
uint32_t end_byte;
} regex_pcre2_match_record;
/* Names are UTF-8 slices into the caller-owned name byte buffer. */
typedef struct regex_pcre2_name_record {
uint32_t group_number;
uint32_t name_offset;
uint32_t name_length;
} regex_pcre2_name_record;
/*
* Trace collection has an independent bound from match materialization. The
* byte cap covers the fixed event records plus copied mark bytes.
*/
typedef struct regex_pcre2_trace_limits {
uint32_t maximum_events;
uint32_t maximum_trace_bytes;
uint32_t match_limit;
uint32_t depth_limit;
uint32_t heap_limit_kib;
} regex_pcre2_trace_limits;
/*
* Every field except the mark slice is copied directly from a PCRE2 callout
* block. Positions and lengths are offsets in the 8-bit UTF-8 pattern or
* subject supplied to the engine.
*/
typedef struct regex_pcre2_trace_event {
uint32_t callout_number;
uint32_t pattern_position_byte;
uint32_t next_item_length_byte;
uint32_t subject_position_byte;
uint32_t capture_top;
uint32_t capture_last;
uint32_t mark_offset;
uint32_t mark_length;
} regex_pcre2_trace_event;
/*
* A zero status means that a bounded trace was returned. native_match_status
* retains pcre2_match()'s exact result, including PCRE2_ERROR_CALLOUT when the
* bridge deliberately stopped at a trace cap. last_complete_event is
* UINT32_MAX when no complete event was copied.
*/
typedef struct regex_pcre2_trace_result {
int32_t status;
uint32_t error_phase;
uint32_t error_offset;
int32_t native_match_status;
uint32_t effective_options;
uint32_t event_count;
uint32_t total_event_count;
uint32_t mark_bytes_length;
uint32_t trace_bytes_length;
uint32_t events_truncated;
uint32_t marks_truncated;
uint32_t last_complete_event;
uint32_t matched;
} regex_pcre2_trace_result;
/*
* This fixed 60-byte record contains no native pointers. `status` is zero on
* success. Otherwise `error_phase` distinguishes bridge validation, compile,
* match and replacement errors; `error_offset` is a UTF-8 byte offset when the
* phase provides one.
*/
typedef struct regex_pcre2_run_result {
int32_t status;
uint32_t error_phase;
uint32_t error_offset;
uint32_t capture_count;
uint32_t name_count;
uint32_t match_count;
uint32_t record_count;
uint32_t name_record_count;
uint32_t name_bytes_length;
uint32_t effective_options;
uint32_t results_truncated;
uint32_t names_truncated;
uint32_t output_length;
uint32_t substitution_count;
uint32_t output_truncated;
} regex_pcre2_run_result;
uint32_t regex_pcre2_bridge_abi_version(void);
uint32_t regex_pcre2_config_flags(void);
const uint8_t *regex_pcre2_version(void);
int32_t regex_pcre2_error_message(int32_t error_code,
uint8_t *output,
uint32_t output_capacity);
int32_t regex_pcre2_compile_probe(const uint8_t *pattern,
uint32_t pattern_length,
uint32_t application_flags,
regex_pcre2_compile_result *result);
int32_t regex_pcre2_execute(
const uint8_t *pattern,
uint32_t pattern_length,
const uint8_t *subject,
uint32_t subject_length,
uint32_t application_flags,
const regex_pcre2_limits *limits,
regex_pcre2_match_record *records,
uint32_t record_capacity,
regex_pcre2_name_record *name_records,
uint32_t name_record_capacity,
uint8_t *name_bytes,
uint32_t name_bytes_capacity,
regex_pcre2_run_result *result);
int32_t regex_pcre2_substitute(
const uint8_t *pattern,
uint32_t pattern_length,
const uint8_t *subject,
uint32_t subject_length,
const uint8_t *replacement,
uint32_t replacement_length,
uint32_t application_flags,
const regex_pcre2_limits *limits,
regex_pcre2_match_record *records,
uint32_t record_capacity,
regex_pcre2_name_record *name_records,
uint32_t name_record_capacity,
uint8_t *name_bytes,
uint32_t name_bytes_capacity,
/* `output` must have output_capacity + 1 writable bytes for PCRE2's NUL. */
uint8_t *output,
uint32_t output_capacity,
regex_pcre2_run_result *result);
int32_t regex_pcre2_trace(
const uint8_t *pattern,
uint32_t pattern_length,
const uint8_t *subject,
uint32_t subject_length,
uint32_t application_flags,
const regex_pcre2_trace_limits *limits,
regex_pcre2_trace_event *events,
uint32_t event_capacity,
uint8_t *mark_bytes,
uint32_t mark_bytes_capacity,
regex_pcre2_trace_result *result);
int32_t regex_pcre2_self_test(void);
#endif