File
Blob: src/node/vm.ts
| 1 | // Copyright (c) 2017-2022 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | |
| 5 | // Copyright Joyent, Inc. and other Node contributors. |
| 6 | // |
| 7 | // Permission is hereby granted, free of charge, to any person obtaining a |
| 8 | // copy of this software and associated documentation files (the |
| 9 | // "Software"), to deal in the Software without restriction, including |
| 10 | // without limitation the rights to use, copy, modify, merge, publish, |
| 11 | // distribute, sublicense, and/or sell copies of the Software, and to permit |
| 12 | // persons to whom the Software is furnished to do so, subject to the |
| 13 | // following conditions: |
| 14 | // |
| 15 | // The above copyright notice and this permission notice shall be included |
| 16 | // in all copies or substantial portions of the Software. |
| 17 | // |
| 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
| 19 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
| 20 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN |
| 21 | // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, |
| 22 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR |
| 23 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE |
| 24 | // USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 25 | |
| 26 | import { |
| 27 | ERR_INVALID_ARG_TYPE, |
| 28 | ERR_METHOD_NOT_IMPLEMENTED, |
| 29 | } from 'node-internal:internal_errors'; |
| 30 | |
| 31 | import { |
| 32 | validateArray, |
| 33 | validateBoolean, |
| 34 | validateBuffer, |
| 35 | validateInt32, |
| 36 | validateOneOf, |
| 37 | validateObject, |
| 38 | validateString, |
| 39 | validateStringArray, |
| 40 | validateUint32, |
| 41 | kValidateObjectAllowArray, |
| 42 | kValidateObjectAllowNullable, |
| 43 | } from 'node-internal:validators'; |
| 44 | |
| 45 | import { Buffer } from 'node-internal:internal_buffer'; |
| 46 | |
| 47 | import type { |
| 48 | CreateContextOptions, |
| 49 | RunningScriptOptions, |
| 50 | RunningScriptInNewContextOptions, |
| 51 | ScriptOptions, |
| 52 | CompileFunctionOptions, |
| 53 | MeasureMemoryOptions, |
| 54 | MemoryMeasurement, |
| 55 | } from 'node:vm'; |
| 56 | |
| 57 | export const constants = { |
| 58 | __proto__: null, |
| 59 | USE_MAIN_CONTEXT_DEFAULT_LOADER: Symbol( |
| 60 | 'vm_dynamic_import_main_context_default' |
| 61 | ), |
| 62 | DONT_CONTEXTIFY: Symbol('vm_context_no_contextify'), |
| 63 | }; |
| 64 | Object.freeze(constants); |
| 65 | |
| 66 | const kContextSymbol = Symbol('vm_context_symbol'); |
| 67 | |
| 68 | export function isContext(object: unknown): boolean { |
| 69 | validateObject(object, 'object', kValidateObjectAllowArray); |
| 70 | return kContextSymbol in object && object[kContextSymbol] === true; |
| 71 | } |
| 72 | |
| 73 | export class Script { |
| 74 | cachedDataRejected: undefined | boolean = undefined; |
| 75 | cachedDataProduced: undefined | boolean = undefined; |
| 76 | cachedData: undefined | Buffer = undefined; |
| 77 | sourceMapURL: undefined | string = undefined; |
| 78 | |
| 79 | constructor(_code: string, options: ScriptOptions | string = {}) { |
| 80 | _code = `${_code}`; // eslint-disable-line @typescript-eslint/no-unnecessary-template-expression |
| 81 | if (typeof options === 'string') { |
| 82 | options = { filename: options }; |
| 83 | } else { |
| 84 | validateObject(options, 'options'); |
| 85 | } |
| 86 | |
| 87 | const { |
| 88 | filename = 'evalmachine.<anonymous>', |
| 89 | lineOffset = 0, |
| 90 | columnOffset = 0, |
| 91 | cachedData, |
| 92 | produceCachedData = false, // eslint-disable-line @typescript-eslint/no-deprecated |
| 93 | importModuleDynamically = false, |
| 94 | } = options; |
| 95 | |
| 96 | validateBoolean(importModuleDynamically, 'options.importModuleDynamically'); |
| 97 | validateString(filename, 'options.filename'); |
| 98 | validateInt32(lineOffset, 'options.lineOffset'); |
| 99 | validateInt32(columnOffset, 'options.columnOffset'); |
| 100 | if (cachedData !== undefined) { |
| 101 | validateBuffer(cachedData, 'options.cachedData'); |
| 102 | } |
| 103 | validateBoolean(produceCachedData, 'options.produceCachedData'); |
| 104 | } |
| 105 | |
| 106 | createCachedData(): Buffer { |
| 107 | return Buffer.alloc(0); |
| 108 | } |
| 109 | |
| 110 | runInThisContext(options: RunningScriptOptions = {}): unknown { |
| 111 | validateObject(options, 'options'); |
| 112 | const { breakOnSigint = false, displayErrors = true, timeout } = options; |
| 113 | validateBoolean(breakOnSigint, 'options.breakOnSigint'); |
| 114 | validateBoolean(displayErrors, 'options.displayErrors'); |
| 115 | if (timeout !== undefined) { |
| 116 | validateUint32(timeout, 'options.timeout', true); |
| 117 | } |
| 118 | throw new ERR_METHOD_NOT_IMPLEMENTED('runInThisContext'); |
| 119 | } |
| 120 | |
| 121 | runInContext( |
| 122 | contextifiedObject: object, |
| 123 | options: RunningScriptOptions = {} |
| 124 | ): unknown { |
| 125 | validateContext(contextifiedObject); |
| 126 | validateObject(options, 'options'); |
| 127 | const { breakOnSigint = false, displayErrors = true, timeout } = options; |
| 128 | validateBoolean(breakOnSigint, 'options.breakOnSigint'); |
| 129 | validateBoolean(displayErrors, 'options.displayErrors'); |
| 130 | if (timeout !== undefined) { |
| 131 | validateUint32(timeout, 'options.timeout', true); |
| 132 | } |
| 133 | throw new ERR_METHOD_NOT_IMPLEMENTED('runInThisContext'); |
| 134 | } |
| 135 | |
| 136 | runInNewContext( |
| 137 | contextObject: object | symbol, |
| 138 | options: RunningScriptInNewContextOptions = {} |
| 139 | ): unknown { |
| 140 | const context = createContext(contextObject, getContextOptions(options)); |
| 141 | return this.runInContext(context, options); |
| 142 | } |
| 143 | } |
| 144 | |
| 145 | function validateContext(contextifiedObject: object): void { |
| 146 | if (!isContext(contextifiedObject)) { |
| 147 | throw new ERR_INVALID_ARG_TYPE( |
| 148 | 'contextifiedObject', |
| 149 | 'vm.Context', |
| 150 | contextifiedObject |
| 151 | ); |
| 152 | } |
| 153 | } |
| 154 | |
| 155 | function getContextOptions( |
| 156 | options?: RunningScriptInNewContextOptions |
| 157 | ): CreateContextOptions { |
| 158 | if (!options) return {}; |
| 159 | const contextOptions: CreateContextOptions = { |
| 160 | name: options.contextName, |
| 161 | origin: options.contextOrigin, |
| 162 | codeGeneration: undefined, |
| 163 | microtaskMode: options.microtaskMode, |
| 164 | }; |
| 165 | if (contextOptions.name !== undefined) |
| 166 | validateString(contextOptions.name, 'options.contextName'); |
| 167 | if (contextOptions.origin !== undefined) |
| 168 | validateString(contextOptions.origin, 'options.contextOrigin'); |
| 169 | if (options.contextCodeGeneration !== undefined) { |
| 170 | validateObject( |
| 171 | options.contextCodeGeneration, |
| 172 | 'options.contextCodeGeneration' |
| 173 | ); |
| 174 | const { strings, wasm } = options.contextCodeGeneration; |
| 175 | if (strings !== undefined) |
| 176 | validateBoolean(strings, 'options.contextCodeGeneration.strings'); |
| 177 | if (wasm !== undefined) |
| 178 | validateBoolean(wasm, 'options.contextCodeGeneration.wasm'); |
| 179 | contextOptions.codeGeneration = { strings, wasm }; |
| 180 | } |
| 181 | if (options.microtaskMode !== undefined) |
| 182 | validateString(options.microtaskMode, 'options.microtaskMode'); |
| 183 | return contextOptions; |
| 184 | } |
| 185 | |
| 186 | export function createContext( |
| 187 | contextObject = {}, |
| 188 | options: CreateContextOptions = {} |
| 189 | ): object { |
| 190 | if (contextObject !== constants.DONT_CONTEXTIFY && isContext(contextObject)) { |
| 191 | return contextObject; |
| 192 | } |
| 193 | |
| 194 | validateObject(options, 'options'); |
| 195 | |
| 196 | const { |
| 197 | name = `VM Context`, |
| 198 | origin, |
| 199 | codeGeneration, |
| 200 | microtaskMode, |
| 201 | importModuleDynamically = false, |
| 202 | } = options; |
| 203 | |
| 204 | validateString(name, 'options.name'); |
| 205 | if (origin !== undefined) validateString(origin, 'options.origin'); |
| 206 | if (codeGeneration !== undefined) |
| 207 | validateObject(codeGeneration, 'options.codeGeneration'); |
| 208 | validateBoolean(importModuleDynamically, 'options.importModuleDynamically'); |
| 209 | |
| 210 | let strings = true; |
| 211 | let wasm = true; |
| 212 | if (codeGeneration !== undefined) { |
| 213 | ({ strings = true, wasm = true } = codeGeneration as { |
| 214 | strings?: boolean | undefined; |
| 215 | wasm?: boolean | undefined; |
| 216 | }); |
| 217 | validateBoolean(strings, 'options.codeGeneration.strings'); |
| 218 | validateBoolean(wasm, 'options.codeGeneration.wasm'); |
| 219 | } |
| 220 | |
| 221 | validateOneOf(microtaskMode, 'options.microtaskMode', [ |
| 222 | 'afterEvaluate', |
| 223 | undefined, |
| 224 | ]); |
| 225 | |
| 226 | return Object.create(null, { |
| 227 | [kContextSymbol]: { |
| 228 | value: true, |
| 229 | }, |
| 230 | }) as object; |
| 231 | } |
| 232 | |
| 233 | export function createScript( |
| 234 | code: string, |
| 235 | options: ScriptOptions = {} |
| 236 | ): Script { |
| 237 | return new Script(code, options); |
| 238 | } |
| 239 | |
| 240 | export function runInContext( |
| 241 | code: string, |
| 242 | contextifiedObject: object, |
| 243 | options: RunningScriptOptions = {} |
| 244 | ): unknown { |
| 245 | validateContext(contextifiedObject); |
| 246 | if (typeof options === 'string') { |
| 247 | options = { |
| 248 | filename: options, |
| 249 | }; |
| 250 | } |
| 251 | return createScript(code, options).runInContext(contextifiedObject, options); |
| 252 | } |
| 253 | |
| 254 | export function runInNewContext( |
| 255 | code: string, |
| 256 | contextObject: object | RunningScriptInNewContextOptions | symbol, |
| 257 | options?: RunningScriptInNewContextOptions | string |
| 258 | ): unknown { |
| 259 | if (typeof options === 'string') { |
| 260 | options = { filename: options }; |
| 261 | } else { |
| 262 | options = { ...options }; |
| 263 | } |
| 264 | contextObject = createContext(contextObject, getContextOptions(options)); |
| 265 | return createScript(code, options).runInNewContext(contextObject, options); |
| 266 | } |
| 267 | |
| 268 | export function runInThisContext( |
| 269 | code: string, |
| 270 | options: RunningScriptOptions | string = {} |
| 271 | ): unknown { |
| 272 | if (typeof options === 'string') { |
| 273 | options = { filename: options }; |
| 274 | } |
| 275 | return createScript(code, options).runInThisContext(options); |
| 276 | } |
| 277 | |
| 278 | export function compileFunction( |
| 279 | code: string, |
| 280 | params: readonly string[] = [], |
| 281 | options: CompileFunctionOptions = {} |
| 282 | // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type |
| 283 | ): Function & { |
| 284 | cachedData?: Script['cachedData'] | undefined; |
| 285 | cachedDataProduced?: Script['cachedDataProduced'] | undefined; |
| 286 | cachedDataRejected?: Script['cachedDataRejected'] | undefined; |
| 287 | } { |
| 288 | validateString(code, 'code'); |
| 289 | validateObject(options, 'options'); |
| 290 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition |
| 291 | if (params !== undefined) { |
| 292 | validateStringArray(params, 'params'); |
| 293 | } |
| 294 | const { |
| 295 | filename = '', |
| 296 | columnOffset = 0, |
| 297 | lineOffset = 0, |
| 298 | cachedData, |
| 299 | produceCachedData = false, // eslint-disable-line @typescript-eslint/no-deprecated |
| 300 | parsingContext, |
| 301 | contextExtensions = [], |
| 302 | importModuleDynamically = false, |
| 303 | } = options; |
| 304 | |
| 305 | validateString(filename, 'options.filename'); |
| 306 | validateInt32(columnOffset, 'options.columnOffset'); |
| 307 | validateInt32(lineOffset, 'options.lineOffset'); |
| 308 | if (cachedData !== undefined) |
| 309 | validateBuffer(cachedData, 'options.cachedData'); |
| 310 | validateBoolean(produceCachedData, 'options.produceCachedData'); |
| 311 | validateBoolean(importModuleDynamically, 'options.importModuleDynamically'); |
| 312 | if (parsingContext !== undefined) { |
| 313 | if ( |
| 314 | typeof parsingContext !== 'object' || |
| 315 | parsingContext === null || // eslint-disable-line @typescript-eslint/no-unnecessary-condition |
| 316 | !isContext(parsingContext) |
| 317 | ) { |
| 318 | throw new ERR_INVALID_ARG_TYPE( |
| 319 | 'options.parsingContext', |
| 320 | 'Context', |
| 321 | parsingContext |
| 322 | ); |
| 323 | } |
| 324 | } |
| 325 | validateArray(contextExtensions, 'options.contextExtensions'); |
| 326 | contextExtensions.forEach((extension, i) => { |
| 327 | const name = `options.contextExtensions[${i}]`; |
| 328 | validateObject(extension, name, kValidateObjectAllowNullable); |
| 329 | }); |
| 330 | |
| 331 | throw new ERR_METHOD_NOT_IMPLEMENTED('compileFunction'); |
| 332 | } |
| 333 | |
| 334 | export function measureMemory( |
| 335 | options: MeasureMemoryOptions = {} |
| 336 | ): Promise<MemoryMeasurement> { |
| 337 | validateObject(options, 'options'); |
| 338 | const { mode = 'summary', execution = 'default' } = options; |
| 339 | validateOneOf(mode, 'options.mode', ['summary', 'detailed']); |
| 340 | validateOneOf(execution, 'options.execution', ['default', 'eager']); |
| 341 | throw new ERR_METHOD_NOT_IMPLEMENTED('measureMemory'); |
| 342 | } |
| 343 | |
| 344 | export default { |
| 345 | Script, |
| 346 | createContext, |
| 347 | createScript, |
| 348 | runInContext, |
| 349 | runInNewContext, |
| 350 | runInThisContext, |
| 351 | isContext, |
| 352 | compileFunction, |
| 353 | measureMemory, |
| 354 | constants, |
| 355 | // TODO(later): They are still experimental, but eventually we should have non-op |
| 356 | // exports for vm.Module, vm.SourceTextModule, and vm.SyntheticModule |
| 357 | }; |