/* 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;
}
