diff --git a/src/node_url_pattern.cc b/src/node_url_pattern.cc index bc007db47da..fe727cceca3 100644 --- a/src/node_url_pattern.cc +++ b/src/node_url_pattern.cc @@ -6,8 +6,11 @@ #include "node_errors.h" #include "node_mem-inl.h" #include "path.h" +#include "simdutf.h" #include "util-inl.h" +#include + namespace node { using node::url_pattern::URLPatternRegexProvider; @@ -64,7 +67,6 @@ using v8::Local; using v8::LocalVector; using v8::MaybeLocal; using v8::Name; -using v8::NewStringType; using v8::Object; using v8::PropertyAttribute; using v8::ReadOnly; @@ -73,25 +75,117 @@ using v8::Signature; using v8::String; using v8::Value; +namespace { + +// String::ValueView holds DisallowGarbageCollection. Copy into a C++ buffer +// and destroy the view before creating any V8 heap objects. +void CopyV8StringToBuffer(Isolate* isolate, + Local str, + MaybeStackBuffer* buffer) { + if (str->Length() == 0) { + buffer->SetLength(0); + return; + } + { + String::ValueView view(isolate, str); + if (view.is_one_byte()) { + const auto* data = reinterpret_cast(view.data8()); + const size_t length = view.length(); + if (simdutf::validate_ascii(data, length)) [[likely]] { + buffer->AllocateSufficientStorage(length); + memcpy(buffer->out(), data, length); + buffer->SetLength(length); + return; + } + } + } + Utf8Value utf8(isolate, str); + buffer->AllocateSufficientStorage(utf8.length()); + if (utf8.length() != 0) { + memcpy(buffer->out(), *utf8, utf8.length()); + } + buffer->SetLength(utf8.length()); +} + +std::string V8StringToStdString(Isolate* isolate, Local str) { + if (str->Length() == 0) { + return std::string(); + } + { + String::ValueView view(isolate, str); + if (view.is_one_byte()) { + const auto* data = reinterpret_cast(view.data8()); + const size_t length = view.length(); + if (simdutf::validate_ascii(data, length)) [[likely]] { + return std::string(data, length); + } + } + } + Utf8Value utf8(isolate, str); + return utf8.ToString(); +} + +// ada::url_pattern_input holds a string_view, so input_buf/base_buf must +// outlive the subsequent test/exec call. +bool ExtractInputAndBaseURL(Environment* env, + const FunctionCallbackInfo& args, + ada::url_pattern_input* input, + std::optional* base_url, + MaybeStackBuffer* input_buf, + MaybeStackBuffer* base_buf) { + Isolate* isolate = env->isolate(); + if (args.Length() == 0 || args[0]->IsNullOrUndefined()) { + *input = ada::url_pattern_init{}; + } else if (args[0]->IsString()) { + CopyV8StringToBuffer(isolate, args[0].As(), input_buf); + *input = input_buf->ToStringView(); + } else if (args[0]->IsObject()) { + auto maybe_input = + URLPattern::URLPatternInit::FromJsObject(env, args[0].As()); + if (!maybe_input.has_value()) { + return false; + } + *input = std::move(*maybe_input); + } else { + THROW_ERR_INVALID_ARG_TYPE( + env, "URLPattern input needs to be a string or an object"); + return false; + } + + if (args.Length() > 1 && !args[1]->IsUndefined()) { + if (args[1]->IsNull()) { + *base_url = std::string_view("null"); + } else if (args[1]->IsString()) { + CopyV8StringToBuffer(isolate, args[1].As(), base_buf); + *base_url = base_buf->ToStringView(); + } else { + THROW_ERR_INVALID_ARG_TYPE(env, "baseURL must be a string"); + return false; + } + } + return true; +} + +} // namespace + std::optional URLPatternRegexProvider::create_instance(std::string_view pattern, bool ignore_case) { auto isolate = Isolate::GetCurrent(); - auto env = Environment::GetCurrent(isolate); int flags = RegExp::Flags::kUnicodeSets | RegExp::Flags::kDotAll; if (ignore_case) { flags |= static_cast(RegExp::Flags::kIgnoreCase); } - Local local_pattern; - if (!String::NewFromUtf8( - isolate, pattern.data(), NewStringType::kNormal, pattern.size()) - .ToLocal(&local_pattern)) { + Local context = isolate->GetCurrentContext(); + Local pattern_value; + if (!ToV8Value(context, pattern, isolate).ToLocal(&pattern_value)) { return std::nullopt; } Local regexp; - if (!RegExp::New( - env->context(), local_pattern, static_cast(flags)) + if (!RegExp::New(context, + pattern_value.As(), + static_cast(flags)) .ToLocal(®exp)) { return std::nullopt; } @@ -101,16 +195,14 @@ URLPatternRegexProvider::create_instance(std::string_view pattern, bool URLPatternRegexProvider::regex_match(std::string_view input, const regex_type& pattern) { auto isolate = Isolate::GetCurrent(); - auto env = Environment::GetCurrent(isolate); - Local local_input; - if (!String::NewFromUtf8( - isolate, input.data(), NewStringType::kNormal, input.size()) - .ToLocal(&local_input)) { + Local context = isolate->GetCurrentContext(); + Local input_value; + if (!ToV8Value(context, input, isolate).ToLocal(&input_value)) { return false; } Local result_object; if (!pattern.Get(isolate) - ->Exec(env->context(), local_input) + ->Exec(context, input_value.As()) .ToLocal(&result_object)) { return false; } @@ -122,16 +214,14 @@ std::optional>> URLPatternRegexProvider::regex_search(std::string_view input, const regex_type& global_pattern) { auto isolate = Isolate::GetCurrent(); - auto env = Environment::GetCurrent(isolate); - Local local_input; - if (!String::NewFromUtf8( - isolate, input.data(), NewStringType::kNormal, input.size()) - .ToLocal(&local_input)) { + Local context = isolate->GetCurrentContext(); + Local input_value; + if (!ToV8Value(context, input, isolate).ToLocal(&input_value)) { return std::nullopt; } Local exec_result_object; auto pattern = global_pattern.Get(isolate); - if (!pattern->Exec(env->context(), local_input) + if (!pattern->Exec(context, input_value.As()) .ToLocal(&exec_result_object) || exec_result_object->IsNull()) { return std::nullopt; @@ -143,7 +233,7 @@ URLPatternRegexProvider::regex_search(std::string_view input, result.reserve(len); for (size_t i = 1; i < len; i++) { Local entry; - if (!exec_result->Get(env->context(), i).ToLocal(&entry)) { + if (!exec_result->Get(context, i).ToLocal(&entry)) { return std::nullopt; } @@ -153,8 +243,7 @@ URLPatternRegexProvider::regex_search(std::string_view input, if (entry->IsUndefined()) { result.emplace_back(std::nullopt); } else if (entry->IsString()) { - Utf8Value utf8_entry(isolate, entry.As()); - result.emplace_back(utf8_entry.ToString()); + result.emplace_back(V8StringToStdString(isolate, entry.As())); } } return result; @@ -193,9 +282,12 @@ void URLPattern::New(const FunctionCallbackInfo& args) { } std::optional init{}; - std::optional input{}; - std::optional base_url{}; + MaybeStackBuffer input_buf; + MaybeStackBuffer base_url_buf; + std::optional input{}; + std::optional base_url{}; std::optional options{}; + Isolate* isolate = env->isolate(); // Following patterns are supported: // - new URLPattern(input) @@ -207,9 +299,8 @@ void URLPattern::New(const FunctionCallbackInfo& args) { if (args[0]->IsNullOrUndefined()) { init = ada::url_pattern_init{}; } else if (args[0]->IsString()) { - BufferValue input_buffer(env->isolate(), args[0]); - CHECK_NOT_NULL(*input_buffer); - input = input_buffer.ToString(); + CopyV8StringToBuffer(isolate, args[0].As(), &input_buf); + input = input_buf.ToStringView(); } else if (args[0]->IsObject()) { init = URLPatternInit::FromJsObject(env, args[0].As()); // If init does not have a value here, the implication is that an @@ -230,13 +321,12 @@ void URLPattern::New(const FunctionCallbackInfo& args) { // USVString ("null"/"undefined"), which will be rejected as invalid // URLs by ada downstream. if (args[1]->IsString()) { - BufferValue base_url_buffer(env->isolate(), args[1]); - CHECK_NOT_NULL(*base_url_buffer); - base_url = base_url_buffer.ToString(); + CopyV8StringToBuffer(isolate, args[1].As(), &base_url_buf); + base_url = base_url_buf.ToStringView(); } else if (args[1]->IsNull()) { - base_url = std::string("null"); + base_url = std::string_view("null"); } else if (args[1]->IsUndefined()) { - base_url = std::string("undefined"); + base_url = std::string_view("undefined"); } else { THROW_ERR_INVALID_ARG_TYPE(env, "second argument must be a string"); return; @@ -257,9 +347,8 @@ void URLPattern::New(const FunctionCallbackInfo& args) { // Overload resolution: string is overload 1 (baseURL), // otherwise overload 2 (options). if (args[1]->IsString()) { - BufferValue base_url_buffer(env->isolate(), args[1]); - CHECK_NOT_NULL(*base_url_buffer); - base_url = base_url_buffer.ToString(); + CopyV8StringToBuffer(isolate, args[1].As(), &base_url_buf); + base_url = base_url_buf.ToStringView(); } else if (args[1]->IsNullOrUndefined()) { // Overload 2, options uses default. } else if (args[1]->IsObject()) { @@ -276,19 +365,16 @@ void URLPattern::New(const FunctionCallbackInfo& args) { // Either url_pattern_init or input as a string must be provided. CHECK_IMPLIES(init.has_value(), !input.has_value()); - std::string_view base_url_view{}; - if (base_url) base_url_view = {base_url->data(), base_url->size()}; - ada::url_pattern_input arg0; if (init.has_value()) { arg0 = std::move(*init); } else { - arg0 = std::move(*input); + arg0 = *input; } auto url_pattern = parse_url_pattern( std::move(arg0), - base_url ? &base_url_view : nullptr, + base_url ? &*base_url : nullptr, options.has_value() ? &options.value() : nullptr); if (!url_pattern) { @@ -358,7 +444,9 @@ MaybeLocal URLPattern::URLPatternInit::ToJsObject( std::optional URLPattern::URLPatternInit::FromJsObject( Environment* env, Local obj) { ada::url_pattern_init init{}; - Local components[] = { + Isolate* isolate = env->isolate(); + Local context = env->context(); + Local names[] = { env->protocol_string(), env->username_string(), env->password_string(), @@ -369,41 +457,31 @@ std::optional URLPattern::URLPatternInit::FromJsObject( env->hash_string(), env->base_url_string(), }; - auto isolate = env->isolate(); - const auto set_parameter = [&](std::string_view key, std::string_view value) { - if (key == "protocol") { - init.protocol = std::string(value); - } else if (key == "username") { - init.username = std::string(value); - } else if (key == "password") { - init.password = std::string(value); - } else if (key == "hostname") { - init.hostname = std::string(value); - } else if (key == "port") { - init.port = std::string(value); - } else if (key == "pathname") { - init.pathname = std::string(value); - } else if (key == "search") { - init.search = std::string(value); - } else if (key == "hash") { - init.hash = std::string(value); - } else if (key == "baseURL") { - init.base_url = std::string(value); - } + std::optional* fields[] = { + &init.protocol, + &init.username, + &init.password, + &init.hostname, + &init.port, + &init.pathname, + &init.search, + &init.hash, + &init.base_url, }; - Local value; - for (const auto& component : components) { - Utf8Value key(isolate, component); - if (obj->Get(env->context(), component).ToLocal(&value)) { - if (value->IsString()) { - Utf8Value utf8_value(isolate, value); - set_parameter(key.ToStringView(), utf8_value.ToStringView()); - } - } else { + static_assert(arraysize(names) == arraysize(fields)); + + for (size_t i = 0; i < arraysize(names); i++) { + Local value; + if (!obj->Get(context, names[i]).ToLocal(&value)) { // If ToLocal failed then we assume an error occurred, // bail out early to propagate the error. return std::nullopt; } + // Non-string values are ignored. This matches the previous binding + // and avoids a ToString of numbers or other primitives. + if (value->IsString()) { + *fields[i] = V8StringToStdString(isolate, value.As()); + } } return init; } @@ -577,40 +655,16 @@ void URLPattern::Exec(const FunctionCallbackInfo& args) { auto env = Environment::GetCurrent(args); ada::url_pattern_input input; - std::optional baseURL{}; - std::string input_base; - if (args.Length() == 0 || args[0]->IsNullOrUndefined()) { - input = ada::url_pattern_init{}; - } else if (args[0]->IsString()) { - Utf8Value input_value(env->isolate(), args[0].As()); - input_base = input_value.ToString(); - input = std::string_view(input_base); - } else if (args[0]->IsObject()) { - auto maybeInput = URLPatternInit::FromJsObject(env, args[0].As()); - if (!maybeInput.has_value()) return; - input = std::move(*maybeInput); - } else { - THROW_ERR_INVALID_ARG_TYPE( - env, "URLPattern input needs to be a string or an object"); + std::optional base_url; + MaybeStackBuffer input_buf; + MaybeStackBuffer base_buf; + if (!ExtractInputAndBaseURL( + env, args, &input, &base_url, &input_buf, &base_buf)) { return; } - if (args.Length() > 1 && !args[1]->IsUndefined()) { - if (args[1]->IsNull()) { - baseURL = std::string("null"); - } else if (args[1]->IsString()) { - Utf8Value base_url_value(env->isolate(), args[1].As()); - baseURL = base_url_value.ToStringView(); - } else { - THROW_ERR_INVALID_ARG_TYPE(env, "baseURL must be a string"); - return; - } - } - Local result; - std::optional baseURL_opt = - baseURL ? std::optional(*baseURL) : std::nullopt; - if (!url_pattern->Exec(env, input, baseURL_opt).ToLocal(&result)) { + if (!url_pattern->Exec(env, input, base_url).ToLocal(&result)) { THROW_ERR_OPERATION_FAILED(env, "Failed to exec URLPattern"); return; } @@ -623,39 +677,15 @@ void URLPattern::Test(const FunctionCallbackInfo& args) { auto env = Environment::GetCurrent(args); ada::url_pattern_input input; - std::optional baseURL{}; - std::string input_base; - if (args.Length() == 0 || args[0]->IsNullOrUndefined()) { - input = ada::url_pattern_init{}; - } else if (args[0]->IsString()) { - Utf8Value input_value(env->isolate(), args[0].As()); - input_base = input_value.ToString(); - input = std::string_view(input_base); - } else if (args[0]->IsObject()) { - auto maybeInput = URLPatternInit::FromJsObject(env, args[0].As()); - if (!maybeInput.has_value()) return; - input = std::move(*maybeInput); - } else { - THROW_ERR_INVALID_ARG_TYPE( - env, "URLPattern input needs to be a string or an object"); + std::optional base_url; + MaybeStackBuffer input_buf; + MaybeStackBuffer base_buf; + if (!ExtractInputAndBaseURL( + env, args, &input, &base_url, &input_buf, &base_buf)) { return; } - if (args.Length() > 1 && !args[1]->IsUndefined()) { - if (args[1]->IsNull()) { - baseURL = std::string("null"); - } else if (args[1]->IsString()) { - Utf8Value base_url_value(env->isolate(), args[1].As()); - baseURL = base_url_value.ToStringView(); - } else { - THROW_ERR_INVALID_ARG_TYPE(env, "baseURL must be a string"); - return; - } - } - - std::optional baseURL_opt = - baseURL ? std::optional(*baseURL) : std::nullopt; - args.GetReturnValue().Set(url_pattern->Test(env, input, baseURL_opt)); + args.GetReturnValue().Set(url_pattern->Test(env, input, base_url)); } #define URL_PATTERN_COMPONENT_GETTERS(uppercase_name, lowercase_name) \ diff --git a/test/parallel/test-urlpattern-fast-path.js b/test/parallel/test-urlpattern-fast-path.js new file mode 100644 index 00000000000..69bff988dae --- /dev/null +++ b/test/parallel/test-urlpattern-fast-path.js @@ -0,0 +1,130 @@ +'use strict'; + +// Tests for the URLPattern constructor / test / exec C++ fast paths. + +require('../common'); +const assert = require('assert'); +const { URLPattern } = require('url'); + +{ + const pattern = new URLPattern('https://example.com/foo'); + assert.strictEqual(pattern.protocol, 'https'); + assert.strictEqual(pattern.hostname, 'example.com'); + assert.strictEqual(pattern.pathname, '/foo'); + assert.strictEqual(pattern.test('https://example.com/foo'), true); + assert.strictEqual(pattern.test('https://example.com/bar'), false); + const result = pattern.exec('https://example.com/foo'); + assert.strictEqual(result.hostname.input, 'example.com'); + assert.strictEqual(result.pathname.input, '/foo'); + assert.strictEqual(result.protocol.input, 'https'); +} + +{ + const pattern = new URLPattern('/foo', 'https://example.com'); + assert.strictEqual(pattern.protocol, 'https'); + assert.strictEqual(pattern.hostname, 'example.com'); + assert.strictEqual(pattern.pathname, '/foo'); + assert.strictEqual(pattern.test('https://example.com/foo'), true); + assert.strictEqual(pattern.test('/foo', 'https://example.com'), true); + const result = pattern.exec('/foo', 'https://example.com'); + assert.strictEqual(result.hostname.input, 'example.com'); + assert.strictEqual(result.pathname.input, '/foo'); +} + +{ + const pattern = new URLPattern({ + pathname: '/foo', + search: 'bar', + hash: 'baz', + baseURL: 'https://example.com:8080', + }); + assert.strictEqual(pattern.protocol, 'https'); + assert.strictEqual(pattern.hostname, 'example.com'); + assert.strictEqual(pattern.port, '8080'); + assert.strictEqual(pattern.pathname, '/foo'); + assert.strictEqual(pattern.search, 'bar'); + assert.strictEqual(pattern.hash, 'baz'); + assert.strictEqual( + pattern.test('https://example.com:8080/foo?bar#baz'), + true, + ); +} + +{ + // Non-string init members are ignored. + const pattern = new URLPattern({ pathname: '/foo', port: 8080 }); + assert.strictEqual(pattern.pathname, '/foo'); + assert.strictEqual(pattern.port, '*'); +} + +{ + const pattern = new URLPattern({ + hostname: 'xn--caf-dma.com', + pathname: '/café', + }); + assert.strictEqual(pattern.hostname, 'xn--caf-dma.com'); + assert.strictEqual(pattern.pathname, '/caf%C3%A9'); + assert.strictEqual( + pattern.test({ hostname: 'xn--caf-dma.com', pathname: '/café' }), + true, + ); + const result = pattern.exec({ + hostname: 'xn--caf-dma.com', + pathname: '/café', + }); + assert.strictEqual(result.hostname.input, 'xn--caf-dma.com'); +} + +{ + const pattern = new URLPattern({ pathname: '/:value' }); + const result = pattern.exec('https://example.com/test'); + assert.strictEqual(result.pathname.groups.value, 'test'); + assert.strictEqual(result.pathname.input, '/test'); +} + +{ + const pattern = new URLPattern({ pathname: '/([a-z]+)' }); + assert.strictEqual(pattern.hasRegExpGroups, true); + assert.strictEqual(pattern.test({ pathname: '/abc' }), true); + assert.strictEqual(pattern.test({ pathname: '/123' }), false); + const result = pattern.exec({ pathname: '/abc' }); + assert.strictEqual(result.pathname.groups['0'], 'abc'); +} + +{ + const pattern = new URLPattern('https://*.example.com/foo'); + assert.strictEqual(pattern.test('https://sub.example.com/foo'), true); + assert.strictEqual(pattern.test('https://example.com/foo'), false); + const result = pattern.exec('https://sub.example.com/foo'); + assert.strictEqual(result.hostname.input, 'sub.example.com'); + assert.strictEqual(result.pathname.input, '/foo'); +} + +{ + // Constructor-string pattern used by the urlpattern-* benchmarks. + const pattern = new URLPattern('https://(sub.)?example(.com/)foo'); + assert.strictEqual(pattern.hostname, '(sub.)?example(.com/)foo'); + assert.strictEqual(pattern.test('https://sub.example.com/foo'), false); + assert.strictEqual(pattern.exec('https://sub.example.com/foo'), null); +} + +{ + const pattern = new URLPattern({ pathname: '/FOO' }, { ignoreCase: true }); + assert.strictEqual(pattern.test({ pathname: '/foo' }), true); + assert.strictEqual(pattern.test({ pathname: '/FOO' }), true); +} + +{ + const pattern = new URLPattern(); + assert.strictEqual(pattern.protocol, '*'); + assert.strictEqual(pattern.test('https://example.com/'), true); + assert.strictEqual(pattern.test(undefined), true); + assert.notStrictEqual(pattern.exec('https://example.com/'), null); +} + +{ + const pattern = new URLPattern({ protocol: 'https' }); + assert.strictEqual(pattern.test('https://example.com', undefined), true); + assert.strictEqual(pattern.test('https://example.com', null), false); + assert.strictEqual(pattern.exec('https://example.com', null), null); +}