url: use ada::url_aggregator for parsing urls

PR-URL: https://github.com/nodejs/node/pull/47339
Backport-PR-URL: https://github.com/nodejs/node/pull/48345
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
This commit is contained in:
Yagiz Nizipli 2023-03-31 09:03:06 -04:00 committed by Danielle Adams
parent 96c50ba71f
commit b395b16c40
No known key found for this signature in database
GPG key ID: D3A89613643B6201
9 changed files with 486 additions and 265 deletions

View file

@ -87,13 +87,7 @@ const querystring = require('querystring');
const { platform } = process; const { platform } = process;
const isWindows = platform === 'win32'; const isWindows = platform === 'win32';
const { const bindingUrl = internalBinding('url');
domainToASCII: _domainToASCII,
domainToUnicode: _domainToUnicode,
parse,
canParse: _canParse,
updateUrl,
} = internalBinding('url');
const { const {
storeDataObject, storeDataObject,
@ -142,16 +136,46 @@ function lazyCryptoRandom() {
// the C++ binding. // the C++ binding.
// Refs: https://url.spec.whatwg.org/#concept-url // Refs: https://url.spec.whatwg.org/#concept-url
class URLContext { class URLContext {
// This is the maximum value uint32_t can get.
// Ada uses uint32_t(-1) for declaring omitted values.
static #omitted = 4294967295;
href = ''; href = '';
origin = ''; protocol_end = 0;
protocol = ''; username_end = 0;
hostname = ''; host_start = 0;
pathname = ''; host_end = 0;
search = ''; pathname_start = 0;
username = ''; search_start = 0;
password = ''; hash_start = 0;
port = ''; port = 0;
hash = ''; /**
* Refers to `ada::scheme::type`
*
* enum type : uint8_t {
* HTTP = 0,
* NOT_SPECIAL = 1,
* HTTPS = 2,
* WS = 3,
* FTP = 4,
* WSS = 5,
* FILE = 6
* };
* @type {number}
*/
scheme_type = 1;
get hasPort() {
return this.port !== URLContext.#omitted;
}
get hasSearch() {
return this.search_start !== URLContext.#omitted;
}
get hasHash() {
return this.hash_start !== URLContext.#omitted;
}
} }
function isURLSearchParams(self) { function isURLSearchParams(self) {
@ -581,13 +605,13 @@ class URL {
base = `${base}`; base = `${base}`;
} }
const isValid = parse(input, const href = bindingUrl.parse(input, base);
base,
this.#onParseComplete);
if (!isValid) { if (!href) {
throw new ERR_INVALID_URL(input); throw new ERR_INVALID_URL(input);
} }
this.#updateContext(href);
} }
[inspect.custom](depth, opts) { [inspect.custom](depth, opts) {
@ -622,23 +646,40 @@ class URL {
return `${constructor.name} ${inspect(obj, opts)}`; return `${constructor.name} ${inspect(obj, opts)}`;
} }
#onParseComplete = (href, origin, protocol, hostname, pathname, #updateContext(href) {
search, username, password, port, hash) => {
const ctx = this[context]; const ctx = this[context];
ctx.href = href; ctx.href = href;
ctx.origin = origin;
ctx.protocol = protocol; const {
ctx.hostname = hostname; 0: protocol_end,
ctx.pathname = pathname; 1: username_end,
ctx.search = search; 2: host_start,
ctx.username = username; 3: host_end,
ctx.password = password; 4: port,
5: pathname_start,
6: search_start,
7: hash_start,
8: scheme_type,
} = bindingUrl.urlComponents;
ctx.protocol_end = protocol_end;
ctx.username_end = username_end;
ctx.host_start = host_start;
ctx.host_end = host_end;
ctx.port = port; ctx.port = port;
ctx.hash = hash; ctx.pathname_start = pathname_start;
ctx.search_start = search_start;
ctx.hash_start = hash_start;
ctx.scheme_type = scheme_type;
if (this[searchParams]) { if (this[searchParams]) {
this[searchParams][searchParams] = parseParams(search); if (ctx.hasSearch) {
this[searchParams][searchParams] = parseParams(this.search);
} else {
this[searchParams][searchParams] = [];
}
} }
}; }
toString() { toString() {
if (!isURL(this)) if (!isURL(this))
@ -655,122 +696,210 @@ class URL {
set href(value) { set href(value) {
if (!isURL(this)) if (!isURL(this))
throw new ERR_INVALID_THIS('URL'); throw new ERR_INVALID_THIS('URL');
const valid = updateUrl(this[context].href, updateActions.kHref, `${value}`, this.#onParseComplete); value = `${value}`;
if (!valid) { throw ERR_INVALID_URL(`${value}`); } const href = bindingUrl.update(this[context].href, updateActions.kHref, value);
if (!href) { throw ERR_INVALID_URL(value); }
this.#updateContext(href);
} }
// readonly // readonly
get origin() { get origin() {
if (!isURL(this)) if (!isURL(this))
throw new ERR_INVALID_THIS('URL'); throw new ERR_INVALID_THIS('URL');
return this[context].origin; const ctx = this[context];
const protocol = StringPrototypeSlice(ctx.href, 0, ctx.protocol_end);
// Check if scheme_type is not `NOT_SPECIAL`
if (ctx.scheme_type !== 1) {
// Check if scheme_type is `FILE`
if (ctx.scheme_type === 6) {
return 'null';
}
return `${protocol}//${this.host}`;
}
if (protocol === 'blob:') {
const path = this.pathname;
if (path.length > 0) {
try {
const out = new URL(path);
if (out[context].scheme_type !== 1) {
return `${out.protocol}//${out.host}`;
}
} catch {
// Do nothing.
}
}
}
return 'null';
} }
get protocol() { get protocol() {
if (!isURL(this)) if (!isURL(this))
throw new ERR_INVALID_THIS('URL'); throw new ERR_INVALID_THIS('URL');
return this[context].protocol; return StringPrototypeSlice(this[context].href, 0, this[context].protocol_end);
} }
set protocol(value) { set protocol(value) {
if (!isURL(this)) if (!isURL(this))
throw new ERR_INVALID_THIS('URL'); throw new ERR_INVALID_THIS('URL');
updateUrl(this[context].href, updateActions.kProtocol, `${value}`, this.#onParseComplete); const href = bindingUrl.update(this[context].href, updateActions.kProtocol, `${value}`);
if (href) {
this.#updateContext(href);
}
} }
get username() { get username() {
if (!isURL(this)) if (!isURL(this))
throw new ERR_INVALID_THIS('URL'); throw new ERR_INVALID_THIS('URL');
return this[context].username; const ctx = this[context];
if (ctx.protocol_end + 2 < ctx.username_end) {
return StringPrototypeSlice(ctx.href, ctx.protocol_end + 2, ctx.username_end);
}
return '';
} }
set username(value) { set username(value) {
if (!isURL(this)) if (!isURL(this))
throw new ERR_INVALID_THIS('URL'); throw new ERR_INVALID_THIS('URL');
updateUrl(this[context].href, updateActions.kUsername, `${value}`, this.#onParseComplete); const href = bindingUrl.update(this[context].href, updateActions.kUsername, `${value}`);
if (href) {
this.#updateContext(href);
}
} }
get password() { get password() {
if (!isURL(this)) if (!isURL(this))
throw new ERR_INVALID_THIS('URL'); throw new ERR_INVALID_THIS('URL');
return this[context].password; const ctx = this[context];
if (ctx.host_start - ctx.username_end > 0) {
return StringPrototypeSlice(ctx.href, ctx.username_end + 1, ctx.host_start);
}
return '';
} }
set password(value) { set password(value) {
if (!isURL(this)) if (!isURL(this))
throw new ERR_INVALID_THIS('URL'); throw new ERR_INVALID_THIS('URL');
updateUrl(this[context].href, updateActions.kPassword, `${value}`, this.#onParseComplete); const href = bindingUrl.update(this[context].href, updateActions.kPassword, `${value}`);
if (href) {
this.#updateContext(href);
}
} }
get host() { get host() {
if (!isURL(this)) if (!isURL(this))
throw new ERR_INVALID_THIS('URL'); throw new ERR_INVALID_THIS('URL');
const port = this[context].port; const ctx = this[context];
const suffix = port.length > 0 ? `:${port}` : ''; let startsAt = ctx.host_start;
return this[context].hostname + suffix; if (ctx.href[startsAt] === '@') {
startsAt++;
}
// If we have an empty host, then the space between components.host_end and
// components.pathname_start may be occupied by /.
if (startsAt === ctx.host_end) {
return '';
}
return StringPrototypeSlice(ctx.href, startsAt, ctx.pathname_start);
} }
set host(value) { set host(value) {
if (!isURL(this)) if (!isURL(this))
throw new ERR_INVALID_THIS('URL'); throw new ERR_INVALID_THIS('URL');
updateUrl(this[context].href, updateActions.kHost, `${value}`, this.#onParseComplete); const href = bindingUrl.update(this[context].href, updateActions.kHost, `${value}`);
if (href) {
this.#updateContext(href);
}
} }
get hostname() { get hostname() {
if (!isURL(this)) if (!isURL(this))
throw new ERR_INVALID_THIS('URL'); throw new ERR_INVALID_THIS('URL');
return this[context].hostname; const ctx = this[context];
let startsAt = ctx.host_start;
// host_start might be "@" if the URL has credentials
if (ctx.href[startsAt] === '@') {
startsAt++;
}
return StringPrototypeSlice(ctx.href, startsAt, ctx.host_end);
} }
set hostname(value) { set hostname(value) {
if (!isURL(this)) if (!isURL(this))
throw new ERR_INVALID_THIS('URL'); throw new ERR_INVALID_THIS('URL');
updateUrl(this[context].href, updateActions.kHostname, `${value}`, this.#onParseComplete); const href = bindingUrl.update(this[context].href, updateActions.kHostname, `${value}`);
if (href) {
this.#updateContext(href);
}
} }
get port() { get port() {
if (!isURL(this)) if (!isURL(this))
throw new ERR_INVALID_THIS('URL'); throw new ERR_INVALID_THIS('URL');
return this[context].port; if (this[context].hasPort) {
return `${this[context].port}`;
}
return '';
} }
set port(value) { set port(value) {
if (!isURL(this)) if (!isURL(this))
throw new ERR_INVALID_THIS('URL'); throw new ERR_INVALID_THIS('URL');
updateUrl(this[context].href, updateActions.kPort, `${value}`, this.#onParseComplete); const href = bindingUrl.update(this[context].href, updateActions.kPort, `${value}`);
if (href) {
this.#updateContext(href);
}
} }
get pathname() { get pathname() {
if (!isURL(this)) if (!isURL(this))
throw new ERR_INVALID_THIS('URL'); throw new ERR_INVALID_THIS('URL');
return this[context].pathname; const ctx = this[context];
let endsAt;
if (ctx.hasSearch) {
endsAt = ctx.search_start;
} else if (ctx.hasHash) {
endsAt = ctx.hash_start;
}
return StringPrototypeSlice(ctx.href, ctx.pathname_start, endsAt);
} }
set pathname(value) { set pathname(value) {
if (!isURL(this)) if (!isURL(this))
throw new ERR_INVALID_THIS('URL'); throw new ERR_INVALID_THIS('URL');
updateUrl(this[context].href, updateActions.kPathname, `${value}`, this.#onParseComplete); const href = bindingUrl.update(this[context].href, updateActions.kPathname, `${value}`);
if (href) {
this.#updateContext(href);
}
} }
get search() { get search() {
if (!isURL(this)) if (!isURL(this))
throw new ERR_INVALID_THIS('URL'); throw new ERR_INVALID_THIS('URL');
return this[context].search; const ctx = this[context];
if (!ctx.hasSearch) { return ''; }
let endsAt = ctx.href.length;
if (ctx.hasHash) { endsAt = ctx.hash_start; }
if (endsAt - ctx.search_start <= 1) { return ''; }
return StringPrototypeSlice(ctx.href, ctx.search_start, endsAt);
} }
set search(value) { set search(value) {
if (!isURL(this)) if (!isURL(this))
throw new ERR_INVALID_THIS('URL'); throw new ERR_INVALID_THIS('URL');
updateUrl(this[context].href, updateActions.kSearch, toUSVString(value), this.#onParseComplete); const href = bindingUrl.update(this[context].href, updateActions.kSearch, toUSVString(value));
if (href) {
this.#updateContext(href);
}
} }
// readonly // readonly
get searchParams() { get searchParams() {
if (!isURL(this)) if (!isURL(this))
throw new ERR_INVALID_THIS('URL'); throw new ERR_INVALID_THIS('URL');
// Create URLSearchParams on demand to greatly improve the URL performance.
if (this[searchParams] == null) { if (this[searchParams] == null) {
this[searchParams] = new URLSearchParams(this[context].search); this[searchParams] = new URLSearchParams(this.search);
this[searchParams][context] = this; this[searchParams][context] = this;
} }
return this[searchParams]; return this[searchParams];
@ -779,13 +908,20 @@ class URL {
get hash() { get hash() {
if (!isURL(this)) if (!isURL(this))
throw new ERR_INVALID_THIS('URL'); throw new ERR_INVALID_THIS('URL');
return this[context].hash; const ctx = this[context];
if (!ctx.hasHash || (ctx.href.length - ctx.hash_start <= 1)) {
return '';
}
return StringPrototypeSlice(ctx.href, ctx.hash_start);
} }
set hash(value) { set hash(value) {
if (!isURL(this)) if (!isURL(this))
throw new ERR_INVALID_THIS('URL'); throw new ERR_INVALID_THIS('URL');
updateUrl(this[context].href, updateActions.kHash, `${value}`, this.#onParseComplete); const href = bindingUrl.update(this[context].href, updateActions.kHash, `${value}`);
if (href) {
this.#updateContext(href);
}
} }
toJSON() { toJSON() {
@ -801,7 +937,7 @@ class URL {
base = `${base}`; base = `${base}`;
} }
return _canParse(url, base); return bindingUrl.canParse(url, base);
} }
static createObjectURL(obj) { static createObjectURL(obj) {
@ -1164,7 +1300,7 @@ function domainToASCII(domain) {
throw new ERR_MISSING_ARGS('domain'); throw new ERR_MISSING_ARGS('domain');
// toUSVString is not needed. // toUSVString is not needed.
return _domainToASCII(`${domain}`); return bindingUrl.domainToASCII(`${domain}`);
} }
function domainToUnicode(domain) { function domainToUnicode(domain) {
@ -1172,7 +1308,7 @@ function domainToUnicode(domain) {
throw new ERR_MISSING_ARGS('domain'); throw new ERR_MISSING_ARGS('domain');
// toUSVString is not needed. // toUSVString is not needed.
return _domainToUnicode(`${domain}`); return bindingUrl.domainToUnicode(`${domain}`);
} }
/** /**
@ -1355,4 +1491,6 @@ module.exports = {
urlToHttpOptions, urlToHttpOptions,
encodeStr, encodeStr,
isURL, isURL,
urlUpdateActions: updateActions,
}; };

View file

@ -59,9 +59,7 @@ const {
urlToHttpOptions, urlToHttpOptions,
} = require('internal/url'); } = require('internal/url');
const { const bindingUrl = internalBinding('url');
formatUrl,
} = internalBinding('url');
const { getOptionValue } = require('internal/options'); const { getOptionValue } = require('internal/options');
@ -627,7 +625,7 @@ function urlFormat(urlObject, options) {
} }
} }
return formatUrl(urlObject.href, fragment, unicode, search, auth); return bindingUrl.format(urlObject.href, fragment, unicode, search, auth);
} }
return Url.prototype.format.call(urlObject); return Url.prototype.format.call(urlObject);

View file

@ -18,6 +18,7 @@
#include "node_metadata.h" #include "node_metadata.h"
#include "node_process.h" #include "node_process.h"
#include "node_snapshot_builder.h" #include "node_snapshot_builder.h"
#include "node_url.h"
#include "node_util.h" #include "node_util.h"
#include "node_v8.h" #include "node_v8.h"
#include "node_v8_platform-inl.h" #include "node_v8_platform-inl.h"

View file

@ -27,6 +27,7 @@ struct PropInfo {
V(v8_binding_data, v8_utils::BindingData) \ V(v8_binding_data, v8_utils::BindingData) \
V(blob_binding_data, BlobBindingData) \ V(blob_binding_data, BlobBindingData) \
V(process_binding_data, process::BindingData) \ V(process_binding_data, process::BindingData) \
V(url_binding_data, url::BindingData) \
V(util_weak_reference, util::WeakReference) V(util_weak_reference, util::WeakReference)
enum class EmbedderObjectType : uint8_t { enum class EmbedderObjectType : uint8_t {

View file

@ -5,14 +5,16 @@
#include "node_external_reference.h" #include "node_external_reference.h"
#include "node_i18n.h" #include "node_i18n.h"
#include "util-inl.h" #include "util-inl.h"
#include "v8.h"
#include <cstdint>
#include <cstdio> #include <cstdio>
#include <numeric> #include <numeric>
namespace node { namespace node {
namespace url {
using v8::Context; using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo; using v8::FunctionCallbackInfo;
using v8::HandleScope; using v8::HandleScope;
using v8::Isolate; using v8::Isolate;
@ -22,102 +24,49 @@ using v8::Object;
using v8::String; using v8::String;
using v8::Value; using v8::Value;
namespace url { void BindingData::MemoryInfo(MemoryTracker* tracker) const {
namespace { tracker->TrackField("url_components_buffer", url_components_buffer_);
enum url_update_action {
kProtocol = 0,
kHost = 1,
kHostname = 2,
kPort = 3,
kUsername = 4,
kPassword = 5,
kPathname = 6,
kSearch = 7,
kHash = 8,
kHref = 9,
};
auto GetCallbackArgs(Environment* env, const ada::result& url) {
Local<Context> context = env->context();
Isolate* isolate = env->isolate();
auto js_string = [&](std::string_view sv) {
return ToV8Value(context, sv, isolate).ToLocalChecked();
};
return std::array{
js_string(url->get_href()),
js_string(url->get_origin()),
js_string(url->get_protocol()),
js_string(url->get_hostname()),
js_string(url->get_pathname()),
js_string(url->get_search()),
js_string(url->get_username()),
js_string(url->get_password()),
js_string(url->get_port()),
js_string(url->get_hash()),
};
} }
void Parse(const FunctionCallbackInfo<Value>& args) { BindingData::BindingData(Realm* realm, v8::Local<v8::Object> object)
CHECK_GE(args.Length(), 3); : SnapshotableObject(realm, object, type_int),
CHECK(args[0]->IsString()); // input url_components_buffer_(realm->isolate(), kURLComponentsLength) {
// args[1] // base url object
CHECK(args[2]->IsFunction()); // complete callback ->Set(realm->context(),
FIXED_ONE_BYTE_STRING(realm->isolate(), "urlComponents"),
Local<Function> success_callback_ = args[2].As<Function>(); url_components_buffer_.GetJSArray())
.Check();
Environment* env = Environment::GetCurrent(args);
HandleScope handle_scope(env->isolate());
Context::Scope context_scope(env->context());
Utf8Value input(env->isolate(), args[0]);
ada::result base;
ada::url* base_pointer = nullptr;
if (args[1]->IsString()) {
base = ada::parse(Utf8Value(env->isolate(), args[1]).ToString());
if (!base) {
return args.GetReturnValue().Set(false);
}
base_pointer = &base.value();
}
ada::result out = ada::parse(input.ToStringView(), base_pointer);
if (!out) {
return args.GetReturnValue().Set(false);
}
auto argv = GetCallbackArgs(env, out);
USE(success_callback_->Call(
env->context(), args.This(), argv.size(), argv.data()));
args.GetReturnValue().Set(true);
} }
void CanParse(const FunctionCallbackInfo<Value>& args) { bool BindingData::PrepareForSerialization(v8::Local<v8::Context> context,
CHECK_GE(args.Length(), 2); v8::SnapshotCreator* creator) {
CHECK(args[0]->IsString()); // input // We'll just re-initialize the buffers in the constructor since their
// args[1] // base url // contents can be thrown away once consumed in the previous call.
url_components_buffer_.Release();
Environment* env = Environment::GetCurrent(args); // Return true because we need to maintain the reference to the binding from
HandleScope handle_scope(env->isolate()); // JS land.
Context::Scope context_scope(env->context()); return true;
Utf8Value input(env->isolate(), args[0]);
ada::result base;
ada::url* base_pointer = nullptr;
if (args[1]->IsString()) {
base = ada::parse(Utf8Value(env->isolate(), args[1]).ToString());
if (!base) {
return args.GetReturnValue().Set(false);
}
base_pointer = &base.value();
}
ada::result out = ada::parse(input.ToStringView(), base_pointer);
args.GetReturnValue().Set(out.has_value());
} }
void DomainToASCII(const FunctionCallbackInfo<Value>& args) { InternalFieldInfoBase* BindingData::Serialize(int index) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
InternalFieldInfo* info =
InternalFieldInfoBase::New<InternalFieldInfo>(type());
return info;
}
void BindingData::Deserialize(v8::Local<v8::Context> context,
v8::Local<v8::Object> holder,
int index,
InternalFieldInfoBase* info) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
v8::HandleScope scope(context->GetIsolate());
Realm* realm = Realm::GetCurrent(context);
BindingData* binding = realm->AddBindingData<BindingData>(context, holder);
CHECK_NOT_NULL(binding);
}
void BindingData::DomainToASCII(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args); Environment* env = Environment::GetCurrent(args);
CHECK_GE(args.Length(), 1); CHECK_GE(args.Length(), 1);
CHECK(args[0]->IsString()); CHECK(args[0]->IsString());
@ -127,11 +76,10 @@ void DomainToASCII(const FunctionCallbackInfo<Value>& args) {
return args.GetReturnValue().Set(FIXED_ONE_BYTE_STRING(env->isolate(), "")); return args.GetReturnValue().Set(FIXED_ONE_BYTE_STRING(env->isolate(), ""));
} }
#if defined(NODE_HAVE_I18N_SUPPORT)
// It is important to have an initial value that contains a special scheme. // It is important to have an initial value that contains a special scheme.
// Since it will change the implementation of `set_hostname` according to URL // Since it will change the implementation of `set_hostname` according to URL
// spec. // spec.
ada::result out = ada::parse("ws://x"); auto out = ada::parse<ada::url>("ws://x");
DCHECK(out); DCHECK(out);
if (!out->set_hostname(input)) { if (!out->set_hostname(input)) {
return args.GetReturnValue().Set(FIXED_ONE_BYTE_STRING(env->isolate(), "")); return args.GetReturnValue().Set(FIXED_ONE_BYTE_STRING(env->isolate(), ""));
@ -139,53 +87,144 @@ void DomainToASCII(const FunctionCallbackInfo<Value>& args) {
std::string host = out->get_hostname(); std::string host = out->get_hostname();
args.GetReturnValue().Set( args.GetReturnValue().Set(
String::NewFromUtf8(env->isolate(), host.c_str()).ToLocalChecked()); String::NewFromUtf8(env->isolate(), host.c_str()).ToLocalChecked());
#else
args.GetReturnValue().Set(
String::NewFromUtf8(env->isolate(), input.c_str()).ToLocalChecked());
#endif
} }
void DomainToUnicode(const FunctionCallbackInfo<Value>& args) { void BindingData::DomainToUnicode(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args); Environment* env = Environment::GetCurrent(args);
CHECK_GE(args.Length(), 1); CHECK_GE(args.Length(), 1);
CHECK(args[0]->IsString()); CHECK(args[0]->IsString());
std::string input = Utf8Value(env->isolate(), args[0]).ToString(); std::string input = Utf8Value(env->isolate(), args[0]).ToString();
#if defined(NODE_HAVE_I18N_SUPPORT)
// It is important to have an initial value that contains a special scheme. // It is important to have an initial value that contains a special scheme.
// Since it will change the implementation of `set_hostname` according to URL // Since it will change the implementation of `set_hostname` according to URL
// spec. // spec.
ada::result out = ada::parse("ws://x"); auto out = ada::parse<ada::url>("ws://x");
DCHECK(out); DCHECK(out);
if (!out->set_hostname(input)) { if (!out->set_hostname(input)) {
return args.GetReturnValue().Set( return args.GetReturnValue().Set(
String::NewFromUtf8(env->isolate(), "").ToLocalChecked()); String::NewFromUtf8(env->isolate(), "").ToLocalChecked());
} }
std::string host = out->get_hostname(); std::string result = ada::unicode::to_unicode(out->get_hostname());
MaybeStackBuffer<char> buf; args.GetReturnValue().Set(String::NewFromUtf8(env->isolate(),
int32_t len = i18n::ToUnicode(&buf, host.data(), host.length()); result.c_str(),
NewStringType::kNormal,
if (len < 0) { result.length())
return args.GetReturnValue().Set( .ToLocalChecked());
String::NewFromUtf8(env->isolate(), "").ToLocalChecked());
}
args.GetReturnValue().Set(
String::NewFromUtf8(env->isolate(), *buf, NewStringType::kNormal, len)
.ToLocalChecked());
#else // !defined(NODE_HAVE_I18N_SUPPORT)
args.GetReturnValue().Set(
String::NewFromUtf8(env->isolate(), input.c_str()).ToLocalChecked());
#endif
} }
void UpdateUrl(const FunctionCallbackInfo<Value>& args) { // TODO(@anonrig): Add V8 Fast API for CanParse method
void BindingData::CanParse(const FunctionCallbackInfo<Value>& args) {
CHECK_GE(args.Length(), 2);
CHECK(args[0]->IsString()); // input
// args[1] // base url
Environment* env = Environment::GetCurrent(args);
HandleScope handle_scope(env->isolate());
Context::Scope context_scope(env->context());
Utf8Value input(env->isolate(), args[0]);
ada::result<ada::url_aggregator> base;
ada::url_aggregator* base_pointer = nullptr;
if (args[1]->IsString()) {
base = ada::parse<ada::url_aggregator>(
Utf8Value(env->isolate(), args[1]).ToString());
if (!base) {
return args.GetReturnValue().Set(false);
}
base_pointer = &base.value();
}
auto out =
ada::parse<ada::url_aggregator>(input.ToStringView(), base_pointer);
args.GetReturnValue().Set(out.has_value());
}
void BindingData::Format(const FunctionCallbackInfo<Value>& args) {
CHECK_GT(args.Length(), 4);
CHECK(args[0]->IsString()); // url href
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
Utf8Value href(isolate, args[0].As<String>());
const bool fragment = args[1]->IsTrue();
const bool unicode = args[2]->IsTrue();
const bool search = args[3]->IsTrue();
const bool auth = args[4]->IsTrue();
// ada::url provides a faster alternative to ada::url_aggregator if we
// directly want to manipulate the url components without using the respective
// setters. therefore we are using ada::url here.
auto out = ada::parse<ada::url>(href.ToStringView());
CHECK(out);
if (!fragment) {
out->fragment = std::nullopt;
}
if (unicode) {
out->host = ada::idna::to_unicode(out->get_hostname());
}
if (!search) {
out->query = std::nullopt;
}
if (!auth) {
out->username = "";
out->password = "";
}
std::string result = out->get_href();
args.GetReturnValue().Set(String::NewFromUtf8(env->isolate(),
result.data(),
NewStringType::kNormal,
result.length())
.ToLocalChecked());
}
void BindingData::Parse(const FunctionCallbackInfo<Value>& args) {
CHECK_GE(args.Length(), 1);
CHECK(args[0]->IsString()); // input
// args[1] // base url
BindingData* binding_data = Realm::GetBindingData<BindingData>(args);
Environment* env = Environment::GetCurrent(args);
HandleScope handle_scope(env->isolate());
Context::Scope context_scope(env->context());
Utf8Value input(env->isolate(), args[0]);
ada::result<ada::url_aggregator> base;
ada::url_aggregator* base_pointer = nullptr;
if (args[1]->IsString()) {
base = ada::parse<ada::url_aggregator>(
Utf8Value(env->isolate(), args[1]).ToString());
if (!base) {
return args.GetReturnValue().Set(false);
}
base_pointer = &base.value();
}
auto out =
ada::parse<ada::url_aggregator>(input.ToStringView(), base_pointer);
if (!out) {
return args.GetReturnValue().Set(false);
}
binding_data->UpdateComponents(out->get_components(), out->type);
args.GetReturnValue().Set(
ToV8Value(env->context(), out->get_href(), env->isolate())
.ToLocalChecked());
}
void BindingData::Update(const FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsString()); // href CHECK(args[0]->IsString()); // href
CHECK(args[1]->IsNumber()); // action type CHECK(args[1]->IsNumber()); // action type
CHECK(args[2]->IsString()); // new value CHECK(args[2]->IsString()); // new value
CHECK(args[3]->IsFunction()); // success callback
BindingData* binding_data = Realm::GetBindingData<BindingData>(args);
Environment* env = Environment::GetCurrent(args); Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate(); Isolate* isolate = env->isolate();
@ -193,10 +232,9 @@ void UpdateUrl(const FunctionCallbackInfo<Value>& args) {
args[1]->Uint32Value(env->context()).FromJust()); args[1]->Uint32Value(env->context()).FromJust());
Utf8Value input(isolate, args[0].As<String>()); Utf8Value input(isolate, args[0].As<String>());
Utf8Value new_value(isolate, args[2].As<String>()); Utf8Value new_value(isolate, args[2].As<String>());
Local<Function> success_callback_ = args[3].As<Function>();
std::string_view new_value_view = new_value.ToStringView(); std::string_view new_value_view = new_value.ToStringView();
ada::result out = ada::parse(input.ToStringView()); auto out = ada::parse<ada::url_aggregator>(input.ToStringView());
CHECK(out); CHECK(out);
bool result{true}; bool result{true};
@ -242,89 +280,60 @@ void UpdateUrl(const FunctionCallbackInfo<Value>& args) {
result = out->set_username(new_value_view); result = out->set_username(new_value_view);
break; break;
} }
default:
UNREACHABLE("Unsupported URL update action");
} }
auto argv = GetCallbackArgs(env, out); if (!result) {
USE(success_callback_->Call( return args.GetReturnValue().Set(false);
env->context(), args.This(), argv.size(), argv.data())); }
args.GetReturnValue().Set(result);
binding_data->UpdateComponents(out->get_components(), out->type);
args.GetReturnValue().Set(
ToV8Value(env->context(), out->get_href(), env->isolate())
.ToLocalChecked());
} }
void FormatUrl(const FunctionCallbackInfo<Value>& args) { void BindingData::UpdateComponents(const ada::url_components& components,
CHECK_GT(args.Length(), 4); const ada::scheme::type type) {
CHECK(args[0]->IsString()); // url href url_components_buffer_[0] = components.protocol_end;
url_components_buffer_[1] = components.username_end;
Environment* env = Environment::GetCurrent(args); url_components_buffer_[2] = components.host_start;
Isolate* isolate = env->isolate(); url_components_buffer_[3] = components.host_end;
url_components_buffer_[4] = components.port;
Utf8Value href(isolate, args[0].As<String>()); url_components_buffer_[5] = components.pathname_start;
const bool fragment = args[1]->IsTrue(); url_components_buffer_[6] = components.search_start;
const bool unicode = args[2]->IsTrue(); url_components_buffer_[7] = components.hash_start;
const bool search = args[3]->IsTrue(); url_components_buffer_[8] = type;
const bool auth = args[4]->IsTrue(); static_assert(kURLComponentsLength == 9,
"kURLComponentsLength should be up-to-date");
ada::result out = ada::parse(href.ToStringView());
CHECK(out);
if (!fragment) {
out->fragment = std::nullopt;
}
if (unicode) {
#if defined(NODE_HAVE_I18N_SUPPORT)
std::string hostname = out->get_hostname();
MaybeStackBuffer<char> buf;
int32_t len = i18n::ToUnicode(&buf, hostname.data(), hostname.length());
if (len < 0) {
out->host = "";
} else {
out->host = buf.ToString();
}
#else
out->host = "";
#endif
}
if (!search) {
out->query = std::nullopt;
}
if (!auth) {
out->username = "";
out->password = "";
}
std::string result = out->get_href();
args.GetReturnValue().Set(String::NewFromUtf8(env->isolate(),
result.data(),
NewStringType::kNormal,
result.length())
.ToLocalChecked());
} }
void Initialize(Local<Object> target, void BindingData::Initialize(Local<Object> target,
Local<Value> unused, Local<Value> unused,
Local<Context> context, Local<Context> context,
void* priv) { void* priv) {
SetMethod(context, target, "parse", Parse); Realm* realm = Realm::GetCurrent(context);
SetMethod(context, target, "updateUrl", UpdateUrl); BindingData* const binding_data =
SetMethodNoSideEffect(context, target, "canParse", CanParse); realm->AddBindingData<BindingData>(context, target);
SetMethodNoSideEffect(context, target, "formatUrl", FormatUrl); if (binding_data == nullptr) return;
SetMethodNoSideEffect(context, target, "domainToASCII", DomainToASCII); SetMethodNoSideEffect(context, target, "domainToASCII", DomainToASCII);
SetMethodNoSideEffect(context, target, "domainToUnicode", DomainToUnicode); SetMethodNoSideEffect(context, target, "domainToUnicode", DomainToUnicode);
SetMethodNoSideEffect(context, target, "canParse", CanParse);
SetMethodNoSideEffect(context, target, "format", Format);
SetMethod(context, target, "parse", Parse);
SetMethod(context, target, "update", Update);
} }
} // namespace
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(Parse);
registry->Register(CanParse);
registry->Register(UpdateUrl);
registry->Register(FormatUrl);
void BindingData::RegisterExternalReferences(
ExternalReferenceRegistry* registry) {
registry->Register(DomainToASCII); registry->Register(DomainToASCII);
registry->Register(DomainToUnicode); registry->Register(DomainToUnicode);
registry->Register(CanParse);
registry->Register(Format);
registry->Register(Parse);
registry->Register(Update);
} }
std::string FromFilePath(const std::string_view file_path) { std::string FromFilePath(const std::string_view file_path) {
@ -338,7 +347,9 @@ std::string FromFilePath(const std::string_view file_path) {
} }
} // namespace url } // namespace url
} // namespace node } // namespace node
NODE_BINDING_CONTEXT_AWARE_INTERNAL(url, node::url::Initialize) NODE_BINDING_CONTEXT_AWARE_INTERNAL(url, node::url::BindingData::Initialize)
NODE_BINDING_EXTERNAL_REFERENCE(url, node::url::RegisterExternalReferences) NODE_BINDING_EXTERNAL_REFERENCE(
url, node::url::BindingData::RegisterExternalReferences)

View file

@ -3,18 +3,74 @@
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#include <cinttypes>
#include "ada.h" #include "ada.h"
#include "aliased_buffer.h"
#include "node.h" #include "node.h"
#include "node_snapshotable.h"
#include "util.h" #include "util.h"
#include <string> #include <string>
namespace node { namespace node {
class ExternalReferenceRegistry;
namespace url { namespace url {
enum url_update_action {
kProtocol = 0,
kHost = 1,
kHostname = 2,
kPort = 3,
kUsername = 4,
kPassword = 5,
kPathname = 6,
kSearch = 7,
kHash = 8,
kHref = 9,
};
class BindingData : public SnapshotableObject {
public:
explicit BindingData(Realm* realm, v8::Local<v8::Object> obj);
using InternalFieldInfo = InternalFieldInfoBase;
SERIALIZABLE_OBJECT_METHODS()
static constexpr FastStringKey type_name{"node::url::BindingData"};
static constexpr EmbedderObjectType type_int =
EmbedderObjectType::k_url_binding_data;
void MemoryInfo(MemoryTracker* tracker) const override;
SET_SELF_SIZE(BindingData)
SET_MEMORY_INFO_NAME(BindingData)
static void DomainToASCII(const v8::FunctionCallbackInfo<v8::Value>& args);
static void DomainToUnicode(const v8::FunctionCallbackInfo<v8::Value>& args);
static void CanParse(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Format(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Parse(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Update(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Initialize(v8::Local<v8::Object> target,
v8::Local<v8::Value> unused,
v8::Local<v8::Context> context,
void* priv);
static void RegisterExternalReferences(ExternalReferenceRegistry* registry);
private:
static constexpr size_t kURLComponentsLength = 9;
AliasedUint32Array url_components_buffer_;
void UpdateComponents(const ada::url_components& components,
const ada::scheme::type type);
};
std::string FromFilePath(const std::string_view file_path); std::string FromFilePath(const std::string_view file_path);
} // namespace url } // namespace url
} // namespace node } // namespace node
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

View file

@ -47,15 +47,18 @@ assert.strictEqual(
hash: '#hash', hash: '#hash',
[Symbol(context)]: URLContext { [Symbol(context)]: URLContext {
href: 'https://username:password@host.name:8080/path/name/?que=ry#hash', href: 'https://username:password@host.name:8080/path/name/?que=ry#hash',
origin: 'https://host.name:8080', protocol_end: 6,
protocol: 'https:', username_end: 16,
hostname: 'host.name', host_start: 25,
pathname: '/path/name/', host_end: 35,
search: '?que=ry', pathname_start: 40,
username: 'username', search_start: 51,
password: 'password', hash_start: 58,
port: '8080', port: 8080,
hash: '#hash' scheme_type: 2,
[hasPort]: [Getter],
[hasSearch]: [Getter],
[hasHash]: [Getter]
} }
}`); }`);

View file

@ -15,6 +15,7 @@
"./typings/internalBinding/symbols.d.ts", "./typings/internalBinding/symbols.d.ts",
"./typings/internalBinding/timers.d.ts", "./typings/internalBinding/timers.d.ts",
"./typings/internalBinding/types.d.ts", "./typings/internalBinding/types.d.ts",
"./typings/internalBinding/url.d.ts",
"./typings/internalBinding/util.d.ts", "./typings/internalBinding/util.d.ts",
"./typings/internalBinding/worker.d.ts", "./typings/internalBinding/worker.d.ts",
"./typings/globals.d.ts", "./typings/globals.d.ts",

12
typings/internalBinding/url.d.ts vendored Normal file
View file

@ -0,0 +1,12 @@
import type { urlUpdateActions } from 'internal/url'
declare function InternalBinding(binding: 'url'): {
urlComponents: Uint32Array;
domainToASCII(input: string): string;
domainToUnicode(input: string): string;
canParse(input: string, base?: string): boolean;
format(input: string, fragment?: boolean, unicode?: boolean, search?: boolean, auth?: boolean): string;
parse(input: string, base?: string): string | false;
update(input: string, actionType: typeof urlUpdateActions, value: string): string | false;
};