From 7a22df9b7641274b2a83ce53845215d17cfda2c8 Mon Sep 17 00:00:00 2001 From: Nayeem Rahman Date: Wed, 13 Oct 2021 18:04:44 +0100 Subject: fix(runtime/ops/worker_host): move permission arg parsing to Rust (#12297) --- runtime/js/10_permissions.js | 253 +++++++++++++++++++++++++++++++++++++++++++ runtime/js/11_workers.js | 144 ++++-------------------- runtime/js/40_permissions.js | 221 ------------------------------------- runtime/js/40_testing.js | 6 +- 4 files changed, 276 insertions(+), 348 deletions(-) create mode 100644 runtime/js/10_permissions.js delete mode 100644 runtime/js/40_permissions.js (limited to 'runtime/js') diff --git a/runtime/js/10_permissions.js b/runtime/js/10_permissions.js new file mode 100644 index 000000000..a6884aab9 --- /dev/null +++ b/runtime/js/10_permissions.js @@ -0,0 +1,253 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +"use strict"; + +((window) => { + const { + Event, + EventTarget, + Deno: { core }, + __bootstrap: { webUtil: { illegalConstructorKey } }, + } = window; + const { pathFromURL } = window.__bootstrap.util; + const { + ArrayIsArray, + ArrayPrototypeIncludes, + ArrayPrototypeMap, + ArrayPrototypeSlice, + Map, + MapPrototypeGet, + MapPrototypeHas, + MapPrototypeSet, + FunctionPrototypeCall, + PromiseResolve, + PromiseReject, + SymbolFor, + TypeError, + } = window.__bootstrap.primordials; + + /** + * @typedef StatusCacheValue + * @property {PermissionState} state + * @property {PermissionStatus} status + */ + + /** @type {ReadonlyArray<"read" | "write" | "net" | "env" | "run" | "ffi" | "hrtime">} */ + const permissionNames = [ + "read", + "write", + "net", + "env", + "run", + "ffi", + "hrtime", + ]; + + /** + * @param {Deno.PermissionDescriptor} desc + * @returns {Deno.PermissionState} + */ + function opQuery(desc) { + return core.opSync("op_query_permission", desc); + } + + /** + * @param {Deno.PermissionDescriptor} desc + * @returns {Deno.PermissionState} + */ + function opRevoke(desc) { + return core.opSync("op_revoke_permission", desc); + } + + /** + * @param {Deno.PermissionDescriptor} desc + * @returns {Deno.PermissionState} + */ + function opRequest(desc) { + return core.opSync("op_request_permission", desc); + } + + class PermissionStatus extends EventTarget { + /** @type {{ state: Deno.PermissionState }} */ + #state; + + /** @type {((this: PermissionStatus, event: Event) => any) | null} */ + onchange = null; + + /** @returns {Deno.PermissionState} */ + get state() { + return this.#state.state; + } + + /** + * @param {{ state: Deno.PermissionState }} state + * @param {unknown} key + */ + constructor(state = null, key = null) { + if (key != illegalConstructorKey) { + throw new TypeError("Illegal constructor."); + } + super(); + this.#state = state; + } + + /** + * @param {Event} event + * @returns {boolean} + */ + dispatchEvent(event) { + let dispatched = super.dispatchEvent(event); + if (dispatched && this.onchange) { + FunctionPrototypeCall(this.onchange, this, event); + dispatched = !event.defaultPrevented; + } + return dispatched; + } + + [SymbolFor("Deno.privateCustomInspect")](inspect) { + return `${this.constructor.name} ${ + inspect({ state: this.state, onchange: this.onchange }) + }`; + } + } + + /** @type {Map} */ + const statusCache = new Map(); + + /** + * @param {Deno.PermissionDescriptor} desc + * @param {Deno.PermissionState} state + * @returns {PermissionStatus} + */ + function cache(desc, state) { + let { name: key } = desc; + if ((desc.name === "read" || desc.name === "write") && "path" in desc) { + key += `-${desc.path}`; + } else if (desc.name === "net" && desc.host) { + key += `-${desc.host}`; + } + if (MapPrototypeHas(statusCache, key)) { + const status = MapPrototypeGet(statusCache, key); + if (status.state !== state) { + status.state = state; + status.status.dispatchEvent(new Event("change", { cancelable: false })); + } + return status.status; + } + /** @type {{ state: Deno.PermissionState; status?: PermissionStatus }} */ + const status = { state }; + status.status = new PermissionStatus(status, illegalConstructorKey); + MapPrototypeSet(statusCache, key, status); + return status.status; + } + + /** + * @param {unknown} desc + * @returns {desc is Deno.PermissionDescriptor} + */ + function isValidDescriptor(desc) { + return desc && desc !== null && + ArrayPrototypeIncludes(permissionNames, desc.name); + } + + class Permissions { + constructor(key = null) { + if (key != illegalConstructorKey) { + throw new TypeError("Illegal constructor."); + } + } + + query(desc) { + if (!isValidDescriptor(desc)) { + return PromiseReject( + new TypeError( + `The provided value "${desc.name}" is not a valid permission name.`, + ), + ); + } + + if ( + desc.name === "read" || desc.name === "write" || desc.name === "ffi" + ) { + desc.path = pathFromURL(desc.path); + } else if (desc.name === "run") { + desc.command = pathFromURL(desc.command); + } + + const state = opQuery(desc); + return PromiseResolve(cache(desc, state)); + } + + revoke(desc) { + if (!isValidDescriptor(desc)) { + return PromiseReject( + new TypeError( + `The provided value "${desc.name}" is not a valid permission name.`, + ), + ); + } + + if (desc.name === "read" || desc.name === "write") { + desc.path = pathFromURL(desc.path); + } else if (desc.name === "run") { + desc.command = pathFromURL(desc.command); + } + + const state = opRevoke(desc); + return PromiseResolve(cache(desc, state)); + } + + request(desc) { + if (!isValidDescriptor(desc)) { + return PromiseReject( + new TypeError( + `The provided value "${desc.name}" is not a valid permission name.`, + ), + ); + } + + if (desc.name === "read" || desc.name === "write") { + desc.path = pathFromURL(desc.path); + } else if (desc.name === "run") { + desc.command = pathFromURL(desc.command); + } + + const state = opRequest(desc); + return PromiseResolve(cache(desc, state)); + } + } + + const permissions = new Permissions(illegalConstructorKey); + + /** Converts all file URLs in FS allowlists to paths. */ + function serializePermissions(permissions) { + if (typeof permissions == "object" && permissions != null) { + const serializedPermissions = {}; + for (const key of ["read", "write", "run", "ffi"]) { + if (ArrayIsArray(permissions[key])) { + serializedPermissions[key] = ArrayPrototypeMap( + permissions[key], + (path) => pathFromURL(path), + ); + } else { + serializedPermissions[key] = permissions[key]; + } + } + for (const key of ["env", "hrtime", "net"]) { + if (ArrayIsArray(permissions[key])) { + serializedPermissions[key] = ArrayPrototypeSlice(permissions[key]); + } else { + serializedPermissions[key] = permissions[key]; + } + } + return serializedPermissions; + } + return permissions; + } + + window.__bootstrap.permissions = { + serializePermissions, + permissions, + Permissions, + PermissionStatus, + }; +})(this); diff --git a/runtime/js/11_workers.js b/runtime/js/11_workers.js index 53b256334..8f0095056 100644 --- a/runtime/js/11_workers.js +++ b/runtime/js/11_workers.js @@ -4,8 +4,6 @@ ((window) => { const core = window.Deno.core; const { - ArrayIsArray, - ArrayPrototypeMap, Error, StringPrototypeStartsWith, String, @@ -15,7 +13,8 @@ const webidl = window.__bootstrap.webidl; const { URL } = window.__bootstrap.url; const { getLocationHref } = window.__bootstrap.location; - const { log, pathFromURL } = window.__bootstrap.util; + const { serializePermissions } = window.__bootstrap.permissions; + const { log } = window.__bootstrap.util; const { defineEventHandler } = window.__bootstrap.event; const { deserializeJsMessageData, serializeJsMessageData } = window.__bootstrap.messagePort; @@ -32,7 +31,7 @@ return core.opSync("op_create_worker", { hasSourceCode, name, - permissions, + permissions: serializePermissions(permissions), sourceCode, specifier, useDenoNamespace, @@ -56,87 +55,6 @@ return core.opAsync("op_host_recv_message", id); } - /** - * @param {"inherit" | boolean} value - * @param {string} permission - * @return {boolean} - */ - function parseUnitPermission( - value, - permission, - ) { - if (value !== "inherit" && typeof value !== "boolean") { - throw new Error( - `Expected 'boolean' for ${permission} permission, ${typeof value} received`, - ); - } - return value === "inherit" ? undefined : value; - } - - /** - * @param {string} permission - * @return {(boolean | string[])} - */ - function parseArrayPermission( - value, - permission, - ) { - if (typeof value === "string") { - if (value !== "inherit") { - throw new Error( - `Expected 'array' or 'boolean' for ${permission} permission, "${value}" received`, - ); - } - } else if (!ArrayIsArray(value) && typeof value !== "boolean") { - throw new Error( - `Expected 'array' or 'boolean' for ${permission} permission, ${typeof value} received`, - ); - //Casts URLs to absolute routes - } else if (ArrayIsArray(value)) { - value = ArrayPrototypeMap(value, (route) => { - if (route instanceof URL) { - if (permission === "net") { - throw new Error( - `Expected 'string' for net permission, received 'URL'`, - ); - } else if (permission === "env") { - throw new Error( - `Expected 'string' for env permission, received 'URL'`, - ); - } else { - route = pathFromURL(route); - } - } - return route; - }); - } - - return value === "inherit" ? undefined : value; - } - - /** - * Normalizes data, runs checks on parameters and deletes inherited permissions - */ - function parsePermissions({ - env = "inherit", - hrtime = "inherit", - net = "inherit", - ffi = "inherit", - read = "inherit", - run = "inherit", - write = "inherit", - }) { - return { - env: parseArrayPermission(env, "env"), - hrtime: parseUnitPermission(hrtime, "hrtime"), - net: parseArrayPermission(net, "net"), - ffi: parseUnitPermission(ffi, "ffi"), - read: parseArrayPermission(read, "read"), - run: parseUnitPermission(run, "run"), - write: parseArrayPermission(write, "write"), - }; - } - class Worker extends EventTarget { #id = 0; #name = ""; @@ -152,43 +70,23 @@ super(); specifier = String(specifier); const { - deno = {}, - name = "unknown", + deno, + name, type = "classic", } = options; - // TODO(Soremwar) - // `deno: boolean` is kept for backwards compatibility with the previous - // worker options implementation. Remove for 2.0 - let workerDenoAttributes; - if (typeof deno == "boolean") { - workerDenoAttributes = { - // Change this to enable the Deno namespace by default - namespace: deno, - permissions: null, - }; + let namespace; + let permissions; + if (typeof deno == "object") { + namespace = deno.namespace ?? false; + permissions = deno.permissions ?? undefined; } else { - workerDenoAttributes = { - // Change this to enable the Deno namespace by default - namespace: !!(deno?.namespace ?? false), - permissions: (deno?.permissions ?? "inherit") === "inherit" - ? null - : deno?.permissions, - }; - - // If the permission option is set to "none", all permissions - // must be removed from the worker - if (workerDenoAttributes.permissions === "none") { - workerDenoAttributes.permissions = { - env: false, - hrtime: false, - net: false, - ffi: false, - read: false, - run: false, - write: false, - }; - } + // Assume `deno: boolean | undefined`. + // TODO(Soremwar) + // `deno: boolean` is kept for backwards compatibility with the previous + // worker options implementation. Remove for 2.0 + namespace = !!deno; + permissions = undefined; } const workerType = webidl.converters["WorkerType"](type); @@ -218,17 +116,16 @@ specifier, hasSourceCode, sourceCode, - workerDenoAttributes.namespace, - workerDenoAttributes.permissions === null - ? null - : parsePermissions(workerDenoAttributes.permissions), - options?.name, + namespace, + permissions, + name, workerType, ); this.#id = id; this.#pollControl(); this.#pollMessages(); } + #handleError(e) { const event = new ErrorEvent("error", { cancelable: true, @@ -359,7 +256,6 @@ ]); window.__bootstrap.worker = { - parsePermissions, Worker, }; })(this); diff --git a/runtime/js/40_permissions.js b/runtime/js/40_permissions.js deleted file mode 100644 index 147ae92f0..000000000 --- a/runtime/js/40_permissions.js +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -"use strict"; - -((window) => { - const { - Event, - EventTarget, - Deno: { core }, - __bootstrap: { webUtil: { illegalConstructorKey } }, - } = window; - const { pathFromURL } = window.__bootstrap.util; - const { - ArrayPrototypeIncludes, - Map, - MapPrototypeGet, - MapPrototypeHas, - MapPrototypeSet, - FunctionPrototypeCall, - PromiseResolve, - PromiseReject, - SymbolFor, - TypeError, - } = window.__bootstrap.primordials; - - /** - * @typedef StatusCacheValue - * @property {PermissionState} state - * @property {PermissionStatus} status - */ - - /** @type {ReadonlyArray<"read" | "write" | "net" | "env" | "run" | "ffi" | "hrtime">} */ - const permissionNames = [ - "read", - "write", - "net", - "env", - "run", - "ffi", - "hrtime", - ]; - - /** - * @param {Deno.PermissionDescriptor} desc - * @returns {Deno.PermissionState} - */ - function opQuery(desc) { - return core.opSync("op_query_permission", desc); - } - - /** - * @param {Deno.PermissionDescriptor} desc - * @returns {Deno.PermissionState} - */ - function opRevoke(desc) { - return core.opSync("op_revoke_permission", desc); - } - - /** - * @param {Deno.PermissionDescriptor} desc - * @returns {Deno.PermissionState} - */ - function opRequest(desc) { - return core.opSync("op_request_permission", desc); - } - - class PermissionStatus extends EventTarget { - /** @type {{ state: Deno.PermissionState }} */ - #state; - - /** @type {((this: PermissionStatus, event: Event) => any) | null} */ - onchange = null; - - /** @returns {Deno.PermissionState} */ - get state() { - return this.#state.state; - } - - /** - * @param {{ state: Deno.PermissionState }} state - * @param {unknown} key - */ - constructor(state = null, key = null) { - if (key != illegalConstructorKey) { - throw new TypeError("Illegal constructor."); - } - super(); - this.#state = state; - } - - /** - * @param {Event} event - * @returns {boolean} - */ - dispatchEvent(event) { - let dispatched = super.dispatchEvent(event); - if (dispatched && this.onchange) { - FunctionPrototypeCall(this.onchange, this, event); - dispatched = !event.defaultPrevented; - } - return dispatched; - } - - [SymbolFor("Deno.privateCustomInspect")](inspect) { - return `${this.constructor.name} ${ - inspect({ state: this.state, onchange: this.onchange }) - }`; - } - } - - /** @type {Map} */ - const statusCache = new Map(); - - /** - * @param {Deno.PermissionDescriptor} desc - * @param {Deno.PermissionState} state - * @returns {PermissionStatus} - */ - function cache(desc, state) { - let { name: key } = desc; - if ((desc.name === "read" || desc.name === "write") && "path" in desc) { - key += `-${desc.path}`; - } else if (desc.name === "net" && desc.host) { - key += `-${desc.host}`; - } - if (MapPrototypeHas(statusCache, key)) { - const status = MapPrototypeGet(statusCache, key); - if (status.state !== state) { - status.state = state; - status.status.dispatchEvent(new Event("change", { cancelable: false })); - } - return status.status; - } - /** @type {{ state: Deno.PermissionState; status?: PermissionStatus }} */ - const status = { state }; - status.status = new PermissionStatus(status, illegalConstructorKey); - MapPrototypeSet(statusCache, key, status); - return status.status; - } - - /** - * @param {unknown} desc - * @returns {desc is Deno.PermissionDescriptor} - */ - function isValidDescriptor(desc) { - return desc && desc !== null && - ArrayPrototypeIncludes(permissionNames, desc.name); - } - - class Permissions { - constructor(key = null) { - if (key != illegalConstructorKey) { - throw new TypeError("Illegal constructor."); - } - } - - query(desc) { - if (!isValidDescriptor(desc)) { - return PromiseReject( - new TypeError( - `The provided value "${desc.name}" is not a valid permission name.`, - ), - ); - } - - if (desc.name === "read" || desc.name === "write") { - desc.path = pathFromURL(desc.path); - } else if (desc.name === "run") { - desc.command = pathFromURL(desc.command); - } - - const state = opQuery(desc); - return PromiseResolve(cache(desc, state)); - } - - revoke(desc) { - if (!isValidDescriptor(desc)) { - return PromiseReject( - new TypeError( - `The provided value "${desc.name}" is not a valid permission name.`, - ), - ); - } - - if (desc.name === "read" || desc.name === "write") { - desc.path = pathFromURL(desc.path); - } else if (desc.name === "run") { - desc.command = pathFromURL(desc.command); - } - - const state = opRevoke(desc); - return PromiseResolve(cache(desc, state)); - } - - request(desc) { - if (!isValidDescriptor(desc)) { - return PromiseReject( - new TypeError( - `The provided value "${desc.name}" is not a valid permission name.`, - ), - ); - } - - if (desc.name === "read" || desc.name === "write") { - desc.path = pathFromURL(desc.path); - } else if (desc.name === "run") { - desc.command = pathFromURL(desc.command); - } - - const state = opRequest(desc); - return PromiseResolve(cache(desc, state)); - } - } - - const permissions = new Permissions(illegalConstructorKey); - - window.__bootstrap.permissions = { - permissions, - Permissions, - PermissionStatus, - }; -})(this); diff --git a/runtime/js/40_testing.js b/runtime/js/40_testing.js index 5b404766e..b4f7c847c 100644 --- a/runtime/js/40_testing.js +++ b/runtime/js/40_testing.js @@ -3,10 +3,10 @@ ((window) => { const core = window.Deno.core; - const { parsePermissions } = window.__bootstrap.worker; const { setExitHandler } = window.__bootstrap.os; const { Console, inspectArgs } = window.__bootstrap.console; const { metrics } = core; + const { serializePermissions } = window.__bootstrap.permissions; const { assert } = window.__bootstrap.util; const { ArrayPrototypeFilter, @@ -230,7 +230,7 @@ finishing test case.`; function pledgePermissions(permissions) { return core.opSync( "op_pledge_test_permissions", - parsePermissions(permissions), + serializePermissions(permissions), ); } @@ -289,7 +289,7 @@ finishing test case.`; if (testDef.permissions) { testDef.fn = withPermissions( testDef.fn, - parsePermissions(testDef.permissions), + testDef.permissions, ); } -- cgit v1.2.3