420 lines
13 KiB
C++
420 lines
13 KiB
C++
#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);
|
|
}
|