Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions NativeScript/runtime/NsBuiltinModules.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
#include "Runtime.h"
#include "StructuredSerialization.h"
#include "TextEncoding.h"
#include "URLImpl.h"
#include "URLSearchParamsImpl.h"
#include "Worker.h"

using namespace v8;
Expand All @@ -25,6 +27,7 @@ constexpr const char* kNodePrefix = "node:";
// Defined below, each next to the natives it gathers.
MaybeLocal<Object> NsModuleBinding(Local<Context> context);
MaybeLocal<Object> NsRuntimeBinding(Local<Context> context);
MaybeLocal<Object> NsUrlBinding(Local<Context> context);
MaybeLocal<Object> NsUtilBinding(Local<Context> context);
MaybeLocal<Object> NsWorkerThreadsBinding(Local<Context> context);

Expand All @@ -47,6 +50,7 @@ struct Registration {
constexpr Registration kRegistry[] = {
{"ns:module", BuiltinId::kNsModule, NsModuleBinding},
{"ns:runtime", BuiltinId::kNsRuntime, NsRuntimeBinding},
{"ns:url", BuiltinId::kNsUrl, NsUrlBinding},
{"ns:util", BuiltinId::kNsUtil, NsUtilBinding},
{"ns:worker_threads", BuiltinId::kNsWorkerThreads, NsWorkerThreadsBinding},
{"node:module", BuiltinId::kNodeModule, nullptr},
Expand Down Expand Up @@ -200,6 +204,27 @@ MaybeLocal<Object> NsRuntimeBinding(Local<Context> context) {
return binding;
}

// URL and URLSearchParams for ns:url: the context's own, from the templates
// their Init installed, rather than whatever the globals name by the time the
// module is first required.
MaybeLocal<Object> NsUrlBinding(Local<Context> context) {
Isolate* isolate = v8::Isolate::GetCurrent();
Local<Object> binding = Object::New(isolate);
Local<v8::Function> url;
Local<v8::Function> searchParams;
if (!URLImpl::Constructor(context).ToLocal(&url) ||
!URLSearchParamsImpl::Constructor(context).ToLocal(&searchParams) ||
!binding->Set(context, tns::ToV8String(isolate, "URL"), url)
.FromMaybe(false) ||
!binding
->Set(context, tns::ToV8String(isolate, "URLSearchParams"),
searchParams)
.FromMaybe(false)) {
return MaybeLocal<Object>();
}
return binding;
}

// The Worker constructor for ns:worker_threads: the context's own, from the
// template Worker::Init installed, rather than whatever `globalThis.Worker`
// names by the time the module is first required.
Expand Down
30 changes: 30 additions & 0 deletions NativeScript/runtime/URLImpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,24 @@

#include "URLImpl.h"

#include "Caches.h"
#include "Helpers.h"
#include "ModuleBinding.hpp"

using namespace tns;
using namespace ada;

namespace {

// The constructor template Init put on the global template. A template's
// function is cached per context, so instantiating it again yields the very
// function the global was created with.
struct URLConstructorState {
v8::Global<v8::FunctionTemplate> constructor;
};

} // namespace

URLImpl::URLImpl(url_aggregator url) : url_(url) {}

void URLImpl::Init(v8::Isolate* isolate,
Expand All @@ -18,6 +30,20 @@ void URLImpl::Init(v8::Isolate* isolate,

v8::Local<v8::String> urlPropertyName = ToV8String(isolate, "URL");
globalTemplate->Set(urlPropertyName, URLTemplate);

if (auto* state = Caches::StateFor<URLConstructorState>(isolate)) {
state->constructor.Reset(isolate, URLTemplate);
}
}

v8::MaybeLocal<v8::Function> URLImpl::Constructor(
v8::Local<v8::Context> context) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
auto* state = Caches::StateFor<URLConstructorState>(isolate);
if (state == nullptr || state->constructor.IsEmpty()) {
return v8::MaybeLocal<v8::Function>();
}
return state->constructor.Get(isolate)->GetFunction(context);
}

URLImpl* URLImpl::GetPointer(v8::Local<v8::Object> object) {
Expand Down Expand Up @@ -67,6 +93,10 @@ v8::Local<v8::FunctionTemplate> URLImpl::GetCtor(v8::Isolate* isolate) {
tmpl->Set(ToV8String(isolate, "toString"),
v8::FunctionTemplate::New(isolate, &ToString));

// WHATWG defines toJSON as the href serialization, the same as toString.
tmpl->Set(ToV8String(isolate, "toJSON"),
v8::FunctionTemplate::New(isolate, &ToString));

ctorTmpl->Set(ToV8String(isolate, "canParse"),
v8::FunctionTemplate::New(isolate, &CanParse));

Expand Down
7 changes: 7 additions & 0 deletions NativeScript/runtime/URLImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ class URLImpl : public IsolateTracked {
static void Init(v8::Isolate* isolate,
v8::Local<v8::ObjectTemplate> globalTemplate);

// The URL constructor of `context`: the function Init's template produces
// for it, which is the object the global of that name was created with,
// whatever `globalThis.URL` names by now. Empty before Init has run for the
// isolate.
static v8::MaybeLocal<v8::Function> Constructor(
v8::Local<v8::Context> context);

static URLImpl* GetPointer(v8::Local<v8::Object> object);

static v8::Local<v8::FunctionTemplate> GetCtor(v8::Isolate* isolate);
Expand Down
27 changes: 27 additions & 0 deletions NativeScript/runtime/URLSearchParamsImpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,24 @@

#include "URLSearchParamsImpl.h"

#include "Caches.h"
#include "Helpers.h"
#include "ModuleBinding.hpp"

using namespace ada;

namespace tns {

namespace {

// The constructor template Init put on the global template; see
// URLConstructorState in URLImpl.cpp.
struct URLSearchParamsConstructorState {
v8::Global<v8::FunctionTemplate> constructor;
};

} // namespace

URLSearchParamsImpl::URLSearchParamsImpl(ada::url_search_params params)
: params_(params) {}

Expand All @@ -20,6 +32,21 @@ void URLSearchParamsImpl::Init(v8::Isolate* isolate,
v8::Local<v8::String> urlSearchParamsPropertyName =
ToV8String(isolate, "URLSearchParams");
globalTemplate->Set(urlSearchParamsPropertyName, URLSearchParamsTemplate);

if (auto* state =
Caches::StateFor<URLSearchParamsConstructorState>(isolate)) {
state->constructor.Reset(isolate, URLSearchParamsTemplate);
}
}

v8::MaybeLocal<v8::Function> URLSearchParamsImpl::Constructor(
v8::Local<v8::Context> context) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
auto* state = Caches::StateFor<URLSearchParamsConstructorState>(isolate);
if (state == nullptr || state->constructor.IsEmpty()) {
return v8::MaybeLocal<v8::Function>();
}
return state->constructor.Get(isolate)->GetFunction(context);
}

URLSearchParamsImpl* URLSearchParamsImpl::GetPointer(
Expand Down
4 changes: 4 additions & 0 deletions NativeScript/runtime/URLSearchParamsImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ class URLSearchParamsImpl : public IsolateTracked {
static void Init(v8::Isolate* isolate,
v8::Local<v8::ObjectTemplate> globalTemplate);

// The URLSearchParams constructor of `context`, as URLImpl::Constructor.
static v8::MaybeLocal<v8::Function> Constructor(
v8::Local<v8::Context> context);

static void Ctor(const v8::FunctionCallbackInfo<v8::Value>& args);

static void Append(const v8::FunctionCallbackInfo<v8::Value>& args);
Expand Down
10 changes: 8 additions & 2 deletions NativeScript/runtime/js/blob-url.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,15 @@ ObjectDefineProperty(URL.prototype, 'searchParams', {
writeBack(this);
};
params._delete = params.delete;
params.delete = function (name) {
params.delete = function (name, value) {
resync(this);
this._delete(name);
// An explicitly passed undefined must still reach the native
// coercion, so forward by arity rather than by value.
if (arguments.length > 1) {
this._delete(name, value);
} else {
this._delete(name);
}
writeBack(this);
};
params._set = params.set;
Expand Down
112 changes: 12 additions & 100 deletions NativeScript/runtime/js/node-url.js
Original file line number Diff line number Diff line change
@@ -1,104 +1,16 @@
"use strict";

// The `node:url` compatibility shim: the two path/URL converters
// (docs/ns-builtin-modules.md). Parsing goes through the URL intrinsic rather
// than a hand-rolled scan, so authority normalization (`file://localhost/x`
// has no host, per the URL spec), percent-decoding and path canonicalization
// all follow the spec instead of an approximation.
// The `node:url` compatibility shim over `ns:url` (docs/ns-builtin-modules.md).
// The two surfaces coincide today, so this only re-exports; any adaptation to
// Node's API belongs here, never in the standard module.

const {
decodeURIComponent,
ObjectFreeze,
StringPrototypeCharCodeAt,
StringPrototypeStartsWith,
TypeError,
URL,
} = primordials;

const INVALID_ARG =
'The "path" argument must be of type string or an instance of URL.';

function toUrl(input) {
let href;
if (typeof input === "string") {
href = input;
} else if (input !== null && typeof input === "object" &&
typeof input.href === "string") {
// Duck-typed so a URL from another realm still works.
href = input.href;
} else {
throw new TypeError(INVALID_ARG);
}

try {
return new URL(href);
} catch {
throw new TypeError(INVALID_ARG);
}
}

function fileURLToPath(input) {
const url = toUrl(input);

if (url.protocol !== "file:") {
throw new TypeError("The URL must be of scheme file");
}
// The URL parser already folded a "localhost" authority to the empty host,
// so anything left here is a real remote host and names no local file.
if (url.hostname !== "") {
throw new TypeError('File URL host must be "localhost" or empty');
}

// `pathname` carries neither the query nor the fragment.
const pathname = url.pathname;
for (let i = 0; i < pathname.length; i++) {
if (pathname[i] !== "%") {
continue;
}
// %2F would decode to a separator and silently change the path's shape.
const third = StringPrototypeCharCodeAt(pathname, i + 2) | 0x20;
if (pathname[i + 1] === "2" && third === 102 /* 'f' */) {
throw new TypeError("File URL path must not include encoded / characters");
}
}
const { ObjectFreeze } = primordials;
const { URL, URLSearchParams, fileURLToPath, pathToFileURL } =
require("ns:url");

return decodeURIComponent(pathname);
}

const kHexDigits = "0123456789ABCDEF";

// Percent-encode everything the URL parser would otherwise read as syntax (or
// reject), leaving `/` as the separator it is. Non-ASCII is left alone: the
// parser UTF-8 encodes it correctly on its own.
function encodePathChars(filepath) {
let encoded = "";
for (let i = 0; i < filepath.length; i++) {
const char = filepath[i];
const code = StringPrototypeCharCodeAt(filepath, i);
const mustEncode =
code < 0x21 || code === 0x7f || char === "%" || char === "?" ||
char === "#" || char === "\\" || char === '"' || char === "<" ||
char === ">" || char === "`" || char === "{" || char === "}";
if (mustEncode) {
encoded += "%" + kHexDigits[(code >> 4) & 0xf] + kHexDigits[code & 0xf];
} else {
encoded += char;
}
}
return encoded;
}

function pathToFileURL(filepath) {
if (typeof filepath !== "string") {
throw new TypeError('The "path" argument must be of type string.');
}
// Node resolves a relative path against the process working directory; there
// is no such thing here, so a relative path has no single correct answer.
if (!StringPrototypeStartsWith(filepath, "/")) {
throw new TypeError('The "path" argument must be an absolute path.');
}

return new URL("file://" + encodePathChars(filepath));
}

module.exports = ObjectFreeze({ fileURLToPath, pathToFileURL });
module.exports = ObjectFreeze({
URL,
URLSearchParams,
fileURLToPath,
pathToFileURL,
});
Loading
Loading