feat: add multi-engine regex flavour support
This commit is contained in:
34
engines/cpp/README.md
Normal file
34
engines/cpp/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# C++ regex engine
|
||||
|
||||
This bridge executes patterns with the `std::wregex` implementation shipped in
|
||||
Emscripten 6.0.4's libc++. The default grammar is the C++ standard library's
|
||||
modified ECMAScript grammar; it is not presented as modern browser ECMAScript.
|
||||
|
||||
The fixed Embind entry points accept structured values only. User patterns,
|
||||
subjects and replacement templates are passed as data and are never evaluated
|
||||
as JavaScript or C++ source. The worker remains subject to Regex Tools' hard
|
||||
timeout and result limits.
|
||||
|
||||
Replacement is streamed through the output byte limit with libc++
|
||||
`match_results::format` default grammar. A load-time differential self-test
|
||||
compares bounded fixture output with the native API; capture amplification
|
||||
never materializes a whole per-match expansion.
|
||||
|
||||
Build with the pinned Emscripten checkout:
|
||||
|
||||
```sh
|
||||
node scripts/cpp-engine-pack.mjs --empp /absolute/path/to/em++
|
||||
node scripts/install-cpp-engine.mjs
|
||||
```
|
||||
|
||||
The builder enables `wasm-ld --trace` and accepts only the reviewed archive
|
||||
set. The generated `SOURCE-MANIFEST.json` records the exact Emscripten, emsdk
|
||||
and LLVM identities; maps the selected Emscripten runtime, musl 1.2.6,
|
||||
emmalloc, compiler-rt 22.1.8, libc++ 21.1.8 and libc++abi 21.1.8 sources to
|
||||
their pinned legal files; and records that JavaScript exceptions select no
|
||||
libunwind object. The libunwind legal text is retained so that exclusion stays
|
||||
explicit and reviewable.
|
||||
|
||||
Native positions are Unicode code-point offsets because the bridge deliberately
|
||||
uses 32-bit `wchar_t`; the TypeScript adapter preserves them and separately
|
||||
normalizes editor ranges to UTF-16.
|
||||
419
engines/cpp/cpp_regex_bridge.cpp
Normal file
419
engines/cpp/cpp_regex_bridge.cpp
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user