diff options
author | snek <snek@deno.com> | 2024-08-06 14:52:53 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-08-06 12:52:53 +0000 |
commit | 897159dc6e1b2319cf2f5f09d8d6cecc0d3175fa (patch) | |
tree | cfe4a043d1fc102a4e051b99c7fcbef7b79bbb91 /ext/node/polyfills/vm.js | |
parent | c0e9512b39a4ed3713d1fd9b28469d0edf68f578 (diff) |
feat: vm rewrite (#24596)
rewrite vm implementation to increase compat.
vm.Module+importModuleDynamically callbacks should be added in a
followup.
Diffstat (limited to 'ext/node/polyfills/vm.js')
-rw-r--r-- | ext/node/polyfills/vm.js | 359 |
1 files changed, 359 insertions, 0 deletions
diff --git a/ext/node/polyfills/vm.js b/ext/node/polyfills/vm.js new file mode 100644 index 000000000..bc1a25045 --- /dev/null +++ b/ext/node/polyfills/vm.js @@ -0,0 +1,359 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// Copyright Joyent, Inc. and Node.js contributors. All rights reserved. MIT license. + +import { Buffer } from "node:buffer"; +import { notImplemented } from "ext:deno_node/_utils.ts"; +import { + op_vm_compile_function, + op_vm_create_context, + op_vm_create_script, + op_vm_is_context, + op_vm_script_create_cached_data, + op_vm_script_get_source_map_url, + op_vm_script_run_in_context, +} from "ext:core/ops"; +import { + validateArray, + validateBoolean, + validateBuffer, + validateInt32, + validateObject, + validateOneOf, + validateString, + validateStringArray, + validateUint32, +} from "ext:deno_node/internal/validators.mjs"; +import { ERR_INVALID_ARG_TYPE } from "ext:deno_node/internal/errors.ts"; + +import { primordials } from "ext:core/mod.js"; + +const { Symbol, ArrayPrototypeForEach } = primordials; + +const kParsingContext = Symbol("script parsing context"); + +export class Script { + #inner; + + constructor(code, options = {}) { + code = `${code}`; + if (typeof options === "string") { + options = { filename: options }; + } else { + validateObject(options, "options"); + } + + const { + filename = "evalmachine.<anonymous>", + lineOffset = 0, + columnOffset = 0, + cachedData, + produceCachedData = false, + // importModuleDynamically, + [kParsingContext]: parsingContext, + } = options; + + validateString(filename, "options.filename"); + validateInt32(lineOffset, "options.lineOffset"); + validateInt32(columnOffset, "options.columnOffset"); + if (cachedData !== undefined) { + validateBuffer(cachedData, "options.cachedData"); + } + validateBoolean(produceCachedData, "options.produceCachedData"); + + // const hostDefinedOptionId = + // getHostDefinedOptionId(importModuleDynamically, filename); + + const result = op_vm_create_script( + code, + filename, + lineOffset, + columnOffset, + cachedData, + produceCachedData, + parsingContext, + ); + this.#inner = result.value; + this.cachedDataProduced = result.cached_data_produced; + this.cachedDataRejected = result.cached_data_rejected; + this.cachedData = result.cached_data + ? Buffer.from(result.cached_data) + : undefined; + } + + #runInContext(contextifiedObject, options = {}) { + validateObject(options, "options"); + + let timeout = options.timeout; + if (timeout === undefined) { + timeout = -1; + } else { + validateUint32(timeout, "options.timeout", true); + } + + const { + displayErrors = true, + breakOnSigint = false, + } = options; + + validateBoolean(displayErrors, "options.displayErrors"); + validateBoolean(breakOnSigint, "options.breakOnSigint"); + + //if (breakOnSigint && process.listenerCount('SIGINT') > 0) { + // return sigintHandlersWrap(super.runInContext, this, args); + //} + + return op_vm_script_run_in_context( + this.#inner, + contextifiedObject, + timeout, + displayErrors, + breakOnSigint, + ); + } + + runInThisContext(options) { + return this.#runInContext(null, options); + } + + runInContext(contextifiedObject, options) { + validateContext(contextifiedObject); + return this.#runInContext(contextifiedObject, options); + } + + runInNewContext(contextObject, options) { + const context = createContext(contextObject, getContextOptions(options)); + return this.runInContext(context, options); + } + + get sourceMapURL() { + return op_vm_script_get_source_map_url(this.#inner); + } + + createCachedData() { + return Buffer.from(op_vm_script_create_cached_data(this.#inner)); + } +} + +function validateContext(contextifiedObject) { + if (!isContext(contextifiedObject)) { + throw new ERR_INVALID_ARG_TYPE( + "contextifiedObject", + "vm.Context", + contextifiedObject, + ); + } +} + +function getContextOptions(options) { + if (!options) { + return {}; + } + const contextOptions = { + name: options.contextName, + origin: options.contextOrigin, + codeGeneration: undefined, + microtaskMode: options.microtaskMode, + }; + if (contextOptions.name !== undefined) { + validateString(contextOptions.name, "options.contextName"); + } + if (contextOptions.origin !== undefined) { + validateString(contextOptions.origin, "options.contextOrigin"); + } + if (options.contextCodeGeneration !== undefined) { + validateObject( + options.contextCodeGeneration, + "options.contextCodeGeneration", + ); + const { strings, wasm } = options.contextCodeGeneration; + if (strings !== undefined) { + validateBoolean(strings, "options.contextCodeGeneration.strings"); + } + if (wasm !== undefined) { + validateBoolean(wasm, "options.contextCodeGeneration.wasm"); + } + contextOptions.codeGeneration = { strings, wasm }; + } + if (options.microtaskMode !== undefined) { + validateString(options.microtaskMode, "options.microtaskMode"); + } + return contextOptions; +} + +let defaultContextNameIndex = 1; +export function createContext(contextObject = {}, options = {}) { + if (isContext(contextObject)) { + return contextObject; + } + + validateObject(options, "options"); + + const { + name = `VM Context ${defaultContextNameIndex++}`, + origin, + codeGeneration, + microtaskMode, + // importModuleDynamically, + } = options; + + validateString(name, "options.name"); + if (origin !== undefined) { + validateString(origin, "options.origin"); + } + if (codeGeneration !== undefined) { + validateObject(codeGeneration, "options.codeGeneration"); + } + + let strings = true; + let wasm = true; + if (codeGeneration !== undefined) { + ({ strings = true, wasm = true } = codeGeneration); + validateBoolean(strings, "options.codeGeneration.strings"); + validateBoolean(wasm, "options.codeGeneration.wasm"); + } + + validateOneOf(microtaskMode, "options.microtaskMode", [ + "afterEvaluate", + undefined, + ]); + const microtaskQueue = microtaskMode === "afterEvaluate"; + + // const hostDefinedOptionId = + // getHostDefinedOptionId(importModuleDynamically, name); + + op_vm_create_context( + contextObject, + name, + origin, + strings, + wasm, + microtaskQueue, + ); + // Register the context scope callback after the context was initialized. + // registerImportModuleDynamically(contextObject, importModuleDynamically); + return contextObject; +} + +export function createScript(code, options) { + return new Script(code, options); +} + +export function runInContext(code, contextifiedObject, options) { + validateContext(contextifiedObject); + if (typeof options === "string") { + options = { + filename: options, + [kParsingContext]: contextifiedObject, + }; + } else { + options = { + ...options, + [kParsingContext]: contextifiedObject, + }; + } + return createScript(code, options) + .runInContext(contextifiedObject, options); +} + +export function runInNewContext(code, contextObject, options) { + if (typeof options === "string") { + options = { filename: options }; + } + contextObject = createContext(contextObject, getContextOptions(options)); + options = { ...options, [kParsingContext]: contextObject }; + return createScript(code, options).runInNewContext(contextObject, options); +} + +export function runInThisContext(code, options) { + if (typeof options === "string") { + options = { filename: options }; + } + return createScript(code, options).runInThisContext(options); +} + +export function isContext(object) { + validateObject(object, "object", { allowArray: true }); + return op_vm_is_context(object); +} + +export function compileFunction(code, params, options = {}) { + validateString(code, "code"); + if (params !== undefined) { + validateStringArray(params, "params"); + } + const { + filename = "", + columnOffset = 0, + lineOffset = 0, + cachedData = undefined, + produceCachedData = false, + parsingContext = undefined, + contextExtensions = [], + // importModuleDynamically, + } = options; + + validateString(filename, "options.filename"); + validateInt32(columnOffset, "options.columnOffset"); + validateInt32(lineOffset, "options.lineOffset"); + if (cachedData !== undefined) { + validateBuffer(cachedData, "options.cachedData"); + } + validateBoolean(produceCachedData, "options.produceCachedData"); + if (parsingContext !== undefined) { + if ( + typeof parsingContext !== "object" || + parsingContext === null || + !isContext(parsingContext) + ) { + throw new ERR_INVALID_ARG_TYPE( + "options.parsingContext", + "Context", + parsingContext, + ); + } + } + validateArray(contextExtensions, "options.contextExtensions"); + ArrayPrototypeForEach(contextExtensions, (extension, i) => { + const name = `options.contextExtensions[${i}]`; + validateObject(extension, name, { nullable: true }); + }); + + // const hostDefinedOptionId = + // getHostDefinedOptionId(importModuleDynamically, filename); + + const result = op_vm_compile_function( + code, + filename, + lineOffset, + columnOffset, + cachedData, + produceCachedData, + parsingContext, + contextExtensions, + params, + ); + + result.value.cachedDataProduced = result.cached_data_produced; + result.value.cachedDataRejected = result.cached_data_rejected; + result.value.cachedData = result.cached_data + ? Buffer.from(result.cached_data) + : undefined; + + return result.value; +} + +export function measureMemory(_options) { + notImplemented("measureMemory"); +} + +export default { + Script, + createContext, + createScript, + runInContext, + runInNewContext, + runInThisContext, + isContext, + compileFunction, + measureMemory, +}; |