feat: add multi-engine regex flavour support

This commit is contained in:
2026-07-27 11:43:51 +02:00
parent 7079cde15f
commit 7f3a91ad37
340 changed files with 643286 additions and 483 deletions

View File

@@ -0,0 +1,419 @@
#include <emscripten/bind.h>
#include <emscripten/version.h>
#include <cstdint>
#include <regex>
#include <stdexcept>
#include <string>
namespace {
using emscripten::val;
constexpr int kBridgeAbiVersion = 1;
constexpr std::size_t kMaximumCaptureGroups = 1000;
std::wstring decode_utf8(const std::string& input) {
std::wstring output;
output.reserve(input.size());
for (std::size_t index = 0; index < input.size();) {
const auto first = static_cast<unsigned char>(input[index]);
std::uint32_t code_point = 0;
std::size_t width = 0;
if (first <= 0x7f) {
code_point = first;
width = 1;
} else if ((first & 0xe0) == 0xc0) {
code_point = first & 0x1f;
width = 2;
} else if ((first & 0xf0) == 0xe0) {
code_point = first & 0x0f;
width = 3;
} else if ((first & 0xf8) == 0xf0) {
code_point = first & 0x07;
width = 4;
} else {
throw std::invalid_argument("Input is not valid UTF-8.");
}
if (index + width > input.size()) {
throw std::invalid_argument("Input ends inside a UTF-8 sequence.");
}
for (std::size_t offset = 1; offset < width; ++offset) {
const auto continuation =
static_cast<unsigned char>(input[index + offset]);
if ((continuation & 0xc0) != 0x80) {
throw std::invalid_argument("Input contains invalid UTF-8.");
}
code_point = (code_point << 6) | (continuation & 0x3f);
}
const bool overlong = (width == 2 && code_point < 0x80) ||
(width == 3 && code_point < 0x800) ||
(width == 4 && code_point < 0x10000);
if (overlong || code_point > 0x10ffff ||
(code_point >= 0xd800 && code_point <= 0xdfff)) {
throw std::invalid_argument("Input contains invalid Unicode.");
}
output.push_back(static_cast<wchar_t>(code_point));
index += width;
}
return output;
}
void append_utf8_code_point(std::string& output, std::uint32_t code_point) {
if (code_point <= 0x7f) {
output.push_back(static_cast<char>(code_point));
} else if (code_point <= 0x7ff) {
output.push_back(static_cast<char>(0xc0 | (code_point >> 6)));
output.push_back(static_cast<char>(0x80 | (code_point & 0x3f)));
} else if (code_point <= 0xffff) {
output.push_back(static_cast<char>(0xe0 | (code_point >> 12)));
output.push_back(static_cast<char>(0x80 | ((code_point >> 6) & 0x3f)));
output.push_back(static_cast<char>(0x80 | (code_point & 0x3f)));
} else {
output.push_back(static_cast<char>(0xf0 | (code_point >> 18)));
output.push_back(
static_cast<char>(0x80 | ((code_point >> 12) & 0x3f)));
output.push_back(static_cast<char>(0x80 | ((code_point >> 6) & 0x3f)));
output.push_back(static_cast<char>(0x80 | (code_point & 0x3f)));
}
}
std::string encode_utf8(const std::wstring& input) {
std::string output;
output.reserve(input.size());
for (const wchar_t character : input) {
append_utf8_code_point(output, static_cast<std::uint32_t>(character));
}
return output;
}
class BoundedOutput {
public:
explicit BoundedOutput(std::size_t maximum_bytes)
: maximum_bytes_(maximum_bytes) {}
void append(
std::wstring::const_iterator first,
std::wstring::const_iterator last) {
if (truncated_) return;
for (; first != last; ++first) {
std::string encoded;
append_utf8_code_point(
encoded, static_cast<std::uint32_t>(*first));
if (value_.size() + encoded.size() > maximum_bytes_) {
truncated_ = true;
return;
}
value_ += encoded;
}
}
void append(const std::wstring& value) {
append(value.cbegin(), value.cend());
}
const std::string& value() const { return value_; }
bool truncated() const { return truncated_; }
private:
std::size_t maximum_bytes_;
std::string value_;
bool truncated_ = false;
};
template <typename Iterator>
void append_native_format_bounded(
BoundedOutput& output,
const std::match_results<Iterator>& match,
const std::wstring& replacement) {
for (std::size_t index = 0;
index < replacement.size() && !output.truncated();
++index) {
if (replacement[index] != L'$' || index + 1 == replacement.size()) {
output.append(
replacement.cbegin() + index,
replacement.cbegin() + index + 1);
continue;
}
const wchar_t next = replacement[index + 1];
if (next == L'$') {
output.append(
replacement.cbegin() + index + 1,
replacement.cbegin() + index + 2);
++index;
} else if (next == L'&') {
output.append(match[0].first, match[0].second);
++index;
} else if (next == L'`') {
output.append(match.prefix().first, match.prefix().second);
++index;
} else if (next == L'\'') {
output.append(match.suffix().first, match.suffix().second);
++index;
} else if (next >= L'0' && next <= L'9') {
std::size_t capture = static_cast<std::size_t>(next - L'0');
++index;
if (index + 1 < replacement.size() &&
replacement[index + 1] >= L'0' &&
replacement[index + 1] <= L'9') {
capture =
capture * 10 +
static_cast<std::size_t>(replacement[index + 1] - L'0');
++index;
}
output.append(match[capture].first, match[capture].second);
} else {
output.append(
replacement.cbegin() + index,
replacement.cbegin() + index + 1);
}
}
}
bool replacement_expansion_self_test() {
const std::wstring subject = L"xab!";
const std::wregex expression(L"(a)(b)?");
std::wsmatch match;
if (!std::regex_search(subject, match, expression)) return false;
const std::wstring fixtures[] = {
L"<$1:$2:$&>",
L"$$-$`-$'",
L"$0-$00-$01-$99",
L"$x-$",
};
for (const auto& replacement : fixtures) {
BoundedOutput bounded(4096);
append_native_format_bounded(bounded, match, replacement);
if (bounded.truncated() ||
bounded.value() != encode_utf8(match.format(replacement))) {
return false;
}
}
const std::wstring large_subject(4096, L'a');
const std::wregex large_expression(L"(a+)");
std::wsmatch large_match;
if (!std::regex_search(
large_subject, large_match, large_expression)) {
return false;
}
std::wstring amplifying_replacement;
amplifying_replacement.reserve(65'536);
for (std::size_t index = 0; index < 32'768; ++index) {
amplifying_replacement += L"$1";
}
BoundedOutput adversarial(5);
append_native_format_bounded(
adversarial, large_match, amplifying_replacement);
return adversarial.truncated() && adversarial.value() == "aaaaa";
}
val failure(
const std::string& phase,
const std::string& code,
const std::string& message) {
val result = val::object();
result.set("abi", kBridgeAbiVersion);
result.set("ok", false);
result.set("phase", phase);
result.set("code", code);
result.set(
"message",
message.size() <= 1024 ? message : message.substr(0, 1024));
return result;
}
std::regex_constants::syntax_option_type syntax_options(
const val& request) {
using namespace std::regex_constants;
auto result = syntax_option_type{};
const val options = request["options"];
std::string grammar = "ECMAScript";
const val configured_grammar = options["grammar"];
if (!configured_grammar.isUndefined()) {
grammar = configured_grammar.as<std::string>();
}
if (grammar == "ECMAScript") {
result = ECMAScript;
} else if (grammar == "basic") {
result = basic;
} else if (grammar == "extended") {
result = extended;
} else if (grammar == "awk") {
result = awk;
} else if (grammar == "grep") {
result = grep;
} else if (grammar == "egrep") {
result = egrep;
} else {
throw std::invalid_argument("Unsupported std::regex grammar.");
}
const std::string flags = request["flags"].as<std::string>();
if (flags.find('i') != std::string::npos) result |= icase;
if (flags.find('n') != std::string::npos) result |= nosubs;
if (flags.find('o') != std::string::npos) result |= optimize;
if (flags.find('c') != std::string::npos) result |= collate;
return result;
}
template <typename Iterator>
val match_record(
const std::match_results<Iterator>& match,
Iterator subject_begin) {
val record = val::object();
val span = val::array();
span.call<void>(
"push",
static_cast<double>(std::distance(subject_begin, match[0].first)));
span.call<void>(
"push",
static_cast<double>(std::distance(subject_begin, match[0].second)));
record.set("span", span);
val captures = val::array();
for (std::size_t group = 1; group < match.size(); ++group) {
if (!match[group].matched) {
captures.call<void>("push", val::null());
continue;
}
val capture = val::array();
capture.call<void>(
"push",
static_cast<double>(
std::distance(subject_begin, match[group].first)));
capture.call<void>(
"push",
static_cast<double>(
std::distance(subject_begin, match[group].second)));
captures.call<void>("push", capture);
}
record.set("captures", captures);
return record;
}
val cpp_regex_identity() {
val identity = val::object();
identity.set("implementation", "libc++ std::wregex");
identity.set("cplusplus", static_cast<double>(__cplusplus));
#ifdef __EMSCRIPTEN_MAJOR__
identity.set(
"emscripten",
std::to_string(__EMSCRIPTEN_MAJOR__) + "." +
std::to_string(__EMSCRIPTEN_MINOR__) + "." +
std::to_string(__EMSCRIPTEN_TINY__));
#else
identity.set("emscripten", "unknown");
#endif
identity.set("wcharBits", static_cast<double>(sizeof(wchar_t) * 8));
identity.set(
"replacementSelfTest", replacement_expansion_self_test());
return identity;
}
val cpp_regex_run(const val& request) {
try {
if (request["abi"].as<int>() != kBridgeAbiVersion) {
return failure("bridge", "unsupported-abi", "Unsupported bridge ABI.");
}
const std::string operation = request["operation"].as<std::string>();
if (operation != "execute" && operation != "replace") {
return failure(
"bridge", "invalid-operation", "Unsupported bridge operation.");
}
const std::wstring pattern =
decode_utf8(request["pattern"].as<std::string>());
const std::wstring subject =
decode_utf8(request["subject"].as<std::string>());
const std::wstring replacement =
operation == "replace"
? decode_utf8(request["replacement"].as<std::string>())
: std::wstring();
const std::string flags = request["flags"].as<std::string>();
const bool global = flags.find('g') != std::string::npos;
const auto maximum_matches =
request["maximumMatches"].as<std::uint32_t>();
const auto maximum_capture_rows =
request["maximumCaptureRows"].as<std::uint32_t>();
const auto maximum_output_bytes =
operation == "replace"
? request["maximumOutputBytes"].as<std::uint32_t>()
: 1U;
std::wregex expression;
try {
expression.assign(pattern, syntax_options(request));
} catch (const std::regex_error& error) {
return failure("compile", "regex-error", error.what());
}
const std::size_t group_count = expression.mark_count();
if (group_count > kMaximumCaptureGroups) {
return failure(
"compile",
"capture-group-limit",
"The pattern exceeds the 1,000 capture-group bridge limit.");
}
val matches = val::array();
std::size_t match_count = 0;
std::size_t capture_rows = 0;
bool results_truncated = false;
BoundedOutput output(maximum_output_bytes);
auto next_output = subject.cbegin();
using Iterator = std::wstring::const_iterator;
std::regex_iterator<Iterator> current(
subject.cbegin(), subject.cend(), expression);
const std::regex_iterator<Iterator> end;
for (; current != end; ++current) {
const auto& match = *current;
const std::size_t rows = std::max<std::size_t>(1, group_count);
if (match_count >= maximum_matches ||
capture_rows + rows > maximum_capture_rows) {
results_truncated = true;
break;
}
matches.call<void>(
"push", match_record(match, subject.cbegin()));
++match_count;
capture_rows += rows;
if (operation == "replace") {
output.append(next_output, match[0].first);
append_native_format_bounded(output, match, replacement);
next_output = match[0].second;
}
if (!global) {
++current;
break;
}
}
if (operation == "replace") {
output.append(next_output, subject.cend());
}
val result = val::object();
result.set("abi", kBridgeAbiVersion);
result.set("ok", true);
result.set("groupCount", static_cast<double>(group_count));
result.set("groupNames", val::array());
result.set("matches", matches);
result.set("resultsTruncated", results_truncated);
if (operation == "replace") {
result.set("output", output.value());
result.set(
"outputBytes", static_cast<double>(output.value().size()));
result.set("outputTruncated", output.truncated());
}
return result;
} catch (const std::exception& error) {
return failure("bridge", "bridge-error", error.what());
} catch (...) {
return failure(
"bridge", "bridge-error", "Unknown C++ bridge failure.");
}
}
} // namespace
EMSCRIPTEN_BINDINGS(regex_tools_cpp_bridge) {
emscripten::function("regexToolsCppIdentity", &cpp_regex_identity);
emscripten::function("regexToolsCppRun", &cpp_regex_run);
}