Skip to content
File

Blob: src/node/vm.ts

typescript358 lines
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 
26import {
27 ERR_INVALID_ARG_TYPE,
28 ERR_METHOD_NOT_IMPLEMENTED,
29} from 'node-internal:internal_errors';
30 
31import {
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 
45import { Buffer } from 'node-internal:internal_buffer';
46 
47import type {
48 CreateContextOptions,
49 RunningScriptOptions,
50 RunningScriptInNewContextOptions,
51 ScriptOptions,
52 CompileFunctionOptions,
53 MeasureMemoryOptions,
54 MemoryMeasurement,
55} from 'node:vm';
56 
57export 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};
64Object.freeze(constants);
65 
66const kContextSymbol = Symbol('vm_context_symbol');
67 
68export function isContext(object: unknown): boolean {
69 validateObject(object, 'object', kValidateObjectAllowArray);
70 return kContextSymbol in object && object[kContextSymbol] === true;
71}
72 
73export 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 
145function 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 
155function 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 
186export 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 
233export function createScript(
234 code: string,
235 options: ScriptOptions = {}
236): Script {
237 return new Script(code, options);
238}
239 
240export 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 
254export 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 
268export 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 
278export 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 
334export 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 
344export 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};