diff --git a/lib/internal/url.js b/lib/internal/url.js index be6413eee86a..b5f665ea4959 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -6,11 +6,14 @@ const { ArrayPrototypeJoin, ArrayPrototypeMap, ArrayPrototypePush, + ArrayPrototypeSymbolIterator, Boolean, Int8Array, IteratorPrototype, Number, ObjectDefineProperties, + ObjectGetOwnPropertySymbols, + ObjectKeys, ObjectSetPrototypeOf, ReflectGetOwnPropertyDescriptor, ReflectOwnKeys, @@ -356,6 +359,11 @@ class URLSearchParams { // mutation so repeated toString()/URL.href reads stay cheap. #serialized; + // Last name passed to get/has/getAll, after USVString conversion. + // Repeated lookups of the same string skip toWellFormed. + #nameKey; + #nameUSV; + static { setURLSearchParamsContext = (obj, ctx) => { obj.#context = ctx; @@ -378,6 +386,19 @@ class URLSearchParams { } } + #usvName(name) { + if (typeof name === 'string') { + if (name === this.#nameKey) { + return this.#nameUSV; + } + const usv = StringPrototypeToWellFormed(name); + this.#nameKey = name; + this.#nameUSV = usv; + return usv; + } + return toUSVString(name); + } + // URL Standard says the default value is '', but as undefined and '' have // the same result, undefined is used to prevent unnecessary parsing. // Default parameter is necessary to keep URLSearchParams.length === 0 in @@ -411,42 +432,39 @@ class URLSearchParams { // with a significant cost (~40-80%). In order optimize URLSearchParams // creation duration, Node.js merges the iteration and converting // iterations into a single iteration. - for (const pair of init) { - if (pair == null) { - throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]'); - } else if (ArrayIsArray(pair)) { - // If innerSequence's size is not 2, then throw a TypeError. - if (pair.length !== 2) { - throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]'); - } - // Append (innerSequence[0], innerSequence[1]) to query's list. - ArrayPrototypePush( - this.#searchParams, - toUSVString(pair[0]), - toUSVString(pair[1]), - ); + const list = this.#searchParams; + // Arrays that still use the default iterator are indexed directly. + // A replaced @@iterator, or a non-array iterable, keeps the generic loop. + if (method === ArrayPrototypeSymbolIterator && ArrayIsArray(init)) { + for (let i = 0; i < init.length; i++) { + appendQueryPair(list, init[i]); + } + } else { + for (const pair of init) { + appendQueryPair(list, pair); + } + } + } else if (ObjectGetOwnPropertySymbols(init).length === 0) { + // Plain records have no symbol keys. Object.keys matches the + // enumerable string keys Reflect.ownKeys would keep. + const visited = new SafeMap(); + const keys = ObjectKeys(init); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const typedKey = toUSVString(key); + const typedValue = toUSVString(init[key]); + const keyIdx = visited.get(typedKey); + if (keyIdx !== undefined) { + this.#searchParams[keyIdx] = typedValue; } else { - if (((typeof pair !== 'object' && typeof pair !== 'function') || - typeof pair[SymbolIterator] !== 'function')) { - throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]'); - } - - let length = 0; - - for (const element of pair) { - length++; - ArrayPrototypePush(this.#searchParams, toUSVString(element)); - } - - // If innerSequence's size is not 2, then throw a TypeError. - if (length !== 2) { - throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]'); - } + visited.set(typedKey, ArrayPrototypePush(this.#searchParams, + typedKey, + typedValue) - 1); } } } else { // Record - // Need to use reflection APIs for full spec compliance. + // Symbol keys and non-enumerable names need reflection. const visited = new SafeMap(); const keys = ReflectOwnKeys(init); for (let i = 0; i < keys.length; i++) { @@ -525,7 +543,7 @@ class URLSearchParams { throw new ERR_MISSING_ARGS('name', 'value'); } - name = toUSVString(name); + name = this.#usvName(name); value = toUSVString(value); ArrayPrototypePush(this.#searchParams, name, value); this.#markUpdated(); @@ -540,7 +558,7 @@ class URLSearchParams { } const list = this.#searchParams; - name = toUSVString(name); + name = this.#usvName(name); const { length } = list; let write = 0; @@ -588,7 +606,7 @@ class URLSearchParams { } const list = this.#searchParams; - name = toUSVString(name); + name = this.#usvName(name); const { length } = list; for (let i = 0; i < length; i += 2) { if (list[i] === name) { @@ -608,7 +626,7 @@ class URLSearchParams { const list = this.#searchParams; const values = []; - name = toUSVString(name); + name = this.#usvName(name); const { length } = list; for (let i = 0; i < length; i += 2) { if (list[i] === name) { @@ -627,7 +645,7 @@ class URLSearchParams { } const list = this.#searchParams; - name = toUSVString(name); + name = this.#usvName(name); if (value !== undefined) { value = toUSVString(value); @@ -654,7 +672,7 @@ class URLSearchParams { } const list = this.#searchParams; - name = toUSVString(name); + name = this.#usvName(name); value = toUSVString(value); const { length } = list; @@ -998,6 +1016,30 @@ class URL { } } + // Skip the ada reparse when the setter is handed the value already stored. + // `start`/`end` are the same href indexes the getter would slice. A length + // mismatch cannot be equal, so the changing-value path does not allocate + // that slice. A no-op write still flushes a dirty URLSearchParams: the + // previous path flushed from `#updateContext` after `bindingUrl.update`. + #commitComponent(action, value, start, end) { + if (typeof value !== 'string') { + value = `${value}`; + } + const href = this.#context.href; + const to = end === undefined ? href.length : end; + if (value.length === to - start && + (start === to || value === StringPrototypeSlice(href, start, to))) { + if (this.#searchParams != null) { + this.#ensureSearchParamsUpdated(); + } + return; + } + const updated = bindingUrl.update(href, action, value); + if (updated) { + this.#updateContext(updated); + } + } + toString() { // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync. this.#ensureSearchParamsUpdated(); @@ -1011,8 +1053,18 @@ class URL { } set href(value) { - value = `${value}`; - const href = bindingUrl.update(this.#context.href, updateActions.kHref, value); + if (typeof value !== 'string') { + value = `${value}`; + } + // Read the field before the private-method call so a wrong receiver throws + // "Cannot read private member", matching the other setters. + const ctx = this.#context; + this.#ensureSearchParamsUpdated(); + const current = ctx.href; + if (value.length === current.length && value === current) { + return; + } + const href = bindingUrl.update(current, updateActions.kHref, value); if (!href) { throw new ERR_INVALID_URL(value); } this.#updateContext(href, true); } @@ -1054,10 +1106,8 @@ class URL { } set protocol(value) { - const href = bindingUrl.update(this.#context.href, updateActions.kProtocol, `${value}`); - if (href) { - this.#updateContext(href); - } + const end = this.#context.protocol_end; + this.#commitComponent(updateActions.kProtocol, value, 0, end); } get username() { @@ -1068,9 +1118,12 @@ class URL { } set username(value) { - const href = bindingUrl.update(this.#context.href, updateActions.kUsername, `${value}`); - if (href) { - this.#updateContext(href); + const ctx = this.#context; + if (ctx.protocol_end + 2 < ctx.username_end) { + this.#commitComponent(updateActions.kUsername, value, + ctx.protocol_end + 2, ctx.username_end); + } else { + this.#commitComponent(updateActions.kUsername, value, 0, 0); } } @@ -1082,9 +1135,12 @@ class URL { } set password(value) { - const href = bindingUrl.update(this.#context.href, updateActions.kPassword, `${value}`); - if (href) { - this.#updateContext(href); + const ctx = this.#context; + if (ctx.host_start - ctx.username_end > 0) { + this.#commitComponent(updateActions.kPassword, value, + ctx.username_end + 1, ctx.host_start); + } else { + this.#commitComponent(updateActions.kPassword, value, 0, 0); } } @@ -1102,9 +1158,23 @@ class URL { } set host(value) { - const href = bindingUrl.update(this.#context.href, updateActions.kHost, `${value}`); - if (href) { - this.#updateContext(href); + const ctx = this.#context; + let startsAt = ctx.host_start; + if (ctx.href[startsAt] === '@') { + startsAt++; + } + // An empty host is not a no-op on non-special URLs: setting "" can insert + // an authority and reserialize the path (`a:/foo` becomes `a:///foo`). + if (startsAt === ctx.host_end) { + if (typeof value !== 'string') { + value = `${value}`; + } + const href = bindingUrl.update(ctx.href, updateActions.kHost, value); + if (href) { + this.#updateContext(href); + } + } else { + this.#commitComponent(updateActions.kHost, value, startsAt, ctx.pathname_start); } } @@ -1118,9 +1188,21 @@ class URL { } set hostname(value) { - const href = bindingUrl.update(this.#context.href, updateActions.kHostname, `${value}`); - if (href) { - this.#updateContext(href); + const ctx = this.#context; + let startsAt = ctx.host_start; + if (ctx.href[startsAt] === '@') { + startsAt++; + } + if (startsAt === ctx.host_end) { + if (typeof value !== 'string') { + value = `${value}`; + } + const href = bindingUrl.update(ctx.href, updateActions.kHostname, value); + if (href) { + this.#updateContext(href); + } + } else { + this.#commitComponent(updateActions.kHostname, value, startsAt, ctx.host_end); } } @@ -1132,7 +1214,18 @@ class URL { } set port(value) { - const href = bindingUrl.update(this.#context.href, updateActions.kPort, `${value}`); + // Port is serialized without the leading ':' in href, so it is not a slice. + const current = this.#context.hasPort ? `${this.#context.port}` : ''; + if (typeof value !== 'string') { + value = `${value}`; + } + if (value.length === current.length && value === current) { + if (this.#searchParams != null) { + this.#ensureSearchParamsUpdated(); + } + return; + } + const href = bindingUrl.update(this.#context.href, updateActions.kPort, value); if (href) { this.#updateContext(href); } @@ -1149,10 +1242,14 @@ class URL { } set pathname(value) { - const href = bindingUrl.update(this.#context.href, updateActions.kPathname, `${value}`); - if (href) { - this.#updateContext(href); + const ctx = this.#context; + let end; + if (ctx.hasSearch) { + end = ctx.search_start; + } else if (ctx.hasHash) { + end = ctx.hash_start; } + this.#commitComponent(updateActions.kPathname, value, ctx.pathname_start, end); } get search() { @@ -1162,7 +1259,14 @@ class URL { } set search(value) { - const href = bindingUrl.update(this.#context.href, updateActions.kSearch, StringPrototypeToWellFormed(`${value}`)); + value = StringPrototypeToWellFormed(typeof value === 'string' ? value : `${value}`); + const ctx = this.#context; + this.#ensureSearchParamsUpdated(); + // A lone "?" is omitted by the getter, but setting "" removes it. + if (value === this.#getSearchFromContext() && !(ctx.hasSearch && value === '')) { + return; + } + const href = bindingUrl.update(ctx.href, updateActions.kSearch, value); if (href) { this.#updateContext(href, true); } @@ -1187,9 +1291,22 @@ class URL { } set hash(value) { - const href = bindingUrl.update(this.#context.href, updateActions.kHash, `${value}`); - if (href) { - this.#updateContext(href); + const ctx = this.#context; + // A lone "#" is omitted by the getter, but setting "" removes it. + if (ctx.hasHash && ctx.href.length - ctx.hash_start <= 1) { + if (typeof value !== 'string') { + value = `${value}`; + } + const href = bindingUrl.update(ctx.href, updateActions.kHash, value); + if (href) { + this.#updateContext(href); + } + return; + } + if (!ctx.hasHash) { + this.#commitComponent(updateActions.kHash, value, 0, 0); + } else { + this.#commitComponent(updateActions.kHash, value, ctx.hash_start); } } @@ -1302,6 +1419,33 @@ function toUSVString(value) { StringPrototypeToWellFormed(`${value}`); } +function appendQueryPair(list, pair) { + if (pair == null) { + throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]'); + } else if (ArrayIsArray(pair)) { + // If innerSequence's size is not 2, then throw a TypeError. + if (pair.length !== 2) { + throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]'); + } + ArrayPrototypePush(list, toUSVString(pair[0]), toUSVString(pair[1])); + } else { + if ((typeof pair !== 'object' && typeof pair !== 'function') || + typeof pair[SymbolIterator] !== 'function') { + throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]'); + } + + let length = 0; + for (const element of pair) { + length++; + ArrayPrototypePush(list, toUSVString(element)); + } + + if (length !== 2) { + throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]'); + } + } +} + function unescapeFormComponent(s) { try { return decodeURIComponent(s); diff --git a/src/node_url.cc b/src/node_url.cc index 0c5c9e65136a..16c986ee8f93 100644 --- a/src/node_url.cc +++ b/src/node_url.cc @@ -24,6 +24,7 @@ namespace url { using v8::CFunction; using v8::Context; using v8::FastApiCallbackOptions; +using v8::FastOneByteString; using v8::FunctionCallbackInfo; using v8::HandleScope; using v8::Isolate; @@ -36,6 +37,34 @@ using v8::Value; namespace { +// FastOneByteString bytes are Latin-1. ada wants UTF-8. ASCII is already +// UTF-8 and is returned as a view of `input`. Anything else is converted +// into `stack` when it fits, otherwise into `storage`. +std::string_view OneByteAsUtf8(const FastOneByteString& input, + char* stack, + size_t stack_size, + std::string* storage) { + if (simdutf::validate_ascii(input.data, input.length)) { + return {input.data, input.length}; + } + const size_t len = simdutf::utf8_length_from_latin1(input.data, input.length); + char* dest; + if (len <= stack_size) { + dest = stack; + } else { + storage->resize(len); + dest = storage->data(); + } + const size_t written = + simdutf::convert_latin1_to_utf8(input.data, input.length, dest); + CHECK_EQ(written, len); + return {dest, len}; +} + +} // namespace + +namespace { + // Parse a V8 string as a URL. One-byte ASCII inputs are parsed in place // without allocating a UTF-8 copy. When `reuse_input` is non-null it is set // if the serialized href is identical to that ASCII input so the caller can @@ -323,49 +352,39 @@ void BindingData::CanParse(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(can_parse); } -bool BindingData::FastCanParse( +bool BindingData::FastCanParseOneByte( Local receiver, - Local input, + const FastOneByteString& input, // NOLINTNEXTLINE(runtime/references) This is V8 api. FastApiCallbackOptions& options) { TRACK_V8_FAST_API_CALL("url.canParse"); - auto isolate = options.isolate; - HandleScope handleScope(isolate); - Local str; - if (!input->ToString(isolate->GetCurrentContext()).ToLocal(&str)) { - return false; - } - Utf8Value utf8(isolate, str); - return ada::can_parse(utf8.ToStringView()); + char stack[512]; + std::string storage; + return ada::can_parse(OneByteAsUtf8(input, stack, sizeof(stack), &storage)); } -bool BindingData::FastCanParseWithBase( +bool BindingData::FastCanParseOneByteWithBase( Local receiver, - Local input, - Local base, + const FastOneByteString& input, + const FastOneByteString& base, // NOLINTNEXTLINE(runtime/references) This is V8 api. FastApiCallbackOptions& options) { TRACK_V8_FAST_API_CALL("url.canParse.withBase"); - auto isolate = options.isolate; - HandleScope handleScope(isolate); - auto context = isolate->GetCurrentContext(); - Local input_str; - if (!input->ToString(context).ToLocal(&input_str)) { - return false; - } - Local base_str; - if (!base->ToString(context).ToLocal(&base_str)) { - return false; - } - Utf8Value input_utf8(isolate, input_str); - Utf8Value base_utf8(isolate, base_str); - - auto base_view = base_utf8.ToStringView(); - return ada::can_parse(input_utf8.ToStringView(), &base_view); + char input_stack[512]; + char base_stack[512]; + std::string input_storage; + std::string base_storage; + const std::string_view input_view = + OneByteAsUtf8(input, input_stack, sizeof(input_stack), &input_storage); + const std::string_view base_view = + OneByteAsUtf8(base, base_stack, sizeof(base_stack), &base_storage); + return ada::can_parse(input_view, &base_view); } CFunction BindingData::fast_can_parse_methods_[] = { - CFunction::Make(FastCanParse), CFunction::Make(FastCanParseWithBase)}; + CFunction::Make(FastCanParseOneByte), + CFunction::Make(FastCanParseOneByteWithBase), +}; void BindingData::Format(const FunctionCallbackInfo& args) { CHECK_GT(args.Length(), 4); diff --git a/src/node_url.h b/src/node_url.h index c41dc9304335..bbe5524b6b4d 100644 --- a/src/node_url.h +++ b/src/node_url.h @@ -50,14 +50,15 @@ class BindingData : public SnapshotableObject { static void DomainToUnicode(const v8::FunctionCallbackInfo& args); static void CanParse(const v8::FunctionCallbackInfo& args); - static bool FastCanParse(v8::Local receiver, - v8::Local input, - // NOLINTNEXTLINE(runtime/references) This is V8 api. - v8::FastApiCallbackOptions& options); - static bool FastCanParseWithBase( + static bool FastCanParseOneByte( v8::Local receiver, - v8::Local input, - v8::Local base, + const v8::FastOneByteString& input, + // NOLINTNEXTLINE(runtime/references) This is V8 api. + v8::FastApiCallbackOptions& options); + static bool FastCanParseOneByteWithBase( + v8::Local receiver, + const v8::FastOneByteString& input, + const v8::FastOneByteString& base, // NOLINTNEXTLINE(runtime/references) This is V8 api. v8::FastApiCallbackOptions& options); diff --git a/src/node_url_pattern.cc b/src/node_url_pattern.cc index 63f01d58170f..1d7b82b91bbe 100644 --- a/src/node_url_pattern.cc +++ b/src/node_url_pattern.cc @@ -3,11 +3,17 @@ #include "debug_utils-inl.h" #include "env-inl.h" #include "node.h" +#include "node_debug.h" #include "node_errors.h" #include "node_mem-inl.h" #include "path.h" +#include "simdutf.h" #include "util-inl.h" +#include +#include +#include + namespace node { using node::url_pattern::URLPatternRegexProvider; @@ -53,9 +59,12 @@ struct MemoryRetainerTraits< namespace node::url_pattern { using v8::Array; +using v8::CFunction; using v8::Context; using v8::DictionaryTemplate; using v8::DontDelete; +using v8::FastApiCallbackOptions; +using v8::FastOneByteString; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; using v8::Global; @@ -73,6 +82,40 @@ using v8::Signature; using v8::String; using v8::Value; +namespace { + +// Copy Latin-1 fast-call bytes to UTF-8 the caller owns. The copy finishes +// before any V8 allocation so the FastOneByteString pointer stays valid. +std::string OwnedUtf8(const FastOneByteString& input) { + if (simdutf::validate_ascii(input.data, input.length)) { + return std::string(input.data, input.length); + } + const size_t len = simdutf::utf8_length_from_latin1(input.data, input.length); + std::string out(len, '\0'); + const size_t written = + simdutf::convert_latin1_to_utf8(input.data, input.length, out.data()); + CHECK_EQ(written, len); + return out; +} + +MaybeLocal PatternInputString(Isolate* isolate, + std::string_view input) { + if (input.size() <= static_cast(std::numeric_limits::max()) && + simdutf::validate_ascii(input.data(), input.size())) { + return String::NewFromOneByte( + isolate, + reinterpret_cast(input.data()), + NewStringType::kNormal, + static_cast(input.size())); + } + return String::NewFromUtf8( + isolate, input.data(), NewStringType::kNormal, input.size()); +} + +} // namespace + +const CFunction kFastTest = CFunction::Make(URLPattern::FastTest); + std::optional URLPatternRegexProvider::create_instance(std::string_view pattern, bool ignore_case) { @@ -103,9 +146,7 @@ bool URLPatternRegexProvider::regex_match(std::string_view input, 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)) { + if (!PatternInputString(isolate, input).ToLocal(&local_input)) { return false; } Local result_object; @@ -124,9 +165,7 @@ URLPatternRegexProvider::regex_search(std::string_view input, 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)) { + if (!PatternInputString(isolate, input).ToLocal(&local_input)) { return std::nullopt; } Local exec_result_object; @@ -348,51 +387,59 @@ std::optional URLPattern::URLPatternInit::FromJsObject( Environment* env, Local obj) { ada::url_pattern_init init{}; Local components[] = { - env->base_url_string(), - env->hash_string(), - env->hostname_string(), - env->password_string(), - env->pathname_string(), - env->port_string(), - env->protocol_string(), - env->search_string(), - env->username_string(), + env->base_url_string(), // 0 + env->hash_string(), // 1 + env->hostname_string(), // 2 + env->password_string(), // 3 + env->pathname_string(), // 4 + env->port_string(), // 5 + env->protocol_string(), // 6 + env->search_string(), // 7 + env->username_string(), // 8 }; 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); - } - }; 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 { - // If ToLocal failed then we assume an error occurred, - // bail out early to propagate the error. + for (size_t i = 0; i < 9; i++) { + if (!obj->Get(env->context(), components[i]).ToLocal(&value)) { + // Getting the property threw. Propagate that. return std::nullopt; } + if (!value->IsString()) { + continue; + } + Utf8Value utf8_value(isolate, value); + const std::string_view view = utf8_value.ToStringView(); + switch (i) { + case 0: + init.base_url = std::string(view); + break; + case 1: + init.hash = std::string(view); + break; + case 2: + init.hostname = std::string(view); + break; + case 3: + init.password = std::string(view); + break; + case 4: + init.pathname = std::string(view); + break; + case 5: + init.port = std::string(view); + break; + case 6: + init.protocol = std::string(view); + break; + case 7: + init.search = std::string(view); + break; + case 8: + init.username = std::string(view); + break; + default: + UNREACHABLE(); + } } return init; } @@ -564,7 +611,6 @@ void URLPattern::Exec(const FunctionCallbackInfo& args) { URLPattern* url_pattern; ASSIGN_OR_RETURN_UNWRAP(&url_pattern, args.This()); auto env = Environment::GetCurrent(args); - ada::url_pattern_input input; std::optional baseURL{}; std::string input_base; @@ -606,6 +652,37 @@ void URLPattern::Exec(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(result); } +bool URLPattern::FastTest(Local receiver, + const FastOneByteString& input, + FastApiCallbackOptions& options) { + TRACK_V8_FAST_API_CALL("urlpattern.test"); + (void)options; + if (receiver->InternalFieldCount() < BaseObject::kInternalFieldCount) { + return false; + } + URLPattern* self = BaseObject::FromJSObject(receiver); + if (self == nullptr) { + return false; + } + // Copy out of the fast-call pointer before regexp matching allocates. + char stack[512]; + std::string heap; + std::string_view view; + if (simdutf::validate_ascii(input.data, input.length) && + input.length <= sizeof(stack)) { + memcpy(stack, input.data, input.length); + view = {stack, input.length}; + } else { + heap = OwnedUtf8(input); + view = heap; + } + ada::url_pattern_input pattern_input{view}; + if (auto result = self->url_pattern_.test(pattern_input, nullptr)) { + return *result; + } + return false; +} + void URLPattern::Test(const FunctionCallbackInfo& args) { URLPattern* url_pattern; ASSIGN_OR_RETURN_UNWRAP(&url_pattern, args.This()); @@ -674,6 +751,7 @@ static void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(URLPattern::HasRegexpGroups); registry->Register(URLPattern::Exec); registry->Register(URLPattern::Test); + registry->Register(kFastTest); } static void Initialize(Local target, @@ -720,7 +798,16 @@ static void Initialize(Local target, attributes); SetProtoMethodNoSideEffect(isolate, ctor_tmpl, "exec", URLPattern::Exec); - SetProtoMethodNoSideEffect(isolate, ctor_tmpl, "test", URLPattern::Test); + Local test_template = + NewFunctionTemplate(isolate, + URLPattern::Test, + signature, + v8::ConstructorBehavior::kThrow, + v8::SideEffectType::kHasNoSideEffect, + &kFastTest); + test_template->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "test")); + prototype_template->Set(FIXED_ONE_BYTE_STRING(isolate, "test"), + test_template); SetConstructorFunction(context, target, "URLPattern", ctor_tmpl); } diff --git a/src/node_url_pattern.h b/src/node_url_pattern.h index 48f3956cb744..e8994ebca25f 100644 --- a/src/node_url_pattern.h +++ b/src/node_url_pattern.h @@ -6,6 +6,7 @@ #include "ada.h" #include "base_object.h" #include "util.h" +#include "v8-fast-api-calls.h" #include @@ -52,6 +53,9 @@ class URLPattern : public BaseObject { // - Functions static void Exec(const v8::FunctionCallbackInfo& info); static void Test(const v8::FunctionCallbackInfo& info); + static bool FastTest(v8::Local receiver, + const v8::FastOneByteString& input, + v8::FastApiCallbackOptions& options); // - Component Getters #define URL_PATTERN_COMPONENT_GETTERS(name, _) \ static void name(const v8::FunctionCallbackInfo& info);