#include #include #include #include #include #include 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(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(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(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(code_point)); } else if (code_point <= 0x7ff) { output.push_back(static_cast(0xc0 | (code_point >> 6))); output.push_back(static_cast(0x80 | (code_point & 0x3f))); } else if (code_point <= 0xffff) { output.push_back(static_cast(0xe0 | (code_point >> 12))); output.push_back(static_cast(0x80 | ((code_point >> 6) & 0x3f))); output.push_back(static_cast(0x80 | (code_point & 0x3f))); } else { output.push_back(static_cast(0xf0 | (code_point >> 18))); output.push_back( static_cast(0x80 | ((code_point >> 12) & 0x3f))); output.push_back(static_cast(0x80 | ((code_point >> 6) & 0x3f))); output.push_back(static_cast(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(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(*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 void append_native_format_bounded( BoundedOutput& output, const std::match_results& 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(next - L'0'); ++index; if (index + 1 < replacement.size() && replacement[index + 1] >= L'0' && replacement[index + 1] <= L'9') { capture = capture * 10 + static_cast(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(); } 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(); 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 val match_record( const std::match_results& match, Iterator subject_begin) { val record = val::object(); val span = val::array(); span.call( "push", static_cast(std::distance(subject_begin, match[0].first))); span.call( "push", static_cast(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("push", val::null()); continue; } val capture = val::array(); capture.call( "push", static_cast( std::distance(subject_begin, match[group].first))); capture.call( "push", static_cast( std::distance(subject_begin, match[group].second))); captures.call("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(__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(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() != kBridgeAbiVersion) { return failure("bridge", "unsupported-abi", "Unsupported bridge ABI."); } const std::string operation = request["operation"].as(); if (operation != "execute" && operation != "replace") { return failure( "bridge", "invalid-operation", "Unsupported bridge operation."); } const std::wstring pattern = decode_utf8(request["pattern"].as()); const std::wstring subject = decode_utf8(request["subject"].as()); const std::wstring replacement = operation == "replace" ? decode_utf8(request["replacement"].as()) : std::wstring(); const std::string flags = request["flags"].as(); const bool global = flags.find('g') != std::string::npos; const auto maximum_matches = request["maximumMatches"].as(); const auto maximum_capture_rows = request["maximumCaptureRows"].as(); const auto maximum_output_bytes = operation == "replace" ? request["maximumOutputBytes"].as() : 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 current( subject.cbegin(), subject.cend(), expression); const std::regex_iterator end; for (; current != end; ++current) { const auto& match = *current; const std::size_t rows = std::max(1, group_count); if (match_count >= maximum_matches || capture_rows + rows > maximum_capture_rows) { results_truncated = true; break; } matches.call( "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(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(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); }